Automatically reduce camera use cases on error.

This commit is contained in:
Greyson Parrelli
2026-03-02 16:07:36 +00:00
committed by jeffrey-signal
parent 5140c41c58
commit 3c5774960a

View File

@@ -24,6 +24,7 @@ import androidx.camera.core.ImageCaptureException
import androidx.camera.core.ImageProxy import androidx.camera.core.ImageProxy
import androidx.camera.core.Preview import androidx.camera.core.Preview
import androidx.camera.core.SurfaceOrientedMeteringPointFactory import androidx.camera.core.SurfaceOrientedMeteringPointFactory
import androidx.camera.core.UseCase
import androidx.camera.core.resolutionselector.AspectRatioStrategy import androidx.camera.core.resolutionselector.AspectRatioStrategy
import androidx.camera.core.resolutionselector.ResolutionSelector import androidx.camera.core.resolutionselector.ResolutionSelector
import androidx.camera.lifecycle.ProcessCameraProvider import androidx.camera.lifecycle.ProcessCameraProvider
@@ -328,6 +329,47 @@ class CameraScreenViewModel : ViewModel() {
state: CameraScreenState, state: CameraScreenState,
event: CameraScreenEvents.BindCamera event: CameraScreenEvents.BindCamera
) { ) {
val cameraSelector = CameraSelector.Builder()
.requireLensFacing(state.lensFacing)
.build()
// Build binding attempts with progressively fewer optional use cases.
// Some devices cannot support all use cases simultaneously, so we fall back
// by first dropping video capture, then QR scanning.
val bindingAttempts = buildBindingAttempts(event)
for ((index, attempt) in bindingAttempts.withIndex()) {
try {
event.cameraProvider.unbindAll()
camera = event.cameraProvider.bindToLifecycle(
event.lifecycleOwner,
cameraSelector,
*attempt.toTypedArray()
)
if (index > 0) {
Log.w(TAG, "Use case binding succeeded on fallback attempt ${index + 1} of ${bindingAttempts.size}")
}
lifecycleOwner = event.lifecycleOwner
cameraProvider = event.cameraProvider
imageCapture = attempt.imageCapture
videoCapture = attempt.videoCapture
setupOrientationListener(event.context)
return
} catch (e: Exception) {
Log.e(TAG, "Use case binding failed (attempt ${index + 1} of ${bindingAttempts.size})", e)
}
}
Log.e(TAG, "All use case binding attempts failed")
}
private fun buildBindingAttempts(
event: CameraScreenEvents.BindCamera
): List<BindingAttempt> {
val resolutionSelector = ResolutionSelector.Builder() val resolutionSelector = ResolutionSelector.Builder()
.setAspectRatioStrategy(AspectRatioStrategy.RATIO_16_9_FALLBACK_AUTO_STRATEGY) .setAspectRatioStrategy(AspectRatioStrategy.RATIO_16_9_FALLBACK_AUTO_STRATEGY)
.build() .build()
@@ -339,16 +381,12 @@ class CameraScreenViewModel : ViewModel() {
.also { it.surfaceProvider = event.surfaceProvider } .also { it.surfaceProvider = event.surfaceProvider }
// Image capture with 16:9 aspect ratio (optimized for speed) // Image capture with 16:9 aspect ratio (optimized for speed)
val imageCaptureUseCase = ImageCapture.Builder() val imageCapture = ImageCapture.Builder()
.setCaptureMode(ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY) .setCaptureMode(ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY)
.setResolutionSelector(resolutionSelector) .setResolutionSelector(resolutionSelector)
.build() .build()
// Build the list of use cases based on device capabilities val videoCapture: VideoCapture<Recorder>? = if (event.enableVideoCapture) {
val useCases = mutableListOf<androidx.camera.core.UseCase>(preview, imageCaptureUseCase)
var videoCaptureUseCase: VideoCapture<Recorder>? = null
if (event.enableVideoCapture) {
val recorder = Recorder.Builder() val recorder = Recorder.Builder()
.setAspectRatio(AspectRatio.RATIO_16_9) .setAspectRatio(AspectRatio.RATIO_16_9)
.setQualitySelector( .setQualitySelector(
@@ -358,12 +396,13 @@ class CameraScreenViewModel : ViewModel() {
) )
) )
.build() .build()
videoCaptureUseCase = VideoCapture.withOutput(recorder) VideoCapture.withOutput(recorder)
useCases += videoCaptureUseCase } else {
null
} }
if (event.enableQrScanning) { val qrAnalysis: ImageAnalysis? = if (event.enableQrScanning) {
val imageAnalysisUseCase = ImageAnalysis.Builder() ImageAnalysis.Builder()
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST) .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
.build() .build()
.also { .also {
@@ -371,33 +410,23 @@ class CameraScreenViewModel : ViewModel() {
processImageForQrCode(imageProxy) processImageForQrCode(imageProxy)
} }
} }
useCases += imageAnalysisUseCase } else {
null
} }
// Select camera based on lensFacing return buildList {
val cameraSelector = CameraSelector.Builder() // Attempt 1: All use cased
.requireLensFacing(state.lensFacing) add(BindingAttempt(preview = preview, imageCapture = imageCapture, videoCapture = videoCapture, imageAnalysis = qrAnalysis))
.build()
try { // Attempt 2: Drop video capture
// Unbind use cases before rebinding if (videoCapture != null) {
event.cameraProvider.unbindAll() add(BindingAttempt(preview = preview, imageCapture = imageCapture, videoCapture = null, imageAnalysis = qrAnalysis))
}
// Bind use cases to camera // Attempt 3: Drop QR scanning
camera = event.cameraProvider.bindToLifecycle( if (qrAnalysis != null) {
event.lifecycleOwner, add(BindingAttempt(preview = preview, imageCapture = imageCapture, videoCapture = null, imageAnalysis = null))
cameraSelector, }
*useCases.toTypedArray()
)
lifecycleOwner = event.lifecycleOwner
cameraProvider = event.cameraProvider
imageCapture = imageCaptureUseCase
videoCapture = videoCaptureUseCase
setupOrientationListener(event.context)
} catch (e: Exception) {
Log.e(TAG, "Use case binding failed", e)
} }
} }
@@ -682,4 +711,15 @@ class CameraScreenViewModel : ViewModel() {
} }
} }
} }
private data class BindingAttempt(
val preview: Preview,
val imageCapture: ImageCapture,
val videoCapture: VideoCapture<Recorder>?,
val imageAnalysis: ImageAnalysis?
) {
fun toTypedArray(): Array<UseCase> {
return listOfNotNull(preview, imageCapture, videoCapture, imageAnalysis).toTypedArray()
}
}
} }