blob: 861f79f747e04eb5ac21dce5ed366ea48ef5674c [file] [log] [blame]
/*
* Copyright 2023 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.
*/
package androidx.camera.camera2.pipe.integration.impl
import android.hardware.camera2.CameraDevice
import android.hardware.camera2.CaptureRequest
import android.os.Build
import android.view.Surface
import androidx.annotation.GuardedBy
import androidx.annotation.RequiresApi
import androidx.camera.camera2.pipe.CameraStream
import androidx.camera.camera2.pipe.core.Log
import androidx.camera.camera2.pipe.integration.adapter.RequestProcessorAdapter
import androidx.camera.camera2.pipe.integration.adapter.SessionConfigAdapter
import androidx.camera.camera2.pipe.integration.interop.CaptureRequestOptions
import androidx.camera.camera2.pipe.integration.interop.ExperimentalCamera2Interop
import androidx.camera.core.ImageAnalysis
import androidx.camera.core.ImageCapture
import androidx.camera.core.Preview
import androidx.camera.core.UseCase
import androidx.camera.core.impl.CameraInfoInternal
import androidx.camera.core.impl.CaptureConfig
import androidx.camera.core.impl.DeferrableSurface
import androidx.camera.core.impl.DeferrableSurfaces
import androidx.camera.core.impl.OutputSurface
import androidx.camera.core.impl.OutputSurfaceConfiguration
import androidx.camera.core.impl.SessionConfig
import androidx.camera.core.impl.SessionProcessor
import androidx.camera.core.impl.SessionProcessor.CaptureCallback
import androidx.camera.core.impl.utils.executor.CameraXExecutors
import androidx.camera.core.impl.utils.futures.Futures
import androidx.camera.core.streamsharing.StreamSharing
import androidx.concurrent.futures.await
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import kotlinx.coroutines.withTimeoutOrNull
@OptIn(ExperimentalCamera2Interop::class)
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
class SessionProcessorManager(
private val sessionProcessor: SessionProcessor,
private val cameraInfoInternal: CameraInfoInternal,
private val scope: CoroutineScope,
) {
private val lock = Any()
enum class State {
/**
* [CREATED] is the initial state, and indicates that the [SessionProcessorManager] has
* been created but not initialized yet.
*/
CREATED,
/**
* [INITIALIZED] indicates that the [SessionProcessor] has been initialized and we've
* received the updated session configurations. See also:
* [SessionProcessor.deInitSession].
*/
INITIALIZED,
/**
* [STARTED] indicates that we've provided our
* [androidx.camera.core.impl.RequestProcessor] implementation to [SessionProcessor].
* See also [SessionProcessor.onCaptureSessionStart].
*/
STARTED,
/**
* [CLOSING] indicates that we're ending our capture session, and we'll no longer accept
* any further capture requests. See also: [SessionProcessor.onCaptureSessionEnd].
*/
CLOSING,
/**
* [CLOSED] indicates that the underlying capture session has been completely closed
* and we've de-initialized the session. See also: [SessionProcessor.deInitSession].
*/
CLOSED,
}
@GuardedBy("lock")
private var state: State = State.CREATED
@GuardedBy("lock")
private var sessionOptions = CaptureRequestOptions.Builder().build()
@GuardedBy("lock")
private var stillCaptureOptions = CaptureRequestOptions.Builder().build()
@GuardedBy("lock")
private var requestProcessor: RequestProcessorAdapter? = null
@GuardedBy("lock")
private var pendingCaptureConfigs: List<CaptureConfig>? = null
@GuardedBy("lock")
private var pendingCaptureCallbacks: List<CaptureCallback>? = null
@GuardedBy("lock")
internal var sessionConfig: SessionConfig? = null
set(value) = synchronized(lock) {
field = checkNotNull(value)
if (state != State.STARTED) return
checkNotNull(requestProcessor).sessionConfig = value
sessionOptions =
CaptureRequestOptions.Builder.from(value.implementationOptions).build()
updateOptions()
}
internal fun isClosed() = synchronized(lock) {
state == State.CLOSED || state == State.CLOSING
}
internal fun initialize(
useCaseManager: UseCaseManager,
useCases: List<UseCase>,
timeoutMillis: Long = 5_000L,
configure: (UseCaseManager.Companion.UseCaseManagerConfig?) -> Unit,
) = scope.launch {
val sessionConfigAdapter = SessionConfigAdapter(useCases, null)
val deferrableSurfaces = sessionConfigAdapter.deferrableSurfaces
val surfaces = getSurfaces(deferrableSurfaces, timeoutMillis)
if (!isActive) return@launch configure(null)
if (surfaces.isEmpty()) {
Log.error { "Cannot initialize ${this@SessionProcessorManager}: Surface list is empty" }
return@launch configure(null)
}
if (surfaces.contains(null)) {
Log.error {
"Cannot initialize ${this@SessionProcessorManager}: Some Surfaces are invalid!"
}
sessionConfigAdapter.reportSurfaceInvalid(
deferrableSurfaces[surfaces.indexOf(null)]
)
return@launch configure(null)
}
var previewOutputSurface: OutputSurface? = null
var captureOutputSurface: OutputSurface? = null
var analysisOutputSurface: OutputSurface? = null
var postviewOutputSurface: OutputSurface? = null
for ((deferrableSurface, surface) in deferrableSurfaces.zip(surfaces)) {
when (deferrableSurface.containerClass) {
Preview::class.java ->
previewOutputSurface = createOutputSurface(deferrableSurface, surface!!)
StreamSharing::class.java ->
previewOutputSurface = createOutputSurface(deferrableSurface, surface!!)
ImageCapture::class.java ->
captureOutputSurface = createOutputSurface(deferrableSurface, surface!!)
ImageAnalysis::class.java ->
analysisOutputSurface = createOutputSurface(deferrableSurface, surface!!)
}
}
val postviewOutputConfig =
sessionConfigAdapter.getValidSessionConfigOrNull()?.postviewOutputConfig
var postviewDeferrableSurface: DeferrableSurface? = null
if (postviewOutputConfig != null) {
postviewDeferrableSurface = postviewOutputConfig.surface
postviewOutputSurface = createOutputSurface(
postviewDeferrableSurface,
postviewDeferrableSurface.surface.get()!!
)
}
Log.debug {
"SessionProcessorSurfaceManager: Identified surfaces: " +
"previewOutputSurface = $previewOutputSurface, " +
"captureOutputSurface = $captureOutputSurface, " +
"analysisOutputSurface = $analysisOutputSurface, " +
"postviewOutputSurface = $postviewOutputSurface"
}
// IMPORTANT: The critical section (covered by synchronized) is intentionally expanded to
// cover the sections where we increment and decrement (on failure) the use count on the
// DeferrableSurfaces. This is needed because the SessionProcessorManager could be closed
// while we're still initializing, and we need to make sure we either initialize to a point
// where all the lifetimes of Surfaces are setup or we don't initialize at all beyond this
// point.
val processorSessionConfig = synchronized(lock) {
if (isClosed()) return@launch configure(null)
try {
val surfacesToIncrement = ArrayList(deferrableSurfaces)
postviewDeferrableSurface?.let { surfacesToIncrement.add(it) }
DeferrableSurfaces.incrementAll(surfacesToIncrement)
} catch (exception: DeferrableSurface.SurfaceClosedException) {
sessionConfigAdapter.reportSurfaceInvalid(exception.deferrableSurface)
return@launch configure(null)
}
try {
Log.debug { "Invoking $sessionProcessor SessionProcessor#initSession" }
sessionProcessor.initSession(
cameraInfoInternal,
OutputSurfaceConfiguration.create(
previewOutputSurface!!,
captureOutputSurface!!,
analysisOutputSurface,
postviewOutputSurface,
),
).also {
state = State.INITIALIZED
}
} catch (throwable: Throwable) {
Log.error(throwable) { "initSession() failed" }
DeferrableSurfaces.decrementAll(deferrableSurfaces)
postviewDeferrableSurface?.decrementUseCount()
throw throwable
}
}
// DecrementAll the output surfaces when ProcessorSurface terminates.
processorSessionConfig.surfaces.first().terminationFuture.addListener({
DeferrableSurfaces.decrementAll(deferrableSurfaces)
postviewDeferrableSurface?.decrementUseCount()
}, CameraXExecutors.directExecutor())
val processorSessionConfigAdapter =
SessionConfigAdapter(useCases, processorSessionConfig)
val streamConfigMap = mutableMapOf<CameraStream.Config, DeferrableSurface>()
val cameraGraphConfig = useCaseManager.createCameraGraphConfig(
processorSessionConfigAdapter,
streamConfigMap,
isExtensions = true,
)
val useCaseManagerConfig = UseCaseManager.Companion.UseCaseManagerConfig(
useCases,
processorSessionConfigAdapter,
cameraGraphConfig,
streamConfigMap,
)
return@launch configure(useCaseManagerConfig)
}
internal fun onCaptureSessionStart(requestProcessor: RequestProcessorAdapter) {
var captureConfigsToIssue: List<CaptureConfig>?
var captureCallbacksToIssue: List<CaptureCallback>?
synchronized(lock) {
if (state != State.INITIALIZED) {
Log.warn { "onCaptureSessionStart called on an uninitialized extensions session" }
return
}
requestProcessor.sessionConfig = sessionConfig
this.requestProcessor = requestProcessor
captureConfigsToIssue = pendingCaptureConfigs
captureCallbacksToIssue = pendingCaptureCallbacks
pendingCaptureConfigs = null
pendingCaptureCallbacks = null
Log.debug { "Invoking SessionProcessor#onCaptureSessionStart" }
sessionProcessor.onCaptureSessionStart(requestProcessor)
state = State.STARTED
}
startRepeating(object : CaptureCallback {})
captureConfigsToIssue?.let { captureConfigs ->
submitCaptureConfigs(captureConfigs, checkNotNull(captureCallbacksToIssue))
}
}
internal fun startRepeating(captureCallback: CaptureCallback) {
synchronized(lock) {
if (state != State.STARTED) return
Log.debug { "Invoking SessionProcessor#startRepeating" }
sessionProcessor.startRepeating(captureCallback)
}
}
internal fun stopRepeating() {
synchronized(lock) {
if (state != State.STARTED) return
Log.debug { "Invoking SessionProcessor#stopRepeating" }
sessionProcessor.stopRepeating()
}
}
internal fun submitCaptureConfigs(
captureConfigs: List<CaptureConfig>,
captureCallbacks: List<CaptureCallback>,
) = synchronized(lock) {
check(captureConfigs.size == captureCallbacks.size)
if (state != State.STARTED) {
// The lifetime of image capture requests is separate from the extensions lifetime.
// It is therefore possible for capture requests to be issued when the capture session
// hasn't yet started (before invoking SessionProcessor.onCaptureSessionStart). This is
// a copy of camera-camera2's behavior where it stores the last capture configs that
// weren't submitted.
Log.info {
"SessionProcessor#submitCaptureConfigs: Session not yet started. " +
"The capture requests will be submitted later"
}
pendingCaptureConfigs = captureConfigs
pendingCaptureCallbacks = captureCallbacks
return
}
for ((config, callback) in captureConfigs.zip(captureCallbacks)) {
if (config.templateType == CameraDevice.TEMPLATE_STILL_CAPTURE) {
val builder = CaptureRequestOptions.Builder.from(config.implementationOptions)
if (config.implementationOptions.containsOption(CaptureConfig.OPTION_ROTATION)) {
builder.setCaptureRequestOption(
CaptureRequest.JPEG_ORIENTATION,
config.implementationOptions.retrieveOption(CaptureConfig.OPTION_ROTATION)
)
}
if (config.implementationOptions.containsOption(
CaptureConfig.OPTION_JPEG_QUALITY
)
) {
builder.setCaptureRequestOption(
CaptureRequest.JPEG_QUALITY,
config.implementationOptions.retrieveOption(
CaptureConfig.OPTION_JPEG_QUALITY
)!!.toByte()
)
}
synchronized(lock) {
stillCaptureOptions = builder.build()
updateOptions()
}
Log.debug { "Invoking SessionProcessor.startCapture()" }
sessionProcessor.startCapture(config.isPostviewEnabled, callback)
} else {
val options =
CaptureRequestOptions.Builder.from(config.implementationOptions).build()
Log.debug { "Invoking SessionProcessor.startTrigger()" }
sessionProcessor.startTrigger(options, callback)
}
}
}
internal fun prepareClose() = synchronized(lock) {
if (state == State.STARTED) {
sessionProcessor.onCaptureSessionEnd()
}
// If we have an initialized SessionProcessor session (i.e., initSession was called), we
// need to make sure close() invokes deInitSession and only does it when necessary.
if (state == State.INITIALIZED || state == State.STARTED) {
state = State.CLOSING
} else {
state = State.CLOSED
}
}
internal fun close() = synchronized(lock) {
// These states indicate that we had previously initialized a session (but not yet
// de-initialized), and thus we need to de-initialize the session here.
if (state == State.INITIALIZED || state == State.STARTED || state == State.CLOSING) {
Log.debug { "Invoking $sessionProcessor SessionProcessor#deInitSession" }
sessionProcessor.deInitSession()
}
state = State.CLOSED
}
@GuardedBy("lock")
private fun updateOptions() {
val builder = Camera2ImplConfig.Builder().apply {
insertAllOptions(sessionOptions)
insertAllOptions(stillCaptureOptions)
}
sessionProcessor.setParameters(builder.build())
}
companion object {
private suspend fun getSurfaces(
deferrableSurfaces: List<DeferrableSurface>,
timeoutMillis: Long,
): List<Surface?> {
return withTimeoutOrNull(timeMillis = timeoutMillis) {
Futures.successfulAsList(deferrableSurfaces.map {
Futures.nonCancellationPropagating(it.surface)
}).await()
}.orEmpty()
}
private fun createOutputSurface(
deferrableSurface: DeferrableSurface,
surface: Surface
) =
OutputSurface.create(
surface,
deferrableSurface.prescribedSize,
deferrableSurface.prescribedStreamFormat,
)
}
}