@capgo/camera-preview 8.2.2 → 8.3.1

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.
Files changed (33) hide show
  1. package/README.md +31 -29
  2. package/android/build.gradle +4 -0
  3. package/android/src/main/java/app/capgo/capacitor/camera/preview/CameraPreview.java +87 -20
  4. package/android/src/main/java/app/capgo/capacitor/camera/preview/CameraXView.java +551 -157
  5. package/android/src/main/java/app/capgo/capacitor/camera/preview/model/CameraSessionConfiguration.java +13 -0
  6. package/dist/docs.json +17 -1
  7. package/dist/esm/definitions.d.ts +9 -1
  8. package/dist/esm/definitions.js.map +1 -1
  9. package/dist/esm/web.d.ts +1 -1
  10. package/dist/esm/web.js +120 -92
  11. package/dist/esm/web.js.map +1 -1
  12. package/dist/plugin.cjs.js +119 -92
  13. package/dist/plugin.cjs.js.map +1 -1
  14. package/dist/plugin.js +119 -92
  15. package/dist/plugin.js.map +1 -1
  16. package/ios/Sources/CapgoCameraPreviewPlugin/CameraController.swift +14 -12
  17. package/ios/Sources/CapgoCameraPreviewPlugin/Plugin.swift +16 -14
  18. package/package.json +8 -7
  19. package/android/.gradle/8.14.4/checksums/checksums.lock +0 -0
  20. package/android/.gradle/8.14.4/checksums/md5-checksums.bin +0 -0
  21. package/android/.gradle/8.14.4/checksums/sha1-checksums.bin +0 -0
  22. package/android/.gradle/8.14.4/executionHistory/executionHistory.bin +0 -0
  23. package/android/.gradle/8.14.4/executionHistory/executionHistory.lock +0 -0
  24. package/android/.gradle/8.14.4/fileChanges/last-build.bin +0 -0
  25. package/android/.gradle/8.14.4/fileHashes/fileHashes.bin +0 -0
  26. package/android/.gradle/8.14.4/fileHashes/fileHashes.lock +0 -0
  27. package/android/.gradle/8.14.4/fileHashes/resourceHashesCache.bin +0 -0
  28. package/android/.gradle/8.14.4/gc.properties +0 -0
  29. package/android/.gradle/buildOutputCleanup/buildOutputCleanup.lock +0 -0
  30. package/android/.gradle/buildOutputCleanup/cache.properties +0 -2
  31. package/android/.gradle/buildOutputCleanup/outputFiles.bin +0 -0
  32. package/android/.gradle/file-system.probe +0 -0
  33. package/android/.gradle/vcs-1/gc.properties +0 -0
@@ -96,11 +96,14 @@ import java.util.Collections;
96
96
  import java.util.Date;
97
97
  import java.util.List;
98
98
  import java.util.Locale;
99
+ import java.util.Map;
99
100
  import java.util.Objects;
100
101
  import java.util.Set;
102
+ import java.util.concurrent.ConcurrentHashMap;
101
103
  import java.util.concurrent.Executor;
102
104
  import java.util.concurrent.ExecutorService;
103
105
  import java.util.concurrent.Executors;
106
+ import java.util.concurrent.RejectedExecutionException;
104
107
  import org.json.JSONObject;
105
108
 
106
109
  public class CameraXView implements LifecycleOwner, LifecycleObserver {
@@ -138,6 +141,8 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
138
141
  private long focusIndicatorAnimationId = 0; // Incrementing token to invalidate previous animations
139
142
  private CameraSelector currentCameraSelector;
140
143
  private String currentDeviceId;
144
+ private String currentPhysicalDeviceId;
145
+ private String currentLogicalDeviceId;
141
146
  private int currentFlashMode = ImageCapture.FLASH_MODE_OFF;
142
147
  private CameraSessionConfiguration sessionConfig;
143
148
  private CameraXViewListener listener;
@@ -150,6 +155,10 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
150
155
  private final LifecycleRegistry lifecycleRegistry;
151
156
  private final Executor mainExecutor;
152
157
  private ExecutorService cameraExecutor;
158
+ private static volatile Map<String, app.capgo.capacitor.camera.preview.model.CameraDevice> enumeratedDeviceCache =
159
+ new ConcurrentHashMap<>();
160
+ private static final Object enumeratedDeviceCacheLock = new Object();
161
+ private static volatile boolean enumeratedDeviceCacheRefreshInProgress = false;
153
162
  private boolean isRunning = false;
154
163
  private Size currentPreviewResolution = null;
155
164
  private ListenableFuture<FocusMeteringResult> currentFocusFuture = null; // Track current focus operation
@@ -213,6 +222,56 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
213
222
  }
214
223
  };
215
224
 
225
+ private static final class PhysicalCameraBindingTarget {
226
+
227
+ private final CameraInfo logicalCameraInfo;
228
+ private final String logicalCameraId;
229
+ private final int requiredFacing;
230
+
231
+ private PhysicalCameraBindingTarget(CameraInfo logicalCameraInfo, String logicalCameraId, int requiredFacing) {
232
+ this.logicalCameraInfo = logicalCameraInfo;
233
+ this.logicalCameraId = logicalCameraId;
234
+ this.requiredFacing = requiredFacing;
235
+ }
236
+ }
237
+
238
+ private static final class PhysicalDeviceMetadata {
239
+
240
+ private final String position;
241
+ private final float fallbackZoom;
242
+
243
+ private PhysicalDeviceMetadata(String position, float fallbackZoom) {
244
+ this.position = position;
245
+ this.fallbackZoom = fallbackZoom;
246
+ }
247
+ }
248
+
249
+ private static final class CameraBindingPlan {
250
+
251
+ private final CameraSelector selector;
252
+ private final String reportedDeviceId;
253
+ private final String logicalCameraId;
254
+ private final String physicalCameraId;
255
+ private final float fallbackZoom;
256
+ private final boolean usesPhysicalSelection;
257
+
258
+ private CameraBindingPlan(
259
+ CameraSelector selector,
260
+ String reportedDeviceId,
261
+ String logicalCameraId,
262
+ String physicalCameraId,
263
+ float fallbackZoom,
264
+ boolean usesPhysicalSelection
265
+ ) {
266
+ this.selector = selector;
267
+ this.reportedDeviceId = reportedDeviceId;
268
+ this.logicalCameraId = logicalCameraId;
269
+ this.physicalCameraId = physicalCameraId;
270
+ this.fallbackZoom = fallbackZoom;
271
+ this.usesPhysicalSelection = usesPhysicalSelection;
272
+ }
273
+ }
274
+
216
275
  private boolean IsOperationRunning(String name) {
217
276
  synchronized (operationLock) {
218
277
  if (stopPending) {
@@ -453,6 +512,7 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
453
512
 
454
513
  this.sessionConfig = config;
455
514
  cameraExecutor = Executors.newSingleThreadExecutor();
515
+ requestEnumeratedDeviceCacheRefresh();
456
516
 
457
517
  // Reset cached orientation so we don't reuse stale values across sessions
458
518
  synchronized (accelerometerLock) {
@@ -523,6 +583,9 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
523
583
 
524
584
  private void performImmediateStop() {
525
585
  isRunning = false;
586
+ currentDeviceId = null;
587
+ currentPhysicalDeviceId = null;
588
+ currentLogicalDeviceId = null;
526
589
  // Stop accelerometer and rotation vector sensor
527
590
  if (sensorManager != null && accelerometer != null) {
528
591
  sensorManager.unregisterListener(accelerometerListener);
@@ -928,7 +991,8 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
928
991
  " and position: " +
929
992
  sessionConfig.getPosition()
930
993
  );
931
- currentCameraSelector = buildCameraSelector();
994
+ CameraBindingPlan bindingPlan = buildCameraBindingPlan(sessionConfig);
995
+ currentCameraSelector = bindingPlan.selector;
932
996
 
933
997
  ResolutionSelector.Builder resolutionSelectorBuilder = new ResolutionSelector.Builder().setResolutionStrategy(
934
998
  ResolutionStrategy.HIGHEST_AVAILABLE_STRATEGY
@@ -1006,17 +1070,24 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
1006
1070
  // is connected and video frames are captured correctly
1007
1071
  preview.setSurfaceProvider(previewView.getSurfaceProvider());
1008
1072
 
1009
- // Bind with or without video capture based on enableVideoMode
1010
- if (sessionConfig.isVideoModeEnabled() && videoCapture != null) {
1011
- camera = cameraProvider.bindToLifecycle(this, currentCameraSelector, preview, imageCapture, videoCapture);
1012
- CameraInfo cameraInfo = camera.getCameraInfo();
1013
- currentDeviceId = Camera2CameraInfo.from(cameraInfo).getCameraId();
1014
- Log.d(TAG, "bindCameraUseCases: Camera successfully bound to device ID: " + currentDeviceId);
1015
- } else {
1016
- camera = cameraProvider.bindToLifecycle(this, currentCameraSelector, preview, imageCapture);
1017
- CameraInfo cameraInfo = camera.getCameraInfo();
1018
- currentDeviceId = Camera2CameraInfo.from(cameraInfo).getCameraId();
1019
- Log.d(TAG, "bindCameraUseCases: Camera successfully bound to device ID: " + currentDeviceId);
1073
+ try {
1074
+ bindConfiguredUseCases(bindingPlan, preview);
1075
+ } catch (Exception initialBindError) {
1076
+ if (!bindingPlan.usesPhysicalSelection) {
1077
+ throw initialBindError;
1078
+ }
1079
+
1080
+ Log.w(
1081
+ TAG,
1082
+ "bindCameraUseCases: Physical camera binding failed for " +
1083
+ bindingPlan.physicalCameraId +
1084
+ ", falling back to logical camera behavior",
1085
+ initialBindError
1086
+ );
1087
+
1088
+ bindingPlan = buildLogicalFallbackPlan(sessionConfig, bindingPlan);
1089
+ currentCameraSelector = bindingPlan.selector;
1090
+ bindConfiguredUseCases(bindingPlan, preview);
1020
1091
  }
1021
1092
 
1022
1093
  resetExposureCompensationToDefault();
@@ -1024,7 +1095,10 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
1024
1095
  // Log details about the active camera
1025
1096
  Log.d(TAG, "Use cases bound. Inspecting active camera and use cases.");
1026
1097
  CameraInfo cameraInfo = camera.getCameraInfo();
1027
- Log.d(TAG, "Bound Camera ID: " + Camera2CameraInfo.from(cameraInfo).getCameraId());
1098
+ Log.d(TAG, "Bound Camera ID: " + currentLogicalDeviceId);
1099
+ if (currentPhysicalDeviceId != null) {
1100
+ Log.d(TAG, "Bound Physical Camera ID: " + currentPhysicalDeviceId);
1101
+ }
1028
1102
 
1029
1103
  // Log zoom state
1030
1104
  ZoomState zoomState = cameraInfo.getZoomState().getValue();
@@ -1100,7 +1174,11 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
1100
1174
  previewView.setScaleType("cover".equals(aspectMode) ? PreviewView.ScaleType.FILL_CENTER : PreviewView.ScaleType.FIT_CENTER);
1101
1175
 
1102
1176
  // Set initial zoom if specified, prioritizing targetZoom over default zoomFactor
1103
- float initialZoom = sessionConfig.getTargetZoom() != 1.0f ? sessionConfig.getTargetZoom() : sessionConfig.getZoomFactor();
1177
+ float initialZoom = !bindingPlan.usesPhysicalSelection &&
1178
+ bindingPlan.fallbackZoom != 1.0f &&
1179
+ sessionConfig.getTargetZoom() == 1.0f
1180
+ ? bindingPlan.fallbackZoom
1181
+ : (sessionConfig.getTargetZoom() != 1.0f ? sessionConfig.getTargetZoom() : sessionConfig.getZoomFactor());
1104
1182
  if (initialZoom != 1.0f) {
1105
1183
  Log.d(TAG, "Applying initial zoom of " + initialZoom);
1106
1184
 
@@ -1170,24 +1248,320 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
1170
1248
 
1171
1249
  @OptIn(markerClass = ExperimentalCamera2Interop.class)
1172
1250
  private CameraSelector buildCameraSelector() {
1173
- CameraSelector.Builder builder = new CameraSelector.Builder();
1174
- final String deviceId = sessionConfig.getDeviceId();
1251
+ return buildCameraBindingPlan(sessionConfig).selector;
1252
+ }
1253
+
1254
+ @OptIn(markerClass = ExperimentalCamera2Interop.class)
1255
+ private CameraBindingPlan buildCameraBindingPlan(CameraSessionConfiguration config) {
1256
+ final String deviceId = config.getDeviceId();
1257
+ final String position = config.getPosition();
1175
1258
 
1176
1259
  if (deviceId != null && !deviceId.isEmpty()) {
1177
- builder.addCameraFilter((cameraInfos) -> {
1178
- for (CameraInfo cameraInfo : cameraInfos) {
1179
- if (deviceId.equals(Camera2CameraInfo.from(cameraInfo).getCameraId())) {
1180
- return Collections.singletonList(cameraInfo);
1260
+ CameraInfo directCameraInfo = findAvailableCameraInfoById(deviceId);
1261
+ if (directCameraInfo != null) {
1262
+ CameraSelector.Builder directBuilder = new CameraSelector.Builder();
1263
+ directBuilder.addCameraFilter((cameraInfos) -> {
1264
+ for (CameraInfo cameraInfo : cameraInfos) {
1265
+ if (deviceId.equals(Camera2CameraInfo.from(cameraInfo).getCameraId())) {
1266
+ return Collections.singletonList(cameraInfo);
1267
+ }
1181
1268
  }
1269
+ return Collections.emptyList();
1270
+ });
1271
+
1272
+ return new CameraBindingPlan(directBuilder.build(), deviceId, deviceId, null, 1.0f, false);
1273
+ }
1274
+
1275
+ CameraBindingPlan logicalFallbackPlan = buildLogicalFallbackPlanForDeviceId(deviceId);
1276
+ if (config.isPhysicalDeviceSelectionEnabled()) {
1277
+ PhysicalCameraBindingTarget physicalTarget = findPhysicalCameraBindingTarget(deviceId);
1278
+ if (physicalTarget != null) {
1279
+ CameraSelector.Builder physicalBuilder = new CameraSelector.Builder()
1280
+ .requireLensFacing(physicalTarget.requiredFacing)
1281
+ .setPhysicalCameraId(deviceId)
1282
+ .addCameraFilter((cameraInfos) -> {
1283
+ for (CameraInfo cameraInfo : cameraInfos) {
1284
+ if (physicalTarget.logicalCameraId.equals(Camera2CameraInfo.from(cameraInfo).getCameraId())) {
1285
+ return Collections.singletonList(cameraInfo);
1286
+ }
1287
+ }
1288
+ return Collections.emptyList();
1289
+ });
1290
+
1291
+ return new CameraBindingPlan(
1292
+ physicalBuilder.build(),
1293
+ deviceId,
1294
+ physicalTarget.logicalCameraId,
1295
+ deviceId,
1296
+ logicalFallbackPlan != null ? logicalFallbackPlan.fallbackZoom : getFallbackZoomForDeviceId(deviceId),
1297
+ true
1298
+ );
1182
1299
  }
1183
- return Collections.emptyList();
1184
- });
1300
+ }
1301
+
1302
+ if (logicalFallbackPlan != null) {
1303
+ return logicalFallbackPlan;
1304
+ }
1305
+
1306
+ throw invalidDeviceId(deviceId);
1307
+ }
1308
+
1309
+ return buildPositionPlan(position);
1310
+ }
1311
+
1312
+ private CameraBindingPlan buildLogicalFallbackPlan(CameraSessionConfiguration config, CameraBindingPlan failedPhysicalPlan) {
1313
+ String fallbackPosition = config.getPosition();
1314
+
1315
+ if (failedPhysicalPlan.logicalCameraId != null) {
1316
+ CameraInfo logicalCameraInfo = findAvailableCameraInfoById(failedPhysicalPlan.logicalCameraId);
1317
+ if (logicalCameraInfo != null) {
1318
+ fallbackPosition = isBackCamera(logicalCameraInfo) ? "rear" : "front";
1319
+ }
1320
+ }
1321
+
1322
+ CameraBindingPlan positionPlan = buildPositionPlan(fallbackPosition);
1323
+ return new CameraBindingPlan(
1324
+ positionPlan.selector,
1325
+ failedPhysicalPlan.reportedDeviceId,
1326
+ positionPlan.logicalCameraId,
1327
+ null,
1328
+ failedPhysicalPlan.fallbackZoom,
1329
+ false
1330
+ );
1331
+ }
1332
+
1333
+ private CameraBindingPlan buildLogicalFallbackPlanForDeviceId(String deviceId) {
1334
+ String fallbackPosition = resolveFallbackPositionForDeviceId(deviceId);
1335
+ if (fallbackPosition == null) {
1336
+ return null;
1337
+ }
1338
+
1339
+ CameraBindingPlan positionPlan = buildPositionPlan(fallbackPosition);
1340
+ return new CameraBindingPlan(
1341
+ positionPlan.selector,
1342
+ deviceId,
1343
+ positionPlan.logicalCameraId,
1344
+ null,
1345
+ getFallbackZoomForDeviceId(deviceId),
1346
+ false
1347
+ );
1348
+ }
1349
+
1350
+ private CameraBindingPlan buildPositionPlan(String position) {
1351
+ int requiredFacing = "front".equals(position) ? CameraSelector.LENS_FACING_FRONT : CameraSelector.LENS_FACING_BACK;
1352
+ CameraSelector selector = new CameraSelector.Builder().requireLensFacing(requiredFacing).build();
1353
+ return new CameraBindingPlan(selector, null, null, null, 1.0f, false);
1354
+ }
1355
+
1356
+ private IllegalArgumentException invalidDeviceId(String deviceId) {
1357
+ return new IllegalArgumentException("Unknown or unsupported deviceId: " + deviceId);
1358
+ }
1359
+
1360
+ private CameraInfo findAvailableCameraInfoById(String deviceId) {
1361
+ if (cameraProvider == null || deviceId == null || deviceId.isEmpty()) {
1362
+ return null;
1363
+ }
1364
+
1365
+ for (CameraInfo cameraInfo : cameraProvider.getAvailableCameraInfos()) {
1366
+ if (deviceId.equals(Camera2CameraInfo.from(cameraInfo).getCameraId())) {
1367
+ return cameraInfo;
1368
+ }
1369
+ }
1370
+
1371
+ return null;
1372
+ }
1373
+
1374
+ @OptIn(markerClass = ExperimentalCamera2Interop.class)
1375
+ private PhysicalCameraBindingTarget findPhysicalCameraBindingTarget(String physicalDeviceId) {
1376
+ if (
1377
+ cameraProvider == null ||
1378
+ physicalDeviceId == null ||
1379
+ physicalDeviceId.isEmpty() ||
1380
+ Build.VERSION.SDK_INT < Build.VERSION_CODES.P
1381
+ ) {
1382
+ return null;
1383
+ }
1384
+
1385
+ CameraManager cameraManager = (CameraManager) context.getSystemService(Context.CAMERA_SERVICE);
1386
+ if (cameraManager == null) {
1387
+ return null;
1388
+ }
1389
+
1390
+ for (CameraInfo cameraInfo : cameraProvider.getAvailableCameraInfos()) {
1391
+ String logicalCameraId = Camera2CameraInfo.from(cameraInfo).getCameraId();
1392
+ try {
1393
+ CameraCharacteristics characteristics = cameraManager.getCameraCharacteristics(logicalCameraId);
1394
+ if (characteristics.getPhysicalCameraIds().contains(physicalDeviceId)) {
1395
+ int requiredFacing = isBackCamera(cameraInfo) ? CameraSelector.LENS_FACING_BACK : CameraSelector.LENS_FACING_FRONT;
1396
+ return new PhysicalCameraBindingTarget(cameraInfo, logicalCameraId, requiredFacing);
1397
+ }
1398
+ } catch (CameraAccessException e) {
1399
+ Log.w(TAG, "findPhysicalCameraBindingTarget: Failed to inspect logical camera " + logicalCameraId, e);
1400
+ }
1401
+ }
1402
+
1403
+ return null;
1404
+ }
1405
+
1406
+ private PhysicalDeviceMetadata resolvePhysicalDeviceMetadata(String deviceId) {
1407
+ if (deviceId == null || deviceId.isEmpty()) {
1408
+ return null;
1409
+ }
1410
+
1411
+ CameraManager cameraManager = (CameraManager) context.getSystemService(Context.CAMERA_SERVICE);
1412
+ if (cameraManager == null) {
1413
+ return null;
1414
+ }
1415
+
1416
+ try {
1417
+ CameraCharacteristics characteristics = cameraManager.getCameraCharacteristics(deviceId);
1418
+ Integer lensFacing = characteristics.get(CameraCharacteristics.LENS_FACING);
1419
+ String position = lensFacing != null && lensFacing == CameraCharacteristics.LENS_FACING_FRONT ? "front" : "rear";
1420
+ return new PhysicalDeviceMetadata(position, getFallbackZoomForCharacteristics(characteristics));
1421
+ } catch (CameraAccessException | IllegalArgumentException e) {
1422
+ Log.w(TAG, "resolvePhysicalDeviceMetadata: Failed to inspect camera " + deviceId, e);
1423
+ return null;
1424
+ }
1425
+ }
1426
+
1427
+ private String resolveFallbackPositionForDeviceId(String deviceId) {
1428
+ app.capgo.capacitor.camera.preview.model.CameraDevice device = findEnumeratedDeviceById(deviceId);
1429
+ if (device != null) {
1430
+ return device.getPosition();
1431
+ }
1432
+
1433
+ PhysicalDeviceMetadata metadata = resolvePhysicalDeviceMetadata(deviceId);
1434
+ return metadata != null ? metadata.position : null;
1435
+ }
1436
+
1437
+ private app.capgo.capacitor.camera.preview.model.CameraDevice findEnumeratedDeviceById(String deviceId) {
1438
+ if (deviceId == null || deviceId.isEmpty()) {
1439
+ return null;
1440
+ }
1441
+
1442
+ app.capgo.capacitor.camera.preview.model.CameraDevice cachedDevice = enumeratedDeviceCache.get(deviceId);
1443
+ if (cachedDevice != null) {
1444
+ return cachedDevice;
1445
+ }
1446
+
1447
+ requestEnumeratedDeviceCacheRefresh();
1448
+ return null;
1449
+ }
1450
+
1451
+ private float getFallbackZoomForDeviceId(String deviceId) {
1452
+ app.capgo.capacitor.camera.preview.model.CameraDevice device = findEnumeratedDeviceById(deviceId);
1453
+ if (device != null) {
1454
+ for (LensInfo lens : device.getLenses()) {
1455
+ if ("ultraWide".equals(lens.getDeviceType())) {
1456
+ return 0.5f;
1457
+ }
1458
+ if ("telephoto".equals(lens.getDeviceType())) {
1459
+ return 2.0f;
1460
+ }
1461
+ }
1462
+ }
1463
+
1464
+ PhysicalDeviceMetadata metadata = resolvePhysicalDeviceMetadata(deviceId);
1465
+ if (metadata != null) {
1466
+ return metadata.fallbackZoom;
1467
+ }
1468
+
1469
+ return 1.0f;
1470
+ }
1471
+
1472
+ private float getFallbackZoomForCharacteristics(CameraCharacteristics characteristics) {
1473
+ float[] focalLengths = characteristics.get(CameraCharacteristics.LENS_INFO_AVAILABLE_FOCAL_LENGTHS);
1474
+ android.util.SizeF sensorSize = characteristics.get(CameraCharacteristics.SENSOR_INFO_PHYSICAL_SIZE);
1475
+
1476
+ if (focalLengths != null && focalLengths.length > 0) {
1477
+ float focalLength = focalLengths[0];
1478
+ if (sensorSize != null && sensorSize.getWidth() > 0) {
1479
+ double fov = 2 * Math.toDegrees(Math.atan(sensorSize.getWidth() / (2 * focalLength)));
1480
+ if (fov > 90) {
1481
+ return 0.5f;
1482
+ }
1483
+ if (fov < 40) {
1484
+ return 2.0f;
1485
+ }
1486
+ } else {
1487
+ if (focalLength < 3.0f) {
1488
+ return 0.5f;
1489
+ }
1490
+ if (focalLength > 5.0f) {
1491
+ return 2.0f;
1492
+ }
1493
+ }
1494
+ }
1495
+
1496
+ return 1.0f;
1497
+ }
1498
+
1499
+ private void bindConfiguredUseCases(CameraBindingPlan bindingPlan, Preview preview) {
1500
+ if (sessionConfig.isVideoModeEnabled() && videoCapture != null) {
1501
+ camera = cameraProvider.bindToLifecycle(this, bindingPlan.selector, preview, imageCapture, videoCapture);
1185
1502
  } else {
1186
- String position = sessionConfig.getPosition();
1187
- int requiredFacing = "front".equals(position) ? CameraSelector.LENS_FACING_FRONT : CameraSelector.LENS_FACING_BACK;
1188
- builder.requireLensFacing(requiredFacing);
1503
+ camera = cameraProvider.bindToLifecycle(this, bindingPlan.selector, preview, imageCapture);
1504
+ }
1505
+
1506
+ CameraInfo cameraInfo = camera.getCameraInfo();
1507
+ currentLogicalDeviceId = Camera2CameraInfo.from(cameraInfo).getCameraId();
1508
+ currentPhysicalDeviceId = bindingPlan.physicalCameraId;
1509
+ currentDeviceId = currentPhysicalDeviceId != null ? currentPhysicalDeviceId : currentLogicalDeviceId;
1510
+
1511
+ Log.d(
1512
+ TAG,
1513
+ "bindConfiguredUseCases: Camera successfully bound. activeDeviceId=" +
1514
+ currentDeviceId +
1515
+ ", logicalCameraId=" +
1516
+ currentLogicalDeviceId +
1517
+ ", physicalCameraId=" +
1518
+ currentPhysicalDeviceId
1519
+ );
1520
+ }
1521
+
1522
+ private void copyMutableSessionConfigState(CameraSessionConfiguration source, CameraSessionConfiguration target) {
1523
+ target.setCentered(source.isCentered());
1524
+ target.setTargetZoom(source.getTargetZoom());
1525
+ target.setEnablePhysicalDeviceSelection(source.isPhysicalDeviceSelectionEnabled());
1526
+ }
1527
+
1528
+ private void requestEnumeratedDeviceCacheRefresh() {
1529
+ synchronized (enumeratedDeviceCacheLock) {
1530
+ if (enumeratedDeviceCacheRefreshInProgress) {
1531
+ return;
1532
+ }
1533
+ enumeratedDeviceCacheRefreshInProgress = true;
1534
+ }
1535
+
1536
+ Runnable refreshTask = () -> {
1537
+ try {
1538
+ getAvailableDevicesStatic(context);
1539
+ } finally {
1540
+ synchronized (enumeratedDeviceCacheLock) {
1541
+ enumeratedDeviceCacheRefreshInProgress = false;
1542
+ }
1543
+ }
1544
+ };
1545
+
1546
+ if (cameraExecutor != null && !cameraExecutor.isShutdown()) {
1547
+ try {
1548
+ cameraExecutor.execute(refreshTask);
1549
+ return;
1550
+ } catch (RejectedExecutionException e) {
1551
+ Log.w(TAG, "requestEnumeratedDeviceCacheRefresh: cameraExecutor rejected refresh task", e);
1552
+ }
1553
+ }
1554
+
1555
+ try {
1556
+ Thread refreshThread = new Thread(refreshTask, "CameraPreview-DeviceCacheRefresh");
1557
+ refreshThread.setDaemon(true);
1558
+ refreshThread.start();
1559
+ } catch (RuntimeException e) {
1560
+ synchronized (enumeratedDeviceCacheLock) {
1561
+ enumeratedDeviceCacheRefreshInProgress = false;
1562
+ }
1563
+ throw e;
1189
1564
  }
1190
- return builder.build();
1191
1565
  }
1192
1566
 
1193
1567
  private static boolean isBackCamera(androidx.camera.core.CameraInfo cameraInfo) {
@@ -2273,6 +2647,7 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
2273
2647
  }
2274
2648
 
2275
2649
  Log.d(TAG, "=== Enumeration Complete: " + devices.size() + " cameras ===");
2650
+ updateEnumeratedDeviceCache(devices);
2276
2651
  return devices;
2277
2652
  } catch (Exception e) {
2278
2653
  Log.e(TAG, "getAvailableDevicesStatic: Error getting devices", e);
@@ -2280,6 +2655,14 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
2280
2655
  }
2281
2656
  }
2282
2657
 
2658
+ private static void updateEnumeratedDeviceCache(List<app.capgo.capacitor.camera.preview.model.CameraDevice> devices) {
2659
+ Map<String, app.capgo.capacitor.camera.preview.model.CameraDevice> newCache = new ConcurrentHashMap<>();
2660
+ for (app.capgo.capacitor.camera.preview.model.CameraDevice device : devices) {
2661
+ newCache.put(device.getDeviceId(), device);
2662
+ }
2663
+ enumeratedDeviceCache = newCache;
2664
+ }
2665
+
2283
2666
  public static ZoomFactors getZoomFactorsStatic() {
2284
2667
  try {
2285
2668
  // For static method, return default zoom factors
@@ -2357,6 +2740,9 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
2357
2740
  }
2358
2741
 
2359
2742
  camera.getCameraControl().setZoomRatio(zoomRatio);
2743
+ if (sessionConfig != null) {
2744
+ sessionConfig.setTargetZoom(zoomRatio);
2745
+ }
2360
2746
  // Note: autofocus is intentionally not triggered on zoom because it's done by CameraX
2361
2747
  } catch (Exception e) {
2362
2748
  Log.e(TAG, "Failed to set zoom: " + e.getMessage());
@@ -2880,53 +3266,52 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
2880
3266
 
2881
3267
  mainExecutor.execute(() -> {
2882
3268
  try {
2883
- // Standard physical device selection logic...
2884
- List<CameraInfo> cameraInfos = cameraProvider.getAvailableCameraInfos();
2885
-
2886
- CameraInfo targetCameraInfo = null;
2887
- for (CameraInfo cameraInfo : cameraInfos) {
2888
- String id = Camera2CameraInfo.from(cameraInfo).getCameraId();
2889
- if (deviceId.equals(id)) {
2890
- targetCameraInfo = cameraInfo;
2891
- break;
3269
+ CameraSessionConfiguration previousConfig = sessionConfig;
3270
+ CameraInfo targetCameraInfo = findAvailableCameraInfoById(deviceId);
3271
+ String fallbackPosition = resolveFallbackPositionForDeviceId(deviceId);
3272
+ String position = fallbackPosition != null ? fallbackPosition : previousConfig.getPosition();
3273
+ if (targetCameraInfo != null) {
3274
+ position = isBackCamera(targetCameraInfo) ? "rear" : "front";
3275
+ } else if (previousConfig.isPhysicalDeviceSelectionEnabled()) {
3276
+ PhysicalCameraBindingTarget physicalTarget = findPhysicalCameraBindingTarget(deviceId);
3277
+ if (physicalTarget != null) {
3278
+ position = physicalTarget.requiredFacing == CameraSelector.LENS_FACING_FRONT ? "front" : "rear";
3279
+ } else if (fallbackPosition == null) {
3280
+ Log.e(TAG, "switchToDevice: Could not resolve deviceId: " + deviceId);
3281
+ return;
2892
3282
  }
3283
+ } else if (fallbackPosition == null) {
3284
+ Log.e(TAG, "switchToDevice: Could not find any CameraInfo matching deviceId: " + deviceId);
3285
+ return;
2893
3286
  }
2894
3287
 
2895
- if (targetCameraInfo != null) {
2896
- // Determine position from the target camera
2897
- String position = isBackCamera(targetCameraInfo) ? "rear" : "front";
2898
- boolean wasCentered = sessionConfig.isCentered();
2899
-
2900
- // Update sessionConfig with the new device ID
2901
- sessionConfig = new CameraSessionConfiguration(
2902
- deviceId,
2903
- position,
2904
- sessionConfig.getX(),
2905
- sessionConfig.getY(),
2906
- sessionConfig.getWidth(),
2907
- sessionConfig.getHeight(),
2908
- sessionConfig.getPaddingBottom(),
2909
- sessionConfig.getToBack(),
2910
- sessionConfig.getStoreToFile(),
2911
- sessionConfig.getEnableOpacity(),
2912
- sessionConfig.getDisableExifHeaderStripping(),
2913
- sessionConfig.getDisableAudio(),
2914
- sessionConfig.getZoomFactor(),
2915
- sessionConfig.getAspectRatio(),
2916
- sessionConfig.getAspectMode(),
2917
- sessionConfig.getGridMode(),
2918
- sessionConfig.getDisableFocusIndicator(),
2919
- sessionConfig.isVideoModeEnabled(),
2920
- sessionConfig.getVideoQuality()
2921
- );
2922
-
2923
- sessionConfig.setCentered(wasCentered);
3288
+ CameraSessionConfiguration updatedConfig = new CameraSessionConfiguration(
3289
+ deviceId,
3290
+ position,
3291
+ previousConfig.getX(),
3292
+ previousConfig.getY(),
3293
+ previousConfig.getWidth(),
3294
+ previousConfig.getHeight(),
3295
+ previousConfig.getPaddingBottom(),
3296
+ previousConfig.getToBack(),
3297
+ previousConfig.getStoreToFile(),
3298
+ previousConfig.getEnableOpacity(),
3299
+ previousConfig.getDisableExifHeaderStripping(),
3300
+ previousConfig.getDisableAudio(),
3301
+ previousConfig.getZoomFactor(),
3302
+ previousConfig.getAspectRatio(),
3303
+ previousConfig.getAspectMode(),
3304
+ previousConfig.getGridMode(),
3305
+ previousConfig.getDisableFocusIndicator(),
3306
+ previousConfig.isVideoModeEnabled(),
3307
+ previousConfig.getVideoQuality()
3308
+ );
3309
+ copyMutableSessionConfigState(previousConfig, updatedConfig);
3310
+ updatedConfig.setTargetZoom(1.0f);
3311
+ sessionConfig = updatedConfig;
2924
3312
 
2925
- Log.d(TAG, "switchToDevice: Updated sessionConfig with deviceId: " + deviceId);
2926
- bindCameraUseCases(); // Will now use deviceId from sessionConfig
2927
- } else {
2928
- Log.e(TAG, "switchToDevice: Could not find any CameraInfo matching deviceId: " + deviceId);
2929
- }
3313
+ Log.d(TAG, "switchToDevice: Updated sessionConfig with deviceId: " + deviceId);
3314
+ bindCameraUseCases();
2930
3315
  } catch (Exception e) {
2931
3316
  Log.e(TAG, "switchToDevice: Error switching camera", e);
2932
3317
  }
@@ -2944,38 +3329,39 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
2944
3329
  boolean wasCentered = sessionConfig.isCentered();
2945
3330
  Log.d(TAG, "flipCamera: Switching from " + currentPosition + " to " + newPosition);
2946
3331
 
3332
+ CameraSessionConfiguration previousConfig = sessionConfig;
2947
3333
  sessionConfig = new CameraSessionConfiguration(
2948
3334
  null, // deviceId - clear device ID to force position-based selection
2949
3335
  newPosition, // position
2950
- sessionConfig.getX(), // x
2951
- sessionConfig.getY(), // y
2952
- sessionConfig.getWidth(), // width
2953
- sessionConfig.getHeight(), // height
2954
- sessionConfig.getPaddingBottom(), // paddingBottom
2955
- sessionConfig.isToBack(), // toBack
2956
- sessionConfig.isStoreToFile(), // storeToFile
2957
- sessionConfig.isEnableOpacity(), // enableOpacity
2958
- sessionConfig.isDisableExifHeaderStripping(), // disableExifHeaderStripping
2959
- sessionConfig.isDisableAudio(), // disableAudio
2960
- sessionConfig.getZoomFactor(), // zoomFactor
2961
- sessionConfig.getAspectRatio(), // aspectRatio
2962
- sessionConfig.getAspectMode(), // aspectMode
2963
- sessionConfig.getGridMode(), // gridMode
2964
- sessionConfig.getDisableFocusIndicator(), // disableFocusIndicator
2965
- sessionConfig.isVideoModeEnabled(), // enableVideoMode
2966
- sessionConfig.getVideoQuality() // videoQuality
3336
+ previousConfig.getX(), // x
3337
+ previousConfig.getY(), // y
3338
+ previousConfig.getWidth(), // width
3339
+ previousConfig.getHeight(), // height
3340
+ previousConfig.getPaddingBottom(), // paddingBottom
3341
+ previousConfig.isToBack(), // toBack
3342
+ previousConfig.isStoreToFile(), // storeToFile
3343
+ previousConfig.isEnableOpacity(), // enableOpacity
3344
+ previousConfig.isDisableExifHeaderStripping(), // disableExifHeaderStripping
3345
+ previousConfig.isDisableAudio(), // disableAudio
3346
+ previousConfig.getZoomFactor(), // zoomFactor
3347
+ previousConfig.getAspectRatio(), // aspectRatio
3348
+ previousConfig.getAspectMode(), // aspectMode
3349
+ previousConfig.getGridMode(), // gridMode
3350
+ previousConfig.getDisableFocusIndicator(), // disableFocusIndicator
3351
+ previousConfig.isVideoModeEnabled(), // enableVideoMode
3352
+ previousConfig.getVideoQuality() // videoQuality
2967
3353
  );
2968
-
3354
+ copyMutableSessionConfigState(previousConfig, sessionConfig);
3355
+ sessionConfig.setTargetZoom(1.0f);
2969
3356
  sessionConfig.setCentered(wasCentered);
2970
3357
 
2971
- // Clear current device ID to force position-based selection
3358
+ // Clear current device IDs to force position-based selection
2972
3359
  currentDeviceId = null;
3360
+ currentPhysicalDeviceId = null;
3361
+ currentLogicalDeviceId = null;
2973
3362
 
2974
- // Camera operations must run on main thread
2975
- cameraExecutor.execute(() -> {
2976
- currentCameraSelector = buildCameraSelector();
2977
- bindCameraUseCases();
2978
- });
3363
+ // Rebind camera with the new position
3364
+ bindCameraUseCases();
2979
3365
  }
2980
3366
 
2981
3367
  public void setOpacity(float opacity) {
@@ -3059,32 +3445,34 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
3059
3445
  return;
3060
3446
  }
3061
3447
 
3062
- String currentGridMode = sessionConfig.getGridMode();
3448
+ CameraSessionConfiguration previousConfig = sessionConfig;
3449
+ String currentGridMode = previousConfig.getGridMode();
3063
3450
  Log.d(TAG, "Changing aspect ratio from " + currentAspectRatio + " to " + aspectRatio);
3064
3451
  Log.d(TAG, "Auto-centering will be applied (matching iOS behavior)");
3065
3452
 
3066
3453
  // Match iOS behavior: when aspect ratio changes, always auto-center
3067
3454
  sessionConfig = new CameraSessionConfiguration(
3068
- sessionConfig.getDeviceId(),
3069
- sessionConfig.getPosition(),
3455
+ previousConfig.getDeviceId(),
3456
+ previousConfig.getPosition(),
3070
3457
  -1, // Force auto-center X (iOS: self.posX = -1)
3071
3458
  -1, // Force auto-center Y (iOS: self.posY = -1)
3072
- sessionConfig.getWidth(),
3073
- sessionConfig.getHeight(),
3074
- sessionConfig.getPaddingBottom(),
3075
- sessionConfig.getToBack(),
3076
- sessionConfig.getStoreToFile(),
3077
- sessionConfig.getEnableOpacity(),
3078
- sessionConfig.getDisableExifHeaderStripping(),
3079
- sessionConfig.getDisableAudio(),
3080
- sessionConfig.getZoomFactor(),
3459
+ previousConfig.getWidth(),
3460
+ previousConfig.getHeight(),
3461
+ previousConfig.getPaddingBottom(),
3462
+ previousConfig.getToBack(),
3463
+ previousConfig.getStoreToFile(),
3464
+ previousConfig.getEnableOpacity(),
3465
+ previousConfig.getDisableExifHeaderStripping(),
3466
+ previousConfig.getDisableAudio(),
3467
+ previousConfig.getZoomFactor(),
3081
3468
  aspectRatio,
3082
- sessionConfig.getAspectMode(),
3469
+ previousConfig.getAspectMode(),
3083
3470
  currentGridMode,
3084
- sessionConfig.getDisableFocusIndicator(),
3085
- sessionConfig.isVideoModeEnabled(),
3086
- sessionConfig.getVideoQuality()
3471
+ previousConfig.getDisableFocusIndicator(),
3472
+ previousConfig.isVideoModeEnabled(),
3473
+ previousConfig.getVideoQuality()
3087
3474
  );
3475
+ copyMutableSessionConfigState(previousConfig, sessionConfig);
3088
3476
  sessionConfig.setCentered(true);
3089
3477
 
3090
3478
  // Update layout and rebind camera with new aspect ratio
@@ -3135,32 +3523,34 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
3135
3523
  return;
3136
3524
  }
3137
3525
 
3138
- String currentGridMode = sessionConfig.getGridMode();
3526
+ CameraSessionConfiguration previousConfig = sessionConfig;
3527
+ String currentGridMode = previousConfig.getGridMode();
3139
3528
  Log.d(TAG, "Forcing aspect ratio recalculation for: " + aspectRatio);
3140
3529
  Log.d(TAG, "Auto-centering will be applied (matching iOS behavior)");
3141
3530
 
3142
3531
  // Match iOS behavior: when aspect ratio changes, always auto-center
3143
3532
  sessionConfig = new CameraSessionConfiguration(
3144
- sessionConfig.getDeviceId(),
3145
- sessionConfig.getPosition(),
3533
+ previousConfig.getDeviceId(),
3534
+ previousConfig.getPosition(),
3146
3535
  -1, // Force auto-center X (iOS: self.posX = -1)
3147
3536
  -1, // Force auto-center Y (iOS: self.posY = -1)
3148
- sessionConfig.getWidth(),
3149
- sessionConfig.getHeight(),
3150
- sessionConfig.getPaddingBottom(),
3151
- sessionConfig.getToBack(),
3152
- sessionConfig.getStoreToFile(),
3153
- sessionConfig.getEnableOpacity(),
3154
- sessionConfig.getDisableExifHeaderStripping(),
3155
- sessionConfig.getDisableAudio(),
3156
- sessionConfig.getZoomFactor(),
3537
+ previousConfig.getWidth(),
3538
+ previousConfig.getHeight(),
3539
+ previousConfig.getPaddingBottom(),
3540
+ previousConfig.getToBack(),
3541
+ previousConfig.getStoreToFile(),
3542
+ previousConfig.getEnableOpacity(),
3543
+ previousConfig.getDisableExifHeaderStripping(),
3544
+ previousConfig.getDisableAudio(),
3545
+ previousConfig.getZoomFactor(),
3157
3546
  aspectRatio,
3158
- sessionConfig.getAspectMode(),
3547
+ previousConfig.getAspectMode(),
3159
3548
  currentGridMode,
3160
- sessionConfig.getDisableFocusIndicator(),
3161
- sessionConfig.isVideoModeEnabled(),
3162
- sessionConfig.getVideoQuality()
3549
+ previousConfig.getDisableFocusIndicator(),
3550
+ previousConfig.isVideoModeEnabled(),
3551
+ previousConfig.getVideoQuality()
3163
3552
  );
3553
+ copyMutableSessionConfigState(previousConfig, sessionConfig);
3164
3554
  sessionConfig.setCentered(true);
3165
3555
 
3166
3556
  // Update layout and rebind camera with new aspect ratio
@@ -3203,27 +3593,29 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
3203
3593
  public void setGridMode(String gridMode) {
3204
3594
  if (sessionConfig != null) {
3205
3595
  Log.d(TAG, "setGridMode: Changing grid mode to: " + gridMode);
3596
+ CameraSessionConfiguration previousConfig = sessionConfig;
3206
3597
  sessionConfig = new CameraSessionConfiguration(
3207
- sessionConfig.getDeviceId(),
3208
- sessionConfig.getPosition(),
3209
- sessionConfig.getX(),
3210
- sessionConfig.getY(),
3211
- sessionConfig.getWidth(),
3212
- sessionConfig.getHeight(),
3213
- sessionConfig.getPaddingBottom(),
3214
- sessionConfig.getToBack(),
3215
- sessionConfig.getStoreToFile(),
3216
- sessionConfig.getEnableOpacity(),
3217
- sessionConfig.getDisableExifHeaderStripping(),
3218
- sessionConfig.getDisableAudio(),
3219
- sessionConfig.getZoomFactor(),
3220
- sessionConfig.getAspectRatio(),
3221
- sessionConfig.getAspectMode(),
3598
+ previousConfig.getDeviceId(),
3599
+ previousConfig.getPosition(),
3600
+ previousConfig.getX(),
3601
+ previousConfig.getY(),
3602
+ previousConfig.getWidth(),
3603
+ previousConfig.getHeight(),
3604
+ previousConfig.getPaddingBottom(),
3605
+ previousConfig.getToBack(),
3606
+ previousConfig.getStoreToFile(),
3607
+ previousConfig.getEnableOpacity(),
3608
+ previousConfig.getDisableExifHeaderStripping(),
3609
+ previousConfig.getDisableAudio(),
3610
+ previousConfig.getZoomFactor(),
3611
+ previousConfig.getAspectRatio(),
3612
+ previousConfig.getAspectMode(),
3222
3613
  gridMode,
3223
- sessionConfig.getDisableFocusIndicator(),
3224
- sessionConfig.isVideoModeEnabled(),
3225
- sessionConfig.getVideoQuality()
3614
+ previousConfig.getDisableFocusIndicator(),
3615
+ previousConfig.isVideoModeEnabled(),
3616
+ previousConfig.getVideoQuality()
3226
3617
  );
3618
+ copyMutableSessionConfigState(previousConfig, sessionConfig);
3227
3619
 
3228
3620
  // Update the grid overlay immediately
3229
3621
  if (gridOverlayView != null) {
@@ -3543,27 +3935,29 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
3543
3935
  );
3544
3936
  }
3545
3937
 
3938
+ CameraSessionConfiguration previousConfig = sessionConfig;
3546
3939
  sessionConfig = new CameraSessionConfiguration(
3547
- sessionConfig.getDeviceId(),
3548
- sessionConfig.getPosition(),
3940
+ previousConfig.getDeviceId(),
3941
+ previousConfig.getPosition(),
3549
3942
  params.leftMargin,
3550
3943
  params.topMargin,
3551
3944
  params.width,
3552
3945
  params.height,
3553
- sessionConfig.getPaddingBottom(),
3554
- sessionConfig.getToBack(),
3555
- sessionConfig.getStoreToFile(),
3556
- sessionConfig.getEnableOpacity(),
3557
- sessionConfig.getDisableExifHeaderStripping(),
3558
- sessionConfig.getDisableAudio(),
3559
- sessionConfig.getZoomFactor(),
3946
+ previousConfig.getPaddingBottom(),
3947
+ previousConfig.getToBack(),
3948
+ previousConfig.getStoreToFile(),
3949
+ previousConfig.getEnableOpacity(),
3950
+ previousConfig.getDisableExifHeaderStripping(),
3951
+ previousConfig.getDisableAudio(),
3952
+ previousConfig.getZoomFactor(),
3560
3953
  calculatedAspectRatio,
3561
- sessionConfig.getAspectMode(),
3562
- sessionConfig.getGridMode(),
3563
- sessionConfig.getDisableFocusIndicator(),
3564
- sessionConfig.isVideoModeEnabled(),
3565
- sessionConfig.getVideoQuality()
3954
+ previousConfig.getAspectMode(),
3955
+ previousConfig.getGridMode(),
3956
+ previousConfig.getDisableFocusIndicator(),
3957
+ previousConfig.isVideoModeEnabled(),
3958
+ previousConfig.getVideoQuality()
3566
3959
  );
3960
+ copyMutableSessionConfigState(previousConfig, sessionConfig);
3567
3961
 
3568
3962
  // If aspect ratio changed due to size update, rebind camera
3569
3963
  if (isRunning && !Objects.equals(currentAspectRatio, calculatedAspectRatio)) {