From a201204215f0c48f74b1723e916fa67ffae636d8 Mon Sep 17 00:00:00 2001 From: Amit Hilbuch Date: Mon, 3 Dec 2018 11:35:05 -0800 Subject: [PATCH] Adding SDP parsing for Simulcast. Parsing simulcast according to: https://tools.ietf.org/html/draft-ietf-mmusic-sdp-simulcast-13#section-5.1 Created SdpSerializer for making serialized components more testable. Simulcast functionality is still not accessible to users. Bug: webrtc:10055 Change-Id: Ia6e4cef756cb954521dd19e22911f8eb6498880e Reviewed-on: https://webrtc-review.googlesource.com/c/112160 Commit-Queue: Amit Hilbuch Reviewed-by: Seth Hampson Reviewed-by: Steve Anton Cr-Commit-Position: refs/heads/master@{#25883} --- pc/BUILD.gn | 5 + pc/sdpserializer.cc | 217 ++++++++++++++++++++++++++++++++ pc/sdpserializer.h | 49 ++++++++ pc/sdpserializer_unittest.cc | 235 +++++++++++++++++++++++++++++++++++ pc/sessiondescription.h | 14 +++ pc/simulcastdescription.cc | 44 +++++++ pc/simulcastdescription.h | 104 ++++++++++++++++ pc/webrtcsdp.cc | 52 ++++++++ pc/webrtcsdp_unittest.cc | 65 ++++++++++ 9 files changed, 785 insertions(+) create mode 100644 pc/sdpserializer.cc create mode 100644 pc/sdpserializer.h create mode 100644 pc/sdpserializer_unittest.cc create mode 100644 pc/simulcastdescription.cc create mode 100644 pc/simulcastdescription.h diff --git a/pc/BUILD.gn b/pc/BUILD.gn index 6999ddd4ee..89c4df8855 100644 --- a/pc/BUILD.gn +++ b/pc/BUILD.gn @@ -56,6 +56,8 @@ rtc_static_library("rtc_pc_base") { "rtptransportinternaladapter.h", "sessiondescription.cc", "sessiondescription.h", + "simulcastdescription.cc", + "simulcastdescription.h", "srtpfilter.cc", "srtpfilter.h", "srtpsession.cc", @@ -163,6 +165,8 @@ rtc_static_library("peerconnection") { "rtptransceiver.h", "sctputils.cc", "sctputils.h", + "sdpserializer.cc", + "sdpserializer.h", "sdputils.cc", "sdputils.h", "statscollector.cc", @@ -456,6 +460,7 @@ if (rtc_include_tests) { "rtpsenderreceiver_unittest.cc", "rtptransceiver_unittest.cc", "sctputils_unittest.cc", + "sdpserializer_unittest.cc", "statscollector_unittest.cc", "test/fakeaudiocapturemodule_unittest.cc", "test/testsdpstrings.h", diff --git a/pc/sdpserializer.cc b/pc/sdpserializer.cc new file mode 100644 index 0000000000..b5a1d877f1 --- /dev/null +++ b/pc/sdpserializer.cc @@ -0,0 +1,217 @@ +/* + * 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 "pc/sdpserializer.h" + +#include +#include +#include + +#include "api/jsep.h" +#include "rtc_base/strings/string_builder.h" + +using cricket::SimulcastDescription; +using cricket::SimulcastLayer; +using cricket::SimulcastLayerList; + +namespace webrtc { + +namespace { + +// delimiters +const char kDelimiterComma[] = ","; +const char kDelimiterCommaChar = ','; +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 +const char kSimulcastPausedStream[] = "~"; +const char kSimulcastPausedStreamChar = '~'; +const char kSimulcastSendStreams[] = "send"; +const char kSimulcastReceiveStreams[] = "recv"; + +RTCError ParseError(const std::string& message) { + return RTCError(RTCErrorType::SYNTAX_ERROR, message); +} + +// These methods serialize simulcast according to the specification: +// https://tools.ietf.org/html/draft-ietf-mmusic-sdp-simulcast-13#section-5.1 +rtc::StringBuilder& operator<<(rtc::StringBuilder& builder, + const SimulcastLayer& simulcast_layer) { + if (simulcast_layer.is_paused) { + builder << kSimulcastPausedStream; + } + builder << simulcast_layer.rid; + return builder; +} + +rtc::StringBuilder& operator<<( + rtc::StringBuilder& builder, + const std::vector& layer_alternatives) { + bool first = true; + for (const SimulcastLayer& rid : layer_alternatives) { + if (!first) { + builder << kDelimiterComma; + } + builder << rid; + first = false; + } + return builder; +} + +rtc::StringBuilder& operator<<(rtc::StringBuilder& builder, + const SimulcastLayerList& simulcast_layers) { + bool first = true; + for (auto alternatives : simulcast_layers) { + if (!first) { + builder << kDelimiterSemicolon; + } + builder << alternatives; + first = false; + } + return builder; +} + +// These methods deserialize 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 ) +// sc-id-paused = "~" +// sc-id = [sc-id-paused] rid-id +// rid-id = 1*(alpha-numeric / "-" / "_") ; see: I-D.ietf-mmusic-rid +RTCErrorOr ParseSimulcastLayerList(const std::string& str) { + std::vector tokens; + rtc::tokenize_with_empty_tokens(str, kDelimiterSemicolonChar, &tokens); + if (tokens.empty()) { + return ParseError("Layer list cannot be empty."); + } + + SimulcastLayerList result; + for (const std::string& token : tokens) { + if (token.empty()) { + return ParseError("Simulcast alternative layer list is empty."); + } + + 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) { + 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)); + } + + result.AddLayerWithAlternatives(layers); + } + + return std::move(result); +} + +} // namespace + +std::string SdpSerializer::SerializeSimulcastDescription( + const cricket::SimulcastDescription& simulcast) const { + rtc::StringBuilder sb; + std::string delimiter; + + if (!simulcast.send_layers().empty()) { + sb << kSimulcastSendStreams << kDelimiterSpace << simulcast.send_layers(); + delimiter = kDelimiterSpace; + } + + if (!simulcast.receive_layers().empty()) { + sb << delimiter << kSimulcastReceiveStreams << kDelimiterSpace + << simulcast.receive_layers(); + } + + return sb.str(); +} + +// https://tools.ietf.org/html/draft-ietf-mmusic-sdp-simulcast-13#section-5.1 +// a:simulcast: +// Formal Grammar +// sc-value = ( sc-send [SP sc-recv] ) / ( sc-recv [SP sc-send] ) +// sc-send = %s"send" SP sc-str-list +// sc-recv = %s"recv" SP sc-str-list +// sc-str-list = sc-alt-list *( ";" sc-alt-list ) +// sc-alt-list = sc-id *( "," sc-id ) +// sc-id-paused = "~" +// sc-id = [sc-id-paused] rid-id +// rid-id = 1*(alpha-numeric / "-" / "_") ; see: I-D.ietf-mmusic-rid +RTCErrorOr SdpSerializer::DeserializeSimulcastDescription( + absl::string_view string) const { + std::vector tokens; + rtc::tokenize(std::string(string), kDelimiterSpaceChar, &tokens); + + if (tokens.size() != 2 && tokens.size() != 4) { + return ParseError("Must have one or two pairs."); + } + + 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) || + (bidirectional && tokens[0] == tokens[2])) { + return ParseError("Valid values: send / recv."); + } + + // Tokens 1, 3 (if exists) should be alternative layer lists + RTCErrorOr list1, list2; + list1 = ParseSimulcastLayerList(tokens[1]); + if (!list1.ok()) { + return list1.MoveError(); + } + + if (bidirectional) { + list2 = ParseSimulcastLayerList(tokens[3]); + if (!list2.ok()) { + return list2.MoveError(); + } + } + + // Set the layers so that list1 is for send and list2 is for recv + if (tokens[0] != kSimulcastSendStreams) { + std::swap(list1, list2); + } + + // Set the layers according to which pair is send and which is recv + // At this point if the simulcast is unidirectional then + // either |list1| or |list2| will be in 'error' state indicating that + // the value should not be used. + SimulcastDescription simulcast; + if (list1.ok()) { + simulcast.send_layers() = list1.MoveValue(); + } + + if (list2.ok()) { + simulcast.receive_layers() = list2.MoveValue(); + } + + return std::move(simulcast); +} + +} // namespace webrtc diff --git a/pc/sdpserializer.h b/pc/sdpserializer.h new file mode 100644 index 0000000000..428b4209c9 --- /dev/null +++ b/pc/sdpserializer.h @@ -0,0 +1,49 @@ +/* + * 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 PC_SDPSERIALIZER_H_ +#define PC_SDPSERIALIZER_H_ + +#include + +#include "absl/strings/string_view.h" +#include "api/rtcerror.h" +#include "pc/sessiondescription.h" + +namespace webrtc { + +// This class should serialize components of the SDP (and not the SDP itself). +// Example: +// SimulcastDescription can be serialized and deserialized by this class. +// The serializer will know how to translate the data to spec-compliant +// format without knowing about the SDP attribute details (a=simulcast:) +// Usage: +// Consider the SDP attribute for simulcast a=simulcast:. +// The SDP serializtion code (webrtcsdp.h) should use |SdpSerializer| to +// serialize and deserialize the section. +// This class will allow testing the serialization of components without +// having to serialize the entire SDP while hiding implementation details +// from callers of sdp serialization (webrtcsdp.h). +class SdpSerializer { + public: + // Serialization for the Simulcast description according to + // https://tools.ietf.org/html/draft-ietf-mmusic-sdp-simulcast-13#section-5.1 + std::string SerializeSimulcastDescription( + const cricket::SimulcastDescription& simulcast) const; + + // Deserialization for the SimulcastDescription according to + // https://tools.ietf.org/html/draft-ietf-mmusic-sdp-simulcast-13#section-5.1 + RTCErrorOr DeserializeSimulcastDescription( + absl::string_view string) const; +}; + +} // namespace webrtc + +#endif // PC_SDPSERIALIZER_H_ diff --git a/pc/sdpserializer_unittest.cc b/pc/sdpserializer_unittest.cc new file mode 100644 index 0000000000..8b10da1a16 --- /dev/null +++ b/pc/sdpserializer_unittest.cc @@ -0,0 +1,235 @@ +/* + * 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 +#include + +#include "pc/sdpserializer.h" +#include "rtc_base/gunit.h" + +using ::testing::ValuesIn; +using cricket::SimulcastDescription; +using cricket::SimulcastLayer; +using cricket::SimulcastLayerList; + +namespace webrtc { + +class SdpSerializerTest : public ::testing::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 { + SdpSerializer deserializer; + auto result = deserializer.DeserializeSimulcastDescription(str); + EXPECT_TRUE(result.ok()); + ExpectEqual(expected, result.value()); + } + + // 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 { + 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) { + 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); +} + +// Test simulcast deserialization with alternative streams. +TEST_F(SdpSerializerTest, DeserializeSimulcast_SimpleCaseWithAlternatives) { + std::string simulcast_str = "send 1,5;2,6 recv 3,7;4,8"; + SimulcastDescription expected; + expected.send_layers().AddLayerWithAlternatives( + {SimulcastLayer("1", false), SimulcastLayer("5", false)}); + expected.send_layers().AddLayerWithAlternatives( + {SimulcastLayer("2", false), SimulcastLayer("6", false)}); + expected.receive_layers().AddLayerWithAlternatives( + {SimulcastLayer("3", false), SimulcastLayer("7", false)}); + expected.receive_layers().AddLayerWithAlternatives( + {SimulcastLayer("4", false), SimulcastLayer("8", false)}); + TestSimulcastDeserialization(simulcast_str, expected); +} + +// Test simulcast deserialization when only some streams have alternatives. +TEST_F(SdpSerializerTest, DeserializeSimulcast_WithSomeAlternatives) { + std::string simulcast_str = "send 1;2,6 recv 3,7;4"; + SimulcastDescription expected; + expected.send_layers().AddLayer(SimulcastLayer("1", false)); + expected.send_layers().AddLayerWithAlternatives( + {SimulcastLayer("2", false), SimulcastLayer("6", false)}); + expected.receive_layers().AddLayerWithAlternatives( + {SimulcastLayer("3", false), SimulcastLayer("7", false)}); + expected.receive_layers().AddLayer(SimulcastLayer("4", false)); + TestSimulcastDeserialization(simulcast_str, expected); +} + +// Test simulcast deserialization when only send streams are specified. +TEST_F(SdpSerializerTest, DeserializeSimulcast_OnlySendStreams) { + std::string simulcast_str = "send 1;2,6;3,7;4"; + SimulcastDescription expected; + expected.send_layers().AddLayer(SimulcastLayer("1", false)); + expected.send_layers().AddLayerWithAlternatives( + {SimulcastLayer("2", false), SimulcastLayer("6", false)}); + expected.send_layers().AddLayerWithAlternatives( + {SimulcastLayer("3", false), SimulcastLayer("7", false)}); + expected.send_layers().AddLayer(SimulcastLayer("4", false)); + TestSimulcastDeserialization(simulcast_str, expected); +} + +// Test simulcast deserialization when only receive streams are specified. +TEST_F(SdpSerializerTest, DeserializeSimulcast_OnlyReceiveStreams) { + std::string simulcast_str = "recv 1;2,6;3,7;4"; + SimulcastDescription expected; + expected.receive_layers().AddLayer(SimulcastLayer("1", false)); + expected.receive_layers().AddLayerWithAlternatives( + {SimulcastLayer("2", false), SimulcastLayer("6", false)}); + expected.receive_layers().AddLayerWithAlternatives( + {SimulcastLayer("3", false), SimulcastLayer("7", false)}); + expected.receive_layers().AddLayer(SimulcastLayer("4", false)); + TestSimulcastDeserialization(simulcast_str, expected); +} + +// Test simulcast deserialization with receive streams before send streams. +TEST_F(SdpSerializerTest, DeserializeSimulcast_SendReceiveReversed) { + std::string simulcast_str = "recv 1;2,6 send 3,7;4"; + SimulcastDescription expected; + expected.receive_layers().AddLayer(SimulcastLayer("1", false)); + expected.receive_layers().AddLayerWithAlternatives( + {SimulcastLayer("2", false), SimulcastLayer("6", false)}); + expected.send_layers().AddLayerWithAlternatives( + {SimulcastLayer("3", false), SimulcastLayer("7", false)}); + expected.send_layers().AddLayer(SimulcastLayer("4", false)); + TestSimulcastDeserialization(simulcast_str, expected); +} + +// Test simulcast deserialization with some streams set to paused state. +TEST_F(SdpSerializerTest, DeserializeSimulcast_PausedStreams) { + std::string simulcast_str = "recv 1;~2,6 send 3,7;~4"; + SimulcastDescription expected; + expected.receive_layers().AddLayer(SimulcastLayer("1", false)); + expected.receive_layers().AddLayerWithAlternatives( + {SimulcastLayer("2", true), SimulcastLayer("6", false)}); + expected.send_layers().AddLayerWithAlternatives( + {SimulcastLayer("3", false), SimulcastLayer("7", false)}); + expected.send_layers().AddLayer(SimulcastLayer("4", true)); + TestSimulcastDeserialization(simulcast_str, expected); +} + +// Parameterized negative test case for deserialization with invalid inputs. +TEST_P(SdpSerializerTest, SimulcastDeserializationFailed) { + SdpSerializer deserializer; + auto result = deserializer.DeserializeSimulcastDescription(GetParam()); + EXPECT_FALSE(result.ok()); +} + +// The malformed Simulcast inputs to use in the negative test case. +const char* kSimulcastMalformedStrings[] = { + "send ", + "recv ", + "recv 1 send", + "receive 1", + "recv 1;~2,6 recv 3,7;~4", + "send 1;~2,6 send 3,7;~4", + "send ~;~2,6", + "send 1; ;~2,6", + "send 1,;~2,6", + "recv 1 send 2 3", + "", +}; + +INSTANTIATE_TEST_CASE_P(SimulcastDeserializationErrors, + SdpSerializerTest, + ValuesIn(kSimulcastMalformedStrings)); + +// Test a simple serialization scenario. +TEST_F(SdpSerializerTest, SerializeSimulcast_SimpleCase) { + SimulcastDescription simulcast; + simulcast.send_layers().AddLayer(SimulcastLayer("1", false)); + simulcast.receive_layers().AddLayer(SimulcastLayer("2", false)); + TestSimulcastSerialization(simulcast, "send 1 recv 2"); +} + +// Test serialization with only send streams. +TEST_F(SdpSerializerTest, SerializeSimulcast_OnlySend) { + SimulcastDescription simulcast; + simulcast.send_layers().AddLayer(SimulcastLayer("1", false)); + simulcast.send_layers().AddLayer(SimulcastLayer("2", false)); + TestSimulcastSerialization(simulcast, "send 1;2"); +} + +// Test serialization with only receive streams +TEST_F(SdpSerializerTest, SerializeSimulcast_OnlyReceive) { + SimulcastDescription simulcast; + simulcast.receive_layers().AddLayer(SimulcastLayer("1", false)); + simulcast.receive_layers().AddLayer(SimulcastLayer("2", false)); + TestSimulcastSerialization(simulcast, "recv 1;2"); +} + +// Test a complex serialization with multiple streams, alternatives and states. +TEST_F(SdpSerializerTest, SerializeSimulcast_ComplexSerialization) { + SimulcastDescription simulcast; + simulcast.send_layers().AddLayerWithAlternatives( + {SimulcastLayer("2", false), SimulcastLayer("1", true)}); + simulcast.send_layers().AddLayerWithAlternatives( + {SimulcastLayer("4", false), SimulcastLayer("3", false)}); + + simulcast.receive_layers().AddLayerWithAlternatives( + {SimulcastLayer("6", false), SimulcastLayer("7", false)}); + simulcast.receive_layers().AddLayer(SimulcastLayer("8", true)); + 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"); +} + +} // namespace webrtc diff --git a/pc/sessiondescription.h b/pc/sessiondescription.h index 691046f31c..ff6cd2f94d 100644 --- a/pc/sessiondescription.h +++ b/pc/sessiondescription.h @@ -25,6 +25,7 @@ #include "media/base/streamparams.h" #include "p2p/base/transportdescription.h" #include "p2p/base/transportinfo.h" +#include "pc/simulcastdescription.h" #include "rtc_base/socketaddress.h" namespace cricket { @@ -203,6 +204,17 @@ class MediaContentDescription { } bool extmap_allow_mixed() const { return extmap_allow_mixed_enum_ != kNo; } + // Simulcast functionality. + virtual bool HasSimulcast() const { return !simulcast_.empty(); } + virtual SimulcastDescription& simulcast_description() { return simulcast_; } + virtual const SimulcastDescription& simulcast_description() const { + return simulcast_; + } + virtual void set_simulcast_description( + const SimulcastDescription& simulcast) { + simulcast_ = simulcast; + } + protected: bool rtcp_mux_ = false; bool rtcp_reduced_size_ = false; @@ -220,6 +232,8 @@ class MediaContentDescription { // session level, but we will respond that we support it. The plan is to add // it to our offer on session level. See todo in SessionDescription. ExtmapAllowMixed extmap_allow_mixed_enum_ = kNo; + + SimulcastDescription simulcast_; }; // TODO(bugs.webrtc.org/8620): Remove this alias once downstream projects have diff --git a/pc/simulcastdescription.cc b/pc/simulcastdescription.cc new file mode 100644 index 0000000000..eca67a7855 --- /dev/null +++ b/pc/simulcastdescription.cc @@ -0,0 +1,44 @@ +/* + * 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 + +#include "pc/simulcastdescription.h" +#include "rtc_base/checks.h" + +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()); +} + +void SimulcastLayerList::AddLayer(const SimulcastLayer& layer) { + list_.push_back({layer}); +} + +void SimulcastLayerList::AddLayerWithAlternatives( + const std::vector& rids) { + RTC_DCHECK(!rids.empty()); + list_.push_back(rids); +} + +const std::vector& SimulcastLayerList::operator[]( + size_t index) const { + RTC_DCHECK_LT(index, list_.size()); + return list_[index]; +} + +bool SimulcastDescription::empty() const { + return send_layers_.empty() && receive_layers_.empty(); +} + +} // namespace cricket diff --git a/pc/simulcastdescription.h b/pc/simulcastdescription.h new file mode 100644 index 0000000000..f4708ff025 --- /dev/null +++ b/pc/simulcastdescription.h @@ -0,0 +1,104 @@ +/* + * 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 PC_SIMULCASTDESCRIPTION_H_ +#define PC_SIMULCASTDESCRIPTION_H_ + +#include +#include + +namespace cricket { + +// Describes a Simulcast Layer. +// Each simulcast layer has a rid as the identifier and a paused flag. +// See also: https://tools.ietf.org/html/draft-ietf-mmusic-rid-15 for +// an explanation about rids. +struct SimulcastLayer final { + SimulcastLayer(const std::string& rid, bool is_paused); + + SimulcastLayer(const SimulcastLayer& other) = default; + SimulcastLayer& operator=(const SimulcastLayer& other) = default; + + std::string rid; + bool is_paused; +}; + +// Describes a list of Simulcast layers. +// Simulcast layers are specified in order of preference. +// Each layer can have a list of alternatives (in order of preference). +// https://tools.ietf.org/html/draft-ietf-mmusic-sdp-simulcast-13#section-5.1 +// Example Usage: +// To populate a list that specifies the following: +// 1. Layer 1 or Layer 2 +// 2. Layer 3 +// 3. Layer 4 or Layer 5 +// Use the following code: +// SimulcastLayerList list; +// list.AddLayerWithAlternatives( +// {SimulcastLayer("1", false), SimulcastLayer("2", false}); +// list.AddLayer("3"); +// list.AddLayerWithAlternatives( +// {SimulcastLayer("4", false), SimulcastLayer("5", false}); +class SimulcastLayerList final { + public: + // Use to add a layer when there will be no alternatives. + void AddLayer(const SimulcastLayer& layer); + + // Use to add a list of alternatives. + // The alternatives should be specified in order of preference. + void AddLayerWithAlternatives(const std::vector& layers); + + // Read-only access to the contents. + // Note: This object does not allow removal of layers. + std::vector>::const_iterator begin() const { + return list_.begin(); + } + + std::vector>::const_iterator end() const { + return list_.end(); + } + + const std::vector& operator[](size_t index) const; + + size_t size() const { return list_.size(); } + bool empty() const { return list_.empty(); } + + private: + // TODO(amithi, bugs.webrtc.org/10075): + // Validate that rids do not repeat in the list. + std::vector> list_; +}; + +// Describes the simulcast options of a video media section. +// This will list the send and receive layers (along with their alternatives). +// Each simulcast layer has an identifier (rid) and can optionally be paused. +// The order of the layers (as well as alternates) indicates user preference +// from first to last (most preferred to least preferred). +// https://tools.ietf.org/html/draft-ietf-mmusic-sdp-simulcast-13#section-5.1 +class SimulcastDescription final { + public: + const SimulcastLayerList& send_layers() const { return send_layers_; } + SimulcastLayerList& send_layers() { return send_layers_; } + + const SimulcastLayerList& receive_layers() const { return receive_layers_; } + SimulcastLayerList& receive_layers() { return receive_layers_; } + + bool empty() const; + + private: + // TODO(amithi, bugs.webrtc.org/10075): + // Validate that rids do not repeat in send and receive layers. + SimulcastLayerList send_layers_; + SimulcastLayerList receive_layers_; +}; + +} // namespace cricket + +#endif // PC_SIMULCASTDESCRIPTION_H_ diff --git a/pc/webrtcsdp.cc b/pc/webrtcsdp.cc index 0cd6beceef..37d765404e 100644 --- a/pc/webrtcsdp.cc +++ b/pc/webrtcsdp.cc @@ -38,6 +38,7 @@ #include "p2p/base/p2pconstants.h" #include "p2p/base/port.h" #include "pc/mediasession.h" +#include "pc/sdpserializer.h" #include "rtc_base/arraysize.h" #include "rtc_base/checks.h" #include "rtc_base/logging.h" @@ -73,6 +74,7 @@ using cricket::MediaContentDescription; using cricket::MediaType; using cricket::RtpHeaderExtensions; using cricket::MediaProtocolType; +using cricket::SimulcastDescription; using cricket::SsrcGroup; using cricket::StreamParams; using cricket::StreamParamsVec; @@ -162,6 +164,9 @@ static const char kAttributeInactive[] = "inactive"; // draft-ietf-mmusic-sctp-sdp-07 // a=sctp-port static const char kAttributeSctpPort[] = "sctp-port"; +// draft-ietf-mmusic-sdp-simulcast-13 +// a=simulcast +static const char kAttributeSimulcast[] = "simulcast"; // Experimental flags static const char kAttributeXGoogleFlag[] = "x-google-flag"; @@ -1652,6 +1657,17 @@ void BuildRtpContentAttributes(const MediaContentDescription* media_desc, } } } + + // 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(); + InitAttrLine(kAttributeSimulcast, &os); + SdpSerializer serializer; + os << kSdpDelimiterColon + << serializer.SerializeSimulcastDescription(simulcast); + AddLine(os.str(), message); + } } void WriteFmtpHeader(int payload_type, rtc::StringBuilder* os) { @@ -2750,6 +2766,7 @@ bool ParseContent(const std::string& message, std::string ptime_as_string; std::vector stream_ids; std::string track_id; + SimulcastDescription simulcast; // Loop until the next m line while (!IsLineType(message, kLineTypeMedia, *pos)) { @@ -2954,6 +2971,34 @@ bool ParseContent(const std::string& message, return false; } *msid_signaling |= cricket::kMsidSignalingMediaSection; + } else if (HasAttribute(line, kAttributeSimulcast)) { + const size_t kSimulcastPrefixLength = + kLinePrefixLength + arraysize(kAttributeSimulcast); + if (line.size() <= kSimulcastPrefixLength) { + return ParseFailed(line, "Simulcast attribute is empty.", error); + } + + if (!simulcast.empty()) { + return ParseFailed(line, "Multiple Simulcast attributes specified.", + error); + } + + SdpSerializer deserializer; + RTCErrorOr error_or_simulcast = + deserializer.DeserializeSimulcastDescription( + line.substr(kSimulcastPrefixLength)); + if (!error_or_simulcast.ok()) { + return ParseFailed(line, + std::string("Malformed simulcast line: ") + + error_or_simulcast.error().message(), + error); + } + + simulcast = error_or_simulcast.value(); + } else { + // Unrecognized attribute in RTP protocol. + RTC_LOG(LS_INFO) << "Ignored line: " << line; + continue; } } else { // Only parse lines that we are interested of. @@ -3030,6 +3075,13 @@ bool ParseContent(const std::string& message, candidates->push_back( 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 2002230233..d8b6811b7d 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::SimulcastDescription; +using cricket::SimulcastLayer; using cricket::StreamParams; using cricket::STUN_PORT_TYPE; using cricket::TransportDescription; @@ -1314,6 +1316,13 @@ class WebRtcSdpTest : public testing::Test { } } + void CompareSimulcastDescription(const SimulcastDescription& simulcast1, + const SimulcastDescription& simulcast2) { + EXPECT_EQ(simulcast1.send_layers().size(), simulcast2.send_layers().size()); + EXPECT_EQ(simulcast1.receive_layers().size(), + simulcast2.receive_layers().size()); + } + void CompareDataContentDescription(const DataContentDescription* dcd1, const DataContentDescription* dcd2) { EXPECT_EQ(dcd1->use_sctpmap(), dcd2->use_sctpmap()); @@ -1360,6 +1369,10 @@ class WebRtcSdpTest : public testing::Test { const DataContentDescription* dcd2 = c2.media_description()->as_data(); CompareDataContentDescription(dcd1, dcd2); } + + CompareSimulcastDescription( + c1.media_description()->simulcast_description(), + c2.media_description()->simulcast_description()); } // group @@ -3918,3 +3931,55 @@ TEST_F(WebRtcSdpTest, DeserializeEmptySessionName) { Replace("s=-\r\n", "s= \r\n", &sdp); EXPECT_TRUE(SdpDeserialize(sdp, &jsep_desc)); } + +// Simulcast malformed input test for invalid format. +TEST_F(WebRtcSdpTest, DeserializeSimulcastNegative_EmptyAttribute) { + ExpectParseFailureWithNewLines("a=ssrc:3 label:video_track_id_1\r\n", + "a=simulcast:\r\n", "a=simulcast:"); +} + +// Tests that duplicate simulcast entries in the SDP triggers a parse failure. +TEST_F(WebRtcSdpTest, DeserializeSimulcastNegative_DuplicateAttribute) { + ExpectParseFailureWithNewLines("a=ssrc:3 label:video_track_id_1\r\n", + "a=simulcast:send 1\r\na=simulcast:recv 2\r\n", + "a=simulcast:"); +} + +// Validates that deserialization uses the a=simulcast: attribute +TEST_F(WebRtcSdpTest, TestDeserializeSimulcastAttribute) { + std::string sdp = kSdpFullString; + 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()); + EXPECT_EQ(2ul, media->simulcast_description().send_layers().size()); + EXPECT_EQ(3ul, media->simulcast_description().receive_layers().size()); +} + +// Simulcast serialization integration test. +// This test will serialize and deserialize the description and compare. +// More detailed tests for parsing simulcast can be found in +// unit tests for SdpSerializer. +TEST_F(WebRtcSdpTest, SerializeSimulcast_ComplexSerialization) { + MakeUnifiedPlanDescription(); + auto description = jdesc_.description(); + auto media = description->GetContentDescriptionByName(kVideoContentName3); + SimulcastDescription& simulcast = media->simulcast_description(); + simulcast.send_layers().AddLayerWithAlternatives( + {SimulcastLayer("2", false), SimulcastLayer("1", true)}); + simulcast.send_layers().AddLayerWithAlternatives( + {SimulcastLayer("4", false), SimulcastLayer("3", false)}); + + simulcast.receive_layers().AddLayerWithAlternatives( + {SimulcastLayer("6", false), SimulcastLayer("7", false)}); + simulcast.receive_layers().AddLayer(SimulcastLayer("8", true)); + simulcast.receive_layers().AddLayerWithAlternatives( + {SimulcastLayer("9", false), SimulcastLayer("10", true), + SimulcastLayer("11", false)}); + + TestSerialize(jdesc_); +}