Computer >> Hướng Dẫn Máy Tính >  >> Hệ Thống >> Android

Làm Chủ Camera2 API Android: Hướng Dẫn Bước Để Bước Chụp Ảnh & Quay Video

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:

  1. Take Photos
  2. 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.

Làm Chủ Camera2 API Android: Hướng Dẫn Bước Để Bước Chụp Ảnh & Quay Video
Nhìn nhiều thành phần vậy

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:

  1. onSurfaceTextureAvailable
  2. onSurfaceTextureSizeChanged
  3. onSurfaceTextureUpdated
  4. 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ố:

  1. CameraId - String
  2. CameraDevice.StateCallback
  3. 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.

Làm Chủ Camera2 API Android: Hướng Dẫn Bước Để Bước Chụp Ảnh & Quay Video
Ảnh bởi Markus Spiske trên Unsplash

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) //6
Tạo preview

Code 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.

Làm Chủ Camera2 API Android: Hướng Dẫn Bước Để Bước Chụp Ảnh & Quay Video
Ảnh bởi Dicky Jiang trên Unsplash

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.

  1. Tạo captureRequest
  2. Dùng ImageReader và listener thu thập ảnh chụp
  3. 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)
Tạo capture request với TEMPLATE_STILL_CAPTURE

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 }
Chú ý dòng 12 – 14

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.

Làm Chủ Camera2 API Android: Hướng Dẫn Bước Để Bước Chụp Ảnh & Quay Video
Ảnh bởi Jakob Owens trên Unsplash

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() }
Thứ tự setter quan trọng

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() } }
Cũng set repeating request vì muốn quay video liên tục

Đ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

Làm Chủ Camera2 API Android: Hướng Dẫn Bước Để Bước Chụp Ảnh & Quay Video
Làm Chủ Camera2 API Android: Hướng Dẫn Bước Để Bước Chụp Ảnh & Quay Video

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.