7、媒体流处理:MediaStream与MediaStreamTrack

说到媒体流处理,这其实是WebRTC里最基础也最核心的一块。我刚开始接触WebRTC时,以为拿到摄像头画面就直接推流就行了,结果发现根本不是这么回事。媒体流就像一条水管,里面流的是音频和视频数据,而MediaStream和MediaStreamTrack就是管理这条水管的阀门和接口。

说白了,MediaStream是一个容器,里面可以装多个MediaStreamTrack。每个Track代表一路独立的媒体数据,要么是音频,要么是视频。你想想看,一个视频通话场景里,你需要同时传输摄像头画面和麦克风声音,这时候一个MediaStream里就包含了两个Track——一个视频Track和一个音频Track。

核心概念速览:

  • MediaStream:媒体流的容器,可以包含多个Track
  • MediaStreamTrack:单个媒体轨道,音频或视频
  • 每个Track有独立的id、kind、label、enabled、muted等属性
  • Track可以单独控制开关、停止、克隆
MediaStream 与 MediaStreamTrack 关系图 MediaStream AudioTrack kind: "audio" VideoTrack kind: "video" 常用操作 getAudioTracks() getVideoTracks() addTrack() / removeTrack() clone() stop() enabled / muted

7.1 获取媒体流:getUserMedia

获取本地媒体流,最常用的就是navigator.mediaDevices.getUserMedia()。这个方法返回一个Promise,resolve后给你一个MediaStream对象。

// 获取摄像头+麦克风
async function getLocalStream() {
  try {
    const stream = await navigator.mediaDevices.getUserMedia({
      video: true,
      audio: true
    });
    // stream 就是一个 MediaStream 实例
    console.log('获取到媒体流:', stream);
    return stream;
  } catch (err) {
    console.error('获取媒体流失败:', err);
  }
}

嗯,这里要注意一点:getUserMedia必须在安全上下文(HTTPS或localhost)下才能调用。我在项目中遇到过,部署到测试服务器上忘了配HTTPS,结果一直报错,排查了半天才发现是这个问题。

小技巧:如果你只想获取音频或视频,可以把另一个设为false。比如只做语音通话,就写{ audio: true, video: false }。这样可以减少权限请求,用户体验更好。

7.2 操作音频轨道与视频轨道

拿到MediaStream之后,怎么单独控制音频和视频呢?这就用到getAudioTracks()getVideoTracks()了。它们返回一个数组,因为一个流里可能有多个同类型的Track(比如多个摄像头)。

// 获取流中的音频和视频轨道
const stream = await getLocalStream();

const audioTracks = stream.getAudioTracks();
const videoTracks = stream.getVideoTracks();

console.log('音频轨道数量:', audioTracks.length);
console.log('视频轨道数量:', videoTracks.length);

// 单独控制某个轨道
if (audioTracks.length > 0) {
  const audioTrack = audioTracks[0];
  audioTrack.enabled = false;  // 静音
  console.log('音频轨道已静音');
}

if (videoTracks.length > 0) {
  const videoTrack = videoTracks[0];
  videoTrack.enabled = false;  // 关闭画面
  console.log('视频轨道已关闭');
}

这里有个坑,我一开始也搞混过:enabledmuted是两个不同的概念。enabled是你可以主动控制的开关,设为false就停止发送数据;而muted是浏览器检测到设备被占用或断开时自动设置的,你只能读取,不能手动修改。

注意:设置enabled = false只是让轨道静音或黑屏,但并没有释放设备资源。要彻底释放摄像头和麦克风,必须调用track.stop()

7.3 媒体流克隆与停止

有时候我们需要把同一个媒体流用在多个地方,比如同时显示在本地预览和发送给远端。这时候直接复用同一个Track可能会有问题,因为一个Track同一时间只能绑定到一个RTCPeerConnection上。解决方案就是——克隆

// 克隆媒体流
const originalStream = await getLocalStream();
const clonedStream = originalStream.clone();

console.log('原始流:', originalStream.id);
console.log('克隆流:', clonedStream.id);
// 两个流的id不同,但内部的Track是新的独立实例

// 克隆单个Track
const originalVideoTrack = originalStream.getVideoTracks()[0];
const clonedVideoTrack = originalVideoTrack.clone();

// 把克隆的Track添加到另一个流
const newStream = new MediaStream();
newStream.addTrack(clonedVideoTrack);

我个人习惯在多人通话场景里,把本地流克隆一份用于本地预览,原始流用于推流。这样即使推流端出了问题,本地预览也不会受影响。

说到停止媒体流,很多新手会直接调用stream.stop()——但这个方法其实不存在!正确的做法是遍历所有Track,逐个调用stop()

// 正确停止媒体流的方式
function stopMediaStream(stream) {
  if (!stream) return;
  
  stream.getTracks().forEach(track => {
    track.stop();
    console.log(`已停止轨道: ${track.kind} - ${track.id}`);
  });
  
  // 停止后,摄像头指示灯会熄灭
  // 麦克风也会被释放
}

// 使用示例
const myStream = await getLocalStream();
// ... 使用流 ...
stopMediaStream(myStream);

避坑指南:我曾经在切换摄像头时,没有先停止旧的Track就直接调用getUserMedia获取新流,结果浏览器报错说设备被占用。后来我养成了习惯:每次重新获取设备前,先调用stopMediaStream()释放所有资源。

7.4 实际项目中的常见操作模式

在实际项目中,我总结了一套比较稳的操作流程,分享给你参考:

  1. 获取流:调用getUserMedia,拿到MediaStream
  2. 分离轨道:用getAudioTracks()getVideoTracks()分别处理
  3. 克隆备用:如果需要多路使用,提前克隆
  4. 绑定到元素:把流赋值给videoaudio元素的srcObject
  5. 推流:把Track添加到RTCPeerConnectionaddTrack()
  6. 清理:通话结束时,调用stopMediaStream()
// 完整示例:获取流 → 预览 → 克隆 → 推流 → 清理
async function handleCall() {
  // 1. 获取本地流
  const localStream = await getLocalStream();
  
  // 2. 本地预览
  const localVideo = document.getElementById('localVideo');
  localVideo.srcObject = localStream;
  
  // 3. 克隆一份用于推流(避免直接操作预览流)
  const sendStream = localStream.clone();
  
  // 4. 创建PeerConnection并添加轨道
  const pc = new RTCPeerConnection();
  sendStream.getTracks().forEach(track => {
    pc.addTrack(track, sendStream);
  });
  
  // 5. 通话结束,清理资源
  function endCall() {
    stopMediaStream(localStream);
    stopMediaStream(sendStream);
    pc.close();
    localVideo.srcObject = null;
  }
}

你看,整个流程其实不复杂。关键是要理解MediaStream和MediaStreamTrack的关系,以及它们各自的生命周期管理。我刚开始做WebRTC时,经常忘记清理Track,导致摄像头一直亮着,用户还以为被监控了。后来每次写完代码都会检查:有没有调用stop()?有没有释放资源?

个人经验:建议在项目里封装一个MediaManager类,统一管理所有流的获取、克隆、停止操作。这样代码更清晰,也不容易漏掉清理步骤。

好了,关于媒体流处理的核心内容就这些。记住一句话:拿到流只是开始,管好流才是关键。音频轨道和视频轨道的操作、克隆与停止,这些基本功打扎实了,后面做多人通话、屏幕共享、录制等功能就会顺手很多。

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