SkyWayによるビデオ・音声通話の技術概要


この記事は「[マイスター・ギルド：暑中見舞！夏のアドベントカレンダー2020](https://note.com/meisterguild/n/n09826f484617)」3日目の記事です。
# 初めに．．．
コロナの流行が始まったとき、「Stay Home」対策で自宅に閉じ込められたとき、ITワーカーの私たちは特に自宅からリモートで仕事をすることが可能でラッキーでした。
残念ながら、私たちのほとんどはリモートでの作業に慣れておらず、最初は上司、同僚、お客さん等とリモート通信が特に困難でした。
特殊なツールを使用しても改善されましたが、同じオフィスで作業するほど自然ではありません。
弊社の[Meister Guild](https://m-gild.com/)でもその新しい作業環境に答えるツールを探して色々なツールを使ってみた：

- [Zoom](https://zoom.us/)：ヴァーチャル背景を使える
- [Discord](https://discord.com/new)：完全に無料
- [Remo](https://remo.co/)：ヴァーチャルルームに入れる、共有ホワイトボードもある
- [Spatial Chat](https://spatial.chat/)：距離によると声の高さが変わる

ビデオ会議ができるツールはほかにもあります：

- [Microsoft Teams](https://www.microsoft.com/en/microsoft-365/microsoft-teams/group-chat-software)
- [Skype](https://www.skype.com/ja/)
- [Google Hangouts](https://hangouts.google.com/)
- ...

各ツールが得点と弱点を持つけど「これ！」ってなるツールがなかったので「私たちの理想なビデオ会議のツール作れるかな？」と思って調査することになりました。
# ビデオ会議のツール作りの調査
そのようなツールを一から開発するのはとても大変な仕事になるので、開発をスピードアップするWebRTCフレームワークを探しました。
日本製で無料プランあり、NTTコミュニケーションズが作成したWebRTCフレームワークを見つけました：![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/531993/44088093-0a4e-51b7-c47a-f8826b155d86.png)
ユーザー認証をテストするために、認証付きのLaravelアプリケーションを作成し、ユーザーのメールをbase64でエンコーディングしてPeerIDとして使用しました。

## SkyWayとは
ホームページによると[SkyWay](https://webrtc.ecl.ntt.com/)は：
>ビデオ・音声通話の機能をアプリケーションに簡単に実装できる、
マルチプラットフォームなSDK & フルマネージドなAPIサービスです。

無料プランで下記のSDKを使える：
- [Javascript SDK](https://webrtc.ecl.ntt.com/documents/javascript-sdk.html)
- [iOS SDK](https://webrtc.ecl.ntt.com/documents/ios-sdk.html)
- [Android SDK](https://webrtc.ecl.ntt.com/documents/android-sdk.html)
- [WebRTC Gateway](https://webrtc.ecl.ntt.com/documents/webrtc-gateway.html)
- [APIキー認証](https://github.com/skyway/skyway-peer-authentication-samples/blob/master/README.jp.md)

有料プランで[録音SDK](https://webrtc.ecl.ntt.com/documents/recorder-sdk.html)と[管理API](https://webrtc.ecl.ntt.com/documents/ecl-api.html)も使えるんですが例えば録音も録画も普通の[Media Capture and Streams API (Media Streams)](https://developer.mozilla.org/ja/docs/Web/API/Media_Streams_API)でできる。
## Javascriptサンプル：
### ビデオ会議
[SkyWay Room example](https://example.webrtc.ecl.ntt.com/room/index.html)
### P2Pビデオ通話
[SkyWay P2P Media example](https://example.webrtc.ecl.ntt.com/p2p-media/index.html)
### P2Pテキスト通話
[SkyWay P2P Data example](https://example.webrtc.ecl.ntt.com/p2p-data/index.html)
### 録音と録画
[WebRTC samples MediaRecorder](https://webrtc.github.io/samples/src/content/getusermedia/record/)

## P2Pビデオ通話
通話の相手は一人です。
### 基本
CDNからSDKをインポートする：

```html:headタグ内
  <script src="https://cdn.webrtc.ecl.ntt.com/skyway-latest.js"></script>
```
カメラ映像を表示するvideo要素を追加する：

```html:bodyタグ内
  <video id="my-video" width="400px" autoplay muted playsinline></video>
```
カメラ映像・マイク音声を取得する：

```javascript:bodyタグ下部のscriptタグ内
  let localStream;

  // カメラ映像取得
  navigator.mediaDevices.getUserMedia({video: true, audio: true})
    .then( stream => {
    // 成功時にvideo要素にカメラ映像をセットし、再生
    const videoElm = document.getElementById('my-video')
    videoElm.srcObject = stream;
    videoElm.play();
    // 着信時に相手にカメラ映像を返せるように、グローバル変数に保存しておく
    localStream = stream;
  }).catch( error => {
    // 失敗時にはエラーログを出力
    console.error('mediaDevice.getUserMedia() error:', error);
    return;
  });
```
通話の相手のは「peer」と呼ばれてる。
通話出来るように自分のPeerオブジェクトを作成して相手のPeerオブジェクトと繋がる。
### Peerオブジェクトの作成
Peerオブジェクトを作成するときに引数のIDを渡さない場合はランダムなIDが生成される：

```javascript:scriptタグ内
        const peer = new Peer({
            key: '<SkyWayのAPIキー>',
            debug: 3
        });
```
PeerオブジェクトのIDは`peer.id`で取得できる。

またはメールアドレスなどからIDを生成できる。
例えばLaravelのコントローラーで`base64`にエンコード：

```php:app/Http/Controllers/<通話>Controller.php
    public function index()
    {
        $user = Auth::user();
        return view('videochat',['user'=>['email'=>rtrim(base64_encode($user->email),"=")]]);
    }
```
ページでPeerオブジェクトに渡す：

```javascript:scriptタグ内
            const peer = new Peer('{{$user['email']}}',{
                key: '<SkyWayのAPIキー>',
                debug: 3
            });
```
### 発信
相手のカメラ映像を表示するvideo要素を追加する：

```html:bodyタグ内
  <video id="their-video" width="400px" autoplay muted playsinline></video>
```
相手へ発信してリスナーで接続することを待つ：

```javascript:scriptタグ内
// 発信処理
const mediaConnection = peer.call('<相手のPeerID>', localStream);
setEventListener(mediaConnection);
```
接続ができたときにビデオ要素を設定する：

```javascript:scriptタグ内
let remoteStream;
// イベントリスナを設置する関数
const setEventListener = mediaConnection => {
  mediaConnection.on('stream', stream => {
    // video要素にカメラ映像をセットして再生
    const videoElm = document.getElementById('their-video')
    videoElm.srcObject = stream;
    remoteStream = stream;
    videoElm.play();
  });
}
```
### 着信
相手側はPeerオブジェクトのcallイベントを待って着信の時ビデオ要素を設定する：

```javascript:scriptタグ内
//着信処理
peer.on('call', mediaConnection => {
  mediaConnection.answer(localStream);
  setEventListener(mediaConnection);
});
```
### 映像・音声はオン・オフ等

##### マイク音声オフ
ミュートする：

```javascript:scriptタグ内
    localStream.getAudioTracks().forEach(track => track.enabled = false);
```
##### 音声オフ
相手の音声を削音する：

```javascript:scriptタグ内
    remoteStream.getAudioTracks().forEach(track => track.enabled = false);
```
※　音全部消したいときマイク音声もオフしなければならない。
##### カメラ映像オフ
カメラの映像を消す：

```javascript:scriptタグ内
    localStream.getVideoTracks().forEach(track => track.enabled = false);
```
##### 反響キャンセリング
 反響を消す：

```javascript:scriptタグ内
    localStream.getAudioTracks().forEach(track => {
        let constraints = track.getConstraints();
        constraints.echoCancellation = true;
        track.applyConstraints(constraints);
    });
```
## ビデオ会議
ビデオ会議はビデオ通話との違いは2つ：
- 相手の数は一人以上になる
- roomオブジェクトで他のユーザーの存在（presence）が確認できる
### 基本
CDNからSDKをインポートする：

```html:headタグ内
  <script src="https://cdn.webrtc.ecl.ntt.com/skyway-latest.js"></script>
```
カメラ映像を表示するvideo要素を追加する：

```html:bodyタグ内
  <video id="js-local-stream"></video>
```
カメラ映像・マイク音声を取得する：

```javascript:bodyタグ下部のscriptタグ内
  // カメラ映像取得
const localStream = await navigator.mediaDevices
    .getUserMedia({
      audio: true,
      video: true,
    })
    .catch(console.error);

  // Render local stream
  const localVideo = document.getElementById('js-local-stream');
  localVideo.muted = true;
  localVideo.srcObject = localStream;
  localVideo.playsInline = true;
  await localVideo.play().catch(console.error);
```
相手達のカメラ映像を表示する要素を追加する：

```html:bodyタグ内
    <div class="remote-streams" id="js-remote-streams"></div>
```
### Peerオブジェクトの作成
PeerオブジェクトのIDを生成する。
例えばLaravelのコントローラーで`base64`にエンコード：

```php:app/Http/Controllers/<通話>Controller.php
    public function index()
    {
        $user = Auth::user();
        return view('videochat',['user'=>['email'=>rtrim(base64_encode($user->email),"=")]]);
    }
```
ページでPeerオブジェクトに渡す：

```javascript:scriptタグ内
            const peer = new Peer('{{$user['email']}}',{
                key: '<SkyWayのAPIキー>',
                debug: 3
            });
```
### roomオブジェクト
Peerオブジェクトが生成された後でroomに参加する：

```javascript:scriptタグ内
  peer.on('open', () => {
    const room = peer.joinRoom('test', {
      mode: 'sfu',
      stream: localStream,
    });
  });
```
※　roomは2つのタイプがある：`'sfu'`（通信がサーバーを通す）と`'mesh'`（通信が直接にPeerへ発信する）。
### roomのイベント
##### open
roomに入ったとき：

```javascript:scriptタグ内
    room.once('open', () => {
      ...
    });
```
##### close
roomを出たとき：

```javascript:scriptタグ内
    room.once('close', () => {
      // テキスト通信を止める
      sendTrigger.removeEventListener('click', onClickSend);
      // 相手達のビデオストリームを止める
      Array.from(remoteVideos.children).forEach(remoteVideo => {
        remoteVideo.srcObject.getTracks().forEach(track => track.stop());
        remoteVideo.srcObject = null;
        remoteVideo.remove();
      });
    });
```
##### peerJoin
一人がroomに入ったとき：

```javascript:scriptタグ内
    room.on('peerJoin', peerId => {
      ...
    });
```
##### peerLeave
一人がroomを出たとき：

```javascript:scriptタグ内
    room.on('peerLeave', peerId => {
      // ストリームを閉じてvideo要素を消す
      const remoteVideo = remoteVideos.querySelector(
        `[data-peer-id=${peerId}]`
      );
      remoteVideo.srcObject.getTracks().forEach(track => track.stop());
      remoteVideo.srcObject = null;
      remoteVideo.remove();
    });
```
##### stream
roomに入った一人のストリームを表示：

```javascript:scriptタグ内
    room.on('stream', async stream => {
      // video要素を生成
      const newVideo = document.createElement('video');
      newVideo.srcObject = stream;
      newVideo.playsInline = true;
      // peerLeaveイベントのときにストリームを見つけるためにpeerIdを付ける
      newVideo.setAttribute('data-peer-id', stream.peerId);
      remoteVideos.append(newVideo);
      await newVideo.play().catch(console.error);
    });

```
##### data
メッセージを着信したとき：

```javascript:scriptタグ内
    room.on('data', ({ data, src }) => {
      // メッセージと発信者を表示
      messages.textContent += `${src}: ${data}\n`;
    });

```
### テキスト発信
メッセージを発信するとき：

```javascript:scriptタグ内
    sendTrigger.addEventListener('click', onClickSend);

    function onClickSend() {
      // websocketでroomの皆さんにメッセージを起こる
      room.send(localText.value);
      // メッセージと発信者を表示
      messages.textContent += `${peer.id}: ${localText.value}\n`;
      // インプットを消す
      localText.value = '';
    }
  });
```
## 映像・音声はオン・オフ等

##### マイク音声オフ
ミュートする：

```javascript:scriptタグ内
    localStream.getAudioTracks().forEach(track => track.enabled = !audioInStatus);
```
##### 音声オフ
相手の音声を削音する：

```javascript:scriptタグ内
    remoteVideos.forEach(video => {
      video.srcObject.getAudioTracks().forEach(track => track.enabled = !audioOutStatus);
    });
```
※　音全部消したいときマイク音声もオフしなければならない。
##### カメラ映像オフ
カメラの映像を消す：

```javascript:scriptタグ内
    localStream.getVideoTracks().forEach(track => track.enabled = false);
```
##### 反響キャンセリング
 反響を消す：

```javascript:scriptタグ内
    localStream.getAudioTracks().forEach(track => {
        let constraints = track.getConstraints();
        constraints.echoCancellation = true;
        track.applyConstraints(constraints);
    });
```
## 音声通話
[`navigator.mediaDevices.getUserMedia()`](https://developer.mozilla.org/ja/docs/Web/API/MediaDevices/getUserMedia)の引数でメディアのタイプ（画像・音声・両方）等を選択できる：

```javascript:scriptタグ内
  // カメラ映像取得
const localStream = await navigator.mediaDevices
    .getUserMedia({
      audio: true,
      video: false,
    })
```
## 画面共有
[自分の画面をMediaStreamとして取得できる](https://webrtc.ecl.ntt.com/en/api-reference/javascript.html#screen-sharing)：

```javascript:scriptタグ内
const stream = await navigator.mediaDevices.getDisplayMedia({ video: true });
```
このメディアストリームを使って画面共有機能が実現できる。
# 終わりに．．．
WebRTCを使用できるのでウェブアプリケーションの元に使用できるフレームワークと思いました。

その調査をして私の理想なリモートワークのツールについていっぱいなアイデアが生まれて社長が本気で開発始めようのは本気になって欲しいです。
## 参考
- [MediaDevices.getDisplayMedia()](https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getDisplayMedia)
- [Using the Screen Capture API](https://developer.mozilla.org/en-US/docs/Web/API/Screen_Capture_API/Using_Screen_Capture)