نقل Camera1 إلى CameraX

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

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

في هذا الدليل، ستجد سيناريوهات شائعة لتطبيقات الكاميرا. يتضمّن كل سيناريو عملية تنفيذ Camera1 وعملية تنفيذ CameraX بغرض مقارنة بينهما.

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

CameraController

CameraProvider

تتطلّب القليل من رمز الإعداد السماح بمزيد من التحكّم
يتيح السماح لـ CameraX بمعالجة المزيد من عملية الإعداد استخدام وظائف مثل النقر للتركيز والتصغير/التكبير عن طريق تصغير إصبعَيك تلقائيًا. بما أنّ مطوّر التطبيقات هو المسؤول عن الإعداد، تتوفّر المزيد من الفرص لتخصيص الإعدادات، مثل تفعيل دوران الصورة المعروضة أو ضبط تنسيق الصورة المعروضة في ImageAnalysis.
إنّ طلب PreviewView لمعاينة الكاميرا يسمح لـ CameraX بتوفير عملية دمج سلسة من البداية إلى النهاية، كما هو الحال في دمج ML Kit الذي يمكنه ربط إحداثيات نتيجة نموذج تعلُّم الآلة (مثل مربّعات حدود الوجوه) مباشرةً بإحداثيات المعاينة. توفّر إمكانية استخدام عنصر "سطح" مخصّص لمعاينة الكاميرا قدرًا أكبر من المرونة، مثل استخدام رمز "سطح" الحالي الذي يمكن أن يكون إدخالًا لأجزاء أخرى من تطبيقك.

إذا واجهت مشكلة في نقل البيانات، يُرجى التواصل معنا على مجموعة مناقشة CameraX.

قبل نقل البيانات

مقارنة استخدام CameraX بتطبيق Camera1

على الرغم من أنّ الرمز قد يبدو مختلفًا، إلا أنّ المفاهيم الأساسية في Camera1 و CameraX متشابهة جدًا. تُنشئ CameraX وظائف تجريدية للكاميرا الشائعة في حالات الاستخدام، وبالتالي، تعالج CameraX تلقائيًا العديد من المهام التي كان المطوّر ينفّذها في Camera1. هناك أربعة UseCase في CameraX، يمكنك استخدامها لتنفيذ مجموعة متنوعة من مهام الكاميرا: Preview، ImageCapture، VideoCapture، ImageAnalysis.

من الأمثلة على تعامل CameraX مع التفاصيل المنخفضة المستوى للمطوّرين هو ViewPort التي تتم مشاركتها بين UseCases النشطة. ويضمن ذلك أن ترى جميع UseCases وحدات البكسل نفسها تمامًا. في Camera1، عليك إدارة هذه التفاصيل بنفسك، ونظرًا للاختلاف في نِسب العرض إلى الارتفاع على مستوى كاميرات الأجهزة وشاشاتها، قد يكون من الصعب ضمان تطابق المعاينة مع الصور والفيديوهات التي تم التقاطها.

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

أخيرًا، تتعامل CameraX مع التدوير والتكبير/التصغير بدون الحاجة إلى أي رمز إضافي من جانبك. في حال استخدام Activity مع اتجاه غير مقفل، يتم إعداد UseCase في كل مرة يتم فيها تدوير الجهاز، لأنّ النظام يزيل Activity ويعيد إنشائه عند تغيير الاتجاه. يؤدي ذلك إلى ضبط UseCases لدوران الشاشة المستهدَف ليطابق اتجاه الشاشة بشكل تلقائي في كل مرة. اطّلِع على مزيد من المعلومات عن عمليات التدوير في CameraX.

قبل الانتقال إلى التفاصيل، إليك نظرة عامة على واجهة برمجة التطبيقات CameraX UseCase وكيفية ارتباط تطبيق Camera1 بها. (مفاهيم CameraX باللون الأزرق ومفاهيم Camera1 باللون الأخضر)

CameraX

إعدادات CameraController / CameraProvider
معاينة ImageCapture VideoCapture ImageAnalysis
إدارة سطح المعاينة وضبطه على الكاميرا ضبط PictureCallback واستدعاء takePicture() في Camera إدارة إعدادات Camera وMediaRecorder بترتيب محدّد رمز تحليل مخصّص تم إنشاؤه على سطح المعاينة
رمز خاص بالجهاز
إدارة تدوير الجهاز وتغيير حجمه
إدارة جلسة الكاميرا (اختيار الكاميرا وإدارة مراحل النشاط)

Camera1

التوافق والأداء في CameraX

تتوافق CameraX مع الأجهزة التي تعمل بالإصدار Android 5.0 (المستوى 21 من واجهة برمجة التطبيقات) والإصدارات الأحدث. ويمثّل ذلك أكثر من% 98 من أجهزة Android الحالية. تم تصميم CameraX للتعامل مع الاختلافات بين الأجهزة تلقائيًا، ما يقلل من الحاجة إلى استخدام رمزبرمجي خاص بالجهاز في تطبيقك. بالإضافة إلى ذلك، نختبر أكثر من 150 جهازًا على جميع إصدارات Android منذ الإصدار 5.0 في مركز اختبار CameraX. يمكنك مراجعة القائمة الكاملة للأجهزة المُدرَجة حاليًا في "مختبر الاختبار".

يستخدم CameraX Executor لتشغيل حِزمة الكاميرا. يمكنك ضبط أداة التنفيذ الخاصة بك في CameraX إذا كان تطبيقك يتضمّن متطلبات خاصة لإنشاء مؤشرات الترابط. في حال عدم ضبط هذه القيمة، تنشئ CameraX و تستخدم Executor داخليًا تلقائيًا محسّنًا. تتطلّب العديد من واجهات برمجة التطبيقات للمنصة التي تم إنشاء CameraX عليها حظر الاتصالات بين العمليات (IPC) باستخدام الأجهزة التي يمكن أن تستغرق أحيانًا مئات المللي ثانية للردّ. لهذا السبب، لا تستدعي CameraX واجهات برمجة التطبيقات هذه إلا من سلاسل المحادثات التي تعمل في الخلفية، ما يضمن عدم حظر السلسلة الرئيسية للمحادثات والحفاظ على سلاسة واجهة المستخدم. مزيد من المعلومات حول سلاسل المحادثات

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

مفاهيم تطوير تطبيقات Android

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

  • يُنشئ ربط العناصر فئة ربط لملفات تنسيق XML، ما يتيح لك بسهولة الإشارة إلى عناصرك في الأنشطة، كما هو موضّح في العديد من مقتطفات الرموز البرمجية أدناه. هناك بعض الاختلافات بين ربط العناصر وfindViewById() (الطريقة السابقة للإشارة إلى العناصر)، ولكن في الرمز البرمجي أدناه، من المفترض أن تتمكّن من استبدال أسطر ربط العناصر باستدعاء findViewById() مشابه.
  • عمليات التشغيل المتعدّدة غير المتزامنة هي أسلوب تصميم لعملية المعالجة المتزامنة تمت إضافته في Kotlin 1.3 ويمكن استخدامه لمعالجة طرق CameraX التي تؤدي إلى عرض ListenableFuture. وقد تم تسهيل ذلك من خلال مكتبة Concurrent في Jetpack اعتبارًا من الإصدار 1.1.0. لإضافة دالة معالجة مهام غير متزامنة إلى تطبيقك:
    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 "Android development concepts"
// section above.
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 "Android development concepts"
// section above.
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 هو مستطيل بوزن محدّد. ويتراوح وزن العنصر بين 1 و1000، ويتم استخدامه لمنح الأولوية للعنصر الذي تم التركيز عليه Areas في حال ضبط عناصر متعدّدة. يستخدم هذا المثال Area واحدًا فقط، لذا لا يهمّ قيمة الوزن. تتراوح إحداثيات المستطيل بين -1000 و1000. النقطة العلوية اليسرى هي (-1000، -1000). النقطة في أسفل يسار الشاشة هي (1000، 1000). يكون الاتجاه نسبيًا إلى اتجاه الاستشعار، أي ما يرصده المستشعر. لا يتأثّر الاتجاه بمحاولة تدوير 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
// "Android development concepts" section above.
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-zooom 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، استخدِم ميزة التكبير/التصغير التلقائية (1.0) في الكاميرا.
  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-zooom 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() متى أردت التقاط صورة. اطّلِع على رمز CameraController في هذا القسم للاطّلاع على مثال كامل للدالة takePhoto().

// 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. افتح قفل الكاميرا لاستخدامها من قِبل MediaRecorder من خلال الاتصال على Camera.unlock().
  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. يُرجى الاتصال برقم prepare() لإنهاء ضبط MediaRecorder.
  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 بشكل مستقل، ما دام بإمكانك استخدام قائمة حالات الاستخدام بشكل متزامن. تكون ميزة ImageCapture وImageAnalysis UseCase مفعّلة تلقائيًا، لذلك لم تكن بحاجة إلى الاتصال بـ setEnabledUseCases() لالتقاط صورة.

لاستخدام CameraController لتسجيل الفيديو، عليك أولاً استخدام setEnabledUseCases() للسماحVideoCapture UseCase.

// CameraX: Enable VideoCapture UseCase on CameraController.

cameraController.setEnabledUseCases(VIDEO_CAPTURE);

عندما تريد بدء تسجيل فيديو، يمكنك استدعاء الدالة CameraController.startRecording(). يمكن لهذه الوظيفة حفظ الفيديو المسجّل في File، كما هو موضّح في المثال أدناه. بالإضافة إلى ذلك، عليك تمرير Executor وclass تنفِّذ 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 بمثيل CameraProvider باستخدام UseCase الأخرى.

// 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 Camera API، يُرجى التواصل معنا على مجموعة مناقشة CameraX.