Computer >> บทช่วยสอนคอมพิวเตอร์ >  >> ระบบ >> Android

เชี่ยวชาญ Android Camera2 API: คู่มือทีละขั้นตอนถ่ายภาพและบันทึกวิดีโอ

เราทุกคนใช้กล้องโทรศัพท์บ่อยมาก มีแอปพลิเคชันบางตัวรวมกล้องเป็นฟีเจอร์ด้วย

มีวิธีมาตรฐานในการโต้ตอบกับกล้อง และมีวิธีปรับแต่งการโต้ตอบ การแยกแยะนี้สำคัญ และที่นี่ Camera2 เข้ามามีบทบาท

Camera2 คืออะไร?

พร้อมใช้งานตั้งแต่ API level 21 Camera2 API เป็นสถาปัตยกรรมที่ซับซ้อนที่สุดที่นักพัฒนาต้องจัดการ

API นี้และตัวก่อนหน้าวางไว้เพื่อให้นักพัฒนาสามารถใช้พลังของกล้องภายในแอปได้

คล้ายกับการโต้ตอบกับไมโครโฟนหรือระดับเสียง Camera2 API ให้เครื่องมือควบคุมกล้องอุปกรณ์

โดยทั่วไป หากต้องการแค่ถ่ายรูปหรือบันทึกวิดีโอ ไม่จำเป็นต้องใช้ Camera2 API เพราะ API นี้ให้การควบคุมลึกโดยเปิดเผยคลาสต่างๆ ที่ต้องตั้งค่าต่ออุปกรณ์เฉพาะ

แม้คุณเคยจัดการกับกล้องมาก่อน Camera2 API เปลี่ยนแปลงมากจนควรลืมความรู้เก่าไปเลย

มีทรัพยากรมากมายพยายามสอน Camera2 API แต่บางอย่างล้าสมัยหรือไม่ครบถ้วน บทความนี้หวังจะเป็นที่หยุดเดียวสำหรับ Camera2 API

กรณีการใช้งาน Camera2

ก่อนดำเนินการ ต้องเข้าใจว่า หากต้องการแค่ถ่ายรูปหรือบันทึกวิดีโอ ไม่ต้อง ใช้ Camera2 API

เหตุผลหลักใช้ Camera2 API คือ แอปต้องการการโต้ตอบแบบกำหนดเองกับกล้องหรือฟังก์ชันของมัน

หากสนใจแค่ถ่ายรูป/วิดีโอ แนะนำให้อ่านเอกสารของ Google:

  1. Take Photos
  2. Capture Video

บทความนี้เน้น Camera2

เพิ่มใน Manifest:

Camera permissions:

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

Camera feature:

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

ต้องตรวจสอบ permission แต่เนื่องจากครอบคลุมกันอย่างกว้างขวาง จะไม่กล่าวถึงในบทความนี้

ตั้งค่าคอมโพเนนต์ Camera2 API

Camera2 API นำเสนอ interface และ class ใหม่ๆ มาแบ่งแยกแต่ละอย่าง

เชี่ยวชาญ Android Camera2 API: คู่มือทีละขั้นตอนถ่ายภาพและบันทึกวิดีโอ
ดูคอมโพเนนต์ทั้งหมด

เริ่มจาก TextureView

คอมโพเนนต์ Camera2 TextureView

TextureView เป็น UI component แสดง content stream (เช่นวิดีโอ) เราใช้ TextureView แสดง feed จากกล้อง ไม่ว่าจะเป็น preview หรือก่อนถ่ายรูป/วิดีโอ

สอง property สำคัญ:

  • SurfaceTexture field
  • SurfaceTextureListener interface

แรกคือที่ content จะแสดง สองมี callback 4 ตัว:

  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 แรกสำคัญที่สุด เพราะเราต้องรู้เมื่อ SurfaceTexture พร้อม เพื่อเริ่มแสดง feed

จำไว้ว่า TextureView ต้อง attach กับ window ก่อนจึงจะพร้อม

การโต้ตอบกับกล้องเปลี่ยนไป ตอนนี้มี CameraManager system service ให้โต้ตอบกับ CameraDevice objects

Method สำคัญ:

  • openCamera
  • getCameraCharacteristics
  • getCameraIdList

หลังทราบว่า TextureView พร้อม เรียก openCamera เพื่อเปิดการเชื่อมต่อกล้อง รับ 3 arguments:

  1. CameraId - String
  2. CameraDevice.StateCallback
  3. A Handler

CameraId ระบุกล้องที่ต้องการเชื่อมต่อ โทรศัพท์มีกล้องหลัก 2 ตัว (หน้า/หลัง) แต่ละตัวมี id เฉพาะ (มักเป็น 0 หรือ 1)

ได้ camera id จาก CameraManager.getCameraIdList() คืนค่า array string ของ camera ids ทั้งหมด

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 }

Argument ถัดไปคือ callbacks สถานะกล้องหลังพยายามเปิด ผลลัพธ์เป็นได้แค่:

  • กล้องเปิดสำเร็จ
  • กล้องขาดการเชื่อมต่อ
  • เกิดข้อผิดพลาด

นี่คือ 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") } }

Argument ที่ 3 จัดการ thread เนื่องจากไม่ต้องการ block main thread ใช้ background thread ผ่าน 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)

ใน callback onOpened เริ่มจัดการ logic แสดง feed บน TextureView

เชี่ยวชาญ Android Camera2 API: คู่มือทีละขั้นตอนถ่ายภาพและบันทึกวิดีโอ
Photo by Markus Spiske on Unsplash

แสดง Preview ของ Feed

เรามี cameraDevice และ TextureView แล้ว ต้องเชื่อมกันเพื่อแสดง preview ใช้ SurfaceTexture ของ TextureView และสร้าง 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
สร้าง Preview

โค้ดด้านบน: ดึง surfaceTexture จาก TextureView ใช้ cameraCharacteristics หา output sizes ตั้งค่า surfaceTexture สร้าง captureRequest ด้วย TEMPLATE_PREVIEW เพิ่ม input surface เริ่ม captureSession ด้วย input/output surfaces, captureStateCallback, null สำหรับ handler

captureStateCallback คืออะไร? เป็นส่วนของ CameraCaptureSession ที่เราเริ่มต้น ติดตามความคืบหน้าของ captureRequest ด้วย callbacks:

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

เมื่อ cameraCaptureSession configured สำเร็จ ตั้ง repeating request เพื่อแสดง preview ต่อเนื่อง

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

captureRequestBuilder ที่สร้างไว้ก่อนหน้าเป็น argument แรก ใช้ build() เพื่อสร้าง CaptureRequest argument ที่ 2 คือ CameraCaptureSession.captureCallback listener แต่ไม่ต้องทำอะไรกับภาพถ่าย (เพราะเป็น preview) ส่ง null argument ที่ 3 คือ handler ใช้ backgroundHandler ของเรา (เหตุนี้จึงส่ง null ใน createCaptureSession)

เชี่ยวชาญ Android Camera2 API: คู่มือทีละขั้นตอนถ่ายภาพและบันทึกวิดีโอ
Photo by Dicky Jiang on Unsplash

ถ่ายภาพ

มี live preview แล้ว แต่ผู้ใช้ต้องการทำอะไรกับมัน Logic ถ่ายรูปคล้ายส่วน preview

  1. สร้าง captureRequest
  2. ใช้ ImageReader และ listener รวบรวมภาพที่ถ่าย
  3. ใช้ cameraCaptureSession เรียก 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)
คราวนี้สร้าง capture request ด้วย TEMPLATE_STILL_CAPTURE

ImageReader คืออะไร? ให้เข้าถึงข้อมูลภาพที่ render บน surface ในกรณีนี้คือ surface ของ TextureView

ใน snippet ส่วน preview เราได้ define 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 }
สังเกตบรรทัด 12 – 14

สร้าง ImageReader ด้วย width, height, image format, และจำนวนภาพที่จับได้

ImageReader มี listener onImageAvailableListener ทริกเกอร์เมื่อถ่ายรูป (เพราะส่ง surface ของมันเป็น output source)

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

⚠️ ต้องปิด image หลังประมวลผล มิฉะนั้นจะถ่ายรูปต่อไม่ได้

เชี่ยวชาญ Android Camera2 API: คู่มือทีละขั้นตอนถ่ายภาพและบันทึกวิดีโอ
Photo by Jakob Owens on Unsplash

บันทึกวิดีโอ

บันทึกวิดีโอต้องใช้ MediaRecorder object ที่จัดการบันทึกเสียงและวิดีโอ

ตั้งค่า MediaRecorder ก่อน มี configurations มาก และ ต้องเรียงลำดับถูกต้อง มิฉะนั้นจะโยน exception

ตัวอย่าง config สำหรับบันทึกวิดีโอ (ไม่มีเสียง):

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() }
ลำดับของ setters สำคัญ

สังเกต setOutputFile ต้องการ path ไฟล์เก็บวิดีโอ หลังตั้งค่าทั้งหมดต้องเรียก prepare

mediaRecorder มี start method ต้อง prepare ก่อนถึงจะ start ได้

หลัง setup 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) }

คล้ายการตั้งค่า preview หรือถ่ายรูป ต้องกำหนด input/output surfaces สร้าง Surface จาก surfaceTexture และ surface จาก mediaRecorder ใช้ TEMPLATE_RECORD สร้าง capture request

captureStateVideoCallback เหมือน captureStateCallback แต่ใน onConfigured เรียก 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() } }
ตั้ง repeating request เพราะต้องการบันทึกวิดีโอต่อเนื่อง

หยุดบันทึกอย่างไร? ใช้ stop และ reset ของ mediaRecorder:

mediaRecorder.stop() mediaRecorder.reset()

สรุป

เนื้อหาเยอะมาก หากคุณอ่านจนจบ ยินดีด้วย! ไม่มีทางลัด ต้องลงมือเขียนโค้ดจริงถึงจะเข้าใจว่าทุกอย่างเชื่อมต่อกันอย่างไร

โปรดดูโค้ดทั้งหมดในบทความนี้:

MediumArticles/Camrea2API at master · TomerPacific/MediumArticlesA repository that contains code associated with various Medium articles I have written - MediumArticles/Camrea2API at master · TomerPacific/MediumArticlesเชี่ยวชาญ Android Camera2 API: คู่มือทีละขั้นตอนถ่ายภาพและบันทึกวิดีโอTomerPacificGitHubเชี่ยวชาญ Android Camera2 API: คู่มือทีละขั้นตอนถ่ายภาพและบันทึกวิดีโอ

จำไว้ว่านี่แค่ปลายคุณของ Camera2 API ยังมีอะไรทำได้อีกมาก เช่น บันทึกวิดีโอ slow motion, สลับกล้องหน้า/หลัง, ควบโฟกัส ฯลฯ