@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.
- package/CapgoCameraPreview.podspec +16 -13
- package/README.md +467 -73
- package/android/.gradle/8.14.2/checksums/checksums.lock +0 -0
- package/android/.gradle/8.14.2/checksums/md5-checksums.bin +0 -0
- package/android/.gradle/8.14.2/checksums/sha1-checksums.bin +0 -0
- package/android/.gradle/8.14.2/executionHistory/executionHistory.bin +0 -0
- package/android/.gradle/8.14.2/executionHistory/executionHistory.lock +0 -0
- package/android/.gradle/8.14.2/fileChanges/last-build.bin +0 -0
- package/android/.gradle/8.14.2/fileHashes/fileHashes.bin +0 -0
- package/android/.gradle/8.14.2/fileHashes/fileHashes.lock +0 -0
- package/android/.gradle/8.14.2/fileHashes/resourceHashesCache.bin +0 -0
- package/android/.gradle/8.14.2/gc.properties +0 -0
- package/android/.gradle/buildOutputCleanup/buildOutputCleanup.lock +0 -0
- package/android/.gradle/buildOutputCleanup/cache.properties +2 -0
- package/android/.gradle/buildOutputCleanup/outputFiles.bin +0 -0
- package/android/.gradle/file-system.probe +0 -0
- package/android/.gradle/vcs-1/gc.properties +0 -0
- package/android/build.gradle +11 -0
- package/android/gradle/wrapper/gradle-wrapper.properties +1 -1
- package/android/src/main/AndroidManifest.xml +5 -3
- package/android/src/main/java/com/ahm/capacitor/camera/preview/CameraPreview.java +472 -541
- package/android/src/main/java/com/ahm/capacitor/camera/preview/CameraXView.java +1648 -0
- package/android/src/main/java/com/ahm/capacitor/camera/preview/GridOverlayView.java +82 -0
- package/android/src/main/java/com/ahm/capacitor/camera/preview/model/CameraDevice.java +54 -0
- package/android/src/main/java/com/ahm/capacitor/camera/preview/model/CameraLens.java +70 -0
- package/android/src/main/java/com/ahm/capacitor/camera/preview/model/CameraSessionConfiguration.java +79 -0
- package/android/src/main/java/com/ahm/capacitor/camera/preview/model/LensInfo.java +34 -0
- package/android/src/main/java/com/ahm/capacitor/camera/preview/model/ZoomFactors.java +34 -0
- package/dist/docs.json +934 -154
- package/dist/esm/definitions.d.ts +445 -83
- package/dist/esm/definitions.js +10 -1
- package/dist/esm/definitions.js.map +1 -1
- package/dist/esm/web.d.ts +73 -3
- package/dist/esm/web.js +492 -68
- package/dist/esm/web.js.map +1 -1
- package/dist/plugin.cjs.js +498 -68
- package/dist/plugin.cjs.js.map +1 -1
- package/dist/plugin.js +498 -68
- package/dist/plugin.js.map +1 -1
- package/ios/{Plugin → Sources/CapgoCameraPreview}/CameraController.swift +601 -59
- package/ios/Sources/CapgoCameraPreview/GridOverlayView.swift +65 -0
- package/ios/Sources/CapgoCameraPreview/Plugin.swift +1369 -0
- package/ios/Tests/CameraPreviewPluginTests/CameraPreviewPluginTests.swift +15 -0
- package/package.json +1 -1
- package/android/src/main/java/com/ahm/capacitor/camera/preview/CameraActivity.java +0 -1279
- package/android/src/main/java/com/ahm/capacitor/camera/preview/CustomSurfaceView.java +0 -29
- package/android/src/main/java/com/ahm/capacitor/camera/preview/CustomTextureView.java +0 -39
- package/android/src/main/java/com/ahm/capacitor/camera/preview/Preview.java +0 -461
- package/android/src/main/java/com/ahm/capacitor/camera/preview/TapGestureDetector.java +0 -24
- package/ios/Plugin/Info.plist +0 -24
- package/ios/Plugin/Plugin.h +0 -10
- package/ios/Plugin/Plugin.m +0 -18
- package/ios/Plugin/Plugin.swift +0 -511
- package/ios/Plugin.xcodeproj/project.pbxproj +0 -593
- package/ios/Plugin.xcodeproj/project.xcworkspace/contents.xcworkspacedata +0 -7
- package/ios/Plugin.xcworkspace/contents.xcworkspacedata +0 -10
- package/ios/Plugin.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +0 -8
- package/ios/PluginTests/Info.plist +0 -22
- package/ios/PluginTests/PluginTests.swift +0 -83
- package/ios/Podfile +0 -13
- 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
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
60
|
-
|
|
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
|
-
|
|
64
|
-
|
|
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
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
-
|
|
80
|
-
if let rearCamera = self.rearCamera {
|
|
81
|
-
self.rearCameraInput = try AVCaptureDeviceInput(device: rearCamera)
|
|
174
|
+
var selectedDevice: AVCaptureDevice?
|
|
82
175
|
|
|
83
|
-
|
|
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
|
-
|
|
184
|
+
selectedDevice = allDevices.first(where: { $0.uniqueID == deviceId })
|
|
185
|
+
guard selectedDevice != nil else {
|
|
186
|
+
throw CameraControllerError.noCamerasAvailable
|
|
86
187
|
}
|
|
87
|
-
} else
|
|
88
|
-
|
|
89
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
-
//
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
342
|
-
|
|
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
|
-
|
|
345
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1142
|
+
@objc
|
|
613
1143
|
private func handlePinch(_ pinch: UIPinchGestureRecognizer) {
|
|
614
1144
|
guard let device = self.currentCameraPosition == .rear ? rearCamera : frontCamera else { return }
|
|
615
1145
|
|
|
616
|
-
|
|
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
|
-
|
|
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
|
-
|
|
622
|
-
|
|
623
|
-
device.
|
|
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
|
}
|