Merge pull request #134 from copenhagentruckwash/fix/pos-scanner-live-preview-fps
Keep POS scanner camera preview live
This commit is contained in:
@@ -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<string>) | 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<boolean> => {
|
||||
const quick = getLPRFrameFingerprint(image);
|
||||
@@ -236,10 +235,7 @@ const shouldSkipDuplicateFrame = async (image: LPRFrameInput): Promise<boolean>
|
||||
}
|
||||
|
||||
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<boolean>
|
||||
}
|
||||
};
|
||||
|
||||
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(
|
||||
/>
|
||||
<!-- Scanner outline object -->
|
||||
<div class="is-align-content-center is-flex is-justify-content-center">
|
||||
<div
|
||||
v-if="!views.attachmentView.value"
|
||||
ref="scannerFocusRef"
|
||||
class="scanner-focus-target"
|
||||
>
|
||||
<div v-if="!views.attachmentView.value" ref="scannerFocusRef" class="scanner-focus-target">
|
||||
<ScannerOutline :loading="false" />
|
||||
</div>
|
||||
</div>
|
||||
@@ -820,10 +782,7 @@ watch(
|
||||
<!-- Location -->
|
||||
<PosDepartmentStepMobile1Location />
|
||||
<!-- Buttons -->
|
||||
<PosDepartmentStepMobileFixedBottomControl
|
||||
variant="pos-step"
|
||||
:use-backdrop-blur="views.attachmentView.value"
|
||||
>
|
||||
<PosDepartmentStepMobileFixedBottomControl variant="pos-step" :use-backdrop-blur="views.attachmentView.value">
|
||||
<!-- Attachments -->
|
||||
<div class="is-flex is-justify-content-center">
|
||||
<PosDepartmentStepMobileAttachments :showDefaultControls="false" v-show="views.attachmentView.value" />
|
||||
|
||||
@@ -1,33 +1,35 @@
|
||||
<script setup lang="ts">
|
||||
import { nextTick, ref, onMounted, onUnmounted, watch } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { nextTick, ref, onMounted, onUnmounted, watch } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import {
|
||||
captureVideoFrameBlobForLPR,
|
||||
isVideoFrameReadyForLPR,
|
||||
LPR_CAMERA_FRAME_RATE,
|
||||
LPR_CAMERA_VIDEO_HEIGHT,
|
||||
LPR_CAMERA_VIDEO_WIDTH,
|
||||
type LPRFrameEncodeCandidate,
|
||||
type LPRFrameViewportRect,
|
||||
} from "@/components/viewport/page/templates/scanner/lprFrameCapture";
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
captureEnabled?: boolean;
|
||||
captureIntervalMs?: number | null;
|
||||
captureMode?: 'lpr' | 'preview';
|
||||
getFocusViewportRect?: () => LPRFrameViewportRect | null;
|
||||
pausePreview?: boolean;
|
||||
shouldBuildVisualFingerprint?: () => boolean;
|
||||
shouldEncodeFrame?: (_candidate: LPRFrameEncodeCandidate) => boolean | Promise<boolean>;
|
||||
}>(), {
|
||||
captureEnabled: true,
|
||||
captureIntervalMs: null,
|
||||
captureMode: 'lpr',
|
||||
getFocusViewportRect: undefined,
|
||||
pausePreview: false,
|
||||
shouldBuildVisualFingerprint: undefined,
|
||||
shouldEncodeFrame: undefined,
|
||||
});
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
captureEnabled?: boolean;
|
||||
captureIntervalMs?: number | null;
|
||||
captureMode?: "lpr" | "preview";
|
||||
getFocusViewportRect?: () => LPRFrameViewportRect | null;
|
||||
pausePreview?: boolean;
|
||||
shouldBuildVisualFingerprint?: () => boolean;
|
||||
shouldEncodeFrame?: (_candidate: LPRFrameEncodeCandidate) => boolean | Promise<boolean>;
|
||||
}>(),
|
||||
{
|
||||
captureEnabled: true,
|
||||
captureIntervalMs: null,
|
||||
captureMode: "lpr",
|
||||
getFocusViewportRect: undefined,
|
||||
pausePreview: false,
|
||||
shouldBuildVisualFingerprint: undefined,
|
||||
shouldEncodeFrame: undefined,
|
||||
}
|
||||
);
|
||||
type GetUserMediaConstraints = Parameters<typeof navigator.mediaDevices.getUserMedia>[0];
|
||||
type CameraConstraintCaps = {
|
||||
capFrameRate: boolean;
|
||||
@@ -35,14 +37,15 @@ type CameraConstraintCaps = {
|
||||
};
|
||||
const LPR_VIDEO_NOT_READY_RETRY_MS = 100;
|
||||
const LPR_PREVIEW_RESUME_CAPTURE_DELAY_MS = LPR_VIDEO_NOT_READY_RETRY_MS;
|
||||
const emits = defineEmits(['camera-toggled', 'scanner-toggled', 'update:frame']);
|
||||
const LPR_CAMERA_PREVIEW_FRAME_RATE = 30;
|
||||
const emits = defineEmits(["camera-toggled", "scanner-toggled", "update:frame"]);
|
||||
const { t } = useI18n();
|
||||
const videoRef = ref<HTMLVideoElement | null>(null);
|
||||
const canvasRef = ref<HTMLCanvasElement | null>(null);
|
||||
const visualFingerprintCanvasRef = ref<HTMLCanvasElement | null>(null);
|
||||
const cameraStream = ref<MediaStream | null>(null);
|
||||
const isCameraActive = ref(false);
|
||||
const cameraErrorKey = ref('pos.camera_permission_denied');
|
||||
const cameraErrorKey = ref("pos.camera_permission_denied");
|
||||
let captureIntervalId: ReturnType<typeof window.setInterval> | null = null;
|
||||
let firstCaptureTimeoutId: ReturnType<typeof window.setTimeout> | null = null;
|
||||
let isFrameCaptureInProgress = false;
|
||||
@@ -52,13 +55,14 @@ let cachedRelativeFocusViewportRect: LPRFrameViewportRect | null = null;
|
||||
let hasCachedRelativeFocusViewportRect = false;
|
||||
let cachedVideoViewportRect: DOMRect | null = null;
|
||||
let videoResizeObserver: ResizeObserver | null = null;
|
||||
import { isCameraMounted, camera } from '@/components/displays/department/pos/steps/mobile/objects/PosDepartmentStepMobileFlow.vue';
|
||||
import {
|
||||
isCameraMounted,
|
||||
camera,
|
||||
} from "@/components/displays/department/pos/steps/mobile/objects/PosDepartmentStepMobileFlow.vue";
|
||||
|
||||
const isDocumentVisible = (): boolean =>
|
||||
typeof document === 'undefined' || document.visibilityState !== 'hidden';
|
||||
const isDocumentVisible = (): boolean => typeof document === "undefined" || document.visibilityState !== "hidden";
|
||||
|
||||
const shouldRunLivePreview = (): boolean =>
|
||||
!props.pausePreview && isDocumentVisible();
|
||||
const shouldRunLivePreview = (): boolean => !props.pausePreview && isDocumentVisible();
|
||||
|
||||
const canCaptureFrames = (): boolean =>
|
||||
isCameraActive.value && props.captureEnabled && !props.pausePreview && isDocumentVisible();
|
||||
@@ -68,44 +72,42 @@ const getCameraErrorName = (err: unknown): string => {
|
||||
return err.name;
|
||||
}
|
||||
|
||||
return typeof err === 'object' && err !== null
|
||||
? String((err as { name?: unknown }).name ?? '')
|
||||
: '';
|
||||
return typeof err === "object" && err !== null ? String((err as { name?: unknown }).name ?? "") : "";
|
||||
};
|
||||
|
||||
const getCameraErrorKey = (err: unknown) => {
|
||||
const errorName = getCameraErrorName(err);
|
||||
if (errorName === 'NotFoundError' || errorName === 'DevicesNotFoundError') {
|
||||
return 'pos.no_camera_found';
|
||||
if (errorName === "NotFoundError" || errorName === "DevicesNotFoundError") {
|
||||
return "pos.no_camera_found";
|
||||
}
|
||||
|
||||
return 'pos.camera_permission_denied';
|
||||
return "pos.camera_permission_denied";
|
||||
};
|
||||
|
||||
const shouldRetryWithRelaxedCameraConstraints = (err: unknown): boolean => {
|
||||
const errorName = getCameraErrorName(err);
|
||||
|
||||
return errorName === 'OverconstrainedError' || errorName === 'ConstraintNotSatisfiedError';
|
||||
return errorName === "OverconstrainedError" || errorName === "ConstraintNotSatisfiedError";
|
||||
};
|
||||
|
||||
const getRejectedCameraConstraintName = (err: unknown): string => {
|
||||
if (typeof err !== 'object' || err === null) {
|
||||
return '';
|
||||
if (typeof err !== "object" || err === null) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return String((err as { constraint?: unknown }).constraint ?? '').toLowerCase();
|
||||
return String((err as { constraint?: unknown }).constraint ?? "").toLowerCase();
|
||||
};
|
||||
|
||||
const getCameraConstraintFallbacks = (err: unknown): CameraConstraintCaps[] => {
|
||||
const rejectedConstraint = getRejectedCameraConstraintName(err);
|
||||
if (rejectedConstraint === 'framerate') {
|
||||
if (rejectedConstraint === "framerate") {
|
||||
return [
|
||||
{ capFrameRate: false, capResolution: true },
|
||||
{ capFrameRate: false, capResolution: false },
|
||||
];
|
||||
}
|
||||
|
||||
if (['width', 'height', 'aspectratio', 'resizemode'].includes(rejectedConstraint)) {
|
||||
if (["width", "height", "aspectratio", "resizemode"].includes(rejectedConstraint)) {
|
||||
return [
|
||||
{ capFrameRate: true, capResolution: false },
|
||||
{ capFrameRate: false, capResolution: false },
|
||||
@@ -119,38 +121,38 @@ const getCameraConstraintFallbacks = (err: unknown): CameraConstraintCaps[] => {
|
||||
];
|
||||
};
|
||||
|
||||
const getCameraConstraints = ({
|
||||
capFrameRate,
|
||||
capResolution,
|
||||
}: CameraConstraintCaps = { capFrameRate: true, capResolution: true }) => ({
|
||||
video: {
|
||||
facingMode: 'environment',
|
||||
zoom: camera.getZoom(),
|
||||
width: capResolution
|
||||
? { ideal: LPR_CAMERA_VIDEO_WIDTH, max: LPR_CAMERA_VIDEO_WIDTH }
|
||||
: { ideal: LPR_CAMERA_VIDEO_WIDTH },
|
||||
height: capResolution
|
||||
? { ideal: LPR_CAMERA_VIDEO_HEIGHT, max: LPR_CAMERA_VIDEO_HEIGHT }
|
||||
: { ideal: LPR_CAMERA_VIDEO_HEIGHT },
|
||||
frameRate: capFrameRate
|
||||
? { ideal: LPR_CAMERA_FRAME_RATE, max: LPR_CAMERA_FRAME_RATE }
|
||||
: { ideal: LPR_CAMERA_FRAME_RATE },
|
||||
const getCameraConstraints = (
|
||||
{ capFrameRate, capResolution }: CameraConstraintCaps = { capFrameRate: true, capResolution: true }
|
||||
) =>
|
||||
({
|
||||
video: {
|
||||
facingMode: "environment",
|
||||
zoom: camera.getZoom(),
|
||||
width: capResolution
|
||||
? { ideal: LPR_CAMERA_VIDEO_WIDTH, max: LPR_CAMERA_VIDEO_WIDTH }
|
||||
: { ideal: LPR_CAMERA_VIDEO_WIDTH },
|
||||
height: capResolution
|
||||
? { ideal: LPR_CAMERA_VIDEO_HEIGHT, max: LPR_CAMERA_VIDEO_HEIGHT }
|
||||
: { ideal: LPR_CAMERA_VIDEO_HEIGHT },
|
||||
frameRate: capFrameRate
|
||||
? { ideal: LPR_CAMERA_PREVIEW_FRAME_RATE, max: LPR_CAMERA_PREVIEW_FRAME_RATE }
|
||||
: { ideal: LPR_CAMERA_PREVIEW_FRAME_RATE },
|
||||
|
||||
// New spec
|
||||
advanced: [
|
||||
{ focusMode: 'continuous' },
|
||||
{ torch: false } // Set to true to enable flashlight if supported
|
||||
],
|
||||
// Old spec
|
||||
//focusMode: 'continuous',
|
||||
// Zoom in on the environment camera if available
|
||||
//facingMode: 'environment',
|
||||
//width: { ideal: 1920 },
|
||||
//height: { ideal: 1080 },
|
||||
//aspectRatio: { ideal: 16/9 },
|
||||
//frameRate: { ideal: 30 }
|
||||
}
|
||||
} as unknown as GetUserMediaConstraints);
|
||||
// New spec
|
||||
advanced: [
|
||||
{ focusMode: "continuous" },
|
||||
{ torch: false }, // Set to true to enable flashlight if supported
|
||||
],
|
||||
// Old spec
|
||||
//focusMode: 'continuous',
|
||||
// Zoom in on the environment camera if available
|
||||
//facingMode: 'environment',
|
||||
//width: { ideal: 1920 },
|
||||
//height: { ideal: 1080 },
|
||||
//aspectRatio: { ideal: 16/9 },
|
||||
//frameRate: { ideal: 30 }
|
||||
},
|
||||
} as unknown as GetUserMediaConstraints);
|
||||
|
||||
const requestCameraStream = async (): Promise<MediaStream> => {
|
||||
try {
|
||||
@@ -183,7 +185,7 @@ const playVideoPreview = (video: HTMLVideoElement) => {
|
||||
|
||||
hasRequestedVideoPreviewPlay = true;
|
||||
const playResult = video.play();
|
||||
if (playResult && typeof playResult.catch === 'function') {
|
||||
if (playResult && typeof playResult.catch === "function") {
|
||||
void playResult.catch(() => {
|
||||
hasRequestedVideoPreviewPlay = false;
|
||||
});
|
||||
@@ -204,11 +206,11 @@ const getCameraVideoTracks = (): MediaStreamTrack[] => {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (typeof cameraStream.value.getVideoTracks === 'function') {
|
||||
if (typeof cameraStream.value.getVideoTracks === "function") {
|
||||
return cameraStream.value.getVideoTracks();
|
||||
}
|
||||
|
||||
return cameraStream.value.getTracks().filter(track => track.kind === 'video');
|
||||
return cameraStream.value.getTracks().filter((track) => track.kind === "video");
|
||||
};
|
||||
|
||||
const syncCameraVideoTracksEnabled = () => {
|
||||
@@ -252,7 +254,7 @@ const applyCameraStream = (stream: MediaStream) => {
|
||||
lastAppliedTrackEnabled = null;
|
||||
if (videoRef.value) {
|
||||
videoRef.value.srcObject = stream;
|
||||
videoRef.value.setAttribute('playsinline', '');
|
||||
videoRef.value.setAttribute("playsinline", "");
|
||||
syncVideoPreviewPlayback();
|
||||
}
|
||||
startCaptureTimers();
|
||||
@@ -264,12 +266,12 @@ function startCamera() {
|
||||
}
|
||||
|
||||
requestCameraStream()
|
||||
.then(applyCameraStream)
|
||||
.catch((err) => {
|
||||
isCameraActive.value = false;
|
||||
cameraErrorKey.value = getCameraErrorKey(err);
|
||||
console.error('Camera access error:', err);
|
||||
});
|
||||
.then(applyCameraStream)
|
||||
.catch((err) => {
|
||||
isCameraActive.value = false;
|
||||
cameraErrorKey.value = getCameraErrorKey(err);
|
||||
console.error("Camera access error:", err);
|
||||
});
|
||||
}
|
||||
|
||||
function clearCaptureInterval() {
|
||||
@@ -300,12 +302,12 @@ function captureFrameIfReady(): Promise<void> {
|
||||
|
||||
clearFirstCaptureTimeout();
|
||||
return getFrame()
|
||||
.then(() => undefined)
|
||||
.finally(() => {
|
||||
if (canCaptureFrames()) {
|
||||
startCaptureInterval();
|
||||
}
|
||||
});
|
||||
.then(() => undefined)
|
||||
.finally(() => {
|
||||
if (canCaptureFrames()) {
|
||||
startCaptureInterval();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return Promise.resolve();
|
||||
@@ -384,7 +386,7 @@ const observeVideoGeometry = () => {
|
||||
videoResizeObserver?.disconnect();
|
||||
videoResizeObserver = null;
|
||||
|
||||
if (typeof ResizeObserver === 'undefined' || !videoRef.value) {
|
||||
if (typeof ResizeObserver === "undefined" || !videoRef.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -399,7 +401,7 @@ function stopCamera() {
|
||||
hasRequestedVideoPreviewPlay = false;
|
||||
lastAppliedTrackEnabled = null;
|
||||
if (cameraStream.value) {
|
||||
cameraStream.value.getTracks().forEach(track => track.stop());
|
||||
cameraStream.value.getTracks().forEach((track) => track.stop());
|
||||
cameraStream.value = null;
|
||||
}
|
||||
isCameraActive.value = false;
|
||||
@@ -436,13 +438,10 @@ function handleVisibilityChange() {
|
||||
syncVideoPreviewPlayback();
|
||||
}
|
||||
|
||||
const shouldUseFocusedLPRCrop = () => props.captureMode === 'lpr';
|
||||
const shouldUseFocusedLPRCrop = () => props.captureMode === "lpr";
|
||||
|
||||
const hasUsableRectSize = (rect: { height: number; width: number }): boolean =>
|
||||
Number.isFinite(rect.width)
|
||||
&& Number.isFinite(rect.height)
|
||||
&& rect.width > 0
|
||||
&& rect.height > 0;
|
||||
Number.isFinite(rect.width) && Number.isFinite(rect.height) && rect.width > 0 && rect.height > 0;
|
||||
|
||||
const getVideoViewportRect = (): DOMRect | null => {
|
||||
if (!videoRef.value) {
|
||||
@@ -502,9 +501,9 @@ const getFrameCaptureOptions = () => {
|
||||
shouldEncode: shouldUseFocusedCrop ? props.shouldEncodeFrame : undefined,
|
||||
...(videoViewportRect
|
||||
? {
|
||||
viewportHeight: videoViewportRect.height,
|
||||
viewportWidth: videoViewportRect.width,
|
||||
}
|
||||
viewportHeight: videoViewportRect.height,
|
||||
viewportWidth: videoViewportRect.width,
|
||||
}
|
||||
: {}),
|
||||
visualFingerprintCanvas: shouldUseFocusedCrop ? visualFingerprintCanvasRef.value : null,
|
||||
};
|
||||
@@ -521,72 +520,84 @@ const getFrame = () => {
|
||||
isFrameCaptureInProgress = true;
|
||||
|
||||
return captureVideoFrameBlobForLPR(videoRef.value, canvas, getFrameCaptureOptions())
|
||||
.then((frameData) => {
|
||||
if (frameData && canCaptureFrames()) {
|
||||
emits('update:frame', frameData);
|
||||
}
|
||||
.then((frameData) => {
|
||||
if (frameData && canCaptureFrames()) {
|
||||
emits("update:frame", frameData);
|
||||
}
|
||||
|
||||
return nextTick().then(() => frameData);
|
||||
})
|
||||
.finally(() => {
|
||||
isFrameCaptureInProgress = false;
|
||||
syncVideoPreviewPlayback();
|
||||
});
|
||||
return nextTick().then(() => frameData);
|
||||
})
|
||||
.finally(() => {
|
||||
isFrameCaptureInProgress = false;
|
||||
syncVideoPreviewPlayback();
|
||||
});
|
||||
}
|
||||
}
|
||||
return Promise.resolve(null);
|
||||
};
|
||||
|
||||
// Watch for zoom level changes
|
||||
watch(() => camera.getZoom(), () => {
|
||||
if (isCameraActive.value) {
|
||||
stopCamera();
|
||||
startCamera();
|
||||
watch(
|
||||
() => camera.getZoom(),
|
||||
() => {
|
||||
if (isCameraActive.value) {
|
||||
stopCamera();
|
||||
startCamera();
|
||||
}
|
||||
}
|
||||
});
|
||||
);
|
||||
|
||||
watch(() => props.captureEnabled, (isCaptureEnabled) => {
|
||||
if (isCaptureEnabled) {
|
||||
resumeCaptureTimers(0);
|
||||
} else {
|
||||
pauseCaptureTimers();
|
||||
watch(
|
||||
() => props.captureEnabled,
|
||||
(isCaptureEnabled) => {
|
||||
if (isCaptureEnabled) {
|
||||
resumeCaptureTimers(0);
|
||||
} else {
|
||||
pauseCaptureTimers();
|
||||
}
|
||||
}
|
||||
});
|
||||
);
|
||||
|
||||
watch([() => props.captureMode, () => props.getFocusViewportRect], clearRelativeFocusViewportRectCache);
|
||||
|
||||
watch(() => props.captureIntervalMs, () => {
|
||||
if (canCaptureFrames() && !isFrameCaptureInProgress && firstCaptureTimeoutId === null) {
|
||||
startCaptureInterval();
|
||||
watch(
|
||||
() => props.captureIntervalMs,
|
||||
() => {
|
||||
if (canCaptureFrames() && !isFrameCaptureInProgress && firstCaptureTimeoutId === null) {
|
||||
startCaptureInterval();
|
||||
}
|
||||
}
|
||||
});
|
||||
);
|
||||
|
||||
watch(() => props.pausePreview, (isPreviewPaused) => {
|
||||
syncVideoPreviewPlayback();
|
||||
watch(
|
||||
() => props.pausePreview,
|
||||
(isPreviewPaused) => {
|
||||
syncVideoPreviewPlayback();
|
||||
|
||||
if (isPreviewPaused) {
|
||||
pauseCaptureTimers();
|
||||
return;
|
||||
if (isPreviewPaused) {
|
||||
pauseCaptureTimers();
|
||||
return;
|
||||
}
|
||||
|
||||
resumeCaptureTimers(LPR_PREVIEW_RESUME_CAPTURE_DELAY_MS);
|
||||
}
|
||||
|
||||
resumeCaptureTimers(LPR_PREVIEW_RESUME_CAPTURE_DELAY_MS);
|
||||
});
|
||||
);
|
||||
|
||||
onMounted(() => {
|
||||
document.addEventListener('visibilitychange', handleVisibilityChange);
|
||||
document.addEventListener('scroll', clearRelativeFocusViewportRectCache, true);
|
||||
window.addEventListener('orientationchange', clearRelativeFocusViewportRectCache);
|
||||
window.addEventListener('resize', clearRelativeFocusViewportRectCache);
|
||||
document.addEventListener("visibilitychange", handleVisibilityChange);
|
||||
document.addEventListener("scroll", clearRelativeFocusViewportRectCache, true);
|
||||
window.addEventListener("orientationchange", clearRelativeFocusViewportRectCache);
|
||||
window.addEventListener("resize", clearRelativeFocusViewportRectCache);
|
||||
observeVideoGeometry();
|
||||
isCameraMounted.value = true;
|
||||
startCamera();
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
document.removeEventListener('visibilitychange', handleVisibilityChange);
|
||||
document.removeEventListener('scroll', clearRelativeFocusViewportRectCache, true);
|
||||
window.removeEventListener('orientationchange', clearRelativeFocusViewportRectCache);
|
||||
window.removeEventListener('resize', clearRelativeFocusViewportRectCache);
|
||||
document.removeEventListener("visibilitychange", handleVisibilityChange);
|
||||
document.removeEventListener("scroll", clearRelativeFocusViewportRectCache, true);
|
||||
window.removeEventListener("orientationchange", clearRelativeFocusViewportRectCache);
|
||||
window.removeEventListener("resize", clearRelativeFocusViewportRectCache);
|
||||
videoResizeObserver?.disconnect();
|
||||
videoResizeObserver = null;
|
||||
clearRelativeFocusViewportRectCache();
|
||||
@@ -600,34 +611,29 @@ defineExpose({
|
||||
stopCamera,
|
||||
});
|
||||
|
||||
watch(() => isCameraActive.value, (newVal) => {
|
||||
emits('camera-toggled', newVal);
|
||||
});
|
||||
|
||||
watch(
|
||||
() => isCameraActive.value,
|
||||
(newVal) => {
|
||||
emits("camera-toggled", newVal);
|
||||
}
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="scanner-camera">
|
||||
<video
|
||||
ref="videoRef"
|
||||
autoplay
|
||||
playsinline
|
||||
:class="{ 'is-active': isCameraActive }"
|
||||
@loadeddata="handleVideoLoadedData"
|
||||
ref="videoRef"
|
||||
autoplay
|
||||
playsinline
|
||||
:class="{ 'is-active': isCameraActive }"
|
||||
@loadeddata="handleVideoLoadedData"
|
||||
>
|
||||
Your browser does not support the video tag.
|
||||
</video>
|
||||
|
||||
<canvas
|
||||
ref="canvasRef"
|
||||
class="capture-canvas"
|
||||
></canvas>
|
||||
<canvas ref="canvasRef" class="capture-canvas"></canvas>
|
||||
|
||||
<canvas
|
||||
ref="visualFingerprintCanvasRef"
|
||||
class="visual-fingerprint-canvas"
|
||||
aria-hidden="true"
|
||||
></canvas>
|
||||
<canvas ref="visualFingerprintCanvasRef" class="visual-fingerprint-canvas" aria-hidden="true"></canvas>
|
||||
|
||||
<div v-if="!isCameraActive" class="camera-inactive">
|
||||
<p>{{ t(cameraErrorKey) }}</p>
|
||||
@@ -686,7 +692,7 @@ video {
|
||||
}
|
||||
|
||||
.capture-button {
|
||||
background: var(--primary-color, #4CAF50);
|
||||
background: var(--primary-color, #4caf50);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 0.8rem 1.5rem;
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
export const LPR_CAMERA_VIDEO_WIDTH = 1024;
|
||||
export const LPR_CAMERA_VIDEO_HEIGHT = 576;
|
||||
export const LPR_CAMERA_FRAME_RATE = 5;
|
||||
export const LPR_FRAME_MAX_WIDTH = 1024;
|
||||
export const LPR_FRAME_MAX_HEIGHT = 576;
|
||||
export const LPR_FRAME_SCANNER_MAX_SIZE = 384;
|
||||
|
||||
@@ -408,7 +408,7 @@ describe("POS mobile camera LPR", () => {
|
||||
await flushPromises();
|
||||
expect(mocks.request).toHaveBeenCalledTimes(1);
|
||||
const cameraWrapper = wrapper.findComponent({ name: "ScannerCamera" });
|
||||
expect(cameraWrapper.props("pausePreview")).toBe(true);
|
||||
expect(cameraWrapper.props("pausePreview")).toBe(false);
|
||||
let rawRequest = await expectRawLPRRequest(0, "frame-a");
|
||||
expect(rawRequest.searchParams.get(LPR_FRAME_CLIENT_CAPTURE_MS_FIELD)).toBe("12.345");
|
||||
expect(Number(rawRequest.searchParams.get(LPR_FRAME_CLIENT_PREFLIGHT_MS_FIELD))).toBeGreaterThanOrEqual(0);
|
||||
@@ -429,7 +429,7 @@ describe("POS mobile camera LPR", () => {
|
||||
|
||||
resolveFirstRequest({ data: { success: false } });
|
||||
await flushPromises();
|
||||
expect(cameraWrapper.props("pausePreview")).toBe(true);
|
||||
expect(cameraWrapper.props("pausePreview")).toBe(false);
|
||||
vi.advanceTimersByTime(1500);
|
||||
await flushPromises();
|
||||
expect(cameraWrapper.props("pausePreview")).toBe(false);
|
||||
@@ -465,7 +465,7 @@ describe("POS mobile camera LPR", () => {
|
||||
expect(searchParams.has(LPR_FRAME_CLIENT_VISUAL_FINGERPRINT_MS_FIELD)).toBe(false);
|
||||
});
|
||||
|
||||
it("pauses the camera preview while scanner duplicate preflight is still running", async () => {
|
||||
it("keeps the camera preview live while scanner duplicate preflight is still running", async () => {
|
||||
vi.useFakeTimers();
|
||||
mocks.request.mockResolvedValue({ data: { success: false } });
|
||||
|
||||
@@ -476,7 +476,7 @@ describe("POS mobile camera LPR", () => {
|
||||
await flushPromises();
|
||||
|
||||
expect(mocks.request).toHaveBeenCalledTimes(1);
|
||||
expect(cameraWrapper.props("pausePreview")).toBe(true);
|
||||
expect(cameraWrapper.props("pausePreview")).toBe(false);
|
||||
|
||||
vi.advanceTimersByTime(2500);
|
||||
await flushPromises();
|
||||
@@ -495,13 +495,13 @@ describe("POS mobile camera LPR", () => {
|
||||
await flushPromises();
|
||||
|
||||
expect(mocks.request).toHaveBeenCalledTimes(1);
|
||||
expect(cameraWrapper.props("pausePreview")).toBe(true);
|
||||
expect(cameraWrapper.props("pausePreview")).toBe(false);
|
||||
|
||||
resolveContentFingerprint("frame-a-new-content");
|
||||
await flushPromises();
|
||||
|
||||
expect(mocks.request).toHaveBeenCalledTimes(2);
|
||||
expect(cameraWrapper.props("pausePreview")).toBe(true);
|
||||
expect(cameraWrapper.props("pausePreview")).toBe(false);
|
||||
|
||||
vi.advanceTimersByTime(2500);
|
||||
await flushPromises();
|
||||
@@ -584,7 +584,7 @@ describe("POS mobile camera LPR", () => {
|
||||
|
||||
expect(mocks.request).not.toHaveBeenCalled();
|
||||
expect(mocks.cameraSetLatestImageBlob).not.toHaveBeenCalled();
|
||||
expect(cameraWrapper.props("pausePreview")).toBe(true);
|
||||
expect(cameraWrapper.props("pausePreview")).toBe(false);
|
||||
});
|
||||
|
||||
it("does not capture or post LPR frames while the active registration slot is already filled", async () => {
|
||||
@@ -599,7 +599,7 @@ describe("POS mobile camera LPR", () => {
|
||||
|
||||
expect(mocks.request).not.toHaveBeenCalled();
|
||||
expect(mocks.cameraSetLatestImageBlob).not.toHaveBeenCalled();
|
||||
expect(cameraWrapper.props("pausePreview")).toBe(true);
|
||||
expect(cameraWrapper.props("pausePreview")).toBe(false);
|
||||
});
|
||||
|
||||
it("resumes camera LPR capture after the active registration slot moves to an empty slot", async () => {
|
||||
@@ -628,7 +628,7 @@ describe("POS mobile camera LPR", () => {
|
||||
|
||||
expect(mocks.request).toHaveBeenCalledTimes(1);
|
||||
expect(mocks.cameraSetLatestImageBlob).not.toHaveBeenCalled();
|
||||
expect(cameraWrapper.props("pausePreview")).toBe(true);
|
||||
expect(cameraWrapper.props("pausePreview")).toBe(false);
|
||||
|
||||
await wrapper.get('[data-testid="camera-frame-b"]').trigger("click");
|
||||
await flushPromises();
|
||||
@@ -665,7 +665,7 @@ describe("POS mobile camera LPR", () => {
|
||||
|
||||
expect(mocks.request).toHaveBeenCalledTimes(1);
|
||||
expect(mocks.cameraSetLatestImageBlob).not.toHaveBeenCalled();
|
||||
expect(cameraWrapper.props("pausePreview")).toBe(true);
|
||||
expect(cameraWrapper.props("pausePreview")).toBe(false);
|
||||
|
||||
await wrapper.get('[data-testid="camera-frame-b"]').trigger("click");
|
||||
await flushPromises();
|
||||
@@ -742,7 +742,7 @@ describe("POS mobile camera LPR", () => {
|
||||
expect(mocks.cameraSetLastSuccess).toHaveBeenCalledTimes(1);
|
||||
expect(mocks.cameraSetLatestImageBlob).toHaveBeenCalledTimes(1);
|
||||
expect(mocks.vehicleSelect).not.toHaveBeenCalled();
|
||||
expect(cameraWrapper.props("pausePreview")).toBe(true);
|
||||
expect(cameraWrapper.props("pausePreview")).toBe(false);
|
||||
|
||||
await wrapper.get('[data-testid="camera-frame-b"]').trigger("click");
|
||||
await flushPromises();
|
||||
@@ -1005,7 +1005,7 @@ describe("POS mobile camera LPR", () => {
|
||||
expect(mocks.request).toHaveBeenCalledTimes(1);
|
||||
expect(mocks.cameraSetLatestImageBlob).not.toHaveBeenCalled();
|
||||
expect(cameraWrapper.props("captureEnabled")).toBe(false);
|
||||
expect(cameraWrapper.props("pausePreview")).toBe(true);
|
||||
expect(cameraWrapper.props("pausePreview")).toBe(false);
|
||||
|
||||
vi.advanceTimersByTime(699);
|
||||
await flushPromises();
|
||||
|
||||
@@ -21,7 +21,6 @@ vi.mock("@/components/viewport/page/templates/scanner/lprFrameCapture", () => ({
|
||||
getLPRFrameSourceRect: mocks.getLPRFrameSourceRect,
|
||||
getLPRFrameTargetSize: mocks.getLPRFrameTargetSize,
|
||||
isVideoFrameReadyForLPR: mocks.isVideoFrameReadyForLPR,
|
||||
LPR_CAMERA_FRAME_RATE: 5,
|
||||
LPR_CAMERA_VIDEO_HEIGHT: 576,
|
||||
LPR_CAMERA_VIDEO_WIDTH: 1024,
|
||||
}));
|
||||
@@ -386,7 +385,7 @@ describe("ScannerCamera capture gating", () => {
|
||||
expect(mocks.getUserMedia).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
video: expect.objectContaining({
|
||||
frameRate: { ideal: 5, max: 5 },
|
||||
frameRate: { ideal: 30, max: 30 },
|
||||
height: { ideal: 576, max: 576 },
|
||||
width: { ideal: 1024, max: 1024 },
|
||||
}),
|
||||
@@ -414,7 +413,7 @@ describe("ScannerCamera capture gating", () => {
|
||||
expect(mocks.getUserMedia.mock.calls[0][0]).toEqual(
|
||||
expect.objectContaining({
|
||||
video: expect.objectContaining({
|
||||
frameRate: { ideal: 5, max: 5 },
|
||||
frameRate: { ideal: 30, max: 30 },
|
||||
height: { ideal: 576, max: 576 },
|
||||
width: { ideal: 1024, max: 1024 },
|
||||
}),
|
||||
@@ -423,7 +422,7 @@ describe("ScannerCamera capture gating", () => {
|
||||
expect(mocks.getUserMedia.mock.calls[1][0]).toEqual(
|
||||
expect.objectContaining({
|
||||
video: expect.objectContaining({
|
||||
frameRate: { ideal: 5 },
|
||||
frameRate: { ideal: 30 },
|
||||
height: { ideal: 576, max: 576 },
|
||||
width: { ideal: 1024, max: 1024 },
|
||||
}),
|
||||
@@ -454,7 +453,7 @@ describe("ScannerCamera capture gating", () => {
|
||||
expect(mocks.getUserMedia.mock.calls[0][0]).toEqual(
|
||||
expect.objectContaining({
|
||||
video: expect.objectContaining({
|
||||
frameRate: { ideal: 5, max: 5 },
|
||||
frameRate: { ideal: 30, max: 30 },
|
||||
height: { ideal: 576, max: 576 },
|
||||
width: { ideal: 1024, max: 1024 },
|
||||
}),
|
||||
@@ -463,13 +462,13 @@ describe("ScannerCamera capture gating", () => {
|
||||
expect(mocks.getUserMedia.mock.calls[1][0]).toEqual(
|
||||
expect.objectContaining({
|
||||
video: expect.objectContaining({
|
||||
frameRate: { ideal: 5, max: 5 },
|
||||
frameRate: { ideal: 30, max: 30 },
|
||||
height: { ideal: 576 },
|
||||
width: { ideal: 1024 },
|
||||
}),
|
||||
})
|
||||
);
|
||||
expect(mocks.getUserMedia.mock.calls[1][0].video.frameRate).toHaveProperty("max", 5);
|
||||
expect(mocks.getUserMedia.mock.calls[1][0].video.frameRate).toHaveProperty("max", 30);
|
||||
expect(mocks.getUserMedia.mock.calls[1][0].video.height).not.toHaveProperty("max");
|
||||
expect(mocks.getUserMedia.mock.calls[1][0].video.width).not.toHaveProperty("max");
|
||||
|
||||
|
||||
Reference in New Issue
Block a user