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

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

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

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

  • التكبير/التصغير: التبديل بين الكاميرات حسب منطقة الاقتصاص أو البُعد البؤري الذي تم اختياره
  • العمق: استخدام كاميرات متعدّدة لإنشاء خريطة عمق
  • الخلفية الضبابية: استخدام معلومات العمق المستنتَجة لمحاكاة نطاق تركيز ضيق يشبه نطاق كاميرا DSLR

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

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

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

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

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

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

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

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

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

بسبب التغييرات التي طرأت على مستند تعريف معايير التوافق (CDD) مع Android، فإنّ واجهة برمجة التطبيقات المتعددة الكاميرات تأتي أيضًا مع توقعات معيّنة من المطوّرين. كانت الأجهزة المزوّدة بكاميرات مزدوجة متوفّرة قبل إصدار 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 من واجهة برمجة التطبيقات. تحتوي إعدادات الجلسة على عدد من إعدادات الإخراج، ولكل منها مجموعة من أهداف الإخراج، ويمكن اختياريًا تحديد رقم تعريف الكاميرا المادية.

نموذج SessionConfiguration وOutputConfiguration
الشكل 3: نموذج SessionConfiguration وOutputConfiguration

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

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

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

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

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

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