Interfejs API do obsługi wielu kamer

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

Funkcja wielu aparatów została wprowadzona w Androidzie 9 (poziom interfejsu API 28). Od czasu jego wydania na rynku pojawiły się urządzenia obsługujące ten interfejs API. Wiele przypadków użycia wielu kamer 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, co sprawia, że funkcje korzystające z wielu aparatów są dobrym kandydatem do Play Feature Delivery.

Oto kilka typowych przypadków użycia:

  • Powiększenie: przełączanie się między aparatami w zależności od przyciętego obszaru lub wybranej ogniskowej.
  • Głębia: używanie wielu aparatów do tworzenia mapy głębi.
  • Efekt bokeh: wykorzystanie informacji o głębi do symulowania wąskiej głębi ostrości, podobnej do tej uzyskiwanej za pomocą lustrzanki cyfrowej.

Różnica między aparatami logicznymi i fizycznymi

Aby zrozumieć interfejs API wielu kamer, musisz poznać różnicę między kamerami logicznymi i fizycznymi. Dla przykładu weźmy urządzenie z 3 tylnymi aparatami. W tym przykładzie każdy z 3 tylnych aparatów jest traktowany jako aparat fizyczny. Aparat logiczny to grupa co najmniej 2 aparatów fizycznych. Dane wyjściowe kamery logicznej mogą być strumieniem pochodzącym z jednej z podstawowych kamer fizycznych lub połączonym strumieniem pochodzącym z więcej niż jednej podstawowej kamery fizycznej jednocześnie. W obu przypadkach strumień jest obsługiwany przez warstwę abstrakcji sprzętowej (HAL) aparatu.

Wielu producentów telefonów tworzy własne aplikacje do obsługi aparatu, które zwykle są fabrycznie zainstalowane na urządzeniach. Aby korzystać ze wszystkich możliwości sprzętu, mogą one używać prywatnych lub ukrytych interfejsów API albo korzystać ze specjalnego traktowania przez implementację sterownika, do którego inne aplikacje nie mają dostępu. Niektóre urządzenia implementują koncepcję kamer logicznych, udostępniając połączony strumień klatek z różnych kamer fizycznych, ale tylko niektórym uprzywilejowanym aplikacjom. Często tylko jeden z aparatów fizycznych jest widoczny dla platformy. Sytuację deweloperów zewnętrznych przed Androidem 9 ilustruje ten diagram:

Funkcje aparatu zwykle dostępne tylko dla aplikacji uprzywilejowanych
Rysunek 1. Funkcje aparatu zwykle dostępne tylko dla aplikacji uprzywilejowanych

Od Androida 9 prywatne interfejsy API nie są już dozwolone w aplikacjach na Androida. W związku z wprowadzeniem obsługi wielu aparatów w ramach sprawdzonych metod dotyczących Androida zdecydowanie zalecamy producentom telefonów udostępnianie aparatu logicznego dla wszystkich aparatów fizycznych skierowanych w tę samą stronę. Oto czego deweloperzy zewnętrzni powinni się spodziewać na urządzeniach z Androidem 9 lub nowszym:

Pełny dostęp deweloperów do wszystkich aparatów od Androida 9
Rysunek 2. Pełny dostęp deweloperów do wszystkich aparatów od Androida 9

To, co zapewnia aparat logiczny, zależy całkowicie od implementacji komponentu HAL aparatu przez OEM. Na przykład urządzenie takie jak Pixel 3 implementuje aparat logiczny w taki sposób, że wybiera jeden z aparatów fizycznych na podstawie żądanej ogniskowej i obszaru przycięcia.

Interfejs API wielu aparatów

Nowy interfejs API dodaje te nowe stałe, klasy i metody:

Ze względu na zmiany w dokumencie definicji zgodności (CDD) Androida interfejs API wielu aparatów wiąże się też z pewnymi oczekiwaniami wobec deweloperów. Urządzenia z 2 aparatami istniały już przed Androidem 9, ale otwieranie więcej niż 1 aparatu jednocześnie wymagało prób i błędów. W Androidzie 9 i nowszych funkcja wielu aparatów zawiera zestaw reguł określających, kiedy można otworzyć parę aparatów fizycznych, które są częścią tego samego aparatu logicznego.

W większości przypadków urządzenia z Androidem 9 lub nowszym udostępniają wszystkie aparaty fizyczne (z wyjątkiem mniej popularnych typów czujników, takich jak podczerwień) oraz łatwiejszy w użyciu aparat logiczny. W przypadku każdej kombinacji strumieni, które na pewno będą działać, jeden strumień należący do kamery logicznej można zastąpić 2 strumieniami z podstawowych kamer fizycznych.

Wiele transmisji jednocześnie

Korzystanie z wielu strumieni z kamery jednocześnie opisuje zasady korzystania z wielu strumieni jednocześnie w przypadku jednej kamery. Z jednym istotnym wyjątkiem te same zasady obowiązują w przypadku wielu kamer. CameraMetadata.REQUEST_AVAILABLE_CAPABILITIES_LOGICAL_MULTI_CAMERA wyjaśnia, jak zastąpić logiczny strumień YUV_420_888 lub RAW 2 strumieniami fizycznymi. Oznacza to, że każdy strumień typu YUV lub RAW można zastąpić 2 strumieniami tego samego typu i rozmiaru. Możesz zacząć od strumienia z kamery o następującej gwarantowanej konfiguracji w przypadku urządzeń z jednym aparatem:

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

Następnie urządzenie z obsługą wielu aparatów umożliwia utworzenie sesji, w której logiczny strumień YUV jest zastępowany 2 strumieniami fizycznymi:

  • Strumień 1: typ YUV, rozmiar MAXIMUM z kamery fizycznej id = 1
  • Strumień 2: typ YUV, rozmiar MAXIMUM z fizycznego aparatu 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 jest wymieniona w sekcji CameraCharacteristics.getPhysicalCameraIds().

Gwarancje zapewniane przez platformę to tylko absolutne minimum wymagane do jednoczesnego pobierania klatek z więcej niż 1 kamery fizycznej. Większość urządzeń obsługuje dodatkowe strumienie, a niektóre umożliwiają nawet niezależne otwieranie wielu fizycznych aparatów. Ponieważ nie jest to twarda gwarancja ze strony platformy, wymaga to testowania i dostrajania na każdym urządzeniu metodą prób i błędów.

Tworzenie sesji z wieloma kamerami fizycznymi

Jeśli używasz fizycznych aparatów na urządzeniu z kilkoma aparatami, otwórz 1 CameraDevice (aparat logiczny) i korzystaj z niego w ramach 1 sesji. Utwórz pojedynczą sesję za pomocą interfejsu API CameraDevice.createCaptureSession(SessionConfiguration config), który został dodany na poziomie interfejsu API 28. Konfiguracja sesji ma kilka konfiguracji wyjściowych, z których każda ma zestaw celów wyjściowych i opcjonalnie wybrany identyfikator aparatu fizycznego.

Model SessionConfiguration i OutputConfiguration
Rysunek 3. Model SessionConfiguration i OutputConfiguration

Żądania przechwytywania mają powiązany z nimi cel wyjściowy. Platforma określa, do którego aparatu fizycznego (lub logicznego) są wysyłane żądania, na podstawie dołączonego celu wyjściowego. Jeśli cel wyjściowy odpowiada jednemu z celów wyjściowych, które zostały wysłane jako konfiguracja wyjściowa wraz z identyfikatorem aparatu fizycznego, ten aparat fizyczny odbiera i przetwarza żądanie.

Używanie pary fizycznych aparatów

Kolejnym dodatkiem do interfejsów API aparatu w przypadku wielu aparatów jest możliwość identyfikowania aparatów logicznych i znajdowania powiązanych z nimi aparatów fizycznych. Możesz zdefiniować funkcję, która pomoże zidentyfikować potencjalne pary kamer fizycznych, których możesz użyć do zastąpienia jednego ze strumieni z kamery logicznej:

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

Obsługa stanu aparatów fizycznych jest kontrolowana przez aparat logiczny. Aby otworzyć „obraz z obu aparatów”, otwórz aparat logiczny odpowiadający aparatom 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 wyboru aparatu, który ma zostać otwarty, proces jest taki sam jak w przypadku otwierania aparatu w poprzednich wersjach Androida. Utworzenie sesji przechwytywania za pomocą nowego interfejsu API konfiguracji sesji powoduje, że platforma powiąże określone cele z konkretnymi identyfikatorami aparatów fizycznych:

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

Informacje o obsługiwanych kombinacjach strumieni znajdziesz w artykule createCaptureSession. Łączenie strumieni dotyczy wielu strumieni na jednej kamerze logicznej. Zgodność obejmuje używanie tej samej konfiguracji i zastąpienie jednego z tych strumieni 2 strumieniami z 2 aparatów fizycznych, które są częścią tego samego aparatu logicznego.

Gdy sesja aparatu będzie gotowa, wyślij wybrane żądania przechwytywania. Każdy cel żądania przechwytywania otrzymuje dane z powiązanego aparatu fizycznego, jeśli jest on używany, lub z aparatu logicznego.

Przykładowy przypadek użycia funkcji powiększenia

Można połączyć fizyczne kamery w jedną transmisję, aby użytkownicy mogli przełączać się między różnymi kamerami i uzyskiwać różne pola widzenia, co pozwala na efektywne rejestrowanie różnych „poziomów powiększenia”.

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

Zacznij od wybrania pary fizycznych aparatów, między którymi użytkownicy będą mogli się przełączać. Aby uzyskać maksymalny efekt, możesz wybrać parę aparatów, które zapewniają minimalną i maksymalną dostępną ogniskową.

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

Rozsądna architektura w tym przypadku obejmowałaby 2 SurfaceViews – po jednym dla każdego strumienia. Te SurfaceViews są zamieniane na podstawie interakcji użytkownika, dzięki czemu w danym momencie widoczny jest tylko jeden z nich.

Poniższy kod pokazuje, jak otworzyć kamerę logiczną, skonfigurować wyjścia kamery, 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();
            }
        });

Pozostaje tylko udostępnić użytkownikowi interfejs, który pozwoli mu przełączać się między tymi 2 powierzchniami, np. przycisk lub dwukrotne kliknięcie SurfaceView. Możesz nawet przeprowadzić analizę sceny i automatycznie przełączać się między tymi 2 strumieniami.

Zniekształcenie obiektywu

Wszystkie obiektywy powodują pewne zniekształcenia. W Androidzie możesz sprawdzić zniekształcenia powodowane przez obiektywy, używając CameraCharacteristics.LENS_DISTORTION, które zastępuje wycofane już CameraCharacteristics.LENS_RADIAL_DISTORTION. W przypadku kamer logicznych zniekształcenia są minimalne, a aplikacja może używać klatek mniej więcej w takiej postaci, w jakiej pochodzą z kamery. W przypadku fizycznych aparatów konfiguracje obiektywów mogą się znacznie różnić, zwłaszcza w przypadku obiektywów szerokokątnych.

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

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 przechwytywania w tym trybie może wpłynąć na liczbę klatek, które może wygenerować aparat. Możesz ustawić korektę zniekształceń tylko w przypadku robienia zdjęć.