@capgo/camera-preview 8.6.0 → 8.8.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.
@@ -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,11 +150,15 @@ 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"
160
+ private var cachedSupportedVideoStabilizationModes: [String] = []
161
+ private var isSupportedVideoStabilizationModesCacheValid = false
158
162
 
159
163
  private static let allVideoQualities = ["low", "medium", "high", "2160p", "1080p", "720p", "480p", "4:3"]
160
164
 
@@ -221,23 +225,10 @@ class CameraController: NSObject {
221
225
  }
222
226
 
223
227
  func getSupportedVideoStabilizationModes() -> [String] {
224
- guard let connection = self.fileVideoOutput?.connection(with: .video),
225
- connection.isVideoStabilizationSupported else {
226
- return ["off"]
227
- }
228
-
229
- let savedPreferred = connection.preferredVideoStabilizationMode
230
- var supported: [String] = []
231
-
232
- for candidate in Self.allVideoStabilizationModeCandidates() {
233
- connection.preferredVideoStabilizationMode = candidate.mode
234
- if connection.preferredVideoStabilizationMode == candidate.mode {
235
- supported.append(candidate.name)
236
- }
228
+ if self.isSupportedVideoStabilizationModesCacheValid {
229
+ return self.cachedSupportedVideoStabilizationModes
237
230
  }
238
-
239
- connection.preferredVideoStabilizationMode = savedPreferred
240
- return supported.isEmpty ? ["off"] : supported
231
+ return self.refreshSupportedVideoStabilizationModesCache()
241
232
  }
242
233
 
243
234
  func getVideoStabilizationMode() -> String {
@@ -255,12 +246,96 @@ class CameraController: NSObject {
255
246
  guard let avMode = self.avVideoStabilizationMode(from: mode) else {
256
247
  throw CameraControllerError.invalidOperation
257
248
  }
249
+
250
+ if self.isVideoStabilizationOutputAttached() {
251
+ try self.validateVideoStabilizationMode(mode)
252
+ }
253
+
254
+ self.videoStabilizationMode = mode
255
+
256
+ guard self.isVideoStabilizationOutputAttached() else {
257
+ return
258
+ }
259
+
260
+ self.configureCaptureSession {
261
+ self.applyVideoStabilizationMode(avMode)
262
+ }
263
+ }
264
+
265
+ @discardableResult
266
+ private func refreshSupportedVideoStabilizationModesCache() -> [String] {
267
+ let modes = self.probeSupportedVideoStabilizationModes()
268
+ self.cachedSupportedVideoStabilizationModes = modes
269
+ self.isSupportedVideoStabilizationModesCacheValid = true
270
+ return modes
271
+ }
272
+
273
+ private func invalidateSupportedVideoStabilizationModesCache() {
274
+ self.isSupportedVideoStabilizationModesCacheValid = false
275
+ self.cachedSupportedVideoStabilizationModes = []
276
+ }
277
+
278
+ private func validateVideoStabilizationMode(_ mode: String) throws {
258
279
  let supportedModes = self.getSupportedVideoStabilizationModes()
259
280
  guard supportedModes.contains(mode) else {
260
281
  throw CameraControllerError.invalidOperation
261
282
  }
262
- self.videoStabilizationMode = mode
263
- self.applyVideoStabilizationMode(avMode)
283
+ }
284
+
285
+ private func isVideoStabilizationOutputAttached() -> Bool {
286
+ guard let captureSession = self.captureSession,
287
+ let fileVideoOutput = self.fileVideoOutput else {
288
+ return false
289
+ }
290
+ return captureSession.outputs.contains(where: { $0 === fileVideoOutput })
291
+ }
292
+
293
+ private func configureCaptureSession(_ block: () -> Void) {
294
+ guard let captureSession = self.captureSession else {
295
+ block()
296
+ return
297
+ }
298
+
299
+ captureSession.beginConfiguration()
300
+ defer { captureSession.commitConfiguration() }
301
+ block()
302
+ }
303
+
304
+ private func activeVideoCaptureDevice() -> AVCaptureDevice? {
305
+ switch self.currentCameraPosition {
306
+ case .front:
307
+ return self.frontCamera
308
+ case .rear:
309
+ return self.rearCamera
310
+ default:
311
+ return nil
312
+ }
313
+ }
314
+
315
+ private func probeSupportedVideoStabilizationModes() -> [String] {
316
+ guard let connection = self.fileVideoOutput?.connection(with: .video),
317
+ connection.isVideoStabilizationSupported,
318
+ let device = self.activeVideoCaptureDevice() else {
319
+ return ["off"]
320
+ }
321
+
322
+ let format = device.activeFormat
323
+ var supported: [String] = ["off"]
324
+
325
+ for candidate in Self.allVideoStabilizationModeCandidates() where candidate.mode != .off {
326
+ if format.isVideoStabilizationModeSupported(candidate.mode) {
327
+ supported.append(candidate.name)
328
+ }
329
+ }
330
+
331
+ return supported
332
+ }
333
+
334
+ private func refreshVideoStabilizationAfterMovieOutputAttached() throws {
335
+ self.invalidateSupportedVideoStabilizationModesCache()
336
+ _ = self.refreshSupportedVideoStabilizationModesCache()
337
+ try self.validateVideoStabilizationMode(self.videoStabilizationMode)
338
+ self.applyVideoStabilizationMode()
264
339
  }
265
340
 
266
341
  private static func allVideoStabilizationModeCandidates() -> [(name: String, mode: AVCaptureVideoStabilizationMode)] {
@@ -269,7 +344,7 @@ class CameraController: NSObject {
269
344
  ("standard", .standard),
270
345
  ("cinematic", .cinematic),
271
346
  ("cinematicExtended", .cinematicExtended),
272
- ("auto", .auto),
347
+ ("auto", .auto)
273
348
  ]
274
349
  if #available(iOS 17.0, *) {
275
350
  candidates.append(("previewOptimized", .previewOptimized))
@@ -647,7 +722,7 @@ extension CameraController {
647
722
  captureSession.addOutput(fileVideoOutput)
648
723
  // Set orientation immediately
649
724
  self.setVideoOrientation(videoOrientation, on: fileVideoOutput.connections)
650
- self.applyVideoStabilizationMode()
725
+ try self.refreshVideoStabilizationAfterMovieOutputAttached()
651
726
  }
652
727
 
653
728
  // Set up preview layer session in the same configuration block
@@ -1120,6 +1195,7 @@ extension CameraController {
1120
1195
  }
1121
1196
 
1122
1197
  func switchCameras() throws {
1198
+ configuredVideoFrameRate = nil
1123
1199
  guard let currentCameraPosition = currentCameraPosition,
1124
1200
  let captureSession = self.captureSession else {
1125
1201
  throw CameraControllerError.captureSessionIsMissing
@@ -1227,7 +1303,7 @@ extension CameraController {
1227
1303
  captureSession.removeOutput(fileVideoOutput)
1228
1304
  if captureSession.canAddOutput(fileVideoOutput) {
1229
1305
  captureSession.addOutput(fileVideoOutput)
1230
- self.applyVideoStabilizationMode()
1306
+ try self.refreshVideoStabilizationAfterMovieOutputAttached()
1231
1307
  }
1232
1308
  }
1233
1309
 
@@ -2369,11 +2445,22 @@ extension CameraController {
2369
2445
  captureSession.addInput(audioInput)
2370
2446
  }
2371
2447
 
2448
+ // Re-attach movie file output so its connection is bound to the new input.
2449
+ if let fileVideoOutput = self.fileVideoOutput,
2450
+ captureSession.outputs.contains(where: { $0 === fileVideoOutput }) {
2451
+ captureSession.removeOutput(fileVideoOutput)
2452
+ if captureSession.canAddOutput(fileVideoOutput) {
2453
+ captureSession.addOutput(fileVideoOutput)
2454
+ try self.refreshVideoStabilizationAfterMovieOutputAttached()
2455
+ }
2456
+ }
2457
+
2372
2458
  // Update video orientation
2373
2459
  self.updateVideoOrientation()
2374
2460
  }
2375
2461
 
2376
2462
  func cleanup() {
2463
+ configuredVideoFrameRate = nil
2377
2464
  stopBarcodeScanner()
2378
2465
  if let captureSession = self.captureSession {
2379
2466
  captureSession.stopRunning()
@@ -2409,6 +2496,7 @@ extension CameraController {
2409
2496
 
2410
2497
  // Reset output preparation status
2411
2498
  self.outputsPrepared = false
2499
+ self.invalidateSupportedVideoStabilizationModesCache()
2412
2500
 
2413
2501
  // Reset first frame detection
2414
2502
  self.hasReceivedFirstFrame = false
@@ -2581,7 +2669,130 @@ extension CameraController {
2581
2669
  }
2582
2670
  }
2583
2671
 
2584
- func captureVideo(maxDuration: Float? = nil, maxFileSize: Int? = nil) throws {
2672
+ private func activeVideoDevice() -> AVCaptureDevice? {
2673
+ switch currentCameraPosition {
2674
+ case .front:
2675
+ return self.frontCamera
2676
+ case .rear:
2677
+ return self.rearCamera
2678
+ default:
2679
+ return nil
2680
+ }
2681
+ }
2682
+
2683
+ private func frameRates(from format: AVCaptureDevice.Format) -> [Int] {
2684
+ var rates = Set<Int>()
2685
+ let standardRates = [24, 25, 30, 50, 60, 120]
2686
+
2687
+ for range in format.videoSupportedFrameRateRanges {
2688
+ let minFps = Int(ceil(range.minFrameRate))
2689
+ let maxFps = Int(floor(range.maxFrameRate))
2690
+
2691
+ if minFps == maxFps {
2692
+ rates.insert(minFps)
2693
+ continue
2694
+ }
2695
+
2696
+ rates.insert(minFps)
2697
+ rates.insert(maxFps)
2698
+
2699
+ for rate in standardRates where rate >= minFps && rate <= maxFps {
2700
+ rates.insert(rate)
2701
+ }
2702
+ }
2703
+
2704
+ return rates.sorted()
2705
+ }
2706
+
2707
+ private func formatSupportsFrameRate(_ format: AVCaptureDevice.Format, frameRate: Int) -> Bool {
2708
+ let fps = Double(frameRate)
2709
+ return format.videoSupportedFrameRateRanges.contains { range in
2710
+ fps >= range.minFrameRate && fps <= range.maxFrameRate
2711
+ }
2712
+ }
2713
+
2714
+ private func frameRate(from duration: CMTime) -> Int {
2715
+ guard duration.value > 0 else { return 30 }
2716
+ return Int(round(Double(duration.timescale) / Double(duration.value)))
2717
+ }
2718
+
2719
+ private func allSupportedFrameRates(for device: AVCaptureDevice) -> [Int] {
2720
+ var rates = Set<Int>()
2721
+ for format in device.formats {
2722
+ for rate in frameRates(from: format) {
2723
+ rates.insert(rate)
2724
+ }
2725
+ }
2726
+ return rates.sorted()
2727
+ }
2728
+
2729
+ func getSupportedVideoFrameRates() throws -> [Int] {
2730
+ guard let device = activeVideoDevice() else {
2731
+ throw CameraControllerError.noCamerasAvailable
2732
+ }
2733
+
2734
+ return allSupportedFrameRates(for: device)
2735
+ }
2736
+
2737
+ func getVideoFrameRate() throws -> Int {
2738
+ if let configuredVideoFrameRate = self.configuredVideoFrameRate {
2739
+ return configuredVideoFrameRate
2740
+ }
2741
+
2742
+ guard let device = activeVideoDevice() else {
2743
+ throw CameraControllerError.noCamerasAvailable
2744
+ }
2745
+
2746
+ return frameRate(from: device.activeVideoMinFrameDuration)
2747
+ }
2748
+
2749
+ func setVideoFrameRate(_ frameRate: Int) throws {
2750
+ guard frameRate > 0 else {
2751
+ throw NSError(domain: "CameraPreview", code: 0, userInfo: [NSLocalizedDescriptionKey: "frameRate must be greater than 0"])
2752
+ }
2753
+
2754
+ if let fileVideoOutput = self.fileVideoOutput, fileVideoOutput.isRecording {
2755
+ throw NSError(domain: "CameraPreview", code: 0, userInfo: [NSLocalizedDescriptionKey: "Cannot change video frame rate while recording"])
2756
+ }
2757
+
2758
+ guard let device = activeVideoDevice() else {
2759
+ throw CameraControllerError.noCamerasAvailable
2760
+ }
2761
+
2762
+ let supportedRates = allSupportedFrameRates(for: device)
2763
+ guard supportedRates.contains(frameRate) else {
2764
+ throw NSError(
2765
+ domain: "CameraPreview",
2766
+ code: 0,
2767
+ userInfo: [NSLocalizedDescriptionKey: "Unsupported frame rate \(frameRate). Supported values: \(supportedRates.map(String.init).joined(separator: ", "))"]
2768
+ )
2769
+ }
2770
+
2771
+ guard let targetFormat = device.formats.first(where: { formatSupportsFrameRate($0, frameRate: frameRate) }) else {
2772
+ throw NSError(
2773
+ domain: "CameraPreview",
2774
+ code: 0,
2775
+ userInfo: [NSLocalizedDescriptionKey: "Unsupported frame rate \(frameRate) for the active camera"]
2776
+ )
2777
+ }
2778
+
2779
+ let frameDuration = CMTime(value: 1, timescale: CMTimeScale(frameRate))
2780
+
2781
+ do {
2782
+ try device.lockForConfiguration()
2783
+ if device.activeFormat != targetFormat {
2784
+ device.activeFormat = targetFormat
2785
+ }
2786
+ device.activeVideoMinFrameDuration = frameDuration
2787
+ device.activeVideoMaxFrameDuration = frameDuration
2788
+ device.unlockForConfiguration()
2789
+ self.configuredVideoFrameRate = frameRate
2790
+ } catch {
2791
+ throw CameraControllerError.invalidOperation
2792
+ }
2793
+ }
2794
+
2795
+ func captureVideo(maxDuration: Float? = nil, maxFileSize: Int? = nil, allowHapticsAndSystemSoundsDuringRecording: Bool = false) throws {
2585
2796
  guard let captureSession = self.captureSession, captureSession.isRunning else {
2586
2797
  throw CameraControllerError.captureSessionIsMissing
2587
2798
  }
@@ -2602,6 +2813,7 @@ extension CameraController {
2602
2813
  let audioSession = AVAudioSession.sharedInstance()
2603
2814
  try audioSession.setCategory(.playAndRecord, mode: .videoRecording, options: [.defaultToSpeaker])
2604
2815
  try audioSession.setActive(true)
2816
+ try audioSession.setAllowHapticsAndSystemSoundsDuringRecording(allowHapticsAndSystemSoundsDuringRecording)
2605
2817
  } catch {
2606
2818
  print("[CameraPreview] Failed to configure AVAudioSession for video recording: \(error)")
2607
2819
  }
@@ -2610,15 +2822,21 @@ extension CameraController {
2610
2822
  // Ensure the movie file output is attached to the active session.
2611
2823
  // If the camera was started without cameraMode=true, the output may not have been added yet.
2612
2824
  if !captureSession.outputs.contains(where: { $0 === fileVideoOutput }) {
2613
- captureSession.beginConfiguration()
2614
- if captureSession.canAddOutput(fileVideoOutput) {
2615
- captureSession.addOutput(fileVideoOutput)
2616
- self.applyVideoStabilizationMode()
2617
- } else {
2618
- captureSession.commitConfiguration()
2825
+ var didAddOutput = false
2826
+ self.configureCaptureSession {
2827
+ if captureSession.canAddOutput(fileVideoOutput) {
2828
+ captureSession.addOutput(fileVideoOutput)
2829
+ do {
2830
+ try self.refreshVideoStabilizationAfterMovieOutputAttached()
2831
+ didAddOutput = true
2832
+ } catch {
2833
+ didAddOutput = false
2834
+ }
2835
+ }
2836
+ }
2837
+ guard didAddOutput else {
2619
2838
  throw CameraControllerError.invalidOperation
2620
2839
  }
2621
- captureSession.commitConfiguration()
2622
2840
  }
2623
2841
 
2624
2842
  if let connection = fileVideoOutput.connection(with: .video) {
@@ -2655,7 +2873,9 @@ extension CameraController {
2655
2873
  fileVideoOutput.setOutputSettings([AVVideoCodecKey: codecType], for: connection)
2656
2874
  }
2657
2875
 
2658
- self.applyVideoStabilizationMode()
2876
+ self.configureCaptureSession {
2877
+ self.applyVideoStabilizationMode()
2878
+ }
2659
2879
 
2660
2880
  // Start recording video
2661
2881
  fileVideoOutput.startRecording(to: fileUrl, recordingDelegate: self)
@@ -2664,21 +2884,26 @@ extension CameraController {
2664
2884
  self.videoFileURL = fileUrl
2665
2885
  }
2666
2886
 
2667
- func stopRecording(completion: @escaping (URL?, Error?) -> Void) {
2887
+ func stopRecording(completion: @escaping (URL?, String?, Error?) -> Void) {
2668
2888
  guard let captureSession = self.captureSession, captureSession.isRunning else {
2669
- completion(nil, CameraControllerError.captureSessionIsMissing)
2889
+ completion(nil, nil, CameraControllerError.captureSessionIsMissing)
2670
2890
  return
2671
2891
  }
2672
2892
  guard let fileVideoOutput = self.fileVideoOutput else {
2673
- completion(nil, CameraControllerError.fileVideoOutputNotFound)
2893
+ completion(nil, nil, CameraControllerError.fileVideoOutputNotFound)
2894
+ return
2895
+ }
2896
+ guard fileVideoOutput.isRecording else {
2897
+ completion(nil, nil, CameraControllerError.invalidOperation)
2898
+ return
2899
+ }
2900
+ guard self.stopRecordingCompletion == nil else {
2901
+ completion(nil, nil, CameraControllerError.invalidOperation)
2674
2902
  return
2675
2903
  }
2676
2904
 
2677
- // Stop recording video
2905
+ self.stopRecordingCompletion = completion
2678
2906
  fileVideoOutput.stopRecording()
2679
-
2680
- // Return the video file URL in the completion handler
2681
- completion(self.videoFileURL, nil)
2682
2907
  }
2683
2908
  }
2684
2909
 
@@ -3074,6 +3299,19 @@ extension UIImage {
3074
3299
  extension CameraController: AVCaptureFileOutputRecordingDelegate {
3075
3300
  func fileOutput(_ output: AVCaptureFileOutput, didFinishRecordingTo outputFileURL: URL, from connections: [AVCaptureConnection], error: Error?) {
3076
3301
  let reason = recordingFinishReason(from: error)
3302
+ let stopCompletion = self.stopRecordingCompletion
3303
+ self.stopRecordingCompletion = nil
3304
+
3305
+ if let stopCompletion = stopCompletion {
3306
+ DispatchQueue.main.async {
3307
+ if reason == "error" {
3308
+ stopCompletion(nil, nil, error ?? CameraControllerError.unknown)
3309
+ } else {
3310
+ stopCompletion(outputFileURL, reason, nil)
3311
+ }
3312
+ }
3313
+ }
3314
+
3077
3315
  if reason == "error" {
3078
3316
  print("Error recording movie: \(error?.localizedDescription ?? "unknown")")
3079
3317
  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.8.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: "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
@@ -673,6 +676,7 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin, CLLocationManagerDelega
673
676
  print(" - disableFocusIndicator: \(call.getBool("disableFocusIndicator") ?? false)")
674
677
  print(" - force: \(call.getBool("force") ?? false)")
675
678
  print(" - videoQuality: \(call.getString("videoQuality") ?? "high")")
679
+ print(" - videoStabilizationMode: \(call.getString("videoStabilizationMode") ?? "off")")
676
680
 
677
681
  let force = call.getBool("force") ?? false
678
682
 
@@ -775,6 +779,7 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin, CLLocationManagerDelega
775
779
 
776
780
  // Default to high if not provided
777
781
  let videoQuality = call.getString("videoQuality") ?? "high"
782
+ let videoStabilizationMode = call.getString("videoStabilizationMode")
778
783
  self.pendingStartBarcodeScannerOptions = self.barcodeScannerStartOptions(from: call)
779
784
 
780
785
  let initialZoomLevel = call.getFloat("initialZoomLevel")
@@ -798,6 +803,19 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin, CLLocationManagerDelega
798
803
  return
799
804
  }
800
805
 
806
+ if let videoStabilizationMode = videoStabilizationMode {
807
+ do {
808
+ try self.cameraController.setVideoStabilizationMode(videoStabilizationMode)
809
+ } catch {
810
+ DispatchQueue.main.async {
811
+ self.isInitializing = false
812
+ self.pendingStartBarcodeScannerOptions = nil
813
+ call.reject("Failed to set video stabilization mode: \(error.localizedDescription)")
814
+ }
815
+ return
816
+ }
817
+ }
818
+
801
819
  self.cameraController.prepare(cameraPosition: self.cameraPosition, deviceId: deviceId, disableAudio: self.disableAudio, cameraMode: cameraMode, aspectRatio: self.aspectRatio, aspectMode: self.aspectMode, initialZoomLevel: initialZoomLevel, disableFocusIndicator: self.disableFocusIndicator, videoQuality: videoQuality) { error in
802
820
  if let error = error {
803
821
  print(error)
@@ -1693,7 +1711,15 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin, CLLocationManagerDelega
1693
1711
  }
1694
1712
 
1695
1713
  do {
1696
- try self.cameraController.captureVideo(maxDuration: maxDuration, maxFileSize: maxFileSize)
1714
+ if let frameRate = call.getInt("frameRate") {
1715
+ try self.cameraController.setVideoFrameRate(frameRate)
1716
+ }
1717
+ let allowHapticsAndSystemSoundsDuringRecording = call.getBool("allowHapticsAndSystemSoundsDuringRecording") ?? false
1718
+ try self.cameraController.captureVideo(
1719
+ maxDuration: maxDuration,
1720
+ maxFileSize: maxFileSize,
1721
+ allowHapticsAndSystemSoundsDuringRecording: allowHapticsAndSystemSoundsDuringRecording
1722
+ )
1697
1723
  call.resolve()
1698
1724
  } catch {
1699
1725
  call.reject(error.localizedDescription)
@@ -1701,8 +1727,8 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin, CLLocationManagerDelega
1701
1727
  }
1702
1728
 
1703
1729
  @objc func stopRecordVideo(_ call: CAPPluginCall) {
1704
- self.cameraController.stopRecording { (fileURL, error) in
1705
- guard let fileURL = fileURL else {
1730
+ self.cameraController.stopRecording { (fileURL, reason, error) in
1731
+ guard let fileURL = fileURL, let reason = reason else {
1706
1732
  print(error ?? "Video capture error")
1707
1733
  guard let error = error else {
1708
1734
  call.reject("Video capture error")
@@ -1712,7 +1738,7 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin, CLLocationManagerDelega
1712
1738
  return
1713
1739
  }
1714
1740
 
1715
- call.resolve(["videoFilePath": fileURL.absoluteString, "reason": "manual"])
1741
+ call.resolve(["videoFilePath": fileURL.absoluteString, "reason": reason])
1716
1742
  }
1717
1743
  }
1718
1744
 
@@ -2473,6 +2499,49 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin, CLLocationManagerDelega
2473
2499
  }
2474
2500
  }
2475
2501
 
2502
+ @objc func getSupportedVideoFrameRates(_ call: CAPPluginCall) {
2503
+ guard isInitialized else {
2504
+ call.reject("Camera not initialized")
2505
+ return
2506
+ }
2507
+ do {
2508
+ let frameRates = try self.cameraController.getSupportedVideoFrameRates()
2509
+ call.resolve(["frameRates": frameRates])
2510
+ } catch {
2511
+ call.reject("Failed to get supported video frame rates: \(error.localizedDescription)")
2512
+ }
2513
+ }
2514
+
2515
+ @objc func getVideoFrameRate(_ call: CAPPluginCall) {
2516
+ guard isInitialized else {
2517
+ call.reject("Camera not initialized")
2518
+ return
2519
+ }
2520
+ do {
2521
+ let frameRate = try self.cameraController.getVideoFrameRate()
2522
+ call.resolve(["frameRate": frameRate])
2523
+ } catch {
2524
+ call.reject("Failed to get video frame rate: \(error.localizedDescription)")
2525
+ }
2526
+ }
2527
+
2528
+ @objc func setVideoFrameRate(_ call: CAPPluginCall) {
2529
+ guard isInitialized else {
2530
+ call.reject("Camera not initialized")
2531
+ return
2532
+ }
2533
+ guard let frameRate = call.getInt("frameRate") else {
2534
+ call.reject("frameRate parameter is required")
2535
+ return
2536
+ }
2537
+ do {
2538
+ try self.cameraController.setVideoFrameRate(frameRate)
2539
+ call.resolve()
2540
+ } catch {
2541
+ call.reject(error.localizedDescription)
2542
+ }
2543
+ }
2544
+
2476
2545
  private var lastOrientation: String?
2477
2546
 
2478
2547
  @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.8.0",
4
4
  "description": "Camera preview",
5
5
  "license": "MPL-2.0",
6
6
  "repository": {