20、录制与回放:MediaRecorder API录制音视频、录制格式(webm、mp4)、录制文件上传与存储、回放录制内容

录制与回放,是WebRTC应用中非常实用的功能。我最早接触这个需求,是在做一个在线面试系统的时候——面试官要求把整个面试过程录下来,方便后续复盘。当时我第一反应是“直接用MediaRecorder不就行了?”结果踩了不少坑。今天我把这些经验整理出来,希望能帮你少走弯路。

核心知识点速览

  • MediaRecorder API的基本用法
  • 录制格式的选择(webm vs mp4)
  • 录制文件的上传与存储策略
  • 回放录制内容的实现方式
录制与回放知识体系 MediaRecorder API 录制流程 获取MediaStream 创建MediaRecorder实例 监听ondataavailable事件 调用start()/stop() 录制格式 webm mp4 浏览器原生支持度不同 存储与上传 Blob → FormData 分片上传(大文件) 服务端存储策略 回放 创建ObjectURL 赋值给video.src

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();

录制与回放这块,说白了就是“录得下来、存得上去、放得出来”。技术本身不复杂,但细节很多。你只要把格式检测、分片上传、内存释放这几个点把握好,基本不会出大问题。

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