ブラウザでZoomのようなアプリが作成できるWebRTCとは？

# 初めに
これはWebRTCの概要やWebRTCを使用したサービスであるSkyWayを見ていき、WebRTCについての理解を深めることを目的とした記事です。

# WebRTCとは？
WebRTCについてはこちらの記事を読んでいただければそれの仕組みや特徴が分かると思います。

https://developer.mozilla.org/ja/docs/Web/API/WebRTC_API

上記の記事をざっくりまとめると、WebRTC（Web Real-Time Communications、ウェブリアルタイムコミュニケーション）とはウェブアプリケーションなどにてブラウザ間で直接ユーザー同士のリアルタイムの音声や動画をやり取りすることのできる技術です。また、WebRTCの基本的な概念は、`MediaStream`オブジェクト（あとで詳しく取り上げる）を利用してユーザーたちの音声・画像などを取得し、 `RTCPeerConnection`インターフェースにより接続されたユーザー同士でそれらをやり取りする、というものです。

#SkyWayとは？
SkyWayとはWebRTCの仕組みを利用したサービスで、APIとして利用できます。SkyWayの基本的な仕組みは、ユニークなIDを所有するユーザー同士が、`Peer`というユーザーたちとSkyWayが提供するサーバ（シグナリングサーバ、WebRTCの利用に必要な情報のやり取りを行う）との接続やユーザー同士の接続を管理するエージェントを通して、`MediaStream`オブジェクトの情報をやり取りするものとなっています。
詳しくはこちらの記事に記載されています。

https://webrtc.ecl.ntt.com/documents/at-first.html#peer

もしSkyWayで一対一のユーザー同士をする場合はこんな感じになります。
![スライド1.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/478883/1a157f8a-69ea-90f4-db19-5ca94457c413.jpeg)


SkyWayの特徴は以下の点です。

- WebRTCの実装部分をコーディングする必要がないため、WebRTCについて知識がなくてもWebRTCを利用することができる。
- 多人数通話や多人数に対する映像配信ができる。
- 通常であればWebRTCが利用できない状況（UDP通信が利用できないなど）でもSkyWayならWebRTCを利用することができる。
- IOSやAndroidなどのブラウザ以外からの実行環境でもWebRTCの利用ができる。

**参考資料**

https://webrtc.ecl.ntt.com/skyway/overview.html

https://support.skyway.io/hc/ja/articles/360048404634

他のサービスにはない様々な特徴がSkyWayにはあるので、詳しくは[SkyWayのホームページ](https://webrtc.ecl.ntt.com/skyway/function.html)をご覧ください。

#SkyWayチュートリアルからSkyWayの仕組みを見てみる
最後にSkyWayが提供しているチュートリアルからSkyWayの仕組みについてみていこうと思います。ko
のチュートリアル自体はhtmlとjavascriptのみから構成されていて、GoogleChrome、Firefox、MicrosftEdgeなどのブラウザーから実行できるのでぜひやってみてください。（今回見ていくチュートリアルのコードは見やすくするために表記が実際のものと異なる部分があります。）
ちなみにチュートリアルはこちらから閲覧できます。

https://webrtc.ecl.ntt.com/documents/javascript-sdk.html

##SkyWayチュートリアルの処理の大まかな流れ
SkyWayチュートリアルの処理を図で表してみました。次の章でこの処理について詳しく見ていくので、ぜひそちらの方もご覧ください。
![スライド2.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/478883/bcab93d7-85c7-f431-6646-a690fff61188.jpeg)


##表示部分
表示部分であるhtmlのコードはこちら。

```html
<html lang="ja">                                                                                                                                    <head>
                <meta charset="UTF-8">
                <meta name="viewport" content="width=device-width, initial-scale=1.0">
                <title>WebRTC Test</title>
                <script src="https://cdn.webrtc.ecl.ntt.com/skyway-4.4.1.js"></script>
        </head>
        <body>
                <video id="my-video" width="400px" autoplay muted playsinline></video>
                <p id="my-id"></p>
                <textarea id="their-id"></textarea>
                <button id="make-call">発信</button>
                <video id="their-video" width="400px" autoplay playsinline></video>
                <script src="./rtc.js"></script>
        </body>
</html>
```
`<script src="https://cdn.webrtc.ecl.ntt.com/skyway-4.4.1.js">`によりSkyWayサービスを利用します。`<script src="./rtc.js"></script>`で表記されている`rtc.js`はこのhtmlファイルと同じ階層にあるSkyWayを使用したWebRTCの実装を担うファイルで、表記部分と実装部分を分けたいがために勝手に作りました。なのでこの部分に直接WebRTCの実装プログラムを書き込んでも動きます。

##実装部分
実装部分のrtc.jsの中身はこちらです。

```js
//音声、カメラの取得
let localStream;

navigator.mediaDevices.getUserMedia({video: true, audio: true})
.then( stream => {
        const videoElm = document.getElementById('my-video');
        videoElm.srcObject = stream;
        videoElm.play();

        localStream = stream;
}).catch( error => {
        console.error('mediaDevice.getUserMedia() error:', error);
        return;
});

//Peer idの取得
const peer = new Peer({
        key: '各々Skywayで登録したAPIキー',
        debug: 2
});
peer.on('open', () => {
        document.getElementById('my-id').textContent = peer.id;
});

//発信処理
document.getElementById('make-call').onclick = () => {
        const theirId = document.getElementById('their-id').value;
        const mediaConnection = peer.call(theirId, localStream);
        setEventListener(mediaConnection);
};

setEventListener = mediaConnection => {
        mediaConnection.on('stream', stream => {
                const videoElm = document.getElementById('their-video');
                videoElm.srcObject = stream;
                videoElm.play();
        });
}

//着信処理
peer.on('call', mediaConnection => {
        mediaConnection.answer(localStream);
        setEventListener(mediaConnection);
});
```
 実装内容を部分ごとに見ていきます。

```js
//音声、カメラの取得
let localStream;

navigator.mediaDevices.getUserMedia({video: true, audio: true})
.then( stream => {
        const videoElm = document.getElementById('my-video');
        videoElm.srcObject = stream;
        videoElm.play();

        localStream = stream;
}).catch( error => {
        console.error('mediaDevice.getUserMedia() error:', error);
        return;
});
```
まずはユーザーの音声、カメラの取得について見ていきます。
`navigator`はあらかじめ定められたグローバルなプロパティです。このプロパティを呼び出すことにより、アプリのユーザーが使用するブラウザの状態や情報(ブラウザが使用するメディアの情報、そのブラウザが動いているデバイスの位置情報など)をアプリ側が取得できます。
**参考記事**

https://developer.mozilla.org/ja/docs/Web/API/Navigator

`navigator`が所有する読み取り専用プロパティである`mediaDevices`はブラウザのカメラやマイクなどのメディア入力装置へのアクセスを提供する`mediaDevice`オブジェクトを返します。
`mediaDevice`オブジェクトが返された後、このオブジェクトが有する`getUserMedia`メソッドが実行されます。このメソッドはブラウザのメディア入力の許可をユーザーに求め、最終的に`MediaStream`オブジェクトを返す`Promise`オブジェクトを生成し、ユーザーが許可した場合、`then`メソッドの引数(今回は`stream`)から`MediaStream`オブジェクトが利用できるようになります。`getUserMedia`メソッドの引数から`mediaStream`にて扱うメディア入力の種類（トラック）を制約する`MediaStreamConstraints`オブジェクトが作成され、もしユーザーの音声と映像が取得したい場合は、その引数は`{ audio: true, video: true }`となります(`audio`、`video`のどちらかは`true`として指定しなくてはErrorが発生し、正常に処理をこなせません)。`audio`、`video`の両オブジェクトの値を変えることで映像の解像度を指定するなどのさらなる制約を付け加えることもできます。
この部分で扱っている`MediaStream`オブジェクトはストリーム（リアルタイムの動画データなど）もデータを扱い、音声・映像などのストリームの要素はトラックという単位で管理されます。（`MediaStream.getTracks`メソッドなどで`MediaStream`オブジェクトのトラックを取得することでストリームの音声・映像などの制御ができます。）
**参考記事**

https://developer.mozilla.org/ja/docs/Web/API/Navigator/mediaDevices

https://developer.mozilla.org/ja/docs/Web/API/MediaDevices

https://developer.mozilla.org/ja/docs/Web/API/MediaDevices/getUserMedia

https://developer.mozilla.org/ja/docs/Web/API/MediaStreamConstraints

https://developer.mozilla.org/ja/docs/Web/API/MediaStream

https://developer.mozilla.org/en-US/docs/Web/API/MediaStream/getTracks

今度は`getUserMedia`メソッドの成功時に行われる`then`メソッドを見てみます。
まずはid="my-video"で指定されたvideo要素のオブジェクトを変数videoElmに代入し、そのオブジェクトが有する`srcObject`プロパティに`MediaStream`オブジェクトを代入します。`srcObject`プロパティはメディアが利用できる`HTMLElement`(`<video>`など)に含まれるプロパティであり、`HTMLElement`で利用するメディアソースを提供するオブジェクトが入ります。ここに入るオブジェクトの種類は、`MediaStream、MediaSource、Blob、File`がありますが、注釈を見ると2017年11月のブラウザは`MediaSrream`のみしかサポートしていないため、`srcObject`にオブジェクトを代入するときは`MediaStream`オブジェクトのみにした方がよさそうです。
変数`videoElm`が`play`メソッドを実行し、先ほど`srcObject`に代入されたメディアソースが再生されます。`play`メソッドは`HTMLElement`にあらかじめ含まれているメソッドであり、最終的に`Promise`を返すので、今回は実装してませんが`catch`メソッドを用いて失敗時の処理を実装することもできます。ちなみに`<video src="流したい動画">`に対して`play`メソッドを使用すると流したい動画を再生させることができます。なので言い換えると、このメソッドにより`HTMLMediaElement.src`プロパティにあるメディアソースも再生できます。
最後に`MediaStream`オブジェクトをグローバル変数である`localStream`に代入し、次の処理へ移ります。
もしこの処理の中で`NotAllowedError`または`NotFoundError`などが発生した場合は、`catch`メソッド以降の処理へ移り、失敗を告げるコンソール出力とともにすべての処理をここで終えることとなります。
**参考記事**

https://developer.mozilla.org/ja/docs/Web/API/HTMLMediaElement/srcObject

https://developer.mozilla.org/ja/docs/Web/API/HTMLMediaElement/play

https://developer.mozilla.org/ja/docs/Web/API/HTMLMediaElement/src

```js
//Peer idの取得
const peer = new Peer({
        key: '各々Skywayで登録したAPIキー',
        debug: 2
});
peer.on('open', () => {
        document.getElementById('my-id').textContent = peer.id;
});
```
次に通信を管理する`Peer`を使用し、通信に必要なPeer IDを取得する部分について見ていきます。これ以降の処理はSkyWayをCDNなどでアプリ上で利用できる状態でないと作動しません。
`Peer`クラスはSkyWayを使用するうえで必要不可欠なクラス。`new Peer(id, option)`または`new Peer(option)`の二つからこのクラスの新スタンスが取得可能で、今回は後者の方法で取得しています。`id`はユーザーがほかのユーザーと通信を行う際に必要なPeer IDというもので、指定しない場合は自動で作成されます。`optinon`はSkywayの接続時のオプションを指定するオブジェクトで、このオブジェクト内の`key`プロパティにアプリ製作者がSkyWayを利用するために作成したAPIキーを代入しないとWebRTCの通信ができません。`debug`は通信時にブラウザのコンソールに何を出力するのか決められるもので、今回は通信時の情報の全てを出力するように指定しています。
`Peer`インスタンスに含まれる`on`メソッドは、SkyWayが提供する全てのクラスが継承する`EventEmitter`が所持するメソッドで、第一引数のイベント（今回は`open`）の実行時に第二引数のクロージャが実行されます。`open`イベントとは`Peer`インスタンスの生成時にシグナリングサーバとの接続が鵜なくいった際に発生するイベントであり、今回は他のユーザーと通信する際に必要なユーザー自身のPeer IDをブラウザ上の決まった要素へ表示するようにしています。
ちなみにシグナリングサーバとは最初の方にも紹介したように、SkyWayが提供している、通信するユーザー同士のIPアドレスなどの通信に必要な情報をそのユーザー間で交換する機能を持つサーバです。
**参考記事**

https://webrtc.ecl.ntt.com/api-reference/javascript.html#eventemitter%E3%81%AB%E3%81%A4%E3%81%84%E3%81%A6

https://skyway.github.io/js-reference/Peer.html

https://webrtc.ecl.ntt.com/skyway/overview.html#_1-%E3%82%B7%E3%82%B0%E3%83%8A%E3%83%AA%E3%83%B3%E3%82%B0%E3%82%B5%E3%83%BC%E3%83%90

```js
//発信処理
document.getElementById('make-call').onclick = () => {
        const theirId = document.getElementById('their-id').value;
        const mediaConnection = peer.call(theirId, localStream);
        setEventListener(mediaConnection);
};

setEventListener = mediaConnection => {
        mediaConnection.on('stream', stream => {
                const videoElm = document.getElementById('their-video');
                videoElm.srcObject = stream;
                videoElm.play();
        });
}
```
次にほかのユーザーへ`MediaStream`オブジェクトを送信する際の発信処理について見ていきます。こちらは受信側のユーザーは行いません。
`id='make-call'`の要素をクリックした場合、以下の三つを実行します。

- `id="their-id"`の要素の値を変数`theirId`へ代入
- `Peer`インスタンスの`call`メソッドの結果を変数`mediaConnection`へ代入。`call`メソッドは接続を管理する`MediaConnection`インスタンスを返すメソッドであり、第一引数に接続先のユーザーが所有するPeer IDを、第二引数にブラウザのストリームの音声・映像の情報を有する`MediaStream`オブジェクトが入り、第三引数に発信時のオプションを入れることもできる。
- 独自に作成した`setEventListener`メソッドに変数`mediaConnection`を引数にとったものを実行する。（このメソッドは次に説明する）

`setEventListener`メソッドの処理は以下の通りです。
引数の`mediaConnection`オブジェクトの`on`メソッドにの第一引数に`stream`を指定して実行します。もし`stream`を指定した場合、`MediaStream`を受信したときに第二引数のクロージャを実行します。ここで使用するクロージャの引数`stream`にはあとで見ていく着信処理の中の`mediaConnection`オブジェクトの`answer`メソッドから送られてきた`MediaStream`オブジェクトが入ります。その後の処理は先ほど見ていったブラウザに自分のストリームを表示する処理と同じです。
**参考記事**

https://webrtc.ecl.ntt.com/api-reference/javascript.html#members

https://webrtc.ecl.ntt.com/api-reference/javascript.html#mediaconnection

https://webrtc.ecl.ntt.com/api-reference/javascript.html#methods-2

```js
//着信処理
peer.on('call', mediaConnection => {
        mediaConnection.answer(localStream);
        setEventListener(mediaConnection);
});
```
最後に他のユーザーから`MediaStream`の送信が来た際の着信処理について見ていきます。この処理は送信側は行いません。
`Peer`インスタンスが入っている`peer`オブジェクトの`on`メソッドは第一引数に`call`が指定された場合に、`Peer`インスタンスの`call`メソッドの実行時に第二引数の`MediaConnection`のインスタンスが代入された`mediaConnection`オブジェクトを引数にとったクロージャが実行されます。`mediaConnection`オブジェクトの`answer`メソッドはPeerIDを持ったほかのユーザーが自分のPeerIDへ接続があった場合に実行するもので、第一引数の`MediaStream`オブジェクト(今回は受信側のストリームデータを持つ`localStream`)を接続元のPeerIDを持つブラウザへ送信します。また、第二引数に応答時のオプションを指定することもできます。最後に送信元からの`MediaStrean`オブジェクトを含んだ`mediaConnection`オブジェクトを引数にとり、送信側と同様に`setEventListener`メソッドを実行し、送信元のストリームを受信側のブラウザにも映します。
**参考記事**

https://webrtc.ecl.ntt.com/api-reference/javascript.html#events

https://webrtc.ecl.ntt.com/api-reference/javascript.html#members-2


#終わりに
この記事を見ていただきありがとうございます。
Javascriptの他にもPHPの記事も書いているので、もしよければそちらもぜひ！

https://qiita.com/SanQ/items/a08f200614718315a63b

https://qiita.com/SanQ/items/d927f6e887b3f531dc6c

https://qiita.com/SanQ/items/e12083fce1de3f569df1