@capgo/camera-preview 8.5.0 → 8.6.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.
@@ -57,6 +57,15 @@ class CameraController: NSObject {
57
57
  }
58
58
  }
59
59
 
60
+ private func setVideoOrientation(_ orientation: AVCaptureVideoOrientation, on connection: AVCaptureConnection) {
61
+ guard connection.isVideoOrientationSupported else { return }
62
+ connection.videoOrientation = orientation
63
+ }
64
+
65
+ private func setVideoOrientation(_ orientation: AVCaptureVideoOrientation, on connections: [AVCaptureConnection]) {
66
+ connections.forEach { setVideoOrientation(orientation, on: $0) }
67
+ }
68
+
60
69
  // Continuous focus with significant movement if focus was locked from setFocus earlier
61
70
  @objc private func subjectAreaDidChange(notification: NSNotification) {
62
71
  guard let device = self.currentCameraPosition == .rear ? rearCamera : frontCamera else { return }
@@ -145,6 +154,7 @@ class CameraController: NSObject {
145
154
 
146
155
  var videoQuality: String = "high"
147
156
  var videoCodec: String = "avc1"
157
+ var videoStabilizationMode: String = "off"
148
158
 
149
159
  private static let allVideoQualities = ["low", "medium", "high", "2160p", "1080p", "720p", "480p", "4:3"]
150
160
 
@@ -203,6 +213,138 @@ class CameraController: NSObject {
203
213
  return self.videoCodec
204
214
  }
205
215
 
216
+ func isVideoStabilizationSupported() -> Bool {
217
+ guard let connection = self.fileVideoOutput?.connection(with: .video) else {
218
+ return false
219
+ }
220
+ return connection.isVideoStabilizationSupported
221
+ }
222
+
223
+ 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
+ }
237
+ }
238
+
239
+ connection.preferredVideoStabilizationMode = savedPreferred
240
+ return supported.isEmpty ? ["off"] : supported
241
+ }
242
+
243
+ func getVideoStabilizationMode() -> String {
244
+ if let connection = self.fileVideoOutput?.connection(with: .video),
245
+ connection.isVideoStabilizationSupported {
246
+ return self.videoStabilizationModeString(from: connection.activeVideoStabilizationMode)
247
+ }
248
+ return self.videoStabilizationMode
249
+ }
250
+
251
+ func setVideoStabilizationMode(_ mode: String) throws {
252
+ if self.fileVideoOutput?.isRecording == true {
253
+ throw CameraControllerError.recordingInProgress
254
+ }
255
+ guard let avMode = self.avVideoStabilizationMode(from: mode) else {
256
+ throw CameraControllerError.invalidOperation
257
+ }
258
+ let supportedModes = self.getSupportedVideoStabilizationModes()
259
+ guard supportedModes.contains(mode) else {
260
+ throw CameraControllerError.invalidOperation
261
+ }
262
+ self.videoStabilizationMode = mode
263
+ self.applyVideoStabilizationMode(avMode)
264
+ }
265
+
266
+ private static func allVideoStabilizationModeCandidates() -> [(name: String, mode: AVCaptureVideoStabilizationMode)] {
267
+ var candidates: [(String, AVCaptureVideoStabilizationMode)] = [
268
+ ("off", .off),
269
+ ("standard", .standard),
270
+ ("cinematic", .cinematic),
271
+ ("cinematicExtended", .cinematicExtended),
272
+ ("auto", .auto),
273
+ ]
274
+ if #available(iOS 17.0, *) {
275
+ candidates.append(("previewOptimized", .previewOptimized))
276
+ }
277
+ if #available(iOS 18.0, *) {
278
+ candidates.append(("cinematicExtendedEnhanced", .cinematicExtendedEnhanced))
279
+ }
280
+ return candidates
281
+ }
282
+
283
+ private func applyVideoStabilizationMode(_ mode: AVCaptureVideoStabilizationMode? = nil) {
284
+ guard let connection = self.fileVideoOutput?.connection(with: .video),
285
+ connection.isVideoStabilizationSupported else {
286
+ return
287
+ }
288
+ let targetMode = mode ?? self.avVideoStabilizationMode(from: self.videoStabilizationMode) ?? .off
289
+ connection.preferredVideoStabilizationMode = targetMode
290
+ }
291
+
292
+ private func avVideoStabilizationMode(from mode: String) -> AVCaptureVideoStabilizationMode? {
293
+ switch mode {
294
+ case "off":
295
+ return .off
296
+ case "standard":
297
+ return .standard
298
+ case "cinematic":
299
+ return .cinematic
300
+ case "cinematicExtended":
301
+ return .cinematicExtended
302
+ case "auto":
303
+ return .auto
304
+ case "previewOptimized":
305
+ if #available(iOS 17.0, *) {
306
+ return .previewOptimized
307
+ }
308
+ return nil
309
+ case "cinematicExtendedEnhanced":
310
+ if #available(iOS 18.0, *) {
311
+ return .cinematicExtendedEnhanced
312
+ }
313
+ return nil
314
+ case "lowLatency":
315
+ return nil
316
+ default:
317
+ return nil
318
+ }
319
+ }
320
+
321
+ private func videoStabilizationModeString(from mode: AVCaptureVideoStabilizationMode) -> String {
322
+ switch mode {
323
+ case .off:
324
+ return "off"
325
+ case .standard:
326
+ return "standard"
327
+ case .cinematic:
328
+ return "cinematic"
329
+ case .cinematicExtended:
330
+ return "cinematicExtended"
331
+ case .auto:
332
+ return "auto"
333
+ default:
334
+ if #available(iOS 17.0, *) {
335
+ if mode == .previewOptimized {
336
+ return "previewOptimized"
337
+ }
338
+ }
339
+ if #available(iOS 18.0, *) {
340
+ if mode == .cinematicExtendedEnhanced {
341
+ return "cinematicExtendedEnhanced"
342
+ }
343
+ }
344
+ return self.videoStabilizationMode
345
+ }
346
+ }
347
+
206
348
  private func avVideoCodecType(for codec: String) -> AVVideoCodecType? {
207
349
  switch codec.lowercased() {
208
350
  case "avc1":
@@ -416,45 +558,7 @@ extension CameraController {
416
558
  let layer = AVCaptureVideoPreviewLayer()
417
559
  // Configure orientation immediately
418
560
  if let connection = layer.connection {
419
- // Ensure UI calls are made on the main thread
420
- if Thread.isMainThread {
421
- if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene {
422
- switch windowScene.interfaceOrientation {
423
- case .portrait:
424
- connection.videoOrientation = .portrait
425
- case .landscapeLeft:
426
- connection.videoOrientation = .landscapeLeft
427
- case .landscapeRight:
428
- connection.videoOrientation = .landscapeRight
429
- case .portraitUpsideDown:
430
- connection.videoOrientation = .portraitUpsideDown
431
- case .unknown:
432
- fallthrough
433
- @unknown default:
434
- connection.videoOrientation = .portrait
435
- }
436
- }
437
- } else {
438
- // If not on main thread, use a sync call to get the orientation
439
- DispatchQueue.main.sync {
440
- if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene {
441
- switch windowScene.interfaceOrientation {
442
- case .portrait:
443
- connection.videoOrientation = .portrait
444
- case .landscapeLeft:
445
- connection.videoOrientation = .landscapeLeft
446
- case .landscapeRight:
447
- connection.videoOrientation = .landscapeRight
448
- case .portraitUpsideDown:
449
- connection.videoOrientation = .portraitUpsideDown
450
- case .unknown:
451
- fallthrough
452
- @unknown default:
453
- connection.videoOrientation = .portrait
454
- }
455
- }
456
- }
457
- }
561
+ self.setVideoOrientation(self.getVideoOrientation(), on: connection)
458
562
  }
459
563
  // Don't set session here - we'll do it during configuration
460
564
  self.previewLayer = layer
@@ -527,7 +631,7 @@ extension CameraController {
527
631
  let videoQueue = DispatchQueue(label: "com.camera.videoQueue", qos: .userInteractive)
528
632
  dataOutput.setSampleBufferDelegate(self, queue: videoQueue)
529
633
  // Set orientation immediately
530
- dataOutput.connections.forEach { $0.videoOrientation = videoOrientation }
634
+ self.setVideoOrientation(videoOrientation, on: dataOutput.connections)
531
635
  }
532
636
 
533
637
  // Add photo output immediately to avoid later reconfiguration
@@ -535,21 +639,22 @@ extension CameraController {
535
639
  photoOutput.isHighResolutionCaptureEnabled = true
536
640
  captureSession.addOutput(photoOutput)
537
641
  // Set orientation immediately
538
- photoOutput.connections.forEach { $0.videoOrientation = videoOrientation }
642
+ self.setVideoOrientation(videoOrientation, on: photoOutput.connections)
539
643
  }
540
644
 
541
645
  // Add video output if in camera mode
542
646
  if cameraMode, let fileVideoOutput = self.fileVideoOutput, captureSession.canAddOutput(fileVideoOutput) {
543
647
  captureSession.addOutput(fileVideoOutput)
544
648
  // Set orientation immediately
545
- fileVideoOutput.connections.forEach { $0.videoOrientation = videoOrientation }
649
+ self.setVideoOrientation(videoOrientation, on: fileVideoOutput.connections)
650
+ self.applyVideoStabilizationMode()
546
651
  }
547
652
 
548
653
  // Set up preview layer session in the same configuration block
549
654
  if let layer = self.previewLayer {
550
655
  layer.session = captureSession
551
656
  // Set orientation for preview layer
552
- layer.connection?.videoOrientation = videoOrientation
657
+ if let connection = layer.connection { self.setVideoOrientation(videoOrientation, on: connection) }
553
658
  // Start with a very subtle fade to smooth any remaining visual artifacts
554
659
  layer.opacity = 0.95
555
660
  }
@@ -930,9 +1035,10 @@ extension CameraController {
930
1035
  // Apply orientation asynchronously on main thread
931
1036
  let updateBlock = { [weak self] in
932
1037
  guard let self = self else { return }
933
- self.previewLayer?.connection?.videoOrientation = videoOrientation
934
- self.dataOutput?.connections.forEach { $0.videoOrientation = videoOrientation }
935
- self.photoOutput?.connections.forEach { $0.videoOrientation = videoOrientation }
1038
+ if let connection = self.previewLayer?.connection { self.setVideoOrientation(videoOrientation, on: connection) }
1039
+ if let connections = self.dataOutput?.connections { self.setVideoOrientation(videoOrientation, on: connections) }
1040
+ if let connections = self.photoOutput?.connections { self.setVideoOrientation(videoOrientation, on: connections) }
1041
+ if let connections = self.fileVideoOutput?.connections { self.setVideoOrientation(videoOrientation, on: connections) }
936
1042
  }
937
1043
 
938
1044
  if Thread.isMainThread {
@@ -1121,6 +1227,7 @@ extension CameraController {
1121
1227
  captureSession.removeOutput(fileVideoOutput)
1122
1228
  if captureSession.canAddOutput(fileVideoOutput) {
1123
1229
  captureSession.addOutput(fileVideoOutput)
1230
+ self.applyVideoStabilizationMode()
1124
1231
  }
1125
1232
  }
1126
1233
 
@@ -1138,7 +1245,7 @@ extension CameraController {
1138
1245
  if let connection = photoOutput.connection(with: .video) {
1139
1246
  let captureOrientation = self.getPhysicalOrientation()
1140
1247
  self.lastCaptureOrientation = captureOrientation
1141
- connection.videoOrientation = captureOrientation
1248
+ self.setVideoOrientation(captureOrientation, on: connection)
1142
1249
  }
1143
1250
  let settings = AVCapturePhotoSettings()
1144
1251
  // Configure photo capture settings optimized for speed
@@ -2506,6 +2613,7 @@ extension CameraController {
2506
2613
  captureSession.beginConfiguration()
2507
2614
  if captureSession.canAddOutput(fileVideoOutput) {
2508
2615
  captureSession.addOutput(fileVideoOutput)
2616
+ self.applyVideoStabilizationMode()
2509
2617
  } else {
2510
2618
  captureSession.commitConfiguration()
2511
2619
  throw CameraControllerError.invalidOperation
@@ -2516,7 +2624,7 @@ extension CameraController {
2516
2624
  if let connection = fileVideoOutput.connection(with: .video) {
2517
2625
  if connection.isEnabled == false { connection.isEnabled = true }
2518
2626
  // Goes off accelerometer now
2519
- connection.videoOrientation = self.getPhysicalOrientation()
2627
+ self.setVideoOrientation(self.getPhysicalOrientation(), on: connection)
2520
2628
 
2521
2629
  // Front camera: mirror the recorded video so it looks natural (selfie style).
2522
2630
  if self.currentCameraPosition == .front, connection.isVideoMirroringSupported {
@@ -2547,6 +2655,8 @@ extension CameraController {
2547
2655
  fileVideoOutput.setOutputSettings([AVVideoCodecKey: codecType], for: connection)
2548
2656
  }
2549
2657
 
2658
+ self.applyVideoStabilizationMode()
2659
+
2550
2660
  // Start recording video
2551
2661
  fileVideoOutput.startRecording(to: fileUrl, recordingDelegate: self)
2552
2662
 
@@ -2851,6 +2961,7 @@ enum CameraControllerError: Swift.Error {
2851
2961
  case invalidOperation
2852
2962
  case noCamerasAvailable
2853
2963
  case cannotFindDocumentsDirectory
2964
+ case recordingInProgress
2854
2965
  case fileVideoOutputNotFound
2855
2966
  case unknown
2856
2967
  case invalidZoomLevel(min: CGFloat, max: CGFloat, requested: CGFloat)
@@ -2878,6 +2989,8 @@ extension CameraControllerError: LocalizedError {
2878
2989
  return NSLocalizedString("Unknown", comment: "Unknown")
2879
2990
  case .cannotFindDocumentsDirectory:
2880
2991
  return NSLocalizedString("Cannot find documents directory", comment: "This should never happen")
2992
+ case .recordingInProgress:
2993
+ return NSLocalizedString("Cannot change video stabilization while recording is in progress.", comment: "Recording in progress")
2881
2994
  case .fileVideoOutputNotFound:
2882
2995
  return NSLocalizedString("Video recording is not available. Make sure the camera is properly initialized.", comment: "Video recording not available")
2883
2996
  case .invalidZoomLevel(let min, let max, let requested):
@@ -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.0"
38
+ private let pluginVersion: String = "8.6.0"
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),
@@ -652,7 +656,7 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin, CLLocationManagerDelega
652
656
  print("[CameraPreview] 📋 Settings received:")
653
657
  print(" - position: \(call.getString("position") ?? "rear")")
654
658
  print(" - deviceId: \(call.getString("deviceId") ?? "nil")")
655
- print(" - cameraMode: \(call.getBool("cameraMode") ?? false)")
659
+ print(" - enableVideoMode: \(call.getBool("enableVideoMode") ?? call.getBool("cameraMode") ?? false)")
656
660
  print(" - width: \(call.getInt("width") ?? 0)")
657
661
  print(" - height: \(call.getInt("height") ?? 0)")
658
662
  print(" - x: \(call.getInt("x") ?? -1)")
@@ -724,7 +728,7 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin, CLLocationManagerDelega
724
728
 
725
729
  self.cameraPosition = call.getString("position") ?? "rear"
726
730
  let deviceId = call.getString("deviceId")
727
- let cameraMode = call.getBool("cameraMode") ?? false
731
+ let cameraMode = call.getBool("enableVideoMode") ?? call.getBool("cameraMode") ?? false
728
732
 
729
733
  // Set width - use screen width if not provided or if 0
730
734
  if let width = call.getInt("width"), width > 0 {
@@ -1643,12 +1647,45 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin, CLLocationManagerDelega
1643
1647
  call.resolve(["codecs": self.cameraController.getSupportedVideoCodecs()])
1644
1648
  }
1645
1649
 
1650
+ @objc func isVideoStabilizationSupported(_ call: CAPPluginCall) {
1651
+ call.resolve(["supported": self.cameraController.isVideoStabilizationSupported()])
1652
+ }
1653
+
1654
+ @objc func getSupportedVideoStabilizationModes(_ call: CAPPluginCall) {
1655
+ call.resolve(["modes": self.cameraController.getSupportedVideoStabilizationModes()])
1656
+ }
1657
+
1658
+ @objc func getVideoStabilizationMode(_ call: CAPPluginCall) {
1659
+ call.resolve(["mode": self.cameraController.getVideoStabilizationMode()])
1660
+ }
1661
+
1662
+ @objc func setVideoStabilizationMode(_ call: CAPPluginCall) {
1663
+ guard let mode = call.getString("mode") else {
1664
+ call.reject("mode is required")
1665
+ return
1666
+ }
1667
+ do {
1668
+ try self.cameraController.setVideoStabilizationMode(mode)
1669
+ call.resolve()
1670
+ } catch {
1671
+ call.reject("Failed to set video stabilization mode: \(error.localizedDescription)")
1672
+ }
1673
+ }
1674
+
1646
1675
  @objc func startRecordVideo(_ call: CAPPluginCall) {
1647
1676
  let maxDuration = call.getFloat("maxDuration")
1648
1677
  let maxFileSize = call.getInt("maxFileSize")
1649
1678
  if let videoCodec = call.getString("videoCodec") {
1650
1679
  try? self.cameraController.setVideoCodec(videoCodec)
1651
1680
  }
1681
+ if let videoStabilizationMode = call.getString("videoStabilizationMode") {
1682
+ do {
1683
+ try self.cameraController.setVideoStabilizationMode(videoStabilizationMode)
1684
+ } catch {
1685
+ call.reject("Failed to set video stabilization mode: \(error.localizedDescription)")
1686
+ return
1687
+ }
1688
+ }
1652
1689
  self.cameraController.recordingFinishedCallback = { [weak self] fileURL, reason in
1653
1690
  DispatchQueue.main.async {
1654
1691
  self?.notifyListeners("recordingFinished", data: ["videoFilePath": fileURL.absoluteString, "reason": reason])
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@capgo/camera-preview",
3
- "version": "8.5.0",
3
+ "version": "8.6.0",
4
4
  "description": "Camera preview",
5
5
  "license": "MPL-2.0",
6
6
  "repository": {