@capgo/camera-preview 7.13.6 → 7.13.8
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.
|
@@ -252,9 +252,30 @@ extension CameraController {
|
|
|
252
252
|
]
|
|
253
253
|
self.dataOutput?.alwaysDiscardsLateVideoFrames = true
|
|
254
254
|
|
|
255
|
-
// Pre-create preview layer to avoid delay later
|
|
255
|
+
// Pre-create preview layer without session to avoid delay later
|
|
256
256
|
if self.previewLayer == nil {
|
|
257
|
-
|
|
257
|
+
let layer = AVCaptureVideoPreviewLayer()
|
|
258
|
+
// Configure orientation immediately
|
|
259
|
+
if let connection = layer.connection {
|
|
260
|
+
if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene {
|
|
261
|
+
switch windowScene.interfaceOrientation {
|
|
262
|
+
case .portrait:
|
|
263
|
+
connection.videoOrientation = .portrait
|
|
264
|
+
case .landscapeLeft:
|
|
265
|
+
connection.videoOrientation = .landscapeLeft
|
|
266
|
+
case .landscapeRight:
|
|
267
|
+
connection.videoOrientation = .landscapeRight
|
|
268
|
+
case .portraitUpsideDown:
|
|
269
|
+
connection.videoOrientation = .portraitUpsideDown
|
|
270
|
+
case .unknown:
|
|
271
|
+
fallthrough
|
|
272
|
+
@unknown default:
|
|
273
|
+
connection.videoOrientation = .portrait
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
// Don't set session here - we'll do it during configuration
|
|
278
|
+
self.previewLayer = layer
|
|
258
279
|
}
|
|
259
280
|
|
|
260
281
|
// Mark as prepared
|
|
@@ -282,10 +303,10 @@ extension CameraController {
|
|
|
282
303
|
throw CameraControllerError.captureSessionIsMissing
|
|
283
304
|
}
|
|
284
305
|
|
|
285
|
-
// Prepare outputs
|
|
306
|
+
// Prepare outputs early
|
|
286
307
|
self.prepareOutputs()
|
|
287
308
|
|
|
288
|
-
//
|
|
309
|
+
// Single configuration block for all initial setup
|
|
289
310
|
captureSession.beginConfiguration()
|
|
290
311
|
|
|
291
312
|
// Set aspect ratio preset and remember requested ratio
|
|
@@ -298,10 +319,54 @@ extension CameraController {
|
|
|
298
319
|
// Configure device inputs
|
|
299
320
|
try self.configureDeviceInputs(cameraPosition: cameraPosition, deviceId: deviceId, disableAudio: disableAudio)
|
|
300
321
|
|
|
301
|
-
// Add
|
|
322
|
+
// Add ALL outputs BEFORE starting session to avoid flashes from reconfiguration
|
|
323
|
+
|
|
324
|
+
// Determine initial orientation once
|
|
325
|
+
var videoOrientation: AVCaptureVideoOrientation = .portrait
|
|
326
|
+
if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene {
|
|
327
|
+
switch windowScene.interfaceOrientation {
|
|
328
|
+
case .portrait: videoOrientation = .portrait
|
|
329
|
+
case .landscapeLeft: videoOrientation = .landscapeLeft
|
|
330
|
+
case .landscapeRight: videoOrientation = .landscapeRight
|
|
331
|
+
case .portraitUpsideDown: videoOrientation = .portraitUpsideDown
|
|
332
|
+
case .unknown: fallthrough
|
|
333
|
+
@unknown default: videoOrientation = .portrait
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// Add data output for preview
|
|
302
338
|
if let dataOutput = self.dataOutput, captureSession.canAddOutput(dataOutput) {
|
|
303
339
|
captureSession.addOutput(dataOutput)
|
|
304
|
-
|
|
340
|
+
// Use dedicated queue for better performance
|
|
341
|
+
let videoQueue = DispatchQueue(label: "com.camera.videoQueue", qos: .userInteractive)
|
|
342
|
+
dataOutput.setSampleBufferDelegate(self, queue: videoQueue)
|
|
343
|
+
// Set orientation immediately
|
|
344
|
+
dataOutput.connections.forEach { $0.videoOrientation = videoOrientation }
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// Add photo output immediately to avoid later reconfiguration
|
|
348
|
+
if let photoOutput = self.photoOutput, captureSession.canAddOutput(photoOutput) {
|
|
349
|
+
photoOutput.isHighResolutionCaptureEnabled = true
|
|
350
|
+
captureSession.addOutput(photoOutput)
|
|
351
|
+
// Set orientation immediately
|
|
352
|
+
photoOutput.connections.forEach { $0.videoOrientation = videoOrientation }
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// Add video output if in camera mode
|
|
356
|
+
if cameraMode, let fileVideoOutput = self.fileVideoOutput, captureSession.canAddOutput(fileVideoOutput) {
|
|
357
|
+
captureSession.addOutput(fileVideoOutput)
|
|
358
|
+
// Set orientation immediately
|
|
359
|
+
fileVideoOutput.connections.forEach { $0.videoOrientation = videoOrientation }
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
// Set up preview layer session in the same configuration block
|
|
364
|
+
if let layer = self.previewLayer {
|
|
365
|
+
layer.session = captureSession
|
|
366
|
+
// Set orientation for preview layer
|
|
367
|
+
layer.connection?.videoOrientation = videoOrientation
|
|
368
|
+
// Start with a very subtle fade to smooth any remaining visual artifacts
|
|
369
|
+
layer.opacity = 0.95
|
|
305
370
|
}
|
|
306
371
|
|
|
307
372
|
captureSession.commitConfiguration()
|
|
@@ -309,28 +374,17 @@ extension CameraController {
|
|
|
309
374
|
// Set initial zoom
|
|
310
375
|
self.setInitialZoom(level: initialZoomLevel)
|
|
311
376
|
|
|
312
|
-
// Start the session
|
|
377
|
+
// Start the session - all outputs are already configured
|
|
313
378
|
captureSession.startRunning()
|
|
314
379
|
|
|
315
|
-
//
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
// Add photo output
|
|
323
|
-
if let photoOutput = self.photoOutput, captureSession.canAddOutput(photoOutput) {
|
|
324
|
-
photoOutput.isHighResolutionCaptureEnabled = true
|
|
325
|
-
captureSession.addOutput(photoOutput)
|
|
380
|
+
// Bring to full opacity after a tiny moment to smooth any visual artifacts
|
|
381
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) { [weak self] in
|
|
382
|
+
if let layer = self?.previewLayer {
|
|
383
|
+
CATransaction.begin()
|
|
384
|
+
CATransaction.setAnimationDuration(0.1)
|
|
385
|
+
layer.opacity = 1.0
|
|
386
|
+
CATransaction.commit()
|
|
326
387
|
}
|
|
327
|
-
|
|
328
|
-
// Add video output if needed
|
|
329
|
-
if cameraMode, let fileVideoOutput = self.fileVideoOutput, captureSession.canAddOutput(fileVideoOutput) {
|
|
330
|
-
captureSession.addOutput(fileVideoOutput)
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
captureSession.commitConfiguration()
|
|
334
388
|
}
|
|
335
389
|
|
|
336
390
|
// Success callback
|
|
@@ -352,10 +406,12 @@ extension CameraController {
|
|
|
352
406
|
if let aspectRatio = aspectRatio {
|
|
353
407
|
switch aspectRatio {
|
|
354
408
|
case "16:9":
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
409
|
+
// Start with 1080p for faster initialization, 4K only when explicitly needed
|
|
410
|
+
// This maintains capture quality while optimizing preview performance
|
|
411
|
+
if captureSession.canSetSessionPreset(.hd1920x1080) {
|
|
358
412
|
targetPreset = .hd1920x1080
|
|
413
|
+
} else if captureSession.canSetSessionPreset(.hd4K3840x2160) {
|
|
414
|
+
targetPreset = .hd4K3840x2160
|
|
359
415
|
}
|
|
360
416
|
case "4:3":
|
|
361
417
|
if captureSession.canSetSessionPreset(.photo) {
|
|
@@ -538,25 +594,33 @@ extension CameraController {
|
|
|
538
594
|
}
|
|
539
595
|
|
|
540
596
|
func displayPreview(on view: UIView) throws {
|
|
597
|
+
let startTime = CFAbsoluteTimeGetCurrent()
|
|
598
|
+
|
|
541
599
|
guard let captureSession = self.captureSession, captureSession.isRunning else {
|
|
542
600
|
throw CameraControllerError.captureSessionIsMissing
|
|
543
601
|
}
|
|
544
602
|
|
|
545
|
-
|
|
546
|
-
let
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
existingLayer.session = captureSession
|
|
552
|
-
}
|
|
553
|
-
} else {
|
|
554
|
-
// Create layer with minimal properties to speed up creation
|
|
555
|
-
previewLayer = AVCaptureVideoPreviewLayer()
|
|
556
|
-
previewLayer.session = captureSession
|
|
603
|
+
print("[CameraPreview] ⏱ Guard check took \(CFAbsoluteTimeGetCurrent() - startTime) seconds")
|
|
604
|
+
let layerStartTime = CFAbsoluteTimeGetCurrent()
|
|
605
|
+
|
|
606
|
+
// Get preview layer - should already be created in prepareOutputs
|
|
607
|
+
guard let previewLayer = self.previewLayer else {
|
|
608
|
+
throw CameraControllerError.captureSessionIsMissing
|
|
557
609
|
}
|
|
558
610
|
|
|
559
|
-
//
|
|
611
|
+
// Session should already be set during configuration
|
|
612
|
+
|
|
613
|
+
print("[CameraPreview] ⏱ Layer session update took \(CFAbsoluteTimeGetCurrent() - layerStartTime) seconds")
|
|
614
|
+
|
|
615
|
+
let configStartTime = CFAbsoluteTimeGetCurrent()
|
|
616
|
+
// Optimize layer configuration with explicit transaction
|
|
617
|
+
CATransaction.begin()
|
|
618
|
+
CATransaction.setDisableActions(true) // Disable implicit animations for faster setup
|
|
619
|
+
CATransaction.setAnimationDuration(0) // No animation duration
|
|
620
|
+
|
|
621
|
+
// Start with zero alpha for smooth fade-in
|
|
622
|
+
previewLayer.opacity = 0
|
|
623
|
+
|
|
560
624
|
// Configure video gravity and frame based on aspect ratio
|
|
561
625
|
if let aspectRatio = requestedAspectRatio {
|
|
562
626
|
// Calculate the frame based on requested aspect ratio
|
|
@@ -568,12 +632,28 @@ extension CameraController {
|
|
|
568
632
|
previewLayer.frame = view.bounds
|
|
569
633
|
previewLayer.videoGravity = .resizeAspect
|
|
570
634
|
}
|
|
635
|
+
print("[CameraPreview] ⏱ Layer configuration took \(CFAbsoluteTimeGetCurrent() - configStartTime) seconds")
|
|
636
|
+
|
|
637
|
+
let insertStartTime = CFAbsoluteTimeGetCurrent()
|
|
638
|
+
// Set additional performance optimizations
|
|
639
|
+
previewLayer.shouldRasterize = false // Avoid unnecessary rasterization
|
|
640
|
+
previewLayer.drawsAsynchronously = true // Enable async rendering
|
|
641
|
+
previewLayer.allowsGroupOpacity = true // Enable group opacity animations
|
|
571
642
|
|
|
572
643
|
// Insert layer immediately (only if new)
|
|
573
644
|
if previewLayer.superlayer != view.layer {
|
|
574
645
|
view.layer.insertSublayer(previewLayer, at: 0)
|
|
646
|
+
|
|
647
|
+
// Fade in the preview layer smoothly
|
|
648
|
+
CATransaction.begin()
|
|
649
|
+
CATransaction.setAnimationDuration(0.2)
|
|
650
|
+
previewLayer.opacity = 1.0
|
|
651
|
+
CATransaction.commit()
|
|
575
652
|
}
|
|
576
|
-
|
|
653
|
+
|
|
654
|
+
CATransaction.commit()
|
|
655
|
+
print("[CameraPreview] ⏱ Layer insertion took \(CFAbsoluteTimeGetCurrent() - insertStartTime) seconds")
|
|
656
|
+
print("[CameraPreview] ⏱ Total display preview took \(CFAbsoluteTimeGetCurrent() - startTime) seconds")
|
|
577
657
|
}
|
|
578
658
|
|
|
579
659
|
func addGridOverlay(to view: UIView, gridMode: String) {
|
|
@@ -626,19 +706,9 @@ extension CameraController {
|
|
|
626
706
|
}
|
|
627
707
|
|
|
628
708
|
func updateVideoOrientation() {
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
} else {
|
|
632
|
-
DispatchQueue.main.sync {
|
|
633
|
-
self.updateVideoOrientationOnMainThread()
|
|
634
|
-
}
|
|
635
|
-
}
|
|
636
|
-
}
|
|
709
|
+
// Get orientation on the current thread to avoid blocking
|
|
710
|
+
var videoOrientation: AVCaptureVideoOrientation = .portrait
|
|
637
711
|
|
|
638
|
-
private func updateVideoOrientationOnMainThread() {
|
|
639
|
-
var videoOrientation: AVCaptureVideoOrientation
|
|
640
|
-
|
|
641
|
-
// Use window scene interface orientation
|
|
642
712
|
if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene {
|
|
643
713
|
switch windowScene.interfaceOrientation {
|
|
644
714
|
case .portrait:
|
|
@@ -654,13 +724,21 @@ extension CameraController {
|
|
|
654
724
|
@unknown default:
|
|
655
725
|
videoOrientation = .portrait
|
|
656
726
|
}
|
|
657
|
-
} else {
|
|
658
|
-
videoOrientation = .portrait
|
|
659
727
|
}
|
|
660
728
|
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
729
|
+
// Apply orientation asynchronously on main thread
|
|
730
|
+
let updateBlock = { [weak self] in
|
|
731
|
+
guard let self = self else { return }
|
|
732
|
+
self.previewLayer?.connection?.videoOrientation = videoOrientation
|
|
733
|
+
self.dataOutput?.connections.forEach { $0.videoOrientation = videoOrientation }
|
|
734
|
+
self.photoOutput?.connections.forEach { $0.videoOrientation = videoOrientation }
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
if Thread.isMainThread {
|
|
738
|
+
updateBlock()
|
|
739
|
+
} else {
|
|
740
|
+
DispatchQueue.main.async(execute: updateBlock)
|
|
741
|
+
}
|
|
664
742
|
}
|
|
665
743
|
|
|
666
744
|
private func setDefaultZoomAfterFlip() {
|
|
@@ -789,23 +867,21 @@ extension CameraController {
|
|
|
789
867
|
}
|
|
790
868
|
|
|
791
869
|
func captureImage(width: Int?, height: Int?, quality: Float, gpsLocation: CLLocation?, completion: @escaping (UIImage?, Data?, [AnyHashable: Any]?, Error?) -> Void) {
|
|
792
|
-
print("[CameraPreview] captureImage called - width: \(width ?? -1), height: \(height ?? -1), requestedAspectRatio: \(self.requestedAspectRatio ?? "nil")")
|
|
793
|
-
|
|
794
870
|
guard let photoOutput = self.photoOutput else {
|
|
795
871
|
completion(nil, nil, nil, NSError(domain: "Camera", code: 0, userInfo: [NSLocalizedDescriptionKey: "Photo output is not available"]))
|
|
796
872
|
return
|
|
797
873
|
}
|
|
798
874
|
|
|
799
875
|
let settings = AVCapturePhotoSettings()
|
|
800
|
-
// Configure photo capture settings
|
|
876
|
+
// Configure photo capture settings optimized for speed
|
|
801
877
|
if #available(iOS 13.0, *) {
|
|
802
|
-
//
|
|
803
|
-
|
|
804
|
-
let shouldUseHighRes = (width != nil || height != nil) || (self.requestedAspectRatio == nil)
|
|
878
|
+
// Only use high res if explicitly requesting large dimensions
|
|
879
|
+
let shouldUseHighRes = width.map { $0 > 1920 } ?? false || height.map { $0 > 1920 } ?? false
|
|
805
880
|
settings.isHighResolutionPhotoEnabled = shouldUseHighRes
|
|
806
881
|
}
|
|
807
882
|
if #available(iOS 15.0, *) {
|
|
808
|
-
|
|
883
|
+
// Prioritize speed over quality
|
|
884
|
+
settings.photoQualityPrioritization = .speed
|
|
809
885
|
}
|
|
810
886
|
|
|
811
887
|
// Apply the current flash mode to the photo settings
|
|
@@ -1965,20 +2041,30 @@ extension CameraController: AVCapturePhotoCaptureDelegate {
|
|
|
1965
2041
|
return
|
|
1966
2042
|
}
|
|
1967
2043
|
|
|
1968
|
-
//
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
2044
|
+
// Process photo in background to avoid blocking main thread
|
|
2045
|
+
DispatchQueue.global(qos: .userInitiated).async {
|
|
2046
|
+
// Get the photo data using the modern API
|
|
2047
|
+
guard let imageData = photo.fileDataRepresentation() else {
|
|
2048
|
+
DispatchQueue.main.async {
|
|
2049
|
+
self.photoCaptureCompletionBlock?(nil, nil, nil, CameraControllerError.unknown)
|
|
2050
|
+
}
|
|
2051
|
+
return
|
|
2052
|
+
}
|
|
1973
2053
|
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
2054
|
+
// Create image from data
|
|
2055
|
+
guard let image = UIImage(data: imageData) else {
|
|
2056
|
+
DispatchQueue.main.async {
|
|
2057
|
+
self.photoCaptureCompletionBlock?(nil, nil, nil, CameraControllerError.unknown)
|
|
2058
|
+
}
|
|
2059
|
+
return
|
|
2060
|
+
}
|
|
1978
2061
|
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
2062
|
+
// Pass through original file data and metadata so callers can preserve EXIF
|
|
2063
|
+
// Don't call fixedOrientation() here - let the completion block handle it after cropping
|
|
2064
|
+
DispatchQueue.main.async {
|
|
2065
|
+
self.photoCaptureCompletionBlock?(image, imageData, photo.metadata, nil)
|
|
2066
|
+
}
|
|
2067
|
+
}
|
|
1982
2068
|
}
|
|
1983
2069
|
}
|
|
1984
2070
|
|
|
@@ -142,6 +142,8 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin, CLLocationManagerDelega
|
|
|
142
142
|
guard let webView = self.webView else { return }
|
|
143
143
|
|
|
144
144
|
DispatchQueue.main.async {
|
|
145
|
+
let startTransparency = CFAbsoluteTimeGetCurrent()
|
|
146
|
+
|
|
145
147
|
// Define a recursive function to traverse the view hierarchy
|
|
146
148
|
func makeSubviewsTransparent(_ view: UIView) {
|
|
147
149
|
// Set the background color to clear
|
|
@@ -156,7 +158,6 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin, CLLocationManagerDelega
|
|
|
156
158
|
// Set the main webView to be transparent
|
|
157
159
|
webView.isOpaque = false
|
|
158
160
|
webView.backgroundColor = .clear
|
|
159
|
-
|
|
160
161
|
// Recursively make all subviews transparent
|
|
161
162
|
makeSubviewsTransparent(webView)
|
|
162
163
|
|
|
@@ -503,7 +504,6 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin, CLLocationManagerDelega
|
|
|
503
504
|
}
|
|
504
505
|
|
|
505
506
|
@objc func start(_ call: CAPPluginCall) {
|
|
506
|
-
let startTime = CFAbsoluteTimeGetCurrent()
|
|
507
507
|
print("[CameraPreview] 🚀 START CALLED at \(Date())")
|
|
508
508
|
|
|
509
509
|
// Log all received settings
|
|
@@ -629,21 +629,24 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin, CLLocationManagerDelega
|
|
|
629
629
|
// Create and configure the preview view first
|
|
630
630
|
self.updateCameraFrame()
|
|
631
631
|
|
|
632
|
-
//
|
|
633
|
-
self.makeWebViewTransparent()
|
|
634
|
-
|
|
635
|
-
// Add the preview view to the webview itself to use same coordinate system
|
|
632
|
+
// Add preview view to hierarchy first
|
|
636
633
|
self.webView?.addSubview(self.previewView)
|
|
637
634
|
if self.toBack! {
|
|
638
635
|
self.webView?.sendSubviewToBack(self.previewView)
|
|
639
636
|
}
|
|
640
637
|
|
|
641
|
-
//
|
|
642
|
-
|
|
638
|
+
// Make webview transparent
|
|
639
|
+
self.makeWebViewTransparent()
|
|
643
640
|
|
|
644
|
-
//
|
|
645
|
-
|
|
641
|
+
// Don't block on orientation update - it's already set during layer creation
|
|
642
|
+
// Just update asynchronously if needed for future rotations
|
|
643
|
+
DispatchQueue.main.async { [weak self] in
|
|
644
|
+
self?.cameraController.updateVideoOrientation()
|
|
645
|
+
}
|
|
646
646
|
|
|
647
|
+
// Configure preview layer - it's already hidden from CameraController
|
|
648
|
+
try? self.cameraController.displayPreview(on: self.previewView)
|
|
649
|
+
// Setup gestures
|
|
647
650
|
self.cameraController.setupGestures(target: self.previewView, enableZoom: self.enableZoom!)
|
|
648
651
|
|
|
649
652
|
// Add grid overlay if enabled
|
|
@@ -651,11 +654,10 @@ public class CameraPreview: CAPPlugin, CAPBridgedPlugin, CLLocationManagerDelega
|
|
|
651
654
|
self.cameraController.addGridOverlay(to: self.previewView, gridMode: self.gridMode)
|
|
652
655
|
}
|
|
653
656
|
|
|
657
|
+
// Setup observers for device rotation and app state changes
|
|
654
658
|
if self.rotateWhenOrientationChanged == true {
|
|
655
659
|
NotificationCenter.default.addObserver(self, selector: #selector(CameraPreview.rotated), name: UIDevice.orientationDidChangeNotification, object: nil)
|
|
656
660
|
}
|
|
657
|
-
|
|
658
|
-
// Add observers for app state changes to maintain transparency
|
|
659
661
|
NotificationCenter.default.addObserver(self, selector: #selector(CameraPreview.appDidBecomeActive), name: UIApplication.didBecomeActiveNotification, object: nil)
|
|
660
662
|
NotificationCenter.default.addObserver(self, selector: #selector(CameraPreview.appWillEnterForeground), name: UIApplication.willEnterForegroundNotification, object: nil)
|
|
661
663
|
|