WebRTC GCC 輻輳制御アルゴリズム (TFB-GCC)

目次

I.はじめに 

2.TFB-GCCの原理

1. 受信側は、受信したパケットを記録してフィードバックします

(1)トランスポート全体のシーケンス番号

(2) RTCP RTPFB TW メッセージ

 2. 送信者は、パケット受信フィードバックに基づいて、帯域推定と輻輳制御を実行します。

(1) 遅延勾配による帯域推定

(2) パケットロス率からの帯域推定

3.参考文献


I.はじめに 

        ネットワーク伝送におけるリンクの帯域幅には限りがあり、過負荷のデータをリンクに送信してネットワークの輻輳を引き起こさないようにするために、帯域幅を推定し、推定された帯域幅に基づいて調整してネットワークの輻輳を回避する必要があります。

「WebRTC GCC Congestion Control Algorithm (REMB-GCC)」        では、REMB-GCC 輻輳制御アルゴリズムをまとめ、記事の最後で、Google が REMB-GCC に代わる TFB-GCC を開始したことを述べました。TFB-GCC の原理も、遅延勾配とパケット損失率に基づいて帯域幅を推定し、ネットワークの輻輳を回避します. 違いは、ロジックが送信側で計算され、受信側は受信ステータスのみをフィードバックすることです.パケット (受信したかどうか、および前のパケットの受信時間の差)。

        この記事では、主にTFB-GCCの原理と実装について説明します. 遅延勾配とパケット損失に基づく推定帯域幅の原理について明確でない場合は、最初にこの記事を読むことができます.

2.TFB-GCCの原理

        上はTFB-GCCのアーキテクチャ図で、左側が送信側部分、右側が受信側部分です。

        受信側は、送信側でのデータ パケットの到着を記録し、送信側にフィードバックする RTCP メッセージを構築する責任がありますが、遅延勾配計算のロジックは実行しません。

        RTCP フィードバック メッセージを受信した後、送信側は、パケット損失率に従って帯域幅 As を推定し、遅延勾配に従って帯域幅 Ar を推定します. 最終的に推定される帯域幅は、2 つの A=min(As, Ar の小さい方の値です。 ) 、帯域幅の推定と輻輳制御を実行します。

1. 受信側は、受信したパケットを記録してフィードバックします

        WebRTC が TFB-GCC を使用する場合は、RTP メッセージ拡張フィールド (トランスポート全体のシーケンス番号) を有効にし、RTCP RTPFB TW メッセージ フィードバック (伝送帯域幅フィードバック メッセージ) を使用する必要があります.RTP および RTCP プロトコルの詳細については、を参照してください対応するリンク記事。

(1)トランスポート全体のシーケンス番号

        RTP 拡張フィールドのトランスポート全体のシーケンス番号の構造は次のとおりです. これは、チャネル シーケンス番号として理解できる、長さ 2 バイトの 1 ヘッダーの拡張ヘッダーです。

質問: トランスポート全体のシーケンス番号を使用する必要があるのはなぜですか?

        チャネルは音声と映像を同時に伝送することが多く、帯域を見積もる際には、音声ストリームのパケットを送受信する状況や、送受信する状況だけを組み合わせるのではなく、チャネル全体の帯域を見積もる必要があります。ビデオ ストリームのパケットを受信します。オーディオストリームとビデオストリームを送信する際、RTPパケットの初期シーケンス番号が異なり、シーケンス番号の成長率も異なるため、オーディオパケットとビデオストリームにチャネルシーケンス番号を付けて送信しますまた、受信側がチャネルパケットの受信に応答するのも便利です。

        以下は、Wireshark によってキャプチャされた RTP 送信パケット情報です. ペイロード タイプ: 96 はビデオ ストリーム パケットであり、ペイロード タイプ: 111 はオーディオ ストリーム パケットです. これら 4 つのパケットのトランスポート全体のシーケンス番号が増加しています。

備考: 上記では ID=3 を使用して、transport-wide-cc 拡張ヘッダー a=extmap:3 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions- を表しています。 01.

(2) RTCP RTPFB TW メッセージ

RTCP RTPFB TW 送信帯域幅フィードバックメッセージのフォーマットは以下の通りです (PT=205, FMT=15)。

基本シーケンス番号: フィードバックされる最初の RTP パケットのトランスポート全体のシーケンス番号を記録します。

packet status count : フィードバック メッセージには、RTP パケットの到着ステータスが含まれています。

基準時間: 受信側からフィードバック メッセージの最初のパケットを受信する基準時間 (24 ビット) であり、その値の単位は 64ms です。

fb pkt.count : 送信されたフィードバック パケットの数。これは、RTCP RTPFB パケットのシーケンス番号に相当します。

パケット チャンク: 送信者が送信した RTP パケットの到着状態を記録する. この構造はビット 0 の値に応じてランレングス チャンクとステータス ベクター チャンクとして表すことができる.

ランレングスチャンク構造

T (チャンク タイプ): このビットは 0 であり、これがラン長チャンクであることを示します。

S (パケット ステータス シンボル): パケットの到着ステータスをマークします。

        00:パケット未受信

        01:パケット受信、デルタ小

        10:パケット受信、大または負のデルタ

        11:貸切

Run Length: 同じ到着状態にある連続したパケットの数を示す長さ

次のラン長チャンクは、221 個の連続したパケットが受信されていないことを示しています。

ステータスベクターのチャンク構造

T (チャンク タイプ): このビットは 1 で、これがステータス ベクター チャンクであることを示します。

S (シンボル サイズ): このビットが 0 の場合、14 ビットのシンボル リストが 14 個のパケットのステータスを表すことができるように、パケットが受信されていない/受信されたパケットの 2 つの状態のみが含まれていることを意味し、このビットが 1 の場合は、その 2 ビット が パケット の 状態 を 表す の に 使用 さ れる ので , シンボル リスト は 7 個 の パッケージ の 状態 を 表す ことが でき ます .

シンボル リスト: 一連のパッケージのステータスを識別します

備考: 多くのパケットの受信状態が一致している場合, 例えば, それらがすべて Received で到着間隔が SmallDelta である場合, これらのパケットの受信状態を表すために Run length チャンクを使用します. パケットの受信状態が不一致の場合, Run length チャンクを使用して同じ受信ステータスを示すことはできません。このとき、Status vector チャンクが使用されます。

        上図のステータスベクターチャンクでは、S ビットが 0 で、シンボルリストの 1 ビットがパケットの受信状態を表し、0 が未受信、1 が受信を意味するため、上図は 1 を示します。パケットは受信されておらず、次の 5 パケットは受信され、次の 3 パケットは受信されず、次の 3 パケットは受信され、最後の 2 パケットは受信されませんでした。

        上図に示すステータスベクターチャンクでは、S ビットが 1 で、シンボルリストの 2 ビットがパケットの受信ステータスを表していることを示しているため、上図は最初のパケットが受信されておらず、2 番目のパケットが受信されていないことを示しています。 (タイムスタンプなし) を受信すると、次の 3 つのパケットが受信され、最後の 2 つのパケットは受信されません。

recv delta : 前後の 2 つの RTP パケットの到着時間間隔。単位値は 250us を表し、到着時間間隔が 63.75ms 以下の場合は SmallDelta と見なされ、1 バイトが使用され、それより大きい場合は 1 バイトが使用されます。 63.75ms、LargeDelta とみなされ、2 バイトが使用されます。

 2. 送信者は、パケット受信フィードバックに基づいて、帯域推定と輻輳制御を実行します。

(1) 遅延勾配による帯域推定

        下の図に示すように、送信者は T(i-1) と T(i) で 2 つのデータ パケットを送信し、受信者はこれら 2 つのデータ パケットをそれぞれ t(i-1) と t(i) で受信します。 d(t(i)) = [t(i) - t(i-1)] - [T(i) - T(i-1)].

        理想的なネットワーク伝送では、d(t(i)) は 0 である必要があります. ネットワークが混雑している場合、T(i) で送信されたパケットが受信側で受信されるまでに時間がかかります. このとき、d(t (i) )) が 0 より大きい場合、d(t(i)) が 0 より大きい場合、ネットワークの輻輳がより深刻であることを意味し、d(t(i)) が 0 より大きいが、小さいほど、ネットワークの混雑が改善されていることを意味します。

         WebRTC コードを使用して、送信者が帯域幅を推定し、RTCP フィードバック メッセージとパケット損失率に基づいて輻輳制御処理ロジックを実行する方法を説明します. TFB-GCC の場合、遅延勾配に基づく帯域幅推定には、主に次のものが含まれます: 到着時間フィルター , TrendlineEstimator.

を。到着時間フィルター

        RTCP RTPFB TW メッセージを受信すると、RtpTransportControllerSend::OnTransportFeedback 関数が呼び出されます. この関数は、最初に TransportFeedbackAdapter::ProcessTransportFeedback を呼び出してパケットの到着ステータス情報を取得し、次に GoogCcNetworkController::OnTransportPacketsFeedback を呼び出して帯域幅を計算します。パケットの到着ステータス情報に基づいて遅延勾配を推定し、最終的に推定された帯域幅値をペーサー、エンコーディング、およびその他のモジュールに更新します。具体的なロジックは次のとおりです。

        TransportFeedbackAdapter::ProcessTransportFeedback は、主に ProcessTransportFeedbackInner を呼び出して、パケットの遅延勾配を計算するために使用される RTCP RTPFB TW メッセージの内容に従って、パケット グループの相対的な到着時間を取得します。

std::vector<PacketResult>
TransportFeedbackAdapter::ProcessTransportFeedbackInner(
    const rtcp::TransportFeedback& feedback,
    Timestamp feedback_receive_time) {
  // Add timestamp deltas to a local time base selected on first packet arrival.
  // This won't be the true time base, but makes it easier to manually inspect
  // time stamps.
  if (last_timestamp_.IsInfinite()) {
    current_offset_ = feedback_receive_time;
  } else {
    // TODO(srte): We shouldn't need to do rounding here.
    const TimeDelta delta = feedback.GetBaseDelta(last_timestamp_)
                                .RoundDownTo(TimeDelta::Millis(1));
    // Protect against assigning current_offset_ negative value.
    if (delta < Timestamp::Zero() - current_offset_) {
      RTC_LOG(LS_WARNING) << "Unexpected feedback timestamp received.";
      current_offset_ = feedback_receive_time;
    } else {
      current_offset_ += delta;
    }
  }
  last_timestamp_ = feedback.GetBaseTime();

  std::vector<PacketResult> packet_result_vector;
  packet_result_vector.reserve(feedback.GetPacketStatusCount());

  size_t failed_lookups = 0;
  size_t ignored = 0;
  TimeDelta packet_offset = TimeDelta::Zero();
  for (const auto& packet : feedback.GetAllPackets()) {
    int64_t seq_num = seq_num_unwrapper_.Unwrap(packet.sequence_number());

    if (seq_num > last_ack_seq_num_) {
      // Starts at history_.begin() if last_ack_seq_num_ < 0, since any valid
      // sequence number is >= 0.
      for (auto it = history_.upper_bound(last_ack_seq_num_);
           it != history_.upper_bound(seq_num); ++it) {
        in_flight_.RemoveInFlightPacketBytes(it->second);
      }
      last_ack_seq_num_ = seq_num;
    }

    auto it = history_.find(seq_num);
    if (it == history_.end()) {
      ++failed_lookups;
      continue;
    }

    if (it->second.sent.send_time.IsInfinite()) {
      // TODO(srte): Fix the tests that makes this happen and make this a
      // DCHECK.
      RTC_DLOG(LS_ERROR)
          << "Received feedback before packet was indicated as sent";
      continue;
    }

    PacketFeedback packet_feedback = it->second;
    if (packet.received()) {
      packet_offset += packet.delta();
      packet_feedback.receive_time =
          current_offset_ + packet_offset.RoundDownTo(TimeDelta::Millis(1));
      // Note: Lost packets are not removed from history because they might be
      // reported as received by a later feedback.
      history_.erase(it);
    }
    if (packet_feedback.network_route == network_route_) {
      PacketResult result;
      result.sent_packet = packet_feedback.sent;
      result.receive_time = packet_feedback.receive_time;
      packet_result_vector.push_back(result);
    } else {
      ++ignored;
    }
  }

  if (failed_lookups > 0) {
    RTC_LOG(LS_WARNING) << "Failed to lookup send time for " << failed_lookups
                        << " packet" << (failed_lookups > 1 ? "s" : "")
                        << ". Send time history too small?";
  }
  if (ignored > 0) {
    RTC_LOG(LS_INFO) << "Ignoring " << ignored
                     << " packets because they were sent on a different route.";
  }

  return packet_result_vector;
}

         パケットの相対的な到着時間を取得した後, DelayBasedBwe::IncomingPacketFeedbackVector を呼び出して遅延勾配の変化を分析します. 主要な呼び出しプロセスは次のとおりです: DelayBasedBwe::IncomingPacketFeedbackVector -> DelayBasedBwe::IncomingPacketFeedback -> InterArrival::ComputeDeltas.

b. トレンドライン推定器

        REMB-GCC では、遅延勾配の変化を計算するためにカルマン フィルターが使用されますが、TFB-GCC では、累積遅延勾配の変化傾向を計算するために線形フィルターが使用されます。最小二乗法 ( x, y ) でフィッティングすると、変化の傾向は直線の傾きで判断できます。フィッティング ライン方程式に代入すると、x は時間に相当し、y は平滑化された累積遅延勾配に相当します。

        線形フィルタの傾きを取得した後、TrendlineEstimator::Detect を呼び出して、現在の帯域使用状況を判断します. TrendlineEstimator::Detect は、現在の帯域の使用状況が過剰/通常/不足の状態にあるかどうかを、帯域のトレンド トレンドとの関係に従って判断します。累積遅延勾配と動的しきい値、および動的しきい値 threshold_ については、次のサブセクション適応しきい値で説明します。

a. 傾向 > しきい値_の場合、ネットワーク輻輳キューが増加しており、現在輻輳状態であることを意味します.輻輳継続時間が overusing_time_threshold_ より大きく、遅延勾配が前回の遅延勾配よりも大きい場合、混雑していると判断されます過負荷状態 閾値を一度も上回らないことに注意 過負荷と判断し、一定時間継続する必要があり、過負荷と判断するまでに遅延勾配が大きくなっている

b. トレンド < -threshold_ の場合、ネットワークの輻輳キューが小さくなり、輻輳状況が改善していることを意味し、使用率が低い状態にあると判断されます

c. -threshold_ <= trend <= threshold_ の場合、判定は正常な状態です

(3)適応閾値

        前述のように、TrendlineEstimator は累積遅延勾配の変化をしきい値と比較することで現在の帯域使用状況を判断します. 理想的なネットワークでは遅延勾配は 0 ですが、通常の帯域占有率では遅延勾配も 0 になる場合があります.上下に変動しますが、累積遅延勾配は 0 に近づくはずなので、累積遅延勾配の変化は 0 に近くなります。しきい値の設定は非常に重要です. しきい値が固定値の場合, 設定が大きすぎるとネットワークの輻輳を検出できない場合があります, 設定が小さすぎると敏感すぎる可能性があります. WebRTC は適応動的しきい値方式を使用します.

演算方法:threshold(t(i)) = threshold(t(i-1)) + k * [ t(i) - t(i-1) ] * [ | トレンド(t(i)) | - しきい値 (t(i-1)) ]

ここで、k は変化率を表し、| トレンド (t(i)) | < しきい値 (t(i-1)) の場合、k の値は 0.039 であり、それ以外の場合、k の値は 0.0087 です。

threshold(t(i)) は、i 番目のパッケージを計算した後に新しいしきい値を決定する必要があることを意味します。threshold(t(i-1)) は、i-1 番目のパッケージを計算した後に決定されたしきい値を意味します。t(i) - t (i-1) は遅延勾配を計算した 2 つのパッケージ間の時間差を示し、trend(t(i)) は現在計算されている遅延勾配のトレンド (拡大された値) を示します。

(4)レートコントローラー

帯域幅の現在の使用状況を判断した後、下図に示すように、現在の状況に応じて最大ビットレート値を調整する必要があります。

過負荷状態にあるときは、Decr 状態に対応します. このとき、最大ビットレート値は、過去 500ms の時間ウィンドウで最大の acked_bitrate の 0.85 倍に減少する必要があります. acked_bitrate は、パケットの受信状態をフィードバックできます.メッセージを RTCP 経由で取得し、ローカルに維持されている送信リストと結合します

保留状態に対応する未使用状態にある場合、現在の最大ビット レートはこの時点で変更されないままである必要があります。 

正常な状態の場合、Incr に対応して、この時点でコード レートを適切に増加させることができ、その増加は元の最大コード レート値の 1.08 倍になります。

最大ビットレートを調整するWebRTC対応のRate Controllerのコードは以下の通り。

void AimdRateControl::ChangeBitrate(const RateControlInput& input,
                                    Timestamp at_time) {
  absl::optional<DataRate> new_bitrate;
  DataRate estimated_throughput =
      input.estimated_throughput.value_or(latest_estimated_throughput_);
  if (input.estimated_throughput)
    latest_estimated_throughput_ = *input.estimated_throughput;

  // An over-use should always trigger us to reduce the bitrate, even though
  // we have not yet established our first estimate. By acting on the over-use,
  // we will end up with a valid estimate.
  if (!bitrate_is_initialized_ &&
      input.bw_state != BandwidthUsage::kBwOverusing)
    return;

  ChangeState(input, at_time);

  // We limit the new bitrate based on the troughput to avoid unlimited bitrate
  // increases. We allow a bit more lag at very low rates to not too easily get
  // stuck if the encoder produces uneven outputs.
  const DataRate troughput_based_limit =
      1.5 * estimated_throughput + DataRate::KilobitsPerSec(10);

  switch (rate_control_state_) {
    case kRcHold:
      break;

    case kRcIncrease:
      if (estimated_throughput > link_capacity_.UpperBound())
        link_capacity_.Reset();

      // Do not increase the delay based estimate in alr since the estimator
      // will not be able to get transport feedback necessary to detect if
      // the new estimate is correct.
      // If we have previously increased above the limit (for instance due to
      // probing), we don't allow further changes.
      if (current_bitrate_ < troughput_based_limit &&
          !(send_side_ && in_alr_ && no_bitrate_increase_in_alr_)) {
        DataRate increased_bitrate = DataRate::MinusInfinity();
        if (link_capacity_.has_estimate()) {
          // The link_capacity estimate is reset if the measured throughput
          // is too far from the estimate. We can therefore assume that our
          // target rate is reasonably close to link capacity and use additive
          // increase.
          DataRate additive_increase =
              AdditiveRateIncrease(at_time, time_last_bitrate_change_);
          increased_bitrate = current_bitrate_ + additive_increase;
        } else {
          // If we don't have an estimate of the link capacity, use faster ramp
          // up to discover the capacity.
          DataRate multiplicative_increase = MultiplicativeRateIncrease(
              at_time, time_last_bitrate_change_, current_bitrate_);
          increased_bitrate = current_bitrate_ + multiplicative_increase;
        }
        new_bitrate = std::min(increased_bitrate, troughput_based_limit);
      }

      time_last_bitrate_change_ = at_time;
      break;

    case kRcDecrease: {
      DataRate decreased_bitrate = DataRate::PlusInfinity();

      // Set bit rate to something slightly lower than the measured throughput
      // to get rid of any self-induced delay.
      decreased_bitrate = estimated_throughput * beta_;
      if (decreased_bitrate > current_bitrate_ && !link_capacity_fix_) {
        // TODO(terelius): The link_capacity estimate may be based on old
        // throughput measurements. Relying on them may lead to unnecessary
        // BWE drops.
        if (link_capacity_.has_estimate()) {
          decreased_bitrate = beta_ * link_capacity_.estimate();
        }
      }
      if (estimate_bounded_backoff_ && network_estimate_) {
        decreased_bitrate = std::max(
            decreased_bitrate, network_estimate_->link_capacity_lower * beta_);
      }

      // Avoid increasing the rate when over-using.
      if (decreased_bitrate < current_bitrate_) {
        new_bitrate = decreased_bitrate;
      }

      if (bitrate_is_initialized_ && estimated_throughput < current_bitrate_) {
        if (!new_bitrate.has_value()) {
          last_decrease_ = DataRate::Zero();
        } else {
          last_decrease_ = current_bitrate_ - *new_bitrate;
        }
      }
      if (estimated_throughput < link_capacity_.LowerBound()) {
        // The current throughput is far from the estimated link capacity. Clear
        // the estimate to allow an immediate update in OnOveruseDetected.
        link_capacity_.Reset();
      }

      bitrate_is_initialized_ = true;
      link_capacity_.OnOveruseDetected(estimated_throughput);
      // Stay on hold until the pipes are cleared.
      rate_control_state_ = kRcHold;
      time_last_bitrate_change_ = at_time;
      time_last_bitrate_decrease_ = at_time;
      break;
    }
    default:
      assert(false);
  }

  current_bitrate_ = ClampBitrate(new_bitrate.value_or(current_bitrate_));
}

        最後に、遅延勾配に基づいて推定された最大ビット レート値を SendSideBandwidthEstimation の delay_based_limit_ 変数に保存します。

(2) パケットロス率からの帯域推定

        送信側のパケットロスによる帯域推定の考え方は、パケットロス率から輻輳の有無を判断するというものです。

        パケット損失率が 10% を超える場合、輻輳が発生していると見なされます. このとき、輻輳を軽減するために送信ビット レートを積極的に下げる必要があります. パケット損失率が 2% 未満の場合は、ネットワークが良好な状態にあり、送信ビット レートを適切に増加させて、利用可能な帯域幅がさらにあるかどうかを検出できます; パケット損失率が 2% から 10% の間の場合、ネットワークの状態は正常であると見なされます。前回と同じ送信ビットレートを維持します。

        パケット損失率の取得は、RTCP RR メッセージの損失パケット数と受信したシーケンス番号が最大のパケット数からパケット損失率を判断する. RR メッセージのフォーマットとフィールドの意味は以下のとおりである.続きます。

        WebRTC が RR パケットを受信し、パケット損失率に基づいて帯域幅を推定するコードは次のとおりです。

最後に、UpdateTargetBitrate では、パケット損失による推定値と遅延勾配による推定値の小さい方の値を、最終的な推定最大ビットレートとします。

ターゲット ビット レートが決定されると、ペーサー、fec、およびエンコーディング モジュールに更新され、役割を果たすようになります。

3.参考文献

RFC トランスポート ワイド cc 拡張

WebRTC 輻輳制御アルゴリズムに関するディスカッション: GCC の紹介

ウェブ リアルタイム向け Google Congestion Control の分析と設計

おすすめ

転載: blog.csdn.net/weixin_38102771/article/details/128218672