最新のWebRTCのtrack系処理を、手動＋データチャネルシグナリングで観察してみる

# はじめに

2018年のWebRTCはJavaScriptでもメディアをtrack単位で扱う必要があります。Chrome 65 / Firefox 59で動きを確認してみました。(2018.03.14時点)

# track単位の処理

- PeerConnectionにローカルメディアを追加するには、peer.addTrack()
    - → peer.onnegotiationneeded() が発火する
- リモートメディアのトラックが追加された時のイベント
    - peer.ontrack() ... 各トラックごとに発火する。今後はこれを扱うのが良さそう
    - mediastream.onaddtrack() ... 2つ目以降のトラックでは発火する
    - peer.onaddstream() ... （まだ）1つ目のトラックの時には発火する
- PeerConnectionからローカルメディアを除去するには、peer.removeTrack()
- リモートメディアのトラックが除去された時のイベント
    - mediastream.onremovetrack() ... 各トラックごとに発火する。今後はこれを扱うのが良さそう
    - peer.onremovestream() ... Firefox 59では発火しない。Chrome 65ではトラックがゼロになると（まだ）発火する

いまは複数のイベントが同時に発火しますが、今後はpeer.ontrack()とmediastream.onremovetrack()をハンドリングするのが良いと見ています。

# コード例

```js:トラック追加
const videoTrack = localStream.getVideoTracks()[0];
const videoSender = peer.addTrack(videoTrack, localStream);
const audioTrack = localStream.getAudioTracks()[0];
const audioSender = peer.addTrack(audioTrack, localStream);
```

```js:トラック除去
peer.removeTrack(videoSender);
peer.removeTrack(audioSender);
```

```js:リモートトラック追加/除去時の処理
peer.ontrack = function (event) {
  const track = event.track;
  const 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除去時の処理
    }
  };
};
```

# 動かして見る

WebRTCのtrack系のAPIを使ったサンプルをGitHub Pagesに用意しました。手動シグナリングをベースにしているので、自分でシグナリングサーバーを立てる必要はありません。

- [https://mganeko.github.io/webrtcexpjp/basic2016/datachannel_signaling_track.html](https://mganeko.github.io/webrtcexpjp/basic2016/datachannel_signaling_track.html)

また、ソースはこちらになります。
- [https://github.com/mganeko/webrtcexpjp/blob/master/basic2016/datachannel_signaling_track.html](https://github.com/mganeko/webrtcexpjp/blob/master/basic2016/datachannel_signaling_track.html)

（コードはグローバル変数使いまくりなのですが、きれいに書くのはみなさんにお任せします..）

## 準備

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

PC版のChrome 65同士、Firefox 59同士、Chrome 65 と Firefox 59 の間での動作確認をしています。原因はわかっていませんが、Safariではうまく動いていません。

## DataChannelの接続まで

手動シグナリングを使って、DataChannelの通信を確立します。

![hand_datachannel_step.png](https://qiita-image-store.s3.amazonaws.com/0/19403/54920804-b00f-ea6c-45f0-8364c8c55a49.png)

- 1. 片方のブラウザA（左）で [Connect Datachannel by hand]ボタンをクリック
    - Offer SDP が生成される
- 2. ブラウザA（左）の[SDP to send]のテキストが選択されているので、そのままコピー
- 3. もう一方のブラウザB（右）で、[SDP to receive]の領域にペースト
- 4. ブラウザB（右）の[Receive remote SDP]ボタンをクリック
    - リモートのOffer SDP をセットし、Answer SDPが生成される
- 5. ブラウザB（右）の[SDP to send]のテキストが選択されているので、そのままコピー
- 6. ブラウザA（左）で、[SDP to receive]の領域にペースト
- 7. ブラウザA（左）の[Receive remote SDP]ボタンをクリック
    - リモートのAnswer SDP をセット
- 8. 両方のブラウザのコンソールに、"datachannel open" のログが出ていれば接続OK
- 9. どちらかのブラウザの[Send Hello]ボタンをクリックすると datachannel でメッセージを送信
- 10. 反対側のブラウザのコンソールにメッセージのログが表示される

## メディアトラックの追加、除去

この後はUserMediaの取得→ addTrack()/removeTrack() → DataChannel経由でSDPを交換して再シグナリング、を行います。

![media_ontrack.png](https://qiita-image-store.s3.amazonaws.com/0/19403/534021b6-ae27-7784-abc3-02e234362a14.png)


### メディアの取得

- 1. どちらか一方、または両方のブラウザで[Start Media]ボタンをクリックして、カメラの映像/マイクの音声を取得
    - ※この際に [use Video][use Audio]のチェックを外すと、映像/音声を取得しません

### メディアトラックの追加

- 2. 映像を取得したブラウザで[add Video]ボタンをクリック
    - peer.addTrack()でビデオトラックを追加、戻り値のsenderを保持
    - peer.onnegotiationnneded()が発火
    - DataChannel経由で、Offer/Answerが交換される
- 3. 反対のブラウザに映像が伝送、表示される
    - peer.addTarck()イベントが発火
    - 2つ目以降のトラックだった場合は、mediastream.onaddtrack()も発火
    - video要素で、映像を再生
- 4. 音声を取得したブラウザで[add Audio]ボタンをクリック
    - peer.addTrack()でオーディオトラックを追加、戻り値のsenderを保持
    - peer.onnegotiationnneded()が発火
    - DataChannel経由で、Offer/Answerが交換される
- 5. 反対のブラウザに音声が伝送、表示される
    - peer.addTarck()イベントが発火
    - 2つ目以降のトラックだった場合は、mediastream.onaddtrack()も発火
    - audio要素で、音声を再生

※今回のサンプルでは、わかりやすくするためにVideoとAudioを別々の要素で再生しています。またビデオ/オーディオを追加するブラウザは同じでも異なっても良く、さらに双方向でも追加できます。


### メディアトラックの除去

- 映像トラックを追加（送信）しているブラウザで、[remove Video]ボタンをクリック
    - peer.removeTrack()に対応するsenderを渡す
    - peer.onnegotiationnneded()が発火
    - DataChannel経由で、Offer/Answerが交換される
- 反対側のブラウザで映像が停止する
    - mediastream.onremovetrack()が発火
    - Chromeの場合には最後のトラックが除去されるタイミングで peer.onremovestream()も発火
    - video要素の再生を停止
- 音声トラックを追加（送信）しているブラウザで、[remove Audio]ボタンをクリック
    - peer.removeTrack()に対応するsenderを渡す
    - peer.onnegotiationnneded()が発火
    - DataChannel経由で、Offer/Answerが交換される
- 反対側のブラウザで音声が停止する
    - mediastream.onremovetrack()が発火
    - Chromeの場合には最後のトラックが除去されるタイミングで peer.onremovestream()も発火
    - audio要素の再生を停止


# 終わりに

JavaScriptのWebRTCでもtrack単位での処理が必須になってきました。どんなイベントが発生しているか、手動シグナリング＋データチャネルシグナリングで手軽に動作を確認してみてはいかがでしょうか？