@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.
- package/README.md +146 -3
- package/android/src/main/java/com/ahm/capacitor/camera/preview/CameraPreview.java +97 -0
- package/android/src/main/java/com/ahm/capacitor/camera/preview/CameraXView.java +126 -0
- package/dist/docs.json +133 -2
- package/dist/esm/definitions.d.ts +48 -1
- package/dist/esm/definitions.js.map +1 -1
- package/dist/esm/web.d.ts +21 -1
- package/dist/esm/web.js +19 -0
- package/dist/esm/web.js.map +1 -1
- package/dist/plugin.cjs.js +19 -0
- package/dist/plugin.cjs.js.map +1 -1
- package/dist/plugin.js +19 -0
- package/dist/plugin.js.map +1 -1
- package/ios/Sources/CapgoCameraPreviewPlugin/CameraController.swift +218 -123
- package/ios/Sources/CapgoCameraPreviewPlugin/Plugin.swift +115 -9
- package/package.json +1 -1
|
@@ -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
|
-
|
|
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)
|
|
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
|
-
//
|
|
827
|
-
|
|
828
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
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
|
-
|
|
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()
|