blob: 5db75d8d3d79976e38d1e7fa69d5de415ad823d3 [file] [log] [blame]
/*
* 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.
*/
package androidx.camera.camera2.pipe.graph
import android.os.Build
import android.view.Surface
import androidx.camera.camera2.pipe.AudioRestrictionMode
import androidx.camera.camera2.pipe.CameraBackend
import androidx.camera.camera2.pipe.CameraController
import androidx.camera.camera2.pipe.CameraGraph
import androidx.camera.camera2.pipe.CameraMetadata
import androidx.camera.camera2.pipe.GraphState
import androidx.camera.camera2.pipe.StreamGraph
import androidx.camera.camera2.pipe.StreamId
import androidx.camera.camera2.pipe.compat.AudioRestrictionController
import androidx.camera.camera2.pipe.config.CameraGraphScope
import androidx.camera.camera2.pipe.core.Debug
import androidx.camera.camera2.pipe.core.Log
import androidx.camera.camera2.pipe.core.Token
import androidx.camera.camera2.pipe.core.acquireToken
import androidx.camera.camera2.pipe.core.acquireTokenAndSuspend
import androidx.camera.camera2.pipe.core.tryAcquireToken
import androidx.camera.camera2.pipe.internal.FrameCaptureQueue
import androidx.camera.camera2.pipe.internal.FrameDistributor
import androidx.camera.camera2.pipe.internal.GraphLifecycleManager
import javax.inject.Inject
import kotlinx.atomicfu.atomic
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.ensureActive
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.sync.Mutex
@CameraGraphScope
internal class CameraGraphImpl
@Inject
constructor(
graphConfig: CameraGraph.Config,
metadata: CameraMetadata,
private val cameraGraphId: CameraGraphId,
private val graphLifecycleManager: GraphLifecycleManager,
private val graphProcessor: GraphProcessor,
private val graphListener: GraphListener,
private val streamGraph: StreamGraphImpl,
private val surfaceGraph: SurfaceGraph,
private val cameraBackend: CameraBackend,
private val cameraController: CameraController,
private val graphState3A: GraphState3A,
private val listener3A: Listener3A,
private val frameDistributor: FrameDistributor,
private val frameCaptureQueue: FrameCaptureQueue,
private val audioRestrictionController: AudioRestrictionController
) : CameraGraph {
private val sessionMutex = Mutex()
private val controller3A = Controller3A(graphProcessor, metadata, graphState3A, listener3A)
private val closed = atomic(false)
init {
// Log out the configuration of the camera graph when it is created.
Log.info { Debug.formatCameraGraphProperties(metadata, graphConfig, this) }
// Enforce preview and video stream use cases for high speed sessions
if (graphConfig.sessionMode == CameraGraph.OperatingMode.HIGH_SPEED) {
require(streamGraph.outputs.isNotEmpty()) {
"Cannot create a HIGH_SPEED CameraGraph without outputs."
}
require(streamGraph.outputs.size <= 2) {
"Cannot create a HIGH_SPEED CameraGraph with more than two outputs. " +
"Configured outputs are ${streamGraph.outputs}"
}
// Streams must be preview and/or video for high speed sessions
val allStreamsValidForHighSpeedOperatingMode =
this.streamGraph.outputs.all { it.isValidForHighSpeedOperatingMode() }
require(allStreamsValidForHighSpeedOperatingMode) {
"HIGH_SPEED CameraGraph must only contain Preview and/or Video " +
"streams. Configured outputs are ${streamGraph.outputs}"
}
}
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
require(graphConfig.input == null) { "Reprocessing not supported under Android M" }
}
if (graphConfig.input != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
require(graphConfig.input.isNotEmpty()) {
"At least one InputConfiguration is required for reprocessing"
}
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
require(graphConfig.input.size <= 1) {
"Multi resolution reprocessing not supported under Android S"
}
}
}
}
override val streams: StreamGraph
get() = streamGraph
override val graphState: StateFlow<GraphState>
get() = graphProcessor.graphState
private var _isForeground = false
override var isForeground: Boolean
get() = _isForeground
set(value) {
_isForeground = value
cameraController.isForeground = value
}
override fun start() {
check(!closed.value) { "Cannot start $this after calling close()" }
Debug.traceStart { "$this#start" }
Log.info { "Starting $this" }
graphListener.onGraphStarting()
graphLifecycleManager.monitorAndStart(cameraBackend, cameraController)
Debug.traceStop()
}
override fun stop() {
check(!closed.value) { "Cannot stop $this after calling close()" }
Debug.traceStart { "$this#stop" }
Log.info { "Stopping $this" }
graphListener.onGraphStopping()
graphLifecycleManager.monitorAndStop(cameraBackend, cameraController)
Debug.traceStop()
}
override suspend fun acquireSession(): CameraGraph.Session {
// Step 1: Acquire a lock on the session mutex, which returns a releasable token. This may
// or may not suspend.
val token = sessionMutex.acquireToken()
// Step 2: Return a session that can be used to interact with the session. The session must
// be closed when it is no longer needed.
return createSessionFromToken(token)
}
override fun acquireSessionOrNull(): CameraGraph.Session? {
val token = sessionMutex.tryAcquireToken() ?: return null
return createSessionFromToken(token)
}
override suspend fun <T> useSession(
action: suspend CoroutineScope.(CameraGraph.Session) -> T
): T =
acquireSession().use {
// Wrap the block in a coroutineScope to ensure all operations are completed before
// releasing the lock.
coroutineScope { action(it) }
}
override fun <T> useSessionIn(
scope: CoroutineScope,
action: suspend CoroutineScope.(CameraGraph.Session) -> T
): Deferred<T> =
scope.async(start = CoroutineStart.UNDISPATCHED) {
ensureActive() // Exit early if the parent scope has been canceled.
// It is very important to acquire *and* suspend here. Invoking a coroutine using
// UNDISPATCHED will execute on the current thread until the suspension point, and this
// will
// force the execution to switch to the provided scope after ensuring the lock is
// acquired
// or in the queue. This guarantees exclusion, ordering, and execution within the
// correct
// scope.
val token = sessionMutex.acquireTokenAndSuspend()
// Create and use the session.
createSessionFromToken(token).use {
// Wrap the block in a coroutineScope to ensure all operations are completed before
// exiting and releasing the lock. The lock can be released early if the calling
// action
// decided to call session.close() early.
coroutineScope { action(it) }
}
}
private fun createSessionFromToken(token: Token) =
CameraGraphSessionImpl(token, graphProcessor, controller3A, frameCaptureQueue)
override fun setSurface(stream: StreamId, surface: Surface?) {
Debug.traceStart { "$stream#setSurface" }
if (surface != null && !surface.isValid) {
Log.warn { "$this#setSurface: $surface is invalid" }
}
surfaceGraph[stream] = surface
Debug.traceStop()
}
override fun updateAudioRestrictionMode(mode: AudioRestrictionMode) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
audioRestrictionController.updateCameraGraphAudioRestrictionMode(this, mode)
}
}
override fun close() {
if (closed.compareAndSet(expect = false, update = true)) {
Debug.traceStart { "$this#close" }
Log.info { "Closing $this" }
graphProcessor.close()
graphLifecycleManager.monitorAndClose(cameraBackend, cameraController)
frameDistributor.close()
frameCaptureQueue.close()
surfaceGraph.close()
audioRestrictionController.removeCameraGraph(this)
Debug.traceStop()
}
}
override fun toString(): String = cameraGraphId.toString()
}