3、媒体流基础:MediaStream API详解、获取音频与视频轨道、控制摄像头与麦克风。

好,我们正式开始接触 WebRTC 里最实在的东西——媒体流。

你想想看,WebRTC 之所以叫 WebRTC,核心就是能传视频、能传音频。而这一切的起点,就是 MediaStream 这个 API。说白了,它就是浏览器里摄像头和麦克风的“数据管家”。

我个人习惯把 MediaStream 理解成一个“轨道容器”。它本身不生产数据,但它负责把音频轨道和视频轨道组织起来,统一调度。你拿到一个 MediaStream 对象,就等于拿到了一个或多个媒体源的访问权。

3.1 什么是 MediaStream?

MediaStream 是一个接口,代表一个媒体内容的流。一个流可以包含多个轨道,比如一个视频轨道和一个音频轨道。每个轨道都是一个 MediaStreamTrack 对象。

我在项目中遇到过不少新手,以为 MediaStream 就是摄像头画面。其实不对。它更像是一个“遥控器”,你可以通过它来开关麦克风、切换摄像头、甚至调整分辨率。

核心概念:

  • MediaStream:媒体流容器,管理多个轨道。
  • MediaStreamTrack:单个媒体轨道,可以是音频或视频。
  • getUserMedia():获取用户媒体设备的入口 API。

嗯,这里要注意:一个 MediaStream 可以包含多个同类型的轨道吗?理论上可以,但实际开发中很少这么干。大部分场景下,一个视频轨道、一个音频轨道就够了。

3.2 获取音频与视频轨道

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

我曾经踩过一个坑:在本地测试时一切正常,但部署到 HTTPS 环境后,getUserMedia 直接报错。后来才发现,这个 API 在安全上下文(HTTPS 或 localhost)下才能用。你想想看,要是随便一个 HTTP 网站都能调你摄像头,那还得了?

3.2.1 基本用法

// 请求摄像头和麦克风
async function getMedia() {
  try {
    const stream = await navigator.mediaDevices.getUserMedia({
      video: true,
      audio: true
    });
    // 拿到 stream 后,可以赋值给 video 元素
    const videoElement = document.getElementById('localVideo');
    videoElement.srcObject = stream;
  } catch (err) {
    console.error('获取媒体流失败:', err);
  }
}

这段代码很简单,但有几个细节值得注意:

  • video: true 表示请求视频轨道,默认使用前置摄像头(移动端)。
  • audio: true 表示请求音频轨道,默认使用系统默认麦克风。
  • 如果用户拒绝授权,Promise 会 reject,错误类型通常是 NotAllowedError。

3.2.2 精细控制:指定分辨率与帧率

在实际项目中,你很少会直接用 true 来请求媒体。更常见的做法是传入一个约束对象,精确控制视频的宽度、高度、帧率。

const constraints = {
  video: {
    width: { min: 640, ideal: 1280, max: 1920 },
    height: { min: 480, ideal: 720, max: 1080 },
    frameRate: { ideal: 30, max: 60 }
  },
  audio: {
    echoCancellation: true,
    noiseSuppression: true,
    sampleRate: 48000
  }
};

const stream = await navigator.mediaDevices.getUserMedia(constraints);

这里我解释一下约束的含义:

约束属性 说明 常用值
width / height 视频分辨率,支持 min、ideal、max 640x480、1280x720、1920x1080
frameRate 视频帧率,单位 fps 15、30、60
echoCancellation 回声消除,音频通话必备 true / false
noiseSuppression 降噪,减少背景噪音 true / false
sampleRate 音频采样率,越高音质越好 44100、48000

小技巧:ideal 值表示“最希望的值”,浏览器会尽量满足。如果设备不支持,会自动降级到最接近的值。所以别把 ideal 设得太离谱,否则可能拿到很差的画质。

3.3 控制摄像头与麦克风

拿到 MediaStream 之后,真正的控制才刚刚开始。你可以通过 getTracks() 方法获取所有轨道,然后对每个轨道进行启用、禁用、甚至替换。

3.3.1 启用/禁用轨道

最常见的需求就是“静音”和“关闭摄像头”。这不需要重新调用 getUserMedia,直接操作轨道即可。

// 获取所有视频轨道
const videoTracks = stream.getVideoTracks();
videoTracks.forEach(track => {
  track.enabled = false;  // 关闭摄像头画面(但设备仍在工作)
});

// 获取所有音频轨道
const audioTracks = stream.getAudioTracks();
audioTracks.forEach(track => {
  track.enabled = false;  // 静音麦克风
});

注意:enabled = false 只是让轨道不输出数据,但摄像头指示灯可能还亮着。如果你想彻底释放设备,需要用 stop() 方法。

3.3.2 停止轨道(释放设备)

我记得有一次做视频会议项目,用户挂断后摄像头灯还亮着,被投诉了。后来发现是忘了调用 stop()。

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

// 或者只停止视频轨道
stream.getVideoTracks().forEach(track => track.stop());

调用 stop() 后,轨道会进入 ended 状态,设备被释放,指示灯熄灭。这是一个好习惯,尤其是在单页应用中,页面不刷新,资源需要手动清理。

3.3.3 切换摄像头(前后置)

移动端开发中,切换前后摄像头是高频需求。这需要用到 MediaStreamTrack.applyConstraints() 或者重新调用 getUserMedia。

我个人更推荐重新调用 getUserMedia,因为切换设备时往往需要改变分辨率等参数,重新获取更干净。

async function switchCamera() {
  // 先停止当前视频轨道
  const videoTracks = localStream.getVideoTracks();
  videoTracks.forEach(track => track.stop());

  // 重新获取,切换 facingMode
  const newStream = await navigator.mediaDevices.getUserMedia({
    video: {
      facingMode: 'environment'  // 'user' 前置,'environment' 后置
    },
    audio: false
  });

  // 替换视频轨道
  const newVideoTrack = newStream.getVideoTracks()[0];
  const oldVideoTrack = localStream.getVideoTracks()[0];
  localStream.removeTrack(oldVideoTrack);
  localStream.addTrack(newVideoTrack);

  // 更新 video 元素
  videoElement.srcObject = localStream;
}

避坑指南:我曾经在切换摄像头时直接替换了 srcObject,结果导致音频轨道也丢失了。正确做法是只替换视频轨道,保留音频轨道。上面代码中的 removeTrack 和 addTrack 就是干这个的。

3.4 知识体系总览

为了让你更直观地理解 MediaStream 的整体结构,我画了一张图。这张图涵盖了从设备获取到轨道控制的完整链路。

MediaStream 知识体系总览 摄像头 / 麦克风 getUserMedia(constraints) MediaStream 对象 音频轨道 (AudioTrack) 视频轨道 (VideoTrack) enabled / stop / applyConstraints enabled / stop / applyConstraints 核心操作:getUserMedia → MediaStream → getTracks() → 控制轨道

3.5 常见问题与调试技巧

最后,分享几个我在实际开发中遇到的坑,希望能帮你少走弯路。

  • 权限问题:如果 getUserMedia 一直 reject,先检查浏览器地址栏是否有摄像头/麦克风图标。用户拒绝后,需要手动重置权限。
  • 多设备选择:使用 navigator.mediaDevices.enumerateDevices() 可以列出所有可用设备,然后通过 deviceId 精确指定。
  • 性能问题:高分辨率视频流会消耗大量 CPU。如果只是做视频通话,720p 30fps 就足够了,别盲目追求 4K。
  • 移动端兼容:iOS Safari 要求 getUserMedia 必须在用户手势(如点击按钮)中调用,否则会失败。

调试小工具:在 Chrome 开发者工具中,打开 chrome://webrtc-internals 可以查看所有媒体流的详细信息,包括分辨率、帧率、丢包率等。这是排查问题的利器。