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
@@ -1,5 +1,7 @@
1
1
  import AVFoundation
2
+ import CoreImage
2
3
  import Foundation
4
+ import Metal
3
5
  import UIKit
4
6
  import WebKit
5
7
 
@@ -11,50 +13,97 @@ internal let SUPPORTED_CAMERA_DEVICE_TYPES: [AVCaptureDevice.DeviceType] = [
11
13
  .builtInDualCamera,
12
14
  .builtInDualWideCamera,
13
15
  .builtInTripleCamera,
14
- .builtInTrueDepthCamera,
16
+ .builtInTrueDepthCamera
15
17
  ]
16
18
 
17
19
  /// A camera implementation that handles camera session management and photo capture.
18
20
  @objc public class CameraViewManager: NSObject {
21
+ // MARK: - Shared Resources
22
+
23
+ /// Metal-backed CIContext singleton for efficient image processing.
24
+ /// Creating CIContext per frame is extremely expensive (80%+ CPU waste).
25
+ /// This shared instance uses Metal for GPU acceleration when available.
26
+ internal static let sharedCIContext: CIContext = {
27
+ if let metalDevice = MTLCreateSystemDefaultDevice() {
28
+ return CIContext(mtlDevice: metalDevice, options: [.cacheIntermediates: false])
29
+ }
30
+ return CIContext(options: [.useSoftwareRenderer: true])
31
+ }()
32
+
33
+ // MARK: - Capture Session Components
34
+
19
35
  internal let captureSession = AVCaptureSession()
20
36
  internal let avPhotoOutput = AVCapturePhotoOutput()
21
37
  internal let avVideoDataOutput = AVCaptureVideoDataOutput()
22
38
  internal let videoPreviewLayer = AVCaptureVideoPreviewLayer()
23
-
39
+
40
+ /// Dedicated queue for all capture session operations.
41
+ /// Using a consistent queue prevents race conditions and ensures thread safety.
42
+ internal let sessionQueue = DispatchQueue(
43
+ label: "com.michaelwolz.capacitorcameraview.session",
44
+ qos: .userInitiated
45
+ )
46
+
47
+ /// Reusable queue for sample buffer processing during snapshot capture.
48
+ /// Creating a new queue per snapshot causes memory allocation churn.
49
+ internal let sampleBufferQueue = DispatchQueue(
50
+ label: "com.michaelwolz.capacitorcameraview.sampleBuffer",
51
+ qos: .userInitiated
52
+ )
53
+
24
54
  /// The currently active camera device.
25
- private var currentCameraDevice: AVCaptureDevice?
26
-
55
+ internal var currentCameraDevice: AVCaptureDevice?
56
+
27
57
  /// List of preferred camera devices, this overrides the SUPPORTED_CAMERA_DEVICE_TYPES for the capture session
28
58
  private var preferredCameraDeviceTypes = SUPPORTED_CAMERA_DEVICE_TYPES
29
-
59
+
30
60
  /// Currently selected flash mode.
31
61
  private var flashMode: AVCaptureDevice.FlashMode = .auto
32
-
62
+
33
63
  /// Reference to the blur overlay view that is shown when switching to the triple camera in order to have a smooth transition
34
64
  private var blurOverlayView: UIVisualEffectView?
35
-
65
+
36
66
  /// Reference to the webView that is used by the Capacitor plugin for the preview layer is shown on
37
67
  private var webView: UIView?
38
-
39
- /// Callback for when photo capture completes.
68
+
69
+ /// Callback for when photo capture completes (legacy UIImage-based API).
40
70
  internal var photoCaptureHandler: ((UIImage?, Error?) -> Void)?
41
-
71
+
72
+ /// Callback for when photo capture completes with raw Data (optimized API).
73
+ /// This avoids double JPEG encoding by returning the camera's JPEG data directly.
74
+ internal var photoDataCaptureHandler: ((Data?, Error?) -> Void)?
75
+
42
76
  /// Callback for when snapshot capture completes.
43
77
  internal var snapshotCompletionHandler: ((UIImage?, Error?) -> Void)?
44
-
78
+
79
+ /// Emits typed camera events to the delegate and NotificationCenter.
80
+ internal let eventEmitter = CameraEventEmitter()
81
+
82
+ /// Movie file output for video recording.
83
+ internal let avMovieOutput = AVCaptureMovieFileOutput()
84
+
85
+ /// Callback invoked when video recording completes with the output URL or an error.
86
+ internal var videoRecordingCompletionHandler: ((URL?, Error?) -> Void)?
87
+
88
+ /// Whether audio was added to the session for the current recording.
89
+ internal var recordingWithAudio = false
90
+
91
+ /// Session preset used before starting recording, restored when recording ends.
92
+ internal var sessionPresetBeforeRecording: AVCaptureSession.Preset?
93
+
45
94
  override public init() {
46
95
  super.init()
47
96
  setupOrientationObserver()
48
97
  setupAppLifecycleObservers()
49
98
  }
50
-
99
+
51
100
  deinit {
52
101
  stopSession()
53
102
  NotificationCenter.default.removeObserver(self)
54
103
  }
55
-
104
+
56
105
  // MARK: - Plugin API
57
-
106
+
58
107
  /// Starts capture session for the specified camera position.
59
108
  /// This will reuse the existing capture session if it is already running.
60
109
  ///
@@ -67,16 +116,15 @@ internal let SUPPORTED_CAMERA_DEVICE_TYPES: [AVCaptureDevice.DeviceType] = [
67
116
  completion: @escaping (Error?) -> Void
68
117
  ) {
69
118
  if let preferredCameraDeviceTypes = configuration
70
- .preferredCameraDeviceTypes
71
- {
119
+ .preferredCameraDeviceTypes {
72
120
  self.preferredCameraDeviceTypes = convertToNativeCameraTypes(
73
121
  preferredCameraDeviceTypes
74
122
  )
75
123
  }
76
-
77
- DispatchQueue.global(qos: .userInitiated).async { [weak self] in
124
+
125
+ sessionQueue.async { [weak self] in
78
126
  guard let self = self else { return }
79
-
127
+
80
128
  do {
81
129
  try self.initiateCaptureSession(configuration: configuration)
82
130
  } catch {
@@ -85,29 +133,32 @@ internal let SUPPORTED_CAMERA_DEVICE_TYPES: [AVCaptureDevice.DeviceType] = [
85
133
  }
86
134
  return
87
135
  }
88
-
136
+
89
137
  // Start the capture session
90
138
  self.captureSession.startRunning()
91
-
139
+
92
140
  // Display the camera preview on the provided webview
93
141
  self.displayPreview(
94
142
  on: webView,
95
143
  completion: { error in
96
- if error != nil { completion(error) }
97
-
144
+ if error != nil {
145
+ completion(error)
146
+ return
147
+ }
148
+
98
149
  // Handle barcode detection after session is running
99
150
  if configuration.enableBarcodeDetection {
100
151
  do {
101
- try self.enableBarcodeDetection()
152
+ try self.enableBarcodeDetection(barcodeTypes: configuration.barcodeTypes)
102
153
  } catch {
103
154
  completion(error)
104
155
  return
105
156
  }
106
157
  }
107
-
158
+
108
159
  // Complete already because the camera is ready to be used
109
160
  completion(nil)
110
-
161
+
111
162
  // We might asynchronously upgrade to a triple camera in the background if available and configured
112
163
  if configuration.useTripleCameraIfAvailable {
113
164
  Task {
@@ -118,39 +169,48 @@ internal let SUPPORTED_CAMERA_DEVICE_TYPES: [AVCaptureDevice.DeviceType] = [
118
169
  )
119
170
  }
120
171
  }
121
-
122
- /// Stops the current capture session.
172
+
173
+ /// Stops the current capture session
123
174
  public func stopSession(completion: (() -> Void)? = nil) {
124
175
  guard captureSession.isRunning else {
125
176
  completion?()
126
177
  return
127
178
  }
128
-
129
- DispatchQueue.global(qos: .userInitiated).async { [weak self] in
179
+
180
+ sessionQueue.async { [weak self] in
181
+ if let self = self, self.avMovieOutput.isRecording {
182
+ self.avMovieOutput.stopRecording()
183
+ self.videoRecordingCompletionHandler = nil
184
+ self.recordingWithAudio = false
185
+ }
186
+
130
187
  self?.captureSession.stopRunning()
131
-
188
+
132
189
  DispatchQueue.main.async { [weak self] in
133
- guard let self = self else { return }
190
+ guard let self = self else {
191
+ completion?()
192
+ return
193
+ }
134
194
  self.videoPreviewLayer.removeFromSuperlayer()
135
195
  self.webView?.isOpaque = true
136
196
  self.webView?.backgroundColor = nil
137
197
  self.webView = nil
138
-
198
+
139
199
  if let blurOverlayView = self.blurOverlayView {
140
200
  blurOverlayView.removeFromSuperview()
141
201
  self.blurOverlayView = nil
142
202
  }
203
+
204
+ completion?()
143
205
  }
144
-
145
- completion?()
146
206
  }
147
207
  }
148
-
208
+
149
209
  /// Checks if the capture session is currently running.
150
210
  public func isRunning() -> Bool {
151
211
  return captureSession.isRunning
152
212
  }
153
-
213
+
154
214
  /// Captures a photo with the current camera settings.
155
215
  /// - Returns: The picture as UIImage via `AVCapturePhotoCaptureDelegate`
156
216
  public func capturePhoto(completion: @escaping (UIImage?, Error?) -> Void) {
@@ -158,33 +218,66 @@ internal let SUPPORTED_CAMERA_DEVICE_TYPES: [AVCaptureDevice.DeviceType] = [
158
218
  completion(nil, CameraError.cameraUnavailable)
159
219
  return
160
220
  }
161
-
221
+
162
222
  guard captureSession.isRunning else {
163
223
  completion(nil, CameraError.sessionNotRunning)
164
224
  return
165
225
  }
166
-
226
+
167
227
  let photoSettings = AVCapturePhotoSettings()
168
228
  if cameraDevice.hasFlash {
169
229
  photoSettings.flashMode = flashMode
170
230
  } else {
171
231
  photoSettings.flashMode = .off
172
232
  }
173
-
233
+
174
234
  // Ensure proper orientation
175
235
  if let photoConnection = avPhotoOutput.connection(with: .video),
176
- let previewConnection = videoPreviewLayer.connection
177
- {
236
+ let previewConnection = videoPreviewLayer.connection {
178
237
  if photoConnection.isVideoOrientationSupported {
179
- photoConnection.videoOrientation =
180
- previewConnection.videoOrientation
238
+ photoConnection.videoOrientation = previewConnection.videoOrientation
181
239
  }
182
240
  }
183
-
241
+
184
242
  avPhotoOutput.capturePhoto(with: photoSettings, delegate: self)
185
243
  photoCaptureHandler = completion
186
244
  }
187
-
245
+
246
+ /// Captures a photo and returns the raw JPEG data directly.
247
+ /// This optimized method avoids double JPEG encoding by returning the camera's
248
+ /// native JPEG data instead of converting through UIImage.
249
+ ///
250
+ /// - Parameter completion: Called with the captured JPEG data or an error.
251
+ public func capturePhotoData(completion: @escaping (Data?, Error?) -> Void) {
252
+ guard let cameraDevice = currentCameraDevice else {
253
+ completion(nil, CameraError.cameraUnavailable)
254
+ return
255
+ }
256
+
257
+ guard captureSession.isRunning else {
258
+ completion(nil, CameraError.sessionNotRunning)
259
+ return
260
+ }
261
+
262
+ let photoSettings = AVCapturePhotoSettings()
263
+ if cameraDevice.hasFlash {
264
+ photoSettings.flashMode = flashMode
265
+ } else {
266
+ photoSettings.flashMode = .off
267
+ }
268
+
269
+ // Ensure proper orientation
270
+ if let photoConnection = avPhotoOutput.connection(with: .video),
271
+ let previewConnection = videoPreviewLayer.connection {
272
+ if photoConnection.isVideoOrientationSupported {
273
+ photoConnection.videoOrientation = previewConnection.videoOrientation
274
+ }
275
+ }
276
+
277
+ avPhotoOutput.capturePhoto(with: photoSettings, delegate: self)
278
+ photoDataCaptureHandler = completion
279
+ }
280
+
188
281
  /// Capture a snapshot of the current camera view. This is faster than actually processing a
189
282
  /// photo via capturePhoto
190
283
  /// - Parameter completion: called with the captured UIImage or an error.
@@ -195,46 +288,39 @@ internal let SUPPORTED_CAMERA_DEVICE_TYPES: [AVCaptureDevice.DeviceType] = [
195
288
  completion(nil, CameraError.cameraUnavailable)
196
289
  return
197
290
  }
198
-
291
+
199
292
  guard captureSession.isRunning else {
200
293
  completion(nil, CameraError.sessionNotRunning)
201
294
  return
202
295
  }
203
-
296
+
204
297
  // Ensure proper orientation
205
298
  if let videoConnection = avVideoDataOutput.connection(with: .video),
206
- let previewConnection = videoPreviewLayer.connection
207
- {
299
+ let previewConnection = videoPreviewLayer.connection {
208
300
  if videoConnection.isVideoOrientationSupported {
209
- videoConnection.videoOrientation =
210
- previewConnection.videoOrientation
301
+ videoConnection.videoOrientation = previewConnection.videoOrientation
211
302
  }
212
303
  }
213
-
214
- // Create a serial queue for sample buffer processing
215
- let sampleBufferQueue = DispatchQueue(
216
- label: "com.michaelwolz.capacitorcameraview.snapshotQueue"
217
- )
218
-
219
- // Set the delegate for a single frame capture
304
+
305
+ // Set the delegate for a single frame capture using the reusable queue
220
306
  snapshotCompletionHandler = completion
221
307
  avVideoDataOutput.setSampleBufferDelegate(
222
308
  self,
223
309
  queue: sampleBufferQueue
224
310
  )
225
311
  }
226
-
312
+
227
313
  /// Flips the camera to the opposite position (front to back or back to front).
228
314
  public func flipCamera() throws {
229
315
  let currentPosition: AVCaptureDevice.Position =
230
- currentCameraDevice?.position ?? .back
316
+ currentCameraDevice?.position ?? .back
231
317
  let newPosition: AVCaptureDevice.Position =
232
- currentPosition == .back ? .front : .back
233
-
318
+ currentPosition == .back ? .front : .back
319
+
234
320
  let newCamera = try getCameraDevice(for: newPosition)
235
321
  try setInput(with: newCamera)
236
322
  }
237
-
323
+
238
324
  /// Sets the flash mode for the currently active camera device.
239
325
  ///
240
326
  /// - Parameter mode: The desired flash mode (.on, .of, or .auto).
@@ -248,15 +334,15 @@ internal let SUPPORTED_CAMERA_DEVICE_TYPES: [AVCaptureDevice.DeviceType] = [
248
334
  else {
249
335
  throw CameraError.unsupportedFlashMode
250
336
  }
251
-
337
+
252
338
  flashMode = mode
253
339
  }
254
-
340
+
255
341
  /// Gets the current flash mode for the current camera device.
256
342
  public func getFlashMode() -> AVCaptureDevice.FlashMode {
257
343
  return flashMode
258
344
  }
259
-
345
+
260
346
  /// Gets the supported flash modes for the current camera device.
261
347
  ///
262
348
  /// - Returns: An array of supported flash modes, fallback is .off
@@ -264,10 +350,10 @@ internal let SUPPORTED_CAMERA_DEVICE_TYPES: [AVCaptureDevice.DeviceType] = [
264
350
  if let camera = currentCameraDevice, camera.hasFlash {
265
351
  return avPhotoOutput.supportedFlashModes
266
352
  }
267
-
353
+
268
354
  return [.off]
269
355
  }
270
-
356
+
271
357
  /// Checks if torch is available on the current camera device.
272
358
  ///
273
359
  /// - Returns: True if torch is available, false otherwise
@@ -275,19 +361,19 @@ internal let SUPPORTED_CAMERA_DEVICE_TYPES: [AVCaptureDevice.DeviceType] = [
275
361
  guard let camera = currentCameraDevice else { return false }
276
362
  return camera.hasTorch
277
363
  }
278
-
364
+
279
365
  /// Gets the current torch mode and level.
280
366
  ///
281
367
  /// - Returns: A tuple containing the torch enabled state and level
282
368
  public func getTorchMode() -> (enabled: Bool, level: Float) {
283
369
  guard let camera = currentCameraDevice else { return (false, 0.0) }
284
-
370
+
285
371
  let isEnabled = camera.torchMode == .on
286
372
  let level = camera.torchLevel
287
-
373
+
288
374
  return (isEnabled, level)
289
375
  }
290
-
376
+
291
377
  /// Sets the torch mode and level for the currently active camera device.
292
378
  ///
293
379
  /// - Parameters:
@@ -299,11 +385,11 @@ internal let SUPPORTED_CAMERA_DEVICE_TYPES: [AVCaptureDevice.DeviceType] = [
299
385
  throw CameraError.cameraUnavailable
300
386
  }
301
387
  guard camera.hasTorch else { throw CameraError.torchUnavailable }
302
-
388
+
303
389
  do {
304
390
  try camera.lockForConfiguration()
305
391
  defer { camera.unlockForConfiguration() }
306
-
392
+
307
393
  if enabled && level > 0.0 {
308
394
  try camera.setTorchModeOn(level: level)
309
395
  } else {
@@ -313,7 +399,7 @@ internal let SUPPORTED_CAMERA_DEVICE_TYPES: [AVCaptureDevice.DeviceType] = [
313
399
  throw CameraError.configurationFailed(error)
314
400
  }
315
401
  }
316
-
402
+
317
403
  /// Gets the minimum, maximum, and current zoom factors supported by the current camera device.
318
404
  /// The maximum zoom factor is limited to a reasonable value of 10x to prevent excessive zooming
319
405
  /// because some devices report very high zoom factors that aren't useful.
@@ -329,21 +415,21 @@ internal let SUPPORTED_CAMERA_DEVICE_TYPES: [AVCaptureDevice.DeviceType] = [
329
415
  current: 1.0
330
416
  )
331
417
  }
332
-
418
+
333
419
  let minZoomFactor = currentDevice.minAvailableVideoZoomFactor
334
420
  let maxZoomFactor = min(
335
421
  currentDevice.activeFormat.videoMaxZoomFactor,
336
422
  10.0
337
423
  )
338
424
  let currentZoomFactor = currentDevice.videoZoomFactor
339
-
425
+
340
426
  return (
341
427
  min: minZoomFactor,
342
428
  max: maxZoomFactor,
343
429
  current: currentZoomFactor
344
430
  )
345
431
  }
346
-
432
+
347
433
  /// Sets the zoom factor for the current camera device.
348
434
  ///
349
435
  /// - Parameters:
@@ -354,7 +440,7 @@ internal let SUPPORTED_CAMERA_DEVICE_TYPES: [AVCaptureDevice.DeviceType] = [
354
440
  guard let device = currentCameraDevice else {
355
441
  throw CameraError.cameraUnavailable
356
442
  }
357
-
443
+
358
444
  let supportedZoomFactors = getSupportedZoomFactors()
359
445
  guard
360
446
  factor >= supportedZoomFactors.min
@@ -362,11 +448,11 @@ internal let SUPPORTED_CAMERA_DEVICE_TYPES: [AVCaptureDevice.DeviceType] = [
362
448
  else {
363
449
  throw CameraError.zoomFactorOutOfRange
364
450
  }
365
-
451
+
366
452
  do {
367
453
  try device.lockForConfiguration()
368
454
  defer { device.unlockForConfiguration() }
369
-
455
+
370
456
  if ramp {
371
457
  device.ramp(toVideoZoomFactor: factor, withRate: 6.0)
372
458
  } else {
@@ -376,7 +462,7 @@ internal let SUPPORTED_CAMERA_DEVICE_TYPES: [AVCaptureDevice.DeviceType] = [
376
462
  throw CameraError.configurationFailed(error)
377
463
  }
378
464
  }
379
-
465
+
380
466
  /// Initiates the capture session with the specified camera device.
381
467
  ///
382
468
  /// - Parameters:
@@ -386,7 +472,7 @@ internal let SUPPORTED_CAMERA_DEVICE_TYPES: [AVCaptureDevice.DeviceType] = [
386
472
  ) throws {
387
473
  captureSession.beginConfiguration()
388
474
  defer { captureSession.commitConfiguration() }
389
-
475
+
390
476
  // Configure the camera device
391
477
  let device: AVCaptureDevice
392
478
  if let deviceId = configuration.deviceId {
@@ -394,33 +480,33 @@ internal let SUPPORTED_CAMERA_DEVICE_TYPES: [AVCaptureDevice.DeviceType] = [
394
480
  } else {
395
481
  device = try getCameraDevice(for: configuration.position)
396
482
  }
397
-
483
+
398
484
  // Set the session preset to photo if supported (which should be the case for all devices)
399
485
  if captureSession.canSetSessionPreset(.photo) {
400
486
  captureSession.sessionPreset = .photo
401
487
  }
402
-
488
+
403
489
  // Set the camera input
404
490
  try setInput(with: device)
405
-
491
+
406
492
  // Set up the photo output
407
493
  try setupPhotoOutput()
408
-
494
+
409
495
  // Set up the video data output for snapshots
410
496
  try setupVideoDataOutput()
411
-
497
+
412
498
  if !configuration.enableBarcodeDetection {
413
499
  // Remove the metadata output in case it already existed for the
414
500
  // capture session
415
501
  removeMetadataOutput()
416
502
  }
417
-
503
+
418
504
  // Set the initial zoom factor if specified
419
505
  if let zoomFactor = configuration.zoomFactor {
420
506
  try setZoomFactor(zoomFactor, ramp: false)
421
507
  }
422
508
  }
423
-
509
+
424
510
  /// Sets the input for the capture session.
425
511
  /// Make sure to call `captureSession.beginConfiguration` before calling this
426
512
  ///
@@ -431,16 +517,16 @@ internal let SUPPORTED_CAMERA_DEVICE_TYPES: [AVCaptureDevice.DeviceType] = [
431
517
  // Nothing todo, input is already configured for the desired device
432
518
  return
433
519
  }
434
-
520
+
435
521
  // Remove any existing inputs
436
522
  captureSession.inputs.forEach { captureSession.removeInput($0) }
437
-
523
+
438
524
  do {
439
525
  let input = try AVCaptureDeviceInput(device: device)
440
526
  if !captureSession.canAddInput(input) {
441
527
  throw CameraError.inputAdditionFailed
442
528
  }
443
-
529
+
444
530
  captureSession.addInput(input)
445
531
  currentCameraDevice = device
446
532
  } catch {
@@ -451,7 +537,7 @@ internal let SUPPORTED_CAMERA_DEVICE_TYPES: [AVCaptureDevice.DeviceType] = [
451
537
  }
452
538
  }
453
539
  }
454
-
540
+
455
541
  /// Enables barcode detection by adding metadata output to the running session.
456
542
  /// Somehow adding the metadata output with the session not being started yet
457
543
  /// caused issues on some devices (iPad 7th Gen) where the session would just
@@ -459,18 +545,19 @@ internal let SUPPORTED_CAMERA_DEVICE_TYPES: [AVCaptureDevice.DeviceType] = [
459
545
  /// figure out the root cause of this but it might generally be a good idea
460
546
  /// to only add the metadata output when the session is already running.
461
547
  ///
548
+ /// - Parameter barcodeTypes: Optional array of barcode types to detect. If nil, all supported types are used.
462
549
  /// - Throws: An error if the metadata output cannot be added.
463
- private func enableBarcodeDetection() throws {
550
+ private func enableBarcodeDetection(barcodeTypes: [AVMetadataObject.ObjectType]? = nil) throws {
464
551
  guard captureSession.isRunning else {
465
552
  throw CameraError.sessionNotRunning
466
553
  }
467
-
554
+
468
555
  captureSession.beginConfiguration()
469
556
  defer { captureSession.commitConfiguration() }
470
-
471
- try setupMetadataOutput()
557
+
558
+ try setupMetadataOutput(barcodeTypes: barcodeTypes)
472
559
  }
473
-
560
+
474
561
  /// Retrieve a list of a available camera devices
475
562
  ///
476
563
  /// - Returns: a list of all available camera devices.
@@ -481,7 +568,7 @@ internal let SUPPORTED_CAMERA_DEVICE_TYPES: [AVCaptureDevice.DeviceType] = [
481
568
  position: .unspecified
482
569
  ).devices
483
570
  }
484
-
571
+
485
572
  /// Returns a list of available camera devices based on the preferences by the user
486
573
  ///
487
574
  /// - Returns: a list of camera devices based on the preferredCameraDeviceTypes
@@ -492,7 +579,7 @@ internal let SUPPORTED_CAMERA_DEVICE_TYPES: [AVCaptureDevice.DeviceType] = [
492
579
  position: .unspecified
493
580
  ).devices
494
581
  }
495
-
582
+
496
583
  /// Gets the best match camera device for the specified position.
497
584
  /// This method will consider preferredCameraDevices as possibly provided by the user allowing a best
498
585
  /// match to the users request.
@@ -502,45 +589,40 @@ internal let SUPPORTED_CAMERA_DEVICE_TYPES: [AVCaptureDevice.DeviceType] = [
502
589
  /// - Returns: The camera device for the specified position
503
590
  /// - Throws: An error if no camera device is found.
504
591
  private func getCameraDevice(for position: AVCaptureDevice.Position?) throws
505
- -> AVCaptureDevice
506
- {
592
+ -> AVCaptureDevice {
507
593
  let preferredDevices = getPreferredCameraDevices()
508
-
594
+
509
595
  // First try to get the best match based on the users preferred camera device types
510
596
  if let match = preferredDevices.first(where: { $0.position == position }
511
597
  ) {
512
598
  return match
513
599
  }
514
-
600
+
515
601
  // If we haven't found one we try to get a best match for the position by iterating all supported device types
516
602
  // Only doing this when preferredCameraDeviceTypes size differs from SUPPORTED_CAMERA_DEVICE_TYPES, otherwise
517
603
  // we don't have to initialize a new discovery session
518
- if preferredCameraDeviceTypes.count
519
- < SUPPORTED_CAMERA_DEVICE_TYPES.count,
520
- let match = getAvailableDevices().first(where: {
521
- $0.position == position
522
- })
523
- {
604
+ if preferredCameraDeviceTypes.count < SUPPORTED_CAMERA_DEVICE_TYPES.count,
605
+ let match = getAvailableDevices().first(where: {
606
+ $0.position == position
607
+ }) {
524
608
  return match
525
609
  }
526
-
610
+
527
611
  // Otherwise, fallback to default video device or throw an error
528
612
  guard let device = AVCaptureDevice.default(for: .video) else {
529
613
  throw CameraError.cameraUnavailable
530
614
  }
531
-
615
+
532
616
  // Log when we're falling back to a device with different position than requested
533
- if let requestedPosition = position,
534
- device.position != requestedPosition
535
- {
617
+ if let requestedPosition = position, device.position != requestedPosition {
536
618
  print(
537
619
  "Warning: Falling back to camera at position \(device.position) when \(requestedPosition) was requested"
538
620
  )
539
621
  }
540
-
622
+
541
623
  return device
542
624
  }
543
-
625
+
544
626
  /// Gets the best camera device for the specified position.
545
627
  ///
546
628
  /// - Parameters:
@@ -548,8 +630,7 @@ internal let SUPPORTED_CAMERA_DEVICE_TYPES: [AVCaptureDevice.DeviceType] = [
548
630
  /// - Returns: The camera device for the specified position
549
631
  /// - Throws: An error if no camera device is found.
550
632
  private func getCameraDeviceById(_ deviceId: String) throws
551
- -> AVCaptureDevice
552
- {
633
+ -> AVCaptureDevice {
553
634
  guard
554
635
  let device = getAvailableDevices().first(where: {
555
636
  $0.uniqueID == deviceId
@@ -559,9 +640,9 @@ internal let SUPPORTED_CAMERA_DEVICE_TYPES: [AVCaptureDevice.DeviceType] = [
559
640
  }
560
641
  return device
561
642
  }
562
-
643
+
563
644
  // MARK: - UI Preview Layer
564
-
645
+
565
646
  /// Sets up the preview layer for the capture session which will
566
647
  /// display the camera feed in the view.
567
648
  ///
@@ -577,56 +658,56 @@ internal let SUPPORTED_CAMERA_DEVICE_TYPES: [AVCaptureDevice.DeviceType] = [
577
658
  completion(CameraError.sessionNotRunning)
578
659
  return
579
660
  }
580
-
661
+
581
662
  self.webView = view
582
-
663
+
583
664
  videoPreviewLayer.session = captureSession
584
665
  videoPreviewLayer.videoGravity = .resizeAspectFill
585
-
666
+
586
667
  DispatchQueue.main.async { [weak self] in
587
668
  guard let self = self else { return }
588
669
  view.isOpaque = false
589
670
  view.backgroundColor = UIColor.clear
590
671
  (view as? WKWebView)?.scrollView.backgroundColor = UIColor.clear
591
-
672
+
592
673
  self.videoPreviewLayer.frame = view.bounds
593
674
  view.layer.insertSublayer(self.videoPreviewLayer, at: 0)
594
-
675
+
595
676
  self.updatePreviewOrientation()
596
-
677
+
597
678
  completion(nil)
598
679
  }
599
680
  }
600
-
681
+
601
682
  // MARK: - Triple Camera
602
-
683
+
603
684
  /// Upgrades the camera to the triple camera if available.
604
685
  /// Initializing the triple camera is an expensive operation and takes some time.
605
686
  /// This is why by default the regular physical camera is used and then later upgraded to the triple camera if available (Pro models only).
606
687
  private func upgradeToTripleCameraIfAvailable() async {
607
688
  guard captureSession.isRunning else { return }
608
-
689
+
609
690
  // Check if a triple camera is available (only on newer Pro models)
610
691
  let devices = AVCaptureDevice.DiscoverySession(
611
692
  deviceTypes: [.builtInTripleCamera],
612
693
  mediaType: .video,
613
694
  position: .back
614
695
  ).devices
615
-
696
+
616
697
  // If we don't have a triple camera, exit early
617
698
  guard let tripleCamera = devices.first else { return }
618
-
699
+
619
700
  // Don't do anything if we're already using the triple camera
620
701
  if currentCameraDevice?.uniqueID == tripleCamera.uniqueID {
621
702
  return
622
703
  }
623
-
704
+
624
705
  // Add a blur overlay to the webview to have a smooth transition when switching to the triple camera
625
706
  await addBlurOverlay()
626
-
707
+
627
708
  await Task.detached(priority: .userInitiated) {
628
709
  self.captureSession.beginConfiguration()
629
-
710
+
630
711
  do {
631
712
  try self.setInput(with: tripleCamera)
632
713
  // TODO: Consider configured zoom factor from the initial camera???
@@ -637,41 +718,40 @@ internal let SUPPORTED_CAMERA_DEVICE_TYPES: [AVCaptureDevice.DeviceType] = [
637
718
  "Failed to upgrade to triple camera: \(error.localizedDescription)"
638
719
  )
639
720
  }
640
-
721
+
641
722
  self.captureSession.commitConfiguration()
642
723
  }.value
643
-
724
+
644
725
  // Small delay to let camera stabilize
645
726
  try? await Task.sleep(nanoseconds: 300_000_000) // 0.3 seconds
646
-
727
+
647
728
  await removeBlurOverlayWithAnimation()
648
729
  }
649
-
730
+
650
731
  /// Adds a blur overlay to the webview to have a smooth transition when switching to the triple camera
651
732
  @MainActor
652
733
  private func addBlurOverlay() async {
653
734
  guard let view = self.webView else { return }
654
-
735
+
655
736
  let blurEffect = UIBlurEffect(style: .light)
656
737
  let blurOverlayView = UIVisualEffectView(effect: blurEffect)
657
738
  self.blurOverlayView = blurOverlayView
658
-
739
+
659
740
  blurOverlayView.frame = view.bounds
660
741
  blurOverlayView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
661
-
742
+
662
743
  // Add the blurEffect layer to the view hierarchy just above the preview layer
663
744
  // but below the web content
664
745
  view.insertSubview(blurOverlayView, at: 1)
665
746
  }
666
-
747
+
667
748
  /// Removes the blur overlay with a fade out animation to have a smooth transition
668
749
  /// - Parameter duration: The duration of the fade out animation
669
750
  @MainActor
670
751
  private func removeBlurOverlayWithAnimation(duration: TimeInterval = 0.3)
671
- async
672
- {
752
+ async {
673
753
  guard let blurEffectView = blurOverlayView else { return }
674
-
754
+
675
755
  await withCheckedContinuation { continuation in
676
756
  UIView.animate(
677
757
  withDuration: duration,
@@ -686,9 +766,9 @@ internal let SUPPORTED_CAMERA_DEVICE_TYPES: [AVCaptureDevice.DeviceType] = [
686
766
  )
687
767
  }
688
768
  }
689
-
769
+
690
770
  // MARK: - Orientation Observer
691
-
771
+
692
772
  /// Sets up an observer for device orientation changes to update the preview layer orientation.
693
773
  private func setupOrientationObserver() {
694
774
  NotificationCenter.default.addObserver(
@@ -699,20 +779,20 @@ internal let SUPPORTED_CAMERA_DEVICE_TYPES: [AVCaptureDevice.DeviceType] = [
699
779
  self?.updatePreviewOrientation()
700
780
  }
701
781
  }
702
-
782
+
703
783
  /// Updates the preview layer orientation based on the current device orientation.
704
784
  private func updatePreviewOrientation() {
705
785
  guard let connection = self.videoPreviewLayer.connection,
706
- connection.isVideoOrientationSupported
786
+ connection.isVideoOrientationSupported
707
787
  else {
708
788
  return
709
789
  }
710
-
711
- let interfaceOrientation =
712
- UIApplication.shared.windows.first?.windowScene?
713
- .interfaceOrientation ?? .portrait
790
+
791
+ let interfaceOrientation = UIApplication.shared.connectedScenes
792
+ .compactMap({ $0 as? UIWindowScene })
793
+ .first?.interfaceOrientation ?? .portrait
714
794
  let videoOrientation: AVCaptureVideoOrientation
715
-
795
+
716
796
  switch interfaceOrientation {
717
797
  case .portrait:
718
798
  videoOrientation = .portrait
@@ -725,18 +805,18 @@ internal let SUPPORTED_CAMERA_DEVICE_TYPES: [AVCaptureDevice.DeviceType] = [
725
805
  default:
726
806
  videoOrientation = .portrait
727
807
  }
728
-
808
+
729
809
  connection.videoOrientation = videoOrientation
730
-
810
+
731
811
  // Update the frame of the preview layer to match the new bounds
732
812
  DispatchQueue.main.async { [weak self] in
733
813
  guard let self = self, let view = self.webView else { return }
734
814
  self.videoPreviewLayer.frame = view.bounds
735
815
  }
736
816
  }
737
-
817
+
738
818
  // MARK: - App Lifecycle Observers
739
-
819
+
740
820
  /// Sets up observers for app lifecycle events to pause and resume the camera session.
741
821
  private func setupAppLifecycleObservers() {
742
822
  NotificationCenter.default.addObserver(
@@ -745,7 +825,7 @@ internal let SUPPORTED_CAMERA_DEVICE_TYPES: [AVCaptureDevice.DeviceType] = [
745
825
  name: UIApplication.willResignActiveNotification,
746
826
  object: nil
747
827
  )
748
-
828
+
749
829
  NotificationCenter.default.addObserver(
750
830
  self,
751
831
  selector: #selector(handleAppDidBecomeActive),
@@ -753,22 +833,22 @@ internal let SUPPORTED_CAMERA_DEVICE_TYPES: [AVCaptureDevice.DeviceType] = [
753
833
  object: nil
754
834
  )
755
835
  }
756
-
836
+
757
837
  /// Handles the app going to background by pausing the camera session.
758
838
  @objc private func handleAppWillResignActive() {
759
839
  // Pause the session when app goes to background to save resources
760
840
  if captureSession.isRunning {
761
- DispatchQueue.global(qos: .userInitiated).async { [weak self] in
841
+ sessionQueue.async { [weak self] in
762
842
  self?.captureSession.stopRunning()
763
843
  }
764
844
  }
765
845
  }
766
-
846
+
767
847
  /// Handles the app coming back to foreground by resuming the camera session.
768
848
  @objc private func handleAppDidBecomeActive() {
769
849
  // Resume the session when app comes back to foreground
770
850
  if !captureSession.isRunning && webView != nil {
771
- DispatchQueue.global(qos: .userInitiated).async { [weak self] in
851
+ sessionQueue.async { [weak self] in
772
852
  self?.captureSession.startRunning()
773
853
  }
774
854
  }