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 db31fcf..6162dc2 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 @@ -42,6 +42,7 @@ class PersistentCameraCaptureSession(private val cameraManager: CameraManager, p companion object { private const val TAG = "PersistentCameraCaptureSession" private const val FOCUS_RESET_TIMEOUT = 3000L + private const val PRECAPTURE_LOCK_TIMEOUT = 5000L } // Inputs/Dependencies @@ -178,21 +179,30 @@ class PersistentCameraCaptureSession(private val cameraManager: CameraManager, p Log.i(TAG, "Locking AF/AE/AWB...") // 1. Run precapture sequence - val precaptureRequest = repeatingRequest.createCaptureRequest(device, deviceDetails, repeatingOutputs) - val skipIfPassivelyFocused = flash == Flash.OFF - val options = - PrecaptureOptions( + var needsFlash: Boolean + try { + val precaptureRequest = repeatingRequest.createCaptureRequest(device, deviceDetails, repeatingOutputs) + val skipIfPassivelyFocused = flash == Flash.OFF + val options = PrecaptureOptions( listOf(PrecaptureTrigger.AF, PrecaptureTrigger.AE, PrecaptureTrigger.AWB), flash, emptyList(), - skipIfPassivelyFocused + skipIfPassivelyFocused, + PRECAPTURE_LOCK_TIMEOUT ) - val result = session.precapture(precaptureRequest, deviceDetails, options) + val result = session.precapture(precaptureRequest, deviceDetails, options) + needsFlash = result.needsFlash + } catch (e: CaptureTimedOutError) { + // the precapture just timed out after 5 seconds, take picture anyways without focus. + needsFlash = false + } catch (e: FocusCanceledError) { + throw CaptureAbortedError(false) + } try { // 2. Once precapture AF/AE/AWB successfully locked, capture the actual photo val singleRequest = photoRequest.createCaptureRequest(device, deviceDetails, outputs) - if (result.needsFlash) { + if (needsFlash) { singleRequest.set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON) singleRequest.set(CaptureRequest.FLASH_MODE, CaptureRequest.FLASH_MODE_SINGLE) } @@ -224,7 +234,8 @@ class PersistentCameraCaptureSession(private val cameraManager: CameraManager, p // 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) + val options = + PrecaptureOptions(listOf(PrecaptureTrigger.AF, PrecaptureTrigger.AE), Flash.OFF, listOf(point), false, FOCUS_RESET_TIMEOUT) session.precapture(request, deviceDetails, options) } focusJob?.join() 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 ea9178d..e52ad2c 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 @@ -18,7 +18,8 @@ data class PrecaptureOptions( val modes: List, val flash: Flash = Flash.OFF, val pointsOfInterest: List, - val skipIfPassivelyFocused: Boolean + val skipIfPassivelyFocused: Boolean, + val timeoutMs: Long ) data class PrecaptureResult(val needsFlash: Boolean) @@ -57,12 +58,8 @@ suspend fun CameraCaptureSession.precapture( aeState = ExposureState.fromAEState(result.get(CaptureResult.CONTROL_AE_STATE) ?: CaptureResult.CONTROL_AE_STATE_INACTIVE) awbState = WhiteBalanceState.fromAWBState(result.get(CaptureResult.CONTROL_AWB_STATE) ?: CaptureResult.CONTROL_AWB_STATE_INACTIVE) - if (aeState == ExposureState.FlashRequired) { - Log.i(TAG, "Auto-Flash: Flash is required for photo capture, enabling flash...") - enableFlash = true - } else { - Log.i(TAG, "Auto-Flash: Flash is not required for photo capture.") - } + Log.i(TAG, "Precapture current states: AF: $afState, AE: $aeState, AWB: $awbState") + enableFlash = aeState == ExposureState.FlashRequired && options.flash == Flash.AUTO } else { // we either want Flash ON or OFF, so we don't care about lighting conditions - do a fast capture. this.capture(request.build(), null, null) @@ -99,23 +96,41 @@ suspend fun CameraCaptureSession.precapture( // AF Precapture if (deviceDetails.afModes.contains(CaptureRequest.CONTROL_AF_MODE_AUTO)) { request.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_AUTO) + request.set(CaptureRequest.CONTROL_AF_TRIGGER, CaptureRequest.CONTROL_AF_TRIGGER_START) + if (meteringRectangles.isNotEmpty() && deviceDetails.supportsFocusRegions) { + request.set(CaptureRequest.CONTROL_AF_REGIONS, meteringRectangles) + } + } else { + // AF is not supported on this device. + precaptureModes.remove(PrecaptureTrigger.AF) } - if (meteringRectangles.isNotEmpty() && deviceDetails.supportsFocusRegions) { - request.set(CaptureRequest.CONTROL_AF_REGIONS, meteringRectangles) - } - request.set(CaptureRequest.CONTROL_AF_TRIGGER, CaptureRequest.CONTROL_AF_TRIGGER_START) } - if (precaptureModes.contains(PrecaptureTrigger.AE) && deviceDetails.hardwareLevel.isAtLeast(HardwareLevel.LIMITED)) { + if (precaptureModes.contains(PrecaptureTrigger.AE)) { // AE Precapture - if (meteringRectangles.isNotEmpty() && deviceDetails.supportsExposureRegions) { - request.set(CaptureRequest.CONTROL_AE_REGIONS, meteringRectangles) + if (deviceDetails.aeModes.contains(CaptureRequest.CONTROL_AE_MODE_ON) && deviceDetails.hardwareLevel.isAtLeast(HardwareLevel.LIMITED)) { + request.set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON) + request.set(CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER, CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_START) + if (meteringRectangles.isNotEmpty() && + deviceDetails.supportsExposureRegions && + deviceDetails.hardwareLevel.isAtLeast(HardwareLevel.LIMITED) + ) { + request.set(CaptureRequest.CONTROL_AE_REGIONS, meteringRectangles) + } + } else { + // AE is not supported on this device. + precaptureModes.remove(PrecaptureTrigger.AE) } - request.set(CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER, CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_START) } if (precaptureModes.contains(PrecaptureTrigger.AWB)) { // AWB Precapture - if (meteringRectangles.isNotEmpty() && deviceDetails.supportsWhiteBalanceRegions) { - request.set(CaptureRequest.CONTROL_AWB_REGIONS, meteringRectangles) + if (deviceDetails.awbModes.contains(CaptureRequest.CONTROL_AWB_MODE_AUTO)) { + request.set(CaptureRequest.CONTROL_AWB_MODE, CaptureRequest.CONTROL_AWB_MODE_AUTO) + if (meteringRectangles.isNotEmpty() && deviceDetails.supportsWhiteBalanceRegions) { + request.set(CaptureRequest.CONTROL_AWB_REGIONS, meteringRectangles) + } + } else { + // AWB is not supported on this device. + precaptureModes.remove(PrecaptureTrigger.AWB) } } this.capture(request.build(), null, null) @@ -125,7 +140,7 @@ suspend fun CameraCaptureSession.precapture( // 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()) + val result = this.setRepeatingRequestAndWaitForPrecapture(request.build(), options.timeoutMs, *precaptureModes.toTypedArray()) if (!coroutineContext.isActive) throw FocusCanceledError() diff --git a/package/android/src/main/java/com/mrousavy/camera/extensions/CameraCaptureSession+setRepeatingRequestAndWaitForPrecapture.kt b/package/android/src/main/java/com/mrousavy/camera/extensions/CameraCaptureSession+setRepeatingRequestAndWaitForPrecapture.kt index 916debb..48ca860 100644 --- a/package/android/src/main/java/com/mrousavy/camera/extensions/CameraCaptureSession+setRepeatingRequestAndWaitForPrecapture.kt +++ b/package/android/src/main/java/com/mrousavy/camera/extensions/CameraCaptureSession+setRepeatingRequestAndWaitForPrecapture.kt @@ -117,6 +117,7 @@ data class ResultState(val focusState: FocusState, val exposureState: ExposureSt */ suspend fun CameraCaptureSession.setRepeatingRequestAndWaitForPrecapture( request: CaptureRequest, + timeoutMs: Long, vararg precaptureTriggers: PrecaptureTrigger ): ResultState = suspendCancellableCoroutine { continuation -> @@ -124,9 +125,9 @@ suspend fun CameraCaptureSession.setRepeatingRequestAndWaitForPrecapture( val completed = precaptureTriggers.associateWith { false }.toMutableMap() CoroutineScope(Dispatchers.Default).launch { - delay(5000) // after 5s, cancel capture + delay(timeoutMs) // after timeout, cancel capture if (continuation.isActive) { - Log.e(TAG, "Precapture timed out after 5 seconds!") + Log.e(TAG, "Precapture timed out after ${timeoutMs / 1000} seconds!") continuation.resumeWithException(CaptureTimedOutError()) try { setRepeatingRequest(request, null, null) @@ -144,25 +145,25 @@ suspend fun CameraCaptureSession.setRepeatingRequestAndWaitForPrecapture( super.onCaptureCompleted(session, request, result) if (continuation.isActive) { - // AF Precapture val afState = FocusState.fromAFState(result.get(CaptureResult.CONTROL_AF_STATE) ?: CaptureResult.CONTROL_AF_STATE_INACTIVE) - val aeState = ExposureState.fromAEState(result.get(CaptureResult.CONTROL_AE_STATE) ?: CaptureResult.CONTROL_AE_STATE_INACTIVE) + val aeState = ExposureState.fromAEState( + result.get(CaptureResult.CONTROL_AE_STATE) ?: CaptureResult.CONTROL_AE_STATE_INACTIVE + ) val awbState = WhiteBalanceState.fromAWBState( result.get(CaptureResult.CONTROL_AWB_STATE) ?: CaptureResult.CONTROL_AWB_STATE_INACTIVE ) + Log.i(TAG, "Precapture state: AF: $afState, AE: $aeState, AWB: $awbState") + // AF Precapture if (precaptureTriggers.contains(PrecaptureTrigger.AF)) { - Log.i(TAG, "AF State: $afState (isCompleted: ${afState.isCompleted})") completed[PrecaptureTrigger.AF] = afState.isCompleted } // AE Precapture if (precaptureTriggers.contains(PrecaptureTrigger.AE)) { - Log.i(TAG, "AE State: $aeState (isCompleted: ${aeState.isCompleted})") completed[PrecaptureTrigger.AE] = aeState.isCompleted } // AWB Precapture if (precaptureTriggers.contains(PrecaptureTrigger.AWB)) { - Log.i(TAG, "AWB State: $awbState (isCompleted: ${awbState.isCompleted})") completed[PrecaptureTrigger.AWB] = awbState.isCompleted }