libwebrtc (Android) のカメラ API

SUZUKI Tetsuya
shiguredo
Published in
16 min readJun 29, 2020

--

はじめに

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) オブジェクトを生成できます。生成した映像キャプチャーを使ってカメラの映像を取得します。初めて起動したアプリでは、ここでカメラのパーミッションがユーザーに要求されます。

このインターフェースを実装したクラスは Camera1EnumeratorCamera2Enumerator です。数字が異なるだけの両者の違いは「対応する 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 はないので省略します。また、CameraCapturerpublic な API ではないのでサブクラスは作れませんが、具象クラスである Camera1CapturerCamera2Capturer が使用可能です。

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?)
}

VideoCapturerCameraVideoCapturer は実質的に CameraCapturer と考えてもらっても大丈夫です (以降そうします) 。 CameraCapturer は抽象クラスなので、ユーザーが使うのは具象クラスである Camera1CapturerCamera2Capturer です。前述の通り、両クラスの違いは対応する Android バージョンです。

CameraCapturer の主な役割を次に示します。

  • キャプチャーの初期化
  • キャプチャーの起動・停止
  • 取得した映像フレームを CapturerObserver に渡す
  • 使用するカメラの変更

ポイントとなるのは CapturerObserver です。 CapturerObserver はキャプチャーのイベントハンドラ (リスナー) で、キャプチャーの起動・停止と映像フレーム取得時のイベントを受け取ります。

interface CapturerObserver {
fun onCapturerStarted(success: Boolean)
fun onCapturerStopped()
fun onFrameCaptured(frame: VideoFrame)
}

CapturerObserverSurfaceTextureHelper と共にキャプチャーの初期化に使われます。 後述する VideoSourceCapturerObserver を内部で生成して使うので、通常ユーザーが実装する必要はありません。

SurfaceTextureHelperCameraVideoCapturer が取得したカメラの映像を libwebrtc が処理できるフォーマット (VideoFrame) に変換します。普通にコンストラクタで生成できます。

ここまでの流れをまとめると、カメラが取得した映像は SurfaceTextureHelper で映像フレームに変換され、映像フレームは CapturerObserver を通して VideoSource に渡ります。配信と描画まであと一歩です。

セッション管理

カメラの起動から停止までの期間をセッションとし、 CameraSession で表されます。 CameraSession の具象クラスの実装では、直接カメラにアクセスして起動と停止を行います。 CameraSessionCameraCapturer によって生成されます。 VideoCapturerCameraVideoCapturer の定義にはセッションが登場しないので、 CameraCapturerCameraSession は実装方法の一つと言えます。

CameraSessionCameraCapturer の内部でのみ使われるので、ユーザーはセッションにアクセスできません。キャプチャーを停止するには 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?)
}

VideoProcessorCapturerObserver を継承したインターフェースで、 VideoSource にセットして使います。ちょっと頭が混乱しそうなのが、追加されたメソッドでシンクを指定可能なことです。加工した映像の出力先をユーザーが指定しなければならないように思えますが、このメソッドは VideoSource から呼ばれます。このとき VideoSource は出力先がトラックとなるシンクを渡してくるので、 VideoProcessor の実装クラスではこのシンクを保持しておいて、加工した映像フレームをシンクに渡せば完了です。なんか面倒臭い回り道をしてる感じのインターフェースですが、シンクを無視すればトラックに出力させないという処理も可能になります。

ただし、 VideoProcessorVideoSource を対象とするので、 VideoSource が手に入らないリモートの映像は VideoProcessor で加工できません。じゃあどうすればいいのかと言えば単純で、トラックに追加するシンク側で加工して描画すれば問題ないでしょう。リモートの映像は受信するのみですから、加工した映像をトラックに返す必要はありません。

UI コンポーネント

映像を描画するための UI コンポーネントは SurfaceViewRenderer です。全然それらしくない名前なので見落としてしまいそうですが、 SurfaceView を継承した SurfaceViewRenderer です。

SurfaceViewRendererVideoSink を実装しており、トラックに追加すれば映像を描画できます。また、 SurfaceView を継承したビューコンポーネントなので、他のビューコンポーネントと同様に XML ファイルでレイアウト可能です。

SurfaceViewRenderer は簡単なライフサイクルがあり、使用前に init() を、使用後に release() を呼ぶ必要があります。通常はアクティビティのライフサイクルと合わせておけばいいでしょう。

映像の描画には EGL (OpenGL ES) コンテキストが使われます (init() に渡します) 。 libwebrtc で利用する EGL コンテキストは EglBase で生成できます。 EGL コンテキストは複数の SurfaceViewRenderer で共有しても問題ないので、通常はあまり気を使わなくてもいいと思います。

映像の送受信

VideoTrack の映像は PeerConnection を通してリモートピアと送受信します。一見すると簡単なんですが、この周辺のオブジェクトは生成の関係が混み合っていてややこしいので注意です。

生成の中心となるのは PeerConnectionFactory です。 PeerConnectionFactory は名前の通り PeerConnection を生成するクラスですが、実はローカル (カメラ) なメディアデータ送信用のVideoSourceVideoTrack も生成します。生成した VideoTrackPeerConnection に追加すると、トラックの映像をリモートピアに送信できます。

リモートピアから映像を受信した場合、トラックが自動的に生成されて PeerConnection に追加されます。受信にあたって特に何かしらの操作を行う必要はありません。

PeerConnectionFactory はユーザーが生成します。様々な生成オプションがありますが、 PeerConnectionFactory.Builder を使うと簡単です。映像・音声のカスタマイズが必要なければ、特にオプションを指定しなくても問題ありません。

映像コーデック

これまでの説明では、映像のコーデックについて触れませんでした。描画するにしても送信するにしても、指定されたコーデックに従って映像をエンコードまたはデコードしないといけません。

各コーデックに対応するエンコーダーは VideoEncoder 、デコードは VideoDecoder です。 VP8, VP9, H.264 など、対応するコーデックそれぞれに具象クラスが用意されています。実行時、これらのオブジェクトはそれぞれ VideoEncoderFactoryVideoDecoderFactory によって生成されます。

使用するVideoEncoderFactoryVideoDecoderFactoryPeerConnectionFactory の生成オプションに指定します。デフォルト以外のエンコーダー・デコーダーを使いたい場合は、任意の実装を PeerConnectionFactory.Builder に追加します。

エンコード・デコード処理の実装は 2 種類あります。一つはソフトウェアによる実装 (C++ で実装されています) 、もう一つはハードウェアエンコーダー・デコーダーによるハードウェアアクセラレーションを利用した実装です。ハードウェアアクセラレーションは MediaCodec API を利用しています。

C++ で実装されていても、映像のエンコード・デコードはかなりの CPU ・メモリのリソースを食い潰します。バッテリーの消耗が大きくなるので、スマホでの使用は現実的ではないでしょう。通常は端末のハードウェアアクセラレーションの恩恵を受けられるコーデックを選ぶのが無難です。

おわりに

以上です。

--

--