WebRTCの映像フロー制御の働きを実際に確かめてみた

今年の私のテーマは、**「SkyWayを利用する開発者の皆さんからの質問にお答えする」**というテーマで、カレンダー記事を何本か書きたいと思います。どうぞお付き合いください。

*この記事は [WebRTC Meetup Tokyo #22](https://atnd.org/events/108171) の発表資料を公開記事用に加筆修正し、2019/12/1に全体公開したものです。予めご了承ください。*

# こんなお悩みがよく寄せられます…

WebRTCを使ったP2P通信のビデオチャットアプリを作っていると、こんな悩みはありませんか？

- 開発環境だと何に問題もなく実装できるのに、実際の環境に導入したら問題発生
- 映像の品質が悪い
- 長時間接続していると映像が乱れる
- 音が途切れる

などなど…。

# WebRTCは実環境に導入したら不確定要素だらけ

P2P通信の場合、その通信が上手くいくかどうかは、利用者の端末(PCやスマホ)、ネットワーク環境に依存する部分が多いです。サービス開発者がアプリケーションとして正しく動くものを作っていても、意図した動作をしない…という事はよくあります。

不確定要素でよく問題となるのが、ビデオチャットを提供した場合の映像品質です。品質が低下すると視認性が下がってしまいます（下記図参照）。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/6651/1222c2a4-7542-d385-f79e-cdce05c04cba.png)

映像品質は、P2P通信の場合（TURNサーバ利用を含む）、完全に利用者のネットワーク環境に依存するため、残念ながら、サービス開発者からコントロールできないことが多いです。では、品質は完全にネットワーク環境任せなのか？というとそういうわけでも有りません。

WebRTCでは、パブリックなインターネット回線を利用して映像トラフィックをやり取りすることを考慮し、回線帯域をフェアに分け合えるように独自のフロー制御機構が搭載されています。このフロー制御の仕組みを知っておくと、映像品質が劣化した際に、どういう事が起きているのか？ということをある程度想像できるようになります。絶対とは言えませんが、品質問題にぶち当たった時に解決の糸口になる可能性もあります。

この記事では、Chromeの実装を利用して、フロー制御がどのように動作しているのかを解説します。

# Google Congestion Control

libwebrtc(オープンソースのWebRTCエンジン)を利用している場合は、Googleが開発したWebRTC用のフロー制御アルゴリズムGoogle Congestion Controlに基づき、フロー制御が行われ、インターネットを含むネットワークの状況に応じた、品質調整が行われています。

アルゴリズムの詳細はこの記事では割愛しますが、興味があるか方はRFCを御覧下さい。

- 詳しくはRFCを参照
    - https://tools.ietf.org/html/draft-ietf-rmcat-gcc-02

- 日本語で要約された記事もあります
    - WebRTC 映像フロー制御の仕組み - Google Congestion Control
        - https://qiita.com/komasshu/items/1cb5d4469595a635c689

尚、ドラフトについては、2016年当時の情報なので、現在同じアルゴリズムが使われているかどうかは分かりませんが、アルゴリズムを要約すると…

> * 受信側ではジッタベースの推定
> * 送信側ではロスベースの推定
>
> をします。そして、これらの値を比較することで最終的な送信可能帯域推定値が決定されます。このアルゴリズムは非常に単純で、
> 
> **互いの推定値のうち、低い値とする**

- **ジッターベースの推定とは？**

ジッター(送信時のパケット送信間隔と受診時のパケットの送信間隔の差分)を計測し、この値がある閾値を越える=ネットワークが輻輳する予兆と判断し、送信可能帯域推定値を下げる

- **ロスベースの推定とは？**

パケットロスを計測し、ある閾値以上になると送信可能帯域推定値を下げる、ある閾値以下になると送信可能帯域推定値を上げる

- **最終的に…**

ジッターベースで推定した送信可能帯域推定値とロスベースで推定した送信可能帯域推定値を比較し、小さい値を基に、メディアの品質を落とし送信帯域を下げていく

となります。
https://qiita.com/komasshu/items/1cb5d4469595a635c689 より抜粋＆要約


# 実際のところどうやって動くの？

ということで、理論はこのくらいにして、実際にどう動くのかを調査してみました。


## 調査環境と調査方法

### 端末
  - macOS版 Chrome M76
  - Android版 Chrome M76

注意: 元ネタが2019/9の記事なので公開時の最新バージョンではありません。

### アプリ
  - SkyWayのMeshRoomアプリ(P2P通信)
    - https://example.webrtc.ecl.ntt.com/room/index.html#mesh
        - getUserMediaでvideoの品質に関する
### ネットワーク
  - 同一のLAN内に接続

### ネットワーク環境のエミュレート方法

- dnctl -- Traffic shaper control program
- pfctl — control the packet filter (PF) device

これらのツールを使って、macOS上で帯域制限、パケットロスが発生する環境を作り、Android端末から受信した映像の品質を確認します。

#### 例: 設定例

- dummynet(トラフィックシェーパー)のpipeに再現させたい状況を設定

> $ sudo dnctl pipe 1 config bw 500kbit/s // 帯域制限
> $ sudo dnctl pipe 2 config plr 0.1  // パケットロス率
> $ sudo dnctl pipe 3 config delay 500ms // 遅延

- 全てのUDP通信(IN/OUT)のトラフィックにpipeに設定した状況を適用

> $ sudo vi /etc/pf.conf
> 
> dummynet in proto udp all pipe 1
> dummynet out proto udp all pipe 1
> 
> $ sudo pfctl -f /etc/pf.conf
> $ sudo pfctl -e

# 調査結果

*実際の通信環境、端末環境に依存するためこの結果のとおりにならない可能性もあります。一例ということで御覧下さい。*

## 0. 何もしない場合

PCで受信したビデオストリームの統計情報はこちらです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/6651/80a0dca1-90c6-e1e4-f194-c679f8b15fa4.png)

## 1. 帯域制限をかけてジッターの変化を起こしてみる

### 1000kbpsに制限

制限したタイミングでジッターの値がはねて、そのタイミングで、リモートから受信した映像の解像度、フレームレートが一時的に低下しています（だいたい青丸の所）が、その後回復しました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/6651/66f0bd06-2a7f-4547-5532-069fe86d6c96.png)

### 500kbpsに制限

制限したタイミングで、リモートから受信した映像の解像度が下がり始めました。

<img width="532" alt="スクリーンショット 2019-12-01 13.33.16.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/6651/d2fc4468-2b60-8c9a-181c-e9572faa6d88.png">
*framesReceivedとframesReceived/sのグラフは取得していなかったのでありません。*


### 250kbpsに制限

250kbpsに制限すると、ジッターに加えて、パケットロスやRTTにも影響が出始めました。リモートから受信した映像の解像度やフレームレートは不安定に上下を繰り替えしていますが、映像自体は殆ど動きがない状態になります。実際の帯域は5byte/s(40kbps)前後でビデオチャットを成立させるのは難し状況です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/6651/24dbc9d8-4948-b9b9-2ba3-6d2640e69baf.png)

尚、この後、100kbpsまで制限をきつくしたところで、WebRTCのコネクションが切断されてしまいました。

## 2. パケットロスを発生させてみる

帯域制限の時と同じ条件で、今度はパケットロスのみを発生させてみます。

### ロス率5%

パケットロスの累積パケット数は増えますが映像品質への影響はほぼ見受けられません。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/6651/54aa3d14-d6d9-e147-adee-9fe3caf0db6f.png)

### ロス率10%

10%でも5%と同様の結果です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/6651/edb8ea23-a52d-1d53-1c7a-b30018bc96bf.png)

### ロス率 20%

20%に増やしたところで、リモートから受信した映像の解像度、フレームレートが下がりました。フレームレート自体はその後回復しているように見えますが、解像度はおちたままです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/6651/3159de99-b249-5a83-486c-42b918584579.png)
*packetLossグラフの青丸の位置は、パケットロスの変化量が増えたあたりについていますが、制限を変更したタイミングは、解像度が変化したタイミングとなります。*


### ロス率 50%

フレームレートは不安定に上下していますが、解像度に変更はありません。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/6651/e3f1d88d-7c7a-99ed-f976-26b52cd389cb.png)

### 制限解除

最後にパケットロスの制限を解除させてみます。そのタイミングでリモートから受信した映像の解像度は元の解像度に戻り、その他のグラフも安定するようになりました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/6651/a3756b74-2881-3006-5d85-b49e5ff362a4.png)

省略してしまいましたが、先に述べた帯域制限についても、制限解除すると同様に安定した状態に復活することを確認済みです。

---

#　分かった事 

- ジッターの変化（正確には帯域制限からのジッターの変化）とパケットロスの変化に伴い、受信側のビデオ解像度（WidthとHeight）が変化する

ネットワーク環境悪化に伴い解像度は小さくなり、通信環境がよくなると元の解像度に戻ります。ネットワーク環境が実際の映像品質に影響を与えていることがわかりました。

- 解像度が低下する場合、**300ピクセル x 220ピクセル 以下には下がらない**

これは、実装依存だと思いますが、今回の環境では 600ピクセル✗500ピクセル ~ 300ピクセル✗220ピクセル の間で解像度が変動しました。元の解像度によっても変化のステップや劣化した場合の最終的な解像度は変わる可能性がありますが、どんな時も状況に応じてリニアに解像度を変化させるような実装にはなっていないようです。

---

# 今回計測した情報を自身のサービスに活かしたい場合

今回資料中で紹介したグラフは、chrome://webrtc-internalsの情報の抜粋です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/6651/9f2bc0d4-7aa8-def2-0352-524d0512e04b.png)
参考: [Chrome M75から chrome://webrtc-internals が新しくなります](https://qiita.com/yusuke84/items/428affe75affa16f104d)

WebRTCの**統計情報API**を利用することで同様の情報が取得できるため、統計的な通話品質の分析などに使ってみるのも良いかもしれません。SkyWayで統計情報APIを利用する場合は、JavaScript SDKに限った話ですが、 [RTCPeerConnectionを取得するAPI](https://webrtc.ecl.ntt.com/skyway-js-sdk-doc/ja/mediaconnection/#getpeerconnection)があるため、そちらを利用します。

具体的には、RTCPeerConnectionに生えている、**getStats()**というAPIを実行してレポートを取得します。そのまま叩くのはなかなかしんどいので、[rtcstats-wrapper](https://github.com/skyway-lab/rtcstats-wrapper)を使ってみてください。

使い方の記事[WebRTCの通信状況をプログラマブルに判別するライブラリを作ってみた](https://qiita.com/monmee/items/77310482b0a6a034a887)も公開されているため、御覧下さい。