Chúng ta đều dùng camera điện thoại rất nhiều. Có ứng dụng tích hợp camera như tính năng.
Một bên là cách tiêu chuẩn tương tác camera. Bên kia là cách tùy chỉnh tương tác. Sự khác biệt này quan trọng. Đó là nơi Camera2 xuất hiện.
Camera2 Là Gì?
Dù có từ API level 21, Camera2 API là một trong những kiến trúc phức tạp nhất developer phải đối mặt.
API này và tiền nhiệm giúp developer khai thác sức mạnh tương tác camera trong ứng dụng.
Tương tự cách tương tác microphone hoặc âm lượng thiết bị, Camera2 API cung cấp công cụ tương tác camera thiết bị.
Nói chung, nếu muốn dùng Camera2 API, thường không chỉ chụp ảnh/quay video đơn thuần. Vì API cho kiểm soát sâu camera bằng cách expose các class cần cấu hình cho từng thiết bị cụ thể.
Dù đã làm việc với camera trước, sự thay đổi lớn từ camera API cũ khiến bạn gần như quên tất cả đã biết.
Nhiều tài nguyên hướng dẫn dùng API trực tiếp, nhưng một số lỗi thời, một số không đầy đủ.
Thay vì tự lấp đầy phần thiếu, bài này hy vọng là "one-stop shop" cho Camera2 API.
Các Trường Hợp Dùng Camera2
Trước khi đi sâu, quan trọng: nếu chỉ muốn chụp ảnh/quay video, không cần Camera2 API.
Lý do chính dùng Camera2 là khi ứng dụng cần tương tác tùy chỉnh với camera hoặc chức năng của nó.
Nếu quan tâm chụp ảnh/quay video đơn giản, xem tài liệu Google:
- Take Photos
- Capture Video
Ở đó có đầy đủ bước chụp ảnh/quay video tốt. Bài này tập trung vào Camera2.
Cần thêm vào manifest:
Quyền camera:
<uses-permission android:name="android.permission.CAMERA" />Tính năng camera:
<uses-feature android:name="android.hardware.camera" />Cần xử lý kiểm tra quyền camera đã cấp chưa, nhưng chủ đề này đã được bao rộng, bài này không đề cập.
Thiết Lập Các Thành Phần Camera2 API
Camera2 API giới thiệu nhiều interface và class mới. Phân tích từng cái để hiểu cách dùng.

Bắt đầu với TextureView.
Camera2 TextureView Component
TextureView là UI component hiển thị luồng nội dung (video). Cần TextureView hiển thị feed từ camera, dù preview hay trước khi chụp ảnh/video.
2 thuộc tính quan trọng:
- Trường SurfaceTexture
- Interface SurfaceTextureListener
Thứ nhất là nơi nội dung hiển thị, thứ hai có 4 callback:
- 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) { } }Callback đầu tiên quan trọng khi dùng camera. Cần biết khi SurfaceTexture available để bắt đầu hiển thị feed.
Lưu ý: chỉ khi TextureView gắn vào window thì mới available.
Tương tác camera đã thay đổi từ API cũ. Giờ có CameraManager - system service tương tác với CameraDevice objects.
Các method quan trọng:
- openCamera
- getCameraCharacteristics
- getCameraIdList
Sau khi biết TextureView ready, gọi openCamera mở kết nối camera. Method nhận 3 tham số:
- CameraId - String
- CameraDevice.StateCallback
- Handler
CameraId chỉ camera muốn kết nối. Trên điện thoại có 2 camera chính: trước và sau. Mỗi cái có id riêng, thường là "0" hoặc "1".
Lấy camera id? Dùng getCameraIdList của CameraManager. Trả về mảng string các camera id.
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) // Nếu muốn chọn camera sau thay vì trước 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 }2 tham số tiếp theo là callback trạng thái camera sau khi mở. Chỉ có vài kết quả:
- Camera mở thành công
- Camera disconnect
- Có lỗi xảy ra
Đó là nội dung 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") } }Tham số 3 xử lý nơi công việc diễn ra. Không muốn chiếm main thread, nên làm background. Cần truyền Handler. Nên khởi tạo handler instance với thread tự chọn để delegate công việc.
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() }Với mọi thứ đã làm, giờ gọi openCamera:
cameraManager.openCamera(cameraId, cameraStateCallback,backgroundHandler)Trong callback onOpened, bắt đầu logic hiển thị camera feed lên TextureView.

Hiển Thị Preview Feed
Đã có camera (cameraDevice) và TextureView hiển thị feed. Cần kết nối để show preview.
Dùng thuộc tính SurfaceTexture của TextureView và xây dựng 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) //6Code trên: lấy surfaceTexture từ TextureView. Dùng cameraCharacteristics lấy danh sách output sizes. Set size cho surfaceTexture.
Tạo captureRequest với TEMPLATE_PREVIEW. Thêm input surface vào captureRequest.
Cuối, bắt đầu captureSession với input/output surfaces, captureStateCallback, null cho handler.
captureStateCallback là gì? Nhớ diagram đầu bài, nó là phần của CameraCaptureSession đang bắt đầu. Object theo dõi tiến độ captureRequest với callback:
- onConfigured
- onConfigureFailed
private val captureStateCallback = object : CameraCaptureSession.StateCallback() { override fun onConfigureFailed(session: CameraCaptureSession) { } override fun onConfigured(session: CameraCaptureSession) { } }Khi cameraCaptureSession cấu hình thành công, set repeating request cho session để show preview liên tục.
Dùng session object từ callback:
session.setRepeatingRequest(captureRequestBuilder.build(), null, backgroundHandler)Nhận ra captureRequestBuilder tạo trước là tham số đầu. Gọi build() để tham số cuối là CaptureRequest.
Tham số 2 là CameraCaptureSession.captureCallback listener, nhưng không muốn xử lý ảnh captured (vì là preview), truyền null.
Tham số 3 là handler, dùng backgroundHandler. Đây cũng là lý do truyền null ở phần trước, vì repeating request chạy trên background thread.

Chụp Ảnh
Preview trực tiếp tuyệt vời, nhưng user muốn làm gì đó với nó. Logic chụp ảnh tương tự phần trước.
- Tạo captureRequest
- Dùng ImageReader và listener thu thập ảnh chụp
- Dùng cameraCaptureSession, gọi capture method
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 là gì? Cung cấp truy cập dữ liệu ảnh render lên surface. Trong trường hợp này, surface của TextureView.
Nhìn code snippet phần trước, đã định nghĩa 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 }Nhìn trên, khởi tạo ImageReader truyền width, height, format ảnh, số ảnh có thể capture.
ImageReader có listener onImageAvailableListener. Trigger khi chụp ảnh (vì truyền surface của nó làm output source cho capture request).
val onImageAvailableListener = object: ImageReader.OnImageAvailableListener{ override fun onImageAvailable(reader: ImageReader) { val image: Image = reader.acquireLatestImage() } }⚠️ Đảm bảo close image sau khi xử lý, không thì không chụp ảnh tiếp được.

Quay Video
Để quay video, tương tác với MediaRecorder - object quản lý ghi âm/video.
Trước tiên setup MediaRecorder. Có nhiều config phải đúng thứ tự hoặc sẽ throw exception.
Ví dụ config cho quay video (không audio):
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() }Chú ý setOutputFile mong đợi path file lưu video. Cuối cùng gọi prepare.
Lưu ý mediaRecorder có method start, phải gọi prepare trước start.
Sau setup mediaRecorder, tạo capture request và 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) }Tương tự setup preview/chụp ảnh, định nghĩa input/output surfaces.
Tạo Surface từ surfaceTexture của TextureView và surface từ mediaRecorder. Truyền TEMPLATE_RECORD khi tạo capture request.
captureStateVideoCallback cùng loại dùng cho still photo, nhưng trong onConfigured gọi mediaRecorder.start().
val captureStateVideoCallback = object : CameraCaptureSession.StateCallback() { override fun onConfigureFailed(session: CameraCaptureSession) { } override fun onConfigured(session: CameraCaptureSession) { session.setRepeatingRequest(captureRequestBuilder.build(), null, backgroundHandler) mediaRecorder.start() } }Đang quay video, làm sao dừng? Dùng stop và reset trên mediaRecorder:
mediaRecorder.stop() mediaRecorder.reset()Kết Luận
Đã nhiều thứ để xử lý. Nếu đọc đến đây, chúc mừng! Không có cách nào khác - chỉ khi "đổ mồ hôi" với code mới hiểu mọi thứ kết nối như thế nào.
Khuyến khích xem toàn bộ code trong bài tại:
MediumArticles/Camrea2API at master · TomerPacific/MediumArticles
Repository chứa code liên quan các bài Medium - MediumArticles/Camrea2API at master · TomerPacific/MediumArticles


Nhớ rằng đây chỉ là đỉnh núi băng của Camera2 API. Còn nhiều việc khác: quay slow motion, chuyển camera trước/sau, kiểm soát focus, v.v.