2、WebRTC 媒体流基础:getUserMedia 获取音视频、MediaStream 与 Track、浏览器端媒体约束

这一章我们聊聊 WebRTC 最核心的起点——怎么把摄像头和麦克风的数据拿到浏览器里来。说白了,就是 getUserMedia 这个 API。我刚开始接触 WebRTC 的时候,觉得这不就是调个接口嘛,后来踩了几个坑才发现,里面的门道比想象中多得多。

2.1 getUserMedia:打开媒体设备的第一把钥匙

getUserMedia 是浏览器原生提供的 API,用来请求用户的音视频权限。它返回一个 Promise,resolve 之后你会得到一个 MediaStream 对象。嗯,这里要注意——这个 API 只能在 HTTPS 或者 localhost 下使用,否则浏览器直接拒绝。

核心概念:getUserMedia 不是直接操作硬件,而是通过浏览器中间层去访问设备。浏览器会弹一个权限框,用户点了“允许”之后,数据流才开始流动。

来看一个最基础的调用示例:

// 最简单的音视频获取
async function startMedia() {
  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);
  }
}

这段代码看起来简单,但我曾经在一个项目里遇到过——用户明明插了摄像头,但 getUserMedia 一直返回 NotAllowedError。排查了半天,发现是用户之前点了“阻止”,浏览器记住了这个选择。解决办法是让用户去浏览器设置里重置权限。

避坑指南:我曾经在生产环境遇到过 NotReadableError,原因是另一个标签页已经占用了摄像头。WebRTC 不支持多页面同时使用同一个摄像头(某些浏览器除外)。解决方案是加一个设备占用检测逻辑,或者提示用户关闭其他标签页。

2.2 MediaStream 与 Track:流和轨道的区别

很多人搞混 MediaStream 和 MediaStreamTrack。我简单解释一下:MediaStream 是一个容器,里面可以放多个 Track(轨道)。每个 Track 代表一路独立的媒体数据,比如视频轨道、音频轨道。

你想想看,一个 MediaStream 可以同时包含:

  • 一个视频轨道(来自摄像头)
  • 一个音频轨道(来自麦克风)
  • 甚至多个音频轨道(比如同时采集麦克风和系统声音)

实际操作中,我们经常需要单独控制某个轨道。比如我只想关掉麦克风,但保留摄像头画面:

// 获取所有音频轨道并禁用
stream.getAudioTracks().forEach(track => {
  track.enabled = false;  // 静音,但不断开
  // 或者 track.stop();  // 彻底关闭轨道
});

// 获取视频轨道
const videoTrack = stream.getVideoTracks()[0];
console.log('视频轨道设置:', videoTrack.getSettings());

我记得有一次做远程医疗项目,需要动态切换摄像头(从前置切到后置)。当时我直接 stop() 了旧轨道,再重新 getUserMedia 获取新轨道,然后替换到 MediaStream 里。后来发现有个更优雅的方式——用 MediaStream.addTrack() 和 removeTrack() 来动态管理。

小技巧:track.getSettings() 可以拿到当前轨道的实际参数,比如分辨率、帧率、音量等。这在调试时特别有用,我经常用它来确认摄像头是否真的按照我的约束在工作。

2.3 浏览器端媒体约束:精确控制采集参数

光拿到流还不够,我们得告诉浏览器“我要什么样的流”。这就是 媒体约束(MediaConstraints) 的作用。约束分为两种:

约束类型 说明 示例
必选约束(required) 浏览器必须满足,否则 reject width: { exact: 1280 }
理想约束(ideal) 浏览器尽量满足,但不强制 width: { ideal: 1920 }
范围约束(min/max) 指定一个可接受的范围 frameRate: { min: 15, max: 30 }

来看一个实际项目中常用的约束配置:

const constraints = {
  video: {
    width: { min: 640, ideal: 1280, max: 1920 },
    height: { min: 480, ideal: 720, max: 1080 },
    frameRate: { ideal: 30, max: 60 },
    facingMode: { ideal: 'user' },  // 前置摄像头
    // 或者 'environment' 后置
  },
  audio: {
    echoCancellation: { exact: true },
    noiseSuppression: { ideal: true },
    sampleRate: { ideal: 48000 },
  }
};

async function startWithConstraints() {
  try {
    const stream = await navigator.mediaDevices.getUserMedia(constraints);
    // 检查实际获取到的参数
    const track = stream.getVideoTracks()[0];
    const settings = track.getSettings();
    console.log('实际分辨率:', settings.width, 'x', settings.height);
  } catch (err) {
    console.error('约束不满足:', err.name, err.message);
  }
}

这里有个坑——exact 约束很严格。我曾经在项目里写了 width: { exact: 1920 },结果用户的摄像头只支持 1280x720,直接报 OverconstrainedError。后来我改成 ideal 加 min 的组合,兼容性就好多了。

经验之谈:我个人习惯在正式环境里只用 ideal 和 min/max,尽量避免 exact。除非你明确知道用户的设备参数,否则 exact 很容易导致获取失败。

2.4 知识体系总览

下面这张图把本章的核心逻辑串起来了。从 getUserMedia 发起请求,到浏览器弹权限框,再到生成 MediaStream 和内部的 Track,最后通过约束控制采集质量。你想想看,整个流程其实就是一个“请求-授权-采集-控制”的闭环。

WebRTC 媒体流获取流程 getUserMedia 权限弹框 MediaStream VideoTrack AudioTrack 其他Track 媒体约束(Constraints) 控制采集参数 核心流程:getUserMedia → 权限确认 → MediaStream → 多Track分离 → 约束反向控制采集质量

2.5 实际项目中的常见场景

最后分享几个我在项目中遇到的真实场景,帮你理解这些概念怎么落地:

  • 场景一:视频通话默认开摄像头,但麦克风静音——用 getUserMedia 获取音视频流,然后立即 audioTrack.enabled = false,等用户点击“取消静音”再打开。
  • 场景二:动态切换摄像头——先 videoTrack.stop(),再重新 getUserMedia 指定 facingMode: 'environment',最后用 stream.addTrack(newTrack) 替换。
  • 场景三:限制带宽下的降级策略——如果网络差,通过 track.applyConstraints({ width: { ideal: 640 }, frameRate: { ideal: 15 } }) 动态降低分辨率。

个人建议:在调用 getUserMedia 之前,先用 navigator.mediaDevices.enumerateDevices() 列出所有可用设备,让用户选择用哪个摄像头或麦克风。这样用户体验会好很多,也避免了你硬编码设备 ID 导致的兼容性问题。

嗯,这一章的内容就到这里。媒体流是 WebRTC 的基石,搞懂了 getUserMedia、MediaStream、Track 和约束之间的关系,后面的编码传输环节你会轻松很多。