/* * Copyright (c) 2026 The WebRTC project authors. All Rights Reserved. * * Use of this source code is governed by a BSD-style license * that can be found in the LICENSE file in the root of the source * tree. An additional intellectual property rights grant can be found * in the file PATENTS. All contributing project authors may * be found in the AUTHORS file in the root of the source tree. */ #include "modules/congestion_controller/ect1_policy.h" #include #include #include "api/units/time_delta.h" #include "api/units/timestamp.h" #include "rtc_base/logging.h" namespace webrtc { namespace { // How long ECT(1) packets must be sent without any feedback arriving before // assuming the path drops them. The timeout is short until an ECT(1) packet // has been acknowledged, so that a path that drops them from the start is // detected quickly. Once the path is known to deliver them, the timeout // matches the ICE unwritable timeout, so that an outage affecting all packets // has made ICE look for another route before the marking is blamed for it. constexpr TimeDelta kEct1FeedbackTimeoutBeforeAck = TimeDelta::Millis(500); constexpr TimeDelta kEct1FeedbackTimeoutAfterAck = TimeDelta::Seconds(5); // The most a single gap between two sent packets can contribute to the time // spent sending, so that idle time does not count toward the timeout. A sender // that sends rarely, such as a screencast of static content without audio, // therefore needs longer to reach the timeout. That delays detection but can // not cause a false one. constexpr TimeDelta kMaxCountedSendGap = TimeDelta::Millis(200); // Resuming after a pause can then never by itself trigger detection. A sender // that gets no feedback still reaches the timeout, because the pacer keeps // sending a packet every kCongestedPacketInterval. static_assert(kMaxCountedSendGap < kEct1FeedbackTimeoutBeforeAck); // Sending ECT(1) is retried once per route this long after the path was judged // to drop the marking. A round trip time longer than the timeout used before // the first acknowledgement makes that judgement wrong, and the round trip // time can also improve during a call. constexpr TimeDelta kEct1RetryDelay = TimeDelta::Seconds(10); } // namespace void Ect1Policy::SetFeedbackSupportsEcn(bool supports_ecn) { feedback_supports_ecn_ = supports_ecn; LogIfDecisionChanged(); } void Ect1Policy::SetCongestionControllerSupportsEcn(bool supports_ecn) { congestion_controller_supports_ecn_ = supports_ecn; LogIfDecisionChanged(); } void Ect1Policy::OnNetworkRouteChanged() { // The new path may handle the ECT(1) marking even if the old one did not. route_bleaches_ect1_ = false; ect1_drop_detected_time_ = std::nullopt; ect1_drop_retried_ = false; ect1_preserved_on_route_ = false; time_sending_ect1_without_feedback_ = TimeDelta::Zero(); LogIfDecisionChanged(); } void Ect1Policy::OnPacketsFeedback(bool has_bleached_ect1, bool has_preserved_ect1) { // Feedback of any kind proves that packets reach the receiver, so the path // is not dropping them and the time spent sending starts over. time_sending_ect1_without_feedback_ = TimeDelta::Zero(); if (has_preserved_ect1) { ect1_preserved_on_route_ = true; } if (has_bleached_ect1 && !route_bleaches_ect1_) { route_bleaches_ect1_ = true; RTC_LOG(LS_INFO) << "Transport does not preserve the ECT(1) marking. Stop " "sending ECT(1) on this route."; } LogIfDecisionChanged(); } void Ect1Policy::OnPacketSent(Timestamp send_time, bool sent_as_ect1) { if (ect1_drop_detected_time_.has_value() && !ect1_drop_retried_ && send_time - *ect1_drop_detected_time_ > kEct1RetryDelay) { ect1_drop_retried_ = true; // Clearing the drop detection time makes ShouldSendEct1() true again. ect1_drop_detected_time_ = std::nullopt; time_sending_ect1_without_feedback_ = TimeDelta::Zero(); RTC_LOG(LS_INFO) << "Retrying ECT(1) on this route."; } if (sent_as_ect1) { if (last_send_time_.has_value()) { time_sending_ect1_without_feedback_ += std::min(send_time - *last_send_time_, kMaxCountedSendGap); } TimeDelta timeout = Ect1FeedbackTimeout(); if (!ect1_drop_detected_time_.has_value() && time_sending_ect1_without_feedback_ > timeout) { ect1_drop_detected_time_ = send_time; RTC_LOG(LS_INFO) << "No feedback while sending ECT(1) packets during " << ToString(timeout) << ". Assuming the transport drops them. Stop sending " "ECT(1) on this route."; } } last_send_time_ = send_time; LogIfDecisionChanged(); } TimeDelta Ect1Policy::Ect1FeedbackTimeout() const { return ect1_preserved_on_route_ ? kEct1FeedbackTimeoutAfterAck : kEct1FeedbackTimeoutBeforeAck; } bool Ect1Policy::ShouldSendEct1() const { return feedback_supports_ecn_ && congestion_controller_supports_ecn_ && !route_bleaches_ect1_ && !ect1_drop_detected_time_.has_value(); } void Ect1Policy::LogIfDecisionChanged() { bool send_ect1 = ShouldSendEct1(); if (send_ect1 == logged_send_ect1_) { return; } logged_send_ect1_ = send_ect1; RTC_LOG(LS_INFO) << "Sending packets as " << (send_ect1 ? "ECT(1)." : "Not-ECT."); } } // namespace webrtc