4、媒体流操作:MediaStream与MediaStreamTrack、视频轨道控制、音频轨道控制、媒体流录制

各位同学,欢迎来到第四章。这一章我们聊聊媒体流操作。

说实话,媒体流是WebRTC的“血液”。没有它,你连个画面都传不过去。我刚开始接触WebRTC时,以为拿到一个MediaStream对象就完事了。后来才发现,里面的MediaStreamTrack才是真正干活的“小兵”。

这一章,我会带你拆解这两个核心对象。然后手把手教你控制视频轨道、音频轨道。最后,我们还会把媒体流录下来,存成文件。

嗯,内容有点多,但都很实用。我们开始吧。

4.1 MediaStream 与 MediaStreamTrack 的关系

先搞清楚这两个东西是什么。

MediaStream 是一个容器。它里面可以装多个 MediaStreamTrack。每个 MediaStreamTrack 代表一种类型的媒体数据,比如视频或音频。

我打个比方:MediaStream 就像一根HDMI线,而 MediaStreamTrack 就是线里面传输的视频信号和音频信号。你拔掉视频信号,画面就没了;你拔掉音频信号,声音就没了。

核心概念:

  • MediaStream:媒体流的集合,可以包含多个轨道。
  • MediaStreamTrack:单个媒体轨道,要么是视频,要么是音频。
  • 一个 MediaStream 可以包含多个 MediaStreamTrack,但通常我们只用一个视频轨道和一个音频轨道。

下面这张图,帮你理清它们的关系:

MediaStream 容器 MediaStreamTrack kind: "video" label: "摄像头" enabled: true muted: false MediaStreamTrack kind: "audio" label: "麦克风" enabled: true muted: false 多个轨道

你看,一个 MediaStream 里可以同时包含视频和音频轨道。每个轨道都有自己的属性,比如是否启用、是否静音。

4.2 获取媒体流:从摄像头和麦克风

获取媒体流,最常用的方法是 navigator.mediaDevices.getUserMedia()。这个方法会请求用户授权,然后返回一个 MediaStream 对象。

我个人习惯在调用时,明确指定需要视频还是音频。不要一股脑全要,免得用户觉得你“贪心”。

// 请求视频和音频
async function startStream() {
  try {
    const stream = await navigator.mediaDevices.getUserMedia({
      video: true,
      audio: true
    });
    // 把流绑定到 video 元素上
    const video = document.getElementById('localVideo');
    video.srcObject = stream;
  } catch (err) {
    console.error('获取媒体流失败:', err);
  }
}

小技巧: 如果你只需要视频,可以只传 { video: true }。这样用户就不会看到麦克风授权弹窗。我在做纯视频会议时,经常这么干。

4.3 视频轨道控制

拿到 MediaStream 后,我们可以通过 getVideoTracks() 方法获取视频轨道。然后就能控制它的开关、分辨率、帧率等。

我曾经在一个项目中,需要根据网络状况动态调整视频质量。说白了,就是通过修改视频轨道的约束条件来实现的。

4.3.1 启用/禁用视频轨道

调用 track.enabled = false 可以关闭视频轨道。注意,这不会释放摄像头,只是停止发送数据。摄像头指示灯可能还亮着。

const videoTrack = stream.getVideoTracks()[0];
// 关闭视频
videoTrack.enabled = false;
// 重新开启
videoTrack.enabled = true;

注意: 如果你真的想释放摄像头,应该调用 track.stop()。调用后,摄像头指示灯会熄灭。但 stop() 是永久性的,不能再重新开启,除非重新调用 getUserMedia

4.3.2 切换摄像头(前后置)

在移动端,经常需要切换前置和后置摄像头。怎么做?其实很简单:停止当前视频轨道,然后用新的约束重新获取流。

async function switchCamera() {
  // 假设当前是前置摄像头
  const constraints = {
    video: { facingMode: { exact: 'environment' } }, // 后置
    audio: true
  };
  // 停止旧轨道
  stream.getTracks().forEach(track => track.stop());
  // 获取新流
  stream = await navigator.mediaDevices.getUserMedia(constraints);
  video.srcObject = stream;
}

嗯,这里要注意:facingMode 的值可以是 'user'(前置)或 'environment'(后置)。但有些浏览器不支持 exact,所以最好加个降级方案。

4.4 音频轨道控制

音频轨道的控制逻辑和视频类似。但有几个特殊点,我单独拎出来说。

4.4.1 静音与取消静音

音频轨道的 enabled 属性,控制的是“是否发送音频数据”。设置为 false 就是静音。

const audioTrack = stream.getAudioTracks()[0];
// 静音
audioTrack.enabled = false;
// 取消静音
audioTrack.enabled = true;

你可能会问:这和 HTMLMediaElement.muted 有什么区别?

区别大了。 track.enabled = false 是停止发送音频数据,远端完全听不到。而 video.muted = true 只是本地听不到,数据还在发送。我在做通话测试时,经常用这个来模拟远端静音。

4.4.2 音量检测(选读)

有时候我们需要检测用户是否在说话。比如,实现“谁在说话”的指示器。这需要用到 AnalyserNode,但那是 Web Audio API 的范畴。这里简单提一下思路:

  1. MediaStream 创建 AudioContextMediaStreamSource
  2. 连接一个 AnalyserNode
  3. 定时读取频域数据,计算音量大小。

代码略长,我就不贴了。感兴趣的同学可以查一下 getByteTimeDomainData 的用法。

4.5 媒体流录制

录制媒体流,是 WebRTC 应用中的一个常见需求。比如录制会议内容、录制教学视频等。

浏览器提供了 MediaRecorder API,专门干这个事。它可以把 MediaStream 中的数据编码成视频文件(如 WebM)。

4.5.1 基本录制流程

let mediaRecorder;
let recordedChunks = [];

function startRecording(stream) {
  // 指定 MIME 类型,浏览器支持度不同
  const options = { mimeType: 'video/webm; codecs=vp9' };
  mediaRecorder = new MediaRecorder(stream, options);

  mediaRecorder.ondataavailable = (event) => {
    if (event.data.size > 0) {
      recordedChunks.push(event.data);
    }
  };

  mediaRecorder.onstop = () => {
    // 生成 Blob 并下载
    const blob = new Blob(recordedChunks, { 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);
  };

  mediaRecorder.start(1000); // 每秒触发一次 ondataavailable
}

function stopRecording() {
  mediaRecorder.stop();
}

关键点:

  • start(timeslice):传入时间片(毫秒),会定期触发 ondataavailable。如果不传,则只在 stop() 时触发一次。
  • ondataavailable:每次触发时,把数据块存起来。
  • onstop:录制结束时,把所有数据块合并成一个 Blob,然后下载。

4.5.2 录制时的注意事项

我在实际项目中发现几个坑,分享给你:

  • MIME 类型兼容性:不是所有浏览器都支持 video/webm; codecs=vp9。建议先检测:MediaRecorder.isTypeSupported()
  • 录制时长:如果录制时间很长,recordedChunks 数组会占用大量内存。可以考虑边录边上传,或者分段录制。
  • 音频采样率:录制时,音频的采样率可能会被改变。如果需要高保真,最好在 getUserMedia 时指定 sampleRate 约束。

我的经验: 有一次录制会议,发现录出来的视频没有声音。排查了半天,原来是 MediaRecordermimeType 不支持音频编码。后来改用 video/webm; codecs=vp8,opus 就好了。所以,记得检查 isTypeSupported

4.6 本章小结

这一章,我们深入了媒体流的内部。

你学会了:

  • MediaStreamMediaStreamTrack 的关系。
  • 如何获取、启用、禁用视频和音频轨道。
  • 如何切换摄像头。
  • 如何使用 MediaRecorder 录制媒体流。

这些操作,是构建实时通信应用的基础。下一章,我们会把这些知识串起来,做一个完整的视频通话 Demo。

嗯,今天就到这里。记得动手敲代码,光看是学不会的。


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