From 2594cd475f0a7b82258a53cb015b75924dcf39c5 Mon Sep 17 00:00:00 2001 From: Jeppe Bundgaard Date: Mon, 15 Jun 2026 11:06:19 +0200 Subject: [PATCH] Keep POS scanner camera preview live --- .../steps/mobile/PosDepartmentStepMobile1.vue | 177 ++++------ .../scanner/graphics/ScannerCamera.vue | 318 +++++++++--------- .../page/templates/scanner/lprFrameCapture.ts | 1 - tests/unit/pos-mobile-camera-lpr.spec.js | 24 +- .../scanner-camera-capture-enabled.spec.js | 13 +- 5 files changed, 248 insertions(+), 285 deletions(-) diff --git a/src/components/displays/department/pos/steps/mobile/PosDepartmentStepMobile1.vue b/src/components/displays/department/pos/steps/mobile/PosDepartmentStepMobile1.vue index d6d68c55..dabdf73f 100644 --- a/src/components/displays/department/pos/steps/mobile/PosDepartmentStepMobile1.vue +++ b/src/components/displays/department/pos/steps/mobile/PosDepartmentStepMobile1.vue @@ -93,9 +93,7 @@ const LPR_ENDPOINT = "/modules/scanner/lpr"; let consecutiveNoPlateResponses = 0; const nowMs = (): number => - typeof performance !== "undefined" && typeof performance.now === "function" - ? performance.now() - : Date.now(); + typeof performance !== "undefined" && typeof performance.now === "function" ? performance.now() : Date.now(); const activeVehicleIndexNext = () => { // Increment the active vehicle index, wrapping around if necessary @@ -129,8 +127,7 @@ type LPRFrameInput = string | LPRFramePayload; const isLPRFramePayload = (image: LPRFrameInput): image is LPRFramePayload => typeof image === "object" && image !== null && image.blob instanceof Blob; -const getLPRFrameFingerprint = (image: LPRFrameInput): string => - isLPRFramePayload(image) ? image.fingerprint : image; +const getLPRFrameFingerprint = (image: LPRFrameInput): string => (isLPRFramePayload(image) ? image.fingerprint : image); const getLPRFrameContentFingerprint = (image: LPRFrameInput): (() => Promise) | null => isLPRFramePayload(image) ? image.getContentFingerprint ?? null : null; @@ -180,19 +177,22 @@ const resolveParsedFrameContentFingerprint = (entry: ParsedFrameFingerprint): Pr return null; } - entry.contentFingerprintPromise ??= entry.getContentFingerprint().then((content) => { - if (lastParsedImage.value === entry) { - entry.content = content; - } + entry.contentFingerprintPromise ??= entry + .getContentFingerprint() + .then((content) => { + if (lastParsedImage.value === entry) { + entry.content = content; + } - return content; - }).catch((error) => { - if (lastParsedImage.value === entry) { - entry.contentFingerprintPromise = null; - } + return content; + }) + .catch((error) => { + if (lastParsedImage.value === entry) { + entry.contentFingerprintPromise = null; + } - throw error; - }); + throw error; + }); return entry.contentFingerprintPromise; }; @@ -200,18 +200,17 @@ const resolveParsedFrameContentFingerprint = (entry: ParsedFrameFingerprint): Pr const isVisuallySimilarToLastMiss = (visualFingerprint: string | null | undefined): boolean => { const lastParsed = lastParsedImage.value; - return lastParsed !== null - && lastParsed.outcome === "miss" - && getVisualFingerprintDistance(lastParsed.visual, visualFingerprint) <= LPR_VISUAL_DUPLICATE_DISTANCE_THRESHOLD; + return ( + lastParsed !== null && + lastParsed.outcome === "miss" && + getVisualFingerprintDistance(lastParsed.visual, visualFingerprint) <= LPR_VISUAL_DUPLICATE_DISTANCE_THRESHOLD + ); }; const hasLastMissVisualFingerprint = (): boolean => - lastParsedImage.value !== null - && lastParsedImage.value.outcome === "miss" - && lastParsedImage.value.visual !== null; + lastParsedImage.value !== null && lastParsedImage.value.outcome === "miss" && lastParsedImage.value.visual !== null; -const shouldBuildLPRVisualFingerprint = (): boolean => - hasLastMissVisualFingerprint(); +const shouldBuildLPRVisualFingerprint = (): boolean => hasLastMissVisualFingerprint(); const shouldSkipDuplicateFrame = async (image: LPRFrameInput): Promise => { const quick = getLPRFrameFingerprint(image); @@ -236,10 +235,7 @@ const shouldSkipDuplicateFrame = async (image: LPRFrameInput): Promise } try { - const [lastContent, currentContent] = await Promise.all([ - lastContentFingerprint, - currentContentFingerprint(), - ]); + const [lastContent, currentContent] = await Promise.all([lastContentFingerprint, currentContentFingerprint()]); return lastContent === currentContent; } catch { @@ -247,11 +243,7 @@ const shouldSkipDuplicateFrame = async (image: LPRFrameInput): Promise } }; -const appendFiniteTimingParam = ( - queryParts: string[], - field: string, - value: number | null | undefined -) => { +const appendFiniteTimingParam = (queryParts: string[], field: string, value: number | null | undefined) => { if (value === null || value === undefined) { return; } @@ -267,11 +259,7 @@ const appendFiniteTimingParam = ( } }; -const appendPositiveIntegerParam = ( - queryParts: string[], - field: string, - value: number | null | undefined -) => { +const appendPositiveIntegerParam = (queryParts: string[], field: string, value: number | null | undefined) => { const numericValue = Number(value); if (!Number.isFinite(numericValue) || numericValue <= 0) { return; @@ -333,9 +321,9 @@ const shouldEncodeLPRFrame = (candidate: LPRFrameEncodeCandidate): boolean => { } if ( - shouldSkipLPRForCurrentState({ ignoreNoPlateBackoff: true }) - || isLPRFrameProcessing.value - || isLPRRequestInFlight.value + shouldSkipLPRForCurrentState({ ignoreNoPlateBackoff: true }) || + isLPRFrameProcessing.value || + isLPRRequestInFlight.value ) { return false; } @@ -369,19 +357,18 @@ const rememberLatestCameraImage = (image: LPRFrameInput) => { const hasRegistrationNumber = (registrationNumber: string | null | undefined): boolean => String(registrationNumber ?? "").trim().length > 0; -const isActiveRegistrationSlotFilled = (): boolean => - hasRegistrationNumber(vehicles.getActiveVehicle()?.reg); +const isActiveRegistrationSlotFilled = (): boolean => hasRegistrationNumber(vehicles.getActiveVehicle()?.reg); const areAllRegistrationSlotsFilled = (): boolean => [1, 2, 3].every((vehicleIndex) => hasRegistrationNumber(vehicles.get(vehicleIndex)?.reg)); const shouldSkipLPRForCurrentState = (options: LPRCurrentStateSkipOptions = {}): boolean => - views.attachmentView.value - || isActiveRegistrationSlotFilled() - || areAllRegistrationSlotsFilled() - || (!options.ignoreNoPlateBackoff && isNoPlateBackoffActive.value) - || isDuplicateFrameBackoffActive.value - || isSuccessCooldownActive.value; + views.attachmentView.value || + isActiveRegistrationSlotFilled() || + areAllRegistrationSlotsFilled() || + (!options.ignoreNoPlateBackoff && isNoPlateBackoffActive.value) || + isDuplicateFrameBackoffActive.value || + isSuccessCooldownActive.value; const getScannerFocusViewportRect = (): LPRFrameViewportRect | null => { if (views.attachmentView.value || scannerFocusRef.value === null) { @@ -389,12 +376,7 @@ const getScannerFocusViewportRect = (): LPRFrameViewportRect | null => { } const rect = scannerFocusRef.value.getBoundingClientRect(); - if ( - !Number.isFinite(rect.width) || - !Number.isFinite(rect.height) || - rect.width <= 0 || - rect.height <= 0 - ) { + if (!Number.isFinite(rect.width) || !Number.isFinite(rect.height) || rect.width <= 0 || rect.height <= 0) { return null; } @@ -411,28 +393,17 @@ const isCameraFrameCaptureEnabled = computed(() => { return true; } - return !isLPRFrameProcessing.value - && !isLPRRequestInFlight.value - && (!isNoPlateBackoffActive.value || hasLastMissVisualFingerprint()) - && !isDuplicateFrameBackoffActive.value - && !isSuccessCooldownActive.value - && !isActiveRegistrationSlotFilled() - && !areAllRegistrationSlotsFilled(); + return ( + !isLPRFrameProcessing.value && + !isLPRRequestInFlight.value && + (!isNoPlateBackoffActive.value || hasLastMissVisualFingerprint()) && + !isDuplicateFrameBackoffActive.value && + !isSuccessCooldownActive.value && + !isActiveRegistrationSlotFilled() && + !areAllRegistrationSlotsFilled() + ); }); -const shouldPauseScannerPreview = computed(() => - !views.attachmentView.value - && ( - isLPRFrameProcessing.value - || isLPRRequestInFlight.value - || isDuplicateFrameBackoffActive.value - || isSuccessCooldownActive.value - || isActiveRegistrationSlotFilled() - || areAllRegistrationSlotsFilled() - || (isNoPlateBackoffActive.value && !hasLastMissVisualFingerprint()) - ) -); - const lprCameraCaptureIntervalMs = computed(() => !views.attachmentView.value && isNoPlateBackoffActive.value && hasLastMissVisualFingerprint() ? LPR_VISUAL_DUPLICATE_RECHECK_DELAY_MS @@ -444,8 +415,7 @@ const abortLPRRequest = () => { lprRequestAbortController = null; }; -const isDocumentHidden = (): boolean => - typeof document !== "undefined" && document.visibilityState === "hidden"; +const isDocumentHidden = (): boolean => typeof document !== "undefined" && document.visibilityState === "hidden"; const handleDocumentVisibilityChange = () => { if (!isDocumentHidden()) { @@ -490,9 +460,8 @@ const resetNoPlateBackoff = () => { const scheduleNoPlateBackoff = () => { consecutiveNoPlateResponses += 1; - const delay = NO_PLATE_BACKOFF_DELAYS_MS[ - Math.min(consecutiveNoPlateResponses - 1, NO_PLATE_BACKOFF_DELAYS_MS.length - 1) - ]; + const delay = + NO_PLATE_BACKOFF_DELAYS_MS[Math.min(consecutiveNoPlateResponses - 1, NO_PLATE_BACKOFF_DELAYS_MS.length - 1)]; clearNoPlateBackoff(); isNoPlateBackoffActive.value = true; @@ -525,16 +494,20 @@ const isAbortError = (error: unknown): boolean => { return true; } - return typeof error === "object" && error !== null && (error as { name?: string; code?: string }).code === "ERR_CANCELED"; + return ( + typeof error === "object" && error !== null && (error as { name?: string; code?: string }).code === "ERR_CANCELED" + ); }; const isSameLPRScanContext = (first: LPRScanContext, second: LPRScanContext): boolean => - first.activeVehicleIndex === second.activeVehicleIndex - && first.attachmentView === second.attachmentView - && first.manualInput === second.manualInput - && first.transactionHistoryView === second.transactionHistoryView - && first.registrationNumbers.length === second.registrationNumbers.length - && first.registrationNumbers.every((registrationNumber, index) => registrationNumber === second.registrationNumbers[index]); + first.activeVehicleIndex === second.activeVehicleIndex && + first.attachmentView === second.attachmentView && + first.manualInput === second.manualInput && + first.transactionHistoryView === second.transactionHistoryView && + first.registrationNumbers.length === second.registrationNumbers.length && + first.registrationNumbers.every( + (registrationNumber, index) => registrationNumber === second.registrationNumbers[index] + ); const parseImage = async (image: LPRFrameInput) => { if (views.attachmentView.value) { @@ -574,18 +547,11 @@ const parseImage = async (image: LPRFrameInput) => { const lprRequest = buildLPRRequestPayload(image, clientPreflightDurationMs); try { - const response = await SessionUser.request( - lprRequest.url, - "POST", - lprRequest.payload, - null, - null, - { - ...(lprRequest.headers ? { headers: lprRequest.headers } : {}), - signal: abortController.signal, - transport: "fetch", - } - ); + const response = await SessionUser.request(lprRequest.url, "POST", lprRequest.payload, null, null, { + ...(lprRequest.headers ? { headers: lprRequest.headers } : {}), + signal: abortController.signal, + transport: "fetch", + }); if (debug_mode.value) { debug_request_results.value.push(response); @@ -770,7 +736,7 @@ watch( :capture-interval-ms="lprCameraCaptureIntervalMs" :capture-mode="views.attachmentView.value ? 'preview' : 'lpr'" :get-focus-viewport-rect="getScannerFocusViewportRect" - :pause-preview="shouldPauseScannerPreview" + :pause-preview="false" :should-build-visual-fingerprint="shouldBuildLPRVisualFingerprint" :should-encode-frame="shouldEncodeLPRFrame" @update:frame="parseImage" @@ -800,11 +766,7 @@ watch( />
-
+
@@ -820,10 +782,7 @@ watch( - +
diff --git a/src/components/viewport/page/templates/scanner/graphics/ScannerCamera.vue b/src/components/viewport/page/templates/scanner/graphics/ScannerCamera.vue index 0f354702..4c89b7bb 100644 --- a/src/components/viewport/page/templates/scanner/graphics/ScannerCamera.vue +++ b/src/components/viewport/page/templates/scanner/graphics/ScannerCamera.vue @@ -1,33 +1,35 @@