forked from jsnbuchanan/crowd-funder-for-time-pwa
- Add proper handling of QRScannerOptions in startScan method - Implement camera selection (front/back) via options.camera - Add video preview toggle via options.showPreview - Store options as class property for persistence - Improve logging with options context - Fix TypeScript error for unused options parameter This change makes the QR scanner more configurable and properly implements the QRScannerService interface contract.
691 lines
22 KiB
TypeScript
691 lines
22 KiB
TypeScript
import {
|
|
QRScannerService,
|
|
ScanListener,
|
|
QRScannerOptions,
|
|
CameraState,
|
|
CameraStateListener,
|
|
} from "./types";
|
|
import { logger } from "@/utils/logger";
|
|
import { EventEmitter } from "events";
|
|
import jsQR from "jsqr";
|
|
|
|
// Build identifier to help distinguish between builds
|
|
const BUILD_ID = `build-${Date.now()}`;
|
|
|
|
export class WebInlineQRScanner implements QRScannerService {
|
|
private scanListener: ScanListener | null = null;
|
|
private isScanning = false;
|
|
private stream: MediaStream | null = null;
|
|
private events = new EventEmitter();
|
|
private canvas: HTMLCanvasElement | null = null;
|
|
private context: CanvasRenderingContext2D | null = null;
|
|
private video: HTMLVideoElement | null = null;
|
|
private animationFrameId: number | null = null;
|
|
private scanAttempts = 0;
|
|
private lastScanTime = 0;
|
|
private readonly id: string;
|
|
private readonly TARGET_FPS = 15; // Target 15 FPS for scanning
|
|
private readonly FRAME_INTERVAL = 1000 / 15; // ~67ms between frames
|
|
private lastFrameTime = 0;
|
|
private cameraStateListeners: Set<CameraStateListener> = new Set();
|
|
private currentState: CameraState = "off";
|
|
private currentStateMessage?: string;
|
|
private options: QRScannerOptions;
|
|
|
|
constructor(options?: QRScannerOptions) {
|
|
// Generate a short random ID for this scanner instance
|
|
this.id = Math.random().toString(36).substring(2, 8).toUpperCase();
|
|
this.options = options ?? {};
|
|
logger.error(
|
|
`[WebInlineQRScanner:${this.id}] Initializing scanner with options:`,
|
|
{
|
|
...this.options,
|
|
buildId: BUILD_ID,
|
|
targetFps: this.TARGET_FPS,
|
|
},
|
|
);
|
|
// Create canvas and video elements
|
|
this.canvas = document.createElement("canvas");
|
|
this.context = this.canvas.getContext("2d", { willReadFrequently: true });
|
|
this.video = document.createElement("video");
|
|
this.video.setAttribute("playsinline", "true"); // Required for iOS
|
|
logger.error(
|
|
`[WebInlineQRScanner:${this.id}] DOM elements created successfully`,
|
|
);
|
|
}
|
|
|
|
private updateCameraState(state: CameraState, message?: string) {
|
|
this.currentState = state;
|
|
this.currentStateMessage = message;
|
|
this.cameraStateListeners.forEach((listener) => {
|
|
try {
|
|
listener.onStateChange(state, message);
|
|
logger.info(
|
|
`[WebInlineQRScanner:${this.id}] Camera state changed to: ${state}`,
|
|
{
|
|
state,
|
|
message,
|
|
},
|
|
);
|
|
} catch (error) {
|
|
logger.error(
|
|
`[WebInlineQRScanner:${this.id}] Error in camera state listener:`,
|
|
error,
|
|
);
|
|
}
|
|
});
|
|
}
|
|
|
|
addCameraStateListener(listener: CameraStateListener): void {
|
|
this.cameraStateListeners.add(listener);
|
|
// Immediately notify the new listener of current state
|
|
listener.onStateChange(this.currentState, this.currentStateMessage);
|
|
}
|
|
|
|
removeCameraStateListener(listener: CameraStateListener): void {
|
|
this.cameraStateListeners.delete(listener);
|
|
}
|
|
|
|
async checkPermissions(): Promise<boolean> {
|
|
try {
|
|
this.updateCameraState("initializing", "Checking camera permissions...");
|
|
logger.error(
|
|
`[WebInlineQRScanner:${this.id}] Checking camera permissions...`,
|
|
);
|
|
|
|
// First try the Permissions API if available
|
|
if (navigator.permissions && navigator.permissions.query) {
|
|
try {
|
|
const permissions = await navigator.permissions.query({
|
|
name: "camera" as PermissionName,
|
|
});
|
|
logger.error(
|
|
`[WebInlineQRScanner:${this.id}] Permission state from Permissions API:`,
|
|
permissions.state,
|
|
);
|
|
if (permissions.state === "granted") {
|
|
this.updateCameraState("ready", "Camera permissions granted");
|
|
return true;
|
|
}
|
|
} catch (permError) {
|
|
// Permissions API might not be supported, continue with getUserMedia check
|
|
logger.error(
|
|
`[WebInlineQRScanner:${this.id}] Permissions API not supported:`,
|
|
permError,
|
|
);
|
|
}
|
|
}
|
|
|
|
// If Permissions API is not available or didn't return granted,
|
|
// try a test getUserMedia call
|
|
try {
|
|
const testStream = await navigator.mediaDevices.getUserMedia({
|
|
video: true,
|
|
});
|
|
// If we get here, we have permission
|
|
testStream.getTracks().forEach((track) => track.stop());
|
|
this.updateCameraState("ready", "Camera permissions granted");
|
|
return true;
|
|
} catch (mediaError) {
|
|
const error = mediaError as Error;
|
|
logger.error(
|
|
`[WebInlineQRScanner:${this.id}] getUserMedia test failed:`,
|
|
{
|
|
name: error.name,
|
|
message: error.message,
|
|
},
|
|
);
|
|
|
|
if (
|
|
error.name === "NotAllowedError" ||
|
|
error.name === "PermissionDeniedError"
|
|
) {
|
|
this.updateCameraState("permission_denied", "Camera access denied");
|
|
return false;
|
|
}
|
|
// For other errors, we'll try requesting permissions explicitly
|
|
return false;
|
|
}
|
|
} catch (error) {
|
|
logger.error(
|
|
`[WebInlineQRScanner:${this.id}] Error checking camera permissions:`,
|
|
{
|
|
error: error instanceof Error ? error.message : String(error),
|
|
stack: error instanceof Error ? error.stack : undefined,
|
|
},
|
|
);
|
|
this.updateCameraState("error", "Error checking camera permissions");
|
|
return false;
|
|
}
|
|
}
|
|
|
|
async requestPermissions(): Promise<boolean> {
|
|
try {
|
|
this.updateCameraState(
|
|
"initializing",
|
|
"Requesting camera permissions...",
|
|
);
|
|
logger.error(
|
|
`[WebInlineQRScanner:${this.id}] Requesting camera permissions...`,
|
|
);
|
|
|
|
// First check if we have any video devices
|
|
const devices = await navigator.mediaDevices.enumerateDevices();
|
|
const videoDevices = devices.filter(
|
|
(device) => device.kind === "videoinput",
|
|
);
|
|
|
|
logger.error(`[WebInlineQRScanner:${this.id}] Found video devices:`, {
|
|
count: videoDevices.length,
|
|
devices: videoDevices.map((d) => ({ id: d.deviceId, label: d.label })),
|
|
userAgent: navigator.userAgent,
|
|
});
|
|
|
|
if (videoDevices.length === 0) {
|
|
logger.error(`[WebInlineQRScanner:${this.id}] No video devices found`);
|
|
this.updateCameraState("not_found", "No camera found on this device");
|
|
throw new Error("No camera found on this device");
|
|
}
|
|
|
|
// Try to get a stream with specific constraints
|
|
logger.error(
|
|
`[WebInlineQRScanner:${this.id}] Requesting camera stream with constraints:`,
|
|
{
|
|
facingMode: "environment",
|
|
width: { ideal: 1280 },
|
|
height: { ideal: 720 },
|
|
},
|
|
);
|
|
|
|
try {
|
|
const stream = await navigator.mediaDevices.getUserMedia({
|
|
video: {
|
|
facingMode: "environment",
|
|
width: { ideal: 1280 },
|
|
height: { ideal: 720 },
|
|
},
|
|
});
|
|
|
|
this.updateCameraState("ready", "Camera permissions granted");
|
|
|
|
// Stop the test stream immediately
|
|
stream.getTracks().forEach((track) => {
|
|
logger.error(`[WebInlineQRScanner:${this.id}] Stopping test track:`, {
|
|
kind: track.kind,
|
|
label: track.label,
|
|
readyState: track.readyState,
|
|
});
|
|
track.stop();
|
|
});
|
|
return true;
|
|
} catch (mediaError) {
|
|
const error = mediaError as Error;
|
|
logger.error(
|
|
`[WebInlineQRScanner:${this.id}] Error requesting camera access:`,
|
|
{
|
|
name: error.name,
|
|
message: error.message,
|
|
userAgent: navigator.userAgent,
|
|
},
|
|
);
|
|
|
|
// Update state based on error type
|
|
if (
|
|
error.name === "NotFoundError" ||
|
|
error.name === "DevicesNotFoundError"
|
|
) {
|
|
this.updateCameraState("not_found", "No camera found on this device");
|
|
throw new Error("No camera found on this device");
|
|
} else if (
|
|
error.name === "NotAllowedError" ||
|
|
error.name === "PermissionDeniedError"
|
|
) {
|
|
this.updateCameraState("permission_denied", "Camera access denied");
|
|
throw new Error(
|
|
"Camera access denied. Please grant camera permission and try again",
|
|
);
|
|
} else if (
|
|
error.name === "NotReadableError" ||
|
|
error.name === "TrackStartError"
|
|
) {
|
|
this.updateCameraState(
|
|
"in_use",
|
|
"Camera is in use by another application",
|
|
);
|
|
throw new Error("Camera is in use by another application");
|
|
} else {
|
|
this.updateCameraState("error", error.message);
|
|
throw new Error(`Camera error: ${error.message}`);
|
|
}
|
|
}
|
|
} catch (error) {
|
|
const wrappedError =
|
|
error instanceof Error ? error : new Error(String(error));
|
|
logger.error(
|
|
`[WebInlineQRScanner:${this.id}] Error in requestPermissions:`,
|
|
{
|
|
error: wrappedError.message,
|
|
stack: wrappedError.stack,
|
|
userAgent: navigator.userAgent,
|
|
},
|
|
);
|
|
throw wrappedError;
|
|
}
|
|
}
|
|
|
|
async isSupported(): Promise<boolean> {
|
|
try {
|
|
logger.error(
|
|
`[WebInlineQRScanner:${this.id}] Checking browser support...`,
|
|
);
|
|
// Check for secure context first
|
|
if (!window.isSecureContext) {
|
|
logger.error(
|
|
`[WebInlineQRScanner:${this.id}] Camera access requires HTTPS (secure context)`,
|
|
);
|
|
return false;
|
|
}
|
|
|
|
// Check for camera API support
|
|
if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
|
|
logger.error(
|
|
`[WebInlineQRScanner:${this.id}] Camera API not supported in this browser`,
|
|
);
|
|
return false;
|
|
}
|
|
|
|
// Check if we have any video devices
|
|
const devices = await navigator.mediaDevices.enumerateDevices();
|
|
const hasVideoDevices = devices.some(
|
|
(device) => device.kind === "videoinput",
|
|
);
|
|
|
|
logger.error(`[WebInlineQRScanner:${this.id}] Device support check:`, {
|
|
hasSecureContext: window.isSecureContext,
|
|
hasMediaDevices: !!navigator.mediaDevices,
|
|
hasGetUserMedia: !!navigator.mediaDevices?.getUserMedia,
|
|
hasVideoDevices,
|
|
deviceCount: devices.length,
|
|
});
|
|
|
|
if (!hasVideoDevices) {
|
|
logger.error(`[WebInlineQRScanner:${this.id}] No video devices found`);
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
} catch (error) {
|
|
logger.error(
|
|
`[WebInlineQRScanner:${this.id}] Error checking camera support:`,
|
|
{
|
|
error: error instanceof Error ? error.message : String(error),
|
|
stack: error instanceof Error ? error.stack : undefined,
|
|
},
|
|
);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
private async scanQRCode(): Promise<void> {
|
|
if (!this.video || !this.canvas || !this.context || !this.stream) {
|
|
logger.error(
|
|
`[WebInlineQRScanner:${this.id}] Cannot scan: missing required elements`,
|
|
{
|
|
hasVideo: !!this.video,
|
|
hasCanvas: !!this.canvas,
|
|
hasContext: !!this.context,
|
|
hasStream: !!this.stream,
|
|
},
|
|
);
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const now = Date.now();
|
|
const timeSinceLastFrame = now - this.lastFrameTime;
|
|
|
|
// Throttle frame processing to target FPS
|
|
if (timeSinceLastFrame < this.FRAME_INTERVAL) {
|
|
this.animationFrameId = requestAnimationFrame(() => this.scanQRCode());
|
|
return;
|
|
}
|
|
|
|
this.lastFrameTime = now;
|
|
|
|
// Set canvas dimensions to match video
|
|
this.canvas.width = this.video.videoWidth;
|
|
this.canvas.height = this.video.videoHeight;
|
|
|
|
// Draw video frame to canvas
|
|
this.context.drawImage(
|
|
this.video,
|
|
0,
|
|
0,
|
|
this.canvas.width,
|
|
this.canvas.height,
|
|
);
|
|
|
|
// Get image data from canvas
|
|
const imageData = this.context.getImageData(
|
|
0,
|
|
0,
|
|
this.canvas.width,
|
|
this.canvas.height,
|
|
);
|
|
|
|
// Increment scan attempts
|
|
this.scanAttempts++;
|
|
const timeSinceLastScan = now - this.lastScanTime;
|
|
|
|
// Log scan attempt every 100 frames or 1 second
|
|
if (this.scanAttempts % 100 === 0 || timeSinceLastScan >= 1000) {
|
|
logger.error(`[WebInlineQRScanner:${this.id}] Scanning frame:`, {
|
|
attempt: this.scanAttempts,
|
|
dimensions: {
|
|
width: this.canvas.width,
|
|
height: this.canvas.height,
|
|
},
|
|
fps: Math.round(1000 / timeSinceLastScan),
|
|
imageDataSize: imageData.data.length,
|
|
imageDataWidth: imageData.width,
|
|
imageDataHeight: imageData.height,
|
|
timeSinceLastFrame,
|
|
targetFPS: this.TARGET_FPS,
|
|
});
|
|
this.lastScanTime = now;
|
|
}
|
|
|
|
// Scan for QR code
|
|
const code = jsQR(imageData.data, imageData.width, imageData.height, {
|
|
inversionAttempts: "attemptBoth", // Try both normal and inverted
|
|
});
|
|
|
|
if (code) {
|
|
// Check if the QR code is blurry by examining the location points
|
|
const { topRightCorner, topLeftCorner, bottomLeftCorner } =
|
|
code.location;
|
|
const width = Math.sqrt(
|
|
Math.pow(topRightCorner.x - topLeftCorner.x, 2) +
|
|
Math.pow(topRightCorner.y - topLeftCorner.y, 2),
|
|
);
|
|
const height = Math.sqrt(
|
|
Math.pow(bottomLeftCorner.x - topLeftCorner.x, 2) +
|
|
Math.pow(bottomLeftCorner.y - topLeftCorner.y, 2),
|
|
);
|
|
|
|
// Adjust minimum size based on canvas dimensions
|
|
const minSize = Math.min(this.canvas.width, this.canvas.height) * 0.1; // 10% of the smaller dimension
|
|
const isBlurry =
|
|
width < minSize ||
|
|
height < minSize ||
|
|
!code.data ||
|
|
code.data.length === 0;
|
|
|
|
logger.error(`[WebInlineQRScanner:${this.id}] QR Code detected:`, {
|
|
data: code.data,
|
|
location: code.location,
|
|
attempts: this.scanAttempts,
|
|
isBlurry,
|
|
dimensions: {
|
|
width,
|
|
height,
|
|
minSize,
|
|
canvasWidth: this.canvas.width,
|
|
canvasHeight: this.canvas.height,
|
|
relativeWidth: width / this.canvas.width,
|
|
relativeHeight: height / this.canvas.height,
|
|
},
|
|
corners: {
|
|
topLeft: topLeftCorner,
|
|
topRight: topRightCorner,
|
|
bottomLeft: bottomLeftCorner,
|
|
},
|
|
});
|
|
|
|
if (isBlurry) {
|
|
if (this.scanListener?.onError) {
|
|
this.scanListener.onError(
|
|
new Error(
|
|
"QR code detected but too blurry to read. Please hold the camera steady and ensure the QR code is well-lit.",
|
|
),
|
|
);
|
|
}
|
|
// Continue scanning if QR code is blurry
|
|
this.animationFrameId = requestAnimationFrame(() =>
|
|
this.scanQRCode(),
|
|
);
|
|
return;
|
|
}
|
|
|
|
if (this.scanListener?.onScan) {
|
|
this.scanListener.onScan(code.data);
|
|
}
|
|
// Stop scanning after successful detection
|
|
await this.stopScan();
|
|
return;
|
|
}
|
|
|
|
// Continue scanning if no QR code found
|
|
this.animationFrameId = requestAnimationFrame(() => this.scanQRCode());
|
|
} catch (error) {
|
|
logger.error(`[WebInlineQRScanner:${this.id}] Error scanning QR code:`, {
|
|
error: error instanceof Error ? error.message : String(error),
|
|
stack: error instanceof Error ? error.stack : undefined,
|
|
attempt: this.scanAttempts,
|
|
videoState: this.video
|
|
? {
|
|
readyState: this.video.readyState,
|
|
paused: this.video.paused,
|
|
ended: this.video.ended,
|
|
width: this.video.videoWidth,
|
|
height: this.video.videoHeight,
|
|
}
|
|
: null,
|
|
canvasState: this.canvas
|
|
? {
|
|
width: this.canvas.width,
|
|
height: this.canvas.height,
|
|
}
|
|
: null,
|
|
});
|
|
if (this.scanListener?.onError) {
|
|
this.scanListener.onError(
|
|
error instanceof Error ? error : new Error(String(error)),
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
async startScan(options?: QRScannerOptions): Promise<void> {
|
|
if (this.isScanning) {
|
|
logger.error(`[WebInlineQRScanner:${this.id}] Scanner already running`);
|
|
return;
|
|
}
|
|
|
|
// Update options if provided
|
|
if (options) {
|
|
this.options = { ...this.options, ...options };
|
|
}
|
|
|
|
try {
|
|
this.isScanning = true;
|
|
this.scanAttempts = 0;
|
|
this.lastScanTime = Date.now();
|
|
this.updateCameraState("initializing", "Starting camera...");
|
|
logger.error(`[WebInlineQRScanner:${this.id}] Starting scan with options:`, this.options);
|
|
|
|
// Get camera stream with options
|
|
logger.error(
|
|
`[WebInlineQRScanner:${this.id}] Requesting camera stream...`,
|
|
);
|
|
this.stream = await navigator.mediaDevices.getUserMedia({
|
|
video: {
|
|
facingMode: this.options.camera === "front" ? "user" : "environment",
|
|
width: { ideal: 1280 },
|
|
height: { ideal: 720 },
|
|
},
|
|
});
|
|
|
|
this.updateCameraState("active", "Camera is active");
|
|
|
|
logger.error(`[WebInlineQRScanner:${this.id}] Camera stream obtained:`, {
|
|
tracks: this.stream.getTracks().map((t) => ({
|
|
kind: t.kind,
|
|
label: t.label,
|
|
readyState: t.readyState,
|
|
})),
|
|
options: this.options,
|
|
});
|
|
|
|
// Set up video element
|
|
if (this.video) {
|
|
this.video.srcObject = this.stream;
|
|
// Only show preview if showPreview is true
|
|
if (this.options.showPreview) {
|
|
this.video.style.display = "block";
|
|
} else {
|
|
this.video.style.display = "none";
|
|
}
|
|
await this.video.play();
|
|
logger.error(
|
|
`[WebInlineQRScanner:${this.id}] Video element started playing`,
|
|
);
|
|
}
|
|
|
|
// Emit stream to component
|
|
this.events.emit("stream", this.stream);
|
|
logger.error(`[WebInlineQRScanner:${this.id}] Stream event emitted`);
|
|
|
|
// Start QR code scanning
|
|
this.scanQRCode();
|
|
} catch (error) {
|
|
this.isScanning = false;
|
|
const wrappedError =
|
|
error instanceof Error ? error : new Error(String(error));
|
|
|
|
// Update state based on error type
|
|
if (
|
|
wrappedError.name === "NotReadableError" ||
|
|
wrappedError.name === "TrackStartError"
|
|
) {
|
|
this.updateCameraState(
|
|
"in_use",
|
|
"Camera is in use by another application",
|
|
);
|
|
} else {
|
|
this.updateCameraState("error", wrappedError.message);
|
|
}
|
|
|
|
if (this.scanListener?.onError) {
|
|
this.scanListener.onError(wrappedError);
|
|
}
|
|
throw wrappedError;
|
|
}
|
|
}
|
|
|
|
async stopScan(): Promise<void> {
|
|
if (!this.isScanning) {
|
|
logger.error(
|
|
`[WebInlineQRScanner:${this.id}] Scanner not running, nothing to stop`,
|
|
);
|
|
return;
|
|
}
|
|
|
|
try {
|
|
logger.error(`[WebInlineQRScanner:${this.id}] Stopping scan`, {
|
|
scanAttempts: this.scanAttempts,
|
|
duration: Date.now() - this.lastScanTime,
|
|
});
|
|
|
|
// Stop animation frame
|
|
if (this.animationFrameId !== null) {
|
|
cancelAnimationFrame(this.animationFrameId);
|
|
this.animationFrameId = null;
|
|
logger.error(
|
|
`[WebInlineQRScanner:${this.id}] Animation frame cancelled`,
|
|
);
|
|
}
|
|
|
|
// Stop video
|
|
if (this.video) {
|
|
this.video.pause();
|
|
this.video.srcObject = null;
|
|
logger.error(`[WebInlineQRScanner:${this.id}] Video element stopped`);
|
|
}
|
|
|
|
// Stop all tracks in the stream
|
|
if (this.stream) {
|
|
this.stream.getTracks().forEach((track) => {
|
|
logger.error(`[WebInlineQRScanner:${this.id}] Stopping track:`, {
|
|
kind: track.kind,
|
|
label: track.label,
|
|
readyState: track.readyState,
|
|
});
|
|
track.stop();
|
|
});
|
|
this.stream = null;
|
|
}
|
|
|
|
// Emit stream stopped event
|
|
this.events.emit("stream", null);
|
|
logger.error(
|
|
`[WebInlineQRScanner:${this.id}] Stream stopped event emitted`,
|
|
);
|
|
} catch (error) {
|
|
logger.error(
|
|
`[WebInlineQRScanner:${this.id}] Error stopping scan:`,
|
|
error,
|
|
);
|
|
this.updateCameraState("error", "Error stopping camera");
|
|
throw error;
|
|
} finally {
|
|
this.isScanning = false;
|
|
logger.error(`[WebInlineQRScanner:${this.id}] Scan stopped successfully`);
|
|
}
|
|
}
|
|
|
|
addListener(listener: ScanListener): void {
|
|
logger.error(`[WebInlineQRScanner:${this.id}] Adding scan listener`);
|
|
this.scanListener = listener;
|
|
}
|
|
|
|
onStream(callback: (stream: MediaStream | null) => void): void {
|
|
logger.error(
|
|
`[WebInlineQRScanner:${this.id}] Adding stream event listener`,
|
|
);
|
|
this.events.on("stream", callback);
|
|
}
|
|
|
|
async cleanup(): Promise<void> {
|
|
try {
|
|
logger.error(`[WebInlineQRScanner:${this.id}] Starting cleanup`);
|
|
await this.stopScan();
|
|
this.events.removeAllListeners();
|
|
logger.error(`[WebInlineQRScanner:${this.id}] Event listeners removed`);
|
|
|
|
// Clean up DOM elements
|
|
if (this.video) {
|
|
this.video.remove();
|
|
this.video = null;
|
|
logger.error(`[WebInlineQRScanner:${this.id}] Video element removed`);
|
|
}
|
|
if (this.canvas) {
|
|
this.canvas.remove();
|
|
this.canvas = null;
|
|
logger.error(`[WebInlineQRScanner:${this.id}] Canvas element removed`);
|
|
}
|
|
this.context = null;
|
|
logger.error(
|
|
`[WebInlineQRScanner:${this.id}] Cleanup completed successfully`,
|
|
);
|
|
} catch (error) {
|
|
logger.error(
|
|
`[WebInlineQRScanner:${this.id}] Error during cleanup:`,
|
|
error,
|
|
);
|
|
this.updateCameraState("error", "Error during cleanup");
|
|
throw error;
|
|
}
|
|
}
|
|
}
|