@capgo/camera-preview 7.13.7 → 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
- self.previewLayer = AVCaptureVideoPreviewLayer()
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
- // Configure the session
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 data output BEFORE starting session for faster first frame
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
- dataOutput.setSampleBufferDelegate(self, queue: DispatchQueue.main)
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
- // Defer adding photo/video outputs to avoid blocking
316
- // These aren't needed immediately for preview
317
- DispatchQueue.global(qos: .utility).async { [weak self] in
318
- guard let self = self else { return }
319
-
320
- captureSession.beginConfiguration()
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
- if captureSession.canSetSessionPreset(.hd4K3840x2160) {
356
- targetPreset = .hd4K3840x2160
357
- } else if captureSession.canSetSessionPreset(.hd1920x1080) {
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
- // Create or reuse preview layer
546
- let previewLayer: AVCaptureVideoPreviewLayer
547
- if let existingLayer = self.previewLayer {
548
- // Always reuse if we have one - just update the session if needed
549
- previewLayer = existingLayer
550
- if existingLayer.session != captureSession {
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
- // Fast configuration without CATransaction overhead
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
- self.previewLayer = previewLayer
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
- if Thread.isMainThread {
630
- updateVideoOrientationOnMainThread()
631
- } else {
632
- DispatchQueue.main.sync {
633
- self.updateVideoOrientationOnMainThread()
634
- }
635
- }
636
- }
637
-
638
- private func updateVideoOrientationOnMainThread() {
639
- var videoOrientation: AVCaptureVideoOrientation
709
+ // Get orientation on the current thread to avoid blocking
710
+ var videoOrientation: AVCaptureVideoOrientation = .portrait
640
711
 
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
- previewLayer?.connection?.videoOrientation = videoOrientation
662
- dataOutput?.connections.forEach { $0.videoOrientation = videoOrientation }
663
- photoOutput?.connections.forEach { $0.videoOrientation = videoOrientation }
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() {
@@ -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
- // Make webview transparent - comprehensive approach
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
- // Display the camera preview on the configured view
642
- try? self.cameraController.displayPreview(on: self.previewView)
638
+ // Make webview transparent
639
+ self.makeWebViewTransparent()
643
640
 
644
- // Ensure the preview orientation matches the current interface orientation at startup
645
- self.cameraController.updateVideoOrientation()
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
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@capgo/camera-preview",
3
- "version": "7.13.7",
3
+ "version": "7.13.8",
4
4
  "description": "Camera preview",
5
5
  "license": "MIT",
6
6
  "repository": {