Arquitectura de CameraX

En esta página, se describe la arquitectura de CameraX, que incluye la estructura, cómo trabajar con la API y con ciclos de vida, y cómo combinar los casos de uso.

Estructura de CameraX

Puedes usar CameraX para interactuar con la cámara del dispositivo mediante una abstracción llamada caso de uso. Están disponibles los siguientes casos de uso:

  • Vista previa: Acepta una plataforma para mostrar una vista previa, como un elemento PreviewView.
  • Análisis de imágenes: Proporciona búferes a los que se puede acceder desde la CPU para análisis, que sirven, por ejemplo, para aprendizaje automático.
  • Captura de imágenes: Captura y guarda una foto.
  • Captura de video: Captura video y audio con VideoCapture

Los casos de uso pueden combinarse y estar activos en simultáneo. Por ejemplo, una app puede permitir que el usuario vea lo mismo que la cámara con un caso de uso de vista previa, tener otro caso de análisis de imágenes para determinar si las personas que aparecen en la foto están sonriendo e incluir otro de captura de imágenes que tome una foto cuando efectivamente estén sonriendo.

Modelo de API

Para trabajar con la biblioteca, tienes que especificar lo siguiente:

  • El caso de uso deseado con sus opciones de configuración
  • Qué hacer con los datos resultantes al juntar objetos de escucha
  • Cuál es el flujo pretendido, como los momentos para habilitar las cámaras y producir datos, mediante la vinculación del caso de uso a los ciclos de vida de la arquitectura de Android

Hay 2 maneras de escribir una app de CameraX: un CameraController (ideal si quieres la manera más sencilla de usar CameraX) o un CameraProvider (ideal si necesitas más flexibilidad).

CameraController

Un CameraController proporciona la mayor parte de la funcionalidad principal de CameraX en una sola clase. Requiere poco código de configuración y controla automáticamente la inicialización de la cámara, la administración de casos de uso, la rotación objetivo, la función de presionar para enfocar, pellizcar para acercar y mucho más. La clase concreta que extiende CameraController es LifecycleCameraController.

Kotlin

val previewView: PreviewView = viewBinding.previewView
var cameraController = LifecycleCameraController(baseContext)
cameraController.bindToLifecycle(this)
cameraController.cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
previewView.controller = cameraController

Java

PreviewView previewView = viewBinding.previewView;
LifecycleCameraController cameraController = new LifecycleCameraController(baseContext);
cameraController.bindToLifecycle(this);
cameraController.setCameraSelector(CameraSelector.DEFAULT_BACK_CAMERA);
previewView.setController(cameraController);

Los UseCase predeterminados para CameraController son Preview, ImageCapture y ImageAnalysis. Para desactivar ImageCapture o ImageAnalysis, o activar VideoCapture, usa el método setEnabledUseCases().

Para ver más usos de CameraController, consulta la muestra del escáner de código QR o el video introductorio de CameraController.

CameraProvider

Un CameraProvider sigue siendo fácil de usar, pero como el desarrollador de apps controla más aspectos de la configuración, existen más oportunidades para personalizar la configuración, como habilitar la rotación de la imagen de salida o establecer el formato en ImageAnalysis. También puedes usar una Surface personalizada para obtener una vista previa de la cámara que brinda mayor flexibilidad, mientras que, con CameraController, debes usar una PreviewView. Puede ser útil usar el código Surface existente si ya es una entrada para otras partes de la app.

Puedes configurar casos de uso con los métodos set() y finalizarlos con el método build(). Cada objeto de caso de uso proporciona un conjunto de APIs específicas de casos de uso. Por ejemplo, el caso de uso de la captura de imagen proporciona una llamada al método takePicture().

Una aplicación especificará un ciclo de vida con el que asociar la cámara mediante cameraProvider.bindToLifecycle(), en lugar de llamar métodos de inicio y finalización en onResume() y onPause(). El ciclo de vida informa a CameraX cuándo se debe configurar la sesión de captura de la cámara y se asegura de que el estado de la cámara cambie de manera adecuada para que coincida con sus transiciones.

Si quieres conocer los pasos de implementación para cada caso de uso, consulta Cómo implementar una vista previa, Análisis de imágenes, Cómo capturar imágenes y Captura de video.

El caso de uso de la vista previa interactúa con un elemento Surface para mostrarla. Las aplicaciones crean el caso de uso con opciones de configuración que utilizan el siguiente código:

Kotlin

val preview = Preview.Builder().build()
val viewFinder: PreviewView = findViewById(R.id.previewView)

// The use case is bound to an Android Lifecycle with the following code
val camera = cameraProvider.bindToLifecycle(lifecycleOwner, cameraSelector, preview)

// PreviewView creates a surface provider and is the recommended provider
preview.setSurfaceProvider(viewFinder.getSurfaceProvider())

Java

Preview preview = new Preview.Builder().build();
PreviewView viewFinder = findViewById(R.id.view_finder);

// The use case is bound to an Android Lifecycle with the following code
Camera camera = cameraProvider.bindToLifecycle(lifecycleOwner, cameraSelector, preview);

// PreviewView creates a surface provider, using a Surface from a different
// kind of view will require you to implement your own surface provider.
preview.previewSurfaceProvider = viewFinder.getSurfaceProvider();

Para ver más ejemplos de código, consulta la app de muestra de CameraX oficial.

Ciclos de vida de CameraX

CameraX obedece un ciclo de vida para determinar cuándo abrir la cámara, crear una sesión de captura, detenerse y apagarse. Las APIs de casos de uso proporcionan llamadas y devoluciones de llamada de métodos para supervisar el progreso.

Según se explica en el artículo para combinar casos de uso, puedes vincular combinaciones de casos de uso a un solo ciclo de vida. Cuando tu app necesite admitir casos de uso que no se puedan combinar, podrás realizar una de las siguientes acciones:

  • Agrupar casos de uso compatibles en más de un fragmento y, luego, intercambiar fragmentos
  • Crear un componente del ciclo de vida personalizado y usarlo para controlar el ciclo de vida de la cámara de forma manual

En caso de desacoplar la vista y los LifecycleOwner de los casos de uso de la cámara (por ejemplo, si usas un ciclo de vida personalizado o un fragmento de retención), debes asegurarte de que todos los casos de uso estén desvinculados de CameraX. Para ello, utiliza ProcessCameraProvider.unbindAll() o desvincula cada caso de uso de forma individual. De manera alternativa, cuando vinculas casos de uso a un ciclo de vida, puedes permitir que CameraX administre la apertura y el cierre de la sesión de captura, y la desvinculación de los casos de uso.

Si todas las funciones de tu cámara corresponden al ciclo de vida de un solo componente relacionado con los ciclos de vida, como un fragmento AppCompatActivity o AppCompat, el uso del ciclo de vida de ese componente cuando vincules todos los casos de uso deseados garantizará que las funciones de la cámara estén listas cuando el componente esté activo o que se descarten de manera segura y sin consumir recursos.

LifecycleOwner personalizado

Para casos avanzados, puedes crear un LifecycleOwner personalizado para permitir que tu app controle explícitamente el ciclo de vida de la sesión de CameraX en lugar de vincularlo a un LifecycleOwner de Android estándar.

En la siguiente muestra de código, se indica cómo crear un LifecycleOwner simple y personalizado:

Kotlin

class CustomLifecycle : LifecycleOwner {
    private val lifecycleRegistry: LifecycleRegistry

    init {
        lifecycleRegistry = LifecycleRegistry(this);
        lifecycleRegistry.markState(Lifecycle.State.CREATED)
    }
    ...
    fun doOnResume() {
        lifecycleRegistry.markState(State.RESUMED)
    }
    ...
    override fun getLifecycle(): Lifecycle {
        return lifecycleRegistry
    }
}

Java

public class CustomLifecycle implements LifecycleOwner {
    private LifecycleRegistry lifecycleRegistry;
    public CustomLifecycle() {
        lifecycleRegistry = new LifecycleRegistry(this);
        lifecycleRegistry.markState(Lifecycle.State.CREATED);
    }
   ...
   public void doOnResume() {
        lifecycleRegistry.markState(State.RESUMED);
    }
   ...
    public Lifecycle getLifecycle() {
        return lifecycleRegistry;
    }
}

Con este LifecycleOwner, tu app puede colocar transiciones de estado en los puntos deseados de su código. Si quieres obtener más información para implementar esta función en tu app, consulta Cómo implementar un LifecycleOwner personalizado.

Casos de uso simultáneos

Los casos de uso pueden ejecutarse en simultáneo. Si bien pueden estar vinculados de manera secuencial a un ciclo de vida, te recomendamos que vincules todos los casos de uso con una sola llamada a CameraProcessProvider.bindToLifecycle(). Obtén más información sobre las prácticas recomendadas para los cambios de configuración en Administra los cambios en la configuración.

En la siguiente muestra de código, la app especifica los dos casos de uso que se crearán y ejecutarán de manera simultánea. También especifica el ciclo de vida que se usará para ambos casos de uso, para que los dos se inicien y se detengan según el ciclo de vida.

Kotlin

private lateinit var imageCapture: ImageCapture

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)

    val cameraProviderFuture = ProcessCameraProvider.getInstance(this)

    cameraProviderFuture.addListener(Runnable {
        // Camera provider is now guaranteed to be available
        val cameraProvider = cameraProviderFuture.get()

        // Set up the preview use case to display camera preview.
        val preview = Preview.Builder().build()

        // Set up the capture use case to allow users to take photos.
        imageCapture = ImageCapture.Builder()
                .setCaptureMode(ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY)
                .build()

        // Choose the camera by requiring a lens facing
        val cameraSelector = CameraSelector.Builder()
                .requireLensFacing(CameraSelector.LENS_FACING_FRONT)
                .build()

        // Attach use cases to the camera with the same lifecycle owner
        val camera = cameraProvider.bindToLifecycle(
                this as LifecycleOwner, cameraSelector, preview, imageCapture)

        // Connect the preview use case to the previewView
        preview.setSurfaceProvider(
                previewView.getSurfaceProvider())
    }, ContextCompat.getMainExecutor(this))
}

Java

private ImageCapture imageCapture;

@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    PreviewView previewView = findViewById(R.id.previewView);

    ListenableFuture<ProcessCameraProvider> cameraProviderFuture =
            ProcessCameraProvider.getInstance(this);

    cameraProviderFuture.addListener(() -> {
        try {
            // Camera provider is now guaranteed to be available
            ProcessCameraProvider cameraProvider = cameraProviderFuture.get();

            // Set up the view finder use case to display camera preview
            Preview preview = new Preview.Builder().build();

            // Set up the capture use case to allow users to take photos
            imageCapture = new ImageCapture.Builder()
                    .setCaptureMode(ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY)
                    .build();

            // Choose the camera by requiring a lens facing
            CameraSelector cameraSelector = new CameraSelector.Builder()
                    .requireLensFacing(lensFacing)
                    .build();

            // Attach use cases to the camera with the same lifecycle owner
            Camera camera = cameraProvider.bindToLifecycle(
                    ((LifecycleOwner) this),
                    cameraSelector,
                    preview,
                    imageCapture);

            // Connect the preview use case to the previewView
            preview.setSurfaceProvider(
                    previewView.getSurfaceProvider());
        } catch (InterruptedException | ExecutionException e) {
            // Currently no exceptions thrown. cameraProviderFuture.get()
            // shouldn't block since the listener is being called, so no need to
            // handle InterruptedException.
        }
    }, ContextCompat.getMainExecutor(this));
}

CameraX permite el uso simultáneo de una instancia de Preview, VideoCapture, ImageAnalysis y ImageCapture. Además:

  • Todos los casos de uso pueden funcionar por su cuenta. Por ejemplo, una app puede grabar video sin usar la vista previa.
  • Cuando se habilitan las extensiones, solo se garantiza que funcione la combinación ImageCapture y Preview. En función de la implementación del OEM, es posible que tampoco se pueda agregar ImageAnalysis. No se pueden habilitar las extensiones para el caso de uso de VideoCapture. Para obtener más información, consulta el documento de referencia de extensiones.
  • En función de sus capacidades, algunas cámaras pueden admitir la combinación en modos de menor resolución, pero no pueden admitir la misma combinación en resoluciones más altas.
  • En dispositivos con un nivel de hardware de la cámara FULL o inferior, combinar Preview, VideoCapture y ImageCapture o ImageAnalysis puede forzar a CameraX a duplicar el flujo PRIV de la cámara para Preview y VideoCapture. Esta duplicación, llamada uso compartido de transmisiones, permite el uso simultáneo de estas funciones, pero a costa de aumentar las demandas de procesamiento. Como resultado, es posible que experimentes una latencia ligeramente más alta y una duración de batería reducida.

El nivel de hardware compatible se puede recuperar desde Camera2CameraInfo. Por ejemplo, el siguiente código verifica si la cámara posterior predeterminada es un dispositivo LEVEL_3:

Kotlin

@androidx.annotation.OptIn(ExperimentalCamera2Interop::class)
fun isBackCameraLevel3Device(cameraProvider: ProcessCameraProvider) : Boolean {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
        return CameraSelector.DEFAULT_BACK_CAMERA
            .filter(cameraProvider.availableCameraInfos)
            .firstOrNull()
            ?.let { Camera2CameraInfo.from(it) }
            ?.getCameraCharacteristic(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL) ==
            CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_3
    }
    return false
}

Java

@androidx.annotation.OptIn(markerClass = ExperimentalCamera2Interop.class)
Boolean isBackCameraLevel3Device(ProcessCameraProvider cameraProvider) {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
        List\ filteredCameraInfos = CameraSelector.DEFAULT_BACK_CAMERA
                .filter(cameraProvider.getAvailableCameraInfos());
        if (!filteredCameraInfos.isEmpty()) {
            return Objects.equals(
                Camera2CameraInfo.from(filteredCameraInfos.get(0)).getCameraCharacteristic(
                        CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL),
                CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_3);
        }
    }
    return false;
}

Permisos

La app necesita el permiso CAMERA. Para guardar imágenes en archivos, también necesitarás el permiso WRITE_EXTERNAL_STORAGE, excepto en dispositivos que ejecuten Android 10 o versiones posteriores.

Si quieres obtener más información para configurar permisos en tu app, consulta este artículo.

Requisitos

CameraX tiene los siguientes requisitos mínimos de versión:

  • Nivel 21 de la API de Android
  • Componentes de la arquitectura de Android 1.1.1

Para actividades relacionadas con el ciclo de vida, usa FragmentActivity o AppCompatActivity.

Cómo declarar dependencias

Para agregar una dependencia en CameraX, debes agregar el repositorio de Maven de Google a tu proyecto.

Abre el archivo settings.gradle de tu proyecto y agrega el repositorio google() como se muestra a continuación:

Groovy

dependencyResolutionManagement {
    repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
    repositories {
        google()
        mavenCentral()
    }
}

Kotlin

dependencyResolutionManagement {
    repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
    repositories {
        google()
        mavenCentral()
    }
}

Agrega lo siguiente al final del bloque de Android:

Groovy

android {
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
    // For Kotlin projects
    kotlinOptions {
        jvmTarget = "1.8"
    }
}

Kotlin

android {
    compileOptions {
        sourceCompatibility = JavaVersion.VERSION_1_8
        targetCompatibility = JavaVersion.VERSION_1_8
    }
    // For Kotlin projects
    kotlinOptions {
        jvmTarget = "1.8"
    }
}

Agrega lo siguiente al archivo build.gradle de cada módulo para una app:

Groovy

dependencies {
  // CameraX core library using the camera2 implementation
  def camerax_version = "1.5.0-alpha03"
  // The following line is optional, as the core library is included indirectly by camera-camera2
  implementation "androidx.camera:camera-core:${camerax_version}"
  implementation "androidx.camera:camera-camera2:${camerax_version}"
  // If you want to additionally use the CameraX Lifecycle library
  implementation "androidx.camera:camera-lifecycle:${camerax_version}"
  // If you want to additionally use the CameraX VideoCapture library
  implementation "androidx.camera:camera-video:${camerax_version}"
  // If you want to additionally use the CameraX View class
  implementation "androidx.camera:camera-view:${camerax_version}"
  // If you want to additionally add CameraX ML Kit Vision Integration
  implementation "androidx.camera:camera-mlkit-vision:${camerax_version}"
  // If you want to additionally use the CameraX Extensions library
  implementation "androidx.camera:camera-extensions:${camerax_version}"
}

Kotlin

dependencies {
    // CameraX core library using the camera2 implementation
    val camerax_version = "1.5.0-alpha03"
    // The following line is optional, as the core library is included indirectly by camera-camera2
    implementation("androidx.camera:camera-core:${camerax_version}")
    implementation("androidx.camera:camera-camera2:${camerax_version}")
    // If you want to additionally use the CameraX Lifecycle library
    implementation("androidx.camera:camera-lifecycle:${camerax_version}")
    // If you want to additionally use the CameraX VideoCapture library
    implementation("androidx.camera:camera-video:${camerax_version}")
    // If you want to additionally use the CameraX View class
    implementation("androidx.camera:camera-view:${camerax_version}")
    // If you want to additionally add CameraX ML Kit Vision Integration
    implementation("androidx.camera:camera-mlkit-vision:${camerax_version}")
    // If you want to additionally use the CameraX Extensions library
    implementation("androidx.camera:camera-extensions:${camerax_version}")
}

Si quieres obtener más información para configurar tu app y que cumpla con estos requisitos, consulta Cómo declarar dependencias.

Interoperabilidad de CameraX con Camera2

CameraX se compiló en Camera2 y expone formas de leer, así como también de escribir, propiedades en la implementación de Camera2. Para obtener más información, consulta el paquete de interoperabilidad.

Si deseas obtener más información sobre la forma en la que CameraX configuró las propiedades de Camera2, usa Camera2CameraInfo para leer las CameraCharacteristics subyacentes. También puedes optar por escribir las propiedades subyacentes de Camera2 en una de las dos rutas siguientes:

En la siguiente muestra de código, se usan casos de uso de transmisión para realizar optimizaciones con el objetivo de realizar una videollamada. Usa Camera2CameraInfo para recuperar si está disponible el caso de uso de transmisión de videollamadas. Luego, usa un Camera2Interop.Extender para establecer el caso de uso de transmisión subyacente.

Kotlin

// Set underlying Camera2 stream use case to optimize for video calls.

val videoCallStreamId =
    CameraMetadata.SCALER_AVAILABLE_STREAM_USE_CASES_VIDEO_CALL.toLong()

// Check available CameraInfos to find the first one that supports
// the video call stream use case.
val frontCameraInfo = cameraProvider.getAvailableCameraInfos()
    .first { cameraInfo ->
        val isVideoCallStreamingSupported = Camera2CameraInfo.from(cameraInfo)
            .getCameraCharacteristic(
                CameraCharacteristics.SCALER_AVAILABLE_STREAM_USE_CASES
            )?.contains(videoCallStreamId)
        val isFrontFacing = (cameraInfo.getLensFacing() == 
                             CameraSelector.LENS_FACING_FRONT)
        (isVideoCallStreamingSupported == true) && isFrontFacing
    }

val cameraSelector = frontCameraInfo.cameraSelector

// Start with a Preview Builder.
val previewBuilder = Preview.Builder()
    .setTargetAspectRatio(screenAspectRatio)
    .setTargetRotation(rotation)

// Use Camera2Interop.Extender to set the video call stream use case.
Camera2Interop.Extender(previewBuilder).setStreamUseCase(videoCallStreamId)

// Bind the Preview UseCase and the corresponding CameraSelector.
val preview = previewBuilder.build()
camera = cameraProvider.bindToLifecycle(this, cameraSelector, preview)

Java

// Set underlying Camera2 stream use case to optimize for video calls.

Long videoCallStreamId =
    CameraMetadata.SCALER_AVAILABLE_STREAM_USE_CASES_VIDEO_CALL.toLong();

// Check available CameraInfos to find the first one that supports
// the video call stream use case.
List<CameraInfo> cameraInfos = cameraProvider.getAvailableCameraInfos();
CameraInfo frontCameraInfo = null;
for (cameraInfo in cameraInfos) {
    Long[] availableStreamUseCases = Camera2CameraInfo.from(cameraInfo)
        .getCameraCharacteristic(
            CameraCharacteristics.SCALER_AVAILABLE_STREAM_USE_CASES
        );
    boolean isVideoCallStreamingSupported = Arrays.List(availableStreamUseCases)
                .contains(videoCallStreamId);
    boolean isFrontFacing = (cameraInfo.getLensFacing() ==
                             CameraSelector.LENS_FACING_FRONT);

    if (isVideoCallStreamingSupported && isFrontFacing) {
        frontCameraInfo = cameraInfo;
    }
}

if (frontCameraInfo == null) {
    // Handle case where video call streaming is not supported.
}

CameraSelector cameraSelector = frontCameraInfo.getCameraSelector();

// Start with a Preview Builder.
Preview.Builder previewBuilder = Preview.Builder()
    .setTargetAspectRatio(screenAspectRatio)
    .setTargetRotation(rotation);

// Use Camera2Interop.Extender to set the video call stream use case.
Camera2Interop.Extender(previewBuilder).setStreamUseCase(videoCallStreamId);

// Bind the Preview UseCase and the corresponding CameraSelector.
Preview preview = previewBuilder.build()
Camera camera = cameraProvider.bindToLifecycle(this, cameraSelector, preview)

Recursos adicionales

Para obtener más información acerca de CameraX, consulta los siguientes recursos adicionales.

Codelab

  • Cómo comenzar a usar CameraX
  • Muestra de código

  • Apps de ejemplo de CameraX