RID parsing for Simulcast support.

Adding RidDescription to StreamParams that will contain the list of rids
for the track.
Adding receive_stream to MediaContentDescription to allow identifying
the stream that originates from the answerer (but is referenced by the
sender). For example, to signal that it will be received in Simulcast.

Bug: webrtc:10073.
Change-Id: Icd9a6b0a69d42bef51f525e673ce447255584334
Reviewed-on: https://webrtc-review.googlesource.com/c/113794
Commit-Queue: Amit Hilbuch <amithi@webrtc.org>
Reviewed-by: Steve Anton <steveanton@webrtc.org>
Reviewed-by: Seth Hampson <shampson@webrtc.org>
Cr-Commit-Position: refs/heads/master@{#25978}
diff --git a/media/BUILD.gn b/media/BUILD.gn
index b00615e..affbefa 100644
--- a/media/BUILD.gn
+++ b/media/BUILD.gn
@@ -97,6 +97,8 @@
     "base/mediaconstants.h",
     "base/mediaengine.cc",
     "base/mediaengine.h",
+    "base/riddescription.cc",
+    "base/riddescription.h",
     "base/rtpdataengine.cc",
     "base/rtpdataengine.h",
     "base/rtputils.cc",
diff --git a/media/base/riddescription.cc b/media/base/riddescription.cc
new file mode 100644
index 0000000..7f0c5d0
--- /dev/null
+++ b/media/base/riddescription.cc
@@ -0,0 +1,28 @@
+/*
+ *  Copyright 2018 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 "media/base/riddescription.h"
+
+namespace cricket {
+
+RidDescription::RidDescription() = default;
+RidDescription::RidDescription(const std::string& rid, RidDirection direction)
+    : rid{rid}, direction{direction} {}
+RidDescription::RidDescription(const RidDescription& other) = default;
+RidDescription::~RidDescription() = default;
+RidDescription& RidDescription::operator=(const RidDescription& other) =
+    default;
+bool RidDescription::operator==(const RidDescription& other) const {
+  return rid == other.rid && direction == other.direction &&
+         payload_types == other.payload_types &&
+         restrictions == other.restrictions;
+}
+
+}  // namespace cricket
diff --git a/media/base/riddescription.h b/media/base/riddescription.h
new file mode 100644
index 0000000..5a616ff
--- /dev/null
+++ b/media/base/riddescription.h
@@ -0,0 +1,93 @@
+/*
+ *  Copyright 2018 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.
+ */
+
+#ifndef MEDIA_BASE_RIDDESCRIPTION_H_
+#define MEDIA_BASE_RIDDESCRIPTION_H_
+
+#include <map>
+#include <string>
+#include <vector>
+
+namespace cricket {
+
+enum class RidDirection { kSend, kReceive };
+
+// Description of a Restriction Id (RID) according to:
+// https://tools.ietf.org/html/draft-ietf-mmusic-rid-15
+// A Restriction Identifier serves two purposes:
+//   1. Uniquely identifies an RTP stream inside an RTP session.
+//      When combined with MIDs (https://tools.ietf.org/html/rfc5888),
+//      RIDs uniquely identify an RTP stream within an RTP session.
+//      The MID will identify the media section and the RID will identify
+//      the stream within the section.
+//      RID identifiers must be unique within the media section.
+//   2. Allows indicating further restrictions to the stream.
+//      These restrictions are added according to the direction specified.
+//      The direction field identifies the direction of the RTP stream packets
+//      to which the restrictions apply. The direction is independent of the
+//      transceiver direction and can be one of {send, recv}.
+//      The following are some examples of these restrictions:
+//        a. max-width, max-height, max-fps, max-br, ...
+//        b. further restricting the codec set (from what m= section specified)
+//
+// Note: Indicating dependencies between streams (using depend) will not be
+// supported, since the WG is adopting a different approach to achieve this.
+// As of 2018-12-04, the new SVC (Scalable Video Coder) approach is still not
+// mature enough to be implemented as part of this work.
+// See: https://w3c.github.io/webrtc-svc/ for more details.
+struct RidDescription final {
+  RidDescription();
+  RidDescription(const std::string& rid, RidDirection direction);
+  RidDescription(const RidDescription& other);
+  ~RidDescription();
+  RidDescription& operator=(const RidDescription& other);
+
+  // This is currently required for unit tests of StreamParams which contains
+  // RidDescription objects and checks for equality using operator==.
+  bool operator==(const RidDescription& other) const;
+  bool operator!=(const RidDescription& other) const {
+    return !(*this == other);
+  }
+
+  // The RID identifier that uniquely identifies the stream within the session.
+  std::string rid;
+
+  // Specifies the direction for which the specified restrictions hold.
+  // This direction is either send or receive and is independent of the
+  // direction of the transceiver.
+  // https://tools.ietf.org/html/draft-ietf-mmusic-rid-15#section-4 :
+  // The "direction" field identifies the direction of the RTP Stream
+  // packets to which the indicated restrictions are applied.  It may be
+  // either "send" or "recv".  Note that these restriction directions are
+  // expressed independently of any "inactive", "sendonly", "recvonly", or
+  // "sendrecv" attributes associated with the media section.  It is, for
+  // example, valid to indicate "recv" restrictions on a "sendonly"
+  // stream; those restrictions would apply if, at a future point in time,
+  // the stream were changed to "sendrecv" or "recvonly".
+  RidDirection direction;
+
+  // The list of codec payload types for this stream.
+  // It should be a subset of the payloads supported for the media section.
+  std::vector<int> payload_types;
+
+  // Contains key-value pairs for restrictions.
+  // The keys are not validated against a known set.
+  // The meaning to infer for the values depends on each key.
+  // Examples:
+  // 1. An entry for max-width will have a value that is interpreted as an int.
+  // 2. An entry for max-bpp (bits per pixel) will have a float value.
+  // Interpretation (and validation of value) is left for the implementation.
+  // I.E. the media engines should validate values for parameters they support.
+  std::map<std::string, std::string> restrictions;
+};
+
+}  // namespace cricket
+
+#endif  // MEDIA_BASE_RIDDESCRIPTION_H_
diff --git a/media/base/streamparams.cc b/media/base/streamparams.cc
index 8f52559..8fb1719 100644
--- a/media/base/streamparams.cc
+++ b/media/base/streamparams.cc
@@ -11,6 +11,7 @@
 #include "media/base/streamparams.h"
 
 #include <stdint.h>
+#include <algorithm>
 #include <list>
 
 #include "api/array_view.h"
@@ -23,6 +24,35 @@
 void AddStream(std::vector<StreamParams>* streams, const StreamParams& stream) {
   streams->push_back(stream);
 }
+
+std::string SsrcsToString(const std::vector<uint32_t>& ssrcs) {
+  char buf[1024];
+  rtc::SimpleStringBuilder sb(buf);
+  sb << "ssrcs:[";
+  for (std::vector<uint32_t>::const_iterator it = ssrcs.begin();
+       it != ssrcs.end(); ++it) {
+    if (it != ssrcs.begin()) {
+      sb << ",";
+    }
+    sb << *it;
+  }
+  sb << "]";
+  return sb.str();
+}
+
+std::string RidsToString(const std::vector<RidDescription>& rids) {
+  char buf[1024];
+  rtc::SimpleStringBuilder sb(buf);
+  sb << "rids:[";
+  const char* delimiter = "";
+  for (const RidDescription& rid : rids) {
+    sb << delimiter << rid.rid;
+    delimiter = ",";
+  }
+  sb << "]";
+  return sb.str();
+}
+
 }  // namespace
 
 const char kFecSsrcGroupSemantics[] = "FEC";
@@ -87,21 +117,6 @@
   return RemoveStream(&data_, selector);
 }
 
-static std::string SsrcsToString(const std::vector<uint32_t>& ssrcs) {
-  char buf[1024];
-  rtc::SimpleStringBuilder sb(buf);
-  sb << "ssrcs:[";
-  for (std::vector<uint32_t>::const_iterator it = ssrcs.begin();
-       it != ssrcs.end(); ++it) {
-    if (it != ssrcs.begin()) {
-      sb << ",";
-    }
-    sb << *it;
-  }
-  sb << "]";
-  return sb.str();
-}
-
 SsrcGroup::SsrcGroup(const std::string& usage,
                      const std::vector<uint32_t>& ssrcs)
     : semantics(usage), ssrcs(ssrcs) {}
@@ -133,6 +148,15 @@
 StreamParams& StreamParams::operator=(const StreamParams&) = default;
 StreamParams& StreamParams::operator=(StreamParams&&) = default;
 
+bool StreamParams::operator==(const StreamParams& other) const {
+  return (groupid == other.groupid && id == other.id && ssrcs == other.ssrcs &&
+          ssrc_groups == other.ssrc_groups && cname == other.cname &&
+          stream_ids_ == other.stream_ids_ &&
+          // RIDs are not required to be in the same order for equality.
+          rids_.size() == other.rids_.size() &&
+          std::is_permutation(rids_.begin(), rids_.end(), other.rids_.begin()));
+}
+
 std::string StreamParams::ToString() const {
   char buf[2 * 1024];
   rtc::SimpleStringBuilder sb(buf);
@@ -165,6 +189,9 @@
     sb << *it;
   }
   sb << ";";
+  if (!rids_.empty()) {
+    sb << RidsToString(rids_) << ";";
+  }
   sb << "}";
   return sb.str();
 }
@@ -267,15 +294,22 @@
   return false;
 }
 
-static void RemoveFirst(std::list<uint32_t>* ssrcs, uint32_t value) {
+namespace {
+void RemoveFirst(std::list<uint32_t>* ssrcs, uint32_t value) {
   std::list<uint32_t>::iterator it =
       std::find(ssrcs->begin(), ssrcs->end(), value);
   if (it != ssrcs->end()) {
     ssrcs->erase(it);
   }
 }
+}  // namespace
 
 bool IsSimulcastStream(const StreamParams& sp) {
+  // Check for spec-compliant Simulcast using rids.
+  if (sp.rids().size() > 1) {
+    return true;
+  }
+
   const SsrcGroup* const sg = sp.get_ssrc_group(kSimSsrcGroupSemantics);
   if (sg == NULL || sg->ssrcs.size() < 2) {
     return false;
diff --git a/media/base/streamparams.h b/media/base/streamparams.h
index 27069ed..f128532 100644
--- a/media/base/streamparams.h
+++ b/media/base/streamparams.h
@@ -22,6 +22,26 @@
 // StreamParams would then contain ssrc = {10,11,20,21,30,31} and
 // ssrc_groups = {{SIM,{10,20,30}, {FEC,{10,11}, {FEC, {20,21}, {FEC {30,31}}}
 // Please see RFC 5576.
+// A spec-compliant way to achieve this is to use RIDs and Simulcast attribute
+// instead of the ssrc-group. In this method, the StreamParam object will
+// have multiple RidDescriptions, each corresponding to a simulcast layer
+// and the media section will have a simulcast attribute that indicates
+// that these layers are for the same source. This also removes the extra
+// lines for redundancy streams, as the same RIDs appear in the redundancy
+// packets.
+// Note: in the spec compliant simulcast scenario, some of the RIDs might be
+// alternatives for one another (such as different encodings for same data).
+// In the context of the StreamParams class, the notion of alternatives does
+// not exist and all the RIDs will describe different layers of the same source.
+// When the StreamParams class is used to configure the media engine, simulcast
+// considerations will be used to remove the alternative layers outside of this
+// class.
+// As an example, let the simulcast layers have RID 10, 20, 30.
+// StreamParams would contain rid = { 10, 20, 30 }.
+// MediaSection would contain SimulcastDescription specifying these rids.
+// a=simulcast:send 10;20;30 (or a=simulcast:send 10,20;30 or similar).
+// See https://tools.ietf.org/html/draft-ietf-mmusic-sdp-simulcast-13
+// and https://tools.ietf.org/html/draft-ietf-mmusic-rid-15.
 
 #ifndef MEDIA_BASE_STREAMPARAMS_H_
 #define MEDIA_BASE_STREAMPARAMS_H_
@@ -32,6 +52,7 @@
 #include <string>
 #include <vector>
 
+#include "media/base/riddescription.h"
 #include "rtc_base/constructormagic.h"
 
 namespace cricket {
@@ -80,11 +101,7 @@
     return stream;
   }
 
-  bool operator==(const StreamParams& other) const {
-    return (groupid == other.groupid && id == other.id &&
-            ssrcs == other.ssrcs && ssrc_groups == other.ssrc_groups &&
-            cname == other.cname && stream_ids_ == other.stream_ids_);
-  }
+  bool operator==(const StreamParams& other) const;
   bool operator!=(const StreamParams& other) const { return !(*this == other); }
 
   uint32_t first_ssrc() const {
@@ -172,6 +189,16 @@
   std::vector<SsrcGroup> ssrc_groups;  // e.g. FID, FEC, SIM
   std::string cname;                   // RTCP CNAME
 
+  // RID functionality according to
+  // https://tools.ietf.org/html/draft-ietf-mmusic-rid-15
+  // Each layer can be represented by a RID identifier and can also have
+  // restrictions (such as max-width, max-height, etc.)
+  // If the track has multiple layers (ex. Simulcast), each layer will be
+  // represented by a RID.
+  bool has_rids() const { return !rids_.empty(); }
+  const std::vector<RidDescription>& rids() const { return rids_; }
+  void set_rids(const std::vector<RidDescription>& rids) { rids_ = rids; }
+
  private:
   bool AddSecondarySsrc(const std::string& semantics,
                         uint32_t primary_ssrc,
@@ -184,6 +211,8 @@
   // with. In Plan B this should always be size of 1, while in Unified Plan this
   // could be none or multiple stream IDs.
   std::vector<std::string> stream_ids_;
+
+  std::vector<RidDescription> rids_;
 };
 
 // A Stream can be selected by either groupid+id or ssrc.
diff --git a/pc/sdpserializer.cc b/pc/sdpserializer.cc
index b5a1d87..8e20aa2 100644
--- a/pc/sdpserializer.cc
+++ b/pc/sdpserializer.cc
@@ -10,13 +10,19 @@
 
 #include "pc/sdpserializer.h"
 
+#include <algorithm>
 #include <string>
 #include <utility>
 #include <vector>
 
 #include "api/jsep.h"
+#include "rtc_base/checks.h"
+#include "rtc_base/string_to_number.h"
+#include "rtc_base/stringencode.h"
 #include "rtc_base/strings/string_builder.h"
 
+using cricket::RidDescription;
+using cricket::RidDirection;
 using cricket::SimulcastDescription;
 using cricket::SimulcastLayer;
 using cricket::SimulcastLayerList;
@@ -28,16 +34,20 @@
 // delimiters
 const char kDelimiterComma[] = ",";
 const char kDelimiterCommaChar = ',';
+const char kDelimiterEqual[] = "=";
+const char kDelimiterEqualChar = '=';
 const char kDelimiterSemicolon[] = ";";
 const char kDelimiterSemicolonChar = ';';
 const char kDelimiterSpace[] = " ";
 const char kDelimiterSpaceChar = ' ';
 
 // https://tools.ietf.org/html/draft-ietf-mmusic-sdp-simulcast-13#section-5.1
+// https://tools.ietf.org/html/draft-ietf-mmusic-rid-15#section-10
 const char kSimulcastPausedStream[] = "~";
 const char kSimulcastPausedStreamChar = '~';
-const char kSimulcastSendStreams[] = "send";
-const char kSimulcastReceiveStreams[] = "recv";
+const char kSendDirection[] = "send";
+const char kReceiveDirection[] = "recv";
+const char kPayloadType[] = "pt";
 
 RTCError ParseError(const std::string& message) {
   return RTCError(RTCErrorType::SYNTAX_ERROR, message);
@@ -80,8 +90,7 @@
   }
   return builder;
 }
-
-// These methods deserialize simulcast according to the specification:
+// This method deserializes simulcast according to the specification:
 // https://tools.ietf.org/html/draft-ietf-mmusic-sdp-simulcast-13#section-5.1
 // sc-str-list  = sc-alt-list *( ";" sc-alt-list )
 // sc-alt-list  = sc-id *( "," sc-id )
@@ -103,22 +112,19 @@
 
     std::vector<std::string> rid_tokens;
     rtc::tokenize_with_empty_tokens(token, kDelimiterCommaChar, &rid_tokens);
+
     if (rid_tokens.empty()) {
       return ParseError("Simulcast alternative layer list is malformed.");
     }
 
     std::vector<SimulcastLayer> layers;
-    for (const auto& rid_token : rid_tokens) {
+    for (const std::string& rid_token : rid_tokens) {
       if (rid_token.empty() || rid_token == kSimulcastPausedStream) {
         return ParseError("Rid must not be empty.");
       }
 
       bool paused = rid_token[0] == kSimulcastPausedStreamChar;
       std::string rid = paused ? rid_token.substr(1) : rid_token;
-
-      // TODO(amithi, bugs.webrtc.org/10073):
-      // Validate the rid format.
-      // See also: https://github.com/w3c/webrtc-pc/issues/2013
       layers.push_back(SimulcastLayer(rid, paused));
     }
 
@@ -128,6 +134,48 @@
   return std::move(result);
 }
 
+webrtc::RTCError ParseRidPayloadList(const std::string& payload_list,
+                                     RidDescription* rid_description) {
+  RTC_DCHECK(rid_description);
+  std::vector<int>& payload_types = rid_description->payload_types;
+  // Check that the description doesn't have any payload types or restrictions.
+  // If the pt= field is specified, it must be first and must not repeat.
+  if (!payload_types.empty()) {
+    return ParseError("Multiple pt= found in RID Description.");
+  }
+  if (!rid_description->restrictions.empty()) {
+    return ParseError("Payload list must appear first in the restrictions.");
+  }
+
+  // If the pt= field is specified, it must have a value.
+  if (payload_list.empty()) {
+    return ParseError("Payload list must have at least one value.");
+  }
+
+  // Tokenize the ',' delimited list
+  std::vector<std::string> string_payloads;
+  rtc::tokenize(payload_list, kDelimiterCommaChar, &string_payloads);
+  if (string_payloads.empty()) {
+    return ParseError("Payload list must have at least one value.");
+  }
+
+  for (const std::string& payload_type : string_payloads) {
+    absl::optional<int> value = rtc::StringToNumber<int>(payload_type);
+    if (!value.has_value()) {
+      return ParseError("Invalid payload type: " + payload_type);
+    }
+
+    // Check if the value already appears in the payload list.
+    if (std::find(payload_types.begin(), payload_types.end(), value.value()) !=
+        payload_types.end()) {
+      return ParseError("Duplicate payload type in list: " + payload_type);
+    }
+    payload_types.push_back(value.value());
+  }
+
+  return RTCError::OK();
+}
+
 }  // namespace
 
 std::string SdpSerializer::SerializeSimulcastDescription(
@@ -136,12 +184,12 @@
   std::string delimiter;
 
   if (!simulcast.send_layers().empty()) {
-    sb << kSimulcastSendStreams << kDelimiterSpace << simulcast.send_layers();
+    sb << kSendDirection << kDelimiterSpace << simulcast.send_layers();
     delimiter = kDelimiterSpace;
   }
 
   if (!simulcast.receive_layers().empty()) {
-    sb << delimiter << kSimulcastReceiveStreams << kDelimiterSpace
+    sb << delimiter << kReceiveDirection << kDelimiterSpace
        << simulcast.receive_layers();
   }
 
@@ -171,10 +219,9 @@
   bool bidirectional = tokens.size() == 4;  // indicates both send and recv
 
   // Tokens 0, 2 (if exists) should be send / recv
-  if ((tokens[0] != kSimulcastSendStreams &&
-       tokens[0] != kSimulcastReceiveStreams) ||
-      (bidirectional && tokens[2] != kSimulcastSendStreams &&
-       tokens[2] != kSimulcastReceiveStreams) ||
+  if ((tokens[0] != kSendDirection && tokens[0] != kReceiveDirection) ||
+      (bidirectional && tokens[2] != kSendDirection &&
+       tokens[2] != kReceiveDirection) ||
       (bidirectional && tokens[0] == tokens[2])) {
     return ParseError("Valid values: send / recv.");
   }
@@ -194,7 +241,7 @@
   }
 
   // Set the layers so that list1 is for send and list2 is for recv
-  if (tokens[0] != kSimulcastSendStreams) {
+  if (tokens[0] != kSendDirection) {
     std::swap(list1, list2);
   }
 
@@ -214,4 +261,127 @@
   return std::move(simulcast);
 }
 
+std::string SdpSerializer::SerializeRidDescription(
+    const RidDescription& rid_description) const {
+  RTC_DCHECK(!rid_description.rid.empty());
+  RTC_DCHECK(rid_description.direction == RidDirection::kSend ||
+             rid_description.direction == RidDirection::kReceive);
+
+  rtc::StringBuilder builder;
+  builder << rid_description.rid << kDelimiterSpace
+          << (rid_description.direction == RidDirection::kSend
+                  ? kSendDirection
+                  : kReceiveDirection);
+
+  const auto& payload_types = rid_description.payload_types;
+  const auto& restrictions = rid_description.restrictions;
+
+  // First property is separated by ' ', the next ones by ';'.
+  const char* propertyDelimiter = kDelimiterSpace;
+
+  // Serialize any codecs in the description.
+  if (!payload_types.empty()) {
+    builder << propertyDelimiter << kPayloadType << kDelimiterEqual;
+    propertyDelimiter = kDelimiterSemicolon;
+    const char* formatDelimiter = "";
+    for (int payload_type : payload_types) {
+      builder << formatDelimiter << payload_type;
+      formatDelimiter = kDelimiterComma;
+    }
+  }
+
+  // Serialize any restrictions in the description.
+  for (const auto& pair : restrictions) {
+    // Serialize key=val pairs. =val part is ommitted if val is empty.
+    builder << propertyDelimiter << pair.first;
+    if (!pair.second.empty()) {
+      builder << kDelimiterEqual << pair.second;
+    }
+
+    propertyDelimiter = kDelimiterSemicolon;
+  }
+
+  return builder.str();
+}
+
+// https://tools.ietf.org/html/draft-ietf-mmusic-rid-15#section-10
+// Formal Grammar
+// rid-syntax         = %s"a=rid:" rid-id SP rid-dir
+//                      [ rid-pt-param-list / rid-param-list ]
+// rid-id             = 1*(alpha-numeric / "-" / "_")
+// rid-dir            = %s"send" / %s"recv"
+// rid-pt-param-list  = SP rid-fmt-list *( ";" rid-param )
+// rid-param-list     = SP rid-param *( ";" rid-param )
+// rid-fmt-list       = %s"pt=" fmt *( "," fmt )
+// rid-param          = 1*(alpha-numeric / "-") [ "=" param-val ]
+// param-val          = *( %x20-58 / %x60-7E )
+//                      ; Any printable character except semicolon
+RTCErrorOr<RidDescription> SdpSerializer::DeserializeRidDescription(
+    absl::string_view string) const {
+  std::vector<std::string> tokens;
+  rtc::tokenize(std::string(string), kDelimiterSpaceChar, &tokens);
+
+  if (tokens.size() < 2) {
+    return ParseError("RID Description must contain <RID> <direction>.");
+  }
+
+  if (tokens.size() > 3) {
+    return ParseError("Invalid RID Description format. Too many arguments.");
+  }
+
+  if (tokens[1] != kSendDirection && tokens[1] != kReceiveDirection) {
+    return ParseError("Invalid RID direction. Supported values: send / recv.");
+  }
+
+  RidDirection direction = tokens[1] == kSendDirection ? RidDirection::kSend
+                                                       : RidDirection::kReceive;
+
+  RidDescription rid_description(tokens[0], direction);
+
+  // If there is a third argument it is a payload list and/or restriction list.
+  if (tokens.size() == 3) {
+    std::vector<std::string> restrictions;
+    rtc::tokenize(tokens[2], kDelimiterSemicolonChar, &restrictions);
+
+    // Check for malformed restriction list, such as ';' or ';;;' etc.
+    if (restrictions.empty()) {
+      return ParseError("Invalid RID restriction list: " + tokens[2]);
+    }
+
+    // Parse the restrictions. The payload indicator (pt) can only appear first.
+    for (const std::string& restriction : restrictions) {
+      std::vector<std::string> parts;
+      rtc::tokenize(restriction, kDelimiterEqualChar, &parts);
+      if (parts.empty() || parts.size() > 2) {
+        return ParseError("Invalid format for restriction: " + restriction);
+      }
+
+      // |parts| contains at least one value and it does not contain a space.
+      // Note: |parts| and other values might still contain tab, newline,
+      // unprintable characters, etc. which will not generate errors here but
+      // will (most-likely) be ignored by components down stream.
+      if (parts[0] == kPayloadType) {
+        RTCError error = ParseRidPayloadList(
+            parts.size() > 1 ? parts[1] : std::string(), &rid_description);
+        if (!error.ok()) {
+          return std::move(error);
+        }
+
+        continue;
+      }
+
+      // Parse |parts| as a key=value pair which allows unspecified values.
+      if (rid_description.restrictions.find(parts[0]) !=
+          rid_description.restrictions.end()) {
+        return ParseError("Duplicate restriction specified: " + parts[0]);
+      }
+
+      rid_description.restrictions[parts[0]] =
+          parts.size() > 1 ? parts[1] : std::string();
+    }
+  }
+
+  return std::move(rid_description);
+}
+
 }  // namespace webrtc
diff --git a/pc/sdpserializer.h b/pc/sdpserializer.h
index 428b420..3c6f31b 100644
--- a/pc/sdpserializer.h
+++ b/pc/sdpserializer.h
@@ -15,6 +15,7 @@
 
 #include "absl/strings/string_view.h"
 #include "api/rtcerror.h"
+#include "media/base/riddescription.h"
 #include "pc/sessiondescription.h"
 
 namespace webrtc {
@@ -42,6 +43,16 @@
   // https://tools.ietf.org/html/draft-ietf-mmusic-sdp-simulcast-13#section-5.1
   RTCErrorOr<cricket::SimulcastDescription> DeserializeSimulcastDescription(
       absl::string_view string) const;
+
+  // Serialization for the RID description according to
+  // https://tools.ietf.org/html/draft-ietf-mmusic-rid-15#section-10
+  std::string SerializeRidDescription(
+      const cricket::RidDescription& rid_description) const;
+
+  // Deserialization for the RidDescription according to
+  // https://tools.ietf.org/html/draft-ietf-mmusic-rid-15#section-10
+  RTCErrorOr<cricket::RidDescription> DeserializeRidDescription(
+      absl::string_view string) const;
 };
 
 }  // namespace webrtc
diff --git a/pc/sdpserializer_unittest.cc b/pc/sdpserializer_unittest.cc
index 8b10da1..c5e95d0 100644
--- a/pc/sdpserializer_unittest.cc
+++ b/pc/sdpserializer_unittest.cc
@@ -8,27 +8,97 @@
  *  be found in the AUTHORS file in the root of the source tree.
  */
 
+#include <map>
 #include <string>
+#include <utility>
 #include <vector>
 
 #include "pc/sdpserializer.h"
 #include "rtc_base/gunit.h"
 
 using ::testing::ValuesIn;
+using ::testing::TestWithParam;
+using cricket::RidDescription;
+using cricket::RidDirection;
 using cricket::SimulcastDescription;
 using cricket::SimulcastLayer;
 using cricket::SimulcastLayerList;
 
 namespace webrtc {
 
-class SdpSerializerTest : public ::testing::TestWithParam<const char*> {
+namespace {
+// Checks that two vectors have the same objects in the same order.
+template <typename TElement>
+void ExpectEqual(const std::vector<TElement>& expected,
+                 const std::vector<TElement>& actual) {
+  ASSERT_EQ(expected.size(), actual.size());
+  for (size_t i = 0; i < expected.size(); i++) {
+    EXPECT_EQ(expected[i], actual[i]) << "Vectors differ at element " << i;
+  }
+}
+
+// Template specialization for vectors of SimulcastLayer objects.
+template <>
+void ExpectEqual(const std::vector<SimulcastLayer>& expected,
+                 const std::vector<SimulcastLayer>& actual) {
+  EXPECT_EQ(expected.size(), actual.size());
+  for (size_t i = 0; i < expected.size(); i++) {
+    EXPECT_EQ(expected[i].rid, actual[i].rid);
+    EXPECT_EQ(expected[i].is_paused, actual[i].is_paused);
+  }
+}
+
+// Checks that two maps have the same key-value pairs.
+// Even though a map is technically ordered, the order semantics are not
+// tested because having the same key-set in both maps implies that they
+// are ordered the same because the template enforces that they have the
+// same Key-Comparer type.
+template <typename TKey, typename TValue>
+void ExpectEqual(const std::map<TKey, TValue>& expected,
+                 const std::map<TKey, TValue>& actual) {
+  typedef typename std::map<TKey, TValue>::const_iterator const_iterator;
+  ASSERT_EQ(expected.size(), actual.size());
+  // Maps have unique keys, so if size is equal, it is enough to check
+  // that all the keys (and values) from one map exist in the other.
+  for (const std::pair<TKey, TValue>& pair : expected) {
+    const_iterator iter = actual.find(pair.first);
+    EXPECT_NE(iter, actual.end()) << "Key: " << pair.first << " not found";
+    EXPECT_EQ(pair.second, iter->second);
+  }
+}
+
+// Checks that the two SimulcastLayerLists are equal.
+void ExpectEqual(const SimulcastLayerList& expected,
+                 const SimulcastLayerList& actual) {
+  EXPECT_EQ(expected.size(), actual.size());
+  for (size_t i = 0; i < expected.size(); i++) {
+    ExpectEqual(expected[i], actual[i]);
+  }
+}
+
+// Checks that the two SimulcastDescriptions are equal.
+void ExpectEqual(const SimulcastDescription& expected,
+                 const SimulcastDescription& actual) {
+  ExpectEqual(expected.send_layers(), actual.send_layers());
+  ExpectEqual(expected.receive_layers(), actual.receive_layers());
+}
+
+// Checks that the two RidDescriptions are equal.
+void ExpectEqual(const RidDescription& expected, const RidDescription& actual) {
+  EXPECT_EQ(expected.rid, actual.rid);
+  EXPECT_EQ(expected.direction, actual.direction);
+  ExpectEqual(expected.payload_types, actual.payload_types);
+  ExpectEqual(expected.restrictions, actual.restrictions);
+}
+}  // namespace
+
+class SimulcastSdpSerializerTest : public TestWithParam<const char*> {
  public:
   // Runs a test for deserializing Simulcast.
   // |str| - The serialized Simulcast to parse.
   // |expected| - The expected output Simulcast to compare to.
-  void TestSimulcastDeserialization(
-      const std::string& str,
-      const SimulcastDescription& expected) const {
+  void TestDeserialization(const std::string& str,
+                           const SimulcastDescription& expected) const {
     SdpSerializer deserializer;
     auto result = deserializer.DeserializeSimulcastDescription(str);
     EXPECT_TRUE(result.ok());
@@ -38,55 +108,29 @@
   // Runs a test for serializing Simulcast.
   // |simulcast| - The Simulcast to serialize.
   // |expected| - The expected output string to compare to.
-  void TestSimulcastSerialization(const SimulcastDescription& simulcast,
-                                  const std::string& expected) const {
+  void TestSerialization(const SimulcastDescription& simulcast,
+                         const std::string& expected) const {
     SdpSerializer serializer;
     auto result = serializer.SerializeSimulcastDescription(simulcast);
     EXPECT_EQ(expected, result);
   }
-
-  // Checks that the two vectors of SimulcastLayer objects are equal.
-  void ExpectEqual(const std::vector<SimulcastLayer>& expected,
-                   const std::vector<SimulcastLayer>& actual) const {
-    EXPECT_EQ(expected.size(), actual.size());
-    for (size_t i = 0; i < expected.size(); i++) {
-      EXPECT_EQ(expected[i].rid, actual[i].rid);
-      EXPECT_EQ(expected[i].is_paused, actual[i].is_paused);
-    }
-  }
-
-  // Checks that the two SimulcastLayerLists are equal.
-  void ExpectEqual(const SimulcastLayerList& expected,
-                   const SimulcastLayerList& actual) const {
-    EXPECT_EQ(expected.size(), actual.size());
-    for (size_t i = 0; i < expected.size(); i++) {
-      ExpectEqual(expected[i], actual[i]);
-    }
-  }
-
-  // Checks that the two SimulcastDescriptions are equal.
-  void ExpectEqual(const SimulcastDescription& expected,
-                   const SimulcastDescription& actual) const {
-    ExpectEqual(expected.send_layers(), actual.send_layers());
-    ExpectEqual(expected.receive_layers(), actual.receive_layers());
-  }
 };
 
 // Test Cases
 
 // Test simple deserialization with no alternative streams.
-TEST_F(SdpSerializerTest, DeserializeSimulcast_SimpleCaseNoAlternatives) {
+TEST_F(SimulcastSdpSerializerTest, Deserialize_SimpleCaseNoAlternatives) {
   std::string simulcast_str = "send 1;2 recv 3;4";
   SimulcastDescription expected;
   expected.send_layers().AddLayer(SimulcastLayer("1", false));
   expected.send_layers().AddLayer(SimulcastLayer("2", false));
   expected.receive_layers().AddLayer(SimulcastLayer("3", false));
   expected.receive_layers().AddLayer(SimulcastLayer("4", false));
-  TestSimulcastDeserialization(simulcast_str, expected);
+  TestDeserialization(simulcast_str, expected);
 }
 
 // Test simulcast deserialization with alternative streams.
-TEST_F(SdpSerializerTest, DeserializeSimulcast_SimpleCaseWithAlternatives) {
+TEST_F(SimulcastSdpSerializerTest, Deserialize_SimpleCaseWithAlternatives) {
   std::string simulcast_str = "send 1,5;2,6 recv 3,7;4,8";
   SimulcastDescription expected;
   expected.send_layers().AddLayerWithAlternatives(
@@ -97,11 +141,11 @@
       {SimulcastLayer("3", false), SimulcastLayer("7", false)});
   expected.receive_layers().AddLayerWithAlternatives(
       {SimulcastLayer("4", false), SimulcastLayer("8", false)});
-  TestSimulcastDeserialization(simulcast_str, expected);
+  TestDeserialization(simulcast_str, expected);
 }
 
 // Test simulcast deserialization when only some streams have alternatives.
-TEST_F(SdpSerializerTest, DeserializeSimulcast_WithSomeAlternatives) {
+TEST_F(SimulcastSdpSerializerTest, Deserialize_WithSomeAlternatives) {
   std::string simulcast_str = "send 1;2,6 recv 3,7;4";
   SimulcastDescription expected;
   expected.send_layers().AddLayer(SimulcastLayer("1", false));
@@ -110,11 +154,11 @@
   expected.receive_layers().AddLayerWithAlternatives(
       {SimulcastLayer("3", false), SimulcastLayer("7", false)});
   expected.receive_layers().AddLayer(SimulcastLayer("4", false));
-  TestSimulcastDeserialization(simulcast_str, expected);
+  TestDeserialization(simulcast_str, expected);
 }
 
 // Test simulcast deserialization when only send streams are specified.
-TEST_F(SdpSerializerTest, DeserializeSimulcast_OnlySendStreams) {
+TEST_F(SimulcastSdpSerializerTest, Deserialize_OnlySendStreams) {
   std::string simulcast_str = "send 1;2,6;3,7;4";
   SimulcastDescription expected;
   expected.send_layers().AddLayer(SimulcastLayer("1", false));
@@ -123,11 +167,11 @@
   expected.send_layers().AddLayerWithAlternatives(
       {SimulcastLayer("3", false), SimulcastLayer("7", false)});
   expected.send_layers().AddLayer(SimulcastLayer("4", false));
-  TestSimulcastDeserialization(simulcast_str, expected);
+  TestDeserialization(simulcast_str, expected);
 }
 
 // Test simulcast deserialization when only receive streams are specified.
-TEST_F(SdpSerializerTest, DeserializeSimulcast_OnlyReceiveStreams) {
+TEST_F(SimulcastSdpSerializerTest, Deserialize_OnlyReceiveStreams) {
   std::string simulcast_str = "recv 1;2,6;3,7;4";
   SimulcastDescription expected;
   expected.receive_layers().AddLayer(SimulcastLayer("1", false));
@@ -136,11 +180,11 @@
   expected.receive_layers().AddLayerWithAlternatives(
       {SimulcastLayer("3", false), SimulcastLayer("7", false)});
   expected.receive_layers().AddLayer(SimulcastLayer("4", false));
-  TestSimulcastDeserialization(simulcast_str, expected);
+  TestDeserialization(simulcast_str, expected);
 }
 
 // Test simulcast deserialization with receive streams before send streams.
-TEST_F(SdpSerializerTest, DeserializeSimulcast_SendReceiveReversed) {
+TEST_F(SimulcastSdpSerializerTest, Deserialize_SendReceiveReversed) {
   std::string simulcast_str = "recv 1;2,6 send 3,7;4";
   SimulcastDescription expected;
   expected.receive_layers().AddLayer(SimulcastLayer("1", false));
@@ -149,11 +193,11 @@
   expected.send_layers().AddLayerWithAlternatives(
       {SimulcastLayer("3", false), SimulcastLayer("7", false)});
   expected.send_layers().AddLayer(SimulcastLayer("4", false));
-  TestSimulcastDeserialization(simulcast_str, expected);
+  TestDeserialization(simulcast_str, expected);
 }
 
 // Test simulcast deserialization with some streams set to paused state.
-TEST_F(SdpSerializerTest, DeserializeSimulcast_PausedStreams) {
+TEST_F(SimulcastSdpSerializerTest, Deserialize_PausedStreams) {
   std::string simulcast_str = "recv 1;~2,6 send 3,7;~4";
   SimulcastDescription expected;
   expected.receive_layers().AddLayer(SimulcastLayer("1", false));
@@ -162,11 +206,11 @@
   expected.send_layers().AddLayerWithAlternatives(
       {SimulcastLayer("3", false), SimulcastLayer("7", false)});
   expected.send_layers().AddLayer(SimulcastLayer("4", true));
-  TestSimulcastDeserialization(simulcast_str, expected);
+  TestDeserialization(simulcast_str, expected);
 }
 
 // Parameterized negative test case for deserialization with invalid inputs.
-TEST_P(SdpSerializerTest, SimulcastDeserializationFailed) {
+TEST_P(SimulcastSdpSerializerTest, SimulcastDeserializationFailed) {
   SdpSerializer deserializer;
   auto result = deserializer.DeserializeSimulcastDescription(GetParam());
   EXPECT_FALSE(result.ok());
@@ -188,35 +232,35 @@
 };
 
 INSTANTIATE_TEST_CASE_P(SimulcastDeserializationErrors,
-                        SdpSerializerTest,
+                        SimulcastSdpSerializerTest,
                         ValuesIn(kSimulcastMalformedStrings));
 
 // Test a simple serialization scenario.
-TEST_F(SdpSerializerTest, SerializeSimulcast_SimpleCase) {
+TEST_F(SimulcastSdpSerializerTest, Serialize_SimpleCase) {
   SimulcastDescription simulcast;
   simulcast.send_layers().AddLayer(SimulcastLayer("1", false));
   simulcast.receive_layers().AddLayer(SimulcastLayer("2", false));
-  TestSimulcastSerialization(simulcast, "send 1 recv 2");
+  TestSerialization(simulcast, "send 1 recv 2");
 }
 
 // Test serialization with only send streams.
-TEST_F(SdpSerializerTest, SerializeSimulcast_OnlySend) {
+TEST_F(SimulcastSdpSerializerTest, Serialize_OnlySend) {
   SimulcastDescription simulcast;
   simulcast.send_layers().AddLayer(SimulcastLayer("1", false));
   simulcast.send_layers().AddLayer(SimulcastLayer("2", false));
-  TestSimulcastSerialization(simulcast, "send 1;2");
+  TestSerialization(simulcast, "send 1;2");
 }
 
 // Test serialization with only receive streams
-TEST_F(SdpSerializerTest, SerializeSimulcast_OnlyReceive) {
+TEST_F(SimulcastSdpSerializerTest, Serialize_OnlyReceive) {
   SimulcastDescription simulcast;
   simulcast.receive_layers().AddLayer(SimulcastLayer("1", false));
   simulcast.receive_layers().AddLayer(SimulcastLayer("2", false));
-  TestSimulcastSerialization(simulcast, "recv 1;2");
+  TestSerialization(simulcast, "recv 1;2");
 }
 
 // Test a complex serialization with multiple streams, alternatives and states.
-TEST_F(SdpSerializerTest, SerializeSimulcast_ComplexSerialization) {
+TEST_F(SimulcastSdpSerializerTest, Serialize_ComplexSerialization) {
   SimulcastDescription simulcast;
   simulcast.send_layers().AddLayerWithAlternatives(
       {SimulcastLayer("2", false), SimulcastLayer("1", true)});
@@ -229,7 +273,206 @@
   simulcast.receive_layers().AddLayerWithAlternatives(
       {SimulcastLayer("9", false), SimulcastLayer("10", true),
        SimulcastLayer("11", false)});
-  TestSimulcastSerialization(simulcast, "send 2,~1;4,3 recv 6,7;~8;9,~10,11");
+  TestSerialization(simulcast, "send 2,~1;4,3 recv 6,7;~8;9,~10,11");
 }
 
+class RidDescriptionSdpSerializerTest : public TestWithParam<const char*> {
+ public:
+  // Runs a test for deserializing Rid Descriptions.
+  // |str| - The serialized Rid Description to parse.
+  // |expected| - The expected output RidDescription to compare to.
+  void TestDeserialization(const std::string& str,
+                           const RidDescription& expected) const {
+    SdpSerializer deserializer;
+    auto result = deserializer.DeserializeRidDescription(str);
+    EXPECT_TRUE(result.ok());
+    ExpectEqual(expected, result.value());
+  }
+
+  // Runs a test for serializing RidDescriptions.
+  // |rid_description| - The RidDescription to serialize.
+  // |expected| - The expected output string to compare to.
+  void TestSerialization(const RidDescription& rid_description,
+                         const std::string& expected) const {
+    SdpSerializer serializer;
+    auto result = serializer.SerializeRidDescription(rid_description);
+    EXPECT_EQ(expected, result);
+  }
+};
+
+// Test serialization for RidDescription that only specifies send.
+TEST_F(RidDescriptionSdpSerializerTest, Serialize_OnlyDirectionSend) {
+  RidDescription rid_description("1", RidDirection::kSend);
+  TestSerialization(rid_description, "1 send");
+}
+
+// Test serialization for RidDescription that only specifies receive.
+TEST_F(RidDescriptionSdpSerializerTest, Serialize_OnlyDirectionReceive) {
+  RidDescription rid_description("2", RidDirection::kReceive);
+  TestSerialization(rid_description, "2 recv");
+}
+
+// Test serialization for RidDescription with format list.
+TEST_F(RidDescriptionSdpSerializerTest, Serialize_FormatList) {
+  RidDescription rid_description("3", RidDirection::kSend);
+  rid_description.payload_types = {102, 101};
+  TestSerialization(rid_description, "3 send pt=102,101");
+}
+
+// Test serialization for RidDescription with format list.
+TEST_F(RidDescriptionSdpSerializerTest, Serialize_FormatListSingleFormat) {
+  RidDescription rid_description("4", RidDirection::kReceive);
+  rid_description.payload_types = {100};
+  TestSerialization(rid_description, "4 recv pt=100");
+}
+
+// Test serialization for RidDescription with restriction list.
+// Note: restriction list will be sorted because it is stored in a map.
+TEST_F(RidDescriptionSdpSerializerTest, Serialize_AttributeList) {
+  RidDescription rid_description("5", RidDirection::kSend);
+  rid_description.restrictions["max-width"] = "1280";
+  rid_description.restrictions["max-height"] = "720";
+  TestSerialization(rid_description, "5 send max-height=720;max-width=1280");
+}
+
+// Test serialization for RidDescription with format list and attribute list.
+// Note: restriction list will be sorted because it is stored in a map.
+TEST_F(RidDescriptionSdpSerializerTest, Serialize_FormatAndAttributeList) {
+  RidDescription rid_description("6", RidDirection::kSend);
+  rid_description.payload_types = {103, 104};
+  rid_description.restrictions["max-mbps"] = "108000";
+  rid_description.restrictions["max-br"] = "64000";
+  TestSerialization(rid_description,
+                    "6 send pt=103,104;max-br=64000;max-mbps=108000");
+}
+
+// Test serialization for attribute list that has key with no value.
+// Note: restriction list will be sorted because it is stored in a map.
+TEST_F(RidDescriptionSdpSerializerTest, Serialize_RestrictionWithoutValue) {
+  RidDescription rid_description("7", RidDirection::kReceive);
+  rid_description.payload_types = {103};
+  rid_description.restrictions["max-width"] = "1280";
+  rid_description.restrictions["max-height"] = "720";
+  rid_description.restrictions["max-myval"] = "";
+  TestSerialization(rid_description,
+                    "7 recv pt=103;max-height=720;max-myval;max-width=1280");
+}
+
+// Test simulcast deserialization with simple send stream.
+TEST_F(RidDescriptionSdpSerializerTest, Deserialize_SimpleSendCase) {
+  RidDescription rid_description("1", RidDirection::kSend);
+  TestDeserialization("1 send", rid_description);
+}
+
+// Test simulcast deserialization with simple receive stream.
+TEST_F(RidDescriptionSdpSerializerTest, Deserialize_SimpleReceiveCase) {
+  RidDescription rid_description("2", RidDirection::kReceive);
+  TestDeserialization("2 recv", rid_description);
+}
+
+// Test simulcast deserialization with single format.
+TEST_F(RidDescriptionSdpSerializerTest, Deserialize_WithFormat) {
+  RidDescription rid_description("3", RidDirection::kSend);
+  rid_description.payload_types = {101};
+  TestDeserialization("3 send pt=101", rid_description);
+}
+
+// Test simulcast deserialization with multiple formats.
+TEST_F(RidDescriptionSdpSerializerTest, Deserialize_WithMultipleFormats) {
+  RidDescription rid_description("4", RidDirection::kSend);
+  rid_description.payload_types = {103, 104, 101, 102};
+  TestDeserialization("4 send pt=103,104,101,102", rid_description);
+}
+
+// Test simulcast deserialization with restriction.
+TEST_F(RidDescriptionSdpSerializerTest, Deserialize_WithRestriction) {
+  RidDescription rid_description("5", RidDirection::kReceive);
+  rid_description.restrictions["max-height"] = "720";
+  TestDeserialization("5 recv max-height=720", rid_description);
+}
+
+// Test simulcast deserialization with multiple restrictions.
+TEST_F(RidDescriptionSdpSerializerTest, Deserialize_WithMultipleRestrictions) {
+  RidDescription rid_description("6", RidDirection::kReceive);
+  rid_description.restrictions["max-height"] = "720";
+  rid_description.restrictions["max-width"] = "1920";
+  rid_description.restrictions["max-fr"] = "60";
+  rid_description.restrictions["max-bps"] = "14000";
+  TestDeserialization(
+      "6 recv max-height=720;max-width=1920;max-bps=14000;max-fr=60",
+      rid_description);
+}
+
+// Test simulcast deserialization with custom (non-standard) restriction.
+TEST_F(RidDescriptionSdpSerializerTest, Deserialize_WithCustomRestrictions) {
+  RidDescription rid_description("7", RidDirection::kSend);
+  rid_description.restrictions["foo"] = "bar";
+  rid_description.restrictions["max-height"] = "720";
+  TestDeserialization("7 send max-height=720;foo=bar", rid_description);
+}
+
+// Test simulcast deserialization with multiple formats and restrictions.
+TEST_F(RidDescriptionSdpSerializerTest, Deserialize_WithFormatAndRestrictions) {
+  RidDescription rid_description("8", RidDirection::kSend);
+  rid_description.payload_types = {104, 103};
+  rid_description.restrictions["max-height"] = "720";
+  rid_description.restrictions["max-width"] = "1920";
+  TestDeserialization("8 send pt=104,103;max-height=720;max-width=1920",
+                      rid_description);
+}
+
+// Test simulcast deserialization with restriction that has no value.
+TEST_F(RidDescriptionSdpSerializerTest, Deserialize_RestrictionHasNoValue) {
+  RidDescription rid_description("9", RidDirection::kReceive);
+  rid_description.payload_types = {104};
+  rid_description.restrictions["max-height"];
+  rid_description.restrictions["max-width"] = "1920";
+  TestDeserialization("9 recv pt=104;max-height;max-width=1920",
+                      rid_description);
+}
+
+// Add this test to explicitly indicate that this is not an error.
+// The following string "1 send recv" looks malformed because it specifies
+// two directions, but in fact, the recv can be interpreted as a parameter
+// without a value. While such a use case is dubious, the input string is
+// not malformed.
+TEST_F(RidDescriptionSdpSerializerTest, Deserialize_AmbiguousCase) {
+  RidDescription rid_description("1", RidDirection::kSend);
+  rid_description.restrictions["recv"];  // No value.
+  TestDeserialization("1 send recv", rid_description);
+}
+
+// Parameterized negative test case for deserialization with invalid inputs.
+TEST_P(RidDescriptionSdpSerializerTest, RidDescriptionDeserializationFailed) {
+  SdpSerializer deserializer;
+  auto result = deserializer.DeserializeRidDescription(GetParam());
+  EXPECT_FALSE(result.ok());
+}
+
+// The malformed Rid Description inputs to use in the negative test case.
+const char* kRidDescriptionMalformedStrings[] = {
+    "1",
+    "recv",
+    "send",
+    "recv 1",
+    "send 1",
+    "1 receive",
+    "one direction",
+    "1 send pt=1 max-width=720",  // The ' ' should be ';' in restriction list.
+    "1 recv ;",
+    "1 recv =",
+    "1 recv a=b=c",
+    "1 send max-width=720;pt=101",  // pt= should appear first.
+    "1 send pt=101;pt=102",
+    "1 send pt=101,101",
+    "1 recv max-width=720;max-width=720",
+    "1 send pt=",
+    "1 send pt=abc",
+    "1 recv ;;",
+};
+
+INSTANTIATE_TEST_CASE_P(RidDescriptionDeserializationErrors,
+                        RidDescriptionSdpSerializerTest,
+                        ValuesIn(kRidDescriptionMalformedStrings));
+
 }  // namespace webrtc
diff --git a/pc/sessiondescription.h b/pc/sessiondescription.h
index ff6cd2f..ceb9fd7 100644
--- a/pc/sessiondescription.h
+++ b/pc/sessiondescription.h
@@ -140,39 +140,56 @@
   // provide the ClearRtpHeaderExtensions method to allow "no support" to be
   // clearly indicated (i.e. when derived from other information).
   bool rtp_header_extensions_set() const { return rtp_header_extensions_set_; }
-  const StreamParamsVec& streams() const { return streams_; }
+  const StreamParamsVec& streams() const { return send_streams_; }
   // TODO(pthatcher): Remove this by giving mediamessage.cc access
   // to MediaContentDescription
-  StreamParamsVec& mutable_streams() { return streams_; }
-  void AddStream(const StreamParams& stream) { streams_.push_back(stream); }
+  StreamParamsVec& mutable_streams() { return send_streams_; }
+  void AddStream(const StreamParams& stream) {
+    send_streams_.push_back(stream);
+  }
   // Legacy streams have an ssrc, but nothing else.
   void AddLegacyStream(uint32_t ssrc) {
-    streams_.push_back(StreamParams::CreateLegacy(ssrc));
+    send_streams_.push_back(StreamParams::CreateLegacy(ssrc));
   }
   void AddLegacyStream(uint32_t ssrc, uint32_t fid_ssrc) {
     StreamParams sp = StreamParams::CreateLegacy(ssrc);
     sp.AddFidSsrc(ssrc, fid_ssrc);
-    streams_.push_back(sp);
+    send_streams_.push_back(sp);
   }
+
+  // In Unified Plan (ex. Simulcast scenario) the receive stream might need
+  // to be specified in the media section description to allow specifying
+  // restrictions and identifying it within the session (see also RID).
+  const StreamParams& receive_stream() const {
+    RTC_DCHECK(has_receive_stream());
+    return receive_stream_.value();
+  }
+
+  bool has_receive_stream() const { return receive_stream_.has_value(); }
+
+  void set_receive_stream(const StreamParams& receive_stream) {
+    receive_stream_ = receive_stream;
+  }
+
   // Sets the CNAME of all StreamParams if it have not been set.
   void SetCnameIfEmpty(const std::string& cname) {
-    for (cricket::StreamParamsVec::iterator it = streams_.begin();
-         it != streams_.end(); ++it) {
+    for (cricket::StreamParamsVec::iterator it = send_streams_.begin();
+         it != send_streams_.end(); ++it) {
       if (it->cname.empty())
         it->cname = cname;
     }
   }
   uint32_t first_ssrc() const {
-    if (streams_.empty()) {
+    if (send_streams_.empty()) {
       return 0;
     }
-    return streams_[0].first_ssrc();
+    return send_streams_[0].first_ssrc();
   }
   bool has_ssrcs() const {
-    if (streams_.empty()) {
+    if (send_streams_.empty()) {
       return false;
     }
-    return streams_[0].has_ssrcs();
+    return send_streams_[0].has_ssrcs();
   }
 
   void set_conference_mode(bool enable) { conference_mode_ = enable; }
@@ -223,7 +240,8 @@
   std::vector<CryptoParams> cryptos_;
   std::vector<webrtc::RtpExtension> rtp_header_extensions_;
   bool rtp_header_extensions_set_ = false;
-  StreamParamsVec streams_;
+  StreamParamsVec send_streams_;
+  absl::optional<StreamParams> receive_stream_;
   bool conference_mode_ = false;
   webrtc::RtpTransceiverDirection direction_ =
       webrtc::RtpTransceiverDirection::kSendRecv;
diff --git a/pc/simulcastdescription.cc b/pc/simulcastdescription.cc
index eca67a7..8fff520 100644
--- a/pc/simulcastdescription.cc
+++ b/pc/simulcastdescription.cc
@@ -17,7 +17,6 @@
 
 SimulcastLayer::SimulcastLayer(const std::string& rid, bool is_paused)
     : rid{rid}, is_paused{is_paused} {
-  // TODO(amithi, bugs.webrtc.org/10073): Validate rid format.
   RTC_DCHECK(!rid.empty());
 }
 
@@ -41,4 +40,15 @@
   return send_layers_.empty() && receive_layers_.empty();
 }
 
+std::vector<SimulcastLayer> SimulcastLayerList::GetAllLayers() const {
+  std::vector<SimulcastLayer> result;
+  for (auto groupIt = begin(); groupIt != end(); groupIt++) {
+    for (auto it = groupIt->begin(); it != groupIt->end(); it++) {
+      result.push_back(*it);
+    }
+  }
+
+  return result;
+}
+
 }  // namespace cricket
diff --git a/pc/simulcastdescription.h b/pc/simulcastdescription.h
index f4708ff..9f15f78 100644
--- a/pc/simulcastdescription.h
+++ b/pc/simulcastdescription.h
@@ -70,6 +70,10 @@
   size_t size() const { return list_.size(); }
   bool empty() const { return list_.empty(); }
 
+  // Provides access to all the layers in the simulcast without their
+  // association into groups of alternatives.
+  std::vector<SimulcastLayer> GetAllLayers() const;
+
  private:
   // TODO(amithi, bugs.webrtc.org/10075):
   // Validate that rids do not repeat in the list.
diff --git a/pc/webrtcsdp.cc b/pc/webrtcsdp.cc
index 37d7654..5a1b2f8 100644
--- a/pc/webrtcsdp.cc
+++ b/pc/webrtcsdp.cc
@@ -74,7 +74,10 @@
 using cricket::MediaType;
 using cricket::RtpHeaderExtensions;
 using cricket::MediaProtocolType;
+using cricket::RidDescription;
 using cricket::SimulcastDescription;
+using cricket::SimulcastLayer;
+using cricket::SimulcastLayerList;
 using cricket::SsrcGroup;
 using cricket::StreamParams;
 using cricket::StreamParamsVec;
@@ -167,6 +170,9 @@
 // draft-ietf-mmusic-sdp-simulcast-13
 // a=simulcast
 static const char kAttributeSimulcast[] = "simulcast";
+// draft-ietf-mmusic-rid-15
+// a=rid
+static const char kAttributeRid[] = "rid";
 
 // Experimental flags
 static const char kAttributeXGoogleFlag[] = "x-google-flag";
@@ -354,6 +360,17 @@
                                std::string* track_id,
                                SdpParseError* error);
 
+static void RemoveInvalidRidDescriptions(const std::vector<int>& payload_types,
+                                         std::vector<RidDescription>* rids);
+
+static SimulcastLayerList RemoveRidsFromSimulcastLayerList(
+    const std::set<std::string>& to_remove,
+    const SimulcastLayerList& layers);
+
+static void RemoveInvalidRidsFromSimulcast(
+    const std::vector<RidDescription>& rids,
+    SimulcastDescription* simulcast);
+
 // Helper functions
 
 // Below ParseFailed*** functions output the line that caused the parsing
@@ -628,6 +645,7 @@
 // stream_ids/track_id if it's signaled with a=msid lines.
 void CreateTrackWithNoSsrcs(const std::vector<std::string>& msid_stream_ids,
                             const std::string& msid_track_id,
+                            const std::vector<RidDescription>& rids,
                             StreamParamsVec* tracks) {
   StreamParams track;
   if (msid_track_id.empty() || msid_stream_ids.empty()) {
@@ -636,6 +654,7 @@
   }
   track.set_stream_ids(msid_stream_ids);
   track.id = msid_track_id;
+  track.set_rids(rids);
   tracks->push_back(track);
 }
 
@@ -1496,6 +1515,7 @@
                                const cricket::MediaType media_type,
                                int msid_signaling,
                                std::string* message) {
+  SdpSerializer serializer;
   rtc::StringBuilder os;
   // RFC 8285
   // a=extmap-allow-mixed
@@ -1656,14 +1676,31 @@
         AddSsrcLine(ssrc, kSSrcAttributeLabel, track.id, message);
       }
     }
+
+    // Build the rid lines for each layer of the track
+    for (const RidDescription& rid_description : track.rids()) {
+      InitAttrLine(kAttributeRid, &os);
+      os << kSdpDelimiterColon
+         << serializer.SerializeRidDescription(rid_description);
+      AddLine(os.str(), message);
+    }
+  }
+
+  if (media_desc->has_receive_stream()) {
+    const auto& rids = media_desc->receive_stream().rids();
+    for (const RidDescription& rid_description : rids) {
+      InitAttrLine(kAttributeRid, &os);
+      os << kSdpDelimiterColon
+         << serializer.SerializeRidDescription(rid_description);
+      AddLine(os.str(), message);
+    }
   }
 
   // Simulcast (a=simulcast)
   // https://tools.ietf.org/html/draft-ietf-mmusic-sdp-simulcast-13#section-5.1
-  if (media_desc->as_video() && media_desc->as_video()->HasSimulcast()) {
-    const auto& simulcast = media_desc->as_video()->simulcast_description();
+  if (media_desc->HasSimulcast()) {
+    const auto& simulcast = media_desc->simulcast_description();
     InitAttrLine(kAttributeSimulcast, &os);
-    SdpSerializer serializer;
     os << kSdpDelimiterColon
        << serializer.SerializeSimulcastDescription(simulcast);
     AddLine(os.str(), message);
@@ -2309,6 +2346,143 @@
   return true;
 }
 
+static void RemoveInvalidRidDescriptions(const std::vector<int>& payload_types,
+                                         std::vector<RidDescription>* rids) {
+  RTC_DCHECK(rids);
+  std::set<std::string> to_remove;
+  std::set<std::string> unique_rids;
+
+  // Check the rids to see which ones should be removed.
+  for (RidDescription& rid : *rids) {
+    // In the case of a duplicate, the entire "a=rid" line, and all "a=rid"
+    // lines with rid-ids that duplicate this line, are discarded and MUST NOT
+    // be included in the SDP Answer.
+    auto pair = unique_rids.insert(rid.rid);
+    // Insert will "fail" if element already exists.
+    if (!pair.second) {
+      to_remove.insert(rid.rid);
+      continue;
+    }
+
+    // If the "a=rid" line contains a "pt=", the list of payload types
+    // is verified against the list of valid payload types for the media
+    // section (that is, those listed on the "m=" line).  Any PT missing
+    // from the "m=" line is discarded from the set of values in the
+    // "pt=".  If no values are left in the "pt=" parameter after this
+    // processing, then the "a=rid" line is discarded.
+    if (rid.payload_types.empty()) {
+      // If formats were not specified, rid should not be removed.
+      continue;
+    }
+
+    // Note: Spec does not mention how to handle duplicate formats.
+    // Media section does not handle duplicates either.
+    std::set<int> removed_formats;
+    for (int payload_type : rid.payload_types) {
+      if (std::find(payload_types.begin(), payload_types.end(), payload_type) ==
+          payload_types.end()) {
+        removed_formats.insert(payload_type);
+      }
+    }
+
+    rid.payload_types.erase(
+        std::remove_if(rid.payload_types.begin(), rid.payload_types.end(),
+                       [&removed_formats](int format) {
+                         return removed_formats.count(format) > 0;
+                       }),
+        rid.payload_types.end());
+
+    // If all formats were removed then remove the rid alogether.
+    if (rid.payload_types.empty()) {
+      to_remove.insert(rid.rid);
+    }
+  }
+
+  // Remove every rid description that appears in the to_remove list.
+  if (!to_remove.empty()) {
+    rids->erase(std::remove_if(rids->begin(), rids->end(),
+                               [&to_remove](const RidDescription& rid) {
+                                 return to_remove.count(rid.rid) > 0;
+                               }),
+                rids->end());
+  }
+}
+
+// Create a new list (because SimulcastLayerList is immutable) without any
+// layers that have a rid in the to_remove list.
+// If a group of alternatives is empty after removing layers, the group should
+// be removed altogether.
+static SimulcastLayerList RemoveRidsFromSimulcastLayerList(
+    const std::set<std::string>& to_remove,
+    const SimulcastLayerList& layers) {
+  SimulcastLayerList result;
+  for (const std::vector<SimulcastLayer>& vector : layers) {
+    std::vector<SimulcastLayer> new_layers;
+    for (const SimulcastLayer& layer : vector) {
+      if (to_remove.find(layer.rid) == to_remove.end()) {
+        new_layers.push_back(layer);
+      }
+    }
+    // If all layers were removed, do not add an entry.
+    if (!new_layers.empty()) {
+      result.AddLayerWithAlternatives(new_layers);
+    }
+  }
+
+  return result;
+}
+
+// Will remove Simulcast Layers if:
+// 1. They appear in both send and receive directions.
+// 2. They do not appear in the list of |valid_rids|.
+static void RemoveInvalidRidsFromSimulcast(
+    const std::vector<RidDescription>& valid_rids,
+    SimulcastDescription* simulcast) {
+  RTC_DCHECK(simulcast);
+  std::set<std::string> to_remove;
+  std::vector<SimulcastLayer> all_send_layers =
+      simulcast->send_layers().GetAllLayers();
+  std::vector<SimulcastLayer> all_receive_layers =
+      simulcast->receive_layers().GetAllLayers();
+
+  // If a rid appears in both send and receive directions, remove it from both.
+  // This algorithm runs in O(n^2) time, but for small n (as is the case with
+  // simulcast layers) it should still perform well.
+  for (const SimulcastLayer& send_layer : all_send_layers) {
+    if (std::find_if(all_receive_layers.begin(), all_receive_layers.end(),
+                     [&send_layer](const SimulcastLayer& layer) {
+                       return layer.rid == send_layer.rid;
+                     }) != all_receive_layers.end()) {
+      to_remove.insert(send_layer.rid);
+    }
+  }
+
+  // Add any rid that is not in the valid list to the remove set.
+  for (const SimulcastLayer& send_layer : all_send_layers) {
+    if (std::find_if(valid_rids.begin(), valid_rids.end(),
+                     [&send_layer](const RidDescription& rid) {
+                       return send_layer.rid == rid.rid;
+                     }) == valid_rids.end()) {
+      to_remove.insert(send_layer.rid);
+    }
+  }
+
+  // Add any rid that is not in the valid list to the remove set.
+  for (const SimulcastLayer& receive_layer : all_receive_layers) {
+    if (std::find_if(valid_rids.begin(), valid_rids.end(),
+                     [&receive_layer](const RidDescription& rid) {
+                       return receive_layer.rid == rid.rid;
+                     }) == valid_rids.end()) {
+      to_remove.insert(receive_layer.rid);
+    }
+  }
+
+  simulcast->send_layers() =
+      RemoveRidsFromSimulcastLayerList(to_remove, simulcast->send_layers());
+  simulcast->receive_layers() =
+      RemoveRidsFromSimulcastLayerList(to_remove, simulcast->receive_layers());
+}
+
 // RFC 3551
 //  PT   encoding    media type  clock rate   channels
 //                      name                    (Hz)
@@ -2766,6 +2940,8 @@
   std::string ptime_as_string;
   std::vector<std::string> stream_ids;
   std::string track_id;
+  SdpSerializer deserializer;
+  std::vector<RidDescription> rids;
   SimulcastDescription simulcast;
 
   // Loop until the next m line
@@ -2971,6 +3147,26 @@
           return false;
         }
         *msid_signaling |= cricket::kMsidSignalingMediaSection;
+      } else if (HasAttribute(line, kAttributeRid)) {
+        const size_t kRidPrefixLength =
+            kLinePrefixLength + arraysize(kAttributeRid);
+        if (line.size() <= kRidPrefixLength) {
+          RTC_LOG(LS_INFO) << "Ignoring empty RID attribute: " << line;
+          continue;
+        }
+        RTCErrorOr<RidDescription> error_or_rid_description =
+            deserializer.DeserializeRidDescription(
+                line.substr(kRidPrefixLength));
+
+        // Malformed a=rid lines are discarded.
+        if (!error_or_rid_description.ok()) {
+          RTC_LOG(LS_INFO) << "Ignoring malformed RID line: '" << line
+                           << "'. Error: "
+                           << error_or_rid_description.error().message();
+          continue;
+        }
+
+        rids.push_back(error_or_rid_description.MoveValue());
       } else if (HasAttribute(line, kAttributeSimulcast)) {
         const size_t kSimulcastPrefixLength =
             kLinePrefixLength + arraysize(kAttributeSimulcast);
@@ -2983,7 +3179,6 @@
                              error);
         }
 
-        SdpSerializer deserializer;
         RTCErrorOr<SimulcastDescription> error_or_simulcast =
             deserializer.DeserializeSimulcastDescription(
                 line.substr(kSimulcastPrefixLength));
@@ -3007,6 +3202,40 @@
     }
   }
 
+  // Remove duplicate or inconsistent rids.
+  RemoveInvalidRidDescriptions(payload_types, &rids);
+
+  // If simulcast is specifed, split the rids into send and receive.
+  // Rids that do not appear in simulcast attribute will be removed.
+  // If it is not specified, we assume that all rids are for send layers.
+  std::vector<RidDescription> send_rids, receive_rids;
+  if (!simulcast.empty()) {
+    // Verify that the rids in simulcast match rids in sdp.
+    RemoveInvalidRidsFromSimulcast(rids, &simulcast);
+
+    // Use simulcast description to figure out Send / Receive RIDs.
+    std::map<std::string, RidDescription> rid_map;
+    for (const RidDescription& rid : rids) {
+      rid_map[rid.rid] = rid;
+    }
+
+    for (const auto& layer : simulcast.send_layers().GetAllLayers()) {
+      auto iter = rid_map.find(layer.rid);
+      RTC_DCHECK(iter != rid_map.end());
+      send_rids.push_back(iter->second);
+    }
+
+    for (const auto& layer : simulcast.receive_layers().GetAllLayers()) {
+      auto iter = rid_map.find(layer.rid);
+      RTC_DCHECK(iter != rid_map.end());
+      receive_rids.push_back(iter->second);
+    }
+
+    media_desc->set_simulcast_description(simulcast);
+  } else {
+    send_rids = rids;
+  }
+
   // Create tracks from the |ssrc_infos|.
   // If the stream_id/track_id for all SSRCS are identical, one StreamParams
   // will be created in CreateTracksFromSsrcInfos, containing all the SSRCs from
@@ -3020,7 +3249,14 @@
     // still create a track. This isn't done for data media types because
     // StreamParams aren't used for SCTP streams, and RTP data channels don't
     // support unsignaled SSRCs.
-    CreateTrackWithNoSsrcs(stream_ids, track_id, &tracks);
+    CreateTrackWithNoSsrcs(stream_ids, track_id, send_rids, &tracks);
+  }
+
+  // Create receive track when we have incoming receive rids.
+  if (!receive_rids.empty()) {
+    StreamParams receive_track;
+    receive_track.set_rids(receive_rids);
+    media_desc->set_receive_stream(receive_track);
   }
 
   // Add the ssrc group to the track.
@@ -3076,12 +3312,6 @@
         new JsepIceCandidate(mline_id, mline_index, candidate));
   }
 
-  if (!simulcast.empty()) {
-    // TODO(amithi, bugs.webrtc.org/10073):
-    // Verify that the rids in simulcast match rids in sdp.
-    media_desc->set_simulcast_description(simulcast);
-  }
-
   return true;
 }
 
diff --git a/pc/webrtcsdp_unittest.cc b/pc/webrtcsdp_unittest.cc
index 8a3c28f..0ecba4c 100644
--- a/pc/webrtcsdp_unittest.cc
+++ b/pc/webrtcsdp_unittest.cc
@@ -60,6 +60,8 @@
 using cricket::RELAY_PORT_TYPE;
 using cricket::SessionDescription;
 using cricket::MediaProtocolType;
+using cricket::RidDescription;
+using cricket::RidDirection;
 using cricket::SimulcastDescription;
 using cricket::SimulcastLayer;
 using cricket::StreamParams;
@@ -697,6 +699,105 @@
     "a=ssrc:7 mslabel:-\r\n"
     "a=ssrc:7 label:audio_track_id_3\r\n";
 
+// SDP string for unified plan without SSRCs
+static const char kUnifiedPlanSdpFullStringNoSsrc[] =
+    "v=0\r\n"
+    "o=- 18446744069414584320 18446462598732840960 IN IP4 127.0.0.1\r\n"
+    "s=-\r\n"
+    "t=0 0\r\n"
+    "a=msid-semantic: WMS local_stream_1\r\n"
+    // Audio track 1, stream 1 (with candidates).
+    "m=audio 2345 RTP/SAVPF 111 103 104\r\n"
+    "c=IN IP4 74.125.127.126\r\n"
+    "a=rtcp:2347 IN IP4 74.125.127.126\r\n"
+    "a=candidate:a0+B/1 1 udp 2130706432 192.168.1.5 1234 typ host "
+    "generation 2\r\n"
+    "a=candidate:a0+B/1 2 udp 2130706432 192.168.1.5 1235 typ host "
+    "generation 2\r\n"
+    "a=candidate:a0+B/2 1 udp 2130706432 ::1 1238 typ host "
+    "generation 2\r\n"
+    "a=candidate:a0+B/2 2 udp 2130706432 ::1 1239 typ host "
+    "generation 2\r\n"
+    "a=candidate:a0+B/3 1 udp 2130706432 74.125.127.126 2345 typ srflx "
+    "raddr 192.168.1.5 rport 2346 "
+    "generation 2\r\n"
+    "a=candidate:a0+B/3 2 udp 2130706432 74.125.127.126 2347 typ srflx "
+    "raddr 192.168.1.5 rport 2348 "
+    "generation 2\r\n"
+    "a=ice-ufrag:ufrag_voice\r\na=ice-pwd:pwd_voice\r\n"
+    "a=mid:audio_content_name\r\n"
+    "a=msid:local_stream_1 audio_track_id_1\r\n"
+    "a=sendrecv\r\n"
+    "a=rtcp-mux\r\n"
+    "a=rtcp-rsize\r\n"
+    "a=crypto:1 AES_CM_128_HMAC_SHA1_32 "
+    "inline:NzB4d1BINUAvLEw6UzF3WSJ+PSdFcGdUJShpX1Zj|2^20|1:32 "
+    "dummy_session_params\r\n"
+    "a=rtpmap:111 opus/48000/2\r\n"
+    "a=rtpmap:103 ISAC/16000\r\n"
+    "a=rtpmap:104 ISAC/32000\r\n"
+    // Video track 1, stream 1 (with candidates).
+    "m=video 3457 RTP/SAVPF 120\r\n"
+    "c=IN IP4 74.125.224.39\r\n"
+    "a=rtcp:3456 IN IP4 74.125.224.39\r\n"
+    "a=candidate:a0+B/1 2 udp 2130706432 192.168.1.5 1236 typ host "
+    "generation 2\r\n"
+    "a=candidate:a0+B/1 1 udp 2130706432 192.168.1.5 1237 typ host "
+    "generation 2\r\n"
+    "a=candidate:a0+B/2 2 udp 2130706432 ::1 1240 typ host "
+    "generation 2\r\n"
+    "a=candidate:a0+B/2 1 udp 2130706432 ::1 1241 typ host "
+    "generation 2\r\n"
+    "a=candidate:a0+B/4 2 udp 2130706432 74.125.224.39 3456 typ relay "
+    "generation 2\r\n"
+    "a=candidate:a0+B/4 1 udp 2130706432 74.125.224.39 3457 typ relay "
+    "generation 2\r\n"
+    "a=ice-ufrag:ufrag_video\r\na=ice-pwd:pwd_video\r\n"
+    "a=mid:video_content_name\r\n"
+    "a=msid:local_stream_1 video_track_id_1\r\n"
+    "a=sendrecv\r\n"
+    "a=crypto:1 AES_CM_128_HMAC_SHA1_80 "
+    "inline:d0RmdmcmVCspeEc3QGZiNWpVLFJhQX1cfHAwJSoj|2^20|1:32\r\n"
+    "a=rtpmap:120 VP8/90000\r\n"
+    // Audio track 2, stream 2.
+    "m=audio 9 RTP/SAVPF 111 103 104\r\n"
+    "c=IN IP4 0.0.0.0\r\n"
+    "a=rtcp:9 IN IP4 0.0.0.0\r\n"
+    "a=ice-ufrag:ufrag_voice_2\r\na=ice-pwd:pwd_voice_2\r\n"
+    "a=mid:audio_content_name_2\r\n"
+    "a=msid:local_stream_2 audio_track_id_2\r\n"
+    "a=sendrecv\r\n"
+    "a=rtcp-mux\r\n"
+    "a=rtcp-rsize\r\n"
+    "a=crypto:1 AES_CM_128_HMAC_SHA1_32 "
+    "inline:NzB4d1BINUAvLEw6UzF3WSJ+PSdFcGdUJShpX1Zj|2^20|1:32 "
+    "dummy_session_params\r\n"
+    "a=rtpmap:111 opus/48000/2\r\n"
+    "a=rtpmap:103 ISAC/16000\r\n"
+    "a=rtpmap:104 ISAC/32000\r\n"
+    // Video track 2, stream 2.
+    "m=video 9 RTP/SAVPF 120\r\n"
+    "c=IN IP4 0.0.0.0\r\n"
+    "a=rtcp:9 IN IP4 0.0.0.0\r\n"
+    "a=ice-ufrag:ufrag_video_2\r\na=ice-pwd:pwd_video_2\r\n"
+    "a=mid:video_content_name_2\r\n"
+    "a=msid:local_stream_2 video_track_id_2\r\n"
+    "a=sendrecv\r\n"
+    "a=crypto:1 AES_CM_128_HMAC_SHA1_80 "
+    "inline:d0RmdmcmVCspeEc3QGZiNWpVLFJhQX1cfHAwJSoj|2^20|1:32\r\n"
+    "a=rtpmap:120 VP8/90000\r\n"
+    // Video track 3, stream 2.
+    "m=video 9 RTP/SAVPF 120\r\n"
+    "c=IN IP4 0.0.0.0\r\n"
+    "a=rtcp:9 IN IP4 0.0.0.0\r\n"
+    "a=ice-ufrag:ufrag_video_3\r\na=ice-pwd:pwd_video_3\r\n"
+    "a=mid:video_content_name_3\r\n"
+    "a=msid:local_stream_2 video_track_id_3\r\n"
+    "a=sendrecv\r\n"
+    "a=crypto:1 AES_CM_128_HMAC_SHA1_80 "
+    "inline:d0RmdmcmVCspeEc3QGZiNWpVLFJhQX1cfHAwJSoj|2^20|1:32\r\n"
+    "a=rtpmap:120 VP8/90000\r\n";
+
 // One candidate reference string as per W3c spec.
 // candidate:<blah> not a=candidate:<blah>CRLF
 static const char kRawCandidate[] =
@@ -1118,14 +1219,16 @@
 
   // Turns the existing reference description into a unified plan description,
   // with 2 audio tracks and 3 video tracks.
-  void MakeUnifiedPlanDescription() {
+  void MakeUnifiedPlanDescription(bool use_ssrcs = true) {
     // Audio track 2.
     AudioContentDescription* audio_desc_2 = CreateAudioContentDescription();
     StreamParams audio_track_2;
     audio_track_2.id = kAudioTrackId2;
-    audio_track_2.cname = kStream2Cname;
     audio_track_2.set_stream_ids({kStreamId2});
-    audio_track_2.ssrcs.push_back(kAudioTrack2Ssrc);
+    if (use_ssrcs) {
+      audio_track_2.cname = kStream2Cname;
+      audio_track_2.ssrcs.push_back(kAudioTrack2Ssrc);
+    }
     audio_desc_2->AddStream(audio_track_2);
     desc_.AddContent(kAudioContentName2, MediaProtocolType::kRtp, audio_desc_2);
     EXPECT_TRUE(desc_.AddTransportInfo(TransportInfo(
@@ -1134,9 +1237,11 @@
     VideoContentDescription* video_desc_2 = CreateVideoContentDescription();
     StreamParams video_track_2;
     video_track_2.id = kVideoTrackId2;
-    video_track_2.cname = kStream2Cname;
     video_track_2.set_stream_ids({kStreamId2});
-    video_track_2.ssrcs.push_back(kVideoTrack2Ssrc);
+    if (use_ssrcs) {
+      video_track_2.cname = kStream2Cname;
+      video_track_2.ssrcs.push_back(kVideoTrack2Ssrc);
+    }
     video_desc_2->AddStream(video_track_2);
     desc_.AddContent(kVideoContentName2, MediaProtocolType::kRtp, video_desc_2);
     EXPECT_TRUE(desc_.AddTransportInfo(TransportInfo(
@@ -1146,9 +1251,11 @@
     VideoContentDescription* video_desc_3 = CreateVideoContentDescription();
     StreamParams video_track_3;
     video_track_3.id = kVideoTrackId3;
-    video_track_3.cname = kStream2Cname;
     video_track_3.set_stream_ids({kStreamId2});
-    video_track_3.ssrcs.push_back(kVideoTrack3Ssrc);
+    if (use_ssrcs) {
+      video_track_3.cname = kStream2Cname;
+      video_track_3.ssrcs.push_back(kVideoTrack3Ssrc);
+    }
     video_desc_3->AddStream(video_track_3);
     desc_.AddContent(kVideoContentName3, MediaProtocolType::kRtp, video_desc_3);
     EXPECT_TRUE(desc_.AddTransportInfo(TransportInfo(
@@ -1300,6 +1407,10 @@
 
     // streams
     EXPECT_EQ(cd1->streams(), cd2->streams());
+    EXPECT_EQ(cd1->has_receive_stream(), cd2->has_receive_stream());
+    if (cd1->has_receive_stream() && cd2->has_receive_stream()) {
+      EXPECT_EQ(cd1->receive_stream(), cd2->receive_stream());
+    }
 
     // extmap-allow-mixed
     EXPECT_EQ(cd1->extmap_allow_mixed_enum(), cd2->extmap_allow_mixed_enum());
@@ -1316,6 +1427,18 @@
     }
   }
 
+  void CompareRidDescriptionIds(const std::vector<RidDescription>& rids,
+                                const std::vector<std::string>& ids) {
+    // Order of elements does not matter, only equivalence of sets.
+    EXPECT_EQ(rids.size(), ids.size());
+    for (const std::string& id : ids) {
+      EXPECT_EQ(1l, std::count_if(rids.begin(), rids.end(),
+                                  [id](const RidDescription& rid) {
+                                    return rid.rid == id;
+                                  }));
+    }
+  }
+
   void CompareSimulcastDescription(const SimulcastDescription& simulcast1,
                                    const SimulcastDescription& simulcast2) {
     EXPECT_EQ(simulcast1.send_layers().size(), simulcast2.send_layers().size());
@@ -3952,7 +4075,13 @@
 
 // Validates that deserialization uses the a=simulcast: attribute
 TEST_F(WebRtcSdpTest, TestDeserializeSimulcastAttribute) {
-  std::string sdp = kSdpFullString;
+  std::string sdp = kUnifiedPlanSdpFullStringNoSsrc;
+  sdp += "a=rid:1 send\r\n";
+  sdp += "a=rid:2 send\r\n";
+  sdp += "a=rid:3 send\r\n";
+  sdp += "a=rid:4 recv\r\n";
+  sdp += "a=rid:5 recv\r\n";
+  sdp += "a=rid:6 recv\r\n";
   sdp += "a=simulcast:send 1,2;3 recv 4;5;6\r\n";
   JsepSessionDescription output(kDummyType);
   SdpParseError error;
@@ -3963,6 +4092,188 @@
   EXPECT_TRUE(media->HasSimulcast());
   EXPECT_EQ(2ul, media->simulcast_description().send_layers().size());
   EXPECT_EQ(3ul, media->simulcast_description().receive_layers().size());
+  EXPECT_FALSE(media->streams().empty());
+  const std::vector<RidDescription>& rids = media->streams()[0].rids();
+  CompareRidDescriptionIds(rids, {"1", "2", "3"});
+  ASSERT_TRUE(media->has_receive_stream());
+  CompareRidDescriptionIds(media->receive_stream().rids(), {"4", "5", "6"});
+}
+
+// Validates that deserialization removes rids that do not appear in SDP
+TEST_F(WebRtcSdpTest, TestDeserializeSimulcastAttributeRemovesUnknownRids) {
+  std::string sdp = kUnifiedPlanSdpFullStringNoSsrc;
+  sdp += "a=rid:1 send\r\n";
+  sdp += "a=rid:3 send\r\n";
+  sdp += "a=rid:4 recv\r\n";
+  sdp += "a=simulcast:send 1,2;3 recv 4;5,6\r\n";
+  JsepSessionDescription output(kDummyType);
+  SdpParseError error;
+  EXPECT_TRUE(webrtc::SdpDeserialize(sdp, &output, &error));
+  const cricket::ContentInfos& contents = output.description()->contents();
+  const cricket::MediaContentDescription* media =
+      contents.back().media_description();
+  EXPECT_TRUE(media->HasSimulcast());
+  const SimulcastDescription& simulcast = media->simulcast_description();
+  EXPECT_EQ(2ul, simulcast.send_layers().size());
+  EXPECT_EQ(1ul, simulcast.receive_layers().size());
+
+  std::vector<SimulcastLayer> all_send_layers =
+      simulcast.send_layers().GetAllLayers();
+  EXPECT_EQ(2ul, all_send_layers.size());
+  EXPECT_EQ(0, std::count_if(all_send_layers.begin(), all_send_layers.end(),
+                             [](const SimulcastLayer& layer) {
+                               return layer.rid == "2";
+                             }));
+
+  std::vector<SimulcastLayer> all_receive_layers =
+      simulcast.receive_layers().GetAllLayers();
+  ASSERT_EQ(1ul, all_receive_layers.size());
+  EXPECT_EQ("4", all_receive_layers[0].rid);
+
+  EXPECT_FALSE(media->streams().empty());
+  const std::vector<RidDescription>& rids = media->streams()[0].rids();
+  CompareRidDescriptionIds(rids, {"1", "3"});
+  ASSERT_TRUE(media->has_receive_stream());
+  CompareRidDescriptionIds(media->receive_stream().rids(), {"4"});
+}
+
+// Validates that Simulcast removes rids that appear in both send and receive.
+TEST_F(WebRtcSdpTest,
+       TestDeserializeSimulcastAttributeRemovesDuplicateSendReceive) {
+  std::string sdp = kUnifiedPlanSdpFullStringNoSsrc;
+  sdp += "a=rid:1 send\r\n";
+  sdp += "a=rid:2 send\r\n";
+  sdp += "a=rid:3 send\r\n";
+  sdp += "a=rid:4 recv\r\n";
+  sdp += "a=simulcast:send 1;2;3 recv 2;4\r\n";
+  JsepSessionDescription output(kDummyType);
+  SdpParseError error;
+  EXPECT_TRUE(webrtc::SdpDeserialize(sdp, &output, &error));
+  const cricket::ContentInfos& contents = output.description()->contents();
+  const cricket::MediaContentDescription* media =
+      contents.back().media_description();
+  EXPECT_TRUE(media->HasSimulcast());
+  const SimulcastDescription& simulcast = media->simulcast_description();
+  EXPECT_EQ(2ul, simulcast.send_layers().size());
+  EXPECT_EQ(1ul, simulcast.receive_layers().size());
+  EXPECT_EQ(2ul, simulcast.send_layers().GetAllLayers().size());
+  EXPECT_EQ(1ul, simulcast.receive_layers().GetAllLayers().size());
+
+  EXPECT_FALSE(media->streams().empty());
+  const std::vector<RidDescription>& rids = media->streams()[0].rids();
+  CompareRidDescriptionIds(rids, {"1", "3"});
+  ASSERT_TRUE(media->has_receive_stream());
+  CompareRidDescriptionIds(media->receive_stream().rids(), {"4"});
+}
+
+// Ignores empty rid line.
+TEST_F(WebRtcSdpTest, TestDeserializeIgnoresEmptyRidLines) {
+  std::string sdp = kUnifiedPlanSdpFullStringNoSsrc;
+  sdp += "a=rid:1 send\r\n";
+  sdp += "a=rid:2 send\r\n";
+  sdp += "a=rid\r\n";   // Should ignore this line.
+  sdp += "a=rid:\r\n";  // Should ignore this line.
+  sdp += "a=simulcast:send 1;2\r\n";
+  JsepSessionDescription output(kDummyType);
+  SdpParseError error;
+  EXPECT_TRUE(webrtc::SdpDeserialize(sdp, &output, &error));
+  const cricket::ContentInfos& contents = output.description()->contents();
+  const cricket::MediaContentDescription* media =
+      contents.back().media_description();
+  EXPECT_TRUE(media->HasSimulcast());
+  const SimulcastDescription& simulcast = media->simulcast_description();
+  EXPECT_TRUE(simulcast.receive_layers().empty());
+  EXPECT_EQ(2ul, simulcast.send_layers().size());
+  EXPECT_EQ(2ul, simulcast.send_layers().GetAllLayers().size());
+
+  EXPECT_FALSE(media->streams().empty());
+  const std::vector<RidDescription>& rids = media->streams()[0].rids();
+  CompareRidDescriptionIds(rids, {"1", "2"});
+  ASSERT_FALSE(media->has_receive_stream());
+}
+
+// Ignores malformed rid lines.
+TEST_F(WebRtcSdpTest, TestDeserializeIgnoresMalformedRidLines) {
+  std::string sdp = kUnifiedPlanSdpFullStringNoSsrc;
+  sdp += "a=rid:1 send pt=\r\n";              // Should ignore this line.
+  sdp += "a=rid:2 receive\r\n";               // Should ignore this line.
+  sdp += "a=rid:3 max-width=720;pt=120\r\n";  // Should ignore this line.
+  sdp += "a=rid:4\r\n";                       // Should ignore this line.
+  sdp += "a=rid:5 send\r\n";
+  sdp += "a=simulcast:send 1,2,3;4,5\r\n";
+  JsepSessionDescription output(kDummyType);
+  SdpParseError error;
+  EXPECT_TRUE(webrtc::SdpDeserialize(sdp, &output, &error));
+  const cricket::ContentInfos& contents = output.description()->contents();
+  const cricket::MediaContentDescription* media =
+      contents.back().media_description();
+  EXPECT_TRUE(media->HasSimulcast());
+  const SimulcastDescription& simulcast = media->simulcast_description();
+  EXPECT_TRUE(simulcast.receive_layers().empty());
+  EXPECT_EQ(1ul, simulcast.send_layers().size());
+  EXPECT_EQ(1ul, simulcast.send_layers().GetAllLayers().size());
+
+  EXPECT_FALSE(media->streams().empty());
+  const std::vector<RidDescription>& rids = media->streams()[0].rids();
+  CompareRidDescriptionIds(rids, {"5"});
+  ASSERT_FALSE(media->has_receive_stream());
+}
+
+// Removes RIDs that specify a different format than the m= section.
+TEST_F(WebRtcSdpTest, TestDeserializeRemovesRidsWithInvalidCodec) {
+  std::string sdp = kUnifiedPlanSdpFullStringNoSsrc;
+  sdp += "a=rid:1 send pt=121,120\r\n";  // Should remove 121 and keep RID.
+  sdp += "a=rid:2 send pt=121\r\n";      // Should remove RID altogether.
+  sdp += "a=simulcast:send 1;2\r\n";
+  JsepSessionDescription output(kDummyType);
+  SdpParseError error;
+  EXPECT_TRUE(webrtc::SdpDeserialize(sdp, &output, &error));
+  const cricket::ContentInfos& contents = output.description()->contents();
+  const cricket::MediaContentDescription* media =
+      contents.back().media_description();
+  EXPECT_TRUE(media->HasSimulcast());
+  const SimulcastDescription& simulcast = media->simulcast_description();
+  EXPECT_TRUE(simulcast.receive_layers().empty());
+  EXPECT_EQ(1ul, simulcast.send_layers().size());
+  EXPECT_EQ(1ul, simulcast.send_layers().GetAllLayers().size());
+  EXPECT_EQ("1", simulcast.send_layers()[0][0].rid);
+  EXPECT_EQ(1ul, media->streams().size());
+  const std::vector<RidDescription>& rids = media->streams()[0].rids();
+  EXPECT_EQ(1ul, rids.size());
+  EXPECT_EQ("1", rids[0].rid);
+  EXPECT_EQ(1ul, rids[0].payload_types.size());
+  EXPECT_EQ(120, rids[0].payload_types[0]);
+
+  ASSERT_FALSE(media->has_receive_stream());
+}
+
+// Ignores duplicate rid lines
+TEST_F(WebRtcSdpTest, TestDeserializeIgnoresDuplicateRidLines) {
+  std::string sdp = kUnifiedPlanSdpFullStringNoSsrc;
+  sdp += "a=rid:1 send\r\n";
+  sdp += "a=rid:2 send\r\n";
+  sdp += "a=rid:2 send\r\n";
+  sdp += "a=rid:3 send\r\n";
+  sdp += "a=rid:4 recv\r\n";
+  sdp += "a=simulcast:send 1,2;3 recv 4\r\n";
+  JsepSessionDescription output(kDummyType);
+  SdpParseError error;
+  EXPECT_TRUE(webrtc::SdpDeserialize(sdp, &output, &error));
+  const cricket::ContentInfos& contents = output.description()->contents();
+  const cricket::MediaContentDescription* media =
+      contents.back().media_description();
+  EXPECT_TRUE(media->HasSimulcast());
+  const SimulcastDescription& simulcast = media->simulcast_description();
+  EXPECT_EQ(2ul, simulcast.send_layers().size());
+  EXPECT_EQ(1ul, simulcast.receive_layers().size());
+  EXPECT_EQ(2ul, simulcast.send_layers().GetAllLayers().size());
+  EXPECT_EQ(1ul, simulcast.receive_layers().GetAllLayers().size());
+
+  EXPECT_FALSE(media->streams().empty());
+  const std::vector<RidDescription>& rids = media->streams()[0].rids();
+  CompareRidDescriptionIds(rids, {"1", "3"});
+  ASSERT_TRUE(media->has_receive_stream());
+  CompareRidDescriptionIds(media->receive_stream().rids(), {"4"});
 }
 
 // Simulcast serialization integration test.
@@ -3970,9 +4281,28 @@
 // More detailed tests for parsing simulcast can be found in
 // unit tests for SdpSerializer.
 TEST_F(WebRtcSdpTest, SerializeSimulcast_ComplexSerialization) {
-  MakeUnifiedPlanDescription();
+  MakeUnifiedPlanDescription(/* use_ssrcs = */ false);
   auto description = jdesc_.description();
   auto media = description->GetContentDescriptionByName(kVideoContentName3);
+  ASSERT_EQ(media->streams().size(), 1ul);
+  StreamParams& send_stream = media->mutable_streams()[0];
+  std::vector<RidDescription> send_rids;
+  send_rids.push_back(RidDescription("1", RidDirection::kSend));
+  send_rids.push_back(RidDescription("2", RidDirection::kSend));
+  send_rids.push_back(RidDescription("3", RidDirection::kSend));
+  send_rids.push_back(RidDescription("4", RidDirection::kSend));
+  send_stream.set_rids(send_rids);
+  StreamParams recv_stream;
+  std::vector<RidDescription> recv_rids;
+  recv_rids.push_back(RidDescription("6", RidDirection::kReceive));
+  recv_rids.push_back(RidDescription("7", RidDirection::kReceive));
+  recv_rids.push_back(RidDescription("8", RidDirection::kReceive));
+  recv_rids.push_back(RidDescription("9", RidDirection::kReceive));
+  recv_rids.push_back(RidDescription("10", RidDirection::kReceive));
+  recv_rids.push_back(RidDescription("11", RidDirection::kReceive));
+  recv_stream.set_rids(recv_rids);
+  media->set_receive_stream(recv_stream);
+
   SimulcastDescription& simulcast = media->simulcast_description();
   simulcast.send_layers().AddLayerWithAlternatives(
       {SimulcastLayer("2", false), SimulcastLayer("1", true)});