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

@@ -48,12 +48,14 @@ final class ControllerViewModel: NSObject, ObservableObject {
// MARK: - UI 状态
@Published var serverUrl: String = "wss://www.ttstd.com/signal"
@Published var targetDeviceId: String = ""
@Published var targetDeviceId: String = "981964879"
@Published var statusText: String = "未连接"
/// 是否显示控制界面(对应 Android 的 setupPanel/controlPanel 切换)
@Published var isControlling: Bool = false
@Published var isConnecting: Bool = false
@Published var statsText: String = ""
/// 统计信息面板是否展开(默认收起,仅显示概要)
@Published var statsExpanded: Bool = false
@Published var streamMode: StreamMode = .webrtc
@Published var selectedResolution: ResolutionOption = ResolutionOption.all[0]
/// 帧率档位:默认常用档位,收到被控端上报的 supported_fps 后以上报列表为准
@@ -65,6 +67,11 @@ final class ControllerViewModel: NSObject, ObservableObject {
@Published var showAuthSheet: Bool = false
@Published var alertMessage: String?
/// 是否正在录制远端视频
@Published var isRecording: Bool = false
/// 最近一次录制保存的文件路径(沙盒内)
@Published var lastRecordingPath: String?
let myDeviceId: String = DeviceUtils.deviceId()
// MARK: - UIKit 渲染视图(由 ViewModel 持有,SwiftUI 通过 Representable 嵌入)
@@ -77,6 +84,8 @@ final class ControllerViewModel: NSObject, ObservableObject {
private var signalingClient: SignalingClient?
private var webRTCClient: WebRTCClient?
private let selfCodecDecoder = SelfCodecDecoder()
/// 远端视频录制器(MP4),跟随两种串流模式工作
private let videoRecorder = VideoRecorder()
private var statsTimer: Timer?
private var connectionStartTime: Date?
@@ -98,6 +107,24 @@ final class ControllerViewModel: NSObject, ObservableObject {
guard let self, self.streamMode == .selfCodec, w > 0, h > 0 else { return }
self.videoAspect = CGFloat(w) / CGFloat(h)
}
// 自编码模式:把硬解出的 AVCC 帧回传给录制器写盘
selfCodecDecoder.onDecodedSampleBuffer = { [weak self] sample, isKey in
self?.videoRecorder.appendDecodedSampleBuffer(sample, isKey: isKey)
}
// 录制状态/结果回调
videoRecorder.onStateChange = { [weak self] state in
self?.isRecording = (state == .recording)
}
videoRecorder.onSaved = { [weak self] path in
self?.lastRecordingPath = path
self?.alertMessage = "录制已保存:\n\(path)"
}
videoRecorder.onSavedEmpty = { [weak self] in
self?.alertMessage = "未录到任何画面"
}
videoRecorder.onError = { [weak self] msg in
self?.alertMessage = "录制失败: \(msg)"
}
}
// MARK: - 连接 / 断开
@@ -134,6 +161,8 @@ final class ControllerViewModel: NSObject, ObservableObject {
func disconnect() {
stopStatsTimer()
videoRecorder.cancel()
isRecording = false
selfCodecDecoder.setEnabled(false)
webRTCClient?.close()
webRTCClient = nil
@@ -225,6 +254,25 @@ final class ControllerViewModel: NSObject, ObservableObject {
selfCodecDecoder.setEnabled(mode == .selfCodec)
}
// MARK: - 录制
/// 开始录制远端画面(WebRTC 与自编码两种模式均支持)。
/// 需在已连接后调用;若尚未建立连接会提示。
func startRecording() {
guard isControlling else {
alertMessage = "请先连接被控设备"
return
}
guard !isRecording else { return }
videoRecorder.start()
}
/// 停止录制并保存 MP4 到沙盒 Documents/WebRTCRecordings。
func stopRecording() {
guard isRecording else { return }
videoRecorder.stop()
}
// MARK: - 私有
fileprivate func startWebRTC() {
@@ -233,6 +281,7 @@ final class ControllerViewModel: NSObject, ObservableObject {
let client = WebRTCClient(signaling: signaling, myDeviceId: myDeviceId)
client.delegate = self
client.selfCodecDecoder = selfCodecDecoder
client.extraRenderer = videoRecorder
webRTCClient = client
client.createOffer(
targetDeviceId: targetDeviceId.trimmingCharacters(in: .whitespacesAndNewlines),