11、媒体流处理:MediaStream API、音轨分离与合并、媒体流录制

媒体流处理,说白了就是WebRTC里最核心的“原材料加工”环节。你想想看,无论你是做视频通话、直播推流,还是录个屏分享,最终操作的对象都是MediaStream。我刚开始接触WebRTC时,以为拿到摄像头流就直接扔给PeerConnection就完事了。后来才发现,真正的坑都在流处理上——音轨分离、动态替换、录制保存,每一步都有讲究。

11.1 MediaStream API 基础

MediaStream 是一个容器,里面可以装多个MediaStreamTrack。每个Track代表一种媒体类型——音频或视频。我个人习惯把MediaStream想象成一个“多轨录音机”,每个轨道独立控制,互不干扰。

核心概念:

  • MediaStream:媒体流容器,包含零个或多个Track
  • MediaStreamTrack:单个媒体轨道,音频或视频
  • getUserMedia():获取本地媒体流
  • getDisplayMedia():获取屏幕共享流

获取本地摄像头和麦克风,代码其实很简单:

// 获取音视频流
const stream = await navigator.mediaDevices.getUserMedia({
  video: {
    width: { ideal: 1280 },
    height: { ideal: 720 },
    frameRate: { ideal: 30 }
  },
  audio: {
    echoCancellation: true,
    noiseSuppression: true,
    sampleRate: 48000
  }
});

// 查看流中的轨道
console.log('音频轨道数:', stream.getAudioTracks().length);
console.log('视频轨道数:', stream.getVideoTracks().length);

嗯,这里要注意一点——getUserMedia返回的Promise可能会被拒绝。用户拒绝授权、设备不存在、或者浏览器不支持,都会抛异常。我在项目中遇到过用户插拔摄像头导致流中断的情况,所以建议始终加上try-catch。

11.2 音轨分离与合并

为什么要分离音轨?场景太多了。比如你只想录制屏幕画面但不要系统声音,或者你想把麦克风音频和远端音频混在一起。说白了,分离和合并是媒体流处理的“基本功”。

11.2.1 从流中提取单个轨道

// 假设已经有一个完整的音视频流
const fullStream = await navigator.mediaDevices.getUserMedia({
  video: true, audio: true
});

// 分离出视频轨道
const videoTrack = fullStream.getVideoTracks()[0];
// 分离出音频轨道
const audioTrack = fullStream.getAudioTracks()[0];

// 创建只包含视频的新流
const videoOnlyStream = new MediaStream([videoTrack]);
// 创建只包含音频的新流
const audioOnlyStream = new MediaStream([audioTrack]);

你可能会问:直接操作Track和操作Stream有什么区别?区别大了。Track是实际的数据源,Stream只是容器。同一个Track可以被多个Stream引用,但Track的状态(比如enabled、muted)是全局的。我曾经踩过一个坑——把同一个Track同时添加到本地预览流和发送流,结果mute了其中一个,另一个也跟着静音了。

注意:同一个MediaStreamTrack不能同时属于多个PeerConnection的发送端。如果你需要把同一路视频发给多个用户,必须使用clone()方法创建副本。

11.2.2 合并多个轨道到同一个流

// 从不同来源获取轨道
const cameraStream = await navigator.mediaDevices.getUserMedia({ video: true });
const micStream = await navigator.mediaDevices.getUserMedia({ audio: true });

// 合并到一个流中
const mergedStream = new MediaStream([
  cameraStream.getVideoTracks()[0],
  micStream.getAudioTracks()[0]
]);

// 合并后的流可以直接用于RTCPeerConnection
pc.addStream(mergedStream);

合并时有个细节——如果两个来源的采样率或时钟不同,可能会出现音画不同步。我建议在合并前统一约束条件,比如都设置sampleRate: 48000

11.3 媒体流录制

录制是媒体流处理里最实用的功能之一。WebRTC原生提供了MediaRecorder API,可以直接把MediaStream录制成文件。我个人习惯用它来做本地录制、回放调试,甚至做简单的直播录制。

11.3.1 基本录制流程

// 获取要录制的流
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);
  }
};

// 录制结束后的处理
recorder.onstop = () => {
  const blob = new Blob(chunks, { type: 'video/webm' });
  const url = URL.createObjectURL(blob);
  const a = document.createElement('a');
  a.href = url;
  a.download = 'recording.webm';
  a.click();
  URL.revokeObjectURL(url);
};

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

// 10秒后停止录制
setTimeout(() => recorder.stop(), 10000);

小技巧:录制时timeslice参数很关键。设得太小(比如100ms)会产生大量小数据块,影响性能;设得太大(比如5000ms)又可能导致内存占用过高。我个人推荐1000ms,兼顾性能和实时性。

11.3.2 录制格式与兼容性

MIME类型 浏览器支持 说明
video/webm;codecs=vp8,opus Chrome, Firefox, Edge 兼容性最好,VP8编码
video/webm;codecs=vp9,opus Chrome, Firefox 压缩率更高,但编码慢
video/mp4 部分浏览器 支持有限,不建议依赖
audio/webm;codecs=opus Chrome, Firefox, Edge 纯音频录制首选

录制前一定要检查MediaRecorder.isTypeSupported()。我在项目中遇到过用户用Safari,结果video/webm完全不支持,最后只能降级到audio/webm或者提示用户换浏览器。

11.3.3 动态控制录制

// 暂停录制
recorder.pause();

// 恢复录制
recorder.resume();

// 检查录制状态
console.log(recorder.state); // 'inactive' | 'recording' | 'paused'

// 请求当前数据块(不停止录制)
recorder.requestData();

动态控制录制在实际场景中非常有用。比如做“一键静音”功能时,你可以暂停录制,等用户取消静音后再恢复。注意requestData()会立即触发ondataavailable,适合做分段保存。

11.4 知识体系总览

下面这张图是我自己整理的媒体流处理核心逻辑,你可以对照着理解整个流程:

媒体流处理核心逻辑 获取媒体流 getUserMedia / getDisplayMedia 创建MediaStream new MediaStream([tracks]) 轨道管理 getTracks / clone / stop 音轨分离 getAudioTracks / getVideoTracks 音轨合并 new MediaStream([track1, track2]) 动态替换 replaceTrack / enabled 录制 MediaRecorder → Blob → 下载 发送 addTrack → PeerConnection 预览 video.srcObject = stream 获取 → 处理 → 输出,三个环节环环相扣

11.5 避坑指南

做媒体流处理这几年,我踩过的坑不少。挑几个典型的分享给你:

  • Track被占用问题:同一个Track不能同时发送给多个PeerConnection。解决方案是用track.clone()创建副本,每个连接用独立的Track。
  • 录制文件损坏:如果录制过程中流中断(比如用户拔掉摄像头),生成的Blob可能无法播放。建议监听track.onended事件,及时停止录制并提示用户。
  • 内存泄漏:录制完成后一定要调用URL.revokeObjectURL()释放内存。我见过有人录了半小时,页面直接卡死,就是因为没释放URL。
  • 音频回环:如果同时播放本地流和录制流,可能会产生刺耳的啸叫。录制时建议把本地预览的音频mute掉。

我的习惯:每次操作Track之前,先检查track.readyState。如果是'ended',说明轨道已经失效,再操作会抛异常。这个检查虽然简单,但能避免很多线上事故。

媒体流处理这块,说白了就是“拿流、拆流、组流、存流”四个步骤。每个步骤都有它的坑,但只要掌握了核心API和常见模式,大部分场景都能应对。我个人建议你多动手写demo,尤其是音轨分离和合并,这两个操作在实际项目中出现的频率非常高。


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