news 2026/9/26 20:33:32

H5扫码实战:jsQR与html5-qrcode选型与避坑指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
H5扫码实战:jsQR与html5-qrcode选型与避坑指南

简介:面向H5页面与uni-app开发者的扫码功能实现包,整合jsQR与html5-qrcode两种解码方案,帮助前端工程师在浏览器中快速完成摄像头扫码、图片识别与跨端集成。压缩包共43个文件,以JavaScript脚本、TypeScript类型定义、Vue页面组件及JSON工程配置为主,另有HTML示例页、PNG测试图片和说明文档,整体仅488KB,目录结构清晰,便于按模块取用。目前已有3586人学习下载,适合需要快速落地扫码能力的Web与跨平台项目。资源内含可直接运行的uni-app项目骨架,包含manifest.json、pages.json、App.vue等工程文件,以及两种解码库的调用示例、常用工具函数和页面逻辑;配套的静态二维码图片可用来验证解析结果。通过对照该包,可系统掌握视频流获取、逐帧图像抽取、二维码定位识别、错误容错及HTTPS环境适配等完整流程,同时也可作为在现有工程中引入H5扫码功能的参考起点。

1. h5 扫码不是吊起摄像头就完事:jsQR 与 html5-qrcode 到底解决什么问题

在 H5 里做扫码,最容易遇到的黑匣子就是“摄像头已经亮了,二维码也怼在镜头前了,但结果一直是 undefined”。很多人以为 h5 扫码就是打开摄像头,其实摄像头只负责取景,二维码解析是另一套算法。h5 扫码这个需求要拆成“拿视频流”和“从图像帧里解码”两件事。jsQR 是一个纯前端二维码解码库,给它一张图像的像素数组,它返回二维码内容;html5-qrcode 则把摄像头调用、扫码框、连续识别这些环节打成包,底层识别用的同样是 jsQR。两者的关系,类似发动机和整车。这篇文章按落地顺序讲:先讲 jsQR 怎么搭一个最小扫码器,再讲 html5-qrcode 怎么少写代码,最后把微信、企业微信、App 内嵌 WebView 里的高频坑说清楚。适合正在做 H5 扫码、不想被手机型号和容器兼容性反复折磨的开发者。

2. 自己写一个最小 h5 扫码器:jsQR 的摄像头、画布与解码循环

2.1 getUserMedia 打开摄像头:分辨率、环境与前置摄像头的选法

在写 jsQR 之前,得先把摄像头打开。浏览器侧负责这件事的是navigator.mediaDevices.getUserMedia,它只在安全上下文里可用:线上必须 HTTPS,本地可以把 localhost 当成例外。我习惯优先让系统挑后置摄像头,因为扫码场景里,用户多数是去扫别人出示的二维码,前置摄像头拍出来是镜像,二维码方向一错,识别率就下来了。

async function openCamera(videoEl) { const stream = await navigator.mediaDevices.getUserMedia({ audio: false, video: { facingMode: { ideal: 'environment' }, width: { ideal: 1280 }, height: { ideal: 720 } } }); videoEl.srcObject = stream; await videoEl.play(); return stream; }

facingMode: { ideal: 'environment' }是让浏览器优先选择后置摄像头。ideal表期望,不强制;设备没有后置时也会退回前置。改成{ exact: 'environment' }会强制抛OverconstrainedError,我一般不用 exact。width/height同样给 ideal,720p 而不是 1080p 是因为解码耗时会随像素量上涨,二维码识别并不需要 4K 画面,分辨率太高反而容易掉帧。

另一个容易被忽略的点:videoEl.play()在部分 Android WebView 里可能一直 pending。如果视频画面出不来,先检查 video 有没有playsinline属性,这个坑在第 4 章会再展开。

每次扫码结束后记得把摄像头停掉,否则指示灯常亮,用户会很慌。停止的方式是遍历轨道调用stop():

function stopCamera(stream) { stream.getTracks().forEach(track => track.stop()); }

这个函数虽然简单,但真正会写的人不多。很多扫码页从 A 页面跳到 B 页面后,摄像头一直开着,再回来时浏览器报NotReadableError,就是因为上一个页面的流没有被释放。

2.2 把视频帧画到 canvas:为什么必须用 canvas 而不是直接截图

jsQR 不认视频元素,它认的是ImageData对象,也就是一帧画面展开后的 RGBA 像素数组。所以要想让 jsQR 工作,必须把视频当前帧画到一个 canvas 上,再getImageData取像素。有人会偷懒用video.toDataURL()先转 base64,再走一遍图片解析,这在单次截图上没问题,但做成每秒 10 帧的连续扫码,性能和内存都扛不住。

const scanCanvas = document.createElement('canvas'); const scanCtx = scanCanvas.getContext('2d', { willReadFrequently: true }); function captureFrame(videoEl) { const vw = videoEl.videoWidth; const vh = videoEl.videoHeight; scanCanvas.width = vw; scanCanvas.height = vh; scanCtx.drawImage(videoEl, 0, 0, vw, vh); return scanCtx.getImageData(0, 0, vw, vh); }

drawImage(videoEl, 0, 0, vw, vh)会把 video 当前帧完整画到 canvas。这里有个关键参数:videoWidth和videoHeight是视频源的真实分辨率,不是 video 元素的 CSS 尺寸。如果元素设置了width="100%"的样式,video.videoWidth仍旧是原始分辨率。如果 canvas 尺寸小于视频尺寸,drawImage 会自动缩放,这个缩放是免费的,等于帮 jsQR 降采样。

我一般会把 canvas 宽度压到视频宽度的一半,比如源 1280 就画到 640,解码耗时会明显下降。scanCtx.getContext('2d', { willReadFrequently: true })里的willReadFrequently值得单独说:它告诉浏览器这个上下文会被高频调用getImageData,浏览器会为它选择更适合回读像素的存储方式。不加这个参数,在某些浏览器上连续取像素会慢得明显。

还有一个边界是 canvas 的宽高比不能和视频不一致,否则二维码会被拉伸变形,jsQR 的定位图形检测会失败。如果扫码框不是满屏,只截取框内区域,那就在 drawImage 的第 2 到第 5 个参数里设定源区域,这样还能顺便减少背景干扰,我最后一章会再讲。

2.3 调用 jsQR 解码:返回结果、失败判断与两个关键参数

图像数据准备好后,就可以交给 jsQR。函数签名是jsQR(imageData, width, height, options)。第一个参数必须是Uint8ClampedArray或直接传ImageData.data;width 和 height 要和 imageData 的宽高一致,填错会出现解码定位错误或者直接返回 null。

import jsQR from 'jsqr'; function decodeFrame(imageData) { const result = jsQR( imageData.data, imageData.width, imageData.height, { inversionAttempts: 'attemptBoth', greyScale: false } ); return result ? result.data : null; }

识别成功返回result.data,内容可能是 URL、JSON 或普通文本;识别失败返回null,不是抛异常,所以不用 try/catch 包这一层。options 里我调的最多是inversionAttempts。扫码时常见两种反向画面:一种是黑底白字的二维码,一种是屏幕或镜头反光导致明暗反转,attemptBoth会让 jsQR 尝试正常和反色两种识别路径,成功率更高,但耗时也更高。如果你明确只扫自己生成的白底黑码,比如内部系统的固定码,可以设成dontInvert,解码速度会有可感知的提升。

greyScale默认是 false,我保持默认。有的教程会建议先手动灰度再传给 jsQR,实际上 jsQR 内部会做自适应二值化,提前转灰度可能丢失二维码边缘的对比度。

2.4 连续扫码的 requestAnimationFrame 循环与节流:别让手机变成暖手宝

实时扫码是一个持续动作,摄像头每秒送来 30 帧,但不该每帧都去调 jsQR。我之前为了追求“秒识别”,把 fps 调高,结果是识别没快多少,手机先烫了,iOS 上还出现热降频,识别反而更慢。现在的做法是:用 requestAnimationFrame 保持视频预览的流畅,但用一个时间戳门槛控制实际解码频率。

let scanning = true; let lastDecodeTime = 0; let activeStream = null; function loop(timestamp) { if (!scanning) return; if (timestamp - lastDecodeTime > 200) { const imageData = captureFrame(videoEl); const text = decodeFrame(imageData); if (text) { scanning = false; handleSuccess(text); stopCamera(activeStream); return; } lastDecodeTime = timestamp; } requestAnimationFrame(loop); } requestAnimationFrame(loop);

timestamp - lastDecodeTime > 200就是节流:每 200ms 解码一次,对应 5fps 的解码频率。200ms 是我在多数中端 Android 上的折中值。如果页面里只有扫码一个任务,可以缩到 150ms;如果扫码的同时还要上传轨迹、渲染列表,放到 300ms 更安全。scanning = false放在成功回调之前,是为了防止用户按住手机时同一个二维码被连续识别多次。真实业务里扫码成功跳转前,最好也加一个 500ms 的结果防抖:第一次弹窗用户没点,第二次又识别到同一个码,容易重复提交。

到这里,最小扫码器已经能用了:打开摄像头、取帧、解码、循环、停止。剩下的大多数问题不是代码逻辑,而是手机摄像头和环境光,这部分的坑我在第 4 章集中写。

3. 用 html5-qrcode 少写一半代码:npm 依赖、扫码区配置与 Vue 生命周期

3.1 html5-qrcode 和 jsQR 的分工:库内部还是用 jsQR 解码

如果你不想自己管理摄像头、canvas、requestAnimationFrame 和节流,html5-qrcode 是更务实的方案。它是一个 npm 包,内置了摄像头选择、扫码框、失败提示、文件识别这些完整逻辑。很多人以为 html5-qrcode 和 jsQR 是并列的两种选择,其实不是:html5-qrcode 的二维码识别核心用的还是 jsQR,它把摄像头权限、视频流、帧提取和解码循环都封装好了。换句话说,先用 jsQR 自己写一遍,是为了理解它的边界;用 html5-qrcode 是为了让代码量和维护成本下来。

安装方式就是常规的 npm 依赖:

npm install html5-qrcode

这个包自带 TypeScript 类型,不需要额外安装 @types 包。引入路径上注意,浏览器环境直接使用Html5Qrcode和Html5QrcodeScanner两个类,避免手滑写成import html5Qrcode from 'html5-qrcode',默认导入不是类。

3.2 最小调用:Html5QrcodeScanner 与 Html5Qrcode 两种模式

这个库提供两套 API,名字容易混:Html5QrcodeScanner自带一套扫码 UI,包括摄像头选择下拉框、扫码区域和“更换摄像头”按钮,适合快速上线;Html5Qrcode是底层类,不带 UI,适合要自己画扫码框、做个性化界面的情况。我大部分业务场景用的是 Scanner,因为扫码页的设计通常由 UI 同学先行,调用代码最少。

import { Html5QrcodeScanner, Html5QrcodeSupportedFormats } from 'html5-qrcode'; const scanner = new Html5QrcodeScanner( 'qr-reader', { fps: 10, qrbox: { width: 250, height: 250 } } ); scanner.render( (decodedText) => { console.log('识别到:', decodedText); scanner.clear(); }, (error) => { // jsQR 每帧解码失败都会走这里,别把错误吐给用户 } );

这里的fps不是摄像头本身帧率,而是 html5-qrcode 每隔多久从视频流里抽一帧去解码,10 对应每 100ms 一帧。qrbox是扫码区域,宽高给的是实际像素,不是比例。render的第一个参数是成功回调,第二个是每帧失败回调。注意:jsQR 识别失败非常频繁,尤其当前几帧画面里没有二维码时,失败回调可能每秒执行几十次,千万别在里面做弹窗、上报或者 toast。

Html5Qrcode的用法更接近自己管理摄像头:

const html5QrCode = new Html5Qrcode('qr-reader'); await html5QrCode.start( { facingMode: 'environment' }, { fps: 10, qrbox: 250 }, (decodedText) => console.log(decodedText) ); // 页面离开时一定要 stop,否则摄像头不会关闭 await html5QrCode.stop();

start的第一参数可以传摄像头 id 字符串,也可以传{ facingMode: 'environment' }这种约束。它返回 Promise,摄像头权限被拒时会 reject,记得用 try/catch 包住,不然页面上会留下一个未处理的 Promise 错误。

3.3 扫码框、二维码格式白名单、每秒帧数的参数设置

scanner 的配置项比想象中多,但真正值得调的也就几个。formatsToSupport用来限定只识别二维码,避免相机被一维码带偏;rememberLastUsedCamera可以让用户第二次进入时继续用上次选的摄像头;showTorchButtonIfSupported是手电筒开关,适合夜间扫屏幕码的场景。

const config = { fps: 10, qrbox: { width: 240, height: 240 }, aspectRatio: 1.0, formatsToSupport: [Html5QrcodeSupportedFormats.QR_CODE], rememberLastUsedCamera: true, showTorchButtonIfSupported: true };

几个常见参数的作用整理如下:

参数常见值说明
fps10每秒解码帧数,不是摄像头帧率
qrbox{ width: 240, height: 240 }扫码识别区域,像素单位
aspectRatio1.0视频画面宽高比,避免二维码变形
formatsToSupport[QR_CODE]只识别二维码,不去追一维码
rememberLastUsedCameratrue下次打开记住上次摄像头
showTorchButtonIfSupportedtrue手电筒开关,仅支持的设备出现

aspectRatio我建议设成 1.0 或者和扫码框接近的比例,这样视频画面不会因为拉伸导致二维码变形。qrbox是识别区域的像素尺寸,不是整个视频的尺寸;它越小,解码时越省 CPU,但也要求用户把二维码放到框内。实际扫码场景里,用户往往习惯离远一点扫,框太小时识别失败率会上升,我的经验是宽度至少占屏幕宽度的 70%。

3.4 在 Vue 组件里封装:start/stop 与 beforeDestroy 的生命周期管理

html5-qrcode 在 Vue 项目里最常见的坑是重复初始化。Scanner 一旦渲染到某个 DOM 节点,就不可再对同一个节点调用第二次 render,否则会报错。原因在于路由切换时组件销毁,但摄像头和扫描循环没跟着停。解决方法是把 scanner 实例存在组件作用域里,在路由组件卸载时主动 clear 或 stop。

<script setup> import { onMounted, onBeforeUnmount } from 'vue'; import { Html5QrcodeScanner } from 'html5-qrcode'; let scanner = null; onMounted(() => { scanner = new Html5QrcodeScanner('qr-reader', { fps: 10, qrbox: { width: 250, height: 250 } }); scanner.render(onScanSuccess, onScanFailure); }); onBeforeUnmount(() => { if (scanner) { scanner.clear(); scanner = null; } }); function onScanSuccess(text) { // 跳转前避免连续回调多次 scanner.clear(); console.log(text); } function onScanFailure() {} </script>

clear()和stop()有一点区别:Scanner 实例用clear(),Html5Qrcode 实例用stop()。如果用的是 Vue 2 的 Options API,就把清理逻辑放到beforeDestroy里;Vue 3 用onBeforeUnmount或onUnmounted。还有一个容易漏的:scanner.clear()之后,同一组件如果再次进入 mounted,需要重新 new Scanner,不能复用旧实例。

4. h5 扫码翻车避坑指南:权限、对焦、双镜头与误码率排查

4.1 权限被拒或 getUserMedia 不存在:先检查是不是 HTTPS 和 iframe 权限

现象:navigator.mediaDevices是 undefined,或者getUserMedia一直返回NotAllowedError;还有一种是在微信里点扫码,摄像头权限弹窗一闪而过,页面拿到 no permission。

原因:浏览器摄像头 API 只在安全上下文里开放,http 页面访问非 localhost 域名时直接禁用;iframe 嵌套时还需要在 iframe 标签上声明 allow 属性;部分 Android WebView 如果不实现onPermissionRequest,H5 拿不到授权。

解决:线上页面强制 HTTPS,调试用 localhost;如果业务要嵌到别的平台,让客户端开发在 WebView 配置里允许摄像头权限;iframe 场景在标签上加allow="camera"。我一般会在 getUserMedia 之前先做环境判断:

if (!navigator.mediaDevices?.getUserMedia) { showError('当前浏览器不支持摄像头,请使用 HTTPS 或最新版浏览器'); }

不要等到catch里才提示,undefined 的情况根本没有 Promise 可 reject。

4.2 二维码怼在镜头前也扫不出来:光线、反光和对焦是三大玄学

现象:摄像头能看到画面,二维码也清晰,但 jsQR 持续返回 null;尤其是拿手机去扫另一个手机屏幕,屏幕上出现彩色条纹,然后识别失败。

原因:手机屏幕刷新和摄像头传感器之间存在频闪;环境光过强或过暗都会让二维码二值化失败;摄像头对焦没有锁定,二维码边缘发虚。

解决:把扫码框放在屏幕中央,提示用户距离控制在 10-30cm;夜间开启手电筒或者引导用户调节环境亮度;如果场景是扫屏幕码,可以在页面上增加一个“切换摄像头”或“打开手电筒”按钮。html5-qrcode 的showTorchButtonIfSupported只对支持的设备有效,不要指望所有 Android 都有。自己写 jsQR 循环时,如果连续识别失败超过几秒,就在 UI 上给“光线不足或距离太近”的提示,不要盲目调大 fps,帧率再高也解决不了对焦问题。

4.3 iOS 和安卓 WebView 表现不一致:有些设备 getUserMedia 一直 pending

现象:同一个 H5 页面,Android Chrome 扫码正常,iOS Safari 也正常,但嵌入 App 的 WKWebView 后摄像头打开了,画面却卡住,或者一直黑屏;部分 iOS 上点击开始扫码后,页面直接全屏播放视频。

原因:iOS WebView 对媒体播放有额外要求:页面里的 video 要加playsinline属性,不然调用play()时会进入全屏播放;WKWebView 还需要在 App 的 Info.plist 里声明NSCameraUsageDescription。另外,getUserMedia在 WebView 里如果不在用户手势触发下调用,授权回调可能一直不回来。

解决:给 video 元素加playsinline和muted;确认 App 的 WKWebView 配置允许媒体权限;扫码入口按钮的点击事件里再调用getUserMedia,不要在页面加载时就自动打开摄像头。如果 App 是别人家做的,H5 无法控制客户端,那就退而求其次,在页面上加一个“调起系统扫码”的降级方案,走微信 JSSDK 或原生 bridge。

4.4 误码率高、识别到错误内容:inversionAttempts、灰度、扫码区域调优

现象:识别结果偶发不对,明明扫的是 A 二维码,返回的却是上一帧 B 二维码的内容;或者同一个二维码,时灵时不灵。

原因:jsQR 每帧都从当前画面解一次码,用户移动手机时,画面里同时出现两个二维码,jsQR 不保证返回哪个;如果解码循环没有加冷却,识别成功后没有立刻停,下一帧可能又触发一次回调。另外,inversionAttempts开成attemptBoth后,反色尝试遇到低对比度画面时,可能把噪声误识别成码。

解决:识别成功回调里先置scanning = false,再处理业务;给结果加一个最短响应间隔,比如 500ms 内的重复结果直接忽略;如果业务允许,只识别框内区域,减小背景干扰。用 jsQR 时可以在 drawImage 时只截取扫码框范围的图像,而不是全画面,这样误码率会明显下降。还有一种做法是把inversionAttempts设成dontInvert,前提是你确定二维码一定是白底黑码,比如自己生成的码,识别速度会快不少。

5. 把扫码页装进微信/企业微信/App 内嵌 H5:容器适配与结果处理

5.1 微信/企业微信内打开扫码页:JSSDK 的扫一扫和 WebRTC 如何选

如果页面只在微信内使用,还有一个更省事的方案:微信 JSSDK 的wx.scanQRCode,它直接调起微信原生扫码,识别速度和成功率都比网页摄像头好。但它的问题是只能在微信内置浏览器里用,而且 JSSDK 签名配置本身也是一道门槛。企业微信客服场景里打开的 H5,签名逻辑和企业微信的agentConfig绑定,不像普通公众号那样只配一个wx.config就完事,很多团队在这里卡住。

如果不想绑死微信,或者产品要求同一个 H5 在微信、普通浏览器、App 内都能访问,那还是用 html5-qrcode 的方案。我的选择依据是:目标用户全部在微信内,用 JSSDK;存在混合分发,用 html5-qrcode 加降级。判断当前是不是微信环境,常规做法是看navigator.userAgent里有没有MicroMessenger,但企业微信的 UA 里同时有wxwork和MicroMessenger,要分开处理。

5.2 App 内嵌 WebView 的摄像头授权与 WebRTC 白名单

很多 App 内嵌 H5 后,摄像头打开失败,第一反应是 H5 代码有问题,其实问题经常在客户端 WebView 配置。iOS 的 WKWebView 必须先在 Info.plist 里加NSCameraUsageDescription,否则 getUserMedia 直接拒绝;Android WebView 需要在 WebChromeClient 里重写onPermissionRequest,并且 grant 之后还要允许页面继续使用摄像头。单纯让 H5 上 HTTPS 是不够的。

如果你控制不了客户端,至少要在 H5 端把错误类型区分开:NotAllowedError是权限问题,NotFoundError是设备没有摄像头,NotReadableError是摄像头被其他程序占用或权限冲突。弹给用户的话术完全不同。“摄像头正在被占用”在 Android 上很常见,比如用户刚用微信扫过码,再切到 App 里扫,摄像头没释放干净,就会出现这个错。

5.3 uniapp 封装 H5 指向多个域名时的摄像头权限坑

有同学问过“uniapp 封装 h5 如何指向 2 个域名”,这个场景和扫码结合时有一个隐蔽问题:浏览器把摄像头授权存在“源”上。在 A 域名授权后,跳到 B 域名,B 源没有授权记录,又要重新弹一次权限框;如果 B 域名是 http,那直接在源层面就没有摄像头能力。所以 uniapp 的 H5 如果分发给多个域名,一定要保证所有入口都是 HTTPS,并且主动告诉用户第一次进入时需要重新授权。

另一个坑是 uniapp 的chooseImage和真正的摄像头权限不是一回事。很多 uniapp 页面用<input type="file" capture>只能拿到相册或拍照结果,这不是持续的视频流扫码。要在 uniapp H5 里做实时扫码,仍然得写自己的navigator.mediaDevices.getUserMedia或引 html5-qrcode,uniapp 组件层帮不上忙,只有 App 端走原生插件才另说。

5.4 扫码结果处理:跳转、上传、图片 blob 与键盘联动

扫码成功后的处理比想象中更影响体验。如果结果是 URL,直接location.href跳转会丢失当前扫码页的历史记录,用户按返回键可能直接退出;我一般用location.replace或在新窗口打开,给用户一个停留在原页面的选择。如果结果是 JSON 字符串,别急着JSON.parse,先try/catch,因为二维码里的 JSON 经常带不可见字符或被二次编码。

有的业务需要把扫码连带的二维码图片保存到服务器,这时把相机当前帧截出来转成 blob 上传是常见做法。h5 里的 blob 文件是能上传的,前提是 canvas 来源没有被污染,getUserMedia 的视频帧不会污染 canvas,可以放心toBlob:

scanCanvas.toBlob(async (blob) => { if (!blob) return; const formData = new FormData(); formData.append('file', blob, 'scan.png'); formData.append('code', decodedText); await fetch('/api/upload', { method: 'POST', body: formData }); }, 'image/png');

扫码完成后如果需要跳到页面上某个输入框并自动弹出键盘,注意 App 内 WebView 的软键盘会挤压视口,扫码页面高度要用position: fixed或动态计算visualViewport,否则键盘弹起来时扫码框被顶出屏幕,用户以为页面崩了。企业微信里做客服聊天场景时,扫码结果通常是发给客服的一条消息,多模态格式要注意图片大小,扫码截图 PNG 可能几百 KB,改成 JPEG 质量 0.8 会明显降低上传耗时。

6. 把 jsQR 用得更顺手:相册识别、连续扫码降功耗和一个小工具验证

6.1 用 jsQR 识别相册里的二维码图片:让扫码页多一个降级入口

实时扫码偶尔会被权限、光线卡住,给页面加一个“从相册选择二维码”的入口很实用。流程是用<input type="file" accept="image/*">拿到图片,读入 Image 对象,绘制到 canvas 后再用同样的 jsQR 调用。

async function decodeFromFile(file) { const url = URL.createObjectURL(file); const img = new Image(); await new Promise((resolve, reject) => { img.onload = resolve; img.onerror = reject; img.src = url; }); const canvas = document.createElement('canvas'); canvas.width = img.naturalWidth; canvas.height = img.naturalHeight; const ctx = canvas.getContext('2d'); ctx.drawImage(img, 0, 0); const frame = ctx.getImageData(0, 0, canvas.width, canvas.height); const result = jsQR(frame.data, frame.width, frame.height); URL.revokeObjectURL(url); return result?.data ?? null; }

注意大图要先压缩,否则一张 4000x3000 的图片,getImageData会拿到 4800 万个像素值,解码可能卡一两秒。可以在drawImage时直接把图片画到一个宽度不超过 1000 的 canvas 上,解码速度能快很多。

6.2 连续扫码降功耗:把解码间隔从 50ms 调到 300ms

实时扫码的功耗主要来自三块:摄像头常开、视频帧持续绘制、jsQR 持续解码。前两块省不掉,第三块是唯一能让手机不烫的调节空间。fps 不是越高越好:10fps 和 20fps 在识别率上的差距,远小于发热降频带来的差距。如果用户在室内稳定环境下扫码,把解码间隔放到 300ms,一秒钟只解 3 次,手机温度会明显降下来,识别率反而稳定。

6.3 用 performance.now 验证解码耗时:别凭感觉调参数

调参不能靠肉眼,我习惯把单帧解码耗时打出来,用数据决定到底该降分辨率还是降 fps。

const start = performance.now(); const code = jsQR(frame.data, frame.width, frame.height, { inversionAttempts: 'dontInvert' }); const spent = performance.now() - start; console.log('jsQR 解码耗时:', spent.toFixed(1), 'ms');

如果spent经常超过 80ms,优先减小 canvas 尺寸,而不是降 fps;如果spent稳定在 30ms 以内,可以尝试把inversionAttempts切回attemptBoth提升反色码识别率。我之前有一次上线后用户反馈扫码页烫手,最后发现是 fps 设了 30,每帧全分辨率解码。改成 10fps 加半分辨率画布之后,识别率反而提升,因为手机不再降频。这个教训让我后来每次调扫码参数,都会先测单帧耗时,而不是凭感觉堆配置。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/26 20:33:17

通达信妙在三红主图指标:底部区域三红共振源码详解

1. 先拆设计&#xff1a;这个“三红”到底在表达什么逻辑做技术分析这么多年&#xff0c;“底部区域怎么找”一直是散户问得最多的问题。趋势没走完就急着抄底&#xff0c;结果买在半山腰&#xff1b;等真正见底了&#xff0c;又因为恐慌不敢上车&#xff0c;最后看着行情拉升干…

作者头像 李华
网站建设 2026/9/26 20:32:53

115网盘一键转存脚本实战:从原理到避坑指南

简介&#xff1a;115一键转存.user.js 是一款面向115网盘用户的效率辅助脚本&#xff0c;基于油猴&#xff08;Tampermonkey&#xff09;等浏览器扩展运行&#xff0c;能够在115网盘页面中直接触发&#xff0c;显著简化批量转存、批量创建共享链接以及获取下载链接等高频操作&a…

作者头像 李华
网站建设 2026/9/26 20:32:43

校园管理系统源码部署与二次开发实战指南

简介&#xff1a;这是一套面向计算机专业本科生的校园管理系统毕业设计/课程设计源码&#xff0c;适用于高校或中小学信息化管理场景&#xff0c;帮助学生快速完成教学实践类项目开发。资源包含219个文件&#xff0c;以43个JSP页面实现前端交互、32个Java类与32个Class文件构成…

作者头像 李华
网站建设 2026/9/26 20:31:23

外星人AWCC彻底卸载重装与启动失败排查指南

1. 外星人智控中心 AWCC 到底是个什么东西 1.1 先搞清楚 AWCC 的定位与核心功能 Alienware Command Center&#xff0c;圈内人一般直接叫 AWCC&#xff0c;是外星人系列笔记本和台式机上的一个系统级控制软件。它不是一个简单的驱动&#xff0c;也不是一个可有可无的装饰性工具…

作者头像 李华
网站建设 2026/9/26 20:29:46

eNSP PRO部署实战:从环境准备到首个拓扑跑通

1. 为什么eNSP PRO值得折腾&#xff1a;从老版eNSP的痛点说起如果你在国内网络工程圈待过几年&#xff0c;大概率绕不开华为eNSP这个模拟器。老版eNSP陪伴了无数人考HCIA、HCIP、HCIE&#xff0c;但它的问题也很明显&#xff1a;只支持Windows、依赖VirtualBox、设备镜像老旧、…

作者头像 李华
网站建设 2026/9/26 20:28:12

专知智库白皮书:用思维操作系统破解信息过载与决策难题

这几年做商业咨询和内部管理&#xff0c;我最大的感受是&#xff1a;大多数人的决策问题&#xff0c;根本不是信息不够&#xff0c;而是处理信息的系统不够。打开手机&#xff0c;行业报告、专家观点、竞品动态、内部数据&#xff0c;全部涌过来&#xff0c;好像什么都看到了&a…

作者头像 李华