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