diff --git a/CHANGELOG.md b/CHANGELOG.md index ff599c6..1ef5f7a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # bedrock-vue-barcode-scanner ChangeLog +## 1.4.0 - 2025-04-xx + +### Changed +- Use Barcode Detection API to detect barcodes. + ## 1.3.1 - 2025-03-11 ### Changed diff --git a/components/BarcodeScanner.vue b/components/BarcodeScanner.vue index 6d2498e..aa96943 100644 --- a/components/BarcodeScanner.vue +++ b/components/BarcodeScanner.vue @@ -20,7 +20,9 @@ import {Html5Qrcode, Html5QrcodeScannerState, Html5QrcodeSupportedFormats} from 'html5-qrcode'; import {inject, onMounted, onUnmounted, reactive, ref} from 'vue'; +import {detectBarcodes} from '../lib/barcodes.js'; import ScannerUI from './ScannerUI.vue'; +import {useQuasar} from 'quasar'; export default { name: 'BarcodeScanner', @@ -46,6 +48,7 @@ export default { // Constants let scanner = null; + const abortController = new AbortController(); // Refs const cameraList = ref([]); @@ -61,6 +64,9 @@ export default { } }); + // use functions + const $q = useQuasar(); + // Inject const {selectedCameraId, updateSelectedCamera} = inject('selectedCameraId'); @@ -69,7 +75,7 @@ export default { // map formats from Web standard to `Html5QrcodeSupportedFormats` scanner = new Html5Qrcode( 'dce-video-container', { - fps: 60, + fps: 30, formatsToSupport: _mapFormats(formatsToSupport), useBarCodeDetectorIfSupported: true, } @@ -99,8 +105,18 @@ export default { await getZoomConstraints(); // Set focus mode scanner.applyVideoConstraints({ - advanced: [{focusMode: 'continuous'}], + advanced: [ + {frameRate: 30}, + {resizeMode: 'none'}, + {focusMode: 'continuous'}, + ], }); + // Start scanner at zoom level 2 for iOS + if($q.platform.is.ios) { + onZoomChange(2); + } + // Use Barcode Detection API + startBarcodeDetection(); loadingCamera.value = false; }); @@ -116,6 +132,7 @@ export default { // Helper functions function handleClose() { emit('close'); + abortController.abort(); } // Toggle camera light on and off @@ -138,6 +155,38 @@ export default { cameraConstraints.zoom.step = step; } + async function emitScanResult({barcodeDetector, video}) { + const {signal} = abortController; + try { + const barcodes = await detectBarcodes({barcodeDetector, video, signal}); + const [result] = barcodes; + emit('result', {type: result.format, text: result.rawValue}); + } catch(e) { + // ignore scan cancellation; log errors and close scanner + if(e.name !== 'AbortError') { + console.error(e); + emit('close'); + } + } + } + + function startBarcodeDetection() { + // get video element + const video = document.querySelector( + '#dce-video-container > video'); + const {BarcodeDetector} = globalThis; + // check if BarcodeDetector is supported + if(!BarcodeDetector) { + alert('Barcode Detector is not supported in this browser.'); + return; + } + const barcodeDetector = new BarcodeDetector({ + formats: formatsToSupport + }); + // emit the first scanned result + emitScanResult({barcodeDetector, video}); + } + // Update camera zoom async function onZoomChange(updatedValue) { scanner.applyVideoConstraints({ @@ -145,21 +194,15 @@ export default { }); } - function onScanSuccess(decodedText, decodedResult) { - const text = decodedText; - const type = decodedResult?.result?.format?.formatName; - console.log( - 'BarcodeScanner detected something:', - decodedText ? decodedText : '' - ); - if(!type || !text) { - return; - } - emit('result', {type, text}); + // use Barcode Detection API instead of html5qrcode's logic + function onScanSuccess() { + return; } function onError(error) { - console.error('BarcodeScanner error:', error); + if(!String(error).startsWith('QR code parse error')) { + console.error('BarcodeScanner error:', error); + } } function getCameraScanConfig() { @@ -171,14 +214,14 @@ export default { } /** - * A function that takes in the width and height of the video stream - * and returns QrDimensions. Viewfinder refers to the video showing - * camera stream. - * - * @param {number} viewfinderWidth - Video screen width. - * @param {number} viewfinderHeight - Video screen height. - * @returns {object} Qrbox width and height. - */ + * A function that takes in the width and height of the video stream + * and returns QrDimensions. Viewfinder refers to the video showing + * camera stream. + * + * @param {number} viewfinderWidth - Video screen width. + * @param {number} viewfinderHeight - Video screen height. + * @returns {object} Qrbox width and height. + */ function qrboxFunction(viewfinderWidth, viewfinderHeight) { const minEdgePercentage = 0.9; // 90% const minEdgeSize = Math.min(viewfinderWidth, viewfinderHeight); @@ -242,16 +285,15 @@ const FORMAT_MAP = new Map([ ['pdf417', Html5QrcodeSupportedFormats.PDF_417], ['qr_code', Html5QrcodeSupportedFormats.QR_CODE], ['upc_a', Html5QrcodeSupportedFormats.UPC_A], - ['upc_e', Html5QrcodeSupportedFormats.UPC_E] + ['upc_e', Html5QrcodeSupportedFormats.UPC_E], ]); // map from Web-native format to `Html5QrcodeSupportedFormats` -function _mapFormats(formats) { +export function _mapFormats(formats) { return formats.map(format => { const result = FORMAT_MAP.get(format); if(result === undefined) { - if(typeof result !== 'string' || - !isNaN(Number.parseInt(result, 10))) { + if(typeof result !== 'string' || !isNaN(Number.parseInt(result, 10))) { throw new TypeError( `Unsupported format "${format}"; ` + 'a string supported by the "BarcodeFormat" enumeration ' + @@ -262,7 +304,6 @@ function _mapFormats(formats) { return result; }); } -