Camera1 را به CameraX منتقل کنید

اگر برنامه شما از کلاس اصلی Camera ("Camera1") استفاده می‌کند که از اندروید 5.0 (API سطح 21) منسوخ شده است، اکیداً توصیه می‌کنیم آن را به یک API دوربین مدرن اندروید به‌روزرسانی کنید. اندروید CameraX (یک API دوربین استاندارد و قوی Jetpack ) و Camera2 (یک API چارچوب سطح پایین) را ارائه می‌دهد. در بیشتر موارد، توصیه می‌کنیم برنامه خود را به CameraX منتقل کنید. دلیل آن به شرح زیر است:

  • سهولت استفاده: CameraX جزئیات سطح پایین را مدیریت می‌کند، به طوری که می‌توانید کمتر روی ساخت یک تجربه دوربین از ابتدا تمرکز کنید و بیشتر روی متمایز کردن برنامه خود تمرکز کنید.
  • CameraX پراکندگی را برای شما مدیریت می‌کند: CameraX هزینه‌های نگهداری بلندمدت و کدهای مختص دستگاه را کاهش می‌دهد و تجربیات با کیفیت‌تری را برای کاربران به ارمغان می‌آورد. برای اطلاعات بیشتر در این مورد، به پست وبلاگ «سازگاری بهتر دستگاه با CameraX» مراجعه کنید.
  • قابلیت‌های پیشرفته: CameraX با دقت طراحی شده است تا قابلیت‌های پیشرفته را به راحتی در برنامه شما بگنجاند. به عنوان مثال، می‌توانید به راحتی با CameraX Extensions حالت بوکه، روتوش صورت، HDR (محدوده دینامیکی بالا) و حالت عکاسی در شب با روشنایی کم را روی عکس‌های خود اعمال کنید.
  • قابلیت به‌روزرسانی: اندروید در طول سال قابلیت‌های جدید و رفع اشکالات را برای CameraX منتشر می‌کند. با مهاجرت به CameraX، برنامه شما جدیدترین فناوری دوربین اندروید را با هر نسخه CameraX دریافت می‌کند، نه فقط با انتشار نسخه‌های سالانه اندروید.

در این راهنما، سناریوهای رایج برای برنامه‌های دوربین را خواهید یافت. هر سناریو شامل یک پیاده‌سازی Camera1 و یک پیاده‌سازی CameraX برای مقایسه است.

وقتی صحبت از مهاجرت می‌شود، گاهی اوقات برای ادغام با یک کدبیس موجود به انعطاف‌پذیری بیشتری نیاز دارید. تمام کدهای CameraX در این راهنما دارای پیاده‌سازی CameraController هستند - اگر می‌خواهید روش ساده‌تری برای استفاده از CameraX داشته باشید، عالی است - و همچنین دارای پیاده‌سازی CameraProvider هستند - اگر به انعطاف‌پذیری بیشتری نیاز دارید، عالی است. برای کمک به شما در تصمیم‌گیری در مورد اینکه کدام یک برای شما مناسب است، در اینجا مزایای هر کدام آمده است:

کنترل‌کننده دوربین

ارائه دهنده دوربین

به کد راه‌اندازی کمی نیاز دارد امکان کنترل بیشتر را فراهم می‌کند
اجازه دادن به CameraX برای مدیریت بیشتر فرآیند راه‌اندازی به این معنی است که قابلیت‌هایی مانند فوکوس با لمس و زوم با دو انگشت به صورت خودکار کار می‌کنند. از آنجایی که توسعه‌دهنده‌ی برنامه تنظیمات را مدیریت می‌کند، فرصت‌های بیشتری برای سفارشی‌سازی پیکربندی وجود دارد، مانند فعال کردن چرخش تصویر خروجی یا تنظیم فرمت تصویر خروجی در ImageAnalysis
الزام PreviewView برای پیش‌نمایش دوربین به CameraX اجازه می‌دهد تا یکپارچه‌سازی سرتاسری یکپارچه‌ای را ارائه دهد، همانطور که در یکپارچه‌سازی کیت ML ما وجود دارد که می‌تواند مختصات نتیجه مدل ML (مانند کادرهای محدودکننده چهره) را مستقیماً بر روی مختصات پیش‌نمایش نگاشت کند. امکان استفاده از یک «Surface» سفارشی برای پیش‌نمایش دوربین، انعطاف‌پذیری بیشتری را فراهم می‌کند، مانند استفاده از کد «Surface» موجود شما که می‌تواند ورودی سایر بخش‌های برنامه شما باشد.

اگر در تلاش برای مهاجرت به مشکل برخوردید، در گروه بحث CameraX با ما تماس بگیرید.

قبل از اینکه مهاجرت کنید

مقایسه میزان مصرف CameraX با Camera1

اگرچه ممکن است کد متفاوت به نظر برسد، مفاهیم اساسی در Camera1 و CameraX بسیار مشابه هستند. CameraX قابلیت‌های رایج دوربین را در موارد استفاده خلاصه می‌کند و در نتیجه، بسیاری از وظایفی که در Camera1 به توسعه‌دهنده واگذار شده بود، به طور خودکار توسط CameraX انجام می‌شوند. چهار UseCase در CameraX وجود دارد که می‌توانید برای انواع وظایف دوربین از آنها استفاده کنید: Preview ، ImageCapture ، VideoCapture و ImageAnalysis .

یک نمونه از مدیریت جزئیات سطح پایین توسط CameraX برای توسعه‌دهندگان، ViewPort است که بین UseCase های فعال به اشتراک گذاشته می‌شود. این امر تضمین می‌کند که همه UseCase ها دقیقاً پیکسل‌های یکسانی را ببینند. در Camera1، شما باید خودتان این جزئیات را مدیریت کنید. با توجه به نسبت ابعاد متغیر در دستگاه‌ها، تطبیق پیش‌نمایش با رسانه‌های ضبط‌شده دشوار است.

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

در نهایت، CameraX چرخش و مقیاس‌بندی را بدون نیاز به هیچ کد اضافی از طرف شما مدیریت می‌کند. در مورد یک Activity با جهت‌گیری قفل نشده، تنظیم UseCase هر بار که دستگاه چرخانده می‌شود انجام می‌شود، زیرا سیستم Activity در صورت تغییر جهت از بین می‌برد و دوباره ایجاد می‌کند. این امر منجر به این می‌شود که UseCases چرخش هدف خود را هر بار به طور پیش‌فرض مطابق با جهت‌گیری صفحه نمایش تنظیم کنند. درباره چرخش‌ها در CameraX بیشتر بخوانید .

قبل از پرداختن به جزئیات، در اینجا نگاهی سطح بالا به UseCase های CameraX و نحوه ارتباط آنها با یک برنامه Camera1 می‌اندازیم. (مفاهیم CameraX با رنگ آبی و مفاهیم Camera1 با رنگ سبز نشان داده شده‌اند.)

دوربین ایکس

پیکربندی CameraController / CameraProvider
پیش‌نمایش ضبط تصویر ضبط ویدئو تحلیل تصویر
مدیریت پیش‌نمایش Surface و تنظیم آن روی دوربین تنظیم PictureCallback و فراخوانی تابع takePicture() روی دوربین مدیریت پیکربندی دوربین و ضبط‌کننده رسانه به ترتیب خاص کد تحلیل سفارشی ساخته شده بر روی پیش‌نمایش Surface
کد مخصوص دستگاه
مدیریت چرخش و مقیاس‌بندی دستگاه
مدیریت جلسه دوربین (انتخاب دوربین، مدیریت چرخه عمر)

دوربین1

سازگاری و عملکرد در CameraX

CameraX از دستگاه‌هایی که اندروید ۵.۰ (سطح API ۲۱) و بالاتر را اجرا می‌کنند پشتیبانی می‌کند. این تعداد بیش از ۹۸٪ از دستگاه‌های اندروید موجود را شامل می‌شود. CameraX طوری ساخته شده است که به طور خودکار تفاوت‌های بین دستگاه‌ها را مدیریت کند و نیاز به کد مخصوص دستگاه را در برنامه شما کاهش دهد. علاوه بر این، ما بیش از ۱۵۰ دستگاه فیزیکی را در تمام نسخه‌های اندروید از ۵.۰ به بعد در آزمایشگاه تست CameraX خود آزمایش می‌کنیم. می‌توانید لیست کامل دستگاه‌ها را در آزمایشگاه تست مرور کنید.

CameraX از یک Executor برای هدایت پشته دوربین استفاده می‌کند. اگر برنامه شما الزامات خاصی برای threading دارد، می‌توانید Executor خود را روی CameraX تنظیم کنید . در صورت عدم تنظیم، CameraX یک Executor داخلی پیش‌فرض بهینه شده ایجاد و استفاده می‌کند. بسیاری از APIهای پلتفرم که CameraX بر روی آنها ساخته شده است، نیاز به مسدود کردن ارتباط بین پردازشی (IPC) با سخت‌افزار دارند که گاهی اوقات می‌تواند صدها میلی‌ثانیه طول بکشد تا پاسخ دهد. به همین دلیل، CameraX فقط این APIها را از threadهای پس‌زمینه فراخوانی می‌کند، که تضمین می‌کند thread اصلی مسدود نشده و رابط کاربری روان باقی بماند. درباره threads بیشتر بخوانید .

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

مفاهیم توسعه اندروید

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

  • View Binding یک کلاس اتصال برای فایل‌های طرح‌بندی XML شما ایجاد می‌کند و به شما امکان می‌دهد تا به viewهای خود در Activities ارجاع دهید ، همانطور که در چندین قطعه کد بعدی نشان داده شده است. تفاوت‌هایی بین view binding و findViewById() (روش قبلی برای ارجاع به viewها) وجود دارد، اما در کد زیر باید بتوانید خطوط اتصال view را با فراخوانی مشابه findViewById() جایگزین کنید.
  • کوروتین‌های ناهمگام یک الگوی طراحی همزمانی هستند که در کاتلین ۱.۳ اضافه شده‌اند و می‌توانند برای مدیریت متدهای CameraX که ListenableFuture برمی‌گردانند، استفاده شوند. این کار با کتابخانه Jetpack Concurrent از نسخه ۱.۱.۰ آسان‌تر شده است. برای افزودن یک کوروتین ناهمگام به برنامه خود:
    1. implementation("androidx.concurrent:concurrent-futures-ktx:1.1.0") به فایل Gradle خود اضافه کنید.
    2. هر کد CameraX که ListenableFuture برمی‌گرداند را در یک بلوک launch یا تابع معلق قرار دهید.
    3. یک فراخوانی await() به فراخوانی تابع اضافه کنید که ListenableFuture را برمی‌گرداند.
    4. برای درک عمیق‌تر از نحوه‌ی کار کوروتین‌ها، به راهنمای « شروع یک کوروتین» مراجعه کنید.

سناریوهای رایج را منتقل کنید

این بخش نحوه‌ی انتقال سناریوهای رایج از Camera1 به CameraX را توضیح می‌دهد. هر سناریو شامل پیاده‌سازی Camera1، پیاده‌سازی CameraProvider در CameraX و پیاده‌سازی CameraController در CameraX می‌شود.

انتخاب دوربین

در اپلیکیشن دوربین خود، یکی از اولین چیزهایی که ممکن است بخواهید ارائه دهید، راهی برای انتخاب دوربین‌های مختلف است.

دوربین1

در Camera1، می‌توانید Camera.open() بدون هیچ پارامتری برای باز کردن اولین دوربین پشتی فراخوانی کنید، یا می‌توانید یک شناسه عدد صحیح برای دوربینی که می‌خواهید باز شود، ارسال کنید. در اینجا مثالی از نحوه نمایش آن آورده شده است:

// Camera1: select a camera from id.

// Note: opening the camera is a non-trivial task, and it shouldn't be
// called from the main thread, unlike CameraX calls, which can be
// on the main thread since CameraX kicks off background threads
// internally as needed.

private fun safeCameraOpen(id: Int): Boolean {
    return try {
        releaseCameraAndPreview()
        camera = Camera.open(id)
        true
    } catch (e: Exception) {
        Log.e(TAG, "failed to open camera", e)
        false
    }
}

private fun releaseCameraAndPreview() {
    preview?.setCamera(null)
    camera?.release()
    camera = null
}

CameraX: کنترل‌کننده دوربین

در CameraX، انتخاب دوربین توسط کلاس CameraSelector انجام می‌شود. CameraX حالت رایج استفاده از دوربین پیش‌فرض را ساده می‌کند. می‌توانید مشخص کنید که آیا دوربین جلو پیش‌فرض یا دوربین عقب پیش‌فرض را می‌خواهید. علاوه بر این، شیء CameraControl در CameraX به شما امکان می‌دهد سطح زوم را برای برنامه خود تنظیم کنید ، بنابراین اگر برنامه شما روی دستگاهی اجرا می‌شود که از دوربین‌های منطقی پشتیبانی می‌کند، به لنز مناسب تغییر حالت می‌دهد.

کد CameraX برای استفاده از دوربین پشتی پیش‌فرض با CameraController به صورت زیر است:

// CameraX: select a camera with CameraController

var cameraController = LifecycleCameraController(baseContext)
val selector = CameraSelector.Builder()
    .requireLensFacing(CameraSelector.LENS_FACING_BACK).build()
cameraController.cameraSelector = selector

CameraX: ارائه دهنده دوربین

در اینجا مثالی از انتخاب دوربین جلو پیش‌فرض با CameraProvider آورده شده است (می‌توان از دوربین جلو یا عقب با CameraController یا CameraProvider استفاده کرد):

// CameraX: select a camera with CameraProvider.

// Use await() within a suspend function to get CameraProvider instance.
// For more details on await(), see the preceding "Android development concepts"
// section.
private suspend fun startCamera() {
    val cameraProvider = ProcessCameraProvider.getInstance(this).await()

    // Set up UseCases (more on UseCases in later scenarios)
    var useCases:Array = ...

    // Set the cameraSelector to use the default front-facing (selfie)
    // camera.
    val cameraSelector = CameraSelector.DEFAULT_FRONT_CAMERA

    try {
        // Unbind UseCases before rebinding.
        cameraProvider.unbindAll()

        // Bind UseCases to camera. This function returns a camera
        // object which can be used to perform operations like zoom,
        // flash, and focus.
        var camera = cameraProvider.bindToLifecycle(
            this, cameraSelector, useCases)

    } catch(exc: Exception) {
        Log.e(TAG, "UseCase binding failed", exc)
    }
})

...

// Call startCamera in the setup flow of your app, such as in onViewCreated.
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)

    ...

    lifecycleScope.launch {
        startCamera()
    }
}

اگر می‌خواهید روی دوربین انتخاب شده کنترل داشته باشید، این کار در CameraX نیز امکان‌پذیر است، اگر CameraProvider با فراخوانی getAvailableCameraInfos() استفاده کنید، که یک شیء CameraInfo برای بررسی ویژگی‌های خاص دوربین مانند isFocusMeteringSupported() به شما می‌دهد. سپس می‌توانید آن را به یک CameraSelector تبدیل کنید تا همانطور که در مثال‌های قبلی با متد CameraInfo.getCameraSelector() نشان داده شده است، مورد استفاده قرار گیرد.

شما می‌توانید با استفاده از کلاس Camera2CameraInfo جزئیات بیشتری در مورد هر دوربین دریافت کنید. تابع getCameraCharacteristic() را با یک کلید برای داده‌های دوربین مورد نظر خود فراخوانی کنید. برای مشاهده لیستی از تمام کلیدهایی که می‌توانید برای آنها پرس‌وجو کنید، کلاس CameraCharacteristics را بررسی کنید.

در اینجا مثالی با استفاده از یک تابع سفارشی checkFocalLength() که می‌توانید خودتان تعریف کنید، آورده شده است:

// CameraX: get a cameraSelector for first camera that matches the criteria
// defined in checkFocalLength().

val cameraInfo = cameraProvider.getAvailableCameraInfos()
    .first { cameraInfo ->
        val focalLengths = Camera2CameraInfo.from(cameraInfo)
            .getCameraCharacteristic(
                CameraCharacteristics.LENS_INFO_AVAILABLE_FOCAL_LENGTHS
            )
        return checkFocalLength(focalLengths)
    }
val cameraSelector = cameraInfo.getCameraSelector()

نمایش پیش‌نمایش

اکثر برنامه‌های دوربین نیاز دارند که در مقطعی، فید دوربین را روی صفحه نمایش دهند. با Camera1، شما باید فراخوانی‌های چرخه عمر را به درستی مدیریت کنید و همچنین باید چرخش و مقیاس‌بندی را برای پیش‌نمایش خود تعیین کنید.

علاوه بر این، در Camera1 باید تصمیم بگیرید که از TextureView یا SurfaceView به عنوان سطح پیش‌نمایش خود استفاده کنید. هر دو گزینه با بده‌بستان‌هایی همراه هستند و در هر صورت، Camera1 از شما می‌خواهد که چرخش و مقیاس‌بندی را به درستی مدیریت کنید. از سوی دیگر، PreviewView دوربین X، پیاده‌سازی‌های اساسی برای TextureView و SurfaceView دارد. Camera X بسته به عواملی مانند نوع دستگاه و نسخه اندرویدی که برنامه شما روی آن اجرا می‌شود، تصمیم می‌گیرد که کدام پیاده‌سازی بهترین است. اگر هر یک از پیاده‌سازی‌ها سازگار باشد، می‌توانید ترجیح خود را با PreviewView.ImplementationMode اعلام کنید. گزینه COMPATIBLE از TextureView برای پیش‌نمایش استفاده می‌کند و مقدار PERFORMANCE از SurfaceView (در صورت امکان) استفاده می‌کند.

دوربین1

برای نمایش پیش‌نمایش، باید کلاس Preview خودتان را با پیاده‌سازی رابط android.view.SurfaceHolder.Callback بنویسید که برای انتقال داده‌های تصویر از سخت‌افزار دوربین به برنامه استفاده می‌شود. سپس، قبل از اینکه بتوانید پیش‌نمایش زنده تصویر را شروع کنید، کلاس Preview باید به شیء Camera ارسال شود.

// Camera1: set up a camera preview.

class Preview(
        context: Context,
        private val camera: Camera
) : SurfaceView(context), SurfaceHolder.Callback {

    private val holder: SurfaceHolder = holder.apply {
        addCallback(this@Preview)
        setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS)
    }

    override fun surfaceCreated(holder: SurfaceHolder) {
        // The Surface has been created, now tell the camera
        // where to draw the preview.
        camera.apply {
            try {
                setPreviewDisplay(holder)
                startPreview()
            } catch (e: IOException) {
                Log.d(TAG, "error setting camera preview", e)
            }
        }
    }

    override fun surfaceDestroyed(holder: SurfaceHolder) {
        // Take care of releasing the Camera preview in your activity.
    }

    override fun surfaceChanged(holder: SurfaceHolder, format: Int,
                                w: Int, h: Int) {
        // If your preview can change or rotate, take care of those
        // events here. Make sure to stop the preview before resizing
        // or reformatting it.
        if (holder.surface == null) {
            return  // The preview surface does not exist.
        }

        // Stop preview before making changes.
        try {
            camera.stopPreview()
        } catch (e: Exception) {
            // Tried to stop a non-existent preview; nothing to do.
        }

        // Set preview size and make any resize, rotate or
        // reformatting changes here.

        // Start preview with new settings.
        camera.apply {
            try {
                setPreviewDisplay(holder)
                startPreview()
            } catch (e: Exception) {
                Log.d(TAG, "error starting camera preview", e)
            }
        }
    }
}

class CameraActivity : AppCompatActivity() {
    private lateinit var viewBinding: ActivityMainBinding
    private var camera: Camera? = null
    private var preview: Preview? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        viewBinding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(viewBinding.root)

        // Create an instance of Camera.
        camera = getCameraInstance()

        preview = camera?.let {
            // Create the Preview view.
            Preview(this, it)
        }

        // Set the Preview view as the content of the activity.
        val cameraPreview: FrameLayout = viewBinding.cameraPreview
        cameraPreview.addView(preview)
    }
}

CameraX: کنترل‌کننده دوربین

در CameraX، شما به عنوان توسعه‌دهنده، چیزهای بسیار کمتری برای مدیریت دارید. اگر از CameraController استفاده می‌کنید، باید PreviewView نیز استفاده کنید. این بدان معناست که Preview UseCase ضمنی است و باعث می‌شود تنظیمات بسیار کمتر کار کنند:

// CameraX: set up a camera preview with a CameraController.

class MainActivity : AppCompatActivity() {
    private lateinit var viewBinding: ActivityMainBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        viewBinding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(viewBinding.root)

        // Create the CameraController and set it on the previewView.
        var cameraController = LifecycleCameraController(baseContext)
        cameraController.bindToLifecycle(this)
        val previewView: PreviewView = viewBinding.cameraPreview
        previewView.controller = cameraController
    }
}

CameraX: ارائه دهنده دوربین

با CameraProvider مربوط به CameraX، نیازی به استفاده PreviewView ندارید، اما همچنان تنظیمات پیش‌نمایش را نسبت به Camera1 بسیار ساده می‌کند. برای اهداف نمایشی، این مثال از PreviewView استفاده می‌کند، اما اگر نیازهای پیچیده‌تری دارید، می‌توانید یک SurfaceProvider سفارشی بنویسید تا به setSurfaceProvider() ارسال شود.

در اینجا، Preview UseCase مانند CameraController به صورت ضمنی تعریف نشده است، بنابراین باید آن را تنظیم کنید:

// CameraX: set up a camera preview with a CameraProvider.

// Use await() within a suspend function to get CameraProvider instance.
// For more details on await(), see the preceding "Android development concepts"
// section.
private suspend fun startCamera() {
    val cameraProvider = ProcessCameraProvider.getInstance(this).await()

    // Create Preview UseCase.
    val preview = Preview.Builder()
        .build()
        .also {
            it.setSurfaceProvider(
                viewBinding.viewFinder.surfaceProvider
            )
        }

    // Select default back camera.
    val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA

    try {
        // Unbind UseCases before rebinding.
        cameraProvider.unbindAll()

        // Bind UseCases to camera. This function returns a camera
        // object which can be used to perform operations like zoom,
        // flash, and focus.
        var camera = cameraProvider.bindToLifecycle(
            this, cameraSelector, useCases)

    } catch(exc: Exception) {
        Log.e(TAG, "UseCase binding failed", exc)
    }
})

...

// Call startCamera() in the setup flow of your app, such as in onViewCreated.
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)

    ...

    lifecycleScope.launch {
        startCamera()
    }
}

ضربه برای فوکوس

وقتی پیش‌نمایش دوربین شما روی صفحه نمایش داده می‌شود، یک کنترل رایج، تنظیم نقطه فوکوس هنگام لمس پیش‌نمایش توسط کاربر است.

دوربین1

برای پیاده‌سازی فوکوس با لمس در Camera1، باید Area فوکوس بهینه را محاسبه کنید تا مشخص شود Camera باید کجا فوکوس کند. این Area به تابع setFocusAreas() ارسال می‌شود. همچنین، باید یک حالت فوکوس سازگار روی Camera تنظیم کنید. ناحیه فوکوس فقط در صورتی تأثیر دارد که حالت فوکوس فعلی FOCUS_MODE_AUTO ، FOCUS_MODE_MACRO ، FOCUS_MODE_CONTINUOUS_VIDEO یا FOCUS_MODE_CONTINUOUS_PICTURE باشد.

هر Area یک مستطیل با وزن مشخص است. وزن مقداری بین ۱ تا ۱۰۰۰ دارد و برای اولویت‌بندی Areas فوکوس در صورت تنظیم چندین ناحیه استفاده می‌شود. این مثال فقط از یک Area استفاده می‌کند، بنابراین مقدار وزن اهمیتی ندارد. مختصات مستطیل از -۱۰۰۰ تا ۱۰۰۰ متغیر است. نقطه بالا سمت چپ (-۱۰۰۰، -۱۰۰۰) است. نقطه پایین سمت راست (۱۰۰۰، ۱۰۰۰) است. جهت نسبت به جهت حسگر، یعنی آنچه حسگر می‌بیند، است. جهت تحت تأثیر چرخش یا آینه‌سازی Camera.setDisplayOrientation() قرار نمی‌گیرد، بنابراین باید مختصات رویداد لمسی را به مختصات حسگر تبدیل کنید.

// Camera1: implement tap-to-focus.

class TapToFocusHandler : Camera.AutoFocusCallback {
    private fun handleFocus(event: MotionEvent) {
        val camera = camera ?: return
        val parameters = try {
            camera.getParameters()
        } catch (e: RuntimeException) {
            return
        }

        // Cancel previous auto-focus function, if one was in progress.
        camera.cancelAutoFocus()

        // Create focus Area.
        val rect = calculateFocusAreaCoordinates(event.x, event.y)
        val weight = 1  // This value's not important since there's only 1 Area.
        val focusArea = Camera.Area(rect, weight)

        // Set the focus parameters.
        parameters.setFocusMode(Parameters.FOCUS_MODE_AUTO)
        parameters.setFocusAreas(listOf(focusArea))

        // Set the parameters back on the camera and initiate auto-focus.
        camera.setParameters(parameters)
        camera.autoFocus(this)
    }

    private fun calculateFocusAreaCoordinates(x: Int, y: Int) {
        // Define the size of the Area to be returned. This value
        // should be optimized for your app.
        val focusAreaSize = 100

        // You must define functions to rotate and scale the x and y values to
        // be values between 0 and 1, where (0, 0) is the upper left-hand side
        // of the preview, and (1, 1) is the lower right-hand side.
        val normalizedX = (rotateAndScaleX(x) - 0.5) * 2000
        val normalizedY = (rotateAndScaleY(y) - 0.5) * 2000

        // Calculate the values for left, top, right, and bottom of the Rect to
        // be returned. If the Rect would extend beyond the allowed values of
        // (-1000, -1000, 1000, 1000), then crop the values to fit inside of
        // that boundary.
        val left = max(normalizedX - (focusAreaSize / 2), -1000)
        val top = max(normalizedY - (focusAreaSize / 2), -1000)
        val right = min(left + focusAreaSize, 1000)
        val bottom = min(top + focusAreaSize, 1000)

        return Rect(left, top, left + focusAreaSize, top + focusAreaSize)
    }

    override fun onAutoFocus(focused: Boolean, camera: Camera) {
        if (!focused) {
            Log.d(TAG, "tap-to-focus failed")
        }
    }
}

CameraX: کنترل‌کننده دوربین

CameraController به رویدادهای لمسی PreviewView گوش می‌دهد تا به طور خودکار قابلیت tap-to-focus را مدیریت کند. می‌توانید tap-to-focus را با setTapToFocusEnabled() فعال و غیرفعال کنید و مقدار را با getter مربوطه isTapToFocusEnabled() بررسی کنید.

متد getTapToFocusState() یک شیء LiveData را برای ردیابی تغییرات وضعیت فوکوس در CameraController برمی‌گرداند.

// CameraX: track the state of tap-to-focus over the Lifecycle of a PreviewView,
// with handlers you can define for focused, not focused, and failed states.

val tapToFocusStateObserver = Observer { state ->
    when (state) {
        CameraController.TAP_TO_FOCUS_NOT_STARTED ->
            Log.d(TAG, "tap-to-focus init")
        CameraController.TAP_TO_FOCUS_STARTED ->
            Log.d(TAG, "tap-to-focus started")
        CameraController.TAP_TO_FOCUS_FOCUSED ->
            Log.d(TAG, "tap-to-focus finished (focus successful)")
        CameraController.TAP_TO_FOCUS_NOT_FOCUSED ->
            Log.d(TAG, "tap-to-focus finished (focused unsuccessful)")
        CameraController.TAP_TO_FOCUS_FAILED ->
            Log.d(TAG, "tap-to-focus failed")
    }
}

cameraController.getTapToFocusState().observe(this, tapToFocusStateObserver)

CameraX: ارائه دهنده دوربین

هنگام استفاده از CameraProvider ، برای فعال کردن قابلیت فوکوس با لمس، به برخی تنظیمات نیاز است. این مثال فرض می‌کند که شما از PreviewView استفاده می‌کنید. در غیر این صورت، باید منطق را برای اعمال روی Surface سفارشی خود تطبیق دهید.

مراحل استفاده از PreviewView به شرح زیر است:

  1. یک آشکارساز ژست برای مدیریت رویدادهای ضربه تنظیم کنید.
  2. با استفاده از رویداد tap، با استفاده از MeteringPointFactory.createPoint() یک MeteringPoint ایجاد کنید.
  3. با استفاده از MeteringPoint ، یک FocusMeteringAction ایجاد کنید.
  4. با شیء CameraControl روی Camera خود (که از bindToLifecycle() برگردانده می‌شود)، تابع startFocusAndMetering() را فراخوانی کنید و FocusMeteringAction را به آن ارسال کنید.
  5. (اختیاری) به FocusMeteringResult پاسخ دهید.
  6. حسگر حرکت خود را طوری تنظیم کنید که به رویدادهای لمسی در PreviewView.setOnTouchListener() پاسخ دهد.
// CameraX: implement tap-to-focus with CameraProvider.

// Define a gesture detector to respond to tap events and call
// startFocusAndMetering on CameraControl. If you want to use a
// coroutine with await() to check the result of focusing, see the
// preceding "Android development concepts" section.
val gestureDetector = GestureDetectorCompat(context,
    object : SimpleOnGestureListener() {
        override fun onSingleTapUp(e: MotionEvent): Boolean {
            val previewView = previewView ?: return
            val camera = camera ?: return
            val meteringPointFactory = previewView.meteringPointFactory
            val focusPoint = meteringPointFactory.createPoint(e.x, e.y)
            val meteringAction = FocusMeteringAction
                .Builder(meteringPoint).build()
            lifecycleScope.launch {
                val focusResult = camera.cameraControl
                    .startFocusAndMetering(meteringAction).await()
                if (!result.isFocusSuccessful()) {
                    Log.d(TAG, "tap-to-focus failed")
                }
            }
        }
    }
)

...

// Set the gestureDetector in a touch listener on the PreviewView.
previewView.setOnTouchListener { _, event ->
    // See pinch-to-zoom scenario for scaleGestureDetector definition.
    var didConsume = scaleGestureDetector.onTouchEvent(event)
    if (!scaleGestureDetector.isInProgress) {
        didConsume = gestureDetector.onTouchEvent(event)
    }
    didConsume
}

بزرگنمایی با دو انگشت

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

دوربین1

دو روش برای بزرگنمایی با استفاده از Camera1 وجود دارد. متد Camera.startSmoothZoom() از سطح بزرگنمایی فعلی تا سطح بزرگنمایی که شما ارسال می‌کنید، متحرک‌سازی می‌کند. متد Camera.Parameters.setZoom() مستقیماً به سطح بزرگنمایی که شما ارسال می‌کنید، پرش می‌کند. قبل از استفاده از هر یک از آنها، به ترتیب isSmoothZoomSupported() یا isZoomSupported() را فراخوانی کنید تا مطمئن شوید که متدهای بزرگنمایی مرتبط مورد نیاز شما در دوربین شما موجود هستند.

برای پیاده‌سازی قابلیت pinch-to-zoom، این مثال از setZoom() استفاده می‌کند، زیرا شنونده‌ی لمس روی سطح پیش‌نمایش، همزمان با انجام حرکت pinch، مرتباً رویدادها را فعال می‌کند، بنابراین هر بار سطح بزرگنمایی را فوراً به‌روزرسانی می‌کند. کلاس ZoomTouchListener بعداً در این بخش تعریف می‌شود و شما باید آن را به عنوان یک callback برای شنونده‌ی لمس سطح پیش‌نمایش خود تنظیم کنید.

// Camera1: implement pinch-to-zoom.

// Define a scale gesture detector to respond to pinch events and call
// setZoom on Camera.Parameters.
val scaleGestureDetector = ScaleGestureDetector(context,
    object : ScaleGestureDetector.OnScaleGestureListener {
        override fun onScale(detector: ScaleGestureDetector): Boolean {
            val camera = camera ?: return false
            val parameters = try {
                camera.parameters
            } catch (e: RuntimeException) {
                return false
            }

            // In case there is any focus happening, stop it.
            camera.cancelAutoFocus()

            // Set the zoom level on the Camera.Parameters, and set
            // the Parameters back onto the Camera.
            val currentZoom = parameters.zoom
            parameters.setZoom(detector.scaleFactor * currentZoom)
        camera.setParameters(parameters)
            return true
        }
    }
)

// Define a View.OnTouchListener to attach to your preview view.
class ZoomTouchListener : View.OnTouchListener {
    override fun onTouch(v: View, event: MotionEvent): Boolean =
        scaleGestureDetector.onTouchEvent(event)
}

// Set a ZoomTouchListener to handle touch events on your preview view
// if zoom is supported by the current camera.
if (camera.getParameters().isZoomSupported()) {
    view.setOnTouchListener(ZoomTouchListener())
}

CameraX: کنترل‌کننده دوربین

مشابه tap-to-focus، CameraController به رویدادهای لمسی PreviewView گوش می‌دهد تا به طور خودکار زوم با نیشگون گرفتن را مدیریت کند. می‌توانید زوم با نیشگون گرفتن را با setPinchToZoomEnabled() فعال و غیرفعال کنید و مقدار را با getter مربوطه isPinchToZoomEnabled() بررسی کنید.

متد getZoomState() یک شیء LiveData را برای ردیابی تغییرات ZoomState در CameraController برمی‌گرداند.

// CameraX: track the state of pinch-to-zoom over the Lifecycle of
// a PreviewView, logging the linear zoom ratio.

val pinchToZoomStateObserver = Observer { state ->
    val zoomRatio = state.getZoomRatio()
    Log.d(TAG, "ptz-zoom-ratio $zoomRatio")
}

cameraController.getZoomState().observe(this, pinchToZoomStateObserver)

CameraX: ارائه دهنده دوربین

برای اینکه بتوانید با استفاده از قابلیت زوم دو انگشتی (pinch-to-zoom) با CameraProvider کار کنید، به برخی تنظیمات نیاز دارید. اگر از PreviewView استفاده نمی‌کنید، باید منطق آن را برای اعمال روی Surface سفارشی خود تطبیق دهید.

مراحل استفاده از PreviewView به شرح زیر است:

  1. یک آشکارساز ژست مقیاس‌پذیر برای مدیریت رویدادهای نیشگون گرفتن تنظیم کنید.
  2. ZoomState را از شیء Camera.CameraInfo دریافت کنید، جایی که نمونه Camera هنگام فراخوانی bindToLifecycle() بازگردانده می‌شود.
  3. اگر ZoomState مقداری zoomRatio دارد، آن را به عنوان نسبت زوم فعلی ذخیره کنید. اگر zoomRatio در ZoomState وجود ندارد، از نرخ زوم پیش‌فرض دوربین (۱.۰) استفاده کنید.
  4. حاصلضرب نسبت زوم فعلی را در scaleFactor بگیرید تا نسبت زوم جدید را تعیین کنید و آن را به CameraControl.setZoomRatio() ارسال کنید.
  5. حسگر حرکت خود را طوری تنظیم کنید که به رویدادهای لمسی در PreviewView.setOnTouchListener() پاسخ دهد.
// CameraX: implement pinch-to-zoom with CameraProvider.

// Define a scale gesture detector to respond to pinch events and call
// setZoomRatio on CameraControl.
val scaleGestureDetector = ScaleGestureDetector(context,
    object : SimpleOnGestureListener() {
        override fun onScale(detector: ScaleGestureDetector): Boolean {
            val camera = camera ?: return
            val zoomState = camera.cameraInfo.zoomState
            val currentZoomRatio: Float = zoomState.value?.zoomRatio ?: 1f
            camera.cameraControl.setZoomRatio(
                detector.scaleFactor * currentZoomRatio
            )
        }
    }
)

...

// Set the scaleGestureDetector in a touch listener on the PreviewView.
previewView.setOnTouchListener { _, event ->
    var didConsume = scaleGestureDetector.onTouchEvent(event)
    if (!scaleGestureDetector.isInProgress) {
        // See pinch-to-zoom scenario for gestureDetector definition.
        didConsume = gestureDetector.onTouchEvent(event)
    }
    didConsume
}

گرفتن عکس

این بخش نحوه‌ی فعال کردن ثبت عکس را نشان می‌دهد، چه بخواهید این کار را با فشردن دکمه‌ی شاتر، چه پس از گذشت زمان تایمر یا هر رویداد دیگری که خودتان انتخاب می‌کنید، انجام دهید.

دوربین1

در Camera1، ابتدا یک Camera.PictureCallback تعریف می‌کنید تا داده‌های تصویر را هنگام درخواست مدیریت کند. در اینجا یک مثال ساده از PictureCallback برای مدیریت داده‌های تصویر JPEG آورده شده است:

// Camera1: define a Camera.PictureCallback to handle JPEG data.

private val picture = Camera.PictureCallback { data, _ ->
    val pictureFile: File = getOutputMediaFile(MEDIA_TYPE_IMAGE) ?: run {
        Log.d(TAG,
              "error creating media file, check storage permissions")
        return@PictureCallback
    }

    try {
        val fos = FileOutputStream(pictureFile)
        fos.write(data)
        fos.close()
    } catch (e: FileNotFoundException) {
        Log.d(TAG, "file not found", e)
    } catch (e: IOException) {
        Log.d(TAG, "error accessing file", e)
    }
}

سپس، هر زمان که می‌خواهید عکس بگیرید، متد takePicture() را در نمونه Camera خود فراخوانی می‌کنید. این متد takePicture() سه پارامتر مختلف برای انواع داده‌های مختلف دارد. پارامتر اول برای ShutterCallback است (که در این مثال تعریف نشده است). پارامتر دوم برای PictureCallback است تا داده‌های خام (فشرده نشده) دوربین را مدیریت کند. پارامتر سوم همان پارامتری است که در این مثال استفاده می‌شود، زیرا یک PictureCallback برای مدیریت داده‌های تصویر JPEG است.

// Camera1: call takePicture on Camera instance, passing our PictureCallback.

camera?.takePicture(null, null, picture)

CameraX: کنترل‌کننده دوربین

CameraController در CameraX با پیاده‌سازی متد takePicture() خود، سادگی Camera1 را برای ضبط تصویر حفظ می‌کند. در اینجا، یک تابع برای پیکربندی ورودی MediaStore تعریف کنید و عکسی را برای ذخیره در آنجا بگیرید.

// CameraX: define a function that uses CameraController to take a photo.

private val FILENAME_FORMAT = "yyyy-MM-dd-HH-mm-ss-SSS"

private fun takePhoto() {
   // Create time stamped name and MediaStore entry.
   val name = SimpleDateFormat(FILENAME_FORMAT, Locale.US)
              .format(System.currentTimeMillis())
   val contentValues = ContentValues().apply {
       put(MediaStore.MediaColumns.DISPLAY_NAME, name)
       put(MediaStore.MediaColumns.MIME_TYPE, "image/jpeg")
       if(Build.VERSION.SDK_INT > Build.VERSION_CODES.P) {
           put(MediaStore.Images.Media.RELATIVE_PATH, "Pictures/CameraX-Image")
       }
   }

   // Create output options object which contains file + metadata.
   val outputOptions = ImageCapture.OutputFileOptions
       .Builder(context.getContentResolver(),
            MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues)
       .build()

   // Set up image capture listener, which is triggered after photo has
   // been taken.
   cameraController.takePicture(
       outputOptions,
       ContextCompat.getMainExecutor(this),
       object : ImageCapture.OnImageSavedCallback {
           override fun onError(e: ImageCaptureException) {
               Log.e(TAG, "photo capture failed", e)
           }

           override fun onImageSaved(
               output: ImageCapture.OutputFileResults
           ) {
               val msg = "Photo capture succeeded: ${output.savedUri}"
               Toast.makeText(baseContext, msg, Toast.LENGTH_SHORT).show()
               Log.d(TAG, msg)
           }
       }
   )
}

CameraX: ارائه دهنده دوربین

گرفتن عکس با CameraProvider تقریباً دقیقاً به همان روشی که با CameraController انجام می‌شود، کار می‌کند، اما ابتدا باید یک ImageCapture UseCase ایجاد و متصل کنید تا شیء‌ای برای فراخوانی takePicture() داشته باشید:

// CameraX: create and bind an ImageCapture UseCase.

// Make a reference to the ImageCapture UseCase at a scope that can be accessed
// throughout the camera logic in your app.
private var imageCapture: ImageCapture? = null

...

// Create an ImageCapture instance (can be added with other
// UseCase definitions).
imageCapture = ImageCapture.Builder().build()

...

// Bind UseCases to camera (adding imageCapture along with preview here, but
// preview is not required to use imageCapture). This function returns a camera
// object which can be used to perform operations like zoom, flash, and focus.
var camera = cameraProvider.bindToLifecycle(
    this, cameraSelector, preview, imageCapture)

سپس، هر زمان که می‌خواهید عکس بگیرید، می‌توانید ImageCapture.takePicture() را فراخوانی کنید. برای مثال کامل تابع takePhoto() به کد CameraController در این بخش مراجعه کنید.

// CameraX: define a function that uses CameraController to take a photo.

private fun takePhoto() {
    // Get a stable reference of the modifiable ImageCapture UseCase.
    val imageCapture = imageCapture ?: return

    ...

    // Call takePicture on imageCapture instance.
    imageCapture.takePicture(
        ...
    )
}

ضبط ویدیو

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

همانطور که خواهید دید، CameraX دوباره بسیاری از این پیچیدگی‌ها را برای شما مدیریت می‌کند.

دوربین1

ضبط ویدیو با استفاده از Camera1 نیاز به مدیریت دقیق Camera و MediaRecorder دارد و متدها باید به ترتیب خاصی فراخوانی شوند. برای عملکرد صحیح برنامه، باید این ترتیب را دنبال کنید:

  1. دوربین را باز کنید.
  2. پیش‌نمایش را آماده و شروع کنید (اگر برنامه شما ویدیوی در حال ضبط را نشان می‌دهد، که معمولاً همینطور است).
  3. با فراخوانی Camera.unlock() دوربین را برای استفاده توسط MediaRecorder باز کنید.
  4. با فراخوانی این متدها در MediaRecorder ضبط را پیکربندی کنید:
    1. نمونه Camera خود را با setCamera(camera) متصل کنید.
    2. فراخوانی تابع setAudioSource(MediaRecorder.AudioSource.CAMCORDER) .
    3. فراخوانی setVideoSource(MediaRecorder.VideoSource.CAMERA) .
    4. برای تنظیم کیفیت، تابع setProfile(CamcorderProfile.get(CamcorderProfile.QUALITY_1080P)) را فراخوانی کنید. برای مشاهده‌ی تمام گزینه‌های کیفیت، به CamcorderProfile مراجعه کنید.
    5. فراخوانی تابع setOutputFile(getOutputMediaFile(MEDIA_TYPE_VIDEO).toString()) .
    6. اگر برنامه شما پیش‌نمایشی از ویدیو دارد، تابع setPreviewDisplay(preview?.holder?.surface) را فراخوانی کنید.
    7. فراخوانی setOutputFormat(MediaRecorder.OutputFormat.MPEG_4) .
    8. فراخوانی تابع setAudioEncoder(MediaRecorder.AudioEncoder.DEFAULT) .
    9. با setVideoEncoder(MediaRecorder.VideoEncoder.DEFAULT) تماس بگیرید.
    10. برای نهایی کردن پیکربندی MediaRecorder خود، تابع prepare() فراخوانی کنید.
  5. برای شروع ضبط، تابع MediaRecorder.start() را فراخوانی کنید.
  6. برای متوقف کردن ضبط، این متدها را فراخوانی کنید. دوباره، دقیقاً این دستور را دنبال کنید:
    1. تابع MediaRecorder.stop() را فراخوانی کنید.
    2. به صورت اختیاری، پیکربندی فعلی MediaRecorder را با فراخوانی MediaRecorder.reset() حذف کنید.
    3. MediaRecorder.release() را فراخوانی کنید.
    4. دوربین را قفل کنید تا جلسات آینده MediaRecorder بتوانند با فراخوانی Camera.lock() از آن استفاده کنند.
  7. برای متوقف کردن پیش‌نمایش، تابع Camera.stopPreview() را فراخوانی کنید.
  8. در نهایت، برای آزاد کردن Camera تا سایر فرآیندها بتوانند از آن استفاده کنند، Camera.release() را فراخوانی کنید.

در اینجا همه این مراحل با هم ترکیب شده‌اند:

// Camera1: set up a MediaRecorder and a function to start and stop video
// recording.

// Make a reference to the MediaRecorder at a scope that can be accessed
// throughout the camera logic in your app.
private var mediaRecorder: MediaRecorder? = null
private var isRecording = false

...

private fun prepareMediaRecorder(): Boolean {
    mediaRecorder = MediaRecorder()

    // Unlock and set camera to MediaRecorder.
    camera?.unlock()

    mediaRecorder?.run {
        setCamera(camera)

        // Set the audio and video sources.
        setAudioSource(MediaRecorder.AudioSource.CAMCORDER)
        setVideoSource(MediaRecorder.VideoSource.CAMERA)

        // Set a CamcorderProfile (requires API Level 8 or higher).
        setProfile(CamcorderProfile.get(CamcorderProfile.QUALITY_HIGH))

        // Set the output file.
        setOutputFile(getOutputMediaFile(MEDIA_TYPE_VIDEO).toString())

        // Set the preview output.
        setPreviewDisplay(preview?.holder?.surface)

        setOutputFormat(MediaRecorder.OutputFormat.MPEG_4)
        setAudioEncoder(MediaRecorder.AudioEncoder.DEFAULT)
        setVideoEncoder(MediaRecorder.VideoEncoder.DEFAULT)

        // Prepare configured MediaRecorder.
        return try {
            prepare()
            true
        } catch (e: IllegalStateException) {
            Log.d(TAG, "preparing MediaRecorder failed", e)
            releaseMediaRecorder()
            false
        } catch (e: IOException) {
            Log.d(TAG, "setting MediaRecorder file failed", e)
            releaseMediaRecorder()
            false
        }
    }
    return false
}

private fun releaseMediaRecorder() {
    mediaRecorder?.reset()
    mediaRecorder?.release()
    mediaRecorder = null
    camera?.lock()
}

private fun startStopVideo() {
    if (isRecording) {
        // Stop recording and release camera.
        mediaRecorder?.stop()
        releaseMediaRecorder()
        camera?.lock()
        isRecording = false

        // This is a good place to inform user that video recording has stopped.
    } else {
        // Initialize video camera.
        if (prepareVideoRecorder()) {
            // Camera is available and unlocked, MediaRecorder is prepared, now
            // you can start recording.
            mediaRecorder?.start()
            isRecording = true

            // This is a good place to inform the user that recording has
            // started.
        } else {
            // Prepare didn't work, release the camera.
            releaseMediaRecorder()

            // Inform user here.
        }
    }
}

CameraX: کنترل‌کننده دوربین

با استفاده از CameraController در CameraX، می‌توانید UseCase های ImageCapture ، VideoCapture و ImageAnalysis را به‌طور مستقل تغییر دهید، البته تا زمانی که لیست UseCaseها به‌طور همزمان قابل استفاده باشند . UseCase های ImageCapture و ImageAnalysis به‌طور پیش‌فرض فعال هستند، به همین دلیل است که برای گرفتن عکس نیازی به فراخوانی setEnabledUseCases() نداشتید.

برای استفاده از CameraController برای ضبط ویدیو، ابتدا باید setEnabledUseCases() برای فعال کردن UseCase VideoCapture استفاده کنید.

// CameraX: Enable VideoCapture UseCase on CameraController.

cameraController.setEnabledUseCases(VIDEO_CAPTURE);

وقتی می‌خواهید ضبط ویدیو را شروع کنید، می‌توانید تابع CameraController.startRecording() را فراخوانی کنید. این تابع می‌تواند ویدیوی ضبط شده را در یک File ذخیره کند، همانطور که در مثال زیر مشاهده می‌کنید. علاوه بر این، باید یک Executor و یک کلاس که OnVideoSavedCallback را پیاده‌سازی می‌کند، برای مدیریت فراخوانی‌های موفقیت‌آمیز و خطا، ارسال کنید. وقتی ضبط باید پایان یابد، CameraController.stopRecording() را فراخوانی کنید.

توجه: اگر از CameraX 1.3.0-alpha02 یا بالاتر استفاده می‌کنید، یک پارامتر AudioConfig اضافی وجود دارد که به شما امکان می‌دهد ضبط صدا را روی ویدیوی خود فعال یا غیرفعال کنید. برای فعال کردن ضبط صدا، باید مطمئن شوید که مجوزهای میکروفون را دارید. علاوه بر این، متد stopRecording() در 1.3.0-alpha02 حذف شده است و startRecording() یک شیء Recording را برمی‌گرداند که می‌تواند برای مکث، از سرگیری و توقف ضبط ویدیو استفاده شود.

// CameraX: implement video capture with CameraController.

private val FILENAME_FORMAT = "yyyy-MM-dd-HH-mm-ss-SSS"

// Define a VideoSaveCallback class for handling success and error states.
class VideoSaveCallback : OnVideoSavedCallback {
    override fun onVideoSaved(outputFileResults: OutputFileResults) {
        val msg = "Video capture succeeded: ${outputFileResults.savedUri}"
        Toast.makeText(baseContext, msg, Toast.LENGTH_SHORT).show()
        Log.d(TAG, msg)
    }

    override fun onError(videoCaptureError: Int, message: String,
                         cause: Throwable?) {
        Log.d(TAG, "error saving video: $message", cause)
    }
}

private fun startStopVideo() {
    if (cameraController.isRecording()) {
        // Stop the current recording session.
        cameraController.stopRecording()
        return
    }

    // Define the File options for saving the video.
    val name = SimpleDateFormat(FILENAME_FORMAT, Locale.US)
        .format(System.currentTimeMillis())

    val outputFileOptions = OutputFileOptions
        .Builder(File(this.filesDir, name))
        .build()

    // Call startRecording on the CameraController.
    cameraController.startRecording(
        outputFileOptions,
        ContextCompat.getMainExecutor(this),
        VideoSaveCallback()
    )
}

CameraX: ارائه دهنده دوربین

اگر از CameraProvider استفاده می‌کنید، باید یک UseCase VideoCapture ایجاد کنید و یک شیء Recorder به آن ارسال کنید. در Recorder.Builder ، می‌توانید کیفیت ویدیو و به صورت اختیاری، یک FallbackStrategy تنظیم کنید که مواردی را که یک دستگاه نمی‌تواند مشخصات کیفیت مورد نظر شما را برآورده کند، مدیریت می‌کند. سپس نمونه VideoCapture را با UseCase های دیگر خود به CameraProvider متصل کنید.

// CameraX: create and bind a VideoCapture UseCase with CameraProvider.

// Make a reference to the VideoCapture UseCase and Recording at a
// scope that can be accessed throughout the camera logic in your app.
private lateinit var videoCapture: VideoCapture
private var recording: Recording? = null

...

// Create a Recorder instance to set on a VideoCapture instance (can be
// added with other UseCase definitions).
val recorder = Recorder.Builder()
    .setQualitySelector(QualitySelector.from(Quality.FHD))
    .build()
videoCapture = VideoCapture.withOutput(recorder)

...

// Bind UseCases to camera (adding videoCapture along with preview here, but
// preview is not required to use videoCapture). This function returns a camera
// object which can be used to perform operations like zoom, flash, and focus.
var camera = cameraProvider.bindToLifecycle(
    this, cameraSelector, preview, videoCapture)

در این مرحله، می‌توان از طریق ویژگی videoCapture.output به Recorder دسترسی پیدا کرد. Recorder می‌تواند ضبط‌های ویدیویی را که در یک File ، ParcelFileDescriptor یا MediaStore ذخیره می‌شوند، شروع کند. در این مثال از MediaStore استفاده شده است.

در Recorder ، چندین متد برای آماده‌سازی آن وجود دارد. برای تنظیم گزینه‌های خروجی MediaStore ، تابع prepareRecording() را فراخوانی کنید. اگر برنامه شما اجازه استفاده از میکروفون دستگاه را دارد، تابع withAudioEnabled() را نیز فراخوانی کنید. سپس، برای شروع ضبط، تابع start() را فراخوانی کنید و یک context و یک شنونده رویداد Consumer<VideoRecordEvent> را برای مدیریت رویدادهای ضبط ویدیو ارسال کنید. در صورت موفقیت، Recording برگردانده شده می‌تواند برای مکث، از سرگیری یا توقف ضبط استفاده شود.

// CameraX: implement video capture with CameraProvider.

private val FILENAME_FORMAT = "yyyy-MM-dd-HH-mm-ss-SSS"

private fun startStopVideo() {
   val videoCapture = this.videoCapture ?: return

   if (recording != null) {
       // Stop the current recording session.
       recording.stop()
       recording = null
       return
   }

   // Create and start a new recording session.
   val name = SimpleDateFormat(FILENAME_FORMAT, Locale.US)
       .format(System.currentTimeMillis())
   val contentValues = ContentValues().apply {
       put(MediaStore.MediaColumns.DISPLAY_NAME, name)
       put(MediaStore.MediaColumns.MIME_TYPE, "video/mp4")
       if (Build.VERSION.SDK_INT > Build.VERSION_CODES.P) {
           put(MediaStore.Video.Media.RELATIVE_PATH, "Movies/CameraX-Video")
       }
   }

   val mediaStoreOutputOptions = MediaStoreOutputOptions
       .Builder(contentResolver, MediaStore.Video.Media.EXTERNAL_CONTENT_URI)
       .setContentValues(contentValues)
       .build()

   recording = videoCapture.output
       .prepareRecording(this, mediaStoreOutputOptions)
       .withAudioEnabled()
       .start(ContextCompat.getMainExecutor(this)) { recordEvent ->
           when(recordEvent) {
               is VideoRecordEvent.Start -> {
                   viewBinding.videoCaptureButton.apply {
                       text = getString(R.string.stop_capture)
                       isEnabled = true
                   }
               }
               is VideoRecordEvent.Finalize -> {
                   if (!recordEvent.hasError()) {
                       val msg = "Video capture succeeded: " +
                           "${recordEvent.outputResults.outputUri}"
                       Toast.makeText(
                           baseContext, msg, Toast.LENGTH_SHORT
                       ).show()
                       Log.d(TAG, msg)
                   } else {
                       recording?.close()
                       recording = null
                       Log.e(TAG, "video capture ends with error",
                             recordEvent.error)
                   }
                   viewBinding.videoCaptureButton.apply {
                       text = getString(R.string.start_capture)
                       isEnabled = true
                   }
               }
           }
       }
}

منابع اضافی

ما چندین برنامه کامل CameraX را در مخزن GitHub نمونه‌های دوربین خود داریم. این نمونه‌ها به شما نشان می‌دهند که چگونه سناریوهای این راهنما در یک برنامه اندروید کامل جای می‌گیرند.

اگر در مهاجرت به CameraX به پشتیبانی بیشتری نیاز دارید یا در مورد مجموعه APIهای دوربین اندروید سؤالی دارید، لطفاً در گروه بحث CameraX با ما تماس بگیرید.