libwebrtc (Android) のカメラ API
はじめに
libwebrtc は Android, iOS 共にデバイスのカメラとマイクを操作する API を提供しています。両プラットフォームの API は統一されておらず、大まかな処理の流れは似ていますが個別に覚える必要があります。 Android 版はクラスとインターフェースの数が多くて把握に苦労したのでメモしておきます。カメラの映像を深くカスタマイズしたい方 (意外と少なくないと思います) のお役に立てれば幸いです。
なお、 iOS 版ではデバイス操作に関してあまり凝った API は提供されていません。内部ではわりと OS と密結合に実装されており、カスタマイズの余地は Android より少ないです。
諸注意
- この記事は M83 時点での情報です。
- 記事中のコードは Kotlin です。元のコードが Java の場合は Kotlin の表記に変更しています。
処理の流れ
カメラの映像の取得から描画までの処理は次に示す API 群で構成されます。
- デバイス管理: カメラにアクセスします。
- 映像キャプチャー: カメラの映像を取得します。
- セッション管理: 動作中のカメラを管理します。
- 映像の制御: カメラが取得した映像やリモートから受信した映像を制御します。
- UI コンポーネント: 映像を画面に描画します。
- 映像の送受信: リモートピアとの映像の送受信を行います。
- 映像コーデック: 映像のエンコード・デコードを行います。
API の関係を整理した図を以下に示します。ご笑納ください。
デバイス管理
まずはカメラにアクセスしなくては始まりません。カメラへのアクセスは CameraEnumerator
インターフェースを実装したオブジェクトで行います。 以下に定義を示します。
interface CameraEnumerator {
fun getDeviceNames(): Array<String>
fun isFrontFacing(deviceName: String): Boolean
fun isBackFacing(deviceName: String): Boolean
fun getSupportedFormats(deviceName: String): List<CaptureFormat>
fun createCapturer(deviceName: String,
eventsHandler: CameraVideoCapturer.CameraEventsHandler): CameraVideoCapturer
}
CameraEnumerator
では使用可能なカメラ (の名前) とサポートする映像フォーマットのリストを取得でき、映像キャプチャー (CameraVideoCapturer
) オブジェクトを生成できます。生成した映像キャプチャーを使ってカメラの映像を取得します。初めて起動したアプリでは、ここでカメラのパーミッションがユーザーに要求されます。
このインターフェースを実装したクラスは Camera1Enumerator
と Camera2Enumerator
です。数字が異なるだけの両者の違いは「対応する Android のバージョン」です。 Camera1Enumerator
は Android 5.0 Lolipop 以前向けの実装で、 5.0 以降に対応した実装が Camera2Enumerator
です。これから同様のクラス名がいくつか登場しますが、いずれも違いは同じです。現在は基本的に “2” のつくクラスのみを気にすればいいでしょう。
Camera2Enumerator
オブジェクトはコンストラクタで生成できます。 iOS 開発の感覚だとシングルトンインスタンスがありそうな感じしますがないです。
var enumerator = Camera2Enumerator(context) // android.content.Context
var capturer = enumerator.createCapturer(deviceName, null)
使用可能なカメラのリストは getDeviceNames()
で取得できます。使用するカメラを指定し、映像キャプチャーを生成して次に進みます。
映像キャプチャー
インターフェースの継承関係も相まって最も複雑な部分です。何がわかりにくかったかって、 VideoCapturer
を継承した CameraVideoCapturer
を継承した抽象クラスの名前が CameraCapturer
である点です。 Video
どこいった?
VideoCapturer
,CameraVideoCapturer
の定義を以下に示します。 CameraCapturer
にはこれらのインターフェース以外に特に有用な API はないので省略します。また、CameraCapturer
は public
な API ではないのでサブクラスは作れませんが、具象クラスである Camera1Capturer
と Camera2Capturer
が使用可能です。
interface VideoCapturer {
fun initialize(surfaceTextureHelper: SurfaceTextureHelper,
applicationContext: Context,
capturerObserver: CapturerObserver)
fun startCapture(width: Int, height: Int, framerate: Int)
@Throws(InterruptedException::class)
fun stopCapture()
fun changeCaptureFormat(width: Int, height: Int, framerate: Int)
fun dispose()
fun isScreencast(): Boolean
}interface CameraVideoCapturer : VideoCapturer {
fun switchCamera(switchEventsHandler: CameraSwitchHandler?)
fun switchCamera(switchEventsHandler: CameraSwitchHandler?, cameraName: String?)
}
VideoCapturer
と CameraVideoCapturer
は実質的に CameraCapturer
と考えてもらっても大丈夫です (以降そうします) 。 CameraCapturer
は抽象クラスなので、ユーザーが使うのは具象クラスである Camera1Capturer
か Camera2Capturer
です。前述の通り、両クラスの違いは対応する Android バージョンです。
CameraCapturer
の主な役割を次に示します。
- キャプチャーの初期化
- キャプチャーの起動・停止
- 取得した映像フレームを
CapturerObserver
に渡す - 使用するカメラの変更
ポイントとなるのは CapturerObserver
です。 CapturerObserver
はキャプチャーのイベントハンドラ (リスナー) で、キャプチャーの起動・停止と映像フレーム取得時のイベントを受け取ります。
interface CapturerObserver {
fun onCapturerStarted(success: Boolean)
fun onCapturerStopped()
fun onFrameCaptured(frame: VideoFrame)
}
CapturerObserver
は SurfaceTextureHelper
と共にキャプチャーの初期化に使われます。 後述する VideoSource
が CapturerObserver
を内部で生成して使うので、通常ユーザーが実装する必要はありません。
SurfaceTextureHelper
は CameraVideoCapturer
が取得したカメラの映像を libwebrtc が処理できるフォーマット (VideoFrame
) に変換します。普通にコンストラクタで生成できます。
ここまでの流れをまとめると、カメラが取得した映像は SurfaceTextureHelper
で映像フレームに変換され、映像フレームは CapturerObserver
を通して VideoSource
に渡ります。配信と描画まであと一歩です。
セッション管理
カメラの起動から停止までの期間をセッションとし、 CameraSession
で表されます。 CameraSession
の具象クラスの実装では、直接カメラにアクセスして起動と停止を行います。 CameraSession
は CameraCapturer
によって生成されます。 VideoCapturer
と CameraVideoCapturer
の定義にはセッションが登場しないので、 CameraCapturer
と CameraSession
は実装方法の一つと言えます。
CameraSession
は CameraCapturer
の内部でのみ使われるので、ユーザーはセッションにアクセスできません。キャプチャーを停止するには CameraCapturer.stopCapture()
メソッドを呼びます。
CameraSession
の具象クラス (Camera1Session
, Camera2Session
) の違いも、前述の他の具象クラスと同様です。
映像の制御
ここまでの処理はカメラの映像の取得でした。ここからの処理は取得した映像の制御になります。
ソース
CapturerObserver
を通して取得したカメラの映像 (VideoFrame
) は、 VideoSource
に渡されます。 VideoSource
は映像の供給源となり、トラック (VideoTrack)
に映像を渡します。キャプチャー API との違いは、ソースとトラックの概念が WebRTC の仕様であることです。そのためキャプチャーまでの処理はプラットフォームによって異なりますが、ソースとトラックの API は各プラットフォーム間で似ています。
ただし、アクセス可能な VideoSource
は操作中のカメラの映像を扱うもののみです。受信したリモートの映像の VideoSource
は隠蔽されており、ユーザーはアクセスできません。リモートの映像の加工については後述します。
トラック
トラックは映像や音声などのメディアデータの送信を制御します。ここでの送信とは、リモートピアではなく映像を扱うオブジェクトが対象です。ですので、リモートピアとの接続方向 (送信・受信) は関係ありません。接続方向に関わらず、メディアデータはトラックで制御されます。
映像の担当が VideoTrack
、音声の担当が AudioTrack
です (上記の図には含まれていません) 。映像を扱う際は、主に VideoTrack
を使うことになります。
現在の実装では、メディアデータ送信の停止・再開と音量の調節ができます。ただし、映像の送信を停止しても、カメラは停止しない&キャプチャーからの映像の供給は止まらないので注意です。映像の配信が止まっているのにカメラが起動したままだと、アプリによってはユーザーにあらぬ誤解を与えてしまうかもしれません。
シンク
上の図で VideoTrack
の右にある VideoSink
がシンク (Sink) です。シンクとは映像の出力先で、映像の供給源のソースと対になる概念です。 VideoSink
はインターフェースで、トラックから映像を受け取るメソッドが定義されています。
以下に定義を示します。トラックから映像を受け取るメソッドが定義されています。
interface VideoSink {
@CalledByNative
fun onFrame(videoFrame: VideoFrame?)
}
たった 1 つしかメソッドがないので簡単ですね。 onFrame()
はネイティブから呼ばれます。
VideoSink
は映像の流れの終着点の一つです。VideoSink
を実装したオブジェクトを VideoTrack
に追加すると、 VideoTrack
から映像フレームが送られてきます。あとは VideoSink
を実装した UI コンポーネントを用意すれば映像を描画できます。 UI コンポーネントについては後述します。
映像フレームを受け取るメソッドが定義されたインターフェースはこれまでにもありましたが、 VideoSink
が異なるのはトラックの存在です。つまり、送信する映像も受信した映像も等しく VideoSink
に流れてきます。送信する映像の描画と受信した映像の描画で異なる UI コンポーネントを使う必要はありません (当然と言えば当然ですが) 。
ちなみに iOS ではシンクという名前は出てきません。同様の役割を果たす API に VideoRenderer
というプロトコルがあります。
加工
M74 より、 VideoProcessor
という映像を加工するインターフェースが追加されました。それまではカメラの映像を加工するにはかなりの工夫をしなければならなかったのですが、今は簡単になっています。
VideoProcessor
の定義を以下に示します。
interface VideoProcessor: CapturerObserver {
fun setSink(sink: VideoSink?)
}
VideoProcessor
は CapturerObserver
を継承したインターフェースで、 VideoSource
にセットして使います。ちょっと頭が混乱しそうなのが、追加されたメソッドでシンクを指定可能なことです。加工した映像の出力先をユーザーが指定しなければならないように思えますが、このメソッドは VideoSource
から呼ばれます。このとき VideoSource
は出力先がトラックとなるシンクを渡してくるので、 VideoProcessor
の実装クラスではこのシンクを保持しておいて、加工した映像フレームをシンクに渡せば完了です。なんか面倒臭い回り道をしてる感じのインターフェースですが、シンクを無視すればトラックに出力させないという処理も可能になります。
ただし、 VideoProcessor
は VideoSource
を対象とするので、 VideoSource
が手に入らないリモートの映像は VideoProcessor
で加工できません。じゃあどうすればいいのかと言えば単純で、トラックに追加するシンク側で加工して描画すれば問題ないでしょう。リモートの映像は受信するのみですから、加工した映像をトラックに返す必要はありません。
UI コンポーネント
映像を描画するための UI コンポーネントは SurfaceViewRenderer
です。全然それらしくない名前なので見落としてしまいそうですが、 SurfaceView
を継承した SurfaceViewRenderer
です。
SurfaceViewRenderer
は VideoSink
を実装しており、トラックに追加すれば映像を描画できます。また、 SurfaceView
を継承したビューコンポーネントなので、他のビューコンポーネントと同様に XML ファイルでレイアウト可能です。
SurfaceViewRenderer
は簡単なライフサイクルがあり、使用前に init()
を、使用後に release()
を呼ぶ必要があります。通常はアクティビティのライフサイクルと合わせておけばいいでしょう。
映像の描画には EGL (OpenGL ES) コンテキストが使われます (init()
に渡します) 。 libwebrtc で利用する EGL コンテキストは EglBase
で生成できます。 EGL コンテキストは複数の SurfaceViewRenderer
で共有しても問題ないので、通常はあまり気を使わなくてもいいと思います。
映像の送受信
VideoTrack
の映像は PeerConnection
を通してリモートピアと送受信します。一見すると簡単なんですが、この周辺のオブジェクトは生成の関係が混み合っていてややこしいので注意です。
生成の中心となるのは PeerConnectionFactory
です。 PeerConnectionFactory
は名前の通り PeerConnection
を生成するクラスですが、実はローカル (カメラ) なメディアデータ送信用のVideoSource
と VideoTrack
も生成します。生成した VideoTrack
を PeerConnection
に追加すると、トラックの映像をリモートピアに送信できます。
リモートピアから映像を受信した場合、トラックが自動的に生成されて PeerConnection
に追加されます。受信にあたって特に何かしらの操作を行う必要はありません。
PeerConnectionFactory
はユーザーが生成します。様々な生成オプションがありますが、 PeerConnectionFactory.Builder
を使うと簡単です。映像・音声のカスタマイズが必要なければ、特にオプションを指定しなくても問題ありません。
映像コーデック
これまでの説明では、映像のコーデックについて触れませんでした。描画するにしても送信するにしても、指定されたコーデックに従って映像をエンコードまたはデコードしないといけません。
各コーデックに対応するエンコーダーは VideoEncoder
、デコードは VideoDecoder
です。 VP8, VP9, H.264 など、対応するコーデックそれぞれに具象クラスが用意されています。実行時、これらのオブジェクトはそれぞれ VideoEncoderFactory
と VideoDecoderFactory
によって生成されます。
使用するVideoEncoderFactory
と VideoDecoderFactory
は PeerConnectionFactory
の生成オプションに指定します。デフォルト以外のエンコーダー・デコーダーを使いたい場合は、任意の実装を PeerConnectionFactory.Builder
に追加します。
エンコード・デコード処理の実装は 2 種類あります。一つはソフトウェアによる実装 (C++ で実装されています) 、もう一つはハードウェアエンコーダー・デコーダーによるハードウェアアクセラレーションを利用した実装です。ハードウェアアクセラレーションは MediaCodec API を利用しています。
C++ で実装されていても、映像のエンコード・デコードはかなりの CPU ・メモリのリソースを食い潰します。バッテリーの消耗が大きくなるので、スマホでの使用は現実的ではないでしょう。通常は端末のハードウェアアクセラレーションの恩恵を受けられるコーデックを選ぶのが無難です。
おわりに
以上です。