359 lines
9.6 KiB
Markdown
359 lines
9.6 KiB
Markdown
# 本地录制调试指南(第二版)
|
||
|
||
## 问题描述
|
||
本地录制总是失败,`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. 数据块数量(如果有)
|