1. 始める前に
この Codelab では、CameraX を使用してビューファインダーの表示、写真の撮影、動画のキャプチャ、カメラからの画像ストリームの分析を行うカメラアプリを作成します。
これを実現するために、CameraX にはユースケースの概念が導入されています。これは、ビューファインダーの表示から動画のキャプチャまで、さまざまなカメラ操作に使用できます。
前提条件
- 基本的な Android 開発を経験していること。
- MediaStore の知識があることが望ましいですが、必須ではありません。
演習内容
- CameraX の依存関係を追加する方法について学習します。
- アクティビティでカメラのプレビューを表示する方法について学習します。(プレビューのユースケース)
- 写真を撮影してストレージに保存できるアプリを作成します。(ImageCaptureのユースケース)
- カメラのフレームをリアルタイムで分析する方法を学習します。(ImageAnalysis のユースケース)
- MediaStore に動画をキャプチャする方法を学習します。(VideoCapture のユースケース)
必要なもの
- Android デバイスまたは Android Studio のエミュレータ:
- Android 10 以降を推奨: MediaStore の動作は対象範囲別ストレージの可用性によって異なります。
- Android Emulator**を使用する場合は、Android 11 以降を搭載した Android Virtual Device(AVD)を使用することをおすすめします**。
- なお、CameraX で必要となるのは、サポートされる最小 API レベルが 21 であることのみです。
- Android Studio Arctic Fox 2020.3.1 以降。
- Kotlin と Android ViewBinding についての知識
2. プロジェクトを作成する
- Android Studio で、新しいプロジェクトを作成し、プロンプトが表示されたら [Empty Activity] を選択します。
- 次に、アプリに「CameraXApp」という名前を付け、パッケージ名が「
com.android.example.cameraxapp
」であるか確認し、そうでなければこの名前に変更します。言語として Kotlin を選択し、最小 API レベルを 21 に設定します(これは CameraX での最小要件です)。旧バージョンの Android Studio の場合は、必ず AndroidX アーティファクトのサポートを含めてください。
Gradle 依存関係を追加する
CameraXApp.app
モジュールのbuild.gradle
ファイルを開き、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 では Java 8 の一部であるメソッドが必要となるため、それに応じてコンパイル オプションを設定する必要があります。
android
ブロックの最後で、buildTypes
の直後に以下を追加します。
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
- この Codelab では ViewBinding を使用するため、次のようにして有効にします(
android{}
ブロックの最後)。
buildFeatures {
viewBinding true
}
メッセージが表示されたら [Sync Now] をクリックすると、アプリで CameraX を使用できるようになります。
Codelab レイアウトを作成する
この Codelab の UI では、以下を使用します。
- CameraX PreviewView(カメラの画像や動画のプレビュー用)。
- 画像キャプチャを制御するための標準ボタン。
- 動画キャプチャを開始および停止するための標準ボタン。
- 2 つのボタンを配置するための垂直ガイドライン。
デフォルトのレイアウトを次のコードに置き換えてみましょう。
res/layout/activity_main.xml
のactivity_main
レイアウト ファイルを開き、次のコードに置き換えます。
<?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>
res/values/strings.xml
ファイルを以下のように更新します。
<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>
MainActivity.kt を設定する
MainActivity.kt
のコードを次のコードに置き換えます。ただし、パッケージ名は変更しないでください。これには、インポート ステートメント、インスタンス化する変数、実装する関数、定数が含まれます。
onCreate()
はすでに実装されていて、カメラの権限の確認、カメラの起動、写真ボタンとキャプチャ ボタンの onClickListener()
の設定、cameraExecutor
の実装が可能です。onCreate()
は実装されていますが、ファイル内のメソッドを実装するまでは、カメラは動作しません。
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. 必要な権限をリクエストする
アプリでカメラを開く前に、ユーザーからの許可が必要です。録音する場合はマイクへのアクセスを許可する必要があります。Android 9(P)以前の場合、MediaStore は外部ストレージの書き込み権限を必要とします。この手順では、必要な権限を設定します。
AndroidManifest.xml
を開き、application
タグの前に次の行を追加します。
<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" />
android.hardware.camera.any
を追加すると、デバイスにカメラが搭載されていることが明確になります。.any
を指定すると、前面カメラも背面カメラも指定できます。
- このコードを
MainActivity.kt.
にコピーします。以下の箇条書きは、コピーしたコードの内訳です。
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()
}
}
}
- リクエスト コードが正しいかどうかを確認します。正しくない場合は無視します。
if (requestCode == REQUEST_CODE_PERMISSIONS) {
}
- 権限が付与されている場合は、
startCamera()
を呼び出します。
if (allPermissionsGranted()) {
startCamera()
}
- 権限が付与されていない場合は、トーストを表示し、権限が付与されていないことをユーザーに通知します。
else {
Toast.makeText(this,
"Permissions not granted by the user.",
Toast.LENGTH_SHORT).show()
finish()
}
- アプリを実行します。
カメラとマイクを使用する許可を求められるようになっています。
4. プレビューのユースケースを実装する
カメラアプリでは、ユーザーが撮影した写真をプレビューするためにビューファインダーを使用します。CameraX の Preview
クラスを使用してビューファインダーを実装します。
Preview
を使用するには、まず設定を定義する必要があります。この設定は、ユースケースのインスタンスの作成に使用されます。作成されたインスタンスは、CameraX のライフサイクルにバインドします。
- このコードを
startCamera()
関数にコピーします。
以下の箇条書きは、コピーしたコードの内訳です。
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))
}
ProcessCameraProvider
のインスタンスを作成します。これは、カメラのライフサイクルをライフサイクルの所有者にバインドするために使用されます。CameraX はライフサイクル対応であるため、これによりカメラの開閉の必要がなくなります。
val cameraProviderFuture = ProcessCameraProvider.getInstance(this)
cameraProviderFuture
にリスナーを追加します。Runnable
を 1 つの引数として追加します。これは後で入力します。2 番目の引数としてContextCompat
.getMainExecutor()
を追加します。メインスレッドで実行されるExecutor
が返されます。
cameraProviderFuture.addListener(Runnable {}, ContextCompat.getMainExecutor(this))
Runnable
にProcessCameraProvider
を追加します。これは、カメラのライフサイクルをアプリのプロセス内でLifecycleOwner
にバインドするために使用されます。
val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get()
Preview
オブジェクトを初期化し、そのオブジェクトで build を呼び出して、ビューファインダーからサーフェス プロバイダを取得して、プレビューに設定します。
val preview = Preview.Builder()
.build()
.also {
it.setSurfaceProvider(viewBinding.viewFinder.surfaceProvider)
}
CameraSelector
オブジェクトを作成し、DEFAULT_BACK_CAMERA
を選択します。
val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
try
ブロックを作成します。このブロック内で、cameraProvider
に何もバインドされていないことを確認してから、cameraSelector
とプレビュー オブジェクトをcameraProvider
にバインドします。
try {
cameraProvider.unbindAll()
cameraProvider.bindToLifecycle(
this, cameraSelector, preview)
}
- アプリがフォーカスされなくなった場合などに、このコードが失敗することがあります。障害が発生した場合に記録するために、このコードを
catch
ブロックでラップします。
catch(exc: Exception) {
Log.e(TAG, "Use case binding failed", exc)
}
- アプリを実行します。カメラのプレビューが表示されます。
5. ImageCapture のユースケースを実装する
他のユースケースも Preview
と同様に機能します。まず、実際のユースケース オブジェクトのインスタンス化に使用する設定オブジェクトを定義します。写真を撮るには、takePhoto()
メソッドを実装します。このメソッドは、[Take photo] ボタンが押されたときに呼び出されます。
- このコードを
takePhoto()
メソッドにコピーします。
以下の箇条書きは、コピーしたコードの内訳です。
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)
}
}
)
}
- まず、
ImageCapture
のユースケースへの参照を取得します。ユースケースが null の場合は、関数を終了します。画像キャプチャを設定する前に写真ボタンをタップすると null になります。return
ステートメントを指定しないと、アプリがnull
であった場合にクラッシュします。
val imageCapture = imageCapture ?: return
- 次に、画像を格納する MediaStore コンテンツの値を作成します。MediaStore の表示名が一意になるように、タイムスタンプを使用してください。
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")
}
}
OutputFileOptions
オブジェクトを作成します。このオブジェクトでは、出力方法を指定できます。出力を他のアプリで表示できるように MediaStore に保存するため、MediaStore エントリを追加します。
val outputOptions = ImageCapture.OutputFileOptions
.Builder(contentResolver,
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
contentValues)
.build()
imageCapture
オブジェクトでtakePicture()
を呼び出します。outputOptions
、エグゼキュータ、画像保存時のコールバックを渡します。次はコールバックを記入します。
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 savedUri = Uri.fromFile(photoFile)
val msg = "Photo capture succeeded: $savedUri"
Toast.makeText(baseContext, msg, Toast.LENGTH_SHORT).show()
Log.d(TAG, msg)
}
startCamera()
メソッドに移動し、プレビューするコードの下に次のコードをコピーします。
imageCapture = ImageCapture.Builder().build()
- 最後に、
try
ブロックのbindToLifecycle()
の呼び出しを更新して、新しいユースケースを含めます。
cameraProvider.bindToLifecycle(
this, cameraSelector, preview, imageCapture)
この時点で、このメソッドは次のようになります。
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))
}
- アプリを再実行し、[写真を撮る] を押します。トーストが画面に表示され、ログにメッセージが表示されます。
写真を表示
新しく撮影した写真は MediaStore に保存され、どの MediaStore アプリケーションでも表示できるようになります。たとえば、Google フォトアプリの場合は、次のようにします。
- Google フォト を起動します。
- [ライブラリ] をタップして(自分のアカウントでフォトアプリにログインしていない場合は不要)、並べ替えたメディア ファイルを表示します。
"CameraX-Image"
フォルダは Google のものです。
- 画像アイコンをタップして全体像を確認し、画面右上のその他アイコン をタップしてキャプチャした写真の詳細を表示します。
写真を撮るためのシンプルなカメラアプリについては、これで完了です。手順はこれだけです。画像解析ツールを実装する場合は、この後に進んでください。
6. ImageAnalysis ユースケースを実装する
カメラアプリをさらに高度なものにするには、ImageAnalysis
機能を使用するのがおすすめです。これにより、受信カメラフレームで呼び出される ImageAnalysis.Analyzer
インターフェースを実装するカスタムクラスを定義できます。カメラのセッション状態を管理したり、画像を破棄したりする必要はありません。他のライフサイクル対応コンポーネントと同様に、アプリの希望するライフサイクルにバインドするだけで十分です。
- このアナライザを
MainActivity.kt
の内部クラスとして追加します。アナライザは、画像の平均輝度を記録します。アナライザを作成するには、ImageAnalysis.Analyzer
インターフェースを実装するクラスでanalyze
関数をオーバーライドします。
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()
}
}
クラスが ImageAnalysis.Analyzer
インターフェースを実装しているので、他のユースケースと同様に ImageAnalysis,
で LuminosityAnalyzer
のインスタンスをインスタンス化し、CameraX.bindToLifecycle()
を呼び出す前に再度 startCamera()
関数を更新します。
startCamera()
メソッドのimageCapture
コードの下に次のコードを追加します。
val imageAnalyzer = ImageAnalysis.Builder()
.build()
.also {
it.setAnalyzer(cameraExecutor, LuminosityAnalyzer { luma ->
Log.d(TAG, "Average luminosity: $luma")
})
}
cameraProvider
のbindToLifecycle()
呼び出しを更新して、imageAnalyzer
を含めます。
cameraProvider.bindToLifecycle(
this, cameraSelector, preview, imageCapture, imageAnalyzer)
メソッド全体は次のようになります。
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))
}
- アプリを実行します。Logcat では、約 1 秒ごとに次のようなメッセージが表示されます。
D/CameraXApp: Average luminosity: ...
7. VideoCapture のユースケースを実装する
CameraX は、バージョン 1.1.0-alpha10 で VideoCapture のユースケースを追加し、それ以降、さらに改良を加えています。VideoCapture
API は多くの動画キャプチャ機能をサポートしているため、この Codelab を簡単に管理できるように、この Codelab では MediaStore
への動画と音声のキャプチャのみを紹介しています。
- このコードを
captureVideo()
メソッドにコピーします。これにより、VideoCapture
のユースケースの開始と停止の両方を制御します。以下の箇条書きは、コピーしたコードの内訳です。
// 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
}
}
}
}
}
- VideoCapture のユースケースが作成されているかどうかを確認します。作成されていない場合は何もしません。
val videoCapture = videoCapture ?: return
- CameraX によってリクエスト アクションが完了するまで UI を無効にします。後で、登録された VideoRecordListener 内で再度有効にします。
viewBinding.videoCaptureButton.isEnabled = false
- アクティブな録画が進行中の場合は、それを停止し、現在の
recording
を解放します。キャプチャした動画ファイルをアプリケーションで利用できるようになると、通知が届きます。
val curRecording = recording
if (curRecording != null) {
curRecording.stop()
recording = null
return
}
- 録画を開始するには、新しい録画セッションを作成します。まず、目的の MediaStore 動画コンテンツ オブジェクトを作成します。表示名としてシステム タイムスタンプを使用します(これで複数の動画をキャプチャできます)。
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")
}
}
- 外部コンテンツ オプションを指定して
MediaStoreOutputOptions.Builder
を作成します。
val mediaStoreOutputOptions = MediaStoreOutputOptions
.Builder(contentResolver,
MediaStore.Video.Media.EXTERNAL_CONTENT_URI)
- 作成した動画
contentValues
をMediaStoreOutputOptions.Builder
に設定し、MediaStoreOutputOptions
インスタンスを作成します。
.setContentValues(contentValues)
.build()
videoCapture
.output
.prepareRecording(this, mediaStoreOutputOptions)
.withAudioEnabled()
- この録画で音声を有効にします。
.apply {
if (PermissionChecker.checkSelfPermission(this@MainActivity,
Manifest.permission.RECORD_AUDIO) ==
PermissionChecker.PERMISSION_GRANTED)
{
withAudioEnabled()
}
}
- この新しい録画を開始し、ラムダ
VideoRecordEvent
リスナーを登録します。
.start(ContextCompat.getMainExecutor(this)) { recordEvent ->
//lambda event listener
}
- カメラデバイスによってリクエスト録画が開始されたら、[Start Capture] ボタンのテキストを切り替えて「Stop Capture」と言います。
is VideoRecordEvent.Start -> {
viewBinding.videoCaptureButton.apply {
text = getString(R.string.stop_capture)
isEnabled = true
}
}
- アクティブな録画が完了したら、トーストでユーザーに通知し、[Stop Capture] ボタンを [Start Capture] に戻して再度有効にします。
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
}
}
startCamera()
のpreview
作成行の後に次のコードを追加します。これにより、VideoCapture
のユースケースが作成されます。
val recorder = Recorder.Builder()
.setQualitySelector(QualitySelector.from(Quality.HIGHEST))
.build()
videoCapture = VideoCapture.withOutput(recorder)
- (省略可)
startCamera()
内で、次のコードを削除するかコメントアウトして、imageCapture
とimageAnalyzer
のユースケースを無効にします。
/* 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")
})
}
*/
Preview
とVideoCapture
のユースケースをライフサイクル カメラにバインドします。同じくstartCamera()
内で、cameraProvider.bindToLifecycle()
呼び出しを次のように置き換えます。
// Bind use cases to camera
cameraProvider.bindToLifecycle(this, cameraSelector, preview, videoCapture)
この時点で、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))
.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))
}
- ビルドして実行します。これまでの手順から使い慣れた UI が表示されます。
- いくつかのクリップを録画します。
- [START CAPTURE] ボタンを押します。字幕が「STOP CAPTURE」に変わります。
- 数秒間または数分間動画を撮影します。
- [STOP CAPTURE] ボタン(キャプチャ開始と同じボタン)を押します。
動画を見る(キャプチャ画像ファイルの表示と同じ)
撮影した動画は Google フォトアプリを使って確認します。
- Google フォト を起動します。
- [ライブラリ] をタップすると、メディア ファイルが並べ替えられて表示されます。
"CameraX-Video"
フォルダ アイコンをタップして、利用可能な動画クリップの一覧を表示します。
- アイコンをタップすると、キャプチャしたばかりの動画クリップが再生されます。再生が完了したら、右上のその他アイコン をタップすると、クリップの詳細を確認できます。
動画の録画に必要なことはこれだけですが、CameraX VideoCapture
には他にも次のような多くの機能があります。
- 録画を一時停止/再開する。
File
またはFileDescriptor
にキャプチャする。- その他。
使用方法については、公式ドキュメントをご覧ください。
8. (省略可)VideoCapture を他のユースケースと組み合わせる
前の VideoCapture
ステップでは、Preview
と VideoCapture
の組み合わせを説明しました。これらは、デバイス機能の表に記載されているすべてのデバイスでサポートされています。このステップでは、ImageCapture
のユースケースを既存の VideoCapture
と Preview
の組み合わせに追加して、Preview + ImageCapture + VideoCapture
の説明を行います。
- 前のステップで作成した既存のコードで、
startCamera()
でコメントを解除してimageCapture
の作成を有効にします。
imageCapture = ImageCapture.Builder().build()
FallbackStrategy
を既存のQualitySelector
作成に追加します。これにより、必要なQuality.HIGHEST
がimageCapture
のユースケースでサポートされていない場合、CameraX はサポートされている解像度を選択できます。
.setQualitySelector(QualitySelector.from(Quality.HIGHEST,
FallbackStrategy.higherQualityOrLowerThan(Quality.SD)))
- また、
startCamera()
では、imageCapture
のユースケースを既存のプレビューと videoCapture のユースケースにバインドします(注:preview + imageCapture + videoCapture + imageAnalysis
の組み合わせはサポートされていないため、imageAnalyzer
をバインドしないでください)。
cameraProvider.bindToLifecycle(
this, cameraSelector, preview, imageCapture, videoCapture)
最終的な startCamera()
関数は次のようになります。
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))
}
- ビルドして実行します。これまでのステップで見慣れた UI が表示されるはずですが、今回は [Take Photo] ボタンと [Start Capture] ボタンの両方が機能しています。
- キャプチャを実行します。
- [START CAPTURE] ボタンをタップしてキャプチャを開始します。
- [TAKE PHOTO] をタップして写真を撮影します。
- 画像キャプチャが完了するまで待ちます(以前にも表示されたトーストが表示されます)。
- [STOP CAPTURE] ボタンをタップして録画を停止します。
プレビューと動画のキャプチャの進行中に、画像キャプチャが実行されています。
- これまでのステップの Google フォトアプリの場合と同様に、キャプチャした画像ファイルと動画ファイルを表示します。今回は 2 枚の写真と 2 つの動画クリップが表示されます。
- (省略可)
imageCapture
を上記のステップ(1~4)のImageAnalyzer
のユースケースに置き換えます。ここではPreview
+ImageAnalysis
+VideoCapture
の組み合わせを使用します(ただし、Preview
+Analysis
+ImageCapture
+VideoCapture
の組み合わせはLEVEL_3
カメラデバイスでもサポートされません)。
9. 完了
以下の項目を新しい Android アプリに最初から実装することができました。
- 新しいプロジェクトに CameraX の依存関係を含める。
Preview
のユースケースを使用してカメラのビューファインダーを表示する。ImageCapture
のユースケースを使用して、写真キャプチャを実装し、画像をストレージに保存する。ImageAnalysis
ユースケースを使用して、カメラからのフレーム分析をリアルタイムで実装する。VideoCapture
のユースケースで動画キャプチャを実装する。