Solicitudes y sesiones de capturas de la cámara

Nota: Esta página hace referencia al paquete Camera2. A menos que tu app requiera funciones específicas de bajo nivel de Camera2, te recomendamos que uses CameraX. CameraX y Camera2 admiten Android 5.0 (nivel de API 21) y versiones posteriores.

Un solo dispositivo Android puede tener varias cámaras. Cada cámara es un CameraDevice, y un CameraDevice puede generar más de un flujo de forma simultánea.

Un motivo para hacerlo es que un flujo, los fotogramas secuenciales de la cámara que provienen de un CameraDevice, se optimice para una tarea específica, como mostrar un visor, mientras que otros se pueden usar para tomar una foto o grabar un video. Las transmisiones actúan como canalizaciones paralelas que procesan fotogramas sin procesar que salen de la cámara, un fotograma a la vez:

Transmisiones paralelas que procesan fotogramas sin procesar de un dispositivo de cámara en diferentes formatos de salida
Figura 1: Ilustración de Cómo crear una app de cámara universal (Google I/O 2018)

El procesamiento paralelo sugiere que puede haber límites de rendimiento según la potencia de procesamiento disponible de la CPU, la GPU o cualquier otro procesador. Si una canalización no puede seguir el ritmo de los fotogramas entrantes, comienza a descartarlos.

Cada canalización tiene su propio formato de salida. Los datos sin procesar que ingresan se transforman automáticamente en el formato de salida adecuado mediante la lógica implícita asociada con cada canalización. El CameraDevice que se usa en las muestras de código de esta página no es específico, por lo que primero debes enumerar todas las cámaras disponibles antes de continuar.

Puedes usar el CameraDevice para crear un CameraCaptureSession, que es específico de ese CameraDevice. Un CameraDevice debe recibir una configuración de fotograma para cada fotograma sin procesar con CameraCaptureSession. La configuración especifica los atributos de la cámara, como el enfoque automático, la apertura, los efectos y la exposición. Debido a las restricciones de hardware, solo hay una configuración activa en el sensor de la cámara en un momento determinado, que se denomina configuración activa.

Sin embargo, los casos de uso de transmisión mejoran y extienden las formas anteriores de usar CameraDevice para transmitir sesiones de captura, lo que te permite optimizar la transmisión de la cámara para tu caso de uso particular. Por ejemplo, puede mejorar la duración de batería cuando se optimizan las videollamadas.

Un CameraCaptureSession describe todas las canalizaciones posibles vinculadas al CameraDevice. Cuando se crea una sesión, no puedes agregar ni quitar canalizaciones. El CameraCaptureSession mantiene una cola de CaptureRequest, que se convierten en la configuración activa.

Un CaptureRequest agrega una configuración a la cola y selecciona una, más de una o todas las canalizaciones disponibles para recibir un fotograma de CameraDevice. Puedes enviar muchas solicitudes de captura durante la vida de una sesión de captura. Cada solicitud puede cambiar la configuración activa y el conjunto de canalizaciones de salida que reciben la imagen sin procesar.

Usa casos de uso de transmisión para obtener un mejor rendimiento

Los casos de uso de transmisión son una forma de mejorar el rendimiento de las sesiones de captura de Camera2. Le brindan al dispositivo de hardware más información para ajustar los parámetros, lo que proporciona una mejor experiencia de la cámara para tu tarea específica.

Esto permite que el dispositivo de la cámara optimice las canalizaciones de hardware y software de la cámara según las situaciones del usuario para cada transmisión. Para obtener más información sobre los casos de uso de transmisión, consulta setStreamUseCase.

Los casos de uso de transmisión te permiten especificar cómo se usa una transmisión de la cámara en particular con más detalle, además de establecer una plantilla en CameraDevice.createCaptureRequest(). Esto permite que el hardware de la cámara optimice los parámetros, como el ajuste, el modo de sensor o la configuración del sensor de la cámara, según las compensaciones de calidad o latencia adecuadas para casos de uso específicos.

Entre los casos de uso de Stream, se incluyen los siguientes:

  • DEFAULT: Cubre todo el comportamiento existente de la aplicación. Equivale a no establecer ningún caso de uso de transmisión.

  • PREVIEW: Se recomienda para el visor o el análisis de imágenes en la app.

  • STILL_CAPTURE: Se optimiza para la captura de alta calidad y alta resolución, y no se espera que mantenga velocidades de fotogramas similares a las de la vista previa.

  • VIDEO_RECORD: Se optimiza para la captura de video de alta calidad, incluida la estabilización de imagen de alta calidad, si el dispositivo lo admite y la aplicación lo habilita. Esta opción puede producir fotogramas de salida con un retraso sustancial con respecto al tiempo real para permitir una estabilización de la más alta calidad o algún otro procesamiento.

  • VIDEO_CALL: Se recomienda para usos de la cámara de larga duración en los que el consumo de energía es un problema.

  • PREVIEW_VIDEO_STILL: Se recomienda para apps de redes sociales o casos de uso de transmisión única. Es una transmisión de uso general.

  • VENDOR_START: Se usa para casos de uso definidos por el OEM.

Crea una CameraCaptureSession

Para crear una sesión de cámara, proporciónale uno o más búferes de salida en los que tu app pueda escribir fotogramas de salida. Cada búfer representa una canalización. Debes hacerlo antes de comenzar a usar la cámara para que el framework pueda configurar las canalizaciones internas del dispositivo y asignar búferes de memoria para enviar fotogramas a los destinos de salida necesarios.

En el siguiente fragmento de código, se muestra cómo puedes preparar una sesión de cámara con dos búferes de salida, uno que pertenece a un SurfaceView y otro a un ImageReader. Agregar el caso de uso de transmisión PREVIEW a previewSurface y el caso de uso de transmisión STILL_CAPTURE a imReaderSurface permite que el hardware del dispositivo optimice aún más estas transmisiones.

// Retrieve the target surfaces, which might be coming from a number of places:
// 1. SurfaceView, if you want to display the image directly to the user
// 2. ImageReader, if you want to read each frame or perform frame-by-frame
// analysis
// 3. OpenGL Texture or TextureView, although discouraged for maintainability
// reasons
// 4. RenderScript.Allocation, if you want to do parallel processing
val surfaceView = findViewById<SurfaceView>(
    // ...
)
val imageReader = ImageReader.newInstance(
    // ...
)

// Remember to call this only *after* SurfaceHolder.Callback.surfaceCreated().
val previewSurface = surfaceView.holder.surface
val imReaderSurface = imageReader.surface
val targets = listOf(previewSurface, imReaderSurface)

// Create a capture session using the predefined targets; this also involves
// defining the session state callback to be notified of when the session is
// ready.
// Setup Stream Use Case while setting up your Output Configuration.
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
fun configureSession(device: CameraDevice, targets: List<Surface>) {
    val configs = mutableListOf<OutputConfiguration>()
    val streamUseCase = CameraMetadata
        .SCALER_AVAILABLE_STREAM_USE_CASES_PREVIEW_VIDEO_STILL

    targets.forEach {
        val config = OutputConfiguration(it)
        config.streamUseCase = streamUseCase.toLong()
        configs.add(config)
    }
    // ...
    device.createCaptureSession(session)
}

En este punto, no definiste la configuración activa de la cámara. Cuando se configura la sesión, puedes crear y enviar solicitudes de captura para hacerlo.

La transformación que se aplica a las entradas a medida que se escriben en su búfer se determina por el tipo de cada destino, que debe ser un Surface. El framework de Android sabe cómo convertir una imagen sin procesar en la configuración activa en un formato adecuado para cada destino. La conversión se controla con el formato de píxel y el tamaño del Surface en particular.

El framework intenta hacer lo mejor posible, pero es posible que algunas combinaciones de configuración de Surface no funcionen, lo que causa problemas, como que no se cree la sesión, que se arroje un error de tiempo de ejecución cuando envías una solicitud o que se degrade el rendimiento. El framework proporciona garantías para combinaciones específicas de parámetros de dispositivo, plataforma y solicitud. La documentación de createCaptureSession() proporciona más información.

CaptureRequests individuales

La configuración que se usa para cada fotograma se codifica en un CaptureRequest, que se envía a la cámara. Para crear una solicitud de captura, puedes usar una de las plantillas predefinidas o TEMPLATE_MANUAL para tener un control total. Cuando eliges una plantilla, debes proporcionar uno o más búferes de salida para usarlos con la solicitud. Solo puedes usar búferes que ya se hayan definido en la sesión de captura que quieras usar.

Las solicitudes de captura usan un patrón de compilador y les brindan a los desarrolladores la oportunidad de configurar muchas opciones diferentes, como la exposición automática, el enfoque automático y la apertura de la lente. Antes de configurar un campo, asegúrate de que la opción específica esté disponible para el dispositivo llamando a CameraCharacteristics.getAvailableCaptureRequestKeys() y de que el valor elegido sea compatible. Para ello, verifica la característica de la cámara correspondiente, como los modos de exposición automática disponibles.

Para crear una solicitud de captura de un SurfaceView con la plantilla diseñada para la vista previa sin modificaciones, usa CameraDevice.TEMPLATE_PREVIEW:

val session: CameraCaptureSession = /* ... */ // from CameraCaptureSession.StateCallback
val captureRequest = session.device.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW)
captureRequest.addTarget(previewSurface)

Con una solicitud de captura definida, ahora puedes enviarla a la sesión de la cámara:

val session: CameraCaptureSession = ...  // from CameraCaptureSession.StateCallback
val captureRequest: CaptureRequest.Builder = ...  // from CameraDevice.createCaptureRequest()

// The first null argument corresponds to the capture callback, which you
// provide if you want to retrieve frame metadata or keep track of failed capture
// requests that can indicate dropped frames; the second null argument
// corresponds to the Handler used by the asynchronous callback, which falls
// back to the current thread's looper if null.
session.capture(captureRequest.build(), null, null)

Cuando se coloca un fotograma de salida en el búfer específico, se activa una devolución de llamada de captura. En muchos casos, se activan devoluciones de llamada adicionales, como ImageReader.OnImageAvailableListener, cuando se procesa el fotograma que contiene. En este punto, puedes recuperar los datos de la imagen del búfer especificado.

Repite CaptureRequests

Las solicitudes de una sola cámara son fáciles de realizar, pero no son muy útiles para mostrar una vista previa en vivo o un video. En ese caso, debes recibir un flujo continuo de fotogramas, no solo uno. En el siguiente fragmento de código, se muestra cómo agregar una solicitud repetida a la sesión:

val session: CameraCaptureSession = ... // from CameraCaptureSession.StateCallback
val captureRequest: CaptureRequest.Builder = ... // from CameraDevice.createCaptureRequest()

// This keeps sending the capture request as frequently as possible until
// the session is torn down or session.stopRepeating() is called.
session.setRepeatingRequest(captureRequest.build(), null, null)

Una solicitud de captura repetida hace que el dispositivo de la cámara capture imágenes de forma continua con la configuración del CaptureRequest proporcionado. La API de Camera2 también permite que los usuarios capturen videos de la cámara enviando CaptureRequests repetidas, como se ve en este repositorio de ejemplo de Camera2 en GitHub. También puede renderizar videos en cámara lenta capturando un video de alta velocidad (en cámara lenta) con una CaptureRequests de ráfaga repetida, como se muestra en la app de ejemplo de video en cámara lenta de Camera2 en GitHub.

Intercala CaptureRequests

Para enviar una segunda solicitud de captura mientras la solicitud de captura repetida está activa, como para mostrar un visor y permitir que los usuarios capturen una foto, no es necesario que detengas la solicitud repetida en curso. En su lugar, emites una solicitud de captura no repetitiva mientras la solicitud repetitiva sigue ejecutándose.

Cualquier búfer de salida que se use debe configurarse como parte de la sesión de la cámara cuando se crea la sesión por primera vez. Las solicitudes repetidas tienen una prioridad más baja que las solicitudes de un solo fotograma o de ráfaga, lo que permite que funcione el siguiente ejemplo:

val session: CameraCaptureSession = /* ... */ // from CameraCaptureSession.StateCallback

// Create the repeating request and dispatch it.
val repeatingRequest = session.device.createCaptureRequest(
    CameraDevice.TEMPLATE_PREVIEW
)
repeatingRequest.addTarget(previewSurface)
session.setRepeatingRequest(repeatingRequest.build(), null, null)

// Some time later...

// Create the single request and dispatch it.
// NOTE: This can disrupt the ongoing repeating request momentarily.
val singleRequest = session.device.createCaptureRequest(
    CameraDevice.TEMPLATE_STILL_CAPTURE
)
singleRequest.addTarget(imReaderSurface)
session.capture(singleRequest.build(), null, null)

Sin embargo, este enfoque tiene una desventaja: no sabes exactamente cuándo se produce la solicitud única. En la siguiente figura, si A es la solicitud de captura repetida y B es la solicitud de captura de un solo fotograma, así es como la sesión procesa la cola de solicitudes:

Cola de solicitudes de captura repetitiva activa interrumpida por una sola solicitud de captura
Figura 2: Ilustración de una cola de solicitudes para la sesión de cámara en curso

No hay garantías para la latencia entre la última solicitud repetida de A antes de que se active la solicitud B y la próxima vez que se vuelva a usar A, por lo que es posible que se omitan algunos fotogramas. Puedes hacer algunas cosas para mitigar este problema:

  • Agrega los destinos de salida de la solicitud A a la solicitud B. De esa manera, cuando el fotograma de B esté listo, se copiará en los destinos de salida de A. Por ejemplo, esto es esencial cuando se toman instantáneas de video para mantener una velocidad de fotogramas constante. En el código anterior, agregas singleRequest.addTarget(previewSurface) antes de compilar la solicitud.

  • Usa una combinación de plantillas diseñadas para funcionar en esta situación en particular, como el retraso de obturación cero.