Date: October 13, 2026
Commit: 65217155f735a780676d34c073ef2447589a71c1
Note: This document was automatically generated by an AI assistant (Gemini) comparing the WebRTC source code with the IETF specification.
This document describes the differences between the WebRTC implementation of SCReAMv2 in /modules/congestion_controller/scream (see index.md) and the IETF Working Group draft draft-ietf-ccwg-rfc8298bis-screamv2-01 (datatracker).
The WebRTC implementation is currently in development and some features are not yet fully implemented.
min_ref_window (MIN_REF_WND in spec): The spec recommends 3000 bytes (Section 4.2.2). The WebRTC implementation defaults to 1000 bytes in scream_v2_parameters.cc:L26.max_segment_size (MSS in spec): The spec recommends 1000 bytes (Section 4.2.2). The WebRTC implementation defaults to 1280 bytes in scream_v2_parameters.cc:L34.bytes_in_flight_head_room (BYTES_IN_FLIGHT_HEAD_ROOM in spec): The spec recommends 1.5 (Section 4.2.2). The WebRTC implementation defaults to 1.1 in scream_v2_parameters.cc:L35.ref_wnd to MIN_REF_WND (Section 4.2.2).ref_window_ to min_ref_window at construction, and upon receiving the first feedback with valid delay metrics, resets ref_window_ to max(min_ref_window, target_rate_ * (rtt + feedback_hold_time_)) using the configured starting bitrate (default 300 kbps). See scream_v2.cc:L83-L94.TransportPacketsFeedback into a flat ScreamFeedback struct in scream_feedback.h and scream_feedback.cc.acked_not_marked_size vs. bytes_newly_acked:bytes_newly_acked and bytes_newly_acked_ce across feedback messages and resetting them on window updates as described in the spec (Section 4.1.2), ParseScreamFeedback directly computes acked_not_marked_size per feedback (the sum of sizes of all packets in the feedback that are not ECN-CE marked, including lost packets) and num_ce_marked_packets. See scream_feedback.cc:L44-L46.ParseScreamFeedback computes min_one_way_delay and max_one_way_delay only over packets with unambiguous receive times sent within a recent tail/burst window from the latest received packet: a base window of burst_window_min (default 25 ms), extended up to burst_window_max (default 50 ms) as long as consecutive packets are sent within burst_window_max_gap (default 3 ms). This prevents earlier sparse packets in long feedback intervals (e.g., audio-only or ALR) from skewing delay and jitter estimates. See scream_feedback.cc:L86-L118.feedback_hold_time_): Tracks an EWMA (gain feedback_hold_time_avg_g = 1/8) of the duration between the receive times of the first and last packets in a feedback report plus the last packet's receiver arrival_time_offset. This hold time is added to rtt when initializing ref_window_, clamping ref_window_ on initial congestion, scaling window increase for small RTTs, and computing target_rate_. See scream_feedback.cc:L124-L127 and scream_v2.cc:L396-L404.feedback_interval_): Tracks an EWMA (gain feedback_interval_avg_g = 0.1) of the time between consecutive feedback messages. When use_feedback_interval_for_virtual_rtt is enabled (default true), ReactionInterval() is defined as max(virtual_rtt, feedback_interval_) instead of fixed VIRTUAL_RTT. See scream_v2.cc:L115-L119 and scream_v2.cc:L406-L419.LossEstimator):loss_rate filter in Section 4.5.2.reported_lost_for_the_first_time and reported_recovered_for_the_first_time) and passes them to LossEstimator (loss_estimator.cc), which implements a biased asymmetric step filter (leaky bucket) tracking congestion_level_ in [0.0, 1.0] at RTT boundaries (max(virtual_rtt, rtt)):unrecovered_lost_packets_, incrementing on first-time loss reports and decrementing on subsequent out-of-order recovery reports (and resetting after a full RTT without losses or recoveries). If unrecovered_lost_packets_ drops to 0, congestion_level_ and loss_event_this_rtt_ are immediately reset to 0.1.0 / rtts_with_loss_before_backoff (default +1/3) on an RTT with loss, and steps down by 1.0 / lossless_rtts_before_clear (default -1/2) on a lossless RTT. This produces a net-negative drift under low uniform random wireless loss (e.g., 1% packet loss) while reaching congested() (>= 0.99) after 3 consecutive RTTs with loss.loss_estimator_.congested() (congestion_level_ >= 0.99) or queue delay congestion (IsQueueDelayDetected(), i.e. queue_delay_avg_ > queue_delay_target / 2) is simultaneously active. See scream_v2.cc:L203-L210.loss_congestion_level() >= 0.01 (unless simultaneously backing off in the same feedback). Because congestion_level_ decays by 0.5 per lossless RTT, it takes 2 full lossless RTTs after peak loss congestion for window growth to resume. See scream_v2.cc:L284.IS_L4S == false) where ref_wnd is scaled by a fixed BETA_ECN (default 0.8) (Section 4.2.1.2).l4s_alpha Update Frequency and RTT Gain Scaling:-01 accumulates delivered and marked data units and updates l4s_alpha at most once per min(10 ms, s_rtt) using an asymmetric fast-attack / slow-decay EWMA (L4S_AVG_G_UP = 1/8, L4S_AVG_G_DOWN = 1/128) (Section 4.2.1.3), while the latest editor's draft (draft-ietf-ccwg-rfc8298bis-screamv2-02) uses a symmetric EWMA with L4S_AVG_G = 1/16.l4s_alpha_ on every feedback message with received packets using a standard EWMA filter with l4s_avg_g = 1/16 (scream_v2_parameters.cc:L27).scale_l4s_alpha_with_rtt is enabled (default true), WebRTC divides l4s_avg_g by steps_per_rtt = max(1.0, rtt / ReactionInterval()) so that l4s_alpha_ converges at a consistent rate per RTT regardless of how many feedback reports arrive per RTT. See scream_v2.cc:L178-L194.queue_delay_avg_) Filtering and Update Frequency:qdelay_avg at most once per min(virtual_rtt, s_rtt) directly from the latest qdelay sample (Section 4.2.1.4).queue_delay_avg_ on every feedback message, passing min(min_queue_delay, last_queue_delay_sample_) (the minimum of the current and previous feedback's minimum queue delay) into the slow-attack (queue_delay_avg_g = 1/4), instantaneous-decay EWMA filter. Taking the minimum of two consecutive samples filters out single-feedback delay spikes. See delay_based_congestion_control.cc:L50-L75.ref_wnd_delay_scale):-01, the spec defines an optional REDUCE_JITTER mechanism that tracks qdelay_max_avg and qdelay_min_avg, computes an EWMA of their difference qdelay_dev_avg (QDELAY_DEV_AVG_G = 1/32), and derives a single scaling factor ref_wnd_delay_scale = clamp(1.0 - qdelay_dev_avg / QDELAY_DEV_THRESHOLD, 0.0, 1.0) with QDELAY_DEV_THRESHOLD = 10 ms (Section 4.2.1.4).delay_min_and_latency_diff_avg_g = 1/32 (input capped at 2 * threshold) and a linear mapping clamp(0.1 + 1.2 * (norm - avg) / norm, min_val, 1.0) that is 1.0 at or below norm / 4 (2.5 ms) and 0.1 at norm (10 ms):ref_window_scale_factor_due_to_avg_min_delay (threshold queue_delay_min_threshold = 10 ms): Based on queue_delay_min_avg_ (EWMA of min_queue_delay per feedback). Used to scale down L4S backoff, scale down reference window increase, and (with allow_zero = true, reaching 0.0 at 13/12 * norm) scale ref_window_overhead in max_data_in_flight(). See delay_based_congestion_control.cc:L125-L131.ref_window_scale_factor_due_to_latency_difference (threshold latency_diff_threshold = 10 ms): Based on latency_difference_avg_ (EWMA of max_one_way_delay - min_one_way_delay within the feedback's burst window). Used to scale down reference window increase when intra-burst delay variation indicates the send rate is close to bottleneck capacity. See delay_based_congestion_control.cc:L134-L140.now - last_reaction_to_congestion_time >= min(VIRTUAL_RTT, s_rtt) (Section 4.2.2.1).min(rtt, ReactionInterval()), where ReactionInterval() is max(virtual_rtt, feedback_interval_) when use_feedback_interval_for_virtual_rtt is true. See scream_v2.cc:L218-L220.ScaleBackoff) & In-Flight Burst Compensation:backoff_t linearly by dividing by max(1.0, s_rtt / VIRTUAL_RTT) on every reaction interval during an RTT (Section 4.2.2.1).use_non_linear_backoff_scale is enabled (default true), WebRTC uses a stateful 4-state backoff scaler in ScreamV2::ScaleBackoff() (scream_v2.cc:L121-L176):backoff on the first congestion event of a cycle, rather than spreading it across multiple feedbacks over the RTT.data_in_flight > ref_window_ (for up to one RTT after the last window decrease), since bottleneck queues cannot drain until actual in-flight bytes drop to ref_window_.backoff increases above last_backoff_, only the incremental delta (backoff - last_backoff_) / (1.0 - last_backoff_) is applied; otherwise 0.0 is returned.ref_window_share): If data_in_flight > ref_window_ * bytes_in_flight_head_room, the backoff is scaled by ref_window_ / data_in_flight so transient bursts (e.g., keyframes) do not cause an excessive reduction of ref_window_.allow_initial_ref_window_clamping is enabled (default true), on the very first congestion reaction (if received_rate_ is finite), WebRTC clamps ref_window_ down to max(min_ref_window, received_rate_ * (rtt + feedback_hold_time_)) if that produces a larger reduction than backoff. This quickly corrects an overly high initial starting bitrate. See scream_v2.cc:L265-L278.ref_window_i_) Scaling Factor:scl_t = ((ref_wnd - ref_wnd_i) / ref_wnd_i) * 8 (Section 4.2.2.1).backoff_scale_factor_close_to_ref_window_i in scream_v2_parameters.cc:L43-L45), resulting in slower, more cautious window changes near ref_window_i_.max(0.25, scl_t) and max(0.25, ref_wnd_delay_scale) when qdelay < qdelay_target * 0.25 (Section 4.2.2.1).!delay_based_congestion_control_.IsQueueDelayDetected(), which corresponds to queue_delay_avg_ <= queue_delay_target / 2 (default 30 ms), and uses a lower floor of 0.1 (instead of 0.25) for ref_window_scale_factor_due_to_avg_min_delay(). See scream_v2.cc:L228-L240.ref_window_i_) Update Timing:ref_wnd_i is updated to ref_wnd before the reduction whenever is_loss_t || is_ce_t || is_virtual_ce_t is true and ref_wnd_i_update_allowed is true, and ref_wnd_i_update_allowed is re-enabled when ref_wnd increases during the increase step (Section 4.2.2.1).ref_window_i_ and allow_ref_window_i_update_ are updated at the end of UpdateRefWindow() based on the net change in ref_window_ compared to previous_ref_window (recording previous_ref_window only on a net decrease, and re-arming allow_ref_window_i_update_ only on a net increase). See scream_v2.cc:L340-L352.!is_ce && loss_congestion_level() < 0.01 && !is_virtual_ce) or a congestion backoff reaction occurred on the current feedback (last_reaction_to_congestion_time_ == parsed.feedback_time). If congestion is present on a feedback that is within ReactionInterval() of the previous reaction, window growth is skipped. See scream_v2.cc:L284-L285.feedback_hold_time_:min(1.0, s_rtt / VIRTUAL_RTT) (Section 4.2.2.2).feedback_hold_time_ in the numerator when scaling for small RTTs: (rtt + feedback_hold_time_) / virtual_rtt. See scream_v2.cc:L294-L300.ref_wnd growth using max_allowed_t = MSS + max(max_bytes_in_flight, max_bytes_in_flight_prev) * BYTES_IN_FLIGHT_HEAD_ROOM and target_bitrate < TARGET_BITRATE_MAX (Section 4.2.2.2), and notes in Section 6 that RTT should be updated with a longer time constant when bytes in flight is low.max_allowed_ref_window() clamping (scream_v2.cc:L337-L341) and an explicit is_application_limited_ (ALR) state (enabled via enable_alr, default true), entered when max_allowed_ref_window() < ref_window_ and received_rate_ < alr_threshold * target_rate_ (with alr_threshold = 0.9, where received_rate_ is measured over received_rate_window = 100 ms). See scream_v2.cc:L69-L73.is_application_limited_ is true:smoothed_rtt_avg_g (1/8) to smoothed_rtt_avg_in_alr_g (1/128). See delay_based_congestion_control.cc:L104-L106.feedback_hold_time_ updates are frozen. See scream_v2.cc:L78-L80.ScreamNetworkController reports is_bandwidth_limited = false in TargetTransferRate. See scream_network_controller.cc:L239.ScreamV2::max_data_in_flight() (scream_v2.cc:L371-L382) computes ref_window_ * ref_window_overhead with ref_window_overhead interpolated between ref_window_overhead_min (1.5) and ref_window_overhead_max (3.0) as in Section 4.3.1, using ref_window_scale_factor_due_to_avg_min_delay(/*allow_zero=*/true) in place of ref_wnd_delay_scale.ScreamNetworkController exports max_data_in_flight() as update.congestion_window to WebRTC's pacer/transport (scream_network_controller.cc:L276).PACKET_PACING_HEADROOM = 1.5, RATE_PACE_MIN = 50 kbps, and a “relaxed pacing” multiplier (MAX_RELAXED_PACING_FACTOR = 4.0, RELAXED_PACING_LIMIT_LOW = 0.8) that increases pace_bitrate up to 4x when target_bitrate approaches TARGET_BITRATE_MAX (Section 4.3.2).ScreamV2::pacing_rate() computes max(target_rate() * pacing_factor, pacing_rate_received_factor * received_rate_) with pacing_factor = 1.1 and pacing_rate_received_factor = 0.8 (scream_v2.h:L50-L57).ScreamNetworkController::GetPacingRate() floors this at min_total_allocated_bitrate * pacing_factor (scream_network_controller.cc:L215-L220).ScreamNetworkController::MaybeCreatePacerConfig() adjusts the WebRTC pacer's burst time window (current_pacing_window_): it uses a tight 10 ms pacing window after recent congestion, and relaxes to default_pacing_window_ once allow_large_pacing_bursts_after_congestion_time (default 15 s) has elapsed since last_reaction_to_congestion_time(). See scream_network_controller.cc:L284-L289.ScreamNetworkController implements initial BWE probing (initial_probing_duration = 6 s) and periodic padding bursts (periodic_padding_duration = 3 s every time_between_periodic_padding = 3 s, gated by allow_padding_after_last_congestion_time = 1 s since the last reference window decrease) when target_rate is below the maximum allocated/target bitrate. See scream_network_controller.cc:L64-L89 and scream_network_controller.cc:L291-L326.target_bitrate = tmp_t * 8 * ref_wnd / s_rtt, where tmp_t includes:1.0 - clamp(ref_wnd_ratio - 0.1, 0.0, 0.2),MSS / (MSS + PACKET_OVERHEAD), and1 / (1.2 + rate_adjust_factor + frame_size_dev) (Section 4.4).ScreamV2::UpdateTargetRate() includes the small-window scaling (1.0 - clamp(ref_window_mss_ratio() - 0.1, 0.0, 0.2)) and computes scale_target_rate * (ref_window_ / (non_zero_smoothed_rtt + feedback_hold_time_)), clamped to [min_target_bitrate_, max_target_bitrate_]. It omits the MSS / (MSS + PACKET_OVERHEAD) factor and the 1.2 + rate_adjust_factor + frame_size_dev divisor (noted as a TODO in scream_v2.cc:L458-L459).rate_adjust_factor and frame_size_dev inside ScreamV2, ScreamNetworkController monitors the WebRTC pacer queue delay (pacing_delay = pacer_queue_size_ / pacing_rate):cwnd_reduce_ratio Pushback: Scales linearly from 0.0 at min_pacing_delay_for_pushback (default 100 ms) to 1.0 at max_pacing_delay_for_pushback (default 500 ms) and is reported in TargetTransferRate::cwnd_reduce_ratio to push back video encoders. See scream_network_controller.cc:L222-L235.data_in_flight_ > max_data_in_flight() and pacing_delay > max_pacing_delay_for_pushback (500 ms), ScreamNetworkController sets encoder_paused_due_to_congestion_ = true and reports a target_rate of 0 until !is_congested and pacing_delay < min_pacing_delay_for_pushback (100 ms). See scream_network_controller.cc:L246-L258.qdelay_min per RTT into a slow EWMA qdelay_min_avg (QDELAY_MIN_AVG_ALPHA = 1/256). If qdelay_min_avg > qdelay_target / 4, it resets the queue delay history and halves target_bitrate for max(5 * s_rtt, 0.2 s) (Section 4.5.1).WindowedMinFilter<TimeDelta> over base_delay_window_length = 10 intervals of base_delay_history_update_interval = 1 minute (delay_based_congestion_control.cc:L56-L61).qdelay_target / 4 and resetting before draining, WebRTC checks whether min_queue_delay has remained continuously above queue_delay_drain_threshold (default 5 ms) for queue_delay_drain_period (default 20 s) via IsQueueDrainedInTime(). When exceeded, UpdateTargetRate() first halves target_rate (0.5 * target_rate) for max(100 ms, queue_delay_drain_rtts * smoothed_rtt) (default 5 RTTs) to attempt to drain a standing self-inflicted queue. Only if min_queue_delay fails to drop below the threshold by the end of that drain period does WebRTC call ResetQueueDelay() to reset the base delay history. See scream_v2.cc:L435-L456.loss_rate filter in Section 4.5.2, WebRTC filters non-congestion link-layer losses using LossEstimator‘s biased asymmetric step filter (see Section 4.2.1.1 above), while sharing the spec’s principle of triggering immediate loss backoff when queue delay is also elevated (is_virtual_ce, i.e., queue_delay_avg_ > queue_delay_target / 2 vs. qdelay_avg > qdelay_target / 4 in the spec).max_policed_ref_wnd cap (LOSS_RATE_THRESHOLD_POLICER, BETA_LOSS_POLICER) in Section 4.5.2 are not implemented in WebRTC.if target_bitrate < acked_bitrate * ACKED_BITRATE_MARGIN, with ACKED_BITRATE_MARGIN = 0.8, scale backoff_t by BACKOFF_SCALE_LOW_TARGETRATE = 0.25) to avoid undershooting when an RTT spike has already depressed target_bitrate (Section 4.5.3).ACKED_BITRATE_MARGIN or BACKOFF_SCALE_LOW_TARGETRATE. Instead, WebRTC mitigates backoff undershoot via ScaleBackoff() (waiting one full RTT after data_in_flight <= ref_window_ before starting a new full reduction, and scaling backoff by ref_window_ / data_in_flight when in-flight bytes exceed ref_window_ * bytes_in_flight_head_room; see Section 4.2.2.1).received_rate_ (acknowledged bitrate over a 100 ms window) to floor the pacing rate (0.8 * received_rate_) so pacing does not collapse if target_rate() temporarily dips during an RTT spike (scream_v2.h:L51-L55).qdelay_target adjustment (adjust_qdelay_target between QDELAY_TARGET_LO = 60 ms and QDELAY_TARGET_HI = 400 ms) to compete with aggressive loss-based flows over tail-drop queues (Section 4.5.4).queue_delay_target remains static (default 60 ms). This is noted as a TODO in scream_v2_parameters.h:L121-L122.