Skip to content

移动端网页调用前置摄像头宽高反转问题踩坑实践

更新: 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 容器尺寸,要读流的真实分辨率):

js
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,改为声明你想要的宽高比,让浏览器自己挑分辨率:

js
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:运行时检测真实尺寸 + 动态纠正方向

由于不同设备/浏览器协商结果不可控,必须在拿到流之后实测一次,发现是横向就主动旋转或重新请求:

js
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 自适应填充(解决变形/黑边)

无论流是横是竖,让容器自适应填满,屏蔽方向差异带来的拉伸:

css
.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 按目标方向重绘,保证产出的图片方向正确:

js
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。这样无论真机返回横屏还是竖屏,都能稳定得到正确的竖屏效果。

处理方案:比例约束 + 运行时检测 + CSS 自适应