انتقال Camera1 به CameraX

اگر برنامه‌تان از کلاس اصلی Camera («Camera1») استفاده می‌کند که از زمان Android 5.0 (سطح میانای برنامه‌سازی کاربردی ۲۱) منسوخ شده است، اکیداً توصیه می‌کنیم آن را به میانای برنامه‌سازی کاربردی دوربین Android مدرن به‌روز کنید. Android CameraX (یک میانای برنامه‌سازی کاربردی دوربین استانداردشده و قدرتمند Jetpack) و Camera2 (یک میانای برنامه‌سازی کاربردی چارچوب سطح پایین) را ارائه می‌دهد. در اکثر موارد، توصیه می‌کنیم برنامه‌تان را به CameraX منتقل کنید. علت این است:

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

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

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

CameraController

CameraProvider

به کد راه‌اندازی کمی نیاز دارد امکان کنترل بیشتر را فراهم می‌کند
اجازه دادن به CameraX برای مدیریت بیشتر فرایند راه‌اندازی به این معنی است که عملکردهایی مانند ضربه زدن برای فوکوس و نیشگون گرفتن برای بزرگ‌نمایی به‌طور خودکار کار می‌کنند ازآنجایی‌که توسعه‌دهنده برنامه راه‌اندازی را انجام می‌دهد، فرصت‌های بیشتری برای سفارشی‌سازی پیکربندی وجود دارد، مانند فعال کردن چرخش تصویر خروجی یا تنظیم قالب تصویر خروجی در ImageAnalysis
الزام PreviewView برای پیش‌نمایش دوربین به CameraX امکان می‌دهد تا ادغام یکپارچه سرتاسری ارائه دهد، مانند ادغام ML Kit ما که می‌تواند مختصات نتیجه مدل 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 اتصال برنامه‌تان به دوربین را درطول کل چرخه حیات فعالیت Android مدیریت می‌کند، ازجمله در موارد زیر: بستن دوربین وقتی برنامه‌تان به پس‌زمینه می‌رود؛ برداشتن پیش‌نمایش دوربین وقتی دیگر نیازی به نمایش آن در صفحه نیست؛ و موقتاً متوقف کردن پیش‌نمایش دوربین وقتی فعالیت دیگری اولویت پیش‌زمینه را می‌گیرد، مثل تماس تصویری ورودی.

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

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

CameraX

پیکربندی CameraController / CameraProvider
↓ ↓ ↓ ↓
پیش‌نمایش ImageCapture ضبط کردن ویدیو ImageAnalysis
⁞ ⁞ ⁞ ⁞
مدیریت کردن Surface پیش‌نمایش و تنظیم آن روی «دوربین» PictureCallback را تنظیم کنید و takePicture()‎ را در «دوربین» فراخوانی کنید مدیریت پیکربندی Camera و MediaRecorder به ترتیب خاص کد تجزیه‌وتحلیل سفارشی ساخته‌شده روی Surface پیش‌نمایش
↑ ↑ ↑ ↑
کد ویژه دستگاه
↑
مدیریت چرخش و مقیاس‌بندی دستگاه
↑
مدیریت جلسه دوربین (انتخاب دوربین، مدیریت چرخه حیات)

Camera1

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

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

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

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

مفاهیم توسعه نرم‌افزار Android

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

  • View Binding یک کلاس اتصال برای فایل‌های چیدمان XML شما تولید می‌کند، که به شما امکان می‌دهد به نماهای خود در Activities ارجاع دهید، همان‌طور که در چندین قطعه کد بعدی نشان داده شده است. تفاوت‌هایی بین اتصال نما و findViewById() (روش قبلی برای ارجاع نماها) وجود دارد، اما در کد زیر باید بتوانید خطوط اتصال نما را با فراخوانی findViewById() مشابه جایگزین کنید.
  • Asynchronous Coroutines یک الگوی طراحی همزمانی است که در Kotlin 1.3 اضافه شده است و می‌توان از آن برای مدیریت روش‌های 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، یک پیاده‌سازی CameraX CameraProvider، و یک پیاده‌سازی CameraX CameraController است.

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

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

Camera1

در 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: CameraController

در 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

در اینجا نمونه‌ای از انتخاب دوربین جلو پیش‌فرض با 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 CameraX پیاده‌سازی‌های اساسی برای هر دو TextureView و SurfaceView دارد. CameraX تصمیم می‌گیرد که کدام پیاده‌سازی براساس عواملی مانند نوع دستگاه و نسخه Android که برنامه شما روی آن اجرا می‌شود، بهترین است. اگر هر دو پیاده‌سازی سازگار باشد، می‌توانید اولویتتان را با PreviewView.ImplementationMode اعلام کنید. گزینه COMPATIBLE از TextureView برای پیش‌نمایش استفاده می‌کند و مقدار PERFORMANCE از SurfaceView (درصورت امکان) استفاده می‌کند.

Camera1

برای نمایش پیش‌نمایش، باید کلاس 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: CameraController

در 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

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

تک‌ضرب زدن برای فوکوس کردن

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

Camera1

برای پیاده‌سازی «تک‌ضرب زدن برای فوکوس» در 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

CameraController به رویدادهای لمسی PreviewView گوش می‌دهد تا به‌طور خودکار «ضربه زدن برای فوکوس» را مدیریت کند. می‌توانید «تک‌ضرب برای فوکوس» را با setTapToFocusEnabled() فعال و غیرفعال کنید و مقدار را با گیرنده مربوطه 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

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

مراحل استفاده از PreviewView در اینجا آمده است:

  1. برای مدیریت رویدادهای ضربه زدن، یک تشخیص‌دهنده اشاره راه‌اندازی کنید.
  2. با رویداد تک‌ضرب، MeteringPoint را بااستفاده از MeteringPointFactory.createPoint() ایجاد کنید.
  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
}

نیشگون لمسی گرفتن برای بزرگ‌نمایی

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

Camera1

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

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

// 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: CameraController

مشابه ضربه زدن برای فوکوس، CameraController به رویدادهای لمسی PreviewView گوش می‌دهد تا به‌طور خودکار بزرگ‌نمایی با نزدیک کردن انگشتان را مدیریت کند. می‌توانید با setPinchToZoomEnabled()، «نزدیک کردن انگشتان برای بزرگ‌نمایی» را فعال و غیرفعال کنید و مقدار را با گیرنده مربوطه 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: CameraProvider

برای اینکه «نزدیک کردن انگشتان برای بزرگ‌نمایی» با CameraProvider کار کند، باید چند راه‌اندازی انجام شود. اگر از PreviewView استفاده نمی‌کنید، باید منطق را طوری تطبیق دهید که برای Surface سفارشی شما اعمال شود.

مراحل استفاده از PreviewView در اینجا آمده است:

  1. یک آشکارساز اشاره مقیاس راه‌اندازی کنید تا رویدادهای نیشگون گرفتن را مدیریت کند.
  2. ZoomState را از شیء Camera.CameraInfo دریافت کنید، جایی که نمونه Camera هنگام فراخوانی bindToLifecycle() برگردانده می‌شود.
  3. اگر ZoomState دارای مقدار zoomRatio است، آن را به‌عنوان نسبت بزرگ‌نمایی فعلی ذخیره کنید. اگر zoomRatio در ZoomState وجود ندارد، از نرخ بزرگ‌نمایی پیش‌فرض دوربین (۱٫۰) استفاده کنید.
  4. Take the product of the current zoom ratio with the scaleFactor to determine the new zoom ratio, and pass that into 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
}

گرفتن عکس

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

Camera1

در 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

CameraController CameraX سادگی Camera1 را برای ضبط تصویر با پیاده‌سازی روش takePicture() خود حفظ می‌کند. در اینجا، تابعی را برای پیکربندی ورودی 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

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

Camera1

ضبط ویدیو بااستفاده از 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

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

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

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

اگر از CameraProvider استفاده می‌کنید، باید VideoCapture UseCase ایجاد کنید و شیء 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)

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

در Recorder، روش‌های مختلفی برای آماده کردن آن وجود دارد. prepareRecording() را فراخوانی کنید تا گزینه‌های خروجی MediaStore را تنظیم کنید. اگر برنامه‌تان اجازه استفاده از میکروفون دستگاه را دارد، با withAudioEnabled() هم تماس بگیرید. سپس، start() را فراخوانی کنید تا ضبط شروع شود، و بافت و شنونده رویداد 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 نمونه‌های دوربین داریم. این نمونه‌ها به شما نشان می‌دهند که سناریوهای این راهنما چگونه در یک برنامه Android کامل قرار می‌گیرند.

اگر برای انتقال به CameraX به پشتیبانی بیشتری نیاز دارید یا درباره مجموعه میاناهای برنامه‌سازی کاربردی «دوربین Android» سؤالی دارید، لطفاً ازطریق گروه بحث CameraX با ما تماس بگیرید.