@capgo/camera-preview 7.13.11 → 7.14.3

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/.project CHANGED
@@ -14,4 +14,15 @@
14
14
  <natures>
15
15
  <nature>org.eclipse.buildship.core.gradleprojectnature</nature>
16
16
  </natures>
17
+ <filteredResources>
18
+ <filter>
19
+ <id>1756902493462</id>
20
+ <name></name>
21
+ <type>30</type>
22
+ <matcher>
23
+ <id>org.eclipse.core.resources.regexFilterMatcher</id>
24
+ <arguments>node_modules|\.git|__CREATED_BY_JAVA_LANGUAGE_SERVER__</arguments>
25
+ </matcher>
26
+ </filter>
27
+ </filteredResources>
17
28
  </projectDescription>
@@ -51,6 +51,7 @@ import androidx.camera.core.MeteringPoint;
51
51
  import androidx.camera.core.MeteringPointFactory;
52
52
  import androidx.camera.core.Preview;
53
53
  import androidx.camera.core.ResolutionInfo;
54
+ import androidx.camera.core.TorchState;
54
55
  import androidx.camera.core.UseCase;
55
56
  import androidx.camera.core.ZoomState;
56
57
  import androidx.camera.core.resolutionselector.AspectRatioStrategy;
@@ -2321,7 +2322,8 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
2321
2322
  try {
2322
2323
  boolean hasFlash = camera.getCameraInfo().hasFlashUnit();
2323
2324
  if (hasFlash) {
2324
- return Arrays.asList("off", "on", "auto");
2325
+ // Include torch for devices with a flash unit
2326
+ return Arrays.asList("off", "on", "auto", "torch");
2325
2327
  } else {
2326
2328
  return Collections.singletonList("off");
2327
2329
  }
@@ -2332,6 +2334,16 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
2332
2334
  }
2333
2335
 
2334
2336
  public String getFlashMode() {
2337
+ // If torch is enabled, report torch regardless of ImageCapture flash mode
2338
+ try {
2339
+ if (camera != null) {
2340
+ Integer torch = camera.getCameraInfo().getTorchState().getValue();
2341
+ if (torch != null && torch == TorchState.ON) {
2342
+ return "torch";
2343
+ }
2344
+ }
2345
+ } catch (Exception ignore) {}
2346
+
2335
2347
  switch (currentFlashMode) {
2336
2348
  case ImageCapture.FLASH_MODE_ON:
2337
2349
  return "on";
@@ -2343,6 +2355,35 @@ public class CameraXView implements LifecycleOwner, LifecycleObserver {
2343
2355
  }
2344
2356
 
2345
2357
  public void setFlashMode(String mode) {
2358
+ // Handle torch separately via CameraControl
2359
+ if ("torch".equals(mode)) {
2360
+ try {
2361
+ if (camera != null) {
2362
+ camera.getCameraControl().enableTorch(true);
2363
+ }
2364
+ } catch (Exception e) {
2365
+ Log.e(TAG, "setFlashMode: Failed to enable torch", e);
2366
+ }
2367
+ // Keep ImageCapture flash mode OFF to avoid conflicts with torch
2368
+ currentFlashMode = ImageCapture.FLASH_MODE_OFF;
2369
+ if (imageCapture != null) {
2370
+ imageCapture.setFlashMode(ImageCapture.FLASH_MODE_OFF);
2371
+ }
2372
+ if (sampleImageCapture != null) {
2373
+ sampleImageCapture.setFlashMode(ImageCapture.FLASH_MODE_OFF);
2374
+ }
2375
+ return;
2376
+ }
2377
+
2378
+ // For non-torch modes, ensure torch is disabled
2379
+ try {
2380
+ if (camera != null) {
2381
+ camera.getCameraControl().enableTorch(false);
2382
+ }
2383
+ } catch (Exception e) {
2384
+ Log.w(TAG, "setFlashMode: Failed to disable torch", e);
2385
+ }
2386
+
2346
2387
  int flashMode;
2347
2388
  switch (mode) {
2348
2389
  case "on":
@@ -3,6 +3,39 @@ import UIKit
3
3
  import CoreLocation
4
4
 
5
5
  class CameraController: NSObject {
6
+ private func getVideoOrientation() -> AVCaptureVideoOrientation {
7
+ var orientation: AVCaptureVideoOrientation = .portrait
8
+ if Thread.isMainThread {
9
+ if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene {
10
+ switch windowScene.interfaceOrientation {
11
+ case .portrait: orientation = .portrait
12
+ case .landscapeLeft: orientation = .landscapeLeft
13
+ case .landscapeRight: orientation = .landscapeRight
14
+ case .portraitUpsideDown: orientation = .portraitUpsideDown
15
+ case .unknown: fallthrough
16
+ @unknown default: orientation = .portrait
17
+ }
18
+ }
19
+ } else {
20
+ let semaphore = DispatchSemaphore(value: 0)
21
+ DispatchQueue.main.async {
22
+ if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene {
23
+ switch windowScene.interfaceOrientation {
24
+ case .portrait: orientation = .portrait
25
+ case .landscapeLeft: orientation = .landscapeLeft
26
+ case .landscapeRight: orientation = .landscapeRight
27
+ case .portraitUpsideDown: orientation = .portraitUpsideDown
28
+ case .unknown: fallthrough
29
+ @unknown default: orientation = .portrait
30
+ }
31
+ }
32
+ semaphore.signal()
33
+ }
34
+ _ = semaphore.wait(timeout: .now() + 0.1) // Timeout after 100ms to prevent deadlocks
35
+ }
36
+ return orientation
37
+ }
38
+
6
39
  var captureSession: AVCaptureSession?
7
40
  var disableFocusIndicator: Bool = false
8
41
 
@@ -101,25 +134,9 @@ class CameraController: NSObject {
101
134
  let components = aspectRatio.split(separator: ":").compactMap { Float(String($0)) }
102
135
  guard components.count == 2 else { return nil }
103
136
 
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
- }
137
+ // Get orientation in a thread-safe way
138
+ let orientation = self.getVideoOrientation()
139
+ let isPortrait = (orientation == .portrait || orientation == .portraitUpsideDown)
123
140
 
124
141
  let originalWidth = CGFloat(components[0])
125
142
  let originalHeight = CGFloat(components[1])
@@ -180,7 +197,7 @@ extension CameraController {
180
197
  // Log all found devices for debugging
181
198
 
182
199
  for camera in cameras {
183
- let constituentCount = camera.isVirtualDevice ? camera.constituentDevices.count : 1
200
+ _ = camera.isVirtualDevice ? camera.constituentDevices.count : 1
184
201
 
185
202
  }
186
203
 
@@ -257,20 +274,43 @@ extension CameraController {
257
274
  let layer = AVCaptureVideoPreviewLayer()
258
275
  // Configure orientation immediately
259
276
  if let connection = layer.connection {
260
- if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene {
261
- switch windowScene.interfaceOrientation {
262
- case .portrait:
263
- connection.videoOrientation = .portrait
264
- case .landscapeLeft:
265
- connection.videoOrientation = .landscapeLeft
266
- case .landscapeRight:
267
- connection.videoOrientation = .landscapeRight
268
- case .portraitUpsideDown:
269
- connection.videoOrientation = .portraitUpsideDown
270
- case .unknown:
271
- fallthrough
272
- @unknown default:
273
- connection.videoOrientation = .portrait
277
+ // Ensure UI calls are made on the main thread
278
+ if Thread.isMainThread {
279
+ if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene {
280
+ switch windowScene.interfaceOrientation {
281
+ case .portrait:
282
+ connection.videoOrientation = .portrait
283
+ case .landscapeLeft:
284
+ connection.videoOrientation = .landscapeLeft
285
+ case .landscapeRight:
286
+ connection.videoOrientation = .landscapeRight
287
+ case .portraitUpsideDown:
288
+ connection.videoOrientation = .portraitUpsideDown
289
+ case .unknown:
290
+ fallthrough
291
+ @unknown default:
292
+ connection.videoOrientation = .portrait
293
+ }
294
+ }
295
+ } else {
296
+ // If not on main thread, use a sync call to get the orientation
297
+ DispatchQueue.main.sync {
298
+ if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene {
299
+ switch windowScene.interfaceOrientation {
300
+ case .portrait:
301
+ connection.videoOrientation = .portrait
302
+ case .landscapeLeft:
303
+ connection.videoOrientation = .landscapeLeft
304
+ case .landscapeRight:
305
+ connection.videoOrientation = .landscapeRight
306
+ case .portraitUpsideDown:
307
+ connection.videoOrientation = .portraitUpsideDown
308
+ case .unknown:
309
+ fallthrough
310
+ @unknown default:
311
+ connection.videoOrientation = .portrait
312
+ }
313
+ }
274
314
  }
275
315
  }
276
316
  }
@@ -283,7 +323,7 @@ extension CameraController {
283
323
  }
284
324
 
285
325
  func prepare(cameraPosition: String, deviceId: String? = nil, disableAudio: Bool, cameraMode: Bool, aspectRatio: String? = nil, initialZoomLevel: Float?, disableFocusIndicator: Bool = false, completionHandler: @escaping (Error?) -> Void) {
286
- print("[CameraPreview] 🎬 Starting prepare - position: \(cameraPosition), deviceId: \(deviceId ?? "nil"), disableAudio: \(disableAudio), cameraMode: \(cameraMode), aspectRatio: \(aspectRatio ?? "nil"), zoom: \(initialZoomLevel)")
326
+ print("[CameraPreview] 🎬 Starting prepare - position: \(cameraPosition), deviceId: \(deviceId ?? "nil"), disableAudio: \(disableAudio), cameraMode: \(cameraMode), aspectRatio: \(aspectRatio ?? "nil"), zoom: \(initialZoomLevel ?? 1)")
287
327
 
288
328
  DispatchQueue.global(qos: .userInitiated).async { [weak self] in
289
329
  guard let self = self else {
@@ -321,18 +361,8 @@ extension CameraController {
321
361
 
322
362
  // Add ALL outputs BEFORE starting session to avoid flashes from reconfiguration
323
363
 
324
- // Determine initial orientation once
325
- var videoOrientation: AVCaptureVideoOrientation = .portrait
326
- if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene {
327
- switch windowScene.interfaceOrientation {
328
- case .portrait: videoOrientation = .portrait
329
- case .landscapeLeft: videoOrientation = .landscapeLeft
330
- case .landscapeRight: videoOrientation = .landscapeRight
331
- case .portraitUpsideDown: videoOrientation = .portraitUpsideDown
332
- case .unknown: fallthrough
333
- @unknown default: videoOrientation = .portrait
334
- }
335
- }
364
+ // Get orientation in a thread-safe way
365
+ let videoOrientation = self.getVideoOrientation()
336
366
 
337
367
  // Add data output for preview
338
368
  if let dataOutput = self.dataOutput, captureSession.canAddOutput(dataOutput) {
@@ -706,25 +736,8 @@ extension CameraController {
706
736
  }
707
737
 
708
738
  func updateVideoOrientation() {
709
- // Get orientation on the current thread to avoid blocking
710
- var videoOrientation: AVCaptureVideoOrientation = .portrait
711
-
712
- if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene {
713
- switch windowScene.interfaceOrientation {
714
- case .portrait:
715
- videoOrientation = .portrait
716
- case .landscapeLeft:
717
- videoOrientation = .landscapeLeft
718
- case .landscapeRight:
719
- videoOrientation = .landscapeRight
720
- case .portraitUpsideDown:
721
- videoOrientation = .portraitUpsideDown
722
- case .unknown:
723
- fallthrough
724
- @unknown default:
725
- videoOrientation = .portrait
726
- }
727
- }
739
+ // Get orientation in a thread-safe way
740
+ let videoOrientation = self.getVideoOrientation()
728
741
 
729
742
  // Apply orientation asynchronously on main thread
730
743
  let updateBlock = { [weak self] in
@@ -768,102 +781,131 @@ extension CameraController {
768
781
  }
769
782
  }
770
783
 
784
+ // Helper: pick the best preset the TARGET device supports for a given aspect ratio
785
+ private func bestPreset(for aspectRatio: String?, on device: AVCaptureDevice) -> AVCaptureSession.Preset {
786
+ // Preference order depends on aspect ratio
787
+ if aspectRatio == "16:9" {
788
+ // Prefer 4K → 1080p → 720p → high → photo → vga
789
+ if device.supportsSessionPreset(.hd4K3840x2160) { return .hd4K3840x2160 }
790
+ if device.supportsSessionPreset(.hd1920x1080) { return .hd1920x1080 }
791
+ if device.supportsSessionPreset(.hd1280x720) { return .hd1280x720 }
792
+ if device.supportsSessionPreset(.high) { return .high }
793
+ if device.supportsSessionPreset(.photo) { return .photo } // safe, though 4:3
794
+ return .vga640x480
795
+ } else {
796
+ // 4:3 or unknown: prefer photo → high → 1080p → 720p → vga
797
+ if device.supportsSessionPreset(.photo) { return .photo }
798
+ if device.supportsSessionPreset(.high) { return .high }
799
+ if device.supportsSessionPreset(.hd1920x1080){ return .hd1920x1080 }
800
+ if device.supportsSessionPreset(.hd1280x720) { return .hd1280x720 }
801
+ return .vga640x480
802
+ }
803
+ }
804
+
771
805
  func switchCameras() throws {
772
806
  guard let currentCameraPosition = currentCameraPosition,
773
807
  let captureSession = self.captureSession else {
774
808
  throw CameraControllerError.captureSessionIsMissing
775
809
  }
776
810
 
777
- // Ensure we have the necessary cameras
778
- guard (currentCameraPosition == .front && rearCamera != nil) ||
779
- (currentCameraPosition == .rear && frontCamera != nil) else {
780
- throw CameraControllerError.noCamerasAvailable
811
+ // Determine the device we’re switching TO
812
+ let targetDevice: AVCaptureDevice
813
+ switch currentCameraPosition {
814
+ case .front:
815
+ guard let rear = rearCamera else { throw CameraControllerError.invalidOperation }
816
+ targetDevice = rear
817
+ case .rear:
818
+ guard let front = frontCamera else { throw CameraControllerError.invalidOperation }
819
+ targetDevice = front
781
820
  }
782
821
 
783
- // Store the current running state
784
- let wasRunning = captureSession.isRunning
785
- if wasRunning {
786
- captureSession.stopRunning()
787
- }
822
+ // Compute the desired preset for the TARGET device up front
823
+ let desiredPreset = bestPreset(for: self.requestedAspectRatio, on: targetDevice)
788
824
 
789
- // Begin configuration
825
+ // Keep the preview layer visually stable during the swap
826
+ let savedPreviewFrame = self.previewLayer?.frame
827
+ CATransaction.begin()
828
+ CATransaction.setDisableActions(true)
829
+ self.previewLayer?.connection?.isEnabled = false // reduce visible glitching
830
+
831
+ // No need to stopRunning; Apple recommends reconfiguring within begin/commit
790
832
  captureSession.beginConfiguration()
791
833
  defer {
792
834
  captureSession.commitConfiguration()
793
- // Restart the session if it was running before
794
- if wasRunning {
795
- DispatchQueue.global(qos: .userInitiated).async {
796
- captureSession.startRunning()
797
- }
835
+ self.previewLayer?.connection?.isEnabled = true
836
+ // Restore frame (it shouldn't change, but this ensures zero animation)
837
+ if let f = savedPreviewFrame { self.previewLayer?.frame = f }
838
+ CATransaction.commit()
839
+ DispatchQueue.main.async { [weak self] in
840
+ self?.setDefaultZoomAfterFlip() // normalize zoom (UI 1.0x)
798
841
  }
799
842
  }
800
843
 
801
- // Store audio input if it exists
802
- let audioInput = captureSession.inputs.first { ($0 as? AVCaptureDeviceInput)?.device.hasMediaType(.audio) ?? false }
844
+ // Preserve audio input (if any)
845
+ let existingAudioInput = captureSession.inputs.first {
846
+ ($0 as? AVCaptureDeviceInput)?.device.hasMediaType(.audio) ?? false
847
+ }
803
848
 
804
- // Remove only video inputs
805
- captureSession.inputs.forEach { input in
849
+ // Remove ONLY video inputs
850
+ for input in captureSession.inputs {
806
851
  if (input as? AVCaptureDeviceInput)?.device.hasMediaType(.video) ?? false {
807
852
  captureSession.removeInput(input)
808
853
  }
809
854
  }
810
855
 
811
- // Configure new camera
812
- switch currentCameraPosition {
813
- case .front:
814
- guard let rearCamera = rearCamera else {
815
- throw CameraControllerError.invalidOperation
816
- }
817
-
818
- // Configure rear camera
819
- try rearCamera.lockForConfiguration()
820
- if rearCamera.isFocusModeSupported(.continuousAutoFocus) {
821
- rearCamera.focusMode = .continuousAutoFocus
822
- }
823
- rearCamera.unlockForConfiguration()
824
-
825
- if let newInput = try? AVCaptureDeviceInput(device: rearCamera),
826
- captureSession.canAddInput(newInput) {
827
- captureSession.addInput(newInput)
828
- rearCameraInput = newInput
829
- self.currentCameraPosition = .rear
830
- } else {
831
- throw CameraControllerError.invalidOperation
832
- }
833
- case .rear:
834
- guard let frontCamera = frontCamera else {
835
- throw CameraControllerError.invalidOperation
856
+ // Only downgrade to a safe preset if the TARGET cannot support the CURRENT one
857
+ let currentPreset = captureSession.sessionPreset
858
+ let targetSupportsCurrent = targetDevice.supportsSessionPreset(currentPreset)
859
+ if !targetSupportsCurrent {
860
+ // Choose the first preset supported by BOTH the target device and the session
861
+ let fallbacks: [AVCaptureSession.Preset] =
862
+ (self.requestedAspectRatio == "16:9")
863
+ ? [.hd4K3840x2160, .hd1920x1080, .hd1280x720, .high, .photo, .vga640x480]
864
+ : [.photo, .high, .hd1920x1080, .hd1280x720, .vga640x480]
865
+ for p in fallbacks {
866
+ if targetDevice.supportsSessionPreset(p), captureSession.canSetSessionPreset(p) {
867
+ captureSession.sessionPreset = p
868
+ break
869
+ }
836
870
  }
871
+ }
837
872
 
838
- // Configure front camera
839
- try frontCamera.lockForConfiguration()
840
- if frontCamera.isFocusModeSupported(.continuousAutoFocus) {
841
- frontCamera.focusMode = .continuousAutoFocus
842
- }
843
- frontCamera.unlockForConfiguration()
873
+ // Add the new video input
874
+ let newInput = try AVCaptureDeviceInput(device: targetDevice)
875
+ guard captureSession.canAddInput(newInput) else {
876
+ throw CameraControllerError.invalidOperation
877
+ }
878
+ captureSession.addInput(newInput)
844
879
 
845
- if let newInput = try? AVCaptureDeviceInput(device: frontCamera),
846
- captureSession.canAddInput(newInput) {
847
- captureSession.addInput(newInput)
848
- frontCameraInput = newInput
849
- self.currentCameraPosition = .front
850
- } else {
851
- throw CameraControllerError.invalidOperation
852
- }
880
+ // Update pointers / focus defaults
881
+ if targetDevice.position == .front {
882
+ self.frontCameraInput = newInput
883
+ self.currentCameraPosition = .front
884
+ } else {
885
+ self.rearCameraInput = newInput
886
+ self.currentCameraPosition = .rear
887
+ }
888
+ // (Lightweight focus config; non-fatal on failure)
889
+ try? targetDevice.lockForConfiguration()
890
+ if targetDevice.isFocusModeSupported(.continuousAutoFocus) {
891
+ targetDevice.focusMode = .continuousAutoFocus
853
892
  }
893
+ targetDevice.unlockForConfiguration()
854
894
 
855
- // Re-add audio input if it existed
856
- if let audioInput = audioInput, captureSession.canAddInput(audioInput) {
895
+ // Restore audio input if it existed
896
+ if let audioInput = existingAudioInput, captureSession.canAddInput(audioInput) {
857
897
  captureSession.addInput(audioInput)
858
898
  }
859
899
 
860
- // Update video orientation
861
- self.updateVideoOrientation()
862
-
863
- // Set default 1.0 zoom level after camera switch to prevent iOS 18+ zoom jumps
864
- DispatchQueue.main.async { [weak self] in
865
- self?.setDefaultZoomAfterFlip()
900
+ // Now apply the BEST preset for the target device & requested AR
901
+ if captureSession.sessionPreset != desiredPreset,
902
+ targetDevice.supportsSessionPreset(desiredPreset),
903
+ captureSession.canSetSessionPreset(desiredPreset) {
904
+ captureSession.sessionPreset = desiredPreset
866
905
  }
906
+
907
+ // Keep orientation correct
908
+ self.updateVideoOrientation()
867
909
  }
868
910
 
869
911
  func captureImage(width: Int?, height: Int?, quality: Float, gpsLocation: CLLocation?, completion: @escaping (UIImage?, Data?, [AnyHashable: Any]?, Error?) -> Void) {
@@ -874,11 +916,9 @@ extension CameraController {
874
916
 
875
917
  let settings = AVCapturePhotoSettings()
876
918
  // Configure photo capture settings optimized for speed
877
- if #available(iOS 13.0, *) {
878
- // Only use high res if explicitly requesting large dimensions
879
- let shouldUseHighRes = width.map { $0 > 1920 } ?? false || height.map { $0 > 1920 } ?? false
880
- settings.isHighResolutionPhotoEnabled = shouldUseHighRes
881
- }
919
+ // Only use high res if explicitly requesting large dimensions
920
+ let shouldUseHighRes = width.map { $0 > 1920 } ?? false || height.map { $0 > 1920 } ?? false
921
+ settings.isHighResolutionPhotoEnabled = shouldUseHighRes
882
922
  if #available(iOS 15.0, *) {
883
923
  // Prioritize speed over quality
884
924
  settings.photoQualityPrioritization = .speed
@@ -2207,11 +2247,11 @@ extension UIImage {
2207
2247
  // Flip image one more time if needed to, this is to prevent flipped image
2208
2248
  switch imageOrientation {
2209
2249
  case .upMirrored, .downMirrored:
2210
- transform.translatedBy(x: size.width, y: 0)
2211
- transform.scaledBy(x: -1, y: 1)
2250
+ _ = transform.translatedBy(x: size.width, y: 0)
2251
+ _ = transform.scaledBy(x: -1, y: 1)
2212
2252
  case .leftMirrored, .rightMirrored:
2213
- transform.translatedBy(x: size.height, y: 0)
2214
- transform.scaledBy(x: -1, y: 1)
2253
+ _ = transform.translatedBy(x: size.height, y: 0)
2254
+ _ = transform.scaledBy(x: -1, y: 1)
2215
2255
  case .up, .down, .left, .right:
2216
2256
  break
2217
2257
  @unknown default:
@@ -8,26 +8,22 @@ import MobileCoreServices
8
8
 
9
9
  extension UIWindow {
10
10
  static var isLandscape: Bool {
11
- if #available(iOS 13.0, *) {
12
- return UIApplication.shared.windows
13
- .first?
14
- .windowScene?
15
- .interfaceOrientation
16
- .isLandscape ?? false
17
- } else {
18
- return UIApplication.shared.statusBarOrientation.isLandscape
19
- }
11
+ // iOS 14+ only: derive from the active window scene's interface orientation
12
+ let scene = UIApplication.shared
13
+ .connectedScenes
14
+ .compactMap { $0 as? UIWindowScene }
15
+ .first { $0.activationState == .foregroundActive }
16
+
17
+ return scene?.interfaceOrientation.isLandscape ?? false
20
18
  }
21
19
  static var isPortrait: Bool {
22
- if #available(iOS 13.0, *) {
23
- return UIApplication.shared.windows
24
- .first?
25
- .windowScene?
26
- .interfaceOrientation
27
- .isPortrait ?? false
28
- } else {
29
- return UIApplication.shared.statusBarOrientation.isPortrait
30
- }
20
+ // iOS 14+ only: derive from the active window scene's interface orientation
21
+ let scene = UIApplication.shared
22
+ .connectedScenes
23
+ .compactMap { $0 as? UIWindowScene }
24
+ .first { $0.activationState == .foregroundActive }
25
+
26
+ return scene?.interfaceOrientation.isPortrait ?? false
31
27
  }
32
28
  }
33
29
 
@@ -142,7 +138,7 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin, CLLocationManagerDelega
142
138
  guard let webView = self.webView else { return }
143
139
 
144
140
  DispatchQueue.main.async {
145
- let startTransparency = CFAbsoluteTimeGetCurrent()
141
+ _ = CFAbsoluteTimeGetCurrent()
146
142
 
147
143
  // Define a recursive function to traverse the view hierarchy
148
144
  func makeSubviewsTransparent(_ view: UIView) {
@@ -225,15 +221,11 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin, CLLocationManagerDelega
225
221
  let displayMultiplier = self.cameraController.getDisplayZoomMultiplier()
226
222
  var teleStep: Float
227
223
 
228
- if #available(iOS 13.0, *) {
229
- let switchFactors = device.virtualDeviceSwitchOverVideoZoomFactors
230
- if !switchFactors.isEmpty {
231
- // Choose the highest switch-over factor (typically the wide->tele threshold)
232
- let maxSwitch = switchFactors.map { $0.floatValue }.max() ?? Float(device.maxAvailableVideoZoomFactor)
233
- teleStep = maxSwitch * displayMultiplier
234
- } else {
235
- teleStep = Float(device.maxAvailableVideoZoomFactor) * displayMultiplier
236
- }
224
+ let switchFactors = device.virtualDeviceSwitchOverVideoZoomFactors
225
+ if !switchFactors.isEmpty {
226
+ // Choose the highest switch-over factor (typically the wide->tele threshold)
227
+ let maxSwitch = switchFactors.map { $0.floatValue }.max() ?? Float(device.maxAvailableVideoZoomFactor)
228
+ teleStep = maxSwitch * displayMultiplier
237
229
  } else {
238
230
  teleStep = Float(device.maxAvailableVideoZoomFactor) * displayMultiplier
239
231
  }
@@ -268,15 +260,9 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin, CLLocationManagerDelega
268
260
  }
269
261
 
270
262
  @objc func rotated() {
271
- guard let previewView = self.previewView,
272
- let posX = self.posX,
273
- let posY = self.posY,
274
- let width = self.width,
275
- let heightValue = self.height else {
263
+ guard let previewView = self.previewView else {
276
264
  return
277
265
  }
278
- let paddingBottom = self.paddingBottom ?? 0
279
- let height = heightValue - paddingBottom
280
266
 
281
267
  // Handle auto-centering during rotation
282
268
  // Always use the factorized method for consistent positioning
@@ -786,7 +772,7 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin, CLLocationManagerDelega
786
772
  }
787
773
 
788
774
  @objc func capture(_ call: CAPPluginCall) {
789
- print("[CameraPreview] capture called with options: \(call.options)")
775
+ print("[CameraPreview] capture called with options: \(call.options ?? [:])")
790
776
  let withExifLocation = call.getBool("withExifLocation", false)
791
777
  print("[CameraPreview] capture called, withExifLocation: \(withExifLocation)")
792
778
 
@@ -857,7 +843,7 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin, CLLocationManagerDelega
857
843
 
858
844
  private func performCapture(call: CAPPluginCall) {
859
845
  print("[CameraPreview] performCapture called")
860
- print("[CameraPreview] Call parameters: \(call.options)")
846
+ print("[CameraPreview] Call parameters: \(call.options ?? [:])")
861
847
  let quality = call.getFloat("quality", 85)
862
848
  let saveToGallery = call.getBool("saveToGallery", false)
863
849
  let withExifLocation = call.getBool("withExifLocation", false)
@@ -1016,8 +1002,13 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin, CLLocationManagerDelega
1016
1002
  notchInset = safeAreaInsets.top
1017
1003
  }
1018
1004
  } else {
1019
- // Fallback: use status bar height as approximation
1020
- notchInset = UIApplication.shared.statusBarFrame.height
1005
+ // Fallback for iOS 14+: try to derive from any available window's safe area
1006
+ let anyWindow = UIApplication.shared
1007
+ .connectedScenes
1008
+ .compactMap { $0 as? UIWindowScene }
1009
+ .flatMap { $0.windows }
1010
+ .first
1011
+ notchInset = anyWindow?.safeAreaInsets.top ?? 0
1021
1012
  }
1022
1013
 
1023
1014
  let result: [String: Any] = [
@@ -1681,7 +1672,7 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin, CLLocationManagerDelega
1681
1672
  }
1682
1673
 
1683
1674
  private func updateCameraFrame() {
1684
- guard let width = self.width, let height = self.height, let posX = self.posX, let posY = self.posY else {
1675
+ guard let posX = self.posX, let posY = self.posY else {
1685
1676
  return
1686
1677
  }
1687
1678
 
@@ -1940,7 +1931,7 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin, CLLocationManagerDelega
1940
1931
  private var lastOrientation: String?
1941
1932
 
1942
1933
  @objc private func handleOrientationChange() {
1943
- var currentOrientation = self.currentOrientationString()
1934
+ let currentOrientation = self.currentOrientationString()
1944
1935
  if currentOrientation == "portrait-upside-down" || currentOrientation == lastOrientation {
1945
1936
  return
1946
1937
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@capgo/camera-preview",
3
- "version": "7.13.11",
3
+ "version": "7.14.3",
4
4
  "description": "Camera preview",
5
5
  "license": "MIT",
6
6
  "repository": {