From 4168d8f752550658fa8a1b5b7f8f2c2555eae5b1 Mon Sep 17 00:00:00 2001 From: Marc Rousavy Date: Thu, 15 Feb 2024 17:33:19 +0100 Subject: [PATCH] feat: Allow focus calls to be cancelable (#2567) * feat: Allow focus calls to be cancelable * Cancelable --- .../com/mrousavy/camera/core/CameraError.kt | 1 + .../core/PersistentCameraCaptureSession.kt | 34 ++++++++++++------- .../CameraCaptureSession+precapture.kt | 10 +++++- package/src/CameraError.ts | 1 + 4 files changed, 33 insertions(+), 13 deletions(-) diff --git a/package/android/src/main/java/com/mrousavy/camera/core/CameraError.kt b/package/android/src/main/java/com/mrousavy/camera/core/CameraError.kt index 6e7c8c6..834c190 100644 --- a/package/android/src/main/java/com/mrousavy/camera/core/CameraError.kt +++ b/package/android/src/main/java/com/mrousavy/camera/core/CameraError.kt @@ -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") diff --git a/package/android/src/main/java/com/mrousavy/camera/core/PersistentCameraCaptureSession.kt b/package/android/src/main/java/com/mrousavy/camera/core/PersistentCameraCaptureSession.kt index df580b5..707c7b3 100644 --- a/package/android/src/main/java/com/mrousavy/camera/core/PersistentCameraCaptureSession.kt +++ b/package/android/src/main/java/com/mrousavy/camera/core/PersistentCameraCaptureSession.kt @@ -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) } diff --git a/package/android/src/main/java/com/mrousavy/camera/extensions/CameraCaptureSession+precapture.kt b/package/android/src/main/java/com/mrousavy/camera/extensions/CameraCaptureSession+precapture.kt index 89af52c..ea9178d 100644 --- a/package/android/src/main/java/com/mrousavy/camera/extensions/CameraCaptureSession+precapture.kt +++ b/package/android/src/main/java/com/mrousavy/camera/extensions/CameraCaptureSession+precapture.kt @@ -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, @@ -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) diff --git a/package/src/CameraError.ts b/package/src/CameraError.ts index f47f61f..cca7c75 100644 --- a/package/src/CameraError.ts +++ b/package/src/CameraError.ts @@ -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 =