拓冰建站拓冰建站
首页 / 资讯中心 / 正文

H5调用摄像头与扫一扫实战:getUserMedia、拍照和二维码识别全攻略

简介面向需要在移动端实现摄像头调用与扫码识别的前端开发者这份资源以可直接运行的示例代码为主集中演示了基于 HTTPS 环境下 H5 页面的两种典型能力一是通过 navigator.mediaDevices.getUserMedia 完成拍照二是扫一扫功能分别用 zepto qrcode 解析相册图片、用 html5-qrcode 实现拍照解析、实时摄像头解析以及从相册选图解析。压缩包共 10 个文件含 4 个 JavaScript 脚本、3 个 HTML 页面、2 张演示图片和 1 份 CSS 样式整体仅 73KB便于下载后快速打开浏览器进行 PC 端与手机端联调。资源围绕网络协议与测试场景展开尤其强调 HTTPS 部署要求适合前端初中级学习者或需要快速搭建扫码页面的开发者。目前已有 1023 人学习可帮助理解两种二维码识别方案的差异并直接获取各 HTML 页面与脚本的对应关系及调用流程。1. H5调用摄像头与扫一扫不是“能打开”就算通在移动端 H5 里调用摄像头最容易被低估的问题不是“怎么打开”而是“打开之后能不能在真机上稳定跑完权限申请、拍照、扫码、关闭摄像头这一整条链路”。本地 Chrome 调试一切正常放到微信内置浏览器或 iOS Safari 上就黑屏、白屏或者扫不了码这类现象几乎每个做过摄像头 H5 的都碰到过。这里把摄像头调用和扫一扫的落地路径拆开讲清楚从 getUserMedia 的权限链路到设备切换、canvas 拍照、二维码识别库选型再到真机调试时最容易翻车的几个细节。适合正在做 H5 摄像头功能或者需要给现有页面加扫一扫入口的工程师。2. 摄像头调用getUserMedia 的权限链路与兼容性边界2.1 最小可运行代码video 标签绑定视频流要调用摄像头最核心的 API 是navigator.mediaDevices.getUserMedia。它返回一个MediaStream然后把 stream 塞进 video 标签的srcObject页面就能实时显示画面。下面是最小可运行的例子video idcamera autoplay muted playsinline/video script const video document.getElementById(camera); const constraints { video: { facingMode: environment, width: { ideal: 1280 }, height: { ideal: 720 } }, audio: false }; async function openCamera() { try { const stream await navigator.mediaDevices.getUserMedia(constraints); video.srcObject stream; video.play(); } catch (err) { console.error(Camera error:, err.name, err.message); } } openCamera(); /script这段代码要注意几个点。facingMode取值environment表示后置摄像头扫一扫应该用后置如果写user则是前置。getUserMedia必须由用户主动操作触发比如点击按钮后调用不能页面加载完直接弹权限否则 Safari 会拒绝。autoplay和muted必须同时存在移动端不允许静音视频以外的自动播放playsinline这个属性是为了让 iOS Safari 不自动进入全屏播放否则 video 会铺满屏幕页面交互全被挡住。2.2 前置/后置切换枚举设备再重建约束很多场景需要让用户手动切换前后摄像头常见做法是先用enumerateDevices()拿到所有视频输入设备再根据设备的 label 或 facingMode 重建 constraints 并重新调用 getUserMedia。async function listVideoDevices() { if (!navigator.mediaDevices?.enumerateDevices) return []; const devices await navigator.mediaDevices.enumerateDevices(); return devices.filter(device device.kind videoinput); } async function switchCamera(deviceId) { if (!deviceId) return; if (window._stream) { window._stream.getTracks().forEach(track track.stop()); } const newConstrains { video: { deviceId: { exact: deviceId }, width: { ideal: 1280 }, height: { ideal: 720 } }, audio: false }; const stream await navigator.mediaDevices.getUserMedia(newConstrains); document.getElementById(camera).srcObject stream; window._stream stream; }这里deviceId是enumerateDevices返回的字符串像1a2b...在 WebView 或者混入其他 USB 摄像头后可能非常长用localStorage.setItem(cameraId, deviceId)存一下下次进入页面可以直接恢复到上次使用的设备。切换前用window._stream.getTracks().forEach(track track.stop())关闭旧流否则摄像头灯会一直亮而且后面重新申请时可能被系统判定为“设备占用”而崩溃。注意 iOS 15 的 Safari 对enumerateDevices返回的 label 有严格限制必须先获得摄像头权限才能看到非空 label否则只有 deviceId 和空白 label这是正常现象。2.3 权限策略HTTPS、iframe allow 与用户手势H5 调用摄像头在非 localhost 环境必须走 HTTPS这是浏览器安全策略HTTP 下navigator.mediaDevices通常为 undefined。如果页面被嵌在 iframe 里比如嵌入微信开发者工具或者第三方应用iframe 标签上必须有allowcamera否则即使主页面有权限子 iframe 也不能调用。iframe allowcamera * srchttps://example.com/scan/iframe用户手势这个约束也很硬getUserMedia 必须在 click、touchend 之类的事件回调里直接调用不能在setTimeout里包一层等几秒也不能在 fetch 回调里间接调用否则浏览器会报NotAllowedError。下面表格是几种常见异常和处理建议错误名称触发原因建议处理NotAllowedError用户拒绝或未正确触发手势提示用户点击开启或检查 iframe allowNotFoundError设备没有摄像头降级为文件选择NotReadableError设备被其他应用占用提示关闭其他摄像头应用后重试OverconstrainedErrorconstraints 条件无法满足去掉 exact改用 ideal如果出现黑屏但 console 没报错优先检查 video 是否真的拿到了 stream以及video.readyState是否大于 0。移动端常见坑是 video 元素样式没有设置宽高或者 canvas 绘制时忽略了实际视频尺寸这类问题在拍照环节体现得最明显。2.4 WebView 与微信内置浏览器的摄像头差异很多 H5 最终跑在 App 的 WebView 里而不是普通浏览器。微信内置浏览器比较特殊它会把 getUserMedia 的权限直接交给用户只要 HTTPS 域名在微信后台有校验一般直接弹窗。但在自定义 WebView 中比如 App 内嵌 H5原生层没有响应 WebChromeClient 的onPermissionRequest前端调用 getUserMedia 就会超时或返回 undefined。这里的关键是H5 侧没法绕过必须让原生同事在 WebView 注册权限处理。如果拿不到原生代码可以做一个降级通过判断 UA 里的 App 标识直接调原生 JSBridge 拍照或扫码。在公共方法里封装一层检测优先使用原生桥拿不到再走 getUserMedia是很多项目的常见做法。3. 拍照实现从视频流到图片的 canvas 缓冲3.1 将 video 帧绘制到 canvas 并导出图片摄像头打开后拍照本质就是截图把 video 的某一帧画到 canvas 上然后toDataURL或toBlob输出。这里比较常见的写法是function captureImage() { const video document.getElementById(camera); const canvas document.createElement(canvas); canvas.width video.videoWidth; canvas.height video.videoHeight; const ctx canvas.getContext(2d); ctx.drawImage(video, 0, 0, canvas.width, canvas.height); canvas.toBlob((blob) { if (!blob) return; const url URL.createObjectURL(blob); const img document.createElement(img); img.src url; document.body.appendChild(img); URL.revokeObjectURL(url); }, image/jpeg, 0.85); }这段代码里video.videoWidth和video.videoHeight是视频帧的真实尺寸不能直接用window.innerWidth因为摄像头传感器尺寸和 CSS 显示尺寸不一样。drawImage的五个参数把整帧画到 canvas 同尺寸画布上。toBlob回调里拿到的是压缩后的 Blob可以直接用FormData传给后端比toDataURL省内存。质量参数0.85是工程上比较平衡的默认值如果要更清晰可以调到0.92再高对 JPEG 来说体积增长明显肉眼几乎看不出差异。3.2 图片压缩与 EXIF 方向处理H5 前端压缩图片是上传场景里的刚性需求。上面toBlob就是最简单的压缩手段但真正的坑是 iOS 拍照后的 EXIF 方向。用前置摄像头拍照或者用户在相册选择一张竖图canvas 拿到的图片可能被自动旋转导致上传后横躺。原因是drawImage默认忽略 EXIF 中的 Orientation。处理方法要么在绘制前把 canvas 按方向信息旋转要么直接给 img 加 CSS 属性image-orientation: from-image。如果整个流程都在 canvas 内推荐使用createImageBitmap exif 读取。这里给一个兼容性较好的直接方案专门处理前置摄像头的镜像问题function drawImageWithOrientation(video) { const canvas document.createElement(canvas); canvas.width video.videoWidth; canvas.height video.videoHeight; const ctx canvas.getContext(2d); ctx.translate(canvas.width, 0); ctx.scale(-1, 1); ctx.drawImage(video, 0, 0, canvas.width, canvas.height); return canvas; }这个方案针对前置摄像头镜像的情况前置摄像头画面显示时本身就是镜像如果不做水平翻转导出图片别人看到的是反的。至于 EXIF 方向更简单粗暴的做法是让后端用 python Pillow 里的ImageOps.exif_transpose统一校正前端只保证原图数据不丢失。如果产品要求前端一步到位可以用 exif-js 读取 Orientation再按角度旋转 canvas网上有成熟代码但要注意 iOS 17 之后部分机型读取方向会返回 0仍需以后端矫正兜底。3.3 拍照上传FormData 组装与质量参数实测拍照后通常要走上传接口前端需要把 Blob 组装到 FormData 里这一步直接决定后端拿到的文件信息和命名canvas.toBlob((blob) { const form new FormData(); form.append(file, blob, scene.jpg); fetch(/api/upload, { method: POST, body: form }); }, image/jpeg, 0.8);form.append的第三个参数是文件名如果是以 Base64 上传后端解析需要额外处理用 FormData 最省事。质量参数不是越高越好根据实际项目经验给出一组参考值质量值1280x720 体积肉眼观感适用场景0.660-100KB有压缩痕迹列表头像0.8100-160KB可接受普通业务0.92200-300KB接近原图证件核验需要注意的是这个体积只是参考实际跟画面复杂度、噪点有关。如果一定要在 200KB 以内建议先固定分辨率再动态调质量用二分法连试几次而不是写死一个压缩参数。4. 扫一扫三种方案选型与参数调优4.1 html5-qrcode开箱即用的扫码库H5 扫一扫本质上是图像识别二维码纯前端主流方案基本是三种html5-qrcode、jsQR、zxing-js。html5-qrcode 封装程度最高支持直接调用摄像头扫码也支持上传图片识别适合时间紧、只需要识别二维码的场景。代码如下script srchttps://unpkg.com/html5-qrcode/script div idqr-reader stylewidth: 100%; max-width: 500px/div script const html5QrCode new Html5Qrcode(qr-reader); html5QrCode.start( { facingMode: environment }, { fps: 10, qrbox: { width: 250, height: 250 } }, (decodedText) { console.log(扫码结果:, decodedText); html5QrCode.stop(); }, (error) { /* 没有识别到就会回调不需要处理 */ } ).catch((err) { console.error(启动失败, err); }); /script这里start的第一个参数是摄像头约束第二个参数里面fps表示每秒尝试识别的次数越大对 CPU 压力越大qrbox是扫码框大小不是识别区域实际识别区域会按视频比例裁剪。第三个参数是每成功解析一次的回调第四个参数是每帧没识别到时的回调这个回调很频繁通常直接忽略。如果二维码是屏幕上的可以搭配调高亮度。4.2 jsQR从视频帧中手动识别jsQR 更有控制力适合需要自定义扫码界面的团队。用法是自己每隔一段时间从 video 画 canvas取imageData然后调用 jsQR 识别。示例import jsQR from jsqr; function scanFrame() { const video document.getElementById(camera); const canvas document.createElement(canvas); const ctx canvas.getContext(2d); canvas.width video.videoWidth; canvas.height video.videoHeight; ctx.drawImage(video, 0, 0); const imageData ctx.getImageData(0, 0, canvas.width, canvas.height); const code jsQR(imageData.data, imageData.width, imageData.height, { inversionAttempts: dontInvert }); if (code code.data) { console.log(识别结果:, code.data); return code.data; } return null; } setInterval(scanFrame, 100);这段代码里 jsQR 接收原始 RGBA 数组和宽高不需要 canvas 元素。inversionAttempts控制是否尝试反转颜色识别反色二维码场景设为attemptBoth会更稳但性能会降一半。手动实现的好处是可以用requestAnimationFrame控制识别帧率或只在摄像头画面稳定后识别。缺点是 jsQR 对模糊和畸变的鲁棒性不如商用 SDK二维码太远、太小、反光都会导致识别率下降。4.3 扫码性能关键参数与常见失败原因扫码识别的性能主要由分辨率、识别频率、二维码占比和光线决定。经验参数参数推荐值说明视频宽度1280 或 1920太高会增加检测耗时太小则小二维码无法解码fps812同时兼顾流畅与 CPUqrbox屏幕宽度 60% 左右过大容易跟上边缘反光区域对焦默认H5 无法直接控制摄像头自动对焦依赖系统行为识别间隔100ms过低会导致页面卡顿最常见失败原因不是算法而是摄像头没有聚焦到二维码。特别是 iPhone 的 H5 页面系统默认可能会把焦点放到远处近距二维码就会虚此时引导用户点一下视频区域部分浏览器会触发重对焦。另一个原因是二维码区域在视频画面占比太小哪怕识别区域只占屏幕的 20%也要保证二维码在整个视频帧里占 30% 以上。4.4 BarcodeDetector 原生 API 的渐进增强Chrome 内核的浏览器支持BarcodeDetector不需要额外引入 JS 库但 iOS Safari 不支持。如果只针对安卓 WebView可以用它替代 jsQR性能更好代码也更简洁if (BarcodeDetector in window) { const detector new BarcodeDetector({ formats: [qr_code] }); detector.detect(video) .then(codes { if (codes.length 0) console.log(codes[0].rawValue); }); }detect可以直接传 video 元素浏览器内部会截帧检测。但注意当前支持度不稳发布时不建议让业务完全依赖它。可以采用“先检测是否支持支持用原生不支持回退 jsQR”的策略在代码里做一个 Promise 封装把两种识别方式统一成同一个返回值。5. 真机调试与内存回收技巧5.1 用真机调试解决“本地正常真机黑屏”本地 chrome 用桌面摄像头没问题手机打开黑屏最常见的三个原因页面不是 HTTPS、用户在小程序 WebView 里没有在触发事件中调用、以及 video 元素被页面其他样式覆盖。真机调试最简单的方式是用 Chrome 的 Remote DebuggingAndroid 手机连 USB开启开发者模式在电脑chrome://inspect里选中页面可以实时查看 console 和 network。iOS 需要 macOS Safari 的“开发”菜单。如果没有数据线也可以用内网穿透或手动打包扫码但调试体验会很差。建议在代码里把 camera 错误主动上报navigator.mediaDevices?.getUserMedia(constraints) .then(stream { window._stream stream; video.srcObject stream; }) .catch(err { window.onCameraError window.onCameraError(err.name, err.message); });这样方便在远程日志里定位“黑屏”是权限错误、设备错误还是视频渲染问题。5.2 离开页面时释放摄像头与扫码器资源摄像头不释放会直接导致后续页面再打开时黑屏。需要在页面隐藏或卸载时调用 stop 方法。document.addEventListener(visibilitychange, () { if (document.hidden) { window._stream?.getTracks().forEach(track track.stop()); } }); window.addEventListener(pagehide, () { html5QrCode?.stop().catch(() {}); });如果是用 html5-qrcode要调用stop()并传入一个空回调否则内部摄像头关闭不了。还有一个进阶技巧在扫码页面把 video 的 muted 设置好如果页面有音频播放需求不要在扫码期间开背景音乐因为 iOS 会认为音频会话占用了麦克风通道导致摄像头授权失败或视频流中断。把扫码流程放到一个独立路由进页面开流离开页面立即停止比频繁复用同一个 video 更稳。本文还有配套的精品资源点击获取
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门