diff --git a/Development/sdp/sdp_grammar.cpp b/Development/sdp/sdp_grammar.cpp index 25b559e23..cc925a76d 100644 --- a/Development/sdp/sdp_grammar.cpp +++ b/Development/sdp/sdp_grammar.cpp @@ -139,7 +139,7 @@ namespace sdp }; } - converter array_converter(const converter& converter, const std::string& delimiter = " ") + converter array_converter(const converter& converter, const std::string& delimiter) { return{ [=](const web::json::value& v) { @@ -211,7 +211,7 @@ namespace sdp // in other RFCs and SMPTE standards are inconsistent, so allow additional whitespace const converter named_values_converter = array_converter(key_value_converter('=', { sdp::fields::name, string_converter }, { sdp::fields::value, string_converter }), "; ", "[ \\t]*(;[ \\t]*|$)"); - converter object_converter(const std::vector>& field_converters, const std::string& delimiter = " ") + converter object_converter(const std::vector>& field_converters, const std::string& delimiter) { return{ [=](const web::json::value& v) { diff --git a/Development/sdp/sdp_grammar.h b/Development/sdp/sdp_grammar.h index 0ac97c362..0c3bac627 100644 --- a/Development/sdp/sdp_grammar.h +++ b/Development/sdp/sdp_grammar.h @@ -26,6 +26,45 @@ namespace sdp parser parse; }; + // converters for the value types used by this grammar, and generators for compound + // values, which may also be used to define additional attribute converters + // see https://tools.ietf.org/html/rfc4566#section-9 + // none of these, or get_default_attribute_converters, default_attribute_converter + // or session_description below, are safe to use during static initialization; + // prefer a function-local static for application-defined grammars + + // + extern const converter string_converter; + + // a number, e.g. in "a=framerate:" + extern const converter number_converter; + + // 1*DIGIT + extern const converter digits_converter; + + // 1*DIGIT, represented as a numeric string, for values which may exceed the range + // of a number, e.g. + extern const converter big_digits_converter; + + // a space-separated list of + extern const converter strings_converter; + + // a semicolon-separated list of [=], e.g. + extern const converter named_values_converter; + + // [] + converter key_value_converter(char separator, const std::pair& key_converter, const std::pair& value_converter); + + // a delimited list of values, represented as a json array + converter array_converter(const converter& converter, const std::string& delimiter = " "); + + // identical to above except that parse_delimiter is a regex pattern + converter array_converter(const converter& converter, const std::string& format_delimiter, const std::string& parse_delimiter); + + // a delimited sequence of values, represented as a json object with the specified field names + // an empty field name indicates the values of the converted json object are merged into the result + converter object_converter(const std::vector>& field_converters, const std::string& delimiter = " "); + // "An SDP session description consists of a number of lines of text of // the form: = // where MUST be exactly one case-significant character and @@ -80,7 +119,12 @@ namespace sdp // See https://tools.ietf.org/html/rfc4566#section-6 attribute_converters get_default_attribute_converters(); + // converter for an attribute with no specific converter, which treats the as an opaque + // and an attribute with no as a property attribute + extern const converter default_attribute_converter; + // construct a grammar with the specified attribute converters + // the converters and default_converter are captured by reference and must outlive the returned grammar description session_description(const attribute_converters& converters, const converter& default_converter); } diff --git a/Development/sdp/test/sdp_test.cpp b/Development/sdp/test/sdp_test.cpp index ee6cf52af..23fec2cdf 100644 --- a/Development/sdp/test/sdp_test.cpp +++ b/Development/sdp/test/sdp_test.cpp @@ -3,6 +3,7 @@ #include "bst/test/test.h" #include "sdp/json.h" +#include "sdp/sdp_grammar.h" //////////////////////////////////////////////////////////////////////////////////////////// BST_TEST_CASE(testSdpRoundtrip) @@ -1120,3 +1121,93 @@ a=mid:SECONDARY } while (!expected.fail() && !actual.fail()); } } + +//////////////////////////////////////////////////////////////////////////////////////////// +namespace +{ + // an example application-defined attribute + // a=x-example-foo: + const utility::string_t example_foo{ U("x-example-foo") }; + + namespace example_fields + { + const web::json::field_as_integer format{ U("format") }; + const web::json::field_as_string bar{ U("bar") }; + } + + // session_description captures converters by reference, so they must outlive + // the returned grammar; a function-local static is the natural application pattern + const sdp::grammar::attribute_converters& example_attribute_converters() + { + static const auto converters = [] { + auto converters = sdp::grammar::get_default_attribute_converters(); + converters[example_foo] = sdp::grammar::object_converter({ + { example_fields::format, sdp::grammar::digits_converter }, + { example_fields::bar, sdp::grammar::string_converter } + }); + return converters; + }(); + return converters; + } + + const sdp::grammar::description& example_grammar() + { + static const auto grammar = sdp::grammar::session_description( + example_attribute_converters(), + sdp::grammar::default_attribute_converter + ); + return grammar; + } +} + +BST_TEST_CASE(testSdpApplicationDefinedAttributes) +{ + const auto& grammar = example_grammar(); + + const std::string test_sdp = R"(v=0 +o=- 3745911798 3745911798 IN IP4 192.168.9.142 +s=Example Sender 1 (Video) +t=0 0 +a=x-example-foo:0 session-level +m=video 50020 RTP/AVP 96 +c=IN IP4 239.22.142.1/32 +a=rtpmap:96 raw/90000 +a=x-example-foo:96 media-level +a=x-example-bar:unknown attributes are still handled by the default converter +a=recvonly +)"; + + auto session_description = sdp::parse_session_description(test_sdp, grammar); + + auto& session_attributes = sdp::fields::attributes(session_description).as_array(); + auto session_foo = sdp::find_name(session_attributes, example_foo); + BST_REQUIRE(session_attributes.end() != session_foo); + BST_REQUIRE_EQUAL(0, example_fields::format(sdp::fields::value(*session_foo))); + BST_REQUIRE_EQUAL(U("session-level"), example_fields::bar(sdp::fields::value(*session_foo))); + + auto& media_attributes = sdp::fields::attributes(sdp::fields::media_descriptions(session_description).at(0)).as_array(); + auto media_foo = sdp::find_name(media_attributes, example_foo); + BST_REQUIRE(media_attributes.end() != media_foo); + BST_REQUIRE_EQUAL(96, example_fields::format(sdp::fields::value(*media_foo))); + BST_REQUIRE_EQUAL(U("media-level"), example_fields::bar(sdp::fields::value(*media_foo))); + + auto media_bar = sdp::find_name(media_attributes, U("x-example-bar")); + BST_REQUIRE(media_attributes.end() != media_bar); + BST_REQUIRE_EQUAL(U("unknown attributes are still handled by the default converter"), sdp::fields::value(*media_bar).as_string()); + + auto recvonly = sdp::find_name(media_attributes, sdp::attributes::recvonly); + BST_REQUIRE(media_attributes.end() != recvonly); + BST_REQUIRE(sdp::fields::value(*recvonly).is_null()); + + auto test_sdp2 = sdp::make_session_description(session_description, grammar); + std::istringstream expected(test_sdp), actual(test_sdp2); + do + { + std::string expected_line, actual_line; + std::getline(expected, expected_line); + std::getline(actual, actual_line); + // CR cannot appear in a raw string literal, so remove it from the actual line + if (!actual_line.empty() && '\r' == actual_line.back()) actual_line.pop_back(); + BST_CHECK_EQUAL(expected_line, actual_line); + } while (!expected.fail() && !actual.fail()); +}