diff --git a/.github/workflows/src/amwa-test.yml b/.github/workflows/src/amwa-test.yml index 68f7778b1..dce8d6078 100644 --- a/.github/workflows/src/amwa-test.yml +++ b/.github/workflows/src/amwa-test.yml @@ -164,7 +164,7 @@ # nmos-cpp-node doesn't currently support advertising hostnames to Avahi avahi-publish -a -R nmos-api.local ${{ env.HOST_IP_ADDRESS }} & fi - + ${{ env.GITHUB_WORKSPACE_BASH }}/Sandbox/run_nmos_testing.sh "$run_python" ${domain} ${root_dir}/build/nmos-cpp-node ${root_dir}/build/nmos-cpp-registry results badges $GITHUB_STEP_SUMMARY ${{ env.HOST_IP_ADDRESS }} "${{ env.GITHUB_COMMIT }}-${{ env.BUILD_NAME }}-" if [[ "${{ runner.os }}" == "Linux" ]]; then diff --git a/Development/cmake/NmosCppLibraries.cmake b/Development/cmake/NmosCppLibraries.cmake index ca49cd857..dc325e8ee 100644 --- a/Development/cmake/NmosCppLibraries.cmake +++ b/Development/cmake/NmosCppLibraries.cmake @@ -761,6 +761,95 @@ target_include_directories(nmos_is10_schemas PUBLIC list(APPEND NMOS_CPP_TARGETS nmos_is10_schemas) add_library(nmos-cpp::nmos_is10_schemas ALIAS nmos_is10_schemas) +# nmos_is11_schemas library + +set(NMOS_IS11_SCHEMAS_HEADERS + nmos/is11_schemas/is11_schemas.h + ) + +set(NMOS_IS11_V1_0_TAG v1.0.x) + +set(NMOS_IS11_V1_0_SCHEMAS_JSON + third_party/is-11/${NMOS_IS11_V1_0_TAG}/APIs/schemas/constraints_active.json + third_party/is-11/${NMOS_IS11_V1_0_TAG}/APIs/schemas/constraints-base.json + third_party/is-11/${NMOS_IS11_V1_0_TAG}/APIs/schemas/constraint_set.json + third_party/is-11/${NMOS_IS11_V1_0_TAG}/APIs/schemas/constraints_supported.json + third_party/is-11/${NMOS_IS11_V1_0_TAG}/APIs/schemas/empty_constraints_active.json + third_party/is-11/${NMOS_IS11_V1_0_TAG}/APIs/schemas/error.json + third_party/is-11/${NMOS_IS11_V1_0_TAG}/APIs/schemas/input-edid-base.json + third_party/is-11/${NMOS_IS11_V1_0_TAG}/APIs/schemas/input.json + third_party/is-11/${NMOS_IS11_V1_0_TAG}/APIs/schemas/input-output-base.json + third_party/is-11/${NMOS_IS11_V1_0_TAG}/APIs/schemas/output.json + third_party/is-11/${NMOS_IS11_V1_0_TAG}/APIs/schemas/param_constraint_boolean.json + third_party/is-11/${NMOS_IS11_V1_0_TAG}/APIs/schemas/param_constraint_integer.json + third_party/is-11/${NMOS_IS11_V1_0_TAG}/APIs/schemas/param_constraint.json + third_party/is-11/${NMOS_IS11_V1_0_TAG}/APIs/schemas/param_constraint_number.json + third_party/is-11/${NMOS_IS11_V1_0_TAG}/APIs/schemas/param_constraint_rational.json + third_party/is-11/${NMOS_IS11_V1_0_TAG}/APIs/schemas/param_constraint_string.json + third_party/is-11/${NMOS_IS11_V1_0_TAG}/APIs/schemas/receiver-base.json + third_party/is-11/${NMOS_IS11_V1_0_TAG}/APIs/schemas/receiver-status.json + third_party/is-11/${NMOS_IS11_V1_0_TAG}/APIs/schemas/resource_core.json + third_party/is-11/${NMOS_IS11_V1_0_TAG}/APIs/schemas/resource-list.json + third_party/is-11/${NMOS_IS11_V1_0_TAG}/APIs/schemas/sender-base.json + third_party/is-11/${NMOS_IS11_V1_0_TAG}/APIs/schemas/sender-status.json + third_party/is-11/${NMOS_IS11_V1_0_TAG}/APIs/schemas/streamcompatibility-api-base.json + third_party/is-11/${NMOS_IS11_V1_0_TAG}/APIs/schemas/uuid-list.json + ) + +set(NMOS_IS11_SCHEMAS_JSON_MATCH "third_party/is-11/([^/]+)/APIs/schemas/([^;]+)\\.json") +set(NMOS_IS11_SCHEMAS_SOURCE_REPLACE "${CMAKE_CURRENT_BINARY_DIR_REPLACE}/nmos/is11_schemas/\\1/\\2.cpp") +string(REGEX REPLACE "${NMOS_IS11_SCHEMAS_JSON_MATCH}(;|$)" "${NMOS_IS11_SCHEMAS_SOURCE_REPLACE}\\3" NMOS_IS11_V1_0_SCHEMAS_SOURCES "${NMOS_IS11_V1_0_SCHEMAS_JSON}") + +foreach(JSON ${NMOS_IS11_V1_0_SCHEMAS_JSON}) + string(REGEX REPLACE "${NMOS_IS11_SCHEMAS_JSON_MATCH}" "${NMOS_IS11_SCHEMAS_SOURCE_REPLACE}" SOURCE "${JSON}") + string(REGEX REPLACE "${NMOS_IS11_SCHEMAS_JSON_MATCH}" "\\1" NS "${JSON}") + string(REGEX REPLACE "${NMOS_IS11_SCHEMAS_JSON_MATCH}" "\\2" VAR "${JSON}") + string(MAKE_C_IDENTIFIER "${NS}" NS) + string(MAKE_C_IDENTIFIER "${VAR}" VAR) + + file(WRITE "${SOURCE}.in" "\ +// Auto-generated from: ${JSON}\n\ +\n\ +namespace nmos\n\ +{\n\ + namespace is11_schemas\n\ + {\n\ + namespace ${NS}\n\ + {\n\ + const char* ${VAR} = R\"-auto-generated-(") + + file(READ "${JSON}" RAW) + file(APPEND "${SOURCE}.in" "${RAW}") + + file(APPEND "${SOURCE}.in" ")-auto-generated-\";\n\ + }\n\ + }\n\ +}\n") + + configure_file("${SOURCE}.in" "${SOURCE}" COPYONLY) +endforeach() + +add_library( + nmos_is11_schemas STATIC + ${NMOS_IS11_SCHEMAS_HEADERS} + ${NMOS_IS11_V1_0_SCHEMAS_SOURCES} + ) + +source_group("nmos\\is11_schemas\\Header Files" FILES ${NMOS_IS11_SCHEMAS_HEADERS}) +source_group("nmos\\is11_schemas\\${NMOS_IS11_V1_0_TAG}\\Source Files" FILES ${NMOS_IS11_V1_0_SCHEMAS_SOURCES}) + +target_link_libraries( + nmos_is11_schemas PRIVATE + nmos-cpp::compile-settings + ) +target_include_directories(nmos_is11_schemas PUBLIC + $ + $ + ) + +list(APPEND NMOS_CPP_TARGETS nmos_is11_schemas) +add_library(nmos-cpp::nmos_is11_schemas ALIAS nmos_is11_schemas) + # nmos_is12_schemas library set(NMOS_IS12_SCHEMAS_HEADERS @@ -1021,6 +1110,7 @@ set(NMOS_CPP_NMOS_SOURCES nmos/connection_api.cpp nmos/connection_events_activation.cpp nmos/connection_resources.cpp + nmos/constraints.cpp nmos/control_protocol_behaviour.cpp nmos/control_protocol_handlers.cpp nmos/control_protocol_methods.cpp @@ -1077,6 +1167,12 @@ set(NMOS_CPP_NMOS_SOURCES nmos/server_utils.cpp nmos/settings.cpp nmos/settings_api.cpp + nmos/streamcompatibility_api.cpp + nmos/streamcompatibility_behaviour.cpp + nmos/streamcompatibility_resource.cpp + nmos/streamcompatibility_resources.cpp + nmos/streamcompatibility_utils.cpp + nmos/streamcompatibility_validation.cpp nmos/system_api.cpp nmos/system_resources.cpp nmos/video_jxsv.cpp @@ -1120,6 +1216,7 @@ set(NMOS_CPP_NMOS_HEADERS nmos/connection_api.h nmos/connection_events_activation.h nmos/connection_resources.h + nmos/constraints.h nmos/control_protocol_behaviour.h nmos/control_protocol_handlers.h nmos/control_protocol_methods.h @@ -1200,6 +1297,7 @@ set(NMOS_CPP_NMOS_HEADERS nmos/scope.h nmos/sdp_attributes.h nmos/sdp_utils.h + nmos/streamcompatibility_state.h nmos/server.h nmos/server_utils.h nmos/settings.h @@ -1207,6 +1305,12 @@ set(NMOS_CPP_NMOS_HEADERS nmos/slog.h nmos/ssl_context_options.h nmos/st2110_21_sender_type.h + nmos/streamcompatibility_api.h + nmos/streamcompatibility_behaviour.h + nmos/streamcompatibility_resource.h + nmos/streamcompatibility_resources.h + nmos/streamcompatibility_utils.h + nmos/streamcompatibility_validation.h nmos/string_enum.h nmos/string_enum_fwd.h nmos/system_api.h @@ -1301,6 +1405,7 @@ target_link_libraries( nmos-cpp::nmos_is08_schemas nmos-cpp::nmos_is09_schemas nmos-cpp::nmos_is10_schemas + nmos-cpp::nmos_is11_schemas nmos-cpp::nmos_is12_schemas nmos-cpp::nmos_is14_schemas nmos-cpp::mdns diff --git a/Development/cmake/NmosCppTest.cmake b/Development/cmake/NmosCppTest.cmake index b2328fd74..180a93cd7 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/constraints_test.cpp nmos/test/configuration_methods_test.cpp nmos/test/configuration_resources_test.cpp nmos/test/configuration_utils_test.cpp @@ -60,6 +61,7 @@ set(NMOS_CPP_TEST_NMOS_TEST_SOURCES nmos/test/sdp_test_utils.cpp nmos/test/sdp_utils_test.cpp nmos/test/slog_test.cpp + nmos/test/streamcompatibility_validation_test.cpp nmos/test/system_resources_test.cpp nmos/test/video_jxsv_test.cpp ) diff --git a/Development/nmos-cpp-node/config.json b/Development/nmos-cpp-node/config.json index c529de695..ce8b29c91 100644 --- a/Development/nmos-cpp-node/config.json +++ b/Development/nmos-cpp-node/config.json @@ -63,6 +63,13 @@ // simulate_status_monitor_activity: when true status monitor statuses will change randomly after activation //"simulate_status_monitor_activity": true, + // edid_support: controls whether inputs and output have EDID support + //"edid_support": false, + + // simulate_sender_volatile_activity: when true sender status will change randomly after activation + // this can trigger the Stream Compatibility behaviour thread to deactivate the active sender when its status has changed to active_constraints_violation + //"simulate_sender_volatile_activity": false, + // Configuration settings and defaults for logging // error_log [registry, node]: filename for the error log or an empty string to write to stderr @@ -107,6 +114,9 @@ // is10_versions [registry, node]: used to specify the enabled API versions for a version-locked configuration //"is10_versions": ["v1.0"], + // is11_versions [node]: used to specify the enabled API versions for a version-locked configuration + //"is11_versions": ["v1.0"], + // is12_versions [node]: used to specify the enabled API versions for a version-locked configuration //"is12_versions": ["v1.0"], @@ -153,6 +163,7 @@ // control_protocol_ws_port [node]: used to construct request URLs for the Control Protocol websocket, or negative to disable the control protocol features //"control_protocol_ws_port": 3218, //"configuration_port": 3219, + //"streamcompatibility_port": 3220, // listen_backlog [registry, node]: the maximum length of the queue of pending connections, or zero for the implementation default (the implementation may not honour this value) //"listen_backlog": 0, diff --git a/Development/nmos-cpp-node/node_implementation.cpp b/Development/nmos-cpp-node/node_implementation.cpp index 4d76ab15c..6a958306f 100644 --- a/Development/nmos-cpp-node/node_implementation.cpp +++ b/Development/nmos-cpp-node/node_implementation.cpp @@ -8,6 +8,7 @@ #include #include #include +#include "bst/optional.h" #include "pplx/pplx_utils.h" // for pplx::complete_after, etc. #include "cpprest/host_utils.h" #ifdef HAVE_LLDP @@ -15,6 +16,7 @@ #endif #include "nmos/activation_mode.h" #include "nmos/capabilities.h" +#include "nmos/constraints.h" #include "nmos/channels.h" #include "nmos/channelmapping_resources.h" #include "nmos/clock_name.h" @@ -47,6 +49,9 @@ #include "nmos/sdp_utils.h" #include "nmos/slog.h" #include "nmos/st2110_21_sender_type.h" +#include "nmos/streamcompatibility_resources.h" +#include "nmos/streamcompatibility_utils.h" +#include "nmos/streamcompatibility_validation.h" #include "nmos/system_resources.h" #include "nmos/transfer_characteristic.h" #include "nmos/transport.h" @@ -127,6 +132,13 @@ namespace impl // simulate_status_monitor_activity: when true status monitor statuses will change randomly after activation const web::json::field_as_bool_or simulate_status_monitor_activity{U("simulate_status_monitor_activity"), true}; + + // edid_support: controls whether inputs and output have EDID support + const web::json::field_as_bool_or edid_support{ U("edid_support"), false }; + + // simulate_sender_volatile_activity: when true sender status will change randomly after activation + // this can trigger the Stream Compatibility behaviour thread to deactivate the active sender when its status has changed to active_constraints_violation + const web::json::field_as_bool_or simulate_sender_volatile_activity{ U("simulate_sender_volatile_activity"), false }; } nmos::interlace_mode get_interlace_mode(const nmos::settings& settings); @@ -203,6 +215,19 @@ namespace impl nmos::nc::counter late_packet_counter; }; std::vector nic_packet_counters; + + 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)) + { + nmos::validate_video_jxsv_sdp_parameters(receiver, sdp_params); + } + else + { + // validate core media types, i.e., "video/raw", "audio/L", "video/smpte291" and "video/SMPTE2022-6" + nmos::validate_sdp_parameters(receiver, sdp_params); + } + }; } // forward declarations for node_implementation_thread @@ -281,6 +306,7 @@ void node_implementation_init(nmos::node_model& model, nmos::experimental::contr const auto video_type = nmos::media_type{ impl::fields::video_type(model.settings) }; const auto channel_count = impl::fields::channel_count(model.settings); const auto smpte2022_7 = impl::fields::smpte2022_7(model.settings); + const auto edid_support = impl::fields::edid_support(model.settings); // for now, some typical values for video/jxsv, based on VSF TR-08:2022 // see https://vsf.tv/download/technical_recommendations/VSF_TR-08_2022-04-20.pdf @@ -295,7 +321,7 @@ void node_implementation_init(nmos::node_model& model, nmos::experimental::contr const unsigned int delay_millis{ 0 }; // it is important that the model be locked before inserting, updating or deleting a resource - // and that the the node behaviour thread be notified after doing so + // and that the node behaviour thread be notified after doing so const auto insert_resource_after = [&model, &lock](unsigned int milliseconds, nmos::resources& resources, nmos::resource&& resource, slog::base_gate& gate) { if (nmos::details::wait_for(model.shutdown_condition, lock, bst::chrono::milliseconds(milliseconds), [&] { return model.shutdown; })) return false; @@ -938,6 +964,113 @@ void node_implementation_init(nmos::node_model& model, nmos::experimental::contr if (!insert_resource_after(delay_millis, model.channelmapping_resources, std::move(channelmapping_output), gate)) throw node_implementation_init_exception(); } + // example IS-11 input and senders + if (0 <= nmos::fields::streamcompatibility_port(model.settings)) + { + unsigned char edid_bytes[] = { + 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, + 0x04, 0x21, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x0a, 0x01, 0x04, 0x80, 0x00, 0x00, 0x00, + 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x01, + 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, + 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x00, 0x00, + 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + }; + utility::string_t edid(edid_bytes, edid_bytes + sizeof(edid_bytes)); + + const auto input_id = impl::make_id(seed_id, nmos::types::input); + const auto sender_ids = impl::make_ids(seed_id, nmos::types::sender, { impl::ports::video, impl::ports::audio }, how_many); + + auto input = edid_support + ? nmos::experimental::make_streamcompatibility_input(input_id, device_id, true, true, edid, sender_ids, model.settings) + : nmos::experimental::make_streamcompatibility_input(input_id, device_id, true, sender_ids, model.settings); + impl::set_label_description(input, impl::ports::mux, 0); // The single Input consumes both video and audio signals + if (!insert_resource_after(delay_millis, model.streamcompatibility_resources, std::move(input), gate)) throw node_implementation_init_exception(); + + const std::vector video_parameter_constraints{ + nmos::caps::meta::label.key, + nmos::caps::meta::preference.key, + nmos::caps::meta::enabled.key, + nmos::caps::format::media_type.key, + nmos::caps::format::grain_rate.key, + nmos::caps::format::frame_width.key, + nmos::caps::format::frame_height.key, + nmos::caps::format::interlace_mode.key, + nmos::caps::format::colorspace.key, + nmos::caps::format::color_sampling.key, + nmos::caps::format::component_depth.key + }; + + const std::vector audio_parameter_constraints{ + nmos::caps::meta::label.key, + nmos::caps::meta::preference.key, + nmos::caps::meta::enabled.key, + nmos::caps::format::media_type.key, + nmos::caps::format::channel_count.key, + nmos::caps::format::sample_rate.key, + nmos::caps::format::sample_depth.key + }; + + for (int index = 0; index < how_many; ++index) + { + for (const auto& port : { impl::ports::video, impl::ports::audio }) + { + const auto sender_id = impl::make_id(seed_id, nmos::types::sender, port, index); + const auto& supported_param_constraints = port == impl::ports::video ? video_parameter_constraints : audio_parameter_constraints; + auto streamcompatibility_sender = nmos::experimental::make_streamcompatibility_sender(sender_id, { input_id }, supported_param_constraints); + if (!insert_resource_after(delay_millis, model.streamcompatibility_resources, std::move(streamcompatibility_sender), gate)) throw node_implementation_init_exception(); + } + } + } + + // example IS-11 output and receivers + if (0 <= nmos::fields::streamcompatibility_port(model.settings)) + { + unsigned char edid_bytes[] = { + 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, + 0x04, 0x21, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x0a, 0x01, 0x04, 0x80, 0x00, 0x00, 0x00, + 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x01, + 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, + 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x00, 0x00, + 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + }; + utility::string_t edid(edid_bytes, edid_bytes + sizeof(edid_bytes)); + + const auto output_id = impl::make_id(seed_id, nmos::types::output); + const auto receiver_ids = impl::make_ids(seed_id, nmos::types::receiver, { impl::ports::video, impl::ports::audio }, how_many); + + auto output = edid_support + ? nmos::experimental::make_streamcompatibility_output(output_id, device_id, true, boost::variant(edid), receiver_ids, model.settings) + : nmos::experimental::make_streamcompatibility_output(output_id, device_id, true, receiver_ids, model.settings); + impl::set_label_description(output, impl::ports::mux, 0); // The single Output produces both video and audio signals + if (!insert_resource_after(delay_millis, model.streamcompatibility_resources, std::move(output), gate)) throw node_implementation_init_exception(); + + for (const auto& receiver_id : receiver_ids) + { + auto streamcompatibility_receiver = nmos::experimental::make_streamcompatibility_receiver(receiver_id, { output_id }); + if (!insert_resource_after(delay_millis, model.streamcompatibility_resources, std::move(streamcompatibility_receiver), gate)) throw node_implementation_init_exception(); + } + } + // examples of using IS-12 control protocol // they are based on the NC-DEVICE-MOCK // See https://specs.amwa.tv/nmos-device-control-mock/#about-nc-device-mock @@ -1366,6 +1499,7 @@ void node_implementation_run(nmos::node_model& model, nmos::experimental::contro const auto ws_sender_ports = boost::copy_range>(sender_ports | boost::adaptors::filtered(impl::is_ws_port)); const auto rtp_receiver_ports = boost::copy_range>(impl::parse_ports(impl::fields::receivers(model.settings)) | boost::adaptors::filtered(impl::is_rtp_port)); const auto simulate_status_monitor_activity = impl::fields::simulate_status_monitor_activity(model.settings); + const auto simulate_sender_volatile_activity = impl::fields::simulate_sender_volatile_activity(model.settings); auto& control_protocol_resources = model.control_protocol_resources; @@ -1392,10 +1526,10 @@ void node_implementation_run(nmos::node_model& model, nmos::experimental::contro auto cancellation_source = pplx::cancellation_token_source(); auto token = cancellation_source.get_token(); - auto events = pplx::do_while([&model, seed_id, how_many, simulate_status_monitor_activity, ws_sender_ports, rtp_receiver_ports, rtp_sender_ports, get_control_protocol_property, set_receiver_monitor_link_status, set_receiver_monitor_connection_status, set_receiver_monitor_external_synchronization_status, set_receiver_monitor_stream_status, set_receiver_monitor_synchronization_source_id, set_sender_monitor_link_status, set_sender_monitor_transmission_status, set_sender_monitor_external_synchronization_status, set_sender_monitor_essence_status, set_sender_monitor_synchronization_source_id, set_control_protocol_property, events_engine, &gate, token] + auto events = pplx::do_while([&model, seed_id, how_many, simulate_status_monitor_activity, simulate_sender_volatile_activity, ws_sender_ports, rtp_receiver_ports, rtp_sender_ports, get_control_protocol_property, set_receiver_monitor_link_status, set_receiver_monitor_connection_status, set_receiver_monitor_external_synchronization_status, set_receiver_monitor_stream_status, set_receiver_monitor_synchronization_source_id, set_sender_monitor_link_status, set_sender_monitor_transmission_status, set_sender_monitor_external_synchronization_status, set_sender_monitor_essence_status, set_sender_monitor_synchronization_source_id, set_control_protocol_property, events_engine, &gate, token] { const auto event_interval = std::uniform_real_distribution<>(0.5, 5.0)(*events_engine); - return pplx::complete_after(std::chrono::milliseconds(std::chrono::milliseconds::rep(1000 * event_interval)), token).then([&model, seed_id, how_many, simulate_status_monitor_activity, ws_sender_ports, rtp_receiver_ports, rtp_sender_ports, get_control_protocol_property, set_receiver_monitor_link_status, set_receiver_monitor_connection_status, set_receiver_monitor_external_synchronization_status, set_receiver_monitor_stream_status, set_receiver_monitor_synchronization_source_id, set_sender_monitor_link_status, set_sender_monitor_transmission_status, set_sender_monitor_external_synchronization_status, set_sender_monitor_essence_status, set_sender_monitor_synchronization_source_id, set_control_protocol_property, events_engine, &gate] + return pplx::complete_after(std::chrono::milliseconds(std::chrono::milliseconds::rep(1000 * event_interval)), token).then([&model, seed_id, how_many, simulate_status_monitor_activity, simulate_sender_volatile_activity, ws_sender_ports, rtp_receiver_ports, rtp_sender_ports, get_control_protocol_property, set_receiver_monitor_link_status, set_receiver_monitor_connection_status, set_receiver_monitor_external_synchronization_status, set_receiver_monitor_stream_status, set_receiver_monitor_synchronization_source_id, set_sender_monitor_link_status, set_sender_monitor_transmission_status, set_sender_monitor_external_synchronization_status, set_sender_monitor_essence_status, set_sender_monitor_synchronization_source_id, set_control_protocol_property, events_engine, &gate] { auto lock = model.write_lock(); @@ -1605,6 +1739,21 @@ void node_implementation_run(nmos::node_model& model, nmos::experimental::contro } } } + + // example volatiling video sender stream status + if (simulate_sender_volatile_activity) + { + for (int index = 0; index < how_many; ++index) + { + const auto sender_id = impl::make_id(seed_id, nmos::types::sender, impl::ports::video, index); + const auto states = { nmos::sender_states::no_essence, nmos::sender_states::awaiting_essence, nmos::sender_states::constrained }; + const auto& state = *(states.begin() + (std::min)(std::geometric_distribution()(*events_engine), states.size() - 1)); + + slog::log(gate, SLOG_FLF) << "Changing IS-11 sender: " << sender_id << " status to " << state.name << " for sender volatile activity simulation"; + nmos::experimental::set_sender_status(model.node_resources, model.streamcompatibility_resources, sender_id, state, {}, gate); + } + } + model.notify(); return true; @@ -1687,11 +1836,9 @@ nmos::transport_file_parser make_node_implementation_transport_file_parser() } // Example Connection API callback to perform application-specific validation of the merged /staged endpoint during a PATCH /staged request -nmos::details::connection_resource_patch_validator make_node_implementation_patch_validator() +nmos::details::connection_resource_patch_validator make_node_implementation_patch_validator(nmos::node_model& model) { - // this example uses an 'empty' std::function because it does not need to do any validation - // beyond what is expressed by the schemas and /constraints endpoint - return{}; + return nmos::experimental::make_connection_streamcompatibility_validator(model); } // Example Connection API activation callback to resolve "auto" values when /staged is transitioned to /active @@ -1917,6 +2064,240 @@ nmos::channelmapping_activation_handler make_node_implementation_channelmapping_ }; } +// Example Stream Compatibility Management API callback to perform application-specific validation of the Base EDID update during a PUT /edid/base or DEL /edid/base request +// Note: Since decoding the EDID does not formed part of the IS-11 specification, no validation is implemented for the reference example. +// i.e. it doesn't reject an invalid EDIDs. +nmos::experimental::details::streamcompatibility_base_edid_handler make_node_implementation_streamcompatibility_base_edid_handler(slog::base_gate& gate) +{ + return [&gate](const nmos::id& input_id, const bst::optional& base_edid_binary) + { + if (base_edid_binary) + { + slog::log(gate, SLOG_FLF) << "validate the Base EDID for input: " << input_id; + } + else + { + slog::log(gate, SLOG_FLF) << "delete Base EDID for input: " << input_id; + } + return std::make_pair( true, U("Succeeded") ); + }; +} + +// Example Stream Compatibility Management API callback to update Effective EDID for the specified IS-11 Input +// during PUT /edid/base, DEL /edid/base, PUT /constraints/active or DEL /constraints/active request +nmos::experimental::details::streamcompatibility_effective_edid_setter make_node_implementation_streamcompatibility_effective_edid_setter(const nmos::resources& streamcompatibility_resources, slog::base_gate& gate) +{ + return [&streamcompatibility_resources, &gate](const nmos::id& input_id, boost::variant& effective_edid) + { + unsigned char edid_bytes[] = { + 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, + 0x04, 0x21, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x0a, 0x01, 0x04, 0x80, 0x00, 0x00, 0x00, + 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x01, + 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, + 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x00, 0x00, + 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + }; + + bst::optional base_edid_binary{}; + + const std::pair input_id_type{ input_id, nmos::types::input }; + const auto streamcompatibility_input = find_resource(streamcompatibility_resources, input_id_type); + if (streamcompatibility_resources.end() != streamcompatibility_input) + { + // hmm, just log the input adjust_to_caps and the intersection_of_caps_and_constraints of all senders + { + std::stringstream ss; + + ss << "Intersection of Sender Caps and Active Constraints for"; + + for (const auto& sender_id_ : nmos::fields::senders(streamcompatibility_input->data)) + { + const auto sender_id = sender_id_.as_string(); + const std::pair sender_id_type{ sender_id, nmos::types::sender }; + const auto streamcompatibility_sender = find_resource(streamcompatibility_resources, sender_id_type); + if (streamcompatibility_resources.end() != streamcompatibility_sender) + { + ss << "\nsender: " << utility::us2s(sender_id) << " is " << utility::us2s(nmos::fields::intersection_of_caps_and_constraints(streamcompatibility_sender->data).serialize()); + } + } + + if (nmos::fields::adjust_to_caps(streamcompatibility_input->data)) + { + ss << "\nand input: " << utility::us2s(streamcompatibility_input->id) << " support adjusting Base EDID to internal capabilities"; + } + + slog::log(gate, SLOG_FLF) << ss.str(); + } + + // get input's Base EDID binary + const auto& edid_endpoint = nmos::fields::endpoint_base_edid(streamcompatibility_input->data); + if (!edid_endpoint.is_null()) + { + const auto& edid_binary = nmos::fields::edid_binary(edid_endpoint); + if (!edid_binary.is_null()) + { + base_edid_binary = edid_binary.as_string(); + } + } + } + else + { + slog::log(gate, SLOG_FLF) << "Effective EDID not created, " << input_id_type << " not found"; + return; + } + + // set effective EDID + effective_edid = base_edid_binary ? *base_edid_binary : utility::string_t(edid_bytes, edid_bytes + sizeof(edid_bytes)); + + slog::log(gate, SLOG_FLF) << "Effective EDID is created for " << input_id_type; + }; +} + +// Example Stream Compatibility Management API callback to perform application-specific sender capabilities against active constraints, +// during PUT /constraints/active or DEL /constraints/active request, this callback returns the intersection of capabilities constraints via the callback parameters +nmos::experimental::details::streamcompatibility_active_constraints_handler make_node_implementation_streamcompatibility_active_constraints_handler(const nmos::node_model& model, slog::base_gate& gate) +{ + using web::json::value_of; + + const auto seed_id = nmos::experimental::fields::seed_id(model.settings); + const auto how_many = impl::fields::how_many(model.settings); + const auto video_sender_ids = impl::make_ids(seed_id, nmos::types::sender, impl::ports::video, how_many); + const auto audio_sender_ids = impl::make_ids(seed_id, nmos::types::sender, impl::ports::audio, how_many); + + const auto frame_rate = nmos::parse_rational(impl::fields::frame_rate(model.settings)); + const auto frame_width = impl::fields::frame_width(model.settings); + const auto frame_height = impl::fields::frame_height(model.settings); + const auto color_sampling = impl::fields::color_sampling(model.settings); + const auto interlace_mode = impl::get_interlace_mode(model.settings); + const auto colorspace = impl::fields::colorspace(model.settings); + const auto transfer_characteristic = impl::fields::transfer_characteristic(model.settings); + const auto component_depth = impl::fields::component_depth(model.settings); + const auto video_type = impl::fields::video_type(model.settings); + const auto channel_count = impl::fields::channel_count(model.settings); + + // Each Constraint Set in Sender Caps should contain all parameter constraints from Sender's /constraints/supported + auto video_sender_capabilities = value_of({ + value_of({ + { nmos::caps::format::media_type, nmos::make_caps_string_constraint({ video_type }) }, + { nmos::caps::format::grain_rate, nmos::make_caps_rational_constraint({ frame_rate }) }, + { nmos::caps::format::frame_width, nmos::make_caps_integer_constraint({ frame_width }) }, + { nmos::caps::format::frame_height, nmos::make_caps_integer_constraint({ frame_height }) }, + { nmos::caps::format::color_sampling, nmos::make_caps_string_constraint({ color_sampling }) }, + { nmos::caps::format::interlace_mode, nmos::make_caps_string_constraint({ interlace_mode.name }) }, + { nmos::caps::format::colorspace, nmos::make_caps_string_constraint({ colorspace }) }, + { nmos::caps::format::transfer_characteristic, nmos::make_caps_string_constraint({ transfer_characteristic }) }, + { nmos::caps::format::component_depth, nmos::make_caps_integer_constraint({ component_depth }) }, + { nmos::caps::transport::st2110_21_sender_type, nmos::make_caps_string_constraint({ sdp::type_parameters::type_N.name }) } + }) + }); + + auto audio_sender_capabilities = value_of({ + value_of({ + { nmos::caps::format::media_type, nmos::make_caps_string_constraint({ nmos::media_types::audio_L(24).name }) }, + { nmos::caps::format::channel_count, nmos::make_caps_integer_constraint({ channel_count }) }, + { nmos::caps::format::sample_rate, nmos::make_caps_rational_constraint({ nmos::rational{48000, 1} }) }, + { nmos::caps::format::sample_depth, nmos::make_caps_integer_constraint({ 24 }) } + }) + }); + + return [&gate, video_sender_capabilities, audio_sender_capabilities, video_sender_ids, audio_sender_ids](const nmos::resource& streamcompatibility_sender, const web::json::value& active_constraints, web::json::value& intersection) + { + const auto& constraint_sets = nmos::fields::constraint_sets(active_constraints).as_array(); + + if (web::json::empty(constraint_sets)) + { + intersection = web::json::value::array(); + return std::make_pair(true, U("Succeeded")); + } + + const auto& sender_id = streamcompatibility_sender.id; + const bool video_found = video_sender_ids.end() != boost::range::find(video_sender_ids, sender_id); + const bool audio_found = audio_sender_ids.end() != boost::range::find(audio_sender_ids, sender_id); + + const auto& sender_capabilities_ = video_found ? video_sender_capabilities : audio_found ? audio_sender_capabilities : web::json::value{}; + if (sender_capabilities_.is_null()) + { + return std::make_pair(false, U("No Sender Capabilities found")); + } + + const auto& sender_capabilities = sender_capabilities_.as_array(); + + std::vector v; + + for (const auto& constraint_set : constraint_sets) + { + if (!nmos::caps::meta::enabled(constraint_set)) continue; + for (const auto& sender_caps_constraint_set : sender_capabilities) + { + const auto intersection = nmos::experimental::get_constraint_set_intersection(sender_caps_constraint_set, constraint_set); + if (!intersection.is_null()) + { + v.push_back(intersection); + } + } + } + + if (v.empty()) + { + slog::log(gate, SLOG_FLF) << "Sender: " << sender_id << " doesn't support proposed Active Constraints: " << nmos::fields::constraint_sets(active_constraints).serialize(); + return std::make_pair(false, U("Sender doesn't support proposed Active Constraints")); + } + + intersection = value_from_elements(v); + return std::make_pair(true, U("Succeeded")); + }; +} + +// Example Stream Compatibility Management API callback to perform application-specific sender validation with its associated resources, returns sender's "state" and "debug" values +// It may throw exception, which will be logged +nmos::experimental::details::streamcompatibility_sender_validator make_node_implementation_streamcompatibility_sender_validator() +{ + using nmos::experimental::make_streamcompatibility_sender_resources_validator; + using nmos::experimental::make_streamcompatibility_sdp_constraint_sets_matcher; + + const auto validate_video_jxsv_sender_resources = make_streamcompatibility_sender_resources_validator(&nmos::experimental::match_video_jxsv_resource_parameters_constraint_set, make_streamcompatibility_sdp_constraint_sets_matcher(&nmos::match_video_jxsv_sdp_parameters_constraint_sets)); + const auto validate_sender_resources = make_streamcompatibility_sender_resources_validator(&nmos::experimental::match_resource_parameters_constraint_set, make_streamcompatibility_sdp_constraint_sets_matcher(&nmos::match_sdp_parameters_constraint_sets)); + + return [validate_video_jxsv_sender_resources, validate_sender_resources](const nmos::resource& source, const nmos::resource& flow, const nmos::resource& sender, const nmos::resource& connection_sender, const web::json::array& constraint_sets) -> std::pair + { + if (nmos::media_types::video_jxsv.name == nmos::fields::media_type(flow.data)) + { + // validate "video/jxsv" + return validate_video_jxsv_sender_resources(source, flow, sender, connection_sender, constraint_sets); + } + else + { + // validate core media types, i.e., "video/raw", "audio/L", "video/smpte291" and "video/SMPTE2022-6" + return validate_sender_resources(source, flow, sender, connection_sender, constraint_sets); + } + }; +} + +// Example Stream Compatibility Management API callback to perform application-specific receiver validation with its transport file, returns receiver's "state" and "debug" values +// It may throw exception, which will be logged +nmos::experimental::details::streamcompatibility_receiver_validator make_node_implementation_streamcompatibility_receiver_validator() +{ + // this example uses a custom transport file validator to handle video/jxsv in addition to the core media types + const auto transport_file_validator = std::bind( + nmos::experimental::details::validate_rtp_transport_file, + impl::validate_sdp_parameters, + std::placeholders::_1, + std::placeholders::_2, + std::placeholders::_3 + ); + return nmos::experimental::make_streamcompatibility_receiver_validator(transport_file_validator); +} + // Example Control Protocol WebSocket API property changed callback to perform application-specific operations to complete the property changed nmos::control_protocol_property_changed_handler make_node_implementation_control_protocol_property_changed_handler(slog::base_gate& gate) { @@ -2194,6 +2575,11 @@ namespace impl // into the server instance for the NMOS Node. nmos::experimental::node_implementation make_node_implementation(nmos::node_model& model, slog::base_gate& gate) { + // may be omitted if IS-11 not required and IS-11 Set Effective EDID functionality not required + const auto set_effective_edid = impl::fields::edid_support(model.settings) + ? make_node_implementation_streamcompatibility_effective_edid_setter(model.streamcompatibility_resources, gate) + : nmos::experimental::details::streamcompatibility_effective_edid_setter{}; + return nmos::experimental::node_implementation() .on_load_server_certificates(nmos::make_load_server_certificates_handler(model.settings, gate)) .on_load_dh_param(nmos::make_load_dh_param_handler(model.settings, gate)) @@ -2201,12 +2587,17 @@ nmos::experimental::node_implementation make_node_implementation(nmos::node_mode .on_system_changed(make_node_implementation_system_global_handler(model, gate)) // may be omitted if not required .on_registration_changed(make_node_implementation_registration_handler(gate)) // may be omitted if not required .on_parse_transport_file(make_node_implementation_transport_file_parser()) // may be omitted if the default is sufficient - .on_validate_connection_resource_patch(make_node_implementation_patch_validator()) // may be omitted if not required + .on_validate_connection_resource_patch(make_node_implementation_patch_validator(model)) // may be omitted if not required .on_resolve_auto(make_node_implementation_auto_resolver(model.settings)) .on_set_transportfile(make_node_implementation_transportfile_setter(model.node_resources, model.settings)) .on_connection_activated(make_node_implementation_connection_activation_handler(model, gate)) .on_validate_channelmapping_output_map(make_node_implementation_map_validator()) // may be omitted if not required .on_channelmapping_activated(make_node_implementation_channelmapping_activation_handler(gate)) + .on_validate_base_edid(make_node_implementation_streamcompatibility_base_edid_handler(gate)) // may be omitted if IS-11 not required + .on_set_effective_edid(set_effective_edid) // may be omitted if IS-11 not required, or IS-11 Set Effective EDID functionality not required + .on_active_constraints_changed(make_node_implementation_streamcompatibility_active_constraints_handler(model, gate)) + .on_validate_sender_resources_against_active_constraints(make_node_implementation_streamcompatibility_sender_validator()) // may be omitted if the default is sufficient + .on_validate_receiver_against_transport_file(make_node_implementation_streamcompatibility_receiver_validator()) // may be omitted if the default is sufficient .on_control_protocol_property_changed(make_node_implementation_control_protocol_property_changed_handler(gate)) // may be omitted if IS-12 not required .on_create_validation_fingerprint(make_create_validation_fingerprint_handler()) .on_validate_validation_fingerprint(make_validate_validation_fingerprint_handler()) diff --git a/Development/nmos/api_utils.cpp b/Development/nmos/api_utils.cpp index a4fa44be0..63627e878 100644 --- a/Development/nmos/api_utils.cpp +++ b/Development/nmos/api_utils.cpp @@ -97,6 +97,43 @@ namespace nmos return extract_json<>(res, gate); } + // extract istream into vector of bytes after checking the Content-Type header + template + inline pplx::task> extract_istream_vector(const HttpMessage& msg, slog::base_gate& gate) + { + auto mime_type = web::http::details::get_mime_type(msg.headers().content_type()); + + if (U("application/octet-stream") == mime_type) + { + return msg.extract_vector(); + } + else if (mime_type.empty()) + { + // "If a Content-Type header field is not present, the recipient MAY + // [...] examine the data to determine its type." + // See https://tools.ietf.org/html/rfc7231#section-3.1.1.5 + + slog::log(gate, SLOG_FLF) << "Missing Content-Type: should be application/octet-stream"; + + return msg.extract_vector(); + } + else + { + // more helpful message than from web::http::details::http_msg_base::parse_and_check_content_type for unacceptable content-type + return pplx::task_from_exception>(web::http::http_exception(U("Incorrect Content-Type: ") + msg.headers().content_type() + U(", should be application/octet-stream"))); + } + } + + pplx::task> extract_istream_vector(const web::http::http_request& req, slog::base_gate& gate) + { + return extract_istream_vector<>(req, gate); + } + + pplx::task> extract_istream_vector(const web::http::http_response& res, slog::base_gate& gate) + { + return extract_istream_vector<>(res, gate); + } + // add the NMOS-specified CORS response headers web::http::http_response& add_cors_preflight_headers(const web::http::http_request& req, web::http::http_response& res) { diff --git a/Development/nmos/api_utils.h b/Development/nmos/api_utils.h index f6ef29e62..77906008c 100644 --- a/Development/nmos/api_utils.h +++ b/Development/nmos/api_utils.h @@ -57,6 +57,8 @@ namespace nmos const route_pattern channelmapping_api = make_route_pattern(U("api"), U("channelmapping")); // IS-09 System API (originally specified in JT-NM TR-1001-1:2018 Annex A) const route_pattern system_api = make_route_pattern(U("api"), U("system")); + // IS-11 Stream Compatibility Management API + const route_pattern streamcompatibility_api = make_route_pattern(U("api"), U("streamcompatibility")); // IS-14 Configuration API const route_pattern configuration_api = make_route_pattern(U("api"), U("configuration")); @@ -94,6 +96,12 @@ namespace nmos const route_pattern propertyId = make_route_pattern(U("propertyId"), U("[0-9]+p[0-9]+")); const route_pattern methodId = make_route_pattern(U("methodId"), U("[0-9]+m[0-9]+")); + // Stream Compatibility Management API + const route_pattern streamCompatibilityResourceType = make_route_pattern(U("resourceType"), U("senders|receivers|inputs|outputs")); + const route_pattern streamCompatibilityInputOutputType = make_route_pattern(U("inputOutputType"), U("inputs|outputs")); + const route_pattern constraintsType = make_route_pattern(U("constraintsType"), U("active|supported")); + const route_pattern edidType = make_route_pattern(U("edidType"), U("base|effective")); + // Common patterns const route_pattern resourceId = make_route_pattern(U("resourceId"), U("[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}")); } @@ -193,6 +201,12 @@ namespace nmos // extract JSON after checking the Content-Type header pplx::task extract_json(const web::http::http_response& res, slog::base_gate& gate); + // extract istream into vector of bytes after checking the Content-Type header + pplx::task> extract_istream_vector(const web::http::http_request& req, slog::base_gate& gate); + + // extract istream into vector of bytes after checking the Content-Type header + pplx::task> extract_istream_vector(const web::http::http_response& res, slog::base_gate& gate); + // add the NMOS-specified CORS response headers web::http::http_response& add_cors_preflight_headers(const web::http::http_request& req, web::http::http_response& res); web::http::http_response& add_cors_headers(web::http::http_response& res); diff --git a/Development/nmos/connection_api.cpp b/Development/nmos/connection_api.cpp index eb63bcafb..355d72b2a 100644 --- a/Development/nmos/connection_api.cpp +++ b/Development/nmos/connection_api.cpp @@ -1248,7 +1248,7 @@ namespace nmos const std::pair id_type{ resourceId, nmos::type_from_resourceType(resourceType) }; - slog::log(gate, SLOG_FLF) << "Operation requested for single " << id_type; + slog::log(gate, SLOG_FLF) << "Connection staged requested for single " << id_type; details::handle_connection_resource_patch(res, model, version, id_type, body, parse_transport_file, validate_merged, gate); diff --git a/Development/nmos/connection_api.h b/Development/nmos/connection_api.h index bc6b24182..c3c5efb25 100644 --- a/Development/nmos/connection_api.h +++ b/Development/nmos/connection_api.h @@ -50,6 +50,7 @@ namespace nmos // and experimental Manifest API for Node API "manifest_href" namespace details { + std::pair get_transport_type_data(const web::json::value& transport_file); void handle_connection_resource_patch(web::http::http_response res, nmos::node_model& model, const nmos::api_version& version, const std::pair& id_type, const web::json::value& patch, transport_file_parser parse_transport_file, connection_resource_patch_validator validate_merged, slog::base_gate& gate); void handle_connection_resource_transportfile(web::http::http_response res, const nmos::node_model& model, const nmos::api_version& version, const std::pair& id_type, const utility::string_t& accept, slog::base_gate& gate); } diff --git a/Development/nmos/constraints.cpp b/Development/nmos/constraints.cpp new file mode 100644 index 000000000..975c52bd7 --- /dev/null +++ b/Development/nmos/constraints.cpp @@ -0,0 +1,271 @@ +#include "nmos/constraints.h" + +#include +#include +#include +#include +#include +#include "cpprest/basic_utils.h" // for utility::us2s +#include "cpprest/json_utils.h" +#include "nmos/capabilities.h" +#include "nmos/json_fields.h" +#include "nmos/rational.h" + +namespace nmos +{ + namespace experimental + { + namespace details + { + bool constraint_value_less(const web::json::value& lhs, const web::json::value& rhs) + { + if (nmos::is_rational(lhs) && nmos::is_rational(rhs)) + { + if (nmos::parse_rational(lhs) < nmos::parse_rational(rhs)) + { + return true; + } + return false; + } + return lhs < rhs; + } + } + + web::json::value get_intersection(const web::json::array& lhs_, const web::json::array& rhs_) + { + std::set lhs(lhs_.begin(), lhs_.end(), &details::constraint_value_less); + std::set rhs(rhs_.begin(), rhs_.end(), &details::constraint_value_less); + + std::vector v(std::min(lhs.size(), rhs.size())); + + const auto it = std::set_intersection(lhs.begin(), lhs.end(), rhs.begin(), rhs.end(), v.begin(), details::constraint_value_less); + v.resize(it - v.begin()); + + return web::json::value_from_elements(v); + } + + web::json::value get_intersection(const web::json::array& enumeration, const web::json::value& constraint) + { + return web::json::value_from_elements(enumeration | boost::adaptors::filtered([&constraint](const web::json::value& enum_value) + { + return match_constraint(enum_value, constraint); + })); + } + + web::json::value get_constraint_intersection(const web::json::value& lhs, const web::json::value& rhs) + { + web::json::value result; + + // "enum" + if (lhs.has_field(nmos::fields::constraint_enum) && rhs.has_field(nmos::fields::constraint_enum)) + { + result[nmos::fields::constraint_enum] = get_intersection(nmos::fields::constraint_enum(lhs).as_array(), nmos::fields::constraint_enum(rhs).as_array()); + } + else if (lhs.has_field(nmos::fields::constraint_enum)) + { + result[nmos::fields::constraint_enum] = nmos::fields::constraint_enum(lhs); + } + else if (rhs.has_field(nmos::fields::constraint_enum)) + { + result[nmos::fields::constraint_enum] = nmos::fields::constraint_enum(rhs); + } + + // "minimum" + if (lhs.has_field(nmos::fields::constraint_minimum) && rhs.has_field(nmos::fields::constraint_minimum)) + { + result[nmos::fields::constraint_minimum] = std::max(nmos::fields::constraint_minimum(lhs), nmos::fields::constraint_minimum(rhs), details::constraint_value_less); + } + else if (lhs.has_field(nmos::fields::constraint_minimum)) + { + result[nmos::fields::constraint_minimum] = nmos::fields::constraint_minimum(lhs); + } + else if (rhs.has_field(nmos::fields::constraint_minimum)) + { + result[nmos::fields::constraint_minimum] = nmos::fields::constraint_minimum(rhs); + } + + // "maximum" + if (lhs.has_field(nmos::fields::constraint_maximum) && rhs.has_field(nmos::fields::constraint_maximum)) + { + result[nmos::fields::constraint_maximum] = std::min(nmos::fields::constraint_maximum(lhs), nmos::fields::constraint_maximum(rhs), details::constraint_value_less); + } + else if (lhs.has_field(nmos::fields::constraint_maximum)) + { + result[nmos::fields::constraint_maximum] = nmos::fields::constraint_maximum(lhs); + } + else if (rhs.has_field(nmos::fields::constraint_maximum)) + { + result[nmos::fields::constraint_maximum] = nmos::fields::constraint_maximum(rhs); + } + + // "min" > "max" + if (result.has_field(nmos::fields::constraint_minimum) && result.has_field(nmos::fields::constraint_maximum)) + { + if (details::constraint_value_less(nmos::fields::constraint_maximum(result), nmos::fields::constraint_minimum(result))) + { + return web::json::value::null(); + } + } + + // "enum" matches "min"/"max" + if (result.has_field(nmos::fields::constraint_enum) && (result.has_field(nmos::fields::constraint_minimum) || result.has_field(nmos::fields::constraint_maximum))) + { + const auto remove_keywords = [](web::json::value constraint, const std::vector& keywords) + { + for (const auto& keyword : keywords) + { + if (constraint.has_field(keyword)) + { + constraint.erase(keyword); + } + } + return constraint; + }; + + result[nmos::fields::constraint_enum] = get_intersection(nmos::fields::constraint_enum(result).as_array(), remove_keywords(result, { nmos::fields::constraint_enum })); + + // "The Parameter Constraint is satisfied if all of the constraints expressed by the Constraint Keywords are satisfied." + // After "enum" lost any values out of [min, max], the Parameter Constraint can be simplified by removing "min" and "max" + result = remove_keywords(result, { nmos::fields::constraint_minimum, nmos::fields::constraint_maximum }); + } + + // "enum" is empty + if (result.has_field(nmos::fields::constraint_enum) && web::json::empty(nmos::fields::constraint_enum(result))) + { + return web::json::value::null(); + } + + return result; + } + + web::json::value get_constraint_set_intersection(const web::json::value& lhs_, const web::json::value& rhs_) + { + using web::json::value; + using web::json::value_from_elements; + + const auto& lhs = lhs_.as_object(); + const auto& rhs = rhs_.as_object(); + + auto result = value::object(); + + auto lhs_iter = lhs.begin(); + auto rhs_iter = rhs.begin(); + + const auto insert_if_not_meta = [](value& j, web::json::object::const_iterator property) { + if (!boost::algorithm::starts_with(property->first, U("urn:x-nmos:cap:meta:"))) + { + j[property->first] = property->second; + } + }; + + while (lhs_iter != lhs.end() || rhs_iter != rhs.end()) + { + if (lhs_iter != lhs.end() && rhs_iter != rhs.end()) + { + if (lhs_iter->first < rhs_iter->first) + { + insert_if_not_meta(result, lhs_iter++); + } + else if (lhs_iter->first > rhs_iter->first) + { + insert_if_not_meta(result, rhs_iter++); + } + else + { + if (!boost::algorithm::starts_with(lhs_iter->first, U("urn:x-nmos:cap:meta:"))) + { + const value intersection = get_constraint_intersection(lhs_iter->second, rhs_iter->second); + if (intersection.is_null()) + { + return value::null(); + } + result[lhs_iter->first] = intersection; + } + lhs_iter++; rhs_iter++; + } + } + else if (lhs_iter == lhs.end()) + { + insert_if_not_meta(result, rhs_iter++); + } + else if (rhs_iter == rhs.end()) + { + insert_if_not_meta(result, lhs_iter++); + } + } + + return result; + } + + // Constraint B is a subconstraint of Constraint A if: + // 1. Constraint B has enum keyword when Constraint A has it and enumerated values of Constraint B are a subset of enumerated values of Constraint A + // 2. Constraint B has enum or minimum keyword when Constraint A has minimum keyword and allowed values of Constraint B are greater than minimum value of Constraint A + // 3. Constraint B has enum or maximum keyword when Constraint A has maximum keyword and allowed values of Constraint B are less than maximum value of Constraint A + bool is_subconstraint(const web::json::value& constraint, const web::json::value& subconstraint) + { + // subconstraint should have enum if constraint has enum + if (constraint.has_field(nmos::fields::constraint_enum) && !subconstraint.has_field(nmos::fields::constraint_enum)) + { + return false; + } + // subconstraint should have minimum or enum if constraint has minimum + if (constraint.has_field(nmos::fields::constraint_minimum) && !subconstraint.has_field(nmos::fields::constraint_enum) && !subconstraint.has_field(nmos::fields::constraint_minimum)) + { + return false; + } + // subconstraint should have maximum or enum if constraint has maximum + if (constraint.has_field(nmos::fields::constraint_maximum) && !subconstraint.has_field(nmos::fields::constraint_enum) && !subconstraint.has_field(nmos::fields::constraint_maximum)) + { + return false; + } + if (constraint.has_field(nmos::fields::constraint_minimum) && subconstraint.has_field(nmos::fields::constraint_minimum)) + { + const auto& constraint_min = nmos::fields::constraint_minimum(constraint); + const auto& subconstraint_min = nmos::fields::constraint_minimum(subconstraint); + + if (details::constraint_value_less(subconstraint_min, constraint_min)) + { + return false; + } + } + if (constraint.has_field(nmos::fields::constraint_maximum) && subconstraint.has_field(nmos::fields::constraint_maximum)) + { + const auto& constraint_max = nmos::fields::constraint_maximum(constraint); + const auto& subconstraint_max = nmos::fields::constraint_maximum(subconstraint); + + if (details::constraint_value_less(constraint_max, subconstraint_max)) + { + return false; + } + } + // subconstraint enum values should match constraint + if (subconstraint.has_field(nmos::fields::constraint_enum)) + { + const auto& subconstraint_enum_values = nmos::fields::constraint_enum(subconstraint).as_array(); + return subconstraint_enum_values.end() == std::find_if_not(subconstraint_enum_values.begin(), subconstraint_enum_values.end(), [&constraint](const web::json::value& enum_value) + { + return match_constraint(enum_value, constraint); + }); + } + return true; + } + + // Constraint Set B is a subset of Constraint Set A if all Parameter Constraints of Constraint Set A are present in Constraint Set B, and for each Parameter Constraint + // that is present in both, the Parameter Constraint of Constraint Set B is a subconstraint of the Parameter Constraint of Constraint Set A. + bool is_constraint_subset(const web::json::value& constraint_set, const web::json::value& constraint_subset) + { + using web::json::value; + + const auto& param_constraints_set = constraint_set.as_object(); + const auto& param_constraints_subset = constraint_subset.as_object(); + + return param_constraints_set.end() == std::find_if_not(param_constraints_set.begin(), param_constraints_set.end(), [¶m_constraints_subset](const std::pair& constraint) + { + if (boost::algorithm::starts_with(constraint.first, U("urn:x-nmos:cap:meta:"))) return true; + + const auto& subconstraint = param_constraints_subset.find(constraint.first); + return param_constraints_subset.end() != subconstraint && is_subconstraint(constraint.second, subconstraint->second); + }); + } + } +} diff --git a/Development/nmos/constraints.h b/Development/nmos/constraints.h new file mode 100644 index 000000000..9aa7aa52f --- /dev/null +++ b/Development/nmos/constraints.h @@ -0,0 +1,29 @@ +#ifndef NMOS_CONSTRAINTS_H +#define NMOS_CONSTRAINTS_H + +namespace web +{ + namespace json + { + class value; + } +} + +namespace nmos +{ + namespace experimental + { + namespace details + { + bool constraint_value_less(const web::json::value& lhs, const web::json::value& rhs); + } + + web::json::value get_constraint_intersection(const web::json::value& lhs, const web::json::value& rhs); + web::json::value get_constraint_set_intersection(const web::json::value& lhs, const web::json::value& rhs); + + bool is_subconstraint(const web::json::value& constraint, const web::json::value& subconstraint); + bool is_constraint_subset(const web::json::value& constraint_set, const web::json::value& constraint_subset); + } +} + +#endif diff --git a/Development/nmos/is11_schemas/is11_schemas.h b/Development/nmos/is11_schemas/is11_schemas.h new file mode 100644 index 000000000..8f2513cb0 --- /dev/null +++ b/Development/nmos/is11_schemas/is11_schemas.h @@ -0,0 +1,40 @@ +#ifndef NMOS_IS11_SCHEMAS_H +#define NMOS_IS11_SCHEMAS_H + +// Extern declarations for auto-generated constants +// could be auto-generated, but isn't currently! +namespace nmos +{ + namespace is11_schemas + { + namespace v1_0_x + { + extern const char* constraints_active; + extern const char* constraints_base; + extern const char* constraint_set; + extern const char* constraints_supported; + extern const char* empty_constraints_active; + extern const char* error; + extern const char* input_edid_base; + extern const char* input; + extern const char* input_output_base; + extern const char* output; + extern const char* param_constraint_boolean; + extern const char* param_constraint_integer; + extern const char* param_constraint; + extern const char* param_constraint_number; + extern const char* param_constraint_rational; + extern const char* param_constraint_string; + extern const char* receiver_base; + extern const char* receiver_status; + extern const char* resource_core; + extern const char* resource_list; + extern const char* sender_base; + extern const char* sender_status; + extern const char* streamcompatibility_api_base; + extern const char* uuid_list; + } + } +} + +#endif diff --git a/Development/nmos/is11_versions.h b/Development/nmos/is11_versions.h new file mode 100644 index 000000000..b2ebca4a6 --- /dev/null +++ b/Development/nmos/is11_versions.h @@ -0,0 +1,26 @@ +#ifndef NMOS_IS11_VERSIONS_H +#define NMOS_IS11_VERSIONS_H + +#include +#include +#include "nmos/api_version.h" +#include "nmos/settings.h" + +namespace nmos +{ + namespace is11_versions + { + const api_version v1_0{ 1, 0 }; + + const std::set all{ nmos::is11_versions::v1_0 }; + + inline std::set from_settings(const nmos::settings& settings) + { + return settings.has_field(nmos::fields::is11_versions) + ? boost::copy_range>(nmos::fields::is11_versions(settings) | boost::adaptors::transformed([](const web::json::value& v) { return nmos::parse_api_version(v.as_string()); })) + : nmos::is11_versions::all; + } + } +} + +#endif diff --git a/Development/nmos/json_fields.h b/Development/nmos/json_fields.h index 0ff9921db..8420c7d2d 100644 --- a/Development/nmos/json_fields.h +++ b/Development/nmos/json_fields.h @@ -230,6 +230,38 @@ namespace nmos const web::json::field_as_string hostname{ U("hostname") }; // hostname, ipv4 or ipv6 const web::json::field_as_integer port{ U("port") }; // 1..65535 + // IS-11 Stream Compatibility Management + + // for streamcompatibility_api + const web::json::field_as_array inputs{ U("inputs") }; + const web::json::field_as_array outputs{ U("outputs") }; + const web::json::field_as_bool temporarily_locked{ U("temporarily_locked") }; + + // for input/output properties + const web::json::field_as_bool connected{ U("connected") }; + const web::json::field_as_bool edid_support{ U("edid_support") }; + const web::json::field_as_bool base_edid_support{ U("base_edid_support") }; + const web::json::field_as_bool adjust_to_caps{ U("adjust_to_caps") }; + + // for sender + const web::json::field_as_value endpoint_active_constraints{ U("endpoint_active_constraints") }; // object + const web::json::field_as_value active_constraint_sets{ U("active_constraint_sets") }; // object + const web::json::field_as_value supported_param_constraints{ U("supported_param_constraints") }; // object + const web::json::field_as_array parameter_constraints{ U("parameter_constraints") }; + const web::json::field_as_value intersection_of_caps_and_constraints{ U("intersection_of_caps_and_constraints") }; // array + + // for status + const web::json::field_as_value status{ U("status") }; // object + const web::json::field_as_string state{ U("state") }; + const web::json::field_as_string debug{ U("debug") }; + + // for EDID endpoints + const web::json::field_as_value_or endpoint_base_edid{ U("endpoint_base_edid"), {} }; // object + const web::json::field_as_value_or endpoint_effective_edid{ U("endpoint_effective_edid"), {} }; // object + const web::json::field_as_value_or endpoint_edid{ U("endpoint_edid"), {} }; // object + const web::json::field_as_value_or edid_binary{ U("edid_binary"), {} }; // string + const web::json::field_as_value_or edid_href{ U("edid_href"), {} }; // string + // IS-12 Control Protocol and MS-05 model definitions namespace nc { diff --git a/Development/nmos/json_schema.cpp b/Development/nmos/json_schema.cpp index 3860509ca..1cc412e32 100644 --- a/Development/nmos/json_schema.cpp +++ b/Development/nmos/json_schema.cpp @@ -10,6 +10,8 @@ #include "nmos/is09_versions.h" #include "nmos/is09_schemas/is09_schemas.h" #include "nmos/is10_schemas/is10_schemas.h" +#include "nmos/is11_versions.h" +#include "nmos/is11_schemas/is11_schemas.h" #include "nmos/is12_versions.h" #include "nmos/is12_schemas/is12_schemas.h" #include "nmos/is14_versions.h" @@ -154,6 +156,23 @@ namespace nmos } } + namespace is11_schemas + { + web::uri make_schema_uri(const utility::string_t& tag, const utility::string_t& ref = {}) + { + return{ _XPLATSTR("https://github.com/AMWA-TV/is-11/raw/") + tag + _XPLATSTR("/APIs/schemas/") + ref }; + } + + // See https://github.com/AMWA-TV/is-11/tree/v1.0-dev/APIs/schemas/ + namespace v1_0 + { + using namespace nmos::is11_schemas::v1_0_x; + const utility::string_t tag(_XPLATSTR("v1.0-dev")); + + const web::uri senders_active_constraints_put_request_uri = make_schema_uri(tag, _XPLATSTR("constraints_active.json")); + } + } + namespace is12_schemas { web::uri make_schema_uri(const utility::string_t& tag, const utility::string_t& ref = {}) @@ -394,6 +413,24 @@ namespace nmos }; } + static std::map make_is11_schemas() + { + using namespace nmos::is11_schemas; + + return + { + // v1.0 + { make_schema_uri(v1_0::tag, _XPLATSTR("constraints_active.json")), make_schema(v1_0::constraints_active) }, + { make_schema_uri(v1_0::tag, _XPLATSTR("constraint_set.json")), make_schema(v1_0::constraint_set) }, + { make_schema_uri(v1_0::tag, _XPLATSTR("param_constraint_boolean.json")), make_schema(v1_0::param_constraint_boolean) }, + { make_schema_uri(v1_0::tag, _XPLATSTR("param_constraint_integer.json")), make_schema(v1_0::param_constraint_integer) }, + { make_schema_uri(v1_0::tag, _XPLATSTR("param_constraint.json")), make_schema(v1_0::param_constraint) }, + { make_schema_uri(v1_0::tag, _XPLATSTR("param_constraint_number.json")), make_schema(v1_0::param_constraint_number) }, + { make_schema_uri(v1_0::tag, _XPLATSTR("param_constraint_rational.json")), make_schema(v1_0::param_constraint_rational) }, + { make_schema_uri(v1_0::tag, _XPLATSTR("param_constraint_string.json")), make_schema(v1_0::param_constraint_string) }, + }; + } + static std::map make_is12_schemas() { using namespace nmos::is12_schemas; @@ -439,6 +476,7 @@ namespace nmos merge(result, make_is08_schemas()); merge(result, make_is09_schemas()); merge(result, make_is10_schemas()); + merge(result, make_is11_schemas()); merge(result, make_is12_schemas()); merge(result, make_is14_schemas()); return result; @@ -532,6 +570,11 @@ namespace nmos return is10_schemas::v1_0::authapi_token_response_schema_uri; } + web::uri make_streamcompatibilityapi_senders_active_constraints_put_request_uri(const nmos::api_version& version) + { + return is11_schemas::v1_0::senders_active_constraints_put_request_uri; + } + web::uri make_controlprotocolapi_base_message_schema_uri(const nmos::api_version& version) { return is12_schemas::v1_0::controlprotocolapi_base_message_schema_uri; diff --git a/Development/nmos/json_schema.h b/Development/nmos/json_schema.h index 28d6ea861..694f93cc0 100644 --- a/Development/nmos/json_schema.h +++ b/Development/nmos/json_schema.h @@ -36,6 +36,8 @@ namespace nmos web::uri make_authapi_token_schema_schema_uri(const nmos::api_version& version); web::uri make_authapi_token_response_schema_uri(const nmos::api_version& version); + web::uri make_streamcompatibilityapi_senders_active_constraints_put_request_uri(const nmos::api_version& version); + web::uri make_controlprotocolapi_base_message_schema_uri(const nmos::api_version& version); web::uri make_controlprotocolapi_command_message_schema_uri(const nmos::api_version& version); web::uri make_controlprotocolapi_subscription_message_schema_uri(const nmos::api_version& version); diff --git a/Development/nmos/model.h b/Development/nmos/model.h index d9c25559c..24acd217c 100644 --- a/Development/nmos/model.h +++ b/Development/nmos/model.h @@ -102,6 +102,10 @@ namespace nmos // see nmos/channelmapping_resources.h nmos::resources channelmapping_resources; + // IS-11 senders, receivers, inputs and outputs for this node + // see nmos/streamcompatibility_resources.h + nmos::resources streamcompatibility_resources; + // IS-12 resources for this node // see nmos/control_protocol_resources.h nmos::resources control_protocol_resources; diff --git a/Development/nmos/node_resources.cpp b/Development/nmos/node_resources.cpp index 6e8a1cba1..70b9d442a 100644 --- a/Development/nmos/node_resources.cpp +++ b/Development/nmos/node_resources.cpp @@ -16,6 +16,7 @@ #include "nmos/is05_versions.h" #include "nmos/is07_versions.h" #include "nmos/is08_versions.h" +#include "nmos/is11_versions.h" #include "nmos/is12_versions.h" #include "nmos/is14_versions.h" #include "nmos/media_type.h" @@ -111,6 +112,27 @@ namespace nmos } } + if (0 <= nmos::fields::streamcompatibility_port(settings)) + { + for (const auto& version : nmos::is11_versions::from_settings(settings)) + { + auto streamcompatibility_uri = web::uri_builder() + .set_scheme(nmos::http_scheme(settings)) + .set_port(nmos::fields::streamcompatibility_port(settings)) + .set_path(U("/x-nmos/streamcompatibility/") + make_api_version(version)); + auto type = U("urn:x-nmos:control:stream-compat/") + make_api_version(version); + + for (const auto& host : hosts) + { + web::json::push_back(data[U("controls")], value_of({ + { U("href"), streamcompatibility_uri.set_host(host).to_uri().to_string() }, + { U("type"), type }, + { U("authorization"), nmos::experimental::fields::server_authorization(settings) } + })); + } + } + } + if (0 <= nmos::experimental::fields::manifest_port(settings)) { // See https://specs.amwa.tv/nmos-parameter-registers/branches/main/device-control-types/manifest-base.html diff --git a/Development/nmos/node_server.cpp b/Development/nmos/node_server.cpp index ba0586f41..54f898931 100644 --- a/Development/nmos/node_server.cpp +++ b/Development/nmos/node_server.cpp @@ -17,6 +17,8 @@ #include "nmos/server_utils.h" #include "nmos/settings_api.h" #include "nmos/slog.h" +#include "nmos/streamcompatibility_api.h" +#include "nmos/streamcompatibility_behaviour.h" namespace nmos { @@ -79,6 +81,10 @@ namespace nmos node_server.api_routers[{ {}, nmos::fields::configuration_port(node_model.settings) }].mount({}, nmos::make_configuration_api(node_model, validate_authorization ? validate_authorization(nmos::experimental::scopes::configuration) : nullptr, node_implementation.get_control_protocol_class_descriptor, node_implementation.get_control_protocol_datatype_descriptor, node_implementation.get_control_protocol_method_descriptor, node_implementation.create_validation_fingerprint, node_implementation.validate_validation_fingerprint, node_implementation.get_read_only_modification_allow_list, node_implementation.remove_device_model_object, node_implementation.create_device_model_object, node_implementation.control_protocol_property_changed, gate)); + // Configure the Stream Compatibility API + + node_server.api_routers[{ {}, nmos::fields::streamcompatibility_port(node_model.settings) }].mount({}, nmos::experimental::make_streamcompatibility_api(node_model, node_implementation.validate_base_edid, node_implementation.set_effective_edid, node_implementation.validate_active_constraints, validate_authorization ? validate_authorization(nmos::experimental::scopes::streamcompatibility) : nullptr, gate)); + const auto& events_ws_port = nmos::fields::events_ws_port(node_model.settings); auto& events_ws_api = node_server.ws_handlers[{ {}, nmos::fields::events_ws_port(node_model.settings) }]; events_ws_api.first = nmos::make_events_ws_api(node_model, events_ws_api.second, node_implementation.ws_validate_authorization, gate); @@ -148,12 +154,15 @@ namespace nmos auto monitor_connection_activated = node_implementation.monitor_connection_activated; auto channelmapping_activated = node_implementation.channelmapping_activated; auto get_authorization_bearer_token = node_implementation.get_authorization_bearer_token; + auto validate_sender_resources = node_implementation.validate_sender_resources; + auto validate_receiver = node_implementation.validate_receiver; node_server.thread_functions.assign({ [&, load_ca_certificates, registration_changed, get_authorization_bearer_token] { nmos::node_behaviour_thread(node_model, load_ca_certificates, registration_changed, get_authorization_bearer_token, gate); }, [&] { nmos::send_events_ws_messages_thread(events_ws_listener, node_model, events_ws_api.second, gate); }, [&] { nmos::erase_expired_events_resources_thread(node_model, gate); }, [&, resolve_auto, set_transportfile, connection_activated, monitor_connection_activated] { nmos::connection_activation_thread(node_model, resolve_auto, set_transportfile, connection_activated, monitor_connection_activated, gate); }, - [&, channelmapping_activated] { nmos::channelmapping_activation_thread(node_model, channelmapping_activated, gate); } + [&, channelmapping_activated] { nmos::channelmapping_activation_thread(node_model, channelmapping_activated, gate); }, + [&, validate_sender_resources, validate_receiver] { nmos::experimental::streamcompatibility_behaviour_thread(node_model, validate_sender_resources, validate_receiver, gate); } }); auto system_changed = node_implementation.system_changed; diff --git a/Development/nmos/node_server.h b/Development/nmos/node_server.h index 84597a3f8..b914c05a8 100644 --- a/Development/nmos/node_server.h +++ b/Development/nmos/node_server.h @@ -9,6 +9,8 @@ #include "nmos/connection_activation.h" #include "nmos/configuration_handlers.h" #include "nmos/control_protocol_handlers.h" +#include "nmos/streamcompatibility_api.h" +#include "nmos/streamcompatibility_validation.h" #include "nmos/node_behaviour.h" #include "nmos/node_system_behaviour.h" #include "nmos/ocsp_response_handler.h" @@ -28,7 +30,7 @@ namespace nmos // underlying implementation into the server instance for the NMOS Node struct node_implementation { - node_implementation(nmos::load_server_certificates_handler load_server_certificates, nmos::load_dh_param_handler load_dh_param, nmos::load_ca_certificates_handler load_ca_certificates, nmos::system_global_handler system_changed, nmos::registration_handler registration_changed, nmos::transport_file_parser parse_transport_file, nmos::details::connection_resource_patch_validator validate_staged, nmos::connection_resource_auto_resolver resolve_auto, nmos::connection_sender_transportfile_setter set_transportfile, nmos::connection_activation_handler connection_activated, nmos::ocsp_response_handler get_ocsp_response, get_authorization_bearer_token_handler get_authorization_bearer_token, validate_authorization_handler validate_authorization, ws_validate_authorization_handler ws_validate_authorization, nmos::load_rsa_private_keys_handler load_rsa_private_keys, load_authorization_clients_handler load_authorization_clients, save_authorization_client_handler save_authorization_client, request_authorization_code_handler request_authorization_code, nmos::get_control_protocol_class_descriptor_handler get_control_protocol_class_descriptor, nmos::get_control_protocol_datatype_descriptor_handler get_control_protocol_datatype_descriptor, nmos::get_control_protocol_method_descriptor_handler get_control_protocol_method_descriptor, nmos::control_protocol_property_changed_handler control_protocol_property_changed, nmos::create_validation_fingerprint_handler create_validation_fingerprint, nmos::validate_validation_fingerprint_handler validate_validation_fingerprint, nmos::get_read_only_modification_allow_list_handler get_read_only_modification_allow_list, remove_device_model_object_handler remove_device_model_object, create_device_model_object_handler create_device_model_object,nmos::control_protocol_connection_activation_handler monitor_connection_activated, nmos::get_packet_counters_handler get_lost_packet_counters, nmos::get_packet_counters_handler get_late_packet_counters, nmos::reset_monitor_handler reset_monitor) + node_implementation(nmos::load_server_certificates_handler load_server_certificates, nmos::load_dh_param_handler load_dh_param, nmos::load_ca_certificates_handler load_ca_certificates, nmos::system_global_handler system_changed, nmos::registration_handler registration_changed, nmos::transport_file_parser parse_transport_file, nmos::details::connection_resource_patch_validator validate_staged, nmos::connection_resource_auto_resolver resolve_auto, nmos::connection_sender_transportfile_setter set_transportfile, nmos::connection_activation_handler connection_activated, nmos::ocsp_response_handler get_ocsp_response, get_authorization_bearer_token_handler get_authorization_bearer_token, validate_authorization_handler validate_authorization, ws_validate_authorization_handler ws_validate_authorization, nmos::load_rsa_private_keys_handler load_rsa_private_keys, load_authorization_clients_handler load_authorization_clients, save_authorization_client_handler save_authorization_client, request_authorization_code_handler request_authorization_code, nmos::get_control_protocol_class_descriptor_handler get_control_protocol_class_descriptor, nmos::get_control_protocol_datatype_descriptor_handler get_control_protocol_datatype_descriptor, nmos::get_control_protocol_method_descriptor_handler get_control_protocol_method_descriptor, nmos::control_protocol_property_changed_handler control_protocol_property_changed, nmos::create_validation_fingerprint_handler create_validation_fingerprint, nmos::validate_validation_fingerprint_handler validate_validation_fingerprint, nmos::get_read_only_modification_allow_list_handler get_read_only_modification_allow_list, remove_device_model_object_handler remove_device_model_object, create_device_model_object_handler create_device_model_object,nmos::control_protocol_connection_activation_handler monitor_connection_activated, nmos::get_packet_counters_handler get_lost_packet_counters, nmos::get_packet_counters_handler get_late_packet_counters, nmos::reset_monitor_handler reset_monitor, nmos::experimental::details::streamcompatibility_base_edid_handler validate_base_edid, nmos::experimental::details::streamcompatibility_effective_edid_setter set_effective_edid, nmos::experimental::details::streamcompatibility_active_constraints_handler validate_active_constraints, nmos::experimental::details::streamcompatibility_sender_validator validate_sender_resources, nmos::experimental::details::streamcompatibility_receiver_validator validate_receiver) : load_server_certificates(std::move(load_server_certificates)) , load_dh_param(std::move(load_dh_param)) , load_ca_certificates(std::move(load_ca_certificates)) @@ -60,12 +62,19 @@ namespace nmos , get_lost_packet_counters(std::move(get_lost_packet_counters)) , get_late_packet_counters(std::move(get_late_packet_counters)) , reset_monitor(std::move(reset_monitor)) + , validate_base_edid(std::move(validate_base_edid)) + , set_effective_edid(std::move(set_effective_edid)) + , validate_active_constraints(std::move(validate_active_constraints)) + , validate_sender_resources(std::move(validate_sender_resources)) + , validate_receiver(std::move(validate_receiver)) {} // use the default constructor and chaining member functions for fluent initialization // (by itself, the default constructor does not construct a valid instance) node_implementation() : parse_transport_file(&nmos::parse_rtp_transport_file) + , validate_sender_resources(make_streamcompatibility_sender_resources_validator(&nmos::experimental::match_resource_parameters_constraint_set, make_streamcompatibility_sdp_constraint_sets_matcher(&nmos::match_sdp_parameters_constraint_sets))) + , validate_receiver(make_streamcompatibility_receiver_validator(&nmos::experimental::validate_rtp_transport_file)) {} node_implementation& on_load_server_certificates(nmos::load_server_certificates_handler load_server_certificates) { this->load_server_certificates = std::move(load_server_certificates); return *this; } @@ -88,6 +97,11 @@ namespace nmos node_implementation& on_load_authorization_clients(load_authorization_clients_handler load_authorization_clients) { this->load_authorization_clients = std::move(load_authorization_clients); return *this; } node_implementation& on_save_authorization_client(save_authorization_client_handler save_authorization_client) { this->save_authorization_client = std::move(save_authorization_client); return *this; } node_implementation& on_request_authorization_code(request_authorization_code_handler request_authorization_code) { this->request_authorization_code = std::move(request_authorization_code); return *this; } + node_implementation& on_validate_base_edid(nmos::experimental::details::streamcompatibility_base_edid_handler validate_base_edid) { this->validate_base_edid = std::move(validate_base_edid); return *this; } + node_implementation& on_set_effective_edid(nmos::experimental::details::streamcompatibility_effective_edid_setter set_effective_edid) { this->set_effective_edid = std::move(set_effective_edid); return *this; } + node_implementation& on_active_constraints_changed(nmos::experimental::details::streamcompatibility_active_constraints_handler validate_active_constraints) { this->validate_active_constraints = std::move(validate_active_constraints); return *this; } + node_implementation& on_validate_sender_resources_against_active_constraints(nmos::experimental::details::streamcompatibility_sender_validator validate_sender_resources) { this->validate_sender_resources = std::move(validate_sender_resources); return *this; } + node_implementation& on_validate_receiver_against_transport_file(nmos::experimental::details::streamcompatibility_receiver_validator validate_receiver) { this->validate_receiver = std::move(validate_receiver); return *this; } node_implementation& on_get_control_class_descriptor(nmos::get_control_protocol_class_descriptor_handler get_control_protocol_class_descriptor) { this->get_control_protocol_class_descriptor = std::move(get_control_protocol_class_descriptor); return *this; } node_implementation& on_get_control_datatype_descriptor(nmos::get_control_protocol_datatype_descriptor_handler get_control_protocol_datatype_descriptor) { this->get_control_protocol_datatype_descriptor = std::move(get_control_protocol_datatype_descriptor); return *this; } node_implementation& on_get_control_protocol_method_descriptor(nmos::get_control_protocol_method_descriptor_handler get_control_protocol_method_descriptor) { this->get_control_protocol_method_descriptor = std::move(get_control_protocol_method_descriptor); return *this; } @@ -139,6 +153,13 @@ namespace nmos save_authorization_client_handler save_authorization_client; request_authorization_code_handler request_authorization_code; + // Stream Compatibility Management handlers + nmos::experimental::details::streamcompatibility_base_edid_handler validate_base_edid; + nmos::experimental::details::streamcompatibility_effective_edid_setter set_effective_edid; + nmos::experimental::details::streamcompatibility_active_constraints_handler validate_active_constraints; + nmos::experimental::details::streamcompatibility_sender_validator validate_sender_resources; + nmos::experimental::details::streamcompatibility_receiver_validator validate_receiver; + nmos::get_control_protocol_class_descriptor_handler get_control_protocol_class_descriptor; nmos::get_control_protocol_datatype_descriptor_handler get_control_protocol_datatype_descriptor; nmos::get_control_protocol_method_descriptor_handler get_control_protocol_method_descriptor; diff --git a/Development/nmos/scope.h b/Development/nmos/scope.h index ee623e55f..359fc16d1 100644 --- a/Development/nmos/scope.h +++ b/Development/nmos/scope.h @@ -24,6 +24,8 @@ namespace nmos const scope events{ U("events") }; // IS-08 const scope channelmapping{ U("channelmapping") }; + // IS-11 + const scope streamcompatibility{ U("streamcompatibility") }; // IS-12 const scope ncp{ U("ncp") }; // IS-14 @@ -44,6 +46,7 @@ namespace nmos if (scopes::netctrl.name == scope) { return scopes::netctrl; } if (scopes::events.name == scope) { return scopes::events; } if (scopes::channelmapping.name == scope) { return scopes::channelmapping; } + if (scopes::streamcompatibility.name == scope) { return scopes::streamcompatibility; } if (scopes::ncp.name == scope) { return scopes::ncp; } if (scopes::configuration.name == scope) { return scopes::configuration; } return{}; diff --git a/Development/nmos/sdp_utils.cpp b/Development/nmos/sdp_utils.cpp index 9e59dca0c..2f70c9702 100644 --- a/Development/nmos/sdp_utils.cpp +++ b/Development/nmos/sdp_utils.cpp @@ -1634,4 +1634,13 @@ namespace nmos } return s; } + + // Check the specified SDP parameters against the specified constraint sets + // for "video/raw", "audio/L", "video/smpte291" or "video/SMPTE2022-6" + bool match_sdp_parameters_constraint_sets(const web::json::array& constraint_sets, const sdp_parameters& sdp_params) + { + const auto format_params = nmos::details::get_format_parameters(sdp_params); + const auto found = std::find_if(constraint_sets.begin(), constraint_sets.end(), [&](const web::json::value& constraint_set) { return details::match_sdp_parameters_constraint_set(details::format_constraints, sdp_params, format_params, constraint_set); }); + return constraint_sets.end() != found; + } } diff --git a/Development/nmos/sdp_utils.h b/Development/nmos/sdp_utils.h index bf854a47c..bb27574c7 100644 --- a/Development/nmos/sdp_utils.h +++ b/Development/nmos/sdp_utils.h @@ -63,6 +63,10 @@ namespace nmos // Validate the SDP parameters against a receiver for "video/raw", "audio/L", "video/smpte291" or "video/SMPTE2022-6" void validate_sdp_parameters(const web::json::value& receiver, const sdp_parameters& sdp_params); + // Check the specified SDP parameters against the specified constraint sets + // for "video/raw", "audio/L", "video/smpte291" or "video/SMPTE2022-6" + bool match_sdp_parameters_constraint_sets(const web::json::array& constraint_sets, const sdp_parameters& sdp_params); + // sdp_parameters helper functions // Validate the numeric string diff --git a/Development/nmos/settings.cpp b/Development/nmos/settings.cpp index 94251cb4d..f0d916d5c 100644 --- a/Development/nmos/settings.cpp +++ b/Development/nmos/settings.cpp @@ -76,6 +76,7 @@ namespace nmos if (!registry) web::json::insert(settings, std::make_pair(nmos::fields::connection_port, http_port)); if (!registry) web::json::insert(settings, std::make_pair(nmos::fields::events_port, http_port)); if (!registry) web::json::insert(settings, std::make_pair(nmos::fields::channelmapping_port, http_port)); + if (!registry) web::json::insert(settings, std::make_pair(nmos::fields::streamcompatibility_port, http_port)); if (!registry) web::json::insert(settings, std::make_pair(nmos::fields::events_ws_port, ws_port)); if (!registry) web::json::insert(settings, std::make_pair(nmos::experimental::fields::manifest_port, http_port)); web::json::insert(settings, std::make_pair(nmos::experimental::fields::settings_port, http_port)); diff --git a/Development/nmos/settings.h b/Development/nmos/settings.h index 4f1a09328..c16dce344 100644 --- a/Development/nmos/settings.h +++ b/Development/nmos/settings.h @@ -104,6 +104,9 @@ namespace nmos // is10_versions [registry, node]: used to specify the enabled API versions for a version-locked configuration const web::json::field_as_array is10_versions{ U("is10_versions") }; // when omitted, nmos::is10_versions::all is used + // is11_versions [node]: used to specify the enabled API versions for a version-locked configuration + const web::json::field_as_array is11_versions{ U("is11_versions") }; // when omitted, nmos::is11_versions::all is used + // is12_versions [node]: used to specify the enabled API versions for a version-locked configuration const web::json::field_as_array is12_versions{ U("is12_versions") }; // when omitted, nmos::is12_versions::all is used @@ -155,6 +158,7 @@ namespace nmos // control_protocol_ws_port [node]: used to construct request URLs for the Control Protocol websocket, or negative to disable the control protocol features const web::json::field_as_integer_or control_protocol_ws_port{ U("control_protocol_ws_port"), 3218 }; const web::json::field_as_integer_or configuration_port{ U("configuration_port"), 3219 }; + const web::json::field_as_integer_or streamcompatibility_port{ U("streamcompatibility_port"), 3220 }; // listen_backlog [registry, node]: the maximum length of the queue of pending connections, or zero for the implementation default (the implementation may not honour this value) const web::json::field_as_integer_or listen_backlog{ U("listen_backlog"), 0 }; diff --git a/Development/nmos/streamcompatibility_api.cpp b/Development/nmos/streamcompatibility_api.cpp new file mode 100644 index 000000000..e03a1b36b --- /dev/null +++ b/Development/nmos/streamcompatibility_api.cpp @@ -0,0 +1,972 @@ +#include "nmos/streamcompatibility_api.h" +#include "nmos/streamcompatibility_utils.h" + +#include +#include +#include +#include "cpprest/containerstream.h" +#include "cpprest/json_validator.h" +#include "nmos/api_utils.h" // for set_error_reply +#include "nmos/capabilities.h" // for nmos::fields::constraint_sets +#include "nmos/streamcompatibility_resource.h" +#include "nmos/streamcompatibility_resources.h" +#include "nmos/is11_versions.h" +#include "nmos/json_schema.h" +#include "nmos/model.h" + +namespace nmos +{ + namespace experimental + { + namespace details + { + void set_edid_endpoint_as_reply(web::http::http_response& res, const std::pair& id_type, const web::json::value& edid_endpoint, nmos::api_gate& gate) + { + if (!edid_endpoint.is_null()) + { + // The base edid endpoint may be an empty object which signalizes that Base EDID is currently absent but it's allowed to be set + // The effective edid and edid endpoints should contain either edid_binary or edid_href + auto& edid_binary = nmos::fields::edid_binary(edid_endpoint); + auto& edid_href = nmos::fields::edid_href(edid_endpoint); + + if (!edid_binary.is_null()) + { + slog::log(gate, SLOG_FLF) << "Returning EDID binary for " << id_type; + // Convert wchar_t to uint8_t if utility::string_t consists of wide chars + auto edid_string = edid_binary.as_string(); + std::vector edid_vector; + std::transform(edid_string.begin(), edid_string.end(), std::back_inserter(edid_vector), [](utility::char_t char_element) { + return static_cast(char_element); + }); + auto i_stream = concurrency::streams::bytestream::open_istream(edid_vector); + set_reply(res, web::http::status_codes::OK, i_stream); + } + else if (!edid_href.is_null()) + { + slog::log(gate, SLOG_FLF) << "Redirecting to EDID file for " << id_type; + + set_reply(res, web::http::status_codes::TemporaryRedirect); + res.headers().add(web::http::header_names::location, nmos::fields::edid_href(edid_endpoint).as_string()); + } + else + { + slog::log(gate, SLOG_FLF) << "EDID requested for " << id_type << " does not exist"; + + set_reply(res, web::http::status_codes::NoContent); + } + } + else + { + slog::log(gate, SLOG_FLF) << "EDID requested for " << id_type << " does not exist"; + + set_reply(res, web::http::status_codes::NoContent); + } + } + + bool all_resources_exist(nmos::resources& resources, const web::json::array& resource_ids, const nmos::type& type) + { + for (const auto& resource_id : resource_ids) + { + if (resources.end() == find_resource(resources, std::make_pair(resource_id.as_string(), type))) + { + return false; + } + } + return true; + } + + bool validate_constraint_sets(const web::json::array& constraint_sets, const std::unordered_set& supported_param_constraints) + { + for (const auto& constraint_set : constraint_sets) + { + const auto& param_constraints = constraint_set.as_object(); + if (param_constraints.end() != std::find_if(param_constraints.begin(), param_constraints.end(), [&supported_param_constraints](const std::pair& constraint) + { + return supported_param_constraints.count(constraint.first) == 0; + })) + { + return false; + } + } + return true; + } + + // it's expected that model write lock is already obtained and an input with the input_id exists + void update_effective_edid(nmos::resources& node_resources, nmos::resources& streamcompatibility_resources, const streamcompatibility_effective_edid_setter& effective_edid_setter, const nmos::id& input_id) + { + boost::variant effective_edid; + + // get effective_edid from application code + effective_edid_setter(input_id, effective_edid); + + utility::string_t updated_timestamp{ nmos::make_version() }; + + modify_resource(streamcompatibility_resources, input_id, [&effective_edid, updated_timestamp](nmos::resource& input) + { + input.data[nmos::fields::endpoint_effective_edid] = boost::apply_visitor(edid_file_visitor(), effective_edid); + + input.data[nmos::fields::version] = web::json::value::string(updated_timestamp); + }); + + const std::pair id_type{ input_id, nmos::types::input }; + auto input = find_resource(streamcompatibility_resources, id_type); + + // `If the Effective EDID for the Input changes and it is associated with any Senders, then all of the Senders in question MUST update their versions` + // See https://specs.amwa.tv/is-11/branches/v1.0.x/docs/Behaviour_-_Server_Side.html#effective-edid + const auto& sender_ids = nmos::fields::senders(input->data); + update_version(node_resources, + boost::copy_range>(sender_ids | boost::adaptors::transformed([&node_resources](const web::json::value& sender_id) {return sender_id.as_string(); })), + updated_timestamp); + + // `When properties of any Input/Output are changed, then the version attribute of the relevant IS-04 Device MUST be incremented. + // Inputs/Outputs identify the corresponding Devices via the device_id property.` + // See https://specs.amwa.tv/is-11/branches/v1.0.x/docs/Interoperability.html#version-increments + const auto device_ids = boost::copy_range>(sender_ids | boost::adaptors::transformed([&node_resources](const web::json::value& sender_id) + { + auto sender = find_resource(node_resources, { sender_id.as_string(), nmos::types::sender }); + return (node_resources.end() != sender && sender->has_data()) ? nmos::fields::device_id(sender->data) : nmos::id{}; + })); + update_version(node_resources, device_ids, updated_timestamp); + } + + // it's expected that model write lock is already obtained and IS-11 sender exists + void set_active_constraints(nmos::resources& node_resources, nmos::resources& streamcompatibility_resources, const nmos::id& sender_id, const web::json::value& constraints, const web::json::value& intersection, const streamcompatibility_effective_edid_setter& effective_edid_setter) + { + const std::pair sender_id_type{ sender_id, nmos::types::sender }; + auto resource = find_resource(streamcompatibility_resources, sender_id_type); + auto matching_resource = find_resource(node_resources, sender_id_type); + if (node_resources.end() == matching_resource) + { + throw std::logic_error("matching IS-04 sender not found"); + } + + // Pre-check for resources existence before Active Constraints modified and effective_edid_setter executed + if (effective_edid_setter) + { + for (const auto& input_id : nmos::fields::inputs(resource->data)) + { + const std::pair input_id_type{ input_id.as_string(), nmos::types::input }; + auto input = find_resource(streamcompatibility_resources, input_id_type); + if (streamcompatibility_resources.end() != input) + { + if (!all_resources_exist(node_resources, nmos::fields::senders(input->data), nmos::types::sender)) + { + throw std::logic_error("associated IS-04 sender not found"); + } + } + else + { + throw std::logic_error("associated IS-11 input not found"); + } + } + } + + utility::string_t updated_timestamp{ nmos::make_version() }; + + // Update Active Constraints in streamcompatibility_resources + modify_resource(streamcompatibility_resources, sender_id, [&constraints, &intersection, &updated_timestamp](nmos::resource& sender) + { + sender.data[nmos::fields::intersection_of_caps_and_constraints] = intersection; + sender.data[nmos::fields::endpoint_active_constraints] = make_streamcompatibility_active_constraints_endpoint(constraints); + + sender.data[nmos::fields::version] = web::json::value::string(updated_timestamp); + }); + + // `When Active Constraints of Sender are modified, then the version attribute of the relevant IS-04 Sender MUST be incremented.` + // See https://specs.amwa.tv/is-11/branches/v1.0.x/docs/Interoperability.html#version-increments + update_version(node_resources, sender_id, updated_timestamp); + + // hmmmm, should the version of the associated Device be incremented? + + // Update Input effective EDID, which is provided by the application code + if (effective_edid_setter) + { + for (const auto& input_id : nmos::fields::inputs(resource->data)) + { + details::update_effective_edid(node_resources, streamcompatibility_resources, effective_edid_setter, input_id.as_string()); + } + } + } + } + + web::http::experimental::listener::api_router make_unmounted_streamcompatibility_api(nmos::node_model& model, details::streamcompatibility_base_edid_handler validate_base_edid, details::streamcompatibility_effective_edid_setter effective_edid_setter, details::streamcompatibility_active_constraints_handler validate_active_constraints, slog::base_gate& gate); + + web::http::experimental::listener::api_router make_streamcompatibility_api(nmos::node_model& model, details::streamcompatibility_base_edid_handler validate_base_edid, details::streamcompatibility_effective_edid_setter effective_edid_setter, details::streamcompatibility_active_constraints_handler validate_active_constraints, web::http::experimental::listener::route_handler validate_authorization, slog::base_gate& gate) + { + using namespace web::http::experimental::listener::api_router_using_declarations; + + api_router streamcompatibility_api; + + streamcompatibility_api.support(U("/?"), methods::GET, [](http_request req, http_response res, const string_t&, const route_parameters&) + { + set_reply(res, status_codes::OK, nmos::make_sub_routes_body({ U("x-nmos/") }, req, res)); + return pplx::task_from_result(true); + }); + + streamcompatibility_api.support(U("/x-nmos/?"), methods::GET, [](http_request req, http_response res, const string_t&, const route_parameters&) + { + set_reply(res, status_codes::OK, nmos::make_sub_routes_body({ U("streamcompatibility/") }, req, res)); + return pplx::task_from_result(true); + }); + + if (validate_authorization) + { + streamcompatibility_api.support(U("/x-nmos/") + nmos::patterns::streamcompatibility_api.pattern + U("/?"), validate_authorization); + streamcompatibility_api.support(U("/x-nmos/") + nmos::patterns::streamcompatibility_api.pattern + U("/.*"), validate_authorization); + } + + const auto versions = with_read_lock(model.mutex, [&model] { return nmos::is11_versions::from_settings(model.settings); }); + streamcompatibility_api.support(U("/x-nmos/") + nmos::patterns::streamcompatibility_api.pattern + U("/?"), methods::GET, [versions](http_request req, http_response res, const string_t&, const route_parameters&) + { + set_reply(res, status_codes::OK, nmos::make_sub_routes_body(nmos::make_api_version_sub_routes(versions), req, res)); + return pplx::task_from_result(true); + }); + + streamcompatibility_api.mount(U("/x-nmos/") + nmos::patterns::streamcompatibility_api.pattern + U("/") + nmos::patterns::version.pattern, make_unmounted_streamcompatibility_api(model, validate_base_edid, effective_edid_setter, validate_active_constraints, gate)); + + return streamcompatibility_api; + } + + web::http::experimental::listener::api_router make_unmounted_streamcompatibility_api(nmos::node_model& model, details::streamcompatibility_base_edid_handler validate_base_edid, details::streamcompatibility_effective_edid_setter effective_edid_setter, details::streamcompatibility_active_constraints_handler validate_active_constraints, slog::base_gate& gate_) + { + using namespace web::http::experimental::listener::api_router_using_declarations; + + api_router streamcompatibility_api; + + // check for supported API version + const auto versions = with_read_lock(model.mutex, [&model] { return nmos::is11_versions::from_settings(model.settings); }); + + const web::json::experimental::json_validator validator + { + nmos::experimental::load_json_schema, + boost::copy_range>(versions | boost::adaptors::transformed(experimental::make_streamcompatibilityapi_senders_active_constraints_put_request_uri)) + }; + + streamcompatibility_api.support(U(".*"), nmos::details::make_api_version_handler(versions, gate_)); + + streamcompatibility_api.support(U("/?"), methods::GET, [](http_request req, http_response res, const string_t&, const route_parameters&) + { + set_reply(res, status_codes::OK, nmos::make_sub_routes_body({ U("senders/"), U("receivers/"), U("inputs/"), U("outputs/") }, req, res)); + return pplx::task_from_result(true); + }); + + streamcompatibility_api.support(U("/") + nmos::patterns::streamCompatibilityResourceType.pattern + U("/?"), methods::GET, [&model, &gate_](http_request req, http_response res, const string_t&, const route_parameters& parameters) + { + nmos::api_gate gate(gate_, req, parameters); + auto lock = model.read_lock(); + auto& resources = model.streamcompatibility_resources; + + const string_t resourceType = parameters.at(nmos::patterns::streamCompatibilityResourceType.name); + + const auto match = [&resourceType](const nmos::resources::value_type& resource) { return resource.type == nmos::type_from_resourceType(resourceType); }; + + size_t count = 0; + + // experimental extension, to support human-readable HTML rendering of NMOS responses + if (experimental::details::is_html_response_preferred(req, web::http::details::mime_types::application_json)) + { + set_reply(res, status_codes::OK, + web::json::serialize_array(resources + | boost::adaptors::filtered(match) + | boost::adaptors::transformed( + [&count, &req](const nmos::resource& resource) { ++count; return experimental::details::make_html_response_a_tag(resource.id + U("/"), req); } + )), + web::http::details::mime_types::application_json); + } + else + { + set_reply(res, status_codes::OK, + web::json::serialize_array(resources + | boost::adaptors::filtered(match) + | boost::adaptors::transformed( + [&count](const nmos::resource& resource) { ++count; return value(resource.id + U("/")); } + ) + ), + web::http::details::mime_types::application_json); + } + + slog::log(gate, SLOG_FLF) << "Returning " << count << " matching " << resourceType; + + return pplx::task_from_result(true); + }); + + streamcompatibility_api.support(U("/") + nmos::patterns::streamCompatibilityResourceType.pattern + U("/") + nmos::patterns::resourceId.pattern + U("/?"), methods::GET, [&model](http_request req, http_response res, const string_t&, const route_parameters& parameters) + { + auto lock = model.read_lock(); + auto& resources = model.streamcompatibility_resources; + + const string_t resourceType = parameters.at(nmos::patterns::streamCompatibilityResourceType.name); + const string_t resourceId = parameters.at(nmos::patterns::resourceId.name); + + const std::pair id_type{ resourceId, nmos::type_from_resourceType(resourceType) }; + auto resource = find_resource(resources, id_type); + if (resources.end() != resource) + { + // check if there is a matching IS-04 sender/receiver resource + if (nmos::types::sender == resource->type || + nmos::types::receiver == resource->type) + { + auto matching_resource = find_resource(model.node_resources, id_type); + if (model.node_resources.end() == matching_resource) + { + throw std::logic_error("matching IS-04 " + utility::us2s(id_type.second.name) + " not found"); + } + } + + std::set sub_routes; + if (nmos::types::sender == resource->type) + { + sub_routes = { U("inputs/"), U("status/"), U("constraints/") }; + } + else if (nmos::types::receiver == resource->type) + { + sub_routes = { U("outputs/"), U("status/") }; + } + else + { + sub_routes = { U("edid/"), U("properties/") }; + } + + set_reply(res, status_codes::OK, nmos::make_sub_routes_body(std::move(sub_routes), req, res)); + } + else + { + set_reply(res, status_codes::NotFound); + } + + return pplx::task_from_result(true); + }); + + streamcompatibility_api.support(U("/") + nmos::patterns::connectorType.pattern + U("/") + nmos::patterns::resourceId.pattern + U("/") + nmos::patterns::streamCompatibilityInputOutputType.pattern + U("/?"), methods::GET, [&model](http_request req, http_response res, const string_t&, const route_parameters& parameters) + { + auto lock = model.read_lock(); + auto& resources = model.streamcompatibility_resources; + + const auto resourceType = nmos::type_from_resourceType(parameters.at(nmos::patterns::connectorType.name)); + const string_t resourceId = parameters.at(nmos::patterns::resourceId.name); + const auto associatedResourceType = nmos::type_from_resourceType(parameters.at(nmos::patterns::streamCompatibilityInputOutputType.name)); + + const bool consistentTypes + { + (nmos::types::sender == resourceType && nmos::types::input == associatedResourceType) || + (nmos::types::receiver == resourceType && nmos::types::output == associatedResourceType) + }; + + const std::pair id_type{ resourceId, resourceType }; + auto resource = find_resource(resources, id_type); + if (resources.end() != resource && consistentTypes) + { + // check if there is a matching IS-04 sender/receiver resource + auto matching_resource = find_resource(model.node_resources, id_type); + if (model.node_resources.end() == matching_resource) + { + throw std::logic_error("matching IS-04 " + utility::us2s(id_type.second.name) + " not found"); + } + + const auto filter = (nmos::types::input == associatedResourceType) ? + nmos::fields::inputs : + nmos::fields::outputs; + + set_reply(res, status_codes::OK, web::json::value_from_elements(filter(resource->data))); + } + else + { + set_reply(res, status_codes::NotFound); + } + + return pplx::task_from_result(true); + }); + + streamcompatibility_api.support(U("/") + nmos::patterns::connectorType.pattern + U("/") + nmos::patterns::resourceId.pattern + U("/status/?"), methods::GET, [&model](http_request req, http_response res, const string_t&, const route_parameters& parameters) + { + auto lock = model.read_lock(); + auto& resources = model.streamcompatibility_resources; + + const string_t resourceType = parameters.at(nmos::patterns::connectorType.name); + const string_t resourceId = parameters.at(nmos::patterns::resourceId.name); + + const std::pair id_type{ resourceId, nmos::type_from_resourceType(resourceType) }; + auto resource = find_resource(resources, id_type); + if (resources.end() != resource) + { + // check if there is a matching IS-04 sender/receiver resource + auto matching_resource = find_resource(model.node_resources, id_type); + if (model.node_resources.end() == matching_resource) + { + throw std::logic_error("matching IS-04 " + utility::us2s(id_type.second.name) + " not found"); + } + + set_reply(res, status_codes::OK, nmos::fields::status(resource->data)); + } + else + { + set_reply(res, status_codes::NotFound); + } + + return pplx::task_from_result(true); + }); + + streamcompatibility_api.support(U("/") + nmos::patterns::senderType.pattern + U("/") + nmos::patterns::resourceId.pattern + U("/constraints/?"), methods::GET, [&model](http_request req, http_response res, const string_t&, const route_parameters& parameters) + { + auto lock = model.read_lock(); + auto& resources = model.streamcompatibility_resources; + + const string_t resourceId = parameters.at(nmos::patterns::resourceId.name); + + const std::pair id_type{ resourceId, nmos::types::sender }; + auto resource = find_resource(resources, id_type); + if (resources.end() != resource) + { + // check if there is a matching IS-04 sender/receiver resource + auto matching_resource = find_resource(model.node_resources, id_type); + if (model.node_resources.end() == matching_resource) + { + throw std::logic_error("matching IS-04 " + utility::us2s(id_type.second.name) + " not found"); + } + + std::set sub_routes{ U("active/"), U("supported/") }; + + set_reply(res, status_codes::OK, nmos::make_sub_routes_body(std::move(sub_routes), req, res)); + } + else + { + set_reply(res, status_codes::NotFound); + } + + return pplx::task_from_result(true); + }); + + streamcompatibility_api.support(U("/") + nmos::patterns::senderType.pattern + U("/") + nmos::patterns::resourceId.pattern + U("/constraints/") + nmos::patterns::constraintsType.pattern + U("/?"), methods::GET, [&model](http_request req, http_response res, const string_t&, const route_parameters& parameters) + { + auto lock = model.read_lock(); + auto& resources = model.streamcompatibility_resources; + + const string_t constraintsType = parameters.at(nmos::patterns::constraintsType.name); + const string_t resourceId = parameters.at(nmos::patterns::resourceId.name); + + const std::pair id_type{ resourceId, nmos::types::sender }; + auto resource = find_resource(resources, id_type); + if (resources.end() != resource) + { + // check if there is a matching IS-04 sender/receiver resource + auto matching_resource = find_resource(model.node_resources, id_type); + if (model.node_resources.end() == matching_resource) + { + throw std::logic_error("matching IS-04 " + utility::us2s(id_type.second.name) + " not found"); + } + + if (U("active") == constraintsType) + { + set_reply(res, status_codes::OK, nmos::fields::active_constraint_sets(nmos::fields::endpoint_active_constraints(resource->data))); + } + else if (U("supported") == constraintsType) + { + set_reply(res, status_codes::OK, nmos::fields::supported_param_constraints(resource->data)); + } + else + { + // should never happen + set_reply(res, status_codes::NotFound); + } + } + else + { + set_reply(res, status_codes::NotFound); + } + + return pplx::task_from_result(true); + }); + + streamcompatibility_api.support(U("/") + nmos::patterns::inputOutputType.pattern + U("/") + nmos::patterns::resourceId.pattern + U("/properties/?"), methods::GET, [&model](http_request req, http_response res, const string_t&, const route_parameters& parameters) + { + auto lock = model.read_lock(); + auto& resources = model.streamcompatibility_resources; + + const string_t resourceType = parameters.at(nmos::patterns::inputOutputType.name); + const string_t resourceId = parameters.at(nmos::patterns::resourceId.name); + + const std::pair id_type{ resourceId, nmos::type_from_resourceType(resourceType) }; + auto resource = find_resource(resources, id_type); + if (resources.end() != resource) + { + auto data = resource->data; + + // Note: input/output resources hold information about the EDID binary and they shouldn't be included in the response + if (nmos::types::input == nmos::type_from_resourceType(resourceType)) + { + if (!nmos::fields::endpoint_base_edid(resource->data).is_null()) + { + data.erase(nmos::fields::endpoint_base_edid); + } + if (!nmos::fields::endpoint_effective_edid(resource->data).is_null()) + { + data.erase(nmos::fields::endpoint_effective_edid); + } + data.erase(nmos::fields::senders); + } + else if (nmos::types::output == nmos::type_from_resourceType(resourceType)) + { + if (!nmos::fields::endpoint_edid(resource->data).is_null()) + { + data.erase(nmos::fields::endpoint_edid); + } + data.erase(nmos::fields::receivers); + } + else + { + // should never happen + set_reply(res, status_codes::NotImplemented); + } + + set_reply(res, status_codes::OK, data); + } + else + { + set_reply(res, status_codes::NotFound); + } + + return pplx::task_from_result(true); + }); + + streamcompatibility_api.support(U("/") + nmos::patterns::inputOutputType.pattern + U("/") + nmos::patterns::resourceId.pattern + U("/edid/?"), methods::GET, [&model, &gate_](http_request req, http_response res, const string_t&, const route_parameters& parameters) + { + nmos::api_gate gate(gate_, req, parameters); + auto lock = model.read_lock(); + auto& resources = model.streamcompatibility_resources; + + const string_t resourceType = parameters.at(nmos::patterns::inputOutputType.name); + const string_t resourceId = parameters.at(nmos::patterns::resourceId.name); + + const std::pair id_type{ resourceId, nmos::type_from_resourceType(resourceType) }; + auto resource = find_resource(resources, id_type); + if (resources.end() != resource) + { + if (nmos::types::input == nmos::type_from_resourceType(resourceType)) + { + std::set sub_routes{ U("base/"), U("effective/") }; + + set_reply(res, status_codes::OK, nmos::make_sub_routes_body(std::move(sub_routes), req, res)); + } + else if (nmos::types::output == nmos::type_from_resourceType(resourceType)) + { + auto& edid_endpoint = nmos::fields::endpoint_edid(resource->data); + + slog::log(gate, SLOG_FLF) << "GET EDID requested for " << id_type; + + details::set_edid_endpoint_as_reply(res, id_type, edid_endpoint, gate); + } + else + { + // should never happen + set_reply(res, status_codes::NotImplemented); + } + } + else + { + set_reply(res, status_codes::NotFound); + } + + return pplx::task_from_result(true); + }); + + streamcompatibility_api.support(U("/") + nmos::patterns::inputType.pattern + U("/") + nmos::patterns::resourceId.pattern + U("/edid/") + nmos::patterns::edidType.pattern + U("/?"), methods::GET, [&model, &gate_](http_request req, http_response res, const string_t&, const route_parameters& parameters) + { + nmos::api_gate gate(gate_, req, parameters); + auto lock = model.read_lock(); + auto& resources = model.streamcompatibility_resources; + + const string_t resourceId = parameters.at(nmos::patterns::resourceId.name); + const string_t edidType = parameters.at(nmos::patterns::edidType.name); + + const std::pair id_type{ resourceId, nmos::types::input }; + auto resource = find_resource(resources, id_type); + if (resources.end() != resource) + { + const auto filter = (U("base") == edidType) ? + nmos::fields::endpoint_base_edid : + nmos::fields::endpoint_effective_edid; + + auto& edid_endpoint = filter(resource->data); + + slog::log(gate, SLOG_FLF) << "GET EDID " << edidType << " requested for " << id_type; + + details::set_edid_endpoint_as_reply(res, id_type, edid_endpoint, gate); + } + else + { + set_reply(res, status_codes::NotFound); + } + + return pplx::task_from_result(true); + }); + + streamcompatibility_api.support(U("/") + nmos::patterns::inputType.pattern + U("/") + nmos::patterns::resourceId.pattern + U("/edid/base/?"), methods::PUT, [&model, validate_base_edid, effective_edid_setter, &gate_](http_request req, http_response res, const string_t&, const route_parameters& parameters) + { + nmos::api_gate gate(gate_, req, parameters); + + // Extract "adjust_to_caps" query parameter + bool adjust_to_caps = false; + const auto query_params = web::json::value_from_query((req.request_uri().query())); + if (query_params.has_field(nmos::fields::adjust_to_caps)) + { + const auto& query_adjust_to_caps = query_params.at(nmos::fields::adjust_to_caps).as_string(); + if (query_adjust_to_caps == U("true") || query_adjust_to_caps == U("1")) // hmm, should "1" be allowed instead of "true" ? + { + adjust_to_caps = true; + } + } + + auto lock = model.read_lock(); + auto& resources = model.streamcompatibility_resources; + + const string_t input_id = parameters.at(nmos::patterns::resourceId.name); + + const std::pair id_type{ input_id, nmos::types::input }; + + auto resource = find_resource(resources, id_type); + if (resources.end() != resource) + { + auto& endpoint_base_edid = nmos::fields::endpoint_base_edid(resource->data); + + if (!endpoint_base_edid.is_null()) + { + if (!nmos::fields::temporarily_locked(endpoint_base_edid)) + { + return nmos::details::extract_istream_vector(req, gate).then([&model, req, res, parameters, input_id, adjust_to_caps, validate_base_edid, effective_edid_setter, gate](std::vector body) mutable + { + auto lock = model.write_lock(); + auto& streamcompatibility_resources = model.streamcompatibility_resources; + auto& node_resources = model.node_resources; + + const std::pair id_type{ input_id, nmos::types::input }; + + const utility::string_t base_edid_binary{ body.begin(), body.end() }; + + auto streamcompatibility_resource = find_resource(streamcompatibility_resources, id_type); + if (streamcompatibility_resources.end() == streamcompatibility_resource) + { + // should never happen, just in case + throw std::logic_error("associated IS-11 input not found"); + } + + slog::log(gate, SLOG_FLF) << "PUT Base EDID binary requested for " << id_type << " with binary data size " << base_edid_binary.size(); + + // Notify the application code for the Base EDID modification request + // It throws exception to indicate there are EDID failures (e.g. EDID validation failure) + if (validate_base_edid) + { + const auto result = validate_base_edid(input_id, base_edid_binary); + if (!result.first) + { + slog::log(gate, SLOG_FLF) << "Rejecting PUT Base EDID request for " << id_type << " due to " << result.second; + set_error_reply(res, status_codes::BadRequest, {}, result.second); + return true; + } + } + + // Pre-check for resources existence before Base EDID modified and effective_edid_setter executed + if (effective_edid_setter) + { + if (!details::all_resources_exist(node_resources, nmos::fields::senders(streamcompatibility_resource->data), nmos::types::sender)) + { + throw std::logic_error("associated IS-04 sender not found"); + } + } + + utility::string_t updated_timestamp{ nmos::make_version() }; + + // Update Base EDID in streamcompatibility_resources + modify_resource(streamcompatibility_resources, input_id, [&base_edid_binary, &updated_timestamp, &adjust_to_caps](nmos::resource& input) + { + input.data[nmos::fields::endpoint_base_edid] = make_streamcompatibility_edid_endpoint(base_edid_binary); + input.data[nmos::fields::adjust_to_caps] = value::boolean(adjust_to_caps); + + input.data[nmos::fields::version] = web::json::value::string(updated_timestamp); + }); + + // `If the Base EDID for an Input changes, then all Senders associated with this Input MUST update their versions` + // See https://specs.amwa.tv/is-11/branches/v1.0.x/docs/Behaviour_-_Server_Side.html#base-edid + const auto& sender_ids = nmos::fields::senders(streamcompatibility_resource->data); + update_version(node_resources, + boost::copy_range>(sender_ids | boost::adaptors::transformed([&node_resources](const web::json::value& sender_id) {return sender_id.as_string(); })), + updated_timestamp); + + // `When properties of any Input/Output are changed, then the version attribute of the relevant IS-04 Device MUST be incremented. + // Inputs/Outputs identify the corresponding Devices via the device_id property.` + // See https://specs.amwa.tv/is-11/branches/v1.0.x/docs/Interoperability.html#version-increments + const auto device_ids = boost::copy_range>(sender_ids | boost::adaptors::transformed([&node_resources](const web::json::value& sender_id) + { + auto sender = find_resource(node_resources, { sender_id.as_string(), nmos::types::sender }); + return (node_resources.end() != sender && sender->has_data()) ? nmos::fields::device_id(sender->data) : nmos::id{}; + })); + update_version(node_resources, device_ids, updated_timestamp); + + // Update Input effective EDID, which is provided by the application code + if (effective_edid_setter) + { + details::update_effective_edid(node_resources, streamcompatibility_resources, effective_edid_setter, input_id); + } + + model.notify(); + + set_reply(res, status_codes::NoContent); + + return true; + }); + } + else + { + slog::log(gate, SLOG_FLF) << "Rejecting PUT Base EDID request for " << id_type << " due to operation is locked"; + set_error_reply(res, status_codes::Locked); + } + } + else + { + slog::log(gate, SLOG_FLF) << "Rejecting PUT Base EDID request for " << id_type << " due to this input not configured to allow it"; + set_error_reply(res, status_codes::MethodNotAllowed); + } + } + else + { + set_reply(res, status_codes::NotFound); + } + + return pplx::task_from_result(true); + }); + + streamcompatibility_api.support(U("/") + nmos::patterns::inputType.pattern + U("/") + nmos::patterns::resourceId.pattern + U("/edid/base/?"), methods::DEL, [&model, validate_base_edid, effective_edid_setter, &gate_](http_request req, http_response res, const string_t&, const route_parameters& parameters) + { + nmos::api_gate gate(gate_, req, parameters); + auto lock = model.write_lock(); + auto& streamcompatibility_resources = model.streamcompatibility_resources; + auto& node_resources = model.node_resources; + + const string_t resourceId = parameters.at(nmos::patterns::resourceId.name); + + const std::pair id_type{ resourceId, nmos::types::input }; + auto streamcompatibility_resource = find_resource(streamcompatibility_resources, id_type); + if (streamcompatibility_resources.end() != streamcompatibility_resource) + { + auto& endpoint_base_edid = nmos::fields::endpoint_base_edid(streamcompatibility_resource->data); + + if (!endpoint_base_edid.is_null()) + { + if (!nmos::fields::temporarily_locked(endpoint_base_edid)) + { + slog::log(gate, SLOG_FLF) << "DELETE Base EDID binary requested for " << id_type; + + // Notify the application code for the Base EDID deletion request + if (validate_base_edid) + { + const auto result = validate_base_edid(resourceId, {}); + if (!result.first) + { + slog::log(gate, SLOG_FLF) << "Rejecting DELETE Base EDID request for " << id_type << " due to " << result.second; + set_error_reply(res, status_codes::BadRequest, {}, result.second); + + return pplx::task_from_result(true); + } + } + + // Pre-check for resources existence before Base EDID modified and effective_edid_setter executed + if (effective_edid_setter) + { + if (!details::all_resources_exist(model.node_resources, nmos::fields::senders(streamcompatibility_resource->data), nmos::types::sender)) + { + throw std::logic_error("associated IS-04 sender not found"); + } + } + + utility::string_t updated_timestamp{ nmos::make_version() }; + + modify_resource(streamcompatibility_resources, resourceId, [&updated_timestamp](nmos::resource& input) + { + input.data[nmos::fields::endpoint_base_edid] = make_streamcompatibility_edid_endpoint(); + + input.data[nmos::fields::version] = web::json::value::string(updated_timestamp); + }); + + // `If the Base EDID for an Input changes, then all Senders associated with this Input MUST update their versions` + // See https://specs.amwa.tv/is-11/branches/v1.0.x/docs/Behaviour_-_Server_Side.html#base-edid + const auto& sender_ids = nmos::fields::senders(streamcompatibility_resource->data); + update_version(node_resources, + boost::copy_range>(sender_ids | boost::adaptors::transformed([&node_resources](const web::json::value& sender_id) {return sender_id.as_string(); })), + updated_timestamp); + + // `When properties of any Input/Output are changed, then the version attribute of the relevant IS-04 Device MUST be incremented. + // Inputs/Outputs identify the corresponding Devices via the device_id property.` + // See https://specs.amwa.tv/is-11/branches/v1.0.x/docs/Interoperability.html#version-increments + const auto device_ids = boost::copy_range>(sender_ids | boost::adaptors::transformed([&node_resources](const web::json::value& sender_id) + { + auto sender = find_resource(node_resources, { sender_id.as_string(), nmos::types::sender }); + return (node_resources.end() != sender && sender->has_data()) ? nmos::fields::device_id(sender->data) : nmos::id{}; + })); + update_version(node_resources, device_ids, updated_timestamp); + + // Update Input effective EDID, which is provided by the application code + if (effective_edid_setter) + { + details::update_effective_edid(node_resources, streamcompatibility_resources, effective_edid_setter, resourceId); + } + + model.notify(); + + set_reply(res, status_codes::NoContent); + } + else + { + slog::log(gate, SLOG_FLF) << "Rejecting DELETE Base EDID request for " << id_type << " due to operation is locked"; + set_error_reply(res, status_codes::Locked); + } + } + else + { + slog::log(gate, SLOG_FLF) << "Rejecting DELETE Base EDID request for " << id_type << " due to this input not configured to allow it"; + set_error_reply(res, status_codes::MethodNotAllowed); + } + } + else + { + set_reply(res, status_codes::NotFound); + } + + return pplx::task_from_result(true); + }); + + streamcompatibility_api.support(U("/") + nmos::patterns::senderType.pattern + U("/") + nmos::patterns::resourceId.pattern + U("/constraints/active/?"), methods::PUT, [&model, validator, validate_active_constraints, effective_edid_setter, &gate_](http_request req, http_response res, const string_t&, const route_parameters& parameters) + { + nmos::api_gate gate(gate_, req, parameters); + return nmos::details::extract_json(req, gate).then([&model, req, res, parameters, &validator, validate_active_constraints, effective_edid_setter, gate](value data) mutable + { + const nmos::api_version version = nmos::parse_api_version(parameters.at(nmos::patterns::version.name)); + + validator.validate(data, experimental::make_streamcompatibilityapi_senders_active_constraints_put_request_uri(version)); + + auto lock = model.write_lock(); + auto& resources = model.streamcompatibility_resources; + + const string_t resourceId = parameters.at(nmos::patterns::resourceId.name); + + const std::pair id_type{ resourceId, nmos::types::sender }; + auto streamcompatibility_sender = find_resource(resources, id_type); + if (resources.end() != streamcompatibility_sender) + { + auto& endpoint_active_constraints = nmos::fields::endpoint_active_constraints(streamcompatibility_sender->data); + + if (!nmos::fields::temporarily_locked(endpoint_active_constraints)) + { + const auto supported_param_constraints = boost::copy_range>(nmos::fields::parameter_constraints(nmos::fields::supported_param_constraints(streamcompatibility_sender->data)) | boost::adaptors::transformed([](const web::json::value& param_constraint) + { + return std::unordered_set::value_type{ param_constraint.as_string() }; + })); + + // validate the requested constraint sets against the supported constraint sets + if (details::validate_constraint_sets(nmos::fields::constraint_sets(data).as_array(), supported_param_constraints)) + { + slog::log(gate, SLOG_FLF) << "PUT " << id_type << " Active Constraints requested: " << nmos::fields::constraint_sets(data).serialize(); + + bool can_adhere = true; + web::json::value intersection = web::json::value::array(); + + // Notify the application code for the Sender Active Constraints modification request, it returns the intersection of the capabilities constraints + if (validate_active_constraints) + { + const auto result = validate_active_constraints(*streamcompatibility_sender, data, intersection); + if (!result.first) + { + can_adhere = false; + + slog::log(gate, SLOG_FLF) << "Rejecting PUT Active Constraints request for " << id_type << " due to sender can't adhere to these Constraints: " << nmos::fields::constraint_sets(data).serialize(); + set_error_reply(res, status_codes::UnprocessableEntity, {}, result.second); + } + } + + if (can_adhere) + { + details::set_active_constraints(model.node_resources, model.streamcompatibility_resources, resourceId, nmos::fields::constraint_sets(data), intersection, effective_edid_setter); + + model.notify(); + + set_reply(res, status_codes::OK, data); + } + } + else + { + slog::log(gate, SLOG_FLF) << "Rejecting PUT Active Constraints request for " << id_type << " due to unsupported Parameter Constraint URN used in any of the Constraints: " << nmos::fields::constraint_sets(data).serialize(); + set_error_reply(res, status_codes::BadRequest, U("The requested constraint set includes parameter constraints that this sender does not support")); + } + } + else + { + slog::log(gate, SLOG_FLF) << "Rejecting PUT Active Constraints request for " << id_type << " due to operation is locked"; + set_error_reply(res, status_codes::Locked); + } + } + else + { + set_reply(res, status_codes::NotFound); + } + + return true; + }); + }); + + streamcompatibility_api.support(U("/") + nmos::patterns::senderType.pattern + U("/") + nmos::patterns::resourceId.pattern + U("/constraints/active/?"), methods::DEL, [&model, effective_edid_setter, validate_active_constraints, &gate_](http_request req, http_response res, const string_t&, const route_parameters& parameters) + { + nmos::api_gate gate(gate_, req, parameters); + auto lock = model.write_lock(); + auto& resources = model.streamcompatibility_resources; + + const string_t resourceId = parameters.at(nmos::patterns::resourceId.name); + + const std::pair id_type{ resourceId, nmos::types::sender }; + const auto streamcompatibility_sender = find_resource(resources, id_type); + + if (resources.end() != streamcompatibility_sender) + { + const auto& endpoint_active_constraints = nmos::fields::endpoint_active_constraints(streamcompatibility_sender->data); + + if (!nmos::fields::temporarily_locked(endpoint_active_constraints)) + { + slog::log(gate, SLOG_FLF) << "DELETE " << id_type << " Active Constraints requested"; + + // create an empty constraint_sets + const auto empty_active_constraints = web::json::value_of({ + { nmos::fields::constraint_sets, web::json::value::array() } + }); + web::json::value dummy_intersection = web::json::value::array(); + + // Notify the application code for the Sender Active Constraints deletion request, it returns the intersection of the capabilities constraints is ignored + if (validate_active_constraints) + { + const auto result = validate_active_constraints(*streamcompatibility_sender, empty_active_constraints, dummy_intersection); + if (!result.first) + { + slog::log(gate, SLOG_FLF) << "Rejecting DELETE Active Constraints request for " << id_type << " due to " << result.second; + throw std::runtime_error(utility::us2s(result.second)); + } + } + details::set_active_constraints(model.node_resources, model.streamcompatibility_resources, resourceId, nmos::fields::constraint_sets(empty_active_constraints), {}, effective_edid_setter); + + model.notify(); + + set_reply(res, status_codes::OK, nmos::fields::active_constraint_sets(nmos::fields::endpoint_active_constraints(streamcompatibility_sender->data))); + } + else + { + slog::log(gate, SLOG_FLF) << "Rejecting DELETE Active Constraints request for " << id_type << " due to operation is locked"; + set_error_reply(res, status_codes::Locked); + } + } + else + { + set_reply(res, status_codes::NotFound); + } + + return pplx::task_from_result(true); + }); + + return streamcompatibility_api; + } + } +} diff --git a/Development/nmos/streamcompatibility_api.h b/Development/nmos/streamcompatibility_api.h new file mode 100644 index 000000000..1a7e57dae --- /dev/null +++ b/Development/nmos/streamcompatibility_api.h @@ -0,0 +1,43 @@ +#ifndef NMOS_STREAMCOMPATIBILITY_API_H +#define NMOS_STREAMCOMPATIBILITY_API_H + +#include +#include "bst/optional.h" +#include "nmos/api_utils.h" +#include "nmos/slog.h" + +// Stream Compatibility Management API implementation +// See https://specs.amwa.tv/is-11/branches/v1.0.x/APIs/StreamCompatibilityManagementAPI.html +namespace nmos +{ + struct node_model; + struct resource; + + namespace experimental + { + namespace details + { + // a streamcompatibility_base_edid_handler is a notification that the Base EDID for the specified IS-11 input has received the modification request (PUT or DELETEd) + // it can be used to perform any final validation of the specified Base EDID + // the validation result is returned along with the error string + // this callback should not throw exceptions + typedef std::function(const nmos::id& input_id, const bst::optional& base_edid)> streamcompatibility_base_edid_handler; + + // a streamcompatibility_active_constraints_handler is a notification that the Active Constraints for the specified IS-11 sender has received the modification request (PUT or DELETEd) + // it can be used to perform any final validation of the specified Active Constraints + // the validation result is returned along with the error string + // this callback should not throw exceptions + typedef std::function(const nmos::resource& streamcompatibility_sender, const web::json::value& constraint_sets, web::json::value& intersection)> streamcompatibility_active_constraints_handler; + + // a streamcompatibility_effective_edid_setter updates the specified Effective EDID for the specified IS-11 input + // effective EDID of the input is updated when either Active Constraints of a Sender associated with this input are updated + // or base EDID of the input is updated or deleted + // this callback should not throw exceptions, as it's called after the mentioned changes which will not be rolled back + typedef std::function& effective_edid)> streamcompatibility_effective_edid_setter; + } + + web::http::experimental::listener::api_router make_streamcompatibility_api(nmos::node_model& model, details::streamcompatibility_base_edid_handler validate_base_edid, details::streamcompatibility_effective_edid_setter effective_edid_setter, details::streamcompatibility_active_constraints_handler active_constraints_handler, web::http::experimental::listener::route_handler validate_authorization, slog::base_gate& gate); + } +} + +#endif diff --git a/Development/nmos/streamcompatibility_behaviour.cpp b/Development/nmos/streamcompatibility_behaviour.cpp new file mode 100644 index 000000000..c3093dd6e --- /dev/null +++ b/Development/nmos/streamcompatibility_behaviour.cpp @@ -0,0 +1,281 @@ +#include "nmos/streamcompatibility_behaviour.h" + +#include +#include +#include +#include "nmos/activation_mode.h" +#include "nmos/activation_utils.h" +#include "nmos/capabilities.h" // for constraint_sets +#include "nmos/connection_api.h" // for get_transport_type_data +#include "nmos/constraints.h" +#include "nmos/id.h" +#include "nmos/media_type.h" +#include "nmos/model.h" +#include "nmos/resources.h" +#include "nmos/sdp_utils.h" +#include "nmos/slog.h" +#include "nmos/streamcompatibility_state.h" +#include "nmos/streamcompatibility_utils.h" +#include "sdp/sdp.h" + +namespace nmos +{ + namespace experimental + { + namespace details + { + std::vector get_resources_ids(const nmos::resources& resources, const nmos::type& type) + { + return boost::copy_range>(resources | boost::adaptors::filtered([&type](const nmos::resource& resource) + { + return type == resource.type; + }) | boost::adaptors::transformed([](const nmos::resource& resource) + { + return std::vector::value_type{ resource.id }; + })); + } + + nmos::sender_state update_sender_status(nmos::resources& node_resources, nmos::resources& connection_resources, nmos::resources& streamcompatibility_resources, + const nmos::resource& source, const nmos::resource& flow, const nmos::resource& sender, const nmos::resource& connection_sender, const nmos::resource& streamcompatibility_sender, + const details::streamcompatibility_sender_validator& validate_sender_resources, slog::base_gate& gate) + { + nmos::sender_state sender_state(nmos::fields::state(nmos::fields::status(streamcompatibility_sender.data))); + utility::string_t sender_state_debug; + + const std::pair sender_id_type{ streamcompatibility_sender.id, nmos::types::sender }; + + slog::log(gate, SLOG_FLF) << "IS-11 " << sender_id_type << " status before active constraints validation: " << sender_state.name; + + // Setting the State to any value except for "no_essence" or "awaiting_essence" triggers Active Constraints validation + if (sender_state != nmos::sender_states::no_essence && sender_state != nmos::sender_states::awaiting_essence) + { + const auto& constraint_sets = nmos::fields::constraint_sets(nmos::fields::active_constraint_sets(nmos::fields::endpoint_active_constraints(streamcompatibility_sender.data))).as_array(); + + slog::log(gate, SLOG_FLF) << "Validating " << sender_id_type << " active constraints against with its sender, flow, source and transport file"; + if (validate_sender_resources) + { + // do custom sender_source_flow constraints validation to update the sender_state and sender_state_debug + std::tie(sender_state, sender_state_debug) = validate_sender_resources(source, flow, sender, connection_sender, constraint_sets); + } + } + + slog::log(gate, SLOG_FLF) << "IS-11 " << sender_id_type << " status after active constraints validation: " << sender_state.name; + + // update sender state and sender_state_debug if state has changed + if (nmos::fields::state(nmos::fields::status(streamcompatibility_sender.data)) != sender_state.name) + { + set_sender_status(node_resources, streamcompatibility_resources, streamcompatibility_sender.id, sender_state, sender_state_debug, gate); + } + + return sender_state; + } + + nmos::receiver_state update_receiver_status(nmos::resources& node_resources, nmos::resources& streamcompatibility_resources, + const nmos::resource& receiver, const nmos::resource& connection_receiver, const nmos::resource& streamcompatibility_receiver, + const details::streamcompatibility_receiver_validator& validate_receiver, slog::base_gate& gate) + { + nmos::receiver_state receiver_state(nmos::receiver_states::unknown); + utility::string_t receiver_state_debug; + + const std::pair receiver_id_type{ streamcompatibility_receiver.id, nmos::types::receiver }; + + if (nmos::fields::master_enable(nmos::fields::endpoint_active(connection_receiver.data))) + { + const auto& transport_file = nmos::fields::transport_file(nmos::fields::endpoint_active(connection_receiver.data)); + + slog::log(gate, SLOG_FLF) << "Validating " << receiver_id_type << " against with its transport file"; + if (validate_receiver) + { + // do custom receiver constraints validation to update the receiver_state and receiver_state_debug + std::tie(receiver_state, receiver_state_debug) = validate_receiver(receiver, transport_file); + } + } + + if (nmos::fields::state(nmos::fields::status(streamcompatibility_receiver.data)) != receiver_state.name) + { + set_receiver_status(node_resources, streamcompatibility_resources, streamcompatibility_receiver.id, receiver_state, receiver_state_debug, gate); + } + + return receiver_state; + } + } + + void streamcompatibility_behaviour_thread(nmos::node_model& model, details::streamcompatibility_sender_validator validate_sender_resources, details::streamcompatibility_receiver_validator validate_receiver, slog::base_gate& gate) + { + using web::json::value; + + auto lock = model.write_lock(); // in order to update state of Sender/Receiver + auto& node_resources = model.node_resources; + auto& connection_resources = model.connection_resources; + auto& streamcompatibility_resources = model.streamcompatibility_resources; + + auto most_recent_update = nmos::tai_min(); + + for (;;) + { + model.wait(lock, [&] { return model.shutdown || most_recent_update < nmos::most_recent_update(node_resources); }); + if (model.shutdown) break; + + // find Senders with recently updated IS-11 properties, associated Flow or Source + auto streamcompatibility_senders_ids = details::get_resources_ids(streamcompatibility_resources, nmos::types::sender); + for (const nmos::id& sender_id : streamcompatibility_senders_ids) + { + const std::pair sender_id_type{ sender_id, nmos::types::sender }; + + try + { + auto sender = find_resource(node_resources, sender_id_type); + if (node_resources.end() == sender) throw std::logic_error("matching IS-04 sender not found"); + + const std::pair flow_id_type{ nmos::fields::flow_id(sender->data).as_string(), nmos::types::flow }; + auto flow = find_resource(node_resources, flow_id_type); + if (node_resources.end() == flow) throw std::logic_error("matching IS-04 flow not found"); + + const std::pair source_id_type{ nmos::fields::source_id(flow->data), nmos::types::source }; + auto source = find_resource(node_resources, source_id_type); + if (node_resources.end() == source) throw std::logic_error("matching IS-04 source not found"); + + // check any updated on IS-04 source/flow/sender which is related to the IS-11 sender + const auto updated = most_recent_update < sender->updated || + most_recent_update < flow->updated || + most_recent_update < source->updated; + + if (updated) + { + slog::log(gate, SLOG_FLF) << "The " << sender_id_type << ", its Flow, or its Source has been updated recently, and now's the time to update its IS-11 status"; + + auto connection_sender = find_resource(connection_resources, sender_id_type); + if (connection_resources.end() == connection_sender) throw std::logic_error("matching IS-05 sender not found"); + + auto streamcompatibility_sender = find_resource(streamcompatibility_resources, sender_id_type); + if (streamcompatibility_resources.end() == streamcompatibility_sender) throw std::logic_error("matching IS-11 sender not found"); + + // update sender status against sender constraints + const auto sender_state = details::update_sender_status(node_resources, connection_resources, streamcompatibility_resources, *source, *flow, *sender, *connection_sender, *streamcompatibility_sender, validate_sender_resources, gate); + + // `At any time if State of an active Sender becomes active_constraints_violation, the Sender MUST become inactive. An inactive Sender in this state MUST NOT allow activations.` + // See https://specs.amwa.tv/is-11/branches/v1.0.x/docs/Behaviour_-_Server_Side.html#preventing-restrictions-violation + if (nmos::sender_states::active_constraints_violation == sender_state) + { + // deactivate sender iff it is scheduled or it is currently active + + const auto& staged_activation = nmos::fields::activation(nmos::fields::endpoint_staged(connection_sender->data)); + const auto staged_state = nmos::details::get_activation_state(staged_activation); + const auto& active = nmos::fields::endpoint_active(connection_sender->data); + if (nmos::fields::master_enable(active) || nmos::details::scheduled_activation_pending == staged_state) + { + // modify connection /staged endpoint to deactivated sender immediately + + slog::log(gate, SLOG_FLF) << sender_state.name << " on IS-11 " << sender_id_type << " detected; deactivate sender"; + + web::json::value merged; + nmos::modify_resource(connection_resources, sender_id, [&merged](nmos::resource& connection_resource) + { + // rebuild the sender connection /staged endpoint json + merged = nmos::fields::endpoint_staged(connection_resource.data); + // set master_enable (false), and activation (activate_immediate) + merged[nmos::fields::master_enable] = value::boolean(false); + auto activation = nmos::make_activation(); + activation[nmos::fields::mode] = value::string(nmos::activation_modes::activate_immediate.name); + nmos::details::merge_activation(merged[nmos::fields::activation], activation, nmos::tai_now()); + connection_resource.data[nmos::fields::endpoint_staged] = merged; // modify connection /staged endpoint to deactivated sender immediately + connection_resource.data[nmos::fields::version] = web::json::value::string(nmos::make_version()); + }); + + // notifying connection activation thread to deactivate sender immediately + model.notify(); + + nmos::details::handle_immediate_activation_pending(model, lock, sender_id_type, merged[nmos::fields::activation], gate); + } + else + { + slog::log(gate, SLOG_FLF) << sender_state.name << " on IS-11 " << sender_id_type << " detected, but no deactivation required; as it has already been deactivated"; + } + } + } + } + catch (const std::exception& e) + { + slog::log(gate, SLOG_FLF) << "Failed to update IS-11 " << sender_id_type << " status, unexpected exception: " << e.what(); + continue; + } + } + + // find IS-11 Receivers with recently updated "caps" to check whether the active transport file still satisfies them + auto streamcompatibility_receivers_ids = details::get_resources_ids(streamcompatibility_resources, nmos::types::receiver); + for (const nmos::id& receiver_id : streamcompatibility_receivers_ids) + { + const std::pair receiver_id_type{ receiver_id, nmos::types::receiver }; + + try + { + auto receiver = find_resource(node_resources, receiver_id_type); + if (node_resources.end() == receiver) throw std::logic_error("matching IS-04 receiver not found"); + + const auto updated = most_recent_update < receiver->updated; + if (updated) + { + slog::log(gate, SLOG_FLF) << "The " << receiver_id_type << " has been updated recently, and now's the time to update its IS-11 status"; + + auto connection_receiver = find_resource(connection_resources, receiver_id_type); + if (connection_resources.end() == connection_receiver) throw std::logic_error("matching IS-05 receiver not found"); + + auto streamcompatibility_receiver = find_resource(streamcompatibility_resources, receiver_id_type); + if (streamcompatibility_resources.end() == streamcompatibility_receiver) throw std::logic_error("matching IS-11 receiver not found"); + + // update receiver status against receiver constraints + const auto receiver_state = details::update_receiver_status(node_resources, streamcompatibility_resources, *receiver, *connection_receiver, *streamcompatibility_receiver, validate_receiver, gate); + + // `At any time if State of an active Receiver becomes non_compliant_stream, the Receiver SHOULD become inactive. An inactive Receiver in this state SHOULD NOT allow activations.` + // See https://specs.amwa.tv/is-11/branches/v1.0.x/docs/Behaviour_-_Server_Side.html#preventing-restrictions-violation + if (nmos::receiver_states::non_compliant_stream == receiver_state) + { + // deactivate receiver iff it is scheduled or it is currently active + + const auto& staged_activation = nmos::fields::activation(nmos::fields::endpoint_staged(connection_receiver->data)); + const auto staged_state = nmos::details::get_activation_state(staged_activation); + const auto& active = nmos::fields::endpoint_active(connection_receiver->data); + if (nmos::fields::master_enable(active) || nmos::details::scheduled_activation_pending == staged_state) + { + // modify connection /staged endpoint to deactivated receiver immediately + + slog::log(gate, SLOG_FLF) << receiver_state.name << " on IS-11 " << receiver_id_type << " detected; deactivate receiver"; + + web::json::value merged; + nmos::modify_resource(connection_resources, receiver_id, [&merged](nmos::resource& connection_resource) + { + // rebuild the receiver connection /staged endpoint json + merged = nmos::fields::endpoint_staged(connection_resource.data); + // set master_enable (false), and activation (activate_immediate) + merged[nmos::fields::master_enable] = value::boolean(false); + auto activation = nmos::make_activation(); + activation[nmos::fields::mode] = value::string(nmos::activation_modes::activate_immediate.name); + nmos::details::merge_activation(merged[nmos::fields::activation], activation, nmos::tai_now()); + connection_resource.data[nmos::fields::endpoint_staged] = merged; // modify connection /staged endpoint to deactivated receiver immediately + connection_resource.data[nmos::fields::version] = web::json::value::string(nmos::make_version()); + }); + + // notifying connection activation thread to deactivate receiver immediately + model.notify(); + + nmos::details::handle_immediate_activation_pending(model, lock, receiver_id_type, merged[nmos::fields::activation], gate); + } + else + { + slog::log(gate, SLOG_FLF) << receiver_state.name << " on IS-11 " << receiver_id_type << " detected, but no deactivation required; as it has already been deactivated"; + } + } + } + } + catch (const std::exception& e) + { + slog::log(gate, SLOG_FLF) << "Failed to update IS-11 " << receiver_id_type << " status, unexpected exception: " << e.what(); + continue; + } + } + + most_recent_update = nmos::most_recent_update(node_resources); + } + } + } +} diff --git a/Development/nmos/streamcompatibility_behaviour.h b/Development/nmos/streamcompatibility_behaviour.h new file mode 100644 index 000000000..41da79d3d --- /dev/null +++ b/Development/nmos/streamcompatibility_behaviour.h @@ -0,0 +1,34 @@ +#ifndef NMOS_STREAMCOMPATIBILITY_BEHAVIOUR_H +#define NMOS_STREAMCOMPATIBILITY_BEHAVIOUR_H + +#include + +#include "nmos/streamcompatibility_state.h" +#include "nmos/streamcompatibility_validation.h" + +namespace slog +{ + class base_gate; +} + +namespace web +{ + namespace json + { + class value; + class array; + } +} + +namespace nmos +{ + struct node_model; + + namespace experimental + { + std::pair validate_receiver_resources(const web::json::value& transport_file, const web::json::value& receiver); + void streamcompatibility_behaviour_thread(nmos::node_model& model, details::streamcompatibility_sender_validator validate_sender_resources, details::streamcompatibility_receiver_validator validate_receiver, slog::base_gate& gate); + } +} + +#endif diff --git a/Development/nmos/streamcompatibility_resource.cpp b/Development/nmos/streamcompatibility_resource.cpp new file mode 100644 index 000000000..15984f35b --- /dev/null +++ b/Development/nmos/streamcompatibility_resource.cpp @@ -0,0 +1,75 @@ +#include "nmos/streamcompatibility_resource.h" + +#include +#include "nmos/capabilities.h" // for nmos::fields::constraint_sets +#include "nmos/resource.h" +#include "nmos/json_fields.h" + +namespace nmos +{ + namespace experimental + { + namespace details + { + web::json::value make_streamcompatibility_edid_endpoint(const web::uri& edid_uri, const utility::string_t& edid_binary, bool locked) + { + using web::json::value; + + value data; + if (!edid_uri.is_empty()) + { + data[nmos::fields::edid_href] = value::string(edid_uri.to_string()); + } + if (!edid_binary.empty()) + { + data[nmos::fields::edid_binary] = value::string(edid_binary); + } + data[nmos::fields::temporarily_locked] = value::boolean(locked); + + return data; + } + } + + web::json::value make_streamcompatibility_active_constraints_endpoint(const web::json::value& constraint_sets, bool locked) + { + using web::json::value_of; + + auto active_constraint_sets = value_of({ + { nmos::fields::constraint_sets, constraint_sets } + }); + + return value_of({ + { nmos::fields::active_constraint_sets, active_constraint_sets }, + { nmos::fields::temporarily_locked, locked } + }); + } + + web::json::value make_streamcompatibility_edid_endpoint(bool locked) + { + return details::make_streamcompatibility_edid_endpoint({}, {}, locked); + } + + web::json::value make_streamcompatibility_edid_endpoint(const web::uri& edid_uri, bool locked) + { + return details::make_streamcompatibility_edid_endpoint(edid_uri, {}, locked); + } + + web::json::value make_streamcompatibility_edid_endpoint(const utility::string_t& edid_binary, bool locked) + { + return details::make_streamcompatibility_edid_endpoint({}, edid_binary, locked); + } + + web::json::value make_streamcompatibility_input_output_base(const nmos::id& id, const nmos::id& device_id, bool connected, bool edid_support, const nmos::settings& settings) + { + using web::json::value; + + auto data = nmos::details::make_resource_core(id, settings); + + data[nmos::fields::connected] = value::boolean(connected); + data[nmos::fields::device_id] = value::string(device_id); + data[nmos::fields::edid_support] = value::boolean(edid_support); + + return data; + } + } +} diff --git a/Development/nmos/streamcompatibility_resource.h b/Development/nmos/streamcompatibility_resource.h new file mode 100644 index 000000000..a2f5715f4 --- /dev/null +++ b/Development/nmos/streamcompatibility_resource.h @@ -0,0 +1,40 @@ +#ifndef NMOS_STREAMCOMPATIBILITY_RESOURCE_H +#define NMOS_STREAMCOMPATIBILITY_RESOURCE_H + +#include +#include "bst/optional.h" +#include "cpprest/base_uri.h" +#include "nmos/id.h" +#include "nmos/settings.h" + +namespace nmos +{ + namespace experimental + { + web::json::value make_streamcompatibility_active_constraints_endpoint(const web::json::value& constraint_sets, bool locked = false); + + // Makes an EDID endpoint to show that an input supports EDID of this type but it currently has no value + web::json::value make_streamcompatibility_edid_endpoint(bool locked = false); + // Makes an EDID endpoint to show that an input supports EDID using EDID reference + web::json::value make_streamcompatibility_edid_endpoint(const web::uri& edid_uri, bool locked = false); + // Makes an EDID endpoint to show that an input supports EDID using EDID binary + web::json::value make_streamcompatibility_edid_endpoint(const utility::string_t& edid_binary, bool locked = false); + + web::json::value make_streamcompatibility_input_output_base(const nmos::id& id, const nmos::id& device_id, bool connected, bool edid_support, const nmos::settings& settings); + + struct edid_file_visitor : public boost::static_visitor + { + web::json::value operator()(const utility::string_t& edid_file_binary) const + { + return make_streamcompatibility_edid_endpoint(edid_file_binary); + } + + web::json::value operator()(const web::uri& edid_file_uri) const + { + return make_streamcompatibility_edid_endpoint(edid_file_uri); + } + }; + } +} + +#endif diff --git a/Development/nmos/streamcompatibility_resources.cpp b/Development/nmos/streamcompatibility_resources.cpp new file mode 100644 index 000000000..b9519ad51 --- /dev/null +++ b/Development/nmos/streamcompatibility_resources.cpp @@ -0,0 +1,120 @@ +#include "nmos/streamcompatibility_resources.h" + +#include "nmos/capabilities.h" // for nmos::fields::constraint_sets +#include "nmos/is11_versions.h" +#include "nmos/resource.h" +#include "nmos/streamcompatibility_resource.h" +#include "nmos/streamcompatibility_state.h" + +namespace nmos +{ + namespace experimental + { + nmos::resource make_streamcompatibility_sender(const nmos::id& id, const std::vector& inputs, const std::vector& param_constraints) + { + using web::json::value; + using web::json::value_of; + using web::json::value_from_elements; + + auto supported_param_constraints = value_of({ + { nmos::fields::parameter_constraints, value_from_elements(param_constraints) }, + }); + + auto data = value_of({ + { nmos::fields::id, id }, + { nmos::fields::device_id, U("these are not the droids you are looking for") }, // hmm, the device_id key is used to satisfy when inserting this resource into the model while using the nmos::insert_resource() + { nmos::fields::endpoint_active_constraints, make_streamcompatibility_active_constraints_endpoint(value::array()) }, + { nmos::fields::inputs, value_from_elements(inputs) }, + { nmos::fields::supported_param_constraints, supported_param_constraints }, + { nmos::fields::status, value_of({ { nmos::fields::state, nmos::sender_states::unconstrained.name } }) }, + { nmos::fields::intersection_of_caps_and_constraints, value::array() } + }); + + return{ is11_versions::v1_0, types::sender, std::move(data), id, true }; + } + + nmos::resource make_streamcompatibility_receiver(const nmos::id& id, const std::vector& outputs) + { + using web::json::value_of; + using web::json::value_from_elements; + + auto data = value_of({ + { nmos::fields::id, id }, + { nmos::fields::device_id, U("these are not the droids you are looking for") }, // hmm, the device_id key is used to satisfy when inserting this resource into the model while using the nmos::insert_resource() + { nmos::fields::outputs, value_from_elements(outputs) }, + { nmos::fields::status, value_of({ { nmos::fields::state, nmos::receiver_states::unknown.name } }) }, + }); + + return{ is11_versions::v1_0, types::receiver, std::move(data), id, true }; + } + + nmos::resource make_streamcompatibility_input(const nmos::id& id, const nmos::id& device_id, bool connected, const std::vector& senders, const nmos::settings& settings) + { + using web::json::value; + using web::json::value_from_elements; + using web::json::value_of; + + auto data = make_streamcompatibility_input_output_base(id, device_id, connected, false, settings); + data[nmos::fields::base_edid_support] = value::boolean(false); + data[nmos::fields::senders] = value_from_elements(senders); + data[nmos::fields::status] = value_of({ { nmos::fields::state, nmos::input_states::signal_present.name } }); + + return{ is11_versions::v1_0, types::input, std::move(data), id, true }; + } + + nmos::resource make_streamcompatibility_input(const nmos::id& id, const nmos::id& device_id, bool connected, bool base_edid_support, const boost::variant& effective_edid, const std::vector& senders, const nmos::settings& settings) + { + using web::json::value; + using web::json::value_from_elements; + using web::json::value_of; + + auto data = make_streamcompatibility_input_output_base(id, device_id, connected, true, settings); + + if (base_edid_support) + { + data[nmos::fields::endpoint_base_edid] = make_streamcompatibility_edid_endpoint(false); + } + + data[nmos::fields::endpoint_effective_edid] = boost::apply_visitor(edid_file_visitor(), effective_edid); + + data[nmos::fields::base_edid_support] = value::boolean(base_edid_support); + if (base_edid_support) + { + data[nmos::fields::adjust_to_caps] = value::boolean(false); + } + data[nmos::fields::senders] = value_from_elements(senders); + data[nmos::fields::status] = value_of({ { nmos::fields::state, nmos::input_states::signal_present.name } }); + + return{ is11_versions::v1_0, types::input, std::move(data), id, true }; + } + + nmos::resource make_streamcompatibility_output(const nmos::id& id, const nmos::id& device_id, bool connected, const std::vector& receivers, const nmos::settings& settings) + { + using web::json::value_from_elements; + using web::json::value_of; + + auto data = make_streamcompatibility_input_output_base(id, device_id, connected, false, settings); + data[nmos::fields::receivers] = value_from_elements(receivers); + data[nmos::fields::status] = value_of({ { nmos::fields::state, nmos::output_states::signal_present.name } }); + + return{ is11_versions::v1_0, types::output, std::move(data), id, true }; + } + + nmos::resource make_streamcompatibility_output(const nmos::id& id, const nmos::id& device_id, bool connected, const bst::optional>& edid, const std::vector& receivers, const nmos::settings& settings) + { + using web::json::value_from_elements; + using web::json::value_of; + + auto data = make_streamcompatibility_input_output_base(id, device_id, connected, true, settings); + data[nmos::fields::receivers] = value_from_elements(receivers); + data[nmos::fields::status] = value_of({ { nmos::fields::state, nmos::output_states::signal_present.name } }); + + if (edid) + { + data[nmos::fields::endpoint_edid] = boost::apply_visitor(edid_file_visitor(), *edid); + } + + return{ is11_versions::v1_0, types::output, std::move(data), id, true }; + } + } +} diff --git a/Development/nmos/streamcompatibility_resources.h b/Development/nmos/streamcompatibility_resources.h new file mode 100644 index 000000000..13d68523f --- /dev/null +++ b/Development/nmos/streamcompatibility_resources.h @@ -0,0 +1,34 @@ +#ifndef NMOS_STREAMCOMPATIBILITY_RESOURCES_H +#define NMOS_STREAMCOMPATIBILITY_RESOURCES_H + +#include +#include +#include "bst/optional.h" +#include "cpprest/base_uri.h" +#include "nmos/id.h" +#include "nmos/settings.h" + +namespace nmos +{ + struct resource; + + namespace experimental + { + nmos::resource make_streamcompatibility_sender(const nmos::id& id, const std::vector& inputs, const std::vector& param_constraints); + nmos::resource make_streamcompatibility_receiver(const nmos::id& id, const std::vector& outputs); + + // See https://specs.amwa.tv/is-11/branches/v1.0.x/APIs/schemas/with-refs/input.html + // Makes an input without EDID support + nmos::resource make_streamcompatibility_input(const nmos::id& id, const nmos::id& device_id, bool connected, const std::vector& senders, const nmos::settings& settings); + // Makes an input with EDID support + nmos::resource make_streamcompatibility_input(const nmos::id& id, const nmos::id& device_id, bool connected, bool base_edid_support, const boost::variant& effective_edid, const std::vector& senders, const nmos::settings& settings); + + // See https://specs.amwa.tv/is-11/branches/v1.0.x/APIs/schemas/with-refs/output.html + // Makes an output without EDID support + nmos::resource make_streamcompatibility_output(const nmos::id& id, const nmos::id& device_id, bool connected, const std::vector& receivers, const nmos::settings& settings); + // Makes an output with EDID support + nmos::resource make_streamcompatibility_output(const nmos::id& id, const nmos::id& device_id, bool connected, const bst::optional>& edid, const std::vector& receivers, const nmos::settings& settings); + } +} + +#endif diff --git a/Development/nmos/streamcompatibility_state.h b/Development/nmos/streamcompatibility_state.h new file mode 100644 index 000000000..0fde92e52 --- /dev/null +++ b/Development/nmos/streamcompatibility_state.h @@ -0,0 +1,55 @@ +#ifndef NMOS_SENDER_STATE_H +#define NMOS_SENDER_STATE_H + +#include "nmos/string_enum.h" + +namespace nmos +{ + // Stream Compatibility Input states + // See https://specs.amwa.tv/is-11/branches/v1.0.x/docs/Behaviour_-_Server_Side.html#status-of-input + // and https://specs.amwa.tv/is-11/branches/v1.0.x/APIs/schemas/with-refs/input.html + DEFINE_STRING_ENUM(input_state) + namespace input_states + { + const input_state no_signal{ U("no_signal") }; + const input_state awaiting_signal{ U("awaiting_signal") }; + const input_state signal_present{ U("signal_present") }; + } + + // Stream Compatibility Output states + // See https://specs.amwa.tv/is-11/branches/v1.0.x/docs/Behaviour_-_Server_Side.html#status-of-output + // and https://specs.amwa.tv/is-11/branches/v1.0.x/APIs/schemas/with-refs/output.html + DEFINE_STRING_ENUM(output_state) + namespace output_states + { + const output_state no_signal{ U("no_signal") }; + const output_state default_signal{ U("default_signal") }; + const output_state signal_present{ U("signal_present") }; + } + + // Stream Compatibility Sender states + // See https://specs.amwa.tv/is-11/branches/v1.0.x/docs/Behaviour_-_Server_Side.html#status-of-sender + // and https://specs.amwa.tv/is-11/branches/v1.0.x/APIs/schemas/with-refs/sender-status.html + DEFINE_STRING_ENUM(sender_state) + namespace sender_states + { + const sender_state no_essence{ U("no_essence") }; + const sender_state awaiting_essence{ U("awaiting_essence") }; + const sender_state unconstrained{ U("unconstrained") }; + const sender_state constrained{ U("constrained") }; + const sender_state active_constraints_violation{ U("active_constraints_violation") }; + } + + // Stream Compatibility Receiver states + // See https://specs.amwa.tv/is-11/branches/v1.0.x/docs/Behaviour_-_Server_Side.html#status-of-receiver + // and https://specs.amwa.tv/is-11/branches/v1.0.x/APIs/schemas/with-refs/receiver-status.html + DEFINE_STRING_ENUM(receiver_state) + namespace receiver_states + { + const receiver_state unknown{ U("unknown") }; + const receiver_state compliant_stream{ U("compliant_stream") }; + const receiver_state non_compliant_stream{ U("non_compliant_stream") }; + } +} + +#endif diff --git a/Development/nmos/streamcompatibility_utils.cpp b/Development/nmos/streamcompatibility_utils.cpp new file mode 100644 index 000000000..f2408421f --- /dev/null +++ b/Development/nmos/streamcompatibility_utils.cpp @@ -0,0 +1,160 @@ +#include "nmos/streamcompatibility_utils.h" + +#include +#include "cpprest/basic_utils.h" // for utility::us2s + +namespace nmos +{ + namespace experimental + { + void update_version(nmos::resources& resources, const nmos::id& resource_id, const utility::string_t& version) + { + using web::json::value; + + modify_resource(resources, resource_id, [&version](nmos::resource& resource) + { + resource.data[nmos::fields::version] = value::string(version); + }); + } + + void update_version(nmos::resources& resources, const std::set& resource_ids, const utility::string_t& version) + { + for (const auto& resource_id : resource_ids) + { + update_version(resources, resource_id, version); + } + } + + bool set_sender_status(resources& node_resources, resources& streamcompatibility_resources, const nmos::id& sender_id, const nmos::sender_state& state, const utility::string_t& state_debug, slog::base_gate& gate) + { + using web::json::value; + using web::json::value_of; + + const auto version = nmos::make_version(); + try + { + modify_resource(streamcompatibility_resources, sender_id, [&](nmos::resource& resource) + { + resource.data[nmos::fields::status] = value_of({ { nmos::fields::state, state.name } }); + + if (!state_debug.empty()) + { + nmos::fields::status(resource.data)[nmos::fields::debug] = value::string(state_debug); + } + else if (nmos::fields::status(resource.data).has_field(nmos::fields::debug) && !nmos::fields::debug(nmos::fields::status(resource.data)).empty()) + { + nmos::fields::status(resource.data).erase(nmos::fields::debug); + } + + resource.data[nmos::fields::version] = value::string(version); + }); + + // `When State of Sender is changed, then the version attribute of the relevant IS-04 Sender MUST be incremented.` + // See https://specs.amwa.tv/is-11/branches/v1.0.x/docs/Interoperability.html#version-increments + update_version(node_resources, sender_id, version); + + // hmmmm, should the version of the associated Device be incremented? + } + catch (const std::exception& e) + { + slog::log(gate, SLOG_FLF) << "Set sender " << utility::us2s(sender_id) << " status: {state=" << utility::us2s(state.name) << "} error: " << e.what(); + return false; + } + return true; + } + + bool set_receiver_status(resources& node_resources, resources& streamcompatibility_resources, const nmos::id& receiver_id, const nmos::receiver_state& state, const utility::string_t& state_debug, slog::base_gate& gate) + { + using web::json::value; + using web::json::value_of; + + const auto version = nmos::make_version(); + try + { + modify_resource(streamcompatibility_resources, receiver_id, [&](nmos::resource& resource) + { + resource.data[nmos::fields::status] = value_of({ { nmos::fields::state, state.name } }); + + if (!state_debug.empty()) + { + nmos::fields::status(resource.data)[nmos::fields::debug] = value::string(state_debug); + } + else if (nmos::fields::status(resource.data).has_field(nmos::fields::debug) && !nmos::fields::debug(nmos::fields::status(resource.data)).empty()) + { + nmos::fields::status(resource.data).erase(nmos::fields::debug); + } + + resource.data[nmos::fields::version] = value::string(version); + }); + + // `When State of Receiver is changed, then the version attribute of the relevant IS-04 Receiver MUST be incremented.` + // See https://specs.amwa.tv/is-11/branches/v1.0.x/docs/Interoperability.html#version-increments + update_version(node_resources, receiver_id, version); + + // hmmmm, should the version of the associated Device be incremented? + } + catch (const std::exception& e) + { + slog::log(gate, SLOG_FLF) << "Set receiver: " << utility::us2s(receiver_id) << " status: {state=" << utility::us2s(state.name) << "} error: " << e.what(); + return false; + } + return true; + } + + bool set_sender_inputs(resources& node_resources, resources& streamcompatibility_resources, const nmos::id& sender_id, const std::vector& input_ids, slog::base_gate& gate) + { + using web::json::value; + using web::json::value_from_elements; + + const auto version = nmos::make_version(); + try + { + modify_resource(streamcompatibility_resources, sender_id, [&](nmos::resource& resource) + { + resource.data[nmos::fields::inputs] = value_from_elements(input_ids); + resource.data[nmos::fields::version] = value::string(version); + }); + + // `When the set of Inputs associated with the Sender is changed, then the version attribute of the relevant IS-04 Sender MUST be incremented.` + // See https://specs.amwa.tv/is-11/branches/v1.0.x/docs/Interoperability.html#version-increments + update_version(node_resources, sender_id, version); + + // hmmmm, should the version of the associated Device be incremented? + } + catch (const std::exception& e) + { + slog::log(gate, SLOG_FLF) << "Set sender: " << utility::us2s(sender_id) << " inputs error: " << e.what(); + return false; + } + return true; + } + + bool set_receiver_outputs(resources& node_resources, resources& streamcompatibility_resources, const nmos::id& receiver_id, const std::vector& output_ids, slog::base_gate& gate) + { + using web::json::value; + using web::json::value_from_elements; + + const auto version = nmos::make_version(); + try + { + modify_resource(streamcompatibility_resources, receiver_id, [&](nmos::resource& resource) + { + resource.data[nmos::fields::outputs] = value_from_elements(output_ids); + resource.data[nmos::fields::version] = value::string(version); + }); + + // `When the set of Outputs associated with the Receiver is changed, then the version attribute of the relevant IS-04 Receiver MUST be incremented.` + // See https://specs.amwa.tv/is-11/branches/v1.0.x/docs/Interoperability.html#version-increments + update_version(node_resources, receiver_id, version); + + // hmmmm, should the version of the associated Device be incremented? + } + catch (const std::exception& e) + { + slog::log(gate, SLOG_FLF) << "Set receiver: " << utility::us2s(receiver_id) << " outputs error: " << e.what(); + return false; + } + return true; + } + } +} diff --git a/Development/nmos/streamcompatibility_utils.h b/Development/nmos/streamcompatibility_utils.h new file mode 100644 index 000000000..da6c6f51e --- /dev/null +++ b/Development/nmos/streamcompatibility_utils.h @@ -0,0 +1,37 @@ +#include "nmos/id.h" +#include "nmos/resources.h" +#include "nmos/streamcompatibility_state.h" + +namespace nmos +{ + namespace experimental + { + // Update version of the given resource Id + void update_version(nmos::resources& resources, const nmos::id& resource_id, const utility::string_t& version); + + // Update version of all the given resource Ids + void update_version(nmos::resources& resources, const std::set& resource_ids, const utility::string_t& version); + + // Set sender status + // `When State of Sender is changed, then the version attribute of the relevant IS-04 Sender MUST be incremented.` + // See https://specs.amwa.tv/is-11/branches/v1.0.x/docs/Interoperability.html#version-increments + bool set_sender_status(resources& node_resources, resources& streamcompatibility_resources, const nmos::id& sender_id, const nmos::sender_state& state, const utility::string_t& state_debug, slog::base_gate& gate); + + // Set receiver status + // `When State of Receiver is changed, then the version attribute of the relevant IS-04 Receiver MUST be incremented.` + // See https://specs.amwa.tv/is-11/branches/v1.0.x/docs/Interoperability.html#version-increments + bool set_receiver_status(resources& node_resources, resources& streamcompatibility_resources, const nmos::id& receiver_id, const nmos::receiver_state& state, const utility::string_t& state_debug, slog::base_gate& gate); + + // Set sender inputs + // `When the set of Inputs associated with the Sender is changed, then the version attribute of the relevant IS-04 Sender MUST be incremented.` + // See https://specs.amwa.tv/is-11/branches/v1.0.x/docs/Interoperability.html#version-increments + bool set_sender_inputs(resources& node_resources, resources& streamcompatibility_resources, const nmos::id& sender_id, const std::vector& input_ids, slog::base_gate& gate); + + // Set receiver outputs + // `When the set of Outputs associated with the Receiver is changed, then the version attribute of the relevant IS-04 Receiver MUST be incremented.` + // See https://specs.amwa.tv/is-11/branches/v1.0.x/docs/Interoperability.html#version-increments + bool set_receiver_outputs(resources& node_resources, resources& streamcompatibility_resources, const nmos::id& receiver_id, const std::vector& output_ids, slog::base_gate& gate); + + // hmm, add other helper functions, such as set_input_properties and set_output_properties in future + } +} diff --git a/Development/nmos/streamcompatibility_validation.cpp b/Development/nmos/streamcompatibility_validation.cpp new file mode 100644 index 000000000..40e2ac265 --- /dev/null +++ b/Development/nmos/streamcompatibility_validation.cpp @@ -0,0 +1,175 @@ +#include "nmos/streamcompatibility_validation.h" + +#include "nmos/connection_api.h" +#include "nmos/json_fields.h" +#include "nmos/model.h" +#include "nmos/resource.h" +#include "nmos/resources.h" +#include "nmos/slog.h" +#include "nmos/streamcompatibility_state.h" +#include "sdp/sdp.h" + +namespace nmos +{ + namespace experimental + { + namespace detail + { + bool match_resource_parameters_constraint_set(const parameter_constraints& constraints, const web::json::value& resource, const web::json::value& constraint_set_) + { + if (!nmos::caps::meta::enabled(constraint_set_)) return false; + + const auto& constraint_set = constraint_set_.as_object(); + return constraint_set.end() == std::find_if(constraint_set.begin(), constraint_set.end(), [&](const std::pair& constraint) + { + const auto found = constraints.find(constraint.first); + return constraints.end() != found && !found->second(resource, constraint.second); + }); + } + } + + void validate_rtp_transport_file(const nmos::resource& receiver, const utility::string_t& transport_file_type, const utility::string_t& transport_file_data) + { + return details::validate_rtp_transport_file(&validate_sdp_parameters, receiver, transport_file_type, transport_file_data); + } + + void details::validate_rtp_transport_file(nmos::details::sdp_parameters_validator validate_sdp_parameters, const nmos::resource& receiver, const utility::string_t& transport_file_type, const utility::string_t& transport_file_data) + { + if (transport_file_type != nmos::media_types::application_sdp.name) + { + throw std::runtime_error("unexpected type: " + utility::us2s(transport_file_type)); + } + + const auto session_description = sdp::parse_session_description(utility::us2s(transport_file_data)); + auto sdp_transport_params = nmos::parse_session_description(session_description); + + // Validate transport file according to the IS-04 receiver, throws an exception when validation fails + + validate_sdp_parameters(receiver.data, sdp_transport_params.first); + } + + // "At any time if State of an active Sender becomes active_constraints_violation, the Sender MUST become inactive. + // An inactive Sender in this state MUST NOT allow activations. + // At any time if State of an active Receiver becomes non_compliant_stream, the Receiver SHOULD become inactive. + // An inactive Receiver in this state SHOULD NOT allow activations." + // See https://specs.amwa.tv/is-11/branches/v1.0.x/docs/Behaviour_-_Server_Side.html#preventing-restrictions-violation + nmos::details::connection_resource_patch_validator make_connection_streamcompatibility_validator(nmos::node_model& model) + { + return [&model] (const nmos::resource& resource, const nmos::resource& connection_resource, const web::json::value& endpoint_staged, slog::base_gate& gate) + { + if (nmos::fields::master_enable(endpoint_staged)) + { + const auto id = nmos::fields::id(resource.data); + const auto type = resource.type; + + auto streamcompatibility_resource = find_resource(model.streamcompatibility_resources, {id, type}); + if (model.streamcompatibility_resources.end() != streamcompatibility_resource) + { + auto resource_state = nmos::fields::state(nmos::fields::status(streamcompatibility_resource->data)); + + if (resource_state == nmos::sender_states::active_constraints_violation.name || resource_state == nmos::receiver_states::non_compliant_stream.name) + { + throw std::logic_error("activation failed due to Status of the connector"); + } + } + } + }; + } + + details::transport_file_constraint_sets_matcher make_streamcompatibility_sdp_constraint_sets_matcher(const details::sdp_constraint_sets_matcher& match_sdp_parameters_constraint_sets) + { + return [match_sdp_parameters_constraint_sets](const std::pair& transport_file, const web::json::array& constraint_sets) -> bool + { + if (nmos::media_types::application_sdp.name != transport_file.first) + { + throw std::runtime_error("unknown transport file type"); + } + const auto session_description = sdp::parse_session_description(utility::us2s(transport_file.second)); + auto sdp_params = nmos::parse_session_description(session_description).first; + + return match_sdp_parameters_constraint_sets(constraint_sets, sdp_params); + }; + + } + + bool match_resource_parameters_constraint_set(const nmos::resource& resource, const web::json::value& constraint_set) + { + const std::map resource_parameter_constraints + { + { nmos::types::source, source_parameter_constraints }, + { nmos::types::flow, flow_parameter_constraints }, + { nmos::types::sender, sender_parameter_constraints } + }; + + if (0 == resource_parameter_constraints.count(resource.type)) + { + throw std::logic_error("wrong resource type"); + } + + return detail::match_resource_parameters_constraint_set(resource_parameter_constraints.at(resource.type), resource.data, constraint_set); + } + + details::streamcompatibility_sender_validator make_streamcompatibility_sender_resources_validator(const details::resource_constraints_matcher& match_resource_constraint_set, const details::transport_file_constraint_sets_matcher& match_transport_file_constraint_sets) + { + return [match_resource_constraint_set, match_transport_file_constraint_sets](const nmos::resource& source, const nmos::resource& flow, const nmos::resource& sender, const nmos::resource& connection_sender, const web::json::array& constraint_sets) -> std::pair + { + nmos::sender_state sender_state; + + if (!web::json::empty(constraint_sets)) + { + bool constrained = true; + auto source_found = std::find_if(constraint_sets.begin(), constraint_sets.end(), [&](const web::json::value& constraint_set) { return match_resource_constraint_set(source, constraint_set); }); + auto flow_found = std::find_if(constraint_sets.begin(), constraint_sets.end(), [&](const web::json::value& constraint_set) { return match_resource_constraint_set(flow, constraint_set); }); + auto sender_found = std::find_if(constraint_sets.begin(), constraint_sets.end(), [&](const web::json::value& constraint_set) { return match_resource_constraint_set(sender, constraint_set); }); + + constrained = constraint_sets.end() != source_found && constraint_sets.end() != flow_found && constraint_sets.end() != sender_found; + + const auto& transport_file = nmos::fields::endpoint_transportfile(connection_sender.data); + + if (!transport_file.is_null() && !transport_file.as_object().empty()) + { + constrained = constrained && match_transport_file_constraint_sets(nmos::details::get_transport_type_data(transport_file), constraint_sets); + } + + sender_state = constrained ? nmos::sender_states::constrained : nmos::sender_states::active_constraints_violation; + } + else + { + sender_state = nmos::sender_states::unconstrained; + } + + return { sender_state, {} }; + }; + } + + details::streamcompatibility_receiver_validator make_streamcompatibility_receiver_validator(const details::transport_file_validator& validate_transport_file) + { + return [validate_transport_file](const nmos::resource& receiver, const web::json::value& transport_file_) -> std::pair + { + nmos::receiver_state receiver_state = nmos::receiver_states::unknown; + utility::string_t receiver_state_debug; + + if (!transport_file_.is_null() && !transport_file_.as_object().empty()) + { + const auto transport_file = nmos::details::get_transport_type_data(transport_file_); + if (std::pair{} != transport_file) + { + receiver_state = nmos::receiver_states::compliant_stream; + + try + { + validate_transport_file(receiver, transport_file.first, transport_file.second); + } + catch (const std::exception& e) + { + receiver_state = nmos::receiver_states::non_compliant_stream; + receiver_state_debug = utility::conversions::to_string_t(e.what()); + } + } + } + + return { receiver_state, receiver_state_debug }; + }; + } + } +} diff --git a/Development/nmos/streamcompatibility_validation.h b/Development/nmos/streamcompatibility_validation.h new file mode 100644 index 000000000..edc0c115e --- /dev/null +++ b/Development/nmos/streamcompatibility_validation.h @@ -0,0 +1,132 @@ +#ifndef NMOS_STREAMCOMPATIBILITY_VALIDATION_H +#define NMOS_STREAMCOMPATIBILITY_VALIDATION_H + +#include +#include +#include "nmos/capabilities.h" +#include "nmos/connection_api.h" +#include "nmos/media_type.h" +#include "nmos/resource.h" +#include "nmos/sdp_utils.h" +#include "nmos/streamcompatibility_state.h" + +namespace web +{ + namespace json + { + class array; + class value; + } +} + +namespace nmos +{ + struct node_model; + + namespace experimental + { + namespace details + { + // Sender validation with its associated resources, returns sender's "state" and "debug" values + // It is used by the stream compatibility behaviour thread + // It may throw exception, which will be logged + typedef std::function(const nmos::resource& source, const nmos::resource& flow, const nmos::resource& sender, const nmos::resource& connection_sender, const web::json::array& constraint_sets)> streamcompatibility_sender_validator; + + // Receiver validation with its transport file, returns receiver's "state" and "debug" values + // It is used by the stream compatibility behaviour thread + // It may throw exception, which will be logged + typedef std::function(const nmos::resource& receiver, const web::json::value& transport_file)> streamcompatibility_receiver_validator; + + // Check the specified receiver against the specified transport file + typedef std::function transport_file_validator; + + // Check the specified resource against the specified constraint sets + typedef std::function resource_constraints_matcher; + + // Check the specified transport file against the specified constraint sets + typedef std::function& transport_file, const web::json::array& constraint_sets)> transport_file_constraint_sets_matcher; + + // Check the specified sdp parameters against the specified constraint sets + typedef std::function sdp_constraint_sets_matcher; + + // Validate the specified transport file for the specified receiver using the specified validator + void validate_rtp_transport_file(nmos::details::sdp_parameters_validator validate_sdp_parameters, const nmos::resource& receiver, const utility::string_t& transport_file_type, const utility::string_t& transport_file_data); + } + + // Validate the specified transport file for the specified receiver using the default validator + // (this is the default transport file validator) + void validate_rtp_transport_file(const nmos::resource& receiver, const utility::string_t& transport_file_type, const utility::string_t& transport_file_data); + + typedef std::map> parameter_constraints; + + // NMOS Parameter Registers - Capabilities register + // See https://github.com/AMWA-TV/nmos-parameter-registers/blob/main/capabilities/README.md + + const parameter_constraints source_parameter_constraints + { + // Audio Constraints + + // urn:x-nmos:cap:format:channel_count - see https://github.com/AMWA-TV/nmos-parameter-registers/blob/main/capabilities/README.md#channel-count + { nmos::caps::format::channel_count, [](const web::json::value& source, const web::json::value& constraint) { return nmos::match_integer_constraint((uint32_t)nmos::fields::channels(source).size(), constraint); } } + }; + + const parameter_constraints flow_parameter_constraints + { + // General Constraints + + // urn:x-nmos:cap:format:media_type - see https://github.com/AMWA-TV/nmos-parameter-registers/blob/main/capabilities/README.md#media-type + { nmos::caps::format::media_type, [](const web::json::value& flow, const web::json::value& constraint) { return nmos::match_string_constraint(nmos::fields::media_type(flow), constraint); } }, + // urn:x-nmos:cap:format:grain_rate - see https://github.com/AMWA-TV/nmos-parameter-registers/blob/main/capabilities/README.md#grain-rate + { nmos::caps::format::grain_rate, [](const web::json::value& flow, const web::json::value& constraint) { return nmos::match_rational_constraint(nmos::parse_rational(nmos::fields::grain_rate(flow)), constraint); } }, + + // Video Constraints + + // urn:x-nmos:cap:format:frame_height - see https://github.com/AMWA-TV/nmos-parameter-registers/blob/main/capabilities/README.md#frame-height + { nmos::caps::format::frame_height, [](const web::json::value& flow, const web::json::value& constraint) { return nmos::match_integer_constraint(nmos::fields::frame_height(flow), constraint); } }, + // urn:x-nmos:cap:format:frame_width - see https://github.com/AMWA-TV/nmos-parameter-registers/blob/main/capabilities/README.md#frame-width + { nmos::caps::format::frame_width, [](const web::json::value& flow, const web::json::value& constraint) { return nmos::match_integer_constraint(nmos::fields::frame_width(flow), constraint); } }, + // urn:x-nmos:cap:format:color_sampling - https://github.com/AMWA-TV/nmos-parameter-registers/blob/main/capabilities/README.md#color-sampling + { nmos::caps::format::color_sampling, [](const web::json::value& flow, const web::json::value& constraint) { return nmos::match_string_constraint(nmos::details::make_sampling(nmos::fields::components(flow)).name, constraint); } }, + // urn:x-nmos:cap:format:interlace_mode - see https://github.com/AMWA-TV/nmos-parameter-registers/blob/main/capabilities/README.md#interlace-mode + { nmos::caps::format::interlace_mode, [](const web::json::value& flow, const web::json::value& constraint) { return nmos::match_string_constraint(nmos::fields::interlace_mode(flow), constraint); } }, + // urn:x-nmos:cap:format:colorspace - see https://github.com/AMWA-TV/nmos-parameter-registers/blob/main/capabilities/README.md#colorspace + { nmos::caps::format::colorspace, [](const web::json::value& flow, const web::json::value& constraint) { return nmos::match_string_constraint(nmos::fields::colorspace(flow), constraint); } }, + // transfer_characteristic - see https://github.com/AMWA-TV/nmos-parameter-registers/blob/main/capabilities/README.md#transfer-characteristic + { nmos::caps::format::transfer_characteristic, [](const web::json::value& flow, const web::json::value& constraint) { return nmos::match_string_constraint(nmos::fields::transfer_characteristic(flow), constraint); } }, + // urn:x-nmos:cap:format:component_depth - see https://github.com/AMWA-TV/nmos-parameter-registers/blob/main/capabilities/README.md#component-depth + { nmos::caps::format::component_depth, [](const web::json::value& flow, const web::json::value& constraint) { return nmos::match_integer_constraint(nmos::fields::bit_depth(nmos::fields::components(flow).at(0)), constraint); } }, + + // Audio Constraints + + // urn:x-nmos:cap:format:sample_rate - see https://github.com/AMWA-TV/nmos-parameter-registers/blob/main/capabilities/README.md#sample-rate + { nmos::caps::format::sample_rate, [](const web::json::value& flow, const web::json::value& constraint) { return nmos::match_rational_constraint(nmos::parse_rational(nmos::fields::sample_rate(flow)), constraint); } }, + // urn:x-nmos:cap:format:sample_depth - see https://github.com/AMWA-TV/nmos-parameter-registers/blob/main/capabilities/README.md#sample-depth + { nmos::caps::format::sample_depth, [](const web::json::value& flow, const web::json::value& constraint) { return nmos::match_integer_constraint(nmos::fields::bit_depth(flow), constraint); } } + }; + + const parameter_constraints sender_parameter_constraints + { + // urn:x-nmos:cap:transport:st2110_21_sender_type - see https://github.com/AMWA-TV/nmos-parameter-registers/blob/main/capabilities/README.md#st-2110-21-sender-type + { nmos::caps::transport::st2110_21_sender_type, [](const web::json::value& sender, const web::json::value& constraint) { return nmos::match_string_constraint(nmos::fields::st2110_21_sender_type(sender), constraint); } } + }; + + namespace detail + { + bool match_resource_parameters_constraint_set(const parameter_constraints& constraints, const web::json::value& resource, const web::json::value& constraint_set); + } + + // "At any time if State of an active Sender becomes active_constraints_violation, the Sender MUST become inactive. + // An inactive Sender in this state MUST NOT allow activations. + // At any time if State of an active Receiver becomes non_compliant_stream, the Receiver SHOULD become inactive. + // An inactive Receiver in this state SHOULD NOT allow activations." + // See https://specs.amwa.tv/is-11/branches/v1.0.x/docs/Behaviour_-_Server_Side.html#preventing-restrictions-violation + nmos::details::connection_resource_patch_validator make_connection_streamcompatibility_validator(nmos::node_model& model); + + details::streamcompatibility_sender_validator make_streamcompatibility_sender_resources_validator(const details::resource_constraints_matcher& resource_matcher, const details::transport_file_constraint_sets_matcher& transport_file_matcher); + details::streamcompatibility_receiver_validator make_streamcompatibility_receiver_validator(const details::transport_file_validator& validate_transport_file); + bool match_resource_parameters_constraint_set(const nmos::resource& resource, const web::json::value& constraint_set); + details::transport_file_constraint_sets_matcher make_streamcompatibility_sdp_constraint_sets_matcher(const details::sdp_constraint_sets_matcher& match_sdp_parameters_constraint_sets); + } +} + +#endif diff --git a/Development/nmos/test/constraints_test.cpp b/Development/nmos/test/constraints_test.cpp new file mode 100644 index 000000000..334b8e6d7 --- /dev/null +++ b/Development/nmos/test/constraints_test.cpp @@ -0,0 +1,423 @@ +#include "nmos/capabilities.h" +#include "nmos/constraints.h" +#include "nmos/interlace_mode.h" +#include "nmos/json_fields.h" +#include "nmos/media_type.h" +#include "nmos/sdp_utils.h" + +#include "bst/test/test.h" + +//////////////////////////////////////////////////////////////////////////////////////////// +BST_TEST_CASE(testJsonComparator) +{ + { + using nmos::experimental::details::constraint_value_less; + + const auto a = nmos::make_caps_rational_constraint({}, nmos::rates::rate25, nmos::rates::rate30); + const auto b = nmos::make_caps_rational_constraint({}, nmos::rates::rate25, nmos::rates::rate29_97); + + BST_REQUIRE(constraint_value_less(nmos::fields::constraint_minimum(a), nmos::fields::constraint_maximum(a))); + BST_REQUIRE(constraint_value_less(nmos::fields::constraint_minimum(a), nmos::fields::constraint_maximum(b))); + BST_REQUIRE(constraint_value_less(nmos::fields::constraint_minimum(b), nmos::fields::constraint_maximum(a))); + BST_REQUIRE(constraint_value_less(nmos::fields::constraint_minimum(b), nmos::fields::constraint_maximum(b))); + + BST_REQUIRE(!constraint_value_less(nmos::fields::constraint_minimum(a), nmos::fields::constraint_minimum(b))); + BST_REQUIRE(!constraint_value_less(nmos::fields::constraint_maximum(a), nmos::fields::constraint_maximum(b))); + + BST_REQUIRE(!constraint_value_less(nmos::fields::constraint_minimum(b), nmos::fields::constraint_minimum(a))); + BST_REQUIRE(constraint_value_less(nmos::fields::constraint_maximum(b), nmos::fields::constraint_maximum(a))); + } +} + +//////////////////////////////////////////////////////////////////////////////////////////// +BST_TEST_CASE(testSimpleCase) +{ + { + using web::json::value; + using web::json::value_of; + using nmos::experimental::is_constraint_subset; + + auto a = value_of({ + { nmos::caps::format::frame_width, nmos::make_caps_integer_constraint({}, 1920) } + }); + + auto b1 = value_of({ + { nmos::caps::format::frame_width, nmos::make_caps_integer_constraint({}, 2000) }, + { nmos::caps::format::media_type, nmos::make_caps_string_constraint({ nmos::media_types::video_raw.name }) }, + }); + + auto b2 = value_of({ + { nmos::caps::format::frame_width, nmos::make_caps_integer_constraint({}, 1900) } + }); + + auto b3 = value::object(); + + BST_REQUIRE(is_constraint_subset(a, b1)); + BST_REQUIRE(!is_constraint_subset(a, b2)); + BST_REQUIRE(!is_constraint_subset(a, b3)); + } +} + +//////////////////////////////////////////////////////////////////////////////////////////// +BST_TEST_CASE(testLessConstraints) +{ + { + using web::json::value_of; + using nmos::experimental::is_constraint_subset; + + auto a = value_of({ + { nmos::caps::format::media_type, nmos::make_caps_string_constraint({ nmos::media_types::video_raw.name }) }, + { nmos::caps::format::grain_rate, nmos::make_caps_rational_constraint({ nmos::rates::rate25, nmos::rates::rate29_97 }) }, + { nmos::caps::format::frame_width, nmos::make_caps_integer_constraint({ 1920 }) }, + { nmos::caps::format::frame_height, nmos::make_caps_integer_constraint({ 1080 }) }, + { nmos::caps::format::color_sampling, nmos::make_caps_string_constraint({ sdp::samplings::YCbCr_4_2_2.name }) }, + { nmos::caps::format::interlace_mode, nmos::make_caps_string_constraint({ nmos::interlace_modes::interlaced_bff.name, nmos::interlace_modes::interlaced_tff.name, nmos::interlace_modes::interlaced_psf.name }) }, + { nmos::caps::format::colorspace, nmos::make_caps_string_constraint({ sdp::colorimetries::BT2020.name, sdp::colorimetries::BT709.name }) }, + { nmos::caps::format::transfer_characteristic, nmos::make_caps_string_constraint({ sdp::transfer_characteristic_systems::SDR.name }) }, + { nmos::caps::format::component_depth, nmos::make_caps_integer_constraint({}, 8, 12) }, + { nmos::caps::transport::st2110_21_sender_type, nmos::make_caps_string_constraint({ sdp::type_parameters::type_N.name }) } + }); + + auto b = value_of({ + { nmos::caps::format::media_type, nmos::make_caps_string_constraint({ nmos::media_types::video_raw.name }) }, + { nmos::caps::format::grain_rate, nmos::make_caps_rational_constraint({ nmos::rates::rate25, nmos::rates::rate29_97 }) }, + { nmos::caps::format::frame_width, nmos::make_caps_integer_constraint({ 1920 }) }, + { nmos::caps::format::frame_height, nmos::make_caps_integer_constraint({ 1080 }) } + }); + + BST_REQUIRE(!is_constraint_subset(a, b)); + BST_REQUIRE(is_constraint_subset(b, a)); + } +} + +//////////////////////////////////////////////////////////////////////////////////////////// +BST_TEST_CASE(testRoundTrip) +{ + { + using web::json::value_of; + using nmos::experimental::is_constraint_subset; + + auto constraint_set = value_of({ + { nmos::caps::format::media_type, nmos::make_caps_string_constraint({ nmos::media_types::video_raw.name }) }, + { nmos::caps::format::grain_rate, nmos::make_caps_rational_constraint({ nmos::rates::rate25, nmos::rates::rate29_97 }) }, + { nmos::caps::format::frame_width, nmos::make_caps_integer_constraint({ 1920 }) }, + { nmos::caps::format::frame_height, nmos::make_caps_integer_constraint({ 1080 }) }, + { nmos::caps::format::color_sampling, nmos::make_caps_string_constraint({ sdp::samplings::YCbCr_4_2_2.name }) }, + { nmos::caps::format::interlace_mode, nmos::make_caps_string_constraint({ nmos::interlace_modes::interlaced_bff.name, nmos::interlace_modes::interlaced_tff.name, nmos::interlace_modes::interlaced_psf.name }) }, + { nmos::caps::format::colorspace, nmos::make_caps_string_constraint({ sdp::colorimetries::BT2020.name, sdp::colorimetries::BT709.name }) }, + { nmos::caps::format::transfer_characteristic, nmos::make_caps_string_constraint({ sdp::transfer_characteristic_systems::SDR.name }) }, + { nmos::caps::format::component_depth, nmos::make_caps_integer_constraint({}, 8, 12) }, + { nmos::caps::transport::st2110_21_sender_type, nmos::make_caps_string_constraint({ sdp::type_parameters::type_N.name }) } + }); + + BST_REQUIRE(is_constraint_subset(constraint_set, constraint_set)); + } +} + +//////////////////////////////////////////////////////////////////////////////////////////// +BST_TEST_CASE(testSubconstraints) +{ + { + using web::json::value_of; + using nmos::experimental::is_constraint_subset; + + auto constraint_set = value_of({ + { nmos::caps::format::media_type, nmos::make_caps_string_constraint({ nmos::media_types::video_raw.name }) }, + { nmos::caps::format::grain_rate, nmos::make_caps_rational_constraint({ nmos::rates::rate25, nmos::rates::rate29_97 }) }, + { nmos::caps::format::frame_width, nmos::make_caps_integer_constraint({ 1920 }) }, + { nmos::caps::format::frame_height, nmos::make_caps_integer_constraint({ 1080 }) }, + { nmos::caps::format::color_sampling, nmos::make_caps_string_constraint({ sdp::samplings::YCbCr_4_2_2.name }) }, + { nmos::caps::format::interlace_mode, nmos::make_caps_string_constraint({ nmos::interlace_modes::interlaced_bff.name, nmos::interlace_modes::interlaced_tff.name, nmos::interlace_modes::interlaced_psf.name }) }, + { nmos::caps::format::colorspace, nmos::make_caps_string_constraint({ sdp::colorimetries::BT2020.name, sdp::colorimetries::BT709.name }) }, + { nmos::caps::format::transfer_characteristic, nmos::make_caps_string_constraint({ sdp::transfer_characteristic_systems::SDR.name }) }, + { nmos::caps::format::component_depth, nmos::make_caps_integer_constraint({}, 8, 12) }, + { nmos::caps::transport::st2110_21_sender_type, nmos::make_caps_string_constraint({ sdp::type_parameters::type_N.name }) } + }); + + auto constraint_subset = value_of({ + { nmos::caps::format::media_type, nmos::make_caps_string_constraint({ nmos::media_types::video_raw.name }) }, + { nmos::caps::format::grain_rate, nmos::make_caps_rational_constraint({ nmos::rates::rate25 }) }, + { nmos::caps::format::frame_width, nmos::make_caps_integer_constraint({ 1920 }) }, + { nmos::caps::format::frame_height, nmos::make_caps_integer_constraint({ 1080 }) }, + { nmos::caps::format::color_sampling, nmos::make_caps_string_constraint({ sdp::samplings::YCbCr_4_2_2.name }) }, + { nmos::caps::format::interlace_mode, nmos::make_caps_string_constraint({ nmos::interlace_modes::interlaced_tff.name, nmos::interlace_modes::interlaced_psf.name }) }, + { nmos::caps::format::colorspace, nmos::make_caps_string_constraint({ sdp::colorimetries::BT2020.name, sdp::colorimetries::BT709.name }) }, + { nmos::caps::format::transfer_characteristic, nmos::make_caps_string_constraint({ sdp::transfer_characteristic_systems::SDR.name }) }, + { nmos::caps::format::component_depth, nmos::make_caps_integer_constraint({ 10 }) }, + { nmos::caps::transport::st2110_21_sender_type, nmos::make_caps_string_constraint({ sdp::type_parameters::type_N.name }) } + }); + + BST_REQUIRE(is_constraint_subset(constraint_set, constraint_subset)); + } +} + +//////////////////////////////////////////////////////////////////////////////////////////// +BST_TEST_CASE(testRationalMinMaxSubconstraints) +{ + { + using web::json::value_of; + using nmos::experimental::is_subconstraint; + + const auto wideRange = nmos::make_caps_rational_constraint({}, nmos::rates::rate25, nmos::rates::rate30); + const auto narrowRange = nmos::make_caps_rational_constraint({}, nmos::rates::rate25, nmos::rates::rate29_97); + + BST_REQUIRE(is_subconstraint(wideRange, narrowRange)); + } +} + +//////////////////////////////////////////////////////////////////////////////////////////// +BST_TEST_CASE(testEnumConstraintIntersection) +{ + { + using web::json::value_of; + using nmos::experimental::get_constraint_intersection; + + const auto a = nmos::make_caps_integer_constraint({ 8, 10 }); + const auto b = nmos::make_caps_integer_constraint({ 10, 12 }); + + const auto c = nmos::make_caps_integer_constraint({ 10 }); + + BST_REQUIRE_EQUAL(get_constraint_intersection(a, b), c); + } +} + +//////////////////////////////////////////////////////////////////////////////////////////// +BST_TEST_CASE(testNoEnumConstraintIntersection) +{ + { + using web::json::value_of; + using nmos::experimental::get_constraint_intersection; + + const auto a = nmos::make_caps_integer_constraint({}, 8, 12); + const auto b = nmos::make_caps_integer_constraint({}, 8, 12); + + const auto c = nmos::make_caps_integer_constraint({}, 8, 12); + + BST_REQUIRE_EQUAL(get_constraint_intersection(a, b), c); + } +} + +//////////////////////////////////////////////////////////////////////////////////////////// +BST_TEST_CASE(testEnumRationalConstraintIntersection) +{ + { + using web::json::value_of; + using nmos::experimental::get_constraint_intersection; + + const auto a = nmos::make_caps_rational_constraint({ nmos::rates::rate25, nmos::rates::rate29_97, nmos::rates::rate60 }); + const auto b = nmos::make_caps_rational_constraint({ nmos::rates::rate60 }); + + const auto c = nmos::make_caps_rational_constraint({ nmos::rates::rate60 }); + + BST_REQUIRE_EQUAL(b, c); + BST_REQUIRE_EQUAL(get_constraint_intersection(a, b), c); + } +} + +//////////////////////////////////////////////////////////////////////////////////////////// +BST_TEST_CASE(testConstraintSetIntersectionSameParamConstraints) +{ + { + using web::json::value_of; + using nmos::experimental::get_constraint_set_intersection; + + const auto a = value_of({ + { nmos::caps::format::media_type, nmos::make_caps_string_constraint({ nmos::media_types::video_raw.name }) }, + { nmos::caps::format::grain_rate, nmos::make_caps_rational_constraint({ nmos::rates::rate25, nmos::rates::rate29_97 }) }, + { nmos::caps::format::frame_width, nmos::make_caps_integer_constraint({ 1920 }) }, + { nmos::caps::format::frame_height, nmos::make_caps_integer_constraint({ 1080 }) }, + { nmos::caps::format::color_sampling, nmos::make_caps_string_constraint({ sdp::samplings::YCbCr_4_2_2.name }) }, + { nmos::caps::format::interlace_mode, nmos::make_caps_string_constraint({ nmos::interlace_modes::interlaced_bff.name, nmos::interlace_modes::interlaced_tff.name, nmos::interlace_modes::interlaced_psf.name }) }, + { nmos::caps::format::colorspace, nmos::make_caps_string_constraint({ sdp::colorimetries::BT2020.name, sdp::colorimetries::BT709.name }) }, + { nmos::caps::format::transfer_characteristic, nmos::make_caps_string_constraint({ sdp::transfer_characteristic_systems::SDR.name }) }, + { nmos::caps::format::component_depth, nmos::make_caps_integer_constraint({}, 8, 12) }, + { nmos::caps::transport::st2110_21_sender_type, nmos::make_caps_string_constraint({ sdp::type_parameters::type_N.name }) } + }); + + const auto b = value_of({ + { nmos::caps::format::media_type, nmos::make_caps_string_constraint({ nmos::media_types::video_raw.name }) }, + { nmos::caps::format::grain_rate, nmos::make_caps_rational_constraint({ nmos::rates::rate25 }) }, + { nmos::caps::format::frame_width, nmos::make_caps_integer_constraint({ 1920 }) }, + { nmos::caps::format::frame_height, nmos::make_caps_integer_constraint({ 1080 }) }, + { nmos::caps::format::color_sampling, nmos::make_caps_string_constraint({ sdp::samplings::YCbCr_4_2_2.name }) }, + { nmos::caps::format::interlace_mode, nmos::make_caps_string_constraint({ nmos::interlace_modes::interlaced_tff.name, nmos::interlace_modes::interlaced_psf.name }) }, + { nmos::caps::format::colorspace, nmos::make_caps_string_constraint({ sdp::colorimetries::BT2020.name, sdp::colorimetries::BT709.name }) }, + { nmos::caps::format::transfer_characteristic, nmos::make_caps_string_constraint({ sdp::transfer_characteristic_systems::SDR.name }) }, + { nmos::caps::format::component_depth, nmos::make_caps_integer_constraint({ 10 }) }, + { nmos::caps::transport::st2110_21_sender_type, nmos::make_caps_string_constraint({ sdp::type_parameters::type_N.name }) } + }); + + const auto c = value_of({ + { nmos::caps::format::media_type, nmos::make_caps_string_constraint({ nmos::media_types::video_raw.name }) }, + { nmos::caps::format::grain_rate, nmos::make_caps_rational_constraint({ nmos::rates::rate25 }) }, + { nmos::caps::format::frame_width, nmos::make_caps_integer_constraint({ 1920 }) }, + { nmos::caps::format::frame_height, nmos::make_caps_integer_constraint({ 1080 }) }, + { nmos::caps::format::color_sampling, nmos::make_caps_string_constraint({ sdp::samplings::YCbCr_4_2_2.name }) }, + { nmos::caps::format::interlace_mode, nmos::make_caps_string_constraint({ nmos::interlace_modes::interlaced_psf.name, nmos::interlace_modes::interlaced_tff.name }) }, + { nmos::caps::format::colorspace, nmos::make_caps_string_constraint({ sdp::colorimetries::BT2020.name, sdp::colorimetries::BT709.name }) }, + { nmos::caps::format::transfer_characteristic, nmos::make_caps_string_constraint({ sdp::transfer_characteristic_systems::SDR.name }) }, + { nmos::caps::format::component_depth, nmos::make_caps_integer_constraint({ 10 }) }, + { nmos::caps::transport::st2110_21_sender_type, nmos::make_caps_string_constraint({ sdp::type_parameters::type_N.name }) } + }); + + BST_REQUIRE_EQUAL(get_constraint_set_intersection(a, b), c); + } +} + +//////////////////////////////////////////////////////////////////////////////////////////// +BST_TEST_CASE(testConstraintSetIntersection) +{ + { + using web::json::value_of; + using nmos::experimental::get_constraint_set_intersection; + + const auto a = value_of({ + { nmos::caps::format::media_type, nmos::make_caps_string_constraint({ nmos::media_types::video_raw.name }) }, + { nmos::caps::format::grain_rate, nmos::make_caps_rational_constraint({ nmos::rates::rate25, nmos::rates::rate29_97 }) }, + { nmos::caps::format::frame_width, nmos::make_caps_integer_constraint({ 1920 }) }, + { nmos::caps::format::frame_height, nmos::make_caps_integer_constraint({ 1080 }) }, + { nmos::caps::format::color_sampling, nmos::make_caps_string_constraint({ sdp::samplings::YCbCr_4_2_2.name }) }, + { nmos::caps::format::interlace_mode, nmos::make_caps_string_constraint({ nmos::interlace_modes::interlaced_bff.name, nmos::interlace_modes::interlaced_tff.name, nmos::interlace_modes::interlaced_psf.name }) }, + { nmos::caps::format::colorspace, nmos::make_caps_string_constraint({ sdp::colorimetries::BT2020.name, sdp::colorimetries::BT709.name }) }, + { nmos::caps::format::transfer_characteristic, nmos::make_caps_string_constraint({ sdp::transfer_characteristic_systems::SDR.name }) }, + { nmos::caps::format::component_depth, nmos::make_caps_integer_constraint({}, 8, 12) }, + { nmos::caps::transport::st2110_21_sender_type, nmos::make_caps_string_constraint({ sdp::type_parameters::type_N.name }) } + }); + + const auto b = value_of({ + { nmos::caps::format::grain_rate, nmos::make_caps_rational_constraint({ nmos::rates::rate25 }) }, + { nmos::caps::format::frame_width, nmos::make_caps_integer_constraint({ 1920 }) }, + { nmos::caps::format::frame_height, nmos::make_caps_integer_constraint({ 1080 }) }, + { nmos::caps::format::interlace_mode, nmos::make_caps_string_constraint({ nmos::interlace_modes::interlaced_tff.name, nmos::interlace_modes::interlaced_psf.name }) }, + { nmos::caps::format::component_depth, nmos::make_caps_integer_constraint({ 10 }) } + }); + + const auto c = value_of({ + { nmos::caps::format::media_type, nmos::make_caps_string_constraint({ nmos::media_types::video_raw.name }) }, + { nmos::caps::format::grain_rate, nmos::make_caps_rational_constraint({ nmos::rates::rate25 }) }, + { nmos::caps::format::frame_width, nmos::make_caps_integer_constraint({ 1920 }) }, + { nmos::caps::format::frame_height, nmos::make_caps_integer_constraint({ 1080 }) }, + { nmos::caps::format::color_sampling, nmos::make_caps_string_constraint({ sdp::samplings::YCbCr_4_2_2.name }) }, + { nmos::caps::format::interlace_mode, nmos::make_caps_string_constraint({ nmos::interlace_modes::interlaced_psf.name, nmos::interlace_modes::interlaced_tff.name }) }, + { nmos::caps::format::colorspace, nmos::make_caps_string_constraint({ sdp::colorimetries::BT2020.name, sdp::colorimetries::BT709.name }) }, + { nmos::caps::format::transfer_characteristic, nmos::make_caps_string_constraint({ sdp::transfer_characteristic_systems::SDR.name }) }, + { nmos::caps::format::component_depth, nmos::make_caps_integer_constraint({ 10 }) }, + { nmos::caps::transport::st2110_21_sender_type, nmos::make_caps_string_constraint({ sdp::type_parameters::type_N.name }) } + }); + + BST_REQUIRE_EQUAL(get_constraint_set_intersection(a, b), c); + } +} + +//////////////////////////////////////////////////////////////////////////////////////////// +BST_TEST_CASE(testConstraintSetIntersectionUniqueParamConstraints) +{ + { + using web::json::value_of; + using nmos::experimental::get_constraint_set_intersection; + + const auto a = value_of({ + { nmos::caps::format::media_type, nmos::make_caps_string_constraint({ nmos::media_types::video_raw.name }) }, + { nmos::caps::format::color_sampling, nmos::make_caps_string_constraint({ sdp::samplings::YCbCr_4_2_2.name }) }, + { nmos::caps::format::colorspace, nmos::make_caps_string_constraint({ sdp::colorimetries::BT2020.name, sdp::colorimetries::BT709.name }) }, + { nmos::caps::format::transfer_characteristic, nmos::make_caps_string_constraint({ sdp::transfer_characteristic_systems::SDR.name }) }, + { nmos::caps::transport::st2110_21_sender_type, nmos::make_caps_string_constraint({ sdp::type_parameters::type_N.name }) } + }); + + const auto b = value_of({ + { nmos::caps::format::grain_rate, nmos::make_caps_rational_constraint({ nmos::rates::rate25 }) }, + { nmos::caps::format::frame_width, nmos::make_caps_integer_constraint({ 1920 }) }, + { nmos::caps::format::frame_height, nmos::make_caps_integer_constraint({ 1080 }) }, + { nmos::caps::format::interlace_mode, nmos::make_caps_string_constraint({ nmos::interlace_modes::interlaced_tff.name, nmos::interlace_modes::interlaced_psf.name }) }, + { nmos::caps::format::component_depth, nmos::make_caps_integer_constraint({ 10 }) } + }); + + const auto c = value_of({ + { nmos::caps::format::media_type, nmos::make_caps_string_constraint({ nmos::media_types::video_raw.name }) }, + { nmos::caps::format::grain_rate, nmos::make_caps_rational_constraint({ nmos::rates::rate25 }) }, + { nmos::caps::format::frame_width, nmos::make_caps_integer_constraint({ 1920 }) }, + { nmos::caps::format::frame_height, nmos::make_caps_integer_constraint({ 1080 }) }, + { nmos::caps::format::color_sampling, nmos::make_caps_string_constraint({ sdp::samplings::YCbCr_4_2_2.name }) }, + { nmos::caps::format::interlace_mode, nmos::make_caps_string_constraint({ nmos::interlace_modes::interlaced_tff.name, nmos::interlace_modes::interlaced_psf.name }) }, + { nmos::caps::format::colorspace, nmos::make_caps_string_constraint({ sdp::colorimetries::BT2020.name, sdp::colorimetries::BT709.name }) }, + { nmos::caps::format::transfer_characteristic, nmos::make_caps_string_constraint({ sdp::transfer_characteristic_systems::SDR.name }) }, + { nmos::caps::format::component_depth, nmos::make_caps_integer_constraint({ 10 }) }, + { nmos::caps::transport::st2110_21_sender_type, nmos::make_caps_string_constraint({ sdp::type_parameters::type_N.name }) } + }); + + BST_REQUIRE_EQUAL(get_constraint_set_intersection(a, b), c); + } +} + +//////////////////////////////////////////////////////////////////////////////////////////// +BST_TEST_CASE(testConstraintSetIntersectionOfEmpties) +{ + { + using web::json::value_of; + using nmos::experimental::get_constraint_set_intersection; + + const auto a = web::json::value::object(); + + BST_REQUIRE_EQUAL(get_constraint_set_intersection(a, a), a); + } +} + +//////////////////////////////////////////////////////////////////////////////////////////// +BST_TEST_CASE(testConstraintSetIntersectionOfEmptyAndNonEmpty) +{ + { + using web::json::value_of; + using nmos::experimental::get_constraint_set_intersection; + + const auto a = web::json::value::object(); + + const auto b = value_of({ + { nmos::caps::format::grain_rate, nmos::make_caps_rational_constraint({ nmos::rates::rate25 }) }, + { nmos::caps::format::frame_width, nmos::make_caps_integer_constraint({ 1920 }) }, + { nmos::caps::format::frame_height, nmos::make_caps_integer_constraint({ 1080 }) }, + { nmos::caps::format::interlace_mode, nmos::make_caps_string_constraint({ nmos::interlace_modes::interlaced_tff.name, nmos::interlace_modes::interlaced_psf.name }) }, + { nmos::caps::format::component_depth, nmos::make_caps_integer_constraint({ 10 }) } + }); + + BST_REQUIRE_EQUAL(get_constraint_set_intersection(a, b), b); + BST_REQUIRE_EQUAL(get_constraint_set_intersection(b, a), b); + } +} + +//////////////////////////////////////////////////////////////////////////////////////////// +BST_TEST_CASE(testConstraintSetIntersectionMeta) +{ + { + using web::json::value_of; + using nmos::experimental::get_constraint_set_intersection; + + const auto a = value_of({ + { nmos::caps::meta::label, U("test1") }, + { nmos::caps::format::grain_rate, nmos::make_caps_rational_constraint({ nmos::rates::rate25 }) }, + { nmos::caps::format::frame_width, nmos::make_caps_integer_constraint({ 1920 }) }, + { nmos::caps::format::frame_height, nmos::make_caps_integer_constraint({ 1080 }) }, + { nmos::caps::format::interlace_mode, nmos::make_caps_string_constraint({ nmos::interlace_modes::interlaced_psf.name, nmos::interlace_modes::interlaced_tff.name }) }, + { nmos::caps::format::component_depth, nmos::make_caps_integer_constraint({ 10 }) } + }); + + const auto b = value_of({ + { nmos::caps::meta::label, U("test2") }, + { nmos::caps::format::grain_rate, nmos::make_caps_rational_constraint({ nmos::rates::rate25 }) }, + { nmos::caps::format::frame_width, nmos::make_caps_integer_constraint({ 1920 }) }, + { nmos::caps::format::frame_height, nmos::make_caps_integer_constraint({ 1080 }) }, + { nmos::caps::format::interlace_mode, nmos::make_caps_string_constraint({ nmos::interlace_modes::interlaced_tff.name, nmos::interlace_modes::interlaced_psf.name }) }, + { nmos::caps::format::component_depth, nmos::make_caps_integer_constraint({ 10 }) } + }); + + const auto c = value_of({ + { nmos::caps::format::grain_rate, nmos::make_caps_rational_constraint({ nmos::rates::rate25 }) }, + { nmos::caps::format::frame_width, nmos::make_caps_integer_constraint({ 1920 }) }, + { nmos::caps::format::frame_height, nmos::make_caps_integer_constraint({ 1080 }) }, + { nmos::caps::format::interlace_mode, nmos::make_caps_string_constraint({ nmos::interlace_modes::interlaced_psf.name, nmos::interlace_modes::interlaced_tff.name }) }, + { nmos::caps::format::component_depth, nmos::make_caps_integer_constraint({ 10 }) } + }); + + BST_REQUIRE_EQUAL(get_constraint_set_intersection(a, b), c); + BST_REQUIRE_EQUAL(get_constraint_set_intersection(b, a), c); + } +} diff --git a/Development/nmos/test/streamcompatibility_validation_test.cpp b/Development/nmos/test/streamcompatibility_validation_test.cpp new file mode 100644 index 000000000..d0767655d --- /dev/null +++ b/Development/nmos/test/streamcompatibility_validation_test.cpp @@ -0,0 +1,35 @@ +#include "nmos/st2110_21_sender_type.h" +#include "nmos/streamcompatibility_validation.h" + +#include "bst/test/test.h" + +//////////////////////////////////////////////////////////////////////////////////////////// +BST_TEST_CASE(testResourceValidator) +{ + { + using web::json::value_of; + using nmos::experimental::flow_parameter_constraints; + using nmos::experimental::sender_parameter_constraints; + using nmos::experimental::detail::match_resource_parameters_constraint_set; + + auto constraint_set = value_of({ + { nmos::caps::format::media_type, nmos::make_caps_string_constraint({ nmos::media_types::video_raw.name }) }, + { nmos::caps::format::frame_width, nmos::make_caps_integer_constraint({ 1920 }) }, + { nmos::caps::format::frame_height, nmos::make_caps_integer_constraint({ 1080 }) }, + { nmos::caps::transport::st2110_21_sender_type, nmos::make_caps_string_constraint({ sdp::type_parameters::type_N.name, sdp::type_parameters::type_NL.name }) } + }); + + auto flow = value_of({ + { nmos::fields::media_type, nmos::media_types::video_raw.name }, + { nmos::fields::frame_width, 1920 }, + { nmos::fields::frame_height, 1080 } + }); + + auto sender = value_of({ + { nmos::fields::st2110_21_sender_type, nmos::st2110_21_sender_types::type_N.name } + }); + + BST_REQUIRE(match_resource_parameters_constraint_set(flow_parameter_constraints, flow, constraint_set)); + BST_REQUIRE(match_resource_parameters_constraint_set(sender_parameter_constraints, sender, constraint_set)); + } +} diff --git a/Development/nmos/type.h b/Development/nmos/type.h index 98a69252f..5e7b6ea13 100644 --- a/Development/nmos/type.h +++ b/Development/nmos/type.h @@ -33,7 +33,8 @@ namespace nmos // see https://specs.amwa.tv/is-04/releases/v1.2.1/docs/4.1._Behaviour_-_Registration.html#referential-integrity const std::vector all{ nmos::types::node, nmos::types::device, nmos::types::source, nmos::types::flow, nmos::types::sender, nmos::types::receiver, nmos::types::subscription, nmos::types::grain }; - // the Channel Mapping API resource types, see nmos/channelmapping_resources.h + // the Channel Mapping API resource types, see nmos/channelmapping_resources.h and + // the Stream Compatibility Management API resource types, see nmos/streamcompatibility_resources.h const type input{ U("input") }; const type output{ U("output") }; diff --git a/Development/nmos/video_jxsv.cpp b/Development/nmos/video_jxsv.cpp index 1ee2a6ff4..06cbf3068 100644 --- a/Development/nmos/video_jxsv.cpp +++ b/Development/nmos/video_jxsv.cpp @@ -6,6 +6,7 @@ #include "nmos/interlace_mode.h" #include "nmos/json_fields.h" #include "nmos/resource.h" +#include "nmos/streamcompatibility_validation.h" namespace sdp { @@ -347,6 +348,32 @@ namespace nmos nmos::details::validate_sdp_parameters(details::jxsv_constraints, sdp_params, nmos::formats::video, get_video_jxsv_parameters(sdp_params), receiver); } + // Check the specified SDP parameters against the specified constraint sets + // for "video/jxsv" + bool match_video_jxsv_sdp_parameters_constraint_sets(const web::json::array& constraint_sets, const sdp_parameters& sdp_params) + { + const auto format_params = get_video_jxsv_parameters(sdp_params); + const auto found = std::find_if(constraint_sets.begin(), constraint_sets.end(), [&](const web::json::value& constraint_set) { return details::match_sdp_parameters_constraint_set(details::jxsv_constraints, sdp_params, format_params, constraint_set); }); + return constraint_sets.end() != found; + } + + bool experimental::match_video_jxsv_resource_parameters_constraint_set(const nmos::resource& resource, const web::json::value& constraint_set) + { + const std::map resource_parameter_constraints + { + { nmos::types::source, nmos::experimental::source_parameter_constraints }, + { nmos::types::flow, nmos::experimental::video_jxsv_flow_parameter_constraints }, + { nmos::types::sender, nmos::experimental::video_jxsv_sender_parameter_constraints } + }; + + if (0 == resource_parameter_constraints.count(resource.type)) + { + throw std::logic_error("wrong resource type"); + } + + return nmos::experimental::detail::match_resource_parameters_constraint_set(resource_parameter_constraints.at(resource.type), resource.data, constraint_set); + } + // See https://specs.amwa.tv/bcp-006-01/branches/v1.0-dev/docs/NMOS_With_JPEG_XS.html#flows // cf. nmos::make_coded_video_flow nmos::resource make_video_jxsv_flow( diff --git a/Development/nmos/video_jxsv.h b/Development/nmos/video_jxsv.h index 947b8a49d..53f1a38f3 100644 --- a/Development/nmos/video_jxsv.h +++ b/Development/nmos/video_jxsv.h @@ -1,6 +1,8 @@ #ifndef NMOS_VIDEO_JXSV_H #define NMOS_VIDEO_JXSV_H +#include "nmos/capabilities.h" +#include "nmos/json_fields.h" #include "nmos/media_type.h" #include "nmos/node_resources.h" #include "nmos/sdp_utils.h" @@ -364,6 +366,10 @@ namespace nmos // Validate SDP parameters for "video/jxsv" against IS-04 receiver capabilities void validate_video_jxsv_sdp_parameters(const web::json::value& receiver, const nmos::sdp_parameters& sdp_params); + // Check the specified SDP parameters against the specified constraint sets + // for "video/jxsv" + bool match_video_jxsv_sdp_parameters_constraint_sets(const web::json::array& constraint_sets, const sdp_parameters& sdp_params); + // Calculate the format bit rate (kilobits/second) from the specified frame rate, dimensions and bits per pixel uint64_t get_video_jxsv_bit_rate(const nmos::rational& grain_rate, uint32_t frame_width, uint32_t frame_height, double bits_per_pixel); @@ -386,6 +392,35 @@ namespace nmos const nmos::sublevel& sublevel, double bits_per_pixel, const nmos::settings& settings); + + namespace experimental + { + const std::map> video_jxsv_flow_parameter_constraints + { + { nmos::caps::format::media_type, [](const web::json::value& flow, const web::json::value& con) { return nmos::match_string_constraint(nmos::fields::media_type(flow), con); } }, + { nmos::caps::format::grain_rate, [](const web::json::value& flow, const web::json::value& con) { return nmos::match_rational_constraint(nmos::parse_rational(nmos::fields::grain_rate(flow)), con); } }, + { nmos::caps::format::profile, [](const web::json::value& flow, const web::json::value& con) { return nmos::match_string_constraint(nmos::fields::profile(flow), con); } }, + { nmos::caps::format::level, [](const web::json::value& flow, const web::json::value& con) { return nmos::match_string_constraint(nmos::fields::level(flow), con); } }, + { nmos::caps::format::sublevel, [](const web::json::value& flow, const web::json::value& con) { return nmos::match_string_constraint(nmos::fields::sublevel(flow), con); } }, + { nmos::caps::format::frame_height, [](const web::json::value& flow, const web::json::value& con) { return nmos::match_integer_constraint(nmos::fields::frame_height(flow), con); } }, + { nmos::caps::format::frame_width, [](const web::json::value& flow, const web::json::value& con) { return nmos::match_integer_constraint(nmos::fields::frame_width(flow), con); } }, + { nmos::caps::format::color_sampling, [](const web::json::value& flow, const web::json::value& con) { return nmos::match_string_constraint(nmos::details::make_sampling(nmos::fields::components(flow)).name, con); } }, + { nmos::caps::format::interlace_mode, [](const web::json::value& flow, const web::json::value& con) { return nmos::match_string_constraint(nmos::fields::interlace_mode(flow), con); } }, + { nmos::caps::format::colorspace, [](const web::json::value& flow, const web::json::value& con) { return nmos::match_string_constraint(nmos::fields::colorspace(flow), con); } }, + { nmos::caps::format::transfer_characteristic, [](const web::json::value& flow, const web::json::value& con) { return nmos::match_string_constraint(nmos::fields::transfer_characteristic(flow), con); } }, + { nmos::caps::format::component_depth, [](const web::json::value& flow, const web::json::value& con) { return nmos::match_integer_constraint(nmos::fields::bit_depth(nmos::fields::components(flow).at(0)), con); } }, + { nmos::caps::format::bit_rate, [](const web::json::value& flow, const web::json::value& con) { return nmos::match_integer_constraint(nmos::fields::bit_rate(flow), con); } }, + }; + + const std::map> video_jxsv_sender_parameter_constraints + { + { nmos::caps::transport::packet_transmission_mode, [](const web::json::value& sender, const web::json::value& con) { return nmos::match_string_constraint(nmos::fields::packet_transmission_mode(sender), con); } }, + { nmos::caps::transport::st2110_21_sender_type, [](const web::json::value& sender, const web::json::value& con) { return nmos::match_string_constraint(nmos::fields::st2110_21_sender_type(sender), con); } }, + { nmos::caps::transport::bit_rate, [](const web::json::value& sender, const web::json::value& con) { return nmos::match_integer_constraint(nmos::fields::bit_rate(sender), con); } } + }; + + bool match_video_jxsv_resource_parameters_constraint_set(const nmos::resource& resource, const web::json::value& constraint_set); + } } #endif diff --git a/Development/third_party/is-11/README.md b/Development/third_party/is-11/README.md new file mode 100644 index 000000000..1e79bca2b --- /dev/null +++ b/Development/third_party/is-11/README.md @@ -0,0 +1,8 @@ +# AMWA IS-11 NMOS Stream Compatibility Management + +This directory contains files from the [AMWA IS-11 NMOS Stream Compatibility Management](https://github.com/AMWA-TV/is-11), in particular tagged versions of the JSON schemas used by the API specifications. + +Original source code: + +- (c) AMWA 2022 +- Licensed under the Apache License, Version 2.0; http://www.apache.org/licenses/LICENSE-2.0 diff --git a/Development/third_party/is-11/v1.0.x/APIs/schemas/README.md b/Development/third_party/is-11/v1.0.x/APIs/schemas/README.md new file mode 100644 index 000000000..e9847502d --- /dev/null +++ b/Development/third_party/is-11/v1.0.x/APIs/schemas/README.md @@ -0,0 +1,16 @@ +This directory is for JSON Schemas used in documentation. Some of them were copied from other AMWA NMOS specifications. + +[IS-04 v1.3.2][]: +- [resource_core.json](https://github.com/AMWA-TV/is-04/raw/v1.3.2/APIs/schemas/resource_core.json) + +[BCP-004-01 v1.0.0][]: +- [constraint_set.json](https://github.com/AMWA-TV/bcp-004-01/raw/v1.0.0/APIs/schemas/constraint_set.json) +- [param_constraint.json](https://github.com/AMWA-TV/bcp-004-01/raw/v1.0.0/APIs/schemas/param_constraint.json) +- [param_constraint_boolean.json](https://github.com/AMWA-TV/bcp-004-01/raw/v1.0.0/APIs/schemas/param_constraint_boolean.json) +- [param_constraint_integer.json](https://github.com/AMWA-TV/bcp-004-01/raw/v1.0.0/APIs/schemas/param_constraint_integer.json) +- [param_constraint_number.json](https://github.com/AMWA-TV/bcp-004-01/raw/v1.0.0/APIs/schemas/param_constraint_number.json) +- [param_constraint_rational.json](https://github.com/AMWA-TV/bcp-004-01/raw/v1.0.0/APIs/schemas/param_constraint_rational.json) +- [param_constraint_string.json](https://github.com/AMWA-TV/bcp-004-01/raw/v1.0.0/APIs/schemas/param_constraint_string.json) + +[IS-04 v1.3.2]: https://specs.amwa.tv/is-04/releases/v1.3.2/ +[BCP-004-01 v1.0.0]: https://specs.amwa.tv/bcp-004-01/releases/v1.0.0/ diff --git a/Development/third_party/is-11/v1.0.x/APIs/schemas/constraint_set.json b/Development/third_party/is-11/v1.0.x/APIs/schemas/constraint_set.json new file mode 100644 index 000000000..ab439d378 --- /dev/null +++ b/Development/third_party/is-11/v1.0.x/APIs/schemas/constraint_set.json @@ -0,0 +1,30 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "Describes a Constraint Set", + "title": "Constraint Set", + "type": "object", + "minProperties": 1, + "properties": { + "urn:x-nmos:cap:meta:label": { + "description": "Freeform string label for the Constraint Set", + "type": "string" + }, + "urn:x-nmos:cap:meta:preference": { + "description": "This value expresses the relative 'weight' that the Receiver assigns to its preference for the streams satisfied by the associated Constraint Set. The weight is an integer in the range -100 through 100, where -100 is least preferred and 100 is most preferred. When the attribute is omitted, the effective value for the associated Constraint Set is 0.", + "type": "integer", + "default": 0, + "maximum": 100, + "minimum": -100 + }, + "urn:x-nmos:cap:meta:enabled": { + "description": "This value indicates whether a Constraint Set is available to use immediately (true) or whether this is an offline capability which can be activated via some unspecified configuration mechanism (false). When the attribute is omitted its value is assumed to be true.", + "type": "boolean", + "default": true + } + }, + "patternProperties": { + "^urn:x-nmos:cap:(?!meta:)": { + "$ref": "param_constraint.json" + } + } +} diff --git a/Development/third_party/is-11/v1.0.x/APIs/schemas/constraints-base.json b/Development/third_party/is-11/v1.0.x/APIs/schemas/constraints-base.json new file mode 100644 index 000000000..df2cf92f1 --- /dev/null +++ b/Development/third_party/is-11/v1.0.x/APIs/schemas/constraints-base.json @@ -0,0 +1,16 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema", + "type": "array", + "description": "Describes the Stream Compatibility Management API /senders/{senderId}/constraints base resource", + "title": "Stream Compatibility Management API /senders/{senderId}/constraints base resource", + "items": { + "type": "string", + "enum": [ + "active/", + "supported/" + ] + }, + "minItems": 2, + "maxItems": 2, + "uniqueItems": true +} diff --git a/Development/third_party/is-11/v1.0.x/APIs/schemas/constraints_active.json b/Development/third_party/is-11/v1.0.x/APIs/schemas/constraints_active.json new file mode 100644 index 000000000..539dbec7d --- /dev/null +++ b/Development/third_party/is-11/v1.0.x/APIs/schemas/constraints_active.json @@ -0,0 +1,17 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema", + "description": "Describes Constraints", + "title": "Constraints", + "type": "object", + "required": [ + "constraint_sets" + ], + "properties": { + "constraint_sets": { + "type": "array", + "items": { + "$ref": "constraint_set.json" + } + } + } +} diff --git a/Development/third_party/is-11/v1.0.x/APIs/schemas/constraints_supported.json b/Development/third_party/is-11/v1.0.x/APIs/schemas/constraints_supported.json new file mode 100644 index 000000000..b1d39351c --- /dev/null +++ b/Development/third_party/is-11/v1.0.x/APIs/schemas/constraints_supported.json @@ -0,0 +1,38 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema", + "type": "object", + "required": [ + "parameter_constraints" + ], + "properties": { + "parameter_constraints": { + "type": "array", + "items": { + "type": "string", + "anyOf": [ + { + "enum": [ + "urn:x-nmos:cap:meta:label", + "urn:x-nmos:cap:meta:preference", + "urn:x-nmos:cap:meta:enabled", + "urn:x-nmos:cap:format:media_type", + "urn:x-nmos:cap:format:grain_rate", + "urn:x-nmos:cap:format:frame_width", + "urn:x-nmos:cap:format:frame_height", + "urn:x-nmos:cap:format:interlace_mode", + "urn:x-nmos:cap:format:color_sampling", + "urn:x-nmos:cap:format:component_depth", + "urn:x-nmos:cap:format:channel_count", + "urn:x-nmos:cap:format:sample_rate", + "urn:x-nmos:cap:format:sample_depth" + ] + }, + { + "pattern": "^urn:x-nmos:cap:" + } + ] + }, + "uniqueItems": true + } + } +} diff --git a/Development/third_party/is-11/v1.0.x/APIs/schemas/empty_constraints_active.json b/Development/third_party/is-11/v1.0.x/APIs/schemas/empty_constraints_active.json new file mode 100644 index 000000000..a75a5e24f --- /dev/null +++ b/Development/third_party/is-11/v1.0.x/APIs/schemas/empty_constraints_active.json @@ -0,0 +1,18 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema", + "description": "Describes empty Constraints", + "title": "Constraints", + "required": [ + "constraint_sets" + ], + "properties": { + "constraint_sets": { + "type": "array", + "items": { + "$ref": "constraint_set.json" + }, + "minItems": 0, + "maxItems": 0 + } + } +} diff --git a/Development/third_party/is-11/v1.0.x/APIs/schemas/error.json b/Development/third_party/is-11/v1.0.x/APIs/schemas/error.json new file mode 100644 index 000000000..d0db3f72b --- /dev/null +++ b/Development/third_party/is-11/v1.0.x/APIs/schemas/error.json @@ -0,0 +1,27 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema", + "type": "object", + "description": "Describes the standard error response which is returned with HTTP codes 400 and above", + "title": "Error response", + "required": [ + "code", + "error", + "debug" + ], + "properties": { + "code": { + "description": "HTTP error code", + "type": "integer", + "minimum": 400, + "maximum": 599 + }, + "error": { + "description": "Human readable message which is suitable for user interface display, and helpful to the user", + "type": "string" + }, + "debug": { + "description": "Debug information which may assist a programmer working with the API", + "type": ["null", "string"] + } + } +} diff --git a/Development/third_party/is-11/v1.0.x/APIs/schemas/input-edid-base.json b/Development/third_party/is-11/v1.0.x/APIs/schemas/input-edid-base.json new file mode 100644 index 000000000..21c6f35f3 --- /dev/null +++ b/Development/third_party/is-11/v1.0.x/APIs/schemas/input-edid-base.json @@ -0,0 +1,16 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema", + "type": "array", + "description": "Describes the Stream Compatibility Management API /inputs/{inputId}/edid base resource", + "title": "Stream Compatibility Management API /inputs/{inputId}/edid base resource", + "items": { + "type": "string", + "enum": [ + "base/", + "effective/" + ] + }, + "minItems": 2, + "maxItems": 2, + "uniqueItems": true +} diff --git a/Development/third_party/is-11/v1.0.x/APIs/schemas/input-output-base.json b/Development/third_party/is-11/v1.0.x/APIs/schemas/input-output-base.json new file mode 100644 index 000000000..3ea9f5cb3 --- /dev/null +++ b/Development/third_party/is-11/v1.0.x/APIs/schemas/input-output-base.json @@ -0,0 +1,16 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema", + "type": "array", + "description": "Describes the Stream Compatibility Management API /inputs/{inputId} and /outputs/{outputId} base resource", + "title": "Stream Compatibility Management API /inputs/{inputId} and /outputs/{outputId} base resource", + "items": { + "type": "string", + "enum": [ + "edid/", + "properties/" + ] + }, + "minItems": 2, + "maxItems": 2, + "uniqueItems": true +} diff --git a/Development/third_party/is-11/v1.0.x/APIs/schemas/input.json b/Development/third_party/is-11/v1.0.x/APIs/schemas/input.json new file mode 100644 index 000000000..0d915fb38 --- /dev/null +++ b/Development/third_party/is-11/v1.0.x/APIs/schemas/input.json @@ -0,0 +1,58 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema", + "type": "object", + "description": "Describes an Input", + "title": "Input resource", + "allOf": [ + { "$ref": "resource_core.json" }, + { + "type": "object", + "required": [ + "base_edid_support", + "connected", + "edid_support", + "status", + "device_id" + ], + "properties": { + "base_edid_support": { + "description": "Whether the Input supports Base EDID", + "type": "boolean" + }, + "connected": { + "description": "Whether the upstream counterpart of this Input is connected", + "type": "boolean" + }, + "edid_support": { + "description": "Whether the Input supports EDID", + "type": "boolean" + }, + "status": { + "description": "Status of Input", + "required": [ + "state" + ], + "properties": { + "state": { + "type": "string", + "enum": [ + "no_signal", + "awaiting_signal", + "signal_present" + ] + }, + "debug": { + "description": "Debug information which may give additional information on the state", + "type": "string" + } + } + }, + "device_id": { + "description": "Device ID which this Input forms part of", + "type": "string", + "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$" + } + } + } + ] +} diff --git a/Development/third_party/is-11/v1.0.x/APIs/schemas/output.json b/Development/third_party/is-11/v1.0.x/APIs/schemas/output.json new file mode 100644 index 000000000..0c9ccfc2b --- /dev/null +++ b/Development/third_party/is-11/v1.0.x/APIs/schemas/output.json @@ -0,0 +1,53 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema", + "type": "object", + "description": "Describes an Output", + "title": "Output resource", + "allOf": [ + { "$ref": "resource_core.json" }, + { + "type": "object", + "required": [ + "connected", + "edid_support", + "status", + "device_id" + ], + "properties": { + "connected": { + "description": "Whether the downstream counterpart of this Output is connected", + "type": "boolean" + }, + "edid_support": { + "description": "Whether the Output supports EDID", + "type": "boolean" + }, + "status": { + "description": "Status of Output", + "required": [ + "state" + ], + "properties": { + "state": { + "type": "string", + "enum": [ + "no_signal", + "default_signal", + "signal_present" + ] + }, + "debug": { + "description": "Debug information which may give additional information on the state", + "type": "string" + } + } + }, + "device_id": { + "description": "Device ID which this Output forms part of", + "type": "string", + "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$" + } + } + } + ] +} diff --git a/Development/third_party/is-11/v1.0.x/APIs/schemas/param_constraint.json b/Development/third_party/is-11/v1.0.x/APIs/schemas/param_constraint.json new file mode 100644 index 000000000..0537109c3 --- /dev/null +++ b/Development/third_party/is-11/v1.0.x/APIs/schemas/param_constraint.json @@ -0,0 +1,13 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "Describes a Parameter Constraint", + "title": "Parameter Constraint", + "type": "object", + "anyOf": [ + { "$ref": "param_constraint_string.json" }, + { "$ref": "param_constraint_integer.json" }, + { "$ref": "param_constraint_number.json" }, + { "$ref": "param_constraint_boolean.json" }, + { "$ref": "param_constraint_rational.json" } + ] +} diff --git a/Development/third_party/is-11/v1.0.x/APIs/schemas/param_constraint_boolean.json b/Development/third_party/is-11/v1.0.x/APIs/schemas/param_constraint_boolean.json new file mode 100644 index 000000000..fac6b9966 --- /dev/null +++ b/Development/third_party/is-11/v1.0.x/APIs/schemas/param_constraint_boolean.json @@ -0,0 +1,15 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "Describes a Boolean Parameter Constraint", + "title": "Boolean Parameter Constraint", + "type": "object", + "properties": { + "enum": { + "type": "array", + "minItems": 1, + "items": { + "type": "boolean" + } + } + } +} diff --git a/Development/third_party/is-11/v1.0.x/APIs/schemas/param_constraint_integer.json b/Development/third_party/is-11/v1.0.x/APIs/schemas/param_constraint_integer.json new file mode 100644 index 000000000..35c5aec23 --- /dev/null +++ b/Development/third_party/is-11/v1.0.x/APIs/schemas/param_constraint_integer.json @@ -0,0 +1,21 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "Describes an Integer Parameter Constraint", + "title": "Integer Parameter Constraint", + "type": "object", + "properties": { + "enum": { + "type": "array", + "minItems": 1, + "items": { + "type": "integer" + } + }, + "minimum": { + "type": "integer" + }, + "maximum": { + "type": "integer" + } + } +} diff --git a/Development/third_party/is-11/v1.0.x/APIs/schemas/param_constraint_number.json b/Development/third_party/is-11/v1.0.x/APIs/schemas/param_constraint_number.json new file mode 100644 index 000000000..24a93c1d6 --- /dev/null +++ b/Development/third_party/is-11/v1.0.x/APIs/schemas/param_constraint_number.json @@ -0,0 +1,21 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "Describes a Number Parameter Constraint", + "title": "Number Parameter Constraint", + "type": "object", + "properties": { + "enum": { + "type": "array", + "minItems": 1, + "items": { + "type": "number" + } + }, + "minimum": { + "type": "number" + }, + "maximum": { + "type": "number" + } + } +} diff --git a/Development/third_party/is-11/v1.0.x/APIs/schemas/param_constraint_rational.json b/Development/third_party/is-11/v1.0.x/APIs/schemas/param_constraint_rational.json new file mode 100644 index 000000000..31a8db61c --- /dev/null +++ b/Development/third_party/is-11/v1.0.x/APIs/schemas/param_constraint_rational.json @@ -0,0 +1,39 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "Describes a Rational Parameter Constraint", + "title": "Rational Parameter Constraint", + "type": "object", + "properties": { + "enum": { + "type": "array", + "minItems": 1, + "items": { + "$ref": "#/definitions/rational" + } + }, + "minimum": { + "$ref": "#/definitions/rational" + }, + "maximum": { + "$ref": "#/definitions/rational" + } + }, + "definitions": { + "rational": { + "type": "object", + "required": [ + "numerator" + ], + "properties": { + "numerator": { + "type": "integer" + }, + "denominator": { + "type": "integer", + "default": 1 + } + }, + "additionalProperties": false + } + } +} diff --git a/Development/third_party/is-11/v1.0.x/APIs/schemas/param_constraint_string.json b/Development/third_party/is-11/v1.0.x/APIs/schemas/param_constraint_string.json new file mode 100644 index 000000000..a86600836 --- /dev/null +++ b/Development/third_party/is-11/v1.0.x/APIs/schemas/param_constraint_string.json @@ -0,0 +1,15 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "Describes a String Parameter Constraint", + "title": "String Parameter Constraint", + "type": "object", + "properties": { + "enum": { + "type": "array", + "minItems": 1, + "items": { + "type": "string" + } + } + } +} diff --git a/Development/third_party/is-11/v1.0.x/APIs/schemas/receiver-base.json b/Development/third_party/is-11/v1.0.x/APIs/schemas/receiver-base.json new file mode 100644 index 000000000..a9383368f --- /dev/null +++ b/Development/third_party/is-11/v1.0.x/APIs/schemas/receiver-base.json @@ -0,0 +1,16 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema", + "type": "array", + "description": "Describes the Stream Compatibility Management API /receivers/{receiverId} base resource", + "title": "Stream Compatibility Management API /receivers/{receiverId} base resource", + "items": { + "type": "string", + "enum": [ + "outputs/", + "status/" + ] + }, + "minItems": 2, + "maxItems": 2, + "uniqueItems": true +} diff --git a/Development/third_party/is-11/v1.0.x/APIs/schemas/receiver-status.json b/Development/third_party/is-11/v1.0.x/APIs/schemas/receiver-status.json new file mode 100644 index 000000000..c14df6476 --- /dev/null +++ b/Development/third_party/is-11/v1.0.x/APIs/schemas/receiver-status.json @@ -0,0 +1,22 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema", + "type": "object", + "description": "Status of Receiver", + "required": [ + "state" + ], + "properties": { + "state": { + "type": "string", + "enum": [ + "unknown", + "compliant_stream", + "non_compliant_stream" + ] + }, + "debug": { + "description": "Debug information which may give additional information on the state", + "type": "string" + } + } +} diff --git a/Development/third_party/is-11/v1.0.x/APIs/schemas/resource-list.json b/Development/third_party/is-11/v1.0.x/APIs/schemas/resource-list.json new file mode 100644 index 000000000..8c44c9cb7 --- /dev/null +++ b/Development/third_party/is-11/v1.0.x/APIs/schemas/resource-list.json @@ -0,0 +1,11 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema", + "type": "array", + "description": "List of UUIDs", + "title": "List of resources", + "items": { + "type": "string", + "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}/$" + }, + "uniqueItems": true +} diff --git a/Development/third_party/is-11/v1.0.x/APIs/schemas/resource_core.json b/Development/third_party/is-11/v1.0.x/APIs/schemas/resource_core.json new file mode 100644 index 000000000..4eb583a89 --- /dev/null +++ b/Development/third_party/is-11/v1.0.x/APIs/schemas/resource_core.json @@ -0,0 +1,45 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "type": "object", + "description": "Describes the foundations of all NMOS resources", + "title": "Base resource", + "required": [ + "id", + "version", + "label", + "description", + "tags" + ], + "properties": { + "id": { + "description": "Globally unique identifier for the resource", + "type": "string", + "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$" + }, + "version": { + "description": "String formatted TAI timestamp (:) indicating precisely when an attribute of the resource last changed", + "type": "string", + "pattern": "^[0-9]+:[0-9]+$" + }, + "label": { + "description": "Freeform string label for the resource", + "type": "string" + }, + "description": { + "description": "Detailed description of the resource", + "type": "string" + }, + "tags": { + "description": "Key value set of freeform string tags to aid in filtering resources. Values should be represented as an array of strings. Can be empty.", + "type": "object", + "patternProperties": { + "": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + } +} \ No newline at end of file diff --git a/Development/third_party/is-11/v1.0.x/APIs/schemas/sender-base.json b/Development/third_party/is-11/v1.0.x/APIs/schemas/sender-base.json new file mode 100644 index 000000000..d68fcc2ba --- /dev/null +++ b/Development/third_party/is-11/v1.0.x/APIs/schemas/sender-base.json @@ -0,0 +1,17 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema", + "type": "array", + "description": "Describes the Stream Compatibility Management API /senders/{senderId} base resource", + "title": "Stream Compatibility Management API /senders/{senderId} base resource", + "items": { + "type": "string", + "enum": [ + "constraints/", + "inputs/", + "status/" + ] + }, + "minItems": 3, + "maxItems": 3, + "uniqueItems": true +} diff --git a/Development/third_party/is-11/v1.0.x/APIs/schemas/sender-status.json b/Development/third_party/is-11/v1.0.x/APIs/schemas/sender-status.json new file mode 100644 index 000000000..992c4da48 --- /dev/null +++ b/Development/third_party/is-11/v1.0.x/APIs/schemas/sender-status.json @@ -0,0 +1,24 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema", + "type": "object", + "description": "Status of Sender", + "required": [ + "state" + ], + "properties": { + "state": { + "type": "string", + "enum": [ + "unconstrained", + "constrained", + "active_constraints_violation", + "no_essence", + "awaiting_essence" + ] + }, + "debug": { + "description": "Debug information which may give additional information on the state", + "type": "string" + } + } +} diff --git a/Development/third_party/is-11/v1.0.x/APIs/schemas/streamcompatibility-api-base.json b/Development/third_party/is-11/v1.0.x/APIs/schemas/streamcompatibility-api-base.json new file mode 100644 index 000000000..dd99300b8 --- /dev/null +++ b/Development/third_party/is-11/v1.0.x/APIs/schemas/streamcompatibility-api-base.json @@ -0,0 +1,18 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema", + "type": "array", + "description": "Describes the Stream Compatibility Management API base resource", + "title": "Stream Compatibility Management API base resource", + "items": { + "type": "string", + "enum": [ + "inputs/", + "outputs/", + "senders/", + "receivers/" + ] + }, + "minItems": 4, + "maxItems": 4, + "uniqueItems": true +} diff --git a/Development/third_party/is-11/v1.0.x/APIs/schemas/uuid-list.json b/Development/third_party/is-11/v1.0.x/APIs/schemas/uuid-list.json new file mode 100644 index 000000000..02de49a1a --- /dev/null +++ b/Development/third_party/is-11/v1.0.x/APIs/schemas/uuid-list.json @@ -0,0 +1,10 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema", + "type": "array", + "description": "List of UUIDs", + "items": { + "type": "string", + "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$" + }, + "uniqueItems": true +} diff --git a/Documents/Architecture.md b/Documents/Architecture.md index 6b550bf05..d2c94caec 100644 --- a/Documents/Architecture.md +++ b/Documents/Architecture.md @@ -14,13 +14,15 @@ The module also provides the concept of a server which combines the REST APIs an The top-level data structures for an NMOS Node and Registry are ``nmos::node_model`` and ``nmos::registry_model`` respectively. -A ``node_model`` has four member variables which are containers, of IS-04 resources, IS-05 resources, IS-07 resources and IS-08 resources, respectively: +A ``node_model`` has six member variables which are containers, of IS-04 resources, IS-05 resources, IS-07 resources, IS-08 resources, IS-11 resources and IS-12 resources, respectively: ```C++ nmos::resources node_resources; nmos::resources connection_resources; nmos::resources events_resources; nmos::resources channelmapping_resources; +nmos::resources streamcompatibility_resources; +nmos::resources control_protocol_resources; ``` A ``registry_model`` has two containers, this time for the Registry's own Node API "self" resource, and for the resources that have been registered with the Registration API: @@ -121,9 +123,11 @@ for (;;) > [nmos/registration_api.cpp](../Development/nmos/registration_api.cpp), > [nmos/query_api.cpp](../Development/nmos/query_api.cpp), > [nmos/system_api.cpp](../Development/nmos/system_api.cpp), -> [nmos/authorization_redirect_api.cpp](../Development/nmos/authorization_redirect_api.cpp) +> [nmos/authorization_redirect_api.cpp](../Development/nmos/authorization_redirect_api.cpp), +> [nmos/streamcompatibility_api.cpp](../Development/nmos/streamcompatibility_api.cpp), +> [nmos/configuration_api.cpp](../Development/nmos/configuration_api.cpp), -The ``nmos`` module also provides the implementation of each of the REST APIs defined by AMWA IS-04, IS-05, IS-07, IS-08, IS-09 and IS-10. +The ``nmos`` module also provides the implementation of each of the REST APIs defined by AMWA IS-04, IS-05, IS-07, IS-08, IS-09, IS-10, IS-11 and IS-14. The C++ REST SDK provides a general purpose HTTP listener, that accepts requests at a particular base URL and passes them to a user-specified request handler for processing. Therefore the ``nmos`` module implements each API as a request handler which reads and/or writes the relevant parts of the NMOS data model, and provides a convenience function, ``nmos::support_api``, for associating the API request handler with the HTTP listener. diff --git a/Documents/images/IS-11-Client-Request-Callbacks.png b/Documents/images/IS-11-Client-Request-Callbacks.png new file mode 100644 index 000000000..729110e41 Binary files /dev/null and b/Documents/images/IS-11-Client-Request-Callbacks.png differ diff --git a/Documents/images/IS-11-Node-Model-Update-Callbacks.png b/Documents/images/IS-11-Node-Model-Update-Callbacks.png new file mode 100644 index 000000000..941182cbf Binary files /dev/null and b/Documents/images/IS-11-Node-Model-Update-Callbacks.png differ diff --git a/README.md b/README.md index 25967f0e8..0c3b6dac5 100644 --- a/README.md +++ b/README.md @@ -11,8 +11,9 @@ This repository contains an implementation of the [AMWA Networked Media Open Spe - [AMWA IS-08 NMOS Audio Channel Mapping Specification](https://specs.amwa.tv/is-08/) - [AMWA IS-09 NMOS System Parameters Specification](https://specs.amwa.tv/is-09/) (originally defined in JT-NM TR-1001-1:2018 Annex A) - [AMWA IS-10 NMOS Authorization Specification](https://specs.amwa.tv/is-10/) -- [AMWA IS-12 AMWA IS-12 NMOS Control Protocol](https://specs.amwa.tv/is-12/) -- [AMWA IS-14 AMWA IS-14 NMOS Device Configuration Specification](https://specs.amwa.tv/is-14/) +- [AMWA IS-11 NMOS Stream Compatibility Management](https://specs.amwa.tv/is-11/) +- [AMWA IS-12 NMOS Control Protocol](https://specs.amwa.tv/is-12/) +- [AMWA IS-14 NMOS Device Configuration Specification](https://specs.amwa.tv/is-14/) - [AMWA BCP-002-01 NMOS Grouping Recommendations - Natural Grouping](https://specs.amwa.tv/bcp-002-01/) - [AMWA BCP-002-02 NMOS Asset Distinguishing Information](https://specs.amwa.tv/bcp-002-02/) - [AMWA BCP-003-01 Secure Communication in NMOS Systems](https://specs.amwa.tv/bcp-003-01/) @@ -95,6 +96,7 @@ The [AMWA NMOS API Testing Tool](https://github.com/AMWA-TV/nmos-testing) is aut [![IS-08-02][IS-08-02-badge]][IS-08-02-sheet] [![IS-09-01][IS-09-01-badge]][IS-09-01-sheet] [![IS-09-02][IS-09-02-badge]][IS-09-02-sheet] +[![IS-11-01][IS-11-01-badge]][IS-11-01-sheet] [![IS-12-01][IS-12-01-badge]][IS-12-01-sheet] [![IS-14-01][IS-14-01-badge]][IS-14-01-sheet] @@ -113,6 +115,7 @@ The [AMWA NMOS API Testing Tool](https://github.com/AMWA-TV/nmos-testing) is aut [IS-08-02-badge]: https://raw.githubusercontent.com/sony/nmos-cpp/badges/IS-08-02.svg [IS-09-01-badge]: https://raw.githubusercontent.com/sony/nmos-cpp/badges/IS-09-01.svg [IS-09-02-badge]: https://raw.githubusercontent.com/sony/nmos-cpp/badges/IS-09-02.svg +[IS-11-01-badge]: https://raw.githubusercontent.com/sony/nmos-cpp/badges/IS-11-01.svg [IS-12-01-badge]: https://raw.githubusercontent.com/sony/nmos-cpp/badges/IS-12-01.svg [IS-14-01-badge]: https://raw.githubusercontent.com/sony/nmos-cpp/badges/IS-14-01.svg [BCP-003-01-sheet]: https://docs.google.com/spreadsheets/d/1UgZoI0lGCMDn9-zssccf2Azil3WN6jogroMT8Wh6H64/edit#gid=468090822 @@ -130,6 +133,7 @@ The [AMWA NMOS API Testing Tool](https://github.com/AMWA-TV/nmos-testing) is aut [IS-08-02-sheet]: https://docs.google.com/spreadsheets/d/1UgZoI0lGCMDn9-zssccf2Azil3WN6jogroMT8Wh6H64/edit#gid=1558470201 [IS-09-01-sheet]: https://docs.google.com/spreadsheets/d/1UgZoI0lGCMDn9-zssccf2Azil3WN6jogroMT8Wh6H64/edit#gid=919453974 [IS-09-02-sheet]: https://docs.google.com/spreadsheets/d/1UgZoI0lGCMDn9-zssccf2Azil3WN6jogroMT8Wh6H64/edit#gid=2135469955 +[IS-11-01-sheet]: https://docs.google.com/spreadsheets/d/1UgZoI0lGCMDn9-zssccf2Azil3WN6jogroMT8Wh6H64/edit?gid=1390547567 [IS-12-01-sheet]: https://docs.google.com/spreadsheets/d/1UgZoI0lGCMDn9-zssccf2Azil3WN6jogroMT8Wh6H64/edit?gid=1026699230 [IS-14-01-sheet]: https://docs.google.com/spreadsheets/d/1UgZoI0lGCMDn9-zssccf2Azil3WN6jogroMT8Wh6H64/edit?gid=342707873 @@ -139,6 +143,7 @@ The implementation is designed to be extended. Development is ongoing, following Recent activity on the project (newest first): +- Added support for IS-11 NMOS Stream Compatibility Management - Added support for IS-14 NMOS Device Configuration - Added support for BCP-008-01 Receiver Status Monitoring - Added support for BCP-008-02 Sender Status Monitoring diff --git a/Sandbox/run_nmos_testing.sh b/Sandbox/run_nmos_testing.sh index 12fa14b62..6c03fc207 100755 --- a/Sandbox/run_nmos_testing.sh +++ b/Sandbox/run_nmos_testing.sh @@ -36,6 +36,9 @@ expected_disabled_IS_08_02=0 expected_disabled_IS_09_02=0 expected_disabled_IS_04_02=0 expected_disabled_IS_09_01=0 +# IS-11 test_01_01, test_01_02, test_01_03, test_01_05, test_01_06, test_02_03_04, test_02_03_05_01, +# test_02_03_05_02, test_02_04, test_02_04_01, test_03_00, test_04_04, test_04_04_01, test_04_04_02 +expected_disabled_IS_11_01=14 expected_disabled_IS_12_01=0 expected_disabled_IS_14_01=1 expected_disabled_BCP_006_01_01=0 @@ -147,7 +150,7 @@ else (( expected_disabled_IS_07_02+=21 )) (( expected_disabled_IS_08_01+=7 )) (( expected_disabled_IS_08_02+=14 )) - # test_33, test_33_1 + (( expected_disabled_IS_11_01+=21 )) (( expected_disabled_IS_04_02+=16 )) (( expected_disabled_IS_09_01+=7 )) (( expected_disabled_IS_14_01+=7 )) @@ -220,6 +223,8 @@ do_run_test IS-08-02 $expected_disabled_IS_08_02 --host "${host}" "${host}" --po do_run_test IS-09-02 $expected_disabled_IS_09_02 --host "${host}" null --port 0 0 --version null v1.0 +do_run_test IS-11-01 $expected_disabled_IS_11_01 --host "${host}" "${host}" "${host}" --port 1080 1080 1080 --version v1.0 v1.3 v1.1 + do_run_test IS-12-01 $expected_disabled_IS_12_01 --host "${host}" "${host}" null null --port 1080 1082 0 0 --version v1.3 v1.0 v1.0 v1.0 --urlpath null x-nmos/ncp/v1.0 null null do_run_test IS-14-01 $expected_disabled_IS_14_01 --host "${host}" "${host}" null null --port 1080 1080 0 0 --version v1.3 v1.0 v1.0 v1.0 --selector null null null null --ignore test_ms05_05