@capgo/camera-preview 7.4.0-beta.2 → 7.4.0-beta.21

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 (35) hide show
  1. package/README.md +218 -35
  2. package/android/.gradle/8.14.2/checksums/checksums.lock +0 -0
  3. package/android/.gradle/8.14.2/checksums/md5-checksums.bin +0 -0
  4. package/android/.gradle/8.14.2/checksums/sha1-checksums.bin +0 -0
  5. package/android/.gradle/8.14.2/executionHistory/executionHistory.bin +0 -0
  6. package/android/.gradle/8.14.2/executionHistory/executionHistory.lock +0 -0
  7. package/android/.gradle/8.14.2/fileHashes/fileHashes.bin +0 -0
  8. package/android/.gradle/8.14.2/fileHashes/fileHashes.lock +0 -0
  9. package/android/.gradle/8.14.2/fileHashes/resourceHashesCache.bin +0 -0
  10. package/android/.gradle/buildOutputCleanup/buildOutputCleanup.lock +0 -0
  11. package/android/.gradle/file-system.probe +0 -0
  12. package/android/build.gradle +3 -1
  13. package/android/src/main/AndroidManifest.xml +1 -4
  14. package/android/src/main/java/com/ahm/capacitor/camera/preview/CameraPreview.java +759 -83
  15. package/android/src/main/java/com/ahm/capacitor/camera/preview/CameraXView.java +2813 -805
  16. package/android/src/main/java/com/ahm/capacitor/camera/preview/GridOverlayView.java +112 -0
  17. package/android/src/main/java/com/ahm/capacitor/camera/preview/model/CameraDevice.java +55 -46
  18. package/android/src/main/java/com/ahm/capacitor/camera/preview/model/CameraLens.java +61 -52
  19. package/android/src/main/java/com/ahm/capacitor/camera/preview/model/CameraSessionConfiguration.java +161 -59
  20. package/android/src/main/java/com/ahm/capacitor/camera/preview/model/LensInfo.java +29 -23
  21. package/android/src/main/java/com/ahm/capacitor/camera/preview/model/ZoomFactors.java +24 -23
  22. package/dist/docs.json +333 -29
  23. package/dist/esm/definitions.d.ts +156 -13
  24. package/dist/esm/definitions.js.map +1 -1
  25. package/dist/esm/web.d.ts +52 -3
  26. package/dist/esm/web.js +592 -95
  27. package/dist/esm/web.js.map +1 -1
  28. package/dist/plugin.cjs.js +590 -95
  29. package/dist/plugin.cjs.js.map +1 -1
  30. package/dist/plugin.js +590 -95
  31. package/dist/plugin.js.map +1 -1
  32. package/ios/Sources/CapgoCameraPreview/CameraController.swift +907 -222
  33. package/ios/Sources/CapgoCameraPreview/GridOverlayView.swift +65 -0
  34. package/ios/Sources/CapgoCameraPreview/Plugin.swift +986 -250
  35. package/package.json +2 -2
package/dist/esm/web.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { WebPlugin } from "@capacitor/core";
2
2
  import { DeviceType } from "./definitions";
3
+ const DEFAULT_VIDEO_ID = "capgo_video";
3
4
  export class CameraPreviewWeb extends WebPlugin {
4
5
  constructor() {
5
6
  super();
@@ -9,87 +10,319 @@ export class CameraPreviewWeb extends WebPlugin {
9
10
  */
10
11
  this.isBackCamera = false;
11
12
  this.currentDeviceId = null;
13
+ this.videoElement = null;
14
+ this.isStarted = false;
12
15
  }
13
16
  async getSupportedPictureSizes() {
14
17
  throw new Error("getSupportedPictureSizes not supported under the web platform");
15
18
  }
16
19
  async start(options) {
17
- var _a;
18
- await navigator.mediaDevices
19
- .getUserMedia({
20
- audio: !options.disableAudio,
21
- video: true,
22
- })
23
- .then((stream) => {
24
- // Stop any existing stream so we can request media with different constraints based on user input
25
- stream.getTracks().forEach((track) => track.stop());
26
- })
27
- .catch((error) => {
28
- Promise.reject(error);
29
- });
30
- const video = document.getElementById("video");
20
+ if (options.aspectRatio && (options.width || options.height)) {
21
+ throw new Error("Cannot set both aspectRatio and size (width/height). Use setPreviewSize after start.");
22
+ }
23
+ if (this.isStarted) {
24
+ throw new Error("camera already started");
25
+ }
26
+ this.isBackCamera = true;
27
+ this.isStarted = false;
31
28
  const parent = document.getElementById((options === null || options === void 0 ? void 0 : options.parent) || "");
32
- if (!video) {
33
- const videoElement = document.createElement("video");
34
- videoElement.id = "video";
35
- videoElement.setAttribute("class", (options === null || options === void 0 ? void 0 : options.className) || "");
36
- // Don't flip video feed if camera is rear facing
37
- if (options.position !== "rear") {
38
- videoElement.setAttribute("style", "-webkit-transform: scaleX(-1); transform: scaleX(-1);");
29
+ const gridMode = (options === null || options === void 0 ? void 0 : options.gridMode) || "none";
30
+ const positioning = (options === null || options === void 0 ? void 0 : options.positioning) || "center";
31
+ if (options.position) {
32
+ this.isBackCamera = options.position === "rear";
33
+ }
34
+ const video = document.getElementById(DEFAULT_VIDEO_ID);
35
+ if (video) {
36
+ video.remove();
37
+ }
38
+ const container = options.parent
39
+ ? document.getElementById(options.parent)
40
+ : document.body;
41
+ if (!container) {
42
+ throw new Error("container not found");
43
+ }
44
+ this.videoElement = document.createElement("video");
45
+ this.videoElement.id = DEFAULT_VIDEO_ID;
46
+ this.videoElement.className = options.className || "";
47
+ this.videoElement.playsInline = true;
48
+ this.videoElement.muted = true;
49
+ this.videoElement.autoplay = true;
50
+ // Remove objectFit as we'll match camera's native aspect ratio
51
+ this.videoElement.style.backgroundColor = "transparent";
52
+ // Reset any default margins that might interfere
53
+ this.videoElement.style.margin = "0";
54
+ this.videoElement.style.padding = "0";
55
+ container.appendChild(this.videoElement);
56
+ if (options.toBack) {
57
+ this.videoElement.style.zIndex = "-1";
58
+ }
59
+ // Default to 16:9 vertical (9:16 for portrait) if no aspect ratio or size specified
60
+ const useDefaultAspectRatio = !options.aspectRatio && !options.width && !options.height;
61
+ const effectiveAspectRatio = options.aspectRatio || (useDefaultAspectRatio ? "16:9" : null);
62
+ if (options.width) {
63
+ this.videoElement.width = options.width;
64
+ this.videoElement.style.width = `${options.width}px`;
65
+ }
66
+ if (options.height) {
67
+ this.videoElement.height = options.height;
68
+ this.videoElement.style.height = `${options.height}px`;
69
+ }
70
+ // Handle positioning - center if x or y not provided
71
+ const centerX = options.x === undefined;
72
+ const centerY = options.y === undefined;
73
+ // Always set position to absolute for proper positioning
74
+ this.videoElement.style.position = "absolute";
75
+ console.log("Initial positioning flags:", {
76
+ centerX,
77
+ centerY,
78
+ x: options.x,
79
+ y: options.y,
80
+ });
81
+ if (options.x !== undefined) {
82
+ this.videoElement.style.left = `${options.x}px`;
83
+ }
84
+ if (options.y !== undefined) {
85
+ this.videoElement.style.top = `${options.y}px`;
86
+ }
87
+ // Create and add grid overlay if needed
88
+ if (gridMode !== "none") {
89
+ const gridOverlay = this.createGridOverlay(gridMode);
90
+ gridOverlay.id = "camera-grid-overlay";
91
+ parent === null || parent === void 0 ? void 0 : parent.appendChild(gridOverlay);
92
+ }
93
+ // Aspect ratio handling is now done after getting camera stream
94
+ // Store centering flags for later use
95
+ const needsCenterX = centerX;
96
+ const needsCenterY = centerY;
97
+ console.log("Centering flags stored:", { needsCenterX, needsCenterY });
98
+ // First get the camera stream with basic constraints
99
+ const constraints = {
100
+ video: {
101
+ facingMode: this.isBackCamera ? "environment" : "user",
102
+ },
103
+ };
104
+ const stream = await navigator.mediaDevices.getUserMedia(constraints);
105
+ if (!stream) {
106
+ throw new Error("could not acquire stream");
107
+ }
108
+ if (!this.videoElement) {
109
+ throw new Error("video element not found");
110
+ }
111
+ // Get the actual camera dimensions from the video track
112
+ const videoTrack = stream.getVideoTracks()[0];
113
+ const settings = videoTrack.getSettings();
114
+ const cameraWidth = settings.width || 640;
115
+ const cameraHeight = settings.height || 480;
116
+ const cameraAspectRatio = cameraWidth / cameraHeight;
117
+ console.log("Camera native dimensions:", {
118
+ width: cameraWidth,
119
+ height: cameraHeight,
120
+ aspectRatio: cameraAspectRatio,
121
+ });
122
+ console.log("Container dimensions:", {
123
+ width: container.offsetWidth,
124
+ height: container.offsetHeight,
125
+ id: container.id,
126
+ });
127
+ // Now adjust video element size based on camera's native aspect ratio
128
+ if (!options.width && !options.height && !options.aspectRatio) {
129
+ // No size specified, fit camera view within container bounds
130
+ const containerWidth = container.offsetWidth || window.innerWidth;
131
+ const containerHeight = container.offsetHeight || window.innerHeight;
132
+ // Calculate dimensions that fit within container while maintaining camera aspect ratio
133
+ let targetWidth, targetHeight;
134
+ // Try fitting to container width first
135
+ targetWidth = containerWidth;
136
+ targetHeight = targetWidth / cameraAspectRatio;
137
+ // If height exceeds container, fit to height instead
138
+ if (targetHeight > containerHeight) {
139
+ targetHeight = containerHeight;
140
+ targetWidth = targetHeight * cameraAspectRatio;
39
141
  }
40
- const userAgent = navigator.userAgent.toLowerCase();
41
- const isSafari = userAgent.includes("safari") && !userAgent.includes("chrome");
42
- // Safari on iOS needs to have the autoplay, muted and playsinline attributes set for video.play() to be successful
43
- // Without these attributes videoElement.play() will throw a NotAllowedError
44
- // https://developer.apple.com/documentation/webkit/delivering_video_content_for_safari
45
- if (isSafari) {
46
- videoElement.setAttribute("autoplay", "true");
47
- videoElement.setAttribute("muted", "true");
48
- videoElement.setAttribute("playsinline", "true");
142
+ console.log("Video element dimensions:", {
143
+ width: targetWidth,
144
+ height: targetHeight,
145
+ container: { width: containerWidth, height: containerHeight },
146
+ });
147
+ this.videoElement.width = targetWidth;
148
+ this.videoElement.height = targetHeight;
149
+ this.videoElement.style.width = `${targetWidth}px`;
150
+ this.videoElement.style.height = `${targetHeight}px`;
151
+ // Center the video element within its parent container
152
+ if (needsCenterX || options.x === undefined) {
153
+ const x = Math.round((containerWidth - targetWidth) / 2);
154
+ this.videoElement.style.left = `${x}px`;
49
155
  }
50
- parent === null || parent === void 0 ? void 0 : parent.appendChild(videoElement);
51
- if ((_a = navigator === null || navigator === void 0 ? void 0 : navigator.mediaDevices) === null || _a === void 0 ? void 0 : _a.getUserMedia) {
52
- const constraints = {
53
- video: {
54
- width: { ideal: options.width },
55
- height: { ideal: options.height },
56
- },
57
- };
58
- if (options.deviceId) {
59
- constraints.video.deviceId = { exact: options.deviceId };
60
- this.currentDeviceId = options.deviceId;
61
- // Try to determine camera position from device
62
- const devices = await navigator.mediaDevices.enumerateDevices();
63
- const device = devices.find(d => d.deviceId === options.deviceId);
64
- this.isBackCamera = (device === null || device === void 0 ? void 0 : device.label.toLowerCase().includes('back')) || (device === null || device === void 0 ? void 0 : device.label.toLowerCase().includes('rear')) || false;
156
+ if (needsCenterY || options.y === undefined) {
157
+ let y;
158
+ switch (positioning) {
159
+ case "top":
160
+ y = 0;
161
+ break;
162
+ case "bottom":
163
+ y = window.innerHeight - targetHeight;
164
+ break;
165
+ case "center":
166
+ default:
167
+ y = Math.round((window.innerHeight - targetHeight) / 2);
168
+ break;
65
169
  }
66
- else if (options.position === "rear") {
67
- constraints.video.facingMode = "environment";
68
- this.isBackCamera = true;
69
- }
70
- else {
71
- this.isBackCamera = false;
170
+ this.videoElement.style.setProperty("top", `${y}px`, "important");
171
+ // Force a style recalculation
172
+ this.videoElement.offsetHeight;
173
+ console.log("Positioning video:", {
174
+ positioning,
175
+ viewportHeight: window.innerHeight,
176
+ targetHeight,
177
+ calculatedY: y,
178
+ actualTop: this.videoElement.style.top,
179
+ position: this.videoElement.style.position,
180
+ });
181
+ }
182
+ }
183
+ else if (effectiveAspectRatio && !options.width && !options.height) {
184
+ // Aspect ratio specified but no size
185
+ const [widthRatio, heightRatio] = effectiveAspectRatio
186
+ .split(":")
187
+ .map(Number);
188
+ const targetRatio = widthRatio / heightRatio;
189
+ const viewportWidth = window.innerWidth;
190
+ const viewportHeight = window.innerHeight;
191
+ let targetWidth, targetHeight;
192
+ // Try fitting to viewport width first
193
+ targetWidth = viewportWidth;
194
+ targetHeight = targetWidth / targetRatio;
195
+ // If height exceeds viewport, fit to height instead
196
+ if (targetHeight > viewportHeight) {
197
+ targetHeight = viewportHeight;
198
+ targetWidth = targetHeight * targetRatio;
199
+ }
200
+ this.videoElement.width = targetWidth;
201
+ this.videoElement.height = targetHeight;
202
+ this.videoElement.style.width = `${targetWidth}px`;
203
+ this.videoElement.style.height = `${targetHeight}px`;
204
+ // Center the video element within its parent container
205
+ if (needsCenterX || options.x === undefined) {
206
+ const parentWidth = container.offsetWidth || viewportWidth;
207
+ const x = Math.round((parentWidth - targetWidth) / 2);
208
+ this.videoElement.style.left = `${x}px`;
209
+ }
210
+ if (needsCenterY || options.y === undefined) {
211
+ const parentHeight = container.offsetHeight || viewportHeight;
212
+ let y;
213
+ switch (positioning) {
214
+ case "top":
215
+ y = 0;
216
+ break;
217
+ case "bottom":
218
+ y = parentHeight - targetHeight;
219
+ break;
220
+ case "center":
221
+ default:
222
+ y = Math.round((parentHeight - targetHeight) / 2);
223
+ break;
72
224
  }
73
- const self = this;
74
- await navigator.mediaDevices.getUserMedia(constraints).then((stream) => {
75
- if (document.getElementById("video")) {
76
- // video.src = window.URL.createObjectURL(stream);
77
- videoElement.srcObject = stream;
78
- videoElement.play();
79
- Promise.resolve({});
225
+ this.videoElement.style.top = `${y}px`;
226
+ }
227
+ }
228
+ this.videoElement.srcObject = stream;
229
+ if (!this.isBackCamera) {
230
+ this.videoElement.style.transform = "scaleX(-1)";
231
+ }
232
+ // Set initial zoom level if specified and supported
233
+ if (options.initialZoomLevel && options.initialZoomLevel !== 1.0) {
234
+ // videoTrack already declared above
235
+ if (videoTrack) {
236
+ const capabilities = videoTrack.getCapabilities();
237
+ if (capabilities.zoom) {
238
+ const zoomLevel = options.initialZoomLevel;
239
+ const minZoom = capabilities.zoom.min || 1;
240
+ const maxZoom = capabilities.zoom.max || 1;
241
+ if (zoomLevel < minZoom || zoomLevel > maxZoom) {
242
+ stream.getTracks().forEach((track) => track.stop());
243
+ throw new Error(`Initial zoom level ${zoomLevel} is not available. Valid range is ${minZoom} to ${maxZoom}`);
244
+ }
245
+ try {
246
+ await videoTrack.applyConstraints({
247
+ advanced: [{ zoom: zoomLevel }],
248
+ });
80
249
  }
81
- else {
82
- self.stopStream(stream);
83
- Promise.reject(new Error("camera already stopped"));
250
+ catch (error) {
251
+ console.warn(`Failed to set initial zoom level: ${error}`);
252
+ // Don't throw, just continue without zoom
84
253
  }
85
- }, (err) => {
86
- Promise.reject(new Error(err));
254
+ }
255
+ }
256
+ }
257
+ this.isStarted = true;
258
+ // Wait for video to be ready and get actual dimensions
259
+ await new Promise((resolve) => {
260
+ if (this.videoElement.readyState >= 2) {
261
+ resolve();
262
+ }
263
+ else {
264
+ this.videoElement.addEventListener("loadeddata", () => resolve(), {
265
+ once: true,
87
266
  });
88
267
  }
268
+ });
269
+ // Ensure centering is applied after DOM updates
270
+ await new Promise((resolve) => requestAnimationFrame(resolve));
271
+ console.log("About to re-center, flags:", { needsCenterX, needsCenterY });
272
+ // Re-apply centering with correct parent dimensions
273
+ if (needsCenterX) {
274
+ const parentWidth = container.offsetWidth;
275
+ const x = Math.round((parentWidth - this.videoElement.offsetWidth) / 2);
276
+ this.videoElement.style.left = `${x}px`;
277
+ console.log("Re-centering X:", {
278
+ parentWidth,
279
+ videoWidth: this.videoElement.offsetWidth,
280
+ x,
281
+ });
89
282
  }
90
- else {
91
- Promise.reject(new Error("camera already started"));
283
+ if (needsCenterY) {
284
+ let y;
285
+ switch (positioning) {
286
+ case "top":
287
+ y = 0;
288
+ break;
289
+ case "bottom":
290
+ y = window.innerHeight - this.videoElement.offsetHeight;
291
+ break;
292
+ case "center":
293
+ default:
294
+ y = Math.round((window.innerHeight - this.videoElement.offsetHeight) / 2);
295
+ break;
296
+ }
297
+ this.videoElement.style.setProperty("top", `${y}px`, "important");
298
+ console.log("Re-positioning Y:", {
299
+ positioning,
300
+ viewportHeight: window.innerHeight,
301
+ videoHeight: this.videoElement.offsetHeight,
302
+ y,
303
+ position: this.videoElement.style.position,
304
+ top: this.videoElement.style.top,
305
+ });
92
306
  }
307
+ // Get the actual rendered dimensions after video is loaded
308
+ const rect = this.videoElement.getBoundingClientRect();
309
+ const computedStyle = window.getComputedStyle(this.videoElement);
310
+ console.log("Final video element state:", {
311
+ rect: { x: rect.x, y: rect.y, width: rect.width, height: rect.height },
312
+ style: {
313
+ position: computedStyle.position,
314
+ left: computedStyle.left,
315
+ top: computedStyle.top,
316
+ width: computedStyle.width,
317
+ height: computedStyle.height,
318
+ },
319
+ });
320
+ return {
321
+ width: Math.round(rect.width),
322
+ height: Math.round(rect.height),
323
+ x: Math.round(rect.x),
324
+ y: Math.round(rect.y),
325
+ };
93
326
  }
94
327
  stopStream(stream) {
95
328
  if (stream) {
@@ -99,16 +332,20 @@ export class CameraPreviewWeb extends WebPlugin {
99
332
  }
100
333
  }
101
334
  async stop() {
102
- const video = document.getElementById("video");
335
+ const video = document.getElementById(DEFAULT_VIDEO_ID);
103
336
  if (video) {
104
337
  video.pause();
105
338
  this.stopStream(video.srcObject);
106
339
  video.remove();
340
+ this.isStarted = false;
107
341
  }
342
+ // Remove grid overlay if it exists
343
+ const gridOverlay = document.getElementById("camera-grid-overlay");
344
+ gridOverlay === null || gridOverlay === void 0 ? void 0 : gridOverlay.remove();
108
345
  }
109
346
  async capture(options) {
110
347
  return new Promise((resolve, reject) => {
111
- const video = document.getElementById("video");
348
+ const video = document.getElementById(DEFAULT_VIDEO_ID);
112
349
  if (!(video === null || video === void 0 ? void 0 : video.srcObject)) {
113
350
  reject(new Error("camera is not running"));
114
351
  return;
@@ -118,17 +355,59 @@ export class CameraPreviewWeb extends WebPlugin {
118
355
  if (video && video.videoWidth > 0 && video.videoHeight > 0) {
119
356
  const canvas = document.createElement("canvas");
120
357
  const context = canvas.getContext("2d");
121
- canvas.width = video.videoWidth;
122
- canvas.height = video.videoHeight;
358
+ // Calculate capture dimensions
359
+ let captureWidth = video.videoWidth;
360
+ let captureHeight = video.videoHeight;
361
+ let sourceX = 0;
362
+ let sourceY = 0;
363
+ // Check for conflicting parameters
364
+ if (options.aspectRatio && (options.width || options.height)) {
365
+ reject(new Error("Cannot set both aspectRatio and size (width/height). Use setPreviewSize after start."));
366
+ return;
367
+ }
368
+ // Handle aspect ratio if no width/height specified
369
+ if (!options.width && !options.height && options.aspectRatio) {
370
+ const [widthRatio, heightRatio] = options.aspectRatio.split(':').map(Number);
371
+ if (widthRatio && heightRatio) {
372
+ // For capture in portrait orientation, swap the aspect ratio (16:9 becomes 9:16)
373
+ const isPortrait = video.videoHeight > video.videoWidth;
374
+ const targetAspectRatio = isPortrait ? heightRatio / widthRatio : widthRatio / heightRatio;
375
+ const videoAspectRatio = video.videoWidth / video.videoHeight;
376
+ if (videoAspectRatio > targetAspectRatio) {
377
+ // Video is wider than target - crop sides
378
+ captureWidth = video.videoHeight * targetAspectRatio;
379
+ captureHeight = video.videoHeight;
380
+ sourceX = (video.videoWidth - captureWidth) / 2;
381
+ }
382
+ else {
383
+ // Video is taller than target - crop top/bottom
384
+ captureWidth = video.videoWidth;
385
+ captureHeight = video.videoWidth / targetAspectRatio;
386
+ sourceY = (video.videoHeight - captureHeight) / 2;
387
+ }
388
+ }
389
+ }
390
+ else if (options.width || options.height) {
391
+ // If width or height is specified, use them
392
+ if (options.width)
393
+ captureWidth = options.width;
394
+ if (options.height)
395
+ captureHeight = options.height;
396
+ }
397
+ canvas.width = captureWidth;
398
+ canvas.height = captureHeight;
123
399
  // flip horizontally back camera isn't used
124
400
  if (!this.isBackCamera) {
125
- context === null || context === void 0 ? void 0 : context.translate(video.videoWidth, 0);
401
+ context === null || context === void 0 ? void 0 : context.translate(captureWidth, 0);
126
402
  context === null || context === void 0 ? void 0 : context.scale(-1, 1);
127
403
  }
128
- context === null || context === void 0 ? void 0 : context.drawImage(video, 0, 0, video.videoWidth, video.videoHeight);
404
+ context === null || context === void 0 ? void 0 : context.drawImage(video, sourceX, sourceY, captureWidth, captureHeight, 0, 0, captureWidth, captureHeight);
129
405
  if (options.saveToGallery) {
130
406
  // saveToGallery is not supported on web
131
407
  }
408
+ if (options.withExifLocation) {
409
+ // withExifLocation is not supported on web
410
+ }
132
411
  if ((options.format || "jpeg") === "jpeg") {
133
412
  base64EncodedImage = canvas
134
413
  .toDataURL("image/jpeg", (options.quality || 85) / 100.0)
@@ -166,7 +445,7 @@ export class CameraPreviewWeb extends WebPlugin {
166
445
  throw new Error(`setFlashMode not supported under the web platform${_options}`);
167
446
  }
168
447
  async flip() {
169
- const video = document.getElementById("video");
448
+ const video = document.getElementById(DEFAULT_VIDEO_ID);
170
449
  if (!(video === null || video === void 0 ? void 0 : video.srcObject)) {
171
450
  throw new Error("camera is not running");
172
451
  }
@@ -206,12 +485,12 @@ export class CameraPreviewWeb extends WebPlugin {
206
485
  }
207
486
  }
208
487
  async setOpacity(_options) {
209
- const video = document.getElementById("video");
488
+ const video = document.getElementById(DEFAULT_VIDEO_ID);
210
489
  if (!!video && !!_options.opacity)
211
490
  video.style.setProperty("opacity", _options.opacity.toString());
212
491
  }
213
492
  async isRunning() {
214
- const video = document.getElementById("video");
493
+ const video = document.getElementById(DEFAULT_VIDEO_ID);
215
494
  return { isRunning: !!video && !!video.srcObject };
216
495
  }
217
496
  async getAvailableDevices() {
@@ -220,7 +499,7 @@ export class CameraPreviewWeb extends WebPlugin {
220
499
  throw new Error("getAvailableDevices not supported under the web platform");
221
500
  }
222
501
  const devices = await navigator.mediaDevices.enumerateDevices();
223
- const videoDevices = devices.filter(device => device.kind === 'videoinput');
502
+ const videoDevices = devices.filter((device) => device.kind === "videoinput");
224
503
  // Group devices by position (front/back)
225
504
  const frontDevices = [];
226
505
  const backDevices = [];
@@ -230,15 +509,19 @@ export class CameraPreviewWeb extends WebPlugin {
230
509
  // Determine device type based on label
231
510
  let deviceType = DeviceType.WIDE_ANGLE;
232
511
  let baseZoomRatio = 1.0;
233
- if (labelLower.includes('ultra') || labelLower.includes('0.5')) {
512
+ if (labelLower.includes("ultra") || labelLower.includes("0.5")) {
234
513
  deviceType = DeviceType.ULTRA_WIDE;
235
514
  baseZoomRatio = 0.5;
236
515
  }
237
- else if (labelLower.includes('telephoto') || labelLower.includes('tele') || labelLower.includes('2x') || labelLower.includes('3x')) {
516
+ else if (labelLower.includes("telephoto") ||
517
+ labelLower.includes("tele") ||
518
+ labelLower.includes("2x") ||
519
+ labelLower.includes("3x")) {
238
520
  deviceType = DeviceType.TELEPHOTO;
239
521
  baseZoomRatio = 2.0;
240
522
  }
241
- else if (labelLower.includes('depth') || labelLower.includes('truedepth')) {
523
+ else if (labelLower.includes("depth") ||
524
+ labelLower.includes("truedepth")) {
242
525
  deviceType = DeviceType.TRUE_DEPTH;
243
526
  baseZoomRatio = 1.0;
244
527
  }
@@ -249,10 +532,10 @@ export class CameraPreviewWeb extends WebPlugin {
249
532
  focalLength: 4.25,
250
533
  baseZoomRatio,
251
534
  minZoom: 1.0,
252
- maxZoom: 1.0
535
+ maxZoom: 1.0,
253
536
  };
254
537
  // Determine position and add to appropriate array
255
- if (labelLower.includes('back') || labelLower.includes('rear')) {
538
+ if (labelLower.includes("back") || labelLower.includes("rear")) {
256
539
  backDevices.push(lensInfo);
257
540
  }
258
541
  else {
@@ -267,8 +550,8 @@ export class CameraPreviewWeb extends WebPlugin {
267
550
  position: "front",
268
551
  lenses: frontDevices,
269
552
  isLogical: false,
270
- minZoom: Math.min(...frontDevices.map(d => d.minZoom)),
271
- maxZoom: Math.max(...frontDevices.map(d => d.maxZoom))
553
+ minZoom: Math.min(...frontDevices.map((d) => d.minZoom)),
554
+ maxZoom: Math.max(...frontDevices.map((d) => d.maxZoom)),
272
555
  });
273
556
  }
274
557
  if (backDevices.length > 0) {
@@ -278,14 +561,14 @@ export class CameraPreviewWeb extends WebPlugin {
278
561
  position: "rear",
279
562
  lenses: backDevices,
280
563
  isLogical: false,
281
- minZoom: Math.min(...backDevices.map(d => d.minZoom)),
282
- maxZoom: Math.max(...backDevices.map(d => d.maxZoom))
564
+ minZoom: Math.min(...backDevices.map((d) => d.minZoom)),
565
+ maxZoom: Math.max(...backDevices.map((d) => d.maxZoom)),
283
566
  });
284
567
  }
285
568
  return { devices: result };
286
569
  }
287
570
  async getZoom() {
288
- const video = document.getElementById("video");
571
+ const video = document.getElementById(DEFAULT_VIDEO_ID);
289
572
  if (!(video === null || video === void 0 ? void 0 : video.srcObject)) {
290
573
  throw new Error("camera is not running");
291
574
  }
@@ -304,18 +587,22 @@ export class CameraPreviewWeb extends WebPlugin {
304
587
  let baseZoomRatio = 1.0;
305
588
  if (this.currentDeviceId) {
306
589
  const devices = await navigator.mediaDevices.enumerateDevices();
307
- const device = devices.find(d => d.deviceId === this.currentDeviceId);
590
+ const device = devices.find((d) => d.deviceId === this.currentDeviceId);
308
591
  if (device) {
309
592
  const labelLower = device.label.toLowerCase();
310
- if (labelLower.includes('ultra') || labelLower.includes('0.5')) {
593
+ if (labelLower.includes("ultra") || labelLower.includes("0.5")) {
311
594
  deviceType = DeviceType.ULTRA_WIDE;
312
595
  baseZoomRatio = 0.5;
313
596
  }
314
- else if (labelLower.includes('telephoto') || labelLower.includes('tele') || labelLower.includes('2x') || labelLower.includes('3x')) {
597
+ else if (labelLower.includes("telephoto") ||
598
+ labelLower.includes("tele") ||
599
+ labelLower.includes("2x") ||
600
+ labelLower.includes("3x")) {
315
601
  deviceType = DeviceType.TELEPHOTO;
316
602
  baseZoomRatio = 2.0;
317
603
  }
318
- else if (labelLower.includes('depth') || labelLower.includes('truedepth')) {
604
+ else if (labelLower.includes("depth") ||
605
+ labelLower.includes("truedepth")) {
319
606
  deviceType = DeviceType.TRUE_DEPTH;
320
607
  baseZoomRatio = 1.0;
321
608
  }
@@ -326,7 +613,7 @@ export class CameraPreviewWeb extends WebPlugin {
326
613
  focalLength: 4.25,
327
614
  deviceType,
328
615
  baseZoomRatio,
329
- digitalZoom: currentZoom / baseZoomRatio
616
+ digitalZoom: currentZoom / baseZoomRatio,
330
617
  };
331
618
  return {
332
619
  min: capabilities.zoom.min || 1,
@@ -336,7 +623,7 @@ export class CameraPreviewWeb extends WebPlugin {
336
623
  };
337
624
  }
338
625
  async setZoom(options) {
339
- const video = document.getElementById("video");
626
+ const video = document.getElementById(DEFAULT_VIDEO_ID);
340
627
  if (!(video === null || video === void 0 ? void 0 : video.srcObject)) {
341
628
  throw new Error("camera is not running");
342
629
  }
@@ -350,9 +637,10 @@ export class CameraPreviewWeb extends WebPlugin {
350
637
  throw new Error("zoom not supported by this device");
351
638
  }
352
639
  const zoomLevel = Math.max(capabilities.zoom.min || 1, Math.min(capabilities.zoom.max || 1, options.level));
640
+ // Note: autoFocus is not supported on web platform
353
641
  try {
354
642
  await videoTrack.applyConstraints({
355
- advanced: [{ zoom: zoomLevel }]
643
+ advanced: [{ zoom: zoomLevel }],
356
644
  });
357
645
  }
358
646
  catch (error) {
@@ -366,7 +654,7 @@ export class CameraPreviewWeb extends WebPlugin {
366
654
  return { deviceId: this.currentDeviceId || "" };
367
655
  }
368
656
  async setDeviceId(options) {
369
- const video = document.getElementById("video");
657
+ const video = document.getElementById(DEFAULT_VIDEO_ID);
370
658
  if (!(video === null || video === void 0 ? void 0 : video.srcObject)) {
371
659
  throw new Error("camera is not running");
372
660
  }
@@ -385,8 +673,11 @@ export class CameraPreviewWeb extends WebPlugin {
385
673
  try {
386
674
  // Try to determine camera position from device
387
675
  const devices = await navigator.mediaDevices.enumerateDevices();
388
- const device = devices.find(d => d.deviceId === options.deviceId);
389
- this.isBackCamera = (device === null || device === void 0 ? void 0 : device.label.toLowerCase().includes('back')) || (device === null || device === void 0 ? void 0 : device.label.toLowerCase().includes('rear')) || false;
676
+ const device = devices.find((d) => d.deviceId === options.deviceId);
677
+ this.isBackCamera =
678
+ (device === null || device === void 0 ? void 0 : device.label.toLowerCase().includes("back")) ||
679
+ (device === null || device === void 0 ? void 0 : device.label.toLowerCase().includes("rear")) ||
680
+ false;
390
681
  const stream = await navigator.mediaDevices.getUserMedia(constraints);
391
682
  video.srcObject = stream;
392
683
  // Update video transform based on camera
@@ -404,5 +695,211 @@ export class CameraPreviewWeb extends WebPlugin {
404
695
  throw new Error(`Failed to swap to device ${options.deviceId}: ${error}`);
405
696
  }
406
697
  }
698
+ async getAspectRatio() {
699
+ const video = document.getElementById(DEFAULT_VIDEO_ID);
700
+ if (!video) {
701
+ throw new Error("camera is not running");
702
+ }
703
+ const width = video.offsetWidth;
704
+ const height = video.offsetHeight;
705
+ if (width && height) {
706
+ const ratio = width / height;
707
+ // Check for portrait camera ratios: 4:3 -> 3:4, 16:9 -> 9:16
708
+ if (Math.abs(ratio - 3 / 4) < 0.01) {
709
+ return { aspectRatio: "4:3" };
710
+ }
711
+ if (Math.abs(ratio - 9 / 16) < 0.01) {
712
+ return { aspectRatio: "16:9" };
713
+ }
714
+ }
715
+ // Default to 4:3 if no specific aspect ratio is matched
716
+ return { aspectRatio: "4:3" };
717
+ }
718
+ async setAspectRatio(options) {
719
+ const video = document.getElementById(DEFAULT_VIDEO_ID);
720
+ if (!video) {
721
+ throw new Error("camera is not running");
722
+ }
723
+ if (options.aspectRatio) {
724
+ const [widthRatio, heightRatio] = options.aspectRatio
725
+ .split(":")
726
+ .map(Number);
727
+ // For camera, use portrait orientation: 4:3 becomes 3:4, 16:9 becomes 9:16
728
+ const ratio = heightRatio / widthRatio;
729
+ // Get current position and size
730
+ const rect = video.getBoundingClientRect();
731
+ const currentWidth = rect.width;
732
+ const currentHeight = rect.height;
733
+ const currentRatio = currentWidth / currentHeight;
734
+ let newWidth;
735
+ let newHeight;
736
+ if (currentRatio > ratio) {
737
+ // Width is larger, fit by height and center horizontally
738
+ newWidth = currentHeight * ratio;
739
+ newHeight = currentHeight;
740
+ }
741
+ else {
742
+ // Height is larger, fit by width and center vertically
743
+ newWidth = currentWidth;
744
+ newHeight = currentWidth / ratio;
745
+ }
746
+ // Calculate position
747
+ let x, y;
748
+ if (options.x !== undefined && options.y !== undefined) {
749
+ // Use provided coordinates, ensuring they stay within screen boundaries
750
+ x = Math.max(0, Math.min(options.x, window.innerWidth - newWidth));
751
+ y = Math.max(0, Math.min(options.y, window.innerHeight - newHeight));
752
+ }
753
+ else {
754
+ // Auto-center the view
755
+ x = (window.innerWidth - newWidth) / 2;
756
+ y = (window.innerHeight - newHeight) / 2;
757
+ }
758
+ video.style.width = `${newWidth}px`;
759
+ video.style.height = `${newHeight}px`;
760
+ video.style.left = `${x}px`;
761
+ video.style.top = `${y}px`;
762
+ video.style.position = "absolute";
763
+ const offsetX = newWidth / 8;
764
+ const offsetY = newHeight / 8;
765
+ return {
766
+ width: Math.round(newWidth),
767
+ height: Math.round(newHeight),
768
+ x: Math.round(x + offsetX),
769
+ y: Math.round(y + offsetY),
770
+ };
771
+ }
772
+ else {
773
+ video.style.objectFit = "cover";
774
+ const rect = video.getBoundingClientRect();
775
+ const offsetX = rect.width / 8;
776
+ const offsetY = rect.height / 8;
777
+ return {
778
+ width: Math.round(rect.width),
779
+ height: Math.round(rect.height),
780
+ x: Math.round(rect.left + offsetX),
781
+ y: Math.round(rect.top + offsetY),
782
+ };
783
+ }
784
+ }
785
+ createGridOverlay(gridMode) {
786
+ const overlay = document.createElement("div");
787
+ overlay.style.position = "absolute";
788
+ overlay.style.top = "0";
789
+ overlay.style.left = "0";
790
+ overlay.style.width = "100%";
791
+ overlay.style.height = "100%";
792
+ overlay.style.pointerEvents = "none";
793
+ overlay.style.zIndex = "10";
794
+ const divisions = gridMode === "3x3" ? 3 : 4;
795
+ // Create SVG for grid lines
796
+ const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
797
+ svg.style.width = "100%";
798
+ svg.style.height = "100%";
799
+ svg.style.position = "absolute";
800
+ svg.style.top = "0";
801
+ svg.style.left = "0";
802
+ // Create grid lines
803
+ for (let i = 1; i < divisions; i++) {
804
+ // Vertical lines
805
+ const verticalLine = document.createElementNS("http://www.w3.org/2000/svg", "line");
806
+ verticalLine.setAttribute("x1", `${(i / divisions) * 100}%`);
807
+ verticalLine.setAttribute("y1", "0%");
808
+ verticalLine.setAttribute("x2", `${(i / divisions) * 100}%`);
809
+ verticalLine.setAttribute("y2", "100%");
810
+ verticalLine.setAttribute("stroke", "rgba(255, 255, 255, 0.5)");
811
+ verticalLine.setAttribute("stroke-width", "1");
812
+ svg.appendChild(verticalLine);
813
+ // Horizontal lines
814
+ const horizontalLine = document.createElementNS("http://www.w3.org/2000/svg", "line");
815
+ horizontalLine.setAttribute("x1", "0%");
816
+ horizontalLine.setAttribute("y1", `${(i / divisions) * 100}%`);
817
+ horizontalLine.setAttribute("x2", "100%");
818
+ horizontalLine.setAttribute("y2", `${(i / divisions) * 100}%`);
819
+ horizontalLine.setAttribute("stroke", "rgba(255, 255, 255, 0.5)");
820
+ horizontalLine.setAttribute("stroke-width", "1");
821
+ svg.appendChild(horizontalLine);
822
+ }
823
+ overlay.appendChild(svg);
824
+ return overlay;
825
+ }
826
+ async setGridMode(options) {
827
+ // Web implementation of grid mode would need to be implemented
828
+ // For now, just resolve as a no-op
829
+ console.warn(`Grid mode '${options.gridMode}' is not yet implemented for web platform`);
830
+ }
831
+ async getGridMode() {
832
+ // Web implementation - default to none
833
+ return { gridMode: "none" };
834
+ }
835
+ async getPreviewSize() {
836
+ const video = document.getElementById(DEFAULT_VIDEO_ID);
837
+ if (!video) {
838
+ throw new Error("camera is not running");
839
+ }
840
+ const offsetX = video.width / 8;
841
+ const offsetY = video.height / 8;
842
+ return {
843
+ x: video.offsetLeft + offsetX,
844
+ y: video.offsetTop + offsetY,
845
+ width: video.width,
846
+ height: video.height,
847
+ };
848
+ }
849
+ async setPreviewSize(options) {
850
+ const video = document.getElementById(DEFAULT_VIDEO_ID);
851
+ if (!video) {
852
+ throw new Error("camera is not running");
853
+ }
854
+ video.style.left = `${options.x}px`;
855
+ video.style.top = `${options.y}px`;
856
+ video.width = options.width;
857
+ video.height = options.height;
858
+ const offsetX = options.width / 8;
859
+ const offsetY = options.height / 8;
860
+ return {
861
+ width: options.width,
862
+ height: options.height,
863
+ x: options.x + offsetX,
864
+ y: options.y + offsetY,
865
+ };
866
+ }
867
+ async setFocus(options) {
868
+ // Reject if values are outside 0-1 range
869
+ if (options.x < 0 || options.x > 1 || options.y < 0 || options.y > 1) {
870
+ throw new Error("Focus coordinates must be between 0 and 1");
871
+ }
872
+ const video = document.getElementById(DEFAULT_VIDEO_ID);
873
+ if (!(video === null || video === void 0 ? void 0 : video.srcObject)) {
874
+ throw new Error("camera is not running");
875
+ }
876
+ const stream = video.srcObject;
877
+ const videoTrack = stream.getVideoTracks()[0];
878
+ if (!videoTrack) {
879
+ throw new Error("no video track found");
880
+ }
881
+ const capabilities = videoTrack.getCapabilities();
882
+ // Check if focusing is supported
883
+ if (capabilities.focusMode) {
884
+ try {
885
+ // Web API supports focus mode settings but not coordinate-based focus
886
+ // Setting to manual mode allows for coordinate focus if supported
887
+ await videoTrack.applyConstraints({
888
+ advanced: [
889
+ {
890
+ focusMode: "manual",
891
+ focusDistance: 0.5, // Mid-range focus as fallback
892
+ },
893
+ ],
894
+ });
895
+ }
896
+ catch (error) {
897
+ console.warn(`setFocus is not fully supported on this device: ${error}. Focus coordinates (${options.x}, ${options.y}) were provided but cannot be applied.`);
898
+ }
899
+ }
900
+ else {
901
+ console.warn("Focus control is not supported on this device. Focus coordinates were provided but cannot be applied.");
902
+ }
903
+ }
407
904
  }
408
905
  //# sourceMappingURL=web.js.map