1. Sebelum memulai
Dalam codelab ini, Anda akan mempelajari cara membuat aplikasi kamera yang menggunakan CameraX untuk menampilkan jendela bidik, mengambil foto, merekam video, dan menganalisis aliran gambar dari kamera.
Untuk mencapai hal ini, kami akan memperkenalkan konsep kasus penggunaan di CameraX, yang dapat Anda gunakan untuk berbagai operasi kamera, dari menampilkan jendela bidik hingga merekam video.
Prasyarat
- Pengalaman pengembangan Android dasar.
- Pengetahuan tentang MediaStore akan berguna, tetapi tidak wajib.
Yang akan Anda lakukan
- Pelajari cara menambahkan dependensi CameraX.
- Pelajari cara menampilkan pratinjau kamera dalam aktivitas. (Kasus penggunaan Pratinjau)
- Buat aplikasi yang dapat mengambil foto dan menyimpannya di penyimpanan. (Kasus penggunaan ImageCapture)
- Pelajari cara menganalisis frame dari kamera secara real time. (Kasus penggunaan ImageAnalysis)
- Pelajari cara merekam video ke MediaStore. (Kasus penggunaan VideoCapture)
Yang Anda butuhkan
- Perangkat Android atau emulator Android Studio:
- Android 10 dan yang lebih baru direkomendasikan: Perilaku MediaStore bergantung pada ketersediaan penyimpanan yang dibatasi.
- Dengan Android Emulator**, sebaiknya gunakan Perangkat Virtual Android (AVD) yang didasarkan pada Android 11 atau yang lebih tinggi**.
- Perhatikan bahwa CameraX hanya memerlukan API level yang didukung minimum 21.
- Android Studio Arctic Fox 2020.3.1 atau yang lebih baru.
- Pemahaman tentang Kotlin dan Android ViewBinding
2. Membuat project
- Di Android Studio, buat project baru, lalu pilih Empty Activity saat diminta.
- Selanjutnya, beri nama aplikasi "CameraXApp", dan konfirmasi atau ubah nama paket menjadi "
com.android.example.cameraxapp
". Pilih Kotlin untuk bahasa, lalu setel API level minimum ke 21 (yang merupakan persyaratan minimum untuk CameraX). Untuk versi Android Studio yang lebih lama, pastikan Anda menyertakan dukungan artefak AndroidX.
Menambahkan dependensi Gradle
- Buka file
build.gradle
untuk modulCameraXApp.app
, dan tambahkan dependensi CameraX:
dependencies {
def camerax_version = "1.1.0-beta01"
implementation "androidx.camera:camera-core:${camerax_version}"
implementation "androidx.camera:camera-camera2:${camerax_version}"
implementation "androidx.camera:camera-lifecycle:${camerax_version}"
implementation "androidx.camera:camera-video:${camerax_version}"
implementation "androidx.camera:camera-view:${camerax_version}"
implementation "androidx.camera:camera-extensions:${camerax_version}"
}
- CameraX membutuhkan beberapa metode yang merupakan bagian dari Java 8, sehingga kita perlu menyetel opsi kompilasi sebagaimana mestinya. Di akhir blok
android
, tepat setelahbuildTypes
, tambahkan kode berikut:
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
- Codelab ini menggunakan ViewBinding, jadi aktifkan dengan hal berikut (di akhir blok
android{}
):
buildFeatures {
viewBinding true
}
Saat diminta, klik Sync Now, dan kita akan siap menggunakan CameraX di aplikasi.
Membuat tata letak codelab
Di UI untuk codelab ini, kita menggunakan hal berikut:
- CameraX PreviewView (untuk melihat pratinjau gambar/video kamera).
- Tombol standar untuk mengontrol pengambilan gambar.
- Tombol standar untuk memulai/menghentikan perekaman video.
- Panduan vertikal untuk memosisikan dua tombol.
Mari kita ganti tata letak default dengan kode ini untuk:
- Membuka file tata letak
activity_main
dires/layout/activity_main.xml
, dan menggantinya dengan kode berikut.
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<androidx.camera.view.PreviewView
android:id="@+id/viewFinder"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<Button
android:id="@+id/image_capture_button"
android:layout_width="110dp"
android:layout_height="110dp"
android:layout_marginBottom="50dp"
android:layout_marginEnd="50dp"
android:elevation="2dp"
android:text="@string/take_photo"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintEnd_toStartOf="@id/vertical_centerline" />
<Button
android:id="@+id/video_capture_button"
android:layout_width="110dp"
android:layout_height="110dp"
android:layout_marginBottom="50dp"
android:layout_marginStart="50dp"
android:elevation="2dp"
android:text="@string/start_capture"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="@id/vertical_centerline" />
<androidx.constraintlayout.widget.Guideline
android:id="@+id/vertical_centerline"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintGuide_percent=".50" />
</androidx.constraintlayout.widget.ConstraintLayout>
- Mengupdate file
res/values/strings.xml
dengan kode berikut
<resources>
<string name="app_name">CameraXApp</string>
<string name="take_photo">Take Photo</string>
<string name="start_capture">Start Capture</string>
<string name="stop_capture">Stop Capture</string>
</resources>
Menyiapkan MainActivity.kt
- Ganti kode di
MainActivity.kt
dengan kode berikut, tetapi jangan ubah nama paket. Hal ini mencakup pernyataan impor, variabel yang akan kita buat instance-nya, fungsi yang akan kita implementasikan, dan konstanta.
onCreate()
telah diimplementasikan agar kita dapat memeriksa izin kamera, memulai kamera, menetapkan onClickListener()
untuk tombol rekam dan foto, serta mengimplementasikan cameraExecutor
. Meskipun onCreate()
diimplementasikan untuk Anda, kamera belum akan berfungsi hingga kami mengimplementasikan metode dalam file tersebut.
package com.android.example.cameraxapp
import android.Manifest
import android.content.ContentValues
import android.content.pm.PackageManager
import android.os.Build
import android.os.Bundle
import android.provider.MediaStore
import androidx.appcompat.app.AppCompatActivity
import androidx.camera.core.ImageCapture
import androidx.camera.video.Recorder
import androidx.camera.video.Recording
import androidx.camera.video.VideoCapture
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import com.android.example.cameraxapp.databinding.ActivityMainBinding
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
import android.widget.Toast
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.camera.core.Preview
import androidx.camera.core.CameraSelector
import android.util.Log
import androidx.camera.core.ImageAnalysis
import androidx.camera.core.ImageCaptureException
import androidx.camera.core.ImageProxy
import androidx.camera.video.FallbackStrategy
import androidx.camera.video.MediaStoreOutputOptions
import androidx.camera.video.Quality
import androidx.camera.video.QualitySelector
import androidx.camera.video.VideoRecordEvent
import androidx.core.content.PermissionChecker
import java.nio.ByteBuffer
import java.text.SimpleDateFormat
import java.util.Locale
typealias LumaListener = (luma: Double) -> Unit
class MainActivity : AppCompatActivity() {
private lateinit var viewBinding: ActivityMainBinding
private var imageCapture: ImageCapture? = null
private var videoCapture: VideoCapture<Recorder>? = null
private var recording: Recording? = null
private lateinit var cameraExecutor: ExecutorService
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewBinding = ActivityMainBinding.inflate(layoutInflater)
setContentView(viewBinding.root)
// Request camera permissions
if (allPermissionsGranted()) {
startCamera()
} else {
ActivityCompat.requestPermissions(
this, REQUIRED_PERMISSIONS, REQUEST_CODE_PERMISSIONS)
}
// Set up the listeners for take photo and video capture buttons
viewBinding.imageCaptureButton.setOnClickListener { takePhoto() }
viewBinding.videoCaptureButton.setOnClickListener { captureVideo() }
cameraExecutor = Executors.newSingleThreadExecutor()
}
private fun takePhoto() {}
private fun captureVideo() {}
private fun startCamera() {}
private fun allPermissionsGranted() = REQUIRED_PERMISSIONS.all {
ContextCompat.checkSelfPermission(
baseContext, it) == PackageManager.PERMISSION_GRANTED
}
override fun onDestroy() {
super.onDestroy()
cameraExecutor.shutdown()
}
companion object {
private const val TAG = "CameraXApp"
private const val FILENAME_FORMAT = "yyyy-MM-dd-HH-mm-ss-SSS"
private const val REQUEST_CODE_PERMISSIONS = 10
private val REQUIRED_PERMISSIONS =
mutableListOf (
Manifest.permission.CAMERA,
Manifest.permission.RECORD_AUDIO
).apply {
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P) {
add(Manifest.permission.WRITE_EXTERNAL_STORAGE)
}
}.toTypedArray()
}
}
3. Meminta izin yang diperlukan
Sebelum aplikasi membuka kamera, aplikasi memerlukan izin dari pengguna untuk melakukannya. Izin mikrofon juga diperlukan untuk merekam audio. Di Android 9 (P) dan yang sebelumnya, MediaStore memerlukan izin penulisan penyimpanan eksternal. Pada langkah ini, kita akan mengimplementasikan izin yang diperlukan tersebut.
- Buka
AndroidManifest.xml
dan tambahkan baris berikut sebelum tagapplication
.
<uses-feature android:name="android.hardware.camera.any" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="28" />
Menambahkan android.hardware.camera.any
akan memastikan bahwa perangkat memiliki kamera. Menentukan .any
berarti bahwa kamera dapat berupa kamera depan atau kamera belakang.
- Salin kode ini ke
MainActivity.kt.
Poin-poin di bawah akan menguraikan kode yang baru saja kita salin.
override fun onRequestPermissionsResult(
requestCode: Int, permissions: Array<String>, grantResults:
IntArray) {
if (requestCode == REQUEST_CODE_PERMISSIONS) {
if (allPermissionsGranted()) {
startCamera()
} else {
Toast.makeText(this,
"Permissions not granted by the user.",
Toast.LENGTH_SHORT).show()
finish()
}
}
}
- Periksa apakah kode permintaan sudah benar; abaikan jika sebaliknya.
if (requestCode == REQUEST_CODE_PERMISSIONS) {
}
- Jika izin diberikan, panggil
startCamera()
.
if (allPermissionsGranted()) {
startCamera()
}
- Jika izin tidak diberikan, tampilkan toast untuk memberi tahu pengguna bahwa izin tidak diberikan.
else {
Toast.makeText(this,
"Permissions not granted by the user.",
Toast.LENGTH_SHORT).show()
finish()
}
- Jalankan aplikasi.
Kini aplikasi akan meminta izin untuk menggunakan kamera dan mikrofon:
4. Mengimplementasikan kasus penggunaan Pratinjau
Di aplikasi kamera, jendela bidik digunakan untuk memungkinkan pengguna melihat pratinjau foto yang akan mereka ambil. Kita akan mengimplementasikan jendela bidik menggunakan class Preview
CameraX.
Untuk menggunakan Preview
, pertama-tama kita harus menentukan konfigurasi, yang kemudian akan digunakan untuk membuat instance kasus penggunaan. Instance yang dihasilkan adalah hal yang kita ikat ke siklus proses CameraX.
- Salin kode ini ke dalam fungsi
startCamera()
.
Poin-poin di bawah ini akan menguraikan kode yang baru saja kita salin.
private fun startCamera() {
val cameraProviderFuture = ProcessCameraProvider.getInstance(this)
cameraProviderFuture.addListener({
// Used to bind the lifecycle of cameras to the lifecycle owner
val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get()
// Preview
val preview = Preview.Builder()
.build()
.also {
it.setSurfaceProvider(viewBinding.viewFinder.surfaceProvider)
}
// Select back camera as a default
val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
try {
// Unbind use cases before rebinding
cameraProvider.unbindAll()
// Bind use cases to camera
cameraProvider.bindToLifecycle(
this, cameraSelector, preview)
} catch(exc: Exception) {
Log.e(TAG, "Use case binding failed", exc)
}
}, ContextCompat.getMainExecutor(this))
}
- Buat instance
ProcessCameraProvider
. Instance ini digunakan untuk mengikat siklus proses kamera ke pemilik siklus proses. Tindakan ini akan meniadakan tugas membuka dan menutup kamera karena CameraX memahami siklus proses.
val cameraProviderFuture = ProcessCameraProvider.getInstance(this)
- Tambahkan pemroses ke
cameraProviderFuture
. TambahkanRunnable
sebagai satu argumen. Kita akan mengisinya nanti. TambahkanContextCompat
.getMainExecutor()
sebagai argumen kedua. Tindakan ini akan menampilkanExecutor
yang berjalan di thread utama.
cameraProviderFuture.addListener(Runnable {}, ContextCompat.getMainExecutor(this))
- Di
Runnable
, tambahkanProcessCameraProvider
. Ini digunakan untuk mengikat siklus proses kamera kita keLifecycleOwner
dalam proses aplikasi.
val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get()
- Lakukan inisialisasi objek
Preview
, panggil build di dalamnya, dapatkan penyedia platform dari jendela bidik, lalu tetapkan pada pratinjau.
val preview = Preview.Builder()
.build()
.also {
it.setSurfaceProvider(viewBinding.viewFinder.surfaceProvider)
}
- Buat objek
CameraSelector
, lalu pilihDEFAULT_BACK_CAMERA
.
val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
- Buat blok
try
. Di dalam blok tersebut, pastikan tidak ada yang terikat kecameraProvider
, lalu ikatcameraSelector
dan objek pratinjau kita kecameraProvider
.
try {
cameraProvider.unbindAll()
cameraProvider.bindToLifecycle(
this, cameraSelector, preview)
}
- Kode ini dapat gagal dengan beberapa cara, misalnya jika aplikasi tidak lagi menjadi fokus. Gabungkan kode ini dalam blok
catch
untuk mencatat log jika ada kegagalan.
catch(exc: Exception) {
Log.e(TAG, "Use case binding failed", exc)
}
- Jalankan aplikasi. Sekarang kita melihat pratinjau kamera.
5. Mengimplementasikan kasus penggunaan ImageCapture
Kasus penggunaan lainnya berfungsi dengan cara yang sangat mirip dengan Preview
. Pertama, kita tentukan objek konfigurasi yang digunakan untuk membuat instance objek kasus penggunaan sebenarnya. Untuk mengambil foto, Anda akan mengimplementasikan metode takePhoto()
, yang dipanggil saat tombol Take photo ditekan.
- Salin kode ini ke dalam metode
takePhoto()
.
Poin-poin di bawah ini akan menguraikan kode yang baru saja kita salin.
private fun takePhoto() {
// Get a stable reference of the modifiable image capture use case
val imageCapture = imageCapture ?: return
// 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(contentResolver,
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
contentValues)
.build()
// Set up image capture listener, which is triggered after photo has
// been taken
imageCapture.takePicture(
outputOptions,
ContextCompat.getMainExecutor(this),
object : ImageCapture.OnImageSavedCallback {
override fun onError(exc: ImageCaptureException) {
Log.e(TAG, "Photo capture failed: ${exc.message}", exc)
}
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)
}
}
)
}
- Pertama, dapatkan referensi ke kasus penggunaan
ImageCapture
. Jika kasus penggunaan adalah null, keluar dari fungsi. Kasus penggunaan akan null jika kita mengetuk tombol foto sebelum pengambilan gambar disiapkan. Tanpa pernyataanreturn
, aplikasi akan error jika kasus penggunaannull
.
val imageCapture = imageCapture ?: return
- Selanjutnya, buat nilai konten MediaStore untuk menyimpan gambar. Gunakan stempel waktu agar nama tampilan di MediaStore menjadi unik.
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")
}
}
- Buat objek
OutputFileOptions
. Di objek ini, kita dapat menentukan hal-hal tentang bagaimana output yang kita inginkan. Kita ingin output disimpan di MediaStore sehingga aplikasi lain dapat menampilkannya, jadi tambahkan entri MediaStore.
val outputOptions = ImageCapture.OutputFileOptions
.Builder(contentResolver,
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
contentValues)
.build()
- Panggil
takePicture()
pada objekimageCapture
. TeruskanoutputOptions
, eksekutor, dan callback saat gambar disimpan. Anda akan mengisi callback berikutnya.
imageCapture.takePicture(
outputOptions, ContextCompat.getMainExecutor(this),
object : ImageCapture.OnImageSavedCallback {}
)
- Jika pengambilan gambar gagal atau pengambilan gambar gagal disimpan, tambahkan kasus error untuk mencatat log bahwa pengambilan gambar gagal.
override fun onError(exc: ImageCaptureException) {
Log.e(TAG, "Photo capture failed: ${exc.message}", exc)
}
- Jika pengambilan gambar tidak gagal, foto berhasil diambil. Simpan foto ke file yang kita buat sebelumnya, tampilkan toast untuk memberi tahu pengguna bahwa pengambilan gambar berhasil, lalu cetak laporan log.
override fun onImageSaved(output: ImageCapture.OutputFileResults) {
val savedUri = Uri.fromFile(photoFile)
val msg = "Photo capture succeeded: $savedUri"
Toast.makeText(baseContext, msg, Toast.LENGTH_SHORT).show()
Log.d(TAG, msg)
}
- Buka metode
startCamera()
, lalu salin kode ini di bawah kode untuk melihat pratinjau.
imageCapture = ImageCapture.Builder().build()
- Terakhir, update panggilan ke
bindToLifecycle()
di bloktry
untuk menyertakan kasus penggunaan baru:
cameraProvider.bindToLifecycle(
this, cameraSelector, preview, imageCapture)
Metode akan terlihat seperti ini pada tahap ini:
private fun startCamera() {
val cameraProviderFuture = ProcessCameraProvider.getInstance(this)
cameraProviderFuture.addListener({
// Used to bind the lifecycle of cameras to the lifecycle owner
val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get()
// Preview
val preview = Preview.Builder()
.build()
.also {
it.setSurfaceProvider(viewFinder.surfaceProvider)
}
imageCapture = ImageCapture.Builder()
.build()
// Select back camera as a default
val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
try {
// Unbind use cases before rebinding
cameraProvider.unbindAll()
// Bind use cases to camera
cameraProvider.bindToLifecycle(
this, cameraSelector, preview, imageCapture)
} catch(exc: Exception) {
Log.e(TAG, "Use case binding failed", exc)
}
}, ContextCompat.getMainExecutor(this))
}
- Jalankan kembali aplikasi dan tekan Take Photo. Kita akan melihat toast yang ditampilkan di layar dan pesan di log.
Melihat foto
Setelah foto yang baru diambil disimpan ke MediaStore, kita dapat menggunakan aplikasi MediaStore apa pun untuk melihatnya. Misalnya, dengan aplikasi Google Foto, lakukan:
- Mulai Google Foto .
- Ketuk "Galeri Foto" (tidak diperlukan jika tidak login ke aplikasi Foto dengan akun Anda) untuk melihat file media yang diurutkan, dan folder
"CameraX-Image"
milik kita.
- Ketuk ikon gambar untuk meninjau foto lengkap; dan ketuk tombol Lainnya di pojok kanan atas untuk melihat detail foto yang diambil.
Jika hanya mencari aplikasi kamera yang sederhana untuk mengambil foto, kita sudah selesai. Semudah itu. Jika kita ingin mengimplementasikan penganalisis gambar, baca terus.
6. Mengimplementasikan kasus penggunaan ImageAnalysis
Cara terbaik untuk membuat aplikasi kamera kita lebih menarik adalah menggunakan fitur ImageAnalysis
. Fitur ini memungkinkan kita menentukan class kustom yang mengimplementasikan antarmuka ImageAnalysis.Analyzer
, dan yang akan dipanggil dengan frame kamera yang akan datang. Kita tidak perlu mengelola status sesi kamera atau bahkan membuang gambar; mengikat ke siklus proses yang diinginkan aplikasi kita sudah cukup, seperti dengan komponen berbasis siklus proses lainnya.
- Tambahkan penganalisis ini sebagai class dalam di
MainActivity.kt
. Penganalisis mencatat log luminositas rata-rata gambar. Untuk membuat penganalisis, kita mengganti fungsianalyze
di class yang mengimplementasikan antarmukaImageAnalysis.Analyzer
.
private class LuminosityAnalyzer(private val listener: LumaListener) : ImageAnalysis.Analyzer {
private fun ByteBuffer.toByteArray(): ByteArray {
rewind() // Rewind the buffer to zero
val data = ByteArray(remaining())
get(data) // Copy the buffer into a byte array
return data // Return the byte array
}
override fun analyze(image: ImageProxy) {
val buffer = image.planes[0].buffer
val data = buffer.toByteArray()
val pixels = data.map { it.toInt() and 0xFF }
val luma = pixels.average()
listener(luma)
image.close()
}
}
Dengan class yang mengimplementasikan antarmuka ImageAnalysis.Analyzer
, yang perlu kita lakukan adalah membuat instance LuminosityAnalyzer
di ImageAnalysis,
mirip dengan kasus penggunaan lainnya, dan mengupdate fungsi startCamera()
sekali lagi, sebelum panggilan ke CameraX.bindToLifecycle()
:
- Di metode
startCamera()
, tambahkan kode ini di bawah kodeimageCapture
.
val imageAnalyzer = ImageAnalysis.Builder()
.build()
.also {
it.setAnalyzer(cameraExecutor, LuminosityAnalyzer { luma ->
Log.d(TAG, "Average luminosity: $luma")
})
}
- Update panggilan
bindToLifecycle()
dicameraProvider
untuk menyertakanimageAnalyzer
.
cameraProvider.bindToLifecycle(
this, cameraSelector, preview, imageCapture, imageAnalyzer)
Metode lengkap kini akan terlihat seperti ini:
private fun startCamera() {
val cameraProviderFuture = ProcessCameraProvider.getInstance(this)
cameraProviderFuture.addListener({
// Used to bind the lifecycle of cameras to the lifecycle owner
val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get()
// Preview
val preview = Preview.Builder()
.build()
.also {
it.setSurfaceProvider(viewBinding.viewFinder.surfaceProvider)
}
imageCapture = ImageCapture.Builder()
.build()
val imageAnalyzer = ImageAnalysis.Builder()
.build()
.also {
it.setAnalyzer(cameraExecutor, LuminosityAnalyzer { luma ->
Log.d(TAG, "Average luminosity: $luma")
})
}
// Select back camera as a default
val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
try {
// Unbind use cases before rebinding
cameraProvider.unbindAll()
// Bind use cases to camera
cameraProvider.bindToLifecycle(
this, cameraSelector, preview, imageCapture, imageAnalyzer)
} catch(exc: Exception) {
Log.e(TAG, "Use case binding failed", exc)
}
}, ContextCompat.getMainExecutor(this))
}
- Jalankan aplikasi sekarang. Aplikasi akan menghasilkan pesan yang serupa dengan ini di logcat kira-kira setiap detik.
D/CameraXApp: Average luminosity: ...
7. Mengimplementasikan kasus penggunaan VideoCapture
CameraX menambahkan kasus penggunaan VideoCapture di versi 1.1.0-alpha10 dan telah melakukan peningkatan lebih lanjut sejak saat itu. Perlu diperhatikan bahwa VideoCapture
API mendukung banyak fitur perekaman video, jadi agar codelab ini tetap dapat dikelola, codelab ini hanya menunjukkan perekaman video dan audio ke MediaStore
.
- Salin kode ini ke dalam metode
captureVideo()
: kode ini mengontrol awal dan penghentian kasus penggunaanVideoCapture
. Poin-poin di bawah ini akan menguraikan kode yang baru saja kita salin.
// Implements VideoCapture use case, including start and stop capturing.
private fun captureVideo() {
val videoCapture = this.videoCapture ?: return
viewBinding.videoCaptureButton.isEnabled = false
val curRecording = recording
if (curRecording != null) {
// Stop the current recording session.
curRecording.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)
.apply {
if (PermissionChecker.checkSelfPermission(this@MainActivity,
Manifest.permission.RECORD_AUDIO) ==
PermissionChecker.PERMISSION_GRANTED)
{
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
}
}
}
}
}
- Periksa apakah kasus penggunaan VideoCapture telah dibuat: jika tidak, jangan lakukan apa pun.
val videoCapture = videoCapture ?: return
- Nonaktifkan UI hingga tindakan permintaan diselesaikan oleh CameraX; UI akan diaktifkan kembali di dalam VideoRecordListener yang terdaftar pada langkah berikutnya.
viewBinding.videoCaptureButton.isEnabled = false
- Jika ada perekaman aktif yang sedang berlangsung, hentikan dan lepaskan
recording
saat ini. Kita akan diberi tahu saat file video yang direkam siap digunakan oleh aplikasi kita.
val curRecording = recording
if (curRecording != null) {
curRecording.stop()
recording = null
return
}
- Untuk mulai merekam, kita membuat sesi perekaman baru. Pertama, kita membuat objek konten video MediaStore yang diinginkan, dengan stempel waktu sistem sebagai nama tampilan (sehingga kita dapat merekam beberapa video).
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")
}
}
- Buat
MediaStoreOutputOptions.Builder
dengan opsi konten eksternal.
val mediaStoreOutputOptions = MediaStoreOutputOptions
.Builder(contentResolver,
MediaStore.Video.Media.EXTERNAL_CONTENT_URI)
- Tetapkan
contentValues
video yang dibuat keMediaStoreOutputOptions.Builder
, dan build instanceMediaStoreOutputOptions
kita.
.setContentValues(contentValues)
.build()
- Konfigurasikan opsi output ke
Recorder
dariVideoCapture<Recorder>
dan aktifkan rekaman audio:
videoCapture
.output
.prepareRecording(this, mediaStoreOutputOptions)
.withAudioEnabled()
- Aktifkan Audio di rekaman ini.
.apply {
if (PermissionChecker.checkSelfPermission(this@MainActivity,
Manifest.permission.RECORD_AUDIO) ==
PermissionChecker.PERMISSION_GRANTED)
{
withAudioEnabled()
}
}
- Mulai rekaman baru ini, dan daftarkan pemroses
VideoRecordEvent
lambda.
.start(ContextCompat.getMainExecutor(this)) { recordEvent ->
//lambda event listener
}
- Saat perekaman permintaan dimulai oleh perangkat kamera, alihkan teks tombol "Start Capture" ke "Stop Capture".
is VideoRecordEvent.Start -> {
viewBinding.videoCaptureButton.apply {
text = getString(R.string.stop_capture)
isEnabled = true
}
}
- Setelah perekaman aktif selesai, beri tahu pengguna dengan toast, dan alihkan tombol "Stop Capture" kembali ke "Start Capture", serta aktifkan kembali:
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 succeeded: " +
"${recordEvent.outputResults.outputUri}")
}
viewBinding.videoCaptureButton.apply {
text = getString(R.string.start_capture)
isEnabled = true
}
}
- Di
startCamera()
, tempatkan kode berikut setelah baris pembuatanpreview
. Tindakan ini akan membuat kasus penggunaanVideoCapture
.
val recorder = Recorder.Builder()
.setQualitySelector(QualitySelector.from(Quality.HIGHEST))
.build()
videoCapture = VideoCapture.withOutput(recorder)
- (Opsional) juga di dalam
startCamera()
, nonaktifkan kasus penggunaanimageCapture
danimageAnalyzer
dengan menghapus atau menjadikan kode berikut sebagai komentar:
/* comment out ImageCapture and ImageAnalyzer use cases
imageCapture = ImageCapture.Builder().build()
val imageAnalyzer = ImageAnalysis.Builder()
.build()
.also {
it.setAnalyzer(cameraExecutor, LuminosityAnalyzer { luma ->
Log.d(TAG, "Average luminosity: $luma")
})
}
*/
- Ikat kasus penggunaan
Preview
+VideoCapture
ke kamera siklus proses. Masih di dalamstartCamera()
, ganti panggilancameraProvider.bindToLifecycle()
dengan kode berikut:
// Bind use cases to camera
cameraProvider.bindToLifecycle(this, cameraSelector, preview, videoCapture)
Pada tahap ini, startCamera()
akan terlihat seperti ini:
val cameraProviderFuture = ProcessCameraProvider.getInstance(this)
cameraProviderFuture.addListener({
// Used to bind the lifecycle of cameras to the lifecycle owner
val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get()
// Preview
val preview = Preview.Builder()
.build()
.also {
it.setSurfaceProvider(viewBinding.viewFinder.surfaceProvider)
}
val recorder = Recorder.Builder()
.setQualitySelector(QualitySelector.from(Quality.HIGHEST))
.build()
videoCapture = VideoCapture.withOutput(recorder)
/*
imageCapture = ImageCapture.Builder().build()
val imageAnalyzer = ImageAnalysis.Builder()
.build()
.also {
it.setAnalyzer(cameraExecutor, LuminosityAnalyzer { luma ->
Log.d(TAG, "Average luminosity: $luma")
})
}
*/
// Select back camera as a default
val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
try {
// Unbind use cases before rebinding
cameraProvider.unbindAll()
// Bind use cases to camera
cameraProvider
.bindToLifecycle(this, cameraSelector, preview, videoCapture)
} catch(exc: Exception) {
Log.e(TAG, "Use case binding failed", exc)
}
}, ContextCompat.getMainExecutor(this))
}
- Build dan jalankan. Kita akan melihat UI yang sudah dikenal dari langkah sebelumnya.
- Rekam beberapa klip:
- Tekan tombol "START CAPTURE". Perhatikan bahwa teks akan berubah menjadi "STOP CAPTURE".
- Rekam video selama beberapa detik/menit.
- Tekan tombol "STOP CAPTURE" (tombol yang sama untuk memulai pengambilan gambar).
Melihat video (sama seperti melihat file gambar tangkapan)
Kita akan menggunakan aplikasi Google Foto untuk meninjau video yang direkam:
- Mulai Google Foto .
- Ketuk "Galeri Foto" untuk melihat file media yang diurutkan. Ketuk ikon folder
"CameraX-Video"
untuk melihat daftar klip video yang tersedia.
- Ketuk ikon untuk memutar klip video yang baru saja direkam. Setelah pemutaran selesai, ketuk tombol Lainnya di pojok kanan atas untuk memeriksa detail klip.
Hanya itu yang kita perlukan untuk merekam video. Namun, VideoCapture
CameraX menawarkan banyak fitur lain, termasuk:
- menjeda/melanjutkan perekaman.
- merekam ke
File
atauFileDescriptor
. - dan lainnya.
Untuk petunjuk cara menggunakannya, lihat dokumentasi resmi.
8. (Opsional) Menggabungkan VideoCapture dengan kasus penggunaan lainnya
Langkah VideoCapture
sebelumnya menunjukkan kombinasi Preview
dan VideoCapture
yang didukung di semua perangkat seperti yang didokumentasikan dalam tabel kemampuan perangkat. Pada langkah ini, kita akan menambahkan kasus penggunaan ImageCapture
ke kombinasi VideoCapture
+ Preview
yang ada untuk menunjukkan Preview + ImageCapture + VideoCapture
.
- Dengan kode yang ada dari langkah sebelumnya, hapus tanda komentar dan aktifkan pembuatan
imageCapture
distartCamera()
:
imageCapture = ImageCapture.Builder().build()
- Tambahkan
FallbackStrategy
ke pembuatanQualitySelector
yang ada. CameraX dapat mengambil resolusi yang didukung jikaQuality.HIGHEST
yang diperlukan tidak didukung dengan kasus penggunaanimageCapture
.
.setQualitySelector(QualitySelector.from(Quality.HIGHEST,
FallbackStrategy.higherQualityOrLowerThan(Quality.SD)))
- Selain itu, di
startCamera()
, ikat kasus penggunaanimageCapture
dengan pratinjau dan kasus penggunaan videoCapture yang ada (catatan: jangan mengikatimageAnalyzer
, karena kombinasipreview + imageCapture + videoCapture + imageAnalysis
tidak didukung):
cameraProvider.bindToLifecycle(
this, cameraSelector, preview, imageCapture, videoCapture)
Sekarang fungsi startCamera()
akhir akan terlihat seperti ini:
private fun startCamera() {
val cameraProviderFuture = ProcessCameraProvider.getInstance(this)
cameraProviderFuture.addListener({
// Used to bind the lifecycle of cameras to the lifecycle owner
val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get()
// Preview
val preview = Preview.Builder()
.build()
.also {
it.setSurfaceProvider(viewBinding.viewFinder.surfaceProvider)
}
val recorder = Recorder.Builder()
.setQualitySelector(QualitySelector.from(Quality.HIGHEST,
FallbackStrategy.higherQualityOrLowerThan(Quality.SD)))
.build()
videoCapture = VideoCapture.withOutput(recorder)
imageCapture = ImageCapture.Builder().build()
/*
val imageAnalyzer = ImageAnalysis.Builder().build()
.also {
setAnalyzer(
cameraExecutor,
LuminosityAnalyzer { luma ->
Log.d(TAG, "Average luminosity: $luma")
}
)
}
*/
// Select back camera as a default
val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
try {
// Unbind use cases before rebinding
cameraProvider.unbindAll()
// Bind use cases to camera
cameraProvider.bindToLifecycle(
this, cameraSelector, preview, imageCapture, videoCapture)
} catch(exc: Exception) {
Log.e(TAG, "Use case binding failed", exc)
}
}, ContextCompat.getMainExecutor(this))
}
- Build dan jalankan. Kita akan melihat UI yang sudah dikenal dari langkah sebelumnya. Namun, kali ini tombol "Take Photo" dan "Start Capture" berfungsi.
- Lakukan perekaman:
- Ketuk tombol "START CAPTURE" untuk mulai merekam.
- Ketuk "TAKE PHOTO" untuk mengambil gambar.
- Tunggu hingga pengambilan gambar selesai (kita akan melihat toast seperti yang kita lihat sebelumnya).
- Ketuk tombol "STOP CAPTURE" untuk berhenti merekam.
Kita sedang melakukan pengambilan gambar saat pratinjau dan perekaman video sedang berlangsung.
- Lihat file gambar dan video yang diambil seperti yang kami lakukan di aplikasi Google Foto dari langkah sebelumnya. Kali ini, kita akan melihat dua foto dan dua klip video.
- (Opsional) Ganti
imageCapture
denganImageAnalyzer
kasus penggunaan pada langkah-langkah di atas (langkah 1 hingga langkah 4): kita akan menggunakan kombinasiPreview
+ImageAnalysis
+VideoCapture
(Perhatikan lagi bahwa kombinasiPreview
+Analysis
+ImageCapture
+VideoCapture
mungkin tidak didukung meskipun dengan perangkat kameraLEVEL_3
.)
9. Selamat!
Anda telah berhasil menerapkan hal berikut ke aplikasi Android baru dari awal:
- Menyertakan dependensi CameraX ke dalam project baru.
- Menampilkan jendela bidik kamera menggunakan kasus penggunaan
Preview
. - Menerapkan pengambilan foto dan menyimpan gambar ke penyimpanan menggunakan kasus penggunaan
ImageCapture
. - Mengimplementasikan analisis frame dari kamera secara real time menggunakan kasus penggunaan
ImageAnalysis
. - Mengimplementasikan perekaman video dengan kasus penggunaan
VideoCapture
.
Jika Anda tertarik untuk membaca lebih lanjut tentang CameraX dan berbagai hal yang dapat Anda lakukan dengan CameraX, lihat dokumentasi atau clone contoh resmi.