- 新增 VideoRecorder 组件,支持 WebRTC 和自编码两种模式录制远端视频为 MP4 - 优化 SignalingClient 连接管理,修复 WebSocket 握手前 receive 导致的断开问题 - 重构控制面板 UI,支持统计信息折叠展开和录制控制按钮 - 添加录制状态指示、保存路径提示及断开自动取消录制逻辑 - 更新项目配置和文档,补充录制功能说明
160 lines
5.4 KiB
Swift
160 lines
5.4 KiB
Swift
import Foundation
|
||
|
||
protocol SignalingClientDelegate: AnyObject {
|
||
func signalingDidConnect()
|
||
func signalingDidDisconnect()
|
||
func signaling(didFail error: String)
|
||
func signaling(didReceive message: SignalMessage)
|
||
}
|
||
|
||
/// 信令 WebSocket 客户端(URLSessionWebSocketTask 实现)。
|
||
/// 连接成功后自动发送 REGISTER(deviceType = 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()
|
||
}
|
||
}
|
||
}
|