KlakNDIをAndroidに対応させる


![Klak.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/101808/b00c2a60-ac01-2991-fadd-bb25449d7dd1.gif)


# はじめに
NDI®はNetwork Device Interfaceの略でNewTek社が開発したプロトコルです。簡単に言うとLanケーブルやWifiを使ってリアルタイムに映像の伝送ができます。NewTek社はSDKを公開してくれているので、開発者は自由にNDIを利用したソフトウェアを開発できます。

そのNDI SDKがAndroidに対応しました。どのバージョンから対応したのかは未確認ですが現状の最新版であるv4.5.3にはAndroid向けのSDKが存在します。ツイッターとか見てる限り最近のことのようです。個人的にAndroidとWindows/MacのUnity製アプリ間でリアルタイムに映像の送受信をしたかったのでこれを試してみました。

NDI SDKはUE4向けプラグインを公式でサポートして公開していますが、Unityに関しては特にサポートはありません。ですが、keijiroさんがUnity向けのプラグイン[KlakNDI](https://github.com/keijiro/KlakNDI)を公開してくださっています。これを使いたいと思います。しかしKlakNDIに含まれているNDI SDKはv4.1でAndroid向けのSDKは含まれておらず非対応です。そこで自前でNDI SDK の Android向けのネイティブプラグインを組み込んで使えるようにしてみました。この記事はその記録です。

ソースコードはGitHubで公開しています。

https://github.com/tarakoKutibiru/Unity-NDI-Plugin-Android


# 直面している問題点
AndroidからUnity内のカメラ映像の送信はできましたが、受信はできませんでした。原因については調査中ですが、SDK自体が未対応の可能性が高そうです。

NDI SDK 4.5に添付された公式のドキュメントには、NDI SDKはiOSにおいて映像の送信には対応しているが受信は未対応であることが明言されています。それに対してAndroidには特に映像の受信が未対応であるという明確な記述はありません。しかし公式のフォーラムには

> ARM CPUのエンコードには対応しているが、デコードには対応していないため、Android/iOSは映像の送信のみで、受信は対応していない。

という情報がありました。


# NDI SDK本体のダウンロード

![new_tek.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/101808/6d922335-df3f-618f-e20e-8c7d2083e472.png)
[NDIの公式サイト](https://www.ndi.tv/sdk/)でユーザー登録するとリンクが記述されたメールが届くのでそこからイントールします。Android,Windows,MAC,LINUXなどがあるので、必要に応じてインストールしてください。v4.5とv.4.1間で映像の伝送ができるかは未確認なので、とりあえず更新しておいたほうが無難だと思います。

余談ですが本体のSDKにドキュメントやサンプルコードが付属してくるので、このへんを読むとすごく参考になります。特にC#の実装は参考になります。`NDILibDotNet2`っていうラッパークラス郡があるんですが、これは便利なのでUnityに取り込んでしまっても良いと思います。

Windowsの場合はSDKが

```C:\Program Files\NewTek\NDI 4 SDK (Android)\Lib\armeabi-v7a\libndi.so```

にインストールされるのでこれを

`KlakNDI\Packages\jp.keijiro.klak.ndi\Plugin\Android\libndi.so`

のように配置します。

使いたいAndroid端末のABIが`armeabi-v7a`じゃない場合は対応するSDKを使って下さい。

# Config.csの修正

Config.csを修正してAndroidのSDKを読み込むようにします。


``` C#:KlakNDI\Packages\jp.keijiro.klak.ndi\Runtime\Interop\Config.cs
namespace Klak.Ndi.Interop
{
    static class Config
    {
　　#if UNITY_EDITOR_WIN || UNITY_STANDALONE_WIN
        public const string DllName = "Processing.NDI.Lib.x64";
　　#elif UNITY_EDITOR_OSX || UNITY_STANDALONE_OSX
        public const string DllName = "libndi.4";
　　#elif UNITY_EDITOR_LINUX || UNITY_STANDALONE_LINUX
        public const string DllName = "ndi";
　　#elif UNITY_ANDROID
        public const string DllName = "ndi";
　　#else
        public const string DllName = "__Internal";
　　#endif
    }
}
```

# WifiManagerの作成

公式のドキュメントによるとAndroidでNDIを利用するにはNSDManagerのインスタンスを取得して保持する必要があるそうです。


>Because Android handles discovery differently than other NDI platforms, some additional work is needed.  The NDI library requires use of the “NsdManager” from Android and, unfortunately, there is no way for a third-party library to do this on its own.  As long as an NDI sender, finder, or receiver is instantiated, an instance of the NsdManager will need to exist to ensure that Android’s Network Service Discovery Manager is running and available to NDI. 
This is normally done by adding the following code to the beginning of your long running activities:
At some point before creating an NDI sender, finder, or receiver, instantiate the NsdManager: 
You will also need to ensure that your application has configured to have the correct privileges required for this functionality to operate. 


DeepLを使った翻訳文は以下です。


>Androidは他のNDIプラットフォームとは異なるディスカバリーを扱うため、いくつかの追加作業が必要になります。 NDI ライブラリは Android の「NsdManager」を使用する必要があり、残念ながら、サードパーティのライブラリが独自にこれを行う方法はありません。 NDI の送信者、検出者、または受信者がインスタンス化されている限り、Android の  Network Service Discovery Manager  が実行され、NDI で利用できるようにするために、NsdManager のインスタンスが存在する必要があります。
これは通常、以下のコードを長時間実行しているアクティビティの先頭に追加することで行います。
NDI のsender、finder、またはreceiverを作成する前のある時点で、NsdManager のインスタンスを作成します。
また、アプリケーションがこの機能を動作させるために必要な正しい権限を持つように構成されていることを確認する必要があります。


ということなので`NsdManager`を取得して保持する`WifiManager`クラスを適当な場所に作成します。

```C#:WifiManager.cs
using UnityEngine;
using System.Collections.Generic;

public class WifiManager
{
#if UNITY_ANDROID && !UNITY_EDITOR
    AndroidJavaObject nsdManager = null;
#endif

    private static WifiManager instance = new WifiManager();

    public static WifiManager GetInstance()
    {
        return instance;
    }

    private WifiManager() { }

    public void SetupNetwork()
    {
        // The NDI SDK for Android uses NsdManager to search for NDI video sources on the local network.
        // So we need to create and maintain an instance of NSDManager before performing Find, Send and Recv operations.
    #if UNITY_ANDROID && !UNITY_EDITOR
        using (AndroidJavaObject activity = new AndroidJavaClass("com.unity3d.player.UnityPlayer").GetStatic<AndroidJavaObject>("currentActivity"))
        {
            using (AndroidJavaObject context = activity.Call<AndroidJavaObject>("getApplicationContext"))
            {
                using (AndroidJavaObject nsdManager = context.Call<AndroidJavaObject>("getSystemService", "servicediscovery"))
                {
                    this.nsdManager = nsdManager;
                }
            }
        }
    #endif
    }
}

```

公式のドキュメントには適切な権限を取得しておく必要があるとのことでしたが、特にAndroidManifest.xmlにPermissionの追加をしなくても動作しました。おそらくUnityが勝手に解決してくれているのだと思います。

あとは適切なタイミングで`WifiManager.GetInstance().SetupNetwork();`を呼べばOKです。

例えばKlakNDIの`SourceSelector.cs`のStartで呼べばOKです。

```C#:SourceSelector.cs
using UnityEngine;
using UnityEngine.UI;
using System.Collections.Generic;
using System.Linq;
using Klak.Ndi;

public class SourceSelector : MonoBehaviour
{
    [SerializeField] Dropdown _dropdown = null;

    NdiReceiver _receiver;
    List<string> _sourceNames;
    bool _disableCallback;

    // HACK: Assuming that the dropdown has more than
    // three child objects only while it's opened.
    bool IsOpened => _dropdown.transform.childCount > 3;

    void Start()
    {
        WifiManager.GetInstance().SetupNetwork();
        _receiver = GetComponent<NdiReceiver>();
    }

    void Update()
    {
        // Do nothing if the menu is opened.
        if (IsOpened) return;

        // NDI source name retrieval
        _sourceNames = NdiFinder.sourceNames.ToList();

        // Currect selection
        var index = _sourceNames.IndexOf(_receiver.ndiName);

        // Append the current name to the list if it's not found.
        if (index < 0)
        {
            index = _sourceNames.Count;
            _sourceNames.Add(_receiver.ndiName);
        }

        // Disable the callback while updating the menu options.
        _disableCallback = true;

        // Menu option update
        _dropdown.ClearOptions();
        _dropdown.AddOptions(_sourceNames);
        _dropdown.value = index;
        _dropdown.RefreshShownValue();

        // Resume the callback.
        _disableCallback = false;
    }

    public void OnChangeValue(int value)
    {
        if (_disableCallback) return;
        _receiver.ndiName = _sourceNames[value];
    }
}

```

# PlayerSettingの変更

次にUnityの設定を変更していきます。

ビルド設定をAndroidに変更します。
するとエラーがでるのでPlayerSettingsを変更します。

![build_setting.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/101808/83d1fe98-14f8-deb4-518a-207f149c3d52.png)

KlakNDIではUnityのカメラが描画している情報を取得するのに`AsyncGPUReadback`を使っていますが、これはOpenGLでは動かないのでGraphics APIsをVulkanに変更します。
![build_setting.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/101808/7766c2bc-82bb-b816-0d9e-ad67079fbdb5.png)

未確認ですが`AsyncGPUReadback`をOpenGLでも使えるようにするプラグインが存在するようです。
https://github.com/Alabate/AsyncGPUReadbackPlugin

# 結果
以上でKlakNDIをAndroidで利用できるようになったはずです:tada:
![Klak.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/101808/0731e87f-3ab4-cc0c-9eb3-7b9b0301a0b2.gif)

このGifはAndroid->Macですが、Android->Win10も動作確認できました。

使っているスマホはMotoG8という実売25000円くらいのローエンド端末です。あまり安定性はなくカクついています。映像の品質は端末やWifiルータの性能にも依存すると思うので、実用するなら機種の選定は必要な気がします。AndroidからPC２台に送信する実験もしてみましたが、さらにカクつくようになりました。

自分がやりたかったことは、PC上のUnityカメラの映像をAndroidに伝送して表示することだったので、今回紹介したNDI SDKでは実現できませんでした...ローカルネットワークでやりとりできれば十分だったので、NDIが使えると良かったんですけどね...WebRTCを勉強する必要があるかもですね...

# 追記
KlakNDIが公式でAndroidにサポートするかもしれません。
Issueで議論されています。
https://github.com/keijiro/KlakNDI/issues/85

現時点でまだプルリクが投げられてもいないですが、この人のリポジトリを確認すると、このコミットでAndroidに対応させたようです。

https://github.com/lincode/KlakNDI/commit/d26021098be6bfb860c9a556d541b08a934f3e52

私のアプローチだとC#側からJavaのコードを適当に呼び出す実装でしたが、この人はActivityを継承したクラスを作ってそこで、NSDManagerのインスタンスを生成する実装にしているようです。細かいハンドリングをしないなら、こっちの実装のほうが良いかもしれませんね。この実装ならプラットフォームの違いをUnity側で意識する必要はなさそうです。