diff --git a/README.md b/README.md index ca06bc7..308e76d 100644 --- a/README.md +++ b/README.md @@ -127,3 +127,6 @@ Open `cpp/demos/ossrf-nmos-api/config/nmos_config.json` and adjust the following ## Build Container and Code simultaneously ./scripts/build-inside-container.sh + +## Notes +At the moment, the gstreamer plugins with NMOS (nmossender and nmosreceiver) aren't 100% functional, being more of a proof of concept that will be refined in the near future. diff --git a/cpp/demos/config/nmos_plugin_node_config.json b/cpp/demos/config/nmos_plugin_node_config.json new file mode 100644 index 0000000..74df109 --- /dev/null +++ b/cpp/demos/config/nmos_plugin_node_config.json @@ -0,0 +1,39 @@ +{ + "node": { + "id": "d5504cd1-fe68-489d-99d4-20d3f075f062", + "configuration": { + "label": "BISECT OSSRF Node2", + "description": "BISECT OSSRF node2", + "host_addresses": [ + "192.168.1.120" + ], + "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.120", + "registry_version": "v1.3", + "registration_port": 8010, + "system_address": "192.168.1.120", + "system_version": "v1.0", + "system_port": 8010, + "http_port": 5113 + } + }, + "device": {}, + "receivers": [], + "senders": [] +} diff --git a/cpp/libs/CMakeLists.txt b/cpp/libs/CMakeLists.txt index c5298e1..1e0bd5d 100644 --- a/cpp/libs/CMakeLists.txt +++ b/cpp/libs/CMakeLists.txt @@ -2,6 +2,7 @@ add_subdirectory(bisect_expected) add_subdirectory(bisect_nmoscpp) add_subdirectory(bisect_sdp) add_subdirectory(bisect_gst) +add_subdirectory(gst_nmos_sender_plugin) add_subdirectory(ossrf_nmos_api) add_subdirectory(ossrf_gstreamer_api) add_subdirectory(bisect_json) diff --git a/cpp/libs/gst_nmos_sender_plugin/CMakeLists.txt b/cpp/libs/gst_nmos_sender_plugin/CMakeLists.txt new file mode 100644 index 0000000..c51e312 --- /dev/null +++ b/cpp/libs/gst_nmos_sender_plugin/CMakeLists.txt @@ -0,0 +1,67 @@ +cmake_minimum_required(VERSION 3.16) +project(gst_nmos_sender_plugin LANGUAGES CXX) + +find_package(nlohmann_json REQUIRED) +find_package(PkgConfig REQUIRED) + +# Locate GLib package +pkg_check_modules(GLIB REQUIRED glib-2.0) + +# Locate GStreamer packages +pkg_search_module(GSTREAMER REQUIRED gstreamer-1.0>=1.4) +pkg_search_module(GSTREAMER_APP REQUIRED gstreamer-app-1.0>=1.4) +pkg_search_module(GSTREAMER_AUDIO REQUIRED gstreamer-audio-1.0>=1.4) +pkg_search_module(GSTREAMER_VIDEO REQUIRED gstreamer-video-1.0>=1.4) + +# Include GLib directories and link libraries +include_directories(${GLIB_INCLUDE_DIRS}) +link_directories(${GLIB_LIBRARY_DIRS}) +add_definitions(${GLIB_CFLAGS_OTHER}) + +# Include source files for gst_nmos_sender_plugin +file(GLOB_RECURSE PLUGIN_SOURCE_FILES *.cpp *.h) + +# Include source files for utils +file(GLOB_RECURSE UTILS_SOURCE_FILES utils/*.cpp utils/*.h) + +# Combine utils and plugin sources +set(SOURCE_FILES ${PLUGIN_SOURCE_FILES} ${UTILS_SOURCE_FILES}) + +# Define the plugin as a shared library +add_library(${PROJECT_NAME} MODULE ${SOURCE_FILES}) + +# Include directories +target_include_directories(${PROJECT_NAME} + PRIVATE ${GSTREAMER_INCLUDE_DIRS} + PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/utils/include +) + +# Link GStreamer libraries +target_compile_options(${PROJECT_NAME} PRIVATE ${GSTREAMER_CFLAGS_OTHER}) +target_link_libraries(${PROJECT_NAME} + PRIVATE + ${GSTREAMER_LIBRARIES} + ${GSTREAMER_APP_LIBRARIES} + ${GSTREAMER_AUDIO_LIBRARIES} + ${GSTREAMER_VIDEO_LIBRARIES} + PUBLIC + bisect::project_warnings + bisect::expected + bisect::bisect_nmoscpp + bisect::bisect_json + nlohmann_json::nlohmann_json + ossrf::ossrf_nmos_api + ${GLIB_LIBRARIES} +) + +# Specify the output directory and the library name +set_target_properties(${PROJECT_NAME} PROPERTIES + LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/plugins + OUTPUT_NAME "gstnmossender" # Custom .so name +) + +add_library(ossrf::${PROJECT_NAME} ALIAS ${PROJECT_NAME}) + +# Install the plugin to the system's GStreamer plugin directory +install(TARGETS ${PROJECT_NAME} + LIBRARY DESTINATION ~/.local/lib/gstreamer-1.0) diff --git a/cpp/libs/gst_nmos_sender_plugin/gst_nmos_sender_plugin.cpp b/cpp/libs/gst_nmos_sender_plugin/gst_nmos_sender_plugin.cpp new file mode 100644 index 0000000..f8938e6 --- /dev/null +++ b/cpp/libs/gst_nmos_sender_plugin/gst_nmos_sender_plugin.cpp @@ -0,0 +1,475 @@ + +/* GStreamer + * + * Copyright (C) <2024> Bisect Lda + * @author: Luís Ferreira + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 51 Franklin Street, Suite 500, + * Boston, MA 02110-1335, USA. + */ + +#include "bisect/json.h" +#include "utils.h" +#include "ossrf/nmos/api/nmos_client.h" +#include +#include + +GST_DEBUG_CATEGORY_STATIC(gst_nmossender_debug_category); +#define GST_CAT_DEFAULT gst_nmossender_debug_category +#define GST_TYPE_NMOSSENDER (gst_nmossender_get_type()) +#define GST_NMOSSENDER(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), GST_TYPE_NMOSSENDER, GstNmossender)) +#define GST_NMOSSENDER_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST((klass), GST_TYPE_NMOSSENDER, GstNmossenderClass)) +#define GST_IS_NMOSSENDER(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), GST_TYPE_NMOSSENDER)) +#define GST_IS_NMOSSENDER_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), GST_TYPE_NMOSSENDER)) + +typedef struct _GstNmossender +{ + GstBin parent; + GstElement* queue; + GstElement* video_payloader; + GstElement* audio_payloader; + GstElement* udpsink; + ossrf::nmos_client_uptr client; + GstCaps* caps; + config_fields_t config; +} GstNmossender; + +typedef struct _GstNmossenderClass +{ + GstBinClass parent_class; +} GstNmossenderClass; + +enum class PropertyId : uint32_t +{ + NodeId = 1, + NodeConfigFileLocation = 2, + DeviceId = 3, + DeviceLabel = 4, + DeviceDescription = 5, + SenderId = 6, + SenderLabel = 7, + SenderDescription = 8, + SourceAddress = 9, + InterfaceName = 10, + DestinationAddress = 11, + DestinationPort = 12 +}; + +G_DEFINE_TYPE_WITH_CODE(GstNmossender, gst_nmossender, GST_TYPE_BIN, + GST_DEBUG_CATEGORY_INIT(gst_nmossender_debug_category, "nmossender", 0, "NMOS Sender Plugin")) + +/* Pad template */ +static GstStaticPadTemplate sink_template = GST_STATIC_PAD_TEMPLATE("sink", GST_PAD_SINK, GST_PAD_REQUEST, + GST_STATIC_CAPS("video/x-raw, " + "format=(string){ UYVP }; " + "audio/x-raw, " + "format=(string)S24BE, " + "layout=(string)interleaved, " + "rate=(int)[ 1, 2147483647 ], " + "channels=(int)[ 1, 2147483647 ]")); + +/* Set properties so element variables can change depending on them */ +static void gst_nmossender_set_property(GObject* object, guint property_id, const GValue* value, GParamSpec* pspec) +{ + GstNmossender* self = GST_NMOSSENDER(object); + + switch(static_cast(property_id)) + { + case PropertyId::NodeId: + g_free(self->config.node.id); + self->config.node.id = g_value_dup_string(value); + break; + + case PropertyId::NodeConfigFileLocation: + g_free(self->config.node.configuration_location); + self->config.node.configuration_location = g_value_dup_string(value); + break; + + case PropertyId::DeviceId: + g_free(self->config.device.id); + self->config.device.id = g_value_dup_string(value); + break; + + case PropertyId::DeviceLabel: + g_free(self->config.device.label); + self->config.device.label = g_value_dup_string(value); + break; + + case PropertyId::DeviceDescription: + g_free(self->config.device.description); + self->config.device.description = g_value_dup_string(value); + break; + + case PropertyId::SenderId: + g_free(self->config.sender_id); + self->config.sender_id = g_value_dup_string(value); + break; + + case PropertyId::SenderLabel: + g_free(self->config.sender_label); + self->config.sender_label = g_value_dup_string(value); + break; + + case PropertyId::SenderDescription: + g_free(self->config.sender_description); + self->config.sender_description = g_value_dup_string(value); + break; + + case PropertyId::SourceAddress: + g_free(self->config.network.source_address); + self->config.network.source_address = g_value_dup_string(value); + break; + + case PropertyId::InterfaceName: + g_free(self->config.network.interface_name); + self->config.network.interface_name = g_value_dup_string(value); + g_object_set(G_OBJECT(self->udpsink), "bind_address", self->config.network.interface_name, NULL); + break; + + case PropertyId::DestinationAddress: + g_free(self->config.network.destination_address); + self->config.network.destination_address = g_value_dup_string(value); + g_object_set(G_OBJECT(self->udpsink), "host", self->config.network.destination_address, NULL); + break; + + case PropertyId::DestinationPort: + self->config.network.destination_port = atoi(g_value_dup_string(value)); + g_object_set(G_OBJECT(self->udpsink), "port", self->config.network.destination_port, NULL); + break; + + default: G_OBJECT_WARN_INVALID_PROPERTY_ID(object, property_id, pspec); break; + } +} + +static void gst_nmossender_get_property(GObject* object, guint property_id, GValue* value, GParamSpec* pspec) +{ + GstNmossender* self = GST_NMOSSENDER(object); + + switch(static_cast(property_id)) + { + case PropertyId::NodeId: g_value_set_string(value, self->config.node.id); break; + case PropertyId::NodeConfigFileLocation: g_value_set_string(value, self->config.node.configuration_location); break; + case PropertyId::DeviceId: g_value_set_string(value, self->config.device.id); break; + case PropertyId::DeviceLabel: g_value_set_string(value, self->config.device.label); break; + case PropertyId::DeviceDescription: g_value_set_string(value, self->config.device.description); break; + case PropertyId::SenderId: g_value_set_string(value, self->config.sender_id); break; + case PropertyId::SenderLabel: g_value_set_string(value, self->config.sender_label); break; + case PropertyId::SenderDescription: g_value_set_string(value, self->config.sender_description); break; + case PropertyId::SourceAddress: g_value_set_string(value, self->config.network.source_address); break; + case PropertyId::InterfaceName: g_value_set_string(value, self->config.network.interface_name); break; + case PropertyId::DestinationAddress: g_value_set_string(value, self->config.network.destination_address); break; + case PropertyId::DestinationPort: g_value_set_int(value, self->config.network.destination_port); break; + + default: G_OBJECT_WARN_INVALID_PROPERTY_ID(object, property_id, pspec); break; + } +} + +/* Event handler for the sink pad */ +static gboolean gst_nmossender_sink_event(GstPad* pad, GstObject* parent, GstEvent* event) +{ + GstNmossender* self = GST_NMOSSENDER(parent); + + switch(GST_EVENT_TYPE(event)) + { + case GST_EVENT_CAPS: { + GstCaps* caps = NULL; + gst_event_parse_caps(event, &caps); + + if(caps) + { + gchar* caps_str = gst_caps_to_string(caps); + // DEBUG + // g_print("\n\nReceived CAPS from upstream: %s\n", caps_str); + + const GstStructure* structure = gst_caps_get_structure(caps, 0); + const gchar* media_type = gst_structure_get_name(structure); + + if(g_strcmp0(media_type, "audio/x-raw") == 0) + { + // DEBUG + // g_print("Audio caps detected\n"); + // GST_INFO_OBJECT(self, "Audio caps detected"); + // + if(!gst_element_link(self->queue, self->audio_payloader)) + { + GST_ERROR_OBJECT(self, "Failed to link queue to audio_payloader"); + return FALSE; + } + if(!gst_element_link(self->audio_payloader, self->udpsink)) + { + GST_ERROR_OBJECT(self, "Failed to link audio_payloader to udpsink"); + return FALSE; + } + self->caps = gst_caps_ref(caps); + self->config.is_audio = TRUE; + for(guint i = 0; i < gst_caps_get_size(caps); i++) + { + gint rate, channels; + const gchar* format; + if(gst_structure_get_int(structure, "rate", &rate)) + { + self->config.audio_sender_fields.sampling_rate = rate; + } + if(gst_structure_get_int(structure, "channels", &channels)) + { + self->config.audio_sender_fields.number_of_channels = channels; + } + if((format = gst_structure_get_string(structure, "format"))) + { + g_free(self->config.audio_sender_fields.format); + self->config.audio_sender_fields.format = g_strdup(translate_audio_format(format).c_str()); + } + } + // DEBUG + // g_print("Rate: %d, Channels: %d, Format: %s\n", self->config.audio_sender_fields.sampling_rate, + // self->config.audio_sender_fields.number_of_channels, + // self->config.audio_sender_fields.format); + } + else if(g_strcmp0(media_type, "video/x-raw") == 0) + { + // DEBUG + // g_print("Video caps detected\n"); + // GST_INFO_OBJECT(self, "Video caps detected"); + // + if(!gst_element_link(self->queue, self->video_payloader)) + { + GST_ERROR_OBJECT(self, "Failed to link queue to video_payloader"); + return FALSE; + } + if(!gst_element_link(self->video_payloader, self->udpsink)) + { + GST_ERROR_OBJECT(self, "Failed to link video_payloader to udpsink"); + return FALSE; + } + self->caps = gst_caps_ref(caps); + self->config.is_audio = FALSE; + for(guint i = 0; i < gst_caps_get_size(caps); i++) + { + gint width, height; + const gchar* format; + if(gst_structure_get_int(structure, "width", &width)) + { + self->config.video_media_fields.width = width; + } + if(gst_structure_get_int(structure, "height", &height)) + { + self->config.video_media_fields.height = height; + } + if((format = gst_structure_get_string(structure, "format"))) + { + g_free(self->config.video_media_fields.sampling); + self->config.video_media_fields.sampling = g_strdup(translate_video_format(format).c_str()); + } + } + + // DEBUG + // g_print("Width: %d, Height: %d, Format: %s\n", self->config.video_media_fields.width, + // self->config.video_media_fields.height, self->config.video_media_fields.sampling); + } + else + { + GST_WARNING_OBJECT(self, "Unsupported media type: %s", media_type); + } + + g_free(caps_str); + } + else + { + GST_WARNING_OBJECT(self, "No caps found in CAPS event"); + } + + break; + } + default: break; + } + // Pass the event downstream + return gst_pad_event_default(pad, parent, event); +} + +/* State Change so it doesn't boot NMOS without pipeline being set to playing */ +static GstStateChangeReturn gst_nmossender_change_state(GstElement* element, GstStateChange transition) +{ + GstStateChangeReturn ret; + GstNmossender* self = GST_NMOSSENDER(element); + + auto sender_activation_callback = [](bool master_enabled, const nlohmann::json& transport_params) { + fmt::print("nmos_sender_callback: master_enabled={}, transport_params={}\n", master_enabled, + transport_params.dump()); + }; + + switch(transition) + { + case GST_STATE_CHANGE_PAUSED_TO_PLAYING: { + + const auto node_config_json = create_node_config(self->config); + const auto device_config_json = create_device_config(self->config); + nlohmann::json_abi_v3_11_3::json sender_config_json = NULL; + + if(self->config.is_audio) + { + sender_config_json = create_audio_sender_config(self->config); + } + else + { + sender_config_json = create_video_sender_config(self->config); + } + + // DEBUG + // g_print("Node Configuration: %s\n", node_config_json.dump().c_str()); + // g_print("Device Configuration: %s\n", device_config_json.dump().c_str()); + // g_print("Sender Configuration: %s\n", sender_config_json.dump().c_str()); + + auto result = ossrf::nmos_client_t::create(self->config.node.id, node_config_json.dump()); + if(!result.has_value()) + { + GST_ERROR_OBJECT(self, "Failed to initialize NMOS client. Node ID: %s", self->config.node.id); + return GST_STATE_CHANGE_FAILURE; + } + + self->client = std::move(result.value()); + if(!self->client->add_device(device_config_json.dump())) + { + GST_ERROR_OBJECT(self, "Failed to add device to NMOS client"); + return GST_STATE_CHANGE_FAILURE; + } + + if(!self->client->add_sender(self->config.device.id, sender_config_json.dump(), sender_activation_callback)) + { + GST_ERROR_OBJECT(self, "Failed to add sender to NMOS client"); + return GST_STATE_CHANGE_FAILURE; + } + } + break; + + default: break; + } + + // This is needed to work ¯\_(ツ)_/¯ + ret = GST_ELEMENT_CLASS(gst_nmossender_parent_class)->change_state(element, transition); + + return ret; +} + +/* Helper function to add properties */ +static void add_property(GObjectClass* object_class, guint id, const gchar* name, const gchar* nick, const gchar* blurb, + const gchar* default_value) +{ + g_object_class_install_property(object_class, id, + g_param_spec_string(name, nick, blurb, default_value, + (GParamFlags)(G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS))); +} + +/* Class initialization */ +static void gst_nmossender_class_init(GstNmossenderClass* klass) +{ + GObjectClass* object_class = G_OBJECT_CLASS(klass); + GstElementClass* element_class = GST_ELEMENT_CLASS(klass); + + gst_element_class_add_pad_template(element_class, gst_static_pad_template_get(&sink_template)); + + object_class->set_property = gst_nmossender_set_property; + object_class->get_property = gst_nmossender_get_property; + + // Add properties using helper function + add_property(object_class, 1, "node-id", "Node ID", "The ID of the node", "0aad3458-1081-4fba-af02-a8ebd9feeae3"); + add_property( + object_class, 2, "node-config-file-location", "Node Config File Location", + "The location of the configuration file for the node", + "/home/nmos/repos/nmos-sender-receiver-framework/cpp/demos/ossrf-nmos-api/config/nmos_plugin_config.json"); + add_property(object_class, 3, "device-id", "Device ID", "The ID of the device", + "b9b85f97-58db-41fe-934f-c2afbf7bd46f"); + add_property(object_class, 4, "device-label", "Device Label", "The label to identify the device", "OSSRF Device"); + add_property(object_class, 5, "device-description", "Device Description", "Description of the device", + "OSSRF Device Description"); + add_property(object_class, 6, "sender-id", "Sender ID", "The ID of the sender", + "e543a2c1-d6a2-47f5-8d14-296bb6714ef2"); + add_property(object_class, 7, "sender-label", "Sender Label", "Label to identify the sender", + "BISECT sender video"); + add_property(object_class, 8, "sender-description", "Sender Description", + "Description string to better describe the sender", "BISECT sender video"); + add_property(object_class, 9, "source-address", "Source Address", "The address of the source", "127.0.0.1"); + add_property(object_class, 10, "interface-name", "Interface Name", "Name of the interface", "wlp1s0"); + add_property(object_class, 11, "destination-address", "Destination Address", "The address of the destination", + "127.0.0.1"); + add_property(object_class, 12, "destination-port", "Destination Port", "Port of the destination", "9999"); + + gst_element_class_set_static_metadata(element_class, "NMOS Sender", "Sink/Network", + "Processes raw video and sends it over RTP and UDP to NMOS client", + "Luis Ferreira "); + + element_class->change_state = gst_nmossender_change_state; +} + +/* Object initialization */ +static void gst_nmossender_init(GstNmossender* self) +{ + self->queue = gst_element_factory_make("queue", NULL); + self->video_payloader = gst_element_factory_make("rtpvrawpay", NULL); + self->audio_payloader = gst_element_factory_make("rtpL24pay", NULL); + self->udpsink = gst_element_factory_make("udpsink", NULL); + + if(!self->queue || !self->video_payloader || !self->audio_payloader || !self->udpsink) + { + GST_ERROR_OBJECT(self, + "Failed to create internal elements: queue or video_payloader or audio_playloader or udpsink"); + return; + } + + // set properties + g_object_set(G_OBJECT(self->queue), "max-size-buffers", 1, NULL); + g_object_set(G_OBJECT(self->udpsink), "host", "127.0.0.1", "port", 9999, NULL); + self->config = create_default_config_fields(); + + gst_bin_add_many(GST_BIN(self), self->queue, self->video_payloader, self->audio_payloader, self->udpsink, NULL); + + // create and configure the sink pad + GstPad* queue_sinkpad = gst_element_get_static_pad(self->queue, "sink"); + if(!queue_sinkpad) + { + GST_ERROR_OBJECT(self, "Failed to get static pad 'sink' from queue"); + return; + } + GstPad* sink_ghost_pad = gst_ghost_pad_new("sink", queue_sinkpad); + gst_object_unref(queue_sinkpad); + + if(!sink_ghost_pad) + { + GST_ERROR_OBJECT(self, "Failed to create ghost pad for sink"); + return; + } + + gst_pad_set_event_function(sink_ghost_pad, gst_nmossender_sink_event); + + if(!gst_element_add_pad(GST_ELEMENT(self), sink_ghost_pad)) + { + GST_ERROR_OBJECT(self, "Failed to add ghost pad to element; pad with same name might exist"); + gst_object_unref(sink_ghost_pad); + return; + } +} + +static gboolean plugin_init(GstPlugin* plugin) +{ + return gst_element_register(plugin, "nmossender", GST_RANK_NONE, GST_TYPE_NMOSSENDER); +} + +#define VERSION "0.1" +#define PACKAGE "gst-nmos-sender-plugin" +#define PACKAGE_NAME "AMWA NMOS Sender and Receiver Framework Plugins" +#define GST_PACKAGE_ORIGIN "https://www.amwa.tv/" +#define GST_PACKAGE_LICENSE "Apache-2.0" + +GST_PLUGIN_DEFINE(GST_VERSION_MAJOR, GST_VERSION_MINOR, nmossender, "Plugin to send to NMOS application video stream", + plugin_init, VERSION, GST_PACKAGE_LICENSE, PACKAGE_NAME, GST_PACKAGE_ORIGIN) diff --git a/cpp/libs/gst_nmos_sender_plugin/utils.cpp b/cpp/libs/gst_nmos_sender_plugin/utils.cpp new file mode 100644 index 0000000..438321f --- /dev/null +++ b/cpp/libs/gst_nmos_sender_plugin/utils.cpp @@ -0,0 +1,245 @@ +#include "utils.h" +#include "ossrf/nmos/api/nmos_client.h" +#include "bisect/nmoscpp/configuration.h" +#include "bisect/expected/macros.h" +#include "bisect/expected.h" +#include "bisect/json.h" +#include + +using namespace bisect; +using json = nlohmann::json; + +config_fields_t create_default_config_fields() +{ + config_fields_t config; + + // Initialize node_fields_t + config.node.id = g_strdup("d5504cd1-fe68-489d-99d4-20d3f075f062"); + config.node.configuration_location = + g_strdup("/home/nmos/repos/nmos-sender-receiver-framework/cpp/demos/config/nmos_plugin_node_config.json"); + + // Initialize device_fields_t + config.device.id = g_strdup("e92e628b-7421-4723-9fb9-c1f3b38af9d3"); + config.device.label = g_strdup("OSSRF Device2"); + config.device.description = g_strdup("OSSRF Device2"); + + // Initialize video_media_fields_t + config.video_media_fields.width = 640; + config.video_media_fields.height = 480; + config.video_media_fields.frame_rate_num = 50; + config.video_media_fields.frame_rate_density = 1; + config.video_media_fields.sampling = g_strdup("YCbCr-4:2:2"); + config.video_media_fields.structure = g_strdup("progressive"); + + // Initialize audio_media_fields_t + config.audio_sender_fields.format = g_strdup("audio/L24"); + config.audio_sender_fields.number_of_channels = 1; + config.audio_sender_fields.packet_time = 1.000; + config.audio_sender_fields.sampling_rate = 48000; + + // Initialize network_fields_t + config.network.source_address = g_strdup("192.168.1.120"); + config.network.interface_name = g_strdup("wlp1s0"); + config.network.destination_address = g_strdup("192.168.1.120"); + config.network.destination_port = 5004; + + // Initialize config_fields_t + config.sender_id = g_strdup("1c920570-e0b4-4637-b02c-26c9d4275c71"); + config.sender_label = g_strdup("BISECT OSSRF Media Sender"); + config.sender_description = g_strdup("BISECT OSSRF Media Sender"); + + return config; +} + +json create_node_config(config_fields_t& config) +{ + const std::string node_config_str = get_node_config(config.node.configuration_location); + + // Parse the configuration into a JSON object + json node_config = json::parse(node_config_str); + + // Override the "id" field with the one from the config_fields_t structure + node_config["id"] = config.node.id; + + return node_config; +} + +json create_device_config(config_fields_t& config) +{ + json device = { + {"id", config.device.id}, {"label", config.device.label}, {"description", config.device.description}}; + return device; +} + +json create_video_sender_config(config_fields_t& config) +{ + json sender = { + {"id", config.sender_id}, + {"label", config.sender_label}, + {"description", config.sender_description}, + {"network", + {{"primary", + {{"source_address", config.network.source_address}, + {"interface_name", config.network.interface_name}, + {"destination_address", config.network.destination_address}, + {"destination_port", config.network.destination_port}}}}}, + {"payload_type", 97}, + {"media_type", "video/raw"}, + {"media", + {{"width", config.video_media_fields.width}, + {"height", config.video_media_fields.height}, + {"frame_rate", + {{"num", config.video_media_fields.frame_rate_num}, {"den", config.video_media_fields.frame_rate_density}}}, + {"sampling", config.video_media_fields.sampling}, + {"structure", config.video_media_fields.structure}}}}; + return sender; +} + +json create_audio_sender_config(config_fields_t& config) +{ + json sender = {{"id", config.sender_id}, + {"label", config.sender_label}, + {"description", config.sender_description}, + {"network", + {{"primary", + {{"source_address", config.network.source_address}, + {"interface_name", config.network.interface_name}, + {"destination_address", config.network.destination_address}, + {"destination_port", config.network.destination_port}}}}}, + {"payload_type", 97}, + {"media_type", "audio/L24"}, + {"media", + {{"number_of_channels", config.audio_sender_fields.number_of_channels}, + {"sampling_rate", config.audio_sender_fields.sampling_rate}, + {"packet_time", config.audio_sender_fields.packet_time}}}}; + return sender; +} + +std::string translate_video_format(const std::string& gst_format) +{ + static const std::unordered_map video_format_map = { + {"UYVP", "YCbCr-4:2:2"}, {"I420", "YCbCr-4:2:0"}, {"NV12", "YCbCr-4:2:0"}, {"RGB", "RGB-8:8:8"}, + {"RGBA", "RGBA-8:8:8:8"}, {"BGR", "BGR-8:8:8"}, {"BGRA", "BGRA-8:8:8:8"}}; + + auto it = video_format_map.find(gst_format); + if(it != video_format_map.end()) + { + return it->second; + } + return gst_format; +} + +std::string translate_audio_format(const std::string& gst_format) +{ + static const std::unordered_map audio_format_map = { + {"S24BE", "L24"}, {"S16LE", "L16"}, {"F32LE", "F32"}, {"F64LE", "F64"}, + {"S32LE", "L32"}, {"S24_32LE", "L24_32"}, {"U8", "U8"}}; + + auto it = audio_format_map.find(gst_format); + if(it != audio_format_map.end()) + { + return it->second; + } + return gst_format; +} + +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()); +} + +std::string get_node_id(char* node_configuration_location) +{ + const auto configuration_result = load_configuration_from_file(node_configuration_location); + + const json& configuration = configuration_result.value(); + + auto node_result = find(configuration, "node"); + + const json& node = node_result.value(); + + auto node_id_result = find(node, "id"); + auto node_config_result = find(node, "configuration"); + + if(node_id_result.has_value() && node_config_result.has_value()) + { + const std::string node_id = node_id_result.value(); + const std::string node_configuration = node_config_result.value().dump(); + + return node_id; + } + + return ""; +} + +std::string get_node_config(char* node_configuration_location) +{ + const auto configuration_result = load_configuration_from_file(node_configuration_location); + + const json& configuration = configuration_result.value(); + + auto node_result = find(configuration, "node"); + + const json& node = node_result.value(); + + auto node_id_result = find(node, "id"); + auto node_config_result = find(node, "configuration"); + + if(node_id_result.has_value() && node_config_result.has_value()) + { + const std::string node_id = node_id_result.value(); + const std::string node_configuration = node_config_result.value().dump(); + + return node_configuration; + } + + return ""; +} + +std::string get_device_id(char* device_configuration_location) +{ + const auto configuration_result = load_configuration_from_file(device_configuration_location); + + const json& configuration = configuration_result.value(); + + auto device_result = find(configuration, "device"); + + const json& device = device_result.value(); + + auto device_id_result = find(device, "id"); + + const std::string device_id = device_id_result.value(); + const std::string device_config = device.dump(); + + return device_id; +} + +std::string get_device_config(char* device_configuration_location) +{ + const auto configuration_result = load_configuration_from_file(device_configuration_location); + + const json& configuration = configuration_result.value(); + + auto device_result = find(configuration, "device"); + + const json& device = device_result.value(); + + auto device_id_result = find(device, "id"); + + const std::string device_id = device_id_result.value(); + const std::string device_config = device.dump(); + + return device_config; +} + +std::string get_sender_config(char* sender_configuration_location) +{ + // FIX ME: Gonna be hardcoded for now + const auto sender_config = load_configuration_from_file(sender_configuration_location); + + return sender_config.value().dump(); +} diff --git a/cpp/libs/gst_nmos_sender_plugin/utils.h b/cpp/libs/gst_nmos_sender_plugin/utils.h new file mode 100644 index 0000000..0b0b086 --- /dev/null +++ b/cpp/libs/gst_nmos_sender_plugin/utils.h @@ -0,0 +1,78 @@ +#pragma once +#include +#include +#include + +typedef struct node_fields_t +{ + gchar* id; + gchar* configuration_location; +} node_fields_t; + +typedef struct device_fields_t +{ + gchar* id; + gchar* label; + gchar* description; +} device_fields_t; + +typedef struct video_media_fields_t +{ + gint width; + gint height; + gint frame_rate_num; + gint frame_rate_density; + gchar* sampling; + gchar* structure; +} video_media_fields_t; + +typedef struct audio_media_fields_t +{ + gchar* format; + gint number_of_channels; + gint sampling_rate; + gint packet_time; +} audio_media_fields_t; + +typedef struct network_fields_t +{ + gchar* source_address; + gchar* interface_name; + gchar* destination_address; + gint destination_port; +} network_fields_t; + +typedef struct config_fields_t +{ + node_fields_t node; + device_fields_t device; + gboolean is_audio; + video_media_fields_t video_media_fields; + audio_media_fields_t audio_sender_fields; + gchar* sender_id; + gchar* sender_label; + gchar* sender_description; + network_fields_t network; +} config_fields_t; + +config_fields_t create_default_config_fields(); + +nlohmann::json create_node_config(config_fields_t& config); +nlohmann::json create_device_config(config_fields_t& config); +nlohmann::json create_video_sender_config(config_fields_t& config); +nlohmann::json create_audio_sender_config(config_fields_t& config); + +std::string translate_video_format(const std::string& gst_format); + +std::string translate_audio_format(const std::string& gst_format); + +std::string get_node_id(char* node_configuration_location); + +std::string get_node_config(char* node_configuration_location); + +std::string get_device_id(char* device_configuration_location); + +std::string get_device_config(char* device_configuration_location); + +std::string get_sender_config(char* sender_configuration_location); + diff --git a/images/docker-compose-x86-development.yml b/images/docker-compose-x86-development.yml index 2bce88b..66d5d25 100755 --- a/images/docker-compose-x86-development.yml +++ b/images/docker-compose-x86-development.yml @@ -6,13 +6,20 @@ services: image: ossrf-dev environment: - PULSE_SERVER=unix:${XDG_RUNTIME_DIR}/pulse/native + - DISPLAY:$DISPLAY + - TZ=Europe/Lisbon devices: - "/dev/snd:/dev/snd" volumes: - ~/.config/pulse/cookie:/root/.config/pulse/cookie - ${XDG_RUNTIME_DIR}/pulse/native:${XDG_RUNTIME_DIR}/pulse/native + - /tmp/.X11-unix:/tmp/.X11-unix - ../volumes/home:/home/nmos/ + - /.conan2:/home/nmos/.conan2:rw + - /var/run/dbus:/var/run/dbus + - /var/run/avahi-daemon/socket:/var/run/avahi-daemon/socket network_mode: host + privileged: true nmos-registry: image: docker.io/rhastie/nmos-cpp:latest