UNPKG

scandit-sdk

Version:

Scandit Barcode Scanner SDK for the Web

811 lines 39.3 kB
import { BrowserHelper } from "../browserHelper"; import { Camera } from "../camera"; import { CameraAccess } from "../cameraAccess"; import { CameraSettings } from "../cameraSettings"; import { CustomError } from "../customError"; import { Logger } from "../logger"; export var MeteringMode; (function (MeteringMode) { MeteringMode["CONTINUOUS"] = "continuous"; MeteringMode["MANUAL"] = "manual"; MeteringMode["NONE"] = "none"; MeteringMode["SINGLE_SHOT"] = "single-shot"; })(MeteringMode || (MeteringMode = {})); export var CameraResolutionConstraint; (function (CameraResolutionConstraint) { CameraResolutionConstraint[CameraResolutionConstraint["ULTRA_HD"] = 0] = "ULTRA_HD"; CameraResolutionConstraint[CameraResolutionConstraint["FULL_HD"] = 1] = "FULL_HD"; CameraResolutionConstraint[CameraResolutionConstraint["HD"] = 2] = "HD"; CameraResolutionConstraint[CameraResolutionConstraint["SD"] = 3] = "SD"; CameraResolutionConstraint[CameraResolutionConstraint["NONE"] = 4] = "NONE"; })(CameraResolutionConstraint || (CameraResolutionConstraint = {})); /** * A barcode picker utility class used to handle camera interaction. */ export class CameraManager { static cameraAccessTimeoutMs = 4000; static videoMetadataCheckTimeoutMs = 4000; static videoMetadataCheckIntervalMs = 50; static getCapabilitiesTimeoutMs = 500; static autofocusIntervalMs = 1500; static manualToAutofocusResumeTimeoutMs = 5000; static manualFocusWaitTimeoutMs = 400; static noCameraErrorParameters = { name: "NoCameraAvailableError", message: "No camera available", }; static notReadableErrorParameters = { name: "NotReadableError", message: "Could not initialize camera correctly", }; selectedCamera; activeCamera; activeCameraSettings; scanner; triggerCameraAccessError; gui; postStreamInitializationListener = this.postStreamInitialization.bind(this); videoResizeListener = this.videoResizeHandle.bind(this); videoTrackEndedListener = this.videoTrackEndedRecovery.bind(this); videoTrackMuteListener = this.videoTrackMuteRecovery.bind(this); triggerManualFocusListener = this.triggerManualFocus.bind(this); triggerZoomStartListener = this.triggerZoomStart.bind(this); triggerZoomMoveListener = this.triggerZoomMove.bind(this); checkCameraVideoStreamAccessIfVisibleListener = this.checkCameraVideoStreamAccessIfVisible.bind(this); cameraType; selectedCameraSettings; mediaStream; mediaTrackCapabilities; cameraAccessTimeout; cameraAccessRejectCallback; videoMetadataCheckInterval; getCapabilitiesTimeout; autofocusInterval; manualToAutofocusResumeTimeout; manualFocusWaitTimeout; cameraSwitcherEnabled; torchToggleEnabled; tapToFocusEnabled; pinchToZoomEnabled; pinchToZoomDistance; pinchToZoomInitialZoom; torchEnabled; cameraInitializationPromise; abortedCameraInitializationResolveCallback; cameraSetupPromise; constructor(scanner, triggerCameraAccessError, gui) { this.scanner = scanner; this.triggerCameraAccessError = triggerCameraAccessError; this.gui = gui; this.cameraType = Camera.Type.BACK; } setInteractionOptions(cameraSwitcherEnabled, torchToggleEnabled, tapToFocusEnabled, pinchToZoomEnabled) { this.cameraSwitcherEnabled = cameraSwitcherEnabled; this.torchToggleEnabled = torchToggleEnabled; this.tapToFocusEnabled = tapToFocusEnabled; this.pinchToZoomEnabled = pinchToZoomEnabled; } isCameraSwitcherEnabled() { return this.cameraSwitcherEnabled; } async setCameraSwitcherEnabled(enabled) { this.cameraSwitcherEnabled = enabled; if (this.cameraSwitcherEnabled) { const cameras = await CameraAccess.getCameras(); if (cameras.length > 1) { this.gui.setCameraSwitcherVisible(true); } } else { this.gui.setCameraSwitcherVisible(false); } } isTorchToggleEnabled() { return this.torchToggleEnabled; } setTorchToggleEnabled(enabled) { this.torchToggleEnabled = enabled; if (this.torchToggleEnabled) { if (this.mediaStream != null && this.mediaTrackCapabilities?.torch === true) { this.gui.setTorchTogglerVisible(true); } } else { this.gui.setTorchTogglerVisible(false); } } isTapToFocusEnabled() { return this.tapToFocusEnabled; } setTapToFocusEnabled(enabled) { this.tapToFocusEnabled = enabled; if (this.mediaStream != null) { if (this.tapToFocusEnabled) { this.enableTapToFocusListeners(); } else { this.disableTapToFocusListeners(); } } } isPinchToZoomEnabled() { return this.pinchToZoomEnabled; } setPinchToZoomEnabled(enabled) { this.pinchToZoomEnabled = enabled; if (this.mediaStream != null) { if (this.pinchToZoomEnabled) { this.enablePinchToZoomListeners(); } else { this.disablePinchToZoomListeners(); } } } setInitialCameraType(cameraType) { this.cameraType = cameraType; } async setCameraType(cameraType) { this.setInitialCameraType(cameraType); const mainCameraForType = CameraAccess.getMainCameraForType(await CameraAccess.getCameras(), cameraType); if (mainCameraForType != null && mainCameraForType !== this.selectedCamera) { return this.initializeCameraWithSettings(mainCameraForType, this.selectedCameraSettings); } } setSelectedCamera(camera) { this.selectedCamera = camera; } setSelectedCameraSettings(cameraSettings) { this.selectedCameraSettings = cameraSettings; } async setupCameras() { if (this.cameraSetupPromise != null) { return this.cameraSetupPromise; } this.cameraSetupPromise = this.setupCamerasAndStream(); return this.cameraSetupPromise; } async stopStream(cameraInitializationFailure = false) { if (this.activeCamera != null) { this.activeCamera.currentResolution = undefined; } this.activeCamera = undefined; if (this.mediaStream != null) { Logger.log(Logger.Level.DEBUG, `Stop camera video stream access${cameraInitializationFailure ? " (abort access detection)" : ""}:`, this.mediaStream); document.removeEventListener("visibilitychange", this.checkCameraVideoStreamAccessIfVisibleListener); window.clearTimeout(this.cameraAccessTimeout); window.clearInterval(this.videoMetadataCheckInterval); window.clearTimeout(this.getCapabilitiesTimeout); window.clearTimeout(this.manualFocusWaitTimeout); window.clearTimeout(this.manualToAutofocusResumeTimeout); window.clearInterval(this.autofocusInterval); // Pause video and asynchronously stop tracks to prevent bug on some Android devices freezing the camera on Chrome this.gui.videoElement.pause(); return new Promise((resolve) => { setTimeout(() => { this.mediaStream?.getVideoTracks().forEach((track) => { track.removeEventListener("ended", this.videoTrackEndedListener); track.stop(); }); this.gui.videoElement.srcObject = null; this.mediaStream = undefined; this.mediaTrackCapabilities = undefined; if (!cameraInitializationFailure) { this.abortedCameraInitializationResolveCallback?.(); } resolve(); }, 0); }); } } async applyCameraSettings(cameraSettings) { this.selectedCameraSettings = cameraSettings; if (this.activeCamera == null) { throw new CustomError(CameraManager.noCameraErrorParameters); } return this.initializeCameraWithSettings(this.activeCamera, cameraSettings); } async reinitializeCamera() { if (this.activeCamera == null) { // If the initial camera isn't active yet, do nothing: if and when the camera is later confirmed to be the correct // (main with wanted type or only) one this method will be called again after the camera is set to be active Logger.log(Logger.Level.DEBUG, "Camera reinitialization delayed"); } else { Logger.log(Logger.Level.DEBUG, "Reinitialize camera:", this.activeCamera); try { await this.initializeCameraWithSettings(this.activeCamera, this.activeCameraSettings); } catch (error) { Logger.log(Logger.Level.WARN, "Couldn't access camera:", this.activeCamera, error); this.triggerCameraAccessError(error); throw error; } } } async initializeCameraWithSettings(camera, cameraSettings) { if (this.cameraInitializationPromise != null) { await this.cameraInitializationPromise; } this.setSelectedCamera(camera); this.selectedCameraSettings = this.activeCameraSettings = cameraSettings; this.cameraInitializationPromise = this.initializeCameraAndCheckUpdatedSettings(camera); return this.cameraInitializationPromise; } async setTorchEnabled(enabled) { if (this.mediaStream != null && this.mediaTrackCapabilities?.torch === true) { this.torchEnabled = enabled; const videoTracks = this.mediaStream.getVideoTracks(); // istanbul ignore else if (videoTracks.length !== 0 && typeof videoTracks[0].applyConstraints === "function") { await videoTracks[0].applyConstraints({ advanced: [{ torch: enabled }] }); } } } async toggleTorch() { this.torchEnabled = !this.torchEnabled; await this.setTorchEnabled(this.torchEnabled); } async setZoom(zoomPercentage, currentZoom) { if (this.mediaStream != null && this.mediaTrackCapabilities?.zoom != null) { const videoTracks = this.mediaStream.getVideoTracks(); // istanbul ignore else if (videoTracks.length !== 0 && typeof videoTracks[0].applyConstraints === "function") { const zoomRange = this.mediaTrackCapabilities.zoom.max - this.mediaTrackCapabilities.zoom.min; const targetZoom = Math.max(this.mediaTrackCapabilities.zoom.min, Math.min((currentZoom ?? this.mediaTrackCapabilities.zoom.min) + zoomRange * zoomPercentage, this.mediaTrackCapabilities.zoom.max)); await videoTracks[0].applyConstraints({ advanced: [{ zoom: targetZoom }], }); } } } async recoverStreamIfNeeded() { // Due to non-standard behaviour, it could happen that the stream got interrupted while getting the list of // cameras, this isn't handled by the existing "ended" event listener as the active camera wasn't set until // before this, so manually reinitialize camera if needed const videoTracks = this.mediaStream?.getVideoTracks(); if (videoTracks?.[0]?.readyState === "ended") { await this.reinitializeCamera(); } } async setupCamerasAndStream() { try { let initialCamera; if (this.selectedCamera == null) { this.gui.setVideoVisible(false); initialCamera = await this.accessInitialCamera(); } const cameras = await CameraAccess.getCameras(false, true); if (this.cameraSwitcherEnabled && cameras.length > 1) { this.gui.setCameraSwitcherVisible(true); } // Get but don't save deviceId in initialCamera to differentiate it from final cameras const initialCameraDeviceId = this.mediaStream?.getVideoTracks()[0]?.getSettings?.().deviceId; if (this.mediaStream != null && initialCamera != null) { // We successfully accessed the initial camera const activeCamera = cameras.length === 1 ? cameras[0] : cameras.find((camera) => { return (camera.deviceId === initialCameraDeviceId || (camera.label !== "" && camera.label === initialCamera?.label)); }); if (activeCamera != null) { CameraAccess.adjustCameraFromMediaStream(this.mediaStream, activeCamera); if (BrowserHelper.isDesktopDevice()) { // When the device is a desktop/laptop, we store the initial camera as it should be considered the main one // for its camera type and the currently set camera type (which might be different) CameraAccess.mainCameraForTypeOverridesOnDesktop.set(this.cameraType, activeCamera); CameraAccess.mainCameraForTypeOverridesOnDesktop.set(activeCamera.cameraType, activeCamera); } if (cameras.length === 1 || CameraAccess.getMainCameraForType(cameras, this.cameraType) === activeCamera) { Logger.log(Logger.Level.DEBUG, "Initial camera access was correct (main camera), keep camera:", activeCamera); this.setSelectedCamera(activeCamera); this.updateActiveCameraCurrentResolution(activeCamera); await this.recoverStreamIfNeeded(); return; } else { Logger.log(Logger.Level.DEBUG, "Initial camera access was incorrect (not main camera), change camera", { ...initialCamera, deviceId: initialCameraDeviceId, }); } } else { Logger.log(Logger.Level.DEBUG, "Initial camera access was incorrect (unknown camera), change camera", { ...initialCamera, deviceId: initialCameraDeviceId, }); } } if (this.selectedCamera == null) { return await this.accessAutoselectedCamera(cameras); } else { return await this.initializeCameraWithSettings(this.selectedCamera, this.selectedCameraSettings); } } finally { this.gui.setVideoVisible(true); this.cameraSetupPromise = undefined; } } getInitialCameraResolutionConstraint() { let cameraResolutionConstraint; switch (this.activeCameraSettings?.resolutionPreference) { case CameraSettings.ResolutionPreference.ULTRA_HD: cameraResolutionConstraint = CameraResolutionConstraint.ULTRA_HD; break; case CameraSettings.ResolutionPreference.FULL_HD: cameraResolutionConstraint = CameraResolutionConstraint.FULL_HD; break; case CameraSettings.ResolutionPreference.HD: default: cameraResolutionConstraint = CameraResolutionConstraint.HD; break; } return cameraResolutionConstraint; } async accessAutoselectedCamera(cameras) { cameras = CameraAccess.sortCamerasForCameraType(cameras, this.cameraType); let autoselectedCamera = cameras.shift(); while (autoselectedCamera != null) { try { return await this.initializeCameraWithSettings(autoselectedCamera, this.selectedCameraSettings); } catch (error) { this.setSelectedCamera(); if (cameras.length === 1) { this.gui.setCameraSwitcherVisible(false); } if (cameras.length >= 1) { Logger.log(Logger.Level.WARN, "Couldn't access camera:", autoselectedCamera, error); autoselectedCamera = cameras.shift(); continue; } throw error; } } throw new CustomError(CameraManager.noCameraErrorParameters); } async accessInitialCamera() { // Note that the initial camera's information (except deviceId) will be updated after a successful access const initialCamera = { deviceId: "", label: "", cameraType: this.cameraType, }; try { await this.initializeCameraWithSettings(initialCamera, this.selectedCameraSettings); } catch { // Ignored } finally { this.setSelectedCamera(); } return initialCamera; } updateActiveCameraCurrentResolution(camera) { if (this.gui.videoElement.videoWidth > 2 && this.gui.videoElement.videoHeight > 2) { camera.currentResolution = { width: this.gui.videoElement.videoWidth, height: this.gui.videoElement.videoHeight, }; } // If it's the initial camera, do nothing: if and when the camera is later confirmed to be the // correct (main with wanted type or only) one this method will be called again with the right camera object if (camera.deviceId !== "") { this.activeCamera = camera; this.gui.setMirrorImageEnabled(this.gui.isMirrorImageEnabled(), false); } } postStreamInitialization() { window.clearTimeout(this.getCapabilitiesTimeout); this.getCapabilitiesTimeout = window.setTimeout(() => { this.storeStreamCapabilities(); this.setupAutofocus(); if (this.torchToggleEnabled && this.mediaStream != null && this.mediaTrackCapabilities?.torch === true) { this.gui.setTorchTogglerVisible(true); } }, CameraManager.getCapabilitiesTimeoutMs); } videoResizeHandle() { if (this.activeCamera != null) { this.updateActiveCameraCurrentResolution(this.activeCamera); } } checkCameraVideoStreamAccessIfVisible() { if (document.visibilityState === "visible") { Logger.log(Logger.Level.DEBUG, "Page is visible again, waiting for camera video stream start..."); document.removeEventListener("visibilitychange", this.checkCameraVideoStreamAccessIfVisibleListener); this.setCameraAccessTimeout(); } } async videoTrackEndedRecovery() { Logger.log(Logger.Level.DEBUG, 'Detected video track "ended" event, try to reinitialize camera'); if (document.visibilityState !== "visible") { Logger.log(Logger.Level.DEBUG, "Page is currently not visible, delay camera reinitialization until visible"); document.addEventListener("visibilitychange", this.checkCameraVideoStreamAccessIfVisibleListener); } else { try { await this.reinitializeCamera(); } catch { // Ignored } } } async videoTrackMuteRecovery(event) { if (this.gui.videoElement.onloadeddata != null) { Logger.log(Logger.Level.DEBUG, `Detected video track "${event.type}" event, delay camera video stream access detection`); this.setCameraAccessTimeout(); return; } const isMuteEvent = event.type === "mute"; if (isMuteEvent !== this.gui.isCameraRecoveryVisible()) { Logger.log(Logger.Level.DEBUG, `Detected video track "${event.type}" event, ${isMuteEvent ? "enable" : "disable"} camera recovery`); this.gui.setCameraRecoveryVisible(isMuteEvent); } } async triggerManualFocusForContinuous() { if (this.mediaStream == null) { return; } this.manualToAutofocusResumeTimeout = window.setTimeout(async () => { await this.triggerFocusMode(MeteringMode.CONTINUOUS); }, CameraManager.manualToAutofocusResumeTimeoutMs); let manualFocusResetNeeded = true; const videoTracks = this.mediaStream.getVideoTracks(); // istanbul ignore else if (videoTracks.length !== 0 && typeof videoTracks[0].getConstraints === "function") { manualFocusResetNeeded = videoTracks[0].getConstraints().advanced?.some((constraint) => { return constraint.focusMode === MeteringMode.MANUAL; }) === true; } if (manualFocusResetNeeded) { await this.triggerFocusMode(MeteringMode.CONTINUOUS); this.manualFocusWaitTimeout = window.setTimeout(async () => { await this.triggerFocusMode(MeteringMode.MANUAL); }, CameraManager.manualFocusWaitTimeoutMs); } else { await this.triggerFocusMode(MeteringMode.MANUAL); } } async triggerManualFocusForSingleShot() { window.clearInterval(this.autofocusInterval); this.manualToAutofocusResumeTimeout = window.setTimeout(() => { this.autofocusInterval = window.setInterval(this.triggerAutoFocus.bind(this), CameraManager.autofocusIntervalMs); }, CameraManager.manualToAutofocusResumeTimeoutMs); try { await this.triggerFocusMode(MeteringMode.SINGLE_SHOT); } catch { // istanbul ignore next } } async triggerManualFocus(event) { if (event != null) { event.preventDefault(); if (event.type === "touchend" && event.touches.length !== 0) { return; } // Check if we were using pinch-to-zoom if (this.pinchToZoomDistance != null) { this.pinchToZoomDistance = undefined; return; } } window.clearTimeout(this.manualFocusWaitTimeout); window.clearTimeout(this.manualToAutofocusResumeTimeout); if (this.mediaStream != null && this.mediaTrackCapabilities != null) { const focusModeCapability = this.mediaTrackCapabilities.focusMode; if (focusModeCapability instanceof Array) { if (focusModeCapability.includes(MeteringMode.CONTINUOUS) && focusModeCapability.includes(MeteringMode.MANUAL)) { await this.triggerManualFocusForContinuous(); } else if (focusModeCapability.includes(MeteringMode.SINGLE_SHOT)) { await this.triggerManualFocusForSingleShot(); } } } } triggerZoomStart(event) { if (event?.touches.length !== 2) { return; } event.preventDefault(); this.pinchToZoomDistance = Math.hypot((event.touches[1].screenX - event.touches[0].screenX) / screen.width, (event.touches[1].screenY - event.touches[0].screenY) / screen.height); if (this.mediaStream != null && this.mediaTrackCapabilities?.zoom != null) { const videoTracks = this.mediaStream.getVideoTracks(); // istanbul ignore else if (videoTracks.length !== 0 && typeof videoTracks[0].getConstraints === "function") { this.pinchToZoomInitialZoom = this.mediaTrackCapabilities.zoom.min; const currentConstraints = videoTracks[0].getConstraints(); if (currentConstraints.advanced != null) { const currentZoomConstraint = currentConstraints.advanced.find((constraint) => { return "zoom" in constraint; }); if (currentZoomConstraint?.zoom != null) { this.pinchToZoomInitialZoom = currentZoomConstraint.zoom; } } } } } async triggerZoomMove(event) { if (this.pinchToZoomDistance == null || event?.touches.length !== 2) { return; } event.preventDefault(); await this.setZoom((Math.hypot((event.touches[1].screenX - event.touches[0].screenX) / screen.width, (event.touches[1].screenY - event.touches[0].screenY) / screen.height) - this.pinchToZoomDistance) * 2, this.pinchToZoomInitialZoom); } storeStreamCapabilities() { // istanbul ignore else if (this.mediaStream != null) { const videoTracks = this.mediaStream.getVideoTracks(); // istanbul ignore else if (videoTracks.length !== 0 && typeof videoTracks[0].getCapabilities === "function") { this.mediaTrackCapabilities = videoTracks[0].getCapabilities(); } } if (this.activeCamera != null) { this.scanner.reportCameraProperties(this.activeCamera.cameraType, this.mediaTrackCapabilities?.focusMode == null || // assume the camera supports autofocus by default this.mediaTrackCapabilities.focusMode.includes(MeteringMode.CONTINUOUS)); } } setupAutofocus() { window.clearTimeout(this.manualFocusWaitTimeout); window.clearTimeout(this.manualToAutofocusResumeTimeout); // istanbul ignore else if (this.mediaStream != null && this.mediaTrackCapabilities != null) { const focusModeCapability = this.mediaTrackCapabilities.focusMode; if (focusModeCapability instanceof Array && !focusModeCapability.includes(MeteringMode.CONTINUOUS) && focusModeCapability.includes(MeteringMode.SINGLE_SHOT)) { window.clearInterval(this.autofocusInterval); this.autofocusInterval = window.setInterval(this.triggerAutoFocus.bind(this), CameraManager.autofocusIntervalMs); } } } async triggerAutoFocus() { await this.triggerFocusMode(MeteringMode.SINGLE_SHOT); } async triggerFocusMode(focusMode) { // istanbul ignore else if (this.mediaStream != null) { const videoTracks = this.mediaStream.getVideoTracks(); if (videoTracks.length !== 0 && typeof videoTracks[0].applyConstraints === "function") { try { await videoTracks[0].applyConstraints({ advanced: [{ focusMode }] }); } catch { // Ignored } } } } enableTapToFocusListeners() { ["touchend", "mousedown"].forEach((eventName) => { this.gui.videoElement.addEventListener(eventName, this.triggerManualFocusListener); }); } enablePinchToZoomListeners() { this.gui.videoElement.addEventListener("touchstart", this.triggerZoomStartListener); this.gui.videoElement.addEventListener("touchmove", this.triggerZoomMoveListener); } disableTapToFocusListeners() { ["touchend", "mousedown"].forEach((eventName) => { this.gui.videoElement.removeEventListener(eventName, this.triggerManualFocusListener); }); } disablePinchToZoomListeners() { this.gui.videoElement.removeEventListener("touchstart", this.triggerZoomStartListener); this.gui.videoElement.removeEventListener("touchmove", this.triggerZoomMoveListener); } async initializeCameraAndCheckUpdatedSettings(camera) { try { await this.initializeCamera(camera); // Check if due to asynchronous behaviour camera settings were changed while camera was initialized if (this.selectedCameraSettings !== this.activeCameraSettings && (this.selectedCameraSettings == null || this.activeCameraSettings == null || Object.keys(this.selectedCameraSettings).some((cameraSettingsProperty) => { return (this.selectedCameraSettings[cameraSettingsProperty] !== this.activeCameraSettings[cameraSettingsProperty]); }))) { this.activeCameraSettings = this.selectedCameraSettings; return await this.initializeCameraAndCheckUpdatedSettings(camera); } } finally { this.cameraInitializationPromise = undefined; } } async handleCameraInitializationError(camera, cameraResolutionConstraint, error) { if (!["OverconstrainedError", "NotReadableError"].includes(error.name) || (error.name === "NotReadableError" && cameraResolutionConstraint === CameraResolutionConstraint.NONE)) { // Camera is not accessible at all Logger.log(Logger.Level.DEBUG, "Camera video stream access failure (unrecoverable error)", camera, error); if (error.name !== "NotAllowedError") { CameraAccess.markCameraAsInaccessible(camera); } throw error; } if (error.name === "OverconstrainedError" && cameraResolutionConstraint === CameraResolutionConstraint.NONE) { // Camera device has changed deviceId // We can't rely on checking whether the constraint error property in the browsers reporting it is equal to // "deviceId" as it is used even when the error is due to a too high resolution being requested. // Whenever we get an OverconstrainedError or NotReadableError we keep trying until we are using no constraints // except for deviceId (cameraResolutionConstraint is NONE), if an error still happens we know said device doesn't // exist anymore (the device has changed deviceId). // If it's the initial camera, do nothing if (camera.deviceId === "") { Logger.log(Logger.Level.DEBUG, "Camera video stream access failure (no camera with such type error)", camera, error); throw error; } Logger.log(Logger.Level.DEBUG, "Detected non-existent deviceId error, attempt to find and reaccess updated camera", camera, error); const currentCameraDeviceId = camera.deviceId; // Refresh camera deviceId information await CameraAccess.getCameras(true); if (currentCameraDeviceId === camera.deviceId) { Logger.log(Logger.Level.DEBUG, "Camera video stream access failure (updated camera not found after non-existent deviceId error)", camera, error); CameraAccess.markCameraAsInaccessible(camera); throw error; } else { Logger.log(Logger.Level.DEBUG, "Updated camera found (recovered from non-existent deviceId error), attempt to access it", camera); return this.initializeCamera(camera); } } return this.initializeCamera(camera, cameraResolutionConstraint + 1); } async initializeCamera(camera, cameraResolutionConstraint) { this.gui.setCameraRecoveryVisible(false); if (camera == null) { throw new CustomError(CameraManager.noCameraErrorParameters); } await this.stopStream(); this.torchEnabled = false; this.gui.setTorchTogglerVisible(false); cameraResolutionConstraint ??= this.getInitialCameraResolutionConstraint(); try { const stream = await CameraAccess.accessCameraStream(cameraResolutionConstraint, camera); Logger.log(Logger.Level.DEBUG, "Camera accessed, waiting for camera video stream start..."); // Detect weird browser behaviour that on unsupported resolution returns a 2x2 video instead if (typeof stream.getTracks()[0].getSettings === "function") { const mediaTrackSettings = stream.getTracks()[0].getSettings(); if (mediaTrackSettings.width != null && mediaTrackSettings.height != null && (mediaTrackSettings.width === 2 || mediaTrackSettings.height === 2)) { Logger.log(Logger.Level.DEBUG, "Camera video stream access failure (invalid video metadata):", camera); if (cameraResolutionConstraint === CameraResolutionConstraint.NONE) { throw new CustomError(CameraManager.notReadableErrorParameters); } else { return this.initializeCamera(camera, cameraResolutionConstraint + 1); } } } this.mediaStream = stream; this.mediaStream.getVideoTracks().forEach((track) => { // Handle unexpected stream end events track.addEventListener("ended", this.videoTrackEndedListener); // If the track gets muted we need to give the chance to manually reinitialize the camera to access it again // (this is also tried automatically if and when the page is detected to be visible again and the track in a // muted state). // This will add the listeners only once in case of multiple calls: identical listeners are ignored. track.addEventListener("mute", this.videoTrackMuteListener); track.addEventListener("unmute", this.videoTrackMuteListener); }); try { await this.setupCameraStreamVideo(camera, stream); } catch (error) { if (cameraResolutionConstraint === CameraResolutionConstraint.NONE) { throw error; } else { return this.initializeCamera(camera, cameraResolutionConstraint + 1); } } } catch (error) { return this.handleCameraInitializationError(camera, cameraResolutionConstraint, error); } } setCameraAccessTimeout() { window.clearTimeout(this.cameraAccessTimeout); this.cameraAccessTimeout = window.setTimeout(async () => { if (document.visibilityState !== "visible") { Logger.log(Logger.Level.DEBUG, "Page is currently not visible, delay camera video stream access detection"); document.addEventListener("visibilitychange", this.checkCameraVideoStreamAccessIfVisibleListener); } else { await this.stopStream(true); this.cameraAccessRejectCallback?.(new CustomError(CameraManager.notReadableErrorParameters)); } }, CameraManager.cameraAccessTimeoutMs); } checkCameraAccess(camera) { return new Promise((_, reject) => { this.cameraAccessRejectCallback = (reason) => { Logger.log(Logger.Level.DEBUG, "Camera video stream access failure (video data load timeout):", camera); this.gui.setCameraRecoveryVisible(true); reject(reason); }; this.setCameraAccessTimeout(); }); } async checkVideoMetadata(camera) { return new Promise((resolve, reject) => { this.gui.videoElement.onloadeddata = () => { this.gui.videoElement.onloadeddata = null; window.clearTimeout(this.cameraAccessTimeout); // Detect weird browser behaviour that on unsupported resolution returns a 2x2 video instead // Also detect failed camera access with no error but also no video stream provided if (this.gui.videoElement.videoWidth > 2 && this.gui.videoElement.videoHeight > 2 && this.gui.videoElement.currentTime > 0) { this.updateActiveCameraCurrentResolution(camera); Logger.log(Logger.Level.DEBUG, "Camera video stream access success:", camera); return resolve(); } const videoMetadataCheckStartTime = performance.now(); window.clearInterval(this.videoMetadataCheckInterval); this.videoMetadataCheckInterval = window.setInterval(async () => { // Detect weird browser behaviour that on unsupported resolution returns a 2x2 video instead // Also detect failed camera access with no error but also no video stream provided if (this.gui.videoElement.videoWidth <= 2 || this.gui.videoElement.videoHeight <= 2 || this.gui.videoElement.currentTime === 0) { if (performance.now() - videoMetadataCheckStartTime > CameraManager.videoMetadataCheckTimeoutMs) { Logger.log(Logger.Level.DEBUG, "Camera video stream access failure (valid video metadata timeout):", camera); window.clearInterval(this.videoMetadataCheckInterval); await this.stopStream(true); return reject(new CustomError(CameraManager.notReadableErrorParameters)); } return; } window.clearInterval(this.videoMetadataCheckInterval); this.updateActiveCameraCurrentResolution(camera); Logger.log(Logger.Level.DEBUG, "Camera video stream access success:", camera); resolve(); }, CameraManager.videoMetadataCheckIntervalMs); }; }); } setupCameraStreamVideo(camera, stream) { // These will add the listeners only once in the case of multiple calls, identical listeners are ignored this.gui.videoElement.addEventListener("loadedmetadata", this.postStreamInitializationListener); this.gui.videoElement.addEventListener("resize", this.videoResizeListener); if (this.tapToFocusEnabled) { this.enableTapToFocusListeners(); } if (this.pinchToZoomEnabled) { this.enablePinchToZoomListeners(); } const cameraStreamVideoCheck = Promise.race([ this.checkCameraAccess(camera), this.checkVideoMetadata(camera), // tslint:disable-next-line: promise-must-complete new Promise((resolve) => { this.abortedCameraInitializationResolveCallback = resolve; }), ]); this.gui.videoElement.srcObject = stream; this.gui.videoElement.load(); this.gui.playVideo(); // Report camera properties already now in order to have type information before autofocus information is available. // Even if later the initialization could fail nothing bad results from this. this.scanner.reportCameraProperties(camera.cameraType); return cameraStreamVideoCheck; } } //# sourceMappingURL=cameraManager.js.map