| /* | |
| * Copyright (c) 2025 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 "call/rtp_transport_controller_send.h" | |
| #include <cstdint> | |
| #include <optional> | |
| #include <utility> | |
| #include <vector> | |
| #include "api/rtp_parameters.h" | |
| #include "api/transport/ecn_marking.h" | |
| #include "api/units/time_delta.h" | |
| #include "api/units/timestamp.h" | |
| #include "modules/congestion_controller/rtp/congestion_controller_feedback_stats.h" | |
| #include "modules/rtp_rtcp/include/rtp_rtcp_defines.h" | |
| #include "modules/rtp_rtcp/source/rtcp_packet/congestion_control_feedback.h" | |
| #include "rtc_base/containers/flat_map.h" | |
| #include "rtc_base/thread.h" | |
| #include "test/create_test_environment.h" | |
| #include "test/gmock.h" | |
| #include "test/gtest.h" | |
| namespace webrtc { | |
| namespace { | |
| using ::testing::AllOf; | |
| using ::testing::Eq; | |
| using ::testing::Field; | |
| using ::testing::IsEmpty; | |
| using ::testing::UnorderedElementsAre; | |
| constexpr uint32_t kSsrc = 0x554c; | |
| class PacketSender { | |
| public: | |
| explicit PacketSender(RtpTransportControllerSend& transport) | |
| : transport_(transport) {} | |
| struct SentPacketsOptions { | |
| uint32_t ssrc = kSsrc; | |
| uint16_t first_sequence_number = 1; | |
| int num_packets = 1; | |
| bool send_as_ect1 = true; | |
| }; | |
| void SimulateSentPackets(SentPacketsOptions options) { | |
| uint16_t sequence_number = options.first_sequence_number; | |
| for (int i = 0; i < options.num_packets; ++i, ++sequence_number) { | |
| RtpPacketToSend rtp_packet(nullptr); | |
| rtp_packet.SetSsrc(options.ssrc); | |
| rtp_packet.SetSequenceNumber(sequence_number); | |
| rtp_packet.set_transport_sequence_number(++transport_sequence_number_); | |
| rtp_packet.set_packet_type(RtpPacketMediaType::kVideo); | |
| if (options.send_as_ect1) { | |
| rtp_packet.set_send_as_ect1(); | |
| } | |
| rtp_packet.SetPayloadSize(100); | |
| transport_.NotifyBweOfSentPacketForTesting(rtp_packet); | |
| } | |
| } | |
| private: | |
| int64_t transport_sequence_number_ = 0; | |
| RtpTransportControllerSend& transport_; | |
| }; | |
| struct FeedbakPacketTemplate { | |
| EcnMarking ecn = EcnMarking::kNotEct; | |
| // If absent, the SSRC defaults to the previous SSRC. | |
| std::optional<uint32_t> ssrc; | |
| // If absent, the sequence number defaults to the previous sequence number | |
| // plus one. | |
| std::optional<uint16_t> sequence_number; | |
| bool received = true; | |
| }; | |
| rtcp::CongestionControlFeedback GenerateFeedback( | |
| std::vector<FeedbakPacketTemplate> packets) { | |
| std::vector<rtcp::CongestionControlFeedback::PacketInfo> packet_infos; | |
| packet_infos.reserve(packets.size()); | |
| uint32_t ssrc = kSsrc; | |
| uint16_t sequence_number = 1; | |
| for (const FeedbakPacketTemplate& p : packets) { | |
| ssrc = p.ssrc.value_or(ssrc); | |
| sequence_number = p.sequence_number.value_or(sequence_number + 1); | |
| packet_infos.push_back( | |
| {.ssrc = ssrc, | |
| .sequence_number = sequence_number, | |
| .arrival_time_offset = | |
| p.received ? TimeDelta::Millis(10) : TimeDelta::MinusInfinity(), | |
| .ecn = p.ecn}); | |
| } | |
| return rtcp::CongestionControlFeedback(std::move(packet_infos), | |
| /*report_timestamp_compact_ntp=*/0); | |
| } | |
| TEST(RtpTransportControllerSendTest, | |
| IgnoresFeedbackForReportedReceivedPacketThatWereNotSent) { | |
| AutoThread main_thread; | |
| RtpTransportControllerSend transport({.env = CreateTestEnvironment()}); | |
| transport.SetPreferredRtcpCcAckType(RtcpFeedbackType::CCFB); | |
| PacketSender sender(transport); | |
| sender.SimulateSentPackets({.ssrc = 123, | |
| .first_sequence_number = 111, | |
| .num_packets = 10, | |
| .send_as_ect1 = true}); | |
| sender.SimulateSentPackets({.ssrc = 321, | |
| .first_sequence_number = 10'111, | |
| .num_packets = 10, | |
| .send_as_ect1 = true}); | |
| // Generate feedback for packets that weren't sent: reuse sequence number | |
| // range from 1st batch, but SSRC from the 2nd batch to double check sequence | |
| // numbers are checked per SSRC. | |
| transport.OnCongestionControlFeedback( | |
| /*receive_time=*/Timestamp::Seconds(123), | |
| GenerateFeedback( | |
| {{.ecn = EcnMarking::kEct1, .ssrc = 321, .sequence_number = 111}, | |
| {.ecn = EcnMarking::kEct1}, | |
| {.ecn = EcnMarking::kCe}})); | |
| EXPECT_THAT(transport.GetCongestionControlFeedbackStatsPerSsrc(), IsEmpty()); | |
| } | |
| TEST(RtpTransportControllerSendTest, | |
| AccumulatesNumberOfReportedReceivedPacketsPerSsrcPerEcnMarkingType) { | |
| constexpr uint32_t kSsrc1 = 1'000; | |
| constexpr uint32_t kSsrc2 = 2'000; | |
| AutoThread main_thread; | |
| RtpTransportControllerSend transport({.env = CreateTestEnvironment()}); | |
| transport.SetPreferredRtcpCcAckType(RtcpFeedbackType::CCFB); | |
| PacketSender sender(transport); | |
| sender.SimulateSentPackets( | |
| {.ssrc = kSsrc1, .first_sequence_number = 1, .num_packets = 10}); | |
| sender.SimulateSentPackets( | |
| {.ssrc = kSsrc2, .first_sequence_number = 101, .num_packets = 10}); | |
| transport.OnCongestionControlFeedback( | |
| /*receive_time=*/Timestamp::Seconds(123), | |
| GenerateFeedback( | |
| {{.ecn = EcnMarking::kEct1, .ssrc = kSsrc1, .sequence_number = 1}, | |
| {.ecn = EcnMarking::kEct1}, | |
| {.ecn = EcnMarking::kCe}, | |
| {.ecn = EcnMarking::kEct1, .ssrc = kSsrc2, .sequence_number = 101}, | |
| {.ecn = EcnMarking::kEct1}, | |
| {.ecn = EcnMarking::kEct1}, | |
| {.ecn = EcnMarking::kCe}, | |
| {.ecn = EcnMarking::kCe}, | |
| {.ecn = EcnMarking::kCe}, | |
| {.ecn = EcnMarking::kCe}})); | |
| flat_map<uint32_t, ReceivedCongestionControlFeedbackStats> stats = | |
| transport.GetCongestionControlFeedbackStatsPerSsrc(); | |
| EXPECT_EQ(stats[kSsrc1].num_packets_received_with_ect1, 2); | |
| EXPECT_EQ(stats[kSsrc1].num_packets_received_with_ce, 1); | |
| EXPECT_EQ(stats[kSsrc2].num_packets_received_with_ect1, 3); | |
| EXPECT_EQ(stats[kSsrc2].num_packets_received_with_ce, 4); | |
| transport.OnCongestionControlFeedback( | |
| /*receive_time=*/Timestamp::Seconds(123), | |
| GenerateFeedback( | |
| {{.ecn = EcnMarking::kEct1, .ssrc = kSsrc1, .sequence_number = 5}, | |
| {.ecn = EcnMarking::kEct1}, | |
| {.ecn = EcnMarking::kEct1}, | |
| {.ecn = EcnMarking::kCe}})); | |
| stats = transport.GetCongestionControlFeedbackStatsPerSsrc(); | |
| EXPECT_EQ(stats[kSsrc1].num_packets_received_with_ect1, 2 + 3); | |
| EXPECT_EQ(stats[kSsrc1].num_packets_received_with_ce, 1 + 1); | |
| } | |
| TEST(RtpTransportControllerSendTest, CalculatesNumberOfBleachedPackets) { | |
| AutoThread main_thread; | |
| RtpTransportControllerSend transport({.env = CreateTestEnvironment()}); | |
| transport.SetPreferredRtcpCcAckType(RtcpFeedbackType::CCFB); | |
| PacketSender sender(transport); | |
| // Packets send as ect1 and received without ect1 are the bleached packets. | |
| sender.SimulateSentPackets( | |
| {.first_sequence_number = 1, .num_packets = 10, .send_as_ect1 = true}); | |
| transport.OnCongestionControlFeedback( | |
| /*receive_time=*/Timestamp::Seconds(123), | |
| GenerateFeedback({{.ecn = EcnMarking::kNotEct, .sequence_number = 1}, | |
| {.ecn = EcnMarking::kNotEct}, | |
| {.ecn = EcnMarking::kEct1}, | |
| {.ecn = EcnMarking::kEct1}, | |
| {.ecn = EcnMarking::kEct1}, | |
| {.ecn = EcnMarking::kCe}})); | |
| EXPECT_EQ(transport.GetCongestionControlFeedbackStatsPerSsrc()[kSsrc] | |
| .num_packets_with_bleached_ect1_marking, | |
| 2); | |
| // Packets not send as ect1 do not add to number of bleached packets. | |
| sender.SimulateSentPackets( | |
| {.first_sequence_number = 11, .num_packets = 10, .send_as_ect1 = false}); | |
| transport.OnCongestionControlFeedback( | |
| /*receive_time=*/Timestamp::Seconds(123), | |
| GenerateFeedback({{.ecn = EcnMarking::kNotEct, .sequence_number = 11}, | |
| {.ecn = EcnMarking::kNotEct}, | |
| {.ecn = EcnMarking::kNotEct}})); | |
| EXPECT_EQ(transport.GetCongestionControlFeedbackStatsPerSsrc()[kSsrc] | |
| .num_packets_with_bleached_ect1_marking, | |
| 2 + 0); | |
| } | |
| TEST(RtpTransportControllerSendTest, | |
| AccumulatesNumberOfReportedLostAndRecoveredPackets) { | |
| AutoThread main_thread; | |
| RtpTransportControllerSend transport({.env = CreateTestEnvironment()}); | |
| transport.SetPreferredRtcpCcAckType(RtcpFeedbackType::CCFB); | |
| PacketSender sender(transport); | |
| sender.SimulateSentPackets({.first_sequence_number = 1, .num_packets = 30}); | |
| // Produce 1st report with 2 received and 3 lost packets. | |
| transport.OnCongestionControlFeedback( | |
| /*receive_time=*/Timestamp::Seconds(123), | |
| GenerateFeedback({{.sequence_number = 1, .received = true}, | |
| {.sequence_number = 2, .received = false}, | |
| {.sequence_number = 3, .received = false}, | |
| {.sequence_number = 4, .received = false}, | |
| {.sequence_number = 5, .received = true}})); | |
| ReceivedCongestionControlFeedbackStats stats = | |
| transport.GetCongestionControlFeedbackStatsPerSsrc()[kSsrc]; | |
| EXPECT_EQ(stats.num_packets_reported_as_lost, 3); | |
| EXPECT_EQ(stats.num_packets_reported_as_lost_but_recovered, 0); | |
| // Produce 2nd report with 1 packet recovered, 1 old packet reported still | |
| // lost, and 2 new packets lost. | |
| transport.OnCongestionControlFeedback( | |
| /*receive_time=*/Timestamp::Seconds(123), | |
| GenerateFeedback({{.sequence_number = 3, .received = true}, | |
| {.sequence_number = 4, .received = false}, | |
| {.sequence_number = 5, .received = true}, | |
| {.sequence_number = 6, .received = false}, | |
| {.sequence_number = 7, .received = false}, | |
| {.sequence_number = 8, .received = true}})); | |
| stats = transport.GetCongestionControlFeedbackStatsPerSsrc()[kSsrc]; | |
| EXPECT_EQ(stats.num_packets_reported_as_lost, 3 + 2); | |
| EXPECT_EQ(stats.num_packets_reported_as_lost_but_recovered, 1); | |
| // Produce 3rd report with 2 more packets recovered. | |
| transport.OnCongestionControlFeedback( | |
| /*receive_time=*/Timestamp::Seconds(123), | |
| GenerateFeedback({{.sequence_number = 6, .received = true}, | |
| {.sequence_number = 7, .received = true}, | |
| {.sequence_number = 8, .received = true}})); | |
| stats = transport.GetCongestionControlFeedbackStatsPerSsrc()[kSsrc]; | |
| EXPECT_EQ(stats.num_packets_reported_as_lost_but_recovered, 1 + 2); | |
| } | |
| TEST(RtpTransportControllerSendTest, | |
| DoesNotCountGapsInSequenceNumberBetweenReportsAsLoss) { | |
| AutoThread main_thread; | |
| RtpTransportControllerSend transport({.env = CreateTestEnvironment()}); | |
| transport.SetPreferredRtcpCcAckType(RtcpFeedbackType::CCFB); | |
| PacketSender sender(transport); | |
| sender.SimulateSentPackets({.first_sequence_number = 1, .num_packets = 30}); | |
| // Produce two report with a sequence number gap between them. | |
| transport.OnCongestionControlFeedback( | |
| /*receive_time=*/Timestamp::Seconds(123), | |
| GenerateFeedback({{.sequence_number = 1}})); | |
| transport.OnCongestionControlFeedback( | |
| /*receive_time=*/Timestamp::Seconds(123), | |
| GenerateFeedback({{.sequence_number = 5}})); | |
| // It is unclear if packets 2-4 weren't received and thus were excluded from | |
| // the feedback report, or report about these packets was itself lost. | |
| // Such packets are not counted as loss. | |
| EXPECT_EQ(transport.GetCongestionControlFeedbackStatsPerSsrc()[kSsrc] | |
| .num_packets_reported_as_lost, | |
| 0); | |
| // Only count losses explicitly marked as such in a report to align with | |
| // the metric definition "report has been sent with a zero R bit" | |
| transport.OnCongestionControlFeedback( | |
| /*receive_time=*/Timestamp::Seconds(123), | |
| GenerateFeedback({{.sequence_number = 3, .received = true}, | |
| {.sequence_number = 4, .received = false}, | |
| {.sequence_number = 5, .received = true}})); | |
| EXPECT_EQ(transport.GetCongestionControlFeedbackStatsPerSsrc()[kSsrc] | |
| .num_packets_reported_as_lost, | |
| 1); | |
| } | |
| } // namespace | |
| } // namespace webrtc |