多人录制方案:同时录制多个视频流

说实话,多人录制这个需求,我最早是在做在线教育平台时遇到的。一个老师加三个学生,四路视频要同时录下来。当时我想,这不就是每个 PeerConnection 各开一个 MediaRecorder 嘛?结果一跑起来,时间轴全对不上,画面和声音错位得离谱。嗯,今天就把我踩过的坑和最终方案,一次性说清楚。

核心挑战:不是录制,是同步

同时录多路视频,技术上并不难。每个 RTCPeerConnection 收到远程流后,各自 new 一个 MediaRecorder 就行。但问题出在——每个录制器的起始时间不一样

为什么会这样?因为每个 MediaRecorder 的 start() 调用有微小延迟,浏览器内部的时间戳基准也不同。你想想看,四个录制器各自为政,最后生成的四个 WebM 文件,播放时根本对不上嘴型。

核心原则:多人录制不是「各自录完再拼」,而是「用一个时钟指挥所有录制器」。

我的方案:统一时间轴 + 主控录制器

我在项目中最终采用的做法是:

  1. 选一个「主控」——通常是发起端(老师端)的 MediaRecorder。
  2. 所有录制器共享同一个起始时间——用 performance.now()Date.now() 打一个全局时间戳。
  3. 每个录制器的 start() 都传入这个时间戳,而不是各自 new 一个时间。

代码大概长这样:

// 主控端发起同步信号
const syncTime = performance.now();

// 每个远程流到达时,都用同一个 syncTime 启动录制
remoteStreams.forEach((stream, index) => {
  const recorder = new MediaRecorder(stream, {
    mimeType: 'video/webm;codecs=vp9'
  });
  
  // 关键:传入统一时间戳
  recorder.start(1000); // 1秒切片
  recorder.syncTimestamp = syncTime; // 自定义属性,后续使用
  
  recorders.push(recorder);
});

小技巧:我习惯在 ondataavailable 回调里,把 syncTimestamp 写入每个 chunk 的 metadata 中。这样后期合并时,可以根据这个时间戳对齐各段视频。

同步多个录制器:时间同步问题处理

你以为传同一个时间戳就完事了?太天真了。浏览器实际执行 start() 时,会有几毫秒到几十毫秒的偏差。我遇到过最夸张的一次,Chrome 和 Firefox 之间差了 200ms。

怎么解决?我用了两招:

  • 第一招:NTP 时间同步——如果各端在不同机器上,通过信令服务器交换 NTP 时间戳。WebRTC 的 RTCP 报告里其实就带 NTP 时间,可以直接拿来用。
  • 第二招:录制前先录一段「同步信号」——比如让所有参与者同时拍一下手,或者显示一个倒计时动画。后期合并时,以这个事件为对齐点。

注意:千万不要依赖 new Date().getTime() 做跨设备同步。各设备的系统时间可能差好几秒。我吃过这个亏,录出来的视频完全没法用。

合并多人视频:用 FFmpeg 还是浏览器端?

合并方案我试过两种,各有优劣:

方案 优点 缺点
服务端 FFmpeg 合并 功能强大,支持各种布局 需要上传原始文件,带宽成本高
浏览器端 Canvas 合并 实时性高,无需上传 性能瓶颈,最多支持 4-6 路

我个人更推荐混合方案:浏览器端先做初步合并(比如把 4 路小画面拼成一个大画面),然后只上传合并后的单路视频。这样既节省带宽,又保留了同步精度。

浏览器端合并的核心代码:

// 创建一个离屏 Canvas
const canvas = document.createElement('canvas');
canvas.width = 1280;
canvas.height = 720;
const ctx = canvas.getContext('2d');

// 每帧绘制所有视频流
function drawFrame() {
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  
  // 假设我们有 4 个视频元素,排成 2x2 网格
  const streams = [video1, video2, video3, video4];
  streams.forEach((video, index) => {
    const x = (index % 2) * 640;
    const y = Math.floor(index / 2) * 360;
    ctx.drawImage(video, x, y, 640, 360);
  });
  
  requestAnimationFrame(drawFrame);
}

// 用 MediaRecorder 录制 Canvas 流
const combinedStream = canvas.captureStream(30);
const finalRecorder = new MediaRecorder(combinedStream, {
  mimeType: 'video/webm;codecs=vp9'
});

避坑指南:我曾经用 canvas.captureStream(60) 试图录 60fps,结果移动端直接卡死。后来发现 30fps 足够清晰,而且兼容性好得多。嗯,别贪心。

时间同步问题处理的终极方案

如果你问我,最稳妥的时间同步方法是什么?我会说:在录制过程中持续校准

具体做法是:

  • 每个录制器每 5 秒向主控发送一次「当前时间 + 录制进度」。
  • 主控收到后,计算偏差,然后通过 DataChannel 广播一个「时间校正值」。
  • 各录制器根据校正值调整自己的时间戳。

这样即使一开始有偏差,也能在录制过程中逐渐拉齐。我在一个 8 人会议的项目中实测过,最终偏差控制在 16ms 以内(一帧的误差)。

总结一下:多人录制不是技术难题,而是工程细节。统一时间轴、持续校准、合理合并,这三步走下来,基本不会出大问题。

知识体系总览

下面这张图,是我自己梳理的多人录制核心逻辑。你看一眼,心里就有谱了:

多人录制核心逻辑 多路 WebRTC 流 统一时间轴(NTP / 主控时间戳) MediaRecorder 1 MediaRecorder 2 MediaRecorder N Canvas 合并 / FFmpeg 后处理 持续校准

这张图把整个流程串起来了:多路流进来,先统一时间轴,然后各自录制,最后合并输出。中间别忘了持续校准——这是最容易忽略但最关键的一步。


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