Implement L4S send stats

described in
  https://github.com/w3c/webrtc-stats/pull/792
with a location change from
  https://github.com/w3c/webrtc-stats/pull/808

Bug: webrtc:42225697
Change-Id: I2660a17928ed6ecfb75ca597759c960678f17eaf
Reviewed-on: https://webrtc-review.googlesource.com/c/src/+/390866
Reviewed-by: Harald Alvestrand <hta@webrtc.org>
Reviewed-by: Per Kjellander <perkj@webrtc.org>
Commit-Queue: Philipp Hancke <phancke@meta.com>
Cr-Commit-Position: refs/heads/main@{#45283}
diff --git a/api/stats/rtcstats_objects.h b/api/stats/rtcstats_objects.h
index 82acb45..46fe7cc 100644
--- a/api/stats/rtcstats_objects.h
+++ b/api/stats/rtcstats_objects.h
@@ -369,6 +369,9 @@
 
   // RTX ssrc. Only present if RTX is negotiated.
   std::optional<uint32_t> rtx_ssrc;
+
+  // https://w3c.github.io/webrtc-stats/#dom-rtcoutboundrtpstreamstats-packetssentwithect1
+  std::optional<uint64_t> packets_sent_with_ect1;
 };
 
 // https://w3c.github.io/webrtc-stats/#remoteinboundrtpstats-dict*
diff --git a/audio/audio_send_stream.cc b/audio/audio_send_stream.cc
index 5c7fa93..5b6c070 100644
--- a/audio/audio_send_stream.cc
+++ b/audio/audio_send_stream.cc
@@ -442,6 +442,7 @@
       call_stats.header_and_padding_bytes_sent;
   stats.retransmitted_bytes_sent = call_stats.retransmitted_bytes_sent;
   stats.packets_sent = call_stats.packetsSent;
+  stats.packets_sent_with_ect1 = call_stats.packets_sent_with_ect1;
   stats.total_packet_send_delay = call_stats.total_packet_send_delay;
   stats.retransmitted_packets_sent = call_stats.retransmitted_packets_sent;
   // RTT isn't known until a RTCP report is received. Until then, VoiceEngine
diff --git a/audio/channel_send.cc b/audio/channel_send.cc
index 06ebdd2..b075940 100644
--- a/audio/channel_send.cc
+++ b/audio/channel_send.cc
@@ -810,6 +810,8 @@
   stats.retransmitted_bytes_sent = rtp_stats.retransmitted.payload_bytes;
   stats.packetsSent =
       rtp_stats.transmitted.packets + rtx_stats.transmitted.packets;
+  stats.packets_sent_with_ect1 = rtp_stats.transmitted.packets_with_ect1 +
+                                 rtx_stats.transmitted.packets_with_ect1;
   stats.total_packet_send_delay = rtp_stats.transmitted.total_packet_delay;
   stats.retransmitted_packets_sent = rtp_stats.retransmitted.packets;
   stats.report_block_datas = rtp_rtcp_->GetLatestReportBlockData();
diff --git a/audio/channel_send.h b/audio/channel_send.h
index 0ed544a..1706406 100644
--- a/audio/channel_send.h
+++ b/audio/channel_send.h
@@ -46,6 +46,8 @@
   // https://w3c.github.io/webrtc-stats/#dom-rtcoutboundrtpstreamstats-retransmittedbytessent
   uint64_t retransmitted_bytes_sent;
   int packetsSent;
+  // https://w3c.github.io/webrtc-stats/#dom-rtcoutboundrtpstreamstats-packetssentwithect1
+  int packets_sent_with_ect1;
   // https://w3c.github.io/webrtc-stats/#dom-rtcoutboundrtpstreamstats-totalpacketsenddelay
   TimeDelta total_packet_send_delay = TimeDelta::Zero();
   // https://w3c.github.io/webrtc-stats/#dom-rtcoutboundrtpstreamstats-retransmittedpacketssent
diff --git a/call/audio_send_stream.h b/call/audio_send_stream.h
index 2cc5397..851c7ae 100644
--- a/call/audio_send_stream.h
+++ b/call/audio_send_stream.h
@@ -48,6 +48,8 @@
     // https://w3c.github.io/webrtc-stats/#dom-rtcoutboundrtpstreamstats-retransmittedbytessent
     uint64_t retransmitted_bytes_sent = 0;
     int32_t packets_sent = 0;
+    // https://w3c.github.io/webrtc-stats/#dom-rtcoutboundrtpstreamstats-packetssentwithect1
+    int32_t packets_sent_with_ect1 = 0;
     // https://w3c.github.io/webrtc-stats/#dom-rtcoutboundrtpstreamstats-totalpacketsenddelay
     TimeDelta total_packet_send_delay = TimeDelta::Zero();
     // https://w3c.github.io/webrtc-stats/#dom-rtcoutboundrtpstreamstats-retransmittedpacketssent
diff --git a/media/base/media_channel.h b/media/base/media_channel.h
index 6a34639..17ad9ad 100644
--- a/media/base/media_channel.h
+++ b/media/base/media_channel.h
@@ -366,6 +366,8 @@
   // https://w3c.github.io/webrtc-stats/#dom-rtcoutboundrtpstreamstats-retransmittedbytessent
   uint64_t retransmitted_bytes_sent = 0;
   int packets_sent = 0;
+  // https://w3c.github.io/webrtc-stats/#dom-rtcoutboundrtpstreamstats-packetssentwithect1
+  int packets_sent_with_ect1 = 0;
   // https://w3c.github.io/webrtc-stats/#dom-rtcoutboundrtpstreamstats-retransmittedpacketssent
   uint64_t retransmitted_packets_sent = 0;
   // https://w3c.github.io/webrtc-stats/#dom-rtcoutboundrtpstreamstats-nackcount
diff --git a/media/engine/webrtc_video_engine.cc b/media/engine/webrtc_video_engine.cc
index 553020d..0b62de6 100644
--- a/media/engine/webrtc_video_engine.cc
+++ b/media/engine/webrtc_video_engine.cc
@@ -2514,6 +2514,8 @@
         stream_stats.rtp_stats.transmitted.header_bytes +
         stream_stats.rtp_stats.transmitted.padding_bytes;
     info.packets_sent = stream_stats.rtp_stats.transmitted.packets;
+    info.packets_sent_with_ect1 =
+        stream_stats.rtp_stats.transmitted.packets_with_ect1;
     info.total_packet_send_delay +=
         stream_stats.rtp_stats.transmitted.total_packet_delay;
     info.send_frame_width = stream_stats.width;
diff --git a/media/engine/webrtc_voice_engine.cc b/media/engine/webrtc_voice_engine.cc
index 3690ffb..d5be912 100644
--- a/media/engine/webrtc_voice_engine.cc
+++ b/media/engine/webrtc_voice_engine.cc
@@ -1800,6 +1800,7 @@
     sinfo.header_and_padding_bytes_sent = stats.header_and_padding_bytes_sent;
     sinfo.retransmitted_bytes_sent = stats.retransmitted_bytes_sent;
     sinfo.packets_sent = stats.packets_sent;
+    sinfo.packets_sent_with_ect1 = stats.packets_sent_with_ect1;
     sinfo.total_packet_send_delay = stats.total_packet_send_delay;
     sinfo.retransmitted_packets_sent = stats.retransmitted_packets_sent;
     sinfo.packets_lost = stats.packets_lost;
diff --git a/modules/rtp_rtcp/include/rtp_rtcp_defines.cc b/modules/rtp_rtcp/include/rtp_rtcp_defines.cc
index cc06d1b..1ac853b 100644
--- a/modules/rtp_rtcp/include/rtp_rtcp_defines.cc
+++ b/modules/rtp_rtcp/include/rtp_rtcp_defines.cc
@@ -51,12 +51,16 @@
     : header_bytes(packet.headers_size()),
       payload_bytes(packet.payload_size()),
       padding_bytes(packet.padding_size()),
-      packets(1) {}
+      packets(1),
+      packets_with_ect1(0) {}
 
 RtpPacketCounter::RtpPacketCounter(const RtpPacketToSend& packet_to_send)
     : RtpPacketCounter(static_cast<const RtpPacket&>(packet_to_send)) {
   total_packet_delay =
       packet_to_send.time_in_send_queue().value_or(TimeDelta::Zero());
+  if (packet_to_send.send_as_ect1()) {
+    ++packets_with_ect1;
+  }
 }
 
 void RtpPacketCounter::AddPacket(const RtpPacket& packet) {
@@ -70,6 +74,9 @@
   AddPacket(static_cast<const RtpPacket&>(packet_to_send));
   total_packet_delay +=
       packet_to_send.time_in_send_queue().value_or(TimeDelta::Zero());
+  if (packet_to_send.send_as_ect1()) {
+    ++packets_with_ect1;
+  }
 }
 
 }  // namespace webrtc
diff --git a/modules/rtp_rtcp/include/rtp_rtcp_defines.h b/modules/rtp_rtcp/include/rtp_rtcp_defines.h
index a888e4f..2552792 100644
--- a/modules/rtp_rtcp/include/rtp_rtcp_defines.h
+++ b/modules/rtp_rtcp/include/rtp_rtcp_defines.h
@@ -268,7 +268,11 @@
 
 struct RtpPacketCounter {
   RtpPacketCounter()
-      : header_bytes(0), payload_bytes(0), padding_bytes(0), packets(0) {}
+      : header_bytes(0),
+        payload_bytes(0),
+        padding_bytes(0),
+        packets(0),
+        packets_with_ect1(0) {}
 
   explicit RtpPacketCounter(const RtpPacket& packet);
   explicit RtpPacketCounter(const RtpPacketToSend& packet_to_send);
@@ -278,6 +282,7 @@
     payload_bytes += other.payload_bytes;
     padding_bytes += other.padding_bytes;
     packets += other.packets;
+    packets_with_ect1 += other.packets_with_ect1;
     total_packet_delay += other.total_packet_delay;
   }
 
@@ -285,6 +290,7 @@
     return header_bytes == other.header_bytes &&
            payload_bytes == other.payload_bytes &&
            padding_bytes == other.padding_bytes && packets == other.packets &&
+           packets_with_ect1 == other.packets_with_ect1 &&
            total_packet_delay == other.total_packet_delay;
   }
 
@@ -300,6 +306,7 @@
   size_t payload_bytes;  // Payload bytes, excluding RTP headers and padding.
   size_t padding_bytes;  // Number of padding bytes.
   size_t packets;        // Number of packets.
+  size_t packets_with_ect1;  // Number of packets with ECT1 flag set to true.
   // The total delay of all `packets`. For RtpPacketToSend packets, this is
   // `time_in_send_queue()`. For receive packets, this is zero.
   TimeDelta total_packet_delay = TimeDelta::Zero();
diff --git a/pc/rtc_stats_collector.cc b/pc/rtc_stats_collector.cc
index 021baae..38c7ba2 100644
--- a/pc/rtc_stats_collector.cc
+++ b/pc/rtc_stats_collector.cc
@@ -730,6 +730,8 @@
   if (media_sender_info.active.has_value()) {
     outbound_stats->active = *media_sender_info.active;
   }
+  outbound_stats->packets_sent_with_ect1 =
+      media_sender_info.packets_sent_with_ect1;
 }
 
 std::unique_ptr<RTCOutboundRtpStreamStats>
diff --git a/pc/rtc_stats_collector_unittest.cc b/pc/rtc_stats_collector_unittest.cc
index 8686354..737b71f 100644
--- a/pc/rtc_stats_collector_unittest.cc
+++ b/pc/rtc_stats_collector_unittest.cc
@@ -2523,6 +2523,7 @@
   voice_media_info.senders[0].local_stats.push_back(SsrcSenderInfo());
   voice_media_info.senders[0].local_stats[0].ssrc = 1;
   voice_media_info.senders[0].packets_sent = 2;
+  voice_media_info.senders[0].packets_sent_with_ect1 = 2;
   voice_media_info.senders[0].total_packet_send_delay = TimeDelta::Seconds(1);
   voice_media_info.senders[0].retransmitted_packets_sent = 20;
   voice_media_info.senders[0].payload_bytes_sent = 3;
@@ -2558,6 +2559,7 @@
   expected_audio.transport_id = "TTransportName1";
   expected_audio.codec_id = "COTTransportName1_42";
   expected_audio.packets_sent = 2;
+  expected_audio.packets_sent_with_ect1 = 2;
   expected_audio.total_packet_send_delay = 1;
   expected_audio.retransmitted_packets_sent = 20;
   expected_audio.bytes_sent = 3;
@@ -2592,6 +2594,7 @@
   video_media_info.senders[0].plis_received = 3;
   video_media_info.senders[0].nacks_received = 4;
   video_media_info.senders[0].packets_sent = 5;
+  video_media_info.senders[0].packets_sent_with_ect1 = 3;
   video_media_info.senders[0].retransmitted_packets_sent = 50;
   video_media_info.senders[0].payload_bytes_sent = 6;
   video_media_info.senders[0].header_and_padding_bytes_sent = 12;
@@ -2669,6 +2672,7 @@
   expected_video.pli_count = 3;
   expected_video.nack_count = 4;
   expected_video.packets_sent = 5;
+  expected_video.packets_sent_with_ect1 = 3;
   expected_video.retransmitted_packets_sent = 50;
   expected_video.bytes_sent = 6;
   expected_video.header_bytes_sent = 12;
@@ -2971,6 +2975,7 @@
   voice_media_info.senders[0].local_stats.push_back(SsrcSenderInfo());
   voice_media_info.senders[0].local_stats[0].ssrc = 1;
   voice_media_info.senders[0].packets_sent = 2;
+  voice_media_info.senders[0].packets_sent_with_ect1 = 0;
   voice_media_info.senders[0].total_packet_send_delay = TimeDelta::Seconds(0.5);
   voice_media_info.senders[0].retransmitted_packets_sent = 20;
   voice_media_info.senders[0].payload_bytes_sent = 3;
@@ -3005,6 +3010,7 @@
   expected_audio.transport_id = "TTransportName1";
   expected_audio.codec_id = "COTTransportName1_42";
   expected_audio.packets_sent = 2;
+  expected_audio.packets_sent_with_ect1 = 0;
   expected_audio.total_packet_send_delay = 0.5;
   expected_audio.retransmitted_packets_sent = 20;
   expected_audio.bytes_sent = 3;
diff --git a/pc/rtc_stats_integrationtest.cc b/pc/rtc_stats_integrationtest.cc
index f2e92dd..3b7f616 100644
--- a/pc/rtc_stats_integrationtest.cc
+++ b/pc/rtc_stats_integrationtest.cc
@@ -799,6 +799,8 @@
     verifier.TestAttributeIsNonNegative<uint64_t>(
         outbound_stream.retransmitted_bytes_sent);
     verifier.TestAttributeIsNonNegative<double>(outbound_stream.target_bitrate);
+    verifier.TestAttributeIsNonNegative<uint64_t>(
+        outbound_stream.packets_sent_with_ect1);
     if (outbound_stream.kind.has_value() && *outbound_stream.kind == "video") {
       verifier.TestAttributeIsDefined(outbound_stream.frames_encoded);
       verifier.TestAttributeIsDefined(outbound_stream.key_frames_encoded);
diff --git a/stats/rtcstats_objects.cc b/stats/rtcstats_objects.cc
index 5a801cc..7d2bb64 100644
--- a/stats/rtcstats_objects.cc
+++ b/stats/rtcstats_objects.cc
@@ -324,7 +324,8 @@
     AttributeInit("active", &active),
     AttributeInit("powerEfficientEncoder", &power_efficient_encoder),
     AttributeInit("scalabilityMode", &scalability_mode),
-    AttributeInit("rtxSsrc", &rtx_ssrc))
+    AttributeInit("rtxSsrc", &rtx_ssrc),
+    AttributeInit("packetsSentWithEct1", &packets_sent_with_ect1))
 // clang-format on
 
 RTCOutboundRtpStreamStats::RTCOutboundRtpStreamStats(std::string id,
diff --git a/test/peer_scenario/tests/l4s_test.cc b/test/peer_scenario/tests/l4s_test.cc
index 0134d77..a706a78 100644
--- a/test/peer_scenario/tests/l4s_test.cc
+++ b/test/peer_scenario/tests/l4s_test.cc
@@ -9,6 +9,8 @@
  */
 
 #include <atomic>
+#include <cstdint>
+#include <optional>
 #include <string>
 
 #include "absl/strings/str_cat.h"
@@ -121,6 +123,15 @@
   return DataRate::BitsPerSec(*stats[0]->available_outgoing_bitrate);
 }
 
+std::optional<uint64_t> GetPacketsSentWithEct1(
+    const scoped_refptr<const RTCStatsReport>& report) {
+  auto stats = report->GetStatsOfType<RTCOutboundRtpStreamStats>();
+  if (stats.empty()) {
+    return std::nullopt;
+  }
+  return stats[0]->packets_sent_with_ect1;
+}
+
 TEST(L4STest, NegotiateAndUseCcfbIfEnabled) {
   PeerScenario s(*test_info_);
 
@@ -303,11 +314,11 @@
     feedback_counter.Count(packet);
     if (feedback_counter.ect1() > 0) {
       seen_ect1_feedback = true;
-      RTC_LOG(LS_INFO) << " ect 1" << feedback_counter.ect1();
+      RTC_LOG(LS_INFO) << " ect 1: " << feedback_counter.ect1();
     }
     if (feedback_counter.not_ect() > 0) {
       seen_not_ect_feedback = true;
-      RTC_LOG(LS_INFO) << " not ect" << feedback_counter.not_ect();
+      RTC_LOG(LS_INFO) << " not ect: " << feedback_counter.not_ect();
     }
   });
 
@@ -331,6 +342,9 @@
   EXPECT_TRUE(s.WaitAndProcess(&seen_ect1_feedback, TimeDelta::Seconds(1)));
   EXPECT_FALSE(seen_not_ect_feedback);
   EXPECT_TRUE(s.WaitAndProcess(&seen_not_ect_feedback, TimeDelta::Seconds(1)));
+  auto packets_sent_with_ect1_stats =
+      GetPacketsSentWithEct1(GetStatsAndProcess(s, caller));
+  EXPECT_EQ(packets_sent_with_ect1_stats, feedback_counter.ect1());
 }
 
 TEST(L4STest, SendsEct1AfterRouteChange) {
@@ -416,6 +430,10 @@
   s.net()->DisableEndpoint(callee->endpoint(0));
   EXPECT_TRUE(
       s.WaitAndProcess(&seen_ect1_on_cellular_feedback, TimeDelta::Seconds(5)));
+  auto packets_sent_with_ect1_stats =
+      GetPacketsSentWithEct1(GetStatsAndProcess(s, caller));
+  EXPECT_EQ(packets_sent_with_ect1_stats,
+            wifi_feedback_counter.ect1() + cellular_feedback_counter.ect1());
 }
 
 }  // namespace