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