@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.
- package/README.md +218 -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 +759 -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 +333 -29
- package/dist/esm/definitions.d.ts +156 -13
- package/dist/esm/definitions.js.map +1 -1
- package/dist/esm/web.d.ts +52 -3
- package/dist/esm/web.js +592 -95
- package/dist/esm/web.js.map +1 -1
- package/dist/plugin.cjs.js +590 -95
- package/dist/plugin.cjs.js.map +1 -1
- package/dist/plugin.js +590 -95
- package/dist/plugin.js.map +1 -1
- package/ios/Sources/CapgoCameraPreview/CameraController.swift +907 -222
- package/ios/Sources/CapgoCameraPreview/GridOverlayView.swift +65 -0
- package/ios/Sources/CapgoCameraPreview/Plugin.swift +986 -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,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
|
-
|
|
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
|
+
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
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
-
|
|
82
|
-
|
|
83
|
-
|
|
250
|
+
catch (error) {
|
|
251
|
+
console.warn(`Failed to set initial zoom level: ${error}`);
|
|
252
|
+
// Don't throw, just continue without zoom
|
|
84
253
|
}
|
|
85
|
-
}
|
|
86
|
-
|
|
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
|
-
|
|
91
|
-
|
|
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(
|
|
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(
|
|
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
|
-
|
|
122
|
-
|
|
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(
|
|
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,
|
|
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(
|
|
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(
|
|
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(
|
|
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 ===
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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 =
|
|
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
|