一、WebRTC 录制与回放:MediaRecorder API、存储与回放技术
大家好,我是你们的老朋友。今天我们来聊聊 WebRTC 里一个非常实用的功能——录制与回放。说实话,很多同学做 P2P 通信时,总觉得能连上、能通话就完事了。但实际项目中,用户经常问:「刚才那段视频能录下来吗?」「我想回看之前的会议。」这时候,你就得掏出 MediaRecorder 这把利器了。
我个人习惯把录制回放分成三个环节:采集→存储→回放。听起来简单,但每个环节都有坑。咱们一个一个说。
核心要点:MediaRecorder 负责将 MediaStream 编码成容器格式(如 WebM、MP4),存储层负责持久化,回放层负责解码与渲染。三者缺一不可。
1.1 MediaRecorder API 基础
MediaRecorder 是浏览器原生提供的录制接口。你只要给它一个 MediaStream,它就能把音视频数据一段一段吐出来。我在项目中遇到过不少同学直接用默认参数,结果录出来的文件要么没声音,要么花屏。嗯,这里要注意参数配置。
基本用法如下:
// 获取本地媒体流
const stream = await navigator.mediaDevices.getUserMedia({
video: true,
audio: true
});
// 创建 MediaRecorder 实例
const recorder = new MediaRecorder(stream, {
mimeType: 'video/webm;codecs=vp9,opus',
videoBitsPerSecond: 2500000, // 2.5 Mbps
audioBitsPerSecond: 128000 // 128 kbps
});
// 收集数据块
const chunks = [];
recorder.ondataavailable = (event) => {
if (event.data.size > 0) {
chunks.push(event.data);
}
};
// 开始录制,每 1 秒触发一次 ondataavailable
recorder.start(1000);
// 停止录制
recorder.onstop = () => {
const blob = new Blob(chunks, { type: recorder.mimeType });
// 接下来可以保存或上传这个 blob
};
这里有个关键点:mimeType 的选择。不同浏览器支持的编码格式不一样。Chrome 偏好 WebM + VP9,Safari 更倾向 MP4 + H.264。我曾经在跨浏览器测试时踩过坑——在 Safari 上直接用了 WebM,结果录出来的文件播放不了。所以建议先检测支持情况:
function getSupportedMimeType() {
const types = [
'video/webm;codecs=vp9,opus',
'video/webm;codecs=vp8,opus',
'video/mp4;codecs=h264,aac',
'video/x-matroska;codecs=h264,aac'
];
for (const type of types) {
if (MediaRecorder.isTypeSupported(type)) {
return type;
}
}
return ''; // 回退到浏览器默认
}
小技巧:录制时建议把 timeslice 参数设成 1000ms 左右。太短会导致大量小文件块,太长又容易丢数据。1000ms 是个不错的平衡点。
1.2 录制文件存储
录完的数据怎么存?这得看你的应用场景。如果是本地录制,直接保存到磁盘就行。如果是远程会议录制,通常要上传到服务器。
本地存储方案:
// 利用 URL.createObjectURL 生成下载链接
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `recording-${Date.now()}.webm`;
a.click();
URL.revokeObjectURL(url); // 释放内存
但要注意,createObjectURL 会占用内存。如果录制文件很大(比如 1GB 以上),浏览器可能会崩溃。我建议大文件用 FileReader 分片读取,或者直接用 IndexedDB 存储。
服务器存储方案:
用 FormData 上传是最直接的方式:
const formData = new FormData();
formData.append('file', blob, 'recording.webm');
fetch('/upload', {
method: 'POST',
body: formData
}).then(response => {
console.log('上传成功');
});
不过,如果录制时间很长(比如 1 小时),一次性上传整个 blob 会超时。更好的做法是边录边传:
recorder.ondataavailable = (event) => {
if (event.data.size > 0) {
// 直接上传每个数据块
uploadChunk(event.data);
}
};
function uploadChunk(chunk) {
const formData = new FormData();
formData.append('chunk', chunk);
formData.append('sessionId', sessionId);
formData.append('index', chunkIndex++);
fetch('/upload-chunk', { method: 'POST', body: formData });
}
服务器端收到所有分片后,再按顺序合并成完整文件。这样即使网络中断,也只需要重传丢失的分片。
注意:分片上传时一定要带上序号和会话 ID,否则服务器没法正确拼接。我曾经因为忘了加序号,结果回放时画面顺序全乱了,用户投诉说「你们录的是鬼片吗?」
1.3 回放技术
回放说白了就是把录好的文件塞进 <video> 标签里。但这里有几个细节要注意。
基础回放:
<video id="playback" controls autoplay muted></video>
<script>
const video = document.getElementById('playback');
const url = URL.createObjectURL(blob);
video.src = url;
</script>
嗯,看起来很简单对吧?但实际项目中,你可能会遇到这些问题:
- 格式不兼容: Safari 不支持 WebM,Chrome 对 MP4 支持有限。解决方案是录制时同时生成两种格式,或者用 FFmpeg 在服务端转码。
- 大文件加载慢: 几百 MB 的视频直接加载会卡死页面。建议用
MediaSource分片加载,或者用 HLS/DASH 流式播放。 - 音视频不同步: 这通常是因为录制时时间戳不准。检查一下
timeslice是否稳定,或者用requestVideoFrameCallback做同步。
进阶回放——使用 MediaSource:
const mediaSource = new MediaSource();
video.src = URL.createObjectURL(mediaSource);
mediaSource.onsourceopen = () => {
const sourceBuffer = mediaSource.addSourceBuffer('video/webm;codecs=vp9,opus');
// 分片追加数据
function appendChunk(chunk) {
if (!sourceBuffer.updating) {
sourceBuffer.appendBuffer(chunk);
} else {
// 等待上一个追加完成
sourceBuffer.onupdateend = () => sourceBuffer.appendBuffer(chunk);
}
}
// 从服务器拉取分片
fetchChunks().then(chunks => {
chunks.forEach(appendChunk);
});
};
这样做的好处是:用户可以边下载边看,不用等整个文件下载完。而且支持 seek 到任意位置(前提是服务器支持 Range 请求)。
个人经验:回放时一定要加 muted 属性,否则浏览器可能会因为自动播放策略而拒绝播放。等用户交互后再取消静音。
1.4 整体架构图
下面这张图展示了录制与回放的完整流程。我把它画成了 SVG,方便你理解各个环节的关系。
1.5 常见问题与避坑指南
最后,我总结几个实际项目中容易踩的坑,希望能帮你少走弯路。
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 录制文件无声 | 音频轨道未启用或编码不匹配 | 检查 getUserMedia 的 audio 参数,确保 mimeType 包含音频编码 |
| 回放时画面卡顿 | 文件过大或网络带宽不足 | 使用 MediaSource 分片加载,或降低录制码率 |
| 跨浏览器不兼容 | 不同浏览器支持的编码格式不同 | 用 MediaRecorder.isTypeSupported 检测,准备多种格式 |
| 录制过程中内存溢出 | 长时间录制未及时清理数据块 | 边录边传,或定期清理已处理的数据块 |
| 音视频不同步 | 时间戳漂移或 timeslice 不稳定 |
使用 requestVideoFrameCallback 做同步校准 |
曾经踩过的坑:有一次我在录制远程桌面时,发现录出来的视频只有前 10 秒正常,后面全是黑屏。排查了半天,原来是 RTCPeerConnection 的 ontrack 事件在录制过程中被重新触发了,导致 MediaRecorder 的输入流被替换。解决方案是在录制开始后锁定轨道,或者用 clone() 方法复制一份流。
好了,关于录制与回放的核心内容就这些。你想想看,其实原理并不复杂,关键是要把每个环节的细节处理好。从采集到存储再到回放,每一步都有它的讲究。希望今天的分享能帮你在实际项目中少踩几个坑。