feat: add barcode reader
This commit is contained in:
22
src/app/types/globalThis.d.ts
vendored
Normal file
22
src/app/types/globalThis.d.ts
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
type ImageBitmapSource = HTMLImageElement | SVGImageElement | HTMLVideoElement | HTMLCanvasElement | ImageBitmap | OffscreenCanvas | VideoFrame | Blob | ImageData
|
||||
|
||||
export type BarcodeDetectorOptions = {
|
||||
formats: string[]
|
||||
}
|
||||
|
||||
export type DetectedBarcode = {
|
||||
boundingBox: DOMRectReadOnly;
|
||||
cornerPoints: coords[];
|
||||
format: string[];
|
||||
rawValue: string;
|
||||
}
|
||||
|
||||
declare global {
|
||||
var BarcodeDetector: BarcodeDetector;
|
||||
}
|
||||
|
||||
declare type BarcodeDetector = {
|
||||
new (formats?: {formats: string[]}): BarcodeDetector;
|
||||
static getSupportedFormats(): Promise<string[]>;
|
||||
detect(imageBitmapSource: ImageBitmapSource): Promise<DetectedBarcode[]>;
|
||||
}
|
||||
192
src/app/web-components/barcode-reader.ts
Normal file
192
src/app/web-components/barcode-reader.ts
Normal file
@@ -0,0 +1,192 @@
|
||||
import type { BarcodeDetector, DetectedBarcode } from '../types/globalThis';
|
||||
|
||||
export class BarcodeReader extends HTMLElement {
|
||||
animationFrameId = null;
|
||||
constraints = {
|
||||
audio: false,
|
||||
video: {
|
||||
facingMode: 'environment',
|
||||
},
|
||||
};
|
||||
container = document.createElement('div');
|
||||
video = document.createElement('video');
|
||||
dataCanvas = document.createElement('canvas');
|
||||
dataCtx: CanvasRenderingContext2D | null | undefined;
|
||||
detector: BarcodeDetector | null = null;
|
||||
code: DetectedBarcode | null = null;
|
||||
stream: MediaStream | null = null;
|
||||
scanId = -1;
|
||||
frame = document.createElement('div');
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
const link = document.createElement('link');
|
||||
link.rel = 'stylesheet';
|
||||
link.href = '/barcode-reader/barcode-reader.css';
|
||||
this.container.setAttribute('id', 'container');
|
||||
const videoContainer = document.createElement('div');
|
||||
videoContainer.setAttribute('id', 'video-container');
|
||||
this.container.appendChild(videoContainer);
|
||||
|
||||
const overlay = document.createElement('div');
|
||||
overlay.classList.add('overlay');
|
||||
this.video.setAttribute('autoplay', '');
|
||||
this.video.setAttribute('playsinline', '');
|
||||
this.dataCanvas.setAttribute('id', 'data-canvas');
|
||||
|
||||
this.frame.setAttribute('id', 'frame');
|
||||
this.frame.style.display = 'none';
|
||||
for (let el of ['top-left', 'top-right', 'bottom-right', 'bottom-left']) {
|
||||
const div = document.createElement('div');
|
||||
div.classList.add('corner', el);
|
||||
this.frame.appendChild(div);
|
||||
}
|
||||
videoContainer.appendChild(overlay);
|
||||
videoContainer.appendChild(this.dataCanvas);
|
||||
videoContainer.appendChild(this.video);
|
||||
videoContainer.appendChild(this.frame);
|
||||
|
||||
const shadowRoot = this.attachShadow({ mode: 'open' });
|
||||
shadowRoot.appendChild(link);
|
||||
shadowRoot.appendChild(this.container);
|
||||
this.capturing = false;
|
||||
|
||||
this.dataCtx = this.dataCanvas.getContext('2d', { willReadFrequently: true, alpha: false });
|
||||
}
|
||||
|
||||
get capturing() {
|
||||
return this.hasAttribute('capturing');
|
||||
}
|
||||
|
||||
set capturing(isCapturing) {
|
||||
if (isCapturing) {
|
||||
this.setAttribute('capturing', '');
|
||||
} else {
|
||||
this.removeAttribute('capturing');
|
||||
}
|
||||
}
|
||||
|
||||
async connectedCallback() {
|
||||
this.detector = await this.getDetector();
|
||||
this.video.onplaying = async () => {
|
||||
if (this.dataCanvas) {
|
||||
this.dataCanvas.width = this.dataCanvas.offsetWidth;
|
||||
this.dataCanvas.height = this.dataCanvas.offsetHeight;
|
||||
this.getResults();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
async getDetector(): Promise<BarcodeDetector | null> {
|
||||
if (globalThis.BarcodeDetector) {
|
||||
const formats = await BarcodeDetector.getSupportedFormats();
|
||||
return new BarcodeDetector({ formats });
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
async scan() {
|
||||
this.showContainer(true);
|
||||
|
||||
try {
|
||||
this.capturing = true;
|
||||
this.code = null;
|
||||
|
||||
if (this.stream instanceof MediaStream) {
|
||||
await this.getResults();
|
||||
} else {
|
||||
this.dispatchEvent(new CustomEvent('scan-status', { detail: { state: 'loading' } }));
|
||||
this.stream = await navigator.mediaDevices.getUserMedia(this.constraints);
|
||||
this.startVideo();
|
||||
this.showFrame();
|
||||
this.dispatchEvent(new CustomEvent('scan-status', { detail: { state: 'started' } }));
|
||||
this.dispatchEvent(new CustomEvent('scan-start'));
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
this.showContainer(false);
|
||||
this.dispatchEvent(new CustomEvent('scan-status', { detail: { state: 'stopped' } }));
|
||||
this.dispatchEvent(new CustomEvent('scan-permission-denied'));
|
||||
this.capturing = false;
|
||||
}
|
||||
}
|
||||
|
||||
stopScan() {
|
||||
clearTimeout(this.scanId);
|
||||
if (this.stream) {
|
||||
this.capturing = false;
|
||||
this.stream.getTracks().forEach((track) => track.stop());
|
||||
this.stream = null;
|
||||
this.video.srcObject = null;
|
||||
this.dispatchEvent(new CustomEvent('scan-stop'));
|
||||
this.dispatchEvent(new CustomEvent('scan-status', { detail: { state: 'stopped' } }));
|
||||
this.hideFrame();
|
||||
this.showContainer(false);
|
||||
}
|
||||
}
|
||||
|
||||
startVideo() {
|
||||
if (this.video) this.video.srcObject = this.stream;
|
||||
}
|
||||
|
||||
parseResults(results: DetectedBarcode[]) {
|
||||
const code = results.length > 0 ? results[0] : undefined;
|
||||
if (code && code.rawValue !== '') {
|
||||
clearTimeout(this.scanId);
|
||||
this.code = code;
|
||||
this.capturing = false;
|
||||
|
||||
setTimeout(() => {
|
||||
this.dispatchEvent(
|
||||
new CustomEvent('result', {
|
||||
detail: { code },
|
||||
})
|
||||
);
|
||||
this.stopScan();
|
||||
}, 300);
|
||||
} else {
|
||||
this.scanId = setTimeout(this.getResults.bind(this), 300);
|
||||
}
|
||||
}
|
||||
|
||||
async getResults() {
|
||||
if (this.code) {
|
||||
clearTimeout(this.scanId);
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
if (this.video && this.detector) {
|
||||
let imgData: ImageData | null = this.getImageData(this.video);
|
||||
if (imgData) {
|
||||
const results = await this.detector.detect(imgData);
|
||||
imgData = null;
|
||||
this.parseResults(results);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.log('error', e);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
getImageData(source: HTMLVideoElement) {
|
||||
if (this.dataCtx) {
|
||||
this.dataCtx.drawImage(source, 0, 0, this.dataCanvas.width, this.dataCanvas.height);
|
||||
return this.dataCtx.getImageData(0, 0, this.dataCanvas.width, this.dataCanvas.height);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
showContainer(show: boolean) {
|
||||
this.container.style.display = show ? 'block' : 'none';
|
||||
}
|
||||
|
||||
showFrame() {
|
||||
this.frame.style.display = 'block';
|
||||
}
|
||||
|
||||
hideFrame() {
|
||||
this.frame.style.display = 'none';
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
import { bootstrapApplication } from '@angular/platform-browser';
|
||||
import { appConfig } from './app/app.config';
|
||||
import { App } from './app/app';
|
||||
import { BarcodeReader } from './app/web-components/barcode-reader';
|
||||
|
||||
bootstrapApplication(App, appConfig)
|
||||
.then(() => customElements.define('barcode-reader', BarcodeReader))
|
||||
.catch((err) => console.error(err));
|
||||
|
||||
Reference in New Issue
Block a user