@capgo/camera-preview 8.4.7 → 8.5.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.
@@ -140,9 +140,79 @@ class CameraController: NSObject {
140
140
  private var lastCaptureOrientation: AVCaptureVideoOrientation?
141
141
 
142
142
  var videoFileURL: URL?
143
+ var recordingFinishedCallback: ((URL, String) -> Void)?
143
144
  private let saneMaxZoomFactor: CGFloat = 25.5
144
145
 
145
146
  var videoQuality: String = "high"
147
+ var videoCodec: String = "avc1"
148
+
149
+ private static let allVideoQualities = ["low", "medium", "high", "2160p", "1080p", "720p", "480p", "4:3"]
150
+
151
+ func getSupportedVideoQualities() -> [String] {
152
+ guard let captureSession = self.captureSession else {
153
+ return Self.allVideoQualities
154
+ }
155
+ let device = self.currentCameraPosition == .front ? self.frontCamera : self.rearCamera
156
+ guard let device = device else {
157
+ return Self.allVideoQualities
158
+ }
159
+ return Self.allVideoQualities.filter { quality in
160
+ let preset = self.bestPreset(for: self.requestedAspectRatio, quality: quality, on: device)
161
+ return captureSession.canSetSessionPreset(preset)
162
+ }
163
+ }
164
+
165
+ func setVideoQuality(_ quality: String) throws {
166
+ guard Self.allVideoQualities.contains(quality.lowercased()) else {
167
+ throw CameraControllerError.invalidOperation
168
+ }
169
+ self.videoQuality = quality.lowercased()
170
+ self.configureSessionPreset(for: self.requestedAspectRatio)
171
+ }
172
+
173
+ func getVideoQuality() -> String {
174
+ return self.videoQuality
175
+ }
176
+
177
+ func getSupportedVideoCodecs() -> [String] {
178
+ guard let fileVideoOutput = self.fileVideoOutput else {
179
+ return ["avc1"]
180
+ }
181
+ var codecs: [String] = []
182
+ if fileVideoOutput.availableVideoCodecTypes.contains(.h264) {
183
+ codecs.append("avc1")
184
+ }
185
+ if fileVideoOutput.availableVideoCodecTypes.contains(.hevc) {
186
+ codecs.append("hvc1")
187
+ }
188
+ return codecs.isEmpty ? ["avc1"] : codecs
189
+ }
190
+
191
+ func setVideoCodec(_ codec: String) throws {
192
+ let normalized = codec.lowercased()
193
+ guard ["avc1", "hvc1"].contains(normalized) else {
194
+ throw CameraControllerError.invalidOperation
195
+ }
196
+ if !getSupportedVideoCodecs().contains(normalized) {
197
+ throw CameraControllerError.invalidOperation
198
+ }
199
+ self.videoCodec = normalized
200
+ }
201
+
202
+ func getVideoCodec() -> String {
203
+ return self.videoCodec
204
+ }
205
+
206
+ private func avVideoCodecType(for codec: String) -> AVVideoCodecType? {
207
+ switch codec.lowercased() {
208
+ case "avc1":
209
+ return .h264
210
+ case "hvc1":
211
+ return .hevc
212
+ default:
213
+ return nil
214
+ }
215
+ }
146
216
 
147
217
  // Track output preparation status
148
218
  private var outputsPrepared: Bool = false
@@ -528,28 +598,40 @@ extension CameraController {
528
598
 
529
599
  // Prioritize video quality setting
530
600
  switch self.videoQuality.lowercased() {
531
- case "low":
532
- // Match Android "Low" (SD/480p)
533
- if captureSession.canSetSessionPreset(.vga640x480) {
534
- targetPreset = .vga640x480
535
- } else {
536
- targetPreset = .low
601
+ case "2160p":
602
+ if captureSession.canSetSessionPreset(.hd4K3840x2160) {
603
+ targetPreset = .hd4K3840x2160
604
+ } else if captureSession.canSetSessionPreset(.hd1920x1080) {
605
+ targetPreset = .hd1920x1080
606
+ }
607
+ case "1080p":
608
+ if captureSession.canSetSessionPreset(.hd1920x1080) {
609
+ targetPreset = .hd1920x1080
610
+ } else if captureSession.canSetSessionPreset(.hd1280x720) {
611
+ targetPreset = .hd1280x720
537
612
  }
538
- case "medium":
539
- // Match Android "Medium" (HD/720p)
613
+ case "720p", "medium":
540
614
  if captureSession.canSetSessionPreset(.hd1280x720) {
541
615
  targetPreset = .hd1280x720
542
616
  } else {
543
617
  targetPreset = .medium
544
618
  }
619
+ case "480p", "low":
620
+ if captureSession.canSetSessionPreset(.vga640x480) {
621
+ targetPreset = .vga640x480
622
+ } else {
623
+ targetPreset = .low
624
+ }
625
+ case "4:3":
626
+ if captureSession.canSetSessionPreset(.photo) {
627
+ targetPreset = .photo
628
+ } else if captureSession.canSetSessionPreset(.high) {
629
+ targetPreset = .high
630
+ }
545
631
  case "high":
546
- // Exisiting logic for High Quality (4K/1080p based on Asepct Ratio)
547
-
548
632
  if let aspectRatio = aspectRatio {
549
633
  switch aspectRatio {
550
634
  case "16:9":
551
- // Start with 1080p for faster initialization, 4K only when explicitly needed
552
- // This maintains capture quality while optimizing preview performance
553
635
  if captureSession.canSetSessionPreset(.hd1920x1080) {
554
636
  targetPreset = .hd1920x1080
555
637
  } else if captureSession.canSetSessionPreset(.hd4K3840x2160) {
@@ -573,7 +655,6 @@ extension CameraController {
573
655
  }
574
656
  }
575
657
  }
576
- // Handle unexpected values
577
658
  default:
578
659
  if captureSession.canSetSessionPreset(.photo) {
579
660
  targetPreset = .photo
@@ -893,28 +974,37 @@ extension CameraController {
893
974
 
894
975
  // Handle specific quality overrides first
895
976
  switch quality.lowercased() {
896
- case "low":
897
- if device.supportsSessionPreset(.vga640x480) { return .vga640x480 }
898
- return .low
899
- case "medium":
977
+ case "2160p":
978
+ if device.supportsSessionPreset(.hd4K3840x2160) { return .hd4K3840x2160 }
979
+ if device.supportsSessionPreset(.hd1920x1080) { return .hd1920x1080 }
980
+ if device.supportsSessionPreset(.hd1280x720) { return .hd1280x720 }
981
+ return .vga640x480
982
+ case "1080p":
983
+ if device.supportsSessionPreset(.hd1920x1080) { return .hd1920x1080 }
984
+ if device.supportsSessionPreset(.hd1280x720) { return .hd1280x720 }
985
+ return .vga640x480
986
+ case "720p", "medium":
900
987
  if device.supportsSessionPreset(.hd1280x720) { return .hd1280x720 }
901
988
  return .medium
902
- case "high":
903
- break // Exit and go off code below
989
+ case "480p", "low":
990
+ if device.supportsSessionPreset(.vga640x480) { return .vga640x480 }
991
+ return .low
992
+ case "4:3":
993
+ if device.supportsSessionPreset(.photo) { return .photo }
994
+ if device.supportsSessionPreset(.high) { return .high }
995
+ return .vga640x480
904
996
  default:
905
- break // Exit and go off code below
997
+ break
906
998
  }
907
999
  // Preference order depends on aspect ratio
908
1000
  if aspectRatio == "16:9" {
909
- // Prefer 4K → 1080p → 720p → high → photo → vga
910
1001
  if device.supportsSessionPreset(.hd4K3840x2160) { return .hd4K3840x2160 }
911
1002
  if device.supportsSessionPreset(.hd1920x1080) { return .hd1920x1080 }
912
1003
  if device.supportsSessionPreset(.hd1280x720) { return .hd1280x720 }
913
1004
  if device.supportsSessionPreset(.high) { return .high }
914
- if device.supportsSessionPreset(.photo) { return .photo } // safe, though 4:3
1005
+ if device.supportsSessionPreset(.photo) { return .photo }
915
1006
  return .vga640x480
916
1007
  } else {
917
- // 4:3 or unknown: prefer photo → high → 1080p → 720p → vga
918
1008
  if device.supportsSessionPreset(.photo) { return .photo }
919
1009
  if device.supportsSessionPreset(.high) { return .high }
920
1010
  if device.supportsSessionPreset(.hd1920x1080) { return .hd1920x1080 }
@@ -2384,7 +2474,7 @@ extension CameraController {
2384
2474
  }
2385
2475
  }
2386
2476
 
2387
- func captureVideo() throws {
2477
+ func captureVideo(maxDuration: Float? = nil, maxFileSize: Int? = nil) throws {
2388
2478
  guard let captureSession = self.captureSession, captureSession.isRunning else {
2389
2479
  throw CameraControllerError.captureSessionIsMissing
2390
2480
  }
@@ -2444,6 +2534,19 @@ extension CameraController {
2444
2534
  let fileUrl = documentsDirectory.appendingPathComponent(fileName)
2445
2535
  try? FileManager.default.removeItem(at: fileUrl)
2446
2536
 
2537
+ if let maxDuration = maxDuration, maxDuration > 0 {
2538
+ fileVideoOutput.maxRecordedDuration = CMTime(seconds: Double(maxDuration), preferredTimescale: 600)
2539
+ } else {
2540
+ fileVideoOutput.maxRecordedDuration = .invalid
2541
+ }
2542
+ fileVideoOutput.maxRecordedFileSize = Int64(maxFileSize ?? 0)
2543
+
2544
+ if let connection = fileVideoOutput.connection(with: .video),
2545
+ let codecType = avVideoCodecType(for: self.videoCodec),
2546
+ fileVideoOutput.availableVideoCodecTypes.contains(codecType) {
2547
+ fileVideoOutput.setOutputSettings([AVVideoCodecKey: codecType], for: connection)
2548
+ }
2549
+
2447
2550
  // Start recording video
2448
2551
  fileVideoOutput.startRecording(to: fileUrl, recordingDelegate: self)
2449
2552
 
@@ -2857,11 +2960,33 @@ extension UIImage {
2857
2960
 
2858
2961
  extension CameraController: AVCaptureFileOutputRecordingDelegate {
2859
2962
  func fileOutput(_ output: AVCaptureFileOutput, didFinishRecordingTo outputFileURL: URL, from connections: [AVCaptureConnection], error: Error?) {
2860
- if let error = error {
2861
- print("Error recording movie: \(error.localizedDescription)")
2862
- } else {
2863
- print("Movie recorded successfully: \(outputFileURL)")
2864
- // You can save the file to the library, upload it, etc.
2963
+ let reason = recordingFinishReason(from: error)
2964
+ if reason == "error" {
2965
+ print("Error recording movie: \(error?.localizedDescription ?? "unknown")")
2966
+ return
2967
+ }
2968
+
2969
+ print("Movie recorded successfully: \(outputFileURL)")
2970
+ self.recordingFinishedCallback?(outputFileURL, reason)
2971
+ }
2972
+
2973
+ private func recordingFinishReason(from error: Error?) -> String {
2974
+ guard let error = error else {
2975
+ return "manual"
2976
+ }
2977
+
2978
+ let nsError = error as NSError
2979
+ guard nsError.domain == AVFoundationErrorDomain else {
2980
+ return "error"
2981
+ }
2982
+
2983
+ switch AVError.Code(rawValue: nsError.code) {
2984
+ case .maximumDurationReached:
2985
+ return "maxDuration"
2986
+ case .maximumFileSizeReached:
2987
+ return "maxFileSize"
2988
+ default:
2989
+ return "error"
2865
2990
  }
2866
2991
  }
2867
2992
  }
@@ -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.4.7"
38
+ private let pluginVersion: String = "8.5.0"
39
39
  public let identifier = "CameraPreviewPlugin"
40
40
  public let jsName = "CameraPreview"
41
41
  public let pluginMethods: [CAPPluginMethod] = [
@@ -51,6 +51,12 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin, CLLocationManagerDelega
51
51
  CAPPluginMethod(name: "setFlashMode", returnType: CAPPluginReturnPromise),
52
52
  CAPPluginMethod(name: "startRecordVideo", returnType: CAPPluginReturnPromise),
53
53
  CAPPluginMethod(name: "stopRecordVideo", returnType: CAPPluginReturnPromise),
54
+ CAPPluginMethod(name: "getSupportedVideoCodecs", returnType: CAPPluginReturnPromise),
55
+ CAPPluginMethod(name: "getVideoCodec", returnType: CAPPluginReturnPromise),
56
+ CAPPluginMethod(name: "setVideoCodec", returnType: CAPPluginReturnPromise),
57
+ CAPPluginMethod(name: "getSupportedVideoQualities", returnType: CAPPluginReturnPromise),
58
+ CAPPluginMethod(name: "getVideoQuality", returnType: CAPPluginReturnPromise),
59
+ CAPPluginMethod(name: "setVideoQuality", returnType: CAPPluginReturnPromise),
54
60
  CAPPluginMethod(name: "getTempFilePath", returnType: CAPPluginReturnPromise),
55
61
  CAPPluginMethod(name: "getSupportedPictureSizes", returnType: CAPPluginReturnPromise),
56
62
  CAPPluginMethod(name: "isRunning", returnType: CAPPluginReturnPromise),
@@ -1595,9 +1601,62 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin, CLLocationManagerDelega
1595
1601
  }
1596
1602
  }
1597
1603
 
1604
+ @objc func setVideoQuality(_ call: CAPPluginCall) {
1605
+ guard let quality = call.getString("quality") else {
1606
+ call.reject("quality is required")
1607
+ return
1608
+ }
1609
+ do {
1610
+ try self.cameraController.setVideoQuality(quality)
1611
+ call.resolve()
1612
+ } catch {
1613
+ call.reject("Failed to set video quality: \(error.localizedDescription)")
1614
+ }
1615
+ }
1616
+
1617
+ @objc func getVideoQuality(_ call: CAPPluginCall) {
1618
+ call.resolve(["quality": self.cameraController.getVideoQuality()])
1619
+ }
1620
+
1621
+ @objc func getSupportedVideoQualities(_ call: CAPPluginCall) {
1622
+ call.resolve(["qualities": self.cameraController.getSupportedVideoQualities()])
1623
+ }
1624
+
1625
+ @objc func setVideoCodec(_ call: CAPPluginCall) {
1626
+ guard let codec = call.getString("codec") else {
1627
+ call.reject("codec is required")
1628
+ return
1629
+ }
1630
+ do {
1631
+ try self.cameraController.setVideoCodec(codec)
1632
+ call.resolve()
1633
+ } catch {
1634
+ call.reject("Failed to set video codec: \(error.localizedDescription)")
1635
+ }
1636
+ }
1637
+
1638
+ @objc func getVideoCodec(_ call: CAPPluginCall) {
1639
+ call.resolve(["codec": self.cameraController.getVideoCodec()])
1640
+ }
1641
+
1642
+ @objc func getSupportedVideoCodecs(_ call: CAPPluginCall) {
1643
+ call.resolve(["codecs": self.cameraController.getSupportedVideoCodecs()])
1644
+ }
1645
+
1598
1646
  @objc func startRecordVideo(_ call: CAPPluginCall) {
1647
+ let maxDuration = call.getFloat("maxDuration")
1648
+ let maxFileSize = call.getInt("maxFileSize")
1649
+ if let videoCodec = call.getString("videoCodec") {
1650
+ try? self.cameraController.setVideoCodec(videoCodec)
1651
+ }
1652
+ self.cameraController.recordingFinishedCallback = { [weak self] fileURL, reason in
1653
+ DispatchQueue.main.async {
1654
+ self?.notifyListeners("recordingFinished", data: ["videoFilePath": fileURL.absoluteString, "reason": reason])
1655
+ }
1656
+ }
1657
+
1599
1658
  do {
1600
- try self.cameraController.captureVideo()
1659
+ try self.cameraController.captureVideo(maxDuration: maxDuration, maxFileSize: maxFileSize)
1601
1660
  call.resolve()
1602
1661
  } catch {
1603
1662
  call.reject(error.localizedDescription)
@@ -1616,7 +1675,7 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin, CLLocationManagerDelega
1616
1675
  return
1617
1676
  }
1618
1677
 
1619
- call.resolve(["videoFilePath": fileURL.absoluteString])
1678
+ call.resolve(["videoFilePath": fileURL.absoluteString, "reason": "manual"])
1620
1679
  }
1621
1680
  }
1622
1681
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@capgo/camera-preview",
3
- "version": "8.4.7",
3
+ "version": "8.5.0",
4
4
  "description": "Camera preview",
5
5
  "license": "MPL-2.0",
6
6
  "repository": {
@@ -57,8 +57,7 @@
57
57
  "prepublishOnly": "bun run build",
58
58
  "check:wiring": "node scripts/check-capacitor-plugin-wiring.mjs",
59
59
  "example:install": "cd example-app && bun install --frozen-lockfile",
60
- "example:build": "bun run build && cd example-app && bun install --frozen-lockfile && bun run build",
61
- "example:capgo:deploy": "bun run example:build && bun scripts/deploy-example-capgo.mjs"
60
+ "example:build": "bun run build && cd example-app && bun install --frozen-lockfile && bun run build"
62
61
  },
63
62
  "devDependencies": {
64
63
  "@capacitor/android": "^8.0.0",