移动端网页调用前置摄像头宽高反转问题踩坑实践
更新: 6/9/2026 字数: 0 字 时长: 0 分钟
一、背景情况
在 Web 端做摄像头采集(人脸识别、扫码、视频通话、AR 等)时,前端通常通过 navigator.mediaDevices.getUserMedia() 拉取摄像头视频流,并在 <video> 元素中渲染。
本次踩坑的具体表现是:
- 开发阶段(电脑浏览器):在
getUserMedia的 constraints 中设置width: 448, height: 800,电脑端采集到的是竖屏 448×800 的画面,渲染正常,方向符合预期。 - 生产环境(手机真机):同一份代码、同一套参数,手机浏览器打开后,摄像头实际采集到的内容变成了横屏 800×448——宽高发生了对调,画面横置、被裁切变形,与电脑端完全不一致。
这是一个典型的「模拟器表现正常、真机翻车」的场景。问题的本质不在代码写错了,而在于电脑摄像头与手机摄像头的物理朝向、传感器输出方向、以及浏览器对 constraints 的解释策略存在根本差异。下面从底层逐层拆解。

二、原因
宽高反转不是单一原因造成的,而是「传感器原生方向 + constraints 协商机制 + 设备朝向」三者叠加的结果。
2.1 摄像头传感器的原生输出方向是横向的
无论手机还是电脑,绝大多数摄像头传感器(CMOS)的原生扫描方向都是横向(landscape)。它吐出来的原始帧天然是「宽 > 高」,例如 1280×720、640×480。
- 电脑场景:摄像头横向、屏幕也横向,两者方向一致。你请求 448×800 时,浏览器/系统能在一致的坐标系下完成尺寸协商,最终给你竖向画面,看起来正常。
- 手机场景:你竖着拿手机使用,但前置摄像头传感器仍然按横向坐标系输出。传感器坐标系与设备显示坐标系之间存在一个 90° 的旋转关系(即 EXIF / sensor orientation)。
2.2 getUserMedia 的 constraints 是"协商"而非"强制"
width / height 在 W3C Media Capture 规范里属于 constraints(约束),而不是命令。浏览器会在设备真实能力范围内做一次「最佳匹配」:
- 当你写
{ width: 448, height: 800 }时,桌面浏览器会尽量返回接近这个比例的流。 - 而移动端浏览器(尤其 Android Chrome / 各厂商 WebView)在内部往往以传感器原生横向坐标系来解释这组数值:它会把你期望的「短边 448 / 长边 800」匹配到传感器的横向输出上,于是返回
width: 800, height: 448——数值看似被对调,实则是浏览器在传感器坐标系下做了「长边对长边、短边对短边」的匹配。
换句话说:你想要的是"竖向 448×800",但移动端把它理解成了"长边 800、短边 448 的横向流",方向信息在协商过程中丢失了。
2.3 设备朝向与渲染坐标系不一致
最终视觉上的「横置、变形」,是因为横向的视频帧被塞进了一个竖向的 <video> 容器。若 CSS 没有正确处理 object-fit,画面就会被拉伸或裁切,呈现为横屏效果。
可以用以下代码验证真实采集尺寸(不要信 CSS 容器尺寸,要读流的真实分辨率):
const stream = await navigator.mediaDevices.getUserMedia({ video: { width: 448, height: 800 } });
const track = stream.getVideoTracks()[0];
console.log('实际采集 settings:', track.getSettings());
// 手机端大概率输出 { width: 800, height: 448, ... } —— 印证宽高被对调
const video = document.querySelector('video');
video.addEventListener('loadedmetadata', () => {
console.log('视频帧真实分辨率:', video.videoWidth, 'x', video.videoHeight);
});小结:传感器原生横向 + 移动端按横向坐标系协商 constraints + 竖屏渲染坐标系,三者叠加导致了「电脑竖屏、手机横屏」的宽高反转。

三、处理方案
核心思路:不要写死 width/height 的绝对方向,改用「比例约束 + 运行时检测 + CSS 自适应」三层兜底。
方案 1:用 aspectRatio 替代写死宽高(推荐首选)
不要直接给 width: 448, height: 800,改为声明你想要的宽高比,让浏览器自己挑分辨率:
async function openCamera() {
// 期望竖屏,宽高比 448/800 = 0.56
const constraints = {
audio: false,
video: {
facingMode: 'user', // 前置摄像头
aspectRatio: { ideal: 448 / 800 }, // 用比例而非绝对尺寸
width: { ideal: 448 },
height: { ideal: 800 },
},
};
const stream = await navigator.mediaDevices.getUserMedia(constraints);
return stream;
}方案 2:运行时检测真实尺寸 + 动态纠正方向
由于不同设备/浏览器协商结果不可控,必须在拿到流之后实测一次,发现是横向就主动旋转或重新请求:
async function openCameraWithFix(videoEl) {
const stream = await openCamera();
const track = stream.getVideoTracks()[0];
const { width, height } = track.getSettings();
videoEl.srcObject = stream;
await videoEl.play();
// 期望竖屏,但实际拿到横屏 → 需要纠正
const isLandscape = width > height;
const needPortrait = true; // 业务需要竖屏
if (isLandscape && needPortrait) {
// 方式 A:CSS 旋转 90° 兜底(适合纯展示)
videoEl.style.transform = 'rotate(90deg)';
// 方式 B:若需对视频帧做后续处理(截图/上传),
// 建议用 canvas 旋转重绘,保证落地数据方向正确,见方案 3
console.warn(`采集为横屏 ${width}x${height},已按竖屏纠正`);
}
return { stream, width, height };
}方案 3:CSS 用 object-fit 自适应填充(解决变形/黑边)
无论流是横是竖,让容器自适应填满,屏蔽方向差异带来的拉伸:
.camera-wrap {
width: 100%;
height: 100dvh; /* 用 dvh 适配移动端动态视口,避免地址栏导致的高度抖动 */
overflow: hidden;
position: relative;
}
.camera-wrap video {
width: 100%;
height: 100%;
object-fit: cover; /* 填满容器、裁切多余,无黑边无变形 */
object-position: center;
}
cover会裁切但填满、不变形;若业务必须看到完整画面可改用contain(会有黑边)。
方案 4:需要把帧落地(截图/上传)时,用 canvas 统一方向
如果采集结果要截图上传到后端,仅靠 CSS transform 是不够的——CSS 旋转不改变实际像素数据。需用 canvas 按目标方向重绘,保证产出的图片方向正确:
function capturePortraitFrame(videoEl) {
const vw = videoEl.videoWidth;
const vh = videoEl.videoHeight;
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
if (vw > vh) {
// 横屏流 → 旋转 90° 输出竖屏画布
canvas.width = vh;
canvas.height = vw;
ctx.translate(canvas.width / 2, canvas.height / 2);
ctx.rotate(Math.PI / 2);
ctx.drawImage(videoEl, -vw / 2, -vh / 2, vw, vh);
} else {
canvas.width = vw;
canvas.height = vh;
ctx.drawImage(videoEl, 0, 0, vw, vh);
}
return canvas.toDataURL('image/jpeg', 0.9); // 方向正确的竖屏截图
}适配逻辑总结
| 层级 | 手段 | 作用 |
|---|---|---|
| 采集层 | aspectRatio + facingMode | 用比例协商,不写死方向 |
| 检测层 | track.getSettings() / videoWidth | 实测真实尺寸,判断是否横置 |
| 渲染层 | object-fit: cover + 100dvh | 自适应填满,屏蔽变形与视口抖动 |
| 落地层 | canvas 旋转重绘 | 保证截图/上传的像素数据方向正确 |
完整调用链建议:先用比例约束请求 → 拿到流后实测尺寸 → 横置则纠正 → CSS 填满渲染 → 截图走 canvas。这样无论真机返回横屏还是竖屏,都能稳定得到正确的竖屏效果。
