blob: f94abb03f32c7017da2b3e54a3cc86ed045412a7 [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.
*/
@file:RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
package androidx.camera.camera2.pipe.compat
import android.annotation.SuppressLint
import android.hardware.camera2.CameraManager
import android.os.Build
import androidx.annotation.RequiresApi
import androidx.camera.camera2.pipe.CameraId
import androidx.camera.camera2.pipe.core.Debug
import androidx.camera.camera2.pipe.core.Log
import androidx.camera.camera2.pipe.core.Permissions
import androidx.camera.camera2.pipe.core.Threads
import androidx.camera.camera2.pipe.core.Timestamps
import androidx.camera.camera2.pipe.core.WakeLock
import javax.inject.Inject
import javax.inject.Provider
import javax.inject.Singleton
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineName
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.cancel
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.channels.SendChannel
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import kotlinx.coroutines.withTimeoutOrNull
internal sealed class CameraRequest
internal data class RequestOpen(
val virtualCamera: VirtualCameraState,
val share: Boolean = false
) : CameraRequest()
internal data class RequestClose(
val activeCamera: VirtualCameraManager.ActiveCamera
) : CameraRequest()
internal object RequestCloseAll : CameraRequest()
private const val requestQueueDepth = 8
@Suppress("EXPERIMENTAL_API_USAGE")
@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
@Singleton
internal class VirtualCameraManager @Inject constructor(
private val cameraManager: Provider<CameraManager>,
private val cameraMetadata: Camera2MetadataCache,
private val permissions: Permissions,
private val threads: Threads
) {
// TODO: Consider rewriting this as a MutableSharedFlow
private val requestQueue: Channel<CameraRequest> = Channel(requestQueueDepth)
private val activeCameras: MutableSet<ActiveCamera> = mutableSetOf()
init {
threads.globalScope.launch(CoroutineName("CXCP-VirtualCameraManager")) { requestLoop() }
}
internal fun open(cameraId: CameraId, share: Boolean = false): VirtualCamera {
val result = VirtualCameraState(cameraId)
offerChecked(RequestOpen(result, share))
return result
}
internal fun closeAll() {
offerChecked(RequestCloseAll)
}
private fun offerChecked(request: CameraRequest) {
check(requestQueue.trySend(request).isSuccess) {
"There are more than $requestQueueDepth requests buffered!"
}
}
private suspend fun requestLoop() = coroutineScope {
val requests = arrayListOf<CameraRequest>()
while (true) {
// Stage 1: We have a request, but there is a chance we have received multiple
// requests.
readRequestQueue(requests)
// Prioritize requests that remove specific cameras from the list of active cameras.
val closeRequest = requests.firstOrNull { it is RequestClose } as? RequestClose
if (closeRequest != null) {
requests.remove(closeRequest)
if (activeCameras.contains(closeRequest.activeCamera)) {
activeCameras.remove(closeRequest.activeCamera)
}
launch {
closeRequest.activeCamera.close()
}
closeRequest.activeCamera.awaitClosed()
continue
}
// If we received a closeAll request, then close every request leading up to it.
val closeAll = requests.indexOfLast { it is RequestCloseAll }
if (closeAll >= 0) {
for (i in 0..closeAll) {
val request = requests[0]
if (request is RequestOpen) {
request.virtualCamera.disconnect()
}
requests.removeAt(0)
}
// Close all active cameras.
for (activeCamera in activeCameras) {
launch {
activeCamera.close()
}
}
for (camera in activeCameras) {
camera.awaitClosed()
}
activeCameras.clear()
continue
}
// The only way we get to this point is if:
// A) We received a request
// B) That request was NOT a Close, or CloseAll request
val request = requests[0]
check(request is RequestOpen)
// Sanity Check: If the camera we are attempting to open is now closed or disconnected,
// skip this virtual camera request.
if (request.virtualCamera.state.value !is CameraStateUnopened) {
requests.remove(request)
continue
}
// Stage 2: Intermediate requests have been discarded, and we need to evaluate the set
// of currently open cameras to the set of desired cameras and close ones that are not
// needed. Since close may block, we will re-evaluate the next request after the
// desired cameras are closed since new requests may have arrived.
val cameraIdToOpen = request.virtualCamera.cameraId
val camerasToClose = if (request.share) {
emptyList()
} else {
activeCameras.filter { it.cameraId != cameraIdToOpen }
}
if (camerasToClose.isNotEmpty()) {
// Shutdown of cameras should always happen first (and suspend until complete)
activeCameras.removeAll(camerasToClose)
for (camera in camerasToClose) {
// TODO: This should be a dispatcher instead of scope.launch
launch {
// TODO: Figure out if this should be blocking or not. If we are directly invoking
// close this method could block for 0-1000ms
camera.close()
}
}
for (realCamera in camerasToClose) {
realCamera.awaitClosed()
}
continue
}
// Stage 3: Open or select an active camera device.
var realCamera = activeCameras.firstOrNull { it.cameraId == cameraIdToOpen }
if (realCamera == null) {
realCamera = openCameraWithRetry(cameraIdToOpen, scope = this)
activeCameras.add(realCamera)
continue
}
// Stage 4: Attach camera(s)
realCamera.connectTo(request.virtualCamera)
requests.remove(request)
}
}
@OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class)
private suspend fun readRequestQueue(requests: MutableList<CameraRequest>) {
if (requests.isEmpty()) {
requests.add(requestQueue.receive())
}
// We have a request, but there is a chance we have received multiple requests while we
// were doing other things (like opening a camera).
while (!requestQueue.isEmpty) {
requests.add(requestQueue.receive())
}
}
@SuppressLint(
"MissingPermission", // Permissions are checked by calling methods.
)
private suspend fun openCameraWithRetry(
cameraId: CameraId,
scope: CoroutineScope
): ActiveCamera {
val metadata = cameraMetadata.getMetadata(cameraId)
val requestTimestamp = Timestamps.now()
var cameraState: AndroidCameraState
var attempts = 0
// TODO: Figure out how 1-time permissions work, and see if they can be reset without
// causing the application process to restart.
check(permissions.hasCameraPermission) { "Missing camera permissions!" }
while (true) {
attempts++
val instance = cameraManager.get()
cameraState = AndroidCameraState(
cameraId,
metadata,
attempts,
requestTimestamp
)
var exception: Throwable? = null
try {
Debug.trace("CameraDevice-${cameraId.value}#openCamera") {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
Api28Compat.openCamera(
instance,
cameraId.value,
threads.camera2Executor,
cameraState
)
} else {
instance.openCamera(
cameraId.value,
cameraState,
threads.camera2Handler
)
}
}
// Suspend until we are no longer in a "starting" state.
val result = cameraState.state.first {
it !is CameraStateUnopened
}
if (result is CameraStateOpen) {
return ActiveCamera(
cameraState,
scope,
requestQueue
)
}
} catch (e: Throwable) {
exception = e
Log.warn(e) { "CameraId ${cameraId.value}: Failed to open" }
}
// TODO: Add logic to optimize retry handling for various error codes and exceptions.
// var errorCode: Int? = null
// if (lastResult is CameraClosed) {
// errorCode = lastResult.camera2ErrorCode
// }
//
// if (lastException != null) {
// when (lastException) {
// is CameraAccessException -> retry = true
// is IllegalArgumentException -> retry = true
// is SecurityException -> {
// if ()
// }
// }
// }
if (attempts > 3) {
if (exception != null) {
cameraState.closeWith(exception)
} else {
cameraState.close()
}
}
// Listen to availability - if we are notified that the cameraId is available then
// retry immediately.
awaitAvailableCameraId(cameraId, timeoutMillis = 500)
}
}
/**
* Wait for the specified duration, or until the availability callback is invoked.
*/
private suspend fun awaitAvailableCameraId(
cameraId: CameraId,
timeoutMillis: Long = 200
): Boolean {
val manager = cameraManager.get()
val cameraAvailableEvent = CompletableDeferred<Boolean>()
val availabilityCallback = object : CameraManager.AvailabilityCallback() {
override fun onCameraAvailable(cameraIdString: String) {
if (cameraIdString == cameraId.value) {
Log.debug { "$cameraId is now available. Retry." }
cameraAvailableEvent.complete(true)
}
}
override fun onCameraAccessPrioritiesChanged() {
Log.debug { "Access priorities changed. Retry." }
cameraAvailableEvent.complete(true)
}
}
// WARNING: Only one registerAvailabilityCallback can be set at a time.
// TODO: Turn this into a broadcast service so that multiple listeners can be registered if
// needed.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
Api28Compat.registerAvailabilityCallback(
manager,
threads.camera2Executor,
availabilityCallback
)
} else {
manager.registerAvailabilityCallback(availabilityCallback, threads.camera2Handler)
}
// Suspend until timeout fires or until availability callback fires.
val available = withTimeoutOrNull(timeoutMillis) {
cameraAvailableEvent.await()
} ?: false
manager.unregisterAvailabilityCallback(availabilityCallback)
if (!available) {
Log.info { "$cameraId was not available after $timeoutMillis ms!" }
}
return available
}
internal class ActiveCamera(
private val androidCameraState: AndroidCameraState,
scope: CoroutineScope,
channel: SendChannel<CameraRequest>
) {
val cameraId: CameraId
get() = androidCameraState.cameraId
private val listenerJob: Job
private var current: VirtualCameraState? = null
private val wakelock = WakeLock(
scope,
timeout = 1000,
callback = {
channel.trySend(RequestClose(this)).isSuccess
}
)
init {
listenerJob = scope.launch {
androidCameraState.state.collect {
if (it is CameraStateClosing || it is CameraStateClosed) {
wakelock.release()
this.cancel()
}
}
}
}
suspend fun connectTo(virtualCameraState: VirtualCameraState) {
val token = wakelock.acquire()
val previous = current
current = virtualCameraState
previous?.disconnect()
virtualCameraState.connect(androidCameraState.state, token)
}
fun close() {
wakelock.release()
androidCameraState.close()
}
suspend fun awaitClosed() {
androidCameraState.awaitClosed()
}
}
}