@capgo/camera-preview 8.5.1 → 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
@@ -57,7 +57,6 @@ class CameraController: NSObject {
57
57
  }
58
58
  }
59
59
 
60
-
61
60
  private func setVideoOrientation(_ orientation: AVCaptureVideoOrientation, on connection: AVCaptureConnection) {
62
61
  guard connection.isVideoOrientationSupported else { return }
63
62
  connection.videoOrientation = orientation
@@ -151,10 +150,13 @@ class CameraController: NSObject {
151
150
 
152
151
  var videoFileURL: URL?
153
152
  var recordingFinishedCallback: ((URL, String) -> Void)?
153
+ private var stopRecordingCompletion: ((URL?, String?, Error?) -> Void)?
154
154
  private let saneMaxZoomFactor: CGFloat = 25.5
155
155
 
156
156
  var videoQuality: String = "high"
157
+ private var configuredVideoFrameRate: Int?
157
158
  var videoCodec: String = "avc1"
159
+ var videoStabilizationMode: String = "off"
158
160
 
159
161
  private static let allVideoQualities = ["low", "medium", "high", "2160p", "1080p", "720p", "480p", "4:3"]
160
162
 
@@ -213,6 +215,138 @@ class CameraController: NSObject {
213
215
  return self.videoCodec
214
216
  }
215
217
 
218
+ func isVideoStabilizationSupported() -> Bool {
219
+ guard let connection = self.fileVideoOutput?.connection(with: .video) else {
220
+ return false
221
+ }
222
+ return connection.isVideoStabilizationSupported
223
+ }
224
+
225
+ func getSupportedVideoStabilizationModes() -> [String] {
226
+ guard let connection = self.fileVideoOutput?.connection(with: .video),
227
+ connection.isVideoStabilizationSupported else {
228
+ return ["off"]
229
+ }
230
+
231
+ let savedPreferred = connection.preferredVideoStabilizationMode
232
+ var supported: [String] = []
233
+
234
+ for candidate in Self.allVideoStabilizationModeCandidates() {
235
+ connection.preferredVideoStabilizationMode = candidate.mode
236
+ if connection.preferredVideoStabilizationMode == candidate.mode {
237
+ supported.append(candidate.name)
238
+ }
239
+ }
240
+
241
+ connection.preferredVideoStabilizationMode = savedPreferred
242
+ return supported.isEmpty ? ["off"] : supported
243
+ }
244
+
245
+ func getVideoStabilizationMode() -> String {
246
+ if let connection = self.fileVideoOutput?.connection(with: .video),
247
+ connection.isVideoStabilizationSupported {
248
+ return self.videoStabilizationModeString(from: connection.activeVideoStabilizationMode)
249
+ }
250
+ return self.videoStabilizationMode
251
+ }
252
+
253
+ func setVideoStabilizationMode(_ mode: String) throws {
254
+ if self.fileVideoOutput?.isRecording == true {
255
+ throw CameraControllerError.recordingInProgress
256
+ }
257
+ guard let avMode = self.avVideoStabilizationMode(from: mode) else {
258
+ throw CameraControllerError.invalidOperation
259
+ }
260
+ let supportedModes = self.getSupportedVideoStabilizationModes()
261
+ guard supportedModes.contains(mode) else {
262
+ throw CameraControllerError.invalidOperation
263
+ }
264
+ self.videoStabilizationMode = mode
265
+ self.applyVideoStabilizationMode(avMode)
266
+ }
267
+
268
+ private static func allVideoStabilizationModeCandidates() -> [(name: String, mode: AVCaptureVideoStabilizationMode)] {
269
+ var candidates: [(String, AVCaptureVideoStabilizationMode)] = [
270
+ ("off", .off),
271
+ ("standard", .standard),
272
+ ("cinematic", .cinematic),
273
+ ("cinematicExtended", .cinematicExtended),
274
+ ("auto", .auto)
275
+ ]
276
+ if #available(iOS 17.0, *) {
277
+ candidates.append(("previewOptimized", .previewOptimized))
278
+ }
279
+ if #available(iOS 18.0, *) {
280
+ candidates.append(("cinematicExtendedEnhanced", .cinematicExtendedEnhanced))
281
+ }
282
+ return candidates
283
+ }
284
+
285
+ private func applyVideoStabilizationMode(_ mode: AVCaptureVideoStabilizationMode? = nil) {
286
+ guard let connection = self.fileVideoOutput?.connection(with: .video),
287
+ connection.isVideoStabilizationSupported else {
288
+ return
289
+ }
290
+ let targetMode = mode ?? self.avVideoStabilizationMode(from: self.videoStabilizationMode) ?? .off
291
+ connection.preferredVideoStabilizationMode = targetMode
292
+ }
293
+
294
+ private func avVideoStabilizationMode(from mode: String) -> AVCaptureVideoStabilizationMode? {
295
+ switch mode {
296
+ case "off":
297
+ return .off
298
+ case "standard":
299
+ return .standard
300
+ case "cinematic":
301
+ return .cinematic
302
+ case "cinematicExtended":
303
+ return .cinematicExtended
304
+ case "auto":
305
+ return .auto
306
+ case "previewOptimized":
307
+ if #available(iOS 17.0, *) {
308
+ return .previewOptimized
309
+ }
310
+ return nil
311
+ case "cinematicExtendedEnhanced":
312
+ if #available(iOS 18.0, *) {
313
+ return .cinematicExtendedEnhanced
314
+ }
315
+ return nil
316
+ case "lowLatency":
317
+ return nil
318
+ default:
319
+ return nil
320
+ }
321
+ }
322
+
323
+ private func videoStabilizationModeString(from mode: AVCaptureVideoStabilizationMode) -> String {
324
+ switch mode {
325
+ case .off:
326
+ return "off"
327
+ case .standard:
328
+ return "standard"
329
+ case .cinematic:
330
+ return "cinematic"
331
+ case .cinematicExtended:
332
+ return "cinematicExtended"
333
+ case .auto:
334
+ return "auto"
335
+ default:
336
+ if #available(iOS 17.0, *) {
337
+ if mode == .previewOptimized {
338
+ return "previewOptimized"
339
+ }
340
+ }
341
+ if #available(iOS 18.0, *) {
342
+ if mode == .cinematicExtendedEnhanced {
343
+ return "cinematicExtendedEnhanced"
344
+ }
345
+ }
346
+ return self.videoStabilizationMode
347
+ }
348
+ }
349
+
216
350
  private func avVideoCodecType(for codec: String) -> AVVideoCodecType? {
217
351
  switch codec.lowercased() {
218
352
  case "avc1":
@@ -515,6 +649,7 @@ extension CameraController {
515
649
  captureSession.addOutput(fileVideoOutput)
516
650
  // Set orientation immediately
517
651
  self.setVideoOrientation(videoOrientation, on: fileVideoOutput.connections)
652
+ self.applyVideoStabilizationMode()
518
653
  }
519
654
 
520
655
  // Set up preview layer session in the same configuration block
@@ -987,6 +1122,7 @@ extension CameraController {
987
1122
  }
988
1123
 
989
1124
  func switchCameras() throws {
1125
+ configuredVideoFrameRate = nil
990
1126
  guard let currentCameraPosition = currentCameraPosition,
991
1127
  let captureSession = self.captureSession else {
992
1128
  throw CameraControllerError.captureSessionIsMissing
@@ -1094,6 +1230,7 @@ extension CameraController {
1094
1230
  captureSession.removeOutput(fileVideoOutput)
1095
1231
  if captureSession.canAddOutput(fileVideoOutput) {
1096
1232
  captureSession.addOutput(fileVideoOutput)
1233
+ self.applyVideoStabilizationMode()
1097
1234
  }
1098
1235
  }
1099
1236
 
@@ -2240,6 +2377,7 @@ extension CameraController {
2240
2377
  }
2241
2378
 
2242
2379
  func cleanup() {
2380
+ configuredVideoFrameRate = nil
2243
2381
  stopBarcodeScanner()
2244
2382
  if let captureSession = self.captureSession {
2245
2383
  captureSession.stopRunning()
@@ -2447,6 +2585,129 @@ extension CameraController {
2447
2585
  }
2448
2586
  }
2449
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
+
2450
2711
  func captureVideo(maxDuration: Float? = nil, maxFileSize: Int? = nil) throws {
2451
2712
  guard let captureSession = self.captureSession, captureSession.isRunning else {
2452
2713
  throw CameraControllerError.captureSessionIsMissing
@@ -2479,6 +2740,7 @@ extension CameraController {
2479
2740
  captureSession.beginConfiguration()
2480
2741
  if captureSession.canAddOutput(fileVideoOutput) {
2481
2742
  captureSession.addOutput(fileVideoOutput)
2743
+ self.applyVideoStabilizationMode()
2482
2744
  } else {
2483
2745
  captureSession.commitConfiguration()
2484
2746
  throw CameraControllerError.invalidOperation
@@ -2520,6 +2782,8 @@ extension CameraController {
2520
2782
  fileVideoOutput.setOutputSettings([AVVideoCodecKey: codecType], for: connection)
2521
2783
  }
2522
2784
 
2785
+ self.applyVideoStabilizationMode()
2786
+
2523
2787
  // Start recording video
2524
2788
  fileVideoOutput.startRecording(to: fileUrl, recordingDelegate: self)
2525
2789
 
@@ -2527,21 +2791,26 @@ extension CameraController {
2527
2791
  self.videoFileURL = fileUrl
2528
2792
  }
2529
2793
 
2530
- func stopRecording(completion: @escaping (URL?, Error?) -> Void) {
2794
+ func stopRecording(completion: @escaping (URL?, String?, Error?) -> Void) {
2531
2795
  guard let captureSession = self.captureSession, captureSession.isRunning else {
2532
- completion(nil, CameraControllerError.captureSessionIsMissing)
2796
+ completion(nil, nil, CameraControllerError.captureSessionIsMissing)
2533
2797
  return
2534
2798
  }
2535
2799
  guard let fileVideoOutput = self.fileVideoOutput else {
2536
- 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)
2537
2809
  return
2538
2810
  }
2539
2811
 
2540
- // Stop recording video
2812
+ self.stopRecordingCompletion = completion
2541
2813
  fileVideoOutput.stopRecording()
2542
-
2543
- // Return the video file URL in the completion handler
2544
- completion(self.videoFileURL, nil)
2545
2814
  }
2546
2815
  }
2547
2816
 
@@ -2824,6 +3093,7 @@ enum CameraControllerError: Swift.Error {
2824
3093
  case invalidOperation
2825
3094
  case noCamerasAvailable
2826
3095
  case cannotFindDocumentsDirectory
3096
+ case recordingInProgress
2827
3097
  case fileVideoOutputNotFound
2828
3098
  case unknown
2829
3099
  case invalidZoomLevel(min: CGFloat, max: CGFloat, requested: CGFloat)
@@ -2851,6 +3121,8 @@ extension CameraControllerError: LocalizedError {
2851
3121
  return NSLocalizedString("Unknown", comment: "Unknown")
2852
3122
  case .cannotFindDocumentsDirectory:
2853
3123
  return NSLocalizedString("Cannot find documents directory", comment: "This should never happen")
3124
+ case .recordingInProgress:
3125
+ return NSLocalizedString("Cannot change video stabilization while recording is in progress.", comment: "Recording in progress")
2854
3126
  case .fileVideoOutputNotFound:
2855
3127
  return NSLocalizedString("Video recording is not available. Make sure the camera is properly initialized.", comment: "Video recording not available")
2856
3128
  case .invalidZoomLevel(let min, let max, let requested):
@@ -2934,6 +3206,19 @@ extension UIImage {
2934
3206
  extension CameraController: AVCaptureFileOutputRecordingDelegate {
2935
3207
  func fileOutput(_ output: AVCaptureFileOutput, didFinishRecordingTo outputFileURL: URL, from connections: [AVCaptureConnection], error: Error?) {
2936
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
+
2937
3222
  if reason == "error" {
2938
3223
  print("Error recording movie: \(error?.localizedDescription ?? "unknown")")
2939
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.5.1"
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] = [
@@ -54,6 +54,10 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin, CLLocationManagerDelega
54
54
  CAPPluginMethod(name: "getSupportedVideoCodecs", returnType: CAPPluginReturnPromise),
55
55
  CAPPluginMethod(name: "getVideoCodec", returnType: CAPPluginReturnPromise),
56
56
  CAPPluginMethod(name: "setVideoCodec", returnType: CAPPluginReturnPromise),
57
+ CAPPluginMethod(name: "isVideoStabilizationSupported", returnType: CAPPluginReturnPromise),
58
+ CAPPluginMethod(name: "getSupportedVideoStabilizationModes", returnType: CAPPluginReturnPromise),
59
+ CAPPluginMethod(name: "getVideoStabilizationMode", returnType: CAPPluginReturnPromise),
60
+ CAPPluginMethod(name: "setVideoStabilizationMode", returnType: CAPPluginReturnPromise),
57
61
  CAPPluginMethod(name: "getSupportedVideoQualities", returnType: CAPPluginReturnPromise),
58
62
  CAPPluginMethod(name: "getVideoQuality", returnType: CAPPluginReturnPromise),
59
63
  CAPPluginMethod(name: "setVideoQuality", returnType: CAPPluginReturnPromise),
@@ -86,6 +90,9 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin, CLLocationManagerDelega
86
90
  CAPPluginMethod(name: "getExposureCompensationRange", returnType: CAPPluginReturnPromise),
87
91
  CAPPluginMethod(name: "getExposureCompensation", returnType: CAPPluginReturnPromise),
88
92
  CAPPluginMethod(name: "setExposureCompensation", returnType: CAPPluginReturnPromise),
93
+ CAPPluginMethod(name: "getSupportedVideoFrameRates", returnType: CAPPluginReturnPromise),
94
+ CAPPluginMethod(name: "getVideoFrameRate", returnType: CAPPluginReturnPromise),
95
+ CAPPluginMethod(name: "setVideoFrameRate", returnType: CAPPluginReturnPromise),
89
96
  CAPPluginMethod(name: "getPluginVersion", returnType: CAPPluginReturnPromise)
90
97
  ]
91
98
  // Camera state tracking
@@ -1643,12 +1650,45 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin, CLLocationManagerDelega
1643
1650
  call.resolve(["codecs": self.cameraController.getSupportedVideoCodecs()])
1644
1651
  }
1645
1652
 
1653
+ @objc func isVideoStabilizationSupported(_ call: CAPPluginCall) {
1654
+ call.resolve(["supported": self.cameraController.isVideoStabilizationSupported()])
1655
+ }
1656
+
1657
+ @objc func getSupportedVideoStabilizationModes(_ call: CAPPluginCall) {
1658
+ call.resolve(["modes": self.cameraController.getSupportedVideoStabilizationModes()])
1659
+ }
1660
+
1661
+ @objc func getVideoStabilizationMode(_ call: CAPPluginCall) {
1662
+ call.resolve(["mode": self.cameraController.getVideoStabilizationMode()])
1663
+ }
1664
+
1665
+ @objc func setVideoStabilizationMode(_ call: CAPPluginCall) {
1666
+ guard let mode = call.getString("mode") else {
1667
+ call.reject("mode is required")
1668
+ return
1669
+ }
1670
+ do {
1671
+ try self.cameraController.setVideoStabilizationMode(mode)
1672
+ call.resolve()
1673
+ } catch {
1674
+ call.reject("Failed to set video stabilization mode: \(error.localizedDescription)")
1675
+ }
1676
+ }
1677
+
1646
1678
  @objc func startRecordVideo(_ call: CAPPluginCall) {
1647
1679
  let maxDuration = call.getFloat("maxDuration")
1648
1680
  let maxFileSize = call.getInt("maxFileSize")
1649
1681
  if let videoCodec = call.getString("videoCodec") {
1650
1682
  try? self.cameraController.setVideoCodec(videoCodec)
1651
1683
  }
1684
+ if let videoStabilizationMode = call.getString("videoStabilizationMode") {
1685
+ do {
1686
+ try self.cameraController.setVideoStabilizationMode(videoStabilizationMode)
1687
+ } catch {
1688
+ call.reject("Failed to set video stabilization mode: \(error.localizedDescription)")
1689
+ return
1690
+ }
1691
+ }
1652
1692
  self.cameraController.recordingFinishedCallback = { [weak self] fileURL, reason in
1653
1693
  DispatchQueue.main.async {
1654
1694
  self?.notifyListeners("recordingFinished", data: ["videoFilePath": fileURL.absoluteString, "reason": reason])
@@ -1656,6 +1696,9 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin, CLLocationManagerDelega
1656
1696
  }
1657
1697
 
1658
1698
  do {
1699
+ if let frameRate = call.getInt("frameRate") {
1700
+ try self.cameraController.setVideoFrameRate(frameRate)
1701
+ }
1659
1702
  try self.cameraController.captureVideo(maxDuration: maxDuration, maxFileSize: maxFileSize)
1660
1703
  call.resolve()
1661
1704
  } catch {
@@ -1664,8 +1707,8 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin, CLLocationManagerDelega
1664
1707
  }
1665
1708
 
1666
1709
  @objc func stopRecordVideo(_ call: CAPPluginCall) {
1667
- self.cameraController.stopRecording { (fileURL, error) in
1668
- guard let fileURL = fileURL else {
1710
+ self.cameraController.stopRecording { (fileURL, reason, error) in
1711
+ guard let fileURL = fileURL, let reason = reason else {
1669
1712
  print(error ?? "Video capture error")
1670
1713
  guard let error = error else {
1671
1714
  call.reject("Video capture error")
@@ -1675,7 +1718,7 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin, CLLocationManagerDelega
1675
1718
  return
1676
1719
  }
1677
1720
 
1678
- call.resolve(["videoFilePath": fileURL.absoluteString, "reason": "manual"])
1721
+ call.resolve(["videoFilePath": fileURL.absoluteString, "reason": reason])
1679
1722
  }
1680
1723
  }
1681
1724
 
@@ -2436,6 +2479,49 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin, CLLocationManagerDelega
2436
2479
  }
2437
2480
  }
2438
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
+
2439
2525
  private var lastOrientation: String?
2440
2526
 
2441
2527
  @objc private func handleOrientationChange() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@capgo/camera-preview",
3
- "version": "8.5.1",
3
+ "version": "8.7.1",
4
4
  "description": "Camera preview",
5
5
  "license": "MPL-2.0",
6
6
  "repository": {