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.
- package/README.md +215 -19
- package/android/build.gradle +9 -5
- package/android/src/main/AndroidManifest.xml +1 -0
- package/android/src/main/java/com/michaelwolz/capacitorcameraview/CameraView.kt +491 -116
- package/android/src/main/java/com/michaelwolz/capacitorcameraview/CameraViewPlugin.kt +181 -31
- package/android/src/main/java/com/michaelwolz/capacitorcameraview/model/CameraResult.kt +47 -0
- package/android/src/main/java/com/michaelwolz/capacitorcameraview/model/CameraSessionConfiguration.kt +11 -1
- package/android/src/main/java/com/michaelwolz/capacitorcameraview/model/VideoRecordingQuality.kt +10 -0
- package/android/src/main/java/com/michaelwolz/capacitorcameraview/utils.kt +114 -5
- package/dist/docs.json +281 -8
- package/dist/esm/definitions.d.ts +128 -6
- package/dist/esm/definitions.js.map +1 -1
- package/dist/esm/web.d.ts +26 -4
- package/dist/esm/web.js +218 -18
- package/dist/esm/web.js.map +1 -1
- package/dist/plugin.cjs.js +219 -18
- package/dist/plugin.cjs.js.map +1 -1
- package/dist/plugin.js +219 -18
- package/dist/plugin.js.map +1 -1
- package/ios/Sources/CameraViewPlugin/CameraError.swift +125 -2
- package/ios/Sources/CameraViewPlugin/CameraEvents.swift +109 -0
- package/ios/Sources/CameraViewPlugin/CameraSessionConfiguration.swift +28 -1
- package/ios/Sources/CameraViewPlugin/CameraViewManager+BarcodeScan.swift +30 -41
- package/ios/Sources/CameraViewPlugin/CameraViewManager+PhotoCapture.swift +38 -7
- package/ios/Sources/CameraViewPlugin/CameraViewManager+VideoDataOutput.swift +4 -3
- package/ios/Sources/CameraViewPlugin/CameraViewManager+VideoRecording.swift +302 -0
- package/ios/Sources/CameraViewPlugin/CameraViewManager.swift +246 -166
- package/ios/Sources/CameraViewPlugin/CameraViewPlugin.swift +194 -96
- package/ios/Sources/CameraViewPlugin/TempFileManager.swift +215 -0
- package/ios/Sources/CameraViewPlugin/Utils.swift +102 -0
- 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
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
316
|
+
currentCameraDevice?.position ?? .back
|
|
231
317
|
let newPosition: AVCaptureDevice.Position =
|
|
232
|
-
|
|
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
|
-
|
|
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
|
-
|
|
520
|
-
|
|
521
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
786
|
+
connection.isVideoOrientationSupported
|
|
707
787
|
else {
|
|
708
788
|
return
|
|
709
789
|
}
|
|
710
|
-
|
|
711
|
-
let interfaceOrientation =
|
|
712
|
-
|
|
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
|
-
|
|
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
|
-
|
|
851
|
+
sessionQueue.async { [weak self] in
|
|
772
852
|
self?.captureSession.startRunning()
|
|
773
853
|
}
|
|
774
854
|
}
|