@capgo/camera-preview 7.4.0-beta.13 → 7.4.0-beta.16

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/dist/esm/web.js CHANGED
@@ -46,47 +46,57 @@ export class CameraPreviewWeb extends WebPlugin {
46
46
  this.videoElement.playsInline = true;
47
47
  this.videoElement.muted = true;
48
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";
49
54
  container.appendChild(this.videoElement);
50
55
  if (options.toBack) {
51
56
  this.videoElement.style.zIndex = "-1";
52
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);
53
61
  if (options.width) {
54
62
  this.videoElement.width = options.width;
63
+ this.videoElement.style.width = `${options.width}px`;
55
64
  }
56
65
  if (options.height) {
57
66
  this.videoElement.height = options.height;
58
- }
59
- if (options.x) {
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) {
60
81
  this.videoElement.style.left = `${options.x}px`;
61
82
  }
83
+ if (options.y !== undefined) {
84
+ this.videoElement.style.top = `${options.y}px`;
85
+ }
62
86
  // Create and add grid overlay if needed
63
87
  if (gridMode !== "none") {
64
88
  const gridOverlay = this.createGridOverlay(gridMode);
65
89
  gridOverlay.id = "camera-grid-overlay";
66
90
  parent === null || parent === void 0 ? void 0 : parent.appendChild(gridOverlay);
67
91
  }
68
- if (options.y) {
69
- this.videoElement.style.top = `${options.y}px`;
70
- }
71
- if (options.aspectRatio) {
72
- const [widthRatio, heightRatio] = options.aspectRatio
73
- .split(":")
74
- .map(Number);
75
- const ratio = widthRatio / heightRatio;
76
- if (options.width) {
77
- this.videoElement.height = options.width / ratio;
78
- }
79
- else if (options.height) {
80
- this.videoElement.width = options.height * ratio;
81
- }
82
- }
83
- else {
84
- this.videoElement.style.objectFit = "cover";
85
- }
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
86
98
  const constraints = {
87
99
  video: {
88
- width: { ideal: this.videoElement.width || 640 },
89
- height: { ideal: this.videoElement.height || window.innerHeight },
90
100
  facingMode: this.isBackCamera ? "environment" : "user",
91
101
  },
92
102
  };
@@ -97,16 +107,182 @@ export class CameraPreviewWeb extends WebPlugin {
97
107
  if (!this.videoElement) {
98
108
  throw new Error("video element not found");
99
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`;
154
+ }
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
+ });
167
+ }
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
+ }
100
202
  this.videoElement.srcObject = stream;
101
203
  if (!this.isBackCamera) {
102
204
  this.videoElement.style.transform = "scaleX(-1)";
103
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
+ });
223
+ }
224
+ catch (error) {
225
+ console.warn(`Failed to set initial zoom level: ${error}`);
226
+ // Don't throw, just continue without zoom
227
+ }
228
+ }
229
+ }
230
+ }
104
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,
240
+ });
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
+ });
256
+ }
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
+ });
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
+ });
105
281
  return {
106
- width: this.videoElement.width,
107
- height: this.videoElement.height,
108
- x: this.videoElement.getBoundingClientRect().x,
109
- y: this.videoElement.getBoundingClientRect().y,
282
+ width: Math.round(rect.width),
283
+ height: Math.round(rect.height),
284
+ x: Math.round(rect.x),
285
+ y: Math.round(rect.y),
110
286
  };
111
287
  }
112
288
  stopStream(stream) {
@@ -140,14 +316,53 @@ export class CameraPreviewWeb extends WebPlugin {
140
316
  if (video && video.videoWidth > 0 && video.videoHeight > 0) {
141
317
  const canvas = document.createElement("canvas");
142
318
  const context = canvas.getContext("2d");
143
- canvas.width = video.videoWidth;
144
- canvas.height = video.videoHeight;
319
+ // Calculate capture dimensions
320
+ let captureWidth = video.videoWidth;
321
+ let captureHeight = video.videoHeight;
322
+ let sourceX = 0;
323
+ let sourceY = 0;
324
+ // Check for conflicting parameters
325
+ if (options.aspectRatio && (options.width || options.height)) {
326
+ reject(new Error("Cannot set both aspectRatio and size (width/height). Use setPreviewSize after start."));
327
+ return;
328
+ }
329
+ // Handle aspect ratio if no width/height specified
330
+ if (!options.width && !options.height && options.aspectRatio) {
331
+ const [widthRatio, heightRatio] = options.aspectRatio.split(':').map(Number);
332
+ if (widthRatio && heightRatio) {
333
+ // For capture in portrait orientation, swap the aspect ratio (16:9 becomes 9:16)
334
+ const isPortrait = video.videoHeight > video.videoWidth;
335
+ const targetAspectRatio = isPortrait ? heightRatio / widthRatio : widthRatio / heightRatio;
336
+ const videoAspectRatio = video.videoWidth / video.videoHeight;
337
+ if (videoAspectRatio > targetAspectRatio) {
338
+ // Video is wider than target - crop sides
339
+ captureWidth = video.videoHeight * targetAspectRatio;
340
+ captureHeight = video.videoHeight;
341
+ sourceX = (video.videoWidth - captureWidth) / 2;
342
+ }
343
+ else {
344
+ // Video is taller than target - crop top/bottom
345
+ captureWidth = video.videoWidth;
346
+ captureHeight = video.videoWidth / targetAspectRatio;
347
+ sourceY = (video.videoHeight - captureHeight) / 2;
348
+ }
349
+ }
350
+ }
351
+ else if (options.width || options.height) {
352
+ // If width or height is specified, use them
353
+ if (options.width)
354
+ captureWidth = options.width;
355
+ if (options.height)
356
+ captureHeight = options.height;
357
+ }
358
+ canvas.width = captureWidth;
359
+ canvas.height = captureHeight;
145
360
  // flip horizontally back camera isn't used
146
361
  if (!this.isBackCamera) {
147
- context === null || context === void 0 ? void 0 : context.translate(video.videoWidth, 0);
362
+ context === null || context === void 0 ? void 0 : context.translate(captureWidth, 0);
148
363
  context === null || context === void 0 ? void 0 : context.scale(-1, 1);
149
364
  }
150
- context === null || context === void 0 ? void 0 : context.drawImage(video, 0, 0, video.videoWidth, video.videoHeight);
365
+ context === null || context === void 0 ? void 0 : context.drawImage(video, sourceX, sourceY, captureWidth, captureHeight, 0, 0, captureWidth, captureHeight);
151
366
  if (options.saveToGallery) {
152
367
  // saveToGallery is not supported on web
153
368
  }
@@ -383,6 +598,7 @@ export class CameraPreviewWeb extends WebPlugin {
383
598
  throw new Error("zoom not supported by this device");
384
599
  }
385
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
386
602
  try {
387
603
  await videoTrack.applyConstraints({
388
604
  advanced: [{ zoom: zoomLevel }],
@@ -505,21 +721,25 @@ export class CameraPreviewWeb extends WebPlugin {
505
721
  video.style.left = `${x}px`;
506
722
  video.style.top = `${y}px`;
507
723
  video.style.position = "absolute";
724
+ const offsetX = newWidth / 8;
725
+ const offsetY = newHeight / 8;
508
726
  return {
509
727
  width: Math.round(newWidth),
510
728
  height: Math.round(newHeight),
511
- x: Math.round(x),
512
- y: Math.round(y),
729
+ x: Math.round(x + offsetX),
730
+ y: Math.round(y + offsetY),
513
731
  };
514
732
  }
515
733
  else {
516
734
  video.style.objectFit = "cover";
517
735
  const rect = video.getBoundingClientRect();
736
+ const offsetX = rect.width / 8;
737
+ const offsetY = rect.height / 8;
518
738
  return {
519
739
  width: Math.round(rect.width),
520
740
  height: Math.round(rect.height),
521
- x: Math.round(rect.left),
522
- y: Math.round(rect.top),
741
+ x: Math.round(rect.left + offsetX),
742
+ y: Math.round(rect.top + offsetY),
523
743
  };
524
744
  }
525
745
  }
@@ -578,9 +798,11 @@ export class CameraPreviewWeb extends WebPlugin {
578
798
  if (!video) {
579
799
  throw new Error("camera is not running");
580
800
  }
801
+ const offsetX = video.width / 8;
802
+ const offsetY = video.height / 8;
581
803
  return {
582
- x: video.offsetLeft,
583
- y: video.offsetTop,
804
+ x: video.offsetLeft + offsetX,
805
+ y: video.offsetTop + offsetY,
584
806
  width: video.width,
585
807
  height: video.height,
586
808
  };
@@ -594,14 +816,20 @@ export class CameraPreviewWeb extends WebPlugin {
594
816
  video.style.top = `${options.y}px`;
595
817
  video.width = options.width;
596
818
  video.height = options.height;
819
+ const offsetX = options.width / 8;
820
+ const offsetY = options.height / 8;
597
821
  return {
598
822
  width: options.width,
599
823
  height: options.height,
600
- x: options.x,
601
- y: options.y,
824
+ x: options.x + offsetX,
825
+ y: options.y + offsetY,
602
826
  };
603
827
  }
604
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
+ }
605
833
  const video = document.getElementById(DEFAULT_VIDEO_ID);
606
834
  if (!(video === null || video === void 0 ? void 0 : video.srcObject)) {
607
835
  throw new Error("camera is not running");