21、录制与回放:本地录制、云端录制、回放功能实现

录制与回放,说白了就是给实时通信加上「时光机」功能。我最早接触这个需求,是在一个在线教育项目里——老师上完课,学生想回头复习,结果发现没录上,那叫一个尴尬。从那以后,我对录制这块就特别上心。

今天咱们聊聊 WebRTC 移动端的录制方案。嗯,这里要注意,移动端和 PC 端差别不小,尤其是性能瓶颈和网络波动,搞不好就翻车。

21.1 录制的核心思路

WebRTC 的录制,本质上就是把音视频流「抓」下来,存成文件。你想想看,通话中的每一帧画面、每一段声音,都是通过 MediaStream 传递的。我们只要拿到这个流,就能做文章。

具体来说,有两种主流方式:

  • 本地录制:在客户端直接录制,用 MediaRecorder API 搞定
  • 云端录制:把流推到服务器,由服务端完成录制

我个人习惯是:小规模场景用本地录制,省服务器成本;大规模或需要合规存档的场景,必须上云端录制。

核心知识点:录制不是简单的「录屏」,而是从 MediaStream 中提取音视频轨道,再编码封装。移动端还要考虑电池、内存、存储空间。

21.2 本地录制实现

本地录制最常用的 API 就是 MediaRecorder。它能把 MediaStream 实时编码成 WebM 或 MP4 片段。我在项目中遇到过一个问题:Android 上某些浏览器不支持 MP4,只能录 WebM,但 iOS 又对 WebM 支持不好。嗯,这坑我踩过。

来看一段核心代码:

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

// 创建 MediaRecorder
const options = {
  mimeType: 'video/webm;codecs=vp9',
  videoBitsPerSecond: 2500000
};
const mediaRecorder = new MediaRecorder(localStream, options);

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

// 录制结束,生成文件
mediaRecorder.onstop = () => {
  const blob = new Blob(chunks, { type: 'video/webm' });
  const url = URL.createObjectURL(blob);
  // 保存或播放
};

// 开始录制
mediaRecorder.start(1000); // 每秒生成一个片段

小技巧start(1000) 里的参数是 timeslice,建议设成 1000ms。这样即使录制中断,也能拿到部分数据,不至于全丢。

移动端录制有个大坑:长时间录制会导致内存暴涨。我曾经在测试时录了半小时,结果 App 直接闪退。解决方案是分段录制,每 5 分钟存一个文件,然后释放内存。

21.3 云端录制实现

云端录制,说白了就是把客户端的流推到服务器,由服务端来录。这样做的好处是:客户端不用操心存储和编码,省电省内存。

实现思路分三步:

  1. 客户端把本地流通过 WebRTC 推送到媒体服务器(比如 LiveKit、Janus、或者自建 SFU)
  2. 服务器接收流,调用 FFmpeg 或 GStreamer 进行录制
  3. 录制完成后,把文件存到对象存储(OSS/S3)

来看一个服务端录制的伪代码(基于 Node.js + FFmpeg):

// 接收 WebRTC 流
const rtcStream = await getRTCStream(roomId, userId);

// 用 FFmpeg 录制
const ffmpeg = spawn('ffmpeg', [
  '-i', rtcStream.url,
  '-c:v', 'libx264',
  '-preset', 'ultrafast',
  '-c:a', 'aac',
  '-f', 'mp4',
  `recordings/${roomId}_${Date.now()}.mp4`
]);

ffmpeg.on('close', (code) => {
  if (code === 0) {
    // 上传到 OSS
    uploadToOSS(`recordings/${roomId}_${Date.now()}.mp4`);
  }
});

注意:云端录制对服务器带宽和 CPU 要求很高。如果同时录制 100 路流,建议用 GPU 加速编码,否则服务器分分钟被打满。

21.4 回放功能实现

回放,就是把录好的文件播出来。听起来简单,但移动端回放有几个坑:

  • 格式兼容:iOS 不支持 WebM,Android 部分机型不支持某些编码
  • 进度同步:如果录的是多人通话,回放时要把多路流对齐
  • 交互控制:暂停、快进、倍速播放,这些在移动端 WebView 里表现不一

我建议的做法是:录制时统一转成 HLS 格式。HLS 在 iOS 和 Android 上都原生支持,而且支持自适应码率。回放时直接用 <video> 标签加载 m3u8 文件就行。

<video controls autoplay>
  <source src="https://cdn.example.com/recordings/lesson21.m3u8" type="application/x-mpegURL">
</video>

如果是本地录制,回放时要注意:从 blob:url 读取文件,不要直接操作大文件。我习惯先把 blob 转成 ArrayBuffer,再分段加载,避免卡 UI 线程。

21.5 整体架构图

下面这张图展示了本地录制、云端录制和回放的完整流程。你可以看到,客户端既可以本地录,也可以推流到服务器。回放时,本地文件直接播,云端文件通过 CDN 分发。

录制与回放整体架构 移动客户端 getUserMedia 获取流 MediaRecorder 本地录制 WebRTC 推流到服务器 本地存储 WebM / MP4 文件 媒体服务器 接收 WebRTC 流 FFmpeg 编码录制 生成 HLS / MP4 推流 对象存储 OSS HLS 切片 + MP4 文件 CDN 分发 回放客户端 加载 HLS 播放控制 进度同步 拉流 本地回放 blob:url 直接播放 图例: 客户端 服务端 存储 CDN 回放端 本地录制:客户端 → 本地存储 → 本地回放 云端录制:客户端 → 媒体服务器 → OSS → CDN → 回放客户端

21.6 避坑指南

做录制功能,我踩过的坑能写满一页纸。挑几个最典型的说说:

  • 音频和视频不同步:MediaRecorder 在低端手机上容易音画不同步。我曾经在红米手机上测试,录了 10 分钟,声音比画面快了 3 秒。解决方案是录制时加上时间戳校准,或者用 requestVideoFrameCallback 做同步。
  • 录制中断:App 切到后台,录制可能被系统杀掉。Android 上可以用前台 Service 保活,iOS 上要申请后台权限。
  • 文件太大:高清视频 1 小时能到 10GB。建议录制时控制码率,或者用 HLS 切片,每段 10 秒,方便上传和回放。
  • 云端录制延迟:从推流到文件可用,通常有 5-10 秒延迟。如果要求实时回放,可以考虑用「边录边播」模式,服务器每生成一个切片就推送给客户端。

我的经验:移动端录制,优先考虑云端方案。本地录制虽然简单,但用户手机存储空间有限,而且录制过程中如果来电话、切应用,很容易失败。云端录制虽然复杂一点,但稳定性和可扩展性都好得多。

21.7 性能优化建议

最后给几个性能优化的方向:

优化项 说明 推荐做法
编码器选择 VP9 画质好但编码慢,H.264 兼容性好 移动端优先 H.264,服务端用硬件编码
码率控制 码率越高文件越大,传输越慢 视频 2-4 Mbps,音频 128 Kbps
分段录制 避免单文件过大,降低内存压力 每 5 分钟或 100MB 切一段
上传策略 边录边传,避免录完再传耗时 每生成一个片段立即上传
回放预加载 减少用户等待时间 提前加载下一个切片,用 preload="auto"

嗯,录制与回放这块内容就这些。说白了,核心就是拿到流、存下来、播出去。但每个环节都有不少细节,尤其是移动端,性能和兼容性要格外注意。希望这些经验能帮你少踩几个坑。

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