Multi-camera API

توجه: این صفحه به بسته Camera2 اشاره دارد. توصیه می‌کنیم از CameraX استفاده کنید، مگراینکه برنامه‌تان به ویژگی‌های خاص و سطح پایین Camera2 نیاز داشته باشد. ‫CameraX و Camera2 هر دو از Android 5.0 (سطح میانای برنامه‌سازی کاربردی ۲۱) و بالاتر پشتیبانی می‌کنند.

چنددوربینی با Android 9 (سطح میانای برنامه‌سازی کاربردی ۲۸) معرفی شد. از زمان انتشار آن، دستگاه‌هایی که از این API پشتیبانی می‌کنند به بازار آمده‌اند. بسیاری از موارد استفاده از چند دوربین به‌طور محکم با پیکربندی سخت‌افزاری خاصی مرتبط هستند. به‌عبارت دیگر، همه موارد استفاده با همه دستگاه‌ها سازگار نیستند، که این امر ویژگی‌های چنددوربینی را به گزینه خوبی برای ویژگی تحویل Play تبدیل می‌کند.

برخی‌از موارد استفاده معمول عبارت‌اند از:

  • بزرگ‌نمایی: جابه‌جایی بین دوربین‌ها بسته به ناحیه برش یا فاصله کانونی انتخاب‌شده.
  • عمق: استفاده از چند دوربین برای ساختن نقشه عمق.
  • بوکه: استفاده از اطلاعات عمق استنباط‌شده برای شبیه‌سازی دامنه فوکوس باریک شبیه DSLR.

تفاوت بین دوربین‌های منطقی و فیزیکی

برای درک میانای برنامه‌سازی کاربردی چنددوربینه باید تفاوت بین دوربین‌های منطقی و فیزیکی را درک کنید. برای مثال، دستگاهی را با سه دوربین پشت درنظر بگیرید. در این مثال، هرکدام از سه دوربین پشت دوربین فیزیکی درنظر گرفته می‌شود. دوربین منطقی درواقع گروهی از دو یا چند دوربین فیزیکی است. خروجی دوربین منطقی می‌تواند جاری‌سازی‌ای باشد که از یکی از دوربین‌های فیزیکی زیرین می‌آید، یا جاری‌سازی ادغام‌شده‌ای که به‌طور هم‌زمان از بیش‌از یک دوربین فیزیکی زیرین می‌آید. در هر دو حالت، جاری‌سازی توسط «لایه انتزاعی سخت‌افزار» (HAL) دوربین مدیریت می‌شود.

بسیاری از سازندگان تلفن برنامه‌های دوربین طرف اول را توسعه می‌دهند که معمولاً به‌صورت ازپیش‌نصب‌شده در دستگاه‌هایشان ارائه می‌شود. برای استفاده از همه قابلیت‌های سخت‌افزار، ممکن است از میاناهای برنامه‌سازی کاربردی خصوصی یا پنهان استفاده کنند یا از پیاده‌سازی درایور رفتار ویژه‌ای دریافت کنند که برنامه‌های دیگر به آن دسترسی ندارند. برخی‌از دستگاه‌ها مفهوم دوربین‌های منطقی را با ارائه جریان ادغام‌شده‌ای از فریم‌ها از دوربین‌های فیزیکی مختلف پیاده‌سازی می‌کنند، اما فقط برای برخی‌از برنامه‌های دارای امتیاز. اغلب، فقط یکی از دوربین‌های فیزیکی درمعرض چارچوب قرار می‌گیرد. وضعیت توسعه‌دهندگان طرف سوم قبل‌از Android 9 در نمودار زیر نشان داده شده است:

قابلیت‌های دوربین معمولاً فقط برای برنامه‌های دارای امتیاز دردسترس است
شکل ۱. قابلیت‌های دوربین معمولاً فقط برای برنامه‌های دارای امتیاز دردسترس است

از Android 9 به‌بعد، میاناهای برنامه‌سازی کاربردی خصوصی دیگر در برنامه‌های Android مجاز نیستند. با گنجاندن پشتیبانی از چند دوربین در چارچوب، روال‌های مطلوب Android اکیداً توصیه می‌کنند که سازندگان تلفن برای همه دوربین‌های فیزیکی که در یک جهت قرار دارند، یک دوربین منطقی ارائه دهند. آنچه توسعه‌دهندگان شخص ثالث باید انتظار داشته باشند در دستگاه‌های دارای Android 9 و بالاتر ببینند به شرح زیر است:

دسترسی کامل توسعه‌دهنده به همه دستگاه‌های دوربین از Android 9 شروع می‌شود
شکل ۲. دسترسی کامل توسعه‌دهنده به همه دستگاه‌های دوربین از Android 9

آنچه دوربین منطقی ارائه می‌دهد کاملاً به پیاده‌سازی OEM از HAL دوربین بستگی دارد. برای مثال، دستگاهی مثل Pixel 3 دوربین منطقی خود را به‌گونه‌ای پیاده‌سازی می‌کند که یکی از دوربین‌های فیزیکی خود را براساس فاصله کانونی و منطقه برش درخواست‌شده انتخاب می‌کند.

میانای برنامه‌سازی کاربردی چنددوربینی

میانای برنامه‌سازی کاربردی جدید ثابت‌ها، کلاس‌ها، و روش‌های جدید زیر را اضافه می‌کند:

به‌دلیل تغییرات در «سند تعریف سازگاری Android» (CDD)، میانای برنامه‌سازی کاربردی چنددوربینه نیز با انتظارات خاصی از توسعه‌دهندگان همراه است. دستگاه‌های دارای دوربین دوگانه قبل‌از Android 9 وجود داشتند، اما باز کردن بیش‌از یک دوربین به‌طور هم‌زمان شامل آزمون و خطا بود. در Android نسخه ۹ و بالاتر، چنددوربینی مجموعه‌ای از قوانین را برای مشخص کردن زمان امکان باز کردن یک جفت دوربین فیزیکی که بخشی از یک دوربین منطقی هستند ارائه می‌دهد.

در اکثر موارد، دستگاه‌های دارای Android نسخه ۹ و بالاتر همه دوربین‌های فیزیکی را (به‌جز احتمالاً انواع حسگرهای کمتر رایج مانند مادون قرمز) به‌همراه یک دوربین منطقی با استفاده آسان‌تر نشان می‌دهند. برای هر ترکیبی از جاری‌سازی‌ها که تضمین می‌شود کار کنند، یک جاری‌سازی متعلق به دوربین منطقی می‌تواند با دو جاری‌سازی از دوربین‌های فیزیکی زیرین جایگزین شود.

چندین جاری‌سازی هم‌زمان

استفاده هم‌زمان از چند جاری‌سازی دوربین قوانین استفاده هم‌زمان از چند جاری‌سازی در یک دوربین را پوشش می‌دهد. با یک افزودنی قابل‌توجه، همان قوانین برای چند دوربین اعمال می‌شود. CameraMetadata.REQUEST_AVAILABLE_CAPABILITIES_LOGICAL_MULTI_CAMERA نحوه جایگزین کردن جریان منطقی YUV_420_888 یا جریان خام با دو جریان فیزیکی را توضیح می‌دهد. یعنی هر جاری‌سازی از نوع YUV یا RAW را می‌توان با دو جاری‌سازی از نوع و اندازه یکسان جایگزین کرد. می‌توانید با جاری‌سازی دوربین با پیکربندی تضمین‌شده زیر برای دستگاه‌های تک‌دوربینه شروع کنید:

  • جاری‌سازی ۱: نوع YUV، اندازه MAXIMUM از دوربین منطقی id = 0

سپس، دستگاهی با پشتیبانی از چند دوربین به شما امکان می‌دهد جلسه‌ای ایجاد کنید که آن جاری‌سازی منطقی YUV را با دو جاری‌سازی فیزیکی جایگزین می‌کند:

  • جاری‌سازی ۱: نوع YUV، اندازه MAXIMUM از دوربین فیزیکی id = 1
  • جاری‌سازی ۲: نوع YUV، اندازه MAXIMUM از دوربین فیزیکی id = 2

می‌توانید یک جریان YUV یا RAW را با دو جریان معادل جایگزین کنید، اگر و تنها اگر آن دو دوربین بخشی از یک گروه‌بندی منطقی دوربین باشند، که در زیر CameraCharacteristics.getPhysicalCameraIds() فهرست شده است.

تضمین‌های ارائه‌شده توسط چارچوب فقط حداقل الزامات لازم برای دریافت هم‌زمان قاب‌ها از بیش‌از یک دوربین فیزیکی است. جاری‌سازی‌های اضافی در اکثر دستگاه‌ها پشتیبانی می‌شود و گاهی حتی امکان باز کردن مستقل چندین دستگاه دوربین فیزیکی را فراهم می‌کند. ازآنجایی‌که این تضمین قطعی ازسوی چارچوب نیست، انجام این کار مستلزم آزمایش و تنظیم برای هر دستگاه بااستفاده از آزمون و خطا است.

ایجاد جلسه با چند دوربین فیزیکی

هنگام استفاده از دوربین‌های فیزیکی در دستگاهی که چند دوربین دارد، یک CameraDevice (دوربین منطقی) را باز کنید و در یک جلسه با آن تعامل داشته باشید. ایجاد کردن جلسه تکی بااستفاده از API CameraDevice.createCaptureSession(SessionConfiguration config)، که در سطح API‏ ۲۸ اضافه شده است. پیکربندی جلسه دارای تعدادی پیکربندی خروجی است که هرکدام مجموعه‌ای از اهداف خروجی و به‌صورت اختیاری، شناسه دوربین فیزیکی انتخاب‌شده دارند.

مدل SessionConfiguration و OutputConfiguration
شکل ۳. مدل SessionConfiguration و OutputConfiguration

درخواست‌های ضبط یک هدف خروجی مرتبط با خود دارند. چارچوب براساس اینکه کدام هدف خروجی متصل است، تعیین می‌کند درخواست‌ها به کدام دوربین فیزیکی (یا منطقی) ارسال شود. اگر هدف خروجی با یکی از اهداف خروجی که به‌عنوان پیکربندی خروجی همراه با شناسه دوربین فیزیکی ارسال شده است مطابقت داشته باشد، آن دوربین فیزیکی درخواست را دریافت و پردازش می‌کند.

استفاده از یک جفت دوربین فیزیکی

یکی دیگر از موارد اضافه‌شده به میاناهای برنامه‌سازی کاربردی دوربین برای چنددوربین، توانایی شناسایی دوربین‌های منطقی و پیدا کردن دوربین‌های فیزیکی پشت آن‌ها است. می‌توانید تابعی را تعریف کنید که به شناسایی جفت‌های احتمالی دوربین‌های فیزیکی کمک کند تا بتوانید از آن‌ها برای جایگزینی یکی از جاری‌سازی‌های دوربین منطقی استفاده کنید:

کاتلین

/**
 * 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
}

جاوا

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

مدیریت وضعیت دوربین‌های فیزیکی توسط دوربین منطقی کنترل می‌شود. برای باز کردن «دوربین دوگانه»، دوربین منطقی مربوط به دوربین‌های فیزیکی را باز کنید:

کاتلین

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

جاوا

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 پیکربندی جلسه جدید به چارچوب می‌گوید که اهداف خاصی را با شناسه‌های دوربین فیزیکی خاص مرتبط کند:

کاتلین

/**
 * 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)
    }
}

جاوا

/**
 * 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

می‌توان دوربین‌های فیزیکی را در یک جاری‌سازی واحد ادغام کرد تا کاربران بتوانند بین دوربین‌های فیزیکی مختلف جابه‌جا شوند و میدان دید متفاوتی را تجربه کنند و به‌طور مؤثری «سطح بزرگ‌نمایی» متفاوتی را ثبت کنند.

نمونه‌ای از تعویض دوربین‌ها برای مورد استفاده سطح بزرگ‌نمایی
شکل ۴. نمونه‌ای از جابه‌جایی دوربین‌ها برای مورد استفاده سطح بزرگ‌نمایی (از آگهی Pixel 3)

با انتخاب جفت دوربین فیزیکی شروع کنید تا کاربران بتوانند بین آن‌ها جابه‌جا شوند. برای حداکثر تأثیر، می‌توانید جفت دوربینی را انتخاب کنید که حداقل و حداکثر فاصله کانونی موجود را ارائه می‌دهند.

کاتلین

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
}

جاوا

// 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 براساس تعامل کاربر جابه‌جا می‌شوند تا در هر زمان فقط یکی قابل‌مشاهده باشد.

کد زیر نحوه باز کردن دوربین منطقی، پیکربندی خروجی‌های دوربین، ایجاد جلسه دوربین، و شروع دو جاری‌سازی پیش‌نمایش را نشان می‌دهد:

کاتلین

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

جاوا

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 پیاده‌سازی کنند. تصحیح اعوجاج به‌طور پیش‌فرض برای اکثر دستگاه‌ها روشن است.

کاتلین

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

جاوا

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

تنظیم درخواست ضبط در این حالت می‌تواند بر نرخ فریمی که دوربین می‌تواند تولید کند تأثیر بگذارد. می‌توانید انتخاب کنید که تصحیح اعوجاج فقط برای عکس‌های ثابت تنظیم شود.