@capgo/camera-preview 7.4.0-beta.10 → 7.4.0-beta.11

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.
@@ -1,12 +1,11 @@
1
1
  import Foundation
2
- import Capacitor
3
2
  import AVFoundation
4
3
  import Photos
4
+ import Capacitor
5
5
  import CoreImage
6
6
  import CoreLocation
7
7
  import MobileCoreServices
8
8
 
9
-
10
9
  extension UIWindow {
11
10
  static var isLandscape: Bool {
12
11
  if #available(iOS 13.0, *) {
@@ -90,42 +89,37 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin, CLLocationManagerDelega
90
89
  var currentLocation: CLLocation?
91
90
  private var aspectRatio: String?
92
91
  private var gridMode: String = "none"
92
+ private var permissionCallID: String?
93
+ private var waitingForLocation: Bool = false
93
94
 
94
95
  // MARK: - Transparency Methods
95
96
 
96
-
97
97
  private func makeWebViewTransparent() {
98
98
  guard let webView = self.webView else { return }
99
99
 
100
-
101
- // Define a recursive function to traverse the view hierarchy
102
- func makeSubviewsTransparent(_ view: UIView) {
103
- // Set the background color to clear
104
- view.backgroundColor = .clear
105
-
106
-
107
- // Recurse for all subviews
108
- for subview in view.subviews {
109
- makeSubviewsTransparent(subview)
100
+ DispatchQueue.main.async {
101
+ // Define a recursive function to traverse the view hierarchy
102
+ func makeSubviewsTransparent(_ view: UIView) {
103
+ // Set the background color to clear
104
+ view.backgroundColor = .clear
105
+
106
+ // Recurse for all subviews
107
+ for subview in view.subviews {
108
+ makeSubviewsTransparent(subview)
109
+ }
110
110
  }
111
- }
112
-
113
-
114
- // Set the main webView to be transparent
115
- webView.isOpaque = false
116
- webView.backgroundColor = .clear
117
111
 
112
+ // Set the main webView to be transparent
113
+ webView.isOpaque = false
114
+ webView.backgroundColor = .clear
118
115
 
119
- // Recursively make all subviews transparent
120
- makeSubviewsTransparent(webView)
116
+ // Recursively make all subviews transparent
117
+ makeSubviewsTransparent(webView)
121
118
 
119
+ // Also ensure the webview's container is transparent
120
+ webView.superview?.backgroundColor = .clear
122
121
 
123
- // Also ensure the webview's container is transparent
124
- webView.superview?.backgroundColor = .clear
125
-
126
-
127
- // Force a layout pass to apply changes
128
- DispatchQueue.main.async {
122
+ // Force a layout pass to apply changes
129
123
  webView.setNeedsLayout()
130
124
  webView.layoutIfNeeded()
131
125
  }
@@ -150,7 +144,7 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin, CLLocationManagerDelega
150
144
  // Manual positioning - use original rotation logic with no animation
151
145
  CATransaction.begin()
152
146
  CATransaction.setDisableActions(true)
153
-
147
+
154
148
  if UIWindow.isLandscape {
155
149
  previewView.frame = CGRect(x: posY, y: posX, width: max(height, width), height: min(height, width))
156
150
  self.cameraController.previewLayer?.frame = previewView.bounds
@@ -160,7 +154,7 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin, CLLocationManagerDelega
160
154
  previewView.frame = CGRect(x: posX, y: posY, width: min(height, width), height: max(height, width))
161
155
  self.cameraController.previewLayer?.frame = previewView.bounds
162
156
  }
163
-
157
+
164
158
  CATransaction.commit()
165
159
  }
166
160
 
@@ -202,65 +196,67 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin, CLLocationManagerDelega
202
196
  call.reject("camera not started")
203
197
  return
204
198
  }
205
-
199
+
206
200
  guard let newAspectRatio = call.getString("aspectRatio") else {
207
201
  call.reject("aspectRatio parameter is required")
208
202
  return
209
203
  }
210
-
204
+
211
205
  self.aspectRatio = newAspectRatio
212
-
213
- // When aspect ratio changes, calculate maximum size possible from current position
214
- if let posX = self.posX, let posY = self.posY {
215
- let webViewWidth = self.webView?.frame.width ?? UIScreen.main.bounds.width
216
- let webViewHeight = self.webView?.frame.height ?? UIScreen.main.bounds.height
217
- let paddingBottom = self.paddingBottom ?? 0
218
-
219
- // Calculate available space from current position
220
- let availableWidth: CGFloat
221
- let availableHeight: CGFloat
222
-
223
- if posX == -1 || posY == -1 {
224
- // Auto-centering mode - use full dimensions
225
- availableWidth = webViewWidth
226
- availableHeight = webViewHeight - paddingBottom
227
- } else {
228
- // Manual positioning - calculate remaining space
229
- availableWidth = webViewWidth - posX
230
- availableHeight = webViewHeight - posY - paddingBottom
231
- }
232
-
233
- // Parse aspect ratio - convert to portrait orientation for camera use
234
- let ratioParts = newAspectRatio.split(separator: ":").map { Double($0) ?? 1.0 }
235
- // For camera, we want portrait orientation: 4:3 becomes 3:4, 16:9 becomes 9:16
236
- let ratio = ratioParts[1] / ratioParts[0]
237
-
238
- // Calculate maximum size that fits the aspect ratio in available space
239
- let maxWidthByHeight = availableHeight * CGFloat(ratio)
240
- let maxHeightByWidth = availableWidth / CGFloat(ratio)
241
-
242
- if maxWidthByHeight <= availableWidth {
243
- // Height is the limiting factor
244
- self.width = maxWidthByHeight
245
- self.height = availableHeight
246
- } else {
247
- // Width is the limiting factor
248
- self.width = availableWidth
249
- self.height = maxHeightByWidth
206
+
207
+ DispatchQueue.main.async {
208
+ // When aspect ratio changes, calculate maximum size possible from current position
209
+ if let posX = self.posX, let posY = self.posY {
210
+ let webViewWidth = self.webView?.frame.width ?? UIScreen.main.bounds.width
211
+ let webViewHeight = self.webView?.frame.height ?? UIScreen.main.bounds.height
212
+ let paddingBottom = self.paddingBottom ?? 0
213
+
214
+ // Calculate available space from current position
215
+ let availableWidth: CGFloat
216
+ let availableHeight: CGFloat
217
+
218
+ if posX == -1 || posY == -1 {
219
+ // Auto-centering mode - use full dimensions
220
+ availableWidth = webViewWidth
221
+ availableHeight = webViewHeight - paddingBottom
222
+ } else {
223
+ // Manual positioning - calculate remaining space
224
+ availableWidth = webViewWidth - posX
225
+ availableHeight = webViewHeight - posY - paddingBottom
226
+ }
227
+
228
+ // Parse aspect ratio - convert to portrait orientation for camera use
229
+ let ratioParts = newAspectRatio.split(separator: ":").map { Double($0) ?? 1.0 }
230
+ // For camera, we want portrait orientation: 4:3 becomes 3:4, 16:9 becomes 9:16
231
+ let ratio = ratioParts[1] / ratioParts[0]
232
+
233
+ // Calculate maximum size that fits the aspect ratio in available space
234
+ let maxWidthByHeight = availableHeight * CGFloat(ratio)
235
+ let maxHeightByWidth = availableWidth / CGFloat(ratio)
236
+
237
+ if maxWidthByHeight <= availableWidth {
238
+ // Height is the limiting factor
239
+ self.width = maxWidthByHeight
240
+ self.height = availableHeight
241
+ } else {
242
+ // Width is the limiting factor
243
+ self.width = availableWidth
244
+ self.height = maxHeightByWidth
245
+ }
246
+
247
+ print("[CameraPreview] Aspect ratio changed to \(newAspectRatio), new size: \(self.width!)x\(self.height!)")
250
248
  }
251
-
252
- print("[CameraPreview] Aspect ratio changed to \(newAspectRatio), new size: \(self.width!)x\(self.height!)")
249
+
250
+ self.updateCameraFrame()
251
+
252
+ // Return the actual preview bounds
253
+ var result = JSObject()
254
+ result["x"] = Double(self.previewView.frame.origin.x)
255
+ result["y"] = Double(self.previewView.frame.origin.y)
256
+ result["width"] = Double(self.previewView.frame.width)
257
+ result["height"] = Double(self.previewView.frame.height)
258
+ call.resolve(result)
253
259
  }
254
-
255
- self.updateCameraFrame()
256
-
257
- // Return the actual preview bounds
258
- var result = JSObject()
259
- result["x"] = Double(self.previewView.frame.origin.x)
260
- result["y"] = Double(self.previewView.frame.origin.y)
261
- result["width"] = Double(self.previewView.frame.width)
262
- result["height"] = Double(self.previewView.frame.height)
263
- call.resolve(result)
264
260
  }
265
261
 
266
262
  @objc func getAspectRatio(_ call: CAPPluginCall) {
@@ -312,7 +308,6 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin, CLLocationManagerDelega
312
308
  }
313
309
  }
314
310
 
315
-
316
311
  @objc func appWillEnterForeground() {
317
312
  if self.isInitialized {
318
313
  DispatchQueue.main.async {
@@ -464,7 +459,7 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin, CLLocationManagerDelega
464
459
 
465
460
  print("[CameraPreview] Camera start parameters - aspectRatio: \(String(describing: self.aspectRatio)), gridMode: \(self.gridMode)")
466
461
  print("[CameraPreview] Screen dimensions: \(UIScreen.main.bounds.size)")
467
- print("[CameraPreview] Final frame dimensions - width: \(self.width), height: \(self.height), x: \(self.posX), y: \(self.posY)")
462
+ print("[CameraPreview] Final frame dimensions - width: \(String(describing: self.width)), height: \(String(describing: self.height)), x: \(String(describing: self.posX)), y: \(String(describing: self.posY))")
468
463
 
469
464
  AVCaptureDevice.requestAccess(for: .video, completionHandler: { (granted: Bool) in
470
465
  guard granted else {
@@ -472,16 +467,21 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin, CLLocationManagerDelega
472
467
  return
473
468
  }
474
469
 
475
- DispatchQueue.main.async {
476
- if self.cameraController.captureSession?.isRunning ?? false {
477
- call.reject("camera already started")
478
- } else {
479
- self.cameraController.prepare(cameraPosition: self.cameraPosition, deviceId: deviceId, disableAudio: self.disableAudio, cameraMode: cameraMode, aspectRatio: self.aspectRatio) {error in
480
- if let error = error {
481
- print(error)
482
- call.reject(error.localizedDescription)
483
- return
484
- }
470
+ if self.cameraController.captureSession?.isRunning ?? false {
471
+ call.reject("camera already started")
472
+ } else {
473
+ // Pre-initialize session if not already done
474
+ if self.cameraController.captureSession == nil {
475
+ self.cameraController.prepareFullSession()
476
+ }
477
+
478
+ self.cameraController.prepare(cameraPosition: self.cameraPosition, deviceId: deviceId, disableAudio: self.disableAudio, cameraMode: cameraMode, aspectRatio: self.aspectRatio) {error in
479
+ if let error = error {
480
+ print(error)
481
+ call.reject(error.localizedDescription)
482
+ return
483
+ }
484
+ DispatchQueue.main.async {
485
485
  self.completeStartCamera(call: call)
486
486
  }
487
487
  }
@@ -489,25 +489,6 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin, CLLocationManagerDelega
489
489
  })
490
490
  }
491
491
 
492
- override public func load() {
493
- super.load()
494
- // Initialize camera session in background for faster startup
495
- prepareBackgroundCamera()
496
- }
497
-
498
- private func prepareBackgroundCamera() {
499
- DispatchQueue.global(qos: .background).async {
500
- AVCaptureDevice.requestAccess(for: .video) { granted in
501
- guard granted else { return }
502
-
503
- // Pre-initialize camera controller for faster startup
504
- DispatchQueue.main.async {
505
- self.cameraController.prepareBasicSession()
506
- }
507
- }
508
- }
509
- }
510
-
511
492
  private func completeStartCamera(call: CAPPluginCall) {
512
493
  // Create and configure the preview view first
513
494
  self.updateCameraFrame()
@@ -557,74 +538,44 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin, CLLocationManagerDelega
557
538
  return
558
539
  }
559
540
 
560
- DispatchQueue.main.async { [weak self] in
561
- guard let self = self else {
562
- call.reject("Camera controller deallocated")
563
- return
564
- }
565
-
566
- // Disable user interaction during flip
567
- self.previewView.isUserInteractionEnabled = false
568
-
569
- // Perform camera switch on background thread
570
- DispatchQueue.global(qos: .userInitiated).async {
571
- var retryCount = 0
572
- let maxRetries = 3
573
-
574
- func attemptFlip() {
575
- do {
576
- try self.cameraController.switchCameras()
577
-
578
- DispatchQueue.main.async {
579
- // Update preview layer frame without animation
580
- CATransaction.begin()
581
- CATransaction.setDisableActions(true)
582
- self.cameraController.previewLayer?.frame = self.previewView.bounds
583
- self.cameraController.previewLayer?.videoGravity = .resizeAspectFill
584
- CATransaction.commit()
585
-
586
- self.previewView.isUserInteractionEnabled = true
541
+ // Disable user interaction during flip
542
+ self.previewView.isUserInteractionEnabled = false
587
543
 
544
+ do {
545
+ try self.cameraController.switchCameras()
588
546
 
589
- // Ensure webview remains transparent after flip
590
- self.makeWebViewTransparent()
547
+ // Update preview layer frame without animation
548
+ CATransaction.begin()
549
+ CATransaction.setDisableActions(true)
550
+ self.cameraController.previewLayer?.frame = self.previewView.bounds
551
+ self.cameraController.previewLayer?.videoGravity = .resizeAspectFill
552
+ CATransaction.commit()
591
553
 
554
+ self.previewView.isUserInteractionEnabled = true
592
555
 
593
- call.resolve()
594
- }
595
- } catch {
596
- retryCount += 1
597
-
598
- if retryCount < maxRetries {
599
- DispatchQueue.global(qos: .userInitiated).asyncAfter(deadline: .now() + 0.5) {
600
- attemptFlip()
601
- }
602
- } else {
603
- DispatchQueue.main.async {
604
- self.previewView.isUserInteractionEnabled = true
605
- print("Failed to flip camera after \(maxRetries) attempts: \(error.localizedDescription)")
606
- call.reject("Failed to flip camera: \(error.localizedDescription)")
607
- }
608
- }
609
- }
610
- }
556
+ // Ensure webview remains transparent after flip
557
+ self.makeWebViewTransparent()
611
558
 
612
- attemptFlip()
613
- }
559
+ call.resolve()
560
+ } catch {
561
+ self.previewView.isUserInteractionEnabled = true
562
+ print("Failed to flip camera: \(error.localizedDescription)")
563
+ call.reject("Failed to flip camera: \(error.localizedDescription)")
614
564
  }
615
565
  }
616
566
 
617
567
  @objc func stop(_ call: CAPPluginCall) {
618
- DispatchQueue.main.async {
619
- if self.isInitializing {
620
- call.reject("cannot stop camera while initialization is in progress")
621
- return
622
- }
623
- if !self.isInitialized {
624
- call.reject("camera not initialized")
625
- return
626
- }
568
+ if self.isInitializing {
569
+ call.reject("cannot stop camera while initialization is in progress")
570
+ return
571
+ }
572
+ if !self.isInitialized {
573
+ call.reject("camera not initialized")
574
+ return
575
+ }
627
576
 
577
+ // UI operations must be on main thread
578
+ DispatchQueue.main.async {
628
579
  // Always attempt to stop and clean up, regardless of captureSession state
629
580
  self.cameraController.removeGridOverlay()
630
581
  if let previewView = self.previewView {
@@ -637,7 +588,6 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin, CLLocationManagerDelega
637
588
  self.isInitializing = false
638
589
  self.cameraController.cleanup()
639
590
 
640
-
641
591
  // Remove notification observers
642
592
  NotificationCenter.default.removeObserver(self)
643
593
 
@@ -656,75 +606,138 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin, CLLocationManagerDelega
656
606
  }
657
607
 
658
608
  @objc func capture(_ call: CAPPluginCall) {
659
- DispatchQueue.main.async {
609
+ let withExifLocation = call.getBool("withExifLocation", false)
610
+ print("[CameraPreview] capture called, withExifLocation: \(withExifLocation)")
611
+
612
+ if withExifLocation {
613
+ print("[CameraPreview] Location required for capture")
614
+
615
+ // Check location services before main thread dispatch
616
+ guard CLLocationManager.locationServicesEnabled() else {
617
+ print("[CameraPreview] Location services are disabled")
618
+ call.reject("Location services are disabled")
619
+ return
620
+ }
660
621
 
661
- let quality = call.getFloat("quality", 85)
662
- let saveToGallery = call.getBool("saveToGallery", false)
663
- let withExifLocation = call.getBool("withExifLocation", false)
664
- let width = call.getInt("width")
665
- let height = call.getInt("height")
666
-
667
- if withExifLocation {
668
- self.locationManager = CLLocationManager()
669
- self.locationManager?.delegate = self
670
- self.locationManager?.requestWhenInUseAuthorization()
671
- self.locationManager?.startUpdatingLocation()
622
+ // Check if Info.plist has the required key
623
+ guard Bundle.main.object(forInfoDictionaryKey: "NSLocationWhenInUseUsageDescription") != nil else {
624
+ print("[CameraPreview] ERROR: NSLocationWhenInUseUsageDescription key missing from Info.plist")
625
+ call.reject("NSLocationWhenInUseUsageDescription key missing from Info.plist. Add this key with a description of how your app uses location.")
626
+ return
672
627
  }
673
628
 
674
- self.cameraController.captureImage(width: width, height: height, quality: quality, gpsLocation: self.currentLocation) { (image, error) in
629
+ // Ensure location manager setup happens on main thread
630
+ DispatchQueue.main.async {
631
+ if self.locationManager == nil {
632
+ print("[CameraPreview] Creating location manager on main thread")
633
+ self.locationManager = CLLocationManager()
634
+ self.locationManager?.delegate = self
635
+ self.locationManager?.desiredAccuracy = kCLLocationAccuracyBest
636
+ print("[CameraPreview] Location manager created, delegate set to: \(self)")
637
+ }
638
+
639
+ // Check current authorization status
640
+ let currentStatus = self.locationManager?.authorizationStatus ?? .notDetermined
641
+ print("[CameraPreview] Current authorization status: \(currentStatus.rawValue)")
642
+
643
+ switch currentStatus {
644
+ case .authorizedWhenInUse, .authorizedAlways:
645
+ // Already authorized, get location and capture
646
+ print("[CameraPreview] Already authorized, getting location immediately")
647
+ self.getCurrentLocation { _ in
648
+ self.performCapture(call: call)
649
+ }
650
+
651
+ case .denied, .restricted:
652
+ // Permission denied
653
+ print("[CameraPreview] Location permission denied")
654
+ call.reject("Location permission denied")
655
+
656
+ case .notDetermined:
657
+ // Need to request permission
658
+ print("[CameraPreview] Location permission not determined, requesting...")
659
+ // Save the call for the delegate callback
660
+ print("[CameraPreview] Saving call for location authorization flow")
661
+ self.bridge?.saveCall(call)
662
+ self.permissionCallID = call.callbackId
663
+ self.waitingForLocation = true
664
+
665
+ // Request authorization - this will trigger locationManagerDidChangeAuthorization
666
+ print("[CameraPreview] Requesting location authorization...")
667
+ self.locationManager?.requestWhenInUseAuthorization()
668
+ // The delegate will handle the rest
669
+
670
+ @unknown default:
671
+ print("[CameraPreview] Unknown authorization status")
672
+ call.reject("Unknown location permission status")
673
+ }
674
+ }
675
+ } else {
676
+ print("[CameraPreview] No location required, performing capture directly")
677
+ self.performCapture(call: call)
678
+ }
679
+ }
680
+
681
+ private func performCapture(call: CAPPluginCall) {
682
+ print("[CameraPreview] performCapture called")
683
+ let quality = call.getFloat("quality", 85)
684
+ let saveToGallery = call.getBool("saveToGallery", false)
685
+ let withExifLocation = call.getBool("withExifLocation", false)
686
+ let width = call.getInt("width")
687
+ let height = call.getInt("height")
688
+
689
+ print("[CameraPreview] Capture params - quality: \(quality), saveToGallery: \(saveToGallery), withExifLocation: \(withExifLocation), width: \(width ?? -1), height: \(height ?? -1)")
690
+ print("[CameraPreview] Current location: \(self.currentLocation?.description ?? "nil")")
691
+
692
+ self.cameraController.captureImage(width: width, height: height, quality: quality, gpsLocation: self.currentLocation) { (image, error) in
693
+ print("[CameraPreview] captureImage callback received")
694
+ DispatchQueue.main.async {
695
+ print("[CameraPreview] Processing capture on main thread")
675
696
  if let error = error {
697
+ print("[CameraPreview] Capture error: \(error.localizedDescription)")
676
698
  call.reject(error.localizedDescription)
677
699
  return
678
700
  }
679
701
 
680
- var gallerySuccess = true
681
- var galleryError: String?
682
-
683
- let group = DispatchGroup()
684
-
685
- group.notify(queue: .main) {
686
702
  guard let imageDataWithExif = self.createImageDataWithExif(from: image!, quality: Int(quality), location: withExifLocation ? self.currentLocation : nil) else {
703
+ print("[CameraPreview] Failed to create image data with EXIF")
687
704
  call.reject("Failed to create image data with EXIF")
688
705
  return
689
706
  }
690
707
 
708
+ print("[CameraPreview] Image data created, size: \(imageDataWithExif.count) bytes")
709
+
691
710
  if saveToGallery {
692
- group.enter()
711
+ print("[CameraPreview] Saving to gallery...")
693
712
  self.saveImageDataToGallery(imageData: imageDataWithExif) { success, error in
694
- gallerySuccess = success
695
- if !success {
696
- galleryError = error?.localizedDescription ?? "Unknown error"
697
- print("CameraPreview: Error saving image to gallery: \(galleryError!)")
698
- }
699
- group.leave()
700
- }
701
-
702
- group.notify(queue: .main) {
713
+ print("[CameraPreview] Save to gallery completed, success: \(success), error: \(error?.localizedDescription ?? "none")")
703
714
  let exifData = self.getExifData(from: imageDataWithExif)
704
715
  let base64Image = imageDataWithExif.base64EncodedString()
705
716
 
706
717
  var result = JSObject()
707
718
  result["value"] = base64Image
708
719
  result["exif"] = exifData
709
- result["gallerySaved"] = gallerySuccess
710
- if !gallerySuccess, let error = galleryError {
711
- result["galleryError"] = error
720
+ result["gallerySaved"] = success
721
+ if !success, let error = error {
722
+ result["galleryError"] = error.localizedDescription
712
723
  }
713
-
724
+
725
+ print("[CameraPreview] Resolving capture call with gallery save")
714
726
  call.resolve(result)
715
727
  }
716
728
  } else {
729
+ print("[CameraPreview] Not saving to gallery, returning image data")
717
730
  let exifData = self.getExifData(from: imageDataWithExif)
718
731
  let base64Image = imageDataWithExif.base64EncodedString()
719
732
 
720
733
  var result = JSObject()
721
734
  result["value"] = base64Image
722
735
  result["exif"] = exifData
723
-
736
+
737
+ print("[CameraPreview] Resolving capture call")
724
738
  call.resolve(result)
725
739
  }
726
740
  }
727
- }
728
741
  }
729
742
  }
730
743
 
@@ -735,7 +748,6 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin, CLLocationManagerDelega
735
748
  return [:]
736
749
  }
737
750
 
738
-
739
751
  var exifData = JSObject()
740
752
  for (key, value) in exifDict {
741
753
  // Convert value to JSValue-compatible type
@@ -755,7 +767,6 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin, CLLocationManagerDelega
755
767
  }
756
768
  }
757
769
 
758
-
759
770
  return exifData
760
771
  }
761
772
 
@@ -763,26 +774,26 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin, CLLocationManagerDelega
763
774
  guard let originalImageData = image.jpegData(compressionQuality: CGFloat(Double(quality) / 100.0)) else {
764
775
  return nil
765
776
  }
766
-
777
+
767
778
  guard let imageSource = CGImageSourceCreateWithData(originalImageData as CFData, nil),
768
779
  let imageProperties = CGImageSourceCopyPropertiesAtIndex(imageSource, 0, nil) as? [String: Any],
769
780
  let cgImage = image.cgImage else {
770
781
  return originalImageData
771
782
  }
772
-
783
+
773
784
  let mutableData = NSMutableData()
774
785
  guard let destination = CGImageDestinationCreateWithData(mutableData, kUTTypeJPEG, 1, nil) else {
775
786
  return originalImageData
776
787
  }
777
-
788
+
778
789
  var finalProperties = imageProperties
779
-
790
+
780
791
  // Add GPS location if available
781
792
  if let location = location {
782
793
  let formatter = DateFormatter()
783
794
  formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZ"
784
795
  formatter.timeZone = TimeZone(abbreviation: "UTC")
785
-
796
+
786
797
  let gpsDict: [String: Any] = [
787
798
  kCGImagePropertyGPSLatitude as String: abs(location.coordinate.latitude),
788
799
  kCGImagePropertyGPSLatitudeRef as String: location.coordinate.latitude >= 0 ? "N" : "S",
@@ -792,79 +803,53 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin, CLLocationManagerDelega
792
803
  kCGImagePropertyGPSAltitude as String: location.altitude,
793
804
  kCGImagePropertyGPSAltitudeRef as String: location.altitude >= 0 ? 0 : 1
794
805
  ]
795
-
806
+
796
807
  finalProperties[kCGImagePropertyGPSDictionary as String] = gpsDict
797
808
  }
798
-
799
- // Add lens information
800
- do {
801
- let currentZoom = try self.cameraController.getZoom()
802
- let lensInfo = try self.cameraController.getCurrentLensInfo()
803
-
804
- // Create or update EXIF dictionary
805
- var exifDict = finalProperties[kCGImagePropertyExifDictionary as String] as? [String: Any] ?? [:]
806
-
807
- // Add focal length (in mm)
808
- exifDict[kCGImagePropertyExifFocalLength as String] = lensInfo.focalLength
809
-
810
- // Add digital zoom ratio
811
- let digitalZoom = Float(currentZoom.current) / lensInfo.baseZoomRatio
812
- exifDict[kCGImagePropertyExifDigitalZoomRatio as String] = digitalZoom
813
-
814
- // Add lens model info
815
- exifDict[kCGImagePropertyExifLensModel as String] = lensInfo.deviceType
816
-
817
- finalProperties[kCGImagePropertyExifDictionary as String] = exifDict
818
-
819
- // Create or update TIFF dictionary for device info
820
- var tiffDict = finalProperties[kCGImagePropertyTIFFDictionary as String] as? [String: Any] ?? [:]
821
- tiffDict[kCGImagePropertyTIFFMake as String] = "Apple"
822
- tiffDict[kCGImagePropertyTIFFModel as String] = UIDevice.current.model
823
- finalProperties[kCGImagePropertyTIFFDictionary as String] = tiffDict
824
-
825
- } catch {
826
- print("CameraPreview: Failed to get lens information: \(error)")
827
- }
828
-
809
+
810
+ // Create or update TIFF dictionary for device info
811
+ var tiffDict = finalProperties[kCGImagePropertyTIFFDictionary as String] as? [String: Any] ?? [:]
812
+ tiffDict[kCGImagePropertyTIFFMake as String] = "Apple"
813
+ tiffDict[kCGImagePropertyTIFFModel as String] = UIDevice.current.model
814
+ finalProperties[kCGImagePropertyTIFFDictionary as String] = tiffDict
815
+
829
816
  CGImageDestinationAddImage(destination, cgImage, finalProperties as CFDictionary)
830
-
817
+
831
818
  if CGImageDestinationFinalize(destination) {
832
819
  return mutableData as Data
833
820
  }
834
-
821
+
835
822
  return originalImageData
836
823
  }
837
824
 
838
825
  @objc func captureSample(_ call: CAPPluginCall) {
839
- DispatchQueue.main.async {
840
- let quality: Int? = call.getInt("quality", 85)
826
+ let quality: Int? = call.getInt("quality", 85)
841
827
 
842
- self.cameraController.captureSample { image, error in
843
- guard let image = image else {
844
- print("Image capture error: \(String(describing: error))")
845
- call.reject("Image capture error: \(String(describing: error))")
846
- return
847
- }
828
+ self.cameraController.captureSample { image, error in
829
+ guard let image = image else {
830
+ print("Image capture error: \(String(describing: error))")
831
+ call.reject("Image capture error: \(String(describing: error))")
832
+ return
833
+ }
848
834
 
849
- let imageData: Data?
850
- if self.cameraPosition == "front" {
851
- let flippedImage = image.withHorizontallyFlippedOrientation()
852
- imageData = flippedImage.jpegData(compressionQuality: CGFloat(quality!/100))
853
- } else {
854
- imageData = image.jpegData(compressionQuality: CGFloat(quality!/100))
855
- }
835
+ let imageData: Data?
836
+ if self.cameraPosition == "front" {
837
+ let flippedImage = image.withHorizontallyFlippedOrientation()
838
+ imageData = flippedImage.jpegData(compressionQuality: CGFloat(quality!/100))
839
+ } else {
840
+ imageData = image.jpegData(compressionQuality: CGFloat(quality!/100))
841
+ }
856
842
 
857
- if self.storeToFile == false {
858
- let imageBase64 = imageData?.base64EncodedString()
859
- call.resolve(["value": imageBase64!])
860
- } else {
861
- do {
862
- let fileUrl = self.getTempFilePath()
863
- try imageData?.write(to: fileUrl)
864
- call.resolve(["value": fileUrl.absoluteString])
865
- } catch {
866
- call.reject("Error writing image to file")
867
- }
843
+ if self.storeToFile == false {
844
+ let imageBase64 = imageData?.base64EncodedString()
845
+ call.resolve(["value": imageBase64!])
846
+ } else {
847
+ do {
848
+ let fileUrl = self.getTempFilePath()
849
+ try imageData?.write(to: fileUrl)
850
+ call.resolve(["value": fileUrl.absoluteString])
851
+ } catch {
852
+ call.reject("Error writing image to file")
868
853
  }
869
854
  }
870
855
  }
@@ -919,31 +904,27 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin, CLLocationManagerDelega
919
904
  }
920
905
 
921
906
  @objc func startRecordVideo(_ call: CAPPluginCall) {
922
- DispatchQueue.main.async {
923
- do {
924
- try self.cameraController.captureVideo()
925
- call.resolve()
926
- } catch {
927
- call.reject(error.localizedDescription)
928
- }
907
+ do {
908
+ try self.cameraController.captureVideo()
909
+ call.resolve()
910
+ } catch {
911
+ call.reject(error.localizedDescription)
929
912
  }
930
913
  }
931
914
 
932
915
  @objc func stopRecordVideo(_ call: CAPPluginCall) {
933
- DispatchQueue.main.async {
934
- self.cameraController.stopRecording { (fileURL, error) in
935
- guard let fileURL = fileURL else {
936
- print(error ?? "Video capture error")
937
- guard let error = error else {
938
- call.reject("Video capture error")
939
- return
940
- }
941
- call.reject(error.localizedDescription)
916
+ self.cameraController.stopRecording { (fileURL, error) in
917
+ guard let fileURL = fileURL else {
918
+ print(error ?? "Video capture error")
919
+ guard let error = error else {
920
+ call.reject("Video capture error")
942
921
  return
943
922
  }
944
-
945
- call.resolve(["videoFilePath": fileURL.absoluteString])
923
+ call.reject(error.localizedDescription)
924
+ return
946
925
  }
926
+
927
+ call.resolve(["videoFilePath": fileURL.absoluteString])
947
928
  }
948
929
  }
949
930
 
@@ -975,21 +956,19 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin, CLLocationManagerDelega
975
956
  for device in session.devices {
976
957
  var lenses: [[String: Any]] = []
977
958
 
978
-
979
959
  let constituentDevices = device.isVirtualDevice ? device.constituentDevices : [device]
980
960
 
981
-
982
961
  for lensDevice in constituentDevices {
983
962
  var deviceType: String
984
963
  switch lensDevice.deviceType {
985
- case .builtInWideAngleCamera: deviceType = "wideAngle"
986
- case .builtInUltraWideCamera: deviceType = "ultraWide"
987
- case .builtInTelephotoCamera: deviceType = "telephoto"
988
- case .builtInDualCamera: deviceType = "dual"
989
- case .builtInDualWideCamera: deviceType = "dualWide"
990
- case .builtInTripleCamera: deviceType = "triple"
991
- case .builtInTrueDepthCamera: deviceType = "trueDepth"
992
- default: deviceType = "unknown"
964
+ case .builtInWideAngleCamera: deviceType = "wideAngle"
965
+ case .builtInUltraWideCamera: deviceType = "ultraWide"
966
+ case .builtInTelephotoCamera: deviceType = "telephoto"
967
+ case .builtInDualCamera: deviceType = "dual"
968
+ case .builtInDualWideCamera: deviceType = "dualWide"
969
+ case .builtInTripleCamera: deviceType = "triple"
970
+ case .builtInTrueDepthCamera: deviceType = "trueDepth"
971
+ default: deviceType = "unknown"
993
972
  }
994
973
 
995
974
  var baseZoomRatio: Float = 1.0
@@ -999,7 +978,6 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin, CLLocationManagerDelega
999
978
  baseZoomRatio = 2.0 // A common value for telephoto lenses
1000
979
  }
1001
980
 
1002
-
1003
981
  let lensInfo: [String: Any] = [
1004
982
  "label": lensDevice.localizedName,
1005
983
  "deviceType": deviceType,
@@ -1011,7 +989,6 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin, CLLocationManagerDelega
1011
989
  lenses.append(lensInfo)
1012
990
  }
1013
991
 
1014
-
1015
992
  let deviceData: [String: Any] = [
1016
993
  "deviceId": device.uniqueID,
1017
994
  "label": device.localizedName,
@@ -1022,7 +999,6 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin, CLLocationManagerDelega
1022
999
  "isLogical": device.isVirtualDevice
1023
1000
  ]
1024
1001
 
1025
-
1026
1002
  devices.append(deviceData)
1027
1003
  }
1028
1004
 
@@ -1039,7 +1015,6 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin, CLLocationManagerDelega
1039
1015
  let zoomInfo = try self.cameraController.getZoom()
1040
1016
  let lensInfo = try self.cameraController.getCurrentLensInfo()
1041
1017
 
1042
-
1043
1018
  var minZoom = zoomInfo.min
1044
1019
  var maxZoom = zoomInfo.max
1045
1020
  var currentZoom = zoomInfo.current
@@ -1118,47 +1093,32 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin, CLLocationManagerDelega
1118
1093
  return
1119
1094
  }
1120
1095
 
1121
- DispatchQueue.main.async { [weak self] in
1122
- guard let self = self else {
1123
- call.reject("Camera controller deallocated")
1124
- return
1125
- }
1126
-
1127
- // Disable user interaction during device swap
1128
- self.previewView.isUserInteractionEnabled = false
1129
-
1130
- DispatchQueue.global(qos: .userInitiated).async {
1131
- do {
1132
- try self.cameraController.swapToDevice(deviceId: deviceId)
1096
+ // Disable user interaction during device swap
1097
+ self.previewView.isUserInteractionEnabled = false
1133
1098
 
1134
- DispatchQueue.main.async {
1135
- // Update preview layer frame without animation
1136
- CATransaction.begin()
1137
- CATransaction.setDisableActions(true)
1138
- self.cameraController.previewLayer?.frame = self.previewView.bounds
1139
- self.cameraController.previewLayer?.videoGravity = .resizeAspectFill
1140
- CATransaction.commit()
1141
-
1142
- self.previewView.isUserInteractionEnabled = true
1099
+ do {
1100
+ try self.cameraController.swapToDevice(deviceId: deviceId)
1143
1101
 
1102
+ // Update preview layer frame without animation
1103
+ CATransaction.begin()
1104
+ CATransaction.setDisableActions(true)
1105
+ self.cameraController.previewLayer?.frame = self.previewView.bounds
1106
+ self.cameraController.previewLayer?.videoGravity = .resizeAspectFill
1107
+ CATransaction.commit()
1144
1108
 
1145
- // Ensure webview remains transparent after device switch
1146
- self.makeWebViewTransparent()
1109
+ self.previewView.isUserInteractionEnabled = true
1147
1110
 
1111
+ // Ensure webview remains transparent after device switch
1112
+ self.makeWebViewTransparent()
1148
1113
 
1149
- call.resolve()
1150
- }
1151
- } catch {
1152
- DispatchQueue.main.async {
1153
- self.previewView.isUserInteractionEnabled = true
1154
- call.reject("Failed to swap to device \(deviceId): \(error.localizedDescription)")
1155
- }
1156
- }
1157
- }
1114
+ call.resolve()
1115
+ } catch {
1116
+ self.previewView.isUserInteractionEnabled = true
1117
+ call.reject("Failed to swap to device \(deviceId): \(error.localizedDescription)")
1158
1118
  }
1159
1119
  }
1160
1120
 
1161
- @objc func getDeviceId(_ call: CAPPluginCall) {
1121
+ @objc func getDeviceId(_ call: CAPPluginCall) {
1162
1122
  guard isInitialized else {
1163
1123
  call.reject("Camera not initialized")
1164
1124
  return
@@ -1172,13 +1132,123 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin, CLLocationManagerDelega
1172
1132
  }
1173
1133
  }
1174
1134
 
1175
- public func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
1176
- self.currentLocation = locations.last
1177
- self.locationManager?.stopUpdatingLocation()
1135
+ // MARK: - Capacitor Permissions
1136
+
1137
+ private func requestLocationPermission(completion: @escaping (Bool) -> Void) {
1138
+ print("[CameraPreview] requestLocationPermission called")
1139
+ if self.locationManager == nil {
1140
+ print("[CameraPreview] Creating location manager")
1141
+ self.locationManager = CLLocationManager()
1142
+ self.locationManager?.delegate = self
1143
+ }
1144
+
1145
+ let authStatus = self.locationManager?.authorizationStatus
1146
+ print("[CameraPreview] Current authorization status: \(String(describing: authStatus))")
1147
+
1148
+ switch authStatus {
1149
+ case .authorizedWhenInUse, .authorizedAlways:
1150
+ print("[CameraPreview] Location already authorized")
1151
+ completion(true)
1152
+ case .notDetermined:
1153
+ print("[CameraPreview] Location not determined, requesting authorization...")
1154
+ self.permissionCompletion = completion
1155
+ self.locationManager?.requestWhenInUseAuthorization()
1156
+ case .denied, .restricted:
1157
+ print("[CameraPreview] Location denied or restricted")
1158
+ completion(false)
1159
+ case .none:
1160
+ print("[CameraPreview] Location manager authorization status is nil")
1161
+ completion(false)
1162
+ @unknown default:
1163
+ print("[CameraPreview] Unknown authorization status")
1164
+ completion(false)
1165
+ }
1166
+ }
1167
+
1168
+ private var permissionCompletion: ((Bool) -> Void)?
1169
+
1170
+ public func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
1171
+ let status = manager.authorizationStatus
1172
+ print("[CameraPreview] locationManagerDidChangeAuthorization called, status: \(status.rawValue), thread: \(Thread.current)")
1173
+
1174
+ // Handle pending capture call if we have one
1175
+ if let callID = self.permissionCallID, self.waitingForLocation {
1176
+ print("[CameraPreview] Found pending capture call ID: \(callID)")
1177
+
1178
+ let handleAuthorization = {
1179
+ print("[CameraPreview] Getting saved call on thread: \(Thread.current)")
1180
+ guard let call = self.bridge?.savedCall(withID: callID) else {
1181
+ print("[CameraPreview] ERROR: Could not retrieve saved call")
1182
+ self.permissionCallID = nil
1183
+ self.waitingForLocation = false
1184
+ return
1185
+ }
1186
+ print("[CameraPreview] Successfully retrieved saved call")
1187
+
1188
+ switch status {
1189
+ case .authorizedWhenInUse, .authorizedAlways:
1190
+ print("[CameraPreview] Location authorized, getting location for capture")
1191
+ self.getCurrentLocation { _ in
1192
+ self.performCapture(call: call)
1193
+ self.bridge?.releaseCall(call)
1194
+ self.permissionCallID = nil
1195
+ self.waitingForLocation = false
1196
+ }
1197
+ case .denied, .restricted:
1198
+ print("[CameraPreview] Location denied, rejecting capture")
1199
+ call.reject("Location permission denied")
1200
+ self.bridge?.releaseCall(call)
1201
+ self.permissionCallID = nil
1202
+ self.waitingForLocation = false
1203
+ case .notDetermined:
1204
+ print("[CameraPreview] Authorization not determined yet")
1205
+ // Don't do anything, wait for user response
1206
+ @unknown default:
1207
+ print("[CameraPreview] Unknown status, rejecting capture")
1208
+ call.reject("Unknown location permission status")
1209
+ self.bridge?.releaseCall(call)
1210
+ self.permissionCallID = nil
1211
+ self.waitingForLocation = false
1212
+ }
1213
+ }
1214
+
1215
+ // Check if we're already on main thread
1216
+ if Thread.isMainThread {
1217
+ print("[CameraPreview] Already on main thread")
1218
+ handleAuthorization()
1219
+ } else {
1220
+ print("[CameraPreview] Not on main thread, dispatching")
1221
+ DispatchQueue.main.async(execute: handleAuthorization)
1222
+ }
1223
+ } else {
1224
+ print("[CameraPreview] No pending capture call")
1225
+ }
1178
1226
  }
1179
1227
 
1180
1228
  public func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
1181
- print("CameraPreview: Failed to get location: \(error.localizedDescription)")
1229
+ print("[CameraPreview] locationManager didFailWithError: \(error.localizedDescription)")
1230
+ }
1231
+
1232
+ private func getCurrentLocation(completion: @escaping (CLLocation?) -> Void) {
1233
+ print("[CameraPreview] getCurrentLocation called")
1234
+ self.locationCompletion = completion
1235
+ self.locationManager?.startUpdatingLocation()
1236
+ print("[CameraPreview] Started updating location")
1237
+ }
1238
+
1239
+ private var locationCompletion: ((CLLocation?) -> Void)?
1240
+
1241
+ public func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
1242
+ print("[CameraPreview] locationManager didUpdateLocations called, locations count: \(locations.count)")
1243
+ self.currentLocation = locations.last
1244
+ if let completion = locationCompletion {
1245
+ print("[CameraPreview] Calling location completion with location: \(self.currentLocation?.description ?? "nil")")
1246
+ self.locationManager?.stopUpdatingLocation()
1247
+ completion(self.currentLocation)
1248
+ locationCompletion = nil
1249
+ } else {
1250
+ print("[CameraPreview] No location completion handler found")
1251
+ }
1182
1252
  }
1183
1253
 
1184
1254
  private func saveImageDataToGallery(imageData: Data, completion: @escaping (Bool, Error?) -> Void) {
@@ -1190,20 +1260,18 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin, CLLocationManagerDelega
1190
1260
  completion(false, error)
1191
1261
  return
1192
1262
  }
1193
-
1263
+
1194
1264
  let status = PHPhotoLibrary.authorizationStatus()
1195
-
1265
+
1196
1266
  switch status {
1197
1267
  case .authorized:
1198
1268
  performSaveDataToGallery(imageData: imageData, completion: completion)
1199
1269
  case .notDetermined:
1200
1270
  PHPhotoLibrary.requestAuthorization { newStatus in
1201
- DispatchQueue.main.async {
1202
- if newStatus == .authorized {
1203
- self.performSaveDataToGallery(imageData: imageData, completion: completion)
1204
- } else {
1205
- completion(false, NSError(domain: "CameraPreview", code: 1, userInfo: [NSLocalizedDescriptionKey: "Photo library access denied"]))
1206
- }
1271
+ if newStatus == .authorized {
1272
+ self.performSaveDataToGallery(imageData: imageData, completion: completion)
1273
+ } else {
1274
+ completion(false, NSError(domain: "CameraPreview", code: 1, userInfo: [NSLocalizedDescriptionKey: "Photo library access denied"]))
1207
1275
  }
1208
1276
  }
1209
1277
  case .denied, .restricted:
@@ -1214,28 +1282,24 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin, CLLocationManagerDelega
1214
1282
  completion(false, NSError(domain: "CameraPreview", code: 1, userInfo: [NSLocalizedDescriptionKey: "Unknown photo library authorization status"]))
1215
1283
  }
1216
1284
  }
1217
-
1285
+
1218
1286
  private func performSaveDataToGallery(imageData: Data, completion: @escaping (Bool, Error?) -> Void) {
1219
1287
  // Create a temporary file to write the JPEG data with EXIF
1220
1288
  let tempURL = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString + ".jpg")
1221
-
1289
+
1222
1290
  do {
1223
1291
  try imageData.write(to: tempURL)
1224
-
1292
+
1225
1293
  PHPhotoLibrary.shared().performChanges({
1226
1294
  PHAssetChangeRequest.creationRequestForAssetFromImage(atFileURL: tempURL)
1227
1295
  }, completionHandler: { success, error in
1228
1296
  // Clean up temporary file
1229
1297
  try? FileManager.default.removeItem(at: tempURL)
1230
-
1231
- DispatchQueue.main.async {
1232
- completion(success, error)
1233
- }
1298
+
1299
+ completion(success, error)
1234
1300
  })
1235
1301
  } catch {
1236
- DispatchQueue.main.async {
1237
- completion(false, error)
1238
- }
1302
+ completion(false, error)
1239
1303
  }
1240
1304
  }
1241
1305
 
@@ -1244,6 +1308,14 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin, CLLocationManagerDelega
1244
1308
  return
1245
1309
  }
1246
1310
 
1311
+ // Ensure UI operations happen on main thread
1312
+ guard Thread.isMainThread else {
1313
+ DispatchQueue.main.async {
1314
+ self.updateCameraFrame()
1315
+ }
1316
+ return
1317
+ }
1318
+
1247
1319
  let paddingBottom = self.paddingBottom ?? 0
1248
1320
  height -= paddingBottom
1249
1321
 
@@ -1259,7 +1331,7 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin, CLLocationManagerDelega
1259
1331
  // Handle auto-centering when position is -1
1260
1332
  if posX == -1 || posY == -1 {
1261
1333
  finalWidth = webViewWidth
1262
-
1334
+
1263
1335
  // Calculate height based on aspect ratio or use provided height
1264
1336
  if let aspectRatio = self.aspectRatio {
1265
1337
  let ratioParts = aspectRatio.split(separator: ":").compactMap { Double($0) }
@@ -1269,9 +1341,9 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin, CLLocationManagerDelega
1269
1341
  finalHeight = finalWidth / CGFloat(ratio)
1270
1342
  }
1271
1343
  }
1272
-
1344
+
1273
1345
  finalX = posX == -1 ? 0 : posX
1274
-
1346
+
1275
1347
  if posY == -1 {
1276
1348
  let availableHeight = webViewHeight - paddingBottom
1277
1349
  finalY = finalHeight < availableHeight ? (availableHeight - finalHeight) / 2 : 0
@@ -1287,7 +1359,7 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin, CLLocationManagerDelega
1287
1359
  // For camera, use portrait orientation: 4:3 becomes 3:4, 16:9 becomes 9:16
1288
1360
  let ratio = ratioParts[1] / ratioParts[0]
1289
1361
  let currentRatio = Double(finalWidth) / Double(finalHeight)
1290
-
1362
+
1291
1363
  if currentRatio > ratio {
1292
1364
  let newWidth = Double(finalHeight) * ratio
1293
1365
  frame.origin.x = finalX + (Double(finalWidth) - newWidth) / 2
@@ -1303,7 +1375,7 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin, CLLocationManagerDelega
1303
1375
  // Disable ALL animations for frame updates - we want instant positioning
1304
1376
  CATransaction.begin()
1305
1377
  CATransaction.setDisableActions(true)
1306
-
1378
+
1307
1379
  // Batch UI updates for better performance
1308
1380
  if self.previewView == nil {
1309
1381
  self.previewView = UIView(frame: frame)
@@ -1316,12 +1388,12 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin, CLLocationManagerDelega
1316
1388
  if let previewLayer = self.cameraController.previewLayer {
1317
1389
  previewLayer.frame = self.previewView.bounds
1318
1390
  }
1319
-
1391
+
1320
1392
  // Update grid overlay frame if it exists
1321
1393
  if let gridOverlay = self.cameraController.gridOverlayView {
1322
1394
  gridOverlay.frame = self.previewView.bounds
1323
1395
  }
1324
-
1396
+
1325
1397
  CATransaction.commit()
1326
1398
  }
1327
1399
 
@@ -1330,12 +1402,15 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin, CLLocationManagerDelega
1330
1402
  call.reject("camera not started")
1331
1403
  return
1332
1404
  }
1333
- var result = JSObject()
1334
- result["x"] = Double(self.previewView.frame.origin.x)
1335
- result["y"] = Double(self.previewView.frame.origin.y)
1336
- result["width"] = Double(self.previewView.frame.width)
1337
- result["height"] = Double(self.previewView.frame.height)
1338
- call.resolve(result)
1405
+
1406
+ DispatchQueue.main.async {
1407
+ var result = JSObject()
1408
+ result["x"] = Double(self.previewView.frame.origin.x)
1409
+ result["y"] = Double(self.previewView.frame.origin.y)
1410
+ result["width"] = Double(self.previewView.frame.width)
1411
+ result["height"] = Double(self.previewView.frame.height)
1412
+ call.resolve(result)
1413
+ }
1339
1414
  }
1340
1415
 
1341
1416
  @objc func setPreviewSize(_ call: CAPPluginCall) {
@@ -1343,27 +1418,29 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin, CLLocationManagerDelega
1343
1418
  call.reject("camera not started")
1344
1419
  return
1345
1420
  }
1346
-
1421
+
1347
1422
  // Only update position if explicitly provided, otherwise keep auto-centering
1348
- if let x = call.getInt("x") {
1349
- self.posX = CGFloat(x)
1423
+ if let x = call.getInt("x") {
1424
+ self.posX = CGFloat(x)
1350
1425
  }
1351
- if let y = call.getInt("y") {
1352
- self.posY = CGFloat(y)
1426
+ if let y = call.getInt("y") {
1427
+ self.posY = CGFloat(y)
1353
1428
  }
1354
1429
  if let width = call.getInt("width") { self.width = CGFloat(width) }
1355
1430
  if let height = call.getInt("height") { self.height = CGFloat(height) }
1356
-
1357
- // Direct update without animation for better performance
1358
- self.updateCameraFrame()
1359
- self.makeWebViewTransparent()
1360
-
1361
- // Return the actual preview bounds
1362
- var result = JSObject()
1363
- result["x"] = Double(self.previewView.frame.origin.x)
1364
- result["y"] = Double(self.previewView.frame.origin.y)
1365
- result["width"] = Double(self.previewView.frame.width)
1366
- result["height"] = Double(self.previewView.frame.height)
1367
- call.resolve(result)
1431
+
1432
+ DispatchQueue.main.async {
1433
+ // Direct update without animation for better performance
1434
+ self.updateCameraFrame()
1435
+ self.makeWebViewTransparent()
1436
+
1437
+ // Return the actual preview bounds
1438
+ var result = JSObject()
1439
+ result["x"] = Double(self.previewView.frame.origin.x)
1440
+ result["y"] = Double(self.previewView.frame.origin.y)
1441
+ result["width"] = Double(self.previewView.frame.width)
1442
+ result["height"] = Double(self.previewView.frame.height)
1443
+ call.resolve(result)
1444
+ }
1368
1445
  }
1369
1446
  }