feat: Allow focus calls to be cancelable (#2567)
* feat: Allow focus calls to be cancelable * Cancelable
This commit is contained in:
parent
bcd12649e2
commit
4168d8f752
@ -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")
|
||||||
|
@ -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.
|
||||||
val request = repeatingRequest.createCaptureRequest(device, deviceDetails, outputs)
|
focusJob = coroutineScope.launch {
|
||||||
val options = PrecaptureOptions(listOf(PrecaptureTrigger.AF, PrecaptureTrigger.AE), Flash.OFF, listOf(point), false)
|
val request = repeatingRequest.createCaptureRequest(device, deviceDetails, outputs)
|
||||||
session.precapture(request, deviceDetails, options)
|
val options = PrecaptureOptions(listOf(PrecaptureTrigger.AF, PrecaptureTrigger.AE), Flash.OFF, listOf(point), false)
|
||||||
|
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)
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
|
@ -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 =
|
||||||
|
Loading…
Reference in New Issue
Block a user