@capgo/camera-preview 7.4.0-beta.10 → 7.4.0-beta.11

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.
@@ -24,6 +24,8 @@ class CameraController: NSObject {
24
24
  var rearCamera: AVCaptureDevice?
25
25
  var rearCameraInput: AVCaptureDeviceInput?
26
26
 
27
+ var allDiscoveredDevices: [AVCaptureDevice] = []
28
+
27
29
  var fileVideoOutput: AVCaptureMovieFileOutput?
28
30
 
29
31
  var previewLayer: AVCaptureVideoPreviewLayer?
@@ -54,16 +56,30 @@ class CameraController: NSObject {
54
56
  }
55
57
 
56
58
  extension CameraController {
57
- func prepareBasicSession() {
59
+ func prepareFullSession() {
58
60
  // Only prepare if we don't already have a session
59
61
  guard self.captureSession == nil else { return }
60
-
61
- print("[CameraPreview] Preparing basic camera session in background")
62
-
63
- // Create basic capture session
62
+
63
+ print("[CameraPreview] Preparing full camera session in background")
64
+
65
+ // 1. Create and configure session
64
66
  self.captureSession = AVCaptureSession()
65
-
66
- // Configure basic devices without full preparation
67
+
68
+ // 2. Pre-configure session preset (can be changed later)
69
+ if captureSession!.canSetSessionPreset(.medium) {
70
+ captureSession!.sessionPreset = .medium
71
+ }
72
+
73
+ // 3. Discover and configure all cameras
74
+ discoverAndConfigureCameras()
75
+
76
+ // 4. Pre-create outputs (don't add to session yet)
77
+ prepareOutputs()
78
+
79
+ print("[CameraPreview] Full session preparation complete")
80
+ }
81
+
82
+ private func discoverAndConfigureCameras() {
67
83
  let deviceTypes: [AVCaptureDevice.DeviceType] = [
68
84
  .builtInWideAngleCamera,
69
85
  .builtInUltraWideCamera,
@@ -76,295 +92,206 @@ extension CameraController {
76
92
 
77
93
  let session = AVCaptureDevice.DiscoverySession(deviceTypes: deviceTypes, mediaType: AVMediaType.video, position: .unspecified)
78
94
  let cameras = session.devices.compactMap { $0 }
79
-
95
+
96
+ // Store all discovered devices for fast lookup later
97
+ self.allDiscoveredDevices = cameras
98
+
99
+ // Log all found devices for debugging
100
+ print("[CameraPreview] Found \(cameras.count) devices:")
101
+ for camera in cameras {
102
+ let constituentCount = camera.isVirtualDevice ? camera.constituentDevices.count : 1
103
+ print("[CameraPreview] - \(camera.localizedName) (Position: \(camera.position.rawValue), Virtual: \(camera.isVirtualDevice), Lenses: \(constituentCount))")
104
+ }
105
+
80
106
  // Find best cameras
81
107
  let rearVirtualDevices = cameras.filter { $0.position == .back && $0.isVirtualDevice }
82
108
  let bestRearVirtualDevice = rearVirtualDevices.max { $0.constituentDevices.count < $1.constituentDevices.count }
83
-
109
+
84
110
  self.frontCamera = cameras.first(where: { $0.position == .front })
85
-
111
+
86
112
  if let bestCamera = bestRearVirtualDevice {
87
113
  self.rearCamera = bestCamera
114
+ print("[CameraPreview] Selected best virtual rear camera: \(bestCamera.localizedName) with \(bestCamera.constituentDevices.count) physical cameras.")
88
115
  } else if let firstRearCamera = cameras.first(where: { $0.position == .back }) {
89
116
  self.rearCamera = firstRearCamera
117
+ print("[CameraPreview] WARN: No virtual rear camera found. Selected first available: \(firstRearCamera.localizedName)")
90
118
  }
91
-
92
- print("[CameraPreview] Basic session prepared with \(cameras.count) devices")
119
+
120
+ // Pre-configure focus modes
121
+ configureCameraFocus(camera: self.rearCamera)
122
+ configureCameraFocus(camera: self.frontCamera)
93
123
  }
94
124
 
95
- func prepare(cameraPosition: String, deviceId: String? = nil, disableAudio: Bool, cameraMode: Bool, aspectRatio: String? = nil, completionHandler: @escaping (Error?) -> Void) {
96
- func createCaptureSession() {
97
- // Use existing session if available from background preparation
98
- if self.captureSession == nil {
99
- self.captureSession = AVCaptureSession()
125
+ private func configureCameraFocus(camera: AVCaptureDevice?) {
126
+ guard let camera = camera else { return }
127
+
128
+ do {
129
+ try camera.lockForConfiguration()
130
+ if camera.isFocusModeSupported(.continuousAutoFocus) {
131
+ camera.focusMode = .continuousAutoFocus
100
132
  }
133
+ camera.unlockForConfiguration()
134
+ } catch {
135
+ print("[CameraPreview] Could not configure focus for \(camera.localizedName): \(error)")
101
136
  }
137
+ }
102
138
 
103
- func configureCaptureDevices() throws {
104
- // Skip device discovery if cameras are already found during background preparation
105
- if self.frontCamera != nil || self.rearCamera != nil {
106
- print("[CameraPreview] Using pre-discovered cameras")
107
- return
108
- }
109
-
110
- // Expanded device types to support more camera configurations
111
- let deviceTypes: [AVCaptureDevice.DeviceType] = [
112
- .builtInWideAngleCamera,
113
- .builtInUltraWideCamera,
114
- .builtInTelephotoCamera,
115
- .builtInDualCamera,
116
- .builtInDualWideCamera,
117
- .builtInTripleCamera,
118
- .builtInTrueDepthCamera
119
- ]
120
-
121
- let session = AVCaptureDevice.DiscoverySession(deviceTypes: deviceTypes, mediaType: AVMediaType.video, position: .unspecified)
122
-
123
- let cameras = session.devices.compactMap { $0 }
124
-
125
- // Log all found devices for debugging
126
- print("[CameraPreview] Found \(cameras.count) devices:")
127
- for camera in cameras {
128
- let constituentCount = camera.isVirtualDevice ? camera.constituentDevices.count : 1
129
- print("[CameraPreview] - \(camera.localizedName) (Position: \(camera.position.rawValue), Virtual: \(camera.isVirtualDevice), Lenses: \(constituentCount), Zoom: \(camera.minAvailableVideoZoomFactor)-\(camera.maxAvailableVideoZoomFactor))")
130
- }
139
+ private func prepareOutputs() {
140
+ // Pre-create photo output
141
+ self.photoOutput = AVCapturePhotoOutput()
142
+ self.photoOutput?.isHighResolutionCaptureEnabled = false // Default, can be changed
131
143
 
132
- guard !cameras.isEmpty else {
133
- print("[CameraPreview] ERROR: No cameras found.")
134
- throw CameraControllerError.noCamerasAvailable
135
- }
144
+ // Pre-create video output
145
+ self.fileVideoOutput = AVCaptureMovieFileOutput()
136
146
 
137
- // --- Corrected Device Selection Logic ---
138
- // Find the virtual device with the most constituent cameras (this is the most capable one)
139
- let rearVirtualDevices = cameras.filter { $0.position == .back && $0.isVirtualDevice }
140
- let bestRearVirtualDevice = rearVirtualDevices.max { $0.constituentDevices.count < $1.constituentDevices.count }
147
+ // Pre-create data output
148
+ self.dataOutput = AVCaptureVideoDataOutput()
149
+ self.dataOutput?.videoSettings = [
150
+ (kCVPixelBufferPixelFormatTypeKey as String): NSNumber(value: kCVPixelFormatType_32BGRA as UInt32)
151
+ ]
152
+ self.dataOutput?.alwaysDiscardsLateVideoFrames = true
141
153
 
142
- self.frontCamera = cameras.first(where: { $0.position == .front })
154
+ print("[CameraPreview] Outputs pre-created")
155
+ }
143
156
 
144
- if let bestCamera = bestRearVirtualDevice {
145
- self.rearCamera = bestCamera
146
- print("[CameraPreview] Selected best virtual rear camera: \(bestCamera.localizedName) with \(bestCamera.constituentDevices.count) physical cameras.")
147
- } else if let firstRearCamera = cameras.first(where: { $0.position == .back }) {
148
- // Fallback for devices without a virtual camera system
149
- self.rearCamera = firstRearCamera
150
- print("[CameraPreview] WARN: No virtual rear camera found. Selected first available: \(firstRearCamera.localizedName)")
157
+ func prepare(cameraPosition: String, deviceId: String? = nil, disableAudio: Bool, cameraMode: Bool, aspectRatio: String? = nil, completionHandler: @escaping (Error?) -> Void) {
158
+ do {
159
+ // Session and outputs already created in load(), just configure user-specific settings
160
+ if self.captureSession == nil {
161
+ // Fallback if prepareFullSession() wasn't called
162
+ self.prepareFullSession()
151
163
  }
152
- // --- End of Correction ---
153
164
 
154
- if let rearCamera = self.rearCamera {
155
- do {
156
- try rearCamera.lockForConfiguration()
157
- if rearCamera.isFocusModeSupported(.continuousAutoFocus) {
158
- rearCamera.focusMode = .continuousAutoFocus
159
- }
160
- rearCamera.unlockForConfiguration()
161
- } catch {
162
- print("[CameraPreview] WARN: Could not set focus mode on rear camera. \(error)")
163
- }
165
+ guard let captureSession = self.captureSession else {
166
+ throw CameraControllerError.captureSessionIsMissing
164
167
  }
165
168
 
166
- if disableAudio == false {
167
- self.audioDevice = AVCaptureDevice.default(for: AVMediaType.audio)
168
- }
169
- }
169
+ print("[CameraPreview] Fast prepare - using pre-initialized session")
170
170
 
171
- func configureDeviceInputs() throws {
172
- guard let captureSession = self.captureSession else { throw CameraControllerError.captureSessionIsMissing }
171
+ // Configure device inputs for the requested camera
172
+ try self.configureDeviceInputs(cameraPosition: cameraPosition, deviceId: deviceId, disableAudio: disableAudio)
173
173
 
174
- var selectedDevice: AVCaptureDevice?
174
+ // Add outputs to session and apply user settings
175
+ try self.addOutputsToSession(cameraMode: cameraMode, aspectRatio: aspectRatio)
175
176
 
176
- // If deviceId is specified, use that specific device
177
- if let deviceId = deviceId {
178
- let allDevices = AVCaptureDevice.DiscoverySession(
179
- deviceTypes: [.builtInWideAngleCamera, .builtInUltraWideCamera, .builtInTelephotoCamera, .builtInDualCamera, .builtInDualWideCamera, .builtInTripleCamera, .builtInTrueDepthCamera],
180
- mediaType: .video,
181
- position: .unspecified
182
- ).devices
177
+ // Start the session
178
+ captureSession.startRunning()
179
+ print("[CameraPreview] Session started")
183
180
 
184
- selectedDevice = allDevices.first(where: { $0.uniqueID == deviceId })
185
- guard selectedDevice != nil else {
186
- throw CameraControllerError.noCamerasAvailable
187
- }
188
- } else {
189
- // Use position-based selection
190
- if cameraPosition == "rear" {
191
- selectedDevice = self.rearCamera
192
- } else if cameraPosition == "front" {
193
- selectedDevice = self.frontCamera
194
- }
195
- }
181
+ completionHandler(nil)
182
+ } catch {
183
+ completionHandler(error)
184
+ }
185
+ }
186
+
187
+ private func configureDeviceInputs(cameraPosition: String, deviceId: String?, disableAudio: Bool) throws {
188
+ guard let captureSession = self.captureSession else { throw CameraControllerError.captureSessionIsMissing }
189
+
190
+ var selectedDevice: AVCaptureDevice?
196
191
 
197
- guard let finalDevice = selectedDevice else {
198
- print("[CameraPreview] ERROR: No camera device selected for position: \(cameraPosition)")
192
+ // If deviceId is specified, find that specific device from pre-discovered devices
193
+ if let deviceId = deviceId {
194
+ selectedDevice = self.allDiscoveredDevices.first(where: { $0.uniqueID == deviceId })
195
+ guard selectedDevice != nil else {
196
+ print("[CameraPreview] ERROR: Device with ID \(deviceId) not found in pre-discovered devices")
199
197
  throw CameraControllerError.noCamerasAvailable
200
198
  }
199
+ } else {
200
+ // Use position-based selection from pre-discovered cameras
201
+ if cameraPosition == "rear" {
202
+ selectedDevice = self.rearCamera
203
+ } else if cameraPosition == "front" {
204
+ selectedDevice = self.frontCamera
205
+ }
206
+ }
201
207
 
202
- print("[CameraPreview] Configuring device: \(finalDevice.localizedName)")
203
- let deviceInput = try AVCaptureDeviceInput(device: finalDevice)
208
+ guard let finalDevice = selectedDevice else {
209
+ print("[CameraPreview] ERROR: No camera device selected for position: \(cameraPosition)")
210
+ throw CameraControllerError.noCamerasAvailable
211
+ }
204
212
 
205
- if captureSession.canAddInput(deviceInput) {
206
- captureSession.addInput(deviceInput)
213
+ print("[CameraPreview] Configuring device: \(finalDevice.localizedName)")
214
+ let deviceInput = try AVCaptureDeviceInput(device: finalDevice)
207
215
 
208
- if finalDevice.position == .front {
209
- self.frontCameraInput = deviceInput
210
- self.frontCamera = finalDevice
211
- self.currentCameraPosition = .front
212
- } else {
213
- self.rearCameraInput = deviceInput
214
- self.rearCamera = finalDevice
215
- self.currentCameraPosition = .rear
216
-
217
- // --- Corrected Initial Zoom Logic ---
218
- try finalDevice.lockForConfiguration()
219
- if finalDevice.isFocusModeSupported(.continuousAutoFocus) {
220
- finalDevice.focusMode = .continuousAutoFocus
221
- }
216
+ if captureSession.canAddInput(deviceInput) {
217
+ captureSession.addInput(deviceInput)
222
218
 
223
- // On a multi-camera system, a zoom factor of 2.0 often corresponds to the standard "1x" wide-angle lens.
224
- // We set this as the default to provide a familiar starting point for users.
225
- let defaultWideAngleZoom: CGFloat = 2.0
226
- if finalDevice.isVirtualDevice && finalDevice.constituentDevices.count > 1 && finalDevice.videoZoomFactor != defaultWideAngleZoom {
227
- // Check if 2.0 is a valid zoom factor before setting it.
228
- if defaultWideAngleZoom >= finalDevice.minAvailableVideoZoomFactor && defaultWideAngleZoom <= finalDevice.maxAvailableVideoZoomFactor {
229
- print("[CameraPreview] Multi-camera system detected. Setting initial zoom to \(defaultWideAngleZoom) (standard wide-angle).")
230
- finalDevice.videoZoomFactor = defaultWideAngleZoom
231
- }
232
- }
233
- finalDevice.unlockForConfiguration()
234
- // --- End of Correction ---
235
- }
219
+ if finalDevice.position == .front {
220
+ self.frontCameraInput = deviceInput
221
+ self.currentCameraPosition = .front
236
222
  } else {
237
- print("[CameraPreview] ERROR: Cannot add device input to session.")
238
- throw CameraControllerError.inputsAreInvalid
239
- }
223
+ self.rearCameraInput = deviceInput
224
+ self.currentCameraPosition = .rear
240
225
 
241
- // Add audio input
242
- if disableAudio == false {
243
- if let audioDevice = self.audioDevice {
244
- self.audioInput = try AVCaptureDeviceInput(device: audioDevice)
245
- if captureSession.canAddInput(self.audioInput!) {
246
- captureSession.addInput(self.audioInput!)
247
- } else {
248
- throw CameraControllerError.inputsAreInvalid
226
+ // Configure zoom for multi-camera systems
227
+ try finalDevice.lockForConfiguration()
228
+ let defaultWideAngleZoom: CGFloat = 2.0
229
+ if finalDevice.isVirtualDevice && finalDevice.constituentDevices.count > 1 && finalDevice.videoZoomFactor != defaultWideAngleZoom {
230
+ if defaultWideAngleZoom >= finalDevice.minAvailableVideoZoomFactor && defaultWideAngleZoom <= finalDevice.maxAvailableVideoZoomFactor {
231
+ print("[CameraPreview] Setting initial zoom to \(defaultWideAngleZoom)")
232
+ finalDevice.videoZoomFactor = defaultWideAngleZoom
249
233
  }
250
234
  }
235
+ finalDevice.unlockForConfiguration()
251
236
  }
237
+ } else {
238
+ throw CameraControllerError.inputsAreInvalid
252
239
  }
253
240
 
254
- func configurePhotoOutput(cameraMode: Bool) throws {
255
- guard let captureSession = self.captureSession else { throw CameraControllerError.captureSessionIsMissing }
256
-
257
- // Configure session preset for high-quality preview
258
- // Prioritize higher quality presets for better preview resolution
259
- var targetPreset: AVCaptureSession.Preset = .high // Default to high quality
260
-
261
- if let aspectRatio = aspectRatio {
262
- switch aspectRatio {
263
- case "16:9":
264
- // Use highest available HD preset for 16:9 aspect ratio
265
- if captureSession.canSetSessionPreset(.hd4K3840x2160) {
266
- targetPreset = .hd4K3840x2160
267
- } else if captureSession.canSetSessionPreset(.hd1920x1080) {
268
- targetPreset = .hd1920x1080
269
- } else if captureSession.canSetSessionPreset(.hd1280x720) {
270
- targetPreset = .hd1280x720
271
- } else {
272
- targetPreset = .high
273
- }
274
- case "4:3":
275
- // Use photo preset for 4:3 aspect ratio (highest quality)
276
- if captureSession.canSetSessionPreset(.photo) {
277
- targetPreset = .photo
278
- } else if captureSession.canSetSessionPreset(.high) {
279
- targetPreset = .high
280
- } else {
281
- targetPreset = .medium
282
- }
283
- default:
284
- // Default to highest available quality
285
- if captureSession.canSetSessionPreset(.photo) {
286
- targetPreset = .photo
287
- } else if captureSession.canSetSessionPreset(.high) {
288
- targetPreset = .high
289
- } else {
290
- targetPreset = .medium
291
- }
292
- }
293
- } else {
294
- // Default to highest available quality when no aspect ratio specified
295
- if captureSession.canSetSessionPreset(.photo) {
296
- targetPreset = .photo
297
- } else if captureSession.canSetSessionPreset(.high) {
298
- targetPreset = .high
241
+ // Add audio input if needed
242
+ if !disableAudio {
243
+ if self.audioDevice == nil {
244
+ self.audioDevice = AVCaptureDevice.default(for: AVMediaType.audio)
245
+ }
246
+ if let audioDevice = self.audioDevice {
247
+ self.audioInput = try AVCaptureDeviceInput(device: audioDevice)
248
+ if captureSession.canAddInput(self.audioInput!) {
249
+ captureSession.addInput(self.audioInput!)
299
250
  } else {
300
- targetPreset = .medium
251
+ throw CameraControllerError.inputsAreInvalid
301
252
  }
302
253
  }
254
+ }
255
+ }
303
256
 
304
- // Apply the determined preset
305
- if captureSession.canSetSessionPreset(targetPreset) {
306
- captureSession.sessionPreset = targetPreset
307
- print("[CameraPreview] Set session preset to \(targetPreset) for aspect ratio: \(aspectRatio ?? "default")")
308
- } else {
309
- // Fallback to high quality preset if the target preset is not supported
310
- print("[CameraPreview] Target preset \(targetPreset) not supported, falling back to .high")
311
- if captureSession.canSetSessionPreset(.high) {
312
- captureSession.sessionPreset = .high
313
- } else if captureSession.canSetSessionPreset(.medium) {
314
- captureSession.sessionPreset = .medium
315
- }
257
+ private func addOutputsToSession(cameraMode: Bool, aspectRatio: String?) throws {
258
+ guard let captureSession = self.captureSession else { throw CameraControllerError.captureSessionIsMissing }
259
+
260
+ // Update session preset based on aspect ratio if needed
261
+ if let aspectRatio = aspectRatio {
262
+ var targetPreset: AVCaptureSession.Preset
263
+ switch aspectRatio {
264
+ case "16:9":
265
+ targetPreset = captureSession.canSetSessionPreset(.hd1920x1080) ? .hd1920x1080 : .high
266
+ case "4:3":
267
+ targetPreset = captureSession.canSetSessionPreset(.photo) ? .photo : .high
268
+ default:
269
+ targetPreset = .high
316
270
  }
317
271
 
318
- self.photoOutput = AVCapturePhotoOutput()
319
- self.photoOutput!.setPreparedPhotoSettingsArray([AVCapturePhotoSettings(format: [AVVideoCodecKey: AVVideoCodecType.jpeg])], completionHandler: nil)
320
- self.photoOutput?.isHighResolutionCaptureEnabled = self.highResolutionOutput
321
- if captureSession.canAddOutput(self.photoOutput!) { captureSession.addOutput(self.photoOutput!) }
322
-
323
- let fileVideoOutput = AVCaptureMovieFileOutput()
324
- if captureSession.canAddOutput(fileVideoOutput) {
325
- captureSession.addOutput(fileVideoOutput)
326
- self.fileVideoOutput = fileVideoOutput
272
+ if captureSession.canSetSessionPreset(targetPreset) {
273
+ captureSession.sessionPreset = targetPreset
274
+ print("[CameraPreview] Updated preset to \(targetPreset) for aspect ratio: \(aspectRatio)")
327
275
  }
328
- captureSession.startRunning()
329
276
  }
330
277
 
331
- func configureDataOutput() throws {
332
- guard let captureSession = self.captureSession else { throw CameraControllerError.captureSessionIsMissing }
333
-
334
- self.dataOutput = AVCaptureVideoDataOutput()
335
- self.dataOutput?.videoSettings = [
336
- (kCVPixelBufferPixelFormatTypeKey as String): NSNumber(value: kCVPixelFormatType_32BGRA as UInt32)
337
- ]
338
- self.dataOutput?.alwaysDiscardsLateVideoFrames = true
339
- if captureSession.canAddOutput(self.dataOutput!) {
340
- captureSession.addOutput(self.dataOutput!)
341
- }
342
-
343
- captureSession.commitConfiguration()
344
-
345
- let queue = DispatchQueue(label: "DataOutput", attributes: [])
346
- self.dataOutput?.setSampleBufferDelegate(self, queue: queue)
278
+ // Add photo output (already created in prepareOutputs)
279
+ if let photoOutput = self.photoOutput, captureSession.canAddOutput(photoOutput) {
280
+ photoOutput.isHighResolutionCaptureEnabled = self.highResolutionOutput
281
+ captureSession.addOutput(photoOutput)
347
282
  }
348
283
 
349
- DispatchQueue(label: "prepare").async {
350
- do {
351
- createCaptureSession()
352
- try configureCaptureDevices()
353
- try configureDeviceInputs()
354
- try configurePhotoOutput(cameraMode: cameraMode)
355
- try configureDataOutput()
356
- // try configureVideoOutput()
357
- } catch {
358
- DispatchQueue.main.async {
359
- completionHandler(error)
360
- }
284
+ // Add video output only if camera mode is enabled
285
+ if cameraMode, let videoOutput = self.fileVideoOutput, captureSession.canAddOutput(videoOutput) {
286
+ captureSession.addOutput(videoOutput)
287
+ }
361
288
 
362
- return
363
- }
289
+ // Add data output
290
+ if let dataOutput = self.dataOutput, captureSession.canAddOutput(dataOutput) {
291
+ captureSession.addOutput(dataOutput)
292
+ captureSession.commitConfiguration()
364
293
 
365
- DispatchQueue.main.async {
366
- completionHandler(nil)
367
- }
294
+ dataOutput.setSampleBufferDelegate(self, queue: DispatchQueue.main)
368
295
  }
369
296
  }
370
297
 
@@ -386,7 +313,7 @@ extension CameraController {
386
313
  }
387
314
 
388
315
  view.layer.insertSublayer(self.previewLayer!, at: 0)
389
-
316
+
390
317
  // Disable animation for frame update
391
318
  CATransaction.begin()
392
319
  CATransaction.setDisableActions(true)
@@ -443,8 +370,8 @@ extension CameraController {
443
370
  if Thread.isMainThread {
444
371
  updateVideoOrientationOnMainThread()
445
372
  } else {
446
- DispatchQueue.main.async { [weak self] in
447
- self?.updateVideoOrientationOnMainThread()
373
+ DispatchQueue.main.sync {
374
+ self.updateVideoOrientationOnMainThread()
448
375
  }
449
376
  }
450
377
  }
@@ -485,7 +412,7 @@ extension CameraController {
485
412
 
486
413
  // Ensure we have the necessary cameras
487
414
  guard (currentCameraPosition == .front && rearCamera != nil) ||
488
- (currentCameraPosition == .rear && frontCamera != nil) else {
415
+ (currentCameraPosition == .rear && frontCamera != nil) else {
489
416
  throw CameraControllerError.noCamerasAvailable
490
417
  }
491
418
 
@@ -501,9 +428,7 @@ extension CameraController {
501
428
  captureSession.commitConfiguration()
502
429
  // Restart the session if it was running before
503
430
  if wasRunning {
504
- DispatchQueue.global(qos: .userInitiated).async { [weak self] in
505
- self?.captureSession?.startRunning()
506
- }
431
+ captureSession.startRunning()
507
432
  }
508
433
  }
509
434
 
@@ -513,7 +438,7 @@ extension CameraController {
513
438
  // Remove only video inputs
514
439
  captureSession.inputs.forEach { input in
515
440
  if (input as? AVCaptureDeviceInput)?.device.hasMediaType(.video) ?? false {
516
- captureSession.removeInput(input)
441
+ captureSession.removeInput(input)
517
442
  }
518
443
  }
519
444
 
@@ -532,7 +457,7 @@ extension CameraController {
532
457
  rearCamera.unlockForConfiguration()
533
458
 
534
459
  if let newInput = try? AVCaptureDeviceInput(device: rearCamera),
535
- captureSession.canAddInput(newInput) {
460
+ captureSession.canAddInput(newInput) {
536
461
  captureSession.addInput(newInput)
537
462
  rearCameraInput = newInput
538
463
  self.currentCameraPosition = .rear
@@ -552,7 +477,7 @@ extension CameraController {
552
477
  frontCamera.unlockForConfiguration()
553
478
 
554
479
  if let newInput = try? AVCaptureDeviceInput(device: frontCamera),
555
- captureSession.canAddInput(newInput) {
480
+ captureSession.canAddInput(newInput) {
556
481
  captureSession.addInput(newInput)
557
482
  frontCameraInput = newInput
558
483
  self.currentCameraPosition = .front
@@ -567,9 +492,7 @@ extension CameraController {
567
492
  }
568
493
 
569
494
  // Update video orientation
570
- DispatchQueue.main.async { [weak self] in
571
- self?.updateVideoOrientation()
572
- }
495
+ self.updateVideoOrientation()
573
496
  }
574
497
 
575
498
  func captureImage(width: Int?, height: Int?, quality: Float, gpsLocation: CLLocation?, completion: @escaping (UIImage?, Error?) -> Void) {
@@ -637,7 +560,7 @@ extension CameraController {
637
560
 
638
561
  func resizeImage(image: UIImage, to size: CGSize) -> UIImage? {
639
562
  let renderer = UIGraphicsImageRenderer(size: size)
640
- let resizedImage = renderer.image { (context) in
563
+ let resizedImage = renderer.image { (_) in
641
564
  image.draw(in: CGRect(origin: .zero, size: size))
642
565
  }
643
566
  return resizedImage
@@ -963,9 +886,7 @@ extension CameraController {
963
886
  captureSession.commitConfiguration()
964
887
  // Restart the session if it was running before
965
888
  if wasRunning {
966
- DispatchQueue.global(qos: .userInitiated).async { [weak self] in
967
- self?.captureSession?.startRunning()
968
- }
889
+ captureSession.startRunning()
969
890
  }
970
891
  }
971
892
 
@@ -1012,13 +933,9 @@ extension CameraController {
1012
933
  }
1013
934
 
1014
935
  // Update video orientation
1015
- DispatchQueue.main.async { [weak self] in
1016
- self?.updateVideoOrientation()
1017
- }
936
+ self.updateVideoOrientation()
1018
937
  }
1019
938
 
1020
-
1021
-
1022
939
  func cleanup() {
1023
940
  if let captureSession = self.captureSession {
1024
941
  captureSession.stopRunning()
@@ -1139,7 +1056,7 @@ extension CameraController: UIGestureRecognizerDelegate {
1139
1056
  }
1140
1057
  }
1141
1058
 
1142
- @objc
1059
+ @objc
1143
1060
  private func handlePinch(_ pinch: UIPinchGestureRecognizer) {
1144
1061
  guard let device = self.currentCameraPosition == .rear ? rearCamera : frontCamera else { return }
1145
1062
 
@@ -1151,7 +1068,7 @@ extension CameraController: UIGestureRecognizerDelegate {
1151
1068
  // Store the initial zoom factor when pinch begins
1152
1069
  zoomFactor = device.videoZoomFactor
1153
1070
 
1154
- case .changed:
1071
+ case .changed:
1155
1072
  // Throttle zoom updates to prevent excessive CPU usage
1156
1073
  let currentTime = CACurrentMediaTime()
1157
1074
  guard currentTime - lastZoomUpdateTime >= zoomUpdateThrottle else { return }
@@ -1181,16 +1098,24 @@ extension CameraController: UIGestureRecognizerDelegate {
1181
1098
  }
1182
1099
 
1183
1100
  extension CameraController: AVCapturePhotoCaptureDelegate {
1184
- public func photoOutput(_ captureOutput: AVCapturePhotoOutput, didFinishProcessingPhoto photoSampleBuffer: CMSampleBuffer?, previewPhoto previewPhotoSampleBuffer: CMSampleBuffer?,
1185
- resolvedSettings: AVCaptureResolvedPhotoSettings, bracketSettings: AVCaptureBracketedStillImageSettings?, error: Swift.Error?) {
1101
+ public func photoOutput(_ output: AVCapturePhotoOutput, didFinishProcessingPhoto photo: AVCapturePhoto, error: Error?) {
1186
1102
  if let error = error {
1187
1103
  self.photoCaptureCompletionBlock?(nil, error)
1188
- } else if let buffer = photoSampleBuffer, let data = AVCapturePhotoOutput.jpegPhotoDataRepresentation(forJPEGSampleBuffer: buffer, previewPhotoSampleBuffer: nil),
1189
- let image = UIImage(data: data) {
1190
- self.photoCaptureCompletionBlock?(image.fixedOrientation(), nil)
1191
- } else {
1104
+ return
1105
+ }
1106
+
1107
+ // Get the photo data using the modern API
1108
+ guard let imageData = photo.fileDataRepresentation() else {
1109
+ self.photoCaptureCompletionBlock?(nil, CameraControllerError.unknown)
1110
+ return
1111
+ }
1112
+
1113
+ guard let image = UIImage(data: imageData) else {
1192
1114
  self.photoCaptureCompletionBlock?(nil, CameraControllerError.unknown)
1115
+ return
1193
1116
  }
1117
+
1118
+ self.photoCaptureCompletionBlock?(image.fixedOrientation(), nil)
1194
1119
  }
1195
1120
  }
1196
1121
 
@@ -1312,6 +1237,8 @@ extension UIImage {
1312
1237
  print("right")
1313
1238
  case .up, .upMirrored:
1314
1239
  break
1240
+ @unknown default:
1241
+ break
1315
1242
  }
1316
1243
 
1317
1244
  // Flip image one more time if needed to, this is to prevent flipped image
@@ -1324,6 +1251,8 @@ extension UIImage {
1324
1251
  transform.scaledBy(x: -1, y: 1)
1325
1252
  case .up, .down, .left, .right:
1326
1253
  break
1254
+ @unknown default:
1255
+ break
1327
1256
  }
1328
1257
 
1329
1258
  ctx.concatenate(transform)