mirror of
https://github.com/HaishinKit/HaishinKit.swift.git
synced 2026-05-07 20:12:28 +00:00
improve RTCMediaStreamTrack.
This commit is contained in:
@@ -4,7 +4,7 @@ import HaishinKit
|
||||
actor HTTPSession: Session {
|
||||
var connected: Bool {
|
||||
get async {
|
||||
peerConnection.connectionState == .connected
|
||||
peerConnection?.connectionState == .connected
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,10 +18,10 @@ actor HTTPSession: Session {
|
||||
private let uri: URL
|
||||
private var location: URL?
|
||||
private var maxRetryCount: Int = 0
|
||||
private var _stream = MediaStream()
|
||||
private var _stream = RTCStream()
|
||||
private var mode: SessionMode
|
||||
private var configuration: HTTPSessionConfiguration?
|
||||
private lazy var peerConnection: RTCPeerConnection = makePeerConnection()
|
||||
private var peerConnection: RTCPeerConnection?
|
||||
|
||||
init(uri: URL, mode: SessionMode, configuration: (any SessionConfiguration)?) {
|
||||
logger.level = .debug
|
||||
@@ -41,18 +41,20 @@ actor HTTPSession: Session {
|
||||
return
|
||||
}
|
||||
_readyState.value = .connecting
|
||||
peerConnection = makePeerConnection()
|
||||
let peerConnection = try makePeerConnection()
|
||||
switch mode {
|
||||
case .publish:
|
||||
await _stream.tracks.forEach { track in
|
||||
peerConnection.addTrack(track)
|
||||
}
|
||||
let audioSettings = await _stream.audioSettings
|
||||
try peerConnection.addTrack(AudioStreamTrack(audioSettings), stream: _stream)
|
||||
let videoSettings = await _stream.videoSettings
|
||||
try peerConnection.addTrack(VideoStreamTrack(videoSettings), stream: _stream)
|
||||
case .playback:
|
||||
await _stream.setDirection(.recvonly)
|
||||
try peerConnection.addTrack(.audio, stream: _stream)
|
||||
try peerConnection.addTrack(.video, stream: _stream)
|
||||
}
|
||||
do {
|
||||
self.peerConnection = peerConnection
|
||||
try peerConnection.setLocalDesciption(.offer)
|
||||
let answer = try await requestOffer(uri, offer: peerConnection.createOffer())
|
||||
try peerConnection.setRemoteDesciption(answer, type: .answer)
|
||||
@@ -76,7 +78,7 @@ actor HTTPSession: Session {
|
||||
request.addValue("application/sdp", forHTTPHeaderField: "Content-Type")
|
||||
_ = try await URLSession.shared.data(for: request)
|
||||
await _stream.close()
|
||||
peerConnection.close()
|
||||
peerConnection?.close()
|
||||
self.location = nil
|
||||
_readyState.value = .closed
|
||||
}
|
||||
@@ -104,12 +106,8 @@ actor HTTPSession: Session {
|
||||
return String(data: data, encoding: .utf8) ?? ""
|
||||
}
|
||||
|
||||
private func makePeerConnection() -> RTCPeerConnection {
|
||||
let conneciton = if let configuration {
|
||||
RTCPeerConnection(configuration)
|
||||
} else {
|
||||
RTCPeerConnection(RTCConfiguration())
|
||||
}
|
||||
private func makePeerConnection() throws -> RTCPeerConnection {
|
||||
let conneciton = try RTCPeerConnection(configuration)
|
||||
conneciton.delegate = self
|
||||
return conneciton
|
||||
}
|
||||
@@ -127,15 +125,15 @@ extension HTTPSession: RTCPeerConnectionDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
nonisolated func peerConnection(_ peerConnection: RTCPeerConnection, signalingStateChanged signalingState: RTCPeerConnection.SignalingState) {
|
||||
}
|
||||
|
||||
nonisolated func peerConnection(_ peerConnection: RTCPeerConnection, iceConnectionStateChanged iceConnectionState: RTCPeerConnection.IceConnectionState) {
|
||||
}
|
||||
|
||||
nonisolated func peerConnection(_ peerConnection: RTCPeerConnection, iceGatheringStateChanged gatheringState: RTCPeerConnection.IceGatheringState) {
|
||||
}
|
||||
|
||||
nonisolated func peerConnection(_ peerConnection: RTCPeerConnection, addedTrack track: RTCTrack) {
|
||||
}
|
||||
|
||||
nonisolated func peerConnection(_ peerConnection: RTCPeerConnection, gotIceCandidate candidated: RTCIceCandidate) {
|
||||
}
|
||||
|
||||
|
||||
@@ -62,6 +62,8 @@ extension RTCConfigurationConvertible {
|
||||
}
|
||||
|
||||
public struct RTCConfiguration: RTCConfigurationConvertible {
|
||||
static let empty = RTCConfiguration()
|
||||
|
||||
public let iceServers: [String]
|
||||
public let bindAddress: String?
|
||||
public let certificateType: RTCCertificateType?
|
||||
|
||||
@@ -62,29 +62,35 @@ public final class RTCDataChannel: RTCChannel {
|
||||
|
||||
let id: Int32
|
||||
|
||||
init(id: Int32) {
|
||||
init(id: Int32) throws {
|
||||
self.id = id
|
||||
rtcSetUserPointer(id, Unmanaged.passUnretained(self).toOpaque())
|
||||
rtcSetOpenCallback(id) { _, pointer in
|
||||
guard let pointer else { return }
|
||||
Unmanaged<RTCDataChannel>.fromOpaque(pointer).takeUnretainedValue().readyState = .open
|
||||
}
|
||||
rtcSetClosedCallback(id) { _, pointer in
|
||||
guard let pointer else { return }
|
||||
Unmanaged<RTCDataChannel>.fromOpaque(pointer).takeUnretainedValue().readyState = .connecting
|
||||
}
|
||||
rtcSetMessageCallback(id) { _, bytes, size, pointer in
|
||||
guard let bytes, let pointer else { return }
|
||||
if 0 <= size {
|
||||
let data = Data(bytes: bytes, count: Int(size))
|
||||
Unmanaged<RTCDataChannel>.fromOpaque(pointer).takeUnretainedValue().didReceiveMessage(data)
|
||||
} else {
|
||||
Unmanaged<RTCDataChannel>.fromOpaque(pointer).takeUnretainedValue().didReceiveMessage(String(cString: bytes))
|
||||
}
|
||||
}
|
||||
rtcSetErrorCallback(id) { _, error, pointer in
|
||||
guard let error, let pointer else { return }
|
||||
Unmanaged<RTCDataChannel>.fromOpaque(pointer).takeUnretainedValue().errorOccurred(String(cString: error))
|
||||
try RTCError.check(id)
|
||||
do {
|
||||
try RTCError.check(rtcSetOpenCallback(id) { _, pointer in
|
||||
guard let pointer else { return }
|
||||
Unmanaged<RTCDataChannel>.fromOpaque(pointer).takeUnretainedValue().readyState = .open
|
||||
})
|
||||
try RTCError.check(rtcSetClosedCallback(id) { _, pointer in
|
||||
guard let pointer else { return }
|
||||
Unmanaged<RTCDataChannel>.fromOpaque(pointer).takeUnretainedValue().readyState = .connecting
|
||||
})
|
||||
try RTCError.check(rtcSetMessageCallback(id) { _, bytes, size, pointer in
|
||||
guard let bytes, let pointer else { return }
|
||||
if 0 <= size {
|
||||
let data = Data(bytes: bytes, count: Int(size))
|
||||
Unmanaged<RTCDataChannel>.fromOpaque(pointer).takeUnretainedValue().didReceiveMessage(data)
|
||||
} else {
|
||||
Unmanaged<RTCDataChannel>.fromOpaque(pointer).takeUnretainedValue().didReceiveMessage(String(cString: bytes))
|
||||
}
|
||||
})
|
||||
try RTCError.check(rtcSetErrorCallback(id) { _, error, pointer in
|
||||
guard let error, let pointer else { return }
|
||||
Unmanaged<RTCDataChannel>.fromOpaque(pointer).takeUnretainedValue().errorOccurred(String(cString: error))
|
||||
})
|
||||
rtcSetUserPointer(id, Unmanaged.passUnretained(self).toOpaque())
|
||||
} catch {
|
||||
rtcDeleteDataChannel(id)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
import libdatachannel
|
||||
|
||||
enum RTCIceState: Sendable {
|
||||
case new
|
||||
case checking
|
||||
case connected
|
||||
case completed
|
||||
case failed
|
||||
case disconnected
|
||||
case closed
|
||||
}
|
||||
|
||||
extension RTCIceState {
|
||||
init?(cValue: rtcIceState) {
|
||||
switch cValue {
|
||||
case RTC_ICE_NEW:
|
||||
self = .new
|
||||
case RTC_ICE_CHECKING:
|
||||
self = .checking
|
||||
case RTC_ICE_CONNECTED:
|
||||
self = .connected
|
||||
case RTC_ICE_COMPLETED:
|
||||
self = .completed
|
||||
case RTC_ICE_FAILED:
|
||||
self = .failed
|
||||
case RTC_ICE_DISCONNECTED:
|
||||
self = .disconnected
|
||||
case RTC_ICE_CLOSED:
|
||||
self = .closed
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ public protocol RTCPeerConnectionDelegate: AnyObject {
|
||||
func peerConnection(_ peerConnection: RTCPeerConnection, connectionStateChanged connectionState: RTCPeerConnection.ConnectionState)
|
||||
func peerConnection(_ peerConnection: RTCPeerConnection, iceGatheringStateChanged iceGatheringState: RTCPeerConnection.IceGatheringState)
|
||||
func peerConnection(_ peerConnection: RTCPeerConnection, iceConnectionStateChanged iceConnectionState: RTCPeerConnection.IceConnectionState)
|
||||
func peerConnection(_ peerConnection: RTCPeerConnection, signalingStateChanged signalingState: RTCPeerConnection.SignalingState)
|
||||
func peerConnection(_ peerConneciton: RTCPeerConnection, didOpen dataChannel: RTCDataChannel)
|
||||
func peerConnection(_ peerConnection: RTCPeerConnection, gotIceCandidate candidated: RTCIceCandidate)
|
||||
}
|
||||
@@ -26,22 +27,48 @@ public final class RTCPeerConnection {
|
||||
case closed
|
||||
}
|
||||
|
||||
/// Represents the ICE gathering state of an RTCPeerConnection.
|
||||
public enum IceGatheringState: Sendable {
|
||||
/// ICE gathering has not yet started.
|
||||
case new
|
||||
/// The agent is currently gathering ICE candidates.
|
||||
case inProgress
|
||||
/// ICE gathering has finished. No more candidates will be gathered.
|
||||
case complete
|
||||
}
|
||||
|
||||
/// Represents the state of the ICE connection for an RTCPeerConnection.
|
||||
public enum IceConnectionState: Sendable {
|
||||
/// The ICE agent is newly created and no checks have started yet.
|
||||
case new
|
||||
/// The ICE agent is checking candidate pairs to find a workable connection.
|
||||
case checking
|
||||
/// A usable ICE connection has been established.
|
||||
case connected
|
||||
/// ICE checks have completed successfully, and the connection is fully stable.
|
||||
case completed
|
||||
/// The ICE connection has failed and cannot recover.
|
||||
case failed
|
||||
/// The ICE connection has been lost or interrupted.
|
||||
case disconnected
|
||||
/// The ICE agent has been closed and will not be used again.
|
||||
case closed
|
||||
}
|
||||
|
||||
/// Represents the signaling state of an RTCPeerConnection.
|
||||
public enum SignalingState: Sendable {
|
||||
/// The signaling state is stable; there is no outstanding local or remote offer.
|
||||
case stable
|
||||
/// A local offer has been created and set as the local description.
|
||||
case haveLocalOffer
|
||||
/// A remote offer has been received and set as the remote description.
|
||||
case haveRemoteOffer
|
||||
/// A provisional (pr-answer) has been set as the local description.
|
||||
case haveLocalPRAnswer
|
||||
/// A provisional (pr-answer) has been set as the remote description.
|
||||
case haveRemotePRAnswer
|
||||
}
|
||||
|
||||
static let audioMediaDescription = """
|
||||
m=audio 9 UDP/TLS/RTP/SAVPF 111
|
||||
a=mid:0
|
||||
@@ -63,75 +90,109 @@ a=fmtp:98 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f
|
||||
|
||||
static let bufferSize: Int = 1024 * 16
|
||||
|
||||
/// Specifies the delegate of an RTCPeerConnection.
|
||||
public weak var delegate: (any RTCPeerConnectionDelegate)?
|
||||
/// The current state of connection.
|
||||
public private(set) var connectionState: ConnectionState = .new {
|
||||
didSet {
|
||||
guard connectionState != oldValue else {
|
||||
return
|
||||
}
|
||||
delegate?.peerConnection(self, connectionStateChanged: connectionState)
|
||||
}
|
||||
}
|
||||
/// The current state of ice connection.
|
||||
public private(set) var iceConnectionState: IceConnectionState = .new {
|
||||
didSet {
|
||||
guard iceConnectionState != oldValue else {
|
||||
return
|
||||
}
|
||||
delegate?.peerConnection(self, iceConnectionStateChanged: iceConnectionState)
|
||||
}
|
||||
}
|
||||
private let connection: Int32
|
||||
private(set) var tracks: [RTCTrack] = []
|
||||
private(set) var candidates: [RTCIceCandidate] = []
|
||||
private(set) var signalingState: RTCSignalingState = .stable
|
||||
private(set) var iceGatheringState: IceGatheringState = .new {
|
||||
/// The current state of ice gathering.
|
||||
public private(set) var iceGatheringState: IceGatheringState = .new {
|
||||
didSet {
|
||||
guard iceGatheringState != oldValue else {
|
||||
return
|
||||
}
|
||||
delegate?.peerConnection(self, iceGatheringStateChanged: iceGatheringState)
|
||||
}
|
||||
}
|
||||
/// The current state of signaling.
|
||||
public private(set) var signalingState: SignalingState = .stable {
|
||||
didSet {
|
||||
guard signalingState != oldValue else {
|
||||
return
|
||||
}
|
||||
delegate?.peerConnection(self, signalingStateChanged: signalingState)
|
||||
}
|
||||
}
|
||||
private let connection: Int32
|
||||
private(set) var localDescription: String = ""
|
||||
|
||||
public init(_ config: some RTCConfigurationConvertible) {
|
||||
connection = config.createPeerConnection()
|
||||
rtcSetUserPointer(connection, Unmanaged.passUnretained(self).toOpaque())
|
||||
rtcSetLocalDescriptionCallback(connection) { _, sdp, _, pointer in
|
||||
guard let pointer else { return }
|
||||
if let sdp {
|
||||
Unmanaged<RTCPeerConnection>.fromOpaque(pointer).takeUnretainedValue().localDescription = String(cString: sdp)
|
||||
}
|
||||
/// Creates a peerConnection instance.
|
||||
public init(_ config: (some RTCConfigurationConvertible)? = nil) throws {
|
||||
if let config {
|
||||
connection = config.createPeerConnection()
|
||||
} else {
|
||||
connection = RTCConfiguration.empty.createPeerConnection()
|
||||
}
|
||||
rtcSetLocalCandidateCallback(connection) { _, candidate, mid, pointer in
|
||||
guard let pointer else { return }
|
||||
Unmanaged<RTCPeerConnection>.fromOpaque(pointer).takeUnretainedValue().didGenerateCandidate(.init(
|
||||
candidate: candidate,
|
||||
mid: mid
|
||||
))
|
||||
}
|
||||
rtcSetStateChangeCallback(connection) { _, state, pointer in
|
||||
guard let pointer else { return }
|
||||
if let state = ConnectionState(cValue: state) {
|
||||
Unmanaged<RTCPeerConnection>.fromOpaque(pointer).takeUnretainedValue().connectionState = state
|
||||
}
|
||||
}
|
||||
rtcSetIceStateChangeCallback(connection) { _, state, pointer in
|
||||
guard let pointer else { return }
|
||||
if let state = IceConnectionState(cValue: state) {
|
||||
Unmanaged<RTCPeerConnection>.fromOpaque(pointer).takeUnretainedValue().iceConnectionState = state
|
||||
}
|
||||
}
|
||||
rtcSetGatheringStateChangeCallback(connection) { _, gatheringState, pointer in
|
||||
guard let pointer else { return }
|
||||
if let gatheringState = IceGatheringState(cValue: gatheringState) {
|
||||
Unmanaged<RTCPeerConnection>.fromOpaque(pointer).takeUnretainedValue().iceGatheringState = gatheringState
|
||||
}
|
||||
}
|
||||
rtcSetSignalingStateChangeCallback(connection) { _, signalingState, pointer in
|
||||
guard let pointer else { return }
|
||||
if let signalingState = RTCSignalingState(cValue: signalingState) {
|
||||
Unmanaged<RTCPeerConnection>.fromOpaque(pointer).takeUnretainedValue().signalingState = signalingState
|
||||
}
|
||||
}
|
||||
rtcSetTrackCallback(connection) { _, track, pointer in
|
||||
guard let pointer else { return }
|
||||
Unmanaged<RTCPeerConnection>.fromOpaque(pointer).takeUnretainedValue().didReceiveTrack(.init(id: track))
|
||||
}
|
||||
rtcSetDataChannelCallback(connection) { _, dataChannel, pointer in
|
||||
guard let pointer else { return }
|
||||
Unmanaged<RTCPeerConnection>.fromOpaque(pointer).takeUnretainedValue().didReceiveDataChannel(.init(id: dataChannel))
|
||||
try RTCError.check(connection)
|
||||
do {
|
||||
try RTCError.check(rtcSetLocalDescriptionCallback(connection) { _, sdp, _, pointer in
|
||||
guard let pointer else { return }
|
||||
if let sdp {
|
||||
Unmanaged<RTCPeerConnection>.fromOpaque(pointer).takeUnretainedValue().localDescription = String(cString: sdp)
|
||||
}
|
||||
})
|
||||
try RTCError.check(rtcSetLocalCandidateCallback(connection) { _, candidate, mid, pointer in
|
||||
guard let pointer else { return }
|
||||
Unmanaged<RTCPeerConnection>.fromOpaque(pointer).takeUnretainedValue().didGenerateCandidate(.init(
|
||||
candidate: candidate,
|
||||
mid: mid
|
||||
))
|
||||
})
|
||||
try RTCError.check(rtcSetStateChangeCallback(connection) { _, state, pointer in
|
||||
guard let pointer else { return }
|
||||
if let state = ConnectionState(cValue: state) {
|
||||
Unmanaged<RTCPeerConnection>.fromOpaque(pointer).takeUnretainedValue().connectionState = state
|
||||
}
|
||||
})
|
||||
try RTCError.check(rtcSetIceStateChangeCallback(connection) { _, state, pointer in
|
||||
guard let pointer else { return }
|
||||
if let state = IceConnectionState(cValue: state) {
|
||||
Unmanaged<RTCPeerConnection>.fromOpaque(pointer).takeUnretainedValue().iceConnectionState = state
|
||||
}
|
||||
})
|
||||
try RTCError.check(rtcSetGatheringStateChangeCallback(connection) { _, gatheringState, pointer in
|
||||
guard let pointer else { return }
|
||||
if let gatheringState = IceGatheringState(cValue: gatheringState) {
|
||||
Unmanaged<RTCPeerConnection>.fromOpaque(pointer).takeUnretainedValue().iceGatheringState = gatheringState
|
||||
}
|
||||
})
|
||||
try RTCError.check(rtcSetSignalingStateChangeCallback(connection) { _, signalingState, pointer in
|
||||
guard let pointer else { return }
|
||||
if let signalingState = SignalingState(cValue: signalingState) {
|
||||
Unmanaged<RTCPeerConnection>.fromOpaque(pointer).takeUnretainedValue().signalingState = signalingState
|
||||
}
|
||||
})
|
||||
try RTCError.check(rtcSetTrackCallback(connection) { _, track, pointer in
|
||||
guard let pointer else { return }
|
||||
if let track = try? RTCTrack(id: track) {
|
||||
Unmanaged<RTCPeerConnection>.fromOpaque(pointer).takeUnretainedValue().didOpenTrack(track)
|
||||
}
|
||||
})
|
||||
try RTCError.check(rtcSetDataChannelCallback(connection) { _, dataChannel, pointer in
|
||||
guard let pointer else { return }
|
||||
if let channel = try? RTCDataChannel(id: dataChannel) {
|
||||
Unmanaged<RTCPeerConnection>.fromOpaque(pointer).takeUnretainedValue().didOpenDataChannel(channel)
|
||||
}
|
||||
})
|
||||
rtcSetUserPointer(connection, Unmanaged.passUnretained(self).toOpaque())
|
||||
} catch {
|
||||
rtcDeletePeerConnection(connection)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
@@ -140,15 +201,33 @@ a=fmtp:98 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f
|
||||
rtcDeletePeerConnection(connection)
|
||||
}
|
||||
|
||||
public func addTrack(_ track: MediaStreamTrack) {
|
||||
let connection = self.connection
|
||||
Task {
|
||||
try await track.addTrack(connection, direction: .sendrecv)
|
||||
/// Adds a `MediaStreamTrack` to the peer connection and associates it with the given `MediaStream`.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - track: The media track to add (audio or video).
|
||||
/// - stream: The `MediaStream` that the track belongs to.
|
||||
public func addTrack(_ track: some RTCStreamTrack, stream: RTCStream) throws {
|
||||
let msid = stream.id
|
||||
switch track {
|
||||
case let track as AudioStreamTrack:
|
||||
let config = RTCTrackConfiguration(mid: "0", streamId: msid, audioCodecSettings: track.settings)
|
||||
let id = try config.addTrack(connection, direction: .sendrecv)
|
||||
Task {
|
||||
await stream.addTrack(try RTCSendableStreamTrack(id, id: track.id))
|
||||
}
|
||||
case let track as VideoStreamTrack:
|
||||
let config = RTCTrackConfiguration(mid: "1", streamId: msid, videoCodecSettings: track.settings)
|
||||
let id = try config.addTrack(connection, direction: .sendrecv)
|
||||
Task {
|
||||
await stream.addTrack(try RTCSendableStreamTrack(id, id: track.id))
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func addTrack(_ kind: MediaStreamKind, stream: MediaStream) throws -> RTCTrack {
|
||||
func addTrack(_ kind: RTCStreamKind, stream: RTCStream) throws -> RTCTrack {
|
||||
let sdp: String
|
||||
switch kind {
|
||||
case .audio:
|
||||
@@ -159,9 +238,8 @@ a=fmtp:98 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f
|
||||
let result = try RTCError.check(sdp.withCString { cString in
|
||||
rtcAddTrack(connection, cString)
|
||||
})
|
||||
let track = RTCTrack(id: result)
|
||||
let track = try RTCTrack(id: result)
|
||||
track.delegate = stream
|
||||
tracks.append(track)
|
||||
return track
|
||||
}
|
||||
|
||||
@@ -195,7 +273,7 @@ a=fmtp:98 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f
|
||||
let result = try RTCError.check([label].withCStrings { cStrings in
|
||||
rtcCreateDataChannel(connection, cStrings[0])
|
||||
})
|
||||
return RTCDataChannel(id: result)
|
||||
return try RTCDataChannel(id: result)
|
||||
}
|
||||
|
||||
public func close() {
|
||||
@@ -207,15 +285,14 @@ a=fmtp:98 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f
|
||||
}
|
||||
|
||||
private func didGenerateCandidate(_ candidated: RTCIceCandidate) {
|
||||
candidates.append(candidated)
|
||||
delegate?.peerConnection(self, gotIceCandidate: candidated)
|
||||
}
|
||||
|
||||
private func didReceiveTrack(_ track: RTCTrack) {
|
||||
private func didOpenTrack(_ track: RTCTrack) {
|
||||
logger.info(track)
|
||||
}
|
||||
|
||||
private func didReceiveDataChannel(_ dataChannel: RTCDataChannel) {
|
||||
private func didOpenDataChannel(_ dataChannel: RTCDataChannel) {
|
||||
delegate?.peerConnection(self, didOpen: dataChannel)
|
||||
}
|
||||
}
|
||||
@@ -278,3 +355,22 @@ extension RTCPeerConnection.IceConnectionState {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension RTCPeerConnection.SignalingState {
|
||||
init?(cValue: rtcSignalingState) {
|
||||
switch cValue {
|
||||
case RTC_SIGNALING_STABLE:
|
||||
self = .stable
|
||||
case RTC_SIGNALING_HAVE_LOCAL_OFFER:
|
||||
self = .haveLocalOffer
|
||||
case RTC_SIGNALING_HAVE_REMOTE_OFFER:
|
||||
self = .haveRemoteOffer
|
||||
case RTC_SIGNALING_HAVE_LOCAL_PRANSWER:
|
||||
self = .haveLocalPRAnswer
|
||||
case RTC_SIGNALING_HAVE_REMOTE_PRANSWER:
|
||||
self = .haveRemotePRAnswer
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import AVFoundation
|
||||
import HaishinKit
|
||||
import libdatachannel
|
||||
|
||||
actor RTCSendableStreamTrack: RTCStreamTrack {
|
||||
let id: String
|
||||
private let track: RTCTrack
|
||||
|
||||
init(_ tid: Int32, id: String) throws {
|
||||
track = try RTCTrack(id: tid)
|
||||
self.id = id
|
||||
}
|
||||
|
||||
func send(_ buffer: CMSampleBuffer) {
|
||||
track.send(buffer)
|
||||
}
|
||||
|
||||
func send(_ buffer: AVAudioCompressedBuffer, when: AVAudioTime) {
|
||||
track.send(buffer, when: when)
|
||||
}
|
||||
|
||||
func setDelegate(_ delegate: some RTCTrackDelegate) {
|
||||
track.delegate = delegate
|
||||
}
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
import libdatachannel
|
||||
|
||||
enum RTCSignalingState: Sendable {
|
||||
case stable
|
||||
case haveLocalOffer
|
||||
case haveRemoteOffer
|
||||
case haveLocalPRAnswer
|
||||
case haveRemotePRAnswer
|
||||
}
|
||||
|
||||
extension RTCSignalingState {
|
||||
init?(cValue: rtcSignalingState) {
|
||||
switch cValue {
|
||||
case RTC_SIGNALING_STABLE:
|
||||
self = .stable
|
||||
case RTC_SIGNALING_HAVE_LOCAL_OFFER:
|
||||
self = .haveLocalOffer
|
||||
case RTC_SIGNALING_HAVE_REMOTE_OFFER:
|
||||
self = .haveRemoteOffer
|
||||
case RTC_SIGNALING_HAVE_LOCAL_PRANSWER:
|
||||
self = .haveLocalPRAnswer
|
||||
case RTC_SIGNALING_HAVE_REMOTE_PRANSWER:
|
||||
self = .haveRemotePRAnswer
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
+14
-16
@@ -2,7 +2,7 @@ import AVFoundation
|
||||
import HaishinKit
|
||||
import libdatachannel
|
||||
|
||||
public actor MediaStream {
|
||||
public actor RTCStream {
|
||||
enum Error: Swift.Error {
|
||||
case unsupportedCodec
|
||||
}
|
||||
@@ -11,14 +11,8 @@ public actor MediaStream {
|
||||
static let supportedVideoCodecs: [VideoCodecSettings.Format] = [.h264]
|
||||
|
||||
let id: String = UUID().uuidString
|
||||
private var _tracks: [MediaStreamTrack] = []
|
||||
public var tracks: [MediaStreamTrack] {
|
||||
if _tracks.isEmpty {
|
||||
_tracks.append(.init(mid: "1", streamId: id, audioCodecSettings: outgoing.audioSettings))
|
||||
// _tracks.append(.init(mid: "0", streamId: id, videoCodecSettings: outgoing.videoSettings))
|
||||
}
|
||||
return _tracks
|
||||
}
|
||||
private(set) var tracks: [RTCSendableStreamTrack] = []
|
||||
public private(set) var readyState: StreamReadyState = .idle
|
||||
public private(set) var videoTrackId: UInt8? = UInt8.max
|
||||
public private(set) var audioTrackId: UInt8? = UInt8.max
|
||||
package lazy var incoming = IncomingStream(self)
|
||||
@@ -28,7 +22,6 @@ public actor MediaStream {
|
||||
return stream
|
||||
}()
|
||||
package var outputs: [any StreamOutput] = []
|
||||
public var readyState: StreamReadyState = .idle
|
||||
package var bitRateStrategy: (any StreamBitRateStrategy)?
|
||||
private var direction: RTCDirection = .sendonly
|
||||
|
||||
@@ -65,7 +58,7 @@ public actor MediaStream {
|
||||
}
|
||||
|
||||
public func close() async {
|
||||
_tracks.removeAll()
|
||||
tracks.removeAll()
|
||||
switch direction {
|
||||
case .sendonly:
|
||||
outgoing.stopRunning()
|
||||
@@ -77,9 +70,14 @@ public actor MediaStream {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
func addTrack(_ track: RTCSendableStreamTrack) async {
|
||||
await track.setDelegate(self)
|
||||
tracks.append(track)
|
||||
}
|
||||
}
|
||||
|
||||
extension MediaStream: _Stream {
|
||||
extension RTCStream: _Stream {
|
||||
public func setAudioSettings(_ audioSettings: AudioCodecSettings) throws {
|
||||
guard Self.supportedAudioCodecs.contains(audioSettings.format) else {
|
||||
throw Error.unsupportedCodec
|
||||
@@ -99,7 +97,7 @@ extension MediaStream: _Stream {
|
||||
case .video:
|
||||
if sampleBuffer.formatDescription?.isCompressed == true {
|
||||
Task {
|
||||
for track in _tracks {
|
||||
for track in tracks {
|
||||
await track.send(sampleBuffer)
|
||||
}
|
||||
}
|
||||
@@ -125,7 +123,7 @@ extension MediaStream: _Stream {
|
||||
outputs.forEach { $0.stream(self, didOutput: audioBuffer, when: when) }
|
||||
case let audioBuffer as AVAudioCompressedBuffer:
|
||||
Task {
|
||||
for track in _tracks {
|
||||
for track in tracks {
|
||||
await track.send(audioBuffer, when: when)
|
||||
}
|
||||
}
|
||||
@@ -139,7 +137,7 @@ extension MediaStream: _Stream {
|
||||
}
|
||||
}
|
||||
|
||||
extension MediaStream: RTCTrackDelegate {
|
||||
extension RTCStream: RTCTrackDelegate {
|
||||
// MARK: RTCTrackDelegate
|
||||
nonisolated func track(_ track: RTCTrack, readyStateChanged readyState: RTCTrack.ReadyState) {
|
||||
}
|
||||
@@ -157,7 +155,7 @@ extension MediaStream: RTCTrackDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
extension MediaStream: MediaMixerOutput {
|
||||
extension RTCStream: MediaMixerOutput {
|
||||
// MARK: MediaMixerOutput
|
||||
public func selectTrack(_ id: UInt8?, mediaType: CMFormatDescription.MediaType) {
|
||||
switch mediaType {
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
public enum MediaStreamKind {
|
||||
public enum RTCStreamKind {
|
||||
case audio
|
||||
case video
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import AVFAudio
|
||||
import CoreMedia
|
||||
import Foundation
|
||||
import HaishinKit
|
||||
import libdatachannel
|
||||
|
||||
public protocol RTCStreamTrack: Sendable {
|
||||
var id: String { get }
|
||||
}
|
||||
|
||||
public struct AudioStreamTrack: RTCStreamTrack, Sendable {
|
||||
public let id: String
|
||||
public let settings: AudioCodecSettings
|
||||
|
||||
public init(_ settings: AudioCodecSettings) {
|
||||
self.id = UUID().uuidString
|
||||
self.settings = settings
|
||||
}
|
||||
}
|
||||
|
||||
public struct VideoStreamTrack: RTCStreamTrack, Sendable {
|
||||
public let id: String
|
||||
public let settings: VideoCodecSettings
|
||||
|
||||
public init(_ settings: VideoCodecSettings) {
|
||||
self.id = UUID().uuidString
|
||||
self.settings = settings
|
||||
}
|
||||
}
|
||||
@@ -75,27 +75,33 @@ class RTCTrack: RTCChannel {
|
||||
|
||||
private var packetizer: (any RTPPacketizer)?
|
||||
|
||||
init(id: Int32) {
|
||||
init(id: Int32) throws {
|
||||
self.id = id
|
||||
rtcSetUserPointer(id, Unmanaged.passUnretained(self).toOpaque())
|
||||
rtcSetOpenCallback(id) { _, pointer in
|
||||
guard let pointer else { return }
|
||||
Unmanaged<RTCTrack>.fromOpaque(pointer).takeUnretainedValue().readyState = .open
|
||||
}
|
||||
rtcSetClosedCallback(id) { _, pointer in
|
||||
guard let pointer else { return }
|
||||
Unmanaged<RTCTrack>.fromOpaque(pointer).takeUnretainedValue().readyState = .closed
|
||||
}
|
||||
rtcSetMessageCallback(id) { _, bytes, size, pointer in
|
||||
guard let bytes, let pointer else { return }
|
||||
if 0 <= size {
|
||||
let data = Data(bytes: bytes, count: Int(size))
|
||||
Unmanaged<RTCTrack>.fromOpaque(pointer).takeUnretainedValue().didReceiveMessage(data)
|
||||
}
|
||||
}
|
||||
rtcSetErrorCallback(id) { _, error, pointer in
|
||||
guard let error, let pointer else { return }
|
||||
Unmanaged<RTCTrack>.fromOpaque(pointer).takeUnretainedValue().errorOccurred(String(cString: error))
|
||||
try RTCError.check(id)
|
||||
do {
|
||||
rtcSetUserPointer(id, Unmanaged.passUnretained(self).toOpaque())
|
||||
try RTCError.check(rtcSetOpenCallback(id) { _, pointer in
|
||||
guard let pointer else { return }
|
||||
Unmanaged<RTCTrack>.fromOpaque(pointer).takeUnretainedValue().readyState = .open
|
||||
})
|
||||
try RTCError.check(rtcSetClosedCallback(id) { _, pointer in
|
||||
guard let pointer else { return }
|
||||
Unmanaged<RTCTrack>.fromOpaque(pointer).takeUnretainedValue().readyState = .closed
|
||||
})
|
||||
try RTCError.check(rtcSetMessageCallback(id) { _, bytes, size, pointer in
|
||||
guard let bytes, let pointer else { return }
|
||||
if 0 <= size {
|
||||
let data = Data(bytes: bytes, count: Int(size))
|
||||
Unmanaged<RTCTrack>.fromOpaque(pointer).takeUnretainedValue().didReceiveMessage(data)
|
||||
}
|
||||
})
|
||||
try RTCError.check(rtcSetErrorCallback(id) { _, error, pointer in
|
||||
guard let error, let pointer else { return }
|
||||
Unmanaged<RTCTrack>.fromOpaque(pointer).takeUnretainedValue().errorOccurred(String(cString: error))
|
||||
})
|
||||
} catch {
|
||||
rtcDeleteTrack(id)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+9
-17
@@ -1,10 +1,8 @@
|
||||
import AVFAudio
|
||||
import CoreMedia
|
||||
import Foundation
|
||||
import HaishinKit
|
||||
import libdatachannel
|
||||
|
||||
public actor MediaStreamTrack {
|
||||
struct RTCTrackConfiguration: Sendable {
|
||||
private static func generateSSRC() -> UInt32 {
|
||||
var ssrc: UInt32 = 0
|
||||
repeat {
|
||||
@@ -25,9 +23,9 @@ public actor MediaStreamTrack {
|
||||
let msid: String
|
||||
let trackId: String
|
||||
let profile: String?
|
||||
let id: String = UUID().uuidString
|
||||
private var track: RTCTrack?
|
||||
}
|
||||
|
||||
extension RTCTrackConfiguration {
|
||||
init(mid: String, streamId: String, audioCodecSettings: AudioCodecSettings) {
|
||||
self.codec = audioCodecSettings.format.cValue ?? RTC_CODEC_OPUS
|
||||
self.ssrc = Self.generateSSRC()
|
||||
@@ -35,7 +33,7 @@ public actor MediaStreamTrack {
|
||||
self.mid = mid
|
||||
self.name = Self.generateCName()
|
||||
self.msid = streamId
|
||||
self.trackId = id
|
||||
self.trackId = UUID().uuidString
|
||||
self.profile = "minptime=10;useinbandfec=1;stereo=1;sprop-stereo=1"
|
||||
}
|
||||
|
||||
@@ -46,22 +44,16 @@ public actor MediaStreamTrack {
|
||||
self.mid = mid
|
||||
self.name = Self.generateCName()
|
||||
self.msid = streamId
|
||||
self.trackId = id
|
||||
self.trackId = UUID().uuidString
|
||||
self.profile = nil
|
||||
}
|
||||
}
|
||||
|
||||
func send(_ buffer: CMSampleBuffer) {
|
||||
track?.send(buffer)
|
||||
}
|
||||
|
||||
func send(_ buffer: AVAudioCompressedBuffer, when: AVAudioTime) {
|
||||
track?.send(buffer, when: when)
|
||||
}
|
||||
|
||||
func addTrack(_ connection: Int32, direction: RTCDirection) throws {
|
||||
extension RTCTrackConfiguration {
|
||||
func addTrack(_ connection: Int32, direction: RTCDirection) throws -> Int32 {
|
||||
var rtcTrackInit = makeRtcTrackInit(direction)
|
||||
let result = try RTCError.check(rtcAddTrackEx(connection, &rtcTrackInit))
|
||||
track = RTCTrack(id: result)
|
||||
return result
|
||||
}
|
||||
|
||||
private func makeRtcTrackInit(_ direction: RTCDirection) -> rtcTrackInit {
|
||||
Reference in New Issue
Block a user