智能录制与回放:MediaRecorder API、AI自动剪辑与云端录制架构

大家好,我是你们的老朋友。今天我们来聊聊录制这件事。

说到录制,很多人第一反应就是「打开OBS,点开始录制」。但在WebRTC的世界里,录制远没那么简单。你想想看,一个实时音视频通话,怎么才能把双方的画面和声音都完整记录下来?而且录下来的东西,能不能自动把那些没人说话的静音段、画面静止的垃圾时间给剪掉?

嗯,这就是我们今天要啃的硬骨头。

一、MediaRecorder API:浏览器端的录制利器

先说说最基础的东西——MediaRecorder。这是浏览器原生提供的录制接口,说白了就是让你能把摄像头和麦克风采集到的数据,直接录成视频文件。

我个人习惯用这个API来做本地录制,因为它真的够简单。你只需要拿到一个MediaStream,然后丢给MediaRecorder,它就能帮你搞定编码和封装。

核心思路:MediaRecorder接收MediaStream,输出Blob数据块,最后合并成完整的视频文件。

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

// 创建MediaRecorder实例
const recorder = new MediaRecorder(stream, {
  mimeType: 'video/webm;codecs=vp9,opus'
});

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

// 录制结束,生成文件
recorder.onstop = () => {
  const blob = new Blob(chunks, { type: 'video/webm' });
  const url = URL.createObjectURL(blob);
  // 可以下载或预览
};

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

这里有个坑,我曾经踩过。就是mimeType这个参数,不同浏览器支持的不一样。Chrome对VP9支持很好,但Safari只认H.264。所以我在项目中一般会先检测一下:

function getSupportedMimeType() {
  const types = [
    'video/webm;codecs=vp9,opus',
    'video/webm;codecs=vp8,opus',
    'video/mp4;codecs=h264,aac'
  ];
  for (const type of types) {
    if (MediaRecorder.isTypeSupported(type)) {
      return type;
    }
  }
  return 'video/webm'; // 兜底
}

小技巧:录制WebRTC的远程流时,记得把ontrack事件里拿到的流合并成一个新的MediaStream,再传给MediaRecorder。否则你录到的只有本地画面。

二、AI自动剪辑:静音检测与无画面裁剪

录完的视频,往往有一大堆垃圾内容。比如会议开始前的等待时间、大家都不说话的空白段、摄像头被遮挡的黑屏画面。手动剪?太累了。这时候就该AI上场了。

我做过一个自动剪辑工具,核心逻辑其实就两步:

  1. 音频分析:检测音量低于阈值的静音段
  2. 视频分析:检测画面变化率低于阈值的静止段

说白了,就是找出那些「既没声音又没画面变化」的时间段,然后咔嚓剪掉。

2.1 静音检测

用Web Audio API的AnalyserNode就能搞定。我习惯用getByteTimeDomainData来获取音频波形数据,然后计算RMS值:

function detectSilence(audioContext, stream, threshold = 0.02) {
  const source = audioContext.createMediaStreamSource(stream);
  const analyser = audioContext.createAnalyser();
  analyser.fftSize = 2048;
  source.connect(analyser);

  const bufferLength = analyser.frequencyBinCount;
  const dataArray = new Uint8Array(bufferLength);

  return new Promise((resolve) => {
    const check = () => {
      analyser.getByteTimeDomainData(dataArray);
      let sum = 0;
      for (let i = 0; i < bufferLength; i++) {
        const value = (dataArray[i] - 128) / 128;
        sum += value * value;
      }
      const rms = Math.sqrt(sum / bufferLength);
      if (rms < threshold) {
        resolve(true); // 检测到静音
      } else {
        requestAnimationFrame(check);
      }
    };
    check();
  });
}

嗯,这里要注意阈值的选择。我试过0.01太敏感,正常呼吸声都会被当成静音。0.05又太迟钝,小声说话都检测不到。最后发现0.02到0.03之间比较合适,当然具体还得看你的麦克风质量。

2.2 无画面检测

画面静止检测,我用的方法是计算相邻帧的像素差异。把视频画到Canvas上,然后逐像素对比:

function detectStillFrame(video, canvas, threshold = 0.05) {
  const ctx = canvas.getContext('2d');
  const width = canvas.width;
  const height = canvas.height;
  let previousFrame = null;

  return () => {
    ctx.drawImage(video, 0, 0, width, height);
    const imageData = ctx.getImageData(0, 0, width, height);
    const currentFrame = imageData.data;

    if (!previousFrame) {
      previousFrame = new Uint8Array(currentFrame);
      return false;
    }

    let diff = 0;
    for (let i = 0; i < currentFrame.length; i += 4) {
      diff += Math.abs(currentFrame[i] - previousFrame[i]);
    }
    diff /= (width * height * 3); // 归一化

    previousFrame = new Uint8Array(currentFrame);
    return diff < threshold; // 画面变化小于阈值,判定为静止
  };
}

注意:逐像素对比性能开销不小。如果视频分辨率是1080p,每帧要处理超过200万个像素点。我建议先降采样到320x240再分析,准确率几乎不受影响,但速度能快10倍。

三、云端录制架构:从端到云的进化

本地录制虽然方便,但局限性也很明显——用户不能关浏览器,网络不能断,存储空间有限。所以真正生产级的方案,一定是云端录制。

我参与过一个教育平台的录制系统,架构大概是这样的:

云端录制架构图 WebRTC客户端 采集音视频流 SFU媒体服务器 转发/混流 录制服务 MediaRecorder 对象存储 S3/MinIO AI处理管道 静音/画面检测 Webhook回调 通知客户端 客户端 → SFU → 录制服务 → 存储 → AI处理 → 回调

这个架构里,客户端不直接参与录制。它只管把音视频流推给SFU(比如Janus或Mediasoup),然后由专门的录制服务订阅这些流。这样做的好处是:

  • 客户端无感:用户关掉浏览器,录制照样进行
  • 弹性扩展:录制服务可以水平扩展,支持大规模并发
  • 集中管理:所有录制任务统一调度,方便做AI后处理

3.1 录制服务的实现要点

录制服务本质上就是一个跑在服务器上的MediaRecorder。但服务器端没有DOM,所以我们需要用一些特殊手段。

我推荐用puppeteerplaywright来模拟浏览器环境。具体做法是:

  1. 启动一个无头浏览器页面
  2. 在页面里创建RTCPeerConnection,接收SFU转发过来的流
  3. 用MediaRecorder录制这个流
  4. 把录好的数据块上传到对象存储
// 伪代码示例
const browser = await puppeteer.launch();
const page = await browser.newPage();

await page.evaluate(async () => {
  const pc = new RTCPeerConnection();
  // 接收SFU的流
  pc.ontrack = (event) => {
    const stream = event.streams[0];
    const recorder = new MediaRecorder(stream);
    recorder.ondataavailable = async (e) => {
      // 上传到S3
      await fetch('/upload', {
        method: 'POST',
        body: e.data
      });
    };
    recorder.start(5000);
  };
});

经验之谈:无头浏览器录制有个坑——如果页面失去焦点,Chrome会降低定时器精度,导致录制出来的视频时间戳不准。解决办法是在启动参数里加上--disable-background-timer-throttling

3.2 AI后处理管道

录制完成后,我们还需要做AI自动剪辑。这个我一般放在异步任务队列里处理:

步骤 处理内容 技术选型
1. 音频提取 从视频中分离音频轨道 FFmpeg
2. 静音检测 分析音频波形,标记静音段 Web Audio API / Python librosa
3. 画面分析 检测画面静止、黑屏、模糊 OpenCV / TensorFlow.js
4. 时间线合并 合并静音和静止的时间段 自定义算法
5. 视频裁剪 根据时间线裁剪视频 FFmpeg

我曾经用这个管道处理过一个长达3小时的会议录像,最后剪出来只剩1小时20分钟。去掉的全是「大家低头看手机」、「等待主持人上线」这种垃圾时间。用户反馈说体验好太多了。

四、总结与避坑

好了,今天的内容差不多就这些。我最后再唠叨几句:

  • MediaRecorder的兼容性:不同浏览器对编码格式的支持差异很大,记得做降级处理
  • 静音检测的阈值:别用固定值,最好根据环境噪音动态调整
  • 云端录制的成本:无头浏览器实例很吃内存,一个实例大概占200-300MB,要规划好资源
  • AI剪辑的精度:自动剪辑不可能100%准确,建议保留原始文件,给用户手动调整的入口

嗯,如果你在实际项目中遇到什么问题,欢迎随时交流。毕竟这些东西,光看文档是学不会的,得亲手踩过坑才知道怎么填。


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