一、WebRTC 录制与回放:MediaRecorder API、存储与回放技术

大家好,我是你们的老朋友。今天我们来聊聊 WebRTC 里一个非常实用的功能——录制与回放。说实话,很多同学做 P2P 通信时,总觉得能连上、能通话就完事了。但实际项目中,用户经常问:「刚才那段视频能录下来吗?」「我想回看之前的会议。」这时候,你就得掏出 MediaRecorder 这把利器了。

我个人习惯把录制回放分成三个环节:采集→存储→回放。听起来简单,但每个环节都有坑。咱们一个一个说。

核心要点:MediaRecorder 负责将 MediaStream 编码成容器格式(如 WebM、MP4),存储层负责持久化,回放层负责解码与渲染。三者缺一不可。

1.1 MediaRecorder API 基础

MediaRecorder 是浏览器原生提供的录制接口。你只要给它一个 MediaStream,它就能把音视频数据一段一段吐出来。我在项目中遇到过不少同学直接用默认参数,结果录出来的文件要么没声音,要么花屏。嗯,这里要注意参数配置。

基本用法如下:

// 获取本地媒体流
const stream = await navigator.mediaDevices.getUserMedia({
  video: true,
  audio: true
});

// 创建 MediaRecorder 实例
const recorder = new MediaRecorder(stream, {
  mimeType: 'video/webm;codecs=vp9,opus',
  videoBitsPerSecond: 2500000, // 2.5 Mbps
  audioBitsPerSecond: 128000   // 128 kbps
});

// 收集数据块
const chunks = [];
recorder.ondataavailable = (event) => {
  if (event.data.size > 0) {
    chunks.push(event.data);
  }
};

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

// 停止录制
recorder.onstop = () => {
  const blob = new Blob(chunks, { type: recorder.mimeType });
  // 接下来可以保存或上传这个 blob
};

这里有个关键点:mimeType 的选择。不同浏览器支持的编码格式不一样。Chrome 偏好 WebM + VP9,Safari 更倾向 MP4 + H.264。我曾经在跨浏览器测试时踩过坑——在 Safari 上直接用了 WebM,结果录出来的文件播放不了。所以建议先检测支持情况:

function getSupportedMimeType() {
  const types = [
    'video/webm;codecs=vp9,opus',
    'video/webm;codecs=vp8,opus',
    'video/mp4;codecs=h264,aac',
    'video/x-matroska;codecs=h264,aac'
  ];
  for (const type of types) {
    if (MediaRecorder.isTypeSupported(type)) {
      return type;
    }
  }
  return ''; // 回退到浏览器默认
}

小技巧:录制时建议把 timeslice 参数设成 1000ms 左右。太短会导致大量小文件块,太长又容易丢数据。1000ms 是个不错的平衡点。

1.2 录制文件存储

录完的数据怎么存?这得看你的应用场景。如果是本地录制,直接保存到磁盘就行。如果是远程会议录制,通常要上传到服务器。

本地存储方案:

// 利用 URL.createObjectURL 生成下载链接
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `recording-${Date.now()}.webm`;
a.click();
URL.revokeObjectURL(url); // 释放内存

但要注意,createObjectURL 会占用内存。如果录制文件很大(比如 1GB 以上),浏览器可能会崩溃。我建议大文件用 FileReader 分片读取,或者直接用 IndexedDB 存储。

服务器存储方案:

FormData 上传是最直接的方式:

const formData = new FormData();
formData.append('file', blob, 'recording.webm');

fetch('/upload', {
  method: 'POST',
  body: formData
}).then(response => {
  console.log('上传成功');
});

不过,如果录制时间很长(比如 1 小时),一次性上传整个 blob 会超时。更好的做法是边录边传:

recorder.ondataavailable = (event) => {
  if (event.data.size > 0) {
    // 直接上传每个数据块
    uploadChunk(event.data);
  }
};

function uploadChunk(chunk) {
  const formData = new FormData();
  formData.append('chunk', chunk);
  formData.append('sessionId', sessionId);
  formData.append('index', chunkIndex++);
  fetch('/upload-chunk', { method: 'POST', body: formData });
}

服务器端收到所有分片后,再按顺序合并成完整文件。这样即使网络中断,也只需要重传丢失的分片。

注意:分片上传时一定要带上序号和会话 ID,否则服务器没法正确拼接。我曾经因为忘了加序号,结果回放时画面顺序全乱了,用户投诉说「你们录的是鬼片吗?」

1.3 回放技术

回放说白了就是把录好的文件塞进 <video> 标签里。但这里有几个细节要注意。

基础回放:

<video id="playback" controls autoplay muted></video>

<script>
  const video = document.getElementById('playback');
  const url = URL.createObjectURL(blob);
  video.src = url;
</script>

嗯,看起来很简单对吧?但实际项目中,你可能会遇到这些问题:

  • 格式不兼容: Safari 不支持 WebM,Chrome 对 MP4 支持有限。解决方案是录制时同时生成两种格式,或者用 FFmpeg 在服务端转码。
  • 大文件加载慢: 几百 MB 的视频直接加载会卡死页面。建议用 MediaSource 分片加载,或者用 HLS/DASH 流式播放。
  • 音视频不同步: 这通常是因为录制时时间戳不准。检查一下 timeslice 是否稳定,或者用 requestVideoFrameCallback 做同步。

进阶回放——使用 MediaSource:

const mediaSource = new MediaSource();
video.src = URL.createObjectURL(mediaSource);

mediaSource.onsourceopen = () => {
  const sourceBuffer = mediaSource.addSourceBuffer('video/webm;codecs=vp9,opus');
  
  // 分片追加数据
  function appendChunk(chunk) {
    if (!sourceBuffer.updating) {
      sourceBuffer.appendBuffer(chunk);
    } else {
      // 等待上一个追加完成
      sourceBuffer.onupdateend = () => sourceBuffer.appendBuffer(chunk);
    }
  }
  
  // 从服务器拉取分片
  fetchChunks().then(chunks => {
    chunks.forEach(appendChunk);
  });
};

这样做的好处是:用户可以边下载边看,不用等整个文件下载完。而且支持 seek 到任意位置(前提是服务器支持 Range 请求)。

个人经验:回放时一定要加 muted 属性,否则浏览器可能会因为自动播放策略而拒绝播放。等用户交互后再取消静音。

1.4 整体架构图

下面这张图展示了录制与回放的完整流程。我把它画成了 SVG,方便你理解各个环节的关系。

WebRTC 录制与回放架构图 采集阶段 getUserMedia / RTCPeerConnection 录制阶段 MediaRecorder API 存储阶段 本地 / 服务器 数据流细节 录制数据流 MediaStream → MediaRecorder → ondataavailable (Blob) → 分片 / 完整文件 → 存储 (IndexedDB / 服务器) 回放数据流 存储 → 读取 Blob / 分片 → URL.createObjectURL → <video> 标签 / MediaSource → 渲染播放 可选转码

1.5 常见问题与避坑指南

最后,我总结几个实际项目中容易踩的坑,希望能帮你少走弯路。

问题 原因 解决方案
录制文件无声 音频轨道未启用或编码不匹配 检查 getUserMedia 的 audio 参数,确保 mimeType 包含音频编码
回放时画面卡顿 文件过大或网络带宽不足 使用 MediaSource 分片加载,或降低录制码率
跨浏览器不兼容 不同浏览器支持的编码格式不同 MediaRecorder.isTypeSupported 检测,准备多种格式
录制过程中内存溢出 长时间录制未及时清理数据块 边录边传,或定期清理已处理的数据块
音视频不同步 时间戳漂移或 timeslice 不稳定 使用 requestVideoFrameCallback 做同步校准

曾经踩过的坑:有一次我在录制远程桌面时,发现录出来的视频只有前 10 秒正常,后面全是黑屏。排查了半天,原来是 RTCPeerConnectionontrack 事件在录制过程中被重新触发了,导致 MediaRecorder 的输入流被替换。解决方案是在录制开始后锁定轨道,或者用 clone() 方法复制一份流。

好了,关于录制与回放的核心内容就这些。你想想看,其实原理并不复杂,关键是要把每个环节的细节处理好。从采集到存储再到回放,每一步都有它的讲究。希望今天的分享能帮你在实际项目中少踩几个坑。

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