diff --git a/Development/cmake/NmosCppTest.cmake b/Development/cmake/NmosCppTest.cmake index ea7718283..b0b46802a 100644 --- a/Development/cmake/NmosCppTest.cmake +++ b/Development/cmake/NmosCppTest.cmake @@ -60,6 +60,7 @@ set(NMOS_CPP_TEST_NMOS_TEST_SOURCES nmos/test/paging_utils_test.cpp nmos/test/query_api_test.cpp nmos/test/sdp_test_utils.cpp + nmos/test/sdp_temporal_redundancy_test.cpp nmos/test/sdp_utils_test.cpp nmos/test/settings_test.cpp nmos/test/slog_test.cpp diff --git a/Development/nmos/sdp_utils.cpp b/Development/nmos/sdp_utils.cpp index 93eec88d4..d6a5b452c 100644 --- a/Development/nmos/sdp_utils.cpp +++ b/Development/nmos/sdp_utils.cpp @@ -1,5 +1,6 @@ #include "nmos/sdp_utils.h" +#include #include #include #include @@ -445,6 +446,7 @@ namespace nmos const auto& destination_ip = nmos::fields::destination_ip(transport_param); return destination_ip.as_string(); } + } // Make a json representation of an SDP file, e.g. for sdp::make_session_description, from the specified parameters; explicitly specify whether 'source-filter' attributes are included to override the default behaviour @@ -457,8 +459,25 @@ namespace nmos // and the rtp_enabled status does not affect the leg's media description // see https://github.com/AMWA-TV/is-05/issues/109#issuecomment-598721418 + const bool temporal_redundancy = !sdp_params.temporal_redundancy.synchronization_sources.empty(); + + if (temporal_redundancy) + { + if (2 != transport_params.size() || 2 != sdp_params.temporal_redundancy.synchronization_sources.size()) + { + throw details::sdp_creation_error("SMPTE ST 2022-7 temporal redundancy requires two transport parameter legs and two synchronization sources"); + } + if (sdp_params.temporal_redundancy.media_stream_id.empty()) + { + throw details::sdp_creation_error("SMPTE ST 2022-7 temporal redundancy requires a media stream id"); + } + if (transport_params.at(0) != transport_params.at(1)) + { + throw details::sdp_creation_error("SMPTE ST 2022-7 temporal redundancy requires identical transport parameter legs"); + } + } // check to ensure enough media_stream_ids for multi-leg transport_params - if (transport_params.size() > 1 && transport_params.size() > sdp_params.group.media_stream_ids.size()) + else if (transport_params.size() > 1 && transport_params.size() > sdp_params.group.media_stream_ids.size()) { throw details::sdp_creation_error("not enough sdp parameters media stream ids for transport_params"); } @@ -684,8 +703,56 @@ namespace nmos web::json::push_back(media_attributes, fmtp); } + if (temporal_redundancy) + { + auto synchronization_source_ids = value::array(); + for (const auto& synchronization_source : sdp_params.temporal_redundancy.synchronization_sources) + { + web::json::push_back(synchronization_source_ids, value::number(synchronization_source.id)); + web::json::push_back( + media_attributes, value_of({ + { sdp::fields::name, sdp::attributes::ssrc }, + { sdp::fields::value, value_of({ + { sdp::fields::ssrc_id, synchronization_source.id }, + { sdp::fields::attribute, value_of({ + { sdp::fields::name, U("cname") }, + { sdp::fields::value, synchronization_source.cname } + }, keep_order) } + }, keep_order) } + }, keep_order) + ); + } + + web::json::push_back( + media_attributes, value_of({ + { sdp::fields::name, sdp::attributes::ssrc_group }, + { sdp::fields::value, value_of({ + { sdp::fields::semantics, sdp::group_semantics::duplication.name }, + { sdp::fields::ssrc_ids, std::move(synchronization_source_ids) } + }, keep_order) } + }, keep_order) + ); + + if (sdp_params.temporal_redundancy.duplication_delay) + { + web::json::push_back( + media_attributes, value_of({ + { sdp::fields::name, sdp::attributes::duplication_delay }, + { sdp::fields::value, *sdp_params.temporal_redundancy.duplication_delay } + }, keep_order) + ); + } + + web::json::push_back( + media_attributes, value_of({ + { sdp::fields::name, sdp::attributes::mid }, + { sdp::fields::value, sdp_params.temporal_redundancy.media_stream_id } + }, keep_order) + ); + } + // insert "media stream identification" if there is more than 1 leg - if (transport_params.size() > 1) + if (transport_params.size() > 1 && !temporal_redundancy) { // a=mid: // See https://tools.ietf.org/html/rfc5888 @@ -705,6 +772,7 @@ namespace nmos web::json::push_back(media_descriptions, std::move(media_description)); ++leg; + if (temporal_redundancy) break; } // add group attribute if there is more than 1 leg @@ -954,10 +1022,10 @@ namespace nmos // * Any Source Multicast // * Operation with SMPTE 2022-7 - Separate Source Addresses // * Operation with SMPTE 2022-7 - Separate Destination Addresses + // * Operation with SMPTE 2022-7 - Temporal Redundancy // The following cases are not yet handled: // * Operation with SMPTE 2022-5 - // * Operation with SMPTE 2022-7 - Temporal Redundancy // * Operation with RTCP auto& media_descriptions = sdp::fields::media_descriptions(session_description); @@ -1013,10 +1081,21 @@ namespace nmos // take account of the number of source addresses (cf. Operation with SMPTE 2022-7 - Separate Source Addresses) auto& media_attributes = sdp::fields::attributes(media_description); + size_t ssrc_duplication_count = 0; if (!media_attributes.is_null()) { auto& ma = media_attributes.as_array(); + const auto ssrc_group = sdp::find_name(ma, sdp::attributes::ssrc_group); + if (ma.end() != ssrc_group) + { + const auto& value = sdp::fields::value(*ssrc_group); + if (sdp::group_semantics::duplication == sdp::group_semantics_type{ sdp::fields::semantics(value) }) + { + ssrc_duplication_count = sdp::fields::ssrc_ids(value).size(); + } + } + // hmm, this code assumes that is 'incl' and ought to check that , and // match the connection address, and fall back to any "session-level" source-filter values if they don't match @@ -1026,18 +1105,21 @@ namespace nmos auto& sf = sdp::fields::value(*source_filter); auto& sa = sdp::fields::source_addresses(sf); - if (sa.size() <= source_address) + if (sa.size() <= source_address && !(1 == sa.size() && source_address < ssrc_duplication_count)) { source_address -= sa.size(); continue; } details::set_multicast_ip_interface_ip(params, sdp::fields::destination_address(sf)); - params[nmos::fields::source_ip] = sdp::fields::source_addresses(sf).at(source_address); + params[nmos::fields::source_ip] = sdp::fields::source_addresses(sf).at(1 == sa.size() ? 0 : source_address); source_address = 0; } } + // SSRC-multiplexed temporal redundancy uses one media description for both IS-05 legs. + if (0 != source_address && source_address < ssrc_duplication_count) source_address = 0; + if (0 != source_address) { --source_address; @@ -1099,11 +1181,9 @@ namespace nmos } } - // hmm, this code does not handle Synchronization Source (SSRC) level grouping or attributes - // i.e. the 'ssrc-group' attribute or 'ssrc' used to convey e.g. 'fmtp', 'mediaclk' or 'ts-refclk' - // see https://tools.ietf.org/html/rfc7104#section-3.2 - // and https://tools.ietf.org/html/rfc5576 - // and https://www.iana.org/assignments/sdp-parameters/sdp-parameters.xhtml#sdp-att-field + // SSRC-level attributes used to convey e.g. 'fmtp', 'mediaclk' or 'ts-refclk' are not handled. + // SSRC-level duplication grouping and cname attributes are handled below. + // See https://www.iana.org/assignments/sdp-parameters/sdp-parameters.xhtml#sdp-att-field // Group // a=group:[ ]* @@ -1125,6 +1205,60 @@ namespace nmos // See https://tools.ietf.org/html/rfc4566#section-5 const auto& media_descriptions = sdp::fields::media_descriptions(sdp); + // SSRC-level duplication for SMPTE ST 2022-7 temporal redundancy. + // See https://tools.ietf.org/html/rfc5576 and https://tools.ietf.org/html/rfc7104 + for (const auto& media_description : media_descriptions.as_array()) + { + const auto& media_attributes = sdp::fields::attributes(media_description); + if (media_attributes.is_null()) continue; + + const auto& attributes = media_attributes.as_array(); + const auto ssrc_group = sdp::find_name(attributes, sdp::attributes::ssrc_group); + if (attributes.end() == ssrc_group) continue; + + const auto& ssrc_group_value = sdp::fields::value(*ssrc_group); + if (sdp::group_semantics::duplication != sdp::group_semantics_type{ sdp::fields::semantics(ssrc_group_value) }) continue; + if (2 != sdp::fields::ssrc_ids(ssrc_group_value).size()) continue; + + // Separate-source-address redundancy also uses ssrc-group:DUP, but has more than one source address. + const auto source_filter = sdp::find_name(attributes, sdp::attributes::source_filter); + if (attributes.end() != source_filter + && 1 != sdp::fields::source_addresses(sdp::fields::value(*source_filter)).size()) continue; + + for (const auto& id : sdp::fields::ssrc_ids(ssrc_group_value)) + { + const auto id_value = id.as_number().to_uint64(); + if (id_value > (std::numeric_limits::max)()) throw std::out_of_range("SSRC id exceeds 32 bits"); + utility::string_t cname; + for (const auto& attribute : attributes) + { + if (sdp::attributes::ssrc != sdp::fields::name(attribute)) continue; + const auto& value = sdp::fields::value(attribute); + const auto& source_attribute = sdp::fields::attribute(value); + if (id_value == sdp::fields::ssrc_id(value) + && U("cname") == sdp::fields::name(source_attribute) + && !sdp::fields::value(source_attribute).is_null()) + { + cname = sdp::fields::value(source_attribute).as_string(); + break; + } + } + sdp_params.temporal_redundancy.synchronization_sources.push_back({ static_cast(id_value), cname }); + } + + const auto duplication_delay = sdp::find_name(attributes, sdp::attributes::duplication_delay); + if (attributes.end() != duplication_delay) + { + const auto delay = sdp::fields::value(*duplication_delay).as_number().to_uint64(); + if (delay > (std::numeric_limits::max)()) throw std::out_of_range("duplication delay exceeds 32 bits"); + sdp_params.temporal_redundancy.duplication_delay = static_cast(delay); + } + + const auto mid = sdp::find_name(attributes, sdp::attributes::mid); + if (attributes.end() != mid) sdp_params.temporal_redundancy.media_stream_id = sdp::fields::value(*mid).as_string(); + break; + } + // ts-refclk attributes // See https://tools.ietf.org/html/rfc7273 sdp_params.ts_refclk = boost::copy_range>(media_descriptions.as_array() | boost::adaptors::transformed([&sdp](const value& media_description) -> sdp_parameters::ts_refclk_t diff --git a/Development/nmos/sdp_utils.h b/Development/nmos/sdp_utils.h index 59f5bed71..ede965f9b 100644 --- a/Development/nmos/sdp_utils.h +++ b/Development/nmos/sdp_utils.h @@ -107,6 +107,7 @@ namespace nmos // with each RTP sender and receiver. // When redundancy is being used, the media description and media-level attributes for each stream are assumed // to be identical except for the values corresponding to the IS-05 transport parameters for each leg. + // SSRC-multiplexed temporal redundancy is represented by one media description and two transport parameter legs. struct sdp_parameters { // Origin ("o=") @@ -170,6 +171,31 @@ namespace nmos group_t(const sdp::group_semantics_type& semantics, const std::vector& media_stream_ids) : semantics(semantics), media_stream_ids(media_stream_ids) {} } group; + // SSRC-level duplication for SMPTE ST 2022-7 temporal redundancy. + // See https://tools.ietf.org/html/rfc5576 and https://tools.ietf.org/html/rfc7104 + struct temporal_redundancy_t + { + struct synchronization_source_t + { + uint32_t id; + utility::string_t cname; + + synchronization_source_t() : id() {} + synchronization_source_t(uint32_t id, const utility::string_t& cname) : id(id), cname(cname) {} + }; + + std::vector synchronization_sources; + bst::optional duplication_delay; + utility::string_t media_stream_id; + + temporal_redundancy_t() {} + temporal_redundancy_t(const std::vector& synchronization_sources, bst::optional duplication_delay, const utility::string_t& media_stream_id) + : synchronization_sources(synchronization_sources) + , duplication_delay(duplication_delay) + , media_stream_id(media_stream_id) + {} + } temporal_redundancy; + // Media ("m=") // See https://tools.ietf.org/html/rfc4566#section-5.14 sdp::media_type media_type; diff --git a/Development/nmos/test/sdp_temporal_redundancy_test.cpp b/Development/nmos/test/sdp_temporal_redundancy_test.cpp new file mode 100644 index 000000000..76f04ac01 --- /dev/null +++ b/Development/nmos/test/sdp_temporal_redundancy_test.cpp @@ -0,0 +1,110 @@ +#include "nmos/sdp_utils.h" + +#include "bst/test/test.h" +#include "nmos/json_fields.h" +#include "sdp/sdp.h" + +//////////////////////////////////////////////////////////////////////////////////////////// +BST_TEST_CASE(testInterpretationOfSdpFilesTemporalRedundancy) +{ + using web::json::value_of; + + // See https://specs.amwa.tv/is-05/releases/v1.1.1/docs/4.1._Behaviour_-_RTP_Transport_Type.html#temporal-redundancy + + const std::string test_sdp = R"(v=0 +o=ali 1122334455 1122334466 IN IP4 dup.example.com +s=Delayed Duplication +t=0 0 +m=video 30000 RTP/AVP 100 +c=IN IP4 233.252.0.1/127 +a=source-filter:incl IN IP4 233.252.0.1 198.51.100.1 +a=rtpmap:100 MP2T/90000 +a=ssrc:1000 cname:ch1a@example.com +a=ssrc:1010 cname:ch1a@example.com +a=ssrc-group:DUP 1000 1010 +a=duplication-delay:50 +a=mid:Ch1 +)"; + + const auto leg = value_of({ + { nmos::fields::source_ip, U("198.51.100.1") }, + { nmos::fields::multicast_ip, U("233.252.0.1") }, + { nmos::fields::interface_ip, U("auto") }, + { nmos::fields::destination_port, 30000 }, + { nmos::fields::rtp_enabled, true } + }); + const auto test_params = value_of({ leg, leg }); + + const auto session_description = sdp::parse_session_description(test_sdp); + BST_REQUIRE(test_params == nmos::get_session_description_transport_params(session_description)); + + const auto& media_description = sdp::fields::media_descriptions(session_description).at(0); + const auto& attributes = sdp::fields::attributes(media_description).as_array(); + const auto synchronization_source = sdp::find_name(attributes, sdp::attributes::ssrc); + BST_REQUIRE(attributes.end() != synchronization_source); + const auto& source_attribute = sdp::fields::attribute(sdp::fields::value(*synchronization_source)); + BST_REQUIRE_EQUAL(U("cname"), sdp::fields::name(source_attribute)); + BST_REQUIRE_EQUAL(U("ch1a@example.com"), sdp::fields::value(source_attribute).as_string()); + + const auto sdp_params = nmos::get_session_description_sdp_parameters(session_description); + BST_REQUIRE_EQUAL(2, sdp_params.temporal_redundancy.synchronization_sources.size()); + BST_REQUIRE_EQUAL(1000, sdp_params.temporal_redundancy.synchronization_sources.at(0).id); + BST_REQUIRE_EQUAL(U("ch1a@example.com"), sdp_params.temporal_redundancy.synchronization_sources.at(1).cname); + BST_REQUIRE(bool(sdp_params.temporal_redundancy.duplication_delay)); + BST_REQUIRE_EQUAL(50, *sdp_params.temporal_redundancy.duplication_delay); + BST_REQUIRE_EQUAL(U("Ch1"), sdp_params.temporal_redundancy.media_stream_id); + + auto invalid_sdp = test_sdp; + const std::string ssrc_group = "a=ssrc-group:DUP 1000 1010"; + invalid_sdp.replace(invalid_sdp.find(ssrc_group), ssrc_group.size(), "a=ssrc-group:DUP"); + BST_REQUIRE_THROW(sdp::parse_session_description(invalid_sdp), std::runtime_error); + + invalid_sdp = test_sdp; + const std::string ssrc = "a=ssrc:1000 cname:ch1a@example.com"; + invalid_sdp.replace(invalid_sdp.find(ssrc), ssrc.size(), "a=ssrc:1000"); + BST_REQUIRE_THROW(sdp::parse_session_description(invalid_sdp), std::runtime_error); +} + +//////////////////////////////////////////////////////////////////////////////////////////// +BST_TEST_CASE(testSdpTransportParamsTemporalRedundancyRoundtrip) +{ + using web::json::value_of; + + auto test_sdp_params = nmos::sdp_parameters(U("Delayed Duplication"), nmos::sdp_parameters::mux_t{}, 100); + test_sdp_params.temporal_redundancy = { + { + { 1000, U("ch1a@example.com") }, + { 1010, U("ch1a@example.com") } + }, + 50, + U("Ch1") + }; + + const auto sender_leg = value_of({ + { nmos::fields::source_ip, U("198.51.100.1") }, + { nmos::fields::destination_ip, U("233.252.0.1") }, + { nmos::fields::source_port, 30000 }, + { nmos::fields::destination_port, 30000 }, + { nmos::fields::rtp_enabled, true } + }); + const auto receiver_leg = value_of({ + { nmos::fields::source_ip, U("198.51.100.1") }, + { nmos::fields::multicast_ip, U("233.252.0.1") }, + { nmos::fields::interface_ip, U("auto") }, + { nmos::fields::destination_port, 30000 }, + { nmos::fields::rtp_enabled, true } + }); + + const auto session_description = nmos::make_session_description(test_sdp_params, value_of({ sender_leg, sender_leg })); + BST_REQUIRE_EQUAL(1, sdp::fields::media_descriptions(session_description).size()); + BST_REQUIRE(value_of({ receiver_leg, receiver_leg }) == nmos::get_session_description_transport_params(session_description)); + + const auto text = sdp::make_session_description(session_description); + BST_REQUIRE(std::string::npos != text.find("a=ssrc-group:DUP 1000 1010")); + BST_REQUIRE(std::string::npos != text.find("a=duplication-delay:50")); + BST_REQUIRE(value_of({ receiver_leg, receiver_leg }) == nmos::get_session_description_transport_params(sdp::parse_session_description(text))); + + auto different_leg = sender_leg; + different_leg[nmos::fields::destination_port] = web::json::value::number(30002); + BST_REQUIRE_THROW(nmos::make_session_description(test_sdp_params, value_of({ sender_leg, different_leg })), std::logic_error); +} diff --git a/Development/sdp/json.h b/Development/sdp/json.h index 39da70878..7624624d6 100644 --- a/Development/sdp/json.h +++ b/Development/sdp/json.h @@ -140,6 +140,11 @@ namespace sdp const utility::string_t group{ U("group") }; const utility::string_t mid{ U("mid") }; + // See https://tools.ietf.org/html/rfc5576 and https://tools.ietf.org/html/rfc7104 + const utility::string_t ssrc{ U("ssrc") }; + const utility::string_t ssrc_group{ U("ssrc-group") }; + const utility::string_t duplication_delay{ U("duplication-delay") }; + // See https://tools.ietf.org/html/rfc7273 const utility::string_t ts_refclk{ U("ts-refclk") }; const utility::string_t mediaclk{ U("mediaclk") }; @@ -173,6 +178,14 @@ namespace sdp const web::json::field_as_string semantics{ U("semantics") }; // see sdp::group_semantics const web::json::field_as_array mids{ U("mids") }; + // a=ssrc: [:] + // a=ssrc-group: ... + // See https://tools.ietf.org/html/rfc5576 and https://tools.ietf.org/html/rfc7104 + const web::json::field ssrc_id{ U("ssrc_id") }; + const web::json::field_as_value attribute{ U("attribute") }; + const web::json::field_as_array ssrc_ids{ U("ssrc_ids") }; + const web::json::field duplication_delay{ U("duplication_delay") }; + // a=source-filter: // See https://tools.ietf.org/html/rfc4570 const web::json::field_as_string filter_mode{ U("filter_mode") }; // see sdp::filter_modes diff --git a/Development/sdp/sdp_grammar.cpp b/Development/sdp/sdp_grammar.cpp index 16e03be19..25b559e23 100644 --- a/Development/sdp/sdp_grammar.cpp +++ b/Development/sdp/sdp_grammar.cpp @@ -203,6 +203,7 @@ namespace sdp } const converter strings_converter = array_converter(string_converter, " "); + const converter digits_array_converter = array_converter(digits_converter, " "); // ST 2110-20:2022 says "the section shall consist of a sequence of // media type parameter entries, separated by the semicolon (";") character followed by whitespace" @@ -676,6 +677,54 @@ namespace sdp } }, { sdp::attributes::mid, string_converter }, + // See https://tools.ietf.org/html/rfc5576 + { + sdp::attributes::ssrc, // [:] + { + [](const web::json::value& v) { + std::string s; + s += digits_converter.format(v.at(sdp::fields::ssrc_id)); + const auto& attribute = sdp::fields::attribute(v); + s += " " + string_converter.format(attribute.at(sdp::fields::name)); + const auto& attribute_value = sdp::fields::value(attribute); + if (!attribute_value.is_null()) s += ":" + string_converter.format(attribute_value); + return s; + }, + [](const std::string& s) { + auto v = web::json::value::object(keep_order); + size_t pos = 0; + v[sdp::fields::ssrc_id] = digits_converter.parse(substr_find(s, pos, " ")); + if (std::string::npos == pos) throw sdp_parse_error("expected a value for " + utility::us2s(sdp::fields::attribute)); + const auto attribute_text = substr_find(s, pos); + const auto colon = attribute_text.find(':'); + auto attribute = web::json::value_of({ + { sdp::fields::name, string_converter.parse(attribute_text.substr(0, colon)) } + }, keep_order); + if (std::string::npos != colon) attribute[sdp::fields::value] = string_converter.parse(attribute_text.substr(colon + 1)); + v[sdp::fields::attribute] = std::move(attribute); + return v; + }, + } + }, + // See https://tools.ietf.org/html/rfc5576 and https://tools.ietf.org/html/rfc7104 + { + sdp::attributes::ssrc_group, // ... + { + [](const web::json::value& v) { + return string_converter.format(v.at(sdp::fields::semantics)) + " " + digits_array_converter.format(v.at(sdp::fields::ssrc_ids)); + }, + [](const std::string& s) { + auto v = web::json::value::object(keep_order); + size_t pos = 0; + v[sdp::fields::semantics] = string_converter.parse(substr_find(s, pos, " ")); + if (std::string::npos == pos) throw sdp_parse_error("expected a value for " + utility::us2s(sdp::fields::ssrc_ids)); + v[sdp::fields::ssrc_ids] = digits_array_converter.parse(substr_find(s, pos)); + return v; + }, + } + }, + // See https://datatracker.ietf.org/doc/html/draft-ietf-mmusic-delayed-duplication + { sdp::attributes::duplication_delay, digits_converter }, // See https://tools.ietf.org/html/rfc7273 { sdp::attributes::ts_refclk,