From 85aa2981ad572ab0a3f570687d14c49fbe6e6ef0 Mon Sep 17 00:00:00 2001 From: Matt Raymer Date: Tue, 20 May 2025 02:50:56 -0400 Subject: [PATCH] docs: add comprehensive camera switching implementation guide Add detailed documentation for camera switching functionality across web and mobile platforms: - Add camera management interfaces to QRScannerService - Document MLKit Barcode Scanner configuration for Capacitor - Add platform-specific implementations for iOS and Android - Include camera state management and error handling - Add performance optimization guidelines - Document testing requirements and scenarios Key additions: - Camera switching implementation for both platforms - Platform-specific considerations (iOS/Android) - Battery and memory optimization strategies - Comprehensive testing guidelines - Error handling and state management - Security and permission considerations This update provides a complete reference for implementing robust camera switching functionality in the QR code scanner. --- doc/qr-code-implementation-guide.md | 525 +++++++++++++++++++++++++++- 1 file changed, 523 insertions(+), 2 deletions(-) diff --git a/doc/qr-code-implementation-guide.md b/doc/qr-code-implementation-guide.md index bf7e448d..e6e36fcd 100644 --- a/doc/qr-code-implementation-guide.md +++ b/doc/qr-code-implementation-guide.md @@ -54,6 +54,9 @@ interface QRScannerService { addListener(listener: ScanListener): void; onStream(callback: (stream: MediaStream | null) => void): void; cleanup(): Promise; + getAvailableCameras(): Promise; + switchCamera(deviceId: string): Promise; + getCurrentCamera(): Promise; } interface ScanListener { @@ -87,7 +90,15 @@ const config: CapacitorConfig = { formats: ['QR_CODE'], detectorSize: 1.0, lensFacing: 'back', - googleBarcodeScannerModuleInstallState: true + googleBarcodeScannerModuleInstallState: true, + // Additional camera options + cameraOptions: { + quality: 0.8, + allowEditing: false, + resultType: 'uri', + sourceType: 'CAMERA', + saveToGallery: false + } } } }; @@ -281,4 +292,514 @@ export async function createBuildConfig(mode: string) { - Track memory usage - Monitor camera performance - Check error rates -- Analyze user feedback \ No newline at end of file +- Analyze user feedback + +## Camera Handling + +### Camera Switching Implementation + +The QR scanner supports camera switching on both mobile and desktop platforms through a unified interface. + +#### Platform-Specific Implementations + +1. **Mobile (Capacitor)** + - Uses `@capacitor-mlkit/barcode-scanning` + - Supports front/back camera switching + - Native camera access through platform APIs + - Optimized for mobile performance + + ```typescript + // CapacitorQRScanner.ts + async startScan(options?: QRScannerOptions): Promise { + const scanOptions: StartScanOptions = { + formats: [BarcodeFormat.QrCode], + lensFacing: options?.camera === "front" ? + LensFacing.Front : LensFacing.Back + }; + await BarcodeScanner.startScan(scanOptions); + } + ``` + +2. **Web (Desktop)** + - Uses browser's MediaDevices API + - Supports multiple camera devices + - Dynamic camera enumeration + - Real-time camera switching + + ```typescript + // WebInlineQRScanner.ts + async getAvailableCameras(): Promise { + const devices = await navigator.mediaDevices.enumerateDevices(); + return devices.filter(device => device.kind === 'videoinput'); + } + + async switchCamera(deviceId: string): Promise { + // Stop current stream + await this.stopScan(); + + // Start new stream with selected camera + this.stream = await navigator.mediaDevices.getUserMedia({ + video: { + deviceId: { exact: deviceId }, + width: { ideal: 1280 }, + height: { ideal: 720 } + } + }); + + // Update video and restart scanning + if (this.video) { + this.video.srcObject = this.stream; + await this.video.play(); + } + this.scanQRCode(); + } + ``` + +### Core Interfaces + +```typescript +interface QRScannerService { + // ... existing methods ... + + /** Get available cameras */ + getAvailableCameras(): Promise; + + /** Switch to a specific camera */ + switchCamera(deviceId: string): Promise; + + /** Get current camera info */ + getCurrentCamera(): Promise; +} + +interface QRScannerOptions { + /** Camera to use ('front' or 'back' for mobile) */ + camera?: "front" | "back"; + /** Whether to show a preview of the camera feed */ + showPreview?: boolean; + /** Whether to play a sound on successful scan */ + playSound?: boolean; +} +``` + +### UI Components + +The camera switching UI adapts to the platform: + +1. **Mobile Interface** + - Simple toggle button for front/back cameras + - Positioned in bottom-right corner + - Clear visual feedback during switching + - Native camera controls + + ```vue + + ``` + +2. **Desktop Interface** + - Dropdown menu with all available cameras + - Camera labels and device IDs + - Real-time camera switching + - Responsive design + + ```vue + + ``` + +### Error Handling + +The camera switching implementation includes comprehensive error handling: + +1. **Common Error Scenarios** + - Camera in use by another application + - Permission denied during switch + - Device not available + - Stream initialization failure + - Camera switch timeout + +2. **Error Response** + ```typescript + private async handleCameraSwitch(deviceId: string): Promise { + try { + this.updateCameraState("initializing", "Switching camera..."); + await this.switchCamera(deviceId); + this.updateCameraState("active", "Camera switched successfully"); + } catch (error) { + this.updateCameraState("error", "Failed to switch camera"); + throw error; + } + } + ``` + +3. **User Feedback** + - Visual indicators during switching + - Error notifications + - Camera state updates + - Permission request dialogs + +### State Management + +The camera system maintains several states: + +1. **Camera States** + ```typescript + type CameraState = + | "initializing" // Camera is being initialized + | "ready" // Camera is ready to use + | "active" // Camera is actively streaming + | "in_use" // Camera is in use by another application + | "permission_denied" // Camera permission was denied + | "not_found" // No camera found on device + | "error" // Generic error state + | "off"; // Camera is off + ``` + +2. **State Transitions** + - Initialization → Ready + - Ready → Active + - Active → Switching + - Switching → Active/Error + - Any state → Off (on cleanup) + +### Best Practices + +1. **Camera Access** + - Always check permissions before switching + - Handle camera busy states + - Implement proper cleanup + - Monitor camera state changes + +2. **Performance** + - Optimize camera resolution + - Handle stream switching efficiently + - Manage memory usage + - Implement proper cleanup + +3. **User Experience** + - Clear visual feedback + - Smooth camera transitions + - Intuitive camera controls + - Responsive UI updates + - Accessible camera selection + +4. **Security** + - Secure camera access + - Permission management + - Device validation + - Stream security + +### Testing + +1. **Test Scenarios** + - Camera switching on both platforms + - Permission handling + - Error conditions + - Multiple camera devices + - Camera busy states + - Stream initialization + - UI responsiveness + +2. **Test Environment** + - Multiple mobile devices + - Various desktop browsers + - Different camera configurations + - Network conditions + - Permission states + +### Capacitor Implementation Details + +#### MLKit Barcode Scanner Configuration + +1. **Plugin Setup** + ```typescript + // capacitor.config.ts + const config: CapacitorConfig = { + plugins: { + MLKitBarcodeScanner: { + formats: ['QR_CODE'], + detectorSize: 1.0, + lensFacing: 'back', + googleBarcodeScannerModuleInstallState: true, + // Additional camera options + cameraOptions: { + quality: 0.8, + allowEditing: false, + resultType: 'uri', + sourceType: 'CAMERA', + saveToGallery: false + } + } + } + }; + ``` + +2. **Camera Management** + ```typescript + // CapacitorQRScanner.ts + export class CapacitorQRScanner implements QRScannerService { + private currentLensFacing: LensFacing = LensFacing.Back; + + async getAvailableCameras(): Promise { + // On mobile, we have two fixed cameras + return [ + { + deviceId: 'back', + label: 'Back Camera', + kind: 'videoinput' + }, + { + deviceId: 'front', + label: 'Front Camera', + kind: 'videoinput' + } + ] as MediaDeviceInfo[]; + } + + async switchCamera(deviceId: string): Promise { + if (!this.isScanning) return; + + const newLensFacing = deviceId === 'front' ? + LensFacing.Front : LensFacing.Back; + + // Stop current scan + await this.stopScan(); + + // Update lens facing + this.currentLensFacing = newLensFacing; + + // Restart scan with new camera + await this.startScan({ + camera: deviceId as 'front' | 'back' + }); + } + + async getCurrentCamera(): Promise { + return { + deviceId: this.currentLensFacing === LensFacing.Front ? 'front' : 'back', + label: this.currentLensFacing === LensFacing.Front ? + 'Front Camera' : 'Back Camera', + kind: 'videoinput' + } as MediaDeviceInfo; + } + } + ``` + +3. **Camera State Management** + ```typescript + // CapacitorQRScanner.ts + private async handleCameraState(): Promise { + try { + // Check if camera is available + const { camera } = await BarcodeScanner.checkPermissions(); + + if (camera === 'denied') { + this.updateCameraState('permission_denied'); + return; + } + + // Check if camera is in use + const isInUse = await this.isCameraInUse(); + if (isInUse) { + this.updateCameraState('in_use'); + return; + } + + this.updateCameraState('ready'); + } catch (error) { + this.updateCameraState('error', error.message); + } + } + + private async isCameraInUse(): Promise { + try { + // Try to start a test scan + await BarcodeScanner.startScan({ + formats: [BarcodeFormat.QrCode], + lensFacing: this.currentLensFacing + }); + // If successful, stop it immediately + await BarcodeScanner.stopScan(); + return false; + } catch (error) { + return error.message.includes('camera in use'); + } + } + ``` + +4. **Error Handling** + ```typescript + // CapacitorQRScanner.ts + private async handleCameraError(error: Error): Promise { + switch (error.name) { + case 'CameraPermissionDenied': + this.updateCameraState('permission_denied'); + break; + case 'CameraInUse': + this.updateCameraState('in_use'); + break; + case 'CameraUnavailable': + this.updateCameraState('not_found'); + break; + default: + this.updateCameraState('error', error.message); + } + } + ``` + +#### Platform-Specific Considerations + +1. **iOS Implementation** + - Camera permissions in Info.plist + - Privacy descriptions + - Camera usage description + - Background camera access + + ```xml + + NSCameraUsageDescription + We need access to your camera to scan QR codes + NSPhotoLibraryUsageDescription + We need access to save scanned QR codes + ``` + +2. **Android Implementation** + - Camera permissions in AndroidManifest.xml + - Runtime permission handling + - Camera features declaration + - Hardware feature requirements + + ```xml + + + + + ``` + +3. **Platform-Specific Features** + - iOS: Camera orientation handling + - Android: Camera resolution optimization + - Both: Battery usage optimization + - Both: Memory management + + ```typescript + // Platform-specific optimizations + private getPlatformSpecificOptions(): StartScanOptions { + const baseOptions: StartScanOptions = { + formats: [BarcodeFormat.QrCode], + lensFacing: this.currentLensFacing + }; + + if (Capacitor.getPlatform() === 'ios') { + return { + ...baseOptions, + // iOS-specific options + cameraOptions: { + quality: 0.7, // Lower quality for better performance + allowEditing: false, + resultType: 'uri' + } + }; + } else if (Capacitor.getPlatform() === 'android') { + return { + ...baseOptions, + // Android-specific options + cameraOptions: { + quality: 0.8, + allowEditing: false, + resultType: 'uri', + saveToGallery: false + } + }; + } + + return baseOptions; + } + ``` + +#### Performance Optimization + +1. **Battery Usage** + ```typescript + // CapacitorQRScanner.ts + private optimizeBatteryUsage(): void { + // Reduce scan frequency when battery is low + if (this.isLowBattery()) { + this.scanInterval = 2000; // 2 seconds between scans + } else { + this.scanInterval = 1000; // 1 second between scans + } + } + + private isLowBattery(): boolean { + // Check battery level if available + if (Capacitor.isPluginAvailable('Battery')) { + const { level } = await Battery.getBatteryLevel(); + return level < 0.2; // 20% or lower + } + return false; + } + ``` + +2. **Memory Management** + ```typescript + // CapacitorQRScanner.ts + private async cleanupResources(): Promise { + // Stop scanning + await this.stopScan(); + + // Clear any stored camera data + this.currentLensFacing = LensFacing.Back; + + // Remove listeners + this.listenerHandles.forEach(handle => handle()); + this.listenerHandles = []; + + // Reset state + this.isScanning = false; + this.updateCameraState('off'); + } + ``` + +#### Testing on Capacitor + +1. **Device Testing** + - Test on multiple iOS devices + - Test on multiple Android devices + - Test different camera configurations + - Test with different screen sizes + - Test with different OS versions + +2. **Camera Testing** + - Test front camera switching + - Test back camera switching + - Test camera permissions + - Test camera in use scenarios + - Test low light conditions + - Test different QR code sizes + - Test different QR code distances + +3. **Performance Testing** + - Battery usage monitoring + - Memory usage monitoring + - Camera switching speed + - QR code detection speed + - App responsiveness + - Background/foreground transitions \ No newline at end of file