4、约束与配置:MediaTrackConstraints详解、分辨率/帧率设置、前后摄像头切换
上一章我们拿到了摄像头的数据流,但你可能发现了——画面糊得像马赛克,或者帧率低得跟幻灯片似的。别急,这不是设备的问题,是我们还没告诉浏览器「我要什么样的画面」。
这一章,我们就来聊聊怎么给摄像头下指令。说白了,就是通过 MediaTrackConstraints 这个对象,精确控制分辨率、帧率,甚至切换前后摄像头。
4.1 MediaTrackConstraints 是什么?
你可以把它理解成一张「需求清单」。你告诉浏览器:我想要 1280x720 的分辨率,帧率至少 30fps,优先用后置摄像头。浏览器会拿着这张清单去匹配设备,能完全满足最好,满足不了就降级。
它的基本结构是这样的:
const constraints = {
video: {
width: { ideal: 1280 },
height: { ideal: 720 },
frameRate: { ideal: 30 }
},
audio: true
};
每个约束项都可以用三种模式来指定:
| 模式 | 含义 | 示例 |
|---|---|---|
| exact | 精确值,必须满足,否则报错 | width: { exact: 1920 } |
| ideal | 理想值,尽量满足,不行就降级 | width: { ideal: 1280 } |
| min / max | 范围值,在区间内都可以 | width: { min: 640, max: 1920 } |
核心原则:能用 ideal 就别用 exact。exact 太死板,一旦设备不支持就直接罢工。ideal 是「商量着来」,用户体验好得多。
4.2 分辨率设置:别一上来就 4K
很多人觉得分辨率越高越好,上来就写 width: { exact: 3840 }。结果呢?要么摄像头不支持,要么画面卡成狗。
我个人习惯的做法是:先列一个「理想-保底」组合。
const constraints = {
video: {
width: { ideal: 1280, min: 640 },
height: { ideal: 720, min: 480 },
frameRate: { ideal: 30, min: 15 }
}
};
这样浏览器会优先尝试 1280x720@30fps。如果设备性能不够,它会自动降到 640x480@15fps。至少画面是流畅的,不会直接黑屏。
我在项目中遇到过一个问题:某款 Android 手机的前置摄像头只支持 640x480,但我写了 exact: 1280,结果 getUserMedia 直接抛了个 OverconstrainedError。嗯,从那以后我再也不敢随便用 exact 了。
4.3 帧率设置:流畅 vs 清晰
帧率决定了画面的流畅度。30fps 是视频通话的及格线,15fps 以下就能感觉到明显的卡顿。
但帧率不是越高越好。高帧率意味着每秒钟要处理更多帧画面,CPU 和带宽压力都会增大。我建议这样权衡:
- 视频通话:24-30fps 就够了,人眼在这个区间感觉很自然。
- 屏幕共享:5-15fps 就行,因为画面变化不频繁。
- 运动场景(比如直播健身):可以尝试 60fps,但要确认设备支持。
// 视频通话推荐配置
const constraints = {
video: {
frameRate: { ideal: 30, min: 20 }
}
};
// 屏幕共享推荐配置
const constraints = {
video: {
frameRate: { ideal: 10, min: 5 }
}
};
小技巧:如果你不确定设备支持哪些分辨率/帧率组合,可以用 enumerateDevices() 配合 getCapabilities() 来查询。后面章节会详细讲。
4.4 前后摄像头切换
手机上有前置和后置摄像头,怎么切换?关键就在 facingMode 这个约束上。
| facingMode 值 | 含义 |
|---|---|
user |
前置摄像头(自拍) |
environment |
后置摄像头(拍景物) |
left |
前置左侧摄像头(少见) |
right |
前置右侧摄像头(少见) |
切换的代码其实很简单:
// 切换到后置摄像头
async function switchToBack() {
const constraints = {
video: {
facingMode: { exact: 'environment' }
}
};
const stream = await navigator.mediaDevices.getUserMedia(constraints);
// 替换 video 标签的 srcObject
videoElement.srcObject = stream;
}
// 切换到前置摄像头
async function switchToFront() {
const constraints = {
video: {
facingMode: { exact: 'user' }
}
};
const stream = await navigator.mediaDevices.getUserMedia(constraints);
videoElement.srcObject = stream;
}
注意:切换摄像头时,一定要先停止旧的媒体流(调用 track.stop()),否则会报错。我曾经踩过这个坑——切换了三次摄像头,结果浏览器直接崩溃了。
4.5 完整示例:带切换功能的视频页面
下面是一个完整的 HTML 示例,包含了分辨率设置和前后摄像头切换:
<!DOCTYPE html>
<html>
<body>
<video id="video" autoplay playsinline></video>
<button onclick="switchCamera('user')">前置</button>
<button onclick="switchCamera('environment')">后置</button>
<script>
let currentStream = null;
async function switchCamera(facingMode) {
// 停止旧流
if (currentStream) {
currentStream.getTracks().forEach(track => track.stop());
}
const constraints = {
video: {
facingMode: { exact: facingMode },
width: { ideal: 1280, min: 640 },
height: { ideal: 720, min: 480 },
frameRate: { ideal: 30, min: 15 }
},
audio: true
};
try {
currentStream = await navigator.mediaDevices.getUserMedia(constraints);
document.getElementById('video').srcObject = currentStream;
} catch (err) {
console.error('摄像头切换失败:', err);
// 降级:去掉 exact,用 ideal 再试一次
constraints.video.facingMode = { ideal: facingMode };
currentStream = await navigator.mediaDevices.getUserMedia(constraints);
document.getElementById('video').srcObject = currentStream;
}
}
// 默认启动前置摄像头
switchCamera('user');
</script>
</body>
</html>
注意看降级逻辑:如果 exact 失败,我会用 ideal 再试一次。这样即使用户的设备不支持精确匹配,也能拿到一个可用的流。
4.6 本章知识体系
下面这张图帮你梳理了约束与配置的核心脉络:
4.7 避坑指南
最后,分享几个我踩过的坑:
- 不要同时用 exact 和 ideal:同一个属性你只能选一种模式,混着写会导致意外行为。
- 切换摄像头前一定要 stop():否则浏览器会报
NotReadableError,因为设备被占用了。 - 移动端注意 playsinline:在 iOS Safari 上,
<video>标签必须加playsinline属性,否则摄像头无法正常工作。 - 降级策略要写在 catch 里:不要指望一次 getUserMedia 就能成功,做好降级准备。
嗯,这一章的内容就到这里。约束配置是 WebRTC 里看似简单但坑最多的地方之一。你只要记住:用 ideal 商量着来,别用 exact 硬刚,大部分问题都能避免。