API de varias cámaras

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.

La función de varias cámaras se introdujo en Android 9 (nivel de API 28). Desde su lanzamiento, se lanzaron al mercado dispositivos que admiten la API. Muchos casos de uso de varias cámaras están estrechamente vinculados a una configuración de hardware específica. En otras palabras, no todos los casos de uso son compatibles con todos los dispositivos,  lo que hace que las funciones de varias cámaras sean una buena opción para Play Feature Delivery.

Estos son algunos casos prácticos típicos:

  • Zoom: Cambia entre cámaras según la región de recorte o la longitud focal elegida.
  • Profundidad: Se usan varias cámaras para crear un mapa de profundidad.
  • Bokeh: Usa información de profundidad inferida para simular una profundidad de campo estrecha similar a la de una DSLR.

La diferencia entre las 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. Como 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, entonces, una agrupación de dos o más de esas cámaras físicas. El resultado de la cámara lógica puede ser una transmisión que provenga de una de las cámaras físicas subyacentes o una transmisión fusionada que provenga de más de una cámara física subyacente de forma simultánea. 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 privadas o escondidas, 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 fotogramas de las diferentes cámaras físicas, pero solo para ciertas aplicaciones privilegiadas. A menudo, solo una de las cámaras físicas se expone al framework. En el siguiente diagrama, se ilustra la situación de los desarrolladores externos antes de Android 9:

Funciones de la cámara que, por lo general, solo están disponibles para las aplicaciones con privilegios
Figura 1: Funciones de la cámara que, por lo general, solo están disponibles para las aplicaciones privilegiadas

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

Acceso completo para desarrolladores a todos los dispositivos de cámara a partir de Android 9
Figura 2: Acceso completo de los 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 de la HAL de la cámara que realice el OEM. Por ejemplo, un dispositivo como Pixel 3 implementa su cámara lógica de tal manera que elige una de sus cámaras físicas según la longitud focal y la región de recorte solicitadas.

La API de varias cámaras

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

Debido a los cambios en el Documento de definición de compatibilidad (CDD) de Android, la API de varias cámaras también incluye ciertas expectativas de los desarrolladores. Los dispositivos con cámaras duales existían antes de Android 9, pero abrir más de una cámara de forma simultánea implicaba un proceso de prueba y error. 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 que ejecutan Android 9 y versiones posteriores exponen todas las cámaras físicas (excepto, posiblemente, los tipos de sensores menos comunes, como el infrarrojo) junto con una cámara lógica más fácil de usar. Para cada combinación de transmisiones que se garantiza que funcionarán, una transmisión que pertenezca a una cámara lógica se puede reemplazar por dos transmisiones de las cámaras físicas subyacentes.

Múltiples transmisiones simultáneas

En Cómo usar varias transmisiones de cámara a la vez, se explican 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 una transmisión lógica YUV_420_888 o 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 la cámara con la siguiente configuración garantizada para dispositivos con una sola cámara:

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

Luego, un dispositivo con compatibilidad 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, tamaño MAXIMUM de la cámara física id = 1
  • Transmisión 2: Tipo YUV, tamaño MAXIMUM de la cámara física id = 2

Puedes reemplazar un flujo de YUV o RAW por dos flujos equivalentes solo si esas dos cámaras forman parte de una agrupación de cámaras lógicas,  que se indica en CameraCharacteristics.getPhysicalCameraIds().

Las garantías que proporciona el framework son solo el mínimo necesario para obtener fotogramas de más de una cámara física de forma simultánea. La mayoría de los dispositivos admiten transmisiones adicionales, lo que, a veces, incluso permite abrir varios dispositivos de cámara físicos de forma independiente. Como no es una garantía estricta del framework, para hacerlo, se deben realizar pruebas y ajustes por dispositivo mediante prueba y error.

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

Cuando uses cámaras físicas en un dispositivo 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 una serie de parámetros de configuración de salida, cada uno de los cuales tiene un conjunto de destinos de salida y, de manera opcional, un ID de cámara física elegido.

Modelo SessionConfiguration y OutputConfiguration
Figura 3: Modelo 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 según el destino de salida que se adjunte. Si el destino de salida corresponde a uno de los destinos de salida que se enviaron 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.

Usa un par de cámaras físicas

Otra incorporación a 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 posibles pares de cámaras físicas que puedes usar para reemplazar una de las transmisiones de la cámara lógica:

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<DualCamera> {
    val dualCameras = MutableList<DualCamera>()

    // 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<DualCamera> findDualCameras(CameraManager manager, Integer facing) {
    List<DualCamera> dualCameras = new ArrayList<>();

    List<String> 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 manejo de 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 para abrir una cámara en versiones anteriores de Android. Cuando se crea una sesión de captura con la nueva API de configuración de sesión, se le indica al framework que asocie ciertos destinos 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<Surface>?, MutableList<Surface>?, MutableList<Surface>?>

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 using the callback
        it.createCaptureSession(sessionConfiguration)
    }
}

Java

/**
 * Helper class definition that encapsulates 3 sets of output targets:
 * <p>
 * 1. Logical camera
 * 2. First physical camera
 * 3. Second physical camera
 */
final class DualCameraOutputs {
    private final List<Surface> logicalCamera;
    private final List<Surface> firstPhysicalCamera;
    private final List<Surface> secondPhysicalCamera;

    public DualCameraOutputs(List<Surface> logicalCamera, List<Surface> firstPhysicalCamera, List<Surface> third) {
        this.logicalCamera = logicalCamera;
        this.firstPhysicalCamera = firstPhysicalCamera;
        this.secondPhysicalCamera = third;
    }

    public List<Surface> getLogicalCamera() {
        return logicalCamera;
    }

    public List<Surface> getFirstPhysicalCamera() {
        return firstPhysicalCamera;
    }

    public List<Surface> 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<OutputConfiguration> outputConfigsLogical = targets.getLogicalCamera().stream()
            .map(OutputConfiguration::new)
            .collect(Collectors.toList());
    List<OutputConfiguration> outputConfigsPhysical1 = targets.getFirstPhysicalCamera().stream()
            .map(s -> {
                OutputConfiguration outputConfiguration = new OutputConfiguration(s);
                outputConfiguration.setPhysicalCameraId(dualCamera.physicalId1);
                return outputConfiguration;
            })
            .collect(Collectors.toList());
    List<OutputConfiguration> 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<OutputConfiguration> 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 using the callback
            c.createCaptureSession(sessionConfiguration));
}

Consulta createCaptureSession para obtener información sobre qué combinación de transmisiones se admite. La combinación de transmisiones es 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 uno de esos flujos por dos flujos de dos cámaras físicas que forman parte de la misma cámara lógica.

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

Ejemplo de caso de uso de Zoom

Es posible fusionar las 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, lo que captura de manera efectiva un "nivel de zoom" diferente.

Ejemplo de intercambio de cámaras para el caso de uso de nivel de zoom
Figura 4. Ejemplo de cambio de cámaras para el caso de uso de nivel de zoom (del anuncio del Pixel 3)

Para comenzar, selecciona el par de cámaras físicas entre las que los usuarios podrán cambiar. 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 las salidas de la cámara, crear una sesión de cámara y, luego, 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();
            }
        });

Solo queda proporcionar una IU para que el usuario alterne entre las dos superficies, como un botón o presionar dos veces la SurfaceView. Incluso podrías realizar algún tipo de análisis de escena y cambiar entre los dos flujos automáticamente.

Distorsión de la lente

Todos los lentes producen una cierta cantidad de distorsión. En Android, puedes consultar la distorsión que crean las lentes con CameraCharacteristics.LENS_DISTORTION, que reemplaza a CameraCharacteristics.LENS_RADIAL_DISTORTION, que ahora está 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 como provienen de la cámara. En el caso de las cámaras físicas, puede haber configuraciones de lentes muy diferentes, en especial en las lentes gran angular.

Algunos dispositivos pueden implementar la corrección automática de distorsión con CaptureRequest.DISTORTION_CORRECTION_MODE. La corrección de distorsión está 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(), ...);

Establecer una solicitud de captura en este modo puede afectar la velocidad de fotogramas que puede producir la cámara. Puedes optar por establecer la corrección de distorsión solo en las capturas de imágenes fijas.