MediaRecorder 入门:从零开始录制浏览器音视频

说实话,我第一次接触 MediaRecorder 的时候,心里还挺没底的。那时候项目要求做一个浏览器端的录音功能,我第一反应是「这玩意儿浏览器能搞定?」。结果翻了一下 MDN 文档,发现 MediaRecorder 这个 API 早在 2016 年左右就已经被各大浏览器支持了。嗯,是我孤陋寡闻了。

这一章,我们就来聊聊 MediaRecorder 到底是什么,它能干什么,以及怎么上手用起来。我会把我踩过的坑、总结的经验一并分享给你。

MediaRecorder API 概述

MediaRecorder 是 WebRTC 生态里一个非常重要的接口。它的作用很简单:把从摄像头、麦克风或者屏幕捕获到的媒体流,编码成文件。说白了,就是帮你把实时流「录下来」。

你想想看,如果没有 MediaRecorder,我们要实现浏览器端录制,就得靠 Flash 或者后端转码,那体验简直不敢想。现在有了它,前端自己就能搞定录制、暂停、恢复、停止这一整套流程。

核心要点:MediaRecorder 接收一个 MediaStream 对象,输出一个 Blob 文件。中间的过程——编码、封装、分片——全部由浏览器底层完成,我们只需要调用几个方法就行。

我个人习惯把 MediaRecorder 理解成一个「管道」:

  • 入口:MediaStream(来自 getUserMedia 或 getDisplayMedia)
  • 管道本身:MediaRecorder 实例
  • 出口:Blob 数据(通过 dataavailable 事件获取)

这个模型一旦建立起来,后面所有操作都会变得很清晰。

支持的编码格式与 MIME 类型

这里有个坑,我必须先跟你说清楚:不同浏览器支持的编码格式是不一样的。我在项目中遇到过,Chrome 上跑得好好的录制代码,换到 Firefox 上直接报错。原因就是 MIME 类型不兼容。

常见的 MIME 类型有这些:

MIME 类型 说明 浏览器支持情况
video/webm WebM 容器,VP8/VP9 编码 Chrome、Firefox、Edge 均支持
video/webm;codecs=vp9 指定 VP9 编码,压缩率更高 Chrome 支持较好,Firefox 部分支持
video/mp4 MP4 容器,H.264 编码 仅部分浏览器(如 Safari)支持
audio/webm 纯音频 WebM,Opus 编码 Chrome、Firefox 均支持
audio/wav WAV 格式,PCM 编码 支持有限,不建议用于录制

注意:不要硬编码 MIME 类型!我曾经在代码里直接写了 'video/webm;codecs=vp9',结果用户用 Safari 打开,直接抛异常。正确的做法是先调用 MediaRecorder.isTypeSupported() 检测一下。

检测方法很简单:

const mimeTypes = [
  'video/webm;codecs=vp9',
  'video/webm;codecs=vp8',
  'video/webm',
  'audio/webm'
];

const supported = mimeTypes.filter(type => 
  MediaRecorder.isTypeSupported(type)
);

console.log('当前浏览器支持的格式:', supported);

我个人建议,优先使用 video/webm;codecs=vp8 作为保底方案。因为 VP8 的兼容性最好,几乎所有现代浏览器都支持。如果你追求更高的压缩率,再尝试 VP9。

创建 MediaRecorder 实例

创建实例其实就一行代码,但有几个参数值得细说。

const mediaRecorder = new MediaRecorder(stream, {
  mimeType: 'video/webm;codecs=vp8',
  audioBitsPerSecond: 128000,
  videoBitsPerSecond: 2500000
});

第一个参数是 MediaStream,这个不用多说。第二个参数是配置对象,我重点讲两个:

  • mimeType:指定编码格式。如果不传,浏览器会使用默认格式。但我建议你显式指定,否则不同浏览器行为不一致,调试起来很头疼。
  • bitsPerSecond:控制码率。音频一般 128kbps 就够用,视频的话 2.5Mbps 是个不错的起点。码率设得太低画面会糊,设得太高文件又太大。

小技巧:如果你不确定该设多少码率,可以先用默认值跑一遍,看看文件大小和画质,再逐步调整。我在做录屏工具时就是这么干的,先跑一轮测试,再根据用户反馈微调参数。

创建实例时还有一个容易忽略的点:stream 必须是活跃的。如果摄像头已经被关闭,或者 track 已经 ended,new MediaRecorder 会直接抛出一个 InvalidStateError。所以创建实例前,最好检查一下 stream 的状态。

录制状态管理

MediaRecorder 有四种状态,理解它们对调试非常重要:

  • inactive:初始状态,还没开始录制
  • recording:正在录制中
  • paused:已暂停录制
  • error:发生错误

你可以通过 mediaRecorder.state 随时查看当前状态。状态之间的转换关系是这样的:

inactive → recording  (调用 start())
recording → paused     (调用 pause())
paused    → recording  (调用 resume())
recording → inactive   (调用 stop())
paused    → inactive   (调用 stop())

嗯,这里要注意:从 paused 状态调用 stop() 是合法的,它会直接结束录制,而不是先恢复再停止。我刚开始用的时候以为必须 resume 之后才能 stop,结果代码里多写了一步,浪费了不少时间。

状态变化会触发对应的事件:

  • onstart:开始录制时触发
  • onpause:暂停时触发
  • onresume:恢复时触发
  • onstop:停止时触发
  • onerror:出错时触发

一个完整的录制流程大概是这样的:

const mediaRecorder = new MediaRecorder(stream);

mediaRecorder.ondataavailable = (event) => {
  if (event.data.size > 0) {
    // 这里拿到录制的数据块
    chunks.push(event.data);
  }
};

mediaRecorder.onstop = () => {
  const blob = new Blob(chunks, { type: mediaRecorder.mimeType });
  // 现在你可以下载或者上传这个 blob 了
};

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

这里有个细节:start(1000) 里的参数是 timeslice,单位毫秒。它决定了 dataavailable 事件触发的频率。如果不传这个参数,dataavailable 只会在 stop() 时触发一次。我个人习惯传一个 1000 或 2000,这样录制过程中就能拿到中间数据,方便做实时预览或者分段上传。

重要提醒:录制过程中一定要监听 dataavailable 事件,把数据块存起来。否则 stop 之后你什么都拿不到。这个坑我踩过,当时 debug 了半天才发现是忘了加事件监听。

本章知识体系

为了让你更直观地理解 MediaRecorder 的整体结构,我画了一张流程图:

MediaRecorder 核心流程 MediaStream 摄像头 / 麦克风 / 屏幕 MediaRecorder mimeType / bitsPerSecond 状态: inactive / recording / paused Blob 数据 通过 dataavailable 获取 状态转换 inactive recording paused start() pause() resume() stop() stop()

这张图把 MediaRecorder 的输入、输出、状态转换都串起来了。你可以把它当作一个速查表,写代码的时候瞄一眼,思路会清晰很多。

好了,MediaRecorder 的基础知识就讲到这里。下一章我们会深入 dataavailable 事件和分片录制,到时候我会分享一个我在做录屏工具时遇到的「数据丢失」的坑,保证让你印象深刻。


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