Di chuyển Camera1 sang CameraX

Nếu ứng dụng của bạn dùng lớp Camera gốc ("Camera1"), lớp này không được dùng nữa kể từ Android 5.0 (API cấp 21), thì bạn nên cập nhật lên API camera Android hiện đại. Android cung cấp CameraX (API camera Jetpack chuẩn và mạnh mẽ) và Camera2 (API khung cấp thấp). Trong phần lớn trường hợp, bạn nên di chuyển ứng dụng của mình sang CameraX. Dưới đây là lý do:

  • Dễ sử dụng: CameraX xử lý các chi tiết cấp thấp để bạn không phải quá tập trung vào việc xây dựng trải nghiệm máy ảnh từ đầu, trong khi tập trung nhiều hơn nữa vào việc phân biệt ứng dụng của mình.
  • CameraX xử lý tình trạng phân mảnh cho bạn: CameraX giảm chi phí bảo trì dài hạn và mã dành riêng cho thiết bị, mang lại trải nghiệm chất lượng cao hơn cho người dùng. Để biết thêm về điều này, hãy xem bài đăng Better Device Compatibility with CameraX (Cải thiện khả năng tương thích của thiết bị với CameraX) trên blog.
  • Tính năng nâng cao: CameraX được thiết kế cẩn thận khiến chức năng nâng cao trở nên đơn giản để tích hợp vào ứng dụng của bạn. Ví dụ: bạn có thể dễ dàng áp dụng hiệu ứng Bokeh, Làm đẹp khuôn mặt, HDR (Dải động cao) và chế độ chụp ban đêm với khả năng làm sáng khi ở điều kiện ánh sáng yếu cho ảnh của bạn với tiện ích của CameraX.
  • Khả năng cập nhật: Android phát hành các tính năng mới và bản sửa lỗi cho CameraX suốt cả năm. Bằng cách chuyển sang CameraX, ứng dụng của bạn sẽ nhận được công nghệ mới nhất cho máy ảnh Android với mỗi bản phát hành của CameraX, chứ không chỉ trên các bản phát hành phiên bản Android hằng năm.

Trong hướng dẫn này, bạn sẽ thấy các trường hợp phổ biến cho ứng dụng máy ảnh. Mỗi tình huống sẽ bao gồm cách triển khai Camera1 và CameraX để so sánh.

Trong quá trình di chuyển, đôi khi bạn cần thêm sự linh hoạt để tích hợp với cơ sở mã hiện có. Tất cả mã CameraX trong hướng dẫn này đều có cách triển khai CameraController phù hợp nếu bạn muốn có cách sử dụng CameraX đơn giản nhất cũng như cách triển khai CameraProvider phù hợp nếu bạn cần sự linh hoạt hơn. Để giúp bạn quyết định xem cách triển khai nào phù hợp với mình, sau đây là các lợi ích của từng cách:

CameraController

CameraProvider

Yêu cầu ít mã thiết lập Cho phép có nhiều quyền kiểm soát hơn
Việc cho phép CameraX xử lý nhiều quy trình thiết lập hơn có nghĩa là các chức năng như nhấn để lấy nét và chụm để thu phóng sẽ tự động hoạt động Vì nhà phát triển ứng dụng sẽ xử lý việc thiết lập nên sẽ có nhiều cơ hội hơn để tuỳ chỉnh cấu hình, chẳng hạn như bật tính năng xoay hình ảnh đầu ra hoặc đặt định dạng hình ảnh đầu ra trong ImageAnalysis
Việc yêu cầu PreviewView cho bản xem trước của máy ảnh cho phép CameraX tích hợp hai đầu liền mạch, như trong quá trình tích hợp Bộ công cụ học máy của chúng tôi. Quá trình này có thể liên kết toạ độ kết quả mô hình học máy (như hộp giới hạn khuôn mặt) trực tiếp vào toạ độ bản xem trước Khả năng sử dụng `Surface` tuỳ chỉnh cho bản xem trước của máy ảnh giúp cải thiện tính linh hoạt, chẳng hạn như sử dụng mã `Surface` hiện có, đây có thể là dữ liệu đầu vào cho các phần khác của ứng dụng

Nếu bạn gặp khó khăn khi di chuyển, hãy liên hệ với chúng tôi qua Nhóm thảo luận về CameraX.

Trước khi bạn di chuyển

So sánh mức sử dụng CameraX với Camera1

Mặc dù mã có thể khác nhau, nhưng các khái niệm cơ bản trong Camera1 và CameraX rất giống nhau. CameraX tóm tắt chức năng phổ biến của máy ảnh thành các trường hợp sử dụng, do đó, nhiều tác vụ mà nhà phát triển để lại trong Camera1 sẽ do CameraX tự động xử lý. Có 4 UseCase trong CameraX mà bạn có thể dùng cho nhiều nhiệm vụ của máy ảnh: Preview, ImageCapture, VideoCaptureImageAnalysis.

Một ví dụ về việc CameraX xử lý thông tin chi tiết cấp thấp cho nhà phát triển là ViewPort được dùng chung giữa các UseCase đang hoạt động. Điều này đảm bảo rằng tất cả các UseCase đều thấy chính xác cùng một điểm ảnh. Trong Camera1, bạn phải tự quản lý các thông tin chi tiết này và cung cấp sự thay đổi về tỷ lệ khung hình trên các màn hình và cảm biến của máy ảnh trên thiết bị. Tuy nhiên, việc đảm bảo bản xem trước khớp với ảnh và video đã chụp có thể rất khó khăn.

Một ví dụ khác là CameraX tự động xử lý các lệnh gọi lại Lifecycle trên thực thể Lifecycle mà bạn truyền. Điều này có nghĩa là CameraX xử lý kết nối của ứng dụng với máy ảnh trong toàn bộ vòng đời hoạt động trên Android, bao gồm các trường hợp sau: đóng máy ảnh khi ứng dụng chuyển đến chế độ nền; xoá bản xem trước của máy ảnh khi màn hình không còn yêu cầu hiển thị bản xem trước; và tạm dừng bản xem trước của máy ảnh khi một hoạt động khác ưu tiên nền trước, chẳng hạn như cuộc gọi video đến.

Cuối cùng, CameraX xử lý việc xoay và chia tỷ lệ mà bạn không cần thêm mã nào. Trong trường hợp Activity có hướng được mở khoá, việc thiết lập UseCase sẽ được thực hiện mỗi khi xoay thiết bị, vì hệ thống sẽ huỷ và tái tạo Activity khi hướng thay đổi. Kết quả là lần nào UseCases cũng được đặt thành chế độ xoay mục tiêu cho phù hợp với hướng của màn hình theo mặc định. Đọc thêm về chế độ xoay trong CameraX.

Trước khi đi vào chi tiết, dưới đây là thông tin tổng quan về UseCase của CameraX và mối liên hệ của một ứng dụng Camera1. (Các khái niệm của CameraX đều có màu xanh dương và khái niệm của Camera1 có màu xanh lục.)

CameraX

Cấu hình CameraController/CameraProvider
Xem trước ImageCapture Quay video ImageAnalysis
Quản lý nền tảng xem trước và thiết lập trên máy ảnh Đặt PictureCallback và gọi takePicture() trên máy ảnh Quản lý cấu hình của máy ảnh và MediaRecorder theo thứ tự cụ thể Mã phân tích tuỳ chỉnh được tạo dựa trên nền tảng xem trước
Mã dành riêng cho thiết bị
Quản lý việc điều chỉnh theo lỷ lệ và xoay thiết bị
Quản lý phiên máy ảnh (Lựa chọn máy ảnh, Quản lý vòng đời)

Camera1

Khả năng tương thích và hiệu suất khi dùng CameraX

CameraX hỗ trợ các thiết bị chạy Android 5.0 (API cấp 21) trở lên. Số thiết bị này tương đương với hơn 98% số thiết bị Android hiện có. CameraX được xây dựng để tự động xử lý điểm khác biệt giữa các thiết bị, giúp giảm nhu cầu sử dụng mã dành riêng cho thiết bị trong ứng dụng của bạn. Hơn nữa, chúng tôi cũng kiểm thử hơn 150 thiết bị thực trên tất cả các phiên bản Android kể từ phiên bản 5.0 trong Phòng thử nghiệm CameraX. Bạn có thể xem lại danh sách đầy đủ các thiết bị hiện có trong Phòng thử nghiệm.

CameraX sử dụng Executor để điều khiển ngăn xếp máy ảnh. Bạn có thể đặt trình thực thi của mình trên CameraX nếu ứng dụng của bạn có các yêu cầu cụ thể về việc phân luồng. Nếu không, CameraX sẽ tạo và sử dụng Executor nội bộ mặc định được tối ưu hoá. Nhiều API nền tảng là cơ sở CameraX được xây dựng yêu cầu chặn giao tiếp liên quy trình (IPC) với phần cứng đôi khi có thể mất hàng trăm mili giây để phản hồi. Vì lý do này, CameraX chỉ gọi những API này từ các luồng trong nền, giúp đảm bảo luồng chính không bị chặn và giao diện người dùng vẫn hiển thị linh hoạt. Đọc thêm về luồng.

Nếu thị trường mục tiêu cho ứng dụng của bạn có thiết bị cấp thấp, thì CameraX sẽ cung cấp một cách để giảm thời gian thiết lập bằng trình giới hạn máy ảnh. Vì quá trình kết nối với các thành phần phần cứng có thể làm tiêu hao nhiều thời gian, đặc biệt là trên các thiết bị cấp thấp, nên bạn có thể chỉ định nhóm máy ảnh mà ứng dụng của bạn cần. CameraX chỉ kết nối với các máy ảnh này trong quá trình thiết lập. Ví dụ: Nếu chỉ sử dụng máy ảnh mặt sau, thì ứng dụng có thể thiết lập cấu hình này bằng DEFAULT_BACK_CAMERA, sau đó CameraX sẽ tránh khởi chạy máy ảnh mặt trước để giảm độ trễ.

Khái niệm phát triển Android

Hướng dẫn này giả định rằng bạn đã làm quen với quá trình phát triển Android. Ngoài những khái niệm cơ bản, dưới đây là một số khái niệm hữu ích cần hiểu trước khi chuyển đến mã bên dưới:

Di chuyển các trường hợp phổ biến

Phần này giải thích cách di chuyển các trường hợp phổ biến từ Camera1 sang CameraX. Mỗi tình huống bao gồm cách triển khai Camera1, cách triển khai CameraProvider CameraX và cách triển khai CameraController CameraX.

Chọn camera

Trong ứng dụng máy ảnh, một trong những điều đầu tiên bạn nên cung cấp đó là cách chọn nhiều loại máy ảnh.

Camera1

Trong Camera1, bạn có thể gọi Camera.open() mà không cần có tham số để mở máy ảnh mặt sau đầu tiên hoặc bạn có thể truyền mã nhận dạng bằng số nguyên cho máy ảnh mà bạn muốn mở. Sau đây là một ví dụ về cách thực hiện:

// 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

Trong CameraX, việc lựa chọn máy ảnh do lớp CameraSelector xử lý. CameraX giúp bạn dễ dàng sử dụng máy ảnh mặc định. Bạn có thể chỉ định xem bạn muốn dùng camera trước mặc định hay camera sau mặc định. Hơn nữa, đối tượng CameraControl của CameraX cho phép bạn dễ dàng đặt mức thu phóng cho ứng dụng, vì vậy, nếu ứng dụng đang chạy trên một thiết bị hỗ trợ máy ảnh logic, thì máy ảnh sẽ chuyển sang ống kính thích hợp.

Dưới đây là mã của CameraX để sử dụng camera sau mặc định bằng 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

Dưới đây là ví dụ về cách chọn camera trước mặc định bằng CameraProvider (bạn có thể sử dụng camera trước hoặc sau với CameraController hoặc 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()
    }
}

Nếu muốn kiểm soát xem máy ảnh nào được chọn, bạn cũng có thể thực hiện việc này trong CameraX nếu sử dụng CameraProvider bằng cách gọi getAvailableCameraInfos(). Việc này sẽ cung cấp đối tượng CameraInfo cho bạn để kiểm tra một số thuộc tính của máy ảnh như isFocusMeteringSupported(). Sau đó, bạn có thể chuyển đổi thành CameraSelector để sử dụng như trong các ví dụ ở trên bằng phương thức CameraInfo.getCameraSelector().

Bạn có thể xem thêm thông tin chi tiết về từng máy ảnh bằng cách dùng lớp Camera2CameraInfo. Gọi getCameraCharacteristic() bằng khoá cho dữ liệu máy ảnh mà bạn muốn. Hãy xem lớp CameraCharacteristics để biết danh sách tất cả các khoá mà bạn có thể truy vấn.

Sau đây là ví dụ về cách sử dụng hàm checkFocalLength() tuỳ chỉnh mà bạn có thể tự xác định:

// 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()

Hiển thị bản xem trước

Phần lớn các ứng dụng máy ảnh cần hiển thị nguồn cấp dữ liệu máy ảnh trên màn hình tại điểm nào đó. Với Camera1, bạn cần quản lý chính xác các phương thức gọi lại trong vòng đời cũng như cần xác định việc xoay và điều chỉnh theo tỷ lệ cho bản xem trước của mình.

Ngoài ra, trong Camera1, bạn cần quyết định sử dụng TextureView hay SurfaceView làm nền tảng xem trước. Cả hai tuỳ chọn đều đi kèm với sự đánh đổi và dù trong trường hợp nào, Camera1 sẽ yêu cầu bạn xử lý việc xoay và chia tỷ lệ một cách chính xác. Mặt khác, PreviewView của CameraX có các biện pháp triển khai cơ bản cho cả TextureViewSurfaceView. CameraX sẽ quyết định cách triển khai phù hợp nhất tuỳ thuộc vào các yếu tố như loại thiết bị và phiên bản Android mà ứng dụng đang chạy. Nếu cách triển khai nào cũng đều tương thích, thì bạn có thể khai báo lựa chọn ưu tiên bằng PreviewView.ImplementationMode. Tuỳ chọn COMPATIBLE dùng TextureView cho bản xem trước và giá trị PERFORMANCE sẽ dùng SurfaceView (khi có thể).

Camera1

Để hiển thị bản xem trước, bạn cần viết lớp Preview của riêng mình bằng cách triển khai giao diện android.view.SurfaceHolder.Callback được dùng để truyền dữ liệu hình ảnh từ phần cứng máy ảnh đến ứng dụng. Sau đó, trước khi có thể bắt đầu xem trước hình ảnh trực tiếp, lớp Preview phải được truyền vào đối tượng 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

Trong CameraX, cả bạn và nhà phát triển đều không cần phải quản lý quá nhiều thứ. Nếu sử dụng CameraController, bạn cũng phải sử dụng PreviewView. Điều này có nghĩa là Preview UseCase được ngụ ý, giúp quá trình thiết lập trở nên ít hoạt động hơn:

// 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

Với CameraProvider của CameraX, bạn không nhất thiết phải sử dụng PreviewView, nhưng vẫn giúp đơn giản hoá đáng kể việc thiết lập bản xem trước so với Camera1. Đối với mục đích minh hoạ, ví dụ này sử dụng PreviewView, nhưng bạn có thể viết SurfaceProvider tuỳ chỉnh để truyền vào setSurfaceProvider() nếu có nhu cầu phức tạp hơn.

Ở đây, Preview UseCase không được ngụ ý như với CameraController, vì vậy, bạn cần phải thiết lập:

// 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()
    }
}

Nhấn để lấy nét

Khi bản xem trước của máy ảnh xuất hiện trên màn hình, một tuỳ chọn điều khiển phổ biến là đặt điểm lấy nét khi người dùng nhấn vào bản xem trước.

Camera1

Để triển khai tính năng nhấn để lấy nét trong Camera1, bạn phải tính toán tiêu điểm tối ưu Area để cho biết Camera sẽ cố gắng lấy nét ở đâu. Area này được truyền vào setFocusAreas(). Ngoài ra, bạn phải đặt chế độ lấy nét tương thích trên Camera. Vùng lấy nét chỉ có hiệu lực nếu chế độ lấy nét hiện tại là FOCUS_MODE_AUTO, FOCUS_MODE_MACRO, FOCUS_MODE_CONTINUOUS_VIDEO hoặc FOCUS_MODE_CONTINUOUS_PICTURE.

Mỗi Area là một hình chữ nhật có trọng số được chỉ định. Trọng số là một giá trị nằm trong khoảng từ 1 đến 1000 và dùng để ưu tiên điểm lấy nét Areas nếu bạn đặt nhiều tiêu điểm. Ví dụ này chỉ sử dụng một Area, vì vậy, giá trị trọng số là không đáng kể. Toạ độ của hình chữ nhật nằm trong khoảng từ -1000 đến 1000. Điểm trên bên trái là (-1000, -1000). Điểm dưới bên phải là (1000, 1000). Hướng là tương đối với hướng của cảm biến, tức là những gì cảm biến nhìn thấy. Hướng không bị ảnh hưởng bởi chế độ xoay hoặc phản chiếu của Camera.setDisplayOrientation(), vì vậy, bạn cần chuyển đổi toạ độ sự kiện chạm sang toạ độ cảm biến.

// 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 theo dõi các sự kiện chạm của PreviewView để tự động xử lý thao tác nhấn để lấy nét. Bạn có thể bật và tắt chế độ nhấn để lấy nét bằng setTapToFocusEnabled() và kiểm tra giá trị bằng phương thức getter tương ứng isTapToFocusEnabled().

Phương thức getTapToFocusState() sẽ trả về một đối tượng LiveData để theo dõi các thay đổi đối với trạng thái lấy nét trên 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

Khi sử dụng CameraProvider, bạn cần thực hiện bước thiết lập nào đó để chế độ nhấn để lấy nét hoạt động. Ví dụ này giả định bạn đang sử dụng PreviewView. Nếu không, bạn cần điều chỉnh logic để áp dụng cho Surface tuỳ chỉnh.

Dưới đây là các bước khi sử dụng PreviewView:

  1. Thiết lập trình phát hiện cử chỉ để xử lý các sự kiện nhấn.
  2. Với sự kiện nhấn, hãy tạo MeteringPoint bằng MeteringPointFactory.createPoint().
  3. Với MeteringPoint, tạo FocusMeteringAction.
  4. Với đối tượng CameraControl trên Camera (được trả về từ bindToLifecycle()), hãy gọi startFocusAndMetering() truyền trong FocusMeteringAction.
  5. (Không bắt buộc) Phản hồi FocusMeteringResult.
  6. Đặt trình phát hiện cử chỉ ở chế độ phản hồi với các sự kiện chạm trong 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
}

Chụm để thu phóng

Phóng to và thu nhỏ bản xem trước là một thao tác trực tiếp phổ biến khác đối với bản xem trước của máy ảnh. Khi số lượng máy ảnh trên các thiết bị tăng lên, người dùng cũng kỳ vọng rằng ống kính có tiêu cự tốt nhất sẽ tự động được chọn do việc thu phóng.

Camera1

Có 2 cách để thu phóng bằng Camera1. Phương thức Camera.startSmoothZoom() tạo hiệu ứng ảnh động từ mức thu phóng hiện tại đến mức thu phóng mà bạn chuyển vào. Phương thức Camera.Parameters.setZoom() sẽ chuyển thẳng đến mức thu phóng mà bạn chuyển vào. Trước khi sử dụng một trong hai phương thức này, hãy gọi lần lượt isSmoothZoomSupported() hoặc isZoomSupported() để đảm bảo các phương thức thu phóng liên quan mà bạn cần có trong Máy ảnh.

Để triển khai tính năng chụm để thu phóng, ví dụ này sử dụng setZoom() vì trình nghe thao tác chạm trên nền tảng xem trước liên tục kích hoạt các sự kiện khi cử chỉ chụm xảy ra, vì vậy, tính năng này sẽ cập nhật mức thu phóng ngay lập tức. Lớp ZoomTouchListener được định nghĩa ở bên dưới và lớp này nên được đặt làm lệnh gọi lại cho trình nghe thao tác chạm của giao diện xem trước.

// 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

Tương tự như thao tác nhấn để lấy nét, CameraController theo dõi các sự kiện chạm của PreviewView để tự động xử lý quá trình chụm để thu phóng. Bạn có thể bật và tắt tính năng chụm để thu phóng bằng setPinchToZoomEnabled() và kiểm tra giá trị bằng phương thức getter isPinchToZoomEnabled() tương ứng.

Phương thức getZoomState() sẽ trả về đối tượng LiveData để theo dõi các thay đổi đối với ZoomState trên 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

Để tính năng chụm để thu phóng hoạt động với CameraProvider, bạn bắt buộc phải thực hiện một vài bước thiết lập. Nếu không sử dụng PreviewView, bạn cần điều chỉnh logic để áp dụng cho Surface tuỳ chỉnh.

Dưới đây là các bước khi sử dụng PreviewView:

  1. Thiết lập trình phát hiện cử chỉ điều chỉnh theo tỷ lệ để xử lý các sự kiện chụm.
  2. Lấy ZoomState từ đối tượng Camera.CameraInfo, trong đó thực thể Camera sẽ được trả về khi bạn gọi bindToLifecycle().
  3. Nếu ZoomState có giá trị zoomRatio, hãy lưu giá trị đó dưới dạng tỷ lệ thu phóng hiện tại. Nếu không có zoomRatio trên ZoomState, hãy dùng tỷ lệ thu phóng mặc định của máy ảnh (1.0).
  4. Lấy tích của tỷ lệ thu phóng hiện tại với scaleFactor để xác định tỷ lệ thu phóng mới và truyền tỷ lệ đó vào CameraControl.setZoomRatio().
  5. Đặt trình phát hiện cử chỉ ở chế độ phản hồi với các sự kiện chạm trong 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
}

Chụp ảnh

Phần này trình bày cách kích hoạt tính năng chụp ảnh, cho dù bạn cần thực hiện thao tác này khi nhấn nút chụp, sau khi hết thời gian hẹn giờ hoặc trên bất kỳ sự kiện nào khác mà bạn chọn.

Camera1

Trong Camera1, trước tiên bạn cần xác định Camera.PictureCallback để quản lý dữ liệu hình ảnh khi được yêu cầu. Dưới đây là ví dụ đơn giản về PictureCallback để xử lý dữ liệu hình ảnh 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)
    }
}

Sau đó, bất cứ khi nào bạn muốn chụp ảnh, hãy gọi phương thức takePicture() trên thực thể Camera. Phương thức takePicture() này có 3 tham số khác nhau cho các loại dữ liệu khác nhau. Tham số đầu tiên là dành cho ShutterCallback (không được xác định trong ví dụ này). Tham số thứ hai là dành cho PictureCallback để xử lý dữ liệu thô của máy ảnh (không nén). Tham số thứ ba là tham số mà ví dụ này sử dụng, vì đây là PictureCallback để xử lý dữ liệu hình ảnh JPEG.

// Camera1: call takePicture on Camera instance, passing our PictureCallback.

camera?.takePicture(null, null, picture)

CameraX: CameraController

CameraController của CameraX duy trì sự đơn giản của Camera1 phục vụ việc chụp ảnh bằng cách triển khai phương thức takePicture() của riêng CameraX này. Ở đây, hãy xác định một hàm để định cấu hình mục nhập MediaStore và chụp ảnh để lưu vào đó.

// 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

Việc chụp ảnh bằng CameraProvider hoạt động gần giống như cách thực hiện với CameraController, nhưng trước tiên, bạn cần tạo và liên kết ImageCapture UseCase để có một đối tượng cần gọi takePicture() vào:

// 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)

Sau đó, bất cứ khi nào muốn chụp ảnh, bạn có thể gọi ImageCapture.takePicture(). Hãy xem mã CameraController trong phần này để biết ví dụ đầy đủ về hàm 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(
        ...
    )
}

Quay video

Việc quay video phức tạp hơn nhiều so với các tình huống đã xem xét từ trước đến nay. Bạn phải thiết lập từng phần của quy trình đúng cách, thường là theo một thứ tự cụ thể. Ngoài ra, bạn có thể cần xác minh rằng video và âm thanh được đồng bộ hoá hoặc cần xử lý các vấn đề không nhất quán khác trên thiết bị.

Như bạn thấy, CameraX tiếp tục xử lý những phức tạp này cho bạn.

Camera1

Việc quay video bằng Camera1 yêu cầu bạn phải quản lý cẩn thận CameraMediaRecorder. Ngoài ra, các phương thức phải được gọi theo thứ tự cụ thể. Bạn phải tuân theo thứ tự sau để ứng dụng hoạt động đúng cách:

  1. Mở máy ảnh.
  2. Chuẩn bị và bắt đầu xem trước (nếu ứng dụng của bạn hiển thị video đang được quay, thường là như vậy).
  3. Mở khoá máy ảnh để MediaRecorder sử dụng bằng cách gọi Camera.unlock().
  4. Định cấu hình bản ghi bằng cách gọi các phương thức này trên MediaRecorder:
    1. Kết nối thực thể Camera với setCamera(camera).
    2. Gọi setAudioSource(MediaRecorder.AudioSource.CAMCORDER).
    3. Gọi setVideoSource(MediaRecorder.VideoSource.CAMERA).
    4. Gọi setProfile(CamcorderProfile.get(CamcorderProfile.QUALITY_1080P)) để đặt chất lượng. Xem CamcorderProfile để biết tất cả các tuỳ chọn chất lượng.
    5. Gọi setOutputFile(getOutputMediaFile(MEDIA_TYPE_VIDEO).toString()).
    6. Nếu ứng dụng của bạn có bản xem trước của video, hãy gọi setPreviewDisplay(preview?.holder?.surface).
    7. Gọi setOutputFormat(MediaRecorder.OutputFormat.MPEG_4).
    8. Gọi setAudioEncoder(MediaRecorder.AudioEncoder.DEFAULT).
    9. Gọi setVideoEncoder(MediaRecorder.VideoEncoder.DEFAULT).
    10. Gọi prepare() để hoàn tất cấu hình của MediaRecorder.
  5. Để bắt đầu quay video, hãy gọi MediaRecorder.start().
  6. Để dừng ghi, hãy gọi các phương thức này. Một lần nữa, hãy làm theo thứ tự chính xác sau:
    1. Gọi MediaRecorder.stop().
    2. Nếu muốn, hãy xoá cấu hình MediaRecorder hiện tại bằng cách gọi MediaRecorder.reset().
    3. Gọi MediaRecorder.release().
    4. Khoá máy ảnh để các phiên MediaRecorder trong tương lai có thể sử dụng máy ảnh bằng cách gọi Camera.lock().
  7. Để dừng bản xem trước, hãy gọi Camera.stopPreview().
  8. Cuối cùng, để phát hành Camera để các quy trình khác có thể sử dụng, hãy gọi Camera.release().

Dưới đây là tất cả các bước kết hợp đó:

// 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

Với CameraController của CameraX, bạn có thể chuyển đổi ImageCapture, VideoCaptureImageAnalysis UseCase một cách độc lập, miễn là danh sách trường hợp sử dụng có thể được dùng một cách đồng thời. Theo mặc định, ImageCaptureImageAnalysis UseCase được bật. Đó là lý do bạn không cần gọi setEnabledUseCases() để chụp ảnh.

Nếu muốn dùng CameraController để quay video, trước tiên, bạn cần dùng setEnabledUseCases() để cho phép VideoCapture UseCase.

// CameraX: Enable VideoCapture UseCase on CameraController.

cameraController.setEnabledUseCases(VIDEO_CAPTURE);

Khi muốn bắt đầu quay video, bạn có thể gọi hàm CameraController.startRecording(). Hàm này có thể lưu video đã quay vào File, như bạn thấy trong ví dụ bên dưới. Ngoài ra, bạn cần truyền Executor và một lớp giúp triển khai OnVideoSavedCallback để xử lý các phương thức gọi lại thành công và có lỗi. Khi quá trình ghi kết thúc, hãy gọi CameraController.stopRecording().

Lưu ý: Nếu đang sử dụng CameraX 1.3.0-alpha02 trở lên, sẽ có thêm một tham số AudioConfig cho phép bạn bật hoặc tắt tính năng ghi âm trên video của bạn. Để bật tính năng ghi âm, bạn phải đảm bảo rằng mình có quyền truy cập vào micrô. Ngoài ra, phương thức stopRecording() sẽ bị xoá trong phiên bản 1.3.0-alpha02 và startRecording() sẽ trả về đối tượng Recording có thể dùng để tạm dừng, tiếp tục và dừng quay video.

// 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

Nếu đang sử dụng CameraProvider, bạn cần tạo VideoCapture UseCase và truyền vào đối tượng Recorder. Trên Recorder.Builder, bạn có thể đặt chất lượng video và FallbackStrategy một cách tuỳ ý nhằm xử lý các trường hợp mà thiết bị không thể đáp ứng các thông số kỹ thuật mong muốn về chất lượng. Sau đó, hãy liên kết thực thể VideoCapture với CameraProvider bằng UseCase khác.

// 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)

Tại thời điểm này, bạn có thể truy cập vào Recorder trên thuộc tính videoCapture.output. Recorder có thể bắt đầu quay video được lưu vào File, ParcelFileDescriptor hoặc MediaStore. Ví dụ này sử dụng MediaStore.

Trên Recorder, bạn có thể gọi một số phương thức để chuẩn bị. Hãy gọi prepareRecording() để đặt các tuỳ chọn đầu ra MediaStore. Nếu ứng dụng có quyền sử dụng micrô của thiết bị, hãy gọi withAudioEnabled(). Sau đó, hãy gọi start() để bắt đầu quay video, truyền trong một ngữ cảnh và trình nghe sự kiện Consumer<VideoRecordEvent> để xử lý các sự kiện quay video. Nếu thành công, bạn có thể sử dụng Recording được trả về để tạm dừng, tiếp tục hoặc dừng quá trình quay video.

// 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
                   }
               }
           }
       }
}

Tài nguyên khác

Chúng tôi có một số ứng dụng CameraX hoàn chỉnh trong Kho lưu trữ GitHub mẫu cho máy ảnh. Các mẫu này cho bạn biết các tình huống trong hướng dẫn này phù hợp với ứng dụng Android hoàn chỉnh.

Nếu bạn muốn được hỗ trợ thêm khi di chuyển sang CameraX hoặc có thắc mắc về bộ API dành cho máy ảnh Android, vui lòng liên hệ với chúng tôi qua Nhóm thảo luận về CameraX.