90%摄像头黑屏或报错源于权限拒绝、非https协议、设备占用;须确保https/localhost、用户手势触发、video加载完成后再drawimage,并优先选用jsqr库。

用 MediaDevices.getUserMedia 获取摄像头流但黑屏或报错
浏览器不给权限、HTTPS 缺失、设备被占用,这三类原因占了 90% 的「打不开摄像头」问题。非 HTTPS 环境下 getUserMedia 直接拒绝调用,本地开发用 localhost 可以绕过,但 file:// 协议一定失败。
- 检查控制台是否报
NotAllowedError或SecurityError—— 前者是用户拒权,后者基本就是协议问题 - 确保页面运行在
https://或http://localhost下,否则直接放弃调试 - 用
navigator.mediaDevices.enumerateDevices()看设备列表里是否有videoinput,避免误用音频设备 - 别在
DOMContentLoaded里立刻调getUserMedia,等用户手势(如按钮点击)触发,否则 chrome 会静默拒绝
用 canvasRenderingContext2D.drawImage 抓帧但解码失败
扫码本质是「持续从视频流抽帧 → 转成图像数据 → 交给解码库识别」。很多人卡在 drawImage 后得到的 canvas 是空的,或者 getImageData 返回全黑 —— 这通常是因为视频还没开始播放就画了。
- 必须监听
video元素的loadeddata事件后再首次 draw,否则 canvas 内容未初始化 - canvas 尺寸要和 video 实际渲染尺寸一致(不是
video.width属性值),建议用video.videoWidth/video.videoHeight - 频繁调用
drawImage+getImageData会卡顿,建议用requestAnimationFrame控制频率(比如 15fps 足够),别用setInterval - 部分安卓 webview 对
getImageData返回的Uint8ClampedArray处理异常,可先用canvas.toDataURL('image/png')回退验证是否真有图像
选 jsQR 还是 quaggaJS?
quaggaJS 已停止维护,最后发布是 2019 年,对现代浏览器兼容性差,尤其在 ios safari 上常因 webgl 权限或 OffscreenCanvas 缺失崩溃;jsQR 是当前事实标准,纯 JS 实现、无依赖、支持 Data URL / ImageData / Uint8Array 输入,体积小(gzip 后约 25KB)。
- 别碰
quaggaJS,哪怕文档看着“功能多”,实际跑不起来的概率远高于jsQR -
jsQR解码前需把 canvas 数据转成灰度Uint8Array,它不接受 RGB 四通道数组,漏掉这步会返回NULL - 传入的图像宽高最好在 300–600px 区间:太小特征不足,太大计算慢且手机发热明显
- 解码失败时别立即重试,加个简单防抖(如 500ms 内忽略重复结果),避免同一帧反复识别出错
iOS Safari 扫码失败的硬限制
iOS 14.5+ 强制要求视频流必须启用 mediaStreamTrack.applyConstraints({ advanced: [{ focusMode: 'manual' }] }) 才能获得清晰帧,否则默认自动对焦延迟高、边缘模糊,jsQR 直接无法识别。这不是 bug,是 Apple 的隐私策略:模糊帧更难用于人脸识别。
立即学习“前端免费学习笔记(深入)”;
- 拿到
MediaStreamTrack后,立刻调用applyConstraints设置{ focusMode: 'manual' },即使你不需要手动调焦 - 某些旧版 iOS 不支持该约束,需包裹
try/catch,失败则降级但保留基础流程 - 别依赖
video.play()的promise结果来判断是否就绪 —— iOS 上它可能提前 resolve,但画面仍是黑的,仍得靠loadeddata - 全屏模式(
video.webkitEnterFullscreen())在部分 iOS 版本会中断媒体流,扫码界面尽量用 inline 播放
实际最难调的不是代码,是不同机型对自动对焦、曝光、白平衡的响应差异。哪怕同一行代码,在 iphone 12 和 ipad mini 6 上,可能一个秒扫一个要晃三秒——没有银弹,只能靠真机反复测。