feat: Allow focus calls to be cancelable (#2567)
* feat: Allow focus calls to be cancelable * Cancelable
This commit is contained in:
		@@ -104,6 +104,7 @@ class PhotoNotEnabledError :
 | 
			
		||||
  CameraError("capture", "photo-not-enabled", "Photo capture is disabled! Pass `photo={true}` to enable photo capture.")
 | 
			
		||||
class CaptureAbortedError(wasImageCaptured: Boolean) :
 | 
			
		||||
  CameraError("capture", "aborted", "The image capture was aborted! Was Image captured: $wasImageCaptured")
 | 
			
		||||
class FocusCanceledError : CameraError("capture", "focus-canceled", "The focus operation was canceled.")
 | 
			
		||||
class CaptureTimedOutError : CameraError("capture", "timed-out", "The image capture was aborted because it timed out.")
 | 
			
		||||
class UnknownCaptureError(wasImageCaptured: Boolean) :
 | 
			
		||||
  CameraError("capture", "unknown", "An unknown error occurred while trying to capture an Image! Was Image captured: $wasImageCaptured")
 | 
			
		||||
 
 | 
			
		||||
@@ -25,7 +25,7 @@ import com.mrousavy.camera.types.QualityPrioritization
 | 
			
		||||
import java.io.Closeable
 | 
			
		||||
import kotlinx.coroutines.CoroutineScope
 | 
			
		||||
import kotlinx.coroutines.Job
 | 
			
		||||
import kotlinx.coroutines.cancelAndJoin
 | 
			
		||||
import kotlinx.coroutines.coroutineScope
 | 
			
		||||
import kotlinx.coroutines.delay
 | 
			
		||||
import kotlinx.coroutines.isActive
 | 
			
		||||
import kotlinx.coroutines.launch
 | 
			
		||||
@@ -56,7 +56,7 @@ class PersistentCameraCaptureSession(private val cameraManager: CameraManager, p
 | 
			
		||||
 | 
			
		||||
  private val mutex = Mutex()
 | 
			
		||||
  private var didDestroyFromOutside = false
 | 
			
		||||
  private var focusResetJob: Job? = null
 | 
			
		||||
  private var focusJob: Job? = null
 | 
			
		||||
  private val coroutineScope = CoroutineScope(CameraQueues.cameraQueue.coroutineDispatcher)
 | 
			
		||||
 | 
			
		||||
  val isRunning: Boolean
 | 
			
		||||
@@ -74,6 +74,10 @@ class PersistentCameraCaptureSession(private val cameraManager: CameraManager, p
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  suspend fun withConfiguration(block: suspend () -> Unit) {
 | 
			
		||||
    // Cancel any ongoing focus jobs
 | 
			
		||||
    focusJob?.cancel()
 | 
			
		||||
    focusJob = null
 | 
			
		||||
 | 
			
		||||
    mutex.withLock {
 | 
			
		||||
      block()
 | 
			
		||||
      configure()
 | 
			
		||||
@@ -141,6 +145,10 @@ class PersistentCameraCaptureSession(private val cameraManager: CameraManager, p
 | 
			
		||||
    orientation: Orientation,
 | 
			
		||||
    enableShutterSound: Boolean
 | 
			
		||||
  ): TotalCaptureResult {
 | 
			
		||||
    // Cancel any ongoing focus jobs
 | 
			
		||||
    focusJob?.cancel()
 | 
			
		||||
    focusJob = null
 | 
			
		||||
 | 
			
		||||
    mutex.withLock {
 | 
			
		||||
      Log.i(TAG, "Capturing photo...")
 | 
			
		||||
      val session = session ?: throw CameraNotReadyError()
 | 
			
		||||
@@ -198,6 +206,10 @@ class PersistentCameraCaptureSession(private val cameraManager: CameraManager, p
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  suspend fun focus(point: Point) {
 | 
			
		||||
    // Cancel any previous focus jobs
 | 
			
		||||
    focusJob?.cancel()
 | 
			
		||||
    focusJob = null
 | 
			
		||||
 | 
			
		||||
    mutex.withLock {
 | 
			
		||||
      Log.i(TAG, "Focusing to $point...")
 | 
			
		||||
      val session = session ?: throw CameraNotReadyError()
 | 
			
		||||
@@ -209,17 +221,16 @@ class PersistentCameraCaptureSession(private val cameraManager: CameraManager, p
 | 
			
		||||
      }
 | 
			
		||||
      val outputs = outputs.filter { it.isRepeating }
 | 
			
		||||
 | 
			
		||||
      // 0. Cancel the 3 second focus reset task
 | 
			
		||||
      focusResetJob?.cancelAndJoin()
 | 
			
		||||
      focusResetJob = null
 | 
			
		||||
 | 
			
		||||
      // 1. Run a precapture sequence for AF, AE and AWB.
 | 
			
		||||
      val request = repeatingRequest.createCaptureRequest(device, deviceDetails, outputs)
 | 
			
		||||
      val options = PrecaptureOptions(listOf(PrecaptureTrigger.AF, PrecaptureTrigger.AE), Flash.OFF, listOf(point), false)
 | 
			
		||||
      session.precapture(request, deviceDetails, options)
 | 
			
		||||
      focusJob = coroutineScope.launch {
 | 
			
		||||
        val request = repeatingRequest.createCaptureRequest(device, deviceDetails, outputs)
 | 
			
		||||
        val options = PrecaptureOptions(listOf(PrecaptureTrigger.AF, PrecaptureTrigger.AE), Flash.OFF, listOf(point), false)
 | 
			
		||||
        session.precapture(request, deviceDetails, options)
 | 
			
		||||
      }
 | 
			
		||||
      focusJob?.join()
 | 
			
		||||
 | 
			
		||||
      // 2. Wait 3 seconds
 | 
			
		||||
      focusResetJob = coroutineScope.launch {
 | 
			
		||||
      // 2. Reset AF/AE/AWB again after 3 seconds timeout
 | 
			
		||||
      focusJob = coroutineScope.launch {
 | 
			
		||||
        delay(FOCUS_RESET_TIMEOUT)
 | 
			
		||||
        if (!this.isActive) {
 | 
			
		||||
          // this job got canceled from the outside
 | 
			
		||||
@@ -230,7 +241,6 @@ class PersistentCameraCaptureSession(private val cameraManager: CameraManager, p
 | 
			
		||||
          return@launch
 | 
			
		||||
        }
 | 
			
		||||
        Log.i(TAG, "Resetting focus to auto-focus...")
 | 
			
		||||
        // 3. Reset AF/AE/AWB to continuous auto-focus again, which is the default here.
 | 
			
		||||
        repeatingRequest.createCaptureRequest(device, deviceDetails, outputs).also { request ->
 | 
			
		||||
          session.setRepeatingRequest(request.build(), null, null)
 | 
			
		||||
        }
 | 
			
		||||
 
 | 
			
		||||
@@ -8,8 +8,11 @@ import android.hardware.camera2.params.MeteringRectangle
 | 
			
		||||
import android.util.Log
 | 
			
		||||
import android.util.Size
 | 
			
		||||
import com.mrousavy.camera.core.CameraDeviceDetails
 | 
			
		||||
import com.mrousavy.camera.core.FocusCanceledError
 | 
			
		||||
import com.mrousavy.camera.types.Flash
 | 
			
		||||
import com.mrousavy.camera.types.HardwareLevel
 | 
			
		||||
import kotlin.coroutines.coroutineContext
 | 
			
		||||
import kotlinx.coroutines.isActive
 | 
			
		||||
 | 
			
		||||
data class PrecaptureOptions(
 | 
			
		||||
  val modes: List<PrecaptureTrigger>,
 | 
			
		||||
@@ -65,6 +68,8 @@ suspend fun CameraCaptureSession.precapture(
 | 
			
		||||
    this.capture(request.build(), null, null)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (!coroutineContext.isActive) throw FocusCanceledError()
 | 
			
		||||
 | 
			
		||||
  val meteringWeight = MeteringRectangle.METERING_WEIGHT_MAX - 1
 | 
			
		||||
  val meteringRectangles = options.pointsOfInterest.map { point ->
 | 
			
		||||
    MeteringRectangle(point, DEFAULT_METERING_SIZE, meteringWeight)
 | 
			
		||||
@@ -115,13 +120,16 @@ suspend fun CameraCaptureSession.precapture(
 | 
			
		||||
  }
 | 
			
		||||
  this.capture(request.build(), null, null)
 | 
			
		||||
 | 
			
		||||
  if (!coroutineContext.isActive) throw FocusCanceledError()
 | 
			
		||||
 | 
			
		||||
  // 3. Start a repeating request without the trigger and wait until AF/AE/AWB locks
 | 
			
		||||
  request.set(CaptureRequest.CONTROL_AF_TRIGGER, null)
 | 
			
		||||
  request.set(CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER, null)
 | 
			
		||||
  val result = this.setRepeatingRequestAndWaitForPrecapture(request.build(), *precaptureModes.toTypedArray())
 | 
			
		||||
 | 
			
		||||
  if (!coroutineContext.isActive) throw FocusCanceledError()
 | 
			
		||||
 | 
			
		||||
  Log.i(TAG, "AF/AE/AWB successfully locked!")
 | 
			
		||||
  // TODO: Set to idle again?
 | 
			
		||||
 | 
			
		||||
  val needsFlash = result.exposureState == ExposureState.FlashRequired
 | 
			
		||||
  return PrecaptureResult(needsFlash)
 | 
			
		||||
 
 | 
			
		||||
@@ -43,6 +43,7 @@ export type CaptureError =
 | 
			
		||||
  | 'capture/photo-not-enabled'
 | 
			
		||||
  | 'capture/frame-invalid'
 | 
			
		||||
  | 'capture/aborted'
 | 
			
		||||
  | 'capture/focus-canceled'
 | 
			
		||||
  | 'capture/timed-out'
 | 
			
		||||
  | 'capture/unknown'
 | 
			
		||||
export type SystemError =
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user