@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.
- package/README.md +212 -35
- package/android/.gradle/8.14.2/checksums/checksums.lock +0 -0
- package/android/.gradle/8.14.2/checksums/md5-checksums.bin +0 -0
- package/android/.gradle/8.14.2/checksums/sha1-checksums.bin +0 -0
- package/android/.gradle/8.14.2/executionHistory/executionHistory.bin +0 -0
- package/android/.gradle/8.14.2/executionHistory/executionHistory.lock +0 -0
- package/android/.gradle/8.14.2/fileHashes/fileHashes.bin +0 -0
- package/android/.gradle/8.14.2/fileHashes/fileHashes.lock +0 -0
- package/android/.gradle/8.14.2/fileHashes/resourceHashesCache.bin +0 -0
- package/android/.gradle/buildOutputCleanup/buildOutputCleanup.lock +0 -0
- package/android/.gradle/file-system.probe +0 -0
- package/android/build.gradle +3 -1
- package/android/src/main/AndroidManifest.xml +1 -4
- package/android/src/main/java/com/ahm/capacitor/camera/preview/CameraPreview.java +731 -83
- package/android/src/main/java/com/ahm/capacitor/camera/preview/CameraXView.java +2813 -805
- package/android/src/main/java/com/ahm/capacitor/camera/preview/GridOverlayView.java +112 -0
- package/android/src/main/java/com/ahm/capacitor/camera/preview/model/CameraDevice.java +55 -46
- package/android/src/main/java/com/ahm/capacitor/camera/preview/model/CameraLens.java +61 -52
- package/android/src/main/java/com/ahm/capacitor/camera/preview/model/CameraSessionConfiguration.java +161 -59
- package/android/src/main/java/com/ahm/capacitor/camera/preview/model/LensInfo.java +29 -23
- package/android/src/main/java/com/ahm/capacitor/camera/preview/model/ZoomFactors.java +24 -23
- package/dist/docs.json +292 -29
- package/dist/esm/definitions.d.ts +148 -13
- package/dist/esm/definitions.js.map +1 -1
- package/dist/esm/web.d.ts +52 -3
- package/dist/esm/web.js +555 -97
- package/dist/esm/web.js.map +1 -1
- package/dist/plugin.cjs.js +553 -97
- package/dist/plugin.cjs.js.map +1 -1
- package/dist/plugin.js +553 -97
- package/dist/plugin.js.map +1 -1
- package/ios/Sources/CapgoCameraPreview/CameraController.swift +888 -214
- package/ios/Sources/CapgoCameraPreview/GridOverlayView.swift +65 -0
- package/ios/Sources/CapgoCameraPreview/Plugin.swift +967 -250
- 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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
|
|
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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
-
|
|
82
|
-
|
|
83
|
-
|
|
224
|
+
catch (error) {
|
|
225
|
+
console.warn(`Failed to set initial zoom level: ${error}`);
|
|
226
|
+
// Don't throw, just continue without zoom
|
|
84
227
|
}
|
|
85
|
-
}
|
|
86
|
-
|
|
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
|
-
|
|
91
|
-
|
|
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(
|
|
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(
|
|
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
|
-
|
|
122
|
-
|
|
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(
|
|
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,
|
|
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(
|
|
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(
|
|
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(
|
|
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 ===
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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 =
|
|
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
|