8、媒体流录制:MediaRecorder API介绍、录制音视频流、获取录制数据(Blob)

好,咱们今天聊点实在的。前面几章我们把摄像头、麦克风、屏幕共享都折腾了一遍,拿到了 MediaStream。但有个问题——你拿到了流,然后呢?总得存下来吧?

我记得我第一次做 WebRTC 录制功能时,天真地以为浏览器会自带一个「保存视频」的按钮。结果找了半天,发现得自己动手。嗯,MediaRecorder 就是干这个的。

8.1 MediaRecorder 是什么?

说白了,MediaRecorder 是浏览器提供的一个原生 API。它能把 MediaStream 里的音视频数据,实时编码成你想要的格式,然后一段一段吐给你。

你想想看,如果没有它,你要自己处理音视频编码、分片、存储……那工作量可就大了去了。MediaRecorder 把这些脏活累活都包了。

核心要点: MediaRecorder 接收一个 MediaStream,输出 Blob 数据块。

8.2 基本用法

用法其实很简单。三步走:

  1. 创建 MediaRecorder 实例,传入 MediaStream
  2. 监听 dataavailable 事件,拿到数据块
  3. 调用 start() 开始录制,stop() 停止录制

来,直接看代码:

// 假设 stream 是之前获取到的 MediaStream
const mediaRecorder = new MediaRecorder(stream, {
  mimeType: 'video/webm;codecs=vp9'
});

let recordedChunks = [];

mediaRecorder.ondataavailable = function(event) {
  if (event.data.size > 0) {
    recordedChunks.push(event.data);
  }
};

mediaRecorder.onstop = function() {
  // 所有数据收集完毕,生成最终的 Blob
  const blob = new Blob(recordedChunks, {
    type: 'video/webm'
  });
  // 接下来你可以下载或上传这个 blob
};

// 开始录制,每 1000ms 触发一次 dataavailable
mediaRecorder.start(1000);

这里有个细节我提一下:start(1000) 里的参数是 timeslice,单位毫秒。它告诉浏览器每隔多久给你一次数据。我个人习惯设 1000,也就是每秒一个数据块。这样即使录制中途崩溃,你至少能拿到前面几秒的数据。

小技巧: 如果不传 timeslice,dataavailable 只会在 stop() 时触发一次。那样的话,如果录制时间很长,内存压力会很大。

8.3 支持的格式

不是所有浏览器都支持所有格式。我踩过这个坑。你得先检查一下:

// 检查浏览器支持的录制格式
function getSupportedMimeTypes() {
  const types = [
    'video/webm;codecs=vp9',
    'video/webm;codecs=vp8',
    'video/webm;codecs=h264',
    'video/mp4',
    'audio/webm'
  ];
  return types.filter(type => MediaRecorder.isTypeSupported(type));
}

console.log('支持的格式:', getSupportedMimeTypes());

我曾经在 Chrome 上测试得好好的,换到 Firefox 发现 h264 不支持。所以,上线前一定要做兼容性检查。

浏览器 支持的常见格式
Chrome video/webm (vp8/vp9), audio/webm
Firefox video/webm (vp8/vp9), audio/webm
Safari video/mp4 (h264), audio/mp4
Edge video/webm (vp8/vp9), video/mp4

8.4 录制状态管理

MediaRecorder 有几种状态:inactive、recording、paused。你可以通过 state 属性查看。

console.log(mediaRecorder.state); // 'inactive' | 'recording' | 'paused'

// 暂停录制
mediaRecorder.pause();

// 恢复录制
mediaRecorder.resume();

// 停止录制
mediaRecorder.stop();

嗯,这里要注意:pause() 和 resume() 不会触发 dataavailable。只有 start() 和 stop() 才会。我刚开始用的时候以为暂停也会吐数据,结果 debug 了半天。

8.5 获取录制数据:Blob 对象

录制完成后,我们拿到的是一个 Blob。Blob 是二进制大对象,你可以把它转成 URL 下载,或者用 FormData 上传到服务器。

// 生成下载链接
function downloadBlob(blob, filename) {
  const url = URL.createObjectURL(blob);
  const a = document.createElement('a');
  a.href = url;
  a.download = filename || 'recording.webm';
  a.click();
  URL.revokeObjectURL(url); // 释放内存
}

// 上传到服务器
function uploadBlob(blob) {
  const formData = new FormData();
  formData.append('video', blob, 'recording.webm');
  
  fetch('/upload', {
    method: 'POST',
    body: formData
  }).then(response => {
    console.log('上传成功');
  });
}
注意: 使用 URL.createObjectURL 后,一定要在不需要时调用 revokeObjectURL 释放内存。否则,随着录制次数增多,内存会一直涨。

8.6 完整示例:录制并下载

把上面所有知识点串起来,就是一个完整的录制功能:

async function startRecording() {
  const stream = await navigator.mediaDevices.getUserMedia({
    video: true,
    audio: true
  });

  const mimeType = MediaRecorder.isTypeSupported('video/webm;codecs=vp9')
    ? 'video/webm;codecs=vp9'
    : 'video/webm';

  const mediaRecorder = new MediaRecorder(stream, { mimeType });
  let chunks = [];

  mediaRecorder.ondataavailable = (e) => {
    if (e.data.size > 0) chunks.push(e.data);
  };

  mediaRecorder.onstop = () => {
    const blob = new Blob(chunks, { type: mimeType });
    downloadBlob(blob, `recording-${Date.now()}.webm`);
    stream.getTracks().forEach(track => track.stop());
  };

  mediaRecorder.start(1000);
  console.log('录制中...');

  // 5秒后自动停止
  setTimeout(() => {
    mediaRecorder.stop();
    console.log('录制结束');
  }, 5000);
}

8.7 核心流程示意图

下面这张图,把整个录制流程串起来了。你看一眼就能明白数据是怎么流动的:

MediaRecorder 录制流程 获取 MediaStream getUserMedia / getDisplayMedia new MediaRecorder 指定 mimeType start(timeslice) 开始录制 ondataavailable 收集 Blob 数据块 循环 new Blob(chunks) 合并为完整文件 下载 / 上传 URL.createObjectURL

8.8 避坑指南

最后,分享几个我踩过的坑:

  • 格式不兼容: 我曾经在 Safari 上录视频,默认用了 webm,结果 Safari 不支持。后来加了 isTypeSupported 检查才解决。
  • 内存泄漏: 录制长时间视频时,如果 chunks 数组一直不释放,内存会暴涨。建议每收集到一定数量的数据块,就合并一次并清空数组。
  • 音频缺失: 有时候 getUserMedia 获取的流没有音频轨道,但录制时你指定了 audio: true。MediaRecorder 不会报错,但录出来的视频没声音。记得先检查 track 是否存在。
  • 停止后释放资源: 录制结束后,一定要调用 stream.getTracks().forEach(track => track.stop()),否则摄像头和麦克风会一直亮着。
我的习惯: 录制前先打印一下 MediaRecorder.isTypeSupported 的结果,确认当前浏览器支持什么格式。这个小动作能省去很多调试时间。

好了,MediaRecorder 的核心内容就这些。说白了就是三步:拿流、录数据、存 Blob。你动手试一遍,比看十遍文档都管用。

公众号:蓝海资料掘金营,微信deep3321