Congestion and Queuing Control for Real-Time Communication

WebRTC Architecture

Research Background

In 2012, the IETF established the RMCAT Working Group (RTP Media Congestion Avoidance Techniques) to develop standardized congestion control algorithms for real-time communication over the Internet. The emergence of WebRTC and peer-to-peer video conferencing created urgent demand for low-delay, responsive congestion control that could adapt to varying network conditions.

Building on this foundation, our research team received the Google Faculty Research Award in 2014 for advancing congestion control algorithms specifically tailored to WebRTC and low-latency scenarios.

Google Faculty Research Award 2014

GCC Research Architecture

Project Title: "Congestion Control for Web Real-Time Communication (WebRTC)"

Principal Investigator: S. Mascolo — Date: August 2014

Award granted for collaborating on the project "Congestion control for WebRTC" aiming at designing a real-time congestion control algorithm for video conferencing. Press coverage: La Repubblica Bari.

Publications

Our research produced peer-reviewed publications in top-tier venues:

Google Congestion Control Algorithm (GCC)

GCC starvation issues

Overview

Nowadays, the Internet is rapidly evolving to become an equally efficient platform for multimedia content delivery. While YouTube streams video using TCP, time-sensitive applications such as Video Conferencing employ UDP because they can tolerate small loss percentages but not the delays introduced by TCP's retransmission-based loss recovery. Since UDP does not implement congestion control, these applications must implement it at the application layer. In our papers, we experimentally evaluate the Google Congestion Control (GCC) proposed in the RMCAT IETF WG. We found that the algorithm works as expected when a GCC flow accesses the bottleneck in isolation; however, GCC does not provide fair bandwidth utilization when a GCC flow shares the bottleneck with either another GCC or a TCP flow — our experimental investigation shows that the first version of GCC gets starved when a TCP flow joins the bottleneck, and starvation also occurs when two coexisting GCC flows share the bottleneck. To overcome these issues, we proposed an adaptive threshold mechanism which sets the threshold used by the over-use detector.

Overuse Estimator

The OveruseEstimator code is available on the Chromium/WebRTC codebase. The method Update uses a Kalman filter, described in the papers, that filters the one-way delay variation and link capacity values:

void OveruseEstimator::Update(int64_t t_delta,
                              double ts_delta,
                              int size_delta,
                              BandwidthUsage current_hypothesis,
                              int64_t now_ms) {
  const double min_frame_period = UpdateMinFramePeriod(ts_delta);
  const double t_ts_delta = t_delta - ts_delta;
  double fs_delta = size_delta;

  ++num_of_deltas_;
  if (num_of_deltas_ > kDeltaCounterMax) {
    num_of_deltas_ = kDeltaCounterMax;
  }

  // Update the Kalman filter.
  E_[0][0] += process_noise_[0];
  E_[1][1] += process_noise_[1];

  if ((current_hypothesis == BandwidthUsage::kBwOverusing &&
       offset_ < prev_offset_) ||
      (current_hypothesis == BandwidthUsage::kBwUnderusing &&
       offset_ > prev_offset_)) {
    E_[1][1] += 10 * process_noise_[1];
  }

  const double h[2] = {fs_delta, 1.0};
  const double Eh[2] = {E_[0][0] * h[0] + E_[0][1] * h[1],
                        E_[1][0] * h[0] + E_[1][1] * h[1]};

  const double residual = t_ts_delta - slope_ * h[0] - offset_;

  const bool in_stable_state =
      (current_hypothesis == BandwidthUsage::kBwNormal);
  const double max_residual = 3.0 * sqrt(var_noise_);
  // We try to filter out very late frames. For instance periodic key
  // frames doesn't fit the Gaussian model well.
  if (fabs(residual) < max_residual) {
    UpdateNoiseEstimate(residual, min_frame_period, in_stable_state);
  } else {
    UpdateNoiseEstimate(residual < 0 ? -max_residual : max_residual,
                        min_frame_period, in_stable_state);
  }

  const double denom = var_noise_ + h[0] * Eh[0] + h[1] * Eh[1];

  const double K[2] = {Eh[0] / denom, Eh[1] / denom};

  const double IKh[2][2] = {{1.0 - K[0] * h[0], -K[0] * h[1]},
                            {-K[1] * h[0], 1.0 - K[1] * h[1]}};
  const double e00 = E_[0][0];
  const double e01 = E_[0][1];

  // Update state.
  E_[0][0] = e00 * IKh[0][0] + E_[1][0] * IKh[0][1];
  E_[0][1] = e01 * IKh[0][0] + E_[1][1] * IKh[0][1];
  E_[1][0] = e00 * IKh[1][0] + E_[1][0] * IKh[1][1];
  E_[1][1] = e01 * IKh[1][0] + E_[1][1] * IKh[1][1];

  // The covariance matrix must be positive semi-definite.
  bool positive_semi_definite =
      E_[0][0] + E_[1][1] >= 0 &&
      E_[0][0] * E_[1][1] - E_[0][1] * E_[1][0] >= 0 && E_[0][0] >= 0;
  RTC_DCHECK(positive_semi_definite);
  if (!positive_semi_definite) {
    RTC_LOG(LS_ERROR)
        << "The over-use estimator's covariance matrix is no longer "
           "semi-definite.";
  }

  slope_ = slope_ + K[0] * residual;
  prev_offset_ = offset_;
  offset_ = offset_ + K[1] * residual;
}

Overuse Detector

The OveruseDetector code checks if the filtered one-way delay variation is above a threshold. These events drive a finite state machine that controls the encoding bitrate:

BandwidthUsage OveruseDetector::Detect(double offset,
                                       double ts_delta,
                                       int num_of_deltas,
                                       int64_t now_ms) {
  if (num_of_deltas < 2) {
    return BandwidthUsage::kBwNormal;
  }
  const double T = std::min(num_of_deltas, kMaxNumDeltas) * offset;
  if (T > threshold_) {
    if (time_over_using_ == -1) {
      // Initialize the timer. Assume that we've been
      // over-using half of the time since the previous
      // sample.
      time_over_using_ = ts_delta / 2;
    } else {
      // Increment timer
      time_over_using_ += ts_delta;
    }
    overuse_counter_++;
    if (time_over_using_ > kOverUsingTimeThreshold && overuse_counter_ > 1) {
      if (offset >= prev_offset_) {
        time_over_using_ = 0;
        overuse_counter_ = 0;
        hypothesis_ = BandwidthUsage::kBwOverusing;
      }
    }
  } else if (T < -threshold_) {
    time_over_using_ = -1;
    overuse_counter_ = 0;
    hypothesis_ = BandwidthUsage::kBwUnderusing;
  } else {
    time_over_using_ = -1;
    overuse_counter_ = 0;
    hypothesis_ = BandwidthUsage::kBwNormal;
  }
  prev_offset_ = offset;

  UpdateThreshold(T, now_ms);

  return hypothesis_;
}

Adaptive Threshold

The method UpdateThreshold adapts the threshold according to the papers:

void OveruseDetector::UpdateThreshold(double modified_offset, int64_t now_ms) {
  if (last_update_ms_ == -1)
    last_update_ms_ = now_ms;

  if (fabs(modified_offset) > threshold_ + kMaxAdaptOffsetMs) {
    // Avoid adapting the threshold to big latency spikes, caused e.g.,
    // by a sudden capacity drop.
    last_update_ms_ = now_ms;
    return;
  }

  const double k = fabs(modified_offset) < threshold_ ? kDown : kUp;
  const int64_t kMaxTimeDeltaMs = 100;
  int64_t time_delta_ms = std::min(now_ms - last_update_ms_, kMaxTimeDeltaMs);
  threshold_ += k * (fabs(modified_offset) - threshold_) * time_delta_ms;
  threshold_ = rtc::SafeClamp(threshold_, 6.f, 600.f);
  last_update_ms_ = now_ms;
}
Adaptive threshold results

Chromium Patches and Get Involved

Chromium patches implementing these changes:

Ways to get involved with Chromium:

Key Insight: Low-latency congestion control requires rethinking the traditional paradigm of maximizing throughput. By detecting congestion early (before packet loss) and responding gently, video quality and user experience improve dramatically, even at the cost of slightly lower average throughput.
#research #webrtc #congestion-control