浏览器摄像头采集与分辨率裁剪机制学习文档
更新: 5/16/2026 字数: 0 字 时长: 0 分钟
主题:
getUserMedia分辨率请求的真实行为、resizeMode的作用,以及如何在 Web 端实现可控的视频帧裁剪。
一、问题起点:浏览器请求的分辨率 ≠ 摄像头输出的分辨率
1.1 一个常见的误解
很多开发者会这样理解:
"我请求 400×800,摄像头最大 1280×720,所以摄像头会输出 400×720(被高度卡住)。"
这个心智模型是错的。 真实情况是:
- 摄像头驱动通常只暴露固定档位(如 320×240、640×480、1280×720、1920×1080…),不会输出 400×720 这种非标准模式。
- 你请求的 400×800 从来不是 sensor 输出的尺寸,而是浏览器加工后给你的"逻辑尺寸"。
- 真正发生的事:浏览器选了一个最接近 ideal 的原生模式(大概率是 1280×720),然后通过 裁剪 + 缩放 把它变成 400×800 给你。
1.2 约束写法的差异
// 写法 A:ideal(默认行为,柔性约束)
getUserMedia({ video: { width: 400, height: 800 } });
// → 浏览器尽量靠近,不行就挑最接近的原生档 + 后处理
// 写法 B:exact(强制约束)
getUserMedia({ video: { width: { exact: 400 }, height: { exact: 800 } } });
// → 硬件无法满足时抛 OverconstrainedError绝大多数项目应该用 ideal;只有当确实需要拒绝降级时才用 exact。
二、track.getSettings() 输出解读
一个真实例子:
{
"aspectRatio": 0.5,
"deviceId": "...",
"frameRate": 30,
"groupId": "...",
"height": 800,
"resizeMode": "crop-and-scale",
"width": 400
}2.1 字段含义对照表
| 字段 | 含义 | 关键解读 |
|---|---|---|
width / height | track 对外的逻辑输出尺寸 | 不等于 sensor 真实输出 |
aspectRatio | width/height | 0.5 即竖屏 |
frameRate | 帧率 | 30fps |
resizeMode | track 是否经过浏览器后处理 | 核心字段 |
2.2 resizeMode 的两个取值
| 取值 | 含义 |
|---|---|
none | 原生输出,sensor 给什么就是什么 |
crop-and-scale | 浏览器在中间做了裁剪 + 缩放 |
crop-and-scale 是定位"有没有发生二次加工"的最直接信号。 看到它就意味着:sensor 真实输出是别的尺寸,画面经过了 Chromium 的 VideoTrackAdapter 加工。
2.3 怎么知道 sensor 真实输出什么
const track = stream.getVideoTracks()[0];
console.log('settings:', track.getSettings());
console.log('capabilities:', track.getCapabilities());
// capabilities.width.max / height.max ≈ sensor 最大原生分辨率三、Chromium 内置裁剪规则(中心裁剪 + 比例对齐)
3.1 一句话规则
保持目标宽高比,从原生帧的几何中心裁出最大可能区域,再缩放到目标尺寸。
3.2 套用真实例子(原生 1280×720 → 目标 400×800)
| 步骤 | 计算 | 结果 |
|---|---|---|
| 原生比例 | 1280 / 720 | ≈ 1.778(横屏) |
| 目标比例 | 400 / 800 | 0.5(竖屏) |
| 决定主裁方向 | 目标更"瘦",需要横向大幅裁掉 | 高度用满 720 |
| 裁剪框宽度 | 720 × 0.5 | 360 |
| 裁剪 X 范围 | (1280-360)/2 ~ (1280+360)/2 | [460, 820] |
| 裁剪 Y 范围 | 完整 | [0, 720] |
| 输出 | 360×720 缩放至 400×800 | 视场被砍掉约 72% |
直观感受:画面变窄、被"拉近",因为横向只保留了中间 360 列像素。
四、不同分辨率请求之间的"中心稳定性"问题
4.1 为什么会问"切分辨率时中心内容会不会变"
如果应用依赖跨分辨率的像素对齐(人脸框、AR 标定、ROI 跟踪),中心是否稳定直接决定算法成败。
4.2 两种情形
| 场景 | sensor 实际输出 | 中心是否稳定 |
|---|---|---|
| 两次请求都让浏览器选 1280×720 原生模式,仅裁剪框宽度变化 | 同一原生模式 | ✅ 中心一致,仅 FOV 横向变窄 |
| 两次请求触发了不同 sensor mode(如 1280×720 vs 640×480) | 不同原生模式 | ❌ FOV、畸变、曝光、白平衡都会变 |
4.3 副作用清单(sensor mode 切换时)
- 视场角整体变化(不是简单中心裁剪)
- 自动曝光 / 自动对焦重新收敛 → 短暂闪烁
- 等效焦距变化
- 帧率有可能跟着变(某些档位强制 30/15)
4.4 稳妥策略
始终请求 sensor 最大原生分辨率,自己做下游裁剪。 这样无论目标尺寸怎么变,sensor mode 永不切换,中心区域像素级一致。
五、自定义裁剪区域:标准 API 的能力边界
5.1 哪些参数能控制裁剪框?
| 能力 | 能否指定裁剪坐标 |
|---|---|
getUserMedia constraints | ❌ 只能设宽高 |
track.applyConstraints() | ❌ 同上 |
resizeMode | ❌ 只有 none / crop-and-scale |
| PTZ(pan/tilt/zoom) | ⚠️ 极少硬件支持,且语义是"模拟云台",不是裁剪框 |
结论:标准 API 没有 cropX/cropY/cropW/cropH 这类参数。 浏览器内置裁剪固定为中心对齐。
5.2 要自定义裁剪,必须自己接管
核心思路:resizeMode: 'none' → 拿原生帧 → 自己裁。
六、三种自定义裁剪方案对比
6.1 方案 A:Canvas drawImage(兼容性最好)
const stream = await navigator.mediaDevices.getUserMedia({
video: {
width: { ideal: 1280 },
height: { ideal: 720 },
resizeMode: { ideal: 'none' }
}
});
const video = document.createElement('video');
video.srcObject = stream;
await video.play();
const canvas = document.createElement('canvas');
canvas.width = 400;
canvas.height = 800;
const ctx = canvas.getContext('2d');
// 9 参 drawImage:完全可控的裁剪框
const sx = 100, sy = 0, sw = 360, sh = 720;
function draw() {
ctx.drawImage(video, sx, sy, sw, sh, 0, 0, 400, 800);
requestAnimationFrame(draw);
}
draw();
// 想作为新流给下游用:
const croppedStream = canvas.captureStream(30);**适用:**录制、WebRTC 推流、对兼容性要求高的场景。
6.2 方案 B:MediaStreamTrackProcessor + VideoFrame.visibleRect(性能最好,Chromium 94+)
const [track] = stream.getVideoTracks();
const processor = new MediaStreamTrackProcessor({ track });
const generator = new MediaStreamTrackGenerator({ kind: 'video' });
const transformer = new TransformStream({
async transform(frame, controller) {
const cropped = new VideoFrame(frame, {
visibleRect: { x: 100, y: 0, width: 360, height: 720 },
displayWidth: 400,
displayHeight: 800
});
frame.close();
controller.enqueue(cropped);
}
});
processor.readable.pipeThrough(transformer).pipeTo(generator.writable);
const customStream = new MediaStream([generator]);**适用:**实时推流、AI 推理、对延迟和 GPU 利用率敏感的场景。
6.3 方案 C:CSS object-fit / object-position(仅展示)
video {
width: 400px;
height: 800px;
object-fit: cover;
object-position: 30% 50%;
}**适用:**仅页面展示,不影响 track 实际数据。下游(录制、推流、AI)拿到的还是原始/浏览器裁过的帧。
6.4 决策矩阵
| 需求 | 推荐方案 |
|---|---|
| 仅页面展示某个区域 | C(CSS) |
| 录制 / 推流 / AI,兼容性优先 | A(Canvas) |
| 同上,要求低延迟 + 新版 Chromium | B(VideoFrame.visibleRect) |
| 真正的硬件变焦/平移 | PTZ API(先 getCapabilities() 检查) |
七、最佳实践清单
- 不要假设你请求的分辨率就是 sensor 输出。 始终用
track.getSettings()验证,特别留意resizeMode。 - 跨分辨率切换前,先用
getCapabilities()摸清原生档位,避免无意中触发 sensor mode 切换。 - 对像素对齐敏感的应用,固定请求 sensor 最大原生分辨率 +
resizeMode: 'none',把裁剪逻辑写在自己代码里。 - **裁剪实现按场景选型:**展示用 CSS,处理用 Canvas,性能敏感用
VideoFrame。 - PTZ 不是软件裁剪的替代品,它是硬件能力,绝大多数设备不支持。
exact约束慎用,用前确认硬件支持,否则直接抛错没有降级空间。
八、调试速查代码片段
// 1. 看摄像头真实能力
const tmp = await navigator.mediaDevices.getUserMedia({ video: true });
const t = tmp.getVideoTracks()[0];
console.table(t.getCapabilities());
console.table(t.getSettings());
tmp.getTracks().forEach(x => x.stop());
// 2. 强制原生输出
const native = await navigator.mediaDevices.getUserMedia({
video: {
width: { ideal: 1280 },
height: { ideal: 720 },
resizeMode: { ideal: 'none' }
}
});
// 3. 在线切换分辨率(不重新 open 设备)
await native.getVideoTracks()[0].applyConstraints({
width: { ideal: 640 },
height: { ideal: 480 }
});九、关键概念回顾
| 概念 | 一句话定义 |
|---|---|
| sensor mode | 摄像头硬件实际工作的原生分辨率档位 |
| resizeMode | track 是否经过浏览器二次裁剪/缩放(none / crop-and-scale) |
| ideal 约束 | 柔性目标,浏览器尽力靠近 |
| exact 约束 | 强制目标,不满足则抛错 |
| 中心裁剪 | Chromium 默认策略:保持目标比例,从几何中心裁最大可行区域 |
VideoFrame.visibleRect | 目前 Web 平台最接近"参数化裁剪框"的官方 API |
掌握这套模型后,再遇到"为什么我请求的分辨率画面变形/中心漂移/视场变窄",都能直接定位到是 sensor mode、resizeMode 还是裁剪比例的问题。