# 本地录制调试指南(第二版) ## 问题描述 本地录制总是失败,`blob` 为空,导致无法保存通话录制文件。 ## 根本原因分析 从实际日志分析发现: 1. **录制启动太晚**:原来的 1200ms 延迟导致录制在对方挂断后才启动 2. **SDK 提前销毁轨道**:`stopLocalVideo/stopLocalAudio` 在 `exitRoom` 之前就销毁了视频轨道 3. **时间窗口太短**:从对方离开到轨道销毁只有约 200-300ms 关键时间线(问题场景): ``` 15:30:51.841 - 对方离开(USER_LEAVE 事件) 15:30:51.xxx - beginStopNow 被调用(但录制还未启动) 15:30:52.072 - stopLocalVideo/stopLocalAudio(轨道被销毁) 15:30:52.102 - exitRoom 15:30:52.374 - MediaRecorder 已启动(太晚了!) 15:30:52.xxx - 数据块数: 0(因为轨道已销毁) ``` ## 已实施的修复(第二版) ### 1. 大幅缩短录制启动延迟 - **从 1200ms 缩短到 500ms** - 使用 300ms 间隔重试(最多 10 次) - 更快地检测和启动录制 ```typescript // 延迟 500ms 后开始尝试(给视频元素一点加载时间) startLocalRecTimer = setTimeout(() => attemptStartRecording(0), 500) ``` ### 2. 在通话结束事件时立即停止录制 监听 TUICallEngine 事件,在检测到通话结束时立即停止: ```typescript const onEarlyStopRecording = () => { console.log('[chat-dialog] 检测到通话结束事件,立即停止录制') if (localCallRecorder.isRecording()) { console.log('[chat-dialog] 录制进行中,触发 beginStopNow') localCallRecorder.beginStopNow() } } // 监听: CALL_END, USER_LEAVE, ON_CALL_NOT_CONNECTED ``` ### 3. 缩短停止延迟 - **从 300ms 缩短到 100ms** - 减少等待时间,避免 SDK 提前销毁轨道 ```typescript // 缩短延迟到 100ms,避免 SDK 提前销毁轨道 setTimeout(() => { mr.stop() }, 100) ``` ### 4. 增强日志输出 在关键位置添加了详细的 console.log: - **录制启动阶段**: - 检查容器和通话状态 - 视频元素数量和就绪状态 - MediaRecorder 创建和启动状态 - Canvas 和音频混音初始化 - **录制进行阶段**: - 数据块收集(ondataavailable) - MediaRecorder 状态变化 - **录制停止阶段**: - beginStopNow 调用时机 - 数据块数量 - Blob 生成结果 ### 5. 智能视频元素检测 改进了视频元素查找逻辑: ```typescript // 1. 首先检查容器内的视频 let videos = Array.from(root.querySelectorAll('video')) // 2. 如果没有,检查 body(TUICallKit Teleport) if (videos.length === 0) { const bodyVideos = Array.from(document.body.querySelectorAll('video')) videos = bodyVideos.filter(v => { const so = v.srcObject if (!(so instanceof MediaStream)) return false return so.getVideoTracks().some(t => t.readyState === 'live') }) } // 3. 检查视频是否有有效数据 const hasReadyVideo = videos.some(v => v.readyState >= HTMLMediaElement.HAVE_CURRENT_DATA ) ``` ## 调试步骤 ### 1. 打开浏览器控制台 在发起视频通话前,打开浏览器开发者工具(F12),切换到 Console 标签。 ### 2. 发起视频通话 点击视频通话按钮,观察控制台输出。 ### 3. 关键日志检查点 #### 通话接通后(约 0.5-1 秒) 应该看到: ``` [chat-dialog] 尝试启动本地录制(第 1 次) [chat-dialog] 在 body 中找到活跃视频: 2 [chat-dialog] 视频元素已就绪,立即启动录制 [call-local-recorder] 使用 MIME 类型: video/webm;codecs=vp9,opus [call-local-recorder] Canvas 视频轨道数: 1 [call-local-recorder] 找到视频元素数量: 2 [call-local-recorder] 输出流轨道: { video: 1, audio: 2 } [call-local-recorder] MediaRecorder 创建成功 [call-local-recorder] MediaRecorder.start() 调用成功 [call-local-recorder] MediaRecorder 已启动 [chat-dialog] ✅ 本地录制已成功启动 ``` #### 录制过程中(每 250ms) 应该看到: ``` [call-local-recorder] 收到数据块,大小: 12345 (总计: 1 块) [call-local-recorder] 收到数据块,大小: 15678 (总计: 2 块) ... ``` **重要**:如果没有看到数据块,说明录制有问题! #### 对方挂断或己方挂断时 应该看到: ``` [chat-dialog] 检测到通话结束事件,立即停止录制 [chat-dialog] 录制进行中,触发 beginStopNow [call-local-recorder] beginStopNow 被调用 [call-local-recorder] 准备停止 MediaRecorder,当前状态: recording 已收集数据块: 15 [call-local-recorder] 调用 requestData() [call-local-recorder] 延迟后数据块数: 16 [call-local-recorder] 调用 stop() [call-local-recorder] MediaRecorder onstop 触发,数据块数: 16 [call-local-recorder] 生成 Blob: 1234567 bytes, type: video/webm [chat-dialog] localCallRecorder.stop() 返回: 1234567 bytes [chat-dialog] 开始上传录制文件 [chat-dialog] 上传成功,文件 URL: https://... [chat-dialog] 关联录制文件到诊单 本地录制已上传并关联到通话记录 ``` ## 常见问题诊断 ### 问题 1:录制启动太晚 **症状**: ``` [call-local-recorder] MediaRecorder 已启动 (出现在 stopLocalVideo 之后) ``` **原因**: - 视频元素加载慢 - 重试次数不够 **解决**: - 已缩短初始延迟到 500ms - 增加重试次数到 10 次 - 每次重试间隔 300ms ### 问题 2:找不到视频元素 **症状**: ``` [chat-dialog] 未找到视频元素,300ms 后重试 (重复多次) ``` **原因**: - TUICallKit 将视频 Teleport 到 body - 视频元素尚未渲染 **解决**: - 代码已添加 body 视频检查 - 添加了活跃视频流过滤 ### 问题 3:视频元素未就绪 **症状**: ``` [chat-dialog] 视频元素未就绪,300ms 后重试 ``` **原因**: - 视频流尚未加载 - readyState < HAVE_CURRENT_DATA **解决**: - 代码已添加就绪检查和重试 - 最多重试 10 次 ### 问题 4:没有收到数据块 **症状**: - 录制启动成功 - 但从未看到 "收到数据块" 日志 - 最终 `数据块数: 0` **可能原因**: 1. Canvas 绘制失败(跨域问题) 2. MediaRecorder 不支持当前 MIME 类型 3. 视频流被中断或为空 **检查方法**: ```javascript // 在控制台手动检查 const videos = document.querySelectorAll('video') videos.forEach((v, i) => { console.log(`Video ${i}:`, { readyState: v.readyState, videoWidth: v.videoWidth, videoHeight: v.videoHeight, srcObject: v.srcObject, videoTracks: v.srcObject?.getVideoTracks().map(t => ({ id: t.id, label: t.label, readyState: t.readyState, enabled: t.enabled })), audioTracks: v.srcObject?.getAudioTracks().map(t => ({ id: t.id, label: t.label, readyState: t.readyState, enabled: t.enabled })) }) }) ``` ### 问题 5:通话时间过短 **症状**: ``` [call-local-recorder] 生成 Blob: null 或 [call-local-recorder] 生成 Blob: 1234 bytes(很小) ``` **原因**: - 通话时长 < 1 秒 - 录制启动后立即挂断 **解决**: - 确保通话时长 > 2 秒 - 测试时保持通话至少 5 秒 ### 问题 6:SDK 提前销毁轨道 **症状**: ``` [call-local-recorder] MediaRecorder 错误 或 数据块数为 0 ``` **原因**: - SDK exitRoom 在 MediaRecorder.stop() 之前执行 - 视频轨道被销毁 **解决**: - 已在 USER_LEAVE 事件时立即停止录制 - 已在多个挂断入口添加钩子 - 缩短停止延迟到 100ms ## 测试建议 ### 最小测试场景 1. 发起视频通话 2. 等待接通(看到对方画面) 3. **保持通话至少 5 秒**(重要!) 4. 正常挂断 5. 检查控制台日志和录制结果 ### 关键检查点 - [ ] 录制在接通后 1 秒内启动 - [ ] 看到 "✅ 本地录制已成功启动" - [ ] 看到持续的 "收到数据块" 日志 - [ ] 数据块数 > 0(至少 4-5 块) - [ ] 生成的 Blob 大小 > 10KB - [ ] 上传成功并关联到诊单 ### 压力测试场景 1. 快速挂断(2-3 秒) 2. 对方挂断 3. 网络中断 4. 切换会话后挂断 5. 关闭聊天窗口 ## 预期改进效果 ### 修复前 ``` 通话接通 -> 等待 1200ms -> 尝试启动录制 -> 对方挂断 -> SDK 销毁轨道 -> 录制失败 ``` ### 修复后 ``` 通话接通 -> 等待 500ms -> 快速启动录制 -> 开始收集数据 -> 检测到挂断 -> 立即停止录制 -> 生成 Blob -> 上传成功 ``` ## 浏览器兼容性 ### 支持的浏览器 - Chrome/Edge 85+ - Firefox 78+ - Safari 14.1+ ### MIME 类型优先级 1. `video/webm;codecs=vp9,opus` (最佳) 2. `video/webm;codecs=vp8,opus` 3. `video/webm;codecs=vp9` 4. `video/webm` (兜底) ### 检查浏览器支持 ```javascript const mimes = [ 'video/webm;codecs=vp9,opus', 'video/webm;codecs=vp8,opus', 'video/webm;codecs=vp9', 'video/webm' ] mimes.forEach(m => { console.log(m, MediaRecorder.isTypeSupported(m)) }) ``` ## 性能优化建议 1. **Canvas 分辨率**:当前 1280x720,可根据需要调整 2. **数据块间隔**:当前 250ms,可调整为 500ms 减少开销 3. **音频混音**:如果不需要音频,可以禁用 AudioContext ## 下一步优化(如果问题仍存在) 1. **进一步缩短启动延迟**:从 500ms 减少到 300ms 2. **增加录制状态监控**:定期检查 MediaRecorder 状态 3. **添加降级方案**:如果本地录制失败,提示用户依赖云端录制 4. **添加手动录制控制**:让用户手动开始/停止录制 5. **使用 MutationObserver**:监听视频元素的添加,立即启动录制 ## 联系支持 如果按照本指南操作后问题仍未解决,请提供: 1. 完整的控制台日志(从通话开始到结束) 2. 浏览器版本和操作系统 3. 通话时长(秒) 4. 是否看到视频画面 5. 是否有任何错误提示 6. 数据块数量(如果有)