diff --git a/.github/workflows/src/amwa-test.yml b/.github/workflows/src/amwa-test.yml index 89396a152..663c0b331 100644 --- a/.github/workflows/src/amwa-test.yml +++ b/.github/workflows/src/amwa-test.yml @@ -160,7 +160,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 b49f78736..f7ecb36d2 100644 --- a/Development/cmake/NmosCppLibraries.cmake +++ b/Development/cmake/NmosCppLibraries.cmake @@ -760,6 +760,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 @@ -925,6 +1014,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_handlers.cpp nmos/control_protocol_methods.cpp nmos/control_protocol_resource.cpp @@ -938,6 +1028,11 @@ set(NMOS_CPP_NMOS_SOURCES nmos/events_ws_api.cpp nmos/events_ws_client.cpp nmos/filesystem_route.cpp + nmos/streamcompatibility_api.cpp + nmos/streamcompatibility_behaviour.cpp + nmos/streamcompatibility_resources.cpp + nmos/streamcompatibility_utils.cpp + nmos/streamcompatibility_validation.cpp nmos/group_hint.cpp nmos/id.cpp nmos/lldp_handler.cpp @@ -1018,6 +1113,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_handlers.h nmos/control_protocol_methods.h nmos/control_protocol_nmos_channel_mapping_resource_type.h @@ -1036,6 +1132,11 @@ set(NMOS_CPP_NMOS_HEADERS nmos/events_ws_api.h nmos/events_ws_client.h nmos/filesystem_route.h + nmos/streamcompatibility_api.h + nmos/streamcompatibility_behaviour.h + nmos/streamcompatibility_resources.h + nmos/streamcompatibility_utils.h + nmos/streamcompatibility_validation.h nmos/format.h nmos/group_hint.h nmos/health.h @@ -1096,6 +1197,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 @@ -1197,6 +1299,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::mdns nmos-cpp::slog diff --git a/Development/cmake/NmosCppTest.cmake b/Development/cmake/NmosCppTest.cmake index ac56a57d8..763d2e40a 100644 --- a/Development/cmake/NmosCppTest.cmake +++ b/Development/cmake/NmosCppTest.cmake @@ -43,6 +43,7 @@ set(NMOS_CPP_TEST_NMOS_TEST_SOURCES nmos/test/api_utils_test.cpp nmos/test/capabilities_test.cpp nmos/test/channels_test.cpp + nmos/test/constraints_test.cpp nmos/test/control_protocol_test.cpp nmos/test/did_sdid_test.cpp nmos/test/event_type_test.cpp @@ -52,6 +53,7 @@ set(NMOS_CPP_TEST_NMOS_TEST_SOURCES nmos/test/query_api_test.cpp nmos/test/sdp_test_utils.cpp nmos/test/sdp_utils_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 f7e5216e6..116f4580d 100644 --- a/Development/nmos-cpp-node/config.json +++ b/Development/nmos-cpp-node/config.json @@ -60,6 +60,15 @@ // smpte2022_7: controls whether senders and receivers have one leg (false) or two legs (true, default) //"smpte2022_7": false, + // edid_support: controls whether inputs and output have EDID support + //"edid_support": false, + + // streamcompatibility_index: specifies index of video/audio sender/receiver for marking as IS-11 compatible + //"streamcompatibility_index": 0, + + // volatile_status_of_sender: specifies whether Status of Sender voluntarily changes (for demo purposes) + //"volatile_status_of_sender": false, + // Configuration settings and defaults for logging // error_log [registry, node]: filename for the error log or an empty string to write to stderr @@ -103,6 +112,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"], @@ -145,6 +157,7 @@ //"events_port": 3216, //"events_ws_port": 3217, //"channelmapping_port": 3215, + //"streamcompatibility_port": 3218, // system_port [node]: used to construct request URLs for the System API (if not discovered via DNS-SD) //"system_port": 10641, // control_protocol_ws_port [node]: used to construct request URLs for the Control Protocol websocket, or negative to disable the control protocol features diff --git a/Development/nmos-cpp-node/node_implementation.cpp b/Development/nmos-cpp-node/node_implementation.cpp index ab1a686ff..98a72da6c 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" @@ -43,6 +45,8 @@ #include "nmos/sdp_utils.h" #include "nmos/slog.h" #include "nmos/st2110_21_sender_type.h" +#include "nmos/streamcompatibility_resources.h" +#include "nmos/streamcompatibility_validation.h" #include "nmos/system_resources.h" #include "nmos/transfer_characteristic.h" #include "nmos/transport.h" @@ -120,6 +124,15 @@ namespace impl // smpte2022_7: controls whether senders and receivers have one leg (false) or two legs (true, default) const web::json::field_as_bool_or smpte2022_7{ U("smpte2022_7"), true }; + + // edid_support: controls whether inputs and output have EDID support + const web::json::field_as_bool_or edid_support{ U("edid_support"), false }; + + // streamcompatibility_index: specifies index of video/audio sender/receiver for marking as IS-11 compatible + const web::json::field_as_integer_or streamcompatibility_index{ U("streamcompatibility_index"), 0 }; + + // volatile_status_of_sender: specifies whether Status of Sender voluntarily changes (for demo purposes) + const web::json::field_as_bool_or volatile_status_of_sender{ U("volatile_status_of_sender"), false }; } nmos::interlace_mode get_interlace_mode(const nmos::settings& settings); @@ -188,6 +201,19 @@ namespace impl const auto temperature_Celsius = nmos::event_types::measurement(U("temperature"), U("C")); const auto temperature_wildcard = nmos::event_types::measurement(U("temperature"), nmos::event_types::wildcard); const auto catcall = nmos::event_types::named_enum(nmos::event_types::number, U("caterwaul")); + + 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 @@ -266,6 +292,8 @@ 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); + const auto streamcompatibility_index = impl::fields::streamcompatibility_index(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 @@ -921,6 +949,108 @@ 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 + { + 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 }); + + 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)) return; + + 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 (const auto& port : { impl::ports::video, impl::ports::audio }) + { + const auto sender_id = impl::make_id(seed_id, nmos::types::sender, port, streamcompatibility_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)) return; + } + } + + // Example IS-11 Output and Receivers + { + 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 }); + + 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)) return; + + 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)) return; + } + } + // 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 @@ -1299,6 +1429,9 @@ void node_implementation_run(nmos::node_model& model, slog::base_gate& gate) const auto sender_ports = impl::parse_ports(impl::fields::senders(model.settings)); const auto ws_sender_ports = boost::copy_range>(sender_ports | boost::adaptors::filtered(impl::is_ws_port)); + const auto streamcompatibility_index = impl::fields::streamcompatibility_index(model.settings); + const auto update_sender = impl::fields::volatile_status_of_sender(model.settings); + // start background tasks to intermittently update the state of the event sources, to cause events to be emitted to connected receivers nmos::details::seed_generator events_seeder; @@ -1307,10 +1440,10 @@ void node_implementation_run(nmos::node_model& model, slog::base_gate& gate) auto cancellation_source = pplx::cancellation_token_source(); auto token = cancellation_source.get_token(); - auto events = pplx::do_while([&model, seed_id, how_many, ws_sender_ports, events_engine, &gate, token] + auto events = pplx::do_while([&model, seed_id, how_many, ws_sender_ports, streamcompatibility_index, update_sender, 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, ws_sender_ports, events_engine, &gate] + return pplx::complete_after(std::chrono::milliseconds(std::chrono::milliseconds::rep(1000 * event_interval)), token).then([&model, seed_id, how_many, ws_sender_ports, streamcompatibility_index, update_sender, events_engine, &gate] { auto lock = model.write_lock(); @@ -1380,6 +1513,25 @@ void node_implementation_run(nmos::node_model& model, slog::base_gate& gate) slog::log(gate, SLOG_FLF) << "Temperature updated: " << temp.scaled_value() << " (" << impl::temperature_Celsius.name << ")"; + if (update_sender) + { + const auto streamcompatibility_video_sender_id = impl::make_id(seed_id, nmos::types::sender, impl::ports::video, streamcompatibility_index); + const auto states_of_sender = { U("no_essence"), U("awaiting_essence"), U("constrained") }; + const auto& state_of_sender = *(states_of_sender.begin() + (std::min)(std::geometric_distribution()(*events_engine), states_of_sender.size() - 1)); + + modify_resource(model.streamcompatibility_resources, streamcompatibility_video_sender_id, [&](nmos::resource& resource) + { + resource.data[nmos::fields::status] = web::json::value_of({ { nmos::fields::state, state_of_sender } }); + }); + + modify_resource(model.node_resources, streamcompatibility_video_sender_id, [&](nmos::resource& resource) + { + resource.data[nmos::fields::version] = web::json::value::string(nmos::make_version()); + }); + + slog::log(gate, SLOG_FLF) << "Status of Sender " << streamcompatibility_video_sender_id << " updated: " << state_of_sender; + } + model.notify(); return true; @@ -1445,28 +1597,14 @@ nmos::transport_file_parser make_node_implementation_transport_file_parser() // (if this callback is specified, an 'empty' std::function is not allowed) return [](const nmos::resource& receiver, const nmos::resource& connection_receiver, const utility::string_t& transport_file_type, const utility::string_t& transport_file_data, slog::base_gate& gate) { - 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); - } - }; - return nmos::details::parse_rtp_transport_file(validate_sdp_parameters, receiver, connection_receiver, transport_file_type, transport_file_data, gate); + return nmos::details::parse_rtp_transport_file(impl::validate_sdp_parameters, receiver, connection_receiver, transport_file_type, transport_file_data, gate); }; } // 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 @@ -1696,6 +1834,226 @@ nmos::channelmapping_activation_handler make_node_implementation_channelmapping_ }; } +// Example Stream Compatibility Management API Base EDID update callback to perform application-specific operations to apply updated Base EDID +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) + { + if (base_edid) + { + slog::log(gate, SLOG_FLF) << "Base EDID updated for Input " << input_id; + } + else + { + slog::log(gate, SLOG_FLF) << "Base EDID deleted for Input " << input_id; + } + }; +} + +// Example Stream Compatibility Management API callback to update Effective EDID - captures streamcompatibility_resources by reference! +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 = bst::nullopt; + + const std::pair id_type{ input_id, nmos::types::input }; + const auto streamcompatibility_input = find_resource(streamcompatibility_resources, id_type); + if (streamcompatibility_resources.end() != streamcompatibility_input) + { + 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) + { + slog::log(gate, SLOG_FLF) + << "Intersection of Sender Caps and Active Constraints for Sender " << sender_id << " is " + << utility::us2s(nmos::fields::intersection_of_caps_and_constraints(streamcompatibility_sender->data).serialize()); + } + } + + bool adjust_to_caps = nmos::fields::adjust_to_caps(streamcompatibility_input->data); + slog::log(gate, SLOG_FLF) << "adjust_to_caps is " << adjust_to_caps; + + 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 = edid_binary.as_string(); + } + } + } + else + { + slog::log(gate, SLOG_FLF) << "Requested " << id_type << " not found"; + return; + } + + if (base_edid) + { + effective_edid = *base_edid; + } + else + { + effective_edid = utility::string_t(edid_bytes, edid_bytes + sizeof(edid_bytes)); + } + + slog::log(gate, SLOG_FLF) << "Effective EDID is set for Input " << input_id; + }; +} + +// Example Stream Compatibility Management API callback to update Active Constraints of a Sender +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 true; + } + + 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 : throw std::logic_error("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"; + return false; + } + + intersection = value_from_elements(v); + return true; + }; +} + +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)); + + // this example uses a custom sender resources validator to handle video/jxsv in addition to the core media types + // (if this callback is specified, an 'empty' std::function is not allowed) + 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)) + { + std::pair res = validate_video_jxsv_sender_resources(source, flow, sender, connection_sender, constraint_sets); + return res; + } + 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); + } + }; +} + +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) { @@ -1855,6 +2213,10 @@ 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) { + 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)) @@ -1862,11 +2224,16 @@ 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_base_edid_changed(make_node_implementation_streamcompatibility_base_edid_handler(gate)) + .on_set_effective_edid(set_effective_edid) // may be omitted if 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 } diff --git a/Development/nmos/api_utils.h b/Development/nmos/api_utils.h index 0f8b132a3..0bdf26d71 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")); // API version pattern const route_pattern version = make_route_pattern(U("version"), U("v[0-9]+\\.[0-9]+")); @@ -87,6 +89,12 @@ namespace nmos const route_pattern outputSubroute = make_route_pattern(U("outputSubroute"), U("properties|sourceid|channels|caps")); const route_pattern activationId = make_route_pattern(U("activationId"), U("[a-zA-Z0-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}")); } 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 7374c5a90..063a4edc4 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 d24f2a5df..28fbdb2b0 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/type.h" @@ -152,6 +154,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 = {}) @@ -372,6 +391,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; @@ -403,6 +440,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()); return result; } @@ -495,6 +533,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 57cb0996b..ddd305409 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 2d6d8d232..ea908a36e 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/media_type.h" #include "nmos/resource.h" @@ -110,6 +111,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 00258389d..c8a54e1dd 100644 --- a/Development/nmos/node_server.cpp +++ b/Development/nmos/node_server.cpp @@ -16,6 +16,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 { @@ -74,6 +76,9 @@ namespace nmos node_server.api_routers[{ {}, nmos::fields::channelmapping_port(node_model.settings) }].mount({}, nmos::make_channelmapping_api(node_model, node_implementation.validate_map, validate_authorization ? validate_authorization(nmos::experimental::scopes::channelmapping) : nullptr, 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.base_edid_changed, node_implementation.set_effective_edid, node_implementation.active_constraints_changed, 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); @@ -142,12 +147,15 @@ namespace nmos auto connection_activated = node_implementation.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] { nmos::connection_activation_thread(node_model, resolve_auto, set_transportfile, 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 25a15d4b7..63b71aa06 100644 --- a/Development/nmos/node_server.h +++ b/Development/nmos/node_server.h @@ -8,6 +8,9 @@ #include "nmos/connection_api.h" #include "nmos/connection_activation.h" #include "nmos/control_protocol_handlers.h" +#include "nmos/streamcompatibility_api.h" +#include "nmos/streamcompatibility_behaviour.h" +#include "nmos/streamcompatibility_validation.h" #include "nmos/node_behaviour.h" #include "nmos/node_system_behaviour.h" #include "nmos/ocsp_response_handler.h" @@ -56,6 +59,8 @@ namespace nmos // (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; } @@ -78,6 +83,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_base_edid_changed(nmos::experimental::details::streamcompatibility_base_edid_handler base_edid_changed) { this->base_edid_changed = std::move(base_edid_changed); 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 active_constraints_changed) { this->active_constraints_changed = std::move(active_constraints_changed); 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; } @@ -120,6 +130,12 @@ namespace nmos save_authorization_client_handler save_authorization_client; request_authorization_code_handler request_authorization_code; + nmos::experimental::details::streamcompatibility_base_edid_handler base_edid_changed; + nmos::experimental::details::streamcompatibility_effective_edid_setter set_effective_edid; + nmos::experimental::details::streamcompatibility_active_constraints_handler active_constraints_changed; + 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 25d65004e..e28338af8 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") }; } @@ -42,6 +44,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; } return{}; } diff --git a/Development/nmos/sdp_utils.cpp b/Development/nmos/sdp_utils.cpp index 22f693b9a..b1e6ae200 100644 --- a/Development/nmos/sdp_utils.cpp +++ b/Development/nmos/sdp_utils.cpp @@ -1613,4 +1613,13 @@ namespace nmos { details::validate_sdp_parameters(details::format_constraints, sdp_params, details::get_format(sdp_params), details::get_format_parameters(sdp_params), receiver); } + + // 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 c4ce30e68..09b452377 100644 --- a/Development/nmos/sdp_utils.h +++ b/Development/nmos/sdp_utils.h @@ -62,6 +62,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); + // Format-specific types struct video_raw_parameters; diff --git a/Development/nmos/settings.cpp b/Development/nmos/settings.cpp index 5608fbcac..929665086 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 c0981f8bf..0f475d26d 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 @@ -147,6 +150,7 @@ namespace nmos const web::json::field_as_integer_or events_port{ U("events_port"), 3216 }; const web::json::field_as_integer_or events_ws_port{ U("events_ws_port"), 3217 }; const web::json::field_as_integer_or channelmapping_port{ U("channelmapping_port"), 3215 }; + const web::json::field_as_integer_or streamcompatibility_port{ U("streamcompatibility_port"), 3218 }; // system_port [node]: used to construct request URLs for the System API (if not discovered via DNS-SD) const web::json::field_as_integer_or system_port{ U("system_port"), 10641 }; // control_protocol_ws_port [node]: used to construct request URLs for the Control Protocol websocket, or negative to disable the control protocol features diff --git a/Development/nmos/streamcompatibility_api.cpp b/Development/nmos/streamcompatibility_api.cpp new file mode 100644 index 000000000..8db28f098 --- /dev/null +++ b/Development/nmos/streamcompatibility_api.cpp @@ -0,0 +1,907 @@ +#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_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"; + + nmos::set_error_reply(res, web::http::status_codes::NoContent); + } + } + else + { + slog::log(gate, SLOG_FLF) << "EDID requested for " << id_type << " does not exist"; + + nmos::set_error_reply(res, web::http::status_codes::NoContent); + } + } + + // it's expected that read lock is already catched for the model + 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 write lock is already catched for the model and an input with the resource_id exists + void update_effective_edid(nmos::node_model& model, const streamcompatibility_effective_edid_setter& effective_edid_setter, const utility::string_t resource_id) + { + boost::variant effective_edid; + + effective_edid_setter(resource_id, effective_edid); + + utility::string_t updated_timestamp; + + modify_resource(model.streamcompatibility_resources, resource_id, [&effective_edid, &updated_timestamp](nmos::resource& input) + { + input.data[nmos::fields::endpoint_effective_edid] = boost::apply_visitor(edid_file_visitor(), effective_edid); + + updated_timestamp = nmos::make_version(); + input.data[nmos::fields::version] = web::json::value::string(updated_timestamp); + }); + + const std::pair id_type{ resource_id, nmos::types::input }; + auto resource = find_resource(model.streamcompatibility_resources, id_type); + + update_version(model.node_resources, nmos::fields::senders(resource->data), updated_timestamp); + } + + // it's expected that write lock is already catched for the model and IS-11 sender exists + void set_active_constraints(nmos::node_model& model, 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(model.streamcompatibility_resources, sender_id_type); + auto matching_resource = find_resource(model.node_resources, sender_id_type); + if (model.node_resources.end() == matching_resource) + { + throw std::logic_error("matching IS-04 resource 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(model.streamcompatibility_resources, input_id_type); + if (model.streamcompatibility_resources.end() != input) + { + if (!all_resources_exist(model.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; + + // Update Active Constraints in streamcompatibility_resources + modify_resource(model.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); + + updated_timestamp = nmos::make_version(); + sender.data[nmos::fields::version] = web::json::value::string(updated_timestamp); + }); + + update_version(model.node_resources, sender_id, updated_timestamp); + + if (effective_edid_setter) + { + for (const auto& input_id : nmos::fields::inputs(resource->data)) + { + details::update_effective_edid(model, effective_edid_setter, input_id.as_string()); + } + } + + model.notify(); + } + } + + web::http::experimental::listener::api_router make_unmounted_streamcompatibility_api(nmos::node_model& model, details::streamcompatibility_base_edid_handler base_edid_handler, details::streamcompatibility_effective_edid_setter effective_edid_setter, details::streamcompatibility_active_constraints_handler active_constraints_handler, slog::base_gate& gate); + + web::http::experimental::listener::api_router make_streamcompatibility_api(nmos::node_model& model, details::streamcompatibility_base_edid_handler base_edid_handler, 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) + { + 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, base_edid_handler, effective_edid_setter, active_constraints_handler, gate)); + + return streamcompatibility_api; + } + + web::http::experimental::listener::api_router make_unmounted_streamcompatibility_api(nmos::node_model& model, details::streamcompatibility_base_edid_handler base_edid_handler, details::streamcompatibility_effective_edid_setter effective_edid_setter, details::streamcompatibility_active_constraints_handler active_constraints_handler, 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) + { + 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 resource 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 if (nmos::details::is_erased_resource(resources, id_type)) + { + set_error_reply(res, status_codes::NotFound, U("Not Found; ") + nmos::details::make_erased_resource_error()); + } + 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) + { + auto matching_resource = find_resource(model.node_resources, id_type); + if (model.node_resources.end() == matching_resource) + { + throw std::logic_error("matching IS-04 resource 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 if (nmos::details::is_erased_resource(resources, id_type)) + { + set_error_reply(res, status_codes::NotFound, U("Not Found; ") + nmos::details::make_erased_resource_error()); + } + 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) + { + auto matching_resource = find_resource(model.node_resources, id_type); + if (model.node_resources.end() == matching_resource) + { + throw std::logic_error("matching IS-04 resource not found"); + } + + set_reply(res, status_codes::OK, nmos::fields::status(resource->data)); + } + else if (nmos::details::is_erased_resource(resources, id_type)) + { + set_error_reply(res, status_codes::NotFound, U("Not Found; ") + nmos::details::make_erased_resource_error()); + } + 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) + { + auto matching_resource = find_resource(model.node_resources, id_type); + if (model.node_resources.end() == matching_resource) + { + throw std::logic_error("matching IS-04 resource 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 if (nmos::details::is_erased_resource(resources, id_type)) + { + set_error_reply(res, status_codes::NotFound, U("Not Found; ") + nmos::details::make_erased_resource_error()); + } + 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) + { + auto matching_resource = find_resource(model.node_resources, id_type); + if (model.node_resources.end() == matching_resource) + { + throw std::logic_error("matching IS-04 resource 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 { + set_reply(res, status_codes::NotFound); + } + } + else if (nmos::details::is_erased_resource(resources, id_type)) + { + set_error_reply(res, status_codes::NotFound, U("Not Found; ") + nmos::details::make_erased_resource_error()); + } + 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; + + // EDID endpoints hold information about 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); + } + + set_reply(res, status_codes::OK, data); + } + else if (nmos::details::is_erased_resource(resources, id_type)) + { + set_error_reply(res, status_codes::NotFound, U("Not Found; ") + nmos::details::make_erased_resource_error()); + } + 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) << "EDID requested for " << id_type; + + details::set_edid_endpoint_as_reply(res, id_type, edid_endpoint, gate); + } + else { + set_reply(res, status_codes::NotImplemented); + } + } + else if (nmos::details::is_erased_resource(resources, id_type)) + { + set_error_reply(res, status_codes::NotFound, U("Not Found; ") + nmos::details::make_erased_resource_error()); + } + 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) << "EDID requested for " << id_type << ": " << edidType; + + details::set_edid_endpoint_as_reply(res, id_type, edid_endpoint, gate); + } + else if (nmos::details::is_erased_resource(resources, id_type)) + { + set_error_reply(res, status_codes::NotFound, U("Not Found; ") + nmos::details::make_erased_resource_error()); + } + 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, base_edid_handler, 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& resources = model.streamcompatibility_resources; + + const string_t resourceId = parameters.at(nmos::patterns::resourceId.name); + + const std::pair id_type{ resourceId, nmos::types::input }; + auto resource = find_resource(resources, id_type); + + // 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")) + { + adjust_to_caps = true; + } + } + + 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)) + { + const auto request_body = req.content_ready().get().extract_vector().get(); + const utility::string_t base_edid{ request_body.begin(), request_body.end() }; + + slog::log(gate, SLOG_FLF) << "Base EDID update is requested for " << id_type << " with file size " << base_edid.size(); + + if (base_edid_handler) + { + base_edid_handler(resourceId, base_edid); + } + + // 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(resource->data), nmos::types::sender)) + { + throw std::logic_error("associated IS-04 sender not found"); + } + } + + utility::string_t updated_timestamp; + + // Update Base EDID in streamcompatibility_resources + modify_resource(resources, resourceId, [&base_edid, &updated_timestamp, adjust_to_caps](nmos::resource& input) + { + input.data[nmos::fields::endpoint_base_edid] = make_streamcompatibility_edid_endpoint(base_edid); + input.data[nmos::fields::adjust_to_caps] = value::boolean(adjust_to_caps); + + updated_timestamp = nmos::make_version(); + input.data[nmos::fields::version] = web::json::value::string(updated_timestamp); + }); + + update_version(model.node_resources, nmos::fields::senders(resource->data), updated_timestamp); + + if (effective_edid_setter) + { + details::update_effective_edid(model, effective_edid_setter, resourceId); + } + + model.notify(); + + set_reply(res, status_codes::NoContent); + } + else + { + slog::log(gate, SLOG_FLF) << "Base EDID update is requested for " << id_type << " but this operation is locked"; + + set_error_reply(res, status_codes::Locked); + } + } + else + { + slog::log(gate, SLOG_FLF) << "Base EDID update is requested for " << id_type << " but this input is not configured to allow it"; + + set_error_reply(res, status_codes::MethodNotAllowed); + } + } + else if (nmos::details::is_erased_resource(resources, id_type)) + { + set_error_reply(res, status_codes::NotFound, U("Not Found; ") + nmos::details::make_erased_resource_error()); + } + 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, base_edid_handler, 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& resources = model.streamcompatibility_resources; + + const string_t resourceId = parameters.at(nmos::patterns::resourceId.name); + + const std::pair id_type{ resourceId, 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)) + { + slog::log(gate, SLOG_FLF) << "Base EDID deletion is requested for " << id_type; + + if (base_edid_handler) + { + base_edid_handler(resourceId, bst::nullopt); + } + + // 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(resource->data), nmos::types::sender)) + { + throw std::logic_error("associated IS-04 sender not found"); + } + } + + utility::string_t updated_timestamp; + + modify_resource(resources, resourceId, [&updated_timestamp](nmos::resource& input) + { + input.data[nmos::fields::endpoint_base_edid] = make_streamcompatibility_dummy_edid_endpoint(); + + updated_timestamp = nmos::make_version(); + input.data[nmos::fields::version] = web::json::value::string(updated_timestamp); + }); + + update_version(model.node_resources, nmos::fields::senders(resource->data), updated_timestamp); + + if (effective_edid_setter) + { + details::update_effective_edid(model, effective_edid_setter, resourceId); + } + + model.notify(); + + set_reply(res, status_codes::NoContent); + } + else + { + slog::log(gate, SLOG_FLF) << "Base EDID deletion is requested for " << id_type << " but this operation is locked"; + + set_error_reply(res, status_codes::Locked); + } + } + else + { + slog::log(gate, SLOG_FLF) << "Base EDID deletion is requested for " << id_type << " but this input is not configured to allow it"; + + set_error_reply(res, status_codes::MethodNotAllowed); + } + } + else if (nmos::details::is_erased_resource(resources, id_type)) + { + set_error_reply(res, status_codes::NotFound, U("Not Found; ") + nmos::details::make_erased_resource_error()); + } + 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, active_constraints_handler, 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, &active_constraints_handler, &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() }; + })); + + if (details::validate_constraint_sets(nmos::fields::constraint_sets(data).as_array(), supported_param_constraints)) + { + slog::log(gate, SLOG_FLF) << "Active Constraints update is requested for " << id_type; + + bool can_adhere = true; + web::json::value intersection = web::json::value::array(); + + if (active_constraints_handler) + { + if (!active_constraints_handler(*streamcompatibility_sender, data, intersection)) + { + can_adhere = false; + + slog::log(gate, SLOG_FLF) << "Active Constraints update is requested for " << id_type << " but this sender can't adhere to these Constraints"; + set_error_reply(res, status_codes::UnprocessableEntity); + } + } + + if (can_adhere) + { + details::set_active_constraints(model, resourceId, nmos::fields::constraint_sets(data), intersection, effective_edid_setter); + set_reply(res, status_codes::OK, data); + } + } + else + { + set_error_reply(res, status_codes::BadRequest, U("The requested Constraint Set uses Parameter Constraints unsupported by this Sender.")); + } + } + else + { + slog::log(gate, SLOG_FLF) << "Active Constraints update is requested for " << id_type << " but this operation is locked"; + set_error_reply(res, status_codes::Locked); + } + } + else if (nmos::details::is_erased_resource(resources, id_type)) + { + set_error_reply(res, status_codes::NotFound, U("Not Found; ") + nmos::details::make_erased_resource_error()); + } + 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, active_constraints_handler, &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) << "Active Constraints deletion is requested for " << id_type; + + const auto active_constraints = web::json::value_of({ + { nmos::fields::constraint_sets, web::json::value::array() } + }); + web::json::value intersection = web::json::value::array(); + + active_constraints_handler(*streamcompatibility_sender, active_constraints, intersection); + details::set_active_constraints(model, resourceId, nmos::fields::constraint_sets(active_constraints), intersection, effective_edid_setter); + + 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) << "Active Constraints update is requested for " << id_type << " but this operation is locked"; + set_error_reply(res, status_codes::Locked); + } + } + else if (nmos::details::is_erased_resource(resources, id_type)) + { + set_error_reply(res, status_codes::NotFound, U("Not Found; ") + nmos::details::make_erased_resource_error()); + } + 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..b49ac37af --- /dev/null +++ b/Development/nmos/streamcompatibility_api.h @@ -0,0 +1,44 @@ +#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 changed (PUT or DELETEd) + // it can be used to perform any final validation of the specified Base EDID + // when PUT, it may throw web::json::json_exception, which will be mapped to a 400 Bad Request status code with NMOS error "debug" information including the exception message + // or std::runtime_error, which will be mapped to a 500 Internal Error status code with NMOS error "debug" information including the exception message + // when DELETE, this callback should not throw exceptions, as the Base EDID will already have been changed and those changes will not be rolled back + typedef std::function& 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 changed (PUT or DELETEd) + // it can be used to perform any final validation of the specified Active Constraints + // it may throw web::json::json_exception, which will be mapped to a 400 Bad Request status code with NMOS error "debug" information including the exception message + // or std::runtime_error, which will be mapped to a 500 Internal Error status code with NMOS error "debug" information including the exception message + typedef std::function 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 base_edid_handler, 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..900064d4b --- /dev/null +++ b/Development/nmos/streamcompatibility_behaviour.cpp @@ -0,0 +1,255 @@ +#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 + { + 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::receiver_state update_status_of_receiver(nmos::node_model& model, const nmos::id& receiver_id, const details::streamcompatibility_receiver_validator& validate_receiver, slog::base_gate& gate) + { + using web::json::value; + + auto& node_resources = model.node_resources; + auto& connection_resources = model.connection_resources; + auto& streamcompatibility_resources = model.streamcompatibility_resources; + + const std::pair receiver_id_type{ receiver_id, nmos::types::receiver }; + + auto node_receiver = find_resource(node_resources, receiver_id_type); + if (node_resources.end() == node_receiver) throw std::logic_error("Matching IS-04 receiver not found"); + + 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"); + + nmos::receiver_state receiver_state(nmos::receiver_states::unknown); + utility::string_t receiver_state_debug; + + 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)); + + if (validate_receiver) + { + std::tie(receiver_state, receiver_state_debug) = validate_receiver(*node_receiver, transport_file); + } + } + + if (nmos::fields::state(nmos::fields::status(streamcompatibility_receiver->data)) != receiver_state.name) + { + modify_resource(streamcompatibility_resources, receiver_id, [&receiver_state, &receiver_state_debug, &gate](nmos::resource& receiver) + { + nmos::fields::status(receiver.data)[nmos::fields::state] = value::string(receiver_state.name); + if (!receiver_state_debug.empty()) + { + nmos::fields::status(receiver.data)[nmos::fields::debug] = value::string(receiver_state_debug); + } + else if (!nmos::fields::debug(nmos::fields::status(receiver.data)).empty()) + { + nmos::fields::status(receiver.data).erase(nmos::fields::debug); + } + }); + + update_version(node_resources, receiver_id, nmos::make_version()); + } + + 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; + using web::json::value_of; + + 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; + + auto streamcompatibility_senders_ids = get_resources_ids(streamcompatibility_resources, nmos::types::sender); + auto streamcompatibility_receivers_ids = get_resources_ids(streamcompatibility_resources, nmos::types::receiver); + + // find Senders with recently updated IS-11 properties, associated Flow or Source + for (const nmos::id& sender_id : streamcompatibility_senders_ids) + { + try + { + bool updated = false; + + const std::pair sender_id_type{ sender_id, nmos::types::sender }; + 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"); + + updated = most_recent_update < sender->updated || + most_recent_update < flow->updated || + most_recent_update < source->updated; + + if (updated) + { + slog::log(gate, SLOG_FLF) << "Sender " << sender_id << " or its Flow or Source has been updated recently and the state of this Sender is being updated as well"; + + 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"); + + 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"); + + nmos::sender_state sender_state(nmos::fields::state(nmos::fields::status(streamcompatibility_sender->data))); + utility::string_t sender_state_debug; + + // 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) << "Sender " << sender_id << " is being validated with its Flow, Source and transport file"; + if (validate_sender_resources) + { + std::tie(sender_state, sender_state_debug) = validate_sender_resources(*source, *flow, *sender, *connection_sender, constraint_sets); + } + } + + if (nmos::fields::state(nmos::fields::status(streamcompatibility_sender->data)) != sender_state.name) + { + utility::string_t updated_timestamp; + + modify_resource(streamcompatibility_resources, sender_id, [&sender_state, &sender_state_debug, &updated_timestamp, &gate](nmos::resource& sender) + { + nmos::fields::status(sender.data)[nmos::fields::state] = web::json::value::string(sender_state.name); + if (!sender_state_debug.empty()) + { + nmos::fields::status(sender.data)[nmos::fields::debug] = web::json::value::string(sender_state_debug); + } + else if (!nmos::fields::debug(nmos::fields::status(sender.data)).empty()) + { + nmos::fields::status(sender.data).erase(nmos::fields::debug); + } + + updated_timestamp = nmos::make_version(); + sender.data[nmos::fields::version] = web::json::value::string(updated_timestamp); + }); + + update_version(node_resources, sender_id, updated_timestamp); + } + + if (sender_state == nmos::sender_states::active_constraints_violation) + { + slog::log(gate, SLOG_FLF) << "Stopping Sender " << sender->id; + web::json::value merged; + nmos::modify_resource(connection_resources, sender_id, [&merged](nmos::resource& connection_resource) + { + connection_resource.data[nmos::fields::version] = web::json::value::string(nmos::make_version()); + merged = nmos::fields::endpoint_staged(connection_resource.data); + 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; + }); + + nmos::details::handle_immediate_activation_pending(model, lock, sender_id_type, merged[nmos::fields::activation], gate); + } + } + } + catch (const std::exception& e) + { + slog::log(gate, SLOG_FLF) << "An exception appeared while updating the state of Sender " << sender_id << ": " << e.what(); + continue; + } + } + + // find IS-11 Receivers with recently updated "caps" to check whether the active transport file still satisfies them + for (const nmos::id& receiver_id : streamcompatibility_receivers_ids) + { + try + { + bool updated = false; + + const std::pair receiver_id_type{ receiver_id, nmos::types::receiver }; + auto receiver = find_resource(node_resources, receiver_id_type); + if (node_resources.end() == receiver) throw std::logic_error("Matching IS-04 receiver not found"); + + updated = most_recent_update < receiver->updated; + if (updated) + { + slog::log(gate, SLOG_FLF) << "Receiver " << receiver_id << " has been updated recently and Status of Receiver is being updated as well"; + + nmos::receiver_state receiver_state = update_status_of_receiver(model, receiver_id, validate_receiver, gate); + + if (receiver_state == nmos::receiver_states::non_compliant_stream) + { + slog::log(gate, SLOG_FLF) << "Stopping Receiver " << receiver->id; + web::json::value merged; + nmos::modify_resource(connection_resources, receiver_id, [&merged](nmos::resource& connection_resource) + { + connection_resource.data[nmos::fields::version] = web::json::value::string(nmos::make_version()); + merged = nmos::fields::endpoint_staged(connection_resource.data); + 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; + }); + + nmos::details::handle_immediate_activation_pending(model, lock, receiver_id_type, merged[nmos::fields::activation], gate); + } + } + } + catch (const std::exception& e) + { + slog::log(gate, SLOG_FLF) << "Updating Status of Receiver for " << receiver_id << " raised exception: " << e.what(); + continue; + } + } + + model.notify(); + 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_resources.cpp b/Development/nmos/streamcompatibility_resources.cpp new file mode 100644 index 000000000..fa5ae5e88 --- /dev/null +++ b/Development/nmos/streamcompatibility_resources.cpp @@ -0,0 +1,176 @@ +#include "nmos/streamcompatibility_resources.h" + +#include +#include "nmos/capabilities.h" // for nmos::fields::constraint_sets +#include "nmos/is11_versions.h" +#include "nmos/resource.h" +#include "nmos/streamcompatibility_state.h" + +namespace nmos +{ + namespace experimental + { + 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 } + }); + } + + 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") }, + { 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, false }; + } + + 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") }, + { 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, false }; + } + + web::json::value make_streamcompatibility_dummy_edid_endpoint(bool locked) + { + using web::json::value_of; + + return value_of({ + { nmos::fields::temporarily_locked, locked }, + }); + } + + web::json::value make_streamcompatibility_edid_endpoint(const web::uri& edid_file, bool locked) + { + using web::json::value_of; + + return value_of({ + { nmos::fields::edid_href, edid_file.to_string() }, + { nmos::fields::temporarily_locked, locked }, + }); + } + + web::json::value make_streamcompatibility_edid_endpoint(const utility::string_t& edid_file, bool locked) + { + using web::json::value_of; + + return value_of({ + { nmos::fields::edid_binary, edid_file }, + { nmos::fields::temporarily_locked, 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 = 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; + } + + 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, false }; + } + + 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_dummy_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, false }; + } + + 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, false }; + } + + 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, false }; + } + } +} diff --git a/Development/nmos/streamcompatibility_resources.h b/Development/nmos/streamcompatibility_resources.h new file mode 100644 index 000000000..2beabb8f7 --- /dev/null +++ b/Development/nmos/streamcompatibility_resources.h @@ -0,0 +1,54 @@ +#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 + { + web::json::value make_streamcompatibility_active_constraints_endpoint(const web::json::value& constraint_sets, bool locked = false); + + 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); + + // Makes a dummy EDID endpoint to show that an input supports EDID of this type but it currently has no value + web::json::value make_streamcompatibility_dummy_edid_endpoint(bool locked = false); + web::json::value make_streamcompatibility_edid_endpoint(const web::uri& edid_file, bool locked = false); + web::json::value make_streamcompatibility_edid_endpoint(const utility::string_t& edid_file, bool locked = false); + + // 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); + + struct edid_file_visitor : public boost::static_visitor + { + web::json::value operator()(utility::string_t edid_file) const + { + return make_streamcompatibility_edid_endpoint(edid_file); + } + + web::json::value operator()(web::uri edid_file) const + { + return make_streamcompatibility_edid_endpoint(edid_file); + } + }; + } +} + +#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..9f04318b1 --- /dev/null +++ b/Development/nmos/streamcompatibility_utils.cpp @@ -0,0 +1,25 @@ +#include "nmos/streamcompatibility_utils.h" + +namespace nmos +{ + namespace experimental + { + // it's expected that write lock is already catched for the model + void update_version(nmos::resources& resources, const nmos::id& resource_id, const utility::string_t& new_version) + { + modify_resource(resources, resource_id, [&new_version](nmos::resource& resource) + { + resource.data[nmos::fields::version] = web::json::value::string(new_version); + }); + } + + // it's expected that write lock is already catched for the model + void update_version(nmos::resources& resources, const web::json::array& resource_ids, const utility::string_t& new_version) + { + for (const auto& resource_id : resource_ids) + { + update_version(resources, resource_id.as_string(), new_version); + } + } + } +} diff --git a/Development/nmos/streamcompatibility_utils.h b/Development/nmos/streamcompatibility_utils.h new file mode 100644 index 000000000..fedcb8ea3 --- /dev/null +++ b/Development/nmos/streamcompatibility_utils.h @@ -0,0 +1,11 @@ +#include "nmos/id.h" +#include "nmos/resources.h" + +namespace nmos +{ + namespace experimental + { + void update_version(nmos::resources& resources, const nmos::id& resource_id, const utility::string_t& new_version); + void update_version(nmos::resources& resources, const web::json::array& resource_ids, const utility::string_t& new_version); + } +} diff --git a/Development/nmos/streamcompatibility_validation.cpp b/Development/nmos/streamcompatibility_validation.cpp new file mode 100644 index 000000000..da921370e --- /dev/null +++ b/Development/nmos/streamcompatibility_validation.cpp @@ -0,0 +1,176 @@ +#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 + + 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::runtime_error& 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..9bad6a688 --- /dev/null +++ b/Development/nmos/streamcompatibility_validation.h @@ -0,0 +1,107 @@ +#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 + { + // returns Sender's "state" and "debug" values + 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; + // returns Receiver's "state" and "debug" values + typedef std::function(const nmos::resource& receiver, const web::json::value& transport_file)> streamcompatibility_receiver_validator; + + typedef std::function transport_file_validator; + + typedef std::function resource_constraints_matcher; + typedef std::function& transport_file, const web::json::array& constraint_sets)> transport_file_constraint_sets_matcher; + 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 std::map> source_parameter_constraints + { + // Audio Constraints + + { nmos::caps::format::channel_count, [](const web::json::value& source, const web::json::value& con) { return nmos::match_integer_constraint((uint32_t)nmos::fields::channels(source).size(), con); } } + }; + + const std::map> flow_parameter_constraints + { + // General 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); } }, + + // Video Constraints + + { 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); } }, + + // Audio Constraints + + { nmos::caps::format::sample_rate, [](const web::json::value& flow, const web::json::value& con) { return nmos::match_rational_constraint(nmos::parse_rational(nmos::fields::sample_rate(flow)), con); } }, + { nmos::caps::format::sample_depth, [](const web::json::value& flow, const web::json::value& con) { return nmos::match_integer_constraint(nmos::fields::bit_depth(flow), con); } } + }; + + const std::map> sender_parameter_constraints + { + { 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); } } + }; + + 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/video_jxsv.cpp b/Development/nmos/video_jxsv.cpp index bde7df1e2..fb598728e 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 { @@ -334,6 +335,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 90d0b740b..5f26569f4 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" @@ -363,6 +365,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); @@ -385,6 +391,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/IS-11.md b/Documents/IS-11.md new file mode 100644 index 000000000..cfac4b3f6 --- /dev/null +++ b/Documents/IS-11.md @@ -0,0 +1,131 @@ +# IS-11 implementation in nmos-cpp + +``nmos-cpp`` provides three means of control over application-specific IS-11 implementation behaviour: +- callback functions being called on client requests to the Stream Compatibility Management API +- callback functions being called on NMOS resource updates in ``node_model`` +- internal JSON properties of ``streamcompatibility_resources`` that are not shown to a client + +## Callbacks for client request handling + +``streamcompatibility_`` prefix in callback type names here and below is omitted for brevity (e.g. ``base_edid_handler``, cf. ``streamcompatibility_base_edid_handler``). + +See the sequence diagram below on how a Node uses these callbacks. + +![IS-11-Client-Request-Callbacks](images/IS-11-Client-Request-Callbacks.png) + +### base_edid_handler + +Callback input parameters: +- an Input ID +- an Optional of a Base EDID + +Callback output parameters: +- an exception if present + +``base_edid_handler`` notifies application-specific code about a Base EDID modification request (``PUT`` and ``DELETE`` operations). + +It's expected to throw on ``PUT`` in order to indicate failures (e.g. an EDID validation failure). + +Since ``nmos-cpp`` doesn't parse EDID, the reference implementation of this callback violates the spec because it doesn't reject invalid EDIDs. Production implementations using IS-11 Inputs with Base EDID support must replace the reference callback with an EDID-aware one. + +### effective_edid_setter + +Callback input parameters: +- an Input ID + +Callback output parameters: +- a Variant of an Effective EDID or an href to it + +``effective_edid_setter`` demands application-specific code to set Effective EDID of a specific Input when it may be required. These cases are: +- a client requested a Base EDID modification (``PUT`` or ``DELETE``) +- a client requested modification of Active Constraints of some Sender (``PUT`` or ``DELETE``) that has this Input in its list of Inputs + +It's expected to never throw. + +### active_constraints_handler + +Callback input parameters: +- a Sender resource +- requested Constraint Sets + +Callback output parameters: +- a Boolean indicating whether the device can adhere to the requested Constraint Sets +- intersections of all combinations of each of the requested Constraint Sets and each of the Constraint Sets that describe the internal device capabilities +- an exception if present + +``active_constraints_handler`` notifies application-specific code about Active Constraints modification request (``PUT`` and ``DELETE`` operations). + +It's expected to throw in order to indicate failures. + +## Callbacks for node_model update handling + +[IS-11](https://specs.amwa.tv/is-11/) introduces Sender and Receiver states and has normative language on disabling Senders and Receivers in "bad" states (``active_constraints_violation`` for Senders and ``non_compliant_stream`` for Receivers). + +Mostly, they occur when a correctly operating Sender or Receiver gets reconfigured (a Flow of the Sender changes so that the Sender doesn't satisfy its Active Constraints anymore or the Receiver gets its Receiver Caps updated so that the ``transport_file`` in Connection API doesn't satisfy them anymore). + +``streamcompatibility_behaviour_thread`` tracks changes in the Node Model in case application-specific code updates resources in a way that causes these "bad" states. + +Though, application-specific code is free to decide whether a violation happened after such an update via ``sender_validator`` and ``receiver_validator`` callbacks. + +See the sequence diagram below on how this whole process looks like. + +![IS-11-Node-Model-Update-Callbacks](images/IS-11-Node-Model-Update-Callbacks.png) + +### sender_validator + +Callback input parameters: +- a Sender resource +- its Connection Sender resource +- its Source resource +- its Flow resource +- Constraint Sets from Active Constraints + +Callback output parameters: +- a Sender state +- a complementary debug message if present + +``sender_validator`` demands application-specific code to determine the Status of Sender based on provided info. + +It's expected to never throw. + +### receiver_validator + +Callback input parameters: +- a Receiver resource +- its transport file from Connection API /active + +Callback output parameters: +- a Receiver state +- a complementary debug message if present + +``receiver_validator`` demands application-specific code to determine the Status of Sender based on provided info. + +It's expected to never throw. + +## Internal JSON properties + +### temporarily_locked + +[IS-11](https://specs.amwa.tv/is-11/) prescribes that a ``PUT`` operation for ``/inputs/{inputId}/edid/base`` and ``/senders/{senderId}/constraints/active`` may be responded with ``423 Locked`` when the ``PUT`` payload "can't be applied temporarily due to Device restrictions (e.g. if the Sender is active)". + +As far as a variety of application-specific scenarios fall under this case, ``nmos-cpp`` provides ``temporarily_locked`` read-write property that controls whether the resources above are locked at the moment. + +#### Inputs + +NMOS Node applications based on ``nmos-cpp`` can create an IS-11 Input calling one of overloaded ``nmos::experimental::make_streamcompatibility_input``. Depending on whether the Input supports EDID and Base EDID, ``data`` member of the returned ``nmos::resource`` contains appropriate properties ``endpoint_effective_edid`` and ``endpoint_base_edid`` with ``temporarily_locked`` inside each one. + +#### Senders + +Each IS-11 Sender's ``nmos::resource`` representation contains ``endpoint_active_constraints`` property with ``temporarily_locked`` inside. + +### intersection_of_caps_and_constraints + +Each time Active Constraints are being ``PUT`` into a Sender, an NMOS Node application must verify whether the Sender can adhere to them. ``nmos-cpp-node`` application verifies it by checking if each Constraint Set (from ``M`` overall) in the Active Constraints gives a valid intersection with at least one of Constraint Sets (of ``N``) that represent capabilities of the Sender. ``M * N`` intersections are stored in ``intersection_of_caps_and_constraints`` to help reconfiguring the Sender and its related resources (e.g. building Effective EDIDs) in a way that takes into account not only the will of the user (Active Constraints) but also the capabilities of the Sender. + + \ No newline at end of file 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/Sandbox/run_nmos_testing.sh b/Sandbox/run_nmos_testing.sh index f22cf5804..ec661b542 100755 --- a/Sandbox/run_nmos_testing.sh +++ b/Sandbox/run_nmos_testing.sh @@ -36,6 +36,7 @@ expected_disabled_IS_08_02=0 expected_disabled_IS_09_02=0 expected_disabled_IS_04_02=0 expected_disabled_IS_09_01=0 +# expected_disabled_IS_11_01=0 config_secure=`${run_python} -c $'from nmostesting import Config\nprint(Config.ENABLE_HTTPS)'` || (echo "error running python"; exit 1) config_dns_sd_mode=`${run_python} -c $'from nmostesting import Config\nprint(Config.DNS_SD_MODE)'` || (echo "error running python"; exit 1) @@ -140,6 +141,8 @@ else (( expected_disabled_IS_07_02+=21 )) (( expected_disabled_IS_08_01+=7 )) (( expected_disabled_IS_08_02+=14 )) + # test_04_03, test_04_03_01, test_04_03_01_01, test_04_03_02, test_04_03_02_01 + # (( expected_disabled_IS_11_01+=26 )) # test_33, test_33_1 (( expected_disabled_IS_04_02+=16 )) (( expected_disabled_IS_09_01+=7 )) @@ -185,6 +188,7 @@ function do_run_test() { ;; *) echo "Fail" | tee ${badges_dir}/${suite}.txt echo "${suite} :x:" >> ${summary_path} + cat ${output_file} ;; esac } @@ -211,6 +215,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 + # Run Registry tests (leave Node running) "${registry_command}" "{\"pri\":0,\"http_port\":8088 ${common_params} ${registry_params}}" > ${results_dir}/registryoutput 2>&1 & REGISTRY_PID=$!