Posted by Scott Nien – Software Engineer (scottnien@)
Get ready to level up your Android camera apps! CameraX 1.4.0 just dropped with a load of awesome new features and improvements. We’re talking expanded HDR capabilities, preview stabilization and the versatile effect framework, and a whole lot of cool stuff to explore. We will also explore how to seamlessly integrate CameraX with Jetpack Compose! Let’s dive in and see how these enhancements can take your camera app to the next level.
HDR preview and Ultra HDR
High Dynamic Range (HDR) is a game-changer for photography, capturing a wider range of light and detail to create stunningly realistic images. With CameraX 1.3.0, we brought you HDR video recording capabilities, and now in 1.4.0, we’re taking it even further! Get ready for HDR Preview and Ultra HDR. These exciting additions empower you to deliver an even richer visual experience to your users.
HDR Preview
This new feature allows you to enable HDR on Preview without needing to bind a VideoCapture use case. This is especially useful for apps that use a single preview stream for both showing preview on display and video recording with an OpenGL pipeline.
To fully enable the HDR, you need to ensure your OpenGL pipeline is capable of processing the specific dynamic range format and then check the camera capability.
See following code snippet as an example to enable HLG10 which is the baseline HDR standard that device makers must support on cameras with 10-bit output.
// Declare your OpenGL pipeline supported dynamic range format. val openGLPipelineSupportedDynamicRange = setOf( DynamicRange.SDR, DynamicRange.HLG_10_BIT ) // Check camera dynamic range capabilities. val isHlg10Supported = cameraProvider.getCameraInfo(cameraSelector) .querySupportedDynamicRanges(openGLPipelineSupportedDynamicRange) .contains(DynamicRange.HLG_10_BIT) val preview = Preview.Builder().apply { if (isHlg10Supported) { setDynamicRange(DynamicRange.HLG_10_BIT) } }
Ultra HDR
Introducing Ultra HDR, a new format in Android 14 that lets users capture stunningly realistic photos with incredible dynamic range. And the best part? CameraX 1.4.0 makes it incredibly easy to add Ultra HDR capture to your app with just a few lines of code:
val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA val cameraInfo = cameraProvider.getCameraInfo(cameraSelector) val isUltraHdrSupported = ImageCapture.getImageCaptureCapabilities(cameraInfo) .supportedOutputFormats .contains(ImageCapture.OUTPUT_FORMAT_JPEG_ULTRA_HDR) val imageCapture = ImageCapture.Builder().apply { if (isUltraHdrSupported) { setOutputFormat(ImageCapture.OUTPUT_FORMAT_JPEG_ULTRA_HDR) } }.build()
Jetpack Compose support
While this post focuses on 1.4.0, we’re excited to announce the Jetpack Compose support in CameraX 1.5.0 alpha. We’re adding support for a Composable Viewfinder built on top of AndroidExternalSurface and AndroidEmbeddedExternalSurface. The CameraXViewfinder Composable hooks up a display surface to a CameraX Preview use case, handling the complexities of rotation, scaling and Surface lifecycle so you don’t need to.
// in build.gradle implementation ("androidx.camera:camera-compose:1.5.0-alpha03") class PreviewViewModel : ViewModel() { private val _surfaceRequests = MutableStateFlow(null) val surfaceRequests: StateFlow get() = _surfaceRequests.asStateFlow() private fun produceSurfaceRequests(previewUseCase: Preview) { // Always publish new SurfaceRequests from Preview previewUseCase.setSurfaceProvider { newSurfaceRequest -> _surfaceRequests.value = newSurfaceRequest } } // ... } @Composable fun MyCameraViewfinder( viewModel: PreviewViewModel, modifier: Modifier = Modifier ) { val currentSurfaceRequest: SurfaceRequest? by viewModel.surfaceRequests.collectAsState() currentSurfaceRequest?.let { surfaceRequest -> CameraXViewfinder( surfaceRequest = surfaceRequest, implementationMode = ImplementationMode.EXTERNAL, // Or EMBEDDED modifier = modifier ) } }
Kotlin-friendly APIs
CameraX is getting even more Kotlin-friendly! In 1.4.0, we’ve introduced two new suspend functions to streamline camera initialization and image capture.
// CameraX initialization val cameraProvider = ProcessCameraProvider.awaitInstance() val imageProxy = imageCapture.takePicture() // Processing imageProxy imageProxy.close()
Preview Stabilization and Mirror mode
Preview Stabilization
Preview stabilization mode was added in Android 13 to enable the stabilization on all non-RAW streams, including previews and MediaCodec input surfaces. Compared to the previous video stabilization mode, which may have inconsistent FoV (Field of View) between the preview and recorded video, this new preview stabilization mode ensures consistency and thus provides a better user experience. For apps that record the preview directly for video recording, this mode is also the only way to enable stabilization.
Follow the code below to enable preview stabilization. Please note that once preview stabilization is turned on, it is not only applied to the Preview but also to the VideoCapture if it is bound as well.
val isPreviewStabilizationSupported = Preview.getPreviewCapabilities(cameraProvider.getCameraInfo(cameraSelector)) .isStabilizationSupported val preview = Preview.Builder().apply { if (isPreviewStabilizationSupported) { setPreviewStabilizationEnabled(true) } }.build()
MirrorMode
While CameraX 1.3.0 introduced mirror mode for VideoCapture, we’ve now brought this handy feature to Preview in 1.4.0. This is especially useful for devices with outer displays, allowing you to create a more natural selfie experience when using the rear camera.
To enable the mirror mode, simply call Preview.Builder.setMirrorMode APIs. This feature is supported for Android 13 and above.
Real-time Effect
CameraX 1.3.0 introduced the CameraEffect framework, giving you the power to customize your camera output with OpenGL. Now, in 1.4.0, we’re taking it a step further. In addition to applying your own custom effects, you can now leverage a set of pre-built effects provided by CameraX and Media3, making it easier than ever to enhance your app’s camera features.
Overlay Effect
The new camera-effects artifact aims to provide ready-to-use effect implementations, starting with the OverlayEffect. This effect lets you draw overlays on top of camera frames using the familiar Canvas API.
The following sample code shows how to detect the QR code and draw the shape of the QR code once it is detected.
By default, drawing is performed in surface frame coordinates. But what if you need to use camera sensor coordinates? No problem! OverlayEffect provides the Frame#getSensorToBufferTransform function, allowing you to apply the necessary transformation matrix to your overlayCanvas.
In this example, we use CameraX’s MLKit Vision APIs (MlKitAnalyzer) and specify COORDINATE_SYSTEM_SENSOR to obtain QR code corner points in sensor coordinates. This ensures accurate overlay placement regardless of device orientation or screen aspect ratio.
// in build.gradle implementation ("androidx.camera:camera-effects:1.4.1}") implementation ("androidx.camera:camera-mlkit-vision:1.4.1") var qrcodePoints: Array? = null var qrcodeTimestamp = 0L val qrcodeBoxEffect = OverlayEffect( PREVIEW /* applied on the preview only */, 5, /* hold multiple frames in the queue so we can match analysis result with preview frame */, Handler(Looper.getMainLooper()), {} ) fun initCamera() { qrcodeBoxEffect.setOnDrawListener { frame -> if(frame.timestamp != qrcodeTimestamp) { // Do not change the drawing if the frame doesn’t match the analysis // result. return@setOnDrawListener true } frame.overlayCanvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR) qrcodePoints?.let { // Using sensor coordinates to draw. frame.overlayCanvas.setMatrix(frame.sensorToBufferTransform) val path = android.graphics.Path().apply { it.forEachIndexed { index, point -> if (index == 0) { moveTo(point.x.toFloat(), point.y.toFloat()) } else { lineTo(point.x.toFloat(), point.y.toFloat()) } } lineTo(it[0].x.toFloat(), it[0].y.toFloat()) } frame.overlayCanvas.drawPath(path, paint) } true } val imageAnalysis = ImageAnalysis.Builder() .build() .apply { setAnalyzer(executor, MlKitAnalyzer( listOf(barcodeScanner!!), COORDINATE_SYSTEM_SENSOR, executor ) { result -> val barcodes = result.getValue(barcodeScanner!!) qrcodePoints = barcodes?.takeIf { it.size > 0}?.get(0)?.cornerPoints // track the timestamp of the analysis result and release the // preview frame. qrcodeTimestamp = result.timestamp qrcodeBoxEffect.drawFrameAsync(qrcodeTimestamp) } ) } val useCaseGroup = UseCaseGroup.Builder() .addUseCase(preview) .addUseCase(imageAnalysis) .addEffect(qrcodeBoxEffect) .build() cameraProvider.bindToLifecycle( lifecycleOwner, cameraSelector, usecaseGroup) }
Here is what the effect looks like:
Screen Flash
Taking selfies in low light just got easier with CameraX 1.4.0! This release introduces a powerful new feature: screen flash. Instead of relying on a traditional LED flash which most selfie cameras don’t have, screen flash cleverly utilizes your phone’s display. By momentarily turning the screen bright white, it provides a burst of illumination that helps capture clear and vibrant selfies even in challenging lighting conditions.
Integrating screen flash into your CameraX app is flexible and straightforward. You have two main options:
1. Implement the ScreenFlash interface: This gives you full control over the screen flash behavior. You can customize the color, intensity, duration, and any other aspect of the flash. This is ideal if you need a highly tailored solution.
2. Use the built-in implementation: For a quick and easy solution, leverage the pre-built screen flash functionality in ScreenFlashView or PreviewView. This implementation handles all the heavy lifting for you.
If you’re already using PreviewView in your app, enabling screen flash is incredibly simple. Just enable it directly on the PreviewView instance. If you need more control or aren’t using PreviewView, you can use ScreenFlashView directly.
Here’s a code example demonstrating how to enable screen flash:
// case 1: PreviewView + CameraX core API. previewView.setScreenFlashWindow(activity.getWindow()); imageCapture.screenFlash = previewView.screenFlash imageCapture.setFlashMode(ImageCapture.FLASH_MODE_SCREEN) // case 2: PreviewView + CameraController previewView.setScreenFlashWindow(activity.getWindow()); cameraController.setImageCaptureFlashMode(ImageCapture.FLASH_MODE_SCREEN); // case 3 : use ScreenFlashView screenFlashView.setScreenFlashWindow(activity.getWindow()); imageCapture.setScreenFlash(screenFlashView.getScreenFlash()); imageCapture.setFlashMode(ImageCapture.FLASH_MODE_SCREEN);
Camera Extensions new features
Camera Extensions APIs aim to help apps to access the cutting-edge capabilities previously available only on built-in camera apps. And the ecosystem is growing rapidly! In 2024, we’ve seen major players like Pixel, Samsung, Xiaomi, Oppo, OnePlus, Vivo, and Honor all embrace Camera Extensions, particularly for Night Mode and Bokeh Mode. CameraX 1.4.0 takes this even further by adding support for brand-new Android 15 Camera Extensions features, including:
- Postview: Provides a preview of the captured image almost instantly before the long-exposure shots are completed
- Capture Process Progress: Displays a progress indicator so users know how long capturing and processing will take, improving the experience for features like Night Mode
- Extensions Strength: Allows users to fine-tune the intensity of the applied effect
Below is an example of the improved UX that uses postview and capture process progress features on Samsung S24 Ultra.
Interested to know how this can be implemented? See the sample code below:
val extensionsCameraSelector = extensionsManager .getExtensionEnabledCameraSelector(DEFAULT_BACK_CAMERA, extensionMode) val isPostviewSupported = ImageCapture.getImageCaptureCapabilities( cameraProvider.getCameraInfo(extensionsCameraSelector) ).isPostviewSupported val imageCapture = ImageCapture.Builder().apply { setPostviewEnabled(isPostviewSupported) }.build() imageCapture.takePicture(outputfileOptions, executor, object : OnImageSavedCallback { override fun onImageSaved(outputFileResults: OutputFileResults) { // final image saved. } override fun onPostviewBitmapAvailable(bitmap: Bitmap) { // Postview bitmap is available. } override fun onCaptureProcessProgressed(progress: Int) { // capture process progress update } }
Important: If your app ran into the CameraX Extensions issue on Pixel 9 series devices, please use CameraX 1.4.1 instead. This release fixes a critical issue that prevented Night Mode from working correctly with takePicture.
What’s Next
We hope you enjoy this new release. Our mission is to make camera development a joy, removing the friction and pain points so you can focus on innovation. With CameraX, you can easily harness the power of Android’s camera capabilities and build truly amazing app experiences.
Have questions or want to connect with the CameraX team? Join the CameraX developer discussion group or file a bug report:
We can’t wait to see what you create!