18、录制与回放:MediaRecorder录制、服务器端录制、录制文件存储、回放功能实现。

说到WebRTC的录制与回放,我估计不少朋友第一反应就是「录个屏嘛,简单」。嗯,真上手做的时候,坑还是挺多的。我自己最早做这个功能时,就踩过存储格式不兼容的雷,也遇到过录制文件大到浏览器直接崩溃的情况。

今天咱们就把这块彻底捋清楚。从浏览器端的MediaRecorder,到服务器端的录制方案,再到文件怎么存、怎么回放,一条线走通。

18.1 浏览器端录制:MediaRecorder

MediaRecorder是浏览器原生提供的API。说白了,它能把MediaStream直接转成视频文件。你想想看,我们做视频通话时,对方的画面和声音都在一个MediaStream里,用MediaRecorder一接,就能录下来。

核心思路:把WebRTC的远程流(remoteStream)塞进MediaRecorder,然后监听它的dataavailable事件,拿到数据块。

我个人的习惯是,先判断浏览器是否支持MediaRecorder:

if (!window.MediaRecorder) {
  alert('您的浏览器不支持录制功能,建议使用Chrome或Firefox');
  return;
}

然后创建录制器实例:

const mediaRecorder = new MediaRecorder(remoteStream, {
  mimeType: 'video/webm;codecs=vp9'
});

这里有个坑——mimeType不是所有浏览器都支持。我曾经在Safari上试过vp9,直接报错。后来学乖了,先检测一下:

const mimeTypes = [
  'video/webm;codecs=vp9',
  'video/webm;codecs=vp8',
  'video/webm'
];
let selectedMimeType = '';
for (const type of mimeTypes) {
  if (MediaRecorder.isTypeSupported(type)) {
    selectedMimeType = type;
    break;
  }
}

录制过程中,数据是一段一段来的。每过一段时间(比如100毫秒),MediaRecorder就会触发dataavailable事件:

const chunks = [];
mediaRecorder.ondataavailable = (event) => {
  if (event.data.size > 0) {
    chunks.push(event.data);
  }
};

录制结束时,把这些chunks合并成一个Blob:

mediaRecorder.onstop = () => {
  const blob = new Blob(chunks, { type: selectedMimeType });
  // 接下来就可以下载或上传了
};

小技巧:录制过程中如果想暂停,可以用mediaRecorder.pause()和mediaRecorder.resume()。我做过一个功能,用户点击暂停后,画面定格,声音也停录,再点继续接着录——体验比直接stop好很多。

18.2 服务器端录制

浏览器端录制有个硬伤——页面一关,录制就没了。所以真正生产环境里,服务器端录制才是主流。

服务器端怎么录?思路其实不复杂:把WebRTC的媒体流通过SFU(比如Janus、Mediasoup)转发到服务器,服务器再用FFmpeg或者GStreamer去转码存储。

我参与过一个项目,用的是Mediasoup做SFU。服务器端收到RTP包后,直接喂给FFmpeg:

ffmpeg -i rtp://0.0.0.0:5004 -c copy output.webm

但说实话,这种方式对服务器压力不小。每个房间、每个用户都开一个FFmpeg进程,内存和CPU很快就吃满了。

后来我们换了个方案——先录成原始格式,再异步转码。录制时只存原始的RTP包或H264裸流,等通话结束后,后台任务再去转成MP4或WebM。这样录制过程几乎不消耗CPU。

注意:服务器端录制一定要处理好音视频同步。RTP包里有时间戳,但不同源的时钟可能不同步。我遇到过音频比视频快半秒的情况,排查了好久才发现是时间戳换算时精度丢了。

18.3 录制文件存储

录好的文件放哪儿?这得看你的场景。

存储方式 适用场景 优点 缺点
本地磁盘 开发测试、小规模部署 简单直接,无额外成本 扩展性差,容易占满磁盘
对象存储(OSS/S3) 生产环境、大规模部署 弹性扩展,支持CDN分发 需要额外费用,上传有延迟
分布式文件系统 企业内部、高可用场景 高可靠,低延迟 运维复杂

我个人比较推荐对象存储。原因很简单——录制文件通常需要回放,而回放时最好走CDN。对象存储天然支持CDN加速,用户回放时加载快,服务器带宽压力也小。

上传到OSS的代码示例(Node.js):

const OSS = require('ali-oss');
const client = new OSS({
  region: 'oss-cn-hangzhou',
  accessKeyId: '你的AK',
  accessKeySecret: '你的SK',
  bucket: 'my-recording-bucket'
});

async function uploadRecording(blob, roomId, userId) {
  const fileName = `${roomId}/${userId}_${Date.now()}.webm`;
  const buffer = Buffer.from(await blob.arrayBuffer());
  const result = await client.put(fileName, buffer);
  return result.url; // 返回可访问的URL
}

建议:文件名里带上房间ID和时间戳,方便后续检索。我见过有人直接用随机字符串命名,结果回放时根本找不到对应的录制文件。

18.4 回放功能实现

回放说白了就是把录好的文件用video标签播出来。但这里有几个细节要注意。

首先,格式兼容性。WebM在Chrome和Firefox上没问题,但Safari不支持。所以服务器端转码时,最好同时生成一个MP4版本。或者用HLS协议,兼容性最好。

其次,进度控制。用户可能需要拖动进度条、快进快退。这要求录制文件有完整的元数据(时长、关键帧索引)。如果录制时没处理好,回放时可能拖不动。

我做过一个回放组件,核心逻辑是这样的:

<video id="playback" controls preload="metadata">
  <source src="https://cdn.example.com/recording.webm" type="video/webm">
  <source src="https://cdn.example.com/recording.mp4" type="video/mp4">
  您的浏览器不支持视频回放
</video>

但光有video标签还不够。用户可能需要只看某一段,或者倍速播放。这时候就得自己封装控制逻辑了:

const video = document.getElementById('playback');
// 倍速播放
video.playbackRate = 1.5;
// 跳转到30秒位置
video.currentTime = 30;
// 监听播放结束
video.onended = () => {
  console.log('回放完成');
};

重点:回放时一定要处理好音视频同步。如果录制时音视频不同步,回放时问题会更明显。我建议在录制阶段就做好时间戳校准,不要指望回放时再去修正。

18.5 整体流程梳理

说了这么多,咱们用一张图把整个流程串起来:

录制与回放整体流程 WebRTC媒体流 远程/本地流 MediaRecorder 浏览器端录制 SFU服务器 服务器端录制 本地磁盘 开发测试用 对象存储 OSS/S3 分布式文件系统 高可用场景 异步转码处理 FFmpeg转码 → WebM / MP4 / HLS 回放功能(Video标签 / HLS播放器)

从图上能看出来,整个链路其实就三步:采集 → 存储 → 回放。但每一步都有不少细节。我个人觉得,最容易出问题的是存储和转码之间的衔接——文件还没转完就开始回放,用户看到的就是一个打不开的视频。

解决办法也简单:加一个状态标记。录制完成后先标记为「转码中」,转码完成后再标记为「已就绪」。回放时只展示已就绪的文件。

最后一个小建议:不管用哪种录制方案,一定要做录制文件的完整性校验。我遇到过录制过程中网络闪断,文件只有一半的情况。加个MD5校验或者文件大小检查,能省去很多排查时间。


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