@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.
- package/README.md +110 -37
- package/android/src/main/java/app/capgo/capacitor/camera/preview/CameraPreview.java +69 -16
- package/android/src/main/java/app/capgo/capacitor/camera/preview/CameraXView.java +222 -1
- package/dist/docs.json +102 -3
- package/dist/esm/definitions.d.ts +50 -1
- package/dist/esm/definitions.js.map +1 -1
- package/dist/esm/web.d.ts +9 -0
- package/dist/esm/web.js +10 -0
- package/dist/esm/web.js.map +1 -1
- package/dist/plugin.cjs.js +9 -0
- package/dist/plugin.cjs.js.map +1 -1
- package/dist/plugin.js +9 -0
- package/dist/plugin.js.map +1 -1
- package/ios/Sources/CapgoCameraPreviewPlugin/CameraController.swift +276 -38
- package/ios/Sources/CapgoCameraPreviewPlugin/Plugin.swift +74 -5
- package/package.json +1 -1
|
@@ -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
|
-
|
|
225
|
-
|
|
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
|
-
|
|
263
|
-
|
|
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.
|
|
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.
|
|
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
|
|
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
|
-
|
|
2614
|
-
|
|
2615
|
-
captureSession.
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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":
|
|
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() {
|