【Unity】pixiv社のWebRTC.NETを動かしてみた

この記事は[Unity #2 Advent Calendar 2019](https://qiita.com/advent-calendar/2019/unity2)の20日目の記事です．

前日の記事
https://qiita.com/herieru/items/ab3ec79610bf2deae8da

## 概要
先日，pixiv社がWebRTCの.NETバインディングを公開しました．
WindowsはもちろんiOS, Androidなど様々なプラットフォームでテストされているようで気になった（Oculus Questで動かしてみたかった）ので試してみました．
今回は[Sora Labo](https://github.com/shiguredo/sora-labo-doc)との映像の送受信までの動作確認をします．

リリース記事
https://inside.pixiv.blog/nekomanma/7920

## 環境
- Windows10 64bit
- Unity 2019.3.0f3

## 動作テスト
### WebRTCの導入
Unity Hubから新規プロジェクトを作成します．
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/170854/e5d85c6a-97d4-5d06-4ca1-c5e493379a7c.png)

[リリース記事](https://inside.pixiv.blog/nekomanma/7920)を参考にWebRTC.NETをプロジェクトに導入していきます．

公開されているGitHubのreleases( https://github.com/pixiv/webrtc/releases )からパッケージファイル`com.pixiv.webrtc.tgz`をダウンロードします．
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/170854/26bf5376-2e3f-fcee-dc4a-59ff095c73f8.png)

ダウンロードしたパッケージをプロジェクトのルートディレクトリなど，好きな場所に移動します．

プロジェクトからパッケージを利用するため，`Packages/manifest.json`を編集します．
パッケージの場所をPackagesからの相対パスで指定するため，dependenciesに以下を追加します．

` { "dependencies": { "com.pixiv.webrtc": "file:../com.pixiv.webrtc.tgz" } } `

追加後プロジェクトを開くとパッケージのインポートが走り，WebRTCが追加されていることがわかります．
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/170854/eb5a7833-a9ab-d41c-5445-0522b6288359.png)

### サンプルシーンの導入
pixiv/webrtcのリポジトリをClone，あるいはzipでダウンロードします．
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/170854/dc24b0d5-3797-19b0-e9d6-22aad015e7ba.png)

Unityのサンプルプロジェクトは`webrtc/examples/unity/`に含まれています．
Assets/以下のファイルを先程作成したプロジェクトにまとめてD&Dします．
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/170854/f84a4d95-77cb-2b59-3e8d-2e8505434e4e.png)

ここまで問題なく進んでいれば，Scenes/SampleSceneを開くとこのような画面が出てくるかと思います．
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/170854/c62c1217-a50b-7ec1-3566-be6d32fbc21e.png)

### サンプルの改造
今回は，動作確認のため株式会社時雨堂さんが提供してくださっている[Sora Labo](https://github.com/shiguredo/sora-labo-doc)を利用します．
これは，同社が提供しているWebRTC SFU Soraを検証目的であれば無料で試せるサービスです．

Sora Laboのアカウント作成などは公式ドキュメント等をご参照ください．
https://sora-labo.shiguredo.jp/
https://github.com/shiguredo/sora-labo-doc

WebRTC.NETのUnityサンプルはSFU Sora用に作られていますが，Sora Laboの利用に必要なsignaling_keyなどのmetadataの設定がされていないので，コードを少しだけ改造します．

`Sora/Connection.cs`の中の`internal async Task Standby(string sdp)`の中にJsonWriterでSoraに送るJSONが記述されています．
そこにmetadataの加筆を行います．

```c#

using (var writer = JsonReaderWriterFactory.CreateJsonWriter(
    connect,
    Encoding.UTF8,
    false))
{
    writer.WriteStartElement("root");
    writer.WriteAttributeString("type", "object");
    writer.WriteElementString("type", "connect");
    writer.WriteElementString("channel_id", _channelId);
    writer.WriteElementString("sdp", sdp);
    writer.WriteElementString("user_agent", "sora-dotnet-sdk");
    writer.WriteStartElement("audio");
    writer.WriteAttributeString("type", "boolean");
    writer.WriteString("false");
    writer.WriteEndElement();
    writer.WriteStartElement("video");
    writer.WriteAttributeString("type", "object");
    writer.WriteElementString("codec_type", "VP8");
    writer.WriteEndElement();
    
    // add metadata signaling key
    writer.WriteStartElement("metadata");
    writer.WriteAttributeString("type", "object");
    writer.WriteElementString("signaling_key", _signalingKey);
    writer.WriteEndElement();

    switch (_role)
    {
        case Role.Downstream:
            writer.WriteElementString("role", "downstream");
            break;

        case Role.Upstream:
            writer.WriteElementString("role", "upstream");
            break;
    }

    writer.WriteEndElement();
}
```

`_signalingKey`には直接signaling_keyを記述しても動作しますが，Connectionクラスに\_signalingKey変数を定義してそれに伴って初期化の定義などを追加するとサンプルシーンのスクリプト`Main.cs`からもsignaling\_keyが指定できるようになります．

Sora Laboとの接続を行うため，`Main.cs`にSora LaboのSignalingUri，作成したchannelId，(signalng_key)を設定します．

```C#
    public string signalingUri = "signalingUri";
    public string channelId = "channelId";
    public string signalingKey = "signaling_key";
```

### 映像の送受信
metadataの設定が完了したので，実際の映像の送受信のテストを行います．
テストのため，Sora Laboのブラウザ上のサンプルを利用します．
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/170854/c20124cb-3962-62c7-f74a-8a27114ab016.png)

今回のテストでは，

- シングルストリーム送信
- シングルストリーム受信

の２種類を利用します．

#### Unity -> ブラウザ
まず，Unityから出た映像をブラウザで受信します．

Unityのサンプルシーンを実行し，`Upstream`ボタンをクリックします．
これで，Sora Laboとの接続が完了します．

ブラウザでは，`シングルストリーム受信`のサンプルを開き，videoの設定を`VP8`に変更し`connect`をクリックします．

すると以下のようにUnityのゲームビューのキャプチャがブラウザに送られていることがわかります．(低遅延ですごい）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/170854/997f0532-5fb4-5f0b-ecd1-cb7d7bdd2b94.png)

#### ブラウザ -> Unity
Unity -> ブラウザと同様の操作でUnityでは`Downstream`をクリックします．

ブラウザでは，`シングルストリーム送信`のサンプルを開き，videoを`VP8`に，音声入力や映像入力のデバイスを設定して`connect`をクリックします．

正しく動作していればブラウザで取得した映像デバイスの映像がUnityに送られていることがわかります．
（以下で利用しているのはテスト用の仮想デバイスです）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/170854/929e5bfb-36c0-1e89-17a5-3754738b3311.png)

## VRヘッドセットでの利用について（おまけ）
pixiv社によると，Androidでの動作もテストされているようですのでOculus Questでの利用が可能か検討しました．
具体的には，VR内のカメラの映像を別のVRヘッドセットに流すようなものを想定しています．

結論を言うと，***VRヘッドセットでの利用は厳しい***という結論に至りました．

サンプルプロジェクトでは，`VideoTrackSource.cs`でWebRTCに流すテクスチャを取得する処理を行っているようです．
具体的には，`ScreenCapture.CaptureScreenshotAsTexture()`でスクリーンキャプチャのTexture2Dを取得し，`Texture2D.GetRawTextureData<Color32>()`でNativeArray\<Color32>を取り出しています．

```C#
// こんな感じ
var texture = ScreenCapture.CaptureScreenshotAsTexture(1);
var data = texture.GetRawTextureData<Color32>();
```

これより，最終的にプラグインに渡すのはNativeArray\<Color32>であり，それに伴ってTexture2Dの取得が必要であることがわかります．
つまり，通常カメラから取得できる***RenderTextureからTexture2Dへの変換が必要である***ということだと思います．

察してくださった方も多いと思いますが，この***RenderTextureからTexture2Dに変換する処理***はかなり重い処理で毎フレーム実行するのはVRヘッドセットでの利用に際して現実的ではなく，今回Oculus Questで利用するのは厳しいという結論に至りました．
実際，上記の動作テストでもUpstreamとDownstreamを比較するとUpstreamでかなりfpsが低下しています．

参考 : RenderTexture to Texture2D
https://docs.unity3d.com/jp/460/ScriptReference/Texture2D.ReadPixels.html
https://docs.unity3d.com/ScriptReference/Rendering.AsyncGPUReadback.html

## 最後に
当初の目的での利用は厳しい，という悲しい結論に至りましたが，WebRTCをUnityで利用できる（しかも複数プラットフォームで）のはやれることの幅が広がる素晴らしいことだと思います．

処理負荷の軽減の策がある方いらっしゃいましたら教えてくださると嬉しいです...!
（`RenderTexture.GetNativeTexturePtr`とか使いたい）