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.
This commit is contained in:
@@ -54,6 +54,9 @@ interface QRScannerService {
|
|||||||
addListener(listener: ScanListener): void;
|
addListener(listener: ScanListener): void;
|
||||||
onStream(callback: (stream: MediaStream | null) => void): void;
|
onStream(callback: (stream: MediaStream | null) => void): void;
|
||||||
cleanup(): Promise<void>;
|
cleanup(): Promise<void>;
|
||||||
|
getAvailableCameras(): Promise<MediaDeviceInfo[]>;
|
||||||
|
switchCamera(deviceId: string): Promise<void>;
|
||||||
|
getCurrentCamera(): Promise<MediaDeviceInfo | null>;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ScanListener {
|
interface ScanListener {
|
||||||
@@ -87,7 +90,15 @@ const config: CapacitorConfig = {
|
|||||||
formats: ['QR_CODE'],
|
formats: ['QR_CODE'],
|
||||||
detectorSize: 1.0,
|
detectorSize: 1.0,
|
||||||
lensFacing: 'back',
|
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
|
- Track memory usage
|
||||||
- Monitor camera performance
|
- Monitor camera performance
|
||||||
- Check error rates
|
- 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
|
||||||
Reference in New Issue
Block a user