@capgo/camera-preview 7.3.9 → 7.4.0-beta.2

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 (56) hide show
  1. package/CapgoCameraPreview.podspec +16 -13
  2. package/README.md +306 -70
  3. package/android/.gradle/8.14.2/checksums/checksums.lock +0 -0
  4. package/android/.gradle/8.14.2/checksums/sha1-checksums.bin +0 -0
  5. package/android/.gradle/8.14.2/executionHistory/executionHistory.bin +0 -0
  6. package/android/.gradle/8.14.2/executionHistory/executionHistory.lock +0 -0
  7. package/android/.gradle/8.14.2/fileChanges/last-build.bin +0 -0
  8. package/android/.gradle/8.14.2/fileHashes/fileHashes.bin +0 -0
  9. package/android/.gradle/8.14.2/fileHashes/fileHashes.lock +0 -0
  10. package/android/.gradle/8.14.2/fileHashes/resourceHashesCache.bin +0 -0
  11. package/android/.gradle/8.14.2/gc.properties +0 -0
  12. package/android/.gradle/buildOutputCleanup/buildOutputCleanup.lock +0 -0
  13. package/android/.gradle/buildOutputCleanup/cache.properties +2 -0
  14. package/android/.gradle/buildOutputCleanup/outputFiles.bin +0 -0
  15. package/android/.gradle/file-system.probe +0 -0
  16. package/android/.gradle/vcs-1/gc.properties +0 -0
  17. package/android/build.gradle +9 -0
  18. package/android/src/main/AndroidManifest.xml +5 -0
  19. package/android/src/main/java/com/ahm/capacitor/camera/preview/CameraPreview.java +260 -551
  20. package/android/src/main/java/com/ahm/capacitor/camera/preview/CameraXView.java +968 -0
  21. package/android/src/main/java/com/ahm/capacitor/camera/preview/model/CameraDevice.java +54 -0
  22. package/android/src/main/java/com/ahm/capacitor/camera/preview/model/CameraLens.java +70 -0
  23. package/android/src/main/java/com/ahm/capacitor/camera/preview/model/CameraSessionConfiguration.java +65 -0
  24. package/android/src/main/java/com/ahm/capacitor/camera/preview/model/LensInfo.java +34 -0
  25. package/android/src/main/java/com/ahm/capacitor/camera/preview/model/ZoomFactors.java +34 -0
  26. package/dist/docs.json +729 -153
  27. package/dist/esm/definitions.d.ts +337 -80
  28. package/dist/esm/definitions.js +10 -1
  29. package/dist/esm/definitions.js.map +1 -1
  30. package/dist/esm/web.d.ts +27 -1
  31. package/dist/esm/web.js +248 -4
  32. package/dist/esm/web.js.map +1 -1
  33. package/dist/plugin.cjs.js +256 -4
  34. package/dist/plugin.cjs.js.map +1 -1
  35. package/dist/plugin.js +256 -4
  36. package/dist/plugin.js.map +1 -1
  37. package/ios/{Plugin → Sources/CapgoCameraPreview}/CameraController.swift +359 -34
  38. package/ios/{Plugin → Sources/CapgoCameraPreview}/Plugin.swift +348 -42
  39. package/ios/Tests/CameraPreviewPluginTests/CameraPreviewPluginTests.swift +15 -0
  40. package/package.json +1 -1
  41. package/android/src/main/java/com/ahm/capacitor/camera/preview/CameraActivity.java +0 -1279
  42. package/android/src/main/java/com/ahm/capacitor/camera/preview/CustomSurfaceView.java +0 -29
  43. package/android/src/main/java/com/ahm/capacitor/camera/preview/CustomTextureView.java +0 -39
  44. package/android/src/main/java/com/ahm/capacitor/camera/preview/Preview.java +0 -461
  45. package/android/src/main/java/com/ahm/capacitor/camera/preview/TapGestureDetector.java +0 -24
  46. package/ios/Plugin/Info.plist +0 -24
  47. package/ios/Plugin/Plugin.h +0 -10
  48. package/ios/Plugin/Plugin.m +0 -18
  49. package/ios/Plugin.xcodeproj/project.pbxproj +0 -593
  50. package/ios/Plugin.xcodeproj/project.xcworkspace/contents.xcworkspacedata +0 -7
  51. package/ios/Plugin.xcworkspace/contents.xcworkspacedata +0 -10
  52. package/ios/Plugin.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +0 -8
  53. package/ios/PluginTests/Info.plist +0 -22
  54. package/ios/PluginTests/PluginTests.swift +0 -83
  55. package/ios/Podfile +0 -13
  56. package/ios/Podfile.lock +0 -23
@@ -40,34 +40,78 @@ class CameraController: NSObject {
40
40
  var zoomFactor: CGFloat = 1.0
41
41
 
42
42
  var videoFileURL: URL?
43
+ private let saneMaxZoomFactor: CGFloat = 25.5
44
+
45
+ var isUsingMultiLensVirtualCamera: Bool {
46
+ guard let device = (currentCameraPosition == .rear) ? rearCamera : frontCamera else { return false }
47
+ // A rear multi-lens virtual camera will have a min zoom of 1.0 but support wider angles
48
+ return device.position == .back && device.isVirtualDevice && device.constituentDevices.count > 1
49
+ }
43
50
  }
44
51
 
45
52
  extension CameraController {
46
- func prepare(cameraPosition: String, disableAudio: Bool, cameraMode: Bool, completionHandler: @escaping (Error?) -> Void) {
53
+ func prepare(cameraPosition: String, deviceId: String? = nil, disableAudio: Bool, cameraMode: Bool, completionHandler: @escaping (Error?) -> Void) {
47
54
  func createCaptureSession() {
48
55
  self.captureSession = AVCaptureSession()
49
56
  }
50
57
 
51
58
  func configureCaptureDevices() throws {
59
+ // Expanded device types to support more camera configurations
60
+ let deviceTypes: [AVCaptureDevice.DeviceType] = [
61
+ .builtInWideAngleCamera,
62
+ .builtInUltraWideCamera,
63
+ .builtInTelephotoCamera,
64
+ .builtInDualCamera,
65
+ .builtInDualWideCamera,
66
+ .builtInTripleCamera,
67
+ .builtInTrueDepthCamera
68
+ ]
52
69
 
53
- let session = AVCaptureDevice.DiscoverySession(deviceTypes: [.builtInWideAngleCamera], mediaType: AVMediaType.video, position: .unspecified)
70
+ let session = AVCaptureDevice.DiscoverySession(deviceTypes: deviceTypes, mediaType: AVMediaType.video, position: .unspecified)
54
71
 
55
72
  let cameras = session.devices.compactMap { $0 }
56
- guard !cameras.isEmpty else { throw CameraControllerError.noCamerasAvailable }
57
73
 
74
+ // Log all found devices for debugging
75
+ print("[CameraPreview] Found \(cameras.count) devices:")
58
76
  for camera in cameras {
59
- if camera.position == .front {
60
- self.frontCamera = camera
61
- }
77
+ let constituentCount = camera.isVirtualDevice ? camera.constituentDevices.count : 1
78
+ print("[CameraPreview] - \(camera.localizedName) (Position: \(camera.position.rawValue), Virtual: \(camera.isVirtualDevice), Lenses: \(constituentCount), Zoom: \(camera.minAvailableVideoZoomFactor)-\(camera.maxAvailableVideoZoomFactor))")
79
+ }
62
80
 
63
- if camera.position == .back {
64
- self.rearCamera = camera
81
+ guard !cameras.isEmpty else {
82
+ print("[CameraPreview] ERROR: No cameras found.")
83
+ throw CameraControllerError.noCamerasAvailable
84
+ }
85
+
86
+ // --- Corrected Device Selection Logic ---
87
+ // Find the virtual device with the most constituent cameras (this is the most capable one)
88
+ let rearVirtualDevices = cameras.filter { $0.position == .back && $0.isVirtualDevice }
89
+ let bestRearVirtualDevice = rearVirtualDevices.max { $0.constituentDevices.count < $1.constituentDevices.count }
90
+
91
+ self.frontCamera = cameras.first(where: { $0.position == .front })
92
+
93
+ if let bestCamera = bestRearVirtualDevice {
94
+ self.rearCamera = bestCamera
95
+ print("[CameraPreview] Selected best virtual rear camera: \(bestCamera.localizedName) with \(bestCamera.constituentDevices.count) physical cameras.")
96
+ } else if let firstRearCamera = cameras.first(where: { $0.position == .back }) {
97
+ // Fallback for devices without a virtual camera system
98
+ self.rearCamera = firstRearCamera
99
+ print("[CameraPreview] WARN: No virtual rear camera found. Selected first available: \(firstRearCamera.localizedName)")
100
+ }
101
+ // --- End of Correction ---
65
102
 
66
- try camera.lockForConfiguration()
67
- camera.focusMode = .continuousAutoFocus
68
- camera.unlockForConfiguration()
103
+ if let rearCamera = self.rearCamera {
104
+ do {
105
+ try rearCamera.lockForConfiguration()
106
+ if rearCamera.isFocusModeSupported(.continuousAutoFocus) {
107
+ rearCamera.focusMode = .continuousAutoFocus
108
+ }
109
+ rearCamera.unlockForConfiguration()
110
+ } catch {
111
+ print("[CameraPreview] WARN: Could not set focus mode on rear camera. \(error)")
69
112
  }
70
113
  }
114
+
71
115
  if disableAudio == false {
72
116
  self.audioDevice = AVCaptureDevice.default(for: AVMediaType.audio)
73
117
  }
@@ -76,23 +120,72 @@ extension CameraController {
76
120
  func configureDeviceInputs() throws {
77
121
  guard let captureSession = self.captureSession else { throw CameraControllerError.captureSessionIsMissing }
78
122
 
79
- if cameraPosition == "rear" {
80
- if let rearCamera = self.rearCamera {
81
- self.rearCameraInput = try AVCaptureDeviceInput(device: rearCamera)
123
+ var selectedDevice: AVCaptureDevice?
82
124
 
83
- if captureSession.canAddInput(self.rearCameraInput!) { captureSession.addInput(self.rearCameraInput!) }
125
+ // If deviceId is specified, use that specific device
126
+ if let deviceId = deviceId {
127
+ let allDevices = AVCaptureDevice.DiscoverySession(
128
+ deviceTypes: [.builtInWideAngleCamera, .builtInUltraWideCamera, .builtInTelephotoCamera, .builtInDualCamera, .builtInDualWideCamera, .builtInTripleCamera, .builtInTrueDepthCamera],
129
+ mediaType: .video,
130
+ position: .unspecified
131
+ ).devices
84
132
 
85
- self.currentCameraPosition = .rear
133
+ selectedDevice = allDevices.first(where: { $0.uniqueID == deviceId })
134
+ guard selectedDevice != nil else {
135
+ throw CameraControllerError.noCamerasAvailable
86
136
  }
87
- } else if cameraPosition == "front" {
88
- if let frontCamera = self.frontCamera {
89
- self.frontCameraInput = try AVCaptureDeviceInput(device: frontCamera)
137
+ } else {
138
+ // Use position-based selection
139
+ if cameraPosition == "rear" {
140
+ selectedDevice = self.rearCamera
141
+ } else if cameraPosition == "front" {
142
+ selectedDevice = self.frontCamera
143
+ }
144
+ }
90
145
 
91
- if captureSession.canAddInput(self.frontCameraInput!) { captureSession.addInput(self.frontCameraInput!) } else { throw CameraControllerError.inputsAreInvalid }
146
+ guard let finalDevice = selectedDevice else {
147
+ print("[CameraPreview] ERROR: No camera device selected for position: \(cameraPosition)")
148
+ throw CameraControllerError.noCamerasAvailable
149
+ }
150
+
151
+ print("[CameraPreview] Configuring device: \(finalDevice.localizedName)")
152
+ let deviceInput = try AVCaptureDeviceInput(device: finalDevice)
92
153
 
154
+ if captureSession.canAddInput(deviceInput) {
155
+ captureSession.addInput(deviceInput)
156
+
157
+ if finalDevice.position == .front {
158
+ self.frontCameraInput = deviceInput
159
+ self.frontCamera = finalDevice
93
160
  self.currentCameraPosition = .front
161
+ } else {
162
+ self.rearCameraInput = deviceInput
163
+ self.rearCamera = finalDevice
164
+ self.currentCameraPosition = .rear
165
+
166
+ // --- Corrected Initial Zoom Logic ---
167
+ try finalDevice.lockForConfiguration()
168
+ if finalDevice.isFocusModeSupported(.continuousAutoFocus) {
169
+ finalDevice.focusMode = .continuousAutoFocus
170
+ }
171
+
172
+ // On a multi-camera system, a zoom factor of 2.0 often corresponds to the standard "1x" wide-angle lens.
173
+ // We set this as the default to provide a familiar starting point for users.
174
+ let defaultWideAngleZoom: CGFloat = 2.0
175
+ if finalDevice.isVirtualDevice && finalDevice.constituentDevices.count > 1 && finalDevice.videoZoomFactor != defaultWideAngleZoom {
176
+ // Check if 2.0 is a valid zoom factor before setting it.
177
+ if defaultWideAngleZoom >= finalDevice.minAvailableVideoZoomFactor && defaultWideAngleZoom <= finalDevice.maxAvailableVideoZoomFactor {
178
+ print("[CameraPreview] Multi-camera system detected. Setting initial zoom to \(defaultWideAngleZoom) (standard wide-angle).")
179
+ finalDevice.videoZoomFactor = defaultWideAngleZoom
180
+ }
181
+ }
182
+ finalDevice.unlockForConfiguration()
183
+ // --- End of Correction ---
94
184
  }
95
- } else { throw CameraControllerError.noCamerasAvailable }
185
+ } else {
186
+ print("[CameraPreview] ERROR: Cannot add device input to session.")
187
+ throw CameraControllerError.inputsAreInvalid
188
+ }
96
189
 
97
190
  // Add audio input
98
191
  if disableAudio == false {
@@ -213,7 +306,7 @@ extension CameraController {
213
306
 
214
307
  private func updateVideoOrientationOnMainThread() {
215
308
  let videoOrientation: AVCaptureVideoOrientation
216
-
309
+
217
310
  // Use window scene interface orientation
218
311
  if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene {
219
312
  switch windowScene.interfaceOrientation {
@@ -244,19 +337,19 @@ extension CameraController {
244
337
  let captureSession = self.captureSession else {
245
338
  throw CameraControllerError.captureSessionIsMissing
246
339
  }
247
-
340
+
248
341
  // Ensure we have the necessary cameras
249
342
  guard (currentCameraPosition == .front && rearCamera != nil) ||
250
343
  (currentCameraPosition == .rear && frontCamera != nil) else {
251
344
  throw CameraControllerError.noCamerasAvailable
252
345
  }
253
-
346
+
254
347
  // Store the current running state
255
348
  let wasRunning = captureSession.isRunning
256
349
  if wasRunning {
257
350
  captureSession.stopRunning()
258
351
  }
259
-
352
+
260
353
  // Begin configuration
261
354
  captureSession.beginConfiguration()
262
355
  defer {
@@ -268,7 +361,7 @@ extension CameraController {
268
361
  }
269
362
  }
270
363
  }
271
-
364
+
272
365
  // Store audio input if it exists
273
366
  let audioInput = captureSession.inputs.first { ($0 as? AVCaptureDeviceInput)?.device.hasMediaType(.audio) ?? false }
274
367
 
@@ -278,21 +371,21 @@ extension CameraController {
278
371
  captureSession.removeInput(input)
279
372
  }
280
373
  }
281
-
374
+
282
375
  // Configure new camera
283
376
  switch currentCameraPosition {
284
377
  case .front:
285
378
  guard let rearCamera = rearCamera else {
286
379
  throw CameraControllerError.invalidOperation
287
380
  }
288
-
381
+
289
382
  // Configure rear camera
290
383
  try rearCamera.lockForConfiguration()
291
384
  if rearCamera.isFocusModeSupported(.continuousAutoFocus) {
292
385
  rearCamera.focusMode = .continuousAutoFocus
293
386
  }
294
387
  rearCamera.unlockForConfiguration()
295
-
388
+
296
389
  if let newInput = try? AVCaptureDeviceInput(device: rearCamera),
297
390
  captureSession.canAddInput(newInput) {
298
391
  captureSession.addInput(newInput)
@@ -306,13 +399,13 @@ extension CameraController {
306
399
  throw CameraControllerError.invalidOperation
307
400
  }
308
401
 
309
- // Configure front camera
402
+ // Configure front camera
310
403
  try frontCamera.lockForConfiguration()
311
404
  if frontCamera.isFocusModeSupported(.continuousAutoFocus) {
312
405
  frontCamera.focusMode = .continuousAutoFocus
313
406
  }
314
407
  frontCamera.unlockForConfiguration()
315
-
408
+
316
409
  if let newInput = try? AVCaptureDeviceInput(device: frontCamera),
317
410
  captureSession.canAddInput(newInput) {
318
411
  captureSession.addInput(newInput)
@@ -327,7 +420,7 @@ extension CameraController {
327
420
  if let audioInput = audioInput, captureSession.canAddInput(audioInput) {
328
421
  captureSession.addInput(audioInput)
329
422
  }
330
-
423
+
331
424
  // Update video orientation
332
425
  DispatchQueue.main.async { [weak self] in
333
426
  self?.updateVideoOrientation()
@@ -415,8 +508,15 @@ extension CameraController {
415
508
  throw CameraControllerError.noCamerasAvailable
416
509
  }
417
510
 
418
- return device.activeFormat.videoFieldOfView
511
+ // Get the active format and field of view
512
+ let activeFormat = device.activeFormat
513
+ let fov = activeFormat.videoFieldOfView
419
514
 
515
+ // Adjust for current zoom level
516
+ let zoomFactor = device.videoZoomFactor
517
+ let adjustedFov = fov / Float(zoomFactor)
518
+
519
+ return adjustedFov
420
520
  }
421
521
  func setFlashMode(flashMode: AVCaptureDevice.FlashMode) throws {
422
522
  var currentCamera: AVCaptureDevice?
@@ -489,6 +589,230 @@ extension CameraController {
489
589
  }
490
590
  }
491
591
 
592
+ func getZoom() throws -> (min: Float, max: Float, current: Float) {
593
+ var currentCamera: AVCaptureDevice?
594
+ switch currentCameraPosition {
595
+ case .front:
596
+ currentCamera = self.frontCamera
597
+ case .rear:
598
+ currentCamera = self.rearCamera
599
+ default: break
600
+ }
601
+
602
+ guard let device = currentCamera else {
603
+ throw CameraControllerError.noCamerasAvailable
604
+ }
605
+
606
+ let effectiveMaxZoom = min(device.maxAvailableVideoZoomFactor, self.saneMaxZoomFactor)
607
+
608
+ return (
609
+ min: Float(device.minAvailableVideoZoomFactor),
610
+ max: Float(effectiveMaxZoom),
611
+ current: Float(device.videoZoomFactor)
612
+ )
613
+ }
614
+
615
+ func setZoom(level: CGFloat, ramp: Bool) throws {
616
+ var currentCamera: AVCaptureDevice?
617
+ switch currentCameraPosition {
618
+ case .front:
619
+ currentCamera = self.frontCamera
620
+ case .rear:
621
+ currentCamera = self.rearCamera
622
+ default: break
623
+ }
624
+
625
+ guard let device = currentCamera else {
626
+ throw CameraControllerError.noCamerasAvailable
627
+ }
628
+
629
+ let effectiveMaxZoom = min(device.maxAvailableVideoZoomFactor, self.saneMaxZoomFactor)
630
+ let zoomLevel = max(device.minAvailableVideoZoomFactor, min(level, effectiveMaxZoom))
631
+
632
+ do {
633
+ try device.lockForConfiguration()
634
+
635
+ if ramp {
636
+ device.ramp(toVideoZoomFactor: zoomLevel, withRate: 1.0)
637
+ } else {
638
+ device.videoZoomFactor = zoomLevel
639
+ }
640
+
641
+ device.unlockForConfiguration()
642
+
643
+ // Update our internal zoom factor tracking
644
+ self.zoomFactor = zoomLevel
645
+ } catch {
646
+ throw CameraControllerError.invalidOperation
647
+ }
648
+ }
649
+
650
+ func getFlashMode() throws -> String {
651
+ switch self.flashMode {
652
+ case .off:
653
+ return "off"
654
+ case .on:
655
+ return "on"
656
+ case .auto:
657
+ return "auto"
658
+ @unknown default:
659
+ return "off"
660
+ }
661
+ }
662
+
663
+ func getCurrentDeviceId() throws -> String {
664
+ var currentCamera: AVCaptureDevice?
665
+ switch currentCameraPosition {
666
+ case .front:
667
+ currentCamera = self.frontCamera
668
+ case .rear:
669
+ currentCamera = self.rearCamera
670
+ default:
671
+ break
672
+ }
673
+
674
+ guard let device = currentCamera else {
675
+ throw CameraControllerError.noCamerasAvailable
676
+ }
677
+
678
+ return device.uniqueID
679
+ }
680
+
681
+ func getCurrentLensInfo() throws -> (focalLength: Float, deviceType: String, baseZoomRatio: Float) {
682
+ var currentCamera: AVCaptureDevice?
683
+ switch currentCameraPosition {
684
+ case .front:
685
+ currentCamera = self.frontCamera
686
+ case .rear:
687
+ currentCamera = self.rearCamera
688
+ default:
689
+ break
690
+ }
691
+
692
+ guard let device = currentCamera else {
693
+ throw CameraControllerError.noCamerasAvailable
694
+ }
695
+
696
+ var deviceType = "wideAngle"
697
+ var baseZoomRatio: Float = 1.0
698
+
699
+ switch device.deviceType {
700
+ case .builtInWideAngleCamera:
701
+ deviceType = "wideAngle"
702
+ baseZoomRatio = 1.0
703
+ case .builtInUltraWideCamera:
704
+ deviceType = "ultraWide"
705
+ baseZoomRatio = 0.5
706
+ case .builtInTelephotoCamera:
707
+ deviceType = "telephoto"
708
+ baseZoomRatio = 2.0
709
+ case .builtInDualCamera:
710
+ deviceType = "dual"
711
+ baseZoomRatio = 1.0
712
+ case .builtInDualWideCamera:
713
+ deviceType = "dualWide"
714
+ baseZoomRatio = 1.0
715
+ case .builtInTripleCamera:
716
+ deviceType = "triple"
717
+ baseZoomRatio = 1.0
718
+ case .builtInTrueDepthCamera:
719
+ deviceType = "trueDepth"
720
+ baseZoomRatio = 1.0
721
+ default:
722
+ deviceType = "wideAngle"
723
+ baseZoomRatio = 1.0
724
+ }
725
+
726
+ // Approximate focal length for mobile devices
727
+ let focalLength: Float = 4.25
728
+
729
+ return (focalLength: focalLength, deviceType: deviceType, baseZoomRatio: baseZoomRatio)
730
+ }
731
+
732
+ func swapToDevice(deviceId: String) throws {
733
+ guard let captureSession = self.captureSession else {
734
+ throw CameraControllerError.captureSessionIsMissing
735
+ }
736
+
737
+ // Find the device with the specified deviceId
738
+ let allDevices = AVCaptureDevice.DiscoverySession(
739
+ deviceTypes: [.builtInWideAngleCamera, .builtInUltraWideCamera, .builtInTelephotoCamera, .builtInDualCamera, .builtInDualWideCamera, .builtInTripleCamera, .builtInTrueDepthCamera],
740
+ mediaType: .video,
741
+ position: .unspecified
742
+ ).devices
743
+
744
+ guard let targetDevice = allDevices.first(where: { $0.uniqueID == deviceId }) else {
745
+ throw CameraControllerError.noCamerasAvailable
746
+ }
747
+
748
+ // Store the current running state
749
+ let wasRunning = captureSession.isRunning
750
+ if wasRunning {
751
+ captureSession.stopRunning()
752
+ }
753
+
754
+ // Begin configuration
755
+ captureSession.beginConfiguration()
756
+ defer {
757
+ captureSession.commitConfiguration()
758
+ // Restart the session if it was running before
759
+ if wasRunning {
760
+ DispatchQueue.global(qos: .userInitiated).async { [weak self] in
761
+ self?.captureSession?.startRunning()
762
+ }
763
+ }
764
+ }
765
+
766
+ // Store audio input if it exists
767
+ let audioInput = captureSession.inputs.first { ($0 as? AVCaptureDeviceInput)?.device.hasMediaType(.audio) ?? false }
768
+
769
+ // Remove only video inputs
770
+ captureSession.inputs.forEach { input in
771
+ if (input as? AVCaptureDeviceInput)?.device.hasMediaType(.video) ?? false {
772
+ captureSession.removeInput(input)
773
+ }
774
+ }
775
+
776
+ // Configure the new device
777
+ let newInput = try AVCaptureDeviceInput(device: targetDevice)
778
+
779
+ if captureSession.canAddInput(newInput) {
780
+ captureSession.addInput(newInput)
781
+
782
+ // Update camera references based on device position
783
+ if targetDevice.position == .front {
784
+ self.frontCameraInput = newInput
785
+ self.frontCamera = targetDevice
786
+ self.currentCameraPosition = .front
787
+ } else {
788
+ self.rearCameraInput = newInput
789
+ self.rearCamera = targetDevice
790
+ self.currentCameraPosition = .rear
791
+
792
+ // Configure rear camera
793
+ try targetDevice.lockForConfiguration()
794
+ if targetDevice.isFocusModeSupported(.continuousAutoFocus) {
795
+ targetDevice.focusMode = .continuousAutoFocus
796
+ }
797
+ targetDevice.unlockForConfiguration()
798
+ }
799
+ } else {
800
+ throw CameraControllerError.invalidOperation
801
+ }
802
+
803
+ // Re-add audio input if it existed
804
+ if let audioInput = audioInput, captureSession.canAddInput(audioInput) {
805
+ captureSession.addInput(audioInput)
806
+ }
807
+
808
+ // Update video orientation
809
+ DispatchQueue.main.async { [weak self] in
810
+ self?.updateVideoOrientation()
811
+ }
812
+ }
813
+
814
+
815
+
492
816
  func cleanup() {
493
817
  if let captureSession = self.captureSession {
494
818
  captureSession.stopRunning()
@@ -613,7 +937,8 @@ extension CameraController: UIGestureRecognizerDelegate {
613
937
  private func handlePinch(_ pinch: UIPinchGestureRecognizer) {
614
938
  guard let device = self.currentCameraPosition == .rear ? rearCamera : frontCamera else { return }
615
939
 
616
- func minMaxZoom(_ factor: CGFloat) -> CGFloat { return max(1.0, min(factor, device.activeFormat.videoMaxZoomFactor)) }
940
+ let effectiveMaxZoom = min(device.maxAvailableVideoZoomFactor, self.saneMaxZoomFactor)
941
+ func minMaxZoom(_ factor: CGFloat) -> CGFloat { return max(device.minAvailableVideoZoomFactor, min(factor, effectiveMaxZoom)) }
617
942
 
618
943
  func update(scale factor: CGFloat) {
619
944
  do {