@capgo/camera-preview 7.4.0-beta.9 → 7.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +242 -50
- package/android/gradle/wrapper/gradle-wrapper.properties +1 -1
- package/android/src/main/java/com/ahm/capacitor/camera/preview/CameraPreview.java +1249 -143
- package/android/src/main/java/com/ahm/capacitor/camera/preview/CameraXView.java +3400 -1432
- package/android/src/main/java/com/ahm/capacitor/camera/preview/GridOverlayView.java +95 -58
- 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 +160 -72
- 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 +441 -40
- package/dist/esm/definitions.d.ts +167 -25
- package/dist/esm/definitions.js.map +1 -1
- package/dist/esm/index.d.ts +2 -0
- package/dist/esm/index.js +24 -1
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/web.d.ts +23 -3
- package/dist/esm/web.js +463 -65
- package/dist/esm/web.js.map +1 -1
- package/dist/plugin.cjs.js +485 -64
- package/dist/plugin.cjs.js.map +1 -1
- package/dist/plugin.js +485 -64
- package/dist/plugin.js.map +1 -1
- package/ios/Sources/{CapgoCameraPreview → CapgoCameraPreviewPlugin}/CameraController.swift +731 -315
- package/ios/Sources/CapgoCameraPreviewPlugin/Plugin.swift +1902 -0
- package/package.json +11 -3
- 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/fileChanges/last-build.bin +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/8.14.2/gc.properties +0 -0
- package/android/.gradle/buildOutputCleanup/buildOutputCleanup.lock +0 -0
- package/android/.gradle/buildOutputCleanup/cache.properties +0 -2
- package/android/.gradle/buildOutputCleanup/outputFiles.bin +0 -0
- package/android/.gradle/file-system.probe +0 -0
- package/android/.gradle/vcs-1/gc.properties +0 -0
- package/ios/Sources/CapgoCameraPreview/Plugin.swift +0 -1369
- /package/ios/Sources/{CapgoCameraPreview → CapgoCameraPreviewPlugin}/GridOverlayView.swift +0 -0
package/dist/esm/web.js
CHANGED
|
@@ -12,6 +12,72 @@ export class CameraPreviewWeb extends WebPlugin {
|
|
|
12
12
|
this.currentDeviceId = null;
|
|
13
13
|
this.videoElement = null;
|
|
14
14
|
this.isStarted = false;
|
|
15
|
+
this.orientationListenerBound = false;
|
|
16
|
+
}
|
|
17
|
+
getCurrentOrientation() {
|
|
18
|
+
var _a, _b;
|
|
19
|
+
try {
|
|
20
|
+
const so = screen.orientation;
|
|
21
|
+
const type = (so === null || so === void 0 ? void 0 : so.type) || (so === null || so === void 0 ? void 0 : so.mozOrientation) || (so === null || so === void 0 ? void 0 : so.msOrientation);
|
|
22
|
+
if (typeof type === "string") {
|
|
23
|
+
if (type.includes("portrait-primary"))
|
|
24
|
+
return "portrait";
|
|
25
|
+
if (type.includes("portrait-secondary"))
|
|
26
|
+
return "portrait-upside-down";
|
|
27
|
+
if (type.includes("landscape-primary"))
|
|
28
|
+
return "landscape-left";
|
|
29
|
+
if (type.includes("landscape-secondary"))
|
|
30
|
+
return "landscape-right";
|
|
31
|
+
if (type.includes("landscape"))
|
|
32
|
+
return "landscape";
|
|
33
|
+
if (type.includes("portrait"))
|
|
34
|
+
return "portrait";
|
|
35
|
+
}
|
|
36
|
+
const angle = window.orientation;
|
|
37
|
+
if (typeof angle === "number") {
|
|
38
|
+
if (angle === 0)
|
|
39
|
+
return "portrait";
|
|
40
|
+
if (angle === 180)
|
|
41
|
+
return "portrait-upside-down";
|
|
42
|
+
if (angle === 90)
|
|
43
|
+
return "landscape-right";
|
|
44
|
+
if (angle === -90)
|
|
45
|
+
return "landscape-left";
|
|
46
|
+
if (angle === 270)
|
|
47
|
+
return "landscape-left";
|
|
48
|
+
}
|
|
49
|
+
if ((_a = window.matchMedia("(orientation: portrait)")) === null || _a === void 0 ? void 0 : _a.matches) {
|
|
50
|
+
return "portrait";
|
|
51
|
+
}
|
|
52
|
+
if ((_b = window.matchMedia("(orientation: landscape)")) === null || _b === void 0 ? void 0 : _b.matches) {
|
|
53
|
+
return "landscape";
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
catch (e) {
|
|
57
|
+
console.error(e);
|
|
58
|
+
}
|
|
59
|
+
return "unknown";
|
|
60
|
+
}
|
|
61
|
+
ensureOrientationListener() {
|
|
62
|
+
if (this.orientationListenerBound)
|
|
63
|
+
return;
|
|
64
|
+
const emit = () => {
|
|
65
|
+
this.notifyListeners("orientationChange", {
|
|
66
|
+
orientation: this.getCurrentOrientation(),
|
|
67
|
+
});
|
|
68
|
+
};
|
|
69
|
+
window.addEventListener("orientationchange", emit);
|
|
70
|
+
window.addEventListener("resize", emit);
|
|
71
|
+
this.orientationListenerBound = true;
|
|
72
|
+
}
|
|
73
|
+
async getOrientation() {
|
|
74
|
+
return { orientation: this.getCurrentOrientation() };
|
|
75
|
+
}
|
|
76
|
+
getSafeAreaInsets() {
|
|
77
|
+
throw new Error("Method not implemented.");
|
|
78
|
+
}
|
|
79
|
+
async getZoomButtonValues() {
|
|
80
|
+
throw new Error("getZoomButtonValues not supported under the web platform");
|
|
15
81
|
}
|
|
16
82
|
async getSupportedPictureSizes() {
|
|
17
83
|
throw new Error("getSupportedPictureSizes not supported under the web platform");
|
|
@@ -27,6 +93,7 @@ export class CameraPreviewWeb extends WebPlugin {
|
|
|
27
93
|
this.isStarted = false;
|
|
28
94
|
const parent = document.getElementById((options === null || options === void 0 ? void 0 : options.parent) || "");
|
|
29
95
|
const gridMode = (options === null || options === void 0 ? void 0 : options.gridMode) || "none";
|
|
96
|
+
const positioning = (options === null || options === void 0 ? void 0 : options.positioning) || "top";
|
|
30
97
|
if (options.position) {
|
|
31
98
|
this.isBackCamera = options.position === "rear";
|
|
32
99
|
}
|
|
@@ -34,7 +101,9 @@ export class CameraPreviewWeb extends WebPlugin {
|
|
|
34
101
|
if (video) {
|
|
35
102
|
video.remove();
|
|
36
103
|
}
|
|
37
|
-
const container = options.parent
|
|
104
|
+
const container = options.parent
|
|
105
|
+
? document.getElementById(options.parent)
|
|
106
|
+
: document.body;
|
|
38
107
|
if (!container) {
|
|
39
108
|
throw new Error("container not found");
|
|
40
109
|
}
|
|
@@ -44,45 +113,57 @@ export class CameraPreviewWeb extends WebPlugin {
|
|
|
44
113
|
this.videoElement.playsInline = true;
|
|
45
114
|
this.videoElement.muted = true;
|
|
46
115
|
this.videoElement.autoplay = true;
|
|
116
|
+
// Remove objectFit as we'll match camera's native aspect ratio
|
|
117
|
+
this.videoElement.style.backgroundColor = "transparent";
|
|
118
|
+
// Reset any default margins that might interfere
|
|
119
|
+
this.videoElement.style.margin = "0";
|
|
120
|
+
this.videoElement.style.padding = "0";
|
|
47
121
|
container.appendChild(this.videoElement);
|
|
48
122
|
if (options.toBack) {
|
|
49
123
|
this.videoElement.style.zIndex = "-1";
|
|
50
124
|
}
|
|
125
|
+
// Default to 4:3 if no aspect ratio or size specified
|
|
126
|
+
const useDefaultAspectRatio = !options.aspectRatio && !options.width && !options.height;
|
|
127
|
+
const effectiveAspectRatio = options.aspectRatio || (useDefaultAspectRatio ? "4:3" : null);
|
|
51
128
|
if (options.width) {
|
|
52
129
|
this.videoElement.width = options.width;
|
|
130
|
+
this.videoElement.style.width = `${options.width}px`;
|
|
53
131
|
}
|
|
54
132
|
if (options.height) {
|
|
55
133
|
this.videoElement.height = options.height;
|
|
56
|
-
|
|
57
|
-
|
|
134
|
+
this.videoElement.style.height = `${options.height}px`;
|
|
135
|
+
}
|
|
136
|
+
// Handle positioning - center if x or y not provided
|
|
137
|
+
const centerX = options.x === undefined;
|
|
138
|
+
const centerY = options.y === undefined;
|
|
139
|
+
// Always set position to absolute for proper positioning
|
|
140
|
+
this.videoElement.style.position = "absolute";
|
|
141
|
+
console.log("Initial positioning flags:", {
|
|
142
|
+
centerX,
|
|
143
|
+
centerY,
|
|
144
|
+
x: options.x,
|
|
145
|
+
y: options.y,
|
|
146
|
+
});
|
|
147
|
+
if (options.x !== undefined) {
|
|
58
148
|
this.videoElement.style.left = `${options.x}px`;
|
|
59
149
|
}
|
|
150
|
+
if (options.y !== undefined) {
|
|
151
|
+
this.videoElement.style.top = `${options.y}px`;
|
|
152
|
+
}
|
|
60
153
|
// Create and add grid overlay if needed
|
|
61
154
|
if (gridMode !== "none") {
|
|
62
155
|
const gridOverlay = this.createGridOverlay(gridMode);
|
|
63
156
|
gridOverlay.id = "camera-grid-overlay";
|
|
64
157
|
parent === null || parent === void 0 ? void 0 : parent.appendChild(gridOverlay);
|
|
65
158
|
}
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
if (options.width) {
|
|
73
|
-
this.videoElement.height = options.width / ratio;
|
|
74
|
-
}
|
|
75
|
-
else if (options.height) {
|
|
76
|
-
this.videoElement.width = options.height * ratio;
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
else {
|
|
80
|
-
this.videoElement.style.objectFit = 'cover';
|
|
81
|
-
}
|
|
159
|
+
// Aspect ratio handling is now done after getting camera stream
|
|
160
|
+
// Store centering flags for later use
|
|
161
|
+
const needsCenterX = centerX;
|
|
162
|
+
const needsCenterY = centerY;
|
|
163
|
+
console.log("Centering flags stored:", { needsCenterX, needsCenterY });
|
|
164
|
+
// First get the camera stream with basic constraints
|
|
82
165
|
const constraints = {
|
|
83
166
|
video: {
|
|
84
|
-
width: { ideal: this.videoElement.width || 640 },
|
|
85
|
-
height: { ideal: this.videoElement.height || window.innerHeight },
|
|
86
167
|
facingMode: this.isBackCamera ? "environment" : "user",
|
|
87
168
|
},
|
|
88
169
|
};
|
|
@@ -93,16 +174,226 @@ export class CameraPreviewWeb extends WebPlugin {
|
|
|
93
174
|
if (!this.videoElement) {
|
|
94
175
|
throw new Error("video element not found");
|
|
95
176
|
}
|
|
177
|
+
// Get the actual camera dimensions from the video track
|
|
178
|
+
const videoTrack = stream.getVideoTracks()[0];
|
|
179
|
+
const settings = videoTrack.getSettings();
|
|
180
|
+
const cameraWidth = settings.width || 640;
|
|
181
|
+
const cameraHeight = settings.height || 480;
|
|
182
|
+
const cameraAspectRatio = cameraWidth / cameraHeight;
|
|
183
|
+
console.log("Camera native dimensions:", {
|
|
184
|
+
width: cameraWidth,
|
|
185
|
+
height: cameraHeight,
|
|
186
|
+
aspectRatio: cameraAspectRatio,
|
|
187
|
+
});
|
|
188
|
+
console.log("Container dimensions:", {
|
|
189
|
+
width: container.offsetWidth,
|
|
190
|
+
height: container.offsetHeight,
|
|
191
|
+
id: container.id,
|
|
192
|
+
});
|
|
193
|
+
// Now adjust video element size based on camera's native aspect ratio
|
|
194
|
+
if (!options.width && !options.height && !options.aspectRatio) {
|
|
195
|
+
// No size specified, fit camera view within container bounds
|
|
196
|
+
const containerWidth = container.offsetWidth || window.innerWidth;
|
|
197
|
+
const containerHeight = container.offsetHeight || window.innerHeight;
|
|
198
|
+
// Calculate dimensions that fit within container while maintaining camera aspect ratio
|
|
199
|
+
let targetWidth, targetHeight;
|
|
200
|
+
// Try fitting to container width first
|
|
201
|
+
targetWidth = containerWidth;
|
|
202
|
+
targetHeight = targetWidth / cameraAspectRatio;
|
|
203
|
+
// If height exceeds container, fit to height instead
|
|
204
|
+
if (targetHeight > containerHeight) {
|
|
205
|
+
targetHeight = containerHeight;
|
|
206
|
+
targetWidth = targetHeight * cameraAspectRatio;
|
|
207
|
+
}
|
|
208
|
+
console.log("Video element dimensions:", {
|
|
209
|
+
width: targetWidth,
|
|
210
|
+
height: targetHeight,
|
|
211
|
+
container: { width: containerWidth, height: containerHeight },
|
|
212
|
+
});
|
|
213
|
+
this.videoElement.width = targetWidth;
|
|
214
|
+
this.videoElement.height = targetHeight;
|
|
215
|
+
this.videoElement.style.width = `${targetWidth}px`;
|
|
216
|
+
this.videoElement.style.height = `${targetHeight}px`;
|
|
217
|
+
// Center the video element within its parent container
|
|
218
|
+
if (needsCenterX || options.x === undefined) {
|
|
219
|
+
const x = Math.round((containerWidth - targetWidth) / 2);
|
|
220
|
+
this.videoElement.style.left = `${x}px`;
|
|
221
|
+
}
|
|
222
|
+
if (needsCenterY || options.y === undefined) {
|
|
223
|
+
let y;
|
|
224
|
+
switch (positioning) {
|
|
225
|
+
case "top":
|
|
226
|
+
y = 0;
|
|
227
|
+
break;
|
|
228
|
+
case "bottom":
|
|
229
|
+
y = window.innerHeight - targetHeight;
|
|
230
|
+
break;
|
|
231
|
+
case "center":
|
|
232
|
+
default:
|
|
233
|
+
y = Math.round((window.innerHeight - targetHeight) / 2);
|
|
234
|
+
break;
|
|
235
|
+
}
|
|
236
|
+
this.videoElement.style.setProperty("top", `${y}px`, "important");
|
|
237
|
+
// Force a style recalculation
|
|
238
|
+
this.videoElement.offsetHeight;
|
|
239
|
+
console.log("Positioning video:", {
|
|
240
|
+
positioning,
|
|
241
|
+
viewportHeight: window.innerHeight,
|
|
242
|
+
targetHeight,
|
|
243
|
+
calculatedY: y,
|
|
244
|
+
actualTop: this.videoElement.style.top,
|
|
245
|
+
position: this.videoElement.style.position,
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
else if (effectiveAspectRatio && !options.width && !options.height) {
|
|
250
|
+
// Aspect ratio specified but no size
|
|
251
|
+
const [widthRatio, heightRatio] = effectiveAspectRatio
|
|
252
|
+
.split(":")
|
|
253
|
+
.map(Number);
|
|
254
|
+
const targetRatio = widthRatio / heightRatio;
|
|
255
|
+
const viewportWidth = window.innerWidth;
|
|
256
|
+
const viewportHeight = window.innerHeight;
|
|
257
|
+
let targetWidth, targetHeight;
|
|
258
|
+
// Try fitting to viewport width first
|
|
259
|
+
targetWidth = viewportWidth;
|
|
260
|
+
targetHeight = targetWidth / targetRatio;
|
|
261
|
+
// If height exceeds viewport, fit to height instead
|
|
262
|
+
if (targetHeight > viewportHeight) {
|
|
263
|
+
targetHeight = viewportHeight;
|
|
264
|
+
targetWidth = targetHeight * targetRatio;
|
|
265
|
+
}
|
|
266
|
+
this.videoElement.width = targetWidth;
|
|
267
|
+
this.videoElement.height = targetHeight;
|
|
268
|
+
this.videoElement.style.width = `${targetWidth}px`;
|
|
269
|
+
this.videoElement.style.height = `${targetHeight}px`;
|
|
270
|
+
// Center the video element within its parent container
|
|
271
|
+
if (needsCenterX || options.x === undefined) {
|
|
272
|
+
const parentWidth = container.offsetWidth || viewportWidth;
|
|
273
|
+
const x = Math.round((parentWidth - targetWidth) / 2);
|
|
274
|
+
this.videoElement.style.left = `${x}px`;
|
|
275
|
+
}
|
|
276
|
+
if (needsCenterY || options.y === undefined) {
|
|
277
|
+
const parentHeight = container.offsetHeight || viewportHeight;
|
|
278
|
+
let y;
|
|
279
|
+
switch (positioning) {
|
|
280
|
+
case "top":
|
|
281
|
+
y = 0;
|
|
282
|
+
break;
|
|
283
|
+
case "bottom":
|
|
284
|
+
y = parentHeight - targetHeight;
|
|
285
|
+
break;
|
|
286
|
+
case "center":
|
|
287
|
+
default:
|
|
288
|
+
y = Math.round((parentHeight - targetHeight) / 2);
|
|
289
|
+
break;
|
|
290
|
+
}
|
|
291
|
+
this.videoElement.style.top = `${y}px`;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
96
294
|
this.videoElement.srcObject = stream;
|
|
97
295
|
if (!this.isBackCamera) {
|
|
98
296
|
this.videoElement.style.transform = "scaleX(-1)";
|
|
99
297
|
}
|
|
298
|
+
// Set initial zoom level if specified and supported
|
|
299
|
+
if (options.initialZoomLevel !== undefined &&
|
|
300
|
+
options.initialZoomLevel !== 1.0) {
|
|
301
|
+
// videoTrack already declared above
|
|
302
|
+
if (videoTrack) {
|
|
303
|
+
const capabilities = videoTrack.getCapabilities();
|
|
304
|
+
if (capabilities.zoom) {
|
|
305
|
+
const zoomLevel = options.initialZoomLevel;
|
|
306
|
+
const minZoom = capabilities.zoom.min || 1;
|
|
307
|
+
const maxZoom = capabilities.zoom.max || 1;
|
|
308
|
+
if (zoomLevel < minZoom || zoomLevel > maxZoom) {
|
|
309
|
+
stream.getTracks().forEach((track) => track.stop());
|
|
310
|
+
throw new Error(`Initial zoom level ${zoomLevel} is not available. Valid range is ${minZoom} to ${maxZoom}`);
|
|
311
|
+
}
|
|
312
|
+
try {
|
|
313
|
+
await videoTrack.applyConstraints({
|
|
314
|
+
advanced: [{ zoom: zoomLevel }],
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
catch (error) {
|
|
318
|
+
console.warn(`Failed to set initial zoom level: ${error}`);
|
|
319
|
+
// Don't throw, just continue without zoom
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
100
324
|
this.isStarted = true;
|
|
325
|
+
this.ensureOrientationListener();
|
|
326
|
+
// Wait for video to be ready and get actual dimensions
|
|
327
|
+
await new Promise((resolve) => {
|
|
328
|
+
const videoEl = this.videoElement;
|
|
329
|
+
if (!videoEl) {
|
|
330
|
+
throw new Error("video element not found");
|
|
331
|
+
}
|
|
332
|
+
if (videoEl.readyState >= 2) {
|
|
333
|
+
resolve();
|
|
334
|
+
}
|
|
335
|
+
else {
|
|
336
|
+
videoEl.addEventListener("loadeddata", () => resolve(), {
|
|
337
|
+
once: true,
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
});
|
|
341
|
+
// Ensure centering is applied after DOM updates
|
|
342
|
+
await new Promise((resolve) => requestAnimationFrame(resolve));
|
|
343
|
+
console.log("About to re-center, flags:", { needsCenterX, needsCenterY });
|
|
344
|
+
// Re-apply centering with correct parent dimensions
|
|
345
|
+
if (needsCenterX) {
|
|
346
|
+
const parentWidth = container.offsetWidth;
|
|
347
|
+
const x = Math.round((parentWidth - this.videoElement.offsetWidth) / 2);
|
|
348
|
+
this.videoElement.style.left = `${x}px`;
|
|
349
|
+
console.log("Re-centering X:", {
|
|
350
|
+
parentWidth,
|
|
351
|
+
videoWidth: this.videoElement.offsetWidth,
|
|
352
|
+
x,
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
if (needsCenterY) {
|
|
356
|
+
let y;
|
|
357
|
+
switch (positioning) {
|
|
358
|
+
case "top":
|
|
359
|
+
y = 0;
|
|
360
|
+
break;
|
|
361
|
+
case "bottom":
|
|
362
|
+
y = window.innerHeight - this.videoElement.offsetHeight;
|
|
363
|
+
break;
|
|
364
|
+
case "center":
|
|
365
|
+
default:
|
|
366
|
+
y = Math.round((window.innerHeight - this.videoElement.offsetHeight) / 2);
|
|
367
|
+
break;
|
|
368
|
+
}
|
|
369
|
+
this.videoElement.style.setProperty("top", `${y}px`, "important");
|
|
370
|
+
console.log("Re-positioning Y:", {
|
|
371
|
+
positioning,
|
|
372
|
+
viewportHeight: window.innerHeight,
|
|
373
|
+
videoHeight: this.videoElement.offsetHeight,
|
|
374
|
+
y,
|
|
375
|
+
position: this.videoElement.style.position,
|
|
376
|
+
top: this.videoElement.style.top,
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
// Get the actual rendered dimensions after video is loaded
|
|
380
|
+
const rect = this.videoElement.getBoundingClientRect();
|
|
381
|
+
const computedStyle = window.getComputedStyle(this.videoElement);
|
|
382
|
+
console.log("Final video element state:", {
|
|
383
|
+
rect: { x: rect.x, y: rect.y, width: rect.width, height: rect.height },
|
|
384
|
+
style: {
|
|
385
|
+
position: computedStyle.position,
|
|
386
|
+
left: computedStyle.left,
|
|
387
|
+
top: computedStyle.top,
|
|
388
|
+
width: computedStyle.width,
|
|
389
|
+
height: computedStyle.height,
|
|
390
|
+
},
|
|
391
|
+
});
|
|
101
392
|
return {
|
|
102
|
-
width:
|
|
103
|
-
height:
|
|
104
|
-
x:
|
|
105
|
-
y:
|
|
393
|
+
width: Math.round(rect.width),
|
|
394
|
+
height: Math.round(rect.height),
|
|
395
|
+
x: Math.round(rect.x),
|
|
396
|
+
y: Math.round(rect.y),
|
|
106
397
|
};
|
|
107
398
|
}
|
|
108
399
|
stopStream(stream) {
|
|
@@ -136,14 +427,57 @@ export class CameraPreviewWeb extends WebPlugin {
|
|
|
136
427
|
if (video && video.videoWidth > 0 && video.videoHeight > 0) {
|
|
137
428
|
const canvas = document.createElement("canvas");
|
|
138
429
|
const context = canvas.getContext("2d");
|
|
139
|
-
|
|
140
|
-
|
|
430
|
+
// Calculate capture dimensions
|
|
431
|
+
let captureWidth = video.videoWidth;
|
|
432
|
+
let captureHeight = video.videoHeight;
|
|
433
|
+
let sourceX = 0;
|
|
434
|
+
let sourceY = 0;
|
|
435
|
+
// Check for conflicting parameters
|
|
436
|
+
if (options.aspectRatio && (options.width || options.height)) {
|
|
437
|
+
reject(new Error("Cannot set both aspectRatio and size (width/height). Use setPreviewSize after start."));
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
// Handle aspect ratio if no width/height specified
|
|
441
|
+
if (!options.width && !options.height && options.aspectRatio) {
|
|
442
|
+
const [widthRatio, heightRatio] = options.aspectRatio
|
|
443
|
+
.split(":")
|
|
444
|
+
.map(Number);
|
|
445
|
+
if (widthRatio && heightRatio) {
|
|
446
|
+
// For capture in portrait orientation, swap the aspect ratio (16:9 becomes 9:16)
|
|
447
|
+
const isPortrait = video.videoHeight > video.videoWidth;
|
|
448
|
+
const targetAspectRatio = isPortrait
|
|
449
|
+
? heightRatio / widthRatio
|
|
450
|
+
: widthRatio / heightRatio;
|
|
451
|
+
const videoAspectRatio = video.videoWidth / video.videoHeight;
|
|
452
|
+
if (videoAspectRatio > targetAspectRatio) {
|
|
453
|
+
// Video is wider than target - crop sides
|
|
454
|
+
captureWidth = video.videoHeight * targetAspectRatio;
|
|
455
|
+
captureHeight = video.videoHeight;
|
|
456
|
+
sourceX = (video.videoWidth - captureWidth) / 2;
|
|
457
|
+
}
|
|
458
|
+
else {
|
|
459
|
+
// Video is taller than target - crop top/bottom
|
|
460
|
+
captureWidth = video.videoWidth;
|
|
461
|
+
captureHeight = video.videoWidth / targetAspectRatio;
|
|
462
|
+
sourceY = (video.videoHeight - captureHeight) / 2;
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
else if (options.width || options.height) {
|
|
467
|
+
// If width or height is specified, use them
|
|
468
|
+
if (options.width)
|
|
469
|
+
captureWidth = options.width;
|
|
470
|
+
if (options.height)
|
|
471
|
+
captureHeight = options.height;
|
|
472
|
+
}
|
|
473
|
+
canvas.width = captureWidth;
|
|
474
|
+
canvas.height = captureHeight;
|
|
141
475
|
// flip horizontally back camera isn't used
|
|
142
476
|
if (!this.isBackCamera) {
|
|
143
|
-
context === null || context === void 0 ? void 0 : context.translate(
|
|
477
|
+
context === null || context === void 0 ? void 0 : context.translate(captureWidth, 0);
|
|
144
478
|
context === null || context === void 0 ? void 0 : context.scale(-1, 1);
|
|
145
479
|
}
|
|
146
|
-
context === null || context === void 0 ? void 0 : context.drawImage(video, 0, 0,
|
|
480
|
+
context === null || context === void 0 ? void 0 : context.drawImage(video, sourceX, sourceY, captureWidth, captureHeight, 0, 0, captureWidth, captureHeight);
|
|
147
481
|
if (options.saveToGallery) {
|
|
148
482
|
// saveToGallery is not supported on web
|
|
149
483
|
}
|
|
@@ -241,7 +575,7 @@ export class CameraPreviewWeb extends WebPlugin {
|
|
|
241
575
|
throw new Error("getAvailableDevices not supported under the web platform");
|
|
242
576
|
}
|
|
243
577
|
const devices = await navigator.mediaDevices.enumerateDevices();
|
|
244
|
-
const videoDevices = devices.filter(device => device.kind ===
|
|
578
|
+
const videoDevices = devices.filter((device) => device.kind === "videoinput");
|
|
245
579
|
// Group devices by position (front/back)
|
|
246
580
|
const frontDevices = [];
|
|
247
581
|
const backDevices = [];
|
|
@@ -251,15 +585,19 @@ export class CameraPreviewWeb extends WebPlugin {
|
|
|
251
585
|
// Determine device type based on label
|
|
252
586
|
let deviceType = DeviceType.WIDE_ANGLE;
|
|
253
587
|
let baseZoomRatio = 1.0;
|
|
254
|
-
if (labelLower.includes(
|
|
588
|
+
if (labelLower.includes("ultra") || labelLower.includes("0.5")) {
|
|
255
589
|
deviceType = DeviceType.ULTRA_WIDE;
|
|
256
590
|
baseZoomRatio = 0.5;
|
|
257
591
|
}
|
|
258
|
-
else if (labelLower.includes(
|
|
592
|
+
else if (labelLower.includes("telephoto") ||
|
|
593
|
+
labelLower.includes("tele") ||
|
|
594
|
+
labelLower.includes("2x") ||
|
|
595
|
+
labelLower.includes("3x")) {
|
|
259
596
|
deviceType = DeviceType.TELEPHOTO;
|
|
260
597
|
baseZoomRatio = 2.0;
|
|
261
598
|
}
|
|
262
|
-
else if (labelLower.includes(
|
|
599
|
+
else if (labelLower.includes("depth") ||
|
|
600
|
+
labelLower.includes("truedepth")) {
|
|
263
601
|
deviceType = DeviceType.TRUE_DEPTH;
|
|
264
602
|
baseZoomRatio = 1.0;
|
|
265
603
|
}
|
|
@@ -270,10 +608,10 @@ export class CameraPreviewWeb extends WebPlugin {
|
|
|
270
608
|
focalLength: 4.25,
|
|
271
609
|
baseZoomRatio,
|
|
272
610
|
minZoom: 1.0,
|
|
273
|
-
maxZoom: 1.0
|
|
611
|
+
maxZoom: 1.0,
|
|
274
612
|
};
|
|
275
613
|
// Determine position and add to appropriate array
|
|
276
|
-
if (labelLower.includes(
|
|
614
|
+
if (labelLower.includes("back") || labelLower.includes("rear")) {
|
|
277
615
|
backDevices.push(lensInfo);
|
|
278
616
|
}
|
|
279
617
|
else {
|
|
@@ -288,8 +626,8 @@ export class CameraPreviewWeb extends WebPlugin {
|
|
|
288
626
|
position: "front",
|
|
289
627
|
lenses: frontDevices,
|
|
290
628
|
isLogical: false,
|
|
291
|
-
minZoom: Math.min(...frontDevices.map(d => d.minZoom)),
|
|
292
|
-
maxZoom: Math.max(...frontDevices.map(d => d.maxZoom))
|
|
629
|
+
minZoom: Math.min(...frontDevices.map((d) => d.minZoom)),
|
|
630
|
+
maxZoom: Math.max(...frontDevices.map((d) => d.maxZoom)),
|
|
293
631
|
});
|
|
294
632
|
}
|
|
295
633
|
if (backDevices.length > 0) {
|
|
@@ -299,8 +637,8 @@ export class CameraPreviewWeb extends WebPlugin {
|
|
|
299
637
|
position: "rear",
|
|
300
638
|
lenses: backDevices,
|
|
301
639
|
isLogical: false,
|
|
302
|
-
minZoom: Math.min(...backDevices.map(d => d.minZoom)),
|
|
303
|
-
maxZoom: Math.max(...backDevices.map(d => d.maxZoom))
|
|
640
|
+
minZoom: Math.min(...backDevices.map((d) => d.minZoom)),
|
|
641
|
+
maxZoom: Math.max(...backDevices.map((d) => d.maxZoom)),
|
|
304
642
|
});
|
|
305
643
|
}
|
|
306
644
|
return { devices: result };
|
|
@@ -325,18 +663,22 @@ export class CameraPreviewWeb extends WebPlugin {
|
|
|
325
663
|
let baseZoomRatio = 1.0;
|
|
326
664
|
if (this.currentDeviceId) {
|
|
327
665
|
const devices = await navigator.mediaDevices.enumerateDevices();
|
|
328
|
-
const device = devices.find(d => d.deviceId === this.currentDeviceId);
|
|
666
|
+
const device = devices.find((d) => d.deviceId === this.currentDeviceId);
|
|
329
667
|
if (device) {
|
|
330
668
|
const labelLower = device.label.toLowerCase();
|
|
331
|
-
if (labelLower.includes(
|
|
669
|
+
if (labelLower.includes("ultra") || labelLower.includes("0.5")) {
|
|
332
670
|
deviceType = DeviceType.ULTRA_WIDE;
|
|
333
671
|
baseZoomRatio = 0.5;
|
|
334
672
|
}
|
|
335
|
-
else if (labelLower.includes(
|
|
673
|
+
else if (labelLower.includes("telephoto") ||
|
|
674
|
+
labelLower.includes("tele") ||
|
|
675
|
+
labelLower.includes("2x") ||
|
|
676
|
+
labelLower.includes("3x")) {
|
|
336
677
|
deviceType = DeviceType.TELEPHOTO;
|
|
337
678
|
baseZoomRatio = 2.0;
|
|
338
679
|
}
|
|
339
|
-
else if (labelLower.includes(
|
|
680
|
+
else if (labelLower.includes("depth") ||
|
|
681
|
+
labelLower.includes("truedepth")) {
|
|
340
682
|
deviceType = DeviceType.TRUE_DEPTH;
|
|
341
683
|
baseZoomRatio = 1.0;
|
|
342
684
|
}
|
|
@@ -347,7 +689,7 @@ export class CameraPreviewWeb extends WebPlugin {
|
|
|
347
689
|
focalLength: 4.25,
|
|
348
690
|
deviceType,
|
|
349
691
|
baseZoomRatio,
|
|
350
|
-
digitalZoom: currentZoom / baseZoomRatio
|
|
692
|
+
digitalZoom: currentZoom / baseZoomRatio,
|
|
351
693
|
};
|
|
352
694
|
return {
|
|
353
695
|
min: capabilities.zoom.min || 1,
|
|
@@ -371,9 +713,10 @@ export class CameraPreviewWeb extends WebPlugin {
|
|
|
371
713
|
throw new Error("zoom not supported by this device");
|
|
372
714
|
}
|
|
373
715
|
const zoomLevel = Math.max(capabilities.zoom.min || 1, Math.min(capabilities.zoom.max || 1, options.level));
|
|
716
|
+
// Note: autoFocus is not supported on web platform
|
|
374
717
|
try {
|
|
375
718
|
await videoTrack.applyConstraints({
|
|
376
|
-
advanced: [{ zoom: zoomLevel }]
|
|
719
|
+
advanced: [{ zoom: zoomLevel }],
|
|
377
720
|
});
|
|
378
721
|
}
|
|
379
722
|
catch (error) {
|
|
@@ -406,8 +749,11 @@ export class CameraPreviewWeb extends WebPlugin {
|
|
|
406
749
|
try {
|
|
407
750
|
// Try to determine camera position from device
|
|
408
751
|
const devices = await navigator.mediaDevices.enumerateDevices();
|
|
409
|
-
const device = devices.find(d => d.deviceId === options.deviceId);
|
|
410
|
-
this.isBackCamera =
|
|
752
|
+
const device = devices.find((d) => d.deviceId === options.deviceId);
|
|
753
|
+
this.isBackCamera =
|
|
754
|
+
(device === null || device === void 0 ? void 0 : device.label.toLowerCase().includes("back")) ||
|
|
755
|
+
(device === null || device === void 0 ? void 0 : device.label.toLowerCase().includes("rear")) ||
|
|
756
|
+
false;
|
|
411
757
|
const stream = await navigator.mediaDevices.getUserMedia(constraints);
|
|
412
758
|
video.srcObject = stream;
|
|
413
759
|
// Update video transform based on camera
|
|
@@ -435,15 +781,15 @@ export class CameraPreviewWeb extends WebPlugin {
|
|
|
435
781
|
if (width && height) {
|
|
436
782
|
const ratio = width / height;
|
|
437
783
|
// Check for portrait camera ratios: 4:3 -> 3:4, 16:9 -> 9:16
|
|
438
|
-
if (Math.abs(ratio -
|
|
439
|
-
return { aspectRatio:
|
|
784
|
+
if (Math.abs(ratio - 3 / 4) < 0.01) {
|
|
785
|
+
return { aspectRatio: "4:3" };
|
|
440
786
|
}
|
|
441
|
-
if (Math.abs(ratio -
|
|
442
|
-
return { aspectRatio:
|
|
787
|
+
if (Math.abs(ratio - 9 / 16) < 0.01) {
|
|
788
|
+
return { aspectRatio: "16:9" };
|
|
443
789
|
}
|
|
444
790
|
}
|
|
445
791
|
// Default to 4:3 if no specific aspect ratio is matched
|
|
446
|
-
return { aspectRatio:
|
|
792
|
+
return { aspectRatio: "4:3" };
|
|
447
793
|
}
|
|
448
794
|
async setAspectRatio(options) {
|
|
449
795
|
const video = document.getElementById(DEFAULT_VIDEO_ID);
|
|
@@ -451,7 +797,9 @@ export class CameraPreviewWeb extends WebPlugin {
|
|
|
451
797
|
throw new Error("camera is not running");
|
|
452
798
|
}
|
|
453
799
|
if (options.aspectRatio) {
|
|
454
|
-
const [widthRatio, heightRatio] = options.aspectRatio
|
|
800
|
+
const [widthRatio, heightRatio] = options.aspectRatio
|
|
801
|
+
.split(":")
|
|
802
|
+
.map(Number);
|
|
455
803
|
// For camera, use portrait orientation: 4:3 becomes 3:4, 16:9 becomes 9:16
|
|
456
804
|
const ratio = heightRatio / widthRatio;
|
|
457
805
|
// Get current position and size
|
|
@@ -487,22 +835,26 @@ export class CameraPreviewWeb extends WebPlugin {
|
|
|
487
835
|
video.style.height = `${newHeight}px`;
|
|
488
836
|
video.style.left = `${x}px`;
|
|
489
837
|
video.style.top = `${y}px`;
|
|
490
|
-
video.style.position =
|
|
838
|
+
video.style.position = "absolute";
|
|
839
|
+
const offsetX = newWidth / 8;
|
|
840
|
+
const offsetY = newHeight / 8;
|
|
491
841
|
return {
|
|
492
842
|
width: Math.round(newWidth),
|
|
493
843
|
height: Math.round(newHeight),
|
|
494
|
-
x: Math.round(x),
|
|
495
|
-
y: Math.round(y)
|
|
844
|
+
x: Math.round(x + offsetX),
|
|
845
|
+
y: Math.round(y + offsetY),
|
|
496
846
|
};
|
|
497
847
|
}
|
|
498
848
|
else {
|
|
499
|
-
video.style.objectFit =
|
|
849
|
+
video.style.objectFit = "cover";
|
|
500
850
|
const rect = video.getBoundingClientRect();
|
|
851
|
+
const offsetX = rect.width / 8;
|
|
852
|
+
const offsetY = rect.height / 8;
|
|
501
853
|
return {
|
|
502
854
|
width: Math.round(rect.width),
|
|
503
855
|
height: Math.round(rect.height),
|
|
504
|
-
x: Math.round(rect.left),
|
|
505
|
-
y: Math.round(rect.top)
|
|
856
|
+
x: Math.round(rect.left + offsetX),
|
|
857
|
+
y: Math.round(rect.top + offsetY),
|
|
506
858
|
};
|
|
507
859
|
}
|
|
508
860
|
}
|
|
@@ -554,18 +906,20 @@ export class CameraPreviewWeb extends WebPlugin {
|
|
|
554
906
|
}
|
|
555
907
|
async getGridMode() {
|
|
556
908
|
// Web implementation - default to none
|
|
557
|
-
return { gridMode:
|
|
909
|
+
return { gridMode: "none" };
|
|
558
910
|
}
|
|
559
911
|
async getPreviewSize() {
|
|
560
912
|
const video = document.getElementById(DEFAULT_VIDEO_ID);
|
|
561
913
|
if (!video) {
|
|
562
914
|
throw new Error("camera is not running");
|
|
563
915
|
}
|
|
916
|
+
const offsetX = video.width / 8;
|
|
917
|
+
const offsetY = video.height / 8;
|
|
564
918
|
return {
|
|
565
|
-
x: video.offsetLeft,
|
|
566
|
-
y: video.offsetTop,
|
|
919
|
+
x: video.offsetLeft + offsetX,
|
|
920
|
+
y: video.offsetTop + offsetY,
|
|
567
921
|
width: video.width,
|
|
568
|
-
height: video.height
|
|
922
|
+
height: video.height,
|
|
569
923
|
};
|
|
570
924
|
}
|
|
571
925
|
async setPreviewSize(options) {
|
|
@@ -577,12 +931,56 @@ export class CameraPreviewWeb extends WebPlugin {
|
|
|
577
931
|
video.style.top = `${options.y}px`;
|
|
578
932
|
video.width = options.width;
|
|
579
933
|
video.height = options.height;
|
|
934
|
+
const offsetX = options.width / 8;
|
|
935
|
+
const offsetY = options.height / 8;
|
|
580
936
|
return {
|
|
581
937
|
width: options.width,
|
|
582
938
|
height: options.height,
|
|
583
|
-
x: options.x,
|
|
584
|
-
y: options.y
|
|
939
|
+
x: options.x + offsetX,
|
|
940
|
+
y: options.y + offsetY,
|
|
585
941
|
};
|
|
586
942
|
}
|
|
943
|
+
async setFocus(options) {
|
|
944
|
+
// Reject if values are outside 0-1 range
|
|
945
|
+
if (options.x < 0 || options.x > 1 || options.y < 0 || options.y > 1) {
|
|
946
|
+
throw new Error("Focus coordinates must be between 0 and 1");
|
|
947
|
+
}
|
|
948
|
+
const video = document.getElementById(DEFAULT_VIDEO_ID);
|
|
949
|
+
if (!(video === null || video === void 0 ? void 0 : video.srcObject)) {
|
|
950
|
+
throw new Error("camera is not running");
|
|
951
|
+
}
|
|
952
|
+
const stream = video.srcObject;
|
|
953
|
+
const videoTrack = stream.getVideoTracks()[0];
|
|
954
|
+
if (!videoTrack) {
|
|
955
|
+
throw new Error("no video track found");
|
|
956
|
+
}
|
|
957
|
+
const capabilities = videoTrack.getCapabilities();
|
|
958
|
+
// Check if focusing is supported
|
|
959
|
+
if (capabilities.focusMode) {
|
|
960
|
+
try {
|
|
961
|
+
// Web API supports focus mode settings but not coordinate-based focus
|
|
962
|
+
// Setting to manual mode allows for coordinate focus if supported
|
|
963
|
+
await videoTrack.applyConstraints({
|
|
964
|
+
advanced: [
|
|
965
|
+
{
|
|
966
|
+
focusMode: "manual",
|
|
967
|
+
focusDistance: 0.5, // Mid-range focus as fallback
|
|
968
|
+
},
|
|
969
|
+
],
|
|
970
|
+
});
|
|
971
|
+
}
|
|
972
|
+
catch (error) {
|
|
973
|
+
console.warn(`setFocus is not fully supported on this device: ${error}. Focus coordinates (${options.x}, ${options.y}) were provided but cannot be applied.`);
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
else {
|
|
977
|
+
console.warn("Focus control is not supported on this device. Focus coordinates were provided but cannot be applied.");
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
async deleteFile(_options) {
|
|
981
|
+
// Mark parameter as intentionally unused to satisfy linter
|
|
982
|
+
void _options;
|
|
983
|
+
throw new Error("deleteFile not supported under the web platform");
|
|
984
|
+
}
|
|
587
985
|
}
|
|
588
986
|
//# sourceMappingURL=web.js.map
|