录制错误处理:常见录制错误类型、设备被占用处理、录制中断恢复、用户权限撤销处理
做 WebRTC 录制,说白了就是在跟各种意外打交道。
我刚开始做这个功能时,以为核心逻辑写好了就万事大吉。结果上线第一天,用户反馈就炸了——有的录到一半没画面,有的麦克风没声音,有的干脆点开始录制就报错。嗯,那时候我才意识到,错误处理才是录制的重头戏。
常见录制错误类型
先说说我遇到最多的几类错误。你想想看,录制本质上是在操作两样东西:媒体流和文件写入。所以错误也基本围绕这两块展开。
| 错误类型 | 典型场景 | 错误名称 |
|---|---|---|
| 设备不可用 | 摄像头被拔掉、麦克风驱动异常 | NotReadableError |
| 权限拒绝 | 用户点了「禁止」、浏览器设置拦截 | NotAllowedError |
| 设备被占用 | 另一个标签页正在使用摄像头 | TrackStartError |
| 录制中断 | 网络抖动导致 MediaRecorder 停止 | MediaRecorder 的 onerror |
| 文件写入失败 | 磁盘空间不足、浏览器沙箱限制 | Blob 创建异常 |
我个人习惯把错误分成两类:可恢复的和不可恢复的。比如权限拒绝,用户重新授权就能解决,属于可恢复。而设备硬件损坏,那就只能提示用户换设备了。
设备被占用处理
这个坑我踩过好几次。用户同时打开两个标签页,都调用了摄像头,第二个标签页就会报 TrackStartError。浏览器不会帮你排队,它直接拒绝。
核心思路:捕获到设备占用错误后,引导用户关闭其他占用标签页,或者释放设备。
代码里怎么判断?看错误名称就行:
try {
const stream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true });
// 开始录制
} catch (err) {
if (err.name === 'TrackStartError') {
// 设备被占用
showToast('摄像头或麦克风被其他应用占用,请关闭后重试');
} else if (err.name === 'NotReadableError') {
// 设备不可读
showToast('设备出现硬件问题,请检查连接');
}
}
我曾经在项目里加了一个「检测设备状态」的按钮。用户点击后,我们先用 enumerateDevices() 列出所有设备,再逐个尝试获取流。如果某个设备返回 TrackStartError,就标记为「被占用」,在界面上用红色标签显示。这样用户不用猜,一眼就知道哪个设备出了问题。
小技巧:调用 getUserMedia 之前,先调用 enumerateDevices() 检查设备 label 是否为空。如果 label 为空,说明用户还没授权,这时候直接请求权限,而不是报设备占用。
录制中断恢复
录制中断是最让用户崩溃的场景。录了半小时,突然断了,什么都没保存。我遇到过最典型的情况是:用户手机内存不足,浏览器把录制进程杀了。
MediaRecorder 有一个 onerror 事件,但说实话,它给的信息很有限。我建议你主要监听 ondataavailable 和 onstop。
let recordedChunks = [];
mediaRecorder.ondataavailable = (event) => {
if (event.data.size > 0) {
recordedChunks.push(event.data);
// 每收到一段数据,就存一次临时文件
saveTempChunk(event.data);
}
};
mediaRecorder.onstop = () => {
if (recordedChunks.length === 0) {
// 录制中断,没有有效数据
showToast('录制意外中断,请检查设备状态');
// 尝试恢复:重新获取流,从上次断点续录
attemptRecovery();
} else {
// 正常结束,合并数据
const blob = new Blob(recordedChunks, { type: 'video/webm' });
saveFinalFile(blob);
}
};
这里有个关键点:分段保存。我习惯设置 timeslice 参数,比如每 5 秒触发一次 ondataavailable。这样即使录制中断,我们也至少保存了前几秒的数据,用户不会一无所获。
注意:分段保存会增加磁盘写入频率,对移动端不太友好。建议在桌面端用 5 秒间隔,移动端用 10 秒。我踩过坑,移动端写入太频繁会导致浏览器卡死。
如果真的发生了中断,怎么恢复?我的做法是:
- 保存当前已录制的所有 chunk 到 IndexedDB
- 重新调用
getUserMedia获取新流 - 创建新的 MediaRecorder,继续录制
- 录制结束后,把新旧 chunk 合并成一个 Blob
说白了,就是手动实现一个「断点续录」。虽然不能做到完全无缝,但至少比全部丢失强得多。
用户权限撤销处理
这个场景比较隐蔽。用户一开始给了权限,录到一半突然去浏览器设置里把摄像头权限关了。这时候你的 MediaRecorder 不会立刻报错,但 getUserMedia 返回的 MediaStream 会变成 inactive 状态。
怎么检测?监听 track.onended 事件:
stream.getTracks().forEach(track => {
track.onended = () => {
// 权限被撤销,或者设备被拔出
console.warn('Track ended unexpectedly:', track.kind);
mediaRecorder.stop();
showToast('录制已停止:摄像头或麦克风权限被撤销');
// 释放资源
stream.getTracks().forEach(t => t.stop());
};
});
我曾经遇到一个奇葩情况:用户用的是外接 USB 摄像头,录到一半不小心踢到了 USB 线。摄像头断开,track.onended 触发,但用户重新插上后,浏览器不会自动恢复录制。所以我在界面上加了一个「恢复录制」按钮,用户重新插好设备后,点击按钮就能继续。
最佳实践:权限撤销后,不要立刻清空已录制的数据。把数据暂存起来,等用户重新授权后,尝试合并。如果用户拒绝重新授权,再提示下载已录制的部分。
知识体系总览
下面这张图是我自己梳理的录制错误处理流程,你可以对照着看:
这张图把整个流程串起来了。从开始录制到获取设备,再到错误分支和录制中断恢复,每一步都有对应的处理策略。我个人建议你把这张图打印出来贴在工位上,写代码时对照着看,不容易漏掉边界情况。
我的习惯:每次上线前,我会手动模拟三种错误场景:拔掉摄像头、关闭权限、杀掉录制进程。确保每个错误都有对应的 UI 提示和恢复路径。别嫌麻烦,上线后少挨骂。
录制错误处理这块,说白了就是「防患于未然」加上「亡羊补牢」。把能想到的异常都提前处理了,用户遇到问题时至少不会一脸懵。嗯,今天就聊到这儿,下次我们聊聊录制的性能优化。