CameraX のスタートガイド

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. プロジェクトを作成する

  1. Android Studio で、新しいプロジェクトを作成し、プロンプトが表示されたら [Empty Activity] を選択します。

ed0f21e863f9e38f.png

  1. 次に、アプリに「CameraXApp」という名前を付け、パッケージ名が「com.android.example.cameraxapp」であるか確認し、そうでなければこの名前に変更します。言語として Kotlin を選択し、最小 API レベルを 21 に設定します(これは CameraX での最小要件です)。旧バージョンの Android Studio の場合は、必ず AndroidX アーティファクトのサポートを含めてください。

10f0a12f6c8b997c.png

Gradle 依存関係を追加する

  1. 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}"
}
  1. CameraX では Java 8 の一部であるメソッドが必要となるため、それに応じてコンパイル オプションを設定する必要があります。android ブロックの最後で、buildTypes の直後に以下を追加します。
compileOptions {
    sourceCompatibility JavaVersion.VERSION_1_8
    targetCompatibility JavaVersion.VERSION_1_8
}
  1. この Codelab では ViewBinding を使用するため、次のようにして有効にします(android{} ブロックの最後)。
buildFeatures {
   viewBinding true
}

メッセージが表示されたら [Sync Now] をクリックすると、アプリで CameraX を使用できるようになります。

Codelab レイアウトを作成する

この Codelab の UI では、以下を使用します。

  • CameraX PreviewView(カメラの画像や動画のプレビュー用)。
  • 画像キャプチャを制御するための標準ボタン。
  • 動画キャプチャを開始および停止するための標準ボタン。
  • 2 つのボタンを配置するための垂直ガイドライン。

デフォルトのレイアウトを次のコードに置き換えてみましょう。

  1. res/layout/activity_main.xmlactivity_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>
  1. 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 を設定する

  1. 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 は外部ストレージの書き込み権限を必要とします。この手順では、必要な権限を設定します。

  1. 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 を指定すると、前面カメラも背面カメラも指定できます。

  1. このコードを 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()
}
  1. アプリを実行します。

カメラとマイクを使用する許可を求められるようになっています。

dcdf8aa3d87e74be.png

4. プレビューのユースケースを実装する

カメラアプリでは、ユーザーが撮影した写真をプレビューするためにビューファインダーを使用します。CameraX の Preview クラスを使用してビューファインダーを実装します。

Preview を使用するには、まず設定を定義する必要があります。この設定は、ユースケースのインスタンスの作成に使用されます。作成されたインスタンスは、CameraX のライフサイクルにバインドします。

  1. このコードを 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))
  • RunnableProcessCameraProvider を追加します。これは、カメラのライフサイクルをアプリのプロセス内で 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)
}
  1. アプリを実行します。カメラのプレビューが表示されます。

d61a4250f6a3ed35.png

5. ImageCapture のユースケースを実装する

他のユースケースも Preview と同様に機能します。まず、実際のユースケース オブジェクトのインスタンス化に使用する設定オブジェクトを定義します。写真を撮るには、takePhoto() メソッドを実装します。このメソッドは、[Take photo] ボタンが押されたときに呼び出されます。

  1. このコードを 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)
}
  1. startCamera() メソッドに移動し、プレビューするコードの下に次のコードをコピーします。
imageCapture = ImageCapture.Builder().build()
  1. 最後に、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))
}
  1. アプリを再実行し、[写真を撮る] を押します。トーストが画面に表示され、ログにメッセージが表示されます。

54292eaa4ce3be0a.png

写真を表示

新しく撮影した写真は MediaStore に保存され、どの MediaStore アプリケーションでも表示できるようになります。たとえば、Google フォトアプリの場合は、次のようにします。

  1. Google フォト 写真 を起動します。
  2. [ライブラリ] をタップして(自分のアカウントでフォトアプリにログインしていない場合は不要)、並べ替えたメディア ファイルを表示します。"CameraX-Image" フォルダは Google のものです。

8e884489ca2599e9.png 9ca38ee62f08ef6f.png

  1. 画像アイコンをタップして全体像を確認し、画面右上のその他アイコン もっと見る をタップしてキャプチャした写真の詳細を表示します。

55e1a442ab5f25e7.png 70a8b27a76523f56.png

写真を撮るためのシンプルなカメラアプリについては、これで完了です。手順はこれだけです。画像解析ツールを実装する場合は、この後に進んでください。

6. ImageAnalysis ユースケースを実装する

カメラアプリをさらに高度なものにするには、ImageAnalysis 機能を使用するのがおすすめです。これにより、受信カメラフレームで呼び出される ImageAnalysis.Analyzer インターフェースを実装するカスタムクラスを定義できます。カメラのセッション状態を管理したり、画像を破棄したりする必要はありません。他のライフサイクル対応コンポーネントと同様に、アプリの希望するライフサイクルにバインドするだけで十分です。

  1. このアナライザを 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() 関数を更新します。

  1. startCamera() メソッドの imageCapture コードの下に次のコードを追加します。
val imageAnalyzer = ImageAnalysis.Builder()
   .build()
   .also {
       it.setAnalyzer(cameraExecutor, LuminosityAnalyzer { luma ->
           Log.d(TAG, "Average luminosity: $luma")
       })
   }
  1. cameraProviderbindToLifecycle() 呼び出しを更新して、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))
}
  1. アプリを実行します。Logcat では、約 1 秒ごとに次のようなメッセージが表示されます。
D/CameraXApp: Average luminosity: ...

7. VideoCapture のユースケースを実装する

CameraX は、バージョン 1.1.0-alpha10 で VideoCapture のユースケースを追加し、それ以降、さらに改良を加えています。VideoCapture API は多くの動画キャプチャ機能をサポートしているため、この Codelab を簡単に管理できるように、この Codelab では MediaStore への動画と音声のキャプチャのみを紹介しています。

  1. このコードを 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")
       }
}
val mediaStoreOutputOptions = MediaStoreOutputOptions
      .Builder(contentResolver,
               MediaStore.Video.Media.EXTERNAL_CONTENT_URI)
  • 作成した動画 contentValuesMediaStoreOutputOptions.Builder に設定し、MediaStoreOutputOptions インスタンスを作成します。
    .setContentValues(contentValues)
    .build()
  • 出力オプションを VideoCapture<Recorder>Recorder に設定し、音声録音を有効にします。
    videoCapture
    .output
    .prepareRecording(this, mediaStoreOutputOptions)
.withAudioEnabled()
  • この録画で音声を有効にします。
.apply {
   if (PermissionChecker.checkSelfPermission(this@MainActivity,
           Manifest.permission.RECORD_AUDIO) ==
       PermissionChecker.PERMISSION_GRANTED)
   {
       withAudioEnabled()
   }
}
.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
   }
}
  1. startCamera()preview 作成行の後に次のコードを追加します。これにより、VideoCapture のユースケースが作成されます。
val recorder = Recorder.Builder()
   .setQualitySelector(QualitySelector.from(Quality.HIGHEST))
   .build()
videoCapture = VideoCapture.withOutput(recorder)
  1. (省略可)startCamera() 内で、次のコードを削除するかコメントアウトして、imageCaptureimageAnalyzer のユースケースを無効にします。
/* 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")
       })
   }
*/
  1. PreviewVideoCapture のユースケースをライフサイクル カメラにバインドします。同じく 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))
}
  1. ビルドして実行します。これまでの手順から使い慣れた UI が表示されます。
  2. いくつかのクリップを録画します。
  • [START CAPTURE] ボタンを押します。字幕が「STOP CAPTURE」に変わります。
  • 数秒間または数分間動画を撮影します。
  • [STOP CAPTURE] ボタン(キャプチャ開始と同じボタン)を押します。

ef2a6005defc4977.png 8acee41fd0f4af0f.png

動画を見る(キャプチャ画像ファイルの表示と同じ

撮影した動画は Google フォトアプリを使って確認します。

  1. Google フォト 写真 を起動します。
  2. [ライブラリ] をタップすると、メディア ファイルが並べ替えられて表示されます。"CameraX-Video" フォルダ アイコンをタップして、利用可能な動画クリップの一覧を表示します。

71f07e32d5f4f268.png 596819ad391fac37.png

  1. アイコンをタップすると、キャプチャしたばかりの動画クリップが再生されます。再生が完了したら、右上のその他アイコン もっと見る をタップすると、クリップの詳細を確認できます。

7c7125726af9e429.png 44da18b15ad2f607.png

動画の録画に必要なことはこれだけですが、CameraX VideoCapture には他にも次のような多くの機能があります。

  • 録画を一時停止/再開する。
  • File または FileDescriptor にキャプチャする。
  • その他。

使用方法については、公式ドキュメントをご覧ください。

8. (省略可)VideoCapture を他のユースケースと組み合わせる

前の VideoCapture ステップでは、PreviewVideoCapture の組み合わせを説明しました。これらは、デバイス機能の表に記載されているすべてのデバイスでサポートされています。このステップでは、ImageCapture のユースケースを既存の VideoCapturePreview の組み合わせに追加して、Preview + ImageCapture + VideoCapture の説明を行います。

  1. 前のステップで作成した既存のコードで、startCamera() でコメントを解除して imageCapture の作成を有効にします。
imageCapture = ImageCapture.Builder().build()
  1. FallbackStrategy を既存の QualitySelector 作成に追加します。これにより、必要な Quality.HIGHESTimageCapture のユースケースでサポートされていない場合、CameraX はサポートされている解像度を選択できます。
.setQualitySelector(QualitySelector.from(Quality.HIGHEST,
    FallbackStrategy.higherQualityOrLowerThan(Quality.SD)))
  1. また、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))
   }
  1. ビルドして実行します。これまでのステップで見慣れた UI が表示されるはずですが、今回は [Take Photo] ボタンと [Start Capture] ボタンの両方が機能しています。
  2. キャプチャを実行します。
  • [START CAPTURE] ボタンをタップしてキャプチャを開始します。
  • [TAKE PHOTO] をタップして写真を撮影します。
  • 画像キャプチャが完了するまで待ちます(以前にも表示されたトーストが表示されます)。
  • [STOP CAPTURE] ボタンをタップして録画を停止します。

プレビューと動画のキャプチャの進行中に、画像キャプチャが実行されています。

ef2a6005defc4977.png 16bc70ec3346fa66.png

  1. これまでのステップの Google フォトアプリの場合と同様に、キャプチャした画像ファイルと動画ファイルを表示します。今回は 2 枚の写真と 2 つの動画クリップが表示されます。

3f3feb19c8c73532.png

  1. (省略可)imageCapture を上記のステップ(1~4)の ImageAnalyzer のユースケースに置き換えます。ここでは Preview + ImageAnalysis + VideoCapture の組み合わせを使用します(ただし、Preview + Analysis + ImageCapture + VideoCapture の組み合わせは LEVEL_3 カメラデバイスでもサポートされません)。

9. 完了

以下の項目を新しい Android アプリに最初から実装することができました。

  • 新しいプロジェクトに CameraX の依存関係を含める。
  • Preview のユースケースを使用してカメラのビューファインダーを表示する。
  • ImageCapture のユースケースを使用して、写真キャプチャを実装し、画像をストレージに保存する。
  • ImageAnalysis ユースケースを使用して、カメラからのフレーム分析をリアルタイムで実装する。
  • VideoCapture のユースケースで動画キャプチャを実装する。

CameraX とその機能について詳しくは、ドキュメントをご覧いただくか、公式サンプルのクローンを作成してください。