UNPKG

scandit-sdk

Version:

Scandit Barcode Scanner SDK for the Web

925 lines 44 kB
import { ResizeObserver as ResizeObserverPolyfill } from "@juggle/resize-observer"; /** * @hidden */ // tslint:disable-next-line: variable-name no-any const ResizeObserver = window.ResizeObserver ?? ResizeObserverPolyfill; import { laserActiveImage, laserPausedImage, scanditLogoImage, switchCameraFOVUltraWideImage, switchCameraFOVWideImage, switchCameraImage, toggleTorchImage, transparentPixelImage, } from "../assets/base64assets"; import { BrowserHelper } from "../browserHelper"; import { Camera } from "../camera"; import { CameraAccess } from "../cameraAccess"; import { ImageSettings } from "../imageSettings"; import { Logger } from "../logger"; import { SingleImageModeSettings } from "../singleImageModeSettings"; import { BarcodePicker } from "./barcodePicker"; export class GUI { static grandParentElementClassName = "scandit scandit-container"; static parentElementClassName = "scandit scandit-barcode-picker"; static hiddenClassName = "scandit-hidden"; static hiddenOpacityClassName = "scandit-hidden-opacity"; static videoElementClassName = "scandit-video"; static scanditLogoImageElementClassName = "scandit-logo"; static laserContainerElementClassName = "scandit-laser"; static viewfinderElementClassName = "scandit-viewfinder"; static cameraSwitcherElementClassName = "scandit-camera-switcher"; static cameraFOVSwitcherElementClassName = "scandit-camera-fov-switcher"; static cameraRecoveryElementClassName = "scandit-camera-recovery"; static torchTogglerElementClassName = "scandit-torch-toggle"; static cameraUploadElementClassName = "scandit-camera-upload"; static flashColorClassName = "scandit-flash-color"; static flashWhiteClassName = "scandit-flash-white"; static flashInsetClassName = "scandit-flash-inset"; static opacityPulseClassName = "scandit-opacity-pulse"; static mirroredClassName = "mirrored"; static pausedClassName = "paused"; videoElement; cameraSwitcherElement; cameraFOVSwitcherElement; cameraFOVSwitcherUltraWideElement; cameraFOVSwitcherWideElement; torchTogglerElement; cameraRecoveryElement; scanner; singleImageModeEnabled; singleImageModeSettings; grandParentElement; parentElement; laserContainerElement; laserActiveImageElement; laserPausedImageElement; viewfinderElement; cameraUploadElement; cameraUploadLabelElement; cameraUploadInputElement; cameraUploadProgressElement; visibilityListener; videoPauseListener; videoResizeListener; webGLContextLostListener; newScanSettingsListener; contextCreatedShowLogoListener; contextCreatedActivateGUIListener; resizeObserver; cameraUploadCallback; mirrorImageOverrides; contextWebGL; context2d; cameraManager; originElement; scanningPaused; visible; guiStyle; videoFit; customLaserArea; customViewfinderArea; cameraUploadInProgress; cameraSwitchInProgress; dataCaptureContextCreated; // This property was introduced because testing videoElement.offsetParent in Safari // proved to be unreliable. isVideoElementDetached = false; constructor(options) { this.scanner = options.scanner; this.originElement = options.originElement; this.singleImageModeEnabled = options.singleImageModeEnabled; this.singleImageModeSettings = options.singleImageModeSettings; this.scanningPaused = options.scanningPaused; this.cameraUploadCallback = options.cameraUploadCallback; this.mirrorImageOverrides = new Map(); this.cameraUploadInProgress = false; this.cameraSwitchInProgress = false; this.dataCaptureContextCreated = false; this.grandParentElement = document.createElement("div"); this.grandParentElement.className = GUI.grandParentElementClassName; this.originElement.appendChild(this.grandParentElement); this.parentElement = document.createElement("div"); this.parentElement.className = GUI.parentElementClassName; this.grandParentElement.appendChild(this.parentElement); this.videoElement = document.createElement("video"); this.cameraSwitcherElement = document.createElement("img"); this.cameraFOVSwitcherElement = document.createElement("div"); this.cameraFOVSwitcherUltraWideElement = document.createElement("img"); this.cameraFOVSwitcherWideElement = document.createElement("img"); this.torchTogglerElement = document.createElement("img"); this.laserContainerElement = document.createElement("div"); this.laserActiveImageElement = document.createElement("img"); this.laserPausedImageElement = document.createElement("img"); this.cameraRecoveryElement = document.createElement("div"); this.viewfinderElement = document.createElement("div"); const canvas = document.createElement("canvas"); this.webGLContextLostListener = this.handleWebGLContextLost.bind(this); if (options.singleImageModeEnabled) { this.context2d = canvas.getContext("2d"); this.cameraUploadElement = document.createElement("div"); this.cameraUploadLabelElement = document.createElement("label"); this.cameraUploadInputElement = document.createElement("input"); this.cameraUploadProgressElement = document.createElement("div"); this.setupCameraUploadGuiAssets(); this.guiStyle = BarcodePicker.GuiStyle.NONE; } else { this.setupContext(canvas); this.setupVideoElement(); this.setupCameraSwitcher(); this.setupCameraFOVSwitcher(); this.setupTorchToggler(); this.setupCameraRecovery(options.cameraRecoveryText); this.setupFullGuiAssets(); this.setGuiStyle(options.guiStyle); this.setVideoFit(options.videoFit); this.setLaserArea(options.laserArea); this.setViewfinderArea(options.viewfinderArea); // Ensure the camera is accessed and the video plays again correctly when visibility changes this.visibilityListener = this.checkAndRecoverPlayback.bind(this); document.addEventListener("visibilitychange", this.visibilityListener); this.newScanSettingsListener = this.handleNewScanSettings.bind(this); this.scanner.on("newScanSettings", this.newScanSettingsListener); this.handleNewScanSettings(); this.videoPauseListener = this.handleVideoPause.bind(this); this.videoElement.addEventListener("pause", this.videoPauseListener); this.videoResizeListener = this.handleVideoResize.bind(this); this.videoElement.addEventListener("resize", this.videoResizeListener); } if (options.hideLogo) { this.contextCreatedShowLogoListener = this.showScanditLogo.bind(this, options.hideLogo); this.scanner.on("contextCreated", this.contextCreatedShowLogoListener); } else { this.showScanditLogo(options.hideLogo); } this.contextCreatedActivateGUIListener = this.activateGUI.bind(this); this.scanner.on("contextCreated", this.contextCreatedActivateGUIListener); this.resize(); this.resizeObserver = new ResizeObserver( /* istanbul ignore next */ (entries) => { if (typeof window.requestAnimationFrame != null) { window.requestAnimationFrame(() => { this.resize(entries[0]); }); } else { this.resize(entries[0]); } }); this.resizeObserver.observe(this.originElement); this.setVisible(options.visible); } destroy() { if (this.visibilityListener != null) { document.removeEventListener("visibilitychange", this.visibilityListener); } if (this.newScanSettingsListener != null) { this.scanner.removeListener("newScanSettings", this.newScanSettingsListener); } if (this.videoPauseListener != null) { this.videoElement.removeEventListener("pause", this.videoPauseListener); } if (this.videoResizeListener != null) { this.videoElement.removeEventListener("resize", this.videoResizeListener); } if (this.contextCreatedShowLogoListener != null) { this.scanner.removeListener("contextCreated", this.contextCreatedShowLogoListener); } if (this.contextCreatedActivateGUIListener != null) { this.scanner.removeListener("contextCreated", this.contextCreatedActivateGUIListener); } this.resizeObserver.disconnect(); this.grandParentElement.remove(); this.videoElement.remove(); this.contextWebGL?.canvas?.removeEventListener("webglcontextlost", this.webGLContextLostListener); this.contextWebGL?.getExtension("WEBGL_lose_context")?.loseContext(); this.contextWebGL = undefined; this.context2d = undefined; this.originElement.classList.remove(GUI.hiddenClassName); } setCameraManager(cameraManager) { this.cameraManager = cameraManager; } pauseScanning() { this.scanningPaused = true; this.laserActiveImageElement.classList.add(GUI.hiddenOpacityClassName); this.laserPausedImageElement.classList.remove(GUI.hiddenOpacityClassName); this.viewfinderElement.classList.add(GUI.pausedClassName); } resumeScanning() { this.scanningPaused = false; if (this.dataCaptureContextCreated) { this.laserPausedImageElement.classList.add(GUI.hiddenOpacityClassName); this.laserActiveImageElement.classList.remove(GUI.hiddenOpacityClassName); this.viewfinderElement.classList.remove(GUI.pausedClassName); } } isVisible() { return this.visible; } setVisible(visible) { this.visible = visible; if (visible) { this.originElement.classList.remove(GUI.hiddenClassName); if (this.guiStyle === BarcodePicker.GuiStyle.LASER) { this.laserActiveImageElement.classList.remove(GUI.flashColorClassName); } else if (this.guiStyle === BarcodePicker.GuiStyle.VIEWFINDER) { this.viewfinderElement.classList.remove(GUI.flashWhiteClassName); } } else { this.originElement.classList.add(GUI.hiddenClassName); } } isMirrorImageEnabled() { if (this.cameraManager?.selectedCamera != null && this.cameraManager?.activeCamera != null) { const mirrorImageOverride = this.mirrorImageOverrides.get(this.cameraManager.activeCamera); return mirrorImageOverride ?? this.cameraManager.activeCamera.cameraType === Camera.Type.FRONT; } else { return false; } } setMirrorImageEnabled(enabled, override) { if (this.cameraManager?.selectedCamera != null) { if (enabled) { this.videoElement.classList.add(GUI.mirroredClassName); } else { this.videoElement.classList.remove(GUI.mirroredClassName); } if (override) { this.mirrorImageOverrides.set(this.cameraManager.selectedCamera, enabled); } } } setGuiStyle(guiStyle) { if (this.singleImageModeEnabled) { return; } switch (guiStyle) { case BarcodePicker.GuiStyle.LASER: this.guiStyle = guiStyle; this.laserContainerElement.classList.remove(GUI.hiddenClassName); this.viewfinderElement.classList.add(GUI.hiddenClassName); break; case BarcodePicker.GuiStyle.VIEWFINDER: this.guiStyle = guiStyle; this.laserContainerElement.classList.add(GUI.hiddenClassName); this.viewfinderElement.classList.remove(GUI.hiddenClassName); break; case BarcodePicker.GuiStyle.NONE: default: this.guiStyle = BarcodePicker.GuiStyle.NONE; this.laserContainerElement.classList.add(GUI.hiddenClassName); this.viewfinderElement.classList.add(GUI.hiddenClassName); break; } } setLaserArea(area) { this.customLaserArea = area; if (area == null) { area = this.scanner.getScanSettings().getSearchArea(); } const borderPercentage = 0.025; const usablePercentage = 1 - borderPercentage * 2; this.laserContainerElement.style.left = `${(borderPercentage + area.x * usablePercentage) * 100}%`; this.laserContainerElement.style.width = `${area.width * usablePercentage * 100}%`; this.laserContainerElement.style.top = `${(borderPercentage + area.y * usablePercentage) * 100}%`; this.laserContainerElement.style.height = `${area.height * usablePercentage * 100}%`; } setViewfinderArea(area) { this.customViewfinderArea = area; if (area == null) { area = this.scanner.getScanSettings().getSearchArea(); } const borderPercentage = 0.025; const usablePercentage = 1 - borderPercentage * 2; this.viewfinderElement.style.left = `${(borderPercentage + area.x * usablePercentage) * 100}%`; this.viewfinderElement.style.width = `${area.width * usablePercentage * 100}%`; this.viewfinderElement.style.top = `${(borderPercentage + area.y * usablePercentage) * 100}%`; this.viewfinderElement.style.height = `${area.height * usablePercentage * 100}%`; } setVideoFit(objectFit) { if (this.singleImageModeEnabled) { return; } this.videoFit = objectFit; if (objectFit === BarcodePicker.ObjectFit.COVER) { this.videoElement.style.objectFit = "cover"; this.videoElement.dataset.objectFit = "cover"; // used by "objectFitPolyfill" library } else { this.videoElement.style.objectFit = "contain"; this.videoElement.dataset.objectFit = "contain"; // used by "objectFitPolyfill" library this.scanner.applyScanSettings(this.scanner.getScanSettings().setBaseSearchArea({ x: 0, y: 0, width: 1.0, height: 1.0 })); } this.resize(); } reassignOriginElement(originElement) { if (!this.visible) { this.originElement.classList.remove(GUI.hiddenClassName); originElement.classList.add(GUI.hiddenClassName); } originElement.appendChild(this.grandParentElement); this.checkAndRecoverPlayback().catch( /* istanbul ignore next */ () => { // Ignored }); this.resize(); this.resizeObserver.disconnect(); this.resizeObserver.observe(originElement); this.originElement = originElement; this.resize(); } flashGUI() { if (this.guiStyle === BarcodePicker.GuiStyle.LASER) { this.flashLaser(); } else if (this.guiStyle === BarcodePicker.GuiStyle.VIEWFINDER) { this.flashViewfinder(); } } getImageData(imageData) { function isVideoAndContextStateValid(videoElement, context) { // This could happen in unexpected situations and should be temporary return (videoElement.readyState === 4 && videoElement.videoWidth > 2 && videoElement.videoHeight > 2 && context.canvas.width > 2 && context.canvas.height > 2); } if (this.singleImageModeEnabled && this.context2d != null) { return new Uint8Array(this.context2d.getImageData(0, 0, this.context2d.canvas.width, this.context2d.canvas.height).data.buffer); } // istanbul ignore else if (!this.singleImageModeEnabled) { if (this.contextWebGL != null) { if (!isVideoAndContextStateValid(this.videoElement, this.contextWebGL) || this.contextWebGL.drawingBufferWidth <= 2 || this.contextWebGL.drawingBufferHeight <= 2) { return; } const imageDataLength = this.contextWebGL.drawingBufferWidth * this.contextWebGL.drawingBufferHeight * 4; if (imageData == null || imageData.byteLength === 0 || imageData.byteLength !== imageDataLength) { imageData = new Uint8Array(imageDataLength); } this.contextWebGL.texImage2D(this.contextWebGL.TEXTURE_2D, 0, this.contextWebGL.RGBA, this.contextWebGL.RGBA, this.contextWebGL.UNSIGNED_BYTE, this.videoElement); this.contextWebGL.readPixels(0, 0, this.contextWebGL.drawingBufferWidth, this.contextWebGL.drawingBufferHeight, this.contextWebGL.RGBA, this.contextWebGL.UNSIGNED_BYTE, imageData); // Detect incorrect GPU accelerated WebGL image processing by checking for incorrect alpha channel data if (imageData[3] !== 255) { Logger.log(Logger.Level.WARN, "Detected incorrect GPU accelerated WebGL image processing, switching to canvas mode"); this.contextWebGL = undefined; this.setupContext(document.createElement("canvas"), true); this.handleVideoResize(); return this.getImageData(imageData); } return imageData; } // istanbul ignore else if (this.context2d != null) { if (!isVideoAndContextStateValid(this.videoElement, this.context2d)) { return; } this.context2d.drawImage(this.videoElement, 0, 0); return new Uint8Array(this.context2d.getImageData(0, 0, this.context2d.canvas.width, this.context2d.canvas.height).data.buffer); } } // istanbul ignore next return; } getVideoCurrentTime() { return this.videoElement.currentTime; } setCameraSwitcherVisible(visible) { if (visible) { this.cameraSwitcherElement.classList.remove(GUI.hiddenClassName); } else { this.cameraSwitcherElement.classList.add(GUI.hiddenClassName); } } setCameraFOVSwitcherVisible(visible) { if (visible) { this.cameraFOVSwitcherElement.classList.remove(GUI.hiddenClassName); } else { this.cameraFOVSwitcherElement.classList.add(GUI.hiddenClassName); } } setCameraFOVSwitcherState(ultraWide) { if (ultraWide) { this.cameraFOVSwitcherUltraWideElement.classList.remove(GUI.hiddenClassName); this.cameraFOVSwitcherWideElement.classList.add(GUI.hiddenClassName); } else { this.cameraFOVSwitcherWideElement.classList.remove(GUI.hiddenClassName); this.cameraFOVSwitcherUltraWideElement.classList.add(GUI.hiddenClassName); } } isCameraRecoveryVisible() { return !this.cameraRecoveryElement.classList.contains(GUI.hiddenClassName); } setCameraRecoveryVisible(visible) { if (visible) { this.cameraRecoveryElement.classList.remove(GUI.hiddenClassName); } else { this.cameraRecoveryElement.classList.add(GUI.hiddenClassName); } } setTorchTogglerVisible(visible) { if (visible) { this.torchTogglerElement.classList.remove(GUI.hiddenClassName); } else { this.torchTogglerElement.classList.add(GUI.hiddenClassName); } } playVideo() { const playPromise = this.videoElement.play(); playPromise?.catch( /* istanbul ignore next */ () => { // Can sometimes cause an incorrect rejection (all is good, ignore). }); } setVideoVisible(visible) { this.videoElement.style.visibility = visible ? "visible" : "hidden"; } setCameraType(cameraType) { this.cameraUploadInputElement?.setAttribute("capture", cameraType === Camera.Type.FRONT ? "user" : "environment"); } setCameraUploadGuiBusyScanning(busyScanning) { if (busyScanning) { this.cameraUploadProgressElement.classList.remove(GUI.flashInsetClassName); this.cameraUploadElement.classList.add(GUI.opacityPulseClassName); } else { this.cameraUploadProgressElement.classList.add(GUI.flashInsetClassName); this.cameraUploadElement.classList.remove(GUI.opacityPulseClassName); } } setupContext(canvas, force2d = false) { if (force2d) { this.context2d = canvas.getContext("2d"); return; } let context = canvas.getContext("webgl", { alpha: false, antialias: false }); // istanbul ignore if if (context == null) { context = canvas.getContext("experimental-webgl", { alpha: false, antialias: false }); } if (context != null) { this.setupWebGL(context); canvas.addEventListener("webglcontextlost", this.webGLContextLostListener); } else { this.context2d = canvas.getContext("2d"); } } setupWebGL(contextWebGL) { const texture = contextWebGL.createTexture(); contextWebGL.bindTexture(contextWebGL.TEXTURE_2D, texture); const frameBuffer = contextWebGL.createFramebuffer(); contextWebGL.bindFramebuffer(contextWebGL.FRAMEBUFFER, frameBuffer); contextWebGL.framebufferTexture2D(contextWebGL.FRAMEBUFFER, contextWebGL.COLOR_ATTACHMENT0, contextWebGL.TEXTURE_2D, texture, 0); contextWebGL.texParameteri(contextWebGL.TEXTURE_2D, contextWebGL.TEXTURE_WRAP_S, contextWebGL.CLAMP_TO_EDGE); contextWebGL.texParameteri(contextWebGL.TEXTURE_2D, contextWebGL.TEXTURE_WRAP_T, contextWebGL.CLAMP_TO_EDGE); contextWebGL.texParameteri(contextWebGL.TEXTURE_2D, contextWebGL.TEXTURE_MIN_FILTER, contextWebGL.NEAREST); contextWebGL.texParameteri(contextWebGL.TEXTURE_2D, contextWebGL.TEXTURE_MAG_FILTER, contextWebGL.NEAREST); this.contextWebGL = contextWebGL; } setupVideoElement() { this.videoElement.setAttribute("autoplay", "autoplay"); this.videoElement.setAttribute("playsinline", "true"); this.videoElement.setAttribute("muted", "muted"); this.videoElement.setAttribute("poster", transparentPixelImage); this.videoElement.className = GUI.videoElementClassName; this.parentElement.appendChild(this.videoElement); } setupCameraUploadGuiAssets() { const deviceType = BrowserHelper.userAgentInfo.getDevice().type; const defaultSettings = deviceType === "mobile" || deviceType === "tablet" ? SingleImageModeSettings.defaultMobile : SingleImageModeSettings.defaultDesktop; this.cameraUploadElement.className = GUI.cameraUploadElementClassName; Object.assign(this.cameraUploadElement.style, defaultSettings.containerStyle, this.singleImageModeSettings.containerStyle); this.parentElement.appendChild(this.cameraUploadElement); const informationElement = this.singleImageModeSettings.informationElement ?? defaultSettings.informationElement; Object.assign(informationElement.style, defaultSettings.informationStyle, this.singleImageModeSettings.informationStyle); this.cameraUploadElement.appendChild(informationElement); this.cameraUploadInputElement.type = "file"; this.cameraUploadInputElement.accept = "image/*"; this.cameraUploadInputElement.addEventListener("change", this.cameraUploadFile.bind(this)); const cameraUploadInputCheckFunction = (event) => { // istanbul ignore next if (this.scanningPaused || this.cameraUploadInProgress) { event.preventDefault(); } }; this.cameraUploadInputElement.addEventListener("click", cameraUploadInputCheckFunction); this.cameraUploadInputElement.addEventListener("keydown", cameraUploadInputCheckFunction); this.cameraUploadLabelElement.appendChild(this.cameraUploadInputElement); const cameraUploadButtonIconElement = this.singleImageModeSettings.buttonElement ?? defaultSettings.buttonElement; [this.cameraUploadProgressElement.style, cameraUploadButtonIconElement.style].forEach((style) => { Object.assign(style, defaultSettings.buttonStyle, this.singleImageModeSettings.buttonStyle); }); cameraUploadButtonIconElement.style.maxWidth = "100px"; cameraUploadButtonIconElement.style.maxHeight = "100px"; this.cameraUploadLabelElement.appendChild(cameraUploadButtonIconElement); this.cameraUploadProgressElement.classList.add("radial-progress"); this.cameraUploadLabelElement.appendChild(this.cameraUploadProgressElement); this.cameraUploadElement.appendChild(this.cameraUploadLabelElement); } setupFullGuiAssets() { this.laserActiveImageElement.src = laserActiveImage; this.laserContainerElement.appendChild(this.laserActiveImageElement); this.laserPausedImageElement.src = laserPausedImage; this.laserContainerElement.appendChild(this.laserPausedImageElement); this.laserContainerElement.className = GUI.laserContainerElementClassName; this.parentElement.appendChild(this.laserContainerElement); this.viewfinderElement.className = GUI.viewfinderElementClassName; this.parentElement.appendChild(this.viewfinderElement); // Show inactive GUI, as for now the scanner isn't ready yet this.laserActiveImageElement.classList.add(GUI.hiddenOpacityClassName); this.laserPausedImageElement.classList.remove(GUI.hiddenOpacityClassName); this.viewfinderElement.classList.add(GUI.pausedClassName); } flashLaser() { this.laserActiveImageElement.classList.remove(GUI.flashColorClassName); // tslint:disable-next-line:no-unused-expression this.laserActiveImageElement.offsetHeight; // NOSONAR // Trigger reflow to restart animation this.laserActiveImageElement.classList.add(GUI.flashColorClassName); } flashViewfinder() { this.viewfinderElement.classList.remove(GUI.flashWhiteClassName); // tslint:disable-next-line:no-unused-expression this.viewfinderElement.offsetHeight; // NOSONAR // Trigger reflow to restart animation this.viewfinderElement.classList.add(GUI.flashWhiteClassName); } resize(originElement) { this.parentElement.style.maxWidth = ""; this.parentElement.style.maxHeight = ""; const { width, height } = originElement?.contentRect ?? { width: this.originElement.clientWidth, height: this.originElement.clientHeight, }; if (width === 0 || height === 0) { if (!this.singleImageModeEnabled) { this.handleVideoDisplay(true); } return; } if (this.singleImageModeEnabled) { this.resizeCameraUpload(width, height); } else { this.resizeVideo(width, height); this.handleVideoDisplay(false); } } resizeCameraUpload(width, height) { this.cameraUploadLabelElement.style.transform = `scale(${Math.min(1, width / 300, height / 300)})`; } resizeVideo(width, height) { if (this.videoElement.videoWidth <= 2 || this.videoElement.videoHeight <= 2) { return; } const videoRatio = this.videoElement.videoWidth / this.videoElement.videoHeight; if (this.videoFit === BarcodePicker.ObjectFit.COVER) { let widthPercentage = 1; let heightPercentage = 1; if (videoRatio < width / height) { heightPercentage = Math.min(1, height / (width / videoRatio)); } else { widthPercentage = Math.min(1, width / (height * videoRatio)); } this.scanner.applyScanSettings(this.scanner.getScanSettings().setBaseSearchArea({ x: (1 - widthPercentage) / 2, y: (1 - heightPercentage) / 2, width: widthPercentage, height: heightPercentage, })); return; } if (videoRatio > width / height) { height = width / videoRatio; } else { width = height * videoRatio; } this.parentElement.style.maxWidth = `${Math.ceil(width)}px`; this.parentElement.style.maxHeight = `${Math.ceil(height)}px`; window.objectFitPolyfill(this.videoElement); } async checkAndRecoverPlayback() { const srcObject = this.videoElement.srcObject; if (document.visibilityState === "visible" && this.cameraManager?.activeCamera != null && this.videoElement?.srcObject != null) { if (!srcObject.active || srcObject.getVideoTracks()[0]?.muted !== false) { try { Logger.log(Logger.Level.DEBUG, 'Detected visibility change ("visible") event with inactive video source, try to reinitialize camera'); await this.cameraManager.reinitializeCamera(); } catch { // Ignored } } else { Logger.log(Logger.Level.DEBUG, 'Detected visibility change ("visible") event with active video source, replay video'); this.playVideo(); } } } updateCameraUploadProgress(progressPercentageValue) { this.cameraUploadProgressElement.setAttribute("data-progress", progressPercentageValue); } async cameraUploadImageLoad(image) { this.updateCameraUploadProgress("100"); let resizedImageWidth; let resizedImageHeight; const resizedImageSizeLimit = 1440; if (image.naturalWidth <= resizedImageSizeLimit && image.naturalHeight <= resizedImageSizeLimit) { resizedImageWidth = image.naturalWidth; resizedImageHeight = image.naturalHeight; } else { if (image.naturalWidth > image.naturalHeight) { resizedImageWidth = resizedImageSizeLimit; resizedImageHeight = Math.round((image.naturalHeight / image.naturalWidth) * resizedImageSizeLimit); } else { resizedImageWidth = Math.round((image.naturalWidth / image.naturalHeight) * resizedImageSizeLimit); resizedImageHeight = resizedImageSizeLimit; } } await this.cameraUploadFileProcess(image, resizedImageWidth, resizedImageHeight); } async cameraUploadFileProcess(image, width, height) { // istanbul ignore else if (this.context2d != null) { this.context2d.canvas.width = width; this.context2d.canvas.height = height; this.context2d.drawImage(image, 0, 0, width, height); this.scanner.applyImageSettings({ width, height, format: ImageSettings.Format.RGBA_8U, }); } this.setCameraUploadGuiBusyScanning(true); await this.cameraUploadCallback(); this.setCameraUploadGuiBusyScanning(false); this.cameraUploadInProgress = false; } cameraUploadFile() { const files = this.cameraUploadInputElement.files; if (files != null && files.length !== 0) { this.cameraUploadInProgress = true; const image = new Image(); const fileReader = new FileReader(); fileReader.onload = () => { this.cameraUploadInputElement.value = ""; // istanbul ignore else if (fileReader.result != null) { image.onload = this.cameraUploadImageLoad.bind(this, image); // istanbul ignore next image.onprogress = (event2) => { if (event2.lengthComputable) { const progress = Math.round((event2.loaded / event2.total) * 20) * 5; if (progress <= 100) { this.updateCameraUploadProgress(progress.toString()); } } }; // istanbul ignore next image.onerror = () => { this.cameraUploadInProgress = false; Logger.log(Logger.Level.WARN, "Could not load image from selected file"); }; image.src = fileReader.result; } }; // istanbul ignore next fileReader.onerror = () => { this.cameraUploadInProgress = false; Logger.log(Logger.Level.WARN, `Error while reading the file: ${fileReader.error?.toString()}`); }; this.updateCameraUploadProgress("0"); fileReader.readAsDataURL(files[0]); } } async cameraSwitcherListener(event) { if (!this.cameraSwitchInProgress && this.cameraManager != null) { const cameraManager = this.cameraManager; event.preventDefault(); try { const cameras = await CameraAccess.getCameras(); if (cameraManager.activeCamera == null) { return; } if (cameras.length <= 1) { this.setCameraSwitcherVisible(false); return; } this.cameraSwitchInProgress = true; const currentCameraIndex = cameras.indexOf(cameraManager.activeCamera); let newCameraIndex = (currentCameraIndex + 1) % cameras.length; while (newCameraIndex !== currentCameraIndex) { try { await cameraManager.initializeCameraWithSettings(cameras[newCameraIndex], cameraManager.activeCameraSettings); } catch (error) { Logger.log(Logger.Level.WARN, "Couldn't access camera:", cameras[newCameraIndex], error); newCameraIndex = (newCameraIndex + 1) % cameras.length; if (newCameraIndex === currentCameraIndex) { this.setCameraSwitcherVisible(false); await cameraManager.initializeCameraWithSettings(cameras[newCameraIndex], cameraManager.activeCameraSettings); } continue; } break; } this.cameraSwitchInProgress = false; } catch (error) { Logger.log(Logger.Level.ERROR, error); this.cameraSwitchInProgress = false; } } } async cameraFOVSwitcherListener(event) { if (!this.cameraSwitchInProgress && this.cameraManager != null) { const cameraManager = this.cameraManager; event.preventDefault(); try { const cameras = await CameraAccess.getCameras(); if (cameraManager.activeCamera == null) { return; } this.cameraSwitchInProgress = true; const alternativeCamera = cameras.find((camera) => { // tslint:disable-next-line: no-non-null-assertion return CameraAccess.isIOSUltraWideBackCameraLabel(cameraManager.activeCamera.label) ? CameraAccess.isIOSWideBackCameraLabel(camera.label) : CameraAccess.isIOSUltraWideBackCameraLabel(camera.label); }); if (alternativeCamera == null) { this.setCameraFOVSwitcherVisible(false); return; } try { await cameraManager.initializeCameraWithSettings(alternativeCamera, cameraManager.activeCameraSettings); } catch (error) { Logger.log(Logger.Level.WARN, "Couldn't access camera:", alternativeCamera, error); } } catch (error) { Logger.log(Logger.Level.ERROR, error); } this.cameraSwitchInProgress = false; } } async cameraRecoveryListener(event) { event.preventDefault(); if (this.cameraManager != null) { this.cameraManager.activeCamera = this.cameraManager.selectedCamera; await this.cameraManager?.reinitializeCamera(); } } setupCameraSwitcher() { this.cameraSwitcherElement.src = switchCameraImage; this.cameraSwitcherElement.className = GUI.cameraSwitcherElementClassName; this.cameraSwitcherElement.classList.add(GUI.hiddenClassName); this.parentElement.appendChild(this.cameraSwitcherElement); ["touchstart", "mousedown"].forEach((eventName) => { this.cameraSwitcherElement.addEventListener(eventName, this.cameraSwitcherListener.bind(this)); }); } setupCameraFOVSwitcher() { this.cameraFOVSwitcherElement.className = GUI.cameraFOVSwitcherElementClassName; this.cameraFOVSwitcherElement.classList.add(GUI.hiddenClassName); this.cameraFOVSwitcherUltraWideElement.src = switchCameraFOVUltraWideImage; this.cameraFOVSwitcherUltraWideElement.classList.add(GUI.hiddenClassName); this.cameraFOVSwitcherWideElement.src = switchCameraFOVWideImage; this.cameraFOVSwitcherWideElement.classList.add(GUI.hiddenClassName); this.cameraFOVSwitcherElement.appendChild(this.cameraFOVSwitcherUltraWideElement); this.cameraFOVSwitcherElement.appendChild(this.cameraFOVSwitcherWideElement); this.parentElement.appendChild(this.cameraFOVSwitcherElement); ["touchstart", "mousedown"].forEach((eventName) => { this.cameraFOVSwitcherElement.addEventListener(eventName, this.cameraFOVSwitcherListener.bind(this)); }); } setupCameraRecovery(cameraRecoveryText) { this.cameraRecoveryElement.textContent = cameraRecoveryText; this.cameraRecoveryElement.className = GUI.cameraRecoveryElementClassName; this.cameraRecoveryElement.classList.add(GUI.hiddenClassName); this.parentElement.appendChild(this.cameraRecoveryElement); ["touchstart", "mousedown"].forEach((eventName) => { this.cameraRecoveryElement.addEventListener(eventName, this.cameraRecoveryListener.bind(this)); }); } setupTorchToggler() { this.torchTogglerElement.src = toggleTorchImage; this.torchTogglerElement.className = GUI.torchTogglerElementClassName; this.torchTogglerElement.classList.add(GUI.hiddenClassName); this.parentElement.appendChild(this.torchTogglerElement); ["touchstart", "mousedown"].forEach((eventName) => { this.torchTogglerElement.addEventListener(eventName, async (event) => { if (this.cameraManager != null) { event.preventDefault(); await this.cameraManager.toggleTorch(); } }); }); } showScanditLogo(hideLogo, licenseKeyFeatures) { if (hideLogo && licenseKeyFeatures?.hiddenScanditLogoAllowed === true) { return; } const scanditLogoImageElement = document.createElement("img"); scanditLogoImageElement.src = scanditLogoImage; scanditLogoImageElement.className = GUI.scanditLogoImageElementClassName; this.parentElement.appendChild(scanditLogoImageElement); } activateGUI() { this.dataCaptureContextCreated = true; if (!this.scanningPaused) { this.resumeScanning(); } } handleNewScanSettings() { if (this.customLaserArea == null) { this.setLaserArea(); } if (this.customViewfinderArea == null) { this.setViewfinderArea(); } } handleVideoDisplay(hidden) { // Safari on iOS 14 behaves weirdly when hiding the video element: // it stops camera access after a few seconds if the related video element is not "visible". // We do the following to maintain the video element "visible" but actually hidden. if (hidden && !this.isVideoElementDetached) { this.videoElement.width = this.videoElement.height = 0; this.videoElement.style.opacity = "0"; document.body.appendChild(this.videoElement); this.isVideoElementDetached = true; } else if (!hidden && this.isVideoElementDetached) { this.parentElement.insertAdjacentElement("afterbegin", this.videoElement); this.isVideoElementDetached = false; this.videoElement.removeAttribute("width"); this.videoElement.removeAttribute("height"); this.videoElement.style.removeProperty("opacity"); this.resize(); } } handleVideoPause() { // Safari behaves weirdly when displaying the video element again after hiding it: // it pauses the video on hide and resumes it on show, then reusing video frames "buffered" from the video just // before it was hidden. We do the following to avoid processing old data. this.playVideo(); } handleVideoResize() { this.resize(); if (this.videoElement.videoWidth <= 2 || this.videoElement.videoHeight <= 2) { return; } if (this.contextWebGL != null) { if (this.contextWebGL.canvas.width === this.videoElement.videoWidth && this.contextWebGL.canvas.height === this.videoElement.videoHeight) { return; } this.contextWebGL.canvas.width = this.videoElement.videoWidth; this.contextWebGL.canvas.height = this.videoElement.videoHeight; this.contextWebGL.viewport(0, 0, this.contextWebGL.drawingBufferWidth, this.contextWebGL.drawingBufferHeight); this.scanner.applyImageSettings({ width: this.contextWebGL.drawingBufferWidth, height: this.contextWebGL.drawingBufferHeight, format: ImageSettings.Format.RGBA_8U, }); } else if (this.context2d != null) { if (this.context2d.canvas.width === this.videoElement.videoWidth && this.context2d.canvas.height === this.videoElement.videoHeight) { return; } this.context2d.canvas.width = this.videoElement.videoWidth; this.context2d.canvas.height = this.videoElement.videoHeight; this.scanner.applyImageSettings({ width: this.videoElement.videoWidth, height: this.videoElement.videoHeight, format: ImageSettings.Format.RGBA_8U, }); } } handleWebGLContextLost() { // We recreate instead of waiting for restore via the webglcontextrestored event as restore might never happen Logger.log(Logger.Level.WARN, "WebGL context has been lost, restoring..."); this.contextWebGL = undefined; this.setupContext(document.createElement("canvas")); this.handleVideoResize(); Logger.log(Logger.Level.WARN, "WebGL context restored"); } } //# sourceMappingURL=gui.js.map