本篇是 WebRTC 101 系列 第 1 篇。原理/协议为主,代码点到为止——只给能验证结论的最小片段。
媒体采集:getUserMedia、轨道、约束与坑
进入连接之前,先搞清"采集"这件事本身。WebRTC 里采集与通话是分离的两个概念:getUserMedia 只是从本机设备拿到本地轨道,它还什么都没"传"出去;真正的传输发生在第 2 篇开始讲的 RTCPeerConnection 上。先把这个心智模型钉住:
MediaStream= 一组轨道的容器(一个视频 track + N 个音频 track)。MediaStreamTrack= 一条真实数据流(一路视频/一路音频)。轨道才是"有数据的东西"。
1. 三件套 API 的分工回顾
| API | 作用域 | 一句话 |
|---|---|---|
getUserMedia | 本机采集 | 产出一条 MediaStream |
RTCPeerConnection.addTrack | 加入会话 | 把本地轨道放进点对点连接去编码传输 |
RTCDataChannel | 数据传输 | 不走媒体轨道的任意数据(第 5 篇) |
所以"摄像头画面能进通话"要两步:先 getUserMedia 采集,再 pc.addTrack(track) 把轨道交给连接——第 2 篇会看到这一步其实体现在 SDP 的 m= 段里。
2. getUserMedia:最小可用片段
const stream = await navigator.mediaDevices.getUserMedia({
video: true,
audio: true,
});
const video = document.querySelector("video");
video.srcObject = stream; // 注意是 srcObject,不是 src
几个立刻要知道的硬规则:
- 返回 Promise,必须处理拒绝:用户拒绝授权 / 无设备 / 非安全上下文都会 reject;
- 必须安全上下文:
https或localhost才可用(file://不行); video.srcObject而不是video.src:后者给 URL 字符串,前者接受MediaStream;- 预览/挂断后要主动停轨道:
track.stop(),否则摄像头灯常亮。
3. 约束(constraints):别用魔法数字
采集参数的写法分三层:总开关 → 每个轨道的理想值 → 尽力而为。WebRTC 的约束不是"必须满足",而是理想区间,浏览器在设备能力范围内尽量靠近:
const stream = await navigator.mediaDevices.getUserMedia({
video: {
width: { ideal: 1280 }, // 理想宽度
height: { ideal: 720 },
frameRate: { ideal: 30, max: 60 },
facingMode: "user", // 前置摄像头;environment = 后置
deviceId: { exact: selectedId }, // exact = 必须精确匹配
},
audio: {
echoCancellation: true, // 回声消除(AEC)
noiseSuppression: true, // 降噪
autoGainControl: true, // 自动增益
},
});
idealvsexact:ideal是"尽量",exact是"没有就报错"。用deviceId: { exact }前先enumerateDevices()拿到真实 id。- 视频编码分辨率在编码端由浏览器决定,约束只是输入设备侧的期望——别指望约束 4K 就真给你传 4K。
4. 设备枚举与热插拔
const devices = await navigator.mediaDevices.enumerateDevices();
// [{ kind: "videoinput", deviceId, label, groupId }, ...]
navigator.mediaDevices.addEventListener("devicechange", () => {
// 拔插摄像头/耳机时重新枚举
});
label 在未授权前是空串——先 getUserMedia 授权,才能拿到可读的设备名(隐私设计)。
5. 回声、降噪与"处理管线"(原理)
很多人忽略:getUserMedia 出来的音频不是纯设备声音。浏览器(通过底层,如 Chromium 的 audio processing 与回声消除模块)在把它交给你之前,默认已经跑了 AEC(回声消除)、NS(降噪)、AGC(自动增益)。这是 VoIP 通话质量的基石——没有 AEC,扬声器出来的对方声音会再进麦克风,形成回声/啸叫。
采集 →(AEC/NS/AGC)→
MediaStreamTrack→ 编码。WebRTC 标准强制实现最小音频能力(Opus 等,见 RFC 7478/6716),保证端到端可互通;处理管线的具体品质则依赖浏览器实现。
想拿到"干声"(如做混音/音效)用 audio: { echoCancellation: false } 等关掉——但要明白代价。
6. 关键坑位清单(按踩中频率排序)
- 非安全上下文被拒:本地记得开
localhost,线上必须https; - 用户拒绝/无设备时没处理 reject:空实现会静默失败,加错误提示;
- 预览用
srcObject,有人写成video.src = stream导致黑屏; - 摄像头灯不灭:没有
track.stop(); - iframe 内嵌被权限策略挡住:父页面需给 iframe 加
allow="camera; microphone",否则getUserMedia直接 reject; - 约束被当"必须":用了
{ width: 1920 }以为必得 1080,其实浏览器可能给你别的——想锁定用exact并做好拿不到的准备。
7. 无设备环境怎么办(CI / 无头)
本地调试 / CI 想"假装有摄像头",浏览器启动参数给假设备即可,代码不用改:
# Chromium 系:自动应答授权 + 注入假摄像头/麦克风(生成彩色测试画面/蜂鸣声)
google-chrome --use-fake-device-for-media-stream \
--use-fake-ui-for-media-stream \
--allow-http ... # 配合 headless
WebDriver/Puppeteer 可在 launch() 时带这两个 flag。这是把"采集"环节做成可自动化测试的前提——采集本不该依赖真实硬件。
参考与来源
- MDN:MediaDevices.getUserMedia()、MediaStreamTrack、MediaTrackConstraints
- W3C:Media Capture and Streams
- 下一篇 信令与 SDP —— 把采集到的轨道真正"连起来"