Browse Source

WIP: Enhance QR scanner initialization and error handling

- Add visual processing overlay with status feedback
- Implement robust worker initialization with retry mechanism
- Add detailed error tracking and logging
- Improve image processing with proper cleanup
- Handle worker lifecycle and state management
- Add proper module context simulation for jsQR initialization

Still investigating jsQR initialization failure in worker context.
qrcode-capacitor
Matthew Raymer 2 months ago
parent
commit
3487f49f49
  1. 588
      src/views/ContactQRScanShowView.vue

588
src/views/ContactQRScanShowView.vue

@ -2,6 +2,15 @@
<QuickNav selected="Profile" />
<!-- CONTENT -->
<section id="Content" class="p-6 pb-24 max-w-3xl mx-auto">
<!-- Processing Overlay -->
<div v-if="isProcessing" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div class="bg-white rounded-lg p-6 text-center">
<div class="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-500 mx-auto mb-4"></div>
<p class="text-lg font-semibold">{{ processingStatus }}</p>
<p class="text-sm text-gray-600 mt-2">{{ processingDetails }}</p>
</div>
</div>
<!-- Breadcrumb -->
<div class="mb-8">
<!-- Back -->
@ -202,6 +211,20 @@ interface WorkerErrorMessage {
type WorkerMessage = WorkerSuccessMessage | WorkerErrorMessage;
interface WorkerResult {
success: boolean;
code?: {
data: string;
location: {
topLeft: { x: number; y: number };
topRight: { x: number; y: number };
bottomLeft: { x: number; y: number };
bottomRight: { x: number; y: number };
};
};
error?: string;
}
@Component({
components: {
QrcodeStream: __USE_QR_READER__ ? QrcodeStream : null,
@ -238,42 +261,75 @@ export default class ContactQRScanShow extends Vue {
private isCapturingPhoto = false;
private appStateListener?: { remove: () => Promise<void> };
private isProcessing = false;
private processingStatus = '';
private processingDetails = '';
private worker: Worker | null = null;
private workerInitialized = false;
private initializationAttempts = 0;
private readonly MAX_INITIALIZATION_ATTEMPTS = 3;
async created() {
logger.log('ContactQRScanShow component created');
try {
// Remove any existing listeners first
await App.removeAllListeners();
await this.cleanupAppListeners();
// Add app state listeners
const stateListener = await App.addListener('appStateChange', (state: AppStateChangeEvent) => {
await this.setupAppLifecycleListeners();
// Load initial data
await this.loadInitialData();
logger.log('ContactQRScanShow initialization complete');
} catch (error) {
logger.error('Failed to initialize ContactQRScanShow:', error);
this.showError('Failed to initialize. Please try again.');
}
}
private async cleanupAppListeners(): Promise<void> {
try {
if (this.appStateListener) {
await this.appStateListener.remove();
this.appStateListener = undefined;
}
await App.removeAllListeners();
logger.log('App listeners cleaned up successfully');
} catch (error) {
logger.error('Error cleaning up app listeners:', error);
throw error;
}
}
private async setupAppLifecycleListeners(): Promise<void> {
try {
// Add app state change listener
this.appStateListener = await App.addListener('appStateChange', (state: AppStateChangeEvent) => {
logger.log('App state changed:', state);
if (!state.isActive && this.cameraActive) {
if (!state.isActive) {
this.cleanupCamera();
}
});
this.appStateListener = stateListener;
// Add pause listener
await App.addListener('pause', () => {
logger.log('App paused');
if (this.cameraActive) {
this.cleanupCamera();
}
this.cleanupCamera();
});
// Add resume listener
await App.addListener('resume', () => {
logger.log('App resumed');
if (this.cameraActive) {
this.initializeCamera();
}
// Don't automatically reinitialize camera on resume
// Let user explicitly request camera access again
});
// Load initial data
await this.loadInitialData();
logger.log('ContactQRScanShow initialization complete');
logger.log('App lifecycle listeners setup complete');
} catch (error) {
logger.error('Failed to initialize ContactQRScanShow:', error);
this.showError('Failed to initialize. Please try again.');
logger.error('Error setting up app lifecycle listeners:', error);
throw error;
}
}
@ -292,22 +348,34 @@ export default class ContactQRScanShow extends Vue {
}
}
private cleanupCamera() {
private async cleanupCamera() {
try {
this.cameraActive = false;
this.isCapturingPhoto = false;
this.addCameraState('cleanup');
logger.log('Camera cleaned up successfully');
if (this.cameraActive) {
this.cameraActive = false;
this.isCapturingPhoto = false;
this.addCameraState('cleanup');
logger.log('Camera cleaned up successfully');
}
} catch (error) {
logger.error('Error during camera cleanup:', error);
}
}
private addCameraState(state: CameraState | QRProcessingState, details?: Record<string, unknown>) {
// Prevent duplicate state transitions
if (this.lastCameraState === state) {
return;
}
const entry: CameraStateHistoryEntry = {
state,
timestamp: Date.now(),
details
details: {
...details,
cameraActive: this.cameraActive,
isCapturingPhoto: this.isCapturingPhoto,
isProcessing: this.isProcessing
}
};
this.cameraStateHistory.push(entry);
@ -316,17 +384,11 @@ export default class ContactQRScanShow extends Vue {
this.cameraStateHistory.shift();
}
// Enhanced logging with better details
logger.log('Camera state transition:', {
state,
details: {
...details,
cameraActive: this.cameraActive,
isCapturingPhoto: this.isCapturingPhoto,
historyLength: this.cameraStateHistory.length
}
this.logWithDetails('Camera state transition', {
state: entry.state,
timestamp: entry.timestamp,
details: entry.details
});
this.lastCameraState = state;
}
@ -363,6 +425,11 @@ export default class ContactQRScanShow extends Vue {
]).catch(error => {
logger.error('Error during component cleanup:', error);
});
if (this.worker) {
this.worker.terminate();
this.worker = null;
}
}
private async handleQRCodeResult(data: string): Promise<void> {
@ -456,114 +523,376 @@ export default class ContactQRScanShow extends Vue {
}
}
async processImageForQRCode(imageDataUrl: string): Promise<void> {
private async initializeWorker(): Promise<void> {
try {
logger.log('Starting QR code processing');
if (this.initializationAttempts >= this.MAX_INITIALIZATION_ATTEMPTS) {
throw new Error('Maximum worker initialization attempts reached');
}
this.initializationAttempts++;
this.setProcessingStatus('Initializing', `Setting up QR scanner (attempt ${this.initializationAttempts})...`);
logger.log('Starting worker initialization', { attempt: this.initializationAttempts });
// Create worker for image processing
const worker = new Worker(URL.createObjectURL(new Blob([`
self.onmessage = async function(e) {
// Import and wait for jsQR to be fully loaded
logger.log('Loading jsQR module...');
const jsQRModule = await import('jsqr');
if (!jsQRModule?.default) {
throw new Error('Failed to load jsQR module');
}
logger.log('jsQR module loaded successfully');
// Create worker with inline code and bundled jsQR
const workerCode = `
let jsQRInitialized = false;
let initializationError = null;
try {
// Get the raw jsQR function code
const jsQRCode = ${jsQRModule.default.toString()};
// Create a proper module-like environment
const context = {
module: { exports: {} },
exports: {},
self: self,
window: self, // Some modules expect window to be defined
global: self // Some modules expect global to be defined
};
// Create a function that will run in the proper context
const initFunction = new Function(
'module', 'exports', 'self', 'window', 'global',
jsQRCode
);
// Execute the function in our context
initFunction.call(
context,
context.module,
context.exports,
context.self,
context.window,
context.global
);
// Get the jsQR function - it might be exported directly or as default
self.jsQR = context.module.exports.default || context.module.exports;
// Verify the function works
if (typeof self.jsQR === 'function') {
// Test with a small dummy image
const testData = new Uint8ClampedArray(4 * 4 * 4);
self.jsQR(testData, 4, 4, { inversionAttempts: "dontInvert" });
jsQRInitialized = true;
self.postMessage({ initialized: true });
} else {
throw new Error('jsQR is not a function after initialization');
}
} catch (error) {
initializationError = error;
console.error('Worker initialization error:', error);
self.postMessage({
error: 'Failed to initialize jsQR: ' + error.message,
details: {
errorType: error.name,
errorStack: error.stack,
jsQRType: typeof self.jsQR
}
});
}
self.onmessage = function(e) {
if (!jsQRInitialized) {
self.postMessage({
success: false,
error: 'QR scanner not initialized',
details: {
initializationError: initializationError ? initializationError.message : 'Unknown error',
jsQRType: typeof self.jsQR
}
});
return;
}
const { imageData, width, height } = e.data;
try {
// Import jsQR in the worker
importScripts('${window.location.origin}/assets/jsqr.js');
const code = self.jsQR(imageData, width, height, {
inversionAttempts: "dontInvert"
const uint8Array = new Uint8ClampedArray(imageData);
// Try normal orientation
let code = self.jsQR(uint8Array, width, height, {
inversionAttempts: "attemptBoth"
});
self.postMessage({ success: true, code });
if (!code) {
// Try rotated 90 degrees
const rotated = rotateImageData(new ImageData(uint8Array, width, height), width, height);
if (rotated) {
code = self.jsQR(new Uint8ClampedArray(rotated.data), rotated.width, rotated.height, {
inversionAttempts: "attemptBoth"
});
}
}
self.postMessage(code ? { success: true, code } : { success: false, error: 'No QR code found' });
} catch (error) {
self.postMessage({ success: false, error: error.message });
console.error('QR processing error:', error);
self.postMessage({
success: false,
error: error instanceof Error ? error.message : String(error),
details: {
errorType: error.name,
errorStack: error instanceof Error ? error.stack : undefined,
jsQRType: typeof self.jsQR
}
});
}
};
`], { type: 'text/javascript' })));
const image = new Image();
image.crossOrigin = 'anonymous';
image.src = imageDataUrl;
function rotateImageData(imageData, width, height) {
const canvas = new OffscreenCanvas(height, width);
const ctx = canvas.getContext('2d');
if (!ctx) return null;
const newImageData = ctx.createImageData(height, width);
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
const srcIndex = (y * width + x) * 4;
const destIndex = ((width - x - 1) * height + y) * 4;
newImageData.data[destIndex] = imageData.data[srcIndex];
newImageData.data[destIndex + 1] = imageData.data[srcIndex + 1];
newImageData.data[destIndex + 2] = imageData.data[srcIndex + 2];
newImageData.data[destIndex + 3] = imageData.data[srcIndex + 3];
}
}
return { data: newImageData.data, width: height, height: width };
}
`;
// Terminate existing worker if it exists
if (this.worker) {
logger.log('Terminating existing worker');
this.worker.terminate();
this.worker = null;
this.workerInitialized = false;
}
// Create blob and worker
logger.log('Creating worker blob and URL');
const blob = new Blob([workerCode], { type: 'application/javascript' });
const workerUrl = URL.createObjectURL(blob);
await new Promise((resolve, reject) => {
const timeout = setTimeout(() => reject(new Error('Image load timeout')), 5000);
image.onload = () => {
clearTimeout(timeout);
resolve(undefined);
logger.log('Creating new worker');
this.worker = new Worker(workerUrl);
// Wait for worker to be ready with increased timeout
await new Promise<void>((resolve, reject) => {
if (!this.worker) {
reject(new Error('Worker failed to initialize'));
return;
}
logger.log('Setting up worker message handlers');
const timeoutId = setTimeout(() => {
logger.error('Worker initialization timed out after 10 seconds');
if (this.worker) {
this.worker.terminate();
this.worker = null;
}
reject(new Error('Worker initialization timed out'));
}, 10000);
this.worker.onmessage = (e) => {
logger.log('Received worker message:', e.data);
if (e.data?.initialized) {
clearTimeout(timeoutId);
this.workerInitialized = true;
resolve();
} else if (e.data?.error) {
clearTimeout(timeoutId);
reject(new Error(e.data.error));
}
};
image.onerror = () => {
clearTimeout(timeout);
reject(new Error('Failed to load image'));
this.worker.onerror = (error) => {
logger.error('Worker error during initialization:', error);
clearTimeout(timeoutId);
reject(new Error(`Worker error: ${error.message}`));
};
});
logger.log('Image loaded, creating canvas...');
const canvas = document.createElement('canvas');
const maxDimension = 1024; // Limit image size for better performance
// Clean up the URL after worker is initialized
URL.revokeObjectURL(workerUrl);
logger.log('Worker initialized successfully');
this.setProcessingStatus('Ready', 'QR scanner initialized');
// Scale down image if needed while maintaining aspect ratio
let width = image.naturalWidth || 800;
let height = image.naturalHeight || 600;
if (width > maxDimension || height > maxDimension) {
if (width > height) {
height = Math.floor(height * (maxDimension / width));
width = maxDimension;
} else {
width = Math.floor(width * (maxDimension / height));
height = maxDimension;
}
}
canvas.width = width;
canvas.height = height;
} catch (error) {
logger.error('Failed to initialize worker:', error);
this.setProcessingStatus('Error', `Failed to initialize QR scanner: ${error instanceof Error ? error.message : String(error)}`);
const ctx = canvas.getContext('2d');
if (!ctx) {
throw new Error('Failed to get canvas context');
if (this.worker) {
this.worker.terminate();
this.worker = null;
}
this.workerInitialized = false;
// Draw image maintaining orientation
ctx.save();
ctx.drawImage(image, 0, 0, width, height);
ctx.restore();
// Show appropriate error message based on attempt count
if (this.initializationAttempts >= this.MAX_INITIALIZATION_ATTEMPTS) {
this.$notify({
group: "alert",
type: "error",
title: "QR Scanner Error",
text: "Failed to initialize QR scanner after multiple attempts. Please try again later.",
});
} else {
this.$notify({
group: "alert",
type: "warning",
title: "QR Scanner Warning",
text: "Failed to initialize QR scanner. Retrying...",
});
}
throw error;
}
}
const imageData = ctx.getImageData(0, 0, width, height);
private async loadImage(imageDataUrl: string): Promise<HTMLImageElement> {
return new Promise((resolve, reject) => {
const img = new Image();
img.crossOrigin = 'anonymous';
logger.log('Processing image data for QR code...', {
width,
height,
dataLength: imageData.data.length
});
const loadTimeout = setTimeout(() => {
img.src = '';
reject(new Error('Image load timeout'));
}, 5000);
img.onload = () => {
clearTimeout(loadTimeout);
resolve(img);
};
img.onerror = () => {
clearTimeout(loadTimeout);
reject(new Error('Failed to load image'));
};
img.src = imageDataUrl;
});
}
// Process QR code in worker
const result = await new Promise<QRCodeResult>((resolve, reject) => {
worker.onmessage = (e: MessageEvent<WorkerMessage>) => {
if (e.data.success) {
resolve(e.data.code);
} else {
reject(new Error(e.data.error));
private getImageData(img: HTMLImageElement): { imageData: ImageData; width: number; height: number } {
const MAX_IMAGE_DIMENSION = 1024;
const aspectRatio = img.naturalWidth / img.naturalHeight;
// Calculate dimensions maintaining aspect ratio
let width = MAX_IMAGE_DIMENSION;
let height = MAX_IMAGE_DIMENSION;
if (aspectRatio > 1) {
height = Math.round(width / aspectRatio);
} else {
width = Math.round(height * aspectRatio);
}
const canvas = new OffscreenCanvas(width, height);
const ctx = canvas.getContext('2d', { willReadFrequently: true });
if (!ctx) {
throw new Error('Failed to get canvas context');
}
// Clear canvas and ensure proper rendering
ctx.clearRect(0, 0, width, height);
ctx.imageSmoothingEnabled = false;
ctx.drawImage(img, 0, 0, width, height);
const imageData = ctx.getImageData(0, 0, width, height);
return { imageData, width, height };
}
private async processImageForQRCode(imageDataUrl: string): Promise<void> {
try {
if (!this.worker || !this.workerInitialized) {
this.setProcessingStatus('Initializing', 'Setting up QR scanner...');
try {
await this.initializeWorker();
} catch (error) {
this.setProcessingStatus('Error', 'QR scanner initialization failed');
this.$notify({
group: "alert",
type: "error",
title: "QR Scanner Error",
text: "Failed to initialize QR scanner. Please try again.",
});
return;
}
}
this.isProcessing = true;
this.setProcessingStatus('Processing', 'Loading image...');
try {
const img = await this.loadImage(imageDataUrl);
const { imageData, width, height } = this.getImageData(img);
this.setProcessingStatus('Processing', 'Analyzing image for QR code...');
const result = await new Promise<WorkerResult>((resolve, reject) => {
if (!this.worker) {
reject(new Error('Worker not initialized'));
return;
}
};
worker.onerror = reject;
worker.postMessage({
imageData: imageData.data,
width: imageData.width,
height: imageData.height
});
});
worker.terminate();
const timeoutId = setTimeout(() => {
this.setProcessingStatus('Error', 'QR code processing timed out');
reject(new Error('QR code processing timed out'));
}, 30000);
this.worker.onmessage = (e: MessageEvent<WorkerResult>) => {
clearTimeout(timeoutId);
resolve(e.data);
};
this.worker.onerror = (error) => {
clearTimeout(timeoutId);
this.setProcessingStatus('Error', `Processing error: ${error.message}`);
reject(error);
};
this.setProcessingStatus('Processing', 'Scanning for QR code...');
this.worker.postMessage({
imageData: Array.from(imageData.data),
width,
height
});
});
if (result) {
logger.log('QR code found:', { data: result.data });
await this.handleQRCodeResult(result.data);
} else {
logger.log('No QR code found in image');
this.showError('No QR code found. Please try again.');
if (result.success && result.code) {
this.setProcessingStatus('Success', 'QR code found!');
await this.handleQRCodeResult(result.code.data);
} else {
this.setProcessingStatus('Error', result.error || 'No QR code found');
this.$notify({
group: "alert",
type: "warning",
title: "No QR Code Found",
text: "Please make sure the QR code is clearly visible and try again.",
});
}
} catch (error) {
console.error('QR processing error:', error);
this.setProcessingStatus('Error', `Failed to process QR code: ${error instanceof Error ? error.message : String(error)}`);
this.$notify({
group: "alert",
type: "error",
title: "Processing Error",
text: "Failed to process the image. Please try again.",
});
}
} catch (error) {
logger.error('QR code processing failed:', error);
this.showError('Failed to process QR code. Please try again.');
} finally {
this.cameraActive = false;
this.isProcessing = false;
this.isCapturingPhoto = false;
this.addCameraState('processing_completed');
this.addCameraState('capture_completed');
}
}
@ -609,9 +938,9 @@ export default class ContactQRScanShow extends Vue {
this.danger(
"Could not extract contact information from the QR code. Please try again.",
"Invalid QR Code",
);
return;
}
);
return;
}
// Validate JWT format
if (
@ -1057,5 +1386,34 @@ export default class ContactQRScanShow extends Vue {
this.addCameraState('capture_completed');
}
}
private setProcessingStatus(status: string, details = '') {
this.isProcessing = status !== 'Error' && status !== 'Success';
this.processingStatus = status;
this.processingDetails = details;
// Log the status change
this.logWithDetails('Processing status changed', {
status,
details,
isProcessing: this.isProcessing
});
}
private logWithDetails(message: string, details?: Record<string, unknown>) {
const formattedDetails = details ?
'\n' + JSON.stringify(details, (key, value) => {
if (value instanceof Error) {
return {
message: value.message,
stack: value.stack,
name: value.name
};
}
return value;
}, 2) : '';
logger.log(`${message}${formattedDetails}`);
}
}
</script>

Loading…
Cancel
Save