API de varias cámaras

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

La función de varias cámaras se introdujo con Android 9 (nivel de API 28). Desde su lanzamiento, salieron al mercado dispositivos compatibles con la API. Muchos casos de uso de varias cámaras tienen acoplamiento alto con una configuración de hardware específica. En otras palabras, no todos los casos de uso son compatibles con todos los dispositivos,  por lo que las funciones de varias cámaras son una buena opción para la entrega de funciones en Play.

Estos son algunos casos prácticos típicos:

  • Zoom: Alterna entre cámaras según la región de recorte o la longitud focal deseada.
  • Profundidad: Se usan varias cámaras para crear un mapa de profundidad.
  • Bokeh: Uso de información de profundidad inferida para simular un rango de enfoque estrecho similar a una cámara réflex digital

La diferencia entre cámaras lógicas y físicas

Para comprender la API de varias cámaras, es necesario entender la diferencia entre las cámaras lógicas y las físicas. A modo de referencia, considera un dispositivo con tres cámaras posteriores. En este ejemplo, cada una de las tres cámaras posteriores se considera una cámara física. Una cámara lógica es un grupo de dos o más de esas cámaras físicas. La salida de la cámara lógica puede ser una transmisión que proviene de una de las cámaras físicas subyacentes o una transmisión fusionada que proviene de más de una cámara física subyacente a la vez. De cualquier manera, la capa de abstracción de hardware (HAL) de la cámara controla la transmisión.

Muchos fabricantes de teléfonos desarrollan aplicaciones de cámara propias, que suelen venir preinstaladas en sus dispositivos. Para usar todas las capacidades del hardware, pueden usar APIs ocultas o privadas, o recibir un tratamiento especial de la implementación del controlador al que otras aplicaciones no tienen acceso. Algunos dispositivos implementan el concepto de cámaras lógicas proporcionando un flujo fusionado de marcos de las diferentes cámaras físicas, pero solo a ciertas aplicaciones con privilegios. A menudo, solo una de las cámaras físicas se expone al framework. La situación de los desarrolladores externos antes de Android 9 se ilustra en el siguiente diagrama:

Figura 1: Por lo general, las funciones de la cámara solo están disponibles para las aplicaciones con privilegios.

A partir de Android 9, ya no se permiten las APIs privadas en las apps para Android. Dado que se agregó compatibilidad con varias cámaras en el framework, las prácticas recomendadas de Android recomiendan que los fabricantes de teléfonos expongan una cámara lógica para todas las cámaras físicas que estén orientadas en la misma dirección. A continuación, se muestra lo que los desarrolladores externos deberían ver en los dispositivos con Android 9 y versiones posteriores:

Figura 2: Acceso completo para desarrolladores a todos los dispositivos de cámara a partir de Android 9

Lo que proporciona la cámara lógica depende por completo de la implementación del OEM de la HAL de la cámara. Por ejemplo, un dispositivo como Pixel 3 implementa su cámara lógica de manera que elige una de sus cámaras físicas en función de la longitud focal y la región de recorte solicitadas.

API de varias cámaras

La nueva API agrega las siguientes constantes, clases y métodos nuevos:

Debido a los cambios en el Documento de definición de compatibilidad de Android (CDD), la API de varias cámaras también incluye ciertas expectativas de los desarrolladores. Los dispositivos con dos cámaras existían antes de Android 9, pero abrir más de una cámara implicaba ensayo y error de forma simultánea. En Android 9 y versiones posteriores, la función de varias cámaras proporciona un conjunto de reglas para especificar cuándo es posible abrir un par de cámaras físicas que forman parte de la misma cámara lógica.

En la mayoría de los casos, los dispositivos con Android 9 y versiones posteriores exponen todas las cámaras físicas (excepto posiblemente para los tipos de sensores menos comunes, como los infrarrojos), junto con una cámara lógica más fácil de usar. Para cada combinación de transmisiones que tenga garantía de funcionar, se puede reemplazar una que pertenezca a una cámara lógica por dos transmisiones de las cámaras físicas subyacentes.

Varias transmisiones a la vez

El uso de varias transmisiones de cámara a la vez abarca las reglas para usar varias transmisiones de forma simultánea en una sola cámara. Con una adición notable, se aplican las mismas reglas para varias cámaras. CameraMetadata.REQUEST_AVAILABLE_CAPABILITIES_LOGICAL_MULTI_CAMERA explica cómo reemplazar un YUV_420_888 lógico o una transmisión sin procesar por dos transmisiones físicas. Es decir, cada transmisión de tipo YUV o RAW se puede reemplazar por dos transmisiones de tipo y tamaño idénticos. Puedes comenzar con una transmisión de cámara con la siguiente configuración garantizada para dispositivos de una sola cámara:

  • Transmisión 1: tipo YUV, tamaño MAXIMUM de la cámara lógica id = 0

Luego, un dispositivo compatible con varias cámaras te permite crear una sesión que reemplace esa transmisión YUV lógica por dos transmisiones físicas:

  • Transmisión 1: tipo YUV, MAXIMUM tamaño de la cámara física id = 1
  • Transmisión 2: tipo YUV, MAXIMUM de tamaño de la cámara física id = 2

Puedes reemplazar una transmisión YUV o RAW por dos transmisiones equivalentes solo si esas dos cámaras forman parte de una agrupación lógica de cámaras,  que aparece en CameraCharacteristics.getPhysicalCameraIds().

Las garantías que proporciona el framework son solo el mínimo requerido para obtener fotogramas de más de una cámara física a la vez. La mayoría de los dispositivos admiten transmisiones adicionales, que a veces permiten abrir varios dispositivos de cámara físicos de forma independiente. Dado que no es una garantía estricta del framework, hacerlo requiere realizar pruebas y ajustes por dispositivo con prueba y error.

Cómo crear una sesión con varias cámaras físicas

Cuando uses cámaras físicas en un dispositivo compatible con varias cámaras, abre una sola CameraDevice (la cámara lógica) y, luego, interactúa con ella en una sola sesión. Crea la sesión única con la API CameraDevice.createCaptureSession(SessionConfiguration config), que se agregó en el nivel de API 28. La configuración de la sesión tiene varias configuraciones de salida, cada una de las cuales tiene un conjunto de objetivos de salida y, de forma opcional, un ID de cámara física deseado.

Figura 3: Modelos SessionConfiguration y OutputConfiguration

Las solicitudes de captura tienen un destino de salida asociado. El framework determina a qué cámara física (o lógica) se envían las solicitudes en función del destino de salida que se adjunta. Si el objetivo de salida corresponde a uno de los destinos que se envió como una configuración de salida junto con un ID de cámara física, esa cámara física recibe y procesa la solicitud.

Usar un par de cámaras físicas

Otra incorporación de las APIs de cámara para varias cámaras es la capacidad de identificar cámaras lógicas y encontrar las cámaras físicas detrás de ellas. Puedes definir una función que te ayude a identificar pares potenciales de cámaras físicas que puedes usar para reemplazar una de las transmisiones lógicas de la cámara:

Kotlin

/**
     * Helper class used to encapsulate a logical camera and two underlying
     * physical cameras
     */
    data class DualCamera(val logicalId: String, val physicalId1: String, val physicalId2: String)

    fun findDualCameras(manager: CameraManager, facing: Int? = null): List {
        val dualCameras = MutableList()

        // Iterate over all the available camera characteristics
        manager.cameraIdList.map {
            Pair(manager.getCameraCharacteristics(it), it)
        }.filter {
            // Filter by cameras facing the requested direction
            facing == null || it.first.get(CameraCharacteristics.LENS_FACING) == facing
        }.filter {
            // Filter by logical cameras
            // CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_LOGICAL_MULTI_CAMERA requires API >= 28
            it.first.get(CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES)!!.contains(
                CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_LOGICAL_MULTI_CAMERA)
        }.forEach {
            // All possible pairs from the list of physical cameras are valid results
            // NOTE: There could be N physical cameras as part of a logical camera grouping
            // getPhysicalCameraIds() requires API >= 28
            val physicalCameras = it.first.physicalCameraIds.toTypedArray()
            for (idx1 in 0 until physicalCameras.size) {
                for (idx2 in (idx1 + 1) until physicalCameras.size) {
                    dualCameras.add(DualCamera(
                        it.second, physicalCameras[idx1], physicalCameras[idx2]))
                }
            }
        }

        return dualCameras
    }

Java

/**
     * Helper class used to encapsulate a logical camera and two underlying
     * physical cameras
     */
    final class DualCamera {
        final String logicalId;
        final String physicalId1;
        final String physicalId2;

        DualCamera(String logicalId, String physicalId1, String physicalId2) {
            this.logicalId = logicalId;
            this.physicalId1 = physicalId1;
            this.physicalId2 = physicalId2;
        }
    }
    List findDualCameras(CameraManager manager, Integer facing) {
        List dualCameras = new ArrayList<>();

        List cameraIdList;
        try {
            cameraIdList = Arrays.asList(manager.getCameraIdList());
        } catch (CameraAccessException e) {
            e.printStackTrace();
            cameraIdList = new ArrayList<>();
        }

        // Iterate over all the available camera characteristics
        cameraIdList.stream()
                .map(id -> {
                    try {
                        CameraCharacteristics characteristics = manager.getCameraCharacteristics(id);
                        return new Pair<>(characteristics, id);
                    } catch (CameraAccessException e) {
                        e.printStackTrace();
                        return null;
                    }
                })
                .filter(pair -> {
                    // Filter by cameras facing the requested direction
                    return (pair != null) &&
                            (facing == null || pair.first.get(CameraCharacteristics.LENS_FACING).equals(facing));
                })
                .filter(pair -> {
                    // Filter by logical cameras
                    // CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_LOGICAL_MULTI_CAMERA requires API >= 28
                    IntPredicate logicalMultiCameraPred =
                            arg -> arg == CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_LOGICAL_MULTI_CAMERA;
                    return Arrays.stream(pair.first.get(CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES))
                            .anyMatch(logicalMultiCameraPred);
                })
                .forEach(pair -> {
                    // All possible pairs from the list of physical cameras are valid results
                    // NOTE: There could be N physical cameras as part of a logical camera grouping
                    // getPhysicalCameraIds() requires API >= 28
                    String[] physicalCameras = pair.first.getPhysicalCameraIds().toArray(new String[0]);
                    for (int idx1 = 0; idx1 < physicalCameras.length; idx1++) {
                        for (int idx2 = idx1 + 1; idx2 < physicalCameras.length; idx2++) {
                            dualCameras.add(
                                    new DualCamera(pair.second, physicalCameras[idx1], physicalCameras[idx2]));
                        }
                    }
                });
return dualCameras;
}

La cámara lógica controla el control del estado de las cámaras físicas. Para abrir una "cámara dual", abre la cámara lógica correspondiente a las cámaras físicas:

Kotlin

fun openDualCamera(cameraManager: CameraManager,
                       dualCamera: DualCamera,
        // AsyncTask is deprecated beginning API 30
                       executor: Executor = AsyncTask.SERIAL_EXECUTOR,
                       callback: (CameraDevice) -> Unit) {

        // openCamera() requires API >= 28
        cameraManager.openCamera(
            dualCamera.logicalId, executor, object : CameraDevice.StateCallback() {
                override fun onOpened(device: CameraDevice) = callback(device)
                // Omitting for brevity...
                override fun onError(device: CameraDevice, error: Int) = onDisconnected(device)
                override fun onDisconnected(device: CameraDevice) = device.close()
            })
    }

Java

void openDualCamera(CameraManager cameraManager,
                        DualCamera dualCamera,
                        Executor executor,
                        CameraDeviceCallback cameraDeviceCallback
    ) {

        // openCamera() requires API >= 28
        cameraManager.openCamera(dualCamera.logicalId, executor, new CameraDevice.StateCallback() {
            @Override
            public void onOpened(@NonNull CameraDevice cameraDevice) {
               cameraDeviceCallback.callback(cameraDevice);
            }

            @Override
            public void onDisconnected(@NonNull CameraDevice cameraDevice) {
                cameraDevice.close();
            }

            @Override
            public void onError(@NonNull CameraDevice cameraDevice, int i) {
                onDisconnected(cameraDevice);
            }
        });
    }

Además de seleccionar qué cámara abrir, el proceso es el mismo que el de abrir una en versiones anteriores de Android. Cuando se crea una sesión de captura con la nueva API de configuración de sesiones, se le indica al framework que asocie ciertos objetivos con IDs de cámaras físicas específicos:

Kotlin

/**
 * Helper type definition that encapsulates 3 sets of output targets:
 *
 *   1. Logical camera
 *   2. First physical camera
 *   3. Second physical camera
 */
typealias DualCameraOutputs =
        Triple?, MutableList?, MutableList?>

fun createDualCameraSession(cameraManager: CameraManager,
                            dualCamera: DualCamera,
                            targets: DualCameraOutputs,
                            // AsyncTask is deprecated beginning API 30
                            executor: Executor = AsyncTask.SERIAL_EXECUTOR,
                            callback: (CameraCaptureSession) -> Unit) {

    // Create 3 sets of output configurations: one for the logical camera, and
    // one for each of the physical cameras.
    val outputConfigsLogical = targets.first?.map { OutputConfiguration(it) }
    val outputConfigsPhysical1 = targets.second?.map {
        OutputConfiguration(it).apply { setPhysicalCameraId(dualCamera.physicalId1) } }
    val outputConfigsPhysical2 = targets.third?.map {
        OutputConfiguration(it).apply { setPhysicalCameraId(dualCamera.physicalId2) } }

    // Put all the output configurations into a single flat array
    val outputConfigsAll = arrayOf(
        outputConfigsLogical, outputConfigsPhysical1, outputConfigsPhysical2)
        .filterNotNull().flatMap { it }

    // Instantiate a session configuration that can be used to create a session
    val sessionConfiguration = SessionConfiguration(
        SessionConfiguration.SESSION_REGULAR,
        outputConfigsAll, executor, object : CameraCaptureSession.StateCallback() {
            override fun onConfigured(session: CameraCaptureSession) = callback(session)
            // Omitting for brevity...
            override fun onConfigureFailed(session: CameraCaptureSession) = session.device.close()
        })

    // Open the logical camera using the previously defined function
    openDualCamera(cameraManager, dualCamera, executor = executor) {

        // Finally create the session and return via callback
        it.createCaptureSession(sessionConfiguration)
    }
}

Java

/**
 * Helper class definition that encapsulates 3 sets of output targets:
 * 

* 1. Logical camera * 2. First physical camera * 3. Second physical camera */ final class DualCameraOutputs { private final List logicalCamera; private final List firstPhysicalCamera; private final List secondPhysicalCamera; public DualCameraOutputs(List logicalCamera, List firstPhysicalCamera, List third) { this.logicalCamera = logicalCamera; this.firstPhysicalCamera = firstPhysicalCamera; this.secondPhysicalCamera = third; } public List getLogicalCamera() { return logicalCamera; } public List getFirstPhysicalCamera() { return firstPhysicalCamera; } public List getSecondPhysicalCamera() { return secondPhysicalCamera; } } interface CameraCaptureSessionCallback { void callback(CameraCaptureSession cameraCaptureSession); } void createDualCameraSession(CameraManager cameraManager, DualCamera dualCamera, DualCameraOutputs targets, Executor executor, CameraCaptureSessionCallback cameraCaptureSessionCallback) { // Create 3 sets of output configurations: one for the logical camera, and // one for each of the physical cameras. List outputConfigsLogical = targets.getLogicalCamera().stream() .map(OutputConfiguration::new) .collect(Collectors.toList()); List outputConfigsPhysical1 = targets.getFirstPhysicalCamera().stream() .map(s -> { OutputConfiguration outputConfiguration = new OutputConfiguration(s); outputConfiguration.setPhysicalCameraId(dualCamera.physicalId1); return outputConfiguration; }) .collect(Collectors.toList()); List outputConfigsPhysical2 = targets.getSecondPhysicalCamera().stream() .map(s -> { OutputConfiguration outputConfiguration = new OutputConfiguration(s); outputConfiguration.setPhysicalCameraId(dualCamera.physicalId2); return outputConfiguration; }) .collect(Collectors.toList()); // Put all the output configurations into a single flat array List outputConfigsAll = Stream.of( outputConfigsLogical, outputConfigsPhysical1, outputConfigsPhysical2 ) .filter(Objects::nonNull) .flatMap(Collection::stream) .collect(Collectors.toList()); // Instantiate a session configuration that can be used to create a session SessionConfiguration sessionConfiguration = new SessionConfiguration( SessionConfiguration.SESSION_REGULAR, outputConfigsAll, executor, new CameraCaptureSession.StateCallback() { @Override public void onConfigured(@NonNull CameraCaptureSession cameraCaptureSession) { cameraCaptureSessionCallback.callback(cameraCaptureSession); } // Omitting for brevity... @Override public void onConfigureFailed(@NonNull CameraCaptureSession cameraCaptureSession) { cameraCaptureSession.getDevice().close(); } }); // Open the logical camera using the previously defined function openDualCamera(cameraManager, dualCamera, executor, (CameraDevice c) -> // Finally create the session and return via callback c.createCaptureSession(sessionConfiguration)); }

Consulta createCaptureSession para obtener información sobre la combinación de transmisiones compatible. La combinación de transmisiones sirve para varias transmisiones en una sola cámara lógica. La compatibilidad se extiende al uso de la misma configuración y al reemplazo de una de esas transmisiones por dos transmisiones de dos cámaras físicas que forman parte de la misma cámara lógica.

Con la sesión de cámara lista, envía las solicitudes de captura deseadas. Cada objetivo de la solicitud de captura recibe sus datos de la cámara física asociada, si hay alguno en uso, o recurre a la cámara lógica.

Caso de uso de ejemplo de Zoom

Es posible usar la combinación de cámaras físicas en una sola transmisión para que los usuarios puedan cambiar entre las diferentes cámaras físicas y experimentar un campo visual diferente, y capturar de manera efectiva un "nivel de zoom" diferente.

Figura 4: Ejemplo de intercambio de cámaras para un caso de uso de nivel de zoom (desde un anuncio de Pixel 3)

Primero, selecciona el par de cámaras físicas para permitir que los usuarios cambien entre ellas. Para obtener el máximo efecto, puedes elegir el par de cámaras que proporcionan la longitud focal mínima y máxima disponible.

Kotlin

fun findShortLongCameraPair(manager: CameraManager, facing: Int? = null): DualCamera? {

    return findDualCameras(manager, facing).map {
        val characteristics1 = manager.getCameraCharacteristics(it.physicalId1)
        val characteristics2 = manager.getCameraCharacteristics(it.physicalId2)

        // Query the focal lengths advertised by each physical camera
        val focalLengths1 = characteristics1.get(
            CameraCharacteristics.LENS_INFO_AVAILABLE_FOCAL_LENGTHS) ?: floatArrayOf(0F)
        val focalLengths2 = characteristics2.get(
            CameraCharacteristics.LENS_INFO_AVAILABLE_FOCAL_LENGTHS) ?: floatArrayOf(0F)

        // Compute the largest difference between min and max focal lengths between cameras
        val focalLengthsDiff1 = focalLengths2.maxOrNull()!! - focalLengths1.minOrNull()!!
        val focalLengthsDiff2 = focalLengths1.maxOrNull()!! - focalLengths2.minOrNull()!!

        // Return the pair of camera IDs and the difference between min and max focal lengths
        if (focalLengthsDiff1 < focalLengthsDiff2) {
            Pair(DualCamera(it.logicalId, it.physicalId1, it.physicalId2), focalLengthsDiff1)
        } else {
            Pair(DualCamera(it.logicalId, it.physicalId2, it.physicalId1), focalLengthsDiff2)
        }

        // Return only the pair with the largest difference, or null if no pairs are found
    }.maxByOrNull { it.second }?.first
}

Java

// Utility functions to find min/max value in float[]
    float findMax(float[] array) {
        float max = Float.NEGATIVE_INFINITY;
        for(float cur: array)
            max = Math.max(max, cur);
        return max;
    }
    float findMin(float[] array) {
        float min = Float.NEGATIVE_INFINITY;
        for(float cur: array)
            min = Math.min(min, cur);
        return min;
    }

DualCamera findShortLongCameraPair(CameraManager manager, Integer facing) {
        return findDualCameras(manager, facing).stream()
                .map(c -> {
                    CameraCharacteristics characteristics1;
                    CameraCharacteristics characteristics2;
                    try {
                        characteristics1 = manager.getCameraCharacteristics(c.physicalId1);
                        characteristics2 = manager.getCameraCharacteristics(c.physicalId2);
                    } catch (CameraAccessException e) {
                        e.printStackTrace();
                        return null;
                    }

                    // Query the focal lengths advertised by each physical camera
                    float[] focalLengths1 = characteristics1.get(
                            CameraCharacteristics.LENS_INFO_AVAILABLE_FOCAL_LENGTHS);
                    float[] focalLengths2 = characteristics2.get(
                            CameraCharacteristics.LENS_INFO_AVAILABLE_FOCAL_LENGTHS);

                    // Compute the largest difference between min and max focal lengths between cameras
                    Float focalLengthsDiff1 = findMax(focalLengths2) - findMin(focalLengths1);
                    Float focalLengthsDiff2 = findMax(focalLengths1) - findMin(focalLengths2);

                    // Return the pair of camera IDs and the difference between min and max focal lengths
                    if (focalLengthsDiff1 < focalLengthsDiff2) {
                        return new Pair<>(new DualCamera(c.logicalId, c.physicalId1, c.physicalId2), focalLengthsDiff1);
                    } else {
                        return new Pair<>(new DualCamera(c.logicalId, c.physicalId2, c.physicalId1), focalLengthsDiff2);
                    }

                }) // Return only the pair with the largest difference, or null if no pairs are found
                .max(Comparator.comparing(pair -> pair.second)).get().first;
    }

Una arquitectura razonable para esto sería tener dos SurfaceViews, uno para cada transmisión. Estos SurfaceViews se intercambian según la interacción del usuario, de modo que solo uno sea visible en un momento determinado.

En el siguiente código, se muestra cómo abrir la cámara lógica, configurar los resultados de la cámara, crear una sesión de la cámara e iniciar dos transmisiones de vista previa:

Kotlin

val cameraManager: CameraManager = ...

// Get the two output targets from the activity / fragment
val surface1 = ...  // from SurfaceView
val surface2 = ...  // from SurfaceView

val dualCamera = findShortLongCameraPair(manager)!!
val outputTargets = DualCameraOutputs(
    null, mutableListOf(surface1), mutableListOf(surface2))

// Here you open the logical camera, configure the outputs and create a session
createDualCameraSession(manager, dualCamera, targets = outputTargets) { session ->

  // Create a single request which has one target for each physical camera
  // NOTE: Each target receive frames from only its associated physical camera
  val requestTemplate = CameraDevice.TEMPLATE_PREVIEW
  val captureRequest = session.device.createCaptureRequest(requestTemplate).apply {
    arrayOf(surface1, surface2).forEach { addTarget(it) }
  }.build()

  // Set the sticky request for the session and you are done
  session.setRepeatingRequest(captureRequest, null, null)
}

Java

CameraManager manager = ...;

        // Get the two output targets from the activity / fragment
        Surface surface1 = ...;  // from SurfaceView
        Surface surface2 = ...;  // from SurfaceView

        DualCamera dualCamera = findShortLongCameraPair(manager, null);
                DualCameraOutputs outputTargets = new DualCameraOutputs(
                null, Collections.singletonList(surface1), Collections.singletonList(surface2));

        // Here you open the logical camera, configure the outputs and create a session
        createDualCameraSession(manager, dualCamera, outputTargets, null, (session) -> {
            // Create a single request which has one target for each physical camera
            // NOTE: Each target receive frames from only its associated physical camera
            CaptureRequest.Builder captureRequestBuilder;
            try {
                captureRequestBuilder = session.getDevice().createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);
                Arrays.asList(surface1, surface2).forEach(captureRequestBuilder::addTarget);

                // Set the sticky request for the session and you are done
                session.setRepeatingRequest(captureRequestBuilder.build(), null, null);
            } catch (CameraAccessException e) {
                e.printStackTrace();
            }
        });

Lo único que queda por hacer es proporcionar una IU para que el usuario pueda alternar entre las dos plataformas, como un botón o presionar dos veces la SurfaceView. Incluso puedes realizar algún tipo de análisis de escena y alternar automáticamente entre las dos transmisiones.

Distorsión del lente

Todos los lentes producen una determinada cantidad de distorsión. En Android, puedes consultar la distorsión creada por las lentes con CameraCharacteristics.LENS_DISTORTION, que reemplaza el elemento CameraCharacteristics.LENS_RADIAL_DISTORTION obsoleto. En el caso de las cámaras lógicas, la distorsión es mínima y tu aplicación puede usar los fotogramas más o menos a medida que provienen de la cámara. En el caso de las cámaras físicas, las configuraciones de los lentes pueden ser muy diferentes, en especial, con lentes gran angular.

Algunos dispositivos pueden implementar la corrección de distorsión automática mediante CaptureRequest.DISTORTION_CORRECTION_MODE. La corrección de distorsión estará activada de forma predeterminada en la mayoría de los dispositivos.

Kotlin

val cameraSession: CameraCaptureSession = ...

        // Use still capture template to build the capture request
        val captureRequest = cameraSession.device.createCaptureRequest(
            CameraDevice.TEMPLATE_STILL_CAPTURE
        )

        // Determine if this device supports distortion correction
        val characteristics: CameraCharacteristics = ...
        val supportsDistortionCorrection = characteristics.get(
            CameraCharacteristics.DISTORTION_CORRECTION_AVAILABLE_MODES
        )?.contains(
            CameraMetadata.DISTORTION_CORRECTION_MODE_HIGH_QUALITY
        ) ?: false

        if (supportsDistortionCorrection) {
            captureRequest.set(
                CaptureRequest.DISTORTION_CORRECTION_MODE,
                CameraMetadata.DISTORTION_CORRECTION_MODE_HIGH_QUALITY
            )
        }

        // Add output target, set other capture request parameters...

        // Dispatch the capture request
        cameraSession.capture(captureRequest.build(), ...)

Java

CameraCaptureSession cameraSession = ...;

        // Use still capture template to build the capture request
        CaptureRequest.Builder captureRequestBuilder = null;
        try {
            captureRequestBuilder = cameraSession.getDevice().createCaptureRequest(
                    CameraDevice.TEMPLATE_STILL_CAPTURE
            );
        } catch (CameraAccessException e) {
            e.printStackTrace();
        }

        // Determine if this device supports distortion correction
        CameraCharacteristics characteristics = ...;
        boolean supportsDistortionCorrection = Arrays.stream(
                        characteristics.get(
                                CameraCharacteristics.DISTORTION_CORRECTION_AVAILABLE_MODES
                        ))
                .anyMatch(i -> i == CameraMetadata.DISTORTION_CORRECTION_MODE_HIGH_QUALITY);
        if (supportsDistortionCorrection) {
            captureRequestBuilder.set(
                    CaptureRequest.DISTORTION_CORRECTION_MODE,
                    CameraMetadata.DISTORTION_CORRECTION_MODE_HIGH_QUALITY
            );
        }

        // Add output target, set other capture request parameters...

        // Dispatch the capture request
        cameraSession.capture(captureRequestBuilder.build(), ...);

Configurar una solicitud de captura en este modo puede afectar la velocidad de fotogramas que produce la cámara. Puedes elegir configurar la corrección de distorsión solo en capturas de imágenes estáticas.