เราทุกคนใช้กล้องในโทรศัพท์ของเราและเราใช้มันอย่างดีที่สุด มีแม้กระทั่งแอปพลิเคชั่นบางตัวที่รวมกล้องไว้เป็นคุณสมบัติ
ด้านหนึ่งมีวิธีมาตรฐานในการโต้ตอบกับกล้อง ในทางกลับกัน มีวิธีปรับแต่งการโต้ตอบกับกล้องของคุณ ความแตกต่างนี้เป็นสิ่งสำคัญที่จะทำให้ และนั่นคือที่มาของ Camera2
Camera2 คืออะไร
แม้ว่าจะมีให้บริการตั้งแต่ API ระดับ 21 แต่ Camera2 API จะต้องเป็นหนึ่งในส่วนที่ซับซ้อนมากขึ้นของนักพัฒนาสถาปัตยกรรมที่ต้องรับมือ
API นี้และรุ่นก่อนถูกนำมาใช้เพื่อให้นักพัฒนาสามารถควบคุมพลังของการโต้ตอบกับกล้องภายในแอปพลิเคชันของตนได้
คล้ายกับวิธีการโต้ตอบกับไมโครโฟนหรือระดับเสียงของอุปกรณ์ Camera2 API ให้เครื่องมือในการโต้ตอบกับกล้องของอุปกรณ์
โดยทั่วไป หากคุณต้องการใช้ Camera2 API อาจเป็นมากกว่าการถ่ายภาพหรือบันทึกวิดีโอ ทั้งนี้เนื่องจาก API ช่วยให้คุณมีการควบคุมเชิงลึกของกล้องโดยเปิดเผยคลาสต่างๆ ที่จะต้องกำหนดค่าสำหรับแต่ละอุปกรณ์
แม้ว่าคุณจะเคยใช้งานกล้องมาก่อนแล้วก็ตาม การเปลี่ยนแปลงครั้งใหญ่จาก API ของกล้องแบบเดิมคือการเปลี่ยนแปลงครั้งใหญ่ ที่คุณอาจลืมทุกสิ่งที่คุณรู้ได้เช่นกัน
มีแหล่งข้อมูลมากมายที่พยายามแสดงวิธีใช้ API นี้โดยตรง แต่บางส่วนอาจล้าสมัยและบางส่วนไม่ได้นำเสนอภาพรวม
ดังนั้น แทนที่จะพยายามกรอกส่วนที่หายไปด้วยตัวเอง บทความนี้ (หวังว่า) จะเป็นแหล่งรวมของคุณสำหรับการโต้ตอบกับ Camera2 API
เคสการใช้งาน Camera2
ก่อนที่เราจะลงลึกในสิ่งใดๆ สิ่งสำคัญคือต้องเข้าใจว่า หากคุณต้องการใช้กล้องเพื่อถ่ายภาพหรือบันทึกวิดีโอเท่านั้น คุณไม่จำเป็นต้องกังวลกับ Camera2 API
เหตุผลหลักในการใช้ Camera2 API คือหากแอปพลิเคชันของคุณต้องการการโต้ตอบแบบกำหนดเองกับกล้องหรือฟังก์ชันการทำงานของกล้อง
หากคุณสนใจที่จะทำอย่างแรกแทนที่จะเป็นอย่างหลัง เราขอแนะนำให้คุณไปที่เอกสารประกอบต่อไปนี้จาก Google:
- ถ่ายรูป
- จับภาพวิดีโอ
คุณจะพบขั้นตอนที่จำเป็นทั้งหมดเพื่อถ่ายภาพและวิดีโอที่ยอดเยี่ยมด้วยกล้องของคุณ แต่ในบทความนี้จะเน้นไปที่วิธีใช้ Camera2 เป็นหลัก
ตอนนี้ มีบางสิ่งที่เราต้องเพิ่มลงในไฟล์ Manifest:
สิทธิ์ของกล้อง:
<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
- ปรับปรุงพื้นผิวพื้นผิว
- พื้นผิวที่ถูกทำลาย
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
วิธีการที่คุณต้องการให้ความสนใจเป็นพิเศษคือ:
- กล้องเปิด
- getCameraCharacteristics
- getCameraIdList
หลังจากที่เรารู้ว่า TextureView พร้อมใช้งานและพร้อมแล้ว เราจำเป็นต้องเรียก openCamera เพื่อเปิดการเชื่อมต่อกับกล้อง วิธีนี้ใช้สามอาร์กิวเมนต์:
- CameraId - สตริง
- CameraDevice.StateCallback
- ตัวจัดการ
อาร์กิวเมนต์ CameraId หมายถึงกล้องตัวใดที่เราต้องการเชื่อมต่อ ในโทรศัพท์ของคุณ ส่วนใหญ่จะมีกล้องสองตัวคือด้านหน้าและด้านหลัง แต่ละคนมีรหัสเฉพาะของตัวเอง โดยปกติแล้วจะเป็นศูนย์หรือหนึ่ง
เราจะรับ ID กล้องได้อย่างไร? เราใช้วิธี getCamerasIdList ของ CameraManager มันจะส่งคืนอาร์เรย์ของประเภทสตริงของรหัสกล้องทั้งหมดที่ระบุจากอุปกรณ์
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
}
อาร์กิวเมนต์ต่อไปคือการเรียกกลับไปยังสถานะของกล้องหลังจากที่เราพยายามเปิดมัน หากคุณลองคิดดู การดำเนินการนี้จะมีผลลัพธ์ได้หลายอย่างเท่านั้น:
- เปิดกล้องได้สำเร็จ
- กล้องหลุด
- เกิดข้อผิดพลาดบางอย่าง
และนั่นคือสิ่งที่คุณจะพบใน 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")
}
}
อาร์กิวเมนต์ที่สามเกี่ยวข้องกับตำแหน่งที่งานนี้จะเกิดขึ้น เนื่องจากเราไม่ต้องการครอบครองเธรดหลัก การทำงานนี้ในเบื้องหลังจึงดีกว่า
นั่นเป็นเหตุผลที่เราต้องส่ง 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 ของเราเพื่อแสดงฟีด แต่เราจำเป็นต้องเชื่อมต่อเข้าด้วยกันเพื่อให้เราสามารถแสดงตัวอย่างฟีดได้
ในการทำเช่นนั้น เราจะใช้คุณสมบัติ 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 เพื่อรับรายการขนาดเอาต์พุตทั้งหมด เพื่อให้ได้ขนาดที่ต้องการ เราตั้งค่าสำหรับ SurfaceTexture
ต่อไป เราสร้าง captureRequest ที่เราส่งใน TEMPLATE_PREVIEW . เราเพิ่มพื้นผิวอินพุตของเราไปยัง captureRequest
สุดท้าย เราเริ่มการดักจับเซสชันด้วยพื้นผิวอินพุตและเอาต์พุต, captureStateCallback และส่งผ่านค่า null สำหรับตัวจัดการ
แล้ว captureStateCallback นี้คืออะไร? หากคุณจำไดอะแกรมตั้งแต่ต้นบทความนี้ แสดงว่าเป็นส่วนหนึ่งของ CameraCaptureSession ที่เรากำลังจะเริ่มต้น ออบเจ็กต์นี้ติดตามความคืบหน้าของ captureRequest ด้วยการเรียกกลับดังต่อไปนี้:
- onConfigured
- onConfigureFailed
private val captureStateCallback = object : CameraCaptureSession.StateCallback() {
override fun onConfigureFailed(session: CameraCaptureSession) {
}
override fun onConfigured(session: CameraCaptureSession) {
}
}
เมื่อ cameraCaptureSession ได้รับการกำหนดค่าเรียบร้อยแล้ว เราตั้งค่าคำขอซ้ำสำหรับเซสชันเพื่อให้เราสามารถแสดงตัวอย่างได้อย่างต่อเนื่อง
ในการทำเช่นนั้น เราใช้วัตถุเซสชันที่เราได้รับในการเรียกกลับ:
session.setRepeatingRequest(captureRequestBuilder.build(), null, backgroundHandler)
คุณจะรู้จักวัตถุ captureRequestBuilder ที่เราสร้างไว้ก่อนหน้านี้ว่าเป็นอาร์กิวเมนต์แรกสำหรับวิธีนี้ เรากำหนดวิธีการสร้างเพื่อให้พารามิเตอร์สุดท้ายที่ส่งผ่านเข้ามาคือ CaptureRequest
อาร์กิวเมนต์ที่สองคือ Listener CameraCaptureSession.captureCallback แต่เนื่องจากเราไม่ต้องการทำอะไรกับภาพที่ถ่าย (เนื่องจากนี่เป็นการแสดงตัวอย่าง) เราจึงส่งผ่านเป็นโมฆะ
อาร์กิวเมนต์ที่สามคือตัวจัดการ และที่นี่เราใช้ backgroundHandler ของเราเอง นี่คือสาเหตุที่เราส่งค่า null ในส่วนก่อนหน้า เนื่องจากคำขอซ้ำจะทำงานบนเธรดพื้นหลัง
วิธีถ่ายภาพ
การแสดงตัวอย่างกล้องแบบสดนั้นยอดเยี่ยม แต่ผู้ใช้ส่วนใหญ่อาจต้องการทำอะไรกับมัน ตรรกะบางอย่างที่เราจะเขียนเพื่อถ่ายภาพจะคล้ายกับที่เราทำในส่วนที่แล้ว
- เราจะสร้าง captureRequest
- เราจะใช้ ImageReader และผู้ฟังเพื่อรวบรวมภาพที่ถ่าย
- การใช้ cameraCaptureSession ของเรา เราจะเรียกใช้วิธีการจับภาพ
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 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 โดยส่งผ่านความกว้างและความสูง รูปแบบรูปภาพที่เราต้องการให้รูปภาพของเราอยู่ และจำนวนรูปภาพที่สามารถจับภาพได้
คุณสมบัติที่คลาส ImageReader มีคือฟังที่เรียกว่า onImageAvailableListener ผู้ฟังรายนี้จะถูกทริกเกอร์เมื่อมีการถ่ายภาพ (เนื่องจากเราผ่านพื้นผิวเป็นแหล่งเอาต์พุตสำหรับคำขอจับภาพของเรา)
val onImageAvailableListener = object: ImageReader.OnImageAvailableListener{
override fun onImageAvailable(reader: ImageReader) {
val image: Image = reader.acquireLatestImage()
}
}
⚠️ อย่าลืมปิดรูปภาพหลังจากประมวลผลแล้ว มิฉะนั้น คุณจะไม่สามารถถ่ายภาพอื่นได้อีก
วิธีการบันทึกวิดีโอ
ในการบันทึกวิดีโอ เราต้องโต้ตอบกับวัตถุใหม่ที่เรียกว่า MediaRecorder วัตถุตัวบันทึกสื่อมีหน้าที่ในการบันทึกเสียงและวิดีโอ และเราจะใช้มันทำอย่างนั้น
ก่อนที่เราจะทำอะไร เราต้องตั้งค่าเครื่องบันทึกสื่อก่อน มีการกำหนดค่าต่างๆ ให้จัดการและต้องอยู่ในลำดับที่ถูกต้อง มิฉะนั้นจะมีข้อยกเว้น .
ด้านล่างนี้คือตัวอย่างการกำหนดค่าต่างๆ ที่จะช่วยให้เราสามารถจับภาพวิดีโอ (ไม่มีเสียง)
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 วิธีตามที่คาดไว้เส้นทางไปยังไฟล์ที่จะเก็บวิดีโอของเรา เมื่อสิ้นสุดการตั้งค่าการกำหนดค่าทั้งหมดเหล่านี้ เราจำเป็นต้องเรียกการจัดเตรียม
โปรดทราบว่า mediaRecorder มีเมธอด start ด้วยเช่นกัน และเราต้องเรียก prepare ก่อนเรียกใช้
หลังจากตั้งค่า mediaRecoder แล้ว เราจำเป็นต้องสร้างคำขอจับภาพและเซสชันการจับภาพ
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)
}
เช่นเดียวกับการตั้งค่าการแสดงตัวอย่างหรือการถ่ายภาพ เราต้องกำหนดพื้นผิวอินพุตและเอาต์พุตของเรา
ที่นี่เรากำลังสร้างวัตถุ Surface จากพื้นผิวพื้นผิวของ TextureView และนำพื้นผิวจากเครื่องบันทึกสื่อ เรากำลังผ่านใน TEMPLATE_RECORD ค่าเมื่อสร้างคำขอดักจับ
captureStateVideoCallback ของเราเป็นประเภทเดียวกับที่เราใช้สำหรับภาพนิ่ง แต่ในการโทรกลับ onConfigured เราเรียกวิธีการเริ่มต้นของเครื่องบันทึกสื่อ
val captureStateVideoCallback = object : CameraCaptureSession.StateCallback() {
override fun onConfigureFailed(session: CameraCaptureSession) {
}
override fun onConfigured(session: CameraCaptureSession) {
session.setRepeatingRequest(captureRequestBuilder.build(), null, backgroundHandler)
mediaRecorder.start()
}
}
ตอนนี้เรากำลังบันทึกวิดีโอ แต่เราจะหยุดบันทึกได้อย่างไร? สำหรับสิ่งนั้น เราจะใช้วิธีการหยุดและรีเซ็ตบนวัตถุ mediaRecorder:
mediaRecorder.stop()
mediaRecorder.reset()
บทสรุป
นั่นเป็นจำนวนมากในการประมวลผล ดังนั้นหากคุณทำสำเร็จแล้ว ยินดีด้วย! ไม่มีทางแก้ไขได้ เพียงแค่ทำให้มือสกปรกด้วยโค้ด คุณก็จะเริ่มเข้าใจว่าทุกอย่างเชื่อมต่อกันอย่างไร
คุณได้รับการสนับสนุนมากกว่าที่จะดูโค้ดทั้งหมดที่แสดงในบทความนี้ด้านล่าง :
MediumArticles/Camrea2API at master · TomerPacific/MediumArticlesที่เก็บโค้ดที่เกี่ยวข้องกับบทความ Medium ต่างๆ ที่ฉันเขียน - MediumArticles/Camrea2API at master · TomerPacific/MediumArticles TomerPacificGitHubโปรดทราบว่านี่เป็นเพียงส่วนเล็กสุดของภูเขาน้ำแข็งเมื่อพูดถึง Camera2 API มีหลายสิ่งที่คุณทำได้ เช่น ถ่ายวิดีโอสโลว์โมชั่น สลับระหว่างกล้องหน้าและกล้องหลัง ควบคุมโฟกัส และอื่นๆ อีกมากมาย