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