API multi-caméra

Remarque : Cette page fait référence au package Camera2. Sauf si votre application nécessite des fonctionnalités spécifiques de bas niveau de Camera2, nous vous recommandons d'utiliser CameraX. CameraX et Camera2 sont compatibles avec Android 5.0 (niveau d'API 21) ou version ultérieure.

La fonctionnalité multicaméra a été introduite avec Android 9 (niveau d'API 28). Depuis sa sortie, des appareils compatibles avec l'API ont été commercialisés. De nombreux cas d'utilisation de plusieurs caméras sont étroitement liés à une configuration matérielle spécifique. En d'autres termes, tous les cas d'utilisation ne sont pas compatibles avec tous les appareils. Les fonctionnalités multicaméras sont donc de bons candidats pour Play Feature Delivery.

Voici certains cas d'utilisation types :

  • Zoom : bascule entre les caméras en fonction de la zone de recadrage ou de la longueur focale choisie.
  • Profondeur : utilisation de plusieurs caméras pour créer une carte de profondeur.
  • Bokeh : utilisation des informations de profondeur déduites pour simuler une plage de mise au point étroite semblable à celle d'un reflex numérique.

Différence entre les caméras logiques et physiques

Pour comprendre l'API multicaméra, vous devez comprendre la différence entre les caméras logiques et physiques. À titre de référence, prenons l'exemple d'un appareil doté de trois caméras arrière. Dans cet exemple, chacune des trois caméras arrière est considérée comme une caméra physique. Une caméra logique est un regroupement d'au moins deux de ces caméras physiques. La sortie de la caméra logique peut être un flux provenant de l'une des caméras physiques sous-jacentes ou un flux fusionné provenant de plusieurs caméras physiques sous-jacentes simultanément. Dans les deux cas, le flux est géré par la couche d'abstraction matérielle (HAL) de l'appareil photo.

De nombreux fabricants de téléphones développent des applications d'appareil photo propriétaires, qui sont généralement préinstallées sur leurs appareils. Pour utiliser toutes les fonctionnalités du matériel, elles peuvent utiliser des API privées ou masquées, ou bénéficier d'un traitement spécial de la part de l'implémentation du pilote auquel les autres applications n'ont pas accès. Certains appareils implémentent le concept de caméras logiques en fournissant un flux fusionné d'images provenant des différentes caméras physiques, mais uniquement à certaines applications privilégiées. Souvent, une seule des caméras physiques est exposée au framework. La situation des développeurs tiers avant Android 9 est illustrée dans le schéma suivant :

Fonctionnalités de l'appareil photo généralement disponibles uniquement pour les applications privilégiées
Figure 1 Fonctionnalités de l'appareil photo généralement disponibles uniquement pour les applications privilégiées

À partir d'Android 9, les API privées ne sont plus autorisées dans les applications Android. Avec l'inclusion de la compatibilité multi-caméra dans le framework, les bonnes pratiques Android recommandent vivement aux fabricants de téléphones d'exposer une caméra logique pour toutes les caméras physiques orientées dans la même direction. Voici ce que les développeurs tiers doivent s'attendre à voir sur les appareils équipés d'Android 9 ou version ultérieure :

Accès complet des développeurs à tous les appareils photo à partir d'Android 9
Figure 2 Accès complet des développeurs à tous les appareils photo à partir d'Android 9

Ce que fournit l'appareil photo logique dépend entièrement de l'implémentation OEM de la HAL de l'appareil photo. Par exemple, un appareil comme le Pixel 3 implémente sa caméra logique de manière à choisir l'une de ses caméras physiques en fonction de la longueur focale et de la zone de recadrage demandées.

API Multi-Camera

La nouvelle API ajoute les constantes, classes et méthodes suivantes :

En raison des modifications apportées au document de définition de compatibilité Android (CDD), l'API multi-caméras s'accompagne également de certaines attentes de la part des développeurs. Les appareils dotés de double caméra existaient avant Android 9, mais l'ouverture de plusieurs caméras simultanément impliquait des essais et des erreurs. Sur Android 9 ou version ultérieure, la fonctionnalité multicaméra fournit un ensemble de règles permettant de spécifier quand il est possible d'ouvrir une paire de caméras physiques faisant partie de la même caméra logique.

Dans la plupart des cas, les appareils équipés d'Android 9 ou version ultérieure exposent toutes les caméras physiques (à l'exception possible des types de capteurs moins courants tels que l'infrarouge) ainsi qu'une caméra logique plus facile à utiliser. Pour chaque combinaison de flux dont le fonctionnement est garanti, un flux appartenant à une caméra logique peut être remplacé par deux flux provenant des caméras physiques sous-jacentes.

Plusieurs flux simultanément

La section Utiliser plusieurs flux d'appareil photo simultanément décrit les règles d'utilisation de plusieurs flux simultanément dans un même appareil photo. À une exception près, les mêmes règles s'appliquent à plusieurs caméras. CameraMetadata.REQUEST_AVAILABLE_CAPABILITIES_LOGICAL_MULTI_CAMERA explique comment remplacer un flux logique YUV_420_888 ou brut par deux flux physiques. Autrement dit, chaque flux de type YUV ou RAW peut être remplacé par deux flux de type et de taille identiques. Vous pouvez commencer par un flux de caméra de la configuration garantie suivante pour les appareils à caméra unique :

  • Flux 1 : type YUV, taille MAXIMUM de la caméra logique id = 0

Ensuite, un appareil compatible avec plusieurs caméras vous permet de créer une session en remplaçant ce flux YUV logique par deux flux physiques :

  • Flux 1 : type YUV, taille MAXIMUM de la caméra physique id = 1
  • Flux 2 : type YUV, taille MAXIMUM de la caméra physique id = 2

Vous pouvez remplacer un flux YUV ou RAW par deux flux équivalents si et seulement si ces deux caméras font partie d'un regroupement de caméras logiques,  qui est listé sous CameraCharacteristics.getPhysicalCameraIds().

Les garanties fournies par le framework sont le strict minimum requis pour obtenir des images de plusieurs caméras physiques simultanément. Les flux supplémentaires sont compatibles avec la plupart des appareils, ce qui permet parfois d'ouvrir plusieurs appareils photo physiques indépendamment. Comme il ne s'agit pas d'une garantie stricte du framework, cela nécessite d'effectuer des tests et des réglages par appareil par tâtonnement.

Créer une session avec plusieurs caméras physiques

Lorsque vous utilisez des caméras physiques sur un appareil multicaméra, ouvrez une seule CameraDevice (la caméra logique) et interagissez avec elle au cours d'une même session. Créez la session unique à l'aide de l'API CameraDevice.createCaptureSession(SessionConfiguration config), qui a été ajoutée au niveau d'API 28. La configuration de session comporte un certain nombre de configurations de sortie, chacune ayant un ensemble de cibles de sortie et, éventuellement, un ID d'appareil photo physique choisi.

Modèle SessionConfiguration et OutputConfiguration
Figure 3 Modèle SessionConfiguration et OutputConfiguration

Les requêtes de capture sont associées à une cible de sortie. Le framework détermine à quelle caméra physique (ou logique) les requêtes sont envoyées en fonction de la cible de sortie associée. Si la cible de sortie correspond à l'une des cibles de sortie envoyées en tant que configuration de sortie avec un ID de caméra physique, cette caméra physique reçoit et traite la requête.

Utilisation de deux caméras physiques

Une autre nouveauté des API d'appareil photo pour les appareils photo multiples est la possibilité d'identifier les appareils photo logiques et de trouver les appareils photo physiques qui se cachent derrière eux. Vous pouvez définir une fonction pour identifier les paires potentielles de caméras physiques que vous pouvez utiliser pour remplacer l'un des flux de caméra logique :

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 gestion de l'état des caméras physiques est contrôlée par la caméra logique. Pour ouvrir une "double caméra", ouvrez la caméra logique correspondant aux caméras physiques :

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);
            }
        });
    }

En plus de sélectionner la caméra à ouvrir, le processus est le même que pour ouvrir une caméra dans les versions précédentes d'Android. La création d'une session de capture à l'aide de la nouvelle API de configuration de session indique au framework d'associer certaines cibles à des ID de caméra physique spécifiques :

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));
}

Consultez createCaptureSession pour savoir quelles combinaisons de flux sont acceptées. La combinaison de flux est destinée à plusieurs flux sur une seule caméra logique. La compatibilité s'étend à l'utilisation de la même configuration et au remplacement de l'un de ces flux par deux flux provenant de deux caméras physiques faisant partie de la même caméra logique.

Une fois la session de l'appareil photo prête, envoyez les requêtes de capture choisies. Chaque cible de la requête de capture reçoit ses données de la caméra physique associée, le cas échéant, ou revient à la caméra logique.

Exemple de cas d'utilisation de Zoom

Il est possible de fusionner les caméras physiques en un seul flux afin que les utilisateurs puissent basculer entre les différentes caméras physiques pour profiter d'un champ de vision différent, en capturant efficacement un "niveau de zoom" différent.

Exemple de changement de caméra pour le cas d'utilisation du niveau de zoom
Figure 4. Exemple de changement de caméra pour le cas d'utilisation du niveau de zoom (extrait d'une annonce pour le Pixel 3)

Commencez par sélectionner la paire de caméras physiques entre lesquelles les utilisateurs pourront basculer. Pour un effet maximal, vous pouvez choisir la paire de caméras qui fournit la longueur focale minimale et maximale 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;
    }

Une architecture judicieuse pour cela consisterait à avoir deux SurfaceViews, un pour chaque flux. Ces SurfaceViews sont échangés en fonction de l'interaction de l'utilisateur, de sorte qu'un seul soit visible à la fois.

Le code suivant montre comment ouvrir la caméra logique, configurer les sorties de la caméra, créer une session de caméra et démarrer deux flux d'aperçu :

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();
            }
        });

Il ne vous reste plus qu'à fournir une UI permettant à l'utilisateur de basculer entre les deux surfaces, par exemple un bouton ou un double-clic sur le SurfaceView. Vous pouvez même effectuer une analyse de scène et basculer automatiquement entre les deux flux.

Distorsion de l'objectif

Tous les objectifs produisent une certaine distorsion. Sur Android, vous pouvez interroger la distorsion créée par les objectifs à l'aide de CameraCharacteristics.LENS_DISTORTION, qui remplace CameraCharacteristics.LENS_RADIAL_DISTORTION, désormais obsolète. Pour les caméras logiques, la distorsion est minimale et votre application peut utiliser les images plus ou moins telles qu'elles proviennent de la caméra. Pour les caméras physiques, les configurations d'objectif peuvent être très différentes, en particulier sur les objectifs grand angle.

Certains appareils peuvent implémenter la correction automatique de la distorsion à l'aide de CaptureRequest.DISTORTION_CORRECTION_MODE. La correction de la distorsion est activée par défaut sur la plupart des appareils.

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(), ...);

La définition d'une requête de capture dans ce mode peut avoir un impact sur la fréquence d'images pouvant être produite par la caméra. Vous pouvez choisir de définir la correction de la distorsion uniquement sur les captures d'images fixes.