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

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

  • ใช้งานง่าย: CameraX จะจัดการรายละเอียดระดับล่างเพื่อให้คุณมุ่งเน้นที่การสร้างประสบการณ์การใช้งานกล้องตั้งแต่ต้นน้อยลง และมุ่งเน้นที่การทำให้แอปของคุณโดดเด่นมากขึ้น
  • CameraX จัดการการแยกส่วนให้คุณ: 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 จะแยกฟังก์ชันการทำงานของกล้องทั่วไปออกเป็นกรณีการใช้งาน ด้วยเหตุนี้ 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() ในกล้อง จัดการการกำหนดค่ากล้องและ MediaRecorder ตามลำดับที่เจาะจง โค้ดการวิเคราะห์ที่กําหนดเองซึ่งสร้างขึ้นบนพื้นผิวเวอร์ชันตัวอย่าง
รหัสเฉพาะอุปกรณ์
การจัดการการหมุนและการปรับขนาดอุปกรณ์
การจัดการเซสชันกล้อง (การเลือกกล้อง การจัดการวงจรการใช้งาน)

Camera1

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

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

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

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

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

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

  • การเชื่อมโยงมุมมองจะสร้างคลาสการเชื่อมโยงสำหรับไฟล์เลย์เอาต์ XML ซึ่งช่วยให้คุณอ้างอิงมุมมองในกิจกรรมได้อย่างง่ายดาย ดังที่ทำในตัวอย่างโค้ดหลายรายการด้านล่าง การเชื่อมโยงข้อมูลกับวิวและ findViewById() (วิธีอ้างอิงวิวก่อนหน้านี้) มีความแตกต่างกันอยู่บ้าง แต่ในโค้ดด้านล่าง คุณควรแทนที่บรรทัดการเชื่อมโยงข้อมูลกับวิวด้วยการเรียกใช้ findViewById() ที่คล้ายกัน
  • Coroutine แบบไม่พร้อมกันคือรูปแบบการออกแบบแบบพร้อมกันที่เพิ่มเข้ามาใน Kotlin 1.3 ซึ่งสามารถใช้เพื่อจัดการเมธอด CameraX ที่แสดงผล ListenableFuture ซึ่งทำได้ง่ายขึ้นด้วยไลบรารี Concurrent ของ Jetpack เวอร์ชัน 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 "Android development concepts"
// section above.
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 (หากเป็นไปได้)

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 "Android development concepts"
// section above.
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 ถึง 1, 000 และใช้เพื่อจัดลําดับความสําคัญของโฟกัส 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() และตรวจสอบค่าด้วยตัวรับค่าที่เกี่ยวข้อง 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. ใช้ MeteringPoint เพื่อสร้าง FocusMeteringAction
  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
// "Android development concepts" section above.
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-zooom 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() เนื่องจากตัวรับฟังการสัมผัสบนแพลตฟอร์มแสดงตัวอย่างจะเรียกเหตุการณ์อย่างต่อเนื่องเมื่อเกิดท่าทางสัมผัสการบีบนิ้วขึ้น จึงอัปเดตระดับการซูมทันทีทุกครั้ง คลาส ZoomTouchListener ได้รับการกําหนดไว้ด้านล่าง และควรตั้งค่าเป็นคอลแบ็กสําหรับโปรแกรมฟังการสัมผัสของพื้นผิวแสดงตัวอย่าง

// 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-zooom 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.
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() ดูตัวอย่างฟังก์ชัน takePhoto() แบบเต็มได้ที่โค้ด CameraController ในส่วนนี้

// 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 เพื่อจัดการการเรียกกลับสําหรับความสําเร็จและข้อผิดพลาด เมื่อต้องการสิ้นสุดการบันทึก ให้โทรไปที่ 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