@capgo/camera-preview 7.4.0-beta.1 → 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.
Files changed (35) hide show
  1. package/README.md +195 -31
  2. package/android/.gradle/8.14.2/checksums/checksums.lock +0 -0
  3. package/android/.gradle/8.14.2/checksums/md5-checksums.bin +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/fileHashes/fileHashes.bin +0 -0
  8. package/android/.gradle/8.14.2/fileHashes/fileHashes.lock +0 -0
  9. package/android/.gradle/8.14.2/fileHashes/resourceHashesCache.bin +0 -0
  10. package/android/.gradle/buildOutputCleanup/buildOutputCleanup.lock +0 -0
  11. package/android/.gradle/file-system.probe +0 -0
  12. package/android/build.gradle +3 -1
  13. package/android/src/main/AndroidManifest.xml +5 -3
  14. package/android/src/main/java/com/ahm/capacitor/camera/preview/CameraPreview.java +473 -88
  15. package/android/src/main/java/com/ahm/capacitor/camera/preview/CameraXView.java +2065 -704
  16. package/android/src/main/java/com/ahm/capacitor/camera/preview/GridOverlayView.java +95 -0
  17. package/android/src/main/java/com/ahm/capacitor/camera/preview/model/CameraDevice.java +55 -46
  18. package/android/src/main/java/com/ahm/capacitor/camera/preview/model/CameraLens.java +61 -52
  19. package/android/src/main/java/com/ahm/capacitor/camera/preview/model/CameraSessionConfiguration.java +152 -59
  20. package/android/src/main/java/com/ahm/capacitor/camera/preview/model/LensInfo.java +29 -23
  21. package/android/src/main/java/com/ahm/capacitor/camera/preview/model/ZoomFactors.java +24 -23
  22. package/dist/docs.json +235 -6
  23. package/dist/esm/definitions.d.ts +119 -3
  24. package/dist/esm/definitions.js.map +1 -1
  25. package/dist/esm/web.d.ts +47 -3
  26. package/dist/esm/web.js +297 -96
  27. package/dist/esm/web.js.map +1 -1
  28. package/dist/plugin.cjs.js +293 -96
  29. package/dist/plugin.cjs.js.map +1 -1
  30. package/dist/plugin.js +293 -96
  31. package/dist/plugin.js.map +1 -1
  32. package/ios/Sources/CapgoCameraPreview/CameraController.swift +364 -218
  33. package/ios/Sources/CapgoCameraPreview/GridOverlayView.swift +65 -0
  34. package/ios/Sources/CapgoCameraPreview/Plugin.swift +886 -242
  35. package/package.json +1 -1
@@ -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?
@@ -23,9 +24,12 @@ class CameraController: NSObject {
23
24
  var rearCamera: AVCaptureDevice?
24
25
  var rearCameraInput: AVCaptureDeviceInput?
25
26
 
27
+ var allDiscoveredDevices: [AVCaptureDevice] = []
28
+
26
29
  var fileVideoOutput: AVCaptureMovieFileOutput?
27
30
 
28
31
  var previewLayer: AVCaptureVideoPreviewLayer?
32
+ var gridOverlayView: GridOverlayView?
29
33
 
30
34
  var flashMode = AVCaptureDevice.FlashMode.off
31
35
  var photoCaptureCompletionBlock: ((UIImage?, Error?) -> Void)?
@@ -38,6 +42,8 @@ class CameraController: NSObject {
38
42
  var audioInput: AVCaptureDeviceInput?
39
43
 
40
44
  var zoomFactor: CGFloat = 1.0
45
+ private var lastZoomUpdateTime: TimeInterval = 0
46
+ private let zoomUpdateThrottle: TimeInterval = 1.0 / 60.0 // 60 FPS max
41
47
 
42
48
  var videoFileURL: URL?
43
49
  private let saneMaxZoomFactor: CGFloat = 25.5
@@ -50,231 +56,293 @@ class CameraController: NSObject {
50
56
  }
51
57
 
52
58
  extension CameraController {
53
- func prepare(cameraPosition: String, deviceId: String? = nil, disableAudio: Bool, cameraMode: Bool, completionHandler: @escaping (Error?) -> Void) {
54
- func createCaptureSession() {
55
- self.captureSession = AVCaptureSession()
56
- }
57
-
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
- ]
69
-
70
- let session = AVCaptureDevice.DiscoverySession(deviceTypes: deviceTypes, mediaType: AVMediaType.video, position: .unspecified)
71
-
72
- let cameras = session.devices.compactMap { $0 }
73
-
74
- // Log all found devices for debugging
75
- print("[CameraPreview] Found \(cameras.count) devices:")
76
- for camera in cameras {
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
- }
59
+ func prepareFullSession() {
60
+ // Only prepare if we don't already have a session
61
+ guard self.captureSession == nil else { return }
80
62
 
81
- guard !cameras.isEmpty else {
82
- print("[CameraPreview] ERROR: No cameras found.")
83
- throw CameraControllerError.noCamerasAvailable
84
- }
63
+ print("[CameraPreview] Preparing full camera session in background")
85
64
 
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 }
65
+ // 1. Create and configure session
66
+ self.captureSession = AVCaptureSession()
90
67
 
91
- self.frontCamera = cameras.first(where: { $0.position == .front })
68
+ // 2. Pre-configure session preset (can be changed later)
69
+ if captureSession!.canSetSessionPreset(.medium) {
70
+ captureSession!.sessionPreset = .medium
71
+ }
92
72
 
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 ---
73
+ // 3. Discover and configure all cameras
74
+ discoverAndConfigureCameras()
102
75
 
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)")
112
- }
113
- }
76
+ // 4. Pre-create outputs (don't add to session yet)
77
+ prepareOutputs()
114
78
 
115
- if disableAudio == false {
116
- self.audioDevice = AVCaptureDevice.default(for: AVMediaType.audio)
117
- }
79
+ print("[CameraPreview] Full session preparation complete")
80
+ }
81
+
82
+ private func discoverAndConfigureCameras() {
83
+ let deviceTypes: [AVCaptureDevice.DeviceType] = [
84
+ .builtInWideAngleCamera,
85
+ .builtInUltraWideCamera,
86
+ .builtInTelephotoCamera,
87
+ .builtInDualCamera,
88
+ .builtInDualWideCamera,
89
+ .builtInTripleCamera,
90
+ .builtInTrueDepthCamera
91
+ ]
92
+
93
+ let session = AVCaptureDevice.DiscoverySession(deviceTypes: deviceTypes, mediaType: AVMediaType.video, position: .unspecified)
94
+ let cameras = session.devices.compactMap { $0 }
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))")
118
104
  }
119
105
 
120
- func configureDeviceInputs() throws {
121
- guard let captureSession = self.captureSession else { throw CameraControllerError.captureSessionIsMissing }
106
+ // Find best cameras
107
+ let rearVirtualDevices = cameras.filter { $0.position == .back && $0.isVirtualDevice }
108
+ let bestRearVirtualDevice = rearVirtualDevices.max { $0.constituentDevices.count < $1.constituentDevices.count }
122
109
 
123
- var selectedDevice: AVCaptureDevice?
110
+ self.frontCamera = cameras.first(where: { $0.position == .front })
124
111
 
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
112
+ if let bestCamera = bestRearVirtualDevice {
113
+ self.rearCamera = bestCamera
114
+ print("[CameraPreview] Selected best virtual rear camera: \(bestCamera.localizedName) with \(bestCamera.constituentDevices.count) physical cameras.")
115
+ } else if let firstRearCamera = cameras.first(where: { $0.position == .back }) {
116
+ self.rearCamera = firstRearCamera
117
+ print("[CameraPreview] WARN: No virtual rear camera found. Selected first available: \(firstRearCamera.localizedName)")
118
+ }
132
119
 
133
- selectedDevice = allDevices.first(where: { $0.uniqueID == deviceId })
134
- guard selectedDevice != nil else {
135
- throw CameraControllerError.noCamerasAvailable
136
- }
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
- }
120
+ // Pre-configure focus modes
121
+ configureCameraFocus(camera: self.rearCamera)
122
+ configureCameraFocus(camera: self.frontCamera)
123
+ }
145
124
 
146
- guard let finalDevice = selectedDevice else {
147
- print("[CameraPreview] ERROR: No camera device selected for position: \(cameraPosition)")
148
- throw CameraControllerError.noCamerasAvailable
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
149
132
  }
133
+ camera.unlockForConfiguration()
134
+ } catch {
135
+ print("[CameraPreview] Could not configure focus for \(camera.localizedName): \(error)")
136
+ }
137
+ }
150
138
 
151
- print("[CameraPreview] Configuring device: \(finalDevice.localizedName)")
152
- let deviceInput = try AVCaptureDeviceInput(device: finalDevice)
139
+ private func prepareOutputs() {
140
+ // Pre-create photo output
141
+ self.photoOutput = AVCapturePhotoOutput()
142
+ self.photoOutput?.isHighResolutionCaptureEnabled = false // Default, can be changed
153
143
 
154
- if captureSession.canAddInput(deviceInput) {
155
- captureSession.addInput(deviceInput)
144
+ // Pre-create video output
145
+ self.fileVideoOutput = AVCaptureMovieFileOutput()
156
146
 
157
- if finalDevice.position == .front {
158
- self.frontCameraInput = deviceInput
159
- self.frontCamera = finalDevice
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
- }
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
171
153
 
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 ---
184
- }
185
- } else {
186
- print("[CameraPreview] ERROR: Cannot add device input to session.")
187
- throw CameraControllerError.inputsAreInvalid
154
+ print("[CameraPreview] Outputs pre-created")
155
+ }
156
+
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()
188
163
  }
189
164
 
190
- // Add audio input
191
- if disableAudio == false {
192
- if let audioDevice = self.audioDevice {
193
- self.audioInput = try AVCaptureDeviceInput(device: audioDevice)
194
- if captureSession.canAddInput(self.audioInput!) {
195
- captureSession.addInput(self.audioInput!)
196
- } else {
197
- throw CameraControllerError.inputsAreInvalid
198
- }
199
- }
165
+ guard let captureSession = self.captureSession else {
166
+ throw CameraControllerError.captureSessionIsMissing
200
167
  }
201
- }
202
168
 
203
- func configurePhotoOutput(cameraMode: Bool) throws {
204
- guard let captureSession = self.captureSession else { throw CameraControllerError.captureSessionIsMissing }
169
+ print("[CameraPreview] Fast prepare - using pre-initialized session")
205
170
 
206
- // TODO: check if that really useful
207
- if !cameraMode && self.highResolutionOutput && captureSession.canSetSessionPreset(.photo) {
208
- captureSession.sessionPreset = .photo
209
- } else if cameraMode && self.highResolutionOutput && captureSession.canSetSessionPreset(.high) {
210
- captureSession.sessionPreset = .high
211
- }
171
+ // Configure device inputs for the requested camera
172
+ try self.configureDeviceInputs(cameraPosition: cameraPosition, deviceId: deviceId, disableAudio: disableAudio)
212
173
 
213
- self.photoOutput = AVCapturePhotoOutput()
214
- self.photoOutput!.setPreparedPhotoSettingsArray([AVCapturePhotoSettings(format: [AVVideoCodecKey: AVVideoCodecType.jpeg])], completionHandler: nil)
215
- self.photoOutput?.isHighResolutionCaptureEnabled = self.highResolutionOutput
216
- if captureSession.canAddOutput(self.photoOutput!) { captureSession.addOutput(self.photoOutput!) }
174
+ // Add outputs to session and apply user settings
175
+ try self.addOutputsToSession(cameraMode: cameraMode, aspectRatio: aspectRatio)
217
176
 
218
- let fileVideoOutput = AVCaptureMovieFileOutput()
219
- if captureSession.canAddOutput(fileVideoOutput) {
220
- captureSession.addOutput(fileVideoOutput)
221
- self.fileVideoOutput = fileVideoOutput
222
- }
177
+ // Start the session
223
178
  captureSession.startRunning()
179
+ print("[CameraPreview] Session started")
180
+
181
+ completionHandler(nil)
182
+ } catch {
183
+ completionHandler(error)
224
184
  }
185
+ }
186
+
187
+ private func configureDeviceInputs(cameraPosition: String, deviceId: String?, disableAudio: Bool) throws {
188
+ guard let captureSession = self.captureSession else { throw CameraControllerError.captureSessionIsMissing }
225
189
 
226
- func configureDataOutput() throws {
227
- guard let captureSession = self.captureSession else { throw CameraControllerError.captureSessionIsMissing }
190
+ var selectedDevice: AVCaptureDevice?
228
191
 
229
- self.dataOutput = AVCaptureVideoDataOutput()
230
- self.dataOutput?.videoSettings = [
231
- (kCVPixelBufferPixelFormatTypeKey as String): NSNumber(value: kCVPixelFormatType_32BGRA as UInt32)
232
- ]
233
- self.dataOutput?.alwaysDiscardsLateVideoFrames = true
234
- if captureSession.canAddOutput(self.dataOutput!) {
235
- captureSession.addOutput(self.dataOutput!)
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")
197
+ throw CameraControllerError.noCamerasAvailable
236
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
+ }
237
207
 
238
- captureSession.commitConfiguration()
208
+ guard let finalDevice = selectedDevice else {
209
+ print("[CameraPreview] ERROR: No camera device selected for position: \(cameraPosition)")
210
+ throw CameraControllerError.noCamerasAvailable
211
+ }
239
212
 
240
- let queue = DispatchQueue(label: "DataOutput", attributes: [])
241
- self.dataOutput?.setSampleBufferDelegate(self, queue: queue)
213
+ print("[CameraPreview] Configuring device: \(finalDevice.localizedName)")
214
+ let deviceInput = try AVCaptureDeviceInput(device: finalDevice)
215
+
216
+ if captureSession.canAddInput(deviceInput) {
217
+ captureSession.addInput(deviceInput)
218
+
219
+ if finalDevice.position == .front {
220
+ self.frontCameraInput = deviceInput
221
+ self.currentCameraPosition = .front
222
+ } else {
223
+ self.rearCameraInput = deviceInput
224
+ self.currentCameraPosition = .rear
225
+
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
233
+ }
234
+ }
235
+ finalDevice.unlockForConfiguration()
236
+ }
237
+ } else {
238
+ throw CameraControllerError.inputsAreInvalid
242
239
  }
243
240
 
244
- DispatchQueue(label: "prepare").async {
245
- do {
246
- createCaptureSession()
247
- try configureCaptureDevices()
248
- try configureDeviceInputs()
249
- try configurePhotoOutput(cameraMode: cameraMode)
250
- try configureDataOutput()
251
- // try configureVideoOutput()
252
- } catch {
253
- DispatchQueue.main.async {
254
- completionHandler(error)
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!)
250
+ } else {
251
+ throw CameraControllerError.inputsAreInvalid
255
252
  }
253
+ }
254
+ }
255
+ }
256
256
 
257
- return
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
258
270
  }
259
271
 
260
- DispatchQueue.main.async {
261
- completionHandler(nil)
272
+ if captureSession.canSetSessionPreset(targetPreset) {
273
+ captureSession.sessionPreset = targetPreset
274
+ print("[CameraPreview] Updated preset to \(targetPreset) for aspect ratio: \(aspectRatio)")
262
275
  }
263
276
  }
277
+
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)
282
+ }
283
+
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
+ }
288
+
289
+ // Add data output
290
+ if let dataOutput = self.dataOutput, captureSession.canAddOutput(dataOutput) {
291
+ captureSession.addOutput(dataOutput)
292
+ captureSession.commitConfiguration()
293
+
294
+ dataOutput.setSampleBufferDelegate(self, queue: DispatchQueue.main)
295
+ }
264
296
  }
265
297
 
266
298
  func displayPreview(on view: UIView) throws {
267
299
  guard let captureSession = self.captureSession, captureSession.isRunning else { throw CameraControllerError.captureSessionIsMissing }
268
300
 
301
+ print("[CameraPreview] displayPreview called with view frame: \(view.frame)")
302
+
269
303
  self.previewLayer = AVCaptureVideoPreviewLayer(session: captureSession)
270
304
  self.previewLayer?.videoGravity = AVLayerVideoGravity.resizeAspectFill
271
305
 
306
+ // Optimize preview layer for better quality
307
+ self.previewLayer?.connection?.videoOrientation = .portrait
308
+ self.previewLayer?.isOpaque = true
309
+
310
+ // Enable high-quality rendering
311
+ if #available(iOS 13.0, *) {
312
+ self.previewLayer?.videoGravity = .resizeAspectFill
313
+ }
314
+
272
315
  view.layer.insertSublayer(self.previewLayer!, at: 0)
273
- self.previewLayer?.frame = view.frame
316
+
317
+ // Disable animation for frame update
318
+ CATransaction.begin()
319
+ CATransaction.setDisableActions(true)
320
+ self.previewLayer?.frame = view.bounds
321
+ CATransaction.commit()
322
+
323
+ print("[CameraPreview] Set preview layer frame to view bounds: \(view.bounds)")
324
+ print("[CameraPreview] Session preset: \(captureSession.sessionPreset.rawValue)")
274
325
 
275
326
  updateVideoOrientation()
276
327
  }
277
328
 
329
+ func addGridOverlay(to view: UIView, gridMode: String) {
330
+ removeGridOverlay()
331
+
332
+ // Disable animation for grid overlay creation and positioning
333
+ CATransaction.begin()
334
+ CATransaction.setDisableActions(true)
335
+ gridOverlayView = GridOverlayView(frame: view.bounds)
336
+ gridOverlayView?.gridMode = gridMode
337
+ view.addSubview(gridOverlayView!)
338
+ CATransaction.commit()
339
+ }
340
+
341
+ func removeGridOverlay() {
342
+ gridOverlayView?.removeFromSuperview()
343
+ gridOverlayView = nil
344
+ }
345
+
278
346
  func setupGestures(target: UIView, enableZoom: Bool) {
279
347
  setupTapGesture(target: target, selector: #selector(handleTap(_:)), delegate: self)
280
348
  if enableZoom {
@@ -291,6 +359,10 @@ extension CameraController {
291
359
  func setupPinchGesture(target: UIView, selector: Selector, delegate: UIGestureRecognizerDelegate?) {
292
360
  let pinchGesture = UIPinchGestureRecognizer(target: self, action: selector)
293
361
  pinchGesture.delegate = delegate
362
+ // Optimize gesture recognition for better performance
363
+ pinchGesture.delaysTouchesBegan = false
364
+ pinchGesture.delaysTouchesEnded = false
365
+ pinchGesture.cancelsTouchesInView = false
294
366
  target.addGestureRecognizer(pinchGesture)
295
367
  }
296
368
 
@@ -298,8 +370,8 @@ extension CameraController {
298
370
  if Thread.isMainThread {
299
371
  updateVideoOrientationOnMainThread()
300
372
  } else {
301
- DispatchQueue.main.async { [weak self] in
302
- self?.updateVideoOrientationOnMainThread()
373
+ DispatchQueue.main.sync {
374
+ self.updateVideoOrientationOnMainThread()
303
375
  }
304
376
  }
305
377
  }
@@ -340,7 +412,7 @@ extension CameraController {
340
412
 
341
413
  // Ensure we have the necessary cameras
342
414
  guard (currentCameraPosition == .front && rearCamera != nil) ||
343
- (currentCameraPosition == .rear && frontCamera != nil) else {
415
+ (currentCameraPosition == .rear && frontCamera != nil) else {
344
416
  throw CameraControllerError.noCamerasAvailable
345
417
  }
346
418
 
@@ -356,9 +428,7 @@ extension CameraController {
356
428
  captureSession.commitConfiguration()
357
429
  // Restart the session if it was running before
358
430
  if wasRunning {
359
- DispatchQueue.global(qos: .userInitiated).async { [weak self] in
360
- self?.captureSession?.startRunning()
361
- }
431
+ captureSession.startRunning()
362
432
  }
363
433
  }
364
434
 
@@ -368,7 +438,7 @@ extension CameraController {
368
438
  // Remove only video inputs
369
439
  captureSession.inputs.forEach { input in
370
440
  if (input as? AVCaptureDeviceInput)?.device.hasMediaType(.video) ?? false {
371
- captureSession.removeInput(input)
441
+ captureSession.removeInput(input)
372
442
  }
373
443
  }
374
444
 
@@ -387,7 +457,7 @@ extension CameraController {
387
457
  rearCamera.unlockForConfiguration()
388
458
 
389
459
  if let newInput = try? AVCaptureDeviceInput(device: rearCamera),
390
- captureSession.canAddInput(newInput) {
460
+ captureSession.canAddInput(newInput) {
391
461
  captureSession.addInput(newInput)
392
462
  rearCameraInput = newInput
393
463
  self.currentCameraPosition = .rear
@@ -407,7 +477,7 @@ extension CameraController {
407
477
  frontCamera.unlockForConfiguration()
408
478
 
409
479
  if let newInput = try? AVCaptureDeviceInput(device: frontCamera),
410
- captureSession.canAddInput(newInput) {
480
+ captureSession.canAddInput(newInput) {
411
481
  captureSession.addInput(newInput)
412
482
  frontCameraInput = newInput
413
483
  self.currentCameraPosition = .front
@@ -422,20 +492,78 @@ extension CameraController {
422
492
  }
423
493
 
424
494
  // Update video orientation
425
- DispatchQueue.main.async { [weak self] in
426
- self?.updateVideoOrientation()
427
- }
495
+ self.updateVideoOrientation()
428
496
  }
429
497
 
430
- func captureImage(completion: @escaping (UIImage?, Error?) -> Void) {
431
- guard let captureSession = captureSession, captureSession.isRunning else { completion(nil, CameraControllerError.captureSessionIsMissing); return }
498
+ func captureImage(width: Int?, height: Int?, quality: Float, gpsLocation: CLLocation?, completion: @escaping (UIImage?, Error?) -> Void) {
499
+ guard let photoOutput = self.photoOutput else {
500
+ completion(nil, NSError(domain: "Camera", code: 0, userInfo: [NSLocalizedDescriptionKey: "Photo output is not available"]))
501
+ return
502
+ }
503
+
432
504
  let settings = AVCapturePhotoSettings()
433
505
 
434
- settings.flashMode = self.flashMode
435
- settings.isHighResolutionPhotoEnabled = self.highResolutionOutput
506
+ self.photoCaptureCompletionBlock = { (image, error) in
507
+ if let error = error {
508
+ completion(nil, error)
509
+ return
510
+ }
511
+
512
+ guard let image = image else {
513
+ completion(nil, NSError(domain: "Camera", code: 0, userInfo: [NSLocalizedDescriptionKey: "Failed to capture image"]))
514
+ return
515
+ }
516
+
517
+ if let location = gpsLocation {
518
+ self.addGPSMetadata(to: image, location: location)
519
+ }
436
520
 
437
- self.photoOutput?.capturePhoto(with: settings, delegate: self)
438
- self.photoCaptureCompletionBlock = completion
521
+ if let width = width, let height = height {
522
+ let resizedImage = self.resizeImage(image: image, to: CGSize(width: width, height: height))
523
+ completion(resizedImage, nil)
524
+ } else {
525
+ completion(image, nil)
526
+ }
527
+ }
528
+
529
+ photoOutput.capturePhoto(with: settings, delegate: self)
530
+ }
531
+
532
+ func addGPSMetadata(to image: UIImage, location: CLLocation) {
533
+ guard let jpegData = image.jpegData(compressionQuality: 1.0),
534
+ let source = CGImageSourceCreateWithData(jpegData as CFData, nil),
535
+ let uti = CGImageSourceGetType(source) else { return }
536
+
537
+ var metadata = CGImageSourceCopyPropertiesAtIndex(source, 0, nil) as? [String: Any] ?? [:]
538
+
539
+ let formatter = DateFormatter()
540
+ formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZ"
541
+ formatter.timeZone = TimeZone(abbreviation: "UTC")
542
+
543
+ let gpsDict: [String: Any] = [
544
+ kCGImagePropertyGPSLatitude as String: abs(location.coordinate.latitude),
545
+ kCGImagePropertyGPSLatitudeRef as String: location.coordinate.latitude >= 0 ? "N" : "S",
546
+ kCGImagePropertyGPSLongitude as String: abs(location.coordinate.longitude),
547
+ kCGImagePropertyGPSLongitudeRef as String: location.coordinate.longitude >= 0 ? "E" : "W",
548
+ kCGImagePropertyGPSTimeStamp as String: formatter.string(from: location.timestamp),
549
+ kCGImagePropertyGPSAltitude as String: location.altitude,
550
+ kCGImagePropertyGPSAltitudeRef as String: location.altitude >= 0 ? 0 : 1
551
+ ]
552
+
553
+ metadata[kCGImagePropertyGPSDictionary as String] = gpsDict
554
+
555
+ let destData = NSMutableData()
556
+ guard let destination = CGImageDestinationCreateWithData(destData, uti, 1, nil) else { return }
557
+ CGImageDestinationAddImageFromSource(destination, source, 0, metadata as CFDictionary)
558
+ CGImageDestinationFinalize(destination)
559
+ }
560
+
561
+ func resizeImage(image: UIImage, to size: CGSize) -> UIImage? {
562
+ let renderer = UIGraphicsImageRenderer(size: size)
563
+ let resizedImage = renderer.image { (_) in
564
+ image.draw(in: CGRect(origin: .zero, size: size))
565
+ }
566
+ return resizedImage
439
567
  }
440
568
 
441
569
  func captureSample(completion: @escaping (UIImage?, Error?) -> Void) {
@@ -633,7 +761,8 @@ extension CameraController {
633
761
  try device.lockForConfiguration()
634
762
 
635
763
  if ramp {
636
- device.ramp(toVideoZoomFactor: zoomLevel, withRate: 1.0)
764
+ // Use a very fast ramp rate for immediate response
765
+ device.ramp(toVideoZoomFactor: zoomLevel, withRate: 8.0)
637
766
  } else {
638
767
  device.videoZoomFactor = zoomLevel
639
768
  }
@@ -677,7 +806,7 @@ extension CameraController {
677
806
 
678
807
  return device.uniqueID
679
808
  }
680
-
809
+
681
810
  func getCurrentLensInfo() throws -> (focalLength: Float, deviceType: String, baseZoomRatio: Float) {
682
811
  var currentCamera: AVCaptureDevice?
683
812
  switch currentCameraPosition {
@@ -757,9 +886,7 @@ extension CameraController {
757
886
  captureSession.commitConfiguration()
758
887
  // Restart the session if it was running before
759
888
  if wasRunning {
760
- DispatchQueue.global(qos: .userInitiated).async { [weak self] in
761
- self?.captureSession?.startRunning()
762
- }
889
+ captureSession.startRunning()
763
890
  }
764
891
  }
765
892
 
@@ -806,13 +933,9 @@ extension CameraController {
806
933
  }
807
934
 
808
935
  // Update video orientation
809
- DispatchQueue.main.async { [weak self] in
810
- self?.updateVideoOrientation()
811
- }
936
+ self.updateVideoOrientation()
812
937
  }
813
938
 
814
-
815
-
816
939
  func cleanup() {
817
940
  if let captureSession = self.captureSession {
818
941
  captureSession.stopRunning()
@@ -940,40 +1063,59 @@ extension CameraController: UIGestureRecognizerDelegate {
940
1063
  let effectiveMaxZoom = min(device.maxAvailableVideoZoomFactor, self.saneMaxZoomFactor)
941
1064
  func minMaxZoom(_ factor: CGFloat) -> CGFloat { return max(device.minAvailableVideoZoomFactor, min(factor, effectiveMaxZoom)) }
942
1065
 
943
- func update(scale factor: CGFloat) {
1066
+ switch pinch.state {
1067
+ case .began:
1068
+ // Store the initial zoom factor when pinch begins
1069
+ zoomFactor = device.videoZoomFactor
1070
+
1071
+ case .changed:
1072
+ // Throttle zoom updates to prevent excessive CPU usage
1073
+ let currentTime = CACurrentMediaTime()
1074
+ guard currentTime - lastZoomUpdateTime >= zoomUpdateThrottle else { return }
1075
+ lastZoomUpdateTime = currentTime
1076
+
1077
+ // Calculate new zoom factor based on pinch scale
1078
+ let newScaleFactor = minMaxZoom(pinch.scale * zoomFactor)
1079
+
1080
+ // Use ramping for smooth zoom transitions during pinch
1081
+ // This provides much smoother performance than direct setting
944
1082
  do {
945
1083
  try device.lockForConfiguration()
946
- defer { device.unlockForConfiguration() }
947
-
948
- device.videoZoomFactor = factor
1084
+ // Use a very fast ramp rate for immediate response
1085
+ device.ramp(toVideoZoomFactor: newScaleFactor, withRate: 5.0)
1086
+ device.unlockForConfiguration()
949
1087
  } catch {
950
- debugPrint(error)
1088
+ debugPrint("Failed to set zoom: \(error)")
951
1089
  }
952
- }
953
1090
 
954
- switch pinch.state {
955
- case .began: fallthrough
956
- case .changed:
957
- let newScaleFactor = minMaxZoom(pinch.scale * zoomFactor)
958
- update(scale: newScaleFactor)
959
1091
  case .ended:
1092
+ // Update our internal zoom factor tracking
960
1093
  zoomFactor = device.videoZoomFactor
1094
+
961
1095
  default: break
962
1096
  }
963
1097
  }
964
1098
  }
965
1099
 
966
1100
  extension CameraController: AVCapturePhotoCaptureDelegate {
967
- public func photoOutput(_ captureOutput: AVCapturePhotoOutput, didFinishProcessingPhoto photoSampleBuffer: CMSampleBuffer?, previewPhoto previewPhotoSampleBuffer: CMSampleBuffer?,
968
- resolvedSettings: AVCaptureResolvedPhotoSettings, bracketSettings: AVCaptureBracketedStillImageSettings?, error: Swift.Error?) {
1101
+ public func photoOutput(_ output: AVCapturePhotoOutput, didFinishProcessingPhoto photo: AVCapturePhoto, error: Error?) {
969
1102
  if let error = error {
970
1103
  self.photoCaptureCompletionBlock?(nil, error)
971
- } else if let buffer = photoSampleBuffer, let data = AVCapturePhotoOutput.jpegPhotoDataRepresentation(forJPEGSampleBuffer: buffer, previewPhotoSampleBuffer: nil),
972
- let image = UIImage(data: data) {
973
- self.photoCaptureCompletionBlock?(image.fixedOrientation(), nil)
974
- } 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 {
975
1114
  self.photoCaptureCompletionBlock?(nil, CameraControllerError.unknown)
1115
+ return
976
1116
  }
1117
+
1118
+ self.photoCaptureCompletionBlock?(image.fixedOrientation(), nil)
977
1119
  }
978
1120
  }
979
1121
 
@@ -1095,6 +1237,8 @@ extension UIImage {
1095
1237
  print("right")
1096
1238
  case .up, .upMirrored:
1097
1239
  break
1240
+ @unknown default:
1241
+ break
1098
1242
  }
1099
1243
 
1100
1244
  // Flip image one more time if needed to, this is to prevent flipped image
@@ -1107,6 +1251,8 @@ extension UIImage {
1107
1251
  transform.scaledBy(x: -1, y: 1)
1108
1252
  case .up, .down, .left, .right:
1109
1253
  break
1254
+ @unknown default:
1255
+ break
1110
1256
  }
1111
1257
 
1112
1258
  ctx.concatenate(transform)