ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

H5调用摄像头与扫一扫实战:getUserMedia、二维码识别与兼容调试

2026/9/14 3:19:20 拓冰建站 浏览量
H5调用摄像头与扫一扫实战:getUserMedia、二维码识别与兼容调试 简介面向H5前端开发者的摄像头调用与扫码功能测试示例聚焦navigator.mediaDevices.getUserMedia拍照以及zeptoqrcode、html5-qrcode两种扫一扫实现适用于需要兼容PC端与手机端、且部署于HTTPS协议下的移动端页面调试场景。压缩包共10个文件包含4个JavaScript逻辑脚本、3个HTML页面、1个CSS样式及2张PNG参考图整体仅73KB结构轻量便于直接查阅。已有1023人学习适合初涉H5设备API或需要快速验证扫码方案的前端开发者参考。资料既给出了getUserMedia拍照调用核心代码也展示了从相册解析、实时摄像头扫码及相册选图解析的两种实现思路通过示例可快速跑通流程并对比不同方案在PC端与手机端HTTPS环境下的实际表现节省从零搭建测试环境的时间。1. 为什么 H5 里调摄像头和扫一扫不是“打开摄像头”这么简单拿到“H5 调用摄像头和扫一扫.zip”这套资源时很多人第一反应是input typefile acceptimage/*加上 capture 属性就够了。实际落地才发现摄像头流必须依赖navigator.mediaDevices.getUserMedia二维码识别又分成“相册图片解析”和“摄像头实时解析”两套方案二者在 PC 和手机上的授权行为、失败表现完全不同。这篇博文把拍照链路、两种扫一扫实现、HTTPS 与双端联调、以及可复用扫描组件一次性拆开当作可以直接抄作业的素材来用。适合正在做 H5 页面、App 内嵌 H5 或微信公众号 H5 的前端开发也适合要写摄像头兼容性测试用例的 QA。2. 从 getUserMedia 说起的 H5 拍照链路2.1 摄像头权限的前提secure context 与用户手势H5 里的“打开摄像头”并不是释放一个 native API而是浏览器按安全策略交出设备句柄。浏览器要求页面必须运行在 secure context 上https://或http://localhost被视为安全普通局域网 IP 加 HTTP 基本不行。对应到网络协议层面就是页面请求必须走 TLS没有 HTTPS 时navigator.mediaDevices直接是 undefined调用getUserMedia会得到TypeError这跟用户有没有点“允许”没有关系。另一个隐蔽条件是“用户手势”Chrome 会在用户点击事件触发时放行弹窗页面后台自动打开摄像头则容易被拦截。所以实测时要记住不能在DOMContentLoaded里直接调用openCamera()必须由按钮点击或路由返回后触发。2.2 一个兼容 PC 与 Android/iOS 的拍照函数async function openCamera(videoElement, facingMode environment) { if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) { throw new Error(当前环境不是 secure context或浏览器不支持 getUserMedia); } const stream await navigator.mediaDevices.getUserMedia({ video: { facingMode, width: { ideal: 1280 }, height: { ideal: 720 } }, audio: false }); videoElement.srcObject stream; await videoElement.play(); return stream; }facingMode在 PC 上通常没有前后摄之分浏览器会忽略手机上environment指定后置摄像头适合扫二维码和拍实物。width和height用ideal而不是exact因为很多 Android 机型并没有 1280x720 的精确采集档位exact会直接抛OverconstrainedError。测试时应把分辨率切到 1920x1080 与 640x480 各跑一次确认画质和性能的平衡点。audio: false是必须的扫码和拍照场景不需要麦克风也能少弹一次权限窗。function captureToCanvas(videoElement, canvasElement, shouldFlip false) { const w videoElement.videoWidth; const h videoElement.videoHeight; if (!w || !h) { throw new Error(video 还没有可绘制的画面); } canvasElement.width w; canvasElement.height h; const ctx canvasElement.getContext(2d); if (shouldFlip) { // 前置摄像头画面是镜像的水平翻转一下更符合自拍习惯 ctx.translate(w, 0); ctx.scale(-1, 1); } ctx.drawImage(videoElement, 0, 0, w, h); return canvasElement.toDataURL(image/jpeg, 0.92); }videoWidth必须在播放后才有值不能在loadedmetadata触发前直接读。drawImage的四个参数是目标坐标和宽高如果把 canvas 强制固定为 750 宽再直接drawImage(video, 0, 0, 750, 500)会导致画面裁切正确做法是先匹配videoWidth/videoHeight。返回的 dataURL 可以直接给img也可以经过fetch转 Blob 再走 multipart 上传。0.92是 jpeg 压缩质量对白底二维码图片来说降到 0.7 也不会影响解析率体积能小很多。2.3 分辨率与画面方向PC 与移动端差异表运行环境常见输出分辨率是否区分前后摄需要特别注意PC Chrome1280x720 / 1920x1080不区分用 enumerateDevices 取 deviceIdiOS Safari1920x1080 / 1280x720区分首次授权后需刷新Android WebView640x480 / 1280x720区分宿主 App 要处理 onPermissionRequest微信内置 H5不一定支持摄像头 API不一定需降级到 wx.scanQRCode这张表是做双端测试时最容易踩出问题的部分。PC 端如果有外接摄像头facingMode会被忽略但deviceId变化会导致上层维护的“默认摄像头”失效。手机端 iOS Safari 在用户首次拒绝后域名会进入 Safari 的摄像头权限黑名单网页端无法再次弹窗Android WebView 则是宿主 App 在原生层拦截请求与 H5 代码无关。测试用例里至少要有“页面级拒绝”“系统级拒绝”“摄像头被其他 App 占用”三个分支。3. 扫一扫的两种实现zepto qrcode 与 html5-qrcode 的选型边界3.1 为什么不能直接用摄像头原始帧识别二维码浏览器里的实时画面是一帧帧视频流页面脚本无法直接拿到摄像头输出的原始帧只能借助 canvas 的drawImage先把当前帧抽出来再交给二维码解码库做灰度化和定位。这就是为什么“调用摄像头拍照”和“扫一扫”看起来只差一步代码复杂度却差一截。扫一扫本质是“连续拍照 图像识别”浏览器没有现成的scan()API。项目里给出两条路轻量的zepto qrcode适合解析相册图片html5-qrcode适合从摄像头实时取流。选型不能只比解析速度要比取流方式和失败回调频率。3.2 zepto qrcode相册图片解析的实现与局限$(#qr-file).on(change, function () { const file this.files[0]; if (!file) return; const reader new FileReader(); reader.onload (e) { const img new Image(); img.onload () { try { const result qrcode.decode(img); alert(扫描结果: result); } catch (err) { console.error(解析失败, err); } }; img.src e.target.result; }; reader.readAsDataURL(file); });qrcode.decode内部会把img绘制到隐藏 canvas再逐像素扫描定位角点所以图片是否模糊、是否带白边、是否倾斜都会直接影响结果。zepto在这里只负责事件绑定真正干活的是 qrcode 库对 canvas 像素的同步遍历。局限很明显它没有逐帧识别能力用户必须先从相册选图适合“上传二维码凭证”的后台页面手机相册大图直接导入会卡上传前最好用 canvas 压到 800 像素宽。注意qrcode.decode是同步操作图片越大主线程阻塞越久不要让用户点完图片后没有 loading 反馈。3.3 html5-qrcode三种解析模式的接入import { Html5Qrcode } from html5-qrcode; const scanner new Html5Qrcode(qr-reader); // 模式一摄像头实时解析 async function startScan() { await scanner.start( { facingMode: { ideal: environment } }, { fps: 10, qrbox: { width: 250, height: 250 }, aspectRatio: 1.0 }, (text) { console.log(识别成功:, text); stopScan(); }, () { // 该回调在未识别时会高频触发刻意留空 } ); } async function stopScan() { if (scanner.isScanning) { await scanner.stop(); } } // 模式二从相册选择图片解析 fileInput.addEventListener(change, async () { const file fileInput.files[0]; if (!file) return; const text await Html5Qrcode.scanFile(file, false); console.log(相册解析结果:, text); });Html5Qrcode的start()第三个参数是成功回调第四个是失败回调失败回调几乎每帧都会触发在里面打日志会把 console 刷爆。fps: 10表示每秒尝试 10 次低端机应降到 5qrbox是取景框大小二维码不一定要占满全屏框太大反而容易扫到背景里的脏图案。scanFile的第二个参数传false表示不把源图显示在页面里直接走内存解析。这里已经是三合一的写法拍照解析、摄像头解析、相册图片解析都在同一套库内部实现。3.4 两种方案的性能与兼容性对比表维度zepto qrcodehtml5-qrcode摄像头实时解析不支持支持相册图片解析支持支持依赖复杂度zepto qrcode.jshtml5-qrcode内部集成 ZXing 思路失败表现抛异常高频回调适合场景已做图片上传的后台管理扫码枪替代、扫码登录这里有个容易被忽略的点html5-qrcode体积更大但内部把 ZXing 的解析逻辑搬到了浏览器端所以相册解析的成功率通常比纯qrcode更稳定。如果 H5 页面既要“拍照”又要“扫一扫”推荐直接上html5-qrcode少维护一套 canvas 处理逻辑如果只是做后台图片解析就用zepto qrcode资源占用更小改造成本也低。4. HTTPS 与双端测试一套可落地的排错流程4.1 为什么必须 HTTPS非 HTTPS 下发生了什么从网络协议视角看浏览器把摄像头视为敏感设备只有 TLS 加密的页面才允许navigator.mediaDevices.getUserMedia。如果访问地址是http://192.168.1.10:8080控制台多半会出现getUserMedia() no longer works on insecure origins这类提示navigator.mediaDevices也直接不存在。这不是页面 bug而是浏览器策略。验证方法是在 DevTools Console 执行console.log(window.isSecureContext); console.log(navigator.mediaDevices ! undefined);如果第一行是 false先把页面部署到 HTTPS或者用localhost做本地联调。Windows 上localhost加任意 HTTP 端口浏览器也认为安全但 Android 手机调试时不能用localhost指向电脑需要临时签名 HTTPS 证书否则扫码枪类功能测不了。4.2 PC 端测试权限开关、设备枚举、报错定位PC 端的测试路径比手机简单但它能快速暴露代码层面的问题。先打开 Chrome 的chrome://settings/content/camera确认站点是否被允许再在页面里枚举设备const devices await navigator.mediaDevices.enumerateDevices(); const cameras devices.filter(device device.kind videoinput); console.table(cameras.map(({ deviceId, label }) ({ deviceId, label })));这个调用不需要用户授权也能拿到 label如果 cameras 为空说明权限或驱动有问题。拿到deviceId后可以用它精确指定摄像头const stream await navigator.mediaDevices.getUserMedia({ video: { deviceId: { exact: cameras[0].deviceId } } });exact一旦指定设备被拔掉就会抛OverconstrainedError所以生产代码通常用ideal或者做 try/catch 降级。PC 端最常见的三个报错分别是NotAllowedError用户点了拒绝、NotFoundError没有可用摄像头、NotReadableError摄像头被其他软件占用。测试时把这三个分支都触发一遍再在页面上放对应的引导文案。4.3 手机端测试iOS Safari 与 Android WebView 的授权差异手机端不能只考虑网页逻辑还要兼顾系统弹窗和 WebView 的授权机制环境首次授权表现拒绝后恢复路径iOS Safari自动弹出系统相机权限设置 Safari 摄像头手动打开Android Chrome自动弹出系统权限地址栏左侧图标进入站点设置Android WebView 内嵌 H5由宿主 App 的 onPermissionRequest 决定必须在原生设置里授权微信内置 H5不一定暴露 getUserMedia常见做法是降级到微信 JS-SDKAndroid 上做 App 内嵌 H5 时如果 H5 页面清理过缓存摄像头权限状态不会自动重置属于“权限已授权但重新加载后还报错”的典型场景。先清掉 WebView 缓存再关闭页面重新进入能解决大部分残留状态。iOS WKWebView 则需要在原生 Info.plist 声明NSCameraUsageDescription很多内嵌 H5 摄像头打不开原生工程少了这一条描述是常见原因。4.4 扫一扫识别率低的调参步骤如果摄像头画面正常但一直扫不出来先按顺序调把fps从 10 降到 5低端机实时解析来不及完成时降帧比降分辨率更稳。缩小qrbox让二维码在取景框里占比更大减少背景干扰。强制使用后置摄像头前置摄像头解析距离太近容易糊。避免在扫码页同时做动画解析回调会阻塞主线程。调整后的典型配置scanner.start( { facingMode: { ideal: environment } }, { fps: 5, qrbox: { width: 240, height: 240 } }, onSuccess, () {} );facingMode: { ideal: environment }表示优先后摄手机没有后摄也不会直接报错如果写成{ exact: environment }在只有前摄的平板上会抛错需要额外 catch。qrbox调小后识别距离会变近所以还要引导用户把手机靠近二维码而不是站在原地等。5. 进阶封装一个可复用的扫描组件并处理弱光场景5.1 用类封装摄像头扫码的生命周期直接裸写scanner.start()的页面很容易在路由切换时忘记stop()导致摄像头灯一直亮。常见做法是封装成类把启动、停止、结果回调收敛到同一处class QRScanner { constructor(rootId) { this.scanner new Html5Qrcode(rootId); this.running false; this.lastResult ; this.lastTime 0; } async start({ fps 8, qrboxSize 220 } {}) { if (this.running) return; await this.scanner.start( { facingMode: { ideal: environment } }, { fps, qrbox: { width: qrboxSize, height: qrboxSize } }, (text) this.handleResult(text), () {} ); this.running true; } async stop() { if (this.running) { await this.scanner.stop(); this.scanner.clear(); this.running false; } } handleResult(text) { const now Date.now(); if (text this.lastResult now - this.lastTime 3000) return; this.lastResult text; this.lastTime now; this.onResult?.(text); } }running标志避免重复调用 start 导致多个取流循环lastResult与lastTime组成 3 秒去重窗口防止同一个二维码被连续识别后触发多次业务提交。真正的业务逻辑写在onResult回调里组件本身不关心是跳转页面还是发请求。5.2 连续扫码与防抖规则如果业务是连续扫多件商品就不能扫一次就永久停止。常见做法是每次成功后将取景区域遮罩等上层处理完成后再调start()重新扫描。去重窗口的时长要根据业务间隔调整扫码入库场景 3 秒足够领取优惠券场景需要 10 秒以上避免走开时被同一个码反复扫到。5.3 弱光环境手动控制闪光灯识别率低不全是算法问题光线不足也会导致帧内无法定位。部分 Android 摄像头可以通过torch打开补光灯const track stream.getVideoTracks()[0]; const capabilities track.getCapabilities?.() ?? {}; if (capabilities.torch) { await track.applyConstraints({ advanced: [{ torch: true }] }); }getCapabilities()不是所有浏览器都实现所以先做?.()防御。torch不是标准约束iOS Safari 目前基本不支持UI 层应该在打开摄像头后动态判断如果能力缺失就把“打开补光”按钮隐藏而不是让用户点击后无效。弱光场景记得保留一个手动开关不要只依赖浏览器自动曝光。本文还有配套的精品资源点击获取