webRTC 超低遅延化を目指して VRのサーバー処理(2)

スマホVRは、手軽で楽しいが処理能力の制約がある。また、360度映像の配信において、360度全ての映像を常に送り続ける必要はない。
つまり、見ている方向を検知してその部分だけの映像を遅れるようにすれば、映像のサイズも小さくなるので遅延が少なくなるはず！(配信側PCのCPUは上がると思うが)
全て、コンピューターでやって映像だけ送ってしまおう！
そうすれば超低遅延化できるはず！　という記事の続きです。

制作しながら書いてるので、いろいろなところに話が飛んだり、間違ってたりするかもしれません

[webRTC 超低遅延化を目指して VRのサーバー処理(1)](https://qiita.com/kiyu_knowledges/items/1f91bf115b79250c4ed8) 

#双方向の通信
#Unity内のカメラと視聴デバイス同期
##方法
webRTCでは映像以外にもいろいろな情報をやりとりできるみたいだが、
webRTCは奥がとても深そうなのでWebsocketで実装する。
基本的には以下の方法を使う。
client ←(socket.io)→ server ←(OSC)→ Unity


##カメラ姿勢の同期
[前回](https://qiita.com/kiyu_knowledges/items/2685d1f9323221ac335c)作ったwebRTCのシステムをベースに進めていく。

このようにスマホのalpha, beta gamma回転をx, y, zにそのまま入れればいいと思っていたが、そんなに簡単じゃないようだ。
[デバイスの傾き、方角、加速度を取得できる「Device Orientation Event」](http://www.antytle.com/media/topic/26)

カメラのジャイロセンサーをUnityのカメラと同期させるときにうまく変換しないといけない。
[VRゲーム用にスマホジャイロセンサとMainCameraを同期させる](https://qiita.com/fumishitan/items/c2b023b9b9655d0a7d46)
Unityアプリの場合はこの`Input.gyro.attitude`を取ってくればいいのだが、
クライアント側で使う予定はない。どうすればいいか。
姿勢推定にはQuaternionという概念が重要なようだ。
[組込み技術者のための四元数（クォータニオン）入門（２）：応用編](https://kumadasu.com/2017/09/22/%E7%B5%84%E8%BE%BC%E3%81%BF%E6%8A%80%E8%A1%93%E8%80%85%E3%81%AE%E3%81%9F%E3%82%81%E3%81%AE%E5%9B%9B%E5%85%83%E6%95%B0%EF%BC%88%E3%82%AF%E3%82%A9%E3%83%BC%E3%82%BF%E3%83%8B%E3%82%AA%E3%83%B3%EF%BC%89-2/)
結構調べたが、これを実装するのはかなり骨が折れそうだ。

###奥の手
A-frameのカメラを非表示で起動してそこからデータを得よう。
前回の記事で述べたようにバージョンは0.8.2を使う。
0.8.2でカメラの回転がどうしても取れなかったのでstackoverflowにお世話になりました。
[Doesn't work EventListener in a-frame 0.8.2](https://stackoverflow.com/questions/51916850/doesnt-work-eventlistener-in-a-frame-0-8-2)

0.8.0より少し構文が厳しくなったのかな？

```index.html
<script>
    AFRAME.registerComponent("foo", {
        tick: function() {
            socket.emit('CameraInfo', {
                x: this.el.getAttribute("rotation").x,
                y: this.el.getAttribute("rotation").y,
                z: this.el.getAttribute("rotation").z
            });
        }
    });
</script>
<a-scene style="display:none">
    <a-camera id="camera" foo></a-camera>
</a-scene>
```

この値をサーバー側で値を受けてoscに流す。
`node-osc`を使ってoscメッセージを扱うのでnpmでインストールする。
前回のサーバー側コードにnode-oscの宣言を追加

```app.js
// -- OSC part --
var osc = require('node-osc');
var oscClient = new osc.Client("127.0.0.1", 6001);
```
signaling part以下の`socketio.on(){}`内に
サーバーからの情報を受けてoscで送るコードを追加

```app.js
// -- signaling part --
socketio.on('connection', function(socket) {
    //追加
    socket.on('CameraInfo', function(data) {
        oscClient.send("/Server/CameraInfo", socket.id, data.x, data.y, data.z);
    });
```

####サーバー → Unity
OSCメッセージをUnityで受け取れるようにする。
Unity OSCのヒット数が多かったのでやってみたがUnity初心者には理解できない。
解説が丁寧なサンプルを見つけた。
[UnityOSC](http://thomasfredericks.github.io/UnityOSC/)
サンプルを開いてplay, MAXの例も梱包されているのでそれを使うとOSCメッセージが送れている事がわかる。

これをベースにOSCから受け取った値をカメラに入れていく。
A-FrameとUnityの座標変換をする(Z軸の向きが逆)。

```OSCReceiver.cs
using UnityEngine;
using System.Collections;

public class OSCReceiver : MonoBehaviour {
    
   	public OSC osc;

	// Use this for initialization
	void Start () {
        osc.SetAddressHandler( "/Server/CameraInfo" , CameraInfo );
    }
	
	// Update is called once per frame
	void Update () {
	
	}

	void CameraInfo(OscMessage message){
        float x = - message.GetInt(1);
        float y = - message.GetInt(2);
		float z = message.GetInt(3);
		transform.rotation = Quaternion.Euler(x,y,z);
	}
    
}
```

##アスペクト比の同期
スマホの画面にアスペクト比を合わせた映像をUnityから送らなければならない。
ので、まずはスマホから情報を送る。
###クライアント側
画面サイズは`window.innnrHeight`と`window.innnrWidth`で取ればいいのだが、
画面回転時の対応で少しつまづいた。
Androidでのイベント実行順が
`resize` → `window.innnrHeight`→ `orientationchange`
の順番のようだ。
だから`orientationchange`イベントで`window.innnrHeight`を取得すると値が変わらない。
`resize`イベントでとる。
[Android標準ブラウザの本体回転時の横幅取得に関して](http://d.hatena.ne.jp/matbluecat/20121030/1351568333)

```index.html
<script type="text/javascript">
     window.onload = function() {
               socket.emit('CameraSize', {
                    width: window.innerWidth,
                    height: window.innerHeight
                }); 
            }
    window.addEventListener('resize', function(event) {
            socket.emit('CameraSize', {
                w: window.innerWidth,
                h: window.innerHeight
            });
         });
</script>
```
        
###Unity側
(追記) この方法自体は間違っていないのだが、Syphon用に解像度を指定するには、この方法ではダメだった。コードの下に正しい方法を書いた。
~~Unityでは解像度の直接指定はできないようだ。~~
~~出力デバイスの解像度に合わせて自動で調整するらしい。~~
~~ので、アスペクト比を調整していく。~~
~~以下のサイトとUnityOSCのサンプルを大いに参考にしてOSCを受信した値でカメラのアスペクト比を調整する。~~
[Unityで解像度に合わせて画面のサイズを自動調整する](http://www.project-unknown.jp/entry/2017/01/05/212837)
[複数の解像度に対応する（引き伸ばす)](https://unity3d.com/jp/learn/tutorials/projects/2d-shooting-game/supporting-multiple-resolutions%3A-enlarging-screen-size)

~~CameraのInspectorで`projection`を`northographic`に変更後~~
~~以下のスクリプトをカメラに追加~~

```CameraSize.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class CameraSize : MonoBehaviour {
	public OSC osc;

	// Use this for initialization
	void Start () {
        osc.SetAddressHandler( "/Server/CameraSize" , getCameraSize );
       }
	
	// Update is called once per frame
	void Update () {
	
	}
	void getCameraSize(OscMessage message){
        print(message);
        // 開発している画面を元に縦横比取得
            float developAspect = 10f / 16f;
            // 実機のサイズを取得して、縦横比取得
            float deviceAspect = message.GetFloat(1) / message.GetFloat(2);
            // 実機と開発画面との対比
            float scale = deviceAspect / developAspect;
            Camera mainCamera = Camera.main;
            // カメラに設定していたorthographicSizeを実機との対比でスケール
            ///初期値を設定(画面の回転を繰り返すと、カメラの大きさが小さくなってしまうため)
            mainCamera.orthographicSize = 6;
            float deviceSize = mainCamera.orthographicSize;
            // scaleの逆数
            float deviceScale = 1.0f / scale;
            // orthographicSizeを計算し直す
            mainCamera.orthographicSize = deviceSize * deviceScale;
	}
}
```
 ~~これで、アスペクト比を同期できた。~~

(追記　正しい方法)

CamTwistでSyphonからの映像を確認したが、なぜかサイズがあっていない。
よこが削られて黒くなってしまう。
なぜ？？
Unityからの出力はゲームビューコントロールバーのアスペクト比になってしまうみたい。
2時間くらい悩んだ末探してもいい情報がないのでSyphonのServerコードを眺めてた。
コードを変更したらなんとかなりそう！
SyphonServer.csファイルの101行目と102行目あたりを書き換える。
(追記  解決したので webRTC 超低遅延化を目指して VRのサーバー処理(3)で書きます！)

```SyphonServer.cs
101 var width = _hasCamera ? camera.pixelWidth : _sourceTexture.width;
102 var height = _hasCamera ? camera.pixelHeight : _sourceTexture.height;
```

いろいろ調べたが、ここのコードをインタラクティブに制御するうまい方法がなかったので
とりあえずテストとして`camera.pixel~`に直接、スマホの解像度を入力した。
(追記  使用していたSyphonが公式じゃないことに気づいた。公式だったらできるかもしれない)

#Unityから送信した映像を表示する
問題はCamTwistの出力サイズが手動制御しかできないことだが、そこは飛ばして、クライアント側の映像表示について。

##CamTwist → 配信用ブラウザ → クライアント
アスペクト比があってる映像が送られてくるので、そのまま全画面表示すればいいはず。
テストのために、CamTwistの出力設定を手動でスマホのウィンドウサイズに合わせる。
listen.jsより上にvideoタグを追加する

```index.html
<video id="remote-video" style="width:100%; height:100%" autoplay loop="true"></video>
```
時々、勝手に映像のサイズが変わるので`style`を指定した。
なぜだろう。
とりあえずテスト成功。

いろいろと手動ではあるが、システムが一通り完成した。
手動部分は今後変更していきたい。

##テスト
どのくらい効果があったかテストした。
同じソースを元に同じWiFi経由で、
 A. 360度映像全てを送るパターン
 B. 今回の一部だけ送るパターン
を検証した。
A.の場合は1080*2160の映像
B.の場合はスマホのウィンドウサイズに合わせて512*360の映像を送る


###画質
![webRTC.png](https://qiita-image-store.s3.amazonaws.com/0/281453/be6aa10c-e8d6-7375-55fb-4d890281d174.png)

画角が多少異なっていて公平ではないが今回とった方法(B)が画質が良いのがわかるだろうか？

###遅延
####パターンA
`CamTwist → 配信ブラウザ → スマホ` 0.5~0.7秒

####パターンB
#####準備
テストをするためにこの辺を参考にしてUnityにタイマーを追加
[制限時間を表示！Unityでタイマーを扱う方法【初心者向け】](https://techacademy.jp/magazine/9311)
[【Unity】TextMeshがぼやける！テキストをはっきり表示させる方法！](https://www.clrmemory.com/unity/text-mesh-blur/)

#####結果
`Unity → CamTwist → 配信ブラウザ → スマホ` 約0.3秒
これくらいの遅延だと、全然気にならなかった。

youtubeにデモをあげた
[![IMAGE ALT TEXT HERE](http://img.youtube.com/vi/HdbVeQfrRD8/0.jpg)](http://www.youtube.com/watch?v=HdbVeQfrRD8&feature=youtu.be)

#まとめ
これで一通り1:1は完成した。
今後、1:Nへの拡張、また、方法の再検討を行なっていくつもりだ。