Files
VibeCoding/webrtc_controller_ios/web_rtc_controller_ios/WebRTC/WebRTCClient.swift
tongtongstudio 6eb2c7321a feat(controlled): 实现设备激活与安全认证流程
- 添加API客户端、加密存储和provision/token激活逻辑
- WebSocket改用Bearer令牌认证,移除REGISTER请求
- 设备ID改为服务端下发,支持令牌刷新和强制下线处理
- 新增deviceSecret加密存储和accessToken自动刷新
- 更新设备ID获取方式为出厂SN,添加安全存储依赖
2026-08-01 15:13:12 +08:00

347 lines
13 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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)
///
func webRTCClient(didReportFps fps: Int, supportedFps: [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?
/// fan-out WebRTC
weak var extraRenderer: RTCVideoRenderer?
private var connected = false
init(signaling: SignalingClient, myDeviceId: String) {
self.signaling = signaling
self.myDeviceId = myDeviceId
super.init()
}
/// TURN ICE STUN/TURN
static var iceServersOverride: [[String: Any]]?
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()
if let override = WebRTCClient.iceServersOverride {
// TURN RFC 7635
config.iceServers = override.compactMap { entry in
guard let urls = entry["urls"] as? String else { return nil }
return RTCIceServer(urlStrings: [urls],
username: entry["username"] as? String,
credential: entry["credential"] as? String)
}
} else {
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)
}
if let track = remoteVideoTrack, let extra = extraRenderer {
track.remove(extra)
}
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)
}
if let extra = self.extraRenderer {
track.add(extra)
}
}
}
}
// 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))
self.delegate?.webRTCClient(didReportFps: Int(msg.fps),
supportedFps: msg.supportedFps.map(Int.init))
default:
break
}
}
}
}