Vue.jsでWebRTCによるビデオ通話機能を作ってみた

[グレンジ Advent Calendar 2019](https://qiita.com/advent-calendar/2019/grenge) 6日目担当の raitome です。
グレンジでクライアントエンジニアをやっています。
最近はちょっと違う技術を触ってみたくて、Vue.jsを使って、ビデオ通話機能を作ってみました。
ネットの資料が多いですが、古いものも多くて、すぐ使えるコードが少ないので、
メモのためでもあり、必要なものを簡単にまとめました。

##WebRTCについて
詳細は下記でご覧いただけます。
https://ja.wikipedia.org/wiki/WebRTC
https://qiita.com/yusuke84/items/286f569d110daede721e

##使ったライブラリ
とりあえず手軽にビデオ通話機能を作りたくて、下記のライブラリを使いました。
・simple-peer:  https://github.com/feross/simple-peer
　　WebRTCベースで、p2pのビデオ通話を簡単に実装できるライブラリ。ファイルの転送などもできるらしい

・ simple-signal-clientとsimple-signal-server:  https://github.com/t-mullen/simple-signal
　　WebRTCでPeer to Peer通信を行う場合、シグナリングサーバと呼ばれる仲介サーバを用意する必要がありますので、simple-peerのためのシグナルサーバー用ライブラリsimple-signalを使いました。simple-signal-clientはクライアント側で、simple-signal-serverはサーバー側で使います。

他に、シグナルサーバーはSocket通信を使うため、socket.ioとクライアントとサーバー側のライブラリも使います。

（Skyway: https://webrtc.ecl.ntt.com/ というサードパーティのものを使えばもっと早く構築できるらしいが、有料らしいので、今回は自前で色々作ってみました）

##クライアント側の準備
今回は必要な処理だけ紹介します。完成版のコードを割愛します。

###1.必要なもののimport

```sample_client.js
    import SimplePeer from 'simple-peer'
    import SimpleSignalClient from 'simple-signal-client'
    import io from 'socket.io-client'
```

###2.Peerの作成

```sample_client.js
    const socket = io("シグナルサーバーのurl" , {secure: true})
    let signalClient = new SimpleSignalClient(socket)

    // 下記の方法でチャットルームの感じで作れる
    signalClient.discover("チャットルームID的な何か") // これを使ってユーザーリストとかを取得できる
    signalClient.on('discover', receiveUserList) // ユーザーリスト処理のfunctionをここに入れる
```
###2.Peerへの接続リクエスト処理
ビデオ通話したい相手のソケットIDが分かれば、下記のfunctionを使えばシグナルサーバーを経由して接続できます。

```sample_client.js
    const { peer } = await signalClient.connect("自分のソケットID",  "チャットルームID的な何か"})
```

###3.Peerの受信処理

```sample_client.js
    signalClient.on('request', async (request) => {
        const { peer } = await request.accept()  // 接続リクエストを承認

        peer.on('connect', () => {
            peer.send("connected")
        })
        peer.on('stream', (remoteStream) => {
              // 向こうから受け取ったPeerのストリームを画面に表示させる
            const videoElement = document.createElement('video')
            videoElement.autoplay = true
            videoElement.srcObject = remoteStream
            container.appendChild(videoElement) // 事前に用意したコンテナに新しく作ったElementを追加する
        })
        peer.on('close', () => {
             // 停止時の必要処理
        })
        // 自分のカメラの映像も向こうに送信するので、ストリームを取得して画面に追加する。下記の関数はあとで説明します
        addLocalCameraStream()
    })
```
###4. カメラの扱い
下記の処理で、ブラウザがカメラの使用権限を取得して、カメラのストームを扱います。
ブラウザによって挙動が少し違うので、最新版のChromeかFireFoxを使うことをオススメします。
一つ注意点としては、Chromeの場合は、セキュリティの関係で、サーバー側はSSL対応をしないと、下記のメソッドはうまく動きません。

```sample_client.js
    addLocalCameraStream() {
        var mediaDevices = navigator.mediaDevices || ((navigator.mozGetUserMedia || navigator.webkitGetUserMedia || navigator.msGetUserMedia) ? {
            getUserMedia(c) {
                return new Promise(((y, n) => {
                    (navigator.mozGetUserMedia || navigator.webkitGetUserMedia).call(navigator, c, y, n);
                }));
            }
        } : null)

        mediaDevices.getUserMedia({ audio: true, video: true }).then((localStream) => {
            peer.currentPeer.addStream(localStream) // これで接続した相手にストリームを送る
            // ここでストリームを画面に表示させる処理を追加してもいい
            }).catch(err => {
        });
    }
```
ちなみに、mediaDevices.getDisplayMediaを使えば、画面共有機能も作れます。

上記のコードを使えば、クライアント側の最低限の機能が作れます。

##シグナルサーバーの準備
シグナルサーバーのコードは割とシンプルになります。
主に必要に応じてユーザーの管理処理をカスタマイズします。

```index.js
    const io = require('socket.io')(server)
    var SimpleSignalServer = require('simple-signal-server')
    var signal = new SimpleSignalServer(io)

    signal.on('request', (request) => {
        request.forward() // リクエストをそのまま進ませる
    })

    signal.on('discover', (request) => {
        // ここでユーザーのソケットIDやルームIDを管理して、request.discoverを使って、全てのpeerの必要な情報を送る
    })

    signal.on('disconnect', (socket) => {
        // 接続切れる時の処理
    })
```

##最後に
以上でコードを使えば、必要最低限のビデオ通話機能が作れます。少し調整すれば、画面共有機能も作れます。実際にテストしてみたところ、P2Pの関係でもありますが、割とスムーズなビデオ通話ができました。

また、何か不明なところがありましたら、[simple-signal](https://github.com/t-mullen/simple-signal/tree/master/examples)のサンプルコードも一緒にご覧いただければ。