Memigrasikan Camera1 ke CameraX

Jika aplikasi Anda menggunakan class Camera lama ("Camera1"), yang sudah tidak digunakan lagi sejak Android 5.0 (level API 21), sebaiknya update ke API kamera Android modern. Android menawarkan CameraX (API kamera Jetpack yang standar dan kuat) dan Camera2 (API framework level rendah). Untuk sebagian besar kasus, sebaiknya migrasikan aplikasi Anda ke CameraX. Berikut alasannya:

  • Kemudahan penggunaan: CameraX menangani detail level rendah, sehingga Anda dapat lebih santai dalam mem-build pengalaman kamera dari awal dan lebih memperhatikan cara menonjolkan aplikasi Anda.
  • CameraX menangani fragmentasi untuk Anda: CameraX mengurangi biaya pemeliharaan jangka panjang dan kode khusus perangkat, sehingga menghadirkan pengalaman berkualitas lebih tinggi kepada pengguna. Untuk mengetahui informasi selengkapnya, lihat postingan blog Kompatibilitas Perangkat yang Lebih Baik dengan CameraX.
  • Kemampuan lanjutan: CameraX dirancang dengan cermat guna memudahkan fungsi lanjutan untuk diintegrasikan ke dalam aplikasi Anda. Misalnya, Anda dapat dengan mudah menerapkan Bokeh, Retouch Wajah, HDR (Rentang Dinamis Tinggi), dan mode Night capture low-light-brightening ke foto Anda dengan Ekstensi CameraX.
  • Kapasitas update: Android merilis kemampuan baru dan perbaikan bug untuk CameraX sepanjang tahun. Dengan bermigrasi ke CameraX, aplikasi Anda akan mendapatkan teknologi kamera Android terbaru dengan setiap rilis CameraX, bukan hanya pada rilis versi Android tahunan.

Dalam panduan ini, Anda akan menemukan skenario umum untuk aplikasi kamera. Setiap skenario mencakup implementasi Camera1 dan implementasi CameraX untuk perbandingan.

Dalam hal migrasi, terkadang Anda memerlukan fleksibilitas ekstra untuk berintegrasi dengan codebase yang ada. Semua kode CameraX dalam panduan ini memiliki implementasi CameraController —ini bagus jika Anda menginginkan cara paling sederhana untuk menggunakan CameraX—dan juga implementasi CameraProvider —ini bagus jika Anda membutuhkan lebih banyak fleksibilitas. Untuk membantu Anda memutuskan mana yang tepat untuk Anda, berikut ini manfaat masing-masing:

CameraController

CameraProvider

Memerlukan sedikit kode penyiapan Memberikan kontrol yang lebih besar
Mengizinkan CameraX menangani lebih banyak proses penyiapan berarti fungsi seperti ketuk untuk memfokuskan dan cubit untuk zoom dilakukan secara otomatis Karena developer aplikasi menangani penyiapan, ada lebih banyak peluang untuk menyesuaikan konfigurasi, seperti mengaktifkan rotasi gambar output atau menyetel format gambar output di ImageAnalysis
Mewajibkan PreviewView untuk pratinjau kamera memungkinkan CameraX menawarkan integrasi menyeluruh yang lancar, seperti dalam integrasi ML Kit kami yang dapat memetakan koordinat hasil model ML (seperti kotak pembatas wajah) langsung ke koordinat pratinjau Kemampuan untuk menggunakan `Surface` kustom untuk pratinjau kamera memungkinkan lebih banyak fleksibilitas, seperti menggunakan kode `Surface` yang ada dan dapat menjadi input untuk bagian lain aplikasi Anda

Jika Anda mengalami kesulitan saat mencoba melakukan migrasi, hubungi kami di Grup Diskusi CameraX.

Sebelum memigrasi

Membandingkan penggunaan CameraX dengan Camera1

Meskipun kodenya mungkin terlihat berbeda, konsep yang mendasari di Camera1 dan CameraX sangat mirip. CameraX mengabstraksi fungsi kamera umum ke dalam kasus penggunaan, sehingga banyak tugas yang diserahkan kepada developer di Camera1 ditangani secara otomatis oleh CameraX. Ada empat UseCase di CameraX yang dapat Anda gunakan untuk berbagai tugas kamera: Preview, ImageCapture, VideoCapture, dan ImageAnalysis.

Salah satu contoh CameraX yang menangani detail level rendah untuk developer adalah ViewPort yang dibagikan di antara UseCase yang aktif. Tindakan ini memastikan semua UseCase melihat piksel yang sama persis. Di Camera1, Anda harus mengelola detail ini sendiri, dan mengingat variabilitas dalam rasio aspek di seluruh sensor kamera dan layar perangkat, akan sulit untuk memastikan pratinjau Anda cocok dengan foto dan video yang diambil.

Sebagai contoh lainnya, CameraX menangani callback Lifecycle secara otomatis pada instance Lifecycle yang Anda teruskan. Ini berarti CameraX menangani koneksi aplikasi Anda ke kamera sepanjang seluruh siklus proses aktivitas Android, termasuk kasus berikut: menutup kamera saat aplikasi Anda masuk ke latar belakang; menghapus pratinjau kamera saat layar tidak lagi perlu menampilkannya; dan menjeda pratinjau kamera saat aktivitas lain lebih diutamakan di latar depan, seperti panggilan video masuk.

Terakhir, CameraX menangani rotasi dan penskalaan tanpa memerlukan kode tambahan di pihak Anda. Dalam kasus Activity dengan orientasi tidak terkunci, penyiapan UseCase dilakukan setiap kali perangkat diputar, karena sistem menghancurkan dan membuat ulang Activity setiap kali orientasi berubah. Hal ini mengakibatkan UseCases menetapkan rotasi targetnya agar sesuai dengan orientasi tampilan secara default setiap waktu. Baca selengkapnya tentang rotasi di CameraX.

Sebelum membahas detailnya, berikut ini tampilan tingkat tinggi pada UseCase CameraX dan bagaimana aplikasi Camera1 akan dikaitkan. (Konsep CameraX berwarna biru dan konsep Camera1 berwarna hijau.)

CameraX

Konfigurasi CameraController / CameraProvider
Pratinjau ImageCapture VideoCapture ImageAnalysis
Mengelola Surface pratinjau dan menetapkannya di Camera Menetapkan PictureCallback dan memanggil takePicture() di Kamera Mengelola konfigurasi Camera dan MediaRecorder dalam urutan tertentu Kode analisis kustom yang di-build di atas Surface pratinjau
Kode Khusus Perangkat
Pengelolaan Penskalaan dan Rotasi Perangkat
Pengelolaan Sesi Kamera (Pemilihan Kamera, Pengelolaan Siklus Proses)

Camera1

Kompatibilitas dan performa di CameraX

CameraX mendukung perangkat yang menjalankan Android 5.0 (API level 21) dan yang lebih tinggi. Ini mewakili lebih dari 98% perangkat Android yang ada. CameraX di-build untuk menangani perbedaan antar-perangkat secara otomatis, sehingga mengurangi kebutuhan kode khusus perangkat di aplikasi Anda. Selain itu, kami menguji lebih dari 150 perangkat fisik di semua versi Android sejak versi 5.0 di CameraX Test Lab kami. Anda dapat meninjau daftar lengkap perangkat yang saat ini berada di Test Lab.

CameraX menggunakan Executor untuk mendorong stack kamera. Anda dapat menetapkan eksekutor sendiri di CameraX jika aplikasi Anda memiliki persyaratan threading tertentu. Jika tidak disetel, CameraX akan membuat dan menggunakan Executor internal default yang dioptimalkan. Banyak API platform tempat CameraX di-build memerlukan pemblokiran komunikasi antar-proses (IPC) dengan hardware yang terkadang memerlukan waktu ratusan milidetik untuk merespons. Karena alasan ini, CameraX hanya memanggil API ini dari thread latar belakang, yang memastikan thread utama tidak diblokir dan UI tetap sesuai. Baca selengkapnya tentang thread.

Jika target pasar untuk aplikasi Anda meliputi perangkat kelas bawah, CameraX akan menyediakan cara untuk mengurangi waktu penyiapan dengan pembatas kamera. Karena proses menghubungkan ke komponen hardware dapat memakan waktu yang tidak singkat, terutama pada perangkat kelas bawah, Anda dapat menentukan kumpulan kamera yang dibutuhkan aplikasi Anda. CameraX hanya terhubung ke kamera ini selama penyiapan. Misalnya, jika aplikasi hanya menggunakan kamera belakang, aplikasi dapat menetapkan konfigurasi ini dengan DEFAULT_BACK_CAMERA, lalu CameraX menghindari menginisialisasi kamera depan untuk mengurangi latensi.

Konsep pengembangan Android

Panduan ini mengasumsikan Anda telah memiliki pengetahuan umum tentang pengembangan Android. Selain dasar-dasarnya, berikut ini beberapa konsep yang membantu pemahaman Anda sebelum masuk ke kode di bawah ini:

  • View Binding menghasilkan class binding untuk file tata letak XML, sehingga Anda dapat dengan mudah mereferensikan tampilan di Activities, seperti yang dilakukan dalam beberapa cuplikan kode di bawah. Ada beberapa perbedaan antara binding tampilan dan findViewById() (cara sebelumnya untuk mereferensikan tampilan), tetapi dalam kode di bawah ini, Anda akan dapat mengganti baris binding tampilan dengan panggilan findViewById() yang serupa.
  • Coroutine Asinkron adalah pola desain serentak yang ditambahkan di Kotlin 1.3 dan dapat digunakan untuk menangani metode CameraX yang menampilkan ListenableFuture. Hal ini menjadi lebih mudah dengan library Jetpack Concurrent sejak versi 1.1.0. Untuk menambahkan coroutine asinkron ke aplikasi:
    1. Tambahkan implementation("androidx.concurrent:concurrent-futures-ktx:1.1.0") ke file Gradle Anda.
    2. Masukkan kode CameraX yang menampilkan ListenableFuture dalam blok launch atau fungsi penangguhan.
    3. Tambahkan panggilan await() ke panggilan fungsi yang menampilkan ListenableFuture.
    4. Untuk mendapatkan pemahaman yang lebih mendalam tentang cara kerja coroutine, lihat panduan Memulai coroutine.

Memigrasikan skenario umum

Bagian ini menjelaskan cara memigrasikan skenario umum dari Camera1 ke CameraX. Setiap skenario mencakup implementasi Camera1, implementasi CameraProvider CameraX, dan implementasi CameraController CameraX.

Memilih kamera

Dalam aplikasi kamera Anda, salah satu hal pertama yang mungkin ingin Anda tawarkan adalah cara untuk memilih kamera yang berbeda.

Camera1

Di Camera1, Anda dapat memanggil Camera.open() tanpa parameter untuk membuka kamera belakang pertama, atau Anda dapat meneruskan ID bilangan bulat untuk kamera yang ingin dibuka. Berikut ini contoh tampilannya:

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

Di CameraX, pemilihan kamera ditangani oleh class CameraSelector. CameraX memudahkan kasus umum penggunaan kamera default. Anda dapat menentukan apakah akan menggunakan kamera depan default atau kamera belakang default. Selain itu, objek CameraControl CameraX memungkinkan Anda menetapkan level zoom untuk aplikasi dengan mudah, sehingga jika aplikasi Anda berjalan pada perangkat yang mendukung kamera logis, aplikasi akan beralih ke lensa yang tepat.

Berikut ini kode CameraX untuk menggunakan kamera belakang default dengan 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

Berikut ini contoh pemilihan kamera depan default dengan CameraProvider (kamera depan atau belakang dapat digunakan dengan CameraController atau 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()
    }
}

Jika Anda ingin mengontrol kamera mana yang dipilih, hal ini juga dapat dilakukan di CameraX jika Anda menggunakan CameraProvider dengan memanggil getAvailableCameraInfos(), yang memberi Anda objek CameraInfo untuk memeriksa properti kamera tertentu seperti isFocusMeteringSupported(). Kemudian, Anda dapat mengonversinya menjadi CameraSelector untuk digunakan seperti dalam contoh di atas dengan metode CameraInfo.getCameraSelector().

Anda dapat memperoleh detail selengkapnya tentang setiap kamera dengan menggunakan class Camera2CameraInfo. Panggil getCameraCharacteristic() dengan kunci untuk data kamera yang Anda inginkan. Periksa class CameraCharacteristics untuk melihat daftar semua kunci yang dapat Anda buat kuerinya.

Berikut ini contoh penggunaan fungsi checkFocalLength() kustom yang dapat Anda tentukan sendiri:

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

Menampilkan pratinjau

Sebagian besar aplikasi kamera perlu menampilkan feed kamera di layar pada waktu tertentu. Dengan Camera1, Anda perlu mengelola callback siklus proses dengan benar, dan perlu menentukan rotasi dan penskalaan untuk pratinjau.

Selain itu, di Camera1, Anda perlu memutuskan apakah akan menggunakan TextureView atau SurfaceView sebagai platform pratinjau. Kedua opsi tersebut memiliki konsekuensi, dan dalam kedua kasus tersebut, Camera1 mengharuskan Anda menangani rotasi dan penskalaan dengan benar. Di sisi lain, PreviewView CameraX memiliki implementasi yang mendasarinya untuk TextureView dan SurfaceView. CameraX menentukan implementasi terbaik berdasarkan berbagai faktor seperti jenis perangkat dan versi Android yang digunakan untuk menjalankan aplikasi Anda. Jika salah satu implementasi tersebut kompatibel, Anda dapat mendeklarasikan preferensi dengan PreviewView.ImplementationMode. Opsi COMPATIBLE menggunakan TextureView untuk pratinjau, dan nilai PERFORMANCE menggunakan SurfaceView (jika memungkinkan).

Camera1

Untuk menampilkan pratinjau, Anda harus menulis class Preview Anda sendiri dengan implementasi antarmuka android.view.SurfaceHolder.Callback, yang digunakan untuk meneruskan data gambar dari hardware kamera ke aplikasi. Kemudian, sebelum Anda dapat memulai pratinjau gambar live, class Preview harus diteruskan ke objek 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

Di CameraX, hal-hal yang harus Anda kelola sebagai developer semakin berkurang. Jika menggunakan CameraController, Anda juga harus menggunakan PreviewView. Ini berarti Preview UseCase sudah tercakup, sehingga penyiapan menjadi jauh lebih mudah:

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

Dengan CameraProvider CameraX, Anda tidak perlu menggunakan PreviewView, tetapi cara ini tetap sangat menyederhanakan penyiapan pratinjau melalui Camera1. Untuk tujuan demonstrasi, contoh ini menggunakan PreviewView, tetapi Anda dapat menulis SurfaceProvider kustom untuk diteruskan ke setSurfaceProvider() jika Anda memiliki kebutuhan yang lebih kompleks.

Di sini, Preview UseCase tidak tercakup seperti pada CameraController, sehingga Anda perlu menyiapkannya:

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

Ketuk untuk memfokuskan

Saat pratinjau kamera Anda berada di layar, kontrol yang umum adalah menetapkan titik fokus saat pengguna mengetuk pratinjau.

Camera1

Untuk menerapkan "ketuk untuk memfokuskan" di Camera1, Anda harus menghitung fokus optimal Area untuk menunjukkan tempat Camera harus berupaya untuk fokus. Area ini diteruskan ke setFocusAreas(). Selain itu, Anda harus menetapkan mode fokus yang kompatibel pada Camera. Area fokus hanya berpengaruh jika mode fokus saat ini adalah FOCUS_MODE_AUTO, FOCUS_MODE_MACRO, FOCUS_MODE_CONTINUOUS_VIDEO, atau FOCUS_MODE_CONTINUOUS_PICTURE.

Setiap Area adalah persegi panjang dengan bobot yang ditentukan. Bobot tersebut memiliki nilai antara 1 hingga 1000, dan digunakan untuk memprioritaskan fokus Areas jika beberapa nilai ditetapkan. Contoh ini hanya menggunakan satu Area, sehingga nilai bobot tidak menjadi masalah. Koordinat persegi panjang berkisar dari -1000 hingga 1000. Titik kiri atas adalah (-1000, -1000). Titik kanan bawah adalah (1000, 1000). Arahnya relatif terhadap orientasi sensor, yaitu apa yang dilihat sensor. Arah tidak terpengaruh oleh rotasi atau pencerminan Camera.setDisplayOrientation(), sehingga Anda perlu mengonversi koordinat peristiwa sentuh menjadi koordinat sensor.

// 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 akan memproses peristiwa sentuh PreviewView untuk menangani "ketuk untuk memfokuskan" secara otomatis. Anda dapat mengaktifkan dan menonaktifkan "ketuk untuk memfokuskan" dengan setTapToFocusEnabled(), dan memeriksa nilai dengan isTapToFocusEnabled() pengambil yang sesuai.

Metode getTapToFocusState() menampilkan objek LiveData untuk melacak perubahan pada status fokus di 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

Saat menggunakan CameraProvider, ada beberapa penyiapan yang diperlukan agar fungsi "ketuk untuk memfokuskan" berfungsi. Contoh ini mengasumsikan bahwa Anda menggunakan PreviewView. Jika tidak, Anda harus menyesuaikan logika untuk diterapkan ke Surface kustom.

Berikut ini langkah-langkah saat menggunakan PreviewView:

  1. Siapkan detektor gestur untuk menangani peristiwa ketuk.
  2. Dengan peristiwa ketuk, buat MeteringPoint menggunakan MeteringPointFactory.createPoint().
  3. Dengan MeteringPoint, buat FocusMeteringAction.
  4. Dengan objek CameraControl di Camera Anda (yang ditampilkan dari bindToLifecycle()), panggil startFocusAndMetering() dengan meneruskan FocusMeteringAction.
  5. (Opsional) Tanggapi FocusMeteringResult.
  6. Setel detektor gestur untuk merespons peristiwa sentuh di 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
}

Cubit untuk zoom

Memperbesar dan memperkecil pratinjau adalah manipulasi langsung lainnya yang umum ditemui pada pratinjau kamera. Dengan bertambahnya jumlah kamera pada perangkat, pengguna juga mengharapkan lensa dengan panjang fokal terbaik untuk dipilih secara otomatis sebagai hasil zoom.

Camera1

Ada dua cara untuk melakukan zoom menggunakan Camera1. Metode Camera.startSmoothZoom() dianimasikan dari tingkat zoom saat ini ke tingkat zoom yang Anda teruskan. Metode Camera.Parameters.setZoom() akan langsung melompat ke tingkat zoom yang Anda teruskan. Sebelum menggunakan salah satunya, panggil isSmoothZoomSupported() atau isZoomSupported() untuk memastikan metode zoom terkait yang Anda perlukan tersedia di Kamera Anda.

Untuk menerapkan "cubit untuk zoom", contoh ini menggunakan setZoom() karena pemroses sentuh di platform pratinjau terus mengaktifkan peristiwa saat gestur cubit terjadi, sehingga langsung mengupdate tingkat zoom setiap waktu. Class ZoomTouchListener ditentukan di bawah ini, dan harus ditetapkan sebagai callback untuk pemroses sentuh platform pratinjau Anda.

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

Serupa dengan "ketuk untuk memfokuskan", CameraController memproses peristiwa sentuh PreviewView untuk menangani "cubit untuk zoom" secara otomatis. Anda dapat mengaktifkan dan menonaktifkan "cubit untuk zoom" dengan setPinchToZoomEnabled(), dan memeriksa nilainya dengan isPinchToZoomEnabled() pengambil yang sesuai.

Metode getZoomState() menampilkan objek LiveData untuk melacak perubahan pada ZoomState di 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

Agar dapat melakukan "cubit untuk zoom" dengan CameraProvider, beberapa penyiapan diperlukan. Jika tidak menggunakan PreviewView, Anda perlu menyesuaikan logika untuk diterapkan ke Surface kustom.

Berikut ini langkah-langkah saat menggunakan PreviewView:

  1. Siapkan detektor gestur skala untuk menangani peristiwa cubit.
  2. Dapatkan ZoomState dari objek Camera.CameraInfo, tempat instance Camera ditampilkan saat Anda memanggil bindToLifecycle().
  3. Jika ZoomState memiliki nilai zoomRatio, simpan nilai tersebut sebagai rasio zoom saat ini. Jika tidak ada zoomRatio di ZoomState, gunakan tingkat zoom default kamera (1.0).
  4. Ambil produk dari rasio zoom saat ini dengan scaleFactor untuk menentukan rasio zoom baru, dan teruskan ke CameraControl.setZoomRatio().
  5. Setel detektor gestur untuk merespons peristiwa sentuh di 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
}

Mengambil foto

Bagian ini menunjukkan cara memicu pengambilan foto, baik saat Anda melakukannya dengan menekan tombol shutter, setelah timer berlalu, atau pada peristiwa lain yang Anda pilih.

Camera1

Di Camera1, Anda harus terlebih dahulu menentukan Camera.PictureCallback untuk mengelola data gambar saat diminta. Berikut ini contoh sederhana PictureCallback untuk menangani data gambar 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)
    }
}

Kemudian, setiap kali Anda ingin mengambil gambar, panggil metode takePicture() pada instance Camera Anda. Metode takePicture() ini memiliki tiga parameter yang berbeda untuk jenis data yang berbeda. Parameter pertama adalah untuk ShutterCallback (yang tidak ditentukan dalam contoh ini). Parameter kedua adalah untuk PictureCallback guna menangani data kamera raw (tidak dikompresi). Parameter ketiga adalah parameter yang digunakan contoh ini, karena merupakan PictureCallback untuk menangani data gambar JPEG.

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

camera?.takePicture(null, null, picture)

CameraX: CameraController

CameraController CameraX mempertahankan kemudahan Camera1 untuk pengambilan gambar dengan mengimplementasikan metode takePicture()-nya sendiri. Di sini, tentukan fungsi untuk mengonfigurasi entri MediaStore dan mengambil foto untuk disimpan di sana.

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

Mengambil foto dengan CameraProvider memiliki cara kerja yang hampir sama dengan CameraController, tetapi Anda harus membuat dan mengikat ImageCapture UseCase terlebih dahulu agar objek dapat memanggil 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)

Kemudian, setiap kali ingin mengambil foto, Anda dapat memanggil ImageCapture.takePicture(). Lihat kode CameraController di bagian ini untuk mengetahui contoh lengkap fungsi 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(
        ...
    )
}

Merekam video

Perekaman video jauh lebih rumit daripada skenario yang dibahas sejauh ini. Setiap bagian proses harus disiapkan dengan benar, biasanya dalam urutan tertentu. Selain itu, Anda mungkin perlu memastikan bahwa video dan audio sinkron, atau perlu menangani inkonsistensi perangkat tambahan.

Seperti yang akan Anda lihat, sekali lagi CameraX menangani banyak kerumitan ini untuk Anda.

Camera1

Perekaman video menggunakan Camera1 memerlukan pengelolaan Camera dan MediaRecorder dengan cermat, dan metode tersebut harus dipanggil dalam urutan tertentu. Anda harus mengikuti urutan ini agar aplikasi Anda berfungsi dengan baik:

  1. Nyalakan kamera.
  2. Siapkan dan mulai pratinjau (jika aplikasi Anda menunjukkan video yang sedang direkam, dan biasanya begitu).
  3. Buka kunci kamera untuk digunakan oleh MediaRecorder dengan memanggil Camera.unlock().
  4. Konfigurasikan perekaman dengan memanggil metode ini di MediaRecorder:
    1. Hubungkan instance Camera Anda dengan setCamera(camera).
    2. Panggil setAudioSource(MediaRecorder.AudioSource.CAMCORDER).
    3. Panggil setVideoSource(MediaRecorder.VideoSource.CAMERA).
    4. Panggil setProfile(CamcorderProfile.get(CamcorderProfile.QUALITY_1080P)) untuk menetapkan kualitas. Lihat CamcorderProfile untuk semua opsi kualitas.
    5. Panggil setOutputFile(getOutputMediaFile(MEDIA_TYPE_VIDEO).toString()).
    6. Jika aplikasi Anda memiliki pratinjau video, panggil setPreviewDisplay(preview?.holder?.surface).
    7. Panggil setOutputFormat(MediaRecorder.OutputFormat.MPEG_4).
    8. Panggil setAudioEncoder(MediaRecorder.AudioEncoder.DEFAULT).
    9. Panggil setVideoEncoder(MediaRecorder.VideoEncoder.DEFAULT).
    10. Panggil prepare() untuk menyelesaikan konfigurasi MediaRecorder Anda.
  5. Untuk mulai merekam, panggil MediaRecorder.start().
  6. Untuk berhenti merekam, panggil metode ini. Sekali lagi, ikuti urutan berikut:
    1. Panggil MediaRecorder.stop().
    2. Secara opsional, hapus konfigurasi MediaRecorder saat ini dengan memanggil MediaRecorder.reset().
    3. Panggil MediaRecorder.release().
    4. Kunci kamera agar sesi MediaRecorder mendatang dapat menggunakannya dengan memanggil Camera.lock().
  7. Untuk menghentikan pratinjau, panggil Camera.stopPreview().
  8. Terakhir, untuk merilis Camera agar proses lain dapat menggunakannya, panggil Camera.release().

Berikut adalah semua langkah tersebut jika digabungkan:

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

Dengan CameraController CameraX, Anda dapat mengalihkan ImageCapture, VideoCapture, dan UseCase ImageAnalysis secara independen, selama daftar UseCases dapat digunakan secara bersamaan. ImageCapture dan UseCase ImageAnalysis diaktifkan secara default, itulah sebabnya Anda tidak perlu memanggil setEnabledUseCases() untuk mengambil foto.

Agar dapat menggunakan CameraController untuk perekaman video, Anda harus menggunakan setEnabledUseCases() terlebih dahulu untuk memungkinkan UseCase VideoCapture.

// CameraX: Enable VideoCapture UseCase on CameraController.

cameraController.setEnabledUseCases(VIDEO_CAPTURE);

Jika ingin mulai merekam video, Anda dapat memanggil fungsi CameraController.startRecording(). Fungsi ini dapat menyimpan rekaman video ke File, seperti yang dapat Anda lihat pada contoh di bawah. Selain itu, Anda harus meneruskan Executor dan class yang mengimplementasikan OnVideoSavedCallback untuk menangani callback berhasil dan error. Saat perekaman harus berakhir, panggil CameraController.stopRecording().

Catatan: Jika Anda menggunakan CameraX 1.3.0-alpha02 atau yang lebih baru, ada parameter AudioConfig tambahan yang memungkinkan Anda mengaktifkan atau menonaktifkan rekaman audio di video Anda. Untuk mengaktifkan rekaman audio, Anda harus memastikan bahwa Anda memiliki izin mikrofon. Selain itu, metode stopRecording() dihapus di 1.3.0-alpha02, dan startRecording() menampilkan objek Recording yang dapat digunakan untuk menjeda, melanjutkan, dan menghentikan perekaman 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

Jika menggunakan CameraProvider, Anda harus membuat UseCase VideoCapture dan meneruskan objek Recorder. Pada Recorder.Builder, Anda dapat menyetel kualitas video dan, secara opsional, FallbackStrategy, yang menangani kasus saat perangkat tidak dapat memenuhi spesifikasi kualitas yang diinginkan. Lalu, ikat instance VideoCapture ke CameraProvider dengan UseCase Anda yang lain.

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

Pada tahap ini, Recorder dapat diakses di properti videoCapture.output. Recorder dapat memulai perekaman video yang disimpan ke File, ParcelFileDescriptor, atau MediaStore. Contoh ini menggunakan MediaStore.

Di Recorder, ada beberapa metode yang harus dipanggil untuk mempersiapkannya. Panggil prepareRecording() untuk menyetel opsi output MediaStore. Jika aplikasi Anda memiliki izin untuk menggunakan mikrofon perangkat, panggil withAudioEnabled() juga. Kemudian, panggil start() untuk mulai merekam, dengan meneruskan konteks dan pemroses peristiwa Consumer<VideoRecordEvent> untuk menangani peristiwa perekaman video. Jika berhasil, Recording yang ditampilkan dapat digunakan untuk menjeda, melanjutkan, atau menghentikan perekaman.

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

Referensi lainnya

Kami memiliki beberapa aplikasi CameraX lengkap di Repositori GitHub Contoh Kamera. Contoh ini menunjukkan bagaimana skenario dalam panduan ini sesuai dengan aplikasi Android yang lengkap.

Jika Anda menginginkan dukungan tambahan saat bermigrasi ke CameraX atau memiliki pertanyaan terkait rangkaian API Android Camera, hubungi kami di Grup Diskusi CameraX.