capacitor-camera-view 2.0.2 → 2.2.0-rc.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.
Files changed (31) hide show
  1. package/README.md +215 -19
  2. package/android/build.gradle +9 -5
  3. package/android/src/main/AndroidManifest.xml +1 -0
  4. package/android/src/main/java/com/michaelwolz/capacitorcameraview/CameraView.kt +491 -116
  5. package/android/src/main/java/com/michaelwolz/capacitorcameraview/CameraViewPlugin.kt +181 -31
  6. package/android/src/main/java/com/michaelwolz/capacitorcameraview/model/CameraResult.kt +47 -0
  7. package/android/src/main/java/com/michaelwolz/capacitorcameraview/model/CameraSessionConfiguration.kt +11 -1
  8. package/android/src/main/java/com/michaelwolz/capacitorcameraview/model/VideoRecordingQuality.kt +10 -0
  9. package/android/src/main/java/com/michaelwolz/capacitorcameraview/utils.kt +114 -5
  10. package/dist/docs.json +281 -8
  11. package/dist/esm/definitions.d.ts +128 -6
  12. package/dist/esm/definitions.js.map +1 -1
  13. package/dist/esm/web.d.ts +26 -4
  14. package/dist/esm/web.js +218 -18
  15. package/dist/esm/web.js.map +1 -1
  16. package/dist/plugin.cjs.js +219 -18
  17. package/dist/plugin.cjs.js.map +1 -1
  18. package/dist/plugin.js +219 -18
  19. package/dist/plugin.js.map +1 -1
  20. package/ios/Sources/CameraViewPlugin/CameraError.swift +125 -2
  21. package/ios/Sources/CameraViewPlugin/CameraEvents.swift +109 -0
  22. package/ios/Sources/CameraViewPlugin/CameraSessionConfiguration.swift +28 -1
  23. package/ios/Sources/CameraViewPlugin/CameraViewManager+BarcodeScan.swift +30 -41
  24. package/ios/Sources/CameraViewPlugin/CameraViewManager+PhotoCapture.swift +38 -7
  25. package/ios/Sources/CameraViewPlugin/CameraViewManager+VideoDataOutput.swift +4 -3
  26. package/ios/Sources/CameraViewPlugin/CameraViewManager+VideoRecording.swift +302 -0
  27. package/ios/Sources/CameraViewPlugin/CameraViewManager.swift +246 -166
  28. package/ios/Sources/CameraViewPlugin/CameraViewPlugin.swift +194 -96
  29. package/ios/Sources/CameraViewPlugin/TempFileManager.swift +215 -0
  30. package/ios/Sources/CameraViewPlugin/Utils.swift +102 -0
  31. package/package.json +17 -17
@@ -5,30 +5,32 @@ import Foundation
5
5
  /// Please read the Capacitor iOS Plugin Development Guide
6
6
  /// here: https://capacitorjs.com/docs/plugins/ios
7
7
  @objc(CameraViewPlugin)
8
- public class CameraViewPlugin: CAPPlugin, CAPBridgedPlugin {
8
+ public class CameraViewPlugin: CAPPlugin, CAPBridgedPlugin, CameraEventDelegate {
9
9
  public let identifier = "CameraViewPlugin"
10
10
  public let jsName = "CameraView"
11
-
11
+
12
12
  /// Maps string flash mode values to AVCaptureDevice.FlashMode enum values.
13
13
  private let strToFlashModeMap: [String: AVCaptureDevice.FlashMode] = [
14
14
  "off": .off,
15
15
  "on": .on,
16
16
  "auto": .auto
17
17
  ]
18
-
18
+
19
19
  /// Maps AVCaptureDevice.FlashMode enum values to string values.
20
20
  private let flashModeToStrMap: [AVCaptureDevice.FlashMode: String] = [
21
21
  .off: "off",
22
22
  .on: "on",
23
23
  .auto: "auto"
24
24
  ]
25
-
25
+
26
26
  public let pluginMethods: [CAPPluginMethod] = [
27
27
  CAPPluginMethod(name: "start", returnType: CAPPluginReturnPromise),
28
28
  CAPPluginMethod(name: "stop", returnType: CAPPluginReturnPromise),
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),
@@ -42,45 +44,29 @@ public class CameraViewPlugin: CAPPlugin, CAPBridgedPlugin {
42
44
  CAPPluginMethod(name: "checkPermissions", returnType: CAPPluginReturnPromise),
43
45
  CAPPluginMethod(name: "requestPermissions", returnType: CAPPluginReturnPromise)
44
46
  ]
45
-
47
+
46
48
  private let implementation = CameraViewManager()
47
- private var notificationObserver: NSObjectProtocol?
48
-
49
+
49
50
  override public func load() {
50
- // Add observer for barcode detection events
51
- notificationObserver = NotificationCenter.default.addObserver(
52
- forName: Notification.Name("barcodeDetected"),
53
- object: nil,
54
- queue: .main
55
- ) { [weak self] notification in
56
- guard let self = self,
57
- let barcodeData = notification.userInfo as? [String: Any] else {
58
- return
59
- }
60
-
61
- // Emit event to JS
62
- self.notifyListeners("barcodeDetected", data: barcodeData)
63
- }
51
+ implementation.eventEmitter.delegate = self
64
52
  }
65
-
66
- deinit {
67
- if let observer = notificationObserver {
68
- NotificationCenter.default.removeObserver(observer)
69
- }
53
+
54
+ public func cameraDidDetectBarcode(_ event: BarcodeDetectedEvent) {
55
+ notifyListeners("barcodeDetected", data: event.toDictionary())
70
56
  }
71
-
57
+
72
58
  @objc func start(_ call: CAPPluginCall) {
73
59
  guard let webView = self.webView else {
74
60
  call.reject("Cannot find web view")
75
61
  return
76
62
  }
77
-
63
+
78
64
  maybeRequestCameraAccess { [weak self] granted in
79
65
  guard granted else {
80
66
  call.reject("Camera access denied")
81
67
  return
82
68
  }
83
-
69
+
84
70
  self?.implementation.startSession(
85
71
  configuration: sessionConfigFromPluginCall(call),
86
72
  webView: webView,
@@ -93,55 +79,69 @@ public class CameraViewPlugin: CAPPlugin, CAPBridgedPlugin {
93
79
  })
94
80
  }
95
81
  }
96
-
82
+
97
83
  @objc func stop(_ call: CAPPluginCall) {
98
84
  implementation.stopSession {
99
85
  call.resolve()
100
86
  }
101
87
  }
102
-
88
+
103
89
  @objc func isRunning(_ call: CAPPluginCall) {
104
90
  call.resolve([
105
91
  "isRunning": implementation.isRunning()
106
92
  ])
107
93
  }
108
-
94
+
109
95
  @objc func capture(_ call: CAPPluginCall) {
110
96
  let quality = call.getDouble("quality", 90.0)
111
97
  let saveToFile = call.getBool("saveToFile", false)
112
-
98
+
113
99
  guard quality >= 0.0 && quality <= 100.0 else {
114
100
  call.reject("Quality must be between 0 and 100")
115
101
  return
116
102
  }
117
-
118
- implementation.capturePhoto(completion: { (image, error) in
103
+
104
+ // Use optimized Data-based capture to avoid double JPEG encoding
105
+ implementation.capturePhotoData(completion: { [weak self] (data, error) in
119
106
  if let error = error {
120
107
  call.reject("Failed to capture image", nil, error)
121
108
  return
122
109
  }
123
-
124
- guard let image = image else {
110
+
111
+ guard let originalData = data else {
125
112
  call.reject("No image data")
126
113
  return
127
114
  }
128
-
129
- guard let imageData = image.jpegData(compressionQuality: quality / 100.0) else {
130
- call.reject("Failed to compress image")
131
- return
115
+
116
+ // Determine final image data based on quality setting
117
+ // For quality >= 90%, use original camera JPEG data to avoid re-encoding
118
+ // For lower quality, re-encode to reduce file size
119
+ let imageData: Data
120
+ if quality >= 90.0 {
121
+ // Use original JPEG data from camera (avoids quality loss and CPU overhead)
122
+ imageData = originalData
123
+ } else {
124
+ // Re-encode at lower quality for smaller file size
125
+ guard let image = UIImage(data: originalData),
126
+ let compressedData = image.jpegData(compressionQuality: quality / 100.0) else {
127
+ call.reject("Failed to compress image")
128
+ return
129
+ }
130
+ imageData = compressedData
132
131
  }
133
-
132
+
134
133
  if saveToFile {
134
+ // Use TempFileManager for tracked temp files with automatic cleanup
135
+ let tempFileURL = TempFileManager.shared.createTempImageFile()
135
136
  do {
136
- let tempFileURL = try createTempImageFile()
137
137
  try imageData.write(to: tempFileURL)
138
-
138
+
139
139
  // Convert file URL to webView-accessible path using Capacitor bridge
140
- guard let webPath = self.bridge?.portablePath(fromLocalURL: tempFileURL)?.absoluteString else {
140
+ guard let webPath = self?.bridge?.portablePath(fromLocalURL: tempFileURL)?.absoluteString else {
141
141
  call.reject("Failed to create web-accessible path")
142
142
  return
143
143
  }
144
-
144
+
145
145
  call.resolve(["webPath": webPath])
146
146
  } catch {
147
147
  call.reject("Failed to save image to file", nil, error)
@@ -154,43 +154,44 @@ public class CameraViewPlugin: CAPPlugin, CAPBridgedPlugin {
154
154
  }
155
155
  })
156
156
  }
157
-
157
+
158
158
  @objc func captureSample(_ call: CAPPluginCall) {
159
159
  let quality = call.getDouble("quality", 90.0)
160
160
  let saveToFile = call.getBool("saveToFile", false)
161
-
161
+
162
162
  guard quality >= 0.0 && quality <= 100.0 else {
163
163
  call.reject("Quality must be between 0 and 100")
164
164
  return
165
165
  }
166
-
167
- implementation.captureSnapshot { (image, error) in
166
+
167
+ implementation.captureSnapshot { [weak self] (image, error) in
168
168
  if let error = error {
169
169
  call.reject("Failed to capture frame", nil, error)
170
170
  return
171
171
  }
172
-
172
+
173
173
  guard let image = image else {
174
174
  call.reject("No frame data")
175
175
  return
176
176
  }
177
-
177
+
178
178
  guard let imageData = image.jpegData(compressionQuality: quality / 100.0) else {
179
179
  call.reject("Failed to compress image")
180
180
  return
181
181
  }
182
-
182
+
183
183
  if saveToFile {
184
+ // Use TempFileManager for tracked temp files with automatic cleanup
185
+ let tempFileURL = TempFileManager.shared.createTempImageFile()
184
186
  do {
185
- let tempFileURL = try createTempImageFile()
186
187
  try imageData.write(to: tempFileURL)
187
-
188
+
188
189
  // Convert file URL to webView-accessible path using Capacitor bridge
189
- guard let webPath = self.bridge?.portablePath(fromLocalURL: tempFileURL)?.absoluteString else {
190
+ guard let webPath = self?.bridge?.portablePath(fromLocalURL: tempFileURL)?.absoluteString else {
190
191
  call.reject("Failed to create web-accessible path")
191
192
  return
192
193
  }
193
-
194
+
194
195
  call.resolve(["webPath": webPath])
195
196
  } catch {
196
197
  call.reject("Failed to save sample to file", nil, error)
@@ -203,10 +204,67 @@ public class CameraViewPlugin: CAPPlugin, CAPBridgedPlugin {
203
204
  }
204
205
  }
205
206
  }
207
+
208
+ @objc func startRecording(_ call: CAPPluginCall) {
209
+ let enableAudio = call.getBool("enableAudio") ?? false
210
+ let videoQuality = call.getString("videoQuality") ?? "highest"
206
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
+
207
265
  @objc func getAvailableDevices(_ call: CAPPluginCall) {
208
266
  let devices = implementation.getAvailableDevices()
209
-
267
+
210
268
  var result = JSArray()
211
269
  for device in devices {
212
270
  var deviceInfo = JSObject()
@@ -216,12 +274,12 @@ public class CameraViewPlugin: CAPPlugin, CAPBridgedPlugin {
216
274
  deviceInfo["deviceType"] = convertToStringCameraType(device.deviceType)
217
275
  result.append(deviceInfo)
218
276
  }
219
-
277
+
220
278
  call.resolve([
221
279
  "devices": result
222
280
  ])
223
281
  }
224
-
282
+
225
283
  @objc func flipCamera(_ call: CAPPluginCall) {
226
284
  do {
227
285
  try implementation.flipCamera()
@@ -231,25 +289,25 @@ public class CameraViewPlugin: CAPPlugin, CAPBridgedPlugin {
231
289
  return
232
290
  }
233
291
  }
234
-
292
+
235
293
  @objc func getZoom(_ call: CAPPluginCall) {
236
294
  let zoom = implementation.getSupportedZoomFactors()
237
-
295
+
238
296
  call.resolve([
239
297
  "min": zoom.min,
240
298
  "max": zoom.max,
241
299
  "current": zoom.current
242
300
  ])
243
301
  }
244
-
302
+
245
303
  @objc func setZoom(_ call: CAPPluginCall) {
246
304
  guard let level = call.getDouble("level") else {
247
305
  call.reject("Zoom level must be provided")
248
306
  return
249
307
  }
250
-
308
+
251
309
  let ramp = call.getBool("ramp") ?? false
252
-
310
+
253
311
  do {
254
312
  try implementation.setZoomFactor(level, ramp: ramp)
255
313
  call.resolve()
@@ -258,35 +316,35 @@ public class CameraViewPlugin: CAPPlugin, CAPBridgedPlugin {
258
316
  return
259
317
  }
260
318
  }
261
-
319
+
262
320
  @objc func getFlashMode(_ call: CAPPluginCall) {
263
321
  let flashMode = implementation.getFlashMode()
264
-
322
+
265
323
  call.resolve([
266
324
  "flashMode": flashModeToStrMap[flashMode] ?? "off"
267
325
  ])
268
326
  }
269
-
327
+
270
328
  @objc func getSupportedFlashModes(_ call: CAPPluginCall) {
271
329
  let supportedFlashModes = implementation.getSupportedFlashModes()
272
330
  let supportedFlashModeStrArr = supportedFlashModes.map { flashModeToStrMap[$0] }
273
-
331
+
274
332
  call.resolve([
275
333
  "flashModes": supportedFlashModeStrArr
276
334
  ])
277
335
  }
278
-
336
+
279
337
  @objc func setFlashMode(_ call: CAPPluginCall) {
280
338
  guard let mode = call.getString("mode") else {
281
339
  call.reject("Flash mode must be provided")
282
340
  return
283
341
  }
284
-
342
+
285
343
  guard let flashMode = strToFlashModeMap[mode] else {
286
344
  call.reject("Invalid flash mode")
287
345
  return
288
346
  }
289
-
347
+
290
348
  do {
291
349
  try implementation.setFlashMode(flashMode)
292
350
  call.resolve()
@@ -294,14 +352,14 @@ public class CameraViewPlugin: CAPPlugin, CAPBridgedPlugin {
294
352
  call.reject("Failed to set flash mode", nil, error)
295
353
  }
296
354
  }
297
-
355
+
298
356
  @objc func isTorchAvailable(_ call: CAPPluginCall) {
299
357
  let available = implementation.isTorchAvailable()
300
358
  call.resolve([
301
359
  "available": available
302
360
  ])
303
361
  }
304
-
362
+
305
363
  @objc func getTorchMode(_ call: CAPPluginCall) {
306
364
  let torchState = implementation.getTorchMode()
307
365
  call.resolve([
@@ -309,20 +367,20 @@ public class CameraViewPlugin: CAPPlugin, CAPBridgedPlugin {
309
367
  "level": torchState.level
310
368
  ])
311
369
  }
312
-
370
+
313
371
  @objc func setTorchMode(_ call: CAPPluginCall) {
314
372
  guard let enabled = call.getBool("enabled") else {
315
373
  call.reject("Enabled parameter is required")
316
374
  return
317
375
  }
318
-
376
+
319
377
  let level = call.getFloat("level") ?? 1.0
320
-
378
+
321
379
  guard level >= 0.0 && level <= 1.0 else {
322
380
  call.reject("Level must be between 0.0 and 1.0")
323
381
  return
324
382
  }
325
-
383
+
326
384
  do {
327
385
  try implementation.setTorchMode(enabled: enabled, level: level)
328
386
  call.resolve()
@@ -330,32 +388,57 @@ public class CameraViewPlugin: CAPPlugin, CAPBridgedPlugin {
330
388
  call.reject("Failed to set torch mode", nil, error)
331
389
  }
332
390
  }
333
-
391
+
334
392
  @objc override public func checkPermissions(_ call: CAPPluginCall) {
335
- let cameraState: String
336
-
337
- switch AVCaptureDevice.authorizationStatus(for: .video) {
338
- case .notDetermined:
339
- cameraState = "prompt"
340
- case .restricted, .denied:
341
- cameraState = "denied"
342
- case .authorized:
343
- cameraState = "granted"
344
- @unknown default:
345
- cameraState = "prompt"
346
- }
347
-
348
393
  call.resolve([
349
- "camera": cameraState
394
+ "camera": authorizationStateString(for: .video),
395
+ "microphone": authorizationStateString(for: .audio)
350
396
  ])
351
397
  }
352
-
398
+
353
399
  @objc override public func requestPermissions(_ call: CAPPluginCall) {
354
- 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
355
406
  self?.checkPermissions(call)
356
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
+ }
357
426
  }
358
-
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
+ }
440
+ }
441
+
359
442
  private func maybeRequestCameraAccess(completion: @escaping (Bool) -> Void) {
360
443
  let status = AVCaptureDevice.authorizationStatus(for: .video)
361
444
  if status == .authorized {
@@ -370,4 +453,19 @@ public class CameraViewPlugin: CAPPlugin, CAPBridgedPlugin {
370
453
  completion(false)
371
454
  }
372
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
+ }
373
471
  }