scandit-sdk
Version:
Scandit Barcode Scanner SDK for the Web
1,118 lines • 51.2 kB
JavaScript
import { EventEmitter } from "eventemitter3";
import { Howl, Howler } from "howler/dist/howler.core.min.js";
import { beepSound } from "../assets/base64assets";
import { configurePhase } from "../../index";
import { BrowserHelper } from "../browserHelper";
import { Camera } from "../camera";
import { CustomError } from "../customError";
import { Logger } from "../logger";
import { Scanner } from "../scanner";
import { ScanResult } from "../scanResult";
import { ScanSettings } from "../scanSettings";
import { SingleImageModeSettings } from "../singleImageModeSettings";
import { UnsupportedBrowserError } from "../unsupportedBrowserError";
import { CameraManager } from "./cameraManager";
import { DummyCameraManager } from "./dummyCameraManager";
import { GUI } from "./gui";
/**
* @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 Data Capture 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 Data Capture 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 will be ready to start scanning when the library is fully loaded.
*
* By default the external Scandit Data Capture library is preloaded in order to reduce the initialization time as much
* as possible.
*
* 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) 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 on/off. This behaviour can be set up on creation
* via the *singleImageModeSettings* option. 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).
*
* In accordance with our license terms, the Scandit logo displayed in the bottom right corner of the barcode picker
* must be displayed and cannot be hidden by any method. Workarounds are not allowed.
*/
export class BarcodePicker {
cameraManager;
gui;
eventEmitter;
scanner;
beepSound;
vibrateFunction;
scannerReadyEventListener;
playSoundOnScan;
vibrateOnScan;
scanningPaused;
latestVideoTimeProcessed;
destroyed;
isReadyToWork;
cameraAccess;
targetScanningFPS;
averageProcessingTime;
externalImageData;
internalImageData;
constructor(originElement, { visible, singleImageModeEnabled, singleImageModeSettings, playSoundOnScan, vibrateOnScan, scanningPaused, guiStyle, videoFit, cameraRecoveryText, laserArea, viewfinderArea, scanner, scanSettings, cameraType, targetScanningFPS, hideLogo, }) {
this.isReadyToWork = false;
this.destroyed = false;
this.scanningPaused = scanningPaused;
Howler.autoSuspend = false;
this.beepSound = new Howl({
src: beepSound,
});
this.vibrateFunction = navigator.vibrate ?? navigator.webkitVibrate ?? navigator.mozVibrate ?? navigator.msVibrate;
this.eventEmitter = new EventEmitter();
this.setPlaySoundOnScanEnabled(playSoundOnScan);
this.setVibrateOnScanEnabled(vibrateOnScan);
this.setTargetScanningFPS(targetScanningFPS);
this.scanner = scanner?.applyScanSettings(scanSettings) ?? new Scanner({ scanSettings });
this.scannerReadyEventListener = this.handleScannerReady.bind(this);
this.scanner.on("ready", this.scannerReadyEventListener);
this.gui = new GUI({
scanner: this.scanner,
originElement,
singleImageModeEnabled,
singleImageModeSettings,
scanningPaused,
visible,
guiStyle,
videoFit,
hideLogo,
cameraRecoveryText,
laserArea,
viewfinderArea,
cameraUploadCallback: this.processVideoFrame.bind(this, true),
});
if (singleImageModeEnabled) {
this.cameraManager = new DummyCameraManager(this.scanner, this.triggerCameraAccessError.bind(this), this.gui);
this.gui.setCameraType(cameraType);
}
else {
this.cameraManager = new CameraManager(this.scanner, this.triggerCameraAccessError.bind(this), this.gui);
this.scheduleVideoProcessing();
}
this.gui.setCameraManager(this.cameraManager);
}
/**
* Fired when the external Scandit Data Capture library has been loaded and the barcode picker can thus start to scan
* barcodes or texts.
*
* @asMemberOf BarcodePicker
* @event
*/
// istanbul ignore next
static ready() {
// Ignored
}
/**
* Fired when a new frame is submitted to the external Scandit Data Capture library 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
// istanbul ignore next
static submitFrame(scanResult) {
// Ignored
}
/**
* Fired when a new frame is processed by the external Scandit Data Capture library. This event is fired on every
* frame, independently from the number of recognized barcodes or texts (can both be none). The returned barcodes and
* texts are affected by [[ScanSettings]]'s *codeDuplicateFilter* and [[TextRecognitionSettings]]'s
* *textDuplicateFilter* options.
*
* @asMemberOf BarcodePicker
* @event
* @param scanResult The result of the scanning operation on the image.
*/
// @ts-ignore
// istanbul ignore next
static processFrame(scanResult) {
// Ignored
}
/**
* Fired when new barcodes or texts are recognized in the image frame. The returned barcodes and texts are affected by
* [[ScanSettings]]'s *codeDuplicateFilter* and [[TextRecognitionSettings]]'s *textDuplicateFilter* options.
*
* @asMemberOf BarcodePicker
* @event
* @param scanResult The result of the scanning operation on the image.
*/
// @ts-ignore
// istanbul ignore next
static scan(scanResult) {
// Ignored
}
/**
* 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
// istanbul ignore next
static scanError(error) {
// Ignored
}
/**
* Fired when an error occurs during camera access.
*
* @asMemberOf BarcodePicker
* @event
* @param error The Error that was triggered.
*/
// @ts-ignore
// istanbul ignore next
static cameraAccessError(error) {
// Ignored
}
/**
* 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.
*
* Depending on library configuration, parameters, device/browser features and user permissions for camera access, any
* of the following errors could be the rejected result of the returned promise:
* - `AbortError`
* - `LibraryNotConfiguredError`
* - `NoCameraAvailableError`
* - `NoOriginElementError`
* - `NotAllowedError`
* - `NotFoundError`
* - `NotReadableError`
* - `SecurityError`
* - `UnsupportedBrowserError`
*
* @param originElement The HTMLElement inside which all the necessary elements for the picker will be added.
* @param visible <div class="tsd-signature-symbol">Default = true</div>
* Whether the picker starts in a visible state.
* @param singleImageModeSettings <div class="tsd-signature-symbol">Default = </div>
* <pre><code>{
* desktop: {
* usageStrategy: SingleImageModeSettings.UsageStrategy.FALLBACK,
* informationElement: <HTMLElement>,
* buttonElement: <SVGElement>,
* containerStyle: { backgroundColor: "#333333" },
* informationStyle: { color: "#FFFFFF" },
* buttonStyle: { borderColor: "#FFFFFF", color: "#FFFFFF", fill: "#FFFFFF" }
* },
* mobile: {
* usageStrategy: SingleImageModeSettings.UsageStrategy.FALLBACK,
* informationElement: <HTMLElement>,
* buttonElement: <SVGElement>,
* containerStyle: { backgroundColor: "#333333" },
* informationStyle: { color: "#FFFFFF" },
* buttonStyle: { borderColor: "#FFFFFF", color: "#FFFFFF", fill: "#FFFFFF" }
* }
* }</code></pre>
* Settings for Single Image Mode: an alternative/fallback mode for a barcode picker to provide single camera
* pictures to be scanned instead of continuous camera video stream access. In Single Image Mode users click/tap to
* directly take a picture with the camera (mobile) or upload a file (desktop). Its usage depends on the given
* settings and the camera video stream features provided by the OS/browser.
* @param playSoundOnScan <div class="tsd-signature-symbol">Default = false</div>
* Whether a sound is played on barcode/text recognition (iOS requires user input).
* @param vibrateOnScan <div class="tsd-signature-symbol">Default = false</div>
* Whether the device vibrates on barcode/text recognition (only Chrome & Firefox, requires user input).
* @param scanningPaused <div class="tsd-signature-symbol">Default = false</div>
* Whether the picker starts in a paused scanning state.
* @param guiStyle <div class="tsd-signature-symbol">Default = GuiStyle.LASER</div>
* The GUI style for the picker.
* @param videoFit <div class="tsd-signature-symbol">Default = ObjectFit.CONTAIN</div>
* The fit type for the video element of the picker.
* @param cameraRecoveryText <div class="tsd-signature-symbol">Default = "Tap/click to resume scanning"</div>
* The text to display to indicate to the user a necessary tap/click action on the picker to recover camera access.
* @param laserArea <div class="tsd-signature-symbol">Default = undefined</div>
* The area of the laser displayed when the GUI style is set to <em>laser</em> (the laser will match the width and be
* vertically centered), by default the area will match the current [[ScanSettings]]'s <em>searchArea</em> option.
* @param viewfinderArea <div class="tsd-signature-symbol">Default = undefined</div>
* The area of the viewfinder displayed when the GUI style is set to <em>viewfinder</em>, by default the area will
* match the current [[ScanSettings]]'s <em>searchArea</em> option.
* @param enableCameraSwitcher <div class="tsd-signature-symbol">Default = true</div>
* Whether to show a GUI button to switch between different cameras (when available).
* @param enableTorchToggle <div class="tsd-signature-symbol">Default = true</div>
* Whether to show a GUI button to toggle device torch on/off (when available, only Chrome).
* @param enableTapToFocus <div class="tsd-signature-symbol">Default = true</div>
* Whether to trigger a manual focus of the camera when clicking/tapping on the video (when available, only Chrome).
* @param enablePinchToZoom <div class="tsd-signature-symbol">Default = true</div>
* Whether to control the zoom of the camera when doing a pinching gesture on the video (when available, only Chrome).
* @param accessCamera <div class="tsd-signature-symbol">Default = true</div>
* Whether to immediately access the camera (and requesting user permissions if needed) on picker creation.
* @param camera <div class="tsd-signature-symbol">Default = undefined</div>
* The initial camera to be used for video input, if not specified the camera automatically selected depending on
* the <em>cameraType</em> option will be used.
* @param cameraType <div class="tsd-signature-symbol">Default = Camera.Type.BACK</div>
* The preferred initial camera type (facing mode/direction) to be used for video input and Single Image Mode
* (when available), by default the back or only camera will be used. If the <em>camera</em> option is provided then
* <em>cameraType</em> is ignored.
* @param cameraSettings <div class="tsd-signature-symbol">Default = undefined</div>
* The camera options used when accessing the camera, by default <code>hd</code> resolution is used.
* @param scanner <div class="tsd-signature-symbol">Default = undefined</div>
* The scanner object responsible for scanning via the external Scandit Data Capture library
* (a new scanner will be created and initialized if not provided).
* @param scanSettings <div class="tsd-signature-symbol">Default = new ScanSettings()</div>
* The configuration object for scanning options to be applied to the scanner (all symbologies disabled by default).
* @param targetScanningFPS <div class="tsd-signature-symbol">Default = 30</div>
* 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.
*/
static async create(originElement, { visible = true, singleImageModeSettings = {}, playSoundOnScan = false, vibrateOnScan = false, scanningPaused = false, guiStyle = BarcodePicker.GuiStyle.LASER, videoFit = BarcodePicker.ObjectFit.CONTAIN, cameraRecoveryText = "Tap/click to resume scanning", laserArea, viewfinderArea, scanner, scanSettings = new ScanSettings(), enableCameraSwitcher = true, enableTorchToggle = true, enableTapToFocus = true, enablePinchToZoom = true, accessCamera = true, camera, cameraType = Camera.Type.BACK, cameraSettings, targetScanningFPS = 30,
/**
* @hidden
*/
hideLogo = false, // Hidden argument
} = {}) {
const deviceType = BrowserHelper.userAgentInfo.getDevice().type;
const isMobileDevice = deviceType === "mobile" || deviceType === "tablet";
const singleImageModePlatformSettings = (isMobileDevice ? singleImageModeSettings.mobile : singleImageModeSettings.desktop) ?? {};
const singleImageModeDisallowed = singleImageModePlatformSettings.usageStrategy === SingleImageModeSettings.UsageStrategy.NEVER;
const singleImageModeForced = singleImageModePlatformSettings.usageStrategy === SingleImageModeSettings.UsageStrategy.ALWAYS;
const browserCompatibility = BrowserHelper.checkBrowserCompatibility();
if (!browserCompatibility.scannerSupport || (singleImageModeDisallowed && !browserCompatibility.fullSupport)) {
throw new UnsupportedBrowserError(browserCompatibility);
}
if (!browserCompatibility.fullSupport && !singleImageModeForced) {
Logger.log(Logger.Level.INFO, "BarcodePicker's Single Image Mode is being used as fallback as the OS/browser combination doesn't " +
"support camera video stream scanning (https://caniuse.com/#feat=stream). " +
'You can configure this behaviour via the "singleImageModeSettings" option.', browserCompatibility);
}
if (configurePhase !== "done") {
throw new CustomError({
name: "LibraryNotConfiguredError",
message: configurePhase === "started"
? `The library has not completed its configuration yet, please call 'configure' and wait for the returned
promise's resolution`
: `The library was not configured, 'configure' must be called with valid parameters before instantiating
the BarcodePicker`,
});
}
if (!BrowserHelper.isValidHTMLElement(originElement)) {
throw new CustomError({
name: "NoOriginElementError",
message: "A valid origin HTML element must be given",
});
}
const barcodePicker = new BarcodePicker(originElement, {
visible,
singleImageModeEnabled: browserCompatibility.fullSupport ? singleImageModeForced : true,
singleImageModeSettings: singleImageModePlatformSettings,
playSoundOnScan,
vibrateOnScan,
scanningPaused,
guiStyle,
videoFit,
cameraRecoveryText,
laserArea,
viewfinderArea,
scanner,
scanSettings,
cameraType,
targetScanningFPS,
hideLogo,
});
barcodePicker.cameraManager.setInteractionOptions(enableCameraSwitcher, false, enableTorchToggle, enableTapToFocus, enablePinchToZoom);
barcodePicker.cameraManager.setInitialCameraType(cameraType);
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) {
await barcodePicker.cameraManager.setupCameras();
}
return 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.
*/
destroy(destroyScanner = true) {
this.pauseScanning(true);
this.scanner.removeListener("ready", this.scannerReadyEventListener);
this.destroyed = true;
if (destroyScanner) {
this.scanner.destroy();
}
this.gui.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.
*/
applyScanSettings(scanSettings) {
this.scanner.applyScanSettings(scanSettings);
return this;
}
/**
* @returns Whether the scanning is currently paused.
*/
isScanningPaused() {
return this.scanningPaused;
}
/**
* Pause the recognition of codes/texts 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]],
* [[setActiveCamera]], [[setCameraType]] 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.
*/
pauseScanning(pauseCamera = false) {
this.scanningPaused = true;
if (pauseCamera) {
this.cameraManager.stopStream().catch(
/* istanbul ignore next */ () => {
// Ignored
});
}
if (this.scanner.isReady()) {
this.gui.pauseScanning();
}
return this;
}
/**
* Resume the recognition of codes/texts 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.
*/
async resumeScanning() {
this.scanningPaused = false;
if (this.scanner.isReady()) {
this.gui.resumeScanning();
}
if (this.cameraAccess && this.getActiveCamera() == null) {
await this.cameraManager.setupCameras();
}
return this;
}
/**
* @returns The currently active camera.
*/
getActiveCamera() {
return this.cameraManager.activeCamera;
}
/**
* Select a camera to be used for video input, if no camera is passed, the default one (based on *cameraType*) is
* selected.
*
* If camera access is enabled, the camera is enabled and accessed. If not, the camera is stored and used for the
* future initial camera access.
*
* Depending on device features and user permissions for camera access, any of the following errors
* could be the rejected result of the returned promise:
* - `AbortError`
* - `NoCameraAvailableError`
* - `NotAllowedError`
* - `NotFoundError`
* - `NotReadableError`
* - `SecurityError`
*
* 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).
*/
async setActiveCamera(camera, cameraSettings) {
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;
}
/**
* Select a camera to be used for video input by specifying the wanted camera type (facing mode/direction): the main
* camera detected for the given camera type will be used.
*
* If camera access is enabled, the camera is enabled and accessed. If not, the camera type is stored and used for the
* future initial camera access.
*
* If the target camera is already in use or no camera with the given type is found this method has no effect.
*
* Depending on device features and user permissions for camera access, any of the following errors
* could be the rejected result of the returned promise:
* - `AbortError`
* - `NoCameraAvailableError`
* - `NotAllowedError`
* - `NotFoundError`
* - `NotReadableError`
* - `SecurityError`
*
* @param cameraType The new camera type (facing mode/direction) to be used for video input and Single Image Mode
* (when available).
* @returns A promise resolving to the updated [[BarcodePicker]] object when the camera is updated
* (and accessed, if camera access is currently enabled).
*/
async setCameraType(cameraType) {
this.gui.setCameraType(cameraType);
if (this.cameraAccess) {
await this.cameraManager.setCameraType(cameraType);
}
else {
this.cameraManager.setInitialCameraType(cameraType);
}
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. If not, the camera settings
* are stored and used for the future initial camera access.
*
* Depending on device features and user permissions for camera access, any of the following errors
* could be the rejected result of the returned promise:
* - `AbortError`
* - `NoCameraAvailableError`
* - `NotAllowedError`
* - `NotFoundError`
* - `NotReadableError`
* - `SecurityError`
*
* 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).
*/
async applyCameraSettings(cameraSettings) {
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.
*/
isVisible() {
return this.gui.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.
*/
setVisible(visible) {
this.gui.setVisible(visible);
return this;
}
/**
* @returns Whether the currently selected camera's video is mirrored along the vertical axis.
*/
isMirrorImageEnabled() {
return this.gui.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.
*/
setMirrorImageEnabled(enabled) {
this.gui.setMirrorImageEnabled(enabled, true);
return this;
}
/**
* @returns Whether a sound should be played on barcode/text recognition (iOS requires user input).
* Note that the sound is played if there's at least a barcode or text not rejected via [[ScanResult.rejectCode]] or
* [[ScanResult.rejectText]].
*/
isPlaySoundOnScanEnabled() {
return this.playSoundOnScan;
}
/**
* Enable or disable playing a sound on barcode/text recognition (iOS requires user input).
*
* The sound is played if there's at least a barcode or text not rejected via [[ScanResult.rejectCode]] or
* [[ScanResult.rejectText]].
*
* @param enabled Whether a sound should be played on barcode/text recognition.
* @returns The updated [[BarcodePicker]] object.
*/
setPlaySoundOnScanEnabled(enabled) {
this.playSoundOnScan = enabled;
return this;
}
/**
* @returns Whether the device should vibrate on barcode/text recognition (only Chrome & Firefox, requires user
* input).
* Note that the vibration is triggered if there's at least a barcode or text not rejected via
* [[ScanResult.rejectCode]] or [[ScanResult.rejectText]].
*/
isVibrateOnScanEnabled() {
return this.vibrateOnScan;
}
/**
* Enable or disable vibrating the device on barcode/text recognition (only Chrome & Firefox, requires user input).
*
* The vibration is triggered if there's at least a barcode or text not rejected via [[ScanResult.rejectCode]] or
* [[ScanResult.rejectText]].
*
* @param enabled Whether the device should vibrate on barcode/text recognition.
* @returns The updated [[BarcodePicker]] object.
*/
setVibrateOnScanEnabled(enabled) {
this.vibrateOnScan = enabled;
return this;
}
/**
* @returns Whether a GUI button to switch between different cameras is shown (when available).
*/
isCameraSwitcherEnabled() {
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.
*/
setCameraSwitcherEnabled(enabled) {
this.cameraManager.setCameraSwitcherEnabled(enabled).catch(
/* istanbul ignore next */ () => {
// Ignored
});
return this;
}
/**
* @hidden
*
* @returns Whether a GUI button to switch between Wide and UltraWide cameras on specific "broken" iPhone devices is
* shown (when available).
*/
isCameraFOVSwitcherEnabled() {
return this.cameraManager.isCameraFOVSwitcherEnabled();
}
/**
* Show or hide a GUI button to switch between Wide and UltraWide cameras on iPhone devices
* (when available).
*
* In Single Image Mode this method has no effect.
*
* @param enabled Whether to show a GUI button to switch between Wide and UltraWide cameras on specific
* iPhone devices.
* @returns The updated [[BarcodePicker]] object.
*/
async setCameraFOVSwitcherEnabled(enabled) {
await this.cameraManager.setCameraFOVSwitcherEnabled(enabled);
return this;
}
/**
* @returns Whether a GUI button to toggle device torch on/off is shown (when available, only Chrome).
*/
isTorchToggleEnabled() {
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.
*/
setTorchToggleEnabled(enabled) {
this.cameraManager.setTorchToggleEnabled(enabled);
return this;
}
/**
* @returns Whether manual camera focus when clicking/tapping on the video is enabled (when available, only Chrome).
*/
isTapToFocusEnabled() {
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.
*/
setTapToFocusEnabled(enabled) {
this.cameraManager.setTapToFocusEnabled(enabled);
return this;
}
/**
* @returns Whether camera zoom control via pinching gesture on the video is enabled (when available, only Chrome).
*/
isPinchToZoomEnabled() {
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.
*/
setPinchToZoomEnabled(enabled) {
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 A promise resolving to the updated [[BarcodePicker]] object when the torch is enabled/disabled.
*/
async setTorchEnabled(enabled) {
await 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.
*/
async setZoom(zoomPercentage) {
await this.cameraManager.setZoom(zoomPercentage);
return this;
}
/**
* @returns Whether the barcode picker has loaded the external Scandit Data Capture library and is ready to scan.
*/
isReady() {
return this.isReadyToWork;
}
on(eventName, listener, once = false) {
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.
*/
removeListener(eventName, listener) {
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.
*/
removeAllListeners(eventName) {
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 <div class="tsd-signature-symbol">Default = false</div>
* Whether the listener should just be triggered only once and then discarded.
* @returns The updated [[BarcodePicker]] object.
*/
// tslint:disable-next-line:bool-param-default
addListener(eventName, listener, once) {
return this.on(eventName, listener, once);
}
/**
* 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/text recognition.
* Note that the GUI will flash if there's at least a barcode or text not rejected via [[ScanResult.rejectCode]] or
* [[ScanResult.rejectText]].
*
* @param guiStyle The new GUI style to be applied.
* @returns The updated [[BarcodePicker]] object.
*/
setGuiStyle(guiStyle) {
this.gui.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/text 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.
*/
setVideoFit(objectFit) {
this.gui.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:
* - `AbortError`
* - `NoCameraAvailableError`
* - `NotAllowedError`
* - `NotFoundError`
* - `NotReadableError`
* - `SecurityError`
*
* In Single Image Mode this method has no effect.
*
* @returns A promise resolving to the updated [[BarcodePicker]] object when the camera is accessed.
*/
async accessCamera() {
if (!this.cameraAccess || this.getActiveCamera() == 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.
*/
createParserForFormat(dataFormat) {
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.
*/
reassignOriginElement(originElement) {
if (!BrowserHelper.isValidHTMLElement(originElement)) {
throw new CustomError({
name: "NoOriginElementError",
message: "A valid origin HTML element must be given",
});
}
this.gui.reassignOriginElement(originElement);
return this;
}
/**
* Set the target frames per second to be processed by the external Scandit Data Capture library.
*
* 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.
*/
setTargetScanningFPS(targetScanningFPS) {
if (targetScanningFPS <= 0) {
targetScanningFPS = 30;
}
this.targetScanningFPS = targetScanningFPS;
return this;
}
/**
* @returns The internally used initialized (and possibly configured) [[Scanner]] object instance.
*/
getScanner() {
return this.scanner;
}
/**
* Clear the internal scanner session.
*
* This removes all recognized barcodes/texts from the scanner session and allows them to be scanned again in case a
* custom *codeDuplicateFilter* and/or *textDuplicateFilter* option was set in [[ScanSettings]] or
* [[TextRecognitionSettings]].
*
* @returns The updated [[BarcodePicker]] object.
*/
clearSession() {
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/texts 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.
*/
setLaserArea(area) {
this.gui.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/texts 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.
*/
setViewfinderArea(area) {
this.gui.setViewfinderArea(area);
return this;
}
/**
* @hidden
*
* Pause the camera stream (camera access is fully interrupted).
*
* @returns The updated [[BarcodePicker]] object.
*/
pauseCameraAccess() {
this.cameraAccess = false;
this.cameraManager.stopStream().catch(
/* istanbul ignore next */ () => {
// Ignored
});
return this;
}
triggerCameraAccessError(error) {
this.eventEmitter.emit("cameraAccessError", error);
}
handleScanResult(scanResult) {
scanResult.imageData = this.externalImageData;
this.eventEmitter.emit("processFrame", scanResult);
if (scanResult.barcodes.length !== 0 || scanResult.texts.length !== 0) {
// This will get executed only after the other existing listeners for "processFrame" and "scan" are executed
this.eventEmitter.once("scan", () => {
const acceptedBarcodes = scanResult.barcodes.some((barcode) => {
return !scanResult.rejectedCodes.has(barcode);
});
const acceptedTexts = scanResult.texts.some((text) => {
return !scanResult.rejectedTexts.has(text);
});
if (acceptedBarcodes || acceptedTexts) {
this.gui.flashGUI();
if (this.playSoundOnScan) {
this.beepSound.play();
}
if (this.vibrateOnScan) {
this.vibrateFunction?.call(navigator, 300);
}
}
});
this.eventEmitter.emit("scan", scanResult);
}
}
scheduleVideoProcessing(timeout = 0) {
window.setTimeout(async () => {
await this.videoProcessing();
}, timeout); // Leave some breathing room for other operations
}
async scheduleNextVideoProcessing(processingStartTime) {
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 = Math.max(0, 1000 / this.targetScanningFPS - this.averageProcessingTime);
if (Math.round(nextProcessingCallDelay) <= 16) {
await this.videoProcessing();
}
else {
this.scheduleVideoProcessing(nextProcessingCallDelay);
}
}
else {
await this.videoProcessing();
}
}
async processVideoFrame(highQualitySingleFrameMode) {
this.internalImageData = this.gui.getImageData(this.internalImageData);
// This could happen in unexpected situations and should be temporary
// istanbul ignore if
if (this.internalImageData == null) {
return;
}
if (this.externalImageData == null ||
this.externalImageData.byteLength === 0 ||
this.externalImageData.byteLength !== this.internalImageData.byteLength) {
this.externalImageData = new Uint8Array(this.internalImageData);
}
else {
this.externalImageData.set(this.internalImageData);
}
if (!this.scanningPaused) {
if (this.eventEmitter.listenerCount("submitFrame") > 0) {
this.eventEmitter.emit("submitFrame", new ScanResult([], [], this.externalImageData, this.scanner.getImageSettings()));
}
try {
const scanResult = await this.scanner.processImage(this.internalImageData, highQualitySingleFrameMode);
this.internalImageData = scanResult.imageData;
// Paused status could have changed in the meantime
if (!this.scanningPaused) {
this.handleScanResult(scanResult);
}
}
catch (error) {
this.internalImageData = undefined;
if (error.name === "ImageSettingsDataMismatch") {
// This could happen in unexpected situations and should be temporary
return;
}
this.pauseScanning();
this.eventEmitter.emit("scanError", error);
}
}
}
async videoProcessing() {
if (this.destroyed) {
return;
}
if (this.getActiveCamera()?.currentResolution == null ||
this.scanningPaused ||
!this.scanner.isReady() ||
this.scanner.isBusyProcessing() ||
this.latestVideoTimeProcessed === this.gui.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 = performance.now();
this.latestVideoTimeProcessed = this.gui.getVideoCurrentTime();
await this.processVideoFrame(false);
await this.scheduleNextVideoProcessing(processingStartTime);
}
handleScannerReady() {
this.isReadyToWork = true;
this.eventEmitter.emit("ready");
}
}
// istanbul ignore next
(function (BarcodePicker) {
/**
* GUI style to be used by a barcode picker, used to hint barcode/text placement in the frame.
*/
let GuiStyle;
(function (GuiStyle) {
/**
* No GUI is shown to indicate where the barcode/text should be placed.
* Be aware that the Scandit logo continues to be displayed as showing it is part of the license agreement.
*/
GuiStyle["NONE"] = "none";
/**
* A laser line is shown.
*/
GuiStyle["LASER"] = "laser";
/**
* A rect