18、录制与回放:MediaRecorder录制、服务器端录制、录制文件存储、回放功能实现。
说到WebRTC的录制与回放,我估计不少朋友第一反应就是「录个屏嘛,简单」。嗯,真上手做的时候,坑还是挺多的。我自己最早做这个功能时,就踩过存储格式不兼容的雷,也遇到过录制文件大到浏览器直接崩溃的情况。
今天咱们就把这块彻底捋清楚。从浏览器端的MediaRecorder,到服务器端的录制方案,再到文件怎么存、怎么回放,一条线走通。
18.1 浏览器端录制:MediaRecorder
MediaRecorder是浏览器原生提供的API。说白了,它能把MediaStream直接转成视频文件。你想想看,我们做视频通话时,对方的画面和声音都在一个MediaStream里,用MediaRecorder一接,就能录下来。
核心思路:把WebRTC的远程流(remoteStream)塞进MediaRecorder,然后监听它的dataavailable事件,拿到数据块。
我个人的习惯是,先判断浏览器是否支持MediaRecorder:
if (!window.MediaRecorder) {
alert('您的浏览器不支持录制功能,建议使用Chrome或Firefox');
return;
}
然后创建录制器实例:
const mediaRecorder = new MediaRecorder(remoteStream, {
mimeType: 'video/webm;codecs=vp9'
});
这里有个坑——mimeType不是所有浏览器都支持。我曾经在Safari上试过vp9,直接报错。后来学乖了,先检测一下:
const mimeTypes = [
'video/webm;codecs=vp9',
'video/webm;codecs=vp8',
'video/webm'
];
let selectedMimeType = '';
for (const type of mimeTypes) {
if (MediaRecorder.isTypeSupported(type)) {
selectedMimeType = type;
break;
}
}
录制过程中,数据是一段一段来的。每过一段时间(比如100毫秒),MediaRecorder就会触发dataavailable事件:
const chunks = [];
mediaRecorder.ondataavailable = (event) => {
if (event.data.size > 0) {
chunks.push(event.data);
}
};
录制结束时,把这些chunks合并成一个Blob:
mediaRecorder.onstop = () => {
const blob = new Blob(chunks, { type: selectedMimeType });
// 接下来就可以下载或上传了
};
小技巧:录制过程中如果想暂停,可以用mediaRecorder.pause()和mediaRecorder.resume()。我做过一个功能,用户点击暂停后,画面定格,声音也停录,再点继续接着录——体验比直接stop好很多。
18.2 服务器端录制
浏览器端录制有个硬伤——页面一关,录制就没了。所以真正生产环境里,服务器端录制才是主流。
服务器端怎么录?思路其实不复杂:把WebRTC的媒体流通过SFU(比如Janus、Mediasoup)转发到服务器,服务器再用FFmpeg或者GStreamer去转码存储。
我参与过一个项目,用的是Mediasoup做SFU。服务器端收到RTP包后,直接喂给FFmpeg:
ffmpeg -i rtp://0.0.0.0:5004 -c copy output.webm
但说实话,这种方式对服务器压力不小。每个房间、每个用户都开一个FFmpeg进程,内存和CPU很快就吃满了。
后来我们换了个方案——先录成原始格式,再异步转码。录制时只存原始的RTP包或H264裸流,等通话结束后,后台任务再去转成MP4或WebM。这样录制过程几乎不消耗CPU。
注意:服务器端录制一定要处理好音视频同步。RTP包里有时间戳,但不同源的时钟可能不同步。我遇到过音频比视频快半秒的情况,排查了好久才发现是时间戳换算时精度丢了。
18.3 录制文件存储
录好的文件放哪儿?这得看你的场景。
| 存储方式 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| 本地磁盘 | 开发测试、小规模部署 | 简单直接,无额外成本 | 扩展性差,容易占满磁盘 |
| 对象存储(OSS/S3) | 生产环境、大规模部署 | 弹性扩展,支持CDN分发 | 需要额外费用,上传有延迟 |
| 分布式文件系统 | 企业内部、高可用场景 | 高可靠,低延迟 | 运维复杂 |
我个人比较推荐对象存储。原因很简单——录制文件通常需要回放,而回放时最好走CDN。对象存储天然支持CDN加速,用户回放时加载快,服务器带宽压力也小。
上传到OSS的代码示例(Node.js):
const OSS = require('ali-oss');
const client = new OSS({
region: 'oss-cn-hangzhou',
accessKeyId: '你的AK',
accessKeySecret: '你的SK',
bucket: 'my-recording-bucket'
});
async function uploadRecording(blob, roomId, userId) {
const fileName = `${roomId}/${userId}_${Date.now()}.webm`;
const buffer = Buffer.from(await blob.arrayBuffer());
const result = await client.put(fileName, buffer);
return result.url; // 返回可访问的URL
}
建议:文件名里带上房间ID和时间戳,方便后续检索。我见过有人直接用随机字符串命名,结果回放时根本找不到对应的录制文件。
18.4 回放功能实现
回放说白了就是把录好的文件用video标签播出来。但这里有几个细节要注意。
首先,格式兼容性。WebM在Chrome和Firefox上没问题,但Safari不支持。所以服务器端转码时,最好同时生成一个MP4版本。或者用HLS协议,兼容性最好。
其次,进度控制。用户可能需要拖动进度条、快进快退。这要求录制文件有完整的元数据(时长、关键帧索引)。如果录制时没处理好,回放时可能拖不动。
我做过一个回放组件,核心逻辑是这样的:
<video id="playback" controls preload="metadata">
<source src="https://cdn.example.com/recording.webm" type="video/webm">
<source src="https://cdn.example.com/recording.mp4" type="video/mp4">
您的浏览器不支持视频回放
</video>
但光有video标签还不够。用户可能需要只看某一段,或者倍速播放。这时候就得自己封装控制逻辑了:
const video = document.getElementById('playback');
// 倍速播放
video.playbackRate = 1.5;
// 跳转到30秒位置
video.currentTime = 30;
// 监听播放结束
video.onended = () => {
console.log('回放完成');
};
重点:回放时一定要处理好音视频同步。如果录制时音视频不同步,回放时问题会更明显。我建议在录制阶段就做好时间戳校准,不要指望回放时再去修正。
18.5 整体流程梳理
说了这么多,咱们用一张图把整个流程串起来:
从图上能看出来,整个链路其实就三步:采集 → 存储 → 回放。但每一步都有不少细节。我个人觉得,最容易出问题的是存储和转码之间的衔接——文件还没转完就开始回放,用户看到的就是一个打不开的视频。
解决办法也简单:加一个状态标记。录制完成后先标记为「转码中」,转码完成后再标记为「已就绪」。回放时只展示已就绪的文件。
最后一个小建议:不管用哪种录制方案,一定要做录制文件的完整性校验。我遇到过录制过程中网络闪断,文件只有一半的情况。加个MD5校验或者文件大小检查,能省去很多排查时间。
公众号:蓝海资料掘金营,微信deep3321