Add `RtcpRttCalculator`. This class takes a sequences of RTCP messages (SR, RR, XR) and calculates individual RTT samples. It will be integrated into the video timing simulator in an upcoming change. Bug: b/423646186 Change-Id: I59661a50a1491cd0dcf79b68d9d9d91519a8d1cd Reviewed-on: https://webrtc-review.googlesource.com/c/src/+/478882 Commit-Queue: Rasmus Brandt <brandtr@webrtc.org> Reviewed-by: Åsa Persson <asapersson@webrtc.org> Cr-Commit-Position: refs/heads/main@{#47922}
diff --git a/video/timing/simulator/BUILD.gn b/video/timing/simulator/BUILD.gn index 57f1770..b65d6e8 100644 --- a/video/timing/simulator/BUILD.gn +++ b/video/timing/simulator/BUILD.gn
@@ -30,6 +30,8 @@ "results_base.h", "rtc_event_log_driver.cc", "rtc_event_log_driver.h", + "rtcp_rtt_calculator.cc", + "rtcp_rtt_calculator.h", "rtp_packet_simulator.cc", "rtp_packet_simulator.h", "stream_base.h", @@ -61,6 +63,7 @@ "../../../logging:rtc_event_rtp_rtcp", "../../../logging:rtc_event_video", "../../../modules/rtp_rtcp", + "../../../modules/rtp_rtcp:ntp_time_util", "../../../modules/rtp_rtcp:rtp_rtcp_format", "../../../modules/video_coding:nack_requester", "../../../modules/video_coding/timing:timing_module", @@ -68,6 +71,7 @@ "../../../rtc_base:logging", "../../../rtc_base:macromagic", "../../../rtc_base:rtc_numerics", + "../../../system_wrappers", "../../../test/time_controller:simulated_time_task_queue_controller", "../../../video", "../../../video:task_queue_frame_decode_scheduler", @@ -79,6 +83,7 @@ "//third_party/abseil-cpp/absl/container:flat_hash_set", "//third_party/abseil-cpp/absl/container:inlined_vector", "//third_party/abseil-cpp/absl/functional:any_invocable", + "//third_party/abseil-cpp/absl/hash", "//third_party/abseil-cpp/absl/strings:string_view", ] } @@ -97,6 +102,7 @@ "rendering_tracker_unittest.cc", "results_base_unittest.cc", "rtc_event_log_driver_unittest.cc", + "rtcp_rtt_calculator_unittest.cc", "rtp_packet_simulator_unittest.cc", "stream_base_unittest.cc", ] @@ -115,12 +121,14 @@ "../../../api/video:video_frame", "../../../logging:rtc_event_log_parser", "../../../logging:rtc_event_rtp_rtcp", + "../../../modules/rtp_rtcp:ntp_time_util", "../../../modules/rtp_rtcp:rtp_rtcp_format", "../../../modules/video_coding/timing:timing_module", "../../../rtc_base:checks", "../../../rtc_base:macromagic", "../../../system_wrappers", "../../../test:create_test_environment", + "../../../test:near_matcher", "../../../test:test_support", "test", "//third_party/abseil-cpp/absl/algorithm:container",
diff --git a/video/timing/simulator/rtcp_rtt_calculator.cc b/video/timing/simulator/rtcp_rtt_calculator.cc new file mode 100644 index 0000000..4c4380e --- /dev/null +++ b/video/timing/simulator/rtcp_rtt_calculator.cc
@@ -0,0 +1,185 @@ +/* + * 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 "video/timing/simulator/rtcp_rtt_calculator.h" + +#include <cstdint> +#include <optional> +#include <vector> + +#include "absl/container/flat_hash_map.h" +#include "api/sequence_checker.h" +#include "api/units/time_delta.h" +#include "api/units/timestamp.h" +#include "modules/rtp_rtcp/source/ntp_time_util.h" +#include "modules/rtp_rtcp/source/rtcp_packet/extended_reports.h" +#include "modules/rtp_rtcp/source/rtcp_packet/receiver_report.h" +#include "modules/rtp_rtcp/source/rtcp_packet/report_block.h" +#include "modules/rtp_rtcp/source/rtcp_packet/sender_report.h" +#include "rtc_base/checks.h" +#include "rtc_base/logging.h" + +namespace webrtc::video_timing_simulator { + +RtcpRttCalculator::RtcpRttCalculator() = default; + +RtcpRttCalculator::~RtcpRttCalculator() { + RTC_DCHECK_RUN_ON(&sequence_checker_); +} + +void RtcpRttCalculator::OnOutgoingSenderReport(const rtcp::SenderReport& sr, + Timestamp now) { + RTC_DCHECK_RUN_ON(&sequence_checker_); + RTC_DCHECK(now.IsFinite()); + CleanOldReports(now); + // https://www.rfc-editor.org/info/rfc3550/#section-6.4.1 + uint32_t compact_ntp = CompactNtp(sr.ntp()); + if (compact_ntp == 0) { + return; + } + outgoing_srs_[{sr.sender_ssrc(), compact_ntp}] = + SentReportValue{.sent_time = now}; +} + +void RtcpRttCalculator::OnOutgoingExtendedReports( + const rtcp::ExtendedReports& xr, + Timestamp now) { + RTC_DCHECK_RUN_ON(&sequence_checker_); + RTC_DCHECK(now.IsFinite()); + CleanOldReports(now); + if (!xr.rrtr().has_value()) { + return; + } + // https://www.rfc-editor.org/info/rfc3611/#section-4.4 + uint32_t compact_ntp = CompactNtp(xr.rrtr()->ntp()); + if (compact_ntp == 0) { + return; + } + outgoing_xrs_[{xr.sender_ssrc(), compact_ntp}] = + SentReportValue{.sent_time = now}; +} + +std::vector<TimeDelta> RtcpRttCalculator::OnIncomingSenderReport( + const rtcp::SenderReport& sr, + Timestamp now) { + RTC_DCHECK_RUN_ON(&sequence_checker_); + RTC_DCHECK(now.IsFinite()); + CleanOldReports(now); + return ProcessReportBlocks(sr.report_blocks(), now); +} + +std::vector<TimeDelta> RtcpRttCalculator::OnIncomingReceiverReport( + const rtcp::ReceiverReport& rr, + Timestamp now) { + RTC_DCHECK_RUN_ON(&sequence_checker_); + RTC_DCHECK(now.IsFinite()); + CleanOldReports(now); + return ProcessReportBlocks(rr.report_blocks(), now); +} + +std::vector<TimeDelta> RtcpRttCalculator::OnIncomingExtendedReports( + const rtcp::ExtendedReports& xr, + Timestamp now) { + RTC_DCHECK_RUN_ON(&sequence_checker_); + RTC_DCHECK(now.IsFinite()); + CleanOldReports(now); + std::vector<TimeDelta> rtt_samples; + rtt_samples.reserve(xr.dlrr().sub_blocks().size()); + for (const auto& block : xr.dlrr().sub_blocks()) { + uint32_t sender_ssrc = block.ssrc; + uint32_t last_rr = block.last_rr; + // (Quotes from https://www.rfc-editor.org/info/rfc3611/#section-4.5) + // "If no such block has been received, the field is set to zero." + if (last_rr == 0) { + continue; + } + if (auto it = outgoing_xrs_.find({sender_ssrc, last_rr}); + it != outgoing_xrs_.end()) { + if (block.delay_since_last_rr == 0) { + // "If a Receiver Reference Time Report Block has yet to be received" + // "from SSRC_n, the DLRR field is set to zero (or the DLRR is " + // "omitted entirely). + continue; + } + TimeDelta delay_since_last_rr = + CompactNtpIntervalToTimeDelta(block.delay_since_last_rr); + // "It calculates the total round-trip time A-LRR using the" + // "last RR timestamp (LRR) field, and then subtracting this field to" + // "leave the round-trip propagation delay as A-LRR-DLRR." + TimeDelta rtt = now - it->second.sent_time - delay_since_last_rr; + if (rtt <= TimeDelta::Zero()) { + RTC_LOG(LS_INFO) << "Ignoring non-positive RTT: " << rtt.ms() + << "ms (now: " << now.ms() + << ", sent: " << it->second.sent_time.ms() + << ", delay_since_last_rr: " + << delay_since_last_rr.ms() << ")"; + continue; + } + rtt_samples.push_back(rtt); + } + } + return rtt_samples; +} + +std::vector<TimeDelta> RtcpRttCalculator::ProcessReportBlocks( + const std::vector<rtcp::ReportBlock>& report_blocks, + Timestamp now) { + RTC_DCHECK_RUN_ON(&sequence_checker_); + RTC_DCHECK(now.IsFinite()); + std::vector<TimeDelta> rtt_samples; + rtt_samples.reserve(report_blocks.size()); + for (const auto& block : report_blocks) { + uint32_t sender_ssrc = block.source_ssrc(); + uint32_t last_sr = block.last_sr(); + // (Quotes from https://www.rfc-editor.org/info/rfc3550/#section-6.4.1) + if (last_sr == 0) { + // "If no SR has been received yet, the field is set to zero." + continue; + } + if (auto it = outgoing_srs_.find({sender_ssrc, last_sr}); + it != outgoing_srs_.end()) { + if (block.delay_since_last_sr() == 0) { + // "If no SR packet has been received yet from SSRC_n, the DLSR field " + // "is set to zero." + continue; + } + TimeDelta delay_since_last_sr = + CompactNtpIntervalToTimeDelta(block.delay_since_last_sr()); + // "It calculates the total round-trip time A-LSR using the" + // "last SR timestamp (LSR) field, and then subtracting this field to" + // "leave the round-trip propagation delay as (A - LSR - DLSR)." + TimeDelta rtt = now - it->second.sent_time - delay_since_last_sr; + if (rtt <= TimeDelta::Zero()) { + RTC_LOG(LS_INFO) << "Ignoring non-positive RTT: " << rtt.ms() + << "ms (now: " << now.ms() + << ", sent: " << it->second.sent_time.ms() + << ", delay_since_last_sr: " + << delay_since_last_sr.ms() << ")"; + continue; + } + rtt_samples.push_back(rtt); + } + } + return rtt_samples; +} + +void RtcpRttCalculator::CleanOldReports(Timestamp now) { + RTC_DCHECK_RUN_ON(&sequence_checker_); + RTC_DCHECK(now.IsFinite()); + constexpr TimeDelta kCleanupTimeout = TimeDelta::Minutes(1); + absl::erase_if(outgoing_srs_, [&](const auto& kv) { + return now - kv.second.sent_time > kCleanupTimeout; + }); + absl::erase_if(outgoing_xrs_, [&](const auto& kv) { + return now - kv.second.sent_time > kCleanupTimeout; + }); +} + +} // namespace webrtc::video_timing_simulator
diff --git a/video/timing/simulator/rtcp_rtt_calculator.h b/video/timing/simulator/rtcp_rtt_calculator.h new file mode 100644 index 0000000..4835d44 --- /dev/null +++ b/video/timing/simulator/rtcp_rtt_calculator.h
@@ -0,0 +1,100 @@ +/* + * 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. + */ + +#ifndef VIDEO_TIMING_SIMULATOR_RTCP_RTT_CALCULATOR_H_ +#define VIDEO_TIMING_SIMULATOR_RTCP_RTT_CALCULATOR_H_ + +#include <cstdint> +#include <utility> +#include <vector> + +#include "absl/container/flat_hash_map.h" +#include "api/sequence_checker.h" +#include "api/units/time_delta.h" +#include "api/units/timestamp.h" +#include "modules/rtp_rtcp/source/rtcp_packet/extended_reports.h" +#include "modules/rtp_rtcp/source/rtcp_packet/receiver_report.h" +#include "modules/rtp_rtcp/source/rtcp_packet/report_block.h" +#include "modules/rtp_rtcp/source/rtcp_packet/sender_report.h" +#include "rtc_base/thread_annotations.h" + +namespace webrtc::video_timing_simulator { + +// Calculates Round Trip Time (RTT) samples by pairing outgoing reports with +// incoming feedback. It covers all standard RTCP RTT mechanisms: +// +// 1. Sender RTT (SR/RR): +// Maps outgoing Sender Reports (SR) to incoming Receiver Reports (RR) or +// incoming Sender Reports (SR) containing reception report blocks. +// +// 2. Receiver RTT (XR RRTR/DLRR): +// Maps outgoing Extended Reports (XR) containing Receiver Reference Time +// Reports (RRTR) to incoming XRs containing Delay since Last RR (DLRR). +// +// Outgoing reports are cached for up to 1 minute before being cleaned up. +class RtcpRttCalculator { + public: + RtcpRttCalculator(); + ~RtcpRttCalculator(); + + RtcpRttCalculator(const RtcpRttCalculator&) = delete; + RtcpRttCalculator& operator=(const RtcpRttCalculator&) = delete; + + // Registers an outgoing SR to map incoming RRs to. + void OnOutgoingSenderReport(const rtcp::SenderReport& sr, Timestamp now); + + // Registers an outgoing XR (specifically RRTR) to map incoming XRs (DLRR) to. + void OnOutgoingExtendedReports(const rtcp::ExtendedReports& xr, + Timestamp now); + + // Processes an incoming SR and returns a list of calculated RTTs. + std::vector<TimeDelta> OnIncomingSenderReport(const rtcp::SenderReport& sr, + Timestamp now); + + // Processes an incoming RR and returns a list of calculated RTTs. + std::vector<TimeDelta> OnIncomingReceiverReport( + const rtcp::ReceiverReport& rr, + Timestamp now); + + // Processes an incoming XR and returns a list of calculated RTTs. + std::vector<TimeDelta> OnIncomingExtendedReports( + const rtcp::ExtendedReports& xr, + Timestamp now); + + private: + // See https://www.rfc-editor.org/info/rfc3550/#section-6.4.1. + using SentReportKey = + std::pair</*sender_ssrc=*/uint32_t, /*compact_ntp=*/uint32_t>; + struct SentReportValue { + Timestamp sent_time = Timestamp::MinusInfinity(); + }; + + // Helper to process report blocks from either SR or RR. + std::vector<TimeDelta> ProcessReportBlocks( + const std::vector<rtcp::ReportBlock>& report_blocks, + Timestamp now); + + // Cleans up registered outgoing reports that are older than 1 minute. + void CleanOldReports(Timestamp now); + + SequenceChecker sequence_checker_; + + // Outgoing SRs: (sender_ssrc, lsr) -> sent_time. + absl::flat_hash_map<SentReportKey, SentReportValue> outgoing_srs_ + RTC_GUARDED_BY(sequence_checker_); + + // Outgoing XRs: (sender_ssrc, lrr) -> sent_time. + absl::flat_hash_map<SentReportKey, SentReportValue> outgoing_xrs_ + RTC_GUARDED_BY(sequence_checker_); +}; + +} // namespace webrtc::video_timing_simulator + +#endif // VIDEO_TIMING_SIMULATOR_RTCP_RTT_CALCULATOR_H_
diff --git a/video/timing/simulator/rtcp_rtt_calculator_unittest.cc b/video/timing/simulator/rtcp_rtt_calculator_unittest.cc new file mode 100644 index 0000000..bc5a527 --- /dev/null +++ b/video/timing/simulator/rtcp_rtt_calculator_unittest.cc
@@ -0,0 +1,242 @@ +/* + * 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 "video/timing/simulator/rtcp_rtt_calculator.h" + +#include <cstdint> +#include <vector> + +#include "api/units/time_delta.h" +#include "api/units/timestamp.h" +#include "modules/rtp_rtcp/source/ntp_time_util.h" +#include "modules/rtp_rtcp/source/rtcp_packet/dlrr.h" +#include "modules/rtp_rtcp/source/rtcp_packet/extended_reports.h" +#include "modules/rtp_rtcp/source/rtcp_packet/receiver_report.h" +#include "modules/rtp_rtcp/source/rtcp_packet/report_block.h" +#include "modules/rtp_rtcp/source/rtcp_packet/rrtr.h" +#include "modules/rtp_rtcp/source/rtcp_packet/sender_report.h" +#include "system_wrappers/include/clock.h" +#include "system_wrappers/include/ntp_time.h" +#include "test/gmock.h" +#include "test/gtest.h" +#include "test/near_matcher.h" + +namespace webrtc::video_timing_simulator { +namespace { + +using ::testing::ElementsAre; +using ::testing::IsEmpty; + +constexpr uint32_t kSenderSsrc = 123456; +constexpr uint32_t kReceiverSsrc = 987654; + +rtcp::SenderReport CreateSr(NtpTime ntp) { + rtcp::SenderReport sr; + sr.SetSenderSsrc(kSenderSsrc); + sr.SetNtp(ntp); + return sr; +} + +rtcp::SenderReport CreateSrWithReportBlock(NtpTime ntp, + uint32_t last_sr, + uint32_t delay_since_last_sr) { + rtcp::SenderReport sr = CreateSr(ntp); + rtcp::ReportBlock block; + block.SetMediaSsrc(kSenderSsrc); + block.SetLastSr(last_sr); + block.SetDelayLastSr(delay_since_last_sr); + sr.AddReportBlock(block); + return sr; +} + +rtcp::ReceiverReport CreateRrWithReportBlock(uint32_t last_sr, + uint32_t delay_since_last_sr) { + rtcp::ReceiverReport rr; + rr.SetSenderSsrc(kReceiverSsrc); + rtcp::ReportBlock block; + block.SetMediaSsrc(kSenderSsrc); + block.SetLastSr(last_sr); + block.SetDelayLastSr(delay_since_last_sr); + rr.AddReportBlock(block); + return rr; +} + +rtcp::ExtendedReports CreateXrWithRrtr(NtpTime ntp) { + rtcp::ExtendedReports xr; + xr.SetSenderSsrc(kReceiverSsrc); + rtcp::Rrtr rrtr; + rrtr.SetNtp(ntp); + xr.SetRrtr(rrtr); + return xr; +} + +rtcp::ExtendedReports CreateXrWithDlrr(uint32_t last_rr, + uint32_t delay_since_last_rr) { + rtcp::ExtendedReports xr; + xr.SetSenderSsrc(kSenderSsrc); + rtcp::ReceiveTimeInfo dlrr_block; + dlrr_block.ssrc = kReceiverSsrc; + dlrr_block.last_rr = last_rr; + dlrr_block.delay_since_last_rr = delay_since_last_rr; + xr.AddDlrrItem(dlrr_block); + return xr; +} + +TEST(RtcpRttCalculatorTest, SenderCalculatesRttFromIncomingSr) { + RtcpRttCalculator calculator; + SimulatedClock clock(Timestamp::Millis(10000)); + + // Outgoing SR. + NtpTime ntp = clock.ConvertTimestampToNtpTime(clock.CurrentTime()); + rtcp::SenderReport sr = CreateSr(ntp); + calculator.OnOutgoingSenderReport(sr, clock.CurrentTime()); + + // Incoming SR: 50ms delay, arrives after 150ms => RTT is 100ms. + clock.AdvanceTime(TimeDelta::Millis(150)); + uint32_t last_sr = CompactNtp(ntp); + uint32_t delay_since_last_sr = SaturatedToCompactNtp(TimeDelta::Millis(50)); + NtpTime ntp_incoming = clock.ConvertTimestampToNtpTime(clock.CurrentTime()); + rtcp::SenderReport sr_incoming = + CreateSrWithReportBlock(ntp_incoming, last_sr, delay_since_last_sr); + std::vector<TimeDelta> rtts = + calculator.OnIncomingSenderReport(sr_incoming, clock.CurrentTime()); + + EXPECT_THAT(rtts, ElementsAre(Near(TimeDelta::Millis(100)))); +} + +TEST(RtcpRttCalculatorTest, SenderCalculatesRttFromIncomingRr) { + RtcpRttCalculator calculator; + SimulatedClock clock(Timestamp::Millis(10000)); + + // Outgoing SR. + NtpTime ntp = clock.ConvertTimestampToNtpTime(clock.CurrentTime()); + rtcp::SenderReport sr = CreateSr(ntp); + calculator.OnOutgoingSenderReport(sr, clock.CurrentTime()); + + // Incoming RR: 50ms delay, arrives after 150ms => RTT is 100ms. + clock.AdvanceTime(TimeDelta::Millis(150)); + uint32_t last_sr = CompactNtp(ntp); + uint32_t delay_since_last_sr = SaturatedToCompactNtp(TimeDelta::Millis(50)); + rtcp::ReceiverReport rr = + CreateRrWithReportBlock(last_sr, delay_since_last_sr); + std::vector<TimeDelta> rtts = + calculator.OnIncomingReceiverReport(rr, clock.CurrentTime()); + + EXPECT_THAT(rtts, ElementsAre(Near(TimeDelta::Millis(100)))); +} + +TEST(RtcpRttCalculatorTest, ReceiverCalculatesRttFromIncomingXr) { + RtcpRttCalculator calculator; + SimulatedClock clock(Timestamp::Millis(10000)); + + // Outgoing XR with RRTR. + NtpTime ntp = clock.ConvertTimestampToNtpTime(clock.CurrentTime()); + rtcp::ExtendedReports xr_rrtr = CreateXrWithRrtr(ntp); + calculator.OnOutgoingExtendedReports(xr_rrtr, clock.CurrentTime()); + + // Incoming XR with DLRR: 50ms delay, arrives after 150ms => RTT is 100ms. + clock.AdvanceTime(TimeDelta::Millis(150)); + uint32_t last_rr = CompactNtp(ntp); + uint32_t delay_since_last_rr = SaturatedToCompactNtp(TimeDelta::Millis(50)); + rtcp::ExtendedReports xr_dlrr = + CreateXrWithDlrr(last_rr, delay_since_last_rr); + std::vector<TimeDelta> rtts = + calculator.OnIncomingExtendedReports(xr_dlrr, clock.CurrentTime()); + + EXPECT_THAT(rtts, ElementsAre(Near(TimeDelta::Millis(100)))); +} + +TEST(RtcpRttCalculatorTest, SenderCalculatesRttForMultipleRrsMatchingSingleSr) { + RtcpRttCalculator calculator; + SimulatedClock clock(Timestamp::Millis(10000)); + + // Single outgoing SR. + NtpTime ntp = clock.ConvertTimestampToNtpTime(clock.CurrentTime()); + rtcp::SenderReport sr = CreateSr(ntp); + calculator.OnOutgoingSenderReport(sr, clock.CurrentTime()); + + // First incoming RR: 50ms delay, arrives after 150ms => RTT is 100ms. + clock.AdvanceTime(TimeDelta::Millis(150)); + uint32_t last_sr = CompactNtp(ntp); + uint32_t delay_since_last_sr1 = SaturatedToCompactNtp(TimeDelta::Millis(50)); + rtcp::ReceiverReport rr1 = + CreateRrWithReportBlock(last_sr, delay_since_last_sr1); + std::vector<TimeDelta> rtts1 = + calculator.OnIncomingReceiverReport(rr1, clock.CurrentTime()); + EXPECT_THAT(rtts1, ElementsAre(Near(TimeDelta::Millis(100)))); + + // Second incoming RR: 150ms delay, arrives after another 100ms (total 250ms + // since SR) + // => RTT is 100ms. + clock.AdvanceTime(TimeDelta::Millis(100)); + uint32_t delay_since_last_sr2 = SaturatedToCompactNtp(TimeDelta::Millis(150)); + rtcp::ReceiverReport rr2 = + CreateRrWithReportBlock(last_sr, delay_since_last_sr2); + std::vector<TimeDelta> rtts2 = + calculator.OnIncomingReceiverReport(rr2, clock.CurrentTime()); + EXPECT_THAT(rtts2, ElementsAre(Near(TimeDelta::Millis(100)))); +} + +TEST(RtcpRttCalculatorTest, RejectsNegativeRtt) { + RtcpRttCalculator calculator; + SimulatedClock clock(Timestamp::Millis(10000)); + + // Outgoing SR. + NtpTime ntp = clock.ConvertTimestampToNtpTime(clock.CurrentTime()); + rtcp::SenderReport sr = CreateSr(ntp); + calculator.OnOutgoingSenderReport(sr, clock.CurrentTime()); + + // Incoming RR with delay 100ms (RTT would be 50ms - 100ms = -50ms). + clock.AdvanceTime(TimeDelta::Millis(50)); + uint32_t last_sr = CompactNtp(ntp); + uint32_t delay_since_last_sr = SaturatedToCompactNtp(TimeDelta::Millis(100)); + rtcp::ReceiverReport rr = + CreateRrWithReportBlock(last_sr, delay_since_last_sr); + std::vector<TimeDelta> rtts = + calculator.OnIncomingReceiverReport(rr, clock.CurrentTime()); + + EXPECT_THAT(rtts, IsEmpty()); +} + +TEST(RtcpRttCalculatorTest, IgnoresRrWithZeroLastSr) { + RtcpRttCalculator calculator; + SimulatedClock clock(Timestamp::Millis(10000)); + + // Incoming RR with last_sr = 0 (means no SR received yet). + rtcp::ReceiverReport rr = CreateRrWithReportBlock(/*last_sr=*/0, 0); + std::vector<TimeDelta> rtts = + calculator.OnIncomingReceiverReport(rr, clock.CurrentTime()); + + EXPECT_THAT(rtts, IsEmpty()); +} + +TEST(RtcpRttCalculatorTest, CleansOldReportsAfterOneMinute) { + RtcpRttCalculator calculator; + SimulatedClock clock(Timestamp::Millis(10000)); + + // Outgoing SR. + NtpTime ntp = clock.ConvertTimestampToNtpTime(clock.CurrentTime()); + rtcp::SenderReport sr = CreateSr(ntp); + calculator.OnOutgoingSenderReport(sr, clock.CurrentTime()); + + // Advance time by 61 seconds (timeout is 60 seconds). + clock.AdvanceTime(TimeDelta::Seconds(61)); + + // Incoming RR: should be ignored because the SR was cleaned up. + uint32_t last_sr = CompactNtp(ntp); + rtcp::ReceiverReport rr = CreateRrWithReportBlock(last_sr, 0); + std::vector<TimeDelta> rtts = + calculator.OnIncomingReceiverReport(rr, clock.CurrentTime()); + + EXPECT_THAT(rtts, IsEmpty()); +} + +} // namespace +} // namespace webrtc::video_timing_simulator