improve RTCMediaStreamTrack.

This commit is contained in:
shogo4405
2025-11-17 10:24:24 +09:00
parent 6d527ef0d5
commit 776a149db6
12 changed files with 307 additions and 217 deletions
+15 -17
View File
@@ -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?
+28 -22
View File
@@ -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
}
}
}
+158 -62
View File
@@ -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
}
}
}
@@ -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,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
}
}
+26 -20
View File
@@ -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
}
}
@@ -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 {