feat(ios): 添加远端视频录制功能并优化信令连接稳定性

- 新增 VideoRecorder 组件,支持 WebRTC 和自编码两种模式录制远端视频为 MP4
- 优化 SignalingClient 连接管理,修复 WebSocket 握手前 receive 导致的断开问题
- 重构控制面板 UI,支持统计信息折叠展开和录制控制按钮
- 添加录制状态指示、保存路径提示及断开自动取消录制逻辑
- 更新项目配置和文档,补充录制功能说明
This commit is contained in:
2026-08-01 03:59:43 +08:00
parent 30f26f27c3
commit a73f61f366
13 changed files with 651 additions and 84 deletions

View File

@@ -19,6 +19,10 @@ final class SignalingClient: NSObject {
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
@@ -32,16 +36,20 @@ final class SignalingClient: NSObject {
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()
receiveLoop()
}
func disconnect() {
manuallyClosed = true
isOpened = false
task?.cancel(with: .normalClosure, reason: "Disconnecting".data(using: .utf8))
task = nil
session?.invalidateAndCancel()
@@ -85,9 +93,31 @@ final class SignalingClient: NSObject {
}
self.receiveLoop()
case .failure(let error):
if !self.manuallyClosed {
self.notifyError(error.localizedDescription)
}
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)
}
}
}
@@ -106,6 +136,9 @@ extension SignalingClient: URLSessionWebSocketDelegate {
func urlSession(_ session: URLSession,
webSocketTask: URLSessionWebSocketTask,
didOpenWithProtocol protocol: String?) {
isOpened = true
// socket receive
receiveLoop()
registerDevice()
DispatchQueue.main.async {
self.delegate?.signalingDidConnect()
@@ -116,7 +149,9 @@ extension SignalingClient: URLSessionWebSocketDelegate {
webSocketTask: URLSessionWebSocketTask,
didCloseWith closeCode: URLSessionWebSocketTask.CloseCode,
reason: Data?) {
guard !manuallyClosed else { return }
isOpened = false
guard !manuallyClosed, !didNotifyClosure else { return }
didNotifyClosure = true
DispatchQueue.main.async {
self.delegate?.signalingDidDisconnect()
}