diff --git a/.changeset/rotten-garlics-run.md b/.changeset/rotten-garlics-run.md new file mode 100644 index 0000000..6805b6d --- /dev/null +++ b/.changeset/rotten-garlics-run.md @@ -0,0 +1,5 @@ +--- +'@capacitor-mlkit/barcode-scanning': minor +--- + +feat: add web support diff --git a/packages/barcode-scanning/src/definitions.ts b/packages/barcode-scanning/src/definitions.ts index fd98804..0187e64 100644 --- a/packages/barcode-scanning/src/definitions.ts +++ b/packages/barcode-scanning/src/definitions.ts @@ -157,6 +157,14 @@ export interface StartScanOptions { * @since 0.0.1 */ lensFacing?: LensFacing; + /** + * The HTML video element to use for the camera preview. + * + * Only available on Web. + * + * @since 5.1.0 + */ + videoElement?: HTMLVideoElement; } /** diff --git a/packages/barcode-scanning/src/web.ts b/packages/barcode-scanning/src/web.ts index eaa84c1..440fb9c 100644 --- a/packages/barcode-scanning/src/web.ts +++ b/packages/barcode-scanning/src/web.ts @@ -1,6 +1,7 @@ import { CapacitorException, ExceptionCode, WebPlugin } from '@capacitor/core'; import type { + BarcodeScannedEvent, BarcodeScannerPlugin, IsSupportedResult, IsTorchAvailableResult, @@ -11,17 +12,64 @@ import type { ScanResult, StartScanOptions, } from './definitions'; +import { BarcodeValueType } from './definitions'; export class BarcodeScannerWeb extends WebPlugin implements BarcodeScannerPlugin { - async startScan(_options?: StartScanOptions): Promise<void> { - throw this.createUnavailableException(); + public static readonly ERROR_VIDEO_ELEMENT_MISSING = + 'videoElement must be provided.'; + private readonly _isSupported = 'BarcodeDetector' in window; + private intervalId: number | undefined; + private stream: MediaStream | undefined; + + async startScan(options?: StartScanOptions): Promise<void> { + if (!this._isSupported) { + this.throwUnsupportedError(); + } + if (!options?.videoElement) { + throw new Error(BarcodeScannerWeb.ERROR_VIDEO_ELEMENT_MISSING); + } + this.stream = await navigator.mediaDevices.getUserMedia({ + video: { + facingMode: { + ideal: 'environment', + }, + }, + audio: false, + }); + options.videoElement.srcObject = this.stream; + await options.videoElement.play(); + const barcodeDetector = new window.BarcodeDetector({ + formats: ['qr_code'], + }); + this.intervalId = window.setInterval(async () => { + const barcodes = await barcodeDetector.detect(options.videoElement); + if (barcodes.length === 0) { + return; + } else { + for (const barcode of barcodes) { + this.handleScannedBarcode(barcode); + } + } + }, 1000); } async stopScan(): Promise<void> { - throw this.createUnavailableException(); + if (!this._isSupported) { + this.throwUnsupportedError(); + } + if (this.intervalId) { + window.clearInterval(this.intervalId); + this.intervalId = undefined; + } + if (this.stream) { + this.stream.getTracks().forEach(track => { + track.stop(); + }); + this.stream = undefined; + } } async readBarcodesFromImage( @@ -35,7 +83,9 @@ export class BarcodeScannerWeb } async isSupported(): Promise<IsSupportedResult> { - throw this.createUnavailableException(); + return { + supported: this._isSupported, + }; } async enableTorch(): Promise<void> { @@ -76,4 +126,28 @@ export class BarcodeScannerWeb ExceptionCode.Unavailable, ); } + + private throwUnsupportedError(): never { + throw this.unavailable( + 'Barcode Detector API not available in this browser.', + ); + } + + private handleScannedBarcode(barcode: any): void { + const result: BarcodeScannedEvent = { + barcode: { + displayValue: barcode.rawValue, + rawValue: barcode.rawValue, + format: barcode.format, + valueType: BarcodeValueType.Unknown, + }, + }; + this.notifyListeners('barcodeScanned', result); + } +} + +declare global { + interface Window { + BarcodeDetector: any; + } }