16、录制与回放:MediaRecorder API、服务端录制、录制文件管理、回放功能
说到多人视频会议,录制功能几乎是刚需。你想啊,会议开完了,有人迟到了,或者想回顾一下讨论的重点,总不能让人家重新开个会吧?
这一章,我们就来聊聊录制与回放。说白了,就是怎么把音视频流存下来,以及怎么再放出来。我在这块踩过不少坑,尤其是服务端录制那部分,嗯,咱们慢慢聊。
16.1 客户端录制:MediaRecorder API
先讲最简单的方案——直接在浏览器里录。MediaRecorder API 是浏览器原生支持的,不需要额外装任何东西。
它的核心逻辑很简单:你把一个 MediaStream 丢给它,它就开始录,录完给你一个 Blob。这个 Blob 你可以下载,也可以上传到服务器。
核心思路:从 PeerConnection 里拿到远程流的 MediaStream,然后喂给 MediaRecorder。
16.1.1 基本用法
我习惯这样写:
// 假设 remoteStream 是远程用户的视频流
const mediaRecorder = new MediaRecorder(remoteStream, {
mimeType: 'video/webm;codecs=vp9'
});
let recordedChunks = [];
mediaRecorder.ondataavailable = (event) => {
if (event.data.size > 0) {
recordedChunks.push(event.data);
}
};
mediaRecorder.onstop = () => {
const blob = new Blob(recordedChunks, { type: 'video/webm' });
// 这里可以下载或上传
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'meeting.webm';
a.click();
};
// 开始录制
mediaRecorder.start(1000); // 每秒触发一次 ondataavailable
这里有个细节:start(1000) 里的参数是时间片,单位毫秒。我建议设成 1000 或 2000,这样每秒钟拿到一段数据,不会因为单次数据太大导致内存爆掉。
小提示:不是所有浏览器都支持 vp9 编码。你可以先检查一下:
MediaRecorder.isTypeSupported('video/webm;codecs=vp9')
如果不支持,就降级到 vp8。
16.1.2 录制自己的画面
有时候你想同时录自己的摄像头画面和远程画面。怎么做?
我遇到过这个需求,当时用了 Canvas 混流。把两个视频流画到一个 Canvas 上,然后录 Canvas 的流。
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
// 把本地和远程视频画到 canvas 上
function drawFrame() {
ctx.drawImage(localVideo, 0, 0, 320, 240);
ctx.drawImage(remoteVideo, 320, 0, 320, 240);
requestAnimationFrame(drawFrame);
}
const canvasStream = canvas.captureStream(30); // 30fps
const recorder = new MediaRecorder(canvasStream);
recorder.start();
嗯,这里要注意性能。如果帧率太高,CPU 会扛不住。我一般设 15fps 就够用了。
16.2 服务端录制
客户端录制有个硬伤:如果用户浏览器崩了,录到一半的数据就没了。所以,正经的会议系统都会做服务端录制。
服务端录制,说白了就是把客户端的音视频流推到服务器,服务器再把它存成文件。
16.2.1 架构思路
我画了一张图,帮你理解整个流程:
你看,客户端把流推到 SFU,SFU 再转发给录制服务。录制服务拿到原始流后,编码、封装、写入存储。
16.2.2 服务端录制方案
我推荐两种方案:
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| FFmpeg 录制 | 成熟稳定,支持格式多 | 需要转码,CPU 消耗高 | 小规模会议(< 10 人) |
| 直接存储原始流 | 延迟低,CPU 消耗小 | 文件格式不通用 | 大规模会议,后续再转码 |
我个人习惯用 FFmpeg。举个例子,从 SFU 拉 RTMP 流,直接存成 MP4:
ffmpeg -i rtmp://sfu-server/live/meeting123 \
-c:v libx264 -preset ultrafast \
-c:a aac \
-f mp4 meeting123.mp4
注意:FFmpeg 的 -preset ultrafast 虽然快,但文件会大一些。如果对文件大小有要求,可以用 fast 或 medium。
16.3 录制文件管理
录完了,文件怎么管?这可不是简单丢个文件夹就完事的。
我曾经在一个项目里吃过亏——录了上千个文件,结果找起来跟大海捞针一样。后来我总结了一套管理方法:
16.3.1 文件命名规范
我建议用这种格式:
{会议ID}_{开始时间}_{结束时间}_{录制者}.{格式}
举个例子:
mtg_20231001_140000_143000_zhangsan.mp4
这样一看就知道是谁、什么时候、录了多久。
16.3.2 存储结构
我习惯按日期分目录:
/recordings/
├── 2023/
│ ├── 10/
│ │ ├── 01/
│ │ │ ├── mtg_20231001_140000_143000_zhangsan.mp4
│ │ │ └── mtg_20231001_150000_153000_lisi.mp4
│ │ └── 02/
│ └── 11/
└── 2024/
这样查找起来非常快。如果文件量特别大,还可以加一层按用户分。
16.3.3 元数据管理
光有文件名还不够。我建议把元数据存到数据库里:
CREATE TABLE recordings (
id VARCHAR(64) PRIMARY KEY,
meeting_id VARCHAR(64) NOT NULL,
user_id VARCHAR(64) NOT NULL,
start_time DATETIME NOT NULL,
end_time DATETIME,
file_path VARCHAR(512) NOT NULL,
file_size BIGINT,
duration INT,
status ENUM('recording', 'completed', 'failed')
);
这样你就可以按会议、用户、时间范围来查询了。
16.4 回放功能
回放,说白了就是把存好的文件再播出来。但这里有几个坑要注意。
16.4.1 浏览器播放
最简单的方案:直接用 HTML5 的 <video> 标签。
<video controls src="/recordings/mtg_20231001_140000_143000_zhangsan.mp4"></video>
但有个问题——如果文件很大(比如 2 小时的会议),用户得等半天才能开始播。怎么办?
我建议用 HLS 或 DASH 分片播放。把大文件切成小段,用户点开就能看,边看边加载。
16.4.2 分片转码
用 FFmpeg 把 MP4 转成 HLS:
ffmpeg -i meeting.mp4 \
-c:v libx264 -c:a aac \
-hls_time 10 \
-hls_list_size 0 \
-f hls meeting.m3u8
这样会生成一堆 .ts 文件和 .m3u8 索引文件。前端用 hls.js 来播:
const video = document.getElementById('video');
const hls = new Hls();
hls.loadSource('/recordings/meeting.m3u8');
hls.attachMedia(video);
hls.on(Hls.Events.MANIFEST_PARSED, () => {
video.play();
});
小提示:如果用户网络不好,HLS 会自动切换到低码率。这个体验比直接播 MP4 好太多了。
16.4.3 回放控制
回放不只是播放暂停。我遇到过用户想要倍速播放、跳转到特定时间点、甚至只播某一段。
这些功能 HTML5 的 <video> 基本都支持:
// 倍速播放
video.playbackRate = 1.5;
// 跳转到 5 分钟处
video.currentTime = 300;
// 只播 10-20 分钟这一段
video.addEventListener('timeupdate', () => {
if (video.currentTime > 1200) {
video.pause();
}
});
嗯,这里要注意:跳转时如果文件没加载完,会有缓冲。分片播放能缓解这个问题。
16.5 避坑指南
最后,分享几个我踩过的坑:
- 录制文件损坏:我曾经遇到过录制到一半服务器重启,文件没正常关闭,结果整个文件都打不开。解决方案:定期刷新(比如每 5 分钟生成一个分段文件)。
- 音画不同步:服务端录制时,如果音频和视频分开处理,很容易不同步。我建议用 FFmpeg 的
-copyts参数保持时间戳。 - 存储空间爆炸:一个 1 小时的 1080p 会议,大概要 1-2GB。如果不做清理,硬盘很快就满了。我建议设置自动清理策略,比如保留 30 天。
总结一下:录制与回放,核心就是「录得下来,存得规范,播得流畅」。客户端录制适合轻量场景,服务端录制适合正式系统。文件管理要规范,回放要支持分片。