capacitor-camera-view 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/CapacitorCameraView.podspec +17 -0
  2. package/LICENSE +201 -0
  3. package/Package.swift +28 -0
  4. package/README.md +654 -0
  5. package/android/build.gradle +79 -0
  6. package/android/src/main/AndroidManifest.xml +2 -0
  7. package/android/src/main/java/com/michaelwolz/capacitorcameraview/CameraView.kt +555 -0
  8. package/android/src/main/java/com/michaelwolz/capacitorcameraview/CameraViewPlugin.kt +227 -0
  9. package/android/src/main/java/com/michaelwolz/capacitorcameraview/model/BarcodeDetectionResult.kt +11 -0
  10. package/android/src/main/java/com/michaelwolz/capacitorcameraview/model/CameraDevice.kt +14 -0
  11. package/android/src/main/java/com/michaelwolz/capacitorcameraview/model/CameraSessionConfiguration.kt +10 -0
  12. package/android/src/main/java/com/michaelwolz/capacitorcameraview/model/WebBoundingRect.kt +16 -0
  13. package/android/src/main/java/com/michaelwolz/capacitorcameraview/model/ZoomFactors.kt +14 -0
  14. package/android/src/main/java/com/michaelwolz/capacitorcameraview/utils.kt +86 -0
  15. package/android/src/main/res/.gitkeep +0 -0
  16. package/dist/docs.json +968 -0
  17. package/dist/esm/definitions.d.ts +378 -0
  18. package/dist/esm/definitions.js +2 -0
  19. package/dist/esm/definitions.js.map +1 -0
  20. package/dist/esm/index.d.ts +7 -0
  21. package/dist/esm/index.js +10 -0
  22. package/dist/esm/index.js.map +1 -0
  23. package/dist/esm/utils.d.ts +45 -0
  24. package/dist/esm/utils.js +108 -0
  25. package/dist/esm/utils.js.map +1 -0
  26. package/dist/esm/web.d.ts +108 -0
  27. package/dist/esm/web.js +406 -0
  28. package/dist/esm/web.js.map +1 -0
  29. package/dist/plugin.cjs.js +530 -0
  30. package/dist/plugin.cjs.js.map +1 -0
  31. package/dist/plugin.js +533 -0
  32. package/dist/plugin.js.map +1 -0
  33. package/ios/Sources/CameraViewPlugin/CameraError.swift +39 -0
  34. package/ios/Sources/CameraViewPlugin/CameraSessionConfiguration.swift +32 -0
  35. package/ios/Sources/CameraViewPlugin/CameraViewManager+BarcodeScan.swift +91 -0
  36. package/ios/Sources/CameraViewPlugin/CameraViewManager+PhotoCapture.swift +52 -0
  37. package/ios/Sources/CameraViewPlugin/CameraViewManager+VideoDataOutput.swift +78 -0
  38. package/ios/Sources/CameraViewPlugin/CameraViewManager.swift +633 -0
  39. package/ios/Sources/CameraViewPlugin/CameraViewPlugin.swift +295 -0
  40. package/ios/Sources/CameraViewPlugin/Utils.swift +56 -0
  41. package/ios/Tests/CameraViewPluginTests/CameraViewPluginTests.swift +15 -0
  42. package/package.json +94 -0
@@ -0,0 +1,108 @@
1
+ import { WebPlugin } from '@capacitor/core';
2
+ import type { CameraSessionConfiguration, CameraViewPlugin, GetAvailableDevicesResponse, GetFlashModeResponse, GetSupportedFlashModesResponse, GetZoomResponse, IsRunningResponse, PermissionStatus, CaptureResponse, FlashMode } from './definitions';
3
+ /**
4
+ * Web implementation of the CameraViewPlugin.
5
+ * Optimized for performance and battery efficiency.
6
+ */
7
+ export declare class CameraViewWeb extends WebPlugin implements CameraViewPlugin {
8
+ #private;
9
+ private videoElement;
10
+ private canvasElement;
11
+ private stream;
12
+ private currentCamera;
13
+ private currentZoom;
14
+ private currentFlashMode;
15
+ private barcodeDetectionSupported;
16
+ private barcodeDetector;
17
+ constructor();
18
+ /**
19
+ * Start the camera with the given configuration
20
+ */
21
+ start(options?: CameraSessionConfiguration): Promise<void>;
22
+ /**
23
+ * Stop the camera and release resources
24
+ */
25
+ stop(): Promise<void>;
26
+ /**
27
+ * Check if the camera is currently running
28
+ */
29
+ isRunning(): Promise<IsRunningResponse>;
30
+ /**
31
+ * Capture a photo using the camera and return it as a base64-encoded JPEG image.
32
+ * Preserves what the user actually sees in the UI, including cropping from object-fit: cover.
33
+ */
34
+ capture(options: {
35
+ quality: number;
36
+ }): Promise<CaptureResponse>;
37
+ /**
38
+ * Web implementation already uses images from the video stream, so this is the same as `capture()`
39
+ */
40
+ captureSample(options: {
41
+ quality: number;
42
+ }): Promise<CaptureResponse>;
43
+ /**
44
+ * Flip between front and back camera
45
+ */
46
+ flipCamera(): Promise<void>;
47
+ /**
48
+ * Get available camera devices
49
+ */
50
+ getAvailableDevices(): Promise<GetAvailableDevicesResponse>;
51
+ /**
52
+ * Get current zoom information (web has limited zoom support)
53
+ */
54
+ getZoom(): Promise<GetZoomResponse>;
55
+ /**
56
+ * Set zoom level (limited support in web)
57
+ */
58
+ setZoom(options: {
59
+ level: number;
60
+ ramp?: boolean;
61
+ }): Promise<void>;
62
+ /**
63
+ * Get current flash mode
64
+ */
65
+ getFlashMode(): Promise<GetFlashModeResponse>;
66
+ /**
67
+ * Get supported flash modes
68
+ */
69
+ getSupportedFlashModes(): Promise<GetSupportedFlashModesResponse>;
70
+ /**
71
+ * Set flash mode (limited support in web)
72
+ */
73
+ setFlashMode(options: {
74
+ mode: FlashMode;
75
+ }): Promise<void>;
76
+ /**
77
+ * Check camera permission without requesting
78
+ */
79
+ checkPermissions(): Promise<PermissionStatus>;
80
+ /**
81
+ * Request camera permission from the user
82
+ */
83
+ requestPermissions(): Promise<PermissionStatus>;
84
+ /**
85
+ * Start barcode detection if supported
86
+ */
87
+ private startBarcodeDetection;
88
+ /**
89
+ * Clean up resources when the plugin is disposed
90
+ */
91
+ handleOnDestroy(): Promise<void>;
92
+ /**
93
+ * Check if barcode detection is supported in this browser
94
+ */
95
+ private checkBarcodeDetectionSupport;
96
+ /**
97
+ * Set up the video element for the camera view
98
+ */
99
+ private setupVideoElement;
100
+ /**
101
+ * Ensures canvas element exists and returns it
102
+ */
103
+ private getCanvasElement;
104
+ /**
105
+ * Format error message
106
+ */
107
+ private formatError;
108
+ }
@@ -0,0 +1,406 @@
1
+ var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, privateMap) {
2
+ if (!privateMap.has(receiver)) {
3
+ throw new TypeError("attempted to get private field on non-instance");
4
+ }
5
+ return privateMap.get(receiver);
6
+ };
7
+ var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, privateMap, value) {
8
+ if (!privateMap.has(receiver)) {
9
+ throw new TypeError("attempted to set private field on non-instance");
10
+ }
11
+ privateMap.set(receiver, value);
12
+ return value;
13
+ };
14
+ var _isRunning;
15
+ import { WebPlugin } from '@capacitor/core';
16
+ import { calculateVisibleArea, canvasToBase64, drawVisibleAreaToCanvas, transformBarcodeBoundingBox } from './utils';
17
+ /**
18
+ * Web implementation of the CameraViewPlugin.
19
+ * Optimized for performance and battery efficiency.
20
+ */
21
+ export class CameraViewWeb extends WebPlugin {
22
+ constructor() {
23
+ super();
24
+ // DOM elements
25
+ this.videoElement = null;
26
+ this.canvasElement = null;
27
+ // Stream state
28
+ this.stream = null;
29
+ _isRunning.set(this, false);
30
+ // Configuration state
31
+ this.currentCamera = 'environment'; // Default to back camera
32
+ this.currentZoom = 1.0;
33
+ this.currentFlashMode = 'off';
34
+ // Barcode detection support
35
+ this.barcodeDetectionSupported = false;
36
+ this.barcodeDetector = null;
37
+ this.checkBarcodeDetectionSupport();
38
+ }
39
+ /**
40
+ * Start the camera with the given configuration
41
+ */
42
+ async start(options) {
43
+ if (__classPrivateFieldGet(this, _isRunning)) {
44
+ return;
45
+ }
46
+ const permissionStatus = await this.requestPermissions();
47
+ if (permissionStatus.camera !== 'granted') {
48
+ throw new Error('Camera permission was not granted');
49
+ }
50
+ try {
51
+ // Set up video element if it doesn't exist
52
+ if (!this.videoElement) {
53
+ await this.setupVideoElement(options === null || options === void 0 ? void 0 : options.containerElementId);
54
+ }
55
+ // Set up video constraints based on options
56
+ const videoConstraints = {};
57
+ // Prefer deviceId if specified
58
+ if (options === null || options === void 0 ? void 0 : options.deviceId) {
59
+ videoConstraints.deviceId = { exact: options.deviceId };
60
+ // Remember the current camera mode (though we're using a specific device)
61
+ this.currentCamera = (options === null || options === void 0 ? void 0 : options.position) === 'front' ? 'user' : 'environment';
62
+ }
63
+ else {
64
+ // Fall back to facing mode
65
+ const facingMode = (options === null || options === void 0 ? void 0 : options.position) === 'front' ? 'user' : 'environment';
66
+ this.currentCamera = facingMode;
67
+ videoConstraints.facingMode = facingMode;
68
+ }
69
+ const constraints = {
70
+ video: videoConstraints,
71
+ audio: false,
72
+ };
73
+ this.stream = await navigator.mediaDevices.getUserMedia(constraints);
74
+ if (this.videoElement) {
75
+ this.videoElement.srcObject = this.stream;
76
+ this.videoElement.play();
77
+ __classPrivateFieldSet(this, _isRunning, true);
78
+ // If barcode detection is enabled and supported, start detection
79
+ if ((options === null || options === void 0 ? void 0 : options.enableBarcodeDetection) && this.barcodeDetectionSupported) {
80
+ this.startBarcodeDetection();
81
+ }
82
+ }
83
+ }
84
+ catch (err) {
85
+ throw new Error(`Failed to start camera: ${this.formatError(err)}`);
86
+ }
87
+ }
88
+ /**
89
+ * Stop the camera and release resources
90
+ */
91
+ async stop() {
92
+ if (!__classPrivateFieldGet(this, _isRunning)) {
93
+ return;
94
+ }
95
+ try {
96
+ // Stop all tracks in the stream
97
+ if (this.stream) {
98
+ this.stream.getTracks().forEach((track) => track.stop());
99
+ this.stream = null;
100
+ }
101
+ // Clear video source
102
+ if (this.videoElement) {
103
+ this.videoElement = null;
104
+ }
105
+ __classPrivateFieldSet(this, _isRunning, false);
106
+ }
107
+ catch (err) {
108
+ throw new Error(`Failed to stop camera: ${this.formatError(err)}`);
109
+ }
110
+ }
111
+ /**
112
+ * Check if the camera is currently running
113
+ */
114
+ async isRunning() {
115
+ return { isRunning: __classPrivateFieldGet(this, _isRunning) };
116
+ }
117
+ /**
118
+ * Capture a photo using the camera and return it as a base64-encoded JPEG image.
119
+ * Preserves what the user actually sees in the UI, including cropping from object-fit: cover.
120
+ */
121
+ async capture(options) {
122
+ const videoElement = this.videoElement;
123
+ if (!__classPrivateFieldGet(this, _isRunning) || !videoElement) {
124
+ throw new Error('Camera is not running');
125
+ }
126
+ try {
127
+ const canvas = this.getCanvasElement();
128
+ const visibleArea = calculateVisibleArea(videoElement);
129
+ drawVisibleAreaToCanvas(canvas, videoElement, visibleArea);
130
+ const quality = Math.min(1.0, Math.max(0.1, options.quality / 100));
131
+ const base64Data = canvasToBase64(canvas, quality);
132
+ return { photo: base64Data };
133
+ }
134
+ catch (err) {
135
+ throw new Error(`Failed to capture photo: ${this.formatError(err)}`);
136
+ }
137
+ }
138
+ /**
139
+ * Web implementation already uses images from the video stream, so this is the same as `capture()`
140
+ */
141
+ async captureSample(options) {
142
+ return this.capture(options);
143
+ }
144
+ /**
145
+ * Flip between front and back camera
146
+ */
147
+ async flipCamera() {
148
+ if (!__classPrivateFieldGet(this, _isRunning)) {
149
+ throw new Error('Camera is not running');
150
+ }
151
+ try {
152
+ // Switch current camera
153
+ this.currentCamera = this.currentCamera === 'user' ? 'environment' : 'user';
154
+ // Stop current stream
155
+ if (this.stream) {
156
+ this.stream.getTracks().forEach((track) => track.stop());
157
+ }
158
+ // Restart with new facing mode
159
+ const constraints = {
160
+ video: {
161
+ facingMode: this.currentCamera,
162
+ },
163
+ audio: false,
164
+ };
165
+ this.stream = await navigator.mediaDevices.getUserMedia(constraints);
166
+ if (this.videoElement) {
167
+ this.videoElement.srcObject = this.stream;
168
+ }
169
+ }
170
+ catch (err) {
171
+ throw new Error(`Failed to flip camera: ${this.formatError(err)}`);
172
+ }
173
+ }
174
+ /**
175
+ * Get available camera devices
176
+ */
177
+ async getAvailableDevices() {
178
+ try {
179
+ const devices = await navigator.mediaDevices.enumerateDevices();
180
+ const videoDevices = devices.filter((device) => device.kind === 'videoinput');
181
+ return {
182
+ devices: videoDevices.map((device) => ({
183
+ id: device.deviceId,
184
+ name: device.label || `Camera ${device.deviceId.substring(0, 5)}`,
185
+ position: device.label.toLowerCase().includes('front') ? 'front' : 'back',
186
+ })),
187
+ };
188
+ }
189
+ catch (err) {
190
+ console.error('Failed to get available devices', err);
191
+ return { devices: [] };
192
+ }
193
+ }
194
+ /**
195
+ * Get current zoom information (web has limited zoom support)
196
+ */
197
+ async getZoom() {
198
+ // Web has limited zoom capabilities in most browsers,
199
+ // we fake zoomin by scaling the video element
200
+ return {
201
+ min: 1.0,
202
+ max: 3.0,
203
+ current: this.currentZoom,
204
+ };
205
+ }
206
+ /**
207
+ * Set zoom level (limited support in web)
208
+ */
209
+ async setZoom(options) {
210
+ // Store the requested zoom level
211
+ this.currentZoom = options.level;
212
+ // Apply visual zoom using CSS transform when native zoom isn't supported
213
+ if (this.videoElement) {
214
+ this.videoElement.style.transition = options.ramp ? 'transform 0.2s ease-in-out' : 'none';
215
+ const scale = Math.max(1.0, Math.min(options.level, 3.0)); // Limit scale to reasonable bounds
216
+ this.videoElement.style.transform = `scale(${scale})`;
217
+ this.videoElement.style.transformOrigin = 'center';
218
+ }
219
+ }
220
+ /**
221
+ * Get current flash mode
222
+ */
223
+ async getFlashMode() {
224
+ return { flashMode: this.currentFlashMode };
225
+ }
226
+ /**
227
+ * Get supported flash modes
228
+ */
229
+ async getSupportedFlashModes() {
230
+ // Web has limited flash control
231
+ return { flashModes: ['off'] };
232
+ }
233
+ /**
234
+ * Set flash mode (limited support in web)
235
+ */
236
+ async setFlashMode(options) {
237
+ this.currentFlashMode = options.mode;
238
+ console.warn('Flash mode control is not fully supported in the web implementation');
239
+ }
240
+ /**
241
+ * Check camera permission without requesting
242
+ */
243
+ async checkPermissions() {
244
+ try {
245
+ // Use Permissions API if available
246
+ if (navigator.permissions) {
247
+ const result = await navigator.permissions.query({ name: 'camera' });
248
+ return {
249
+ camera: result.state === 'granted' ? 'granted' : result.state === 'denied' ? 'denied' : 'prompt',
250
+ };
251
+ }
252
+ // If Permissions API is not available, check if we have an active stream
253
+ return {
254
+ camera: this.stream ? 'granted' : 'prompt',
255
+ };
256
+ }
257
+ catch (err) {
258
+ // If permissions API is not supported or fails
259
+ return {
260
+ camera: 'prompt',
261
+ };
262
+ }
263
+ }
264
+ /**
265
+ * Request camera permission from the user
266
+ */
267
+ async requestPermissions() {
268
+ try {
269
+ // Try to access the camera to trigger the permission prompt
270
+ const stream = await navigator.mediaDevices.getUserMedia({ video: true });
271
+ // If we get here, permission was granted
272
+ // Clean up the test stream
273
+ stream.getTracks().forEach((track) => track.stop());
274
+ return { camera: 'granted' };
275
+ }
276
+ catch (err) {
277
+ // Permission denied or other error
278
+ return { camera: 'denied' };
279
+ }
280
+ }
281
+ /**
282
+ * Start barcode detection if supported
283
+ */
284
+ async startBarcodeDetection() {
285
+ const barcodeDetector = this.barcodeDetector;
286
+ const videoElement = this.videoElement;
287
+ if (!this.barcodeDetectionSupported || !barcodeDetector || !videoElement) {
288
+ return;
289
+ }
290
+ // Make sure video is fully loaded before starting detection
291
+ if (videoElement.readyState < 2) {
292
+ await new Promise((resolve) => {
293
+ const loadHandler = () => {
294
+ videoElement.removeEventListener('loadeddata', loadHandler);
295
+ resolve();
296
+ };
297
+ videoElement.addEventListener('loadeddata', loadHandler);
298
+ });
299
+ }
300
+ // Add throttling to reduce CPU usage
301
+ let lastDetectionTime = 0;
302
+ const minTimeBetweenDetections = 100; // ms
303
+ // Set up periodic frame analysis for barcode detection
304
+ const detectFrame = async () => {
305
+ if (!__classPrivateFieldGet(this, _isRunning) || !videoElement || !barcodeDetector) {
306
+ return;
307
+ }
308
+ const now = Date.now();
309
+ if (now - lastDetectionTime >= minTimeBetweenDetections) {
310
+ try {
311
+ const barcodes = await barcodeDetector.detect(videoElement);
312
+ lastDetectionTime = now;
313
+ if (barcodes.length > 0) {
314
+ const barcode = barcodes[0];
315
+ // Transform barcode coordinates using the utility function
316
+ const boundingRect = transformBarcodeBoundingBox(barcode.boundingBox, videoElement);
317
+ this.notifyListeners('barcodeDetected', {
318
+ value: barcode.rawValue,
319
+ type: barcode.format.toLowerCase(),
320
+ boundingRect,
321
+ });
322
+ }
323
+ }
324
+ catch (err) {
325
+ console.error('Barcode detection error', err);
326
+ }
327
+ }
328
+ if (__classPrivateFieldGet(this, _isRunning)) {
329
+ requestAnimationFrame(detectFrame);
330
+ }
331
+ };
332
+ requestAnimationFrame(detectFrame);
333
+ }
334
+ /**
335
+ * Clean up resources when the plugin is disposed
336
+ */
337
+ async handleOnDestroy() {
338
+ var _a;
339
+ await this.stop();
340
+ // Remove elements from DOM
341
+ if ((_a = this.videoElement) === null || _a === void 0 ? void 0 : _a.parentNode) {
342
+ this.videoElement.parentNode.removeChild(this.videoElement);
343
+ this.videoElement = null;
344
+ }
345
+ if (this.canvasElement) {
346
+ this.canvasElement = null;
347
+ }
348
+ this.barcodeDetector = null;
349
+ }
350
+ /**
351
+ * Check if barcode detection is supported in this browser
352
+ */
353
+ async checkBarcodeDetectionSupport() {
354
+ if ('BarcodeDetector' in window) {
355
+ try {
356
+ this.barcodeDetector = new BarcodeDetector();
357
+ this.barcodeDetectionSupported = true;
358
+ }
359
+ catch (e) {
360
+ console.warn('BarcodeDetector is not supported by this browser.');
361
+ this.barcodeDetectionSupported = false;
362
+ }
363
+ }
364
+ }
365
+ /**
366
+ * Set up the video element for the camera view
367
+ */
368
+ async setupVideoElement(containerElementId) {
369
+ this.videoElement = document.createElement('video');
370
+ this.videoElement.playsInline = true;
371
+ this.videoElement.autoplay = true;
372
+ this.videoElement.muted = true;
373
+ this.videoElement.style.width = '100%';
374
+ this.videoElement.style.height = '100%';
375
+ this.videoElement.style.objectFit = 'cover';
376
+ // If a container ID is provided, find that element and append the video to it
377
+ if (containerElementId) {
378
+ const container = document.getElementById(containerElementId);
379
+ if (!container) {
380
+ throw new Error(`Container element with ID ${containerElementId} not found`);
381
+ }
382
+ container.appendChild(this.videoElement);
383
+ }
384
+ else {
385
+ // Otherwise, append to body as fallback
386
+ document.body.appendChild(this.videoElement);
387
+ }
388
+ }
389
+ /**
390
+ * Ensures canvas element exists and returns it
391
+ */
392
+ getCanvasElement() {
393
+ if (!this.canvasElement) {
394
+ this.canvasElement = document.createElement('canvas');
395
+ }
396
+ return this.canvasElement;
397
+ }
398
+ /**
399
+ * Format error message
400
+ */
401
+ formatError(err) {
402
+ return err instanceof Error ? err.message : String(err);
403
+ }
404
+ }
405
+ _isRunning = new WeakMap();
406
+ //# sourceMappingURL=web.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"web.js","sourceRoot":"","sources":["../../src/web.ts"],"names":[],"mappings":"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","sourcesContent":["import { WebPlugin } from '@capacitor/core';\n\nimport type {\n CameraSessionConfiguration,\n CameraViewPlugin,\n GetAvailableDevicesResponse,\n GetFlashModeResponse,\n GetSupportedFlashModesResponse,\n GetZoomResponse,\n IsRunningResponse,\n PermissionStatus,\n CaptureResponse,\n FlashMode,\n} from './definitions';\nimport { calculateVisibleArea, canvasToBase64, drawVisibleAreaToCanvas, transformBarcodeBoundingBox } from './utils';\n\n/**\n * Web implementation of the CameraViewPlugin.\n * Optimized for performance and battery efficiency.\n */\nexport class CameraViewWeb extends WebPlugin implements CameraViewPlugin {\n // DOM elements\n private videoElement: HTMLVideoElement | null = null;\n private canvasElement: HTMLCanvasElement | null = null;\n\n // Stream state\n private stream: MediaStream | null = null;\n #isRunning = false;\n\n // Configuration state\n private currentCamera = 'environment'; // Default to back camera\n private currentZoom = 1.0;\n private currentFlashMode: FlashMode = 'off';\n\n // Barcode detection support\n private barcodeDetectionSupported = false;\n private barcodeDetector: BarcodeDetector | null = null;\n\n constructor() {\n super();\n this.checkBarcodeDetectionSupport();\n }\n\n /**\n * Start the camera with the given configuration\n */\n async start(options?: CameraSessionConfiguration): Promise<void> {\n if (this.#isRunning) {\n return;\n }\n\n const permissionStatus = await this.requestPermissions();\n if (permissionStatus.camera !== 'granted') {\n throw new Error('Camera permission was not granted');\n }\n\n try {\n // Set up video element if it doesn't exist\n if (!this.videoElement) {\n await this.setupVideoElement(options?.containerElementId);\n }\n\n // Set up video constraints based on options\n const videoConstraints: MediaTrackConstraints = {};\n\n // Prefer deviceId if specified\n if (options?.deviceId) {\n videoConstraints.deviceId = { exact: options.deviceId };\n // Remember the current camera mode (though we're using a specific device)\n this.currentCamera = options?.position === 'front' ? 'user' : 'environment';\n } else {\n // Fall back to facing mode\n const facingMode = options?.position === 'front' ? 'user' : 'environment';\n this.currentCamera = facingMode;\n videoConstraints.facingMode = facingMode;\n }\n\n const constraints: MediaStreamConstraints = {\n video: videoConstraints,\n audio: false,\n };\n\n this.stream = await navigator.mediaDevices.getUserMedia(constraints);\n\n if (this.videoElement) {\n this.videoElement.srcObject = this.stream;\n this.videoElement.play();\n this.#isRunning = true;\n\n // If barcode detection is enabled and supported, start detection\n if (options?.enableBarcodeDetection && this.barcodeDetectionSupported) {\n this.startBarcodeDetection();\n }\n }\n } catch (err) {\n throw new Error(`Failed to start camera: ${this.formatError(err)}`);\n }\n }\n\n /**\n * Stop the camera and release resources\n */\n async stop(): Promise<void> {\n if (!this.#isRunning) {\n return;\n }\n\n try {\n // Stop all tracks in the stream\n if (this.stream) {\n this.stream.getTracks().forEach((track) => track.stop());\n this.stream = null;\n }\n\n // Clear video source\n if (this.videoElement) {\n this.videoElement = null;\n }\n\n this.#isRunning = false;\n } catch (err) {\n throw new Error(`Failed to stop camera: ${this.formatError(err)}`);\n }\n }\n\n /**\n * Check if the camera is currently running\n */\n async isRunning(): Promise<IsRunningResponse> {\n return { isRunning: this.#isRunning };\n }\n\n /**\n * Capture a photo using the camera and return it as a base64-encoded JPEG image.\n * Preserves what the user actually sees in the UI, including cropping from object-fit: cover.\n */\n async capture(options: { quality: number }): Promise<CaptureResponse> {\n const videoElement = this.videoElement;\n\n if (!this.#isRunning || !videoElement) {\n throw new Error('Camera is not running');\n }\n\n try {\n const canvas = this.getCanvasElement();\n const visibleArea = calculateVisibleArea(videoElement);\n\n drawVisibleAreaToCanvas(canvas, videoElement, visibleArea);\n\n const quality = Math.min(1.0, Math.max(0.1, options.quality / 100));\n const base64Data = canvasToBase64(canvas, quality);\n\n return { photo: base64Data };\n } catch (err) {\n throw new Error(`Failed to capture photo: ${this.formatError(err)}`);\n }\n }\n\n /**\n * Web implementation already uses images from the video stream, so this is the same as `capture()`\n */\n async captureSample(options: { quality: number }): Promise<CaptureResponse> {\n return this.capture(options);\n }\n\n /**\n * Flip between front and back camera\n */\n public async flipCamera(): Promise<void> {\n if (!this.#isRunning) {\n throw new Error('Camera is not running');\n }\n\n try {\n // Switch current camera\n this.currentCamera = this.currentCamera === 'user' ? 'environment' : 'user';\n\n // Stop current stream\n if (this.stream) {\n this.stream.getTracks().forEach((track) => track.stop());\n }\n\n // Restart with new facing mode\n const constraints: MediaStreamConstraints = {\n video: {\n facingMode: this.currentCamera,\n },\n audio: false,\n };\n\n this.stream = await navigator.mediaDevices.getUserMedia(constraints);\n\n if (this.videoElement) {\n this.videoElement.srcObject = this.stream;\n }\n } catch (err) {\n throw new Error(`Failed to flip camera: ${this.formatError(err)}`);\n }\n }\n\n /**\n * Get available camera devices\n */\n public async getAvailableDevices(): Promise<GetAvailableDevicesResponse> {\n try {\n const devices = await navigator.mediaDevices.enumerateDevices();\n const videoDevices = devices.filter((device) => device.kind === 'videoinput');\n\n return {\n devices: videoDevices.map((device) => ({\n id: device.deviceId,\n name: device.label || `Camera ${device.deviceId.substring(0, 5)}`,\n position: device.label.toLowerCase().includes('front') ? 'front' : 'back',\n })),\n };\n } catch (err) {\n console.error('Failed to get available devices', err);\n return { devices: [] };\n }\n }\n\n /**\n * Get current zoom information (web has limited zoom support)\n */\n public async getZoom(): Promise<GetZoomResponse> {\n // Web has limited zoom capabilities in most browsers,\n // we fake zoomin by scaling the video element\n return {\n min: 1.0,\n max: 3.0,\n current: this.currentZoom,\n };\n }\n\n /**\n * Set zoom level (limited support in web)\n */\n public async setZoom(options: { level: number; ramp?: boolean }): Promise<void> {\n // Store the requested zoom level\n this.currentZoom = options.level;\n\n // Apply visual zoom using CSS transform when native zoom isn't supported\n if (this.videoElement) {\n this.videoElement.style.transition = options.ramp ? 'transform 0.2s ease-in-out' : 'none';\n const scale = Math.max(1.0, Math.min(options.level, 3.0)); // Limit scale to reasonable bounds\n this.videoElement.style.transform = `scale(${scale})`;\n this.videoElement.style.transformOrigin = 'center';\n }\n }\n\n /**\n * Get current flash mode\n */\n public async getFlashMode(): Promise<GetFlashModeResponse> {\n return { flashMode: this.currentFlashMode };\n }\n\n /**\n * Get supported flash modes\n */\n public async getSupportedFlashModes(): Promise<GetSupportedFlashModesResponse> {\n // Web has limited flash control\n return { flashModes: ['off'] };\n }\n\n /**\n * Set flash mode (limited support in web)\n */\n public async setFlashMode(options: { mode: FlashMode }): Promise<void> {\n this.currentFlashMode = options.mode;\n console.warn('Flash mode control is not fully supported in the web implementation');\n }\n\n /**\n * Check camera permission without requesting\n */\n public async checkPermissions(): Promise<PermissionStatus> {\n try {\n // Use Permissions API if available\n if (navigator.permissions) {\n const result = await navigator.permissions.query({ name: 'camera' as PermissionName });\n return {\n camera: result.state === 'granted' ? 'granted' : result.state === 'denied' ? 'denied' : 'prompt',\n };\n }\n\n // If Permissions API is not available, check if we have an active stream\n return {\n camera: this.stream ? 'granted' : 'prompt',\n };\n } catch (err) {\n // If permissions API is not supported or fails\n return {\n camera: 'prompt',\n };\n }\n }\n\n /**\n * Request camera permission from the user\n */\n public async requestPermissions(): Promise<PermissionStatus> {\n try {\n // Try to access the camera to trigger the permission prompt\n const stream = await navigator.mediaDevices.getUserMedia({ video: true });\n\n // If we get here, permission was granted\n // Clean up the test stream\n stream.getTracks().forEach((track) => track.stop());\n\n return { camera: 'granted' };\n } catch (err) {\n // Permission denied or other error\n return { camera: 'denied' };\n }\n }\n\n /**\n * Start barcode detection if supported\n */\n private async startBarcodeDetection() {\n const barcodeDetector = this.barcodeDetector;\n const videoElement = this.videoElement;\n\n if (!this.barcodeDetectionSupported || !barcodeDetector || !videoElement) {\n return;\n }\n\n // Make sure video is fully loaded before starting detection\n if (videoElement.readyState < 2) {\n await new Promise<void>((resolve) => {\n const loadHandler = () => {\n videoElement.removeEventListener('loadeddata', loadHandler);\n resolve();\n };\n videoElement.addEventListener('loadeddata', loadHandler);\n });\n }\n\n // Add throttling to reduce CPU usage\n let lastDetectionTime = 0;\n const minTimeBetweenDetections = 100; // ms\n\n // Set up periodic frame analysis for barcode detection\n const detectFrame = async () => {\n if (!this.#isRunning || !videoElement || !barcodeDetector) {\n return;\n }\n\n const now = Date.now();\n if (now - lastDetectionTime >= minTimeBetweenDetections) {\n try {\n const barcodes = await barcodeDetector.detect(videoElement);\n lastDetectionTime = now;\n\n if (barcodes.length > 0) {\n const barcode = barcodes[0];\n\n // Transform barcode coordinates using the utility function\n const boundingRect = transformBarcodeBoundingBox(barcode.boundingBox, videoElement);\n\n this.notifyListeners('barcodeDetected', {\n value: barcode.rawValue,\n type: barcode.format.toLowerCase(),\n boundingRect,\n });\n }\n } catch (err) {\n console.error('Barcode detection error', err);\n }\n }\n\n if (this.#isRunning) {\n requestAnimationFrame(detectFrame);\n }\n };\n\n requestAnimationFrame(detectFrame);\n }\n\n /**\n * Clean up resources when the plugin is disposed\n */\n public async handleOnDestroy(): Promise<void> {\n await this.stop();\n\n // Remove elements from DOM\n if (this.videoElement?.parentNode) {\n this.videoElement.parentNode.removeChild(this.videoElement);\n this.videoElement = null;\n }\n\n if (this.canvasElement) {\n this.canvasElement = null;\n }\n\n this.barcodeDetector = null;\n }\n\n /**\n * Check if barcode detection is supported in this browser\n */\n private async checkBarcodeDetectionSupport() {\n if ('BarcodeDetector' in window) {\n try {\n this.barcodeDetector = new BarcodeDetector();\n this.barcodeDetectionSupported = true;\n } catch (e) {\n console.warn('BarcodeDetector is not supported by this browser.');\n this.barcodeDetectionSupported = false;\n }\n }\n }\n\n /**\n * Set up the video element for the camera view\n */\n private async setupVideoElement(containerElementId?: string) {\n this.videoElement = document.createElement('video');\n this.videoElement.playsInline = true;\n this.videoElement.autoplay = true;\n this.videoElement.muted = true;\n this.videoElement.style.width = '100%';\n this.videoElement.style.height = '100%';\n this.videoElement.style.objectFit = 'cover';\n\n // If a container ID is provided, find that element and append the video to it\n if (containerElementId) {\n const container = document.getElementById(containerElementId);\n if (!container) {\n throw new Error(`Container element with ID ${containerElementId} not found`);\n }\n container.appendChild(this.videoElement);\n } else {\n // Otherwise, append to body as fallback\n document.body.appendChild(this.videoElement);\n }\n }\n\n /**\n * Ensures canvas element exists and returns it\n */\n private getCanvasElement(): HTMLCanvasElement {\n if (!this.canvasElement) {\n this.canvasElement = document.createElement('canvas');\n }\n return this.canvasElement;\n }\n\n /**\n * Format error message\n */\n private formatError(err: unknown): string {\n return err instanceof Error ? err.message : String(err);\n }\n}\n"]}