Implement L4S inbound-rtp stats

described in
https://w3c.github.io/webrtc-stats/#dom-rtcreceivedrtpstreamstats-packetsreceivedwithect1
https://w3c.github.io/webrtc-stats/#dom-rtcreceivedrtpstreamstats-packetsreceivedwithce

This largely mirrors the send side stats:
https://webrtc-review.googlesource.com/c/src/+/390866

The statistics on the remote-inbound-rtp that are sent to the sender via RFC 8888 feedback are not implemented yet.

Bug: webrtc:42225697
Change-Id: Id680c4361d4d7e563b069446c5365388322af55b
Reviewed-on: https://webrtc-review.googlesource.com/c/src/+/403188
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@{#45311}
diff --git a/api/stats/rtcstats_objects.h b/api/stats/rtcstats_objects.h
index 46fe7cc..6f5d108 100644
--- a/api/stats/rtcstats_objects.h
+++ b/api/stats/rtcstats_objects.h
@@ -196,6 +196,10 @@
 
   std::optional<double> jitter;
   std::optional<int32_t> packets_lost;  // Signed per RFC 3550
+  // https://w3c.github.io/webrtc-stats/#dom-rtcreceivedrtpstreamstats-packetsreceivedwithect1
+  std::optional<int64_t> packets_received_with_ect1;
+  // https://w3c.github.io/webrtc-stats/#dom-rtcreceivedrtpstreamstats-packetsreceivedwithce
+  std::optional<int64_t> packets_received_with_ce;
 
  protected:
   RTCReceivedRtpStreamStats(std::string id, Timestamp timestamp);
@@ -371,7 +375,7 @@
   std::optional<uint32_t> rtx_ssrc;
 
   // https://w3c.github.io/webrtc-stats/#dom-rtcoutboundrtpstreamstats-packetssentwithect1
-  std::optional<uint64_t> packets_sent_with_ect1;
+  std::optional<int64_t> packets_sent_with_ect1;
 };
 
 // https://w3c.github.io/webrtc-stats/#remoteinboundrtpstats-dict*
diff --git a/audio/audio_receive_stream.cc b/audio/audio_receive_stream.cc
index ab2383f..bbe8480 100644
--- a/audio/audio_receive_stream.cc
+++ b/audio/audio_receive_stream.cc
@@ -280,6 +280,8 @@
   stats.header_and_padding_bytes_received =
       call_stats.header_and_padding_bytes_received;
   stats.packets_received = call_stats.packets_received;
+  stats.packets_received_with_ect1 = call_stats.packets_received_with_ect1;
+  stats.packets_received_with_ce = call_stats.packets_received_with_ce;
   stats.packets_lost = call_stats.packets_lost;
   stats.jitter_ms = call_stats.jitter_ms;
   stats.nacks_sent = call_stats.nacks_sent;
diff --git a/audio/channel_receive.cc b/audio/channel_receive.cc
index 724fb32..658266e 100644
--- a/audio/channel_receive.cc
+++ b/audio/channel_receive.cc
@@ -851,6 +851,9 @@
         rtp_stats.packet_counter.header_bytes +
         rtp_stats.packet_counter.padding_bytes;
     stats.packets_received = rtp_stats.packet_counter.packets;
+    stats.packets_received_with_ect1 =
+        rtp_stats.packet_counter.packets_with_ect1;
+    stats.packets_received_with_ce = rtp_stats.packet_counter.packets_with_ce;
     stats.last_packet_received = rtp_stats.last_packet_received;
   }
 
diff --git a/audio/channel_receive.h b/audio/channel_receive.h
index 6179c4f..0783363 100644
--- a/audio/channel_receive.h
+++ b/audio/channel_receive.h
@@ -55,6 +55,10 @@
   int64_t payload_bytes_received = 0;
   int64_t header_and_padding_bytes_received = 0;
   int packets_received = 0;
+  // https://w3c.github.io/webrtc-stats/#dom-rtcreceivedrtpstreamstats-packetsreceivedwithect1
+  int64_t packets_received_with_ect1 = 0;
+  // https://w3c.github.io/webrtc-stats/#dom-rtcreceivedrtpstreamstats-packetsreceivedwithce
+  int64_t packets_received_with_ce = 0;
   uint32_t nacks_sent = 0;
   // The capture NTP time (in local timebase) of the first played out audio
   // frame.
diff --git a/call/audio_receive_stream.h b/call/audio_receive_stream.h
index 4ae9ba0..d7ab975 100644
--- a/call/audio_receive_stream.h
+++ b/call/audio_receive_stream.h
@@ -44,6 +44,10 @@
     int64_t payload_bytes_received = 0;
     int64_t header_and_padding_bytes_received = 0;
     uint32_t packets_received = 0;
+    // https://w3c.github.io/webrtc-stats/#dom-rtcreceivedrtpstreamstats-packetsreceivedwithect1
+    int64_t packets_received_with_ect1 = 0;
+    // https://w3c.github.io/webrtc-stats/#dom-rtcreceivedrtpstreamstats-packetsreceivedwithce
+    int64_t packets_received_with_ce = 0;
     uint64_t fec_packets_received = 0;
     uint64_t fec_packets_discarded = 0;
     int32_t packets_lost = 0;
diff --git a/call/rtp_transport_controller_send.cc b/call/rtp_transport_controller_send.cc
index cbef660..fdd7bcc 100644
--- a/call/rtp_transport_controller_send.cc
+++ b/call/rtp_transport_controller_send.cc
@@ -668,9 +668,9 @@
     // TODO: bugs.webrtc.org/42225697 - adapt to ECN feedback and continue to
     // send packets as ECT(1) if transport is ECN capable.
     sending_packets_as_ect1_ = false;
-    RTC_LOG(LS_INFO) << " Transport is "
-                     << (feedback.transport_supports_ecn ? "" : " not ")
-                     << " ECN capable. Stop sending ECT(1).";
+    RTC_LOG(LS_INFO) << "Transport is "
+                     << (feedback.transport_supports_ecn ? "" : "not ")
+                     << "ECN capable. Stop sending ECT(1).";
     packet_router_.ConfigureForRfc8888Feedback(sending_packets_as_ect1_);
   }
 
diff --git a/media/base/media_channel.h b/media/base/media_channel.h
index 17ad9ad..373f160 100644
--- a/media/base/media_channel.h
+++ b/media/base/media_channel.h
@@ -367,7 +367,7 @@
   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;
+  int64_t 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
@@ -431,6 +431,10 @@
   // https://w3c.github.io/webrtc-stats/#dom-rtcinboundrtpstreamstats-headerbytesreceived
   int64_t header_and_padding_bytes_received = 0;
   int packets_received = 0;
+  // https://w3c.github.io/webrtc-stats/#dom-rtcreceivedrtpstreamstats-packetsreceivedwithect1
+  int64_t packets_received_with_ect1 = 0;
+  // https://w3c.github.io/webrtc-stats/#dom-rtcreceivedrtpstreamstats-packetsreceivedwithce
+  int64_t packets_received_with_ce = 0;
   int packets_lost = 0;
 
   std::optional<uint64_t> retransmitted_bytes_received;
diff --git a/media/engine/webrtc_video_engine.cc b/media/engine/webrtc_video_engine.cc
index cef81f9..faa378f 100644
--- a/media/engine/webrtc_video_engine.cc
+++ b/media/engine/webrtc_video_engine.cc
@@ -3787,6 +3787,10 @@
       stats.rtp_stats.packet_counter.header_bytes +
       stats.rtp_stats.packet_counter.padding_bytes;
   info.packets_received = stats.rtp_stats.packet_counter.packets;
+  info.packets_received_with_ect1 =
+      stats.rtp_stats.packet_counter.packets_with_ect1;
+  info.packets_received_with_ce =
+      stats.rtp_stats.packet_counter.packets_with_ce;
   info.packets_lost = stats.rtp_stats.packets_lost;
   info.jitter_ms = stats.rtp_stats.jitter / (kVideoCodecClockrate / 1000);
 
diff --git a/modules/rtp_rtcp/include/rtp_rtcp_defines.cc b/modules/rtp_rtcp/include/rtp_rtcp_defines.cc
index 1ac853b..f61db06 100644
--- a/modules/rtp_rtcp/include/rtp_rtcp_defines.cc
+++ b/modules/rtp_rtcp/include/rtp_rtcp_defines.cc
@@ -15,8 +15,10 @@
 
 #include "absl/algorithm/container.h"
 #include "absl/strings/string_view.h"
+#include "api/transport/ecn_marking.h"
 #include "api/units/time_delta.h"
 #include "modules/rtp_rtcp/source/rtp_packet.h"
+#include "modules/rtp_rtcp/source/rtp_packet_received.h"
 #include "modules/rtp_rtcp/source/rtp_packet_to_send.h"
 
 namespace webrtc {
@@ -52,7 +54,8 @@
       payload_bytes(packet.payload_size()),
       padding_bytes(packet.padding_size()),
       packets(1),
-      packets_with_ect1(0) {}
+      packets_with_ect1(0),
+      packets_with_ce(0) {}
 
 RtpPacketCounter::RtpPacketCounter(const RtpPacketToSend& packet_to_send)
     : RtpPacketCounter(static_cast<const RtpPacket&>(packet_to_send)) {
@@ -63,6 +66,16 @@
   }
 }
 
+RtpPacketCounter::RtpPacketCounter(const RtpPacketReceived& packet_received)
+    : RtpPacketCounter(static_cast<const RtpPacket&>(packet_received)) {
+  EcnMarking ecn = packet_received.ecn();
+  if (ecn == EcnMarking::kEct1) {
+    ++packets_with_ect1;
+  } else if (ecn == EcnMarking::kCe) {
+    ++packets_with_ce;
+  }
+}
+
 void RtpPacketCounter::AddPacket(const RtpPacket& packet) {
   ++packets;
   header_bytes += packet.headers_size();
@@ -79,4 +92,14 @@
   }
 }
 
+void RtpPacketCounter::AddPacket(const RtpPacketReceived& packet_received) {
+  AddPacket(static_cast<const RtpPacket&>(packet_received));
+  EcnMarking ecn = packet_received.ecn();
+  if (ecn == EcnMarking::kEct1) {
+    ++packets_with_ect1;
+  } else if (ecn == EcnMarking::kCe) {
+    ++packets_with_ce;
+  }
+}
+
 }  // namespace webrtc
diff --git a/modules/rtp_rtcp/include/rtp_rtcp_defines.h b/modules/rtp_rtcp/include/rtp_rtcp_defines.h
index 2552792..cb4decc 100644
--- a/modules/rtp_rtcp/include/rtp_rtcp_defines.h
+++ b/modules/rtp_rtcp/include/rtp_rtcp_defines.h
@@ -38,6 +38,8 @@
 namespace webrtc {
 class RtpPacket;
 class RtpPacketToSend;
+class RtpPacketReceived;
+
 namespace rtcp {
 class TransportFeedback;
 }  // namespace rtcp
@@ -272,10 +274,12 @@
         payload_bytes(0),
         padding_bytes(0),
         packets(0),
-        packets_with_ect1(0) {}
+        packets_with_ect1(0),
+        packets_with_ce(0) {}
 
   explicit RtpPacketCounter(const RtpPacket& packet);
   explicit RtpPacketCounter(const RtpPacketToSend& packet_to_send);
+  explicit RtpPacketCounter(const RtpPacketReceived& packet_received);
 
   void Add(const RtpPacketCounter& other) {
     header_bytes += other.header_bytes;
@@ -283,6 +287,7 @@
     padding_bytes += other.padding_bytes;
     packets += other.packets;
     packets_with_ect1 += other.packets_with_ect1;
+    packets_with_ce += other.packets_with_ce;
     total_packet_delay += other.total_packet_delay;
   }
 
@@ -291,12 +296,14 @@
            payload_bytes == other.payload_bytes &&
            padding_bytes == other.padding_bytes && packets == other.packets &&
            packets_with_ect1 == other.packets_with_ect1 &&
+           packets_with_ce == other.packets_with_ce &&
            total_packet_delay == other.total_packet_delay;
   }
 
   // Not inlined, since use of RtpPacket would result in circular includes.
   void AddPacket(const RtpPacket& packet);
   void AddPacket(const RtpPacketToSend& packet_to_send);
+  void AddPacket(const RtpPacketReceived& packet_received);
 
   size_t TotalBytes() const {
     return header_bytes + payload_bytes + padding_bytes;
@@ -307,6 +314,7 @@
   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.
+  size_t packets_with_ce;    // Number of packets with CE 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 451d9ce..be90539 100644
--- a/pc/rtc_stats_collector.cc
+++ b/pc/rtc_stats_collector.cc
@@ -397,6 +397,10 @@
   inbound_stats->ssrc = media_receiver_info.ssrc();
   inbound_stats->packets_received =
       static_cast<uint32_t>(media_receiver_info.packets_received);
+  inbound_stats->packets_received_with_ect1 =
+      media_receiver_info.packets_received_with_ect1;
+  inbound_stats->packets_received_with_ce =
+      media_receiver_info.packets_received_with_ce;
   inbound_stats->bytes_received =
       static_cast<uint64_t>(media_receiver_info.payload_bytes_received);
   inbound_stats->header_bytes_received = static_cast<uint64_t>(
diff --git a/pc/rtc_stats_collector_unittest.cc b/pc/rtc_stats_collector_unittest.cc
index 737b71f..19264d5 100644
--- a/pc/rtc_stats_collector_unittest.cc
+++ b/pc/rtc_stats_collector_unittest.cc
@@ -2147,6 +2147,8 @@
   voice_media_info.receivers[0].packets_lost = -1;  // Signed per RFC3550
   voice_media_info.receivers[0].packets_discarded = 7788;
   voice_media_info.receivers[0].packets_received = 2;
+  voice_media_info.receivers[0].packets_received_with_ect1 = 7;
+  voice_media_info.receivers[0].packets_received_with_ce = 5;
   voice_media_info.receivers[0].nacks_sent = 5;
   voice_media_info.receivers[0].fec_packets_discarded = 5566;
   voice_media_info.receivers[0].fec_packets_received = 6677;
@@ -2204,6 +2206,8 @@
   expected_audio.transport_id = "TTransportName1";
   expected_audio.codec_id = "CITTransportName1_42";
   expected_audio.packets_received = 2;
+  expected_audio.packets_received_with_ect1 = 7;
+  expected_audio.packets_received_with_ce = 5;
   expected_audio.nack_count = 5;
   expected_audio.fec_packets_discarded = 5566;
   expected_audio.fec_packets_received = 6677;
@@ -2300,6 +2304,8 @@
   video_media_info.receivers[0].local_stats.push_back(SsrcReceiverInfo());
   video_media_info.receivers[0].local_stats[0].ssrc = 1;
   video_media_info.receivers[0].packets_received = 2;
+  video_media_info.receivers[0].packets_received_with_ect1 = 7;
+  video_media_info.receivers[0].packets_received_with_ce = 5;
   video_media_info.receivers[0].packets_lost = 42;
   video_media_info.receivers[0].payload_bytes_received = 3;
   video_media_info.receivers[0].header_and_padding_bytes_received = 12;
@@ -2377,6 +2383,8 @@
   expected_video.pli_count = 6;
   expected_video.nack_count = 7;
   expected_video.packets_received = 2;
+  expected_video.packets_received_with_ect1 = 7;
+  expected_video.packets_received_with_ce = 5;
   expected_video.bytes_received = 3;
   expected_video.header_bytes_received = 12;
   expected_video.packets_lost = 42;
diff --git a/pc/rtc_stats_integrationtest.cc b/pc/rtc_stats_integrationtest.cc
index 3b7f616..342637d 100644
--- a/pc/rtc_stats_integrationtest.cc
+++ b/pc/rtc_stats_integrationtest.cc
@@ -556,6 +556,12 @@
         inbound_stream.remote_id, RTCRemoteOutboundRtpStreamStats::kType);
     verifier.TestAttributeIsDefined(inbound_stream.mid);
     verifier.TestAttributeIsDefined(inbound_stream.track_identifier);
+    // TODO: bugs.webrtc.org/42225697 - move to RTCReceivedRtpStreamStats
+    // when wiring the RFC 8888 feedback to stats.
+    verifier.TestAttributeIsNonNegative<int64_t>(
+        inbound_stream.packets_received_with_ect1);
+    verifier.TestAttributeIsNonNegative<int64_t>(
+        inbound_stream.packets_received_with_ce);
     if (inbound_stream.kind.has_value() && *inbound_stream.kind == "video") {
       verifier.TestAttributeIsNonNegative<uint64_t>(inbound_stream.qp_sum);
       verifier.TestAttributeIsDefined(inbound_stream.decoder_implementation);
@@ -799,7 +805,7 @@
     verifier.TestAttributeIsNonNegative<uint64_t>(
         outbound_stream.retransmitted_bytes_sent);
     verifier.TestAttributeIsNonNegative<double>(outbound_stream.target_bitrate);
-    verifier.TestAttributeIsNonNegative<uint64_t>(
+    verifier.TestAttributeIsNonNegative<int64_t>(
         outbound_stream.packets_sent_with_ect1);
     if (outbound_stream.kind.has_value() && *outbound_stream.kind == "video") {
       verifier.TestAttributeIsDefined(outbound_stream.frames_encoded);
@@ -896,6 +902,12 @@
         remote_inbound_stream.total_round_trip_time);
     verifier.TestAttributeIsNonNegative<int32_t>(
         remote_inbound_stream.round_trip_time_measurements);
+    // TODO: bugs.webrtc.org/42225697 - move to RTCReceivedRtpStreamStats
+    // when wiring the RFC 8888 feedback to stats.
+    verifier.TestAttributeIsUndefined<int64_t>(
+        remote_inbound_stream.packets_received_with_ect1);
+    verifier.TestAttributeIsUndefined<int64_t>(
+        remote_inbound_stream.packets_received_with_ce);
     return verifier.ExpectAllAttributesSuccessfullyTested();
   }
 
diff --git a/stats/rtcstats_objects.cc b/stats/rtcstats_objects.cc
index 7d2bb64..ff383d9 100644
--- a/stats/rtcstats_objects.cc
+++ b/stats/rtcstats_objects.cc
@@ -186,7 +186,9 @@
 WEBRTC_RTCSTATS_IMPL(
     RTCReceivedRtpStreamStats, RTCRtpStreamStats, "received-rtp",
     AttributeInit("jitter", &jitter),
-    AttributeInit("packetsLost", &packets_lost))
+    AttributeInit("packetsLost", &packets_lost),
+    AttributeInit("packetsReceivedWithEct1", &packets_received_with_ect1),
+    AttributeInit("packetsReceivedWithCn", &packets_received_with_ce))
 // clang-format on
 
 RTCReceivedRtpStreamStats::RTCReceivedRtpStreamStats(std::string id,
diff --git a/test/peer_scenario/tests/l4s_test.cc b/test/peer_scenario/tests/l4s_test.cc
index 0cd0315..293c6bf 100644
--- a/test/peer_scenario/tests/l4s_test.cc
+++ b/test/peer_scenario/tests/l4s_test.cc
@@ -124,7 +124,7 @@
   return DataRate::BitsPerSec(*stats[0]->available_outgoing_bitrate);
 }
 
-std::optional<uint64_t> GetPacketsSentWithEct1(
+std::optional<int64_t> GetPacketsSentWithEct1(
     const scoped_refptr<const RTCStatsReport>& report) {
   auto stats = report->GetStatsOfType<RTCOutboundRtpStreamStats>();
   if (stats.empty()) {
@@ -133,6 +133,24 @@
   return stats[0]->packets_sent_with_ect1;
 }
 
+std::optional<int64_t> GetPacketsReceivedWithEct1(
+    const scoped_refptr<const RTCStatsReport>& report) {
+  auto stats = report->GetStatsOfType<RTCInboundRtpStreamStats>();
+  if (stats.empty()) {
+    return std::nullopt;
+  }
+  return stats[0]->packets_received_with_ect1;
+}
+
+std::optional<int64_t> GetPacketsReceivedWithCe(
+    const scoped_refptr<const RTCStatsReport>& report) {
+  auto stats = report->GetStatsOfType<RTCInboundRtpStreamStats>();
+  if (stats.empty()) {
+    return std::nullopt;
+  }
+  return stats[0]->packets_received_with_ce;
+}
+
 TEST(L4STest, NegotiateAndUseCcfbIfEnabled) {
   PeerScenario s(*test_info_);
 
@@ -380,11 +398,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: " << 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: " << feedback_counter.not_ect();
     }
   });
 
@@ -447,12 +465,12 @@
         wifi_feedback_counter.Count(packet);
         if (wifi_feedback_counter.ect1() > 0) {
           seen_ect1_on_wifi_feedback = true;
-          RTC_LOG(LS_INFO) << " ect 1 feedback on wifi: "
+          RTC_LOG(LS_INFO) << "ect 1 feedback on wifi: "
                            << wifi_feedback_counter.ect1();
         }
         if (wifi_feedback_counter.not_ect() > 0) {
           seen_not_ect_on_wifi_feedback = true;
-          RTC_LOG(LS_INFO) << " not ect feedback on wifi: "
+          RTC_LOG(LS_INFO) << "not ect feedback on wifi: "
                            << wifi_feedback_counter.not_ect();
         }
       });
@@ -487,7 +505,7 @@
         cellular_feedback_counter.Count(packet);
         if (cellular_feedback_counter.ect1() > 0) {
           seen_ect1_on_cellular_feedback = true;
-          RTC_LOG(LS_INFO) << " ect 1 feedback on cellular: "
+          RTC_LOG(LS_INFO) << "ect 1 feedback on cellular: "
                            << cellular_feedback_counter.ect1();
         }
       });
@@ -496,10 +514,20 @@
   s.net()->DisableEndpoint(callee->endpoint(0));
   EXPECT_TRUE(
       s.WaitAndProcess(&seen_ect1_on_cellular_feedback, TimeDelta::Seconds(5)));
+
+  // Check statistics.
   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());
+
+  auto callee_stats = GetStatsAndProcess(s, callee);
+  auto packets_received_with_ect1_stats =
+      GetPacketsReceivedWithEct1(callee_stats);
+  auto packets_received_with_ce_stats = GetPacketsReceivedWithCe(callee_stats);
+  EXPECT_EQ(packets_received_with_ect1_stats, wifi_feedback_counter.ect1());
+  // TODO: bugs.webrtc.org/42225697 - testing CE would be useful.
+  EXPECT_EQ(packets_received_with_ce_stats, 0);
 }
 
 }  // namespace