| /* |
| * Copyright 2020 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:Suppress("NOTHING_TO_INLINE") |
| |
| package androidx.camera.camera2.pipe.integration.impl |
| |
| import android.hardware.camera2.CaptureFailure |
| import android.hardware.camera2.CaptureRequest |
| import androidx.annotation.GuardedBy |
| import androidx.annotation.RequiresApi |
| import androidx.camera.camera2.pipe.AeMode |
| import androidx.camera.camera2.pipe.AfMode |
| import androidx.camera.camera2.pipe.AwbMode |
| import androidx.camera.camera2.pipe.CameraGraph |
| import androidx.camera.camera2.pipe.FrameInfo |
| import androidx.camera.camera2.pipe.FrameNumber |
| import androidx.camera.camera2.pipe.Metadata |
| import androidx.camera.camera2.pipe.Request |
| import androidx.camera.camera2.pipe.RequestMetadata |
| import androidx.camera.camera2.pipe.RequestTemplate |
| import androidx.camera.camera2.pipe.StreamId |
| import androidx.camera.camera2.pipe.core.Log |
| import androidx.camera.camera2.pipe.integration.config.UseCaseCameraScope |
| import androidx.camera.camera2.pipe.integration.config.UseCaseGraphConfig |
| import javax.inject.Inject |
| import kotlinx.atomicfu.atomic |
| import kotlinx.coroutines.CompletableDeferred |
| import kotlinx.coroutines.CoroutineStart |
| import kotlinx.coroutines.Deferred |
| import kotlinx.coroutines.launch |
| |
| /** |
| * This object keeps track of the state of the current [UseCaseCamera]. |
| * |
| * Updates to the camera from this class are batched together. That is, if multiple updates |
| * happen while some other system is holding the cameraGraph session, those updates will be |
| * aggregated together and applied when the session becomes available. This also serves as a form |
| * of primitive rate limiting that ensures that updates arriving too quickly are only sent to the |
| * underlying camera graph as fast as the camera is capable of consuming them. |
| */ |
| @RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java |
| @UseCaseCameraScope |
| class UseCaseCameraState @Inject constructor( |
| useCaseGraphConfig: UseCaseGraphConfig, |
| private val threads: UseCaseThreads |
| ) { |
| private val lock = Any() |
| |
| private val cameraGraph = useCaseGraphConfig.graph |
| |
| @GuardedBy("lock") |
| private var updateSignal: CompletableDeferred<Unit>? = null |
| |
| @GuardedBy("lock") |
| private val submittedRequestCounter = atomic(0) |
| |
| data class RequestSignal(val requestNo: Int, val signal: CompletableDeferred<Unit>) |
| |
| @GuardedBy("lock") |
| private var updateSignals = ArrayDeque<RequestSignal>() |
| |
| @GuardedBy("lock") |
| private var updating = false |
| |
| @GuardedBy("lock") |
| private val currentParameters = mutableMapOf<CaptureRequest.Key<*>, Any>() |
| |
| @GuardedBy("lock") |
| private val currentInternalParameters = mutableMapOf<Metadata.Key<*>, Any>() |
| |
| @GuardedBy("lock") |
| private val currentStreams = mutableSetOf<StreamId>() |
| |
| @GuardedBy("lock") |
| private val currentListeners = mutableSetOf<Request.Listener>() |
| |
| @GuardedBy("lock") |
| private var currentTemplate: RequestTemplate? = null |
| |
| private val requestListener = RequestListener() |
| |
| /** |
| * Updates the camera state by applying the provided parameters to a repeating request and |
| * returns a [Deferred] signal that is completed only when a capture request with equal or |
| * larger request number is completed or failed. |
| * |
| * In case the corresponding capture request of a signal is aborted, it is not completed right |
| * then. This is because a quick succession of update requests may lead to the previous request |
| * being aborted while the request parameters should still be applied unless it was changed in |
| * the new request. If the new request has a value change for some parameter, it is the |
| * responsibility of the caller to keep track of that and take necessary action. |
| * |
| * @return A [Deferred] signal to represent if the update operation has been completed. |
| */ |
| fun updateAsync( |
| parameters: Map<CaptureRequest.Key<*>, Any>? = null, |
| appendParameters: Boolean = true, |
| internalParameters: Map<Metadata.Key<*>, Any>? = null, |
| appendInternalParameters: Boolean = true, |
| streams: Set<StreamId>? = null, |
| template: RequestTemplate? = null, |
| listeners: Set<Request.Listener>? = null, |
| ): Deferred<Unit> { |
| val result: Deferred<Unit> |
| synchronized(lock) { |
| // This block does several things while locked, and is paired with another |
| // synchronized(lock) section in the submitLatest() method below that prevents these |
| // two blocks from ever executing at the same time, even if invoked by multiple |
| // threads. |
| // 1) Update the internal state (locked) |
| // 2) Since a prior update may have happened that didn't need a completion signal, |
| // it is possible that updateSignal is null. Regardless of the need to resubmit or |
| // not, the updateSignal must have a value to be returned. |
| // 3) If an update is already dispatched, return existing update signal. This |
| // updateSignal may be the value from #2 (this is fine). |
| // 4) If we get this far, we need to dispatch an update. Mark this as updating, and |
| // exit the locked section. |
| // 5) If updating, invoke submit without holding the lock. |
| |
| updateState( |
| parameters, appendParameters, internalParameters, |
| appendInternalParameters, streams, template, |
| listeners |
| ) |
| |
| if (updateSignal == null) { |
| updateSignal = CompletableDeferred() |
| } |
| if (updating) { |
| return updateSignal!! |
| } |
| |
| // Fall through to submit if there is no pending update. |
| updating = true |
| result = updateSignal!! |
| } |
| |
| submitLatest() |
| return result |
| } |
| |
| fun update( |
| parameters: Map<CaptureRequest.Key<*>, Any>? = null, |
| appendParameters: Boolean = true, |
| internalParameters: Map<Metadata.Key<*>, Any>? = null, |
| appendInternalParameters: Boolean = true, |
| streams: Set<StreamId>? = null, |
| template: RequestTemplate? = null, |
| listeners: Set<Request.Listener>? = null |
| ) { |
| synchronized(lock) { |
| // See updateAsync for details. |
| updateState( |
| parameters, appendParameters, internalParameters, |
| appendInternalParameters, streams, template, |
| listeners |
| ) |
| if (updating) { |
| return |
| } |
| updating = true |
| } |
| submitLatest() |
| } |
| |
| fun capture(requests: List<Request>) { |
| threads.scope.launch(start = CoroutineStart.UNDISPATCHED) { |
| cameraGraph.acquireSession().use { |
| it.submit(requests) |
| } |
| } |
| } |
| |
| @GuardedBy("lock") |
| private inline fun updateState( |
| parameters: Map<CaptureRequest.Key<*>, Any>? = null, |
| appendParameters: Boolean = true, |
| internalParameters: Map<Metadata.Key<*>, Any>? = null, |
| appendInternalParameters: Boolean = true, |
| streams: Set<StreamId>? = null, |
| template: RequestTemplate? = null, |
| listeners: Set<Request.Listener>? = null |
| ) { |
| // TODO: Consider if this should detect changes and only invoke an update if state has |
| // actually changed. |
| |
| if (parameters != null) { |
| if (!appendParameters) { |
| currentParameters.clear() |
| } |
| currentParameters.putAll(parameters) |
| } |
| if (internalParameters != null) { |
| if (!appendInternalParameters) { |
| currentInternalParameters.clear() |
| } |
| currentInternalParameters.putAll(internalParameters) |
| } |
| if (streams != null) { |
| currentStreams.clear() |
| currentStreams.addAll(streams) |
| } |
| if (template != null) { |
| currentTemplate = template |
| } |
| if (listeners != null) { |
| currentListeners.clear() |
| currentListeners.addAll(listeners) |
| } |
| } |
| |
| /** |
| * Tries to invoke [androidx.camera.camera2.pipe.CameraGraph.Session.startRepeating] |
| * with current (the most recent) set of values. |
| */ |
| fun tryStartRepeating() = submitLatest() |
| |
| private fun submitLatest() { |
| // Update the cameraGraph with the most recent set of values. |
| // Since acquireSession is a suspending function, it's possible that subsequent updates |
| // can occur while waiting for the acquireSession call to complete. If this happens, |
| // updates to the internal state are aggregated together, and the Request is built |
| // synchronously with the latest values. The startRepeating/stopRepeating call happens |
| // outside of the synchronized block to avoid holding a lock while updating the camera |
| // state. |
| |
| threads.scope.launch(start = CoroutineStart.UNDISPATCHED) { |
| val result: CompletableDeferred<Unit>? |
| val request: Request? |
| cameraGraph.acquireSession().use { |
| synchronized(lock) { |
| request = if (currentStreams.isEmpty()) { |
| null |
| } else { |
| Request( |
| template = currentTemplate, |
| streams = currentStreams.toList(), |
| parameters = currentParameters.toMap(), |
| extras = currentInternalParameters.toMutableMap().also { parameters -> |
| parameters[USE_CASE_CAMERA_STATE_CUSTOM_TAG] = |
| submittedRequestCounter.incrementAndGet() |
| }, |
| listeners = currentListeners.toMutableList().also { listeners -> |
| listeners.add(requestListener) |
| } |
| ) |
| } |
| result = updateSignal |
| updating = false |
| updateSignal = null |
| } |
| |
| if (request == null) { |
| it.stopRepeating() |
| } else { |
| result?.let { result -> |
| synchronized(lock) { |
| updateSignals.add(RequestSignal(submittedRequestCounter.value, result)) |
| } |
| } |
| Log.debug { "Update RepeatingRequest: $request" } |
| it.startRepeating(request) |
| it.update3A(request.parameters) |
| } |
| } |
| |
| // complete the result instantly only when the request was not submitted |
| if (request == null) { |
| // Complete the result after the session closes to allow other threads to acquire a |
| // lock. This also avoids cases where complete() synchronously invokes expensive |
| // calls. |
| result?.complete(Unit) |
| } |
| } |
| } |
| |
| private fun CameraGraph.Session.update3A(parameters: Map<CaptureRequest.Key<*>, Any>?) { |
| val aeMode = parameters.getIntOrNull(CaptureRequest.CONTROL_AE_MODE)?.let { |
| AeMode.fromIntOrNull(it) |
| } |
| val afMode = parameters.getIntOrNull(CaptureRequest.CONTROL_AF_MODE)?.let { |
| AfMode.fromIntOrNull(it) |
| } |
| val awbMode = parameters.getIntOrNull(CaptureRequest.CONTROL_AWB_MODE)?.let { |
| AwbMode.fromIntOrNull(it) |
| } |
| |
| if (aeMode != null || afMode != null || awbMode != null) { |
| update3A(aeMode = aeMode, afMode = afMode, awbMode = awbMode) |
| } |
| } |
| |
| private fun Map<CaptureRequest.Key<*>, Any>?.getIntOrNull( |
| key: CaptureRequest.Key<*> |
| ): Int? = this?.get(key) as? Int |
| |
| inner class RequestListener : Request.Listener { |
| override fun onTotalCaptureResult( |
| requestMetadata: RequestMetadata, |
| frameNumber: FrameNumber, |
| totalCaptureResult: FrameInfo, |
| ) { |
| super.onTotalCaptureResult(requestMetadata, frameNumber, totalCaptureResult) |
| threads.scope.launch(start = CoroutineStart.UNDISPATCHED) { |
| requestMetadata[USE_CASE_CAMERA_STATE_CUSTOM_TAG]?.let { requestNo -> |
| synchronized(lock) { |
| updateSignals.complete(requestNo) |
| } |
| } |
| } |
| } |
| |
| @Deprecated( |
| message = "Migrating to using RequestFailureWrapper instead of CaptureFailure", |
| level = DeprecationLevel.WARNING |
| ) |
| override fun onFailed( |
| requestMetadata: RequestMetadata, |
| frameNumber: FrameNumber, |
| captureFailure: CaptureFailure, |
| ) { |
| @Suppress("DEPRECATION") |
| super.onFailed(requestMetadata, frameNumber, captureFailure) |
| completeExceptionally(requestMetadata, captureFailure) |
| } |
| |
| private fun completeExceptionally( |
| requestMetadata: RequestMetadata, |
| captureFailure: CaptureFailure? = null |
| ) { |
| threads.scope.launch(start = CoroutineStart.UNDISPATCHED) { |
| requestMetadata[USE_CASE_CAMERA_STATE_CUSTOM_TAG]?.let { requestNo -> |
| synchronized(lock) { |
| updateSignals.completeExceptionally( |
| requestNo, |
| Throwable( |
| "Failed in framework level" + (captureFailure?.reason?.let { |
| " with CaptureFailure.reason = $it" |
| } ?: "") |
| ) |
| ) |
| } |
| } |
| } |
| } |
| |
| private fun ArrayDeque<RequestSignal>.complete(requestNo: Int) { |
| while (isNotEmpty() && first().requestNo <= requestNo) { |
| first().signal.complete(Unit) |
| removeFirst() |
| } |
| } |
| |
| private fun ArrayDeque<RequestSignal>.completeExceptionally( |
| requestNo: Int, |
| throwable: Throwable |
| ) { |
| while (isNotEmpty() && first().requestNo <= requestNo) { |
| first().signal.completeExceptionally(throwable) |
| removeFirst() |
| } |
| } |
| } |
| } |