使用方法
- 点「开始测试摄像头」。浏览器会弹出授权提示,选「允许」;如果点了「阻止」,页面会直接指出被拒绝以及怎么改回来,而不是一直转圈。
- 授权通过后画面立刻出现,下面四张卡片会换成设备真实值:实际分辨率、实际帧率、朝向和设备名。
- 「请求分辨率」「请求帧率」是你希望设备给出的值,不是保证值。设备给了别的档位时,卡片里显示的才是真正生效的那一个,两者不一致是正常的。
- 换设备时先授权,「摄像头设备」下拉框才会列出真正的设备名;选中另一颗后会重新取流。
- 带前后置的设备点「切换前后摄像头」,页面会对当前视频轨道调用
applyConstraints在线换镜头,不会重新弹权限框。 - 点「复制设备报告」,可以把当前的分辨率、帧率、宽高比、朝向和能力上限一次性复制成文本,用来贴到工单里。
- 测完点「停止并释放摄像头」,它会调用
track.stop(),系统的指示灯或标签页上的录制圆点会随之熄灭。
这个摄像头测试能解决什么问题
一是确认摄像头到底有没有被系统认到。开会前先看一眼:能出画面说明设备正常;授权成功却始终黑屏,问题多半在系统相机隐私开关被关,或者被别的软件占用了。
二是找出"对方看不到我"的原因。可能是选错了设备。授权之后设备名才会真正列出来(下面会解释为什么),换成另一颗再看画面有没有出现,一目了然。
三是核对这台设备真正给到了什么画质。你请求 1080p,设备可能只给 720p;卡片里的实际分辨率和帧率才是真话,用它来判断该不该换摄像头或调整设置。
四是确认指示灯有没有真的熄灭。很多人以为关掉预览就没事了,其实只要视频轨道还活着,指示灯就一直亮。本页把释放做成一个明确的动作,点了就是 track.stop()。
本页不做这些事:不录像、不拍照、不生成文件、不上传任何画面,也不做美颜和滤镜。它只回答两个问题——这颗摄像头现在能不能用、它实际给了多大的画面和多快的帧率。
检测原理:getUserMedia、getSettings 与 applyConstraints
这个摄像头测试的链路只有三步:getUserMedia 拿到视频流 → 把流挂到 <video> 上预览 → 从视频轨道读出参数。每一步都有必须先满足的前提。
第一步,页面必须在安全上下文里,权限必须由用户手势触发。navigator.mediaDevices.getUserMedia({ video: true }) 只在 HTTPS 或 localhost 下可用;用 file:// 直接打开本地 HTML,或者用 http:// 访问一个真实域名,navigator.mediaDevices 根本就是 undefined,连请求都发不出去。这也是本页把调用放进按钮点击处理里的原因:浏览器要求权限必须由用户手势触发,同时也避免一打开页面就弹权限框。
第二步,enumerateDevices() 在授权前拿不到设备名。这是最容易误解的一点。未授权时调用它,返回的数组里 label 是空字符串、deviceId 也是空的,你只会看到几项没有名字的占位。只有授权之后再次枚举,才能读到"HD Webcam"这样的真实名字。所以本页的设备下拉框是在授权回调里才去填充,而不是在页面加载时。
第三步,getSettings() 和 getCapabilities() 读的是两件不同的事。track.getSettings() 返回此刻真正生效的参数:width、height、frameRate、aspectRatio、deviceId、facingMode;track.getCapabilities() 返回这颗设备的能力上限:width 与 height 的 {min, max} 区间、frameRate 的区间,以及 facingMode 支持哪些取值。两者都是普通对象而不是 Promise,直接取用即可,但只有在流建立、轨道存活期间才有意义。想知道"为什么我要 1080p 却给了 720p",把两者一对比就清楚了:如果 getCapabilities().height.max 只有 720,那是设备上限,不是浏览器在偷懒。
第四步,切换摄像头靠 applyConstraints。对已有的视频轨道调用 track.applyConstraints({ facingMode: { exact: 'environment' } }),就要求这条轨道改用后置摄像头。用 exact 是因为多数手机只有在精确匹配时才会真的换镜头;写成 ideal 或不写,设备可能觉得"当前这颗也还行"而不动。约束里还有一层取舍:{ width: { ideal: 1280 } } 表示"尽量给 1280,给不了就退到最接近的一档",不会报错;而 { width: { exact: 1280 } } 表示"必须是 1280",设备没有这个精确尺寸就抛 OverconstrainedError,采集直接失败。所以日常用 ideal,需要严格标定时才用 exact。约束之间也有优先级:deviceId 与 facingMode 决定"用哪颗镜头",分辨率与帧率只在选定镜头后再谈。
第五步,释放必须落到 track 上。浏览器的摄像头指示灯(或标签页上的红色圆点)绑定的是处于 live 状态的视频轨道,不是页面。把 <video> 的 srcObject 置空、或者把元素从页面里删掉,都不会让指示灯熄灭;只有 track.stop() 让轨道进入 ended 状态,采集才真正停止。本页的「停止并释放摄像头」做的就是这件事,另外还监听了 pagehide,在离开页面时兜底释放一次。
完整示例(数值仅作演示)
页面打开时的默认条件:请求分辨率 1280×720、请求帧率 30 fps、镜像开、自动停止 0。这个摄像头测试给出的下面这组演示数值,与页面上的演示卡片、演示表格和对照表三处一致:
- 请求 1280×720 对应
width: { ideal: 1280 }与height: { ideal: 720 },宽高比是 16:9,约1.7778; - 这一档的像素数
1280 × 720 = 921,600,也就是 0.92 MP; - 若按未压缩的 YUV420(每像素 1.5 字节)估算,单帧是
921600 × 1.5 = 1,382,400字节,即 1.32 MiB; - 30 fps 时原始带宽
1382400 × 30 = 41,472,000字节/秒,约 39.6 MB/s,折合约 332 Mbps——这就是摄像头必须走压缩输出的原因,否则普通接口根本传不动。
把这几组数对上页面:演示卡片里的"实际分辨率 1280×720、实际帧率 30.0 fps"就取自这一档;演示表格里 1280×720、30.0 fps、1.7778 也来自同一组;对照表 1280×720 那一行的 0.92 MP、16:9、1.32 MiB、39.6 MB/s 与上面逐项对应。数值仅作演示:真实值必须以 getSettings() 现场读到的为准。
分辨率、像素与原始带宽对照表
下表每一行都能用同一条公式复核:像素 = 宽 × 高,YUV420 单帧 = 像素 × 1.5 字节,30 fps 带宽 = 单帧 × 30。带宽是未压缩的粗算值,实际传输会因为设备与浏览器使用压缩而小得多。
| 分辨率 | 像素数 | 宽高比 | YUV420 单帧 | 30 fps 原始带宽 |
|---|---|---|---|---|
| 320×240 | 76,800 | 4:3 | 0.11 MiB | 3.3 MB/s |
| 352×288 | 101,376 | 11:9 | 0.15 MiB | 4.4 MB/s |
| 640×480 | 307,200 | 4:3 | 0.44 MiB | 13.2 MB/s |
| 800×600 | 480,000 | 4:3 | 0.69 MiB | 20.6 MB/s |
| 1,024×768 | 786,432 | 4:3 | 1.13 MiB | 33.8 MB/s |
| 1,280×720(默认) | 921,600 | 16:9 | 1.32 MiB | 39.6 MB/s |
| 1,280×800 | 1,024,000 | 8:5 | 1.46 MiB | 43.9 MB/s |
| 1,280×960 | 1,228,800 | 4:3 | 1.76 MiB | 52.7 MB/s |
| 1,600×900 | 1,440,000 | 16:9 | 2.06 MiB | 61.8 MB/s |
| 1,920×1,080 | 2,073,600 | 16:9 | 2.97 MiB | 89.0 MB/s |
| 2,560×1,080 | 2,764,800 | 64:27 | 3.96 MiB | 118.7 MB/s |
| 2,560×1,440 | 3,686,400 | 16:9 | 5.27 MiB | 158.2 MB/s |
| 3,440×1,440 | 4,953,600 | 43:18 | 7.09 MiB | 212.6 MB/s |
| 3,840×2,160 | 8,294,400 | 16:9 | 11.87 MiB | 356.0 MB/s |
| 4,096×2,160 | 8,847,360 | 256:135 | 12.66 MiB | 379.7 MB/s |
别只看"像素更多就更清楚":4:3 的 640×480(0.31 MP)与 16:9 的 1280×720(0.92 MP),宽高比相差 0.4444。如果传感器是 4:3 而你要 16:9,浏览器会按较大者拉伸并居中裁剪,画面上下或左右会被切掉,视野变窄——这正是"换了分辨率后人只剩半个脸"的常见原因。1280×720 与 1920×1080 都是 16:9(都是 1.7778),互切不会裁剪。
边界与权限说明
- 必须 HTTPS 或 localhost。这是浏览器的硬性规定,不是本工具的限制。用
file://打开的页面拿不到navigator.mediaDevices,用http://访问真实域名同样拿不到,页面会明确告诉你属于哪一种。 - 权限只在点击后申请,而且只申请视频。打开页面不会弹任何授权框,也不会占用摄像头。请求里带
audio: false,不会连带申请音频权限。拒绝授权后浏览器会记住这个站点的选择,再次点击不会重新弹框,需要在地址栏左侧的站点设置里改回「允许」。 - 设备名要授权后才可见。授权前
enumerateDevices()返回的label是空串,所以下拉框在授权前只有一项占位提示。 - 请求值不等于最终值。
ideal只是偏好,设备可能给更接近的一档;卡片里的实际值来自getSettings(),以它为准。要看设备的真实上限,看getCapabilities()。 - 指示灯绑定的是轨道。只要视频轨道还是 live,指示灯就亮。隐藏预览元素、清空
srcObject都不算释放,必须track.stop()。 - 同一颗摄像头在多数系统上只允许一个程序使用。被别的软件占用时会拿不到流,通常抛
NotReadableError;关掉占用它的程序再试即可。 - 页面不保留任何画面。视频流只连到
<video>元素做预览,不写入磁盘、不发请求,也不写浏览器存储。刷新即清空。
常见问题
点了「开始测试摄像头」没反应,或者提示拿不到接口?
先看地址栏:必须是 https:// 或 localhost。用 file:// 双击打开的本地网页、或用 http:// 访问真实域名,浏览器都不会提供 navigator.mediaDevices,这与浏览器版本无关,是安全上下文的规定。
授权了,但设备下拉框还是空的?
这正是 enumerateDevices() 的特性:授权前拿不到 label,只能拿到空的占位项。授权成功后本页会重新枚举一次并填上下拉框。如果授权成功仍是空的,检查操作系统是否把它列成了"未插入设备"。
提示「设备被占用」或「无法读取」怎么办?
说明这颗摄像头已经被别的程序独占了,常见的是另一个浏览器标签、会议软件或录制软件。把占用它的程序关掉再点一次即可。部分驱动也允许共享,但独占模式下会返回 NotReadableError。
我要 1080p,卡片却显示 720p,是坏了吗?
不是。先点开页面里那张演示表格看真实上限:如果 getCapabilities().height.max 就只有 720,那这颗设备最高只能给 720p,请求 1080 也只是 ideal,浏览器会退到最接近的一档而不报错。想强制要 1080 得写 exact,但设备做不到时采集会直接失败。
为什么画面是黑的,指示灯却亮着?
说明轨道已经建立、设备确实被占用了,只是没有出图。常见原因有三个:系统的相机隐私开关把画面遮掉了、镜头前的物理遮挡没打开、或者设备被另一个程序以独占方式占用。指示灯亮本身就证明"浏览器已经拿到了这颗摄像头"。
点了「切换前后摄像头」没反应?
这个按钮走的是 track.applyConstraints({ facingMode: { exact: … } }),只有设备支持按朝向切换才有效。台式机的外接摄像头通常没有前后置概念,调用会被拒绝,页面会提示你改用「摄像头设备」下拉框手动选另一颗。手机上如果系统把它当成两颗独立设备,用下拉框切换同样有效。
测完直接关掉标签页可以吗?
可以,但更稳妥的是先点「停止并释放摄像头」。关闭标签页时浏览器通常也会释放采集,但如果你只是切到别的页面继续用,采集中会一直占用设备并让指示灯亮着。本页除了按钮,还在 pagehide 时兜底释放一次。
我的画面会被上传或保存吗?
不会。这个摄像头测试只把视频流连接到一个 <video> 元素,数据既不写入磁盘也不发请求。想自己验证,打开开发者工具的 Network 面板,点「开始测试摄像头」,可以看到没有任何请求发出。