SkyWay API + Rails6 + Vue でビデオチャットアプリを作る

ビデオチャットアプリを作るってハードル高そうですよね？　
SkyWayAPIを使うとリアルタイムでの動画通信が簡単にできます。

#前提知識
ざっくり知っているだけで十分です。
・Websocket
・WebRTC
・RubyOnRails
・Vue.js
・heroku(heroku cliをダウンロードして、heroku loginした状態で始めます)

#前置き
Railsを使う必要性は、この記事のアプリだとありません。

ユーザーや部屋の管理とかをRDS経由で行うことを前提に実装してみました。

#目標物
ユーザー二人がcallIDを使ってビデオチャットをできるようにします。

①WEBアプリ側（Rails）
herokuへのデプロイを念頭に、プロジェクトを作成していきます。
herokuにアプリの初期状態をデプロイするところから始めます。

```console
rails new skyway_test --database=postgresql --webpack=vue
rails db:create
heroku create

git add .
git commit -m "first commit"
git push heroku master

#heroku側でデプロイしたアプリをブラウザーで確認
heroku open
```


![スクリーンショット 2019-12-15 16.07.14.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/224974/dfd36873-743e-3bf3-b813-95c0b9c9c36a.png)


コントローラーとルーティングを追加していきます。

```console
rails g controller rooms show
```


```routes.rb
Rails.application.routes.draw do
  get 'rooms/show'
  root 'rooms#show'
end
```

この時点で、こうなっていれば成功です。

![スクリーンショット 2019-12-15 16.16.03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/224974/290058b5-84fd-0627-22e6-fb609df2ce06.png)

②SkyWayのdeveloper登録
Community Editionが無料なので、このプランで登録していきます。
https://webrtc.ecl.ntt.com/signup.html

![スクリーンショット 2019-12-15 16.19.29.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/224974/193b696d-31c9-f3f9-d9dd-762492cf4800.png)

登録して、ダッシュボードに行ったらアプリケーションを作成に進んでください。
![screencapture-console-webrtc-free-ecl-ntt-add-2019-12-15-16_20_53.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/224974/385e7e49-6643-0a47-692c-31e6ff4d57a3.png)

ドメイン名の部分は、localhostとherokuで自動発行されたドメインを追加します。
それ以外は初期値のままで問題ありません。
それが終わるとアプリ詳細画面にAPIキーが表示されるので、それを控えておきます。

③クライアント側の実装（Vue.js）
ではクライアント側の実装です。

--webpack=vueオプションでrails newしてない方は、ここで下記のコマンドを実行しましょう。

```console
$./bin/rails webpacker:install:vue
```

```app/views/rooms/show.html.erb
#元あった中身を削除、以下を追加
<%= javascript_pack_tag 'hello_vue' %>
<%= stylesheet_pack_tag 'hello_vue' %>
```

これで自動生成されているhello_vue.jsをRails側に読み込んで、vue.jsで作ったコンポーネントをレンダリングしています。

こうなっていれば成功！
![スクリーンショット 2019-12-15 16.31.45.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/224974/fce82510-33aa-2595-efbc-cda47d8b9d62.png)

ではいよいよSkywayAPIを導入していきます。

hello_vueと同じ階層にjsファイルを作成します。

```app/javascript/packs/room.js
import Vue from 'vue/dist/vue.esm'
import Room from '../room.vue'

document.addEventListener('DOMContentLoaded', () => {
  const app = new Vue({
    el: '#room',
    data: {
    },
    components: { Room }
  })
})

```

これでroomというIDをもつDOMがVueの影響範囲内になりました。
viewにそのDOMを設置して、jsファイルを読み込むタグを編集します。

```app/views/rooms/show.html.erb
<div id="room">
 <room />
</div>

<%= javascript_pack_tag 'room' %>
<%= stylesheet_pack_tag 'room' %>
```

app.vueと同じ階層にroom.vueを作成します。
ここは@n0bisukeさんのqiitaの記事とgithubのリポジトリを参考にしました！
単一コンポーネントに書き換えています。

SkyWayのサンプルをVue.jsで書いていくチュートリアル vol1
https://qiita.com/n0bisuke/items/6e1f56678b2eb6318594

githubリポジトリ
https://gist.github.com/n0bisuke/88be07a6a16ee72b9bdf4fdcd12a522f

自分のAPIキーを入力するのを忘れずに！

```app/javascript/room.vue
<template>
    <div id="app">
        <video id="their-video" width="200" autoplay playsinline></video>
        <video id="my-video" muted="true" width="500" autoplay playsinline></video>
        <p>Your Peer ID: <span id="my-id">{{peerId}}</span></p>
        <input v-model="calltoid" placeholder="call id">
        <button @click="makeCall" class="button--green">Call</button>
        <br />

        マイク:
        <select v-model="selectedAudio" @change="onChange">
          <option disabled value="">Please select one</option>
          <option v-for="(audio, key, index) in audios" v-bind:key="index" :value="audio.value">
            {{ audio.text }}
          </option>
        </select>

        カメラ: 
        <select v-model="selectedVideo" @change="onChange">
          <option disabled value="">Please select one</option>
          <option v-for="(video, key, index) in videos" v-bind:key="index" :value="video.value">
            {{ video.text }}
          </option>
        </select>

    </div>
</template>

<script>
const API_KEY = "自分のAPIKEY"; 
// const Peer = require('../skyway-js');
console.log(Peer)
export default {
    data: function () {
        return {
            audios: [],
            videos: [],
            selectedAudio: '',
            selectedVideo: '',
            peerId: '',
            calltoid: '',
            localStream: {}
        }
    },
    methods: {
        onChange: function(){
            if(this.selectedAudio != '' && this.selectedVideo != ''){
                this.connectLocalCamera();
            }
        },

        connectLocalCamera: async function(){
            const constraints = {
                audio: this.selectedAudio ? { deviceId: { exact: this.selectedAudio } } : false,
                video: this.selectedVideo ? { deviceId: { exact: this.selectedVideo } } : false
            }
        
            const stream = await navigator.mediaDevices.getUserMedia(constraints);
            document.getElementById('my-video').srcObject = stream;
            this.localStream = stream;
        },

        makeCall: function(){
            const call = this.peer.call(this.calltoid, this.localStream);
            this.connect(call);
        },

        connect: function(call){
            call.on('stream', stream => {
                const el = document.getElementById('their-video');
                el.srcObject = stream;
                el.play();
            });
        }
    },

    created: async function(){
        console.log(API_KEY)
        this.peer = new Peer({key: API_KEY, debug: 3}); //新規にPeerオブジェクトの作成
        this.peer.on('open', () => this.peerId = this.peer.id); //PeerIDを反映
        this.peer.on('call', call => {
            call.answer(this.localStream);
            this.connect(call);
        });
      
        //デバイスへのアクセス
        const deviceInfos = await navigator.mediaDevices.enumerateDevices();

        //オーディオデバイスの情報を取得
        deviceInfos
        .filter(deviceInfo => deviceInfo.kind === 'audioinput')
        .map(audio => this.audios.push({text: audio.label || `Microphone ${this.audios.length + 1}`, value: audio.deviceId}));

        //カメラの情報を取得
        deviceInfos
        .filter(deviceInfo => deviceInfo.kind === 'videoinput')
        .map(video => this.videos.push({text: video.label || `Camera  ${this.videos.length - 1}`, value: video.deviceId}));
        
        console.log(this.audios, this.videos);        
    }
}
</script>

<style scoped>
    p {
    font-size: 2em;
    text-align: center;
    }
</style>


```

skyway javascript SDKをCDN経由で読み込みます。（yarnやnpmで導入できたら多分そっちの方が良い。）

```application.html.erb
<!DOCTYPE html>
<html>
  <head>
    <title>SkywayTest</title>
    <%= csrf_meta_tags %>
    <%= csp_meta_tag %>
    <script type="text/javascript" src="https://cdn.webrtc.ecl.ntt.com/skyway-latest.js"></script>
    <%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %>
    <%= javascript_pack_tag 'app![error]()
lication', 'data-turbolinks-track': 'reload' %>
  </head>

  <body>
    <%= yield %>
  </body>
</html>

```

カメラとマイクを選んだ時点で、ローカルの映像がvideoタグに反映されます。

![名称未設定.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/224974/35ad9525-6f5f-7fa2-5916-b9edc2606712.gif)

かけたい相手のIDを入力するとP２Pの通信が始まります!


最後にgit push heroku masterした結果がこれです。
あとで変わるかもしれません。
https://morning-meadow-17444.herokuapp.com/

#最後に
いかがでしたでしょうか？

ビデオチャットが簡単に作れるSkywayAPIすごいですね。
本来であれば、Turnサーバー・Stunサーバー立ててーごにょごにょしなきゃいけないと思いますが、そこの部分を全てやってくれます。

もともと複数ユーザーが同時に参加できるカンファレンス式のビデオチャットにする予定だったので、roomという言葉をよく使っています。次回できたら複数参加もできるように実装したいです。

roomよりもchatとかの方がしっくりくるかもしれません。