2.1 getUserMedia 与约束参数 本节摘要:本节承接第 1 章的采集关,讲透「向浏览器要媒体」这门谈判术。约束对象是应用与浏览器之间的一纸合同:ideal 表达愿望、max 与 min 划定底线、exact 是硬性条款。合同写得越聪明,采集在千差万别的用户设备上成功率越高。本节给出约束语法的完整用法、能力回读的正确姿势,以及一套逐级降级的采集流程与全部错误分支的处理代码。 读完本节你能 阅读完本节,你应当能够: 写出带 ideal、max、min、exact 的约束对象,并说清各关键字的语义差别; 用 applyConstraints 在运行期调整参数,用 getSettings 回读实际生效值; 用 enumerateDevices 列出设备并在多个摄像头间指定采集来源;
本节摘要:本节承接第 1 章的采集关,讲透「向浏览器要媒体」这门谈判术。约束对象是应用与浏览器之间的一纸合同:ideal 表达愿望、max 与 min 划定底线、exact 是硬性条款。合同写得越聪明,采集在千差万别的用户设备上成功率越高。本节给出约束语法的完整用法、能力回读的正确姿势,以及一套逐级降级的采集流程与全部错误分支的处理代码。
阅读完本节,你应当能够:
入门教程里的采集代码通常就一行:video: true。它在一部分设备上给你 640x480,在另一部分上给你 1920x1080,还有一部分干脆给你一个竖屏比例。用户打开你的会议页面,看到自己脸被拉伸变形,或者画面卡成幻灯片,而你本地测试一切正常——这就是裸写 video: true 的代价。
约束机制的存在,就是为了把「设备给什么用什么」变成「应用声明要什么、浏览器量力给什么」。但很多开发者把它用成了非黑即白:要么完全不管,要么写死 exact。前者放弃画质控制,后者在支持不了的手机上直接采集失败。正确姿势是谈判式写法:想要什么用 ideal 表达,底线用 max 与 min 守住,极少数业务刚需才动用 exact。
约束对象的基本形态与三个关键字的语义:
// 约束三级写法:愿望、区间、硬性条款 const stream = await navigator.mediaDevices.getUserMedia({ audio: { echoCancellation: true, // 回声消除,会议场景必开 noiseSuppression: true, // 噪声抑制 autoGainControl: true, // 自动增益 }, video: { width: { ideal: 1280 }, // 愿望:越接近越好,不满足也能活 height: { ideal: 720 }, frameRate: { ideal: 30, max: 30 }, // 上限 30,实际值向下浮动 facingMode: 'user', // 前置摄像头,移动端有意义 }, });
ideal 是「软要求」:浏览器从设备能力里挑最接近的;max 与 min 是「硬边界」:超出区间的配置直接不参与挑选;exact 是「死命令」:设备做不到就抛 OverconstrainedError,协商直接失败。生产代码里 exact 应当只用于「语义上必须如此」的场景,比如视频会议强制要求回声消除开启——开着回声消除是能用与不能用的分界,而不是清晰与更清晰的分界。
约束谈成了不等于万事大吉,浏览器实际给了什么要看回读结果:
const track = stream.getVideoTracks()[0]; const settings = track.getSettings(); console.log(settings.width, settings.height, settings.frameRate); // 打印浏览器实际交付的参数,预览界面的宽高比应以此为准 // 运行期改需求:不必重新走一遍授权 await track.applyConstraints({ frameRate: { max: 15 } }); // 弱网时降帧率,画面平滑切换,授权状态与轨道保持不变
enumerateDevices 负责回答「用户都有什么设备」。注意它的一个时序陷阱:未授权前调用,设备 label 一律为空字符串——浏览器用这种方式防止网页在授权前探测用户硬件指纹。所以设备选择列表应当在首次授权之后构建:
const devices = await navigator.mediaDevices.enumerateDevices(); const cams = devices.filter((d) => d.kind === 'videoinput'); // deviceId 可作为约束再次传入 getUserMedia,实现指定设备采集

把前面的零件组装成一个生产可用的采集函数,这是本节的核心交付物:
// 逐级降级采集:理想配置失败自动退到下一级,并回报最终生效配置 async function acquireMedia({ onLevel } = {}) { const levels = [ { label: 'hd', video: { width: { ideal: 1280 }, height: { ideal: 720 }, frameRate: { ideal: 30, max: 30 } } }, { label: 'sd', video: { width: { ideal: 640 }, height: { ideal: 480 }, frameRate: { ideal: 15, max: 15 } } }, { label: 'bare', video: true }, ]; let lastErr = null; for (const lv of levels) { try { const stream = await navigator.mediaDevices.getUserMedia({ audio: { echoCancellation: true }, video: lv.video, }); const s = stream.getVideoTracks()[0].getSettings(); onLevel && onLevel(lv.label, s); // 让调用方知道落在了哪一级 return stream; } catch (err) { // OverconstrainedError 说明该级约束设备给不了,降级重试 // NotAllowedError 等授权类错误不降级,直接抛给上层提示用户 if (err.name === 'NotAllowedError') throw err; if (err.name === 'NotFoundError') throw err; lastErr = err; } } throw lastErr; }
三种典型误用也值得对照。误用一:把 exact 写在分辨率上,Mac mini 的外接采集卡与千元安卓机都过不了这一关。误用二:授权失败后立即二次弹窗——浏览器会记住拒绝并静默失败,正确做法是引导用户到地址栏手动恢复权限。误用三:拿 getCapabilities 当 universally 支持的特性,Safari 长期不支持它,能力探测要用 getSettings 加 try-catch 兜底。
⚠️ 常见坑:在页面加载时立刻调 getUserMedia,用户还没看清页面是什么就要做授权决定,拒绝率最高。等用户点了「开始通话」再采集,授权转化率会明显更好。
背景:某在线教育产品需要同时采集老师的「人像摄像头」与「手写板摄像头」,要求老师能随时指定哪个设备是哪路画面。
操作:先用 enumerateDevices 拿到全部视频设备(此时老师已授权过,label 可读),让老师在设置页把两台设备分别指定为人像与板书;随后以 deviceId 的 exact 约束分别发起两路 getUserMedia——这是 exact 的正当用途,因为「用哪台设备」正是语义刚需;板书那路额外用 applyConstraints 锁定较高分辨率,因为板书文字对清晰度敏感、对人像无关紧要。
结果:老师端两路画面稳定采集;但测试中发现部分老师的手写板是虚拟设备(驱动模拟的摄像头),不支持改分辨率,applyConstraints 抛出 OverconstrainedError。
解读:团队的处理是把板书路的约束从 exact 降为 ideal 加 try-catch,失败就沿用设备默认参数——虚拟设备的能力参差不齐,谈判式写法再次胜过硬性要求。这个案例的要点在于区分两类硬需求:「用哪台设备」必须 exact(错了就是采集错对象),「什么分辨率」应当 ideal(错了只是画质差异)。
变式:如果需求升级为「板书画面自动跟随老师拿起的教具切换」,就要结合 2.3 节的 replaceTrack:预先建立两路采集,用替换轨道的方式切换发送内容,而不是重新走一遍采集与协商。设备能力参差的应对思想是通的:能谈判就不下命令,能降级就不报错。