From b3a88278de55236e56306d1df5bbf743e95e8ea3 Mon Sep 17 00:00:00 2001
From: Marc Rousavy <me@mrousavy.com>
Date: Tue, 13 Feb 2024 13:32:11 +0100
Subject: [PATCH] perf: Fix double configuration flicker on fast device change
 (#2537)

* fix: Fix double configuration on device change

Fixes a situation that happened on every device change (or initial mount) where the device was configured after the session, separately, instead of just all at once causing an additonal delay/flicker of the prevjew.

* Try?

* Format

* Update CameraSession.swift

* Use `defer`

* Throw `.device`
---
 package/ios/Core/CameraSession.swift | 132 ++++++++++++---------------
 1 file changed, 56 insertions(+), 76 deletions(-)

diff --git a/package/ios/Core/CameraSession.swift b/package/ios/Core/CameraSession.swift
index 92caaacc2c..1af5dca6de 100644
--- a/package/ios/Core/CameraSession.swift
+++ b/package/ios/Core/CameraSession.swift
@@ -117,51 +117,64 @@ class CameraSession: NSObject, AVCaptureVideoDataOutputSampleBufferDelegate, AVC
       do {
         // If needed, configure the AVCaptureSession (inputs, outputs)
         if difference.isSessionConfigurationDirty {
-          try self.withSessionLock {
-            // 1. Update input device
-            if difference.inputChanged {
-              try self.configureDevice(configuration: config)
-            }
-            // 2. Update outputs
-            if difference.outputsChanged {
-              try self.configureOutputs(configuration: config)
-            }
-            // 3. Update Video Stabilization
-            if difference.videoStabilizationChanged {
-              self.configureVideoStabilization(configuration: config)
-            }
-            // 4. Update output orientation
-            if difference.orientationChanged {
-              self.configureOrientation(configuration: config)
-            }
+          self.captureSession.beginConfiguration()
+
+          // 1. Update input device
+          if difference.inputChanged {
+            try self.configureDevice(configuration: config)
+          }
+          // 2. Update outputs
+          if difference.outputsChanged {
+            try self.configureOutputs(configuration: config)
+          }
+          // 3. Update Video Stabilization
+          if difference.videoStabilizationChanged {
+            self.configureVideoStabilization(configuration: config)
+          }
+          // 4. Update output orientation
+          if difference.orientationChanged {
+            self.configureOrientation(configuration: config)
           }
         }
 
+        guard let device = self.videoDeviceInput?.device else {
+          throw CameraError.device(.noDevice)
+        }
+
         // If needed, configure the AVCaptureDevice (format, zoom, low-light-boost, ..)
         if difference.isDeviceConfigurationDirty {
-          try self.withDeviceLock { device in
-            // 4. Configure format
-            if difference.formatChanged {
-              try self.configureFormat(configuration: config, device: device)
-            }
-            // 5. After step 2. and 4., we also need to configure the PixelFormat.
-            //    This needs to be done AFTER we updated the `format`, as this controls the supported PixelFormats.
-            if difference.outputsChanged || difference.formatChanged {
-              try self.configurePixelFormat(configuration: config)
-            }
-            // 6. Configure side-props (fps, lowLightBoost)
-            if difference.sidePropsChanged {
-              try self.configureSideProps(configuration: config, device: device)
-            }
-            // 7. Configure zoom
-            if difference.zoomChanged {
-              self.configureZoom(configuration: config, device: device)
-            }
-            // 8. Configure exposure bias
-            if difference.exposureChanged {
-              self.configureExposure(configuration: config, device: device)
-            }
+          try device.lockForConfiguration()
+          defer {
+            device.unlockForConfiguration()
+          }
+
+          // 4. Configure format
+          if difference.formatChanged {
+            try self.configureFormat(configuration: config, device: device)
+          }
+          // 5. After step 2. and 4., we also need to configure the PixelFormat.
+          //    This needs to be done AFTER we updated the `format`, as this controls the supported PixelFormats.
+          if difference.outputsChanged || difference.formatChanged {
+            try self.configurePixelFormat(configuration: config)
+          }
+          // 6. Configure side-props (fps, lowLightBoost)
+          if difference.sidePropsChanged {
+            try self.configureSideProps(configuration: config, device: device)
+          }
+          // 7. Configure zoom
+          if difference.zoomChanged {
+            self.configureZoom(configuration: config, device: device)
           }
+          // 8. Configure exposure bias
+          if difference.exposureChanged {
+            self.configureExposure(configuration: config, device: device)
+          }
+        }
+
+        if difference.isSessionConfigurationDirty {
+          // We commit the session config updates AFTER the device config,
+          // that way we can also batch those changes into one update instead of doing two updates.
+          self.captureSession.commitConfiguration()
         }
 
         // 9. Start or stop the session if needed
@@ -169,9 +182,11 @@ class CameraSession: NSObject, AVCaptureVideoDataOutputSampleBufferDelegate, AVC
 
         // 10. Enable or disable the Torch if needed (requires session to be running)
         if difference.torchChanged {
-          try self.withDeviceLock { device in
-            try self.configureTorch(configuration: config, device: device)
+          try device.lockForConfiguration()
+          defer {
+            device.unlockForConfiguration()
           }
+          try self.configureTorch(configuration: config, device: device)
         }
 
         // Notify about Camera initialization
@@ -206,41 +221,6 @@ class CameraSession: NSObject, AVCaptureVideoDataOutputSampleBufferDelegate, AVC
     }
   }
 
-  /**
-   Runs the given [lambda] under an AVCaptureSession configuration lock (`beginConfiguration()`)
-   */
-  private func withSessionLock(_ lambda: () throws -> Void) throws {
-    // Lock Capture Session for configuration
-    ReactLogger.log(level: .info, message: "Beginning CameraSession configuration...")
-    captureSession.beginConfiguration()
-    defer {
-      // Unlock Capture Session again and submit configuration to Hardware
-      self.captureSession.commitConfiguration()
-      ReactLogger.log(level: .info, message: "Committed CameraSession configuration!")
-    }
-
-    // Call lambda
-    try lambda()
-  }
-
-  /**
-   Runs the given [lambda] under an AVCaptureDevice configuration lock (`lockForConfiguration()`)
-   */
-  private func withDeviceLock(_ lambda: (_ device: AVCaptureDevice) throws -> Void) throws {
-    guard let device = videoDeviceInput?.device else {
-      throw CameraError.session(.cameraNotReady)
-    }
-    ReactLogger.log(level: .info, message: "Beginning CaptureDevice configuration...")
-    try device.lockForConfiguration()
-    defer {
-      device.unlockForConfiguration()
-      ReactLogger.log(level: .info, message: "Committed CaptureDevice configuration!")
-    }
-
-    // Call lambda with Device
-    try lambda(device)
-  }
-
   /**
    Starts or stops the CaptureSession if needed (`isActive`)
    */