feat: 初始化 iOS 远程控制端项目
This commit is contained in:
@@ -0,0 +1,321 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user