webRTCを導入して360度映像を低遅延で生配信する

[昨日](https://qiita.com/kiyu_knowledges/items/062ff23d334a24c4efec)はRTMPを使用するところまで行ったが、
今日はより少ない遅延を求めてwebRTCを導入してみる。(macユーザー)

最終的な目標は360度カメラで撮った映像を遅延をなるべく無くして、
スマホ上に配信すること。

#デモを試してみる
以下のサイトが大変役に立った。
感謝感謝。
[WebRTCでキャスしよう！片方向リアルタイム映像配信を作ろう](https://html5experts.jp/mganeko/11444/)
とりあえずこれをコピペして試せた。
確かに遅延は1秒以下だ。
素晴らしい。。。

ここからカスタマイズしていく。

#ソースの検討
どうやって、配信元のウェブブラウザにライブ映像を届けるか考えられるのは、

- [CamTwist](http://camtwiststudio.com/)
- [OBS](https://obsproject.com/ja)

###CamTwist
CamTwistはwebカメラから入力映像を受け取り、加工を加えて、仮想webカメラとして出力する
したがって、デモのコードをそのまま使える。
###OBS
obsでもプラグインを使うことで同様の機能を使えるようになる。
下記サイトに導入方法や使い方が書かれている。(Windows用だった。。。)

#####Windows
[OBSを使ってPixivSketchLIVEで配信する動機](http://packetroom.net/pixiv_sketch_live/)
#####Mac
[iVirtualCamera](https://github.com/csuft/iVirtualCamera) 
があるのだが、
This project is incompatible with the latest OS, it may causes the system crash at boot.
ということで、大変そうなのでCamTwistを使うことにする。

#カスタマイズ
昨日動いてたコードが動かない・・・・
websocketのconnectとdisconnentのタイミングの問題だったみたい。


##nodejsをhtmlサーバーとして外部から接続できるようにする
シグナリングサーバをhtmlサーバーとしても使用する。
以前つくっったnode.jsのサーバー用コードをシグナリングサーバーファイルと統合する。

```app.js
var BROADCAST_ID = '_broadcast_';

// -- create the socket server on the port ---
var app = require('http').createServer(handler);
var socketio = require('socket.io').listen(app);
var port = 9001;
app.listen(port);
console.log('signaling server started on port:' + port);
 
// ---------- read html part ----------
var fs = require('fs');
var path = require('path');
var mime = {
// 読み取りたいMIMEタイプはここに追記
    ".html": "text/html",
    ".css":  "text/css",
    ".js":  "text/javascript",
};

//ファイルの読み込み
function handler(req, res) {
    console.log(req.url)
    if (req.url == '/') {
        filePath = '/index.html';
  } else {
    filePath = req.url;
  }
  var fullPath = __dirname + filePath;

  res.writeHead(200, {"Content-Type": mime[path.extname(fullPath)] || "text/plain"});
  fs.readFile(fullPath, function(err, data) {
    if (err) {
      // エラー時の応答
    } else {
      res.end(data, 'UTF-8');
    }
  });
}

// ---------- signaling part ----------
// This callback function is called every time a socket
// tries to connect to the server
socketio.on('connection', function(socket) {
 
    // ---- multi room ----
    socket.on('enter', function(roomname) {
      socket.join(roomname);
      console.log('id=' + socket.id + ' enter room=' + roomname);
      setRoomname(roomname);
    });
 
    function setRoomname(room) {
      //// for v0.9
      //socket.set('roomname', room);
 
      // for v1.0
      socket.roomname = room;
    }
 
    function getRoomname() {
      var room = null;
 
      //// for v0.9
      //socket.get('roomname', function(err, _room) {
      //  room = _room;
      //});
 
      // for v1.0
      room = socket.roomname;
 
      return room;
    }
 
 
    function emitMessage(type, message) {
      // ----- multi room ----
      var roomname = getRoomname();
 
      if (roomname) {
        console.log('===== message broadcast to room -->' + roomname);
        socket.broadcast.to(roomname).emit(type, message);
      }
      else {
        console.log('===== message broadcast all');
        socket.broadcast.emit(type, message);
      }
    }
 
 
    // When a user send a SDP message
    // broadcast to all users in the room
    socket.on('message', function(message) {
        message.from = socket.id;
 
        // get send target
        var target = message.sendto;
        if ( (target) && (target != BROADCAST_ID) ) {
          console.log('===== message emit to -->' + target);
          socket.to(target).emit('message', message);
          return;
        }
 
        // broadcast in room
        emitMessage('message', message);
    });
 
    // When the user hangs up
    // broadcast bye signal to all users in the room
    socket.on('disconnect', function() {
        console.log('-- user disconnect: ' + socket.id);
        // --- emit ----
        emitMessage('user disconnected', {id: socket.id});
 
        // --- leave room --
        var roomname = getRoomname();
        if (roomname) {
          socket.leave(roomname);
        }
 
    });
 
});
```

また、ページが読み込まれたときに自動で映像を受信するように変更した。
listen.jsは上記のサイトと全く同じ。

```index.html
<!DOCTYPE html>
<html>

<head>
    <title>broadcast watch</title>
    <meta http-equiv="content-type" content="text/html; charset=UTF-8">
    <script>
        window.onload = function(){
            sendRequest();
            console.log("req");
        }
    </script>
</head>

<body>
    <video id="remote-video" autoplay controls muted></video>
    <!---- socket ※自分のシグナリングサーバーに合わせて変更してください ------>
    <script src="socket.io/socket.io.js"></script>
    <script src="./js/listen.js" type="text/javascript"></script>
</body>

</html>
```
シグナリングサーバーの書き換えがうまくできていないと、ローカルでは動いても、外部からアクセスした場合、シグナリングができないことがある。
スマホだとうまくいかないことが多い。やはりどこかにボタンを仕込むのが確実かも。



##360度映像に対応する
###映像のアスペクト比を2:1にする
360度映像形式は
**CamTwist**
`Preference/General/Video_Size`で`custom`を選択し、サイズを変更
今回は920*1960に設定した。
設定したのちCamTwistをリスタート

**webRTC**

- talk側

  videoタグの設定を変更する

```talk.html
<!DOCTYPE html>
<html>

<head>
    <title>Broadcast Talk</title>
    <meta http-equiv="content-type" content="text/html; charset=UTF-8">
</head>

<body>
    <button type="button" onclick="startVideo();">Start video</button>
    <button type="button" onclick="stopVideo();">Stop video</button> &nbsp;&nbsp;&nbsp;&nbsp;
    <button type="button" onclick="tellReady();">On Air</button>
    <br />
    <div style="position: relative;">
        <video id="local-video" autoplay controls style="width: 100%;"></video>
    </div>

    <!---- socket ※自分のシグナリングサーバーに合わせて変更してください------>
    <script src="http://localhost:9001/socket.io/socket.io.js"></script>
    <script src="./js/talk.js" type="text/javascript"></script>
</body>

</html>
```

- listen側

videoタグの設定を変更
resizeRemoteVideo()のファンクションを削除

```index.html
<!DOCTYPE html>
<html>

<head>
    <title>broadcast watch</title>
    <meta http-equiv="content-type" content="text/html; charset=UTF-8">
    <script>
        window.onload = function(){
            sendRequest();
        }
    </script>
</head>

<body>
    <video id="remote-video" autoplay controls muted style="width: 100%;"></video>

    <!---- socket ※自分のシグナリングサーバーに合わせて変更してください ------>
    <script src="socket.io/socket.io.js"></script>

    <script src="./js/listen.js" type="text/javascript"></script>
</body>

</html>
```

これで、2:1の動画が送れるようになった。

###VR化する
A-frameを使ってこれをVRにする。

A-frameを使うのは2回目なのだがなぜか手間取った。
####通常の360度映像の場合
まずはwebRTCではなく普通の映像ファイルをソースとして使う方法。
A-frameの最新版は0.8.0なのだが、それを使うとなぜか映像が真っ白になって表示されなくなるので0.7.0を使った。
スマホ再生の0.7.0の問題は補足1。
(追記2018.08.19)
A-frameの最新バージョンは0.8.2でした。
これを使うと、映像が白くなりません。
また、スマホでもうまく再生できます。

ポイントはCamera位置を親entityで1.6m下げているところ。
A-frameのデフォルトカメラは人の視点にあわせるため高さが1.6mに設定されている。
今回は360度映像を使うので0mに視点をあわせる。

```index.html
<!DOCTYPE html>
<html>

<head>
    <title>broadcast watch</title>
    <meta http-equiv="content-type" content="text/html; charset=UTF-8">
    <script src="https://aframe.io/releases/0.7.0/aframe.min.js"></script>
</head>

<body>
    <a-scene>
        <a-assets>
            <video id="video" autoplay loop="true" src="video/test.mp4"></video>
        </a-assets>
        <!-- camera -->
        <a-entity id="camera" position="0 -1.6 0" rotate="0 0 0">
            <a-camera id="acamera"></a-camera>
        </a-entity>
        <!-- video sphere -->
        <a-videosphere src="#video"></a-videosphere>
    </a-scene>
</body>

</html>
```

####webRTC
これをベースとして、webRTCの映像を利用できるように書き換える。
書き換えはとても簡単

```index.html
<!DOCTYPE html>
<html>

<head>
    <title>broadcast watch</title>
    <meta http-equiv="content-type" content="text/html; charset=UTF-8">
    <script src="https://aframe.io/releases/0.7.0/aframe.min.js"></script>
    <script>
        window.onload = function(){
            sendRequest();
        }
    </script>
</head>

<body>
    <a-scene>
        <a-assets>
            <video id="remote-video" autoplay loop="true"></video>
        </a-assets>
        <!-- camera -->
        <a-entity id="camera" position="0 -1.6 0" rotate="0 0 0">
            <a-camera id="acamera"></a-camera>
        </a-entity>
        <a-videosphere src="#remote-video"></a-videosphere>
    </a-scene>

    <!---- socket ※自分のシグナリングサーバーに合わせて変更してください ------>
    <script src="socket.io/socket.io.js"></script>

    <script src="./js/listen.js" type="text/javascript"></script>
</body>

</html>
```

これでめでたく360度映像をリアルタイム中継できるようになった！

####比較
webRTCとvideoファイルを読み込む方法の比較
いかに出力される映像のキャプチャを比較した。
伝わるかわからないが明らかにwebRTCの方の画質が悪かった。
また再生もカクカクなりやすい。

<img width="542" alt="Presentation1.png" src="https://qiita-image-store.s3.amazonaws.com/0/281453/6f0fc999-2bc4-4fd1-0031-e74de0ef6232.png">

<img width="542" alt="Screen Shot 2018-08-17 at 15.38.48.png" src="https://qiita-image-store.s3.amazonaws.com/0/281453/428ffd87-c6ad-44a7-b86b-23ec6db5c84c.png">

#補足
##補足1
A-frame0.7.0で実現した360度映像をスマホでみると、すごいグッラグラする。
理由はわからないが解決方法。
~~A-frame0.8.0をローカルにダウンロードする~~(追記: 0.8.2でした)
~~a-frameのソースをローカルの`aframe-master.min.js`にすると解決する。~~
(追記 2018.08.19)
原因わかりました。
Chrome66以降の仕様変更によるものらしいです。
A-frame0.8.2で対応してます。
[Camera is super sensitive to motion on Chrome Beta 66 on Android ](https://github.com/aframevr/aframe/issues/3476)
#まとめ
とりあえずシステムはできた。
あとはどうすればクオリティが上がるか。