forked from trent_larson/crowd-funder-for-time-pwa
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;
|
||||
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
|
||||
Reference in New Issue
Block a user