diff --git a/Development/nmos-cpp-node/node_implementation.cpp b/Development/nmos-cpp-node/node_implementation.cpp index 92814fe03..a2fbca2c2 100644 --- a/Development/nmos-cpp-node/node_implementation.cpp +++ b/Development/nmos-cpp-node/node_implementation.cpp @@ -2105,6 +2105,13 @@ nmos::connection_resource_auto_resolver make_node_implementation_auto_resolver(c const bool smpte2022_7 = 1 < transport_params.size(); nmos::details::resolve_auto(transport_params[0], nmos::fields::interface_ip, [&] { return web::json::front(nmos::fields::constraint_enum(constraints.at(0).at(nmos::fields::interface_ip))); }); if (smpte2022_7) nmos::details::resolve_auto(transport_params[1], nmos::fields::interface_ip, [&] { return web::json::back(nmos::fields::constraint_enum(constraints.at(1).at(nmos::fields::interface_ip))); }); + for (auto& params : transport_params.as_array()) + { + // The IS-05 receiver schema defines "auto" as the highest available number of dimensions. + nmos::details::resolve_auto(params, nmos::fields::fec_mode, [&] { + return value::string(!nmos::fields::fec2D_destination_port(params).is_null() ? U("2D") : U("1D")); + }); + } // lastly, apply the specification defaults for any properties not handled above nmos::resolve_rtp_auto(id_type.second, transport_params); } diff --git a/Development/nmos/json_fields.h b/Development/nmos/json_fields.h index 118dc0380..075307e42 100644 --- a/Development/nmos/json_fields.h +++ b/Development/nmos/json_fields.h @@ -151,6 +151,7 @@ namespace nmos const web::json::field_as_value_or destination_ip{ U("destination_ip"), {} }; // string const web::json::field_as_value_or source_port{ U("source_port"), {} }; // string or integer const web::json::field_as_bool_or rtp_enabled{ U("rtp_enabled"), false }; + const web::json::field_as_bool_or fec_enabled{ U("fec_enabled"), false }; const web::json::field_as_value_or fec_destination_ip{ U("fec_destination_ip"), {} }; // string const web::json::field_as_value_or fec1D_destination_port{ U("fec1D_destination_port"), {} }; // string or integer const web::json::field_as_value_or fec2D_destination_port{ U("fec2D_destination_port"), {} }; // string or integer diff --git a/Development/nmos/sdp_utils.cpp b/Development/nmos/sdp_utils.cpp index 93eec88d4..16389f683 100644 --- a/Development/nmos/sdp_utils.cpp +++ b/Development/nmos/sdp_utils.cpp @@ -521,6 +521,28 @@ namespace nmos size_t leg = 0; for (const auto& transport_param : transport_params.as_array()) { + const sdp_parameters::fec_t* fec = nullptr; + size_t fec_repair_flow_count = 0; + if (nmos::fields::fec_enabled(transport_param)) + { + const auto fec_parameters = 1 < transport_params.size() + ? std::find_if(sdp_params.fec.begin(), sdp_params.fec.end(), [&](const sdp_parameters::fec_t& parameters) + { return sdp_params.group.media_stream_ids[leg] == parameters.media_stream_id; }) + : sdp_params.fec.begin(); + if (sdp_params.fec.end() == fec_parameters) throw details::sdp_creation_error("missing explicit RFC 6364 FEC parameters for transport leg"); + fec = &*fec_parameters; + + const auto& fec_mode = nmos::fields::fec_mode(transport_param); + const auto& fec2D_destination_port = nmos::fields::fec2D_destination_port(transport_param); + if (fec_mode.is_null() || U("1D") == fec_mode.as_string()) fec_repair_flow_count = 1; + else if (U("2D") == fec_mode.as_string()) fec_repair_flow_count = 2; + else if (U("auto") == fec_mode.as_string()) fec_repair_flow_count = !fec2D_destination_port.is_null() ? 2 : 1; + else throw details::sdp_creation_error("unsupported fec_mode"); + + if (fec->repair_flows.size() < fec_repair_flow_count) throw details::sdp_creation_error("not enough explicit RFC 6364 repair-flow parameters"); + if (fec->media_stream_id.empty()) throw details::sdp_creation_error("missing FEC source media stream id"); + } + const auto& connection_address = details::get_connection_address(transport_param); const auto& address_type_multicast = details::get_address_type_multicast(connection_address); @@ -684,26 +706,125 @@ namespace nmos web::json::push_back(media_attributes, fmtp); } - // insert "media stream identification" if there is more than 1 leg - if (transport_params.size() > 1) + utility::string_t source_media_stream_id; + if (transport_params.size() > 1) source_media_stream_id = sdp_params.group.media_stream_ids[leg]; + if (fec) + { + if (!source_media_stream_id.empty() && source_media_stream_id != fec->media_stream_id) + throw details::sdp_creation_error("FEC and redundancy media stream ids differ for transport leg"); + source_media_stream_id = fec->media_stream_id; + + auto source_flow = value_of({ + { sdp::fields::source_id, fec->source_id }, + { fec->tag_length ? sdp::fields::tag_length.key : U(""), fec->tag_length ? *fec->tag_length : 0 } + }, keep_order); + web::json::push_back(media_attributes, value_of({ + { sdp::fields::name, sdp::attributes::fec_source_flow }, + { sdp::fields::value, std::move(source_flow) } + }, keep_order)); + } + + if (!source_media_stream_id.empty()) { // a=mid: // See https://tools.ietf.org/html/rfc5888 - const auto& mid = sdp_params.group.media_stream_ids[leg]; + if (transport_params.size() > 1) web::json::push_back(mids, source_media_stream_id); - // build up mids based on group::media_stream_ids - web::json::push_back(mids, mid); - - web::json::push_back( - media_attributes, value_of({ - { sdp::fields::name, sdp::attributes::mid }, - { sdp::fields::value, mid } - }, keep_order) - ); + web::json::push_back(media_attributes, value_of({ + { sdp::fields::name, sdp::attributes::mid }, + { sdp::fields::value, source_media_stream_id } + }, keep_order)); } web::json::push_back(media_descriptions, std::move(media_description)); + if (fec) + { + const auto& fec_destination_ip = nmos::fields::fec_destination_ip(transport_param); + if (fec_destination_ip.is_null() || !fec_destination_ip.is_string() || U("auto") == fec_destination_ip.as_string()) + throw details::sdp_creation_error("fec_destination_ip must be resolved before creating SDP"); + const auto& repair_connection_address = fec_destination_ip.as_string(); + const auto& repair_address_type_multicast = details::get_address_type_multicast(repair_connection_address); + + for (size_t repair = 0; repair < fec_repair_flow_count; ++repair) + { + const auto& repair_parameters = fec->repair_flows[repair]; + if (repair_parameters.media_stream_id.empty()) throw details::sdp_creation_error("missing FEC repair media stream id"); + const auto& destination_port = 0 == repair + ? nmos::fields::fec1D_destination_port(transport_param) + : nmos::fields::fec2D_destination_port(transport_param); + if (!destination_port.is_number()) throw details::sdp_creation_error("FEC repair destination port must be resolved before creating SDP"); + + auto repair_flow = value_of({ + { sdp::fields::encoding_id, repair_parameters.encoding_id } + }, keep_order); + if (repair_parameters.preference_level) repair_flow[sdp::fields::preference_level] = value::number(*repair_parameters.preference_level); + + const auto make_scheme_specific = [&](const sdp_parameters::fec_t::scheme_specific_t& elements) + { + auto result = value::array(); + for (const auto& element : elements) + { + web::json::push_back(result, value_of({ + { sdp::fields::name, element.first }, + { sdp::fields::value, element.second } + }, keep_order)); + } + return result; + }; + if (!repair_parameters.sender_side_scheme_specific.empty()) repair_flow[sdp::fields::sender_side_scheme_specific] = make_scheme_specific(repair_parameters.sender_side_scheme_specific); + if (!repair_parameters.scheme_specific.empty()) repair_flow[sdp::fields::scheme_specific] = make_scheme_specific(repair_parameters.scheme_specific); + + auto repair_attributes = value_of({ + value_of({ + { sdp::fields::name, sdp::attributes::fec_repair_flow }, + { sdp::fields::value, std::move(repair_flow) } + }, keep_order) + }); + if (repair_parameters.repair_window) + { + web::json::push_back(repair_attributes, value_of({ + { sdp::fields::name, sdp::attributes::repair_window }, + { sdp::fields::value, value_of({ + { sdp::fields::window_size, repair_parameters.repair_window->size }, + { sdp::fields::window_unit, repair_parameters.repair_window->unit.name } + }, keep_order) } + }, keep_order)); + } + web::json::push_back(repair_attributes, value_of({ + { sdp::fields::name, sdp::attributes::mid }, + { sdp::fields::value, repair_parameters.media_stream_id } + }, keep_order)); + + web::json::push_back(media_descriptions, value_of({ + { sdp::fields::media, value_of({ + { sdp::fields::media_type, sdp::media_types::application.name }, + { sdp::fields::port, destination_port }, + { sdp::fields::protocol, sdp::protocols::UDP_FEC.name }, + { sdp::fields::formats, value::array() } + }, keep_order) }, + { sdp::fields::connection_data, value_of({ + value_of({ + { sdp::fields::network_type, sdp::network_types::internet.name }, + { sdp::fields::address_type, repair_address_type_multicast.first.name }, + { sdp::fields::connection_address, sdp::address_types::IP4 == repair_address_type_multicast.first && repair_address_type_multicast.second + ? repair_connection_address + U("/") + utility::ostringstreamed(sdp_params.connection_data.ttl) + : repair_connection_address } + }, keep_order) + }) }, + { sdp::fields::attributes, std::move(repair_attributes) } + }, keep_order)); + + web::json::push_back(session_attributes, value_of({ + { sdp::fields::name, sdp::attributes::group }, + { sdp::fields::value, value_of({ + { sdp::fields::semantics, sdp::group_semantics::fec_fr.name }, + { sdp::fields::mids, value_of({ source_media_stream_id, repair_parameters.media_stream_id }) } + }, keep_order) } + }, keep_order)); + } + } + ++leg; } @@ -935,6 +1056,155 @@ namespace nmos params[nmos::fields::interface_ip] = value::string(address); } } + + struct is05_fec_repair_flow + { + sdp_parameters::fec_t::repair_flow_t parameters; + utility::string_t destination_ip; + uint64_t destination_port; + + is05_fec_repair_flow(const sdp_parameters::fec_t::repair_flow_t& parameters, const utility::string_t& destination_ip, uint64_t destination_port) + : parameters(parameters) + , destination_ip(destination_ip) + , destination_port(destination_port) + {} + }; + + struct is05_fec_source_flow + { + sdp_parameters::fec_t parameters; + std::vector repair_flows; + }; + + utility::string_t get_media_stream_id(const web::json::value& media_description) + { + const auto& attributes = sdp::fields::attributes(media_description).as_array(); + const auto mid = sdp::find_name(attributes, sdp::attributes::mid); + return attributes.end() != mid ? sdp::fields::value(*mid).as_string() : utility::string_t{}; + } + + sdp_parameters::fec_t::scheme_specific_t get_fec_scheme_specific(const web::json::array& elements) + { + return boost::copy_range(elements | boost::adaptors::transformed([](const web::json::value& element) + { + return sdp_parameters::fec_t::scheme_specific_t::value_type{ sdp::fields::name(element), sdp::fields::value(element).as_string() }; + })); + } + + utility::string_t get_media_connection_address(const web::json::value& session_description, const web::json::value& media_description) + { + const web::json::value* connection_data = &sdp::fields::connection_data(media_description); + if (connection_data->is_null() || 0 == connection_data->size()) connection_data = &sdp::fields::connection_data(session_description); + if (connection_data->is_null() || 0 == connection_data->size()) throw sdp_processing_error("missing FEC repair-flow connection data"); + + const auto& connection = connection_data->is_array() ? connection_data->at(0) : *connection_data; + const auto address_type = sdp::address_type{ sdp::fields::address_type(connection) }; + return parse_connection_address(address_type, sdp::fields::connection_address(connection)).base_address; + } + + std::vector get_is05_fec_source_flows(const web::json::value& session_description) + { + const auto& session_attributes = sdp::fields::attributes(session_description).as_array(); + const auto has_fec_group = session_attributes.end() != std::find_if(session_attributes.begin(), session_attributes.end(), [](const web::json::value& attribute) + { + return sdp::attributes::group == sdp::fields::name(attribute) + && sdp::group_semantics::fec_fr == sdp::group_semantics_type{ sdp::fields::semantics(sdp::fields::value(attribute)) }; + }); + if (!has_fec_group) return{}; + + const auto& media_descriptions = sdp::fields::media_descriptions(session_description).as_array(); + std::map media_by_mid; + for (const auto& media_description : media_descriptions) + { + const auto mid = get_media_stream_id(media_description); + if (mid.empty()) continue; + const auto inserted = media_by_mid.insert({ mid, &media_description }); + if (!inserted.second) inserted.first->second = nullptr; + } + + std::map source_flows; + for (const auto& attribute : session_attributes) + { + if (sdp::attributes::group != sdp::fields::name(attribute)) continue; + const auto& group = sdp::fields::value(attribute); + if (sdp::group_semantics::fec_fr != sdp::group_semantics_type{ sdp::fields::semantics(group) }) continue; + + const auto& mids = sdp::fields::mids(group); + // Other RFC 6364 topologies are intentionally left to the generic sdp/ representation. + if (2 != mids.size()) continue; + + const auto source_mid = mids.at(0).as_string(); + const auto repair_mid = mids.at(1).as_string(); + const auto source = media_by_mid.find(source_mid); + const auto repair = media_by_mid.find(repair_mid); + if (media_by_mid.end() == source || media_by_mid.end() == repair) throw sdp_processing_error("FEC group references a missing media description"); + if (!source->second || !repair->second) throw sdp_processing_error("FEC group references a duplicate media stream id"); + + const auto& source_media = sdp::fields::media(*source->second); + const auto source_protocol = sdp::protocol{ sdp::fields::protocol(source_media) }; + const auto source_media_type = sdp::media_type{ sdp::fields::media_type(source_media) }; + if (sdp::protocols::RTP_AVP != source_protocol || !(sdp::media_types::video == source_media_type || sdp::media_types::audio == source_media_type)) continue; + + const auto& repair_media = sdp::fields::media(*repair->second); + if (sdp::protocols::UDP_FEC != sdp::protocol{ sdp::fields::protocol(repair_media) } + || sdp::media_types::application != sdp::media_type{ sdp::fields::media_type(repair_media) }) + { + throw sdp_processing_error("IS-05 FEC repair flow must use application UDP/FEC"); + } + + const auto& source_attributes = sdp::fields::attributes(*source->second).as_array(); + const auto source_flow_attribute = sdp::find_name(source_attributes, sdp::attributes::fec_source_flow); + if (source_attributes.end() == source_flow_attribute) throw sdp_processing_error("missing fec-source-flow attribute"); + const auto& source_flow_value = sdp::fields::value(*source_flow_attribute); + + auto source_flow = source_flows.find(source_mid); + if (source_flows.end() == source_flow) + { + is05_fec_source_flow value; + value.parameters.source_id = (uint32_t)sdp::fields::source_id(source_flow_value); + if (source_flow_value.has_field(sdp::fields::tag_length)) value.parameters.tag_length = sdp::fields::tag_length(source_flow_value); + value.parameters.media_stream_id = source_mid; + source_flow = source_flows.insert({ source_mid, std::move(value) }).first; + } + + if (2 <= source_flow->second.repair_flows.size()) throw sdp_processing_error("IS-05 supports at most two FEC repair flows per RTP leg"); + if (source_flow->second.repair_flows.end() != std::find_if(source_flow->second.repair_flows.begin(), source_flow->second.repair_flows.end(), [&](const is05_fec_repair_flow& flow) + { return repair_mid == flow.parameters.media_stream_id; })) throw sdp_processing_error("duplicate FEC repair flow"); + + const auto& repair_attributes = sdp::fields::attributes(*repair->second).as_array(); + const auto repair_flow_attribute = sdp::find_name(repair_attributes, sdp::attributes::fec_repair_flow); + if (repair_attributes.end() == repair_flow_attribute) throw sdp_processing_error("missing fec-repair-flow attribute"); + const auto& repair_flow_value = sdp::fields::value(*repair_flow_attribute); + + sdp_parameters::fec_t::repair_flow_t repair_parameters; + repair_parameters.encoding_id = sdp::fields::encoding_id(repair_flow_value); + if (repair_flow_value.has_field(sdp::fields::preference_level)) repair_parameters.preference_level = sdp::fields::preference_level(repair_flow_value); + if (repair_flow_value.has_field(sdp::fields::sender_side_scheme_specific)) repair_parameters.sender_side_scheme_specific = get_fec_scheme_specific(sdp::fields::sender_side_scheme_specific(repair_flow_value)); + if (repair_flow_value.has_field(sdp::fields::scheme_specific)) repair_parameters.scheme_specific = get_fec_scheme_specific(sdp::fields::scheme_specific(repair_flow_value)); + repair_parameters.media_stream_id = repair_mid; + + const auto repair_window = sdp::find_name(repair_attributes, sdp::attributes::repair_window); + if (repair_attributes.end() != repair_window) + { + const auto& repair_window_value = sdp::fields::value(*repair_window); + repair_parameters.repair_window = sdp_parameters::fec_t::repair_window_t{ + (uint32_t)sdp::fields::window_size(repair_window_value), + sdp::repair_window_unit{ sdp::fields::window_unit(repair_window_value) } + }; + } + + source_flow->second.parameters.repair_flows.push_back(repair_parameters); + source_flow->second.repair_flows.push_back({ repair_parameters, get_media_connection_address(session_description, *repair->second), sdp::fields::port(repair_media) }); + } + + std::vector result; + for (const auto& media_description : media_descriptions) + { + const auto source = source_flows.find(get_media_stream_id(media_description)); + if (source_flows.end() != source) result.push_back(source->second); + } + return result; + } } // Get IS-05 transport parameters from the json representation of an SDP file, e.g. from sdp::parse_session_description @@ -952,15 +1222,16 @@ namespace nmos // * Unicast // * Source Specific Multicast // * Any Source Multicast + // * Operation with SMPTE 2022-5 // * Operation with SMPTE 2022-7 - Separate Source Addresses // * Operation with SMPTE 2022-7 - Separate Destination Addresses // 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); + const auto fec_source_flows = details::get_is05_fec_source_flows(session_description); for (size_t leg = 0; leg < 2; ++leg) { @@ -1048,6 +1319,28 @@ namespace nmos params[nmos::fields::rtp_enabled] = value::boolean(true); + const auto media_stream_id = details::get_media_stream_id(media_description); + const auto fec_source_flow = std::find_if(fec_source_flows.begin(), fec_source_flows.end(), [&](const details::is05_fec_source_flow& flow) + { return media_stream_id == flow.parameters.media_stream_id; }); + if (fec_source_flows.end() != fec_source_flow) + { + if (fec_source_flow->repair_flows.empty()) throw details::sdp_processing_error("FEC source flow has no repair flow"); + const auto& fec_destination_ip = fec_source_flow->repair_flows.front().destination_ip; + if (fec_source_flow->repair_flows.end() != std::find_if(fec_source_flow->repair_flows.begin(), fec_source_flow->repair_flows.end(), [&](const details::is05_fec_repair_flow& flow) + { return fec_destination_ip != flow.destination_ip; })) + { + throw details::sdp_processing_error("IS-05 cannot represent FEC repair flows with different destination addresses"); + } + + params[nmos::fields::fec_enabled] = value::boolean(true); + params[nmos::fields::fec_mode] = value::string(U("auto")); + params[nmos::fields::fec_destination_ip] = value::string(fec_destination_ip); + params[nmos::fields::fec1D_destination_port] = value::number(fec_source_flow->repair_flows.at(0).destination_port); + params[nmos::fields::fec2D_destination_port] = 1 < fec_source_flow->repair_flows.size() + ? value::number(fec_source_flow->repair_flows.at(1).destination_port) + : value::null(); + } + web::json::push_back(transport_params, params); break; @@ -1109,7 +1402,13 @@ namespace nmos // a=group:[ ]* // See https://tools.ietf.org/html/rfc5888 auto& session_attributes = sdp::fields::attributes(sdp).as_array(); - auto group = sdp::find_name(session_attributes, sdp::attributes::group); + // RFC 6364 FEC groups are represented separately below; retain the non-FEC + // grouping (normally DUP for ST 2022-7) in the existing group member. + auto group = std::find_if(session_attributes.begin(), session_attributes.end(), [](const value& attribute) + { + return sdp::attributes::group == sdp::fields::name(attribute) + && sdp::group_semantics::fec_fr != sdp::group_semantics_type{ sdp::fields::semantics(sdp::fields::value(attribute)) }; + }); if (session_attributes.end() != group) { const auto& value = sdp::fields::value(*group); @@ -1125,9 +1424,21 @@ namespace nmos // See https://tools.ietf.org/html/rfc4566#section-5 const auto& media_descriptions = sdp::fields::media_descriptions(sdp); + // IS-05-compatible RFC 6364 FEC source and repair flows + const auto fec_source_flows = details::get_is05_fec_source_flows(sdp); + sdp_params.fec = boost::copy_range>(fec_source_flows | boost::adaptors::transformed([](const details::is05_fec_source_flow& flow) + { + return flow.parameters; + })); + // 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 + sdp_params.ts_refclk = boost::copy_range>(media_descriptions.as_array() + | boost::adaptors::filtered([](const value& media_description) + { + return sdp::protocols::RTP_AVP == sdp::protocol{ sdp::fields::protocol(sdp::fields::media(media_description)) }; + }) + | boost::adaptors::transformed([&sdp](const value& media_description) -> sdp_parameters::ts_refclk_t { auto& media_attributes = sdp::fields::attributes(media_description).as_array(); auto ts_refclk = sdp::find_name(media_attributes, sdp::attributes::ts_refclk); @@ -1158,8 +1469,12 @@ namespace nmos // hmm, for simplicity, the remainder of this code assumes that format-related information must be the same // in every media description, so reads it only from the first one! - if (0 == media_descriptions.size()) throw details::sdp_processing_error("missing media descriptions"); - const auto& media_description = media_descriptions.at(0); + const auto media_description_ = std::find_if(media_descriptions.as_array().begin(), media_descriptions.as_array().end(), [](const value& media_description) + { + return sdp::protocols::RTP_AVP == sdp::protocol{ sdp::fields::protocol(sdp::fields::media(media_description)) }; + }); + if (media_descriptions.as_array().end() == media_description_) throw details::sdp_processing_error("missing RTP media description"); + const auto& media_description = *media_description_; // Connection Data // get default multicast_ip via Connection Data diff --git a/Development/nmos/sdp_utils.h b/Development/nmos/sdp_utils.h index 59f5bed71..b0bfeb2e4 100644 --- a/Development/nmos/sdp_utils.h +++ b/Development/nmos/sdp_utils.h @@ -170,6 +170,61 @@ 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; + // Forward Error Correction (FEC) Framework parameters for the IS-05-compatible + // topology of one source flow and one or two repair flows per RTP leg. + // RFC-only values are explicit because they cannot be derived from IS-05 transport parameters. + // See https://tools.ietf.org/html/rfc6364 + struct fec_t + { + typedef std::vector> scheme_specific_t; + + struct repair_window_t + { + uint32_t size; + sdp::repair_window_unit unit; + + repair_window_t() : size() {} + repair_window_t(uint32_t size, const sdp::repair_window_unit& unit) : size(size), unit(unit) {} + }; + + struct repair_flow_t + { + uint64_t encoding_id; + bst::optional preference_level; + scheme_specific_t sender_side_scheme_specific; + scheme_specific_t scheme_specific; + bst::optional repair_window; + utility::string_t media_stream_id; + + repair_flow_t() : encoding_id() {} + repair_flow_t(uint64_t encoding_id, const utility::string_t& media_stream_id, + const scheme_specific_t& sender_side_scheme_specific = {}, const scheme_specific_t& scheme_specific = {}, + bst::optional preference_level = bst::nullopt, bst::optional repair_window = bst::nullopt) + : encoding_id(encoding_id) + , preference_level(preference_level) + , sender_side_scheme_specific(sender_side_scheme_specific) + , scheme_specific(scheme_specific) + , repair_window(repair_window) + , media_stream_id(media_stream_id) + {} + }; + + uint32_t source_id; + bst::optional tag_length; + utility::string_t media_stream_id; + std::vector repair_flows; + + fec_t() : source_id() {} + fec_t(uint32_t source_id, const utility::string_t& media_stream_id, const std::vector& repair_flows, + bst::optional tag_length = bst::nullopt) + : source_id(source_id) + , tag_length(tag_length) + , media_stream_id(media_stream_id) + , repair_flows(repair_flows) + {} + }; + std::vector fec; + // Media ("m=") // See https://tools.ietf.org/html/rfc4566#section-5.14 sdp::media_type media_type; diff --git a/Development/nmos/test/sdp_utils_test.cpp b/Development/nmos/test/sdp_utils_test.cpp index 0d34eae33..f6834ff92 100644 --- a/Development/nmos/test/sdp_utils_test.cpp +++ b/Development/nmos/test/sdp_utils_test.cpp @@ -377,6 +377,156 @@ a=rtpmap:103 raw/90000 BST_REQUIRE(test_params == transport_params); } +//////////////////////////////////////////////////////////////////////////////////////////// +BST_TEST_CASE(testInterpretationOfSdpFilesFec) +{ + using web::json::value; + using web::json::value_of; + + // See https://specs.amwa.tv/is-05/releases/v1.1.1/docs/4.1._Behaviour_-_RTP_Transport_Type.html#operation-with-smpte-2022-5 + const std::string test_sdp = R"(v=0 +o=ali 1122334455 1122334466 IN IP4 172.29.26.24 +s=FEC Framework Examples +t=0 0 +a=group:FEC-FR S1 R1 +m=video 30000 RTP/AVP 100 +c=IN IP4 233.252.0.1/127 +a=rtpmap:100 MP2T/90000 +a=fec-source-flow: id=0 +a=mid:S1 +m=application 30000 UDP/FEC +c=IN IP4 233.252.0.2/127 +a=fec-repair-flow: encoding-id=10; ss-fssi=n:7,k:5 +a=mid:R1 +)"; + + const auto expected_transport_params = value_of({ + value_of({ + { nmos::fields::source_ip, value::null() }, + { 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 }, + { nmos::fields::fec_enabled, true }, + { nmos::fields::fec_mode, U("auto") }, + { nmos::fields::fec_destination_ip, U("233.252.0.2") }, + { nmos::fields::fec1D_destination_port, 30000 }, + { nmos::fields::fec2D_destination_port, value::null() } + }) + }); + + const auto session_description = sdp::parse_session_description(test_sdp); + auto parsed = nmos::parse_session_description(session_description); + BST_REQUIRE(expected_transport_params == parsed.second); + + BST_REQUIRE_EQUAL(1, parsed.first.fec.size()); + const auto& fec = parsed.first.fec.at(0); + BST_CHECK_EQUAL(0, fec.source_id); + BST_CHECK_EQUAL(U("S1"), fec.media_stream_id); + BST_REQUIRE_EQUAL(1, fec.repair_flows.size()); + BST_CHECK_EQUAL(10, fec.repair_flows.at(0).encoding_id); + BST_CHECK_EQUAL(U("R1"), fec.repair_flows.at(0).media_stream_id); + BST_REQUIRE_EQUAL(2, fec.repair_flows.at(0).sender_side_scheme_specific.size()); + BST_CHECK_EQUAL(U("n"), fec.repair_flows.at(0).sender_side_scheme_specific.at(0).first); + BST_CHECK_EQUAL(U("7"), fec.repair_flows.at(0).sender_side_scheme_specific.at(0).second); + + // A Receiver must resolve interface_ip before using the parsed parameters to create SDP. + parsed.second[0][nmos::fields::interface_ip] = value::string(U("172.29.26.24")); + const auto made_sdp = nmos::make_session_description(parsed.first, parsed.second); + const auto roundtripped = nmos::parse_session_description(sdp::parse_session_description(sdp::make_session_description(made_sdp))); + BST_REQUIRE(expected_transport_params == roundtripped.second); + BST_REQUIRE_EQUAL(1, roundtripped.first.fec.size()); + BST_CHECK_EQUAL(U("R1"), roundtripped.first.fec.at(0).repair_flows.at(0).media_stream_id); +} + +//////////////////////////////////////////////////////////////////////////////////////////// +BST_TEST_CASE(testSdpTransportParamsFec2D) +{ + using web::json::value_of; + + nmos::sdp_parameters sdp_params{ + U("FEC 2D"), + sdp::media_types::video, + { 100, U("MP2T"), 90000 } + }; + sdp_params.connection_data.ttl = 127; + nmos::sdp_parameters::fec_t::repair_flow_t first_repair{ 10, U("R1"), { { U("n"), U("7") }, { U("k"), U("5") } } }; + first_repair.repair_window = nmos::sdp_parameters::fec_t::repair_window_t{ 200, sdp::repair_window_units::milliseconds }; + nmos::sdp_parameters::fec_t::repair_flow_t second_repair{ 11, U("R2"), { { U("t"), U("3") } }, {}, 1 }; + second_repair.repair_window = nmos::sdp_parameters::fec_t::repair_window_t{ 150500, sdp::repair_window_units::microseconds }; + sdp_params.fec.push_back({ 0, U("S1"), { first_repair, second_repair } }); + + const auto sender_transport_params = value_of({ + value_of({ + { nmos::fields::source_ip, U("192.0.2.1") }, + { nmos::fields::destination_ip, U("233.252.0.1") }, + { nmos::fields::source_port, 5004 }, + { nmos::fields::destination_port, 30000 }, + { nmos::fields::rtp_enabled, true }, + { nmos::fields::fec_enabled, true }, + { nmos::fields::fec_mode, U("2D") }, + { nmos::fields::fec_destination_ip, U("233.252.0.2") }, + { nmos::fields::fec1D_destination_port, 30002 }, + { nmos::fields::fec2D_destination_port, 30004 } + }) + }); + + const auto session_description = sdp::parse_session_description(sdp::make_session_description(nmos::make_session_description(sdp_params, sender_transport_params))); + const auto& media_descriptions = sdp::fields::media_descriptions(session_description); + BST_REQUIRE_EQUAL(3, media_descriptions.size()); + + const auto parsed = nmos::parse_session_description(session_description); + BST_REQUIRE_EQUAL(1, parsed.second.size()); + BST_CHECK(nmos::fields::fec_enabled(parsed.second.at(0))); + BST_CHECK_EQUAL(U("233.252.0.2"), nmos::fields::fec_destination_ip(parsed.second.at(0)).as_string()); + BST_CHECK_EQUAL(30002, nmos::fields::fec1D_destination_port(parsed.second.at(0)).as_integer()); + BST_CHECK_EQUAL(30004, nmos::fields::fec2D_destination_port(parsed.second.at(0)).as_integer()); + BST_REQUIRE_EQUAL(2, parsed.first.fec.at(0).repair_flows.size()); + BST_REQUIRE((bool)parsed.first.fec.at(0).repair_flows.at(1).repair_window); + BST_CHECK_EQUAL(150500, parsed.first.fec.at(0).repair_flows.at(1).repair_window->size); +} + +//////////////////////////////////////////////////////////////////////////////////////////// +BST_TEST_CASE(testSdpTransportParamsFecUnsupportedAndMalformedGroups) +{ + const std::string before = R"(v=0 +o=- 0 0 IN IP4 192.0.2.1 +s=FEC +t=0 0 +)"; + const std::string source = R"(m=video 30000 RTP/AVP 100 +c=IN IP4 233.252.0.1/127 +a=rtpmap:100 MP2T/90000 +a=fec-source-flow: id=0 +a=mid:S1 +)"; + const std::string repair1 = R"(m=application 30002 UDP/FEC +c=IN IP4 233.252.0.2/127 +a=fec-repair-flow: encoding-id=10 +a=mid:R1 +)"; + const std::string repair2_different_address = R"(m=application 30004 UDP/FEC +c=IN IP4 233.252.0.3/127 +a=fec-repair-flow: encoding-id=11 +a=mid:R2 +)"; + + // Multiple-source RFC 6364 groups are valid at the sdp/ layer but cannot be represented by IS-05 transport parameters. + const auto unsupported = sdp::parse_session_description(before + "a=group:FEC-FR S1 S2 R1\n" + source + repair1); + const auto unsupported_transport_params = nmos::get_session_description_transport_params(unsupported); + BST_REQUIRE_EQUAL(1, unsupported_transport_params.size()); + BST_CHECK(!unsupported_transport_params.at(0).has_field(nmos::fields::fec_enabled)); + + const auto missing_media = sdp::parse_session_description(before + "a=group:FEC-FR S1 MISSING\n" + source); + BST_REQUIRE_THROW(nmos::get_session_description_transport_params(missing_media), std::runtime_error); + + const auto different_addresses = sdp::parse_session_description(before + + "a=group:FEC-FR S1 R1\n" + + "a=group:FEC-FR S1 R2\n" + + source + repair1 + repair2_different_address); + BST_REQUIRE_THROW(nmos::get_session_description_transport_params(different_addresses), std::runtime_error); +} + //////////////////////////////////////////////////////////////////////////////////////////// BST_TEST_CASE(testInterpretationOfSdpFilesSeparateSourceAddresses) { diff --git a/Development/sdp/json.h b/Development/sdp/json.h index 39da70878..f658fed96 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/rfc6364 + const utility::string_t fec_source_flow{ U("fec-source-flow") }; + const utility::string_t fec_repair_flow{ U("fec-repair-flow") }; + const utility::string_t repair_window{ U("repair-window") }; + // 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,19 @@ 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=fec-source-flow: id=[; tag-len=] + // a=fec-repair-flow: encoding-id=[; preference-lvl=][; ss-fssi=][; fssi=] + // a=repair-window: + // See https://tools.ietf.org/html/rfc6364 + const web::json::field source_id{ U("source_id") }; + const web::json::field tag_length{ U("tag_length") }; + const web::json::field encoding_id{ U("encoding_id") }; + const web::json::field preference_level{ U("preference_level") }; + const web::json::field_as_array sender_side_scheme_specific{ U("sender_side_scheme_specific") }; + const web::json::field_as_array scheme_specific{ U("scheme_specific") }; + const web::json::field window_size{ U("window_size") }; + const web::json::field_as_string window_unit{ U("window_unit") }; // see sdp::repair_window_units + // 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 @@ -283,6 +301,10 @@ namespace sdp const protocol RTP_SAVP{ U("RTP/SAVP") }; // unspecified protocol running over UDP const protocol udp{ U("udp") }; + // FEC Framework input and repair packet formats + // See https://tools.ietf.org/html/rfc6364#section-4.1 + const protocol FEC_UDP{ U("FEC/UDP") }; + const protocol UDP_FEC{ U("UDP/FEC") }; } // Bandwidth Specifiers @@ -364,6 +386,20 @@ namespace sdp { // See https://tools.ietf.org/html/rfc7104 const group_semantics_type duplication{ U("DUP") }; + // See https://tools.ietf.org/html/rfc5956 and https://tools.ietf.org/html/rfc6364 + const group_semantics_type fec_fr{ U("FEC-FR") }; + } +} + +// Forward Error Correction (FEC) Framework Repair Window Units +// See https://tools.ietf.org/html/rfc6364#section-4.6 +namespace sdp +{ + DEFINE_STRING_ENUM(repair_window_unit) + namespace repair_window_units + { + const repair_window_unit milliseconds{ U("ms") }; + const repair_window_unit microseconds{ U("us") }; } } diff --git a/Development/sdp/sdp_grammar.cpp b/Development/sdp/sdp_grammar.cpp index 16e03be19..dbbb99566 100644 --- a/Development/sdp/sdp_grammar.cpp +++ b/Development/sdp/sdp_grammar.cpp @@ -1,6 +1,7 @@ #include "sdp/sdp_grammar.h" #include "sdp/sdp.h" +#include #include #include "bst/regex.h" #include "cpprest/basic_utils.h" @@ -118,6 +119,76 @@ namespace sdp const converter big_digits_converter{ jns2s, digits2jns }; + converter bounded_digits_converter(uint64_t minimum, uint64_t maximum, const std::string& description) + { + return{ + [=](const web::json::value& v) { + const auto value = v.as_number().to_uint64(); + if (value < minimum || maximum < value) throw sdp_format_error("expected " + description); + return jn2s(v); + }, + [=](const std::string& s) { + if (s.empty()) throw sdp_parse_error("expected " + description); + + uint64_t value = 0; + for (const auto c : s) + { + if (c < '0' || '9' < c) throw sdp_parse_error("expected " + description); + const auto digit = (uint64_t)(c - '0'); + if (((std::numeric_limits::max)() - digit) / 10 < value) throw sdp_parse_error("expected " + description); + value = value * 10 + digit; + } + + if (value < minimum || maximum < value) throw sdp_parse_error("expected " + description); + return web::json::value::number(value); + } + }; + } + + const converter uint32_digits_converter = bounded_digits_converter(0, (std::numeric_limits::max)(), "a 32-bit non-negative integer"); + const converter positive_uint32_digits_converter = bounded_digits_converter(1, (std::numeric_limits::max)(), "a positive 32-bit integer"); + const converter nonnegative_digits_converter = bounded_digits_converter(0, (std::numeric_limits::max)(), "a non-negative integer"); + const converter positive_digits_converter = bounded_digits_converter(1, (std::numeric_limits::max)(), "a positive integer"); + const converter fec_encoding_id_converter = bounded_digits_converter(0, 255, "a FEC Encoding ID in the range 0 to 255"); + + bool is_fec_element_character(char c) + { + const auto uc = (unsigned char)c; + const std::string separators{ "()<>@,;:\\\"/[]?={}" }; + return 0x20 < uc && uc < 0x7f && std::string::npos == separators.find(c); + } + + void validate_fec_element_text(const std::string& text, bool allow_empty, bool formatting) + { + if ((!allow_empty && text.empty()) || !std::all_of(text.begin(), text.end(), is_fec_element_character)) + { + if (formatting) throw sdp_format_error("invalid FEC scheme-specific element"); + else throw sdp_parse_error("invalid FEC scheme-specific element"); + } + } + + const converter fec_element_converter{ + [](const web::json::value& v) { + const auto name = utility::us2s(sdp::fields::name(v)); + const auto value = utility::us2s(sdp::fields::value(v).as_string()); + validate_fec_element_text(name, false, true); + validate_fec_element_text(value, true, true); + return name + ':' + value; + }, + [](const std::string& s) { + const auto colon = s.find(':'); + if (std::string::npos == colon) throw sdp_parse_error("expected ':' in FEC scheme-specific element"); + const auto name = s.substr(0, colon); + const auto value = s.substr(colon + 1); + validate_fec_element_text(name, false, false); + validate_fec_element_text(value, true, false); + return web::json::value_of({ + { sdp::fields::name, utility::s2us(name) }, + { sdp::fields::value, utility::s2us(value) } + }, keep_order); + } + }; + // [] converter key_value_converter(char separator, const std::pair& key_converter, const std::pair& value_converter) { @@ -203,6 +274,7 @@ namespace sdp } const converter strings_converter = array_converter(string_converter, " "); + const converter fec_elements_converter = array_converter(fec_element_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" @@ -210,6 +282,112 @@ 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]*|$)"); + // See https://tools.ietf.org/html/rfc6364#section-4.4 + const converter fec_source_flow_converter{ + [](const web::json::value& v) { + std::string s = " id=" + uint32_digits_converter.format(v.at(sdp::fields::source_id)); + if (v.has_field(sdp::fields::tag_length)) s += "; tag-len=" + positive_digits_converter.format(v.at(sdp::fields::tag_length)); + return s; + }, + [](const std::string& s) { + const std::string source_id_prefix{ " id=" }; + const std::string tag_length_prefix{ "tag-len=" }; + if (0 != s.compare(0, source_id_prefix.size(), source_id_prefix)) throw sdp_parse_error("expected ' id='"); + + const auto separator = s.find("; ", source_id_prefix.size()); + auto v = web::json::value::object(keep_order); + v[sdp::fields::source_id] = uint32_digits_converter.parse(s.substr(source_id_prefix.size(), separator - source_id_prefix.size())); + if (std::string::npos != separator) + { + const auto tag_length = s.substr(separator + 2); + if (0 != tag_length.compare(0, tag_length_prefix.size(), tag_length_prefix)) throw sdp_parse_error("expected 'tag-len='"); + v[sdp::fields::tag_length] = positive_digits_converter.parse(tag_length.substr(tag_length_prefix.size())); + } + return v; + } + }; + + // See https://tools.ietf.org/html/rfc6364#section-4.5 + const converter fec_repair_flow_converter{ + [](const web::json::value& v) { + std::string s = " encoding-id=" + fec_encoding_id_converter.format(v.at(sdp::fields::encoding_id)); + if (v.has_field(sdp::fields::preference_level)) s += "; preference-lvl=" + nonnegative_digits_converter.format(v.at(sdp::fields::preference_level)); + if (v.has_field(sdp::fields::sender_side_scheme_specific)) + { + const auto elements = fec_elements_converter.format(v.at(sdp::fields::sender_side_scheme_specific)); + if (elements.empty()) throw sdp_format_error("expected at least one sender-side FSSI element"); + s += "; ss-fssi=" + elements; + } + if (v.has_field(sdp::fields::scheme_specific)) + { + const auto elements = fec_elements_converter.format(v.at(sdp::fields::scheme_specific)); + if (elements.empty()) throw sdp_format_error("expected at least one FSSI element"); + s += "; fssi=" + elements; + } + return s; + }, + [](const std::string& s) { + std::vector parameters; + size_t pos = 0; + while (std::string::npos != pos) + { + const auto parameter = substr_find(s, pos, "; "); + if (parameter.empty()) throw sdp_parse_error("unexpected FEC repair-flow delimiter"); + parameters.push_back(parameter); + } + + const std::string encoding_id_prefix{ " encoding-id=" }; + const std::string preference_level_prefix{ "preference-lvl=" }; + const std::string sender_side_scheme_specific_prefix{ "ss-fssi=" }; + const std::string scheme_specific_prefix{ "fssi=" }; + if (parameters.empty() || 0 != parameters[0].compare(0, encoding_id_prefix.size(), encoding_id_prefix)) throw sdp_parse_error("expected ' encoding-id='"); + + auto v = web::json::value::object(keep_order); + v[sdp::fields::encoding_id] = fec_encoding_id_converter.parse(parameters[0].substr(encoding_id_prefix.size())); + + size_t parameter = 1; + if (parameter < parameters.size() && 0 == parameters[parameter].compare(0, preference_level_prefix.size(), preference_level_prefix)) + { + v[sdp::fields::preference_level] = nonnegative_digits_converter.parse(parameters[parameter].substr(preference_level_prefix.size())); + ++parameter; + } + if (parameter < parameters.size() && 0 == parameters[parameter].compare(0, sender_side_scheme_specific_prefix.size(), sender_side_scheme_specific_prefix)) + { + auto elements = fec_elements_converter.parse(parameters[parameter].substr(sender_side_scheme_specific_prefix.size())); + if (0 == elements.as_array().size()) throw sdp_parse_error("expected at least one sender-side FSSI element"); + v[sdp::fields::sender_side_scheme_specific] = std::move(elements); + ++parameter; + } + if (parameter < parameters.size() && 0 == parameters[parameter].compare(0, scheme_specific_prefix.size(), scheme_specific_prefix)) + { + auto elements = fec_elements_converter.parse(parameters[parameter].substr(scheme_specific_prefix.size())); + if (0 == elements.as_array().size()) throw sdp_parse_error("expected at least one FSSI element"); + v[sdp::fields::scheme_specific] = std::move(elements); + ++parameter; + } + if (parameter != parameters.size()) throw sdp_parse_error("unexpected or out-of-order FEC repair-flow parameter"); + return v; + } + }; + + // See https://tools.ietf.org/html/rfc6364#section-4.6 + const converter repair_window_converter{ + [](const web::json::value& v) { + const sdp::repair_window_unit unit{ sdp::fields::window_unit(v) }; + if (!(sdp::repair_window_units::milliseconds == unit || sdp::repair_window_units::microseconds == unit)) throw sdp_format_error("expected repair-window unit 'ms' or 'us'"); + return positive_uint32_digits_converter.format(v.at(sdp::fields::window_size)) + utility::us2s(unit.name); + }, + [](const std::string& s) { + if (s.size() < 3) throw sdp_parse_error("expected repair-window size and unit"); + const auto unit = s.substr(s.size() - 2); + if (!("ms" == unit || "us" == unit)) throw sdp_parse_error("expected repair-window unit 'ms' or 'us'"); + return web::json::value_of({ + { sdp::fields::window_size, positive_uint32_digits_converter.parse(s.substr(0, s.size() - 2)) }, + { sdp::fields::window_unit, utility::s2us(unit) } + }, keep_order); + } + }; + converter object_converter(const std::vector>& field_converters, const std::string& delimiter = " ") { return{ @@ -505,8 +683,32 @@ namespace sdp } // See https://tools.ietf.org/html/rfc4566#section-5 - description media_descriptions(const attribute_converters& converters, const converter& converter) + description media_descriptions(const attribute_converters& converters, const converter& default_converter) { + const converter media_converter{ + [](const web::json::value& v) { + const auto port_converter = key_value_converter('/', { sdp::fields::port, digits_converter }, { sdp::fields::port_count, number_converter }); + std::string s = string_converter.format(v.at(sdp::fields::media_type)); + s += " " + port_converter.format(v); + s += " " + string_converter.format(v.at(sdp::fields::protocol)); + if (v.has_field(sdp::fields::formats) && 0 != sdp::fields::formats(v).size()) s += " " + strings_converter.format(v.at(sdp::fields::formats)); + return s; + }, + [](const std::string& s) { + const auto port_converter = key_value_converter('/', { sdp::fields::port, digits_converter }, { sdp::fields::port_count, number_converter }); + auto v = web::json::value::object(keep_order); + size_t pos = 0; + v[sdp::fields::media_type] = string_converter.parse(substr_find(s, pos, " ")); + if (std::string::npos == pos) throw sdp_parse_error("expected a media port"); + const auto port = port_converter.parse(substr_find(s, pos, " ")); + web::json::insert(v, port.as_object().begin(), port.as_object().end()); + if (std::string::npos == pos) throw sdp_parse_error("expected a media protocol"); + v[sdp::fields::protocol] = string_converter.parse(substr_find(s, pos, " ")); + v[sdp::fields::formats] = strings_converter.parse(std::string::npos != pos ? substr_find(s, pos) : ""); + return v; + } + }; + return optional_descriptions( sdp::fields::media_descriptions, { @@ -514,12 +716,7 @@ namespace sdp required_line( sdp::fields::media, 'm', - object_converter({ - { sdp::fields::media_type, string_converter }, - { {}, key_value_converter('/', { sdp::fields::port, digits_converter }, { sdp::fields::port_count, number_converter }) }, - { sdp::fields::protocol, string_converter }, - { sdp::fields::formats, strings_converter } - }) + media_converter ), information, optional_lines( @@ -529,7 +726,7 @@ namespace sdp ), bandwidth_information, encryption_key, - attributes(converters, converter), + attributes(converters, default_converter), } ); } @@ -676,6 +873,10 @@ namespace sdp } }, { sdp::attributes::mid, string_converter }, + // See https://tools.ietf.org/html/rfc6364 + { sdp::attributes::fec_source_flow, fec_source_flow_converter }, + { sdp::attributes::fec_repair_flow, fec_repair_flow_converter }, + { sdp::attributes::repair_window, repair_window_converter }, // See https://tools.ietf.org/html/rfc7273 { sdp::attributes::ts_refclk, diff --git a/Development/sdp/test/sdp_test.cpp b/Development/sdp/test/sdp_test.cpp index ee6cf52af..c24b6976c 100644 --- a/Development/sdp/test/sdp_test.cpp +++ b/Development/sdp/test/sdp_test.cpp @@ -313,6 +313,111 @@ a=mid:SECONDARY BST_CHECK_EQUAL(session_description3.serialize(), session_description2.serialize()); } +//////////////////////////////////////////////////////////////////////////////////////////// +BST_TEST_CASE(testSdpRfc6364Roundtrip) +{ + const std::string test_sdp = R"(v=0 +o=ali 1122334455 1122334466 IN IP4 fec.example.com +s=FEC Framework Examples +t=0 0 +a=group:FEC-FR S6 R5 +a=group:FEC-FR S6 R6 +m=video 30000 RTP/AVP 100 +c=IN IP4 233.252.0.1/127 +a=rtpmap:100 MP2T/90000 +a=fec-source-flow: id=0; tag-len=4 +a=mid:S6 +m=application 30000 UDP/FEC +c=IN IP4 233.252.0.3/127 +a=fec-repair-flow: encoding-id=0; preference-lvl=0; ss-fssi=n:7,k:5; fssi=note: +a=repair-window:200ms +a=mid:R5 +m=application 30000 UDP/FEC +c=IN IP4 233.252.0.4/127 +a=fec-repair-flow: encoding-id=1; preference-lvl=1; ss-fssi=t:3 +a=repair-window:150500us +a=mid:R6 +)"; + + const auto session_description = sdp::parse_session_description(test_sdp); + const auto& session_attributes = sdp::fields::attributes(session_description).as_array(); + const auto group = sdp::find_name(session_attributes, sdp::attributes::group); + BST_REQUIRE(session_attributes.end() != group); + BST_CHECK_EQUAL(sdp::group_semantics::fec_fr, sdp::group_semantics_type{ sdp::fields::semantics(sdp::fields::value(*group)) }); + + const auto& media_descriptions = sdp::fields::media_descriptions(session_description).as_array(); + BST_REQUIRE_EQUAL(3, media_descriptions.size()); + + const auto& source_attributes = sdp::fields::attributes(media_descriptions.at(0)).as_array(); + const auto source_flow = sdp::find_name(source_attributes, sdp::attributes::fec_source_flow); + BST_REQUIRE(source_attributes.end() != source_flow); + const auto& source_flow_value = sdp::fields::value(*source_flow); + BST_CHECK_EQUAL(0, sdp::fields::source_id(source_flow_value)); + BST_CHECK_EQUAL(4, sdp::fields::tag_length(source_flow_value)); + + const auto& first_repair_media = sdp::fields::media(media_descriptions.at(1)); + BST_CHECK_EQUAL(sdp::protocols::UDP_FEC, sdp::protocol{ sdp::fields::protocol(first_repair_media) }); + BST_CHECK_EQUAL(0, sdp::fields::formats(first_repair_media).size()); + + const auto& first_repair_attributes = sdp::fields::attributes(media_descriptions.at(1)).as_array(); + const auto repair_flow = sdp::find_name(first_repair_attributes, sdp::attributes::fec_repair_flow); + BST_REQUIRE(first_repair_attributes.end() != repair_flow); + const auto& repair_flow_value = sdp::fields::value(*repair_flow); + BST_CHECK_EQUAL(0, sdp::fields::encoding_id(repair_flow_value)); + BST_CHECK_EQUAL(0, sdp::fields::preference_level(repair_flow_value)); + const auto& sender_side_elements = sdp::fields::sender_side_scheme_specific(repair_flow_value); + BST_REQUIRE_EQUAL(2, sender_side_elements.size()); + BST_CHECK_EQUAL(U("n"), sdp::fields::name(sender_side_elements.at(0))); + BST_CHECK_EQUAL(U("7"), sdp::fields::value(sender_side_elements.at(0)).as_string()); + const auto& scheme_elements = sdp::fields::scheme_specific(repair_flow_value); + BST_REQUIRE_EQUAL(1, scheme_elements.size()); + BST_CHECK_EQUAL(U(""), sdp::fields::value(scheme_elements.at(0)).as_string()); + + const auto repair_window = sdp::find_name(first_repair_attributes, sdp::attributes::repair_window); + BST_REQUIRE(first_repair_attributes.end() != repair_window); + const auto& repair_window_value = sdp::fields::value(*repair_window); + BST_CHECK_EQUAL(200, sdp::fields::window_size(repair_window_value)); + BST_CHECK_EQUAL(sdp::repair_window_units::milliseconds, sdp::repair_window_unit{ sdp::fields::window_unit(repair_window_value) }); + + BST_CHECK_EQUAL(U("FEC/UDP"), sdp::protocols::FEC_UDP.name); + + std::istringstream expected(test_sdp), actual(sdp::make_session_description(session_description)); + do + { + std::string expected_line, actual_line; + std::getline(expected, expected_line); + std::getline(actual, 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()); +} + +//////////////////////////////////////////////////////////////////////////////////////////// +BST_TEST_CASE(testSdpRfc6364RejectsMalformedAttributes) +{ + const std::string before = "v=0\r\no=- 0 0 IN IP4 192.0.2.1\r\ns=FEC\r\nt=0 0\r\nm=application 30000 UDP/FEC\r\n"; + + const std::vector malformed = { + "a=fec-source-flow:id=0\r\n", + "a=fec-source-flow: id=4294967296\r\n", + "a=fec-source-flow: id=0; tag-len=0\r\n", + "a=fec-source-flow: id=0; tag-len=4; extra=1\r\n", + "a=fec-repair-flow: encoding-id=256\r\n", + "a=fec-repair-flow: encoding-id=0; ss-fssi=n:7; preference-lvl=1\r\n", + "a=fec-repair-flow: encoding-id=0; ss-fssi=\r\n", + "a=fec-repair-flow: encoding-id=0; ss-fssi=n:7/bad\r\n", + "a=repair-window:0ms\r\n", + "a=repair-window:4294967296ms\r\n", + "a=repair-window:1s\r\n", + "a=repair-window:+1ms\r\n" + }; + + for (const auto& attribute : malformed) + { + BST_CHECK_THROW(sdp::parse_session_description(before + attribute), std::runtime_error); + } +} + //////////////////////////////////////////////////////////////////////////////////////////// BST_TEST_CASE(testSdpNumbers) {