libwebrtc ペーサー

注意

この資料の正確性を保証しません。

重要

この資料についての問い合わせは Sora のサポート範囲には含まれません。

この資料は libwebrtc M104 (5112) 時点の資料です。

このドキュメントでは libwebrtc でのペーサーの処理について記載しています。 ペーサーは「RTP パケットの送信ペースを調整し、指定されたビットレート要求を満たすようにする」役目を担っているコンポーネントです。

なおペーサー関連の話の大枠については、libwebrtc のリポジトリで提供されている Paced Sending というドキュメントによくまとまっているので、 理解を深めるためには、そちらに先に目を通しておくことをお勧めします。

注釈

今回取り上げているクラスは、2022 年 7 月現在でも継続的に修正が行われています。 そのため、今後の libwebrtc のバージョン更新に伴い、処理内容が変わる可能性は十分にあり得ます。

概要

  • ペーサー関連のコードは webrtc/src/modules/pacing/ ディレクトリ以下にまとまっている

  • その中でペース調整ロジックを提供している中心的なクラスは PacingController:

  • PacingController のペース制御アルゴリズムは リーキーバケット に基づいている

    • リーキーバケットとは「穴の空いたバケツ」のこと

    • このアルゴリズムでは、水の流量(= パケットの送信ペース)は、バケツ内の水の容量(= キュー内のパケットの合計サイズ)には左右されず、その穴の大きさ(= 指定ビットレート)によって制御されることになる

    • PacingController は「送信済みパケットサイズ」および「経過時間」、「ターゲットビットレート」をもとに「バジェット(現在の送信可能サイズ)」を計算し、その範囲内でパケットの送信を行う

  • デフォルトでは、音声パケットはペース調整の対象外となっており、常に即座に送信される

  • RTP パケットには、その種類に応じた優先度が設定されている:

    • バジェットに余裕ができた際には、優先度が高いパケットから先にキューから取り出されて送信される

      • 優先度が同じパケットを含む複数のストリームがある場合にはラウンドロビン

    • パケットの種類に応じた優先度は以下の通り:

      1. 音声パケット

      2. 再送パケット

      3. 映像ないし FEC パケット

      4. パディングパケット(キープアライブや後述のプローブ用途で使用される)

    • この優先順位(と音声の特別扱い)に照らし合わせて、ペーサーの役割を端的に表現するなら「映像の送信によって、音声の送信が阻害されないようにすること」とも言える

  • PacingController と関係がある主要なクラスとしては以下のものがある:

  • PacingController がやらないこと:

    • ネットワークの状態(帯域や輻輳)の推定

      • 送信ペース調整の際には、これらの状態を考慮はするが、推定自体は外部に任せている

        • PacingController はあくまでも、指定されたペースは正しいものとして、それを遵守するのが役割

      • この辺りは帯域推定や輻輳制御を専門に行うクラスの責務であり、本ドキュメントの対象外となる

    • 送信キューが詰まった場合のパケットドロップ

      • 指定されたターゲット送信ビットレートが、実際に送信されるパケット群に対して小さ過ぎる場合(帯域推定が誤っている場合など)には、キューの消化ペースが追いつかなくなる

      • そのような場合でも PacingController はパケットドロップは行わず、(時間は掛かっても)全てのパケットの送信を試みる

      • ただし、キューが詰まっている場合には、指定されたターゲット送信ビットレートを超過したペースでのパケット送信が許容されるようになる

以降では PacingController および関連するクラスの実装やアルゴリズムの詳細を取り上げていく。

なお、フィールドトライアルやオプションによって挙動が変わる機能については、簡単のためにデフォルトの構成に準拠して記述を行なっている (末尾の「構成オプション」節も参照)。

詳細

PacingController クラスの詳細

ペース調整ロジックの中心を担う PacingController の主要なメソッドについて説明していく。

PacingController::SetPacingRates()

PacingController に「パケットの送信ペース(ターゲットビットレート)」を第一引数で指示するためのメソッド。

また第二引数で「パディングパケットの送信レート」を指定することも可能で、これが 0 より大きい場合には、 送信ペースに余裕がある場合は、パディングパケットが常に送信されることになる。

PacingController の各処理では、ここで指示された送信ペースが厳守されることになる。 ペース調整は、イメージとしては 経過時間 * 送信ペース - これまでの送信量 = バジェット として、そのバジェットに収まる分だけのパケット送信を許可する形となっている。 この式を見て分かる通り、基本的には「時間経過分だけバジェットが増えて、パケット送信分だけ減る(かつ、プラスの場合にだけ送信可能)」という単純なもの。 なお、実際のコードでは「バジェット(= 余裕分)」ではなく「 dept (= 不足分)」として実装されているが、符号の向きが異なるだけで、意味するところはどちらもほぼ同じ。

ただし、いくつか例外もある:

  • (デフォルトでは)音声パケットは、送信ペース管理の対象外(常にすぐ送信可能、また、送信分もバジェットから引かれない)

  • キューが詰まっている場合には、要素が延々と増えていくことを防止するために、送信ペースを(指示されたものよりも)増加させる

  • PacingController::SetSendBurstInterval() によってバースト送信が許可されている場合には、一時的に送信ペースを超過することがある

    • ただし、これによって消費されるバジェットの合計が増える訳ではない

なお、本メソッドの呼び出し側に関しては「 PacingController の利用側クラスや呼び出しタイミング」節を参照。

PacingController::EnqueuePacket()

送信対象の RTP パケット( RtpPacketToSend )を受け取り、それをキューに追加するメソッド。 キューイングされたパケットは PacingController::ProcessPackets() の呼び出し時に、 (ペース調整を満たしたタイミングで)実際に送信される。

メソッドの処理の流れは以下の通り:

  1. BitRateProber::OnIncomingPacket() を呼び出して、プローバに送信対象パケットのサイズを教える

    • プローブ関連処理の詳細は「 BitrateProber クラスの詳細」の節を参照

  2. キューが空だった場合には、現在時刻を基点にして、バジェットを再計算する:

    • 処理としては UpdateBudgetWithElapsedTime(UpdateTimeAndGetElapsed(現在時刻)); が呼ばれるイメージ:

      • UpdateTimeAndGetElapsed() で、最終処理時刻を現在時刻に設定(返り値が前回の処理時刻からの経過時間)

      • UpdateBudgetWithElapsedTime() で、経過時間を元に送信バジェットを更新する

    • この二つのメソッドの組み合わせは PacingController 内の色々なところで登場する頻出パターン

    • それぞれのメソッドの詳細については、後続のサブセクションを参照

  3. キュー( PacingController::PacketQueue の実装クラス)に RTP パケットを追加

  4. キューの要素が増えたので PacingController::MaybeUpdateMediaRateDueToLongQueue() を呼び出して、必要なら送信ペースを増加する(キュー詰まり解消用)

なおユーザが直接生成した RTP パケットだけではなく、 PacingController::ProcessPackets() 呼び出し時に生成される 以下のパケットも、一度キューを経由して送信され、ペース調整の対象となる:

  • FEC 用のパケット

  • プローブ用のパディングパケット

PacingController::ProcessPackets()

キューに格納されているパケット群を PacingController::SetPacingRates() で指定されたペース制約を満たす範囲で送信するメソッド。 その他に、キープアライブやプローブ用のパディングパケットの送信を行なったりもする。

処理の流れ:

  1. PacingController::ShouldSendKeepalive() を呼び出して、キープアライブ用のパディングパケットの送信が必要かどうかを判定する

  2. PacingController::Pause() 呼び出しによって、送信処理が一時停止中になっている場合には、ここで終了

  3. PacingController::NextSendTime() で、パケットの次回送信時刻を取得する

    • もし現在時刻が送信時刻に到達していない場合(i.e., 送信バジェット枯渇中)には、ここで終了

  4. UpdateTimeAndGetElapsed(次回送信時刻) を呼び出して、「前回の処理実施時刻」から「次回送信時刻」までの経過時間を取得する

    • NOTE: バジェットに余裕がある場合には、次回送信時刻は、現在時刻よりも前になることがある

    • 経過時刻が 0 秒以上なら PacingController::UpdateBudgetWithElapsedTime() を呼び出して、バジェットを更新する(経過時間分だけ送れる量が増える)

  5. プローブ中の場合には BitrateProber::RecommendedMinProbeSize() を呼び出して「プローブに必要な送信データサイズ」を取得する

  6. 以降で、キュー内のパケットの送信ループ処理に入る

    • 各イテレーションの冒頭では PacingController::GetPendingPacket() によって、次に送信するパケットがキューから取り出される

    • ただし、送信バジェットが枯渇している場合などは、キューに要素が存在しても null が返されることがある(詳細は後述)

    • 「次に送信するパケットがあるかどうか」で処理が分岐する

  7. [送信パケットがない場合] 必要に応じて、プローブ用のパディングパケットをキューに追加する

    1. PacingController::PaddingToAdd() でパディングパケットのサイズを計算する

      • パディングパケットの送信が不要な場合には 0 が返され、ここで送信ループを抜ける

    2. PacingController::PacketSender::GeneratePadding() で上記のサイズのパディングパケットを生成する

    3. PacingController::EnqueuePacket() を呼び出して、パディングパケットをキューに追加する

    4. 送信ループの次のイテレーションに進む

  8. [送信パケットがある場合] RTP パケットを実際に送信する

    1. PacingController::PacketSender::SendPacket() を呼び出して、パケットを送信する

    2. FEC パケットがある場合には PacingController::PacketSender::FetchFec() で取得して、キューに追加する

    3. PacingController::OnPacketSent() メソッドを呼び出して、(主に)以下を行う

    4. ループ内で送信したパケットのサイズ合計を更新する

    5. プローブ中の場合には、その合計サイズが「プローブに必要な送信データサイズ」を超過しているかどうかをチェックする

      • 超過している場合には、ここでループを抜ける

      • プローブ中の場合は、一度に送信するデータサイズが、少な過ぎても多過ぎても良くない

    6. PacingController::NextSendTime() を呼び出して、パケットの次回送信時刻を取得する

      • 次回送信時刻が「現在時刻を超えている(バジェットが枯渇した)」かつ「プローブ中ではない」場合には、ここでループを抜ける

    7. UpdateBudgetWithElapsedTime(UpdateTimeAndGetElapsed(次回送信時刻)) を呼び出して、次のイテレーションに備えてバジェットを更新する

  9. 送信ループを抜けた後は、プローブ中なら BitRateProber::ProbeSent() を呼び出して、今回送信したデータサイズ合計を BitrateProber に伝える

  10. 最後に PacingController::MaybeUpdateMediaRateDueToLongQueue() を呼び出して、以降の送信ペースの調整を行う

PacingController::NextSendTime()

パケットの次の送信時刻を決定するためのメソッド。

バジェットが枯渇している場合には、それが回復する予定時刻を返す。 反対にバジェットに余裕がある場合には、現在より前の時刻が返されることがある。

決定の際の流れは以下の通り:

  1. PacingController::Pause() が呼ばれており、一時停止中なら「次のキープアライブ時刻」を返す

    • 「次のキープアライブ時刻」は 最後の送信時刻 + 500ms

  2. プローブ中の場合には BitrateProber::NextProbeTime() に次回時刻の決定を委譲する

  3. 送信キューの先頭が音声パケットの場合には、そのパケットをキューに追加した時刻(つまり過去)を返す

    • 音声パケットは、デフォルトではペース調整の対象外なので、即座に送信される

    • 音声パケットがペース調整の対象に含まれる構成の場合には、この部分の処理はスキップされる

  4. 「輻輳発生中」ないし「まだ PacingController::EnqueuePacket() が一度も呼び出されていない」場合には「次のキープアライブ時刻」を返す

    • 「輻輳発生中」かどうかは PacingController::SetCongested() によって、外部から PacingController に伝えられる

    • 輻輳時には、音声パケットやキープアライブ用パディングパケットを除いて、パケット送信は抑制される

  5. キューに要素がある場合には、

    1. 負債 / 送信ペース で負債が解消するまで(= 次にパケットが送信可能になるまで)に掛かる時間を求める

      • 「負債」は、バジェットを超過して送信したパケットの合計サイズ(超過しておらずバジェットに余裕がある場合には負数になる)

    2. その時間分を「最後の送信時刻」に加算し、それを次回の送信時刻とする

  6. キューが空の場合には、

    • PacingController::SetPacingRates() でパディングパケットの送信レートが指定されている」場合は、そのレートに合わせて次回の送信時刻を決定する

    • それ以外の場合には、「次のキープアライブ時刻」を次回送信時刻とする

PacingController::PaddingToAdd()

プローブやその他の目的で使用するパディングパケットのサイズを決定するメソッド。

決定方法:

  • 送信キューが空ではないなら、パディングサイズは 0 (パディングではなく実際のパケットを送るべきなので)

  • 輻輳中ならパディングサイズは 0 (ネットワークに負荷をかけたくない)

  • PacingController::EnqueuePacket() が一度も呼び出されていないなら、パディングサイズは 0 (パディングパケットを生成しても、適切なタイムスタンプを振ることができない)

  • プローブ中の場合には プローブに必要なバイト数 - これまでに送信したバイト数 をパディングサイズとする

  • PacingController::SetPacingRates() で「パディングパケットの送信レート」が指定されている場合には レート * 5ms を返す

    -ただし、パディング用のバジェットに余裕がある場合のみ

PacingController::MaybeUpdateMediaRateDueToLongQueue()

送信キューへの操作(要素追加や削除)があった場合に呼び出されるメソッド。

SetPacingRates() で指定された送信ペース(ターゲットビットレート)に対して、 EnqueuePacket() で追加されるペースが多過ぎると、キューが詰まって延々と要素数が増えてしまう。 それに対処するために、送信ペースの調整(増加)を行うのがこのメソッドの役割。

処理の流れ:

  1. 最初に送信ペースを PacingController::SetPacingRates() で指定された値にリセットする

  2. キューにあるパケットの「サイズ合計」を求める

    • もし 0 ならここで終了(指定された送信ペースをそのまま使う)

  3. キュー内の各要素の平均滞在時間(エンキューされてから、実際に送信されるまでの平均時間)を求める

  4. 「キューが空になるまでの期待時間」を 2 - 平均滞在時間 で求める(0 以下になる場合には 1ms にする)

    • 「2 秒」の部分は PacingController::SetQueueTimeLimit() で変更可能

    • 「キューが空になるまでの期待時間」は、「キュー内の最後のパケットの遅延を 2 秒以下にするために使える残り時間」とも言える

  5. サイズ合計 / キューが空になるまでの期待時間 で、その期待時間を達成するために必要な送信ペースを求める

PacingController::UpdateTimeAndGetElapsed()

基本的には「現在時刻」を引数にとって、それと「最終処理時刻」との差分(経過時間)を計算して返すメソッド。

処理の流れ:

  1. 「最終処理時刻」よりも前の時刻が「現在時刻」として指定された場合には、このメソッドは 0 秒を経過時間として返す

  2. 現在時刻 - 最終処理時刻 で経過時間を求める

#. もし経過時刻が 2 秒を超えている場合には、2 秒に丸める - 「最終処理時刻」を「現在時刻」の値で更新して、経過時間を呼び出し元に返す

PacingController::UpdateBudgetWithElapsedTime() および PacingController::UpdateBudgetWithSentData()

バジェットの増減を行うための単純なメソッド群:

BitrateProber クラスの詳細

  • BitrateProber は「送信帯域の推定(プローブ)用に追加で送信するパディングパケット」のサイズや送信タイミングを決定するためのクラス

  • プローブ処理は BitrateProber::CreateProbeCluster() 呼び出しにより開始され、プローブに必要な期間やパケット送信回数を超えたら終了する

    • この一回のプローブ処理のまとまりは「クラスター」と呼ばれる

    • クラスターは BitrateProber::CreateProbeCluster() 呼び出し毎に新しく生成され、FIFO キューの末尾に追加される

    • 一度に処理されるのはキューの先頭のクラスターのみ

  • BitrateProber には ProbingState で表現される以下のような状態遷移がある:

  • プローブ処理は常時実行されるものではなく、通信相手との接続直後やネットワーク状況の変更に応じて実施される

以降では BitrateProber の主要メソッドについて説明していく。

BitrateProber::CreateProbeCluster()

新しいクラスターを生成するためのメソッド。

処理の流れは以下の通り:

  • 古いクラスターがキューに残っている場合には削除

    • 作成から五秒以上経過しているクラスターは、古いと判定される

  • 引数で指定された以下の情報をもとに、新しいクラスターを生成する:

    • ターゲット送信ビットレート

    • プローブ用のパケット送信回数

    • プローブ用に送信するパケットサイズの合計

      • この値は ターゲット送信ビットレート * プローブ期間(デフォルトでは 15ms) によって算出される

  • 生成したクラスターをキューの末尾に追加

  • BitrateProber の状態を kInactive にする

BitrateProber::OnIncomingPacket()

  • PacingController::EnqueuePacket() の中で、追加対象パケットのサイズを引数として、呼び出されるメソッド

  • 「現在が kInactive 」かつ「クラスターキューが空ではない」かつ「パケットサイズが十分に大きい場合」に kActive 状態に遷移する

    • 「パケットサイズが十分に大きい場合」== 「パケットサイズが min(200, RecommendedMinProbeSize()) 以上」

BitrateProber::RecommendedMinProbeSize()

  • 一度に送信するプローブ用のデータサイズの推奨(最低)値を返すメソッド

    • 送信量がこれより少ないとプローブの結果の信頼度が下がるので、不足分は( PacingController によって)パディングパケットで補填される

  • 計算式: 2 * ターゲット送信ビットレート * 1ms

BitrateProber::NextProbeTime()

  • プローブ用のパケット送信を次に行うべき時刻を返すメソッド

  • 状態が kActive ではない場合には Timestamp::PlusInfinity() が返される

  • kActive (かつクラスターキューが空ではない)場合には BitrateProber::CalculateNextProbeTime() によって事前に計算されていた、次回プローブ時刻、を返す

private なメソッドである BitrateProber::CalculateNextProbeTime()BitrateProber::ProbeSent() の中で呼び出され、 以下のようにして次回プローブ時刻を決定する:

  • 計算式: クラスターでの送信開始時刻 + (クラスターで送信済みのサイズ合計 / ターゲットビットレート)

    • 「送信開始時刻」はクラスターの生成時刻とは異なり、初めて実際にパケットが送信された時刻

  • クラスター生成時に指定されたターゲットビットレート通りの送信ペースになるように設定された単純な式

  • ターゲットビットレートに対して「送信量が多い場合には未来の時刻」になり、逆に「送信量が少ない場合には過去の時刻」になる

BitrateProber::ProbeSent()

PacingController::ProcessPackets() の中で「実際に送信された合計パケットサイズ」を引数に取り呼び出されるメソッド。

処理の流れは以下の通り(キュー内の先頭のクラスターに対して適用される):

  • このクラスターでのパケット送信が初の場合には「送信開始時刻」を現在時刻に設定する

  • 「送信済みバイト数合計」に、今回の送信分を加算する

  • 「プローブ(用のデータ送信)回数」をインクリメントする

  • BitrateProber::CalculateNextProbeTime() を呼び出して、次のプローブ時刻を決定する

  • 以下の両方の条件を満たす場合には、プローブ処理を終えたものと判断し、クラスターをキューから除去する:

  • クラスターキューが空になった場合には、 BitrateProber の状態を kSuspended に遷移する

BitrateProber::CurrentCluster()

現在のプローブ処理を行なっているクラスターを返すメソッド。

基本的にはキューの先頭要素を返すだけだが、細々とした条件や処理がある。

  • BitrateProber の状態が kActive ではない場合には null を返す(クラスター生成直後は kInactive である可能性がある)

  • 「次のプローブ時刻(cf. BitrateProber::CalculateNextProbeTime() )」が現在時刻に対して 10ms よりも前の場合には「そのクラスターには深刻な遅延が発生している」と判断する:

    • そのクラスターはキューから除去して、次のクラスターを返す

    • キューが空の場合には、状態を kSuspended に遷移した上で null を返す

PacingController の利用側クラスや呼び出しタイミング

ペーサーによって発生し得る遅延

おそらく以下のようなケースが考えられる:

  • PacingController::SetPacingRates() で指定されたターゲットビットレートが小さ過ぎる場合:

    • PacingController は一度キューに入ったパケットを破棄しない

    • そのため PacingController::EnqueuePacket() で追加されるパケットのサイズ(ペース)が、そもそもそのターゲットビットレート内で送信可能な量を超えている場合には、キューが詰まり、遅延も増加していく

    • ただし、キュー内に存在するパケットの待機時間の平均値が 2 秒を超えそうな場合には、一時的に送信ビットレートを上げて、キューに無限に要素が増えないようにしようとする

    • 2 秒はデフォルト値で PacingController::SetQueueTimeLimit() を使うことで変更が可能

  • 映像パケットのサイズの振れ幅が大きい場合:

    • PacingController が採用しているリーキーバケットアルゴリズムでは、一度に送信可能なサイズに上限がある

    • そのため「極端にサイズが大きい映像の I フレームを定期的に送信」といった場合には、全体としては余裕があって、その I フレームパケットの送信がペース調整されて一度に行えず、遅延が発生する可能性がある

    • これが問題となる場合には PacingController::SetSendBurstInterval() といったメソッドで、ある程度のバースト送信を許容可能にすることができるので、これで緩和できる可能性がある

構成オプション

フィールドトライアルやメソッド呼び出しなどによって挙動を以下のように変更することが可能(注意: 網羅的ではない):

  • プローブ処理の無効化

  • 音声パケットをペース調整処理に含める(e.g. バジェットが不足している場合には即座に送信しない、バジェット計算の際に送信済み音声パケットサイズを考慮する)

  • 送信済みサイズの決定の際に、RTP / TCP / IP のヘッダー部分を考慮するかどうか(デフォルトでは RTP のペイロードサイズのみが使われる)

  • ある程度のバースト送信を許容する ( PacingController::SetSendBurstInterval() )

    • 利用可能な送信帯域の総容量が増える訳ではないが、一時的に大きなパケット(e.g., 映像の I フレーム)が来た場合に、それを一気に送信することができるようになる

  • 送信キューの実装として RoundRobinPacketQueue の代わりに PrioritizedPacketQueue を使用する

© Copyright 2022, Shiguredo Inc Created using Sphinx 5.2.1