@ -54,6 +54,9 @@ interface QRScannerService {
addListener(listener: ScanListener): void;
onStream(callback: (stream: MediaStream | null) => void): void;
cleanup(): Promise< void > ;
getAvailableCameras(): Promise< MediaDeviceInfo [ ] > ;
switchCamera(deviceId: string): Promise< void > ;
getCurrentCamera(): Promise< MediaDeviceInfo | null > ;
}
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
- 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< void > {
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< MediaDeviceInfo [ ] > {
const devices = await navigator.mediaDevices.enumerateDevices();
return devices.filter(device => device.kind === 'videoinput');
}
async switchCamera(deviceId: string): Promise< void > {
// 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< MediaDeviceInfo [ ] > ;
/** Switch to a specific camera */
switchCamera(deviceId: string): Promise< void > ;
/** Get current camera info */
getCurrentCamera(): Promise< MediaDeviceInfo | null > ;
}
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
< button
v-if="isNativePlatform"
@click ="toggleMobileCamera"
class="camera-switch-btn"
>
< font-awesome icon = "camera-rotate" / >
Switch Camera
< / button >
```
2. **Desktop Interface**
- Dropdown menu with all available cameras
- Camera labels and device IDs
- Real-time camera switching
- Responsive design
```vue
< select
v-model="selectedCameraId"
@change ="onCameraChange"
class="camera-select-dropdown"
>
< option
v-for="camera in availableCameras"
:key="camera.deviceId"
:value="camera.deviceId"
>
{{ camera.label || `Camera ${camera.deviceId.slice(0, 4)}` }}
< / option >
< / select >
```
### 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< void > {
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< MediaDeviceInfo [ ] > {
// 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< void > {
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< MediaDeviceInfo | null > {
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< void > {
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< boolean > {
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< void > {
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
<!-- ios/App/App/Info.plist -->
< key > NSCameraUsageDescription< / key >
< string > We need access to your camera to scan QR codes< / string >
< key > NSPhotoLibraryUsageDescription< / key >
< string > We need access to save scanned QR codes< / string >
```
2. **Android Implementation**
- Camera permissions in AndroidManifest.xml
- Runtime permission handling
- Camera features declaration
- Hardware feature requirements
```xml
<!-- android/app/src/main/AndroidManifest.xml -->
< uses-permission android:name = "android.permission.CAMERA" / >
< uses-feature android:name = "android.hardware.camera" / >
< uses-feature android:name = "android.hardware.camera.autofocus" / >
```
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< void > {
// 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