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 本章知识体系

下面这张图帮你梳理了约束与配置的核心脉络:

MediaTrackConstraints 知识体系 约束对象 分辨率 (width/height) 帧率 (frameRate) 摄像头 (facingMode) ideal / exact / min / max 常用:1280x720 / 640x480 视频通话:24-30fps 屏幕共享:5-15fps user / environment 切换前必须 stop() 旧流 核心原则 能用 ideal 就别用 exact,降级逻辑保体验

4.7 避坑指南

最后,分享几个我踩过的坑:

  • 不要同时用 exact 和 ideal:同一个属性你只能选一种模式,混着写会导致意外行为。
  • 切换摄像头前一定要 stop():否则浏览器会报 NotReadableError,因为设备被占用了。
  • 移动端注意 playsinline:在 iOS Safari 上,<video> 标签必须加 playsinline 属性,否则摄像头无法正常工作。
  • 降级策略要写在 catch 里:不要指望一次 getUserMedia 就能成功,做好降级准备。

嗯,这一章的内容就到这里。约束配置是 WebRTC 里看似简单但坑最多的地方之一。你只要记住:用 ideal 商量着来,别用 exact 硬刚,大部分问题都能避免。