@capgo/camera-preview 7.4.0-beta.13 → 7.4.0-beta.15
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 +10 -10
- 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/src/main/java/com/ahm/capacitor/camera/preview/CameraPreview.java +177 -19
- package/android/src/main/java/com/ahm/capacitor/camera/preview/CameraXView.java +725 -87
- package/android/src/main/java/com/ahm/capacitor/camera/preview/model/CameraSessionConfiguration.java +9 -0
- package/dist/docs.json +27 -23
- package/dist/esm/definitions.d.ts +13 -10
- package/dist/esm/definitions.js.map +1 -1
- package/dist/esm/web.d.ts +1 -0
- package/dist/esm/web.js +223 -34
- package/dist/esm/web.js.map +1 -1
- package/dist/plugin.cjs.js +223 -34
- package/dist/plugin.cjs.js.map +1 -1
- package/dist/plugin.js +223 -34
- package/dist/plugin.js.map +1 -1
- package/ios/Sources/CapgoCameraPreview/CameraController.swift +373 -65
- package/ios/Sources/CapgoCameraPreview/Plugin.swift +35 -22
- package/package.json +1 -1
|
@@ -30,14 +30,13 @@ class CameraController: NSObject {
|
|
|
30
30
|
|
|
31
31
|
var previewLayer: AVCaptureVideoPreviewLayer?
|
|
32
32
|
var gridOverlayView: GridOverlayView?
|
|
33
|
+
var focusIndicatorView: UIView?
|
|
33
34
|
|
|
34
35
|
var flashMode = AVCaptureDevice.FlashMode.off
|
|
35
36
|
var photoCaptureCompletionBlock: ((UIImage?, Error?) -> Void)?
|
|
36
37
|
|
|
37
38
|
var sampleBufferCaptureCompletionBlock: ((UIImage?, Error?) -> Void)?
|
|
38
39
|
|
|
39
|
-
var highResolutionOutput: Bool = false
|
|
40
|
-
|
|
41
40
|
var audioDevice: AVCaptureDevice?
|
|
42
41
|
var audioInput: AVCaptureDeviceInput?
|
|
43
42
|
|
|
@@ -48,6 +47,10 @@ class CameraController: NSObject {
|
|
|
48
47
|
var videoFileURL: URL?
|
|
49
48
|
private let saneMaxZoomFactor: CGFloat = 25.5
|
|
50
49
|
|
|
50
|
+
// Track output preparation status
|
|
51
|
+
private var outputsPrepared: Bool = false
|
|
52
|
+
private let outputPreparationQueue = DispatchQueue(label: "camera.output.preparation", qos: .utility)
|
|
53
|
+
|
|
51
54
|
var isUsingMultiLensVirtualCamera: Bool {
|
|
52
55
|
guard let device = (currentCameraPosition == .rear) ? rearCamera : frontCamera else { return false }
|
|
53
56
|
// A rear multi-lens virtual camera will have a min zoom of 1.0 but support wider angles
|
|
@@ -65,18 +68,27 @@ extension CameraController {
|
|
|
65
68
|
// 1. Create and configure session
|
|
66
69
|
self.captureSession = AVCaptureSession()
|
|
67
70
|
|
|
68
|
-
// 2. Pre-configure session preset (can be changed later)
|
|
69
|
-
if captureSession!.canSetSessionPreset(.
|
|
71
|
+
// 2. Pre-configure session preset (can be changed later) - use medium for faster startup
|
|
72
|
+
if captureSession!.canSetSessionPreset(.medium) {
|
|
73
|
+
captureSession!.sessionPreset = .medium // Start with medium, upgrade later if needed
|
|
74
|
+
} else if captureSession!.canSetSessionPreset(.high) {
|
|
70
75
|
captureSession!.sessionPreset = .high
|
|
71
76
|
}
|
|
72
77
|
|
|
73
|
-
// 3. Discover
|
|
74
|
-
discoverAndConfigureCameras()
|
|
78
|
+
// 3. Discover cameras on-demand (only when needed for better startup performance)
|
|
79
|
+
// discoverAndConfigureCameras() - moved to lazy loading
|
|
75
80
|
|
|
76
|
-
// 4. Pre-create outputs
|
|
77
|
-
|
|
81
|
+
// // 4. Pre-create outputs asynchronously to avoid blocking camera opening
|
|
82
|
+
// outputPreparationQueue.async { [weak self] in
|
|
83
|
+
// self?.prepareOutputs()
|
|
84
|
+
// }
|
|
85
|
+
|
|
86
|
+
print("[CameraPreview] Full session preparation complete - cameras will be discovered on-demand, outputs being prepared asynchronously")
|
|
87
|
+
}
|
|
78
88
|
|
|
79
|
-
|
|
89
|
+
private func ensureCamerasDiscovered() {
|
|
90
|
+
guard allDiscoveredDevices.isEmpty else { return }
|
|
91
|
+
discoverAndConfigureCameras()
|
|
80
92
|
}
|
|
81
93
|
|
|
82
94
|
private func discoverAndConfigureCameras() {
|
|
@@ -137,67 +149,211 @@ extension CameraController {
|
|
|
137
149
|
}
|
|
138
150
|
|
|
139
151
|
private func prepareOutputs() {
|
|
140
|
-
// Pre-create photo output
|
|
152
|
+
// Pre-create photo output with optimized settings
|
|
141
153
|
self.photoOutput = AVCapturePhotoOutput()
|
|
142
|
-
self.photoOutput?.isHighResolutionCaptureEnabled = false //
|
|
154
|
+
self.photoOutput?.isHighResolutionCaptureEnabled = false // Start with lower resolution for speed
|
|
155
|
+
|
|
156
|
+
// Configure photo output for better performance
|
|
157
|
+
if #available(iOS 13.0, *) {
|
|
158
|
+
self.photoOutput?.maxPhotoQualityPrioritization = .speed // Prioritize speed over quality initially
|
|
159
|
+
}
|
|
143
160
|
|
|
144
161
|
// Pre-create video output
|
|
145
162
|
self.fileVideoOutput = AVCaptureMovieFileOutput()
|
|
146
163
|
|
|
147
|
-
// Pre-create data output
|
|
164
|
+
// Pre-create data output with optimized settings
|
|
148
165
|
self.dataOutput = AVCaptureVideoDataOutput()
|
|
149
166
|
self.dataOutput?.videoSettings = [
|
|
150
167
|
(kCVPixelBufferPixelFormatTypeKey as String): NSNumber(value: kCVPixelFormatType_32BGRA as UInt32)
|
|
151
168
|
]
|
|
152
169
|
self.dataOutput?.alwaysDiscardsLateVideoFrames = true
|
|
153
170
|
|
|
154
|
-
|
|
171
|
+
// Use a background queue for sample buffer processing to avoid blocking main thread
|
|
172
|
+
let dataOutputQueue = DispatchQueue(label: "camera.data.output", qos: .userInitiated)
|
|
173
|
+
self.dataOutput?.setSampleBufferDelegate(nil, queue: dataOutputQueue) // Will be set later
|
|
174
|
+
|
|
175
|
+
// Mark outputs as prepared
|
|
176
|
+
self.outputsPrepared = true
|
|
177
|
+
|
|
178
|
+
print("[CameraPreview] Outputs pre-created with performance optimizations")
|
|
155
179
|
}
|
|
156
180
|
|
|
157
|
-
func
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
181
|
+
private func waitForOutputsToBeReady() {
|
|
182
|
+
// If outputs are already prepared, return immediately
|
|
183
|
+
if outputsPrepared {
|
|
184
|
+
return
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Wait for outputs to be prepared with a timeout
|
|
188
|
+
let semaphore = DispatchSemaphore(value: 0)
|
|
189
|
+
var outputsReady = false
|
|
190
|
+
|
|
191
|
+
// Check for outputs readiness periodically
|
|
192
|
+
let timer = Timer.scheduledTimer(withTimeInterval: 0.01, repeats: true) { timer in
|
|
193
|
+
if self.outputsPrepared {
|
|
194
|
+
outputsReady = true
|
|
195
|
+
timer.invalidate()
|
|
196
|
+
semaphore.signal()
|
|
163
197
|
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Wait for outputs to be ready or timeout after 2 seconds
|
|
201
|
+
let timeout = DispatchTime.now() + .seconds(2)
|
|
202
|
+
let result = semaphore.wait(timeout: timeout)
|
|
203
|
+
|
|
204
|
+
timer.invalidate()
|
|
164
205
|
|
|
165
|
-
|
|
166
|
-
|
|
206
|
+
if result == .timedOut && !outputsReady {
|
|
207
|
+
print("[CameraPreview] Warning: Timed out waiting for outputs to be prepared, proceeding anyway")
|
|
208
|
+
// Fallback: prepare outputs synchronously if async preparation failed
|
|
209
|
+
if !outputsPrepared {
|
|
210
|
+
prepareOutputs()
|
|
211
|
+
}
|
|
212
|
+
} else {
|
|
213
|
+
print("[CameraPreview] Outputs ready, proceeding with camera preparation")
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
func upgradeQualitySettings() {
|
|
218
|
+
guard let captureSession = self.captureSession else { return }
|
|
219
|
+
|
|
220
|
+
// Upgrade session preset to high quality after initial startup
|
|
221
|
+
DispatchQueue.global(qos: .utility).async { [weak self] in
|
|
222
|
+
guard let self = self else { return }
|
|
223
|
+
|
|
224
|
+
captureSession.beginConfiguration()
|
|
225
|
+
|
|
226
|
+
// Upgrade to high quality preset
|
|
227
|
+
if captureSession.canSetSessionPreset(.high) && captureSession.sessionPreset != .high {
|
|
228
|
+
captureSession.sessionPreset = .high
|
|
229
|
+
print("[CameraPreview] Upgraded session preset to high quality")
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Upgrade photo output quality
|
|
233
|
+
if let photoOutput = self.photoOutput {
|
|
234
|
+
photoOutput.isHighResolutionCaptureEnabled = true
|
|
235
|
+
if #available(iOS 13.0, *) {
|
|
236
|
+
photoOutput.maxPhotoQualityPrioritization = .quality
|
|
237
|
+
}
|
|
238
|
+
print("[CameraPreview] Upgraded photo output to high resolution")
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
captureSession.commitConfiguration()
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
func prepare(cameraPosition: String, deviceId: String? = nil, disableAudio: Bool, cameraMode: Bool, aspectRatio: String? = nil, initialZoomLevel: Float = 1.0, completionHandler: @escaping (Error?) -> Void) {
|
|
246
|
+
// Use background queue for preparation to avoid blocking main thread
|
|
247
|
+
DispatchQueue.global(qos: .userInitiated).async { [weak self] in
|
|
248
|
+
guard let self = self else {
|
|
249
|
+
DispatchQueue.main.async {
|
|
250
|
+
completionHandler(CameraControllerError.unknown)
|
|
251
|
+
}
|
|
252
|
+
return
|
|
167
253
|
}
|
|
168
254
|
|
|
169
|
-
|
|
255
|
+
do {
|
|
256
|
+
// Session and outputs already created in load(), just configure user-specific settings
|
|
257
|
+
if self.captureSession == nil {
|
|
258
|
+
// Fallback if prepareFullSession() wasn't called
|
|
259
|
+
self.prepareFullSession()
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
guard let captureSession = self.captureSession else {
|
|
263
|
+
throw CameraControllerError.captureSessionIsMissing
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
print("[CameraPreview] Fast prepare - using pre-initialized session")
|
|
170
267
|
|
|
171
|
-
|
|
172
|
-
|
|
268
|
+
// Pre-create outputs asynchronously to avoid blocking camera opening
|
|
269
|
+
outputPreparationQueue.async { [weak self] in
|
|
270
|
+
self?.prepareOutputs()
|
|
271
|
+
}
|
|
173
272
|
|
|
174
|
-
|
|
175
|
-
|
|
273
|
+
// // Configure device inputs for the requested camera
|
|
274
|
+
try self.configureDeviceInputs(cameraPosition: cameraPosition, deviceId: deviceId, disableAudio: disableAudio)
|
|
176
275
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
276
|
+
// Start the session on background thread (AVCaptureSession.startRunning() is thread-safe)
|
|
277
|
+
captureSession.startRunning()
|
|
278
|
+
print("[CameraPreview] Session started")
|
|
180
279
|
|
|
181
|
-
|
|
280
|
+
// Validate and set initial zoom level asynchronously
|
|
281
|
+
if initialZoomLevel != 1.0 {
|
|
282
|
+
DispatchQueue.main.async { [weak self] in
|
|
283
|
+
self?.setInitialZoom(level: initialZoomLevel)
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// Call completion on main thread
|
|
288
|
+
DispatchQueue.main.async {
|
|
289
|
+
completionHandler(nil)
|
|
290
|
+
|
|
291
|
+
// Upgrade quality settings after a short delay for better user experience
|
|
292
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in
|
|
293
|
+
guard let self = self else { return }
|
|
294
|
+
|
|
295
|
+
// Wait for outputs to be prepared before proceeding
|
|
296
|
+
self.waitForOutputsToBeReady()
|
|
297
|
+
|
|
298
|
+
// Add outputs to session and apply user settings
|
|
299
|
+
do {
|
|
300
|
+
try self.addOutputsToSession(cameraMode: cameraMode, aspectRatio: aspectRatio)
|
|
301
|
+
print("[CameraPreview] Outputs successfully added to session")
|
|
302
|
+
} catch {
|
|
303
|
+
print("[CameraPreview] Error adding outputs to session: \(error)")
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
self.upgradeQualitySettings()
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
} catch {
|
|
310
|
+
DispatchQueue.main.async {
|
|
311
|
+
completionHandler(error)
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
private func setInitialZoom(level: Float) {
|
|
318
|
+
let device = (currentCameraPosition == .rear) ? rearCamera : frontCamera
|
|
319
|
+
guard let device = device else { return }
|
|
320
|
+
|
|
321
|
+
let minZoom = device.minAvailableVideoZoomFactor
|
|
322
|
+
let maxZoom = min(device.maxAvailableVideoZoomFactor, saneMaxZoomFactor)
|
|
323
|
+
|
|
324
|
+
guard CGFloat(level) >= minZoom && CGFloat(level) <= maxZoom else {
|
|
325
|
+
print("[CameraPreview] Initial zoom level \(level) out of range (\(minZoom)-\(maxZoom))")
|
|
326
|
+
return
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
do {
|
|
330
|
+
try device.lockForConfiguration()
|
|
331
|
+
device.videoZoomFactor = CGFloat(level)
|
|
332
|
+
device.unlockForConfiguration()
|
|
333
|
+
self.zoomFactor = CGFloat(level)
|
|
334
|
+
print("[CameraPreview] Set initial zoom to \(level)")
|
|
182
335
|
} catch {
|
|
183
|
-
|
|
336
|
+
print("[CameraPreview] Failed to set initial zoom: \(error)")
|
|
184
337
|
}
|
|
185
338
|
}
|
|
186
339
|
|
|
187
340
|
private func configureDeviceInputs(cameraPosition: String, deviceId: String?, disableAudio: Bool) throws {
|
|
188
341
|
guard let captureSession = self.captureSession else { throw CameraControllerError.captureSessionIsMissing }
|
|
189
342
|
|
|
343
|
+
// Ensure cameras are discovered before configuring inputs
|
|
344
|
+
ensureCamerasDiscovered()
|
|
345
|
+
|
|
190
346
|
var selectedDevice: AVCaptureDevice?
|
|
191
347
|
|
|
192
|
-
// If deviceId is specified, find that specific device from
|
|
348
|
+
// If deviceId is specified, find that specific device from discovered devices
|
|
193
349
|
if let deviceId = deviceId {
|
|
194
350
|
selectedDevice = self.allDiscoveredDevices.first(where: { $0.uniqueID == deviceId })
|
|
195
351
|
guard selectedDevice != nil else {
|
|
196
|
-
print("[CameraPreview] ERROR: Device with ID \(deviceId) not found in
|
|
352
|
+
print("[CameraPreview] ERROR: Device with ID \(deviceId) not found in discovered devices")
|
|
197
353
|
throw CameraControllerError.noCamerasAvailable
|
|
198
354
|
}
|
|
199
355
|
} else {
|
|
200
|
-
// Use position-based selection from
|
|
356
|
+
// Use position-based selection from discovered cameras
|
|
201
357
|
if cameraPosition == "rear" {
|
|
202
358
|
selectedDevice = self.rearCamera
|
|
203
359
|
} else if cameraPosition == "front" {
|
|
@@ -223,16 +379,16 @@ extension CameraController {
|
|
|
223
379
|
self.rearCameraInput = deviceInput
|
|
224
380
|
self.currentCameraPosition = .rear
|
|
225
381
|
|
|
226
|
-
// Configure zoom for multi-camera systems
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
382
|
+
// Configure zoom for multi-camera systems - simplified and faster
|
|
383
|
+
if finalDevice.isVirtualDevice && finalDevice.constituentDevices.count > 1 {
|
|
384
|
+
try finalDevice.lockForConfiguration()
|
|
385
|
+
let defaultWideAngleZoom: CGFloat = 1.0 // Changed from 2.0 to 1.0 for faster startup
|
|
230
386
|
if defaultWideAngleZoom >= finalDevice.minAvailableVideoZoomFactor && defaultWideAngleZoom <= finalDevice.maxAvailableVideoZoomFactor {
|
|
231
387
|
print("[CameraPreview] Setting initial zoom to \(defaultWideAngleZoom)")
|
|
232
388
|
finalDevice.videoZoomFactor = defaultWideAngleZoom
|
|
233
389
|
}
|
|
390
|
+
finalDevice.unlockForConfiguration()
|
|
234
391
|
}
|
|
235
|
-
finalDevice.unlockForConfiguration()
|
|
236
392
|
}
|
|
237
393
|
} else {
|
|
238
394
|
throw CameraControllerError.inputsAreInvalid
|
|
@@ -257,6 +413,10 @@ extension CameraController {
|
|
|
257
413
|
private func addOutputsToSession(cameraMode: Bool, aspectRatio: String?) throws {
|
|
258
414
|
guard let captureSession = self.captureSession else { throw CameraControllerError.captureSessionIsMissing }
|
|
259
415
|
|
|
416
|
+
// Begin configuration to batch all changes
|
|
417
|
+
captureSession.beginConfiguration()
|
|
418
|
+
defer { captureSession.commitConfiguration() }
|
|
419
|
+
|
|
260
420
|
// Update session preset based on aspect ratio if needed
|
|
261
421
|
var targetPreset: AVCaptureSession.Preset = .high // Default to high quality
|
|
262
422
|
|
|
@@ -283,7 +443,7 @@ extension CameraController {
|
|
|
283
443
|
|
|
284
444
|
// Add photo output (already created in prepareOutputs)
|
|
285
445
|
if let photoOutput = self.photoOutput, captureSession.canAddOutput(photoOutput) {
|
|
286
|
-
photoOutput.isHighResolutionCaptureEnabled =
|
|
446
|
+
photoOutput.isHighResolutionCaptureEnabled = true
|
|
287
447
|
captureSession.addOutput(photoOutput)
|
|
288
448
|
}
|
|
289
449
|
|
|
@@ -295,9 +455,10 @@ extension CameraController {
|
|
|
295
455
|
// Add data output
|
|
296
456
|
if let dataOutput = self.dataOutput, captureSession.canAddOutput(dataOutput) {
|
|
297
457
|
captureSession.addOutput(dataOutput)
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
458
|
+
// Set delegate after outputs are added for better performance
|
|
459
|
+
DispatchQueue.main.async {
|
|
460
|
+
dataOutput.setSampleBufferDelegate(self, queue: DispatchQueue.main)
|
|
461
|
+
}
|
|
301
462
|
}
|
|
302
463
|
}
|
|
303
464
|
|
|
@@ -306,33 +467,32 @@ extension CameraController {
|
|
|
306
467
|
|
|
307
468
|
print("[CameraPreview] displayPreview called with view frame: \(view.frame)")
|
|
308
469
|
|
|
309
|
-
|
|
310
|
-
|
|
470
|
+
// Create and configure preview layer in one go
|
|
471
|
+
let previewLayer = AVCaptureVideoPreviewLayer(session: captureSession)
|
|
311
472
|
|
|
312
|
-
//
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
// Set contentsScale for retina display quality
|
|
317
|
-
self.previewLayer?.contentsScale = UIScreen.main.scale
|
|
473
|
+
// Batch all layer configuration to avoid multiple redraws
|
|
474
|
+
CATransaction.begin()
|
|
475
|
+
CATransaction.setDisableActions(true)
|
|
318
476
|
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
477
|
+
previewLayer.videoGravity = AVLayerVideoGravity.resizeAspectFill
|
|
478
|
+
previewLayer.connection?.videoOrientation = .portrait
|
|
479
|
+
previewLayer.isOpaque = true
|
|
480
|
+
previewLayer.contentsScale = UIScreen.main.scale
|
|
481
|
+
previewLayer.frame = view.bounds
|
|
323
482
|
|
|
324
|
-
|
|
483
|
+
// Insert layer and store reference
|
|
484
|
+
view.layer.insertSublayer(previewLayer, at: 0)
|
|
485
|
+
self.previewLayer = previewLayer
|
|
325
486
|
|
|
326
|
-
// Disable animation for frame update
|
|
327
|
-
CATransaction.begin()
|
|
328
|
-
CATransaction.setDisableActions(true)
|
|
329
|
-
self.previewLayer?.frame = view.bounds
|
|
330
487
|
CATransaction.commit()
|
|
331
488
|
|
|
332
489
|
print("[CameraPreview] Set preview layer frame to view bounds: \(view.bounds)")
|
|
333
490
|
print("[CameraPreview] Session preset: \(captureSession.sessionPreset.rawValue)")
|
|
334
491
|
|
|
335
|
-
|
|
492
|
+
// Update video orientation asynchronously to avoid blocking
|
|
493
|
+
DispatchQueue.main.async { [weak self] in
|
|
494
|
+
self?.updateVideoOrientation()
|
|
495
|
+
}
|
|
336
496
|
}
|
|
337
497
|
|
|
338
498
|
func addGridOverlay(to view: UIView, gridMode: String) {
|
|
@@ -512,6 +672,26 @@ extension CameraController {
|
|
|
512
672
|
|
|
513
673
|
let settings = AVCapturePhotoSettings()
|
|
514
674
|
|
|
675
|
+
// Apply the current flash mode to the photo settings
|
|
676
|
+
// Check if the current device supports flash
|
|
677
|
+
var currentCamera: AVCaptureDevice?
|
|
678
|
+
switch currentCameraPosition {
|
|
679
|
+
case .front:
|
|
680
|
+
currentCamera = self.frontCamera
|
|
681
|
+
case .rear:
|
|
682
|
+
currentCamera = self.rearCamera
|
|
683
|
+
default:
|
|
684
|
+
break
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
// Only apply flash if the device has flash and the flash mode is supported
|
|
688
|
+
if let device = currentCamera, device.hasFlash {
|
|
689
|
+
let supportedFlashModes = photoOutput.supportedFlashModes
|
|
690
|
+
if supportedFlashModes.contains(self.flashMode) {
|
|
691
|
+
settings.flashMode = self.flashMode
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
|
|
515
695
|
self.photoCaptureCompletionBlock = { (image, error) in
|
|
516
696
|
if let error = error {
|
|
517
697
|
completion(nil, error)
|
|
@@ -749,7 +929,7 @@ extension CameraController {
|
|
|
749
929
|
)
|
|
750
930
|
}
|
|
751
931
|
|
|
752
|
-
func setZoom(level: CGFloat, ramp: Bool) throws {
|
|
932
|
+
func setZoom(level: CGFloat, ramp: Bool, autoFocus: Bool = true) throws {
|
|
753
933
|
var currentCamera: AVCaptureDevice?
|
|
754
934
|
switch currentCameraPosition {
|
|
755
935
|
case .front:
|
|
@@ -780,12 +960,67 @@ extension CameraController {
|
|
|
780
960
|
|
|
781
961
|
// Update our internal zoom factor tracking
|
|
782
962
|
self.zoomFactor = zoomLevel
|
|
963
|
+
|
|
964
|
+
// Trigger autofocus after zoom if requested
|
|
965
|
+
if autoFocus {
|
|
966
|
+
self.triggerAutoFocus()
|
|
967
|
+
}
|
|
783
968
|
} catch {
|
|
784
969
|
throw CameraControllerError.invalidOperation
|
|
785
970
|
}
|
|
786
971
|
}
|
|
787
972
|
|
|
788
|
-
func
|
|
973
|
+
private func triggerAutoFocus() {
|
|
974
|
+
var currentCamera: AVCaptureDevice?
|
|
975
|
+
switch currentCameraPosition {
|
|
976
|
+
case .front:
|
|
977
|
+
currentCamera = self.frontCamera
|
|
978
|
+
case .rear:
|
|
979
|
+
currentCamera = self.rearCamera
|
|
980
|
+
default: break
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
guard let device = currentCamera else {
|
|
984
|
+
return
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
// Focus on the center of the preview (0.5, 0.5)
|
|
988
|
+
let centerPoint = CGPoint(x: 0.5, y: 0.5)
|
|
989
|
+
|
|
990
|
+
do {
|
|
991
|
+
try device.lockForConfiguration()
|
|
992
|
+
|
|
993
|
+
// Set focus mode to auto if supported
|
|
994
|
+
if device.isFocusModeSupported(.autoFocus) {
|
|
995
|
+
device.focusMode = .autoFocus
|
|
996
|
+
if device.isFocusPointOfInterestSupported {
|
|
997
|
+
device.focusPointOfInterest = centerPoint
|
|
998
|
+
}
|
|
999
|
+
} else if device.isFocusModeSupported(.continuousAutoFocus) {
|
|
1000
|
+
device.focusMode = .continuousAutoFocus
|
|
1001
|
+
if device.isFocusPointOfInterestSupported {
|
|
1002
|
+
device.focusPointOfInterest = centerPoint
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
// Also set exposure point if supported
|
|
1007
|
+
if device.isExposurePointOfInterestSupported && device.isExposureModeSupported(.autoExpose) {
|
|
1008
|
+
device.exposureMode = .autoExpose
|
|
1009
|
+
device.exposurePointOfInterest = centerPoint
|
|
1010
|
+
} else if device.isExposureModeSupported(.continuousAutoExposure) {
|
|
1011
|
+
device.exposureMode = .continuousAutoExposure
|
|
1012
|
+
if device.isExposurePointOfInterestSupported {
|
|
1013
|
+
device.exposurePointOfInterest = centerPoint
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
device.unlockForConfiguration()
|
|
1018
|
+
} catch {
|
|
1019
|
+
// Silently ignore errors during autofocus
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
func setFocus(at point: CGPoint, showIndicator: Bool = false, in view: UIView? = nil) throws {
|
|
789
1024
|
var currentCamera: AVCaptureDevice?
|
|
790
1025
|
switch currentCameraPosition {
|
|
791
1026
|
case .front:
|
|
@@ -804,6 +1039,13 @@ extension CameraController {
|
|
|
804
1039
|
return
|
|
805
1040
|
}
|
|
806
1041
|
|
|
1042
|
+
// Show focus indicator if requested and view is provided
|
|
1043
|
+
if showIndicator, let view = view, let previewLayer = self.previewLayer {
|
|
1044
|
+
// Convert the device point to layer point for indicator display
|
|
1045
|
+
let layerPoint = previewLayer.layerPointConverted(fromCaptureDevicePoint: point)
|
|
1046
|
+
showFocusIndicator(at: layerPoint, in: view)
|
|
1047
|
+
}
|
|
1048
|
+
|
|
807
1049
|
do {
|
|
808
1050
|
try device.lockForConfiguration()
|
|
809
1051
|
|
|
@@ -999,6 +1241,9 @@ extension CameraController {
|
|
|
999
1241
|
self.previewLayer?.removeFromSuperlayer()
|
|
1000
1242
|
self.previewLayer = nil
|
|
1001
1243
|
|
|
1244
|
+
self.focusIndicatorView?.removeFromSuperview()
|
|
1245
|
+
self.focusIndicatorView = nil
|
|
1246
|
+
|
|
1002
1247
|
self.frontCameraInput = nil
|
|
1003
1248
|
self.rearCameraInput = nil
|
|
1004
1249
|
self.audioInput = nil
|
|
@@ -1013,6 +1258,9 @@ extension CameraController {
|
|
|
1013
1258
|
|
|
1014
1259
|
self.captureSession = nil
|
|
1015
1260
|
self.currentCameraPosition = nil
|
|
1261
|
+
|
|
1262
|
+
// Reset output preparation status
|
|
1263
|
+
self.outputsPrepared = false
|
|
1016
1264
|
}
|
|
1017
1265
|
|
|
1018
1266
|
func captureVideo() throws {
|
|
@@ -1089,6 +1337,11 @@ extension CameraController: UIGestureRecognizerDelegate {
|
|
|
1089
1337
|
let point = tap.location(in: tap.view)
|
|
1090
1338
|
let devicePoint = self.previewLayer?.captureDevicePointConverted(fromLayerPoint: point)
|
|
1091
1339
|
|
|
1340
|
+
// Show focus indicator at the tap point
|
|
1341
|
+
if let view = tap.view {
|
|
1342
|
+
showFocusIndicator(at: point, in: view)
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1092
1345
|
do {
|
|
1093
1346
|
try device.lockForConfiguration()
|
|
1094
1347
|
defer { device.unlockForConfiguration() }
|
|
@@ -1109,6 +1362,54 @@ extension CameraController: UIGestureRecognizerDelegate {
|
|
|
1109
1362
|
}
|
|
1110
1363
|
}
|
|
1111
1364
|
|
|
1365
|
+
private func showFocusIndicator(at point: CGPoint, in view: UIView) {
|
|
1366
|
+
// Remove any existing focus indicator
|
|
1367
|
+
focusIndicatorView?.removeFromSuperview()
|
|
1368
|
+
|
|
1369
|
+
// Create a new focus indicator
|
|
1370
|
+
let indicator = UIView(frame: CGRect(x: 0, y: 0, width: 80, height: 80))
|
|
1371
|
+
indicator.center = point
|
|
1372
|
+
indicator.layer.borderColor = UIColor.yellow.cgColor
|
|
1373
|
+
indicator.layer.borderWidth = 2.0
|
|
1374
|
+
indicator.layer.cornerRadius = 40
|
|
1375
|
+
indicator.backgroundColor = UIColor.clear
|
|
1376
|
+
indicator.alpha = 0
|
|
1377
|
+
indicator.transform = CGAffineTransform(scaleX: 1.5, y: 1.5)
|
|
1378
|
+
|
|
1379
|
+
// Add inner circle for better visibility
|
|
1380
|
+
let innerCircle = UIView(frame: CGRect(x: 20, y: 20, width: 40, height: 40))
|
|
1381
|
+
innerCircle.layer.borderColor = UIColor.yellow.cgColor
|
|
1382
|
+
innerCircle.layer.borderWidth = 1.0
|
|
1383
|
+
innerCircle.layer.cornerRadius = 20
|
|
1384
|
+
innerCircle.backgroundColor = UIColor.clear
|
|
1385
|
+
indicator.addSubview(innerCircle)
|
|
1386
|
+
|
|
1387
|
+
view.addSubview(indicator)
|
|
1388
|
+
focusIndicatorView = indicator
|
|
1389
|
+
|
|
1390
|
+
// Animate the focus indicator
|
|
1391
|
+
UIView.animate(withDuration: 0.15, animations: {
|
|
1392
|
+
indicator.alpha = 1.0
|
|
1393
|
+
indicator.transform = CGAffineTransform.identity
|
|
1394
|
+
}) { _ in
|
|
1395
|
+
// Keep the indicator visible for a moment
|
|
1396
|
+
UIView.animate(withDuration: 0.2, delay: 0.5, options: [], animations: {
|
|
1397
|
+
indicator.alpha = 0.3
|
|
1398
|
+
}) { _ in
|
|
1399
|
+
// Fade out and remove
|
|
1400
|
+
UIView.animate(withDuration: 0.3, delay: 0.2, options: [], animations: {
|
|
1401
|
+
indicator.alpha = 0
|
|
1402
|
+
indicator.transform = CGAffineTransform(scaleX: 0.8, y: 0.8)
|
|
1403
|
+
}) { _ in
|
|
1404
|
+
indicator.removeFromSuperview()
|
|
1405
|
+
if self.focusIndicatorView == indicator {
|
|
1406
|
+
self.focusIndicatorView = nil
|
|
1407
|
+
}
|
|
1408
|
+
}
|
|
1409
|
+
}
|
|
1410
|
+
}
|
|
1411
|
+
}
|
|
1412
|
+
|
|
1112
1413
|
@objc
|
|
1113
1414
|
private func handlePinch(_ pinch: UIPinchGestureRecognizer) {
|
|
1114
1415
|
guard let device = self.currentCameraPosition == .rear ? rearCamera : frontCamera else { return }
|
|
@@ -1223,6 +1524,7 @@ enum CameraControllerError: Swift.Error {
|
|
|
1223
1524
|
case cannotFindDocumentsDirectory
|
|
1224
1525
|
case fileVideoOutputNotFound
|
|
1225
1526
|
case unknown
|
|
1527
|
+
case invalidZoomLevel(min: CGFloat, max: CGFloat, requested: CGFloat)
|
|
1226
1528
|
}
|
|
1227
1529
|
|
|
1228
1530
|
public enum CameraPosition {
|
|
@@ -1249,6 +1551,8 @@ extension CameraControllerError: LocalizedError {
|
|
|
1249
1551
|
return NSLocalizedString("Cannot find documents directory", comment: "This should never happen")
|
|
1250
1552
|
case .fileVideoOutputNotFound:
|
|
1251
1553
|
return NSLocalizedString("Video recording is not available. Make sure the camera is properly initialized.", comment: "Video recording not available")
|
|
1554
|
+
case .invalidZoomLevel(let min, let max, let requested):
|
|
1555
|
+
return NSLocalizedString("Invalid zoom level. Must be between \(min) and \(max). Requested: \(requested)", comment: "Invalid Zoom Level")
|
|
1252
1556
|
}
|
|
1253
1557
|
}
|
|
1254
1558
|
}
|
|
@@ -1312,9 +1616,13 @@ extension UIImage {
|
|
|
1312
1616
|
|
|
1313
1617
|
switch imageOrientation {
|
|
1314
1618
|
case .left, .leftMirrored, .right, .rightMirrored:
|
|
1315
|
-
|
|
1619
|
+
if let cgImage = self.cgImage {
|
|
1620
|
+
ctx.draw(cgImage, in: CGRect(x: 0, y: 0, width: size.height, height: size.width))
|
|
1621
|
+
}
|
|
1316
1622
|
default:
|
|
1317
|
-
|
|
1623
|
+
if let cgImage = self.cgImage {
|
|
1624
|
+
ctx.draw(cgImage, in: CGRect(x: 0, y: 0, width: size.width, height: size.height))
|
|
1625
|
+
}
|
|
1318
1626
|
}
|
|
1319
1627
|
guard let newCGImage = ctx.makeImage() else { return nil }
|
|
1320
1628
|
return UIImage.init(cgImage: newCGImage, scale: 1, orientation: .up)
|