Appearance
网页摄像头采集分辨率横竖屏异常:从 448×800 被裁剪谈起
更新: 6/27/2026 字数: 0 字 时长: 0 分钟
一份面向前端开发者的 WebRTC
getUserMedia分辨率适配踩坑文档
一、问题背景
我们要解决的是一个具体而典型的现象:同一台设备、同一个摄像头,用 getUserMedia 请求 448×800 时,浏览器返回的画面是把摄像头横向内容裁剪后挤成的竖屏——人脸两侧被切掉、内容变形;而请求 1080×1920 时,却能拿到正常完整的竖屏画面。
更让人困惑的是,通过 track.getCapabilities() 查询,该摄像头明明报告自己最大能采集 1920×1920。既然 1920×1920 这个"正方形"的能力都有,为什么低分辨率的 448×800 反而出问题?
核心矛盾就在这里:getCapabilities 报告的是"能力边界(范围)",不是"真实可用的离散分辨率组合"。 浏览器在拿到一个它无法原生采集的分辨率请求时,会用"就近匹配 + 裁剪缩放"的策略去凑,而这个凑的过程,正是横竖屏异常的根源。

二、踩坑点汇总
在动手分析原因之前,先把这一类问题里最容易踩的五个坑列清楚,方便对照自查:
坑 1:误把
getCapabilities的最大值当成"想要多大就给多大"。 它返回的是{min, max}区间,代表硬件 ISP 能力上限,不代表任意中间值都能原生输出。坑 2:把宽高的独立范围,当成可以任意组合的有效分辨率。
width.max=1920和height.max=1920是两个独立维度的范围,并不意味着 448×800 这样的具体组合存在于摄像头的原生采集模式列表里。坑 3:低分辨率请求被浏览器悄悄裁剪而无报错。 浏览器为满足约束会做 downscale + crop,过程静默、不抛异常,开发者很难第一时间意识到画面已被动过手脚。
坑 4:忽略摄像头只有有限几种"原生采集模式"(native capture formats)。 传感器实际只按少数几组固定分辨率/比例出图,其余分辨率都是软件二次加工的结果。
坑 5:请求的分辨率宽高比与摄像头当前朝向/原生比例不符。 当请求比例和原生帧比例对不上时,浏览器只能裁掉多余的边,这就是"横屏内容被切成竖屏"的直接表现。

三、错误原因拆解
3.1 getCapabilities 到底报告了什么
getCapabilities() 返回的是一组约束范围,典型结构如下:
js
const caps = track.getCapabilities();
// {
// width: { min: 1, max: 1920 },
// height: { min: 1, max: 1920 },
// aspectRatio: { min: 0.000xxx, max: 1920 },
// frameRate: { min: 1, max: 30 },
// ...
// }关键认知:width.max=1920 与 height.max=1920 是两个相互独立的一维区间。它"声明"了传感器在某个方向上最多能到 1920 像素,但绝不等于 (任意宽 ≤1920) × (任意高 ≤1920) 的笛卡尔积都是合法的原生采集模式。所谓"最大 1920×1920",只是把两个维度的上限拼在一起得到的能力包络,而非一个真实存在的输出格式。
3.2 摄像头其实只有几种"原生采集模式"
物理传感器(sensor)+ ISP 管线,出厂时只固化了有限几组原生采集格式,例如常见的:
- 1920×1080(16:9 横向)
- 1280×720(16:9 横向)
- 640×480(4:3 横向)
- 1920×1920(1:1,本例硬件支持的最大正方形)
注意:绝大多数手机/笔记本前置摄像头,传感器的原生出图方向是横向(landscape)。所谓"竖屏",在很多情况下是上层对横向帧做旋转/裁剪后呈现的结果。
3.3 为什么 1080×1920 正常,而 448×800 被裁
当你请求一个分辨率时,浏览器的处理逻辑大致是:在原生采集模式里找一个最接近、且能覆盖请求尺寸的模式,然后通过缩放(scale)和裁剪(crop)凑出你要的目标。
请求 1080×1920(9:16 竖屏): 浏览器找到原生的 1920×1080,做一次旋转得到 1080×1920,比例 9:16 恰好吻合,可以整帧缩放、几乎不裁剪就对齐目标——所以画面完整正常。换句话说,1080×1920 命中了一条"友好路径"。
请求 448×800(比例 ≈ 9:16,但尺寸很小): 这里有两层问题叠加:
- 摄像头没有 448×800 这样的小尺寸原生模式,浏览器只能拿一个大的横向原生帧(如 1920×1080)去 downscale。
- downscale 过程中,源帧是横向 16:9,目标是竖向 9:16,比例方向相反。浏览器为了塞进竖向画布,会沿宽度方向把横向画面的左右两侧裁掉,只保留中间一条,再缩放到 448×800。结果就是"横屏内容被裁成竖屏",人脸两侧丢失、主体变形。
本质上,两个请求走了不同的内部匹配分支:1080×1920 命中了可整帧对齐的路径,而 448×800 落入了"无原生模式 → 从横向帧强行裁切"的路径。问题不在分辨率高低本身,而在于请求尺寸/比例与原生采集模式的对齐程度,以及浏览器在不匹配时静默裁剪的行为。

四、正确规避方案
下面是一套可直接落地的实践路径,核心思路是:用高分辨率原生模式采集,自己掌控缩放裁剪,并始终以 getSettings 校验真实结果。
4.1 用高分辨率友好模式采集,而非直接请求小尺寸
不要直接向 getUserMedia 请求 448×800。改为请求一个能命中原生模式的高分辨率(如 1080×1920),拿到完整、未被意外裁剪的画面:
js
const stream = await navigator.mediaDevices.getUserMedia({
video: {
width: { ideal: 1080 },
height: { ideal: 1920 },
// 用 ideal 而非 exact,给浏览器协商空间,避免 OverconstrainedError
}
});4.2 自己用 Canvas / CSS 缩放裁剪到目标尺寸
需要 448×800 的最终画面时,在拿到高分辨率视频流后,由你自己通过 Canvas 绘制或 CSS object-fit 控制裁剪方式,而不是把裁剪决策权交给浏览器的黑盒:
js
// Canvas 方式:可精确控制裁哪一块、怎么缩放
const canvas = document.createElement('canvas');
canvas.width = 448; canvas.height = 800;
const ctx = canvas.getContext('2d');
function drawFrame() {
// 自行计算 sx,sy,sWidth,sHeight 决定从源帧裁切区域
ctx.drawImage(video, sx, sy, sWidth, sHeight, 0, 0, 448, 800);
requestAnimationFrame(drawFrame);
}这样裁剪逻辑透明可控,绝不会出现"内容被悄悄切掉"的意外。
4.3 始终用 getSettings() 校验真实生效的分辨率
约束是"请求",getSettings() 才是"实际生效值"。任何采集后都应校验:
js
const track = stream.getVideoTracks()[0];
const settings = track.getSettings();
console.log(settings.width, settings.height, settings.aspectRatio);
// 用真实值判断是否需要后续处理,不要假设请求值 = 实际值4.4 显式声明 aspectRatio 与 resizeMode
通过 resizeMode 明确告知浏览器你能否接受裁剪;aspectRatio 帮助协商到比例匹配的模式:
js
video: {
width: { ideal: 1080 },
height: { ideal: 1920 },
aspectRatio: { ideal: 9 / 16 },
resizeMode: 'none' // 'none' 倾向原生不缩放;'crop-and-scale' 允许裁剪缩放
}resizeMode: 'none' 让你更接近拿到原生帧,从而把裁剪控制权收回到自己手里。
4.5 探测真实可用的分辨率组合
不要信任 getCapabilities 的范围去推断组合。用"逐个尝试 + getSettings 回读"的方式,建立该设备真实可用的离散分辨率清单:
js
const candidates = [[1920,1920],[1920,1080],[1080,1920],[1280,720],[640,480]];
for (const [w, h] of candidates) {
try {
const s = await navigator.mediaDevices.getUserMedia({
video: { width: { exact: w }, height: { exact: h } }
});
const got = s.getVideoTracks()[0].getSettings();
console.log(`请求 ${w}x${h} → 实际 ${got.width}x${got.height}`);
s.getTracks().forEach(t => t.stop());
} catch (e) {
console.log(`${w}x${h} 不支持:`, e.name);
}
}
五、总结提醒
把这次踩坑沉淀成几条可以反复套用的原则:
getCapabilities只报"范围",不报"真实可用组合"。 看到max: 1920×1920不要理解成任意尺寸都能原生输出,它只是两个独立维度的上限拼接。低分辨率请求最危险,而非最安全。 越小、越偏离原生模式的尺寸,越容易触发浏览器的静默 downscale + crop,横竖屏不匹配时直接从横向帧里切一条出来。
高分辨率采集 + 自行缩放裁剪是最稳的链路。 先用能命中原生模式的尺寸(如 1080×1920)拿到完整帧,再用 Canvas/CSS 把裁剪决策权握在自己手里。
永远用
getSettings()验证,不要假设"请求即所得"。 约束是协商意向,实际生效值必须回读确认。比例方向比像素数量更关键。 横向传感器配竖向请求时,先想清楚比例如何对齐,再决定旋转还是裁剪,避免主体内容被默默切掉。
记住一句话:摄像头给你的是"它愿意按原生模式出的帧",浏览器只是帮你"凑"你要的尺寸——凑的过程不透明,所以越关键的画面,越要自己掌控采集与裁剪的每一步。
