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

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