WebRTC Platform SkyWay ハンズオン SFU編

# はじめに

- この資料は、[WebRTC Platform SkyWay](https://webrtc.ecl.ntt.com)のハンズオン資料です。

---

# 作者紹介

## なかゆうすけ（@yusuke84）

- SkyWay Team @ NTTコミュニケーションズ
      - Technical solutions　& support engineer
      - Developer relations(Community manager)
      - (Unofficial) SkyWay Evangelist
- コミュニティ
  - WebRTC Meetup Tokyo 主催
  - WebRTC Beginners Tokyo 主催

---

# 今日作るもの

本編で作成する最終的なアプリのデモをお見せします。

- https://skyway.github.io/skyway-handson-js/

---

# 動作確認済みブラウザ

- Chrome M67 Stable
- Firefox 60 Stable

---

# 今日の流れ

---

#### STEP0
- SkyWayの開発者登録をしてみよう

#### STEP1
- ブラウザでカメラ、マイクを利用してみよう

#### STEP2
- 1:1のビデオチャットを実装してみよう

---

#### STEP3
- SkyWayのroom機能を利用して複数人によるビデオチャットを実装してみよう

#### STEP4
- 応用編1
  - カメラとマイクを選択できるようにしてみよう

#### STEP5
- 応用編2
  - 録音録画機能を実装してみよう

---

# ハンズオン用ソースコード

- https://github.com/skyway/skyway-handson-js
  - masterブランチをローカルにダウンロードして下さい
  - STEP1〜STEP5までの各フォルダに各STEPの完成形のソースコードが格納されています
  - handsonフォルダのコードをベースに開発を進めて下さい
  - githubを使ったことがない方は[ZIP](https://github.com/skyway/skyway-handson-js/archive/master.zip)でダウンロードして展開して下さい
- SkyWayとは別にjQueryを利用しています

---

# 開発環境を整える

- Webカメラ：PC内蔵のものもしくはUSB接続の外付のもの、無ければWebブラウザで認識できる仮想カメラソフト
- エディタ: お好きなものをご利用下さい（Visual Studio Code など）
- ブラウザ: Google Chrome（Firefoxでも動くはず）
- ローカルWebサーバが起動できること：apache/simpleHTTPServer/php等
  - 参考: [開発用ローカルサーバを立ち上げる方法](https://qiita.com/higuma/items/b23ca9d96dac49999ab9)
  - 参考: [お手軽なWebサーバーの立て方](https://qiita.com/massie_g/items/2913066e596dae197539)

---

# STEP0

---

## SkyWayの開発者登録をしてみよう

- https://console-webrtc-free.ecl.ntt.com/users/registration

![スクリーンショット 2018-06-22 17.39.07.png](https://qiita-image-store.s3.amazonaws.com/0/6651/26852e1e-4eee-07fc-9e85-8864bb1e5b91.png)


---

### 登録に必要な情報を入力

|項目|内容|
|:--|:--|
|名前|フルネームでお願いします|
|メールアドレス|連絡の取れるアドレスを入れて下さい<BR>メールアドレスがID代わりになります|
|パスワード|パスワードを設定して下さい|

---

### 登録完了後ダッシュボードへログイン

- アプリケーションを作成する

![スクリーンショット 2018-06-22 17.36.48.png](https://qiita-image-store.s3.amazonaws.com/0/6651/cf565af3-1eac-5cec-77c4-21d8adb5d232.png)

---

#### 「利用可能ドメイン」について

- SkyWayのAPIキーにはドメイン認証がかかっています
  - JavaScript SDKの場合は、SkyWayを利用したWebアプリをホスティングするWebサイトのドメイン名を設定
  - iOS / Android SDK同士の場合は、任意の文字列を設定
  - ワイルドカード文字(*)（Ex: *.xxx.co.jp）の利用もできます。

- ハンズオン用のAPIキーには *localhost* を設定

---

#### 「APIキー認証」について

- 「APIキー認証を利用する」をチェックすると認証サーバと連携した認証機能利用が必須になる
  - 詳細は [SkyWay Peer 認証サンプルのREADME](https://github.com/skyway/skyway-peer-authentication-samples/blob/master/README.jp.md)
  - ハンズオンでは利用しないためチェックしない。

<img width="619" alt="スクリーンショット 2018-06-22 17.50.27.png" src="https://qiita-image-store.s3.amazonaws.com/0/6651/71e801c0-c17b-6854-ecab-efc3d29078b3.png">

---

#### アプリケーションの設定内容について

詳しく知りたい方は、[SkyWay公式チュートリアル](https://webrtc.ecl.ntt.com/js-tutorial.html#preparation)をご確認ください。

---

# STEP1

---

## ブラウザでカメラ、マイクを利用してみよう

- getUserMedia()を利用してカメラとマイクを取得する
  - Promiseによる非同期処理を行うAPI

```js

navigator.mediaDevices.getUserMedia({video: true, audio: true})
    .then(function (stream) { // success
    }).catch(function (error) { // error
    return;
});

```

---

### getUserMediaに必要な処理を追加する

- `script.js`に追記して下さい

```js
let localStream = null;

navigator.mediaDevices.getUserMedia({video: true, audio: true})
    .then(function (stream) {
        $('#myStream').get(0).srcObject = stream;
        localStream = stream;
    }).catch(function (error) {
        console.error('mediaDevice.getUserMedia() error:', error);
        return;
    });
```

---

#### 実装する際のポイント

- VideoとAudioの選択

`{video: true, audio: true}`

- 最低でも640x480以上、出来たら1280x720が良いけどだめならよしなにやってください

`{ audio: true, video: { width: {min: 640, ideal: 1280}, height: {min: 480,ideal: 720} } }`

- 絶対に640x480がいいです…それ以外だったら動作しなくてもOK
    - min,maxの代わりに `exact: 640` と指定することも可能

`{ audio: true, video: { width: {min: 640, max: 640}, height: {min: 480, max: 480} } }`

- フレームレートの設定（一例/Chrome限定）

`{ audio: true, video: { frameRate: { min: 10, max: 15 } } }`

---

### 使用する上の注意点

- 許可を求めるダイアログが出てくる
  - 複数のカメラやマイクが接続されている場合は、適切なものを選択する必要あり

Chrome

![chrome_gum.png](https://qiita-image-store.s3.amazonaws.com/0/6651/40813406-fa83-1e00-046d-e0c45fd7c533.png)

Firefox

![firefox_gum.png](https://qiita-image-store.s3.amazonaws.com/0/6651/8b3601cf-6a5e-b5bd-3b4f-a362678a35b3.png)

---

### 使用する上の注意点

- ホスティング先に注意

|スキーマ\ブラウザ|Chrome|Firefox|
|:--|:--:|:--:|
|http://localhost/|◯|◯|
|http://domain.jp|x|◯|
|file://index.html|x|◯|
|https://domain.jp|◯|◯|

---

# STEP2

---

## 1:1のビデオチャットを実装してみよう
SkyWayを利用して1:1のビデオチャットを実現してみます。

---

### SkyWayのSDKを利用する
今回のハンズオンのHTMLには既に記載済みですが、以下の通りScript要素でSDKをインポートします。

```HTML
<script type="text/javascript" src="https://cdn.webrtc.ecl.ntt.com/skyway-latest.js"></script>
```

---

### Peerオブジェクトを作成する

- `script.js`に追記して下さい

```js
let localStream = null;
let peer = null;
let existingCall = null;

navigator.mediaDevices.getUserMedia({video: true, audio: true})
    // 省略
});

peer = new Peer({
    key: 'apikey',
    debug: 3
});
```

---

#### 実装する際のポイント

- ダッシュボードで払い出したAPIキーを設定する
- デバッグレベル（console log）で表示する情報を規定する
- peer.jsを基本的には踏襲しています

```js
peer = new Peer({
    key: 'apikey',
    debug: 3
});
```

---

#### 実装する際のポイント

- new Peer()のoptions object

```js
    peer = new Peer(peerid,options);
```

|Name|Description|Required|
|:--|:--|:--:|
|key|APIキー|*|
|debug|ログレベル（None:0,ERROR:1,WARN:2,FULL:3）||
|turn|SkyWayで提供するTURNを利用するかどうか(true/false)||
|credential|APIキー認証のためのクレデンシャル情報||
|config|RTCPeerConnectionに渡されるオブジェクト<BR>例：TURNサーバやSTUNサーバを個別に指定したりできる||

- 上記以外のオプションパラメータについては[API Reference](https://webrtc.ecl.ntt.com/skyway-js-sdk-doc/ja/peer/)を参照

---

### PeerオブジェクトのEventListenerを追加する

- `script.js`に追記して下さい

```js
peer.on('open', function(){
    $('#my-id').text(peer.id);
});

peer.on('call', function(call){
    call.answer(localStream);
    setupCallEventHandlers(call);
});

peer.on('error', function(err){
    alert(err.message);
});
```

---

#### 実装する際のポイント（１）

- Openイベント
  - SkyWayのシグナリングサーバと接続し、利用する準備が整ったら発火します
  - 今回は、PeerIDと呼ばれるクライアント識別用のIDをシグナリングサーバで発行し、その情報をUIに表示する処理を行っています

```js
peer.on('open', function(){
    $('#my-id').text(peer.id);
});
```

---

#### 実装する際のポイント（２）

- Callイベント
  - 相手から接続要求がきた場合に発火します
  - 相手との接続を管理するためのCallオブジェクトが取得できるため、それを利用して必要な処理を行います
      - Answerメソッドを実行し、接続要求に応答します。この時に、自分自身のlocalStreamをセットすると、相手に映像・音声を送信することができるようになります
      - Callオブジェクトを利用したEventListenerをセットします
          - setupCallEventHandlers()の中身については後ほど説明

```js
peer.on('call', function(call){
    call.answer(localStream);
    setupCallEventHandlers(call);
});
```

----

#### 実装する際のポイント（３）

- Errorイベント
  - 何らかのエラーが発生した場合に発火します

```js
peer.on('error', function(err){
    alert(err.message);
});
```

---

### 発信、切断処理の為の処理を追加する

- `script.js`に追記して下さい

```js
$('#make-call').submit(function(e){
    e.preventDefault();
    const call = peer.call($('#peer-id').val(), localStream);
    setupCallEventHandlers(call);
});

$('#end-call').click(function(){
    existingCall.close();
});
```

---

#### 実装する際のポイント（１）

- 発信処理
  - `peer.call`で相手のPeerID、自分自身のlocalStreamを引数にセットし発信します
      - PeerIDは電話番号のようなもので、何らかの方法で入手する必要があります
  - Callオブジェクトが返ってくるため、必要なEventListenerをセットします
      - setupCallEventHandlers()の中身については後ほど説明

```js
$('#make-call').submit(function(e){
    e.preventDefault();
    const call = peer.call($('#peer-id').val(), localStream);
    setupCallEventHandlers(call);
});
```

---

#### 実装する際のポイント（２）

- 切断処理
  - Callオブジェクトのclose()メソッドを実行します
  - 先程生成したCallオブジェクトは`existingCall`として保持しておきます
      - オブジェクト保持はsetupCallEventHandlers()の中で実行します

```js
$('#end-call').click(function(){
    existingCall.close();
});
```

---

### CallオブジェクトのEventListenerを追加する

- `script.js`に追記して下さい

```js
function setupCallEventHandlers(call){
    if (existingCall) {
        existingCall.close();
    };

    existingCall = call;

    call.on('stream', function(stream){
        addVideo(call,stream);
        setupEndCallUI();
        $('#connected-peer-id').text(call.remoteId);
    });

    call.on('close', function(){
        removeVideo(call.remoteId);
        setupMakeCallUI();
    });
}
```

---

#### 実装する際のポイント（１）

- 今回作るアプリでは既に接続中の場合は一旦既存の接続を切断し、後からきた接続要求を優先する
  - アプリの仕様次第

```js
if (existingCall) {
    existingCall.close();
};

existingCall = call;
```

---

#### 実装する際のポイント（２）

- Streamイベント
  - 相手の映像・音声を受信した際に発火します
  - 取得したStreamオブジェクトをVIDEO要素にセットします
      - addVideo()の中身については後ほど説明
  - UI関連の処理を実施します
      - 切断用のボタンを表示

```js
call.on('stream', function(stream){
    addVideo(call,stream);
    setupEndCallUI();
    $('#connected-peer-id').text(call.remoteId);
});
```

---

#### 実装する際のポイント（３）

- Closeイベント
  - Callオブジェクトのclose()メソッドが実行され切断処理が完了したら発火します
  - close()メソッドを実行した側、実行された側それぞれで発火します
  - 切断時にVIDEO要素を削除します
      - `call.remoteId`で切断先のPeerIDを取得できます
      - removeVideo()の中身については後ほど説明
  - UI関連の処理を実施します
      - 接続用のボタン、PeerIDを入力するInputボックスを用意

```js
call.on('close', function(){
    removeVideo(call.remoteId);
    setupMakeCallUI();
});
```

---

### 必要な関数を準備する

- `script.js`に追記して下さい

```js
function addVideo(call,stream){
    const videoDom = $('<video autoplay>');
    videoDom.attr('id',call.remoteId);
    videoDom.get(0).srcObject = stream;
    $('.videosContainer').append(videoDom);
}

function removeVideo(peerId){
    $('#'+peerId).remove();
}

function setupMakeCallUI(){
    $('#make-call').show();
    $('#end-call').hide();
}

function setupEndCallUI() {
    $('#make-call').hide();
    $('#end-call').show();
}
```

---

#### 実装する際のポイント（１）

- VIDEO要素のsrcObjectプロパティにStreamオブジェクトをセットすることで再生できます
  - 削除する処理のことを考えて、idプロパティに`call.remoteId(PeerID)`をセットします

```js
function addVideo(call,stream){
    const videoDom = $('<video autoplay>');
    videoDom.attr('id',call.remoteId);
    videoDom.get(0).srcObject = stream;
    $('.videosContainer').append(videoDom);
}
```

---

#### 実装する際のポイント（２）

- 切断された（した）相手のVIDEO要素をPeerIDを元に削除します

```js
function removeVideo(peerId){
    $('#'+peerId).remove();
}
```

---

#### 実装する際のポイント（３）

- UI関連処理を実装します

```js
function setupMakeCallUI(){
    $('#make-call').show();
    $('#end-call').hide();
}

function setupEndCallUI() {
    $('#make-call').hide();
    $('#end-call').show();
}
```

---

### 1:1のビデオチャットを試してみる

1. 2つのブラウザタブでアプリを開く
2. 片方の`Your id`を片方のInputボックスにコピペしてCallボックスをクリックする
 
---

# STEP3

---

## SkyWayのRoom機能を利用して複数人によるビデオチャットを実装してみよう
- WebRTCは1:1の通信をP2Pでやることを前提に作られていますが、それを応用することで複数人で通信することが可能になります
- SkyWayではRoom機能を提供し、より直感的に複数人によるビデオチャット等を実現できるようになっています
- STEP2で作成したコードを修正します

---

### getUserMediaのキャプチャサイズを変更する

- `script.js`を修正して下さい

```js
let constraints = {
    video: {},
    audio: true
};
constraints.video.width = {
    min: 320,
    max: 320
};
constraints.video.height = {
    min: 240,
    max: 240        
};

navigator.mediaDevices.getUserMedia(constraints)
    .then(function (stream) { // success
        $('#myStream').get(0).srcObject = stream;
        localStream = stream;
    }).catch(function (error) { // error
    console.error('mediaDevice.getUserMedia() error:', error);
    return;
});
```

---

#### 実装する際のポイント（１）

- 320x240に制限します
  - 複数人のビデオチャットの場合、ブラウザに掛かる負荷が増えるため、メディアのキャプチャサイズを制限し負荷を減らすことが出来ます
  - minとmaxでその解像度を矯正します
      - サイズだけを指定するとブラウザの制御に委ねられる
      - ただし、制御した解像度で映像が取得できなければエラーになります

```js
let constraints = {
    video: {},
    audio: true
};
constraints.video.width = {
    min: 320,
    max: 320
};
constraints.video.height = {
    min: 240,
    max: 240 
};

navigator.mediaDevices.getUserMedia(constraints)
    .then(function (stream) {
        $('#myStream').get(0).srcObject = stream;
        localStream = stream;
    }).catch(function (error) {
        console.error('mediaDevice.getUserMedia() error:', error);
        return;
    });
```

---

### 発信処理をRoomへの参加処理に変更する

- `script.js`を修正して下さい

```js
$('#make-call').submit(function(e){
    e.preventDefault();
    let roomName = $('#join-room').val();
    if (!roomName) {
        return;
    }
    const call = peer.joinRoom(roomName, {mode: 'sfu', stream: localStream});
    setupCallEventHandlers(call);
});
```

---

#### 実装する際のポイント（１）

- 接続先のPeerIDの代わりに参加するRoom名を入力してもらい、そのRoom名を引数に指定し、`peer.joinRoom`メソッドを実行する
  - modeは`sfu`と`mesh`から選択可能

```js
let roomName = $('#join-room').val();
if (!roomName) {
  return;
}
const call = peer.joinRoom(roomName, {mode: 'sfu', stream: localStream});
```

---

#### 実装する際のポイント（２）

- SFUとMeshの違い
  - SFUはサーバに対して自分自身のメディアストリームを送信する
  - Meshは参加者全員に対してメディアストリームを送信する
  - 受信はどちらとも参加者分だけ行う
  - SFUの方が端末の負荷が軽い

<img width="819" alt="sfu_mesh.png" src="https://qiita-image-store.s3.amazonaws.com/0/6651/ae36eaf0-a33b-5b2c-9c6e-e7f9de3afb90.png">

---

### CallオブジェクトのEventListenerにRoom機能を実現するためのイベントを追加する

- `script.js`を修正して下さい

```js
function setupCallEventHandlers(call){
    if (existingCall) {
        existingCall.close();
    };

    existingCall = call;
    setupEndCallUI();
    $('#room-id').text(call.name);

    call.on('stream', function(stream){
        addVideo(stream);
    });

    call.on('peerLeave', function(peerId){
        removeVideo(peerId);
    });

    call.on('close', function(){
        removeAllRemoteVideos();
        setupMakeCallUI();
    });

}
```

---

#### 実装する際のポイント（１）

- 1:1の時はStreamイベント内部にあった処理を外に出す
  - Room機能を利用する場合はRoomに参加した時点で通信開始となるため

```js
setupEndCallUI();
$('#room-id').text(call.name);
```

---

#### 実装する際のポイント（２）

- Room機能を利用するとStreamオブジェクトにPeerIDが格納されるため、addVideoの第一引数に指定したCallオブジェクトは省略できます
    - addVideo()の中身については後ほど

```js
call.on('stream', function(stream){
    addVideo(stream);
});
```

---

#### 実装する際のポイント（３）

- peerLeaveイベント
  - Roomから参加者が抜けたら発火します
  - 抜けた参加者のPeerIDを取得できるため、そのIDを利用して対応するVIDEO要素を削除します

```js
call.on('peerLeave', function(peerId){
    removeVideo(peerId);
});
```

---

#### 実装する際のポイント（４）

- Closeイベント
  - close()メソッドを実行し、自分自身がRoomから抜けた場合に発火します
  - 複数参加者がいる場合があるため、全てのVIDEO要素を削除します
      - removeAllRemoteVideos()の中身については後ほど

```js
call.on('close', function(){
    removeAllRemoteVideos();
    setupMakeCallUI();
});
```

---

### addVideoを修正しremoveAllRemoteVideosを追加する

- `script.js`を修正して下さい

```js
function addVideo(stream){
    const videoDom = $('<video autoplay>');
    videoDom.attr('id',stream.peerId);
    videoDom.get(0).srcObject = stream;
    $('.videosContainer').append(videoDom);
}

// 省略

function removeAllRemoteVideos(){
    $('.videosContainer').empty();
}
```

---

#### 実装する際のポイント（１）

- stream.peerIdでPeerIDが取得できます

```js
function addVideo(stream){
    const videoDom = $('<video autoplay>');
    videoDom.attr('id',stream.peerId);
    videoDom.get(0).srcObject = stream;
    $('.videosContainer').append(videoDom);
}
```

---

#### 実装する際のポイント（２）

- jQueryの機能を使いVIDEO要素全てを削除します

```js
function removeAllRemoteVideos(){
    $('.videosContainer').empty();
}
```

---

### UI上の文言を修正する

- `index.html`を修正して下さい

```HTML
<div class="myControllerContainer">
    <p>Your id: <span id="my-id">...</span></p>
    <form id="make-call">
        <input type="text" placeholder="Join room..." id="join-room">
        <button type="submit">Join</button>
    </form>
    <div id="end-call">
        <p>Currently in room <span id="room-id">...</span></p>
        <button>Leave</button>
    </div>
</div>
```

---

#### 実装する際のポイント（１）

- CallからJoin roomへ変更します

```HTML
<input type="text" placeholder="Join room..." id="join-room">
<button type="submit">Join</button>
```

```HTML
<p>Currently in room <span id="room-id">...</span></p>
<button>Leave</button>
```

---

### 複数人によるビデオチャットを試してみる

1. 3つのブラウザタブでアプリを開く
2. 任意のRoom名（半角英数）を決めて、全アプリでそのRoomに参加する

---

# STEP4（応用編）

---

## カメラとマイクを選択できるようにしてみよう
getUserMediaを利用するとブラウザが提供するダイアログウィンドウにてマイク、カメラを選択することが出来ます。

![chrome_gum.png](https://qiita-image-store.s3.amazonaws.com/0/6651/40813406-fa83-1e00-046d-e0c45fd7c533.png)

---

## カメラとマイクを選択できるようにしてみよう
その機能をアプリ内に組込むことも出来ます。そうすることで、より利用者に優しいUI設計が可能となります。

STEP4ではSTEP3までで開発したアプリにその機能を組み込みます。また、ビデオチャット中にカメラ切り替えを可能にします。

動作確認には複数のカメラ、マイクが必要です。

---

## カメラとマイクを選択できるようにしてみよう

- enumerateDevicesというAPIを利用

```js
navigator.mediaDevices.enumerateDevices()
    .then(function (devices) { // success
    }).catch(function (error) { // error
    return;
});
```

----

### カメラ、マイクの一覧を取得し切り替えられるようにする

- `script.js`に追記します

```js
let audioSelect = $('#audioSource');
let videoSelect = $('#videoSource');

navigator.mediaDevices.enumerateDevices()
    .then(function(deviceInfos) {
        for (let i = 0; i !== deviceInfos.length; ++i) {
            let deviceInfo = deviceInfos[i];
            let option = $('<option>');
            option.val(deviceInfo.deviceId);
            if (deviceInfo.kind === 'audioinput') {
                option.text(deviceInfo.label);
                audioSelect.append(option);
            } else if (deviceInfo.kind === 'videoinput') {
                option.text(deviceInfo.label);
                videoSelect.append(option);
            }
        }
        videoSelect.on('change', setupGetUserMedia);
        audioSelect.on('change', setupGetUserMedia);
        setupGetUserMedia();
    }).catch(function (error) {
        console.error('mediaDevices.enumerateDevices() error:', error);
        return;
    });
```

---

#### 実装する際のポイント（１）

- UI上にマイクとカメラ選択用のリストボックスを用意し、そのリストに選択可能なマイクとカメラを追加します
  - HTMLの修正は後ほど

```js
let audioSelect = $('#audioSource');
let videoSelect = $('#videoSource');
```

---

#### 実装する際のポイント（２）

- enumerateDevices()の実行に成功すると、deviceInfosというマイクとカメラ情報が格納された配列が返ってくる
  - その配列の中にある、deviceIdとlabelを取り出し、Option要素を生成しリストボックスに設定する

```js
navigator.mediaDevices.enumerateDevices()
        .then(function(deviceInfos) {
            for (var i = 0; i !== deviceInfos.length; ++i) {
                let deviceInfo = deviceInfos[i];
                let option = $('<option>');
                option.val(deviceInfo.deviceId);
                if (deviceInfo.kind === 'audioinput') {
                    option.text(deviceInfo.label);
                    audioSelect.append(option);
                } else if (deviceInfo.kind === 'videoinput') {
                    option.text(deviceInfo.label);
                    videoSelect.append(option);
                }
            }
```

---

#### 実装する際のポイント（３）

- deviceInfosの中身

<img width="626" alt="deviceinfos.png" src="https://qiita-image-store.s3.amazonaws.com/0/6651/42c72892-21b5-82a6-5f9a-31668e3a25f7.png">

|項目名|用途|
|:--|:--|
|deviceId|識別IDでgetUserMediaでマイク、カメラを指定する際に利用する|
|groupId|同じ物理デバイスで複数認識されている場合に同一グループになる。例えば、PC等でサウンド出力を内蔵マイクとしている場合、既定と内蔵マイクは同一グループ|
|kind|audioinput/videoinputの２値でAudioデバイスか、Videoデバイスかを識別可能|
|label|PC等で付与されるラベルでユーザには一番わかり易い|

---

#### 実装する際のポイント（４）

- 先程の処理で生成したOption要素の例

```HTML
<option value="c81339aea15028711063a8f861df00e5e2c61cd9415200d2ebda016bf7d3374e">FaceTime HD カメラ</option>
```

---

#### 実装する際のポイント（５）

- セレクトボックスで選択状態が変更されたら都度getUserMedia()を実行する

```JS
videoSelect.on('change', setupGetUserMedia);
audioSelect.on('change', setupGetUserMedia);
setupGetUserMedia();
```

---

### getUserMediaの処理を修正しdeviceIdを指定できるようにする

- `script.js`を修正します

```js
function setupGetUserMedia() {
    let audioSource = $('#audioSource').val();
    let videoSource = $('#videoSource').val();
    let constraints = {
        audio: {deviceId: {exact: audioSource}},
        video: {deviceId: {exact: videoSource}}
    };

    // 省略

    if(localStream){
        localStream = null;
    }

    navigator.mediaDevices.getUserMedia(constraints)
        .then(function (stream) {
            $('#myStream').get(0).srcObject = stream;
            localStream = stream;

            if(existingCall){
                existingCall.replaceStream(stream);
            }

        }).catch(function (error) {
            console.error('mediaDevice.getUserMedia() error:', error);
            return;
        });
}
```

---

#### 実装する際のポイント（１）

- セレクトボックスで選択されたデバイスのdeviceIdを取得してgetUserMediaに渡すパラメーターを生成します

```js
let audioSource = $('#audioSource').val();
let videoSource = $('#videoSource').val();
let constraints = {
    audio: {deviceId: {exact: audioSource}},
    video: {deviceId: {exact: videoSource}}
};

// 省略
```

---

#### 実装する際のポイント（２）

- getUserMediaが複数回実行する事を考慮して毎回ガベージコレクションします

```js
if(localStream){
    localStream = null;
}
```

---

#### 実装する際のポイント（３）

- ビデオチャット中にマイクやカメラを切り替え場合、チャットを中断すること無くそれを反映させることが出来ます
  - replaceStreamというメソッドに新しいStreamオブジェクトを設定し実行します

```js
if(existingCall){
    existingCall.replaceStream(stream);
}
```

---

### 通信中にマイクやカメラが変更された場合に検知するためのイベントを追加する

- `script.js`を修正します

```js
function setupCallEventHandlers(call){

    // 省略

    call.on('stream', function(stream){
        addVideo(stream);
    });

    call.on('removeStream', function(stream){
        removeVideo(stream.peerId);
    });

    call.on('peerLeave', function(peerId){
        removeVideo(peerId);
    });
    
    // 省略

}
```

---

#### 実装する際のポイント（１）

- removeSrtreamイベントを追加します。相手がreplaceStreamメソッドを実行すると発火します
  - 発火したら該当のVIDEO要素を削除します
  - ほぼ同時にstreamイベントも発火し新しいStreamオブジェクトをVIDEO要素にセットします

```js
call.on('removeStream', function(stream){
    removeVideo(stream.peerId);
});
```

---

### セレクトボックスを設置する

- `index.html`を修正して下さい

```HTML
<!-- 省略 -->
<p>Your id: <span id="my-id">...</span></p>
<div class="select">
    <label for="audioSource">Audio input source: </label><select id="audioSource"></select>
</div>
<div class="select">
    <label for="videoSource">Video source: </label><select id="videoSource"></select>
</div>
<form id="make-call">
<!-- 省略 -->
```

---

#### 実装する際のポイント（１）

- マイクとカメラを選択するためのセレクトボックスを設置します

```HTML
<div class="select">
    <label for="audioSource">Audio input source: </label><select id="audioSource"></select>
</div>
<div class="select">
    <label for="videoSource">Video source: </label><select id="videoSource"></select>
</div>
```

---

### マイクカメラ切り替えを試してみる

1. PCに複数のカメラを接続する（マイクよりわかりやすい）
2. 3つのブラウザタブでアプリを開く
3. 任意のRoom名（半角英数）を決めて、全アプリでそのRoomに参加する
4. セレクタで切り替えられることを確認する

---

# STEP5（応用編）

---

## 録音録画機能使ってみよう
ChromeやFirefoxではビデオチャットの映像音声を録音録画するAPIが備わっています。APIの全容は以下の通りです。

```JS
// getUserMedia()で取得したstreamをセットしておく
let localStream = null;
let recorder =  null;
function startRecording() {
  // MediaRecorderオブジェクトを利用できるようにする
 recorder = new MediaRecorder(localStream);
 recorder.ondataavailable = function(evt) {
  // レコーディングしたデータを非同期で取得できる
 }
 
 // レコーディング開始
 recorder.start();
}
 
// レコーディング停止
function stopRecording() {
 recorder.stop();
}
```
出典:[WebRTCで録画する！MediaRecoderを使ってみよう@html5experts.jp](https://html5experts.jp/mganeko/12475/)

---

## 録音録画機能を実装してみよう
- STEP5ではSTEP4までで開発したアプリにその機能を組み込みます。簡易版なのでRoom機能が備わっていますが、1:1のビデオチャットを想定します

---

### Recordingボタンを追加する

- `script.js`に追記して下さい

```js
$('#recording button').click(function(){
    if(recorder){
        recorder.stop();
        $('#recording button').text('Recording');
        $('#downloadlink').hide();
    }else if(remoteStream){
        let chunks = [];
        let options = {
            mimeType : 'video/webm; codecs=vp9'
        };

        recorder = new MediaRecorder(remoteStream,options);

        recorder.ondataavailable = function(evt) {
            console.log("data available: evt.data.type=" + evt.data.type + " size=" + evt.data.size);
            chunks.push(evt.data);
        };

        recorder.onstop = function(evt) {
            console.log('recorder.onstop(), so playback');
            recorder = null;
            const videoBlob = new Blob(chunks, { type: "video/webm" });
            blobUrl = window.URL.createObjectURL(videoBlob);
            $('#downloadlink').attr("download", 'recorded.webm');
            $('#downloadlink').attr("href", blobUrl);
            $('#downloadlink').show();
        };
        recorder.start(1000);
        console.log('start recording');
        $('#recording button').text('Stop');
        $('#downloadlink').hide();
    }
});
```

---

#### 実装する際のポイント（１）

- ボタン1つでRecordingとStopに対応させるため。既にRecordingしている場合はStopするようにします
- UI関連の処理を実施します
  - ボタンのテキストをRecordingに変更
  - ダウンロードリンクを非表示

```js
$('#recording button').click(function(){
    if(recorder){
        recorder.stop();
        $('#recording button').text('Recording');
        $('#downloadlink').hide();
    }else if(remoteStream){
        // 省略
```

---

#### 実装する際のポイント（２）

- Recordingデータを書くのするための配列の初期化

```js
let chunks = [];
```

---

#### 実装する際のポイント（3）

- 生成する映像ファイルのmimetypeを指定します。これを指定しないとダウンロードしたファイルが再生できません

```js
let options = {
    mimeType : 'video/webm; codecs=vp9'
};
```

---


#### 実装する際のポイント（4）

- MediaRecorderオブジェクトを作成します。引数でRecordingするStreamオブジェクトを渡します
  - 今回のハンズオンのソースコードでは、remoteStreamを渡し、相手の映像音声をRecordingします
  - optionsでmimeTypeを指定します

```js
recorder = new MediaRecorder(remoteStream,options);
```

---

#### 実装する際のポイント（４）

- Recordingに関するイベントを2つセットします
  - ondataavailableはデータが到着したら発火します
     - このイベント契機で配列chunksにデータを格納していきます
  - onstopはstopメソッドが実行されてRecordingが停止したら発火します
     - このイベント契機で配列chunksからデータを取り出しダウンロード可能な形式に変換し、ダウンロードリンクのURLとして設定します
- UI関連の処理を実施します
  - ダウンロードリンクを表示にする

```js
recorder.ondataavailable = function(evt) {
    console.log("data available: evt.data.type=" + evt.data.type + " size=" + evt.data.size);
    chunks.push(evt.data);
};

recorder.onstop = function(evt) {
    console.log('recorder.onstop(), so playback');
    recorder = null;
    const videoBlob = new Blob(chunks, { type: "video/webm" });
    blobUrl = window.URL.createObjectURL(videoBlob);
    $('#downloadlink').attr("download", 'recorded.webm');
    $('#downloadlink').attr("href", blobUrl);
    $('#downloadlink').show();
};
```

---

#### 実装する際のポイント（５）

- startメソッドでRecordingを開始します
  - 引数に設定した時間間隔（単位：ms）でデータを区切り、ondataavailableイベントを発火させます
- UI関連の処理を実施します
  - ボタンのテキストをStopに変更
  - ダウンロードリンクを非表示にする

```js
recorder.start(1000);
console.log('start recording');
$('#recording button').text('Stop');
$('#downloadlink').hide();
```

---

### Recordingで利用するオブジェクトを初期化し、remoteStreamを格納する

- `script.js`を修正して下さい

```js
let remoteStream = null;
let recorder = null;

     // 省略

function setupCallEventHandlers(call){

     // 省略

    call.on('stream', function(stream){
        addVideo(stream);
        remoteStream = stream;
    });

     // 省略
```

---

#### 実装する際のポイント（１）

- 今回のハンズオンのソースコードではグローバルオブジェクトとして入れ物を用意します

```js
let remoteStream = null;
let recorder = null;
```

---

#### 実装する際のポイント（２）

- 相手のStreamオブジェクトを受信したタイミングでremoteStreamに格納します
    - 複数人分の録画は想定していないため、最後に入室した人のStreamオブジェクトが録画されます

```js
call.on('stream', function(stream){
    addVideo(stream);
    remoteStream = stream;
});
```

---

### Recordingボタンを設置する

- `index.html`を修正して下さい

```HTML
<div id="end-call">
   <!-- 省略 -->
</div>
<div id="recording">
    <button>Recording</button>
    <a href="#" id="downloadlink">Download</a>
    <p>※ 1:1の場合にリモートを録画可能</p>
</div>
```

---

#### 実装する際のポイント（１）

- Leaveボタンの下に設置し、Roomに接続している最中のみ表示されるようにする
  - ダウンロードリンクはデファクトでCSSで非表示にしています

```HTML
<div id="recording">
    <button>Recording</button>
    <a href="#" id="downloadlink">Download</a>
    <p>※ 1:1の場合にリモートを録画可能</p>
</div>
```

---

### Recordingボタンの表示・非表示を切り替える

- `script.js`を修正して下さい

```
    function setupMakeCallUI(){
        $('#make-call').show();
        $('#end-call').hide();
        $('#recording').hide();
    }

    function setupEndCallUI() {
        $('#make-call').hide();
        $('#end-call').show();
        $('#recording').show();
    }
```

---

#### 実装する際のポイント（1）

- RecordingボタンはデフォルトはCSSで非表示にしている
- Roomに入室したらボタンを表示し、退室したら非表示にする

```
    function setupMakeCallUI(){
        $('#recording').hide();
    }

    function setupEndCallUI() {
        $('#recording').show();
    }
```

---

### 録音録画機能を試してみる

1. 2つのブラウザタブでアプリを開く
2. 任意のRoom名（半角英数）を決めて、全アプリでそのRoomに参加する
3. Recordingボタンをクリックししばらく録音する
4. Stopボタンクリック後にダウンロードリンクをクリックしダウンロードする
5. Chromeにドラッグ・アンド・ドロップして再生できることを確認する

---

# これでハンズオンを終わります