1. Antes de comenzar
En este codelab, aprenderás a crear una app de cámara que use CameraX para mostrar un visor, tomar fotos, capturar videos y analizar un flujo de imágenes desde la cámara.
Para lograr esto, presentaremos el concepto de casos de uso en CameraX, que puedes usar para varias operaciones de cámara, desde mostrar un visor hasta capturar videos.
Requisitos previos
- Experiencia básica de desarrollo de Android
- Conocimientos de MediaStore (deseable, pero no obligatorio)
Actividades
- Obtén información para agregar las dependencias de CameraX.
- Aprende a mostrar la vista previa de la cámara en una actividad (caso de uso de Preview).
- Compila una app que pueda tomar fotos y guardarlas en el almacenamiento (caso de uso de ImageCapture).
- Aprende a analizar fotogramas de la cámara en tiempo real (caso de uso de ImageAnalysis).
- Descubre cómo capturar videos en MediaStore (caso de uso de VideoCapture).
Requisitos
- Un dispositivo Android o el emulador de Android Studio:
- Se recomienda Android 10 y versiones posteriores: el comportamiento de MediaStore depende de la disponibilidad de almacenamiento específico.
- Con Android Emulator**, te recomendamos que uses un dispositivo virtual de Android (AVD) basado en Android 11 o versiones posteriores**.
- Ten en cuenta que CameraX solo requiere que el nivel de API mínimo admitido sea el 21.
- Android Studio Arctic Fox 2020.3.1 o una versión posterior
- Conocimientos sobre Kotlin y Android ViewBinding
2. Crea el proyecto
- En Android Studio, crea un proyecto nuevo y selecciona Empty Activity cuando se te solicite.
- A continuación, asigna el nombre "CameraXApp" a la app y confirma o cambia el nombre del paquete a "
com.android.example.cameraxapp
". Elige Kotlin como lenguaje y establece el nivel mínimo de API en 21 (que es el mínimo requerido para CameraX). Para versiones anteriores de Android Studio, asegúrate de incluir compatibilidad con artefactos de AndroidX.
Agrega las dependencias de Gradle
- Abre el archivo
build.gradle
del móduloCameraXApp.app
y agrega las dependencias de CameraX:
dependencies {
def camerax_version = "1.1.0-beta01"
implementation "androidx.camera:camera-core:${camerax_version}"
implementation "androidx.camera:camera-camera2:${camerax_version}"
implementation "androidx.camera:camera-lifecycle:${camerax_version}"
implementation "androidx.camera:camera-video:${camerax_version}"
implementation "androidx.camera:camera-view:${camerax_version}"
implementation "androidx.camera:camera-extensions:${camerax_version}"
}
- CameraX necesita algunos métodos que forman parte de Java 8, por lo que debemos configurar nuestras opciones de compilación según corresponda. Al final del bloque
android
, justo después debuildTypes
, agrega lo siguiente:
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
- En este codelab, se usa ViewBinding, por lo que debes habilitarlo con lo siguiente (al final del bloque
android{}
):
buildFeatures {
viewBinding true
}
Cuando se te solicite, haz clic en Sync Now y estarás listo para usar CameraX en nuestra app.
Crea el diseño del codelab
En la IU de este codelab, usamos lo siguiente:
- Una PreviewView de CameraX (para obtener una vista previa de la imagen o el video de la cámara)
- Un botón estándar para controlar la captura de imágenes
- Un botón estándar para iniciar o detener la captura de video
- Una directriz vertical para posicionar los 2 botones
Reemplacemos el diseño predeterminado por este código:
- Abre el archivo de diseño
activity_main
enres/layout/activity_main.xml
y reemplázalo por el siguiente código:
<?xml version="1.0" encoding="utf-8"?>
<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="match_parent"
android:layout_height="match_parent" />
<Button
android:id="@+id/image_capture_button"
android:layout_width="110dp"
android:layout_height="110dp"
android:layout_marginBottom="50dp"
android:layout_marginEnd="50dp"
android:elevation="2dp"
android:text="@string/take_photo"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintEnd_toStartOf="@id/vertical_centerline" />
<Button
android:id="@+id/video_capture_button"
android:layout_width="110dp"
android:layout_height="110dp"
android:layout_marginBottom="50dp"
android:layout_marginStart="50dp"
android:elevation="2dp"
android:text="@string/start_capture"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="@id/vertical_centerline" />
<androidx.constraintlayout.widget.Guideline
android:id="@+id/vertical_centerline"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintGuide_percent=".50" />
</androidx.constraintlayout.widget.ConstraintLayout>
- Actualiza el archivo
res/values/strings.xml
con lo siguiente:
<resources>
<string name="app_name">CameraXApp</string>
<string name="take_photo">Take Photo</string>
<string name="start_capture">Start Capture</string>
<string name="stop_capture">Stop Capture</string>
</resources>
Configura MainActivity.kt
- Reemplaza el código de
MainActivity.kt
por el siguiente, pero no cambies el nombre del paquete. Este incluye instrucciones de importación, variables de las que crearemos instancias, funciones que implementaremos y constantes.
onCreate()
ya se implementó para que podamos verificar los permisos de la cámara, iniciarla, configurar el onClickListener()
de los botones de captura y foto, e implementar cameraExecutor
. Aunque ya se implementó onCreate()
, la cámara no funcionará hasta que implementemos los métodos del archivo.
package com.android.example.cameraxapp
import android.Manifest
import android.content.ContentValues
import android.content.pm.PackageManager
import android.os.Build
import android.os.Bundle
import android.provider.MediaStore
import androidx.appcompat.app.AppCompatActivity
import androidx.camera.core.ImageCapture
import androidx.camera.video.Recorder
import androidx.camera.video.Recording
import androidx.camera.video.VideoCapture
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import com.android.example.cameraxapp.databinding.ActivityMainBinding
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
import android.widget.Toast
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.camera.core.Preview
import androidx.camera.core.CameraSelector
import android.util.Log
import androidx.camera.core.ImageAnalysis
import androidx.camera.core.ImageCaptureException
import androidx.camera.core.ImageProxy
import androidx.camera.video.FallbackStrategy
import androidx.camera.video.MediaStoreOutputOptions
import androidx.camera.video.Quality
import androidx.camera.video.QualitySelector
import androidx.camera.video.VideoRecordEvent
import androidx.core.content.PermissionChecker
import java.nio.ByteBuffer
import java.text.SimpleDateFormat
import java.util.Locale
typealias LumaListener = (luma: Double) -> Unit
class MainActivity : AppCompatActivity() {
private lateinit var viewBinding: ActivityMainBinding
private var imageCapture: ImageCapture? = null
private var videoCapture: VideoCapture<Recorder>? = null
private var recording: Recording? = null
private lateinit var cameraExecutor: ExecutorService
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewBinding = ActivityMainBinding.inflate(layoutInflater)
setContentView(viewBinding.root)
// Request camera permissions
if (allPermissionsGranted()) {
startCamera()
} else {
ActivityCompat.requestPermissions(
this, REQUIRED_PERMISSIONS, REQUEST_CODE_PERMISSIONS)
}
// Set up the listeners for take photo and video capture buttons
viewBinding.imageCaptureButton.setOnClickListener { takePhoto() }
viewBinding.videoCaptureButton.setOnClickListener { captureVideo() }
cameraExecutor = Executors.newSingleThreadExecutor()
}
private fun takePhoto() {}
private fun captureVideo() {}
private fun startCamera() {}
private fun allPermissionsGranted() = REQUIRED_PERMISSIONS.all {
ContextCompat.checkSelfPermission(
baseContext, it) == PackageManager.PERMISSION_GRANTED
}
override fun onDestroy() {
super.onDestroy()
cameraExecutor.shutdown()
}
companion object {
private const val TAG = "CameraXApp"
private const val FILENAME_FORMAT = "yyyy-MM-dd-HH-mm-ss-SSS"
private const val REQUEST_CODE_PERMISSIONS = 10
private val REQUIRED_PERMISSIONS =
mutableListOf (
Manifest.permission.CAMERA,
Manifest.permission.RECORD_AUDIO
).apply {
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P) {
add(Manifest.permission.WRITE_EXTERNAL_STORAGE)
}
}.toTypedArray()
}
}
3. Solicita los permisos necesarios
Antes de que la app abra la cámara, necesita el permiso del usuario para hacerlo. También se necesita el permiso de acceso al micrófono para grabar audio. En Android 9 (P) y versiones anteriores, MediaStore necesita el permiso de escritura en almacenamiento externo. En este paso, implementaremos los permisos necesarios.
- Abre
AndroidManifest.xml
y agrega estas líneas antes de la etiquetaapplication
:
<uses-feature android:name="android.hardware.camera.any" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="28" />
Cuando agregas android.hardware.camera.any
, te aseguras de que el dispositivo tenga una cámara. Si especificas .any
, significa que puede tener una cámara frontal o una trasera.
- Copia este código en
MainActivity.kt.
Las viñetas que aparecen a continuación desglosarán el código que acabamos de copiar.
override fun onRequestPermissionsResult(
requestCode: Int, permissions: Array<String>, grantResults:
IntArray) {
if (requestCode == REQUEST_CODE_PERMISSIONS) {
if (allPermissionsGranted()) {
startCamera()
} else {
Toast.makeText(this,
"Permissions not granted by the user.",
Toast.LENGTH_SHORT).show()
finish()
}
}
}
- Verifica si el código de solicitud es correcto. De lo contrario, ignóralo.
if (requestCode == REQUEST_CODE_PERMISSIONS) {
}
- Si se otorgan los permisos, llama a
startCamera()
.
if (allPermissionsGranted()) {
startCamera()
}
- Si no se otorgan los permisos, presenta un aviso para informar al usuario que no se otorgaron los permisos.
else {
Toast.makeText(this,
"Permissions not granted by the user.",
Toast.LENGTH_SHORT).show()
finish()
}
- Ejecuta la app.
Ahora debería solicitar permiso para usar la cámara y el micrófono:
4. Implementa el caso de uso de Preview
En una aplicación de cámara, se usa el visor para permitirle al usuario obtener una vista previa de la foto que tomará. Implementaremos un visor con la clase Preview
de CameraX.
Para usar Preview
, primero debemos definir una configuración, que luego se usa para crear una instancia del caso de uso. La instancia resultante es la que vinculamos al ciclo de vida de CameraX.
- Copia este código en la función
startCamera()
.
Las viñetas que aparecen a continuación desglosarán el código que acabamos de copiar.
private fun startCamera() {
val cameraProviderFuture = ProcessCameraProvider.getInstance(this)
cameraProviderFuture.addListener({
// Used to bind the lifecycle of cameras to the lifecycle owner
val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get()
// Preview
val preview = Preview.Builder()
.build()
.also {
it.setSurfaceProvider(viewBinding.viewFinder.surfaceProvider)
}
// Select back camera as a default
val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
try {
// Unbind use cases before rebinding
cameraProvider.unbindAll()
// Bind use cases to camera
cameraProvider.bindToLifecycle(
this, cameraSelector, preview)
} catch(exc: Exception) {
Log.e(TAG, "Use case binding failed", exc)
}
}, ContextCompat.getMainExecutor(this))
}
- Crea una instancia de
ProcessCameraProvider
. Se usa para vincular el ciclo de vida de las cámaras al propietario del ciclo de vida. Esto elimina la tarea de abrir y cerrar la cámara, ya que CameraX se adapta al ciclo de vida.
val cameraProviderFuture = ProcessCameraProvider.getInstance(this)
- Agrega un objeto de escucha a
cameraProviderFuture
. Agrega un elementoRunnable
como primer argumento. Completaremos el resto más adelante. AgregaContextCompat
.getMainExecutor()
como segundo argumento. Se mostrará unExecutor
que se ejecutará en el subproceso principal.
cameraProviderFuture.addListener(Runnable {}, ContextCompat.getMainExecutor(this))
- En el
Runnable
, agrega unProcessCameraProvider
. Esto se usa para vincular el ciclo de vida de nuestra cámara alLifecycleOwner
dentro del proceso de la aplicación.
val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get()
- Inicializa nuestro objeto
Preview
, llama a su compilación, obtén un proveedor de plataforma desde el visor y, luego, configúralo en la vista previa.
val preview = Preview.Builder()
.build()
.also {
it.setSurfaceProvider(viewBinding.viewFinder.surfaceProvider)
}
- Crea un objeto
CameraSelector
y seleccionaDEFAULT_BACK_CAMERA
.
val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
- Crea un bloque
try
. Dentro de ese bloque, asegúrate de que no haya nada vinculado acameraProvider
y, luego, vincula nuestrocameraSelector
y el objeto de vista previa acameraProvider
.
try {
cameraProvider.unbindAll()
cameraProvider.bindToLifecycle(
this, cameraSelector, preview)
}
- Este código puede fallar de varias maneras, por ejemplo, si la app ya no está en primer plano. Une este código en un bloque
catch
para registrar si hay una falla.
catch(exc: Exception) {
Log.e(TAG, "Use case binding failed", exc)
}
- Ejecuta la app. Ahora obtenemos una vista previa de la cámara.
5. Implementa el caso de uso de ImageCapture
Otros casos de uso funcionan de manera muy similar a Preview
. Primero, definimos un objeto de configuración que se usa para crear una instancia del objeto de caso de uso real. Para capturar fotos, implementarás el método takePhoto()
, al que se llama cuando se presiona el botón Take photo.
- Copia este código en el método
takePhoto()
.
Las viñetas que aparecen a continuación desglosarán el código que acabamos de copiar.
private fun takePhoto() {
// Get a stable reference of the modifiable image capture use case
val imageCapture = imageCapture ?: return
// Create time stamped name and MediaStore entry.
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, "image/jpeg")
if(Build.VERSION.SDK_INT > Build.VERSION_CODES.P) {
put(MediaStore.Images.Media.RELATIVE_PATH, "Pictures/CameraX-Image")
}
}
// Create output options object which contains file + metadata
val outputOptions = ImageCapture.OutputFileOptions
.Builder(contentResolver,
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
contentValues)
.build()
// Set up image capture listener, which is triggered after photo has
// been taken
imageCapture.takePicture(
outputOptions,
ContextCompat.getMainExecutor(this),
object : ImageCapture.OnImageSavedCallback {
override fun onError(exc: ImageCaptureException) {
Log.e(TAG, "Photo capture failed: ${exc.message}", exc)
}
override fun
onImageSaved(output: ImageCapture.OutputFileResults){
val msg = "Photo capture succeeded: ${output.savedUri}"
Toast.makeText(baseContext, msg, Toast.LENGTH_SHORT).show()
Log.d(TAG, msg)
}
}
)
}
- Primero, obtén una referencia al caso de uso de
ImageCapture
. Si el caso de uso es nulo, sal de la función. Este valor será nulo si presionamos el botón de la foto antes de configurar la captura de imágenes. Sin la sentenciareturn
, la app fallará si tuviera un valornull
.
val imageCapture = imageCapture ?: return
- A continuación, crea un valor de contenido de MediaStore para contener la imagen. Usa una marca de tiempo de modo que el nombre visible en MediaStore sea único.
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, "image/jpeg")
if(Build.VERSION.SDK_INT > Build.VERSION_CODES.P) {
put(MediaStore.Images.Media.RELATIVE_PATH, "Pictures/CameraX-Image")
}
}
- Crea un objeto
OutputFileOptions
. En él es donde podemos especificar cosas acerca de cómo queremos que sea nuestro resultado. Queremos que los resultados se guarden en MediaStore para que otras apps puedan mostrarlos, así que agrega nuestra entrada de MediaStore.
val outputOptions = ImageCapture.OutputFileOptions
.Builder(contentResolver,
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
contentValues)
.build()
- Llama a
takePicture()
en el objetoimageCapture
. PasaoutputOptions
, el ejecutor y una devolución de llamada para cuando se guarde la imagen. Deberás completar la devolución de llamada a continuación.
imageCapture.takePicture(
outputOptions, ContextCompat.getMainExecutor(this),
object : ImageCapture.OnImageSavedCallback {}
)
- En caso de que falle la captura de imágenes o de que no se guarde, agrega un caso de error para registrar que falló.
override fun onError(exc: ImageCaptureException) {
Log.e(TAG, "Photo capture failed: ${exc.message}", exc)
}
- Si la captura no falla, la foto se tomó correctamente. Guarda la foto en el archivo que creamos antes, presenta un aviso para informar al usuario que se realizó correctamente e imprime una instrucción de registro.
override fun onImageSaved(output: ImageCapture.OutputFileResults) {
val savedUri = Uri.fromFile(photoFile)
val msg = "Photo capture succeeded: $savedUri"
Toast.makeText(baseContext, msg, Toast.LENGTH_SHORT).show()
Log.d(TAG, msg)
}
- Ve al método
startCamera()
y copia esto debajo del código para obtener la vista previa.
imageCapture = ImageCapture.Builder().build()
- Por último, actualiza la llamada a
bindToLifecycle()
en el bloquetry
de modo que incluya el nuevo caso de uso:
cameraProvider.bindToLifecycle(
this, cameraSelector, preview, imageCapture)
En este punto, el método se verá de la siguiente manera:
private fun startCamera() {
val cameraProviderFuture = ProcessCameraProvider.getInstance(this)
cameraProviderFuture.addListener({
// Used to bind the lifecycle of cameras to the lifecycle owner
val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get()
// Preview
val preview = Preview.Builder()
.build()
.also {
it.setSurfaceProvider(viewFinder.surfaceProvider)
}
imageCapture = ImageCapture.Builder()
.build()
// Select back camera as a default
val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
try {
// Unbind use cases before rebinding
cameraProvider.unbindAll()
// Bind use cases to camera
cameraProvider.bindToLifecycle(
this, cameraSelector, preview, imageCapture)
} catch(exc: Exception) {
Log.e(TAG, "Use case binding failed", exc)
}
}, ContextCompat.getMainExecutor(this))
}
- Vuelve a ejecutar la app y presiona Take Photo. Deberíamos ver un aviso en la pantalla y un mensaje en los registros.
Ve la foto
Ahora que las fotos recién tomadas se guardan en MediaStore, podemos usar cualquier aplicación de MediaStore para verlas. Por ejemplo, con la app de Google Fotos, puedes hacer lo siguiente:
- Inicia Google Fotos .
- Presiona "Biblioteca" (no es necesario si no accediste a la app de Fotos con tu cuenta) para ver los archivos multimedia ordenados. La carpeta
"CameraX-Image"
es nuestra.
- Toca el ícono de la imagen para ver la foto completa y presiona el botón Más en la esquina superior derecha para ver los detalles de la foto tomada.
Si solo buscamos compilar una app de cámara sencilla para tomar fotos, no necesitamos hacer nada más. Es así de sencillo. Si quieres implementar un analizador de imágenes, sigue leyendo.
6. Implementa el caso de uso de ImageAnalysis
Una excelente manera de hacer que nuestra app de cámara sea más interesante es usar la función ImageAnalysis
. Nos permite definir una clase personalizada que implementa la interfaz ImageAnalysis.Analyzer
y a la que se llamará con los fotogramas de cámara entrantes. Ya no tendremos que administrar el estado de la sesión de la cámara ni deshacernos de las imágenes. Basta con hacer la vinculación al ciclo de vida deseado de nuestra app, como sucede con otros componentes optimizados para ciclos de vida.
- Agrega este analizador como una clase interna en
MainActivity.kt
. El analizador registra la luminosidad promedio de la imagen. Para crear un analizador, anulamos la funciónanalyze
en una clase que implementa la interfazImageAnalysis.Analyzer
.
private class LuminosityAnalyzer(private val listener: LumaListener) : ImageAnalysis.Analyzer {
private fun ByteBuffer.toByteArray(): ByteArray {
rewind() // Rewind the buffer to zero
val data = ByteArray(remaining())
get(data) // Copy the buffer into a byte array
return data // Return the byte array
}
override fun analyze(image: ImageProxy) {
val buffer = image.planes[0].buffer
val data = buffer.toByteArray()
val pixels = data.map { it.toInt() and 0xFF }
val luma = pixels.average()
listener(luma)
image.close()
}
}
Con nuestra clase que implementa la interfaz ImageAnalysis.Analyzer
, lo único que debemos hacer es crear una instancia de LuminosityAnalyzer
en ImageAnalysis,
similar a otros casos de uso y actualizar la función startCamera()
una vez más, antes de llamar a CameraX.bindToLifecycle()
:
- En el método
startCamera()
, agrega esto debajo del código deimageCapture
.
val imageAnalyzer = ImageAnalysis.Builder()
.build()
.also {
it.setAnalyzer(cameraExecutor, LuminosityAnalyzer { luma ->
Log.d(TAG, "Average luminosity: $luma")
})
}
- Actualiza la llamada a
bindToLifecycle()
encameraProvider
para incluir elimageAnalyzer
.
cameraProvider.bindToLifecycle(
this, cameraSelector, preview, imageCapture, imageAnalyzer)
El método completo se verá de la siguiente manera:
private fun startCamera() {
val cameraProviderFuture = ProcessCameraProvider.getInstance(this)
cameraProviderFuture.addListener({
// Used to bind the lifecycle of cameras to the lifecycle owner
val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get()
// Preview
val preview = Preview.Builder()
.build()
.also {
it.setSurfaceProvider(viewBinding.viewFinder.surfaceProvider)
}
imageCapture = ImageCapture.Builder()
.build()
val imageAnalyzer = ImageAnalysis.Builder()
.build()
.also {
it.setAnalyzer(cameraExecutor, LuminosityAnalyzer { luma ->
Log.d(TAG, "Average luminosity: $luma")
})
}
// Select back camera as a default
val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
try {
// Unbind use cases before rebinding
cameraProvider.unbindAll()
// Bind use cases to camera
cameraProvider.bindToLifecycle(
this, cameraSelector, preview, imageCapture, imageAnalyzer)
} catch(exc: Exception) {
Log.e(TAG, "Use case binding failed", exc)
}
}, ContextCompat.getMainExecutor(this))
}
- Ejecuta la app ahora. Se generará un mensaje similar a este en logcat a cada segundo, aproximadamente.
D/CameraXApp: Average luminosity: ...
7. Implementa el caso de uso de VideoCapture
CameraX agregó el caso de uso de VideoCapture en la versión 1.1.0-alpha10 y, desde entonces, realiza más mejoras. Ten en cuenta que la API de VideoCapture
admite muchas funciones de captura de video, por lo que, para que resulte manejable, este codelab solo muestra la captura de video y audio a MediaStore
.
- Copia este código en el método
captureVideo()
: controla tanto el inicio como la detención de nuestro caso de uso deVideoCapture
. Las viñetas que aparecen a continuación desglosarán el código que acabamos de copiar.
// Implements VideoCapture use case, including start and stop capturing.
private fun captureVideo() {
val videoCapture = this.videoCapture ?: return
viewBinding.videoCaptureButton.isEnabled = false
val curRecording = recording
if (curRecording != null) {
// Stop the current recording session.
curRecording.stop()
recording = null
return
}
// create and start a new recording session
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 (PermissionChecker.checkSelfPermission(this@MainActivity,
Manifest.permission.RECORD_AUDIO) ==
PermissionChecker.PERMISSION_GRANTED)
{
withAudioEnabled()
}
}
.start(ContextCompat.getMainExecutor(this)) { recordEvent ->
when(recordEvent) {
is VideoRecordEvent.Start -> {
viewBinding.videoCaptureButton.apply {
text = getString(R.string.stop_capture)
isEnabled = true
}
}
is VideoRecordEvent.Finalize -> {
if (!recordEvent.hasError()) {
val msg = "Video capture succeeded: " +
"${recordEvent.outputResults.outputUri}"
Toast.makeText(baseContext, msg, Toast.LENGTH_SHORT)
.show()
Log.d(TAG, msg)
} else {
recording?.close()
recording = null
Log.e(TAG, "Video capture ends with error: " +
"${recordEvent.error}")
}
viewBinding.videoCaptureButton.apply {
text = getString(R.string.start_capture)
isEnabled = true
}
}
}
}
}
- Verifica si se creó el caso de uso de VideoCapture. De lo contrario, no hagas nada.
val videoCapture = videoCapture ?: return
- Inhabilita la IU hasta que CameraX complete la acción de solicitud. Esta se volverá a habilitar dentro de nuestro VideoRecordListener registrado en pasos posteriores.
viewBinding.videoCaptureButton.isEnabled = false
- Si hay una grabación activa en curso, detenla y libera la
recording
actual. Recibiremos una notificación cuando el archivo capturado de video esté listo para que lo use nuestra aplicación.
val curRecording = recording
if (curRecording != null) {
curRecording.stop()
recording = null
return
}
- Para comenzar a grabar, creamos una nueva sesión de grabación. Primero, creamos el objeto de contenido de video de MediaStore que queremos usar, con la marca de tiempo del sistema como nombre visible (para que podamos capturar varios videos).
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")
}
}
- Crea un
MediaStoreOutputOptions.Builder
con la opción de contenido externo.
val mediaStoreOutputOptions = MediaStoreOutputOptions
.Builder(contentResolver,
MediaStore.Video.Media.EXTERNAL_CONTENT_URI)
- Configura el video creado
contentValues
comoMediaStoreOutputOptions.Builder
y compila nuestra instancia deMediaStoreOutputOptions
.
.setContentValues(contentValues)
.build()
- Configura la opción de salida en el
Recorder
deVideoCapture<Recorder>
y habilita la grabación de audio:
videoCapture
.output
.prepareRecording(this, mediaStoreOutputOptions)
.withAudioEnabled()
- Habilita Audio en esta grabación.
.apply {
if (PermissionChecker.checkSelfPermission(this@MainActivity,
Manifest.permission.RECORD_AUDIO) ==
PermissionChecker.PERMISSION_GRANTED)
{
withAudioEnabled()
}
}
- Inicia esta nueva grabación y registra un objeto de escucha lambda
VideoRecordEvent
.
.start(ContextCompat.getMainExecutor(this)) { recordEvent ->
//lambda event listener
}
- Cuando el dispositivo de cámara inicie la grabación de solicitudes, activa o desactiva el texto del botón "Start Capture" para que diga "Stop Capture".
is VideoRecordEvent.Start -> {
viewBinding.videoCaptureButton.apply {
text = getString(R.string.stop_capture)
isEnabled = true
}
}
- Cuando finalice la grabación activa, notifica al usuario con un aviso, vuelve a pasar el botón "Stop Capture" a "Start Capture" y vuelve a habilitarlo:
is VideoRecordEvent.Finalize -> {
if (!recordEvent.hasError()) {
val msg = "Video capture succeeded: " +
"${recordEvent.outputResults.outputUri}"
Toast.makeText(baseContext, msg, Toast.LENGTH_SHORT)
.show()
Log.d(TAG, msg)
} else {
recording?.close()
recording = null
Log.e(TAG, "Video capture succeeded: " +
"${recordEvent.outputResults.outputUri}")
}
viewBinding.videoCaptureButton.apply {
text = getString(R.string.start_capture)
isEnabled = true
}
}
- En
startCamera()
, coloca el siguiente código después de la línea de creación depreview
. Esto creará el caso de uso deVideoCapture
.
val recorder = Recorder.Builder()
.setQualitySelector(QualitySelector.from(Quality.HIGHEST))
.build()
videoCapture = VideoCapture.withOutput(recorder)
- (Opcional) También dentro de
startCamera()
, inhabilita los casos de uso deimageCapture
yimageAnalyzer
borrando o marcando como comentario el siguiente código:
/* comment out ImageCapture and ImageAnalyzer use cases
imageCapture = ImageCapture.Builder().build()
val imageAnalyzer = ImageAnalysis.Builder()
.build()
.also {
it.setAnalyzer(cameraExecutor, LuminosityAnalyzer { luma ->
Log.d(TAG, "Average luminosity: $luma")
})
}
*/
- Vincula los casos de uso de
Preview
+VideoCapture
a una cámara de ciclo de vida. Dentro destartCamera()
, reemplaza la llamada acameraProvider.bindToLifecycle()
por lo siguiente:
// Bind use cases to camera
cameraProvider.bindToLifecycle(this, cameraSelector, preview, videoCapture)
En este punto, startCamera()
debería verse de la siguiente manera:
val cameraProviderFuture = ProcessCameraProvider.getInstance(this)
cameraProviderFuture.addListener({
// Used to bind the lifecycle of cameras to the lifecycle owner
val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get()
// Preview
val preview = Preview.Builder()
.build()
.also {
it.setSurfaceProvider(viewBinding.viewFinder.surfaceProvider)
}
val recorder = Recorder.Builder()
.setQualitySelector(QualitySelector.from(Quality.HIGHEST))
.build()
videoCapture = VideoCapture.withOutput(recorder)
/*
imageCapture = ImageCapture.Builder().build()
val imageAnalyzer = ImageAnalysis.Builder()
.build()
.also {
it.setAnalyzer(cameraExecutor, LuminosityAnalyzer { luma ->
Log.d(TAG, "Average luminosity: $luma")
})
}
*/
// Select back camera as a default
val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
try {
// Unbind use cases before rebinding
cameraProvider.unbindAll()
// Bind use cases to camera
cameraProvider
.bindToLifecycle(this, cameraSelector, preview, videoCapture)
} catch(exc: Exception) {
Log.e(TAG, "Use case binding failed", exc)
}
}, ContextCompat.getMainExecutor(this))
}
- Realiza la compilación y ejecuta la aplicación. Deberíamos ver la IU conocida de los pasos anteriores.
- Graba algunos clips:
- Presiona el botón "START CAPTURE". Observa que la leyenda cambiará a "STOP CAPTURE".
- Graba un video durante unos segundos o minutos.
- Presiona el botón "STOP CAPTURE" (el mismo botón que presionaste para iniciar la captura).
Ve el video (es igual que ver el archivo de imagen capturado)
Usaremos la app de Google Fotos para revisar el video capturado:
- Inicia Google Fotos .
- Presiona "Biblioteca" para ver los archivos multimedia ordenados. Presiona el ícono de la carpeta
"CameraX-Video"
para ver una lista de clips de video disponibles.
- Presiona el ícono para reproducir el clip de video que acabas de capturar. Una vez que se complete la reproducción, presiona el botón More en la esquina superior derecha para inspeccionar los detalles del clip.
Eso es todo lo que necesitamos para grabar un video. Sin embargo, VideoCapture
de CameraX tiene para ofrecer muchas más funciones, como las siguientes:
- pausar o reanudar una grabación
- capturar en
File
oFileDescriptor
- y más
Si deseas obtener instrucciones para utilizarlas, consulta la documentación oficial.
8. Combina VideoCapture con otros casos de uso (opcional)
En el paso VideoCapture
anterior, se demostró la combinación de Preview
y VideoCapture
, que se admite en todos los dispositivos, como se indica en la tabla de funciones de los dispositivos. En este paso, agregaremos los casos de uso de ImageCapture
a la combinación existente de VideoCapture
+ Preview
para expresar Preview + ImageCapture + VideoCapture
.
- Con el código existente del paso anterior, quita el comentario y habilita la creación de
imageCapture
enstartCamera()
:
imageCapture = ImageCapture.Builder().build()
- Agrega un elemento
FallbackStrategy
a la creación deQualitySelector
existente. De esta manera, CameraX puede obtener una resolución compatible si el parámetroQuality.HIGHEST
requerido no es compatible con el caso de uso deimageCapture
.
.setQualitySelector(QualitySelector.from(Quality.HIGHEST,
FallbackStrategy.higherQualityOrLowerThan(Quality.SD)))
- También en
startCamera()
, vincula el caso de usoimageCapture
con los existentes de Preview y videoCapture (nota: no vinculesimageAnalyzer
, ya que no se admite la combinaciónpreview + imageCapture + videoCapture + imageAnalysis
):
cameraProvider.bindToLifecycle(
this, cameraSelector, preview, imageCapture, videoCapture)
La función startCamera()
final se verá de la siguiente manera:
private fun startCamera() {
val cameraProviderFuture = ProcessCameraProvider.getInstance(this)
cameraProviderFuture.addListener({
// Used to bind the lifecycle of cameras to the lifecycle owner
val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get()
// Preview
val preview = Preview.Builder()
.build()
.also {
it.setSurfaceProvider(viewBinding.viewFinder.surfaceProvider)
}
val recorder = Recorder.Builder()
.setQualitySelector(QualitySelector.from(Quality.HIGHEST,
FallbackStrategy.higherQualityOrLowerThan(Quality.SD)))
.build()
videoCapture = VideoCapture.withOutput(recorder)
imageCapture = ImageCapture.Builder().build()
/*
val imageAnalyzer = ImageAnalysis.Builder().build()
.also {
setAnalyzer(
cameraExecutor,
LuminosityAnalyzer { luma ->
Log.d(TAG, "Average luminosity: $luma")
}
)
}
*/
// Select back camera as a default
val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
try {
// Unbind use cases before rebinding
cameraProvider.unbindAll()
// Bind use cases to camera
cameraProvider.bindToLifecycle(
this, cameraSelector, preview, imageCapture, videoCapture)
} catch(exc: Exception) {
Log.e(TAG, "Use case binding failed", exc)
}
}, ContextCompat.getMainExecutor(this))
}
- Realiza la compilación y ejecuta la aplicación. Deberíamos ver la IU conocida de los pasos anteriores, pero ahora funcionan los botones "Take Photo" y "Start Capture".
- Realiza algunas capturas:
- Presiona el botón "START CAPTURE" para iniciar la captura.
- Presiona "TAKE PHOTO" para capturar una imagen.
- Espera a que se complete la captura de imágenes (deberíamos ver un aviso, como se indicó antes).
- Presiona el botón "STOP CAPTURE" para detener la grabación.
Estamos realizando la captura de imágenes mientras están en curso la vista previa y la captura de video.
- Mira los archivos de imágenes y videos capturados como lo hicimos en la app de Google Fotos de los pasos anteriores. Esta vez, deberíamos ver dos fotos y dos clips de video.
- (Opcional) Reemplaza
imageCapture
en el caso de uso deImageAnalyzer
de los pasos anteriores (del paso 1 al paso 4): usaremos la combinaciónPreview
+ImageAnalysis
+VideoCapture
(recuerda que la combinaciónPreview
+Analysis
+ImageCapture
+VideoCapture
podría no ser compatible incluso con los dispositivos de cámara deLEVEL_3
).
9. ¡Felicitaciones!
Implementaste correctamente lo siguiente en una nueva app para Android desde cero:
- Se incluyeron dependencias de CameraX en un proyecto nuevo.
- Se mostró un visor de la cámara con el caso de uso de
Preview
. - Se implementó la captura de fotos y el guardado de imágenes en el almacenamiento mediante el caso de uso de
ImageCapture
. - Se implementó el análisis de fotogramas de la cámara en tiempo real con el caso de uso de
ImageAnalysis
. - Se implementó la captura de video con el caso de uso de
VideoCapture
.
Si quieres obtener más información sobre CameraX y lo que puedes hacer con esta biblioteca, consulta la documentación o clona el ejemplo oficial.