loading...
合同会社Wandbox

Sora Unity SDK を Android 対応した話

melpon profile image melpon Updated on ・2 min read

Sora Unity SDKWebRTC SFU Sora の Unity クライアントです。
Sora Unity SDK は既に Windows と macOS で動くようになっているのですが、今回新しく Android に対応しました。

この記事では、この対応のために具体的にどんなことをしたのかについて書きます。

Android 対応の PR はこれです。全体を見たい時に確認してください。

WebRTC の Android ビルド

Sora Unity SDK を Android で動かせるようにするには、まず Android 用の WebRTC ライブラリが必要です。

WebRTC は既に Android で動作するようになっているので、これをビルドするだけで良いです。
ただし、Android のライブラリは AAR になっているのに対して、ネイティブの実行に必要なのは libwebrtc.a と jar ファイルなので、そのあたりをうまくやる必要があります。

WebRTC のビルドは Sora Unity SDK ではなく、shiguredo-webrtc-build/webrtc-build というリポジトリで管理していて、ここでネイティブの Android 向けの WebRTC に対応しました。

このビルド済みバイナリを Sora Unity SDK でダウンロードして利用する形になります。

Android のカメラに対応する

WebRTC ライブラリには既に Android のカメラを利用してキャプチャする機能が既にあるので、これを Sora Unity SDK から利用するだけです。

ただし、このカメラキャプチャの機能は Java で書かれています。Sora Unity SDK は C++ なので直接扱うのは出来ません。そこでどうするかというと、まあ JNI (Java Native Interface) ですね。

JNI を使ってカメラの映像をキャプチャする

Android 用 WebRTC を使って Java でカメラを利用するには、以下のようなコードを書きます。

// Android のいつものコンテキスト
Context context = UnityPlayer.currentActivity.getApplicationContext();
// キャプチャ処理をやったりするスレッド
SurfaceTextureHelper helper = SurfaceTextureHelper.create("VideoCapturerThread", null);
// デバイスからキャプチャラを作る
Camera2Enumerator enumerator = new Camera2Enumerator(context);
CameraVideoCapturer videoCapturer = enumerator.createCapturer(enumerator.getDeviceNames()[0], null /* eventsHandler */);
// キャプチャ開始
videoCapturer.initialize(helper, context, capturerObserver);
videoCapturer.startCapture(width, height, fps);

これで毎フレーム capturerObserver オブジェクトにコールバックがやってきます。

これを C++ で再現すればうまく動くので、頑張って書くだけです。
例えば UnityPlayer.currentActivity.getApplicationContext() に相当する処理は以下のように書きます。

webrtc::ScopedJavaLocalRef<jobject> GetAndroidApplicationContext(JNIEnv* env) {
  // Context context = UnityPlayer.currentActivity.getApplicationContext()
  // を頑張って C++ から呼んでるだけ
  webrtc::ScopedJavaLocalRef<jclass> upcls = webrtc::GetClass(env, "com/unity3d/player/UnityPlayer");
  jfieldID actid = env->GetStaticFieldID(upcls.obj(), "currentActivity", "Landroid/app/Activity;");
  webrtc::ScopedJavaLocalRef<jobject> activity(env, env->GetStaticObjectField(upcls.obj(), actid));

  webrtc::ScopedJavaLocalRef<jclass> actcls(env, env->GetObjectClass(activity.obj()));
  jmethodID ctxid = env->GetMethodID(actcls.obj(), "getApplicationContext", "()Landroid/content/Context;");
  webrtc::ScopedJavaLocalRef<jobject> context(env, env->CallObjectMethod(activity.obj(), ctxid));

  return context;
}

こんな感じのコードを大量に書いて何とかします。

問題は Java から C++ へのコールバックを受け取る部分ですが、これは大変ありがたいことに、C++ のキャプチャコールバックを受け取るクラスである webrtc::jni::AndroidVideoTrackSource を Java の CapturerObserver に変換するための仕組みを WebRTC 側が用意してくれているので、それを利用するだけで C++ 側にコールバックが渡ってくれるようになります。

JAR ファイルを設定する

Java のコードを利用するということは、class ファイルを Unity のビルド時に含んでやる必要があるということです。
幸いなことに Unity には JAR ファイルを Android に組み込む機能が入っている(JAR プラグイン - Unity マニュアル)ため、上記の「WebRTC の Android ビルド」で作った Android 用 WebRTC に入っている jar ファイルをそのまま持ってくるだけで良いです。

Java から C++ にコールバックを渡るようにする

この状態で普通に libSoraUnitySdk.so を作って実機で動かしてみたのですが、すぐにクラッシュしました。
ログを追いかけてみると、Java 側から Java_org_webrtc_Histogram_nativeAddSample みたいな C++ の関数を呼ぼうとして、その関数が見つからなくてクラッシュしているようでした。

この関数は libwebrtc.a には存在しているのですが、それをリンクした libSoraUnitySdk.so には含まれていませんでした。
それも当然で、C++ のコードからは Java_org_webrtc_Histogram_nativeAddSample 関数を一切呼んでいないため、リンク時にこの関数が取り除かれてしまうのです。
なのでこの Java から呼び出している関数に関しては libSoraUnitySdk.so に含めてやるようにしました。

具体的には、リンカーに渡すオプションとして -Wl,--undefined=Java_org_webrtc_Histogram_nativeAddSample を追加しました。
--undefined を使うことで、未使用の関数であってもライブラリに含められます。

ただし Java から呼び出す C++ 関数はこれだけではないため、これらの関数の一覧を列挙してオプションを作ってやる必要があります。
こういうシンボルを見るためのツールが Android NDK にあるので、それを利用して libwebrtc.a の関数を列挙し、Java_org_webrtc_ から始まる関数を取り出してオプションに加工しています。

具体的には以下のようなコードになります。

# Android 側からのコールバックする関数は消してはいけないので、
# libwebrtc.a の中から消してはいけない関数の一覧を作っておく
if [ ! -e $INSTALL_DIR/android/webrtc.ldflags ]; then
  # readelf を使って libwebrtc.a の関数一覧を列挙して、その中から Java_org_webrtc_ を含む関数を取り出し、
  # -Wl,--undefined=<関数名> に加工する。
  # (-Wl,--undefined はアプリケーションから参照されていなくても関数を削除しないためのフラグ)
  _READELF=$INSTALL_DIR/android-ndk/toolchains/llvm/prebuilt/darwin-x86_64/bin/aarch64-linux-android-readelf
  _LIBWEBRTC_A=$INSTALL_DIR/android/webrtc/lib/arm64-v8a/libwebrtc.a
  $_READELF -Ws $_LIBWEBRTC_A \
    | grep Java_org_webrtc_ \
    | while read a b c d e f g h; do echo -Wl,--undefined=$h; done \
    | sort \
    > $INSTALL_DIR/android/webrtc.ldflags
fi

これで動かすと、無事カメラ映像のキャプチャが出来るようになりました。

Unity カメラのキャプチャに対応する

Sora Unity SDK には、Unity カメラが写している映像をキャプチャする機能が用意されています。
つまりこういうことです。

Sora Unity SDK サンプル集を動かしてみたfor Android - torikiziのブログ より

仕組みとしては、Unity カメラがレンダリングしているテクスチャを指すポインタを C++ 側に渡して、それをゴニョゴニョっとして映像データを取り出すようになっています。
このゴニョゴニョする部分が大変なところで、Windows なら Direct3D 11 を使ったり、macOS なら Metal を使ったり、今回の Android なら Vulkan を使ったりして映像データを取り出します(Android は OpenGLES の場合もあるんですが、そちらは今回対応していません)。

Unity は、ネイティブで Unity のテクスチャを扱えるようにするための仕組みを用意してくれていて、導入に関しては以下のドキュメントに書かれています。

これを利用するのと、あとは Vulkan のドキュメントを読みながら書くだけです。

Unity テクスチャのポインタ

実際書いてみてハマった点としては、「Unity から渡されるテクスチャへのポインタは、Vulkan のテクスチャを指すわけではない」というのがあります。

Unity から渡されるテクスチャへのポインタは、例えば Windows(D3D11) なら ID3D11Resource* のことだったり、macOS(Metal) なら id<MTLTexture> のことだったりしました。
なので Android の場合も Vulkan 用のイメージである VkImage だと思っていたのですが、全然違っていました。

Android の場合、Unity から渡されたテクスチャへのポインタは自前のデータ構造になっているようで、この構造の実際の定義は公開されていないようです。
そしてこの不透明なデータ構造の内容を展開するための関数として IUnityGraphicsVulkan::AccessTexture というのがあり、この関数に Unity から渡されたテクスチャへのポインタを渡すと UnityVulkanImage というのを返してくれるようになっています。

これで得られた UnityVulkanImage を使って、Vulkan の関数を色々と呼び出して何とかして映像データを取り出します。
具体的な処理は sora-unity-sdk/unity_camera_capturer_vulkan.cpp at 469a326213c3b8a7eb4a202c5b0030c9103bc113 · shiguredo/sora-unity-sdk にあるので、気になった人は見てください。

ハードウェアエンコーダに対応する

Android 用の WebRTC は大変素晴らしくて、ハードウェアエンコーダに対応しています。ただし Java コードで。
DefaultVideoEncoderFactory というクラスは、出来る限りハードウェアを利用し、利用できるハードウェアがなければソフトウェアにフォールバックするという実装になっています。

ということで DefaultVideoEncoderFactory オブジェクトを C++ から生成して、それを C++ 用の video_encoder_factory に設定するだけです。

カメラデバイスの列挙に対応する

Android にはフロントカメラとバックカメラの2つが付いている機種があったりするので、その辺を列挙するための機能があると便利です。
Android 用の WebRTC は大変素晴らしくて、このカメラデバイスの列挙に対応しています。ただし Java コードで。
Camera2Enumerator というクラス(以下略)

感想

ということで、振り返ってみると大体はビルド周りと JNI 周りをうまく整備してやる感じで、あとは Unity テクスチャと Vulkan について調べて書く程度しかやっていません。

ただ、Android も JNI も Vulkan もあまり触ったことが無いので結構大変でした。
ひたすら動かなくて、Android のログを眺めてトライアルアンドエラーを繰り返しまくってました。何とか無事動いて良かったです。

Discussion

pic
Editor guide