نقل Camera1 إلى CameraX

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

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

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

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

CameraController

CameraProvider

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

إذا واجهت أي مشاكل أثناء محاولة نقل البيانات، يمكنك التواصل معنا من خلال مجموعة مناقشة CameraX.

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

مقارنة استخدام CameraX مع Camera1

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

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

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

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

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

CameraX

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

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. بالإضافة إلى الأساسيات، إليك بعض المفاهيم التي من المفيد فهمها قبل الانتقال إلى الرمز التالي:

  • تنشئ View Binding فئة ربط لملفات تنسيق 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 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 هو مستطيل بوزن محدّد. الوزن هو قيمة تتراوح بين 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
// 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، استخدِم معدّل التكبير/التصغير التلقائي للكاميرا (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-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

تحافظ واجهة برمجة التطبيقات CameraX CameraController على بساطة 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 بشكل مستقل، طالما أنّه يمكن استخدام قائمة 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().

ملاحظة: إذا كنت تستخدم الإصدار 1.3.0-alpha02 أو إصدارًا أحدث من CameraX، يتوفّر مَعلمة إضافية 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، يُرجى التواصل معنا من خلال مجموعة مناقشة CameraX.