feat: Allow focus calls to be cancelable (#2567)

* feat: Allow focus calls to be cancelable

* Cancelable
This commit is contained in:
Marc Rousavy 2024-02-15 17:33:19 +01:00 committed by GitHub
parent bcd12649e2
commit 4168d8f752
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 33 additions and 13 deletions

View File

@ -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")

View File

@ -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.
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)
}

View File

@ -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)

View File

@ -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 =