หากแอปใช้คลาส Camera เดิม ("Camera1") ซึ่งเลิกใช้งานตั้งแต่ Android 5.0 (ระดับ API 21) เราขอแนะนำอย่างยิ่งให้อัปเดตเป็น Android Camera API รุ่นใหม่ Android มี CameraX (API กล้อง Jetpack มาตรฐานที่เชื่อถือได้
) และ Camera2 (API เฟรมเวิร์กระดับต่ำ)
ในกรณีส่วนใหญ่ เราขอแนะนำให้ย้ายข้อมูลแอปไปยัง CameraX โดยมีเหตุผลดังต่อไปนี้
- ใช้งานง่าย: CameraX จัดการรายละเอียดระดับต่ำเพื่อให้คุณ ไม่ต้องมุ่งเน้นที่การสร้างประสบการณ์การใช้งานกล้องตั้งแต่ต้นมากนัก และมุ่งเน้นที่ การสร้างความแตกต่างให้กับแอปของคุณมากขึ้น
- CameraX จัดการการกระจายตัวให้คุณ: CameraX ช่วยลดต้นทุนการบำรุงรักษาระยะยาว และโค้ดเฉพาะอุปกรณ์ ซึ่งจะช่วยมอบประสบการณ์การใช้งานคุณภาพสูง แก่ผู้ใช้ ดูข้อมูลเพิ่มเติมได้ที่บล็อกโพสต์อุปกรณ์เข้ากันได้ดียิ่งขึ้นด้วย CameraX
- ความสามารถขั้นสูง: CameraX ได้รับการออกแบบมาอย่างพิถีพิถันเพื่อให้การผสานรวมฟังก์ชันการทำงานขั้นสูงเข้ากับแอปเป็นเรื่องง่าย ตัวอย่างเช่น คุณสามารถใช้โบเก้ การรีทัชใบหน้า HDR (High Dynamic Range) และโหมดจับภาพกลางคืนที่ช่วยเพิ่มความสว่างในที่แสงน้อยกับรูปภาพได้อย่างง่ายดายด้วย CameraX Extensions
- ความสามารถในการอัปเดต: Android เปิดตัวความสามารถใหม่และการแก้ไขข้อบกพร่องใน CameraX ตลอดทั้งปี การย้ายข้อมูลไปยัง CameraX จะช่วยให้แอปของคุณได้รับเทคโนโลยีกล้อง Android ล่าสุดในแต่ละรุ่นของ CameraX ไม่ใช่แค่ใน การเปิดตัว Android เวอร์ชันประจำปี
ในคู่มือนี้ คุณจะเห็นสถานการณ์ทั่วไปสำหรับการใช้งานกล้อง แต่ละ สถานการณ์ประกอบด้วยการติดตั้งใช้งาน Camera1 และการติดตั้งใช้งาน CameraX เพื่อ เปรียบเทียบ
เมื่อพูดถึงการย้ายข้อมูล บางครั้งคุณอาจต้องมีความยืดหยุ่นเพิ่มเติมเพื่อผสานรวม
กับฐานของโค้ดที่มีอยู่ โค้ด CameraX ทั้งหมดในคู่มือนี้มีการใช้งานCameraController ซึ่งเหมาะอย่างยิ่งหากคุณต้องการวิธีที่ตรงไปตรงมาในการใช้ CameraX และยังมีการใช้งาน CameraProvider ซึ่งเหมาะอย่างยิ่งหากคุณต้องการความยืดหยุ่นมากขึ้น เราจึงขอแนะนำสิทธิประโยชน์ของแต่ละตัวเลือกเพื่อช่วยคุณตัดสินใจเลือกตัวเลือกที่เหมาะสม
CameraController |
CameraProvider |
| ต้องใช้รหัสการตั้งค่าเพียงเล็กน้อย | ช่วยให้ควบคุมได้มากขึ้น |
| การอนุญาตให้ CameraX จัดการกระบวนการตั้งค่าเพิ่มเติมหมายความว่า ฟังก์ชันต่างๆ เช่น แตะเพื่อโฟกัสและบีบนิ้วเพื่อซูมจะทำงานโดยอัตโนมัติ |
เนื่องจากนักพัฒนาแอปเป็นผู้จัดการการตั้งค่า จึงมีโอกาสมากขึ้น
ในการปรับแต่งการกำหนดค่า เช่น การเปิดใช้การหมุนภาพเอาต์พุต
หรือการตั้งค่ารูปแบบภาพเอาต์พุตใน ImageAnalysis
|
การกำหนดให้มี PreviewView สำหรับตัวอย่างกล้องช่วยให้
CameraX สามารถผสานรวมแบบครบวงจรได้อย่างราบรื่น เช่น ในการผสานรวม ML Kit
ซึ่งสามารถแมปพิกัดผลลัพธ์ของโมเดล ML (เช่น กรอบล้อมรอบใบหน้า)
กับพิกัดตัวอย่างได้โดยตรง
|
ความสามารถในการใช้ `Surface` ที่กำหนดเองสำหรับการแสดงตัวอย่างกล้องช่วยให้มีความยืดหยุ่นมากขึ้น เช่น การใช้โค้ด `Surface` ที่มีอยู่ ซึ่งอาจเป็นอินพุตไปยังส่วนอื่นๆ ของแอป |
หากคุณติดขัดขณะพยายามย้ายข้อมูล โปรดติดต่อเราในกลุ่มสนทนา CameraX Group
ก่อนที่จะย้ายข้อมูล
เปรียบเทียบการใช้งาน CameraX กับ Camera1
แม้ว่าโค้ดอาจดูแตกต่างกัน แต่แนวคิดพื้นฐานใน Camera1 และ
CameraX นั้นคล้ายกันมาก CameraX สรุปฟังก์ชันการทำงานของกล้องทั่วไปเป็น
Use Case และด้วยเหตุนี้ CameraX จึงจัดการงานหลายอย่างที่นักพัฒนาแอปต้องทำใน
Camera1 โดยอัตโนมัติ CameraX มี UseCase อยู่ 4 รายการ ซึ่งคุณใช้กับงานต่างๆ ของกล้องได้ ได้แก่ Preview, ImageCapture, VideoCapture และ ImageAnalysis
ตัวอย่างหนึ่งที่ CameraX จัดการรายละเอียดระดับล่างให้นักพัฒนาแอปคือ ViewPort ซึ่งแชร์ใน UseCase ที่ใช้งานอยู่ วิธีนี้ช่วยให้มั่นใจว่าผู้ใช้ทั้งหมด
UseCaseจะเห็นพิกเซลที่เหมือนกันทุกประการ ใน Camera1 คุณต้องจัดการรายละเอียดเหล่านี้ด้วยตนเอง เนื่องจากสัดส่วนภาพในอุปกรณ์ต่างๆ แตกต่างกัน การจับคู่
ตัวอย่างกับสื่อที่บันทึกไว้จึงเป็นเรื่องยาก
อีกตัวอย่างหนึ่งคือ CameraX จะจัดการLifecycleการเรียกกลับโดยอัตโนมัติในLifecycleอินสแตนซ์ที่คุณระบุ สถาปัตยกรรมนี้ช่วยให้ CameraX จัดการการเชื่อมต่อของแอปกับกล้องตลอดวงจรของกิจกรรม Android รวมถึงกรณีต่อไปนี้ การปิดกล้องเมื่อแอป
เข้าสู่เบื้องหลัง การนำตัวอย่างกล้องออกเมื่อหน้าจอไม่จำเป็นต้อง
แสดงอีกต่อไป และการหยุดตัวอย่างกล้องชั่วคราวเมื่อกิจกรรมอื่น
มีความสำคัญในเบื้องหน้า เช่น สายวิดีโอคอลที่เข้ามา
สุดท้าย CameraX จะจัดการการหมุนและการปรับขนาดโดยที่คุณไม่ต้องเขียนโค้ดเพิ่มเติม
ในกรณีของ Activity ที่มีการวางแนวที่ปลดล็อก ระบบจะตั้งค่า UseCase ทุกครั้งที่หมุนอุปกรณ์ เนื่องจากระบบจะทำลาย และสร้าง Activity ใหม่เมื่อมีการเปลี่ยนแปลงการวางแนว ซึ่งจะส่งผลให้UseCasesตั้งค่าการหมุนเป้าหมายให้ตรงกับการวางแนวของจอแสดงผลโดยค่าเริ่มต้นทุกครั้ง อ่านเพิ่มเติมเกี่ยวกับการหมุนใน CameraX
ก่อนจะลงรายละเอียด เรามาดูภาพรวมของ
UseCaseของ CameraX และความสัมพันธ์กับแอป Camera1 กัน (แนวคิดของ CameraX อยู่ในสีน้ำเงินและแนวคิดของ Camera1 อยู่ในสีเขียว)
CameraX |
|||
| การกำหนดค่า CameraController / CameraProvider | |||
| ↓ | ↓ | ↓ | ↓ |
| แสดงตัวอย่าง | ImageCapture | VideoCapture | ImageAnalysis |
| ⁞ | ⁞ | ⁞ | ⁞ |
| จัดการพื้นผิวแสดงตัวอย่างและตั้งค่าในกล้อง | ตั้งค่า PictureCallback และเรียกใช้ takePicture() ในกล้อง | จัดการการกำหนดค่า Camera และ MediaRecorder ตามลำดับที่เฉพาะเจาะจง | โค้ดการวิเคราะห์ที่กำหนดเองซึ่งสร้างขึ้นบนแพลตฟอร์มตัวอย่าง |
| ↑ | ↑ | ↑ | ↑ |
| รหัสเฉพาะอุปกรณ์ | |||
| ↑ | |||
| การจัดการการหมุนและการปรับขนาดอุปกรณ์ | |||
| ↑ | |||
| การจัดการเซสชันกล้อง (การเลือกกล้อง การจัดการวงจรการใช้งาน) | |||
Camera1 |
|||
ความเข้ากันได้และประสิทธิภาพใน CameraX
CameraX รองรับอุปกรณ์ที่ใช้ Android 5.0 (ระดับ API 21) ขึ้นไป ซึ่งคิดเป็นกว่า 98% ของอุปกรณ์ Android ที่มีอยู่ CameraX สร้างขึ้นเพื่อจัดการ ความแตกต่างระหว่างอุปกรณ์โดยอัตโนมัติ ซึ่งช่วยลดความจำเป็นในการใช้โค้ด เฉพาะอุปกรณ์ในแอป นอกจากนี้ เรายังทดสอบอุปกรณ์จริงกว่า 150 เครื่องใน Android ทุกเวอร์ชันตั้งแต่ 5.0 ขึ้นไปใน CameraX Test Lab คุณดูรายการอุปกรณ์ทั้งหมด ใน Test Lab ได้
CameraX ใช้ Executor เพื่อขับเคลื่อนสแต็กกล้อง คุณตั้งค่า
ตัวดำเนินการของคุณเองใน CameraX ได้หากแอปมีข้อกำหนดด้านการทำงานแบบหลายเธรดที่เฉพาะเจาะจง
หากไม่ได้ตั้งค่า CameraX จะสร้างและใช้ Executor ภายในเริ่มต้นที่ได้รับการเพิ่มประสิทธิภาพ
API ของแพลตฟอร์มหลายรายการที่ใช้สร้าง CameraX ต้องบล็อกการสื่อสารระหว่างโปรเซส (IPC) กับฮาร์ดแวร์ ซึ่งบางครั้งอาจใช้เวลาหลายร้อยมิลลิวินาทีในการตอบสนอง ด้วยเหตุนี้ CameraX จึงเรียกใช้ API เหล่านี้จากเทรดในเบื้องหลังเท่านั้น ซึ่งจะช่วยให้มั่นใจได้ว่าเทรดหลักจะไม่ถูกบล็อกและ UI จะยังคงทำงานได้อย่างราบรื่น อ่านเพิ่มเติมเกี่ยวกับชุดข้อความ
หากตลาดเป้าหมายของแอปมีอุปกรณ์ระดับล่าง CameraX มีวิธีลดเวลาในการตั้งค่าด้วยตัวจำกัดกล้อง เนื่องจากกระบวนการ
เชื่อมต่อกับคอมโพเนนต์ฮาร์ดแวร์อาจใช้เวลานานพอสมควร
โดยเฉพาะในอุปกรณ์ระดับล่าง คุณจึงระบุชุดกล้องที่แอป
ต้องการได้ CameraX จะเชื่อมต่อกับกล้องเหล่านี้ในระหว่างการตั้งค่าเท่านั้น เช่น หาก
แอปพลิเคชันใช้เฉพาะกล้องด้านหลัง ก็สามารถตั้งค่านี้ด้วย
DEFAULT_BACK_CAMERA จากนั้น CameraX จะหลีกเลี่ยงการเริ่มต้นกล้องด้านหน้า
เพื่อลดเวลาในการตอบสนอง
แนวคิดการพัฒนาแอป Android
คู่มือนี้มีสมมติฐานว่าคุณคุ้นเคยกับการพัฒนา Android โดยทั่วไป นอกเหนือจาก พื้นฐานแล้ว แนวคิด 2 อย่างต่อไปนี้จะช่วยให้คุณเข้าใจก่อน ที่จะไปดูโค้ดต่อไปนี้
- View Binding จะสร้างคลาสการเชื่อมโยงสำหรับไฟล์เลย์เอาต์ XML ซึ่งช่วยให้คุณอ้างอิงมุมมองในกิจกรรมได้ ดังที่แสดงในข้อมูลโค้ดหลายๆ รายการที่ตามมา การเชื่อมโยงมุมมอง
findViewById()(วิธีก่อนหน้าในการอ้างอิงมุมมอง) มีความแตกต่างกันอยู่บ้าง แต่ในโค้ดต่อไปนี้ คุณควรจะสามารถแทนที่บรรทัดการเชื่อมโยงมุมมองด้วยการเรียกfindViewById()ที่คล้ายกันได้ - โครูทีนแบบอะซิงโครนัสคือรูปแบบการออกแบบการทำงานพร้อมกันที่เพิ่มเข้ามาใน
Kotlin 1.3 ซึ่งใช้เพื่อจัดการเมธอด CameraX ที่แสดงผลเป็น
ListenableFutureได้ ซึ่งทำได้ง่ายขึ้นด้วยไลบรารี Jetpack Concurrent ตั้งแต่เวอร์ชัน 1.1.0 เป็นต้นไป วิธีเพิ่มโคโรทีนแบบอะซิงโครนัส ลงในแอป- เพิ่ม
implementation("androidx.concurrent:concurrent-futures-ktx:1.1.0")ลงในไฟล์ Gradle - ใส่โค้ด CameraX ที่แสดงผล
ListenableFutureในบล็อกlaunchหรือฟังก์ชันที่ระงับ - เพิ่มการเรียก
await()ไปยังการเรียกใช้ฟังก์ชันที่แสดงผลListenableFuture - หากต้องการทำความเข้าใจวิธีการทำงานของโครูทีนให้ดียิ่งขึ้น โปรดดูคู่มือเริ่มใช้โครูทีน
- เพิ่ม
ย้ายข้อมูลสถานการณ์ที่พบบ่อย
ส่วนนี้จะอธิบายวิธีเปลี่ยนสถานการณ์ที่พบบ่อยจาก Camera1 ไปเป็น CameraX
แต่ละสถานการณ์ครอบคลุมการใช้งาน Camera1, การใช้งาน CameraX CameraProvider
และการใช้งาน CameraX CameraController
การเลือกกล้อง
ในแอปพลิเคชันกล้อง สิ่งแรกๆ ที่คุณอาจต้องการนำเสนอคือ วิธีเลือกกล้องต่างๆ
Camera1
ใน Camera1 คุณจะเรียก Camera.open() โดยไม่มีพารามิเตอร์เพื่อเปิดกล้องหลังตัวแรก หรือส่งรหัสจำนวนเต็มสำหรับกล้องที่ต้องการเปิดก็ได้ ตัวอย่างลักษณะที่อาจปรากฏมีดังนี้
// Camera1: select a camera from id. // Note: opening the camera is a non-trivial task, and it shouldn't be // called from the main thread, unlike CameraX calls, which can be // on the main thread since CameraX kicks off background threads // internally as needed. private fun safeCameraOpen(id: Int): Boolean { return try { releaseCameraAndPreview() camera = Camera.open(id) true } catch (e: Exception) { Log.e(TAG, "failed to open camera", e) false } } private fun releaseCameraAndPreview() { preview?.setCamera(null) camera?.release() camera = null }
CameraX: CameraController
ใน CameraX การเลือกกล้องจะจัดการโดยคลาส CameraSelector CameraX
ช่วยให้การใช้กล้องเริ่มต้นในกรณีทั่วไปเป็นเรื่องง่าย คุณระบุได้ว่าจะใช้กล้องหน้าเริ่มต้นหรือกล้องหลังเริ่มต้น
นอกจากนี้ ออบเจ็กต์ CameraControl ของ CameraX ยังช่วยให้คุณตั้งค่าระดับการซูมสำหรับแอปได้ด้วย
ดังนั้นหากแอปทำงานบนอุปกรณ์ที่รองรับกล้องตรรกะ
ระบบจะเปลี่ยนไปใช้เลนส์ที่เหมาะสม
นี่คือโค้ด CameraX สำหรับการใช้กล้องหลังเริ่มต้นที่มี
CameraController
// CameraX: select a camera with CameraController var cameraController = LifecycleCameraController(baseContext) val selector = CameraSelector.Builder() .requireLensFacing(CameraSelector.LENS_FACING_BACK).build() cameraController.cameraSelector = selector
CameraX: CameraProvider
ตัวอย่างการเลือกกล้องหน้าเริ่มต้นด้วย CameraProvider
(ใช้กล้องหน้าหรือกล้องหลังกับ CameraController หรือ
CameraProvider ก็ได้)
// CameraX: select a camera with CameraProvider. // Use await() within a suspend function to get CameraProvider instance. // For more details on await(), see the preceding "Android development concepts" // section. private suspend fun startCamera() { val cameraProvider = ProcessCameraProvider.getInstance(this).await() // Set up UseCases (more on UseCases in later scenarios) var useCases:Array= ... // Set the cameraSelector to use the default front-facing (selfie) // camera. val cameraSelector = CameraSelector.DEFAULT_FRONT_CAMERA try { // Unbind UseCases before rebinding. cameraProvider.unbindAll() // Bind UseCases to camera. This function returns a camera // object which can be used to perform operations like zoom, // flash, and focus. var camera = cameraProvider.bindToLifecycle( this, cameraSelector, useCases) } catch(exc: Exception) { Log.e(TAG, "UseCase binding failed", exc) } }) ... // Call startCamera in the setup flow of your app, such as in onViewCreated. override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) ... lifecycleScope.launch { startCamera() } }
หากต้องการควบคุมว่าจะเลือกกล้องใด คุณก็ทำได้ใน CameraX โดยใช้ CameraProvider ด้วยการเรียกใช้ getAvailableCameraInfos() ซึ่งจะให้ออบเจ็กต์ CameraInfo สำหรับ
การตรวจสอบพร็อพเพอร์ตี้บางอย่างของกล้อง เช่น isFocusMeteringSupported() จากนั้นคุณสามารถแปลงเป็น CameraSelector เพื่อใช้ตามที่แสดงในตัวอย่างก่อนหน้าด้วยเมธอด CameraInfo.getCameraSelector()
คุณดูรายละเอียดเพิ่มเติมเกี่ยวกับกล้องแต่ละตัวได้โดยใช้คลาส
Camera2CameraInfo เรียกใช้ getCameraCharacteristic() พร้อมคีย์สำหรับข้อมูลกล้องที่ต้องการ ดูรายการคีย์ทั้งหมดที่คุณค้นหาได้ในคลาส CameraCharacteristics
ตัวอย่างการใช้ฟังก์ชัน checkFocalLength() ที่กำหนดเองซึ่งคุณกำหนดเองได้มีดังนี้
// CameraX: get a cameraSelector for first camera that matches the criteria // defined in checkFocalLength(). val cameraInfo = cameraProvider.getAvailableCameraInfos() .first { cameraInfo -> val focalLengths = Camera2CameraInfo.from(cameraInfo) .getCameraCharacteristic( CameraCharacteristics.LENS_INFO_AVAILABLE_FOCAL_LENGTHS ) return checkFocalLength(focalLengths) } val cameraSelector = cameraInfo.getCameraSelector()
การแสดงตัวอย่าง
แอปพลิเคชันกล้องส่วนใหญ่ต้องแสดงฟีดกล้องบนหน้าจอในบางจุด เมื่อใช้ Camera1 คุณต้องจัดการการเรียกกลับของวงจรอย่างถูกต้อง และ คุณยังต้องกำหนดการหมุนและการปรับขนาดสำหรับการแสดงตัวอย่างด้วย
นอกจากนี้ ใน Camera1 คุณต้องตัดสินใจว่าจะใช้ TextureView
หรือ SurfaceView เป็นพื้นผิวตัวอย่าง ทั้ง 2 ตัวเลือกมีข้อดีข้อเสียแตกต่างกันไป และไม่ว่าในกรณีใด Camera1 ก็กำหนดให้คุณต้องจัดการการหมุนและการปรับขนาดอย่างถูกต้อง ในทางกลับกัน PreviewView ของ CameraX มีการใช้งานพื้นฐานสำหรับทั้ง TextureView และ SurfaceView CameraX จะตัดสินว่าการใช้งานใดดีที่สุดโดยพิจารณาจากปัจจัยต่างๆ เช่น ประเภทอุปกรณ์และเวอร์ชัน Android ที่แอปของคุณทำงานอยู่ หากการติดตั้งใช้งานใดใช้งานร่วมกันได้
คุณสามารถประกาศค่ากำหนดด้วย PreviewView.ImplementationMode COMPATIBLE
TextureView ตัวเลือกใช้ PERFORMANCE
SurfaceView สำหรับตัวอย่าง และค่าใช้ PERFORMANCE
SurfaceView (หากเป็นไปได้)
Camera1
หากต้องการแสดงตัวอย่าง คุณต้องเขียนคลาส Preview ของคุณเองโดยใช้ การติดตั้งใช้งานอินเทอร์เฟซ android.view.SurfaceHolder.Callback ซึ่งใช้เพื่อส่งข้อมูลรูปภาพจากฮาร์ดแวร์กล้องไปยังแอปพลิเคชัน
จากนั้นก่อนที่จะเริ่มแสดงตัวอย่างรูปภาพแบบสดได้ คุณต้องส่งผ่านPreviewคลาสไปยังออบเจ็กต์ Camera
// Camera1: set up a camera preview. class Preview( context: Context, private val camera: Camera ) : SurfaceView(context), SurfaceHolder.Callback { private val holder: SurfaceHolder = holder.apply { addCallback(this@Preview) setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS) } override fun surfaceCreated(holder: SurfaceHolder) { // The Surface has been created, now tell the camera // where to draw the preview. camera.apply { try { setPreviewDisplay(holder) startPreview() } catch (e: IOException) { Log.d(TAG, "error setting camera preview", e) } } } override fun surfaceDestroyed(holder: SurfaceHolder) { // Take care of releasing the Camera preview in your activity. } override fun surfaceChanged(holder: SurfaceHolder, format: Int, w: Int, h: Int) { // If your preview can change or rotate, take care of those // events here. Make sure to stop the preview before resizing // or reformatting it. if (holder.surface == null) { return // The preview surface does not exist. } // Stop preview before making changes. try { camera.stopPreview() } catch (e: Exception) { // Tried to stop a non-existent preview; nothing to do. } // Set preview size and make any resize, rotate or // reformatting changes here. // Start preview with new settings. camera.apply { try { setPreviewDisplay(holder) startPreview() } catch (e: Exception) { Log.d(TAG, "error starting camera preview", e) } } } } class CameraActivity : AppCompatActivity() { private lateinit var viewBinding: ActivityMainBinding private var camera: Camera? = null private var preview: Preview? = null override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) viewBinding = ActivityMainBinding.inflate(layoutInflater) setContentView(viewBinding.root) // Create an instance of Camera. camera = getCameraInstance() preview = camera?.let { // Create the Preview view. Preview(this, it) } // Set the Preview view as the content of the activity. val cameraPreview: FrameLayout = viewBinding.cameraPreview cameraPreview.addView(preview) } }
CameraX: CameraController
ใน CameraX คุณในฐานะนักพัฒนาแอปจะมีสิ่งต่างๆ ที่ต้องจัดการน้อยลงมาก หากใช้ CameraController คุณต้องใช้ PreviewView ด้วย ซึ่งหมายความว่าPreview UseCase จะมีผลโดยนัย ทำให้การตั้งค่าใช้เวลาน้อยลงมาก
// CameraX: set up a camera preview with a CameraController. class MainActivity : AppCompatActivity() { private lateinit var viewBinding: ActivityMainBinding override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) viewBinding = ActivityMainBinding.inflate(layoutInflater) setContentView(viewBinding.root) // Create the CameraController and set it on the previewView. var cameraController = LifecycleCameraController(baseContext) cameraController.bindToLifecycle(this) val previewView: PreviewView = viewBinding.cameraPreview previewView.controller = cameraController } }
CameraX: CameraProvider
CameraProvider ของ CameraX ไม่ได้บังคับให้คุณต้องใช้ PreviewView แต่ก็ยังช่วยลดความซับซ้อนในการตั้งค่าตัวอย่างเมื่อเทียบกับ Camera1 ได้มาก ตัวอย่างนี้ใช้ PreviewView เพื่อวัตถุประสงค์ในการสาธิต
แต่คุณสามารถเขียน SurfaceProvider ที่กำหนดเองเพื่อส่งไปยัง setSurfaceProvider() ได้หากมีความต้องการที่ซับซ้อนมากขึ้น
ในกรณีนี้ Preview UseCase จะไม่ถือว่ามีอยู่แล้วเหมือนกับ CameraController
ดังนั้นคุณจึงต้องตั้งค่าดังนี้
// CameraX: set up a camera preview with a CameraProvider. // Use await() within a suspend function to get CameraProvider instance. // For more details on await(), see the preceding "Android development concepts" // section. private suspend fun startCamera() { val cameraProvider = ProcessCameraProvider.getInstance(this).await() // Create Preview UseCase. val preview = Preview.Builder() .build() .also { it.setSurfaceProvider( viewBinding.viewFinder.surfaceProvider ) } // Select default back camera. val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA try { // Unbind UseCases before rebinding. cameraProvider.unbindAll() // Bind UseCases to camera. This function returns a camera // object which can be used to perform operations like zoom, // flash, and focus. var camera = cameraProvider.bindToLifecycle( this, cameraSelector, useCases) } catch(exc: Exception) { Log.e(TAG, "UseCase binding failed", exc) } }) ... // Call startCamera() in the setup flow of your app, such as in onViewCreated. override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) ... lifecycleScope.launch { startCamera() } }
แตะเพื่อโฟกัส
เมื่อตัวอย่างกล้องปรากฏบนหน้าจอ การควบคุมทั่วไปคือการตั้งค่าจุดโฟกัส เมื่อผู้ใช้แตะตัวอย่าง
Camera1
หากต้องการใช้การแตะเพื่อโฟกัสใน Camera1 คุณต้องคำนวณโฟกัสที่เหมาะสม
Area เพื่อระบุตำแหน่งที่ Camera ควรพยายามโฟกัส Area นี้จะ
ส่งไปยัง setFocusAreas() นอกจากนี้ คุณต้องตั้งค่าโหมดโฟกัสที่เข้ากันได้ใน Camera พื้นที่โฟกัสจะมีผลก็ต่อเมื่อโหมดโฟกัสปัจจุบันเป็น
FOCUS_MODE_AUTO, FOCUS_MODE_MACRO, FOCUS_MODE_CONTINUOUS_VIDEO หรือ
FOCUS_MODE_CONTINUOUS_PICTURE
แต่ละ Area คือสี่เหลี่ยมผืนผ้าที่มีน้ำหนักที่ระบุ น้ำหนักคือค่าระหว่าง 1 ถึง 1000 และใช้เพื่อจัดลำดับความสำคัญของโฟกัส Areas หากมีการตั้งค่าหลายรายการ ตัวอย่างนี้ใช้ Area เพียงรายการเดียว ดังนั้นค่าถ่วงน้ำหนักจึงไม่มีผล พิกัดของ
สี่เหลี่ยมผืนผ้าอยู่ในช่วง -1000 ถึง 1000 จุดซ้ายบนคือ (-1000, -1000)
จุดขวาล่างคือ (1000, 1000) ทิศทางจะสัมพันธ์กับ
การวางแนวของเซ็นเซอร์ นั่นคือสิ่งที่เซ็นเซอร์เห็น ทิศทางจะไม่ได้รับผลกระทบจากการ
หมุนหรือการมิเรอร์ของ Camera.setDisplayOrientation() ดังนั้นคุณจึงต้อง
แปลงพิกัดการโต้ตอบแบบสัมผัสเป็นพิกัดเซ็นเซอร์
// Camera1: implement tap-to-focus. class TapToFocusHandler : Camera.AutoFocusCallback { private fun handleFocus(event: MotionEvent) { val camera = camera ?: return val parameters = try { camera.getParameters() } catch (e: RuntimeException) { return } // Cancel previous auto-focus function, if one was in progress. camera.cancelAutoFocus() // Create focus Area. val rect = calculateFocusAreaCoordinates(event.x, event.y) val weight = 1 // This value's not important since there's only 1 Area. val focusArea = Camera.Area(rect, weight) // Set the focus parameters. parameters.setFocusMode(Parameters.FOCUS_MODE_AUTO) parameters.setFocusAreas(listOf(focusArea)) // Set the parameters back on the camera and initiate auto-focus. camera.setParameters(parameters) camera.autoFocus(this) } private fun calculateFocusAreaCoordinates(x: Int, y: Int) { // Define the size of the Area to be returned. This value // should be optimized for your app. val focusAreaSize = 100 // You must define functions to rotate and scale the x and y values to // be values between 0 and 1, where (0, 0) is the upper left-hand side // of the preview, and (1, 1) is the lower right-hand side. val normalizedX = (rotateAndScaleX(x) - 0.5) * 2000 val normalizedY = (rotateAndScaleY(y) - 0.5) * 2000 // Calculate the values for left, top, right, and bottom of the Rect to // be returned. If the Rect would extend beyond the allowed values of // (-1000, -1000, 1000, 1000), then crop the values to fit inside of // that boundary. val left = max(normalizedX - (focusAreaSize / 2), -1000) val top = max(normalizedY - (focusAreaSize / 2), -1000) val right = min(left + focusAreaSize, 1000) val bottom = min(top + focusAreaSize, 1000) return Rect(left, top, left + focusAreaSize, top + focusAreaSize) } override fun onAutoFocus(focused: Boolean, camera: Camera) { if (!focused) { Log.d(TAG, "tap-to-focus failed") } } }
CameraX: CameraController
CameraController จะฟังเหตุการณ์การสัมผัสของ PreviewView เพื่อจัดการ
การแตะเพื่อโฟกัสโดยอัตโนมัติ คุณเปิดและปิดใช้การแตะเพื่อโฟกัสได้ด้วย
setTapToFocusEnabled() และตรวจสอบค่าด้วย
Getter ที่เกี่ยวข้อง isTapToFocusEnabled()
เมธอด getTapToFocusState() จะแสดงผลออบเจ็กต์ LiveData สำหรับ
การติดตามการเปลี่ยนแปลงสถานะโฟกัสใน CameraController
// CameraX: track the state of tap-to-focus over the Lifecycle of a PreviewView, // with handlers you can define for focused, not focused, and failed states. val tapToFocusStateObserver = Observer{ state -> when (state) { CameraController.TAP_TO_FOCUS_NOT_STARTED -> Log.d(TAG, "tap-to-focus init") CameraController.TAP_TO_FOCUS_STARTED -> Log.d(TAG, "tap-to-focus started") CameraController.TAP_TO_FOCUS_FOCUSED -> Log.d(TAG, "tap-to-focus finished (focus successful)") CameraController.TAP_TO_FOCUS_NOT_FOCUSED -> Log.d(TAG, "tap-to-focus finished (focused unsuccessful)") CameraController.TAP_TO_FOCUS_FAILED -> Log.d(TAG, "tap-to-focus failed") } } cameraController.getTapToFocusState().observe(this, tapToFocusStateObserver)
CameraX: CameraProvider
เมื่อใช้ CameraProvider คุณจะต้องตั้งค่าบางอย่างเพื่อให้แตะเพื่อโฟกัส
ทำงานได้ ตัวอย่างนี้สมมติว่าคุณใช้ PreviewView หากไม่เป็นเช่นนั้น คุณต้อง
ปรับตรรกะให้ใช้กับ Surface ที่กำหนดเอง
ขั้นตอนเมื่อใช้ PreviewView มีดังนี้
- ตั้งค่าเครื่องตรวจจับท่าทางสัมผัสเพื่อจัดการเหตุการณ์การแตะ
- สร้าง
MeteringPointโดยใช้MeteringPointFactory.createPoint()ด้วยเหตุการณ์การแตะ - สร้าง
FocusMeteringActionด้วยMeteringPoint - เมื่อมีออบเจ็กต์
CameraControlในCamera(แสดงผลจากbindToLifecycle()) ให้เรียกstartFocusAndMetering()โดยส่งFocusMeteringAction - (ไม่บังคับ) ตอบกลับ
FocusMeteringResult - ตั้งค่าเครื่องตรวจจับท่าทางสัมผัสให้ตอบสนองต่อเหตุการณ์การแตะใน
PreviewView.setOnTouchListener()
// CameraX: implement tap-to-focus with CameraProvider. // Define a gesture detector to respond to tap events and call // startFocusAndMetering on CameraControl. If you want to use a // coroutine with await() to check the result of focusing, see the // preceding "Android development concepts" section. val gestureDetector = GestureDetectorCompat(context, object : SimpleOnGestureListener() { override fun onSingleTapUp(e: MotionEvent): Boolean { val previewView = previewView ?: return val camera = camera ?: return val meteringPointFactory = previewView.meteringPointFactory val focusPoint = meteringPointFactory.createPoint(e.x, e.y) val meteringAction = FocusMeteringAction .Builder(meteringPoint).build() lifecycleScope.launch { val focusResult = camera.cameraControl .startFocusAndMetering(meteringAction).await() if (!result.isFocusSuccessful()) { Log.d(TAG, "tap-to-focus failed") } } } } ) ... // Set the gestureDetector in a touch listener on the PreviewView. previewView.setOnTouchListener { _, event -> // See pinch-to-zoom scenario for scaleGestureDetector definition. var didConsume = scaleGestureDetector.onTouchEvent(event) if (!scaleGestureDetector.isInProgress) { didConsume = gestureDetector.onTouchEvent(event) } didConsume }
บีบเพื่อซูม
การซูมเข้าและออกจากตัวอย่างเป็นอีกหนึ่งการดัดแปลงโดยตรงที่พบบ่อยใน ตัวอย่างกล้อง เมื่ออุปกรณ์มีกล้องมากขึ้น ผู้ใช้ก็คาดหวังว่าระบบจะเลือกเลนส์ที่มีทางยาวโฟกัสดีที่สุดโดยอัตโนมัติเมื่อซูม
Camera1
การซูมโดยใช้ Camera1 ทำได้ 2 วิธี Camera.startSmoothZoom() วิธีการ
จะเคลื่อนไหวจากระดับการซูมปัจจุบันไปยังระดับการซูมที่คุณส่งผ่าน เมธอด
Camera.Parameters.setZoom() จะข้ามไปยังระดับการซูมที่คุณส่งผ่านโดยตรง
ก่อนใช้กล้องตัวใดตัวหนึ่ง โปรดโทรหา isSmoothZoomSupported() หรือ
isZoomSupported() เพื่อตรวจสอบว่าวิธีการซูมที่เกี่ยวข้องที่คุณต้องการ
พร้อมใช้งานในกล้อง
ตัวอย่างนี้ใช้ setZoom() เพื่อใช้การบีบนิ้วเพื่อซูม เนื่องจาก Listener การแตะบนพื้นผิวตัวอย่างจะเริ่มทำงานเหตุการณ์อย่างต่อเนื่องเมื่อมีการบีบนิ้ว จึงอัปเดตระดับการซูมทันทีทุกครั้ง เราจะกำหนดคลาส
ZoomTouchListenerในส่วนนี้ในภายหลัง และคุณควรตั้งค่า
เป็นฟังก์ชันเรียกกลับไปยัง Listener การแตะของพื้นผิวตัวอย่าง
// Camera1: implement pinch-to-zoom. // Define a scale gesture detector to respond to pinch events and call // setZoom on Camera.Parameters. val scaleGestureDetector = ScaleGestureDetector(context, object : ScaleGestureDetector.OnScaleGestureListener { override fun onScale(detector: ScaleGestureDetector): Boolean { val camera = camera ?: return false val parameters = try { camera.parameters } catch (e: RuntimeException) { return false } // In case there is any focus happening, stop it. camera.cancelAutoFocus() // Set the zoom level on the Camera.Parameters, and set // the Parameters back onto the Camera. val currentZoom = parameters.zoom parameters.setZoom(detector.scaleFactor * currentZoom) camera.setParameters(parameters) return true } } ) // Define a View.OnTouchListener to attach to your preview view. class ZoomTouchListener : View.OnTouchListener { override fun onTouch(v: View, event: MotionEvent): Boolean = scaleGestureDetector.onTouchEvent(event) } // Set a ZoomTouchListener to handle touch events on your preview view // if zoom is supported by the current camera. if (camera.getParameters().isZoomSupported()) { view.setOnTouchListener(ZoomTouchListener()) }
CameraX: CameraController
CameraControllerจะฟังเหตุการณ์การแตะของ PreviewView
เพื่อจัดการการบีบนิ้วเพื่อซูมโดยอัตโนมัติ ซึ่งคล้ายกับการแตะเพื่อโฟกัส คุณเปิดและปิดใช้
การบีบนิ้วเพื่อซูมได้ด้วย setPinchToZoomEnabled() และตรวจสอบค่าได้ด้วย
ตัวรับที่เกี่ยวข้อง isPinchToZoomEnabled()
เมธอด getZoomState() จะแสดงผลออบเจ็กต์ LiveData สำหรับการติดตาม
การเปลี่ยนแปลงใน ZoomState ใน CameraController
// CameraX: track the state of pinch-to-zoom over the Lifecycle of // a PreviewView, logging the linear zoom ratio. val pinchToZoomStateObserver = Observer{ state -> val zoomRatio = state.getZoomRatio() Log.d(TAG, "ptz-zoom-ratio $zoomRatio") } cameraController.getZoomState().observe(this, pinchToZoomStateObserver)
CameraX: CameraProvider
หากต้องการให้การบีบนิ้วเพื่อซูมทำงานร่วมกับ CameraProvider คุณต้องตั้งค่าบางอย่าง หากไม่ได้ใช้ PreviewView คุณจะต้องปรับตรรกะให้ใช้กับ Surface ที่กำหนดเอง
ขั้นตอนเมื่อใช้ PreviewView มีดังนี้
- ตั้งค่าเครื่องตรวจจับท่าทางสัมผัสการปรับขนาดเพื่อจัดการเหตุการณ์การบีบนิ้ว
- รับ
ZoomStateจากออบเจ็กต์Camera.CameraInfoซึ่งจะแสดงผลอินสแตนซ์Cameraเมื่อคุณเรียกbindToLifecycle() - หาก
ZoomStateมีค่าzoomRatioให้บันทึกค่าดังกล่าวเป็นอัตราส่วนการซูมปัจจุบัน หากไม่มีzoomRatioในZoomStateให้ใช้อัตราการซูมเริ่มต้นของกล้อง (1.0) - นำผลคูณของอัตราส่วนการซูมปัจจุบันกับ
scaleFactorมา กำหนดอัตราส่วนการซูมใหม่ แล้วส่งค่าดังกล่าวไปยังCameraControl.setZoomRatio() - ตั้งค่าเครื่องตรวจจับท่าทางสัมผัสให้ตอบสนองต่อเหตุการณ์การแตะใน
PreviewView.setOnTouchListener()
// CameraX: implement pinch-to-zoom with CameraProvider. // Define a scale gesture detector to respond to pinch events and call // setZoomRatio on CameraControl. val scaleGestureDetector = ScaleGestureDetector(context, object : SimpleOnGestureListener() { override fun onScale(detector: ScaleGestureDetector): Boolean { val camera = camera ?: return val zoomState = camera.cameraInfo.zoomState val currentZoomRatio: Float = zoomState.value?.zoomRatio ?: 1f camera.cameraControl.setZoomRatio( detector.scaleFactor * currentZoomRatio ) } } ) ... // Set the scaleGestureDetector in a touch listener on the PreviewView. previewView.setOnTouchListener { _, event -> var didConsume = scaleGestureDetector.onTouchEvent(event) if (!scaleGestureDetector.isInProgress) { // See pinch-to-zoom scenario for gestureDetector definition. didConsume = gestureDetector.onTouchEvent(event) } didConsume }
การถ่ายรูป
ส่วนนี้แสดงวิธีทริกเกอร์การจับภาพ ไม่ว่าคุณจะต้องทำเมื่อกดปุ่มชัตเตอร์ หลังจากตัวจับเวลาหมด หรือในเหตุการณ์อื่นๆ ที่คุณเลือก
Camera1
ใน Camera1 คุณต้องกำหนด Camera.PictureCallback ก่อนเพื่อจัดการ
ข้อมูลรูปภาพเมื่อมีการขอ ต่อไปนี้คือตัวอย่างง่ายๆ ของ PictureCallback
สำหรับการจัดการข้อมูลรูปภาพ JPEG
// Camera1: define a Camera.PictureCallback to handle JPEG data. private val picture = Camera.PictureCallback { data, _ -> val pictureFile: File = getOutputMediaFile(MEDIA_TYPE_IMAGE) ?: run { Log.d(TAG, "error creating media file, check storage permissions") return@PictureCallback } try { val fos = FileOutputStream(pictureFile) fos.write(data) fos.close() } catch (e: FileNotFoundException) { Log.d(TAG, "file not found", e) } catch (e: IOException) { Log.d(TAG, "error accessing file", e) } }
จากนั้นเมื่อใดก็ตามที่ต้องการถ่ายภาพ ให้เรียกใช้takePicture()เมธอด
ในอินสแตนซ์ Camera takePicture()วิธีนี้มีพารามิเตอร์ที่แตกต่างกัน 3 รายการ
สำหรับข้อมูลประเภทต่างๆ พารามิเตอร์แรกใช้สำหรับ
ShutterCallback (ซึ่งไม่ได้กำหนดไว้ในตัวอย่างนี้) พารามิเตอร์ที่ 2 คือ
สำหรับ PictureCallback เพื่อจัดการข้อมูลกล้องดิบ (ที่ไม่ได้บีบอัด) พารามิเตอร์ที่สาม
คือพารามิเตอร์ที่ตัวอย่างนี้ใช้ เนื่องจากเป็น PictureCallback เพื่อจัดการ
ข้อมูลรูปภาพ JPEG
// Camera1: call takePicture on Camera instance, passing our PictureCallback. camera?.takePicture(null, null, picture)
CameraX: CameraController
CameraController ของ CameraX ยังคงความเรียบง่ายของ Camera1 สำหรับการจับภาพ
โดยการใช้เมธอด takePicture() ของตัวเอง ในส่วนนี้ ให้กำหนดฟังก์ชันเพื่อกำหนดค่ารายการ MediaStore และถ่ายรูปเพื่อบันทึกไว้ที่นั่น
// CameraX: define a function that uses CameraController to take a photo. private val FILENAME_FORMAT = "yyyy-MM-dd-HH-mm-ss-SSS" private fun takePhoto() { // Create time stamped name and MediaStore entry. val name = SimpleDateFormat(FILENAME_FORMAT, Locale.US) .format(System.currentTimeMillis()) val contentValues = ContentValues().apply { put(MediaStore.MediaColumns.DISPLAY_NAME, name) put(MediaStore.MediaColumns.MIME_TYPE, "image/jpeg") if(Build.VERSION.SDK_INT > Build.VERSION_CODES.P) { put(MediaStore.Images.Media.RELATIVE_PATH, "Pictures/CameraX-Image") } } // Create output options object which contains file + metadata. val outputOptions = ImageCapture.OutputFileOptions .Builder(context.getContentResolver(), MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues) .build() // Set up image capture listener, which is triggered after photo has // been taken. cameraController.takePicture( outputOptions, ContextCompat.getMainExecutor(this), object : ImageCapture.OnImageSavedCallback { override fun onError(e: ImageCaptureException) { Log.e(TAG, "photo capture failed", e) } override fun onImageSaved( output: ImageCapture.OutputFileResults ) { val msg = "Photo capture succeeded: ${output.savedUri}" Toast.makeText(baseContext, msg, Toast.LENGTH_SHORT).show() Log.d(TAG, msg) } } ) }
CameraX: CameraProvider
การถ่ายรูปด้วย CameraProvider จะทำงานในลักษณะเดียวกับ CameraController แต่ก่อนอื่นคุณต้องสร้างและเชื่อมโยง ImageCapture
UseCase เพื่อให้มีออบเจ็กต์ที่จะเรียกใช้ takePicture()
// CameraX: create and bind an ImageCapture UseCase. // Make a reference to the ImageCapture UseCase at a scope that can be accessed // throughout the camera logic in your app. private var imageCapture: ImageCapture? = null ... // Create an ImageCapture instance (can be added with other // UseCase definitions). imageCapture = ImageCapture.Builder().build() ... // Bind UseCases to camera (adding imageCapture along with preview here, but // preview is not required to use imageCapture). This function returns a camera // object which can be used to perform operations like zoom, flash, and focus. var camera = cameraProvider.bindToLifecycle( this, cameraSelector, preview, imageCapture)
จากนั้นเมื่อใดก็ตามที่ต้องการถ่ายรูป คุณก็เรียกใช้
ImageCapture.takePicture()ได้ ดูโค้ด CameraController ในส่วนนี้
เพื่อดูตัวอย่างฟังก์ชัน takePhoto() แบบเต็ม
// CameraX: define a function that uses CameraController to take a photo. private fun takePhoto() { // Get a stable reference of the modifiable ImageCapture UseCase. val imageCapture = imageCapture ?: return ... // Call takePicture on imageCapture instance. imageCapture.takePicture( ... ) }
การบันทึกวิดีโอ
การบันทึกวิดีโอมีความซับซ้อนกว่าสถานการณ์ที่เราพิจารณามาจนถึงตอนนี้มาก คุณต้องตั้งค่าแต่ละส่วนของกระบวนการอย่างถูกต้อง โดยปกติแล้วจะต้องตั้งค่าตามลำดับที่เฉพาะเจาะจง นอกจากนี้ คุณอาจต้องยืนยันว่าวิดีโอและเสียง ซิงค์กัน หรือจัดการกับความไม่สอดคล้องกันของอุปกรณ์เพิ่มเติม
ดังที่คุณจะเห็น CameraX จะจัดการความซับซ้อนนี้ให้คุณอีกครั้ง
Camera1
การจับภาพวิดีโอโดยใช้ Camera1 ต้องมีการจัดการ Camera และ
MediaRecorder อย่างรอบคอบ และต้องเรียกใช้เมธอดตามลำดับที่เฉพาะเจาะจง คุณต้องทำตามลำดับนี้เพื่อให้แอปพลิเคชันทำงานได้อย่างถูกต้อง
- เปิดกล้อง
- เตรียมและเริ่มแสดงตัวอย่าง (หากแอปแสดงวิดีโอที่กำลังบันทึก ซึ่งมักจะเป็นเช่นนั้น)
- ปลดล็อกกล้องเพื่อให้
MediaRecorderใช้งานได้โดยโทรหาCamera.unlock() - กำหนดค่าการบันทึกโดยเรียกใช้เมธอดต่อไปนี้ใน
MediaRecorder- เชื่อมต่ออินสแตนซ์
CameraกับsetCamera(camera) - โทร
setAudioSource(MediaRecorder.AudioSource.CAMCORDER) - โทร
setVideoSource(MediaRecorder.VideoSource.CAMERA) - โทรหา
setProfile(CamcorderProfile.get(CamcorderProfile.QUALITY_1080P))เพื่อตั้งค่าคุณภาพ ดูตัวเลือกคุณภาพทั้งหมดได้ที่CamcorderProfile - โทร
setOutputFile(getOutputMediaFile(MEDIA_TYPE_VIDEO).toString()) - หากแอปมีตัวอย่างวิดีโอ ให้เรียกใช้
setPreviewDisplay(preview?.holder?.surface) - โทร
setOutputFormat(MediaRecorder.OutputFormat.MPEG_4) - โทร
setAudioEncoder(MediaRecorder.AudioEncoder.DEFAULT) - โทร
setVideoEncoder(MediaRecorder.VideoEncoder.DEFAULT) - โทรหา
prepare()เพื่อกำหนดค่าMediaRecorderให้เสร็จสมบูรณ์
- เชื่อมต่ออินสแตนซ์
- หากต้องการเริ่มบันทึก ให้โทรไปที่
MediaRecorder.start() - หากต้องการหยุดบันทึก ให้เรียกใช้เมธอดต่อไปนี้ โปรดทำตามลำดับต่อไปนี้
- โทร
MediaRecorder.stop() - (ไม่บังคับ) นำการกำหนดค่า
MediaRecorderปัจจุบันออกโดยเรียกใช้MediaRecorder.reset() - โทร
MediaRecorder.release() - ล็อกกล้องเพื่อให้เซสชัน
MediaRecorderในอนาคตใช้กล้องได้โดย เรียกใช้Camera.lock()
- โทร
- หากต้องการหยุดการแสดงตัวอย่าง ให้โทรหา
Camera.stopPreview() - สุดท้ายนี้ หากต้องการปล่อย
Cameraเพื่อให้กระบวนการอื่นๆ ใช้ได้ ให้เรียกใช้Camera.release()
ขั้นตอนทั้งหมดมีดังนี้
// Camera1: set up a MediaRecorder and a function to start and stop video // recording. // Make a reference to the MediaRecorder at a scope that can be accessed // throughout the camera logic in your app. private var mediaRecorder: MediaRecorder? = null private var isRecording = false ... private fun prepareMediaRecorder(): Boolean { mediaRecorder = MediaRecorder() // Unlock and set camera to MediaRecorder. camera?.unlock() mediaRecorder?.run { setCamera(camera) // Set the audio and video sources. setAudioSource(MediaRecorder.AudioSource.CAMCORDER) setVideoSource(MediaRecorder.VideoSource.CAMERA) // Set a CamcorderProfile (requires API Level 8 or higher). setProfile(CamcorderProfile.get(CamcorderProfile.QUALITY_HIGH)) // Set the output file. setOutputFile(getOutputMediaFile(MEDIA_TYPE_VIDEO).toString()) // Set the preview output. setPreviewDisplay(preview?.holder?.surface) setOutputFormat(MediaRecorder.OutputFormat.MPEG_4) setAudioEncoder(MediaRecorder.AudioEncoder.DEFAULT) setVideoEncoder(MediaRecorder.VideoEncoder.DEFAULT) // Prepare configured MediaRecorder. return try { prepare() true } catch (e: IllegalStateException) { Log.d(TAG, "preparing MediaRecorder failed", e) releaseMediaRecorder() false } catch (e: IOException) { Log.d(TAG, "setting MediaRecorder file failed", e) releaseMediaRecorder() false } } return false } private fun releaseMediaRecorder() { mediaRecorder?.reset() mediaRecorder?.release() mediaRecorder = null camera?.lock() } private fun startStopVideo() { if (isRecording) { // Stop recording and release camera. mediaRecorder?.stop() releaseMediaRecorder() camera?.lock() isRecording = false // This is a good place to inform user that video recording has stopped. } else { // Initialize video camera. if (prepareVideoRecorder()) { // Camera is available and unlocked, MediaRecorder is prepared, now // you can start recording. mediaRecorder?.start() isRecording = true // This is a good place to inform the user that recording has // started. } else { // Prepare didn't work, release the camera. releaseMediaRecorder() // Inform user here. } } }
CameraX: CameraController
CameraController ของ CameraX ช่วยให้คุณเปิด/ปิดImageCapture
VideoCapture และ ImageAnalysis UseCase ได้อย่างอิสระ ตราบใดที่
รายการ UseCase สามารถใช้พร้อมกันได้ ImageCapture และ
ImageAnalysis UseCase จะเปิดใช้โดยค่าเริ่มต้น คุณจึงไม่จำเป็นต้อง
เรียกใช้ setEnabledUseCases() เพื่อถ่ายรูป
หากต้องการใช้ CameraController สำหรับการบันทึกวิดีโอ คุณต้องใช้
setEnabledUseCases() ก่อนเพื่ออนุญาตให้ VideoCapture UseCase
// CameraX: Enable VideoCapture UseCase on CameraController. cameraController.setEnabledUseCases(VIDEO_CAPTURE);
เมื่อต้องการเริ่มบันทึกวิดีโอ คุณสามารถเรียกใช้ฟังก์ชัน
CameraController.startRecording() ฟังก์ชันนี้จะบันทึกวิดีโอที่บันทึกไว้ลงใน File ดังตัวอย่างต่อไปนี้
นอกจากนี้ คุณต้องส่ง Executor และคลาสที่ใช้ OnVideoSavedCallback เพื่อจัดการ Callback ที่สำเร็จและข้อผิดพลาด เมื่อต้องการให้
การบันทึกสิ้นสุด ให้เรียกใช้ CameraController.stopRecording()
หมายเหตุ: หากใช้ CameraX 1.3.0-alpha02 ขึ้นไป จะมีพารามิเตอร์
AudioConfig เพิ่มเติมที่ช่วยให้คุณเปิดหรือปิดใช้เสียงที่บันทึกไว้ใน
วิดีโอได้ คุณต้องมีสิทธิ์เข้าถึงไมโครโฟนจึงจะเปิดใช้การบันทึกเสียงได้ นอกจากนี้ ระบบยังนำเมธอด stopRecording() ออกใน
1.3.0-alpha02 และ startRecording() จะแสดงออบเจ็กต์ Recording ที่ใช้สำหรับหยุดชั่วคราว เล่นต่อ และหยุดการบันทึกวิดีโอได้
// CameraX: implement video capture with CameraController. private val FILENAME_FORMAT = "yyyy-MM-dd-HH-mm-ss-SSS" // Define a VideoSaveCallback class for handling success and error states. class VideoSaveCallback : OnVideoSavedCallback { override fun onVideoSaved(outputFileResults: OutputFileResults) { val msg = "Video capture succeeded: ${outputFileResults.savedUri}" Toast.makeText(baseContext, msg, Toast.LENGTH_SHORT).show() Log.d(TAG, msg) } override fun onError(videoCaptureError: Int, message: String, cause: Throwable?) { Log.d(TAG, "error saving video: $message", cause) } } private fun startStopVideo() { if (cameraController.isRecording()) { // Stop the current recording session. cameraController.stopRecording() return } // Define the File options for saving the video. val name = SimpleDateFormat(FILENAME_FORMAT, Locale.US) .format(System.currentTimeMillis()) val outputFileOptions = OutputFileOptions .Builder(File(this.filesDir, name)) .build() // Call startRecording on the CameraController. cameraController.startRecording( outputFileOptions, ContextCompat.getMainExecutor(this), VideoSaveCallback() ) }
CameraX: CameraProvider
หากใช้ CameraProvider คุณต้องสร้าง VideoCapture
UseCase และส่งออบเจ็กต์ Recorder ใน Recorder.Builder คุณสามารถ
ตั้งค่าคุณภาพวิดีโอและเลือกFallbackStrategy (ไม่บังคับ) ซึ่งจะจัดการ
ในกรณีที่อุปกรณ์ไม่เป็นไปตามข้อกำหนดด้านคุณภาพที่คุณต้องการ จากนั้นผูกอินสแตนซ์ VideoCapture กับ CameraProvider ด้วย UseCase อื่นๆ
// CameraX: create and bind a VideoCapture UseCase with CameraProvider. // Make a reference to the VideoCapture UseCase and Recording at a // scope that can be accessed throughout the camera logic in your app. private lateinit var videoCapture: VideoCaptureprivate var recording: Recording? = null ... // Create a Recorder instance to set on a VideoCapture instance (can be // added with other UseCase definitions). val recorder = Recorder.Builder() .setQualitySelector(QualitySelector.from(Quality.FHD)) .build() videoCapture = VideoCapture.withOutput(recorder) ... // Bind UseCases to camera (adding videoCapture along with preview here, but // preview is not required to use videoCapture). This function returns a camera // object which can be used to perform operations like zoom, flash, and focus. var camera = cameraProvider.bindToLifecycle( this, cameraSelector, preview, videoCapture)
ตอนนี้คุณเข้าถึง Recorder ได้ในพร็อพเพอร์ตี้ videoCapture.output
Recorder สามารถเริ่มบันทึกวิดีโอที่บันทึกลงใน File,
ParcelFileDescriptor หรือ MediaStore ตัวอย่างนี้ใช้ MediaStore
ใน Recorder มีหลายวิธีในการเรียกใช้เพื่อเตรียมใช้งาน โทร
prepareRecording()เพื่อตั้งค่าMediaStoreตัวเลือกเอาต์พุต หากแอปมี
สิทธิ์ใช้ไมโครโฟนของอุปกรณ์ ให้เรียกใช้ withAudioEnabled() ด้วย
จากนั้นโทรหา start() เพื่อเริ่มบันทึก โดยส่งบริบทและ Consumer<VideoRecordEvent> Listener เหตุการณ์ เพื่อจัดการเหตุการณ์การบันทึกวิดีโอ หากสำเร็จ คุณจะใช้ Recording ที่ส่งคืนมาเพื่อหยุดชั่วคราว กลับมาทำงานต่อ หรือหยุด
การบันทึกได้
// CameraX: implement video capture with CameraProvider. private val FILENAME_FORMAT = "yyyy-MM-dd-HH-mm-ss-SSS" private fun startStopVideo() { val videoCapture = this.videoCapture ?: return if (recording != null) { // Stop the current recording session. recording.stop() recording = null return } // Create and start a new recording session. val name = SimpleDateFormat(FILENAME_FORMAT, Locale.US) .format(System.currentTimeMillis()) val contentValues = ContentValues().apply { put(MediaStore.MediaColumns.DISPLAY_NAME, name) put(MediaStore.MediaColumns.MIME_TYPE, "video/mp4") if (Build.VERSION.SDK_INT > Build.VERSION_CODES.P) { put(MediaStore.Video.Media.RELATIVE_PATH, "Movies/CameraX-Video") } } val mediaStoreOutputOptions = MediaStoreOutputOptions .Builder(contentResolver, MediaStore.Video.Media.EXTERNAL_CONTENT_URI) .setContentValues(contentValues) .build() recording = videoCapture.output .prepareRecording(this, mediaStoreOutputOptions) .withAudioEnabled() .start(ContextCompat.getMainExecutor(this)) { recordEvent -> when(recordEvent) { is VideoRecordEvent.Start -> { viewBinding.videoCaptureButton.apply { text = getString(R.string.stop_capture) isEnabled = true } } is VideoRecordEvent.Finalize -> { if (!recordEvent.hasError()) { val msg = "Video capture succeeded: " + "${recordEvent.outputResults.outputUri}" Toast.makeText( baseContext, msg, Toast.LENGTH_SHORT ).show() Log.d(TAG, msg) } else { recording?.close() recording = null Log.e(TAG, "video capture ends with error", recordEvent.error) } viewBinding.videoCaptureButton.apply { text = getString(R.string.start_capture) isEnabled = true } } } } }
แหล่งข้อมูลเพิ่มเติม
เรามีแอป CameraX ที่สมบูรณ์หลายแอปในที่เก็บตัวอย่างกล้องใน GitHub ตัวอย่างเหล่านี้แสดงให้เห็นว่าสถานการณ์ในคู่มือนี้เหมาะกับแอป Android ที่สมบูรณ์อย่างไร
หากต้องการความช่วยเหลือเพิ่มเติมในการย้ายข้อมูลไปยัง CameraX หรือมีข้อสงสัย เกี่ยวกับชุด Android Camera API โปรดติดต่อเราในกลุ่มสนทนา CameraX