Skip to content

Commit 2fd5066

Browse files
committed
feat(barcode): add logic to use barcodedetector API
1 parent e51dc98 commit 2fd5066

File tree

11 files changed

+397
-99
lines changed

11 files changed

+397
-99
lines changed

packages/pluggableWidgets/barcode-scanner-web/CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66

77
## [Unreleased]
88

9+
### Added
10+
11+
- We added the logic to switch to use native browser BarcodeDetector API if it is available instead of using zxing library.
12+
- We increase ideal image resolution to improve performance on higher end devices.
13+
914
## [2.4.2] - 2024-08-30
1015

1116
### Fixed

packages/pluggableWidgets/barcode-scanner-web/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "@mendix/barcode-scanner-web",
33
"widgetName": "BarcodeScanner",
4-
"version": "2.4.2",
4+
"version": "2.5.0",
55
"description": "Displays a barcode scanner",
66
"copyright": "© Mendix Technology BV 2025. All rights reserved.",
77
"license": "Apache-2.0",

packages/pluggableWidgets/barcode-scanner-web/src/components/BarcodeScanner.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ export function BarcodeScanner({
6969
}: BarcodeScannerProps): ReactElement | null {
7070
const [errorMessage, setError] = useCustomErrorMessage();
7171
const canvasMiddleRef = useRef<HTMLDivElement>(null);
72-
const videoRef = useReader({
72+
const { ref: videoRef, useBrowserAPI } = useReader({
7373
onSuccess: onDetect,
7474
onError: setError,
7575
useCrop: showMask,
@@ -104,7 +104,7 @@ export function BarcodeScanner({
104104

105105
return (
106106
<BarcodeScannerOverlay
107-
class={className}
107+
class={classNames(className, `mx-${useBrowserAPI ? "barcode" : "zxing"}-detector`)}
108108
showMask={showMask}
109109
canvasMiddleMiddleRef={canvasMiddleRef}
110110
{...dimensions}
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
import { BarcodeFormatsType } from "../../typings/BarcodeScannerProps";
2+
import type { BarcodeDetector, BarcodeDetectorOptions, BarcodeFormat, DetectedBarcode } from "./barcode-detector";
3+
4+
// Map Mendix barcode format types to native BarcodeDetector format strings
5+
const mapToNativeFormat = (format: string): string => {
6+
const formatMap: Record<string, string> = {
7+
UPC_A: "upc_a",
8+
UPC_E: "upc_e",
9+
EAN_8: "ean_8",
10+
EAN_13: "ean_13",
11+
CODE_39: "code_39",
12+
CODE_128: "code_128",
13+
ITF: "itf",
14+
QR_CODE: "qr_code",
15+
DATA_MATRIX: "data_matrix",
16+
AZTEC: "aztec",
17+
PDF_417: "pdf417"
18+
};
19+
return formatMap[format] || format.toLowerCase();
20+
};
21+
22+
// Check if BarcodeDetector API is available
23+
export const isBarcodeDetectorSupported = (): boolean => {
24+
return typeof globalThis !== "undefined" && "BarcodeDetector" in globalThis;
25+
};
26+
27+
// Get supported formats for BarcodeDetector
28+
export const getBarcodeDetectorSupportedFormats = async (): Promise<string[]> => {
29+
if (!isBarcodeDetectorSupported()) {
30+
return [];
31+
}
32+
try {
33+
const detector = new window.BarcodeDetector!();
34+
return await detector.getSupportedFormats();
35+
} catch (error) {
36+
console.warn("Failed to get BarcodeDetector supported formats:", error);
37+
return [];
38+
}
39+
};
40+
41+
// Create BarcodeDetector options from widget configuration
42+
export const createBarcodeDetectorOptions = (
43+
useAllFormats: boolean,
44+
barcodeFormats?: BarcodeFormatsType[]
45+
): BarcodeDetectorOptions => {
46+
const options: BarcodeDetectorOptions = {};
47+
48+
if (!useAllFormats && barcodeFormats && barcodeFormats.length > 0) {
49+
options.formats = barcodeFormats.map(format => mapToNativeFormat(format.barcodeFormat)) as Array<
50+
BarcodeFormat["format"]
51+
>;
52+
}
53+
// If useAllFormats is true or no specific formats, don't specify formats to use all supported
54+
55+
return options;
56+
};
57+
58+
// Create BarcodeDetector instance
59+
export const createBarcodeDetector = (options?: BarcodeDetectorOptions): BarcodeDetector | null => {
60+
if (!isBarcodeDetectorSupported()) {
61+
return null;
62+
}
63+
64+
try {
65+
return new window.BarcodeDetector!(options);
66+
} catch (error) {
67+
console.warn("Failed to create BarcodeDetector:", error);
68+
return null;
69+
}
70+
};
71+
72+
// Detect barcodes from video or canvas element using BarcodeDetector API
73+
export const detectBarcodesFromElement = async (
74+
detector: BarcodeDetector,
75+
element: HTMLVideoElement | HTMLCanvasElement
76+
): Promise<DetectedBarcode[]> => {
77+
try {
78+
return await detector.detect(element);
79+
} catch (error) {
80+
console.warn("BarcodeDetector failed to detect:", error);
81+
return [];
82+
}
83+
};
84+
85+
// Convert video frame to canvas for processing
86+
export const captureVideoFrame = (video: HTMLVideoElement, canvas?: HTMLCanvasElement): HTMLCanvasElement => {
87+
if (!canvas) {
88+
canvas = document.createElement("canvas");
89+
}
90+
91+
canvas.width = video.videoWidth;
92+
canvas.height = video.videoHeight;
93+
94+
const ctx = canvas.getContext("2d");
95+
if (ctx) {
96+
ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
97+
}
98+
99+
return canvas;
100+
};
101+
102+
export const setupVideoElement = (video: HTMLVideoElement, stream: MediaStream): void => {
103+
video.autofocus = true;
104+
video.playsInline = true; // Fix error in Safari
105+
video.muted = true;
106+
video.srcObject = stream;
107+
};
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
// TypeScript definitions for the BarcodeDetector API
2+
// Based on https://developer.mozilla.org/en-US/docs/Web/API/BarcodeDetector
3+
4+
// https://developer.mozilla.org/en-US/docs/Web/API/Barcode_Detection_API#supported_barcode_formats
5+
export interface BarcodeFormat {
6+
format:
7+
| "aztec"
8+
| "code_128"
9+
| "code_39"
10+
| "code_93"
11+
| "codabar"
12+
| "data_matrix"
13+
| "ean_13"
14+
| "ean_8"
15+
| "itf"
16+
| "pdf417"
17+
| "qr_code"
18+
| "unknown"
19+
| "upc_a"
20+
| "upc_e";
21+
}
22+
23+
export interface DetectedBarcode {
24+
boundingBox: DOMRectReadOnly;
25+
cornerPoints: ReadonlyArray<{ x: number; y: number }>;
26+
format: BarcodeFormat["format"];
27+
rawValue: string;
28+
}
29+
30+
export interface BarcodeDetectorOptions {
31+
formats?: Array<BarcodeFormat["format"]>;
32+
}
33+
34+
export interface BarcodeDetector {
35+
detect(image: ImageBitmapSource): Promise<DetectedBarcode[]>;
36+
getSupportedFormats(): Promise<Array<BarcodeFormat["format"]>>;
37+
}
38+
39+
export type BarcodeDetectorConstructor = new (options?: BarcodeDetectorOptions) => BarcodeDetector;
40+
41+
// Extend Window interface to include BarcodeDetector
42+
declare global {
43+
interface Window {
44+
BarcodeDetector?: BarcodeDetectorConstructor;
45+
}
46+
}
47+
48+
export interface MxBarcodeReader {
49+
start(onSuccess: (data: string) => void, onError: (e: Error) => void): Promise<void>;
50+
stop(): void;
51+
}

packages/pluggableWidgets/barcode-scanner-web/src/components/utils.tsx renamed to packages/pluggableWidgets/barcode-scanner-web/src/helpers/utils.tsx

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,29 @@ import {
77
HybridBinarizer,
88
Result
99
} from "@zxing/library";
10+
import { RefObject } from "react";
1011
import { BarcodeFormatsType } from "typings/BarcodeScannerProps";
1112

13+
export const mediaStreamConstraints: MediaStreamConstraints = {
14+
audio: false,
15+
video: {
16+
facingMode: "environment",
17+
width: { min: 1280, ideal: 4096, max: 4096 },
18+
height: { min: 720, ideal: 2160, max: 2160 }
19+
}
20+
};
21+
22+
export type ReaderProps = {
23+
onSuccess?: (data: string) => void;
24+
onError?: (e: Error) => void;
25+
useCrop: boolean;
26+
barcodeFormats?: BarcodeFormatsType[];
27+
useAllFormats: boolean;
28+
canvasMiddleRef: RefObject<HTMLDivElement>;
29+
};
30+
31+
export type UseReaderHook = (args: ReaderProps) => RefObject<HTMLVideoElement>;
32+
1233
export const returnVideoWidthHeight = (
1334
curVideoRef: HTMLVideoElement,
1435
canvasMiddle: HTMLDivElement
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import { RefObject } from "react";
2+
import { BarcodeDetector, MxBarcodeReader } from "../helpers/barcode-detector";
3+
import { createBarcodeDetector, createBarcodeDetectorOptions } from "../helpers/barcode-detector-utils";
4+
import { mediaStreamConstraints, ReaderProps } from "../helpers/utils";
5+
6+
export class Reader implements MxBarcodeReader {
7+
private videoRef: RefObject<HTMLVideoElement>;
8+
barcodeDetector: BarcodeDetector | null;
9+
useCrop: boolean;
10+
stopped: boolean = false;
11+
canvasMiddleRef: RefObject<HTMLDivElement>;
12+
stream: MediaStream | null = null;
13+
decodeInterval: NodeJS.Timeout | number | null = null;
14+
15+
constructor(args: ReaderProps, videoRef: RefObject<HTMLVideoElement>) {
16+
this.videoRef = videoRef;
17+
this.useCrop = args.useCrop;
18+
this.canvasMiddleRef = args.canvasMiddleRef;
19+
const options = createBarcodeDetectorOptions(args.useAllFormats, args.barcodeFormats);
20+
this.barcodeDetector = createBarcodeDetector(options);
21+
}
22+
23+
start = async (onSuccess: (data: string) => void, onError: (e: Error) => void): Promise<void> => {
24+
if (this.videoRef.current === null) {
25+
return;
26+
}
27+
28+
if (this.barcodeDetector === null) {
29+
if (onError) {
30+
onError(new Error("Failed to create barcode detector"));
31+
}
32+
33+
return;
34+
}
35+
36+
const stream = await navigator.mediaDevices.getUserMedia(mediaStreamConstraints);
37+
38+
this.videoRef.current.autofocus = true;
39+
this.videoRef.current.playsInline = true; // Fix error in Safari
40+
this.videoRef.current.muted = true;
41+
this.videoRef.current.autoplay = true;
42+
this.videoRef.current.srcObject = stream;
43+
this.decodeInterval = setTimeout(this.decodeStream, 50, onSuccess, onError);
44+
};
45+
46+
stop = (): void => {
47+
if (this.decodeInterval) {
48+
clearTimeout(this.decodeInterval);
49+
}
50+
this.stream?.getVideoTracks().forEach(track => track.stop());
51+
this.barcodeDetector = null;
52+
};
53+
54+
decodeStream = async (
55+
// loop decode canvas till it finds a result
56+
resolve: (value: string) => void,
57+
reject: (reason?: Error) => void
58+
): Promise<void> => {
59+
try {
60+
if (this.videoRef.current === null) {
61+
return;
62+
}
63+
if (this.decodeInterval) {
64+
clearTimeout(this.decodeInterval);
65+
}
66+
const detectionCode = await this.barcodeDetector?.detect(this.videoRef.current);
67+
68+
if (detectionCode && detectionCode.length > 0) {
69+
if (resolve) resolve(detectionCode[0].rawValue);
70+
} else {
71+
this.decodeInterval = setTimeout(this.decodeStream, 50, resolve, reject);
72+
}
73+
} catch (error) {
74+
reject(error);
75+
}
76+
};
77+
}

0 commit comments

Comments
 (0)