เราทุกคนใช้กล้องโทรศัพท์บ่อยมาก มีแอปพลิเคชันบางตัวรวมกล้องเป็นฟีเจอร์ด้วย
มีวิธีมาตรฐานในการโต้ตอบกับกล้อง และมีวิธีปรับแต่งการโต้ตอบ การแยกแยะนี้สำคัญ และที่นี่ 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:
- Take Photos
- 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 ใหม่ๆ มาแบ่งแยกแต่ละอย่าง

เริ่มจาก TextureView
คอมโพเนนต์ Camera2 TextureView
TextureView เป็น UI component แสดง content stream (เช่นวิดีโอ) เราใช้ TextureView แสดง feed จากกล้อง ไม่ว่าจะเป็น preview หรือก่อนถ่ายรูป/วิดีโอ
สอง property สำคัญ:
- SurfaceTexture field
- SurfaceTextureListener interface
แรกคือที่ content จะแสดง สองมี callback 4 ตัว:
- 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 แรกสำคัญที่สุด เพราะเราต้องรู้เมื่อ SurfaceTexture พร้อม เพื่อเริ่มแสดง feed
จำไว้ว่า TextureView ต้อง attach กับ window ก่อนจึงจะพร้อม
การโต้ตอบกับกล้องเปลี่ยนไป ตอนนี้มี CameraManager system service ให้โต้ตอบกับ CameraDevice objects
Method สำคัญ:
- openCamera
- getCameraCharacteristics
- getCameraIdList
หลังทราบว่า TextureView พร้อม เรียก openCamera เพื่อเปิดการเชื่อมต่อกล้อง รับ 3 arguments:
- CameraId - String
- CameraDevice.StateCallback
- 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

แสดง 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โค้ดด้านบน: ดึง 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)

ถ่ายภาพ
มี live preview แล้ว แต่ผู้ใช้ต้องการทำอะไรกับมัน Logic ถ่ายรูปคล้ายส่วน preview
- สร้าง captureRequest
- ใช้ ImageReader และ listener รวบรวมภาพที่ถ่าย
- ใช้ 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)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 }สร้าง 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 หลังประมวลผล มิฉะนั้นจะถ่ายรูปต่อไม่ได้

บันทึกวิดีโอ
บันทึกวิดีโอต้องใช้ 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() }สังเกต 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() } }หยุดบันทึกอย่างไร? ใช้ 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
TomerPacificGitHub
จำไว้ว่านี่แค่ปลายคุณของ Camera2 API ยังมีอะไรทำได้อีกมาก เช่น บันทึกวิดีโอ slow motion, สลับกล้องหน้า/หลัง, ควบโฟกัส ฯลฯ