rtcstats-wrapperとChart.jsでWebRTC統計情報を可視化してみた

## これはなに？
 
前回の記事

https://qiita.com/monmee/items/77310482b0a6a034a887


で紹介したrtcstats-wrapperですが、具体的にどう使えるん？みたいなところは紹介してなかったので、一例を簡単に作ってみた記事。

rtcstats-wrapperの想定される使い方として
- 例えば、アプリ内にwebrtc-internal的な統計グラフを表示、ユーザに通信状況をFBする
- 例えば、パケットロス率が異常値に達したらSlackに通知するなどのWebRTC通信の監視体制を作る
- 例えば、取得した統計情報とユーザIDを紐付けて、各ユーザの通信環境パフォーマンスの解析材料とする
などがあります。

今回は一番上の

> 例えば、アプリ内にwebrtc-internal的な統計グラフを表示、ユーザに通信状況をFBする

をやってみました。

## できたもの
Gistにソースコードをupしています。
とりあえず動かしたれ！精神でやってる気合のコードでお見苦しいところもあるかと思いますが参考程度に見てください。

[index.html](https://gist.github.com/monmee/f6efa56ae39e8c18bff764da9aa5283c)
[sciprt.js](https://gist.github.com/monmee/8c745a9724c2d0a0bd70257e41ab4791)
[config.js](https://gist.github.com/monmee/8c745a9724c2d0a0bd70257e41ab4791)


以下画像が完成物のガワです。
![](https://i.imgur.com/qqd5Wgy.png)

左がSafari、右がFirefoxです。定期的にstatsを取得してグラフをリアタイで動かしてます。
画像では受信/送信トラフィックのみ表示してますが、rtcstats-wrapperから取得できる値であれば他の統計情報も取れます。（本当に取れるかは各ブラウザの`getStats()`実装状況によるけど）

ミニ `chrome://webrtc-internals` が他ブラウザでも見れるという感じです。

## 必要なもの

### rtcstats-wrapper
各ブラウザの`getStats()`を標準化して統計情報の瞬間値を計測してくれるラッパー。

### WebRTCアプリ
rtcstats-wrapperは現在ブラウザP2Pのみ対応です。
[SkyWayのサンプルアプリ](https://github.com/skyway/skyway-js-sdk/tree/master/examples/p2p-media)を使いました。

### データ可視化ライブラリ
今回使った外部ライブラリはこれ。
- [Chart.js](https://www.chartjs.org/)
- [chartjs-plugin-streaming](https://nagix.github.io/chartjs-plugin-streaming/ja/)
- [moment.js](https://momentjs.com/)

時系列データをリアルタイムにブラウザに出力できれば何でも良いと思います。
例えば、グラフ描画系ならChart.js以外にも、[d3.js](https://d3js.org/)、[Google Chart](https://developers.google.com/chart)らへんが候補としてありそうです。
今回はリアルタイムデータ向けChart.jsプラグインのchartjs-plugin-streamingをChart.jsと併用しています。

時間軸がスムーズに動かなかったりDeprecated Warningがコンソール表示されるあたり、chartjs-plugin-streamingはChart.js v2.8.0以降に対応してなさそうなので、今回はChart.js v2.7.3を使用しました。

Chart.jsで時系列グラフを作るために[moment.js](https://momentjs.com/)も必要らしいのでそちらも必要。




## 前準備
使用するライブラリをダウンロードします。
Vanilla JSでimport/export文を使えるように最後の2ファイルには`type="module"`を追加しています。
config.jsについては後述します。

```html:index.html
<script src="/node_modules/rtcstats-wrapper/dist/rtcstats-wrapper.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.24.0/moment.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js@2.7.3/dist/Chart.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chartjs-plugin-streaming@latest/dist/chartjs-plugin-streaming.min.js"></script>
<script src="//cdn.webrtc.ecl.ntt.com/skyway-latest.js"></script>
<script src="../_shared/key.js"></script>
<script type="module" src="./script.js"></script>
<script type="module" src="./config.js"></script>
```

## サンプルアプリ上にChartを生成する
兎にも角にもChartを作ります。
詳しくはChart.js公式ドキュメントをご覧になってください...なんですが、Chartを生成するためには
- canvas要素を作成する
- canvas要素とChart用の設定オブジェクトを引数に渡してnew Chart()する
必要があります。

この処理をサンプルコードに追加します。

```html:index.html
<div class="chart" id="js-chart"></div>
```

```javascript:script.js
const Chart = window.Chart;

// ...

const ctx = document.createElement('canvas');
ctx.id = 'canvas_' + key;
chartArea.appendChild(ctx);

const chart = new Chart(ctx, value);
```

Chart用configはまだ作っていないので、まだこのコードは動きません。
`config.js`というChart用設定ファイルを作っています。

### Chart用設定ファイルを作成する
Chart.jsとchartjs-plugin-streamingの公式ドキュメントに従って書いていきます。

```javascript:config.js

// 簡略化のため一部のみ抜粋

const defaultConfig = {
  type: 'line',
  data: {
    datasets: [],
  },
  options: {
    title: {
      display: true,
    },
    scales: {
      xAxes: [
        {
          type: 'realtime',
          time: {
            unit: 'minute',
          },
        },
      ],
      yAxes: [
        {
          ticks: {
            beginAtZero: true,
            sampleSize: 5,
          },
        },
      ],
    },
    plugins: {
      streaming: {
        duration: 180000, // 180000ミリ秒（5分）のデータを表示
        refresh: 5000,
        frameRate: 5,
      },
    },
  },
};

const RECEIVED_TRAFFIC = deepCopy({}, defaultConfig);

RECEIVED_TRAFFIC.data.datasets = [
  {
    label: 'audio',
    data: [],
    backgroundColor: 'rgba(0, 0, 0, 0)',
    borderColor: 'rgba(255, 99, 132, 0.5)',
    pointRadius: 1,
  },
  {
    label: 'video',
    data: [],
    backgroundColor: 'rgba(0, 0, 0, 0)',
    borderColor: 'rgba(54, 162, 235, 0.5)',
    pointRadius: 1,
  },
];
RECEIVED_TRAFFIC.options.title.text = '受信ビットレート(Kbps)';
export const config = {
  RECEIVED_TRAFFIC,
  // ...
}

function deepCopy(src, dst) {
  return Object.assign(src, JSON.parse(JSON.stringify(dst)));
```

大事なのは`defaultConfig`の設定部分。
webrtc-internalライクなチャートを表示したいのであれば、横軸は時間軸、縦軸は統計値のリアルタイム折れ線チャートを作る必要があるため、その設定を書きます。
あとは取りたい各統計情報に`defaultConfig`をディープコピーして、それぞれデータラベルや折れ線の色を調整したりタイトルを設定したりするなどの細かい作業です。

## 描画処理を書く
先程の設定ファイルをimportして、描画処理を書きます。
`updateChart` 関数がそれに当たります。

```javascript:script.js
// 簡略化のため一部のみ抜粋
import { config } from './config.js';

const { RTCStatsMoment } = window.RTCStatsWrapper;
const Chart = window.Chart;
const Peer = window.Peer;

const charts = new Map();
let timerId;

(async function main() {
  const chartArea = document.getElementById('js-chart');
  
  // ...
  // New each charts for WebRTC stats
  for (const [key, value] of Object.entries(config)) {
    const ctx = document.createElement('canvas');
    ctx.id = 'canvas_' + key;
    chartArea.appendChild(ctx);

    const chart = new Chart(ctx, value);
    charts.set(key, chart);
  }

  // Register caller handler
  callTrigger.addEventListener('click', () => {
    // ...
    mediaConnection.on('stream', async stream => {
      // ...
      // Update chart for stats
      timerId = _updateCharts(mediaConnection);
    });

    mediaConnection.once('close', () => {
      // ...
      // Stop drawing for stats
      clearInterval(timerId);
    });

    closeTrigger.addEventListener('click', () => {
      // ...
      clearInterval(timerId);
    });
  });

  peer.once('open', id => (localId.textContent = id));

  // Register callee handler
  peer.on('call', mediaConnection => {
    // ...    
    mediaConnection.on('stream', async stream => {
      // ...
      timerId = _updateCharts(mediaConnection);
    });

    mediaConnection.once('close', () => {
      // ...
      clearInterval(timerId);
    });

    closeTrigger.addEventListener('click', () => {
      // ...
      clearInterval(timerId);
    });
  });

  // ...

  async function _updateCharts(mc) {
    const moment = new RTCStatsMoment();
    const peerConnection = await mc.getPeerConnection();
    return setInterval(async () => {
      const stats = await peerConnection.getStats();
      moment.update(stats);
      const report = moment.report();

      for (const [key, chart] of charts) {
        switch (key) {
          case 'RECEIVED_TRAFFIC':
            chart.data.datasets[0].data.push({
              t: new Date(),
              y: report.receive.audio.bitrate / 1024, // bps -> Kbps
            });
            chart.data.datasets[1].data.push({
              t: new Date(),
              y: report.receive.video.bitrate / 1024,
            });
            break;
          case 'SENT_TRAFFIC':
          // ...
            
          default:
            console.warn('No value!');
            break;
        }
        chart.update();
      }
    }, 5000);
  }
})();

```

引数にMediaConnectionを持っていたり、関数名と直接関係のない処理を書いていたり、計算処理がハードコードだったりと、かなりイケてないですがやりたかったことを伝えると...。
仕組みとしては、
1. 取得したchartをMap Objectに格納し
2. 5秒ごとに`getStats()`し
3. 標準化したStatsの瞬間値を取得し
4. for-switch文でchartの種類を判別して
5. それぞれ適したデータ`{t, y}`をchartのdataにPush

しています。この処理により、各statsごとにリアルタイムチャートを表示させることが可能です。


## まとめというか感想
- 雑なQiitaになってしまいすみませんでした！
- 各ブラウザアプリ内にstatsグラフを表示させることは可能です
- chartjs-plugin-streamingがここ数ヶ月メンテされていない雰囲気なので運用レベルで可視化を考えるなら他のツールのほうが良いかも