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,最后通过约束控制采集质量。你想想看,整个流程其实就是一个“请求-授权-采集-控制”的闭环。
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 和约束之间的关系,后面的编码传输环节你会轻松很多。