WebRTC 映像フロー制御の仕組み - Google Congestion Control

この記事は、WebRTC Advent Calendar 6日目の記事です。

WebRTC では、映像トラフィックにより他のインターネットトラフィックとフェアに回線の帯域を分け合うように、独自のフロー制御(混み合ってきたら、映像品質を下げることで、使用帯域を下げる)の仕組みが入っています。この仕組みを知っていると、ネットの混雑状況に対して、WebRTCの映像品質がどのように変動するかを推測することができるようになります。

この記事では、そのフロー制御として Google Congestion Control の仕組みを紹介します。

そもそもなんで、WebRTCに独自のフロー制御が必要なの?

そもそもネットワークの混雑(これを輻輳といいます)はどうして起こるのでしょうか。

image.png

上図のように、左にある二つの回線から入ってきたデータ(パケット)が、右にある一本の回線へと出て行く系を考えます。このとき、青・赤のそれぞれのパケットは、ルーターに到達すると、ルーター内のメモリに一旦蓄えられ、出力側の回線に互い違いに出力されていきます。

これは三叉路での車の交通と同じで、左の二本の道路から車がずっと来ると渋滞が起こるように、ネットワークでもメモリにパケットが常に溜まる状況となり、流量の低下、さらにメモリの許容量を超えてしまうと、パケットのロスがおきます。(道路の場合は、とんでもない渋滞となるだけで、車が消えるわけでは無い点が違いますね)

インターネットでは、これを防ぐために、TCP によるフロー制御が行われます。具体的には、受信側でパケットのロスが起きると、送信側でパケットの送信量を下げ、パケットのロスが起きないようにします。

image.png

模式的に書くと、上図のような感じです。こんな感じで、送信側で送信量を減らすことで、routerでの輻輳を防ぐ仕組みになっています。

ただ、この仕組みには弱点があります。それは、一方のデータがフロー制御をしないと もう一方のデータは、もっと送信量を下げなきゃ!と勘違いして極端に送信データ量を下げてしまいます。

image.png

模式的に書くと、上図のような感じ。青色のパケットが「俺はフロー制御なんかしねー」ってやると、赤色だけが「まだ混んでるみたいだから下げなきゃ・・・」と送信流量をどんどん下げて行ってしまい、青色パケット君だけが得する形になってしまいます。悪いものがちですね、良い子はデータを流せなくなっちゃう(実際、最近のサイバー攻撃は、もっぱらこの手法が使われているようです)

で、この悪い子パケットを生み出すもっとも簡単な方法がUDPです。UDPでは、プロトコルとしてフロー制御の仕組みをもっていないため、何もしないと青色パケット君になります。

ここで、WebRTCのデータ転送にはUDPが使われています。これは、映像通信にはTCPは適していないとか、TCPだとP2PでのNAT越えができないとかといった理由なのですが、このためなーーーーんにもしないと、インターネット上で WebRTC は青色パケットくんの振る舞いをして、無茶苦茶な状況を作ってしまいます。残念。

なので、WebRTCでも独自のフロー制御の仕組みを入れましょうということがやられています。輻輳を検知したら、送信側で送信量を下げることで、赤色パケットくんとフェアにインターネットを譲り合って使いましょうという感じ。

前置きがやたら長くなってしまいましたが、以下で紹介する GCC (Google Congestion Control) は、このフロー制御のアルゴリズムになります。

GCCでは、パケットのロスよりも、ジッタの変化に着目する

先に、「TCPでは、パケットのロスを検出すると・・・」と書きましたが、GCC では、よりエレガントな方法で輻輳を検出します。以下に示すジッタの変化を観測することで、輻輳を検出しフロー制御を行います。これにより

「パケットがロスする前に、輻輳の予兆を検出して、送信データ流量を下げる」

というかっこいいフロー制御が実現されます。

ここで、なぜジッタ(パケット到達間隔の変動)の変化が輻輳予兆になるかを説明します。一番最初の図に戻りますが、

image.png

輻輳が起こり始めると、送出側の回線では上手に示すように、それぞれのデータフローのパケット間隔が長くなります(三叉路の例で考えるとイメージしやすいと思います。他の車が間に入っちゃうので、間隔が長くなる)。これにより、輻輳が起こり始めると、送信した時のパケット間の間隔(上の図で言うと ΔT )と、受信した時のパケット間の間隔(上の図で言うと Δt)に変動が起こります。

このため、WebRTCでは、この変動差(簡単に言うと、上図の Δt - ΔT。)をベースにして輻輳を検出し、フロー制御を行います。

(ちなみに、Internet Draftでは、jitter という言葉は使っておらず、delay-based control という定義をしています。本稿では、説明の簡便のためジッタという単語を用います)

どれぐらいのジッタになると、フロー制御が起きる?

GCCでは、受信側端末でパケットを受信すると、

  • RTPのヘッダ情報から送信時のパケット送信間隔(ΔT
  • 受信した時のタイムスタンプ情報から受信時のパケット到達間隔(Δt

より、ジッタ(ΔT - Δt)を算出し、これをベースに輻輳検知を行い、送信可能帯域の推定を行います。

image.png

ここで、ジッタの値(正確には、ジッタの値よりKalman Filterの演算を行い、ジッタ内に含まれる輻輳により発生したジッタの値を推定した値。観測値ジッタには、上図で示した以外の理由で発生するジッタも含まれているのでそれを確率的に除外したもの)が、ある閾値以上になると輻輳状態と判断します。

さて、その閾値ですが、これは一定の値ではなく、その時々のジッタの値に依存して、動的に変化する値となっています。初期値は 12.5 msec となっており、閾値越えを検出すると増加、検出されないと減少していきます( 6msec600msecの範囲で変動)。そして、この閾値越えが 10msec 以上続くと、輻輳と判定し送信可能帯域推定値を下げていきます。

ジッタの値は、getStats() で、jitter として得ることができます。前述のように、そこから演算処理した値をもとに閾値判定しますので、直接的に判断することはできないのですが、jitter の値が 十数ミリ秒を越えると映像帯域の減少が起こり始める と考えるといいと思います(これにともない、映像のフレームレートやサイズが変動しますので、かくかくしたりぼやっとした映像になります)

パケットロスは見てないの?

ここまで、GCCでは ジッタをベースにした輻輳制御 と説明しましたが、輻輳制御して送信帯域を下げて行ってもパケットロスが起こるときは起こります。このため、GCCでは、パケロスとジッタの双方のバランスより 最終的なフロー制御を行うアルゴリズムとなっています。

image.png

具体的には、送信側では受信側よりレポート(前述のジッタベースの推定値や、パケットロス情報など。RTCPにより送られる)を受信すると、そのパケットロス情報より送信可能帯域推定を行います。

その閾値ですが、こちらは受信側のジッタベースアルゴリズムと比較すると非常に単純で、

  • パケロスが10%以上になると、送信可能帯域推定値を下げる
  • パケロスが2%以下であれば、送信可能帯域推定値を上げていく

というものになっています。割と鈍感ですね(10%以上のロスってかなりの状態なので)。

最終的な送信可能帯域推定値の決定

これまで見てきたようにGCCでは

  • 受信側ではジッタベースの推定
  • 送信側ではロスベースの推定

をします。そして、これらの値を比較することで最終的な送信可能帯域推定値が決定されます。このアルゴリズムは非常に単純で、

互いの推定値のうち、低い値とする

というものです。

その他

以上、WebRTCのフロー制御アルゴリズム GCC (Google Congestion Control)について解説しました。

本稿では、Internet Draftに書かれている仕様のうち、特に重要と考えられる部分をピックアップして紹介していますし、説明の簡単のために独自の解釈に基づいた若干正確性に欠ける解説となっています。興味のある方は、原文 https://tools.ietf.org/html/draft-ietf-rmcat-gcc-02 を参照ください。

GCCは、ChromeのM23より実装されており、フィードバックより逐次アルゴリズムの改善が図られています。今回の記事であげた各種値(例えば、パケロスは 10% から考慮を始める)は、今後変更となる可能性がありますので、その点はご注意ください。