import { EventEmitter, ListenerFn } from "eventemitter3"; import { Howl, Howler } from "howler/dist/howler.core.min.js"; import { beepSound } from "./assets/base64assets"; import { userLicenseKey } from "../index"; import { BarcodePickerCameraManager } from "./barcodePickerCameraManager"; import { BarcodePickerGui } from "./barcodePickerGui"; import { BrowserCompatibility } from "./browserCompatibility"; import { BrowserHelper } from "./browserHelper"; import { Camera } from "./camera"; import { CameraManager } from "./cameraManager"; import { CameraSettings } from "./cameraSettings"; import { CustomError } from "./customError"; import { DummyCameraManager } from "./dummyCameraManager"; import { ImageSettings } from "./imageSettings"; import { Parser } from "./parser"; import { Scanner } from "./scanner"; import { ScanResult } from "./scanResult"; import { ScanSettings } from "./scanSettings"; import { SearchArea } from "./searchArea"; import { UnsupportedBrowserError } from "./unsupportedBrowserError"; /** * @hidden */ type EventName = "ready" | "submitFrame" | "processFrame" | "scan" | "scanError"; /** * @hidden */ class BarcodePickerEventEmitter extends EventEmitter {} /** * A barcode picker element used to get and show camera input and perform scanning operations. * * The barcode picker will automatically fit and scale inside the given *originElement*. * * Each barcode picker internally contains a [[Scanner]] object with its own WebWorker thread running a * separate copy of the external Scandit Engine library. To optimize loading times and performance it's * recommended to reuse the same picker and to already create the picker in advance (hidden) and just * display it when needed whenever possible. * * As the loading of the external Scandit Engine library can take some time the picker always starts inactive * (but showing GUI and video) and then activates, if not paused, as soon as the library is ready to scan. * The [[on]] method targeting the [[ready]] event can be used to set up a listener function to be called when the * library is loaded. * * The picker can also operate in "single image mode", letting the user click/tap to take a single image to be scanned * via the camera (mobile/tablet) or a file select dialog (desktop). This is provided automatically as fallback by * default when the OS/browser only supports part of the needed features and cannot provide direct access to the camera * for video streaming and continuous scanning, or can also be forced. This behaviour can be set up on creation. Note * that in this mode some of the functions provided by the picker will have no effect. * * By default an alert is shown if an internal error during scanning is encountered which prevents the scanning * procedure from continuing when running on a local IP address. As this uses the built-in [[scanError]] event * functionality, if unwanted it can be disabled by calling [[removeAllListeners]] on the BarcodePicker * instance (right after creation). * * You are not allowed to hide the Scandit logo present in the corner of the GUI. */ export class BarcodePicker { private readonly cameraManager: CameraManager; private readonly barcodePickerGui: BarcodePickerGui; private readonly eventEmitter: BarcodePickerEventEmitter; private readonly scanner: Scanner; private readonly beepSound: Howl; private readonly vibrateFunction: (pattern: number | number[]) => boolean; private readonly scannerReadyEventListener: () => void; private playSoundOnScan: boolean; private vibrateOnScan: boolean; private scanningPaused: boolean; private fatalError: Error; private latestVideoTimeProcessed: number; private destroyed: boolean; private isReadyToWork: boolean; private cameraAccess: boolean; private targetScanningFPS: number; private averageProcessingTime: number; private constructor( originElement: HTMLElement, { visible, singleImageMode, playSoundOnScan, vibrateOnScan, scanningPaused, guiStyle, videoFit, laserArea, viewfinderArea, scanner, scanSettings, targetScanningFPS, hideLogo }: { visible: boolean; singleImageMode: boolean; playSoundOnScan: boolean; vibrateOnScan: boolean; scanningPaused: boolean; guiStyle: BarcodePicker.GuiStyle; videoFit: BarcodePicker.ObjectFit; laserArea?: SearchArea; viewfinderArea?: SearchArea; scanner?: Scanner; scanSettings: ScanSettings; targetScanningFPS: number; hideLogo: boolean; } ) { this.isReadyToWork = false; this.destroyed = false; this.scanningPaused = scanningPaused; Howler.autoSuspend = false; this.beepSound = new Howl({ src: beepSound }); // istanbul ignore else if (navigator.vibrate != null) { this.vibrateFunction = navigator.vibrate; } else if (navigator.webkitVibrate != null) { this.vibrateFunction = navigator.webkitVibrate; } else if (navigator.mozVibrate != null) { this.vibrateFunction = navigator.mozVibrate; } else if (navigator.msVibrate != null) { this.vibrateFunction = navigator.msVibrate; } this.eventEmitter = new EventEmitter(); this.setPlaySoundOnScanEnabled(playSoundOnScan); this.setVibrateOnScanEnabled(vibrateOnScan); this.setTargetScanningFPS(targetScanningFPS); if (scanner == null) { this.scanner = new Scanner({ scanSettings }); } else { this.scanner = scanner; this.scanner.applyScanSettings(scanSettings); } this.scannerReadyEventListener = this.handleScannerReady.bind(this); this.scanner.on("ready", this.scannerReadyEventListener); this.barcodePickerGui = new BarcodePickerGui({ scanner: this.scanner, originElement, singleImageMode, scanningPaused, visible, guiStyle, videoFit, hideLogo, laserArea, viewfinderArea, cameraUploadCallback: this.processVideoFrame.bind(this, true) }); if (singleImageMode) { this.cameraManager = new DummyCameraManager(); } else { this.cameraManager = new BarcodePickerCameraManager(this.triggerFatalError.bind(this), this.barcodePickerGui); this.scheduleVideoProcessing(); } this.barcodePickerGui.setCameraManager(this.cameraManager); } /** * Create a [[BarcodePicker]] instance, creating the needed HTML in the given origin element. * If the *accessCamera* option is enabled (active by default) and the picker is not in "single image mode", * the available cameras are accessed and camera access permission is requested to the user if needed. * This object expects that at least a camera is available. The active camera is accessed and kept active during the * lifetime of the picker (also when hidden or scanning is paused), and is only released when [[destroy]] is called. * * It is required to having configured the library via [[configure]] before this object can be created. * * The "single image mode" behaviour of the picker can be set up via the * *singleImageMode* option, which accepts a configuration object of the form: * ``` * { * desktop: { * always: false, allowFallback: true * }, * mobile: { * always: false, allowFallback: true * } * } * ``` * * Depending on parameters, device features and user permissions for camera access, any of the following errors * could be the rejected result of the returned promise: * - `LibraryNotConfiguredError` * - `NoOriginElementError` * - `UnsupportedBrowserError` * - `PermissionDeniedError` * - `NotAllowedError` * - `NotFoundError` * - `AbortError` * - `NotReadableError` * - `InternalError` * - `NoCameraAvailableError` * * @param originElement The HTMLElement inside which all the necessary elements for the picker will be added. * @param visible
Default = true
* Whether the picker starts in a visible state. * @param singleImageMode
Default =  * { desktop: { always: false, allowFallback: true }, mobile: { always: false, allowFallback: true } }
* Whether to provide a UI to pick/snap a single image from the camera instead of accessing and using the persistent * video stream from a camera ("force"), or to allow to provide this as a fallback ("allowFallback") in case the * necessary features for direct camera access are not provided by the OS/browser. * @param playSoundOnScan
Default = false
* Whether a sound is played on barcode recognition (iOS requires user input). * @param vibrateOnScan
Default = false
* Whether the device vibrates on barcode recognition (only Chrome & Firefox, requires user input). * @param scanningPaused
Default = false
* Whether the picker starts in a paused scanning state. * @param guiStyle
Default = GuiStyle.LASER
* The GUI style for the picker. * @param videoFit
Default = ObjectFit.CONTAIN
* The fit type for the video element of the picker. * @param laserArea
Default = undefined
* The area of the laser displayed when the GUI style is set to *laser* (the laser will match the width and be * vertically centered), by default the area will match the current [[ScanSettings]]'s *searchArea* option. * @param viewfinderArea
Default = undefined
* The area of the viewfinder displayed when the GUI style is set to *viewfinder*, by default the area will match * the current [[ScanSettings]]'s *searchArea* option. * @param enableCameraSwitcher
Default = true
* Whether to show a GUI button to switch between different cameras (when available). * @param enableTorchToggle
Default = true
* Whether to show a GUI button to toggle device torch on/off (when available, only Chrome). * @param enableTapToFocus
Default = true
* Whether to trigger a manual focus of the camera when clicking/tapping on the video (when available, only Chrome). * @param enablePinchToZoom
Default = true
* Whether to control the zoom of the camera when doing a pinching gesture on the video (when available, only Chrome). * @param accessCamera
Default = true
* Whether to immediately access the camera (and requesting user permissions if needed) on picker creation. * @param camera
Default = undefined
* The camera to be used for video input, if not specified the back or only camera will be used. * @param cameraSettings
Default = undefined
* The camera options used when accessing the camera, by default HD resolution is used. * @param scanner
Default = undefined
* The scanner object responsible for scanning via the external Scandit Engine library * (a new scanner will be created and initialized if not provided). * @param scanSettings
Default = new ScanSettings()
* The configuration object for scanning options to be applied to the scanner (all symbologies disabled by default). * @param targetScanningFPS
Default = 30
* The target frames per second to be processed, the final speed is limited by the camera framerate (usually 30 FPS) * and the frame processing time of the device. By setting this to lower numbers devices can save power by performing * less work during scanning operations, depending on device speed (faster devices can "sleep" for longer periods). * Must be a number bigger than 0. * @returns A promise resolving to the created ready [[BarcodePicker]] object. */ public static create( originElement: HTMLElement, { visible = true, singleImageMode = { desktop: { always: false, allowFallback: true }, mobile: { always: false, allowFallback: true } }, playSoundOnScan = false, vibrateOnScan = false, scanningPaused = false, guiStyle = BarcodePicker.GuiStyle.LASER, videoFit = BarcodePicker.ObjectFit.CONTAIN, laserArea, viewfinderArea, scanner, scanSettings = new ScanSettings(), enableCameraSwitcher = true, enableTorchToggle = true, enableTapToFocus = true, enablePinchToZoom = true, accessCamera = true, camera, cameraSettings, targetScanningFPS = 30 }: { visible?: boolean; singleImageMode?: { desktop: { always: boolean; allowFallback: boolean }; mobile: { always: boolean; allowFallback: boolean }; }; playSoundOnScan?: boolean; vibrateOnScan?: boolean; scanningPaused?: boolean; guiStyle?: BarcodePicker.GuiStyle; videoFit?: BarcodePicker.ObjectFit; laserArea?: SearchArea; viewfinderArea?: SearchArea; scanner?: Scanner; scanSettings?: ScanSettings; enableCameraSwitcher?: boolean; enableTorchToggle?: boolean; enableTapToFocus?: boolean; enablePinchToZoom?: boolean; accessCamera?: boolean; camera?: Camera; cameraSettings?: CameraSettings; targetScanningFPS?: number; } = {} ): Promise { let singleImageModeForced: boolean; let singleImageModeFallbackAllowed: boolean; const deviceType: string | undefined = BrowserHelper.userAgentInfo.getDevice().type; if (deviceType != null && ["mobile", "tablet"].includes(deviceType)) { singleImageModeForced = singleImageMode.mobile.always; singleImageModeFallbackAllowed = singleImageMode.mobile.allowFallback; } else { singleImageModeForced = singleImageMode.desktop.always; singleImageModeFallbackAllowed = singleImageMode.desktop.allowFallback; } const browserCompatibility: BrowserCompatibility = BrowserHelper.checkBrowserCompatibility(); if ( !browserCompatibility.scannerSupport || (!singleImageModeForced && !singleImageModeFallbackAllowed && !browserCompatibility.fullSupport) ) { return Promise.reject(new UnsupportedBrowserError(browserCompatibility)); } if (userLicenseKey == null) { return Promise.reject( new CustomError({ name: "LibraryNotConfiguredError", message: "The library has not correctly been configured yet, please call 'configure' with valid parameters" }) ); } if (!BrowserHelper.isValidHTMLElement(originElement)) { return Promise.reject( new CustomError({ name: "NoOriginElementError", message: "A valid origin HTML element must be given" }) ); } const barcodePicker: BarcodePicker = new BarcodePicker(originElement, { visible, singleImageMode: browserCompatibility.fullSupport ? singleImageModeForced : true, playSoundOnScan, vibrateOnScan, scanningPaused, guiStyle, videoFit, laserArea, viewfinderArea, scanner, scanSettings, targetScanningFPS, // tslint:disable-next-line:use-named-parameter hideLogo: arguments[1] == null ? false : arguments[1].hideLogo === true // Hidden parameter }); barcodePicker.cameraManager.setInteractionOptions( enableCameraSwitcher, enableTorchToggle, enableTapToFocus, enablePinchToZoom ); barcodePicker.cameraManager.setSelectedCamera(camera); barcodePicker.cameraManager.setSelectedCameraSettings(cameraSettings); barcodePicker.cameraAccess = accessCamera; // Show error in alert on ScanError by default when running on local IP address for easier customer debugging barcodePicker.on("scanError", error => { // istanbul ignore if if (["localhost", "127.0.0.1", ""].includes(window.location.hostname)) { alert(error); } }); if (accessCamera) { return barcodePicker.cameraManager.setupCameras().then(() => { return barcodePicker; }); } return Promise.resolve(barcodePicker); } /** * Stop scanning and displaying video output, remove HTML elements added to the page, * destroy the internal [[Scanner]] (by default) and destroy the barcode picker itself; ensuring complete cleanup. * * This method should be called after you don't plan to use the picker anymore, * before the object is automatically cleaned up by JavaScript. * The barcode picker must not be used in any way after this call. * * If the [[Scanner]] is or will be in use for other purposes, the relative option can be passed to prevent * its destruction. * * @param destroyScanner Whether to destroy the internally used [[Scanner]] or not. */ public destroy(destroyScanner: boolean = true): void { this.pauseScanning(true); this.scanner.removeListener("ready", this.scannerReadyEventListener); this.destroyed = true; if (destroyScanner) { this.scanner.destroy(); } this.barcodePickerGui.destroy(); this.eventEmitter.removeAllListeners(); } /** * Apply a new set of scan settings to the internal scanner (replacing old settings). * * @param scanSettings The scan configuration object to be applied to the scanner. * @returns The updated [[BarcodePicker]] object. */ public applyScanSettings(scanSettings: ScanSettings): BarcodePicker { this.scanner.applyScanSettings(scanSettings); return this; } /** * @returns Whether the scanning is currently paused. */ public isScanningPaused(): boolean { return this.scanningPaused; } /** * Pause the recognition of codes in the input image. * * By default video from the camera is still shown, if the *pauseCamera* option is enabled the camera stream * is paused (camera access is fully interrupted) and will be resumed when calling [[resumeScanning]] or * [[accessCamera]], possibly requesting user permissions if needed. * * In "single image mode" the input for submitting a picture is disabled. * * @param pauseCamera Whether to also pause the camera stream. * @returns The updated [[BarcodePicker]] object. */ public pauseScanning(pauseCamera: boolean = false): BarcodePicker { this.scanningPaused = true; if (pauseCamera) { this.cameraManager.stopStream(); } if (this.scanner.isReady()) { this.barcodePickerGui.pauseScanning(); } return this; } /** * Resume the recognition of codes in the input image. * * If the camera stream was stopped when calling [[pauseScanning]], the camera stream is also resumed and * user permissions are requested if needed to resume video input. * * In "single image mode" the input for submitting a picture is enabled. * * @returns The updated [[BarcodePicker]] object. */ public async resumeScanning(): Promise { this.scanningPaused = false; if (this.scanner.isReady()) { this.barcodePickerGui.resumeScanning(); } if (this.cameraManager.activeCamera == null && this.cameraAccess) { await this.cameraManager.setupCameras(); } return this; } /** * @returns The currently active camera. */ public getActiveCamera(): Camera | undefined { return this.cameraManager.activeCamera; } /** * Select a camera to be used for video input, if no camera is passed, the default one is selected. * * If camera access is enabled, the camera is enabled and accessed. * * Depending on device features and user permissions for camera access, any of the following errors * could be the rejected result of the returned promise: * - `PermissionDeniedError` * - `NotAllowedError` * - `NotFoundError` * - `AbortError` * - `NotReadableError` * - `InternalError` * - `NoCameraAvailableError` * * In "single image mode" this method has no effect. * * @param camera The new camera to be used, by default the automatically detected back camera is used. * @param cameraSettings The camera options used when accessing the camera, by default HD resolution is used. * @returns A promise resolving to the updated [[BarcodePicker]] object when the camera is set * (and accessed, if camera access is currently enabled). */ public async setActiveCamera(camera?: Camera, cameraSettings?: CameraSettings): Promise { if (camera == null || !this.cameraAccess) { this.cameraManager.setSelectedCamera(camera); this.cameraManager.setSelectedCameraSettings(cameraSettings); if (this.cameraAccess) { await this.cameraManager.setupCameras(); } } else { await this.cameraManager.initializeCameraWithSettings(camera, cameraSettings); } return this; } /** * Try to apply new settings to the currently used camera for video input, * if no settings are passed the default ones are set. * * If camera access is enabled, the camera is updated and accessed with the new settings. * * Depending on device features and user permissions for camera access, any of the following errors * could be the rejected result of the returned promise: * - `PermissionDeniedError` * - `NotAllowedError` * - `NotFoundError` * - `AbortError` * - `NotReadableError` * - `InternalError` * - `NoCameraAvailableError` * * In "single image mode" this method has no effect. * * @param cameraSettings The new camera options used when accessing the camera, by default HD resolution is used. * @returns A promise resolving to the updated [[BarcodePicker]] object when the camera is updated * (and accessed, if camera access is currently enabled). */ public async applyCameraSettings(cameraSettings?: CameraSettings): Promise { if (!this.cameraAccess) { this.cameraManager.setSelectedCameraSettings(cameraSettings); } else { await this.cameraManager.applyCameraSettings(cameraSettings); } return this; } /** * @returns Whether the picker is in a visible state or not. */ public isVisible(): boolean { return this.barcodePickerGui.isVisible(); } /** * Enable or disable picker visibility. * * Note that this does not affect camera access, frame processing or any other picker logic. * * @param visible Whether the picker is in a visible state or not. * @returns The updated [[BarcodePicker]] object. */ public setVisible(visible: boolean): BarcodePicker { this.barcodePickerGui.setVisible(visible); return this; } /** * @returns Whether the currently selected camera's video is mirrored along the vertical axis. */ public isMirrorImageEnabled(): boolean { return this.barcodePickerGui.isMirrorImageEnabled(); } /** * Enable or disable camera video mirroring along the vertical axis. * By default front cameras are automatically mirrored. * This setting is applied per camera and the method has no effect if no camera is currently selected. * * In "single image mode" this method has no effect. * * @param enabled Whether the camera video is mirrored along the vertical axis. * @returns The updated [[BarcodePicker]] object. */ public setMirrorImageEnabled(enabled: boolean): BarcodePicker { this.barcodePickerGui.setMirrorImageEnabled(enabled, true); return this; } /** * @returns Whether a sound should be played on barcode recognition (iOS requires user input). * Note that the sound is played if there's at least a barcode not rejected via [[ScanResult.rejectCode]]. */ public isPlaySoundOnScanEnabled(): boolean { return this.playSoundOnScan; } /** * Enable or disable playing a sound on barcode recognition (iOS requires user input). * * The sound is played if there's at least a barcode not rejected via [[ScanResult.rejectCode]]. * * @param enabled Whether a sound should be played on barcode recognition. * @returns The updated [[BarcodePicker]] object. */ public setPlaySoundOnScanEnabled(enabled: boolean): BarcodePicker { this.playSoundOnScan = enabled; return this; } /** * @returns Whether the device should vibrate on barcode recognition (only Chrome & Firefox, requires user input). * Note that the vibration is triggered if there's at least a barcode not rejected via [[ScanResult.rejectCode]]. */ public isVibrateOnScanEnabled(): boolean { return this.vibrateOnScan; } /** * Enable or disable vibrating the device on barcode recognition (only Chrome & Firefox, requires user input). * * The vibration is triggered if there's at least a barcode not rejected via [[ScanResult.rejectCode]]. * * @param enabled Whether the device should vibrate on barcode recognition. * @returns The updated [[BarcodePicker]] object. */ public setVibrateOnScanEnabled(enabled: boolean): BarcodePicker { this.vibrateOnScan = enabled; return this; } /** * @returns Whether a GUI button to switch between different cameras is shown (when available). */ public isCameraSwitcherEnabled(): boolean { return this.cameraManager.isCameraSwitcherEnabled(); } /** * Show or hide a GUI button to switch between different cameras (when available). * * In "single image mode" this method has no effect. * * @param enabled Whether to show a GUI button to switch between different cameras. * @returns The updated [[BarcodePicker]] object. */ public setCameraSwitcherEnabled(enabled: boolean): BarcodePicker { this.cameraManager.setCameraSwitcherEnabled(enabled).catch( /* istanbul ignore next */ () => { // Ignored } ); return this; } /** * @returns Whether a GUI button to toggle device torch on/off is shown (when available, only Chrome). */ public isTorchToggleEnabled(): boolean { return this.cameraManager.isTorchToggleEnabled(); } /** * Show or hide a GUI button to toggle device torch on/off (when available, only Chrome). * * In "single image mode" this method has no effect. * * @param enabled Whether to show a GUI button to toggle device torch on/off. * @returns The updated [[BarcodePicker]] object. */ public setTorchToggleEnabled(enabled: boolean): BarcodePicker { this.cameraManager.setTorchToggleEnabled(enabled); return this; } /** * @returns Whether manual camera focus when clicking/tapping on the video is enabled (when available, only Chrome). */ public isTapToFocusEnabled(): boolean { return this.cameraManager.isTapToFocusEnabled(); } /** * Enable or disable manual camera focus when clicking/tapping on the video (when available, only Chrome). * * In "single image mode" this method has no effect. * * @param enabled Whether to enable manual camera focus when clicking/tapping on the video. * @returns The updated [[BarcodePicker]] object. */ public setTapToFocusEnabled(enabled: boolean): BarcodePicker { this.cameraManager.setTapToFocusEnabled(enabled); return this; } /** * @returns Whether camera zoom control via pinching gesture on the video is enabled (when available, only Chrome). */ public isPinchToZoomEnabled(): boolean { return this.cameraManager.isPinchToZoomEnabled(); } /** * Enable or disable camera zoom control via pinching gesture on the video (when available, only Chrome). * * In "single image mode" this method has no effect. * * @param enabled Whether to enable camera zoom control via pinching gesture on the video. * @returns The updated [[BarcodePicker]] object. */ public setPinchToZoomEnabled(enabled: boolean): BarcodePicker { this.cameraManager.setPinchToZoomEnabled(enabled); return this; } /** * Enable or disable the torch/flashlight of the device (when available, only Chrome). * Changing active camera or camera settings will cause the torch to become disabled. * * A button on the [[BarcodePicker]] GUI to let the user toggle this functionality can also be set * on creation via the *enableTorchToggle* option (enabled by default, when available). * * In "single image mode" this method has no effect. * * @param enabled Whether the torch should be enabled or disabled. * @returns The updated [[BarcodePicker]] object. */ public setTorchEnabled(enabled: boolean): BarcodePicker { this.cameraManager.setTorchEnabled(enabled); return this; } /** * Set the zoom level of the device (when available, only Chrome). * Changing active camera or camera settings will cause the zoom to be reset. * * In "single image mode" this method has no effect. * * @param zoomPercentage The percentage of the max zoom (between 0 and 1). * @returns The updated [[BarcodePicker]] object. */ public setZoom(zoomPercentage: number): BarcodePicker { this.cameraManager.setZoom(zoomPercentage); return this; } /** * @returns Whether the barcode picker has loaded the external Scandit Engine library and is ready to scan. */ public isReady(): boolean { return this.isReadyToWork; } /** * Add the listener function to the listeners array for an event. * * No checks are made to see if the listener has already been added. * Multiple calls passing the same listener will result in the listener being added, and called, multiple times. * * @param eventName The name of the event to listen to. * @param listener The listener function. * @param once
Default = false
* Whether the listener should just be triggered only once and then discarded. * @returns The updated [[BarcodePicker]] object. */ // tslint:disable-next-line:bool-param-default public on(eventName: EventName, listener: ListenerFn, once?: boolean): BarcodePicker; /** * Add the listener function to the listeners array for the [[ready]] event, fired when the external * Scandit Engine library has been loaded and the barcode picker can thus start to scan barcodes. * If the library has already been loaded the listener is called immediately. * * No checks are made to see if the listener has already been added. * Multiple calls passing the same listener will result in the listener being added, and called, multiple times. * * @param eventName The name of the event to listen to. * @param listener The listener function. * @returns The updated [[BarcodePicker]] object. */ public on(eventName: "ready", listener: () => void): BarcodePicker; /** * Add the listener function to the listeners array for the [[submitFrame]] event, fired when a new frame is submitted * to the engine to be processed. As the frame is not processed yet, the [[ScanResult.barcodes]] property will * always be empty (no results yet). * * No checks are made to see if the listener has already been added. * Multiple calls passing the same listener will result in the listener being added, and called, multiple times. * * @param eventName The name of the event to listen to. * @param listener The listener function, which will be invoked with a [[ScanResult]] object. * @param once
Default = false
* Whether the listener should just be triggered only once and then discarded. * @returns The updated [[BarcodePicker]] object. */ public on( eventName: "submitFrame", listener: (scanResult: ScanResult) => void, // tslint:disable-next-line:bool-param-default once?: boolean ): BarcodePicker; /** * Add the listener function to the listeners array for the [[processFrame]] event, fired when a new frame is * processed. This event is fired on every frame, independently from the number of recognized barcodes (can be none). * The returned barcodes are affected by [[ScanSettings]]'s *codeDuplicateFilter* option. * * No checks are made to see if the listener has already been added. * Multiple calls passing the same listener will result in the listener being added, and called, multiple times. * * @param eventName The name of the event to listen to. * @param listener The listener function, which will be invoked with a [[ScanResult]] object. * @param once
Default = false
* Whether the listener should just be triggered only once and then discarded. * @returns The updated [[BarcodePicker]] object. */ public on( // tslint:disable-next-line:unified-signatures eventName: "processFrame", listener: (scanResult: ScanResult) => void, // tslint:disable-next-line:bool-param-default once?: boolean ): BarcodePicker; /** * Add the listener function to the listeners array for the [[scan]] event, fired when new barcodes * are recognized in the image frame. The returned barcodes are affected by [[ScanSettings]]'s *codeDuplicateFilter* * option. * * No checks are made to see if the listener has already been added. * Multiple calls passing the same listener will result in the listener being added, and called, multiple times. * * @param eventName The name of the event to listen to. * @param listener The listener function, which will be invoked with a [[ScanResult]] object. * @param once
Default = false
* Whether the listener should just be triggered only once and then discarded. * @returns The updated [[BarcodePicker]] object. */ public on( // tslint:disable-next-line:unified-signatures eventName: "scan", listener: (scanResult: ScanResult) => void, // tslint:disable-next-line:bool-param-default once?: boolean ): BarcodePicker; /** * Add the listener function to the listeners array for the [[scanError]] event, fired when an error occurs * during scanning initialization and execution. The barcode picker will be automatically paused when this happens. * * No checks are made to see if the listener has already been added. * Multiple calls passing the same listener will result in the listener being added, and called, multiple times. * * @param eventName The name of the event to listen to. * @param listener The listener function, which will be invoked with an `ScanditEngineError` object. * @param once
Default = false
* Whether the listener should just be triggered only once and then discarded. * @returns The updated [[BarcodePicker]] object. */ // tslint:disable-next-line:bool-param-default public on(eventName: "scanError", listener: (error: Error) => void, once?: boolean): BarcodePicker; public on(eventName: EventName, listener: ListenerFn, once: boolean = false): BarcodePicker { if (eventName === "ready") { if (this.isReadyToWork) { listener(); } else { this.eventEmitter.once(eventName, listener, this); } } else { if (once === true) { this.eventEmitter.once(eventName, listener, this); } else { this.eventEmitter.on(eventName, listener, this); } } return this; } /** * Remove the specified listener from the given event's listener array. * * @param eventName The name of the event from which to remove the listener. * @param listener The listener function to be removed. * @returns The updated [[BarcodePicker]] object. */ public removeListener(eventName: EventName, listener: ListenerFn): BarcodePicker { this.eventEmitter.removeListener(eventName, listener); return this; } /** * Remove all listeners from the given event's listener array. * * @param eventName The name of the event from which to remove all listeners. * @returns The updated [[BarcodePicker]] object. */ public removeAllListeners(eventName: EventName): BarcodePicker { this.eventEmitter.removeAllListeners(eventName); return this; } /** * *See the [[on]] method.* * * @param eventName The name of the event to listen to. * @param listener The listener function. * @param once
Default = false
* Whether the listener should just be triggered only once and then discarded. * @returns The updated [[BarcodePicker]] object. */ // tslint:disable-next-line:bool-param-default public addListener(eventName: EventName, listener: ListenerFn, once?: boolean): BarcodePicker { return this.on(eventName, listener, once); } /** * Add the listener function to the listeners array for the [[ready]] event, fired when the external * Scandit Engine library has been loaded and the barcode picker can thus start to scan barcodes. * If the library has already been loaded the listener is called immediately. * * No checks are made to see if the listener has already been added. * Multiple calls passing the same listener will result in the listener being added, and called, multiple times. * * @deprecated Use the [[on]] method instead. * * @param listener The listener function. * @returns The updated [[BarcodePicker]] object. */ public onReady(listener: () => void): BarcodePicker { console.warn( "The onReady() method is deprecated and will be removed in the next" + ' major library version. Please use on("ready", ) instead.' ); return this.on("ready", listener); } /** * Add the listener function to the listeners array for the [[scan]] event, fired when new barcodes * are recognized in the image frame. The returned barcodes are affected * by the [[ScanSettings.setCodeDuplicateFilter]] option. * * No checks are made to see if the listener has already been added. * Multiple calls passing the same listener will result in the listener being added, and called, multiple times. * * @deprecated Use the [[on]] method instead. * * @param listener The listener function, which will be invoked with a [[ScanResult]] object. * @param once Whether the listener should just be triggered only once and then discarded. * @returns The updated [[BarcodePicker]] object. */ public onScan(listener: (scanResult: ScanResult) => void, once: boolean = false): BarcodePicker { console.warn( "The onScan() method is deprecated and will be removed in the next" + ' major library version. Please use on("scan", ) instead.' ); return this.on("scan", listener, once); } /** * Remove the specified listener from the [[scan]] event's listener array. * * @deprecated Use the [[removeListener]] method instead. * * @param listener The listener function to be removed. * @returns The updated [[BarcodePicker]] object. */ public removeScanListener(listener: (scanResult: ScanResult) => void): BarcodePicker { console.warn( "The removeScanListener() method is deprecated and will be removed in the next" + ' major library version. Please use removeListener("scan", ) instead.' ); return this.removeListener("scan", listener); } /** * Remove all listeners from the [[scan]] event's listener array. * * @deprecated Use the [[removeAllListeners]] method instead. * * @returns The updated [[BarcodePicker]] object. */ public removeScanListeners(): BarcodePicker { console.warn( "The removeScanListeners() method is deprecated and will be removed in the next" + ' major library version. Please use removeAllListeners("scan") instead.' ); return this.removeAllListeners("scan"); } /** * Add the listener function to the listeners array for the [[scanError]] event, fired when an error occurs * during scanning initialization and execution. The barcode picker will be automatically paused when this happens. * * No checks are made to see if the listener has already been added. * Multiple calls passing the same listener will result in the listener being added, and called, multiple times. * * @deprecated Use the [[on]] method instead. * * @param listener The listener function, which will be invoked with an `ScanditEngineError` object. * @param once Whether the listener should just be triggered only once and then discarded. * @returns The updated [[BarcodePicker]] object. */ // tslint:disable-next-line:no-identical-functions public onScanError(listener: (error: Error) => void, once: boolean = false): BarcodePicker { console.warn( "The onScanError() method is deprecated and will be removed in the next" + ' major library version. Please use on("scanError", ) instead.' ); return this.on("scanError", listener, once); } /** * Remove the specified listener from the [[scanError]] event's listener array. * * @deprecated Use the [[removeListener]] method instead. * * @param listener The listener function to be removed. * @returns The updated [[BarcodePicker]] object. */ // tslint:disable-next-line:no-identical-functions public removeScanErrorListener(listener: (error: Error) => void): BarcodePicker { console.warn( "The removeScanErrorListener() method is deprecated and will be removed in the next" + ' major library version. Please use removeListener("scanError", ) instead.' ); return this.removeListener("scanError", listener); } /** * Remove all listeners from the [[scanError]] event's listener array. * * @deprecated Use the [[removeAllListeners]] method instead. * * @returns The updated [[BarcodePicker]] object. */ // tslint:disable-next-line:no-identical-functions public removeScanErrorListeners(): BarcodePicker { console.warn( "The removeScanErrorListeners() method is deprecated and will be removed in the next" + ' major library version. Please use removeAllListeners("scanError") instead.' ); return this.removeAllListeners("scanError"); } /** * Add the listener function to the listeners array for the [[submitFrame]] event, fired when a new frame is submitted * to the engine to be processed. As the frame is not processed yet, the [[ScanResult.barcodes]] property will * always be empty (no results yet). * * No checks are made to see if the listener has already been added. * Multiple calls passing the same listener will result in the listener being added, and called, multiple times. * * @deprecated Use the [[on]] method instead. * * @param listener The listener function, which will be invoked with a [[ScanResult]] object. * @param once Whether the listener should just be triggered only once and then discarded. * @returns The updated [[BarcodePicker]] object. */ // tslint:disable-next-line:no-identical-functions public onSubmitFrame(listener: (scanResult: ScanResult) => void, once: boolean = false): BarcodePicker { console.warn( "The onSubmitFrame() method is deprecated and will be removed in the next" + ' major library version. Please use on("submitFrame", ) instead.' ); return this.on("submitFrame", listener, once); } /** * Remove the specified listener from the [[submitFrame]] event's listener array. * * @deprecated Use the [[removeListener]] method instead. * * @param listener The listener function to be removed. * @returns The updated [[BarcodePicker]] object. */ // tslint:disable-next-line:no-identical-functions public removeSubmitFrameListener(listener: (scanResult: ScanResult) => void): BarcodePicker { console.warn( "The removeSubmitFrameListener() method is deprecated and will be removed in the next" + ' major library version. Please use removeListener("submitFrame", ) instead.' ); return this.removeListener("submitFrame", listener); } /** * Remove all listeners from the [[submitFrame]] event's listener array. * * @deprecated Use the [[removeAllListeners]] method instead. * * @returns The updated [[BarcodePicker]] object. */ // tslint:disable-next-line:no-identical-functions public removeSubmitFrameListeners(): BarcodePicker { console.warn( "The removeSubmitFrameListeners() method is deprecated and will be removed in the next" + ' major library version. Please use removeAllListeners("submitFrame") instead.' ); return this.removeAllListeners("submitFrame"); } /** * Add the listener function to the listeners array for the [[processFrame]] event, fired when a new frame is * processed. This event is fired on every frame, independently from the number of recognized barcodes (can be none). * The returned barcodes are affected by the [[ScanSettings.setCodeDuplicateFilter]] option. * * No checks are made to see if the listener has already been added. * Multiple calls passing the same listener will result in the listener being added, and called, multiple times. * * @deprecated Use the [[on]] method instead. * * @param listener The listener function, which will be invoked with a [[ScanResult]] object. * @param once Whether the listener should just be triggered only once and then discarded. * @returns The updated [[BarcodePicker]] object. */ // tslint:disable-next-line:no-identical-functions public onProcessFrame(listener: (scanResult: ScanResult) => void, once: boolean = false): BarcodePicker { console.warn( "The onProcessFrame() method is deprecated and will be removed in the next" + ' major library version. Please use on("processFrame", ) instead.' ); return this.on("processFrame", listener, once); } /** * Remove the specified listener from the [[processFrame]] event's listener array. * * @deprecated Use the [[removeListener]] method instead. * * @param listener The listener function to be removed. * @returns The updated [[BarcodePicker]] object. */ // tslint:disable-next-line:no-identical-functions public removeProcessFrameListener(listener: (scanResult: ScanResult) => void): BarcodePicker { console.warn( "The removeProcessFrameListener() method is deprecated and will be removed in the next" + ' major library version. Please use removeListener("processFrame", ) instead.' ); return this.removeListener("processFrame", listener); } /** * Remove all listeners from the [[processFrame]] event's listener array. * * @deprecated Use the [[removeAllListeners]] method instead. * * @returns The updated [[BarcodePicker]] object. */ // tslint:disable-next-line:no-identical-functions public removeProcessFrameListeners(): BarcodePicker { console.warn( "The removeProcessFrameListeners() method is deprecated and will be removed in the next" + ' major library version. Please use removeAllListeners("processFrame") instead.' ); return this.removeAllListeners("processFrame"); } /** * Set the GUI style for the picker. * * In "single image mode" this method has no effect. * * When the GUI style is set to *laser* or *viewfinder*, the GUI will flash on barcode recognition. * Note that the GUI will flash if there's at least a barcode not rejected via [[ScanResult.rejectCode]]. * * @param guiStyle The new GUI style to be applied. * @returns The updated [[BarcodePicker]] object. */ public setGuiStyle(guiStyle: BarcodePicker.GuiStyle): BarcodePicker { this.barcodePickerGui.setGuiStyle(guiStyle); return this; } /** * Set the fit type for the video element of the picker. * * If the "cover" type is selected the maximum available search area for barcode detection is (continuously) adjusted * automatically according to the visible area of the picker. * * In "single image mode" this method has no effect. * * @param objectFit The new fit type to be applied. * @returns The updated [[BarcodePicker]] object. */ public setVideoFit(objectFit: BarcodePicker.ObjectFit): BarcodePicker { this.barcodePickerGui.setVideoFit(objectFit); return this; } /** * Access the currently set or default camera, requesting user permissions if needed. * This method is meant to be used after the picker has been initialized with disabled camera access * (*accessCamera*=false) or after [[pauseScanning]] has been called with the pause camera stream option. * Calling this doesn't do anything if the camera is already being accessed. * * Depending on device features and user permissions for camera access, any of the following errors * could be the rejected result of the returned promise: * - `PermissionDeniedError` * - `NotAllowedError` * - `NotFoundError` * - `AbortError` * - `NotReadableError` * - `InternalError` * - `NoCameraAvailableError` * * In "single image mode" this method has no effect. * * @returns A promise resolving to the updated [[BarcodePicker]] object when the camera is accessed. */ public async accessCamera(): Promise { if (!this.cameraAccess || this.cameraManager.activeCamera == null) { await this.cameraManager.setupCameras(); this.cameraAccess = true; } return this; } /** * Create a new parser object. * * @param dataFormat The format of the input data for the parser. * @returns The newly created parser. */ public createParserForFormat(dataFormat: Parser.DataFormat): Parser { return this.scanner.createParserForFormat(dataFormat); } /** * Reassign the barcode picker to a different HTML element. * * All the barcode picker elements inside the current origin element will be moved to the new given one. * * If an invalid element is given, a `NoOriginElementError` error is thrown. * * @param originElement The HTMLElement into which all the necessary elements for the picker will be moved. * @returns The updated [[BarcodePicker]] object. */ public reassignOriginElement(originElement: HTMLElement): BarcodePicker { if (!BrowserHelper.isValidHTMLElement(originElement)) { throw new CustomError({ name: "NoOriginElementError", message: "A valid origin HTML element must be given" }); } this.barcodePickerGui.reassignOriginElement(originElement); return this; } /** * Set the target frames per second to be processed by the scanning engine. * * The final speed is limited by the camera framerate (usually 30 FPS) and the frame processing time of the device. * By setting this to lower numbers devices can save power by performing less work during scanning operations, * depending on device speed (faster devices can "sleep" for longer periods). * * In "single image mode" this method has no effect. * * @param targetScanningFPS The target frames per second to be processed. * Must be a number bigger than 0, by default set to 30. * @returns The updated [[BarcodePicker]] object. */ public setTargetScanningFPS(targetScanningFPS: number): BarcodePicker { if (targetScanningFPS <= 0) { targetScanningFPS = 30; } this.targetScanningFPS = targetScanningFPS; return this; } /** * @returns The internally used initialized (and possibly configured) [[Scanner]] object instance. */ public getScanner(): Scanner { return this.scanner; } /** * Clear the internal scanner session. * * This removes all recognized barcodes from the scanner session and allows them to be scanned again in case a custom * *codeDuplicateFilter* option was set in the [[ScanSettings]]. * * @returns The updated [[BarcodePicker]] object. */ public clearSession(): BarcodePicker { this.scanner.clearSession(); return this; } /** * Set the area of the laser displayed when the GUI style is set to *laser* (the laser will match the width and be * vertically centered). * Note that this functionality affects UI only and doesn't change the actual *searchArea* option set via * [[ScanSettings]]. If no area is passed, the default automatic size behaviour is set, where the laser will match * the current area of the image in which barcodes are searched, controlled via the *searchArea* option in * [[ScanSettings]]. * * @param area The new search area, by default the area will match [[ScanSettings]]'s *searchArea* option. * @returns The updated [[BarcodePicker]] object. */ public setLaserArea(area?: SearchArea): BarcodePicker { this.barcodePickerGui.setLaserArea(area); return this; } /** * Set the area of the viewfinder displayed when the GUI style is set to *viewfinder*. * Note that this functionality affects UI only and doesn't change the actual search area set via [[ScanSettings]]. * If no area is passed, the default automatic size behaviour is set, where the viewfinder will match the current area * of the image in which barcodes are searched, controlled via the *searchArea* option in [[ScanSettings]]. * * @param area The new search area, by default the area will match the [[ScanSettings]]'s *searchArea*. * @returns The updated [[BarcodePicker]] object. */ public setViewfinderArea(area?: SearchArea): BarcodePicker { this.barcodePickerGui.setViewfinderArea(area); return this; } private triggerFatalError(error: Error): void { this.fatalError = error; console.error(error); } private handleScanResult(scanResult: ScanResult): void { this.eventEmitter.emit("processFrame", scanResult); if (scanResult.barcodes.length !== 0) { // This will get executed only after the other existing listeners for "processFrame" and "scan" are executed this.eventEmitter.once("scan", () => { if ( scanResult.barcodes.some(barcode => { return !scanResult.rejectedCodes.has(barcode); }) ) { this.barcodePickerGui.flashGUI(); if (this.playSoundOnScan) { this.beepSound.play(); } if (this.vibrateOnScan && this.vibrateFunction != null) { this.vibrateFunction.call(navigator, 300); } } }); this.eventEmitter.emit("scan", scanResult); } } private scheduleVideoProcessing(timeout: number = 0): void { window.setTimeout(async () => { await this.videoProcessing(); }, timeout); // Leave some breathing room for other operations } private async scheduleNextVideoProcessing(processingStartTime: number): Promise { if (this.targetScanningFPS < 60) { if (this.averageProcessingTime == null) { this.averageProcessingTime = performance.now() - processingStartTime; } else { this.averageProcessingTime = this.averageProcessingTime * 0.9 + (performance.now() - processingStartTime) * 0.1; } const nextProcessingCallDelay: number = Math.max(0, 1000 / this.targetScanningFPS - this.averageProcessingTime); if (Math.round(nextProcessingCallDelay) <= 16) { await this.videoProcessing(); } else { this.scheduleVideoProcessing(nextProcessingCallDelay); } } else { await this.videoProcessing(); } } private async processVideoFrame(highQualitySingleFrameMode: boolean): Promise { const imageData: Uint8ClampedArray | undefined = this.barcodePickerGui.getVideoImageData(); // This could happen in very weird situations and should be temporary // istanbul ignore if if (imageData == null) { return; } if (!this.scanningPaused) { if (this.eventEmitter.listenerCount("submitFrame") > 0) { this.eventEmitter.emit( "submitFrame", new ScanResult([], imageData.slice(), this.scanner.getImageSettings()) ); } try { const scanResult: ScanResult = await this.scanner.processImage(imageData, highQualitySingleFrameMode); // Paused status could have changed in the meantime if (!this.scanningPaused) { this.handleScanResult(scanResult); } } catch (error) { this.pauseScanning(); this.eventEmitter.emit("scanError", error); } } } private async videoProcessing(): Promise { if (this.destroyed) { return; } if ( this.cameraManager.activeCamera == null || this.cameraManager.activeCamera.currentResolution == null || this.fatalError != null || this.scanningPaused || !this.scanner.isReady() || this.scanner.isBusyProcessing() || this.latestVideoTimeProcessed === this.barcodePickerGui.getVideoCurrentTime() ) { this.scheduleVideoProcessing(); return; } if (this.latestVideoTimeProcessed == null) { // Show active GUI if needed, as now it's the moment the scanner is ready and used for the first time await this.resumeScanning(); } const processingStartTime: number = performance.now(); this.latestVideoTimeProcessed = this.barcodePickerGui.getVideoCurrentTime(); await this.processVideoFrame(false); await this.scheduleNextVideoProcessing(processingStartTime); } private handleScannerReady(): void { this.isReadyToWork = true; this.eventEmitter.emit("ready"); } } // istanbul ignore next export namespace BarcodePicker { /** * Fired when the external Scandit Engine library has been loaded and the barcode picker can thus start to scan * barcodes. * * @asMemberOf BarcodePicker * @event */ // @ts-ignore declare function ready(): void; /** * Fired when a new frame is submitted to the engine to be processed. As the frame is not processed yet, the * [[ScanResult.barcodes]] property will always be empty (no results yet). * * @asMemberOf BarcodePicker * @event * @param scanResult The result of the scanning operation on the image. */ // @ts-ignore declare function submitFrame(scanResult: ScanResult): void; /** * Fired when a new frame is processed by the engine. This event is fired on every frame, independently from the * number of recognized barcodes (can be none). The returned barcodes are affected by [[ScanSettings]]'s * *codeDuplicateFilter* option. * * @asMemberOf BarcodePicker * @event * @param scanResult The result of the scanning operation on the image. */ // @ts-ignore declare function processFrame(scanResult: ScanResult): void; /** * Fired when new barcodes are recognized in the image frame. The returned barcodes are affected by [[ScanSettings]]'s * *codeDuplicateFilter* option. * * @asMemberOf BarcodePicker * @event * @param scanResult The result of the scanning operation on the image. */ // @ts-ignore declare function scan(scanResult: ScanResult): void; /** * Fired when an error occurs during scanning initialization and execution. The barcode picker will be automatically * paused when this happens. * * @asMemberOf BarcodePicker * @event * @param error The ScanditEngineError that was triggered. */ // @ts-ignore declare function scanError(error: Error): void; /** * GUI style to be used by a barcode picker, used to hint barcode placement in the frame. */ export enum GuiStyle { /** * No GUI is shown to indicate where the barcode should be placed. * Be aware that the Scandit logo continues to be displayed as showing it is part of the license agreement. */ NONE = "none", /** * A laser line is shown. */ LASER = "laser", /** * A rectangular viewfinder with rounded corners is shown. */ VIEWFINDER = "viewfinder" } /** * Fit type used to control the resizing (scale) of the barcode picker to fit in its container *originElement*. */ export enum ObjectFit { /** * Scale to maintain aspect ratio while fitting within the *originElement*'s content box. * Aspect ratio is preserved, so the barcode picker will be "letterboxed" if its aspect ratio * does not match the aspect ratio of the box. */ CONTAIN = "contain", /** * Scale to maintain aspect ratio while filling the *originElement*'s entire content box. * Aspect ratio is preserved, so the barcode picker will be clipped to fit if its aspect ratio * does not match the aspect ratio of the box. */ COVER = "cover" } }