From e1d550cc737ae95c83f282f2428d2babae4c6ad4 Mon Sep 17 00:00:00 2001 From: Thang Luu Date: Tue, 14 Jul 2026 22:54:24 +0900 Subject: [PATCH 1/6] Add configurable RTP parameter sets --- Development/cmake/NmosCppTest.cmake | 1 + Development/nmos/connection_resources.cpp | 185 +++++++++++++++++- Development/nmos/connection_resources.h | 29 +++ Development/nmos/json_fields.h | 7 +- .../nmos/test/connection_resources_test.cpp | 100 ++++++++++ 5 files changed, 312 insertions(+), 10 deletions(-) create mode 100644 Development/nmos/test/connection_resources_test.cpp diff --git a/Development/cmake/NmosCppTest.cmake b/Development/cmake/NmosCppTest.cmake index ea7718283..78d130b64 100644 --- a/Development/cmake/NmosCppTest.cmake +++ b/Development/cmake/NmosCppTest.cmake @@ -44,6 +44,7 @@ set(NMOS_CPP_TEST_NMOS_TEST_SOURCES nmos/test/capabilities_test.cpp nmos/test/channels_test.cpp nmos/test/condition_variable_test.cpp + nmos/test/connection_resources_test.cpp nmos/test/configuration_methods_test.cpp nmos/test/configuration_resources_test.cpp nmos/test/configuration_utils_test.cpp diff --git a/Development/nmos/connection_resources.cpp b/Development/nmos/connection_resources.cpp index 55ddd4885..9122cddef 100644 --- a/Development/nmos/connection_resources.cpp +++ b/Development/nmos/connection_resources.cpp @@ -110,6 +110,70 @@ namespace nmos }); } + web::json::value make_connection_rtp_sender_fec_constraints() + { + using web::json::value; + using web::json::value_of; + + const auto unconstrained = value::object(); + return value_of({ + { nmos::fields::fec_enabled, unconstrained }, + { nmos::fields::fec_destination_ip, unconstrained }, + { nmos::fields::fec_type, unconstrained }, + { nmos::fields::fec_mode, unconstrained }, + { nmos::fields::fec_block_width, unconstrained }, + { nmos::fields::fec_block_height, unconstrained }, + { nmos::fields::fec1D_destination_port, unconstrained }, + { nmos::fields::fec2D_destination_port, unconstrained }, + { nmos::fields::fec1D_source_port, unconstrained }, + { nmos::fields::fec2D_source_port, unconstrained } + }); + } + + web::json::value make_connection_rtp_sender_staged_fec_parameter_set() + { + using web::json::value_of; + + return value_of({ + { nmos::fields::fec_enabled, false }, + { nmos::fields::fec_destination_ip, U("auto") }, + { nmos::fields::fec_type, U("XOR") }, + { nmos::fields::fec_mode, U("1D") }, + { nmos::fields::fec_block_width, 4 }, + { nmos::fields::fec_block_height, 4 }, + { nmos::fields::fec1D_destination_port, U("auto") }, + { nmos::fields::fec2D_destination_port, U("auto") }, + { nmos::fields::fec1D_source_port, U("auto") }, + { nmos::fields::fec2D_source_port, U("auto") } + }); + } + + web::json::value make_connection_rtp_sender_rtcp_constraints() + { + using web::json::value; + using web::json::value_of; + + const auto unconstrained = value::object(); + return value_of({ + { nmos::fields::rtcp_enabled, unconstrained }, + { nmos::fields::rtcp_destination_ip, unconstrained }, + { nmos::fields::rtcp_destination_port, unconstrained }, + { nmos::fields::rtcp_source_port, unconstrained } + }); + } + + web::json::value make_connection_rtp_sender_staged_rtcp_parameter_set() + { + using web::json::value_of; + + return value_of({ + { nmos::fields::rtcp_enabled, false }, + { nmos::fields::rtcp_destination_ip, U("auto") }, + { nmos::fields::rtcp_destination_port, U("auto") }, + { nmos::fields::rtcp_source_port, U("auto") } + }); + } + // See https://specs.amwa.tv/is-05/releases/v1.1.0/docs/4.1._Behaviour_-_RTP_Transport_Type.html#receiver-parameter-sets // and https://specs.amwa.tv/is-05/releases/v1.1.0/APIs/schemas/with-refs/constraints-schema-rtp.html web::json::value make_connection_rtp_receiver_core_constraints() @@ -122,8 +186,7 @@ namespace nmos { nmos::fields::source_ip, unconstrained }, { nmos::fields::interface_ip, unconstrained }, { nmos::fields::destination_port, unconstrained }, - { nmos::fields::rtp_enabled, unconstrained }, - { nmos::fields::multicast_ip, unconstrained } + { nmos::fields::rtp_enabled, unconstrained } }); } @@ -138,23 +201,109 @@ namespace nmos { nmos::fields::source_ip, value::null() }, { nmos::fields::interface_ip, U("auto") }, { nmos::fields::destination_port, U("auto") }, - { nmos::fields::rtp_enabled, true }, + { nmos::fields::rtp_enabled, true } + }); + } + + web::json::value make_connection_rtp_receiver_multicast_constraints() + { + using web::json::value; + using web::json::value_of; + + return value_of({ + { nmos::fields::multicast_ip, value::object() } + }); + } + + web::json::value make_connection_rtp_receiver_staged_multicast_parameter_set() + { + using web::json::value; + using web::json::value_of; + + return value_of({ { nmos::fields::multicast_ip, value::null() } }); } + + web::json::value make_connection_rtp_receiver_fec_constraints() + { + using web::json::value; + using web::json::value_of; + + const auto unconstrained = value::object(); + return value_of({ + { nmos::fields::fec_enabled, unconstrained }, + { nmos::fields::fec_destination_ip, unconstrained }, + { nmos::fields::fec_mode, unconstrained }, + { nmos::fields::fec1D_destination_port, unconstrained }, + { nmos::fields::fec2D_destination_port, unconstrained } + }); + } + + web::json::value make_connection_rtp_receiver_staged_fec_parameter_set() + { + using web::json::value_of; + + return value_of({ + { nmos::fields::fec_enabled, false }, + { nmos::fields::fec_destination_ip, U("auto") }, + { nmos::fields::fec_mode, U("auto") }, + { nmos::fields::fec1D_destination_port, U("auto") }, + { nmos::fields::fec2D_destination_port, U("auto") } + }); + } + + web::json::value make_connection_rtp_receiver_rtcp_constraints() + { + using web::json::value; + using web::json::value_of; + + const auto unconstrained = value::object(); + return value_of({ + { nmos::fields::rtcp_enabled, unconstrained }, + { nmos::fields::rtcp_destination_ip, unconstrained }, + { nmos::fields::rtcp_destination_port, unconstrained } + }); + } + + web::json::value make_connection_rtp_receiver_staged_rtcp_parameter_set() + { + using web::json::value_of; + + return value_of({ + { nmos::fields::rtcp_enabled, false }, + { nmos::fields::rtcp_destination_ip, U("auto") }, + { nmos::fields::rtcp_destination_port, U("auto") } + }); + } + + void insert_parameter_set(web::json::value& parameters, const web::json::value& parameter_set) + { + web::json::insert(parameters, parameter_set.as_object().begin(), parameter_set.as_object().end()); + } } nmos::resource make_connection_rtp_sender(const nmos::id& id, bool smpte2022_7) + { + return make_connection_rtp_sender(id, smpte2022_7, rtp_sender_parameter_sets{}); + } + + nmos::resource make_connection_rtp_sender(const nmos::id& id, bool smpte2022_7, const rtp_sender_parameter_sets& parameter_sets) { using web::json::value; - using web::json::value_of; auto data = details::make_connection_resource_core(id, smpte2022_7); - data[nmos::fields::endpoint_constraints] = details::legs_of(details::make_connection_rtp_sender_core_constraints(), smpte2022_7); + auto constraints = details::make_connection_rtp_sender_core_constraints(); + if (parameter_sets.fec) details::insert_parameter_set(constraints, details::make_connection_rtp_sender_fec_constraints()); + if (parameter_sets.rtcp) details::insert_parameter_set(constraints, details::make_connection_rtp_sender_rtcp_constraints()); + data[nmos::fields::endpoint_constraints] = details::legs_of(constraints, smpte2022_7); data[nmos::fields::endpoint_staged][nmos::fields::receiver_id] = value::null(); - data[nmos::fields::endpoint_staged][nmos::fields::transport_params] = details::legs_of(details::make_connection_rtp_sender_staged_core_parameter_set(), smpte2022_7); + auto transport_params = details::make_connection_rtp_sender_staged_core_parameter_set(); + if (parameter_sets.fec) details::insert_parameter_set(transport_params, details::make_connection_rtp_sender_staged_fec_parameter_set()); + if (parameter_sets.rtcp) details::insert_parameter_set(transport_params, details::make_connection_rtp_sender_staged_rtcp_parameter_set()); + data[nmos::fields::endpoint_staged][nmos::fields::transport_params] = details::legs_of(transport_params, smpte2022_7); data[nmos::fields::endpoint_active] = data[nmos::fields::endpoint_staged]; // The caller must resolve all instances of "auto" in the /active endpoint into the actual values that will be used! @@ -188,7 +337,12 @@ namespace nmos nmos::resource make_connection_rtp_sender(const nmos::id& id, bool smpte2022_7, const utility::string_t& transportfile) { - auto resource = make_connection_rtp_sender(id, smpte2022_7); + return make_connection_rtp_sender(id, smpte2022_7, rtp_sender_parameter_sets{}, transportfile); + } + + nmos::resource make_connection_rtp_sender(const nmos::id& id, bool smpte2022_7, const rtp_sender_parameter_sets& parameter_sets, const utility::string_t& transportfile) + { + auto resource = make_connection_rtp_sender(id, smpte2022_7, parameter_sets); const utility::string_t sdp_magic(U("v=0")); @@ -204,16 +358,29 @@ namespace nmos } nmos::resource make_connection_rtp_receiver(const nmos::id& id, bool smpte2022_7) + { + return make_connection_rtp_receiver(id, smpte2022_7, rtp_receiver_parameter_sets{}); + } + + nmos::resource make_connection_rtp_receiver(const nmos::id& id, bool smpte2022_7, const rtp_receiver_parameter_sets& parameter_sets) { using web::json::value; auto data = details::make_connection_resource_core(id, smpte2022_7); - data[nmos::fields::endpoint_constraints] = details::legs_of(details::make_connection_rtp_receiver_core_constraints(), smpte2022_7); + auto constraints = details::make_connection_rtp_receiver_core_constraints(); + if (parameter_sets.multicast) details::insert_parameter_set(constraints, details::make_connection_rtp_receiver_multicast_constraints()); + if (parameter_sets.fec) details::insert_parameter_set(constraints, details::make_connection_rtp_receiver_fec_constraints()); + if (parameter_sets.rtcp) details::insert_parameter_set(constraints, details::make_connection_rtp_receiver_rtcp_constraints()); + data[nmos::fields::endpoint_constraints] = details::legs_of(constraints, smpte2022_7); data[nmos::fields::endpoint_staged][nmos::fields::sender_id] = value::null(); data[nmos::fields::endpoint_staged][nmos::fields::transport_file] = details::make_connection_receiver_staging_transport_file(); - data[nmos::fields::endpoint_staged][nmos::fields::transport_params] = details::legs_of(details::make_connection_rtp_receiver_staged_core_parameter_set(), smpte2022_7); + auto transport_params = details::make_connection_rtp_receiver_staged_core_parameter_set(); + if (parameter_sets.multicast) details::insert_parameter_set(transport_params, details::make_connection_rtp_receiver_staged_multicast_parameter_set()); + if (parameter_sets.fec) details::insert_parameter_set(transport_params, details::make_connection_rtp_receiver_staged_fec_parameter_set()); + if (parameter_sets.rtcp) details::insert_parameter_set(transport_params, details::make_connection_rtp_receiver_staged_rtcp_parameter_set()); + data[nmos::fields::endpoint_staged][nmos::fields::transport_params] = details::legs_of(transport_params, smpte2022_7); data[nmos::fields::endpoint_active] = data[nmos::fields::endpoint_staged]; // The caller must resolve all instances of "auto" in the /active endpoint into the actual values that will be used! diff --git a/Development/nmos/connection_resources.h b/Development/nmos/connection_resources.h index 2f757a728..bf84691d1 100644 --- a/Development/nmos/connection_resources.h +++ b/Development/nmos/connection_resources.h @@ -13,6 +13,32 @@ namespace nmos { struct resource; + // Optional IS-05 RTP parameter sets supported by a Sender or Receiver. + // Core parameters are mandatory and therefore not represented by these types. + struct rtp_sender_parameter_sets + { + bool fec; + bool rtcp; + + explicit rtp_sender_parameter_sets(bool fec = false, bool rtcp = false) + : fec(fec) + , rtcp(rtcp) + {} + }; + + struct rtp_receiver_parameter_sets + { + bool multicast; + bool fec; + bool rtcp; + + explicit rtp_receiver_parameter_sets(bool multicast = true, bool fec = false, bool rtcp = false) + : multicast(multicast) + , fec(fec) + , rtcp(rtcp) + {} + }; + // make an absolute URL for the /transportfile endpoint of the specified sender // e.g. for use in the manifest_href property of the IS-04 Node API sender web::uri make_connection_api_transportfile(const nmos::id& sender_id, const nmos::settings& settings); @@ -36,6 +62,7 @@ namespace nmos // The caller must resolve all instances of "auto" in the /active endpoint into the actual values that will be used! // See nmos::resolve_rtp_auto nmos::resource make_connection_rtp_sender(const nmos::id& id, bool smpte2022_7); + nmos::resource make_connection_rtp_sender(const nmos::id& id, bool smpte2022_7, const rtp_sender_parameter_sets& parameter_sets); web::json::value make_connection_rtp_sender_transportfile(const utility::string_t& transportfile); web::json::value make_connection_rtp_sender_transportfile(const web::uri& transportfile); @@ -44,10 +71,12 @@ namespace nmos // See nmos::resolve_rtp_auto // transportfile may be URL or the contents of the SDP file (yeah, yuck) nmos::resource make_connection_rtp_sender(const nmos::id& id, bool smpte2022_7, const utility::string_t& transportfile); + nmos::resource make_connection_rtp_sender(const nmos::id& id, bool smpte2022_7, const rtp_sender_parameter_sets& parameter_sets, const utility::string_t& transportfile); // The caller must resolve all instances of "auto" in the /active endpoint into the actual values that will be used! // See nmos::resolve_rtp_auto nmos::resource make_connection_rtp_receiver(const nmos::id& id, bool smpte2022_7); + nmos::resource make_connection_rtp_receiver(const nmos::id& id, bool smpte2022_7, const rtp_receiver_parameter_sets& parameter_sets); // Although these functions make "connection" (IS-05) resources, the details are defined by IS-07 Event & Tally // so maybe these belong in nmos/events_resources.h or their own file, e.g. nmos/connection_events_resources.h? diff --git a/Development/nmos/json_fields.h b/Development/nmos/json_fields.h index 118dc0380..96c8659be 100644 --- a/Development/nmos/json_fields.h +++ b/Development/nmos/json_fields.h @@ -151,15 +151,20 @@ 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_string fec_type{ U("fec_type") }; + const web::json::field_as_value_or fec_mode{ U("fec_mode"), {} }; // string + const web::json::field_as_integer fec_block_width{ U("fec_block_width") }; + const web::json::field_as_integer fec_block_height{ U("fec_block_height") }; 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 const web::json::field_as_value_or fec1D_source_port{ U("fec1D_source_port"), {} }; // string or integer const web::json::field_as_value_or fec2D_source_port{ U("fec2D_source_port"), {} }; // string or integer + const web::json::field_as_bool_or rtcp_enabled{ U("rtcp_enabled"), false }; const web::json::field_as_value_or rtcp_destination_ip{ U("rtcp_destination_ip"), {} }; // string const web::json::field_as_value_or rtcp_destination_port{ U("rtcp_destination_port"), {} }; // string or integer const web::json::field_as_value_or rtcp_source_port{ U("rtcp_source_port"), {} }; // string or integer - const web::json::field_as_value_or fec_mode{ U("fec_mode"), {} }; // string // for urn:x-nmos:transport:websocket const web::json::field_as_value_or connection_uri{ U("connection_uri"), {} }; // string or null const web::json::field_as_value_or connection_authorization{ U("connection_authorization"), {} }; // string or bool diff --git a/Development/nmos/test/connection_resources_test.cpp b/Development/nmos/test/connection_resources_test.cpp new file mode 100644 index 000000000..befe777fd --- /dev/null +++ b/Development/nmos/test/connection_resources_test.cpp @@ -0,0 +1,100 @@ +// The first "test" is of course whether the header compiles standalone +#include "nmos/connection_resources.h" + +#include "bst/test/test.h" +#include "nmos/connection_api.h" +#include "nmos/json_fields.h" +#include "nmos/resource.h" + +//////////////////////////////////////////////////////////////////////////////////////////// +BST_TEST_CASE(testConnectionRtpParameterSets) +{ + { + const auto resource = nmos::make_connection_rtp_receiver(U("receiver"), false); + const auto& constraints = resource.data.at(nmos::fields::endpoint_constraints).at(0); + const auto& transport_params = resource.data.at(nmos::fields::endpoint_staged).at(nmos::fields::transport_params).at(0); + + // Preserve the historical factory behaviour: core + multicast, without FEC or RTCP. + BST_REQUIRE(constraints.has_field(nmos::fields::multicast_ip)); + BST_REQUIRE(!constraints.has_field(nmos::fields::fec_enabled)); + BST_REQUIRE(!constraints.has_field(nmos::fields::rtcp_enabled)); + BST_REQUIRE(transport_params.has_field(nmos::fields::multicast_ip)); + BST_REQUIRE(!transport_params.has_field(nmos::fields::fec_enabled)); + BST_REQUIRE(!transport_params.has_field(nmos::fields::rtcp_enabled)); + } + + { + const nmos::rtp_sender_parameter_sets parameter_sets{ true, true }; + const auto resource = nmos::make_connection_rtp_sender(U("sender"), true, parameter_sets); + const auto& constraints = resource.data.at(nmos::fields::endpoint_constraints); + const auto& staged = resource.data.at(nmos::fields::endpoint_staged); + const auto& transport_params = staged.at(nmos::fields::transport_params); + + BST_REQUIRE_EQUAL(2, constraints.size()); + BST_REQUIRE_EQUAL(2, transport_params.size()); + for (size_t leg = 0; leg < constraints.size(); ++leg) + { + BST_REQUIRE(constraints.at(leg).has_field(nmos::fields::fec_enabled)); + BST_REQUIRE(constraints.at(leg).has_field(nmos::fields::fec_type)); + BST_REQUIRE(constraints.at(leg).has_field(nmos::fields::fec_block_width)); + BST_REQUIRE(constraints.at(leg).has_field(nmos::fields::rtcp_enabled)); + BST_REQUIRE(constraints.at(leg).has_field(nmos::fields::rtcp_source_port)); + + BST_REQUIRE(!nmos::fields::fec_enabled(transport_params.at(leg))); + BST_REQUIRE_EQUAL(U("XOR"), nmos::fields::fec_type(transport_params.at(leg))); + BST_REQUIRE(!nmos::fields::rtcp_enabled(transport_params.at(leg))); + BST_REQUIRE_EQUAL(U("auto"), nmos::fields::rtcp_destination_ip(transport_params.at(leg)).as_string()); + } + BST_REQUIRE(staged == resource.data.at(nmos::fields::endpoint_active)); + } + + { + const nmos::rtp_receiver_parameter_sets parameter_sets{ false, true, true }; + const auto resource = nmos::make_connection_rtp_receiver(U("receiver"), false, parameter_sets); + const auto& constraints = resource.data.at(nmos::fields::endpoint_constraints).at(0); + const auto& transport_params = resource.data.at(nmos::fields::endpoint_staged).at(nmos::fields::transport_params).at(0); + + BST_REQUIRE(!constraints.has_field(nmos::fields::multicast_ip)); + BST_REQUIRE(constraints.has_field(nmos::fields::fec_enabled)); + BST_REQUIRE(constraints.has_field(nmos::fields::fec_mode)); + BST_REQUIRE(constraints.has_field(nmos::fields::rtcp_enabled)); + BST_REQUIRE(!constraints.has_field(nmos::fields::rtcp_source_port)); + + BST_REQUIRE(!transport_params.has_field(nmos::fields::multicast_ip)); + BST_REQUIRE(!nmos::fields::fec_enabled(transport_params)); + BST_REQUIRE_EQUAL(U("auto"), nmos::fields::fec_mode(transport_params).as_string()); + BST_REQUIRE(!nmos::fields::rtcp_enabled(transport_params)); + } +} + +//////////////////////////////////////////////////////////////////////////////////////////// +BST_TEST_CASE(testConnectionRtpOptionalParameterSetsResolveAuto) +{ + { + const nmos::rtp_sender_parameter_sets parameter_sets{ false, true }; + auto resource = nmos::make_connection_rtp_sender(U("sender"), false, parameter_sets); + auto& transport_params = resource.data.at(nmos::fields::endpoint_active).at(nmos::fields::transport_params); + auto& params = transport_params.at(0); + params[nmos::fields::source_ip] = web::json::value::string(U("192.0.2.10")); + params[nmos::fields::destination_ip] = web::json::value::string(U("232.21.21.133")); + + nmos::resolve_rtp_auto(nmos::types::sender, transport_params, 5000); + + BST_REQUIRE_EQUAL(U("232.21.21.133"), nmos::fields::rtcp_destination_ip(params).as_string()); + BST_REQUIRE_EQUAL(5001, nmos::fields::rtcp_destination_port(params).as_integer()); + BST_REQUIRE_EQUAL(5001, nmos::fields::rtcp_source_port(params).as_integer()); + } + + { + const nmos::rtp_receiver_parameter_sets parameter_sets{ false, false, true }; + auto resource = nmos::make_connection_rtp_receiver(U("receiver"), false, parameter_sets); + auto& transport_params = resource.data.at(nmos::fields::endpoint_active).at(nmos::fields::transport_params); + auto& params = transport_params.at(0); + params[nmos::fields::interface_ip] = web::json::value::string(U("192.0.2.20")); + + nmos::resolve_rtp_auto(nmos::types::receiver, transport_params, 5000); + + BST_REQUIRE_EQUAL(U("192.0.2.20"), nmos::fields::rtcp_destination_ip(params).as_string()); + BST_REQUIRE_EQUAL(5001, nmos::fields::rtcp_destination_port(params).as_integer()); + } +} From 6918fbc3ff79a274a412f6570d7d57159702e085 Mon Sep 17 00:00:00 2001 From: Thang Luu Date: Tue, 14 Jul 2026 22:54:32 +0900 Subject: [PATCH 2/6] Support RTCP in SDP parsing and generation --- Development/nmos/sdp_utils.cpp | 42 ++++++++++- Development/nmos/test/sdp_utils_test.cpp | 91 ++++++++++++++++++++++++ Development/sdp/json.h | 3 + Development/sdp/sdp_grammar.cpp | 32 +++++++++ 4 files changed, 167 insertions(+), 1 deletion(-) diff --git a/Development/nmos/sdp_utils.cpp b/Development/nmos/sdp_utils.cpp index 93eec88d4..970529035 100644 --- a/Development/nmos/sdp_utils.cpp +++ b/Development/nmos/sdp_utils.cpp @@ -631,6 +631,29 @@ namespace nmos ); } + if (nmos::fields::rtcp_enabled(transport_param)) + { + // a=rtcp: [ ] + // See https://tools.ietf.org/html/rfc3605 + auto rtcp = value_of({ + { sdp::fields::port, transport_param.at(nmos::fields::rtcp_destination_port) } + }, keep_order); + const auto& rtcp_destination_ip = nmos::fields::rtcp_destination_ip(transport_param); + if (!rtcp_destination_ip.is_null()) + { + const auto& rtcp_address = rtcp_destination_ip.as_string(); + rtcp[sdp::fields::network_type] = value::string(sdp::network_types::internet.name); + rtcp[sdp::fields::address_type] = value::string(details::get_address_type_multicast(rtcp_address).first.name); + rtcp[sdp::fields::unicast_address] = value::string(rtcp_address); + } + web::json::push_back( + media_attributes, value_of({ + { sdp::fields::name, sdp::attributes::rtcp }, + { sdp::fields::value, std::move(rtcp) } + }, keep_order) + ); + } + if (0 != sdp_params.packet_time) { // a=ptime: @@ -954,11 +977,11 @@ namespace nmos // * Any Source Multicast // * Operation with SMPTE 2022-7 - Separate Source Addresses // * Operation with SMPTE 2022-7 - Separate Destination Addresses + // * Operation with RTCP // 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); @@ -1046,6 +1069,23 @@ namespace nmos params[nmos::fields::destination_port] = value::number(sdp::fields::port(media)); + if (!media_attributes.is_null()) + { + auto& ma = media_attributes.as_array(); + auto rtcp = sdp::find_name(ma, sdp::attributes::rtcp); + if (ma.end() != rtcp) + { + const auto& rtcp_value = sdp::fields::value(*rtcp); + params[nmos::fields::rtcp_enabled] = value::boolean(true); + params[nmos::fields::rtcp_destination_port] = value::number(sdp::fields::port(rtcp_value)); + params[nmos::fields::rtcp_destination_ip] = rtcp_value.has_field(sdp::fields::unicast_address) + ? rtcp_value.at(sdp::fields::unicast_address) + : !params[nmos::fields::multicast_ip].is_null() + ? params[nmos::fields::multicast_ip] + : params[nmos::fields::interface_ip]; + } + } + params[nmos::fields::rtp_enabled] = value::boolean(true); web::json::push_back(transport_params, params); diff --git a/Development/nmos/test/sdp_utils_test.cpp b/Development/nmos/test/sdp_utils_test.cpp index 0d34eae33..7e29d7b44 100644 --- a/Development/nmos/test/sdp_utils_test.cpp +++ b/Development/nmos/test/sdp_utils_test.cpp @@ -470,6 +470,97 @@ a=mid:S1b BST_REQUIRE(test_params == transport_params); } +//////////////////////////////////////////////////////////////////////////////////////////// +BST_TEST_CASE(testInterpretationOfSdpFilesRtcp) +{ + 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-rtcp + + const std::string test_sdp = R"(v=0 +o=- 1497010742 1497010742 IN IP4 172.29.26.24 +s=SDP Example +t=2873397496 2873404696 +m=video 5000 RTP/AVP 103 +c=IN IP4 232.21.21.133/32 +a=source-filter:incl IN IP4 232.21.21.133 172.29.226.24 +a=rtpmap:103 raw/90000 +a=rtcp:5001 IN IP4 232.21.21.133 +)"; + + const auto test_params = value_of({ + value_of({ + { nmos::fields::source_ip, U("172.29.226.24") }, + { nmos::fields::multicast_ip, U("232.21.21.133") }, + { nmos::fields::interface_ip, U("auto") }, + { nmos::fields::destination_port, 5000 }, + { nmos::fields::rtcp_enabled, true }, + { nmos::fields::rtcp_destination_ip, U("232.21.21.133") }, + { nmos::fields::rtcp_destination_port, 5001 }, + { nmos::fields::rtp_enabled, true } + }) + }); + + const auto session_description = sdp::parse_session_description(test_sdp); + const auto transport_params = nmos::get_session_description_transport_params(session_description); + + BST_REQUIRE(test_params == transport_params); + + // RFC 3605 permits the address to be omitted, in which case the RTP connection address is used. + const std::string rtcp_attribute = "a=rtcp:5001 IN IP4 232.21.21.133"; + auto default_address_sdp = test_sdp; + default_address_sdp.replace(default_address_sdp.find(rtcp_attribute), rtcp_attribute.size(), "a=rtcp:5001"); + BST_REQUIRE(test_params == nmos::get_session_description_transport_params(sdp::parse_session_description(default_address_sdp))); + + auto invalid_sdp = test_sdp; + invalid_sdp.replace(invalid_sdp.find(rtcp_attribute), rtcp_attribute.size(), "a=rtcp:5001 IN IP4"); + BST_REQUIRE_THROW(sdp::parse_session_description(invalid_sdp), std::runtime_error); + + invalid_sdp = test_sdp; + invalid_sdp.replace(invalid_sdp.find(rtcp_attribute), rtcp_attribute.size(), "a=rtcp:5001 IN"); + BST_REQUIRE_THROW(sdp::parse_session_description(invalid_sdp), std::runtime_error); +} + +//////////////////////////////////////////////////////////////////////////////////////////// +BST_TEST_CASE(testSdpTransportParamsRtcpRoundtrip) +{ + using web::json::value_of; + + const auto test_sdp_params = nmos::sdp_parameters(U("SDP Example"), nmos::sdp_parameters::video_t{}, 103); + const auto sender_params = value_of({ + value_of({ + { nmos::fields::source_ip, U("172.29.226.24") }, + { nmos::fields::destination_ip, U("232.21.21.133") }, + { nmos::fields::source_port, 5004 }, + { nmos::fields::destination_port, 5000 }, + { nmos::fields::rtcp_enabled, true }, + { nmos::fields::rtcp_destination_ip, U("232.21.21.133") }, + { nmos::fields::rtcp_destination_port, 5001 }, + { nmos::fields::rtcp_source_port, 5005 }, + { nmos::fields::rtp_enabled, true } + }) + }); + const auto receiver_params = value_of({ + value_of({ + { nmos::fields::source_ip, U("172.29.226.24") }, + { nmos::fields::multicast_ip, U("232.21.21.133") }, + { nmos::fields::interface_ip, U("auto") }, + { nmos::fields::destination_port, 5000 }, + { nmos::fields::rtcp_enabled, true }, + { nmos::fields::rtcp_destination_ip, U("232.21.21.133") }, + { nmos::fields::rtcp_destination_port, 5001 }, + { nmos::fields::rtp_enabled, true } + }) + }); + + const auto sender_sdp = nmos::make_session_description(test_sdp_params, sender_params); + BST_REQUIRE(receiver_params == nmos::get_session_description_transport_params(sender_sdp)); + + const auto text = sdp::make_session_description(sender_sdp); + BST_REQUIRE(std::string::npos != text.find("a=rtcp:5001 IN IP4 232.21.21.133")); + BST_REQUIRE(receiver_params == nmos::get_session_description_transport_params(sdp::parse_session_description(text))); +} + //////////////////////////////////////////////////////////////////////////////////////////// BST_TEST_CASE(testSdpTransportParamsUnicast) { diff --git a/Development/sdp/json.h b/Development/sdp/json.h index 39da70878..f34cfb181 100644 --- a/Development/sdp/json.h +++ b/Development/sdp/json.h @@ -136,6 +136,9 @@ namespace sdp // See https://tools.ietf.org/html/rfc4570 const utility::string_t source_filter{ U("source-filter") }; + // See https://tools.ietf.org/html/rfc3605 + const utility::string_t rtcp{ U("rtcp") }; + // See https://tools.ietf.org/html/rfc5888 const utility::string_t group{ U("group") }; const utility::string_t mid{ U("mid") }; diff --git a/Development/sdp/sdp_grammar.cpp b/Development/sdp/sdp_grammar.cpp index 16e03be19..9151c7196 100644 --- a/Development/sdp/sdp_grammar.cpp +++ b/Development/sdp/sdp_grammar.cpp @@ -654,6 +654,38 @@ namespace sdp }, } }, + // See https://tools.ietf.org/html/rfc3605 + { + sdp::attributes::rtcp, // [ ] + { + [](const web::json::value& v) { + std::string s; + s += digits_converter.format(v.at(sdp::fields::port)); + if (v.has_field(sdp::fields::unicast_address)) + { + s += " " + string_converter.format(v.at(sdp::fields::network_type)); + s += " " + string_converter.format(v.at(sdp::fields::address_type)); + s += " " + string_converter.format(v.at(sdp::fields::unicast_address)); + } + return s; + }, + [](const std::string& s) { + auto v = web::json::value::object(keep_order); + size_t pos = 0; + const bst::regex whitespace{ "[ \\t]+" }; + v[sdp::fields::port] = digits_converter.parse(substr_find(s, pos, whitespace)); + if (std::string::npos != pos) + { + v[sdp::fields::network_type] = string_converter.parse(substr_find(s, pos, whitespace)); + if (std::string::npos == pos) throw sdp_parse_error("expected a value for " + utility::us2s(sdp::fields::address_type)); + v[sdp::fields::address_type] = string_converter.parse(substr_find(s, pos, whitespace)); + if (std::string::npos == pos) throw sdp_parse_error("expected a value for " + utility::us2s(sdp::fields::unicast_address)); + v[sdp::fields::unicast_address] = string_converter.parse(substr_find(s, pos)); + } + return v; + }, + } + }, // See https://tools.ietf.org/html/rfc5888 { sdp::attributes::group, // [ ]* From 5774c30e2a5f81e8a368085026e93e624e04069f Mon Sep 17 00:00:00 2001 From: Thang Luu Date: Tue, 14 Jul 2026 23:15:28 +0900 Subject: [PATCH 3/6] Make RTCP attribute parsing deterministic --- Development/sdp/sdp_grammar.cpp | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/Development/sdp/sdp_grammar.cpp b/Development/sdp/sdp_grammar.cpp index 9151c7196..7ce88fbfb 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 whitespace_strings_converter = array_converter(string_converter, " ", "[ \\t]+"); // 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" @@ -670,17 +671,16 @@ namespace sdp return s; }, [](const std::string& s) { + const auto parts = whitespace_strings_converter.parse(s); + if (1 != parts.size() && 4 != parts.size()) throw sdp_parse_error("expected [ ]"); + auto v = web::json::value::object(keep_order); - size_t pos = 0; - const bst::regex whitespace{ "[ \\t]+" }; - v[sdp::fields::port] = digits_converter.parse(substr_find(s, pos, whitespace)); - if (std::string::npos != pos) + v[sdp::fields::port] = digits_converter.parse(utility::us2s(parts.at(0).as_string())); + if (4 == parts.size()) { - v[sdp::fields::network_type] = string_converter.parse(substr_find(s, pos, whitespace)); - if (std::string::npos == pos) throw sdp_parse_error("expected a value for " + utility::us2s(sdp::fields::address_type)); - v[sdp::fields::address_type] = string_converter.parse(substr_find(s, pos, whitespace)); - if (std::string::npos == pos) throw sdp_parse_error("expected a value for " + utility::us2s(sdp::fields::unicast_address)); - v[sdp::fields::unicast_address] = string_converter.parse(substr_find(s, pos)); + v[sdp::fields::network_type] = parts.at(1); + v[sdp::fields::address_type] = parts.at(2); + v[sdp::fields::unicast_address] = parts.at(3); } return v; }, From 278f2e849d08015a021898da9e99e626f5d23696 Mon Sep 17 00:00:00 2001 From: Thang Luu Date: Fri, 17 Jul 2026 01:32:59 +0900 Subject: [PATCH 4/6] Fix RTCP parsing on Windows --- Development/nmos/test/sdp_utils_test.cpp | 78 +++++++++++++++--------- Development/sdp/sdp_grammar.cpp | 16 +++-- 2 files changed, 60 insertions(+), 34 deletions(-) diff --git a/Development/nmos/test/sdp_utils_test.cpp b/Development/nmos/test/sdp_utils_test.cpp index 7e29d7b44..ea771915e 100644 --- a/Development/nmos/test/sdp_utils_test.cpp +++ b/Development/nmos/test/sdp_utils_test.cpp @@ -470,14 +470,9 @@ a=mid:S1b BST_REQUIRE(test_params == transport_params); } -//////////////////////////////////////////////////////////////////////////////////////////// -BST_TEST_CASE(testInterpretationOfSdpFilesRtcp) +namespace { - 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-rtcp - - const std::string test_sdp = R"(v=0 + const std::string rtcp_test_sdp = R"(v=0 o=- 1497010742 1497010742 IN IP4 172.29.26.24 s=SDP Example t=2873397496 2873404696 @@ -488,36 +483,63 @@ a=rtpmap:103 raw/90000 a=rtcp:5001 IN IP4 232.21.21.133 )"; - const auto test_params = value_of({ - value_of({ - { nmos::fields::source_ip, U("172.29.226.24") }, - { nmos::fields::multicast_ip, U("232.21.21.133") }, - { nmos::fields::interface_ip, U("auto") }, - { nmos::fields::destination_port, 5000 }, - { nmos::fields::rtcp_enabled, true }, - { nmos::fields::rtcp_destination_ip, U("232.21.21.133") }, - { nmos::fields::rtcp_destination_port, 5001 }, - { nmos::fields::rtp_enabled, true } - }) - }); + web::json::value make_rtcp_test_params() + { + using web::json::value_of; - const auto session_description = sdp::parse_session_description(test_sdp); + return value_of({ + value_of({ + { nmos::fields::source_ip, U("172.29.226.24") }, + { nmos::fields::multicast_ip, U("232.21.21.133") }, + { nmos::fields::interface_ip, U("auto") }, + { nmos::fields::destination_port, 5000 }, + { nmos::fields::rtcp_enabled, true }, + { nmos::fields::rtcp_destination_ip, U("232.21.21.133") }, + { nmos::fields::rtcp_destination_port, 5001 }, + { nmos::fields::rtp_enabled, true } + }) + }); + } +} + +//////////////////////////////////////////////////////////////////////////////////////////// +BST_TEST_CASE(testInterpretationOfSdpFilesRtcp) +{ + // See https://specs.amwa.tv/is-05/releases/v1.1.1/docs/4.1._Behaviour_-_RTP_Transport_Type.html#operation-with-rtcp + + const auto session_description = sdp::parse_session_description(rtcp_test_sdp); const auto transport_params = nmos::get_session_description_transport_params(session_description); - BST_REQUIRE(test_params == transport_params); + BST_REQUIRE(make_rtcp_test_params() == transport_params); +} +//////////////////////////////////////////////////////////////////////////////////////////// +BST_TEST_CASE(testInterpretationOfSdpFilesRtcpPortOnly) +{ // RFC 3605 permits the address to be omitted, in which case the RTP connection address is used. + + const std::string rtcp_attribute = "a=rtcp:5001 IN IP4 232.21.21.133"; + auto test_sdp = rtcp_test_sdp; + const auto rtcp_attribute_pos = test_sdp.find(rtcp_attribute); + BST_REQUIRE(std::string::npos != rtcp_attribute_pos); + test_sdp.replace(rtcp_attribute_pos, rtcp_attribute.size(), "a=rtcp:5001"); + + BST_REQUIRE(make_rtcp_test_params() == nmos::get_session_description_transport_params(sdp::parse_session_description(test_sdp))); +} + +//////////////////////////////////////////////////////////////////////////////////////////// +BST_TEST_CASE(testInterpretationOfSdpFilesRtcpMalformed) +{ const std::string rtcp_attribute = "a=rtcp:5001 IN IP4 232.21.21.133"; - auto default_address_sdp = test_sdp; - default_address_sdp.replace(default_address_sdp.find(rtcp_attribute), rtcp_attribute.size(), "a=rtcp:5001"); - BST_REQUIRE(test_params == nmos::get_session_description_transport_params(sdp::parse_session_description(default_address_sdp))); + const auto rtcp_attribute_pos = rtcp_test_sdp.find(rtcp_attribute); + BST_REQUIRE(std::string::npos != rtcp_attribute_pos); - auto invalid_sdp = test_sdp; - invalid_sdp.replace(invalid_sdp.find(rtcp_attribute), rtcp_attribute.size(), "a=rtcp:5001 IN IP4"); + auto invalid_sdp = rtcp_test_sdp; + invalid_sdp.replace(rtcp_attribute_pos, rtcp_attribute.size(), "a=rtcp:5001 IN IP4"); BST_REQUIRE_THROW(sdp::parse_session_description(invalid_sdp), std::runtime_error); - invalid_sdp = test_sdp; - invalid_sdp.replace(invalid_sdp.find(rtcp_attribute), rtcp_attribute.size(), "a=rtcp:5001 IN"); + invalid_sdp = rtcp_test_sdp; + invalid_sdp.replace(rtcp_attribute_pos, rtcp_attribute.size(), "a=rtcp:5001 IN"); BST_REQUIRE_THROW(sdp::parse_session_description(invalid_sdp), std::runtime_error); } diff --git a/Development/sdp/sdp_grammar.cpp b/Development/sdp/sdp_grammar.cpp index 7ce88fbfb..f11d46f87 100644 --- a/Development/sdp/sdp_grammar.cpp +++ b/Development/sdp/sdp_grammar.cpp @@ -203,7 +203,6 @@ namespace sdp } const converter strings_converter = array_converter(string_converter, " "); - const converter whitespace_strings_converter = array_converter(string_converter, " ", "[ \\t]+"); // 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" @@ -671,16 +670,21 @@ namespace sdp return s; }, [](const std::string& s) { - const auto parts = whitespace_strings_converter.parse(s); + std::istringstream is(s); + std::vector parts; + for (std::string part; is >> part;) + { + parts.push_back(std::move(part)); + } if (1 != parts.size() && 4 != parts.size()) throw sdp_parse_error("expected [ ]"); auto v = web::json::value::object(keep_order); - v[sdp::fields::port] = digits_converter.parse(utility::us2s(parts.at(0).as_string())); + v[sdp::fields::port] = digits_converter.parse(parts.at(0)); if (4 == parts.size()) { - v[sdp::fields::network_type] = parts.at(1); - v[sdp::fields::address_type] = parts.at(2); - v[sdp::fields::unicast_address] = parts.at(3); + v[sdp::fields::network_type] = string_converter.parse(parts.at(1)); + v[sdp::fields::address_type] = string_converter.parse(parts.at(2)); + v[sdp::fields::unicast_address] = string_converter.parse(parts.at(3)); } return v; }, From faaab83d6a558c77b9169a51bcf2690762b119e9 Mon Sep 17 00:00:00 2001 From: thang Date: Sat, 25 Jul 2026 00:58:03 +0900 Subject: [PATCH 5/6] Update Development/nmos/sdp_utils.cpp Co-authored-by: Gareth Sylvester-Bradley <31761158+garethsb@users.noreply.github.com> --- Development/nmos/sdp_utils.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Development/nmos/sdp_utils.cpp b/Development/nmos/sdp_utils.cpp index 970529035..7f215ec04 100644 --- a/Development/nmos/sdp_utils.cpp +++ b/Development/nmos/sdp_utils.cpp @@ -1078,11 +1078,12 @@ namespace nmos const auto& rtcp_value = sdp::fields::value(*rtcp); params[nmos::fields::rtcp_enabled] = value::boolean(true); params[nmos::fields::rtcp_destination_port] = value::number(sdp::fields::port(rtcp_value)); - params[nmos::fields::rtcp_destination_ip] = rtcp_value.has_field(sdp::fields::unicast_address) + auto rtcp_destination_ip = rtcp_value.has_field(sdp::fields::unicast_address) ? rtcp_value.at(sdp::fields::unicast_address) : !params[nmos::fields::multicast_ip].is_null() ? params[nmos::fields::multicast_ip] : params[nmos::fields::interface_ip]; + params[nmos::fields::rtcp_destination_ip] = std::move(rtcp_destination_ip); } } From bbafe1f6638827ca30a7b7cce65b22f6838ccff7 Mon Sep 17 00:00:00 2001 From: Thang Luu Date: Sat, 25 Jul 2026 01:09:17 +0900 Subject: [PATCH 6/6] Use strings_converter for RTCP parsing --- Development/sdp/sdp_grammar.cpp | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/Development/sdp/sdp_grammar.cpp b/Development/sdp/sdp_grammar.cpp index f11d46f87..63814538d 100644 --- a/Development/sdp/sdp_grammar.cpp +++ b/Development/sdp/sdp_grammar.cpp @@ -670,21 +670,16 @@ namespace sdp return s; }, [](const std::string& s) { - std::istringstream is(s); - std::vector parts; - for (std::string part; is >> part;) - { - parts.push_back(std::move(part)); - } + const auto parts = strings_converter.parse(s); if (1 != parts.size() && 4 != parts.size()) throw sdp_parse_error("expected [ ]"); auto v = web::json::value::object(keep_order); - v[sdp::fields::port] = digits_converter.parse(parts.at(0)); + v[sdp::fields::port] = digits_converter.parse(utility::us2s(parts.at(0).as_string())); if (4 == parts.size()) { - v[sdp::fields::network_type] = string_converter.parse(parts.at(1)); - v[sdp::fields::address_type] = string_converter.parse(parts.at(2)); - v[sdp::fields::unicast_address] = string_converter.parse(parts.at(3)); + v[sdp::fields::network_type] = parts.at(1); + v[sdp::fields::address_type] = parts.at(2); + v[sdp::fields::unicast_address] = parts.at(3); } return v; },