From b3eb23041c34295b7a9cc5496cc45da8c7e20d86 Mon Sep 17 00:00:00 2001 From: Mariana Assis Date: Fri, 6 Sep 2024 15:55:52 +0100 Subject: [PATCH] ossrf: Add existing developed code --- .github/workflows/cmake-ubuntu.yml | 28 + .gitignore | 8 + CMakeLists.txt | 27 + README.md | 114 +- conanfile.py | 18 + cpp/.clang-format | 25 + cpp/CMakeLists.txt | 12 + cpp/config/cmake/CompilerWarnings.cmake | 90 + cpp/config/cmake/PreventInSourceBuilds.cmake | 18 + .../cmake/StandardProjectSettings.cmake | 42 + cpp/demos/CMakeLists.txt | 3 + cpp/demos/gst-sender/CMakeLists.txt | 34 + cpp/demos/gst-sender/main.cpp | 388 ++++ cpp/demos/nmos-cpp-node/CMakeLists.txt | 17 + cpp/demos/nmos-cpp-node/config.json | 370 ++++ cpp/demos/nmos-cpp-node/main.cpp | 339 ++++ .../nmos-cpp-node/node_implementation.cpp | 1586 +++++++++++++++++ cpp/demos/nmos-cpp-node/node_implementation.h | 28 + cpp/demos/ossrf-nmos-api/CMakeLists.txt | 23 + .../ossrf-nmos-api/config/nmos_config.json | 108 ++ cpp/demos/ossrf-nmos-api/main.cpp | 129 ++ cpp/libs/CMakeLists.txt | 7 + cpp/libs/bisect_expected/CMakeLists.txt | 10 + cpp/libs/bisect_expected/lib/CMakeLists.txt | 26 + .../lib/include/bisect/expected.h | 15 + .../lib/include/bisect/expected/helpers.h | 29 + .../lib/include/bisect/expected/macros.h | 55 + .../lib/include/bisect/expected/match.h | 17 + .../bisect_expected/lib/include/bisect/fmt.h | 9 + cpp/libs/bisect_expected/lib/src/expected.cpp | 1 + cpp/libs/bisect_expected/tests/CMakeLists.txt | 16 + .../tests/expected/test_expected.cpp | 29 + .../tests/expected/test_expected_macros.cpp | 69 + cpp/libs/bisect_gst/CMakeLists.txt | 1 + cpp/libs/bisect_gst/lib/CMakeLists.txt | 60 + .../lib/include/bisect/initializer.h | 12 + .../bisect_gst/lib/include/bisect/pipeline.h | 38 + cpp/libs/bisect_gst/lib/src/initializer.cpp | 13 + cpp/libs/bisect_gst/lib/src/pipeline.cpp | 157 ++ cpp/libs/bisect_json/CMakeLists.txt | 5 + cpp/libs/bisect_json/LICENSE.txt | 1 + cpp/libs/bisect_json/lib/CMakeLists.txt | 36 + .../bisect_json/lib/include/bisect/json.h | 3 + .../lib/include/bisect/json/json.h | 265 +++ cpp/libs/bisect_json/lib/src/json.cpp | 1 + cpp/libs/bisect_json/tests/CMakeLists.txt | 16 + cpp/libs/bisect_json/tests/test_parse.cpp | 21 + cpp/libs/bisect_nmoscpp/CMakeLists.txt | 1 + cpp/libs/bisect_nmoscpp/lib/CMakeLists.txt | 43 + .../bisect/nmoscpp/base_nmos_controller.h | 33 + .../include/bisect/nmoscpp/configuration.h | 146 ++ .../include/bisect/nmoscpp/detail/expected.h | 6 + .../include/bisect/nmoscpp/detail/internal.h | 16 + .../lib/include/bisect/nmoscpp/logger.h | 26 + .../include/bisect/nmoscpp/nmos_controller.h | 88 + .../bisect/nmoscpp/nmos_event_handler.h | 21 + .../lib/src/base_nmos_controller.cpp | 293 +++ cpp/libs/bisect_nmoscpp/lib/src/logger.cpp | 43 + .../lib/src/nmos_controller.cpp | 703 ++++++++ cpp/libs/bisect_nmoscpp/lib/src/utils.cpp | 251 +++ cpp/libs/bisect_nmoscpp/lib/src/utils.h | 123 ++ cpp/libs/bisect_sdp/CMakeLists.txt | 1 + cpp/libs/bisect_sdp/lib/CMakeLists.txt | 45 + cpp/libs/bisect_sdp/lib/include/bisect/sdp.h | 4 + .../lib/include/bisect/sdp/builder.h | 14 + .../lib/include/bisect/sdp/clocks.h | 73 + .../lib/include/bisect/sdp/media_types.h | 10 + .../lib/include/bisect/sdp/reader.h | 11 + .../lib/include/bisect/sdp/settings.h | 34 + cpp/libs/bisect_sdp/lib/src/builder.cpp | 138 ++ cpp/libs/bisect_sdp/lib/src/clocks.cpp | 125 ++ cpp/libs/bisect_sdp/lib/src/reader.cpp | 185 ++ cpp/libs/ossrf_gstreamer_api/CMakeLists.txt | 1 + .../ossrf_gstreamer_api/lib/CMakeLists.txt | 53 + .../api/receiver/receiver_configuration.h | 39 + .../gstreamer/api/receiver/receiver_plugin.h | 22 + .../api/sender/sender_configuration.h | 57 + .../gstreamer/api/sender/sender_plugin.h | 21 + .../lib/src/receiver/receiver_plugin.cpp | 97 + .../receiver/st2110_20_receiver_plugin.cpp | 92 + .../src/receiver/st2110_20_receiver_plugin.h | 10 + .../lib/src/sender/sender_plugin.cpp | 115 ++ .../src/sender/st2110_20_sender_plugin.cpp | 94 + .../lib/src/sender/st2110_20_sender_plugin.h | 10 + cpp/libs/ossrf_nmos_api/CMakeLists.txt | 1 + cpp/libs/ossrf_nmos_api/lib/CMakeLists.txt | 38 + .../lib/include/ossrf/nmos/api/nmos.h | 41 + .../lib/include/ossrf/nmos/api/nmos_client.h | 35 + .../lib/include/ossrf/nmos/api/nmos_impl.h | 46 + .../lib/src/context/context.cpp | 19 + .../ossrf_nmos_api/lib/src/context/context.h | 28 + .../lib/src/context/nmos_event_handler.cpp | 44 + .../lib/src/context/nmos_event_handler.h | 23 + .../lib/src/context/resource_map.cpp | 56 + .../lib/src/context/resource_map.h | 27 + .../ossrf_nmos_api/lib/src/nmos_client.cpp | 89 + cpp/libs/ossrf_nmos_api/lib/src/nmos_impl.cpp | 197 ++ .../lib/src/resources/nmos_resource.h | 28 + .../src/resources/nmos_resource_receiver.cpp | 106 ++ .../src/resources/nmos_resource_receiver.h | 32 + .../src/resources/nmos_resource_sender.cpp | 41 + .../lib/src/resources/nmos_resource_sender.h | 32 + .../lib/src/serialization/device.cpp | 24 + .../lib/src/serialization/device.h | 12 + .../lib/src/serialization/media_types.h | 9 + .../lib/src/serialization/meta.cpp | 17 + .../lib/src/serialization/meta.h | 10 + .../lib/src/serialization/network.cpp | 45 + .../lib/src/serialization/network.h | 12 + .../lib/src/serialization/receiver.cpp | 68 + .../lib/src/serialization/receiver.h | 10 + .../lib/src/serialization/sender.cpp | 130 ++ .../lib/src/serialization/sender.h | 10 + .../lib/src/serialization/video.cpp | 11 + .../lib/src/serialization/video.h | 10 + cpp/libs/ossrf_nmos_api/lib/src/utils.h | 68 + images/build.sh | 5 + images/dev/Dockerfile | 48 + images/docker-compose-x86-development.yml | 18 + images/run-dev.sh | 2 + images/scripts/build-tools/add-user-bisect.sh | 14 + .../scripts/build-tools/install-cmake-x86.sh | 26 + images/scripts/build-tools/install-conan.sh | 21 + images/scripts/build-tools/install-fish.sh | 3 + images/scripts/build-tools/install-git.sh | 3 + images/scripts/common/add-ssh-server.sh | 24 + images/scripts/common/env.sh | 7 + images/scripts/launch.sh | 16 + scripts/build.sh | 8 + scripts/cipipeline/clang-test.sh | 9 + scripts/common.sh | 123 ++ scripts/dist-clean.sh | 9 + scripts/dist.sh | 11 + scripts/pack.sh | 9 + scripts/setup.sh | 8 + scripts/test/dist/bare_demo/.gitignore | 2 + scripts/test/dist/bare_demo/Makefile | 13 + scripts/test/dist/bare_demo/main.cpp | 2 + 138 files changed, 9057 insertions(+), 6 deletions(-) create mode 100644 .github/workflows/cmake-ubuntu.yml create mode 100644 CMakeLists.txt create mode 100644 conanfile.py create mode 100644 cpp/.clang-format create mode 100644 cpp/CMakeLists.txt create mode 100644 cpp/config/cmake/CompilerWarnings.cmake create mode 100644 cpp/config/cmake/PreventInSourceBuilds.cmake create mode 100644 cpp/config/cmake/StandardProjectSettings.cmake create mode 100644 cpp/demos/CMakeLists.txt create mode 100644 cpp/demos/gst-sender/CMakeLists.txt create mode 100644 cpp/demos/gst-sender/main.cpp create mode 100644 cpp/demos/nmos-cpp-node/CMakeLists.txt create mode 100644 cpp/demos/nmos-cpp-node/config.json create mode 100644 cpp/demos/nmos-cpp-node/main.cpp create mode 100644 cpp/demos/nmos-cpp-node/node_implementation.cpp create mode 100644 cpp/demos/nmos-cpp-node/node_implementation.h create mode 100644 cpp/demos/ossrf-nmos-api/CMakeLists.txt create mode 100644 cpp/demos/ossrf-nmos-api/config/nmos_config.json create mode 100644 cpp/demos/ossrf-nmos-api/main.cpp create mode 100644 cpp/libs/CMakeLists.txt create mode 100644 cpp/libs/bisect_expected/CMakeLists.txt create mode 100644 cpp/libs/bisect_expected/lib/CMakeLists.txt create mode 100644 cpp/libs/bisect_expected/lib/include/bisect/expected.h create mode 100644 cpp/libs/bisect_expected/lib/include/bisect/expected/helpers.h create mode 100644 cpp/libs/bisect_expected/lib/include/bisect/expected/macros.h create mode 100644 cpp/libs/bisect_expected/lib/include/bisect/expected/match.h create mode 100644 cpp/libs/bisect_expected/lib/include/bisect/fmt.h create mode 100644 cpp/libs/bisect_expected/lib/src/expected.cpp create mode 100644 cpp/libs/bisect_expected/tests/CMakeLists.txt create mode 100644 cpp/libs/bisect_expected/tests/expected/test_expected.cpp create mode 100644 cpp/libs/bisect_expected/tests/expected/test_expected_macros.cpp create mode 100644 cpp/libs/bisect_gst/CMakeLists.txt create mode 100644 cpp/libs/bisect_gst/lib/CMakeLists.txt create mode 100644 cpp/libs/bisect_gst/lib/include/bisect/initializer.h create mode 100644 cpp/libs/bisect_gst/lib/include/bisect/pipeline.h create mode 100644 cpp/libs/bisect_gst/lib/src/initializer.cpp create mode 100644 cpp/libs/bisect_gst/lib/src/pipeline.cpp create mode 100644 cpp/libs/bisect_json/CMakeLists.txt create mode 100644 cpp/libs/bisect_json/LICENSE.txt create mode 100644 cpp/libs/bisect_json/lib/CMakeLists.txt create mode 100644 cpp/libs/bisect_json/lib/include/bisect/json.h create mode 100644 cpp/libs/bisect_json/lib/include/bisect/json/json.h create mode 100644 cpp/libs/bisect_json/lib/src/json.cpp create mode 100644 cpp/libs/bisect_json/tests/CMakeLists.txt create mode 100644 cpp/libs/bisect_json/tests/test_parse.cpp create mode 100644 cpp/libs/bisect_nmoscpp/CMakeLists.txt create mode 100644 cpp/libs/bisect_nmoscpp/lib/CMakeLists.txt create mode 100644 cpp/libs/bisect_nmoscpp/lib/include/bisect/nmoscpp/base_nmos_controller.h create mode 100644 cpp/libs/bisect_nmoscpp/lib/include/bisect/nmoscpp/configuration.h create mode 100644 cpp/libs/bisect_nmoscpp/lib/include/bisect/nmoscpp/detail/expected.h create mode 100644 cpp/libs/bisect_nmoscpp/lib/include/bisect/nmoscpp/detail/internal.h create mode 100644 cpp/libs/bisect_nmoscpp/lib/include/bisect/nmoscpp/logger.h create mode 100644 cpp/libs/bisect_nmoscpp/lib/include/bisect/nmoscpp/nmos_controller.h create mode 100644 cpp/libs/bisect_nmoscpp/lib/include/bisect/nmoscpp/nmos_event_handler.h create mode 100644 cpp/libs/bisect_nmoscpp/lib/src/base_nmos_controller.cpp create mode 100644 cpp/libs/bisect_nmoscpp/lib/src/logger.cpp create mode 100644 cpp/libs/bisect_nmoscpp/lib/src/nmos_controller.cpp create mode 100644 cpp/libs/bisect_nmoscpp/lib/src/utils.cpp create mode 100644 cpp/libs/bisect_nmoscpp/lib/src/utils.h create mode 100644 cpp/libs/bisect_sdp/CMakeLists.txt create mode 100644 cpp/libs/bisect_sdp/lib/CMakeLists.txt create mode 100644 cpp/libs/bisect_sdp/lib/include/bisect/sdp.h create mode 100644 cpp/libs/bisect_sdp/lib/include/bisect/sdp/builder.h create mode 100644 cpp/libs/bisect_sdp/lib/include/bisect/sdp/clocks.h create mode 100644 cpp/libs/bisect_sdp/lib/include/bisect/sdp/media_types.h create mode 100644 cpp/libs/bisect_sdp/lib/include/bisect/sdp/reader.h create mode 100644 cpp/libs/bisect_sdp/lib/include/bisect/sdp/settings.h create mode 100644 cpp/libs/bisect_sdp/lib/src/builder.cpp create mode 100644 cpp/libs/bisect_sdp/lib/src/clocks.cpp create mode 100644 cpp/libs/bisect_sdp/lib/src/reader.cpp create mode 100644 cpp/libs/ossrf_gstreamer_api/CMakeLists.txt create mode 100644 cpp/libs/ossrf_gstreamer_api/lib/CMakeLists.txt create mode 100644 cpp/libs/ossrf_gstreamer_api/lib/include/ossrf/gstreamer/api/receiver/receiver_configuration.h create mode 100644 cpp/libs/ossrf_gstreamer_api/lib/include/ossrf/gstreamer/api/receiver/receiver_plugin.h create mode 100644 cpp/libs/ossrf_gstreamer_api/lib/include/ossrf/gstreamer/api/sender/sender_configuration.h create mode 100644 cpp/libs/ossrf_gstreamer_api/lib/include/ossrf/gstreamer/api/sender/sender_plugin.h create mode 100644 cpp/libs/ossrf_gstreamer_api/lib/src/receiver/receiver_plugin.cpp create mode 100644 cpp/libs/ossrf_gstreamer_api/lib/src/receiver/st2110_20_receiver_plugin.cpp create mode 100644 cpp/libs/ossrf_gstreamer_api/lib/src/receiver/st2110_20_receiver_plugin.h create mode 100644 cpp/libs/ossrf_gstreamer_api/lib/src/sender/sender_plugin.cpp create mode 100644 cpp/libs/ossrf_gstreamer_api/lib/src/sender/st2110_20_sender_plugin.cpp create mode 100644 cpp/libs/ossrf_gstreamer_api/lib/src/sender/st2110_20_sender_plugin.h create mode 100644 cpp/libs/ossrf_nmos_api/CMakeLists.txt create mode 100644 cpp/libs/ossrf_nmos_api/lib/CMakeLists.txt create mode 100644 cpp/libs/ossrf_nmos_api/lib/include/ossrf/nmos/api/nmos.h create mode 100644 cpp/libs/ossrf_nmos_api/lib/include/ossrf/nmos/api/nmos_client.h create mode 100644 cpp/libs/ossrf_nmos_api/lib/include/ossrf/nmos/api/nmos_impl.h create mode 100644 cpp/libs/ossrf_nmos_api/lib/src/context/context.cpp create mode 100644 cpp/libs/ossrf_nmos_api/lib/src/context/context.h create mode 100644 cpp/libs/ossrf_nmos_api/lib/src/context/nmos_event_handler.cpp create mode 100644 cpp/libs/ossrf_nmos_api/lib/src/context/nmos_event_handler.h create mode 100644 cpp/libs/ossrf_nmos_api/lib/src/context/resource_map.cpp create mode 100644 cpp/libs/ossrf_nmos_api/lib/src/context/resource_map.h create mode 100644 cpp/libs/ossrf_nmos_api/lib/src/nmos_client.cpp create mode 100644 cpp/libs/ossrf_nmos_api/lib/src/nmos_impl.cpp create mode 100644 cpp/libs/ossrf_nmos_api/lib/src/resources/nmos_resource.h create mode 100644 cpp/libs/ossrf_nmos_api/lib/src/resources/nmos_resource_receiver.cpp create mode 100644 cpp/libs/ossrf_nmos_api/lib/src/resources/nmos_resource_receiver.h create mode 100644 cpp/libs/ossrf_nmos_api/lib/src/resources/nmos_resource_sender.cpp create mode 100644 cpp/libs/ossrf_nmos_api/lib/src/resources/nmos_resource_sender.h create mode 100644 cpp/libs/ossrf_nmos_api/lib/src/serialization/device.cpp create mode 100644 cpp/libs/ossrf_nmos_api/lib/src/serialization/device.h create mode 100644 cpp/libs/ossrf_nmos_api/lib/src/serialization/media_types.h create mode 100644 cpp/libs/ossrf_nmos_api/lib/src/serialization/meta.cpp create mode 100644 cpp/libs/ossrf_nmos_api/lib/src/serialization/meta.h create mode 100644 cpp/libs/ossrf_nmos_api/lib/src/serialization/network.cpp create mode 100644 cpp/libs/ossrf_nmos_api/lib/src/serialization/network.h create mode 100644 cpp/libs/ossrf_nmos_api/lib/src/serialization/receiver.cpp create mode 100644 cpp/libs/ossrf_nmos_api/lib/src/serialization/receiver.h create mode 100644 cpp/libs/ossrf_nmos_api/lib/src/serialization/sender.cpp create mode 100644 cpp/libs/ossrf_nmos_api/lib/src/serialization/sender.h create mode 100644 cpp/libs/ossrf_nmos_api/lib/src/serialization/video.cpp create mode 100644 cpp/libs/ossrf_nmos_api/lib/src/serialization/video.h create mode 100644 cpp/libs/ossrf_nmos_api/lib/src/utils.h create mode 100755 images/build.sh create mode 100644 images/dev/Dockerfile create mode 100755 images/docker-compose-x86-development.yml create mode 100755 images/run-dev.sh create mode 100755 images/scripts/build-tools/add-user-bisect.sh create mode 100755 images/scripts/build-tools/install-cmake-x86.sh create mode 100755 images/scripts/build-tools/install-conan.sh create mode 100755 images/scripts/build-tools/install-fish.sh create mode 100755 images/scripts/build-tools/install-git.sh create mode 100755 images/scripts/common/add-ssh-server.sh create mode 100755 images/scripts/common/env.sh create mode 100755 images/scripts/launch.sh create mode 100755 scripts/build.sh create mode 100755 scripts/cipipeline/clang-test.sh create mode 100755 scripts/common.sh create mode 100755 scripts/dist-clean.sh create mode 100755 scripts/dist.sh create mode 100755 scripts/pack.sh create mode 100755 scripts/setup.sh create mode 100644 scripts/test/dist/bare_demo/.gitignore create mode 100644 scripts/test/dist/bare_demo/Makefile create mode 100644 scripts/test/dist/bare_demo/main.cpp diff --git a/.github/workflows/cmake-ubuntu.yml b/.github/workflows/cmake-ubuntu.yml new file mode 100644 index 0000000..f6caf2f --- /dev/null +++ b/.github/workflows/cmake-ubuntu.yml @@ -0,0 +1,28 @@ +# This starter workflow is for a CMake project running on a single platform. There is a different starter workflow if you need cross-platform coverage. +# See: https://github.com/actions/starter-workflows/blob/main/ci/cmake-multi-platform.yml +name: Ubuntu pipeline + +# on: +# pull_request: +# branches: +# - main + +env: + BUILD_TYPE: Release + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + + - name: Clang Check + run: ./scripts/cipipeline/clang-test.sh + + - name: Setup + run: ./scripts/setup.sh + + - name: Build & Test + run: ./scripts/build.sh + diff --git a/.gitignore b/.gitignore index aa14556..de313b2 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,11 @@ *.swp *~ .DS_Store + +/build +/dist +/install +/CMakeUserPresets.json +/.vscode +/volumes +/.user diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..1a230ec --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,27 @@ +cmake_minimum_required(VERSION 3.16) + +project(ossrf LANGUAGES CXX VERSION 0.1.0) + +list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cpp/config/cmake") +include(StandardProjectSettings) +include(PreventInSourceBuilds) + +enable_testing() + +# Link this 'library' to set the c++ standard / compile-time options requested +add_library(project_options INTERFACE) +target_compile_features(project_options INTERFACE cxx_std_20) +add_library(bisect::project_options ALIAS project_options) + +# Link this 'library' to use the warnings specified in CompilerWarnings.cmake +add_library(project_warnings INTERFACE) +add_library(bisect::project_warnings ALIAS project_warnings) + +# Standard compiler warnings +include(CompilerWarnings) +set_project_warnings(project_warnings FALSE) +add_library(project_warnings_c INTERFACE) +add_library(bisect::project_warnings_c ALIAS project_warnings_c) + +add_subdirectory(cpp) + diff --git a/README.md b/README.md index 319ed4a..9823bb7 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,113 @@ -# \[Work In Progress\] AMWA NMOS Sender Receiver Framework +# ossrf -[![Lint Status](https://github.com/AMWA-TV/nmos-sender-receiver-framework/workflows/Lint/badge.svg)](https://github.com/AMWA-TV/nmos-sender-receiver-framework/actions?query=workflow%3ALint) -[![Render Status](https://github.com/AMWA-TV/nmos-sender-receiver-framework/workflows/Render/badge.svg)](https://github.com/AMWA-TV/nmos-sender-receiver-framework/actions?query=workflow%3ARender) +## Overview +Developing OSSRF for AWMA by Bisect. - +## Platforms + +Currently, only Linux is supported. + +## Requirements + +Conan >= 2.0 + +CMake >= 3.16 + +## Code + +Clone the repository: + +`git clone git@github.com:bisect-pt/ossrf.git` + +or + +`git clone https://github.com/bisect-pt/ossrf.git` + +## Setup Docker Containers + +This will create the docker containers base on the docker compose: + + docker compose -f images/docker-compose-x86-development.yml build + +One of the containers is the ossrf-dev where you can find the development container. +The other is nmos-registry where will launch the NVIDIA NMOS Commissioning Controller + +## Run Docker Containers + +This will run the docker containers: + + docker compose -f images/docker-compose-x86-development.yml up -d + +## Access the Development Container + +### Using VSCode + +Install `ms-vscode-remote.remote-ssh` extension on vscode and enter on the container. + +### Using SSH + +First you need to check you IP address. You can do it by running: + + hostname -i + +Once you know your ip address you enter the container by doing: + + ssh -p 55555 bisect@{your-ip-address} -XY + +## Access NVIDIA NMOS Commissioning Controller Container + +### Access the NVIDIA NMOS Commissioning Controller UI + +You can access the UI by opening your favorite browser and go to this link: + + http://localhost:8010/admin/ + +## Build + +### Prepare Conan + +If you have not used Conan before: + +- create a directory: + + `mkdir ~/.conan2` + +- confirm that the Conan version is suitable + + conan --version + +- set the default Conan profile, e.g. + + conan profile detect --force + +### Install the dependencies using Conan + +This only has to be done at the first time or after any of the dependencies change: + + ./scripts/setup.sh + +### Build using CMake + + ./scripts/build.sh + +### Demo ossrf-nmos-api +This example showcases the creation of one video/raw receiver and two video/raw senders, both on the NMOS and GStreamer sides. The receiver can be connected to either sender, allowing you to observe the different outputs. +While it is possible to create NMOS audio resources, GStreamer support for audio is not yet implemented. +#### Configuration file +Open `cpp/demos/ossrf-nmos-api/config/nmos_config.json` and adjust the following parameters: + +- `host_addresses` +- `registry_address` +- `system_address` +- `interface_address` +- `system_address` + + This must be the address of the primary data interface. + *** + +#### To run: + + `./build/Debug/cpp/demos/ossrf-nmos-api/ossrf-nmos-api -f ./cpp/demos/ossrf-nmos-api/config/nmos_config.json` -This repo and site will be used for AMWA's forthcoming open-source NMOS Sender Receiver Framework. - diff --git a/conanfile.py b/conanfile.py new file mode 100644 index 0000000..eaa360c --- /dev/null +++ b/conanfile.py @@ -0,0 +1,18 @@ +from conan import ConanFile +from conan.tools.cmake import cmake_layout + +class OSSRFRecipe(ConanFile): + settings = "os", "compiler", "build_type", "arch" + generators = "CMakeToolchain", "CMakeDeps" + + def requirements(self): + self.requires("nmos-cpp/cci.20240223") + self.requires("gtest/1.14.0") + self.requires("fmt/9.1.0") + self.requires("nlohmann_json/3.11.3") + + def build_requirements(self): + pass + + def layout(self): + cmake_layout(self) diff --git a/cpp/.clang-format b/cpp/.clang-format new file mode 100644 index 0000000..2301e6e --- /dev/null +++ b/cpp/.clang-format @@ -0,0 +1,25 @@ +BasedOnStyle: LLVM +Language: Cpp +ColumnLimit: 120 +IndentWidth: 4 +BreakBeforeBraces: Custom +BraceWrapping: + AfterEnum: true + AfterStruct: true + AfterClass: true + AfterControlStatement: true + AfterFunction: true + AfterNamespace: true + BeforeCatch: true + BeforeElse: true + SplitEmptyFunction: true +NamespaceIndentation: All +PointerAlignment: Left +AllowShortIfStatementsOnASingleLine: WithoutElse +AllowShortCaseLabelsOnASingleLine: true +AllowShortFunctionsOnASingleLine: InlineOnly +AlignAfterOpenBracket: Align +AlignConsecutiveAssignments: true +AlignTrailingComments: true +SpaceBeforeParens: Never +SortIncludes: false \ No newline at end of file diff --git a/cpp/CMakeLists.txt b/cpp/CMakeLists.txt new file mode 100644 index 0000000..e732382 --- /dev/null +++ b/cpp/CMakeLists.txt @@ -0,0 +1,12 @@ +file(WRITE "${CMAKE_CURRENT_BINARY_DIR}/version.txt" "${PROJECT_VERSION}") + +add_subdirectory(demos) +add_subdirectory(libs) + +set(CPACK_PROJECT_NAME ${PROJECT_NAME}) +set(CPACK_PACKAGE_VERSION_MAJOR ${PROJECT_VERSION_MAJOR}) +set(CPACK_PACKAGE_VERSION_MINOR ${PROJECT_VERSION_MINOR}) +set(CPACK_PACKAGE_VERSION_PATCH ${PROJECT_VERSION_PATCH}) +set(CPACK_PACKAGE_VENDOR "BISECT LDA") + +include(CPack) diff --git a/cpp/config/cmake/CompilerWarnings.cmake b/cpp/config/cmake/CompilerWarnings.cmake new file mode 100644 index 0000000..d8fcabd --- /dev/null +++ b/cpp/config/cmake/CompilerWarnings.cmake @@ -0,0 +1,90 @@ +# from here: +# +# https://github.com/lefticus/cppbestpractices/blob/master/02-Use_the_Tools_Available.md + +function(set_project_warnings project_name is_c) + option(WARNINGS_AS_ERRORS "Treat compiler warnings as errors" TRUE) + + set(MSVC_WARNINGS + /W4 # Baseline reasonable warnings + /w14242 # 'identifier': conversion from 'type1' to 'type1', possible loss of data + /w14254 # 'operator': conversion from 'type1:field_bits' to 'type2:field_bits', possible loss of data + /w14263 # 'function': member function does not override any base class virtual member function + /w14265 # 'classname': class has virtual functions, but destructor is not virtual instances of this class may not + # be destructed correctly + /w14287 # 'operator': unsigned/negative constant mismatch + /we4289 # nonstandard extension used: 'variable': loop control variable declared in the for-loop is used outside + # the for-loop scope + /w14296 # 'operator': expression is always 'boolean_value' + /w14311 # 'variable': pointer truncation from 'type1' to 'type2' + /w14545 # expression before comma evaluates to a function which is missing an argument list + /w14546 # function call before comma missing argument list + /w14547 # 'operator': operator before comma has no effect; expected operator with side-effect + /w14549 # 'operator': operator before comma has no effect; did you intend 'operator'? + /w14555 # expression has no effect; expected expression with side- effect + /w14619 # pragma warning: there is no warning number 'number' + /w14640 # Enable warning on thread un-safe static member initialization + /w14826 # Conversion from 'type1' to 'type_2' is sign-extended. This may cause unexpected runtime behavior. + /w14905 # wide string literal cast to 'LPSTR' + /w14906 # string literal cast to 'LPWSTR' + /w14928 # illegal copy-initialization; more than one user-defined conversion has been implicitly applied + /permissive- # standards conformance mode for MSVC compiler. + ) + + set(CLANG_WARNINGS + -Wall + -Wextra # reasonable and standard + -Wshadow # warn the user if a variable declaration shadows one from a parent context + # catch hard to track down memory errors + -Wcast-align # warn for potential performance problem casts + -Wunused # warn on anything being unused + -Wpedantic # warn if non-standard C++ is used + -Wconversion # warn on type conversions that may lose data + -Wsign-conversion # warn on sign conversions + -Wnull-dereference # warn if a null dereference is detected + -Wdouble-promotion # warn if float is implicit promoted to double + -Wformat=2 # warn on security issues around functions that format output (ie printf) + ) + + if (!is_c) + set(CLANG_WARNINGS + ${CLANG_WARNINGS} + -Wnon-virtual-dtor # warn the user if a class with virtual functions has a non-virtual destructor. This helps + -Wold-style-cast # warn for c-style casts + -Woverloaded-virtual # warn if you overload (not override) a virtual function + ) + endif () + + if (WARNINGS_AS_ERRORS) + set(CLANG_WARNINGS ${CLANG_WARNINGS} -Werror) + set(MSVC_WARNINGS ${MSVC_WARNINGS} /WX) + endif () + + set(GCC_WARNINGS + ${CLANG_WARNINGS} + -Wmisleading-indentation # warn if indentation implies blocks where blocks do not exist + -Wduplicated-cond # warn if if / else chain has duplicated conditions + -Wduplicated-branches # warn if if / else branches have duplicated code + -Wlogical-op # warn about logical operations being used where bitwise were probably wanted + ) + + if (!is_c) + set(GCC_WARNINGS + ${GCC_WARNINGS} + -Wuseless-cast # warn if you perform a cast to the same type + ) + endif () + + if (MSVC) + set(PROJECT_WARNINGS ${MSVC_WARNINGS}) + elseif (CMAKE_CXX_COMPILER_ID MATCHES ".*Clang") + set(PROJECT_WARNINGS ${CLANG_WARNINGS}) + elseif (CMAKE_CXX_COMPILER_ID STREQUAL "GNU") + set(PROJECT_WARNINGS ${GCC_WARNINGS}) + else () + message(AUTHOR_WARNING "No compiler warnings set for '${CMAKE_CXX_COMPILER_ID}' compiler.") + endif () + + target_compile_options(${project_name} INTERFACE ${PROJECT_WARNINGS}) + +endfunction() diff --git a/cpp/config/cmake/PreventInSourceBuilds.cmake b/cpp/config/cmake/PreventInSourceBuilds.cmake new file mode 100644 index 0000000..b1bae27 --- /dev/null +++ b/cpp/config/cmake/PreventInSourceBuilds.cmake @@ -0,0 +1,18 @@ +# +# This function will prevent in-source builds +function(AssureOutOfSourceBuilds) + # make sure the user doesn't play dirty with symlinks + get_filename_component(srcdir "${CMAKE_SOURCE_DIR}" REALPATH) + get_filename_component(bindir "${CMAKE_BINARY_DIR}" REALPATH) + + # disallow in-source builds + if ("${srcdir}" STREQUAL "${bindir}") + message("######################################################") + message("Warning: in-source builds are disabled") + message("Please create a separate build directory and run cmake from there") + message("######################################################") + message(FATAL_ERROR "Quitting configuration") + endif () +endfunction() + +assureoutofsourcebuilds() diff --git a/cpp/config/cmake/StandardProjectSettings.cmake b/cpp/config/cmake/StandardProjectSettings.cmake new file mode 100644 index 0000000..14ac4d0 --- /dev/null +++ b/cpp/config/cmake/StandardProjectSettings.cmake @@ -0,0 +1,42 @@ +# Set a default build type if none was specified +if (NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + message(STATUS "Setting build type to 'RelWithDebInfo' as none was specified.") + set(CMAKE_BUILD_TYPE + RelWithDebInfo + CACHE STRING "Choose the type of build." FORCE) + # Set the possible values of build type for cmake-gui, ccmake + set_property( + CACHE CMAKE_BUILD_TYPE + PROPERTY STRINGS + "Debug" + "Release" + "MinSizeRel" + "RelWithDebInfo") +endif () + +# Generate compile_commands.json to make it easier to work with clang based tools +set(CMAKE_EXPORT_COMPILE_COMMANDS ON) + +option(ENABLE_IPO "Enable Interprocedural Optimization, aka Link Time Optimization (LTO)" OFF) + +if (ENABLE_IPO) + include(CheckIPOSupported) + check_ipo_supported( + RESULT + result + OUTPUT + output) + if (result) + set(CMAKE_INTERPROCEDURAL_OPTIMIZATION TRUE) + else () + message(SEND_ERROR "IPO is not supported: ${output}") + endif () +endif () +if (CMAKE_CXX_COMPILER_ID MATCHES ".*Clang") + add_compile_options(-fcolor-diagnostics) +elseif (CMAKE_CXX_COMPILER_ID STREQUAL "GNU") + add_compile_options(-fdiagnostics-color=always) +else () + message(STATUS "No colored compiler diagnostic set for '${CMAKE_CXX_COMPILER_ID}' compiler.") +endif () + diff --git a/cpp/demos/CMakeLists.txt b/cpp/demos/CMakeLists.txt new file mode 100644 index 0000000..fa3eeb2 --- /dev/null +++ b/cpp/demos/CMakeLists.txt @@ -0,0 +1,3 @@ +add_subdirectory(nmos-cpp-node) +add_subdirectory(ossrf-nmos-api) +add_subdirectory(gst-sender) diff --git a/cpp/demos/gst-sender/CMakeLists.txt b/cpp/demos/gst-sender/CMakeLists.txt new file mode 100644 index 0000000..a4f069f --- /dev/null +++ b/cpp/demos/gst-sender/CMakeLists.txt @@ -0,0 +1,34 @@ +cmake_minimum_required(VERSION 3.16) +project(gst-sender LANGUAGES CXX) + +file(GLOB_RECURSE ${PROJECT_NAME}_source_files *.cpp *.h) + +add_executable(${PROJECT_NAME} ${${PROJECT_NAME}_source_files}) + +target_include_directories(${PROJECT_NAME} PRIVATE ${fmt_INCLUDE_DIRS}) +target_link_libraries(${PROJECT_NAME} + PRIVATE project_options project_warnings + PUBLIC + bisect::project_warnings + bisect::expected + bisect::bisect_gst) + +find_package(PkgConfig REQUIRED) +pkg_search_module(gstreamer REQUIRED IMPORTED_TARGET gstreamer-1.0>=1.4) +pkg_search_module(gstreamer-app REQUIRED IMPORTED_TARGET gstreamer-app-1.0>=1.4) +pkg_search_module(gstreamer-audio REQUIRED IMPORTED_TARGET gstreamer-audio-1.0>=1.4) +pkg_search_module(gstreamer-video REQUIRED IMPORTED_TARGET gstreamer-video-1.0>=1.4) + + +target_link_libraries( + ${PROJECT_NAME} + PUBLIC + PkgConfig::gstreamer + PkgConfig::gstreamer-app + PkgConfig::gstreamer-audio + PkgConfig::gstreamer-video +) + +target_compile_features(${PROJECT_NAME} PUBLIC cxx_std_23) + +install(TARGETS ${PROJECT_NAME}) diff --git a/cpp/demos/gst-sender/main.cpp b/cpp/demos/gst-sender/main.cpp new file mode 100644 index 0000000..00b21d3 --- /dev/null +++ b/cpp/demos/gst-sender/main.cpp @@ -0,0 +1,388 @@ +#include "bisect/expected/match.h" +#include "bisect/expected/macros.h" +#include "bisect/expected.h" +#include "bisect/pipeline.h" +#include "bisect/initializer.h" +#include +#include +#include +#include + +namespace +{ + static void decoder_pad_added(GstElement* src, GstPad* new_pad, gpointer data) + { + (void)src; + auto* sink = GST_ELEMENT(data); + auto* sink_pad = gst_element_get_static_pad(sink, "sink"); + if(!gst_pad_is_linked(sink_pad)) + { + if(gst_pad_link(new_pad, sink_pad) == GST_PAD_LINK_OK) + { + g_print("Decoder linked to convert\n"); + } + else + { + g_printerr("Failed to link decoder to convert\n"); + } + } + gst_object_unref(sink_pad); + } + + bool file_exists(const std::string& filename) + { + std::ifstream file(filename); + return file.good(); + } + + bisect::maybe_ok gstreamer_pipeline_video() + { + // Create pipeline and check if all elements are created successfully + BST_ASSIGN_MUT(pipeline_holder, bisect::gst::pipeline::create("sender_pipeline")); + auto* pipeline = pipeline_holder.get(); + + // Add pipeline videotestsrc + auto* source = gst_element_factory_make("videotestsrc", "source"); + BST_ENFORCE(source != nullptr, "Failed creating GStreamer element videotestsrc"); + BST_ENFORCE(gst_bin_add(GST_BIN(pipeline), source), "Failed adding videotestsrc to the pipeline"); + + // Add pipeline capsfilter + auto* capsfilter = gst_element_factory_make("capsfilter", "capsfilter"); + BST_ENFORCE(capsfilter != nullptr, "Failed creating capsfilter"); + // Create caps for capsfilter + auto* caps = gst_caps_new_simple("video/x-raw", "format", G_TYPE_STRING, "RGB", "width", G_TYPE_INT, 640, + "height", G_TYPE_INT, 480, NULL); + BST_ENFORCE(caps != nullptr, "Failed creating GStreamer video caps"); + g_object_set(G_OBJECT(capsfilter), "caps", caps, NULL); + BST_ENFORCE(gst_bin_add(GST_BIN(pipeline), capsfilter), "Failed adding capsfilter to the pipeline"); + gst_caps_unref(caps); + + // Add pipeline queue1 + auto* queue1 = gst_element_factory_make("queue", "queue1"); + BST_ENFORCE(queue1 != nullptr, "Failed creating GStreamer element queue"); + BST_ENFORCE(gst_bin_add(GST_BIN(pipeline), queue1), "Failed adding queue to the pipeline"); + + // Add pipeline rtpvrawpay + auto* rtpvrawpay = gst_element_factory_make("rtpvrawpay", "rtpvrawpay"); + BST_ENFORCE(rtpvrawpay != nullptr, "Failed creating GStreamer element rtpvrawpay"); + BST_ENFORCE(gst_bin_add(GST_BIN(pipeline), rtpvrawpay), "Failed adding rtpvrawpay to the pipeline"); + + // Add pipeline queue2 + auto* queue2 = gst_element_factory_make("queue", "queue2"); + BST_ENFORCE(queue2 != nullptr, "Failed creating GStreamer element queue"); + BST_ENFORCE(gst_bin_add(GST_BIN(pipeline), queue2), "Failed adding queue to the pipeline"); + + // Add pipeline udpsink + auto* udpsink = gst_element_factory_make("udpsink", "udpsink"); + BST_ENFORCE(udpsink != nullptr, "Failed creating GStreamer element udpsink"); + // Set properties + g_object_set(G_OBJECT(udpsink), "host", "239.100.2.1", NULL); + g_object_set(G_OBJECT(udpsink), "port", 6000, NULL); + g_object_set(G_OBJECT(udpsink), "auto-multicast", TRUE, NULL); + g_object_set(G_OBJECT(udpsink), "multicast-iface", "eno2", NULL); + BST_ENFORCE(gst_bin_add(GST_BIN(pipeline), udpsink), "Failed adding udpsink to the pipeline"); + + // Link elements + BST_ENFORCE(gst_element_link_many(source, capsfilter, queue1, rtpvrawpay, queue2, udpsink, NULL), + "Failed linking GStreamer video pipeline"); + + // Setup runner + pipeline_holder.run_loop(); + + return {}; + } + + bisect::maybe_ok gstreamer_pipeline_video(const std::string& file_video) + { + // Create pipeline and check if all elements are created successfully + BST_ASSIGN_MUT(pipeline_holder, bisect::gst::pipeline::create("video-sender")); + auto* pipeline = pipeline_holder.get(); + + // Add pipeline filesrc + auto* filesrc = gst_element_factory_make("filesrc", "file-source"); + BST_ENFORCE(filesrc != nullptr, "Failed creating GStreamer element filesrc"); + g_object_set(G_OBJECT(filesrc), "location", file_video.c_str(), NULL); + BST_ENFORCE(gst_bin_add(GST_BIN(pipeline), filesrc), "Failed adding filesrc to the pipeline"); + + // Add pipeline decoder + auto* decoder = gst_element_factory_make("decodebin", "decoder"); + BST_ENFORCE(decoder != nullptr, "Failed creating GStreamer element decodebin"); + BST_ENFORCE(gst_bin_add(GST_BIN(pipeline), decoder), "Failed adding decodebin to the pipeline"); + + // Add pipeline videoconvert + auto* videoconvert = gst_element_factory_make("videoconvert", "video-convert"); + BST_ENFORCE(videoconvert != nullptr, "Failed creating GStreamer element videoconvert"); + BST_ENFORCE(gst_bin_add(GST_BIN(pipeline), videoconvert), "Failed adding videoconvert to the pipeline"); + + // Add pipeline videoscale + auto* videoscale = gst_element_factory_make("videoscale", "video-scale"); + BST_ENFORCE(videoscale != nullptr, "Failed creating GStreamer element videoscale"); + BST_ENFORCE(gst_bin_add(GST_BIN(pipeline), videoscale), "Failed adding videoscale to the pipeline"); + + // Add pipeline capsfilter + auto* capsfilter = gst_element_factory_make("capsfilter", "capsfilter"); + BST_ENFORCE(capsfilter != nullptr, "Failed creating GStreamer element capsfilter"); + BST_ENFORCE(gst_bin_add(GST_BIN(pipeline), capsfilter), "Failed adding capsfilter to the pipeline"); + + // Add pipeline queue1 + auto* queue1 = gst_element_factory_make("queue", "queue1"); + BST_ENFORCE(queue1 != nullptr, "Failed creating GStreamer element queue"); + BST_ENFORCE(gst_bin_add(GST_BIN(pipeline), queue1), "Failed adding queue to the pipeline"); + + // Add pipeline rtpvrawpay + auto* rtpvrawpay = gst_element_factory_make("rtpvrawpay", "rtpvrawpay"); + BST_ENFORCE(rtpvrawpay != nullptr, "Failed creating GStreamer element rtpvrawpay"); + BST_ENFORCE(gst_bin_add(GST_BIN(pipeline), rtpvrawpay), "Failed adding rtpvrawpay to the pipeline"); + + // Add pipeline queue2 + auto* queue2 = gst_element_factory_make("queue", "queue2"); + BST_ENFORCE(queue2 != nullptr, "Failed creating GStreamer element queue"); + BST_ENFORCE(gst_bin_add(GST_BIN(pipeline), queue2), "Failed adding queue to the pipeline"); + + // Add pipeline udpsink + auto* udpsink = gst_element_factory_make("udpsink", "udpsink"); + BST_ENFORCE(udpsink != nullptr, "Failed creating GStreamer element udpsink"); + // Set properties + g_object_set(G_OBJECT(udpsink), "host", "239.100.2.1", "port", 6000, "auto-multicast", TRUE, "multicast-iface", + "eno2", NULL); + BST_ENFORCE(gst_bin_add(GST_BIN(pipeline), udpsink), "Failed adding udpsink to the pipeline"); + + // Link elements + BST_ENFORCE(gst_element_link_many(filesrc, decoder, NULL), + "Failed linking GStreamer elements filesrc and decodebin"); + + // Set caps for capsfilter + auto* caps = gst_caps_new_simple("video/x-raw", "format", G_TYPE_STRING, "RGB", "width", G_TYPE_INT, 640, + "height", G_TYPE_INT, 480, NULL); + BST_ENFORCE(caps != nullptr, "Failed creating GStreamer video caps"); + g_object_set(G_OBJECT(capsfilter), "caps", caps, NULL); + gst_caps_unref(caps); + + // Connect decoder's pad-added signal + BST_ENFORCE(g_signal_connect(decoder, "pad-added", G_CALLBACK(decoder_pad_added), videoconvert) > 0, + "Failed signaling pad-added in decodebin"); + + BST_ENFORCE( + gst_element_link_many(videoconvert, videoscale, capsfilter, queue1, rtpvrawpay, queue2, udpsink, NULL), + "Failed linking elements in GStreamer video pipeline"); + + // Setup runner + pipeline_holder.run_loop(); + + return {}; + } + + bisect::maybe_ok gstreamer_pipeline_audio() + { + // Create pipeline and check if all elements are created successfully + BST_ASSIGN_MUT(pipeline_holder, bisect::gst::pipeline::create("audio-sender")); + auto* pipeline = pipeline_holder.get(); + + // Add pipeline pulsesrc + auto* source = gst_element_factory_make("pulsesrc", "source"); + BST_ENFORCE(source != nullptr, "Failed creating GStreamer element pulsesrc"); + BST_ENFORCE(gst_bin_add(GST_BIN(pipeline), source), "Failed adding pulsesrc to the pipeline"); + + // Add pipeline audioconvert + auto* convert = gst_element_factory_make("audioconvert", "convert"); + BST_ENFORCE(convert != nullptr, "Failed creating GStreamer element audioconvert"); + BST_ENFORCE(gst_bin_add(GST_BIN(pipeline), convert), "Failed adding audioconvert to the pipeline"); + + // Add pipeline videotestsrc + auto* resample = gst_element_factory_make("audioresample", "resample"); + BST_ENFORCE(resample != nullptr, "Failed creating GStreamer element audioresample"); + BST_ENFORCE(gst_bin_add(GST_BIN(pipeline), resample), "Failed adding audioresample to the pipeline"); + + // Add pipeline capsfilter + auto* capsfilter = gst_element_factory_make("capsfilter", "caps"); + BST_ENFORCE(capsfilter != nullptr, "Failed creating GStreamer element capsfilter"); + // Create caps for capsfilter + auto* caps = gst_caps_new_simple("audio/x-raw", "channels", G_TYPE_INT, 2, "rate", G_TYPE_INT, 48000, NULL); + BST_ENFORCE(caps != nullptr, "Failed creating GStreamer audio caps"); + g_object_set(G_OBJECT(capsfilter), "caps", caps, NULL); + BST_ENFORCE(gst_bin_add(GST_BIN(pipeline), capsfilter), "Failed adding capsfilter to the pipeline"); + gst_caps_unref(caps); + + // Add pipeline queue1 + auto* queue1 = gst_element_factory_make("queue", "queue1"); + BST_ENFORCE(queue1 != nullptr, "Failed creating GStreamer element queue"); + BST_ENFORCE(gst_bin_add(GST_BIN(pipeline), queue1), "Failed adding queue to the pipeline"); + + // Add pipeline payloader + auto* payloader = gst_element_factory_make("rtpL24pay", "payloader"); + BST_ENFORCE(payloader != nullptr, "Failed creating GStreamer element rtpL24pay"); + BST_ENFORCE(gst_bin_add(GST_BIN(pipeline), payloader), "Failed adding rtpL24pay to the pipeline"); + + // Add pipeline queue2 + auto* queue2 = gst_element_factory_make("queue", "queue2"); + BST_ENFORCE(queue2 != nullptr, "Failed creating GStreamer element queue"); + BST_ENFORCE(gst_bin_add(GST_BIN(pipeline), queue2), "Failed adding queue to the pipeline"); + + // Add pipeline udpsink + auto* sink = gst_element_factory_make("udpsink", "sink"); + BST_ENFORCE(sink != nullptr, "Failed creating GStreamer element udpsink"); + // Set properties for udpsink + g_object_set(G_OBJECT(sink), "host", "239.100.2.1", "port", 6000, "auto-multicast", TRUE, "multicast-iface", + "eno2", NULL); + BST_ENFORCE(gst_bin_add(GST_BIN(pipeline), sink), "Failed adding udpsink to the pipeline"); + + // Link elements + BST_ENFORCE(gst_element_link_many(source, convert, resample, capsfilter, queue1, payloader, queue2, sink, NULL), + "Failed linking GStreamer elements in audio pipeline"); + + // Setup runner + pipeline_holder.run_loop(); + + return {}; + } + + bisect::maybe_ok gstreamer_pipeline_audio(const std::string& file_music) + { + // Create pipeline and check if all elements are created successfully + BST_ASSIGN_MUT(pipeline_holder, bisect::gst::pipeline::create("audio-sender")); + auto* pipeline = pipeline_holder.get(); + + // Add pipeline filesrc + auto* filesrc = gst_element_factory_make("filesrc", "file-source"); + BST_ENFORCE(filesrc != nullptr, "Failed creating GStreamer element filesrc"); + g_object_set(G_OBJECT(filesrc), "location", file_music.c_str(), NULL); + BST_ENFORCE(gst_bin_add(GST_BIN(pipeline), filesrc), "Failed adding filesrc to the pipeline"); + + // Add pipeline decodebin + auto* decoder = gst_element_factory_make("decodebin", "decoder"); + BST_ENFORCE(decoder != nullptr, "Failed creating GStreamer element decodebin"); + BST_ENFORCE(gst_bin_add(GST_BIN(pipeline), decoder), "Failed adding decodebin to the pipeline"); + + // Add pipeline audioconvert + auto* audioconvert = gst_element_factory_make("audioconvert", "audio-convert"); + BST_ENFORCE(audioconvert != nullptr, "Failed creating GStreamer element audioconvert"); + BST_ENFORCE(gst_bin_add(GST_BIN(pipeline), audioconvert), "Failed adding audioconvert to the pipeline"); + + // Add pipeline audioresample + auto* audioresample = gst_element_factory_make("audioresample", "audio-resample"); + BST_ENFORCE(audioresample != nullptr, "Failed creating GStreamer element audioresample"); + BST_ENFORCE(gst_bin_add(GST_BIN(pipeline), audioresample), "Failed adding audioresample to the pipeline"); + + // Add pipeline capsfilter + auto* capsfilter = gst_element_factory_make("capsfilter", "caps-filter"); + BST_ENFORCE(capsfilter != nullptr, "Failed creating GStreamer element capsfilter"); + BST_ENFORCE(gst_bin_add(GST_BIN(pipeline), capsfilter), "Failed adding capsfilter to the pipeline"); + + // Add pipeline queue1 + auto* queue1 = gst_element_factory_make("queue", "queue1"); + BST_ENFORCE(queue1 != nullptr, "Failed creating GStreamer element queue"); + BST_ENFORCE(gst_bin_add(GST_BIN(pipeline), queue1), "Failed adding queue to the pipeline"); + + // Add pipeline payloader + auto* payloader = gst_element_factory_make("rtpL24pay", "payloader"); + BST_ENFORCE(payloader != nullptr, "Failed creating GStreamer element rtpL24pay"); + BST_ENFORCE(gst_bin_add(GST_BIN(pipeline), payloader), "Failed adding rtpL24pay to the pipeline"); + + // Add pipeline queue2 + auto* queue2 = gst_element_factory_make("queue", "queue2"); + BST_ENFORCE(queue2 != nullptr, "Failed creating GStreamer element queue"); + BST_ENFORCE(gst_bin_add(GST_BIN(pipeline), queue2), "Failed adding queue to the pipeline"); + + // Add pipeline udpsink + auto* udpsink = gst_element_factory_make("udpsink", "udp-sink"); + BST_ENFORCE(udpsink != nullptr, "Failed creating GStreamer element udpsink"); + // Set properties + g_object_set(G_OBJECT(udpsink), "host", "239.100.2.1", "port", 6000, "auto-multicast", TRUE, "multicast-iface", + "eno2", NULL); + BST_ENFORCE(gst_bin_add(GST_BIN(pipeline), udpsink), "Failed adding udpsink to the pipeline"); + + // Link elements + BST_ENFORCE(gst_element_link_many(filesrc, decoder, NULL), + "Failed linking GStreamer elements filsrc and decodebin"); + + // Set caps for capsfilter + auto* caps = gst_caps_new_simple("audio/x-raw", "media", G_TYPE_STRING, "audio", "payload", G_TYPE_INT, 96, + "clock-rate", G_TYPE_INT, 48000, "channels", G_TYPE_INT, 2, NULL); + BST_ENFORCE(caps != nullptr, "Failed creating GStreamer audio caps"); + g_object_set(G_OBJECT(capsfilter), "caps", caps, NULL); + gst_caps_unref(caps); + + // Connect decoder's pad-added signal + BST_ENFORCE(g_signal_connect(decoder, "pad-added", G_CALLBACK(decoder_pad_added), audioconvert) > 0, + "Failed signaling pad-added in decodebin"); + + // Link remaining elements + BST_ENFORCE(gst_element_link_many(audioconvert, audioresample, capsfilter, payloader, udpsink, NULL), + "Failed linking GStreamer elements in audio pipeline"); + + // Setup runner + pipeline_holder.run_loop(); + + return {}; + } +} // namespace + +int main(int argc, char* argv[]) +{ + bisect::gst::initializer initializer; + + switch(argc) + { + case 2: + if(argv[1] == std::string("-a")) + { + auto result = gstreamer_pipeline_audio(); + if(!result.has_value()) + { + fprintf(stderr, "error: %s", result.error().what()); + return -1; + } + return 0; + } + else if(argv[1] == std::string("-v")) + { + auto result = gstreamer_pipeline_video(); + if(!result.has_value()) + { + fprintf(stderr, "error: %s", result.error().what()); + return -1; + } + return 0; + } + fprintf(stderr, "error: %s option doesn't exist \n\n", argv[1]); + break; + case 3: + + if(!file_exists(argv[2])) + { + fprintf(stderr, "error: file %s doesn't exist \n", argv[2]); + return -1; + } + + if(argv[1] == std::string("-a")) + { + auto result = gstreamer_pipeline_audio(argv[2]); + if(!result.has_value()) + { + fprintf(stderr, "error: %s", result.error().what()); + return -1; + } + return 0; + } + else if(argv[1] == std::string("-v")) + { + auto result = gstreamer_pipeline_video(argv[2]); + if(!result.has_value()) + { + fprintf(stderr, "error: %s", result.error().what()); + return -1; + } + return 0; + } + fprintf(stderr, "error: %s option doesn't exist \n\n", argv[1]); + break; + + default: break; + } + + fprintf(stderr, "usage: %s -a -----> simple audio pipeline\n", argv[0]); + fprintf(stderr, " %s -a [file_source] -----> file sourced audio pipeline\n", argv[0]); + fprintf(stderr, " %s -v -----> simple video pipeline\n", argv[0]); + fprintf(stderr, " %s -v [file_source] -----> file sourced video pipeline\n\n", argv[0]); + return -1; +} diff --git a/cpp/demos/nmos-cpp-node/CMakeLists.txt b/cpp/demos/nmos-cpp-node/CMakeLists.txt new file mode 100644 index 0000000..d91bc1c --- /dev/null +++ b/cpp/demos/nmos-cpp-node/CMakeLists.txt @@ -0,0 +1,17 @@ +cmake_minimum_required(VERSION 3.16) +project(nmos-cpp-node LANGUAGES CXX) + +file(GLOB_RECURSE ${PROJECT_NAME}_source_files *.cpp *.h) + +find_package(nmos-cpp REQUIRED) + +add_executable(${PROJECT_NAME} ${${PROJECT_NAME}_source_files}) + +target_include_directories(${PROJECT_NAME} PRIVATE ${fmt_INCLUDE_DIRS}) +target_link_libraries(${PROJECT_NAME} + nmos-cpp::compile-settings + nmos-cpp::nmos-cpp) + +target_compile_features(${PROJECT_NAME} PUBLIC cxx_std_20) + +install(TARGETS ${PROJECT_NAME}) diff --git a/cpp/demos/nmos-cpp-node/config.json b/cpp/demos/nmos-cpp-node/config.json new file mode 100644 index 0000000..422cc91 --- /dev/null +++ b/cpp/demos/nmos-cpp-node/config.json @@ -0,0 +1,370 @@ +// Note: C++/JavaScript-style single and multi-line comments are permitted and ignored in nmos-cpp config files + +// Configuration settings and defaults +{ + // Custom settings for the example node implementation + + // node_tags, device_tags: used in resource tags fields + // "Each tag has a single key, but MAY have multiple values." + // See https://specs.amwa.tv/is-04/releases/v1.3.2/docs/APIs_-_Common_Keys.html#tags + // { + // "tag_1": [ "tag_1_value_1", "tag_1_value_2" ], + // "tag_2": [ "tag_2_value_1" ] + // } + //"node_tags": {}, + //"device_tags": {}, + + // how_many: provides for very basic testing of a node with many sub-resources of each type + //"how_many": 4, + + // activate_senders: controls whether to activate senders on start up (true, default) or not (false) + //"activate_senders": false, + + // senders, receivers: controls which kinds of sender and receiver are instantiated by the example node + // the values must be an array of unique strings identifying the kinds of 'port', like ["v", "a", "d"], see impl::ports + // when omitted, all ports are instantiated + //"senders": ["v", "a"], + //"receivers": [], + + // frame_rate: controls the grain_rate of video, audio an192.168.1.15d ancillary data sources and flows + // and the equivalent parameter constraint on video receivers + // the value must be an object like { "numerator": 25, "denominator": 1 } + //"frame_rate": { "numerator": 60000, "denominator": 1001 }, + + // frame_width, frame_height: control the frame_width and frame_height of video flows + //"frame_width": 3840, + //"frame_height": 2160, + + // interlace_mode: controls the interlace_mode of video flows, see nmos::interlace_mode + // when omitted, a default of "progressive" or "interlaced_tff" is used based on the frame_rate, etc. + //"interlace_mode": "progressive", + + // colorspace: controls the colorspace of video flows, see nmos::colorspace + //"colorspace": "BT709", + + // transfer_characteristic: controls the transfer characteristic system of video flows, see nmos::transfer_characteristic + //"transfer_characteristic": "SDR", + + // color_sampling: controls the color (sub-)sampling mode of video flows, see sdp::sampling + //"color_sampling": "YCbCr-4:2:2", + + // component_depth: controls the bits per component sample of video flows + //"component_depth": 10, + + // video_type: media type of video flows, e.g. "video/raw" or "video/jxsv", see nmos::media_types + //"video_type": "video/jxsv", + + // channel_count: controls the number of channels in audio sources + //"channel_count": 8, + + // smpte2022_7: controls whether senders and receivers have one leg (false) or two legs (true, default) + //"smpte2022_7": false, + + // Configuration settings and defaults for logging + + // error_log [registry, node]: filename for the error log or an empty string to write to stderr + //"error_log": "", + + // access_log [registry, node]: filename for the access log (in Common Log Format) or an empty string to discard + //"access_log": "", + + // logging_level [registry, node]: integer value, between 40 (least verbose, only fatal messages) and -40 (most verbose) + //"logging_level": 0, + + // logging_categories [registry, node]: array of logging categories to be included in the error log + //"logging_categories": ["node_implementation"], + + // Configuration settings and defaults for the NMOS APIs + + // host_name [registry, node]: the fully-qualified host name for which to advertise services, also used to construct response headers and fields in the data model + "host_name": "mozantech.home", // when omitted or an empty string, the default is used + + // domain [registry, node]: the domain on which to browse for services or an empty string to use the default domain (specify "local." to explictly select mDNS) + // "domain": "local.", + + // host_address/host_addresses [registry, node]: IP addresses used to construct response headers (e.g. 'Link' or 'Location'), and host and URL fields in the data model + "host_address": "192.168.1.15", + //"host_addresses": array-of-ip-address-strings, + + // is04_versions [registry, node]: used to specify the enabled API versions (advertised via 'api_ver') for a version-locked configuration + //"is04_versions": ["v1.2", "v1.3"], + + // is05_versions [node]: used to specify the enabled API versions for a version-locked configuration + //"is05_versions": ["v1.0", "v1.1"], + + // is07_versions [node]: used to specify the enabled API versions for a version-locked configuration + //"is07_versions": ["v1.0"], + + // is08_versions [node]: used to specify the enabled API versions for a version-locked configuration + //"is08_versions": ["v1.0"], + + // is09_versions [registry, node]: used to specify the enabled API versions for a version-locked configuration + //"is09_versions": ["v1.0"], + + // is10_versions [registry, node]: used to specify the enabled API versions for a version-locked configuration + //"is10_versions": ["v1.0"], + + // pri [registry, node]: used for the 'pri' TXT record; specifying nmos::service_priorities::no_priority (maximum value) disables advertisement completely + //"pri": 100, + + // highest_pri, lowest_pri [node]: used to specify the (inclusive) range of suitable 'pri' values of discovered Registration and System APIs, to avoid development and live systems colliding + //"highest_pri": 0, + //"lowest_pri": 2147483647, + + // authorization_highest_pri, authorization_lowest_pri [registry, node]: used to specify the (inclusive) range of suitable 'pri' values of discovered Authorization APIs, to avoid development and live systems colliding + //"authorization_highest_pri": 0, + //"authorization_lowest_pri": 2147483647, + + // discovery_backoff_min/discovery_backoff_max/discovery_backoff_factor [registry, node]: used to back-off after errors interacting with all discoverable service instances + // e.g. Registration APIs, System APIs, Authorization APIs or OCSP servers + //"discovery_backoff_min": 1, + //"discovery_backoff_max": 30, + //"discovery_backoff_factor": 1.5, + + // service_name_prefix [registry, node]: used as a prefix in the advertised service names ("__:", e.g. "nmos-cpp_node_127-0-0-1:3212") + //"service_name_prefix": "nmos-cpp" + + // registry_address [node]: IP address or host name used to construct request URLs for registry APIs (if not discovered via DNS-SD) + "registry_address": "192.168.1.15", + + // registry_version [node]: used to construct request URLs for registry APIs (if not discovered via DNS-SD) + // "registry_version": "v1.2", + + // port numbers [registry, node]: ports to which clients should connect for each API + + // http_port [registry, node]: if specified, this becomes the default port for each HTTP API and the next higher port becomes the default for each WebSocket API + //"http_port": 0, + + // registration_port [node]: used to construct request URLs for the registry's Registration API (if not discovered via DNS-SD) + "registration_port": 8010, + //"node_port": 3212, + //"connection_port": 3215, + //"events_port": 3216, + //"events_ws_port": 3217, + //"channelmapping_port": 3215, + // system_port [node]: used to construct request URLs for the System API (if not discovered via DNS-SD) + "system_port": 8010, + + // listen_backlog [registry, node]: the maximum length of the queue of pending connections, or zero for the implementation default (the implementation may not honour this value) + //"listen_backlog": 0, + + // registration_heartbeat_interval [registry, node]: + // "Nodes are expected to peform a heartbeat every 5 seconds by default." + // See https://specs.amwa.tv/is-04/releases/v1.2.0/docs/4.1._Behaviour_-_Registration.html#heartbeating + //"registration_heartbeat_interval": 5, + + // registration_request_max [node]: timeout for interactions with the Registration API /resource endpoint + //"registration_request_max": 30, + + // registration_heartbeat_max [node]: timeout for interactions with the Registration API /health/nodes endpoint + // Note that the default timeout is the same as the default heartbeat interval, in order that there is then a reasonable opportunity to try the next available Registration API + // though in some circumstances registration expiry could potentially still be avoided with a timeout that is (almost) twice the garbage collection interval... + //"registration_heartbeat_max": 5, + + // immediate_activation_max [node]: timeout for immediate activations within the Connection API /staged endpoint + //"immediate_activation_max": 30, + + // events_heartbeat_interval [node, client]: + // "Upon connection, the client is required to report its health every 5 seconds in order to maintain its session and subscription." + // See https://specs.amwa.tv/is-07/releases/v1.0.1/docs/5.2._Transport_-_Websocket.html#41-heartbeats + //"events_heartbeat_interval": 5, + + // events_expiry_interval [node]: + // "The server is expected to check health commands and after a 12 seconds timeout (2 consecutive missed health commands plus 2 seconds to allow for latencies) + // it should clear the subscriptions for that particular client and close the websocket connection." + // See https://specs.amwa.tv/is-07/releases/v1.0.1/docs/5.2._Transport_-_Websocket.html#41-heartbeats + //"events_expiry_interval": 12, + + // system_address [node]: IP address or host name used to construct request URLs for the System API (if not discovered via DNS-SD) + "system_address": "192.168.1.15" + + // system_version [node]: used to construct request URLs for the System API (if not discovered via DNS-SD) + // "system_version": "v1.0" + + // system_request_max [node]: timeout for interactions with the System API + //"system_request_max": 30, + + // Configuration settings and defaults for experimental extensions + + // seed id [registry, node]: optional, used to generate repeatable id values when running with the same configuration + //"seed_id": uuid-string, + + // label [registry, node]: used in resource label field + //"label": "", + + // description [registry, node]: used in resource description field + //"description": "", + + // port numbers [registry, node]: ports to which clients should connect for each API + // see http_port + + //"settings_port": 3209, + //"logging_port": 5106, + + // addresses [registry, node]: IP addresses on which to listen for each API, or empty string for the wildcard address + + // server_address [registry, node]: if specified, this becomes the default address on which to listen for each API instead of the wildcard address + //"server_address": "", + + // addresses [registry, node]: IP addresses on which to listen for specific APIs + + //"settings_address": "127.0.0.1", + //"logging_address": "", + + // client_address [registry, node]: IP address of the network interface to bind client connections + // for now, only supporting HTTP/HTTPS client connections on Linux + //"client_address": "", + + // logging_limit [registry, node]: maximum number of log events cached for the Logging API + //"logging_limit": 1234, + + // logging_paging_default/logging_paging_limit [registry, node]: default/maximum number of results per "page" when using the Logging API (a client may request a lower limit) + //"logging_paging_default": 100, + //"logging_paging_limit": 100, + + // http_trace [registry, node]: whether server should enable (default) or disable support for HTTP TRACE + //"http_trace": true, + + // proxy_map [registry, node]: mapping between the port numbers to which the client connects, and the port numbers on which the server should listen, if different + // for use with a reverse proxy; each element of the array is an object like { "client_port": 80, "server_port": 8080 } + //"proxy_map": array-of-mappings, + + // proxy_address [registry, node]: address of the forward proxy to use when making HTTP requests or WebSocket connections, or an empty string for no proxy + //"proxy_address": "127.0.0.1", + + // proxy_port [registry, node]: forward proxy port + //"proxy_port": 8080, + + // discovery_mode [node]: whether the discovered host name (1) or resolved addresses (2) are used to construct request URLs for Registration APIs or System APIs + //"discovery_mode": 1, + + // href_mode [registry, node]: whether the host name (1), addresses (2) or both (3) are used to construct response headers, and host and URL fields in the data model + //"href_mode": 1, + + // client_secure [registry, node]: whether clients should use a secure connection for communication (https and wss) + // when true, CA root certificates must also be configured + //"client_secure": false, + + // ca_certificate_file [registry, node]: full path of certification authorities file in PEM format + // on Windows, if C++ REST SDK is built with CPPREST_HTTP_CLIENT_IMPL=winhttp (reported as "client=winhttp" by nmos::get_build_settings_info) + // the trusted root CA certificates must also be imported into the certificate store + //"ca_certificate_file": "ca.pem", + + // server_secure [registry, node]: whether server should listen for secure connection for communication (https and wss) + // e.g. typically false when using a reverse proxy, or the same as client_secure otherwise + // when true, server certificates etc. must also be configured + //"server_secure": false, + + // server_certificates [registry, node]: an array of server certificate objects, each has the name of the key algorithm, the full paths of private key file and certificate chain file + // each value must be an object like { "key_algorithm": "ECDSA", "private_key_file": "server-ecdsa-key.pem", "certificate_chain_file": "server-ecdsa-chain.pem" } + // key_algorithm (attribute of server_certificates objects): name of the key algorithm for the certificate, see nmos::key_algorithm + // private_key_file (attribute of server_certificates objects): full path of private key file in PEM format + // certificate_chain_file (attribute of server_certificates object): full path of certificate chain file in PEM format, which must be sorted + // starting with the server's certificate, followed by any intermediate CA certificates, and ending with the highest level (root) CA + // on Windows, if C++ REST SDK is built with CPPREST_HTTP_LISTENER_IMPL=httpsys (reported as "listener=httpsys" by nmos::get_build_settings_info) + // one of the certificates must also be bound to each port e.g. using 'netsh add sslcert' + //"server_certificates": [{"key_algorithm": "ECDSA", "private_key_file": "server-ecdsa-key.pem", "certificate_chain_file": "server-ecdsa-chain.pem"}, {"key_algorithm": "RSA", "private_key_file": "server-rsa-key.pem", "certificate_chain_file": "server-rsa-chain.pem"}], + + // validate_certificates [registry, node]: boolean value, false (ignore all server certificate validation errors), or true (do not ignore, the default behaviour) + //"validate_certificates": true, + + // dh_param_file [registry, node]: Diffie-Hellman parameters file in PEM format for ephemeral key exchange support, or empty string for no support + //"dh_param_file": "dhparam.pem", + + // system_interval_min/system_interval_max [node]: used to poll for System API changes; default is about one hour + //"system_interval_min": 3600, + //"system_interval_max": 3660, + + // hsts_max_age [registry, node]: the HTTP Strict-Transport-Security response header's max-age value; default is approximately 365 days + // (the header is omitted if server_secure is false, or hsts_max_age is negative) + // See https://tools.ietf.org/html/rfc6797#section-6.1.1 + //"hsts_max_age": 31536000, + + // hsts_include_sub_domains [registry, node]: the HTTP Strict-Transport-Security HTTP response header's includeSubDomains value + // See https://tools.ietf.org/html/rfc6797#section-6.1.2 + //"hsts_include_sub_domains": false, + + // ocsp_interval_min/ocsp_interval_max [registry, node]: used to poll for certificate status (OCSP) changes; default is about one hour + // Note that if half of the server certificate expiry time is shorter, then the ocsp_interval_min/max will be overridden by it + //"ocsp_interval_min": 3600, + //"ocsp_interval_max": 3660, + + // ocsp_request_max [registry, node]: timeout for interactions with the OCSP server + //"ocsp_request_max": 30, + + // authorization_address [registry, node]: IP address or host name used to construct request URLs for the Authorization API (if not discovered via DNS-SD) + //"authorization_address": ip-address-string, + + // authorization_port [registry, node]: used to construct request URLs for the authorization server's Authorization API (if not discovered via DNS-SD) + //"authorization_port" 443, + + // authorization_version [registry, node]: used to construct request URLs for Authorization API (if not discovered via DNS-SD) + //"authorization_version": "v1.0", + + // authorization_selector [registry, node]: used to construct request URLs for the authorization API (if not discovered via DNS-SD) + //"authorization_selector", "", + + // authorization_request_max [registry, node]: timeout for interactions with the Authorization API /certs & /token endpoints + //"authorization_request_max": 30, + + // fetch_authorization_public_keys_interval_min/fetch_authorization_public_keys_interval_max [registry, node]: used to poll for Authorization API public keys changes; default is about one hour + // "Resource Servers (Nodes) SHOULD seek to fetch public keys from the Authorization Server at least once every hour. Resource Servers MUST vary their retrieval + // interval at random by up to at least one minute to avoid overloading the Authorization Server due to Resource Servers synchronising their retrieval time." + // See https://specs.amwa.tv/is-10/releases/v1.0.0/docs/4.1._Behaviour_-_Authorization_Servers.html#authorization-server-public-keys + //"fetch_authorization_public_keys_interval_min": 3600, + //"fetch_authorization_public_keys_interval_max": 3660, + + // access_token_refresh_interval [node]: time interval (in seconds) to refresh access token from Authorization Server + // It specified the access token refresh period otherwise Bearer token's expires_in is used instead. + // See https://specs.amwa.tv/is-10/releases/v1.0.0/docs/4.4._Behaviour_-_Access_Tokens.html#access-token-lifetime + //"access_token_refresh_interval": -1, + + // client_authorization [node]: whether clients should use authorization to access protected APIs + //"client_authorization": false, + + // server_authorization [registry, node]: whether server should use authorization to protect its APIs + //"server_authorization": false, + + // authorization_code_flow_max [node]: timeout for the authorization code flow (in seconds) + // No timeout if value is set to -1, default to 30 seconds + //"authorization_code_flow_max": 30, + + // authorization_flow [node]: used to specify the authorization flow for the registered scopes + // supported flow are authorization_code and client_credentials + // client_credentials SHOULD only be used for NO user interface node, otherwise authorization_code MUST be used + //"authorization_flow": "authorization_code", + + // authorization_redirect_port [node]: redirect URL port for listening authorization code, used for client registration + //"authorization_redirect_port": 3218, + + // initial_access_token [node]: initial access token giving access to the client registration endpoint for non-opened registration + //"initial_access_token", "", + + // authorization_scopes [node]: used to specify the supported scopes for client registration + // supported scopes are registration, query, node, connection, events and channelmapping + //"authorization_scopes": [ "registration" ], + + // token_endpoint_auth_method [node]: String indicator of the requested authentication method for the token endpoint + // supported methods are none, client_secret_basic and private_key_jwt, default to client_secret_basic, where none is used for public client + // when using private_key_jwt, the JWT is created and signed by the node's private key + //"token_endpoint_auth_method": "client_secret_basic", + + // jwks_uri_port [node]: JWKs URL port for providing JSON Web Key Set (public keys) to Authorization Server for verifing client_assertion, used for client registration + //"jwks_uri_port": 3218, + + // validate_openid_client [node]: boolean value, false (bypass openid connect client validation), or true (do not bypass, the default behaviour) + //"validate_openid_client": true, + + // no_trailing_dot_for_authorization_callback_uri [node]: used to specify whether no trailing dot FQDN should be used to construct the URL for the authorization server callbacks + // as it is because not all Authorization server can cope with URL with trailing dot, default to true + //"no_trailing_dot_for_authorization_callback_uri": true, + + // retry_after [registry, node]: used to specify the HTTP Retry-After header to indicate the number of seconds when the client may retry its request again, default to 5 seconds + // "Where a Resource Server has no matching public key for a given token, it SHOULD attempt to obtain the missing public key via the the token iss + // claim as specified in RFC 8414 section 3. In cases where the Resource Server needs to fetch a public key from a remote Authorization Server it + // MAY temporarily respond with an HTTP 503 code in order to avoid blocking the incoming authorized request. When a HTTP 503 code is used, the Resource + // Server SHOULD include an HTTP Retry-After header to indicate when the client may retry its request. + // If the Resource Server fails to verify a token using all public keys available it MUST reject the token." + //"service_unavailable_retry_after": 5, + +} diff --git a/cpp/demos/nmos-cpp-node/main.cpp b/cpp/demos/nmos-cpp-node/main.cpp new file mode 100644 index 0000000..21d7062 --- /dev/null +++ b/cpp/demos/nmos-cpp-node/main.cpp @@ -0,0 +1,339 @@ +#include +#include +// #include "cpprest/grant_type.h" +// #include "cpprest/token_endpoint_auth_method.h" +#include "nmos/api_utils.h" // for make_api_listener +// #include "nmos/authorization_behaviour.h" +// #include "nmos/authorization_redirect_api.h" +// #include "nmos/authorization_state.h" +// #include "nmos/jwks_uri_api.h" +#include "nmos/log_gate.h" +#include "nmos/model.h" +#include "nmos/node_server.h" +#include "nmos/ocsp_behaviour.h" +#include "nmos/ocsp_response_handler.h" +#include "nmos/ocsp_state.h" +#include "nmos/process_utils.h" +#include "nmos/server.h" +#include "nmos/server_utils.h" // for make_http_listener_config +#include "node_implementation.h" + +int main(int argc, char* argv[]) +{ + // Construct our data models including mutexes to protect them + + nmos::node_model node_model; + + nmos::experimental::log_model log_model; + + // Streams for logging, initially configured to write errors to stderr and to discard the access log + std::filebuf error_log_buf; + std::ostream error_log(std::cerr.rdbuf()); + std::filebuf access_log_buf; + std::ostream access_log(&access_log_buf); + + // Logging should all go through this logging gateway + nmos::experimental::log_gate gate(error_log, access_log, log_model); + + try + { + slog::log(gate, SLOG_FLF) << "Starting nmos-cpp node"; + + // Settings can be passed on the command-line, directly or in a configuration file, and a few may be changed + // dynamically by PATCH to /settings/all on the Settings API + // + // * "logging_level": integer value, between 40 (least verbose, only fatal messages) and -40 (most verbose) + // * "registry_address": used to construct request URLs for registry APIs (if not discovered via DNS-SD) + // + // E.g. + // + // # ./nmos-cpp-node "{\"logging_level\":-40}" + // # ./nmos-cpp-node config.json + // # curl -X PATCH -H "Content-Type: application/json" http://localhost:3209/settings/all -d + // "{\"logging_level\":-40}" # curl -X PATCH -H "Content-Type: application/json" + // http://localhost:3209/settings/all -T config.json + + if(argc > 1) + { + std::error_code error; + node_model.settings = web::json::value::parse(utility::s2us(argv[1]), error); + if(error) + { + std::ifstream file(argv[1]); + // check the file can be opened, and is parsed to an object + file.exceptions(std::ios_base::failbit); + node_model.settings = web::json::value::parse(file); + node_model.settings.as_object(); + } + } + + // Prepare run-time default settings (different than header defaults) + + nmos::insert_node_default_settings(node_model.settings); + + // copy to the logging settings + // hmm, this is a bit icky, but simplest for now + log_model.settings = node_model.settings; + + // the logging level is a special case because we want to turn it into an atomic value + // that can be read by logging statements without locking the mutex protecting the settings + log_model.level = nmos::fields::logging_level(log_model.settings); + + // Reconfigure the logging streams according to settings + // (obviously, until this point, the logging gateway has its default behaviour...) + + if(!nmos::fields::error_log(node_model.settings).empty()) + { + error_log_buf.open(nmos::fields::error_log(node_model.settings), std::ios_base::out | std::ios_base::app); + auto lock = log_model.write_lock(); + error_log.rdbuf(&error_log_buf); + } + + if(!nmos::fields::access_log(node_model.settings).empty()) + { + access_log_buf.open(nmos::fields::access_log(node_model.settings), std::ios_base::out | std::ios_base::app); + auto lock = log_model.write_lock(); + access_log.rdbuf(&access_log_buf); + } + + // Log the process ID and initial settings + + slog::log(gate, SLOG_FLF) << "Process ID: " << nmos::details::get_process_id(); + slog::log(gate, SLOG_FLF) << "Build settings: " << nmos::get_build_settings_info(); + slog::log(gate, SLOG_FLF) << "Initial settings: " << node_model.settings.serialize(); + + // Set up the callbacks between the node server and the underlying implementation + + auto node_implementation = make_node_implementation(node_model, gate); + +// only implement communication with OCSP server if http_listener supports OCSP stapling +// cf. preprocessor conditions in nmos::make_http_listener_config +// Note: the get_ocsp_response callback must be set up before executing the make_node_server where +// make_http_listener_config is set up +#if !defined(_WIN32) || defined(CPPREST_FORCE_HTTP_LISTENER_ASIO) + nmos::experimental::ocsp_state ocsp_state; + if(nmos::experimental::fields::server_secure(node_model.settings)) + { + node_implementation.on_get_ocsp_response(nmos::make_ocsp_response_handler(ocsp_state, gate)); + } +#endif + + // only implement communication with Authorization server if IS-10/BCP-003-02 is required + // cf. preprocessor conditions in nmos::make_node_api, nmos::make_connection_api, nmos::make_events_api, + // nmos::make_channelmapping_api, make_events_ws_validate_handler + /* nmos::experimental::authorization_state authorization_state; + if (nmos::experimental::fields::server_authorization(node_model.settings)) + { + node_implementation + .on_validate_authorization(nmos::experimental::make_validate_authorization_handler(node_model, + authorization_state, nmos::experimental::make_validate_authorization_token_handler(authorization_state, + gate), gate)) + .on_ws_validate_authorization(nmos::experimental::make_ws_validate_authorization_handler(node_model, + authorization_state, nmos::experimental::make_validate_authorization_token_handler(authorization_state, + gate), gate)); + } + if (nmos::experimental::fields::client_authorization(node_model.settings)) + { + node_implementation + .on_get_authorization_bearer_token(nmos::experimental::make_get_authorization_bearer_token_handler(authorization_state, + gate)) + .on_load_authorization_clients(nmos::experimental::make_load_authorization_clients_handler(node_model.settings, + gate)) + .on_save_authorization_client(nmos::experimental::make_save_authorization_client_handler(node_model.settings, + gate)) .on_load_rsa_private_keys(nmos::make_load_rsa_private_keys_handler(node_model.settings, gate)) // may + be omitted, only required for OAuth client which is using Private Key JWT as the requested authentication + method for the token endpoint + .on_request_authorization_code(nmos::experimental::make_request_authorization_code_handler(gate)); + // may be omitted, only required for OAuth client which is using the Authorization Code Flow to obtain the + access token + }*/ + + // Set up the node server + + auto node_server = nmos::experimental::make_node_server(node_model, node_implementation, log_model, gate); + + // Add the underlying implementation, which will set up the node resources, etc. + + node_server.thread_functions.push_back([&] { node_implementation_thread(node_model, gate); }); + +// only implement communication with OCSP server if http_listener supports OCSP stapling +// cf. preprocessor conditions in nmos::make_http_listener_config +#if !defined(_WIN32) || defined(CPPREST_FORCE_HTTP_LISTENER_ASIO) + if(nmos::experimental::fields::server_secure(node_model.settings)) + { + auto load_ca_certificates = node_implementation.load_ca_certificates; + auto load_server_certificates = node_implementation.load_server_certificates; + node_server.thread_functions.push_back([&, load_ca_certificates, load_server_certificates] { + nmos::ocsp_behaviour_thread(node_model, ocsp_state, load_ca_certificates, load_server_certificates, + gate); + }); + } +#endif + + // only implement communication with Authorization server if IS-10/BCP-003-02 is required + /* if (nmos::experimental::fields::client_authorization(node_model.settings)) + { + std::map api_routers; + + // Configure the authorization_redirect API (require for Authorization Code Flow support) + + if (web::http::oauth2::experimental::grant_types::authorization_code.name == + nmos::experimental::fields::authorization_flow(node_model.settings)) + { + auto load_ca_certificates = node_implementation.load_ca_certificates; + auto load_rsa_private_keys = node_implementation.load_rsa_private_keys; + api_routers[{ {}, nmos::experimental::fields::authorization_redirect_port(node_model.settings) + }].mount({}, nmos::experimental::make_authorization_redirect_api(node_model, authorization_state, + load_ca_certificates, load_rsa_private_keys, gate)); + } + + // Configure the jwks_uri API (require for Private Key JWK support) + + if (web::http::oauth2::experimental::token_endpoint_auth_methods::private_key_jwt.name == + nmos::experimental::fields::token_endpoint_auth_method(node_model.settings)) + { + auto load_rsa_private_keys = node_implementation.load_rsa_private_keys; + api_routers[{ {}, nmos::experimental::fields::jwks_uri_port(node_model.settings) }].mount({}, + nmos::experimental::make_jwk_uri_api(node_model, load_rsa_private_keys, gate)); + } + + auto http_config = nmos::make_http_listener_config(node_model.settings, + node_implementation.load_server_certificates, node_implementation.load_dh_param, + node_implementation.get_ocsp_response, gate); const auto server_secure = + nmos::experimental::fields::server_secure(node_model.settings); const auto hsts = + nmos::experimental::get_hsts(node_model.settings); for (auto& api_router : api_routers) + { + auto found = node_server.api_routers.find(api_router.first); + + const auto& host = !api_router.first.first.empty() ? api_router.first.first : + web::http::experimental::listener::host_wildcard; const auto& port = + nmos::experimental::server_port(api_router.first.second, node_model.settings); + + if (node_server.api_routers.end() != found) + { + const auto uri = web::http::experimental::listener::make_listener_uri(server_secure, host, port); + auto listener = std::find_if(node_server.http_listeners.begin(), node_server.http_listeners.end(), + [&](const web::http::experimental::listener::http_listener& listener) { return listener.uri() == uri; }); if + (node_server.http_listeners.end() != listener) + { + found->second.pop_back(); // remove the api_finally_handler which was previously added in the + make_node_server, the api_finally_handler will be re-inserted in the make_api_listener + node_server.http_listeners.erase(listener); + } + found->second.mount({}, api_router.second); + node_server.http_listeners.push_back(nmos::make_api_listener(server_secure, host, port, + found->second, http_config, hsts, gate)); + } + else + { + node_server.http_listeners.push_back(nmos::make_api_listener(server_secure, host, port, + api_router.second, http_config, hsts, gate)); + } + } + } + + if (!nmos::experimental::fields::http_trace(node_model.settings)) + { + // Disable TRACE method + + for (auto& http_listener : node_server.http_listeners) + { + http_listener.support(web::http::methods::TRCE, [](web::http::http_request req) { + req.reply(web::http::status_codes::MethodNotAllowed); }); + } + } */ + + // only implement communication with Authorization server if IS-10/BCP-003-02 is required + /* if (nmos::experimental::fields::client_authorization(node_model.settings) || + nmos::experimental::fields::server_authorization(node_model.settings)) + { + // IS-10 client registration, fetch access token, and fetch authorization server token public key + // see https://specs.amwa.tv/is-10/releases/v1.0.0/docs/4.2._Behaviour_-_Clients.html + // and + https://specs.amwa.tv/is-10/releases/v1.0.0/docs/4.5._Behaviour_-_Resource_Servers.html#public-keys auto + load_ca_certificates = node_implementation.load_ca_certificates; auto load_rsa_private_keys = + node_implementation.load_rsa_private_keys; auto load_authorization_clients = + node_implementation.load_authorization_clients; auto save_authorization_client = + node_implementation.save_authorization_client; auto request_authorization_code = + node_implementation.request_authorization_code; node_server.thread_functions.push_back([&, + load_ca_certificates, load_rsa_private_keys, load_authorization_clients, save_authorization_client, + request_authorization_code] { nmos::experimental::authorization_behaviour_thread(node_model, + authorization_state, load_ca_certificates, load_rsa_private_keys, load_authorization_clients, + save_authorization_client, request_authorization_code, gate); }); + + if (nmos::experimental::fields::server_authorization(node_model.settings)) + { + // When no matching public key for a given access token, it SHOULD attempt to obtain the missing + public key + // via the the token iss claim as specified in RFC 8414 section 3. + // see https://tools.ietf.org/html/rfc8414#section-3 + // and + https://specs.amwa.tv/is-10/releases/v1.0.0/docs/4.5._Behaviour_-_Resource_Servers.html#public-keys + node_server.thread_functions.push_back([&, load_ca_certificates] { + nmos::experimental::authorization_token_issuer_thread(node_model, authorization_state, load_ca_certificates, + gate); }); + } + } */ + + // Open the API ports and start up node operation (including the DNS-SD advertisements) + + slog::log(gate, SLOG_FLF) << "Preparing for connections"; + + nmos::server_guard node_server_guard(node_server); + + slog::log(gate, SLOG_FLF) << "Ready for connections"; + + // Wait for a process termination signal + nmos::details::wait_term_signal(); + + slog::log(gate, SLOG_FLF) << "Closing connections"; + } + catch(const web::json::json_exception& e) + { + // most likely from incorrect syntax or incorrect value types in the command line settings + slog::log(gate, SLOG_FLF) << "JSON error: " << e.what(); + return 1; + } + catch(const web::http::http_exception& e) + { + slog::log(gate, SLOG_FLF) + << "HTTP error: " << e.what() << " [" << e.error_code() << "]"; + return 1; + } + catch(const web::websockets::websocket_exception& e) + { + slog::log(gate, SLOG_FLF) + << "WebSocket error: " << e.what() << " [" << e.error_code() << "]"; + return 1; + } + catch(const std::ios_base::failure& e) + { + // most likely from failing to open the command line settings file + slog::log(gate, SLOG_FLF) << "File error: " << e.what(); + return 1; + } + catch(const std::system_error& e) + { + slog::log(gate, SLOG_FLF) << "System error: " << e.what() << " [" << e.code() << "]"; + return 1; + } + catch(const std::runtime_error& e) + { + slog::log(gate, SLOG_FLF) << "Implementation error: " << e.what(); + return 1; + } + catch(const std::exception& e) + { + slog::log(gate, SLOG_FLF) << "Unexpected exception: " << e.what(); + return 1; + } + catch(...) + { + slog::log(gate, SLOG_FLF) << "Unexpected unknown exception"; + return 1; + } + + slog::log(gate, SLOG_FLF) << "Stopping nmos-cpp node"; + + return 0; +} diff --git a/cpp/demos/nmos-cpp-node/node_implementation.cpp b/cpp/demos/nmos-cpp-node/node_implementation.cpp new file mode 100644 index 0000000..386c5b0 --- /dev/null +++ b/cpp/demos/nmos-cpp-node/node_implementation.cpp @@ -0,0 +1,1586 @@ +#include "node_implementation.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include "pplx/pplx_utils.h" // for pplx::complete_after, etc. +#include "cpprest/host_utils.h" +#ifdef HAVE_LLDP +#include "lldp/lldp_manager.h" +#endif +#include "nmos/activation_mode.h" +#include "nmos/capabilities.h" +#include "nmos/channels.h" +#include "nmos/channelmapping_resources.h" +#include "nmos/clock_name.h" +#include "nmos/colorspace.h" +#include "nmos/connection_resources.h" +#include "nmos/connection_events_activation.h" +#include "nmos/events_resources.h" +#include "nmos/format.h" +#include "nmos/group_hint.h" +#include "nmos/interlace_mode.h" +#ifdef HAVE_LLDP +#include "nmos/lldp_manager.h" +#endif +#include "nmos/media_type.h" +#include "nmos/model.h" +#include "nmos/node_interfaces.h" +#include "nmos/node_resource.h" +#include "nmos/node_resources.h" +#include "nmos/node_server.h" +#include "nmos/random.h" +#include "nmos/sdp_utils.h" +#include "nmos/slog.h" +#include "nmos/st2110_21_sender_type.h" +#include "nmos/system_resources.h" +#include "nmos/transfer_characteristic.h" +#include "nmos/transport.h" +#include "nmos/video_jxsv.h" +#include "sdp/sdp.h" + +// example node implementation details +namespace impl +{ + // custom logging category for the example node implementation thread + namespace categories + { + const nmos::category node_implementation{"node_implementation"}; + } + + // custom settings for the example node implementation + namespace fields + { + // node_tags, device_tags: used in resource tags fields + // "Each tag has a single key, but MAY have multiple values." + // See https://specs.amwa.tv/is-04/releases/v1.3.2/docs/APIs_-_Common_Keys.html#tags + // { + // "tag_1": [ "tag_1_value_1", "tag_1_value_2" ], + // "tag_2": [ "tag_2_value_1" ] + // } + const web::json::field_as_value_or node_tags{U("node_tags"), web::json::value::object()}; + const web::json::field_as_value_or device_tags{U("device_tags"), web::json::value::object()}; + + // how_many: provides for very basic testing of a node with many sub-resources of each type + const web::json::field_as_integer_or how_many{U("how_many"), 1}; + + // activate_senders: controls whether to activate senders on start up (true, default) or not (false) + const web::json::field_as_bool_or activate_senders{U("activate_senders"), true}; + + // senders, receivers: controls which kinds of sender and receiver are instantiated by the example node + // the values must be an array of unique strings identifying the kinds of 'port', like ["v", "a", "d"], see + // impl::ports when omitted, all ports are instantiated + const web::json::field_as_value_or senders{U("senders"), {}}; + const web::json::field_as_value_or receivers{U("receivers"), {}}; + + // frame_rate: controls the grain_rate of video, audio and ancillary data sources and flows + // and the equivalent parameter constraint on video receivers + // the value must be an object like { "numerator": 25, "denominator": 1 } + // hm, unfortunately can't use nmos::make_rational(nmos::rates::rate25) during static initialization + const web::json::field_as_value_or frame_rate{ + U("frame_rate"), web::json::value_of({{nmos::fields::numerator, 25}, {nmos::fields::denominator, 1}})}; + + // frame_width, frame_height: control the frame_width and frame_height of video flows + const web::json::field_as_integer_or frame_width{U("frame_width"), 1920}; + const web::json::field_as_integer_or frame_height{U("frame_height"), 1080}; + + // interlace_mode: controls the interlace_mode of video flows, see nmos::interlace_mode + // when omitted, a default of "progressive" or "interlaced_tff" is used based on the frame_rate, etc. + const web::json::field_as_string interlace_mode{U("interlace_mode")}; + + // colorspace: controls the colorspace of video flows, see nmos::colorspace + const web::json::field_as_string_or colorspace{U("colorspace"), U("BT709")}; + + // transfer_characteristic: controls the transfer characteristic system of video flows, see + // nmos::transfer_characteristic + const web::json::field_as_string_or transfer_characteristic{U("transfer_characteristic"), U("SDR")}; + + // color_sampling: controls the color (sub-)sampling mode of video flows, see sdp::sampling + const web::json::field_as_string_or color_sampling{U("color_sampling"), U("YCbCr-4:2:2")}; + + // component_depth: controls the bits per component sample of video flows + const web::json::field_as_integer_or component_depth{U("component_depth"), 10}; + + // video_type: media type of video flows, e.g. "video/raw" or "video/jxsv", see nmos::media_types + const web::json::field_as_string_or video_type{U("video_type"), U("video/raw")}; + + // channel_count: controls the number of channels in audio sources + const web::json::field_as_integer_or channel_count{U("channel_count"), 4}; + + // 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}; + } // namespace fields + + nmos::interlace_mode get_interlace_mode(const nmos::settings& settings); + + // the different kinds of 'port' (standing for the format/media type/event type) implemented by the example node + // each 'port' of the example node has a source, flow, sender and/or compatible receiver + DEFINE_STRING_ENUM(port) + namespace ports + { + // video/raw, video/jxsv, etc. + const port video{U("v")}; + // audio/L24 + const port audio{U("a")}; + // video/smpte291 + const port data{U("d")}; + // video/SMPTE2022-6 + const port mux{U("m")}; + + // example measurement event + const port temperature{U("t")}; + // example boolean event + const port burn{U("b")}; + // example string event + const port nonsense{U("s")}; + // example number/enum event + const port catcall{U("c")}; + + const std::vector rtp{video, audio, data, mux}; + const std::vector ws{temperature, burn, nonsense, catcall}; + const std::vector all{boost::copy_range>(boost::range::join(rtp, ws))}; + } // namespace ports + + bool is_rtp_port(const port& port); + bool is_ws_port(const port& port); + std::vector parse_ports(const web::json::value& value); + + const std::vector channels_repeat{{U("Left Channel"), nmos::channel_symbols::L}, + {U("Right Channel"), nmos::channel_symbols::R}, + {U("Center Channel"), nmos::channel_symbols::C}, + {U("Low Frequency Effects Channel"), nmos::channel_symbols::LFE}}; + + // find interface with the specified address + std::vector::const_iterator + find_interface(const std::vector& interfaces, + const utility::string_t& address); + + // generate repeatable ids for the example node's resources + nmos::id make_id(const nmos::id& seed_id, const nmos::type& type, const port& port = {}, int index = 0); + std::vector make_ids(const nmos::id& seed_id, const nmos::type& type, const port& port, int how_many = 1); + std::vector make_ids(const nmos::id& seed_id, const nmos::type& type, const std::vector& ports, + int how_many = 1); + std::vector make_ids(const nmos::id& seed_id, const std::vector& types, + const std::vector& ports, int how_many = 1); + + // generate a repeatable source-specific multicast address for each leg of a sender + utility::string_t make_source_specific_multicast_address_v4(const nmos::id& id, int leg = 0); + + // add a selection of parents to a source or flow + void insert_parents(nmos::resource& resource, const nmos::id& seed_id, const port& port, int index); + + // add a helpful suffix to the label of a sub-resource for the example node + void set_label_description(nmos::resource& resource, const port& port, int index); + + // add an example "natural grouping" hint to a sender or receiver + void insert_group_hint(nmos::resource& resource, const port& port, int index); + + // specific event types used by the example node + 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")); +} // namespace impl + +// forward declarations for node_implementation_thread +void node_implementation_init(nmos::node_model& model, slog::base_gate& gate); +void node_implementation_run(nmos::node_model& model, slog::base_gate& gate); +nmos::connection_resource_auto_resolver make_node_implementation_auto_resolver(const nmos::settings& settings); +nmos::connection_sender_transportfile_setter +make_node_implementation_transportfile_setter(const nmos::resources& node_resources, const nmos::settings& settings); + +struct node_implementation_init_exception +{ +}; + +// This is an example of how to integrate the nmos-cpp library with a device-specific underlying implementation. +// It constructs and inserts a node resource and some sub-resources into the model, based on the model settings, +// starts background tasks to emit regular events from the temperature event source, and then waits for shutdown. +void node_implementation_thread(nmos::node_model& model, slog::base_gate& gate_) +{ + nmos::details::omanip_gate gate{gate_, nmos::stash_category(impl::categories::node_implementation)}; + + try + { + node_implementation_init(model, gate); + node_implementation_run(model, gate); + } + catch(const node_implementation_init_exception&) + { + // node_implementation_init writes the log message + } + catch(const web::json::json_exception& e) + { + // most likely from incorrect value types in the command line settings + slog::log(gate, SLOG_FLF) << "JSON error: " << e.what(); + } + catch(const std::system_error& e) + { + slog::log(gate, SLOG_FLF) << "System error: " << e.what() << " [" << e.code() << "]"; + } + catch(const std::runtime_error& e) + { + slog::log(gate, SLOG_FLF) << "Implementation error: " << e.what(); + } + catch(const std::exception& e) + { + slog::log(gate, SLOG_FLF) << "Unexpected exception: " << e.what(); + } + catch(...) + { + slog::log(gate, SLOG_FLF) << "Unexpected unknown exception"; + } +} + +void node_implementation_init(nmos::node_model& model, slog::base_gate& gate) +{ + using web::json::value; + using web::json::value_from_elements; + using web::json::value_of; + + auto lock = model.write_lock(); // in order to update the resources + + const auto seed_id = nmos::experimental::fields::seed_id(model.settings); + const auto node_id = impl::make_id(seed_id, nmos::types::node); + const auto device_id = impl::make_id(seed_id, nmos::types::device); + const auto how_many = impl::fields::how_many(model.settings); + const auto sender_ports = impl::parse_ports(impl::fields::senders(model.settings)); + const auto rtp_sender_ports = + boost::copy_range>(sender_ports | boost::adaptors::filtered(impl::is_rtp_port)); + const auto ws_sender_ports = + boost::copy_range>(sender_ports | boost::adaptors::filtered(impl::is_ws_port)); + const auto receiver_ports = impl::parse_ports(impl::fields::receivers(model.settings)); + const auto rtp_receiver_ports = + boost::copy_range>(receiver_ports | boost::adaptors::filtered(impl::is_rtp_port)); + const auto ws_receiver_ports = + boost::copy_range>(receiver_ports | boost::adaptors::filtered(impl::is_ws_port)); + 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 interlace_mode = impl::get_interlace_mode(model.settings); + const auto colorspace = nmos::colorspace{impl::fields::colorspace(model.settings)}; + const auto transfer_characteristic = + nmos::transfer_characteristic{impl::fields::transfer_characteristic(model.settings)}; + const auto sampling = sdp::sampling{impl::fields::color_sampling(model.settings)}; + const auto bit_depth = impl::fields::component_depth(model.settings); + 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); + + // 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 + const auto profile = nmos::profiles::High444_12; + const auto level = nmos::get_video_jxsv_level(frame_rate, frame_width, frame_height); + const auto sublevel = nmos::sublevels::Sublev3bpp; + const auto max_bits_per_pixel = 4.0; // min coding efficiency + const auto bits_per_pixel = 2.0; + const auto transport_bit_rate_factor = 1.05; + + // any delay between updates to the model resources is unnecessary unless for debugging purposes + const unsigned int delay_millis{0}; + + // it is important that the model be locked before inserting, updating or deleting a resource + // and that the the node behaviour thread be notified after doing so + const auto insert_resource_after = [&model, &lock](unsigned int milliseconds, nmos::resources& resources, + nmos::resource&& resource, slog::base_gate& gate) { + if(nmos::details::wait_for(model.shutdown_condition, lock, std::chrono::milliseconds(milliseconds), + [&] { return model.shutdown; })) + return false; + + const std::pair id_type{resource.id, resource.type}; + const bool success = insert_resource(resources, std::move(resource)).second; + + if(success) + slog::log(gate, SLOG_FLF) << "Updated model with " << id_type; + else + slog::log(gate, SLOG_FLF) << "Model update error: " << id_type; + + slog::log(gate, SLOG_FLF) + << "Notifying node behaviour thread"; // and anyone else who cares... + model.notify(); + + return success; + }; + + const auto resolve_auto = make_node_implementation_auto_resolver(model.settings); + const auto set_transportfile = make_node_implementation_transportfile_setter(model.node_resources, model.settings); + + const auto clocks = web::json::value_of({nmos::make_internal_clock(nmos::clock_names::clk0)}); + // filter network interfaces to those that correspond to the specified host_addresses + const auto host_interfaces = nmos::get_host_interfaces(model.settings); + const auto interfaces = nmos::experimental::node_interfaces(host_interfaces); + + // example node + { + auto node = nmos::make_node(node_id, clocks, nmos::make_node_interfaces(interfaces), model.settings); + node.data[nmos::fields::tags] = impl::fields::node_tags(model.settings); + if(!insert_resource_after(delay_millis, model.node_resources, std::move(node), gate)) + throw node_implementation_init_exception(); + } + +#ifdef HAVE_LLDP + // LLDP manager for advertising server identity, capabilities, and discovering neighbours on a local area network + slog::log(gate, SLOG_FLF) << "Attempting to configure LLDP"; + auto lldp_manager = nmos::experimental::make_lldp_manager(model, interfaces, true, gate); + // hm, open may potentially throw? + lldp::lldp_manager_guard lldp_manager_guard(lldp_manager); +#endif + + // prepare interface bindings for all senders and receivers + const auto& host_address = nmos::fields::host_address(model.settings); + // the interface corresponding to the host address is used for the example node's WebSocket senders and receivers + const auto host_interface_ = impl::find_interface(host_interfaces, host_address); + if(host_interfaces.end() == host_interface_) + { + slog::log(gate, SLOG_FLF) << "No network interface corresponding to host_address?"; + throw node_implementation_init_exception(); + } + const auto& host_interface = *host_interface_; + // hmm, should probably add a custom setting to control the primary and secondary interfaces for the example node's + // RTP senders and receivers rather than just picking the one(s) corresponding to the first and last of the + // specified host addresses + const auto& primary_address = model.settings.has_field(nmos::fields::host_addresses) + ? web::json::front(nmos::fields::host_addresses(model.settings)).as_string() + : host_address; + const auto& secondary_address = model.settings.has_field(nmos::fields::host_addresses) + ? web::json::back(nmos::fields::host_addresses(model.settings)).as_string() + : host_address; + const auto primary_interface_ = impl::find_interface(host_interfaces, primary_address); + const auto secondary_interface_ = impl::find_interface(host_interfaces, secondary_address); + if(host_interfaces.end() == primary_interface_ || host_interfaces.end() == secondary_interface_) + { + slog::log(gate, SLOG_FLF) + << "No network interface corresponding to one of the host_addresses?"; + throw node_implementation_init_exception(); + } + const auto& primary_interface = *primary_interface_; + const auto& secondary_interface = *secondary_interface_; + const auto interface_names = smpte2022_7 + ? std::vector{primary_interface.name, secondary_interface.name} + : std::vector{primary_interface.name}; + + // example device + { + auto sender_ids = impl::make_ids(seed_id, nmos::types::sender, rtp_sender_ports, how_many); + if(0 <= nmos::fields::events_port(model.settings)) + boost::range::push_back(sender_ids, + impl::make_ids(seed_id, nmos::types::sender, ws_sender_ports, how_many)); + auto receiver_ids = impl::make_ids(seed_id, nmos::types::receiver, receiver_ports, how_many); + auto device = nmos::make_device(device_id, node_id, sender_ids, receiver_ids, model.settings); + device.data[nmos::fields::tags] = impl::fields::device_tags(model.settings); + if(!insert_resource_after(delay_millis, model.node_resources, std::move(device), gate)) + throw node_implementation_init_exception(); + } + + // example sources, flows and senders + for(int index = 0; index < how_many; ++index) + { + for(const auto& port : rtp_sender_ports) + { + const auto source_id = impl::make_id(seed_id, nmos::types::source, port, index); + const auto flow_id = impl::make_id(seed_id, nmos::types::flow, port, index); + const auto sender_id = impl::make_id(seed_id, nmos::types::sender, port, index); + + nmos::resource source; + if(impl::ports::video == port) + { + source = + nmos::make_video_source(source_id, device_id, nmos::clock_names::clk0, frame_rate, model.settings); + } + else if(impl::ports::audio == port) + { + const auto channels = boost::copy_range>( + boost::irange(0, channel_count) | boost::adaptors::transformed([&](const int& index) { + return impl::channels_repeat[index % (int)impl::channels_repeat.size()]; + })); + + source = nmos::make_audio_source(source_id, device_id, nmos::clock_names::clk0, frame_rate, channels, + model.settings); + } + else if(impl::ports::data == port) + { + source = + nmos::make_data_source(source_id, device_id, nmos::clock_names::clk0, frame_rate, model.settings); + } + else if(impl::ports::mux == port) + { + source = + nmos::make_mux_source(source_id, device_id, nmos::clock_names::clk0, frame_rate, model.settings); + } + impl::insert_parents(source, seed_id, port, index); + impl::set_label_description(source, port, index); + + nmos::resource flow; + if(impl::ports::video == port) + { + if(nmos::media_types::video_raw == video_type) + { + flow = nmos::make_raw_video_flow(flow_id, source_id, device_id, frame_rate, frame_width, + frame_height, interlace_mode, colorspace, transfer_characteristic, + sampling, bit_depth, model.settings); + } + else if(nmos::media_types::video_jxsv == video_type) + { + flow = + nmos::make_video_jxsv_flow(flow_id, source_id, device_id, frame_rate, frame_width, frame_height, + interlace_mode, colorspace, transfer_characteristic, sampling, + bit_depth, profile, level, sublevel, bits_per_pixel, model.settings); + } + else + { + flow = nmos::make_coded_video_flow( + flow_id, source_id, device_id, frame_rate, frame_width, frame_height, interlace_mode, + colorspace, transfer_characteristic, sampling, bit_depth, video_type, model.settings); + } + } + else if(impl::ports::audio == port) + { + flow = nmos::make_raw_audio_flow(flow_id, source_id, device_id, 48000, 24, model.settings); + // add optional grain_rate + flow.data[nmos::fields::grain_rate] = nmos::make_rational(frame_rate); + } + else if(impl::ports::data == port) + { + nmos::did_sdid timecode{0x60, 0x60}; + flow = nmos::make_sdianc_data_flow(flow_id, source_id, device_id, {timecode}, model.settings); + // add optional grain_rate + flow.data[nmos::fields::grain_rate] = nmos::make_rational(frame_rate); + } + else if(impl::ports::mux == port) + { + flow = nmos::make_mux_flow(flow_id, source_id, device_id, model.settings); + // add optional grain_rate + flow.data[nmos::fields::grain_rate] = nmos::make_rational(frame_rate); + } + impl::insert_parents(flow, seed_id, port, index); + impl::set_label_description(flow, port, index); + + // set_transportfile needs to find the matching source and flow for the sender, so insert these first + if(!insert_resource_after(delay_millis, model.node_resources, std::move(source), gate)) + throw node_implementation_init_exception(); + if(!insert_resource_after(delay_millis, model.node_resources, std::move(flow), gate)) + throw node_implementation_init_exception(); + + const auto manifest_href = nmos::experimental::make_manifest_api_manifest(sender_id, model.settings); + auto sender = nmos::make_sender(sender_id, flow_id, nmos::transports::rtp, device_id, + manifest_href.to_string(), interface_names, model.settings); + // hm, could add nmos::make_video_jxsv_sender to encapsulate this? + if(impl::ports::video == port && nmos::media_types::video_jxsv == video_type) + { + // additional attributes required by BCP-006-01 + // see https://specs.amwa.tv/bcp-006-01/branches/v1.0-dev/docs/NMOS_With_JPEG_XS.html#senders + const auto format_bit_rate = + nmos::get_video_jxsv_bit_rate(frame_rate, frame_width, frame_height, bits_per_pixel); + // round to nearest Megabit/second per examples in VSF TR-08:2022 + const auto transport_bit_rate = + uint64_t(transport_bit_rate_factor * format_bit_rate / 1e3 + 0.5) * 1000; + sender.data[nmos::fields::bit_rate] = value(transport_bit_rate); + sender.data[nmos::fields::st2110_21_sender_type] = value(nmos::st2110_21_sender_types::type_N.name); + } + impl::set_label_description(sender, port, index); + impl::insert_group_hint(sender, port, index); + + auto connection_sender = nmos::make_connection_rtp_sender(sender_id, smpte2022_7); + // add some example constraints; these should be completed fully! + connection_sender.data[nmos::fields::endpoint_constraints][0][nmos::fields::source_ip] = + value_of({{nmos::fields::constraint_enum, value_from_elements(primary_interface.addresses)}}); + if(smpte2022_7) + connection_sender.data[nmos::fields::endpoint_constraints][1][nmos::fields::source_ip] = + value_of({{nmos::fields::constraint_enum, value_from_elements(secondary_interface.addresses)}}); + + if(impl::fields::activate_senders(model.settings)) + { + // initialize this sender with a scheduled activation, e.g. to enable the IS-05-01 test suite to run + // immediately + auto& staged = connection_sender.data[nmos::fields::endpoint_staged]; + staged[nmos::fields::master_enable] = value::boolean(true); + staged[nmos::fields::activation] = + value_of({{nmos::fields::mode, nmos::activation_modes::activate_scheduled_relative.name}, + {nmos::fields::requested_time, U("0:0")}, + {nmos::fields::activation_time, nmos::make_version()}}); + } + + if(!insert_resource_after(delay_millis, model.node_resources, std::move(sender), gate)) + throw node_implementation_init_exception(); + if(!insert_resource_after(delay_millis, model.connection_resources, std::move(connection_sender), gate)) + throw node_implementation_init_exception(); + } + } + + // example receivers + for(int index = 0; index < how_many; ++index) + { + for(const auto& port : rtp_receiver_ports) + { + const auto receiver_id = impl::make_id(seed_id, nmos::types::receiver, port, index); + + nmos::resource receiver; + if(impl::ports::video == port) + { + receiver = nmos::make_receiver(receiver_id, device_id, nmos::transports::rtp, interface_names, + nmos::formats::video, {video_type}, model.settings); + // add an example constraint set; these should be completed fully! + if(nmos::media_types::video_raw == video_type) + { + const auto interlace_modes = + nmos::interlace_modes::progressive != interlace_mode + ? std::vector{nmos::interlace_modes::interlaced_bff.name, + nmos::interlace_modes::interlaced_tff.name, + nmos::interlace_modes::interlaced_psf.name} + : std::vector{nmos::interlace_modes::progressive.name}; + receiver.data[nmos::fields::caps][nmos::fields::constraint_sets] = value_of({value_of( + {{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::interlace_mode, nmos::make_caps_string_constraint(interlace_modes)}, + {nmos::caps::format::color_sampling, nmos::make_caps_string_constraint({sampling.name})}})}); + } + else if(nmos::media_types::video_jxsv == video_type) + { + // some of the parameter constraints recommended by BCP-006-01 + // see https://specs.amwa.tv/bcp-006-01/branches/v1.0-dev/docs/NMOS_With_JPEG_XS.html#receivers + const auto max_format_bit_rate = + nmos::get_video_jxsv_bit_rate(frame_rate, frame_width, frame_height, max_bits_per_pixel); + // round to nearest Megabit/second per examples in VSF TR-08:2022 + const auto max_transport_bit_rate = + uint64_t(transport_bit_rate_factor * max_format_bit_rate / 1e3 + 0.5) * 1000; + + receiver.data[nmos::fields::caps][nmos::fields::constraint_sets] = value_of({value_of( + {{nmos::caps::format::profile, nmos::make_caps_string_constraint({profile.name})}, + {nmos::caps::format::level, nmos::make_caps_string_constraint({level.name})}, + {nmos::caps::format::sublevel, + nmos::make_caps_string_constraint( + {nmos::sublevels::Sublev3bpp.name, nmos::sublevels::Sublev4bpp.name})}, + {nmos::caps::format::bit_rate, + nmos::make_caps_integer_constraint({}, nmos::no_minimum(), + (int64_t)max_format_bit_rate)}, + {nmos::caps::transport::bit_rate, + nmos::make_caps_integer_constraint({}, nmos::no_minimum(), + (int64_t)max_transport_bit_rate)}, + {nmos::caps::transport::packet_transmission_mode, + nmos::make_caps_string_constraint({nmos::packet_transmission_modes::codestream.name})}})}); + } + receiver.data[nmos::fields::version] = receiver.data[nmos::fields::caps][nmos::fields::version] = + value(nmos::make_version()); + } + else if(impl::ports::audio == port) + { + receiver = nmos::make_audio_receiver(receiver_id, device_id, nmos::transports::rtp, interface_names, 24, + model.settings); + // add some example constraint sets; these should be completed fully! + receiver.data[nmos::fields::caps][nmos::fields::constraint_sets] = value_of( + {value_of( + {{nmos::caps::format::channel_count, nmos::make_caps_integer_constraint({}, 1, channel_count)}, + {nmos::caps::format::sample_rate, nmos::make_caps_rational_constraint({{48000, 1}})}, + {nmos::caps::format::sample_depth, nmos::make_caps_integer_constraint({16, 24})}, + {nmos::caps::transport::packet_time, nmos::make_caps_number_constraint({0.125})}}), + value_of({{nmos::caps::meta::preference, -1}, + {nmos::caps::format::channel_count, + nmos::make_caps_integer_constraint({}, 1, (std::min)(8, channel_count))}, + {nmos::caps::format::sample_rate, nmos::make_caps_rational_constraint({{48000, 1}})}, + {nmos::caps::format::sample_depth, nmos::make_caps_integer_constraint({16, 24})}, + {nmos::caps::transport::packet_time, nmos::make_caps_number_constraint({1})}})}); + receiver.data[nmos::fields::version] = receiver.data[nmos::fields::caps][nmos::fields::version] = + value(nmos::make_version()); + } + else if(impl::ports::data == port) + { + receiver = nmos::make_sdianc_data_receiver(receiver_id, device_id, nmos::transports::rtp, + interface_names, model.settings); + // add an example constraint set; these should be completed fully! + receiver.data[nmos::fields::caps][nmos::fields::constraint_sets] = value_of( + {value_of({{nmos::caps::format::grain_rate, nmos::make_caps_rational_constraint({frame_rate})}})}); + receiver.data[nmos::fields::version] = receiver.data[nmos::fields::caps][nmos::fields::version] = + value(nmos::make_version()); + } + else if(impl::ports::mux == port) + { + receiver = nmos::make_mux_receiver(receiver_id, device_id, nmos::transports::rtp, interface_names, + model.settings); + // add an example constraint set; these should be completed fully! + receiver.data[nmos::fields::caps][nmos::fields::constraint_sets] = value_of( + {value_of({{nmos::caps::format::grain_rate, nmos::make_caps_rational_constraint({frame_rate})}})}); + receiver.data[nmos::fields::version] = receiver.data[nmos::fields::caps][nmos::fields::version] = + value(nmos::make_version()); + } + impl::set_label_description(receiver, port, index); + impl::insert_group_hint(receiver, port, index); + + auto connection_receiver = nmos::make_connection_rtp_receiver(receiver_id, smpte2022_7); + // add some example constraints; these should be completed fully! + connection_receiver.data[nmos::fields::endpoint_constraints][0][nmos::fields::interface_ip] = + value_of({{nmos::fields::constraint_enum, value_from_elements(primary_interface.addresses)}}); + if(smpte2022_7) + connection_receiver.data[nmos::fields::endpoint_constraints][1][nmos::fields::interface_ip] = + value_of({{nmos::fields::constraint_enum, value_from_elements(secondary_interface.addresses)}}); + + resolve_auto(receiver, connection_receiver, + connection_receiver.data[nmos::fields::endpoint_active][nmos::fields::transport_params]); + + if(!insert_resource_after(delay_millis, model.node_resources, std::move(receiver), gate)) + throw node_implementation_init_exception(); + if(!insert_resource_after(delay_millis, model.connection_resources, std::move(connection_receiver), gate)) + throw node_implementation_init_exception(); + } + } + + // example event sources, flows and senders + for(int index = 0; 0 <= nmos::fields::events_port(model.settings) && index < how_many; ++index) + { + for(const auto& port : ws_sender_ports) + { + const auto source_id = impl::make_id(seed_id, nmos::types::source, port, index); + const auto flow_id = impl::make_id(seed_id, nmos::types::flow, port, index); + const auto sender_id = impl::make_id(seed_id, nmos::types::sender, port, index); + + nmos::event_type event_type; + web::json::value events_type; + web::json::value events_state; + if(impl::ports::temperature == port) + { + event_type = impl::temperature_Celsius; + + // see https://specs.amwa.tv/is-07/releases/v1.0.1/docs/3.0._Event_types.html#231-measurements + // and + // https://specs.amwa.tv/is-07/releases/v1.0.1/examples/eventsapi-type-number-measurement-get-200.html + // and + // https://specs.amwa.tv/is-07/releases/v1.0.1/examples/eventsapi-state-number-measurement-get-200.html + events_type = nmos::make_events_number_type({-200, 10}, {1000, 10}, {1, 10}, U("C")); + events_state = nmos::make_events_number_state({source_id, flow_id}, {201, 10}, event_type); + } + else if(impl::ports::burn == port) + { + event_type = nmos::event_types::boolean; + + // see https://specs.amwa.tv/is-07/releases/v1.0.1/docs/3.0._Event_types.html#21-boolean + events_type = nmos::make_events_boolean_type(); + events_state = nmos::make_events_boolean_state({source_id, flow_id}, false); + } + else if(impl::ports::nonsense == port) + { + event_type = nmos::event_types::string; + + // see https://specs.amwa.tv/is-07/releases/v1.0.1/docs/3.0._Event_types.html#22-string + // and of course, https://en.wikipedia.org/wiki/Metasyntactic_variable + events_type = nmos::make_events_string_type(0, 0, U("^foo|bar|baz|qu+x$")); + events_state = nmos::make_events_string_state({source_id, flow_id}, U("foo")); + } + else if(impl::ports::catcall == port) + { + event_type = impl::catcall; + + // see https://specs.amwa.tv/is-07/releases/v1.0.1/docs/3.0._Event_types.html#3-enum + events_type = nmos::make_events_number_enum_type({{1, {U("meow"), U("chatty")}}, + {2, {U("purr"), U("happy")}}, + {4, {U("hiss"), U("afraid")}}, + {8, {U("yowl"), U("sonorous")}}}); + events_state = nmos::make_events_number_state({source_id, flow_id}, 1, event_type); + } + + // grain_rate is not set because these events are aperiodic + auto source = nmos::make_data_source(source_id, device_id, {}, event_type, model.settings); + impl::set_label_description(source, port, index); + + auto events_source = nmos::make_events_source(source_id, events_state, events_type); + + auto flow = nmos::make_json_data_flow(flow_id, source_id, device_id, event_type, model.settings); + impl::set_label_description(flow, port, index); + + auto sender = nmos::make_sender(sender_id, flow_id, nmos::transports::websocket, device_id, {}, + {host_interface.name}, model.settings); + impl::set_label_description(sender, port, index); + impl::insert_group_hint(sender, port, index); + + // initialize this sender enabled, just to enable the IS-07-02 test suite to run immediately + auto connection_sender = + nmos::make_connection_events_websocket_sender(sender_id, device_id, source_id, model.settings); + connection_sender.data[nmos::fields::endpoint_active][nmos::fields::master_enable] = + connection_sender.data[nmos::fields::endpoint_staged][nmos::fields::master_enable] = + value::boolean(true); + resolve_auto(sender, connection_sender, + connection_sender.data[nmos::fields::endpoint_active][nmos::fields::transport_params]); + nmos::set_resource_subscription( + sender, nmos::fields::master_enable(connection_sender.data[nmos::fields::endpoint_active]), {}, + nmos::tai_now()); + + if(!insert_resource_after(delay_millis, model.node_resources, std::move(source), gate)) + throw node_implementation_init_exception(); + if(!insert_resource_after(delay_millis, model.node_resources, std::move(flow), gate)) + throw node_implementation_init_exception(); + if(!insert_resource_after(delay_millis, model.node_resources, std::move(sender), gate)) + throw node_implementation_init_exception(); + if(!insert_resource_after(delay_millis, model.connection_resources, std::move(connection_sender), gate)) + throw node_implementation_init_exception(); + if(!insert_resource_after(delay_millis, model.events_resources, std::move(events_source), gate)) + throw node_implementation_init_exception(); + } + } + + // example event receivers + for(int index = 0; index < how_many; ++index) + { + for(const auto& port : ws_receiver_ports) + { + const auto receiver_id = impl::make_id(seed_id, nmos::types::receiver, port, index); + + nmos::event_type event_type; + if(impl::ports::temperature == port) + { + // accept e.g. "number/temperature/F" or "number/temperature/K" as well as "number/temperature/C" + event_type = impl::temperature_wildcard; + } + else if(impl::ports::burn == port) + { + // accept any boolean + event_type = nmos::event_types::wildcard(nmos::event_types::boolean); + } + else if(impl::ports::nonsense == port) + { + // accept any string + event_type = nmos::event_types::wildcard(nmos::event_types::string); + } + else if(impl::ports::catcall == port) + { + // accept only a catcall + event_type = impl::catcall; + } + + auto receiver = + nmos::make_data_receiver(receiver_id, device_id, nmos::transports::websocket, {host_interface.name}, + nmos::media_types::application_json, {event_type}, model.settings); + impl::set_label_description(receiver, port, index); + impl::insert_group_hint(receiver, port, index); + + auto connection_receiver = nmos::make_connection_events_websocket_receiver(receiver_id, model.settings); + resolve_auto(receiver, connection_receiver, + connection_receiver.data[nmos::fields::endpoint_active][nmos::fields::transport_params]); + + if(!insert_resource_after(delay_millis, model.node_resources, std::move(receiver), gate)) + throw node_implementation_init_exception(); + if(!insert_resource_after(delay_millis, model.connection_resources, std::move(connection_receiver), gate)) + throw node_implementation_init_exception(); + } + } + + // example channelmapping resources demonstrating a range of input/output capabilities + // see https://github.com/sony/nmos-cpp/issues/111#issuecomment-740613137 + + // example audio inputs + const bool channelmapping_receivers = + 0 <= nmos::fields::channelmapping_port(model.settings) && + rtp_receiver_ports.end() != boost::range::find(rtp_receiver_ports, impl::ports::audio); + for(int index = 0; channelmapping_receivers && index < how_many; ++index) + { + const auto stri = utility::conversions::details::to_string_t(index); + + const auto id = U("input") + stri; + + const auto name = U("IP Input ") + stri; + const auto description = U("SMPTE 2110-30 IP Input ") + stri; + + const auto receiver_id = impl::make_id(seed_id, nmos::types::receiver, impl::ports::audio, index); + const auto parent = std::pair(receiver_id, nmos::types::receiver); + + const auto channel_labels = boost::copy_range>( + boost::irange(0, channel_count) | boost::adaptors::transformed([&](const int& index) { + return impl::channels_repeat[index % (int)impl::channels_repeat.size()].label; + })); + + // use default input capabilities to indicate no constraints + auto channelmapping_input = nmos::make_channelmapping_input(id, name, description, parent, channel_labels); + if(!insert_resource_after(delay_millis, model.channelmapping_resources, std::move(channelmapping_input), gate)) + throw node_implementation_init_exception(); + } + + // example audio outputs + const bool channelmapping_senders = + 0 <= nmos::fields::channelmapping_port(model.settings) && + rtp_sender_ports.end() != boost::range::find(rtp_sender_ports, impl::ports::audio); + for(int index = 0; channelmapping_senders && index < how_many; ++index) + { + const auto stri = utility::conversions::details::to_string_t(index); + + const auto id = U("output") + stri; + + const auto name = U("IP Output ") + stri; + const auto description = U("SMPTE 2110-30 IP Output ") + stri; + + const auto source_id = impl::make_id(seed_id, nmos::types::source, impl::ports::audio, index); + + const auto channel_labels = boost::copy_range>( + boost::irange(0, channel_count) | boost::adaptors::transformed([&](const int& index) { + return impl::channels_repeat[index % (int)impl::channels_repeat.size()].label; + })); + + // omit routable inputs to indicate no restrictions + auto channelmapping_output = nmos::make_channelmapping_output(id, name, description, source_id, channel_labels); + if(!insert_resource_after(delay_millis, model.channelmapping_resources, std::move(channelmapping_output), gate)) + throw node_implementation_init_exception(); + } + + const int input_block_size = 8; + const int input_block_count = 8; + + // example non-IP audio input + if(0 <= nmos::fields::channelmapping_port(model.settings)) + { + const auto id = U("inputA"); + + const auto name = U("MADI Input A"); + const auto description = U("MADI Input A"); + + // non-IP audio inputs have no parent + const auto parent = std::pair(); + + const auto channel_labels = boost::copy_range>( + boost::irange(0, input_block_size * input_block_count) | boost::adaptors::transformed([](const int& index) { + return nmos::channel_symbols::Undefined(1 + index).name; + })); + + // some example constraints; this input's channels can only be used in blocks and the channels cannot be + // reordered within each block + const auto reordering = false; + const auto block_size = input_block_size; + + auto channelmapping_input = + nmos::make_channelmapping_input(id, name, description, parent, channel_labels, reordering, block_size); + if(!insert_resource_after(delay_millis, model.channelmapping_resources, std::move(channelmapping_input), gate)) + throw node_implementation_init_exception(); + } + + // example outputs to some audio gizmo + if(0 <= nmos::fields::channelmapping_port(model.settings)) + { + const auto id = U("outputX"); + + const auto name = U("Gizmo Output X"); + const auto description = U("Gizmo Output X"); + + const auto source_id = impl::make_id(seed_id, nmos::types::source, impl::ports::audio, how_many); + + const auto channel_labels = boost::copy_range>( + boost::irange(0, input_block_size) | boost::adaptors::transformed([](const int& index) { + return nmos::channel_symbols::Undefined(1 + index).name; + })); + + // some example constraints; only allow inputs from the example non-IP audio input + auto routable_inputs = std::vector{U("inputA")}; + // do not allow unrouted channels + + // start with a valid active map + auto active_map = boost::copy_range>>( + boost::irange(0, input_block_size) | boost::adaptors::transformed([](const int& index) { + return std::pair{U("inputA"), index}; + })); + + auto channelmapping_output = nmos::make_channelmapping_output(id, name, description, source_id, channel_labels, + routable_inputs, active_map); + if(!insert_resource_after(delay_millis, model.channelmapping_resources, std::move(channelmapping_output), gate)) + throw node_implementation_init_exception(); + } + + // example source for some audio gizmo + if(0 <= nmos::fields::channelmapping_port(model.settings)) + { + const auto source_id = impl::make_id(seed_id, nmos::types::source, impl::ports::audio, how_many); + + const auto channels = boost::copy_range>( + boost::irange(0, input_block_size) | boost::adaptors::transformed([](const int& index) { + return nmos::channel{{}, nmos::channel_symbols::Undefined(1 + index)}; + })); + + auto source = nmos::make_audio_source(source_id, device_id, nmos::clock_names::clk0, frame_rate, channels, + model.settings); + impl::set_label_description(source, impl::ports::audio, how_many); + + if(!insert_resource_after(delay_millis, model.node_resources, std::move(source), gate)) + throw node_implementation_init_exception(); + } + + // example inputs from some audio gizmo + if(0 <= nmos::fields::channelmapping_port(model.settings)) + { + const auto id = U("inputX"); + + const auto name = U("Gizmo Input X"); + const auto description = U("Gizmo Input X"); + + // the audio gizmo is re-entrant + const auto source_id = impl::make_id(seed_id, nmos::types::source, impl::ports::audio, how_many); + const auto parent = std::pair(source_id, nmos::types::source); + + const auto channel_labels = boost::copy_range>( + boost::irange(0, input_block_size) | boost::adaptors::transformed([](const int& index) { + return nmos::channel_symbols::Undefined(1 + index).name; + })); + + // this input is weird, it is block-based but allows reordering of channels within a block + const auto reordering = true; + const auto block_size = 2; + + auto channelmapping_input = + nmos::make_channelmapping_input(id, name, description, parent, channel_labels, reordering, block_size); + if(!insert_resource_after(delay_millis, model.channelmapping_resources, std::move(channelmapping_input), gate)) + throw node_implementation_init_exception(); + } + + // example non-ST 2110-30 audio output + if(0 <= nmos::fields::channelmapping_port(model.settings)) + { + const auto id = U("outputB"); + + const auto name = U("AES Output B"); + const auto description = U("AES Output B"); + + // non-IP audio outputs have no sourceid + const auto source_id = nmos::id(); + + const auto channel_labels = boost::copy_range>( + nmos::channel_symbols::ST | + boost::adaptors::transformed([](const nmos::channel_symbol& symbol) { return symbol.name; })); + + // allow inputs from the audio gizmo + auto routable_inputs = std::vector{U("inputX")}; + // allow unrouted channels + routable_inputs.push_back({}); + + auto channelmapping_output = + nmos::make_channelmapping_output(id, name, description, source_id, channel_labels, routable_inputs); + if(!insert_resource_after(delay_millis, model.channelmapping_resources, std::move(channelmapping_output), gate)) + throw node_implementation_init_exception(); + } +} + +void node_implementation_run(nmos::node_model& model, slog::base_gate& gate) +{ + auto lock = model.read_lock(); + + const auto seed_id = nmos::experimental::fields::seed_id(model.settings); + const auto how_many = impl::fields::how_many(model.settings); + 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)); + + // 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; + std::shared_ptr events_engine(new std::default_random_engine(events_seeder)); + + 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] { + 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] { + auto lock = model.write_lock(); + + // make example temperature data ... \/\/\/\/ ... around 200 + const nmos::events_number temp(175.0 + std::abs(nmos::tai_now().seconds % 100 - 50), 10); + // i.e. 17.5-22.5 C + + for(int index = 0; 0 <= nmos::fields::events_port(model.settings) && index < how_many; ++index) + { + for(const auto& port : ws_sender_ports) + { + const auto source_id = impl::make_id(seed_id, nmos::types::source, port, index); + const auto flow_id = impl::make_id(seed_id, nmos::types::flow, port, index); + + modify_resource(model.events_resources, source_id, [&](nmos::resource& resource) { + if(impl::ports::temperature == port) + { + nmos::fields::endpoint_state(resource.data) = nmos::make_events_number_state( + {source_id, flow_id}, temp, impl::temperature_Celsius); + } + else if(impl::ports::burn == port) + { + nmos::fields::endpoint_state(resource.data) = nmos::make_events_boolean_state( + {source_id, flow_id}, temp.scaled_value() > 20.0); + } + else if(impl::ports::nonsense == port) + { + const auto nonsenses = {U("foo"), U("bar"), U("baz"), + U("qux"), U("quux"), U("quuux")}; + const auto& nonsense = + *(nonsenses.begin() + + (std::min)(std::geometric_distribution()(*events_engine), + nonsenses.size() - 1)); + nmos::fields::endpoint_state(resource.data) = + nmos::make_events_string_state({source_id, flow_id}, nonsense); + } + else if(impl::ports::catcall == port) + { + const auto catcalls = {1, 2, 4, 8}; + const auto& catcall = + *(catcalls.begin() + + (std::min)(std::geometric_distribution()(*events_engine), + catcalls.size() - 1)); + nmos::fields::endpoint_state(resource.data) = + nmos::make_events_number_state({source_id, flow_id}, catcall, impl::catcall); + } + }); + } + } + + slog::log(gate, SLOG_FLF) + << "Temperature updated: " << temp.scaled_value() << " (" << impl::temperature_Celsius.name + << ")"; + + model.notify(); + + return true; + }); + }, + token); + + // wait for the thread to be interrupted because the server is being shut down + model.shutdown_condition.wait(lock, [&] { return model.shutdown; }); + + cancellation_source.cancel(); + // wait without the lock since it is also used by the background tasks + nmos::details::reverse_lock_guard unlock{lock}; + events.wait(); +} + +// Example System API node behaviour callback to perform application-specific operations when the global configuration +// resource changes +nmos::system_global_handler make_node_implementation_system_global_handler(nmos::node_model& model, + slog::base_gate& gate) +{ + // this example uses the callback to update the settings + // (an 'empty' std::function disables System API node behaviour) + return [&](const web::uri& system_uri, const web::json::value& system_global) { + if(!system_uri.is_empty()) + { + slog::log(gate, SLOG_FLF) + << nmos::stash_category(impl::categories::node_implementation) + << "New system global configuration discovered from the System API at: " << system_uri.to_string(); + + // although this example immediately updates the settings, the effect is not propagated + // in either Registration API behaviour or the senders' /transportfile endpoints until + // an update to these is forced by other circumstances + + auto system_global_settings = nmos::parse_system_global_data(system_global).second; + web::json::merge_patch(model.settings, system_global_settings, true); + } + else + { + slog::log(gate, SLOG_FLF) + << nmos::stash_category(impl::categories::node_implementation) + << "System global configuration is not discoverable"; + } + }; +} + +// Example Registration API node behaviour callback to perform application-specific operations when the current +// Registration API changes +nmos::registration_handler make_node_implementation_registration_handler(slog::base_gate& gate) +{ + return [&](const web::uri& registration_uri) { + if(!registration_uri.is_empty()) + { + slog::log(gate, SLOG_FLF) + << nmos::stash_category(impl::categories::node_implementation) + << "Started registered operation with Registration API at: " << registration_uri.to_string(); + } + else + { + slog::log(gate, SLOG_FLF) + << nmos::stash_category(impl::categories::node_implementation) << "Stopped registered operation"; + } + }; +} + +// Example Connection API callback to parse "transport_file" during a PATCH /staged request +nmos::transport_file_parser make_node_implementation_transport_file_parser() +{ + // this example uses a custom transport file parser to handle video/jxsv in addition to the core media types + // otherwise, it could simply return &nmos::parse_rtp_transport_file + // (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); + }; +} + +// 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() +{ + // 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 {}; +} + +// Example Connection API activation callback to resolve "auto" values when /staged is transitioned to /active +nmos::connection_resource_auto_resolver make_node_implementation_auto_resolver(const nmos::settings& settings) +{ + using web::json::value; + + const auto seed_id = nmos::experimental::fields::seed_id(settings); + const auto device_id = impl::make_id(seed_id, nmos::types::device); + const auto how_many = impl::fields::how_many(settings); + const auto rtp_sender_ports = boost::copy_range>( + impl::parse_ports(impl::fields::senders(settings)) | boost::adaptors::filtered(impl::is_rtp_port)); + const auto rtp_sender_ids = impl::make_ids(seed_id, nmos::types::sender, rtp_sender_ports, how_many); + const auto ws_sender_ports = boost::copy_range>( + impl::parse_ports(impl::fields::senders(settings)) | boost::adaptors::filtered(impl::is_ws_port)); + const auto ws_sender_ids = impl::make_ids(seed_id, nmos::types::sender, ws_sender_ports, how_many); + const auto ws_sender_uri = nmos::make_events_ws_api_connection_uri(device_id, settings); + const auto rtp_receiver_ports = boost::copy_range>( + impl::parse_ports(impl::fields::receivers(settings)) | boost::adaptors::filtered(impl::is_rtp_port)); + const auto rtp_receiver_ids = impl::make_ids(seed_id, nmos::types::receiver, rtp_receiver_ports, how_many); + const auto ws_receiver_ports = boost::copy_range>( + impl::parse_ports(impl::fields::receivers(settings)) | boost::adaptors::filtered(impl::is_ws_port)); + const auto ws_receiver_ids = impl::make_ids(seed_id, nmos::types::receiver, ws_receiver_ports, how_many); + + // although which properties may need to be defaulted depends on the resource type, + // the default value will almost always be different for each resource + return [rtp_sender_ids, rtp_receiver_ids, ws_sender_ids, ws_sender_uri, ws_receiver_ids]( + const nmos::resource& resource, const nmos::resource& connection_resource, value& transport_params) { + const std::pair id_type{connection_resource.id, connection_resource.type}; + // this code relies on the specific constraints added by node_implementation_thread + const auto& constraints = nmos::fields::endpoint_constraints(connection_resource.data); + + // "In some cases the behaviour is more complex, and may be determined by the vendor." + // See https://specs.amwa.tv/is-05/releases/v1.0.0/docs/2.2._APIs_-_Server_Side_Implementation.html#use-of-auto + if(rtp_sender_ids.end() != boost::range::find(rtp_sender_ids, id_type.first)) + { + const bool smpte2022_7 = 1 < transport_params.size(); + nmos::details::resolve_auto(transport_params[0], nmos::fields::source_ip, [&] { + return web::json::front(nmos::fields::constraint_enum(constraints.at(0).at(nmos::fields::source_ip))); + }); + if(smpte2022_7) + nmos::details::resolve_auto(transport_params[1], nmos::fields::source_ip, [&] { + return web::json::back( + nmos::fields::constraint_enum(constraints.at(1).at(nmos::fields::source_ip))); + }); + nmos::details::resolve_auto(transport_params[0], nmos::fields::destination_ip, [&] { + return value::string(impl::make_source_specific_multicast_address_v4(id_type.first, 0)); + }); + if(smpte2022_7) + nmos::details::resolve_auto(transport_params[1], nmos::fields::destination_ip, [&] { + return value::string(impl::make_source_specific_multicast_address_v4(id_type.first, 1)); + }); + // lastly, apply the specification defaults for any properties not handled above + nmos::resolve_rtp_auto(id_type.second, transport_params); + } + else if(rtp_receiver_ids.end() != boost::range::find(rtp_receiver_ids, id_type.first)) + { + const bool smpte2022_7 = 1 < transport_params.size(); + nmos::details::resolve_auto(transport_params[0], nmos::fields::interface_ip, [&] { + return web::json::front( + nmos::fields::constraint_enum(constraints.at(0).at(nmos::fields::interface_ip))); + }); + if(smpte2022_7) + nmos::details::resolve_auto(transport_params[1], nmos::fields::interface_ip, [&] { + return web::json::back( + nmos::fields::constraint_enum(constraints.at(1).at(nmos::fields::interface_ip))); + }); + // lastly, apply the specification defaults for any properties not handled above + nmos::resolve_rtp_auto(id_type.second, transport_params); + } + else if(ws_sender_ids.end() != boost::range::find(ws_sender_ids, id_type.first)) + { + nmos::details::resolve_auto(transport_params[0], nmos::fields::connection_uri, + [&] { return value::string(ws_sender_uri.to_string()); }); + nmos::details::resolve_auto(transport_params[0], nmos::fields::connection_authorization, + [&] { return value::boolean(false); }); + } + else if(ws_receiver_ids.end() != boost::range::find(ws_receiver_ids, id_type.first)) + { + nmos::details::resolve_auto(transport_params[0], nmos::fields::connection_authorization, + [&] { return value::boolean(false); }); + } + }; +} + +// Example Connection API activation callback to update senders' /transportfile endpoint - captures node_resources by +// reference! +nmos::connection_sender_transportfile_setter +make_node_implementation_transportfile_setter(const nmos::resources& node_resources, const nmos::settings& settings) +{ + using web::json::value; + + const auto seed_id = nmos::experimental::fields::seed_id(settings); + const auto node_id = impl::make_id(seed_id, nmos::types::node); + const auto how_many = impl::fields::how_many(settings); + const auto sender_ports = impl::parse_ports(impl::fields::senders(settings)); + const auto rtp_sender_ports = + boost::copy_range>(sender_ports | boost::adaptors::filtered(impl::is_rtp_port)); + const auto rtp_source_ids = impl::make_ids(seed_id, nmos::types::source, rtp_sender_ports, how_many); + const auto rtp_flow_ids = impl::make_ids(seed_id, nmos::types::flow, rtp_sender_ports, how_many); + const auto rtp_sender_ids = impl::make_ids(seed_id, nmos::types::sender, rtp_sender_ports, how_many); + + // as part of activation, the example sender /transportfile should be updated based on the active transport + // parameters + return [&node_resources, node_id, rtp_source_ids, rtp_flow_ids, rtp_sender_ids]( + const nmos::resource& sender, const nmos::resource& connection_sender, value& endpoint_transportfile) { + const auto found = boost::range::find(rtp_sender_ids, connection_sender.id); + if(rtp_sender_ids.end() != found) + { + const auto index = int(found - rtp_sender_ids.begin()); + const auto source_id = rtp_source_ids.at(index); + const auto flow_id = rtp_flow_ids.at(index); + + // note, model mutex is already locked by the calling thread, so access to node_resources is OK... + auto node = nmos::find_resource(node_resources, {node_id, nmos::types::node}); + auto source = nmos::find_resource(node_resources, {source_id, nmos::types::source}); + auto flow = nmos::find_resource(node_resources, {flow_id, nmos::types::flow}); + if(node_resources.end() == node || node_resources.end() == source || node_resources.end() == flow) + { + throw std::logic_error("matching IS-04 node, source or flow not found"); + } + + // the nmos::make_sdp_parameters overload from the IS-04 resources provides a high-level interface + // for common "video/raw", "audio/L", "video/smpte291" and "video/SMPTE2022-6" use cases + // auto sdp_params = nmos::make_sdp_parameters(node->data, source->data, flow->data, sender.data, { + // U("PRIMARY"), U("SECONDARY") }); + + // nmos::make_{video,audio,data,mux}_sdp_parameters provide a little more flexibility for those four media + // types and the combination of nmos::make_{video_raw,audio_L,video_smpte291,video_SMPTE2022_6}_parameters + // with the related make_sdp_parameters overloads provides the most flexible and extensible approach + auto sdp_params = [&] { + const std::vector mids{U("PRIMARY"), U("SECONDARY")}; + const nmos::format format{nmos::fields::format(flow->data)}; + if(nmos::formats::video == format) + { + const nmos::media_type video_type{nmos::fields::media_type(flow->data)}; + if(nmos::media_types::video_raw == video_type) + { + return nmos::make_video_sdp_parameters(node->data, source->data, flow->data, sender.data, + nmos::details::payload_type_video_default, mids, {}, + sdp::type_parameters::type_N); + } + else if(nmos::media_types::video_jxsv == video_type) + { + const auto params = + nmos::make_video_jxsv_parameters(node->data, source->data, flow->data, sender.data); + const auto ts_refclk = nmos::details::make_ts_refclk(node->data, source->data, sender.data, {}); + return nmos::make_sdp_parameters(nmos::fields::label(sender.data), params, + nmos::details::payload_type_video_default, mids, ts_refclk); + } + else + { + throw std::logic_error("unexpected flow media_type"); + } + } + else if(nmos::formats::audio == format) + { + // this example application doesn't actually stream, so just indicate a sensible value for packet + // time + const double packet_time = nmos::fields::channels(source->data).size() > 8 ? 0.125 : 1; + return nmos::make_audio_sdp_parameters(node->data, source->data, flow->data, sender.data, + nmos::details::payload_type_audio_default, mids, {}, + packet_time); + } + else if(nmos::formats::data == format) + { + return nmos::make_data_sdp_parameters(node->data, source->data, flow->data, sender.data, + nmos::details::payload_type_data_default, mids, {}, {}); + } + else if(nmos::formats::mux == format) + { + return nmos::make_mux_sdp_parameters(node->data, source->data, flow->data, sender.data, + nmos::details::payload_type_mux_default, mids, {}, + sdp::type_parameters::type_N); + } + else + { + throw std::logic_error("unexpected flow format"); + } + }(); + + auto& transport_params = + nmos::fields::transport_params(nmos::fields::endpoint_active(connection_sender.data)); + auto session_description = nmos::make_session_description(sdp_params, transport_params); + auto sdp = utility::s2us(sdp::make_session_description(session_description)); + endpoint_transportfile = nmos::make_connection_rtp_sender_transportfile(sdp); + } + }; +} + +// Example Events WebSocket API client message handler +nmos::events_ws_message_handler make_node_implementation_events_ws_message_handler(const nmos::node_model& model, + slog::base_gate& gate) +{ + const auto seed_id = nmos::experimental::fields::seed_id(model.settings); + const auto how_many = impl::fields::how_many(model.settings); + const auto receiver_ports = impl::parse_ports(impl::fields::receivers(model.settings)); + const auto ws_receiver_ports = + boost::copy_range>(receiver_ports | boost::adaptors::filtered(impl::is_ws_port)); + const auto ws_receiver_ids = impl::make_ids(seed_id, nmos::types::receiver, ws_receiver_ports, how_many); + + // the message handler will be used for all Events WebSocket connections, and each connection may potentially + // have subscriptions to a number of sources, for multiple receivers, so this example uses a handler adaptor + // that enables simple processing of "state" messages (events) per receiver + return nmos::experimental::make_events_ws_message_handler( + model, + [ws_receiver_ids, &gate](const nmos::resource& receiver, const nmos::resource& connection_receiver, + const web::json::value& message) { + const auto found = boost::range::find(ws_receiver_ids, connection_receiver.id); + if(ws_receiver_ids.end() != found) + { + const auto event_type = nmos::event_type(nmos::fields::state_event_type(message)); + const auto& payload = nmos::fields::state_payload(message); + + if(nmos::is_matching_event_type(nmos::event_types::wildcard(nmos::event_types::number), event_type)) + { + const nmos::events_number value(nmos::fields::payload_number_value(payload).to_double(), + nmos::fields::payload_number_scale(payload)); + slog::log(gate, SLOG_FLF) + << nmos::stash_category(impl::categories::node_implementation) + << "Event received: " << value.scaled_value() << " (" << event_type.name << ")"; + } + else if(nmos::is_matching_event_type(nmos::event_types::wildcard(nmos::event_types::string), + event_type)) + { + slog::log(gate, SLOG_FLF) + << nmos::stash_category(impl::categories::node_implementation) + << "Event received: " << nmos::fields::payload_string_value(payload) << " (" << event_type.name + << ")"; + } + else if(nmos::is_matching_event_type(nmos::event_types::wildcard(nmos::event_types::boolean), + event_type)) + { + slog::log(gate, SLOG_FLF) + << nmos::stash_category(impl::categories::node_implementation) + << "Event received: " << std::boolalpha << nmos::fields::payload_boolean_value(payload) << " (" + << event_type.name << ")"; + } + } + }, + gate); +} + +// Example Connection API activation callback to perform application-specific operations to complete activation +nmos::connection_activation_handler make_node_implementation_connection_activation_handler(nmos::node_model& model, + slog::base_gate& gate) +{ + auto handle_load_ca_certificates = nmos::make_load_ca_certificates_handler(model.settings, gate); + // this example uses this callback to (un)subscribe a IS-07 Events WebSocket receiver when it is activated + // and, in addition to the message handler, specifies the optional close handler in order that any subsequent + // connection errors are reflected into the /active endpoint by setting master_enable to false + auto handle_events_ws_message = make_node_implementation_events_ws_message_handler(model, gate); + auto handle_close = nmos::experimental::make_events_ws_close_handler(model, gate); + auto connection_events_activation_handler = nmos::make_connection_events_websocket_activation_handler( + handle_load_ca_certificates, handle_events_ws_message, handle_close, model.settings, gate); + + return [connection_events_activation_handler, &gate](const nmos::resource& resource, + const nmos::resource& connection_resource) { + const std::pair id_type{resource.id, resource.type}; + slog::log(gate, SLOG_FLF) + << nmos::stash_category(impl::categories::node_implementation) << "Activating " << id_type; + + connection_events_activation_handler(resource, connection_resource); + }; +} + +// Example Channel Mapping API callback to perform application-specific validation of the merged active map during a +// POST /map/activations request +nmos::details::channelmapping_output_map_validator make_node_implementation_map_validator() +{ + // this example uses an 'empty' std::function because it does not need to do any validation + // beyond what is expressed by the schemas and /caps endpoints + return {}; +} + +// Example Channel Mapping API activation callback to perform application-specific operations to complete activation +nmos::channelmapping_activation_handler +make_node_implementation_channelmapping_activation_handler(slog::base_gate& gate) +{ + return [&gate](const nmos::resource& channelmapping_output) { + const auto output_id = nmos::fields::channelmapping_id(channelmapping_output.data); + slog::log(gate, SLOG_FLF) + << nmos::stash_category(impl::categories::node_implementation) << "Activating output: " << output_id; + }; +} + +namespace impl +{ + nmos::interlace_mode get_interlace_mode(const nmos::settings& settings) + { + if(settings.has_field(impl::fields::interlace_mode)) + { + return nmos::interlace_mode{impl::fields::interlace_mode(settings)}; + } + // for the default, 1080i50 and 1080i59.94 are arbitrarily preferred to 1080p25 and 1080p29.97 + // for 1080i formats, ST 2110-20 says that "the fields of an interlaced image are transmitted in time order, + // first field first [and] the sample rows of the temporally second field are displaced vertically 'below' the + // like-numbered sample rows of the temporally first field." + const auto frame_rate = nmos::parse_rational(impl::fields::frame_rate(settings)); + const auto frame_height = impl::fields::frame_height(settings); + return (nmos::rates::rate25 == frame_rate || nmos::rates::rate29_97 == frame_rate) && 1080 == frame_height + ? nmos::interlace_modes::interlaced_tff + : nmos::interlace_modes::progressive; + } + + bool is_rtp_port(const impl::port& port) + { + return impl::ports::rtp.end() != boost::range::find(impl::ports::rtp, port); + } + + bool is_ws_port(const impl::port& port) + { + return impl::ports::ws.end() != boost::range::find(impl::ports::ws, port); + } + + std::vector parse_ports(const web::json::value& value) + { + if(value.is_null()) return impl::ports::all; + return boost::copy_range>( + value.as_array() | + boost::adaptors::transformed([&](const web::json::value& value) { return port{value.as_string()}; })); + } + + // find interface with the specified address + std::vector::const_iterator + find_interface(const std::vector& interfaces, + const utility::string_t& address) + { + return boost::range::find_if(interfaces, [&](const web::hosts::experimental::host_interface& interface) { + return interface.addresses.end() != boost::range::find(interface.addresses, address); + }); + } + + // generate repeatable ids for the example node's resources + nmos::id make_id(const nmos::id& seed_id, const nmos::type& type, const impl::port& port, int index) + { + return nmos::make_repeatable_id(seed_id, U("/x-nmos/node/") + type.name + U('/') + port.name + + utility::conversions::details::to_string_t(index)); + } + + std::vector make_ids(const nmos::id& seed_id, const nmos::type& type, const impl::port& port, + int how_many) + { + return boost::copy_range>( + boost::irange(0, how_many) | + boost::adaptors::transformed([&](const int& index) { return impl::make_id(seed_id, type, port, index); })); + } + + std::vector make_ids(const nmos::id& seed_id, const nmos::type& type, const std::vector& ports, + int how_many) + { + // hm, boost::range::combine arrived in Boost 1.56.0 + std::vector ids; + for(const auto& port : ports) + { + boost::range::push_back(ids, make_ids(seed_id, type, port, how_many)); + } + return ids; + } + + std::vector make_ids(const nmos::id& seed_id, const std::vector& types, + const std::vector& ports, int how_many) + { + // hm, boost::range::combine arrived in Boost 1.56.0 + std::vector ids; + for(const auto& type : types) + { + boost::range::push_back(ids, make_ids(seed_id, type, ports, how_many)); + } + return ids; + } + + // generate a repeatable source-specific multicast address for each leg of a sender + utility::string_t make_source_specific_multicast_address_v4(const nmos::id& id, int leg) + { + // hash the pseudo-random id and leg to generate the address + const auto s = id + U('/') + utility::conversions::details::to_string_t(leg); + const auto h = std::hash{}(s); + auto a = boost::asio::ip::address_v4(uint32_t(h)).to_bytes(); + // ensure the address is in the source-specific multicast block reserved for local host allocation, + // 232.0.1.0-232.255.255.255 see + // https://www.iana.org/assignments/multicast-addresses/multicast-addresses.xhtml#multicast-addresses-10 + a[0] = 232; + a[2] |= 1; + return utility::s2us(boost::asio::ip::address_v4(a).to_string()); + } + + // add a selection of parents to a source or flow + void insert_parents(nmos::resource& resource, const nmos::id& seed_id, const port& port, int index) + { + // algorithm to produce signal ancestry with a range of depths and breadths + // see https://github.com/sony/nmos-cpp/issues/312#issuecomment-1335641637 + int b = 0; + while(index & (1 << b)) + ++b; + if(!b) return; + index &= ~(1 << (b - 1)); + do + { + index &= ~(1 << b); + web::json::push_back(resource.data[nmos::fields::parents], + impl::make_id(seed_id, resource.type, port, index)); + ++b; + } while(index & (1 << b)); + } + + // add a helpful suffix to the label of a sub-resource for the example node + void set_label_description(nmos::resource& resource, const impl::port& port, int index) + { + using web::json::value; + + auto label = nmos::fields::label(resource.data); + if(!label.empty()) label += U('/'); + label += resource.type.name + U('/') + port.name + utility::conversions::details::to_string_t(index); + resource.data[nmos::fields::label] = value::string(label); + + auto description = nmos::fields::description(resource.data); + if(!description.empty()) description += U('/'); + description += resource.type.name + U('/') + port.name + utility::conversions::details::to_string_t(index); + resource.data[nmos::fields::description] = value::string(description); + } + + // add an example "natural grouping" hint to a sender or receiver + void insert_group_hint(nmos::resource& resource, const impl::port& port, int index) + { + web::json::push_back( + resource.data[nmos::fields::tags][nmos::fields::group_hint], + nmos::make_group_hint({U("example"), resource.type.name + U(' ') + port.name + + utility::conversions::details::to_string_t(index)})); + } +} // namespace impl + +// This constructs all the callbacks used to integrate the example device-specific underlying implementation +// into the server instance for the NMOS Node. +nmos::experimental::node_implementation make_node_implementation(nmos::node_model& model, slog::base_gate& gate) +{ + 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)) + .on_load_ca_certificates(nmos::make_load_ca_certificates_handler(model.settings, gate)) + .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_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)); +} diff --git a/cpp/demos/nmos-cpp-node/node_implementation.h b/cpp/demos/nmos-cpp-node/node_implementation.h new file mode 100644 index 0000000..c83d457 --- /dev/null +++ b/cpp/demos/nmos-cpp-node/node_implementation.h @@ -0,0 +1,28 @@ +#ifndef NMOS_CPP_NODE_NODE_IMPLEMENTATION_H +#define NMOS_CPP_NODE_NODE_IMPLEMENTATION_H + +namespace slog +{ + class base_gate; +} + +namespace nmos +{ + struct node_model; + + namespace experimental + { + struct node_implementation; + } +} // namespace nmos + +// This is an example of how to integrate the nmos-cpp library with a device-specific underlying implementation. +// It constructs and inserts a node resource and some sub-resources into the model, based on the model settings, +// starts background tasks to emit regular events from the temperature event source, and then waits for shutdown. +void node_implementation_thread(nmos::node_model& model, slog::base_gate& gate); + +// This constructs all the callbacks used to integrate the example device-specific underlying implementation +// into the server instance for the NMOS Node. +nmos::experimental::node_implementation make_node_implementation(nmos::node_model& model, slog::base_gate& gate); + +#endif diff --git a/cpp/demos/ossrf-nmos-api/CMakeLists.txt b/cpp/demos/ossrf-nmos-api/CMakeLists.txt new file mode 100644 index 0000000..5cf0a1a --- /dev/null +++ b/cpp/demos/ossrf-nmos-api/CMakeLists.txt @@ -0,0 +1,23 @@ +cmake_minimum_required(VERSION 3.16) +project(ossrf-nmos-api LANGUAGES CXX) + +find_package(nlohmann_json REQUIRED) + +file(GLOB_RECURSE ${PROJECT_NAME}_source_files *.cpp *.h) + +add_executable(${PROJECT_NAME} ${${PROJECT_NAME}_source_files}) + +target_include_directories(${PROJECT_NAME} PRIVATE ${fmt_INCLUDE_DIRS}) +target_link_libraries(${PROJECT_NAME} + PRIVATE project_options project_warnings + nlohmann_json::nlohmann_json + PUBLIC + bisect::project_warnings + bisect::expected + bisect::bisect_json + ossrf::ossrf_nmos_api + ossrf::ossrf_gstreamer_api) + +target_compile_features(${PROJECT_NAME} PUBLIC cxx_std_23) + +install(TARGETS ${PROJECT_NAME}) diff --git a/cpp/demos/ossrf-nmos-api/config/nmos_config.json b/cpp/demos/ossrf-nmos-api/config/nmos_config.json new file mode 100644 index 0000000..cae96fd --- /dev/null +++ b/cpp/demos/ossrf-nmos-api/config/nmos_config.json @@ -0,0 +1,108 @@ +{ + "node": { + "id": "0aad3458-1081-4fba-af02-a8ebd9feeae3", + "configuration": { + "label": "BISECT OSSRF Node", + "description": "BISECT OSSRF node", + "host_addresses": [ + "192.168.1.79" + ], + "interfaces": [ + { + "chassis_id": "c8-94-02-f7-3e-eb", + "name": "wlp1s0", + "port_id": "00-e0-4c-68-01-8d" + } + ], + "clocks": [ + { + "name": "clk0", + "ref_type": "ptp", + "traceable": false, + "version": "IEEE1588-2008", + "gmid": "00-20-fc-ff-fe-35-9c-25", + "locked": true + } + ], + "registry_address": "192.168.1.79", + "registry_version": "v1.3", + "registration_port": 8010, + "system_address": "192.168.1.79", + "system_version": "v1.0", + "system_port": 8010 + } + }, + "device": { + "id": "b9b85f97-58db-41fe-934f-c2afbf7bd46f", + "label": "OSSRF Device", + "description": "OSSRF Device" + }, + "receivers": [ + { + "id": "db9f46cf-2414-4e25-b6c6-2078159857f9", + "label": "BISECT OSSRF receiver video", + "description": "BISECT OSSRF receiver video", + "network": { + "primary": { + "interface_address": "192.168.1.79", + "interface_name": "wlp1s0" + } + }, + "capabilities": [ + "video/raw" + ] + } + ], + "senders": [ + { + "id": "e543a2c1-d6a2-47f5-8d14-296bb6714ef2", + "label": "BISECT OSSRF sender video 1", + "description": "BISECT OSSRF sender video 1", + "network": { + "primary": { + "source_address": "192.168.1.79", + "interface_name": "wlp1s0", + "destination_address": "239.10.10.10", + "destination_port": 5004 + } + }, + "payload_type": 97, + "media_type": "video/raw", + "media": { + "width": 640, + "height": 480, + "frame_rate": { + "num": 50, + "den": 1 + }, + "sampling": "RGB_444", + "structure": "progressive" + } + }, + { + "id": "f2aa5651-c673-448c-bd02-5e8475898c7f", + "label": "BISECT OSSRF sender video 2", + "description": "BISECT OSSRF sender video 2", + "network": { + "primary": { + "source_address": "192.168.1.79", + "interface_name": "wlp1s0", + "destination_address": "239.10.10.11", + "destination_port": 5005 + } + }, + "payload_type": 97, + "media_type": "video/raw", + "media": { + "width": 640, + "height": 480, + "frame_rate": { + "num": 50, + "den": 1 + }, + "sampling": "RGB_444", + "structure": "progressive" + } + } + ] +} diff --git a/cpp/demos/ossrf-nmos-api/main.cpp b/cpp/demos/ossrf-nmos-api/main.cpp new file mode 100644 index 0000000..6ad7d7e --- /dev/null +++ b/cpp/demos/ossrf-nmos-api/main.cpp @@ -0,0 +1,129 @@ +#include "ossrf/nmos/api/nmos_client.h" +#include "ossrf/gstreamer/api/sender/sender_plugin.h" +#include "ossrf/gstreamer/api/receiver/receiver_plugin.h" +#include "bisect/nmoscpp/configuration.h" +#include "bisect/expected/macros.h" +#include "bisect/expected.h" +#include "bisect/json.h" +#include "bisect/initializer.h" +#include +#include + +using namespace bisect; +using namespace ossrf; +using namespace bisect::nmoscpp; +using json = nlohmann::json; + +namespace +{ + + void sender_activation_callback(bool master_enabled, const nlohmann::json& transport_params) + { + fmt::print("nmos_sender_callback: {} {}\n", master_enabled, transport_params.dump()); + } + + maybe_ok go(const json& app_configuration) + { + BST_ASSIGN(node, find(app_configuration, "node")); + BST_ASSIGN(node_id, find(node, "id")); + BST_ASSIGN(node_configuration, find(node, "configuration")); + BST_ASSIGN(device, find(app_configuration, "device")); + BST_ASSIGN(device_id, find(device, "id")); + + BST_ASSIGN(nmos_client, nmos_client_t::create(node_id, node_configuration.dump())); + BST_CHECK(nmos_client->add_device(device.dump())); + + ossrf::gst::plugins::gst_sender_plugin_uptr gst_sender_uptr = nullptr; + ossrf::gst::plugins::gst_sender_plugin_uptr gst_sender_uptr_2 = nullptr; + ossrf::gst::plugins::gst_receiver_plugin_uptr gst_receiver_uptr = nullptr; + + auto receivers_it = app_configuration.find("receivers"); + if(receivers_it != app_configuration.end()) + { + for(auto it = receivers_it->begin(); it != receivers_it->end(); ++it) + { + auto receiver_activation_callback = + [r = (*it).dump(), &gst_receiver_uptr](const std::optional& sdp, bool master_enable) { + if(sdp.has_value()) + { + fmt::print("nmos_receiver_callback: {} {}\n", master_enable, sdp.value()); + auto plugin = ossrf::gst::plugins::create_gst_receiver_plugin(r, sdp.value()); + if(plugin.has_value()) + { + gst_receiver_uptr.reset(plugin.value().release()); + return; + } + fmt::print("failed creating receiver\n"); + return; + } + fmt::print("nmos_receiver_callback: {} no sdp\n", master_enable); + }; + + BST_CHECK(nmos_client->add_receiver(device_id, (*it).dump(), receiver_activation_callback)); + } + } + + auto senders_it = app_configuration.find("senders"); + if(senders_it != app_configuration.end()) + { + auto i = 1; + for(auto it = senders_it->begin(); it != senders_it->end(); ++it) + { + BST_CHECK(nmos_client->add_sender(device_id, (*it).dump(), sender_activation_callback)); + if(i == 1) + { + BST_CHECK_ASSIGN(gst_sender_uptr, ossrf::gst::plugins::create_gst_sender_plugin((*it).dump(), 25)); + } + else if(i == 2) + { + BST_CHECK_ASSIGN(gst_sender_uptr_2, + ossrf::gst::plugins::create_gst_sender_plugin((*it).dump(), 15)); + } + i++; + } + } + + fmt::print("\n >>> Press a key to stop <<< \n"); + char c; + std::cin >> c; + + BST_CHECK(nmos_client->remove_resource(device_id, nmos::types::device)); + fmt::print("\n >>> Stopped <<< \n"); + + return {}; + } + + expected load_configuration_from_file(std::string_view config_file) + { + std::ifstream ifs(config_file.data()); + BST_ENFORCE(ifs.is_open(), "Failed opening file {}", config_file); + std::ostringstream buffer; + buffer << ifs.rdbuf(); + return parse_json(buffer.str()); + } + + maybe_ok run(std::string_view configuration_file) + { + BST_ASSIGN(configuration, load_configuration_from_file(configuration_file)); + auto init = bisect::gst::initializer(); + return go(configuration); + } +} // namespace + +int main(int argc, char* argv[]) +{ + if(argc == 3 && argv[1] == std::string("-f")) + { + const auto configuration_file = argv[2]; + auto result = run(configuration_file); + if(!result.has_value()) + { + fprintf(stderr, "error: %s", result.error().what()); + return -1; + } + return 0; + } + + fprintf(stderr, "usage: %s [-f ]\n\n", argv[0]); + return -1; +} diff --git a/cpp/libs/CMakeLists.txt b/cpp/libs/CMakeLists.txt new file mode 100644 index 0000000..c5298e1 --- /dev/null +++ b/cpp/libs/CMakeLists.txt @@ -0,0 +1,7 @@ +add_subdirectory(bisect_expected) +add_subdirectory(bisect_nmoscpp) +add_subdirectory(bisect_sdp) +add_subdirectory(bisect_gst) +add_subdirectory(ossrf_nmos_api) +add_subdirectory(ossrf_gstreamer_api) +add_subdirectory(bisect_json) diff --git a/cpp/libs/bisect_expected/CMakeLists.txt b/cpp/libs/bisect_expected/CMakeLists.txt new file mode 100644 index 0000000..f3a4382 --- /dev/null +++ b/cpp/libs/bisect_expected/CMakeLists.txt @@ -0,0 +1,10 @@ +add_subdirectory(lib) + +option(BISECT_EXPECTED_ENABLE_TESTS "Enable tests" ON) + +if (BISECT_EXPECTED_ENABLE_TESTS) + include(CTest) + enable_testing() + add_subdirectory(tests) +endif() + diff --git a/cpp/libs/bisect_expected/lib/CMakeLists.txt b/cpp/libs/bisect_expected/lib/CMakeLists.txt new file mode 100644 index 0000000..7fd6ee4 --- /dev/null +++ b/cpp/libs/bisect_expected/lib/CMakeLists.txt @@ -0,0 +1,26 @@ +project(bisect_expected LANGUAGES CXX) + +file(GLOB_RECURSE ${PROJECT_NAME}_source_files *.cpp *.h) + +find_package(fmt REQUIRED) + +add_library(${PROJECT_NAME} INTERFACE) +add_library(bisect::expected ALIAS ${PROJECT_NAME}) + +target_link_libraries( + ${PROJECT_NAME} + INTERFACE bisect::project_options bisect::project_warnings + fmt::fmt) + +target_include_directories( + ${PROJECT_NAME} + INTERFACE + include +) + +set_target_properties( + ${PROJECT_NAME} + PROPERTIES CXX_STANDARD 23 + CXX_STANDARD_REQUIRED YES + CXX_EXTENSIONS NO + POSITION_INDEPENDENT_CODE ON) diff --git a/cpp/libs/bisect_expected/lib/include/bisect/expected.h b/cpp/libs/bisect_expected/lib/include/bisect/expected.h new file mode 100644 index 0000000..4c502fe --- /dev/null +++ b/cpp/libs/bisect_expected/lib/include/bisect/expected.h @@ -0,0 +1,15 @@ +#pragma once + +#include +#include + +namespace bisect +{ + template using expected = std::expected; + + struct result_ok + { + }; + + using maybe_ok = std::expected; +} // namespace bisect diff --git a/cpp/libs/bisect_expected/lib/include/bisect/expected/helpers.h b/cpp/libs/bisect_expected/lib/include/bisect/expected/helpers.h new file mode 100644 index 0000000..b02be47 --- /dev/null +++ b/cpp/libs/bisect_expected/lib/include/bisect/expected/helpers.h @@ -0,0 +1,29 @@ +#pragma once + +#include "bisect/expected.h" +#include + +namespace bisect::core::detail +{ + template inline bool is_error(const expected& result) + { + return !result.has_value(); + } + + template inline std::string get_error_msg(const expected& result) + { + return result.error().what(); + } + + inline std::string make_error_msg(std::string_view expression, std::string error_message) + { + return std::string{"error: "} + error_message + " (in '" + std::string{expression} + "')."; + } + + template + inline std::unexpected make_error(const Result& result, std::string_view expression) + { + return std::unexpected{std::runtime_error{make_error_msg(expression, get_error_msg(result))}}; + } + +} // namespace bisect::core::detail diff --git a/cpp/libs/bisect_expected/lib/include/bisect/expected/macros.h b/cpp/libs/bisect_expected/lib/include/bisect/expected/macros.h new file mode 100644 index 0000000..80ad22d --- /dev/null +++ b/cpp/libs/bisect_expected/lib/include/bisect/expected/macros.h @@ -0,0 +1,55 @@ +#pragma once + +#include "bisect/expected/helpers.h" +#include "bisect/fmt.h" + +#define BISECT_TOKEN_PASTE_1(x, y) x##y +#define BISECT_TOKEN_PASTE(x, y) BISECT_TOKEN_PASTE_1(x, y) + +#define BST_CHECK_IMPL(COUNTER, EXPR) \ + { \ + const auto& BISECT_TOKEN_PASTE(result_, COUNTER) = (EXPR); \ + if(bisect::core::detail::is_error(BISECT_TOKEN_PASTE(result_, COUNTER))) \ + { \ + return bisect::core::detail::make_error(BISECT_TOKEN_PASTE(result_, COUNTER), #EXPR); \ + } \ + } + +#define BST_CHECK(EXPR) BST_CHECK_IMPL(__COUNTER__, EXPR) + +#define BST_ASSIGN(VAR, EXPR) \ + auto BISECT_TOKEN_PASTE(Unique_, __LINE__) = (EXPR); \ + BST_CHECK(BISECT_TOKEN_PASTE(Unique_, __LINE__)); \ + const auto VAR = std::move(BISECT_TOKEN_PASTE(Unique_, __LINE__)).value(); + +#define BST_ASSIGN_MUT(VAR, EXPR) \ + auto BISECT_TOKEN_PASTE(Unique_, __LINE__) = (EXPR); \ + BST_CHECK(BISECT_TOKEN_PASTE(Unique_, __LINE__)); \ + auto VAR = std::move(BISECT_TOKEN_PASTE(Unique_, __LINE__)).value(); + +#define BST_CHECK_ASSIGN(VAR, EXPR) \ + auto BISECT_TOKEN_PASTE(Unique_, __LINE__) = (EXPR); \ + BST_CHECK(BISECT_TOKEN_PASTE(Unique_, __LINE__)); \ + VAR = std::move(BISECT_TOKEN_PASTE(Unique_, __LINE__)).value(); + +#define BST_ENFORCE_IMPL(COUNTER, EXPR, ...) \ + { \ + if(!(EXPR)) \ + { \ + const auto BISECT_TOKEN_PASTE(core_message, COUNTER) = fmt::format(__VA_ARGS__); \ + const auto BISECT_TOKEN_PASTE(t, COUNTER) = \ + fmt::format("{} - at '{}', line {}, '{}'", BISECT_TOKEN_PASTE(core_message, COUNTER), __FILE__, \ + __LINE__, __PRETTY_FUNCTION__); \ + return std::unexpected(std::runtime_error(BISECT_TOKEN_PASTE(t, COUNTER))); \ + } \ + } + +#define BST_ENFORCE(EXPR, ...) BST_ENFORCE_IMPL(__COUNTER__, EXPR, __VA_ARGS__) + +#define BST_FAIL(...) \ + { \ + const auto core_message = fmt::format(__VA_ARGS__); \ + const auto t = \ + fmt::format("{} - at '{}', line {}, '{}'", core_message, __FILE__, __LINE__, __PRETTY_FUNCTION__); \ + return std::unexpected(std::runtime_error(t)); \ + } diff --git a/cpp/libs/bisect_expected/lib/include/bisect/expected/match.h b/cpp/libs/bisect_expected/lib/include/bisect/expected/match.h new file mode 100644 index 0000000..a0a4864 --- /dev/null +++ b/cpp/libs/bisect_expected/lib/include/bisect/expected/match.h @@ -0,0 +1,17 @@ +#pragma once + +#include + +namespace bisect +{ + template struct overload : Ts... + { + using Ts::operator()...; + }; + template overload(Ts...) -> overload; + + template auto match(Variant&& variant, Matchers&&... matchers) + { + return std::visit(overload{std::forward(matchers)...}, std::forward(variant)); + } +} // namespace bisect diff --git a/cpp/libs/bisect_expected/lib/include/bisect/fmt.h b/cpp/libs/bisect_expected/lib/include/bisect/fmt.h new file mode 100644 index 0000000..64fafe6 --- /dev/null +++ b/cpp/libs/bisect_expected/lib/include/bisect/fmt.h @@ -0,0 +1,9 @@ +/** Wrapper around the fmt headers to deal with an issue caused by libs like CppRestSDK. + * CppRestSDK defines a macro "U" and that pollutes everything that uses it directly or indirectly. + */ + +#if defined(U) +#undef U +#endif // defined(U) + +#include diff --git a/cpp/libs/bisect_expected/lib/src/expected.cpp b/cpp/libs/bisect_expected/lib/src/expected.cpp new file mode 100644 index 0000000..6e00aa0 --- /dev/null +++ b/cpp/libs/bisect_expected/lib/src/expected.cpp @@ -0,0 +1 @@ +#include "bisect/core/expected/helpers.h" diff --git a/cpp/libs/bisect_expected/tests/CMakeLists.txt b/cpp/libs/bisect_expected/tests/CMakeLists.txt new file mode 100644 index 0000000..3f82957 --- /dev/null +++ b/cpp/libs/bisect_expected/tests/CMakeLists.txt @@ -0,0 +1,16 @@ +project(bisect_expected_tests LANGUAGES CXX) + +file(GLOB_RECURSE ${PROJECT_NAME}_source_files *.cpp *.h) + +find_package(GTest REQUIRED) + +add_executable(${PROJECT_NAME} ${${PROJECT_NAME}_source_files}) + +target_link_libraries( + ${PROJECT_NAME} + PRIVATE bisect::project_options bisect::project_warnings bisect::expected gtest::gtest) + +target_compile_features(${PROJECT_NAME} PUBLIC cxx_std_23) + +include(GoogleTest) +gtest_discover_tests(${PROJECT_NAME}) diff --git a/cpp/libs/bisect_expected/tests/expected/test_expected.cpp b/cpp/libs/bisect_expected/tests/expected/test_expected.cpp new file mode 100644 index 0000000..e20506b --- /dev/null +++ b/cpp/libs/bisect_expected/tests/expected/test_expected.cpp @@ -0,0 +1,29 @@ +#include "bisect/expected/macros.h" +#include + +using namespace bisect; +using namespace bisect::core::detail; + +namespace +{ + expected f_ok() + { + return {true}; + } + + expected f_fail() + { + return std::unexpected(std::runtime_error("error")); + } +} // namespace + +TEST(bisect_expected, test_expected) +{ + + ASSERT_TRUE(f_ok().has_value()); +} + +TEST(bisect_expected, test_unexpected) +{ + ASSERT_TRUE(f_fail().error().what() == std::string_view("error")); +} diff --git a/cpp/libs/bisect_expected/tests/expected/test_expected_macros.cpp b/cpp/libs/bisect_expected/tests/expected/test_expected_macros.cpp new file mode 100644 index 0000000..a28343e --- /dev/null +++ b/cpp/libs/bisect_expected/tests/expected/test_expected_macros.cpp @@ -0,0 +1,69 @@ +#include "bisect/expected/macros.h" +#include + +using namespace bisect; +using namespace bisect::core::detail; + +namespace +{ + maybe_ok enforce_true(bool b) + { + BST_ENFORCE(b, "Failed!"); + return {}; + } + + maybe_ok do_fail() + { + BST_FAIL("throw"); + }; + + maybe_ok do_check(bool fail) + { + if(fail) + { + BST_CHECK(do_fail()); + } + + return {}; + } + + template expected do_assign(expected v) + { + BST_ASSIGN(a, v); + return a; + } + + template expected do_check_assign(expected v) + { + int a{}; + BST_CHECK_ASSIGN(a, v); + return a; + } +} // namespace + +TEST(bisect_expected_macros, test_enforce) +{ + ASSERT_TRUE(enforce_true(true).has_value()); + ASSERT_TRUE(is_error(enforce_true(false))); +} + +TEST(bisect_expected_macros, test_fail) +{ + ASSERT_TRUE(is_error(do_fail())); +} + +TEST(bisect_expected_macros, test_check) +{ + ASSERT_TRUE(is_error(do_check(true))); + ASSERT_TRUE(do_check(false).has_value()); +} + +TEST(bisect_expected_macros, test_check_assign) +{ + auto v5 = do_check_assign(expected{5}); + ASSERT_TRUE(v5.has_value()); + ASSERT_EQ(v5.value(), 5); + + auto ve = do_check_assign(expected{std::unexpected(std::runtime_error(""))}); + ASSERT_TRUE(is_error(ve)); +} diff --git a/cpp/libs/bisect_gst/CMakeLists.txt b/cpp/libs/bisect_gst/CMakeLists.txt new file mode 100644 index 0000000..3ea7a41 --- /dev/null +++ b/cpp/libs/bisect_gst/CMakeLists.txt @@ -0,0 +1 @@ +add_subdirectory(lib) diff --git a/cpp/libs/bisect_gst/lib/CMakeLists.txt b/cpp/libs/bisect_gst/lib/CMakeLists.txt new file mode 100644 index 0000000..70eca05 --- /dev/null +++ b/cpp/libs/bisect_gst/lib/CMakeLists.txt @@ -0,0 +1,60 @@ +project(bisect_gst LANGUAGES CXX) + +file(GLOB_RECURSE ${PROJECT_NAME}_source_files *.cpp *.h) + +find_package(nmos-cpp REQUIRED) +find_package(fmt REQUIRED) + + +add_library(${PROJECT_NAME} STATIC ${${PROJECT_NAME}_source_files}) + +target_link_libraries( + ${PROJECT_NAME} + PRIVATE + bisect::project_options bisect::project_warnings + PUBLIC + bisect::bisect_nmoscpp + bisect::expected + nmos-cpp::compile-settings + nmos-cpp::nmos-cpp + fmt::fmt + ) + +find_package(PkgConfig REQUIRED) + pkg_search_module(gstreamer REQUIRED IMPORTED_TARGET gstreamer-1.0>=1.4) + pkg_search_module(gstreamer-app REQUIRED IMPORTED_TARGET gstreamer-app-1.0>=1.4) + pkg_search_module(gstreamer-audio REQUIRED IMPORTED_TARGET gstreamer-audio-1.0>=1.4) + pkg_search_module(gstreamer-video REQUIRED IMPORTED_TARGET gstreamer-video-1.0>=1.4) + + +target_link_libraries( + ${PROJECT_NAME} + PUBLIC + PkgConfig::gstreamer + PkgConfig::gstreamer-app + PkgConfig::gstreamer-audio + PkgConfig::gstreamer-video + ) + +set_target_properties( + ${PROJECT_NAME} + PROPERTIES CXX_EXTENSIONS NO + POSITION_INDEPENDENT_CODE ON) + +target_compile_features(${PROJECT_NAME} PUBLIC cxx_std_20) + +target_include_directories( + ${PROJECT_NAME} + PUBLIC $ + $ + $ + PRIVATE lib/src) + +add_library(bisect::${PROJECT_NAME} ALIAS ${PROJECT_NAME}) + +install(TARGETS ${PROJECT_NAME}) +install(DIRECTORY "${CMAKE_CURRENT_LIST_DIR}/include" # source directory + DESTINATION "." # target directory + FILES_MATCHING # install only matched files + PATTERN "*.h" # select header files +) diff --git a/cpp/libs/bisect_gst/lib/include/bisect/initializer.h b/cpp/libs/bisect_gst/lib/include/bisect/initializer.h new file mode 100644 index 0000000..00a8325 --- /dev/null +++ b/cpp/libs/bisect_gst/lib/include/bisect/initializer.h @@ -0,0 +1,12 @@ +#include + +namespace bisect::gst +{ + class initializer + { + public: + initializer(); + + ~initializer(); + }; +} // namespace bisect::gst \ No newline at end of file diff --git a/cpp/libs/bisect_gst/lib/include/bisect/pipeline.h b/cpp/libs/bisect_gst/lib/include/bisect/pipeline.h new file mode 100644 index 0000000..3639a4c --- /dev/null +++ b/cpp/libs/bisect_gst/lib/include/bisect/pipeline.h @@ -0,0 +1,38 @@ +#include "bisect/expected.h" +#include +#include +#include +#include +#include + +namespace bisect::gst +{ + class pipeline + { + public: + static bisect::expected create(const char* klass) noexcept; + + pipeline() noexcept; + ~pipeline(); + pipeline(const pipeline& other) noexcept = delete; + pipeline& operator=(const pipeline& rhs) noexcept = delete; + pipeline(pipeline&& other); + pipeline& operator=(pipeline&& rhs); + + GstElement* get() noexcept; + + void run_loop(); + bisect::maybe_ok pause(); + bisect::maybe_ok play(); + void stop(); + + // After calling release, the destructor will not unref the pipeline. + void release() noexcept; + + private: + explicit pipeline(GstElement* bin) noexcept; + + struct impl; + std::unique_ptr impl_; + }; +} // namespace bisect::gst \ No newline at end of file diff --git a/cpp/libs/bisect_gst/lib/src/initializer.cpp b/cpp/libs/bisect_gst/lib/src/initializer.cpp new file mode 100644 index 0000000..6db7f27 --- /dev/null +++ b/cpp/libs/bisect_gst/lib/src/initializer.cpp @@ -0,0 +1,13 @@ +#include "bisect/initializer.h" + +using namespace bisect::gst; + +initializer::initializer() +{ + gst_init(NULL, NULL); +} + +initializer::~initializer() +{ + gst_deinit(); +} \ No newline at end of file diff --git a/cpp/libs/bisect_gst/lib/src/pipeline.cpp b/cpp/libs/bisect_gst/lib/src/pipeline.cpp new file mode 100644 index 0000000..78dae0c --- /dev/null +++ b/cpp/libs/bisect_gst/lib/src/pipeline.cpp @@ -0,0 +1,157 @@ +#include "bisect/pipeline.h" +#include "bisect/expected/match.h" +#include "bisect/expected/macros.h" +#include "bisect/expected.h" + +using namespace bisect; +using namespace bisect::gst; + +struct pipeline::impl +{ + impl(GstElement* pipeline) : pipeline_(pipeline) {} + + ~impl() + { + g_main_loop_quit(gst_loop_); + this->wait(); + gst_object_unref(GST_OBJECT(pipeline_)); + g_main_loop_unref(gst_loop_); + } + + void start() + { + gst_loop_ = g_main_loop_new(nullptr, FALSE); + thread_ = std::thread([this]() { + auto result = this->run(); + if(!result.has_value()) this->handle_error(result.error().what()); + }); + } + + maybe_ok run() + { + is_owned_ = false; + auto bus = gst_pipeline_get_bus(GST_PIPELINE(pipeline_)); + gst_bus_add_watch(bus, &impl::bus_callback, this); + gst_object_unref(bus); + + /* run */ + BST_ENFORCE(gst_element_set_state(pipeline_, GST_STATE_PLAYING) != GST_STATE_CHANGE_FAILURE, + "Failed changing GStreamer pipeline to Playing"); + g_main_loop_run(gst_loop_); + + /* cleanup */ + BST_ENFORCE(gst_element_set_state(pipeline_, GST_STATE_NULL) != GST_STATE_CHANGE_FAILURE, + "Failed changing GStreamer pipeline to Null"); + + return {}; + } + + void wait() + { + if(thread_.joinable()) + { + thread_.join(); + } + } + + void handle_error(const char* message) { g_main_loop_quit(gst_loop_); } + + static gboolean bus_callback(GstBus* /*bus*/, GstMessage* message, gpointer data) + { + auto self = static_cast(data); + + switch(GST_MESSAGE_TYPE(message)) + { + case GST_MESSAGE_ERROR: { + GError* err; + gchar* debug; + + gst_message_parse_error(message, &err, &debug); + g_printerr("Error received from element %s: %s\n", GST_OBJECT_NAME(message->src), err->message); + g_printerr("Debugging information: %s\n", debug ? debug : "none"); + self->handle_error(err->message); + g_error_free(err); + g_free(debug); + + break; + } + case GST_MESSAGE_EOS: + /* end-of-stream */ + break; + case GST_MESSAGE_STATE_CHANGED: + GstState old_state, new_state, pending_state; + gst_message_parse_state_changed(message, &old_state, &new_state, &pending_state); + g_print("State changed from %s to %s\n", gst_element_state_get_name(old_state), + gst_element_state_get_name(new_state)); + break; + default: + /* unhandled message */ + break; + } + + /* we want to be notified again the next time there is a message + * on the bus, so returning TRUE (FALSE means we want to stop watching + * for messages on the bus and our callback should not be called again) + */ + return TRUE; + } + + GMainLoop* gst_loop_; + GstElement* pipeline_; + std::thread thread_; + bool is_owned_ = true; +}; + +pipeline::pipeline(GstElement* bin) noexcept : impl_(std::make_unique(bin)) +{ +} + +pipeline::pipeline() noexcept +{ +} + +bisect::expected pipeline::create(const char* klass) noexcept +{ + auto bin = gst_pipeline_new(klass); + BST_ENFORCE(bin != nullptr, "Failed to create GStreamer pipeline"); + + return pipeline(bin); +} + +pipeline::~pipeline() = default; +pipeline::pipeline(pipeline&& other) = default; +pipeline& pipeline::operator=(pipeline&& other) = default; + +GstElement* pipeline::get() noexcept +{ + return impl_->pipeline_; +} + +void pipeline::release() noexcept +{ + impl_->is_owned_ = false; +} + +void pipeline::run_loop() +{ + impl_->start(); +} + +maybe_ok pipeline::play() +{ + BST_ENFORCE(gst_element_set_state(impl_->pipeline_, GST_STATE_PLAYING), + "Failed changing GStreamer pipeline to Playing"); + return {}; +} + +maybe_ok pipeline::pause() +{ + BST_ENFORCE(gst_element_set_state(impl_->pipeline_, GST_STATE_PAUSED), + "Failed changing GStreamer pipeline to Pause"); + return {}; +} + +void pipeline::stop() +{ + g_main_loop_quit(impl_->gst_loop_); +} diff --git a/cpp/libs/bisect_json/CMakeLists.txt b/cpp/libs/bisect_json/CMakeLists.txt new file mode 100644 index 0000000..b81ec72 --- /dev/null +++ b/cpp/libs/bisect_json/CMakeLists.txt @@ -0,0 +1,5 @@ +add_subdirectory(lib) + +if (BISECT_CPP_CORE_ENABLE_TESTS) + add_subdirectory(tests) +endif() diff --git a/cpp/libs/bisect_json/LICENSE.txt b/cpp/libs/bisect_json/LICENSE.txt new file mode 100644 index 0000000..cf56baf --- /dev/null +++ b/cpp/libs/bisect_json/LICENSE.txt @@ -0,0 +1 @@ +(C) BISECT LDA - All rights reserved. diff --git a/cpp/libs/bisect_json/lib/CMakeLists.txt b/cpp/libs/bisect_json/lib/CMakeLists.txt new file mode 100644 index 0000000..062080e --- /dev/null +++ b/cpp/libs/bisect_json/lib/CMakeLists.txt @@ -0,0 +1,36 @@ +project(bisect_json LANGUAGES CXX) + +file(GLOB_RECURSE ${PROJECT_NAME}_source_files *.cpp *.h) + +find_package(fmt REQUIRED) +find_package(nlohmann_json REQUIRED) + +add_library(${PROJECT_NAME} STATIC ${${PROJECT_NAME}_source_files}) + +target_link_libraries( + ${PROJECT_NAME} + PRIVATE bisect::project_options bisect::project_warnings + PUBLIC fmt::fmt nlohmann_json::nlohmann_json bisect::expected) + +set_target_properties( + ${PROJECT_NAME} + PROPERTIES CXX_EXTENSIONS NO + POSITION_INDEPENDENT_CODE ON) + +target_compile_features(${PROJECT_NAME} PUBLIC cxx_std_23) + +target_include_directories( + ${PROJECT_NAME} + PUBLIC $ + $ + $ + PRIVATE lib/src) + +add_library(bisect::${PROJECT_NAME} ALIAS ${PROJECT_NAME}) + +install(TARGETS ${PROJECT_NAME}) +install(DIRECTORY "${CMAKE_CURRENT_LIST_DIR}/include" # source directory + DESTINATION "." # target directory + FILES_MATCHING # install only matched files + PATTERN "*.h" # select header files +) diff --git a/cpp/libs/bisect_json/lib/include/bisect/json.h b/cpp/libs/bisect_json/lib/include/bisect/json.h new file mode 100644 index 0000000..07d5b96 --- /dev/null +++ b/cpp/libs/bisect_json/lib/include/bisect/json.h @@ -0,0 +1,3 @@ +#pragma once + +#include "bisect/json/json.h" diff --git a/cpp/libs/bisect_json/lib/include/bisect/json/json.h b/cpp/libs/bisect_json/lib/include/bisect/json/json.h new file mode 100644 index 0000000..658eb91 --- /dev/null +++ b/cpp/libs/bisect_json/lib/include/bisect/json/json.h @@ -0,0 +1,265 @@ +#pragma once + +#include "bisect/expected/macros.h" +#include +#include +#include + +namespace bisect +{ + inline expected parse_json(std::string_view s) + { + try + { + return nlohmann::json::parse(s); + } + catch(std::exception& ex) + { + BST_FAIL("error parsing JSON: {}", ex.what()); + } + } + + namespace detail + { + template T get_pointer_type(T C::*v); + + template bool is_valid(const nlohmann::json& j); + + template <> inline bool is_valid(const nlohmann::json& j) + { + return j.is_number(); + } + + template <> inline bool is_valid(const nlohmann::json& j) + { + return j.is_number(); + } + + template <> inline bool is_valid(const nlohmann::json& j) + { + return j.is_number(); + } + + template <> inline bool is_valid(const nlohmann::json& j) + { + return j.is_number(); + } + + template <> inline bool is_valid(const nlohmann::json& j) + { + return j.is_number(); + } + + template <> inline bool is_valid(const nlohmann::json& j) + { + return j.is_number(); + } + + template <> inline bool is_valid(const nlohmann::json& j) + { + return j.is_number(); + } + + template <> inline bool is_valid(const nlohmann::json& j) + { + return j != nullptr && j.is_string(); + } + + template <> bool inline is_valid(const nlohmann::json& j) + { + return j.is_boolean(); + } + + template <> bool inline is_valid(const nlohmann::json&) + { + return true; + } + } // namespace detail + + template [[nodiscard]] inline expected get_as(const nlohmann::json& j) + { + BST_ENFORCE(detail::is_valid(j), "JSON element does not have the right type: {}", j.dump()); + return expected(j.get()); + } + + template expected find(const nlohmann::json& j, std::string_view next) + { + const auto it = j.find(next); + BST_ENFORCE(it != j.end(), "Value with key '{}' not found in {}", next, j.dump()); + return get_as(*it); + } + + template expected find(const nlohmann::json& j, std::string_view next, Ts... rest) + { + const auto value_it = j.find(next); + BST_ENFORCE(value_it != j.end(), "Value with key '{}' not found", next); + return find(*value_it, rest...); + } + + template + [[nodiscard]] inline expected find_or(const nlohmann::json& j, std::string_view name, R&& default_value = T{}) + { + const auto it = j.find(name); + if(it == j.end()) + { + return default_value; + } + + return get_as(*it); + } + + template > + maybe_ok assign_if( + const nlohmann::json& j, std::string_view name, O& o, M member, F conversion = [](const auto& v) { return v; }) + { + if(const auto it = j.find(name); it != j.end()) + { + if(!detail::is_valid(*it)) + { + const auto message = fmt::format("Invalid type for {}", j.dump()); + fmt::print("{}\n", message); + return std::unexpected(std::runtime_error(message)); + } + +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wnull-dereference" + o.*member = conversion(it->get()); +#pragma GCC diagnostic pop + return {}; + } + + const auto message = fmt::format("{} not found in {}", name, j.dump()); + return std::unexpected(std::runtime_error(message)); + } + + template > + maybe_ok assign_if_or( + const nlohmann::json& j, std::string_view name, O& o, M member, F conversion = [](const auto& v) { return v; }, + R default_value = R{}) + { + if(const auto it = j.find(name); it != j.end()) + { + if(!detail::is_valid(*it)) + { + o.*member = default_value; + + const auto message = fmt::format("Invalid type for {}", j.dump()); + fmt::print("{}\n", message); + return std::unexpected(std::runtime_error(message)); + } + +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wnull-dereference" + o.*member = conversion(it->get()); +#pragma GCC diagnostic pop + + return {}; + } + + o.*member = default_value; + return {}; + } + + template > + maybe_ok assign_if_or_value( + const nlohmann::json& j, std::string_view name, O& o, M member, T value, + F conversion = [](const auto& v) { return v; }) + { + if(const auto it = j.find(name); it != j.end()) + { + if(!detail::is_valid(*it)) + { + const auto message = fmt::format("Invalid type for {}", j.dump()); + fmt::print("{}\n", message); + return std::unexpected(std::runtime_error(message)); + } + +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wnull-dereference" + o.*member = conversion(it->get()); +#pragma GCC diagnostic pop + return {}; + } + + o.*member = value; + return {}; + } + + template expected> maybe_find(const nlohmann::json& j, std::string_view next) + { + const auto it = j.find(next); + if(it == j.end()) return std::nullopt; + BST_ASSIGN(result, get_as(*it)); + return result; + } + + template + expected> maybe_find(const nlohmann::json& j, std::string_view next, Ts... rest) + { + const auto it = j.find(next); + if(it == j.end()) return std::nullopt; + return maybe_find(*it, rest...); + } + + namespace selectors + { + using json = nlohmann::json; + + using mapper_t = std::function(const json&)>; + + namespace detail + { + template expected do_select(const json& j, T matcher) + { + BST_ASSIGN(level, matcher(j)); + return get_as(level); + } + + template + expected do_select(const json& j, T matcher, Ts... matchers) + { + BST_ASSIGN(level, matcher(j)); + return do_select(level, matchers...); + } + } // namespace detail + + inline auto element(std::string name) -> mapper_t + { + return [name](const json& j) -> expected { return find(j, name); }; + } + + inline auto index(json::size_type idx) -> mapper_t + { + return [idx](const json& j) -> expected { + BST_ENFORCE(idx < j.size(), "Index is greater than array length: {} {}", j.dump(), idx); + return expected{j[idx]}; + }; + } + + inline auto name_value(std::string name) -> mapper_t + { + return [name](const json& j) -> expected { + BST_ENFORCE(j.is_array(), "JSON value is not an array."); + + for(const auto& element : j) + { + BST_ASSIGN(actual_name, find(element, "name")); + if(actual_name == name) + { + return find(element, "value"); + } + } + + BST_FAIL("JSON does not contain {}: {}", name, j.dump()); + }; + } + } // namespace selectors + + template expected select(const nlohmann::json& j, Ts... matchers) + { + return selectors::detail::do_select(j, matchers...); + } +} // namespace bisect diff --git a/cpp/libs/bisect_json/lib/src/json.cpp b/cpp/libs/bisect_json/lib/src/json.cpp new file mode 100644 index 0000000..4d1d862 --- /dev/null +++ b/cpp/libs/bisect_json/lib/src/json.cpp @@ -0,0 +1 @@ +#include "bisect/json.h" diff --git a/cpp/libs/bisect_json/tests/CMakeLists.txt b/cpp/libs/bisect_json/tests/CMakeLists.txt new file mode 100644 index 0000000..5012cbc --- /dev/null +++ b/cpp/libs/bisect_json/tests/CMakeLists.txt @@ -0,0 +1,16 @@ +project(bisect_json_tests LANGUAGES CXX) + +file(GLOB_RECURSE ${PROJECT_NAME}_source_files *.cpp *.h) + +find_package(GTest REQUIRED) + +add_executable(${PROJECT_NAME} ${${PROJECT_NAME}_source_files}) + +target_link_libraries( + ${PROJECT_NAME} + PRIVATE bisect::project_options bisect::project_warnings bisect::bisect_json gtest::gtest) + +target_compile_features(${PROJECT_NAME} PUBLIC cxx_std_23) + +include(GoogleTest) +gtest_discover_tests(${PROJECT_NAME}) diff --git a/cpp/libs/bisect_json/tests/test_parse.cpp b/cpp/libs/bisect_json/tests/test_parse.cpp new file mode 100644 index 0000000..c52a00b --- /dev/null +++ b/cpp/libs/bisect_json/tests/test_parse.cpp @@ -0,0 +1,21 @@ +#include "bisect/json.h" +#include + +using namespace bisect; +using json = nlohmann::json; + +TEST(bisect_json, test_find_root) +{ + const auto s1 = R"({ "a": 1})"; + auto j = json::parse(s1); + const auto v = find(j, "a").value(); + ASSERT_TRUE(v == 1); +} + +TEST(bisect_json, test_find_deep) +{ + const auto s1 = R"({ "a1": {"a2": {"a3": "value"}}})"; + auto j = json::parse(s1); + const auto v = find(j, "a1", "a2", "a3").value(); + ASSERT_TRUE(v == "value"); +} diff --git a/cpp/libs/bisect_nmoscpp/CMakeLists.txt b/cpp/libs/bisect_nmoscpp/CMakeLists.txt new file mode 100644 index 0000000..3ea7a41 --- /dev/null +++ b/cpp/libs/bisect_nmoscpp/CMakeLists.txt @@ -0,0 +1 @@ +add_subdirectory(lib) diff --git a/cpp/libs/bisect_nmoscpp/lib/CMakeLists.txt b/cpp/libs/bisect_nmoscpp/lib/CMakeLists.txt new file mode 100644 index 0000000..70e328c --- /dev/null +++ b/cpp/libs/bisect_nmoscpp/lib/CMakeLists.txt @@ -0,0 +1,43 @@ +project(bisect_nmoscpp LANGUAGES CXX) + +file(GLOB_RECURSE ${PROJECT_NAME}_source_files *.cpp *.h) + +find_package(fmt REQUIRED) +find_package(nmos-cpp REQUIRED) + +add_library(${PROJECT_NAME} STATIC ${${PROJECT_NAME}_source_files}) + +target_link_libraries( + ${PROJECT_NAME} + PRIVATE project_options project_warnings + PUBLIC fmt::fmt + bisect::project_warnings + nmos-cpp::compile-settings + nmos-cpp::nmos-cpp + bisect::expected + bisect::bisect_sdp + bisect::bisect_nmoscpp +) + +set_target_properties( + ${PROJECT_NAME} + PROPERTIES CXX_EXTENSIONS NO + POSITION_INDEPENDENT_CODE ON) + +target_compile_features(${PROJECT_NAME} PUBLIC cxx_std_23) + +target_include_directories( + ${PROJECT_NAME} + PUBLIC $ + $ + $ + PRIVATE src) + +add_library(bisect::${PROJECT_NAME} ALIAS ${PROJECT_NAME}) + +install(TARGETS ${PROJECT_NAME}) +install(DIRECTORY "${CMAKE_CURRENT_LIST_DIR}/include" # source directory + DESTINATION "." # target directory + FILES_MATCHING # install only matched files + PATTERN "*.h" # select header files +) diff --git a/cpp/libs/bisect_nmoscpp/lib/include/bisect/nmoscpp/base_nmos_controller.h b/cpp/libs/bisect_nmoscpp/lib/include/bisect/nmoscpp/base_nmos_controller.h new file mode 100644 index 0000000..fbcc546 --- /dev/null +++ b/cpp/libs/bisect_nmoscpp/lib/include/bisect/nmoscpp/base_nmos_controller.h @@ -0,0 +1,33 @@ +#pragma once + +#include "bisect/nmoscpp/detail/internal.h" +#include "bisect/expected.h" +#include "bisect/sdp/settings.h" +#include "bisect/nmoscpp/detail/expected.h" +#include "bisect/nmoscpp/nmos_event_handler.h" +#include +#include +#include +#include +#include + +namespace bisect::nmoscpp +{ + class nmos_base_controller_t + { + public: + nmos_base_controller_t(nmos::experimental::log_gate& gate, web::json::value configuration, + nmos_event_handler_t* event_handler); + + nmos::experimental::log_gate& gate_; + nmos_event_handler_t* const event_handler_; + nmos::node_model node_model_; + // Must be initialized after node_model and gate + nmos::experimental::node_implementation node_implementation_; + + private: + [[nodiscard]] static maybe_ok init(nmos::node_model& node_model_, nmos::experimental::log_gate& gate_, + nmos::experimental::node_implementation& node_implementation_, + web::json::value configuration, nmos_event_handler_t* event_handler); + }; +} // namespace bisect::nmoscpp diff --git a/cpp/libs/bisect_nmoscpp/lib/include/bisect/nmoscpp/configuration.h b/cpp/libs/bisect_nmoscpp/lib/include/bisect/nmoscpp/configuration.h new file mode 100644 index 0000000..ba7dc0f --- /dev/null +++ b/cpp/libs/bisect_nmoscpp/lib/include/bisect/nmoscpp/configuration.h @@ -0,0 +1,146 @@ +#pragma once + +#include "bisect/nmoscpp/detail/internal.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace bisect::nmoscpp +{ + struct network_leg_t + { + bool rtp_enabled; + std::optional source_port; + std::optional source_ip; + std::optional destination_ip; + std::optional destination_port; + std::optional interface_name; + std::optional interface_ip; + }; + + struct network_t + { + network_leg_t primary; + std::optional secondary; + }; + + namespace capabilities + { + struct video_h265_t + { + int width; + int height; + nmos::rational exact_framerate; + std::string color_sampling; + }; + + struct video_h264_t + { + int width; + int height; + nmos::rational exact_framerate; + std::string color_sampling; + }; + + struct video_st2110_20_t + { + int width; + int height; + nmos::rational exact_framerate; + std::string color_sampling; + }; + + struct audio_st2110_30_t + { + int channels; + int bits_per_sample; + int samplerate; + }; + } // namespace capabilities + + using capabilities_t = std::variant; + + struct meta_info_t + { + std::string id; + std::string label; + std::string description; + }; + + struct nmos_receiver_t : meta_info_t + { + bool master_enable = false; + std::optional sender_id; + std::optional sdp_data; + network_t network; + std::string protocol; + nmos::format format; + std::vector media_types; + capabilities_t capabilities; + }; + + struct video_sender_info_t + { + int height = 0; + int width = 0; + nmos::rational exact_framerate; + std::string chroma_sub_sampling; + nmos::interlace_mode structure; + }; + + struct audio_sender_info_t + { + int number_of_channels = 0; + unsigned int bits_per_sample = 0; + int sampling_rate = 0; + float packet_time = 0.; + }; + + using sender_media_info_t = std::variant; + + struct source_t : meta_info_t + { + }; + + struct flow_t : meta_info_t + { + // Additional flow properties + web::json::value extra; + }; + + struct nmos_sender_t : meta_info_t + { + bool master_enable; + network_t network; + std::string protocol; + nmos::format format; + nmos::rational grain_rate; + std::string media_type; + source_t source; + flow_t flow; + sender_media_info_t media; + std::optional payload_type; + std::optional forced_sdp; + + // Additional sender properties + web::json::value extra; + }; + + struct nmos_device_t : meta_info_t + { + std::string node_id; + }; + + using sender_activation_callback_t = + std::function; + + using receiver_activation_callback_t = + std::function& sdp, const bool master_enable)>; +} // namespace bisect::nmoscpp diff --git a/cpp/libs/bisect_nmoscpp/lib/include/bisect/nmoscpp/detail/expected.h b/cpp/libs/bisect_nmoscpp/lib/include/bisect/nmoscpp/detail/expected.h new file mode 100644 index 0000000..5db4aa7 --- /dev/null +++ b/cpp/libs/bisect_nmoscpp/lib/include/bisect/nmoscpp/detail/expected.h @@ -0,0 +1,6 @@ +#pragma once + +#undef U +#include "bisect/expected/macros.h" +#include "bisect/expected/helpers.h" +#define U(X) _XPLATSTR(X) diff --git a/cpp/libs/bisect_nmoscpp/lib/include/bisect/nmoscpp/detail/internal.h b/cpp/libs/bisect_nmoscpp/lib/include/bisect/nmoscpp/detail/internal.h new file mode 100644 index 0000000..1f03c43 --- /dev/null +++ b/cpp/libs/bisect_nmoscpp/lib/include/bisect/nmoscpp/detail/internal.h @@ -0,0 +1,16 @@ +#pragma once + +////////////////////////////////////////////////////////////////////////////// +// Work around a missing forward declaration in cpprest +#include + +namespace web +{ + namespace json + { + bool operator<(const web::json::value& lhs, const web::json::value& rhs); + } +} // namespace web + +#include +////////////////////////////////////////////////////////////////////////////// diff --git a/cpp/libs/bisect_nmoscpp/lib/include/bisect/nmoscpp/logger.h b/cpp/libs/bisect_nmoscpp/lib/include/bisect/nmoscpp/logger.h new file mode 100644 index 0000000..1f4467d --- /dev/null +++ b/cpp/libs/bisect_nmoscpp/lib/include/bisect/nmoscpp/logger.h @@ -0,0 +1,26 @@ +#pragma once + +#include "bisect/nmoscpp/detail/internal.h" +#include +#include + +namespace bisect::nmoscpp +{ + class logger_t + { + public: + logger_t(); + + nmos::experimental::log_gate& gate(); + nmos::experimental::log_model& model(); + + private: + nmos::experimental::log_model model_; + std::ostream error_; + std::ostream access_; + // Before initialize gate, log_model and ostream error/access need to be initialized + nmos::experimental::log_gate gate_; + std::filebuf error_buf; + std::filebuf access_buf_; + }; +} // namespace bisect::nmoscpp diff --git a/cpp/libs/bisect_nmoscpp/lib/include/bisect/nmoscpp/nmos_controller.h b/cpp/libs/bisect_nmoscpp/lib/include/bisect/nmoscpp/nmos_controller.h new file mode 100644 index 0000000..7fdd8fb --- /dev/null +++ b/cpp/libs/bisect_nmoscpp/lib/include/bisect/nmoscpp/nmos_controller.h @@ -0,0 +1,88 @@ +#pragma once + +#include "bisect/nmoscpp/detail/internal.h" +#include "bisect/nmoscpp/base_nmos_controller.h" +#include "bisect/nmoscpp/logger.h" +#include "bisect/nmoscpp/configuration.h" +#include "bisect/expected.h" +#include +#include +#include +#include +#include +#include +#include +#include + +namespace bisect::nmoscpp +{ + class nmos_controller_t + { + public: + nmos_controller_t(logger_t& log, web::json::value configuration, nmos_event_handler_t* event_handler); + + using opt_json = std::optional; + + struct options_t + { + opt_json interfaces; + opt_json clocks; + }; + + nmos::resource make_node(const nmos::id& node_id, options_t options); + nmos::resource make_device(const nmos_device_t& device_config, const std::vector& receivers_ids, + const std::vector& senders_ids); + + nmos::resource make_receiver(const nmos::id& device_id, const nmos_receiver_t& config); + nmos::resource make_connection_receiver(const nmos::id& device_id, const nmos_receiver_t& config); + + nmos::resource make_sender(const nmos::id& device_id, const nmos_sender_t& sender_config); + nmos::resource make_connection_sender(const nmos::id& device_id, const nmos_sender_t& sender_config); + + bisect::expected make_source(const nmos::id& device_id, const nmos_sender_t& sender); + + bisect::expected make_audio_flow(const nmos::id& device_id, const nmos::id& source_id, + const flow_t& flow_config, const audio_sender_info_t& media); + bisect::expected make_video_flow(const nmos::id& device_id, const nmos::id& source_id, + const flow_t& flow_config, std::string media_type, + const video_sender_info_t& media); + + bisect::maybe_ok insert_resource(nmos::resource&& resource); + bisect::maybe_ok insert_connection_resource(nmos::resource&& resource); + bisect::maybe_ok modify_resource(const nmos::id& resource_id, std::function modifier); + bisect::maybe_ok modify_connection_resource(const nmos::id& resource_id, + std::function modifier); + [[nodiscard]] bisect::maybe_ok modify_connection_receiver(const nmos_receiver_t& config); + + bisect::expected find_resource(const nmos::id& id); + bisect::expected find_connection_resource(const nmos::id& id); + + maybe_ok erase_resource(const nmos::id& resource_id); + maybe_ok erase_connection_resource(const nmos::id& resource_id); + maybe_ok erase_device(const nmos::id& device_id); + + [[nodiscard]] maybe_ok update_transport_file(const nmos::id& sender_id); + bool has_resource(const nmos::id& id, const nmos::type& type); + std::vector get_interfaces_names(const nmos::settings& settings, bool smpte2022_7); + bisect::maybe_ok call_senders_with(const nmos::id& node_id, std::function f); + + void open(); + void close(); + + private: + nmos_base_controller_t base_controller_; + nmos::server server_; + nmos::connection_resource_auto_resolver resolve_auto_; + std::function + insert_resource_after_; + + std::function modifier)> + modify_resource_after_; + + std::function + erase_resource_after_; + }; + + using nmos_controller_uptr = std::unique_ptr; +} // namespace bisect::nmoscpp diff --git a/cpp/libs/bisect_nmoscpp/lib/include/bisect/nmoscpp/nmos_event_handler.h b/cpp/libs/bisect_nmoscpp/lib/include/bisect/nmoscpp/nmos_event_handler.h new file mode 100644 index 0000000..9c581bc --- /dev/null +++ b/cpp/libs/bisect_nmoscpp/lib/include/bisect/nmoscpp/nmos_event_handler.h @@ -0,0 +1,21 @@ +#pragma once +#include "bisect/expected.h" +#include + +namespace bisect::nmoscpp +{ + class nmos_event_handler_t + { + public: + virtual ~nmos_event_handler_t() = default; + + [[nodiscard]] virtual bisect::expected + handle_active_state_changed(const nmos::resource& resource, const nmos::resource& connection_resource, + const std::string& transport_params) = 0; + + [[nodiscard]] virtual maybe_ok handle_patch_request(const nmos::resource& resource, + const nmos::resource& connection_resource, + const std::string& endpoint_staged) = 0; + }; + +} // namespace bisect::nmoscpp diff --git a/cpp/libs/bisect_nmoscpp/lib/src/base_nmos_controller.cpp b/cpp/libs/bisect_nmoscpp/lib/src/base_nmos_controller.cpp new file mode 100644 index 0000000..86ce07c --- /dev/null +++ b/cpp/libs/bisect_nmoscpp/lib/src/base_nmos_controller.cpp @@ -0,0 +1,293 @@ +#include "utils.h" +#include "bisect/nmoscpp/base_nmos_controller.h" +#include "bisect/nmoscpp/detail/expected.h" +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace bisect; +using namespace bisect::nmoscpp; +using namespace bisect::core::detail; + +namespace +{ + // Example System API node behaviour callback to perform application-specific operations when the global + // configuration resource changes + nmos::system_global_handler make_node_implementation_system_global_handler(nmos::node_model& model, + slog::base_gate& gate) + { + // this example uses the callback to update the settings + // (an 'empty' std::function disables System API node behaviour) + return [&](const web::uri& system_uri, const web::json::value& system_global) { + if(!system_uri.is_empty()) + { + slog::log(gate, SLOG_FLF) + << nmos::stash_category(bisect::categories::node_implementation) + << "New system global configuration discovered from the System API at: " << system_uri.to_string(); + + // although this example immediately updates the settings, the effect is not propagated + // in either Registration API behaviour or the senders' /transportfile endpoints until + // an update to these is forced by other circumstances + + auto system_global_settings = nmos::parse_system_global_data(system_global).second; + web::json::merge_patch(model.settings, system_global_settings, true); + } + else + { + slog::log(gate, SLOG_FLF) + << nmos::stash_category(bisect::categories::node_implementation) + << "System global configuration is not discoverable"; + } + }; + } + + // Example Registration API node behaviour callback to perform application-specific operations when the current + // Registration API changes + nmos::registration_handler make_node_implementation_registration_handler(slog::base_gate& gate) + { + return [&](const web::uri& registration_uri) { + if(!registration_uri.is_empty()) + { + slog::log(gate, SLOG_FLF) + << nmos::stash_category(bisect::categories::node_implementation) + << "Started registered operation with Registration API at: " << registration_uri.to_string(); + } + else + { + slog::log(gate, SLOG_FLF) + << nmos::stash_category(bisect::categories::node_implementation) << "Stopped registered operation"; + } + }; + } + + // Example Connection API callback to parse "transport_file" during a PATCH /staged request + nmos::transport_file_parser make_node_implementation_transport_file_parser() + { + // this example uses the default transport file parser explicitly + // (if this callback is specified, an 'empty' std::function is not allowed) + return &nmos::parse_rtp_transport_file; + } + + // 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_event_handler_t* event_handler) + { + // 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 [event_handler](const nmos::resource& resource, const nmos::resource& connection_resource, + const web::json::value& endpoint_staged, slog::base_gate& gate) { + auto result = event_handler->handle_patch_request(resource, connection_resource, + utility::us2s(endpoint_staged.serialize())); + + if(is_error(result)) + { + throw web::json::json_exception(result.error().what()); + } + }; + } + + // Example Connection API activation callback to update senders' /transportfile endpoint - captures node_resources + // by reference! + nmos::connection_sender_transportfile_setter + make_node_implementation_transportfile_setter(const nmos::resources& node_resources, const nmos::settings& settings, + nmos_event_handler_t* event_handler) + { + using web::json::value; + + // as part of activation, the sender /transportfile should be updated based on the active transport parameters + return [&node_resources, event_handler](const nmos::resource& sender, const nmos::resource& connection_sender, + value& endpoint_transportfile) { + auto result = + build_transport_file(node_resources, event_handler, sender, connection_sender, endpoint_transportfile); + if(is_error(result)) + { + throw std::logic_error(result.error().what()); + } + }; + } + + // Example Events WebSocket API client message handler + nmos::events_ws_message_handler make_node_implementation_events_ws_message_handler(const nmos::node_model& model, + slog::base_gate& gate) + { + + // the message handler will be used for all Events WebSocket connections, and each connection may potentially + // have subscriptions to a number of sources, for multiple receivers, so this example uses a handler adaptor + // that enables simple processing of "state" messages (events) per receiver + return nmos::experimental::make_events_ws_message_handler( + model, + [&gate](const nmos::resource& receiver, const nmos::resource& connection_receiver, + const web::json::value& message) { + const auto event_type = nmos::event_type(nmos::fields::state_event_type(message)); + const auto& payload = nmos::fields::state_payload(message); + + if(nmos::is_matching_event_type(nmos::event_types::wildcard(nmos::event_types::number), event_type)) + { + const nmos::events_number value(nmos::fields::payload_number_value(payload).to_double(), + nmos::fields::payload_number_scale(payload)); + slog::log(gate, SLOG_FLF) + << nmos::stash_category(bisect::categories::node_implementation) + << "Event received: " << value.scaled_value() << " (" << event_type.name << ")"; + } + else if(nmos::is_matching_event_type(nmos::event_types::wildcard(nmos::event_types::string), + event_type)) + { + slog::log(gate, SLOG_FLF) + << nmos::stash_category(bisect::categories::node_implementation) + << "Event received: " << nmos::fields::payload_string_value(payload) << " (" << event_type.name + << ")"; + } + else if(nmos::is_matching_event_type(nmos::event_types::wildcard(nmos::event_types::boolean), + event_type)) + { + slog::log(gate, SLOG_FLF) + << nmos::stash_category(bisect::categories::node_implementation) + << "Event received: " << std::boolalpha << nmos::fields::payload_boolean_value(payload) << " (" + << event_type.name << ")"; + } + }, + gate); + } + + // Example Connection API activation callback to perform application-specific operations to complete activation + nmos::connection_activation_handler make_node_implementation_connection_activation_handler(nmos::node_model& model, + slog::base_gate& gate) + { + auto handle_load_ca_certificates = nmos::make_load_ca_certificates_handler(model.settings, gate); + // this example uses this callback to (un)subscribe a IS-07 Events WebSocket receiver when it is activated + // and, in addition to the message handler, specifies the optional close handler in order that any subsequent + // connection errors are reflected into the /active endpoint by setting master_enable to false + auto handle_events_ws_message = make_node_implementation_events_ws_message_handler(model, gate); + auto handle_close = nmos::experimental::make_events_ws_close_handler(model, gate); + auto connection_events_activation_handler = nmos::make_connection_events_websocket_activation_handler( + handle_load_ca_certificates, handle_events_ws_message, handle_close, model.settings, gate); + + return [connection_events_activation_handler, &gate](const nmos::resource& resource, + const nmos::resource& connection_resource) { + const std::pair id_type{resource.id, resource.type}; + slog::log(gate, SLOG_FLF) + << nmos::stash_category(bisect::categories::node_implementation) << "Activating " << id_type; + + connection_events_activation_handler(resource, connection_resource); + }; + } + + // Example Channel Mapping API callback to perform application-specific validation of the merged active map during a + // POST /map/activations request + nmos::details::channelmapping_output_map_validator make_node_implementation_map_validator() + { + // this example uses an 'empty' std::function because it does not need to do any validation + // beyond what is expressed by the schemas and /caps endpoints + return {}; + } + + // Example Channel Mapping API activation callback to perform application-specific operations to complete activation + nmos::channelmapping_activation_handler + make_node_implementation_channelmapping_activation_handler(slog::base_gate& gate) + { + return [&gate](const nmos::resource& channelmapping_output) { + const auto output_id = nmos::fields::channelmapping_id(channelmapping_output.data); + slog::log(gate, SLOG_FLF) + << nmos::stash_category(bisect::categories::node_implementation) << "Activating output: " << output_id; + }; + } + + // This constructs all the callbacks used to integrate the example device-specific underlying implementation + // into the server instance for the NMOS Node. + nmos::experimental::node_implementation make_node_implementation(nmos::node_model& model, slog::base_gate& gate, + nmos_event_handler_t* event_handler) + { + 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)) + .on_load_ca_certificates(nmos::make_load_ca_certificates_handler(model.settings, gate)) + .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(event_handler)) + .on_resolve_auto(make_node_implementation_auto_resolver(model.settings, event_handler)) + .on_set_transportfile( + make_node_implementation_transportfile_setter(model.node_resources, model.settings, event_handler)) + .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)); + } +} // namespace + +nmos_base_controller_t::nmos_base_controller_t(nmos::experimental::log_gate& gate, web::json::value configuration, + nmos_event_handler_t* event_handler) + : gate_(gate), event_handler_(event_handler) +{ + (void)init(node_model_, gate_, node_implementation_, configuration, event_handler); +} + +maybe_ok nmos_base_controller_t::init(nmos::node_model& node_model, nmos::experimental::log_gate& gate, + nmos::experimental::node_implementation& node_implementation, + web::json::value configuration, nmos_event_handler_t* event_handler) +{ + try + { + slog::log(gate, SLOG_FLF) << "Starting nmos-cpp node"; + + std::error_code error; + node_model.settings = configuration; + if(error || !node_model.settings.is_object()) + { + BST_FAIL("Bad command-line settings [{}]", error.message()); + } + // Prepare run-time default settings (different from header defaults) + + nmos::insert_node_default_settings(node_model.settings); + + // Log the process ID and initial settings + + slog::log(gate, SLOG_FLF) << "Process ID: " << nmos::details::get_process_id(); + slog::log(gate, SLOG_FLF) << "Build settings: " << nmos::get_build_settings_info(); + slog::log(gate, SLOG_FLF) << "Initial settings: " << node_model.settings.serialize(); + + // Set up the callbacks between the node server and the underlying implementation + + node_implementation = make_node_implementation(node_model, gate, event_handler); + + return {}; + } + catch(const web::json::json_exception& e) + { + // most likely from incorrect types in the command line settings + BST_FAIL("JSON error: {}", e.what()); + } + catch(const web::http::http_exception& e) + { + BST_FAIL("HTTP error: {} [{}]", e.what(), e.error_code().message()); + } + catch(const web::websockets::websocket_exception& e) + { + BST_FAIL("WebSocket error: {} [{}]", e.what(), e.error_code().message()); + } + catch(const std::system_error& e) + { + BST_FAIL("System error: {} [{}]", e.what(), e.code().message()); + } + catch(const std::runtime_error& e) + { + return std::unexpected(e); + } + catch(const std::exception& e) + { + BST_FAIL("Unexpected exception: {}", e.what()); + } + catch(...) + { + BST_FAIL("Unexpected unknown exception"); + } +} diff --git a/cpp/libs/bisect_nmoscpp/lib/src/logger.cpp b/cpp/libs/bisect_nmoscpp/lib/src/logger.cpp new file mode 100644 index 0000000..6c5470f --- /dev/null +++ b/cpp/libs/bisect_nmoscpp/lib/src/logger.cpp @@ -0,0 +1,43 @@ +#include "bisect/nmoscpp/logger.h" + +using namespace bisect::nmoscpp; + +logger_t::logger_t() : error_(std::cerr.rdbuf()), access_(&access_buf_), gate_(error_, access_, model_) +{ + // nmos::insert_node_default_settings(node_model.settings); + + // // copy to the logging settings + // // hmm, this is a bit icky, but simplest for now + // log_model.settings = node_model.settings; + + // // the logging level is a special case because we want to turn it into an atomic value + // // that can be read by logging statements without locking the mutex protecting the settings + // log_model.level = nmos::fields::logging_level(log_model.settings); + + // // Reconfigure the logging streams according to settings + // // (obviously, until this point, the logging gateway has its default behaviour...) + + // if(!nmos::fields::error_log(node_model.settings).empty()) + // { + // error_log_buf.open(nmos::fields::error_log(node_model.settings), std::ios_base::out | std::ios_base::app); + // auto lock = log_model.write_lock(); + // error_log.rdbuf(&error_log_buf); + // } + + // if(!nmos::fields::access_log(node_model.settings).empty()) + // { + // access_log_buf.open(nmos::fields::access_log(node_model.settings), std::ios_base::out | std::ios_base::app); + // auto lock = log_model.write_lock(); + // access_log.rdbuf(&access_log_buf); + // } +} + +nmos::experimental::log_gate& logger_t::gate() +{ + return gate_; +} + +nmos::experimental::log_model& logger_t::model() +{ + return model_; +} diff --git a/cpp/libs/bisect_nmoscpp/lib/src/nmos_controller.cpp b/cpp/libs/bisect_nmoscpp/lib/src/nmos_controller.cpp new file mode 100644 index 0000000..c77117b --- /dev/null +++ b/cpp/libs/bisect_nmoscpp/lib/src/nmos_controller.cpp @@ -0,0 +1,703 @@ +#include "bisect/nmoscpp/nmos_controller.h" +#include "bisect/nmoscpp/detail/expected.h" +#include "bisect/nmoscpp/detail/internal.h" +#include "utils.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace bisect::core::detail; +using namespace bisect::nmoscpp; +using namespace bisect; + +namespace +{ + constexpr auto delay_millis{0}; + + std::vector get_interface_names_from_network(const network_t& net) + { + std::vector names; + + if(net.primary.interface_name.has_value()) + { + names.push_back(utility::s2us(net.primary.interface_name.value())); + } + + if(net.secondary.has_value()) + { + const auto& sec = net.secondary.value(); + if(sec.interface_name.has_value()) + { + names.push_back(utility::s2us(sec.interface_name.value())); + } + } + + return names; + } + + expected do_make_source(const nmos::id& device_id, const nmos_sender_t& sender, + const nmos::settings& settings) + { + if(sender.format == nmos::formats::video) + { + const auto& video = std::get(sender.media); + const auto grain_rate = video.exact_framerate; + return nmos::make_video_source(utility::s2us(sender.source.id), device_id, nmos::clock_names::clk0, + grain_rate, settings); + } + else if(sender.format == nmos::formats::audio) + { + const auto& audio = std::get(sender.media); + const auto grain_rate = nmos::rational(audio.sampling_rate, 1); + const auto channel_count = audio.number_of_channels; + + const auto channels = boost::copy_range>( + boost::irange(0, channel_count) | boost::adaptors::transformed([&](const int& index) { + return bisect::channels_repeat[static_cast( + index % static_cast(bisect::channels_repeat.size()))]; + })); + + return nmos::make_audio_source(utility::s2us(sender.source.id), device_id, nmos::clock_names::clk0, + grain_rate, channels, settings); + } + + BST_FAIL("invalid format"); + } + + web::json::value get_default_interfaces(const nmos::settings& settings) + { + const auto host_interfaces = nmos::get_host_interfaces(settings); + const auto interfaces = nmos::experimental::node_interfaces(host_interfaces); + return nmos::make_node_interfaces(interfaces); + } + + template void set_or_previous_or_auto(web::json::value& param, const std::optional& v) + { + if(v.has_value()) + { + param = v.value(); + } + else if(param.is_null()) + { + param = web::json::value::string(utility::s2us("auto")); + } + } + + template auto map(const std::optional& v, F f) -> std::optional + { + if(v.has_value()) + { + return std::make_optional(f(v.value())); + } + + return std::nullopt; + } + + void set_transport_params(const network_leg_t& leg, web::json::value& params, bool is_receiver) + { + // Common: `source_ip`, `destination_port`, `rtp_enabled` + params[nmos::fields::rtp_enabled] = web::json::value::boolean(leg.rtp_enabled); + set_or_previous_or_auto(params[nmos::fields::source_ip], + map(leg.source_ip, [](auto v) { return web::json::value::string(utility::s2us(v)); })); + set_or_previous_or_auto(params[nmos::fields::destination_port], + map(leg.destination_port, [](auto v) { return web::json::value::number(v); })); + + if(is_receiver) + { + // Receiver: `interface_ip`, `multicast_ip` + set_or_previous_or_auto(params[nmos::fields::interface_ip], map(leg.interface_ip, [](auto v) { + return web::json::value::string(utility::s2us(v)); + })); + set_or_previous_or_auto(params[nmos::fields::multicast_ip], map(leg.destination_ip, [](auto v) { + return web::json::value::string(utility::s2us(v)); + })); + } + else + { + // Sender: `destination_ip`, `source_port` + set_or_previous_or_auto(params[nmos::fields::destination_ip], map(leg.destination_ip, [](auto v) { + return web::json::value::string(utility::s2us(v)); + })); + set_or_previous_or_auto(params[nmos::fields::source_port], + map(leg.source_port, [](auto v) { return web::json::value::number(v); })); + } + } + + void update_connection_receiver_staged_params(const nmos_receiver_t& config, web::json::value& staged) + { + if(config.sender_id.has_value()) + { + staged[nmos::fields::sender_id] = web::json::value::string(utility::s2us(config.sender_id.value())); + } + + if(config.sdp_data.has_value()) + { + auto tf = web::json::value::object(); + tf[nmos::fields::transportfile_type] = web::json::value::string(utility::s2us("application/sdp")); + tf[nmos::fields::transportfile_data] = web::json::value::string(utility::s2us(config.sdp_data.value())); + staged[nmos::fields::transport_file] = tf; + } + + staged[nmos::fields::master_enable] = web::json::value::boolean(config.master_enable); + set_transport_params(config.network.primary, staged[nmos::fields::transport_params][0], true); + if(config.network.secondary.has_value()) + { + set_transport_params(config.network.secondary.value(), staged[nmos::fields::transport_params][1], true); + } + + staged[nmos::fields::activation] = + web::json::value_of({{nmos::fields::mode, nmos::activation_modes::activate_scheduled_relative.name}, + {nmos::fields::requested_time, _XPLATSTR("0:0")}, + {nmos::fields::activation_time, nmos::make_version()}}); + } +} // namespace + +nmos_controller_t::nmos_controller_t(logger_t& logger, web::json::value configuration, + nmos_event_handler_t* event_handler) + : base_controller_(logger.gate(), configuration, event_handler), + server_(nmos::experimental::make_node_server(base_controller_.node_model_, base_controller_.node_implementation_, + logger.model(), logger.gate())) +{ + resolve_auto_ = make_node_implementation_auto_resolver(base_controller_.node_model_.settings, event_handler); + + insert_resource_after_ = [this](unsigned int milliseconds, nmos::resources& resources, + nmos::resource&& resource) -> maybe_ok { + auto lock = base_controller_.node_model_.write_lock(); + if(nmos::details::wait_for(base_controller_.node_model_.shutdown_condition, lock, + std::chrono::milliseconds(milliseconds), + [&] { return base_controller_.node_model_.shutdown; })) + { + BST_FAIL("Could not lock node model in order to write the new resources in it"); + } + + const std::pair id_type{resource.id, resource.type}; + + const auto result = nmos::insert_resource(resources, std::move(resource)).second; + if(!result) + { + slog::log(base_controller_.gate_, SLOG_FLF) << "Model update error: " << id_type; + + slog::log(base_controller_.gate_, SLOG_FLF) + << "Notifying node behaviour thread"; // and anyone else who cares... + base_controller_.node_model_.notify(); + + BST_FAIL("Error updating model with a {} resource with id {}.", utility::us2s(id_type.second.name), + utility::us2s(id_type.first)); + } + + slog::log(base_controller_.gate_, SLOG_FLF) << "Updated model with " << id_type; + slog::log(base_controller_.gate_, SLOG_FLF) + << "Notifying node behaviour thread"; // and anyone else who cares... + base_controller_.node_model_.notify(); + return {}; + }; + + modify_resource_after_ = [this](unsigned int milliseconds, nmos::resources& resources, const nmos::id& id, + std::function modifier) -> maybe_ok { + auto lock = base_controller_.node_model_.write_lock(); + if(nmos::details::wait_for(base_controller_.node_model_.shutdown_condition, lock, + std::chrono::milliseconds(milliseconds), + [&] { return base_controller_.node_model_.shutdown; })) + { + BST_FAIL("Could not lock node model in order to write the new resources in it"); + } + + auto result = nmos::modify_resource(resources, id, [modifier](nmos::resource& resource) { + modifier(resource); + resource.data[nmos::fields::version] = web::json::value(nmos::make_version()); + }); + if(!result) + { + slog::log(base_controller_.gate_, SLOG_FLF) << "Model update error: " << id; + + slog::log(base_controller_.gate_, SLOG_FLF) + << "Notifying node behaviour thread"; // and anyone else who cares... + base_controller_.node_model_.notify(); + + BST_FAIL("Error updating resource with id {}.", utility::us2s(id)); + } + + slog::log(base_controller_.gate_, SLOG_FLF) << "modified resource with id:" << id; + slog::log(base_controller_.gate_, SLOG_FLF) + << "Notifying node behaviour thread"; // and anyone else who cares... + base_controller_.node_model_.notify(); + return {}; + }; + + erase_resource_after_ = [this](unsigned int milliseconds, nmos::resources& resources, + const nmos::id& resource_id) -> maybe_ok { + auto lock = base_controller_.node_model_.write_lock(); + if(nmos::details::wait_for(base_controller_.node_model_.shutdown_condition, lock, + std::chrono::milliseconds(milliseconds), + [&] { return base_controller_.node_model_.shutdown; })) + { + BST_FAIL("Could not lock node model in order to remove resources from it"); + } + + nmos::resources::size_type resources_deleted = nmos::erase_resource(resources, resource_id, false); + nmos::resources::size_type resources_forgotten = nmos::forget_erased_resources(resources); + if(resources_deleted <= 0) + { + slog::log(base_controller_.gate_, SLOG_FLF) + << "Error deleting from model resource with id: " << resource_id; + slog::log(base_controller_.gate_, SLOG_FLF) + << "Notifying node behaviour thread"; // and anyone else who cares... + base_controller_.node_model_.notify(); + + BST_FAIL("Error deleting from model resource with id: {}", utility::us2s(resource_id)); + } + slog::log(base_controller_.gate_, SLOG_FLF) + << "Deleted " << resources_deleted << " resources"; + slog::log(base_controller_.gate_, SLOG_FLF) + << "Forgot " << resources_forgotten << " resources"; + + slog::log(base_controller_.gate_, SLOG_FLF) + << "Deleted from model resource with id " << resource_id; + slog::log(base_controller_.gate_, SLOG_FLF) + << "Notifying node behaviour thread"; // and anyone else who cares... + base_controller_.node_model_.notify(); + return {}; + }; + + if(!nmos::experimental::fields::http_trace(base_controller_.node_model_.settings)) + { + // Disable TRACE method + for(auto& http_listener : server_.http_listeners) + { + http_listener.support(web::http::methods::TRCE, [](web::http::http_request req) { + req.reply(web::http::status_codes::MethodNotAllowed); + }); + } + } +} + +void nmos_controller_t::open() +{ + server_.open().wait(); +} + +void nmos_controller_t::close() +{ + server_.close().wait(); +} + +template web::json::value get_option_or_default(const nmos_controller_t::opt_json& opt, F def) +{ + if(opt.has_value()) + { + return opt.value(); + } + + return def(); +} + +nmos::resource nmos_controller_t::make_node(const nmos::id& node_id, options_t options) +{ + + const auto clocks = get_option_or_default( + options.clocks, []() { return web::json::value_of({nmos::make_internal_clock(nmos::clock_names::clk0)}); }); + const auto interfaces = + get_option_or_default(options.interfaces, [settings = &base_controller_.node_model_.settings]() { + return get_default_interfaces(*settings); + }); + + return nmos::make_node(node_id, clocks, interfaces, base_controller_.node_model_.settings); +} + +nmos::resource nmos_controller_t::make_device(const nmos_device_t& device_config, + const std::vector& receivers_ids, + const std::vector& senders_ids) +{ + auto device = nmos::make_device(utility::s2us(device_config.id), utility::s2us(device_config.node_id), senders_ids, + receivers_ids, base_controller_.node_model_.settings); + bisect::set_label(device, device_config.label); + bisect::set_description(device, device_config.description); + + return device; +} + +nmos::resource nmos_controller_t::make_receiver(const nmos::id& device_id, const nmos_receiver_t& config) +{ + auto receiver = nmos::make_receiver(utility::s2us(config.id), device_id, nmos::transports::rtp_mcast, + get_interface_names_from_network(config.network), config.format, + config.media_types, base_controller_.node_model_.settings); + + bisect::set_label(receiver, config.label); + bisect::set_description(receiver, config.description); + + receiver.data[nmos::fields::version] = receiver.data[nmos::fields::caps][nmos::fields::version] = + web::json::value(nmos::make_version()); + return receiver; +} + +nmos::resource nmos_controller_t::make_connection_receiver(const nmos::id& device_id, const nmos_receiver_t& config) +{ + const auto is_st2022_7 = config.network.secondary.has_value(); + + auto connection_receiver = nmos::make_connection_rtp_receiver(utility::s2us(config.id), is_st2022_7); + + if(config.network.primary.interface_ip.has_value()) + { + auto v = web::json::value::array(); + web::json::push_back(v, utility::s2us(config.network.primary.interface_ip.value())); + connection_receiver.data[nmos::fields::endpoint_constraints][0][nmos::fields::interface_ip] = + web::json::value_of({{nmos::fields::constraint_enum, v}}); + } + + if(config.network.secondary.has_value() && config.network.secondary.value().interface_ip.has_value()) + { + auto v = web::json::value::array(); + web::json::push_back(v, utility::s2us(config.network.secondary.value().interface_ip.value())); + connection_receiver.data[nmos::fields::endpoint_constraints][1][nmos::fields::interface_ip] = + web::json::value_of({{nmos::fields::constraint_enum, v}}); + } + + auto& staged = connection_receiver.data[nmos::fields::endpoint_staged]; + update_connection_receiver_staged_params(config, staged); + return connection_receiver; +} + +expected nmos_controller_t::make_source(const nmos::id& device_id, const nmos_sender_t& config) +{ + BST_ASSIGN_MUT(source, do_make_source(device_id, config, base_controller_.node_model_.settings)); + bisect::set_label(source, config.source.label); + bisect::set_description(source, config.source.description); + + return source; +} + +expected nmos_controller_t::make_audio_flow(const nmos::id& device_id, const nmos::id& source_id, + const flow_t& flow_config, const audio_sender_info_t& media) +{ + auto flow = nmos::make_raw_audio_flow(utility::s2us(flow_config.id), source_id, device_id, media.sampling_rate, + media.bits_per_sample, base_controller_.node_model_.settings); + flow.data[nmos::fields::grain_rate] = nmos::make_rational(media.sampling_rate); + bisect::set_label(flow, flow_config.label); + bisect::set_description(flow, flow_config.description); + + return flow; +} + +expected nmos_controller_t::make_video_flow(const nmos::id& device_id, const nmos::id& source_id, + const flow_t& flow_config, std::string media_type, + const video_sender_info_t& media) +{ + const auto frame_width = static_cast(media.width); + const auto frame_height = static_cast(media.height); + + // TODO: receive these parameters + const auto bit_depth = 10; + const auto colorspace = nmos::colorspaces::BT709; + const auto transfer_characteristic = nmos::transfer_characteristics::SDR; + const auto color_sampling = ::sdp::samplings::YCbCr_4_2_2; + + using web::json::value; + + auto flow = nmos::make_video_flow(utility::s2us(flow_config.id), source_id, device_id, media.exact_framerate, + frame_width, frame_height, media.structure, colorspace, transfer_characteristic, + base_controller_.node_model_.settings); + auto& data = flow.data; + + data[U("media_type")] = value::string(utility::s2us(media_type)); + data[U("components")] = make_components(color_sampling, frame_width, frame_height, bit_depth); + + if(!flow_config.extra.is_null()) + { + merge_patch(flow.data, flow_config.extra, true); + }; + + bisect::set_label(flow, flow_config.label); + bisect::set_description(flow, flow_config.description); + + return flow; +} + +nmos::resource nmos_controller_t::make_sender(const nmos::id& device_id, const nmos_sender_t& sender_config) +{ + const auto interface_names = get_interface_names_from_network(sender_config.network); + + const auto manifest_url = nmos::experimental::make_manifest_api_manifest(utility::s2us(sender_config.id), + base_controller_.node_model_.settings) + .to_string(); + + // TODO: check if interface names are consistent with the ones reported by the node. Warn if otherwise. + auto sender = nmos::make_sender(utility::s2us(sender_config.id), utility::s2us(sender_config.flow.id), + nmos::transports::rtp_mcast, device_id, manifest_url, interface_names, + base_controller_.node_model_.settings); + + if(!sender_config.extra.is_null()) + { + merge_patch(sender.data, sender_config.extra, true); + }; + + bisect::set_label(sender, sender_config.label); + bisect::set_description(sender, sender_config.description); + + return sender; +} + +nmos::resource nmos_controller_t::make_connection_sender(const nmos::id& device_id, const nmos_sender_t& sender_config) +{ + const auto is_st2022_7 = sender_config.network.secondary.has_value(); + + auto connection_sender = nmos::make_connection_rtp_sender(utility::s2us(sender_config.id), is_st2022_7); + if(sender_config.network.primary.source_ip.has_value()) + { + auto v = web::json::value::array(); + web::json::push_back(v, utility::s2us(sender_config.network.primary.source_ip.value())); + connection_sender.data[nmos::fields::endpoint_constraints][0][nmos::fields::source_ip] = + web::json::value_of({{nmos::fields::constraint_enum, v}}); + } + + if(sender_config.network.secondary.has_value() && sender_config.network.secondary.value().source_ip.has_value()) + { + auto v = web::json::value::array(); + web::json::push_back(v, utility::s2us(sender_config.network.secondary.value().source_ip.value())); + connection_sender.data[nmos::fields::endpoint_constraints][1][nmos::fields::source_ip] = + web::json::value_of({{nmos::fields::constraint_enum, v}}); + } + + auto& staged = connection_sender.data[nmos::fields::endpoint_staged]; + + staged[nmos::fields::master_enable] = web::json::value::boolean(sender_config.master_enable); + set_transport_params(sender_config.network.primary, staged[nmos::fields::transport_params][0], false); + if(sender_config.network.secondary.has_value()) + { + set_transport_params(sender_config.network.secondary.value(), staged[nmos::fields::transport_params][1], false); + } + + staged[nmos::fields::activation] = + web::json::value_of({{nmos::fields::mode, nmos::activation_modes::activate_scheduled_relative.name}, + {nmos::fields::requested_time, _XPLATSTR("0:0")}, + {nmos::fields::activation_time, nmos::make_version()}}); + + return connection_sender; +} + +maybe_ok nmos_controller_t::insert_resource(nmos::resource&& resource) +{ + auto id = resource.id; + BST_CHECK(insert_resource_after_(delay_millis, base_controller_.node_model_.node_resources, std::move(resource))); + BST_CHECK(find_resource(id)); + + return {}; +} + +maybe_ok nmos_controller_t::insert_connection_resource(nmos::resource&& resource) +{ + auto id = resource.id; + BST_CHECK( + insert_resource_after_(delay_millis, base_controller_.node_model_.connection_resources, std::move(resource))); + BST_CHECK(find_resource(id)); + + return {}; +} + +maybe_ok nmos_controller_t::modify_resource(const nmos::id& resource_id, std::function modifier) +{ + return modify_resource_after_(delay_millis, base_controller_.node_model_.node_resources, resource_id, modifier); +} + +maybe_ok nmos_controller_t::modify_connection_resource(const nmos::id& resource_id, + std::function modifier) +{ + return modify_resource_after_(delay_millis, base_controller_.node_model_.connection_resources, resource_id, + modifier); +} + +maybe_ok nmos_controller_t::modify_connection_receiver(const nmos_receiver_t& config) +{ + BST_ASSIGN_MUT(connection_receiver, find_connection_resource(utility::s2us(config.id))) + + connection_receiver.data[nmos::fields::version] = web::json::value(nmos::make_version()); + connection_receiver.data[nmos::fields::endpoint_staged] = connection_receiver.data[nmos::fields::endpoint_active]; + update_connection_receiver_staged_params(config, connection_receiver.data[nmos::fields::endpoint_staged]); + + BST_CHECK(modify_connection_resource(utility::s2us(config.id), + [&](nmos::resource& resource) { resource.data = connection_receiver.data; })); + + return {}; +} + +expected nmos_controller_t::find_resource(const nmos::id& id) +{ + const auto resource_it = nmos::find_resource(base_controller_.node_model_.node_resources, id); + BST_ENFORCE(resource_it != base_controller_.node_model_.node_resources.end(), + "trying to find a non-existing NMOS resource {}", utility::us2s(id)); + return *resource_it; +} + +expected nmos_controller_t::find_connection_resource(const nmos::id& id) +{ + const auto resource_it = nmos::find_resource(base_controller_.node_model_.connection_resources, id); + BST_ENFORCE(resource_it != base_controller_.node_model_.connection_resources.end(), + "trying to find a non-existing NMOS connection resource {}", utility::us2s(id)); + return *resource_it; +} + +maybe_ok nmos_controller_t::erase_resource(const nmos::id& resource_id) +{ + + BST_CHECK(erase_resource_after_(delay_millis, base_controller_.node_model_.node_resources, resource_id)); + + const auto resource = nmos::find_resource(base_controller_.node_model_.node_resources, resource_id); + BST_ENFORCE(base_controller_.node_model_.node_resources.end() == resource, + "NMOS Resource with id {} was not deleted", utility::us2s(resource_id)); + + return {}; +} + +maybe_ok nmos_controller_t::erase_connection_resource(const nmos::id& resource_id) +{ + BST_CHECK(erase_resource_after_(delay_millis, base_controller_.node_model_.connection_resources, resource_id)); + + const auto resource = nmos::find_resource(base_controller_.node_model_.connection_resources, resource_id); + BST_ENFORCE(base_controller_.node_model_.connection_resources.end() == resource, + "NMOS Connection resource with id {} was not deleted", utility::us2s(resource_id)); + + return {}; +} + +maybe_ok nmos_controller_t::erase_device(const nmos::id& device_id) +{ + const auto device = + nmos::find_resource(base_controller_.node_model_.node_resources, {device_id, nmos::types::device}); + + std::vector maybe_result_deleting_sub_resources; + + std::transform( + device->sub_resources.begin(), device->sub_resources.end(), + std::back_inserter(maybe_result_deleting_sub_resources), + [this, resources = base_controller_.node_model_.node_resources](const nmos::id& sub_resource_id) -> maybe_ok { + const auto resource = nmos::find_resource(resources, sub_resource_id); + if(resources.end() == resource) + { + slog::log(base_controller_.gate_, SLOG_FLF) + << "Sub-resource does not exist: " << sub_resource_id; + } + if(resource->type == nmos::types::receiver || resource->type == nmos::types::sender) + { + return erase_connection_resource(resource->id); + } + return {}; + }); + + auto maybe_error = + std::find_if(maybe_result_deleting_sub_resources.begin(), maybe_result_deleting_sub_resources.end(), + [&](maybe_ok& result) { return is_error(result); }); + + if(maybe_error != std::end(maybe_result_deleting_sub_resources)) + { + return std::move(*maybe_error); + } + + BST_CHECK(erase_resource(device_id)); + + return {}; +} + +maybe_ok nmos_controller_t::update_transport_file(const nmos::id& sender_id) +{ + const auto sender_it = + nmos::find_resource(base_controller_.node_model_.node_resources, {sender_id, nmos::types::sender}); + BST_ENFORCE(sender_it == base_controller_.node_model_.node_resources.end(), + "trying to update the transport file of a non-existing NMOS sender {}", utility::us2s(sender_id)); + auto& sender = *sender_it; + + modify_connection_resource(sender_id, [this, &sender](nmos::resource& connection_sender) { + web::json::value endpoint_transportfile; + auto result = build_transport_file(base_controller_.node_model_.node_resources, base_controller_.event_handler_, + sender, connection_sender, endpoint_transportfile); + + if(is_error(result)) + { + fmt::print("ERROR updating transport file of sender {}: {}\n", utility::us2s(sender.id), + result.error().what()); + return; + } + + connection_sender.data[nmos::fields::endpoint_transportfile] = endpoint_transportfile; + }); + + return {}; +} + +bool nmos_controller_t::has_resource(const nmos::id& id, const nmos::type& type) +{ + return nmos::has_resource(base_controller_.node_model_.node_resources, {id, type}); +} + +std::vector nmos_controller_t::get_interfaces_names(const nmos::settings& settings, bool smpte2022_7) +{ + const auto host_interfaces = nmos::get_host_interfaces(settings); + const auto interfaces = nmos::experimental::node_interfaces(host_interfaces); + + // prepare interface bindings for all senders and receivers + const auto& host_address = nmos::fields::host_address(settings); + // the interface corresponding to the host address is used for the example node's WebSocket senders and + // receivers + const auto host_interface_ = bisect::find_interface(host_interfaces, host_address); + if(host_interfaces.end() == host_interface_) + { + slog::log(base_controller_.gate_, SLOG_FLF) + << "No network interface corresponding to host_address?"; + return std::vector{}; + } + + // hmm, should probably add a custom setting to control the primary and secondary interfaces for the example + // node's RTP senders and receivers rather than just picking the one(s) corresponding to the first and last of + // the specified host addresses + const auto& primary_address = settings.has_field(nmos::fields::host_addresses) + ? web::json::front(nmos::fields::host_addresses(settings)).as_string() + : host_address; + const auto& secondary_address = settings.has_field(nmos::fields::host_addresses) + ? web::json::back(nmos::fields::host_addresses(settings)).as_string() + : host_address; + const auto primary_interface_ = bisect::find_interface(host_interfaces, primary_address); + const auto secondary_interface_ = bisect::find_interface(host_interfaces, secondary_address); + if(host_interfaces.end() == primary_interface_ || host_interfaces.end() == secondary_interface_) + { + slog::log(base_controller_.gate_, SLOG_FLF) + << "No network interface corresponding to one of the host_addresses?"; + return std::vector{}; + } + const auto& primary_interface = *primary_interface_; + const auto& secondary_interface = *secondary_interface_; + const auto interface_names = smpte2022_7 + ? std::vector{primary_interface.name, secondary_interface.name} + : std::vector{primary_interface.name}; + + return interface_names; +} + +maybe_ok nmos_controller_t::call_senders_with(const nmos::id& node_id, std::function f) +{ + BST_ASSIGN_MUT(n, find_resource(node_id)); + + for(auto& device_id : n.sub_resources) + { + BST_ASSIGN_MUT(d, find_resource(device_id)); + for(auto& r_id : d.sub_resources) + { + BST_ASSIGN_MUT(r, find_resource(r_id)); + if(r.type == nmos::types::sender) + { + BST_CHECK(f(r)); + } + } + } + return {}; +} diff --git a/cpp/libs/bisect_nmoscpp/lib/src/utils.cpp b/cpp/libs/bisect_nmoscpp/lib/src/utils.cpp new file mode 100644 index 0000000..fbc6587 --- /dev/null +++ b/cpp/libs/bisect_nmoscpp/lib/src/utils.cpp @@ -0,0 +1,251 @@ +#include "utils.h" +#include "bisect/nmoscpp/detail/expected.h" +#include +#include +#include +#include +#include +#include +#include + +using namespace web::json; +using namespace bisect::core::detail; +using namespace bisect::nmoscpp; +namespace conan_sdp = sdp; + +// Example Connection API activation callback to resolve "auto" values when /staged is transitioned to /active +nmos::connection_resource_auto_resolver +bisect::nmoscpp::make_node_implementation_auto_resolver(const nmos::settings& settings, + nmos_event_handler_t* event_handler) +{ + // although which properties may need to be defaulted depends on the resource type, + // the default value will almost always be different for each resource + return [&settings, event_handler](const nmos::resource& resource, const nmos::resource& connection_resource, + value& transport_params) { + fmt::print("auto_resolver - type: {}, transport: {}, initial: {}\n", utility::us2s(resource.type.name), + utility::us2s(resource.data.at(U("transport")).as_string()), + utility::us2s(transport_params.serialize())); + + auto result = event_handler->handle_active_state_changed(resource, connection_resource, + utility::us2s(transport_params.serialize())); + + if(is_error(result)) + { + throw web::json::json_exception(result.error().what()); + } + +#if 0 + // this code relies on the specific constraints added by node_implementation_thread + const std::pair id_type{connection_resource.id, connection_resource.type}; + const auto& constraints = nmos::fields::endpoint_constraints(connection_resource.data); + // "In some cases the behaviour is more complex, and may be determined by the vendor." + // See + // https://github.com/AMWA-TV/nmos-device-connection-management/blob/v1.0/docs/2.2.%20APIs%20-%20Server%20Side%20Implementation.md#use-of-auto + if(resource.type == nmos::types::sender && + resource.data.at(U("transport")).as_string().starts_with("urn:x-nmos:transport:rtp")) + { + fmt::print("starting sender auto resolver with: {}", transport_params.serialize()); + const bool smpte2022_7 = 1 < transport_params.size(); + nmos::details::resolve_auto(transport_params[0], nmos::fields::source_ip, [&] { + return web::json::front(nmos::fields::constraint_enum(constraints.at(0).at(nmos::fields::source_ip))); + }); + if(smpte2022_7) + nmos::details::resolve_auto(transport_params[1], nmos::fields::source_ip, [&] { + return web::json::back( + nmos::fields::constraint_enum(constraints.at(1).at(nmos::fields::source_ip))); + }); + nmos::details::resolve_auto(transport_params[0], nmos::fields::destination_ip, [&] { + return value::string(bisect::make_source_specific_multicast_address_v4(id_type.first, 0)); + }); + if(smpte2022_7) + nmos::details::resolve_auto(transport_params[1], nmos::fields::destination_ip, [&] { + return value::string(bisect::make_source_specific_multicast_address_v4(id_type.first, 1)); + }); + // lastly, apply the specification defaults for any properties not handled above + nmos::resolve_rtp_auto(id_type.second, transport_params); + fmt::print("sender auto resolver completed with: {}", transport_params.serialize()); + } + else if(resource.type == nmos::types::receiver && + resource.data.at(U("transport")).as_string().starts_with("urn:x-nmos:transport:rtp")) + { + const bool smpte2022_7 = 1 < transport_params.size(); + nmos::details::resolve_auto(transport_params[0], nmos::fields::interface_ip, [&] { + return web::json::front( + nmos::fields::constraint_enum(constraints.at(0).at(nmos::fields::interface_ip))); + }); + if(smpte2022_7) + nmos::details::resolve_auto(transport_params[1], nmos::fields::interface_ip, [&] { + return web::json::back( + nmos::fields::constraint_enum(constraints.at(1).at(nmos::fields::interface_ip))); + }); + // lastly, apply the specification defaults for any properties not handled above + nmos::resolve_rtp_auto(id_type.second, transport_params); + } + else if(resource.type == nmos::types::sender && + resource.data.at(U("transport")).as_string() == "urn:x-nmos:transport:websocket") + { + const auto device = get_super_resource(resource); + if(device.second == nmos::types::device) + { + const auto ws_sender_uri = nmos::make_events_ws_api_connection_uri(device.first, settings); + + nmos::details::resolve_auto(transport_params[0], nmos::fields::connection_uri, + [&] { return value::string(ws_sender_uri.to_string()); }); + nmos::details::resolve_auto(transport_params[0], nmos::fields::connection_authorization, + [&] { return value::boolean(false); }); + } + } + else if(resource.type == nmos::types::receiver && + resource.data.at(U("transport")).as_string() == "urn:x-nmos:transport:websocket") + { + nmos::details::resolve_auto(transport_params[0], nmos::fields::connection_authorization, + [&] { return value::boolean(false); }); + } +#endif + + fmt::print("auto_resolver - final: {}\n", utility::us2s(transport_params.serialize())); + }; +} + +// find interface with the specified address +std::vector::const_iterator +bisect::find_interface(const std::vector& interfaces, + const utility::string_t& address) +{ + return boost::range::find_if(interfaces, [&](const web::hosts::experimental::host_interface& interface) { + return interface.addresses.end() != boost::range::find(interface.addresses, address); + }); +} + +// add a helpful suffix to the label of a sub-resource for the example node +void bisect::set_label_description(nmos::resource& resource, const bisect::port& port) +{ + // TODO VERIFY IF I CAN REMOVE INDEX + auto label = nmos::fields::label(resource.data); + if(!label.empty()) label += U('/'); + label += resource.type.name + U('/') + port.name; + resource.data[nmos::fields::label] = value::string(label); + + auto description = nmos::fields::description(resource.data); + if(!description.empty()) description += U('/'); + description += resource.type.name + U('/') + port.name; + resource.data[nmos::fields::description] = value::string(description); +} + +void bisect::set_label(nmos::resource& resource, const std::string& label) +{ + + resource.data[nmos::fields::label] = value::string(utility::s2us(label)); +} + +void bisect::set_description(nmos::resource& resource, const std::string& description) +{ + resource.data[nmos::fields::description] = value::string(utility::s2us(description)); +} + +// add an example "natural grouping" hint to a sender or receiver +void bisect::insert_group_hint(nmos::resource& resource, const bisect::port& port) +{ + push_back(resource.data[nmos::fields::tags][nmos::fields::group_hint], + nmos::make_group_hint({U("example"), resource.type.name + U(' ') + port.name})); +} + +nmos::interlace_mode bisect::get_interlace_mode(const nmos::settings& settings) +{ + if(settings.has_field(bisect::fields::interlace_mode)) + { + return nmos::interlace_mode{bisect::fields::interlace_mode(settings)}; + } + // for the default, 1080i50 and 1080i59.94 are arbitrarily preferred to 1080p25 and 1080p29.97 + // for 1080i formats, ST 2110-20 says that "the fields of an interlaced image are transmitted in time order, + // first field first [and] the sample rows of the temporally second field are displaced vertically 'below' the + // like-numbered sample rows of the temporally first field." + const auto frame_rate = nmos::parse_rational(bisect::fields::frame_rate(settings)); + const auto frame_height = bisect::fields::frame_height(settings); + return (nmos::rates::rate25 == frame_rate || nmos::rates::rate29_97 == frame_rate) && 1080 == frame_height + ? nmos::interlace_modes::interlaced_tff + : nmos::interlace_modes::progressive; +} + +bisect::maybe_ok bisect::nmoscpp::build_transport_file(const nmos::resources& node_resources, + nmos_event_handler_t* event_handler, + const nmos::resource& sender, + const nmos::resource& connection_sender, + web::json::value& endpoint_transportfile) +{ + const auto master_enable = + connection_sender.data.at(nmos::fields::endpoint_staged).at(nmos::fields::master_enable).as_bool(); + + if(!master_enable) + { + fmt::print("setting transportfile for {} to null\n", utility::us2s(sender.id)); + endpoint_transportfile = web::json::value::null(); + return {}; + } + + fmt::print("setting transportfile for {}\n", utility::us2s(sender.id)); + + const auto node_id = nmos::find_self_resource(node_resources)->id.c_str(); + const auto node = nmos::find_resource(node_resources, {node_id, nmos::types::node}); + + const auto flow_id = sender.data.at(U("flow_id")).as_string(); + const auto flow = nmos::find_resource(node_resources, {flow_id, nmos::types::flow}); + + if(node_resources.end() == node || node_resources.end() == flow) + { + BST_FAIL("matching IS-04 node, flow not found"); + } + + const auto source_id = flow->data.at(U("source_id")).as_string(); + auto source = nmos::find_resource(node_resources, {source_id, nmos::types::source}); + + if(node_resources.end() == source) + { + BST_FAIL("matching IS-04 source not found"); + } + + auto params = [&]() -> expected { + const std::vector mids{U("PRIMARY"), U("SECONDARY")}; + const nmos::format format{nmos::fields::format(flow->data)}; + if(nmos::formats::video == format) + { + return nmos::make_video_sdp_parameters(node->data, source->data, flow->data, sender.data, 97, mids, {}, + conan_sdp::type_parameters::type_N); + } + else if(nmos::formats::audio == format) + { + const double packet_time = nmos::fields::channels(source->data).size() > 8 ? 0.125 : 1; + return nmos::make_audio_sdp_parameters(node->data, source->data, flow->data, sender.data, + nmos::details::payload_type_audio_default, mids, {}, packet_time); + } + else if(nmos::formats::data == format) + { + return nmos::make_data_sdp_parameters(node->data, source->data, flow->data, sender.data, + nmos::details::payload_type_data_default, mids, {}, {}); + } + else if(nmos::formats::mux == format) + { + return nmos::make_mux_sdp_parameters(node->data, source->data, flow->data, sender.data, + nmos::details::payload_type_mux_default, mids, {}, + conan_sdp::type_parameters::type_N); + } + else + { + BST_FAIL("unexpected flow format"); + } + }(); + BST_ASSIGN_MUT(sdp_params, std::move(params)); + + auto& transport_params = nmos::fields::transport_params(nmos::fields::endpoint_active(connection_sender.data)); + auto session_description = nmos::make_session_description(sdp_params, transport_params); + auto txt = conan_sdp::make_session_description(session_description); + + // TODO: this is to overcome a bug that causes the video parameters not to be terminated by "; " + txt = std::regex_replace(txt, std::regex("; TP=2110TPN"), "; TP=2110TPN; "); + + auto sdp = txt; + fmt::print("transport file for {} set to: {}\n", utility::us2s(sender.id), sdp); + endpoint_transportfile = nmos::make_connection_rtp_sender_transportfile(utility::s2us(sdp)); + + return {}; +} diff --git a/cpp/libs/bisect_nmoscpp/lib/src/utils.h b/cpp/libs/bisect_nmoscpp/lib/src/utils.h new file mode 100644 index 0000000..12a0292 --- /dev/null +++ b/cpp/libs/bisect_nmoscpp/lib/src/utils.h @@ -0,0 +1,123 @@ +#include "bisect/nmoscpp/base_nmos_controller.h" +#include "bisect/nmoscpp/detail/internal.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace bisect +{ + // the different kinds of 'port' (standing for the format/media type/event type) implemented by the example node + // each 'port' of the example node has a source, flow, sender and compatible receiver + DEFINE_STRING_ENUM(port) + namespace ports + { + // video/raw + const port video{U("v")}; + // audio/L24 + const port audio{U("a")}; + // video/smpte291 + const port data{U("d")}; + // video/SMPTE2022-6 + const port mux{U("m")}; + + // example measurement event + const port temperature{U("t")}; + // example boolean event + const port burn{U("b")}; + // example string event + const port nonsense{U("s")}; + // example number/enum event + const port catcall{U("c")}; + + const std::vector rtp{video, audio, data, mux}; + const std::vector ws{temperature, burn, nonsense, catcall}; + const std::vector all{boost::copy_range>(boost::range::join(rtp, ws))}; + } // namespace ports + + nmos::id make_id(const nmos::id& seed_id, const nmos::type& type, const port& port = {}, int index = 0); + std::vector make_ids(const nmos::id& seed_id, const nmos::type& type, const port& port, int how_many = 1); + std::vector make_ids(const nmos::id& seed_id, const nmos::type& type, const std::vector& ports, + int how_many = 1); + std::vector make_ids(const nmos::id& seed_id, const std::vector& types, + const std::vector& ports, int how_many = 1); + + namespace fields + { + // how_many: provides for very basic testing of a node with many sub-resources of each type + const web::json::field_as_integer_or how_many{U("how_many"), 1}; + + // activate_senders: controls whether to activate senders on start up (true, default) or not (false) + const web::json::field_as_bool_or activate_senders{U("activate_senders"), true}; + + // frame_rate: controls the grain_rate of video, audio and ancillary data sources and flows + // and the equivalent parameter constraint on video receivers + // the value must be an object like { "numerator": 25, "denominator": 1 } + // hm, unfortunately can't use nmos::make_rational(nmos::rates::rate25) during static initialization + const web::json::field_as_value_or frame_rate{ + U("frame_rate"), web::json::value_of({{nmos::fields::numerator, 25}, {nmos::fields::denominator, 1}})}; + + // frame_width, frame_height: control the frame_width and frame_height of video flows + const web::json::field_as_integer_or frame_width{U("frame_width"), 1920}; + const web::json::field_as_integer_or frame_height{U("frame_height"), 1080}; + + // interlace_mode: controls the interlace_mode of video flows, see nmos::interlace_mode + // when omitted, a default is used based on the frame_rate, etc. + const web::json::field_as_string interlace_mode{U("interlace_mode")}; + + // channel_count: controls the number of channels in audio sources + const web::json::field_as_integer_or channel_count{U("channel_count"), 4}; + + // 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}; + } // namespace fields + + const std::vector channels_repeat{{U("Left Channel"), nmos::channel_symbols::L}, + {U("Right Channel"), nmos::channel_symbols::R}, + {U("Center Channel"), nmos::channel_symbols::C}, + {U("Low Frequency Effects Channel"), nmos::channel_symbols::LFE}}; + + namespace categories + { + const nmos::category node_implementation{"node_implementation"}; + } + + std::vector::const_iterator + find_interface(const std::vector& interfaces, + const utility::string_t& address); + + void set_label_description(nmos::resource& resource, const bisect::port& port); + // add an example "natural grouping" hint to a sender or receiver + void insert_group_hint(nmos::resource& resource, const bisect::port& port); + nmos::interlace_mode get_interlace_mode(const nmos::settings& settings); + void set_label(nmos::resource& resource, const std::string& label); + void set_description(nmos::resource& resource, const std::string& description); +} // namespace bisect + +namespace bisect::nmoscpp +{ + nmos::connection_resource_auto_resolver make_node_implementation_auto_resolver(const nmos::settings& settings, + nmos_event_handler_t* event_handler); + + [[nodiscard]] maybe_ok build_transport_file(const nmos::resources& node_resources, + nmos_event_handler_t* event_handler, const nmos::resource& sender, + const nmos::resource& connection_sender, + web::json::value& endpoint_transportfile); +} // namespace bisect::nmoscpp diff --git a/cpp/libs/bisect_sdp/CMakeLists.txt b/cpp/libs/bisect_sdp/CMakeLists.txt new file mode 100644 index 0000000..3ea7a41 --- /dev/null +++ b/cpp/libs/bisect_sdp/CMakeLists.txt @@ -0,0 +1 @@ +add_subdirectory(lib) diff --git a/cpp/libs/bisect_sdp/lib/CMakeLists.txt b/cpp/libs/bisect_sdp/lib/CMakeLists.txt new file mode 100644 index 0000000..995f2fa --- /dev/null +++ b/cpp/libs/bisect_sdp/lib/CMakeLists.txt @@ -0,0 +1,45 @@ +project(bisect_sdp LANGUAGES CXX) + +file(GLOB_RECURSE ${PROJECT_NAME}_source_files *.cpp *.h) + +find_package(nmos-cpp REQUIRED) +find_package(fmt REQUIRED) + + +add_library(${PROJECT_NAME} STATIC ${${PROJECT_NAME}_source_files}) + +target_link_libraries( + ${PROJECT_NAME} + PRIVATE + bisect::project_options bisect::project_warnings + PUBLIC + bisect::bisect_json + bisect::bisect_nmoscpp + bisect::expected + nmos-cpp::compile-settings + nmos-cpp::nmos-cpp + fmt::fmt + ) + +set_target_properties( + ${PROJECT_NAME} + PROPERTIES CXX_EXTENSIONS NO + POSITION_INDEPENDENT_CODE ON) + +target_compile_features(${PROJECT_NAME} PUBLIC cxx_std_20) + +target_include_directories( + ${PROJECT_NAME} + PUBLIC $ + $ + $ + PRIVATE lib/src) + +add_library(bisect::${PROJECT_NAME} ALIAS ${PROJECT_NAME}) + +install(TARGETS ${PROJECT_NAME}) +install(DIRECTORY "${CMAKE_CURRENT_LIST_DIR}/include" # source directory + DESTINATION "." # target directory + FILES_MATCHING # install only matched files + PATTERN "*.h" # select header files +) diff --git a/cpp/libs/bisect_sdp/lib/include/bisect/sdp.h b/cpp/libs/bisect_sdp/lib/include/bisect/sdp.h new file mode 100644 index 0000000..95e1173 --- /dev/null +++ b/cpp/libs/bisect_sdp/lib/include/bisect/sdp.h @@ -0,0 +1,4 @@ +#pragma once + +#include "bisect/sdp/builder.h" +#include "bisect/sdp/media_types.h" \ No newline at end of file diff --git a/cpp/libs/bisect_sdp/lib/include/bisect/sdp/builder.h b/cpp/libs/bisect_sdp/lib/include/bisect/sdp/builder.h new file mode 100644 index 0000000..a186153 --- /dev/null +++ b/cpp/libs/bisect_sdp/lib/include/bisect/sdp/builder.h @@ -0,0 +1,14 @@ +#pragma once + +#include "bisect/sdp/settings.h" +#include "bisect/expected/macros.h" +#include "bisect/expected.h" + +#include +#include +#include + +namespace bisect::sdp +{ + expected build_sdp(const sdp_settings_t& settings); +} // namespace bisect::sdp diff --git a/cpp/libs/bisect_sdp/lib/include/bisect/sdp/clocks.h b/cpp/libs/bisect_sdp/lib/include/bisect/sdp/clocks.h new file mode 100644 index 0000000..0d0bb62 --- /dev/null +++ b/cpp/libs/bisect_sdp/lib/include/bisect/sdp/clocks.h @@ -0,0 +1,73 @@ +#pragma once + +#include "bisect/expected/macros.h" +#include "bisect/expected.h" + +#include +#include +#include +#include + +namespace bisect::sdp +{ + + namespace ethernet + { + constexpr size_t mac_address_len = 6; + using mac_address_t = std::array; + + std::string to_string(const mac_address_t& m, char separator = ':'); + expected to_mac_address(std::string_view address); + + bool operator>(const mac_address_t& lhs, const mac_address_t& rhs); + bool operator<(const mac_address_t& lhs, const mac_address_t& rhs); + bool operator==(const mac_address_t& lhs, const mac_address_t& rhs); + bool operator!=(const mac_address_t& lhs, const mac_address_t& rhs); + bool operator>=(const mac_address_t& lhs, const mac_address_t& rhs); + bool operator<=(const mac_address_t& lhs, const mac_address_t& rhs); + } // namespace ethernet + + /// See https://www.rfc-editor.org/rfc/rfc7273.html + namespace refclks + { + /// e.g. ts-refclk:localmac=98-03-9b-8d-7e-5c + struct localmac_t + { + ethernet::mac_address_t address; + }; + + /// e.g. ts-refclk:ptp=IEEE1588-2008:ec-46-70-ff-fe-10-ff-b0:127 + struct ptp_t + { + /// e.g. "ec-46-70-ff-fe-10-ff-b0" + std::string gmid; + /// e.g. 127 + std::optional domain; + }; + + } // namespace refclks + + using refclk_t = std::variant; + + std::string to_string(const refclks::ptp_t&); + std::string to_string(const refclks::localmac_t&); + std::string to_string(const refclk_t& refclk); + + namespace mediaclks + { + struct sender_t + { + }; + + struct direct_t + { + uint64_t offset; + }; + } // namespace mediaclks + + using mediaclk_t = std::variant; + + std::string to_string(const mediaclks::sender_t&); + std::string to_string(const mediaclks::direct_t&); + std::string to_string(const mediaclk_t& mediaclk); +} // namespace bisect::sdp diff --git a/cpp/libs/bisect_sdp/lib/include/bisect/sdp/media_types.h b/cpp/libs/bisect_sdp/lib/include/bisect/sdp/media_types.h new file mode 100644 index 0000000..5ee5557 --- /dev/null +++ b/cpp/libs/bisect_sdp/lib/include/bisect/sdp/media_types.h @@ -0,0 +1,10 @@ +#pragma once + +#include + +namespace bisect::sdp::media_types +{ + constexpr std::string_view VIDEO_RAW = "video/raw"; + constexpr std::string_view VIDEO_JXSV = "video/jxsv"; + constexpr std::string_view AUDIO_L24 = "audio/L24"; +} // namespace bisect::sdp::media_types diff --git a/cpp/libs/bisect_sdp/lib/include/bisect/sdp/reader.h b/cpp/libs/bisect_sdp/lib/include/bisect/sdp/reader.h new file mode 100644 index 0000000..5038372 --- /dev/null +++ b/cpp/libs/bisect_sdp/lib/include/bisect/sdp/reader.h @@ -0,0 +1,11 @@ +#pragma once + +#include "bisect/sdp/settings.h" +#include "bisect/expected.h" + +#include + +namespace bisect::sdp +{ + expected parse_sdp(const std::string& sdp); +} // namespace bisect::sdp \ No newline at end of file diff --git a/cpp/libs/bisect_sdp/lib/include/bisect/sdp/settings.h b/cpp/libs/bisect_sdp/lib/include/bisect/sdp/settings.h new file mode 100644 index 0000000..951fe3b --- /dev/null +++ b/cpp/libs/bisect_sdp/lib/include/bisect/sdp/settings.h @@ -0,0 +1,34 @@ +#pragma once + +#include "bisect/nmoscpp/configuration.h" +#include "bisect/sdp/clocks.h" +#include +#include + +namespace bisect::sdp +{ + struct rtp_settings_t + { + uint8_t payload_type; // 96..127 + }; + + struct origin_settings_t + { + std::string description; + std::string session_id; + std::string session_version; + }; + + struct sdp_settings_t + { + using format_t = std::variant; + + format_t format; + origin_settings_t origin; + rtp_settings_t rtp; + bisect::nmoscpp::network_leg_t primary; + std::optional secondary; + refclk_t ts_refclk; + mediaclk_t mediaclk; + }; +} // namespace bisect::sdp diff --git a/cpp/libs/bisect_sdp/lib/src/builder.cpp b/cpp/libs/bisect_sdp/lib/src/builder.cpp new file mode 100644 index 0000000..88f32ef --- /dev/null +++ b/cpp/libs/bisect_sdp/lib/src/builder.cpp @@ -0,0 +1,138 @@ +#include "bisect/sdp/builder.h" +#include "bisect/nmoscpp/configuration.h" +#include "bisect/expected/match.h" +#include "bisect/sdp/clocks.h" +#include "fmt/format.h" +#include + +using namespace bisect; + +namespace +{ + std::string serialize_atribute(const sdp::mediaclk_t& mediaclk) + { + return fmt::format("a=mediaclk:{}", + match(mediaclk, overload{[&](const auto& s) { return sdp::to_string(s); }})); + } + + std::string serialize_atribute(const sdp::refclk_t& refclk) + { + return fmt::format("a=ts-refclk:{}", match(refclk, overload{[&](const auto& s) { return sdp::to_string(s); }})); + } + + std::string build_header(const sdp::sdp_settings_t& f) + { + + const auto source_ip = f.primary.source_ip.has_value() ? *f.primary.source_ip : " "; + + constexpr auto t = R"(v=0 +o=- {sess_id} {sess_version} IN IP4 {source_ip} +s={description} +t=0 0 +a=recvonly +)"; + + return fmt::format(t, fmt::arg("description", f.origin.description), fmt::arg("sess_id", f.origin.session_id), + fmt::arg("sess_version", f.origin.session_version), fmt::arg("source_ip", source_ip)); + } + + std::string build_clock_info(const sdp::sdp_settings_t& f) + { + constexpr auto clock = + R"({} +{} +)"; + return fmt::format(clock, serialize_atribute(f.mediaclk), serialize_atribute(f.ts_refclk)); + } + + std::string build_connection(const nmoscpp::network_leg_t& net) + { + constexpr auto t = R"(c=IN IP4 {destination_ip}/128 +a=source-filter: incl IN IP4 {destination_ip} {source_ip})"; + + return fmt::format( + t, fmt::arg("source_ip", net.source_ip.has_value() ? net.source_ip.value() : ""), + fmt::arg("destination_ip", net.destination_ip.has_value() ? net.destination_ip.value() : "")); + } + + expected get_video_media_type() + { + return "raw"; + } + + expected build_media(const nmoscpp::network_leg_t& net, const sdp::rtp_settings_t& rtp, + const nmoscpp::video_sender_info_t& f) + { + BST_ASSIGN(media_type, get_video_media_type()); + + const auto destination_port = net.destination_port.has_value() ? *net.destination_port : -1; + + constexpr auto t = + R"(m=video {destination_port} RTP/AVP {payload_type} +{connection} +a=rtpmap:{payload_type} {media_type}/90000 +a=fmtp:{payload_type} sampling=YCbCr-4:2:2; width={width}; height={height}; exactframerate={frame_rate};{interlace} depth=10; colorimetry={colorimetry}; PM=2110GPM; SSN=ST2110-20:2017; +)"; + + const auto frame_rate_s = fmt::format("{}/{}", f.exact_framerate.numerator(), f.exact_framerate.denominator()); + const auto interlace = f.structure == nmos::interlace_modes::progressive ? "progressive" : "interlaced"; + + return fmt::format(t, fmt::arg("media_type", media_type), fmt::arg("payload_type", rtp.payload_type), + fmt::arg("destination_port", destination_port), fmt::arg("width", f.width), + fmt::arg("height", f.height), fmt::arg("frame_rate", frame_rate_s), + fmt::arg("interlace", interlace), fmt::arg("colorimetry", f.chroma_sub_sampling), + fmt::arg("connection", build_connection(net))); + } + + expected build_media(const nmoscpp::network_leg_t& net, const sdp::rtp_settings_t& rtp, + const nmoscpp::audio_sender_info_t& f) + { + constexpr auto t = R"(m=audio {destination_port} RTP/AVP {payload_type} +a=rtpmap:{payload_type} {type}/{sample_rate}/{channel_count} +a=fmtp:{payload_type} channel-order={channel_order}; +a=ptime:{ptime} +a=maxptime:{ptime} +)"; + + const auto destination_port = net.destination_port.has_value() ? *net.destination_port : -1; + // const auto type = f.is_am824 ? "AM824" : fmt::format("L{}", as_bit_count(f.sample_format)); + const auto type = fmt::format("L{}", f.bits_per_sample); + const auto ptime_us = static_cast( + std::chrono::duration_cast(std::chrono::duration(f.packet_time)).count()); + const auto ptime = fmt::format("{:.3f}", ptime_us / 1000.0); + + return fmt::format(t, fmt::arg("payload_type", rtp.payload_type), fmt::arg("channel_order", "SMPTE2110.(U02)"), + fmt::arg("destination_port", destination_port), fmt::arg("type", type), + fmt::arg("sample_rate", f.sampling_rate), fmt::arg("channel_count", f.number_of_channels), + fmt::arg("ptime", ptime)); + } +} // namespace + +expected sdp::build_sdp(const sdp_settings_t& config) +{ + BST_ASSIGN(primary_media, + match(config.format, [&config](const auto& f) { return build_media(config.primary, config.rtp, f); })); + + if(config.secondary.has_value()) + { + BST_ASSIGN(secondary_media, match(config.format, [&config](const auto& f) { + return build_media(config.secondary.value(), config.rtp, f); + })); + constexpr auto group = "a=group:DUP primary secondary\n"; + constexpr auto primary_mid = "a=mid:primary\n"; + constexpr auto secondary_mid = "a=mid:secondary\n"; + + constexpr auto t = + R"({header}{clock_info}{group}{primary_media}{primary_mid}{secondary_media}{clock_info}{secondary_mid})"; + return fmt::format(t, fmt::arg("header", build_header(config)), fmt::arg("group", group), // done + fmt::arg("primary_media", primary_media), fmt::arg("primary_mid", primary_mid), + fmt::arg("secondary_media", secondary_media), fmt::arg("secondary_mid", secondary_mid), + fmt::arg("clock_info", build_clock_info(config))); + } + else + { + constexpr auto t = R"({header}{clock_info}{media})"; + return fmt::format(t, fmt::arg("header", build_header(config)), fmt::arg("media", primary_media), + fmt::arg("clock_info", build_clock_info(config))); + } +} diff --git a/cpp/libs/bisect_sdp/lib/src/clocks.cpp b/cpp/libs/bisect_sdp/lib/src/clocks.cpp new file mode 100644 index 0000000..8df0e5a --- /dev/null +++ b/cpp/libs/bisect_sdp/lib/src/clocks.cpp @@ -0,0 +1,125 @@ +#include "bisect/sdp/clocks.h" +#include "bisect/expected/match.h" +#include "bisect/expected/macros.h" +#include "bisect/expected.h" + +using namespace bisect::sdp; +using namespace bisect; + +std::string sdp::ethernet::to_string(const mac_address_t& m, char separator) +{ + return fmt::format("{:02x}{}{:02x}{}{:02x}{}{:02x}{}{:02x}{}{:02x}", static_cast(m[0]), separator, + static_cast(m[1]), separator, static_cast(m[2]), separator, static_cast(m[3]), + separator, static_cast(m[4]), separator, static_cast(m[5])); +} + +expected sdp::ethernet::to_mac_address(std::string_view address) +{ + BST_ENFORCE(address.size() == 17, "invalid MAC address size for: {}", address); + + ethernet::mac_address_t mac; + for(size_t i = 0; i < sizeof(ethernet::mac_address_t); ++i) + { + auto s = std::string(address.data() + i * 3, address.data() + i * 3 + 2); + size_t count = 0; + + try + { + std::byte x = static_cast(std::stoi(s, &count, 16)); + + BST_ENFORCE(count == 2, "error parsing mac address: {}", address); + mac[i] = x; + } + catch(std::exception& ex) + { + BST_FAIL("error parsing mac address '{}': {}", address, ex.what()); + } + } + + return mac; +} + +bool sdp::ethernet::operator<(const mac_address_t& lhs, const mac_address_t& rhs) +{ + uint64_t nlhs = 0, nrhs = 0, base = 1; + + for(size_t i = 0; i < mac_address_len; i++) + { + nlhs += std::to_integer(lhs[i]) * base; + nrhs += std::to_integer(rhs[i]) * base; + base *= 10; + } + + return nlhs < nrhs; +} + +bool sdp::ethernet::operator>(const mac_address_t& lhs, const mac_address_t& rhs) +{ + uint64_t nlhs = 0, nrhs = 0, base = 1; + + for(size_t i = 0; i < mac_address_len; i++) + { + nlhs += std::to_integer(lhs[i]) * base; + nrhs += std::to_integer(rhs[i]) * base; + base *= 10; + } + + return nlhs > nrhs; +} + +bool sdp::ethernet::operator==(const mac_address_t& lhs, const mac_address_t& rhs) +{ + const bool result = lhs[0] == rhs[0] && lhs[1] == rhs[1] && lhs[2] == rhs[2] && lhs[3] == rhs[3] && + lhs[4] == rhs[4] && lhs[5] == rhs[5]; + return result; +} + +bool sdp::ethernet::operator!=(const mac_address_t& lhs, const mac_address_t& rhs) +{ + const bool r = lhs[0] != rhs[0] && lhs[1] != rhs[1] && lhs[2] != rhs[2] && lhs[3] != rhs[3] && lhs[4] != rhs[4] && + lhs[5] != rhs[5]; + return r; +} + +bool sdp::ethernet::operator<=(const mac_address_t& lhs, const mac_address_t& rhs) +{ + const bool r = lhs < rhs || lhs == rhs; + return r; +} + +bool sdp::ethernet::operator>=(const mac_address_t& lhs, const mac_address_t& rhs) +{ + const bool r = lhs > rhs || lhs == rhs; + return r; +} + +std::string sdp::to_string(const refclks::localmac_t& v) +{ + return fmt::format("localmac={}", ethernet::to_string(v.address, '-')); +} + +std::string sdp::to_string(const refclks::ptp_t& v) +{ + const auto domain = v.domain.has_value() ? fmt::format(":{}", v.domain.value()) : std::string{}; + return fmt::format("ptp=IEEE1588-2008:{}{}", v.gmid, domain); +} + +std::string sdp::to_string(const refclk_t& v) +{ + return match(v, overload([&](const auto& r) { return to_string(r); })); +} + +std::string sdp::to_string(const mediaclks::sender_t&) +{ + return "sender"; +} + +std::string sdp::to_string(const mediaclks::direct_t& mediaclk) +{ + return fmt::format("direct={}", mediaclk.offset); +} + +std::string sdp::to_string(const mediaclk_t& v) +{ + return match(v, overload([&](const auto& r) { return to_string(r); })); +} diff --git a/cpp/libs/bisect_sdp/lib/src/reader.cpp b/cpp/libs/bisect_sdp/lib/src/reader.cpp new file mode 100644 index 0000000..062a8c9 --- /dev/null +++ b/cpp/libs/bisect_sdp/lib/src/reader.cpp @@ -0,0 +1,185 @@ +#include +////////////////////////////////////////////////////////////////////////////// +// Work around a missing forward declaration in cpprest +#include + +namespace web +{ + namespace json + { + bool operator<(const web::json::value& lhs, const web::json::value& rhs); + } +} // namespace web + +#include +////////////////////////////////////////////////////////////////////////////// +#include "bisect/sdp/reader.h" +#if !defined(U) +#define U(X) _XPLATSTR(X) +#endif +#include +#undef U +#include +#include "bisect/expected/macros.h" +#include "bisect/expected/helpers.h" +#include "bisect/sdp/clocks.h" +#include "bisect/json.h" +#include "bisect/nmoscpp/configuration.h" + +using namespace bisect; +using namespace bisect::sdp; +using namespace bisect::selectors; +using namespace bisect::nmoscpp; + +namespace +{ + expected to_refclk_t(const std::vector& params) + { + BST_ENFORCE(params.size() == 1, "params size is different from 1"); + if(params[0].clock_source.name == utility::conversions::to_string_t("localmac")) + { + BST_ASSIGN(mac_address, + ethernet::to_mac_address(utility::conversions::to_utf8string(params[0].mac_address))); + + return refclks::localmac_t{.address = mac_address}; + } + else if(params[0].clock_source.name == utility::conversions::to_string_t("ptp")) + { + auto ptp_server = utility::conversions::to_utf8string(params[0].ptp_server); + auto pos = ptp_server.find(":"); + if(pos == std::string::npos) + { + return refclks::ptp_t{.gmid = utility::conversions::to_utf8string(ptp_server), .domain = std::nullopt}; + } + return refclks::ptp_t{.gmid = utility::conversions::to_utf8string(ptp_server.substr(0, pos)), + .domain = static_cast(std::stoi(ptp_server.substr(pos + 1)))}; + } + BST_FAIL("Reference clock {} is invalid.", utility::conversions::to_utf8string(params[0].clock_source.name)); + } + + expected to_mediaclk_t(const nmos::sdp_parameters::mediaclk_t& params) + { + if(params.clock_source.name == utility::conversions::to_string_t("direct")) + { + return mediaclks::direct_t{.offset = static_cast(std::stoi(params.clock_parameters))}; + } + else if(params.clock_source.name == utility::conversions::to_string_t("sender")) + { + return mediaclks::sender_t{}; + } + BST_FAIL("Media clock {} is invalid.", utility::conversions::to_utf8string(params.clock_source.name)); + } + + expected to_network_settings_t(const nlohmann::json& sdp_settings) + { + auto s = network_leg_t{}; + BST_CHECK_ASSIGN(s.destination_port, select(sdp_settings, element("media_descriptions"), index(0), + element("media"), element("port"))); + + BST_CHECK_ASSIGN(s.destination_ip, select(sdp_settings, element("media_descriptions"), index(0), + element("attributes"), name_value("source-filter"), + element("destination_address"))); + + BST_CHECK_ASSIGN(s.source_ip, select(sdp_settings, element("media_descriptions"), index(0), + element("attributes"), name_value("source-filter"), + element("source_addresses"), index(0))); + + s.interface_ip = "0.0.0.0"; + + return s; + } + + expected get_raw_audio_params(const nmos::sdp_parameters& sdp_params, + const utility::string_t& encoding_name) + { + const auto params = nmos::get_audio_L_parameters(sdp_params); + unsigned int bits_per_sample = 0; + if(encoding_name == utility::conversions::to_string_t("L16")) + { + bits_per_sample = 16; + } + else if(encoding_name == utility::conversions::to_string_t("L24")) + { + bits_per_sample = 24; + } + else + { + BST_FAIL("error parsing SDP: media type audio and encoding {} not supported", + utility::conversions::to_utf8string(encoding_name)); + } + + return audio_sender_info_t{ + .number_of_channels = static_cast(params.channel_count), + .bits_per_sample = bits_per_sample, + .sampling_rate = static_cast(params.sample_rate), + .packet_time = static_cast(params.packet_time), + }; + } + + expected get_raw_video_params(const nmos::sdp_parameters& sdp_params) + { + const auto params = nmos::get_video_raw_parameters(sdp_params); + + if(params.width == 0 || params.height == 0) + { + BST_FAIL("SDP reader: Invalid width and/or height."); + } + + return video_sender_info_t{ + .height = static_cast(params.height), + .width = static_cast(params.width), + .exact_framerate = nmos::rational(params.exactframerate.numerator(), params.exactframerate.denominator()), + .chroma_sub_sampling = "", + .structure = params.interlace ? nmos::interlace_modes::interlaced_tff : nmos::interlace_modes::progressive, + }; + } +} // namespace + +// TODO: US_5727 - having more than one media type per sdp file +// TODO: US_5728 - parsing secondary/redundant network case it exists +expected bisect::sdp::parse_sdp(const std::string& sdp) +{ + try + { + const auto parsed = ::sdp::parse_session_description(sdp); + const auto sdp_parameters = nmos::get_session_description_sdp_parameters(parsed); + BST_ASSIGN(json_sdp, parse_json(utility::conversions::to_utf8string(parsed.serialize()))); + + auto s = sdp_settings_t{}; + s.rtp.payload_type = static_cast(sdp_parameters.rtpmap.payload_type); + s.origin.session_id = std::to_string(sdp_parameters.origin.session_id); + s.origin.session_version = std::to_string(sdp_parameters.origin.session_version); + s.origin.description = utility::conversions::to_utf8string(sdp_parameters.session_name); + BST_CHECK_ASSIGN(s.mediaclk, to_mediaclk_t(sdp_parameters.mediaclk)); + BST_CHECK_ASSIGN(s.ts_refclk, to_refclk_t(sdp_parameters.ts_refclk)); + BST_CHECK_ASSIGN(s.primary, to_network_settings_t(json_sdp)); + + if(sdp_parameters.media_type.name == utility::conversions::to_string_t("video") && + sdp_parameters.rtpmap.encoding_name == utility::conversions::to_string_t("raw")) + { + BST_ASSIGN(video, get_raw_video_params(sdp_parameters)); + + s.format = video; + return s; + } + + if(sdp_parameters.media_type.name == utility::conversions::to_string_t("audio")) + { + const auto params = nmos::get_audio_L_parameters(sdp_parameters); + + BST_ASSIGN(audio, get_raw_audio_params(sdp_parameters, sdp_parameters.rtpmap.encoding_name)); + + s.format = audio; + + return s; + } + + BST_FAIL("error parsing SDP: media type {} and encoding {} not supported", + utility::conversions::to_utf8string(sdp_parameters.media_type.name), + utility::conversions::to_utf8string(sdp_parameters.rtpmap.encoding_name)); + } + catch(std::exception& ex) + { + BST_FAIL("error parsing SDP: {}", ex.what()); + } +} \ No newline at end of file diff --git a/cpp/libs/ossrf_gstreamer_api/CMakeLists.txt b/cpp/libs/ossrf_gstreamer_api/CMakeLists.txt new file mode 100644 index 0000000..3ea7a41 --- /dev/null +++ b/cpp/libs/ossrf_gstreamer_api/CMakeLists.txt @@ -0,0 +1 @@ +add_subdirectory(lib) diff --git a/cpp/libs/ossrf_gstreamer_api/lib/CMakeLists.txt b/cpp/libs/ossrf_gstreamer_api/lib/CMakeLists.txt new file mode 100644 index 0000000..24cb987 --- /dev/null +++ b/cpp/libs/ossrf_gstreamer_api/lib/CMakeLists.txt @@ -0,0 +1,53 @@ +project(ossrf_gstreamer_api LANGUAGES CXX) + +file(GLOB_RECURSE ${PROJECT_NAME}_source_files *.cpp *.h) + +add_library(${PROJECT_NAME} STATIC ${${PROJECT_NAME}_source_files}) + +target_link_libraries( + ${PROJECT_NAME} + PRIVATE project_options project_warnings + PUBLIC + bisect::project_warnings + bisect::expected + bisect::bisect_gst + bisect::bisect_json +) + +set_target_properties( + ${PROJECT_NAME} + PROPERTIES CXX_EXTENSIONS NO + POSITION_INDEPENDENT_CODE ON) + +find_package(PkgConfig REQUIRED) +pkg_search_module(gstreamer REQUIRED IMPORTED_TARGET gstreamer-1.0>=1.4) +pkg_search_module(gstreamer-app REQUIRED IMPORTED_TARGET gstreamer-app-1.0>=1.4) +pkg_search_module(gstreamer-audio REQUIRED IMPORTED_TARGET gstreamer-audio-1.0>=1.4) +pkg_search_module(gstreamer-video REQUIRED IMPORTED_TARGET gstreamer-video-1.0>=1.4) + +target_link_libraries( + ${PROJECT_NAME} + PUBLIC + PkgConfig::gstreamer + PkgConfig::gstreamer-app + PkgConfig::gstreamer-audio + PkgConfig::gstreamer-video +) + +target_compile_features(${PROJECT_NAME} PUBLIC cxx_std_23) + +target_include_directories( + ${PROJECT_NAME} + PUBLIC $ + $ + $ + PRIVATE src) + +add_library(ossrf::${PROJECT_NAME} ALIAS ${PROJECT_NAME}) + +install(TARGETS ${PROJECT_NAME}) +install(DIRECTORY "${CMAKE_CURRENT_LIST_DIR}/include" # source directory + DESTINATION "." # target directory + FILES_MATCHING # install only matched files + PATTERN "*.h" # select header files +) \ No newline at end of file diff --git a/cpp/libs/ossrf_gstreamer_api/lib/include/ossrf/gstreamer/api/receiver/receiver_configuration.h b/cpp/libs/ossrf_gstreamer_api/lib/include/ossrf/gstreamer/api/receiver/receiver_configuration.h new file mode 100644 index 0000000..8b5868a --- /dev/null +++ b/cpp/libs/ossrf_gstreamer_api/lib/include/ossrf/gstreamer/api/receiver/receiver_configuration.h @@ -0,0 +1,39 @@ +#pragma once +#include +#include +#include + +namespace ossrf::gst::receiver +{ + struct network_settings_t + { + std::string interface_name; + std::string interface_address; + std::string source_ip_address; + uint16_t source_port; + }; + + struct video_info_t + { + int height; + int width; + std::string chroma_sub_sampling; + }; + + struct audio_info_t + { + int number_of_channels; + unsigned int bits_per_sample; + int sampling_rate; + float packet_time; + }; + + using format_t = std::variant; + + struct receiver_settings + { + format_t format; + network_settings_t primary; + std::optional secondary; + }; +} // namespace ossrf::gst::receiver \ No newline at end of file diff --git a/cpp/libs/ossrf_gstreamer_api/lib/include/ossrf/gstreamer/api/receiver/receiver_plugin.h b/cpp/libs/ossrf_gstreamer_api/lib/include/ossrf/gstreamer/api/receiver/receiver_plugin.h new file mode 100644 index 0000000..79dbc2f --- /dev/null +++ b/cpp/libs/ossrf_gstreamer_api/lib/include/ossrf/gstreamer/api/receiver/receiver_plugin.h @@ -0,0 +1,22 @@ +#pragma once + +#include "bisect/expected.h" +#include + +namespace ossrf::gst::plugins +{ + class gst_receiver_plugin_t; + using gst_receiver_plugin_uptr = std::unique_ptr; + + class gst_receiver_plugin_t + { + public: + virtual ~gst_receiver_plugin_t() = default; + + virtual void stop() = 0; + }; + + bisect::expected create_gst_receiver_plugin(const std::string& config, + const std::string& sdp) noexcept; + +} // namespace ossrf::gst::plugins \ No newline at end of file diff --git a/cpp/libs/ossrf_gstreamer_api/lib/include/ossrf/gstreamer/api/sender/sender_configuration.h b/cpp/libs/ossrf_gstreamer_api/lib/include/ossrf/gstreamer/api/sender/sender_configuration.h new file mode 100644 index 0000000..928fc64 --- /dev/null +++ b/cpp/libs/ossrf_gstreamer_api/lib/include/ossrf/gstreamer/api/sender/sender_configuration.h @@ -0,0 +1,57 @@ +#pragma once +#include +#include +#include + +namespace ossrf::gst::sender +{ + + struct network_settings_t + { + std::optional source_ip_address; + std::string interface_name; + std::string destination_ip_address; + uint16_t destination_port; + }; + + enum class frame_structure_t + { + progressive, + interlaced_tff, + interlaced_bff, + psf, + }; + + struct frame_rate_t + { + uint32_t num; + uint32_t den; + }; + + struct video_info_t + { + int height; + int width; + frame_rate_t exact_framerate; + std::string chroma_sub_sampling; + frame_structure_t structure; + }; + + struct audio_info_t + { + int number_of_channels; + unsigned int bits_per_sample; + int sampling_rate; + float packet_time; + }; + + using format_t = std::variant; + + struct sender_settings + { + format_t format; + network_settings_t primary; + std::optional secondary; + }; + +} // namespace ossrf::gst::sender \ No newline at end of file diff --git a/cpp/libs/ossrf_gstreamer_api/lib/include/ossrf/gstreamer/api/sender/sender_plugin.h b/cpp/libs/ossrf_gstreamer_api/lib/include/ossrf/gstreamer/api/sender/sender_plugin.h new file mode 100644 index 0000000..048a3fa --- /dev/null +++ b/cpp/libs/ossrf_gstreamer_api/lib/include/ossrf/gstreamer/api/sender/sender_plugin.h @@ -0,0 +1,21 @@ +#pragma once + +#include "bisect/expected.h" +#include + +namespace ossrf::gst::plugins +{ + class gst_sender_plugin_t; + using gst_sender_plugin_uptr = std::unique_ptr; + + class gst_sender_plugin_t + { + public: + virtual ~gst_sender_plugin_t() = default; + + virtual void stop() = 0; + }; + + bisect::expected create_gst_sender_plugin(const std::string& config, int pattern) noexcept; + +} // namespace ossrf::gst::plugins \ No newline at end of file diff --git a/cpp/libs/ossrf_gstreamer_api/lib/src/receiver/receiver_plugin.cpp b/cpp/libs/ossrf_gstreamer_api/lib/src/receiver/receiver_plugin.cpp new file mode 100644 index 0000000..ab23722 --- /dev/null +++ b/cpp/libs/ossrf_gstreamer_api/lib/src/receiver/receiver_plugin.cpp @@ -0,0 +1,97 @@ +#include "ossrf/gstreamer/api/receiver/receiver_plugin.h" +#include "ossrf/gstreamer/api/receiver/receiver_configuration.h" +#include "bisect/expected/macros.h" +#include "bisect/expected/match.h" +#include "bisect/json.h" +#include "bisect/sdp/reader.h" +#include "st2110_20_receiver_plugin.h" +#include + +using namespace bisect; +using namespace bisect::sdp; +using namespace bisect::nmoscpp; +using namespace ossrf::gst; +using namespace ossrf::gst::receiver; +using namespace ossrf::gst::plugins; +using json = nlohmann::json; + +namespace +{ + expected network_from_json(const json& config) + { + network_settings_t net; + BST_ASSIGN(primary, find(config, "primary")); + assign_if(primary, "interface_name", net, &network_settings_t::interface_name); + assign_if(primary, "interface_address", net, &network_settings_t::interface_address); + + return net; + } + + video_info_t translate_sdp_video_settings(const video_sender_info_t video_settings) + { + video_info_t info; + info.chroma_sub_sampling = video_settings.chroma_sub_sampling; + info.width = video_settings.width; + info.height = video_settings.height; + return info; + } + + expected translate_json(const json& config, sdp_settings_t sdp_settings) + { + receiver_settings s; + BST_ASSIGN(network, find(config, "network")); + BST_CHECK_ASSIGN(s.primary, network_from_json(network)); + if(sdp_settings.primary.destination_ip.has_value()) + { + s.primary.source_ip_address = sdp_settings.primary.destination_ip.value(); + } + if(sdp_settings.primary.destination_port.has_value()) + { + s.primary.source_port = static_cast(sdp_settings.primary.destination_port.value()); + } + + BST_ASSIGN(capabilities, find(config, "capabilities")); + BST_ENFORCE(capabilities.is_array(), "capabilities is not an array"); + + // TODO: Only checking the first position but should receive more capabilities in the future + const auto c = capabilities[0]; + + if(c == "video/raw" && std::holds_alternative(sdp_settings.format)) + { + auto v = std::get(sdp_settings.format); + s.format = translate_sdp_video_settings(v); + } + else if(c == "audio/raw" && std::holds_alternative(sdp_settings.format)) + { + audio_info_t info; + s.format = info; + } + else + { + BST_FAIL("invalid media type: {}", c); + } + + return s; + } + + template expected do_create_plugin(const In&, const receiver_settings&) + { + BST_FAIL("invalid format"); + } + + expected do_create_plugin(const video_info_t& format, const receiver_settings& settings) + { + return create_gst_st2110_20_plugin(settings, format); + } +} // namespace + +expected plugins::create_gst_receiver_plugin(const std::string& config, + const std::string& sdp) noexcept +{ + + BST_ASSIGN(sdp_settings, parse_sdp(sdp)); + + BST_ASSIGN(s, translate_json(json::parse(config), sdp_settings)); + + return match(s.format, overload{[&](const auto& f) { return do_create_plugin(f, s); }}); +} \ No newline at end of file diff --git a/cpp/libs/ossrf_gstreamer_api/lib/src/receiver/st2110_20_receiver_plugin.cpp b/cpp/libs/ossrf_gstreamer_api/lib/src/receiver/st2110_20_receiver_plugin.cpp new file mode 100644 index 0000000..1d5034e --- /dev/null +++ b/cpp/libs/ossrf_gstreamer_api/lib/src/receiver/st2110_20_receiver_plugin.cpp @@ -0,0 +1,92 @@ +#include "st2110_20_receiver_plugin.h" +#include "bisect/expected/macros.h" +#include "bisect/pipeline.h" +#include + +using namespace bisect; +using namespace ossrf::gst::receiver; +using namespace ossrf::gst::plugins; + +struct gst_st2110_20_receiver_impl : gst_receiver_plugin_t +{ + receiver_settings s_; + video_info_t f_; + gst::pipeline pipeline_; + + gst_st2110_20_receiver_impl(receiver_settings settings, video_info_t format) : s_(settings), f_(format) {} + + ~gst_st2110_20_receiver_impl() { stop(); } + + maybe_ok create_gstreamer_pipeline() + { + // Create pipeline and check if all elements are created successfully + BST_CHECK_ASSIGN(pipeline_, bisect::gst::pipeline::create("receiver_pipeline")); + auto* pipeline = pipeline_.get(); + + // Add pipeline udp source + auto* source = gst_element_factory_make("udpsrc", "source"); + BST_ENFORCE(source != nullptr, "Failed creating GStreamer element udpsrc"); + BST_ENFORCE(gst_bin_add(GST_BIN(pipeline), source), "Failed adding udpsrc to the pipeline"); + // Set udp source params + g_object_set(G_OBJECT(source), "address", s_.primary.source_ip_address.c_str(), NULL); + g_object_set(G_OBJECT(source), "auto-multicast", TRUE, NULL); + g_object_set(G_OBJECT(source), "port", s_.primary.source_port, NULL); + g_object_set(G_OBJECT(source), "multicast-iface", s_.primary.interface_name.c_str(), NULL); + // Create and set caps for udp source + GstCaps* caps = gst_caps_from_string( + "application/x-rtp, media=(string)video, clock-rate=(int)90000, encoding-name=(string)RAW, " + "sampling=(string)RGB, width=(string)640, height=(string)480"); + g_object_set(G_OBJECT(source), "caps", caps, NULL); + + // Add pipeline rtpjitterbuffer + auto* jitter_buffer = gst_element_factory_make("rtpjitterbuffer", "jitter_buffer"); + BST_ENFORCE(jitter_buffer != nullptr, "Failed creating GStreamer element jitter_buffer"); + BST_ENFORCE(gst_bin_add(GST_BIN(pipeline), jitter_buffer), "Failed adding jitter_buffer to the pipeline"); + + // Add pipeline queue1 + auto* queue1 = gst_element_factory_make("queue", "queue1"); + BST_ENFORCE(queue1 != nullptr, "Failed creating GStreamer element queue"); + BST_ENFORCE(gst_bin_add(GST_BIN(pipeline), queue1), "Failed adding queue to the pipeline"); + + // Add pipeline rtp depay + auto* depay = gst_element_factory_make("rtpvrawdepay", "rtp_raw_depay"); + BST_ENFORCE(depay != nullptr, "Failed creating GStreamer element depay"); + BST_ENFORCE(gst_bin_add(GST_BIN(pipeline), depay), "Failed adding depay to the pipeline"); + + // Add pipeline videoconvert + auto* videoconvert = gst_element_factory_make("videoconvert", "converter"); + BST_ENFORCE(videoconvert != nullptr, "Failed creating GStreamer element converter"); + BST_ENFORCE(gst_bin_add(GST_BIN(pipeline), videoconvert), "Failed adding converter to the pipeline"); + + // Add pipeline video sink + auto* sink = gst_element_factory_make("autovideosink", "sink"); + BST_ENFORCE(sink != nullptr, "Failed creating GStreamer element sink"); + BST_ENFORCE(gst_bin_add(GST_BIN(pipeline), sink), "Failed adding sink to the pipeline"); + + // Link elements + BST_ENFORCE(gst_element_link_many(source, jitter_buffer, queue1, depay, videoconvert, sink, NULL), + "Failed linking GStreamer video pipeline"); + + // Setup runner + pipeline_.run_loop(); + + return {}; + } + + void stop() noexcept override + { + pipeline_.stop(); + pipeline_ = {}; + } +}; + +// TODO this function will need to receive an SDP and use the information in it to build the GST pipeline +expected ossrf::gst::plugins::create_gst_st2110_20_plugin(receiver_settings settings, + video_info_t format) noexcept +{ + auto i = std::make_unique(settings, format); + + BST_CHECK(i->create_gstreamer_pipeline()); + + return i; +} \ No newline at end of file diff --git a/cpp/libs/ossrf_gstreamer_api/lib/src/receiver/st2110_20_receiver_plugin.h b/cpp/libs/ossrf_gstreamer_api/lib/src/receiver/st2110_20_receiver_plugin.h new file mode 100644 index 0000000..1fc0dea --- /dev/null +++ b/cpp/libs/ossrf_gstreamer_api/lib/src/receiver/st2110_20_receiver_plugin.h @@ -0,0 +1,10 @@ +#pragma once +#include "ossrf/gstreamer/api/receiver/receiver_plugin.h" +#include "ossrf/gstreamer/api/receiver/receiver_configuration.h" + +namespace ossrf::gst::plugins +{ + bisect::expected + create_gst_st2110_20_plugin(ossrf::gst::receiver::receiver_settings settings, + ossrf::gst::receiver::video_info_t format) noexcept; +} \ No newline at end of file diff --git a/cpp/libs/ossrf_gstreamer_api/lib/src/sender/sender_plugin.cpp b/cpp/libs/ossrf_gstreamer_api/lib/src/sender/sender_plugin.cpp new file mode 100644 index 0000000..4479504 --- /dev/null +++ b/cpp/libs/ossrf_gstreamer_api/lib/src/sender/sender_plugin.cpp @@ -0,0 +1,115 @@ +#include "ossrf/gstreamer/api/sender/sender_plugin.h" +#include "ossrf/gstreamer/api/sender/sender_configuration.h" +#include "bisect/expected/macros.h" +#include "bisect/expected/match.h" +#include "bisect/json.h" +#include "st2110_20_sender_plugin.h" +#include + +using namespace bisect; +using namespace ossrf::gst; +using namespace ossrf::gst::sender; +using namespace ossrf::gst::plugins; +using json = nlohmann::json; + +namespace +{ + expected framerate_from_json(const json& config) + { + BST_ASSIGN(numerator, find(config, "num")); + BST_ASSIGN(denominator, find(config, "den")); + return frame_rate_t{.num = numerator, .den = denominator}; + } + + expected to_interlace_mode(const std::string& s) + { + if(s == "progressive") return frame_structure_t::progressive; + if(s == "interlaced_tff") return frame_structure_t::interlaced_tff; + if(s == "interlaced_bff") return frame_structure_t::interlaced_bff; + if(s == "interlaced_psf") return frame_structure_t::psf; + BST_FAIL("invalid frame structure '{}'", s); + } + + expected video_sender_info_from_json(const json& media) + { + video_info_t info; + BST_ASSIGN(frame_rate, find(media, "frame_rate")); + BST_CHECK_ASSIGN(info.exact_framerate, framerate_from_json(frame_rate)); + BST_CHECK_ASSIGN(info.chroma_sub_sampling, find(media, "sampling")); + BST_CHECK_ASSIGN(info.width, find(media, "width")); + BST_CHECK_ASSIGN(info.height, find(media, "height")); + BST_ASSIGN(structure_s, find(media, "structure")); + BST_ASSIGN(structure, to_interlace_mode(structure_s)); + info.structure = structure; + + return info; + } + + expected audio_l24_sender_info_from_json(const json& media) + { + audio_info_t info; + BST_CHECK_ASSIGN(info.number_of_channels, find(media, "number_of_channels")); + BST_CHECK_ASSIGN(info.sampling_rate, find(media, "sampling_rate")); + BST_CHECK_ASSIGN(info.packet_time, find(media, "packet_time")); + info.bits_per_sample = 24; + return info; + } + + expected network_from_json(const json& config) + { + network_settings_t net; + BST_ASSIGN(primary, find(config, "primary")); + assign_if(primary, "destination_address", net, &network_settings_t::destination_ip_address); + assign_if(primary, "destination_port", net, &network_settings_t::destination_port); + assign_if(primary, "source_address", net, &network_settings_t::source_ip_address); + assign_if(primary, "interface_name", net, &network_settings_t::interface_name); + + return net; + } + + expected translate_json(const json& config) + { + sender_settings s; + BST_ASSIGN(network, find(config, "network")); + BST_CHECK_ASSIGN(s.primary, network_from_json(network)); + + BST_ASSIGN(media_type, find(config, "media_type")); + + BST_ASSIGN(media, find(config, "media")); + + if(media_type == "video/raw") + { + BST_CHECK_ASSIGN(s.format, video_sender_info_from_json(media)); + } + else if(media_type == "audio/raw") + { + BST_CHECK_ASSIGN(s.format, audio_l24_sender_info_from_json(media)); + } + else + { + BST_FAIL("invalid media type: {}", media_type); + } + + return s; + } + + template + expected do_create_plugin(const In&, const sender_settings&, int pattern) + { + BST_FAIL("invalid format"); + } + + expected do_create_plugin(const video_info_t& format, const sender_settings& settings, + int pattern) + { + return create_gst_st2110_20_plugin(settings, format, pattern); + } +} // namespace + +expected plugins::create_gst_sender_plugin(const std::string& config, int pattern) noexcept +{ + + BST_ASSIGN(s, translate_json(json::parse(config))); + + return match(s.format, overload{[&](const auto& f) { return do_create_plugin(f, s, pattern); }}); +} \ No newline at end of file diff --git a/cpp/libs/ossrf_gstreamer_api/lib/src/sender/st2110_20_sender_plugin.cpp b/cpp/libs/ossrf_gstreamer_api/lib/src/sender/st2110_20_sender_plugin.cpp new file mode 100644 index 0000000..4461f46 --- /dev/null +++ b/cpp/libs/ossrf_gstreamer_api/lib/src/sender/st2110_20_sender_plugin.cpp @@ -0,0 +1,94 @@ +#include "st2110_20_sender_plugin.h" +#include "bisect/expected/macros.h" +#include "bisect/pipeline.h" +#include + +using namespace bisect; +using namespace ossrf::gst::sender; +using namespace ossrf::gst::plugins; + +struct gst_st2110_20_sender_impl : gst_sender_plugin_t +{ + sender_settings s_; + video_info_t f_; + gst::pipeline pipeline_; + + gst_st2110_20_sender_impl(sender_settings settings, video_info_t format) : s_(settings), f_(format) {} + + ~gst_st2110_20_sender_impl() { stop(); } + + maybe_ok create_gstreamer_pipeline(int pattern) + { + // Create pipeline and check if all elements are created successfully + BST_CHECK_ASSIGN(pipeline_, bisect::gst::pipeline::create("sender_pipeline")); + auto* pipeline = pipeline_.get(); + + // Add pipeline videotestsrc + auto* source = gst_element_factory_make("videotestsrc", "source"); + BST_ENFORCE(source != nullptr, "Failed creating GStreamer element videotestsrc"); + g_object_set(G_OBJECT(source), "pattern", pattern, NULL); + BST_ENFORCE(gst_bin_add(GST_BIN(pipeline), source), "Failed adding videotestsrc to the pipeline"); + + // Add pipeline capsfilter + auto* capsfilter = gst_element_factory_make("capsfilter", "capsfilter"); + BST_ENFORCE(capsfilter != nullptr, "Failed creating capsfilter"); + + // Create caps for capsfilter + auto* caps = gst_caps_new_simple("video/x-raw", "format", G_TYPE_STRING, "RGB", "width", G_TYPE_INT, f_.width, + "height", G_TYPE_INT, f_.height, NULL); + BST_ENFORCE(caps != nullptr, "Failed creating GStreamer video caps"); + g_object_set(G_OBJECT(capsfilter), "caps", caps, NULL); + BST_ENFORCE(gst_bin_add(GST_BIN(pipeline), capsfilter), "Failed adding capsfilter to the pipeline"); + gst_caps_unref(caps); + + // Add pipeline queue1 + auto* queue1 = gst_element_factory_make("queue", "queue1"); + BST_ENFORCE(queue1 != nullptr, "Failed creating GStreamer element queue"); + BST_ENFORCE(gst_bin_add(GST_BIN(pipeline), queue1), "Failed adding queue to the pipeline"); + + // Add pipeline rtpvrawpay + auto* rtpvrawpay = gst_element_factory_make("rtpvrawpay", "rtpvrawpay"); + BST_ENFORCE(rtpvrawpay != nullptr, "Failed creating GStreamer element rtpvrawpay"); + BST_ENFORCE(gst_bin_add(GST_BIN(pipeline), rtpvrawpay), "Failed adding rtpvrawpay to the pipeline"); + + // Add pipeline queue2 + auto* queue2 = gst_element_factory_make("queue", "queue2"); + BST_ENFORCE(queue2 != nullptr, "Failed creating GStreamer element queue"); + BST_ENFORCE(gst_bin_add(GST_BIN(pipeline), queue2), "Failed adding queue to the pipeline"); + + // Add pipeline udpsink + auto* udpsink = gst_element_factory_make("udpsink", "udpsink"); + BST_ENFORCE(udpsink != nullptr, "Failed creating GStreamer element udpsink"); + // Set properties + g_object_set(G_OBJECT(udpsink), "host", s_.primary.destination_ip_address.c_str(), NULL); + g_object_set(G_OBJECT(udpsink), "port", s_.primary.destination_port, NULL); + g_object_set(G_OBJECT(udpsink), "auto-multicast", TRUE, NULL); + g_object_set(G_OBJECT(udpsink), "multicast-iface", s_.primary.interface_name.c_str(), NULL); + BST_ENFORCE(gst_bin_add(GST_BIN(pipeline), udpsink), "Failed adding udpsink to the pipeline"); + + // Link elements + BST_ENFORCE(gst_element_link_many(source, capsfilter, queue1, rtpvrawpay, queue2, udpsink, NULL), + "Failed linking GStreamer video pipeline"); + + // Setup runner + pipeline_.run_loop(); + + return {}; + } + + void stop() noexcept override + { + pipeline_.stop(); + pipeline_ = {}; + } +}; + +expected +ossrf::gst::plugins::create_gst_st2110_20_plugin(sender_settings settings, video_info_t format, int pattern) noexcept +{ + auto i = std::make_unique(settings, format); + + BST_CHECK(i->create_gstreamer_pipeline(pattern)); + + return i; +} \ No newline at end of file diff --git a/cpp/libs/ossrf_gstreamer_api/lib/src/sender/st2110_20_sender_plugin.h b/cpp/libs/ossrf_gstreamer_api/lib/src/sender/st2110_20_sender_plugin.h new file mode 100644 index 0000000..aca9f20 --- /dev/null +++ b/cpp/libs/ossrf_gstreamer_api/lib/src/sender/st2110_20_sender_plugin.h @@ -0,0 +1,10 @@ +#pragma once +#include "ossrf/gstreamer/api/sender/sender_plugin.h" +#include "ossrf/gstreamer/api/sender/sender_configuration.h" + +namespace ossrf::gst::plugins +{ + bisect::expected create_gst_st2110_20_plugin(ossrf::gst::sender::sender_settings settings, + ossrf::gst::sender::video_info_t format, + int pattern) noexcept; +} \ No newline at end of file diff --git a/cpp/libs/ossrf_nmos_api/CMakeLists.txt b/cpp/libs/ossrf_nmos_api/CMakeLists.txt new file mode 100644 index 0000000..3ea7a41 --- /dev/null +++ b/cpp/libs/ossrf_nmos_api/CMakeLists.txt @@ -0,0 +1 @@ +add_subdirectory(lib) diff --git a/cpp/libs/ossrf_nmos_api/lib/CMakeLists.txt b/cpp/libs/ossrf_nmos_api/lib/CMakeLists.txt new file mode 100644 index 0000000..583c8dd --- /dev/null +++ b/cpp/libs/ossrf_nmos_api/lib/CMakeLists.txt @@ -0,0 +1,38 @@ +project(ossrf_nmos_api LANGUAGES CXX) + +file(GLOB_RECURSE ${PROJECT_NAME}_source_files *.cpp *.h) + +add_library(${PROJECT_NAME} STATIC ${${PROJECT_NAME}_source_files}) + +target_link_libraries( + ${PROJECT_NAME} + PRIVATE project_options project_warnings + PUBLIC + bisect::project_warnings + bisect::expected + bisect::bisect_nmoscpp + bisect::bisect_json +) + +set_target_properties( + ${PROJECT_NAME} + PROPERTIES CXX_EXTENSIONS NO + POSITION_INDEPENDENT_CODE ON) + +target_compile_features(${PROJECT_NAME} PUBLIC cxx_std_23) + +target_include_directories( + ${PROJECT_NAME} + PUBLIC $ + $ + $ + PRIVATE src) + +add_library(ossrf::${PROJECT_NAME} ALIAS ${PROJECT_NAME}) + +install(TARGETS ${PROJECT_NAME}) +install(DIRECTORY "${CMAKE_CURRENT_LIST_DIR}/include" # source directory + DESTINATION "." # target directory + FILES_MATCHING # install only matched files + PATTERN "*.h" # select header files +) diff --git a/cpp/libs/ossrf_nmos_api/lib/include/ossrf/nmos/api/nmos.h b/cpp/libs/ossrf_nmos_api/lib/include/ossrf/nmos/api/nmos.h new file mode 100644 index 0000000..05dbaaa --- /dev/null +++ b/cpp/libs/ossrf_nmos_api/lib/include/ossrf/nmos/api/nmos.h @@ -0,0 +1,41 @@ +#pragma once +#include "bisect/nmoscpp/nmos_event_handler.h" +#include "bisect/nmoscpp/configuration.h" +#include "bisect/expected.h" +#include +#include + +namespace ossrf +{ + class nmos_t + { + public: + virtual ~nmos_t() = default; + + [[nodiscard]] virtual bisect::maybe_ok add_node(const std::string& node_configuration, + bisect::nmoscpp::nmos_event_handler_t* nmos_event_handler) = 0; + + [[nodiscard]] virtual bisect::maybe_ok add_device(const bisect::nmoscpp::nmos_device_t& config) = 0; + + [[nodiscard]] virtual bisect::maybe_ok add_receiver(const std::string& device_id, + const bisect::nmoscpp::nmos_receiver_t& config) = 0; + + [[nodiscard]] virtual bisect::maybe_ok add_sender(const std::string& device_id, + const bisect::nmoscpp::nmos_sender_t& config) = 0; + + [[nodiscard]] virtual bisect::maybe_ok modify_device(const bisect::nmoscpp::nmos_device_t& device) = 0; + + [[nodiscard]] virtual bisect::maybe_ok modify_receiver(const std::string& device_id, + const bisect::nmoscpp::nmos_receiver_t& config) = 0; + + [[nodiscard]] virtual bisect::maybe_ok modify_sender(const std::string& device_id, + const bisect::nmoscpp::nmos_sender_t& config) = 0; + + [[nodiscard]] virtual bisect::maybe_ok remove_resource(const std::string& resource_id, + const nmos::type& type) = 0; + + [[nodiscard]] virtual bisect::maybe_ok update_clocks(const std::string& clocks) = 0; + }; + + using nmos_uptr = std::unique_ptr; +} // namespace ossrf diff --git a/cpp/libs/ossrf_nmos_api/lib/include/ossrf/nmos/api/nmos_client.h b/cpp/libs/ossrf_nmos_api/lib/include/ossrf/nmos/api/nmos_client.h new file mode 100644 index 0000000..2341147 --- /dev/null +++ b/cpp/libs/ossrf_nmos_api/lib/include/ossrf/nmos/api/nmos_client.h @@ -0,0 +1,35 @@ +#pragma once +#include "bisect/expected.h" +#include "bisect/nmoscpp/configuration.h" +#include +#include +namespace ossrf +{ + class nmos_client_t; + + using nmos_client_uptr = std::unique_ptr; + + class nmos_client_t + { + public: + static bisect::expected create(const std::string& node_id, + const std::string& node_configuration) noexcept; + + ~nmos_client_t(); + + bisect::maybe_ok add_device(const std::string& config) noexcept; + + bisect::maybe_ok add_receiver(const std::string& device_id, const std::string& config, + bisect::nmoscpp::receiver_activation_callback_t callback) noexcept; + + bisect::maybe_ok add_sender(const std::string& device_id, const std::string& config, + bisect::nmoscpp::sender_activation_callback_t callback) noexcept; + + bisect::maybe_ok remove_resource(const std::string& id, const nmos::type& type) noexcept; + + private: + struct impl; + std::unique_ptr impl_; + nmos_client_t(std::unique_ptr&& i) noexcept; + }; +} // namespace ossrf diff --git a/cpp/libs/ossrf_nmos_api/lib/include/ossrf/nmos/api/nmos_impl.h b/cpp/libs/ossrf_nmos_api/lib/include/ossrf/nmos/api/nmos_impl.h new file mode 100644 index 0000000..7977956 --- /dev/null +++ b/cpp/libs/ossrf_nmos_api/lib/include/ossrf/nmos/api/nmos_impl.h @@ -0,0 +1,46 @@ +#pragma once +#include "ossrf/nmos/api/nmos.h" +#include + +namespace ossrf +{ + class nmos_impl : public nmos_t + { + + public: + static nmos_uptr create(const std::string& node_id); + + ~nmos_impl() override; + + [[nodiscard]] bisect::maybe_ok + add_node(const std::string& node_configuration, + bisect::nmoscpp::nmos_event_handler_t* nmos_event_handler) noexcept override; + + [[nodiscard]] bisect::maybe_ok add_device(const bisect::nmoscpp::nmos_device_t& config) noexcept override; + + [[nodiscard]] bisect::maybe_ok add_receiver(const std::string& device_id, + const bisect::nmoscpp::nmos_receiver_t& config) noexcept override; + + [[nodiscard]] bisect::maybe_ok add_sender(const std::string& device_id, + const bisect::nmoscpp::nmos_sender_t& config) noexcept override; + + [[nodiscard]] bisect::maybe_ok modify_device(const bisect::nmoscpp::nmos_device_t& config) noexcept override; + + [[nodiscard]] bisect::maybe_ok + modify_receiver(const std::string& device_id, const bisect::nmoscpp::nmos_receiver_t& config) noexcept override; + + [[nodiscard]] bisect::maybe_ok modify_sender(const std::string& device_id, + const bisect::nmoscpp::nmos_sender_t& config) noexcept override; + + [[nodiscard]] bisect::maybe_ok remove_resource(const std::string& resource_id, + const nmos::type& type) noexcept override; + + [[nodiscard]] bisect::maybe_ok update_clocks(const std::string& clocks) noexcept override; + + private: + struct impl; + std::unique_ptr impl_; + + nmos_impl(std::unique_ptr&& i) noexcept; + }; +}; // namespace ossrf diff --git a/cpp/libs/ossrf_nmos_api/lib/src/context/context.cpp b/cpp/libs/ossrf_nmos_api/lib/src/context/context.cpp new file mode 100644 index 0000000..e5cd932 --- /dev/null +++ b/cpp/libs/ossrf_nmos_api/lib/src/context/context.cpp @@ -0,0 +1,19 @@ +#include "context.h" + +using namespace ossrf; + +nmos_context::nmos_context(const std::string& node_id) +{ + resources_ = std::make_unique(); + nmos_api_ = nmos_impl::create(node_id); +} + +nmos_t& nmos_context::nmos() +{ + return *nmos_api_; +} + +resource_map_t& nmos_context::resources() +{ + return *resources_; +} diff --git a/cpp/libs/ossrf_nmos_api/lib/src/context/context.h b/cpp/libs/ossrf_nmos_api/lib/src/context/context.h new file mode 100644 index 0000000..f9f0f21 --- /dev/null +++ b/cpp/libs/ossrf_nmos_api/lib/src/context/context.h @@ -0,0 +1,28 @@ +#pragma once +#include "resource_map.h" +#include "ossrf/nmos/api/nmos_impl.h" + +namespace ossrf +{ + class nmos_context + { + public: + nmos_context(const std::string& node_id); + ~nmos_context() = default; + nmos_context(nmos_context&) = delete; + nmos_context(nmos_context&&) = delete; + nmos_context& operator=(nmos_context&) = delete; + nmos_context& operator=(nmos_context&&) = delete; + + nmos_t& nmos(); + resource_map_t& resources(); + + private: + resource_map_uptr resources_; + nmos_uptr nmos_api_; + }; + + using nmos_context_ptr = std::shared_ptr; + using nmos_context_uptr = std::unique_ptr; + +} // namespace ossrf diff --git a/cpp/libs/ossrf_nmos_api/lib/src/context/nmos_event_handler.cpp b/cpp/libs/ossrf_nmos_api/lib/src/context/nmos_event_handler.cpp new file mode 100644 index 0000000..423860f --- /dev/null +++ b/cpp/libs/ossrf_nmos_api/lib/src/context/nmos_event_handler.cpp @@ -0,0 +1,44 @@ +#include "nmos_event_handler.h" +#include "bisect/expected.h" +#include "bisect/expected/macros.h" +#include + +using namespace ossrf; +using namespace bisect; +using namespace bisect::nmoscpp; + +using json = nlohmann::json; + +nmos_event_handler::nmos_event_handler(nmos_context_ptr context) : context_(context) +{ +} + +expected nmos_event_handler::handle_active_state_changed(const nmos::resource& resource, + const nmos::resource& connection_resource, + const std::string& transport_params) +{ + const auto master_enable = + connection_resource.data.at(nmos::fields::endpoint_staged).at(nmos::fields::master_enable).as_bool(); + + BST_ASSIGN(r, context_->resources().find_resource(resource.id)); + auto tp = json::parse(transport_params); + // TODO: Check if there are any auto params and return the resolved params + BST_CHECK(r->handle_activation(master_enable, tp)); + + return transport_params; +} + +maybe_ok nmos_event_handler::handle_patch_request(const nmos::resource& resource, + const nmos::resource& connection_resource, + const std::string& endpoint_staged) +{ + fmt::print("handle_patch_request: {} {} {}", utility::us2s(resource.id), utility::us2s(connection_resource.id), + endpoint_staged); + + const auto master_enable = + connection_resource.data.at(nmos::fields::endpoint_staged).at(nmos::fields::master_enable).as_bool(); + + BST_ASSIGN(r, context_->resources().find_resource(resource.id)); + BST_CHECK(r->handle_patch(master_enable, json::parse(endpoint_staged))); + return {}; +} diff --git a/cpp/libs/ossrf_nmos_api/lib/src/context/nmos_event_handler.h b/cpp/libs/ossrf_nmos_api/lib/src/context/nmos_event_handler.h new file mode 100644 index 0000000..f777486 --- /dev/null +++ b/cpp/libs/ossrf_nmos_api/lib/src/context/nmos_event_handler.h @@ -0,0 +1,23 @@ +#include "bisect/expected.h" +#include "bisect/nmoscpp/nmos_event_handler.h" +#include "context.h" + +namespace ossrf +{ + class nmos_event_handler : public bisect::nmoscpp::nmos_event_handler_t + { + public: + nmos_event_handler(nmos_context_ptr context_); + + [[nodiscard]] bisect::expected + handle_active_state_changed(const nmos::resource& resource, const nmos::resource& connection_resource, + const std::string& transport_params) override; + + [[nodiscard]] bisect::maybe_ok handle_patch_request(const nmos::resource& resource, + const nmos::resource& connection_resource, + const std::string& endpoint_staged) override; + + private: + nmos_context_ptr const context_; + }; +} // namespace ossrf diff --git a/cpp/libs/ossrf_nmos_api/lib/src/context/resource_map.cpp b/cpp/libs/ossrf_nmos_api/lib/src/context/resource_map.cpp new file mode 100644 index 0000000..193d577 --- /dev/null +++ b/cpp/libs/ossrf_nmos_api/lib/src/context/resource_map.cpp @@ -0,0 +1,56 @@ +#include "resource_map.h" +#include "bisect/nmoscpp/detail/internal.h" +#include "bisect/expected/macros.h" + +using namespace ossrf; +using namespace bisect; + +void resource_map_t::insert(std::string id, nmos_resource_ptr&& entry) +{ + lock_t lock(mutex_); + auto iter = map_.find(id); + if(iter != map_.end()) + { + iter->second.push_back(entry); + } + else + { + map_.insert({id, {entry}}); + } +} + +void resource_map_t::erase(std::string id) +{ + lock_t lock(mutex_); + // Check if id is a device and removes it + auto it = map_.find(id); + if(it != map_.end()) + { + map_.erase(it); + } + // Check if id is a receiver/sender and removes it + for(auto& [device_id, entry] : map_) + { + entry.erase( + std::remove_if(entry.begin(), entry.end(), [id](const nmos_resource_ptr& r) { return r->get_id() == id; }), + entry.end()); + } +} + +expected resource_map_t::find_resource(const std::string& resource_id) +{ + lock_t lock(mutex_); + for(auto& [device_id, entry] : map_) + { + for(auto& r : entry) + { + if(r->get_id() == resource_id) + { + return r; + } + } + } + + BST_FAIL("didn't find any resource with ID {}", resource_id); + return {}; +} diff --git a/cpp/libs/ossrf_nmos_api/lib/src/context/resource_map.h b/cpp/libs/ossrf_nmos_api/lib/src/context/resource_map.h new file mode 100644 index 0000000..a1edc4a --- /dev/null +++ b/cpp/libs/ossrf_nmos_api/lib/src/context/resource_map.h @@ -0,0 +1,27 @@ +#pragma once + +#include "bisect/expected.h" +#include "resources/nmos_resource.h" +#include +#include +#include + +namespace ossrf +{ + class resource_map_t + { + std::mutex mutex_; + std::unordered_map> map_; + using lock_t = std::unique_lock; + + public: + void insert(std::string, nmos_resource_ptr&&); + void replace(std::string, nmos_resource_ptr&&); + void erase(std::string); + + bisect::expected find_resource(const std::string& resource_id); + }; + + using resource_map_ptr = std::shared_ptr; + using resource_map_uptr = std::unique_ptr; +} // namespace ossrf diff --git a/cpp/libs/ossrf_nmos_api/lib/src/nmos_client.cpp b/cpp/libs/ossrf_nmos_api/lib/src/nmos_client.cpp new file mode 100644 index 0000000..4e13d2a --- /dev/null +++ b/cpp/libs/ossrf_nmos_api/lib/src/nmos_client.cpp @@ -0,0 +1,89 @@ +#include "ossrf/nmos/api/nmos_client.h" +#include "ossrf/nmos/api/nmos_impl.h" +#include "context/context.h" +#include "context/nmos_event_handler.h" +#include "serialization/device.h" +#include "serialization/receiver.h" +#include "serialization/sender.h" +#include "resources/nmos_resource_receiver.h" +#include "resources/nmos_resource_sender.h" +#include "bisect/expected/macros.h" +#include + +using namespace bisect; +using namespace ossrf; +using json = nlohmann::json; + +struct nmos_client_t::impl +{ + std::string node_id_; + nmos_context_ptr context_; + nmos_event_handler event_handler_; +}; + +expected nmos_client_t::create(const std::string& node_id, + const std::string& node_configuration) noexcept +{ + auto context = std::make_shared(node_id); + auto event_handler = nmos_event_handler{context}; + + auto i = std::make_unique(node_id, std::move(context), std::move(event_handler)); + + BST_CHECK(i->context_->nmos().add_node(node_configuration, &i->event_handler_)); + + return nmos_client_uptr(new nmos_client_t{std::move(i)}); +} + +nmos_client_t::nmos_client_t(std::unique_ptr&& i) noexcept : impl_(std::move(i)){}; + +nmos_client_t::~nmos_client_t() +{ + // TODO: Check if deleting the node, delete every other resource + impl_->context_->resources().erase(impl_->node_id_); +}; + +maybe_ok nmos_client_t::add_device(const std::string& config) noexcept +{ + BST_ASSIGN_MUT(device_config, nmos_device_from_json(impl_->node_id_, json::parse(config))); + + BST_CHECK(impl_->context_->nmos().add_device(device_config)); + return {}; +} + +maybe_ok nmos_client_t::add_receiver(const std::string& device_id, const std::string& config, + bisect::nmoscpp::receiver_activation_callback_t callback) noexcept +{ + BST_ASSIGN_MUT(receiver_config, nmos_receiver_from_json(json::parse(config))); + + receiver_config.master_enable = true; + + BST_CHECK(impl_->context_->nmos().add_receiver(device_id, receiver_config)); + + auto r = std::make_shared(device_id, receiver_config, callback); + impl_->context_->resources().insert(receiver_config.id, std::move(r)); + + return {}; +} + +maybe_ok nmos_client_t::add_sender(const std::string& device_id, const std::string& config, + bisect::nmoscpp::sender_activation_callback_t callback) noexcept +{ + BST_ASSIGN_MUT(sender_config, nmos_sender_from_json(json::parse(config))); + + sender_config.master_enable = true; + + BST_CHECK(impl_->context_->nmos().add_sender(device_id, sender_config)); + + auto r = std::make_shared(device_id, sender_config, callback); + impl_->context_->resources().insert(sender_config.id, std::move(r)); + + return {}; +} + +maybe_ok nmos_client_t::remove_resource(const std::string& id, const nmos::type& type) noexcept +{ + BST_CHECK(impl_->context_->nmos().remove_resource(id, type)); + impl_->context_->resources().erase(id); + + return {}; +} diff --git a/cpp/libs/ossrf_nmos_api/lib/src/nmos_impl.cpp b/cpp/libs/ossrf_nmos_api/lib/src/nmos_impl.cpp new file mode 100644 index 0000000..b603f25 --- /dev/null +++ b/cpp/libs/ossrf_nmos_api/lib/src/nmos_impl.cpp @@ -0,0 +1,197 @@ +#include "ossrf/nmos/api/nmos_impl.h" +#include "bisect/nmoscpp/nmos_controller.h" +#include "bisect/nmoscpp/logger.h" +#include "utils.h" +#include +#include + +using namespace bisect; +using namespace bisect::nmoscpp; +using namespace ossrf; +using json = nlohmann::json; + +struct nmos_impl::impl +{ + nmos::id node_id_; + nmos_controller_uptr controller_; + logger_t log_; +}; + +nmos_uptr nmos_impl::create(const std::string& node_id) +{ + auto i = std::make_unique(utility::s2us(node_id)); + + return nmos_uptr(new nmos_impl{std::move(i)}); +} + +nmos_impl::nmos_impl(std::unique_ptr&& i) noexcept : impl_(std::move(i)){}; + +nmos_impl::~nmos_impl() +{ + impl_->controller_->close(); +} + +maybe_ok nmos_impl::add_node(const std::string& node_configuration, nmos_event_handler_t* nmos_event_handler) noexcept +{ + web::json::value config = web::json::value::parse(node_configuration); + + nmos_controller_t::options_t options{}; + + if(config.has_field(U("interfaces"))) + { + options.interfaces = config.at(U("interfaces")); + } + + if(config.has_field(U("clocks"))) + { + options.clocks = config.at(U("clocks")); + } + + impl_->controller_ = nmos_controller_uptr(new nmos_controller_t(impl_->log_, config, nmos_event_handler)); + auto node = impl_->controller_->make_node(impl_->node_id_, options); + BST_CHECK(impl_->controller_->insert_resource(std::move(node))); + impl_->controller_->open(); + + return {}; +} + +maybe_ok nmos_impl::add_device(const nmos_device_t& config) noexcept +{ + std::vector receiver_ids; + std::vector sender_ids; + + auto device = impl_->controller_->make_device(config, receiver_ids, sender_ids); + return impl_->controller_->insert_resource(std::move(device)); +} + +maybe_ok nmos_impl::add_receiver(const std::string& device_id, const nmos_receiver_t& config) noexcept +{ + auto receiver = impl_->controller_->make_receiver(utility::s2us(device_id), config); + BST_CHECK(impl_->controller_->insert_resource(std::move(receiver))); + + auto connection_receiver = impl_->controller_->make_connection_receiver(utility::s2us(device_id), config); + BST_CHECK(impl_->controller_->insert_connection_resource(std::move(connection_receiver))); + + return {}; +} + +maybe_ok nmos_impl::add_sender(const std::string& device_id, const nmos_sender_t& config) noexcept +{ + + BST_ASSIGN_MUT(source, impl_->controller_->make_source(utility::s2us(device_id), config)); + BST_CHECK(impl_->controller_->insert_resource(std::move(source))); + + if(std::holds_alternative(config.media)) + { + const auto& video = std::get(config.media); + BST_ASSIGN_MUT(flow, + impl_->controller_->make_video_flow(utility::s2us(device_id), utility::s2us(config.source.id), + config.flow, config.media_type, video)); + BST_CHECK(impl_->controller_->insert_resource(std::move(flow))); + } + else if(std::holds_alternative(config.media)) + { + const auto& audio = std::get(config.media); + BST_ASSIGN_MUT(flow, impl_->controller_->make_audio_flow(utility::s2us(device_id), + utility::s2us(config.source.id), config.flow, audio)); + BST_CHECK(impl_->controller_->insert_resource(std::move(flow))); + } + + auto sender = impl_->controller_->make_sender(utility::s2us(device_id), config); + BST_CHECK(impl_->controller_->insert_resource(std::move(sender))); + + auto connection_sender = impl_->controller_->make_connection_sender(utility::s2us(device_id), config); + BST_CHECK(impl_->controller_->insert_connection_resource(std::move(connection_sender))); + + return {}; +} + +maybe_ok nmos_impl::modify_device(const nmos_device_t& config) noexcept +{ + std::vector receiver_ids; + std::vector sender_ids; + + auto device_resource = impl_->controller_->make_device(config, receiver_ids, sender_ids); + return impl_->controller_->modify_resource(utility::s2us(config.id), + [&](nmos::resource& resource) { resource = device_resource; }); +} + +maybe_ok nmos_impl::modify_receiver(const std::string& device_id, const nmos_receiver_t& config) noexcept +{ + auto receiver = impl_->controller_->make_receiver(utility::s2us(device_id), config); + BST_CHECK(impl_->controller_->modify_resource(utility::s2us(config.id), + [&](nmos::resource& resource) { resource.data = receiver.data; })); + + BST_CHECK(impl_->controller_->modify_connection_receiver(config)); + + return {}; +} + +maybe_ok nmos_impl::modify_sender(const std::string& device_id, const nmos_sender_t& config) noexcept +{ + BST_ASSIGN_MUT(new_source, impl_->controller_->make_source(utility::s2us(device_id), config)); + BST_CHECK(impl_->controller_->modify_resource(new_source.id, + [&](nmos::resource& resource) { resource.data = new_source.data; })); + + if(std::holds_alternative(config.media)) + { + const auto& video = std::get(config.media); + BST_ASSIGN_MUT(new_flow, + impl_->controller_->make_video_flow(utility::s2us(device_id), utility::s2us(config.source.id), + config.flow, config.media_type, video)); + BST_CHECK(impl_->controller_->modify_resource( + new_flow.id, [&](nmos::resource& resource) { resource.data = new_flow.data; })); + } + else if(std::holds_alternative(config.media)) + { + const auto& audio = std::get(config.media); + BST_ASSIGN_MUT(new_flow, impl_->controller_->make_audio_flow( + utility::s2us(device_id), utility::s2us(config.source.id), config.flow, audio)); + BST_CHECK(impl_->controller_->modify_resource( + new_flow.id, [&](nmos::resource& resource) { resource.data = new_flow.data; })); + } + + auto new_sender = impl_->controller_->make_sender(utility::s2us(device_id), config); + BST_CHECK(impl_->controller_->modify_resource(utility::s2us(config.id), + [&](nmos::resource& resource) { resource.data = new_sender.data; })); + + auto new_connection_sender = impl_->controller_->make_connection_sender(utility::s2us(device_id), config); + BST_CHECK(impl_->controller_->modify_connection_resource( + utility::s2us(config.id), [&](nmos::resource& resource) { resource.data = new_connection_sender.data; })); + + return {}; +} + +maybe_ok nmos_impl::remove_resource(const std::string& resource_id, const nmos::type& type) noexcept +{ + BST_ENFORCE(impl_->controller_->has_resource(utility::s2us(resource_id), type), + "NMOS resource with ID {} was not found.", utility::us2s(resource_id)); + + if(type == nmos::types::device) + { + return impl_->controller_->erase_device(utility::s2us(resource_id)); + } + + BST_CHECK(impl_->controller_->erase_resource(utility::s2us(resource_id))); + + if(type == nmos::types::receiver || type == nmos::types::sender) + { + BST_CHECK(impl_->controller_->erase_connection_resource(utility::s2us(resource_id))); + } + return {}; +} + +maybe_ok nmos_impl::update_clocks(const std::string& clocks) noexcept +{ + auto j_clocks = json::parse(clocks); + + impl_->controller_->modify_resource(impl_->node_id_, [&](nmos::resource& resource) { + resource.data[utility::conversions::to_string_t("clocks")] = web::json::value::parse(j_clocks.dump()); + }); + + BST_CHECK(impl_->controller_->call_senders_with(impl_->node_id_, [&](nmos::resource& resource) -> maybe_ok { + return impl_->controller_->update_transport_file(resource.id); + })); + + return {}; +} diff --git a/cpp/libs/ossrf_nmos_api/lib/src/resources/nmos_resource.h b/cpp/libs/ossrf_nmos_api/lib/src/resources/nmos_resource.h new file mode 100644 index 0000000..213bd91 --- /dev/null +++ b/cpp/libs/ossrf_nmos_api/lib/src/resources/nmos_resource.h @@ -0,0 +1,28 @@ +#pragma once + +#include "bisect/expected.h" +#include +#include +#include +#include + +namespace ossrf +{ + class nmos_resource_t + { + public: + virtual ~nmos_resource_t() = default; + + [[nodiscard]] virtual bisect::maybe_ok handle_patch(bool master_enable, + const nlohmann::json& configuration) = 0; + [[nodiscard]] virtual bisect::maybe_ok handle_activation(bool master_enable, + nlohmann::json& transport_params) = 0; + + [[nodiscard]] virtual const std::string& get_id() const = 0; + + [[nodiscard]] virtual const std::string& get_device_id() const = 0; + }; + + using nmos_resource_ptr = std::shared_ptr; + using nmos_resource_uptr = std::unique_ptr; +} // namespace ossrf diff --git a/cpp/libs/ossrf_nmos_api/lib/src/resources/nmos_resource_receiver.cpp b/cpp/libs/ossrf_nmos_api/lib/src/resources/nmos_resource_receiver.cpp new file mode 100644 index 0000000..6554bf1 --- /dev/null +++ b/cpp/libs/ossrf_nmos_api/lib/src/resources/nmos_resource_receiver.cpp @@ -0,0 +1,106 @@ +#include "nmos_resource_receiver.h" +#include "bisect/fmt.h" +#include + +using namespace ossrf; +using namespace bisect; +using namespace bisect::nmoscpp; +using json = nlohmann::json; + +namespace +{ + std::optional get_sdp_from_configuration(const nlohmann::json& configuration) + { + const auto transport_file_it = configuration.find("transport_file"); + if(transport_file_it == configuration.end()) + { + fmt::print("NMOS Receiver: No transport_file in receiver configuration!\n"); + return std::nullopt; + } + const auto& transport_file = *transport_file_it; + + const auto type_it = transport_file.find("type"); + if(type_it == transport_file.end()) + { + fmt::print("NMOS Receiver: No type in receiver configuration!\n"); + return std::nullopt; + } + const auto& type = *type_it; + if(!type.is_string()) + { + fmt::print("NMOS Receiver: Type is not a string in receiver configuration!\n"); + return std::nullopt; + } + const auto type_value = type.get(); + constexpr auto transport_file_type_sdp = "application/sdp"; + if(type_value != transport_file_type_sdp) + { + fmt::print("NMOS Receiver: Type is not application/sdp in receiver configuration!\n"); + return std::nullopt; + } + + const auto data_it = transport_file.find("data"); + if(data_it == transport_file.end()) + { + fmt::print("NMOS Receiver: No data in receiver configuration!\n"); + return std::nullopt; + } + const auto& data = *data_it; + if(!data.is_string()) + { + fmt::print("NMOS Receiver: Data is not a string in receiver configuration!\n"); + return std::nullopt; + } + const auto data_value = data.get(); + return data; + } + +} // namespace +nmos_resource_receiver_t::nmos_resource_receiver_t(const std::string& device_id, const nmos_receiver_t& config, + receiver_activation_callback_t callback) + : config_(config), activation_callback_(callback), device_id_(device_id) +{ +} + +const std::string& nmos_resource_receiver_t::get_device_id() const +{ + return device_id_; +} + +const std::string& nmos_resource_receiver_t::get_id() const +{ + return config_.id; +} + +maybe_ok nmos_resource_receiver_t::handle_patch(bool master_enable, const json& configuration) +{ + if(master_enable) + { + sdp_ = get_sdp_from_configuration(configuration); + } + else + { + sdp_ = std::nullopt; + } + + return {}; +} + +maybe_ok nmos_resource_receiver_t::handle_activation(bool master_enable, json& transport_params) +{ + // TODO: Resolve auto params + if(master_enable && sdp_.has_value()) + { + fmt::print("receiver {}::handle_activation - callback with active SDP: {}\n", config_.id, sdp_.value()); + } + else + { + fmt::print("receiver {}::handle_activation - callback with disabled\n", config_.id); + } + + activation_callback_(sdp_, master_enable); + + master_enable_ = master_enable; + + return {}; +} diff --git a/cpp/libs/ossrf_nmos_api/lib/src/resources/nmos_resource_receiver.h b/cpp/libs/ossrf_nmos_api/lib/src/resources/nmos_resource_receiver.h new file mode 100644 index 0000000..dfbed06 --- /dev/null +++ b/cpp/libs/ossrf_nmos_api/lib/src/resources/nmos_resource_receiver.h @@ -0,0 +1,32 @@ +#pragma once + +#include "nmos_resource.h" +#include "bisect/nmoscpp/configuration.h" +#include +#include + +namespace ossrf +{ + class nmos_resource_receiver_t : public nmos_resource_t + { + public: + nmos_resource_receiver_t(const std::string& device_id, const bisect::nmoscpp::nmos_receiver_t& config, + bisect::nmoscpp::receiver_activation_callback_t callback); + + bisect::maybe_ok handle_activation(bool master_enable, nlohmann::json& transport_params) override; + bisect::maybe_ok handle_patch(bool master_enable, const nlohmann::json& configuration) override; + + const std::string& get_id() const override; + + const std::string& get_device_id() const override; + + private: + const bisect::nmoscpp::nmos_receiver_t config_; + bisect::nmoscpp::receiver_activation_callback_t activation_callback_; + bool master_enable_ = true; + std::optional sdp_; + std::string device_id_; + }; + + using nmos_receiver_ptr = std::shared_ptr; +} // namespace ossrf diff --git a/cpp/libs/ossrf_nmos_api/lib/src/resources/nmos_resource_sender.cpp b/cpp/libs/ossrf_nmos_api/lib/src/resources/nmos_resource_sender.cpp new file mode 100644 index 0000000..e8e39fe --- /dev/null +++ b/cpp/libs/ossrf_nmos_api/lib/src/resources/nmos_resource_sender.cpp @@ -0,0 +1,41 @@ +#include "nmos_resource_sender.h" +#include "bisect/sdp/media_types.h" +#include "../serialization/sender.h" +#include + +using namespace ossrf; +using namespace bisect; +using namespace bisect::nmoscpp; +using json = nlohmann::json; + +nmos_resource_sender_t::nmos_resource_sender_t(const std::string& device_id, const nmos_sender_t& config, + sender_activation_callback_t callback) + : config_(config), activation_callback_(callback), device_id_(device_id) +{ +} + +const std::string& nmos_resource_sender_t::get_device_id() const +{ + return device_id_; +} + +const std::string& nmos_resource_sender_t::get_id() const +{ + return config_.id; +} + +maybe_ok nmos_resource_sender_t::handle_activation(bool master_enable, json& transport_params) +{ + + // TODO: Resolve auto params + activation_callback_(master_enable, transport_params); + + master_enable_ = master_enable; + + return {}; +} + +maybe_ok nmos_resource_sender_t::handle_patch(bool master_enable, const json& configuration) +{ + return {}; +} diff --git a/cpp/libs/ossrf_nmos_api/lib/src/resources/nmos_resource_sender.h b/cpp/libs/ossrf_nmos_api/lib/src/resources/nmos_resource_sender.h new file mode 100644 index 0000000..d12e90e --- /dev/null +++ b/cpp/libs/ossrf_nmos_api/lib/src/resources/nmos_resource_sender.h @@ -0,0 +1,32 @@ +#pragma once + +#include "nmos_resource.h" +#include "bisect/nmoscpp/configuration.h" +#include +#include + +namespace ossrf +{ + class nmos_resource_sender_t : public nmos_resource_t + { + public: + nmos_resource_sender_t(const std::string& device_id, const bisect::nmoscpp::nmos_sender_t& config, + bisect::nmoscpp::sender_activation_callback_t callback); + + bisect::maybe_ok handle_activation(bool master_enable, nlohmann::json& transport_params) override; + bisect::maybe_ok handle_patch(bool master_enable, const nlohmann::json& configuration) override; + + const std::string& get_id() const override; + + const std::string& get_device_id() const override; + + private: + const bisect::nmoscpp::nmos_sender_t config_; + bisect::nmoscpp::sender_activation_callback_t activation_callback_; + bool master_enable_ = true; + std::optional sdp_; + std::string device_id_; + }; + + using nmos_sender_ptr = std::shared_ptr; +} // namespace ossrf diff --git a/cpp/libs/ossrf_nmos_api/lib/src/serialization/device.cpp b/cpp/libs/ossrf_nmos_api/lib/src/serialization/device.cpp new file mode 100644 index 0000000..3a5b215 --- /dev/null +++ b/cpp/libs/ossrf_nmos_api/lib/src/serialization/device.cpp @@ -0,0 +1,24 @@ +#include "serialization/device.h" +#include "serialization/receiver.h" +#include "serialization/sender.h" +#include "bisect/expected/helpers.h" +#include "bisect/json/json.h" +#include + +using namespace ossrf; +using namespace bisect; +using namespace bisect::nmoscpp; + +using nlohmann::json; + +expected ossrf::nmos_device_from_json(const nmos::id node_id, const nlohmann::json& device_config) +{ + nmos_device_t device{}; + device.node_id = node_id; + + BST_CHECK_ASSIGN(device.id, find(device_config, "id")); + BST_CHECK_ASSIGN(device.label, find(device_config, "label")); + BST_CHECK_ASSIGN(device.description, find(device_config, "description")); + + return device; +} diff --git a/cpp/libs/ossrf_nmos_api/lib/src/serialization/device.h b/cpp/libs/ossrf_nmos_api/lib/src/serialization/device.h new file mode 100644 index 0000000..46d6be4 --- /dev/null +++ b/cpp/libs/ossrf_nmos_api/lib/src/serialization/device.h @@ -0,0 +1,12 @@ +#pragma once + +#include "bisect/expected.h" +#include "bisect/nmoscpp/configuration.h" +#include "bisect/json.h" +#include + +namespace ossrf +{ + bisect::expected nmos_device_from_json(const nmos::id node_id, + const nlohmann::json& device_config); +} diff --git a/cpp/libs/ossrf_nmos_api/lib/src/serialization/media_types.h b/cpp/libs/ossrf_nmos_api/lib/src/serialization/media_types.h new file mode 100644 index 0000000..e8f95e2 --- /dev/null +++ b/cpp/libs/ossrf_nmos_api/lib/src/serialization/media_types.h @@ -0,0 +1,9 @@ +#pragma once + +#include + +namespace ossrf::media_types +{ + constexpr std::string_view VIDEO_RAW = "video/raw"; + constexpr std::string_view AUDIO_L24 = "audio/L24"; +} // namespace ossrf::media_types diff --git a/cpp/libs/ossrf_nmos_api/lib/src/serialization/meta.cpp b/cpp/libs/ossrf_nmos_api/lib/src/serialization/meta.cpp new file mode 100644 index 0000000..453cc71 --- /dev/null +++ b/cpp/libs/ossrf_nmos_api/lib/src/serialization/meta.cpp @@ -0,0 +1,17 @@ +#include "serialization/meta.h" +#include "bisect/expected/helpers.h" +#include + +using namespace ossrf; +using namespace bisect; +using namespace bisect::nmoscpp; + +maybe_ok ossrf::nmos_meta_from_json(const nlohmann::json& config, meta_info_t& info) +{ + BST_CHECK_ASSIGN(info.id, find(config, "id")); + + assign_if(config, "label", info, &meta_info_t::label); + assign_if(config, "description", info, &meta_info_t::description); + + return {}; +} diff --git a/cpp/libs/ossrf_nmos_api/lib/src/serialization/meta.h b/cpp/libs/ossrf_nmos_api/lib/src/serialization/meta.h new file mode 100644 index 0000000..dc3e1c9 --- /dev/null +++ b/cpp/libs/ossrf_nmos_api/lib/src/serialization/meta.h @@ -0,0 +1,10 @@ +#pragma once + +#include "bisect/expected/helpers.h" +#include "bisect/nmoscpp/configuration.h" +#include "bisect/json.h" + +namespace ossrf +{ + bisect::maybe_ok nmos_meta_from_json(const nlohmann::json& config, bisect::nmoscpp::meta_info_t& info); +} diff --git a/cpp/libs/ossrf_nmos_api/lib/src/serialization/network.cpp b/cpp/libs/ossrf_nmos_api/lib/src/serialization/network.cpp new file mode 100644 index 0000000..0777707 --- /dev/null +++ b/cpp/libs/ossrf_nmos_api/lib/src/serialization/network.cpp @@ -0,0 +1,45 @@ +#include "serialization/network.h" +#include "bisect/expected/helpers.h" + +using namespace ossrf; +using namespace bisect; +using namespace bisect::nmoscpp; + +using json = nlohmann::json; + +expected ossrf::network_from_json(const json& config, bool is_receiver) +{ + network_t net; + BST_ASSIGN(primary, find(config, "primary")); + BST_CHECK_ASSIGN(net.primary, network_leg_from_json(primary, is_receiver)); + + auto secondary = config.find("secondary"); + if(secondary != config.end()) + { + BST_CHECK_ASSIGN(net.secondary, network_leg_from_json(*secondary, is_receiver)); + } + + return net; +} + +expected ossrf::network_leg_from_json(const json& config, bool is_receiver) +{ + network_leg_t leg; + assign_if_or_value(config, "rtp_enabled", leg, &network_leg_t::rtp_enabled, true); + assign_if(config, "source_address", leg, &network_leg_t::source_ip); + assign_if(config, "destination_port", leg, &network_leg_t::destination_port); + assign_if(config, "interface_name", leg, &network_leg_t::interface_name); + + if(is_receiver) + { + assign_if(config, "multicast_address", leg, &network_leg_t::destination_ip); + assign_if(config, "interface_address", leg, &network_leg_t::interface_ip); + } + else + { + assign_if(config, "destination_address", leg, &network_leg_t::destination_ip); + assign_if(config, "source_port", leg, &network_leg_t::source_port); + } + + return leg; +} diff --git a/cpp/libs/ossrf_nmos_api/lib/src/serialization/network.h b/cpp/libs/ossrf_nmos_api/lib/src/serialization/network.h new file mode 100644 index 0000000..7b7d9a8 --- /dev/null +++ b/cpp/libs/ossrf_nmos_api/lib/src/serialization/network.h @@ -0,0 +1,12 @@ +#pragma once + +#include "bisect/expected.h" +#include "bisect/nmoscpp/configuration.h" +#include "bisect/json/json.h" + +namespace ossrf +{ + bisect::expected network_from_json(const nlohmann::json& config, bool is_receiver); + bisect::expected network_leg_from_json(const nlohmann::json& config, + bool is_receiver); +} // namespace ossrf diff --git a/cpp/libs/ossrf_nmos_api/lib/src/serialization/receiver.cpp b/cpp/libs/ossrf_nmos_api/lib/src/serialization/receiver.cpp new file mode 100644 index 0000000..c264ee7 --- /dev/null +++ b/cpp/libs/ossrf_nmos_api/lib/src/serialization/receiver.cpp @@ -0,0 +1,68 @@ +#include "serialization/receiver.h" +#include "serialization/network.h" +#include "serialization/meta.h" +#include "utils.h" +#include "bisect/expected/helpers.h" +#include "bisect/expected/macros.h" + +using namespace ossrf; +using namespace bisect; +using namespace bisect::nmoscpp; + +using nlohmann::json; + +namespace +{ + maybe_ok format_specific(const json& config, nmos_receiver_t& receiver) + { + BST_ASSIGN(capabilities, find(config, "capabilities")); + + BST_ENFORCE(capabilities.is_array(), "Receiver capabilities is not an array"); + auto c = capabilities.get>(); + BST_ENFORCE(c.size() == 1, "Only support one receiver capabilitiy"); + + auto media_type = c[0]; + if(media_type == "video/raw") + { + receiver.media_types = {nmos::media_types::video_raw}; + receiver.format = nmos::formats::video; + } + else if(media_type == "audio/raw") + { + receiver.media_types = {nmos::media_types::audio_L24}; + receiver.format = nmos::formats::audio; + } + else + { + BST_FAIL("cap {} not supported", media_type); + } + + return {}; + } + +} // namespace + +expected ossrf::nmos_receiver_from_json(const json& config) +{ + nmos_receiver_t receiver{}; + nmos_meta_from_json(config, receiver); + assign_if_or_value(config, "master_enable", receiver, &nmos_receiver_t::master_enable, true); + assign_if(config, "sender_id", receiver, &nmos_receiver_t::sender_id); + assign_if_or_value(config, "protocol", receiver, &nmos_receiver_t::protocol, + "urn:x-nmos:transport:rtp.mcast"); + + BST_ASSIGN(network, find(config, "network")); + BST_CHECK_ASSIGN(receiver.network, network_from_json(network, true)); + BST_CHECK(format_specific(config, receiver)); + + auto transport_file_it = config.find("transport_file"); + if(transport_file_it != config.end()) + { + BST_ASSIGN(data, find(*transport_file_it, "data")); + BST_ASSIGN(type, find(*transport_file_it, "type")); + BST_ENFORCE(type == "application/sdp", "Sender transport file is not an SDP"); + receiver.sdp_data = data; + } + + return receiver; +} diff --git a/cpp/libs/ossrf_nmos_api/lib/src/serialization/receiver.h b/cpp/libs/ossrf_nmos_api/lib/src/serialization/receiver.h new file mode 100644 index 0000000..becc952 --- /dev/null +++ b/cpp/libs/ossrf_nmos_api/lib/src/serialization/receiver.h @@ -0,0 +1,10 @@ +#pragma once + +#include "bisect/expected.h" +#include "bisect/nmoscpp/configuration.h" +#include "bisect/json.h" + +namespace ossrf +{ + bisect::expected nmos_receiver_from_json(const nlohmann::json& config); +} // namespace ossrf diff --git a/cpp/libs/ossrf_nmos_api/lib/src/serialization/sender.cpp b/cpp/libs/ossrf_nmos_api/lib/src/serialization/sender.cpp new file mode 100644 index 0000000..5e45288 --- /dev/null +++ b/cpp/libs/ossrf_nmos_api/lib/src/serialization/sender.cpp @@ -0,0 +1,130 @@ +#include "serialization/sender.h" +#include "serialization/network.h" +#include "serialization/video.h" +#include "serialization/meta.h" +#include "serialization/media_types.h" +#include "utils.h" +#include "bisect/expected/macros.h" +#include "bisect/sdp/builder.h" +#include + +using namespace ossrf; +using namespace ossrf::media_types; +using namespace bisect; +using namespace bisect::nmoscpp; + +using json = nlohmann::json; + +namespace +{ + expected to_interlace_mode(const std::string& s) + { + if(s == "progressive") return nmos::interlace_modes::progressive; + if(s == "interlaced_tff") return nmos::interlace_modes::interlaced_tff; + if(s == "interlaced_bff") return nmos::interlace_modes::interlaced_bff; + if(s == "interlaced_psf") return nmos::interlace_modes::interlaced_psf; + BST_FAIL("invalid frame structure '{}'", s); + } + + expected video_sender_info_from_json(const json& media) + { + video_sender_info_t info; + BST_ASSIGN(frame_rate, find(media, "frame_rate")); + BST_CHECK_ASSIGN(info.exact_framerate, framerate_from_json(frame_rate)); + BST_CHECK_ASSIGN(info.chroma_sub_sampling, find(media, "sampling")); + BST_CHECK_ASSIGN(info.width, find(media, "width")); + BST_CHECK_ASSIGN(info.height, find(media, "height")); + BST_ASSIGN(structure_s, find(media, "structure")); + BST_ASSIGN(structure, to_interlace_mode(structure_s)); + info.structure = structure; + + return info; + } + + expected audio_l24_sender_info_from_json(const json& media) + { + audio_sender_info_t info; + BST_CHECK_ASSIGN(info.number_of_channels, find(media, "number_of_channels")); + BST_CHECK_ASSIGN(info.sampling_rate, find(media, "sampling_rate")); + BST_CHECK_ASSIGN(info.packet_time, find(media, "packet_time")); + info.bits_per_sample = 24; + return info; + } + + std::string synthetize_id(std::string sender_id, int delta) + { + const auto last_digit_value = std::stoi(sender_id.substr(sender_id.size() - 1), 0, 16); + int new_last_digit_value = (last_digit_value + delta) % 16; + auto new_id = sender_id; + sprintf(new_id.data() + sender_id.size() - 1, "%x", new_last_digit_value); + return new_id; + } + + void get_extra(const json& j, std::string_view name, web::json::value& target) + { + const auto it = j.find(name); + if(it != j.end()) + { + target = web::json::value::parse(utility::s2us(it->dump())); + } + } + +} // namespace + +expected ossrf::nmos_sender_from_json(const json& config) +{ + nmos_sender_t sender{}; + + nmos_meta_from_json(config, sender); + assign_if_or_value(config, "protocol", sender, &nmos_sender_t::protocol, + "urn:x-nmos:transport:rtp.mcast"); + + const auto flow_id = synthetize_id(sender.id, 1); + const auto source_id = synthetize_id(sender.id, 2); + + assign_if_or_value(config, "master_enable", sender, &nmos_sender_t::master_enable, true); + + BST_ASSIGN(network, find(config, "network")); + BST_CHECK_ASSIGN(sender.network, network_from_json(network, false)); + BST_CHECK_ASSIGN(sender.media_type, find(config, "media_type")); + sender.source.id = source_id; + sender.source.label = sender.label; + sender.source.description = sender.description; + sender.flow.id = flow_id; + sender.flow.label = sender.label; + sender.flow.description = sender.description; + BST_CHECK_ASSIGN(sender.payload_type, maybe_find(config, "payload_type")); + + BST_ASSIGN(media, find(config, "media")); + + if(sender.media_type == media_types::VIDEO_RAW) + { + BST_ASSIGN(info, video_sender_info_from_json(media)); + sender.media = info; + sender.format = nmos::formats::video; + sender.grain_rate = info.exact_framerate; + } + else if(sender.media_type == media_types::AUDIO_L24) + { + BST_ASSIGN(info, audio_l24_sender_info_from_json(media)); + sender.media = info; + sender.format = nmos::formats::audio; + sender.grain_rate = info.sampling_rate; + } + else + { + BST_FAIL("invalid media type: {}", sender.media_type); + } + + BST_ASSIGN(maybe_sdp, find_or(config, "sdp")); + + if(!maybe_sdp.empty()) + { + sender.forced_sdp = maybe_sdp; + } + + get_extra(config, "sender", sender.extra); + get_extra(config, "flow", sender.flow.extra); + + return sender; +} diff --git a/cpp/libs/ossrf_nmos_api/lib/src/serialization/sender.h b/cpp/libs/ossrf_nmos_api/lib/src/serialization/sender.h new file mode 100644 index 0000000..14e3f93 --- /dev/null +++ b/cpp/libs/ossrf_nmos_api/lib/src/serialization/sender.h @@ -0,0 +1,10 @@ +#pragma once + +#include "bisect/expected.h" +#include "bisect/nmoscpp/configuration.h" +#include "bisect/json.h" + +namespace ossrf +{ + bisect::expected nmos_sender_from_json(const nlohmann::json& device_config); +} // namespace ossrf diff --git a/cpp/libs/ossrf_nmos_api/lib/src/serialization/video.cpp b/cpp/libs/ossrf_nmos_api/lib/src/serialization/video.cpp new file mode 100644 index 0000000..42f73e8 --- /dev/null +++ b/cpp/libs/ossrf_nmos_api/lib/src/serialization/video.cpp @@ -0,0 +1,11 @@ +#include "serialization/video.h" +#include "bisect/expected/macros.h" + +using namespace bisect; + +expected ossrf::framerate_from_json(const nlohmann::json& config) +{ + BST_ASSIGN(numerator, find(config, "num")); + BST_ASSIGN(denominator, find(config, "den")); + return nmos::rational{numerator, denominator}; +} diff --git a/cpp/libs/ossrf_nmos_api/lib/src/serialization/video.h b/cpp/libs/ossrf_nmos_api/lib/src/serialization/video.h new file mode 100644 index 0000000..c5ab272 --- /dev/null +++ b/cpp/libs/ossrf_nmos_api/lib/src/serialization/video.h @@ -0,0 +1,10 @@ +#pragma once + +#include "bisect/expected.h" +#include "bisect/json.h" +#include + +namespace ossrf +{ + bisect::expected framerate_from_json(const nlohmann::json& config); +} diff --git a/cpp/libs/ossrf_nmos_api/lib/src/utils.h b/cpp/libs/ossrf_nmos_api/lib/src/utils.h new file mode 100644 index 0000000..9075467 --- /dev/null +++ b/cpp/libs/ossrf_nmos_api/lib/src/utils.h @@ -0,0 +1,68 @@ +#pragma once + +#include "bisect/expected/macros.h" +#include +#include + +namespace ossrf +{ + /** Iterates over a container and returns: + * - ok if there are no errors; + * - the first error. + */ + inline bisect::maybe_ok fold_maybe_ok(const std::vector& errors) + { + auto maybe_error = std::find_if(errors.begin(), errors.end(), [](const bisect::maybe_ok& result) { + return bisect::core::detail::is_error(result); + }); + + if(maybe_error == std::end(errors)) + { + return {}; + } + return std::move(*maybe_error); + } + + /** + * Calls f for each of the items and returns a folded result: ok if no errors; the first error otherwise. + */ + template inline bisect::maybe_ok fold_call(const std::vector& items, F f) + { + std::vector result; + result.reserve(items.size()); + std::transform(items.begin(), items.end(), std::back_inserter(result), f); + + return fold_maybe_ok(result); + } + + template inline auto ret(const bisect::expected&) -> V; + + /** + * Calls f for each of the items and returns either: the results vector, if no errors; the first error otherwise. + */ + template + inline auto call_many(Ib begin, Ie end, F f) -> bisect::expected> + { + using R = decltype(ret(f(*begin))); + + std::vector result; + result.reserve(static_cast(std::distance(begin, end))); + + for(; begin != end; ++begin) + { + BST_ASSIGN(r, f(*begin)); + result.push_back(std::move(r)); + } + + return result; + } + + template std::vector get_ids(const T& items) + { + std::vector id_list{}; + id_list.reserve(items.size()); + std::transform(items.begin(), items.end(), std::back_inserter(id_list), + [](const auto& item) { return utility::s2us(item.id); }); + return id_list; + } +} // namespace ossrf diff --git a/images/build.sh b/images/build.sh new file mode 100755 index 0000000..8f8a620 --- /dev/null +++ b/images/build.sh @@ -0,0 +1,5 @@ +#!/bin/bash + +set -eu + +docker build -t ossrf-dev . -f dev/Dockerfile diff --git a/images/dev/Dockerfile b/images/dev/Dockerfile new file mode 100644 index 0000000..07aac16 --- /dev/null +++ b/images/dev/Dockerfile @@ -0,0 +1,48 @@ +FROM ubuntu:22.04 + +# Versions +ENV CONAN_VERSION=2.2.0 +ENV CMAKE_VERSION=3.27.0 +ENV SSHD_PORT=55555 + + +# To make it easier for build and release pipelines to run apt-get, +# configure apt to not require confirmation (assume the -y argument by default) +ENV DEBIAN_FRONTEND=noninteractive +RUN echo "APT::Get::Assume-Yes "true";" > /etc/apt/apt.conf.d/90assumeyes + +COPY scripts /opt/bisect/scripts + +RUN apt-get update + +RUN /opt/bisect/scripts/build-tools/add-user-bisect.sh +RUN /opt/bisect/scripts/build-tools/install-fish.sh +RUN /opt/bisect/scripts/build-tools/install-git.sh +RUN /opt/bisect/scripts/build-tools/install-conan.sh +RUN /opt/bisect/scripts/build-tools/install-cmake-x86.sh +RUN apt-get install -y \ + vim clang-format rsync gdb gdbserver x11-apps xauth iproute2 build-essential +RUN apt-get install -y libgtk-3-dev ninja-build + +RUN /opt/bisect/scripts/common/add-ssh-server.sh + +RUN apt-get install libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libgstreamer-plugins-bad1.0-dev gstreamer1.0-plugins-base \ + gstreamer1.0-plugins-good gstreamer1.0-plugins-bad gstreamer1.0-plugins-ugly gstreamer1.0-libav gstreamer1.0-tools gstreamer1.0-x \ + gstreamer1.0-alsa gstreamer1.0-gl gstreamer1.0-gtk3 gstreamer1.0-qt5 gstreamer1.0-pulseaudio + +RUN apt-get install avahi-utils +RUN apt-get install htop +RUN apt-get install make + +# GCC +RUN apt update && apt -y install gcc-12 g++-12 +RUN update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-12 100 +RUN update-alternatives --install /usr/bin/g++ g++ /usr/bin/g++-12 100 + +RUN apt-get autoremove + +RUN chmod +x /opt/bisect/scripts/launch.sh + +WORKDIR /home/bisect +USER bisect +ENTRYPOINT /opt/bisect/scripts/launch.sh diff --git a/images/docker-compose-x86-development.yml b/images/docker-compose-x86-development.yml new file mode 100755 index 0000000..6243953 --- /dev/null +++ b/images/docker-compose-x86-development.yml @@ -0,0 +1,18 @@ +version: "3" +services: + ossrf-dev: + build: + dockerfile: ./dev/Dockerfile + image: ossrf-dev + + network_mode: host + environment: + - PULSE_SERVER=unix:${XDG_RUNTIME_DIR}/pulse/native + volumes: + - ${XDG_RUNTIME_DIR}/pulse/native:${XDG_RUNTIME_DIR}/pulse/native + - ../volumes/home:/home/bisect/ + - /.conan2:/home/bisect/.conan2:rw + + nmos-registry: + image: docker.io/rhastie/nmos-cpp:latest + network_mode: host \ No newline at end of file diff --git a/images/run-dev.sh b/images/run-dev.sh new file mode 100755 index 0000000..3f259dc --- /dev/null +++ b/images/run-dev.sh @@ -0,0 +1,2 @@ +#!/bin/bash +docker run --rm -it ossrf-dev bash diff --git a/images/scripts/build-tools/add-user-bisect.sh b/images/scripts/build-tools/add-user-bisect.sh new file mode 100755 index 0000000..7c25dd4 --- /dev/null +++ b/images/scripts/build-tools/add-user-bisect.sh @@ -0,0 +1,14 @@ +#!/bin/bash +set -eu + +export DEBIAN_FRONTEND=noninteractive +apt-get install -y sudo --option=Dpkg::Options::=--force-confdef + +adduser --disabled-password --gecos '' bisect +adduser bisect sudo +echo 'bisect:bisect' | chpasswd +echo '%sudo ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers + +# access gst video plugins as non-root +usermod -aG video bisect +usermod -aG audio bisect diff --git a/images/scripts/build-tools/install-cmake-x86.sh b/images/scripts/build-tools/install-cmake-x86.sh new file mode 100755 index 0000000..c50ac47 --- /dev/null +++ b/images/scripts/build-tools/install-cmake-x86.sh @@ -0,0 +1,26 @@ +#!/bin/bash +set -eu + +if [[ -z "${CMAKE_VERSION+x}" ]]; then + echo ">>> ERROR: CMAKE_VERSION must be defined" + exit 1 +fi + +apt-get update -y && apt-get install -y \ + wget \ + curl + +apt-get remove cmake + +cd /tmp +wget https://github.com/Kitware/CMake/releases/download/v${CMAKE_VERSION}/cmake-${CMAKE_VERSION}-linux-x86_64.sh +mkdir /opt/cmake +cp cmake-${CMAKE_VERSION}-linux-x86_64.sh /opt/cmake +cd /opt/cmake +chmod +x cmake-${CMAKE_VERSION}-linux-x86_64.sh +./cmake-${CMAKE_VERSION}-linux-x86_64.sh --skip-license +echo export PATH="/opt/cmake/bin/:${PATH}" >> /etc/profile.d/100-cmake.sh + +cd /tmp +rm cmake-${CMAKE_VERSION}-linux-x86_64.sh +cd / diff --git a/images/scripts/build-tools/install-conan.sh b/images/scripts/build-tools/install-conan.sh new file mode 100755 index 0000000..ad18de9 --- /dev/null +++ b/images/scripts/build-tools/install-conan.sh @@ -0,0 +1,21 @@ +#!/bin/bash + +set -eu + +SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) + +source ${SCRIPT_DIR}/../common/env.sh + +check_env_var CONAN_VERSION + +apt-get update -y && apt-get install -y --no-install-recommends \ + software-properties-common \ + python3 \ + python3-pip \ + python3-setuptools \ + python3-wheel + +# CONAN +pip3 install --upgrade pip +pip3 install setuptools +pip3 install conan==${CONAN_VERSION} --upgrade diff --git a/images/scripts/build-tools/install-fish.sh b/images/scripts/build-tools/install-fish.sh new file mode 100755 index 0000000..adbf1a6 --- /dev/null +++ b/images/scripts/build-tools/install-fish.sh @@ -0,0 +1,3 @@ +set -eu + +apt-get update && apt-get install fish diff --git a/images/scripts/build-tools/install-git.sh b/images/scripts/build-tools/install-git.sh new file mode 100755 index 0000000..cb11a7a --- /dev/null +++ b/images/scripts/build-tools/install-git.sh @@ -0,0 +1,3 @@ +set -eu + +apt-get update && apt-get install git diff --git a/images/scripts/common/add-ssh-server.sh b/images/scripts/common/add-ssh-server.sh new file mode 100755 index 0000000..0f2411e --- /dev/null +++ b/images/scripts/common/add-ssh-server.sh @@ -0,0 +1,24 @@ +#!/bin/bash +set -eu +SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) + +export DEBIAN_FRONTEND=noninteractive +apt-get install -y sudo --option=Dpkg::Options::=--force-confdef + +source ${SCRIPT_DIR}/../common/env.sh + +check_env_var SSHD_PORT + +apt-get update && apt-get install openssh-server + +( +echo "LogLevel DEBUG2"; +echo "PermitRootLogin no"; +echo "X11Forwarding yes"; +echo "X11UseLocalhost no"; +echo "Port ${SSHD_PORT}"; +echo "PasswordAuthentication yes"; +echo "Subsystem sftp /usr/lib/openssh/sftp-server"; +) > /etc/ssh/sshd_config_bisect + +mkdir /run/sshd diff --git a/images/scripts/common/env.sh b/images/scripts/common/env.sh new file mode 100755 index 0000000..ff393de --- /dev/null +++ b/images/scripts/common/env.sh @@ -0,0 +1,7 @@ +check_env_var() { + local name=$1 + if [[ -z "${!name+x}" ]]; then + echo ">>> ERROR: ${name} must be defined" + exit 1 + fi +} diff --git a/images/scripts/launch.sh b/images/scripts/launch.sh new file mode 100755 index 0000000..b9d74ed --- /dev/null +++ b/images/scripts/launch.sh @@ -0,0 +1,16 @@ +#!/bin/bash +set -eu + +HOME="/home/bisect" + +if [ -d $HOME ] +then + echo "Directory $HOME exists." + sudo chown bisect.bisect $HOME +else + echo "Directory $HOME does not exist." + mkdir $HOME +fi + +# start ssh service +sudo /usr/sbin/sshd -D -e -f /etc/ssh/sshd_config_bisect diff --git a/scripts/build.sh b/scripts/build.sh new file mode 100755 index 0000000..a831852 --- /dev/null +++ b/scripts/build.sh @@ -0,0 +1,8 @@ +#!/bin/bash + +set -eu +SCRIPT_DIR="$(realpath "$(dirname "$0")")" +source ${SCRIPT_DIR}/common.sh + +configure +build diff --git a/scripts/cipipeline/clang-test.sh b/scripts/cipipeline/clang-test.sh new file mode 100755 index 0000000..02032b9 --- /dev/null +++ b/scripts/cipipeline/clang-test.sh @@ -0,0 +1,9 @@ +#!/bin/bash +set -eu + +#install clang +sudo apt-get update && sudo apt-get install -y clang-format + +#check clang format +last_commit=$(git log -1 --pretty=format:"%H") +git show --name-only --pretty=format: $last_commit | grep -E '\.(cpp|hpp|c|h)$' | xargs -r clang-format -style=file -output-replacements-xml | grep "