diff --git a/media/BUILD.gn b/media/BUILD.gn index b00615e614..affbefacec 100644 --- a/media/BUILD.gn +++ b/media/BUILD.gn @@ -97,6 +97,8 @@ rtc_static_library("rtc_media_base") { "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 0000000000..7f0c5d02da --- /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 0000000000..5a616ff26b --- /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 +#include +#include + +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 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 restrictions; +}; + +} // namespace cricket + +#endif // MEDIA_BASE_RIDDESCRIPTION_H_ diff --git a/media/base/streamparams.cc b/media/base/streamparams.cc index 8f5255928b..8fb1719c90 100644 --- a/media/base/streamparams.cc +++ b/media/base/streamparams.cc @@ -11,6 +11,7 @@ #include "media/base/streamparams.h" #include +#include #include #include "api/array_view.h" @@ -23,6 +24,35 @@ namespace { void AddStream(std::vector* streams, const StreamParams& stream) { streams->push_back(stream); } + +std::string SsrcsToString(const std::vector& ssrcs) { + char buf[1024]; + rtc::SimpleStringBuilder sb(buf); + sb << "ssrcs:["; + for (std::vector::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& 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 @@ bool MediaStreams::RemoveDataStream(const StreamSelector& selector) { return RemoveStream(&data_, selector); } -static std::string SsrcsToString(const std::vector& ssrcs) { - char buf[1024]; - rtc::SimpleStringBuilder sb(buf); - sb << "ssrcs:["; - for (std::vector::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& ssrcs) : semantics(usage), ssrcs(ssrcs) {} @@ -133,6 +148,15 @@ StreamParams::~StreamParams() = default; 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 @@ std::string StreamParams::ToString() const { sb << *it; } sb << ";"; + if (!rids_.empty()) { + sb << RidsToString(rids_) << ";"; + } sb << "}"; return sb.str(); } @@ -267,15 +294,22 @@ bool IsOneSsrcStream(const StreamParams& sp) { return false; } -static void RemoveFirst(std::list* ssrcs, uint32_t value) { +namespace { +void RemoveFirst(std::list* ssrcs, uint32_t value) { std::list::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 27069ed3c4..f1285329c8 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 #include +#include "media/base/riddescription.h" #include "rtc_base/constructormagic.h" namespace cricket { @@ -80,11 +101,7 @@ struct StreamParams { 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 @@ struct StreamParams { std::vector 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& rids() const { return rids_; } + void set_rids(const std::vector& rids) { rids_ = rids; } + private: bool AddSecondarySsrc(const std::string& semantics, uint32_t primary_ssrc, @@ -184,6 +211,8 @@ struct StreamParams { // 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 stream_ids_; + + std::vector rids_; }; // A Stream can be selected by either groupid+id or ssrc. diff --git a/pc/sdpserializer.cc b/pc/sdpserializer.cc index b5a1d877f1..8e20aa2ec9 100644 --- a/pc/sdpserializer.cc +++ b/pc/sdpserializer.cc @@ -10,13 +10,19 @@ #include "pc/sdpserializer.h" +#include #include #include #include #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 @@ namespace { // 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 @@ rtc::StringBuilder& operator<<(rtc::StringBuilder& builder, } 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 @@ RTCErrorOr ParseSimulcastLayerList(const std::string& str) { std::vector 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 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 @@ RTCErrorOr ParseSimulcastLayerList(const std::string& str) { return std::move(result); } +webrtc::RTCError ParseRidPayloadList(const std::string& payload_list, + RidDescription* rid_description) { + RTC_DCHECK(rid_description); + std::vector& 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 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 value = rtc::StringToNumber(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 SdpSerializer::SerializeSimulcastDescription( 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 @@ RTCErrorOr SdpSerializer::DeserializeSimulcastDescription( 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 @@ RTCErrorOr SdpSerializer::DeserializeSimulcastDescription( } // 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 @@ RTCErrorOr SdpSerializer::DeserializeSimulcastDescription( 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 SdpSerializer::DeserializeRidDescription( + absl::string_view string) const { + std::vector tokens; + rtc::tokenize(std::string(string), kDelimiterSpaceChar, &tokens); + + if (tokens.size() < 2) { + return ParseError("RID Description must contain ."); + } + + 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 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 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 428b4209c9..3c6f31b0e2 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 @@ class SdpSerializer { // https://tools.ietf.org/html/draft-ietf-mmusic-sdp-simulcast-13#section-5.1 RTCErrorOr 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 DeserializeRidDescription( + absl::string_view string) const; }; } // namespace webrtc diff --git a/pc/sdpserializer_unittest.cc b/pc/sdpserializer_unittest.cc index 8b10da1a16..c5e95d01e9 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 #include +#include #include #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 { +namespace { +// Checks that two vectors have the same objects in the same order. +template +void ExpectEqual(const std::vector& expected, + const std::vector& 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& expected, + const std::vector& 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 +void ExpectEqual(const std::map& expected, + const std::map& actual) { + typedef typename std::map::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& 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 { 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 @@ class SdpSerializerTest : public ::testing::TestWithParam { // 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& expected, - const std::vector& 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 @@ TEST_F(SdpSerializerTest, DeserializeSimulcast_SimpleCaseWithAlternatives) { {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 @@ TEST_F(SdpSerializerTest, DeserializeSimulcast_WithSomeAlternatives) { 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 @@ TEST_F(SdpSerializerTest, DeserializeSimulcast_OnlySendStreams) { 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 @@ TEST_F(SdpSerializerTest, DeserializeSimulcast_OnlyReceiveStreams) { 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 @@ TEST_F(SdpSerializerTest, DeserializeSimulcast_SendReceiveReversed) { 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 @@ TEST_F(SdpSerializerTest, DeserializeSimulcast_PausedStreams) { 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 @@ const char* kSimulcastMalformedStrings[] = { }; 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 @@ TEST_F(SdpSerializerTest, SerializeSimulcast_ComplexSerialization) { 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 { + 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 ff6cd2f94d..ceb9fd7c76 100644 --- a/pc/sessiondescription.h +++ b/pc/sessiondescription.h @@ -140,39 +140,56 @@ class MediaContentDescription { // 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 @@ class MediaContentDescription { std::vector cryptos_; std::vector rtp_header_extensions_; bool rtp_header_extensions_set_ = false; - StreamParamsVec streams_; + StreamParamsVec send_streams_; + absl::optional receive_stream_; bool conference_mode_ = false; webrtc::RtpTransceiverDirection direction_ = webrtc::RtpTransceiverDirection::kSendRecv; diff --git a/pc/simulcastdescription.cc b/pc/simulcastdescription.cc index eca67a7855..8fff520fa0 100644 --- a/pc/simulcastdescription.cc +++ b/pc/simulcastdescription.cc @@ -17,7 +17,6 @@ namespace cricket { 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 @@ bool SimulcastDescription::empty() const { return send_layers_.empty() && receive_layers_.empty(); } +std::vector SimulcastLayerList::GetAllLayers() const { + std::vector 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 f4708ff025..9f15f78f43 100644 --- a/pc/simulcastdescription.h +++ b/pc/simulcastdescription.h @@ -70,6 +70,10 @@ class SimulcastLayerList final { 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 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 37d765404e..5a1b2f8406 100644 --- a/pc/webrtcsdp.cc +++ b/pc/webrtcsdp.cc @@ -74,7 +74,10 @@ using cricket::MediaContentDescription; 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 @@ static const char kAttributeSctpPort[] = "sctp-port"; // 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 @@ static bool ParseMsidAttribute(const std::string& line, std::string* track_id, SdpParseError* error); +static void RemoveInvalidRidDescriptions(const std::vector& payload_types, + std::vector* rids); + +static SimulcastLayerList RemoveRidsFromSimulcastLayerList( + const std::set& to_remove, + const SimulcastLayerList& layers); + +static void RemoveInvalidRidsFromSimulcast( + const std::vector& rids, + SimulcastDescription* simulcast); + // Helper functions // Below ParseFailed*** functions output the line that caused the parsing @@ -628,6 +645,7 @@ static bool GetPayloadTypeFromString(const std::string& line, // stream_ids/track_id if it's signaled with a=msid lines. void CreateTrackWithNoSsrcs(const std::vector& msid_stream_ids, const std::string& msid_track_id, + const std::vector& rids, StreamParamsVec* tracks) { StreamParams track; if (msid_track_id.empty() || msid_stream_ids.empty()) { @@ -636,6 +654,7 @@ void CreateTrackWithNoSsrcs(const std::vector& msid_stream_ids, } track.set_stream_ids(msid_stream_ids); track.id = msid_track_id; + track.set_rids(rids); tracks->push_back(track); } @@ -1496,6 +1515,7 @@ void BuildRtpContentAttributes(const MediaContentDescription* media_desc, 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 @@ void BuildRtpContentAttributes(const MediaContentDescription* media_desc, 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 @@ static bool ParseMsidAttribute(const std::string& line, return true; } +static void RemoveInvalidRidDescriptions(const std::vector& payload_types, + std::vector* rids) { + RTC_DCHECK(rids); + std::set to_remove; + std::set 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 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& to_remove, + const SimulcastLayerList& layers) { + SimulcastLayerList result; + for (const std::vector& vector : layers) { + std::vector 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& valid_rids, + SimulcastDescription* simulcast) { + RTC_DCHECK(simulcast); + std::set to_remove; + std::vector all_send_layers = + simulcast->send_layers().GetAllLayers(); + std::vector 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 @@ bool ParseContent(const std::string& message, std::string ptime_as_string; std::vector stream_ids; std::string track_id; + SdpSerializer deserializer; + std::vector rids; SimulcastDescription simulcast; // Loop until the next m line @@ -2971,6 +3147,26 @@ bool ParseContent(const std::string& message, 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 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 @@ bool ParseContent(const std::string& message, error); } - SdpSerializer deserializer; RTCErrorOr error_or_simulcast = deserializer.DeserializeSimulcastDescription( line.substr(kSimulcastPrefixLength)); @@ -3007,6 +3202,40 @@ bool ParseContent(const std::string& message, } } + // 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 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 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 @@ bool ParseContent(const std::string& message, // 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 @@ bool ParseContent(const std::string& message, 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 8a3c28fe5b..0ecba4c25e 100644 --- a/pc/webrtcsdp_unittest.cc +++ b/pc/webrtcsdp_unittest.cc @@ -60,6 +60,8 @@ using cricket::LOCAL_PORT_TYPE; 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 @@ static const char kUnifiedPlanSdpFullStringWithSpecialMsid[] = "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: not a=candidate:CRLF static const char kRawCandidate[] = @@ -1118,14 +1219,16 @@ class WebRtcSdpTest : public testing::Test { // 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 @@ class WebRtcSdpTest : public testing::Test { 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 @@ class WebRtcSdpTest : public testing::Test { 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 @@ class WebRtcSdpTest : public testing::Test { // 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 @@ class WebRtcSdpTest : public testing::Test { } } + void CompareRidDescriptionIds(const std::vector& rids, + const std::vector& 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 @@ TEST_F(WebRtcSdpTest, DeserializeSimulcastNegative_DuplicateAttribute) { // 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 @@ TEST_F(WebRtcSdpTest, TestDeserializeSimulcastAttribute) { 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& 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 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 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& 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& 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& 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& 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& 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& 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 @@ TEST_F(WebRtcSdpTest, TestDeserializeSimulcastAttribute) { // 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 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 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)});