@capgo/camera-preview 7.6.1-alpha.3 → 7.7.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.
@@ -100,48 +100,7 @@ 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
-
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)
103
+ return (width: CGFloat(components[0]), height: CGFloat(components[1]))
145
104
  }
146
105
  }
147
106
 
@@ -381,38 +340,6 @@ extension CameraController {
381
340
  }
382
341
  }
383
342
 
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
-
416
343
  private func setInitialZoom(level: Float?) {
417
344
  let device = (currentCameraPosition == .rear) ? rearCamera : frontCamera
418
345
  guard let device = device else {
@@ -502,6 +429,20 @@ extension CameraController {
502
429
  }
503
430
  }
504
431
  }
432
+
433
+ // Set default exposure mode to CONTINUOUS when starting the camera
434
+ do {
435
+ try finalDevice.lockForConfiguration()
436
+ if finalDevice.isExposureModeSupported(.continuousAutoExposure) {
437
+ finalDevice.exposureMode = .continuousAutoExposure
438
+ if finalDevice.isExposurePointOfInterestSupported {
439
+ finalDevice.exposurePointOfInterest = CGPoint(x: 0.5, y: 0.5)
440
+ }
441
+ }
442
+ finalDevice.unlockForConfiguration()
443
+ } catch {
444
+ // Non-fatal; continue without setting default exposure
445
+ }
505
446
  }
506
447
 
507
448
  func displayPreview(on view: UIView) throws {
@@ -603,7 +544,7 @@ extension CameraController {
603
544
  }
604
545
 
605
546
  private func updateVideoOrientationOnMainThread() {
606
- var videoOrientation: AVCaptureVideoOrientation
547
+ let videoOrientation: AVCaptureVideoOrientation
607
548
 
608
549
  // Use window scene interface orientation
609
550
  if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene {
@@ -754,7 +695,7 @@ extension CameraController {
754
695
  }
755
696
 
756
697
  func captureImage(width: Int?, height: Int?, quality: Float, gpsLocation: CLLocation?, completion: @escaping (UIImage?, Data?, [AnyHashable: Any]?, Error?) -> Void) {
757
- print("[CameraPreview] captureImage called - width: \(width ?? -1), height: \(height ?? -1), requestedAspectRatio: \(self.requestedAspectRatio ?? "nil")")
698
+ print("[CameraPreview] captureImage called - width: \(width ?? -1), height: \(height ?? -1)")
758
699
 
759
700
  guard let photoOutput = self.photoOutput else {
760
701
  completion(nil, nil, nil, NSError(domain: "Camera", code: 0, userInfo: [NSLocalizedDescriptionKey: "Photo output is not available"]))
@@ -823,9 +764,18 @@ extension CameraController {
823
764
  print("[CameraPreview] Resized to max dimensions: \(finalImage.size.width)x\(finalImage.size.height)")
824
765
  } else if let aspectRatio = self.requestedAspectRatio {
825
766
  // No max dimensions specified, but aspect ratio is specified
826
- // Always apply aspect ratio cropping to ensure correct orientation
827
- finalImage = self.cropImageToAspectRatio(image: image, aspectRatio: aspectRatio) ?? image
828
- print("[CameraPreview] Applied aspect ratio cropping for \(aspectRatio): \(finalImage.size.width)x\(finalImage.size.height)")
767
+ // If we used session preset (low-res), image should already be correct aspect ratio
768
+ // If we used high-res (shouldn't happen without max dimensions), crop it
769
+ let imageAspectRatio = image.size.width / image.size.height
770
+ if let targetRatio = self.parseAspectRatio(aspectRatio) {
771
+ let targetAspectRatio = targetRatio.width / targetRatio.height
772
+
773
+ // Allow small tolerance for aspect ratio comparison
774
+ if abs(imageAspectRatio - targetAspectRatio) > 0.01 {
775
+ finalImage = self.cropImageToAspectRatio(image: image, aspectRatio: aspectRatio) ?? image
776
+ print("[CameraPreview] Cropped to match aspect ratio \(aspectRatio): \(finalImage.size.width)x\(finalImage.size.height)")
777
+ }
778
+ }
829
779
  }
830
780
 
831
781
  completion(finalImage, photoData, metadata, nil)
@@ -907,27 +857,13 @@ extension CameraController {
907
857
 
908
858
  func cropImageToAspectRatio(image: UIImage, aspectRatio: String) -> UIImage? {
909
859
  guard let ratio = parseAspectRatio(aspectRatio) else {
910
- print("[CameraPreview] cropImageToAspectRatio - Failed to parse aspect ratio: \(aspectRatio)")
911
860
  return image
912
861
  }
913
862
 
914
- // Only normalize the image orientation if it's not already correct
915
- let normalizedImage: UIImage
916
- if image.imageOrientation == .up {
917
- normalizedImage = image
918
- print("[CameraPreview] cropImageToAspectRatio - Image already has correct orientation")
919
- } else {
920
- normalizedImage = image.fixedOrientation() ?? image
921
- print("[CameraPreview] cropImageToAspectRatio - Normalized image orientation from \(image.imageOrientation.rawValue) to .up")
922
- }
923
-
924
- let imageSize = normalizedImage.size
863
+ let imageSize = image.size
925
864
  let imageAspectRatio = imageSize.width / imageSize.height
926
865
  let targetAspectRatio = ratio.width / ratio.height
927
866
 
928
- print("[CameraPreview] cropImageToAspectRatio - Original image: \(imageSize.width)x\(imageSize.height) (ratio: \(imageAspectRatio))")
929
- print("[CameraPreview] cropImageToAspectRatio - Target ratio: \(ratio.width):\(ratio.height) (ratio: \(targetAspectRatio))")
930
-
931
867
  var cropRect: CGRect
932
868
 
933
869
  if imageAspectRatio > targetAspectRatio {
@@ -935,36 +871,19 @@ extension CameraController {
935
871
  let targetWidth = imageSize.height * targetAspectRatio
936
872
  let xOffset = (imageSize.width - targetWidth) / 2
937
873
  cropRect = CGRect(x: xOffset, y: 0, width: targetWidth, height: imageSize.height)
938
- print("[CameraPreview] cropImageToAspectRatio - Horizontal crop: \(cropRect)")
939
874
  } else {
940
875
  // Image is taller than target - crop vertically (center crop)
941
876
  let targetHeight = imageSize.width / targetAspectRatio
942
877
  let yOffset = (imageSize.height - targetHeight) / 2
943
878
  cropRect = CGRect(x: 0, y: yOffset, width: imageSize.width, height: targetHeight)
944
- print("[CameraPreview] cropImageToAspectRatio - Vertical crop: \(cropRect) - Target height: \(targetHeight)")
945
879
  }
946
880
 
947
- // Validate crop rect is within image bounds
948
- if cropRect.minX < 0 || cropRect.minY < 0 ||
949
- cropRect.maxX > imageSize.width || cropRect.maxY > imageSize.height {
950
- print("[CameraPreview] cropImageToAspectRatio - Warning: Crop rect \(cropRect) exceeds image bounds \(imageSize)")
951
- // Adjust crop rect to fit within image bounds
952
- cropRect = cropRect.intersection(CGRect(origin: .zero, size: imageSize))
953
- print("[CameraPreview] cropImageToAspectRatio - Adjusted crop rect: \(cropRect)")
954
- }
955
-
956
- guard let cgImage = normalizedImage.cgImage,
881
+ guard let cgImage = image.cgImage,
957
882
  let croppedCGImage = cgImage.cropping(to: cropRect) else {
958
- print("[CameraPreview] cropImageToAspectRatio - Failed to crop image")
959
883
  return nil
960
884
  }
961
885
 
962
- let croppedImage = UIImage(cgImage: croppedCGImage, scale: normalizedImage.scale, orientation: .up)
963
- let finalAspectRatio = croppedImage.size.width / croppedImage.size.height
964
- print("[CameraPreview] cropImageToAspectRatio - Final cropped image: \(croppedImage.size.width)x\(croppedImage.size.height) (ratio: \(finalAspectRatio))")
965
-
966
- // Create the cropped image with normalized orientation
967
- return croppedImage
886
+ return UIImage(cgImage: croppedCGImage, scale: image.scale, orientation: image.imageOrientation)
968
887
  }
969
888
 
970
889
  func cropImageToMatchPreview(image: UIImage, previewLayer: AVCaptureVideoPreviewLayer) -> UIImage? {
@@ -1262,17 +1181,12 @@ extension CameraController {
1262
1181
  }
1263
1182
  }
1264
1183
 
1265
- // Also set exposure point if supported
1266
- if device.isExposurePointOfInterestSupported && device.isExposureModeSupported(.autoExpose) {
1267
- device.exposureMode = .autoExpose
1268
- device.exposurePointOfInterest = centerPoint
1269
- } else if device.isExposureModeSupported(.continuousAutoExposure) {
1270
- device.exposureMode = .continuousAutoExposure
1271
- if device.isExposurePointOfInterestSupported {
1184
+ if device.isExposurePointOfInterestSupported {
1185
+ let exposureMode = try getExposureMode()
1186
+ if exposureMode == "AUTO" || exposureMode == "CONTINUOUS" {
1272
1187
  device.exposurePointOfInterest = centerPoint
1273
1188
  }
1274
1189
  }
1275
-
1276
1190
  device.unlockForConfiguration()
1277
1191
  } catch {
1278
1192
  // Silently ignore errors during autofocus
@@ -1327,6 +1241,7 @@ extension CameraController {
1327
1241
  // Also set exposure point if supported
1328
1242
  if device.isExposurePointOfInterestSupported && device.isExposureModeSupported(.autoExpose) {
1329
1243
  device.exposureMode = .autoExpose
1244
+ device.setExposureTargetBias(0.0) { _ in }
1330
1245
  device.exposurePointOfInterest = point
1331
1246
  }
1332
1247
 
@@ -1533,6 +1448,172 @@ extension CameraController {
1533
1448
  self.firstFrameReadyCallback = nil
1534
1449
  }
1535
1450
 
1451
+ // MARK: - Exposure Controls
1452
+
1453
+ func getExposureModes() throws -> [String] {
1454
+ var currentCamera: AVCaptureDevice?
1455
+ switch currentCameraPosition {
1456
+ case .front:
1457
+ currentCamera = self.frontCamera
1458
+ case .rear:
1459
+ currentCamera = self.rearCamera
1460
+ default:
1461
+ break
1462
+ }
1463
+
1464
+ guard let device = currentCamera else {
1465
+ throw CameraControllerError.noCamerasAvailable
1466
+ }
1467
+
1468
+ var modes: [String] = []
1469
+ if device.isExposureModeSupported(.locked) { modes.append("LOCK") }
1470
+ if device.isExposureModeSupported(.autoExpose) { modes.append("AUTO") }
1471
+ if device.isExposureModeSupported(.continuousAutoExposure) { modes.append("CONTINUOUS") }
1472
+ if device.isExposureModeSupported(.custom) { modes.append("CUSTOM") }
1473
+ return modes
1474
+ }
1475
+
1476
+ func getExposureMode() throws -> String {
1477
+ var currentCamera: AVCaptureDevice?
1478
+ switch currentCameraPosition {
1479
+ case .front:
1480
+ currentCamera = self.frontCamera
1481
+ case .rear:
1482
+ currentCamera = self.rearCamera
1483
+ default:
1484
+ break
1485
+ }
1486
+
1487
+ guard let device = currentCamera else {
1488
+ throw CameraControllerError.noCamerasAvailable
1489
+ }
1490
+
1491
+ switch device.exposureMode {
1492
+ case .locked:
1493
+ return "LOCK"
1494
+ case .autoExpose:
1495
+ return "AUTO"
1496
+ case .continuousAutoExposure:
1497
+ return "CONTINUOUS"
1498
+ case .custom:
1499
+ return "CUSTOM"
1500
+ @unknown default:
1501
+ return "CONTINUOUS"
1502
+ }
1503
+ }
1504
+
1505
+ func setExposureMode(mode: String) throws {
1506
+ var currentCamera: AVCaptureDevice?
1507
+ switch currentCameraPosition {
1508
+ case .front:
1509
+ currentCamera = self.frontCamera
1510
+ case .rear:
1511
+ currentCamera = self.rearCamera
1512
+ default:
1513
+ break
1514
+ }
1515
+
1516
+ guard let device = currentCamera else {
1517
+ throw CameraControllerError.noCamerasAvailable
1518
+ }
1519
+
1520
+ let normalized = mode.uppercased()
1521
+ let desiredMode: AVCaptureDevice.ExposureMode?
1522
+ switch normalized {
1523
+ case "LOCK":
1524
+ desiredMode = .locked
1525
+ case "AUTO":
1526
+ desiredMode = .autoExpose
1527
+ case "CONTINUOUS":
1528
+ desiredMode = .continuousAutoExposure
1529
+ case "CUSTOM":
1530
+ desiredMode = .custom
1531
+ default:
1532
+ desiredMode = .continuousAutoExposure
1533
+ }
1534
+
1535
+ guard let finalMode = desiredMode, device.isExposureModeSupported(finalMode) else {
1536
+ throw CameraControllerError.invalidOperation
1537
+ }
1538
+
1539
+ do {
1540
+ try device.lockForConfiguration()
1541
+ device.exposureMode = finalMode
1542
+ // Reset EV to 0 when switching to AUTO or CONTINUOUS
1543
+ if finalMode == .autoExpose || finalMode == .continuousAutoExposure {
1544
+ device.setExposureTargetBias(0.0) { _ in }
1545
+ }
1546
+ device.unlockForConfiguration()
1547
+ } catch {
1548
+ throw CameraControllerError.invalidOperation
1549
+ }
1550
+ }
1551
+
1552
+ func getExposureCompensationRange() throws -> (min: Float, max: Float, step: Float) {
1553
+ var currentCamera: AVCaptureDevice?
1554
+ switch currentCameraPosition {
1555
+ case .front:
1556
+ currentCamera = self.frontCamera
1557
+ case .rear:
1558
+ currentCamera = self.rearCamera
1559
+ default:
1560
+ break
1561
+ }
1562
+
1563
+ guard let device = currentCamera else {
1564
+ throw CameraControllerError.noCamerasAvailable
1565
+ }
1566
+
1567
+ // iOS reports EV bias directly; typical step is 0.1 or 0.125 depending on device
1568
+ // There's no direct API for step; approximate as 0.1 for compatibility
1569
+ let step: Float = 0.1
1570
+ return (min: device.minExposureTargetBias, max: device.maxExposureTargetBias, step: step)
1571
+ }
1572
+
1573
+ func getExposureCompensation() throws -> Float {
1574
+ var currentCamera: AVCaptureDevice?
1575
+ switch currentCameraPosition {
1576
+ case .front:
1577
+ currentCamera = self.frontCamera
1578
+ case .rear:
1579
+ currentCamera = self.rearCamera
1580
+ default:
1581
+ break
1582
+ }
1583
+
1584
+ guard let device = currentCamera else {
1585
+ throw CameraControllerError.noCamerasAvailable
1586
+ }
1587
+
1588
+ return device.exposureTargetBias
1589
+ }
1590
+
1591
+ func setExposureCompensation(_ value: Float) throws {
1592
+ var currentCamera: AVCaptureDevice?
1593
+ switch currentCameraPosition {
1594
+ case .front:
1595
+ currentCamera = self.frontCamera
1596
+ case .rear:
1597
+ currentCamera = self.rearCamera
1598
+ default:
1599
+ break
1600
+ }
1601
+
1602
+ guard let device = currentCamera else {
1603
+ throw CameraControllerError.noCamerasAvailable
1604
+ }
1605
+
1606
+ let clamped = max(device.minExposureTargetBias, min(value, device.maxExposureTargetBias))
1607
+
1608
+ do {
1609
+ try device.lockForConfiguration()
1610
+ device.setExposureTargetBias(clamped) { _ in }
1611
+ device.unlockForConfiguration()
1612
+ } catch {
1613
+ throw CameraControllerError.invalidOperation
1614
+ }
1615
+ }
1616
+
1536
1617
  func captureVideo() throws {
1537
1618
  guard let captureSession = self.captureSession, captureSession.isRunning else {
1538
1619
  throw CameraControllerError.captureSessionIsMissing
@@ -1545,9 +1626,23 @@ extension CameraController {
1545
1626
  throw CameraControllerError.fileVideoOutputNotFound
1546
1627
  }
1547
1628
 
1629
+ // Ensure the movie file output is attached to the active session.
1630
+ // If the camera was started without cameraMode=true, the output may not have been added yet.
1631
+ if !captureSession.outputs.contains(where: { $0 === fileVideoOutput }) {
1632
+ captureSession.beginConfiguration()
1633
+ if captureSession.canAddOutput(fileVideoOutput) {
1634
+ captureSession.addOutput(fileVideoOutput)
1635
+ } else {
1636
+ captureSession.commitConfiguration()
1637
+ throw CameraControllerError.invalidOperation
1638
+ }
1639
+ captureSession.commitConfiguration()
1640
+ }
1641
+
1548
1642
  // cpcp_video_A6C01203 - portrait
1549
1643
  //
1550
1644
  if let connection = fileVideoOutput.connection(with: .video) {
1645
+ if connection.isEnabled == false { connection.isEnabled = true }
1551
1646
  switch UIDevice.current.orientation {
1552
1647
  case .landscapeRight:
1553
1648
  connection.videoOrientation = .landscapeLeft
@@ -1626,6 +1721,7 @@ extension CameraController: UIGestureRecognizerDelegate {
1626
1721
  if device.isExposurePointOfInterestSupported && device.isExposureModeSupported(exposureMode) {
1627
1722
  device.exposurePointOfInterest = CGPoint(x: CGFloat(devicePoint?.x ?? 0), y: CGFloat(devicePoint?.y ?? 0))
1628
1723
  device.exposureMode = exposureMode
1724
+ device.setExposureTargetBias(0.0) { _ in }
1629
1725
  }
1630
1726
  } catch {
1631
1727
  debugPrint(error)
@@ -1765,8 +1861,7 @@ extension CameraController: AVCapturePhotoCaptureDelegate {
1765
1861
  }
1766
1862
 
1767
1863
  // Pass through original file data and metadata so callers can preserve EXIF
1768
- // Don't call fixedOrientation() here - let the completion block handle it after cropping
1769
- self.photoCaptureCompletionBlock?(image, imageData, photo.metadata, nil)
1864
+ self.photoCaptureCompletionBlock?(image.fixedOrientation(), imageData, photo.metadata, nil)
1770
1865
  }
1771
1866
  }
1772
1867
 
@@ -69,7 +69,14 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin, CLLocationManagerDelega
69
69
  CAPPluginMethod(name: "setFocus", returnType: CAPPluginReturnPromise),
70
70
  CAPPluginMethod(name: "deleteFile", returnType: CAPPluginReturnPromise),
71
71
  CAPPluginMethod(name: "getOrientation", returnType: CAPPluginReturnPromise),
72
- CAPPluginMethod(name: "getSafeAreaInsets", returnType: CAPPluginReturnPromise)
72
+ CAPPluginMethod(name: "getSafeAreaInsets", returnType: CAPPluginReturnPromise),
73
+ // Exposure control methods
74
+ CAPPluginMethod(name: "getExposureModes", returnType: CAPPluginReturnPromise),
75
+ CAPPluginMethod(name: "getExposureMode", returnType: CAPPluginReturnPromise),
76
+ CAPPluginMethod(name: "setExposureMode", returnType: CAPPluginReturnPromise),
77
+ CAPPluginMethod(name: "getExposureCompensationRange", returnType: CAPPluginReturnPromise),
78
+ CAPPluginMethod(name: "getExposureCompensation", returnType: CAPPluginReturnPromise),
79
+ CAPPluginMethod(name: "setExposureCompensation", returnType: CAPPluginReturnPromise)
73
80
 
74
81
  ]
75
82
  // Camera state tracking
@@ -101,8 +108,6 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin, CLLocationManagerDelega
101
108
 
102
109
  // MARK: - Helper Methods for Aspect Ratio
103
110
 
104
-
105
-
106
111
  /// Parses aspect ratio string and returns the appropriate ratio for the current orientation
107
112
  private func parseAspectRatio(_ ratio: String, isPortrait: Bool) -> CGFloat {
108
113
  let parts = ratio.split(separator: ":").compactMap { Double($0) }
@@ -305,10 +310,6 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin, CLLocationManagerDelega
305
310
  }
306
311
 
307
312
  self.aspectRatio = newAspectRatio
308
-
309
- // Propagate to camera controller so capture output and preview align
310
- self.cameraController.updateAspectRatio(newAspectRatio)
311
-
312
313
  DispatchQueue.main.async {
313
314
  call.resolve(self.rawSetAspectRatio())
314
315
  }
@@ -590,8 +591,6 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin, CLLocationManagerDelega
590
591
  return
591
592
  }
592
593
 
593
-
594
-
595
594
  AVCaptureDevice.requestAccess(for: .video, completionHandler: { (granted: Bool) in
596
595
 
597
596
  guard granted else {
@@ -1804,6 +1803,113 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin, CLLocationManagerDelega
1804
1803
  }
1805
1804
  }
1806
1805
 
1806
+ // MARK: - Exposure Bridge
1807
+
1808
+ @objc func getExposureModes(_ call: CAPPluginCall) {
1809
+ guard isInitialized else {
1810
+ call.reject("Camera not initialized")
1811
+ return
1812
+ }
1813
+ do {
1814
+ let modes = try self.cameraController.getExposureModes()
1815
+ call.resolve(["modes": modes])
1816
+ } catch {
1817
+ call.reject("Failed to get exposure modes: \(error.localizedDescription)")
1818
+ }
1819
+ }
1820
+
1821
+ @objc func getExposureMode(_ call: CAPPluginCall) {
1822
+ guard isInitialized else {
1823
+ call.reject("Camera not initialized")
1824
+ return
1825
+ }
1826
+ do {
1827
+ let mode = try self.cameraController.getExposureMode()
1828
+ call.resolve(["mode": mode])
1829
+ } catch {
1830
+ call.reject("Failed to get exposure mode: \(error.localizedDescription)")
1831
+ }
1832
+ }
1833
+
1834
+ @objc func setExposureMode(_ call: CAPPluginCall) {
1835
+ guard isInitialized else {
1836
+ call.reject("Camera not initialized")
1837
+ return
1838
+ }
1839
+ guard let mode = call.getString("mode") else {
1840
+ call.reject("mode parameter is required")
1841
+ return
1842
+ }
1843
+ // Validate against allowed exposure modes before delegating
1844
+ let normalized = mode.uppercased()
1845
+ let allowedModes: Set<String> = ["AUTO", "LOCK", "CONTINUOUS", "CUSTOM"]
1846
+ guard allowedModes.contains(normalized) else {
1847
+ let allowedList = Array(allowedModes).sorted().joined(separator: ", ")
1848
+ call.reject("Invalid exposure mode: \(mode). Allowed values: \(allowedList)")
1849
+ return
1850
+ }
1851
+ do {
1852
+ try self.cameraController.setExposureMode(mode: normalized)
1853
+ call.resolve()
1854
+ } catch {
1855
+ call.reject("Failed to set exposure mode: \(error.localizedDescription)")
1856
+ }
1857
+ }
1858
+
1859
+ @objc func getExposureCompensationRange(_ call: CAPPluginCall) {
1860
+ guard isInitialized else {
1861
+ call.reject("Camera not initialized")
1862
+ return
1863
+ }
1864
+ do {
1865
+ let range = try self.cameraController.getExposureCompensationRange()
1866
+ call.resolve(["min": range.min, "max": range.max, "step": range.step])
1867
+ } catch {
1868
+ call.reject("Failed to get exposure compensation range: \(error.localizedDescription)")
1869
+ }
1870
+ }
1871
+
1872
+ @objc func getExposureCompensation(_ call: CAPPluginCall) {
1873
+ guard isInitialized else {
1874
+ call.reject("Camera not initialized")
1875
+ return
1876
+ }
1877
+ do {
1878
+ let value = try self.cameraController.getExposureCompensation()
1879
+ call.resolve(["value": value])
1880
+ } catch {
1881
+ call.reject("Failed to get exposure compensation: \(error.localizedDescription)")
1882
+ }
1883
+ }
1884
+
1885
+ @objc func setExposureCompensation(_ call: CAPPluginCall) {
1886
+ guard isInitialized else {
1887
+ call.reject("Camera not initialized")
1888
+ return
1889
+ }
1890
+ guard var value = call.getFloat("value") else {
1891
+ call.reject("value parameter is required")
1892
+ return
1893
+ }
1894
+ do {
1895
+ // Snap to valid range and step
1896
+ var range = try self.cameraController.getExposureCompensationRange()
1897
+ if range.step <= 0 { range.step = 0.1 }
1898
+ let lo = min(range.min, range.max)
1899
+ let hi = max(range.min, range.max)
1900
+ // Clamp to [lo, hi]
1901
+ value = max(lo, min(hi, value))
1902
+ // Snap to nearest step
1903
+ let steps = round((value - lo) / range.step)
1904
+ let snapped = lo + steps * range.step
1905
+
1906
+ try self.cameraController.setExposureCompensation(snapped)
1907
+ call.resolve()
1908
+ } catch {
1909
+ call.reject("Failed to set exposure compensation: \(error.localizedDescription)")
1910
+ }
1911
+ }
1912
+
1807
1913
  @objc private func handleOrientationChange() {
1808
1914
  DispatchQueue.main.async {
1809
1915
  let result = self.rawSetAspectRatio()
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@capgo/camera-preview",
3
- "version": "7.6.1-alpha.3",
3
+ "version": "7.7.0",
4
4
  "description": "Camera preview",
5
5
  "license": "MIT",
6
6
  "repository": {