@capgo/camera-preview 7.7.0 → 7.8.1-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -53,13 +53,13 @@ dependencies {
53
53
  implementation 'androidx.coordinatorlayout:coordinatorlayout:1.3.0'
54
54
 
55
55
  // CameraX dependencies
56
- def camerax_version = "1.5.0-beta01"
56
+ def camerax_version = "1.5.0-rc01"
57
57
  implementation "androidx.camera:camera-core:${camerax_version}"
58
58
  implementation "androidx.camera:camera-camera2:${camerax_version}"
59
59
  implementation "androidx.camera:camera-lifecycle:${camerax_version}"
60
60
  implementation "androidx.camera:camera-view:${camerax_version}"
61
61
  implementation "androidx.camera:camera-extensions:${camerax_version}"
62
-
62
+
63
63
  testImplementation "junit:junit:$junitVersion"
64
64
  androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion"
65
65
  androidTestImplementation "androidx.test.espresso:espresso-core:$androidxEspressoCoreVersion"
@@ -80,6 +80,7 @@ public class CameraPreview
80
80
  private Location lastLocation;
81
81
  private OrientationEventListener orientationListener;
82
82
  private int lastOrientation = Configuration.ORIENTATION_UNDEFINED;
83
+ private boolean lastDisableAudio = true;
83
84
 
84
85
  @PluginMethod
85
86
  public void getExposureModes(PluginCall call) {
@@ -689,6 +690,7 @@ public class CameraPreview
689
690
  final boolean disableAudio = Boolean.TRUE.equals(
690
691
  call.getBoolean("disableAudio", true)
691
692
  );
693
+ this.lastDisableAudio = disableAudio;
692
694
  final String aspectRatio = call.getString("aspectRatio", "4:3");
693
695
  final String gridMode = call.getString("gridMode", "none");
694
696
  final String positioning = call.getString("positioning", "top");
@@ -1766,6 +1768,101 @@ public class CameraPreview
1766
1768
  }
1767
1769
  }
1768
1770
 
1771
+ @PluginMethod
1772
+ public void startRecordVideo(PluginCall call) {
1773
+ if (cameraXView == null || !cameraXView.isRunning()) {
1774
+ call.reject("Camera is not running");
1775
+ return;
1776
+ }
1777
+
1778
+ boolean disableAudio = call.getBoolean("disableAudio") != null
1779
+ ? Boolean.TRUE.equals(call.getBoolean("disableAudio"))
1780
+ : this.lastDisableAudio;
1781
+ String permissionAlias = disableAudio
1782
+ ? CAMERA_ONLY_PERMISSION_ALIAS
1783
+ : CAMERA_WITH_AUDIO_PERMISSION_ALIAS;
1784
+
1785
+ if (PermissionState.GRANTED.equals(getPermissionState(permissionAlias))) {
1786
+ try {
1787
+ cameraXView.startRecordVideo();
1788
+ call.resolve();
1789
+ } catch (Exception e) {
1790
+ call.reject("Failed to start video recording: " + e.getMessage());
1791
+ }
1792
+ } else {
1793
+ requestPermissionForAlias(
1794
+ permissionAlias,
1795
+ call,
1796
+ "handleVideoRecordingPermissionResult"
1797
+ );
1798
+ }
1799
+ }
1800
+
1801
+ @PluginMethod
1802
+ public void stopRecordVideo(PluginCall call) {
1803
+ if (cameraXView == null || !cameraXView.isRunning()) {
1804
+ call.reject("Camera is not running");
1805
+ return;
1806
+ }
1807
+
1808
+ try {
1809
+ bridge.saveCall(call);
1810
+ final String cbId = call.getCallbackId();
1811
+ cameraXView.stopRecordVideo(
1812
+ new CameraXView.VideoRecordingCallback() {
1813
+ @Override
1814
+ public void onSuccess(String filePath) {
1815
+ PluginCall saved = bridge.getSavedCall(cbId);
1816
+ if (saved != null) {
1817
+ JSObject result = new JSObject();
1818
+ result.put("videoFilePath", filePath);
1819
+ saved.resolve(result);
1820
+ bridge.releaseCall(saved);
1821
+ }
1822
+ }
1823
+
1824
+ @Override
1825
+ public void onError(String message) {
1826
+ PluginCall saved = bridge.getSavedCall(cbId);
1827
+ if (saved != null) {
1828
+ saved.reject("Failed to stop video recording: " + message);
1829
+ bridge.releaseCall(saved);
1830
+ }
1831
+ }
1832
+ }
1833
+ );
1834
+ } catch (Exception e) {
1835
+ call.reject("Failed to stop video recording: " + e.getMessage());
1836
+ }
1837
+ }
1838
+
1839
+ @PermissionCallback
1840
+ private void handleVideoRecordingPermissionResult(PluginCall call) {
1841
+ // Use the persisted session value to determine which permission we requested
1842
+ String permissionAlias = this.lastDisableAudio
1843
+ ? CAMERA_ONLY_PERMISSION_ALIAS
1844
+ : CAMERA_WITH_AUDIO_PERMISSION_ALIAS;
1845
+
1846
+ // Check if either permission is granted (mirroring handleCameraPermissionResult)
1847
+ if (
1848
+ PermissionState.GRANTED.equals(
1849
+ getPermissionState(CAMERA_ONLY_PERMISSION_ALIAS)
1850
+ ) ||
1851
+ PermissionState.GRANTED.equals(
1852
+ getPermissionState(CAMERA_WITH_AUDIO_PERMISSION_ALIAS)
1853
+ )
1854
+ ) {
1855
+ try {
1856
+ cameraXView.startRecordVideo();
1857
+ call.resolve();
1858
+ } catch (Exception e) {
1859
+ call.reject("Failed to start video recording: " + e.getMessage());
1860
+ }
1861
+ } else {
1862
+ call.reject("Permission denied for video recording");
1863
+ }
1864
+ }
1865
+
1769
1866
  @PluginMethod
1770
1867
  public void getSafeAreaInsets(PluginCall call) {
1771
1868
  JSObject ret = new JSObject();
@@ -56,6 +56,14 @@ import androidx.camera.core.resolutionselector.AspectRatioStrategy;
56
56
  import androidx.camera.core.resolutionselector.ResolutionSelector;
57
57
  import androidx.camera.core.resolutionselector.ResolutionStrategy;
58
58
  import androidx.camera.lifecycle.ProcessCameraProvider;
59
+ import androidx.camera.video.FallbackStrategy;
60
+ import androidx.camera.video.FileOutputOptions;
61
+ import androidx.camera.video.Quality;
62
+ import androidx.camera.video.QualitySelector;
63
+ import androidx.camera.video.Recorder;
64
+ import androidx.camera.video.Recording;
65
+ import androidx.camera.video.VideoCapture;
66
+ import androidx.camera.video.VideoRecordEvent;
59
67
  import androidx.camera.view.PreviewView;
60
68
  import androidx.core.content.ContextCompat;
61
69
  import androidx.exifinterface.media.ExifInterface;
@@ -99,10 +107,19 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
99
107
  void onCameraStartError(String message);
100
108
  }
101
109
 
110
+ public interface VideoRecordingCallback {
111
+ void onSuccess(String filePath);
112
+ void onError(String message);
113
+ }
114
+
102
115
  private ProcessCameraProvider cameraProvider;
103
116
  private Camera camera;
104
117
  private ImageCapture imageCapture;
105
118
  private ImageCapture sampleImageCapture;
119
+ private VideoCapture<Recorder> videoCapture;
120
+ private Recording currentRecording;
121
+ private File currentVideoFile;
122
+ private VideoRecordingCallback currentVideoCallback;
106
123
  private PreviewView previewView;
107
124
  private GridOverlayView gridOverlayView;
108
125
  private FrameLayout previewContainer;
@@ -725,6 +742,17 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
725
742
  .setTargetRotation(rotation)
726
743
  .build();
727
744
  sampleImageCapture = imageCapture;
745
+
746
+ // Setup VideoCapture with rotation and quality fallback
747
+ QualitySelector qualitySelector = QualitySelector.fromOrderedList(
748
+ Arrays.asList(Quality.FHD, Quality.HD, Quality.SD),
749
+ FallbackStrategy.higherQualityOrLowerThan(Quality.FHD)
750
+ );
751
+ Recorder recorder = new Recorder.Builder()
752
+ .setQualitySelector(qualitySelector)
753
+ .build();
754
+ videoCapture = VideoCapture.withOutput(recorder);
755
+
728
756
  preview.setSurfaceProvider(previewView.getSurfaceProvider());
729
757
  // Unbind any existing use cases and bind new ones
730
758
  cameraProvider.unbindAll();
@@ -732,7 +760,8 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
732
760
  this,
733
761
  currentCameraSelector,
734
762
  preview,
735
- imageCapture
763
+ imageCapture,
764
+ videoCapture
736
765
  );
737
766
 
738
767
  // Log details about the active camera
@@ -3702,4 +3731,103 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
3702
3731
  Log.e(TAG, "triggerAutoFocus: Failed to trigger autofocus", e);
3703
3732
  }
3704
3733
  }
3734
+
3735
+ public void startRecordVideo() throws Exception {
3736
+ if (videoCapture == null) {
3737
+ throw new Exception("VideoCapture is not initialized");
3738
+ }
3739
+
3740
+ if (currentRecording != null) {
3741
+ throw new Exception("Video recording is already in progress");
3742
+ }
3743
+
3744
+ // Create output file
3745
+ String fileName = "video_" + System.currentTimeMillis() + ".mp4";
3746
+ File outputDir = new File(
3747
+ context.getExternalFilesDir(Environment.DIRECTORY_MOVIES),
3748
+ "CameraPreview"
3749
+ );
3750
+ if (!outputDir.exists()) {
3751
+ outputDir.mkdirs();
3752
+ }
3753
+ currentVideoFile = new File(outputDir, fileName);
3754
+
3755
+ FileOutputOptions outputOptions = new FileOutputOptions.Builder(
3756
+ currentVideoFile
3757
+ ).build();
3758
+
3759
+ // Create recording event listener
3760
+ androidx.core.util.Consumer<VideoRecordEvent> videoRecordEventListener =
3761
+ videoRecordEvent -> {
3762
+ if (videoRecordEvent instanceof VideoRecordEvent.Start) {
3763
+ Log.d(TAG, "Video recording started");
3764
+ } else if (videoRecordEvent instanceof VideoRecordEvent.Finalize) {
3765
+ VideoRecordEvent.Finalize finalizeEvent =
3766
+ (VideoRecordEvent.Finalize) videoRecordEvent;
3767
+ handleRecordingFinalized(finalizeEvent);
3768
+ }
3769
+ };
3770
+
3771
+ // Start recording
3772
+ if (sessionConfig != null && !sessionConfig.isDisableAudio()) {
3773
+ currentRecording = videoCapture
3774
+ .getOutput()
3775
+ .prepareRecording(context, outputOptions)
3776
+ .withAudioEnabled()
3777
+ .start(
3778
+ ContextCompat.getMainExecutor(context),
3779
+ videoRecordEventListener
3780
+ );
3781
+ } else {
3782
+ currentRecording = videoCapture
3783
+ .getOutput()
3784
+ .prepareRecording(context, outputOptions)
3785
+ .start(
3786
+ ContextCompat.getMainExecutor(context),
3787
+ videoRecordEventListener
3788
+ );
3789
+ }
3790
+
3791
+ Log.d(
3792
+ TAG,
3793
+ "Video recording started to: " + currentVideoFile.getAbsolutePath()
3794
+ );
3795
+ }
3796
+
3797
+ public void stopRecordVideo(VideoRecordingCallback callback) {
3798
+ if (currentRecording == null) {
3799
+ callback.onError("No video recording in progress");
3800
+ return;
3801
+ }
3802
+
3803
+ // Store the callback to use when recording is finalized
3804
+ currentVideoCallback = callback;
3805
+ currentRecording.stop();
3806
+
3807
+ Log.d(TAG, "Video recording stop requested");
3808
+ }
3809
+
3810
+ private void handleRecordingFinalized(
3811
+ VideoRecordEvent.Finalize finalizeEvent
3812
+ ) {
3813
+ if (!finalizeEvent.hasError()) {
3814
+ Log.d(TAG, "Video recording completed successfully");
3815
+ if (currentVideoCallback != null) {
3816
+ String filePath = "file://" + currentVideoFile.getAbsolutePath();
3817
+ currentVideoCallback.onSuccess(filePath);
3818
+ }
3819
+ } else {
3820
+ Log.e(TAG, "Video recording failed: " + finalizeEvent.getError());
3821
+ if (currentVideoCallback != null) {
3822
+ currentVideoCallback.onError(
3823
+ "Video recording failed: " + finalizeEvent.getError()
3824
+ );
3825
+ }
3826
+ }
3827
+
3828
+ // Clean up
3829
+ currentRecording = null;
3830
+ currentVideoFile = null;
3831
+ currentVideoCallback = null;
3832
+ }
3705
3833
  }
@@ -100,7 +100,48 @@ class CameraController: NSObject {
100
100
  private func parseAspectRatio(_ aspectRatio: String) -> (width: CGFloat, height: CGFloat)? {
101
101
  let components = aspectRatio.split(separator: ":").compactMap { Float(String($0)) }
102
102
  guard components.count == 2 else { return nil }
103
- return (width: CGFloat(components[0]), height: CGFloat(components[1]))
103
+
104
+ // Check if device is in portrait orientation by looking at the current interface orientation
105
+ var isPortrait = false
106
+ if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene {
107
+ print("[CameraPreview] parseAspectRatio - windowScene.interfaceOrientation: \(windowScene.interfaceOrientation)")
108
+ switch windowScene.interfaceOrientation {
109
+ case .portrait, .portraitUpsideDown:
110
+ isPortrait = true
111
+ case .landscapeLeft, .landscapeRight:
112
+ isPortrait = false
113
+ case .unknown:
114
+ // Fallback to device orientation
115
+ isPortrait = UIDevice.current.orientation.isPortrait
116
+ @unknown default:
117
+ isPortrait = UIDevice.current.orientation.isPortrait
118
+ }
119
+ } else {
120
+ // Fallback to device orientation
121
+ isPortrait = UIDevice.current.orientation.isPortrait
122
+ }
123
+
124
+ let originalWidth = CGFloat(components[0])
125
+ let originalHeight = CGFloat(components[1])
126
+ print("[CameraPreview] parseAspectRatio - isPortrait: \(isPortrait) originalWidth: \(originalWidth) originalHeight: \(originalHeight)")
127
+
128
+ let finalWidth: CGFloat
129
+ let finalHeight: CGFloat
130
+
131
+ if isPortrait {
132
+ // For portrait mode, swap width and height to maintain portrait orientation
133
+ // 4:3 becomes 3:4, 16:9 becomes 9:16
134
+ finalWidth = originalHeight
135
+ finalHeight = originalWidth
136
+ print("[CameraPreview] parseAspectRatio - Portrait mode: \(aspectRatio) -> \(finalWidth):\(finalHeight) (ratio: \(finalWidth/finalHeight))")
137
+ } else {
138
+ // For landscape mode, keep original orientation
139
+ finalWidth = originalWidth
140
+ finalHeight = originalHeight
141
+ print("[CameraPreview] parseAspectRatio - Landscape mode: \(aspectRatio) -> \(finalWidth):\(finalHeight) (ratio: \(finalWidth/finalHeight))")
142
+ }
143
+
144
+ return (width: finalWidth, height: finalHeight)
104
145
  }
105
146
  }
106
147
 
@@ -340,6 +381,38 @@ extension CameraController {
340
381
  }
341
382
  }
342
383
 
384
+ /// Update the requested aspect ratio at runtime and reconfigure session/preview accordingly
385
+ func updateAspectRatio(_ aspectRatio: String?) {
386
+ // Update internal state
387
+ self.requestedAspectRatio = aspectRatio
388
+
389
+ // Reconfigure session preset to match the new ratio for optimal capture resolution
390
+ if let captureSession = self.captureSession {
391
+ captureSession.beginConfiguration()
392
+ self.configureSessionPreset(for: aspectRatio)
393
+ captureSession.commitConfiguration()
394
+ }
395
+
396
+ // Update preview layer geometry on the main thread
397
+ DispatchQueue.main.async { [weak self] in
398
+ guard let self = self, let previewLayer = self.previewLayer else { return }
399
+ if let superlayer = previewLayer.superlayer {
400
+ let bounds = superlayer.bounds
401
+ if let aspect = aspectRatio {
402
+ let frame = self.calculateAspectRatioFrame(for: aspect, in: bounds)
403
+ previewLayer.frame = frame
404
+ previewLayer.videoGravity = .resizeAspectFill
405
+ } else {
406
+ previewLayer.frame = bounds
407
+ previewLayer.videoGravity = .resizeAspect
408
+ }
409
+
410
+ // Keep grid overlay in sync with preview
411
+ self.gridOverlayView?.frame = previewLayer.frame
412
+ }
413
+ }
414
+ }
415
+
343
416
  private func setInitialZoom(level: Float?) {
344
417
  let device = (currentCameraPosition == .rear) ? rearCamera : frontCamera
345
418
  guard let device = device else {
@@ -544,7 +617,7 @@ extension CameraController {
544
617
  }
545
618
 
546
619
  private func updateVideoOrientationOnMainThread() {
547
- let videoOrientation: AVCaptureVideoOrientation
620
+ var videoOrientation: AVCaptureVideoOrientation
548
621
 
549
622
  // Use window scene interface orientation
550
623
  if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene {
@@ -695,7 +768,7 @@ extension CameraController {
695
768
  }
696
769
 
697
770
  func captureImage(width: Int?, height: Int?, quality: Float, gpsLocation: CLLocation?, completion: @escaping (UIImage?, Data?, [AnyHashable: Any]?, Error?) -> Void) {
698
- print("[CameraPreview] captureImage called - width: \(width ?? -1), height: \(height ?? -1)")
771
+ print("[CameraPreview] captureImage called - width: \(width ?? -1), height: \(height ?? -1), requestedAspectRatio: \(self.requestedAspectRatio ?? "nil")")
699
772
 
700
773
  guard let photoOutput = self.photoOutput else {
701
774
  completion(nil, nil, nil, NSError(domain: "Camera", code: 0, userInfo: [NSLocalizedDescriptionKey: "Photo output is not available"]))
@@ -764,18 +837,9 @@ extension CameraController {
764
837
  print("[CameraPreview] Resized to max dimensions: \(finalImage.size.width)x\(finalImage.size.height)")
765
838
  } else if let aspectRatio = self.requestedAspectRatio {
766
839
  // No max dimensions specified, but aspect ratio is specified
767
- // If we used session preset (low-res), image should already be correct aspect ratio
768
- // If we used high-res (shouldn't happen without max dimensions), crop it
769
- let imageAspectRatio = image.size.width / image.size.height
770
- if let targetRatio = self.parseAspectRatio(aspectRatio) {
771
- let targetAspectRatio = targetRatio.width / targetRatio.height
772
-
773
- // Allow small tolerance for aspect ratio comparison
774
- if abs(imageAspectRatio - targetAspectRatio) > 0.01 {
775
- finalImage = self.cropImageToAspectRatio(image: image, aspectRatio: aspectRatio) ?? image
776
- print("[CameraPreview] Cropped to match aspect ratio \(aspectRatio): \(finalImage.size.width)x\(finalImage.size.height)")
777
- }
778
- }
840
+ // Always apply aspect ratio cropping to ensure correct orientation
841
+ finalImage = self.cropImageToAspectRatio(image: image, aspectRatio: aspectRatio) ?? image
842
+ print("[CameraPreview] Applied aspect ratio cropping for \(aspectRatio): \(finalImage.size.width)x\(finalImage.size.height)")
779
843
  }
780
844
 
781
845
  completion(finalImage, photoData, metadata, nil)
@@ -857,13 +921,27 @@ extension CameraController {
857
921
 
858
922
  func cropImageToAspectRatio(image: UIImage, aspectRatio: String) -> UIImage? {
859
923
  guard let ratio = parseAspectRatio(aspectRatio) else {
924
+ print("[CameraPreview] cropImageToAspectRatio - Failed to parse aspect ratio: \(aspectRatio)")
860
925
  return image
861
926
  }
862
927
 
863
- let imageSize = image.size
928
+ // Only normalize the image orientation if it's not already correct
929
+ let normalizedImage: UIImage
930
+ if image.imageOrientation == .up {
931
+ normalizedImage = image
932
+ print("[CameraPreview] cropImageToAspectRatio - Image already has correct orientation")
933
+ } else {
934
+ normalizedImage = image.fixedOrientation() ?? image
935
+ print("[CameraPreview] cropImageToAspectRatio - Normalized image orientation from \(image.imageOrientation.rawValue) to .up")
936
+ }
937
+
938
+ let imageSize = normalizedImage.size
864
939
  let imageAspectRatio = imageSize.width / imageSize.height
865
940
  let targetAspectRatio = ratio.width / ratio.height
866
941
 
942
+ print("[CameraPreview] cropImageToAspectRatio - Original image: \(imageSize.width)x\(imageSize.height) (ratio: \(imageAspectRatio))")
943
+ print("[CameraPreview] cropImageToAspectRatio - Target ratio: \(ratio.width):\(ratio.height) (ratio: \(targetAspectRatio))")
944
+
867
945
  var cropRect: CGRect
868
946
 
869
947
  if imageAspectRatio > targetAspectRatio {
@@ -871,19 +949,36 @@ extension CameraController {
871
949
  let targetWidth = imageSize.height * targetAspectRatio
872
950
  let xOffset = (imageSize.width - targetWidth) / 2
873
951
  cropRect = CGRect(x: xOffset, y: 0, width: targetWidth, height: imageSize.height)
952
+ print("[CameraPreview] cropImageToAspectRatio - Horizontal crop: \(cropRect)")
874
953
  } else {
875
954
  // Image is taller than target - crop vertically (center crop)
876
955
  let targetHeight = imageSize.width / targetAspectRatio
877
956
  let yOffset = (imageSize.height - targetHeight) / 2
878
957
  cropRect = CGRect(x: 0, y: yOffset, width: imageSize.width, height: targetHeight)
958
+ print("[CameraPreview] cropImageToAspectRatio - Vertical crop: \(cropRect) - Target height: \(targetHeight)")
879
959
  }
880
960
 
881
- guard let cgImage = image.cgImage,
961
+ // Validate crop rect is within image bounds
962
+ if cropRect.minX < 0 || cropRect.minY < 0 ||
963
+ cropRect.maxX > imageSize.width || cropRect.maxY > imageSize.height {
964
+ print("[CameraPreview] cropImageToAspectRatio - Warning: Crop rect \(cropRect) exceeds image bounds \(imageSize)")
965
+ // Adjust crop rect to fit within image bounds
966
+ cropRect = cropRect.intersection(CGRect(origin: .zero, size: imageSize))
967
+ print("[CameraPreview] cropImageToAspectRatio - Adjusted crop rect: \(cropRect)")
968
+ }
969
+
970
+ guard let cgImage = normalizedImage.cgImage,
882
971
  let croppedCGImage = cgImage.cropping(to: cropRect) else {
972
+ print("[CameraPreview] cropImageToAspectRatio - Failed to crop image")
883
973
  return nil
884
974
  }
885
975
 
886
- return UIImage(cgImage: croppedCGImage, scale: image.scale, orientation: image.imageOrientation)
976
+ let croppedImage = UIImage(cgImage: croppedCGImage, scale: normalizedImage.scale, orientation: .up)
977
+ let finalAspectRatio = croppedImage.size.width / croppedImage.size.height
978
+ print("[CameraPreview] cropImageToAspectRatio - Final cropped image: \(croppedImage.size.width)x\(croppedImage.size.height) (ratio: \(finalAspectRatio))")
979
+
980
+ // Create the cropped image with normalized orientation
981
+ return croppedImage
887
982
  }
888
983
 
889
984
  func cropImageToMatchPreview(image: UIImage, previewLayer: AVCaptureVideoPreviewLayer) -> UIImage? {
@@ -1861,7 +1956,8 @@ extension CameraController: AVCapturePhotoCaptureDelegate {
1861
1956
  }
1862
1957
 
1863
1958
  // Pass through original file data and metadata so callers can preserve EXIF
1864
- self.photoCaptureCompletionBlock?(image.fixedOrientation(), imageData, photo.metadata, nil)
1959
+ // Don't call fixedOrientation() here - let the completion block handle it after cropping
1960
+ self.photoCaptureCompletionBlock?(image, imageData, photo.metadata, nil)
1865
1961
  }
1866
1962
  }
1867
1963
 
@@ -310,6 +310,10 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin, CLLocationManagerDelega
310
310
  }
311
311
 
312
312
  self.aspectRatio = newAspectRatio
313
+
314
+ // Propagate to camera controller so capture output and preview align
315
+ self.cameraController.updateAspectRatio(newAspectRatio)
316
+
313
317
  DispatchQueue.main.async {
314
318
  call.resolve(self.rawSetAspectRatio())
315
319
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@capgo/camera-preview",
3
- "version": "7.7.0",
3
+ "version": "7.8.1-alpha.0",
4
4
  "description": "Camera preview",
5
5
  "license": "MIT",
6
6
  "repository": {