WebRTCの通信状況をプログラマブルに判別するライブラリを作ってみた

# rtcstats-wrapperとは？
- WebRTC統計情報のブラウザ標準化
- WebRTC統計情報の瞬間値計測
- WebRTC統計値の定期監視

を実現したRTCStatsのラッパーです。

リポジトリ: https://github.com/skyway-lab/rtcstats-wrapper
ドキュメント: https://skyway-lab.github.io/rtcstats-wrapper/index.html

# 背景
WebRTCでは、送受信されているメディアのパフォーマンスやネットワーク環境を監視するための統計情報を規定しています。

WebRTCを使用しているWebアプリ開発者なら、
1. ブラウザからWebRTC統計情報（Chromeならchrome://webrtc-internals）を確認して
2. dumpファイルをダウンロードして
3. https://fippo.github.io/webrtc-dump-importer/ にdumpファイルを投げてデバッグ...

みたいなことをやった経験があるかと思います。
で、1.の中身はw3cで規定されているAPIと同じものです。
詳細は以下参照。

- https://w3c.github.io/webrtc-stats/
- https://qiita.com/yusuke84/items/dc6e5a4ed109c4631b66

RTCStatsの統計情報は`RTCStatsReport`と呼ばれるMap-likeなオブジェクトに格納されていて、これを取得するメソッドとして`getStats()`があります。
「じゃあ`getStats()`叩けばかんたんにプログラム内から簡単に通信状況わかるやん！エンドユーザにリアルタイムでFBできたり...夢がひろがりんぐ！」とかなりそうなんですが、以下の問題があります。

## `getStats()`の問題
### どの統計値が通信状態に関係しているのかWebRTC入門者には分かりづらい
w3cを見れば取得できる統計情報一覧は書かれていますが、どの値取れば通信状況が悪いよ！的なフィードバックを促せるのかなどは一見しただけでは分かりません。

### ブラウザによって実装が異なるので得られる統計情報が異なる
そのまんまですが、`getStats()`を叩いて得られる`RTCStatsReport`がブラウザによって異なる問題。

例えば、`inbound-rtp`のAudioStream（kind="audio"である受信MediaStreamTrackに対応するStats）に関するオブジェクトをChromeとFirefoxで比較すると以下のようになります。


#### Chrome（76）の場合
```json
[
	"RTCInboundRTPAudioStream_407406518",
	{
		"id": "RTCInboundRTPAudioStream_407406518",
		"timestamp": 1566895151656.227,
		"type": "inbound-rtp",
		"ssrc": 407406518,
		"isRemote": false,
		"mediaType": "audio",
		"kind": "audio",
		"trackId": "RTCMediaStreamTrack_receiver_56",
		"transportId": "RTCTransport_0_1",
		"codecId": "RTCCodec_1_Inbound_111",
		"packetsReceived": 3,
		"bytesReceived": 309,
		"packetsLost": 0,
		"lastPacketReceivedTimestamp": 20156.675,
		"jitter": 0,
		"fractionLost": 0
	}
]
```

#### Firefox(68)の場合
```json
[
    "inbound_rtp_audio_2",
    {
        "id": "inbound_rtp_audio_2",
        "timestamp": 1564038672689,
        "type": "inbound-rtp",
        "kind": "audio",
        "mediaType": "audio",
        "ssrc": 4147765750,
        "jitter": 0,
        "packetsLost": 0,
        "packetsReceived": 8,
        "bytesReceived": 1448,
        "nackCount": 0
    }
]
```

#### Safari(12.1.1)の場合
```json
[
	"RTCInboundRTPAudioStream_3078060060",
	{
		"id": "RTCInboundRTPAudioStream_3078060060",
		"timestamp": 1566895329647,
		"type": "inbound-rtp",
		"codecId": "RTCCodec_1_Inbound_111",
		"isRemote": false,
		"mediaType": "audio",
		"qpSum": 0,
		"ssrc": 3078060060,
		"trackId": "RTCMediaStreamTrack_receiver_42",
		"transportId": "RTCTransport_0_1",
		"bytesReceived": 1097,
		"fractionLost": 0,
		"jitter": 0,
		"packetsLost": 0,
		"packetsReceived": 14
	}
]
```

結構違いますね。この差分をブラウザごとに標準化して、特定の値を抜き取る作業は大変です。

上記の問題を解決するために、rtcstats-wrapperが生まれました。

# webrtcstats-wrapperの特徴
##  クロスブラウザ対応（Chrome/Safari/Firefox）
背景で説明したとおり、getStats()の実装はブラウザごとに異なります。rtcstats-wrapperはRTCStatsをクロスブラウザで使用するためのshimとして機能します。

## getStats()の結果から得られる瞬間値が取得可能
特定の統計情報（ex. 受信音声のjitter buffer delayなど）の瞬間値をクライアント側から取得できます。

## EventEmitter対応
定期的に瞬間値を計算し、特定のしきい値を超えたときに何らかのアクションをとりたい、といったイベント駆動なユースケースに対応しています。
具体的には[RTCStatsInsight](#rtcstatsinsight)クラスがEventEmitterを継承しており、これを使って容易にstatsのイベントハンドリングが可能になります。（後ほど紹介します）
これにより、例えば音声のjitterが一定値を超えたときにイベントの発生を収集してユーザーのネットワーク環境情報を分析したり、ユーザーに感じられる通話品質によって引き起こされるストレスを軽減するためにUIに何らかのフィードバックを与えたりすることができます。



# 使い方
インストール方法などは省略。[README.md](https://github.com/skyway-lab/rtcstats-wrapper/blob/master/README.md)を見て下さい。

## RTCStats standardizers
`standardizeReport`メソッドをgetStats()の結果を引数に渡して使用すれば、StatsReportが標準化されます。


```javascript
import { standardizeReport, RTCStatsReferences } from 'rtcstats-wrapper';

const pc = new RTCPeerConnection();
//  ...

const report = standardizeReport(await pc.getStats());
const receiverStats = report.get(RTCStatsReferences.RTCVideoReceivers.key);
const framesDecoded = receiverStats[0].framesDecoded;
// ...
```



## RTCStatsMoment
`getStats()`で取得できるメトリクスのほとんどは総数です。「その瞬間、どんな値になったんか知りたいんや！」というニーズに`RTCStatsMoment`は応えてくれます。
それぞれの`RTCStatsReport`を標準化し、その値を内部でいい感じに計算して、rttやらパケットロス率やらの瞬間値を割り出してくれます。

以下の処理を書いてあげます。

```javascript
import { RTCStatsMoment } from 'rtcstats-wrapper';

// ...

const moment = new RTCStatsMoment();

const report = await pc.getStats();
moment.update(report);
moment.report();
//=> {
//    "send": {
//        "video": { ... },
//        "audio": { ... },
//    },
//    "receive": {
//        "video": { ... },
//        "audio": { ... },
//    },
//    "candidatePair": { ... }
//}
```
`moment.update(report)`　で`getStats()`により取得したreport の値を更新します。
`moment.report()`でその結果を出力します。

これを使ったアプリケーションのExampleを別リポジトリで作成しています。
どうやらビデオチャット中に通信状況が悪化した場合にリアルタイムでフィードバックコメントしてあげるReact製アプリみたいです。
https://github.com/skyway-lab/connection-status-viewer-example


## RTCStatsInsight
`RTCStatsMoment`で瞬間値を取得して、その値が閾値を超えたらアクションを促す...みたいなことを思いつくかもしれない。
`RTCStatsInsight`を使えばstatsの定期監視をより簡単に実装できます。
`getStats()` を定期実行してstatsの瞬間値を取得し、設定された閾値を超えたらイベントが発火します。
通信品質に影響するイベントを列挙した`RTCStatsInsightEvents`クラスと併用して使うと良いでしょう。
イベント一覧はこちらから確認してみてください。
https://skyway-lab.github.io/rtcstats-wrapper/global.html#RTCStatsInsightEvents

rtcstats-wrapperライブラリからもexampleを出していて、これがすべてなんだけど少し解説。
https://github.com/skyway-lab/rtcstats-wrapper/tree/master/examples/insight

streamを受け取ったときに以下の処理を書いています。

```script.js
import {
  RTCStatsInsightEvents,
  RTCStatsInsight
} from 'rtcstats-wrapper';

mediaConnection.on('stream', async stream => {
  // 相手streamのレンダリング（中略）

  // ここから
  const pc = mediaConnection.getPeerConnection();
  insight = new RTCStatsInsight(pc);

  for (const eventKey of RTCStatsInsightEvents.enums) {
    insight.on(eventKey, event => {
      console.log(event);
    });
  }
  insight.watch();
});
```

これだけで、`RTCStatsInsightEvents`のイベント監視ができます。

コードをざっくり説明すると、

1. `getPeerConnection()`を使って`RTCPeerConnection`を取得する
2. それを引数に`RTCStatsInsight`インスタンスを生成する
3. `RTCStatsInsightEvents`のイベント一覧からいずれかのイベントが発火したら`insight.on(eventKey)`で掴んでブラウザコンソールに出力する
4. `insight.watch()`で`getStats()`を定期実行、監視する

という流れです。

デフォルトで各イベントの閾値は設定されている（例えば`auido-rtt`イベントだと400ms以上で`unstable`レベルが、800ms以上で`critical`レベルが発火するよう設定されている）ので、オプション変数を記入しなくても動きます。
が、オプション変数を記述して`RTCStatsInsight`の第2引数に指定してあげることでお好みの形で閾値の設定をカスタマイズできます。
書き方は以下のような感じ。

```javascript
const options = {
  interval: 3000,
  thresholds: {
    'audio-rtt': {
      unstable: 0.05,
      critical: 0.3,
    },
  triggerCondition: {
    failCount: 2,
    within: 3,
  },
};

// ...

const pc = mediaConnection.getPeerConnection();
const insight = new RTCStatsInsight(pc, options);
```

options中に指定可能なプロパティは以下です。

- `interval` : `getStats()`のポーリング間隔(ms)。
- `thresholds`: 各イベントの`unstable`, `critical`レベルに対する閾値設定。
- `triggerCondition`: イベント発火条件の設定。試行回数`within`のうち引っかかった回数`failCount`の条件を達成すると発火する。


## まとめというか余談
`rtcstats-wrapper`は[SkyWay](https://webrtc.ecl.ntt.com/)が実験的に開発した外部ツールです。
そのため現在SkyWayのサポート対象ではありませんが、現時点でIssue,PR baseで改善していくつもりです。
`rtcstats-wrapper`ぜひ使ってみてください！Issue、PRお待ちしてます。