ממשק API של Multi-camera

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

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

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

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

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

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

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

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

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

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

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

‫API של מצלמות מרובות

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

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

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

כמה שידורים בו-זמנית

שימוש בכמה סטרימינג מהמצלמה בו-זמנית מפרט את הכללים לשימוש בכמה סטרימינג בו-זמנית ממצלמה אחת. אותם כללים חלים על כמה מצלמות, עם תוספת אחת חשובה. CameraMetadata.REQUEST_AVAILABLE_CAPABILITIES_LOGICAL_MULTI_CAMERA מסביר איך להחליף סטרימינג לוגי של YUV_420_888 או סטרימינג גולמי בשני סטרימינג פיזיים. כלומר, אפשר להחליף כל סטרימינג מסוג 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. להגדרת הסשן יש כמה הגדרות פלט, ולכל אחת מהן יש קבוצה של יעדי פלט, ואפשר גם לבחור מזהה מצלמה פיזית.

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

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

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

תוספת נוספת לממשקי ה-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<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;
}

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

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. כשיוצרים סשן צילום באמצעות ה-API החדש להגדרת סשנים, המערכת משייכת יעדים מסוימים למזהים ספציפיים של מצלמות פיזיות:

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

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

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

תרחיש לדוגמה של שימוש ב-Zoom

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

דוגמה להחלפת מצלמות בתרחיש לדוגמה של רמת זום
איור 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(), ...);

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