capacitor-camera-view 2.2.0 → 2.3.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.
- package/README.md +196 -10
- package/android/build.gradle +1 -0
- package/android/src/main/AndroidManifest.xml +1 -0
- package/android/src/main/java/com/michaelwolz/capacitorcameraview/CameraView.kt +310 -3
- package/android/src/main/java/com/michaelwolz/capacitorcameraview/CameraViewPlugin.kt +112 -2
- package/android/src/main/java/com/michaelwolz/capacitorcameraview/model/VideoRecordingQuality.kt +10 -0
- package/android/src/main/java/com/michaelwolz/capacitorcameraview/utils.kt +21 -1
- package/dist/docs.json +200 -8
- package/dist/esm/definitions.d.ts +84 -6
- package/dist/esm/definitions.js.map +1 -1
- package/dist/esm/web.d.ts +20 -4
- package/dist/esm/web.js +157 -16
- package/dist/esm/web.js.map +1 -1
- package/dist/plugin.cjs.js +157 -16
- package/dist/plugin.cjs.js.map +1 -1
- package/dist/plugin.js +157 -16
- package/dist/plugin.js.map +1 -1
- package/ios/Sources/CameraViewPlugin/CameraError.swift +28 -0
- package/ios/Sources/CameraViewPlugin/CameraSessionConfiguration.swift +8 -8
- package/ios/Sources/CameraViewPlugin/CameraViewManager+PhotoCapture.swift +13 -14
- package/ios/Sources/CameraViewPlugin/CameraViewManager+VideoRecording.swift +302 -0
- package/ios/Sources/CameraViewPlugin/CameraViewManager.swift +159 -150
- package/ios/Sources/CameraViewPlugin/CameraViewPlugin.swift +114 -15
- package/ios/Sources/CameraViewPlugin/TempFileManager.swift +68 -34
- package/package.json +1 -1
|
@@ -29,6 +29,8 @@ public class CameraViewPlugin: CAPPlugin, CAPBridgedPlugin, CameraEventDelegate
|
|
|
29
29
|
CAPPluginMethod(name: "isRunning", returnType: CAPPluginReturnPromise),
|
|
30
30
|
CAPPluginMethod(name: "capture", returnType: CAPPluginReturnPromise),
|
|
31
31
|
CAPPluginMethod(name: "captureSample", returnType: CAPPluginReturnPromise),
|
|
32
|
+
CAPPluginMethod(name: "startRecording", returnType: CAPPluginReturnPromise),
|
|
33
|
+
CAPPluginMethod(name: "stopRecording", returnType: CAPPluginReturnPromise),
|
|
32
34
|
CAPPluginMethod(name: "getAvailableDevices", returnType: CAPPluginReturnPromise),
|
|
33
35
|
CAPPluginMethod(name: "flipCamera", returnType: CAPPluginReturnPromise),
|
|
34
36
|
CAPPluginMethod(name: "getZoom", returnType: CAPPluginReturnPromise),
|
|
@@ -203,6 +205,63 @@ public class CameraViewPlugin: CAPPlugin, CAPBridgedPlugin, CameraEventDelegate
|
|
|
203
205
|
}
|
|
204
206
|
}
|
|
205
207
|
|
|
208
|
+
@objc func startRecording(_ call: CAPPluginCall) {
|
|
209
|
+
let enableAudio = call.getBool("enableAudio") ?? false
|
|
210
|
+
let videoQuality = call.getString("videoQuality") ?? "highest"
|
|
211
|
+
|
|
212
|
+
guard let parsedVideoQuality = VideoRecordingQuality(rawValue: videoQuality) else {
|
|
213
|
+
call.reject("Invalid videoQuality. Use one of: lowest, sd, hd, fhd, uhd, highest")
|
|
214
|
+
return
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if enableAudio {
|
|
218
|
+
maybeRequestMicrophoneAccess { [weak self] granted in
|
|
219
|
+
guard granted else {
|
|
220
|
+
call.reject("Microphone access denied")
|
|
221
|
+
return
|
|
222
|
+
}
|
|
223
|
+
self?.doStartRecording(call: call, enableAudio: true, videoQuality: parsedVideoQuality)
|
|
224
|
+
}
|
|
225
|
+
} else {
|
|
226
|
+
doStartRecording(call: call, enableAudio: false, videoQuality: parsedVideoQuality)
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
private func doStartRecording(
|
|
231
|
+
call: CAPPluginCall,
|
|
232
|
+
enableAudio: Bool,
|
|
233
|
+
videoQuality: VideoRecordingQuality
|
|
234
|
+
) {
|
|
235
|
+
implementation.startRecording(enableAudio: enableAudio, videoQuality: videoQuality) { error in
|
|
236
|
+
if let error = error {
|
|
237
|
+
call.reject("Failed to start recording", nil, error)
|
|
238
|
+
return
|
|
239
|
+
}
|
|
240
|
+
call.resolve()
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
@objc func stopRecording(_ call: CAPPluginCall) {
|
|
245
|
+
implementation.stopRecording { [weak self] (outputURL, error) in
|
|
246
|
+
if let error = error {
|
|
247
|
+
call.reject("Failed to stop recording", nil, error)
|
|
248
|
+
return
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
guard let outputURL = outputURL else {
|
|
252
|
+
call.reject("No output file URL")
|
|
253
|
+
return
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
guard let webPath = self?.bridge?.portablePath(fromLocalURL: outputURL)?.absoluteString else {
|
|
257
|
+
call.reject("Failed to create web-accessible path")
|
|
258
|
+
return
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
call.resolve(["webPath": webPath])
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
206
265
|
@objc func getAvailableDevices(_ call: CAPPluginCall) {
|
|
207
266
|
let devices = implementation.getAvailableDevices()
|
|
208
267
|
|
|
@@ -331,28 +390,53 @@ public class CameraViewPlugin: CAPPlugin, CAPBridgedPlugin, CameraEventDelegate
|
|
|
331
390
|
}
|
|
332
391
|
|
|
333
392
|
@objc override public func checkPermissions(_ call: CAPPluginCall) {
|
|
334
|
-
let cameraState: String
|
|
335
|
-
|
|
336
|
-
switch AVCaptureDevice.authorizationStatus(for: .video) {
|
|
337
|
-
case .notDetermined:
|
|
338
|
-
cameraState = "prompt"
|
|
339
|
-
case .restricted, .denied:
|
|
340
|
-
cameraState = "denied"
|
|
341
|
-
case .authorized:
|
|
342
|
-
cameraState = "granted"
|
|
343
|
-
@unknown default:
|
|
344
|
-
cameraState = "prompt"
|
|
345
|
-
}
|
|
346
|
-
|
|
347
393
|
call.resolve([
|
|
348
|
-
"camera":
|
|
394
|
+
"camera": authorizationStateString(for: .video),
|
|
395
|
+
"microphone": authorizationStateString(for: .audio)
|
|
349
396
|
])
|
|
350
397
|
}
|
|
351
398
|
|
|
352
399
|
@objc override public func requestPermissions(_ call: CAPPluginCall) {
|
|
353
|
-
|
|
400
|
+
let permissionsList = call.getArray("permissions", String.self) ?? ["camera"]
|
|
401
|
+
|
|
402
|
+
let requestCamera = permissionsList.contains("camera")
|
|
403
|
+
let requestMicrophone = permissionsList.contains("microphone")
|
|
404
|
+
|
|
405
|
+
let completionHandler: () -> Void = { [weak self] in
|
|
354
406
|
self?.checkPermissions(call)
|
|
355
407
|
}
|
|
408
|
+
|
|
409
|
+
if requestCamera {
|
|
410
|
+
AVCaptureDevice.requestAccess(for: .video) { _ in
|
|
411
|
+
if requestMicrophone {
|
|
412
|
+
AVCaptureDevice.requestAccess(for: .audio) { _ in
|
|
413
|
+
completionHandler()
|
|
414
|
+
}
|
|
415
|
+
} else {
|
|
416
|
+
completionHandler()
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
} else if requestMicrophone {
|
|
420
|
+
AVCaptureDevice.requestAccess(for: .audio) { _ in
|
|
421
|
+
completionHandler()
|
|
422
|
+
}
|
|
423
|
+
} else {
|
|
424
|
+
completionHandler()
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/// Maps AVFoundation authorization status to the Capacitor permission state string.
|
|
429
|
+
private func authorizationStateString(for mediaType: AVMediaType) -> String {
|
|
430
|
+
switch AVCaptureDevice.authorizationStatus(for: mediaType) {
|
|
431
|
+
case .notDetermined:
|
|
432
|
+
return "prompt"
|
|
433
|
+
case .restricted, .denied:
|
|
434
|
+
return "denied"
|
|
435
|
+
case .authorized:
|
|
436
|
+
return "granted"
|
|
437
|
+
@unknown default:
|
|
438
|
+
return "prompt"
|
|
439
|
+
}
|
|
356
440
|
}
|
|
357
441
|
|
|
358
442
|
private func maybeRequestCameraAccess(completion: @escaping (Bool) -> Void) {
|
|
@@ -369,4 +453,19 @@ public class CameraViewPlugin: CAPPlugin, CAPBridgedPlugin, CameraEventDelegate
|
|
|
369
453
|
completion(false)
|
|
370
454
|
}
|
|
371
455
|
}
|
|
456
|
+
|
|
457
|
+
private func maybeRequestMicrophoneAccess(completion: @escaping (Bool) -> Void) {
|
|
458
|
+
let status = AVCaptureDevice.authorizationStatus(for: .audio)
|
|
459
|
+
if status == .authorized {
|
|
460
|
+
completion(true)
|
|
461
|
+
} else if status == .notDetermined {
|
|
462
|
+
AVCaptureDevice.requestAccess(for: .audio) { granted in
|
|
463
|
+
DispatchQueue.main.async {
|
|
464
|
+
completion(granted)
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
} else {
|
|
468
|
+
completion(false)
|
|
469
|
+
}
|
|
470
|
+
}
|
|
372
471
|
}
|
|
@@ -11,29 +11,29 @@ import UIKit
|
|
|
11
11
|
public final class TempFileManager: @unchecked Sendable {
|
|
12
12
|
/// Shared singleton instance
|
|
13
13
|
public static let shared = TempFileManager()
|
|
14
|
-
|
|
14
|
+
|
|
15
15
|
/// Serial queue for thread-safe file tracking
|
|
16
16
|
private let queue = DispatchQueue(label: "com.michaelwolz.capacitorcameraview.tempFileManager")
|
|
17
|
-
|
|
17
|
+
|
|
18
18
|
/// Set of tracked temporary file URLs
|
|
19
19
|
private var trackedFiles = Set<URL>()
|
|
20
|
-
|
|
20
|
+
|
|
21
21
|
/// Directory prefix used to identify camera capture temp files
|
|
22
22
|
private let tempFilePrefix = "camera_capture_"
|
|
23
|
-
|
|
23
|
+
|
|
24
24
|
/// Stale file threshold in seconds (30 minutes)
|
|
25
25
|
private let staleThresholdSeconds: TimeInterval = 1800
|
|
26
|
-
|
|
26
|
+
|
|
27
27
|
private init() {
|
|
28
28
|
setupAppLifecycleObservers()
|
|
29
29
|
}
|
|
30
|
-
|
|
30
|
+
|
|
31
31
|
deinit {
|
|
32
32
|
NotificationCenter.default.removeObserver(self)
|
|
33
33
|
}
|
|
34
|
-
|
|
34
|
+
|
|
35
35
|
// MARK: - Public API
|
|
36
|
-
|
|
36
|
+
|
|
37
37
|
/// Creates a temporary file URL for storing captured images and tracks it for cleanup.
|
|
38
38
|
///
|
|
39
39
|
/// - Returns: A URL pointing to the temporary file location
|
|
@@ -43,50 +43,84 @@ public final class TempFileManager: @unchecked Sendable {
|
|
|
43
43
|
let fileName = "\(tempFilePrefix)\(timestamp).jpg"
|
|
44
44
|
let tempDir = FileManager.default.temporaryDirectory
|
|
45
45
|
let fileURL = tempDir.appendingPathComponent(fileName)
|
|
46
|
-
|
|
46
|
+
|
|
47
47
|
queue.sync {
|
|
48
|
-
trackedFiles.insert(fileURL)
|
|
48
|
+
_ = trackedFiles.insert(fileURL)
|
|
49
49
|
}
|
|
50
|
-
|
|
50
|
+
|
|
51
51
|
return fileURL
|
|
52
52
|
}
|
|
53
|
-
|
|
53
|
+
|
|
54
|
+
/// Creates a temporary file URL for storing recorded videos and tracks it for cleanup.
|
|
55
|
+
///
|
|
56
|
+
/// - Returns: A URL pointing to the temporary video file location
|
|
57
|
+
public func createTempVideoFile() -> URL {
|
|
58
|
+
let timestamp = Int(Date().timeIntervalSince1970 * 1000)
|
|
59
|
+
let fileName = "\(tempFilePrefix)\(timestamp).mp4"
|
|
60
|
+
let tempDir = FileManager.default.temporaryDirectory
|
|
61
|
+
let fileURL = tempDir.appendingPathComponent(fileName)
|
|
62
|
+
|
|
63
|
+
queue.sync {
|
|
64
|
+
_ = trackedFiles.insert(fileURL)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return fileURL
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/// Registers an externally created file for tracking.
|
|
71
|
+
///
|
|
72
|
+
/// - Parameter url: The URL of the file to track
|
|
73
|
+
public func trackFile(_ url: URL) {
|
|
74
|
+
queue.sync {
|
|
75
|
+
_ = trackedFiles.insert(url)
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/// Removes a file from tracking without deleting it.
|
|
80
|
+
///
|
|
81
|
+
/// - Parameter url: The URL of the file to untrack
|
|
82
|
+
public func untrackFile(_ url: URL) {
|
|
83
|
+
queue.sync {
|
|
84
|
+
_ = trackedFiles.remove(url)
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
54
88
|
/// Cleans up all tracked temporary files.
|
|
55
89
|
/// Call this when the camera session stops.
|
|
56
90
|
public func cleanupSessionFiles() {
|
|
57
91
|
queue.async { [weak self] in
|
|
58
92
|
guard let self = self else { return }
|
|
59
|
-
|
|
93
|
+
|
|
60
94
|
let filesToDelete = self.trackedFiles
|
|
61
95
|
self.trackedFiles.removeAll()
|
|
62
|
-
|
|
96
|
+
|
|
63
97
|
for fileURL in filesToDelete {
|
|
64
98
|
self.deleteFile(at: fileURL)
|
|
65
99
|
}
|
|
66
100
|
}
|
|
67
101
|
}
|
|
68
|
-
|
|
102
|
+
|
|
69
103
|
/// Cleans up stale temporary files that are older than the threshold.
|
|
70
104
|
/// This helps recover from cases where cleanup was missed.
|
|
71
105
|
public func cleanupStaleFiles() {
|
|
72
106
|
queue.async { [weak self] in
|
|
73
107
|
guard let self = self else { return }
|
|
74
|
-
|
|
108
|
+
|
|
75
109
|
let fileManager = FileManager.default
|
|
76
110
|
let tempDir = fileManager.temporaryDirectory
|
|
77
|
-
|
|
111
|
+
|
|
78
112
|
guard let contents = try? fileManager.contentsOfDirectory(
|
|
79
113
|
at: tempDir,
|
|
80
114
|
includingPropertiesForKeys: [.creationDateKey],
|
|
81
115
|
options: .skipsHiddenFiles
|
|
82
116
|
) else { return }
|
|
83
|
-
|
|
117
|
+
|
|
84
118
|
let staleDate = Date().addingTimeInterval(-self.staleThresholdSeconds)
|
|
85
|
-
|
|
119
|
+
|
|
86
120
|
for fileURL in contents {
|
|
87
121
|
// Only process our camera capture files
|
|
88
122
|
guard fileURL.lastPathComponent.hasPrefix(self.tempFilePrefix) else { continue }
|
|
89
|
-
|
|
123
|
+
|
|
90
124
|
// Check file age
|
|
91
125
|
if let attributes = try? fileManager.attributesOfItem(atPath: fileURL.path),
|
|
92
126
|
let creationDate = attributes[.creationDate] as? Date,
|
|
@@ -97,34 +131,34 @@ public final class TempFileManager: @unchecked Sendable {
|
|
|
97
131
|
}
|
|
98
132
|
}
|
|
99
133
|
}
|
|
100
|
-
|
|
134
|
+
|
|
101
135
|
/// Cleans up all camera capture temporary files in the temp directory.
|
|
102
136
|
/// Use this for aggressive cleanup on app termination.
|
|
103
137
|
public func cleanupAllCaptureFiles() {
|
|
104
138
|
queue.async { [weak self] in
|
|
105
139
|
guard let self = self else { return }
|
|
106
|
-
|
|
140
|
+
|
|
107
141
|
let fileManager = FileManager.default
|
|
108
142
|
let tempDir = fileManager.temporaryDirectory
|
|
109
|
-
|
|
143
|
+
|
|
110
144
|
guard let contents = try? fileManager.contentsOfDirectory(
|
|
111
145
|
at: tempDir,
|
|
112
146
|
includingPropertiesForKeys: nil,
|
|
113
147
|
options: .skipsHiddenFiles
|
|
114
148
|
) else { return }
|
|
115
|
-
|
|
149
|
+
|
|
116
150
|
for fileURL in contents {
|
|
117
151
|
if fileURL.lastPathComponent.hasPrefix(self.tempFilePrefix) {
|
|
118
152
|
self.deleteFile(at: fileURL)
|
|
119
153
|
}
|
|
120
154
|
}
|
|
121
|
-
|
|
155
|
+
|
|
122
156
|
self.trackedFiles.removeAll()
|
|
123
157
|
}
|
|
124
158
|
}
|
|
125
|
-
|
|
159
|
+
|
|
126
160
|
// MARK: - Private Methods
|
|
127
|
-
|
|
161
|
+
|
|
128
162
|
private func deleteFile(at url: URL) {
|
|
129
163
|
do {
|
|
130
164
|
try FileManager.default.removeItem(at: url)
|
|
@@ -132,7 +166,7 @@ public final class TempFileManager: @unchecked Sendable {
|
|
|
132
166
|
// Silently ignore - file may already be deleted or inaccessible
|
|
133
167
|
}
|
|
134
168
|
}
|
|
135
|
-
|
|
169
|
+
|
|
136
170
|
private func setupAppLifecycleObservers() {
|
|
137
171
|
NotificationCenter.default.addObserver(
|
|
138
172
|
self,
|
|
@@ -140,7 +174,7 @@ public final class TempFileManager: @unchecked Sendable {
|
|
|
140
174
|
name: UIApplication.didBecomeActiveNotification,
|
|
141
175
|
object: nil
|
|
142
176
|
)
|
|
143
|
-
|
|
177
|
+
|
|
144
178
|
NotificationCenter.default.addObserver(
|
|
145
179
|
self,
|
|
146
180
|
selector: #selector(handleAppWillTerminate),
|
|
@@ -148,21 +182,21 @@ public final class TempFileManager: @unchecked Sendable {
|
|
|
148
182
|
object: nil
|
|
149
183
|
)
|
|
150
184
|
}
|
|
151
|
-
|
|
185
|
+
|
|
152
186
|
@objc private func handleAppDidBecomeActive() {
|
|
153
187
|
// Clean up any stale files when app becomes active
|
|
154
188
|
cleanupStaleFiles()
|
|
155
189
|
}
|
|
156
|
-
|
|
190
|
+
|
|
157
191
|
@objc private func handleAppWillTerminate() {
|
|
158
192
|
// Clean up all capture files on termination
|
|
159
193
|
// Note: This is synchronous to ensure cleanup happens before termination
|
|
160
194
|
queue.sync { [weak self] in
|
|
161
195
|
guard let self = self else { return }
|
|
162
|
-
|
|
196
|
+
|
|
163
197
|
let fileManager = FileManager.default
|
|
164
198
|
let tempDir = fileManager.temporaryDirectory
|
|
165
|
-
|
|
199
|
+
|
|
166
200
|
if let contents = try? fileManager.contentsOfDirectory(
|
|
167
201
|
at: tempDir,
|
|
168
202
|
includingPropertiesForKeys: nil,
|
|
@@ -174,7 +208,7 @@ public final class TempFileManager: @unchecked Sendable {
|
|
|
174
208
|
}
|
|
175
209
|
}
|
|
176
210
|
}
|
|
177
|
-
|
|
211
|
+
|
|
178
212
|
self.trackedFiles.removeAll()
|
|
179
213
|
}
|
|
180
214
|
}
|
package/package.json
CHANGED