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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/CapgoCameraPreview.podspec +16 -13
  2. package/README.md +467 -73
  3. package/android/.gradle/8.14.2/checksums/checksums.lock +0 -0
  4. package/android/.gradle/8.14.2/checksums/md5-checksums.bin +0 -0
  5. package/android/.gradle/8.14.2/checksums/sha1-checksums.bin +0 -0
  6. package/android/.gradle/8.14.2/executionHistory/executionHistory.bin +0 -0
  7. package/android/.gradle/8.14.2/executionHistory/executionHistory.lock +0 -0
  8. package/android/.gradle/8.14.2/fileChanges/last-build.bin +0 -0
  9. package/android/.gradle/8.14.2/fileHashes/fileHashes.bin +0 -0
  10. package/android/.gradle/8.14.2/fileHashes/fileHashes.lock +0 -0
  11. package/android/.gradle/8.14.2/fileHashes/resourceHashesCache.bin +0 -0
  12. package/android/.gradle/8.14.2/gc.properties +0 -0
  13. package/android/.gradle/buildOutputCleanup/buildOutputCleanup.lock +0 -0
  14. package/android/.gradle/buildOutputCleanup/cache.properties +2 -0
  15. package/android/.gradle/buildOutputCleanup/outputFiles.bin +0 -0
  16. package/android/.gradle/file-system.probe +0 -0
  17. package/android/.gradle/vcs-1/gc.properties +0 -0
  18. package/android/build.gradle +11 -0
  19. package/android/gradle/wrapper/gradle-wrapper.properties +1 -1
  20. package/android/src/main/AndroidManifest.xml +5 -3
  21. package/android/src/main/java/com/ahm/capacitor/camera/preview/CameraPreview.java +472 -541
  22. package/android/src/main/java/com/ahm/capacitor/camera/preview/CameraXView.java +1648 -0
  23. package/android/src/main/java/com/ahm/capacitor/camera/preview/GridOverlayView.java +82 -0
  24. package/android/src/main/java/com/ahm/capacitor/camera/preview/model/CameraDevice.java +54 -0
  25. package/android/src/main/java/com/ahm/capacitor/camera/preview/model/CameraLens.java +70 -0
  26. package/android/src/main/java/com/ahm/capacitor/camera/preview/model/CameraSessionConfiguration.java +79 -0
  27. package/android/src/main/java/com/ahm/capacitor/camera/preview/model/LensInfo.java +34 -0
  28. package/android/src/main/java/com/ahm/capacitor/camera/preview/model/ZoomFactors.java +34 -0
  29. package/dist/docs.json +934 -154
  30. package/dist/esm/definitions.d.ts +445 -83
  31. package/dist/esm/definitions.js +10 -1
  32. package/dist/esm/definitions.js.map +1 -1
  33. package/dist/esm/web.d.ts +73 -3
  34. package/dist/esm/web.js +492 -68
  35. package/dist/esm/web.js.map +1 -1
  36. package/dist/plugin.cjs.js +498 -68
  37. package/dist/plugin.cjs.js.map +1 -1
  38. package/dist/plugin.js +498 -68
  39. package/dist/plugin.js.map +1 -1
  40. package/ios/{Plugin → Sources/CapgoCameraPreview}/CameraController.swift +601 -59
  41. package/ios/Sources/CapgoCameraPreview/GridOverlayView.swift +65 -0
  42. package/ios/Sources/CapgoCameraPreview/Plugin.swift +1369 -0
  43. package/ios/Tests/CameraPreviewPluginTests/CameraPreviewPluginTests.swift +15 -0
  44. package/package.json +1 -1
  45. package/android/src/main/java/com/ahm/capacitor/camera/preview/CameraActivity.java +0 -1279
  46. package/android/src/main/java/com/ahm/capacitor/camera/preview/CustomSurfaceView.java +0 -29
  47. package/android/src/main/java/com/ahm/capacitor/camera/preview/CustomTextureView.java +0 -39
  48. package/android/src/main/java/com/ahm/capacitor/camera/preview/Preview.java +0 -461
  49. package/android/src/main/java/com/ahm/capacitor/camera/preview/TapGestureDetector.java +0 -24
  50. package/ios/Plugin/Info.plist +0 -24
  51. package/ios/Plugin/Plugin.h +0 -10
  52. package/ios/Plugin/Plugin.m +0 -18
  53. package/ios/Plugin/Plugin.swift +0 -511
  54. package/ios/Plugin.xcodeproj/project.pbxproj +0 -593
  55. package/ios/Plugin.xcodeproj/project.xcworkspace/contents.xcworkspacedata +0 -7
  56. package/ios/Plugin.xcworkspace/contents.xcworkspacedata +0 -10
  57. package/ios/Plugin.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +0 -8
  58. package/ios/PluginTests/Info.plist +0 -22
  59. package/ios/PluginTests/PluginTests.swift +0 -83
  60. package/ios/Podfile +0 -13
  61. package/ios/Podfile.lock +0 -23
@@ -8,6 +8,7 @@
8
8
 
9
9
  import AVFoundation
10
10
  import UIKit
11
+ import CoreLocation
11
12
 
12
13
  class CameraController: NSObject {
13
14
  var captureSession: AVCaptureSession?
@@ -26,6 +27,7 @@ class CameraController: NSObject {
26
27
  var fileVideoOutput: AVCaptureMovieFileOutput?
27
28
 
28
29
  var previewLayer: AVCaptureVideoPreviewLayer?
30
+ var gridOverlayView: GridOverlayView?
29
31
 
30
32
  var flashMode = AVCaptureDevice.FlashMode.off
31
33
  var photoCaptureCompletionBlock: ((UIImage?, Error?) -> Void)?
@@ -38,36 +40,129 @@ class CameraController: NSObject {
38
40
  var audioInput: AVCaptureDeviceInput?
39
41
 
40
42
  var zoomFactor: CGFloat = 1.0
43
+ private var lastZoomUpdateTime: TimeInterval = 0
44
+ private let zoomUpdateThrottle: TimeInterval = 1.0 / 60.0 // 60 FPS max
41
45
 
42
46
  var videoFileURL: URL?
47
+ private let saneMaxZoomFactor: CGFloat = 25.5
48
+
49
+ var isUsingMultiLensVirtualCamera: Bool {
50
+ guard let device = (currentCameraPosition == .rear) ? rearCamera : frontCamera else { return false }
51
+ // A rear multi-lens virtual camera will have a min zoom of 1.0 but support wider angles
52
+ return device.position == .back && device.isVirtualDevice && device.constituentDevices.count > 1
53
+ }
43
54
  }
44
55
 
45
56
  extension CameraController {
46
- func prepare(cameraPosition: String, disableAudio: Bool, cameraMode: Bool, completionHandler: @escaping (Error?) -> Void) {
57
+ func prepareBasicSession() {
58
+ // Only prepare if we don't already have a session
59
+ guard self.captureSession == nil else { return }
60
+
61
+ print("[CameraPreview] Preparing basic camera session in background")
62
+
63
+ // Create basic capture session
64
+ self.captureSession = AVCaptureSession()
65
+
66
+ // Configure basic devices without full preparation
67
+ let deviceTypes: [AVCaptureDevice.DeviceType] = [
68
+ .builtInWideAngleCamera,
69
+ .builtInUltraWideCamera,
70
+ .builtInTelephotoCamera,
71
+ .builtInDualCamera,
72
+ .builtInDualWideCamera,
73
+ .builtInTripleCamera,
74
+ .builtInTrueDepthCamera
75
+ ]
76
+
77
+ let session = AVCaptureDevice.DiscoverySession(deviceTypes: deviceTypes, mediaType: AVMediaType.video, position: .unspecified)
78
+ let cameras = session.devices.compactMap { $0 }
79
+
80
+ // Find best cameras
81
+ let rearVirtualDevices = cameras.filter { $0.position == .back && $0.isVirtualDevice }
82
+ let bestRearVirtualDevice = rearVirtualDevices.max { $0.constituentDevices.count < $1.constituentDevices.count }
83
+
84
+ self.frontCamera = cameras.first(where: { $0.position == .front })
85
+
86
+ if let bestCamera = bestRearVirtualDevice {
87
+ self.rearCamera = bestCamera
88
+ } else if let firstRearCamera = cameras.first(where: { $0.position == .back }) {
89
+ self.rearCamera = firstRearCamera
90
+ }
91
+
92
+ print("[CameraPreview] Basic session prepared with \(cameras.count) devices")
93
+ }
94
+
95
+ func prepare(cameraPosition: String, deviceId: String? = nil, disableAudio: Bool, cameraMode: Bool, aspectRatio: String? = nil, completionHandler: @escaping (Error?) -> Void) {
47
96
  func createCaptureSession() {
48
- self.captureSession = AVCaptureSession()
97
+ // Use existing session if available from background preparation
98
+ if self.captureSession == nil {
99
+ self.captureSession = AVCaptureSession()
100
+ }
49
101
  }
50
102
 
51
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
+ ]
52
120
 
53
- let session = AVCaptureDevice.DiscoverySession(deviceTypes: [.builtInWideAngleCamera], mediaType: AVMediaType.video, position: .unspecified)
121
+ let session = AVCaptureDevice.DiscoverySession(deviceTypes: deviceTypes, mediaType: AVMediaType.video, position: .unspecified)
54
122
 
55
123
  let cameras = session.devices.compactMap { $0 }
56
- guard !cameras.isEmpty else { throw CameraControllerError.noCamerasAvailable }
57
124
 
125
+ // Log all found devices for debugging
126
+ print("[CameraPreview] Found \(cameras.count) devices:")
58
127
  for camera in cameras {
59
- if camera.position == .front {
60
- self.frontCamera = camera
61
- }
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
+ }
131
+
132
+ guard !cameras.isEmpty else {
133
+ print("[CameraPreview] ERROR: No cameras found.")
134
+ throw CameraControllerError.noCamerasAvailable
135
+ }
136
+
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 }
141
+
142
+ self.frontCamera = cameras.first(where: { $0.position == .front })
62
143
 
63
- if camera.position == .back {
64
- self.rearCamera = camera
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)")
151
+ }
152
+ // --- End of Correction ---
65
153
 
66
- try camera.lockForConfiguration()
67
- camera.focusMode = .continuousAutoFocus
68
- camera.unlockForConfiguration()
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)")
69
163
  }
70
164
  }
165
+
71
166
  if disableAudio == false {
72
167
  self.audioDevice = AVCaptureDevice.default(for: AVMediaType.audio)
73
168
  }
@@ -76,23 +171,72 @@ extension CameraController {
76
171
  func configureDeviceInputs() throws {
77
172
  guard let captureSession = self.captureSession else { throw CameraControllerError.captureSessionIsMissing }
78
173
 
79
- if cameraPosition == "rear" {
80
- if let rearCamera = self.rearCamera {
81
- self.rearCameraInput = try AVCaptureDeviceInput(device: rearCamera)
174
+ var selectedDevice: AVCaptureDevice?
82
175
 
83
- if captureSession.canAddInput(self.rearCameraInput!) { captureSession.addInput(self.rearCameraInput!) }
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
84
183
 
85
- self.currentCameraPosition = .rear
184
+ selectedDevice = allDevices.first(where: { $0.uniqueID == deviceId })
185
+ guard selectedDevice != nil else {
186
+ throw CameraControllerError.noCamerasAvailable
86
187
  }
87
- } else if cameraPosition == "front" {
88
- if let frontCamera = self.frontCamera {
89
- self.frontCameraInput = try AVCaptureDeviceInput(device: frontCamera)
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
+ }
90
196
 
91
- if captureSession.canAddInput(self.frontCameraInput!) { captureSession.addInput(self.frontCameraInput!) } else { throw CameraControllerError.inputsAreInvalid }
197
+ guard let finalDevice = selectedDevice else {
198
+ print("[CameraPreview] ERROR: No camera device selected for position: \(cameraPosition)")
199
+ throw CameraControllerError.noCamerasAvailable
200
+ }
201
+
202
+ print("[CameraPreview] Configuring device: \(finalDevice.localizedName)")
203
+ let deviceInput = try AVCaptureDeviceInput(device: finalDevice)
92
204
 
205
+ if captureSession.canAddInput(deviceInput) {
206
+ captureSession.addInput(deviceInput)
207
+
208
+ if finalDevice.position == .front {
209
+ self.frontCameraInput = deviceInput
210
+ self.frontCamera = finalDevice
93
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
+ }
222
+
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 ---
94
235
  }
95
- } else { throw CameraControllerError.noCamerasAvailable }
236
+ } else {
237
+ print("[CameraPreview] ERROR: Cannot add device input to session.")
238
+ throw CameraControllerError.inputsAreInvalid
239
+ }
96
240
 
97
241
  // Add audio input
98
242
  if disableAudio == false {
@@ -110,11 +254,65 @@ extension CameraController {
110
254
  func configurePhotoOutput(cameraMode: Bool) throws {
111
255
  guard let captureSession = self.captureSession else { throw CameraControllerError.captureSessionIsMissing }
112
256
 
113
- // TODO: check if that really useful
114
- if !cameraMode && self.highResolutionOutput && captureSession.canSetSessionPreset(.photo) {
115
- captureSession.sessionPreset = .photo
116
- } else if cameraMode && self.highResolutionOutput && captureSession.canSetSessionPreset(.high) {
117
- captureSession.sessionPreset = .high
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
299
+ } else {
300
+ targetPreset = .medium
301
+ }
302
+ }
303
+
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
+ }
118
316
  }
119
317
 
120
318
  self.photoOutput = AVCapturePhotoOutput()
@@ -173,15 +371,51 @@ extension CameraController {
173
371
  func displayPreview(on view: UIView) throws {
174
372
  guard let captureSession = self.captureSession, captureSession.isRunning else { throw CameraControllerError.captureSessionIsMissing }
175
373
 
374
+ print("[CameraPreview] displayPreview called with view frame: \(view.frame)")
375
+
176
376
  self.previewLayer = AVCaptureVideoPreviewLayer(session: captureSession)
177
377
  self.previewLayer?.videoGravity = AVLayerVideoGravity.resizeAspectFill
178
378
 
379
+ // Optimize preview layer for better quality
380
+ self.previewLayer?.connection?.videoOrientation = .portrait
381
+ self.previewLayer?.isOpaque = true
382
+
383
+ // Enable high-quality rendering
384
+ if #available(iOS 13.0, *) {
385
+ self.previewLayer?.videoGravity = .resizeAspectFill
386
+ }
387
+
179
388
  view.layer.insertSublayer(self.previewLayer!, at: 0)
180
- self.previewLayer?.frame = view.frame
389
+
390
+ // Disable animation for frame update
391
+ CATransaction.begin()
392
+ CATransaction.setDisableActions(true)
393
+ self.previewLayer?.frame = view.bounds
394
+ CATransaction.commit()
395
+
396
+ print("[CameraPreview] Set preview layer frame to view bounds: \(view.bounds)")
397
+ print("[CameraPreview] Session preset: \(captureSession.sessionPreset.rawValue)")
181
398
 
182
399
  updateVideoOrientation()
183
400
  }
184
401
 
402
+ func addGridOverlay(to view: UIView, gridMode: String) {
403
+ removeGridOverlay()
404
+
405
+ // Disable animation for grid overlay creation and positioning
406
+ CATransaction.begin()
407
+ CATransaction.setDisableActions(true)
408
+ gridOverlayView = GridOverlayView(frame: view.bounds)
409
+ gridOverlayView?.gridMode = gridMode
410
+ view.addSubview(gridOverlayView!)
411
+ CATransaction.commit()
412
+ }
413
+
414
+ func removeGridOverlay() {
415
+ gridOverlayView?.removeFromSuperview()
416
+ gridOverlayView = nil
417
+ }
418
+
185
419
  func setupGestures(target: UIView, enableZoom: Bool) {
186
420
  setupTapGesture(target: target, selector: #selector(handleTap(_:)), delegate: self)
187
421
  if enableZoom {
@@ -198,6 +432,10 @@ extension CameraController {
198
432
  func setupPinchGesture(target: UIView, selector: Selector, delegate: UIGestureRecognizerDelegate?) {
199
433
  let pinchGesture = UIPinchGestureRecognizer(target: self, action: selector)
200
434
  pinchGesture.delegate = delegate
435
+ // Optimize gesture recognition for better performance
436
+ pinchGesture.delaysTouchesBegan = false
437
+ pinchGesture.delaysTouchesEnded = false
438
+ pinchGesture.cancelsTouchesInView = false
201
439
  target.addGestureRecognizer(pinchGesture)
202
440
  }
203
441
 
@@ -213,7 +451,7 @@ extension CameraController {
213
451
 
214
452
  private func updateVideoOrientationOnMainThread() {
215
453
  let videoOrientation: AVCaptureVideoOrientation
216
-
454
+
217
455
  // Use window scene interface orientation
218
456
  if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene {
219
457
  switch windowScene.interfaceOrientation {
@@ -244,19 +482,19 @@ extension CameraController {
244
482
  let captureSession = self.captureSession else {
245
483
  throw CameraControllerError.captureSessionIsMissing
246
484
  }
247
-
485
+
248
486
  // Ensure we have the necessary cameras
249
487
  guard (currentCameraPosition == .front && rearCamera != nil) ||
250
488
  (currentCameraPosition == .rear && frontCamera != nil) else {
251
489
  throw CameraControllerError.noCamerasAvailable
252
490
  }
253
-
491
+
254
492
  // Store the current running state
255
493
  let wasRunning = captureSession.isRunning
256
494
  if wasRunning {
257
495
  captureSession.stopRunning()
258
496
  }
259
-
497
+
260
498
  // Begin configuration
261
499
  captureSession.beginConfiguration()
262
500
  defer {
@@ -268,7 +506,7 @@ extension CameraController {
268
506
  }
269
507
  }
270
508
  }
271
-
509
+
272
510
  // Store audio input if it exists
273
511
  let audioInput = captureSession.inputs.first { ($0 as? AVCaptureDeviceInput)?.device.hasMediaType(.audio) ?? false }
274
512
 
@@ -278,21 +516,21 @@ extension CameraController {
278
516
  captureSession.removeInput(input)
279
517
  }
280
518
  }
281
-
519
+
282
520
  // Configure new camera
283
521
  switch currentCameraPosition {
284
522
  case .front:
285
523
  guard let rearCamera = rearCamera else {
286
524
  throw CameraControllerError.invalidOperation
287
525
  }
288
-
526
+
289
527
  // Configure rear camera
290
528
  try rearCamera.lockForConfiguration()
291
529
  if rearCamera.isFocusModeSupported(.continuousAutoFocus) {
292
530
  rearCamera.focusMode = .continuousAutoFocus
293
531
  }
294
532
  rearCamera.unlockForConfiguration()
295
-
533
+
296
534
  if let newInput = try? AVCaptureDeviceInput(device: rearCamera),
297
535
  captureSession.canAddInput(newInput) {
298
536
  captureSession.addInput(newInput)
@@ -306,13 +544,13 @@ extension CameraController {
306
544
  throw CameraControllerError.invalidOperation
307
545
  }
308
546
 
309
- // Configure front camera
547
+ // Configure front camera
310
548
  try frontCamera.lockForConfiguration()
311
549
  if frontCamera.isFocusModeSupported(.continuousAutoFocus) {
312
550
  frontCamera.focusMode = .continuousAutoFocus
313
551
  }
314
552
  frontCamera.unlockForConfiguration()
315
-
553
+
316
554
  if let newInput = try? AVCaptureDeviceInput(device: frontCamera),
317
555
  captureSession.canAddInput(newInput) {
318
556
  captureSession.addInput(newInput)
@@ -327,22 +565,82 @@ extension CameraController {
327
565
  if let audioInput = audioInput, captureSession.canAddInput(audioInput) {
328
566
  captureSession.addInput(audioInput)
329
567
  }
330
-
568
+
331
569
  // Update video orientation
332
570
  DispatchQueue.main.async { [weak self] in
333
571
  self?.updateVideoOrientation()
334
572
  }
335
573
  }
336
574
 
337
- func captureImage(completion: @escaping (UIImage?, Error?) -> Void) {
338
- guard let captureSession = captureSession, captureSession.isRunning else { completion(nil, CameraControllerError.captureSessionIsMissing); return }
575
+ func captureImage(width: Int?, height: Int?, quality: Float, gpsLocation: CLLocation?, completion: @escaping (UIImage?, Error?) -> Void) {
576
+ guard let photoOutput = self.photoOutput else {
577
+ completion(nil, NSError(domain: "Camera", code: 0, userInfo: [NSLocalizedDescriptionKey: "Photo output is not available"]))
578
+ return
579
+ }
580
+
339
581
  let settings = AVCapturePhotoSettings()
340
582
 
341
- settings.flashMode = self.flashMode
342
- settings.isHighResolutionPhotoEnabled = self.highResolutionOutput
583
+ self.photoCaptureCompletionBlock = { (image, error) in
584
+ if let error = error {
585
+ completion(nil, error)
586
+ return
587
+ }
588
+
589
+ guard let image = image else {
590
+ completion(nil, NSError(domain: "Camera", code: 0, userInfo: [NSLocalizedDescriptionKey: "Failed to capture image"]))
591
+ return
592
+ }
593
+
594
+ if let location = gpsLocation {
595
+ self.addGPSMetadata(to: image, location: location)
596
+ }
343
597
 
344
- self.photoOutput?.capturePhoto(with: settings, delegate: self)
345
- self.photoCaptureCompletionBlock = completion
598
+ if let width = width, let height = height {
599
+ let resizedImage = self.resizeImage(image: image, to: CGSize(width: width, height: height))
600
+ completion(resizedImage, nil)
601
+ } else {
602
+ completion(image, nil)
603
+ }
604
+ }
605
+
606
+ photoOutput.capturePhoto(with: settings, delegate: self)
607
+ }
608
+
609
+ func addGPSMetadata(to image: UIImage, location: CLLocation) {
610
+ guard let jpegData = image.jpegData(compressionQuality: 1.0),
611
+ let source = CGImageSourceCreateWithData(jpegData as CFData, nil),
612
+ let uti = CGImageSourceGetType(source) else { return }
613
+
614
+ var metadata = CGImageSourceCopyPropertiesAtIndex(source, 0, nil) as? [String: Any] ?? [:]
615
+
616
+ let formatter = DateFormatter()
617
+ formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZ"
618
+ formatter.timeZone = TimeZone(abbreviation: "UTC")
619
+
620
+ let gpsDict: [String: Any] = [
621
+ kCGImagePropertyGPSLatitude as String: abs(location.coordinate.latitude),
622
+ kCGImagePropertyGPSLatitudeRef as String: location.coordinate.latitude >= 0 ? "N" : "S",
623
+ kCGImagePropertyGPSLongitude as String: abs(location.coordinate.longitude),
624
+ kCGImagePropertyGPSLongitudeRef as String: location.coordinate.longitude >= 0 ? "E" : "W",
625
+ kCGImagePropertyGPSTimeStamp as String: formatter.string(from: location.timestamp),
626
+ kCGImagePropertyGPSAltitude as String: location.altitude,
627
+ kCGImagePropertyGPSAltitudeRef as String: location.altitude >= 0 ? 0 : 1
628
+ ]
629
+
630
+ metadata[kCGImagePropertyGPSDictionary as String] = gpsDict
631
+
632
+ let destData = NSMutableData()
633
+ guard let destination = CGImageDestinationCreateWithData(destData, uti, 1, nil) else { return }
634
+ CGImageDestinationAddImageFromSource(destination, source, 0, metadata as CFDictionary)
635
+ CGImageDestinationFinalize(destination)
636
+ }
637
+
638
+ func resizeImage(image: UIImage, to size: CGSize) -> UIImage? {
639
+ let renderer = UIGraphicsImageRenderer(size: size)
640
+ let resizedImage = renderer.image { (context) in
641
+ image.draw(in: CGRect(origin: .zero, size: size))
642
+ }
643
+ return resizedImage
346
644
  }
347
645
 
348
646
  func captureSample(completion: @escaping (UIImage?, Error?) -> Void) {
@@ -415,8 +713,15 @@ extension CameraController {
415
713
  throw CameraControllerError.noCamerasAvailable
416
714
  }
417
715
 
418
- return device.activeFormat.videoFieldOfView
716
+ // Get the active format and field of view
717
+ let activeFormat = device.activeFormat
718
+ let fov = activeFormat.videoFieldOfView
719
+
720
+ // Adjust for current zoom level
721
+ let zoomFactor = device.videoZoomFactor
722
+ let adjustedFov = fov / Float(zoomFactor)
419
723
 
724
+ return adjustedFov
420
725
  }
421
726
  func setFlashMode(flashMode: AVCaptureDevice.FlashMode) throws {
422
727
  var currentCamera: AVCaptureDevice?
@@ -489,6 +794,231 @@ extension CameraController {
489
794
  }
490
795
  }
491
796
 
797
+ func getZoom() throws -> (min: Float, max: Float, current: Float) {
798
+ var currentCamera: AVCaptureDevice?
799
+ switch currentCameraPosition {
800
+ case .front:
801
+ currentCamera = self.frontCamera
802
+ case .rear:
803
+ currentCamera = self.rearCamera
804
+ default: break
805
+ }
806
+
807
+ guard let device = currentCamera else {
808
+ throw CameraControllerError.noCamerasAvailable
809
+ }
810
+
811
+ let effectiveMaxZoom = min(device.maxAvailableVideoZoomFactor, self.saneMaxZoomFactor)
812
+
813
+ return (
814
+ min: Float(device.minAvailableVideoZoomFactor),
815
+ max: Float(effectiveMaxZoom),
816
+ current: Float(device.videoZoomFactor)
817
+ )
818
+ }
819
+
820
+ func setZoom(level: CGFloat, ramp: Bool) throws {
821
+ var currentCamera: AVCaptureDevice?
822
+ switch currentCameraPosition {
823
+ case .front:
824
+ currentCamera = self.frontCamera
825
+ case .rear:
826
+ currentCamera = self.rearCamera
827
+ default: break
828
+ }
829
+
830
+ guard let device = currentCamera else {
831
+ throw CameraControllerError.noCamerasAvailable
832
+ }
833
+
834
+ let effectiveMaxZoom = min(device.maxAvailableVideoZoomFactor, self.saneMaxZoomFactor)
835
+ let zoomLevel = max(device.minAvailableVideoZoomFactor, min(level, effectiveMaxZoom))
836
+
837
+ do {
838
+ try device.lockForConfiguration()
839
+
840
+ if ramp {
841
+ // Use a very fast ramp rate for immediate response
842
+ device.ramp(toVideoZoomFactor: zoomLevel, withRate: 8.0)
843
+ } else {
844
+ device.videoZoomFactor = zoomLevel
845
+ }
846
+
847
+ device.unlockForConfiguration()
848
+
849
+ // Update our internal zoom factor tracking
850
+ self.zoomFactor = zoomLevel
851
+ } catch {
852
+ throw CameraControllerError.invalidOperation
853
+ }
854
+ }
855
+
856
+ func getFlashMode() throws -> String {
857
+ switch self.flashMode {
858
+ case .off:
859
+ return "off"
860
+ case .on:
861
+ return "on"
862
+ case .auto:
863
+ return "auto"
864
+ @unknown default:
865
+ return "off"
866
+ }
867
+ }
868
+
869
+ func getCurrentDeviceId() throws -> String {
870
+ var currentCamera: AVCaptureDevice?
871
+ switch currentCameraPosition {
872
+ case .front:
873
+ currentCamera = self.frontCamera
874
+ case .rear:
875
+ currentCamera = self.rearCamera
876
+ default:
877
+ break
878
+ }
879
+
880
+ guard let device = currentCamera else {
881
+ throw CameraControllerError.noCamerasAvailable
882
+ }
883
+
884
+ return device.uniqueID
885
+ }
886
+
887
+ func getCurrentLensInfo() throws -> (focalLength: Float, deviceType: String, baseZoomRatio: Float) {
888
+ var currentCamera: AVCaptureDevice?
889
+ switch currentCameraPosition {
890
+ case .front:
891
+ currentCamera = self.frontCamera
892
+ case .rear:
893
+ currentCamera = self.rearCamera
894
+ default:
895
+ break
896
+ }
897
+
898
+ guard let device = currentCamera else {
899
+ throw CameraControllerError.noCamerasAvailable
900
+ }
901
+
902
+ var deviceType = "wideAngle"
903
+ var baseZoomRatio: Float = 1.0
904
+
905
+ switch device.deviceType {
906
+ case .builtInWideAngleCamera:
907
+ deviceType = "wideAngle"
908
+ baseZoomRatio = 1.0
909
+ case .builtInUltraWideCamera:
910
+ deviceType = "ultraWide"
911
+ baseZoomRatio = 0.5
912
+ case .builtInTelephotoCamera:
913
+ deviceType = "telephoto"
914
+ baseZoomRatio = 2.0
915
+ case .builtInDualCamera:
916
+ deviceType = "dual"
917
+ baseZoomRatio = 1.0
918
+ case .builtInDualWideCamera:
919
+ deviceType = "dualWide"
920
+ baseZoomRatio = 1.0
921
+ case .builtInTripleCamera:
922
+ deviceType = "triple"
923
+ baseZoomRatio = 1.0
924
+ case .builtInTrueDepthCamera:
925
+ deviceType = "trueDepth"
926
+ baseZoomRatio = 1.0
927
+ default:
928
+ deviceType = "wideAngle"
929
+ baseZoomRatio = 1.0
930
+ }
931
+
932
+ // Approximate focal length for mobile devices
933
+ let focalLength: Float = 4.25
934
+
935
+ return (focalLength: focalLength, deviceType: deviceType, baseZoomRatio: baseZoomRatio)
936
+ }
937
+
938
+ func swapToDevice(deviceId: String) throws {
939
+ guard let captureSession = self.captureSession else {
940
+ throw CameraControllerError.captureSessionIsMissing
941
+ }
942
+
943
+ // Find the device with the specified deviceId
944
+ let allDevices = AVCaptureDevice.DiscoverySession(
945
+ deviceTypes: [.builtInWideAngleCamera, .builtInUltraWideCamera, .builtInTelephotoCamera, .builtInDualCamera, .builtInDualWideCamera, .builtInTripleCamera, .builtInTrueDepthCamera],
946
+ mediaType: .video,
947
+ position: .unspecified
948
+ ).devices
949
+
950
+ guard let targetDevice = allDevices.first(where: { $0.uniqueID == deviceId }) else {
951
+ throw CameraControllerError.noCamerasAvailable
952
+ }
953
+
954
+ // Store the current running state
955
+ let wasRunning = captureSession.isRunning
956
+ if wasRunning {
957
+ captureSession.stopRunning()
958
+ }
959
+
960
+ // Begin configuration
961
+ captureSession.beginConfiguration()
962
+ defer {
963
+ captureSession.commitConfiguration()
964
+ // Restart the session if it was running before
965
+ if wasRunning {
966
+ DispatchQueue.global(qos: .userInitiated).async { [weak self] in
967
+ self?.captureSession?.startRunning()
968
+ }
969
+ }
970
+ }
971
+
972
+ // Store audio input if it exists
973
+ let audioInput = captureSession.inputs.first { ($0 as? AVCaptureDeviceInput)?.device.hasMediaType(.audio) ?? false }
974
+
975
+ // Remove only video inputs
976
+ captureSession.inputs.forEach { input in
977
+ if (input as? AVCaptureDeviceInput)?.device.hasMediaType(.video) ?? false {
978
+ captureSession.removeInput(input)
979
+ }
980
+ }
981
+
982
+ // Configure the new device
983
+ let newInput = try AVCaptureDeviceInput(device: targetDevice)
984
+
985
+ if captureSession.canAddInput(newInput) {
986
+ captureSession.addInput(newInput)
987
+
988
+ // Update camera references based on device position
989
+ if targetDevice.position == .front {
990
+ self.frontCameraInput = newInput
991
+ self.frontCamera = targetDevice
992
+ self.currentCameraPosition = .front
993
+ } else {
994
+ self.rearCameraInput = newInput
995
+ self.rearCamera = targetDevice
996
+ self.currentCameraPosition = .rear
997
+
998
+ // Configure rear camera
999
+ try targetDevice.lockForConfiguration()
1000
+ if targetDevice.isFocusModeSupported(.continuousAutoFocus) {
1001
+ targetDevice.focusMode = .continuousAutoFocus
1002
+ }
1003
+ targetDevice.unlockForConfiguration()
1004
+ }
1005
+ } else {
1006
+ throw CameraControllerError.invalidOperation
1007
+ }
1008
+
1009
+ // Re-add audio input if it existed
1010
+ if let audioInput = audioInput, captureSession.canAddInput(audioInput) {
1011
+ captureSession.addInput(audioInput)
1012
+ }
1013
+
1014
+ // Update video orientation
1015
+ DispatchQueue.main.async { [weak self] in
1016
+ self?.updateVideoOrientation()
1017
+ }
1018
+ }
1019
+
1020
+
1021
+
492
1022
  func cleanup() {
493
1023
  if let captureSession = self.captureSession {
494
1024
  captureSession.stopRunning()
@@ -609,30 +1139,42 @@ extension CameraController: UIGestureRecognizerDelegate {
609
1139
  }
610
1140
  }
611
1141
 
612
- @objc
1142
+ @objc
613
1143
  private func handlePinch(_ pinch: UIPinchGestureRecognizer) {
614
1144
  guard let device = self.currentCameraPosition == .rear ? rearCamera : frontCamera else { return }
615
1145
 
616
- func minMaxZoom(_ factor: CGFloat) -> CGFloat { return max(1.0, min(factor, device.activeFormat.videoMaxZoomFactor)) }
1146
+ let effectiveMaxZoom = min(device.maxAvailableVideoZoomFactor, self.saneMaxZoomFactor)
1147
+ func minMaxZoom(_ factor: CGFloat) -> CGFloat { return max(device.minAvailableVideoZoomFactor, min(factor, effectiveMaxZoom)) }
1148
+
1149
+ switch pinch.state {
1150
+ case .began:
1151
+ // Store the initial zoom factor when pinch begins
1152
+ zoomFactor = device.videoZoomFactor
617
1153
 
618
- func update(scale factor: CGFloat) {
1154
+ case .changed:
1155
+ // Throttle zoom updates to prevent excessive CPU usage
1156
+ let currentTime = CACurrentMediaTime()
1157
+ guard currentTime - lastZoomUpdateTime >= zoomUpdateThrottle else { return }
1158
+ lastZoomUpdateTime = currentTime
1159
+
1160
+ // Calculate new zoom factor based on pinch scale
1161
+ let newScaleFactor = minMaxZoom(pinch.scale * zoomFactor)
1162
+
1163
+ // Use ramping for smooth zoom transitions during pinch
1164
+ // This provides much smoother performance than direct setting
619
1165
  do {
620
1166
  try device.lockForConfiguration()
621
- defer { device.unlockForConfiguration() }
622
-
623
- device.videoZoomFactor = factor
1167
+ // Use a very fast ramp rate for immediate response
1168
+ device.ramp(toVideoZoomFactor: newScaleFactor, withRate: 5.0)
1169
+ device.unlockForConfiguration()
624
1170
  } catch {
625
- debugPrint(error)
1171
+ debugPrint("Failed to set zoom: \(error)")
626
1172
  }
627
- }
628
1173
 
629
- switch pinch.state {
630
- case .began: fallthrough
631
- case .changed:
632
- let newScaleFactor = minMaxZoom(pinch.scale * zoomFactor)
633
- update(scale: newScaleFactor)
634
1174
  case .ended:
1175
+ // Update our internal zoom factor tracking
635
1176
  zoomFactor = device.videoZoomFactor
1177
+
636
1178
  default: break
637
1179
  }
638
1180
  }