diff --git a/Development/cmake/NmosCppLibraries.cmake b/Development/cmake/NmosCppLibraries.cmake index 93d4964a..65d1983b 100644 --- a/Development/cmake/NmosCppLibraries.cmake +++ b/Development/cmake/NmosCppLibraries.cmake @@ -1093,6 +1093,7 @@ set(NMOS_CPP_NMOS_SOURCES nmos/manifest_api.cpp nmos/mdns.cpp nmos/mdns_api.cpp + nmos/media_type.cpp nmos/node_api.cpp nmos/node_api_target_handler.cpp nmos/node_behaviour.cpp diff --git a/Development/nmos-cpp-node/node_implementation.cpp b/Development/nmos-cpp-node/node_implementation.cpp index ba3a93f6..42ebdc1b 100644 --- a/Development/nmos-cpp-node/node_implementation.cpp +++ b/Development/nmos-cpp-node/node_implementation.cpp @@ -2036,7 +2036,7 @@ nmos::transport_file_parser make_node_implementation_transport_file_parser() const auto validate_sdp_parameters = [](const web::json::value& receiver, const nmos::sdp_parameters& sdp_params) { - if (nmos::media_types::video_jxsv == nmos::get_media_type(sdp_params)) + if (equals_media_type(nmos::media_types::video_jxsv, nmos::get_media_type(sdp_params))) { nmos::validate_video_jxsv_sdp_parameters(receiver, sdp_params); } diff --git a/Development/nmos/media_type.cpp b/Development/nmos/media_type.cpp new file mode 100644 index 00000000..3fd16981 --- /dev/null +++ b/Development/nmos/media_type.cpp @@ -0,0 +1,11 @@ +#include "nmos/media_type.h" + +#include + +namespace nmos +{ + bool equals_media_type(const media_type& lhs, const media_type& rhs) + { + return boost::algorithm::iequals(lhs.name, rhs.name); + } +} diff --git a/Development/nmos/media_type.h b/Development/nmos/media_type.h index 0b8da8fc..dcd280ed 100644 --- a/Development/nmos/media_type.h +++ b/Development/nmos/media_type.h @@ -12,6 +12,12 @@ namespace nmos // and https://specs.amwa.tv/is-04/releases/v1.2.0/APIs/schemas/with-refs/receiver_video.html // etc. DEFINE_STRING_ENUM(media_type) + + // RFC 4855: media subtype names (and thus media type strings that embed an + // RTP encoding name) are case-insensitive. operator== remains exact-string + // identity for stored / configured values. + bool equals_media_type(const media_type& lhs, const media_type& rhs); + namespace media_types { // Video media types diff --git a/Development/nmos/sdp_utils.cpp b/Development/nmos/sdp_utils.cpp index cc490794..0eabf679 100644 --- a/Development/nmos/sdp_utils.cpp +++ b/Development/nmos/sdp_utils.cpp @@ -2,6 +2,7 @@ #include #include +#include #include #include #include @@ -955,6 +956,31 @@ namespace nmos return media_type{ sdp_params.media_type.name + U("/") + sdp_params.rtpmap.encoding_name }; } + namespace details + { + // Find the specified fmtp parameter name case-insensitive in the specified fmtp list per RFC 4855 + sdp_parameters::fmtp_t::const_iterator find_fmtp(const sdp_parameters::fmtp_t& fmtp, const utility::string_t& name) + { + return std::find_if(fmtp.begin(), fmtp.end(), [&](const sdp_parameters::fmtp_t::value_type& param) + { + return boost::algorithm::iequals(param.first, name); + }); + } + sdp_parameters::fmtp_t::iterator find_fmtp(sdp_parameters::fmtp_t& fmtp, const utility::string_t& name) + { + return std::find_if(fmtp.begin(), fmtp.end(), [&](const sdp_parameters::fmtp_t::value_type& param) + { + return boost::algorithm::iequals(param.first, name); + }); + } + + // RTP encoding names are case-insensitive per RFC 4855 + bool equals_encoding_name(const utility::string_t& lhs, const utility::string_t& rhs) + { + return boost::algorithm::iequals(lhs, rhs); + } + } + web::json::value make_session_description(const sdp_parameters& sdp_params, const web::json::value& transport_params, bst::optional source_filters) { return make_session_description(sdp_params, transport_params, make_rtpmap(sdp_params), make_fmtp(sdp_params), source_filters); @@ -1573,7 +1599,10 @@ namespace nmos if (0 == params.channel_count) params.channel_count = 1; const auto& encoding_name = sdp_params.rtpmap.encoding_name; - params.bit_depth = !encoding_name.empty() && U('L') == encoding_name.front() ? utility::istringstreamed(encoding_name.substr(1)) : 0; + // RTP encoding names are case-insensitive per RFC 4855 (e.g. "L24" / "l24") + params.bit_depth = !encoding_name.empty() && details::equals_encoding_name(encoding_name.substr(0, 1), U("L")) + ? utility::istringstreamed(encoding_name.substr(1)) + : 0; params.sample_rate = sdp_params.rtpmap.clock_rate; @@ -1674,21 +1703,58 @@ namespace nmos namespace details { + bool is_audio_L_encoding_name(const utility::string_t& encoding_name) + { + return !encoding_name.empty() && equals_encoding_name(encoding_name.substr(0, 1), U("L")); + } + + // Check the specified media type case-insensitive against enum values in the specified string constraint per RFC 4855 + // cf. nmos::match_string_constraint + bool match_media_type_constraint(const utility::string_t& value, const web::json::value& constraint) + { + // first check the enum constraint if present, like nmos::details::match_enum_constraint but with equals_media_type + if (constraint.has_field(nmos::fields::constraint_enum)) + { + const auto& enum_values = nmos::fields::constraint_enum(constraint).as_array(); + const media_type actual{ value }; + if (enum_values.end() == std::find_if(enum_values.begin(), enum_values.end(), [&](const web::json::value& enum_value) + { + return enum_value.is_string() && equals_media_type(nmos::media_type{ enum_value.as_string() }, actual); + })) + { + return false; + } + } + // then use nmos::match_string_constraint to check the pattern constraint if present + if (constraint.has_field(nmos::fields::constraint_pattern)) + { + if (!nmos::match_string_constraint(value, web::json::value_of({ + { nmos::fields::constraint_pattern, nmos::fields::constraint_pattern(constraint) } + }))) + { + return false; + } + } + return true; + } + nmos::format get_format(const sdp_parameters& sdp_params) { - if (sdp::media_types::video == sdp_params.media_type && U("raw") == sdp_params.rtpmap.encoding_name) return nmos::formats::video; - if (sdp::media_types::audio == sdp_params.media_type && U("L") == sdp_params.rtpmap.encoding_name.substr(0, 1)) return nmos::formats::audio; - if (sdp::media_types::video == sdp_params.media_type && U("smpte291") == sdp_params.rtpmap.encoding_name) return nmos::formats::data; - if (sdp::media_types::video == sdp_params.media_type && U("SMPTE2022-6") == sdp_params.rtpmap.encoding_name) return nmos::formats::mux; + const auto& encoding_name = sdp_params.rtpmap.encoding_name; + if (sdp::media_types::video == sdp_params.media_type && equals_encoding_name(encoding_name, U("raw"))) return nmos::formats::video; + if (sdp::media_types::audio == sdp_params.media_type && is_audio_L_encoding_name(encoding_name)) return nmos::formats::audio; + if (sdp::media_types::video == sdp_params.media_type && equals_encoding_name(encoding_name, U("smpte291"))) return nmos::formats::data; + if (sdp::media_types::video == sdp_params.media_type && equals_encoding_name(encoding_name, U("SMPTE2022-6"))) return nmos::formats::mux; throw sdp_processing_error("unsupported media type/encoding name"); } format_parameters get_format_parameters(const sdp_parameters& sdp_params) { - if (sdp::media_types::video == sdp_params.media_type && U("raw") == sdp_params.rtpmap.encoding_name) return get_video_raw_parameters(sdp_params); - if (sdp::media_types::audio == sdp_params.media_type && U("L") == sdp_params.rtpmap.encoding_name.substr(0, 1)) return get_audio_L_parameters(sdp_params); - if (sdp::media_types::video == sdp_params.media_type && U("smpte291") == sdp_params.rtpmap.encoding_name) return get_video_smpte291_parameters(sdp_params); - if (sdp::media_types::video == sdp_params.media_type && U("SMPTE2022-6") == sdp_params.rtpmap.encoding_name) return get_video_SMPTE2022_6_parameters(sdp_params); + const auto& encoding_name = sdp_params.rtpmap.encoding_name; + if (sdp::media_types::video == sdp_params.media_type && equals_encoding_name(encoding_name, U("raw"))) return get_video_raw_parameters(sdp_params); + if (sdp::media_types::audio == sdp_params.media_type && is_audio_L_encoding_name(encoding_name)) return get_audio_L_parameters(sdp_params); + if (sdp::media_types::video == sdp_params.media_type && equals_encoding_name(encoding_name, U("smpte291"))) return get_video_smpte291_parameters(sdp_params); + if (sdp::media_types::video == sdp_params.media_type && equals_encoding_name(encoding_name, U("SMPTE2022-6"))) return get_video_SMPTE2022_6_parameters(sdp_params); throw sdp_processing_error("unsupported media type/encoding name"); } @@ -1723,7 +1789,7 @@ namespace nmos { // General Constraints - { nmos::caps::format::media_type, [](CAPS_ARGS) { return nmos::match_string_constraint(get_media_type(sdp).name, con); } }, + { nmos::caps::format::media_type, [](CAPS_ARGS) { return match_media_type_constraint(get_media_type(sdp).name, con); } }, // hm, how best to match (rational) nmos::caps::format::grain_rate against (double) framerate e.g. for video/SMPTE2022-6? // is 23.976 a match for 24000/1001? how about 23.98, or 23.9? or even 23?! { nmos::caps::format::grain_rate, [](CAPS_ARGS) { auto exactframerate = get_exactframerate(&format); return nmos::rational{} == exactframerate || nmos::match_rational_constraint(exactframerate, con); } }, @@ -1781,7 +1847,10 @@ namespace nmos if (!media_types_or_null.is_null()) { const auto& media_types = media_types_or_null.as_array(); - const auto found = std::find(media_types.begin(), media_types.end(), web::json::value::string(media_type.name)); + const auto found = std::find_if(media_types.begin(), media_types.end(), [&](const web::json::value& candidate) + { + return candidate.is_string() && equals_media_type(nmos::media_type{ candidate.as_string() }, media_type); + }); if (media_types.end() == found) throw details::sdp_processing_error("unsupported encoding name"); } const auto& constraint_sets_or_null = nmos::fields::constraint_sets(caps); diff --git a/Development/nmos/sdp_utils.h b/Development/nmos/sdp_utils.h index ede965f9..b3d7c178 100644 --- a/Development/nmos/sdp_utils.h +++ b/Development/nmos/sdp_utils.h @@ -653,20 +653,12 @@ namespace nmos } }; - inline sdp_parameters::fmtp_t::const_iterator find_fmtp(const sdp_parameters::fmtp_t& fmtp, const utility::string_t& name) - { - return std::find_if(fmtp.begin(), fmtp.end(), [&](const sdp_parameters::fmtp_t::value_type& param) - { - return param.first == name; - }); - } - inline sdp_parameters::fmtp_t::iterator find_fmtp(sdp_parameters::fmtp_t& fmtp, const utility::string_t& name) - { - return std::find_if(fmtp.begin(), fmtp.end(), [&](const sdp_parameters::fmtp_t::value_type& param) - { - return param.first == name; - }); - } + // Find the specified fmtp parameter name case-insensitive in the specified fmtp list per RFC 4855 + sdp_parameters::fmtp_t::const_iterator find_fmtp(const sdp_parameters::fmtp_t& fmtp, const utility::string_t& name); + sdp_parameters::fmtp_t::iterator find_fmtp(sdp_parameters::fmtp_t& fmtp, const utility::string_t& name); + + // RTP encoding names are case-insensitive per RFC 4855 + bool equals_encoding_name(const utility::string_t& lhs, const utility::string_t& rhs); // type-erased format-specific parameters // e.g. can hold a video_raw_parameters, an audio_L_parameters, etc. @@ -688,6 +680,9 @@ namespace nmos // Check the specified SDP interlace and segmented parameters against the specified interlace_mode constraint bool match_interlace_mode_constraint(bool interlace, bool segmented, const web::json::value& constraint); + // Check the specified media type case-insensitive against enum values in the specified string constraint per RFC 4855 + bool match_media_type_constraint(const utility::string_t& value, const web::json::value& constraint); + // Check the specified SDP parameters and format-specific parameters against the specified constraint set // using the specified parameter constraint functions bool match_sdp_parameters_constraint_set(const sdp_parameter_constraints& constraints, const sdp_parameters& sdp_params, const format_parameters& format_params, const web::json::value& constraint_set); diff --git a/Development/nmos/test/sdp_utils_test.cpp b/Development/nmos/test/sdp_utils_test.cpp index ea771915..6b50b293 100644 --- a/Development/nmos/test/sdp_utils_test.cpp +++ b/Development/nmos/test/sdp_utils_test.cpp @@ -840,6 +840,75 @@ BST_TEST_CASE(testSdpParametersVideoRaw) } } +//////////////////////////////////////////////////////////////////////////////////////////// +// RFC 4855: RTP encoding names and fmtp parameter names are case-insensitive. +BST_TEST_CASE(testSdpEncodingNameAndFmtpCaseInsensitive) +{ + using web::json::value_of; + + nmos::sdp_parameters mixed_case_video{ + U("mixed-case"), + sdp::media_types::video, + { + 96, + U("RAW"), + 90000 + }, + { + { U("sampling"), U("YCbCr-4:2:2") }, + { U("depth"), U("10") }, + { U("width"), U("1920") }, + { U("height"), U("1080") }, + { U("exactframerate"), U("50") }, + { U("colorimetry"), U("BT709") }, + { U("pm"), U("2110GPM") }, + { U("ssn"), U("ST2110-20:2017") }, + { U("tp"), U("2110TPN") } + } + }; + + const auto video = nmos::get_video_raw_parameters(mixed_case_video); + BST_REQUIRE_EQUAL(sdp::packing_modes::general.name, video.pm.name); + BST_REQUIRE_EQUAL(sdp::smpte_standard_numbers::ST2110_20_2017.name, video.ssn.name); + BST_REQUIRE_EQUAL(sdp::type_parameters::type_N.name, video.tp.name); + BST_REQUIRE_EQUAL(1920u, video.width); + BST_REQUIRE_EQUAL(1080u, video.height); + + auto video_receiver = value_of({ + { nmos::fields::format, nmos::formats::video.name }, + { nmos::fields::caps, value_of({ + { nmos::fields::media_types, value_of({ nmos::media_types::video_raw.name }) } + }) } + }); + BST_REQUIRE_NO_THROW(nmos::validate_sdp_parameters(video_receiver, mixed_case_video)); + + nmos::sdp_parameters mixed_case_audio{ + U("mixed-case-audio"), + sdp::media_types::audio, + { + 97, + U("l24"), + 48000, + 2 + }, + { + { U("CHANNEL-ORDER"), U("SMPTE2110.(ST)") } + } + }; + + const auto audio = nmos::get_audio_L_parameters(mixed_case_audio); + BST_REQUIRE_EQUAL(24u, audio.bit_depth); + BST_REQUIRE_EQUAL(U("SMPTE2110.(ST)"), audio.channel_order); + + auto audio_receiver = value_of({ + { nmos::fields::format, nmos::formats::audio.name }, + { nmos::fields::caps, value_of({ + { nmos::fields::media_types, value_of({ nmos::media_types::audio_L24.name }) } + }) } + }); + BST_REQUIRE_NO_THROW(nmos::validate_sdp_parameters(audio_receiver, mixed_case_audio)); +} + //////////////////////////////////////////////////////////////////////////////////////////// BST_TEST_CASE(testSdpParametersAudioL) { diff --git a/Development/nmos/video_jxsv.cpp b/Development/nmos/video_jxsv.cpp index 1ee2a6ff..b90510a6 100644 --- a/Development/nmos/video_jxsv.cpp +++ b/Development/nmos/video_jxsv.cpp @@ -317,7 +317,7 @@ namespace nmos #define CAPS_ARGS const sdp_parameters& sdp, const format_parameters& format, const web::json::value& con static const std::map> jxsv_constraints { - { nmos::caps::format::media_type, [](CAPS_ARGS) { return nmos::match_string_constraint(get_media_type(sdp).name, con); } }, + { nmos::caps::format::media_type, [](CAPS_ARGS) { return nmos::details::match_media_type_constraint(get_media_type(sdp).name, con); } }, { nmos::caps::format::grain_rate, [](CAPS_ARGS) { auto jxsv = get_jxsv(&format); return jxsv && (nmos::rational{} == jxsv->exactframerate || nmos::match_rational_constraint(jxsv->exactframerate, con)); } }, { nmos::caps::format::profile, [](CAPS_ARGS) { auto jxsv = get_jxsv(&format); return jxsv && (jxsv->profile.empty() || nmos::match_string_constraint(jxsv->profile.name, con)); } }, { nmos::caps::format::level, [](CAPS_ARGS) { auto jxsv = get_jxsv(&format); return jxsv && (jxsv->level.empty() || nmos::match_string_constraint(jxsv->level.name, con)); } }, @@ -342,7 +342,8 @@ namespace nmos { // this function can only be used to validate SDP data for "video/jxsv"; logic error otherwise const auto media_type = get_media_type(sdp_params); - if (nmos::media_types::video_jxsv != media_type) throw std::invalid_argument("unexpected media type/encoding name"); + if (!equals_media_type(nmos::media_types::video_jxsv, media_type)) + throw std::invalid_argument("unexpected media type/encoding name"); nmos::details::validate_sdp_parameters(details::jxsv_constraints, sdp_params, nmos::formats::video, get_video_jxsv_parameters(sdp_params), receiver); }