Building video features in Android can be complex, requiring careful management of the camera, recording sessions, and user interface. CameraX, a Jetpack support library, simplifies this process by providing a robust and consistent API for camera-related tasks. While Jetpack Compose offers a modern approach to UI development, many projects still use XML layouts, either partially or entirely. This post demonstrates how to combine CameraX for video recording with an XML-based UI in your Android application.
Introduction to CameraX
CameraX is a Jetpack library designed to simplify camera development in Android. It addresses the inconsistencies and complexities of the older Camera
and Camera2
APIs by offering a unified, lifecycle-aware interface. With CameraX, developers can easily implement features such as preview, image capture, and video recording.
Why CameraX?
- Lifecycle Awareness: Integrates seamlessly with Android’s lifecycle, preventing memory leaks and crashes.
- Simplified API: Offers a straightforward API that abstracts away the complexities of camera hardware.
- Consistent Implementation: Provides consistent camera behavior across a wide range of devices.
- Extensibility: Supports extensions like HDR, night mode, and portrait mode.
Setting Up the Project
Before diving into the implementation, let’s set up the project with the necessary dependencies and permissions.
Step 1: Add Dependencies
In your build.gradle
file, include the following CameraX dependencies:
dependencies {
def camerax_version = "1.3.0"
implementation "androidx.camera:camera-camera2:${camerax_version}"
implementation "androidx.camera:camera-lifecycle:${camerax_version}"
implementation "androidx.camera:camera-viewfinder:${camerax_version}"
}
Sync the Gradle file to download and add the dependencies to your project.
Step 2: Add Permissions
In your AndroidManifest.xml
, add the necessary camera and audio permissions:
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-feature android:name="android.hardware.camera" />
<uses-feature android:name="android.hardware.camera.autofocus" />
Also, request these permissions at runtime to comply with Android’s permission model. Here’s a basic example in your MainActivity
:
import android.Manifest
import android.content.pm.PackageManager
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
private val REQUEST_CODE_PERMISSIONS = 10
private val REQUIRED_PERMISSIONS = arrayOf(
Manifest.permission.CAMERA,
Manifest.permission.RECORD_AUDIO,
Manifest.permission.WRITE_EXTERNAL_STORAGE,
Manifest.permission.READ_EXTERNAL_STORAGE
)
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
if (allPermissionsGranted()) {
startCamera() // Function to start camera
} else {
ActivityCompat.requestPermissions(
this, REQUIRED_PERMISSIONS, REQUEST_CODE_PERMISSIONS
)
}
}
private fun allPermissionsGranted() = REQUIRED_PERMISSIONS.all {
ContextCompat.checkSelfPermission(baseContext, it) == PackageManager.PERMISSION_GRANTED
}
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<String>,
grantResults: IntArray
) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
if (requestCode == REQUEST_CODE_PERMISSIONS) {
if (allPermissionsGranted()) {
startCamera()
} else {
// Handle the case where permissions are not granted
}
}
}
private fun startCamera() {
// Implement CameraX setup here
}
}
Building the XML UI
Create the layout file activity_main.xml
with a PreviewView
for displaying the camera feed and a button to start/stop recording:
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<androidx.camera.view.PreviewView
android:id="@+id/viewFinder"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toTopOf="@+id/captureButton"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
<Button
android:id="@+id/captureButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Start Recording"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:layout_marginBottom="16dp" />
</androidx.constraintlayout.widget.ConstraintLayout>
Integrating CameraX for Video Recording
Now, let’s integrate CameraX into your MainActivity
to handle video recording.
Step 1: Initialize CameraX
Initialize CameraX in the startCamera
function:
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.camera.video.*
import androidx.core.content.ContextCompat
import java.io.File
import java.text.SimpleDateFormat
import java.util.*
import android.widget.Toast
import androidx.camera.view.PreviewView
private lateinit var previewView: PreviewView
private lateinit var captureButton: Button
private var videoCapture: VideoCapture<Recorder>? = null
private var recording: Recording? = null
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
previewView = findViewById(R.id.viewFinder)
captureButton = findViewById(R.id.captureButton)
if (allPermissionsGranted()) {
startCamera()
} else {
ActivityCompat.requestPermissions(
this, REQUIRED_PERMISSIONS, REQUEST_CODE_PERMISSIONS
)
}
captureButton.setOnClickListener {
captureVideo()
}
}
private fun startCamera() {
val cameraProviderFuture = ProcessCameraProvider.getInstance(this)
cameraProviderFuture.addListener({
val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get()
val preview = androidx.camera.core.Preview.Builder()
.build()
.also {
it.setSurfaceProvider(previewView.surfaceProvider)
}
val recorder = Recorder.Builder()
.setQualitySelector(QualitySelector.from(Quality.HIGHEST))
.build()
videoCapture = VideoCapture.withOutput(recorder)
try {
cameraProvider.unbindAll()
cameraProvider.bindToLifecycle(
this,
cameraSelector,
preview,
videoCapture
)
} catch (exc: Exception) {
// Handle camera binding failure
}
}, ContextCompat.getMainExecutor(this))
}
private fun captureVideo() {
val videoCapture = this.videoCapture ?: return
captureButton.isEnabled = false
val curRecording = recording
if (curRecording != null) {
curRecording.stop()
recording = null
return
}
val name = SimpleDateFormat(FILENAME_FORMAT, Locale.US)
.format(System.currentTimeMillis())
val contentValues = ContentValues().apply {
put(MediaStore.MediaColumns.DISPLAY_NAME, name)
put(MediaStore.MediaColumns.MIME_TYPE, "video/mp4")
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.P) {
put(MediaStore.Video.Media.RELATIVE_PATH, "Movies/CameraX-Video")
}
}
val mediaStoreOutputOptions = MediaStoreOutputOptions
.Builder(contentResolver, MediaStore.Video.Media.EXTERNAL_CONTENT_URI)
.setContentValues(contentValues)
.build()
recording = videoCapture.output
.prepareRecording(this, mediaStoreOutputOptions)
.apply {
if (ActivityCompat.checkSelfPermission(
this@MainActivity,
Manifest.permission.RECORD_AUDIO
) != PackageManager.PERMISSION_GRANTED
) {
return
}
}
.start(ContextCompat.getMainExecutor(this)) { recordEvent ->
when (recordEvent) {
is VideoRecordEvent.Start -> {
captureButton.apply {
text = "Stop Recording"
isEnabled = true
}
}
is VideoRecordEvent.Finalize -> {
if (!recordEvent.hasError()) {
val msg = "Video capture succeeded: " +
"${recordEvent.outputResults.outputUri}"
Toast.makeText(baseContext, msg, Toast.LENGTH_SHORT)
.show()
} else {
// Handle recording error
}
captureButton.apply {
text = "Start Recording"
isEnabled = true
}
}
}
}
}
private val cameraSelector: CameraSelector = CameraSelector.DEFAULT_BACK
private const val FILENAME_FORMAT = "yyyy-MM-dd-HH-mm-ss-SSS"
}
Step 2: Handle Video Recording
Implement the captureVideo
function to start and stop video recording:
In this setup, when the “Start Recording” button is pressed:
- The
prepareRecording
method initializes the video recording with specified audio and video configurations. - The
start
method begins the recording process asynchronously, providing a callback (Executor
) for status updates. - During the recording, UI updates like button state changes and informational toasts notify the user about the ongoing recording.
- When the “Stop Recording” button is pressed,
stop
is called, which finalizes the recording and saves the video file to the designated location.
Step 3: Update the onRequestPermissionsResult method
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<String>,
grantResults: IntArray
) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
if (requestCode == REQUEST_CODE_PERMISSIONS) {
if (allPermissionsGranted()) {
startCamera()
} else {
Toast.makeText(this,
"Permissions not granted by the user.",
Toast.LENGTH_SHORT).show()
finish()
}
}
}
private fun allPermissionsGranted() = REQUIRED_PERMISSIONS.all {
ContextCompat.checkSelfPermission(baseContext, it) == PackageManager.PERMISSION_GRANTED
}
private fun startCamera() {
val cameraProviderFuture = ProcessCameraProvider.getInstance(this)
cameraProviderFuture.addListener({
val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get()
val preview = Preview.Builder()
.build()
.also {
it.setSurfaceProvider(previewView.surfaceProvider)
}
val recorder = Recorder.Builder()
.setQualitySelector(QualitySelector.from(Quality.HIGHEST))
.build()
videoCapture = VideoCapture.withOutput(recorder)
try {
cameraProvider.unbindAll()
cameraProvider.bindToLifecycle(
this,
cameraSelector,
preview,
videoCapture
)
} catch (exc: Exception) {
Log.e(TAG, "Use case binding failed", exc)
}
}, ContextCompat.getMainExecutor(this))
}
private fun captureVideo() {
val videoCapture = this.videoCapture ?: return
captureButton.isEnabled = false
val curRecording = recording
if (curRecording != null) {
curRecording.stop()
recording = null
return
}
val name = SimpleDateFormat(FILENAME_FORMAT, Locale.US)
.format(System.currentTimeMillis())
val contentValues = ContentValues().apply {
put(MediaStore.MediaColumns.DISPLAY_NAME, name)
put(MediaStore.MediaColumns.MIME_TYPE, "video/mp4")
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.P) {
put(MediaStore.Video.Media.RELATIVE_PATH, "Movies/CameraX-Video")
}
}
val mediaStoreOutputOptions = MediaStoreOutputOptions
.Builder(contentResolver, MediaStore.Video.Media.EXTERNAL_CONTENT_URI)
.setContentValues(contentValues)
.build()
recording = videoCapture.output
.prepareRecording(this, mediaStoreOutputOptions)
.apply {
if (ActivityCompat.checkSelfPermission(
this@MainActivity,
Manifest.permission.RECORD_AUDIO
) != PackageManager.PERMISSION_GRANTED
) {
ActivityCompat.requestPermissions(
this@MainActivity,
arrayOf(Manifest.permission.RECORD_AUDIO),
REQUEST_CODE_PERMISSIONS
)
return
}
}
.start(ContextCompat.getMainExecutor(this)) { recordEvent ->
when (recordEvent) {
is VideoRecordEvent.Start -> {
captureButton.apply {
text = "Stop Recording"
isEnabled = true
}
}
is VideoRecordEvent.Finalize -> {
if (!recordEvent.hasError()) {
val msg = "Video capture succeeded: " +
"${recordEvent.outputResults.outputUri}"
Toast.makeText(baseContext, msg, Toast.LENGTH_SHORT)
.show()
} else {
val error = recordEvent.error
Log.e(TAG, "Video capture ends with error: $error")
Toast.makeText(baseContext, "Video capture ends with error: $error", Toast.LENGTH_SHORT)
.show()
}
captureButton.apply {
text = "Start Recording"
isEnabled = true
}
}
}
}
}
Conclusion
Integrating CameraX for video recording with an XML-based UI in Android simplifies the development process. By using CameraX, you can easily manage camera sessions, handle video recording, and ensure consistent behavior across various devices. While Jetpack Compose offers a modern UI approach, combining CameraX with XML layouts allows developers to leverage existing UI components while benefiting from CameraX’s robust camera functionalities.