Unified Plan時代のWebRTC（半）手動シグナリング (2018.08)

# 2018年8月のWebRTC API

WebRTC 1.0 に向けて、ゆっくりながらも着実にブラウザの実装が進んでいます。

- MediaStream単位から、MediaTrack単位のAPIに
    - 参考：[最新のWebRTCのtrack系処理を、手動＋データチャネルシグナリングで観察してみる](https://qiita.com/massie_g/items/1316eb8c6e0d171307f5(2018.03))
    - addStream()/removeStream()ではなく、addTrack()/removeTrack()を利用
    - peer.onaddstream(), peer.onremovestream()イベントではなく、peer.ontrack(), stream.onremovetrack()イベントを利用
- Multi-streamが、もうすぐUnified Planに
    - Firefoxだけでなく、ChromeでもM70からUnified Planに
    - Edgeも対応予定、Safariは音沙汰なし
    - 参考：[WebRTC の未来 - Unified Plan](https://gist.github.com/voluntas/59a135343538c290e515#unified-plan)
    - 参考：[SDP の Unified Plan と Plan B](https://blog.jxck.io/entries/2018-01-05/sdp-unified-plan.html)


これを踏まえて、いつもの手動シグナリングをアップデートしました。

# MediaTrack 単位のメディアのハンドリング

## ローカルメディアの追加

RTCPeerConnection.addStream()ではなく、RTCPeerConnection.addTrack()を使います。

```js
let peer = new RTCPeerConnection();
let localStream = await navigator.mediaDevices.getUserMedia({video: true, audio: true});

// --- 従来のMediaStream の追加 ---
peer.addStream(localStream);

// --- 新しい MediaTrack単位の追加 ---
let videoSender = peer.addTrack(localStream.getVideoTracks()[0], localStream);
let audioSender = peer.addTrack(localStream.getAudioTracks()[0], localStream);
```

この時、addTrack()の戻り値のRTCRtpSender を保存して置いて、メディアの除去に使います。

## ローカルメディアの除去

```js
// --- 従来のMediaStream の除去 ---
peer.removeStream(localStream);

// --- 新しい MediaTrack単位の除去 ---
peer.removeTrack(videoSender);
peer.removeTrack(audioSender);
```

## リモート側メディアの追加、除去のイベンント

これまでは、リモート側でメディアが追加になると、RTCPeerConnection.onaddstream() が発火していました。これが、RTCPeerConnection.ontrack() に変わります。また、リモート側のメディアが除去された場合のイベントは、MediaStream.onremovetrack() になります。 （RTCPeerConnection.onremovetrack() のようなイベントはありません）

```js

// ---  従来のMediaStream の追加、除去 ---
peer.onaddstream = function(event) {
  let stream = event.stream;
  // ... 追加時の処理 ...
}
peer.onremovestream = function(event) {
  let stream = event.stream;
  // ... 除去時の処理 ...
};


// --- 新しい MediaTrack単位の追加、除去 ---
peer.ontrack = function(event) {
  let track = event.track;
  let stream = event.streams[0];
  if (track.kind === 'video') {
    // video追加時の処理
  }
  else if (track.kind === 'audio') {
    // audio追加時の処理
  }

  stream.onremovetrack = function (evt) {
    const track = evt.track;
    if (track.kind === 'video') {
      // video除去時の処理
    }
    else if (track.kind === 'audio') {
      // audio除去時の処理
    }
  };

  stream.onaddtrack = function (evt) {
    // 2つ目以降のトラック追加では、このイベントも発火する
    // 通常は peer.ontrack() を使えば、こちらでハンドリングする必要はなさそう
  };
};

// --- トラック除去時に、このようなイベントは発火しない
// peer.onremovetrack = function(event) {};

```

# Unified Plan の利用

1つのRTCPeerConnectionで、複数のメディアストリームを通すマルチストリームには、2種類のやり方があり、生成されるSDPが異なります。

- Unified Plan ... Firefoxが採用。WebRTC 1.0もこちら。Chrome 70でも対応予定
- Plan B ... Chrome, Safari が採用
    - ※Edgeは、まだ2017年の段階ではマルチストリームが利用できませんでした。2018年8月現在の状況は未確認です。

Chrome 70 から、オプション指定でUnified Planも選べる様になります。

```js
let peer = new RTCPeerConnection({sdpSemantics : "unified-plan"});
```

※このオプションを付けていても、Chrome 69やFireFox, Safariでもエラーにはなりません。効果もありませんが。

# 半手動で動かしてみる

MediaTrack単位の扱いや、Unified Planによるマルチストリームの動作が確認できるデモを用意しました。
Chrome Canary 70, Firefox 61, Chrome 69（Unified Planは未対応）で動きます。

- GitHub Pages で試す
    - [addTrack()/ontrack()、multi-stream時代の DataChannelシグナリング](https://mganeko.github.io/webrtcexpjp/basic2016/dc_signaling_multistream.html)
- GitHub でソースを見る
    - [github.com/mganeko/webrtcexpjp](https://github.com/mganeko/webrtcexpjp/blob/master/basic2016/dc_signaling_multistream.html)

## シグナリングの仕組み

- 最初はDataChannelを確立する
    - この際に BroadcastChannel を使って、Offer / Answer のSDPを交換 （[参考](https://qiita.com/massie_g/items/925c12076af669106b40))
    - Vanilla ICE を使って、全ての ICE Candidate が揃ってから交換
    - ※異なるブラウザ間で通信する場合は、BroadcastChannelの替わりに手動でコピー＆ペーストする
- addTrack()/removeTrack()をした場合は、DataChannelを使って Offer / Answer のSDPを再交換
    - peer.onnegotiationneeded() イベントの発火で Offer を投げる
    - 最初のSDP交換でOffer を出した側、Answerを返した側のどちらでも、Offerを投げることができる


## 準備

※WebAudioを扱う部分にバグがあり正しく動きませんでした。2018.10.25現在修正済です。

- ブラウザで2つの別々のウィンドウを開き、横に並べてください
    - この時ログが見れるように、デベロッパーコンソールを開いておくと分かりやすいです
- それぞれ https://mganeko.github.io/webrtcexpjp/basic2016/dc_signaling_multistream.html にアクセスしてください

Mac OS X 上のChrome 69どうし, Chrome Canary 70どうし, Firefox 61どうしで動作確認しています。異なるブラウザ間でも、「use BroadcastChannel」のチェックを外して手動でSDPをコピー＆ペーストすれば、通信させることができます。

以下、Chrome Canary 70 どうしで使った場合の例を示します。

## DataChannelの確立まで

BroadcastCannelを使ってシグナリングを行い、DataChannelの接続を確立します。

<img width="711" alt="broadcast_signaling_for.png" src="https://qiita-image-store.s3.amazonaws.com/0/19403/4eb3f70c-5f52-ebbf-010d-90e19c6e294c.png">


- 1. 片方のブラウザA（左）で [Connect Datachannel]ボタンをクリック
Offer SDP が生成される
- 2. BroadcastChannel経由でブラウザA（左）→ ブラウザB（右）Offerが、ブラウザB → ブラウザA にAnswerが交換される
    - この時、テキストエリアにSDPの内容が表示される
- 3. DataChannelが確立すると、両方のブラウザのコンソールに"datachannel open" のログが表示される
- 4. どちらかのブラウザの[Send Hello]ボタンをクリックすると datachannel でメッセージを送信
- 5. 反対側のブラウザのコンソールにメッセージのログが表示され、メッセージを送り返す。メッセージを送った側のコンソールにもログが表示される

手動の場合は、[こちらの手順を](https://qiita.com/massie_g/items/1316eb8c6e0d171307f5#datachannelの接続まで)参照してください。

## メディアの開始

どちらか一方、または両方のブラウザで、[Start Media]ボタンをクリックすると、ビデオ、オーディオが複数開始し、Local Mediaの領域に表示されます。全部で3つのMediaStreamが用意されます。

<img width="709" alt="start_media.png" src="https://qiita-image-store.s3.amazonaws.com/0/19403/efe90300-4c79-b6cd-28fd-cf27b1c1505b.png">


- 一番左が、カメラとマイクで取得された映像と音声です(Video + Audio の MediaStream)
- 真ん中が、Canvasで時刻を表示しています。captureStream()で映像を取り出してます(Video のみの MediaStream)
- 一番右が、WebAudioで作った音声です（Audio のみの MediaStream) ※音量はゼロです

# メデイアトラックの追加/除去

## トラックの追加

- [add Video]でカメラの映像が、[add Audio]でマイクの音声が追加されます （同じMediaStream)
- [add Video of Canvas]でキャンバスからの時刻の映像が追加されます （2番目のMediaStream)
- [add Audio of WebAudio]でWebAudioで作った和音の音声が追加されます（3番目のMediaStream)

送信先では、MediaStream毎にVideoElementが生成されます。手を抜いて音声だけの場合もVideoElementで再生しています。今回は最大3つのVideoElementが表示されることになります。
両方でトラックを追加すれば、双方向にも映像/音声を送信できます。

## 追加処理の流れ

- 1. 送信元
    - peer.addTrack()でビデオ/オーディオトラックを追加、戻り値のsenderを保持
    - peer.onnegotiationnneded()が発火
    - DataChannel経由で、Offer/Answerが交換される
    - 相手側のブラウザに映像または音声が伝送される
- 2. 送信先（受信側）
    - peer.addTarck()イベントが発火
    - 2つ目以降のトラックだった場合は、mediastream.onaddtrack()も発火（何もしていない）
    - （MediaTrackが所属する）MediaSteamに対応するVideo要素がなかったら、新しく生成
    - video要素で、映像または音声を再生

## トラックの除去

- [remove Video]でカメラの映像を、[remove Audio]でマイクの音声を除去します （同じMediaStream)
- [remove Video of Canvas]でキャンバスからの映像を除去します （2番目のMediaStream)
- [remove Audio of WebAudio]でWebAudioで作った音声を除去します（3番目のMediaStream)

送信先では、トラック数がゼロになったMediaStreamに対応するVideoElementは削除されます。

## 除去処理の流れ

- 1. 送信元
    - 除去するトラックに対応するsenderを取得し、peer.removeTrack()で除去
    - peer.onnegotiationnneded()が発火
    - DataChannel経由で、Offer/Answerが交換される
    - 相手側のブラウザで映像または音声が除去される
- 2. 送信先（受信側）
    - mediastream.onremovetrack()が発火
    - 該当するmediastreamのトラック数がゼロになったら、対応するVideoElementの再生を停止、削除

# 終わりに

しょぼいサンプルですが、手軽にマルチストリームのWebRTC通信を行うことができます。お試しあれ。

- GitHub Pages で試す
    - [addTrack()/ontrack()、multi-stream時代の DataChannelシグナリング](https://mganeko.github.io/webrtcexpjp/basic2016/dc_signaling_multistream.html)
- GitHub でソースを見る
    - [github.com/mganeko/webrtcexpjp](https://github.com/mganeko/webrtcexpjp/blob/master/basic2016/dc_signaling_multistream.html)