capacitor-camera-view 2.0.1 → 2.1.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 +19 -9
  2. package/android/build.gradle +8 -5
  3. package/android/src/main/java/com/michaelwolz/capacitorcameraview/CameraView.kt +217 -126
  4. package/android/src/main/java/com/michaelwolz/capacitorcameraview/CameraViewPlugin.kt +70 -30
  5. package/android/src/main/java/com/michaelwolz/capacitorcameraview/model/CameraResult.kt +47 -0
  6. package/android/src/main/java/com/michaelwolz/capacitorcameraview/model/CameraSessionConfiguration.kt +11 -1
  7. package/android/src/main/java/com/michaelwolz/capacitorcameraview/utils.kt +94 -5
  8. package/dist/docs.json +81 -0
  9. package/dist/esm/definitions.d.ts +44 -0
  10. package/dist/esm/definitions.js.map +1 -1
  11. package/dist/esm/web.d.ts +7 -1
  12. package/dist/esm/web.js +67 -2
  13. package/dist/esm/web.js.map +1 -1
  14. package/dist/plugin.cjs.js +68 -2
  15. package/dist/plugin.cjs.js.map +1 -1
  16. package/dist/plugin.js +68 -2
  17. package/dist/plugin.js.map +1 -1
  18. package/ios/Sources/CameraViewPlugin/CameraError.swift +97 -2
  19. package/ios/Sources/CameraViewPlugin/CameraEvents.swift +109 -0
  20. package/ios/Sources/CameraViewPlugin/CameraSessionConfiguration.swift +29 -2
  21. package/ios/Sources/CameraViewPlugin/CameraViewManager+BarcodeScan.swift +30 -41
  22. package/ios/Sources/CameraViewPlugin/CameraViewManager+PhotoCapture.swift +45 -13
  23. package/ios/Sources/CameraViewPlugin/CameraViewManager+VideoDataOutput.swift +4 -3
  24. package/ios/Sources/CameraViewPlugin/CameraViewManager.swift +193 -59
  25. package/ios/Sources/CameraViewPlugin/CameraViewPlugin.swift +83 -84
  26. package/ios/Sources/CameraViewPlugin/TempFileManager.swift +181 -0
  27. package/ios/Sources/CameraViewPlugin/Utils.swift +102 -0
  28. 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,16 +13,44 @@ 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
+ private 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
+ private let sampleBufferQueue = DispatchQueue(
50
+ label: "com.michaelwolz.capacitorcameraview.sampleBuffer",
51
+ qos: .userInitiated
52
+ )
53
+
24
54
  /// The currently active camera device.
25
55
  private var currentCameraDevice: AVCaptureDevice?
26
56
 
@@ -36,12 +66,19 @@ internal let SUPPORTED_CAMERA_DEVICE_TYPES: [AVCaptureDevice.DeviceType] = [
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
68
 
39
- /// Callback for when photo capture completes.
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
+
45
82
  override public init() {
46
83
  super.init()
47
84
  setupOrientationObserver()
@@ -66,11 +103,15 @@ internal let SUPPORTED_CAMERA_DEVICE_TYPES: [AVCaptureDevice.DeviceType] = [
66
103
  webView: UIView,
67
104
  completion: @escaping (Error?) -> Void
68
105
  ) {
69
- if let preferredCameraDeviceTypes = configuration.preferredCameraDeviceTypes {
70
- self.preferredCameraDeviceTypes = convertToNativeCameraTypes(preferredCameraDeviceTypes)
106
+ if let preferredCameraDeviceTypes = configuration
107
+ .preferredCameraDeviceTypes
108
+ {
109
+ self.preferredCameraDeviceTypes = convertToNativeCameraTypes(
110
+ preferredCameraDeviceTypes
111
+ )
71
112
  }
72
113
 
73
- DispatchQueue.global(qos: .userInitiated).async { [weak self] in
114
+ sessionQueue.async { [weak self] in
74
115
  guard let self = self else { return }
75
116
 
76
117
  do {
@@ -89,12 +130,15 @@ internal let SUPPORTED_CAMERA_DEVICE_TYPES: [AVCaptureDevice.DeviceType] = [
89
130
  self.displayPreview(
90
131
  on: webView,
91
132
  completion: { error in
92
- if error != nil { completion(error) }
133
+ if error != nil {
134
+ completion(error)
135
+ return
136
+ }
93
137
 
94
138
  // Handle barcode detection after session is running
95
139
  if configuration.enableBarcodeDetection {
96
140
  do {
97
- try self.enableBarcodeDetection()
141
+ try self.enableBarcodeDetection(barcodeTypes: configuration.barcodeTypes)
98
142
  } catch {
99
143
  completion(error)
100
144
  return
@@ -110,34 +154,38 @@ internal let SUPPORTED_CAMERA_DEVICE_TYPES: [AVCaptureDevice.DeviceType] = [
110
154
  await self.upgradeToTripleCameraIfAvailable()
111
155
  }
112
156
  }
113
- })
157
+ }
158
+ )
114
159
  }
115
160
  }
116
161
 
117
- /// Stops the current capture session.
162
+ /// Stops the current capture session and cleans up temporary files.
118
163
  public func stopSession(completion: (() -> Void)? = nil) {
119
164
  guard captureSession.isRunning else {
120
165
  completion?()
121
166
  return
122
167
  }
123
168
 
124
- DispatchQueue.global(qos: .userInitiated).async { [weak self] in
169
+ sessionQueue.async { [weak self] in
125
170
  self?.captureSession.stopRunning()
126
- }
127
171
 
128
- DispatchQueue.main.async { [weak self] in
129
- guard let self = self else { return }
130
- self.videoPreviewLayer.removeFromSuperlayer()
131
- self.webView?.isOpaque = true
132
- self.webView?.backgroundColor = nil
133
- self.webView = nil
134
-
135
- if let blurOverlayView = self.blurOverlayView {
136
- blurOverlayView.removeFromSuperview()
137
- self.blurOverlayView = nil
138
- }
172
+ DispatchQueue.main.async { [weak self] in
173
+ guard let self = self else {
174
+ completion?()
175
+ return
176
+ }
177
+ self.videoPreviewLayer.removeFromSuperlayer()
178
+ self.webView?.isOpaque = true
179
+ self.webView?.backgroundColor = nil
180
+ self.webView = nil
139
181
 
140
- completion?()
182
+ if let blurOverlayView = self.blurOverlayView {
183
+ blurOverlayView.removeFromSuperview()
184
+ self.blurOverlayView = nil
185
+ }
186
+
187
+ completion?()
188
+ }
141
189
  }
142
190
  }
143
191
 
@@ -168,7 +216,8 @@ internal let SUPPORTED_CAMERA_DEVICE_TYPES: [AVCaptureDevice.DeviceType] = [
168
216
 
169
217
  // Ensure proper orientation
170
218
  if let photoConnection = avPhotoOutput.connection(with: .video),
171
- let previewConnection = videoPreviewLayer.connection {
219
+ let previewConnection = videoPreviewLayer.connection
220
+ {
172
221
  if photoConnection.isVideoOrientationSupported {
173
222
  photoConnection.videoOrientation = previewConnection.videoOrientation
174
223
  }
@@ -178,10 +227,48 @@ internal let SUPPORTED_CAMERA_DEVICE_TYPES: [AVCaptureDevice.DeviceType] = [
178
227
  photoCaptureHandler = completion
179
228
  }
180
229
 
230
+ /// Captures a photo and returns the raw JPEG data directly.
231
+ /// This optimized method avoids double JPEG encoding by returning the camera's
232
+ /// native JPEG data instead of converting through UIImage.
233
+ ///
234
+ /// - Parameter completion: Called with the captured JPEG data or an error.
235
+ public func capturePhotoData(completion: @escaping (Data?, Error?) -> Void) {
236
+ guard let cameraDevice = currentCameraDevice else {
237
+ completion(nil, CameraError.cameraUnavailable)
238
+ return
239
+ }
240
+
241
+ guard captureSession.isRunning else {
242
+ completion(nil, CameraError.sessionNotRunning)
243
+ return
244
+ }
245
+
246
+ let photoSettings = AVCapturePhotoSettings()
247
+ if cameraDevice.hasFlash {
248
+ photoSettings.flashMode = flashMode
249
+ } else {
250
+ photoSettings.flashMode = .off
251
+ }
252
+
253
+ // Ensure proper orientation
254
+ if let photoConnection = avPhotoOutput.connection(with: .video),
255
+ let previewConnection = videoPreviewLayer.connection
256
+ {
257
+ if photoConnection.isVideoOrientationSupported {
258
+ photoConnection.videoOrientation = previewConnection.videoOrientation
259
+ }
260
+ }
261
+
262
+ avPhotoOutput.capturePhoto(with: photoSettings, delegate: self)
263
+ photoDataCaptureHandler = completion
264
+ }
265
+
181
266
  /// Capture a snapshot of the current camera view. This is faster than actually processing a
182
267
  /// photo via capturePhoto
183
268
  /// - Parameter completion: called with the captured UIImage or an error.
184
- public func captureSnapshot(completion: @escaping (UIImage?, Error?) -> Void) {
269
+ public func captureSnapshot(
270
+ completion: @escaping (UIImage?, Error?) -> Void
271
+ ) {
185
272
  guard currentCameraDevice != nil else {
186
273
  completion(nil, CameraError.cameraUnavailable)
187
274
  return
@@ -194,24 +281,27 @@ internal let SUPPORTED_CAMERA_DEVICE_TYPES: [AVCaptureDevice.DeviceType] = [
194
281
 
195
282
  // Ensure proper orientation
196
283
  if let videoConnection = avVideoDataOutput.connection(with: .video),
197
- let previewConnection = videoPreviewLayer.connection {
284
+ let previewConnection = videoPreviewLayer.connection
285
+ {
198
286
  if videoConnection.isVideoOrientationSupported {
199
287
  videoConnection.videoOrientation = previewConnection.videoOrientation
200
288
  }
201
289
  }
202
290
 
203
- // Create a serial queue for sample buffer processing
204
- let sampleBufferQueue = DispatchQueue(label: "com.michaelwolz.capacitorcameraview.snapshotQueue")
205
-
206
- // Set the delegate for a single frame capture
291
+ // Set the delegate for a single frame capture using the reusable queue
207
292
  snapshotCompletionHandler = completion
208
- avVideoDataOutput.setSampleBufferDelegate(self, queue: sampleBufferQueue)
293
+ avVideoDataOutput.setSampleBufferDelegate(
294
+ self,
295
+ queue: sampleBufferQueue
296
+ )
209
297
  }
210
298
 
211
299
  /// Flips the camera to the opposite position (front to back or back to front).
212
300
  public func flipCamera() throws {
213
- let currentPosition: AVCaptureDevice.Position = currentCameraDevice?.position ?? .back
214
- let newPosition: AVCaptureDevice.Position = currentPosition == .back ? .front : .back
301
+ let currentPosition: AVCaptureDevice.Position =
302
+ currentCameraDevice?.position ?? .back
303
+ let newPosition: AVCaptureDevice.Position =
304
+ currentPosition == .back ? .front : .back
215
305
 
216
306
  let newCamera = try getCameraDevice(for: newPosition)
217
307
  try setInput(with: newCamera)
@@ -222,7 +312,9 @@ internal let SUPPORTED_CAMERA_DEVICE_TYPES: [AVCaptureDevice.DeviceType] = [
222
312
  /// - Parameter mode: The desired flash mode (.on, .of, or .auto).
223
313
  /// - Throws: An error if the flash mode cannot be set or is not supported.
224
314
  public func setFlashMode(_ mode: AVCaptureDevice.FlashMode) throws {
225
- guard let camera = currentCameraDevice else { throw CameraError.cameraUnavailable }
315
+ guard let camera = currentCameraDevice else {
316
+ throw CameraError.cameraUnavailable
317
+ }
226
318
  guard camera.hasFlash else { throw CameraError.unsupportedFlashMode }
227
319
  guard avPhotoOutput.supportedFlashModes.contains(mode)
228
320
  else {
@@ -275,7 +367,9 @@ internal let SUPPORTED_CAMERA_DEVICE_TYPES: [AVCaptureDevice.DeviceType] = [
275
367
  /// - level: The torch intensity level (0.0 to 1.0)
276
368
  /// - Throws: An error if the torch mode cannot be set or is not supported.
277
369
  public func setTorchMode(enabled: Bool, level: Float = 1.0) throws {
278
- guard let camera = currentCameraDevice else { throw CameraError.cameraUnavailable }
370
+ guard let camera = currentCameraDevice else {
371
+ throw CameraError.cameraUnavailable
372
+ }
279
373
  guard camera.hasTorch else { throw CameraError.torchUnavailable }
280
374
 
281
375
  do {
@@ -297,7 +391,9 @@ internal let SUPPORTED_CAMERA_DEVICE_TYPES: [AVCaptureDevice.DeviceType] = [
297
391
  /// because some devices report very high zoom factors that aren't useful.
298
392
  ///
299
393
  /// - Returns: A tuple containing the minimum, maximum, and current zoom factors.
300
- public func getSupportedZoomFactors() -> (min: CGFloat, max: CGFloat, current: CGFloat) {
394
+ public func getSupportedZoomFactors() -> (
395
+ min: CGFloat, max: CGFloat, current: CGFloat
396
+ ) {
301
397
  guard let currentDevice = currentCameraDevice else {
302
398
  return (
303
399
  min: 1.0,
@@ -307,7 +403,10 @@ internal let SUPPORTED_CAMERA_DEVICE_TYPES: [AVCaptureDevice.DeviceType] = [
307
403
  }
308
404
 
309
405
  let minZoomFactor = currentDevice.minAvailableVideoZoomFactor
310
- let maxZoomFactor = min(currentDevice.activeFormat.videoMaxZoomFactor, 10.0)
406
+ let maxZoomFactor = min(
407
+ currentDevice.activeFormat.videoMaxZoomFactor,
408
+ 10.0
409
+ )
311
410
  let currentZoomFactor = currentDevice.videoZoomFactor
312
411
 
313
412
  return (
@@ -324,10 +423,15 @@ internal let SUPPORTED_CAMERA_DEVICE_TYPES: [AVCaptureDevice.DeviceType] = [
324
423
  /// - ramp: If enabled the zoom will be applied via ramp
325
424
  /// - Throws: An error if the zoom factor cannot be set.
326
425
  public func setZoomFactor(_ factor: CGFloat, ramp: Bool = true) throws {
327
- guard let device = currentCameraDevice else { throw CameraError.cameraUnavailable }
426
+ guard let device = currentCameraDevice else {
427
+ throw CameraError.cameraUnavailable
428
+ }
328
429
 
329
430
  let supportedZoomFactors = getSupportedZoomFactors()
330
- guard factor >= supportedZoomFactors.min && factor <= supportedZoomFactors.max else {
431
+ guard
432
+ factor >= supportedZoomFactors.min
433
+ && factor <= supportedZoomFactors.max
434
+ else {
331
435
  throw CameraError.zoomFactorOutOfRange
332
436
  }
333
437
 
@@ -349,7 +453,9 @@ internal let SUPPORTED_CAMERA_DEVICE_TYPES: [AVCaptureDevice.DeviceType] = [
349
453
  ///
350
454
  /// - Parameters:
351
455
  /// - configuration: The configuration object for the camera session.
352
- private func initiateCaptureSession(configuration: CameraSessionConfiguration) throws {
456
+ private func initiateCaptureSession(
457
+ configuration: CameraSessionConfiguration
458
+ ) throws {
353
459
  captureSession.beginConfiguration()
354
460
  defer { captureSession.commitConfiguration() }
355
461
 
@@ -418,7 +524,6 @@ internal let SUPPORTED_CAMERA_DEVICE_TYPES: [AVCaptureDevice.DeviceType] = [
418
524
  }
419
525
  }
420
526
 
421
-
422
527
  /// Enables barcode detection by adding metadata output to the running session.
423
528
  /// Somehow adding the metadata output with the session not being started yet
424
529
  /// caused issues on some devices (iPad 7th Gen) where the session would just
@@ -426,8 +531,9 @@ internal let SUPPORTED_CAMERA_DEVICE_TYPES: [AVCaptureDevice.DeviceType] = [
426
531
  /// figure out the root cause of this but it might generally be a good idea
427
532
  /// to only add the metadata output when the session is already running.
428
533
  ///
534
+ /// - Parameter barcodeTypes: Optional array of barcode types to detect. If nil, all supported types are used.
429
535
  /// - Throws: An error if the metadata output cannot be added.
430
- private func enableBarcodeDetection() throws {
536
+ private func enableBarcodeDetection(barcodeTypes: [AVMetadataObject.ObjectType]? = nil) throws {
431
537
  guard captureSession.isRunning else {
432
538
  throw CameraError.sessionNotRunning
433
539
  }
@@ -435,7 +541,7 @@ internal let SUPPORTED_CAMERA_DEVICE_TYPES: [AVCaptureDevice.DeviceType] = [
435
541
  captureSession.beginConfiguration()
436
542
  defer { captureSession.commitConfiguration() }
437
543
 
438
- try setupMetadataOutput()
544
+ try setupMetadataOutput(barcodeTypes: barcodeTypes)
439
545
  }
440
546
 
441
547
  /// Retrieve a list of a available camera devices
@@ -468,11 +574,14 @@ internal let SUPPORTED_CAMERA_DEVICE_TYPES: [AVCaptureDevice.DeviceType] = [
468
574
  /// - position: The position of the camera device to get
469
575
  /// - Returns: The camera device for the specified position
470
576
  /// - Throws: An error if no camera device is found.
471
- private func getCameraDevice(for position: AVCaptureDevice.Position?) throws -> AVCaptureDevice {
577
+ private func getCameraDevice(for position: AVCaptureDevice.Position?) throws
578
+ -> AVCaptureDevice
579
+ {
472
580
  let preferredDevices = getPreferredCameraDevices()
473
581
 
474
582
  // First try to get the best match based on the users preferred camera device types
475
- if let match = preferredDevices.first(where: { $0.position == position }) {
583
+ if let match = preferredDevices.first(where: { $0.position == position }
584
+ ) {
476
585
  return match
477
586
  }
478
587
 
@@ -480,7 +589,10 @@ internal let SUPPORTED_CAMERA_DEVICE_TYPES: [AVCaptureDevice.DeviceType] = [
480
589
  // Only doing this when preferredCameraDeviceTypes size differs from SUPPORTED_CAMERA_DEVICE_TYPES, otherwise
481
590
  // we don't have to initialize a new discovery session
482
591
  if preferredCameraDeviceTypes.count < SUPPORTED_CAMERA_DEVICE_TYPES.count,
483
- let match = getAvailableDevices().first(where: { $0.position == position }) {
592
+ let match = getAvailableDevices().first(where: {
593
+ $0.position == position
594
+ })
595
+ {
484
596
  return match
485
597
  }
486
598
 
@@ -490,8 +602,11 @@ internal let SUPPORTED_CAMERA_DEVICE_TYPES: [AVCaptureDevice.DeviceType] = [
490
602
  }
491
603
 
492
604
  // Log when we're falling back to a device with different position than requested
493
- if let requestedPosition = position, device.position != requestedPosition {
494
- print("Warning: Falling back to camera at position \(device.position) when \(requestedPosition) was requested")
605
+ if let requestedPosition = position, device.position != requestedPosition
606
+ {
607
+ print(
608
+ "Warning: Falling back to camera at position \(device.position) when \(requestedPosition) was requested"
609
+ )
495
610
  }
496
611
 
497
612
  return device
@@ -503,8 +618,14 @@ internal let SUPPORTED_CAMERA_DEVICE_TYPES: [AVCaptureDevice.DeviceType] = [
503
618
  /// - deviceId: The unique identifier of the camera device to get
504
619
  /// - Returns: The camera device for the specified position
505
620
  /// - Throws: An error if no camera device is found.
506
- private func getCameraDeviceById(_ deviceId: String) throws -> AVCaptureDevice {
507
- guard let device = getAvailableDevices().first(where: { $0.uniqueID == deviceId }) else {
621
+ private func getCameraDeviceById(_ deviceId: String) throws
622
+ -> AVCaptureDevice
623
+ {
624
+ guard
625
+ let device = getAvailableDevices().first(where: {
626
+ $0.uniqueID == deviceId
627
+ })
628
+ else {
508
629
  throw CameraError.cameraUnavailable
509
630
  }
510
631
  return device
@@ -519,7 +640,10 @@ internal let SUPPORTED_CAMERA_DEVICE_TYPES: [AVCaptureDevice.DeviceType] = [
519
640
  /// - view: The view that will display the camera preview.
520
641
  /// - completion: The completion handler after successfully adding the previewLayer to the provided view
521
642
  /// - Throws: An error if the preview layer cannot be set up.
522
- private func displayPreview(on view: UIView, completion: @escaping (Error?) -> Void) {
643
+ private func displayPreview(
644
+ on view: UIView,
645
+ completion: @escaping (Error?) -> Void
646
+ ) {
523
647
  guard captureSession.isRunning else {
524
648
  completion(CameraError.sessionNotRunning)
525
649
  return
@@ -580,7 +704,9 @@ internal let SUPPORTED_CAMERA_DEVICE_TYPES: [AVCaptureDevice.DeviceType] = [
580
704
  try self.setZoomFactor(2.0, ramp: false)
581
705
  } catch {
582
706
  // Fail silently if we can't upgrade to the triple camera
583
- print("Failed to upgrade to triple camera: \(error.localizedDescription)")
707
+ print(
708
+ "Failed to upgrade to triple camera: \(error.localizedDescription)"
709
+ )
584
710
  }
585
711
 
586
712
  self.captureSession.commitConfiguration()
@@ -612,7 +738,9 @@ internal let SUPPORTED_CAMERA_DEVICE_TYPES: [AVCaptureDevice.DeviceType] = [
612
738
  /// Removes the blur overlay with a fade out animation to have a smooth transition
613
739
  /// - Parameter duration: The duration of the fade out animation
614
740
  @MainActor
615
- private func removeBlurOverlayWithAnimation(duration: TimeInterval = 0.3) async {
741
+ private func removeBlurOverlayWithAnimation(duration: TimeInterval = 0.3)
742
+ async
743
+ {
616
744
  guard let blurEffectView = blurOverlayView else { return }
617
745
 
618
746
  await withCheckedContinuation { continuation in
@@ -625,7 +753,8 @@ internal let SUPPORTED_CAMERA_DEVICE_TYPES: [AVCaptureDevice.DeviceType] = [
625
753
  blurEffectView.removeFromSuperview()
626
754
  self.blurOverlayView = nil
627
755
  continuation.resume()
628
- })
756
+ }
757
+ )
629
758
  }
630
759
  }
631
760
 
@@ -644,11 +773,15 @@ internal let SUPPORTED_CAMERA_DEVICE_TYPES: [AVCaptureDevice.DeviceType] = [
644
773
 
645
774
  /// Updates the preview layer orientation based on the current device orientation.
646
775
  private func updatePreviewOrientation() {
647
- guard let connection = self.videoPreviewLayer.connection, connection.isVideoOrientationSupported else {
776
+ guard let connection = self.videoPreviewLayer.connection,
777
+ connection.isVideoOrientationSupported
778
+ else {
648
779
  return
649
780
  }
650
781
 
651
- let interfaceOrientation = UIApplication.shared.windows.first?.windowScene?.interfaceOrientation ?? .portrait
782
+ let interfaceOrientation = UIApplication.shared.connectedScenes
783
+ .compactMap({ $0 as? UIWindowScene })
784
+ .first?.interfaceOrientation ?? .portrait
652
785
  let videoOrientation: AVCaptureVideoOrientation
653
786
 
654
787
  switch interfaceOrientation {
@@ -688,14 +821,15 @@ internal let SUPPORTED_CAMERA_DEVICE_TYPES: [AVCaptureDevice.DeviceType] = [
688
821
  self,
689
822
  selector: #selector(handleAppDidBecomeActive),
690
823
  name: UIApplication.didBecomeActiveNotification,
691
- object: nil)
824
+ object: nil
825
+ )
692
826
  }
693
827
 
694
828
  /// Handles the app going to background by pausing the camera session.
695
829
  @objc private func handleAppWillResignActive() {
696
830
  // Pause the session when app goes to background to save resources
697
831
  if captureSession.isRunning {
698
- DispatchQueue.global(qos: .userInitiated).async { [weak self] in
832
+ sessionQueue.async { [weak self] in
699
833
  self?.captureSession.stopRunning()
700
834
  }
701
835
  }
@@ -705,7 +839,7 @@ internal let SUPPORTED_CAMERA_DEVICE_TYPES: [AVCaptureDevice.DeviceType] = [
705
839
  @objc private func handleAppDidBecomeActive() {
706
840
  // Resume the session when app comes back to foreground
707
841
  if !captureSession.isRunning && webView != nil {
708
- DispatchQueue.global(qos: .userInitiated).async { [weak self] in
842
+ sessionQueue.async { [weak self] in
709
843
  self?.captureSession.startRunning()
710
844
  }
711
845
  }