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

หากแอปใช้คลาส 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 เป็นต้นไป วิธีเพิ่มโคโรทีนแบบอะซิงโครนัส ลงในแอป
    1. เพิ่ม implementation("androidx.concurrent:concurrent-futures-ktx:1.1.0") ลงในไฟล์ Gradle
    2. ใส่โค้ด CameraX ที่แสดงผล ListenableFuture ในบล็อก launch หรือฟังก์ชันที่ระงับ
    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(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 มีดังนี้

  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 = 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 มีดังนี้

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

  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 ช่วยให้คุณเปิด/ปิด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: VideoCapture
private 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