[Stats] Explicit RTP-RTX and RTP-FEC mappings. Unblocks simulcast stats.

--- Background ---
The webrtc::VideoSendStream::StreamStats are converted into
VideoSenderInfo objects which turn into "outbound-rtp" stats objects in
getStats() (or "ssrc" objects in legacy getStats()).

StreamStats are created for each type of substream: RTP media streams,
RTX streams and FlexFEC streams - each with individual packet counters.

The RTX stream is responsible for retransmissions of a referenced media
stream and the FlexFEC stream is responsible for FEC of a referenced
media stream. RTX/FEC streams do not show up as separate objects in
getStats(). Only the media streams become "outbound-rtp" objects, but
their packet and byte counters have to include the RTX and FEC counters.

--- Overview of this CL ---
This CL adds MergeInfoAboutOutboundRtpSubstreams(). It takes
StreamStats of all kinds as input, and outputs media-only StreamStats
- incorporating the RTX and FEC counters into the relevant media
StreamStats.

The merged StreamStats objects is a smaller set of objects than the
non-merged counterparts, but when aggregating all packet counters
together we end up with exact same packet and count as before.

Because WebRtcVideoSendStream::GetVideoSenderInfo() currently aggregates
the StreamStats into a single VideoSenderInfo (single "outbound-rtp"),
this CL should not have any observable side-effects. Prior to this CL:
aggregate StreamStats. After this CL: merge StreamStats and then
aggregate them.

However, when simulcast stats are implemented (WIP CL:
https://webrtc-review.googlesource.com/c/src/+/168120) each RTP media
stream should turn into an individual "outbound-rtp" object. We will
then no longer aggregate all StreamStats into a single "info". This CL
unblocks simulcast stats by providing StreamStats objects that could be
turned into individual VideoSenderInfos.

--- The Changes ---
1. Methods added to RtpConfig to be able to easily tell the relationship
   between RTP, RTX and FEC ssrcs.
2. StreamStats gets a StreamType (kMedia, kRtx or kFlexfec) that
   replaces the booleans (is_rtx, is_flexfec).
3. "referenced_media_ssrc" is added to StreamStats, making it possible
   to tell which kRtx/kFlexFec stream stats need to be merged with which
   kMedia StreamStats.
4. MergeInfoAboutOutboundRtpSubstreams() added and used.

Bug: webrtc:11439
Change-Id: Iaf9002041169a054ddfd32c7ea06bd1dc36c6bca
Reviewed-on: https://webrtc-review.googlesource.com/c/src/+/170826
Commit-Queue: Henrik Boström <hbos@webrtc.org>
Reviewed-by: Ilya Nikolaevskiy <ilnik@webrtc.org>
Reviewed-by: Harald Alvestrand <hta@webrtc.org>
Reviewed-by: Stefan Holmer <stefan@webrtc.org>
Cr-Commit-Position: refs/heads/master@{#30869}
diff --git a/call/BUILD.gn b/call/BUILD.gn
index a2e8c05..a33df15 100644
--- a/call/BUILD.gn
+++ b/call/BUILD.gn
@@ -97,7 +97,9 @@
     "../api/transport:bitrate_settings",
     "../api/units:timestamp",
     "../modules/rtp_rtcp:rtp_rtcp_format",
+    "../rtc_base:checks",
     "../rtc_base:rtc_base_approved",
+    "//third_party/abseil-cpp/absl/algorithm:container",
     "//third_party/abseil-cpp/absl/types:optional",
   ]
 }
diff --git a/call/rtp_config.cc b/call/rtp_config.cc
index a23351b..d1db867 100644
--- a/call/rtp_config.cc
+++ b/call/rtp_config.cc
@@ -12,11 +12,29 @@
 
 #include <cstdint>
 
+#include "absl/algorithm/container.h"
 #include "api/array_view.h"
+#include "rtc_base/checks.h"
 #include "rtc_base/strings/string_builder.h"
 
 namespace webrtc {
 
+namespace {
+
+uint32_t FindAssociatedSsrc(uint32_t ssrc,
+                            const std::vector<uint32_t>& ssrcs,
+                            const std::vector<uint32_t>& associated_ssrcs) {
+  RTC_DCHECK_EQ(ssrcs.size(), associated_ssrcs.size());
+  for (size_t i = 0; i < ssrcs.size(); ++i) {
+    if (ssrcs[i] == ssrc)
+      return associated_ssrcs[i];
+  }
+  RTC_NOTREACHED();
+  return 0;
+}
+
+}  // namespace
+
 std::string LntfConfig::ToString() const {
   return enabled ? "{enabled: true}" : "{enabled: false}";
 }
@@ -124,4 +142,51 @@
   ss << '}';
   return ss.str();
 }
+
+bool RtpConfig::IsMediaSsrc(uint32_t ssrc) const {
+  return absl::c_linear_search(ssrcs, ssrc);
+}
+
+bool RtpConfig::IsRtxSsrc(uint32_t ssrc) const {
+  return absl::c_linear_search(rtx.ssrcs, ssrc);
+}
+
+bool RtpConfig::IsFlexfecSsrc(uint32_t ssrc) const {
+  return flexfec.payload_type != -1 && ssrc == flexfec.ssrc;
+}
+
+absl::optional<uint32_t> RtpConfig::GetRtxSsrcAssociatedWithMediaSsrc(
+    uint32_t media_ssrc) const {
+  RTC_DCHECK(IsMediaSsrc(media_ssrc));
+  // If we don't use RTX there is no association.
+  if (rtx.ssrcs.empty())
+    return absl::nullopt;
+  // If we use RTX there MUST be an association ssrcs[i] <-> rtx.ssrcs[i].
+  RTC_DCHECK_EQ(ssrcs.size(), rtx.ssrcs.size());
+  return FindAssociatedSsrc(media_ssrc, ssrcs, rtx.ssrcs);
+}
+
+uint32_t RtpConfig::GetMediaSsrcAssociatedWithRtxSsrc(uint32_t rtx_ssrc) const {
+  RTC_DCHECK(IsRtxSsrc(rtx_ssrc));
+  // If we use RTX there MUST be an association ssrcs[i] <-> rtx.ssrcs[i].
+  RTC_DCHECK_EQ(ssrcs.size(), rtx.ssrcs.size());
+  return FindAssociatedSsrc(rtx_ssrc, rtx.ssrcs, ssrcs);
+}
+
+uint32_t RtpConfig::GetMediaSsrcAssociatedWithFlexfecSsrc(
+    uint32_t flexfec_ssrc) const {
+  RTC_DCHECK(IsFlexfecSsrc(flexfec_ssrc));
+  // If we use FlexFEC there MUST be an associated media ssrc.
+  //
+  // TODO(brandtr/hbos): The current implementation only supports an association
+  // with a single media ssrc. If multiple ssrcs are to be supported in the
+  // future, in order not to break GetStats()'s packet and byte counters, we
+  // must be able to tell how many packets and bytes have contributed to which
+  // SSRC.
+  RTC_DCHECK_EQ(1u, flexfec.protected_media_ssrcs.size());
+  uint32_t media_ssrc = flexfec.protected_media_ssrcs[0];
+  RTC_DCHECK(IsMediaSsrc(media_ssrc));
+  return media_ssrc;
+}
+
 }  // namespace webrtc
diff --git a/call/rtp_config.h b/call/rtp_config.h
index a0596a8..d9caeb0 100644
--- a/call/rtp_config.h
+++ b/call/rtp_config.h
@@ -17,6 +17,7 @@
 #include <string>
 #include <vector>
 
+#include "absl/types/optional.h"
 #include "api/rtp_headers.h"
 #include "api/rtp_parameters.h"
 
@@ -157,6 +158,14 @@
 
   // RTCP CNAME, see RFC 3550.
   std::string c_name;
+
+  bool IsMediaSsrc(uint32_t ssrc) const;
+  bool IsRtxSsrc(uint32_t ssrc) const;
+  bool IsFlexfecSsrc(uint32_t ssrc) const;
+  absl::optional<uint32_t> GetRtxSsrcAssociatedWithMediaSsrc(
+      uint32_t media_ssrc) const;
+  uint32_t GetMediaSsrcAssociatedWithRtxSsrc(uint32_t rtx_ssrc) const;
+  uint32_t GetMediaSsrcAssociatedWithFlexfecSsrc(uint32_t flexfec_ssrc) const;
 };
 }  // namespace webrtc
 #endif  // CALL_RTP_CONFIG_H_
diff --git a/call/rtp_video_sender.cc b/call/rtp_video_sender.cc
index cf9af9f..cb32085 100644
--- a/call/rtp_video_sender.cc
+++ b/call/rtp_video_sender.cc
@@ -231,7 +231,7 @@
   std::vector<RtpStreamSender> rtp_streams;
 
   RTC_DCHECK(rtp_config.rtx.ssrcs.empty() ||
-             rtp_config.rtx.ssrcs.size() == rtp_config.rtx.ssrcs.size());
+             rtp_config.rtx.ssrcs.size() == rtp_config.ssrcs.size());
   for (size_t i = 0; i < rtp_config.ssrcs.size(); ++i) {
     RTPSenderVideo::Config video_config;
     configuration.local_media_ssrc = rtp_config.ssrcs[i];
@@ -241,9 +241,10 @@
     configuration.fec_generator = fec_generator.get();
     video_config.fec_generator = fec_generator.get();
 
-    if (rtp_config.rtx.ssrcs.size() > i) {
-      configuration.rtx_send_ssrc = rtp_config.rtx.ssrcs[i];
-    }
+    configuration.rtx_send_ssrc =
+        rtp_config.GetRtxSsrcAssociatedWithMediaSsrc(rtp_config.ssrcs[i]);
+    RTC_DCHECK_EQ(configuration.rtx_send_ssrc.has_value(),
+                  !rtp_config.rtx.ssrcs.empty());
 
     configuration.need_rtp_packet_infos = rtp_config.lntf.enabled;
 
diff --git a/call/video_send_stream.cc b/call/video_send_stream.cc
index f495d08..a4b6744 100644
--- a/call/video_send_stream.cc
+++ b/call/video_send_stream.cc
@@ -17,12 +17,31 @@
 
 namespace webrtc {
 
+namespace {
+
+const char* StreamTypeToString(VideoSendStream::StreamStats::StreamType type) {
+  switch (type) {
+    case VideoSendStream::StreamStats::StreamType::kMedia:
+      return "media";
+    case VideoSendStream::StreamStats::StreamType::kRtx:
+      return "rtx";
+    case VideoSendStream::StreamStats::StreamType::kFlexfec:
+      return "flexfec";
+  }
+}
+
+}  // namespace
+
 VideoSendStream::StreamStats::StreamStats() = default;
 VideoSendStream::StreamStats::~StreamStats() = default;
 
 std::string VideoSendStream::StreamStats::ToString() const {
   char buf[1024];
   rtc::SimpleStringBuilder ss(buf);
+  ss << "type: " << StreamTypeToString(type);
+  if (referenced_media_ssrc.has_value())
+    ss << " (for: " << referenced_media_ssrc.value() << ")";
+  ss << ", ";
   ss << "width: " << width << ", ";
   ss << "height: " << height << ", ";
   ss << "key: " << frame_counts.key_frames << ", ";
@@ -64,7 +83,8 @@
   ss << "#quality_adaptations: " << number_of_quality_adapt_changes;
   ss << '}';
   for (const auto& substream : substreams) {
-    if (!substream.second.is_rtx && !substream.second.is_flexfec) {
+    if (substream.second.type ==
+        VideoSendStream::StreamStats::StreamType::kMedia) {
       ss << " {ssrc: " << substream.first << ", ";
       ss << substream.second.ToString();
       ss << '}';
diff --git a/call/video_send_stream.h b/call/video_send_stream.h
index 962578d..08806ec 100644
--- a/call/video_send_stream.h
+++ b/call/video_send_stream.h
@@ -40,15 +40,35 @@
 
 class VideoSendStream {
  public:
+  // Multiple StreamStats objects are present if simulcast is used (multiple
+  // kMedia streams) or if RTX or FlexFEC is negotiated. Multiple SVC layers, on
+  // the other hand, does not cause additional StreamStats.
   struct StreamStats {
+    enum class StreamType {
+      // A media stream is an RTP stream for audio or video. Retransmissions and
+      // FEC is either sent over the same SSRC or negotiated to be sent over
+      // separate SSRCs, in which case separate StreamStats objects exist with
+      // references to this media stream's SSRC.
+      kMedia,
+      // RTX streams are streams dedicated to retransmissions. They have a
+      // dependency on a single kMedia stream: |referenced_media_ssrc|.
+      kRtx,
+      // FlexFEC streams are streams dedicated to FlexFEC. They have a
+      // dependency on a single kMedia stream: |referenced_media_ssrc|.
+      kFlexfec,
+    };
+
     StreamStats();
     ~StreamStats();
 
     std::string ToString() const;
 
+    StreamType type = StreamType::kMedia;
+    // If |type| is kRtx or kFlexfec this value is present. The referenced SSRC
+    // is the kMedia stream that this stream is performing retransmissions or
+    // FEC for. If |type| is kMedia, this value is null.
+    absl::optional<uint32_t> referenced_media_ssrc;
     FrameCounts frame_counts;
-    bool is_rtx = false;
-    bool is_flexfec = false;
     int width = 0;
     int height = 0;
     // TODO(holmer): Move bitrate_bps out to the webrtc::Call layer.
@@ -63,6 +83,13 @@
     // A snapshot of the most recent Report Block with additional data of
     // interest to statistics. Used to implement RTCRemoteInboundRtpStreamStats.
     absl::optional<ReportBlockData> report_block_data;
+
+    // These booleans are redundant; this information is already exposed in
+    // |type|.
+    // TODO(hbos): Update downstream projects to use |type| instead and delete
+    // these members.
+    bool is_flexfec = false;
+    bool is_rtx = false;
   };
 
   struct Stats {
diff --git a/media/engine/webrtc_video_engine.cc b/media/engine/webrtc_video_engine.cc
index 434a758..9a8ee88 100644
--- a/media/engine/webrtc_video_engine.cc
+++ b/media/engine/webrtc_video_engine.cc
@@ -324,6 +324,53 @@
   return res;
 }
 
+std::map<uint32_t, webrtc::VideoSendStream::StreamStats>
+MergeInfoAboutOutboundRtpSubstreams(
+    const std::map<uint32_t, webrtc::VideoSendStream::StreamStats>&
+        substreams) {
+  std::map<uint32_t, webrtc::VideoSendStream::StreamStats> rtp_substreams;
+  // Add substreams for all RTP media streams.
+  for (const auto& pair : substreams) {
+    uint32_t ssrc = pair.first;
+    const webrtc::VideoSendStream::StreamStats& substream = pair.second;
+    switch (substream.type) {
+      case webrtc::VideoSendStream::StreamStats::StreamType::kMedia:
+        break;
+      case webrtc::VideoSendStream::StreamStats::StreamType::kRtx:
+      case webrtc::VideoSendStream::StreamStats::StreamType::kFlexfec:
+        continue;
+    }
+    rtp_substreams.insert(std::make_pair(ssrc, substream));
+  }
+  // Complement the kMedia substream stats with the associated kRtx and kFlexfec
+  // substream stats.
+  for (const auto& pair : substreams) {
+    switch (pair.second.type) {
+      case webrtc::VideoSendStream::StreamStats::StreamType::kMedia:
+        continue;
+      case webrtc::VideoSendStream::StreamStats::StreamType::kRtx:
+      case webrtc::VideoSendStream::StreamStats::StreamType::kFlexfec:
+        break;
+    }
+    // The associated substream is an RTX or FlexFEC substream that is
+    // referencing an RTP media substream.
+    const webrtc::VideoSendStream::StreamStats& associated_substream =
+        pair.second;
+    RTC_DCHECK(associated_substream.referenced_media_ssrc.has_value());
+    uint32_t media_ssrc = associated_substream.referenced_media_ssrc.value();
+    RTC_DCHECK(substreams.find(media_ssrc) != substreams.end());
+    webrtc::VideoSendStream::StreamStats& rtp_substream =
+        rtp_substreams[media_ssrc];
+
+    // We only merge |rtp_stats|. All other metrics are not applicable for RTX
+    // and FlexFEC.
+    // TODO(hbos): kRtx and kFlexfec stats should use a separate struct to make
+    // it clear what is or is not applicable.
+    rtp_substream.rtp_stats.Add(associated_substream.rtp_stats);
+  }
+  return rtp_substreams;
+}
+
 }  // namespace
 
 // This constant is really an on/off, lower-level configurable NACK history
@@ -335,6 +382,13 @@
 // Minimum time interval for logging stats.
 static const int64_t kStatsLogIntervalMs = 10000;
 
+std::map<uint32_t, webrtc::VideoSendStream::StreamStats>
+MergeInfoAboutOutboundRtpSubstreamsForTesting(
+    const std::map<uint32_t, webrtc::VideoSendStream::StreamStats>&
+        substreams) {
+  return MergeInfoAboutOutboundRtpSubstreams(substreams);
+}
+
 rtc::scoped_refptr<webrtc::VideoEncoderConfig::EncoderSpecificSettings>
 WebRtcVideoChannel::WebRtcVideoSendStream::ConfigureVideoEncoderSettings(
     const VideoCodec& codec) {
@@ -2420,32 +2474,24 @@
   info.send_frame_width = 0;
   info.send_frame_height = 0;
   info.total_packet_send_delay_ms = 0;
-  for (std::map<uint32_t, webrtc::VideoSendStream::StreamStats>::iterator it =
-           stats.substreams.begin();
-       it != stats.substreams.end(); ++it) {
+  std::map<uint32_t, webrtc::VideoSendStream::StreamStats>
+      outbound_rtp_substreams =
+          MergeInfoAboutOutboundRtpSubstreams(stats.substreams);
+  for (const auto& pair : outbound_rtp_substreams) {
     // TODO(pbos): Wire up additional stats, such as padding bytes.
-    webrtc::VideoSendStream::StreamStats stream_stats = it->second;
+    const webrtc::VideoSendStream::StreamStats& stream_stats = pair.second;
+    RTC_DCHECK_EQ(stream_stats.type,
+                  webrtc::VideoSendStream::StreamStats::StreamType::kMedia);
     info.payload_bytes_sent += stream_stats.rtp_stats.transmitted.payload_bytes;
     info.header_and_padding_bytes_sent +=
         stream_stats.rtp_stats.transmitted.header_bytes +
         stream_stats.rtp_stats.transmitted.padding_bytes;
     info.packets_sent += stream_stats.rtp_stats.transmitted.packets;
     info.total_packet_send_delay_ms += stream_stats.total_packet_send_delay_ms;
-    if (!stream_stats.is_flexfec) {
-      // Retransmissions can happen over the same SSRC that media is sent over,
-      // or a separate RTX stream is negotiated per SSRC, in which case there
-      // will be a |stream_stats| with "is_rtx == true". Since we are currently
-      // aggregating all substreams' counters into a single "info" we do not
-      // need to know the relationship between RTX streams and RTP streams here.
-      // TODO(https://crbug.com/webrtc/11439): To unblock simulcast-aware stats,
-      // where substreams are not aggregated, we need to know the relationship
-      // between RTX streams and RTP streams so that the correct "info" object
-      // accounts for the correct RTX retransmissions.
-      info.retransmitted_bytes_sent +=
-          stream_stats.rtp_stats.retransmitted.payload_bytes;
-      info.retransmitted_packets_sent +=
-          stream_stats.rtp_stats.retransmitted.packets;
-    }
+    info.retransmitted_bytes_sent +=
+        stream_stats.rtp_stats.retransmitted.payload_bytes;
+    info.retransmitted_packets_sent +=
+        stream_stats.rtp_stats.retransmitted.packets;
     info.packets_lost += stream_stats.rtcp_stats.packets_lost;
     if (stream_stats.width > info.send_frame_width)
       info.send_frame_width = stream_stats.width;
@@ -2454,8 +2500,7 @@
     info.firs_rcvd += stream_stats.rtcp_packet_type_counts.fir_packets;
     info.nacks_rcvd += stream_stats.rtcp_packet_type_counts.nack_packets;
     info.plis_rcvd += stream_stats.rtcp_packet_type_counts.pli_packets;
-    if (stream_stats.report_block_data.has_value() && !stream_stats.is_rtx &&
-        !stream_stats.is_flexfec) {
+    if (stream_stats.report_block_data.has_value()) {
       info.report_block_datas.push_back(stream_stats.report_block_data.value());
     }
   }
diff --git a/media/engine/webrtc_video_engine.h b/media/engine/webrtc_video_engine.h
index 9f5e6d3..4eade6f 100644
--- a/media/engine/webrtc_video_engine.h
+++ b/media/engine/webrtc_video_engine.h
@@ -51,6 +51,19 @@
 
 class WebRtcVideoChannel;
 
+// Public for testing.
+// Inputs StreamStats for all types of substreams (kMedia, kRtx, kFlexfec) and
+// merges any non-kMedia substream stats object into its referenced kMedia-type
+// substream. The resulting substreams are all kMedia. This means, for example,
+// that packet and byte counters of RTX and FlexFEC streams are accounted for in
+// the relevant RTP media stream's stats. This makes the resulting StreamStats
+// objects ready to be turned into "outbound-rtp" stats objects for GetStats()
+// which does not create separate stream stats objects for complementary
+// streams.
+std::map<uint32_t, webrtc::VideoSendStream::StreamStats>
+MergeInfoAboutOutboundRtpSubstreamsForTesting(
+    const std::map<uint32_t, webrtc::VideoSendStream::StreamStats>& substreams);
+
 class UnsignalledSsrcHandler {
  public:
   enum Action {
diff --git a/media/engine/webrtc_video_engine_unittest.cc b/media/engine/webrtc_video_engine_unittest.cc
index 563e3f3..bc2f5f2 100644
--- a/media/engine/webrtc_video_engine_unittest.cc
+++ b/media/engine/webrtc_video_engine_unittest.cc
@@ -5269,12 +5269,112 @@
   EXPECT_EQ(stats.number_of_cpu_adapt_changes, info.senders[0].adapt_changes);
 }
 
+TEST(WebRtcVideoChannelHelperTest, MergeInfoAboutOutboundRtpSubstreams) {
+  const uint32_t kFirstMediaStreamSsrc = 10;
+  const uint32_t kSecondMediaStreamSsrc = 20;
+  const uint32_t kRtxSsrc = 30;
+  const uint32_t kFlexfecSsrc = 40;
+  std::map<uint32_t, webrtc::VideoSendStream::StreamStats> substreams;
+  // First kMedia stream.
+  substreams[kFirstMediaStreamSsrc].type =
+      webrtc::VideoSendStream::StreamStats::StreamType::kMedia;
+  substreams[kFirstMediaStreamSsrc].rtp_stats.transmitted.header_bytes = 1;
+  substreams[kFirstMediaStreamSsrc].rtp_stats.transmitted.padding_bytes = 2;
+  substreams[kFirstMediaStreamSsrc].rtp_stats.transmitted.payload_bytes = 3;
+  substreams[kFirstMediaStreamSsrc].rtp_stats.transmitted.packets = 4;
+  substreams[kFirstMediaStreamSsrc].rtp_stats.retransmitted.header_bytes = 5;
+  substreams[kFirstMediaStreamSsrc].rtp_stats.retransmitted.padding_bytes = 6;
+  substreams[kFirstMediaStreamSsrc].rtp_stats.retransmitted.payload_bytes = 7;
+  substreams[kFirstMediaStreamSsrc].rtp_stats.retransmitted.packets = 8;
+  substreams[kFirstMediaStreamSsrc].referenced_media_ssrc = absl::nullopt;
+  substreams[kFirstMediaStreamSsrc].width = 1280;
+  substreams[kFirstMediaStreamSsrc].height = 720;
+  // Second kMedia stream.
+  substreams[kSecondMediaStreamSsrc].type =
+      webrtc::VideoSendStream::StreamStats::StreamType::kMedia;
+  substreams[kSecondMediaStreamSsrc].rtp_stats.transmitted.header_bytes = 10;
+  substreams[kSecondMediaStreamSsrc].rtp_stats.transmitted.padding_bytes = 11;
+  substreams[kSecondMediaStreamSsrc].rtp_stats.transmitted.payload_bytes = 12;
+  substreams[kSecondMediaStreamSsrc].rtp_stats.transmitted.packets = 13;
+  substreams[kSecondMediaStreamSsrc].rtp_stats.retransmitted.header_bytes = 14;
+  substreams[kSecondMediaStreamSsrc].rtp_stats.retransmitted.padding_bytes = 15;
+  substreams[kSecondMediaStreamSsrc].rtp_stats.retransmitted.payload_bytes = 16;
+  substreams[kSecondMediaStreamSsrc].rtp_stats.retransmitted.packets = 17;
+  substreams[kSecondMediaStreamSsrc].referenced_media_ssrc = absl::nullopt;
+  substreams[kSecondMediaStreamSsrc].width = 640;
+  substreams[kSecondMediaStreamSsrc].height = 480;
+  // kRtx stream referencing the first kMedia stream.
+  substreams[kRtxSsrc].type =
+      webrtc::VideoSendStream::StreamStats::StreamType::kRtx;
+  substreams[kRtxSsrc].rtp_stats.transmitted.header_bytes = 19;
+  substreams[kRtxSsrc].rtp_stats.transmitted.padding_bytes = 20;
+  substreams[kRtxSsrc].rtp_stats.transmitted.payload_bytes = 21;
+  substreams[kRtxSsrc].rtp_stats.transmitted.packets = 22;
+  substreams[kRtxSsrc].rtp_stats.retransmitted.header_bytes = 23;
+  substreams[kRtxSsrc].rtp_stats.retransmitted.padding_bytes = 24;
+  substreams[kRtxSsrc].rtp_stats.retransmitted.payload_bytes = 25;
+  substreams[kRtxSsrc].rtp_stats.retransmitted.packets = 26;
+  substreams[kRtxSsrc].referenced_media_ssrc = kFirstMediaStreamSsrc;
+  // kFlexfec stream referencing the second kMedia stream.
+  substreams[kFlexfecSsrc].type =
+      webrtc::VideoSendStream::StreamStats::StreamType::kFlexfec;
+  substreams[kFlexfecSsrc].rtp_stats.transmitted.header_bytes = 19;
+  substreams[kFlexfecSsrc].rtp_stats.transmitted.padding_bytes = 20;
+  substreams[kFlexfecSsrc].rtp_stats.transmitted.payload_bytes = 21;
+  substreams[kFlexfecSsrc].rtp_stats.transmitted.packets = 22;
+  substreams[kFlexfecSsrc].rtp_stats.retransmitted.header_bytes = 23;
+  substreams[kFlexfecSsrc].rtp_stats.retransmitted.padding_bytes = 24;
+  substreams[kFlexfecSsrc].rtp_stats.retransmitted.payload_bytes = 25;
+  substreams[kFlexfecSsrc].rtp_stats.retransmitted.packets = 26;
+  substreams[kFlexfecSsrc].referenced_media_ssrc = kSecondMediaStreamSsrc;
+
+  auto merged_substreams =
+      MergeInfoAboutOutboundRtpSubstreamsForTesting(substreams);
+  // Only kMedia substreams remain.
+  EXPECT_TRUE(merged_substreams.find(kFirstMediaStreamSsrc) !=
+              merged_substreams.end());
+  EXPECT_EQ(merged_substreams[kFirstMediaStreamSsrc].type,
+            webrtc::VideoSendStream::StreamStats::StreamType::kMedia);
+  EXPECT_TRUE(merged_substreams.find(kSecondMediaStreamSsrc) !=
+              merged_substreams.end());
+  EXPECT_EQ(merged_substreams[kSecondMediaStreamSsrc].type,
+            webrtc::VideoSendStream::StreamStats::StreamType::kMedia);
+  EXPECT_FALSE(merged_substreams.find(kRtxSsrc) != merged_substreams.end());
+  EXPECT_FALSE(merged_substreams.find(kFlexfecSsrc) != merged_substreams.end());
+  // Expect kFirstMediaStreamSsrc's rtp_stats to be merged with kRtxSsrc.
+  webrtc::StreamDataCounters first_media_expected_rtp_stats =
+      substreams[kFirstMediaStreamSsrc].rtp_stats;
+  first_media_expected_rtp_stats.Add(substreams[kRtxSsrc].rtp_stats);
+  EXPECT_EQ(merged_substreams[kFirstMediaStreamSsrc].rtp_stats.transmitted,
+            first_media_expected_rtp_stats.transmitted);
+  EXPECT_EQ(merged_substreams[kFirstMediaStreamSsrc].rtp_stats.retransmitted,
+            first_media_expected_rtp_stats.retransmitted);
+  // Expect kSecondMediaStreamSsrc' rtp_stats to be merged with kFlexfecSsrc.
+  webrtc::StreamDataCounters second_media_expected_rtp_stats =
+      substreams[kSecondMediaStreamSsrc].rtp_stats;
+  second_media_expected_rtp_stats.Add(substreams[kFlexfecSsrc].rtp_stats);
+  EXPECT_EQ(merged_substreams[kSecondMediaStreamSsrc].rtp_stats.transmitted,
+            second_media_expected_rtp_stats.transmitted);
+  EXPECT_EQ(merged_substreams[kSecondMediaStreamSsrc].rtp_stats.retransmitted,
+            second_media_expected_rtp_stats.retransmitted);
+  // Expect other metrics to come from the original kMedia stats.
+  EXPECT_EQ(merged_substreams[kFirstMediaStreamSsrc].width,
+            substreams[kFirstMediaStreamSsrc].width);
+  EXPECT_EQ(merged_substreams[kFirstMediaStreamSsrc].height,
+            substreams[kFirstMediaStreamSsrc].height);
+  EXPECT_EQ(merged_substreams[kSecondMediaStreamSsrc].width,
+            substreams[kSecondMediaStreamSsrc].width);
+  EXPECT_EQ(merged_substreams[kSecondMediaStreamSsrc].height,
+            substreams[kSecondMediaStreamSsrc].height);
+}
+
 TEST_F(WebRtcVideoChannelTest,
        GetStatsReportsTransmittedAndRetransmittedBytesAndPacketsCorrectly) {
   FakeVideoSendStream* stream = AddSendStream();
   webrtc::VideoSendStream::Stats stats;
   // Simulcast layer 1, RTP stream. header+padding=10, payload=20, packets=3.
-  stats.substreams[101].is_rtx = false;
+  stats.substreams[101].type =
+      webrtc::VideoSendStream::StreamStats::StreamType::kMedia;
   stats.substreams[101].rtp_stats.transmitted.header_bytes = 5;
   stats.substreams[101].rtp_stats.transmitted.padding_bytes = 5;
   stats.substreams[101].rtp_stats.transmitted.payload_bytes = 20;
@@ -5283,16 +5383,20 @@
   stats.substreams[101].rtp_stats.retransmitted.padding_bytes = 0;
   stats.substreams[101].rtp_stats.retransmitted.payload_bytes = 0;
   stats.substreams[101].rtp_stats.retransmitted.packets = 0;
+  stats.substreams[101].referenced_media_ssrc = absl::nullopt;
   // Simulcast layer 1, RTX stream. header+padding=5, payload=10, packets=1.
-  stats.substreams[102].is_rtx = true;
+  stats.substreams[102].type =
+      webrtc::VideoSendStream::StreamStats::StreamType::kRtx;
   stats.substreams[102].rtp_stats.retransmitted.header_bytes = 3;
   stats.substreams[102].rtp_stats.retransmitted.padding_bytes = 2;
   stats.substreams[102].rtp_stats.retransmitted.payload_bytes = 10;
   stats.substreams[102].rtp_stats.retransmitted.packets = 1;
   stats.substreams[102].rtp_stats.transmitted =
       stats.substreams[102].rtp_stats.retransmitted;
+  stats.substreams[102].referenced_media_ssrc = 101;
   // Simulcast layer 2, RTP stream. header+padding=20, payload=40, packets=7.
-  stats.substreams[201].is_rtx = false;
+  stats.substreams[201].type =
+      webrtc::VideoSendStream::StreamStats::StreamType::kMedia;
   stats.substreams[201].rtp_stats.transmitted.header_bytes = 10;
   stats.substreams[201].rtp_stats.transmitted.padding_bytes = 10;
   stats.substreams[201].rtp_stats.transmitted.payload_bytes = 40;
@@ -5301,14 +5405,30 @@
   stats.substreams[201].rtp_stats.retransmitted.padding_bytes = 0;
   stats.substreams[201].rtp_stats.retransmitted.payload_bytes = 0;
   stats.substreams[201].rtp_stats.retransmitted.packets = 0;
+  stats.substreams[201].referenced_media_ssrc = absl::nullopt;
   // Simulcast layer 2, RTX stream. header+padding=10, payload=20, packets=4.
-  stats.substreams[202].is_rtx = true;
+  stats.substreams[202].type =
+      webrtc::VideoSendStream::StreamStats::StreamType::kRtx;
   stats.substreams[202].rtp_stats.retransmitted.header_bytes = 6;
   stats.substreams[202].rtp_stats.retransmitted.padding_bytes = 4;
   stats.substreams[202].rtp_stats.retransmitted.payload_bytes = 20;
   stats.substreams[202].rtp_stats.retransmitted.packets = 4;
   stats.substreams[202].rtp_stats.transmitted =
       stats.substreams[202].rtp_stats.retransmitted;
+  stats.substreams[202].referenced_media_ssrc = 201;
+  // FlexFEC stream associated with the Simulcast layer 2.
+  // header+padding=15, payload=17, packets=5.
+  stats.substreams[301].type =
+      webrtc::VideoSendStream::StreamStats::StreamType::kFlexfec;
+  stats.substreams[301].rtp_stats.transmitted.header_bytes = 13;
+  stats.substreams[301].rtp_stats.transmitted.padding_bytes = 2;
+  stats.substreams[301].rtp_stats.transmitted.payload_bytes = 17;
+  stats.substreams[301].rtp_stats.transmitted.packets = 5;
+  stats.substreams[301].rtp_stats.retransmitted.header_bytes = 0;
+  stats.substreams[301].rtp_stats.retransmitted.padding_bytes = 0;
+  stats.substreams[301].rtp_stats.retransmitted.payload_bytes = 0;
+  stats.substreams[301].rtp_stats.retransmitted.packets = 0;
+  stats.substreams[301].referenced_media_ssrc = 201;
   stream->SetStats(stats);
 
   cricket::VideoMediaInfo info;
@@ -5321,9 +5441,9 @@
   // to be set up correctly, it may need to be updated such that the
   // relationship between RTP and RTX streams are known. See also
   // https://crbug.com/webrtc/11439.
-  EXPECT_EQ(45u, info.senders[0].header_and_padding_bytes_sent);
-  EXPECT_EQ(90u, info.senders[0].payload_bytes_sent);
-  EXPECT_EQ(15, info.senders[0].packets_sent);
+  EXPECT_EQ(60u, info.senders[0].header_and_padding_bytes_sent);
+  EXPECT_EQ(107u, info.senders[0].payload_bytes_sent);
+  EXPECT_EQ(20, info.senders[0].packets_sent);
   EXPECT_EQ(30u, info.senders[0].retransmitted_bytes_sent);
   EXPECT_EQ(5u, info.senders[0].retransmitted_packets_sent);
 }
diff --git a/modules/rtp_rtcp/include/rtp_rtcp_defines.h b/modules/rtp_rtcp/include/rtp_rtcp_defines.h
index 0d19a4e..a95ac99 100644
--- a/modules/rtp_rtcp/include/rtp_rtcp_defines.h
+++ b/modules/rtp_rtcp/include/rtp_rtcp_defines.h
@@ -314,6 +314,12 @@
     packets -= other.packets;
   }
 
+  bool operator==(const RtpPacketCounter& other) const {
+    return header_bytes == other.header_bytes &&
+           payload_bytes == other.payload_bytes &&
+           padding_bytes == other.padding_bytes && packets == other.packets;
+  }
+
   // Not inlined, since use of RtpPacket would result in circular includes.
   void AddPacket(const RtpPacket& packet);
 
diff --git a/video/send_statistics_proxy.cc b/video/send_statistics_proxy.cc
index 60d84f1..1464605 100644
--- a/video/send_statistics_proxy.cc
+++ b/video/send_statistics_proxy.cc
@@ -16,7 +16,6 @@
 #include <limits>
 #include <utility>
 
-#include "absl/algorithm/container.h"
 #include "api/video/video_codec_constants.h"
 #include "api/video/video_codec_type.h"
 #include "api/video_codecs/video_codec.h"
@@ -206,12 +205,17 @@
     retransmit_byte_counter_.SetLast(
         it.second.rtp_stats.retransmitted.TotalBytes(), ssrc);
     fec_byte_counter_.SetLast(it.second.rtp_stats.fec.TotalBytes(), ssrc);
-    if (it.second.is_rtx) {
-      rtx_byte_counter_.SetLast(it.second.rtp_stats.transmitted.TotalBytes(),
-                                ssrc);
-    } else {
-      media_byte_counter_.SetLast(it.second.rtp_stats.MediaPayloadBytes(),
+    switch (it.second.type) {
+      case VideoSendStream::StreamStats::StreamType::kMedia:
+        media_byte_counter_.SetLast(it.second.rtp_stats.MediaPayloadBytes(),
+                                    ssrc);
+        break;
+      case VideoSendStream::StreamStats::StreamType::kRtx:
+        rtx_byte_counter_.SetLast(it.second.rtp_stats.transmitted.TotalBytes(),
                                   ssrc);
+        break;
+      case VideoSendStream::StreamStats::StreamType::kFlexfec:
+        break;
     }
   }
 }
@@ -761,17 +765,42 @@
   if (it != stats_.substreams.end())
     return &it->second;
 
-  bool is_media = absl::c_linear_search(rtp_config_.ssrcs, ssrc);
+  bool is_media = rtp_config_.IsMediaSsrc(ssrc);
   bool is_flexfec = rtp_config_.flexfec.payload_type != -1 &&
                     ssrc == rtp_config_.flexfec.ssrc;
-  bool is_rtx = absl::c_linear_search(rtp_config_.rtx.ssrcs, ssrc);
+  bool is_rtx = rtp_config_.IsRtxSsrc(ssrc);
   if (!is_media && !is_flexfec && !is_rtx)
     return nullptr;
 
   // Insert new entry and return ptr.
   VideoSendStream::StreamStats* entry = &stats_.substreams[ssrc];
-  entry->is_rtx = is_rtx;
-  entry->is_flexfec = is_flexfec;
+  if (is_media) {
+    entry->type = VideoSendStream::StreamStats::StreamType::kMedia;
+  } else if (is_rtx) {
+    entry->type = VideoSendStream::StreamStats::StreamType::kRtx;
+  } else if (is_flexfec) {
+    entry->type = VideoSendStream::StreamStats::StreamType::kFlexfec;
+  } else {
+    RTC_NOTREACHED();
+  }
+  switch (entry->type) {
+    case VideoSendStream::StreamStats::StreamType::kMedia:
+      break;
+    case VideoSendStream::StreamStats::StreamType::kRtx:
+      entry->referenced_media_ssrc =
+          rtp_config_.GetMediaSsrcAssociatedWithRtxSsrc(ssrc);
+      entry->is_rtx = true;
+      break;
+    case VideoSendStream::StreamStats::StreamType::kFlexfec:
+      entry->referenced_media_ssrc =
+          rtp_config_.GetMediaSsrcAssociatedWithFlexfecSsrc(ssrc);
+      entry->is_flexfec = true;
+      break;
+  }
+  // TODO(hbos): Remove these booleans once downstream projects stop depedning
+  // on them, reading the value of |type| instead.
+  RTC_DCHECK_EQ(entry->is_rtx, is_rtx);
+  RTC_DCHECK_EQ(entry->is_flexfec, is_flexfec);
 
   return entry;
 }
@@ -1252,7 +1281,7 @@
   VideoSendStream::StreamStats* stats = GetStatsEntry(ssrc);
   RTC_DCHECK(stats) << "DataCountersUpdated reported for unknown ssrc " << ssrc;
 
-  if (stats->is_flexfec) {
+  if (stats->type == VideoSendStream::StreamStats::StreamType::kFlexfec) {
     // The same counters are reported for both the media ssrc and flexfec ssrc.
     // Bitrate stats are summed for all SSRCs. Use fec stats from media update.
     return;
@@ -1273,11 +1302,17 @@
   uma_container_->retransmit_byte_counter_.Set(
       counters.retransmitted.TotalBytes(), ssrc);
   uma_container_->fec_byte_counter_.Set(counters.fec.TotalBytes(), ssrc);
-  if (stats->is_rtx) {
-    uma_container_->rtx_byte_counter_.Set(counters.transmitted.TotalBytes(),
-                                          ssrc);
-  } else {
-    uma_container_->media_byte_counter_.Set(counters.MediaPayloadBytes(), ssrc);
+  switch (stats->type) {
+    case VideoSendStream::StreamStats::StreamType::kMedia:
+      uma_container_->media_byte_counter_.Set(counters.MediaPayloadBytes(),
+                                              ssrc);
+      break;
+    case VideoSendStream::StreamStats::StreamType::kRtx:
+      uma_container_->rtx_byte_counter_.Set(counters.transmitted.TotalBytes(),
+                                            ssrc);
+      break;
+    case VideoSendStream::StreamStats::StreamType::kFlexfec:
+      break;
   }
 }
 
diff --git a/video/send_statistics_proxy_unittest.cc b/video/send_statistics_proxy_unittest.cc
index 3f5ebd5..8b49a26 100644
--- a/video/send_statistics_proxy_unittest.cc
+++ b/video/send_statistics_proxy_unittest.cc
@@ -65,10 +65,16 @@
         &fake_clock_, GetTestConfig(),
         VideoEncoderConfig::ContentType::kRealtimeVideo));
     expected_ = VideoSendStream::Stats();
-    for (const auto& ssrc : config_.rtp.ssrcs)
-      expected_.substreams[ssrc].is_rtx = false;
-    for (const auto& ssrc : config_.rtp.rtx.ssrcs)
-      expected_.substreams[ssrc].is_rtx = true;
+    for (const auto& ssrc : config_.rtp.ssrcs) {
+      expected_.substreams[ssrc].type =
+          VideoSendStream::StreamStats::StreamType::kMedia;
+    }
+    for (size_t i = 0; i < config_.rtp.rtx.ssrcs.size(); ++i) {
+      uint32_t ssrc = config_.rtp.rtx.ssrcs[i];
+      expected_.substreams[ssrc].type =
+          VideoSendStream::StreamStats::StreamType::kRtx;
+      expected_.substreams[ssrc].referenced_media_ssrc = config_.rtp.ssrcs[i];
+    }
   }
 
   VideoSendStream::Config GetTestConfig() {
@@ -89,6 +95,7 @@
     config.rtp.rtx.ssrcs.push_back(kSecondRtxSsrc);
     config.rtp.flexfec.payload_type = 50;
     config.rtp.flexfec.ssrc = kFlexFecSsrc;
+    config.rtp.flexfec.protected_media_ssrcs = {kFirstSsrc};
     return config;
   }
 
@@ -123,7 +130,7 @@
       const VideoSendStream::StreamStats& a = it->second;
       const VideoSendStream::StreamStats& b = corresponding_it->second;
 
-      EXPECT_EQ(a.is_rtx, b.is_rtx);
+      EXPECT_EQ(a.type, b.type);
       EXPECT_EQ(a.frame_counts.key_frames, b.frame_counts.key_frames);
       EXPECT_EQ(a.frame_counts.delta_frames, b.frame_counts.delta_frames);
       EXPECT_EQ(a.total_bitrate_bps, b.total_bitrate_bps);
@@ -2379,6 +2386,21 @@
              4 * 100 / 5));
 }
 
+TEST_F(SendStatisticsProxyTest, GetStatsReportsIsRtx) {
+  StreamDataCountersCallback* proxy =
+      static_cast<StreamDataCountersCallback*>(statistics_proxy_.get());
+  StreamDataCounters counters;
+  proxy->DataCountersUpdated(counters, kFirstSsrc);
+  proxy->DataCountersUpdated(counters, kFirstRtxSsrc);
+
+  EXPECT_NE(GetStreamStats(kFirstSsrc).type,
+            VideoSendStream::StreamStats::StreamType::kRtx);
+  EXPECT_EQ(GetStreamStats(kFirstSsrc).referenced_media_ssrc, absl::nullopt);
+  EXPECT_EQ(GetStreamStats(kFirstRtxSsrc).type,
+            VideoSendStream::StreamStats::StreamType::kRtx);
+  EXPECT_EQ(GetStreamStats(kFirstRtxSsrc).referenced_media_ssrc, kFirstSsrc);
+}
+
 TEST_F(SendStatisticsProxyTest, GetStatsReportsIsFlexFec) {
   statistics_proxy_.reset(
       new SendStatisticsProxy(&fake_clock_, GetTestConfigWithFlexFec(),
@@ -2390,8 +2412,12 @@
   proxy->DataCountersUpdated(counters, kFirstSsrc);
   proxy->DataCountersUpdated(counters, kFlexFecSsrc);
 
-  EXPECT_FALSE(GetStreamStats(kFirstSsrc).is_flexfec);
-  EXPECT_TRUE(GetStreamStats(kFlexFecSsrc).is_flexfec);
+  EXPECT_NE(GetStreamStats(kFirstSsrc).type,
+            VideoSendStream::StreamStats::StreamType::kFlexfec);
+  EXPECT_EQ(GetStreamStats(kFirstSsrc).referenced_media_ssrc, absl::nullopt);
+  EXPECT_EQ(GetStreamStats(kFlexFecSsrc).type,
+            VideoSendStream::StreamStats::StreamType::kFlexfec);
+  EXPECT_EQ(GetStreamStats(kFlexFecSsrc).referenced_media_ssrc, kFirstSsrc);
 }
 
 TEST_F(SendStatisticsProxyTest, SendBitratesAreReportedWithFlexFecEnabled) {