394 lines
13 KiB
C++
394 lines
13 KiB
C++
/*
|
|
* 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/sdp_serializer.h"
|
|
|
|
#include <algorithm>
|
|
#include <map>
|
|
#include <string>
|
|
#include <utility>
|
|
#include <vector>
|
|
|
|
#include "absl/algorithm/container.h"
|
|
#include "absl/types/optional.h"
|
|
#include "modules/rtp_rtcp/include/rtp_rtcp_defines.h"
|
|
#include "rtc_base/checks.h"
|
|
#include "rtc_base/string_encode.h"
|
|
#include "rtc_base/string_to_number.h"
|
|
#include "rtc_base/strings/string_builder.h"
|
|
|
|
using cricket::RidDescription;
|
|
using cricket::RidDirection;
|
|
using cricket::SimulcastDescription;
|
|
using cricket::SimulcastLayer;
|
|
using cricket::SimulcastLayerList;
|
|
|
|
namespace webrtc {
|
|
|
|
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 kSendDirection[] = "send";
|
|
const char kReceiveDirection[] = "recv";
|
|
const char kPayloadType[] = "pt";
|
|
|
|
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 (const auto& alternatives : simulcast_layers) {
|
|
if (!first) {
|
|
builder << kDelimiterSemicolon;
|
|
}
|
|
builder << alternatives;
|
|
first = false;
|
|
}
|
|
return builder;
|
|
}
|
|
// 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 )
|
|
// 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 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;
|
|
layers.push_back(SimulcastLayer(rid, paused));
|
|
}
|
|
|
|
result.AddLayerWithAlternatives(layers);
|
|
}
|
|
|
|
return std::move(result);
|
|
}
|
|
|
|
webrtc::RTCError ParseRidPayloadList(const std::string& payload_list,
|
|
RidDescription* rid_description) {
|
|
RTC_DCHECK(rid_description);
|
|
std::vector<int>& payload_types = rid_description->payload_types;
|
|
// Check that the description doesn't have any payload types or restrictions.
|
|
// If the pt= field is specified, it must be first and must not repeat.
|
|
if (!payload_types.empty()) {
|
|
return ParseError("Multiple pt= found in RID Description.");
|
|
}
|
|
if (!rid_description->restrictions.empty()) {
|
|
return ParseError("Payload list must appear first in the restrictions.");
|
|
}
|
|
|
|
// If the pt= field is specified, it must have a value.
|
|
if (payload_list.empty()) {
|
|
return ParseError("Payload list must have at least one value.");
|
|
}
|
|
|
|
// Tokenize the ',' delimited list
|
|
std::vector<std::string> string_payloads;
|
|
rtc::tokenize(payload_list, kDelimiterCommaChar, &string_payloads);
|
|
if (string_payloads.empty()) {
|
|
return ParseError("Payload list must have at least one value.");
|
|
}
|
|
|
|
for (const std::string& payload_type : string_payloads) {
|
|
absl::optional<int> value = rtc::StringToNumber<int>(payload_type);
|
|
if (!value.has_value()) {
|
|
return ParseError("Invalid payload type: " + payload_type);
|
|
}
|
|
|
|
// Check if the value already appears in the payload list.
|
|
if (absl::c_linear_search(payload_types, value.value())) {
|
|
return ParseError("Duplicate payload type in list: " + payload_type);
|
|
}
|
|
payload_types.push_back(value.value());
|
|
}
|
|
|
|
return RTCError::OK();
|
|
}
|
|
|
|
} // namespace
|
|
|
|
std::string SdpSerializer::SerializeSimulcastDescription(
|
|
const cricket::SimulcastDescription& simulcast) const {
|
|
rtc::StringBuilder sb;
|
|
std::string delimiter;
|
|
|
|
if (!simulcast.send_layers().empty()) {
|
|
sb << kSendDirection << kDelimiterSpace << simulcast.send_layers();
|
|
delimiter = kDelimiterSpace;
|
|
}
|
|
|
|
if (!simulcast.receive_layers().empty()) {
|
|
sb << delimiter << kReceiveDirection << 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] != kSendDirection && tokens[0] != kReceiveDirection) ||
|
|
(bidirectional && tokens[2] != kSendDirection &&
|
|
tokens[2] != kReceiveDirection) ||
|
|
(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] != kSendDirection) {
|
|
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);
|
|
}
|
|
|
|
std::string SdpSerializer::SerializeRidDescription(
|
|
const RidDescription& rid_description) const {
|
|
RTC_DCHECK(!rid_description.rid.empty());
|
|
RTC_DCHECK(rid_description.direction == RidDirection::kSend ||
|
|
rid_description.direction == RidDirection::kReceive);
|
|
|
|
rtc::StringBuilder builder;
|
|
builder << rid_description.rid << kDelimiterSpace
|
|
<< (rid_description.direction == RidDirection::kSend
|
|
? kSendDirection
|
|
: kReceiveDirection);
|
|
|
|
const auto& payload_types = rid_description.payload_types;
|
|
const auto& restrictions = rid_description.restrictions;
|
|
|
|
// First property is separated by ' ', the next ones by ';'.
|
|
const char* propertyDelimiter = kDelimiterSpace;
|
|
|
|
// Serialize any codecs in the description.
|
|
if (!payload_types.empty()) {
|
|
builder << propertyDelimiter << kPayloadType << kDelimiterEqual;
|
|
propertyDelimiter = kDelimiterSemicolon;
|
|
const char* formatDelimiter = "";
|
|
for (int payload_type : payload_types) {
|
|
builder << formatDelimiter << payload_type;
|
|
formatDelimiter = kDelimiterComma;
|
|
}
|
|
}
|
|
|
|
// Serialize any restrictions in the description.
|
|
for (const auto& pair : restrictions) {
|
|
// Serialize key=val pairs. =val part is ommitted if val is empty.
|
|
builder << propertyDelimiter << pair.first;
|
|
if (!pair.second.empty()) {
|
|
builder << kDelimiterEqual << pair.second;
|
|
}
|
|
|
|
propertyDelimiter = kDelimiterSemicolon;
|
|
}
|
|
|
|
return builder.str();
|
|
}
|
|
|
|
// https://tools.ietf.org/html/draft-ietf-mmusic-rid-15#section-10
|
|
// Formal Grammar
|
|
// rid-syntax = %s"a=rid:" rid-id SP rid-dir
|
|
// [ rid-pt-param-list / rid-param-list ]
|
|
// rid-id = 1*(alpha-numeric / "-" / "_")
|
|
// rid-dir = %s"send" / %s"recv"
|
|
// rid-pt-param-list = SP rid-fmt-list *( ";" rid-param )
|
|
// rid-param-list = SP rid-param *( ";" rid-param )
|
|
// rid-fmt-list = %s"pt=" fmt *( "," fmt )
|
|
// rid-param = 1*(alpha-numeric / "-") [ "=" param-val ]
|
|
// param-val = *( %x20-58 / %x60-7E )
|
|
// ; Any printable character except semicolon
|
|
RTCErrorOr<RidDescription> SdpSerializer::DeserializeRidDescription(
|
|
absl::string_view string) const {
|
|
std::vector<std::string> tokens;
|
|
rtc::tokenize(std::string(string), kDelimiterSpaceChar, &tokens);
|
|
|
|
if (tokens.size() < 2) {
|
|
return ParseError("RID Description must contain <RID> <direction>.");
|
|
}
|
|
|
|
if (tokens.size() > 3) {
|
|
return ParseError("Invalid RID Description format. Too many arguments.");
|
|
}
|
|
|
|
if (!IsLegalRsidName(tokens[0])) {
|
|
return ParseError("Invalid RID value: " + tokens[0] + ".");
|
|
}
|
|
|
|
if (tokens[1] != kSendDirection && tokens[1] != kReceiveDirection) {
|
|
return ParseError("Invalid RID direction. Supported values: send / recv.");
|
|
}
|
|
|
|
RidDirection direction = tokens[1] == kSendDirection ? RidDirection::kSend
|
|
: RidDirection::kReceive;
|
|
|
|
RidDescription rid_description(tokens[0], direction);
|
|
|
|
// If there is a third argument it is a payload list and/or restriction list.
|
|
if (tokens.size() == 3) {
|
|
std::vector<std::string> restrictions;
|
|
rtc::tokenize(tokens[2], kDelimiterSemicolonChar, &restrictions);
|
|
|
|
// Check for malformed restriction list, such as ';' or ';;;' etc.
|
|
if (restrictions.empty()) {
|
|
return ParseError("Invalid RID restriction list: " + tokens[2]);
|
|
}
|
|
|
|
// Parse the restrictions. The payload indicator (pt) can only appear first.
|
|
for (const std::string& restriction : restrictions) {
|
|
std::vector<std::string> parts;
|
|
rtc::tokenize(restriction, kDelimiterEqualChar, &parts);
|
|
if (parts.empty() || parts.size() > 2) {
|
|
return ParseError("Invalid format for restriction: " + restriction);
|
|
}
|
|
|
|
// |parts| contains at least one value and it does not contain a space.
|
|
// Note: |parts| and other values might still contain tab, newline,
|
|
// unprintable characters, etc. which will not generate errors here but
|
|
// will (most-likely) be ignored by components down stream.
|
|
if (parts[0] == kPayloadType) {
|
|
RTCError error = ParseRidPayloadList(
|
|
parts.size() > 1 ? parts[1] : std::string(), &rid_description);
|
|
if (!error.ok()) {
|
|
return std::move(error);
|
|
}
|
|
|
|
continue;
|
|
}
|
|
|
|
// Parse |parts| as a key=value pair which allows unspecified values.
|
|
if (rid_description.restrictions.find(parts[0]) !=
|
|
rid_description.restrictions.end()) {
|
|
return ParseError("Duplicate restriction specified: " + parts[0]);
|
|
}
|
|
|
|
rid_description.restrictions[parts[0]] =
|
|
parts.size() > 1 ? parts[1] : std::string();
|
|
}
|
|
}
|
|
|
|
return std::move(rid_description);
|
|
}
|
|
|
|
} // namespace webrtc
|