@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.
@@ -63,47 +63,57 @@ class CameraPreviewWeb extends core.WebPlugin {
63
63
  this.videoElement.playsInline = true;
64
64
  this.videoElement.muted = true;
65
65
  this.videoElement.autoplay = true;
66
+ // Remove objectFit as we'll match camera's native aspect ratio
67
+ this.videoElement.style.backgroundColor = "transparent";
68
+ // Reset any default margins that might interfere
69
+ this.videoElement.style.margin = "0";
70
+ this.videoElement.style.padding = "0";
66
71
  container.appendChild(this.videoElement);
67
72
  if (options.toBack) {
68
73
  this.videoElement.style.zIndex = "-1";
69
74
  }
75
+ // Default to 16:9 vertical (9:16 for portrait) if no aspect ratio or size specified
76
+ const useDefaultAspectRatio = !options.aspectRatio && !options.width && !options.height;
77
+ const effectiveAspectRatio = options.aspectRatio || (useDefaultAspectRatio ? "16:9" : null);
70
78
  if (options.width) {
71
79
  this.videoElement.width = options.width;
80
+ this.videoElement.style.width = `${options.width}px`;
72
81
  }
73
82
  if (options.height) {
74
83
  this.videoElement.height = options.height;
75
- }
76
- if (options.x) {
84
+ this.videoElement.style.height = `${options.height}px`;
85
+ }
86
+ // Handle positioning - center if x or y not provided
87
+ const centerX = options.x === undefined;
88
+ const centerY = options.y === undefined;
89
+ // Always set position to absolute for proper positioning
90
+ this.videoElement.style.position = "absolute";
91
+ console.log("Initial positioning flags:", {
92
+ centerX,
93
+ centerY,
94
+ x: options.x,
95
+ y: options.y,
96
+ });
97
+ if (options.x !== undefined) {
77
98
  this.videoElement.style.left = `${options.x}px`;
78
99
  }
100
+ if (options.y !== undefined) {
101
+ this.videoElement.style.top = `${options.y}px`;
102
+ }
79
103
  // Create and add grid overlay if needed
80
104
  if (gridMode !== "none") {
81
105
  const gridOverlay = this.createGridOverlay(gridMode);
82
106
  gridOverlay.id = "camera-grid-overlay";
83
107
  parent === null || parent === void 0 ? void 0 : parent.appendChild(gridOverlay);
84
108
  }
85
- if (options.y) {
86
- this.videoElement.style.top = `${options.y}px`;
87
- }
88
- if (options.aspectRatio) {
89
- const [widthRatio, heightRatio] = options.aspectRatio
90
- .split(":")
91
- .map(Number);
92
- const ratio = widthRatio / heightRatio;
93
- if (options.width) {
94
- this.videoElement.height = options.width / ratio;
95
- }
96
- else if (options.height) {
97
- this.videoElement.width = options.height * ratio;
98
- }
99
- }
100
- else {
101
- this.videoElement.style.objectFit = "cover";
102
- }
109
+ // Aspect ratio handling is now done after getting camera stream
110
+ // Store centering flags for later use
111
+ const needsCenterX = centerX;
112
+ const needsCenterY = centerY;
113
+ console.log("Centering flags stored:", { needsCenterX, needsCenterY });
114
+ // First get the camera stream with basic constraints
103
115
  const constraints = {
104
116
  video: {
105
- width: { ideal: this.videoElement.width || 640 },
106
- height: { ideal: this.videoElement.height || window.innerHeight },
107
117
  facingMode: this.isBackCamera ? "environment" : "user",
108
118
  },
109
119
  };
@@ -114,16 +124,182 @@ class CameraPreviewWeb extends core.WebPlugin {
114
124
  if (!this.videoElement) {
115
125
  throw new Error("video element not found");
116
126
  }
127
+ // Get the actual camera dimensions from the video track
128
+ const videoTrack = stream.getVideoTracks()[0];
129
+ const settings = videoTrack.getSettings();
130
+ const cameraWidth = settings.width || 640;
131
+ const cameraHeight = settings.height || 480;
132
+ const cameraAspectRatio = cameraWidth / cameraHeight;
133
+ console.log("Camera native dimensions:", {
134
+ width: cameraWidth,
135
+ height: cameraHeight,
136
+ aspectRatio: cameraAspectRatio,
137
+ });
138
+ console.log("Container dimensions:", {
139
+ width: container.offsetWidth,
140
+ height: container.offsetHeight,
141
+ id: container.id,
142
+ });
143
+ // Now adjust video element size based on camera's native aspect ratio
144
+ if (!options.width && !options.height && !options.aspectRatio) {
145
+ // No size specified, fit camera view within container bounds
146
+ const containerWidth = container.offsetWidth || window.innerWidth;
147
+ const containerHeight = container.offsetHeight || window.innerHeight;
148
+ // Calculate dimensions that fit within container while maintaining camera aspect ratio
149
+ let targetWidth, targetHeight;
150
+ // Try fitting to container width first
151
+ targetWidth = containerWidth;
152
+ targetHeight = targetWidth / cameraAspectRatio;
153
+ // If height exceeds container, fit to height instead
154
+ if (targetHeight > containerHeight) {
155
+ targetHeight = containerHeight;
156
+ targetWidth = targetHeight * cameraAspectRatio;
157
+ }
158
+ console.log("Video element dimensions:", {
159
+ width: targetWidth,
160
+ height: targetHeight,
161
+ container: { width: containerWidth, height: containerHeight },
162
+ });
163
+ this.videoElement.width = targetWidth;
164
+ this.videoElement.height = targetHeight;
165
+ this.videoElement.style.width = `${targetWidth}px`;
166
+ this.videoElement.style.height = `${targetHeight}px`;
167
+ // Center the video element within its parent container
168
+ if (needsCenterX || options.x === undefined) {
169
+ const x = Math.round((containerWidth - targetWidth) / 2);
170
+ this.videoElement.style.left = `${x}px`;
171
+ }
172
+ if (needsCenterY || options.y === undefined) {
173
+ const y = Math.round((window.innerHeight - targetHeight) / 2);
174
+ this.videoElement.style.setProperty("top", `${y}px`, "important");
175
+ // Force a style recalculation
176
+ this.videoElement.offsetHeight;
177
+ console.log("Centering video:", {
178
+ viewportHeight: window.innerHeight,
179
+ targetHeight,
180
+ calculatedY: y,
181
+ actualTop: this.videoElement.style.top,
182
+ position: this.videoElement.style.position,
183
+ });
184
+ }
185
+ }
186
+ else if (effectiveAspectRatio && !options.width && !options.height) {
187
+ // Aspect ratio specified but no size
188
+ const [widthRatio, heightRatio] = effectiveAspectRatio
189
+ .split(":")
190
+ .map(Number);
191
+ const targetRatio = widthRatio / heightRatio;
192
+ const viewportWidth = window.innerWidth;
193
+ const viewportHeight = window.innerHeight;
194
+ let targetWidth, targetHeight;
195
+ // Try fitting to viewport width first
196
+ targetWidth = viewportWidth;
197
+ targetHeight = targetWidth / targetRatio;
198
+ // If height exceeds viewport, fit to height instead
199
+ if (targetHeight > viewportHeight) {
200
+ targetHeight = viewportHeight;
201
+ targetWidth = targetHeight * targetRatio;
202
+ }
203
+ this.videoElement.width = targetWidth;
204
+ this.videoElement.height = targetHeight;
205
+ this.videoElement.style.width = `${targetWidth}px`;
206
+ this.videoElement.style.height = `${targetHeight}px`;
207
+ // Center the video element within its parent container
208
+ if (needsCenterX || options.x === undefined) {
209
+ const parentWidth = container.offsetWidth || viewportWidth;
210
+ const x = Math.round((parentWidth - targetWidth) / 2);
211
+ this.videoElement.style.left = `${x}px`;
212
+ }
213
+ if (needsCenterY || options.y === undefined) {
214
+ const parentHeight = container.offsetHeight || viewportHeight;
215
+ const y = Math.round((parentHeight - targetHeight) / 2);
216
+ this.videoElement.style.top = `${y}px`;
217
+ }
218
+ }
117
219
  this.videoElement.srcObject = stream;
118
220
  if (!this.isBackCamera) {
119
221
  this.videoElement.style.transform = "scaleX(-1)";
120
222
  }
223
+ // Set initial zoom level if specified and supported
224
+ if (options.initialZoomLevel && options.initialZoomLevel !== 1.0) {
225
+ // videoTrack already declared above
226
+ if (videoTrack) {
227
+ const capabilities = videoTrack.getCapabilities();
228
+ if (capabilities.zoom) {
229
+ const zoomLevel = options.initialZoomLevel;
230
+ const minZoom = capabilities.zoom.min || 1;
231
+ const maxZoom = capabilities.zoom.max || 1;
232
+ if (zoomLevel < minZoom || zoomLevel > maxZoom) {
233
+ stream.getTracks().forEach((track) => track.stop());
234
+ throw new Error(`Initial zoom level ${zoomLevel} is not available. Valid range is ${minZoom} to ${maxZoom}`);
235
+ }
236
+ try {
237
+ await videoTrack.applyConstraints({
238
+ advanced: [{ zoom: zoomLevel }],
239
+ });
240
+ }
241
+ catch (error) {
242
+ console.warn(`Failed to set initial zoom level: ${error}`);
243
+ // Don't throw, just continue without zoom
244
+ }
245
+ }
246
+ }
247
+ }
121
248
  this.isStarted = true;
249
+ // Wait for video to be ready and get actual dimensions
250
+ await new Promise((resolve) => {
251
+ if (this.videoElement.readyState >= 2) {
252
+ resolve();
253
+ }
254
+ else {
255
+ this.videoElement.addEventListener("loadeddata", () => resolve(), {
256
+ once: true,
257
+ });
258
+ }
259
+ });
260
+ // Ensure centering is applied after DOM updates
261
+ await new Promise((resolve) => requestAnimationFrame(resolve));
262
+ console.log("About to re-center, flags:", { needsCenterX, needsCenterY });
263
+ // Re-apply centering with correct parent dimensions
264
+ if (needsCenterX) {
265
+ const parentWidth = container.offsetWidth;
266
+ const x = Math.round((parentWidth - this.videoElement.offsetWidth) / 2);
267
+ this.videoElement.style.left = `${x}px`;
268
+ console.log("Re-centering X:", {
269
+ parentWidth,
270
+ videoWidth: this.videoElement.offsetWidth,
271
+ x,
272
+ });
273
+ }
274
+ if (needsCenterY) {
275
+ const y = Math.round((window.innerHeight - this.videoElement.offsetHeight) / 2);
276
+ this.videoElement.style.setProperty("top", `${y}px`, "important");
277
+ console.log("Re-centering Y:", {
278
+ viewportHeight: window.innerHeight,
279
+ videoHeight: this.videoElement.offsetHeight,
280
+ y,
281
+ position: this.videoElement.style.position,
282
+ top: this.videoElement.style.top,
283
+ });
284
+ }
285
+ // Get the actual rendered dimensions after video is loaded
286
+ const rect = this.videoElement.getBoundingClientRect();
287
+ const computedStyle = window.getComputedStyle(this.videoElement);
288
+ console.log("Final video element state:", {
289
+ rect: { x: rect.x, y: rect.y, width: rect.width, height: rect.height },
290
+ style: {
291
+ position: computedStyle.position,
292
+ left: computedStyle.left,
293
+ top: computedStyle.top,
294
+ width: computedStyle.width,
295
+ height: computedStyle.height,
296
+ },
297
+ });
122
298
  return {
123
- width: this.videoElement.width,
124
- height: this.videoElement.height,
125
- x: this.videoElement.getBoundingClientRect().x,
126
- y: this.videoElement.getBoundingClientRect().y,
299
+ width: Math.round(rect.width),
300
+ height: Math.round(rect.height),
301
+ x: Math.round(rect.x),
302
+ y: Math.round(rect.y),
127
303
  };
128
304
  }
129
305
  stopStream(stream) {
@@ -157,14 +333,53 @@ class CameraPreviewWeb extends core.WebPlugin {
157
333
  if (video && video.videoWidth > 0 && video.videoHeight > 0) {
158
334
  const canvas = document.createElement("canvas");
159
335
  const context = canvas.getContext("2d");
160
- canvas.width = video.videoWidth;
161
- canvas.height = video.videoHeight;
336
+ // Calculate capture dimensions
337
+ let captureWidth = video.videoWidth;
338
+ let captureHeight = video.videoHeight;
339
+ let sourceX = 0;
340
+ let sourceY = 0;
341
+ // Check for conflicting parameters
342
+ if (options.aspectRatio && (options.width || options.height)) {
343
+ reject(new Error("Cannot set both aspectRatio and size (width/height). Use setPreviewSize after start."));
344
+ return;
345
+ }
346
+ // Handle aspect ratio if no width/height specified
347
+ if (!options.width && !options.height && options.aspectRatio) {
348
+ const [widthRatio, heightRatio] = options.aspectRatio.split(':').map(Number);
349
+ if (widthRatio && heightRatio) {
350
+ // For capture in portrait orientation, swap the aspect ratio (16:9 becomes 9:16)
351
+ const isPortrait = video.videoHeight > video.videoWidth;
352
+ const targetAspectRatio = isPortrait ? heightRatio / widthRatio : widthRatio / heightRatio;
353
+ const videoAspectRatio = video.videoWidth / video.videoHeight;
354
+ if (videoAspectRatio > targetAspectRatio) {
355
+ // Video is wider than target - crop sides
356
+ captureWidth = video.videoHeight * targetAspectRatio;
357
+ captureHeight = video.videoHeight;
358
+ sourceX = (video.videoWidth - captureWidth) / 2;
359
+ }
360
+ else {
361
+ // Video is taller than target - crop top/bottom
362
+ captureWidth = video.videoWidth;
363
+ captureHeight = video.videoWidth / targetAspectRatio;
364
+ sourceY = (video.videoHeight - captureHeight) / 2;
365
+ }
366
+ }
367
+ }
368
+ else if (options.width || options.height) {
369
+ // If width or height is specified, use them
370
+ if (options.width)
371
+ captureWidth = options.width;
372
+ if (options.height)
373
+ captureHeight = options.height;
374
+ }
375
+ canvas.width = captureWidth;
376
+ canvas.height = captureHeight;
162
377
  // flip horizontally back camera isn't used
163
378
  if (!this.isBackCamera) {
164
- context === null || context === void 0 ? void 0 : context.translate(video.videoWidth, 0);
379
+ context === null || context === void 0 ? void 0 : context.translate(captureWidth, 0);
165
380
  context === null || context === void 0 ? void 0 : context.scale(-1, 1);
166
381
  }
167
- context === null || context === void 0 ? void 0 : context.drawImage(video, 0, 0, video.videoWidth, video.videoHeight);
382
+ context === null || context === void 0 ? void 0 : context.drawImage(video, sourceX, sourceY, captureWidth, captureHeight, 0, 0, captureWidth, captureHeight);
168
383
  if (options.saveToGallery) ;
169
384
  if (options.withExifLocation) ;
170
385
  if ((options.format || "jpeg") === "jpeg") {
@@ -396,6 +611,7 @@ class CameraPreviewWeb extends core.WebPlugin {
396
611
  throw new Error("zoom not supported by this device");
397
612
  }
398
613
  const zoomLevel = Math.max(capabilities.zoom.min || 1, Math.min(capabilities.zoom.max || 1, options.level));
614
+ // Note: autoFocus is not supported on web platform
399
615
  try {
400
616
  await videoTrack.applyConstraints({
401
617
  advanced: [{ zoom: zoomLevel }],
@@ -518,21 +734,25 @@ class CameraPreviewWeb extends core.WebPlugin {
518
734
  video.style.left = `${x}px`;
519
735
  video.style.top = `${y}px`;
520
736
  video.style.position = "absolute";
737
+ const offsetX = newWidth / 8;
738
+ const offsetY = newHeight / 8;
521
739
  return {
522
740
  width: Math.round(newWidth),
523
741
  height: Math.round(newHeight),
524
- x: Math.round(x),
525
- y: Math.round(y),
742
+ x: Math.round(x + offsetX),
743
+ y: Math.round(y + offsetY),
526
744
  };
527
745
  }
528
746
  else {
529
747
  video.style.objectFit = "cover";
530
748
  const rect = video.getBoundingClientRect();
749
+ const offsetX = rect.width / 8;
750
+ const offsetY = rect.height / 8;
531
751
  return {
532
752
  width: Math.round(rect.width),
533
753
  height: Math.round(rect.height),
534
- x: Math.round(rect.left),
535
- y: Math.round(rect.top),
754
+ x: Math.round(rect.left + offsetX),
755
+ y: Math.round(rect.top + offsetY),
536
756
  };
537
757
  }
538
758
  }
@@ -591,9 +811,11 @@ class CameraPreviewWeb extends core.WebPlugin {
591
811
  if (!video) {
592
812
  throw new Error("camera is not running");
593
813
  }
814
+ const offsetX = video.width / 8;
815
+ const offsetY = video.height / 8;
594
816
  return {
595
- x: video.offsetLeft,
596
- y: video.offsetTop,
817
+ x: video.offsetLeft + offsetX,
818
+ y: video.offsetTop + offsetY,
597
819
  width: video.width,
598
820
  height: video.height,
599
821
  };
@@ -607,14 +829,20 @@ class CameraPreviewWeb extends core.WebPlugin {
607
829
  video.style.top = `${options.y}px`;
608
830
  video.width = options.width;
609
831
  video.height = options.height;
832
+ const offsetX = options.width / 8;
833
+ const offsetY = options.height / 8;
610
834
  return {
611
835
  width: options.width,
612
836
  height: options.height,
613
- x: options.x,
614
- y: options.y,
837
+ x: options.x + offsetX,
838
+ y: options.y + offsetY,
615
839
  };
616
840
  }
617
841
  async setFocus(options) {
842
+ // Reject if values are outside 0-1 range
843
+ if (options.x < 0 || options.x > 1 || options.y < 0 || options.y > 1) {
844
+ throw new Error("Focus coordinates must be between 0 and 1");
845
+ }
618
846
  const video = document.getElementById(DEFAULT_VIDEO_ID);
619
847
  if (!(video === null || video === void 0 ? void 0 : video.srcObject)) {
620
848
  throw new Error("camera is not running");