@capgo/camera-preview 8.11.9 → 8.11.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +54 -1
- package/android/build.gradle +0 -1
- package/android/gradle/wrapper/gradle-wrapper.properties +1 -1
- package/android/src/main/java/app/capgo/capacitor/camera/preview/CameraPreview.java +111 -126
- package/android/src/main/java/app/capgo/capacitor/camera/preview/CameraXView.java +464 -163
- package/android/src/main/java/app/capgo/capacitor/camera/preview/GridOverlayView.java +2 -2
- package/dist/docs.json +97 -0
- package/dist/esm/definitions.d.ts +31 -0
- package/dist/esm/definitions.js.map +1 -1
- package/dist/plugin.cjs.js.map +1 -1
- package/dist/plugin.js.map +1 -1
- package/ios/Sources/CapgoCameraPreviewPlugin/AspectRatioLayout.swift +107 -0
- package/ios/Sources/CapgoCameraPreviewPlugin/CameraController.swift +269 -7
- package/ios/Sources/CapgoCameraPreviewPlugin/Plugin.swift +175 -55
- package/ios/Tests/CameraPreviewPluginTests/CameraPreviewPluginTests.swift +150 -9
- package/package.json +3 -2
|
@@ -66,6 +66,8 @@ import androidx.camera.core.MirrorMode;
|
|
|
66
66
|
import androidx.camera.core.Preview;
|
|
67
67
|
import androidx.camera.core.ResolutionInfo;
|
|
68
68
|
import androidx.camera.core.TorchState;
|
|
69
|
+
import androidx.camera.core.UseCaseGroup;
|
|
70
|
+
import androidx.camera.core.ViewPort;
|
|
69
71
|
import androidx.camera.core.ZoomState;
|
|
70
72
|
import androidx.camera.core.resolutionselector.AspectRatioStrategy;
|
|
71
73
|
import androidx.camera.core.resolutionselector.ResolutionSelector;
|
|
@@ -125,6 +127,7 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
125
127
|
|
|
126
128
|
private static final String TAG = "CameraPreview CameraXView";
|
|
127
129
|
private static final String FOCUS_INDICATOR_TAG = "cpcp_focus_indicator";
|
|
130
|
+
private static final Size DEFAULT_MAX_CAPTURE_RESOLUTION = new Size(1920, 1080);
|
|
128
131
|
|
|
129
132
|
public interface CameraXViewListener {
|
|
130
133
|
void onPictureTaken(String base64, JSONObject exif);
|
|
@@ -169,6 +172,7 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
169
172
|
private String currentPhysicalDeviceId;
|
|
170
173
|
private String currentLogicalDeviceId;
|
|
171
174
|
private int currentFlashMode = ImageCapture.FLASH_MODE_OFF;
|
|
175
|
+
private boolean torchRequested = false;
|
|
172
176
|
private CameraSessionConfiguration sessionConfig;
|
|
173
177
|
private CameraXViewListener listener;
|
|
174
178
|
private final Context context;
|
|
@@ -186,6 +190,10 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
186
190
|
private static volatile boolean enumeratedDeviceCacheRefreshInProgress = false;
|
|
187
191
|
private boolean isRunning = false;
|
|
188
192
|
private Size currentPreviewResolution = null;
|
|
193
|
+
private volatile boolean viewportCropEnabled = false;
|
|
194
|
+
private volatile boolean pendingViewportRebind = false;
|
|
195
|
+
private volatile Size viewportBoundSize = null;
|
|
196
|
+
private View.OnLayoutChangeListener viewportRebindListener = null;
|
|
189
197
|
private ListenableFuture<FocusMeteringResult> currentFocusFuture = null; // Track current focus operation
|
|
190
198
|
private Integer configuredVideoFrameRate = null;
|
|
191
199
|
private Range<Integer> configuredVideoFrameRateRange = null;
|
|
@@ -196,6 +204,7 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
196
204
|
// Capture/stop coordination
|
|
197
205
|
private final Object captureLock = new Object();
|
|
198
206
|
private volatile boolean isCapturingPhoto = false;
|
|
207
|
+
private volatile boolean viewportRebindInFlight = false;
|
|
199
208
|
private volatile boolean stopRequested = false;
|
|
200
209
|
private volatile boolean previewDetachedOnDeferredStop = false;
|
|
201
210
|
private volatile boolean isBarcodeScannerActive = false;
|
|
@@ -619,6 +628,10 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
619
628
|
|
|
620
629
|
private void performImmediateStop() {
|
|
621
630
|
isRunning = false;
|
|
631
|
+
rejectPendingFrameRateBindOnStop();
|
|
632
|
+
viewportCropEnabled = false;
|
|
633
|
+
pendingViewportRebind = false;
|
|
634
|
+
viewportBoundSize = null;
|
|
622
635
|
currentDeviceId = null;
|
|
623
636
|
currentPhysicalDeviceId = null;
|
|
624
637
|
currentLogicalDeviceId = null;
|
|
@@ -683,46 +696,43 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
683
696
|
|
|
684
697
|
private void setupCamera() {
|
|
685
698
|
ListenableFuture<ProcessCameraProvider> cameraProviderFuture = ProcessCameraProvider.getInstance(context);
|
|
686
|
-
cameraProviderFuture.addListener(
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
if (
|
|
690
|
-
|
|
691
|
-
listener.onCameraStartError(this, "Camera binding cancelled: lifecycle destroyed (before provider)");
|
|
692
|
-
}
|
|
693
|
-
return;
|
|
694
|
-
}
|
|
695
|
-
if (stopRequested) {
|
|
696
|
-
if (listener != null) {
|
|
697
|
-
listener.onCameraStartError(this, "Camera binding cancelled: stop requested (before provider)");
|
|
698
|
-
}
|
|
699
|
-
return;
|
|
699
|
+
cameraProviderFuture.addListener(() -> {
|
|
700
|
+
try {
|
|
701
|
+
if (lifecycleRegistry.getCurrentState() == Lifecycle.State.DESTROYED) {
|
|
702
|
+
if (listener != null) {
|
|
703
|
+
listener.onCameraStartError(this, "Camera binding cancelled: lifecycle destroyed (before provider)");
|
|
700
704
|
}
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
return;
|
|
705
|
+
return;
|
|
706
|
+
}
|
|
707
|
+
if (stopRequested) {
|
|
708
|
+
if (listener != null) {
|
|
709
|
+
listener.onCameraStartError(this, "Camera binding cancelled: stop requested (before provider)");
|
|
707
710
|
}
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
711
|
+
return;
|
|
712
|
+
}
|
|
713
|
+
cameraProvider = cameraProviderFuture.get();
|
|
714
|
+
if (lifecycleRegistry.getCurrentState() == Lifecycle.State.DESTROYED) {
|
|
715
|
+
if (listener != null) {
|
|
716
|
+
listener.onCameraStartError(this, "Camera binding cancelled: lifecycle destroyed (after provider)");
|
|
713
717
|
}
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
// Restore webView background on error
|
|
718
|
-
restoreWebViewBackground();
|
|
718
|
+
return;
|
|
719
|
+
}
|
|
720
|
+
if (stopRequested) {
|
|
719
721
|
if (listener != null) {
|
|
720
|
-
listener.onCameraStartError(this, "
|
|
722
|
+
listener.onCameraStartError(this, "Camera binding cancelled: stop requested (after provider)");
|
|
721
723
|
}
|
|
724
|
+
return;
|
|
722
725
|
}
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
+
setupPreviewView();
|
|
727
|
+
bindCameraUseCases();
|
|
728
|
+
} catch (Exception e) {
|
|
729
|
+
// Restore webView background on error
|
|
730
|
+
restoreWebViewBackground();
|
|
731
|
+
if (listener != null) {
|
|
732
|
+
listener.onCameraStartError(this, "Error initializing camera: " + e.getMessage());
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
}, mainExecutor);
|
|
726
736
|
}
|
|
727
737
|
|
|
728
738
|
private void setupPreviewView() {
|
|
@@ -772,13 +782,11 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
772
782
|
new FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT)
|
|
773
783
|
);
|
|
774
784
|
|
|
775
|
-
previewView
|
|
776
|
-
.
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
}
|
|
781
|
-
});
|
|
785
|
+
previewView.getPreviewStreamState().observe(this, (streamState) -> {
|
|
786
|
+
if (sessionConfig != null && sessionConfig.isToBack() && streamState == PreviewView.StreamState.STREAMING) {
|
|
787
|
+
notifyCameraStartedIfNeeded("streaming");
|
|
788
|
+
}
|
|
789
|
+
});
|
|
782
790
|
|
|
783
791
|
// Create and setup the grid overlay
|
|
784
792
|
gridOverlayView = new GridOverlayView(context);
|
|
@@ -801,6 +809,18 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
801
809
|
if (left != oldLeft || top != oldTop || right != oldRight || bottom != oldBottom) {
|
|
802
810
|
Log.d(TAG, "PreviewView layout changed, updating grid bounds");
|
|
803
811
|
updateGridOverlayBounds();
|
|
812
|
+
if (
|
|
813
|
+
isRunning &&
|
|
814
|
+
viewportCropEnabled &&
|
|
815
|
+
viewportBoundSize != null &&
|
|
816
|
+
(v.getWidth() != viewportBoundSize.getWidth() || v.getHeight() != viewportBoundSize.getHeight())
|
|
817
|
+
) {
|
|
818
|
+
pendingViewportRebind = true;
|
|
819
|
+
if (!shouldDeferViewportRebind()) {
|
|
820
|
+
pendingViewportRebind = false;
|
|
821
|
+
bindCameraUseCases();
|
|
822
|
+
}
|
|
823
|
+
}
|
|
804
824
|
}
|
|
805
825
|
});
|
|
806
826
|
|
|
@@ -957,7 +977,7 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
957
977
|
Log.d(TAG, "Device orientation: " + (isPortrait ? "PORTRAIT" : "LANDSCAPE"));
|
|
958
978
|
|
|
959
979
|
// iOS: let ratio = !isPortrait ? ratioParts[0] / ratioParts[1] : ratioParts[1] / ratioParts[0]
|
|
960
|
-
double ratio = !isPortrait ?
|
|
980
|
+
double ratio = !isPortrait ? ratioWidth / ratioHeight : ratioHeight / ratioWidth;
|
|
961
981
|
|
|
962
982
|
Log.d(TAG, "Computed ratio: " + ratio + " (iOS formula: " + (!isPortrait ? "width/height" : "height/width") + ")");
|
|
963
983
|
|
|
@@ -1032,6 +1052,7 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
1032
1052
|
}
|
|
1033
1053
|
previewContainer = null;
|
|
1034
1054
|
}
|
|
1055
|
+
clearViewportRebindListener();
|
|
1035
1056
|
if (previewView != null) {
|
|
1036
1057
|
previewView = null;
|
|
1037
1058
|
}
|
|
@@ -1050,7 +1071,14 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
1050
1071
|
private void bindCameraUseCases() {
|
|
1051
1072
|
if (cameraProvider == null) return;
|
|
1052
1073
|
mainExecutor.execute(() -> {
|
|
1074
|
+
boolean acquiredRebindGuard = false;
|
|
1053
1075
|
try {
|
|
1076
|
+
if (previewView != null && (previewView.getWidth() <= 0 || previewView.getHeight() <= 0) && !isRunning) {
|
|
1077
|
+
pendingViewportRebind = true;
|
|
1078
|
+
scheduleViewportRebindWhenLayoutReady();
|
|
1079
|
+
return;
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1054
1082
|
Log.d(
|
|
1055
1083
|
TAG,
|
|
1056
1084
|
"Building camera selector with deviceId: " +
|
|
@@ -1061,37 +1089,22 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
1061
1089
|
CameraBindingPlan bindingPlan = buildCameraBindingPlan(sessionConfig);
|
|
1062
1090
|
currentCameraSelector = bindingPlan.selector;
|
|
1063
1091
|
|
|
1064
|
-
ResolutionSelector
|
|
1065
|
-
|
|
1066
|
-
);
|
|
1092
|
+
ResolutionSelector previewResolutionSelector = buildPreviewResolutionSelector();
|
|
1093
|
+
ResolutionSelector imageCaptureResolutionSelector = buildImageCaptureResolutionSelector();
|
|
1067
1094
|
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
} else {
|
|
1073
|
-
// "4:3"
|
|
1074
|
-
aspectRatio = AspectRatio.RATIO_4_3;
|
|
1075
|
-
}
|
|
1076
|
-
resolutionSelectorBuilder.setAspectRatioStrategy(
|
|
1077
|
-
new AspectRatioStrategy(aspectRatio, AspectRatioStrategy.FALLBACK_RULE_AUTO)
|
|
1078
|
-
);
|
|
1079
|
-
}
|
|
1080
|
-
|
|
1081
|
-
ResolutionSelector resolutionSelector = resolutionSelectorBuilder.build();
|
|
1082
|
-
|
|
1083
|
-
int rotation = previewView != null && previewView.getDisplay() != null
|
|
1084
|
-
? previewView.getDisplay().getRotation()
|
|
1085
|
-
: android.view.Surface.ROTATION_0;
|
|
1095
|
+
int rotation =
|
|
1096
|
+
previewView != null && previewView.getDisplay() != null
|
|
1097
|
+
? previewView.getDisplay().getRotation()
|
|
1098
|
+
: android.view.Surface.ROTATION_0;
|
|
1086
1099
|
|
|
1087
1100
|
Preview.Builder previewBuilder = new Preview.Builder()
|
|
1088
|
-
.setResolutionSelector(
|
|
1101
|
+
.setResolutionSelector(previewResolutionSelector)
|
|
1089
1102
|
.setTargetRotation(rotation);
|
|
1090
1103
|
previewBuilder = applyTargetFpsToPreviewBuilder(previewBuilder);
|
|
1091
1104
|
Preview preview = previewBuilder.build();
|
|
1092
1105
|
// Keep reference to preview use case for later re-binding (e.g., when enabling video)
|
|
1093
1106
|
imageCapture = new ImageCapture.Builder()
|
|
1094
|
-
.setResolutionSelector(
|
|
1107
|
+
.setResolutionSelector(imageCaptureResolutionSelector)
|
|
1095
1108
|
.setCaptureMode(ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY)
|
|
1096
1109
|
.setFlashMode(currentFlashMode)
|
|
1097
1110
|
.setTargetRotation(rotation)
|
|
@@ -1165,6 +1178,24 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
1165
1178
|
videoCapture = videoCaptureBuilder.build();
|
|
1166
1179
|
}
|
|
1167
1180
|
|
|
1181
|
+
Integer exposureCompensationIndexToRestore = null;
|
|
1182
|
+
if (camera != null) {
|
|
1183
|
+
try {
|
|
1184
|
+
exposureCompensationIndexToRestore = camera.getCameraInfo().getExposureState().getExposureCompensationIndex();
|
|
1185
|
+
} catch (Exception e) {
|
|
1186
|
+
Log.w(TAG, "bindCameraUseCases: Failed to read exposure compensation before rebind", e);
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
synchronized (captureLock) {
|
|
1191
|
+
if (shouldDeferViewportRebind()) {
|
|
1192
|
+
pendingViewportRebind = true;
|
|
1193
|
+
return;
|
|
1194
|
+
}
|
|
1195
|
+
viewportRebindInFlight = true;
|
|
1196
|
+
acquiredRebindGuard = true;
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1168
1199
|
// Unbind any existing use cases and bind new ones
|
|
1169
1200
|
cameraProvider.unbindAll();
|
|
1170
1201
|
|
|
@@ -1189,11 +1220,11 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
1189
1220
|
|
|
1190
1221
|
bindingPlan = buildLogicalFallbackPlan(sessionConfig, bindingPlan);
|
|
1191
1222
|
currentCameraSelector = bindingPlan.selector;
|
|
1223
|
+
cameraProvider.unbindAll();
|
|
1192
1224
|
bindConfiguredUseCases(bindingPlan, preview);
|
|
1193
1225
|
}
|
|
1194
1226
|
|
|
1195
|
-
|
|
1196
|
-
reapplyCameraControlModes();
|
|
1227
|
+
restoreCameraControlStateAfterRebind(exposureCompensationIndexToRestore);
|
|
1197
1228
|
|
|
1198
1229
|
// Log details about the active camera
|
|
1199
1230
|
Log.d(TAG, "Use cases bound. Inspecting active camera and use cases.");
|
|
@@ -1277,11 +1308,12 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
1277
1308
|
previewView.setScaleType("cover".equals(aspectMode) ? PreviewView.ScaleType.FILL_CENTER : PreviewView.ScaleType.FIT_CENTER);
|
|
1278
1309
|
|
|
1279
1310
|
// Set initial zoom if specified, prioritizing targetZoom over default zoomFactor
|
|
1280
|
-
float initialZoom =
|
|
1281
|
-
bindingPlan.fallbackZoom != 1.0f &&
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1311
|
+
float initialZoom =
|
|
1312
|
+
!bindingPlan.usesPhysicalSelection && bindingPlan.fallbackZoom != 1.0f && sessionConfig.getTargetZoom() == 1.0f
|
|
1313
|
+
? bindingPlan.fallbackZoom
|
|
1314
|
+
: sessionConfig.getTargetZoom() != 1.0f
|
|
1315
|
+
? sessionConfig.getTargetZoom()
|
|
1316
|
+
: sessionConfig.getZoomFactor();
|
|
1285
1317
|
if (initialZoom != 1.0f) {
|
|
1286
1318
|
Log.d(TAG, "Applying initial zoom of " + initialZoom);
|
|
1287
1319
|
|
|
@@ -1319,28 +1351,20 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
1319
1351
|
if (streamState == PreviewView.StreamState.STREAMING) {
|
|
1320
1352
|
notifyCameraStartedIfNeeded("already-streaming");
|
|
1321
1353
|
} else if (previewContainer != null) {
|
|
1322
|
-
previewContainer.postDelayed(
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
? previewView.getPreviewStreamState().getValue()
|
|
1337
|
-
: null;
|
|
1338
|
-
if (!cameraStartedCallbackSent && latestState == PreviewView.StreamState.STREAMING) {
|
|
1339
|
-
notifyCameraStartedIfNeeded("fallback-streaming");
|
|
1340
|
-
}
|
|
1341
|
-
},
|
|
1342
|
-
1500
|
|
1343
|
-
);
|
|
1354
|
+
previewContainer.postDelayed(() -> {
|
|
1355
|
+
PreviewView.StreamState latestState =
|
|
1356
|
+
previewView != null ? previewView.getPreviewStreamState().getValue() : null;
|
|
1357
|
+
if (latestState == PreviewView.StreamState.STREAMING) {
|
|
1358
|
+
notifyCameraStartedIfNeeded("watchdog-streaming");
|
|
1359
|
+
}
|
|
1360
|
+
}, 300);
|
|
1361
|
+
previewContainer.postDelayed(() -> {
|
|
1362
|
+
PreviewView.StreamState latestState =
|
|
1363
|
+
previewView != null ? previewView.getPreviewStreamState().getValue() : null;
|
|
1364
|
+
if (!cameraStartedCallbackSent && latestState == PreviewView.StreamState.STREAMING) {
|
|
1365
|
+
notifyCameraStartedIfNeeded("fallback-streaming");
|
|
1366
|
+
}
|
|
1367
|
+
}, 1500);
|
|
1344
1368
|
}
|
|
1345
1369
|
} else {
|
|
1346
1370
|
notifyCameraStartedIfNeeded("bound");
|
|
@@ -1351,6 +1375,13 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
1351
1375
|
restoreWebViewBackground();
|
|
1352
1376
|
completePendingFrameRateBindError("Error binding camera: " + e.getMessage());
|
|
1353
1377
|
if (listener != null) listener.onCameraStartError(this, "Error binding camera: " + e.getMessage());
|
|
1378
|
+
} finally {
|
|
1379
|
+
if (acquiredRebindGuard) {
|
|
1380
|
+
synchronized (captureLock) {
|
|
1381
|
+
viewportRebindInFlight = false;
|
|
1382
|
+
}
|
|
1383
|
+
maybePerformPendingViewportRebind();
|
|
1384
|
+
}
|
|
1354
1385
|
}
|
|
1355
1386
|
});
|
|
1356
1387
|
}
|
|
@@ -1637,13 +1668,216 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
1637
1668
|
return 1.0f;
|
|
1638
1669
|
}
|
|
1639
1670
|
|
|
1671
|
+
private ResolutionSelector buildPreviewResolutionSelector() {
|
|
1672
|
+
ResolutionSelector.Builder resolutionSelectorBuilder = new ResolutionSelector.Builder().setResolutionStrategy(
|
|
1673
|
+
ResolutionStrategy.HIGHEST_AVAILABLE_STRATEGY
|
|
1674
|
+
);
|
|
1675
|
+
applySessionAspectRatioStrategy(resolutionSelectorBuilder);
|
|
1676
|
+
return resolutionSelectorBuilder.build();
|
|
1677
|
+
}
|
|
1678
|
+
|
|
1679
|
+
private ResolutionSelector buildImageCaptureResolutionSelector() {
|
|
1680
|
+
Size targetResolution = new Size(
|
|
1681
|
+
Math.max(DEFAULT_MAX_CAPTURE_RESOLUTION.getWidth(), DEFAULT_MAX_CAPTURE_RESOLUTION.getHeight()),
|
|
1682
|
+
Math.min(DEFAULT_MAX_CAPTURE_RESOLUTION.getWidth(), DEFAULT_MAX_CAPTURE_RESOLUTION.getHeight())
|
|
1683
|
+
);
|
|
1684
|
+
int maxCaptureLongEdge = Math.max(DEFAULT_MAX_CAPTURE_RESOLUTION.getWidth(), DEFAULT_MAX_CAPTURE_RESOLUTION.getHeight());
|
|
1685
|
+
ResolutionSelector.Builder resolutionSelectorBuilder = new ResolutionSelector.Builder();
|
|
1686
|
+
resolutionSelectorBuilder.setResolutionFilter((supportedSizes, rotationDegrees) ->
|
|
1687
|
+
filterCaptureResolutionCandidates(supportedSizes, maxCaptureLongEdge)
|
|
1688
|
+
);
|
|
1689
|
+
resolutionSelectorBuilder.setResolutionStrategy(
|
|
1690
|
+
new ResolutionStrategy(targetResolution, ResolutionStrategy.FALLBACK_RULE_CLOSEST_LOWER_THEN_HIGHER)
|
|
1691
|
+
);
|
|
1692
|
+
applySessionAspectRatioStrategy(resolutionSelectorBuilder);
|
|
1693
|
+
return resolutionSelectorBuilder.build();
|
|
1694
|
+
}
|
|
1695
|
+
|
|
1696
|
+
private List<Size> filterCaptureResolutionCandidates(List<Size> supportedSizes, int maxCaptureLongEdge) {
|
|
1697
|
+
if (supportedSizes == null || supportedSizes.isEmpty()) {
|
|
1698
|
+
return supportedSizes;
|
|
1699
|
+
}
|
|
1700
|
+
|
|
1701
|
+
List<Size> cappedSizes = new ArrayList<>();
|
|
1702
|
+
for (Size size : supportedSizes) {
|
|
1703
|
+
int longEdge = Math.max(size.getWidth(), size.getHeight());
|
|
1704
|
+
if (longEdge <= maxCaptureLongEdge) {
|
|
1705
|
+
cappedSizes.add(size);
|
|
1706
|
+
}
|
|
1707
|
+
}
|
|
1708
|
+
|
|
1709
|
+
if (!cappedSizes.isEmpty()) {
|
|
1710
|
+
return cappedSizes;
|
|
1711
|
+
}
|
|
1712
|
+
|
|
1713
|
+
Size smallest = supportedSizes.get(0);
|
|
1714
|
+
int smallestLongEdge = Math.max(smallest.getWidth(), smallest.getHeight());
|
|
1715
|
+
for (Size size : supportedSizes) {
|
|
1716
|
+
int longEdge = Math.max(size.getWidth(), size.getHeight());
|
|
1717
|
+
if (longEdge < smallestLongEdge) {
|
|
1718
|
+
smallest = size;
|
|
1719
|
+
smallestLongEdge = longEdge;
|
|
1720
|
+
}
|
|
1721
|
+
}
|
|
1722
|
+
return Collections.singletonList(smallest);
|
|
1723
|
+
}
|
|
1724
|
+
|
|
1725
|
+
private void applySessionAspectRatioStrategy(ResolutionSelector.Builder resolutionSelectorBuilder) {
|
|
1726
|
+
if (sessionConfig == null || sessionConfig.getAspectRatio() == null) {
|
|
1727
|
+
return;
|
|
1728
|
+
}
|
|
1729
|
+
|
|
1730
|
+
int aspectRatio;
|
|
1731
|
+
if ("16:9".equals(sessionConfig.getAspectRatio())) {
|
|
1732
|
+
aspectRatio = AspectRatio.RATIO_16_9;
|
|
1733
|
+
} else {
|
|
1734
|
+
aspectRatio = AspectRatio.RATIO_4_3;
|
|
1735
|
+
}
|
|
1736
|
+
resolutionSelectorBuilder.setAspectRatioStrategy(new AspectRatioStrategy(aspectRatio, AspectRatioStrategy.FALLBACK_RULE_AUTO));
|
|
1737
|
+
}
|
|
1738
|
+
|
|
1739
|
+
private ViewPort buildViewPort(int rotation) {
|
|
1740
|
+
if (previewView == null || previewView.getWidth() <= 0 || previewView.getHeight() <= 0) {
|
|
1741
|
+
return null;
|
|
1742
|
+
}
|
|
1743
|
+
ViewPort viewPort = previewView.getViewPort(rotation);
|
|
1744
|
+
if (viewPort != null) {
|
|
1745
|
+
viewportBoundSize = new Size(previewView.getWidth(), previewView.getHeight());
|
|
1746
|
+
}
|
|
1747
|
+
return viewPort;
|
|
1748
|
+
}
|
|
1749
|
+
|
|
1750
|
+
private boolean isViewportCropCurrent() {
|
|
1751
|
+
return (
|
|
1752
|
+
viewportCropEnabled &&
|
|
1753
|
+
viewportBoundSize != null &&
|
|
1754
|
+
previewView != null &&
|
|
1755
|
+
previewView.getWidth() == viewportBoundSize.getWidth() &&
|
|
1756
|
+
previewView.getHeight() == viewportBoundSize.getHeight()
|
|
1757
|
+
);
|
|
1758
|
+
}
|
|
1759
|
+
|
|
1760
|
+
private boolean shouldDeferViewportRebind() {
|
|
1761
|
+
return isCapturingPhoto || currentRecording != null;
|
|
1762
|
+
}
|
|
1763
|
+
|
|
1764
|
+
private void clearViewportRebindListener() {
|
|
1765
|
+
if (previewView != null && viewportRebindListener != null) {
|
|
1766
|
+
previewView.removeOnLayoutChangeListener(viewportRebindListener);
|
|
1767
|
+
viewportRebindListener = null;
|
|
1768
|
+
}
|
|
1769
|
+
}
|
|
1770
|
+
|
|
1771
|
+
private void maybePerformPendingViewportRebind() {
|
|
1772
|
+
mainExecutor.execute(() -> {
|
|
1773
|
+
if (!pendingViewportRebind || !isRunning || shouldDeferViewportRebind()) {
|
|
1774
|
+
return;
|
|
1775
|
+
}
|
|
1776
|
+
pendingViewportRebind = false;
|
|
1777
|
+
bindCameraUseCases();
|
|
1778
|
+
});
|
|
1779
|
+
}
|
|
1780
|
+
|
|
1781
|
+
private void scheduleViewportRebindWhenLayoutReady() {
|
|
1782
|
+
if (previewView == null || !pendingViewportRebind || viewportRebindListener != null) {
|
|
1783
|
+
return;
|
|
1784
|
+
}
|
|
1785
|
+
|
|
1786
|
+
viewportRebindListener = new View.OnLayoutChangeListener() {
|
|
1787
|
+
@Override
|
|
1788
|
+
public void onLayoutChange(
|
|
1789
|
+
View v,
|
|
1790
|
+
int left,
|
|
1791
|
+
int top,
|
|
1792
|
+
int right,
|
|
1793
|
+
int bottom,
|
|
1794
|
+
int oldLeft,
|
|
1795
|
+
int oldTop,
|
|
1796
|
+
int oldRight,
|
|
1797
|
+
int oldBottom
|
|
1798
|
+
) {
|
|
1799
|
+
if (!pendingViewportRebind || previewView == null || stopRequested) {
|
|
1800
|
+
v.removeOnLayoutChangeListener(this);
|
|
1801
|
+
viewportRebindListener = null;
|
|
1802
|
+
return;
|
|
1803
|
+
}
|
|
1804
|
+
if (previewView.getWidth() > 0 && previewView.getHeight() > 0) {
|
|
1805
|
+
v.removeOnLayoutChangeListener(this);
|
|
1806
|
+
viewportRebindListener = null;
|
|
1807
|
+
if (shouldDeferViewportRebind()) {
|
|
1808
|
+
Log.d(
|
|
1809
|
+
TAG,
|
|
1810
|
+
"scheduleViewportRebindWhenLayoutReady: Layout ready but deferring ViewPort rebind (capture/recording active)"
|
|
1811
|
+
);
|
|
1812
|
+
return;
|
|
1813
|
+
}
|
|
1814
|
+
Log.d(TAG, "scheduleViewportRebindWhenLayoutReady: PreviewView layout ready, rebinding with ViewPort");
|
|
1815
|
+
bindCameraUseCases();
|
|
1816
|
+
}
|
|
1817
|
+
}
|
|
1818
|
+
};
|
|
1819
|
+
previewView.addOnLayoutChangeListener(viewportRebindListener);
|
|
1820
|
+
}
|
|
1821
|
+
|
|
1822
|
+
private boolean captureRequiresSoftwareTransform(
|
|
1823
|
+
Integer width,
|
|
1824
|
+
Integer height,
|
|
1825
|
+
boolean embedTimestamp,
|
|
1826
|
+
boolean embedLocation,
|
|
1827
|
+
boolean mirrorFrontCamera
|
|
1828
|
+
) {
|
|
1829
|
+
return width != null || height != null || embedTimestamp || embedLocation || shouldMirrorFrontCamera(mirrorFrontCamera);
|
|
1830
|
+
}
|
|
1831
|
+
|
|
1640
1832
|
private void bindConfiguredUseCases(CameraBindingPlan bindingPlan, Preview preview) {
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1833
|
+
int rotation =
|
|
1834
|
+
previewView != null && previewView.getDisplay() != null
|
|
1835
|
+
? previewView.getDisplay().getRotation()
|
|
1836
|
+
: android.view.Surface.ROTATION_0;
|
|
1837
|
+
ViewPort viewPort = buildViewPort(rotation);
|
|
1838
|
+
|
|
1839
|
+
if (viewPort != null) {
|
|
1840
|
+
UseCaseGroup.Builder groupBuilder = new UseCaseGroup.Builder()
|
|
1841
|
+
.setViewPort(viewPort)
|
|
1842
|
+
.addUseCase(preview)
|
|
1843
|
+
.addUseCase(imageCapture);
|
|
1844
|
+
if (sessionConfig.isVideoModeEnabled() && videoCapture != null) {
|
|
1845
|
+
groupBuilder.addUseCase(videoCapture);
|
|
1846
|
+
}
|
|
1847
|
+
|
|
1848
|
+
try {
|
|
1849
|
+
camera = cameraProvider.bindToLifecycle(this, bindingPlan.selector, groupBuilder.build());
|
|
1850
|
+
viewportCropEnabled = true;
|
|
1851
|
+
pendingViewportRebind = false;
|
|
1852
|
+
Log.d(TAG, "bindConfiguredUseCases: Bound use cases with shared ViewPort");
|
|
1853
|
+
} catch (Exception viewportGroupError) {
|
|
1854
|
+
Log.w(TAG, "bindConfiguredUseCases: ViewPort group binding failed; retrying without ViewPort", viewportGroupError);
|
|
1855
|
+
cameraProvider.unbindAll();
|
|
1856
|
+
viewportCropEnabled = false;
|
|
1857
|
+
viewportBoundSize = null;
|
|
1858
|
+
pendingViewportRebind = false;
|
|
1859
|
+
if (sessionConfig.isVideoModeEnabled() && videoCapture != null) {
|
|
1860
|
+
camera = cameraProvider.bindToLifecycle(this, bindingPlan.selector, preview, imageCapture, videoCapture);
|
|
1861
|
+
} else {
|
|
1862
|
+
camera = cameraProvider.bindToLifecycle(this, bindingPlan.selector, preview, imageCapture);
|
|
1863
|
+
}
|
|
1864
|
+
}
|
|
1645
1865
|
} else {
|
|
1646
|
-
|
|
1866
|
+
viewportCropEnabled = false;
|
|
1867
|
+
viewportBoundSize = null;
|
|
1868
|
+
pendingViewportRebind = true;
|
|
1869
|
+
scheduleViewportRebindWhenLayoutReady();
|
|
1870
|
+
Log.w(TAG, "bindConfiguredUseCases: ViewPort unavailable, falling back to legacy binding without viewport crop");
|
|
1871
|
+
|
|
1872
|
+
if (sessionConfig.isVideoModeEnabled() && videoCapture != null) {
|
|
1873
|
+
camera = cameraProvider.bindToLifecycle(this, bindingPlan.selector, preview, imageCapture, videoCapture);
|
|
1874
|
+
} else {
|
|
1875
|
+
camera = cameraProvider.bindToLifecycle(this, bindingPlan.selector, preview, imageCapture);
|
|
1876
|
+
}
|
|
1877
|
+
}
|
|
1878
|
+
|
|
1879
|
+
if (barcodeAnalysis != null) {
|
|
1880
|
+
cameraProvider.bindToLifecycle(this, bindingPlan.selector, barcodeAnalysis);
|
|
1647
1881
|
}
|
|
1648
1882
|
|
|
1649
1883
|
CameraInfo cameraInfo = camera.getCameraInfo();
|
|
@@ -2078,6 +2312,12 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
2078
2312
|
boolean dispatched = false;
|
|
2079
2313
|
try {
|
|
2080
2314
|
synchronized (captureLock) {
|
|
2315
|
+
if (viewportRebindInFlight) {
|
|
2316
|
+
if (listener != null) {
|
|
2317
|
+
listener.onPictureTakenError("Camera is reconfiguring, try again");
|
|
2318
|
+
}
|
|
2319
|
+
return;
|
|
2320
|
+
}
|
|
2081
2321
|
isCapturingPhoto = true;
|
|
2082
2322
|
}
|
|
2083
2323
|
|
|
@@ -2107,6 +2347,7 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
2107
2347
|
performImmediateStop();
|
|
2108
2348
|
}
|
|
2109
2349
|
}
|
|
2350
|
+
maybePerformPendingViewportRebind();
|
|
2110
2351
|
endOperation("capturePhoto");
|
|
2111
2352
|
}
|
|
2112
2353
|
|
|
@@ -2125,7 +2366,23 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
2125
2366
|
// Build EXIF JSON from captured bytes (location applied by metadata if provided)
|
|
2126
2367
|
JSONObject exifData = getExifData(exifInterface);
|
|
2127
2368
|
|
|
2128
|
-
|
|
2369
|
+
boolean requiresSoftwareTransform = captureRequiresSoftwareTransform(
|
|
2370
|
+
width,
|
|
2371
|
+
height,
|
|
2372
|
+
embedTimestamp,
|
|
2373
|
+
embedLocation,
|
|
2374
|
+
mirrorFrontCamera
|
|
2375
|
+
);
|
|
2376
|
+
|
|
2377
|
+
if (
|
|
2378
|
+
!requiresSoftwareTransform &&
|
|
2379
|
+
isViewportCropCurrent() &&
|
|
2380
|
+
quality == 95 &&
|
|
2381
|
+
exifInterface.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_UNDEFINED) ==
|
|
2382
|
+
ExifInterface.ORIENTATION_NORMAL
|
|
2383
|
+
) {
|
|
2384
|
+
Log.d(TAG, "capturePhoto: Using ViewPort fast path without software decode/crop/re-encode");
|
|
2385
|
+
} else if (width != null || height != null) {
|
|
2129
2386
|
Bitmap bitmap = BitmapFactory.decodeByteArray(originalCaptureBytes, 0, originalCaptureBytes.length);
|
|
2130
2387
|
bitmap = applyExifOrientation(bitmap, exifInterface);
|
|
2131
2388
|
bitmap = maybeMirrorFrontCameraBitmap(bitmap, mirrorFrontCamera);
|
|
@@ -2154,39 +2411,44 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
2154
2411
|
finalWidthOut = resizedBitmap.getWidth();
|
|
2155
2412
|
finalHeightOut = resizedBitmap.getHeight();
|
|
2156
2413
|
} else {
|
|
2157
|
-
// No explicit size
|
|
2414
|
+
// No explicit size: crop to preview unless ViewPort already matched FOV
|
|
2158
2415
|
Bitmap originalBitmap = BitmapFactory.decodeByteArray(originalCaptureBytes, 0, originalCaptureBytes.length);
|
|
2159
2416
|
originalBitmap = applyExifOrientation(originalBitmap, exifInterface);
|
|
2160
2417
|
originalBitmap = maybeMirrorFrontCameraBitmap(originalBitmap, mirrorFrontCamera);
|
|
2161
|
-
Bitmap
|
|
2418
|
+
Bitmap processedBitmap = isViewportCropCurrent()
|
|
2419
|
+
? originalBitmap
|
|
2420
|
+
: cropBitmapToMatchPreview(originalBitmap);
|
|
2421
|
+
if (isViewportCropCurrent()) {
|
|
2422
|
+
Log.d(TAG, "capturePhoto: Skipping software preview crop (ViewPort active)");
|
|
2423
|
+
}
|
|
2162
2424
|
if (embedTimestamp || embedLocation) {
|
|
2163
|
-
|
|
2164
|
-
|
|
2425
|
+
processedBitmap = drawTimestampAndLocationOntoBitmap(
|
|
2426
|
+
processedBitmap,
|
|
2165
2427
|
exifInterface,
|
|
2166
2428
|
embedTimestamp,
|
|
2167
2429
|
embedLocation
|
|
2168
2430
|
);
|
|
2169
2431
|
}
|
|
2170
2432
|
ByteArrayOutputStream stream = new ByteArrayOutputStream();
|
|
2171
|
-
|
|
2433
|
+
processedBitmap.compress(Bitmap.CompressFormat.JPEG, quality, stream);
|
|
2172
2434
|
bytes = stream.toByteArray();
|
|
2173
2435
|
transformedPixels = true;
|
|
2174
2436
|
// Update EXIF JSON to reflect cropped dimensions; no in-place EXIF write to bytes
|
|
2175
2437
|
try {
|
|
2176
|
-
exifData.put("PixelXDimension",
|
|
2177
|
-
exifData.put("PixelYDimension",
|
|
2178
|
-
exifData.put("ImageWidth",
|
|
2179
|
-
exifData.put("ImageLength",
|
|
2438
|
+
exifData.put("PixelXDimension", processedBitmap.getWidth());
|
|
2439
|
+
exifData.put("PixelYDimension", processedBitmap.getHeight());
|
|
2440
|
+
exifData.put("ImageWidth", processedBitmap.getWidth());
|
|
2441
|
+
exifData.put("ImageLength", processedBitmap.getHeight());
|
|
2180
2442
|
exifData.put("Orientation", Integer.toString(ExifInterface.ORIENTATION_NORMAL));
|
|
2181
2443
|
} catch (Exception ignore) {}
|
|
2182
|
-
finalWidthOut =
|
|
2183
|
-
finalHeightOut =
|
|
2444
|
+
finalWidthOut = processedBitmap.getWidth();
|
|
2445
|
+
finalHeightOut = processedBitmap.getHeight();
|
|
2184
2446
|
}
|
|
2185
2447
|
|
|
2186
2448
|
// After any transform, inject EXIF back into the in-memory JPEG bytes (no temp file)
|
|
2187
2449
|
if (transformedPixels) {
|
|
2188
|
-
Integer fW =
|
|
2189
|
-
Integer fH =
|
|
2450
|
+
Integer fW = finalWidthOut > 0 ? finalWidthOut : null;
|
|
2451
|
+
Integer fH = finalHeightOut > 0 ? finalHeightOut : null;
|
|
2190
2452
|
bytes = injectExifInMemory(bytes, originalCaptureBytes, fW, fH);
|
|
2191
2453
|
}
|
|
2192
2454
|
|
|
@@ -2205,8 +2467,8 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
2205
2467
|
if (saveToGallery) {
|
|
2206
2468
|
final byte[] finalBytes = bytes;
|
|
2207
2469
|
final ExifInterface exifForFile = exifInterface;
|
|
2208
|
-
final Integer fW =
|
|
2209
|
-
final Integer fH =
|
|
2470
|
+
final Integer fW = finalWidthOut > 0 ? finalWidthOut : null;
|
|
2471
|
+
final Integer fH = finalHeightOut > 0 ? finalHeightOut : null;
|
|
2210
2472
|
new Thread(() -> saveImageToGallery(finalBytes, exifForFile, fW, fH)).start();
|
|
2211
2473
|
}
|
|
2212
2474
|
|
|
@@ -2253,6 +2515,7 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
2253
2515
|
performImmediateStop();
|
|
2254
2516
|
}
|
|
2255
2517
|
}
|
|
2518
|
+
maybePerformPendingViewportRebind();
|
|
2256
2519
|
endOperation("capturePhoto");
|
|
2257
2520
|
}
|
|
2258
2521
|
}
|
|
@@ -2273,6 +2536,7 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
2273
2536
|
performImmediateStop();
|
|
2274
2537
|
}
|
|
2275
2538
|
}
|
|
2539
|
+
maybePerformPendingViewportRebind();
|
|
2276
2540
|
endOperation("capturePhoto");
|
|
2277
2541
|
}
|
|
2278
2542
|
}
|
|
@@ -2283,7 +2547,7 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
2283
2547
|
|
|
2284
2548
|
// Build strings (null-safe)
|
|
2285
2549
|
final String when = embedTimestamp ? buildTimestampStringFromExif(exif) : null;
|
|
2286
|
-
final String where =
|
|
2550
|
+
final String where = embedLocation ? buildLocationStringFromExif(exif) : null;
|
|
2287
2551
|
|
|
2288
2552
|
// Nothing to draw?
|
|
2289
2553
|
if ((when == null || when.isEmpty()) && (where == null || where.isEmpty())) {
|
|
@@ -2357,7 +2621,7 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
2357
2621
|
if (where != null && !where.isEmpty()) {
|
|
2358
2622
|
// If there was no timestamp drawn, we still start at top margin.
|
|
2359
2623
|
// If there was, we use the accumulated nextTop (= bottom + gap).
|
|
2360
|
-
drawPill.apply(where,
|
|
2624
|
+
drawPill.apply(where, when != null && !when.isEmpty() ? nextTop : margin);
|
|
2361
2625
|
}
|
|
2362
2626
|
|
|
2363
2627
|
return bmp;
|
|
@@ -2461,30 +2725,32 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
2461
2725
|
int originalWidth = bitmap.getWidth();
|
|
2462
2726
|
int originalHeight = bitmap.getHeight();
|
|
2463
2727
|
float originalAspectRatio = (float) originalWidth / originalHeight;
|
|
2728
|
+
int boundedMaxWidth = maxWidth != null ? Math.min(maxWidth, originalWidth) : originalWidth;
|
|
2729
|
+
int boundedMaxHeight = maxHeight != null ? Math.min(maxHeight, originalHeight) : originalHeight;
|
|
2464
2730
|
|
|
2465
2731
|
int targetWidth;
|
|
2466
2732
|
int targetHeight = originalHeight;
|
|
2467
2733
|
|
|
2468
2734
|
if (maxWidth != null && maxHeight != null) {
|
|
2469
2735
|
// Both dimensions specified - fit within both maximums
|
|
2470
|
-
float maxAspectRatio = (float)
|
|
2736
|
+
float maxAspectRatio = (float) boundedMaxWidth / boundedMaxHeight;
|
|
2471
2737
|
if (originalAspectRatio > maxAspectRatio) {
|
|
2472
2738
|
// Original is wider - fit by width
|
|
2473
|
-
targetWidth =
|
|
2474
|
-
targetHeight = (int) (
|
|
2739
|
+
targetWidth = boundedMaxWidth;
|
|
2740
|
+
targetHeight = (int) (boundedMaxWidth / originalAspectRatio);
|
|
2475
2741
|
} else {
|
|
2476
2742
|
// Original is taller - fit by height
|
|
2477
|
-
targetWidth = (int) (
|
|
2478
|
-
targetHeight =
|
|
2743
|
+
targetWidth = (int) (boundedMaxHeight * originalAspectRatio);
|
|
2744
|
+
targetHeight = boundedMaxHeight;
|
|
2479
2745
|
}
|
|
2480
2746
|
} else if (maxWidth != null) {
|
|
2481
2747
|
// Only width specified - maintain aspect ratio
|
|
2482
|
-
targetWidth =
|
|
2483
|
-
targetHeight = (int) (
|
|
2748
|
+
targetWidth = boundedMaxWidth;
|
|
2749
|
+
targetHeight = (int) (boundedMaxWidth / originalAspectRatio);
|
|
2484
2750
|
} else {
|
|
2485
2751
|
// Only height specified - maintain aspect ratio
|
|
2486
|
-
targetWidth = (int) (
|
|
2487
|
-
targetHeight =
|
|
2752
|
+
targetWidth = (int) (boundedMaxHeight * originalAspectRatio);
|
|
2753
|
+
targetHeight = boundedMaxHeight;
|
|
2488
2754
|
}
|
|
2489
2755
|
|
|
2490
2756
|
return Bitmap.createScaledBitmap(bitmap, targetWidth, targetHeight, true);
|
|
@@ -2528,9 +2794,8 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
2528
2794
|
);
|
|
2529
2795
|
org.apache.commons.imaging.formats.tiff.TiffImageMetadata exif = jpegMetadata != null ? jpegMetadata.getExif() : null;
|
|
2530
2796
|
|
|
2531
|
-
org.apache.commons.imaging.formats.tiff.write.TiffOutputSet outputSet =
|
|
2532
|
-
? exif.getOutputSet()
|
|
2533
|
-
: new org.apache.commons.imaging.formats.tiff.write.TiffOutputSet();
|
|
2797
|
+
org.apache.commons.imaging.formats.tiff.write.TiffOutputSet outputSet =
|
|
2798
|
+
exif != null ? exif.getOutputSet() : new org.apache.commons.imaging.formats.tiff.write.TiffOutputSet();
|
|
2534
2799
|
|
|
2535
2800
|
// Update orientation if requested (normalize to 1)
|
|
2536
2801
|
org.apache.commons.imaging.formats.tiff.write.TiffOutputDirectory rootDir = outputSet.getOrCreateRootDirectory();
|
|
@@ -2565,9 +2830,8 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
2565
2830
|
(org.apache.commons.imaging.formats.jpeg.JpegImageMetadata) org.apache.commons.imaging.Imaging.getMetadata(jpeg);
|
|
2566
2831
|
org.apache.commons.imaging.formats.tiff.TiffImageMetadata exif = jpegMetadata != null ? jpegMetadata.getExif() : null;
|
|
2567
2832
|
|
|
2568
|
-
org.apache.commons.imaging.formats.tiff.write.TiffOutputSet outputSet =
|
|
2569
|
-
? exif.getOutputSet()
|
|
2570
|
-
: new org.apache.commons.imaging.formats.tiff.write.TiffOutputSet();
|
|
2833
|
+
org.apache.commons.imaging.formats.tiff.write.TiffOutputSet outputSet =
|
|
2834
|
+
exif != null ? exif.getOutputSet() : new org.apache.commons.imaging.formats.tiff.write.TiffOutputSet();
|
|
2571
2835
|
|
|
2572
2836
|
org.apache.commons.imaging.formats.tiff.write.TiffOutputDirectory gpsDir = outputSet.getOrCreateGpsDirectory();
|
|
2573
2837
|
|
|
@@ -2902,8 +3166,8 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
2902
3166
|
int previewH = Math.max(1, bounds.height());
|
|
2903
3167
|
|
|
2904
3168
|
// Check if device physical orientation differs from UI orientation
|
|
2905
|
-
int rotation =
|
|
2906
|
-
boolean physicalInLandscape =
|
|
3169
|
+
int rotation = lastCaptureRotation != -1 ? lastCaptureRotation : getRotationFromAccelerometer();
|
|
3170
|
+
boolean physicalInLandscape = rotation == android.view.Surface.ROTATION_90 || rotation == android.view.Surface.ROTATION_270;
|
|
2907
3171
|
boolean previewIsPortrait = previewH > previewW;
|
|
2908
3172
|
|
|
2909
3173
|
// If physical orientation doesn't match preview orientation swap ratio
|
|
@@ -3070,7 +3334,7 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
3070
3334
|
physicalMaxZoom = zoomRange.getUpper();
|
|
3071
3335
|
}
|
|
3072
3336
|
}
|
|
3073
|
-
float physicalFocalLength =
|
|
3337
|
+
float physicalFocalLength = focalLengths != null && focalLengths.length > 0 ? focalLengths[0] : 4.25f;
|
|
3074
3338
|
String physicalLabel = "Physical " + physicalDeviceType + " (" + position + ")";
|
|
3075
3339
|
List<LensInfo> physicalLenses = new ArrayList<>();
|
|
3076
3340
|
physicalLenses.add(new LensInfo(physicalFocalLength, physicalDeviceType, 1.0f, physicalMaxZoom));
|
|
@@ -3279,32 +3543,29 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
3279
3543
|
|
|
3280
3544
|
final ListenableFuture<FocusMeteringResult> capturedFuture = future;
|
|
3281
3545
|
final long tokenForListener = indicatorToken;
|
|
3282
|
-
future.addListener(
|
|
3283
|
-
|
|
3284
|
-
|
|
3285
|
-
|
|
3286
|
-
|
|
3287
|
-
|
|
3288
|
-
|
|
3289
|
-
|
|
3290
|
-
|
|
3291
|
-
|
|
3292
|
-
|
|
3293
|
-
)
|
|
3294
|
-
|
|
3295
|
-
|
|
3296
|
-
Log.e(TAG, "Error during focus: " + e.getMessage());
|
|
3297
|
-
}
|
|
3298
|
-
} finally {
|
|
3299
|
-
if (currentFocusFuture == capturedFuture && currentFocusFuture.isDone()) {
|
|
3300
|
-
currentFocusFuture = null;
|
|
3301
|
-
}
|
|
3302
|
-
hideFocusIndicator(tokenForListener);
|
|
3303
|
-
endOperation("setFocus");
|
|
3546
|
+
future.addListener(() -> {
|
|
3547
|
+
try {
|
|
3548
|
+
FocusMeteringResult result = capturedFuture.get();
|
|
3549
|
+
} catch (Exception e) {
|
|
3550
|
+
// Handle cancellation gracefully - this is expected when rapid taps occur
|
|
3551
|
+
if (
|
|
3552
|
+
e.getMessage() != null &&
|
|
3553
|
+
(e.getMessage().contains("Cancelled by another startFocusAndMetering") ||
|
|
3554
|
+
e.getMessage().contains("OperationCanceledException") ||
|
|
3555
|
+
e.getClass().getSimpleName().contains("OperationCanceledException"))
|
|
3556
|
+
) {
|
|
3557
|
+
Log.d(TAG, "Focus operation was cancelled by a newer focus request");
|
|
3558
|
+
} else {
|
|
3559
|
+
Log.e(TAG, "Error during focus: " + e.getMessage());
|
|
3304
3560
|
}
|
|
3305
|
-
}
|
|
3306
|
-
|
|
3307
|
-
|
|
3561
|
+
} finally {
|
|
3562
|
+
if (currentFocusFuture == capturedFuture && currentFocusFuture.isDone()) {
|
|
3563
|
+
currentFocusFuture = null;
|
|
3564
|
+
}
|
|
3565
|
+
hideFocusIndicator(tokenForListener);
|
|
3566
|
+
endOperation("setFocus");
|
|
3567
|
+
}
|
|
3568
|
+
}, ContextCompat.getMainExecutor(context));
|
|
3308
3569
|
} catch (Exception e) {
|
|
3309
3570
|
currentFocusFuture = null;
|
|
3310
3571
|
Log.e(TAG, "Failed to set focus: " + e.getMessage());
|
|
@@ -3484,6 +3745,38 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
3484
3745
|
}
|
|
3485
3746
|
}
|
|
3486
3747
|
|
|
3748
|
+
private void restoreCameraControlStateAfterRebind(Integer exposureCompensationIndex) {
|
|
3749
|
+
if (camera == null) {
|
|
3750
|
+
return;
|
|
3751
|
+
}
|
|
3752
|
+
if (exposureCompensationIndex != null) {
|
|
3753
|
+
try {
|
|
3754
|
+
ExposureState state = camera.getCameraInfo().getExposureState();
|
|
3755
|
+
Range<Integer> range = state.getExposureCompensationRange();
|
|
3756
|
+
int idx = exposureCompensationIndex;
|
|
3757
|
+
if (idx < range.getLower()) {
|
|
3758
|
+
idx = range.getLower();
|
|
3759
|
+
}
|
|
3760
|
+
if (idx > range.getUpper()) {
|
|
3761
|
+
idx = range.getUpper();
|
|
3762
|
+
}
|
|
3763
|
+
camera.getCameraControl().setExposureCompensationIndex(idx);
|
|
3764
|
+
} catch (Exception e) {
|
|
3765
|
+
Log.w(TAG, "restoreCameraControlStateAfterRebind: Failed to restore exposure compensation", e);
|
|
3766
|
+
}
|
|
3767
|
+
} else {
|
|
3768
|
+
resetExposureCompensationToDefault();
|
|
3769
|
+
}
|
|
3770
|
+
reapplyCameraControlModes();
|
|
3771
|
+
if (torchRequested) {
|
|
3772
|
+
try {
|
|
3773
|
+
camera.getCameraControl().enableTorch(true);
|
|
3774
|
+
} catch (Exception e) {
|
|
3775
|
+
Log.w(TAG, "restoreCameraControlStateAfterRebind: Failed to restore torch", e);
|
|
3776
|
+
}
|
|
3777
|
+
}
|
|
3778
|
+
}
|
|
3779
|
+
|
|
3487
3780
|
public float[] getExposureCompensationRange() throws Exception {
|
|
3488
3781
|
if (camera == null) {
|
|
3489
3782
|
throw new Exception("Camera not initialized");
|
|
@@ -3613,6 +3906,10 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
3613
3906
|
});
|
|
3614
3907
|
}
|
|
3615
3908
|
|
|
3909
|
+
private void rejectPendingFrameRateBindOnStop() {
|
|
3910
|
+
completePendingFrameRateBindError("Camera session stopped before camera rebind completed");
|
|
3911
|
+
}
|
|
3912
|
+
|
|
3616
3913
|
private void completePendingFrameRateBindSuccess() {
|
|
3617
3914
|
if (pendingFrameRateBindSuccess == null) {
|
|
3618
3915
|
return;
|
|
@@ -3892,7 +4189,8 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
3892
4189
|
if (parent != null) {
|
|
3893
4190
|
parent.removeView(focusIndicatorView);
|
|
3894
4191
|
}
|
|
3895
|
-
} catch (Exception ignore) {
|
|
4192
|
+
} catch (Exception ignore) {
|
|
4193
|
+
} finally {
|
|
3896
4194
|
focusIndicatorView = null;
|
|
3897
4195
|
}
|
|
3898
4196
|
});
|
|
@@ -3979,6 +4277,7 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
3979
4277
|
public void setFlashMode(String mode) {
|
|
3980
4278
|
// Handle torch separately via CameraControl
|
|
3981
4279
|
if ("torch".equals(mode)) {
|
|
4280
|
+
torchRequested = true;
|
|
3982
4281
|
try {
|
|
3983
4282
|
if (camera != null) {
|
|
3984
4283
|
camera.getCameraControl().enableTorch(true);
|
|
@@ -3997,6 +4296,7 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
3997
4296
|
return;
|
|
3998
4297
|
}
|
|
3999
4298
|
|
|
4299
|
+
torchRequested = false;
|
|
4000
4300
|
// For non-torch modes, ensure torch is disabled
|
|
4001
4301
|
try {
|
|
4002
4302
|
if (camera != null) {
|
|
@@ -4858,7 +5158,7 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
4858
5158
|
Log.d(TAG, "Device orientation: " + (isPortrait ? "PORTRAIT" : "LANDSCAPE"));
|
|
4859
5159
|
|
|
4860
5160
|
// iOS: let ratio = !isPortrait ? ratioParts[0] / ratioParts[1] : ratioParts[1] / ratioParts[0]
|
|
4861
|
-
double ratio = !isPortrait ?
|
|
5161
|
+
double ratio = !isPortrait ? ratioWidth / ratioHeight : ratioHeight / ratioWidth;
|
|
4862
5162
|
|
|
4863
5163
|
Log.d(TAG, "Computed ratio: " + ratio + " (iOS formula: " + (!isPortrait ? "width/height" : "height/width") + ")");
|
|
4864
5164
|
|
|
@@ -4907,7 +5207,7 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
4907
5207
|
// Compare with expected ratio based on orientation
|
|
4908
5208
|
String[] parts = aspectRatio.split(":");
|
|
4909
5209
|
if (parts.length == 2) {
|
|
4910
|
-
double expectedDisplayRatio = isPortrait ?
|
|
5210
|
+
double expectedDisplayRatio = isPortrait ? ratioHeight / ratioWidth : ratioWidth / ratioHeight;
|
|
4911
5211
|
double difference = Math.abs(displayedRatio - expectedDisplayRatio);
|
|
4912
5212
|
Log.d(
|
|
4913
5213
|
TAG,
|
|
@@ -5190,6 +5490,7 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
5190
5490
|
currentRecording = null;
|
|
5191
5491
|
currentVideoFile = null;
|
|
5192
5492
|
currentVideoCallback = null;
|
|
5493
|
+
maybePerformPendingViewportRebind();
|
|
5193
5494
|
}
|
|
5194
5495
|
|
|
5195
5496
|
private boolean isRecordingLimitReached(int error) {
|