Only bind camera use cases that the device supports.

The new camera implementation always bound all four CameraX use cases
(preview, image capture, video capture, and image analysis) regardless
of device capabilities. On devices with LEGACY camera hardware level,
this causes image capture to fail with "Capture request failed with
reason ERROR" because the hardware cannot handle that many simultaneous
use cases.

This change makes video capture and QR scanning use case binding
conditional based on CameraXModePolicy, which already determines device
capabilities. Video capture is only bound when the device supports mixed
mode (image + video simultaneously). QR scanning analysis is only bound
when explicitly requested.
This commit is contained in:
Greyson Parrelli
2026-02-25 15:29:32 +00:00
committed by Cody Henthorne
parent a2057e20d2
commit c37bb96aab
4 changed files with 46 additions and 26 deletions

View File

@@ -138,6 +138,7 @@ class CameraXFragment : ComposeFragment(), CameraFragment {
controller = controller, controller = controller,
isVideoEnabled = isVideoEnabled && Build.VERSION.SDK_INT >= 26, isVideoEnabled = isVideoEnabled && Build.VERSION.SDK_INT >= 26,
isQrScanEnabled = isQrScanEnabled, isQrScanEnabled = isQrScanEnabled,
isVideoCaptureBindingEnabled = cameraXModePolicy is CameraXModePolicy.Mixed,
controlsVisible = controlsVisible.value, controlsVisible = controlsVisible.value,
selectedMediaCount = selectedMediaCount.intValue, selectedMediaCount = selectedMediaCount.intValue,
onCheckPermissions = { checkPermissions(isVideoEnabled) }, onCheckPermissions = { checkPermissions(isVideoEnabled) },
@@ -298,6 +299,7 @@ private fun CameraXScreen(
controller: CameraFragment.Controller?, controller: CameraFragment.Controller?,
isVideoEnabled: Boolean, isVideoEnabled: Boolean,
isQrScanEnabled: Boolean, isQrScanEnabled: Boolean,
isVideoCaptureBindingEnabled: Boolean,
controlsVisible: Boolean, controlsVisible: Boolean,
selectedMediaCount: Int, selectedMediaCount: Int,
onCheckPermissions: () -> Unit, onCheckPermissions: () -> Unit,
@@ -404,6 +406,8 @@ private fun CameraXScreen(
emitter = { event -> cameraViewModel.onEvent(event) }, emitter = { event -> cameraViewModel.onEvent(event) },
roundCorners = cameraDisplay.roundViewFinderCorners, roundCorners = cameraDisplay.roundViewFinderCorners,
contentAlignment = cameraAlignment, contentAlignment = cameraAlignment,
enableVideoCapture = isVideoCaptureBindingEnabled,
enableQrScanning = isQrScanEnabled,
modifier = Modifier.padding(bottom = viewportBottomMargin) modifier = Modifier.padding(bottom = viewportBottomMargin)
) { ) {
AnimatedVisibility( AnimatedVisibility(
@@ -611,6 +615,7 @@ private fun CameraXScreenPreview_20_9() {
controller = null, controller = null,
isVideoEnabled = true, isVideoEnabled = true,
isQrScanEnabled = false, isQrScanEnabled = false,
isVideoCaptureBindingEnabled = true,
controlsVisible = true, controlsVisible = true,
selectedMediaCount = 0, selectedMediaCount = 0,
onCheckPermissions = {}, onCheckPermissions = {},
@@ -638,6 +643,7 @@ private fun CameraXScreenPreview_19_9() {
controller = null, controller = null,
isVideoEnabled = true, isVideoEnabled = true,
isQrScanEnabled = false, isQrScanEnabled = false,
isVideoCaptureBindingEnabled = true,
controlsVisible = true, controlsVisible = true,
selectedMediaCount = 0, selectedMediaCount = 0,
onCheckPermissions = {}, onCheckPermissions = {},
@@ -665,6 +671,7 @@ private fun CameraXScreenPreview_18_9() {
controller = null, controller = null,
isVideoEnabled = true, isVideoEnabled = true,
isQrScanEnabled = false, isQrScanEnabled = false,
isVideoCaptureBindingEnabled = true,
controlsVisible = true, controlsVisible = true,
selectedMediaCount = 0, selectedMediaCount = 0,
onCheckPermissions = {}, onCheckPermissions = {},
@@ -692,6 +699,7 @@ private fun CameraXScreenPreview_16_9() {
controller = null, controller = null,
isVideoEnabled = true, isVideoEnabled = true,
isQrScanEnabled = false, isQrScanEnabled = false,
isVideoCaptureBindingEnabled = true,
controlsVisible = true, controlsVisible = true,
selectedMediaCount = 0, selectedMediaCount = 0,
onCheckPermissions = {}, onCheckPermissions = {},
@@ -719,6 +727,7 @@ private fun CameraXScreenPreview_6_5() {
controller = null, controller = null,
isVideoEnabled = true, isVideoEnabled = true,
isQrScanEnabled = false, isQrScanEnabled = false,
isVideoCaptureBindingEnabled = true,
controlsVisible = true, controlsVisible = true,
selectedMediaCount = 0, selectedMediaCount = 0,
onCheckPermissions = {}, onCheckPermissions = {},

View File

@@ -76,6 +76,8 @@ fun CameraScreen(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
roundCorners: Boolean = true, roundCorners: Boolean = true,
contentAlignment: Alignment = Alignment.Center, contentAlignment: Alignment = Alignment.Center,
enableVideoCapture: Boolean = true,
enableQrScanning: Boolean = false,
content: @Composable BoxScope.() -> Unit = {} content: @Composable BoxScope.() -> Unit = {}
) { ) {
val context = LocalContext.current val context = LocalContext.current
@@ -103,7 +105,9 @@ fun CameraScreen(
lifecycleOwner = lifecycleOwner, lifecycleOwner = lifecycleOwner,
cameraProvider = cameraProvider, cameraProvider = cameraProvider,
surfaceProvider = surfaceProvider, surfaceProvider = surfaceProvider,
context = context context = context,
enableVideoCapture = enableVideoCapture,
enableQrScanning = enableQrScanning
) )
) )
} }

View File

@@ -8,12 +8,14 @@ import androidx.lifecycle.LifecycleOwner
sealed interface CameraScreenEvents { sealed interface CameraScreenEvents {
/** Binds a camera to a sruface provider. */ /** Binds a camera to a surface provider. */
data class BindCamera( data class BindCamera(
val lifecycleOwner: LifecycleOwner, val lifecycleOwner: LifecycleOwner,
val cameraProvider: ProcessCameraProvider, val cameraProvider: ProcessCameraProvider,
val surfaceProvider: Preview.SurfaceProvider, val surfaceProvider: Preview.SurfaceProvider,
val context: Context val context: Context,
val enableVideoCapture: Boolean = true,
val enableQrScanning: Boolean = false
) : CameraScreenEvents ) : CameraScreenEvents
/** Focuses the camera on a point. */ /** Focuses the camera on a point. */

View File

@@ -344,27 +344,35 @@ class CameraScreenViewModel : ViewModel() {
.setResolutionSelector(resolutionSelector) .setResolutionSelector(resolutionSelector)
.build() .build()
// Video capture (16:9 is default for video) // Build the list of use cases based on device capabilities
val recorder = Recorder.Builder() val useCases = mutableListOf<androidx.camera.core.UseCase>(preview, imageCaptureUseCase)
.setAspectRatio(AspectRatio.RATIO_16_9)
.setQualitySelector(
androidx.camera.video.QualitySelector.from(
androidx.camera.video.Quality.HIGHEST,
androidx.camera.video.FallbackStrategy.higherQualityOrLowerThan(androidx.camera.video.Quality.HD)
)
)
.build()
val videoCaptureUseCase = VideoCapture.withOutput(recorder)
// Image analysis for QR code detection var videoCaptureUseCase: VideoCapture<Recorder>? = null
val imageAnalysisUseCase = ImageAnalysis.Builder() if (event.enableVideoCapture) {
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST) val recorder = Recorder.Builder()
.build() .setAspectRatio(AspectRatio.RATIO_16_9)
.also { .setQualitySelector(
it.setAnalyzer(imageAnalysisExecutor) { imageProxy -> androidx.camera.video.QualitySelector.from(
processImageForQrCode(imageProxy) androidx.camera.video.Quality.HIGHEST,
androidx.camera.video.FallbackStrategy.higherQualityOrLowerThan(androidx.camera.video.Quality.HD)
)
)
.build()
videoCaptureUseCase = VideoCapture.withOutput(recorder)
useCases += videoCaptureUseCase
}
if (event.enableQrScanning) {
val imageAnalysisUseCase = ImageAnalysis.Builder()
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
.build()
.also {
it.setAnalyzer(imageAnalysisExecutor) { imageProxy ->
processImageForQrCode(imageProxy)
}
} }
} useCases += imageAnalysisUseCase
}
// Select camera based on lensFacing // Select camera based on lensFacing
val cameraSelector = CameraSelector.Builder() val cameraSelector = CameraSelector.Builder()
@@ -379,10 +387,7 @@ class CameraScreenViewModel : ViewModel() {
camera = event.cameraProvider.bindToLifecycle( camera = event.cameraProvider.bindToLifecycle(
event.lifecycleOwner, event.lifecycleOwner,
cameraSelector, cameraSelector,
preview, *useCases.toTypedArray()
imageCaptureUseCase,
videoCaptureUseCase,
imageAnalysisUseCase
) )
lifecycleOwner = event.lifecycleOwner lifecycleOwner = event.lifecycleOwner