Sora Unity SDK を iOS 対応した話
Tags: webrtc, ios, unity3d
Published: 2020-08-17 03:54:20

---
title: Sora Unity SDK を iOS 対応した話
published: true
description: 
tags: webrtc,ios,unity
//cover_image: https://direct_url_to_image.jpg
---

[Sora Unity SDK](https://github.com/shiguredo/sora-unity-sdk) は [WebRTC SFU Sora](https://sora.shiguredo.jp/) の Unity クライアントです。
Sora Unity SDK は既に Windows と macOS と [Android](https://dev.to/wandbox/sora-unity-sdk-android-1638) で動くようになっているのですが、今回新しく iOS に対応しました。

{% twitter 1293518685143879681 %}

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

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

- iOS 用 WebRTC を作る PR
  - [iOS ビルドに対応 by melpon · Pull Request #6 · shiguredo-webrtc-build/webrtc-build](https://github.com/shiguredo-webrtc-build/webrtc-build/pull/6)
  - [iOS 用パッチを適用する by melpon · Pull Request #7 · shiguredo-webrtc-build/webrtc-build](https://github.com/shiguredo-webrtc-build/webrtc-build/pull/7)
- それを使って Sora Unity SDK 本体を iOS に対応する PR
  - [iOS に対応 by melpon · Pull Request #9 · shiguredo/sora-unity-sdk](https://github.com/shiguredo/sora-unity-sdk/pull/9)
- Sora Unity SDK のサンプル更新
  - [Sora Unity SDK 2020.7 を利用する by torikizi · Pull Request #6 · shiguredo/sora-unity-sdk-samples](https://github.com/shiguredo/sora-unity-sdk-samples/pull/6)

## WebRTC の iOS ビルドを作る

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

iOS 用の WebRTC ライブラリは、時雨堂の [shiguredo/shiguredo-webrtc-build](https://github.com/shiguredo/shiguredo-webrtc-build) を置き換える目的もあるので WebRTC.framework を生成しています。

WebRTC.framework は WebRTC のソースに入っている `tools_webrtc/ios/build_ios_libs.sh` を叩くことで生成できます。こんな感じに利用します。

```bash
  ./tools_webrtc/ios/build_ios_libs.sh -o $BUILD_DIR/webrtc --build_config release --arch $TARGET_ARCHS --bitcode --extra-gn-args " \
    rtc_libvpx_build_vp9=true \
    rtc_include_tests=false \
    rtc_build_examples=false \
    rtc_use_h264=false \
    use_rtti=true \
    libcxx_abi_unstable=false \
  "
```

これで WebRTC.framework が生成できます。
ただし、Unity ではこのライブラリは利用しません。代わりにいつものように自前で GN と ninja を叩いて libwebrtc.a を生成して利用します。

これは他のライブラリと同じような感じでビルドするだけですが、iOS はユニバーサルライブラリが利用可能なので、x86_64 と ARM64 用のライブラリを 1 個の libwebrtc.a にまとめるところが違います。

こんな感じのコードになります。

```bash
    # x86_64 用
    gn gen $BUILD_DIR/webrtc/x64_libs --args="
      ... # いろいろ引数を入れる
    "
    ninja -C $BUILD_DIR/webrtc/x64_libs
    # ar でまとめて x86_64 用の libwebrtc.a を生成
    pushd $BUILD_DIR/webrtc/x64_libs/obj
      ar -rc $BUILD_DIR/webrtc/x64_libs/libwebrtc.a `find . -name '*.o'`
    popd

    # ARM64 用
    gn gen $BUILD_DIR/webrtc/arm64_libs --args="
      ... # いろいろ引数を入れる
    "
    ninja -C $BUILD_DIR/webrtc/arm64_libs
    # ar でまとめて ARM64 用の libwebrtc.a を生成
    pushd $BUILD_DIR/webrtc/arm64_libs/obj
      ar -rc $BUILD_DIR/webrtc/arm64_libs/libwebrtc.a `find . -name '*.o'`
    popd

    # x86_64 と ARM64 の libwebrtc.a をまとめてユニバーサルライブラリにする
    lipo \
      $BUILD_DIR/webrtc/x64_libs/libwebrtc.a \
      $BUILD_DIR/webrtc/arm64_libs/libwebrtc.a \
      -create \
      -output $BUILD_DIR/webrtc/libwebrtc.a
```

また、iOS 用の WebRTC は、デフォルトの実装では受信専用の場合でも録音の権限を要求してしまうため、不要な権限の要求ということで Apple の審査に落ちることがあるようです。
shiguredo/shiguredo-webrtc-build ではその問題を解決するためのパッチも用意されているので、そのパッチも当てています。

詳細は [shiguredo-webrtc-build/patch_shiguredo_ios.md at develop · shiguredo/shiguredo-webrtc-build](https://github.com/shiguredo/shiguredo-webrtc-build/blob/develop/docs/patch_shiguredo_ios.md) を参照して下さい。

このパッチを当てているため、Sora Unity SDK の iOS 版は受信専用の場合にマイクの権限を要求しません。

## Sora Unity SDK を iOS ビルドする

これは普通に CMake に設定を追加するだけです。

CMake は 3.14 以上なら iOS ビルドに[対応している](https://cmake.org/cmake/help/latest/manual/cmake-toolchains.7.html#cross-compiling-for-ios-tvos-or-watchos)ため、ちょっと設定をいくつか入れてやるだけです。

## Sora Unity SDK のコードを iOS に対応する

iOS 専用の処理をいくつか入れてやる必要があります。

ただし、大体のコードは macOS と共通なので、そこまで変更する量は多くありません。

### プラグインの登録を行う

Windows, macOS, Android では、ライブラリのロード時に `UnityPluginLoad` が、アンロード時に `UnityPluginUnload` が呼ばれるという仕組みがあり、この時に `IUnityInterfaces` を渡してもらうことで、Unity の機能をネイティブで触れるようになっています。

しかし iOS は静的ライブラリなので、この関数をエクスポートしていても呼ばれることはありません。
`IUnityInterfaces` を渡してもらうには、`UnityRegisterRenderingPluginV5` 関数を使ってロード時、アンロード時の関数を明示的に登録してやる必要があります。

調べた感じだと、この関数は本来、Unity の iOS 用ビルドをして生成された `UnityAppController:shouldAttachRenderDelegate` を実装して、その中で呼ぶのが良さそうなのですが、ここを弄るのは大変そうだったので、`Sora` オブジェクトを生成する時に一度だけ `UnityRegisterRenderingPluginV5` 関数を呼ぶようにしました。

```cpp
#if defined(SORA_UNITY_SDK_IOS)
// PlaybackEngines/iOSSupport/Trampoline/Classes/Unity/UnityInterface.h から必要な定義だけ拾ってきた
typedef void (*UnityPluginLoadFunc)(IUnityInterfaces* unityInterfaces);
typedef void (*UnityPluginUnloadFunc)();
void UnityRegisterRenderingPluginV5(UnityPluginLoadFunc loadPlugin,
                                    UnityPluginUnloadFunc unloadPlugin);

bool g_ios_plugin_registered = false;

void UNITY_INTERFACE_API SoraUnitySdk_UnityPluginLoad(IUnityInterfaces* ifs);
void UNITY_INTERFACE_API SoraUnitySdk_UnityPluginUnload();
#endif

void* sora_create() {
#if defined(SORA_UNITY_SDK_IOS)
  if (!g_ios_plugin_registered) {
    UnityRegisterRenderingPluginV5(&SoraUnitySdk_UnityPluginLoad,
                                   &SoraUnitySdk_UnityPluginUnload);
    g_ios_plugin_registered = true;
  }
#endif
  ...
}
```

これで問題なく動いているので多分大丈夫だと思います。

ロード時、アンロード時の関数名を `UnityPluginLoad` ではなく `SoraUnitySdk_UnityPluginLoad` などとしているのは、iOS が静的ライブラリであるため、他のライブラリをリンクした際に `UnityPluginLoad` という関数名が衝突する可能性があるからです。

### C#, C++ 間のインターフェースから bool を除ける

Sora Unity SDK を iOS の実機で動かしてみると、特定の C++ の関数を呼び出した時に、変な値が C++ に渡って落ちるという現象が起きました。
調査してみると面白いことが分かったので書いておきます。

C# から C++ のコードを呼び出すために P/Invoke を利用するわけですが、Unity iOS の場合はここはいい感じに IL2CPP で C++ に変換して呼んでくれます。

C# 上で、`sora_connect` 関数は 以下の extern 宣言になっています（引数が多いのは気にしなくて良いです）。

```cs
#if UNITY_IOS && !UNITY_EDITOR
    [DllImport("__Internal")]
#else
    [DllImport("SoraUnitySdk")]
#endif
    private static extern int sora_connect(
        IntPtr p,
        string unity_version,
        string signaling_url,
        string channel_id,
        string metadata,
        string role,
        bool multistream,
        int capturer_type,
        IntPtr unity_camera_texture,
        string video_capturer_device,
        int video_width,
        int video_height,
        string video_codec,
        int video_bitrate,
        bool unity_audio_input,
        bool unity_audio_output,
        string audio_recording_device,
        string audio_playout_device,
        string audio_codec,
        int audio_bitrate);
```

この関数を呼ぶコードを書いて、Unity から iOS 用にビルドすると以下のような C++ コードが生成されます。

```cpp
int32_t returnValue = reinterpret_cast<PInvokeFunc>(sora_connect)(
  ___p0,
  ____unity_version1_marshaled,
  ____signaling_url2_marshaled,
  ____channel_id3_marshaled,
  ____metadata4_marshaled,
  ____role5_marshaled,
  static_cast<int32_t>(___multistream6),
  ___capturer_type7,
  ___unity_camera_texture8, 
  ____video_capturer_device9_marshaled,
  ___video_width10,
  ___video_height11,
  ____video_codec12_marshaled,
  ___video_bitrate13,
  static_cast<int32_t>(___unity_audio_input14),
  static_cast<int32_t>(___unity_audio_output15),
  ____audio_recording_device16_marshaled,
  ____audio_playout_device17_marshaled,
  ____audio_codec18_marshaled,
  ___audio_bitrate19);
```

`PInvokeFunc` は以下の typedef になっています。

```cpp
typedef int32_t (DEFAULT_CALL *PInvokeFunc) (
  intptr_t, char*, char*, char*, char*, char*, int32_t,
  int32_t, intptr_t, char*, int32_t, int32_t, char*, int32_t,
  int32_t, int32_t, char*, char*, char*, int32_t);
```

ここでよく見ると、`sora_connect` 関数にあった bool 型の引数が int32_t 型になっていることが分かります。

C#(というか生成した C++ コード)側では、int32_t 型であるため4バイトのデータとしてスタックに積んで関数を呼び出しているのに、C++ 側では1バイトのデータとしてスタックから取り出している処理になってしまうわけです。
そのため C++ 側の引数が変な値になってしまって落ちていました。

実のところ、アライメントのおかげで bool を使ったら即落ちるという訳でもないのですが、今回に関しては bool が 2 個連続で並んでる引数 `bool unity_audio_input` と `bool unity_audio_output` があったので、ここで引数がずれてしまって落ちました。

とにかく Unity の IL2CPP が bool を int32_t として扱うというのは変えようが無さそうなので、諦めて C++ のインターフェースとして bool を使うのはやめるようにしました。

`typedef int32_t unity_bool_t;` と定義して、bool の部分を unity_bool_t に置き換えていったら、無事 `sora_connect` を呼んでも落ちなくなりました。

### ライブラリのリンク順序を入れ替える

上記の対応で Sora Unity SDK の iOS 版は動くだろうと思ってたんですが、まだ問題がありました。
H.264 ならハードウェアエンコーダを使って無事動くのに、VP8, VP9 だと動かないという現象が起きました。

詳細にエラーを追っていくと、VP8, VP9 ソフトウェアエンコーダである libvpx のバージョンが古い、というエラーが出ていました。

libvpx は libwebrtc.a に同封されていて、古いわけがありません。
いろいろ調べた結果、Unity の iOS ビルドで必ず必要になる libiPhone-lib.a の中に、めちゃめちゃ古い libvpx が含まれていて、それが利用されていることが分かりました。

{% twitter 1290223856221736960 %}

ググってみると、３年前に同じようなことで困ってる人が見つかりましたが、これは未回答のようでした。

- [Unity statically links libvpx, any way to prevent this in a build? - Unity Answers](https://answers.unity.com/questions/1296768/unity-statically-links-libvpx-any-way-to-prevent-t.html)

なので頑張って調べた所、Xcode 用のプロジェクトを生成した後、

![libiPhone-lib.a 入れ替え前](https://dev-to-uploads.s3.amazonaws.com/i/c1z6oez36hgrr87sbgtw.png)

この libiPhone-lib.a を、

![libiPhone-lib.a 入れ替え後](https://dev-to-uploads.s3.amazonaws.com/i/9bqm91qk5dlfgy6d9gn2.png)

このように libwebrtc.a より後ろにしてやると、無事 libwebrtc.a の libvpx が使われるようになることが分かりました。

ほんとにこんなのでいいのか？って思ったんですが、

{% twitter 1290243855032266753 %}

ということらしいし、リンク先のをよく見てリンクのルールを考えると確かに問題無さそうなので安心です。

あとはこれをプログラム上で実現するだけです。Unity なら以下のようなコードを書くだけで実現できました。

```cs
public class SoraUnitySdkPostProcessor
{
    [PostProcessBuildAttribute(500)]
    public static void OnPostprocessBuild(BuildTarget buildTarget, string pathToBuiltProject)
    {
        var projPath = pathToBuiltProject + "/Unity-iPhone.xcodeproj/project.pbxproj";
        PBXProject proj = new PBXProject();
        proj.ReadFromFile(projPath);
        // 2020.3 より前か後かで処理が変わる
#if UNITY_2019_3_OR_NEWER
        string guid = proj.GetUnityFrameworkTargetGuid();
#else
        string guid = proj.TargetGuidByName("Unity-iPhone");
#endif

        // ライブラリを追加したりとかのいろいろな設定

        // libiPhone-lib.a 削除して追加するとリンク順序が一番後ろに行く
        string fileGuid = proj.FindFileGuidByProjectPath("Libraries/libiPhone-lib.a");
        proj.RemoveFileFromBuild(guid, fileGuid);
        proj.AddFileToBuild(guid, fileGuid);

        proj.WriteToFile(projPath);
    }
}
```

これで無事、VP8, VP9 での送受信に成功しました。

### いろいろやる

その他、ADM をワーカースレッドで作るようにしたり、カメラの回転に対応したり、マイク権限を要求しないパッチでうまく再生できないのを回避するためのコードを追加したり、Metal の一部機能が iOS で使えないので除けたりといったことをしました。

ただ詳細に説明するほど面白いことは無かったので省略します。

## 感想

当初の想定では、既に macOS で Metal にも対応してたし、ビデオエンコーダ/デコーダも Objective-C のコードをちょっと呼ぶだけというのは分かってたので、ほぼ何もしなくても動くんじゃないかと思っていました。

が、上記の通り、実際にやってみると結構いろいろと罠を踏んで大変でした。でも無事動いて良かったです。