Raspberry Pi3にuv4l-webrtcをインストールしてNAT外から接続してみる 1

[前回](http://qiita.com/t_furu/items/d31dfa5e4c0881383ea5)で uv4l-webrtcの環境ができたのでfirebase等を使ってルーターを超えて接続できるようにしていきます。

# uv4l-server (uv4l-webrct) 設定を調整する。
ここで uv4l-uvc.conf の webtct に関する設定を変更します。

1. uv4l-uvc.conf をエディタで開いて変更する。  
    
    ```
sudo vi /etc/uv4l/uv4l-uvc.conf
### WebRTC options:
# server-option = --enable-webrtc=yes
...
server-option = --webrtc-receive-video=no <- 受信ストリームのOverlayを無効にする
### video rendering window size on display
...
server-option = --webrtc-preferred-vcodec=2 <- 優先ビデオコーディックを VP9 にする 

    ```
1. リブートして設定を有効にする。
    
    ```
    sudo reboot
    ```
webrtc-receive-video を no にしておかないとxwindowを立ち上げている場合、受信した映像が前面に表示されてしまいます。

# uv4l-webrtc の WebSocket コマンドについて
uv4l-webrtcとシグナリングする場合に WebSocketでローカルのサーバーへコマンドを送る必要があります。
## 接続先
WebSocketで接続する場合の接続先です。

```
wss://localhost:8090/stream/webrtc
```
## コマンド
WebSocketローカルサーバーへ送信する必要のあるコマンド

- offer
   
    ```javascript:answer
var command = {
		command_id: "offer",
		options: {
			force_hw_vcodec: true,
			vformat: "60" /* 1280x720 30 fps */
		}
};
ws.send(JSON.stringify(command));
    ```

- answer
    
    ```javascript:answer
var command = {
		command_id: "answer",
		data: JSON.stringify(sessionDescription)
};
ws.send(JSON.stringify(command));
    ```

- addicecandidate
    
    ```javascript:addicecandidate
const date = JSON.parse(evt.data.icecandidate);
const candidate = {
		sdpMLineIndex: date.sdpMLineIndex,
		sdpMid: date.sdpMid,
		candidate: date.candidate
};
const command = {command_id: "addicecandidate",data:JSON.stringify(candidate)};
ws.send(JSON.stringify(command));
    ```

# Firebase Realtime Databaseを使ってシグナリングして通話してみる
RasPi側でnode.jsアプリを作りFirebase Realtime Databaseを使ってWebRTCシグナリングをするコードを書いてみます。

## node.js で Firebase,WebSocket,wrtcを利用できるようにする

```
npm install firebase --save
npm install websocket --save
```
```
sudo apt-get update
sudo apt-get install python2.7 git-all pkg-config libncurses5-dev libssl-dev libnss3-dev libexpat-dev
sudo apt-get install w3m
git clone https://github.com/ssaroha/node-webrtc.git
cd node-webrtc
gunzip third_party/webrtc/lib/libwebrtc.a.gz
npm install -g
```

## node.jsアプリのコード
```javascript:config.js
/** 各種設定
*
*/
class Config{
  constructor(){
    //このデバイスの固有の識別子(ユニークな文字列)
    this.IDENTIFIER = "Rb6LFUGK";

    // Initialize Firebase
    this.FIREBASE_CONFIG = {
      apiKey: "APIKEY",
      authDomain: "0000000.firebaseapp.com",
      databaseURL: "https://0000000.firebaseio.com",
      storageBucket: "00000.appspot.com",
      messagingSenderId: "00000"
    };

    //uv4l-server (uv4l-webrtc)のWebSocket接続先
    this.UV4L_WEB_RTC_WEB_SOCKET = "ws://localhost:8090/stream/webrtc";

    //シグナリングの為のデータベース名
    this.FIREBASE_DB_WEBRTC_SIGNALING = "/webrtc/signaling";
  }
}
module.exports = new Config();
```


```javascript:web_rtc_signaling.js
/** Firebase Realtime Database でシグナリングする際に利用
*
*/
class WebRtcSignaling {
  constructor(identifier,type,data){
    this.identifier = identifier;
    this.type = type;
    this.data = data;
  }

  toData(){
    return {
      "identifier":this.identifier,
      "eventType":this.eventType,
      "data":this.data
    };
  }
}

WebRtcSignaling.TYPE_OFFER = "offer";
WebRtcSignaling.TYPE_ANSWER = "answer";
WebRtcSignaling.TYPE_CANDI_DATES = "candidates";
WebRtcSignaling.TYPE_CANDI_DATE = "candidate";
WebRtcSignaling.TYPE_REMOTE_CALL = "remote_call";
WebRtcSignaling.TYPE_REMOTE_HUNG_UP = "remote_hung_up";

module.exports = WebRtcSignaling;
```

```javascript:web_rtc_util.js
/** WebRTCの各処理を実装
*/
const W3CWebSocket = require('websocket').w3cwebsocket;
const WebRtcSignaling = require('./web_rtc_signaling.js');

const WebRTC = require('wrtc');
const RTCPeerConnection = WebRTC.RTCPeerConnection;
const RTCIceCandidate= WebRTC.RTCIceCandidate;
const RTCSessionDescription = WebRTC.RTCSessionDescription;
const RTCDataChannel = WebRTC.RTCDataChannel;

class WebRTCUtil{
    constructor(config,firebase){
      this.Config = config;
      this.ws = null;
      //firebaseとの接続
      this.webRtcSignalingAll = null;
      this.firebaseInitialize(firebase);
    }

    //通話 開始
    call(){
      console.log("call");
      const _this = this;
      //WebSocket接続の状態を確認
      if(this.ws == null){
        //WebSocket接続して接続成功したらofferを送る
        const clientConfig = {tlsOptions: {rejectUnauthorized: false}};
        this.ws = new W3CWebSocket(this.Config.UV4L_WEB_RTC_WEB_SOCKET, null, null, null, null, clientConfig);
        this.ws.parent = this;
        this.ws.onopen = this.onWsOpen;
        this.ws.onmessage = this.onWsMessage;
        this.ws.onclose = this.onWsClose;
        this.ws.onerror = this.onWsError;
      }
      else{
        //WSと接続済みならローカルのWSサーバーにofferを送る
        this.wsOffer();
      }
    }

    //通話 終了
    hungUp(){
      console.log("hungUp");
      this.wsStop();
    }

    //firebaseとの接続 初期化
    firebaseInitialize(firebase){
      const _this = this;
      this.webRtcSignalingAll = firebase.database().ref(this.Config.FIREBASE_DB_WEBRTC_SIGNALING+"/"+this.Config.IDENTIFIER);
      this.webRtcSignalingAll.on("value", function(snapshot) {
        const evt = snapshot.val();
        console.log("onvalue");
        console.log(evt);
        //識別子をチェックして自分以外だった時だけ以下の処理を実行
        if((evt == null)||(_this.Config.IDENTIFIER == evt.identifier)){
            return;
        }
        if (evt.type === WebRtcSignaling.TYPE_ANSWER) {
          //answer SDPを受信したので answer を設定
          let sessionDescription = new WebRTC.RTCSessionDescription({
            type : 'answer',
            sdp : evt.data.sdp,
          });
          var command = {
              command_id: "answer",
              data: JSON.stringify(sessionDescription)
          };
          _this.wsSend(command);
        }
        else if (evt.type === WebRtcSignaling.TYPE_OFFER) {
            //offer SDPを受信したので offerを設定
        }
        else if (evt.type === WebRtcSignaling.TYPE_CANDI_DATE) {
          //RasPi側の wsで受け取ったicecandidateがfirebaseから届く
        }
        else if (evt.type === WebRtcSignaling.TYPE_REMOTE_CALL) {
          //Android側から RasPi のカメラへ繋ぐ
          _this.call();
        }
        else if (evt.type === WebRtcSignaling.TYPE_REMOTE_HUNG_UP) {
          //Android側から RasPi のカメラへの接続を中断
          _this.hungUp();
        }
      });
    }

    //WSでコマンドを送信する
    wsSend(command){
      console.log("wsSend");
      if(this.ws == null){
        console.log("　ws is null");
        return;
      }
      console.log("　ws.readyState "+this.ws.readyState);
      //readyState 1 OPEN
      if(this.ws.readyState != 1){
        return;
      }
      this.ws.send(JSON.stringify(command));
    }

    //WS接続に成功
    onWsOpen(){
      console.log("onWsOpen");
      //WS設定が完了したら offerの処理を開始する
      this.parent.wsOffer();
    }

    onWsClose(){
      console.log("onWsClose");
    }

    onWsError(evt){
      console.log("onWsError");
      console.error(evt);
      if (this.ws) {
          this.ws.close();
          this.ws = null;
      }
    }

    onWsMessage(evt){
      console.log("onWsMessage");
      const msg = JSON.parse(evt.data);
      console.log(msg);
      console.log("msg.type="+msg.type);
      switch (msg.type) {
        case "message":
          //alert(msg.data);
          console.error(msg.data);
          break;
        case "offer":
          console.log("msg.sdp="+msg.sdp);
          //RasPi側のSDPをリモートに送信
          this.parent.sendSdp(WebRtcSignaling.TYPE_OFFER,msg.sdp);
          break;
        case "geticecandidate":
          console.log("msg.data="+msg.data);
          const candidates = JSON.parse(msg.data);
          this.parent.sendIceCandiDates(candidates);
          break;
      }
    }

    wsStop(){
      if (this.ws) {
          this.ws.close();
          this.ws = null;
      }
    }

    //ローカルのWSサーバーにofferを送る
    wsOffer() {
        //this.rtcUtil.createPeerConnection();
        var command = {
            command_id: "offer",
            options: {
                force_hw_vcodec: true,
                vformat: "60"
            }
        };
        this.wsSend(command);
        console.log("offer(), command=" + JSON.stringify(command));
    }

    sendSdp(type,sdp){
      console.log("sendSdp");
      const webRtcSignaling = new WebRtcSignaling(this.Config.IDENTIFIER,type,{"sdp":sdp});
      this.webRtcSignalingAll.set(webRtcSignaling);
    }

    sendIceCandiDates(iceCandiDates){
      console.log("sendIceCandiDates");
      const webRtcSignaling = new WebRtcSignaling(this.Config.IDENTIFIER,WebRtcSignaling.TYPE_CANDI_DATES,{"icecandidates":iceCandiDates});
      this.webRtcSignalingAll.set(webRtcSignaling);
    }
}

module.exports = WebRTCUtil;
```

```javascript:app.js
const Config = require('./config.js');
const WebRTCUtil = require('./web_rtc_util.js');
const firebase = require("firebase");

class App{
    constructor(){
      const _this = this;
      this.firebaseApp = firebase.initializeApp(Config.FIREBASE_CONFIG);
      this.webRTCUtil = new WebRTCUtil(Config,firebase);

      //60秒後に終了
      setTimeout(function(){
        this.firebaseApp.delete();
      },60*1000);
    }
}

new App();
```

## パワーセーブをOFFにする
パワーセーブに入るとfirebaseやWSとの接続が切れてしまうのでOFFにする。

```
sudo setterm -powersave off
```

Raspberry Pi側の設定は終わったので次回は、PCのブラウザから接続するhtmlを作成します。

# 参考
[uv4l-server](https://www.linux-projects.org/documentation/uv4l-server/)
[Firebaseリアルタイムデータベースを使ってみる](http://qiita.com/t_furu/items/665bea8b657c78ab2a22)
[WebSocket-Node](https://github.com/theturtle32/WebSocket-Node)
[node-webrtc](https://github.com/js-platform/node-webrtc)
[node-webrtc/issues/152](https://github.com/js-platform/node-webrtc/issues/152)
[Node WebRTC build for Raspberry PI](https://github.com/ssaroha/node-webrtc)