feat: 初始化 iOS 远程控制端项目

This commit is contained in:
2026-07-29 10:34:02 +08:00
parent 11173eb04c
commit 1e37aa64cc
20 changed files with 2610 additions and 0 deletions

View File

@@ -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
}
}
}
}