Skip to content

浏览器摄像头采集与分辨率裁剪机制学习文档

更新: 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 约束写法的差异

js
// 写法 A:ideal(默认行为,柔性约束)
getUserMedia({ video: { width: 400, height: 800 } });
// → 浏览器尽量靠近,不行就挑最接近的原生档 + 后处理

// 写法 B:exact(强制约束)
getUserMedia({ video: { width: { exact: 400 }, height: { exact: 800 } } });
// → 硬件无法满足时抛 OverconstrainedError

绝大多数项目应该用 ideal;只有当确实需要拒绝降级时才用 exact。

二、track.getSettings() 输出解读

一个真实例子:

json
{
  "aspectRatio": 0.5,
  "deviceId": "...",
  "frameRate": 30,
  "groupId": "...",
  "height": 800,
  "resizeMode": "crop-and-scale",
  "width": 400
}

2.1 字段含义对照表

字段含义关键解读
width / heighttrack 对外的逻辑输出尺寸不等于 sensor 真实输出
aspectRatiowidth/height0.5 即竖屏
frameRate帧率30fps
resizeModetrack 是否经过浏览器后处理核心字段

2.2 resizeMode 的两个取值

取值含义
none原生输出,sensor 给什么就是什么
crop-and-scale浏览器在中间做了裁剪 + 缩放

crop-and-scale 是定位"有没有发生二次加工"的最直接信号。 看到它就意味着:sensor 真实输出是别的尺寸,画面经过了 Chromium 的 VideoTrackAdapter 加工。

2.3 怎么知道 sensor 真实输出什么

js
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 / 8000.5(竖屏)
决定主裁方向目标更"瘦",需要横向大幅裁掉高度用满 720
裁剪框宽度720 × 0.5360
裁剪 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(兼容性最好)

js
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+)

js
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(仅展示)

css
video {
  width: 400px;
  height: 800px;
  object-fit: cover;
  object-position: 30% 50%;
}

**适用:**仅页面展示,不影响 track 实际数据。下游(录制、推流、AI)拿到的还是原始/浏览器裁过的帧。

6.4 决策矩阵

需求推荐方案
仅页面展示某个区域C(CSS)
录制 / 推流 / AI,兼容性优先A(Canvas)
同上,要求低延迟 + 新版 ChromiumB(VideoFrame.visibleRect)
真正的硬件变焦/平移PTZ API(先 getCapabilities() 检查)

七、最佳实践清单

  1. 不要假设你请求的分辨率就是 sensor 输出。 始终用 track.getSettings() 验证,特别留意 resizeMode
  2. 跨分辨率切换前,先用 getCapabilities() 摸清原生档位,避免无意中触发 sensor mode 切换。
  3. 对像素对齐敏感的应用,固定请求 sensor 最大原生分辨率 + resizeMode: 'none',把裁剪逻辑写在自己代码里。
  4. **裁剪实现按场景选型:**展示用 CSS,处理用 Canvas,性能敏感用 VideoFrame
  5. PTZ 不是软件裁剪的替代品,它是硬件能力,绝大多数设备不支持。
  6. exact 约束慎用,用前确认硬件支持,否则直接抛错没有降级空间。

八、调试速查代码片段

js
// 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摄像头硬件实际工作的原生分辨率档位
resizeModetrack 是否经过浏览器二次裁剪/缩放(none / crop-and-scale
ideal 约束柔性目标,浏览器尽力靠近
exact 约束强制目标,不满足则抛错
中心裁剪Chromium 默认策略:保持目标比例,从几何中心裁最大可行区域
VideoFrame.visibleRect目前 Web 平台最接近"参数化裁剪框"的官方 API

掌握这套模型后,再遇到"为什么我请求的分辨率画面变形/中心漂移/视场变窄",都能直接定位到是 sensor mode、resizeMode 还是裁剪比例的问题。