ย้ายข้อมูล Camera1 ไปยัง CameraX

หากแอปของคุณใช้คลาส Camera เดิม ("Camera1") ซึ่งเลิกใช้งานแล้วตั้งแต่ Android 5.0 (ระดับ API 21) เราขอแนะนำอย่างยิ่งให้อัปเดตเป็น API กล้อง Android ที่ทันสมัย Android มี CameraX (API กล้อง Jetpack ที่มีประสิทธิภาพและได้มาตรฐาน) และ Camera2 (API เฟรมเวิร์กระดับต่ำ) ในกรณีส่วนใหญ่ เราขอแนะนำให้ย้ายข้อมูลแอปไปยัง CameraX โดยมีเหตุผลดังต่อไปนี้

  • ใช้งานง่าย: CameraX จะจัดการรายละเอียดระดับต่ำ คุณจึงไม่ต้องมุ่งเน้นที่การสร้างประสบการณ์การใช้งานกล้องตั้งแต่ต้น และหันไปมุ่งเน้นที่การสร้างความแตกต่างให้แอปได้มากขึ้น
  • CameraX จัดการการกระจาย Fragment ให้คุณ: CameraX ช่วยลดค่าใช้จ่ายในการบำรุงรักษาระยะยาวและโค้ดเฉพาะอุปกรณ์ ซึ่งจะมอบประสบการณ์การใช้งานที่มีคุณภาพสูงขึ้นให้แก่ผู้ใช้ ดูข้อมูลเพิ่มเติมเกี่ยวกับเรื่องนี้ได้ที่บล็อกโพสต์ความเข้ากันได้ของอุปกรณ์ที่ดีขึ้นด้วย CameraX
  • ความสามารถขั้นสูง: CameraX ได้รับการออกแบบมาอย่างพิถีพิถันเพื่อให้รวมฟังก์ชันการทำงานขั้นสูงเข้ากับแอปของคุณได้ง่ายๆ ตัวอย่างเช่น คุณสามารถใช้โบเก้, รีทัชใบหน้า, HDR (High Dynamic Range) และโหมดจับภาพกลางคืนที่เพิ่มความสว่างในสภาวะแสงน้อยกับรูปภาพได้อย่างง่ายดายด้วยส่วนขยาย CameraX
  • ความสามารถในการอัปเดต: Android จะเปิดตัวความสามารถใหม่ๆ และการแก้ไขข้อบกพร่องใน CameraX ตลอดทั้งปี การย้ายข้อมูลไปยัง CameraX จะช่วยให้แอปของคุณได้รับเทคโนโลยีกล้อง Android ล่าสุดพร้อมกับ CameraX แต่ละรุ่น ไม่ใช่แค่ในรุ่น Android เวอร์ชันประจำปี

ในคู่มือนี้ คุณจะเห็นสถานการณ์ที่พบบ่อยสำหรับแอปพลิเคชันกล้อง แต่ละสถานการณ์จะมีการติดตั้งใช้งาน Camera1 และการติดตั้งใช้งาน CameraX เพื่อเปรียบเทียบ

เมื่อพูดถึงการย้ายข้อมูล บางครั้งคุณอาจต้องการความยืดหยุ่นเพิ่มเติมเพื่อผสานรวมกับฐานของโค้ดที่มีอยู่ โค้ด CameraX ทั้งหมดในคู่มือนี้มีการใช้งาน CameraController ซึ่งเหมาะอย่างยิ่งหากคุณต้องการวิธีใช้ CameraX ที่ตรงไปตรงมามากขึ้น และยังมีการใช้งาน CameraProvider ซึ่งเหมาะอย่างยิ่งหากคุณต้องการความยืดหยุ่นมากขึ้น ประโยชน์ของแต่ละตัวเลือกมีดังนี้ เพื่อช่วยให้คุณตัดสินใจได้ว่าตัวเลือกใดเหมาะกับคุณ

CameraController

CameraProvider

ต้องใช้รหัสการตั้งค่าเพียงเล็กน้อย ช่วยให้ควบคุมได้มากขึ้น
การอนุญาตให้ CameraX จัดการกระบวนการตั้งค่ามากขึ้นหมายความว่าฟังก์ชันต่างๆ เช่น แตะเพื่อโฟกัสและบีบเพื่อซูมจะทำงานโดยอัตโนมัติ เนื่องจากนักพัฒนาแอปเป็นผู้จัดการการตั้งค่า จึงมีโอกาสมากขึ้นในการปรับแต่งการกำหนดค่า เช่น การเปิดใช้การหมุนรูปภาพเอาต์พุตหรือการตั้งค่ารูปแบบรูปภาพเอาต์พุตใน ImageAnalysis
การกำหนดให้ใช้ PreviewView สำหรับตัวอย่างภาพจากกล้องช่วยให้ CameraX สามารถผสานรวมแบบครบวงจรได้อย่างราบรื่น เช่นเดียวกับการผสานรวม ML Kit ของเราซึ่งสามารถแมปพิกัดผลลัพธ์ของโมเดล ML (เช่น กรอบล้อมรอบใบหน้า) กับพิกัดตัวอย่างภาพได้โดยตรง ความสามารถในการใช้ `Surface` ที่กำหนดเองสำหรับตัวอย่างภาพจากกล้องช่วยให้มีความยืดหยุ่นมากขึ้น เช่น การใช้โค้ด `Surface` ที่มีอยู่ซึ่งอาจเป็นอินพุตสำหรับส่วนอื่นๆ ของแอป

หากคุณติดขัดขณะพยายามย้ายข้อมูล โปรดติดต่อเราที่กลุ่มสนทนา CameraX

สิ่งที่ต้องพิจารณาก่อนย้ายข้อมูล

เปรียบเทียบการใช้งาน CameraX กับ Camera1

แม้ว่าโค้ดอาจดูแตกต่างกัน แต่แนวคิดพื้นฐานใน Camera1 และ CameraX นั้นคล้ายกันมาก CameraX แยกฟังก์ชันการทำงานของกล้องทั่วไปออกเป็น Use Case ด้วยเหตุนี้ งานหลายอย่างที่ Camera1 มอบหมายให้นักพัฒนาแอปจึงได้รับการจัดการโดยอัตโนมัติด้วย CameraX CameraX มี UseCase 4 รายการซึ่งคุณใช้สำหรับงานต่างๆ ของกล้องได้ ได้แก่ Preview, ImageCapture, VideoCapture และ ImageAnalysis

ตัวอย่างหนึ่งที่ CameraX จัดการรายละเอียดระดับต่ำสำหรับนักพัฒนาแอปคือ ViewPort ที่แชร์ระหว่าง UseCase ที่ใช้งานอยู่ วิธีนี้ช่วยให้มั่นใจว่า UseCase ทั้งหมดจะเห็นพิกเซลเดียวกันทุกประการ ใน Camera1 คุณต้องจัดการรายละเอียดเหล่านี้ด้วยตนเอง เนื่องจากสัดส่วนภาพในอุปกรณ์ต่างๆ มีความแตกต่างกัน การจับคู่ตัวอย่างกับสื่อที่บันทึกจึงทำได้ยาก

อีกตัวอย่างหนึ่งคือ CameraX จะจัดการ Callback ของ Lifecycle โดยอัตโนมัติในอินสแตนซ์ Lifecycle ที่คุณระบุ สถาปัตยกรรมนี้ช่วยให้ CameraX จัดการการเชื่อมต่อของแอปกับกล้องตลอดวงจรกิจกรรมของ Android ซึ่งรวมถึงกรณีต่อไปนี้ ได้แก่ การปิดกล้องเมื่อแอปทำงานอยู่เบื้องหลัง การนำตัวอย่างภาพจากกล้องออกเมื่อหน้าจอไม่จำเป็นต้องแสดงอีกต่อไป และการหยุดตัวอย่างภาพจากกล้องชั่วคราวเมื่อกิจกรรมอื่นมีความสำคัญเหนือกว่าในเบื้องหน้า เช่น วิดีโอคอลที่เข้ามา

สุดท้าย CameraX จะจัดการการหมุนและการปรับขนาดโดยที่คุณไม่ต้องเขียนโค้ดเพิ่มเติม ในกรณีของ Activity ที่มีการวางแนวแบบปลดล็อก การตั้งค่า UseCase จะดำเนินการทุกครั้งที่มีการหมุนอุปกรณ์ เนื่องจากระบบจะทำลายและสร้าง Activity ขึ้นมาใหม่เมื่อมีการเปลี่ยนแปลงการวางแนว ซึ่งจะส่งผลให้ UseCases ตั้งค่าการหมุนเป้าหมายให้ตรงกับการวางแนวของจอแสดงผลโดยค่าเริ่มต้นทุกครั้ง อ่านเพิ่มเติมเกี่ยวกับการหมุนใน CameraX

ก่อนที่จะลงรายละเอียด ต่อไปนี้เป็นภาพรวมระดับสูงของ UseCase ของ CameraX และความเกี่ยวข้องของแอป Camera1 (แนวคิด CameraX เป็นสีน้ำเงิน และแนวคิด Camera1 เป็นสีเขียว)

CameraX

การกำหนดค่า CameraController / CameraProvider
↓ ↓ ↓ ↓
แสดงตัวอย่าง ImageCapture VideoCapture ImageAnalysis
⁞ ⁞ ⁞ ⁞
จัดการแพลตฟอร์มตัวอย่างและตั้งค่าในกล้อง ตั้งค่า PictureCallback และเรียกใช้ takePicture() ในกล้อง จัดการการกำหนดค่ากล้องและ MediaRecorder ตามลำดับที่เฉพาะเจาะจง โค้ดการวิเคราะห์ที่กำหนดเองซึ่งสร้างขึ้นบนแพลตฟอร์มเวอร์ชันตัวอย่าง
↑ ↑ ↑ ↑
รหัสเฉพาะอุปกรณ์
↑
การจัดการการหมุนและการปรับขนาดอุปกรณ์
↑
การจัดการเซสชันกล้อง (การเลือกกล้อง, การจัดการวงจร)

Camera1

ความเข้ากันได้และประสิทธิภาพใน CameraX

CameraX รองรับอุปกรณ์ที่ใช้ Android 5.0 (ระดับ API 21) ขึ้นไป ซึ่งคิดเป็นมากกว่า 98% ของอุปกรณ์ Android ที่มีอยู่ CameraX สร้างขึ้นเพื่อจัดการความแตกต่างระหว่างอุปกรณ์โดยอัตโนมัติ ซึ่งช่วยลดความจำเป็นในการใช้โค้ดเฉพาะอุปกรณ์ในแอป นอกจากนี้ เรายังทดสอบอุปกรณ์จริงมากกว่า 150 เครื่องใน Android ทุกเวอร์ชันตั้งแต่ 5.0 ใน CameraX Test Lab คุณดูรายการอุปกรณ์ทั้งหมดใน Test Lab ได้

CameraX ใช้ Executor เพื่อขับเคลื่อนสแต็กกล้อง คุณสามารถตั้งค่า Executor ของคุณเองใน CameraX หากแอปมีข้อกำหนดเกี่ยวกับชุดข้อความที่เฉพาะเจาะจง หากไม่ได้ตั้งค่า CameraX จะสร้างและใช้ Executor ภายในเริ่มต้นที่เพิ่มประสิทธิภาพแล้ว API ของแพลตฟอร์มจำนวนมากที่ใช้สร้าง CameraX ต้องมีการบล็อกการสื่อสารระหว่างโปรเซส (IPC) กับฮาร์ดแวร์ ซึ่งบางครั้งอาจใช้เวลาหลายร้อยมิลลิวินาทีในการตอบสนอง ด้วยเหตุนี้ CameraX จึงเรียกใช้ API เหล่านี้จากเทรดที่ทำงานอยู่เบื้องหลังเท่านั้น ซึ่งช่วยให้มั่นใจได้ว่าเทรดหลักจะไม่ถูกบล็อกและ UI จะยังคงลื่นไหล อ่านข้อมูลเพิ่มเติมเกี่ยวกับชุดข้อความ

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

แนวคิดการพัฒนา Android

คู่มือนี้ถือว่าคุณมีความคุ้นเคยกับการพัฒนา Android โดยทั่วไป นอกเหนือจากพื้นฐานแล้ว ต่อไปนี้เป็นแนวคิด 2-3 ข้อที่ควรทำความเข้าใจก่อนที่จะเริ่มเขียนโค้ดต่อไปนี้

  • การเชื่อมโยงมุมมองจะสร้างคลาสการเชื่อมโยงสำหรับไฟล์เลย์เอาต์ XML ซึ่งช่วยให้คุณอ้างอิงมุมมองในกิจกรรมได้ ดังที่แสดงในข้อมูลโค้ดที่ตามมาหลายรายการ การเชื่อมโยง มุมมองและ findViewById() (วิธีอ้างอิงมุมมองก่อนหน้านี้) มีความแตกต่างกันบางประการ แต่ใน โค้ดต่อไปนี้ คุณควรแทนที่บรรทัดการเชื่อมโยงมุมมองด้วย การเรียก findViewById() ที่คล้ายกันได้
  • โครูทีนแบบอะซิงโครนัสคือรูปแบบการออกแบบการทำงานพร้อมกันที่เพิ่มเข้ามาใน Kotlin 1.3 ซึ่งใช้จัดการเมธอด CameraX ที่แสดงผล ListenableFuture ได้ ซึ่งทำได้ง่ายขึ้นด้วยไลบรารี Concurrent ของ Jetpack ตั้งแต่เวอร์ชัน 1.1.0 เป็นต้นไป วิธีเพิ่ม Coroutine แบบอะซิงโครนัสลงในแอป
    1. เพิ่ม implementation("androidx.concurrent:concurrent-futures-ktx:1.1.0") ลงในไฟล์ Gradle
    2. ใส่โค้ด CameraX ที่แสดงผล ListenableFuture ในบล็อก launch หรือ suspending function
    3. เพิ่มการเรียก await() ไปยังการเรียกใช้ฟังก์ชันที่แสดงผล ListenableFuture
    4. หากต้องการทำความเข้าใจวิธีการทำงานของโครูทีนอย่างละเอียดยิ่งขึ้น โปรดดูคู่มือเริ่มโครูทีน

ย้ายข้อมูลสถานการณ์ที่พบบ่อย

ส่วนนี้จะอธิบายวิธีย้ายข้อมูลสถานการณ์ที่พบบ่อยจาก 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(requireContext()).await()

    // Set up UseCases (more on UseCases in later scenarios).
    var useCases: Array<UseCase> = emptyArray()

    // 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.availableCameraInfos
    .first { cameraInfo ->
        val focalLengths = Camera2CameraInfo.from(cameraInfo)
            .getCameraCharacteristic(
                CameraCharacteristics.LENS_INFO_AVAILABLE_FOCAL_LENGTHS
            )
        return@first checkFocalLength(focalLengths)
    }
val cameraSelector = cameraInfo.getCameraSelector()

กำลังแสดงตัวอย่าง

แอปพลิเคชันกล้องส่วนใหญ่จำเป็นต้องแสดงฟีดกล้องบนหน้าจอในบางจุด เมื่อใช้ Camera1 คุณต้องจัดการ Lifecycle Callback อย่างถูกต้อง และยังต้องกำหนดการหมุนและการปรับขนาดสำหรับตัวอย่างด้วย

นอกจากนี้ ใน Camera1 คุณต้องตัดสินใจว่าจะใช้ TextureView หรือ SurfaceView เป็นพื้นผิวตัวอย่าง ทั้ง 2 ตัวเลือกมีข้อดีข้อเสีย และไม่ว่าจะเลือกตัวเลือกใด Camera1 ก็กำหนดให้คุณต้องจัดการการหมุนและการปรับขนาดอย่างถูกต้อง ในทางกลับกัน PreviewView ของ CameraX มีการใช้งานเบื้องหลังสำหรับทั้ง TextureView และ SurfaceView CameraX จะตัดสินว่าการใช้งานใดดีที่สุดโดยพิจารณาจากปัจจัยต่างๆ เช่น ประเภทอุปกรณ์และเวอร์ชัน Android ที่แอปของคุณทำงานอยู่ หากการติดตั้งใช้งานทั้ง 2 แบบเข้ากันได้ คุณสามารถประกาศค่ากำหนดด้วย PreviewView.ImplementationMode ตัวเลือก COMPATIBLE ใช้ TextureView สำหรับตัวอย่าง และค่า 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 มีดังนี้

  1. ตั้งค่าตัวตรวจจับท่าทางสัมผัสเพื่อจัดการเหตุการณ์การแตะ
  2. เมื่อเกิดเหตุการณ์การแตะ ให้สร้าง MeteringPoint โดยใช้ MeteringPointFactory.createPoint()
  3. สร้าง FocusMeteringAction ด้วย MeteringPoint
  4. เมื่อมีออบเจ็กต์ CameraControl ใน Camera (แสดงผลจาก bindToLifecycle()) ให้เรียก startFocusAndMetering() โดยส่ง FocusMeteringAction
  5. (ไม่บังคับ) ตอบกลับ FocusMeteringResult
  6. ตั้งค่าตัวตรวจจับท่าทางสัมผัสให้ตอบสนองต่อการโต้ตอบแบบสัมผัสใน 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 = GestureDetector(context,
    object : SimpleOnGestureListener() {
        override fun onSingleTapUp(e: MotionEvent): Boolean {
            val previewView = previewView ?: return false
            val camera = camera ?: return false
            val meteringPointFactory = previewView.meteringPointFactory
            val focusPoint = meteringPointFactory.createPoint(e.x, e.y)
            val meteringAction = FocusMeteringAction
                .Builder(focusPoint).build()
            lifecycleScope.launch {
                val focusResult = camera.cameraControl
                    .startFocusAndMetering(meteringAction).await()
                if (!focusResult.isFocusSuccessful()) {
                    Log.d(TAG, "tap-to-focus failed")
                }
            }
            return true
        }
    }
)

// ...

// 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 จะกำหนดไว้ในส่วนนี้ และคุณควรตั้งค่าเป็น Callback สำหรับ Touch 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() และตรวจสอบค่าด้วย Getter 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<ZoomState> { state ->
    val zoomRatio = state.getZoomRatio()
    Log.d(TAG, "ptz-zoom-ratio $zoomRatio")
}

cameraController.getZoomState().observe(this, pinchToZoomStateObserver)

CameraX: CameraProvider

หากต้องการใช้การบีบเพื่อซูมกับ CameraProvider คุณต้องตั้งค่าบางอย่าง หากไม่ได้ใช้ PreviewView คุณต้องปรับตรรกะเพื่อใช้กับ Surface ที่กำหนดเอง

ขั้นตอนเมื่อใช้ PreviewView มีดังนี้

  1. ตั้งค่าตัวตรวจจับท่าทางสัมผัสการปรับขนาดเพื่อจัดการเหตุการณ์การบีบ
  2. รับ ZoomState จากออบเจ็กต์ Camera.CameraInfo โดยระบบจะแสดงผลอินสแตนซ์ Camera เมื่อคุณเรียก bindToLifecycle()
  3. หาก ZoomState มีค่า zoomRatio ให้บันทึกค่านั้นเป็นอัตราส่วนการซูมปัจจุบัน หากไม่มี zoomRatio ใน ZoomState ให้ใช้อัตราการซูมเริ่มต้นของกล้อง (1.0)
  4. นำผลคูณของอัตราส่วนการซูมปัจจุบันกับ scaleFactor เพื่อกำหนดอัตราส่วนการซูมใหม่ แล้วส่งค่าดังกล่าวไปยัง CameraControl.setZoomRatio()
  5. ตั้งค่าตัวตรวจจับท่าทางสัมผัสให้ตอบสนองต่อการโต้ตอบแบบสัมผัสใน 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 : ScaleGestureDetector.SimpleOnScaleGestureListener() {
        override fun onScale(detector: ScaleGestureDetector): Boolean {
            val camera = camera ?: return false
            val zoomState = camera.cameraInfo.zoomState
            val currentZoomRatio: Float = zoomState.value?.zoomRatio ?: 1f
            camera.cameraControl.setZoomRatio(
                detector.scaleFactor * currentZoomRatio
            )
            return true
        }
    }
)

// ...

// 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 เพื่อจัดการข้อมูลกล้องดิบ (ไม่บีบอัด) พารามิเตอร์ที่ 3 คือพารามิเตอร์ที่ใช้ในตัวอย่างนี้ เนื่องจากเป็น 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.
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 อย่างระมัดระวัง และต้องเรียกใช้เมธอดตามลำดับที่เฉพาะเจาะจง คุณต้องทำตามลำดับนี้เพื่อให้แอปพลิเคชันทำงานได้อย่างถูกต้อง

  1. เปิดกล้อง
  2. เตรียมและเริ่มแสดงตัวอย่าง (หากแอปแสดงวิดีโอที่กำลังบันทึก ซึ่งมักจะเป็นเช่นนั้น)
  3. ปลดล็อกกล้องเพื่อให้ MediaRecorder ใช้ได้โดยโทรหา Camera.unlock()
  4. กำหนดค่าการบันทึกโดยเรียกใช้เมธอดเหล่านี้ใน MediaRecorder
    1. เชื่อมต่ออินสแตนซ์ Camera กับ setCamera(camera)
    2. โทร setAudioSource(MediaRecorder.AudioSource.CAMCORDER)
    3. โทร setVideoSource(MediaRecorder.VideoSource.CAMERA)
    4. โทรหา setProfile(CamcorderProfile.get(CamcorderProfile.QUALITY_1080P)) เพื่อตั้งค่าคุณภาพ ดูตัวเลือกคุณภาพทั้งหมดได้ที่ CamcorderProfile
    5. โทร setOutputFile(getOutputMediaFile(MEDIA_TYPE_VIDEO).toString())
    6. หากแอปมีตัวอย่างวิดีโอ ให้เรียกใช้ setPreviewDisplay(preview?.holder?.surface)
    7. โทร setOutputFormat(MediaRecorder.OutputFormat.MPEG_4)
    8. โทร setAudioEncoder(MediaRecorder.AudioEncoder.DEFAULT)
    9. โทร setVideoEncoder(MediaRecorder.VideoEncoder.DEFAULT)
    10. โทรหา prepare() เพื่อกำหนดค่า MediaRecorder ให้เสร็จสมบูรณ์
  5. หากต้องการเริ่มบันทึก ให้โทรหา MediaRecorder.start()
  6. หากต้องการหยุดบันทึก ให้เรียกใช้เมธอดเหล่านี้ ขอย้ำอีกครั้งว่าให้ทำตามลำดับต่อไปนี้
    1. โทร MediaRecorder.stop()
    2. หรือจะนำการกำหนดค่า MediaRecorder ปัจจุบันออกโดยเรียกใช้ MediaRecorder.reset() ก็ได้
    3. โทร MediaRecorder.release()
    4. ล็อกกล้องเพื่อให้เซสชัน MediaRecorder ในอนาคตใช้กล้องได้โดยการเรียกใช้ Camera.lock()
  7. หากต้องการหยุดดูตัวอย่าง ให้เรียกใช้ Camera.stopPreview()
  8. สุดท้าย หากต้องการปล่อย 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 ช่วยให้คุณเปิด/ปิด UseCase ImageCapture, VideoCapture และ ImageAnalysis แยกกันได้ ตราบใดที่รายการ 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.
lateinit var videoCapture: VideoCapture<Recorder>
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() เพื่อเริ่มบันทึก โดยส่งบริบทและ Listener เหตุการณ์ Consumer<VideoRecordEvent> เพื่อจัดการเหตุการณ์การบันทึกวิดีโอ หากสำเร็จ ระบบจะใช้ 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