واجهة برمجة تطبيقات كاميرات متعددة

ملاحظة: تشير هذه الصفحة إلى حزمة Camera2. ننصحك باستخدام الكاميراX ما لم يكن تطبيقك يتطلب ميزات محدَّدة منخفضة المستوى من تطبيق Camera2. يتوافق كل من CameraX و Camera2 مع نظام التشغيل Android 5.0 (المستوى 21 لواجهة برمجة التطبيقات) والإصدارات الأحدث.

تم تقديم استخدام كاميرات متعدّدة مع Android 9 (المستوى 28 من واجهة برمجة التطبيقات). منذ إصداره، الأجهزة التي تعمل في السوق التي تدعم واجهة برمجة التطبيقات. حالات استخدام كاميرات متعدّدة ومقترنة إلى حد كبير بتكوين جهاز معين. بعبارة أخرى، ليس تتوافق جميع حالات الاستخدام مع كل جهاز، ما يجعل استخدام كاميرات متعددة تحتوي على مرشح جيد لميزة التسليم:

تتضمن بعض حالات الاستخدام النموذجية ما يلي:

  • التكبير أو التصغير: التبديل بين الكاميرات بناءً على منطقة الاقتصاص أو البؤر البؤري المطلوب المنقار.
  • العمق: استخدام كاميرات متعددة لإنشاء خريطة العمق.
  • الضوء: استخدام معلومات العمق المستنتَجة لمحاكاة كاميرا رقمية ذات عدسة أحادية عاكسة (DSLR) ضيقة نطاق التركيز.

الفرق بين الكاميرات المنطقية والفيزيائية

يتطلب فهم واجهة برمجة التطبيقات للكاميرات المتعددة فهم الفرق بين والكاميرات المنطقية والمادية. كمرجع لك، يمكنك اختيار جهاز يتضمن ثلاثة أجهزة الكاميرات الخلفية. في هذا المثال، تكون كل كاميرا من الكاميرات الخلفية الثلاث تُعد كاميرا فعلية. تكون الكاميرا المنطقية عبارة عن مجموعة من اثنين أو أكثر تلك الكاميرات الفعلية. ناتج الوضع المنطقي الكاميرا هو بث يأتي من إحدى الكاميرات المادية الأساسية، أو بث مدمج صادر من أكثر من كاميرا فعلية واحدة في الوقت نفسه. في كلتا الحالتين، يتم التعامل مع البث بواسطة جهاز الكاميرا. طبقة التجريد (HAL).

وتطور العديد من الشركات المصنعة للهواتف، تطبيقات الكاميرا التابعة للطرف الأول، والتي عادةً ما مثبتة مسبقًا على أجهزتهم. لاستخدام جميع إمكانيات الأجهزة، قد يستخدم هؤلاء الأشخاص واجهات برمجة تطبيقات خاصة أو مخفية أو قد يخضعون لمعاملة خاصة من تنفيذ برنامج التشغيل الذي لا يمكن للتطبيقات الأخرى الوصول إليه. بعض الإشعارات على تنفيذ مفهوم الكاميرات المنطقية من خلال توفير تدفق مدمج الإطارات من الكاميرات الفعلية المختلفة، ولكن إلى بعض الكاميرات المميزة التطبيقات. وغالبًا ما تتعرض كاميرا واحدة فقط إطار العمل. بالنسبة إلى المطوِّرين التابعين لجهات خارجية قبل الإصدار 9 من Android هو كما هو موضح في الرسم التخطيطي التالي:

الشكل 1. لا تتوفّر إمكانات الكاميرا عادةً إلا التطبيقات الحاصلة على امتياز

بدءًا من الإصدار 9 من نظام Android، لم يعد مسموحًا بواجهات برمجة التطبيقات الخاصة في تطبيقات Android. وبفضل إمكانية استخدام كاميرات متعدّدة في إطار العمل، يحقّق Android ننصح بشدّة بأن تعرض الشركات المصنّعة للهاتف كاميرا منطقية لكل الكاميرات الفعلية التي تتجه نحو نفس الاتجاه فيما يلي ما يلي من المفترض أن يتوقع المطوّرون التابعون لجهات خارجية ظهور هذه المعلومات على الأجهزة التي تعمل بنظام Android 9 أعلى:

الشكل 2. إذن وصول المطوّر إلى جميع أجهزة الكاميرات بدءًا من الإصدار 9 من نظام Android

ما تقدمه الكاميرا المنطقية يعتمد كليًا على التنفيذ من قِبل المصنّع الأصلي للجهاز من طبقة تجريد الأجهزة (HAL) للكاميرا. على سبيل المثال، ينفِّذ جهاز مثل Pixel 3 قيمه المنطقية الكاميرا بطريقة تختار فيها إحدى الكاميرات الفعلية بناءً على البعد البؤري ومنطقة الاقتصاص.

واجهة برمجة التطبيقات للكاميرات المتعددة

تضيف واجهة برمجة التطبيقات الجديدة الثوابت والفئات والطرق الجديدة التالية:

نظرًا للتغييرات التي طرأت على مستند تعريف التوافق مع Android (CDD)، كما تأتي واجهة برمجة التطبيقات للكاميرات المتعددة مع توقعات معينة من المطورين. الأجهزة الذي كان يتضمّن كاميرتَين قبل نظام 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 (الكاميرا المنطقية) والتفاعل معها من خلال كاميرا جلسة المراجعة. إنشاء الجلسة الفردية باستخدام واجهة برمجة التطبيقات CameraDevice.createCaptureSession(SessionConfiguration config)، والذي كان التي تمت إضافتها في المستوى 28 من واجهة برمجة التطبيقات. تحتوي إعدادات الجلسة على عدد من النتائج يحتوي كل منها على مجموعة من أهداف الناتج، واختياريًا معرّف الكاميرا الفعلي المطلوب.

الشكل 3. نموذج إعداد الجلسات ومخرجات الضبط

تحتوي طلبات الالتقاط على هدف ناتج مرتبط بها. إطار العمل الكاميرا المادية (أو المنطقية) التي يتم إرسال الطلبات إليها على أساس هدف الإخراج المرفق. وإذا كان هدف الناتج يتوافق مع إحدى للمخرجات المستهدفة التي تم إرسالها كإعداد للمخرجات إلى جانب الكاميرا، فستتلقّى هذه الكاميرا الطلب وتعالجه.

استخدام كاميرتين

إضافة أخرى إلى واجهات برمجة التطبيقات للكاميرا متعددة الكاميرات هي القدرة على تحديد الكاميرات المنطقية والعثور على الكاميرات الفعلية خلفها. يمكنك تحديد للمساعدة في تحديد الأزواج المحتملة من الكاميرات الفعلية التي يمكنك استخدامها لاستبدال أحد مجموعات بث الكاميرا المنطقية:

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 السابقة. جارٍ إنشاء جلسة تسجيل باستخدام تطلب واجهة برمجة التطبيقات لإعداد الجلسات من إطار العمل ربط أهداف معيّنة معرّفات الكاميرا الفعلية المحددة:

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

يمكن أن يؤدي ضبط طلب التقاط في هذا الوضع إلى التأثير في عدد اللقطات في الثانية من إنتاج الكاميرا. يمكنك اختيار ضبط تصحيح التشوّه على فقط لالتقاط الصور الثابتة.