@capgo/camera-preview 8.2.2 → 8.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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 +553 -158
  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);
@@ -744,6 +807,7 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
744
807
  int width = sessionConfig.getWidth();
745
808
  int height = sessionConfig.getHeight();
746
809
  String aspectRatio = sessionConfig.getAspectRatio();
810
+ String aspectMode = sessionConfig.getAspectMode();
747
811
 
748
812
  // Get comprehensive display information
749
813
  int screenWidthPx, screenHeightPx;
@@ -813,7 +877,7 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
813
877
  );
814
878
 
815
879
  // Apply aspect ratio if specified
816
- if (aspectRatio != null && !aspectRatio.isEmpty() && sessionConfig.isCentered()) {
880
+ if (aspectRatio != null && !aspectRatio.isEmpty() && sessionConfig.isCentered() && !"cover".equals(aspectMode)) {
817
881
  String[] ratios = aspectRatio.split(":");
818
882
  if (ratios.length == 2) {
819
883
  try {
@@ -928,7 +992,8 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
928
992
  " and position: " +
929
993
  sessionConfig.getPosition()
930
994
  );
931
- currentCameraSelector = buildCameraSelector();
995
+ CameraBindingPlan bindingPlan = buildCameraBindingPlan(sessionConfig);
996
+ currentCameraSelector = bindingPlan.selector;
932
997
 
933
998
  ResolutionSelector.Builder resolutionSelectorBuilder = new ResolutionSelector.Builder().setResolutionStrategy(
934
999
  ResolutionStrategy.HIGHEST_AVAILABLE_STRATEGY
@@ -1006,17 +1071,24 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
1006
1071
  // is connected and video frames are captured correctly
1007
1072
  preview.setSurfaceProvider(previewView.getSurfaceProvider());
1008
1073
 
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);
1074
+ try {
1075
+ bindConfiguredUseCases(bindingPlan, preview);
1076
+ } catch (Exception initialBindError) {
1077
+ if (!bindingPlan.usesPhysicalSelection) {
1078
+ throw initialBindError;
1079
+ }
1080
+
1081
+ Log.w(
1082
+ TAG,
1083
+ "bindCameraUseCases: Physical camera binding failed for " +
1084
+ bindingPlan.physicalCameraId +
1085
+ ", falling back to logical camera behavior",
1086
+ initialBindError
1087
+ );
1088
+
1089
+ bindingPlan = buildLogicalFallbackPlan(sessionConfig, bindingPlan);
1090
+ currentCameraSelector = bindingPlan.selector;
1091
+ bindConfiguredUseCases(bindingPlan, preview);
1020
1092
  }
1021
1093
 
1022
1094
  resetExposureCompensationToDefault();
@@ -1024,7 +1096,10 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
1024
1096
  // Log details about the active camera
1025
1097
  Log.d(TAG, "Use cases bound. Inspecting active camera and use cases.");
1026
1098
  CameraInfo cameraInfo = camera.getCameraInfo();
1027
- Log.d(TAG, "Bound Camera ID: " + Camera2CameraInfo.from(cameraInfo).getCameraId());
1099
+ Log.d(TAG, "Bound Camera ID: " + currentLogicalDeviceId);
1100
+ if (currentPhysicalDeviceId != null) {
1101
+ Log.d(TAG, "Bound Physical Camera ID: " + currentPhysicalDeviceId);
1102
+ }
1028
1103
 
1029
1104
  // Log zoom state
1030
1105
  ZoomState zoomState = cameraInfo.getZoomState().getValue();
@@ -1100,7 +1175,11 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
1100
1175
  previewView.setScaleType("cover".equals(aspectMode) ? PreviewView.ScaleType.FILL_CENTER : PreviewView.ScaleType.FIT_CENTER);
1101
1176
 
1102
1177
  // Set initial zoom if specified, prioritizing targetZoom over default zoomFactor
1103
- float initialZoom = sessionConfig.getTargetZoom() != 1.0f ? sessionConfig.getTargetZoom() : sessionConfig.getZoomFactor();
1178
+ float initialZoom = !bindingPlan.usesPhysicalSelection &&
1179
+ bindingPlan.fallbackZoom != 1.0f &&
1180
+ sessionConfig.getTargetZoom() == 1.0f
1181
+ ? bindingPlan.fallbackZoom
1182
+ : (sessionConfig.getTargetZoom() != 1.0f ? sessionConfig.getTargetZoom() : sessionConfig.getZoomFactor());
1104
1183
  if (initialZoom != 1.0f) {
1105
1184
  Log.d(TAG, "Applying initial zoom of " + initialZoom);
1106
1185
 
@@ -1170,24 +1249,320 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
1170
1249
 
1171
1250
  @OptIn(markerClass = ExperimentalCamera2Interop.class)
1172
1251
  private CameraSelector buildCameraSelector() {
1173
- CameraSelector.Builder builder = new CameraSelector.Builder();
1174
- final String deviceId = sessionConfig.getDeviceId();
1252
+ return buildCameraBindingPlan(sessionConfig).selector;
1253
+ }
1254
+
1255
+ @OptIn(markerClass = ExperimentalCamera2Interop.class)
1256
+ private CameraBindingPlan buildCameraBindingPlan(CameraSessionConfiguration config) {
1257
+ final String deviceId = config.getDeviceId();
1258
+ final String position = config.getPosition();
1175
1259
 
1176
1260
  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);
1261
+ CameraInfo directCameraInfo = findAvailableCameraInfoById(deviceId);
1262
+ if (directCameraInfo != null) {
1263
+ CameraSelector.Builder directBuilder = new CameraSelector.Builder();
1264
+ directBuilder.addCameraFilter((cameraInfos) -> {
1265
+ for (CameraInfo cameraInfo : cameraInfos) {
1266
+ if (deviceId.equals(Camera2CameraInfo.from(cameraInfo).getCameraId())) {
1267
+ return Collections.singletonList(cameraInfo);
1268
+ }
1181
1269
  }
1270
+ return Collections.emptyList();
1271
+ });
1272
+
1273
+ return new CameraBindingPlan(directBuilder.build(), deviceId, deviceId, null, 1.0f, false);
1274
+ }
1275
+
1276
+ CameraBindingPlan logicalFallbackPlan = buildLogicalFallbackPlanForDeviceId(deviceId);
1277
+ if (config.isPhysicalDeviceSelectionEnabled()) {
1278
+ PhysicalCameraBindingTarget physicalTarget = findPhysicalCameraBindingTarget(deviceId);
1279
+ if (physicalTarget != null) {
1280
+ CameraSelector.Builder physicalBuilder = new CameraSelector.Builder()
1281
+ .requireLensFacing(physicalTarget.requiredFacing)
1282
+ .setPhysicalCameraId(deviceId)
1283
+ .addCameraFilter((cameraInfos) -> {
1284
+ for (CameraInfo cameraInfo : cameraInfos) {
1285
+ if (physicalTarget.logicalCameraId.equals(Camera2CameraInfo.from(cameraInfo).getCameraId())) {
1286
+ return Collections.singletonList(cameraInfo);
1287
+ }
1288
+ }
1289
+ return Collections.emptyList();
1290
+ });
1291
+
1292
+ return new CameraBindingPlan(
1293
+ physicalBuilder.build(),
1294
+ deviceId,
1295
+ physicalTarget.logicalCameraId,
1296
+ deviceId,
1297
+ logicalFallbackPlan != null ? logicalFallbackPlan.fallbackZoom : getFallbackZoomForDeviceId(deviceId),
1298
+ true
1299
+ );
1182
1300
  }
1183
- return Collections.emptyList();
1184
- });
1301
+ }
1302
+
1303
+ if (logicalFallbackPlan != null) {
1304
+ return logicalFallbackPlan;
1305
+ }
1306
+
1307
+ throw invalidDeviceId(deviceId);
1308
+ }
1309
+
1310
+ return buildPositionPlan(position);
1311
+ }
1312
+
1313
+ private CameraBindingPlan buildLogicalFallbackPlan(CameraSessionConfiguration config, CameraBindingPlan failedPhysicalPlan) {
1314
+ String fallbackPosition = config.getPosition();
1315
+
1316
+ if (failedPhysicalPlan.logicalCameraId != null) {
1317
+ CameraInfo logicalCameraInfo = findAvailableCameraInfoById(failedPhysicalPlan.logicalCameraId);
1318
+ if (logicalCameraInfo != null) {
1319
+ fallbackPosition = isBackCamera(logicalCameraInfo) ? "rear" : "front";
1320
+ }
1321
+ }
1322
+
1323
+ CameraBindingPlan positionPlan = buildPositionPlan(fallbackPosition);
1324
+ return new CameraBindingPlan(
1325
+ positionPlan.selector,
1326
+ failedPhysicalPlan.reportedDeviceId,
1327
+ positionPlan.logicalCameraId,
1328
+ null,
1329
+ failedPhysicalPlan.fallbackZoom,
1330
+ false
1331
+ );
1332
+ }
1333
+
1334
+ private CameraBindingPlan buildLogicalFallbackPlanForDeviceId(String deviceId) {
1335
+ String fallbackPosition = resolveFallbackPositionForDeviceId(deviceId);
1336
+ if (fallbackPosition == null) {
1337
+ return null;
1338
+ }
1339
+
1340
+ CameraBindingPlan positionPlan = buildPositionPlan(fallbackPosition);
1341
+ return new CameraBindingPlan(
1342
+ positionPlan.selector,
1343
+ deviceId,
1344
+ positionPlan.logicalCameraId,
1345
+ null,
1346
+ getFallbackZoomForDeviceId(deviceId),
1347
+ false
1348
+ );
1349
+ }
1350
+
1351
+ private CameraBindingPlan buildPositionPlan(String position) {
1352
+ int requiredFacing = "front".equals(position) ? CameraSelector.LENS_FACING_FRONT : CameraSelector.LENS_FACING_BACK;
1353
+ CameraSelector selector = new CameraSelector.Builder().requireLensFacing(requiredFacing).build();
1354
+ return new CameraBindingPlan(selector, null, null, null, 1.0f, false);
1355
+ }
1356
+
1357
+ private IllegalArgumentException invalidDeviceId(String deviceId) {
1358
+ return new IllegalArgumentException("Unknown or unsupported deviceId: " + deviceId);
1359
+ }
1360
+
1361
+ private CameraInfo findAvailableCameraInfoById(String deviceId) {
1362
+ if (cameraProvider == null || deviceId == null || deviceId.isEmpty()) {
1363
+ return null;
1364
+ }
1365
+
1366
+ for (CameraInfo cameraInfo : cameraProvider.getAvailableCameraInfos()) {
1367
+ if (deviceId.equals(Camera2CameraInfo.from(cameraInfo).getCameraId())) {
1368
+ return cameraInfo;
1369
+ }
1370
+ }
1371
+
1372
+ return null;
1373
+ }
1374
+
1375
+ @OptIn(markerClass = ExperimentalCamera2Interop.class)
1376
+ private PhysicalCameraBindingTarget findPhysicalCameraBindingTarget(String physicalDeviceId) {
1377
+ if (
1378
+ cameraProvider == null ||
1379
+ physicalDeviceId == null ||
1380
+ physicalDeviceId.isEmpty() ||
1381
+ Build.VERSION.SDK_INT < Build.VERSION_CODES.P
1382
+ ) {
1383
+ return null;
1384
+ }
1385
+
1386
+ CameraManager cameraManager = (CameraManager) context.getSystemService(Context.CAMERA_SERVICE);
1387
+ if (cameraManager == null) {
1388
+ return null;
1389
+ }
1390
+
1391
+ for (CameraInfo cameraInfo : cameraProvider.getAvailableCameraInfos()) {
1392
+ String logicalCameraId = Camera2CameraInfo.from(cameraInfo).getCameraId();
1393
+ try {
1394
+ CameraCharacteristics characteristics = cameraManager.getCameraCharacteristics(logicalCameraId);
1395
+ if (characteristics.getPhysicalCameraIds().contains(physicalDeviceId)) {
1396
+ int requiredFacing = isBackCamera(cameraInfo) ? CameraSelector.LENS_FACING_BACK : CameraSelector.LENS_FACING_FRONT;
1397
+ return new PhysicalCameraBindingTarget(cameraInfo, logicalCameraId, requiredFacing);
1398
+ }
1399
+ } catch (CameraAccessException e) {
1400
+ Log.w(TAG, "findPhysicalCameraBindingTarget: Failed to inspect logical camera " + logicalCameraId, e);
1401
+ }
1402
+ }
1403
+
1404
+ return null;
1405
+ }
1406
+
1407
+ private PhysicalDeviceMetadata resolvePhysicalDeviceMetadata(String deviceId) {
1408
+ if (deviceId == null || deviceId.isEmpty()) {
1409
+ return null;
1410
+ }
1411
+
1412
+ CameraManager cameraManager = (CameraManager) context.getSystemService(Context.CAMERA_SERVICE);
1413
+ if (cameraManager == null) {
1414
+ return null;
1415
+ }
1416
+
1417
+ try {
1418
+ CameraCharacteristics characteristics = cameraManager.getCameraCharacteristics(deviceId);
1419
+ Integer lensFacing = characteristics.get(CameraCharacteristics.LENS_FACING);
1420
+ String position = lensFacing != null && lensFacing == CameraCharacteristics.LENS_FACING_FRONT ? "front" : "rear";
1421
+ return new PhysicalDeviceMetadata(position, getFallbackZoomForCharacteristics(characteristics));
1422
+ } catch (CameraAccessException | IllegalArgumentException e) {
1423
+ Log.w(TAG, "resolvePhysicalDeviceMetadata: Failed to inspect camera " + deviceId, e);
1424
+ return null;
1425
+ }
1426
+ }
1427
+
1428
+ private String resolveFallbackPositionForDeviceId(String deviceId) {
1429
+ app.capgo.capacitor.camera.preview.model.CameraDevice device = findEnumeratedDeviceById(deviceId);
1430
+ if (device != null) {
1431
+ return device.getPosition();
1432
+ }
1433
+
1434
+ PhysicalDeviceMetadata metadata = resolvePhysicalDeviceMetadata(deviceId);
1435
+ return metadata != null ? metadata.position : null;
1436
+ }
1437
+
1438
+ private app.capgo.capacitor.camera.preview.model.CameraDevice findEnumeratedDeviceById(String deviceId) {
1439
+ if (deviceId == null || deviceId.isEmpty()) {
1440
+ return null;
1441
+ }
1442
+
1443
+ app.capgo.capacitor.camera.preview.model.CameraDevice cachedDevice = enumeratedDeviceCache.get(deviceId);
1444
+ if (cachedDevice != null) {
1445
+ return cachedDevice;
1446
+ }
1447
+
1448
+ requestEnumeratedDeviceCacheRefresh();
1449
+ return null;
1450
+ }
1451
+
1452
+ private float getFallbackZoomForDeviceId(String deviceId) {
1453
+ app.capgo.capacitor.camera.preview.model.CameraDevice device = findEnumeratedDeviceById(deviceId);
1454
+ if (device != null) {
1455
+ for (LensInfo lens : device.getLenses()) {
1456
+ if ("ultraWide".equals(lens.getDeviceType())) {
1457
+ return 0.5f;
1458
+ }
1459
+ if ("telephoto".equals(lens.getDeviceType())) {
1460
+ return 2.0f;
1461
+ }
1462
+ }
1463
+ }
1464
+
1465
+ PhysicalDeviceMetadata metadata = resolvePhysicalDeviceMetadata(deviceId);
1466
+ if (metadata != null) {
1467
+ return metadata.fallbackZoom;
1468
+ }
1469
+
1470
+ return 1.0f;
1471
+ }
1472
+
1473
+ private float getFallbackZoomForCharacteristics(CameraCharacteristics characteristics) {
1474
+ float[] focalLengths = characteristics.get(CameraCharacteristics.LENS_INFO_AVAILABLE_FOCAL_LENGTHS);
1475
+ android.util.SizeF sensorSize = characteristics.get(CameraCharacteristics.SENSOR_INFO_PHYSICAL_SIZE);
1476
+
1477
+ if (focalLengths != null && focalLengths.length > 0) {
1478
+ float focalLength = focalLengths[0];
1479
+ if (sensorSize != null && sensorSize.getWidth() > 0) {
1480
+ double fov = 2 * Math.toDegrees(Math.atan(sensorSize.getWidth() / (2 * focalLength)));
1481
+ if (fov > 90) {
1482
+ return 0.5f;
1483
+ }
1484
+ if (fov < 40) {
1485
+ return 2.0f;
1486
+ }
1487
+ } else {
1488
+ if (focalLength < 3.0f) {
1489
+ return 0.5f;
1490
+ }
1491
+ if (focalLength > 5.0f) {
1492
+ return 2.0f;
1493
+ }
1494
+ }
1495
+ }
1496
+
1497
+ return 1.0f;
1498
+ }
1499
+
1500
+ private void bindConfiguredUseCases(CameraBindingPlan bindingPlan, Preview preview) {
1501
+ if (sessionConfig.isVideoModeEnabled() && videoCapture != null) {
1502
+ camera = cameraProvider.bindToLifecycle(this, bindingPlan.selector, preview, imageCapture, videoCapture);
1185
1503
  } else {
1186
- String position = sessionConfig.getPosition();
1187
- int requiredFacing = "front".equals(position) ? CameraSelector.LENS_FACING_FRONT : CameraSelector.LENS_FACING_BACK;
1188
- builder.requireLensFacing(requiredFacing);
1504
+ camera = cameraProvider.bindToLifecycle(this, bindingPlan.selector, preview, imageCapture);
1505
+ }
1506
+
1507
+ CameraInfo cameraInfo = camera.getCameraInfo();
1508
+ currentLogicalDeviceId = Camera2CameraInfo.from(cameraInfo).getCameraId();
1509
+ currentPhysicalDeviceId = bindingPlan.physicalCameraId;
1510
+ currentDeviceId = currentPhysicalDeviceId != null ? currentPhysicalDeviceId : currentLogicalDeviceId;
1511
+
1512
+ Log.d(
1513
+ TAG,
1514
+ "bindConfiguredUseCases: Camera successfully bound. activeDeviceId=" +
1515
+ currentDeviceId +
1516
+ ", logicalCameraId=" +
1517
+ currentLogicalDeviceId +
1518
+ ", physicalCameraId=" +
1519
+ currentPhysicalDeviceId
1520
+ );
1521
+ }
1522
+
1523
+ private void copyMutableSessionConfigState(CameraSessionConfiguration source, CameraSessionConfiguration target) {
1524
+ target.setCentered(source.isCentered());
1525
+ target.setTargetZoom(source.getTargetZoom());
1526
+ target.setEnablePhysicalDeviceSelection(source.isPhysicalDeviceSelectionEnabled());
1527
+ }
1528
+
1529
+ private void requestEnumeratedDeviceCacheRefresh() {
1530
+ synchronized (enumeratedDeviceCacheLock) {
1531
+ if (enumeratedDeviceCacheRefreshInProgress) {
1532
+ return;
1533
+ }
1534
+ enumeratedDeviceCacheRefreshInProgress = true;
1535
+ }
1536
+
1537
+ Runnable refreshTask = () -> {
1538
+ try {
1539
+ getAvailableDevicesStatic(context);
1540
+ } finally {
1541
+ synchronized (enumeratedDeviceCacheLock) {
1542
+ enumeratedDeviceCacheRefreshInProgress = false;
1543
+ }
1544
+ }
1545
+ };
1546
+
1547
+ if (cameraExecutor != null && !cameraExecutor.isShutdown()) {
1548
+ try {
1549
+ cameraExecutor.execute(refreshTask);
1550
+ return;
1551
+ } catch (RejectedExecutionException e) {
1552
+ Log.w(TAG, "requestEnumeratedDeviceCacheRefresh: cameraExecutor rejected refresh task", e);
1553
+ }
1554
+ }
1555
+
1556
+ try {
1557
+ Thread refreshThread = new Thread(refreshTask, "CameraPreview-DeviceCacheRefresh");
1558
+ refreshThread.setDaemon(true);
1559
+ refreshThread.start();
1560
+ } catch (RuntimeException e) {
1561
+ synchronized (enumeratedDeviceCacheLock) {
1562
+ enumeratedDeviceCacheRefreshInProgress = false;
1563
+ }
1564
+ throw e;
1189
1565
  }
1190
- return builder.build();
1191
1566
  }
1192
1567
 
1193
1568
  private static boolean isBackCamera(androidx.camera.core.CameraInfo cameraInfo) {
@@ -2273,6 +2648,7 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
2273
2648
  }
2274
2649
 
2275
2650
  Log.d(TAG, "=== Enumeration Complete: " + devices.size() + " cameras ===");
2651
+ updateEnumeratedDeviceCache(devices);
2276
2652
  return devices;
2277
2653
  } catch (Exception e) {
2278
2654
  Log.e(TAG, "getAvailableDevicesStatic: Error getting devices", e);
@@ -2280,6 +2656,14 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
2280
2656
  }
2281
2657
  }
2282
2658
 
2659
+ private static void updateEnumeratedDeviceCache(List<app.capgo.capacitor.camera.preview.model.CameraDevice> devices) {
2660
+ Map<String, app.capgo.capacitor.camera.preview.model.CameraDevice> newCache = new ConcurrentHashMap<>();
2661
+ for (app.capgo.capacitor.camera.preview.model.CameraDevice device : devices) {
2662
+ newCache.put(device.getDeviceId(), device);
2663
+ }
2664
+ enumeratedDeviceCache = newCache;
2665
+ }
2666
+
2283
2667
  public static ZoomFactors getZoomFactorsStatic() {
2284
2668
  try {
2285
2669
  // For static method, return default zoom factors
@@ -2357,6 +2741,9 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
2357
2741
  }
2358
2742
 
2359
2743
  camera.getCameraControl().setZoomRatio(zoomRatio);
2744
+ if (sessionConfig != null) {
2745
+ sessionConfig.setTargetZoom(zoomRatio);
2746
+ }
2360
2747
  // Note: autofocus is intentionally not triggered on zoom because it's done by CameraX
2361
2748
  } catch (Exception e) {
2362
2749
  Log.e(TAG, "Failed to set zoom: " + e.getMessage());
@@ -2880,53 +3267,52 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
2880
3267
 
2881
3268
  mainExecutor.execute(() -> {
2882
3269
  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;
3270
+ CameraSessionConfiguration previousConfig = sessionConfig;
3271
+ CameraInfo targetCameraInfo = findAvailableCameraInfoById(deviceId);
3272
+ String fallbackPosition = resolveFallbackPositionForDeviceId(deviceId);
3273
+ String position = fallbackPosition != null ? fallbackPosition : previousConfig.getPosition();
3274
+ if (targetCameraInfo != null) {
3275
+ position = isBackCamera(targetCameraInfo) ? "rear" : "front";
3276
+ } else if (previousConfig.isPhysicalDeviceSelectionEnabled()) {
3277
+ PhysicalCameraBindingTarget physicalTarget = findPhysicalCameraBindingTarget(deviceId);
3278
+ if (physicalTarget != null) {
3279
+ position = physicalTarget.requiredFacing == CameraSelector.LENS_FACING_FRONT ? "front" : "rear";
3280
+ } else if (fallbackPosition == null) {
3281
+ Log.e(TAG, "switchToDevice: Could not resolve deviceId: " + deviceId);
3282
+ return;
2892
3283
  }
3284
+ } else if (fallbackPosition == null) {
3285
+ Log.e(TAG, "switchToDevice: Could not find any CameraInfo matching deviceId: " + deviceId);
3286
+ return;
2893
3287
  }
2894
3288
 
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);
3289
+ CameraSessionConfiguration updatedConfig = new CameraSessionConfiguration(
3290
+ deviceId,
3291
+ position,
3292
+ previousConfig.getX(),
3293
+ previousConfig.getY(),
3294
+ previousConfig.getWidth(),
3295
+ previousConfig.getHeight(),
3296
+ previousConfig.getPaddingBottom(),
3297
+ previousConfig.getToBack(),
3298
+ previousConfig.getStoreToFile(),
3299
+ previousConfig.getEnableOpacity(),
3300
+ previousConfig.getDisableExifHeaderStripping(),
3301
+ previousConfig.getDisableAudio(),
3302
+ previousConfig.getZoomFactor(),
3303
+ previousConfig.getAspectRatio(),
3304
+ previousConfig.getAspectMode(),
3305
+ previousConfig.getGridMode(),
3306
+ previousConfig.getDisableFocusIndicator(),
3307
+ previousConfig.isVideoModeEnabled(),
3308
+ previousConfig.getVideoQuality()
3309
+ );
3310
+ copyMutableSessionConfigState(previousConfig, updatedConfig);
3311
+ updatedConfig.setTargetZoom(1.0f);
3312
+ sessionConfig = updatedConfig;
2924
3313
 
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
- }
3314
+ Log.d(TAG, "switchToDevice: Updated sessionConfig with deviceId: " + deviceId);
3315
+ bindCameraUseCases();
2930
3316
  } catch (Exception e) {
2931
3317
  Log.e(TAG, "switchToDevice: Error switching camera", e);
2932
3318
  }
@@ -2944,38 +3330,39 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
2944
3330
  boolean wasCentered = sessionConfig.isCentered();
2945
3331
  Log.d(TAG, "flipCamera: Switching from " + currentPosition + " to " + newPosition);
2946
3332
 
3333
+ CameraSessionConfiguration previousConfig = sessionConfig;
2947
3334
  sessionConfig = new CameraSessionConfiguration(
2948
3335
  null, // deviceId - clear device ID to force position-based selection
2949
3336
  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
3337
+ previousConfig.getX(), // x
3338
+ previousConfig.getY(), // y
3339
+ previousConfig.getWidth(), // width
3340
+ previousConfig.getHeight(), // height
3341
+ previousConfig.getPaddingBottom(), // paddingBottom
3342
+ previousConfig.isToBack(), // toBack
3343
+ previousConfig.isStoreToFile(), // storeToFile
3344
+ previousConfig.isEnableOpacity(), // enableOpacity
3345
+ previousConfig.isDisableExifHeaderStripping(), // disableExifHeaderStripping
3346
+ previousConfig.isDisableAudio(), // disableAudio
3347
+ previousConfig.getZoomFactor(), // zoomFactor
3348
+ previousConfig.getAspectRatio(), // aspectRatio
3349
+ previousConfig.getAspectMode(), // aspectMode
3350
+ previousConfig.getGridMode(), // gridMode
3351
+ previousConfig.getDisableFocusIndicator(), // disableFocusIndicator
3352
+ previousConfig.isVideoModeEnabled(), // enableVideoMode
3353
+ previousConfig.getVideoQuality() // videoQuality
2967
3354
  );
2968
-
3355
+ copyMutableSessionConfigState(previousConfig, sessionConfig);
3356
+ sessionConfig.setTargetZoom(1.0f);
2969
3357
  sessionConfig.setCentered(wasCentered);
2970
3358
 
2971
- // Clear current device ID to force position-based selection
3359
+ // Clear current device IDs to force position-based selection
2972
3360
  currentDeviceId = null;
3361
+ currentPhysicalDeviceId = null;
3362
+ currentLogicalDeviceId = null;
2973
3363
 
2974
- // Camera operations must run on main thread
2975
- cameraExecutor.execute(() -> {
2976
- currentCameraSelector = buildCameraSelector();
2977
- bindCameraUseCases();
2978
- });
3364
+ // Rebind camera with the new position
3365
+ bindCameraUseCases();
2979
3366
  }
2980
3367
 
2981
3368
  public void setOpacity(float opacity) {
@@ -3059,32 +3446,34 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
3059
3446
  return;
3060
3447
  }
3061
3448
 
3062
- String currentGridMode = sessionConfig.getGridMode();
3449
+ CameraSessionConfiguration previousConfig = sessionConfig;
3450
+ String currentGridMode = previousConfig.getGridMode();
3063
3451
  Log.d(TAG, "Changing aspect ratio from " + currentAspectRatio + " to " + aspectRatio);
3064
3452
  Log.d(TAG, "Auto-centering will be applied (matching iOS behavior)");
3065
3453
 
3066
3454
  // Match iOS behavior: when aspect ratio changes, always auto-center
3067
3455
  sessionConfig = new CameraSessionConfiguration(
3068
- sessionConfig.getDeviceId(),
3069
- sessionConfig.getPosition(),
3456
+ previousConfig.getDeviceId(),
3457
+ previousConfig.getPosition(),
3070
3458
  -1, // Force auto-center X (iOS: self.posX = -1)
3071
3459
  -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(),
3460
+ previousConfig.getWidth(),
3461
+ previousConfig.getHeight(),
3462
+ previousConfig.getPaddingBottom(),
3463
+ previousConfig.getToBack(),
3464
+ previousConfig.getStoreToFile(),
3465
+ previousConfig.getEnableOpacity(),
3466
+ previousConfig.getDisableExifHeaderStripping(),
3467
+ previousConfig.getDisableAudio(),
3468
+ previousConfig.getZoomFactor(),
3081
3469
  aspectRatio,
3082
- sessionConfig.getAspectMode(),
3470
+ previousConfig.getAspectMode(),
3083
3471
  currentGridMode,
3084
- sessionConfig.getDisableFocusIndicator(),
3085
- sessionConfig.isVideoModeEnabled(),
3086
- sessionConfig.getVideoQuality()
3472
+ previousConfig.getDisableFocusIndicator(),
3473
+ previousConfig.isVideoModeEnabled(),
3474
+ previousConfig.getVideoQuality()
3087
3475
  );
3476
+ copyMutableSessionConfigState(previousConfig, sessionConfig);
3088
3477
  sessionConfig.setCentered(true);
3089
3478
 
3090
3479
  // Update layout and rebind camera with new aspect ratio
@@ -3135,32 +3524,34 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
3135
3524
  return;
3136
3525
  }
3137
3526
 
3138
- String currentGridMode = sessionConfig.getGridMode();
3527
+ CameraSessionConfiguration previousConfig = sessionConfig;
3528
+ String currentGridMode = previousConfig.getGridMode();
3139
3529
  Log.d(TAG, "Forcing aspect ratio recalculation for: " + aspectRatio);
3140
3530
  Log.d(TAG, "Auto-centering will be applied (matching iOS behavior)");
3141
3531
 
3142
3532
  // Match iOS behavior: when aspect ratio changes, always auto-center
3143
3533
  sessionConfig = new CameraSessionConfiguration(
3144
- sessionConfig.getDeviceId(),
3145
- sessionConfig.getPosition(),
3534
+ previousConfig.getDeviceId(),
3535
+ previousConfig.getPosition(),
3146
3536
  -1, // Force auto-center X (iOS: self.posX = -1)
3147
3537
  -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(),
3538
+ previousConfig.getWidth(),
3539
+ previousConfig.getHeight(),
3540
+ previousConfig.getPaddingBottom(),
3541
+ previousConfig.getToBack(),
3542
+ previousConfig.getStoreToFile(),
3543
+ previousConfig.getEnableOpacity(),
3544
+ previousConfig.getDisableExifHeaderStripping(),
3545
+ previousConfig.getDisableAudio(),
3546
+ previousConfig.getZoomFactor(),
3157
3547
  aspectRatio,
3158
- sessionConfig.getAspectMode(),
3548
+ previousConfig.getAspectMode(),
3159
3549
  currentGridMode,
3160
- sessionConfig.getDisableFocusIndicator(),
3161
- sessionConfig.isVideoModeEnabled(),
3162
- sessionConfig.getVideoQuality()
3550
+ previousConfig.getDisableFocusIndicator(),
3551
+ previousConfig.isVideoModeEnabled(),
3552
+ previousConfig.getVideoQuality()
3163
3553
  );
3554
+ copyMutableSessionConfigState(previousConfig, sessionConfig);
3164
3555
  sessionConfig.setCentered(true);
3165
3556
 
3166
3557
  // Update layout and rebind camera with new aspect ratio
@@ -3203,27 +3594,29 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
3203
3594
  public void setGridMode(String gridMode) {
3204
3595
  if (sessionConfig != null) {
3205
3596
  Log.d(TAG, "setGridMode: Changing grid mode to: " + gridMode);
3597
+ CameraSessionConfiguration previousConfig = sessionConfig;
3206
3598
  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(),
3599
+ previousConfig.getDeviceId(),
3600
+ previousConfig.getPosition(),
3601
+ previousConfig.getX(),
3602
+ previousConfig.getY(),
3603
+ previousConfig.getWidth(),
3604
+ previousConfig.getHeight(),
3605
+ previousConfig.getPaddingBottom(),
3606
+ previousConfig.getToBack(),
3607
+ previousConfig.getStoreToFile(),
3608
+ previousConfig.getEnableOpacity(),
3609
+ previousConfig.getDisableExifHeaderStripping(),
3610
+ previousConfig.getDisableAudio(),
3611
+ previousConfig.getZoomFactor(),
3612
+ previousConfig.getAspectRatio(),
3613
+ previousConfig.getAspectMode(),
3222
3614
  gridMode,
3223
- sessionConfig.getDisableFocusIndicator(),
3224
- sessionConfig.isVideoModeEnabled(),
3225
- sessionConfig.getVideoQuality()
3615
+ previousConfig.getDisableFocusIndicator(),
3616
+ previousConfig.isVideoModeEnabled(),
3617
+ previousConfig.getVideoQuality()
3226
3618
  );
3619
+ copyMutableSessionConfigState(previousConfig, sessionConfig);
3227
3620
 
3228
3621
  // Update the grid overlay immediately
3229
3622
  if (gridOverlayView != null) {
@@ -3543,27 +3936,29 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
3543
3936
  );
3544
3937
  }
3545
3938
 
3939
+ CameraSessionConfiguration previousConfig = sessionConfig;
3546
3940
  sessionConfig = new CameraSessionConfiguration(
3547
- sessionConfig.getDeviceId(),
3548
- sessionConfig.getPosition(),
3941
+ previousConfig.getDeviceId(),
3942
+ previousConfig.getPosition(),
3549
3943
  params.leftMargin,
3550
3944
  params.topMargin,
3551
3945
  params.width,
3552
3946
  params.height,
3553
- sessionConfig.getPaddingBottom(),
3554
- sessionConfig.getToBack(),
3555
- sessionConfig.getStoreToFile(),
3556
- sessionConfig.getEnableOpacity(),
3557
- sessionConfig.getDisableExifHeaderStripping(),
3558
- sessionConfig.getDisableAudio(),
3559
- sessionConfig.getZoomFactor(),
3947
+ previousConfig.getPaddingBottom(),
3948
+ previousConfig.getToBack(),
3949
+ previousConfig.getStoreToFile(),
3950
+ previousConfig.getEnableOpacity(),
3951
+ previousConfig.getDisableExifHeaderStripping(),
3952
+ previousConfig.getDisableAudio(),
3953
+ previousConfig.getZoomFactor(),
3560
3954
  calculatedAspectRatio,
3561
- sessionConfig.getAspectMode(),
3562
- sessionConfig.getGridMode(),
3563
- sessionConfig.getDisableFocusIndicator(),
3564
- sessionConfig.isVideoModeEnabled(),
3565
- sessionConfig.getVideoQuality()
3955
+ previousConfig.getAspectMode(),
3956
+ previousConfig.getGridMode(),
3957
+ previousConfig.getDisableFocusIndicator(),
3958
+ previousConfig.isVideoModeEnabled(),
3959
+ previousConfig.getVideoQuality()
3566
3960
  );
3961
+ copyMutableSessionConfigState(previousConfig, sessionConfig);
3567
3962
 
3568
3963
  // If aspect ratio changed due to size update, rebind camera
3569
3964
  if (isRunning && !Objects.equals(currentAspectRatio, calculatedAspectRatio)) {