@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.
- package/README.md +195 -31
- 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/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/buildOutputCleanup/buildOutputCleanup.lock +0 -0
- package/android/.gradle/file-system.probe +0 -0
- package/android/build.gradle +3 -1
- package/android/src/main/AndroidManifest.xml +5 -3
- package/android/src/main/java/com/ahm/capacitor/camera/preview/CameraPreview.java +473 -88
- package/android/src/main/java/com/ahm/capacitor/camera/preview/CameraXView.java +2065 -704
- package/android/src/main/java/com/ahm/capacitor/camera/preview/GridOverlayView.java +95 -0
- package/android/src/main/java/com/ahm/capacitor/camera/preview/model/CameraDevice.java +55 -46
- package/android/src/main/java/com/ahm/capacitor/camera/preview/model/CameraLens.java +61 -52
- package/android/src/main/java/com/ahm/capacitor/camera/preview/model/CameraSessionConfiguration.java +152 -59
- package/android/src/main/java/com/ahm/capacitor/camera/preview/model/LensInfo.java +29 -23
- package/android/src/main/java/com/ahm/capacitor/camera/preview/model/ZoomFactors.java +24 -23
- package/dist/docs.json +235 -6
- package/dist/esm/definitions.d.ts +119 -3
- package/dist/esm/definitions.js.map +1 -1
- package/dist/esm/web.d.ts +47 -3
- package/dist/esm/web.js +297 -96
- package/dist/esm/web.js.map +1 -1
- package/dist/plugin.cjs.js +293 -96
- package/dist/plugin.cjs.js.map +1 -1
- package/dist/plugin.js +293 -96
- package/dist/plugin.js.map +1 -1
- package/ios/Sources/CapgoCameraPreview/CameraController.swift +364 -218
- package/ios/Sources/CapgoCameraPreview/GridOverlayView.swift +65 -0
- package/ios/Sources/CapgoCameraPreview/Plugin.swift +886 -242
- 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
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
|
|
82
|
-
print("[CameraPreview] ERROR: No cameras found.")
|
|
83
|
-
throw CameraControllerError.noCamerasAvailable
|
|
84
|
-
}
|
|
63
|
+
print("[CameraPreview] Preparing full camera session in background")
|
|
85
64
|
|
|
86
|
-
|
|
87
|
-
|
|
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
|
-
|
|
68
|
+
// 2. Pre-configure session preset (can be changed later)
|
|
69
|
+
if captureSession!.canSetSessionPreset(.medium) {
|
|
70
|
+
captureSession!.sessionPreset = .medium
|
|
71
|
+
}
|
|
92
72
|
|
|
93
|
-
|
|
94
|
-
|
|
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
|
-
|
|
104
|
-
|
|
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
|
-
|
|
116
|
-
|
|
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
|
-
|
|
121
|
-
|
|
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
|
-
|
|
110
|
+
self.frontCamera = cameras.first(where: { $0.position == .front })
|
|
124
111
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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
|
-
|
|
152
|
-
|
|
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
|
-
|
|
155
|
-
|
|
144
|
+
// Pre-create video output
|
|
145
|
+
self.fileVideoOutput = AVCaptureMovieFileOutput()
|
|
156
146
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
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
|
-
|
|
191
|
-
|
|
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
|
-
|
|
204
|
-
guard let captureSession = self.captureSession else { throw CameraControllerError.captureSessionIsMissing }
|
|
169
|
+
print("[CameraPreview] Fast prepare - using pre-initialized session")
|
|
205
170
|
|
|
206
|
-
//
|
|
207
|
-
|
|
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
|
-
|
|
214
|
-
self.
|
|
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
|
-
|
|
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
|
-
|
|
227
|
-
guard let captureSession = self.captureSession else { throw CameraControllerError.captureSessionIsMissing }
|
|
190
|
+
var selectedDevice: AVCaptureDevice?
|
|
228
191
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
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
|
-
|
|
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
|
-
|
|
241
|
-
|
|
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
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
try
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
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
|
-
|
|
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
|
-
|
|
261
|
-
|
|
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
|
-
|
|
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.
|
|
302
|
-
self
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
426
|
-
self?.updateVideoOrientation()
|
|
427
|
-
}
|
|
495
|
+
self.updateVideoOrientation()
|
|
428
496
|
}
|
|
429
497
|
|
|
430
|
-
func captureImage(completion: @escaping (UIImage?, Error?) -> Void) {
|
|
431
|
-
guard let
|
|
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
|
-
|
|
435
|
-
|
|
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
|
-
|
|
438
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
947
|
-
|
|
948
|
-
device.
|
|
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(_
|
|
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
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
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)
|