@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.
- package/README.md +157 -6
- package/android/src/main/java/app/capgo/capacitor/camera/preview/CameraPreview.java +131 -3
- package/android/src/main/java/app/capgo/capacitor/camera/preview/CameraXView.java +149 -15
- package/android/src/main/java/app/capgo/capacitor/camera/preview/model/CameraSessionConfiguration.java +14 -1
- package/dist/docs.json +366 -7
- package/dist/esm/definitions.d.ts +108 -7
- package/dist/esm/definitions.js.map +1 -1
- package/dist/esm/web.d.ts +15 -1
- package/dist/esm/web.js +19 -1
- package/dist/esm/web.js.map +1 -1
- package/dist/plugin.cjs.js +19 -1
- package/dist/plugin.cjs.js.map +1 -1
- package/dist/plugin.js +19 -1
- package/dist/plugin.js.map +1 -1
- package/ios/Sources/CapgoCameraPreviewPlugin/CameraController.swift +154 -29
- package/ios/Sources/CapgoCameraPreviewPlugin/Plugin.swift +62 -3
- package/package.json +2 -3
|
@@ -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 "
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
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 "
|
|
897
|
-
if device.supportsSessionPreset(.
|
|
898
|
-
return .
|
|
899
|
-
|
|
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 "
|
|
903
|
-
|
|
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
|
|
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 }
|
|
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
|
-
|
|
2861
|
-
|
|
2862
|
-
|
|
2863
|
-
|
|
2864
|
-
|
|
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.
|
|
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.
|
|
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",
|