نقل Camera1 إلى CameraX

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

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

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

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

وحدة التحكّم في الكاميرا

CameraProvider

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

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

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

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

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

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

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

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

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

كاميراX

إعدادات Camera Controller / CameraProvider
معاينة التقاط صورة التقاط الفيديو تحليل الصور
إدارة معاينة Surface وضبطها على الكاميرا ضبط ImageCallback واستدعاء TakeImage() على الكاميرا إدارة إعداد الكاميرا وMediaRecorder بترتيب معيّن رمز تحليل مخصّص تم إنشاؤه فوق مساحة المعاينة
الرمز الخاص بالجهاز
إدارة تدوير الجهاز وتحجيمه
إدارة جلسة الكاميرا (اختيار الكاميرا وإدارة مراحل النشاط)

الكاميرا1

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

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

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

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

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

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

نقل السيناريوهات الشائعة

يوضّح هذا القسم كيفية نقل السيناريوهات الشائعة من Camera1 إلى CameraX. ويغطي كل سيناريو عملية تنفيذ Camera1، وتنفيذ CameraProvider من CameraX، وتنفيذ CameraX CameraController.

اختيار كاميرا

في تطبيق الكاميرا الخاص بك، أحد أول الأشياء التي قد ترغب في تقديمها هو طريقة اختيار كاميرات مختلفة.

الكاميرا1

في تطبيق "كاميرا1"، يمكنك إمّا استدعاء Camera.open() بدون أي معلَمات لفتح أول كاميرا خلفية، أو إدخال رقم تعريف صحيح للكاميرا التي تريد فتحها. فيما يلي مثال للشكل الذي قد يبدو عليه:

// Camera1: select a camera from id.

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

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

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

CameraX: وحدة تحكّم الكاميرا

في CameraX، يتولى صف CameraSelector اختيار الكاميرا. يجعل CameraX الحالة الشائعة لاستخدام الكاميرا الافتراضية أمرًا سهلاً. يمكنك تحديد ما إذا كنت تريد استخدام الكاميرا الأمامية التلقائية أم الكاميرا الخلفية التلقائية. بالإضافة إلى ذلك، يتيح لك الكائن CameraControl في CameraX ضبط مستوى التكبير/التصغير لتطبيقك بسهولة، وبالتالي إذا كان تطبيقك يعمل على جهاز متوافق مع الكاميرات المنطقية، سيتم التبديل إلى العدسة المناسبة.

إليك رمز CameraX لاستخدام الكاميرا الخلفية التلقائية مع CameraController:

// CameraX: select a camera with CameraController

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

CameraX: CameraProvider

في ما يلي مثال على اختيار الكاميرا الأمامية التلقائية مع 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 (إذا أمكن).

الكاميرا1

لعرض معاينة، عليك كتابة صف Preview الخاص بك من خلال تنفيذ واجهة android.view.SurfaceHolder.Callback التي تُستخدَم لتمرير بيانات الصور من أجهزة الكاميرا إلى التطبيق. بعد ذلك، يجب تمرير الفئة Preview إلى العنصر Camera قبل أن تتمكّن من بدء معاينة الصورة المباشرة.

// Camera1: set up a camera preview.

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

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

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

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

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

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

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

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

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

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

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

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

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

CameraX: وحدة تحكّم الكاميرا

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

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

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

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

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

CameraX: CameraProvider

باستخدام CameraProvider في CameraX، لن تحتاج إلى استخدام PreviewView، ولكنّ ذلك يساعد في تبسيط عملية إعداد المعاينة بشكل كبير عبر Camera1. يستخدم هذا المثال PreviewView لأغراض العرض التوضيحي، ولكن يمكنك كتابة SurfaceProvider مخصّص لتمريره إلى setSurfaceProvider() إذا كانت لديك احتياجات أكثر تعقيدًا.

في هذه الحالة، لا يمكن استخدام UseCase Preview كما هو الحال مع 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()
    }
}

النقر للتركيز

عندما تكون معاينة الكاميرا على الشاشة، أحد عناصر التحكم الشائعة هو ضبط نقطة التركيز عندما ينقر المستخدم على المعاينة.

الكاميرا1

لتفعيل ميزة "النقر للتركيز" في تطبيق "كاميرا1"، يجب احتساب التركيز الأمثل 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" إلى أحداث اللمس في "PreviewView" لمعالجة النقر للتركيز تلقائيًا. يمكنك تفعيل ميزة "النقر للتركيز" أو إيقافها باستخدام setTapToFocusEnabled()، والتحقّق من القيمة باستخدام دالة getter المقابلة isTapToFocusEnabled().

تعرض الطريقة getTapToFocusState() كائن LiveData لتتبُّع التغييرات إلى حالة التركيز على CameraController.

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

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

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

CameraX: CameraProvider

عند استخدام 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
}

التصغير أو التكبير بإصبعين

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

الكاميرا1

هناك طريقتان للتكبير أو التصغير باستخدام 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 إلى أحداث اللمس في PreviewView للتعامل مع التصغير أو التكبير بإصبعين تلقائيًا. يمكنك تفعيل أو إيقاف استخدام الإصبعين للتكبير أو التصغير باستخدام setPinchToZoomEnabled()، والتحقّق من القيمة باستخدام دالة getter المناظرة isPinchToZoomEnabled().

تعرض الطريقة getZoomState() عنصر LiveData لتتبُّع التغييرات على ZoomState في CameraController.

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

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

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

CameraX: 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
}

التقاط صورة

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

الكاميرا1

في تطبيق "كاميرا1"، يجب أولاً تحديد 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 على بساطة استخدام 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 مرة أخرى مع الكثير من هذه التعقيدات.

الكاميرا1

يتطلب التقاط الفيديو باستخدام 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 في 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 وفئة يتم فيها تنفيذ علامة 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، يُرجى التواصل معنا من خلال مجموعة مناقشة CameraX.