API für mehrere Kameras

Hinweis:Diese Seite bezieht sich auf das Paket Camera2. Sofern Ihre App keine bestimmten Low-Level-Funktionen von Camera2 erfordert, empfehlen wir die Verwendung von CameraX. CameraX und Camera2 unterstützen Android 5.0 (API-Level 21) und höher.

Die Nutzung mehrerer Kameras wurde mit Android 9 (API-Level 28) eingeführt. Seit der Veröffentlichung sind Geräte auf den Markt gekommen, die die API unterstützen. Viele Anwendungsfälle mit mehreren Kameras sind eng mit einer bestimmten Hardwarekonfiguration verknüpft. Mit anderen Worten: Nicht alle Anwendungsfälle sind mit jedem Gerät kompatibel. Daher sind Funktionen mit mehreren Kameras gut für die Play Feature Delivery geeignet.

Einige typische Anwendungsfälle sind:

  • Zoom: Wechsel zwischen Kameras je nach Zuschneidebereich oder gewünschter Brennweite.
  • Tiefe: Verwendung mehrerer Kameras zum Erstellen einer Tiefenkarte
  • Bokeh: Verwenden abgeleiteter Tiefeninformationen, um einen DSLR-ähnlichen schmalen Fokusbereich zu simulieren.

Unterschied zwischen logischen und physischen Kameras

Um die Multikamera-API zu verstehen, müssen Sie den Unterschied zwischen logischen und physischen Kameras kennen. Nehmen wir als Referenz ein Gerät mit drei Rückkameras. In diesem Beispiel wird jede der drei Rückkameras als physische Kamera betrachtet. Eine logische Kamera ist dann eine Gruppe von zwei oder mehr dieser physischen Kameras. Das Ausgabeformat der logischen Kamera kann ein Stream sein, der von einer der zugrunde liegenden physischen Kameras kommt, oder ein zusammengeführter Strom, der von mehreren zugrunde liegenden physischen Kameras gleichzeitig kommt. In beiden Fällen wird der Stream von der Hardware Extraction Layer (HAL) der Kamera verarbeitet.

Viele Smartphonehersteller entwickeln eigene Kameraanwendungen, die normalerweise bereits auf ihren Geräten vorinstalliert sind. Um alle Funktionen der Hardware zu nutzen, können sie private oder versteckte APIs verwenden oder eine besondere Behandlung durch die Treiberimplementierung erhalten, auf die andere Anwendungen keinen Zugriff haben. Einige Geräte implementieren das Konzept logischer Kameras, indem sie einen zusammengeführten Stream von Frames von verschiedenen physischen Kameras bereitstellen, jedoch nur für bestimmte privilegierte Anwendungen. Oft ist nur eine der physischen Kameras mit dem Framework konfrontiert. Die Situation für Drittanbieter-Entwickler vor Android 9 ist im folgenden Diagramm dargestellt:

Abbildung 1. Kamerafunktionen, die normalerweise nur für privilegierte Anwendungen verfügbar sind

Ab Android 9 sind private APIs in Android-Apps nicht mehr zulässig. Da das Framework auch mehrere Kameras unterstützt, empfehlen die Best Practices von Android dringend, für alle physischen Kameras, die in dieselbe Richtung zeigen, eine logische Kamera zur Verfügung zu stellen. Im Folgenden ist aufgeführt, was Drittanbieter-Entwickler auf Geräten mit Android 9 und höher erwarten können:

Abbildung 2. Vollständiger Entwicklerzugriff auf alle Kamerageräte ab Android 9

Was die logische Kamera bietet, hängt vollständig von der OEM-Implementierung des Kamera-HAL ab. Beispielsweise implementiert ein Gerät wie Pixel 3 seine logische Kamera so, dass eine seiner physischen Kameras basierend auf der angeforderten Brennweite und dem Zuschnittbereich ausgewählt wird.

Die Multikamera-API

Die neue API fügt die folgenden neuen Konstanten, Klassen und Methoden hinzu:

Aufgrund von Änderungen am Android Compatibility Definition Document (CDD) stellt die Multi-Kamera-API auch bestimmte Erwartungen von Entwicklern mit sich. Vor Android 9 gab es Geräte mit zwei Kameras, aber das gleichzeitige Öffnen von mehr als einer Kamera erforderte Versuch und Irrtum. Unter Android 9 und höher gibt es für die Verwendung mehrerer Kameras eine Reihe von Regeln, um festzulegen, wann zwei physische Kameras geöffnet werden können, die Teil derselben logischen Kamera sind.

In den meisten Fällen belichten Geräte mit Android 9 und höher alle physischen Kameras (außer bei weniger gängigen Sensortypen wie Infrarotsensoren) sowie eine nutzerfreundlichere logische Kamera. Für jede Kombination von Streams, die garantiert funktionieren, kann ein Stream, der zu einer logischen Kamera gehört, durch zwei Streams von den zugrunde liegenden physischen Kameras ersetzt werden.

Mehrere Streams gleichzeitig

Mehrere Kamerastreams gleichzeitig verwenden umfasst die Regeln für die gleichzeitige Verwendung mehrerer Streams mit einer einzelnen Kamera. Es gibt jedoch eine wichtige Ergänzung, bei der die gleichen Regeln für mehrere Kameras gelten. In CameraMetadata.REQUEST_AVAILABLE_CAPABILITIES_LOGICAL_MULTI_CAMERA wird erläutert, wie ein logischer YUV_420_888 oder ein Rohstream durch zwei physische Streams ersetzt wird. Das heißt, jeder Stream des Typs YUV oder RAW kann durch zwei Streams desselben Typs und mit identischer Größe ersetzt werden. Sie können mit einem Kamerastream der folgenden garantierten Konfiguration für Geräte mit einer Kamera beginnen:

  • Stream 1: YUV-Typ, MAXIMUM Größe von der logischen Kamera id = 0

Anschließend können Sie mit einem Gerät, das mehrere Kameras unterstützt, eine Sitzung erstellen, in der dieser logische YUV-Stream durch zwei physische Streams ersetzt wird:

  • Stream 1: YUV-Typ, MAXIMUM Größe von physischer Kamera id = 1
  • Stream 2: YUV-Typ, MAXIMUM Größe von physischer Kamera id = 2

Sie können einen YUV- oder RAW-Stream nur dann durch zwei entsprechende Streams ersetzen, wenn diese beiden Kameras Teil einer logischen Kameragruppierung sind, die unter CameraCharacteristics.getPhysicalCameraIds() aufgeführt ist.

Die Garantien des Frameworks sind nur das Minimum, das erforderlich ist, um Frames von mehr als einer physischen Kamera gleichzeitig zu erhalten. In den meisten Geräten werden zusätzliche Streams unterstützt, manchmal sogar damit, dass mehrere physische Kamerageräte unabhängig voneinander geöffnet werden können. Da dies keine feste Garantie durch das Framework ist, müssen dafür gerätespezifische Tests und eine Feinabstimmung durch Versuch und Fehler durchgeführt werden.

Sitzung mit mehreren Kameras erstellen

Wenn Sie physische Kameras an einem Gerät mit mehreren Kameras verwenden, öffnen Sie eine einzelne CameraDevice (die logische Kamera) und interagieren Sie innerhalb einer einzigen Sitzung mit ihr. Erstellen Sie eine einzelne Sitzung mit der API CameraDevice.createCaptureSession(SessionConfiguration config), die in API-Level 28 hinzugefügt wurde. Die Sitzungskonfiguration umfasst eine Reihe von Ausgabekonfigurationen, von denen jede eine Reihe von Ausgabezielen und optional eine gewünschte physische Kamera-ID hat.

Abbildung 3. SessionConfiguration- und OutputConfiguration-Modell

Erfassungsanfragen ist ein Ausgabeziel zugeordnet. Das Framework bestimmt anhand des angehängten Ausgabeziels, an welche physische (oder logische) Kamera die Anfragen gesendet werden. Wenn das Ausgabeziel einem der Ausgabeziele entspricht, die zusammen mit einer physischen Kamera-ID als Ausgabekonfiguration gesendet wurden, empfängt und verarbeitet diese physische Kamera die Anfrage.

Zwei physische Kameras verwenden

Eine weitere Ergänzung zu den Kamera-APIs für mehrere Kameras ist die Möglichkeit, logische Kameras zu identifizieren und die dahinter liegenden physischen Kameras zu finden. Sie können eine Funktion definieren, um potenzielle Paare physischer Kameras zu identifizieren, mit denen Sie einen der logischen Kamerastreams ersetzen können:

Kotlin

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

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

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

        return dualCameras
    }

Java

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

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

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

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

Die Verarbeitung der physischen Kameras wird von der logischen Kamera gesteuert. Um eine „Dual-Kamera“ zu öffnen, öffnen Sie die logische Kamera, die den physischen Kameras entspricht:

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

Abgesehen von der Auswahl der zu öffnenden Kamera ist der Vorgang mit dem Öffnen einer Kamera in früheren Android-Versionen identisch. Wenn Sie mit der neuen API zur Sitzungskonfiguration eine Aufnahmesitzung erstellen, weisen Sie das Framework an, bestimmte Ziele bestimmten physischen Kamera-IDs zuzuordnen:

Kotlin

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

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

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

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

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

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

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

Java

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

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

Informationen dazu, welche Kombination von Streams unterstützt wird, finden Sie unter createCaptureSession. Das Kombinieren von Streams ist für mehrere Streams auf einer einzelnen logischen Kamera möglich. Die Kompatibilität umfasst auch die Verwendung derselben Konfiguration und das Ersetzen eines Streams durch zwei Streams von zwei physischen Kameras, die Teil derselben logischen Kamera sind.

Wenn die Kamerasitzung fertig ist, senden Sie die gewünschten Erfassungsanfragen. Jedes Ziel der Erfassungsanfrage empfängt seine Daten von der zugehörigen physischen Kamera, falls diese gerade verwendet wird, oder greift auf die logische Kamera zurück.

Zoom – Anwendungsfall

Es ist möglich, physische Kameras zu einem einzigen Stream zusammenzuführen, sodass Nutzer zwischen den verschiedenen physischen Kameras wechseln können, um ein anderes Sichtfeld zu sehen und praktisch eine andere „Zoomstufe“ zu erfassen.

Abbildung 4. Beispiel für den Austausch der Kameras für die Zoomstufe (aus der Pixel 3-Anzeige)

Wählen Sie zuerst das Paar physischer Kameras aus, zwischen denen Nutzer wechseln können. Für einen maximalen Effekt können Sie das Kamerapaar mit der verfügbaren minimalen und maximalen Brennweite verwenden.

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

Eine sinnvolle Architektur wäre, zwei SurfaceViews zu haben – eines für jeden Stream. Diese SurfaceViews werden je nach Nutzerinteraktion ausgetauscht, sodass immer nur eine der beiden Optionen sichtbar ist.

Der folgende Code zeigt, wie Sie die logische Kamera öffnen, die Kameraausgaben konfigurieren, eine Kamerasitzung erstellen und zwei Vorschaustreams starten:

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

Jetzt muss der Nutzer nur noch eine UI bereitstellen, über die er zwischen den beiden Oberflächen wechseln kann, z. B. über eine Schaltfläche oder durch Doppeltippen auf SurfaceView. Sie können sogar eine Szenenanalyse durchführen und automatisch zwischen den beiden Streams wechseln.

Objektivverzeichnung

Alle Objektive erzeugen ein gewisses Maß an Verzerrungen. In Android können Sie die von Objektiven erzeugte Verzerrung mit CameraCharacteristics.LENS_DISTORTION abfragen. Dieser ersetzt den inzwischen eingestellten CameraCharacteristics.LENS_RADIAL_DISTORTION. Bei logischen Kameras ist die Verzerrung minimal und Ihre Anwendung kann die Frames mehr oder weniger verwenden, je nachdem, ob sie von der Kamera kommen. Bei physischen Kameras können die Objektivkonfigurationen sehr unterschiedlich sein, insbesondere bei Weitwinkelobjektiven.

Auf einigen Geräten wird die automatische Verzerrungskorrektur über CaptureRequest.DISTORTION_CORRECTION_MODE implementiert. Bei den meisten Geräten ist die Verzeichnungskorrektur standardmäßig aktiviert.

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

Wenn Sie eine Aufnahmeanfrage in diesem Modus festlegen, kann sich das auf die Framerate auswirken, die die Kamera erzeugen kann. Sie können die Verzerrungskorrektur nur für Standaufnahmen festlegen.