【新規事業】SkyWayで1：1通信を作ってみたよ【プロト開発】

# はじめに
株式会社マイスター・ギルド新規事業部のウサギーです。
弊社新規事業部では、新規サービスの立ち上げを目指して
日々、アイディアの検証やプロトタイプの作成を行っています！

今回は、考えていたサービス案についてプロトタイプを作成するため、
オンライン通信の機能を用意したくて、SkyWayの1：1通信を試してみました。

# なぜSkyWay？
今回の目的は、新規事業部で使う実験用のプロトタイプです。
製品そのものを作るわけではないので

**○ とにかく早く**
**○ カスタマイズ性は高く**

むしろ
**△（保守性や拡張性を気にして）実験開始が遅れる**

そのため
**○ ベストやベターな技術選定でなくてもよい**

と考えています。
極論を言うとWebアプリでなくてもよかったのですが、
弊社としてはWebアプリにするのが一番早いと思われたのでWebアプリを採用しました。

そうした事情から社内で調査実績がある**SkyWay**をチョイス。
以前の調査記事はこちら。弊社エンジニア619さんの執筆です。

https://qiita.com/Meister619/items/a480bf913d7dfd50ac73

# SkyWayとは

https://webrtc.ecl.ntt.com/

NTTコミュニケーションズが提供する、ビデオ・音声通話を簡単に実装できるSDKとAPIです。
マルチプラットフォームに対応しており、JavaScript/iOS/Android向けのSDKと
ブラウザ外からも利用できるWebRTCエンジン等が提供されています。
WebRTC（Web Real-Time Communication）はウェブブラウザやモバイルアプリでリアルタイム通信を提供するAPI（と、そのプロジェクト）のことのようです。WebRTC？なにそれ？な知識レベルですが問題なくSkyWayが使えました。

SkyWayには２つの通信モデルが用意されています。
１．電話モデル
２．ルームモデル

ルームモデルはビデオ会議のようなモデルになります。
今回はブラウザ間での１：１、つまり、電話モデル使いたいと思います。

# チュートリアル
さすが日本企業、SkyWayのエンジニア向けドキュメントはとても親切です。
チュートリアルの通り進めれば問題なくサンプルを動かすことができると思います。
[チュートリアル](https://webrtc.ecl.ntt.com/documents/javascript-sdk.html#%E3%83%81%E3%83%A5%E3%83%BC%E3%83%88%E3%83%AA%E3%82%A2%E3%83%AB)

以下、チュートリアルを実施後に個人的にまとめた記録となります。

# アカウントとAPIキーの作成
[SkyWayのWebページ](https://webrtc.ecl.ntt.com/)からCommunity Edition（無料版）に登録します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/913784/fc1decd7-1eaf-f35d-4b2b-83cc9cd135ea.png)
「無料で始める」から登録画面に入れます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/913784/017ae5aa-f666-dd2e-d218-0e0ba2506cf9.png)


アカウントが作成できたら、API Keyを作成します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/913784/23827a7c-bb4b-85f1-9f46-7af72e2b13bf.png)
「新しくアプリケーションを追加する」からアプリケーション作成画面に入ります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/913784/7d299819-83c7-e74b-636a-62c7a5b10a35.png)
「アプリケーション説明文」と「利用可能ドメイン名」を入力します。
ローカル環境での開発用なのでlocalhostを入力しました。値はあとからも変更できます。
Webサーバーにアップロードするときは、サーバーのドメインもここに追加します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/913784/e2b82415-2a59-1564-ca88-2a0ad028d3d3.png)
とりあえず設定はデフォルトのままで「アプリケーションを作成する」をクリック。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/913784/e3124582-b1f6-430b-30db-987123d991f2.png)
※ステータスが利用中になっていないと通信できないので注意。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/913784/8de6f8b2-e93b-6de1-bce6-8fdbf25933e7.png)
ステータスが停止中のときは「利用再開」を押せばOK。

# ローカルWebサーバーの準備
docker環境下でnginxを使用しています。
私がポヤポヤしてる間に[新規事業部のヒツジー](https://qiita.com/Shinkijigyo_no_Hitsuji)さんが用意してくれたのでそのまま使っています。
たぶん私１人でやっていたらVisual Studio Codeの拡張機能Live Serverを使ってたと思います。

# HTML
#### HTMLファイルにSDKをインポート
```HTML:index.html
　　　：
    <script src="//cdn.webrtc.ecl.ntt.com/skyway-latest.js"></script>
    <script src="./js/main.js"></script>
</body>
</html>
```
#### video要素を用意
自分のカメラ映像を表示するvideo要素
```HTML:index.html
<video id="my-video" width="384px" height="216px" autoplay muted playsinline></video>
```
相手のカメラ映像を表示するvideo要素も
```HTML:index.html
<video id="their-video"  width="384px" height="216px" autoplay muted playsinline></video>
```
#### 自分のPeerIDを表示する要素を用意
```HTML:index.html
<p class="my-id-label">自分のPeerID: <span id="my-id"></span></p>
```
#### 通信相手のPeerIDを入力するフォームと発信ボタンを用意
```HTML:index.html
<p>
<label class="call-id-form-label" for="their-id">相手のPeerID: </label>
<input id="their-id" class="call-id-form">
<button type="button" id="call-btn">発信</button>
</p>
```

# JavaScript
#### カメラ映像と音声を取得
```js:main.js
let localStream;
const myVideo = document.getElementById('my-video');
navigator.mediaDevices.getUserMedia({ video: true, audio: true })
    .then(stream => {
        myVideo.srcObject = stream;
        myVideo.play();
        localStream = stream;
    }).catch(error => {
        console.error('mediaDevice.getUserMedia() error:', error);
    });
```
#### Peerの作成
```js:main.js
const peer = new Peer({
    key: 'SkyWayのAPI Keyを入れてね',
    debug: 3
});
```
**Peer**オブジェクトは接続を管理するしているオブジェクトです。
**Peer**オブジェクトを通じてシグナリングサーバーにアクセスしたり、通信相手と音声・映像を送受信します。
keyには作成したAPI Keyを入れてください。

>![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/913784/64ad1448-addf-f99f-f3ff-b1f8f398dc7b.png)
>[Peer：SkyWay](https://webrtc.ecl.ntt.com/documents/at-first.html#peer)より引用

debugはログ出力レベルで、「3」は「開発用に全てのログを出力」の意。

シグナリングサーバー？？？となったので調べてみましたが、「通信相手を探す」ためのサーバーで、
シグナリングサーバーを通じて通信相手の情報を取得できるようになっているとのことです。

>![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/913784/a3ad8261-3558-e92a-c2b0-d9c29c7ba5fe.png)
>[シグナリングサーバー：SkyWay](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)より引用



#### 自分のPeerIDを取得して画面に表示
```js:main.js
peer.on('open', () => {
    document.getElementById('my-id').textContent = peer.id;
});
```
PeerIDは**Peer**オブジェクトの’**open**’イベント発生時に取得。
’**open**’イベントはSkyWayのシグナリングサーバとの接続が成功したタイミングで発生するイベントとのこと。

#### 発信処理
相手のPeerIDを入力し「発信」ボタンを押したときの動作を用意。
```js:main.js
document.getElementById('call-btn').onclick = () => {
    const theirID = document.getElementById('their-id').value;
    const mediaConnection = peer.call(theirID, localStream);
    setEventListener(mediaConnection);
};
```
発信処理は**Peer**オブジェクトの**call**メソッドを使います。
引数は２つ
+ 通信相手のPeerID
+ 自分のカメラ映像(localStream)

**call**メソッドは、相手に接続したときに**MediaConnection**オブジェクトを返します。
そのため、**MediaConnection**を待ち受けるイベントリスナーを用意します。

#### 相手のカメラ映像を表示（MediaConnectionを待ち受けるイベントリスナー)
```js:main.js
const theirVideo = document.getElementById('their-video');
const setEventListener = mediaConnection => {
    mediaConnection.on('stream', stream => {
        theirVideo.srcObject = stream;
        theirVideo.play();
    });
}
```
相手の映像を取得するには**MediaConnection**オブジェクトの’**stream**’イベントを使います。
’**stream**’イベントは相手の映像を取得したときに発生するため、
**MediaConnection**を待ち受けるイベントリスナー内で’**stream**’イベント発生時の処理を用意しておきます。

#### 着信処理
着信時の処理も用意しておきます。
相手から接続要求が来たタイミングで**Peer**オブジェクトの**call**イベントが発生するので、この**call**イベントを使用します。
```js:main.js
peer.on('call', mediaConnection => {
    mediaConnection.answer(localStream);
    setEventListener(mediaConnection);
});
```
**call**イベントの発生時に**MediaConnection**オブジェクトが取得できます。
**MediaConnection**オブジェクトの**answer**メソッドで自分のカメラ映像を相手に渡せます。

相手の映像は発信時と同様に**MediaConnection**オブジェクトの’**stream**’イベントを使います。

# 実行
#### 別タブで開きます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/913784/6bc39ba5-d895-b913-1642-1276e7124426.png)

#### 一方のPeerIDをコピペして発信を押す
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/913784/d7a55196-3b99-6cc7-74da-6f168529ca61.png)

#### 接続できました！
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/913784/3dc422ac-2119-afe3-6103-ea2856bd71aa.png)


↓video要素の”muted”をとれば音声をONにできますが、
ハウリングするので、ミュートでテストするのがおすすめです。
```HTML
<video autoplay muted playsinline></video>
```

# コード全体
```HTML:index.html
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>1対1ビデオ通話</title>
    <style type="text/css">
        .container{
            width: 900px;
        }
        .main-wrapper{
            height: 600px;
        }
        .my-section{
            float: left;
            width: 50%;
            height: 100%;
        }
        .their-section{
            float: right;
            width: 50%;
            height: 100%;
        }
    </style>
</head>
<body>
    <h1>1対1ビデオ通話のサンプル</h1>
    <div class="container">
        <div class="my-section">
            <p class="my-id-label">自分のPeerID: <span id="my-id"></span></p>
            <video id="my-video" width="384px" height="216px" autoplay muted playsinline></video>
        </div>
        <div class="their-section">
            <p>
                <label class="call-id-form-label" for="their-id">相手のPeerID: </label>
                <input id="their-id" class="call-id-form">
                <button type="button" id="call-btn">発信</button>
            </p>
            <video id="their-video"  width="384px" height="216px" autoplay muted playsinline></video>    
        </div>
    </div>
    <script src="//cdn.webrtc.ecl.ntt.com/skyway-latest.js"></script>
    <script src="./main.js"></script>
</body>
</html>
```
```js:main.js
window.__SKYWAY_KEY__ = 'SkyWayのAPI Keyを入れてね';

let localStream;
const myVideo = document.getElementById('my-video');
const theirVideo = document.getElementById('their-video');

navigator.mediaDevices.getUserMedia({ video: true, audio: true })
    .then(stream => {
        myVideo.srcObject = stream;
        myVideo.play();
        localStream = stream;
    }).catch(error => {
        console.error('mediaDevice.getUserMedia() error:', error);
});

const peer = new Peer({
    key: window.__SKYWAY_KEY__,
    debug: 3
});

peer.on('open', () => {
    document.getElementById('my-id').textContent = peer.id;
});

document.getElementById('call-btn').onclick = () => {
    const theirID = document.getElementById('their-id').value;
    const mediaConnection = peer.call(theirID, localStream);
    setEventListener(mediaConnection);
};

const setEventListener = mediaConnection => {
    mediaConnection.on('stream', stream => {
        theirVideo.srcObject = stream;
        theirVideo.play();
    });
}

peer.on('call', mediaConnection => {
    mediaConnection.answer(localStream);
    setEventListener(mediaConnection);
});
```


# おわりに
WebRTC？なにそれ？状態からでも1：1通信のアプリを作ることが出来ました。
これをベースに「実験用のプロトタイプ」を作成し、色々実験していきたいと思います。
お手元で試してみたい方は、上記のコードをコピペしてみてくださいねー