import Foundation import WebRTC protocol WebRTCClientDelegate: AnyObject { /// ICE 连接建立(可以开始远程控制) func webRTCClientDidConnect() /// 连接断开(远端断开或网络故障) func webRTCClientDidDisconnect() /// 连接失败 func webRTCClient(didFail error: String) /// 被控端上报当前串流模式(0=WebRTC 1=自编码) func webRTCClient(didReportStreamMode mode: Int) /// 被控端上报当前采集分辨率 func webRTCClient(didReportResolution width: Int, height: Int) } /// WebRTC 客户端:负责 PeerConnection 的创建、Offer/Answer 协商、 /// ICE 候选交换、控制/视频 DataChannel 管理与统计信息获取。 /// 与 Android 端 WebRtcClient 行为保持一致。 final class WebRTCClient: NSObject { static let streamModeWebRTC: Int32 = 0 static let streamModeSelfCodec: Int32 = 1 private static let controlChannelLabel = "control_channel" private static let videoChannelLabel = "video_channel" private static let factory: RTCPeerConnectionFactory = { RTCInitializeSSL() return RTCPeerConnectionFactory( encoderFactory: RTCDefaultVideoEncoderFactory(), decoderFactory: RTCDefaultVideoDecoderFactory()) }() weak var delegate: WebRTCClientDelegate? /// 自编码模式的 H.264 裸流解码器(video_channel 二进制数据直接转发给它) var selfCodecDecoder: SelfCodecDecoder? private let signaling: SignalingClient private let myDeviceId: String private var targetDeviceId: String = "" private var peerConnection: RTCPeerConnection? private var controlChannel: RTCDataChannel? private var videoChannel: RTCDataChannel? private weak var remoteRenderer: RTCVideoRenderer? private var remoteVideoTrack: RTCVideoTrack? private var connected = false init(signaling: SignalingClient, myDeviceId: String) { self.signaling = signaling self.myDeviceId = myDeviceId super.init() } var isDataChannelOpen: Bool { controlChannel?.readyState == .open } // MARK: - 连接流程 /// 创建 PeerConnection 与 DataChannel,并向目标设备发送 Offer(携带鉴权信息)。 func createOffer(targetDeviceId: String, renderer: RTCVideoRenderer, authType: String?, authValue: String?) { self.targetDeviceId = targetDeviceId self.remoteRenderer = renderer let config = RTCConfiguration() config.iceServers = [ RTCIceServer(urlStrings: ["stun:stun.l.google.com:19302"]), RTCIceServer(urlStrings: ["stun:www.ttstd.com:3478"]), RTCIceServer(urlStrings: ["turn:www.ttstd.com:3478"], username: "ttstd", credential: "ttstd123") ] config.sdpSemantics = .unifiedPlan config.continualGatheringPolicy = .gatherContinually config.iceCandidatePoolSize = 10 config.iceTransportPolicy = .all let pcConstraints = RTCMediaConstraints(mandatoryConstraints: nil, optionalConstraints: nil) guard let pc = Self.factory.peerConnection(with: config, constraints: pcConstraints, delegate: self) else { notifyFail("PeerConnection 创建失败") return } peerConnection = pc // 只接收视频,不发送本地媒体 let transceiverInit = RTCRtpTransceiverInit() transceiverInit.direction = .recvOnly pc.addTransceiver(of: .video, init: transceiverInit) // 控制通道:可靠、有序 let controlConfig = RTCDataChannelConfiguration() controlConfig.isOrdered = true controlChannel = pc.dataChannel(forLabel: Self.controlChannelLabel, configuration: controlConfig) controlChannel?.delegate = self // 自编码视频通道:不可靠、低延迟(丢帧优先) let videoConfig = RTCDataChannelConfiguration() videoConfig.isOrdered = false videoConfig.maxRetransmits = 0 videoChannel = pc.dataChannel(forLabel: Self.videoChannelLabel, configuration: videoConfig) videoChannel?.delegate = self let offerConstraints = RTCMediaConstraints( mandatoryConstraints: [ "OfferToReceiveVideo": "true", "OfferToReceiveAudio": "false" ], optionalConstraints: nil) pc.offer(for: offerConstraints) { [weak self] sdp, error in guard let self else { return } if let error { self.notifyFail("创建 Offer 失败: \(error.localizedDescription)") return } guard let sdp else { self.notifyFail("创建 Offer 失败: SDP 为空") return } pc.setLocalDescription(sdp) { [weak self] error in guard let self else { return } if let error { self.notifyFail("设置本地 SDP 失败: \(error.localizedDescription)") return } self.sendOffer(sdp: sdp.sdp, authType: authType, authValue: authValue) } } } private func sendOffer(sdp: String, authType: String?, authValue: String?) { var msg = SignalMessage() msg.type = "OFFER" msg.fromDeviceId = myDeviceId msg.toDeviceId = targetDeviceId msg.payload = SignalMessage.encodePayload(["sdp": sdp]) msg.authType = authType msg.authValue = authValue signaling.send(msg) } /// 处理被控端返回的 Answer func handleAnswer(sdp: String) { let desc = RTCSessionDescription(type: .answer, sdp: sdp) peerConnection?.setRemoteDescription(desc) { [weak self] error in if let error { self?.notifyFail("设置远端 SDP 失败: \(error.localizedDescription)") } } } /// 添加远端 ICE 候选 func addIceCandidate(sdpMid: String, sdpMLineIndex: Int32, candidate: String) { let ice = RTCIceCandidate(sdp: candidate, sdpMLineIndex: sdpMLineIndex, sdpMid: sdpMid) peerConnection?.add(ice) { error in if let error { NSLog("[WebRTC] addIceCandidate error: \(error.localizedDescription)") } } } // MARK: - 控制指令 func sendControlCommand(_ message: ControlMessage) { guard let channel = controlChannel, channel.readyState == .open else { return } let buffer = RTCDataBuffer(data: message.serializedData(), isBinary: true) channel.sendData(buffer) } /// 请求被控端切换采集分辨率(width=0 表示恢复原生分辨率) func requestResolutionChange(width: Int32, height: Int32, fps: Int32) { var msg = ControlMessage() msg.action = .setResolution msg.width = width msg.height = height msg.fps = fps sendControlCommand(msg) } /// 请求被控端切换串流模式(0=WebRTC 1=自编码) func sendStreamMode(_ mode: Int32) { var msg = ControlMessage() msg.action = .setStreamMode msg.streamMode = mode sendControlCommand(msg) } // MARK: - 统计 func stats(_ completion: @escaping (RTCStatisticsReport) -> Void) { peerConnection?.statistics(completionHandler: completion) } // MARK: - 关闭 func close() { connected = false controlChannel?.close() videoChannel?.close() controlChannel = nil videoChannel = nil if let track = remoteVideoTrack, let renderer = remoteRenderer { track.remove(renderer) } remoteVideoTrack = nil peerConnection?.close() peerConnection = nil } private func notifyFail(_ text: String) { DispatchQueue.main.async { self.delegate?.webRTCClient(didFail: text) } } } // MARK: - RTCPeerConnectionDelegate extension WebRTCClient: RTCPeerConnectionDelegate { func peerConnection(_ peerConnection: RTCPeerConnection, didChange stateChanged: RTCSignalingState) {} func peerConnection(_ peerConnection: RTCPeerConnection, didAdd stream: RTCMediaStream) {} func peerConnection(_ peerConnection: RTCPeerConnection, didRemove stream: RTCMediaStream) {} func peerConnectionShouldNegotiate(_ peerConnection: RTCPeerConnection) {} func peerConnection(_ peerConnection: RTCPeerConnection, didChange newState: RTCIceConnectionState) { DispatchQueue.main.async { switch newState { case .connected, .completed: if !self.connected { self.connected = true self.delegate?.webRTCClientDidConnect() } case .disconnected, .failed, .closed: if self.connected { self.connected = false self.delegate?.webRTCClientDidDisconnect() } else if newState == .failed { self.delegate?.webRTCClient(didFail: "ICE 连接失败") } default: break } } } func peerConnection(_ peerConnection: RTCPeerConnection, didChange newState: RTCIceGatheringState) {} func peerConnection(_ peerConnection: RTCPeerConnection, didGenerate candidate: RTCIceCandidate) { var msg = SignalMessage() msg.type = "ICE_CANDIDATE" msg.fromDeviceId = myDeviceId msg.toDeviceId = targetDeviceId msg.payload = SignalMessage.encodePayload([ "sdpMid": candidate.sdpMid ?? "", "sdpMLineIndex": Int(candidate.sdpMLineIndex), "candidate": candidate.sdp ]) signaling.send(msg) } func peerConnection(_ peerConnection: RTCPeerConnection, didRemove candidates: [RTCIceCandidate]) {} func peerConnection(_ peerConnection: RTCPeerConnection, didOpen dataChannel: RTCDataChannel) { // 作为 Offer 方,通道由本端创建;此回调用于兜底远端创建的通道 dataChannel.delegate = self if dataChannel.label == Self.videoChannelLabel { videoChannel = dataChannel } else if dataChannel.label == Self.controlChannelLabel { controlChannel = dataChannel } } func peerConnection(_ peerConnection: RTCPeerConnection, didAdd rtpReceiver: RTCRtpReceiver, streams mediaStreams: [RTCMediaStream]) { guard let track = rtpReceiver.track as? RTCVideoTrack else { return } DispatchQueue.main.async { self.remoteVideoTrack = track if let renderer = self.remoteRenderer { track.add(renderer) } } } } // MARK: - RTCDataChannelDelegate extension WebRTCClient: RTCDataChannelDelegate { func dataChannelDidChangeState(_ dataChannel: RTCDataChannel) { NSLog("[WebRTC] DataChannel \(dataChannel.label) state: \(dataChannel.readyState.rawValue)") } func dataChannel(_ dataChannel: RTCDataChannel, didReceiveMessageWith buffer: RTCDataBuffer) { if dataChannel.label == Self.videoChannelLabel { // 自编码 H.264 裸流分片 selfCodecDecoder?.onBinaryMessage(buffer.data) return } // 控制通道:被控端上报(串流模式 / 分辨率) guard buffer.isBinary, let msg = ControlMessage.parse(from: buffer.data) else { return } DispatchQueue.main.async { switch msg.action { case .reportStreamMode: self.delegate?.webRTCClient(didReportStreamMode: Int(msg.streamMode)) case .reportResolution: self.delegate?.webRTCClient(didReportResolution: Int(msg.width), height: Int(msg.height)) default: break } } } }