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),