Arquitectura de captura de video de CameraX

Por lo general, un sistema de captura graba transmisiones de video y audio, las comprime, combina varias transmisiones y, luego, escribe la transmisión resultante en el disco.

diagrama conceptual de un sistema de captura de video y audio
Figura 1: Diagrama conceptual de un sistema de captura de video y audio

En CameraX, la solución para la captura de video es el caso de uso de VideoCapture:

diagrama conceptual que muestra cómo camerax maneja el caso de uso de captura de video
Figura 2: Diagrama conceptual que muestra cómo CameraX maneja el caso de uso de VideoCapture

Como se muestra en la figura 2, la captura de video de CameraX incluye algunos componentes arquitectónicos de alto nivel:

  • SurfaceProvider para la fuente de video
  • AudioSource para la fuente de audio
  • Dos codificadores para codificar y comprimir video/audio
  • Un combinador de medios para combinar las dos transmisiones
  • Un ahorro de archivos para escribir el resultado

La API de VideoCapture simplifica el motor de captura complejo y proporciona a las aplicaciones una API mucho más simple y directa.

Descripción general de la API de VideoCapture

VideoCapture es un caso de uso de CameraX que funciona bien por sí solo o cuando se combina con otros casos de uso. Las combinaciones específicas admitidas dependen de las capacidades del hardware de la cámara, aunque Preview y VideoCapture son una combinación válida de casos de uso en todos los dispositivos.

La API de VideoCapture consta de los siguientes objetos que se comunican con aplicaciones:

  • VideoCapture es la clase de caso de uso de nivel superior. VideoCapture se vincula a un LifecycleOwner con un CameraSelector y otros UseCases de CameraX. Para obtener más información sobre estos conceptos y usos, consulta Arquitectura de CameraX.
  • Un Recorder es una implementación de VideoOutput que tiene un acoplamiento alto con VideoCapture. Recorder se usa para realizar la captura de video y audio. Una aplicación crea grabaciones a partir de un Recorder.
  • Un PendingRecording configura una grabación y proporciona opciones como habilitar el audio y configurar un objeto de escucha de eventos. Debes usar Recorder para crear una PendingRecording. PendingRecording no graba nada.
  • Recording realiza la grabación. Debes usar PendingRecording para crear un Recording.

En la Figura 3, se muestran las relaciones entre estos objetos:

diagrama que muestra las interacciones en un caso de uso de videocapture
Figura 3: Diagrama que muestra las interacciones en un caso de uso de VideoCapture

Leyenda:

  1. Crea una Recorder con QualitySelector.
  2. Configura Recorder con una de las OutputOptions.
  3. Si es necesario, habilita el audio con withAudioEnabled().
  4. Llama a start() con un objeto de escucha VideoRecordEvent para comenzar a grabar.
  5. Usa pause()/resume()/stop() en Recording para controlar la grabación.
  6. Responde a VideoRecordEvents dentro del objeto de escucha de eventos.

La lista detallada de las API se encuentra en el archivo current.txt dentro del código fuente.

Cómo usar la API de VideoCapture

Para integrar el caso de uso de CameraX VideoCapture a tu app, haz lo siguiente:

  1. Vincula VideoCapture.
  2. Prepara y configura la grabación.
  3. Inicia y controla la grabación del tiempo de ejecución.

En las siguientes secciones, se describe lo que puedes hacer en cada paso para obtener una sesión de grabación completa.

Cómo vincular VideoCapture

Para vincular el caso de uso de VideoCapure, haz lo siguiente:

  1. Crea un objeto Recorder.
  2. Crea el objeto VideoCapture.
  3. Haz la vinculación a Lifecycle.

La API de CameraX VideoCapture sigue el patrón de diseño del compilador. Las aplicaciones usan Recorder.Builder para crear un Recorder. También puedes configurar la resolución del video para Recorder mediante un objeto QualitySelector.

CameraX Recorder admite las siguientes Qualities predefinidas para resoluciones de video:

  • Quality.UHD para videos con resolución 4K Ultra HD (2160p)
  • Quality.FHD para videos con resolución Full HD (1080p)
  • Quality.HD para videos con resolución HD (720p)
  • Quality.SD para videos con resolución SD (480p)

Ten en cuenta que CameraX también podrá elegir otras resoluciones cuando lo autorice la app.

La resolución exacta del video de cada selección depende de la cámara y las capacidades del codificador. Para obtener más información, consulta la documentación de CamcorderProfile.

Para configurar la resolución, las aplicaciones pueden crear un QualitySelector. QualitySelector se puede crear con uno de los siguientes métodos:

  • Brinda algunas resoluciones preferidas mediante fromOrderedList() y agrega una estrategia de resguardo para usar en caso de que no se admita ninguna de las resoluciones preferidas.

    CameraX puede decidir la mejor coincidencia de resguardo según la capacidad de la cámara seleccionada. Para obtener más detalles, consulta el objeto FallbackStrategy specification de QualitySelector. Por ejemplo, el siguiente código solicita la resolución más alta compatible para la grabación y, si no se admite ninguna de las resoluciones de solicitud, autoriza a CameraX a elegir la que esté más cerca de la resolución Quality.SD:

    val qualitySelector = QualitySelector.fromOrderedList(
             listOf(Quality.UHD, Quality.FHD, Quality.HD, Quality.SD),
             FallbackStrategy.lowerQualityOrHigherThan(Quality.SD))
    
  • Consulta las capacidades de la cámara primero y elige una de las resoluciones compatibles mediante QualitySelector::from():

    val cameraInfo = cameraProvider.availableCameraInfos.filter {
        Camera2CameraInfo
        .from(it)
        .getCameraCharacteristic(CameraCharacteristics.LENS\_FACING) == CameraMetadata.LENS_FACING_BACK
    }
    
    val supportedQualities = QualitySelector.getSupportedQualities(cameraInfo[0])
    val filteredQualities = arrayListOf (Quality.UHD, Quality.FHD, Quality.HD, Quality.SD)
                           .filter { supportedQualities.contains(it) }
    
    // Use a simple ListView with the id of simple_quality_list_view
    viewBinding.simpleQualityListView.apply {
        adapter = ArrayAdapter(context,
                               android.R.layout.simple_list_item_1,
                               filteredQualities.map { it.qualityToString() })
    
        // Set up the user interaction to manually show or hide the system UI.
        setOnItemClickListener { _, _, position, _ ->
            // Inside View.OnClickListener,
            // convert Quality.* constant to QualitySelector
            val qualitySelector = QualitySelector.from(filteredQualities[position])
    
            // Create a new Recorder/VideoCapture for the new quality
            // and bind to lifecycle
            val recorder = Recorder.Builder()
                .setQualitySelector(qualitySelector).build()
    
             // ...
        }
    }
    
    // A helper function to translate Quality to a string
    fun Quality.qualityToString() : String {
        return when (this) {
            Quality.UHD -> "UHD"
            Quality.FHD -> "FHD"
            Quality.HD -> "HD"
            Quality.SD -> "SD"
            else -> throw IllegalArgumentException()
        }
    }
    
    

    Ten en cuenta que se garantiza que la capacidad que se muestra de QualitySelector.getSupportedQualities() funcione para el caso de uso de VideoCapture o la combinación de VideoCapture y Preview. Cuando se realiza la vinculación con el caso de uso de ImageCapture o ImageAnalysis, es posible que CameraX falle al hacerlo si la combinación requerida no es compatible con la cámara solicitada.

Una vez que tengas un QualitySelector, la aplicación podrá crear un objeto VideoCapture y realizar la vinculación. Ten en cuenta que esta vinculación es la misma que con otros casos de uso:

val recorder = Recorder.Builder()
    .setExecutor(cameraExecutor).setQualitySelector(qualitySelector)
    .build()
val videoCapture = VideoCapture.withOutput(recorder)

try {
    // Bind use cases to camera
    cameraProvider.bindToLifecycle(
            this, CameraSelector.DEFAULT_BACK_CAMERA, preview, videoCapture)
} catch(exc: Exception) {
    Log.e(TAG, "Use case binding failed", exc)
}

Ten en cuenta que bindToLifecycle() muestra un objeto Camera. Consulta esta guía a fin de obtener más información para controlar la salida de la cámara, como el zoom y la exposición.

Recorder selecciona el formato más adecuado para el sistema. El códec de video más común es H.264 AVC con formato de contenedor MPEG-4.

Cómo configurar y crear una grabación

Desde un Recorder, la aplicación puede crear objetos de grabación para realizar la captura de audio y de video. Las aplicaciones crean grabaciones de la siguiente manera:

  1. Configuran OutputOptions con prepareRecording().
  2. Habilita la grabación de audio (opcional).
  3. Usa start() para registrar un objeto de escucha VideoRecordEvent y comienza a capturar videos.

El objeto Recorder muestra un objeto Recording cuando llamas a la función start(). Tu aplicación puede usar este objeto Recording para finalizar la captura o realizar otras acciones, como pausar o reanudar.

Recorder admite un objeto Recording por vez. Puedes iniciar una grabación nueva una vez que hayas llamado a Recording.stop() o Recording.close() en el objeto Recording anterior.

Analicemos estos pasos con más detalle. Primero, la aplicación configura OutputOptions para una grabadora con Recorder.prepareRecording(). Recorder admite los siguientes tipos de OutputOptions:

  • FileDescriptorOutputOptions para hacer la captura en un FileDescriptor
  • FileOutputOptions para hacer la captura en un File
  • MediaStoreOutputOptions para hacer la captura en un MediaStore

Todos los tipos OutputOptions te permiten establecer un tamaño de archivo máximo con setFileSizeLimit(). Otras opciones son específicas del tipo de salida individual, como ParcelFileDescriptor para FileDescriptorOutputOptions.

prepareRecording() muestra un objeto PendingRecording, que es un objeto intermedio que se usa para crear la Recording correspondiente. PendingRecording es una clase transitoria que debería ser invisible en la mayoría de los casos y la app rara vez almacena en caché.

Las aplicaciones pueden configurar las siguientes opciones de la grabación:

  • Habilitar el audio con withAudioEnabled()
  • Registrar un objeto de escucha para recibir eventos de grabación de video con start(Executor, Consumer<VideoRecordEvent>)
  • Permitir una grabación continua mientras que el elemento VideoCapture adjunto se envía a otra cámara, con PendingRecording.asPersistentRecording().

Para comenzar a grabar, llama a PendingRecording.start(). CameraX convierte el PendingRecording en un Recording, pone en cola la solicitud de grabación y muestra el objeto Recording recién creado a la aplicación. Una vez que comienza la grabación en el dispositivo correspondiente, CameraX envía un evento VideoRecordEvent.EVENT_TYPE_START.

En el siguiente ejemplo, se muestra cómo grabar video y audio en un archivo MediaStore:

// Create MediaStoreOutputOptions for our recorder
val name = "CameraX-recording-" +
        SimpleDateFormat(FILENAME_FORMAT, Locale.US)
                .format(System.currentTimeMillis()) + ".mp4"
val contentValues = ContentValues().apply {
   put(MediaStore.Video.Media.DISPLAY_NAME, name)
}
val mediaStoreOutput = MediaStoreOutputOptions.Builder(this.contentResolver,
                              MediaStore.Video.Media.EXTERNAL_CONTENT_URI)
                              .setContentValues(contentValues)
                              .build()

// 2. Configure Recorder and Start recording to the mediaStoreOutput.
val recording = videoCapture.output
                .prepareRecording(context, mediaStoreOutput)
                .withAudioEnabled()
                .start(ContextCompat.getMainExecutor(this), captureListener)

La vista previa de la cámara se duplica en la cámara frontal de forma predeterminada, pero eso no sucede con los videos grabados por VideoCapture. Con CameraX 1.3, ahora es posible duplicar las grabaciones de video de modo que la vista previa de la cámara frontal coincida con el video grabado.

Existen tres opciones de MirrorMode: MIRROR_MODE_OFF, MIRROR_MODE_ON y MIRROR_MODE_ON_FRONT_ONLY. Para alinear la vista previa de la cámara, Google recomienda que uses MIRROR_MODE_ON_FRONT_ONLY, lo que significa que no se habilitará la duplicación para la cámara posterior, pero sí para la frontal. Para obtener más información sobre MirrorMode, consulta MirrorMode constants.

Este fragmento de código te muestra cómo llamar a VideoCapture.Builder.setMirrorMode() usando MIRROR_MODE_ON_FRONT_ONLY. Si deseas obtener información detallada, consulta setMirrorMode().

Kotlin


val recorder = Recorder.Builder().build()

val videoCapture = VideoCapture.Builder(recorder)
    .setMirrorMode(MIRROR_MODE_ON_FRONT_ONLY)
    .build()

useCases.add(videoCapture);

Java


Recorder.Builder builder = new Recorder.Builder();
if (mVideoQuality != QUALITY_AUTO) {
    builder.setQualitySelector(
        QualitySelector.from(mVideoQuality));
}
  VideoCapture<Recorder> videoCapture = new VideoCapture.Builder<>(builder.build())
      .setMirrorMode(MIRROR_MODE_ON_FRONT_ONLY)
      .build();
    useCases.add(videoCapture);

Cómo controlar una grabación activa

Puedes pausar, reanudar y detener una Recording en curso mediante los siguientes métodos:

  • pause para pausar la grabación activa actual
  • resume() para reanudar una grabación activa en pausa
  • stop() para finalizar la grabación y limpiar los objetos de grabación asociados
  • mute() para silenciar o activar el sonido de la grabación actual

Ten en cuenta que puedes llamar a stop() para finalizar una Recording, independientemente de si la grabación está en estado de pausa o en estado activo.

Si registraste un EventListener con PendingRecording.start(), Recording se comunica mediante VideoRecordEvent.

  • VideoRecordEvent.EVENT_TYPE_STATUS se usa para grabar estadísticas, como el tamaño actual del archivo y el intervalo de tiempo grabado.
  • VideoRecordEvent.EVENT_TYPE_FINALIZE se usa para el resultado de la grabación y, además, incluye información como el URI del archivo final junto con cualquier error relacionado.

Una vez que la app reciba un EVENT_TYPE_FINALIZE que indique una sesión de grabación exitosa, podrás acceder al video capturado desde la ubicación que se haya especificado en OutputOptions.

Recursos adicionales

Para obtener más información sobre CameraX, consulta los siguientes recursos adicionales: