Interfejs API do obsługi wielu kamer

Uwaga: ta strona dotyczy pakietu Camera2. Jeśli aplikacja nie wymaga określonych, niskiego poziomu funkcji z Aparatu2, zalecamy użycie CameraX. Zarówno aplikacja CameraX, jak i Aparat 2 obsługują Androida 5.0 (poziom interfejsu API 21) i nowsze wersje.

Możliwość korzystania z wielu aparatów została wprowadzona w Androidzie 9 (poziom interfejsu API 28). Od momentu premiery na rynku pojawiły się urządzenia obsługujące ten interfejs API. Wiele zastosowań z wieloma kamerami jest ściśle powiązanych z konkretną konfiguracją sprzętową. Innymi słowy, nie wszystkie przypadki użycia są zgodne z każdym urządzeniem,  dlatego funkcje z obsługą wielu aparatów dobrze nadają się do Play Feature Delivery.

Oto niektóre typowe przypadki użycia:

  • Powiększenie: przełączanie się między kamerami w zależności od obszaru przycięcia lub wybranej długości ogniskowej.
  • Głębokość: tworzenie mapy głębi za pomocą kilku aparatów.
  • Bokeh: korzystanie z wywnioskowanych informacji o głębi do symulowania wąskiego zakresu ostrości przypominającego lustrzankę cyfrową.

Różnica między kamerami logicznymi a fizycznymi

Zrozumienie mechanizmu obsługi wielu kamer wymaga zrozumienia różnicy między kamerami logicznymi a fizycznymi. Weźmy pod uwagę urządzenie z trzema tylnymi aparatami. W tym przykładzie każdy z 3 tylnych aparatów jest uznawany za kamerę fizyczną. czyli kamera logiczna, czyli zgrupowana przez co najmniej dwie takie fizyczne kamery. Wynikiem kamery logicznej może być strumień pochodzący z jednej z podstawowych kamer fizycznych lub strumień utrwalony pochodzący z więcej niż jednej kamery fizycznej jednocześnie. W obu przypadkach strumień jest obsługiwany przez warstwę abstrakcji sprzętowej kamery (HAL).

Wielu producentów telefonów tworzy aplikacje na swój aparat, które zwykle są instalowane fabrycznie na urządzeniach. Aby możliwe było korzystanie ze wszystkich możliwości sprzętu, mogą one korzystać z prywatnych lub ukrytych interfejsów API albo być traktowane w specjalny sposób przez implementację sterownika, do której inne aplikacje nie mają dostępu. Niektóre urządzenia wykorzystują pojęcie kamer logicznych, udostępniając strumień klatek z różnych kamer fizycznych, ale tylko w określonych aplikacjach uprzywilejowanych. Często tylko jedna z fizycznych kamer jest widoczna na scenie. Sytuację programistów zewnętrznych przed Androidem 9 przedstawiono na tym diagramie:

Rysunek 1. Funkcje kamery zwykle są dostępne tylko w aplikacjach z podwyższonymi uprawnieniami.

Od Androida 9 prywatne interfejsy API nie są już dozwolone w aplikacjach na Androida. Uwzględnienie obsługi wielu kamer w ramach platformy sprawia, że zgodnie ze sprawdzonymi metodami dotyczącymi Androida producenci telefonów powinni udostępniać aparaty logiczne dla wszystkich aparatów fizycznych skierowanych w tym samym kierunku. Na urządzeniach z Androidem 9 lub nowszym deweloperzy zewnętrzni mogą się spodziewać tych nowości:

Rysunek 2. Pełny dostęp dla programistów do wszystkich aparatów, począwszy od Androida 9

To, co zapewnia kamera logiczna, zależy całkowicie od implementacji HAL kamery OEM. Na przykład urządzenie takie jak Pixel 3 stosuje swój aparat logiczny w taki sposób, że wybiera jeden ze swoich fizycznych aparatów na podstawie żądanej wartości ogniskowej i obszaru przycięcia.

Interfejs API do obsługi wielu kamer

Nowy interfejs API dodaje następujące stałe, klasy i metody:

Ze względu na zmiany w dokumencie CDD (Android Compatibility Definition Document) dla programistów korzystających z interfejsu API do obsługi wielu aparatów obowiązują pewne oczekiwania. Urządzenia z podwójnymi aparatami istniały przed Androidem 9, ale uruchamianie więcej niż jednej kamery jednocześnie wiązało się z próbami i błędami. W Androidzie 9 i nowszych funkcja trybu wielu kamer określa zestaw reguł, które określają, kiedy można otworzyć parę fizycznych kamer, które są częścią tej samej logicznej kamery.

W większości przypadków urządzenia z Androidem 9 lub nowszym prezentują wszystkie aparaty fizyczne (z wyjątkiem mniej popularnych czujników, takich jak podczerwień) i łatwiej w obsłudze aparat logiczny. W przypadku każdej kombinacji strumieni, który gwarantuje działanie, 1 strumienie z kamery logicznej mogą zostać zastąpione 2 strumieniami z kamer fizycznych.

Jednoczesne strumienie

Jednoczesne korzystanie z kilku transmisji z kamery obejmuje zasady korzystania z kilku transmisji jednocześnie z jednej kamery. Po jednej ważnej zmianie te same zasady dotyczą wielu aparatów. CameraMetadata.REQUEST_AVAILABLE_CAPABILITIES_LOGICAL_MULTI_CAMERA wyjaśnia, jak zastąpić strumień logiczny YUV_420_888 lub nieprzetworzony dwoma strumieniami fizycznymi. Oznacza to, że każdy strumień typu YUV lub RAW można zastąpić 2 strumieniami o jednakowym typie i rozmiarze. Możesz zacząć od strumienia z kamery o podanej niżej gwarantowanej konfiguracji dla urządzeń z jedną kamerą:

  • Strumień 1: typ YUV, rozmiar MAXIMUM z kamery logicznej id = 0

Następnie urządzenie obsługujące wiele kamer umożliwia utworzenie sesji, która zastępuje ten logiczny strumień YUV 2 strumieniami fizycznymi:

  • Strumień 1: typ YUV, rozmiar MAXIMUM z fizycznej kamery id = 1
  • Strumień 2: typ YUV, rozmiar MAXIMUM z fizycznej kamery id = 2

Strumień YUV lub RAW możesz zastąpić 2 równoważnymi strumieniami tylko wtedy, gdy te 2 kamery należą do logicznej grupy kamer, która znajduje się w sekcji CameraCharacteristics.getPhysicalCameraIds().

Gwarancje oferowane przez platformę to absolutne minimum wymagane do pobierania klatek z więcej niż 1 fizycznej kamery w tym samym czasie. Większość urządzeń obsługuje strumienie dodatkowe, a czasem nawet umożliwia niezależne otwarcie kilku fizycznych kamer. Ponieważ nie jest to sztywna gwarancja co do struktury, wymaga to testowania i dostrajania na poszczególnych urządzeniach z wykorzystaniem metody prób i błędów.

Tworzenie sesji z wieloma kamerami fizycznymi

Jeśli używasz fizycznych kamer na urządzeniu obsługującym wiele kamer, otwórz 1 kamerę CameraDevice (kamerę logiczną) i wejdź z nimi w interakcję w trakcie jednej sesji. Utwórz pojedynczą sesję za pomocą interfejsu API CameraDevice.createCaptureSession(SessionConfiguration config), który został dodany na poziomie 28 interfejsu API. Konfiguracja sesji ma kilka konfiguracji wyjściowych, z których każda ma zestaw celów wyjściowych i opcjonalnie odpowiedni fizyczny identyfikator kamery.

Rysunek 3. Model SessionConfiguration i outputConfiguration

Żądania przechwytywania mają powiązany z nim wyjściowy cel. Platforma określa, do której fizycznej (lub logicznej) kamery są wysyłane żądania, na podstawie podłączonego celu wyjściowego. Jeśli miejsce docelowe na wyjściu odpowiada jednemu z celów wyjściowych, które zostały wysłane jako konfiguracja wyjścia wraz z fizycznym identyfikatorem kamery, wtedy ta kamera fizyczna odbiera i przetwarza żądanie.

Korzystanie z pary fizycznych aparatów

Poza interfejsami API kamer do wielu kamer można identyfikować kamery logiczne i znajdować za nimi fizyczne kamery. Możesz zdefiniować funkcję ułatwiającą identyfikację potencjalnych par fizycznych kamer, którą można wykorzystać do zastąpienia jednego z logicznych strumieni kamer:

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

Obsługą fizycznych kamer steruje kamera logiczna. Aby otworzyć „podwójny aparat”, otwórz aparat logiczny odpowiadający kamerom fizycznym:

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

Oprócz wybrania kamery, którą chcesz otworzyć, proces przebiega tak samo jak uruchamianie aparatu w poprzednich wersjach Androida. Utworzenie sesji przechwytywania za pomocą nowego interfejsu API konfiguracji sesji informuje platformę o powiązaniu określonych celów z konkretnymi identyfikatorami fizycznych kamer:

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

W sekcji createCaptureSession znajdziesz informacje o obsługiwanych kombinacjach strumieni. Łączenie strumieni obejmuje wiele transmisji za pomocą jednej logicznej kamery. Zgodność obejmuje korzystanie z tej samej konfiguracji i zastępowanie jednego z nich dwoma strumieniami z 2 fizycznych kamer, które są częścią tej samej kamery logicznej.

Po przygotowaniu sesji kamery wyślij odpowiednie żądania przechwytywania. Każde miejsce docelowe żądania zapisu otrzymuje dane z powiązanej kamery fizycznej (jeśli taka kamera jest używana) lub z kamery logicznej.

Przykład zastosowania aplikacji Zoom

Można wykorzystać połączenie fizycznych kamer w jeden strumień, tak aby użytkownicy mogli przełączać się między różnymi fizycznymi kamerami i mieć różne pole widzenia, co efektywnie rejestrowało „poziom powiększenia”.

Rysunek 4. Przykład przełączania aparatów w celu zwiększenia powiększenia (z reklamy Pixel 3)

Zacznij od wybrania pary fizycznych kamer, aby użytkownicy mogli się między nimi przełączać. Aby uzyskać maksymalny efekt, możesz wybrać parę aparatów, które zapewniają minimalną i maksymalną dostępne parametry ogniskowej.

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

W takiej sytuacji warto mieć 2 komponenty SurfaceViews – po jednym na każdy strumień. Te elementy SurfaceViews są zastępowane w zależności od interakcji użytkownika, więc w danym momencie widoczna jest tylko jedna z nich.

Poniższy kod pokazuje, jak otworzyć kamerę logiczną, skonfigurować jej wyjścia, utworzyć sesję kamery i uruchomić 2 strumienie podglądu:

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

Wystarczy udostępnić interfejs, który umożliwi użytkownikowi przełączanie się między 2 obszarami, np. przycisk lub dwukrotne kliknięcie SurfaceView. Możesz nawet przeprowadzać jakiś rodzaj analizy sceny i automatycznie przełączać się między dwoma strumieniami.

Zniekształcenie obiektywu

Wszystkie soczewki generują pewną ilość zniekształceń. Na Androidzie możesz tworzyć zapytania o zniekształcenia spowodowane przez obiektywy, korzystając z metody CameraCharacteristics.LENS_DISTORTION, która zastępuje już wycofywane właściwości CameraCharacteristics.LENS_RADIAL_DISTORTION. W przypadku kamer logicznych zniekształcenia są niewielkie, a aplikacja może wykorzystywać klatki z aparatu w większym lub mniejszym stopniu. W przypadku fizycznych aparatów konfiguracja obiektywów może się różnić, zwłaszcza w przypadku obiektywów szerokokątnych.

Niektóre urządzenia mogą zaimplementować automatyczną korektę zniekształceń za pomocą CaptureRequest.DISTORTION_CORRECTION_MODE. Na większości urządzeń korekcja zniekształceń jest domyślnie włączona.

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

Ustawienie żądania nagrywania w tym trybie może mieć wpływ na liczbę klatek, która może być generowana przez aparat. Możesz ustawić korekcję zniekształceń tylko dla zrobionych zdjęć.