Camera1 を CameraX に移行する

Android 5.0(API レベル 21)以降の、サポートが終了したオリジナルの Camera クラス(Camera1)をアプリで使用している場合、最新の Android Camera API に更新することを強くおすすめします。Android では、CameraX(標準化された堅牢な Jetpack カメラ API)と、Camera2(低レベルのフレームワーク API)を提供しています。ほとんどの場合、アプリを CameraX に移行することをおすすめします。その理由は次のとおりです。

  • 使いやすさ: CameraX が低レベルの細部を処理するため、カメラ エクスペリエンスをゼロから構築する必要がなく、アプリの差別化に専念できます。
  • CameraX が断片化処理を行う: CameraX により、長期的なメンテナンス コストとデバイス固有のコードを削減し、ユーザーに質の高いエクスペリエンスを提供できます。詳細については、CameraX を使用したデバイスの互換性の向上に関するブログ投稿ご覧ください。
  • 高度な機能: CameraX は、高度な機能を簡単にアプリに組み込めるよう慎重に設計されています。たとえば、CameraX の拡張機能を使用して、ボケ、顔写真加工、HDR(ハイ ダイナミック レンジ)、暗い場所で明るく撮影できる夜間撮影モードを写真に簡単に適用できます。
  • 更新可能性: Android では、年間を通じて CameraX の新機能とバグの修正がリリースされています。CameraX に移行することで、Android の年間バージョンだけでなく、各 CameraX のリリースで提供される最新の Android カメラ テクノロジーを利用できます。

このガイドでは、カメラアプリの一般的なシナリオについて説明します。各シナリオでは、比較のために Camera1 と CameraX の実装を示しています。

移行するにあたり、既存のコードベースと統合するためにさらなる柔軟性が必要になることがあります。このガイド内のすべての CameraX コードには、CameraController 実装(CameraX を最も簡単な方法で使用したい場合に最適)と CameraProvider 実装(さらなる柔軟性が必要な場合に最適)が含まれています。どちらが適しているか判断しやすいように、それぞれのメリットを以下に示します。

CameraController

CameraProvider

セットアップ コードをほとんど必要としない 詳細な制御が可能
CameraX がセットアップ プロセスを処理できる範囲が広がり、「タップしてフォーカス」や「ピンチしてズーム」などの機能が自動的に動作する アプリ デベロッパーがセットアップを処理するため、構成をカスタマイズする機会が増える(例: 出力画像の回転の有効化、ImageAnalysis での出力画像形式の設定)
MLX モデルの結果座標(顔の境界ボックスなど)をプレビューの座標に直接マッピングできる ML Kit 統合のように、カメラ プレビューに PreviewView を必須とすることで、CameraX はシームレスなエンドツーエンドの統合を提供できる カメラ プレビューにカスタムの「サーフェス」を使用する機能により、柔軟性を高めることができる(例: アプリの他の部分に入力できる既存の「サーフェス」コードを使用する)

移行がうまくいかない場合は、CameraX ディスカッション グループからお問い合わせください。

移行前に

CameraX と Camera1 の使用方法を比較する

Camera1 と CameraX はコードの見た目が異なるものの、基本的なコンセプトは非常に似ています。CameraX は、一般的なカメラ機能をユースケースに抽象化します。これにより、Camera1 ではデベロッパーが行っていた多くのタスクが、CameraX によって自動的に処理されます。CameraX には、さまざまなカメラタスクに使用できる 4 つの UseCasePreviewImageCaptureVideoCaptureImageAnalysis)があります。

CameraX がデベロッパーに代わって低レベルの詳細を処理する例として、アクティブな UseCase 間で共有される ViewPort があります。これにより、すべての UseCase がまったく同じピクセルを認識できるようになります。Camera1 ではこうした詳細をデベロッパー自身で管理する必要があります。また、デバイスのカメラセンサーやスクリーンのアスペクト比がばらつくため、撮影した写真や動画にプレビューを一致させるのは簡単ではありません。

別の例として、CameraX は、渡された Lifecycle インスタンスで Lifecycle コールバックを自動的に処理します。つまり CameraX は、Android アクティビティ ライフサイクル全体でカメラへのアプリの接続を処理します。これには、アプリがバックグラウンドに移動したときにカメラを閉じる、画面に表示する必要がなくなったときにカメラ プレビューを削除する、別のアクティビティ(ビデオ通話の着信など)がフォアグラウンドで優先される場合にカメラ プレビューを一時停止するなどのケースが含まれます。

最後に、CameraX は回転とスケーリングを処理します。そのためにデベロッパー側でコードを追加する必要はありません。Activity で向きのロックが解除されている場合は、向きが変わると Activity が破棄されて再作成されるため、UseCase の設定はデバイスが回転されるたびに行われます。それにより UseCases では、ディスプレイの向きに合わせて、デフォルトで毎回ターゲットの回転が設定されます。CameraX での回転の詳細をご覧ください

詳細に入る前に、CameraX の UseCase の概要と、それに対する Camera1 アプリの関係を次に示します(CameraX のコンセプトをで、Camera1 のコンセプトをで示しています)。

CameraX

CameraController / CameraProvider の構成
Preview ImageCapture VideoCapture ImageAnalysis
プレビュー サーフェスを管理し、カメラで設定する カメラで PictureCallback を設定して takePicture() を呼び出す カメラと MediaRecorder の構成を特定の順序で管理する プレビュー サーフェス上に構築されたカスタム分析コード
デバイス固有のコード
デバイスの回転とスケーリング管理
カメラ セッション管理(カメラの選択、ライフサイクル管理)

Camera1

CameraX の互換性とパフォーマンス

CameraX は、Android 5.0(API レベル 21)以降を搭載したデバイス(既存の Android デバイスの 98% 以上)をサポートしています。CameraX はデバイス間の違いを自動的に処理するように設計されているため、アプリでデバイス固有のコードを使用する必要性が軽減されます。さらに、CameraX Test Lab で 150 台以上の実機(5.0 以降のすべての Android バージョン)がテストされています。Test Lab で現在テスト済みのデバイスの全リストを確認できます。

CameraX は、Executor を使用してカメラスタックを動作させます。アプリに特定のスレッド要件がある場合は、CameraX に独自のエグゼキュータを設定できます。設定しない場合、CameraX は最適化されたデフォルトの内部 Executor を作成して使用します。CameraX が構築されるたいていのプラットフォーム API では、ハードウェアとのプロセス間通信(IPC)のブロックが必要です。この処理には、応答まで数百ミリ秒かかる場合があります。このため、CameraX はそれらの API をバックグラウンド スレッドからのみ呼び出します。これにより、メインスレッドがブロックされず、滑らかな UI を維持できます。スレッドの詳細をご確認ください。

アプリのターゲット市場にローエンド デバイスが含まれている場合、CameraX ではカメラ リミッターを使用してセットアップ時間を短縮できます。特にローエンド デバイスでは、ハードウェア コンポーネントへの接続プロセスにかなりの時間がかかるため、アプリが必要とするカメラのセットを指定できます。CameraX は、セットアップ中にのみこれらのカメラに接続します。たとえば、アプリが背面カメラのみを使用している場合、DEFAULT_BACK_CAMERA でこの構成を設定できます。これにより、CameraX は前面カメラの初期化を回避してレイテンシを短縮します。

Android 開発のコンセプト

このガイドは、Android 開発の一般的な知識があることを前提としています。以下のコードの説明に入る前に、基本以外に理解しておくべきコンセプトをいくつかご紹介します。

一般的なシナリオを移行する

このセクションでは、一般的なシナリオを Camera1 から CameraX に移行する方法について説明します。 各シナリオでは、Camera1 の実装と、CameraX の CameraProvider および CameraController の実装について取り上げます。

カメラの選択

カメラアプリでは、さまざまなカメラを選択できるようにすることをおすすめします。

Camera1

Camera1 では、パラメータなしで Camera.open() を呼び出して 1 番目の背面カメラを開くか、開きたいカメラの整数 ID を渡すことができます。この場合、コードは以下のようになります。

// Camera1: select a camera from id.

// Note: opening the camera is a non-trivial task, and it shouldn't be
// called from the main thread, unlike CameraX calls, which can be
// on the main thread since CameraX kicks off background threads
// internally as needed.

private fun safeCameraOpen(id: Int): Boolean {
    return try {
        releaseCameraAndPreview()
        camera = Camera.open(id)
        true
    } catch (e: Exception) {
        Log.e(TAG, "failed to open camera", e)
        false
    }
}

private fun releaseCameraAndPreview() {
    preview?.setCamera(null)
    camera?.release()
    camera = null
}

CameraX: CameraController

CameraX では、カメラの選択は CameraSelector クラスによって処理されます。CameraX を使用すると、デフォルトのカメラを使用する一般的なケースを簡単に実現できます。デフォルトの前面カメラとデフォルトの背面カメラのどちらを使用するかを指定できます。さらに、CameraX の CameraControl オブジェクトを使用することで、簡単にアプリのズームレベルを設定できます。そのため、論理カメラがサポートされているデバイスでアプリが動作している場合、このオブジェクトによって適切なレンズに切り替えられます。

CameraController でデフォルトの背面カメラを使用するための CameraX コードは次のとおりです。

// CameraX: select a camera with CameraController

var cameraController = LifecycleCameraController(baseContext)
val selector = CameraSelector.Builder()
    .requireLensFacing(CameraSelector.LENS_FACING_BACK).build()
cameraController.cameraSelector = selector

CameraX: CameraProvider

次に、CameraProvider でデフォルトの前面カメラを選択する例を示します(前面カメラまたは背面カメラを CameraController または CameraProvider で使用できます)。

// CameraX: select a camera with CameraProvider.

// Use await() within a suspend function to get CameraProvider instance.
// For more details on await(), see the "Android development concepts"
// section above.
private suspend fun startCamera() {
    val cameraProvider = ProcessCameraProvider.getInstance(this).await()

    // Set up UseCases (more on UseCases in later scenarios)
    var useCases:Array = ...

    // Set the cameraSelector to use the default front-facing (selfie)
    // camera.
    val cameraSelector = CameraSelector.DEFAULT_FRONT_CAMERA

    try {
        // Unbind UseCases before rebinding.
        cameraProvider.unbindAll()

        // Bind UseCases to camera. This function returns a camera
        // object which can be used to perform operations like zoom,
        // flash, and focus.
        var camera = cameraProvider.bindToLifecycle(
            this, cameraSelector, useCases)

    } catch(exc: Exception) {
        Log.e(TAG, "UseCase binding failed", exc)
    }
})

...

// Call startCamera in the setup flow of your app, such as in onViewCreated.
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)

    ...

    lifecycleScope.launch {
        startCamera()
    }
}

選択されるカメラを制御したい場合も、CameraX で CameraProvider を使用することで実現できます。それには、getAvailableCameraInfos() を呼び出し、特定のカメラ プロパティを確認するための CameraInfo オブジェクト(isFocusMeteringSupported() など)を取得します。それを CameraSelector に変換し、CameraInfo.getCameraSelector() メソッドを使って上記の例のように使用できます。

各カメラの詳細情報は、Camera2CameraInfo クラスを使用して取得できます。目的のカメラデータのキーを使用して getCameraCharacteristic() を呼び出します。クエリを実行できるすべてのキーのリストについては、CameraCharacteristics クラスをご覧ください。

自身で定義可能なカスタム checkFocalLength() 関数を使用した例を次に示します。

// CameraX: get a cameraSelector for first camera that matches the criteria
// defined in checkFocalLength().

val cameraInfo = cameraProvider.getAvailableCameraInfos()
    .first { cameraInfo ->
        val focalLengths = Camera2CameraInfo.from(cameraInfo)
            .getCameraCharacteristic(
                CameraCharacteristics.LENS_INFO_AVAILABLE_FOCAL_LENGTHS
            )
        return checkFocalLength(focalLengths)
    }
val cameraSelector = cameraInfo.getCameraSelector()

プレビューの表示

カメラアプリの大半は、ある時点でカメラフィードを画面に表示する必要があります。Camera1 では、ライフサイクル コールバックを正しく管理するとともに、プレビューの回転とスケーリングを決める必要もあります。

また、Camera1 では、プレビュー サーフェスとして TextureViewSurfaceView のどちらを使用するかを決めなければなりません。どちらの方法でもトレードオフが発生します。いずれの場合も、Camera1 では回転とスケーリングを正しく処理する必要があります。一方、CameraX の PreviewView は、TextureViewSurfaceView の両方に対して基盤となる実装を備えています。CameraX は、デバイスのタイプや、アプリが動作する Android のバージョンなどの要因に応じて、最適な実装を決定します。いずれかの実装に互換性がある場合は、PreviewView.ImplementationMode を使用して、どちらを優先するかを宣言できます。COMPATIBLE オプションの場合はプレビューに TextureView を使用し、PERFORMANCE 値の場合は(可能な限り)SurfaceView を使用します。

Camera1

プレビューを表示するには、android.view.SurfaceHolder.Callback インターフェースの実装を使用して、独自の Preview クラスを作成する必要があります。これは、カメラ ハードウェアからアプリに画像データを渡すために使用されます。その後、ライブ画像のプレビューを開始する前に、Preview クラスを Camera オブジェクトに渡す必要があります。

// Camera1: set up a camera preview.

class Preview(
        context: Context,
        private val camera: Camera
) : SurfaceView(context), SurfaceHolder.Callback {

    private val holder: SurfaceHolder = holder.apply {
        addCallback(this@Preview)
        setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS)
    }

    override fun surfaceCreated(holder: SurfaceHolder) {
        // The Surface has been created, now tell the camera
        // where to draw the preview.
        camera.apply {
            try {
                setPreviewDisplay(holder)
                startPreview()
            } catch (e: IOException) {
                Log.d(TAG, "error setting camera preview", e)
            }
        }
    }

    override fun surfaceDestroyed(holder: SurfaceHolder) {
        // Take care of releasing the Camera preview in your activity.
    }

    override fun surfaceChanged(holder: SurfaceHolder, format: Int,
                                w: Int, h: Int) {
        // If your preview can change or rotate, take care of those
        // events here. Make sure to stop the preview before resizing
        // or reformatting it.
        if (holder.surface == null) {
            return  // The preview surface does not exist.
        }

        // Stop preview before making changes.
        try {
            camera.stopPreview()
        } catch (e: Exception) {
            // Tried to stop a non-existent preview; nothing to do.
        }

        // Set preview size and make any resize, rotate or
        // reformatting changes here.

        // Start preview with new settings.
        camera.apply {
            try {
                setPreviewDisplay(holder)
                startPreview()
            } catch (e: Exception) {
                Log.d(TAG, "error starting camera preview", e)
            }
        }
    }
}

class CameraActivity : AppCompatActivity() {
    private lateinit var viewBinding: ActivityMainBinding
    private var camera: Camera? = null
    private var preview: Preview? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        viewBinding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(viewBinding.root)

        // Create an instance of Camera.
        camera = getCameraInstance()

        preview = camera?.let {
            // Create the Preview view.
            Preview(this, it)
        }

        // Set the Preview view as the content of the activity.
        val cameraPreview: FrameLayout = viewBinding.cameraPreview
        cameraPreview.addView(preview)
    }
}

CameraX: CameraController

CameraX では、デベロッパーが管理する対象ははるかに少なくなっています。CameraController を使用する場合は、PreviewView も使用する必要があります。つまり、Preview UseCase は暗黙的に指定されるため、セットアップの手間が大幅に軽減されます。

// CameraX: set up a camera preview with a CameraController.

class MainActivity : AppCompatActivity() {
    private lateinit var viewBinding: ActivityMainBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        viewBinding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(viewBinding.root)

        // Create the CameraController and set it on the previewView.
        var cameraController = LifecycleCameraController(baseContext)
        cameraController.bindToLifecycle(this)
        val previewView: PreviewView = viewBinding.cameraPreview
        previewView.controller = cameraController
    }
}

CameraX: CameraProvider

CameraX の CameraProvider では PreviewView を使用する必要はありませんが、Camera1 と比較してプレビューのセットアップが大幅に簡素化されています。この例ではデモ用に PreviewView を使用していますが、より複雑なニーズがある場合は、カスタム SurfaceProvider を作成して setSurfaceProvider() に渡すことができます。

ここでは、Preview UseCaseCameraController のように暗黙的に指定されないため、設定する必要があります。

// CameraX: set up a camera preview with a CameraProvider.

// Use await() within a suspend function to get CameraProvider instance.
// For more details on await(), see the "Android development concepts"
// section above.
private suspend fun startCamera() {
    val cameraProvider = ProcessCameraProvider.getInstance(this).await()

    // Create Preview UseCase.
    val preview = Preview.Builder()
        .build()
        .also {
            it.setSurfaceProvider(
                viewBinding.viewFinder.surfaceProvider
            )
        }

    // Select default back camera.
    val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA

    try {
        // Unbind UseCases before rebinding.
        cameraProvider.unbindAll()

        // Bind UseCases to camera. This function returns a camera
        // object which can be used to perform operations like zoom,
        // flash, and focus.
        var camera = cameraProvider.bindToLifecycle(
            this, cameraSelector, useCases)

    } catch(exc: Exception) {
        Log.e(TAG, "UseCase binding failed", exc)
    }
})

...

// Call startCamera() in the setup flow of your app, such as in onViewCreated.
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)

    ...

    lifecycleScope.launch {
        startCamera()
    }
}

タップしてフォーカス

一般に、カメラ プレビューが画面に表示されている場合、ユーザーがプレビューをタップしたときにフォーカス ポイントが設定されます。

Camera1

Camera1 で「タップしてフォーカス」を実装するには、最適なフォーカス Area を計算して、Camera がフォーカスしようとする場所を示す必要があります。この AreasetFocusAreas() に渡されます。また、Camera に対して互換性のあるフォーカス モードを設定する必要があります。現在のフォーカス モードが FOCUS_MODE_AUTOFOCUS_MODE_MACROFOCUS_MODE_CONTINUOUS_VIDEOFOCUS_MODE_CONTINUOUS_PICTURE のいずれかである場合にのみ、フォーカス エリアが有効になります。

Area は、指定された重みを持つ長方形です。重みは 1~1000 の値で、複数設定されている場合はフォーカス Areas の優先順位付けに使用されます。この例では Area を 1 つだけ使用するため、重み値は重要ではありません。長方形の座標の範囲は -1000~1000 です。左上のポイントは(-1000、-1000)です。右下のポイントは(1000、1000)です。方向はセンサーの向き(センサーが認識する方向)を基準とし、Camera.setDisplayOrientation() の回転やミラーリングの影響を受けないため、タッチイベント座標をセンサー座標に変換する必要があります。

// Camera1: implement tap-to-focus.

class TapToFocusHandler : Camera.AutoFocusCallback {
    private fun handleFocus(event: MotionEvent) {
        val camera = camera ?: return
        val parameters = try {
            camera.getParameters()
        } catch (e: RuntimeException) {
            return
        }

        // Cancel previous auto-focus function, if one was in progress.
        camera.cancelAutoFocus()

        // Create focus Area.
        val rect = calculateFocusAreaCoordinates(event.x, event.y)
        val weight = 1  // This value's not important since there's only 1 Area.
        val focusArea = Camera.Area(rect, weight)

        // Set the focus parameters.
        parameters.setFocusMode(Parameters.FOCUS_MODE_AUTO)
        parameters.setFocusAreas(listOf(focusArea))

        // Set the parameters back on the camera and initiate auto-focus.
        camera.setParameters(parameters)
        camera.autoFocus(this)
    }

    private fun calculateFocusAreaCoordinates(x: Int, y: Int) {
        // Define the size of the Area to be returned. This value
        // should be optimized for your app.
        val focusAreaSize = 100

        // You must define functions to rotate and scale the x and y values to
        // be values between 0 and 1, where (0, 0) is the upper left-hand side
        // of the preview, and (1, 1) is the lower right-hand side.
        val normalizedX = (rotateAndScaleX(x) - 0.5) * 2000
        val normalizedY = (rotateAndScaleY(y) - 0.5) * 2000

        // Calculate the values for left, top, right, and bottom of the Rect to
        // be returned. If the Rect would extend beyond the allowed values of
        // (-1000, -1000, 1000, 1000), then crop the values to fit inside of
        // that boundary.
        val left = max(normalizedX - (focusAreaSize / 2), -1000)
        val top = max(normalizedY - (focusAreaSize / 2), -1000)
        val right = min(left + focusAreaSize, 1000)
        val bottom = min(top + focusAreaSize, 1000)

        return Rect(left, top, left + focusAreaSize, top + focusAreaSize)
    }

    override fun onAutoFocus(focused: Boolean, camera: Camera) {
        if (!focused) {
            Log.d(TAG, "tap-to-focus failed")
        }
    }
}

CameraX: CameraController

CameraController は、PreviewView のタッチイベントをリッスンして、「タップしてフォーカス」を自動的に処理します。setTapToFocusEnabled() で「タップしてフォーカス」を有効または無効にし、対応するゲッター isTapToFocusEnabled() で値を確認できます。

getTapToFocusState() メソッドは、CameraController のフォーカス状態の変化をトラッキングするための LiveData オブジェクトを返します。

// CameraX: track the state of tap-to-focus over the Lifecycle of a PreviewView,
// with handlers you can define for focused, not focused, and failed states.

val tapToFocusStateObserver = Observer { state ->
    when (state) {
        CameraController.TAP_TO_FOCUS_NOT_STARTED ->
            Log.d(TAG, "tap-to-focus init")
        CameraController.TAP_TO_FOCUS_STARTED ->
            Log.d(TAG, "tap-to-focus started")
        CameraController.TAP_TO_FOCUS_FOCUSED ->
            Log.d(TAG, "tap-to-focus finished (focus successful)")
        CameraController.TAP_TO_FOCUS_NOT_FOCUSED ->
            Log.d(TAG, "tap-to-focus finished (focused unsuccessful)")
        CameraController.TAP_TO_FOCUS_FAILED ->
            Log.d(TAG, "tap-to-focus failed")
    }
}

cameraController.getTapToFocusState().observe(this, tapToFocusStateObserver)

CameraX: CameraProvider

CameraProvider を使用する場合、「タップしてフォーカス」が機能するように設定する必要があります。この例では、PreviewView を使用していることを前提としています。そうでない場合は、カスタム Surface に適用するようにロジックを調整する必要があります。

PreviewView を使用する場合の手順は次のとおりです。

  1. タップイベントを処理するジェスチャー検出器を設定します。
  2. タップイベントで、MeteringPointFactory.createPoint() を使用して MeteringPoint を作成します。
  3. MeteringPointFocusMeteringAction を作成します。
  4. CameraCameraControl オブジェクト(bindToLifecycle() から返されたもの)を使用して startFocusAndMetering() を呼び出し、FocusMeteringAction を渡します。
  5. (省略可)FocusMeteringResult に応答します。
  6. PreviewView.setOnTouchListener() のタッチイベントに応答するジェスチャー検出器を設定します。
// CameraX: implement tap-to-focus with CameraProvider.

// Define a gesture detector to respond to tap events and call
// startFocusAndMetering on CameraControl. If you want to use a
// coroutine with await() to check the result of focusing, see the
// "Android development concepts" section above.
val gestureDetector = GestureDetectorCompat(context,
    object : SimpleOnGestureListener() {
        override fun onSingleTapUp(e: MotionEvent): Boolean {
            val previewView = previewView ?: return
            val camera = camera ?: return
            val meteringPointFactory = previewView.meteringPointFactory
            val focusPoint = meteringPointFactory.createPoint(e.x, e.y)
            val meteringAction = FocusMeteringAction
                .Builder(meteringPoint).build()
            lifecycleScope.launch {
                val focusResult = camera.cameraControl
                    .startFocusAndMetering(meteringAction).await()
                if (!result.isFocusSuccessful()) {
                    Log.d(TAG, "tap-to-focus failed")
                }
            }
        }
    }
)

...

// Set the gestureDetector in a touch listener on the PreviewView.
previewView.setOnTouchListener { _, event ->
    // See pinch-to-zooom scenario for scaleGestureDetector definition.
    var didConsume = scaleGestureDetector.onTouchEvent(event)
    if (!scaleGestureDetector.isInProgress) {
        didConsume = gestureDetector.onTouchEvent(event)
    }
    didConsume
}

ピンチしてズーム

プレビューのズームインとズームアウトも、カメラ プレビューの一般的な直接操作の一つです。デバイスのカメラの数が増えると、ユーザーはズームの結果として最適な焦点距離のレンズが自動的に選択されることも期待します。

Camera1

Camera1 を使用してズームするには、次の 2 つの方法があります。Camera.startSmoothZoom() メソッドは、現在のズームレベルから渡されたズームレベルにアニメーション化します。Camera.Parameters.setZoom() メソッドは、渡されたズームレベルに直接ジャンプします。いずれかのメソッドを使用する前に、isSmoothZoomSupported() または isZoomSupported() をそれぞれ呼び出し、必要な関連するズーム方法をカメラで利用できるようにします。

この例では、setZoom() を使用して「ピンチしてズーム」を実装します。これは、プレビュー サーフェスのタッチリスナーがピンチ操作の発生に応じて継続的にイベントを呼び出すことで、毎回直ちにズームレベルが更新されるためです。ZoomTouchListener クラスは以下に定義されており、プレビュー サーフェスのタッチリスナーへのコールバックとして設定する必要があります。

// Camera1: implement pinch-to-zoom.

// Define a scale gesture detector to respond to pinch events and call
// setZoom on Camera.Parameters.
val scaleGestureDetector = ScaleGestureDetector(context,
    object : ScaleGestureDetector.OnScaleGestureListener {
        override fun onScale(detector: ScaleGestureDetector): Boolean {
            val camera = camera ?: return false
            val parameters = try {
                camera.parameters
            } catch (e: RuntimeException) {
                return false
            }

            // In case there is any focus happening, stop it.
            camera.cancelAutoFocus()

            // Set the zoom level on the Camera.Parameters, and set
            // the Parameters back onto the Camera.
            val currentZoom = parameters.zoom
            parameters.setZoom(detector.scaleFactor * currentZoom)
        camera.setParameters(parameters)
            return true
        }
    }
)

// Define a View.OnTouchListener to attach to your preview view.
class ZoomTouchListener : View.OnTouchListener {
    override fun onTouch(v: View, event: MotionEvent): Boolean =
        scaleGestureDetector.onTouchEvent(event)
}

// Set a ZoomTouchListener to handle touch events on your preview view
// if zoom is supported by the current camera.
if (camera.getParameters().isZoomSupported()) {
    view.setOnTouchListener(ZoomTouchListener())
}

CameraX: CameraController

「タップしてフォーカス」と同様に、CameraController は PreviewView のタッチイベントをリッスンし、「ピンチしてズーム」を自動的に処理します。setPinchToZoomEnabled() で「ピンチしてズーム」を有効または無効にし、対応するゲッター isPinchToZoomEnabled() で値を確認できます。

getZoomState() メソッドは、CameraControllerZoomState に対する変更を追跡するための LiveData オブジェクトを返します。

// CameraX: track the state of pinch-to-zoom over the Lifecycle of
// a PreviewView, logging the linear zoom ratio.

val pinchToZoomStateObserver = Observer { state ->
    val zoomRatio = state.getZoomRatio()
    Log.d(TAG, "ptz-zoom-ratio $zoomRatio")
}

cameraController.getZoomState().observe(this, pinchToZoomStateObserver)

CameraX: CameraProvider

CameraProvider で「ピンチしてズーム」を行うには、いくつかの設定が必要です。PreviewView を使用していない場合は、カスタム Surface に適用するようにロジックを調整する必要があります。

PreviewView を使用する場合の手順は次のとおりです。

  1. ピンチイベントを処理するスケール ジェスチャー検出器を設定します。
  2. Camera.CameraInfo オブジェクトから ZoomState を取得します。ここで、bindToLifecycle() を呼び出したときに Camera インスタンスが返されます。
  3. ZoomStatezoomRatio 値がある場合は、現在のズーム倍率として保存します。ZoomStatezoomRatio がない場合は、カメラのデフォルトのズーム倍率(1.0)を使用します。
  4. scaleFactor で、現在のズーム倍率の積を求め、新しいズーム倍率を判断して、それを CameraControl.setZoomRatio() に渡します。
  5. PreviewView.setOnTouchListener() のタッチイベントに応答するジェスチャー検出器を設定します。
// CameraX: implement pinch-to-zoom with CameraProvider.

// Define a scale gesture detector to respond to pinch events and call
// setZoomRatio on CameraControl.
val scaleGestureDetector = ScaleGestureDetector(context,
    object : SimpleOnGestureListener() {
        override fun onScale(detector: ScaleGestureDetector): Boolean {
            val camera = camera ?: return
            val zoomState = camera.cameraInfo.zoomState
            val currentZoomRatio: Float = zoomState.value?.zoomRatio ?: 1f
            camera.cameraControl.setZoomRatio(
                detector.scaleFactor * currentZoomRatio
            )
        }
    }
)

...

// Set the scaleGestureDetector in a touch listener on the PreviewView.
previewView.setOnTouchListener { _, event ->
    var didConsume = scaleGestureDetector.onTouchEvent(event)
    if (!scaleGestureDetector.isInProgress) {
        // See pinch-to-zooom scenario for gestureDetector definition.
        didConsume = gestureDetector.onTouchEvent(event)
    }
    didConsume
}

写真の撮影

このセクションでは、シャッター ボタンを押下したとき、タイマーの経過後、または選択したその他のイベントで必要なときに、写真の撮影をトリガーする方法について説明します。

Camera1

Camera1 ではまず、リクエストされた写真データを管理するための Camera.PictureCallback を定義します。JPEG 画像データを処理するための PictureCallback の簡単な例を次に示します。

// Camera1: define a Camera.PictureCallback to handle JPEG data.

private val picture = Camera.PictureCallback { data, _ ->
    val pictureFile: File = getOutputMediaFile(MEDIA_TYPE_IMAGE) ?: run {
        Log.d(TAG,
              "error creating media file, check storage permissions")
        return@PictureCallback
    }

    try {
        val fos = FileOutputStream(pictureFile)
        fos.write(data)
        fos.close()
    } catch (e: FileNotFoundException) {
        Log.d(TAG, "file not found", e)
    } catch (e: IOException) {
        Log.d(TAG, "error accessing file", e)
    }
}

その後、写真を撮るたびに、Camera インスタンスで takePicture() メソッドを呼び出します。この takePicture() メソッドには、データ型ごとに異なる 3 つのパラメータがあります。1 つ目のパラメータは ShutterCallback 用です(この例では定義されていません)。2 つ目のパラメータは未加工(非圧縮)カメラデータを処理する PictureCallback 用です。3 つ目のパラメータは JPEG 画像データを処理する PictureCallback で、この例ではこれを使用します。

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

camera?.takePicture(null, null, picture)

CameraX: CameraController

CameraX の CameraController は、独自の takePicture() メソッドを実装することにより、Camera1 と同様のシンプルな画像撮影を実現しています。ここでは、MediaStore エントリを構成する関数を定義して、そこに保存する写真を撮影します。

// CameraX: define a function that uses CameraController to take a photo.

private val FILENAME_FORMAT = "yyyy-MM-dd-HH-mm-ss-SSS"

private fun takePhoto() {
   // Create time stamped name and MediaStore entry.
   val name = SimpleDateFormat(FILENAME_FORMAT, Locale.US)
              .format(System.currentTimeMillis())
   val contentValues = ContentValues().apply {
       put(MediaStore.MediaColumns.DISPLAY_NAME, name)
       put(MediaStore.MediaColumns.MIME_TYPE, "image/jpeg")
       if(Build.VERSION.SDK_INT > Build.VERSION_CODES.P) {
           put(MediaStore.Images.Media.RELATIVE_PATH, "Pictures/CameraX-Image")
       }
   }

   // Create output options object which contains file + metadata.
   val outputOptions = ImageCapture.OutputFileOptions
       .Builder(context.getContentResolver(),
            MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues)
       .build()

   // Set up image capture listener, which is triggered after photo has
   // been taken.
   cameraController.takePicture(
       outputOptions,
       ContextCompat.getMainExecutor(this),
       object : ImageCapture.OnImageSavedCallback {
           override fun onError(e: ImageCaptureException) {
               Log.e(TAG, "photo capture failed", e)
           }

           override fun onImageSaved(
               output: ImageCapture.OutputFileResults
           ) {
               val msg = "Photo capture succeeded: ${output.savedUri}"
               Toast.makeText(baseContext, msg, Toast.LENGTH_SHORT).show()
               Log.d(TAG, msg)
           }
       }
   )
}

CameraX: CameraProvider

CameraProvider で写真を撮る手順は CameraController の場合とほとんど同じですが、最初に ImageCapture UseCase を作成してバインドし、takePicture() を呼び出すオブジェクトを用意する必要があります。

// CameraX: create and bind an ImageCapture UseCase.

// Make a reference to the ImageCapture UseCase at a scope that can be accessed
// throughout the camera logic in your app.
private var imageCapture: ImageCapture? = null

...

// Create an ImageCapture instance (can be added with other
// UseCase definitions).
imageCapture = ImageCapture.Builder().build()

...

// Bind UseCases to camera (adding imageCapture along with preview here, but
// preview is not required to use imageCapture). This function returns a camera
// object which can be used to perform operations like zoom, flash, and focus.
var camera = cameraProvider.bindToLifecycle(
    this, cameraSelector, preview, imageCapture)

その後、写真を撮るたびに ImageCapture.takePicture() を呼び出します。takePhoto() 関数の完全な例については、このセクションの CameraController のコードをご覧ください。

// CameraX: define a function that uses CameraController to take a photo.

private fun takePhoto() {
    // Get a stable reference of the modifiable ImageCapture UseCase.
    val imageCapture = imageCapture ?: return

    ...

    // Call takePicture on imageCapture instance.
    imageCapture.takePicture(
        ...
    )
}

動画の録画

動画の録画は、これまでに見てきたシナリオよりもかなり複雑です。プロセスの各部分は、通常、特定の順序で適切に設定する必要があります。また、動画と音声が同期しているか確認したり、デバイスのその他の不整合に対処したりしなければならない場合があります。

これから説明するように、CameraX はこうした複雑な作業の多くを自動的に処理します。

Camera1

Camera1 を使用した動画撮影では、CameraMediaRecorder を慎重に管理する必要があります。また、メソッドは特定の順序で呼び出さなければなりません。アプリケーションを正しく動作させるには、必ず次の順序で呼び出します。

  1. カメラを起動します。
  2. プレビューを準備して開始します(アプリで録画中の動画を表示する場合は、通常これに該当します)。
  3. Camera.unlock() を呼び出して、MediaRecorder で使用するカメラのロックを解除します。
  4. MediaRecorder で次のメソッドを実行して、録画を構成します。
    1. Camera インスタンスを setCamera(camera) に接続します。
    2. setAudioSource(MediaRecorder.AudioSource.CAMCORDER) を呼び出します。
    3. setVideoSource(MediaRecorder.VideoSource.CAMERA) を呼び出します。
    4. setProfile(CamcorderProfile.get(CamcorderProfile.QUALITY_1080P)) を呼び出して画質を設定します。すべての画質オプションについては、CamcorderProfile をご覧ください。
    5. setOutputFile(getOutputMediaFile(MEDIA_TYPE_VIDEO).toString()) を呼び出します。
    6. アプリに動画のプレビューがある場合は、setPreviewDisplay(preview?.holder?.surface) を呼び出します。
    7. setOutputFormat(MediaRecorder.OutputFormat.MPEG_4) を呼び出します。
    8. setAudioEncoder(MediaRecorder.AudioEncoder.DEFAULT) を呼び出します。
    9. setVideoEncoder(MediaRecorder.VideoEncoder.DEFAULT) を呼び出します。
    10. prepare() を呼び出して、MediaRecorder の構成を完了します。
  5. 録画を開始するには、MediaRecorder.start() を呼び出します。
  6. 録画を停止するには、次のメソッドを呼び出します。繰り返しますが、必ず順番どおりに実行してください。
    1. MediaRecorder.stop() を呼び出します。
    2. 必要に応じて、MediaRecorder.reset() を呼び出して、現在の MediaRecorder 構成を削除します。
    3. MediaRecorder.release() を呼び出します。
    4. カメラをロックし、将来の MediaRecorder セッションで Camera.lock() を呼び出して使用できるようにします。
  7. プレビューを停止するには、Camera.stopPreview() を呼び出します。
  8. 最後に、Camera を解放して他のプロセスで使用できるようにするには、Camera.release() を呼び出します。

これらすべての手順をまとめると、次のようになります。

// Camera1: set up a MediaRecorder and a function to start and stop video
// recording.

// Make a reference to the MediaRecorder at a scope that can be accessed
// throughout the camera logic in your app.
private var mediaRecorder: MediaRecorder? = null
private var isRecording = false

...

private fun prepareMediaRecorder(): Boolean {
    mediaRecorder = MediaRecorder()

    // Unlock and set camera to MediaRecorder.
    camera?.unlock()

    mediaRecorder?.run {
        setCamera(camera)

        // Set the audio and video sources.
        setAudioSource(MediaRecorder.AudioSource.CAMCORDER)
        setVideoSource(MediaRecorder.VideoSource.CAMERA)

        // Set a CamcorderProfile (requires API Level 8 or higher).
        setProfile(CamcorderProfile.get(CamcorderProfile.QUALITY_HIGH))

        // Set the output file.
        setOutputFile(getOutputMediaFile(MEDIA_TYPE_VIDEO).toString())

        // Set the preview output.
        setPreviewDisplay(preview?.holder?.surface)

        setOutputFormat(MediaRecorder.OutputFormat.MPEG_4)
        setAudioEncoder(MediaRecorder.AudioEncoder.DEFAULT)
        setVideoEncoder(MediaRecorder.VideoEncoder.DEFAULT)

        // Prepare configured MediaRecorder.
        return try {
            prepare()
            true
        } catch (e: IllegalStateException) {
            Log.d(TAG, "preparing MediaRecorder failed", e)
            releaseMediaRecorder()
            false
        } catch (e: IOException) {
            Log.d(TAG, "setting MediaRecorder file failed", e)
            releaseMediaRecorder()
            false
        }
    }
    return false
}

private fun releaseMediaRecorder() {
    mediaRecorder?.reset()
    mediaRecorder?.release()
    mediaRecorder = null
    camera?.lock()
}

private fun startStopVideo() {
    if (isRecording) {
        // Stop recording and release camera.
        mediaRecorder?.stop()
        releaseMediaRecorder()
        camera?.lock()
        isRecording = false

        // This is a good place to inform user that video recording has stopped.
    } else {
        // Initialize video camera.
        if (prepareVideoRecorder()) {
            // Camera is available and unlocked, MediaRecorder is prepared, now
            // you can start recording.
            mediaRecorder?.start()
            isRecording = true

            // This is a good place to inform the user that recording has
            // started.
        } else {
            // Prepare didn't work, release the camera.
            releaseMediaRecorder()

            // Inform user here.
        }
    }
}

CameraX: CameraController

CameraX の CameraController を使用すると、ユースケースのリストを同時に使用できる限りImageCaptureVideoCaptureImageAnalysisUseCase を個別に切り替えることができます。 ImageCaptureImageAnalysisUseCase はデフォルトで有効になっているため、写真を撮るために setEnabledUseCases() を呼び出す必要はありません。

動画の録画に CameraController を使用するには、まず setEnabledUseCases() を使用して VideoCapture UseCase を許可する必要があります。

// CameraX: Enable VideoCapture UseCase on CameraController.

cameraController.setEnabledUseCases(VIDEO_CAPTURE);

動画の録画を開始するには、CameraController.startRecording() 関数を呼び出します。以下の例に示すように、この関数を使用して、録画された動画を File に保存できます。また、成功とエラーのコールバックを処理するために、Executor と、OnVideoSavedCallback を実装するクラスを渡す必要があります。録画が終了したら、CameraController.stopRecording() を呼び出します。

注: CameraX 1.3.0-alpha02 以降を使用している場合は、AudioConfig パラメータを追加して、動画の音声録音を有効または無効にできます。音声録音を有効にするには、マイクの権限が必要です。また、stopRecording() メソッドは 1.3.0-alpha02 では削除されており、startRecording() は動画の録画の一時停止、再開、停止に使用できる Recording オブジェクトを返します。

// CameraX: implement video capture with CameraController.

private val FILENAME_FORMAT = "yyyy-MM-dd-HH-mm-ss-SSS"

// Define a VideoSaveCallback class for handling success and error states.
class VideoSaveCallback : OnVideoSavedCallback {
    override fun onVideoSaved(outputFileResults: OutputFileResults) {
        val msg = "Video capture succeeded: ${outputFileResults.savedUri}"
        Toast.makeText(baseContext, msg, Toast.LENGTH_SHORT).show()
        Log.d(TAG, msg)
    }

    override fun onError(videoCaptureError: Int, message: String,
                         cause: Throwable?) {
        Log.d(TAG, "error saving video: $message", cause)
    }
}

private fun startStopVideo() {
    if (cameraController.isRecording()) {
        // Stop the current recording session.
        cameraController.stopRecording()
        return
    }

    // Define the File options for saving the video.
    val name = SimpleDateFormat(FILENAME_FORMAT, Locale.US)
        .format(System.currentTimeMillis())

    val outputFileOptions = OutputFileOptions
        .Builder(File(this.filesDir, name))
        .build()

    // Call startRecording on the CameraController.
    cameraController.startRecording(
        outputFileOptions,
        ContextCompat.getMainExecutor(this),
        VideoSaveCallback()
    )
}

CameraX: CameraProvider

CameraProvider を使用している場合は、VideoCapture UseCase を作成し、Recorder オブジェクトを渡す必要があります。Recorder.Builder では、動画の画質を設定できます。また、必要に応じて FallbackStrategy を設定することで、デバイスが望ましい画質仕様を満たさないケースを処理できます。その後、他の UseCase を使用して、VideoCapture インスタンスを CameraProvider にバインドします。

// CameraX: create and bind a VideoCapture UseCase with CameraProvider.

// Make a reference to the VideoCapture UseCase and Recording at a
// scope that can be accessed throughout the camera logic in your app.
private lateinit var videoCapture: VideoCapture
private var recording: Recording? = null

...

// Create a Recorder instance to set on a VideoCapture instance (can be
// added with other UseCase definitions).
val recorder = Recorder.Builder()
    .setQualitySelector(QualitySelector.from(Quality.FHD))
    .build()
videoCapture = VideoCapture.withOutput(recorder)

...

// Bind UseCases to camera (adding videoCapture along with preview here, but
// preview is not required to use videoCapture). This function returns a camera
// object which can be used to perform operations like zoom, flash, and focus.
var camera = cameraProvider.bindToLifecycle(
    this, cameraSelector, preview, videoCapture)

この時点で、RecordervideoCapture.output プロパティからアクセスできるようになります。Recorder で動画の録画を開始し、その動画を FileParcelFileDescriptor、または MediaStore に保存できます。この例では MediaStore を使用しています。

Recorder には、呼び出して準備を行うためのメソッドがいくつかあります。prepareRecording() を呼び出して MediaStore 出力オプションを設定します。アプリにデバイスのマイクを使用する権限がある場合は、withAudioEnabled() も呼び出します。次に、start() を呼び出して録画を開始し、コンテキストと Consumer<VideoRecordEvent> イベント リスナーを渡して、動画の録画イベントを処理します。成功した場合、返された Recording を使用して、録画を一時停止、再開、停止できます。

// CameraX: implement video capture with CameraProvider.

private val FILENAME_FORMAT = "yyyy-MM-dd-HH-mm-ss-SSS"

private fun startStopVideo() {
   val videoCapture = this.videoCapture ?: return

   if (recording != null) {
       // Stop the current recording session.
       recording.stop()
       recording = null
       return
   }

   // Create and start a new recording session.
   val name = SimpleDateFormat(FILENAME_FORMAT, Locale.US)
       .format(System.currentTimeMillis())
   val contentValues = ContentValues().apply {
       put(MediaStore.MediaColumns.DISPLAY_NAME, name)
       put(MediaStore.MediaColumns.MIME_TYPE, "video/mp4")
       if (Build.VERSION.SDK_INT > Build.VERSION_CODES.P) {
           put(MediaStore.Video.Media.RELATIVE_PATH, "Movies/CameraX-Video")
       }
   }

   val mediaStoreOutputOptions = MediaStoreOutputOptions
       .Builder(contentResolver, MediaStore.Video.Media.EXTERNAL_CONTENT_URI)
       .setContentValues(contentValues)
       .build()

   recording = videoCapture.output
       .prepareRecording(this, mediaStoreOutputOptions)
       .withAudioEnabled()
       .start(ContextCompat.getMainExecutor(this)) { recordEvent ->
           when(recordEvent) {
               is VideoRecordEvent.Start -> {
                   viewBinding.videoCaptureButton.apply {
                       text = getString(R.string.stop_capture)
                       isEnabled = true
                   }
               }
               is VideoRecordEvent.Finalize -> {
                   if (!recordEvent.hasError()) {
                       val msg = "Video capture succeeded: " +
                           "${recordEvent.outputResults.outputUri}"
                       Toast.makeText(
                           baseContext, msg, Toast.LENGTH_SHORT
                       ).show()
                       Log.d(TAG, msg)
                   } else {
                       recording?.close()
                       recording = null
                       Log.e(TAG, "video capture ends with error",
                             recordEvent.error)
                   }
                   viewBinding.videoCaptureButton.apply {
                       text = getString(R.string.start_capture)
                       isEnabled = true
                   }
               }
           }
       }
}

参考情報

カメラ サンプル GitHub リポジトリには、完全な CameraX アプリがいくつか用意されています。これらのサンプルは、このガイドで紹介したシナリオがどのように本格的な Android アプリに組み込まれるかを示しています。

CameraX への移行について追加のサポートが必要な場合や、Android Camera API のスイートについてご不明な点がある場合は、CameraX ディスカッション グループからお問い合わせください。