Cómo comenzar a usar CameraX

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

  1. En Android Studio, crea un proyecto nuevo y selecciona Empty Activity cuando se te solicite.

ed0f21e863f9e38f.png

  1. 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.

10f0a12f6c8b997c.png

Agrega las dependencias de Gradle

  1. Abre el archivo build.gradle del módulo CameraXApp.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}"
}
  1. 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 de buildTypes, agrega lo siguiente:
compileOptions {
    sourceCompatibility JavaVersion.VERSION_1_8
    targetCompatibility JavaVersion.VERSION_1_8
}
  1. 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:

  1. Abre el archivo de diseño activity_main en res/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>
  1. 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

  1. 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.

  1. Abre AndroidManifest.xml y agrega estas líneas antes de la etiqueta application:
<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.

  1. 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()
}
  1. Ejecuta la app.

Ahora debería solicitar permiso para usar la cámara y el micrófono:

dcdf8aa3d87e74be.png

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.

  1. 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 elemento Runnable como primer argumento. Completaremos el resto más adelante. Agrega ContextCompat.getMainExecutor() como segundo argumento. Se mostrará un Executor que se ejecutará en el subproceso principal.
cameraProviderFuture.addListener(Runnable {}, ContextCompat.getMainExecutor(this))
  • En el Runnable, agrega un ProcessCameraProvider. Esto se usa para vincular el ciclo de vida de nuestra cámara al LifecycleOwner 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)
   }
val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
  • Crea un bloque try. Dentro de ese bloque, asegúrate de que no haya nada vinculado a cameraProvider y, luego, vincula nuestro cameraSelector y el objeto de vista previa a cameraProvider.
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)
}
  1. Ejecuta la app. Ahora obtenemos una vista previa de la cámara.

d61a4250f6a3ed35.png

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.

  1. 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 sentencia return, la app fallará si tuviera un valor null.
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 objeto imageCapture. Pasa outputOptions, 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)
}
  1. Ve al método startCamera() y copia esto debajo del código para obtener la vista previa.
imageCapture = ImageCapture.Builder().build()
  1. Por último, actualiza la llamada a bindToLifecycle() en el bloque try 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))
}
  1. Vuelve a ejecutar la app y presiona Take Photo. Deberíamos ver un aviso en la pantalla y un mensaje en los registros.

54292eaa4ce3be0a.png

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:

  1. Inicia Google Fotos Fotos.
  2. 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.

8e884489ca2599e9.png 9ca38ee62f08ef6f.png

  1. Toca el ícono de la imagen para ver la foto completa y presiona el botón Más Más en la esquina superior derecha para ver los detalles de la foto tomada.

55e1a442ab5f25e7.png 70a8b27a76523f56.png

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.

  1. 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ón analyze en una clase que implementa la interfaz ImageAnalysis.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():

  1. En el método startCamera(), agrega esto debajo del código de imageCapture.
val imageAnalyzer = ImageAnalysis.Builder()
   .build()
   .also {
       it.setAnalyzer(cameraExecutor, LuminosityAnalyzer { luma ->
           Log.d(TAG, "Average luminosity: $luma")
       })
   }
  1. Actualiza la llamada a bindToLifecycle() en cameraProvider para incluir el imageAnalyzer.
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))
}
  1. 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.

  1. Copia este código en el método captureVideo(): controla tanto el inicio como la detención de nuestro caso de uso de VideoCapture. 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")
       }
}
val mediaStoreOutputOptions = MediaStoreOutputOptions
      .Builder(contentResolver,
               MediaStore.Video.Media.EXTERNAL_CONTENT_URI)
  • Configura el video creado contentValues como MediaStoreOutputOptions.Builder y compila nuestra instancia de MediaStoreOutputOptions.
    .setContentValues(contentValues)
    .build()
    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()
   }
}
.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
   }
}
  1. En startCamera(), coloca el siguiente código después de la línea de creación de preview. Esto creará el caso de uso de VideoCapture.
val recorder = Recorder.Builder()
   .setQualitySelector(QualitySelector.from(Quality.HIGHEST))
   .build()
videoCapture = VideoCapture.withOutput(recorder)
  1. (Opcional) También dentro de startCamera(), inhabilita los casos de uso de imageCapture y imageAnalyzer 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")
       })
   }
*/
  1. Vincula los casos de uso de Preview + VideoCapture a una cámara de ciclo de vida. Dentro de startCamera(), reemplaza la llamada a cameraProvider.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))
}
  1. Realiza la compilación y ejecuta la aplicación. Deberíamos ver la IU conocida de los pasos anteriores.
  2. 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).

ef2a6005defc4977.png 8acee41fd0f4af0f.png

Ve el video (es igual que ver el archivo de imagen capturado)

Usaremos la app de Google Fotos para revisar el video capturado:

  1. Inicia Google Fotos Fotos.
  2. 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.

71f07e32d5f4f268.png 596819ad391fac37.png

  1. 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 Más en la esquina superior derecha para inspeccionar los detalles del clip.

7c7125726af9e429.png 44da18b15ad2f607.png

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 o FileDescriptor
  • 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.

  1. Con el código existente del paso anterior, quita el comentario y habilita la creación de imageCapture en startCamera():
imageCapture = ImageCapture.Builder().build()
  1. Agrega un elemento FallbackStrategy a la creación de QualitySelector existente. De esta manera, CameraX puede obtener una resolución compatible si el parámetro Quality.HIGHEST requerido no es compatible con el caso de uso de imageCapture.
.setQualitySelector(QualitySelector.from(Quality.HIGHEST,
    FallbackStrategy.higherQualityOrLowerThan(Quality.SD)))
  1. También en startCamera(), vincula el caso de usoimageCapture con los existentes de Preview y videoCapture (nota: no vincules imageAnalyzer, ya que no se admite la combinación preview + 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))
   }
  1. 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".
  2. 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.

ef2a6005defc4977.png 16bc70ec3346fa66.png

  1. 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.

3f3feb19c8c73532.png

  1. (Opcional) Reemplaza imageCapture en el caso de uso de ImageAnalyzer de los pasos anteriores (del paso 1 al paso 4): usaremos la combinación Preview + ImageAnalysis + VideoCapture (recuerda que la combinación Preview + Analysis + ImageCapture + VideoCapture podría no ser compatible incluso con los dispositivos de cámara de LEVEL_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.