feat(ios): 添加远端视频录制功能并优化信令连接稳定性
- 新增 VideoRecorder 组件,支持 WebRTC 和自编码两种模式录制远端视频为 MP4 - 优化 SignalingClient 连接管理,修复 WebSocket 握手前 receive 导致的断开问题 - 重构控制面板 UI,支持统计信息折叠展开和录制控制按钮 - 添加录制状态指示、保存路径提示及断开自动取消录制逻辑 - 更新项目配置和文档,补充录制功能说明
This commit is contained in:
@@ -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),
|
||||
|
||||
Reference in New Issue
Block a user