@capgo/camera-preview 7.4.0-alpha.23 → 7.4.0-alpha.25

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.
@@ -6,6 +6,7 @@ import static android.Manifest.permission.RECORD_AUDIO;
6
6
  import android.Manifest;
7
7
  import android.content.pm.ActivityInfo;
8
8
  import android.content.res.Configuration;
9
+ import android.graphics.Color;
9
10
  import android.location.Location;
10
11
  import android.util.DisplayMetrics;
11
12
  import android.util.Log;
@@ -74,6 +75,7 @@ public class CameraPreview
74
75
  private int previousOrientationRequest =
75
76
  ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED;
76
77
  private CameraXView cameraXView;
78
+ private View rotationOverlay;
77
79
  private FusedLocationProviderClient fusedLocationClient;
78
80
  private Location lastLocation;
79
81
  private OrientationEventListener orientationListener;
@@ -241,6 +243,12 @@ public class CameraPreview
241
243
  lastOrientation = Configuration.ORIENTATION_UNDEFINED;
242
244
  }
243
245
 
246
+ // Remove any rotation overlay if present
247
+ if (rotationOverlay != null && rotationOverlay.getParent() != null) {
248
+ ((ViewGroup) rotationOverlay.getParent()).removeView(rotationOverlay);
249
+ rotationOverlay = null;
250
+ }
251
+
244
252
  if (cameraXView != null && cameraXView.isRunning()) {
245
253
  cameraXView.stopSession();
246
254
  cameraXView = null;
@@ -1106,6 +1114,28 @@ public class CameraPreview
1106
1114
  getBridge()
1107
1115
  .getActivity()
1108
1116
  .runOnUiThread(() -> {
1117
+ // Create and show a black full-screen overlay during rotation
1118
+ ViewGroup rootView = (ViewGroup) getBridge()
1119
+ .getActivity()
1120
+ .getWindow()
1121
+ .getDecorView()
1122
+ .getRootView();
1123
+
1124
+ // Remove any existing overlay
1125
+ if (rotationOverlay != null && rotationOverlay.getParent() != null) {
1126
+ ((ViewGroup) rotationOverlay.getParent()).removeView(rotationOverlay);
1127
+ }
1128
+
1129
+ // Create new black overlay
1130
+ rotationOverlay = new View(getContext());
1131
+ rotationOverlay.setBackgroundColor(Color.BLACK);
1132
+ ViewGroup.LayoutParams overlayParams = new ViewGroup.LayoutParams(
1133
+ ViewGroup.LayoutParams.MATCH_PARENT,
1134
+ ViewGroup.LayoutParams.MATCH_PARENT
1135
+ );
1136
+ rotationOverlay.setLayoutParams(overlayParams);
1137
+ rootView.addView(rotationOverlay);
1138
+
1109
1139
  // Reapply current aspect ratio to recompute layout, then emit screenResize
1110
1140
  String ar = cameraXView.getAspectRatio();
1111
1141
  Log.d(TAG, "Reapplying aspect ratio: " + ar);
@@ -1180,6 +1210,38 @@ public class CameraPreview
1180
1210
  oData.put("orientation", o);
1181
1211
  notifyListeners("orientationChange", oData);
1182
1212
 
1213
+ // Don't remove the overlay here - wait for camera to fully start
1214
+ // The overlay will be removed after a delay to ensure camera is stable
1215
+ if (rotationOverlay != null && rotationOverlay.getParent() != null) {
1216
+ // Shorter delay for faster transition
1217
+ int delay = "4:3".equals(ar) ? 200 : 150;
1218
+ rotationOverlay.postDelayed(
1219
+ () -> {
1220
+ if (
1221
+ rotationOverlay != null && rotationOverlay.getParent() != null
1222
+ ) {
1223
+ rotationOverlay
1224
+ .animate()
1225
+ .alpha(0f)
1226
+ .setDuration(100) // Faster fade out
1227
+ .withEndAction(() -> {
1228
+ if (
1229
+ rotationOverlay != null &&
1230
+ rotationOverlay.getParent() != null
1231
+ ) {
1232
+ ((ViewGroup) rotationOverlay.getParent()).removeView(
1233
+ rotationOverlay
1234
+ );
1235
+ rotationOverlay = null;
1236
+ }
1237
+ })
1238
+ .start();
1239
+ }
1240
+ },
1241
+ delay
1242
+ );
1243
+ }
1244
+
1183
1245
  Log.d(
1184
1246
  TAG,
1185
1247
  "================================================================================"
@@ -1213,6 +1275,23 @@ public class CameraPreview
1213
1275
  bridge.releaseCall(pluginCall);
1214
1276
  }
1215
1277
 
1278
+ private JSObject getViewSize(
1279
+ double x,
1280
+ double y,
1281
+ double width,
1282
+ double height
1283
+ ) {
1284
+ JSObject ret = new JSObject();
1285
+ // Return values with proper rounding to avoid gaps
1286
+ // For positions (x, y): ceil to avoid gaps at top/left
1287
+ // For dimensions (width, height): floor to avoid gaps at bottom/right
1288
+ ret.put("x", Math.ceil(x));
1289
+ ret.put("y", Math.ceil(y));
1290
+ ret.put("width", Math.floor(width));
1291
+ ret.put("height", Math.floor(height));
1292
+ return ret;
1293
+ }
1294
+
1216
1295
  @Override
1217
1296
  public void onCameraStarted(int width, int height, int x, int y) {
1218
1297
  PluginCall call = bridge.getSavedCall(cameraStartCallbackId);
@@ -1287,6 +1366,13 @@ public class CameraPreview
1287
1366
  double logicalX = x / pixelRatio;
1288
1367
  double logicalY = relativeY / pixelRatio;
1289
1368
 
1369
+ JSObject result = getViewSize(
1370
+ logicalX,
1371
+ logicalY,
1372
+ logicalWidth,
1373
+ logicalHeight
1374
+ );
1375
+
1290
1376
  // Log exact calculations to debug one-pixel difference
1291
1377
  Log.d("CameraPreview", "========================");
1292
1378
  Log.d("CameraPreview", "FINAL POSITION CALCULATIONS:");
@@ -1360,32 +1446,23 @@ public class CameraPreview
1360
1446
  }
1361
1447
  Log.d("CameraPreview", "========================");
1362
1448
 
1363
- JSObject result = new JSObject();
1364
- // Return values with proper rounding to avoid gaps
1365
- // For positions (x, y): floor to avoid gaps at top/left
1366
- // For dimensions (width, height): ceil to avoid gaps at bottom/right
1367
- result.put("width", Math.floor(logicalWidth));
1368
- result.put("height", Math.floor(logicalHeight));
1369
- result.put("x", Math.ceil(logicalX));
1370
- result.put("y", Math.ceil(logicalY));
1371
-
1372
1449
  // Log what we're returning
1373
1450
  Log.d(
1374
1451
  "CameraPreview",
1375
1452
  "Returning to JS - x: " +
1376
- Math.ceil(logicalX) +
1453
+ logicalX +
1377
1454
  " (from " +
1378
1455
  logicalX +
1379
1456
  "), y: " +
1380
- Math.ceil(logicalY) +
1457
+ logicalY +
1381
1458
  " (from " +
1382
1459
  logicalY +
1383
1460
  "), width: " +
1384
- Math.floor(logicalWidth) +
1461
+ logicalWidth +
1385
1462
  " (from " +
1386
1463
  logicalWidth +
1387
1464
  "), height: " +
1388
- Math.floor(logicalHeight) +
1465
+ logicalHeight +
1389
1466
  " (from " +
1390
1467
  logicalHeight +
1391
1468
  ")"
@@ -1497,15 +1574,26 @@ public class CameraPreview
1497
1574
 
1498
1575
  JSObject ret = new JSObject();
1499
1576
  // Use same rounding strategy as start method
1500
- double x = cameraXView.getPreviewX() / pixelRatio;
1501
- double y = cameraXView.getPreviewY() / pixelRatio;
1502
- double width = cameraXView.getPreviewWidth() / pixelRatio;
1503
- double height = cameraXView.getPreviewHeight() / pixelRatio;
1577
+ double x = Math.ceil(cameraXView.getPreviewX() / pixelRatio);
1578
+ double y = Math.ceil(cameraXView.getPreviewY() / pixelRatio);
1579
+ double width = Math.floor(cameraXView.getPreviewWidth() / pixelRatio);
1580
+ double height = Math.floor(cameraXView.getPreviewHeight() / pixelRatio);
1504
1581
 
1505
- ret.put("x", Math.ceil(x));
1506
- ret.put("y", Math.ceil(y));
1507
- ret.put("width", Math.floor(width));
1508
- ret.put("height", Math.floor(height));
1582
+ Log.d(
1583
+ "CameraPreview",
1584
+ "getPreviewSize: x=" +
1585
+ x +
1586
+ ", y=" +
1587
+ y +
1588
+ ", width=" +
1589
+ width +
1590
+ ", height=" +
1591
+ height
1592
+ );
1593
+ ret.put("x", x);
1594
+ ret.put("y", y);
1595
+ ret.put("width", width);
1596
+ ret.put("height", height);
1509
1597
  call.resolve(ret);
1510
1598
  }
1511
1599
 
@@ -2728,18 +2728,83 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
2728
2728
  boolean usesFillCenter =
2729
2729
  sessionConfig != null && sessionConfig.getAspectRatio() != null;
2730
2730
 
2731
- float widthScale = (float) containerWidth / cameraWidth;
2732
- float heightScale = (float) containerHeight / cameraHeight;
2733
- float scale;
2734
-
2731
+ // For FILL_CENTER with aspect ratio, we need to calculate the actual visible bounds
2732
+ // The preview might extend beyond the container bounds and get clipped
2735
2733
  if (usesFillCenter) {
2736
- // FILL_CENTER uses max scale to fill the container
2737
- scale = Math.max(widthScale, heightScale);
2738
- } else {
2739
- // FIT_CENTER uses min scale to fit within the container
2740
- scale = Math.min(widthScale, heightScale);
2734
+ // Calculate how the camera preview is scaled to fill the container
2735
+ float widthScale = (float) containerWidth / cameraWidth;
2736
+ float heightScale = (float) containerHeight / cameraHeight;
2737
+ float scale = Math.max(widthScale, heightScale); // max for FILL_CENTER
2738
+
2739
+ // Calculate the scaled dimensions
2740
+ int scaledWidth = Math.round(cameraWidth * scale);
2741
+ int scaledHeight = Math.round(cameraHeight * scale);
2742
+
2743
+ // Calculate how much is clipped on each side
2744
+ int excessWidth = Math.max(0, scaledWidth - containerWidth);
2745
+ int excessHeight = Math.max(0, scaledHeight - containerHeight);
2746
+
2747
+ // For the actual visible bounds, we need to account for potential
2748
+ // internal misalignment of PreviewView's SurfaceView
2749
+ int adjustedWidth = containerWidth;
2750
+ int adjustedHeight = containerHeight;
2751
+
2752
+ // Apply small adjustments for 4:3 ratio to prevent blue line
2753
+ // This compensates for PreviewView's internal SurfaceView misalignment
2754
+ String aspectRatio = sessionConfig != null
2755
+ ? sessionConfig.getAspectRatio()
2756
+ : null;
2757
+ if ("4:3".equals(aspectRatio)) {
2758
+ // For 4:3, reduce the reported width slightly to account for
2759
+ // the SurfaceView drawing outside its bounds
2760
+ adjustedWidth = containerWidth - 2;
2761
+ adjustedHeight = containerHeight - 2;
2762
+ }
2763
+
2764
+ Log.d(
2765
+ TAG,
2766
+ "getActualCameraBounds FILL_CENTER: container=" +
2767
+ containerWidth +
2768
+ "x" +
2769
+ containerHeight +
2770
+ ", camera=" +
2771
+ cameraWidth +
2772
+ "x" +
2773
+ cameraHeight +
2774
+ " (portrait=" +
2775
+ isPortrait +
2776
+ ")" +
2777
+ ", scale=" +
2778
+ scale +
2779
+ ", scaled=" +
2780
+ scaledWidth +
2781
+ "x" +
2782
+ scaledHeight +
2783
+ ", excess=" +
2784
+ excessWidth +
2785
+ "x" +
2786
+ excessHeight +
2787
+ ", adjusted=" +
2788
+ adjustedWidth +
2789
+ "x" +
2790
+ adjustedHeight +
2791
+ ", ratio=" +
2792
+ aspectRatio
2793
+ );
2794
+
2795
+ // Return slightly inset bounds for 4:3 to avoid blue line
2796
+ if ("4:3".equals(aspectRatio)) {
2797
+ return new Rect(1, 1, adjustedWidth + 1, adjustedHeight + 1);
2798
+ } else {
2799
+ return new Rect(0, 0, containerWidth, containerHeight);
2800
+ }
2741
2801
  }
2742
2802
 
2803
+ // For FIT_CENTER (no aspect ratio), calculate letterboxing
2804
+ float widthScale = (float) containerWidth / cameraWidth;
2805
+ float heightScale = (float) containerHeight / cameraHeight;
2806
+ float scale = Math.min(widthScale, heightScale);
2807
+
2743
2808
  // Calculate the actual size of the camera content after scaling
2744
2809
  int scaledWidth = Math.round(cameraWidth * scale);
2745
2810
  int scaledHeight = Math.round(cameraHeight * scale);
@@ -2750,7 +2815,7 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
2750
2815
 
2751
2816
  Log.d(
2752
2817
  TAG,
2753
- "getActualCameraBounds: container=" +
2818
+ "getActualCameraBounds FIT_CENTER: container=" +
2754
2819
  containerWidth +
2755
2820
  "x" +
2756
2821
  containerHeight +
@@ -2763,9 +2828,6 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
2763
2828
  ")" +
2764
2829
  ", scale=" +
2765
2830
  scale +
2766
- " (fillCenter=" +
2767
- usesFillCenter +
2768
- ")" +
2769
2831
  ", scaled=" +
2770
2832
  scaledWidth +
2771
2833
  "x" +
@@ -3378,6 +3440,38 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
3378
3440
  int width = (int) Math.floor(actualWidth / pixelRatio);
3379
3441
  int height = (int) Math.floor(actualHeight / pixelRatio);
3380
3442
 
3443
+ // Debug logging to understand the blue line issue
3444
+ Log.d(
3445
+ TAG,
3446
+ "getCurrentPreviewBounds DEBUG: " +
3447
+ "actualBounds=(" +
3448
+ actualX +
3449
+ "," +
3450
+ actualY +
3451
+ "," +
3452
+ actualWidth +
3453
+ "x" +
3454
+ actualHeight +
3455
+ "), " +
3456
+ "logicalBounds=(" +
3457
+ x +
3458
+ "," +
3459
+ y +
3460
+ "," +
3461
+ width +
3462
+ "x" +
3463
+ height +
3464
+ "), " +
3465
+ "pixelRatio=" +
3466
+ pixelRatio +
3467
+ ", " +
3468
+ "insets=(" +
3469
+ webViewLeftInset +
3470
+ "," +
3471
+ webViewTopInset +
3472
+ ")"
3473
+ );
3474
+
3381
3475
  return new int[] { x, y, width, height };
3382
3476
  }
3383
3477
 
@@ -504,23 +504,23 @@ extension CameraController {
504
504
  dataOutput?.connections.forEach { $0.videoOrientation = videoOrientation }
505
505
  photoOutput?.connections.forEach { $0.videoOrientation = videoOrientation }
506
506
  }
507
-
507
+
508
508
  private func setDefaultZoomAfterFlip() {
509
509
  let device = (currentCameraPosition == .rear) ? rearCamera : frontCamera
510
510
  guard let device = device else {
511
511
  print("[CameraPreview] No device available for default zoom after flip")
512
512
  return
513
513
  }
514
-
514
+
515
515
  // Set zoom to 1.0x in UI terms, accounting for display multiplier
516
516
  let multiplier = self.getDisplayZoomMultiplier()
517
517
  let targetUIZoom: Float = 1.0 // We want 1.0x in the UI
518
518
  let nativeZoom = multiplier != 1.0 ? (targetUIZoom / multiplier) : targetUIZoom
519
-
519
+
520
520
  let minZoom = device.minAvailableVideoZoomFactor
521
521
  let maxZoom = min(device.maxAvailableVideoZoomFactor, saneMaxZoomFactor)
522
522
  let clampedZoom = max(minZoom, min(CGFloat(nativeZoom), maxZoom))
523
-
523
+
524
524
  do {
525
525
  try device.lockForConfiguration()
526
526
  device.videoZoomFactor = clampedZoom
@@ -621,7 +621,7 @@ extension CameraController {
621
621
 
622
622
  // Update video orientation
623
623
  self.updateVideoOrientation()
624
-
624
+
625
625
  // Set default 1.0 zoom level after camera switch to prevent iOS 18+ zoom jumps
626
626
  DispatchQueue.main.async { [weak self] in
627
627
  self?.setDefaultZoomAfterFlip()
@@ -170,18 +170,62 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin, CLLocationManagerDelega
170
170
  }
171
171
 
172
172
  var values: [Float] = []
173
- if hasUltraWide { values.append(0.5) }
173
+ if hasUltraWide {
174
+ values.append(0.5)
175
+ }
174
176
  if hasWide {
175
177
  values.append(1.0)
176
- values.append(2.0)
178
+ if self.isProModelSupportingOptical2x() {
179
+ values.append(2.0)
180
+ }
181
+ }
182
+ if hasTele {
183
+ // Use the virtual device's switch-over zoom factors when available
184
+ let displayMultiplier = self.cameraController.getDisplayZoomMultiplier()
185
+ var teleStep: Float
186
+
187
+ if #available(iOS 13.0, *) {
188
+ let switchFactors = device.virtualDeviceSwitchOverVideoZoomFactors
189
+ if !switchFactors.isEmpty {
190
+ // Choose the highest switch-over factor (typically the wide->tele threshold)
191
+ let maxSwitch = switchFactors.map { $0.floatValue }.max() ?? Float(device.maxAvailableVideoZoomFactor)
192
+ teleStep = maxSwitch * displayMultiplier
193
+ } else {
194
+ teleStep = Float(device.maxAvailableVideoZoomFactor) * displayMultiplier
195
+ }
196
+ } else {
197
+ teleStep = Float(device.maxAvailableVideoZoomFactor) * displayMultiplier
198
+ }
199
+ values.append(teleStep)
177
200
  }
178
- if hasTele { values.append(3.0) }
179
201
 
180
202
  // Deduplicate and sort
181
203
  let uniqueSorted = Array(Set(values)).sorted()
182
204
  call.resolve(["values": uniqueSorted])
183
205
  }
184
206
 
207
+ private func isProModelSupportingOptical2x() -> Bool {
208
+ // Detects iPhone 14 Pro/Pro Max, 15 Pro/Pro Max, and 16 Pro/Pro Max
209
+ var systemInfo = utsname()
210
+ uname(&systemInfo)
211
+ let mirror = Mirror(reflecting: systemInfo.machine)
212
+ let identifier = mirror.children.reduce("") { partialResult, element in
213
+ guard let value = element.value as? Int8, value != 0 else { return partialResult }
214
+ return partialResult + String(UnicodeScalar(UInt8(value)))
215
+ }
216
+
217
+ // Known identifiers: 14 Pro (iPhone15,2), 14 Pro Max (iPhone15,3),
218
+ // 15 Pro (iPhone16,1), 15 Pro Max (iPhone16,2),
219
+ // 16 Pro (iPhone17,1), 16 Pro Max (iPhone17,2),
220
+ // 17 Pro (iPhone18,1), 17 Pro Max (iPhone18,2)
221
+ let supportedIdentifiers: Set<String> = [
222
+ "iPhone15,2", "iPhone15,3", // 14 Pro / 14 Pro Max
223
+ "iPhone16,1", "iPhone16,2", // 15 Pro / 15 Pro Max
224
+ "iPhone17,1", "iPhone17,2" // 16 Pro / 16 Pro Max
225
+ ]
226
+ return supportedIdentifiers.contains(identifier)
227
+ }
228
+
185
229
  @objc func rotated() {
186
230
  guard let previewView = self.previewView,
187
231
  let posX = self.posX,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@capgo/camera-preview",
3
- "version": "7.4.0-alpha.23",
3
+ "version": "7.4.0-alpha.25",
4
4
  "description": "Camera preview",
5
5
  "license": "MIT",
6
6
  "repository": {