Calculate L4S stats from the received congestion control feedbacks
Reland of https://webrtc-review.googlesource.com/c/src/+/420821
Bug: webrtc:437303401
Change-Id: I22063a3cad7cae3a8564524f519fee0033e5eef5
Reviewed-on: https://webrtc-review.googlesource.com/c/src/+/422962
Reviewed-by: Per Kjellander <perkj@webrtc.org>
Commit-Queue: Per Kjellander <perkj@webrtc.org>
Auto-Submit: Danil Chapovalov <danilchap@webrtc.org>
Commit-Queue: Danil Chapovalov <danilchap@webrtc.org>
Cr-Commit-Position: refs/heads/main@{#46117}
diff --git a/call/BUILD.gn b/call/BUILD.gn
index 3e7d690..effafa2 100644
--- a/call/BUILD.gn
+++ b/call/BUILD.gn
@@ -193,6 +193,7 @@
"../api/task_queue:pending_task_safety_flag",
"../api/transport:bandwidth_estimation_settings",
"../api/transport:bitrate_settings",
+ "../api/transport:ecn_marking",
"../api/transport:goog_cc",
"../api/transport:network_control",
"../api/transport/rtp:dependency_descriptor",
@@ -214,6 +215,7 @@
"../common_video/generic_frame_descriptor",
"../logging:rtc_event_bwe",
"../modules:module_fec_api",
+ "../modules/congestion_controller/rtp:congestion_controller_feedback_stats",
"../modules/congestion_controller/rtp:control_handler",
"../modules/congestion_controller/rtp:transport_feedback",
"../modules/congestion_controller/scream:scream_network_controller",
@@ -233,6 +235,7 @@
"../rtc_base:random",
"../rtc_base:rate_limiter",
"../rtc_base:safe_conversions",
+ "../rtc_base/containers:flat_map",
"../rtc_base/experiments:field_trial_parser",
"../rtc_base/network:sent_packet",
"../rtc_base/synchronization:mutex",
@@ -500,6 +503,7 @@
"rtp_bitrate_configurator_unittest.cc",
"rtp_demuxer_unittest.cc",
"rtp_payload_params_unittest.cc",
+ "rtp_transport_controller_send_unittest.cc",
"rtp_video_sender_unittest.cc",
"rtx_receive_stream_unittest.cc",
]
@@ -536,6 +540,7 @@
"../api/test/network_emulation",
"../api/test/video:function_video_factory",
"../api/transport:bitrate_settings",
+ "../api/transport:ecn_marking",
"../api/transport:network_control",
"../api/transport/rtp:dependency_descriptor",
"../api/units:data_rate",
@@ -556,6 +561,7 @@
"../media:media_constants",
"../modules/audio_device:mock_audio_device",
"../modules/audio_processing:mocks",
+ "../modules/congestion_controller/rtp:congestion_controller_feedback_stats",
"../modules/rtp_rtcp",
"../modules/rtp_rtcp:mock_rtp_rtcp",
"../modules/rtp_rtcp:rtp_rtcp_format",
@@ -574,6 +580,7 @@
"../rtc_base:task_queue_for_test",
"../rtc_base:threading",
"../rtc_base:timeutils",
+ "../rtc_base/containers:flat_map",
"../rtc_base/synchronization:mutex",
"../test:audio_codec_mocks",
"../test:create_test_environment",
diff --git a/call/rtp_transport_controller_send.cc b/call/rtp_transport_controller_send.cc
index 77ada73..880855b 100644
--- a/call/rtp_transport_controller_send.cc
+++ b/call/rtp_transport_controller_send.cc
@@ -31,6 +31,7 @@
#include "api/task_queue/task_queue_base.h"
#include "api/transport/bandwidth_estimation_settings.h"
#include "api/transport/bitrate_settings.h"
+#include "api/transport/ecn_marking.h"
#include "api/transport/goog_cc_factory.h"
#include "api/transport/network_control.h"
#include "api/transport/network_types.h"
@@ -44,6 +45,7 @@
#include "call/rtp_video_sender.h"
#include "call/rtp_video_sender_interface.h"
#include "logging/rtc_event_log/events/rtc_event_route_change.h"
+#include "modules/congestion_controller/rtp/congestion_controller_feedback_stats.h"
#include "modules/congestion_controller/rtp/control_handler.h"
#include "modules/congestion_controller/scream/scream_network_controller.h"
#include "modules/pacing/packet_router.h"
@@ -53,6 +55,7 @@
#include "modules/rtp_rtcp/source/rtcp_packet/transport_feedback.h"
#include "modules/rtp_rtcp/source/rtp_rtcp_interface.h"
#include "rtc_base/checks.h"
+#include "rtc_base/containers/flat_map.h"
#include "rtc_base/experiments/field_trial_parser.h"
#include "rtc_base/logging.h"
#include "rtc_base/network/sent_packet.h"
@@ -647,6 +650,12 @@
return feedback_count_;
}
+flat_map<uint32_t, ReceivedCongestionControlFeedbackStats>
+RtpTransportControllerSend::GetCongestionControlFeedbackStatsPerSsrc() const {
+ RTC_DCHECK_RUN_ON(&sequence_checker_);
+ return received_ccfb_stats_;
+}
+
std::optional<int>
RtpTransportControllerSend::ReceivedTransportCcFeedbackCount() const {
RTC_DCHECK_RUN_ON(&sequence_checker_);
@@ -678,10 +687,60 @@
transport_feedback_adapter_.ProcessCongestionControlFeedback(
feedback, receive_time);
if (feedback_msg) {
+ ComputeStatsFromCongestionControlFeedback(*feedback_msg);
HandleTransportPacketsFeedback(*feedback_msg);
}
}
+void RtpTransportControllerSend::ComputeStatsFromCongestionControlFeedback(
+ const TransportPacketsFeedback& feedback) {
+ std::optional<uint32_t> last_ssrc;
+ ReceivedCongestionControlFeedbackStats* stats = nullptr;
+ for (const PacketResult& packet_info : feedback.packet_feedbacks) {
+ if (!packet_info.rtp_packet_info.has_value()) {
+ continue;
+ }
+
+ // Most of the time ssrc doesn't change across packets, so reuse last
+ // map lookup when ssrc is the same. Initially last_ssrc is nullopt,
+ // so first check would always trigger map lookup, thus `stats` would always
+ // be nonnull after this block.
+ if (uint32_t ssrc = packet_info.rtp_packet_info->ssrc; ssrc != last_ssrc) {
+ last_ssrc = ssrc;
+ stats = &received_ccfb_stats_[ssrc];
+ }
+
+ if (packet_info.reported_lost_for_the_first_time) {
+ RTC_DCHECK(!packet_info.IsReceived());
+ ++stats->num_packets_reported_as_lost;
+ }
+
+ if (packet_info.reported_recovered_for_the_first_time) {
+ RTC_DCHECK(packet_info.IsReceived());
+ ++stats->num_packets_reported_as_lost_but_recovered;
+ }
+
+ if (packet_info.IsReceived()) {
+ switch (packet_info.ecn) {
+ using enum EcnMarking;
+ case kEct1:
+ ++stats->num_packets_received_with_ect1;
+ break;
+ case kCe:
+ ++stats->num_packets_received_with_ce;
+ break;
+ case kNotEct:
+ if (packet_info.sent_with_ect1) {
+ ++stats->num_packets_with_bleached_ect1_marking;
+ }
+ break;
+ case kEct0:
+ break;
+ }
+ }
+ }
+}
+
void RtpTransportControllerSend::HandleTransportPacketsFeedback(
const TransportPacketsFeedback& feedback) {
if (sending_packets_as_ect1_) {
@@ -869,4 +928,17 @@
last_report_block_time_ = receive_time;
}
+void RtpTransportControllerSend::NotifyBweOfSentPacketForTesting(
+ const RtpPacketToSend& rtp_packet) {
+ NotifyBweOfPacedSentPacket(rtp_packet, /*pacing_info=*/{});
+ PacketInfo packet_info;
+ packet_info.included_in_allocation = true;
+ packet_info.included_in_feedback =
+ rtp_packet.transport_sequence_number().has_value();
+ OnSentPacket(SentPacketInfo(
+ /*packet_id=*/rtp_packet.transport_sequence_number().value_or(-1),
+ /*send_time_ms=*/env_.clock().CurrentTime().ms(),
+ /*info=*/packet_info));
+}
+
} // namespace webrtc
diff --git a/call/rtp_transport_controller_send.h b/call/rtp_transport_controller_send.h
index 826fce6..e8a4db0 100644
--- a/call/rtp_transport_controller_send.h
+++ b/call/rtp_transport_controller_send.h
@@ -41,6 +41,7 @@
#include "call/rtp_transport_config.h"
#include "call/rtp_transport_controller_send_interface.h"
#include "call/rtp_video_sender.h"
+#include "modules/congestion_controller/rtp/congestion_controller_feedback_stats.h"
#include "modules/congestion_controller/rtp/control_handler.h"
#include "modules/congestion_controller/rtp/transport_feedback_adapter.h"
#include "modules/congestion_controller/rtp/transport_feedback_demuxer.h"
@@ -49,6 +50,7 @@
#include "modules/rtp_rtcp/include/report_block_data.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/experiments/field_trial_parser.h"
#include "rtc_base/network_route.h"
#include "rtc_base/rate_limiter.h"
@@ -146,12 +148,19 @@
void EnableCongestionControlFeedbackAccordingToRfc8888() override;
std::optional<int> ReceivedCongestionControlFeedbackCount() const override;
+ flat_map<uint32_t, ReceivedCongestionControlFeedbackStats>
+ GetCongestionControlFeedbackStatsPerSsrc() const;
std::optional<int> ReceivedTransportCcFeedbackCount() const override;
+ // Mimics callbacks for packets sent through this transport.
+ void NotifyBweOfSentPacketForTesting(const RtpPacketToSend& rtp_packet);
+
private:
void MaybeCreateControllers() RTC_RUN_ON(sequence_checker_);
void HandleTransportPacketsFeedback(const TransportPacketsFeedback& feedback)
RTC_RUN_ON(sequence_checker_);
+ void ComputeStatsFromCongestionControlFeedback(
+ const TransportPacketsFeedback& feedback) RTC_RUN_ON(sequence_checker_);
void UpdateNetworkAvailability() RTC_RUN_ON(sequence_checker_);
void UpdateInitialConstraints(TargetRateConstraints new_contraints)
RTC_RUN_ON(sequence_checker_);
@@ -215,6 +224,8 @@
};
std::map<uint32_t, LossReport> last_report_blocks_
RTC_GUARDED_BY(sequence_checker_);
+ flat_map<uint32_t, ReceivedCongestionControlFeedbackStats>
+ received_ccfb_stats_ RTC_GUARDED_BY(sequence_checker_);
Timestamp last_report_block_time_ RTC_GUARDED_BY(sequence_checker_);
NetworkControllerConfig initial_config_ RTC_GUARDED_BY(sequence_checker_);
diff --git a/call/rtp_transport_controller_send_unittest.cc b/call/rtp_transport_controller_send_unittest.cc
new file mode 100644
index 0000000..024e06d
--- /dev/null
+++ b/call/rtp_transport_controller_send_unittest.cc
@@ -0,0 +1,295 @@
+/*
+ * 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/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.EnableCongestionControlFeedbackAccordingToRfc8888();
+ 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.EnableCongestionControlFeedbackAccordingToRfc8888();
+
+ 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.EnableCongestionControlFeedbackAccordingToRfc8888();
+ 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.EnableCongestionControlFeedbackAccordingToRfc8888();
+
+ 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.EnableCongestionControlFeedbackAccordingToRfc8888();
+
+ 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
diff --git a/modules/congestion_controller/rtp/congestion_controller_feedback_stats.h b/modules/congestion_controller/rtp/congestion_controller_feedback_stats.h
index b6a36c2..7c6b648 100644
--- a/modules/congestion_controller/rtp/congestion_controller_feedback_stats.h
+++ b/modules/congestion_controller/rtp/congestion_controller_feedback_stats.h
@@ -28,6 +28,31 @@
int64_t num_packets_reported_recovered = 0;
};
+// Helper struct to pass around stats computed from received RFC8888 reports.
+struct ReceivedCongestionControlFeedbackStats {
+ // Total number of packets reported as received with the "ECT(1)" marking.
+ // https://w3c.github.io/webrtc-stats/#dom-rtcreceivedrtpstreamstats-packetsreceivedwithect1
+ int64_t num_packets_received_with_ect1 = 0;
+
+ // Total number of packets reported as received with the "CE" marking.
+ // https://w3c.github.io/webrtc-stats/#dom-rtcreceivedrtpstreamstats-packetsreceivedwithce
+ int64_t num_packets_received_with_ce = 0;
+
+ // Total number of packets reported as lost in received feedback.
+ // https://w3c.github.io/webrtc-stats/#dom-rtcreceivedrtpstreamstats-packetsreportedaslost
+ int64_t num_packets_reported_as_lost = 0;
+
+ // Total number of packets reported first as lost in received feedback, but
+ // that were also reported as received in a later feedback.
+ // https://w3c.github.io/webrtc-stats/#dom-rtcreceivedrtpstreamstats-packetsreportedaslostbutrecovered
+ int64_t num_packets_reported_as_lost_but_recovered = 0;
+
+ // Total number of packets that were sent with ECT(1) markings, but were
+ // reported as received with "not-ECT" marking.
+ // https://w3c.github.io/webrtc-stats/#dom-rtcremoteinboundrtpstreamstats-packetswithbleachedect1marking
+ int64_t num_packets_with_bleached_ect1_marking = 0;
+};
+
} // namespace webrtc
#endif // MODULES_CONGESTION_CONTROLLER_RTP_CONGESTION_CONTROLLER_FEEDBACK_STATS_H_