20、录制与回放:MediaRecorder API录制音视频、录制格式(webm、mp4)、录制文件上传与存储、回放录制内容
录制与回放,是WebRTC应用中非常实用的功能。我最早接触这个需求,是在做一个在线面试系统的时候——面试官要求把整个面试过程录下来,方便后续复盘。当时我第一反应是“直接用MediaRecorder不就行了?”结果踩了不少坑。今天我把这些经验整理出来,希望能帮你少走弯路。
核心知识点速览
- MediaRecorder API的基本用法
- 录制格式的选择(webm vs mp4)
- 录制文件的上传与存储策略
- 回放录制内容的实现方式
20.1 MediaRecorder API 基础用法
MediaRecorder 是浏览器原生提供的录制接口。说白了,它就是把 MediaStream 里的音视频数据,编码成我们想要的格式。我习惯把它理解成“一个管道”——一头接摄像头和麦克风,另一头输出文件片段。
先看一个最基础的录制示例:
// 获取媒体流
const stream = await navigator.mediaDevices.getUserMedia({
video: true,
audio: true
});
// 创建录制器
const recorder = new MediaRecorder(stream, {
mimeType: 'video/webm;codecs=vp9'
});
// 存储录制数据
const chunks = [];
recorder.ondataavailable = (event) => {
if (event.data.size > 0) {
chunks.push(event.data);
}
};
recorder.onstop = () => {
const blob = new Blob(chunks, { type: 'video/webm' });
// 这里blob就是完整的录制文件
};
// 开始录制,每1秒触发一次ondataavailable
recorder.start(1000);
// 5秒后停止
setTimeout(() => recorder.stop(), 5000);
我的经验:start(1000) 里的参数是 timeslice,单位毫秒。它控制着 ondataavailable 触发的频率。我个人习惯设成 1000-2000ms,这样既能及时拿到数据,又不会产生太多小片段。如果你设得太小(比如 100ms),chunks 数组会变得非常长,合并 Blob 时反而更慢。
20.2 录制格式的选择:webm vs mp4
这里有个现实问题——浏览器对录制格式的支持并不统一。我当年做项目时,产品经理说“必须支持 mp4”,结果我一查,Chrome 和 Firefox 默认只支持 webm。为什么会这样?因为 mp4 的专利授权问题,浏览器厂商不太愿意内置它的编码器。
来看一下各浏览器对录制格式的支持情况:
| 浏览器 | 支持的格式 | 备注 |
|---|---|---|
| Chrome | video/webm, video/webm;codecs=vp8, video/webm;codecs=vp9 | 支持良好 |
| Firefox | video/webm, video/webm;codecs=vp8 | vp9 支持有限 |
| Safari | video/mp4, video/mp4;codecs=h264 | 仅 Safari 原生支持 mp4 |
| Edge | video/webm, video/mp4(部分版本) | Chromium 内核后同 Chrome |
你想想看,如果用户用的是 Chrome,你硬要录 mp4,那 MediaRecorder 会直接抛异常。所以我的做法是——先检测浏览器支持哪些格式,再决定用哪个。
function getSupportedMimeType() {
const types = [
'video/webm;codecs=vp9',
'video/webm;codecs=vp8',
'video/webm',
'video/mp4;codecs=h264',
'video/mp4'
];
for (const type of types) {
if (MediaRecorder.isTypeSupported(type)) {
return type;
}
}
return ''; // 都不支持就返回空
}
const mimeType = getSupportedMimeType();
if (!mimeType) {
console.error('当前浏览器不支持任何录制格式');
return;
}
注意:我曾经遇到过一个坑——在 Safari 上检测到支持 video/mp4,但录出来的文件播放不了。后来发现是 codecs 参数没写对。Safari 要求必须指定 codecs=h264,否则虽然 isTypeSupported 返回 true,但实际录制会失败。所以检测时一定要带上具体的 codecs 参数。
20.3 录制文件的上传与存储
录完的文件怎么处理?直接存到用户本地当然可以,但大多数场景下我们需要上传到服务器。这里有两个关键点:一是文件格式转换,二是上传策略。
文件上传的基本流程:
// 录制完成后,将blob上传
recorder.onstop = async () => {
const blob = new Blob(chunks, { type: mimeType });
// 方式1:直接上传整个blob
const formData = new FormData();
formData.append('file', blob, `recording_${Date.now()}.webm`);
try {
const response = await fetch('/upload', {
method: 'POST',
body: formData
});
console.log('上传成功', response);
} catch (err) {
console.error('上传失败', err);
}
};
但如果录制时间很长(比如超过10分钟),文件会非常大。直接上传整个 Blob 有两个问题:一是内存占用高,二是网络中断就得重传。我建议用分片上传。
// 分片上传示例
const CHUNK_SIZE = 5 * 1024 * 1024; // 5MB一片
async function uploadInChunks(blob, fileName) {
const totalChunks = Math.ceil(blob.size / CHUNK_SIZE);
let start = 0;
for (let i = 0; i < totalChunks; i++) {
const end = Math.min(start + CHUNK_SIZE, blob.size);
const chunk = blob.slice(start, end);
const formData = new FormData();
formData.append('chunk', chunk);
formData.append('index', i);
formData.append('total', totalChunks);
formData.append('fileName', fileName);
await fetch('/upload-chunk', {
method: 'POST',
body: formData
});
start = end;
// 可以在这里更新上传进度
const progress = ((i + 1) / totalChunks * 100).toFixed(0);
console.log(`上传进度: ${progress}%`);
}
// 通知服务端合并分片
await fetch('/merge-chunks', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ fileName })
});
}
我的经验:分片上传时,每片上传完成后最好加一个简单的重试机制。网络波动是常态,我曾经遇到过上传到第8片时断了,结果整个文件废了。加个重试3次的逻辑,能显著提高成功率。
服务端存储方面,我推荐用对象存储(比如阿里云OSS、AWS S3),而不是直接存到应用服务器的磁盘上。原因很简单:视频文件越来越大,磁盘很快会满,而且对象存储自带CDN加速,回放时加载更快。
20.4 回放录制内容
回放其实是最简单的部分。拿到 Blob 后,用 URL.createObjectURL 生成一个临时 URL,然后赋值给 video 元素的 src 属性就行了。
// 回放录制内容
function playRecording(blob) {
const video = document.getElementById('playback');
const url = URL.createObjectURL(blob);
video.src = url;
video.controls = true;
video.play();
// 播放结束后释放内存
video.onended = () => {
URL.revokeObjectURL(url);
};
}
// 使用示例
recorder.onstop = () => {
const blob = new Blob(chunks, { type: mimeType });
playRecording(blob);
};
这里有个细节要注意——URL.createObjectURL 创建的 URL 会占用内存。如果用户反复录制和回放,不释放的话内存会越涨越高。我习惯在视频播放结束后立即调用 revokeObjectURL,或者在组件卸载时统一清理。
回放时的常见问题:
- 黑屏:检查 mimeType 是否与录制时一致。不一致会导致解码失败。
- 没有声音:录制时确保 audio: true,并且麦克风权限已授予。
- 播放卡顿:可能是 timeslice 设得太小,导致 chunks 过多,合并后的 Blob 索引有问题。试试增大 timeslice。
- Safari 上无法播放 webm:Safari 不支持 webm,需要转码。可以在服务端用 FFmpeg 转成 mp4。
嗯,说到转码,如果你一定要在 Safari 上回放 webm,可以在服务端做转码。我之前的项目就是这么干的——前端录 webm,上传后服务端用 FFmpeg 转成 mp4,再返回给前端播放。虽然多了一步,但兼容性最好。
最后提一句,录制文件的命名最好带上时间戳和用户ID,方便后续检索。比如 recording_20250101_张三_面试.webm。这个习惯帮我省了不少找文件的功夫。
小技巧:如果你想让用户能下载录制文件,可以加一个下载按钮:
const a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = 'recording.webm';
a.click();
录制与回放这块,说白了就是“录得下来、存得上去、放得出来”。技术本身不复杂,但细节很多。你只要把格式检测、分片上传、内存释放这几个点把握好,基本不会出大问题。