33
39

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Last updated at Posted at 2019-12-15

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

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

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

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

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

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

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

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

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

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

スクリーンショット 2019-12-15 16.19.29.png

登録して、ダッシュボードに行ったらアプリケーションを作成に進んでください。
screencapture-console-webrtc-free-ecl-ntt-add-2019-12-15-16_20_53.png

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

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

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

$./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

ではいよいよ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

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

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

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

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

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

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

33
39
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
33
39

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?