SkyWay WebRTC Gatewayハンズオン Chapter1

[ハンズオン](https://qiita.com/nakakura/items/faeb4f6df82677139761)のChapter 1

## Chapter 1 - Peer Create

最終的なWebRTC接続はP2Pですが、接続を確立するためには相手側のIPアドレスやポート番号等を交換する必要があります。
SkyWayサーバを利用することでこれらの必要な情報を簡単に交換できるようになるので、まずサーバへの接続を行います。

完成したソースコードは[github](https://github.com/skyway-lab/skyway-webrtc-gw-handson/tree/master/chapter1)を確認して下さい。

### SkyWayのAPI Keyの登録

以下の記事を参照してAPI Keyを取得して下さい。
本ハンズオン手順ではドメイン名はlocalhostで進めていますので、忘れずにホスト名登録をしてください。
https://qiita.com/yusuke84/items/54dce88f9e896903e64f#step0-1

### WebRTC Gatewayのインストール

WebRTC Gateway本体については
https://github.com/skyway/skyway-webrtc-gateway
からARM版をダウンロードしてRaspberry Piの適当なディレクトリに設置してください。
/usr/local/bin配下に置いてもらってもいいですし、ホームディレクトリに置いてもらってもいいです。

```shell
$ wget https://github.com/skyway/skyway-webrtc-gateway/releases/download/0.0.2/gateway_linux_arm
$ chmod +x gateway_linux_arm
```

### WebRTC Gateway側-接続
#### 前置き

WebRTC Gatewayは単独で動作するアプリケーションで、外部からはREST API経由で操作します。
WebRTC Gatewayを利用するプログラムは任意の言語で開発できます。
今回のハンズオンではrubyのサンプルで実装していきますが、同内容のコーディングをしてもらえれば別の言語を使ってもらっても構いません。

#### 共通系のコード

WebRTC GatewayはREST APIでアクセスします。
あちこちから利用することになるhttp clientのコードは共通化し、util.rbとして切り出しておきます

[util.rb](https://github.com/skyway-lab/skyway-webrtc-gw-handson/blob/master/chapter1/ruby/util.rb)

```ruby
require "net/http"
require "json"
require "socket"

#同期的にREST APIにアクセスするための関数
def request(method_name, uri, *args)
  response = nil
  Net::HTTP.start(HOST, PORT) { |http|
    # URLとWebRTCに投げるJSONの外、
    # GET/POST/PUT等を行うのでメソッドの指定もできるようにする
    response = http.send(method_name, uri, *args)
  }
  response
end

#EVENTの取得はLong Pollで行うので、非同期で取れるようにしておく
def async_get_event(uri, event, &callback)
  e = nil
  thread_event = Thread.new do
    # timeoutする場合があるのでその時はやり直す
    while e == nil or e["event"] != event
      res = request(:get, uri)
      # Status Code 200で以下のようなJSONが帰ってくるのでparseする
      # {
      #   "event"=>EVENT_NAME,
      #   "params"=> OBJEDT
      # }
      if res.is_a?(Net::HTTPOK)
        e = JSON.parse(res.body)
      end
    end
    if callback
      callback.call(e)
    end
  end.run
  thread_event
end
```

#### Mainの処理

メインの処理は以下の流れです。

[webrtc_control.rb](https://github.com/skyway-lab/skyway-webrtc-gw-handson/blob/master/chapter1/ruby/webrtc_control.rb)の一部

```ruby
if __FILE__ == $0
  if ARGV.length != 1
    exit(0)
  end
  
  # (0)
  peer_id = ARGV[0]
  skyway_api_key = ENV['API_KEY']

  # (1)
  peer_token = create_peer(skyway_api_key, peer_id)
  # (2)
  th_onopen = listen_open_event(peer_id, peer_token) {|peer_id, peer_token|
    p peer_id
    p peer_token
  }
  th_onopen.join
end
```
(0)  SkyWayサーバは、接続してきているクライアントプログラムをpeer_idというidで識別しています。またSkyWayサーバへ接続できるのは正しいAPI KEYを知っているプログラムだけです。まずはこれらの情報をプログラムに与えます。
自分のIDは秘密の情報ではないので、ハードコーディングでも構いませんが、今回はコマンドライン引数から取得しています。
SkyWayのAPI KEYは盗用を避けるため、ハードコーディングせず環境変数等から取ることをお勧めします。

(1) SkyWay WebRTC GatewayにPeer作成の指示を与えます。これによりWebRTC Gatewayの中にPeer Objectが生成され、SkyWayサーバへ接続を開始します。この時点ではまだSkyWayサーバにrejectされる可能性があるので、与えたpeer_idは正式に利用できる状態ではありません。
また別のプログラムが間違って自分のPeer Objectを操作しないよう、peer_tokenという識別子が発行されます。今後Peer Objectを操作する際にはこのpeer_tokenも一緒に与えて操作します。

(2) SkyWayサーバへの接続に成功するとOPENイベントが発火するのでそれを監視します。この時点からpeer_idはSkyWayサーバに登録された正式なものになるので、別のプログラムからWebRTC接続をかけるのに利用することができます。

ここまででSkyWayサーバへの接続は完了です。詳細を確認しましょう。

#### Peer操作系

Peerの操作系はpeer.rbに切り出して記載します。

##### (1) SkyWay WebRTC GatewayへのPeer作成指示

create_peerメソッドの中身は以下のようになっています。

[peer.rb](https://github.com/skyway-lab/skyway-webrtc-gw-handson/blob/master/chapter1/ruby/peer.rb#L8)の一部

```ruby
# POST http://35.200.46.204/#/1.peers/peer を叩きPeerの割当を行う
def create_peer(key, peer_id)
  # (1)
  params = {
      "key": key,
      "domain": "localhost",
      "turn": false,
      "peer_id": peer_id,
  }
  res = request(:post, "/peers", JSON.generate(params))
  if res.is_a?(Net::HTTPCreated)
    # (2)
    json = JSON.parse(res.body)
    json["params"]["token"]
  else
    # 異常動作の場合は終了する
    p res
    exit(1)
  end
end
```

まずPeer Objectの作成は、REST APIのPOST /peerを叩くことで実施します。
[ドキュメント](http://35.200.46.204/#/1.peers/peer)を参照すると

```json
{
  "key": "KEY_FOO",
  "domain": "example.com",
  "peer_id": "ID_FOO",
  "turn": true
}
```

のようなJSONを与えるよう書かれていますので、自分の環境に合わせて記載します。今回はturnサーバは使わないのでfalseにしています。(1)

正常に動作している場合、Status Code 201で以下のようなJSONが帰ってくると記載されているので、これから使うtokenだけ取得します。(2)

```json
{
  "command_type": "PEERS_CREATE",
  "params": {
    "peer_id": "ID_FOO",
    "token": "pt-9749250e-d157-4f80-9ee2-359ce8524308"
  }
}
```

##### (2) SkyWayサーバへの接続確認

SkyWayサーバへの接続が成功するかどうか、OPENイベントを監視します。

[peer.rb](https://github.com/skyway-lab/skyway-webrtc-gw-handson/blob/master/chapter1/ruby/peer.rb#L34)の一部

```ruby

def listen_open_event(peer_id, peer_token, &callback)
  # (1)
  async_get_event("/peers/#{peer_id}/events?token=#{peer_token}", "OPEN") {|e|
    # (4')
    peer_id = e["params"]["peer_id"]
    peer_token = e["params"]["token"]
    if callback
      callback.call(peer_id, peer_token)
    end
  }
end

def async_get_event(uri, event, &callback)
  e = nil
  thread_event = Thread.new do
    # (2)
    while e == nil or e["event"] != event
      res = request(:get, uri)
      # (3)
      if res.is_a?(Net::HTTPOK)
        e = JSON.parse(res.body)
      end
    end
    if callback
      # (4)
      callback.call(e)
    end
  end.run
  thread_event
end

```

/peers/{peer_id}/events をGETすることでイベントが取得できるので取得を開始します。このGETはlong-pollなので非同期で実施します。(1)

[ドキュメント](http://35.200.46.204/#/1.peers/peer_event)を確認すると、タイムアウトする可能性があることを書かれています。これは長時間イベントが発生しない場合、REST APIの動作上一旦接続を切る必要があるためです。whileループで必要なイベントが取れるまで再接続します。(2)

イベントはStatus Code 200で帰ってきます。(3)

帰ってきた中身をcallbackに与えて処理を行います。以下のようなJSONが帰ってくるのでpeer_id, tokenを取得します。(4)(4')

```json
{
    "event"=>"OPEN",
    "params"=> {
        "peer_id": PEER_ID,
        "token": TOKEN
    }
 }
```

尚、ここで取得できるpeer_idとpeer_tokenは、特殊な使い方をする場合を除き、基本的に自分の与えたものから変更はありませんので、Status Code 200が帰ってきさえすれば取得は必須ではありません。

ここまででSkyWay WebRTC GatewayでのSkyWayサーバへの接続は完了です。

### ブラウザ側
#### 前置き
ブラウザ側ではJavaScript版のSkyWay Clientコードを作成しサーバへと接続します。基本的に内容は同じですが、SkyWayサーバがPeerを認識していることを確認するためlistAllPeersメソッドを実行して確認します。
まずはhtmlファイルを作成して下さい。内容は[chapter1/web/index.html](https://github.com/skyway-lab/skyway-webrtc-gw-handson/blob/master/chapter1/web/index.html)の通りです。

```javascript
'use strict';

// (1)
function getQueryParams() {
    if (1 < document.location.search.length) {
        const query = document.location.search.substring(1);
        const params = query.split('&');

        const result = {};
        for(var param of params) {
            const element = param.split('=');
            const key = decodeURIComponent(element[0]);
            const value = decodeURIComponent(element[1]);
            result[key] = value;
        }
        return result;
    }
    return null;
}

window.onload = ()=> {
    const query = getQueryParams();
    // (1')
    const key = query["key"];
    //peer idもGet Parameterから取る
    const peer_id = query["peer_id"]

    //(2)
    const peer = new Peer(peer_id, {
        key: key,
        debug: 3
    });

    peer.on('open', function () {
        // (4)
        let peers = peer.listAllPeers(peers => {
            console.log(peers);
        });
    });

    peer.on('error', function (err) {
        alert(err.message);
    });
};
```

API KEYとpeer_idをGET Paramterから取得します。(1), (1')
尚、API KEYをGET Parameterから取得するのは演習の都合であり、推奨するわけではありません。
peer_idは公開情報のためどのような方法でも構いません。

SkyWayサーバへ接続します。これはruby側の手順のPOST /peerに相当します(2)

SkyWayサーバへの接続に成功したことを確認します。これはruby側の手順のGET /peer/{peer_id}/eventに相当します(3)

### 動作確認

#### 起動処理

WebRTC Gatewayを実行します

```shell
$ ./gateway_linux_arm
```

次にrubyのコントローラーを実行します。

```shell
$ export API_KEY=YOUR_API_KEY
$ ruby webrtc_control.rb ruby
```

YOUR_API_KEYには取得したSkyWayのAPI Keyを入れて下さい。
この環境変数が設定されていない場合Peerオブジェクト作成の際に以下のようなエラーが表示されます。

```shell
$ ruby webrtc_control.rb ruby
#<Net::HTTPBadRequest 400 Bad Request readbody=true>
```

これでWebRTC GW側の起動処理は終わりです。次にブラウザ側を実行します。
何らかの方法でHTTPサーバを起動して下さい。

```shell
$ cd JavaScriptのあるディレクトリ
$ python -m SimpleHTTPServer 9000
```

ここにブラウザから接続します。今回API Keyはgetのパラメータで入れる形で作っているので、URLのYOUR_API_KEY部分に入れて下さい。
http://localhost:9000/index.html?key=YOUR_API_KEY&peer_id=hoge

peer.listAllPeersメソッドを実行してSkyWayサーバが認識しているpeer_idの一覧を取得します。ruby側、JavaScript側で入力したものが含まれているか、consoleを開いて確認して下さい。
(Webページ右クリック=>Inspectボタン、もしくはControl+Shift+I同時押し)

![6e02820b-4a2f-beb5-abba-b8c95cbe54de.png](https://qiita-image-store.s3.amazonaws.com/0/104451/0839a9ff-a052-4821-e1ea-84a4b30977d3.png)

### WebRTC Gateway側-開放処理

ruby側のスクリプトが終了した後、再度同じpeer_idで起動してみましょう。

```shell
$ ruby webrtc_control.rb ruby
$ ruby webrtc_control.rb ruby
#<Net::HTTPForbidden 403 Forbidden readbody=true>
```

403 Errorが出てしまいます。
これは前回のpeer_idがきちんと開放されず残ってしまっていて、同じpeer_idが使えないからです。
試しに別のpeer_idでやってみると動くことがわかります。

```shell
$ ruby webrtc_control.rb ruby2
```

こんなことを続けているとゴミが貯まる一方です。
先程のlistAllPeersを再度実行してみると、ゴミが残っているのが見えます。

![e906b615-5243-0c6f-4e79-724fe767ebc2.png](https://qiita-image-store.s3.amazonaws.com/0/104451/6fcbebef-3070-e48c-9fa4-ed7bf170cedb.png)



これでは実用上問題があるので、開放処理を行います。

[peer.rb](https://github.com/skyway-lab/skyway-webrtc-gw-handson/blob/master/chapter1/ruby/peer.rb#L52)の一部

```ruby
def close_peer(peer_id, peer_token)
  res = request(:delete, "/peers/#{peer_id}?token=#{peer_token}")
  if res.is_a?(Net::HTTPNoContent)
    # 正常動作の場合NoContentが帰る
  else
    # 異常動作の場合は終了する
    p res
    exit(1)
  end
  p res
end
```

DELETE /peers/{peer_id}を実行すればpeer objectが開放されます。
http://35.200.46.204/#/1.peers/peer_destroy

204 No Contentが帰ってくるのでそれを確認しています。

以下実行側です。

[webrtc_control.rb](https://github.com/skyway-lab/skyway-webrtc-gw-handson/blob/master/chapter1/ruby/webrtc_control.rb#L32)の一部

```ruby
  exit_flag = false
  while !exit_flag
    input = STDIN.readline().chomp!
    exit_flag = input == "exit"
  end

  close_peer(peer_id, peer_token)
```

コンソールでexitと打ち込んだときに終了するようにします。

これにてPeer Objectの生成と開放手順の完了です。
[次章](https://qiita.com/nakakura/items/e64e5aaa9ffed7b89aeb)ではカメラ映像の転送を行います。

## 追記: このプログラムを実行してPOST /peersで403 Forbiddenと表示される場合

POST /peers実行時にWebRTC Gateway内で、与えたPeer IDを保持するPeer Objectが生成されます。
SkyWayの仕様上、Peer IDはAPI_KEYに対して一意でなければならないため、同じPeer IDのPeer Objectを複数生成しようとすると403 Forbiddenが返されます。
これを避けるためには、必要がなくなったタイミングでDELETE　/peers/{peer_id}を呼び出してPeer Objectを開放する必要があります。

このサンプルプログラムがすべて正常終了しclose_peerメソッドまで処理が回った場合は開放処理が行われるため問題は生じないのですが、何らかの理由で途中で強制終了させた場合はこの問題に直面します。
その場合は別のPeer IDを利用していただくか、WebRTC Gatewayを再起動する必要があります。