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 知识体系总览
下面这张图是我自己整理的媒体流处理核心逻辑,你可以对照着理解整个流程:
11.5 避坑指南
做媒体流处理这几年,我踩过的坑不少。挑几个典型的分享给你:
- Track被占用问题:同一个Track不能同时发送给多个PeerConnection。解决方案是用
track.clone()创建副本,每个连接用独立的Track。 - 录制文件损坏:如果录制过程中流中断(比如用户拔掉摄像头),生成的Blob可能无法播放。建议监听
track.onended事件,及时停止录制并提示用户。 - 内存泄漏:录制完成后一定要调用
URL.revokeObjectURL()释放内存。我见过有人录了半小时,页面直接卡死,就是因为没释放URL。 - 音频回环:如果同时播放本地流和录制流,可能会产生刺耳的啸叫。录制时建议把本地预览的音频mute掉。
我的习惯:每次操作Track之前,先检查track.readyState。如果是'ended',说明轨道已经失效,再操作会抛异常。这个检查虽然简单,但能避免很多线上事故。
媒体流处理这块,说白了就是“拿流、拆流、组流、存流”四个步骤。每个步骤都有它的坑,但只要掌握了核心API和常见模式,大部分场景都能应对。我个人建议你多动手写demo,尤其是音轨分离和合并,这两个操作在实际项目中出现的频率非常高。
公众号:蓝海资料掘金营,微信deep3321