@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.
- package/android/build.gradle +2 -2
- package/android/src/main/java/com/ahm/capacitor/camera/preview/CameraPreview.java +97 -0
- package/android/src/main/java/com/ahm/capacitor/camera/preview/CameraXView.java +129 -1
- package/ios/Sources/CapgoCameraPreviewPlugin/CameraController.swift +115 -19
- package/ios/Sources/CapgoCameraPreviewPlugin/Plugin.swift +4 -0
- package/package.json +1 -1
package/android/build.gradle
CHANGED
|
@@ -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-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
768
|
-
|
|
769
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|