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 <amithi@webrtc.org>
Reviewed-by: Seth Hampson <shampson@webrtc.org>
Reviewed-by: Steve Anton <steveanton@webrtc.org>
Cr-Commit-Position: refs/heads/master@{#25883}
diff --git a/pc/BUILD.gn b/pc/BUILD.gn
index 6999ddd..89c4df8 100644
--- a/pc/BUILD.gn
+++ b/pc/BUILD.gn
@@ -56,6 +56,8 @@
"rtptransportinternaladapter.h",
"sessiondescription.cc",
"sessiondescription.h",
+ "simulcastdescription.cc",
+ "simulcastdescription.h",
"srtpfilter.cc",
"srtpfilter.h",
"srtpsession.cc",
@@ -163,6 +165,8 @@
"rtptransceiver.h",
"sctputils.cc",
"sctputils.h",
+ "sdpserializer.cc",
+ "sdpserializer.h",
"sdputils.cc",
"sdputils.h",
"statscollector.cc",
@@ -456,6 +460,7 @@
"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 0000000..b5a1d87
--- /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 <string>
+#include <utility>
+#include <vector>
+
+#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<SimulcastLayer>& 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<SimulcastLayerList> ParseSimulcastLayerList(const std::string& str) {
+ std::vector<std::string> 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<std::string> rid_tokens;
+ rtc::tokenize_with_empty_tokens(token, kDelimiterCommaChar, &rid_tokens);
+ if (rid_tokens.empty()) {
+ return ParseError("Simulcast alternative layer list is malformed.");
+ }
+
+ std::vector<SimulcastLayer> layers;
+ for (const auto& rid_token : rid_tokens) {
+ 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:<send> <streams> <recv> <streams>
+// 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<SimulcastDescription> SdpSerializer::DeserializeSimulcastDescription(
+ absl::string_view string) const {
+ std::vector<std::string> tokens;
+ rtc::tokenize(std::string(string), kDelimiterSpaceChar, &tokens);
+
+ if (tokens.size() != 2 && tokens.size() != 4) {
+ return ParseError("Must have one or two <direction, streams> 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<SimulcastLayerList> 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 0000000..428b420
--- /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 <string>
+
+#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:<configuration>.
+// The SDP serializtion code (webrtcsdp.h) should use |SdpSerializer| to
+// serialize and deserialize the <configuration> 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<cricket::SimulcastDescription> 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 0000000..8b10da1
--- /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 <string>
+#include <vector>
+
+#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<const char*> {
+ public:
+ // Runs a test for deserializing Simulcast.
+ // |str| - The serialized Simulcast to parse.
+ // |expected| - The expected output Simulcast to compare to.
+ void TestSimulcastDeserialization(
+ const std::string& str,
+ const SimulcastDescription& expected) const {
+ 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<SimulcastLayer>& expected,
+ const std::vector<SimulcastLayer>& actual) const {
+ EXPECT_EQ(expected.size(), actual.size());
+ for (size_t i = 0; i < expected.size(); i++) {
+ EXPECT_EQ(expected[i].rid, actual[i].rid);
+ EXPECT_EQ(expected[i].is_paused, actual[i].is_paused);
+ }
+ }
+
+ // Checks that the two SimulcastLayerLists are equal.
+ void ExpectEqual(const SimulcastLayerList& expected,
+ const SimulcastLayerList& actual) const {
+ EXPECT_EQ(expected.size(), actual.size());
+ for (size_t i = 0; i < expected.size(); i++) {
+ ExpectEqual(expected[i], actual[i]);
+ }
+ }
+
+ // Checks that the two SimulcastDescriptions are equal.
+ void ExpectEqual(const SimulcastDescription& expected,
+ const SimulcastDescription& actual) const {
+ ExpectEqual(expected.send_layers(), actual.send_layers());
+ ExpectEqual(expected.receive_layers(), actual.receive_layers());
+ }
+};
+
+// Test Cases
+
+// Test simple deserialization with no alternative streams.
+TEST_F(SdpSerializerTest, DeserializeSimulcast_SimpleCaseNoAlternatives) {
+ 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 691046f..ff6cd2f 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 @@
}
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 @@
// 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 0000000..eca67a7
--- /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 <utility>
+
+#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<SimulcastLayer>& rids) {
+ RTC_DCHECK(!rids.empty());
+ list_.push_back(rids);
+}
+
+const std::vector<SimulcastLayer>& 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 0000000..f4708ff
--- /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 <string>
+#include <vector>
+
+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<SimulcastLayer>& layers);
+
+ // Read-only access to the contents.
+ // Note: This object does not allow removal of layers.
+ std::vector<std::vector<SimulcastLayer>>::const_iterator begin() const {
+ return list_.begin();
+ }
+
+ std::vector<std::vector<SimulcastLayer>>::const_iterator end() const {
+ return list_.end();
+ }
+
+ const std::vector<SimulcastLayer>& 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<std::vector<SimulcastLayer>> 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 0cd6bec..37d7654 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::MediaType;
using cricket::RtpHeaderExtensions;
using cricket::MediaProtocolType;
+using cricket::SimulcastDescription;
using cricket::SsrcGroup;
using cricket::StreamParams;
using cricket::StreamParamsVec;
@@ -162,6 +164,9 @@
// 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 @@
}
}
}
+
+ // 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 @@
std::string ptime_as_string;
std::vector<std::string> stream_ids;
std::string track_id;
+ SimulcastDescription simulcast;
// Loop until the next m line
while (!IsLineType(message, kLineTypeMedia, *pos)) {
@@ -2954,6 +2971,34 @@
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<SimulcastDescription> 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 @@
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 2002230..d8b6811 100644
--- a/pc/webrtcsdp_unittest.cc
+++ b/pc/webrtcsdp_unittest.cc
@@ -60,6 +60,8 @@
using cricket::RELAY_PORT_TYPE;
using cricket::SessionDescription;
using cricket::MediaProtocolType;
+using cricket::SimulcastDescription;
+using cricket::SimulcastLayer;
using cricket::StreamParams;
using cricket::STUN_PORT_TYPE;
using cricket::TransportDescription;
@@ -1314,6 +1316,13 @@
}
}
+ 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 @@
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 @@
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_);
+}