Help us understand the problem. What is going on with this article?

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と同じものです。
詳細は以下参照。

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

getStats()の問題

どの統計値が通信状態に関係しているのかWebRTC入門者には分かりづらい

w3cを見れば取得できる統計情報一覧は書かれていますが、どの値取れば通信状況が悪いよ!的なフィードバックを促せるのかなどは一見しただけでは分かりません。

ブラウザによって実装が異なるので得られる統計情報が異なる

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

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

Chrome(76)の場合

[
    "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)の場合

[
    "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)の場合

[
    "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クラスがEventEmitterを継承しており、これを使って容易にstatsのイベントハンドリングが可能になります。(後ほど紹介します)
これにより、例えば音声のjitterが一定値を超えたときにイベントの発生を収集してユーザーのネットワーク環境情報を分析したり、ユーザーに感じられる通話品質によって引き起こされるストレスを軽減するためにUIに何らかのフィードバックを与えたりすることができます。

使い方

インストール方法などは省略。README.mdを見て下さい。

RTCStats standardizers

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

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やらパケットロス率やらの瞬間値を割り出してくれます。

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

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引数に指定してあげることでお好みの形で閾値の設定をカスタマイズできます。
書き方は以下のような感じ。

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-wrapperSkyWayが実験的に開発した外部ツールです。
そのため現在SkyWayのサポート対象ではありませんが、現時点でIssue,PR baseで改善していくつもりです。
rtcstats-wrapperぜひ使ってみてください!Issue、PRお待ちしてます。

monmee
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした