আমরা সবাই আমাদের ফোনের ক্যামেরা ব্যবহার করি এবং অনেক বেশি ব্যবহার করি। এমনকি কিছু অ্যাপ্লিকেশন আছে যেগুলো ক্যামেরাকে একটি ফিচার হিসেবে ইন্টিগ্রেট করেছে।
এক পাশে ক্যামেরার সাথে ইন্টারঅ্যাক্ট করার একটি স্ট্যান্ডার্ড পদ্ধতি আছে। অন্য পাশে ক্যামেরার সাথে ইন্টারঅ্যাক্ট কাস্টোমাইজ করার একটি পদ্ধতি। এই পার্থক্যটি বোঝা খুব গুরুত্বপূর্ণ। এবং সেটাই Camera2-র কাজ।
Camera2 কী?
API লেভেল ২১ থেকে উপলব্ধ থাকা সত্ত্বেও, Camera2 API ডেভেলপারদের التى করতে হয় সবচেয়ে জটিল আর্কিটেকচারের একটি।
এই API এবং এর পূর্ববর্তীটি ডেভেলপারদের তাদের অ্যাপ্লিকেশনের ভিতরে ক্যামেরার সাথে ইন্টারঅ্যাক্ট করার ক্ষমতা দেওয়ার জন্য রাখা হয়েছিল।
মাইক্রোফোন বা ডিভাইসের ভলিউমের সাথে ইন্টারঅ্যাক্ট করার পদ্ধতির মতোই, Camera2 API আপনাকে ডিভাইসের ক্যামেরার সাথে ইন্টারঅ্যাক্ট করার টুলস দেয়।
সাধারণত, যদি আপনি Camera2 API ব্যবহার করতে চান, তবে সেটি শুধু ছবি তোলা বা ভিডিও রেকর্ড করার জন্য নয়। কারণ এই API ক্যামেরার গভীর নিয়ন্ত্রণ দেয় বিভিন্ন ক্লাস এক্সপোজ করে যেগুলো প্রতিটি স্পেসিফিক ডিভাইসের জন্য কনফিগার করতে হয়।
আপনি যদি আগে ক্যামেরার সাথে কাজ করে থাকেন, তবে এটি সাবেক ক্যামেরা API থেকে এতটাই 劇烈 পরিবর্তন যে, আপনি যেটা জানেন সেটা ভুলে যাওয়া ভালো।
এই API সরাসরি কিভাবে ব্যবহার করবেন সেটা দেখানোর প্রচুর রিসোর্স আছে, কিন্তু 그중 কিছু পুরনো হয়ে যেতে পারে এবং কিছু পুরো ছবি পেশ করে না।
তাই নিজে থেকে মিসিং পিসগুলো পূরণ করার চেষ্টা করার বদলে, এই আর্টিকেলটি (আশা করি) Camera2 API-এর সাথে ইন্টারঅ্যাক্ট করার জন্য আপনার वन-স্টপ শপ হবে।
Camera2 ব্যবহারের ক্ষেত্র
ডুবার আগে, বুঝতে হবে যে যদি আপনি শুধু ছবি তোলা বা ভিডিও রেকর্ড করার জন্য ক্যামেরা ব্যবহার করতে চান, তবে Camera2 API-র সাথে মাথা ঘামানোর দরকার নেই।
Camera2 API ব্যবহারের প্রাইমারি কারণ হল যদি আপনার অ্যাপ্লিকেশন ক্যামেরার বা এর ফাংশনালিটির সাথে কিছু কাস্টোম ইন্টারঅ্যাকশন চায়।
যদি আপনি ছবি তোলা বা ভিডিও রেকর্ড করার 쪽ে আগ্রহী হন, তবে আমি গুগলের নিচের ডকুমেন্টেশন দেখার পরামর্শ দিচ্ছি:
- Take Photos
- Capture Video
সেখানে আপনি আপনার ক্যামেরার সাথে সুন্দর ছবি ও ভিডিও ক্যাপচার করার জন্য সমস্ত প্রয়োজনীয় ধাপ পাবেন। কিন্তু এই আর্টিকেলে মূল ফোকাস থাকবে Camera2 কিভাবে ব্যবহার করবেন সেটা।
এখন, আমাদের ম্যানিফেস্ট ফাইলে কিছু জিনিস যোগ করতে হবে:
ক্যামেরা পারমিশন:
<uses-permission android:name="android.permission.CAMERA" />
ক্যামেরা ফিচার:
<uses-feature android:name="android.hardware.camera" />
আপনাকে ক্যামেরা পারমিশন গ্রান্ট করা হয়েছে কিনা চেক করতে হবে, কিন্তু যেহেতু এই টপিকটা বিস্তারিত আলোচনা হয়েছে, আমরা এখানে সেটা কভার করব না।
Camera2 API কম্পোনেন্ট সেট আপ করা
Camera2 API چندین নতুন ইন্টারফেস এবং ক্লাস পরিচয় করায়। আসুন প্রতিটিকে ভাঙে বুঝি যাতে আমরা কিভাবে ব্যবহার করব সেটা ভালোভাবে বুঝতে পারি।
প্রথমে, TextureView-সহ শুরু করি।
Camera2 TextureView কম্পোনেন্ট
TextureView হল একটি UI কম্পোনেন্ট যা একটি কন্টেন্ট স্ট্রিম ডিসপ্লে করতে ব্যবহৃত হয় (ভিডিও ভাবুন)। ক্যামেরার ফিড ডিসপ্লে করতে হলে TextureView ব্যবহার করতে হয়, ভাবেই প্রিভিউ হোক বা ছবি/ভিডিও তোলার আগে।
TextureView-এর দুটি গুরুত্বপূর্ণ প্রপার্টি:
- SurfaceTexture ফিল্ড
- 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) { } }
প্রথম কলব্যাকটি ক্যামেরা ব্যবহার করার সময় অত্যন্ত গুরুত্বপূর্ণ। কারণ আমরা জানতে চাই কখন SurfaceTexture উপলব্ধ হয়েছে যাতে আমরা সেটাতে ফিড ডিসপ্লে শুরু করতে পারি।
লক্ষ্য করুন যে TextureView শুধু তখন উপলব্ধ হয় যখন এটি একটি উইন্ডোর সাথে আট্যাচড হয়।
ক্যামেরার সাথে ইন্টারঅ্যাক্ট পরিবর্তন হয়েছে সাবেক API থেকে। এখন у нас есть CameraManager। এটি একটি সিস্টেম সার্ভিস যা আমাদের CameraDevice অবজেক্টের সাথে ইন্টারঅ্যাক্ট করতে দেয়।
যে মেথডগুলোতে আপনাকে খুব লক্ষ্য দিতে হবে:
- openCamera
- getCameraCharacteristics
- getCameraIdList
TextureView উপলব্ধ এবং রেডি হলে জানার পর, আমাদের openCamera কল করে ক্যামেরার সাথে কানেকশন খোলতে হবে। এই মেথড তিনটি আর্গুমেন্ট নেয়:
- CameraId - String
- CameraDevice.StateCallback
- A Handler
CameraId আর্গুমেন্ট নির্দেশ করে যে আমরা কোন ক্যামেরার সাথে কানেক্ট করতে চাই। আপনার ফোনে মূলত দুটি ক্যামেরা থাকে, ফ্রন্ট এবং ব্যাক। প্রতিটির নিজস্ব ইউনিক আইডি থাকে। সাধারণত এটি ০ বা ১।
ক্যামেরা আইডি কিভাবে পাব? CameraManager-এর getCameraIdList মেথড ব্যবহার করুন। এটি ডিভাইস থেকে চিন्हিত সকল ক্যামেরা আইডির একটি স্ট্রিং টাইপের অ্যারে রিটার্ন করবে।
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 (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-এ পাবেন:
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") } }
তৃতীয় আর্গুমেন্ট হলো এই কাজ কোথায় হবে। যেহেতু আমরা মেইন থ্রেড আকRANT করব না, তাই ব্যাকগ্রাউন্ডে এই কাজ করা ভালো।
সেজন্য আমাদের একটি 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() }
আমরা যা যা করেছি তার সাথে, এখন openCamera কল করতে পারি:
cameraManager.openCamera(cameraId, cameraStateCallback,backgroundHandler)
তারপর onOpened কলব্যাকে, আমরা TextureView-এর মাধ্যমে ক্যামেরা ফিড ইউজারকে কিভাবে দেখাতে হবে সেটার লজিক শুরু করতে পারি।
ফিডের প্রিভিউ দেখানোর পদ্ধতি
আমাদের ক্যামেরা (cameraDevice) এবং ফিড দেখানোর জন্য TextureView আছে। কিন্তু আমাদের সেগুলোকে একে অপরের সাথে সংযুক্ত করতে হবে যাতে আমরা ফিডের প্রিভিউ দেখাতে পারি।
সেটা করতে, আমরা TextureView-এর SurfaceTexture প্রপার্টি ব্যবহার করব এবং একটি CaptureRequest বিল্ড করব।
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
উপরের কোডে, প্রথমে আমরা TextureView থেকে surfaceTexture পাই। তারপর cameraCharacteristics অবজেক্ট ব্যবহার করে সকল আউটপুট সাইজের তালিকা পাই। ডিজায়ার্ড সাইজ পেতে, সেটি surfaceTexture-এর জন্য সেট করি।
এরপর, TEMPLATE_PREVIEW পাস করে একটি captureRequest তৈরি করি। আমাদের ইনপুট সারফেসটি captureRequest-এ যোগ করি।
শেষে, আমাদের ইনপুট এবং আউটপুট সারফেস, captureStateCallback, এবং হ্যান্ডলারের জন্য null পাস করে একটি captureSession শুরু করি।
এই captureStateCallback কী? আপনি যদি আর্টিকেলের শুরুতে থাকা ডায়াগ্রাম মনে করেন, তবে এটি CameraCaptureSession-এর অংশ যেটি আমরা শুরু করছি। এই অবজেক্ট captureRequest-এর প্রগ্রেস ট্র্যাক করে নিচের কলব্যাক দিয়ে:
- onConfigured
- onConfigureFailed
private val captureStateCallback = object : CameraCaptureSession.StateCallback() { override fun onConfigureFailed(session: CameraCaptureSession) { } override fun onConfigured(session: CameraCaptureSession) { } }
যখন cameraCaptureSession সফলভাবে কনফিগার হয়, আমরা সেশনটির জন্য একটি রিপিটিং রিকোয়েস্ট সেট করি যাতে প্রিভিউ liên tục দেখাতে পারি।
সেটা করতে, কলব্যাক থেকে পাওয়া session অবজেক্ট ব্যবহার করি:
session.setRepeatingRequest(captureRequestBuilder.build(), null, backgroundHandler)
আপনি আমাদের পূর্বে তৈরি captureRequestBuilder অবজেক্টটি এই মেথডের প্রথম আর্গুমেন্ট হিসেবে দেখতে পাচ্ছেন। build মেথড এক্সিকিউট করে ফাইনাল প্যারামিটার CaptureRequest হিসেবে পাস করা হয়।
দ্বিতীয় আর্গুমেন্ট হলো CameraCaptureSession.CaptureCallback লিসেনার, কিন্তু যেহেতু প্রিভিউতে ক্যাপচার করা ইমেজের সাথে কিছু করতে চাই না, তাই null পাস করি।
তৃতীয় আর্গুমেন্ট হলো হ্যান্ডলার, এবং এখানে আমাদের নিজস্ব backgroundHandler ব্যবহার করি। এই কারণেই পূর্বের সেকশনে আমরা null পাস করেছিলাম, কারণ রিপিটিং রিকোয়েস্ট ব্যাকগ্রাউন্ড থ্রেডে চলবে।
ছবি তোলার পদ্ধতি
ক্যামেরার লাইভ প্রিভিউ থাকা চমৎকার, কিন্তু অধিকাংশ ইউজার সেটির সাথে কিছু করতে চাবে। ছবি তোলার জন্য আমরা যে লজিক লিখব সেটা প্রিভিউ সেকশনে যা করলাম তার সাথে মিলে যাবে।
- আমরা একটি captureRequest তৈরি করব
- আমরা একটি ImageReader এবং এর লিসেনার ব্যবহার করে তোলা ছবি সংগ্রহ করব
- আমাদের cameraCaptureSession ব্যবহার করে capture মেথড আহ্বান করব
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)
এই ImageReader কী? এক ImageReader একটি সারফেসে রেন্ডার হওয়া ইমেজ ডেটায় অ্যাক্সেস প্রদান করে। আমাদের কেসে, এটি TextureView-এর সারফেস।
আপনি যদি পূর্বের সেকশনের কোড স্নিপেটে তাকান, তো দেখবেন আমরা ইতিমধ্যে একটি 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 (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 ইনস্ট্যানশিয়েট করছি ওইডথ এবং হাইট, আমরা যে ইমেজ ফরম্যাট চাই, এবং এটি কতগুলো ইমেজ ক্যাপচার করতে পারবে সেটি পাস করে।
ImageReader ক্লাসের একটি প্রপার্টি হলো onImageAvailableListener নামে একটি লিসেনার। এই লিসেনার ট্রিগার হবে একবার ছবি তোলা হলে (যেহেতু আমরা এর সারফেসকে আমাদের ক্যাপচার রিকোয়েস্টের আউটপুট সোর্স হিসেবে পাস করেছি)।
val onImageAvailableListener = object: ImageReader.OnImageAvailableListener{ override fun onImageAvailable(reader: ImageReader) { val image: Image = reader.acquireLatestImage() } }
⚠️ প্রসেসিং করার পর ইমেজটি ক্লোজ করতে ভুলবেন না, অন্যথায় আপনি আবার ছবি তোলা পারবেন না।
ভিডিও রেকর্ড করার পদ্ধতি
ভিডিও রেকর্ড করতে, আমাদের MediaRecorder নামে একটি নতুন অবজেক্টের সাথে ইন্টারঅ্যাক্ট করতে হবে। Media recorder অবজেক্টটি অডিও এবং ভিডিও রেকর্ড করার দায়িত্বে থাকে এবং আমরা সেটি ব্যবহার করব।
কিছু করার আগে, আমাদের media recorder সেটআপ করতে হবে। 다양한 কনফিগারেশন আছে এবং সেগুলো অবশ্যই সঠিক ক্রমে হতে হবে নয়তো এক্সেপশন থ্রো হবে।
নিচে একটি উদাহরণ দেওয়া হলো যা আমাদের ভিডিও ক্যাপচার করতে দেবে (অডিও ছাড়া)।
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 কল করতে হবে।
নোট করুন যে mediaRecorder-র একটি start মেথডও আছে এবং prepare কল করার পরেই সেটি কল করতে হবে।
আমাদের mediaRecorder সেটআপ করার পর, আমাদের একটি capture request এবং capture session তৈরি করতে হবে।
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) }
প্রিভিউ বা স্টিল ফটো সেটআপ করার মতই, আমাদের ইনপুট এবং আউটপুট সারফেস ডিফাইন করতে হবে।
এখানে আমরা TextureView-এর surfaceTexture থেকে একটি Surface অবজেক্ট তৈরি করছি এবং media recorder-এর সারফেসও নিচ্ছি। Capture request তৈরি করার সময় TEMPLATE_RECORD ভ্যালু পাস করছি।
আমাদের captureStateVideoCallback আগের টাইপের মতোই, কিন্তু onConfigured কলব্যাকে media recorder-এর start মেথড কল করছি।
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()
উপসংহার
এটা প্রসেস করতে অনেক কিছু ছিল। তাই আপনি যদি এখানে পৌঁছাতে পারেন, অভিনন্দন! এটা করার अलাদা উপায় নেই – কোডের সাথে হাত গোড়া না করলে আপনি বুঝতে পারবেন না সব কিছু কিভাবে একসাথে কাজ করে।
আপনাকে এই আর্টিকেলে ফিচার করা সকল কোড নিচে দেখতে প্রস্তাবিত:
MediumArticles/Camrea2API at master · TomerPacific/MediumArticles
A repository that contains code associated with various Medium articles I have written - MediumArticles/Camrea2API at master · TomerPacific/MediumArticles
TomerPacific
GitHub
মনে রাখবেন যে Camera2 API-র বিষয়ে এটা শুধু আইসবার্গের টিপ। আরও অনেক কিছু করা যায়, যেমন স্লো মোশন ভিডিও ক্যাপচার, ফ্রন্ট এবং ব্যাক ক্যামেরার মধ্যে সুইচ করা, ফোকাস কন্ট্রোল করা, এবং অনেক কিছু।