Files
HaishinKit.swift/RTCHaishinKit/Sources/RTC/RTCPeerConnection.swift
2025-11-19 00:07:09 +09:00

377 lines
14 KiB
Swift

import Foundation
import libdatachannel
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)
}
public final class RTCPeerConnection {
/// Represents the state of a connection.
public enum ConnectionState: Sendable {
/// The connection has been created, but no connection attempt has started yet.
case new
/// A connection attempt is currently in progress.
case connecting
/// The connection has been successfully established.
case connected
/// The connection was previously established but is now temporarily lost.
case disconnected
/// The connection has encountered an unrecoverable error.
case failed
/// The connection has been closed and will not be used again.
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
a=recvonly
a=rtpmap:111 opus/48000/2
a=fmtp:111 minptime=10;useinbandfec=1;stereo=1;sprop-stereo=1
"""
static let videoMediaDescription = """
m=video 9 UDP/TLS/RTP/SAVPF 98
a=mid:1
a=recvonly
a=rtpmap:98 H264/90000
a=rtcp-fb:98 goog-remb
a=rtcp-fb:98 nack
a=rtcp-fb:98 nack pli
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)
}
}
/// 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 = ""
/// Creates a peerConnection instance.
public init(_ config: (some RTCConfigurationConvertible)? = nil) throws {
if let config {
connection = config.createPeerConnection()
} else {
connection = RTCConfiguration.empty.createPeerConnection()
}
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
}
}
deinit {
close()
rtcDeletePeerConnection(connection)
}
/// 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: RTCStreamKind, stream: RTCStream) throws -> RTCTrack {
let sdp: String
switch kind {
case .audio:
sdp = Self.audioMediaDescription
case .video:
sdp = Self.videoMediaDescription
}
let result = try RTCError.check(sdp.withCString { cString in
rtcAddTrack(connection, cString)
})
let track = try RTCTrack(id: result)
track.delegate = stream
return track
}
public func setRemoteDesciption(_ sdp: String, type: SDPSessionDescriptionType) throws {
logger.debug(sdp, type.rawValue)
try RTCError.check([sdp, type.rawValue].withCStrings { cStrings in
rtcSetRemoteDescription(connection, cStrings[0], cStrings[1])
})
}
public func setLocalDesciption(_ type: SDPSessionDescriptionType) throws {
logger.debug(type.rawValue)
try RTCError.check([type.rawValue].withCStrings { cStrings in
rtcSetLocalDescription(connection, cStrings[0])
})
}
public func createOffer() throws -> String {
return try CUtil.getString { buffer, size in
rtcCreateOffer(connection, buffer, size)
}
}
public func createAnswer() throws -> String {
return try CUtil.getString { buffer, size in
rtcCreateAnswer(connection, buffer, size)
}
}
public func createDataChannel(_ label: String) throws -> RTCDataChannel {
let result = try RTCError.check([label].withCStrings { cStrings in
rtcCreateDataChannel(connection, cStrings[0])
})
return try RTCDataChannel(id: result)
}
public func close() {
do {
try RTCError.check(rtcClosePeerConnection(connection))
} catch {
logger.warn(error)
}
}
private func didGenerateCandidate(_ candidated: RTCIceCandidate) {
delegate?.peerConnection(self, gotIceCandidate: candidated)
}
private func didOpenTrack(_ track: RTCTrack) {
logger.info(track)
}
private func didOpenDataChannel(_ dataChannel: RTCDataChannel) {
delegate?.peerConnection(self, didOpen: dataChannel)
}
}
extension RTCPeerConnection.ConnectionState {
init?(cValue: rtcState) {
switch cValue {
case RTC_NEW:
self = .new
case RTC_CONNECTING:
self = .connecting
case RTC_CONNECTED:
self = .connected
case RTC_DISCONNECTED:
self = .disconnected
case RTC_FAILED:
self = .failed
case RTC_CLOSED:
self = .closed
default:
return nil
}
}
}
extension RTCPeerConnection.IceGatheringState {
init?(cValue: rtcGatheringState) {
switch cValue {
case RTC_GATHERING_NEW:
self = .new
case RTC_GATHERING_INPROGRESS:
self = .inProgress
case RTC_GATHERING_COMPLETE:
self = .complete
default:
return nil
}
}
}
extension RTCPeerConnection.IceConnectionState {
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
}
}
}
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
}
}
}