Files
VibeCoding/webrtc_controller_ios/web_rtc_controller_ios/Signaling/SignalingClient.swift
tongtongstudio a73f61f366 feat(ios): 添加远端视频录制功能并优化信令连接稳定性
- 新增 VideoRecorder 组件,支持 WebRTC 和自编码两种模式录制远端视频为 MP4
- 优化 SignalingClient 连接管理,修复 WebSocket 握手前 receive 导致的断开问题
- 重构控制面板 UI,支持统计信息折叠展开和录制控制按钮
- 添加录制状态指示、保存路径提示及断开自动取消录制逻辑
- 更新项目配置和文档,补充录制功能说明
2026-08-01 03:59:43 +08:00

160 lines
5.4 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
protocol SignalingClientDelegate: AnyObject {
func signalingDidConnect()
func signalingDidDisconnect()
func signaling(didFail error: String)
func signaling(didReceive message: SignalMessage)
}
/// WebSocket URLSessionWebSocketTask
/// REGISTERdeviceType = CONTROLLER
/// 线
final class SignalingClient: NSObject {
weak var delegate: SignalingClientDelegate?
private let serverUrl: String
private let deviceId: String
private var session: URLSession?
private var task: URLSessionWebSocketTask?
private var manuallyClosed = false
/// WebSocket didOpen true
private var isOpened = false
/// /
private var didNotifyClosure = false
init(serverUrl: String, deviceId: String) {
self.serverUrl = serverUrl
self.deviceId = deviceId
super.init()
}
func connect() {
guard let url = URL(string: serverUrl) else {
notifyError("无效的服务器地址: \(serverUrl)")
return
}
manuallyClosed = false
isOpened = false
didNotifyClosure = false
let config = URLSessionConfiguration.default
config.timeoutIntervalForRequest = 15
session = URLSession(configuration: config, delegate: self, delegateQueue: nil)
task = session?.webSocketTask(with: url)
// didOpenWithProtocol
// socket receive Code 57
task?.resume()
}
func disconnect() {
manuallyClosed = true
isOpened = false
task?.cancel(with: .normalClosure, reason: "Disconnecting".data(using: .utf8))
task = nil
session?.invalidateAndCancel()
session = nil
}
func send(_ message: SignalMessage) {
guard let task else { return }
guard let data = try? JSONEncoder().encode(message),
let json = String(data: data, encoding: .utf8) else { return }
task.send(.string(json)) { error in
if let error {
NSLog("[Signaling] send error: \(error.localizedDescription)")
}
}
}
var isConnected: Bool { task != nil }
// MARK: - Private
private func registerDevice() {
var msg = SignalMessage()
msg.type = "REGISTER"
msg.fromDeviceId = deviceId
msg.deviceType = "CONTROLLER"
send(msg)
}
private func receiveLoop() {
task?.receive { [weak self] result in
guard let self else { return }
switch result {
case .success(let wsMessage):
if case .string(let text) = wsMessage,
let data = text.data(using: .utf8),
let message = try? JSONDecoder().decode(SignalMessage.self, from: data) {
DispatchQueue.main.async {
self.delegate?.signaling(didReceive: message)
}
}
self.receiveLoop()
case .failure(let error):
self.handleReceiveFailure(error)
}
}
}
/// """"
/// Socket Code 57
private func handleReceiveFailure(_ error: Error) {
guard !manuallyClosed, !didNotifyClosure else { return }
didNotifyClosure = true
let nsError = error as NSError
// NSPOSIXErrorDomain Code 57: Socket is not connected
let isDisconnect = (nsError.domain == NSPOSIXErrorDomain && nsError.code == 57)
|| (nsError.domain == NSURLErrorDomain
&& (nsError.code == NSURLErrorNetworkConnectionLost
|| nsError.code == NSURLErrorCancelled))
DispatchQueue.main.async {
if self.isOpened || isDisconnect {
// ""
self.delegate?.signalingDidDisconnect()
} else {
// ""
self.delegate?.signaling(didFail: error.localizedDescription)
}
}
}
private func notifyError(_ text: String) {
DispatchQueue.main.async {
self.delegate?.signaling(didFail: text)
}
}
}
// MARK: - URLSessionWebSocketDelegate
extension SignalingClient: URLSessionWebSocketDelegate {
func urlSession(_ session: URLSession,
webSocketTask: URLSessionWebSocketTask,
didOpenWithProtocol protocol: String?) {
isOpened = true
// socket receive
receiveLoop()
registerDevice()
DispatchQueue.main.async {
self.delegate?.signalingDidConnect()
}
}
func urlSession(_ session: URLSession,
webSocketTask: URLSessionWebSocketTask,
didCloseWith closeCode: URLSessionWebSocketTask.CloseCode,
reason: Data?) {
isOpened = false
guard !manuallyClosed, !didNotifyClosure else { return }
didNotifyClosure = true
DispatchQueue.main.async {
self.delegate?.signalingDidDisconnect()
}
}
}