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

View File

@ -43,6 +43,7 @@ export type CaptureError =
| 'capture/photo-not-enabled' | 'capture/photo-not-enabled'
| 'capture/frame-invalid' | 'capture/frame-invalid'
| 'capture/aborted' | 'capture/aborted'
| 'capture/focus-canceled'
| 'capture/timed-out' | 'capture/timed-out'
| 'capture/unknown' | 'capture/unknown'
export type SystemError = export type SystemError =