4. 媒体流处理:MediaStreamTrack详解、音视频轨道的启用与禁用、屏幕共享的实现(getDisplayMedia)、媒体流的录制与保存

好,我们进入第四章。这一章,咱们要跟媒体流里的「最小单元」打交道——MediaStreamTrack。说白了,一个视频流里可能有好几路轨道:一路摄像头、一路麦克风、甚至一路屏幕共享。每一路轨道,都是一个 MediaStreamTrack 对象。

我个人习惯把 MediaStreamTrack 比作「水龙头」。你可以打开它(启用),也可以关上它(禁用),甚至可以把它接到录像机里(录制)。这一章,我们就来拧拧这些水龙头。

4.1 MediaStreamTrack 详解

先看一个最基础的问题:MediaStreamTrack 到底是什么?

当你调用 getUserMedia 拿到一个 MediaStream 后,这个流里包含一个或多个 MediaStreamTrack。每个 track 代表一种媒体类型:"video""audio"

举个例子:

navigator.mediaDevices.getUserMedia({ video: true, audio: true })
  .then(stream => {
    const tracks = stream.getTracks();
    console.log(tracks); // 输出两个 MediaStreamTrack 对象
    tracks.forEach(track => {
      console.log(track.kind);   // "video" 或 "audio"
      console.log(track.label);  // 比如 "FaceTime HD Camera"
      console.log(track.readyState); // "live" 或 "ended"
    });
  });

这里有几个关键属性,我列个表方便你查阅:

属性 类型 说明
kind string "audio""video"
label string 设备名称,比如 "内置麦克风"
enabled boolean 控制轨道是否输出数据
muted boolean 是否被系统静音(只读)
readyState string "live""ended"
小提示: enabledmuted 不一样。enabled 是你主动控制的开关;muted 是系统层面的静音,比如你把麦克风物理拔掉了,浏览器会把它标记为 muted

4.2 音视频轨道的启用与禁用

这是实际开发中最常见的操作之一。比如用户想关掉摄像头,只留声音。或者反过来,静音麦克风但保留画面。

实现方式非常简单:

// 假设 stream 是 getUserMedia 返回的
const videoTrack = stream.getVideoTracks()[0];
const audioTrack = stream.getAudioTracks()[0];

// 禁用视频轨道(摄像头灯会灭)
videoTrack.enabled = false;

// 启用音频轨道
audioTrack.enabled = true;

嗯,这里要注意:enabled = false 只是让轨道停止输出数据,但并没有释放设备资源。摄像头灯虽然灭了,但其他应用还是拿不到这个摄像头,因为 getUserMedia 的权限还没释放。

避坑指南: 我曾经在做一个视频会议项目时,用户点了「关闭摄像头」按钮,我直接设了 enabled = false。结果用户再点「开启摄像头」时,发现摄像头灯不亮,但画面还是黑的。排查了半天,发现是 enabled 虽然设为 true,但轨道状态已经变成了 "ended"。正确的做法是:如果只是临时关闭,用 enabled;如果彻底不用了,调用 track.stop() 释放设备。

如果你想彻底释放设备,让其他应用也能用:

// 停止所有轨道
stream.getTracks().forEach(track => track.stop());

调用 stop() 后,readyState 会变成 "ended",这个轨道就废了,不能再复用。

4.3 屏幕共享的实现(getDisplayMedia)

屏幕共享是 WebRTC 里一个非常实用的功能。你想想看,远程会议、在线教学、游戏直播,哪个离得开它?

实现屏幕共享的核心 API 是 getDisplayMedia。它跟 getUserMedia 很像,但区别在于:

  • getUserMedia 获取摄像头/麦克风
  • getDisplayMedia 获取屏幕/窗口/浏览器标签页

基本用法:

async function startScreenShare() {
  try {
    const stream = await navigator.mediaDevices.getDisplayMedia({
      video: {
        cursor: "always" // 显示鼠标指针
      },
      audio: {
        echoCancellation: true,
        noiseSuppression: true
      }
    });
    // 把 stream 赋值给 video 元素
    const video = document.getElementById("screenVideo");
    video.srcObject = stream;
  } catch (err) {
    console.error("屏幕共享失败:", err);
  }
}

调用 getDisplayMedia 后,浏览器会弹出一个选择框,让用户选择共享整个屏幕、某个窗口、还是某个标签页。这个选择框是浏览器自带的,你没法自定义样式。

重点: getDisplayMedia 返回的 MediaStream 里,视频轨道是必须的,音频轨道是可选的。而且音频轨道只能捕获标签页的音频,不能捕获系统音频。如果你想捕获系统音频,需要借助其他手段(比如浏览器扩展)。

我个人习惯在屏幕共享时,把摄像头画面和屏幕画面合成一个流。怎么做呢?用 MediaStreamaddTrack 方法:

const screenStream = await navigator.mediaDevices.getDisplayMedia({ video: true });
const cameraStream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true });

// 把摄像头轨道加到屏幕流里
cameraStream.getTracks().forEach(track => {
  screenStream.addTrack(track);
});

// 现在 screenStream 里既有屏幕画面,又有摄像头画面
// 可以传给 RTCPeerConnection 了

嗯,这里有个坑:屏幕共享的轨道在用户点击「停止共享」后,会自动变成 "ended" 状态。你需要监听这个事件:

const screenTrack = screenStream.getVideoTracks()[0];
screenTrack.onended = () => {
  console.log("用户停止了屏幕共享");
  // 清理 UI,切换回摄像头模式
};

4.4 媒体流的录制与保存

录制媒体流,说白了就是把 MediaStream 里的数据一块块存下来。浏览器提供了 MediaRecorder API,用起来非常顺手。

基本流程:

  1. 拿到一个 MediaStream(来自摄像头、屏幕共享、或者远程 peer)
  2. 创建 MediaRecorder 实例
  3. 监听 ondataavailable 事件,把数据块存起来
  4. 录制结束后,把数据块合并成一个 Blob,下载或上传

代码示例:

let mediaRecorder;
let recordedChunks = [];

function startRecording(stream) {
  recordedChunks = [];
  // 指定 MIME 类型,浏览器支持的话就用 webm
  const options = { mimeType: "video/webm; codecs=vp9" };
  if (!MediaRecorder.isTypeSupported(options.mimeType)) {
    console.warn("当前浏览器不支持 vp9,回退到默认格式");
    mediaRecorder = new MediaRecorder(stream);
  } else {
    mediaRecorder = new MediaRecorder(stream, options);
  }

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

  mediaRecorder.onstop = () => {
    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);
  };

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

function stopRecording() {
  if (mediaRecorder && mediaRecorder.state !== "inactive") {
    mediaRecorder.stop();
  }
}
小技巧: start(1000) 里的参数是时间片,单位毫秒。意思是每 1 秒生成一个数据块。如果你不传这个参数,默认会在 stop() 时一次性生成所有数据。我个人习惯传一个时间片,这样即使录制过程中崩溃了,也能拿到部分数据。

录制时要注意几个问题:

  • MIME 类型兼容性: 不同浏览器支持的编码格式不一样。Chrome 支持 video/webm; codecs=vp9,Safari 可能只支持 video/mp4。建议先用 MediaRecorder.isTypeSupported() 检测一下。
  • 音频录制: 如果流里有音频轨道,录制的文件会包含声音。但要注意,屏幕共享的音频轨道只能捕获标签页音频,不能捕获系统音频。
  • 性能问题: 录制高清视频(比如 1080p)时,CPU 占用会比较高。如果用户设备性能差,可以考虑降低分辨率再录制。

4.5 本章知识体系

为了让你更直观地理解这一章的内容,我画了一张图:

MediaStreamTrack getUserMedia 摄像头 / 麦克风 getDisplayMedia 屏幕 / 窗口 / 标签页 RTCPeerConnection 远程媒体流 enabled = true/false 启用 / 禁用轨道 track.stop() 释放设备资源 MediaRecorder 录制并保存为文件 核心属性:kind / label / enabled / muted / readyState 核心事件:onended(用户停止共享时触发) 核心方法:addTrack / removeTrack / stop / applyConstraints

这张图把 MediaStreamTrack 的「来源」和「操作」分成了左右两半。左边是三种获取 track 的方式,右边是三种常见的操作。中间就是我们的主角——MediaStreamTrack

好了,这一章的内容就到这里。媒体流处理是 WebRTC 开发中绕不开的一环,尤其是屏幕共享和录制,在实际项目中几乎天天用到。希望你能动手敲一遍代码,把每个 API 都跑通。遇到问题别慌,多看看控制台的报错,多查查 MDN 文档。


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