capacitor-camera-view 2.2.0 → 2.3.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.
@@ -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": cameraState
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
- AVCaptureDevice.requestAccess(for: .video) { [weak self] _ in
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "capacitor-camera-view",
3
- "version": "2.2.0",
3
+ "version": "2.3.0",
4
4
  "description": "A Capacitor plugin for embedding a live camera feed directly into your app.",
5
5
  "main": "dist/plugin.cjs.js",
6
6
  "module": "dist/esm/index.js",