322 lines
12 KiB
Swift
322 lines
12 KiB
Swift
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
|
||
}
|
||
}
|
||
}
|
||
}
|