@capgo/camera-preview 8.8.1 → 8.10.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.
@@ -623,7 +623,10 @@ extension CameraController {
623
623
  self.photoOutput?.isHighResolutionCaptureEnabled = true
624
624
 
625
625
  // Create video output
626
- self.fileVideoOutput = AVCaptureMovieFileOutput()
626
+ let movieFileOutput = AVCaptureMovieFileOutput()
627
+ // MP4 does not support movie fragments; the default 10s interval drops the final partial segment on stop.
628
+ movieFileOutput.movieFragmentInterval = .invalid
629
+ self.fileVideoOutput = movieFileOutput
627
630
 
628
631
  // Create data output for preview
629
632
  self.dataOutput = AVCaptureVideoDataOutput()
@@ -1004,6 +1007,10 @@ extension CameraController {
1004
1007
  finalDevice.exposurePointOfInterest = CGPoint(x: 0.5, y: 0.5)
1005
1008
  }
1006
1009
  }
1010
+ // Default white balance to continuous auto (prevents a warm/yellow cast)
1011
+ if finalDevice.isWhiteBalanceModeSupported(.continuousAutoWhiteBalance) {
1012
+ finalDevice.whiteBalanceMode = .continuousAutoWhiteBalance
1013
+ }
1007
1014
  // Reset exposure compensation so sessions start neutral
1008
1015
  let minBias = finalDevice.minExposureTargetBias
1009
1016
  let maxBias = finalDevice.maxExposureTargetBias
@@ -1441,6 +1448,19 @@ extension CameraController {
1441
1448
  photoOutput.capturePhoto(with: settings, delegate: self)
1442
1449
  }
1443
1450
 
1451
+ /// Horizontally mirrors image pixels (selfie-style). Prefer this over orientation-only flips.
1452
+ func mirrorImageHorizontally(_ image: UIImage) -> UIImage {
1453
+ let base = image.fixedOrientation() ?? image
1454
+ let format = UIGraphicsImageRendererFormat.default()
1455
+ format.scale = base.scale
1456
+ format.opaque = false
1457
+ return UIGraphicsImageRenderer(size: base.size, format: format).image { ctx in
1458
+ ctx.cgContext.translateBy(x: base.size.width, y: 0)
1459
+ ctx.cgContext.scaleBy(x: -1, y: 1)
1460
+ base.draw(in: CGRect(origin: .zero, size: base.size))
1461
+ }
1462
+ }
1463
+
1444
1464
  /// Draws timestamp and/or location pills at the top-right. Pass nil to skip either line.
1445
1465
  func drawTimestampAndLocation(on image: UIImage, when: String?, where whereStr: String?) -> UIImage {
1446
1466
  let base = image.fixedOrientation() ?? image
@@ -2184,7 +2204,6 @@ extension CameraController {
2184
2204
  }
2185
2205
  }
2186
2206
 
2187
-
2188
2207
  private func cancelPendingFocusExposureRestore() {
2189
2208
  self.focusExposureRestoreGeneration &+= 1
2190
2209
  self.focusExposureRestoreTimeoutWorkItem?.cancel()
@@ -2721,6 +2740,100 @@ extension CameraController {
2721
2740
  }
2722
2741
  }
2723
2742
 
2743
+ // MARK: - White Balance Controls
2744
+
2745
+ func getWhiteBalanceModes() throws -> [String] {
2746
+ var currentCamera: AVCaptureDevice?
2747
+ switch currentCameraPosition {
2748
+ case .front:
2749
+ currentCamera = self.frontCamera
2750
+ case .rear:
2751
+ currentCamera = self.rearCamera
2752
+ default:
2753
+ break
2754
+ }
2755
+
2756
+ guard let device = currentCamera else {
2757
+ throw CameraControllerError.noCamerasAvailable
2758
+ }
2759
+
2760
+ var modes: [String] = []
2761
+ if device.isWhiteBalanceModeSupported(.locked) { modes.append("LOCK") }
2762
+ if device.isWhiteBalanceModeSupported(.autoWhiteBalance) { modes.append("AUTO") }
2763
+ if device.isWhiteBalanceModeSupported(.continuousAutoWhiteBalance) { modes.append("CONTINUOUS") }
2764
+ return modes
2765
+ }
2766
+
2767
+ func getWhiteBalanceMode() throws -> String {
2768
+ var currentCamera: AVCaptureDevice?
2769
+ switch currentCameraPosition {
2770
+ case .front:
2771
+ currentCamera = self.frontCamera
2772
+ case .rear:
2773
+ currentCamera = self.rearCamera
2774
+ default:
2775
+ break
2776
+ }
2777
+
2778
+ guard let device = currentCamera else {
2779
+ throw CameraControllerError.noCamerasAvailable
2780
+ }
2781
+
2782
+ switch device.whiteBalanceMode {
2783
+ case .locked:
2784
+ return "LOCK"
2785
+ case .autoWhiteBalance:
2786
+ return "AUTO"
2787
+ case .continuousAutoWhiteBalance:
2788
+ return "CONTINUOUS"
2789
+ @unknown default:
2790
+ return "CONTINUOUS"
2791
+ }
2792
+ }
2793
+
2794
+ func setWhiteBalanceMode(mode: String) throws {
2795
+ var currentCamera: AVCaptureDevice?
2796
+ switch currentCameraPosition {
2797
+ case .front:
2798
+ currentCamera = self.frontCamera
2799
+ case .rear:
2800
+ currentCamera = self.rearCamera
2801
+ default:
2802
+ break
2803
+ }
2804
+
2805
+ guard let device = currentCamera else {
2806
+ throw CameraControllerError.noCamerasAvailable
2807
+ }
2808
+
2809
+ let normalized = mode.uppercased()
2810
+ let desiredMode: AVCaptureDevice.WhiteBalanceMode?
2811
+ switch normalized {
2812
+ case "LOCK":
2813
+ desiredMode = .locked
2814
+ case "AUTO":
2815
+ desiredMode = .autoWhiteBalance
2816
+ case "CONTINUOUS":
2817
+ desiredMode = .continuousAutoWhiteBalance
2818
+ case "CUSTOM":
2819
+ throw CameraControllerError.invalidOperation
2820
+ default:
2821
+ throw CameraControllerError.invalidOperation
2822
+ }
2823
+
2824
+ guard let finalMode = desiredMode, device.isWhiteBalanceModeSupported(finalMode) else {
2825
+ throw CameraControllerError.invalidOperation
2826
+ }
2827
+
2828
+ do {
2829
+ try device.lockForConfiguration()
2830
+ device.whiteBalanceMode = finalMode
2831
+ device.unlockForConfiguration()
2832
+ } catch {
2833
+ throw CameraControllerError.invalidOperation
2834
+ }
2835
+ }
2836
+
2724
2837
  private func activeVideoDevice() -> AVCaptureDevice? {
2725
2838
  switch currentCameraPosition {
2726
2839
  case .front:
@@ -35,7 +35,7 @@ extension UIWindow {
35
35
  */
36
36
  @objc(CameraPreview)
37
37
  public class CameraPreview: CAPPlugin, CAPBridgedPlugin, CLLocationManagerDelegate {
38
- private let pluginVersion: String = "8.8.1"
38
+ private let pluginVersion: String = "8.10.0"
39
39
  public let identifier = "CameraPreviewPlugin"
40
40
  public let jsName = "CameraPreview"
41
41
  public let pluginMethods: [CAPPluginMethod] = [
@@ -90,6 +90,9 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin, CLLocationManagerDelega
90
90
  CAPPluginMethod(name: "getExposureCompensationRange", returnType: CAPPluginReturnPromise),
91
91
  CAPPluginMethod(name: "getExposureCompensation", returnType: CAPPluginReturnPromise),
92
92
  CAPPluginMethod(name: "setExposureCompensation", returnType: CAPPluginReturnPromise),
93
+ CAPPluginMethod(name: "getWhiteBalanceModes", returnType: CAPPluginReturnPromise),
94
+ CAPPluginMethod(name: "getWhiteBalanceMode", returnType: CAPPluginReturnPromise),
95
+ CAPPluginMethod(name: "setWhiteBalanceMode", returnType: CAPPluginReturnPromise),
93
96
  CAPPluginMethod(name: "getSupportedVideoFrameRates", returnType: CAPPluginReturnPromise),
94
97
  CAPPluginMethod(name: "getVideoFrameRate", returnType: CAPPluginReturnPromise),
95
98
  CAPPluginMethod(name: "setVideoFrameRate", returnType: CAPPluginReturnPromise),
@@ -1262,10 +1265,11 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin, CLLocationManagerDelega
1262
1265
  let width = call.getInt("width")
1263
1266
  let height = call.getInt("height")
1264
1267
  let photoQualityPrioritization = call.getString("photoQualityPrioritization", "speed")
1268
+ let mirrorFrontCamera = call.getBool("mirrorFrontCamera", false) ?? false
1265
1269
 
1266
1270
  print("[CameraPreview] Raw parameter values - width: \(String(describing: width)), height: \(String(describing: height))")
1267
1271
 
1268
- print("[CameraPreview] Capture params - quality: \(quality), saveToGallery: \(saveToGallery), withExifLocation: \(withExifLocation ?? false), embedTimestamp: \(embedTimestamp), embedLocation: \(effectiveEmbedLocation) (requested=\(embedLocationRequested)), width: \(width ?? -1), height: \(height ?? -1)")
1272
+ print("[CameraPreview] Capture params - quality: \(quality), saveToGallery: \(saveToGallery), withExifLocation: \(withExifLocation ?? false), embedTimestamp: \(embedTimestamp), embedLocation: \(effectiveEmbedLocation) (requested=\(embedLocationRequested)), width: \(width ?? -1), height: \(height ?? -1), mirrorFrontCamera: \(mirrorFrontCamera)")
1269
1273
  print("[CameraPreview] Current location: \(self.currentLocation?.description ?? "nil")")
1270
1274
  // Safely read frame from main thread for logging
1271
1275
  let (previewWidth, previewHeight): (CGFloat, CGFloat) = {
@@ -1300,9 +1304,21 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin, CLLocationManagerDelega
1300
1304
  return
1301
1305
  }
1302
1306
 
1303
- guard let image = image,
1304
- let imageDataWithExif = self.createImageDataWithExif(
1305
- from: image,
1307
+ guard let capturedImage = image else {
1308
+ print("[CameraPreview] Failed to create image data with EXIF")
1309
+ call.reject("Failed to create image data with EXIF")
1310
+ return
1311
+ }
1312
+
1313
+ let imageToEncode: UIImage
1314
+ if mirrorFrontCamera, self.cameraPosition == "front" {
1315
+ imageToEncode = self.cameraController.mirrorImageHorizontally(capturedImage)
1316
+ } else {
1317
+ imageToEncode = capturedImage
1318
+ }
1319
+
1320
+ guard let imageDataWithExif = self.createImageDataWithExif(
1321
+ from: imageToEncode,
1306
1322
  quality: Int(quality),
1307
1323
  location: withExifLocation ? self.currentLocation : nil,
1308
1324
  heading: withExifLocation ? self.currentHeading : nil,
@@ -1517,6 +1533,7 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin, CLLocationManagerDelega
1517
1533
 
1518
1534
  @objc func captureSample(_ call: CAPPluginCall) {
1519
1535
  let quality: Int = call.getInt("quality") ?? 85
1536
+ let mirrorFrontCamera = call.getBool("mirrorFrontCamera", false) ?? false
1520
1537
 
1521
1538
  self.cameraController.captureSample { image, error in
1522
1539
  guard let image = image else {
@@ -1525,13 +1542,11 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin, CLLocationManagerDelega
1525
1542
  return
1526
1543
  }
1527
1544
 
1528
- let imageData: Data?
1529
- if self.cameraPosition == "front" {
1530
- let flippedImage = image.withHorizontallyFlippedOrientation()
1531
- imageData = flippedImage.jpegData(compressionQuality: CGFloat(quality)/100)
1532
- } else {
1533
- imageData = image.jpegData(compressionQuality: CGFloat(quality)/100)
1545
+ var outputImage = image
1546
+ if mirrorFrontCamera, self.cameraPosition == "front" {
1547
+ outputImage = self.cameraController.mirrorImageHorizontally(image)
1534
1548
  }
1549
+ let imageData = outputImage.jpegData(compressionQuality: CGFloat(quality)/100)
1535
1550
 
1536
1551
  if self.storeToFile == false {
1537
1552
  guard let imageBase64 = imageData?.base64EncodedString() else {
@@ -2392,6 +2407,58 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin, CLLocationManagerDelega
2392
2407
  }
2393
2408
  }
2394
2409
 
2410
+ // MARK: - White Balance Bridge
2411
+
2412
+ @objc func getWhiteBalanceModes(_ call: CAPPluginCall) {
2413
+ guard isInitialized else {
2414
+ call.reject("Camera not initialized")
2415
+ return
2416
+ }
2417
+ do {
2418
+ let modes = try self.cameraController.getWhiteBalanceModes()
2419
+ call.resolve(["modes": modes])
2420
+ } catch {
2421
+ call.reject("Failed to get white balance modes: \(error.localizedDescription)")
2422
+ }
2423
+ }
2424
+
2425
+ @objc func getWhiteBalanceMode(_ call: CAPPluginCall) {
2426
+ guard isInitialized else {
2427
+ call.reject("Camera not initialized")
2428
+ return
2429
+ }
2430
+ do {
2431
+ let mode = try self.cameraController.getWhiteBalanceMode()
2432
+ call.resolve(["mode": mode])
2433
+ } catch {
2434
+ call.reject("Failed to get white balance mode: \(error.localizedDescription)")
2435
+ }
2436
+ }
2437
+
2438
+ @objc func setWhiteBalanceMode(_ call: CAPPluginCall) {
2439
+ guard isInitialized else {
2440
+ call.reject("Camera not initialized")
2441
+ return
2442
+ }
2443
+ guard let mode = call.getString("mode") else {
2444
+ call.reject("mode parameter is required")
2445
+ return
2446
+ }
2447
+ let normalized = mode.uppercased()
2448
+ let allowedModes: Set<String> = ["AUTO", "LOCK", "CONTINUOUS"]
2449
+ guard allowedModes.contains(normalized) else {
2450
+ let allowedList = Array(allowedModes).sorted().joined(separator: ", ")
2451
+ call.reject("Invalid white balance mode: \(mode). Allowed values: \(allowedList)")
2452
+ return
2453
+ }
2454
+ do {
2455
+ try self.cameraController.setWhiteBalanceMode(mode: normalized)
2456
+ call.resolve()
2457
+ } catch {
2458
+ call.reject("Failed to set white balance mode: \(error.localizedDescription)")
2459
+ }
2460
+ }
2461
+
2395
2462
  // MARK: - Exposure Bridge
2396
2463
 
2397
2464
  @objc func getExposureModes(_ call: CAPPluginCall) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@capgo/camera-preview",
3
- "version": "8.8.1",
3
+ "version": "8.10.0",
4
4
  "description": "Camera preview",
5
5
  "license": "MPL-2.0",
6
6
  "repository": {