@capgo/camera-preview 8.0.9 → 8.0.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/android/src/main/java/app/capgo/capacitor/camera/preview/CameraPreview.java +46 -1
- package/android/src/main/java/app/capgo/capacitor/camera/preview/CameraXView.java +315 -52
- package/ios/Sources/CapgoCameraPreviewPlugin/CameraController.swift +80 -26
- package/ios/Sources/CapgoCameraPreviewPlugin/Plugin.swift +18 -8
- package/package.json +1 -1
|
@@ -936,7 +936,9 @@ public class CameraPreview extends Plugin implements CameraXView.CameraXViewList
|
|
|
936
936
|
if (originalWindowBackground == null) {
|
|
937
937
|
originalWindowBackground = getBridge().getActivity().getWindow().getDecorView().getBackground();
|
|
938
938
|
}
|
|
939
|
-
|
|
939
|
+
// Set to solid black first to prevent flickering during transition
|
|
940
|
+
// This provides a stable base before camera preview is ready
|
|
941
|
+
getBridge().getActivity().getWindow().setBackgroundDrawable(new ColorDrawable(Color.BLACK));
|
|
940
942
|
} catch (Exception ignored) {}
|
|
941
943
|
}
|
|
942
944
|
DisplayMetrics metrics = getBridge().getActivity().getResources().getDisplayMetrics();
|
|
@@ -1492,6 +1494,16 @@ public class CameraPreview extends Plugin implements CameraXView.CameraXViewList
|
|
|
1492
1494
|
return ret;
|
|
1493
1495
|
}
|
|
1494
1496
|
|
|
1497
|
+
private boolean isToBackMode() {
|
|
1498
|
+
if (cameraXView != null) {
|
|
1499
|
+
CameraSessionConfiguration config = cameraXView.getSessionConfig();
|
|
1500
|
+
if (config != null) {
|
|
1501
|
+
return config.isToBack();
|
|
1502
|
+
}
|
|
1503
|
+
}
|
|
1504
|
+
return false;
|
|
1505
|
+
}
|
|
1506
|
+
|
|
1495
1507
|
@Override
|
|
1496
1508
|
public void onCameraStarted(int width, int height, int x, int y) {
|
|
1497
1509
|
PluginCall call = bridge.getSavedCall(cameraStartCallbackId);
|
|
@@ -1624,6 +1636,20 @@ public class CameraPreview extends Plugin implements CameraXView.CameraXViewList
|
|
|
1624
1636
|
")"
|
|
1625
1637
|
);
|
|
1626
1638
|
|
|
1639
|
+
// Transition window background to transparent now that camera is ready
|
|
1640
|
+
// This prevents flickering during camera initialization
|
|
1641
|
+
if (isToBackMode()) {
|
|
1642
|
+
getBridge()
|
|
1643
|
+
.getActivity()
|
|
1644
|
+
.runOnUiThread(() -> {
|
|
1645
|
+
try {
|
|
1646
|
+
getBridge().getActivity().getWindow().setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));
|
|
1647
|
+
} catch (Exception e) {
|
|
1648
|
+
Log.w(TAG, "Failed to set window background to transparent", e);
|
|
1649
|
+
}
|
|
1650
|
+
});
|
|
1651
|
+
}
|
|
1652
|
+
|
|
1627
1653
|
call.resolve(result);
|
|
1628
1654
|
bridge.releaseCall(call);
|
|
1629
1655
|
cameraStartCallbackId = null; // Prevent re-use
|
|
@@ -1664,6 +1690,25 @@ public class CameraPreview extends Plugin implements CameraXView.CameraXViewList
|
|
|
1664
1690
|
bridge.releaseCall(call);
|
|
1665
1691
|
cameraStartCallbackId = null;
|
|
1666
1692
|
}
|
|
1693
|
+
|
|
1694
|
+
// Restore original window background on error to prevent black screen
|
|
1695
|
+
// Use synchronized block to ensure only one thread captures and clears the background.
|
|
1696
|
+
// Even if multiple errors occur, only the first will have a non-null background to restore.
|
|
1697
|
+
synchronized (this) {
|
|
1698
|
+
final Drawable backgroundToRestore = originalWindowBackground;
|
|
1699
|
+
if (backgroundToRestore != null) {
|
|
1700
|
+
originalWindowBackground = null; // Clear immediately so other threads won't restore
|
|
1701
|
+
getBridge()
|
|
1702
|
+
.getActivity()
|
|
1703
|
+
.runOnUiThread(() -> {
|
|
1704
|
+
try {
|
|
1705
|
+
getBridge().getActivity().getWindow().setBackgroundDrawable(backgroundToRestore);
|
|
1706
|
+
} catch (Exception e) {
|
|
1707
|
+
Log.w(TAG, "Failed to restore window background on error", e);
|
|
1708
|
+
}
|
|
1709
|
+
});
|
|
1710
|
+
}
|
|
1711
|
+
}
|
|
1667
1712
|
}
|
|
1668
1713
|
|
|
1669
1714
|
@PluginMethod
|
|
@@ -13,6 +13,10 @@ import android.graphics.Paint;
|
|
|
13
13
|
import android.graphics.Rect;
|
|
14
14
|
import android.graphics.Typeface;
|
|
15
15
|
import android.graphics.drawable.GradientDrawable;
|
|
16
|
+
import android.hardware.Sensor;
|
|
17
|
+
import android.hardware.SensorEvent;
|
|
18
|
+
import android.hardware.SensorEventListener;
|
|
19
|
+
import android.hardware.SensorManager;
|
|
16
20
|
import android.hardware.camera2.CameraAccessException;
|
|
17
21
|
import android.hardware.camera2.CameraCharacteristics;
|
|
18
22
|
import android.hardware.camera2.CameraManager;
|
|
@@ -139,6 +143,10 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
139
143
|
private CameraXViewListener listener;
|
|
140
144
|
private final Context context;
|
|
141
145
|
private final WebView webView;
|
|
146
|
+
// WebView's default background is white; we store this to restore on error or cleanup
|
|
147
|
+
// Note: WebView doesn't provide a way to query its current background color, so we assume
|
|
148
|
+
// the default white background. This is consistent across Android versions.
|
|
149
|
+
private int originalWebViewBackground = android.graphics.Color.WHITE;
|
|
142
150
|
private final LifecycleRegistry lifecycleRegistry;
|
|
143
151
|
private final Executor mainExecutor;
|
|
144
152
|
private ExecutorService cameraExecutor;
|
|
@@ -157,6 +165,31 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
157
165
|
private int activeOperations = 0;
|
|
158
166
|
private boolean stopPending = false;
|
|
159
167
|
|
|
168
|
+
// Sensor Fields
|
|
169
|
+
private SensorManager sensorManager;
|
|
170
|
+
private Sensor accelerometer;
|
|
171
|
+
private final float[] lastAccelerometerValues = new float[3]; // x,y, and z
|
|
172
|
+
private final Object accelerometerLock = new Object();
|
|
173
|
+
private volatile int lastCaptureRotation = -1; // -1 unknown
|
|
174
|
+
|
|
175
|
+
private final SensorEventListener accelerometerListener = new SensorEventListener() {
|
|
176
|
+
@Override
|
|
177
|
+
public void onSensorChanged(SensorEvent event) {
|
|
178
|
+
if (event.sensor.getType() == Sensor.TYPE_ACCELEROMETER) {
|
|
179
|
+
synchronized (accelerometerLock) {
|
|
180
|
+
lastAccelerometerValues[0] = event.values[0];
|
|
181
|
+
lastAccelerometerValues[1] = event.values[1];
|
|
182
|
+
lastAccelerometerValues[2] = event.values[2];
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
@Override
|
|
188
|
+
public void onAccuracyChanged(Sensor sensor, int accuracy) {
|
|
189
|
+
// Not Needed
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
|
|
160
193
|
private boolean IsOperationRunning(String name) {
|
|
161
194
|
synchronized (operationLock) {
|
|
162
195
|
if (stopPending) {
|
|
@@ -336,6 +369,23 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
336
369
|
public void startSession(CameraSessionConfiguration config) {
|
|
337
370
|
this.sessionConfig = config;
|
|
338
371
|
cameraExecutor = Executors.newSingleThreadExecutor();
|
|
372
|
+
|
|
373
|
+
// Reset cached orientation so we don't reuse stale values across sessions
|
|
374
|
+
synchronized (accelerometerLock) {
|
|
375
|
+
lastAccelerometerValues[0] = 0f;
|
|
376
|
+
lastAccelerometerValues[1] = 0f;
|
|
377
|
+
lastAccelerometerValues[2] = 0f;
|
|
378
|
+
}
|
|
379
|
+
lastCaptureRotation = -1;
|
|
380
|
+
|
|
381
|
+
// Start accelerometer for orientation detection regardless of lock
|
|
382
|
+
if (sensorManager == null) {
|
|
383
|
+
sensorManager = (SensorManager) context.getSystemService(Context.SENSOR_SERVICE);
|
|
384
|
+
accelerometer = sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
|
|
385
|
+
}
|
|
386
|
+
if (accelerometer != null) {
|
|
387
|
+
sensorManager.registerListener(accelerometerListener, accelerometer, SensorManager.SENSOR_DELAY_UI);
|
|
388
|
+
}
|
|
339
389
|
synchronized (operationLock) {
|
|
340
390
|
activeOperations = 0;
|
|
341
391
|
stopPending = false;
|
|
@@ -386,6 +436,10 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
386
436
|
|
|
387
437
|
private void performImmediateStop() {
|
|
388
438
|
isRunning = false;
|
|
439
|
+
// Stop accelerometer
|
|
440
|
+
if (sensorManager != null && accelerometer != null) {
|
|
441
|
+
sensorManager.unregisterListener(accelerometerListener);
|
|
442
|
+
}
|
|
389
443
|
// Cancel any ongoing focus operation when stopping session
|
|
390
444
|
if (currentFocusFuture != null && !currentFocusFuture.isDone()) {
|
|
391
445
|
currentFocusFuture.cancel(true);
|
|
@@ -421,6 +475,22 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
421
475
|
});
|
|
422
476
|
}
|
|
423
477
|
|
|
478
|
+
private void restoreWebViewBackground() {
|
|
479
|
+
// Capture sessionConfig reference once to avoid race conditions
|
|
480
|
+
CameraSessionConfiguration config = sessionConfig;
|
|
481
|
+
boolean shouldRestore = config != null && config.isToBack();
|
|
482
|
+
if (shouldRestore) {
|
|
483
|
+
// Capture background color before posting to UI thread
|
|
484
|
+
final int backgroundColorToRestore = originalWebViewBackground;
|
|
485
|
+
webView.post(() -> {
|
|
486
|
+
// Additional safety check in case webView context changed
|
|
487
|
+
if (webView != null) {
|
|
488
|
+
webView.setBackgroundColor(backgroundColorToRestore);
|
|
489
|
+
}
|
|
490
|
+
});
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
|
|
424
494
|
private void setupCamera() {
|
|
425
495
|
ListenableFuture<ProcessCameraProvider> cameraProviderFuture = ProcessCameraProvider.getInstance(context);
|
|
426
496
|
cameraProviderFuture.addListener(
|
|
@@ -430,6 +500,8 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
430
500
|
setupPreviewView();
|
|
431
501
|
bindCameraUseCases();
|
|
432
502
|
} catch (Exception e) {
|
|
503
|
+
// Restore webView background on error
|
|
504
|
+
restoreWebViewBackground();
|
|
433
505
|
if (listener != null) {
|
|
434
506
|
listener.onCameraStartError("Error initializing camera: " + e.getMessage());
|
|
435
507
|
}
|
|
@@ -444,7 +516,8 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
444
516
|
removePreviewView();
|
|
445
517
|
}
|
|
446
518
|
if (sessionConfig.isToBack()) {
|
|
447
|
-
|
|
519
|
+
// Set to black initially to prevent flickering, will be transparent after camera starts
|
|
520
|
+
webView.setBackgroundColor(android.graphics.Color.BLACK);
|
|
448
521
|
}
|
|
449
522
|
|
|
450
523
|
// Create a container to hold both the preview and grid overlay
|
|
@@ -542,6 +615,19 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
542
615
|
}
|
|
543
616
|
}
|
|
544
617
|
|
|
618
|
+
/**
|
|
619
|
+
* Compute layout parameters for the camera preview container based on the current session configuration,
|
|
620
|
+
* device screen size, WebView/parent geometry, and optional aspect-ratio centering.
|
|
621
|
+
*
|
|
622
|
+
* The returned FrameLayout.LayoutParams contains width, height, leftMargin (x) and topMargin (y)
|
|
623
|
+
* for placing the preview. When an aspect ratio is specified and sessionConfig is in centered mode,
|
|
624
|
+
* the preview size is scaled to the largest area that fits the aspect ratio within the screen and
|
|
625
|
+
* any axis with a coordinate equal to -1 is auto-centered for that axis; axes explicitly provided
|
|
626
|
+
* in sessionConfig are preserved. Coordinates supplied by sessionConfig are assumed to already
|
|
627
|
+
* include WebView insets.
|
|
628
|
+
*
|
|
629
|
+
* @return a FrameLayout.LayoutParams configured with the computed preview width, height, leftMargin and topMargin
|
|
630
|
+
*/
|
|
545
631
|
private FrameLayout.LayoutParams calculatePreviewLayoutParams() {
|
|
546
632
|
// sessionConfig already contains pixel-converted coordinates with webview offsets applied
|
|
547
633
|
int x = sessionConfig.getX();
|
|
@@ -663,9 +749,13 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
663
749
|
Log.d(TAG, "Width-limited sizing: " + width + "x" + height);
|
|
664
750
|
}
|
|
665
751
|
|
|
666
|
-
// Center the preview
|
|
667
|
-
|
|
668
|
-
|
|
752
|
+
// Center the preview only overwrite what was not explicitly set
|
|
753
|
+
if (sessionConfig.getX() == -1) {
|
|
754
|
+
x = (screenWidthPx - width) / 2;
|
|
755
|
+
}
|
|
756
|
+
if (sessionConfig.getY() == -1) {
|
|
757
|
+
y = (screenHeightPx - height) / 2;
|
|
758
|
+
}
|
|
669
759
|
|
|
670
760
|
Log.d(TAG, "Auto-centered position: x=" + x + ", y=" + y);
|
|
671
761
|
} catch (NumberFormatException e) {
|
|
@@ -714,7 +804,7 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
714
804
|
if (focusIndicatorView != null) {
|
|
715
805
|
focusIndicatorView = null;
|
|
716
806
|
}
|
|
717
|
-
webView.setBackgroundColor(
|
|
807
|
+
webView.setBackgroundColor(originalWebViewBackground);
|
|
718
808
|
}
|
|
719
809
|
|
|
720
810
|
@OptIn(markerClass = ExperimentalCamera2Interop.class)
|
|
@@ -784,8 +874,14 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
784
874
|
// Bind with or without video capture based on enableVideoMode
|
|
785
875
|
if (sessionConfig.isVideoModeEnabled() && videoCapture != null) {
|
|
786
876
|
camera = cameraProvider.bindToLifecycle(this, currentCameraSelector, preview, imageCapture, videoCapture);
|
|
877
|
+
CameraInfo cameraInfo = camera.getCameraInfo();
|
|
878
|
+
currentDeviceId = Camera2CameraInfo.from(cameraInfo).getCameraId();
|
|
879
|
+
Log.d(TAG, "bindCameraUseCases: Camera successfully bound to device ID: " + currentDeviceId);
|
|
787
880
|
} else {
|
|
788
881
|
camera = cameraProvider.bindToLifecycle(this, currentCameraSelector, preview, imageCapture);
|
|
882
|
+
CameraInfo cameraInfo = camera.getCameraInfo();
|
|
883
|
+
currentDeviceId = Camera2CameraInfo.from(cameraInfo).getCameraId();
|
|
884
|
+
Log.d(TAG, "bindCameraUseCases: Camera successfully bound to device ID: " + currentDeviceId);
|
|
789
885
|
}
|
|
790
886
|
|
|
791
887
|
resetExposureCompensationToDefault();
|
|
@@ -926,10 +1022,20 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
926
1022
|
// Update grid overlay bounds after camera is started
|
|
927
1023
|
updateGridOverlayBounds();
|
|
928
1024
|
|
|
1025
|
+
// Now transition to transparent background after camera is ready
|
|
1026
|
+
// This prevents flickering during camera initialization
|
|
1027
|
+
if (sessionConfig.isToBack()) {
|
|
1028
|
+
webView.post(() -> {
|
|
1029
|
+
webView.setBackgroundColor(android.graphics.Color.TRANSPARENT);
|
|
1030
|
+
});
|
|
1031
|
+
}
|
|
1032
|
+
|
|
929
1033
|
listener.onCameraStarted(actualWidth, actualHeight, actualX, actualY);
|
|
930
1034
|
});
|
|
931
1035
|
}
|
|
932
1036
|
} catch (Exception e) {
|
|
1037
|
+
// Restore webView background on error
|
|
1038
|
+
restoreWebViewBackground();
|
|
933
1039
|
if (listener != null) listener.onCameraStartError("Error binding camera: " + e.getMessage());
|
|
934
1040
|
}
|
|
935
1041
|
});
|
|
@@ -971,6 +1077,50 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
971
1077
|
}
|
|
972
1078
|
}
|
|
973
1079
|
|
|
1080
|
+
/**
|
|
1081
|
+
* Get device rotation from accelerometer data.
|
|
1082
|
+
* This works even when portrait lock is enabled.
|
|
1083
|
+
* Falls back to display rotation if accelerometer data is not available.
|
|
1084
|
+
*/
|
|
1085
|
+
private int getRotationFromAccelerometer() {
|
|
1086
|
+
float x, y;
|
|
1087
|
+
synchronized (accelerometerLock) {
|
|
1088
|
+
x = lastAccelerometerValues[0];
|
|
1089
|
+
y = lastAccelerometerValues[1];
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
// If no accelerometer data yet, fall back to display rotation
|
|
1093
|
+
final float epsilon = 1.0f;
|
|
1094
|
+
if (Math.abs(x) < epsilon && Math.abs(y) < epsilon) {
|
|
1095
|
+
if (previewView != null && previewView.getDisplay() != null) {
|
|
1096
|
+
return previewView.getDisplay().getRotation();
|
|
1097
|
+
}
|
|
1098
|
+
return android.view.Surface.ROTATION_0;
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
// Android accelerometer: +X is right, +Y is up, +Z is toward user
|
|
1102
|
+
// Determine orientation based on which axis has the strongest gravity component
|
|
1103
|
+
if (Math.abs(x) > Math.abs(y)) {
|
|
1104
|
+
// Landscape orientation
|
|
1105
|
+
if (x > 0) {
|
|
1106
|
+
// Device tilted to the left (top of device points left)
|
|
1107
|
+
return android.view.Surface.ROTATION_90;
|
|
1108
|
+
} else {
|
|
1109
|
+
// Device tilted to the right (top of device points right)
|
|
1110
|
+
return android.view.Surface.ROTATION_270;
|
|
1111
|
+
}
|
|
1112
|
+
} else {
|
|
1113
|
+
// Portrait orientation
|
|
1114
|
+
if (y > 0) {
|
|
1115
|
+
// Normal portrait (top of device points up)
|
|
1116
|
+
return android.view.Surface.ROTATION_0;
|
|
1117
|
+
} else {
|
|
1118
|
+
// Upside down portrait
|
|
1119
|
+
return android.view.Surface.ROTATION_180;
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
|
|
974
1124
|
public void capturePhoto(
|
|
975
1125
|
int quality,
|
|
976
1126
|
final boolean saveToGallery,
|
|
@@ -993,6 +1143,12 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
993
1143
|
return;
|
|
994
1144
|
}
|
|
995
1145
|
|
|
1146
|
+
// Set rotation from accelerometer for device orientation regardless of lock
|
|
1147
|
+
int rotation = getRotationFromAccelerometer();
|
|
1148
|
+
lastCaptureRotation = rotation;
|
|
1149
|
+
imageCapture.setTargetRotation(rotation);
|
|
1150
|
+
Log.d(TAG, "capturePhoto: Set target rotation to " + rotation + " from accelerometer");
|
|
1151
|
+
|
|
996
1152
|
Log.d(
|
|
997
1153
|
TAG,
|
|
998
1154
|
"capturePhoto: Starting photo capture with: " +
|
|
@@ -1744,6 +1900,19 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
1744
1900
|
Rect bounds = getActualCameraBounds();
|
|
1745
1901
|
int previewW = Math.max(1, bounds.width());
|
|
1746
1902
|
int previewH = Math.max(1, bounds.height());
|
|
1903
|
+
|
|
1904
|
+
// Check if device physical orientation differs from UI orientation
|
|
1905
|
+
int rotation = (lastCaptureRotation != -1) ? lastCaptureRotation : getRotationFromAccelerometer();
|
|
1906
|
+
boolean physicalInLandscape = (rotation == android.view.Surface.ROTATION_90 || rotation == android.view.Surface.ROTATION_270);
|
|
1907
|
+
boolean previewIsPortrait = previewH > previewW;
|
|
1908
|
+
|
|
1909
|
+
// If physical orientation doesn't match preview orientation swap ratio
|
|
1910
|
+
if (physicalInLandscape == previewIsPortrait) {
|
|
1911
|
+
int temp = previewW;
|
|
1912
|
+
previewW = previewH;
|
|
1913
|
+
previewH = temp;
|
|
1914
|
+
}
|
|
1915
|
+
|
|
1747
1916
|
float previewRatio = (float) previewW / (float) previewH;
|
|
1748
1917
|
|
|
1749
1918
|
int imgW = image.getWidth();
|
|
@@ -1771,26 +1940,65 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
1771
1940
|
// not working for xiaomi https://xiaomi.eu/community/threads/mi-11-ultra-unable-to-access-camera-lenses-in-apps-camera2-api.61456/
|
|
1772
1941
|
@OptIn(markerClass = ExperimentalCamera2Interop.class)
|
|
1773
1942
|
public static List<app.capgo.capacitor.camera.preview.model.CameraDevice> getAvailableDevicesStatic(Context context) {
|
|
1774
|
-
Log.d(TAG, "
|
|
1943
|
+
Log.d(TAG, "=== Starting Camera Enumeration ===");
|
|
1775
1944
|
List<app.capgo.capacitor.camera.preview.model.CameraDevice> devices = new ArrayList<>();
|
|
1776
1945
|
try {
|
|
1777
1946
|
ListenableFuture<ProcessCameraProvider> cameraProviderFuture = ProcessCameraProvider.getInstance(context);
|
|
1778
1947
|
ProcessCameraProvider cameraProvider = cameraProviderFuture.get();
|
|
1779
1948
|
CameraManager cameraManager = (CameraManager) context.getSystemService(Context.CAMERA_SERVICE);
|
|
1780
1949
|
|
|
1781
|
-
|
|
1950
|
+
List<CameraInfo> availableCameras = cameraProvider.getAvailableCameraInfos();
|
|
1951
|
+
|
|
1952
|
+
for (CameraInfo cameraInfo : availableCameras) {
|
|
1782
1953
|
String logicalCameraId = Camera2CameraInfo.from(cameraInfo).getCameraId();
|
|
1783
1954
|
String position = isBackCamera(cameraInfo) ? "rear" : "front";
|
|
1784
1955
|
|
|
1785
1956
|
// Add logical camera
|
|
1786
|
-
|
|
1787
|
-
float
|
|
1957
|
+
ZoomState zoomState = cameraInfo.getZoomState().getValue();
|
|
1958
|
+
float minZoom = zoomState != null ? zoomState.getMinZoomRatio() : 1.0f;
|
|
1959
|
+
float maxZoom = zoomState != null ? zoomState.getMaxZoomRatio() : 1.0f;
|
|
1960
|
+
|
|
1961
|
+
// Determine device type by analyzing camera characteristics
|
|
1962
|
+
String deviceType = "wideAngle";
|
|
1963
|
+
float focalLength = 4.25f;
|
|
1964
|
+
|
|
1965
|
+
try {
|
|
1966
|
+
CameraCharacteristics characteristics = cameraManager.getCameraCharacteristics(logicalCameraId);
|
|
1967
|
+
float[] focalLengths = characteristics.get(CameraCharacteristics.LENS_INFO_AVAILABLE_FOCAL_LENGTHS);
|
|
1968
|
+
android.util.SizeF sensorSize = characteristics.get(CameraCharacteristics.SENSOR_INFO_PHYSICAL_SIZE);
|
|
1969
|
+
|
|
1970
|
+
if (focalLengths != null && focalLengths.length > 0) {
|
|
1971
|
+
focalLength = focalLengths[0];
|
|
1972
|
+
|
|
1973
|
+
// Calculate FOV to determine camera type
|
|
1974
|
+
if (sensorSize != null && sensorSize.getWidth() > 0) {
|
|
1975
|
+
double fov = 2 * Math.toDegrees(Math.atan(sensorSize.getWidth() / (2 * focalLength)));
|
|
1976
|
+
if (fov > 90) {
|
|
1977
|
+
deviceType = "ultraWide";
|
|
1978
|
+
} else if (fov < 40) {
|
|
1979
|
+
deviceType = "telephoto";
|
|
1980
|
+
}
|
|
1981
|
+
} else {
|
|
1982
|
+
// Fallback: classify by focal length alone
|
|
1983
|
+
if (focalLength < 3.0f) {
|
|
1984
|
+
deviceType = "ultraWide";
|
|
1985
|
+
} else if (focalLength > 5.0f) {
|
|
1986
|
+
deviceType = "telephoto";
|
|
1987
|
+
}
|
|
1988
|
+
}
|
|
1989
|
+
}
|
|
1990
|
+
} catch (CameraAccessException e) {
|
|
1991
|
+
Log.e(TAG, "Failed to get characteristics for " + logicalCameraId, e);
|
|
1992
|
+
}
|
|
1788
1993
|
List<LensInfo> logicalLenses = new ArrayList<>();
|
|
1789
|
-
logicalLenses.add(new LensInfo(
|
|
1994
|
+
logicalLenses.add(new LensInfo(focalLength, deviceType, 1.0f, maxZoom));
|
|
1995
|
+
|
|
1996
|
+
String label = "Logical " + deviceType + " (" + position + ")";
|
|
1997
|
+
|
|
1790
1998
|
devices.add(
|
|
1791
1999
|
new app.capgo.capacitor.camera.preview.model.CameraDevice(
|
|
1792
2000
|
logicalCameraId,
|
|
1793
|
-
|
|
2001
|
+
label,
|
|
1794
2002
|
position,
|
|
1795
2003
|
logicalLenses,
|
|
1796
2004
|
minZoom,
|
|
@@ -1798,32 +2006,59 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
1798
2006
|
true
|
|
1799
2007
|
)
|
|
1800
2008
|
);
|
|
1801
|
-
Log.d(TAG, "
|
|
2009
|
+
Log.d(TAG, "Added logical camera: " + logicalCameraId + " zoom: " + minZoom + "-" + maxZoom);
|
|
1802
2010
|
|
|
1803
2011
|
// Get and add physical cameras
|
|
1804
2012
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
|
1805
2013
|
Set<CameraInfo> physicalCameraInfos = cameraInfo.getPhysicalCameraInfos();
|
|
1806
|
-
|
|
2014
|
+
Log.d(TAG, "Physical camera count from CameraX: " + physicalCameraInfos.size());
|
|
1807
2015
|
|
|
1808
|
-
|
|
2016
|
+
if (physicalCameraInfos.isEmpty()) {
|
|
2017
|
+
Log.w(TAG, "No physical cameras exposed through CameraX for " + logicalCameraId);
|
|
2018
|
+
|
|
2019
|
+
// Try to get physical IDs from CameraManager
|
|
2020
|
+
try {
|
|
2021
|
+
CameraCharacteristics chars = cameraManager.getCameraCharacteristics(logicalCameraId);
|
|
2022
|
+
Set<String> physicalIds = chars.getPhysicalCameraIds();
|
|
2023
|
+
Log.d(TAG, "CameraManager reports " + physicalIds.size() + " physical cameras for " + logicalCameraId);
|
|
2024
|
+
for (String pid : physicalIds) {
|
|
2025
|
+
Log.d(TAG, " Physical camera ID: " + pid);
|
|
2026
|
+
}
|
|
2027
|
+
} catch (CameraAccessException e) {
|
|
2028
|
+
Log.e(TAG, "Failed to get characteristics", e);
|
|
2029
|
+
}
|
|
2030
|
+
continue;
|
|
2031
|
+
}
|
|
1809
2032
|
|
|
1810
2033
|
for (CameraInfo physicalCameraInfo : physicalCameraInfos) {
|
|
1811
2034
|
String physicalId = Camera2CameraInfo.from(physicalCameraInfo).getCameraId();
|
|
1812
|
-
|
|
2035
|
+
Log.d(TAG, "Processing physical camera: " + physicalId);
|
|
2036
|
+
|
|
2037
|
+
if (physicalId.equals(logicalCameraId)) {
|
|
2038
|
+
Log.d(TAG, "Skipping - same as logical ID");
|
|
2039
|
+
continue;
|
|
2040
|
+
}
|
|
1813
2041
|
|
|
1814
2042
|
try {
|
|
1815
2043
|
CameraCharacteristics characteristics = cameraManager.getCameraCharacteristics(physicalId);
|
|
1816
|
-
String
|
|
2044
|
+
String physicalDeviceType = "wideAngle";
|
|
1817
2045
|
float[] focalLengths = characteristics.get(CameraCharacteristics.LENS_INFO_AVAILABLE_FOCAL_LENGTHS);
|
|
1818
2046
|
android.util.SizeF sensorSize = characteristics.get(CameraCharacteristics.SENSOR_INFO_PHYSICAL_SIZE);
|
|
1819
2047
|
|
|
2048
|
+
Log.d(TAG, " Focal lengths: " + (focalLengths != null ? Arrays.toString(focalLengths) : "null"));
|
|
2049
|
+
Log.d(
|
|
2050
|
+
TAG,
|
|
2051
|
+
" Sensor size: " + (sensorSize != null ? sensorSize.getWidth() + "x" + sensorSize.getHeight() : "null")
|
|
2052
|
+
);
|
|
2053
|
+
|
|
1820
2054
|
if (focalLengths != null && focalLengths.length > 0 && sensorSize != null && sensorSize.getWidth() > 0) {
|
|
1821
2055
|
double fov = 2 * Math.toDegrees(Math.atan(sensorSize.getWidth() / (2 * focalLengths[0])));
|
|
1822
|
-
|
|
1823
|
-
|
|
2056
|
+
Log.d(TAG, " Calculated FOV: " + fov);
|
|
2057
|
+
if (fov > 90) physicalDeviceType = "ultraWide";
|
|
2058
|
+
else if (fov < 40) physicalDeviceType = "telephoto";
|
|
1824
2059
|
} else if (focalLengths != null && focalLengths.length > 0) {
|
|
1825
|
-
if (focalLengths[0] < 3.0f)
|
|
1826
|
-
else if (focalLengths[0] > 5.0f)
|
|
2060
|
+
if (focalLengths[0] < 3.0f) physicalDeviceType = "ultraWide";
|
|
2061
|
+
else if (focalLengths[0] > 5.0f) physicalDeviceType = "telephoto";
|
|
1827
2062
|
}
|
|
1828
2063
|
|
|
1829
2064
|
float physicalMinZoom = 1.0f;
|
|
@@ -1835,17 +2070,15 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
1835
2070
|
physicalMaxZoom = zoomRange.getUpper();
|
|
1836
2071
|
}
|
|
1837
2072
|
}
|
|
1838
|
-
|
|
1839
|
-
String
|
|
2073
|
+
float physicalFocalLength = (focalLengths != null && focalLengths.length > 0) ? focalLengths[0] : 4.25f;
|
|
2074
|
+
String physicalLabel = "Physical " + physicalDeviceType + " (" + position + ")";
|
|
1840
2075
|
List<LensInfo> physicalLenses = new ArrayList<>();
|
|
1841
|
-
physicalLenses.add(
|
|
1842
|
-
new LensInfo(focalLengths != null ? focalLengths[0] : 4.25f, deviceType, 1.0f, physicalMaxZoom)
|
|
1843
|
-
);
|
|
2076
|
+
physicalLenses.add(new LensInfo(physicalFocalLength, physicalDeviceType, 1.0f, physicalMaxZoom));
|
|
1844
2077
|
|
|
1845
2078
|
devices.add(
|
|
1846
2079
|
new app.capgo.capacitor.camera.preview.model.CameraDevice(
|
|
1847
2080
|
physicalId,
|
|
1848
|
-
|
|
2081
|
+
physicalLabel,
|
|
1849
2082
|
position,
|
|
1850
2083
|
physicalLenses,
|
|
1851
2084
|
physicalMinZoom,
|
|
@@ -1853,13 +2086,15 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
1853
2086
|
false
|
|
1854
2087
|
)
|
|
1855
2088
|
);
|
|
1856
|
-
Log.d(TAG, "
|
|
2089
|
+
Log.d(TAG, "Added physical camera: " + physicalId + " (" + physicalDeviceType + ")");
|
|
1857
2090
|
} catch (CameraAccessException e) {
|
|
1858
2091
|
Log.e(TAG, "Failed to access characteristics for physical camera " + physicalId, e);
|
|
1859
2092
|
}
|
|
1860
2093
|
}
|
|
1861
2094
|
}
|
|
1862
2095
|
}
|
|
2096
|
+
|
|
2097
|
+
Log.d(TAG, "=== Enumeration Complete: " + devices.size() + " cameras ===");
|
|
1863
2098
|
return devices;
|
|
1864
2099
|
} catch (Exception e) {
|
|
1865
2100
|
Log.e(TAG, "getAvailableDevicesStatic: Error getting devices", e);
|
|
@@ -1975,18 +2210,21 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
1975
2210
|
currentFocusFuture.cancel(true);
|
|
1976
2211
|
}
|
|
1977
2212
|
|
|
1978
|
-
//
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
2213
|
+
//If locked don't auto adjust exposure
|
|
2214
|
+
if (!"LOCK".equals(currentExposureMode)) {
|
|
2215
|
+
// Reset exposure compensation to 0 on tap-to-focus
|
|
2216
|
+
try {
|
|
2217
|
+
ExposureState state = camera.getCameraInfo().getExposureState();
|
|
2218
|
+
Range<Integer> range = state.getExposureCompensationRange();
|
|
2219
|
+
int zeroIdx = 0;
|
|
2220
|
+
if (!range.contains(0)) {
|
|
2221
|
+
// Choose the closest index to 0 if 0 is not available
|
|
2222
|
+
zeroIdx = Math.abs(range.getLower()) < Math.abs(range.getUpper()) ? range.getLower() : range.getUpper();
|
|
2223
|
+
}
|
|
2224
|
+
camera.getCameraControl().setExposureCompensationIndex(zeroIdx);
|
|
2225
|
+
} catch (Exception e) {
|
|
2226
|
+
Log.w(TAG, "setFocus: Failed to reset exposure compensation to 0", e);
|
|
1986
2227
|
}
|
|
1987
|
-
camera.getCameraControl().setExposureCompensationIndex(zeroIdx);
|
|
1988
|
-
} catch (Exception e) {
|
|
1989
|
-
Log.w(TAG, "setFocus: Failed to reset exposure compensation to 0", e);
|
|
1990
2228
|
}
|
|
1991
2229
|
|
|
1992
2230
|
int viewWidth = previewView.getWidth();
|
|
@@ -2458,40 +2696,60 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
2458
2696
|
|
|
2459
2697
|
@OptIn(markerClass = ExperimentalCamera2Interop.class)
|
|
2460
2698
|
public void switchToDevice(String deviceId) {
|
|
2699
|
+
Log.d(TAG, "======================== SWITCH TO DEVICE ========================");
|
|
2461
2700
|
Log.d(TAG, "switchToDevice: Attempting to switch to device " + deviceId);
|
|
2462
2701
|
|
|
2463
2702
|
mainExecutor.execute(() -> {
|
|
2464
2703
|
try {
|
|
2465
2704
|
// Standard physical device selection logic...
|
|
2466
2705
|
List<CameraInfo> cameraInfos = cameraProvider.getAvailableCameraInfos();
|
|
2706
|
+
|
|
2467
2707
|
CameraInfo targetCameraInfo = null;
|
|
2468
2708
|
for (CameraInfo cameraInfo : cameraInfos) {
|
|
2469
|
-
|
|
2709
|
+
String id = Camera2CameraInfo.from(cameraInfo).getCameraId();
|
|
2710
|
+
if (deviceId.equals(id)) {
|
|
2470
2711
|
targetCameraInfo = cameraInfo;
|
|
2471
2712
|
break;
|
|
2472
2713
|
}
|
|
2473
2714
|
}
|
|
2474
2715
|
|
|
2475
2716
|
if (targetCameraInfo != null) {
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
.
|
|
2487
|
-
|
|
2488
|
-
|
|
2717
|
+
// Determine position from the target camera
|
|
2718
|
+
String position = isBackCamera(targetCameraInfo) ? "rear" : "front";
|
|
2719
|
+
boolean wasCentered = sessionConfig.isCentered();
|
|
2720
|
+
|
|
2721
|
+
// Update sessionConfig with the new device ID
|
|
2722
|
+
sessionConfig = new CameraSessionConfiguration(
|
|
2723
|
+
deviceId,
|
|
2724
|
+
position,
|
|
2725
|
+
sessionConfig.getX(),
|
|
2726
|
+
sessionConfig.getY(),
|
|
2727
|
+
sessionConfig.getWidth(),
|
|
2728
|
+
sessionConfig.getHeight(),
|
|
2729
|
+
sessionConfig.getPaddingBottom(),
|
|
2730
|
+
sessionConfig.getToBack(),
|
|
2731
|
+
sessionConfig.getStoreToFile(),
|
|
2732
|
+
sessionConfig.getEnableOpacity(),
|
|
2733
|
+
sessionConfig.getDisableExifHeaderStripping(),
|
|
2734
|
+
sessionConfig.getDisableAudio(),
|
|
2735
|
+
sessionConfig.getZoomFactor(),
|
|
2736
|
+
sessionConfig.getAspectRatio(),
|
|
2737
|
+
sessionConfig.getGridMode(),
|
|
2738
|
+
sessionConfig.getDisableFocusIndicator(),
|
|
2739
|
+
sessionConfig.isVideoModeEnabled()
|
|
2740
|
+
);
|
|
2741
|
+
|
|
2742
|
+
sessionConfig.setCentered(wasCentered);
|
|
2743
|
+
|
|
2744
|
+
Log.d(TAG, "switchToDevice: Updated sessionConfig with deviceId: " + deviceId);
|
|
2745
|
+
bindCameraUseCases(); // Will now use deviceId from sessionConfig
|
|
2489
2746
|
} else {
|
|
2490
2747
|
Log.e(TAG, "switchToDevice: Could not find any CameraInfo matching deviceId: " + deviceId);
|
|
2491
2748
|
}
|
|
2492
2749
|
} catch (Exception e) {
|
|
2493
2750
|
Log.e(TAG, "switchToDevice: Error switching camera", e);
|
|
2494
2751
|
}
|
|
2752
|
+
Log.d(TAG, "================================================================");
|
|
2495
2753
|
});
|
|
2496
2754
|
}
|
|
2497
2755
|
|
|
@@ -3447,6 +3705,11 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
|
|
|
3447
3705
|
throw new Exception("Video recording is already in progress");
|
|
3448
3706
|
}
|
|
3449
3707
|
|
|
3708
|
+
// Update video capture rotation from accelerometer for device orientation
|
|
3709
|
+
int rotation = getRotationFromAccelerometer();
|
|
3710
|
+
videoCapture.setTargetRotation(rotation);
|
|
3711
|
+
Log.d(TAG, "startRecordVideo: Using rotation " + rotation + " from accelerometer");
|
|
3712
|
+
|
|
3450
3713
|
// Create output file
|
|
3451
3714
|
String fileName = "video_" + System.currentTimeMillis() + ".mp4";
|
|
3452
3715
|
File outputDir = new File(context.getExternalFilesDir(Environment.DIRECTORY_MOVIES), "CameraPreview");
|
|
@@ -2,6 +2,7 @@ import AVFoundation
|
|
|
2
2
|
import UIKit
|
|
3
3
|
import CoreLocation
|
|
4
4
|
import UniformTypeIdentifiers
|
|
5
|
+
import CoreMotion
|
|
5
6
|
|
|
6
7
|
class CameraController: NSObject {
|
|
7
8
|
private func getVideoOrientation() -> AVCaptureVideoOrientation {
|
|
@@ -37,6 +38,24 @@ class CameraController: NSObject {
|
|
|
37
38
|
return orientation
|
|
38
39
|
}
|
|
39
40
|
|
|
41
|
+
// For capture only - uses accelerometer to detect physical orientation to properly position videos/images
|
|
42
|
+
private func getPhysicalOrientation() -> AVCaptureVideoOrientation {
|
|
43
|
+
guard let accelerometerData = motionManager.accelerometerData else {
|
|
44
|
+
return lastCaptureOrientation ?? getVideoOrientation() // Fallback to interface in case of accelerometer fail
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
let x = accelerometerData.acceleration.x
|
|
48
|
+
let y = accelerometerData.acceleration.y
|
|
49
|
+
|
|
50
|
+
if abs(x) > abs(y) {
|
|
51
|
+
// Landscape
|
|
52
|
+
return x > 0 ? .landscapeLeft : .landscapeRight
|
|
53
|
+
} else {
|
|
54
|
+
// Portrait
|
|
55
|
+
return y > 0 ? .portraitUpsideDown : .portrait
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
40
59
|
var captureSession: AVCaptureSession?
|
|
41
60
|
var disableFocusIndicator: Bool = false
|
|
42
61
|
|
|
@@ -74,6 +93,8 @@ class CameraController: NSObject {
|
|
|
74
93
|
var zoomFactor: CGFloat = 1.0
|
|
75
94
|
private var lastZoomUpdateTime: TimeInterval = 0
|
|
76
95
|
private let zoomUpdateThrottle: TimeInterval = 1.0 / 60.0 // 60 FPS max
|
|
96
|
+
private let motionManager = CMMotionManager()
|
|
97
|
+
private var lastCaptureOrientation: AVCaptureVideoOrientation?
|
|
77
98
|
|
|
78
99
|
var videoFileURL: URL?
|
|
79
100
|
private let saneMaxZoomFactor: CGFloat = 25.5
|
|
@@ -338,6 +359,16 @@ extension CameraController {
|
|
|
338
359
|
return
|
|
339
360
|
}
|
|
340
361
|
|
|
362
|
+
// Start accelerometer
|
|
363
|
+
var startedAccelerometer = false
|
|
364
|
+
if self.motionManager.isAccelerometerAvailable {
|
|
365
|
+
self.motionManager.accelerometerUpdateInterval = 1.0 / 60.0
|
|
366
|
+
if !self.motionManager.isAccelerometerActive {
|
|
367
|
+
self.motionManager.startAccelerometerUpdates()
|
|
368
|
+
startedAccelerometer = true
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
341
372
|
do {
|
|
342
373
|
// Create session if needed
|
|
343
374
|
if self.captureSession == nil {
|
|
@@ -426,6 +457,9 @@ extension CameraController {
|
|
|
426
457
|
completionHandler(nil)
|
|
427
458
|
}
|
|
428
459
|
} catch {
|
|
460
|
+
if startedAccelerometer {
|
|
461
|
+
self.motionManager.stopAccelerometerUpdates()
|
|
462
|
+
}
|
|
429
463
|
DispatchQueue.main.async {
|
|
430
464
|
completionHandler(error)
|
|
431
465
|
}
|
|
@@ -900,6 +934,12 @@ extension CameraController {
|
|
|
900
934
|
return
|
|
901
935
|
}
|
|
902
936
|
|
|
937
|
+
// Make sure capture is getting the physical orientation not interface orientation
|
|
938
|
+
if let connection = photoOutput.connection(with: .video) {
|
|
939
|
+
let captureOrientation = self.getPhysicalOrientation()
|
|
940
|
+
self.lastCaptureOrientation = captureOrientation
|
|
941
|
+
connection.videoOrientation = captureOrientation
|
|
942
|
+
}
|
|
903
943
|
let settings = AVCapturePhotoSettings()
|
|
904
944
|
// Configure photo capture settings optimized for speed
|
|
905
945
|
// Only use high res if explicitly requesting large dimensions
|
|
@@ -1285,11 +1325,28 @@ extension CameraController {
|
|
|
1285
1325
|
}
|
|
1286
1326
|
|
|
1287
1327
|
func cropImageToAspectRatio(image: UIImage, aspectRatio: String) -> UIImage? {
|
|
1288
|
-
|
|
1328
|
+
let components = aspectRatio.split(separator: ":").compactMap {Float(String($0))}
|
|
1329
|
+
guard components.count == 2 else {
|
|
1289
1330
|
print("[CameraPreview] cropImageToAspectRatio - Failed to parse aspect ratio: \(aspectRatio)")
|
|
1290
1331
|
return image
|
|
1291
1332
|
}
|
|
1292
1333
|
|
|
1334
|
+
// Use physical orientation for capture works with portrait lock
|
|
1335
|
+
let orientation = self.lastCaptureOrientation ?? self.getPhysicalOrientation()
|
|
1336
|
+
let isPortrait = (orientation == .portrait || orientation == .portraitUpsideDown)
|
|
1337
|
+
|
|
1338
|
+
let ratioWidth: CGFloat
|
|
1339
|
+
let ratioHeight: CGFloat
|
|
1340
|
+
if isPortrait {
|
|
1341
|
+
// For portrait 4:3 becomes 3:4, 16:9 becomes 9:16
|
|
1342
|
+
ratioWidth = CGFloat(components[1])
|
|
1343
|
+
ratioHeight = CGFloat(components[0])
|
|
1344
|
+
} else {
|
|
1345
|
+
// For landscape keep original
|
|
1346
|
+
ratioWidth = CGFloat(components[0])
|
|
1347
|
+
ratioHeight = CGFloat(components[1])
|
|
1348
|
+
}
|
|
1349
|
+
|
|
1293
1350
|
// Only normalize the image orientation if it's not already correct
|
|
1294
1351
|
let normalizedImage: UIImage
|
|
1295
1352
|
if image.imageOrientation == .up {
|
|
@@ -1302,10 +1359,10 @@ extension CameraController {
|
|
|
1302
1359
|
|
|
1303
1360
|
let imageSize = normalizedImage.size
|
|
1304
1361
|
let imageAspectRatio = imageSize.width / imageSize.height
|
|
1305
|
-
let targetAspectRatio =
|
|
1362
|
+
let targetAspectRatio = ratioWidth / ratioHeight
|
|
1306
1363
|
|
|
1307
1364
|
print("[CameraPreview] cropImageToAspectRatio - Original image: \(imageSize.width)x\(imageSize.height) (ratio: \(imageAspectRatio))")
|
|
1308
|
-
print("[CameraPreview] cropImageToAspectRatio - Target ratio: \(
|
|
1365
|
+
print("[CameraPreview] cropImageToAspectRatio - Target ratio: \(ratioWidth):\(ratioHeight) (ratio: \(targetAspectRatio))")
|
|
1309
1366
|
|
|
1310
1367
|
var cropRect: CGRect
|
|
1311
1368
|
|
|
@@ -1698,11 +1755,15 @@ extension CameraController {
|
|
|
1698
1755
|
// Set the focus point
|
|
1699
1756
|
device.focusPointOfInterest = point
|
|
1700
1757
|
|
|
1701
|
-
//
|
|
1702
|
-
if device.
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
device.
|
|
1758
|
+
// Skip exposure point if exposure locked
|
|
1759
|
+
if device.exposureMode != .locked {
|
|
1760
|
+
|
|
1761
|
+
// Also set exposure point if supported
|
|
1762
|
+
if device.isExposurePointOfInterestSupported && device.isExposureModeSupported(.autoExpose) {
|
|
1763
|
+
device.exposureMode = .autoExpose
|
|
1764
|
+
device.setExposureTargetBias(0.0) { _ in }
|
|
1765
|
+
device.exposurePointOfInterest = point
|
|
1766
|
+
}
|
|
1706
1767
|
}
|
|
1707
1768
|
|
|
1708
1769
|
device.unlockForConfiguration()
|
|
@@ -1878,6 +1939,7 @@ extension CameraController {
|
|
|
1878
1939
|
captureSession.outputs.forEach { captureSession.removeOutput($0) }
|
|
1879
1940
|
}
|
|
1880
1941
|
|
|
1942
|
+
self.motionManager.stopAccelerometerUpdates()
|
|
1881
1943
|
self.previewLayer?.removeFromSuperlayer()
|
|
1882
1944
|
self.previewLayer = nil
|
|
1883
1945
|
|
|
@@ -2103,18 +2165,8 @@ extension CameraController {
|
|
|
2103
2165
|
//
|
|
2104
2166
|
if let connection = fileVideoOutput.connection(with: .video) {
|
|
2105
2167
|
if connection.isEnabled == false { connection.isEnabled = true }
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
connection.videoOrientation = .landscapeLeft
|
|
2109
|
-
case .landscapeLeft:
|
|
2110
|
-
connection.videoOrientation = .landscapeRight
|
|
2111
|
-
case .portrait:
|
|
2112
|
-
connection.videoOrientation = .portrait
|
|
2113
|
-
case .portraitUpsideDown:
|
|
2114
|
-
connection.videoOrientation = .portraitUpsideDown
|
|
2115
|
-
default:
|
|
2116
|
-
connection.videoOrientation = .portrait
|
|
2117
|
-
}
|
|
2168
|
+
// Goes off accelerometer now
|
|
2169
|
+
connection.videoOrientation = self.getPhysicalOrientation()
|
|
2118
2170
|
}
|
|
2119
2171
|
|
|
2120
2172
|
let identifier = UUID()
|
|
@@ -2176,12 +2228,14 @@ extension CameraController: UIGestureRecognizerDelegate {
|
|
|
2176
2228
|
device.focusPointOfInterest = CGPoint(x: CGFloat(devicePoint?.x ?? 0), y: CGFloat(devicePoint?.y ?? 0))
|
|
2177
2229
|
device.focusMode = focusMode
|
|
2178
2230
|
}
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
device.
|
|
2183
|
-
|
|
2184
|
-
|
|
2231
|
+
// Skip exposure point if locked
|
|
2232
|
+
if device.exposureMode != .locked {
|
|
2233
|
+
let exposureMode = AVCaptureDevice.ExposureMode.autoExpose
|
|
2234
|
+
if device.isExposurePointOfInterestSupported && device.isExposureModeSupported(exposureMode) {
|
|
2235
|
+
device.exposurePointOfInterest = CGPoint(x: CGFloat(devicePoint?.x ?? 0), y: CGFloat(devicePoint?.y ?? 0))
|
|
2236
|
+
device.exposureMode = exposureMode
|
|
2237
|
+
device.setExposureTargetBias(0.0) { _ in }
|
|
2238
|
+
}
|
|
2185
2239
|
}
|
|
2186
2240
|
} catch {
|
|
2187
2241
|
debugPrint(error)
|
|
@@ -34,7 +34,7 @@ extension UIWindow {
|
|
|
34
34
|
*/
|
|
35
35
|
@objc(CameraPreview)
|
|
36
36
|
public class CameraPreview: CAPPlugin, CAPBridgedPlugin, CLLocationManagerDelegate {
|
|
37
|
-
private let pluginVersion: String = "8.0.
|
|
37
|
+
private let pluginVersion: String = "8.0.10"
|
|
38
38
|
public let identifier = "CameraPreviewPlugin"
|
|
39
39
|
public let jsName = "CameraPreview"
|
|
40
40
|
public let pluginMethods: [CAPPluginMethod] = [
|
|
@@ -83,6 +83,7 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin, CLLocationManagerDelega
|
|
|
83
83
|
private var isInitializing: Bool = false
|
|
84
84
|
private var isInitialized: Bool = false
|
|
85
85
|
private var backgroundSession: AVCaptureSession?
|
|
86
|
+
private var isGeneratingDeviceOrientationNotifications: Bool = false
|
|
86
87
|
|
|
87
88
|
var previewView: UIView!
|
|
88
89
|
var cameraPosition = String()
|
|
@@ -679,7 +680,10 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin, CLLocationManagerDelega
|
|
|
679
680
|
self.cameraController.stopRequestedAfterCapture = false
|
|
680
681
|
self.cameraController.cleanup()
|
|
681
682
|
NotificationCenter.default.removeObserver(self, name: UIDevice.orientationDidChangeNotification, object: nil)
|
|
682
|
-
|
|
683
|
+
if self.isGeneratingDeviceOrientationNotifications {
|
|
684
|
+
UIDevice.current.endGeneratingDeviceOrientationNotifications()
|
|
685
|
+
self.isGeneratingDeviceOrientationNotifications = false
|
|
686
|
+
}
|
|
683
687
|
self.isInitialized = false
|
|
684
688
|
self.isInitializing = false
|
|
685
689
|
}
|
|
@@ -779,11 +783,14 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin, CLLocationManagerDelega
|
|
|
779
783
|
}
|
|
780
784
|
|
|
781
785
|
DispatchQueue.main.async {
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
786
|
+
if self.rotateWhenOrientationChanged == true {
|
|
787
|
+
UIDevice.current.beginGeneratingDeviceOrientationNotifications()
|
|
788
|
+
self.isGeneratingDeviceOrientationNotifications = true
|
|
789
|
+
NotificationCenter.default.addObserver(self,
|
|
790
|
+
selector: #selector(self.handleOrientationChange),
|
|
791
|
+
name: UIDevice.orientationDidChangeNotification,
|
|
792
|
+
object: nil)
|
|
793
|
+
}
|
|
787
794
|
self.completeStartCamera(call: call)
|
|
788
795
|
}
|
|
789
796
|
}
|
|
@@ -970,7 +977,10 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin, CLLocationManagerDelega
|
|
|
970
977
|
// Remove notification observers regardless
|
|
971
978
|
NotificationCenter.default.removeObserver(self)
|
|
972
979
|
NotificationCenter.default.removeObserver(self, name: UIDevice.orientationDidChangeNotification, object: nil)
|
|
973
|
-
|
|
980
|
+
if self.isGeneratingDeviceOrientationNotifications {
|
|
981
|
+
UIDevice.current.endGeneratingDeviceOrientationNotifications()
|
|
982
|
+
self.isGeneratingDeviceOrientationNotifications = false
|
|
983
|
+
}
|
|
974
984
|
|
|
975
985
|
if self.cameraController.isCapturingPhoto && !force {
|
|
976
986
|
// Defer heavy cleanup until capture callback completes (only if not forcing)
|