ממשק API של Multi-camera

הערה: הדף הזה מתייחס לחבילה camera2. מומלץ להשתמש ב-CameraX, אלא אם לאפליקציה שלך נדרשים תכונות ספציפיות ברמה נמוכה. גם CameraX וגם Camera2 תומכים ב-Android 5.0 (רמת API 21) ואילך.

ממשק המצלמות ממספר מצלמות הושק עם Android 9 (רמת API: 28). מאז השקתו, יצאו לשווק מכשירים שתומכים ב-API. תרחישים רבים לדוגמה שפועלים בכמה מצלמות מחוברים היטב עם תצורת חומרה ספציפית. במילים אחרות, לא כל התרחישים לדוגמה תואמים לכל מכשיר,  ולכן מציג מועמד טוב לתכונה של Play מסירה.

דוגמאות לתרחישים נפוצים:

  • זום: מעבר בין מצלמות בהתאם לאזור החיתוך או למוקד הרצוי. האורך.
  • עומק: שימוש בכמה מצלמות כדי לבנות מפת עומק.
  • Bokeh: שימוש בנתוני עומק משוערים כדי לדמות תמונה צרה דמוית DSLR טווח המיקוד.

ההבדל בין מצלמות לוגיות לבין מצלמות פיזיות

כדי להבין את ה-API מרובה המצלמות נדרשת הבנת ההבדל בין למצלמות לוגיות ופיזיות. לידיעתך, מכשיר עם שלושה מצלמות אחוריות. בדוגמה הזו, כל אחת משלוש המצלמות האחוריות נחשבת כמצלמה פיזית. מצלמה לוגית היא קיבוץ של שתיים או יותר של המצלמות הפיזיות האלה. הפלט של הפונקציה הלוגית מצלמה יכולה להיות שידור שמגיע מאחת מהמצלמות הפיזיות שבבסיסה. או שידור משולב שמגיע ממצלמה פיזית אחת בסיסית בו-זמנית. בכל מקרה, השידור מעובד על ידי החומרה של המצלמה Abstraction Layer (HAL).

יצרני טלפונים רבים מפתחים אפליקציות מצלמה של צד ראשון, שבדרך כלל מותקנות מראש במכשירים שלהם. כדי להשתמש בכל היכולות של החומרה, הם עשויים להשתמש בממשקי API פרטיים או נסתרים, או לקבל יחס מיוחד את ההטמעה של מנהל ההתקנים, שאין לאפליקציות אחרות גישה אליו. במידה מסוימת מכשירים מיישמים את הרעיון של מצלמות לוגיות באמצעות זרם משולב מהמצלמות הפיזיות השונות, אבל רק לחלק מהמצלמות תרגום מכונה. במקרים רבים, רק אחת מהמצלמות הפיזיות חשופה . המצב אצל מפתחי צד שלישי לפני Android 9 הוא מיוצגת בתרשים הבא:

איור 1. בדרך כלל היכולות של המצלמה זמינות רק אפליקציות שקיבלו הרשאות

החל מ-Android 9, אנחנו לא מאפשרים יותר להשתמש בממשקי API פרטיים באפליקציות ל-Android. במסגרת התמיכה בריבוי מצלמות, Android הכי טוב מומלץ מאוד ליצרני טלפונים לחשוף מצלמה לוגית לכל המצלמות הפיזיות שפונות לאותו כיוון. זה מה מפתחי צד שלישי אמורים לראות במכשירים שבהם פועלת מערכת Android 9 גבוהה יותר:

איור 2. גישה מלאה למפתחים לכל מכשירי המצלמה החל מ-Android 9

מה שהמצלמה הלוגית מספקת תלוי לחלוטין בהטמעת ה-OEM של המצלמה עם HAL. לדוגמה, מכשיר כמו Pixel 3 מיישם את הלוגיקה שלו את המצלמה באופן שבו היא בוחרת באחת מהמצלמות הפיזיות שלה בהתאם אורך המוקד ואזור החיתוך המבוקש.

ממשק API לכמה מצלמות

ה-API החדש מוסיף את הקבועים, המחלקות והשיטות הבאים הבאים:

בעקבות שינויים במסמך הגדרת התאימות של Android (CDD), התג ממשק API עם מצלמות מרובות מגיע גם מציפיות מסוימות של המפתחים. מכשירים שהיו קיימות עם שתי מצלמות לפני Android 9, אבל פתחו יותר ממצלמה אחת בו-זמנית, ניסוי וטעייה. ב-Android מגרסה 9 ואילך, יש אפשרות להשתמש בכמה מצלמות ההגדרה של כללים שקובעים מתי אפשר לפתוח מצלמות שהן חלק מאותה מצלמה לוגית.

ברוב המקרים, מכשירים עם Android מגרסה 9 ואילך חושפים את כל הנתונים הפיזיים מצלמות (למעט אולי עבור סוגי חיישנים פחות נפוצים, כמו אינפרה-אדום) יחד עם מצלמה לוגית קלה יותר לשימוש. לכל שילוב של שידורים שעובד היטב, אפשר להחליף זרם אחד ששייך למצלמה לוגית שני שידורים מהמצלמות הפיזיות הבסיסיות.

מספר שידורים במקביל

שימוש בכמה שידורי סטרימינג במצלמה בו-זמנית כולל את הכללים לשימוש בשידורים מרובים בו-זמנית במצלמה אחת. אם יש תוספת אחת חשובה, אותם כללים חלים גם על כמה מצלמות. CameraMetadata.REQUEST_AVAILABLE_CAPABILITIES_LOGICAL_MULTI_CAMERA מסביר כיצד להחליף YUV_420_888 או זרם גולמי ב-2 שידורים פיזיים. כלומר, אפשר להחליף כל זרם מסוג YUV או RAW שני שידורים מאותו סוג וגודל. אתם יכולים להתחיל עם סטרימינג במצלמה של את התצורה המובטחת הבאה למכשירים עם מצלמה אחת:

  • שידור 1: סוג YUV, MAXIMUM בגודל מהמצלמה הלוגי id = 0

לאחר מכן, מכשיר עם תמיכה במספר מצלמות יאפשר לך ליצור סשן שמחליפים את זרם ה-YUV הלוגי בשני זרמים פיזיים:

  • שידור 1: סוג YUV, MAXIMUM מהמצלמה הפיזית id = 1
  • שידור 2: סוג YUV, MAXIMUM מהמצלמה הפיזית id = 2

ניתן להחליף סטרימינג בפורמט YUV או RAW בשני שידורים מקבילים, אם ורק אם שתי המצלמות האלה הן חלק מקבוצת מצלמות לוגית,  שמפורטת CameraCharacteristics.getPhysicalCameraIds()

האחריות במסגרת המסגרת היא רק המינימום הנדרש לקבל פריימים מיותר ממצלמה פיזית אחת בו-זמנית. מקורות נתונים נוספים ברוב המכשירים, ולפעמים גם מאפשרת לפתוח מספר מכשירים פיזיים את המצלמות בנפרד. מאחר שזו לא אחריות קשה המשמעות היא ביצוע של בדיקה וכוונון לכל מכשיר בנפרד, באמצעות של ניסוי וטעייה.

יצירת סשן עם כמה מצלמות פיזיות

כשמשתמשים במצלמות פיזיות במכשיר עם כמה מצלמות, צריך לפתוח CameraDevice (המצלמה הלוגית) וליצור איתה אינטראקציה סשן. יצירת סשן יחיד באמצעות ה-API CameraDevice.createCaptureSession(SessionConfiguration config), שהיה נוספה ברמת API 28. להגדרת הסשן יש מספר פלט הגדרות אישיות, ולכל אחת מהן יש קבוצה של יעדי פלט מזהה המצלמה הפיזי הרצוי.

איור 3. המודל sessionConfiguration ו-OutputConfiguration

לבקשות איסוף משויך יעד פלט. המסגרת קובע לאיזו מצלמה פיזית (או לוגית) הבקשות יישלחו על סמך הנתונים איזה יעד פלט מצורף. אם יעד הפלט תואם לאחד יעדי פלט שנשלחו כהגדרת פלט יחד עם הגדרה פיזית מזהה המצלמה, והמצלמה הפיזית מקבלת ומעבדת את הבקשה.

שימוש בשתי מצלמות פיזיות

תוספת נוספת לממשקי ה-API של המצלמה שמיועדת לריבוי מצלמות היא היכולת לזהות במצלמות ההיגיון ולמצוא את המצלמות הפיזיות מאחוריהן. אפשר להגדיר כדי לזהות זוגות פוטנציאליים של מצלמות פיזיות כדי להחליף את אחד מחיבורי המצלמה הלוגיים:

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

הטיפול במצב של המצלמות הפיזיות נשלט על ידי המצלמה הלוגית. שפת תרגום פותחים 'מצלמה כפולה', לפתוח את המצלמה הלוגית שתואמת מצלמות:

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

חוץ מהבחירה איזו מצלמה לפתוח, התהליך זהה לתהליך הפתיחה מצלמה בגרסאות קודמות של Android. יצירת סשן של תיעוד באמצעות session configuration API מורה ל-framework לשייך יעדים מסוימים עם מזהי מצלמות פיזיות ספציפיים:

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

צפייה createCaptureSession כדי לקבל מידע על שילובי השידורים הנתמכים. שילוב שידורים חיים הוא למספר שידורים במצלמה לוגית אחת. התאימות כוללת גם להשתמש באותה הגדרה והחלפת אחד מהשידורים האלה בשני שידורים חיים משתי מצלמות פיזיות שהן חלק מאותה מצלמה לוגית.

עם סשן מצלמה מוכנים, לשלוח את מה שרציתם בקשות לתיעוד. כל אחד היעד של בקשת הלכידה מקבל את הנתונים מהפיזי או להשתמש במצלמה הלוגית, אם בכלל.

תרחיש לדוגמה לזום

אפשר להשתמש במיזוג של מצלמות פיזיות לשידור יחיד כדי שהמשתמשים יכולים לעבור בין המצלמות הפיזיות השונות כדי לחוות טווח שונה של ראייה, וכך תופסים "רמת זום" שונה.

איור 4. דוגמה להחלפת מצלמות בתרחיש לדוגמה של רמת הזום (ממודעת Pixel 3)

בשלב הראשון, בוחרים את שתי המצלמות הפיזיות כדי לאפשר למשתמשים לעבור ביניהם. כדי להשיג את האפקט המקסימלי, אפשר לבחור את זוג המצלמות שמספקות אורך המוקד המינימלי והמקסימלי.

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

ארכיטקטורה הגיונית לשם כך תהיה SurfaceViews – אחד לכל שידור. הערכים מסוג 'SurfaceViews' מוחלפים בהתבסס על אינטראקציה של משתמש, כך שרק אחד מהם גלויים בכל זמן נתון.

הקוד הבא מראה איך לפתוח את המצלמה הלוגית ולהגדיר את המצלמה פלטים, ליצור סשן צילום ולהתחיל שני שידורים של תצוגה מקדימה:

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

כל מה שנשאר לעשות הוא לספק למשתמש ממשק משתמש שיוכל לעבור בין בפלטפורמות שונות, כמו לחצן או הקשה כפולה על SurfaceView. אפשר אפילו לבצע סוג כלשהו של ניתוח סצנות ולעבור בין שני הזרמים באופן אוטומטי.

עיוות עדשה

כל העדשות מייצרות עיוותים ברמה מסוימת. ב-Android, אפשר לבצע שאילתה על עיוות שנוצר על ידי עדשות באמצעות CameraCharacteristics.LENS_DISTORTION שמחליפה את הנתונים שהוצאו משימוש CameraCharacteristics.LENS_RADIAL_DISTORTION במצלמות לוגיות, העיוות הוא מינימלי והאפליקציה יכולה להשתמש את הפריימים יותר או פחות כפי שהם מגיעים מהמצלמה. במצלמות פיזיות, יכולות להיות תצורות עדשות שונות מאוד, במיוחד עדשות מצלמה.

במכשירים מסוימים ייתכן שיתבצע תיקון עיוות אוטומטי דרך CaptureRequest.DISTORTION_CORRECTION_MODE תיקון עיוות מופעל כברירת מחדל ברוב המכשירים.

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

הגדרה של בקשת תיעוד במצב הזה יכולה להשפיע על קצב הפריימים שמופקת על ידי המצלמה. אפשר להגדיר את תיקון העיוות רק לגבי צילומי תמונות סטילס.