دليل استخدام Camera2 في أندرويد لالتقاط الصور وتسجيل الفيديو باحتراف

دقائق القراءة: 9

مقدمة: لماذا تُعد واجهة Camera2 API مهمة في تطبيقات أندرويد؟

أصبحت الكاميرا جزءاً أساسياً من تجربة الاستخدام في الهواتف الذكية، كما تعتمد عليها تطبيقات كثيرة في مجالات مثل التوثيق، والمسح، والتواصل، وصناعة المحتوى. وفي أندرويد، يوجد أكثر من أسلوب للتعامل مع الكاميرا: أسلوب مباشر وسريع للمهام التقليدية، وأسلوب أكثر تقدماً يتيح تحكماً عميقاً في سلوك الكاميرا وإعداداتها. هنا تبرز أهمية Camera2 API.

توفّر Camera2 API للمطورين مجموعة أدوات متقدمة للتعامل مع كاميرا الجهاز على مستوى منخفض نسبياً، ما يمنحهم قدرة أكبر على تخصيص المعاينة، والتقاط الصور، وتسجيل الفيديو، والتحكم في خصائص مثل التركيز والتعريض وغير ذلك. لكن في المقابل، تُعد هذه الواجهة من أكثر واجهات أندرويد تعقيداً، لأنها تتطلب فهماً واضحاً للعلاقات بين عدة مكونات مترابطة.

شرح واجهة Camera2 API في أندرويد لالتقاط الصور وتسجيل الفيديو داخل التطبيقات

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

متى تستخدم Camera2 API ومتى لا تحتاجها؟

قبل الدخول في التفاصيل البرمجية، من المهم تحديد حالة الاستخدام بدقة. استخدام Camera2 API يكون منطقياً عندما يحتاج التطبيق إلى تحكم مخصص في الكاميرا أو في آلية عملها الداخلية، وليس لمجرد تنفيذ وظيفة تصوير بسيطة.

حالات مناسبة لاستخدام Camera2 API

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

حالات لا تستدعي هذا التعقيد

  • التقاط صورة واحدة فقط دون تخصيصات إضافية.
  • تسجيل فيديو بسيط باستخدام حلول أندرويد القياسية.
  • تطوير ميزة سريعة لا تتطلب تحكماً كاملاً بالكاميرا.

باختصار، كلما زادت حاجتك إلى التحكم في تفاصيل الكاميرا، زادت أهمية استخدام Camera2 API.

المتطلبات الأساسية داخل ملف Manifest

قبل تشغيل الكاميرا، يجب تعريف الصلاحيات والميزات المطلوبة داخل ملف AndroidManifest.xml.

إضافة صلاحية الكاميرا

<uses-permission android:name="android.permission.CAMERA" />

تعريف ميزة الكاميرا في الجهاز

<uses-feature android:name="android.hardware.camera" />

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

المكونات الرئيسية في Camera2 API

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

مخطط يوضح المكونات الأساسية في Camera2 API داخل تطبيقات أندرويد

العنصر الأول: TextureView

يُستخدم TextureView كعنصر واجهة لعرض المحتوى المرئي، مثل البث الحي القادم من الكاميرا. وهو الخيار المناسب عندما تريد إظهار معاينة مباشرة داخل الشاشة قبل التقاط الصورة أو أثناء تسجيل الفيديو.

ومن أهم ما يرتبط به:

  • الخاصية SurfaceTexture.
  • الواجهة SurfaceTextureListener.

توفر الواجهة SurfaceTextureListener أربع دوال استدعاء مهمة:

  • onSurfaceTextureAvailable
  • onSurfaceTextureSizeChanged
  • onSurfaceTextureUpdated
  • onSurfaceTextureDestroyed
private val surfaceTextureListener = object : TextureView.SurfaceTextureListener {
    override fun onSurfaceTextureAvailable(texture: SurfaceTexture, width: Int, height: Int) {
    }

    override fun onSurfaceTextureSizeChanged(texture: SurfaceTexture, width: Int, height: Int) {
    }

    override fun onSurfaceTextureDestroyed(texture: SurfaceTexture) {
    }

    override fun onSurfaceTextureUpdated(texture: SurfaceTexture) {
    }
}

تُعد الدالة onSurfaceTextureAvailable الأهم، لأنها اللحظة التي يصبح فيها SurfaceTexture جاهزاً لاستقبال بث الكاميرا. ولن يحدث ذلك إلا بعد إرفاق TextureView فعلياً بنافذة العرض.

العنصر الثاني: CameraManager

من خلال CameraManager يمكن للتطبيق الوصول إلى الكاميرات المتاحة على الجهاز وفتح الاتصال بالكاميرا المطلوبة وقراءة خصائصها. ومن أشهر الدوال المستخدمة معه:

  • openCamera
  • getCameraCharacteristics
  • getCameraIdList

بعد التأكد من جاهزية TextureView، نستخدم الدالة openCamera لفتح اتصال بالكاميرا. وتستقبل هذه الدالة ثلاث معاملات أساسية:

  1. CameraId من النوع String.
  2. CameraDevice.StateCallback.
  3. Handler.

المعرّف CameraId يحدد الكاميرا المستهدفة، وغالباً ما تكون هناك كاميرا خلفية وأخرى أمامية، ولكل واحدة معرّف خاص بها.

val cameraManager: CameraManager = getSystemService(Context.CAMERA_SERVICE) as CameraManager
val cameraIds: Array<String> = cameraManager.cameraIdList
var cameraId: String = ""

for (id in cameraIds) {
    val cameraCharacteristics = cameraManager.getCameraCharacteristics(id)

    // If we want to choose the rear facing camera instead of the front facing one
    if (cameraCharacteristics.get(CameraCharacteristics.LENS_FACING) == CameraCharacteristics.LENS_FACING_FRONT) continue
}

val previewSize = cameraCharacteristics
    .get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP)!!
    .getOutputSizes(ImageFormat.JPEG)
    .maxByOrNull { it.height * it.width }!!

val imageReader = ImageReader.newInstance(previewSize.width, previewSize.height, ImageFormat.JPEG, 1)
imageReader.setOnImageAvailableListener(onImageAvailableListener, backgroundHandler)
cameraId = id

العنصر الثالث: CameraDevice.StateCallback

بعد محاولة فتح الكاميرا، نحتاج إلى معرفة النتيجة. وهنا يأتي دور CameraDevice.StateCallback، والذي يعالج الحالات الأساسية التالية:

  • نجاح فتح الكاميرا.
  • انقطاع الاتصال بالكاميرا.
  • حدوث خطأ أثناء الفتح.
private val cameraStateCallback = object : CameraDevice.StateCallback() {
    override fun onOpened(camera: CameraDevice) {
    }

    override fun onDisconnected(cameraDevice: CameraDevice) {
    }

    override fun onError(cameraDevice: CameraDevice, error: Int) {
        val errorMsg = when (error) {
            ERROR_CAMERA_DEVICE -> "Fatal (device)"
            ERROR_CAMERA_DISABLED -> "Device policy"
            ERROR_CAMERA_IN_USE -> "Camera in use"
            ERROR_CAMERA_SERVICE -> "Fatal (service)"
            ERROR_MAX_CAMERAS_IN_USE -> "Maximum cameras in use"
            else -> "Unknown"
        }
        Log.e(TAG, "Error when trying to connect camera $errorMsg")
    }
}

من الأفضل التعامل مع هذه الحالات بعناية، لأن مشكلات الكاميرا في الأجهزة المختلفة قد تختلف بحسب سياسات النظام أو انشغال الكاميرا بتطبيق آخر.

العنصر الرابع: خيط الخلفية باستخدام HandlerThread

عمليات الكاميرا ليست مناسبة للتنفيذ على الخيط الرئيسي Main Thread، لأن ذلك قد يسبب بطئاً في الواجهة أو تجمداً مؤقتاً. لهذا نستخدم HandlerThread مع Handler لتشغيل هذه العمليات في الخلفية.

private lateinit var backgroundHandlerThread: HandlerThread
private lateinit var backgroundHandler: Handler

private fun startBackgroundThread() {
    backgroundHandlerThread = HandlerThread("CameraVideoThread")
    backgroundHandlerThread.start()
    backgroundHandler = Handler(backgroundHandlerThread.looper)
}

private fun stopBackgroundThread() {
    backgroundHandlerThread.quitSafely()
    backgroundHandlerThread.join()
}

بعد تجهيز كل ما سبق، يمكننا فتح الكاميرا فعلياً:

cameraManager.openCamera(cameraId, cameraStateCallback, backgroundHandler)

وعند نجاح الفتح داخل onOpened نبدأ إعداد منطق عرض المعاينة للمستخدم.

عرض معاينة الكاميرا في أندرويد باستخدام TextureView وCamera2 API

كيفية عرض معاينة الكاميرا باستخدام TextureView

بعد الحصول على كائن الكاميرا CameraDevice وتجهيز TextureView، نحتاج إلى ربطهما ببعضهما حتى يظهر البث الحي للمستخدم. ويتم ذلك عبر استخدام SurfaceTexture وإنشاء طلب التقاط من نوع معاينة.

val surfaceTexture: SurfaceTexture? = textureView.surfaceTexture // 1
val cameraCharacteristics = cameraManager.getCameraCharacteristics(cameraId) // 2
val previewSize = cameraCharacteristics
    .get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP)!!
    .getOutputSizes(ImageFormat.JPEG)
    .maxByOrNull { it.height * it.width }!!

surfaceTexture?.setDefaultBufferSize(previewSize.width, previewSize.height) // 3
val previewSurface: Surface = Surface(surfaceTexture)

captureRequestBuilder = cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW) // 4
captureRequestBuilder.addTarget(previewSurface) // 5

cameraDevice.createCaptureSession(
    listOf(previewSurface, imageReader.surface),
    captureStateCallback,
    null
) // 6

في هذا المقطع تحدث الخطوات التالية:

  1. الحصول على surfaceTexture من textureView.
  2. استخراج خصائص الكاميرا وتحديد حجم المعاينة المناسب.
  3. تعيين حجم المخزن المؤقت الافتراضي عبر setDefaultBufferSize.
  4. إنشاء CaptureRequest باستخدام القالب TEMPLATE_PREVIEW.
  5. إضافة سطح المعاينة إلى الطلب.
  6. بدء جلسة التقاط عبر createCaptureSession.

ما وظيفة CameraCaptureSession.StateCallback؟

جلسة الالتقاط CameraCaptureSession هي المسؤولة عن متابعة تنفيذ طلبات الكاميرا. ومن أهم دوالها:

  • onConfigured
  • onConfigureFailed
private val captureStateCallback = object : CameraCaptureSession.StateCallback() {
    override fun onConfigureFailed(session: CameraCaptureSession) {
    }

    override fun onConfigured(session: CameraCaptureSession) {
    }
}

عند نجاح الإعداد داخل onConfigured، نفعّل طلباً متكرراً حتى تستمر المعاينة بشكل حي ومتواصل:

session.setRepeatingRequest(captureRequestBuilder.build(), null, backgroundHandler)

المعامل الأول هو الطلب النهائي بعد استدعاء build(). أما المعامل الثاني فهو CaptureCallback ويمكن تركه null إذا لم تكن بحاجة لمعالجة كل إطار، بينما يحدد المعامل الثالث الخيط الخلفي الذي ستعمل عليه العملية.

شرح عملي لعرض بث الكاميرا المباشر في تطبيق أندرويد باستخدام Camera2

كيفية التقاط صورة ثابتة باستخدام Camera2 API

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

val orientations: SparseIntArray = SparseIntArray(4).apply {
    append(Surface.ROTATION_0, 0)
    append(Surface.ROTATION_90, 90)
    append(Surface.ROTATION_180, 180)
    append(Surface.ROTATION_270, 270)
}

val captureRequestBuilder = cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE)
captureRequestBuilder.addTarget(imageReader.surface)

val rotation = windowManager.defaultDisplay.rotation
captureRequestBuilder.set(CaptureRequest.JPEG_ORIENTATION, orientations.get(rotation))

cameraCaptureSession.capture(captureRequestBuilder.build(), captureCallback, null)

في هذا المثال نحدد اتجاه الصورة اعتماداً على دوران الشاشة، ثم نرسل الطلب عبر الدالة capture لتنفيذ عملية التصوير.

ما هو ImageReader ولماذا نحتاجه؟

ImageReader هو المكوّن المسؤول عن استقبال بيانات الصورة التي تُنتجها الكاميرا على سطح محدد. وبما أن الصورة الناتجة تحتاج إلى قناة إخراج مستقلة، فإن هذا الكائن يلعب دوراً أساسياً في التقاط الصور الثابتة.

val cameraManager: CameraManager = getSystemService(Context.CAMERA_SERVICE) as CameraManager
val cameraIds: Array<String> = cameraManager.cameraIdList
var cameraId: String = ""

for (id in cameraIds) {
    val cameraCharacteristics = cameraManager.getCameraCharacteristics(id)

    // If we want to choose the rear facing camera instead of the front facing one
    if (cameraCharacteristics.get(CameraCharacteristics.LENS_FACING) == CameraCharacteristics.LENS_FACING_FRONT) continue
}

val previewSize = cameraCharacteristics
    .get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP)!!
    .getOutputSizes(ImageFormat.JPEG)
    .maxByOrNull { it.height * it.width }!!

val imageReader = ImageReader.newInstance(previewSize.width, previewSize.height, ImageFormat.JPEG, 1)
imageReader.setOnImageAvailableListener(onImageAvailableListener, backgroundHandler)
cameraId = id

عند إنشاء ImageReader نمرر:

  • العرض width.
  • الارتفاع height.
  • تنسيق الصورة مثل ImageFormat.JPEG.
  • عدد الصور القصوى التي يمكن الاحتفاظ بها مؤقتاً.

وبعد ذلك نربط المستمع onImageAvailableListener ليتم استدعاؤه فور توفر صورة جديدة.

val onImageAvailableListener = object : ImageReader.OnImageAvailableListener {
    override fun onImageAvailable(reader: ImageReader) {
        val image: Image = reader.acquireLatestImage()
    }
}

ملاحظة مهمة: يجب إغلاق الصورة بعد الانتهاء من معالجتها، وإلا قد تفشل عمليات الالتقاط التالية بسبب امتلاء المخزن المؤقت.

التقاط صورة ثابتة في أندرويد باستخدام Camera2 وImageReader

كيفية تسجيل فيديو باستخدام MediaRecorder وCamera2

لتسجيل الفيديو، نحتاج إلى مكوّن إضافي هو MediaRecorder. هذا الكائن مسؤول عن تسجيل الصوت والفيديو، وسنستخدمه هنا لتسجيل الفيديو وربطه بجلسة الكاميرا.

إعداد MediaRecorder بالشكل الصحيح

من أكثر النقاط حساسية في تسجيل الفيديو ترتيب إعدادات MediaRecorder. إذ يجب ضبط القيم بالتسلسل الصحيح، وإلا قد تظهر استثناءات أثناء التشغيل.

fun setupMediaRecorder(width: Int, height: Int) {
    val mediaRecorder: MediaRecorder = MediaRecorder()
    mediaRecorder.setVideoSource(MediaRecorder.VideoSource.SURFACE)
    mediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4)
    mediaRecorder.setVideoEncoder(MediaRecorder.VideoEncoder.H264)
    mediaRecorder.setVideoSize(videoSize.width, videoSize.height)
    mediaRecorder.setVideoFrameRate(30)
    mediaRecorder.setOutputFile(PATH_TO_FILE)
    mediaRecorder.setVideoEncodingBitRate(10_000_000)
    mediaRecorder.prepare()
}

احرص على ملاحظة الدالة setOutputFile، إذ يجب تمرير مسار ملف صالح لتخزين الفيديو الناتج. وبعد الانتهاء من جميع الإعدادات، يجب استدعاء prepare() قبل تشغيل التسجيل بواسطة start().

بدء تسجيل الفيديو

بعد تجهيز mediaRecorder، نُنشئ طلب التقاط جديداً من نوع تسجيل، ثم نربط بين سطح المعاينة وسطح التسجيل.

fun startRecording() {
    val surfaceTexture: SurfaceTexture? = textureView.surfaceTexture
    surfaceTexture?.setDefaultBufferSize(previewSize.width, previewSize.height)

    val previewSurface: Surface = Surface(surfaceTexture)
    val recordingSurface = mediaRecorder.surface

    captureRequestBuilder = cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_RECORD)
    captureRequestBuilder.addTarget(previewSurface)
    captureRequestBuilder.addTarget(recordingSurface)

    cameraDevice.createCaptureSession(
        listOf(previewSurface, recordingSurface),
        captureStateVideoCallback,
        backgroundHandler
    )
}

في هذا السيناريو نستخدم سطحين أساسيين:

  • previewSurface لعرض المعاينة للمستخدم.
  • recordingSurface لتسجيل الفيديو فعلياً داخل MediaRecorder.

تشغيل التسجيل بعد نجاح تهيئة الجلسة

بعد نجاح إعداد جلسة الالتقاط، نفعّل الطلب المتكرر ثم نبدأ التسجيل:

val captureStateVideoCallback = object : CameraCaptureSession.StateCallback() {
    override fun onConfigureFailed(session: CameraCaptureSession) {
    }

    override fun onConfigured(session: CameraCaptureSession) {
        session.setRepeatingRequest(captureRequestBuilder.build(), null, backgroundHandler)
        mediaRecorder.start()
    }
}

وهذا يعني أن الكاميرا تستمر في بث المعاينة وفي الوقت نفسه ترسل الإطارات إلى MediaRecorder لحفظها على شكل ملف فيديو.

إيقاف التسجيل وإعادة التهيئة

عند الانتهاء من التسجيل، نستخدم الدالتين stop() وreset():

mediaRecorder.stop()
mediaRecorder.reset()

تُستخدم stop() لإنهاء التسجيل الحالي، بينما تعيد reset() الكائن إلى حالة تسمح بإعادة ضبطه من جديد قبل عملية تسجيل أخرى.

أفضل ممارسات عند استخدام Camera2 API

لأن التعامل مع الكاميرا يختلف بين الأجهزة، فمن الجيد اتباع مجموعة من الممارسات التي تقلل الأعطال وتحسن استقرار التطبيق:

  • نفّذ عمليات الكاميرا الثقيلة على background thread وليس على الخيط الرئيسي.
  • تحقق دائماً من الصلاحيات قبل محاولة فتح الكاميرا.
  • اختر أحجام المعاينة والصور بما يتناسب مع إمكانات الجهاز والأداء المطلوب.
  • أغلق الموارد غير المستخدمة مثل Image وCameraDevice وCameraCaptureSession عند الانتهاء.
  • تعامل مع اختلافات الأجهزة، فبعض السلوكيات قد لا تكون متطابقة بين الشركات المصنعة.
  • اختبر التبديل بين الكاميرا الأمامية والخلفية إذا كان تطبيقك يدعم ذلك.

أخطاء شائعة يجب الانتباه لها

  • نسيان إغلاق الصورة بعد acquireLatestImage() مما يمنع التقاط صور جديدة.
  • تشغيل mediaRecorder.start() قبل استدعاء prepare().
  • إجراء عمليات الكاميرا على الواجهة الرئيسية مما يسبب بطئاً ملحوظاً.
  • الاعتماد على حجم إخراج غير مناسب قد يؤدي إلى مشكلات في المعاينة أو الأداء.
  • بدء جلسة التقاط قبل جاهزية SurfaceTexture.

الخلاصة التقنية

تُعد Camera2 API خياراً قوياً للمطورين الذين يحتاجون إلى تحكم متقدم في كاميرا أندرويد، لكنها في الوقت نفسه تتطلب فهماً دقيقاً لتسلسل العمل بين TextureView وCameraManager وCameraCaptureSession وImageReader وMediaRecorder. من الناحية التقنية، أفضل طريقة لإتقانها ليست حفظ الأوامر فقط، بل فهم دورة حياة الكاميرا وكيفية انتقال البيانات بين الأسطح المختلفة. وكلما كان تنظيمك للجلسات والخيوط الخلفية والموارد أدق، حصلت على تجربة تصوير أكثر استقراراً واحترافية داخل التطبيق.

اترك تعليقاً

لن يتم نشر عنوان بريدك الإلكتروني. الحقول الإلزامية مشار إليها بـ *