Android
 Computer >> コンピューター >  >> システム >> Android

Android Camera2 API徹底解説 – 写真・動画を撮影するアプリの作り方

スマートフォンのカメラは、私たちが最も頻繁に使う機能の一つです。中には、カメラ機能をアプリに組み込んでいるケースも数多くあります。

カメラとの連携には、標準的な方法と、カスタマイズされた高度な方法の2つがあります。この違いを理解することは非常に重要であり、そこで登場するのがCamera2 APIです。

Camera2とは?

Camera2 APIはAPIレベル21から利用可能になっていますが、開発者が扱うアーキテクチャの中でも特に複雑な部類に入ります。

このAPIとその前身となるAPIは、開発者がアプリケーション内からカメラを操作できるようにするために用意されました。マイクや音量を操作できるのと同じように、Camera2 APIはデバイスのカメラを操作するためのツールを提供します。

一般的に、Camera2 APIを使う場面は、単に写真を撮ったり動画を録画したりする以上のことをしたい場合です。というのも、このAPIはカメラへの深い制御を可能にする一方で、デバイスごとに設定が必要なさまざまなクラスが公開されているためです。

以前のカメラAPIを扱ったことがある人でも、その変更はあまりにも大きいため、今までの知識はいったん忘れてしまったほうがよいでしょう。

このAPIの使い方を解説するリソースは数多く存在しますが、古くなっているものや、全体像を示していないものも少なくありません。欠けている部分を自分で補う代わりに、本記事がCamera2 APIを扱うための総合ガイドとなれば幸いです。

Camera2 APIを使うべきケース

本題に入る前に、重要な点を押さえておきましょう。単に写真を撮影したり動画を録画したりしたいだけであれば、Camera2 APIを使う必要はありません。

Camera2 APIを使う主な理由は、アプリがカメラやその機能に対してカスタムな操作を必要とする場合です。

もし前者(シンプルな撮影)で十分な場合は、Googleの以下のドキュメントを参照することをおすすめします。

  1. 写真の撮影(Take Photos)
  2. 動画のキャプチャ(Capture Video)

そこには、素晴らしい写真や動画を撮影するために必要な手順がすべて記載されています。ただし、本記事ではCamera2 APIの使い方に焦点を当てます。

まず、マニフェストファイルに以下を追加する必要があります。

カメラのパーミッション:

<uses-permission android:name="android.permission.CAMERA" />

カメラ機能の宣言:

<uses-feature android:name="android.hardware.camera" />

カメラのパーミッションが許可されているかどうかのチェックも必要ですが、このトピックはすでに広く解説されているため、本記事では扱いません。

Camera2 APIコンポーネントのセットアップ方法

Camera2 APIでは、いくつかの新しいインターフェースやクラスが導入されています。それぞれを分解して、使い方をより深く理解していきましょう。

Android Camera2 API徹底解説 – 写真・動画を撮影するアプリの作り方
多数のコンポーネントで構成されている

まずはTextureViewから始めましょう。

Camera2 の TextureView コンポーネント

TextureViewは、コンテンツストリーム(動画などをイメージしてください)を表示するためのUIコンポーネントです。プレビューの表示や写真・動画の撮影前の画面など、カメラからの映像を表示するためにTextureViewを使用します。

TextureViewに関して重要な2つの要素があります。

  • SurfaceTextureフィールド
  • SurfaceTextureListenerインターフェース

前者はコンテンツが表示される場所であり、後者には4つのコールバックがあります。

  1. onSurfaceTextureAvailable
  2. onSurfaceTextureSizeChanged
  3. onSurfaceTextureUpdated
  4. onSurfaceTextureDestroyed
private val surfaceTextureListener = object : TextureView.SurfaceTextureListener {
        override fun onSurfaceTextureAvailable(texture: SurfaceTexture, width: Int, height: Int) {

        }
        override fun onSurfaceTextureSizeChanged(texture: SurfaceTexture, width: Int, height: Int) {
        
        }
        
        override fun onSurfaceTextureDestroyed(texture: SurfaceTexture) {
           
        }
        override fun onSurfaceTextureUpdated(texture: SurfaceTexture) {
          
        }
}

カメラを使う上で最初のコールバックが極めて重要です。SurfaceTextureが利用可能になったタイミングで通知を受け取り、そこへ映像を表示し始められるようにするためです。

なお、TextureViewがウィンドウにアタッチされて初めて利用可能になる点に注意してください。

前世代のAPIと比べ、カメラとのやり取りの方法は大きく変わりました。現在はCameraManagerを使用します。これはシステムサービスであり、CameraDeviceオブジェクトとのやり取りを可能にします。

特に注目すべきメソッドは以下の3つです。

  • openCamera
  • getCameraCharacteristics
  • getCameraIdList

TextureViewが利用可能になり準備が整ったら、openCameraを呼び出してカメラへの接続を開きます。このメソッドは3つの引数を受け取ります。

  1. CameraId(文字列)
  2. CameraDevice.StateCallback
  3. Handler

CameraId引数は、どのカメラに接続するかを示します。スマートフォンには通常、前面カメラと背面カメラの2つがあり、それぞれ固有のIDを持っています。多くの場合、「0」か「1」のいずれかです。

では、カメラIDはどうやって取得するのでしょうか。CameraManagerのgetCamerasIdListメソッドを使います。このメソッドは、デバイスで認識されたすべてのカメラIDを文字列型の配列として返します。

val cameraManager: CameraManager = getSystemService(Context.CAMERA_SERVICE) as CameraManager
val cameraIds: Array<String> = cameraManager.cameraIdList
var cameraId: String = ""
for (id in cameraIds) {
    val cameraCharacteristics = cameraManager.getCameraCharacteristics(id)
    //前面カメラではなく背面カメラを選択したい場合
    if (cameraCharacteristics.get(CameraCharacteristics.LENS_FACING) == CameraCharacteristics.LENS_FACING_FRONT) 
      continue
    }
    
    val previewSize = cameraCharacteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP)!!.getOutputSizes(ImageFormat.JPEG).maxByOrNull { it.height * it.width }!!
    val imageReader = ImageReader.newInstance(previewSize.width, previewSize.height, ImageFormat.JPEG, 1)
    imageReader.setOnImageAvailableListener(onImageAvailableListener, backgroundHandler)
    cameraId = id
}

次の引数は、カメラを開こうとした際の状態変化を受け取るコールバックです。考えてみれば、この操作の結果は限られています。

  • カメラが正常に開いた
  • カメラが切断された
  • 何らかのエラーが発生した

これらはまさにCameraDevice.StateCallbackの中に定義されています。

 private val cameraStateCallback = object : CameraDevice.StateCallback() {
        override fun onOpened(camera: CameraDevice) {
           
        }

        override fun onDisconnected(cameraDevice: CameraDevice) {
           
        }

        override fun onError(cameraDevice: CameraDevice, error: Int) {
            val errorMsg = when(error) {
                ERROR_CAMERA_DEVICE -> "Fatal (device)"
                ERROR_CAMERA_DISABLED -> "Device policy"
                ERROR_CAMERA_IN_USE -> "Camera in use"
                ERROR_CAMERA_SERVICE -> "Fatal (service)"
                ERROR_MAX_CAMERAS_IN_USE -> "Maximum cameras in use"
                else -> "Unknown"
            }
            Log.e(TAG, "Error when trying to connect camera $errorMsg")
        }
    }

3番目の引数は、この処理をどこで実行するかを扱います。メインスレッドを占有したくないため、バックグラウンドで処理を行うのが望ましいでしょう。

そのため、Handlerを渡す必要があります。任意のスレッドでハンドラーインスタンスを生成しておけば、処理を委譲できます。

private lateinit var backgroundHandlerThread: HandlerThread
private lateinit var backgroundHandler: Handler

 private fun startBackgroundThread() {
    backgroundHandlerThread = HandlerThread("CameraVideoThread")
    backgroundHandlerThread.start()
    backgroundHandler = Handler(
        backgroundHandlerThread.looper)
}

private fun stopBackgroundThread() {
    backgroundHandlerThread.quitSafely()
    backgroundHandlerThread.join()
}

ここまでの準備が整えば、openCameraを呼び出せます。

cameraManager.openCamera(cameraId, cameraStateCallback,backgroundHandler)

そしてonOpenedコールバック内で、TextureView経由でユーザーにカメラ映像を表示するロジックの処理を始められます。

Android Camera2 API徹底解説 – 写真・動画を撮影するアプリの作り方
Photo by Markus Spiske on Unsplash

カメラ映像のプレビューを表示する方法

カメラ(cameraDevice)と映像を表示するTextureViewは揃いました。しかし、両者を接続してプレビューを表示する必要があります。

そのために、TextureViewのSurfaceTextureプロパティを使用し、CaptureRequestを構築します。

val surfaceTexture : SurfaceTexture? = textureView.surfaceTexture // 1

val cameraCharacteristics = cameraManager.getCameraCharacteristics(cameraId) //2
val previewSize = cameraCharacteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP)!!
  .getOutputSizes(ImageFormat.JPEG).maxByOrNull { it.height * it.width }!!

surfaceTexture?.setDefaultBufferSize(previewSize.width, previewSize.height) //3

val previewSurface: Surface = Surface(surfaceTexture)

captureRequestBuilder = cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW) //4
captureRequestBuilder.addTarget(previewSurface) //5

cameraDevice.createCaptureSession(listOf(previewSurface, imageReader.surface), captureStateCallback, null) //6
プレビューの作成

上記のコードでは、まずTextureViewからsurfaceTextureを取得します。次にcameraCharacteristicsオブジェクトを使って、すべての出力サイズのリストを取得します。希望するサイズを決めたら、それをsurfaceTextureに設定します。

続いて、TEMPLATE_PREVIEWを渡してcaptureRequestを作成し、入力サーフェスをcaptureRequestに追加します。

最後に、入力・出力サーフェスとcaptureStateCallbackを指定してcaptureSessionを開始し、ハンドラーにはnullを渡します。

さて、このcaptureStateCallbackとは何でしょうか。冒頭の図を覚えていれば、これは開始しようとしているCameraCaptureSessionの一部であることがわかります。このオブジェクトは、以下のコールバックによってcaptureRequestの進行状況を追跡します。

  • onConfigured
  • onConfigureFailed
private val captureStateCallback = object : CameraCaptureSession.StateCallback() {
        override fun onConfigureFailed(session: CameraCaptureSession) {
            
        }
        override fun onConfigured(session: CameraCaptureSession) {
         
        }
}

cameraCaptureSessionの設定が成功すると、セッションに対してrepeating requestを設定することで、プレビューを継続的に表示できるようになります。

そのために、コールバックで受け取るsessionオブジェクトを使用します。

 session.setRepeatingRequest(captureRequestBuilder.build(), null, backgroundHandler)

第1引数には、先ほど作成したcaptureRequestBuilderオブジェクトが使われていることに気づくでしょう。buildメソッドを実行することで、最終的なパラメータとしてCaptureRequestが渡されます。

第2引数はCameraCaptureSession.captureCallbackリスナーですが、これはプレビューなのでキャプチャした画像に対して何もする必要がなく、nullを渡しています。

第3引数はハンドラーで、ここでは独自のbackgroundHandlerを使用します。repeating requestはバックグラウンドスレッドで動作するため、前のセクションでnullを渡したのもこの理由によるものです。

Android Camera2 API徹底解説 – 写真・動画を撮影するアプリの作り方
Photo by Dicky Jiang on Unsplash

写真を撮影する方法

カメラのライブプレビューが表示できるのは素晴らしいことですが、ほとんどのユーザーはそれを使って何かをしたいはずです。写真撮影のために書くロジックの一部は、前のセクションで行ったことと似ています。

  1. captureRequestを作成する
  2. ImageReaderとそのリスナーを使って撮影した写真を取得する
  3. cameraCaptureSessionを使ってcaptureメソッドを呼び出す
val orientations : SparseIntArray = SparseIntArray(4).apply {
    append(Surface.ROTATION_0, 0)
    append(Surface.ROTATION_90, 90)
    append(Surface.ROTATION_180, 180)
    append(Surface.ROTATION_270, 270)
}

val captureRequestBuilder = cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE)
captureRequestBuilder.addTarget(imageReader.surface)

val rotation = windowManager.defaultDisplay.rotation
captureRequestBuilder.set(CaptureRequest.JPEG_ORIENTATION, orientations.get(rotation))
cameraCaptureSession.capture(captureRequestBuilder.build(), captureCallback, null)
今回はTEMPLATE_STILL_CAPTUREを指定してcapture requestを作成している

ところで、このImageReaderとは何でしょうか。ImageReaderは、サーフェスにレンダリングされた画像データへのアクセスを提供するクラスです。今回の場合、それはTextureViewのサーフェスです。

前のセクションのコードを見返すと、すでにImageReaderが定義されていたことに気づくでしょう。

val cameraManager: CameraManager = getSystemService(Context.CAMERA_SERVICE) as CameraManager
val cameraIds: Array<String> = cameraManager.cameraIdList
var cameraId: String = ""
for (id in cameraIds) {
    val cameraCharacteristics = cameraManager.getCameraCharacteristics(id)
    //前面カメラではなく背面カメラを選択したい場合
    if (cameraCharacteristics.get(CameraCharacteristics.LENS_FACING) == CameraCharacteristics.LENS_FACING_FRONT) 
      continue
    }
    
    val previewSize = cameraCharacteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP)!!.getOutputSizes(ImageFormat.JPEG).maxByOrNull { it.height * it.width }!!
    val imageReader = ImageReader.newInstance(previewSize.width, previewSize.height, ImageFormat.JPEG, 1)
    imageReader.setOnImageAvailableListener(onImageAvailableListener, backgroundHandler)
    cameraId = id
}
12〜14行目に注目

上記のように、幅と高さ、画像のフォーマット、そして保持できる画像の枚数を渡してImageReaderをインスタンス化します。

ImageReaderクラスにはonImageAvailableListenerというリスナーがあります。このリスナーは、写真が撮影されるたびに発火します(capture requestの出力先としてそのサーフェスを指定しているためです)。

val onImageAvailableListener = object: ImageReader.OnImageAvailableListener{
        override fun onImageAvailable(reader: ImageReader) {
            val image: Image = reader.acquireLatestImage()
        }
    }

⚠️ 画像を処理した後は必ずcloseを呼び出してください。そうしないと、次の写真が撮影できなくなります。

Android Camera2 API徹底解説 – 写真・動画を撮影するアプリの作り方
Photo by Jakob Owens on Unsplash

動画を録画する方法

動画を録画するには、MediaRecorderという新しいオブジェクトを扱う必要があります。MediaRecorderは音声と動画の録画を担当するオブジェクトで、まさにその目的のために使用します。

何をするにも先立って、MediaRecorderのセットアップが必要です。設定項目は多数あり、正しい順序で呼び出さないと例外がスローされます

以下は、動画(音声なし)をキャプチャするための設定の一例です。

fun setupMediaRecorder(width: Int, height: Int) {
  val mediaRecorder: MediaRecorder = MediaRecorder()
  mediaRecorder.setVideoSource(MediaRecorder.VideoSource.SURFACE)
  mediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4)
  mediaRecorder.setVideoEncoder(MediaRecorder.VideoEncoder.H264)
  mediaRecorder.setVideoSize(videoSize.width, videoSize.height)
  mediaRecorder.setVideoFrameRate(30)
  mediaRecorder.setOutputFile(PATH_TO_FILE)
  mediaRecorder.setVideoEncodingBitRate(10_000_000)
  mediaRecorder.prepare()
}
setterの呼び出し順序が重要

setOutputFileメソッドに注意してください。このメソッドは、動画を保存するファイルのパスを期待しています。すべての設定が完了したら、最後にprepareを呼び出します。

MediaRecorderにはstartメソッドもあり、それを呼び出す前に必ずprepareを呼ぶ必要がある点に留意してください。

MediaRecorderのセットアップが済んだら、capture requestとcapture sessionを作成します。

fun startRecording() {
        val surfaceTexture : SurfaceTexture? = textureView.surfaceTexture
        surfaceTexture?.setDefaultBufferSize(previewSize.width, previewSize.height)
        val previewSurface: Surface = Surface(surfaceTexture)
        val recordingSurface = mediaRecorder.surface
        captureRequestBuilder = cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_RECORD)
        captureRequestBuilder.addTarget(previewSurface)
        captureRequestBuilder.addTarget(recordingSurface)

        cameraDevice.createCaptureSession(listOf(previewSurface, recordingSurface), captureStateVideoCallback, backgroundHandler)
    }

プレビューや静止画撮影の場合と同様に、入力サーフェスと出力サーフェスを定義する必要があります。

ここでは、TextureViewのsurfaceTextureからSurfaceオブジェクトを作成するとともに、MediaRecorderからもサーフェスを取得しています。capture requestの作成時にはTEMPLATE_RECORDの値を渡します。

captureStateVideoCallbackは静止画の場合と同じ型ですが、onConfiguredコールバック内でMediaRecorderのstartメソッドを呼び出す点が異なります。

val captureStateVideoCallback = object : CameraCaptureSession.StateCallback() {
      override fun onConfigureFailed(session: CameraCaptureSession) {
         
      }
      
      override fun onConfigured(session: CameraCaptureSession) {
          session.setRepeatingRequest(captureRequestBuilder.build(), null, backgroundHandler)
          mediaRecorder.start()
      }
  }
連続的な動画をキャプチャしたいので、ここでもrepeating requestを設定している

これで動画の録画が始まりました。では、録画を停止するにはどうすればよいのでしょうか。そのためには、mediaRecorderオブジェクトのstopメソッドとresetメソッドを使用します。

mediaRecorder.stop()
mediaRecorder.reset()

まとめ

盛りだくさんの内容でした。ここまで読み進められたなら、おめでとうございます! 回避策はありません。実際にコードを書いて手を動かしてこそ、すべてがどう連携しているのかが理解できるようになります。

本記事で紹介したコードはすべて、以下のリポジトリで確認できます。ぜひ参考にしてください。

Bear in mind that this is just the tip of the iceberg when it comes to the Camera2 API. スローモーション動画の撮影、前面・背面カメラの切り替え、フォーカスの制御など、Camera2 APIでできることはまだまだたくさんあります。

  1. iPhoneで写真・動画を非表示にする方法|標準機能とロックアプリの使い方を徹底解説

    iPhoneで撮ったプライベートな写真をしっかり守りたいと思っている方は多いのではないでしょうか。つい何枚でも撮影したくなるのがiPhoneの魅力ですが、友人に写真を見せている最中に、うっかり個人的な写真まで見られてしまう…そんな経験はありませんか。 ご安心ください。この記事では、iPhoneで写真を隠す・ロックする方法をわかりやすく解説します。 実はiPhoneには標準で「非表示」アルバムが搭載されており、写真や動画を目立たない場所にしまっておくことができます。ただし、この方法は完全なセキュリティ対策とは言えません。そこで本記事では、写真を隠すことに特化したサードパーティ製アプリも併せてご紹

  2. デジタル保管庫のシークレットカメラ機能で写真を隠す方法【Keep Photos Secret】

    最新のAndroidスマートフォンには高性能なカメラが搭載されており、誰もが手軽に写真撮影を楽しめるようになりました。しかし、Androidには「他人に見られたくない写真を隠しておける秘密の保管庫」という機能が標準では備わっていません。そこで本記事では、Androidスマホで写真を非表示にできるおすすめアプリとして、Google Playストアで配信されているSystweak Software社開発の「Keep Photos Secret」をご紹介します。このアプリは、写真や動画を隠せるだけでなく、アプリ内カメラで撮影した画像を直接秘密の保管庫へ保存できる「シークレットカメラ」機能も備えていま