androidengineers.Book a session

Media & Graphics

CameraX: Use Cases & Pipelines

article25 minHard

CameraX is Jetpack's camera API that abstracts camera2's complexity into lifecycle-aware use cases. You compose use cases — Preview, ImageCapture, ImageAnalysis, VideoCapture — and bind them to a lifecycle.

Core Concepts

  • Use case: a unit of camera work (Preview, ImageCapture, ImageAnalysis, VideoCapture)
  • CameraSelector: chooses which physical camera (LENS_FACING_BACK, LENS_FACING_FRONT)
  • ProcessCameraProvider: manages use case lifecycle and camera resource
  • bindToLifecycle(): ties camera to a LifecycleOwner — camera opens/closes automatically

Setup

// implementation("androidx.camera:camera-core:1.3.2")
// implementation("androidx.camera:camera-camera2:1.3.2")
// implementation("androidx.camera:camera-lifecycle:1.3.2")
// implementation("androidx.camera:camera-view:1.3.2")
// implementation("androidx.camera:camera-video:1.3.2")
// implementation("androidx.camera:camera-mlkit-vision:1.3.2")

Preview Use Case

class CameraFragment : Fragment() {

    private val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
    private lateinit var camera: Camera

    fun startCamera(previewView: PreviewView) {
        val cameraProviderFuture = ProcessCameraProvider.getInstance(requireContext())

        cameraProviderFuture.addListener({
            val cameraProvider = cameraProviderFuture.get()

            val preview = Preview.Builder()
                .setTargetAspectRatio(AspectRatio.RATIO_16_9)
                .build()
                .also { it.setSurfaceProvider(previewView.surfaceProvider) }

            cameraProvider.unbindAll()
            camera = cameraProvider.bindToLifecycle(
                viewLifecycleOwner,  // ← lifecycle-aware: camera closes when fragment stops
                cameraSelector,
                preview
            )
        }, ContextCompat.getMainExecutor(requireContext()))
    }
}

ImageCapture Use Case

val imageCapture = ImageCapture.Builder()
    .setCaptureMode(ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY)  // or MAXIMIZE_QUALITY
    .setTargetRotation(Surface.ROTATION_0)
    .build()

// Bind alongside preview
cameraProvider.bindToLifecycle(viewLifecycleOwner, cameraSelector, preview, imageCapture)

// Take photo
fun takePhoto(outputDir: File) {
    val photoFile = File(outputDir, "${System.currentTimeMillis()}.jpg")
    val outputOptions = ImageCapture.OutputFileOptions.Builder(photoFile).build()

    imageCapture.takePicture(
        outputOptions,
        ContextCompat.getMainExecutor(requireContext()),
        object : ImageCapture.OnImageSavedCallback {
            override fun onImageSaved(output: ImageCapture.OutputFileResults) {
                val savedUri = output.savedUri ?: Uri.fromFile(photoFile)
                onPhotoCaptured(savedUri)
            }

            override fun onError(exception: ImageCaptureException) {
                Timber.e(exception, "Photo capture failed")
            }
        }
    )
}

ImageAnalysis Use Case (ML / QR Scanning)

val imageAnalysis = ImageAnalysis.Builder()
    .setTargetResolution(Size(1280, 720))
    .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)  // drop frames if analyzer is slow
    .build()

imageAnalysis.setAnalyzer(
    Executors.newSingleThreadExecutor(),
    QrCodeAnalyzer { qrCode ->
        // Called on executor thread — post to main for UI
        mainHandler.post { onQrCodeDetected(qrCode) }
    }
)

class QrCodeAnalyzer(private val onDetected: (String) -> Unit) : ImageAnalysis.Analyzer {
    private val scanner = BarcodeScanning.getClient()

    @androidx.camera.core.ExperimentalGetImage
    override fun analyze(imageProxy: ImageProxy) {
        val mediaImage = imageProxy.image ?: return imageProxy.close()
        val image = InputImage.fromMediaImage(mediaImage, imageProxy.imageInfo.rotationDegrees)

        scanner.process(image)
            .addOnSuccessListener { barcodes ->
                barcodes.firstOrNull()?.rawValue?.let(onDetected)
            }
            .addOnCompleteListener { imageProxy.close() }  // always close!
    }
}

VideoCapture Use Case

val recorder = Recorder.Builder()
    .setQualitySelector(QualitySelector.from(Quality.HIGHEST))
    .build()

val videoCapture = VideoCapture.withOutput(recorder)

// Bind
cameraProvider.bindToLifecycle(viewLifecycleOwner, cameraSelector, preview, videoCapture)

// Record
lateinit var recording: Recording

fun startRecording(outputFile: File) {
    val outputOptions = FileOutputOptions.Builder(outputFile).build()

    recording = videoCapture.output
        .prepareRecording(requireContext(), outputOptions)
        .withAudioEnabled()  // requires RECORD_AUDIO permission
        .start(ContextCompat.getMainExecutor(requireContext())) { event ->
            when (event) {
                is VideoRecordEvent.Finalize -> onRecordingFinalized(event)
                is VideoRecordEvent.Status -> updateRecordingDuration(event.recordingStats.recordedDurationNanos)
            }
        }
}

fun stopRecording() = recording.stop()

Use Case Limits

Not all combinations are supported simultaneously on all devices:

CombinationSupport
Preview + ImageCaptureGuaranteed
Preview + ImageAnalysisGuaranteed
Preview + VideoCaptureMost devices
Preview + ImageCapture + ImageAnalysisLimited — check isUseCaseCombinationSupported()

Key Takeaways

Use caseWhen to use
PreviewShow live viewfinder
ImageCaptureTake still photos; low-latency or high-quality mode
ImageAnalysisReal-time ML, QR scanning; use STRATEGY_KEEP_ONLY_LATEST
VideoCaptureRecord video; always close Recording when done
bindToLifecycleAlways bind use cases here; camera opens/closes automatically

YOUR LEARNING JOURNEY

0 of 177 available lessons completed

Progress saved in this browser. No account needed.
CameraX: Use Cases & Pipelines | Android System Design | Android Engineers