@capgo/camera-preview 8.6.0 → 8.7.1

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,4 +1,4 @@
1
- // swiftlint:disable file_length cyclomatic_complexity identifier_name
1
+ // swiftlint:disable file_length type_body_length cyclomatic_complexity identifier_name function_body_length
2
2
  import AVFoundation
3
3
  import UIKit
4
4
  import CoreLocation
@@ -150,9 +150,11 @@ class CameraController: NSObject {
150
150
 
151
151
  var videoFileURL: URL?
152
152
  var recordingFinishedCallback: ((URL, String) -> Void)?
153
+ private var stopRecordingCompletion: ((URL?, String?, Error?) -> Void)?
153
154
  private let saneMaxZoomFactor: CGFloat = 25.5
154
155
 
155
156
  var videoQuality: String = "high"
157
+ private var configuredVideoFrameRate: Int?
156
158
  var videoCodec: String = "avc1"
157
159
  var videoStabilizationMode: String = "off"
158
160
 
@@ -269,7 +271,7 @@ class CameraController: NSObject {
269
271
  ("standard", .standard),
270
272
  ("cinematic", .cinematic),
271
273
  ("cinematicExtended", .cinematicExtended),
272
- ("auto", .auto),
274
+ ("auto", .auto)
273
275
  ]
274
276
  if #available(iOS 17.0, *) {
275
277
  candidates.append(("previewOptimized", .previewOptimized))
@@ -1120,6 +1122,7 @@ extension CameraController {
1120
1122
  }
1121
1123
 
1122
1124
  func switchCameras() throws {
1125
+ configuredVideoFrameRate = nil
1123
1126
  guard let currentCameraPosition = currentCameraPosition,
1124
1127
  let captureSession = self.captureSession else {
1125
1128
  throw CameraControllerError.captureSessionIsMissing
@@ -2374,6 +2377,7 @@ extension CameraController {
2374
2377
  }
2375
2378
 
2376
2379
  func cleanup() {
2380
+ configuredVideoFrameRate = nil
2377
2381
  stopBarcodeScanner()
2378
2382
  if let captureSession = self.captureSession {
2379
2383
  captureSession.stopRunning()
@@ -2581,6 +2585,129 @@ extension CameraController {
2581
2585
  }
2582
2586
  }
2583
2587
 
2588
+ private func activeVideoDevice() -> AVCaptureDevice? {
2589
+ switch currentCameraPosition {
2590
+ case .front:
2591
+ return self.frontCamera
2592
+ case .rear:
2593
+ return self.rearCamera
2594
+ default:
2595
+ return nil
2596
+ }
2597
+ }
2598
+
2599
+ private func frameRates(from format: AVCaptureDevice.Format) -> [Int] {
2600
+ var rates = Set<Int>()
2601
+ let standardRates = [24, 25, 30, 50, 60, 120]
2602
+
2603
+ for range in format.videoSupportedFrameRateRanges {
2604
+ let minFps = Int(ceil(range.minFrameRate))
2605
+ let maxFps = Int(floor(range.maxFrameRate))
2606
+
2607
+ if minFps == maxFps {
2608
+ rates.insert(minFps)
2609
+ continue
2610
+ }
2611
+
2612
+ rates.insert(minFps)
2613
+ rates.insert(maxFps)
2614
+
2615
+ for rate in standardRates where rate >= minFps && rate <= maxFps {
2616
+ rates.insert(rate)
2617
+ }
2618
+ }
2619
+
2620
+ return rates.sorted()
2621
+ }
2622
+
2623
+ private func formatSupportsFrameRate(_ format: AVCaptureDevice.Format, frameRate: Int) -> Bool {
2624
+ let fps = Double(frameRate)
2625
+ return format.videoSupportedFrameRateRanges.contains { range in
2626
+ fps >= range.minFrameRate && fps <= range.maxFrameRate
2627
+ }
2628
+ }
2629
+
2630
+ private func frameRate(from duration: CMTime) -> Int {
2631
+ guard duration.value > 0 else { return 30 }
2632
+ return Int(round(Double(duration.timescale) / Double(duration.value)))
2633
+ }
2634
+
2635
+ private func allSupportedFrameRates(for device: AVCaptureDevice) -> [Int] {
2636
+ var rates = Set<Int>()
2637
+ for format in device.formats {
2638
+ for rate in frameRates(from: format) {
2639
+ rates.insert(rate)
2640
+ }
2641
+ }
2642
+ return rates.sorted()
2643
+ }
2644
+
2645
+ func getSupportedVideoFrameRates() throws -> [Int] {
2646
+ guard let device = activeVideoDevice() else {
2647
+ throw CameraControllerError.noCamerasAvailable
2648
+ }
2649
+
2650
+ return allSupportedFrameRates(for: device)
2651
+ }
2652
+
2653
+ func getVideoFrameRate() throws -> Int {
2654
+ if let configuredVideoFrameRate = self.configuredVideoFrameRate {
2655
+ return configuredVideoFrameRate
2656
+ }
2657
+
2658
+ guard let device = activeVideoDevice() else {
2659
+ throw CameraControllerError.noCamerasAvailable
2660
+ }
2661
+
2662
+ return frameRate(from: device.activeVideoMinFrameDuration)
2663
+ }
2664
+
2665
+ func setVideoFrameRate(_ frameRate: Int) throws {
2666
+ guard frameRate > 0 else {
2667
+ throw NSError(domain: "CameraPreview", code: 0, userInfo: [NSLocalizedDescriptionKey: "frameRate must be greater than 0"])
2668
+ }
2669
+
2670
+ if let fileVideoOutput = self.fileVideoOutput, fileVideoOutput.isRecording {
2671
+ throw NSError(domain: "CameraPreview", code: 0, userInfo: [NSLocalizedDescriptionKey: "Cannot change video frame rate while recording"])
2672
+ }
2673
+
2674
+ guard let device = activeVideoDevice() else {
2675
+ throw CameraControllerError.noCamerasAvailable
2676
+ }
2677
+
2678
+ let supportedRates = allSupportedFrameRates(for: device)
2679
+ guard supportedRates.contains(frameRate) else {
2680
+ throw NSError(
2681
+ domain: "CameraPreview",
2682
+ code: 0,
2683
+ userInfo: [NSLocalizedDescriptionKey: "Unsupported frame rate \(frameRate). Supported values: \(supportedRates.map(String.init).joined(separator: ", "))"]
2684
+ )
2685
+ }
2686
+
2687
+ guard let targetFormat = device.formats.first(where: { formatSupportsFrameRate($0, frameRate: frameRate) }) else {
2688
+ throw NSError(
2689
+ domain: "CameraPreview",
2690
+ code: 0,
2691
+ userInfo: [NSLocalizedDescriptionKey: "Unsupported frame rate \(frameRate) for the active camera"]
2692
+ )
2693
+ }
2694
+
2695
+ let frameDuration = CMTime(value: 1, timescale: CMTimeScale(frameRate))
2696
+
2697
+ do {
2698
+ try device.lockForConfiguration()
2699
+ if device.activeFormat != targetFormat {
2700
+ device.activeFormat = targetFormat
2701
+ }
2702
+ device.activeVideoMinFrameDuration = frameDuration
2703
+ device.activeVideoMaxFrameDuration = frameDuration
2704
+ device.unlockForConfiguration()
2705
+ self.configuredVideoFrameRate = frameRate
2706
+ } catch {
2707
+ throw CameraControllerError.invalidOperation
2708
+ }
2709
+ }
2710
+
2584
2711
  func captureVideo(maxDuration: Float? = nil, maxFileSize: Int? = nil) throws {
2585
2712
  guard let captureSession = self.captureSession, captureSession.isRunning else {
2586
2713
  throw CameraControllerError.captureSessionIsMissing
@@ -2664,21 +2791,26 @@ extension CameraController {
2664
2791
  self.videoFileURL = fileUrl
2665
2792
  }
2666
2793
 
2667
- func stopRecording(completion: @escaping (URL?, Error?) -> Void) {
2794
+ func stopRecording(completion: @escaping (URL?, String?, Error?) -> Void) {
2668
2795
  guard let captureSession = self.captureSession, captureSession.isRunning else {
2669
- completion(nil, CameraControllerError.captureSessionIsMissing)
2796
+ completion(nil, nil, CameraControllerError.captureSessionIsMissing)
2670
2797
  return
2671
2798
  }
2672
2799
  guard let fileVideoOutput = self.fileVideoOutput else {
2673
- completion(nil, CameraControllerError.fileVideoOutputNotFound)
2800
+ completion(nil, nil, CameraControllerError.fileVideoOutputNotFound)
2801
+ return
2802
+ }
2803
+ guard fileVideoOutput.isRecording else {
2804
+ completion(nil, nil, CameraControllerError.invalidOperation)
2805
+ return
2806
+ }
2807
+ guard self.stopRecordingCompletion == nil else {
2808
+ completion(nil, nil, CameraControllerError.invalidOperation)
2674
2809
  return
2675
2810
  }
2676
2811
 
2677
- // Stop recording video
2812
+ self.stopRecordingCompletion = completion
2678
2813
  fileVideoOutput.stopRecording()
2679
-
2680
- // Return the video file URL in the completion handler
2681
- completion(self.videoFileURL, nil)
2682
2814
  }
2683
2815
  }
2684
2816
 
@@ -3074,6 +3206,19 @@ extension UIImage {
3074
3206
  extension CameraController: AVCaptureFileOutputRecordingDelegate {
3075
3207
  func fileOutput(_ output: AVCaptureFileOutput, didFinishRecordingTo outputFileURL: URL, from connections: [AVCaptureConnection], error: Error?) {
3076
3208
  let reason = recordingFinishReason(from: error)
3209
+ let stopCompletion = self.stopRecordingCompletion
3210
+ self.stopRecordingCompletion = nil
3211
+
3212
+ if let stopCompletion = stopCompletion {
3213
+ DispatchQueue.main.async {
3214
+ if reason == "error" {
3215
+ stopCompletion(nil, nil, error ?? CameraControllerError.unknown)
3216
+ } else {
3217
+ stopCompletion(outputFileURL, reason, nil)
3218
+ }
3219
+ }
3220
+ }
3221
+
3077
3222
  if reason == "error" {
3078
3223
  print("Error recording movie: \(error?.localizedDescription ?? "unknown")")
3079
3224
  return
@@ -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.6.0"
38
+ private let pluginVersion: String = "8.7.1"
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: "getSupportedVideoFrameRates", returnType: CAPPluginReturnPromise),
94
+ CAPPluginMethod(name: "getVideoFrameRate", returnType: CAPPluginReturnPromise),
95
+ CAPPluginMethod(name: "setVideoFrameRate", returnType: CAPPluginReturnPromise),
93
96
  CAPPluginMethod(name: "getPluginVersion", returnType: CAPPluginReturnPromise)
94
97
  ]
95
98
  // Camera state tracking
@@ -1693,6 +1696,9 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin, CLLocationManagerDelega
1693
1696
  }
1694
1697
 
1695
1698
  do {
1699
+ if let frameRate = call.getInt("frameRate") {
1700
+ try self.cameraController.setVideoFrameRate(frameRate)
1701
+ }
1696
1702
  try self.cameraController.captureVideo(maxDuration: maxDuration, maxFileSize: maxFileSize)
1697
1703
  call.resolve()
1698
1704
  } catch {
@@ -1701,8 +1707,8 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin, CLLocationManagerDelega
1701
1707
  }
1702
1708
 
1703
1709
  @objc func stopRecordVideo(_ call: CAPPluginCall) {
1704
- self.cameraController.stopRecording { (fileURL, error) in
1705
- guard let fileURL = fileURL else {
1710
+ self.cameraController.stopRecording { (fileURL, reason, error) in
1711
+ guard let fileURL = fileURL, let reason = reason else {
1706
1712
  print(error ?? "Video capture error")
1707
1713
  guard let error = error else {
1708
1714
  call.reject("Video capture error")
@@ -1712,7 +1718,7 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin, CLLocationManagerDelega
1712
1718
  return
1713
1719
  }
1714
1720
 
1715
- call.resolve(["videoFilePath": fileURL.absoluteString, "reason": "manual"])
1721
+ call.resolve(["videoFilePath": fileURL.absoluteString, "reason": reason])
1716
1722
  }
1717
1723
  }
1718
1724
 
@@ -2473,6 +2479,49 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin, CLLocationManagerDelega
2473
2479
  }
2474
2480
  }
2475
2481
 
2482
+ @objc func getSupportedVideoFrameRates(_ call: CAPPluginCall) {
2483
+ guard isInitialized else {
2484
+ call.reject("Camera not initialized")
2485
+ return
2486
+ }
2487
+ do {
2488
+ let frameRates = try self.cameraController.getSupportedVideoFrameRates()
2489
+ call.resolve(["frameRates": frameRates])
2490
+ } catch {
2491
+ call.reject("Failed to get supported video frame rates: \(error.localizedDescription)")
2492
+ }
2493
+ }
2494
+
2495
+ @objc func getVideoFrameRate(_ call: CAPPluginCall) {
2496
+ guard isInitialized else {
2497
+ call.reject("Camera not initialized")
2498
+ return
2499
+ }
2500
+ do {
2501
+ let frameRate = try self.cameraController.getVideoFrameRate()
2502
+ call.resolve(["frameRate": frameRate])
2503
+ } catch {
2504
+ call.reject("Failed to get video frame rate: \(error.localizedDescription)")
2505
+ }
2506
+ }
2507
+
2508
+ @objc func setVideoFrameRate(_ call: CAPPluginCall) {
2509
+ guard isInitialized else {
2510
+ call.reject("Camera not initialized")
2511
+ return
2512
+ }
2513
+ guard let frameRate = call.getInt("frameRate") else {
2514
+ call.reject("frameRate parameter is required")
2515
+ return
2516
+ }
2517
+ do {
2518
+ try self.cameraController.setVideoFrameRate(frameRate)
2519
+ call.resolve()
2520
+ } catch {
2521
+ call.reject(error.localizedDescription)
2522
+ }
2523
+ }
2524
+
2476
2525
  private var lastOrientation: String?
2477
2526
 
2478
2527
  @objc private func handleOrientationChange() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@capgo/camera-preview",
3
- "version": "8.6.0",
3
+ "version": "8.7.1",
4
4
  "description": "Camera preview",
5
5
  "license": "MPL-2.0",
6
6
  "repository": {