@capgo/camera-preview 7.4.0-alpha.0 → 7.4.0-alpha.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.
@@ -25,7 +25,7 @@ class CameraController: NSObject {
25
25
  var focusIndicatorView: UIView?
26
26
 
27
27
  var flashMode = AVCaptureDevice.FlashMode.off
28
- var photoCaptureCompletionBlock: ((UIImage?, Error?) -> Void)?
28
+ var photoCaptureCompletionBlock: ((UIImage?, Data?, [AnyHashable: Any]?, Error?) -> Void)?
29
29
 
30
30
  var sampleBufferCaptureCompletionBlock: ((UIImage?, Error?) -> Void)?
31
31
 
@@ -51,6 +51,25 @@ class CameraController: NSObject {
51
51
  // A rear multi-lens virtual camera will have a min zoom of 1.0 but support wider angles
52
52
  return device.position == .back && device.isVirtualDevice && device.constituentDevices.count > 1
53
53
  }
54
+
55
+ // Returns the display zoom multiplier introduced in iOS 18 to map between
56
+ // native zoom factor and the UI-displayed zoom factor. Falls back to 1.0 on
57
+ // older systems or if the property is unavailable.
58
+ func getDisplayZoomMultiplier() -> Float {
59
+ var multiplier: Float = 1.0
60
+ // Use KVC to avoid compile-time dependency on the iOS 18 SDK symbol
61
+ let device = (currentCameraPosition == .rear) ? rearCamera : frontCamera
62
+ if #available(iOS 18.0, *), let device = device {
63
+ if let value = device.value(forKey: "displayVideoZoomFactorMultiplier") as? NSNumber {
64
+ let m = value.floatValue
65
+ if m > 0 { multiplier = m }
66
+ }
67
+ }
68
+ return multiplier
69
+ }
70
+
71
+ // Track whether an aspect ratio was explicitly requested
72
+ var requestedAspectRatio: String?
54
73
  }
55
74
 
56
75
  extension CameraController {
@@ -68,7 +87,7 @@ extension CameraController {
68
87
  discoverAndConfigureCameras()
69
88
  }
70
89
 
71
- private func discoverAndConfigureCameras() {
90
+ private func discoverAndConfigureCameras() {
72
91
  let deviceTypes: [AVCaptureDevice.DeviceType] = [
73
92
  .builtInWideAngleCamera,
74
93
  .builtInUltraWideCamera,
@@ -82,8 +101,6 @@ extension CameraController {
82
101
  let session = AVCaptureDevice.DiscoverySession(deviceTypes: deviceTypes, mediaType: AVMediaType.video, position: .unspecified)
83
102
  let cameras = session.devices.compactMap { $0 }
84
103
 
85
-
86
-
87
104
  // Store all discovered devices for fast lookup later
88
105
  self.allDiscoveredDevices = cameras
89
106
 
@@ -94,7 +111,7 @@ extension CameraController {
94
111
 
95
112
  }
96
113
 
97
- // Set front camera (usually just one option)
114
+ // Set front camera (usually just one option)
98
115
  self.frontCamera = cameras.first(where: { $0.position == .front })
99
116
 
100
117
  // Find rear camera - prefer tripleCamera for multi-lens support
@@ -171,7 +188,7 @@ extension CameraController {
171
188
  self.outputsPrepared = true
172
189
  }
173
190
 
174
- func prepare(cameraPosition: String, deviceId: String? = nil, disableAudio: Bool, cameraMode: Bool, aspectRatio: String? = nil, initialZoomLevel: Float = 1.0, completionHandler: @escaping (Error?) -> Void) {
191
+ func prepare(cameraPosition: String, deviceId: String? = nil, disableAudio: Bool, cameraMode: Bool, aspectRatio: String? = nil, initialZoomLevel: Float?, completionHandler: @escaping (Error?) -> Void) {
175
192
  print("[CameraPreview] 🎬 Starting prepare - position: \(cameraPosition), deviceId: \(deviceId ?? "nil"), disableAudio: \(disableAudio), cameraMode: \(cameraMode), aspectRatio: \(aspectRatio ?? "nil"), zoom: \(initialZoomLevel)")
176
193
 
177
194
  DispatchQueue.global(qos: .userInitiated).async { [weak self] in
@@ -198,7 +215,8 @@ extension CameraController {
198
215
  // Configure the session
199
216
  captureSession.beginConfiguration()
200
217
 
201
- // Set aspect ratio preset
218
+ // Set aspect ratio preset and remember requested ratio
219
+ self.requestedAspectRatio = aspectRatio
202
220
  self.configureSessionPreset(for: aspectRatio)
203
221
 
204
222
  // Configure device inputs
@@ -255,15 +273,18 @@ extension CameraController {
255
273
  guard let captureSession = self.captureSession else { return }
256
274
 
257
275
  var targetPreset: AVCaptureSession.Preset = .high
258
-
259
276
  if let aspectRatio = aspectRatio {
260
277
  switch aspectRatio {
261
278
  case "16:9":
262
- targetPreset = captureSession.canSetSessionPreset(.hd1920x1080) ? .hd1920x1080 : .high
279
+ if captureSession.canSetSessionPreset(.hd4K3840x2160) {
280
+ targetPreset = .hd4K3840x2160
281
+ } else if captureSession.canSetSessionPreset(.hd1920x1080) {
282
+ targetPreset = .hd1920x1080
283
+ }
263
284
  case "4:3":
264
- targetPreset = captureSession.canSetSessionPreset(.photo) ? .photo : .high
285
+ targetPreset = captureSession.canSetSessionPreset(.high) ? .high : captureSession.sessionPreset
265
286
  default:
266
- targetPreset = .high
287
+ targetPreset = captureSession.canSetSessionPreset(.high) ? .high : captureSession.sessionPreset
267
288
  }
268
289
  }
269
290
 
@@ -272,7 +293,7 @@ extension CameraController {
272
293
  }
273
294
  }
274
295
 
275
- private func setInitialZoom(level: Float) {
296
+ private func setInitialZoom(level: Float?) {
276
297
  let device = (currentCameraPosition == .rear) ? rearCamera : frontCamera
277
298
  guard let device = device else {
278
299
  print("[CameraPreview] No device available for initial zoom")
@@ -282,7 +303,12 @@ extension CameraController {
282
303
  let minZoom = device.minAvailableVideoZoomFactor
283
304
  let maxZoom = min(device.maxAvailableVideoZoomFactor, saneMaxZoomFactor)
284
305
 
285
- let adjustedLevel = level
306
+ // Compute UI-level default = 1 * multiplier when not provided
307
+ let multiplier = self.getDisplayZoomMultiplier()
308
+ // if level is nil, it's the initial zoom
309
+ let uiLevel: Float = level ?? (2.0 * multiplier)
310
+ // Map UI/display zoom to native zoom using iOS 18+ multiplier
311
+ let adjustedLevel = multiplier != 1.0 ? (uiLevel / multiplier) : uiLevel
286
312
 
287
313
  guard CGFloat(adjustedLevel) >= minZoom && CGFloat(adjustedLevel) <= maxZoom else {
288
314
  print("[CameraPreview] Initial zoom level \(adjustedLevel) out of range (\(minZoom)-\(maxZoom))")
@@ -378,7 +404,8 @@ extension CameraController {
378
404
  }
379
405
 
380
406
  // Fast configuration without CATransaction overhead
381
- previewLayer.videoGravity = .resizeAspectFill
407
+ // Use resizeAspect to avoid crop when no aspect ratio is requested; otherwise fill
408
+ previewLayer.videoGravity = (requestedAspectRatio == nil) ? .resizeAspect : .resizeAspectFill
382
409
  previewLayer.frame = view.bounds
383
410
 
384
411
  // Insert layer immediately (only if new)
@@ -557,15 +584,22 @@ extension CameraController {
557
584
  self.updateVideoOrientation()
558
585
  }
559
586
 
560
- func captureImage(width: Int?, height: Int?, aspectRatio: String?, quality: Float, gpsLocation: CLLocation?, completion: @escaping (UIImage?, Error?) -> Void) {
587
+ func captureImage(width: Int?, height: Int?, aspectRatio: String?, quality: Float, gpsLocation: CLLocation?, completion: @escaping (UIImage?, Data?, [AnyHashable: Any]?, Error?) -> Void) {
561
588
  print("[CameraPreview] captureImage called - width: \(width ?? -1), height: \(height ?? -1), aspectRatio: \(aspectRatio ?? "nil")")
562
589
 
563
590
  guard let photoOutput = self.photoOutput else {
564
- completion(nil, NSError(domain: "Camera", code: 0, userInfo: [NSLocalizedDescriptionKey: "Photo output is not available"]))
591
+ completion(nil, nil, nil, NSError(domain: "Camera", code: 0, userInfo: [NSLocalizedDescriptionKey: "Photo output is not available"]))
565
592
  return
566
593
  }
567
594
 
568
595
  let settings = AVCapturePhotoSettings()
596
+ // Request highest quality photo capture
597
+ if #available(iOS 13.0, *) {
598
+ settings.isHighResolutionPhotoEnabled = true
599
+ }
600
+ if #available(iOS 15.0, *) {
601
+ settings.photoQualityPrioritization = .balanced
602
+ }
569
603
 
570
604
  // Apply the current flash mode to the photo settings
571
605
  // Check if the current device supports flash
@@ -587,14 +621,14 @@ extension CameraController {
587
621
  }
588
622
  }
589
623
 
590
- self.photoCaptureCompletionBlock = { (image, error) in
624
+ self.photoCaptureCompletionBlock = { (image, photoData, metadata, error) in
591
625
  if let error = error {
592
- completion(nil, error)
626
+ completion(nil, nil, nil, error)
593
627
  return
594
628
  }
595
629
 
596
630
  guard let image = image else {
597
- completion(nil, NSError(domain: "Camera", code: 0, userInfo: [NSLocalizedDescriptionKey: "Failed to capture image"]))
631
+ completion(nil, nil, nil, NSError(domain: "Camera", code: 0, userInfo: [NSLocalizedDescriptionKey: "Failed to capture image"]))
598
632
  return
599
633
  }
600
634
 
@@ -648,7 +682,7 @@ extension CameraController {
648
682
  }
649
683
  }
650
684
 
651
- completion(finalImage, nil)
685
+ completion(finalImage, photoData, metadata, nil)
652
686
  }
653
687
 
654
688
  photoOutput.capturePhoto(with: settings, delegate: self)
@@ -740,8 +774,6 @@ extension CameraController {
740
774
 
741
775
  }
742
776
 
743
-
744
-
745
777
  // Create the cropped image
746
778
  guard let cgImage = image.cgImage,
747
779
  let croppedCGImage = cgImage.cropping(to: cropRect) else {
@@ -751,7 +783,6 @@ extension CameraController {
751
783
 
752
784
  let result = UIImage(cgImage: croppedCGImage, scale: image.scale, orientation: image.imageOrientation)
753
785
 
754
-
755
786
  return result
756
787
  }
757
788
 
@@ -809,6 +840,7 @@ extension CameraController {
809
840
  return supportedFlashModesAsStrings
810
841
 
811
842
  }
843
+
812
844
  func getHorizontalFov() throws -> Float {
813
845
  var currentCamera: AVCaptureDevice?
814
846
  switch currentCameraPosition {
@@ -835,6 +867,7 @@ extension CameraController {
835
867
 
836
868
  return adjustedFov
837
869
  }
870
+
838
871
  func setFlashMode(flashMode: AVCaptureDevice.FlashMode) throws {
839
872
  var currentCamera: AVCaptureDevice?
840
873
  switch currentCameraPosition {
@@ -908,6 +941,7 @@ extension CameraController {
908
941
 
909
942
  func getZoom() throws -> (min: Float, max: Float, current: Float) {
910
943
  var currentCamera: AVCaptureDevice?
944
+
911
945
  switch currentCameraPosition {
912
946
  case .front:
913
947
  currentCamera = self.frontCamera
@@ -962,9 +996,9 @@ extension CameraController {
962
996
  self.zoomFactor = zoomLevel
963
997
 
964
998
  // Trigger autofocus after zoom if requested
965
- // if autoFocus {
966
- // self.triggerAutoFocus()
967
- // }
999
+ if autoFocus {
1000
+ self.triggerAutoFocus()
1001
+ }
968
1002
  } catch {
969
1003
  throw CameraControllerError.invalidOperation
970
1004
  }
@@ -1465,22 +1499,23 @@ extension CameraController: UIGestureRecognizerDelegate {
1465
1499
  extension CameraController: AVCapturePhotoCaptureDelegate {
1466
1500
  public func photoOutput(_ output: AVCapturePhotoOutput, didFinishProcessingPhoto photo: AVCapturePhoto, error: Error?) {
1467
1501
  if let error = error {
1468
- self.photoCaptureCompletionBlock?(nil, error)
1502
+ self.photoCaptureCompletionBlock?(nil, nil, nil, error)
1469
1503
  return
1470
1504
  }
1471
1505
 
1472
1506
  // Get the photo data using the modern API
1473
1507
  guard let imageData = photo.fileDataRepresentation() else {
1474
- self.photoCaptureCompletionBlock?(nil, CameraControllerError.unknown)
1508
+ self.photoCaptureCompletionBlock?(nil, nil, nil, CameraControllerError.unknown)
1475
1509
  return
1476
1510
  }
1477
1511
 
1478
1512
  guard let image = UIImage(data: imageData) else {
1479
- self.photoCaptureCompletionBlock?(nil, CameraControllerError.unknown)
1513
+ self.photoCaptureCompletionBlock?(nil, nil, nil, CameraControllerError.unknown)
1480
1514
  return
1481
1515
  }
1482
1516
 
1483
- self.photoCaptureCompletionBlock?(image.fixedOrientation(), nil)
1517
+ // Pass through original file data and metadata so callers can preserve EXIF
1518
+ self.photoCaptureCompletionBlock?(image.fixedOrientation(), imageData, photo.metadata, nil)
1484
1519
  }
1485
1520
  }
1486
1521