Skip to content

Use Barcode Detection API #9

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
Apr 3, 2025
Merged
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
92 changes: 63 additions & 29 deletions components/BarcodeScanner.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand All @@ -46,6 +48,7 @@ export default {

// Constants
let scanner = null;
const abortController = new AbortController();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that if this component can do more than one scan (before it has to go out of scope or get cleaned up or whatever by Vue), then we'll need a new AbortController for each scan. Otherwise it will always abort after the first abort and never scan again.


// Refs
const cameraList = ref([]);
Expand All @@ -61,6 +64,9 @@ export default {
}
});

// use functions
const $q = useQuasar();

// Inject
const {selectedCameraId, updateSelectedCamera} = inject('selectedCameraId');

Expand All @@ -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,
}
Expand Down Expand Up @@ -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;
});

Expand All @@ -116,6 +132,7 @@ export default {
// Helper functions
function handleClose() {
emit('close');
abortController.abort();
}

// Toggle camera light on and off
Expand All @@ -138,28 +155,46 @@ export default {
cameraConstraints.zoom.step = step;
}

async function emitScanResult({barcodeDetector, video}) {
const {signal} = abortController;
const barcodes = await detectBarcodes({barcodeDetector, video, signal});
const [result] = barcodes;
emit('result', {type: result.format, text: result.rawValue});
}

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({
advanced: [{zoom: updatedValue}],
});
}

function onScanSuccess(decodedText, decodedResult) {
const text = decodedText;
const type = decodedResult?.result?.format?.formatName;
console.log(
'BarcodeScanner detected something:',
decodedText ? decodedText : '<empty string>'
);
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() {
Expand All @@ -171,14 +206,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);
Expand Down Expand Up @@ -242,27 +277,26 @@ 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 ' +
'must be given, e.g., "qr_code", not a number.');
'a string supported by the "BarcodeFormat" enumeration ' +
'must be given, e.g., "qr_code", not a number.'
);
}
throw new TypeError(`Unsupported format "${format}".`);
}
return result;
});
}

</script>

<style>
Expand Down
7 changes: 6 additions & 1 deletion components/ScannerUI.vue
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@
* Copyright (c) 2024-2025 Digital Bazaar, Inc. All rights reserved.
*/
import {ref, watch} from 'vue';
import {useQuasar} from 'quasar';

export default {
name: 'ScannerUI',
Expand Down Expand Up @@ -165,7 +166,11 @@ export default {
},
emits: ['close', 'updateCamera', 'upload', 'toggle-torch', 'zoom-update'],
setup(_, {emit}) {
const zoom = ref(1);
// use functions
const $q = useQuasar();

// Start scanner at zoom level 2 for iOS
const zoom = ref($q.platform.is.ios ? 2 : 1);

watch(zoom, updatedValue => {
emit('zoom-update', updatedValue);
Expand Down
29 changes: 29 additions & 0 deletions lib/barcodes.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/*!
* Copyright (c) 2025 Digital Bazaar, Inc. All rights reserved.
*/
export function detectBarcodes({barcodeDetector, video, signal} = {}) {
return new Promise((resolve, reject) => {
// schedule barcode detection
video.requestVideoFrameCallback(() => _detect({
barcodeDetector, video, signal, resolve, reject
}));
});
}

async function _detect({barcodeDetector, video, signal, resolve, reject}) {
try {
// detect barcodes in the current video frame
const barcodes = await barcodeDetector.detect(video);
if(barcodes.length > 0) {
return resolve(barcodes);
}
// abort as needed
signal?.throwIfAborted();
// no barcodes found, schedule to try again
video.requestVideoFrameCallback(() => _detect({
barcodeDetector, video, signal, resolve, reject
}));
} catch(error) {
reject(error);
}
}