blob: 484cd5489e2844a9f269b2e23be20d237b82ef1f [file] [log] [blame]
/*
* Copyright 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/*
* Copyright 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
@file:RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
package androidx.camera.camera2.pipe.integration.impl
import android.annotation.SuppressLint
import android.hardware.camera2.CameraCharacteristics.CONTROL_AE_STATE_FLASH_REQUIRED
import android.hardware.camera2.CameraDevice
import android.hardware.camera2.CaptureFailure
import android.hardware.camera2.CaptureResult
import androidx.annotation.RequiresApi
import androidx.camera.camera2.pipe.CameraGraph
import androidx.camera.camera2.pipe.FrameInfo
import androidx.camera.camera2.pipe.FrameNumber
import androidx.camera.camera2.pipe.Lock3ABehavior
import androidx.camera.camera2.pipe.Request
import androidx.camera.camera2.pipe.RequestMetadata
import androidx.camera.camera2.pipe.Result3A
import androidx.camera.camera2.pipe.core.Log.debug
import androidx.camera.camera2.pipe.core.Log.info
import androidx.camera.camera2.pipe.integration.compat.workaround.UseTorchAsFlash
import androidx.camera.camera2.pipe.integration.compat.workaround.isFlashAvailable
import androidx.camera.camera2.pipe.integration.compat.workaround.shouldStopRepeatingBeforeCapture
import androidx.camera.camera2.pipe.integration.config.UseCaseCameraScope
import androidx.camera.camera2.pipe.integration.config.UseCaseGraphConfig
import androidx.camera.core.ImageCapture.CAPTURE_MODE_MAXIMIZE_QUALITY
import androidx.camera.core.ImageCapture.ERROR_CAMERA_CLOSED
import androidx.camera.core.ImageCapture.ERROR_CAPTURE_FAILED
import androidx.camera.core.ImageCapture.FLASH_MODE_AUTO
import androidx.camera.core.ImageCapture.FLASH_MODE_OFF
import androidx.camera.core.ImageCapture.FLASH_MODE_ON
import androidx.camera.core.ImageCapture.FLASH_TYPE_USE_TORCH_AS_FLASH
import androidx.camera.core.ImageCapture.FlashMode
import androidx.camera.core.ImageCapture.FlashType
import androidx.camera.core.ImageCaptureException
import androidx.camera.core.TorchState
import java.util.concurrent.TimeUnit
import javax.inject.Inject
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.joinAll
import kotlinx.coroutines.launch
private val CHECK_3A_TIMEOUT_IN_NS = TimeUnit.SECONDS.toNanos(1)
private val CHECK_3A_WITH_FLASH_TIMEOUT_IN_NS = TimeUnit.SECONDS.toNanos(5)
interface CapturePipeline {
var template: Int
suspend fun submitStillCaptures(
requests: List<Request>,
captureMode: Int,
flashType: Int,
flashMode: Int,
): List<Deferred<Void?>>
}
/**
* Implementations for the single capture.
*/
@UseCaseCameraScope
class CapturePipelineImpl @Inject constructor(
private val torchControl: TorchControl,
private val threads: UseCaseThreads,
private val requestListener: ComboRequestListener,
private val useTorchAsFlash: UseTorchAsFlash,
cameraProperties: CameraProperties,
private val useCaseCameraState: UseCaseCameraState,
useCaseGraphConfig: UseCaseGraphConfig,
) : CapturePipeline {
private val graph = useCaseGraphConfig.graph
// If there is no flash unit, skip the flash related task instead of failing the pipeline.
private val hasFlashUnit = cameraProperties.isFlashAvailable()
override var template = CameraDevice.TEMPLATE_PREVIEW
override suspend fun submitStillCaptures(
requests: List<Request>,
captureMode: Int,
flashType: Int,
flashMode: Int,
): List<Deferred<Void?>> = if (isTorchAsFlash(flashType)) {
torchAsFlashCapture(requests, captureMode, flashMode)
} else {
defaultCapture(requests, captureMode, flashMode)
}
private suspend fun torchAsFlashCapture(
requests: List<Request>,
captureMode: Int,
flashMode: Int,
): List<Deferred<Void?>> =
if (hasFlashUnit && isFlashRequired(flashMode)) {
torchApplyCapture(requests, captureMode, CHECK_3A_WITH_FLASH_TIMEOUT_IN_NS)
} else {
defaultNoFlashCapture(requests, captureMode)
}
private suspend fun defaultCapture(
requests: List<Request>,
captureMode: Int,
flashMode: Int,
): List<Deferred<Void?>> {
return if (hasFlashUnit) {
val isFlashRequired = isFlashRequired(flashMode)
val timeout =
if (isFlashRequired) CHECK_3A_WITH_FLASH_TIMEOUT_IN_NS else CHECK_3A_TIMEOUT_IN_NS
if (isFlashRequired || captureMode == CAPTURE_MODE_MAXIMIZE_QUALITY) {
aePreCaptureApplyCapture(requests, timeout)
} else {
defaultNoFlashCapture(requests, captureMode)
}
} else {
defaultNoFlashCapture(requests, captureMode)
}
}
private suspend fun defaultNoFlashCapture(
requests: List<Request>,
captureMode: Int
): List<Deferred<Void?>> {
val lock3ARequired = captureMode == CAPTURE_MODE_MAXIMIZE_QUALITY
if (lock3ARequired) {
lock3A(CHECK_3A_TIMEOUT_IN_NS)
}
return submitRequestInternal(requests).also { captureSignal ->
if (lock3ARequired) {
threads.sequentialScope.launch {
captureSignal.joinAll()
unlock3A()
}
}
}
}
private suspend fun torchApplyCapture(
requests: List<Request>,
captureMode: Int,
timeLimitNs: Long,
): List<Deferred<Void?>> {
val torchOnRequired = torchControl.torchStateLiveData.value == TorchState.OFF
if (torchOnRequired) {
torchControl.setTorchAsync(true).join()
}
val lock3ARequired = torchOnRequired || captureMode == CAPTURE_MODE_MAXIMIZE_QUALITY
if (lock3ARequired) {
lock3A(timeLimitNs)
}
return submitRequestInternal(requests).also { captureSignal ->
if (torchOnRequired) {
threads.sequentialScope.launch {
captureSignal.joinAll()
@Suppress("DeferredResultUnused")
torchControl.setTorchAsync(false)
}
}
if (lock3ARequired) {
threads.sequentialScope.launch {
captureSignal.joinAll()
unlock3A()
}
}
}
}
private suspend fun aePreCaptureApplyCapture(
requests: List<Request>,
timeLimitNs: Long,
): List<Deferred<Void?>> {
graph.acquireSession().use {
it.lock3AForCapture(timeLimitNs = timeLimitNs).join()
}
return submitRequestInternal(requests).also { captureSignal ->
threads.sequentialScope.launch {
captureSignal.joinAll()
graph.acquireSession().use {
@Suppress("DeferredResultUnused")
it.unlock3APostCapture()
}
}
}
}
private suspend fun lock3A(timeLimitNs: Long): Result3A = graph.acquireSession().use {
it.lock3A(
aeLockBehavior = Lock3ABehavior.AFTER_CURRENT_SCAN,
afLockBehavior = Lock3ABehavior.AFTER_CURRENT_SCAN,
awbLockBehavior = Lock3ABehavior.AFTER_CURRENT_SCAN,
timeLimitNs = timeLimitNs,
)
}.await()
private suspend fun unlock3A(): Result3A = graph.acquireSession().use {
it.unlock3A(ae = true, af = true, awb = true)
}.await()
private fun submitRequestInternal(requests: List<Request>): List<Deferred<Void?>> {
val deferredList = mutableListOf<CompletableDeferred<Void?>>()
val requestsToSubmit = requests.map { request ->
request.copy(listeners = request.listeners.toMutableList().also { newRequestListeners ->
deferredList.add(CompletableDeferred<Void?>().also { completeSignal ->
newRequestListeners.add(object : Request.Listener {
override fun onAborted(request: Request) {
completeSignal.completeExceptionally(
ImageCaptureException(
ERROR_CAMERA_CLOSED,
"Capture request is cancelled because camera is closed",
null
)
)
}
override fun onTotalCaptureResult(
requestMetadata: RequestMetadata,
frameNumber: FrameNumber,
totalCaptureResult: FrameInfo,
) {
completeSignal.complete(null)
}
@Deprecated(
message = "Migrating to using RequestFailureWrapper instead of " +
"CaptureFailure",
level = DeprecationLevel.WARNING,
replaceWith = ReplaceWith("onFailed")
)
@SuppressLint("ClassVerificationFailure")
override fun onFailed(
requestMetadata: RequestMetadata,
frameNumber: FrameNumber,
captureFailure: CaptureFailure
) {
completeSignal.completeExceptionally(
ImageCaptureException(
ERROR_CAPTURE_FAILED,
"Capture request failed with reason " + captureFailure.reason,
null
)
)
}
})
})
})
}
threads.sequentialScope.launch {
// graph.acquireSession may fail if camera has entered closing stage
var cameraGraphSession: CameraGraph.Session? = null
try {
cameraGraphSession = graph.acquireSession()
} catch (_: CancellationException) {
info {
"CapturePipeline#submitRequestInternal:" +
" CameraGraph.Session could not be acquired, requests may need " +
"re-submission"
}
// completing the requests exceptionally so that they are retried with next camera
deferredList.forEach {
it.completeExceptionally(
ImageCaptureException(
ERROR_CAMERA_CLOSED,
"Capture request is cancelled because camera is closed",
null
)
)
}
}
cameraGraphSession?.use {
val requiresStopRepeating = requestsToSubmit.shouldStopRepeatingBeforeCapture()
if (requiresStopRepeating) {
it.stopRepeating()
}
it.submit(requestsToSubmit)
if (requiresStopRepeating) {
deferredList.joinAll()
useCaseCameraState.tryStartRepeating()
}
}
}
return deferredList
}
private suspend fun isFlashRequired(@FlashMode flashMode: Int): Boolean =
when (flashMode) {
FLASH_MODE_ON -> true
FLASH_MODE_AUTO -> {
waitForResult()?.metadata?.get(
CaptureResult.CONTROL_AE_STATE
) == CONTROL_AE_STATE_FLASH_REQUIRED
}
FLASH_MODE_OFF -> false
else -> throw AssertionError(flashMode)
}
private suspend fun waitForResult(
waitTimeout: Long = 0,
checker: (totalCaptureResult: FrameInfo) -> Boolean = { _ -> true }
): FrameInfo? = ResultListener(waitTimeout, checker).also { listener ->
requestListener.addListener(listener, threads.sequentialExecutor)
threads.sequentialScope.launch {
listener.result.join()
requestListener.removeListener(listener)
}
}.result.await()
private fun isTorchAsFlash(@FlashType flashType: Int): Boolean {
return template == CameraDevice.TEMPLATE_RECORD ||
flashType == FLASH_TYPE_USE_TORCH_AS_FLASH ||
useTorchAsFlash.shouldUseTorchAsFlash()
}
}
/**
* A listener receives the result from the repeating request, and sends it to the [checker] to
* determine if the [completeSignal] can be completed.
*
* @constructor
* @param timeLimitNs timeout threshold in Nanos, set 0 for no timeout case.
* @param checker the checker to define the condition to complete the [completeSignal]. Return true
* will complete the [completeSignal], otherwise it will continue to receive the results until the
* timeLimitNs is reached.
*/
class ResultListener(
private val timeLimitNs: Long,
private val checker: (totalCaptureResult: FrameInfo) -> Boolean,
) : Request.Listener {
private val completeSignal = CompletableDeferred<FrameInfo?>()
val result: Deferred<FrameInfo?>
get() = completeSignal
@Volatile
private var timestampOfFirstUpdateNs: Long? = null
override fun onTotalCaptureResult(
requestMetadata: RequestMetadata,
frameNumber: FrameNumber,
totalCaptureResult: FrameInfo,
) {
// Save some compute if the task is already complete or has been canceled.
if (completeSignal.isCompleted || completeSignal.isCancelled) {
return
}
val currentTimestampNs: Long? =
totalCaptureResult.metadata[CaptureResult.SENSOR_TIMESTAMP]
if (currentTimestampNs != null && timestampOfFirstUpdateNs == null) {
timestampOfFirstUpdateNs = currentTimestampNs
}
val timestampOfFirstUpdateNs = timestampOfFirstUpdateNs
if (timeLimitNs != 0L &&
timestampOfFirstUpdateNs != null &&
currentTimestampNs != null &&
currentTimestampNs - timestampOfFirstUpdateNs > timeLimitNs
) {
completeSignal.complete(null)
debug {
"Wait for capture result timeout, current: $currentTimestampNs " +
"first: $timestampOfFirstUpdateNs"
}
return
}
if (!checker(totalCaptureResult)) {
return
}
completeSignal.complete(totalCaptureResult)
}
}