API multi-camera

Nota: questa pagina fa riferimento al pacchetto Camera2. A meno che la tua app non richieda funzionalità specifiche di basso livello di Camera2, ti consigliamo di utilizzare CameraX. Sia CameraX che Camera2 supportano Android 5.0 (livello API 21) e versioni successive.

La funzionalità multicamera è stata introdotta con Android 9 (livello API 28). Dalla sua uscita, sono stati introdotti sul mercato dispositivi che supportano l'API. Molti casi d'uso con più fotocamere sono strettamente legati a una configurazione hardware specifica. In altre parole, non tutti i casi d'uso sono compatibili con ogni dispositivo,  il che rende le funzionalità multi-fotocamera un buon candidato per Play Feature Delivery.

Alcuni casi d'uso tipici includono:

  • Zoom: passaggio da una fotocamera all'altra a seconda della regione di ritaglio o della lunghezza focale scelta.
  • Profondità: utilizza più fotocamere per creare una mappa di profondità.
  • Bokeh: utilizza le informazioni sulla profondità dedotte per simulare un intervallo di messa a fuoco ridotto simile a quello di una fotocamera reflex digitale.

La differenza tra fotocamere logiche e fisiche

Per comprendere l'API multi-camera, è necessario comprendere la differenza tra fotocamere logiche e fisiche. Ad esempio, considera un dispositivo con tre fotocamere posteriori. In questo esempio, ciascuna delle tre fotocamere posteriori è considerata una fotocamera fisica. Una fotocamera logica è quindi un raggruppamento di due o più di queste fotocamere fisiche. L'output della fotocamera logica può essere uno stream proveniente da una delle fotocamere fisiche sottostanti o uno stream combinato proveniente da più fotocamere fisiche sottostanti contemporaneamente. In entrambi i casi, lo stream viene gestito dall'Hardware Abstraction Layer (HAL) della fotocamera.

Molti produttori di smartphone sviluppano applicazioni per fotocamere proprietarie, che di solito sono preinstallate sui loro dispositivi. Per utilizzare tutte le funzionalità dell'hardware, potrebbero utilizzare API private o nascoste o ricevere un trattamento speciale dall'implementazione del driver a cui altre applicazioni non hanno accesso. Alcuni dispositivi implementano il concetto di fotocamere logiche fornendo un flusso combinato di frame provenienti dalle diverse fotocamere fisiche, ma solo ad alcune applicazioni con privilegi. Spesso, solo una delle fotocamere fisiche è esposta al framework. La situazione per gli sviluppatori di terze parti prima di Android 9 è illustrata nel seguente diagramma:

Funzionalità della fotocamera in genere disponibili solo per le applicazioni con privilegi
Figura 1. Funzionalità della fotocamera in genere disponibili solo per le applicazioni con privilegi

A partire da Android 9, le API private non sono più consentite nelle app per Android. Con l'inclusione del supporto della modalità multicamera nel framework, le best practice di Android consigliano vivamente ai produttori di smartphone di esporre una fotocamera logica per tutte le fotocamere fisiche rivolte nella stessa direzione. Di seguito è riportato ciò che gli sviluppatori di terze parti dovrebbero aspettarsi di vedere sui dispositivi con Android 9 e versioni successive:

Accesso completo per gli sviluppatori a tutti i dispositivi con fotocamera a partire da Android 9
Figura 2. Accesso completo degli sviluppatori a tutti i dispositivi con fotocamera a partire da Android 9

Ciò che fornisce la fotocamera logica dipende interamente dall'implementazione OEM dell'HAL della fotocamera. Ad esempio, un dispositivo come Pixel 3 implementa la sua fotocamera logica in modo da scegliere una delle sue fotocamere fisiche in base alla lunghezza focale e alla regione di ritaglio richieste.

API multi-camera

La nuova API aggiunge le seguenti nuove costanti, classi e metodi:

A causa delle modifiche al documento di definizione della compatibilità (CDD) di Android, l'API multicamera prevede anche alcune aspettative da parte degli sviluppatori. I dispositivi con doppia fotocamera esistevano prima di Android 9, ma l'apertura di più fotocamere contemporaneamente comportava tentativi ed errori. Su Android 9 e versioni successive, la funzionalità multicamera fornisce un insieme di regole per specificare quando è possibile aprire una coppia di fotocamere fisiche che fanno parte della stessa fotocamera logica.

Nella maggior parte dei casi, i dispositivi con Android 9 e versioni successive espongono tutte le fotocamere fisiche (ad eccezione, probabilmente, dei tipi di sensori meno comuni come gli infrarossi) insieme a una fotocamera logica più facile da usare. Per ogni combinazione di stream che funzionano, uno stream appartenente a una videocamera logica può essere sostituito da due stream delle videocamere fisiche sottostanti.

Più stream contemporaneamente

Utilizzo di più stream della videocamera contemporaneamente illustra le regole per l'utilizzo di più stream contemporaneamente in una singola videocamera. Con un'aggiunta importante, le stesse regole si applicano a più videocamere. CameraMetadata.REQUEST_AVAILABLE_CAPABILITIES_LOGICAL_MULTI_CAMERA spiega come sostituire uno stream YUV_420_888 o RAW logico con due stream fisici. In altre parole, ogni stream di tipo YUV o RAW può essere sostituito con due stream di tipo e dimensioni identici. Puoi iniziare con uno stream della videocamera con la seguente configurazione garantita per i dispositivi con una sola videocamera:

  • Stream 1: tipo YUV, dimensioni MAXIMUM dalla fotocamera logica id = 0

Un dispositivo con supporto della modalità multicamera consente di creare una sessione sostituendo lo stream YUV logico con due stream fisici:

  • Stream 1: tipo YUV, dimensioni MAXIMUM dalla fotocamera fisica id = 1
  • Stream 2: tipo YUV, dimensioni MAXIMUM dalla fotocamera fisica id = 2

Puoi sostituire uno stream YUV o RAW con due stream equivalenti se e solo se queste due fotocamere fanno parte di un raggruppamento di fotocamere logiche,  elencato in CameraCharacteristics.getPhysicalCameraIds().

Le garanzie fornite dal framework sono solo il minimo indispensabile per ottenere frame da più di una fotocamera fisica contemporaneamente. La maggior parte dei dispositivi supporta stream aggiuntivi, a volte consentendo anche l'apertura indipendente di più dispositivi con fotocamera fisica. Poiché non si tratta di una garanzia assoluta del framework, per farlo è necessario eseguire test e ottimizzazioni per ogni dispositivo utilizzando il metodo di prova ed errore.

Creazione di una sessione con più fotocamere fisiche

Quando utilizzi fotocamere fisiche su un dispositivo con più fotocamere, apri una singola CameraDevice (la fotocamera logica) e interagisci con questa in un'unica sessione. Crea la singola sessione utilizzando l'API CameraDevice.createCaptureSession(SessionConfiguration config), che è stata aggiunta nel livello API 28. La configurazione della sessione ha un numero di configurazioni di output, ognuna delle quali ha un insieme di target di output e, facoltativamente, un ID fotocamera fisica scelto.

Modello SessionConfiguration e OutputConfiguration
Figura 3. Modello SessionConfiguration e OutputConfiguration

Alle richieste di acquisizione è associata una destinazione di output. Il framework determina a quale fotocamera fisica (o logica) vengono inviate le richieste in base alla destinazione di output collegata. Se la destinazione di output corrisponde a una delle destinazioni di output inviate come configurazione di output insieme a un ID fotocamera fisica, la fotocamera fisica riceve ed elabora la richiesta.

Utilizzo di un paio di videocamere fisiche

Un'altra aggiunta alle API della fotocamera per la multicamera è la possibilità di identificare le fotocamere logiche e trovare le fotocamere fisiche dietro di esse. Puoi definire una funzione per identificare le potenziali coppie di fotocamere fisiche che puoi utilizzare per sostituire uno degli stream della videocamera logica:

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 gestione dello stato delle fotocamere fisiche è controllata dalla fotocamera logica. Per aprire una "doppia fotocamera", apri la fotocamera logica corrispondente alle fotocamere fisiche:

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

Oltre a selezionare la fotocamera da aprire, la procedura è la stessa di quella per aprire una fotocamera nelle versioni precedenti di Android. La creazione di una sessione di acquisizione utilizzando la nuova API di configurazione della sessione indica al framework di associare determinati target a ID di fotocamere fisiche specifici:

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

Per informazioni sulla combinazione di stream supportata, consulta createCaptureSession. La combinazione di stream è destinata a più stream su una singola videocamera logica. La compatibilità si estende all'utilizzo della stessa configurazione e alla sostituzione di uno di questi stream con due stream provenienti da due fotocamere fisiche che fanno parte della stessa fotocamera logica.

Con la sessione della fotocamera pronta, invia le richieste di acquisizione scelte. Ogni target della richiesta di acquisizione riceve i dati dalla fotocamera fisica associata, se in uso, o torna alla fotocamera logica.

Caso d'uso di esempio di Zoom

È possibile unire le videocamere fisiche in un unico stream in modo che gli utenti possano passare da una videocamera fisica all'altra per sperimentare un campo visivo diverso, acquisendo di fatto un "livello di zoom" diverso.

Esempio di scambio di fotocamere per il caso d'uso del livello di zoom
Figura 4. Esempio di scambio di fotocamere per il caso d'uso del livello di zoom (dall'annuncio di Pixel 3)

Per prima cosa, seleziona la coppia di fotocamere fisiche tra cui gli utenti possono passare. Per ottenere il massimo effetto, puoi scegliere la coppia di fotocamere che fornisce la lunghezza focale minima e massima disponibile.

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

Un'architettura sensata per questo caso sarebbe avere due SurfaceViews, uno per ogni stream. Questi SurfaceViews vengono scambiati in base all'interazione dell'utente, in modo che ne sia visibile solo uno alla volta.

Il seguente codice mostra come aprire la fotocamera logica, configurare le uscite della fotocamera, creare una sessione della fotocamera e avviare due stream di anteprima:

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

Non resta che fornire un'interfaccia utente che consenta all'utente di passare da una superficie all'altra, ad esempio un pulsante o un doppio tocco su SurfaceView. Potresti persino eseguire una sorta di analisi della scena e passare automaticamente da uno stream all'altro.

Distorsione dell'obiettivo

Tutti gli obiettivi producono una certa quantità di distorsione. In Android, puoi eseguire query sulla distorsione creata dagli obiettivi utilizzando CameraCharacteristics.LENS_DISTORTION, che sostituisce CameraCharacteristics.LENS_RADIAL_DISTORTION, ora ritirato. Per le fotocamere logiche, la distorsione è minima e l'applicazione può utilizzare i frame più o meno così come provengono dalla fotocamera. Per le fotocamere fisiche, esistono configurazioni degli obiettivi potenzialmente molto diverse, in particolare per gli obiettivi grandangolari.

Alcuni dispositivi potrebbero implementare la correzione automatica delle distorsioni utilizzando CaptureRequest.DISTORTION_CORRECTION_MODE. La correzione delle distorsioni è attiva per impostazione predefinita per la maggior parte dei dispositivi.

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

L'impostazione di una richiesta di acquisizione in questa modalità può influire sulla frequenza fotogrammi che può essere prodotta dalla fotocamera. Puoi scegliere di impostare la correzione delle distorsioni solo per l'acquisizione di immagini fisse.