diff --git a/cpp/demos/config/nmos_plugin_node_config_receiver.json b/cpp/demos/config/nmos_plugin_node_config_receiver.json new file mode 100644 index 0000000..f856643 --- /dev/null +++ b/cpp/demos/config/nmos_plugin_node_config_receiver.json @@ -0,0 +1,39 @@ +{ + "node": { + "id": "8968180c-0418-4422-bea1-f0c642389637", + "configuration": { + "label": "BISECT OSSRF Node Receiver", + "description": "BISECT OSSRF node receiver", + "host_addresses": [ + "192.168.1.36" + ], + "interfaces": [ + { + "chassis_id": "c9-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-26", + "locked": true + } + ], + "registry_address": "192.168.1.36", + "registry_version": "v1.3", + "registration_port": 8010, + "system_address": "192.168.1.36", + "system_version": "v1.0", + "system_port": 8010, + "http_port": 5114 + } + }, + "device": {}, + "receivers": [], + "senders": [] +} diff --git a/cpp/demos/config/nmos_plugin_node_config.json b/cpp/demos/config/nmos_plugin_node_config_sender.json similarity index 73% rename from cpp/demos/config/nmos_plugin_node_config.json rename to cpp/demos/config/nmos_plugin_node_config_sender.json index 74df109..3e33ee6 100644 --- a/cpp/demos/config/nmos_plugin_node_config.json +++ b/cpp/demos/config/nmos_plugin_node_config_sender.json @@ -2,16 +2,16 @@ "node": { "id": "d5504cd1-fe68-489d-99d4-20d3f075f062", "configuration": { - "label": "BISECT OSSRF Node2", - "description": "BISECT OSSRF node2", + "label": "BISECT OSSRF Node Sender", + "description": "BISECT OSSRF node sender", "host_addresses": [ - "192.168.1.120" + "192.168.1.36" ], "interfaces": [ { "chassis_id": "c8-94-02-f7-3e-eb", "name": "wlp1s0", - "port_id": "00-e0-4c-68-01-8d" + "port_id": "01-e0-4c-68-01-8d" } ], "clocks": [ @@ -24,13 +24,13 @@ "locked": true } ], - "registry_address": "192.168.1.120", + "registry_address": "192.168.1.36", "registry_version": "v1.3", "registration_port": 8010, - "system_address": "192.168.1.120", + "system_address": "192.168.1.36", "system_version": "v1.0", "system_port": 8010, - "http_port": 5113 + "http_port": 6114 } }, "device": {}, diff --git a/cpp/libs/CMakeLists.txt b/cpp/libs/CMakeLists.txt index 1e0bd5d..1bb41b6 100644 --- a/cpp/libs/CMakeLists.txt +++ b/cpp/libs/CMakeLists.txt @@ -2,7 +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(gst_nmos_plugins) add_subdirectory(ossrf_nmos_api) add_subdirectory(ossrf_gstreamer_api) add_subdirectory(bisect_json) diff --git a/cpp/libs/bisect_nmoscpp/lib/include/bisect/nmoscpp/configuration.h b/cpp/libs/bisect_nmoscpp/lib/include/bisect/nmoscpp/configuration.h index 4e8d2d9..6234892 100644 --- a/cpp/libs/bisect_nmoscpp/lib/include/bisect/nmoscpp/configuration.h +++ b/cpp/libs/bisect_nmoscpp/lib/include/bisect/nmoscpp/configuration.h @@ -107,6 +107,7 @@ namespace bisect::nmoscpp nmos::rational exact_framerate; std::string chroma_sub_sampling; nmos::interlace_mode structure; + int depth = 0; }; struct audio_sender_info_t diff --git a/cpp/libs/bisect_sdp/lib/src/reader.cpp b/cpp/libs/bisect_sdp/lib/src/reader.cpp index 2364b67..634ef5f 100644 --- a/cpp/libs/bisect_sdp/lib/src/reader.cpp +++ b/cpp/libs/bisect_sdp/lib/src/reader.cpp @@ -145,6 +145,7 @@ namespace .exact_framerate = nmos::rational(params.exactframerate.numerator(), params.exactframerate.denominator()), .chroma_sub_sampling = "YCbCr-4:2:2", .structure = params.interlace ? nmos::interlace_modes::interlaced_tff : nmos::interlace_modes::progressive, + .depth = static_cast(params.depth), }; } } // namespace @@ -196,4 +197,4 @@ expected bisect::sdp::parse_sdp(const std::string& sdp) { BST_FAIL("error parsing SDP: {}", ex.what()); } -} \ No newline at end of file +} diff --git a/cpp/libs/gst_nmos_plugins/CMakeLists.txt b/cpp/libs/gst_nmos_plugins/CMakeLists.txt new file mode 100644 index 0000000..2c3304c --- /dev/null +++ b/cpp/libs/gst_nmos_plugins/CMakeLists.txt @@ -0,0 +1,99 @@ +cmake_minimum_required(VERSION 3.16) +project(gst_nmos_plugins 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 the parent directory of gst_nmos_plugins +include_directories(${CMAKE_CURRENT_SOURCE_DIR}/..) + +# Utils library (shared across plugins) +add_library(utils STATIC src/utils.cpp) + +# Enable -fPIC for utils +set_target_properties(utils PROPERTIES POSITION_INDEPENDENT_CODE ON) + +target_include_directories(utils + PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/.. + PRIVATE ${GLIB_INCLUDE_DIRS} + PRIVATE ${GSTREAMER_INCLUDE_DIRS} + PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/../bisect + PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/../ossrf +) +target_link_libraries(utils + PUBLIC nlohmann_json::nlohmann_json + PRIVATE ${GLIB_LIBRARIES} + PRIVATE ${GSTREAMER_LIBRARIES} + PRIVATE ${GSTREAMER_APP_LIBRARIES} + PRIVATE ${GSTREAMER_AUDIO_LIBRARIES} + PRIVATE ${GSTREAMER_VIDEO_LIBRARIES} + PRIVATE bisect::project_warnings + PRIVATE bisect::expected + PRIVATE bisect::bisect_nmoscpp + PRIVATE bisect::bisect_json + PRIVATE ossrf::ossrf_nmos_api +) + +# Function to create a plugin target +function(create_plugin plugin_name plugin_sources output_name) + add_library(${plugin_name} MODULE ${plugin_sources}) + target_include_directories(${plugin_name} + PRIVATE ${GSTREAMER_INCLUDE_DIRS} + PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/.. + PUBLIC $ + ) + target_link_libraries(${plugin_name} + PRIVATE + ${GSTREAMER_LIBRARIES} + ${GSTREAMER_APP_LIBRARIES} + ${GSTREAMER_AUDIO_LIBRARIES} + ${GSTREAMER_VIDEO_LIBRARIES} + utils + PUBLIC + bisect::project_warnings + bisect::expected + bisect::bisect_nmoscpp + bisect::bisect_json + nlohmann_json::nlohmann_json + ossrf::ossrf_nmos_api + ${GLIB_LIBRARIES} + ) + set_target_properties(${plugin_name} PROPERTIES + LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/plugins + OUTPUT_NAME ${output_name} + ) + add_library(ossrf::${plugin_name} ALIAS ${plugin_name}) + install(TARGETS ${plugin_name} + LIBRARY DESTINATION ~/.local/lib/gstreamer-1.0 + ) +endfunction() + +# Plugin: Video Receiver +create_plugin( + gst_nmos_video_receiver_plugin + src/gst_nmos_video_receiver_plugin.cpp + "gstnmosvideoreceiver" +) + +# Plugin: Audio Receiver +create_plugin( + gst_nmos_audio_receiver_plugin + src/gst_nmos_audio_receiver_plugin.cpp + "gstnmosaudioreceiver" +) + +# Plugin: Sender +create_plugin( + gst_nmos_sender_plugin + src/gst_nmos_sender_plugin.cpp + "gstnmossender" +) diff --git a/cpp/libs/gst_nmos_plugins/include/element_class.h b/cpp/libs/gst_nmos_plugins/include/element_class.h new file mode 100644 index 0000000..c7576a1 --- /dev/null +++ b/cpp/libs/gst_nmos_plugins/include/element_class.h @@ -0,0 +1,71 @@ +#pragma once +#include +#include +#include +#include +#include + +template class GstElementHandle +{ + public: + static std::variant create_element(const char* factory_name, + const char* element_name = nullptr) + { + T* elem = reinterpret_cast(gst_element_factory_make(factory_name, element_name)); + if(elem == nullptr) + { + return nullptr; + } + return GstElementHandle(elem); + } + + static std::variant create_bin(const char* bin_name) + { + T* bin = reinterpret_cast(gst_bin_new(bin_name)); + if(bin == nullptr) + { + return nullptr; + } + return GstElementHandle(bin); + } + + GstElementHandle(const GstElementHandle&) = delete; + GstElementHandle& operator=(const GstElementHandle&) = delete; + + GstElementHandle(GstElementHandle&& other) noexcept : handle_(other.handle_) { other.handle_ = nullptr; } + + GstElementHandle& operator=(GstElementHandle&& other) noexcept + { + if(this != &other) + { + if(handle_ != nullptr) + { + gst_object_unref(handle_); + } + handle_ = other.handle_; + other.handle_ = nullptr; + } + return *this; + } + + ~GstElementHandle() + { + if(handle_) + { + gst_object_unref(handle_); + } + } + + /// Destruction of this object will no longer unref the owned element. + void forget(T* new_ptr = nullptr) + { + handle_ = new_ptr; + } + + T* get() const { return handle_; } + + private: + explicit GstElementHandle(T* handle) : handle_(handle) {} + + T* handle_ = nullptr; +}; diff --git a/cpp/libs/gst_nmos_plugins/include/nmos_configuration.h b/cpp/libs/gst_nmos_plugins/include/nmos_configuration.h new file mode 100644 index 0000000..d57e81a --- /dev/null +++ b/cpp/libs/gst_nmos_plugins/include/nmos_configuration.h @@ -0,0 +1,57 @@ +#pragma once +#include +#include + +struct node_fields_t +{ + std::string id; + std::string configuration_location; +}; + +struct device_fields_t +{ + std::string id; + std::string label; + std::string description; +}; + +struct video_media_fields_t +{ + gint width; + gint height; + gint frame_rate_num; + gint frame_rate_den; + std::string sampling; + std::string structure; +}; + +struct audio_media_fields_t +{ + std::string format; + gint number_of_channels; + gint sampling_rate; + gint packet_time; +}; + +struct network_fields_t +{ + std::string source_address; + std::string interface_name; + std::string destination_address; + gint destination_port; +}; + +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; + std::string id; + std::string label; + std::string description; + network_fields_t network; + std::string interface_name; + std::string address; +}; diff --git a/cpp/libs/gst_nmos_plugins/src/gst_nmos_audio_receiver_plugin.cpp b/cpp/libs/gst_nmos_plugins/src/gst_nmos_audio_receiver_plugin.cpp new file mode 100644 index 0000000..4dee7af --- /dev/null +++ b/cpp/libs/gst_nmos_plugins/src/gst_nmos_audio_receiver_plugin.cpp @@ -0,0 +1,476 @@ + +/* 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/expected/macros.h" +#include "bisect/json.h" +#include "bisect/sdp.h" +#include "bisect/sdp/reader.h" +#include "bisect/nmoscpp/configuration.h" +#include "utils.h" +#include "ossrf/nmos/api/nmos_client.h" +#include "../include/element_class.h" +#include "../include/nmos_configuration.h" +#include +#include + +using namespace bisect; +using namespace bisect::sdp; + +GST_DEBUG_CATEGORY_STATIC(gst_nmosaudioreceiver_debug_category); +#define GST_CAT_DEFAULT gst_nmosaudioreceiver_debug_category +#define GST_TYPE_NMOSAUDIORECEIVER (gst_nmosaudioreceiver_get_type()) +#define GST_NMOSAUDIORECEIVER(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), GST_TYPE_NMOSAUDIORECEIVER, GstNmosaudioreceiver)) +#define GST_NMOSAUDIORECEIVER_CLASS(klass) \ + (G_TYPE_CHECK_CLASS_CAST((klass), GST_TYPE_NMOSAUDIORECEIVER, GstNmosaudioreceiverClass)) +#define GST_IS_NMOSAUDIORECEIVER(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), GST_TYPE_NMOSAUDIORECEIVER)) +#define GST_IS_NMOSAUDIORECEIVER_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), GST_TYPE_NMOSAUDIORECEIVER)) + +typedef struct _GstNmosaudioreceiver +{ + GstBin parent; + GstPad* element_pad; + GstPad* bin_pad; + GstClock* clock; + GstElementHandle<_GstElement> bin; + GstElementHandle<_GstElement> udp_src; + GstElementHandle<_GstElement> rtp_audio_depay_16; + GstElementHandle<_GstElement> rtp_audio_depay_24; + GstElementHandle<_GstElement> rtp_jitter_buffer; + GstElementHandle<_GstElement> queue; + ossrf::nmos_client_uptr client; + config_fields_t config; + std::string sdp_string; + sdp_settings_t sdp_settings; + bool nmos_active; +} GstNmosaudioreceiver; + +typedef struct _GstNmosaudioreceiverClass +{ + GstBinClass parent_class; +} GstNmosaudioreceiverClass; + +G_DEFINE_TYPE_WITH_CODE(GstNmosaudioreceiver, gst_nmosaudioreceiver, GST_TYPE_BIN, + GST_DEBUG_CATEGORY_INIT(gst_nmosaudioreceiver_debug_category, "nmosaudioreceiver", 0, + "NMOS Receiver Plugin")) +// Pad template +static GstStaticPadTemplate src_template = GST_STATIC_PAD_TEMPLATE("src", GST_PAD_SRC, GST_PAD_ALWAYS, + GST_STATIC_CAPS("audio/x-raw, " + "format=(string){ S16BE, S24BE }; " + "application/x-rtp")); + +enum class PropertyId : uint32_t +{ + NodeId = 1, + NodeConfigFileLocation = 2, + DeviceId = 3, + DeviceLabel = 4, + DeviceDescription = 5, + ReceiverId = 6, + ReceiverLabel = 7, + ReceiverDescription = 8, + DstAddress = 9 +}; + +// Set properties so element variables can change depending on them +static void gst_nmosaudioreceiver_set_property(GObject* object, guint property_id, const GValue* value, + GParamSpec* pspec) +{ + GstNmosaudioreceiver* self = GST_NMOSAUDIORECEIVER(object); + + switch(static_cast(property_id)) + { + case PropertyId::NodeId: self->config.node.id = g_value_dup_string(value); break; + + case PropertyId::NodeConfigFileLocation: + self->config.node.configuration_location = g_value_dup_string(value); + break; + + case PropertyId::DeviceId: self->config.device.id = g_value_dup_string(value); break; + + case PropertyId::DeviceLabel: self->config.device.label = g_value_dup_string(value); break; + + case PropertyId::DeviceDescription: self->config.device.description = g_value_dup_string(value); break; + + case PropertyId::ReceiverId: self->config.id = g_value_dup_string(value); break; + + case PropertyId::ReceiverLabel: self->config.label = g_value_dup_string(value); break; + + case PropertyId::ReceiverDescription: self->config.description = g_value_dup_string(value); break; + + case PropertyId::DstAddress: self->config.address = g_value_dup_string(value); break; + + default: G_OBJECT_WARN_INVALID_PROPERTY_ID(object, property_id, pspec); break; + } +} + +static void gst_nmosaudioreceiver_get_property(GObject* object, guint property_id, GValue* value, GParamSpec* pspec) +{ + GstNmosaudioreceiver* self = GST_NMOSAUDIORECEIVER(object); + + switch(static_cast(property_id)) + { + + case PropertyId::NodeId: g_value_set_string(value, self->config.node.id.c_str()); break; + case PropertyId::NodeConfigFileLocation: + g_value_set_string(value, self->config.node.configuration_location.c_str()); + break; + case PropertyId::DeviceId: g_value_set_string(value, self->config.device.id.c_str()); break; + case PropertyId::DeviceLabel: g_value_set_string(value, self->config.device.label.c_str()); break; + case PropertyId::DeviceDescription: g_value_set_string(value, self->config.device.description.c_str()); break; + case PropertyId::ReceiverId: g_value_set_string(value, self->config.id.c_str()); break; + case PropertyId::ReceiverLabel: g_value_set_string(value, self->config.label.c_str()); break; + case PropertyId::ReceiverDescription: g_value_set_string(value, self->config.description.c_str()); break; + case PropertyId::DstAddress: g_value_set_string(value, self->config.address.c_str()); break; + + default: G_OBJECT_WARN_INVALID_PROPERTY_ID(object, property_id, pspec); break; + } +} + +static GstPadProbeReturn block_pad_probe_cb(GstPad* pad, GstPadProbeInfo* info, gpointer user_data) +{ + return GST_PAD_PROBE_DROP; +} + +void remove_old_bin(GstNmosaudioreceiver* self) +{ + if(self->element_pad == nullptr) + { + return; + } + gulong block_id = + gst_pad_add_probe(self->element_pad, GST_PAD_PROBE_TYPE_BLOCK_DOWNSTREAM, block_pad_probe_cb, NULL, NULL); + + gst_element_send_event(GST_ELEMENT(self), gst_event_new_flush_start()); + gst_element_send_event(GST_ELEMENT(self), gst_event_new_flush_stop(FALSE)); + + self->clock = gst_element_get_clock(GST_ELEMENT(self)); + + gst_element_set_state(self->bin.get(), GST_STATE_NULL); + + gst_bin_remove(GST_BIN(self), self->udp_src.get()); + gst_bin_remove(GST_BIN(self), self->rtp_jitter_buffer.get()); + gst_bin_remove(GST_BIN(self), self->rtp_audio_depay_16.get()); + gst_bin_remove(GST_BIN(self), self->rtp_audio_depay_24.get()); + gst_bin_remove(GST_BIN(self), self->queue.get()); + + gst_bin_remove(GST_BIN(self), self->bin.get()); + + self->udp_src.forget(); + self->queue.forget(); + self->rtp_jitter_buffer.forget(); + self->rtp_audio_depay_16.forget(); + self->rtp_audio_depay_24.forget(); + self->bin.forget(); + + if(block_id != 0) + { + gst_pad_remove_probe(self->element_pad, block_id); + } +} + +void construct_pipeline(GstNmosaudioreceiver* self) +{ + auto format = self->sdp_settings.format; + try + { + bisect::nmoscpp::audio_sender_info_t audio_info = std::get(format); + fmt::print("\n===== SDP Settings =====\n" + "Audio (from variant):\n" + " - bits per sample: {}\n" + " - number of channels: {}\n" + " - packet time: {}\n" + " - sampling rate: {}\n" + "Primary leg:\n" + " - destination_ip: {}\n" + " - destination_port: {}\n" + "\n", + audio_info.bits_per_sample, audio_info.number_of_channels, audio_info.packet_time, + audio_info.sampling_rate, self->sdp_settings.primary.destination_ip.value().c_str(), + self->sdp_settings.primary.destination_port.value()); + + auto maybeBin = GstElementHandle::create_bin("dynamic-bin"); + if(std::holds_alternative(maybeBin)) + { + GST_ERROR_OBJECT(self, "Failed to create bin"); + return; + } + + self->bin = std::move(std::get>(maybeBin)); + + auto maybeUdpsrc = GstElementHandle::create_element("udpsrc", nullptr); + auto maybeQueue = GstElementHandle::create_element("queue", nullptr); + auto maybeJitter = GstElementHandle::create_element("rtpjitterbuffer", nullptr); + auto maybeDepay16 = GstElementHandle::create_element("rtpL16depay", nullptr); + auto maybeDepay24 = GstElementHandle::create_element("rtpL24depay", nullptr); + + if(std::holds_alternative(maybeUdpsrc) || std::holds_alternative(maybeQueue) || + std::holds_alternative(maybeJitter) || + std::holds_alternative(maybeDepay16) || std::holds_alternative(maybeDepay24)) + { + GST_ERROR_OBJECT(self, "Failed to create pipeline elements."); + return; + } + + self->udp_src = std::move(std::get>(maybeUdpsrc)); + self->queue = std::move(std::get>(maybeQueue)); + self->rtp_jitter_buffer = std::move(std::get>(maybeJitter)); + self->rtp_audio_depay_16 = std::move(std::get>(maybeDepay16)); + self->rtp_audio_depay_24 = std::move(std::get>(maybeDepay24)); + + g_object_set( + G_OBJECT(self->udp_src.get()), "address", self->sdp_settings.primary.destination_ip.value().c_str(), "port", + self->sdp_settings.primary.destination_port.value(), "do-timestamp", true, "buffer-size", 212992, nullptr); + + GstCaps* caps = gst_caps_new_simple("application/x-rtp", "clock-rate", G_TYPE_INT, audio_info.sampling_rate, + "channels", G_TYPE_INT, audio_info.number_of_channels, nullptr); + + g_object_set(G_OBJECT(self->udp_src.get()), "caps", caps, nullptr); + + g_object_set(G_OBJECT(self->rtp_jitter_buffer.get()), "do-retransmission", true, "do-lost", true, "mode", 0, + "latency", 0, nullptr); + + g_object_set(G_OBJECT(self->queue.get()), "max-size-buffers", 3, nullptr); + + gst_bin_add_many(GST_BIN(self->bin.get()), self->udp_src.get(), self->rtp_jitter_buffer.get(), + self->queue.get(), self->rtp_audio_depay_16.get(), self->rtp_audio_depay_24.get(), nullptr); + + gst_element_sync_state_with_parent(self->udp_src.get()); + gst_element_sync_state_with_parent(self->rtp_jitter_buffer.get()); + gst_element_sync_state_with_parent(self->queue.get()); + gst_element_sync_state_with_parent(self->rtp_audio_depay_16.get()); + gst_element_sync_state_with_parent(self->rtp_audio_depay_24.get()); + + GstPad* bin_src_pad = nullptr; + + if(audio_info.bits_per_sample == 16) + { + + if(gst_element_link_many(self->udp_src.get(), self->rtp_jitter_buffer.get(), self->queue.get(), + self->rtp_audio_depay_16.get(), nullptr) == false) + { + GST_ERROR_OBJECT(self, "Failed to link elements inside the bin."); + return; + } + + // Create a ghost pad from the depay's src + bin_src_pad = gst_element_get_static_pad(self->rtp_audio_depay_16.get(), "src"); + if(bin_src_pad == nullptr) + { + GST_ERROR_OBJECT(self, "Failed to get src pad from rtp_audio_depay."); + return; + } + } + else + { + if(gst_element_link_many(self->udp_src.get(), self->rtp_jitter_buffer.get(), self->queue.get(), + self->rtp_audio_depay_24.get(), nullptr) == false) + { + GST_ERROR_OBJECT(self, "Failed to link elements inside the bin."); + return; + } + + // Create a ghost pad from the depay's src + bin_src_pad = gst_element_get_static_pad(self->rtp_audio_depay_24.get(), "src"); + if(bin_src_pad == nullptr) + { + GST_ERROR_OBJECT(self, "Failed to get src pad from rtp_audio_depay."); + return; + } + } + + GstPad* bin_ghost_pad = gst_ghost_pad_new("src", bin_src_pad); + gst_object_unref(bin_src_pad); + self->bin_pad = bin_ghost_pad; + + if(bin_ghost_pad == nullptr || gst_element_add_pad(self->bin.get(), bin_ghost_pad) == false) + { + GST_ERROR_OBJECT(self, "Failed to create or add ghost pad to bin."); + return; + } + + gst_bin_add(GST_BIN(self), self->bin.get()); + + GstPad* plugin_pad = gst_element_get_static_pad(GST_ELEMENT(self), "src"); + if(plugin_pad == nullptr) + { + GST_ERROR_OBJECT(self, "Failed to get plugin ghost pad."); + return; + } + + self->element_pad = plugin_pad; + + if(gst_ghost_pad_set_target(GST_GHOST_PAD(self->element_pad), bin_ghost_pad) == false) + { + GST_ERROR_OBJECT(self, "Failed to link plugin ghost pad to bin's ghost pad."); + return; + } + + gst_object_unref(plugin_pad); + } + catch(std::bad_variant_access const& ex) + { + fmt::print("Invalid format sent for current receiver.\n"); + } +} + +void create_nmos(GstNmosaudioreceiver* self) +{ + const auto node_config_json = create_node_config(self->config); + if(node_config_json == nullptr) + { + GST_ERROR_OBJECT(self, "Failed to initialize NMOS client. No valid node JSON location given."); + return; + } + const auto device_config_json = create_device_config(self->config); + nlohmann::json_abi_v3_11_3::json receiver_config_json = nullptr; + + receiver_config_json = create_receiver_config(self->config); + + auto receiver_callback = [self](const std::optional& sdp, bool master_enabled) { + fmt::print("Receiver Activation Callback: SDP={}, Master Enabled={}\n", sdp.has_value() ? sdp.value() : "None", + master_enabled); + + if(sdp) + { + fmt::print("Received SDP: {}\n", sdp.value()); + auto sdp_settings = parse_sdp(sdp.value()); + if(sdp_settings.has_value() && sdp.value() != self->sdp_string) + { + self->sdp_settings = sdp_settings.value(); + self->sdp_string = sdp.value(); + remove_old_bin(self); + construct_pipeline(self); + gst_element_set_state(GST_ELEMENT(self), GST_STATE_PLAYING); + } + } + else + { + fmt::print("No SDP provided.\n"); + } + fmt::print("Master enabled: {}\n", master_enabled); + }; + + 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."); + return; + } + self->client = std::move(result.value()); + self->client->add_device(device_config_json.dump()); + self->client->add_receiver(self->config.device.id, receiver_config_json.dump(), receiver_callback); + GST_INFO_OBJECT(self, "NMOS client initialized successfully."); +} + +static GstStateChangeReturn gst_nmosaudioreceiver_change_state(GstElement* element, GstStateChange transition) +{ + GstStateChangeReturn ret; + GstNmosaudioreceiver* self = GST_NMOSAUDIORECEIVER(element); + + switch(transition) + { + case GST_STATE_CHANGE_NULL_TO_READY: { + if(self->nmos_active != true) + { + create_nmos(self); + self->nmos_active = true; + } + } + break; + + default: break; + } + + ret = GST_ELEMENT_CLASS(gst_nmosaudioreceiver_parent_class)->change_state(element, transition); + + return ret; +} + +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_nmosaudioreceiver_class_init(GstNmosaudioreceiverClass* 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(&src_template)); + + object_class->set_property = gst_nmosaudioreceiver_set_property; + object_class->get_property = gst_nmosaudioreceiver_get_property; + + add_property(object_class, 1, "node-id", "Node ID", "The ID of the node", "d49c85db-1c33-4f21-b160-58edd2af1810"); + 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/config/nmos_plugin_node_config_receiver.json"); + add_property(object_class, 3, "device-id", "Device ID", "The ID of the device", + "1ad20d7c-8c58-4c84-8f14-3cf3e3af164c"); + 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, "receiver-id", "Receiver ID", "UUID of the receiver", + "db9f46cf-2414-4e25-b6c6-2078159857f9"); + add_property(object_class, 7, "receiver-label", "Label of the receiver", "Label of the receiver", + "BISECT OSSRF Audio Receiver"); + add_property(object_class, 8, "receiver-description", "Description of the receiver", "Description of the receiver", + "BISECT OSSRF Audio Receiver"); + add_property(object_class, 9, "destination-address", "Destination Address", "Address of the destination", + "127.0.0.1"); + + gst_element_class_set_static_metadata(element_class, "NMOS Audio Receiver", "Source/Network", + "Receives raw audio from NMOS", "Luis Ferreira "); + + element_class->change_state = gst_nmosaudioreceiver_change_state; +} + +// Object initialization +static void gst_nmosaudioreceiver_init(GstNmosaudioreceiver* self) +{ + GstPadTemplate* src_tmpl = gst_static_pad_template_get(&src_template); + GstPad* ghost_src = gst_ghost_pad_new_no_target_from_template("src", src_tmpl); + gst_object_unref(src_tmpl); + gst_element_add_pad(GST_ELEMENT(self), ghost_src); + + create_default_config_fields_audio_receiver(&self->config); +} + +static gboolean plugin_init(GstPlugin* plugin) +{ + return gst_element_register(plugin, "nmosaudioreceiver", GST_RANK_NONE, GST_TYPE_NMOSAUDIORECEIVER); +} + +#define VERSION "0.1" +#define PACKAGE "gst-nmos-audio-receiver-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, nmosaudioreceiver, + "Plugin to receive audio stream from NMOS application", plugin_init, VERSION, GST_PACKAGE_LICENSE, + PACKAGE_NAME, GST_PACKAGE_ORIGIN) diff --git a/cpp/libs/gst_nmos_sender_plugin/gst_nmos_sender_plugin.cpp b/cpp/libs/gst_nmos_plugins/src/gst_nmos_sender_plugin.cpp similarity index 76% rename from cpp/libs/gst_nmos_sender_plugin/gst_nmos_sender_plugin.cpp rename to cpp/libs/gst_nmos_plugins/src/gst_nmos_sender_plugin.cpp index 4d88232..ee453d9 100644 --- a/cpp/libs/gst_nmos_sender_plugin/gst_nmos_sender_plugin.cpp +++ b/cpp/libs/gst_nmos_plugins/src/gst_nmos_sender_plugin.cpp @@ -23,6 +23,8 @@ #include "bisect/json.h" #include "utils.h" #include "ossrf/nmos/api/nmos_client.h" +#include "../include/element_class.h" +#include "../include/nmos_configuration.h" #include #include @@ -37,10 +39,11 @@ GST_DEBUG_CATEGORY_STATIC(gst_nmossender_debug_category); typedef struct _GstNmossender { GstBin parent; - GstElement* queue; - GstElement* video_payloader; - GstElement* audio_payloader; - GstElement* udpsink; + GstElementHandle<_GstElement> queue; + GstElementHandle<_GstElement> video_payloader; + GstElementHandle<_GstElement> audio_payloader_16; + GstElementHandle<_GstElement> audio_payloader_24; + GstElementHandle<_GstElement> udpsink; ossrf::nmos_client_uptr client; GstCaps* caps; config_fields_t config; @@ -75,7 +78,7 @@ static GstStaticPadTemplate sink_template = GST_STATIC_PAD_TEMPLATE("sink", GST_ GST_STATIC_CAPS("video/x-raw, " "format=(string){ UYVP }; " "audio/x-raw, " - "format=(string)S24BE, " + "format=(string){ S24BE, S16BE }, " "layout=(string)interleaved, " "rate=(int)[ 1, 2147483647 ], " "channels=(int)[ 1, 2147483647 ]")); @@ -99,28 +102,28 @@ static void gst_nmossender_set_property(GObject* object, guint property_id, cons case PropertyId::DeviceDescription: self->config.device.description = g_value_dup_string(value); break; - case PropertyId::SenderId: self->config.sender_id = g_value_dup_string(value); break; + case PropertyId::SenderId: self->config.id = g_value_dup_string(value); break; - case PropertyId::SenderLabel: self->config.sender_label = g_value_dup_string(value); break; + case PropertyId::SenderLabel: self->config.label = g_value_dup_string(value); break; - case PropertyId::SenderDescription: self->config.sender_description = g_value_dup_string(value); break; + case PropertyId::SenderDescription: self->config.description = g_value_dup_string(value); break; case PropertyId::SourceAddress: self->config.network.source_address = g_value_dup_string(value); break; case PropertyId::InterfaceName: self->config.network.interface_name = g_value_dup_string(value); - g_object_set(G_OBJECT(self->udpsink), "bind_address", self->config.network.interface_name.c_str(), NULL); + g_object_set(G_OBJECT(self->udpsink.get()), "bind_address", self->config.network.interface_name.c_str(), + nullptr); break; case PropertyId::DestinationAddress: self->config.network.destination_address = g_value_dup_string(value); - g_print("\n\n\n\n\n%s", self->config.network.destination_address.c_str()); - g_object_set(G_OBJECT(self->udpsink), "host", self->config.network.destination_address.c_str(), NULL); + g_object_set(G_OBJECT(self->udpsink.get()), "host", self->config.network.destination_address.c_str(), nullptr); break; case PropertyId::DestinationPort: - self->config.network.destination_port = g_value_dup_string(value); - g_object_set(G_OBJECT(self->udpsink), "port", atoi(self->config.network.destination_port.c_str()), NULL); + self->config.network.destination_port = atoi(g_value_get_string(value)); + g_object_set(G_OBJECT(self->udpsink.get()), "port", self->config.network.destination_port, nullptr); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID(object, property_id, pspec); break; @@ -140,15 +143,15 @@ static void gst_nmossender_get_property(GObject* object, guint property_id, GVal case PropertyId::DeviceId: g_value_set_string(value, self->config.device.id.c_str()); break; case PropertyId::DeviceLabel: g_value_set_string(value, self->config.device.label.c_str()); break; case PropertyId::DeviceDescription: g_value_set_string(value, self->config.device.description.c_str()); break; - case PropertyId::SenderId: g_value_set_string(value, self->config.sender_id.c_str()); break; - case PropertyId::SenderLabel: g_value_set_string(value, self->config.sender_label.c_str()); break; - case PropertyId::SenderDescription: g_value_set_string(value, self->config.sender_description.c_str()); break; + case PropertyId::SenderId: g_value_set_string(value, self->config.id.c_str()); break; + case PropertyId::SenderLabel: g_value_set_string(value, self->config.label.c_str()); break; + case PropertyId::SenderDescription: g_value_set_string(value, self->config.description.c_str()); break; case PropertyId::SourceAddress: g_value_set_string(value, self->config.network.source_address.c_str()); break; case PropertyId::InterfaceName: g_value_set_string(value, self->config.network.interface_name.c_str()); break; case PropertyId::DestinationAddress: g_value_set_string(value, self->config.network.destination_address.c_str()); break; - case PropertyId::DestinationPort: g_value_set_string(value, self->config.network.destination_port.c_str()); 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; } @@ -168,29 +171,13 @@ static gboolean gst_nmossender_sink_event(GstPad* pad, GstObject* parent, GstEve 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); std::string media_type = gst_structure_get_name(structure); if(media_type == "audio/x-raw") { - g_print("\n\n\n\n\n\n\n\n\n\n\n"); - // 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++) @@ -207,28 +194,44 @@ static gboolean gst_nmossender_sink_event(GstPad* pad, GstObject* parent, GstEve } if((format = gst_structure_get_string(structure, "format"))) { - self->config.audio_sender_fields.format = translate_audio_format(format); + self->config.audio_sender_fields.format = format; + } + } + if(self->config.audio_sender_fields.format == "S24BE") + { + if(gst_element_link(self->queue.get(), self->audio_payloader_24.get()) == false) + { + GST_ERROR_OBJECT(self, "Failed to link queue to audio_payloader"); + return false; + } + if(gst_element_link(self->audio_payloader_24.get(), self->udpsink.get()) == false) + { + GST_ERROR_OBJECT(self, "Failed to link audio_payloader_24 to udpsink"); + return false; + } + } + else + { + if(gst_element_link(self->queue.get(), self->audio_payloader_16.get()) == false) + { + GST_ERROR_OBJECT(self, "Failed to link queue to audio_payloader"); + return false; + } + if(gst_element_link(self->audio_payloader_16.get(), self->udpsink.get()) == false) + { + GST_ERROR_OBJECT(self, "Failed to link audio_payloader_16 to udpsink"); + return false; } } - // 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(media_type == "video/x-raw") { - - g_print("\n\n\n\n\n\n\n\n\n\n\n"); - // DEBUG - // g_print("Video caps detected\n"); - // GST_INFO_OBJECT(self, "Video caps detected"); - // - if(!gst_element_link(self->queue, self->video_payloader)) + if(gst_element_link(self->queue.get(), self->video_payloader.get()) == false) { GST_ERROR_OBJECT(self, "Failed to link queue to video_payloader"); return false; } - if(!gst_element_link(self->video_payloader, self->udpsink)) + if(gst_element_link(self->video_payloader.get(), self->udpsink.get()) == false) { GST_ERROR_OBJECT(self, "Failed to link video_payloader to udpsink"); return false; @@ -252,10 +255,6 @@ static gboolean gst_nmossender_sink_event(GstPad* pad, GstObject* parent, GstEve self->config.video_media_fields.sampling = translate_video_format(format); } } - - // 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 { @@ -292,7 +291,12 @@ static GstStateChangeReturn gst_nmossender_change_state(GstElement* element, Gst { case GST_STATE_CHANGE_PAUSED_TO_PLAYING: { - const auto node_config_json = create_node_config(self->config); + const auto node_config_json = create_node_config(self->config); + if(node_config_json == nullptr) + { + GST_ERROR_OBJECT(self, "Failed to initialize NMOS client. No valid node JSON location given."); + return GST_STATE_CHANGE_FAILURE; + } const auto device_config_json = create_device_config(self->config); nlohmann::json_abi_v3_11_3::json sender_config_json = nullptr; @@ -305,13 +309,8 @@ static GstStateChangeReturn gst_nmossender_change_state(GstElement* element, Gst 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()) + if(result.has_value() == false) { GST_ERROR_OBJECT(self, "Failed to initialize NMOS client. Node ID: %s", self->config.node.id.c_str()); return GST_STATE_CHANGE_FAILURE; @@ -335,7 +334,6 @@ static GstStateChangeReturn gst_nmossender_change_state(GstElement* element, Gst default: break; } - // This is needed to work ¯\_(ツ)_/¯ ret = GST_ELEMENT_CLASS(gst_nmossender_parent_class)->change_state(element, transition); return ret; @@ -394,28 +392,37 @@ static void gst_nmossender_class_init(GstNmossenderClass* klass) /* 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) + auto maybeQueue = GstElementHandle::create_element("queue", nullptr); + auto maybeVideoPay = GstElementHandle::create_element("rtpvrawpay", nullptr); + auto maybeAudioPay16 = GstElementHandle::create_element("rtpL16pay", nullptr); + auto maybeAudioPay24 = GstElementHandle::create_element("rtpL24pay", nullptr); + auto maybeUdpSink = GstElementHandle::create_element("udpsink", nullptr); + + if(std::holds_alternative(maybeQueue) || std::holds_alternative(maybeVideoPay) || + std::holds_alternative(maybeAudioPay16) || + std::holds_alternative(maybeAudioPay24) || std::holds_alternative(maybeUdpSink)) { - GST_ERROR_OBJECT(self, - "Failed to create internal elements: queue or video_payloader or audio_playloader or udpsink"); + GST_ERROR_OBJECT(self, "Failed to create pipeline elements."); return; } + self->queue = std::move(std::get>(maybeQueue)); + self->video_payloader = std::move(std::get>(maybeVideoPay)); + self->audio_payloader_16 = std::move(std::get>(maybeAudioPay16)); + self->audio_payloader_24 = std::move(std::get>(maybeAudioPay24)); + self->udpsink = std::move(std::get>(maybeUdpSink)); + // 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); - create_default_config_fields(&self->config); + g_object_set(G_OBJECT(self->queue.get()), "max-size-buffers", 1, nullptr); + g_object_set(G_OBJECT(self->udpsink.get()), "host", "127.0.0.1", "port", 9999, nullptr); + create_default_config_fields_sender(&self->config); - gst_bin_add_many(GST_BIN(self), self->queue, self->video_payloader, self->audio_payloader, self->udpsink, NULL); + gst_bin_add_many(GST_BIN(self), self->queue.get(), self->video_payloader.get(), self->audio_payloader_24.get(), + self->audio_payloader_16.get(), self->udpsink.get(), nullptr); // create and configure the sink pad - GstPad* queue_sinkpad = gst_element_get_static_pad(self->queue, "sink"); - if(!queue_sinkpad) + GstPad* queue_sinkpad = gst_element_get_static_pad(self->queue.get(), "sink"); + if(queue_sinkpad == nullptr) { GST_ERROR_OBJECT(self, "Failed to get static pad 'sink' from queue"); return; @@ -423,7 +430,7 @@ static void gst_nmossender_init(GstNmossender* self) GstPad* sink_ghost_pad = gst_ghost_pad_new("sink", queue_sinkpad); gst_object_unref(queue_sinkpad); - if(!sink_ghost_pad) + if(sink_ghost_pad == nullptr) { GST_ERROR_OBJECT(self, "Failed to create ghost pad for sink"); return; @@ -431,7 +438,7 @@ static void gst_nmossender_init(GstNmossender* self) gst_pad_set_event_function(sink_ghost_pad, gst_nmossender_sink_event); - if(!gst_element_add_pad(GST_ELEMENT(self), sink_ghost_pad)) + if(gst_element_add_pad(GST_ELEMENT(self), sink_ghost_pad) == false) { GST_ERROR_OBJECT(self, "Failed to add ghost pad to element; pad with same name might exist"); gst_object_unref(sink_ghost_pad); diff --git a/cpp/libs/gst_nmos_plugins/src/gst_nmos_video_receiver_plugin.cpp b/cpp/libs/gst_nmos_plugins/src/gst_nmos_video_receiver_plugin.cpp new file mode 100644 index 0000000..8b52ef2 --- /dev/null +++ b/cpp/libs/gst_nmos_plugins/src/gst_nmos_video_receiver_plugin.cpp @@ -0,0 +1,460 @@ + +/* 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/expected/macros.h" +#include "bisect/json.h" +#include "bisect/sdp.h" +#include "bisect/sdp/reader.h" +#include "bisect/nmoscpp/configuration.h" +#include "utils.h" +#include "ossrf/nmos/api/nmos_client.h" +#include "gst_nmos_plugins/include/element_class.h" +#include "gst_nmos_plugins/include/nmos_configuration.h" +#include +#include + +using namespace bisect; +using namespace bisect::sdp; + +GST_DEBUG_CATEGORY_STATIC(gst_nmosvideoreceiver_debug_category); +#define GST_CAT_DEFAULT gst_nmosvideoreceiver_debug_category +#define GST_TYPE_NMOSVIDEORECEIVER (gst_nmosvideoreceiver_get_type()) +#define GST_NMOSVIDEORECEIVER(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), GST_TYPE_NMOSVIDEORECEIVER, GstNmosvideoreceiver)) +#define GST_NMOSVIDEORECEIVER_CLASS(klass) \ + (G_TYPE_CHECK_CLASS_CAST((klass), GST_TYPE_NMOSVIDEORECEIVER, GstNmosvideoreceiverClass)) +#define GST_IS_NMOSVIDEORECEIVER(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), GST_TYPE_NMOSVIDEORECEIVER)) +#define GST_IS_NMOSVIDEORECEIVER_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), GST_TYPE_NMOSVIDEORECEIVER)) + +typedef struct _GstNmosvideoreceiver +{ + GstBin parent; + GstPad* element_pad; + GstPad* bin_pad; + GstClock* clock; + GstElementHandle<_GstElement> bin; + GstElementHandle<_GstElement> udp_src; + GstElementHandle<_GstElement> rtp_video_depay; + GstElementHandle<_GstElement> rtp_jitter_buffer; + GstElementHandle<_GstElement> queue; + ossrf::nmos_client_uptr client; + config_fields_t config; + std::string sdp_string; + sdp_settings_t sdp_settings; + bool nmos_active; +} GstNmosvideoreceiver; + +typedef struct _GstNmosvideoreceiverClass +{ + GstBinClass parent_class; +} GstNmosvideoreceiverClass; + +G_DEFINE_TYPE_WITH_CODE(GstNmosvideoreceiver, gst_nmosvideoreceiver, GST_TYPE_BIN, + GST_DEBUG_CATEGORY_INIT(gst_nmosvideoreceiver_debug_category, "nmosvideoreceiver", 0, + "NMOS Receiver Plugin")) +// Pad template +static GstStaticPadTemplate src_template = + GST_STATIC_PAD_TEMPLATE("src", GST_PAD_SRC, GST_PAD_ALWAYS, + GST_STATIC_CAPS("video/x-raw, " + "format=(string){ I420, NV12, RGB, YUV422P, UYVP }; " + "application/x-rtp, " + "media=(string)video, " + "clock-rate=(int)90000")); + +enum class PropertyId : uint32_t +{ + NodeId = 1, + NodeConfigFileLocation = 2, + DeviceId = 3, + DeviceLabel = 4, + DeviceDescription = 5, + ReceiverId = 6, + ReceiverLabel = 7, + ReceiverDescription = 8, + DstAddress = 9 +}; + +// Set properties so element variables can change depending on them +static void gst_nmosvideoreceiver_set_property(GObject* object, guint property_id, const GValue* value, + GParamSpec* pspec) +{ + GstNmosvideoreceiver* self = GST_NMOSVIDEORECEIVER(object); + + switch(static_cast(property_id)) + { + case PropertyId::NodeId: self->config.node.id = g_value_dup_string(value); break; + + case PropertyId::NodeConfigFileLocation: + self->config.node.configuration_location = g_value_dup_string(value); + break; + + case PropertyId::DeviceId: self->config.device.id = g_value_dup_string(value); break; + + case PropertyId::DeviceLabel: self->config.device.label = g_value_dup_string(value); break; + + case PropertyId::DeviceDescription: self->config.device.description = g_value_dup_string(value); break; + + case PropertyId::ReceiverId: self->config.id = g_value_dup_string(value); break; + + case PropertyId::ReceiverLabel: self->config.label = g_value_dup_string(value); break; + + case PropertyId::ReceiverDescription: self->config.description = g_value_dup_string(value); break; + + case PropertyId::DstAddress: self->config.address = g_value_dup_string(value); break; + + default: G_OBJECT_WARN_INVALID_PROPERTY_ID(object, property_id, pspec); break; + } +} + +static void gst_nmosvideoreceiver_get_property(GObject* object, guint property_id, GValue* value, GParamSpec* pspec) +{ + GstNmosvideoreceiver* self = GST_NMOSVIDEORECEIVER(object); + + switch(static_cast(property_id)) + { + + case PropertyId::NodeId: g_value_set_string(value, self->config.node.id.c_str()); break; + case PropertyId::NodeConfigFileLocation: + g_value_set_string(value, self->config.node.configuration_location.c_str()); + break; + case PropertyId::DeviceId: g_value_set_string(value, self->config.device.id.c_str()); break; + case PropertyId::DeviceLabel: g_value_set_string(value, self->config.device.label.c_str()); break; + case PropertyId::DeviceDescription: g_value_set_string(value, self->config.device.description.c_str()); break; + case PropertyId::ReceiverId: g_value_set_string(value, self->config.id.c_str()); break; + case PropertyId::ReceiverLabel: g_value_set_string(value, self->config.label.c_str()); break; + case PropertyId::ReceiverDescription: g_value_set_string(value, self->config.description.c_str()); break; + case PropertyId::DstAddress: g_value_set_string(value, self->config.address.c_str()); break; + + default: G_OBJECT_WARN_INVALID_PROPERTY_ID(object, property_id, pspec); break; + } +} + +static GstPadProbeReturn block_pad_probe_cb(GstPad* pad, GstPadProbeInfo* info, gpointer user_data) +{ + return GST_PAD_PROBE_DROP; +} + +void remove_old_bin(GstNmosvideoreceiver* self) +{ + if(self->element_pad == nullptr) + { + return; + } + gulong block_id = + gst_pad_add_probe(self->element_pad, GST_PAD_PROBE_TYPE_BLOCK_DOWNSTREAM, block_pad_probe_cb, NULL, NULL); + + gst_element_send_event(GST_ELEMENT(self), gst_event_new_flush_start()); + gst_element_send_event(GST_ELEMENT(self), gst_event_new_flush_stop(FALSE)); + + self->clock = gst_element_get_clock(GST_ELEMENT(self)); + + gst_element_set_state(self->bin.get(), GST_STATE_NULL); + + gst_bin_remove(GST_BIN(self), self->udp_src.get()); + gst_bin_remove(GST_BIN(self), self->rtp_jitter_buffer.get()); + gst_bin_remove(GST_BIN(self), self->rtp_video_depay.get()); + gst_bin_remove(GST_BIN(self), self->queue.get()); + + gst_bin_remove(GST_BIN(self), self->bin.get()); + + self->udp_src.forget(); + self->queue.forget(); + self->rtp_jitter_buffer.forget(); + self->rtp_video_depay.forget(); + self->bin.forget(); + + if(block_id != 0) + { + gst_pad_remove_probe(self->element_pad, block_id); + } +} + +void construct_pipeline(GstNmosvideoreceiver* self) +{ + auto format = self->sdp_settings.format; + try + { + bisect::nmoscpp::video_sender_info_t video_info = std::get(format); + auto& fr = video_info.exact_framerate; + fmt::print("\n===== SDP Settings =====\n" + "Video (from variant):\n" + " - height: %d\n" + " - width: %d\n" + " - framerate: %ld/%ld\n" + " - chroma_sub_sampling: %s\n" + " - structure (interlace mode): %s\n" + " - depth: %d\n" + "Primary leg:\n" + " - destination_ip: %s\n" + " - destination_port: %d\n" + "\n", + video_info.height, video_info.width, fr.numerator(), fr.denominator(), + video_info.chroma_sub_sampling.c_str(), video_info.structure.name.c_str(), video_info.depth, + self->sdp_settings.primary.destination_ip.value().c_str(), + self->sdp_settings.primary.destination_port.value()); + + auto maybeBin = GstElementHandle::create_bin("dynamic-bin"); + if(std::holds_alternative(maybeBin)) + { + GST_ERROR_OBJECT(self, "Failed to create bin"); + return; + } + + self->bin = std::move(std::get>(maybeBin)); + + auto maybeUdpsrc = GstElementHandle::create_element("udpsrc", nullptr); + auto maybeQueue = GstElementHandle::create_element("queue", nullptr); + auto maybeJitter = GstElementHandle::create_element("rtpjitterbuffer", nullptr); + auto maybeDepay = GstElementHandle::create_element("rtpvrawdepay", nullptr); + + if(std::holds_alternative(maybeUdpsrc) || std::holds_alternative(maybeQueue) || + std::holds_alternative(maybeJitter) || std::holds_alternative(maybeDepay)) + { + GST_ERROR_OBJECT(self, "Failed to create pipeline elements."); + return; + } + + self->udp_src = std::move(std::get>(maybeUdpsrc)); + self->queue = std::move(std::get>(maybeQueue)); + self->rtp_jitter_buffer = std::move(std::get>(maybeJitter)); + self->rtp_video_depay = std::move(std::get>(maybeDepay)); + + g_object_set(G_OBJECT(self->udp_src.get()), "address", + self->sdp_settings.primary.destination_ip.value().c_str(), "port", + self->sdp_settings.primary.destination_port.value(), "buffer-size", 67108864, nullptr); + + GstCaps* caps = gst_caps_new_simple( + "application/x-rtp", "media", G_TYPE_STRING, "video", "clock-rate", G_TYPE_INT, 90000, "encoding-name", + G_TYPE_STRING, "RAW", "payload", G_TYPE_INT, 96, "width", G_TYPE_STRING, + std::to_string(video_info.width).c_str(), "height", G_TYPE_STRING, + std::to_string(video_info.height).c_str(), "format", G_TYPE_STRING, "UYVP", "sampling", G_TYPE_STRING, + video_info.chroma_sub_sampling.c_str(), "depth", G_TYPE_STRING, std::to_string(video_info.depth).c_str(), + "framerate", GST_TYPE_FRACTION, fr.numerator(), fr.denominator(), nullptr); + + g_object_set(G_OBJECT(self->udp_src.get()), "caps", caps, nullptr); + + g_object_set(G_OBJECT(self->rtp_jitter_buffer.get()), "do-retransmission", true, "do-lost", true, "mode", 2, + "latency", 0, nullptr); + + g_object_set(G_OBJECT(self->queue.get()), "max-size-buffers", 3, nullptr); + + gst_bin_add_many(GST_BIN(self->bin.get()), self->udp_src.get(), self->rtp_jitter_buffer.get(), + self->queue.get(), self->rtp_video_depay.get(), nullptr); + + gst_element_sync_state_with_parent(self->udp_src.get()); + gst_element_sync_state_with_parent(self->rtp_jitter_buffer.get()); + gst_element_sync_state_with_parent(self->queue.get()); + gst_element_sync_state_with_parent(self->rtp_video_depay.get()); + + if(gst_element_link_many(self->udp_src.get(), self->rtp_jitter_buffer.get(), self->queue.get(), + self->rtp_video_depay.get(), nullptr) == false) + { + GST_ERROR_OBJECT(self, "Failed to link elements inside the bin."); + return; + } + + // Create a ghost pad from the depay's src + GstPad* bin_src_pad = gst_element_get_static_pad(self->rtp_video_depay.get(), "src"); + if(bin_src_pad == nullptr) + { + GST_ERROR_OBJECT(self, "Failed to get src pad from rtp_video_depay."); + return; + } + + GstPad* bin_ghost_pad = gst_ghost_pad_new("src", bin_src_pad); + gst_object_unref(bin_src_pad); + self->bin_pad = bin_ghost_pad; + + if(bin_ghost_pad == nullptr || gst_element_add_pad(self->bin.get(), bin_ghost_pad) == false) + { + GST_ERROR_OBJECT(self, "Failed to create or add ghost pad to bin."); + return; + } + + gst_bin_add(GST_BIN(self), self->bin.get()); + + GstPad* plugin_pad = gst_element_get_static_pad(GST_ELEMENT(self), "src"); + if(plugin_pad == nullptr) + { + GST_ERROR_OBJECT(self, "Failed to get plugin ghost pad."); + return; + } + + self->element_pad = plugin_pad; + + if(gst_ghost_pad_set_target(GST_GHOST_PAD(self->element_pad), bin_ghost_pad) == false) + { + GST_ERROR_OBJECT(self, "Failed to link plugin ghost pad to bin's ghost pad."); + return; + } + + gst_object_unref(plugin_pad); + } + catch(std::bad_variant_access const& ex) + { + fmt::print("Invalid format sent for current receiver.\n"); + } +} + +void create_nmos(GstNmosvideoreceiver* self) +{ + const auto node_config_json = create_node_config(self->config); + if(node_config_json == nullptr) + { + GST_ERROR_OBJECT(self, "Failed to initialize NMOS client. No valid node JSON location given."); + return; + } + const auto device_config_json = create_device_config(self->config); + nlohmann::json_abi_v3_11_3::json receiver_config_json = nullptr; + + receiver_config_json = create_receiver_config(self->config); + + auto receiver_callback = [self](const std::optional& sdp, bool master_enabled) { + fmt::print("Receiver Activation Callback: SDP={}, Master Enabled={}\n", sdp.has_value() ? sdp.value() : "None", + master_enabled); + + if(sdp) + { + fmt::print("Received SDP: {}\n", sdp.value()); + auto sdp_settings = parse_sdp(sdp.value()); + if(sdp_settings.has_value() && sdp.value() != self->sdp_string) + { + self->sdp_settings = sdp_settings.value(); + self->sdp_string = sdp.value(); + remove_old_bin(self); + construct_pipeline(self); + auto time = gst_element_get_base_time(GST_ELEMENT(self)); + gst_element_set_state(GST_ELEMENT(self), GST_STATE_PLAYING); + gst_pad_set_offset(self->bin_pad, (long)(gst_clock_get_time(self->clock) - time)); + } + } + else + { + fmt::print("No SDP provided.\n"); + } + fmt::print("Master enabled: {}\n", master_enabled); + }; + + 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."); + return; + } + self->client = std::move(result.value()); + self->client->add_device(device_config_json.dump()); + self->client->add_receiver(self->config.device.id, receiver_config_json.dump(), receiver_callback); + GST_INFO_OBJECT(self, "NMOS client initialized successfully."); +} + +static GstStateChangeReturn gst_nmosvideoreceiver_change_state(GstElement* element, GstStateChange transition) +{ + GstStateChangeReturn ret; + GstNmosvideoreceiver* self = GST_NMOSVIDEORECEIVER(element); + + switch(transition) + { + case GST_STATE_CHANGE_NULL_TO_READY: { + if(self->nmos_active != true) + { + create_nmos(self); + self->nmos_active = true; + } + } + break; + + default: break; + } + + ret = GST_ELEMENT_CLASS(gst_nmosvideoreceiver_parent_class)->change_state(element, transition); + + return ret; +} + +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_nmosvideoreceiver_class_init(GstNmosvideoreceiverClass* 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(&src_template)); + + object_class->set_property = gst_nmosvideoreceiver_set_property; + object_class->get_property = gst_nmosvideoreceiver_get_property; + + add_property(object_class, 1, "node-id", "Node ID", "The ID of the node", "d49c85db-1c33-4f21-b160-58edd2af1810"); + 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/config/nmos_plugin_node_config_receiver.json"); + add_property(object_class, 3, "device-id", "Device ID", "The ID of the device", + "1ad20d7c-8c58-4c84-8f14-3cf3e3af164c"); + 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, "receiver-id", "Receiver ID", "UUID of the receiver", + "db9f46cf-2414-4e25-b6c6-2078159857f9"); + add_property(object_class, 7, "receiver-label", "Label of the receiver", "Label of the receiver", + "BISECT OSSRF Video Receiver"); + add_property(object_class, 8, "receiver-description", "Description of the receiver", "Description of the receiver", + "BISECT OSSRF Video Receiver"); + add_property(object_class, 9, "destination-address", "Destination Address", "Address of the destination", + "127.0.0.1"); + + gst_element_class_set_static_metadata(element_class, "NMOS Video Receiver", "Source/Network", + "Receives raw video from NMOS", "Luis Ferreira "); + + element_class->change_state = gst_nmosvideoreceiver_change_state; +} + +// Object initialization +static void gst_nmosvideoreceiver_init(GstNmosvideoreceiver* self) +{ + GstPadTemplate* src_tmpl = gst_static_pad_template_get(&src_template); + GstPad* ghost_src = gst_ghost_pad_new_no_target_from_template("src", src_tmpl); + gst_object_unref(src_tmpl); + gst_element_add_pad(GST_ELEMENT(self), ghost_src); + + create_default_config_fields_video_receiver(&self->config); +} + +static gboolean plugin_init(GstPlugin* plugin) +{ + return gst_element_register(plugin, "nmosvideoreceiver", GST_RANK_NONE, GST_TYPE_NMOSVIDEORECEIVER); +} + +#define VERSION "0.1" +#define PACKAGE "gst-nmos-video-receiver-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, nmosvideoreceiver, + "Plugin to receive video stream from NMOS application", 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_plugins/src/utils.cpp similarity index 59% rename from cpp/libs/gst_nmos_sender_plugin/utils.cpp rename to cpp/libs/gst_nmos_plugins/src/utils.cpp index 5513e0c..d8a9b0b 100644 --- a/cpp/libs/gst_nmos_sender_plugin/utils.cpp +++ b/cpp/libs/gst_nmos_plugins/src/utils.cpp @@ -4,12 +4,15 @@ #include "bisect/expected/macros.h" #include "bisect/expected.h" #include "bisect/json.h" +#include "gst_nmos_plugins/include/element_class.h" +#include "gst_nmos_plugins/include/nmos_configuration.h" #include using namespace bisect; using json = nlohmann::json; -void create_default_config_fields(config_fields_t* config) + +void create_default_config_fields_sender(config_fields_t* config) { if(!config) { @@ -20,7 +23,7 @@ void create_default_config_fields(config_fields_t* config) // Initialize node_fields_t config->node.id = "d5504cd1-fe68-489d-99d4-20d3f075f062"; config->node.configuration_location = - "/home/nmos/repos/nmos-sender-receiver-framework/cpp/demos/config/nmos_plugin_node_config.json"; + "/home/nmos/repos/nmos-sender-receiver-framework/cpp/demos/config/nmos_plugin_node_config_sender.json"; // Initialize device_fields_t config->device.id = "e92e628b-7421-4723-9fb9-c1f3b38af9d3"; @@ -45,25 +48,82 @@ void create_default_config_fields(config_fields_t* config) config->network.source_address = "192.168.1.120"; config->network.interface_name = "wlp1s0"; config->network.destination_address = "192.168.1.120"; - config->network.destination_port = "9999"; + config->network.destination_port = 9999; // Initialize config_fields_t - config->sender_id = "1c920570-e0b4-4637-b02c-26c9d4275c71"; - config->sender_label = "BISECT OSSRF Media Sender"; - config->sender_description = "BISECT OSSRF Media Sender"; + config->id = "1c920570-e0b4-4637-b02c-26c9d4275c71"; + config->label = "BISECT OSSRF Media Sender"; + config->description = "BISECT OSSRF Media Sender"; } -json create_node_config(config_fields_t& config) +void create_default_config_fields_video_receiver(config_fields_t* config) +{ + if(!config) + { + g_critical("Config pointer is NULL"); + return; + } + + // Initialize node_fields_t + config->node.id = "d49c85db-1c33-4f21-b160-58edd2af1810"; + config->node.configuration_location = + "/home/nmos/repos/nmos-sender-receiver-framework/cpp/demos/config/nmos_plugin_node_config_receiver.json"; + + // Initialize device_fields_t + config->device.id = "1ad20d7c-8c58-4c84-8f14-3cf3e3af164c"; + config->device.label = "OSSRF Device2"; + config->device.description = "OSSRF Device2"; + + // Initialize config_fields_t + config->id = "db9f46cf-2414-4e25-b6c6-2078159857f9"; + config->label = "BISECT OSSRF Video Receiver"; + config->description = "BISECT OSSRF Video Receiver"; + config->interface_name = "wlp1s0"; + config->address = "192.168.1.36"; + config->is_audio = false; +} + +void create_default_config_fields_audio_receiver(config_fields_t* config) { - const std::string node_config_str = get_node_config(config.node.configuration_location.data()); + if(!config) + { + g_critical("Config pointer is NULL"); + return; + } - // Parse the configuration into a JSON object - json node_config = json::parse(node_config_str); + // Initialize node_fields_t + config->node.id = "d1073007-4099-452f-b8e8-837b8987d845"; + config->node.configuration_location = + "/home/nmos/repos/nmos-sender-receiver-framework/cpp/demos/config/nmos_plugin_node_config_receiver.json"; - // Override the "id" field with the one from the config_fields_t structure - node_config["id"] = config.node.id; + // Initialize device_fields_t + config->device.id = "093a1c56-4033-49f7-9e17-fbcb21a19383"; + config->device.label = "OSSRF Device2"; + config->device.description = "OSSRF Device2"; - return node_config; + // Initialize config_fields_t + config->id = "af600517-a452-4c2c-8b60-2caa770d6435"; + config->label = "BISECT OSSRF Audio Receiver"; + config->description = "BISECT OSSRF Audio Receiver"; + config->interface_name = "wlp1s0"; + config->address = "192.168.1.36"; + config->is_audio = true; +} + +json create_node_config(config_fields_t& config) +{ + const std::string node_config_str = get_node_config(config.node.configuration_location); + if(node_config_str != "") + { + json node_config = json::parse(node_config_str); + node_config["id"] = config.node.id; + + return node_config; + } + else + { + return nullptr; + } } json create_device_config(config_fields_t& config) @@ -73,54 +133,25 @@ json create_device_config(config_fields_t& config) return device; } -json create_video_sender_config(config_fields_t& config) +json create_receiver_config(config_fields_t& config) { + std::string caps = "video/raw"; + if(config.is_audio == true){ + caps = "audio/L24"; + } 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_den}}}, - {"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}}}}; + {"id", config.id}, + {"label", config.label}, + {"description", config.description}, + {"network", {{"primary", {{"interface_address", config.address}, {"interface_name", config.interface_name}}}}}, + {"capabilities", {caps}}}; 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"}, {"RGB", "RGB-8:8:8"}, {"RGBA", "RGBA-8:8:8:8"}}; + static const std::unordered_map video_format_map = {{"UYVP", "YCbCr-4:2:2"}, + {"RGBA", "RGBA-8:8:8:8"}}; auto it = video_format_map.find(gst_format); if(it != video_format_map.end()) @@ -175,27 +206,29 @@ std::string get_node_id(char* node_configuration_location) return ""; } -std::string get_node_config(char* node_configuration_location) +std::string get_node_config(std::string node_configuration_location) { - const auto configuration_result = load_configuration_from_file(node_configuration_location); + const auto configuration_result = load_configuration_from_file(node_configuration_location.c_str()); - const json& configuration = configuration_result.value(); + if(configuration_result.has_value()) + { + const json& configuration = configuration_result.value(); - auto node_result = find(configuration, "node"); + auto node_result = find(configuration, "node"); - const json& node = node_result.value(); + const json& node = node_result.value(); - auto node_id_result = find(node, "id"); - auto node_config_result = find(node, "configuration"); + 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(); + 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 node_configuration; + } } - return ""; } @@ -242,3 +275,57 @@ std::string get_sender_config(char* sender_configuration_location) return sender_config.value().dump(); } + + +json create_video_sender_config(config_fields_t& config) +{ + json sender = { + {"id", config.id}, + {"label", config.label}, + {"description", config.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_den}}}, + {"sampling", config.video_media_fields.sampling}, + {"structure", config.video_media_fields.structure}}}}; + return sender; +} + +json create_audio_sender_config(config_fields_t& config) +{ + std::string media_type = "audio/L16"; + if(config.audio_sender_fields.format == "S24BE") + { + media_type = "audio/L24"; + } + else if(config.audio_sender_fields.format == "S16BE") + { + media_type = "audio/L16"; + } + json sender = {{"id", config.id}, + {"label", config.label}, + {"description", config.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", media_type}, + {"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; +} diff --git a/cpp/libs/gst_nmos_plugins/src/utils.h b/cpp/libs/gst_nmos_plugins/src/utils.h new file mode 100644 index 0000000..10ef2f1 --- /dev/null +++ b/cpp/libs/gst_nmos_plugins/src/utils.h @@ -0,0 +1,33 @@ +#pragma once +#include +#include +#include +#include +#include +#include "../include/element_class.h" +#include "../include/nmos_configuration.h" + +void create_default_config_fields_sender(config_fields_t* config); +void create_default_config_fields_video_receiver(config_fields_t* config); +void create_default_config_fields_audio_receiver(config_fields_t* config); + +nlohmann::json create_node_config(config_fields_t& config); +nlohmann::json create_device_config(config_fields_t& config); +nlohmann::json create_receiver_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(std::string 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); + +nlohmann::json create_video_sender_config(config_fields_t& config); +nlohmann::json create_audio_sender_config(config_fields_t& config); diff --git a/cpp/libs/gst_nmos_sender_plugin/CMakeLists.txt b/cpp/libs/gst_nmos_sender_plugin/CMakeLists.txt deleted file mode 100644 index c51e312..0000000 --- a/cpp/libs/gst_nmos_sender_plugin/CMakeLists.txt +++ /dev/null @@ -1,67 +0,0 @@ -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/utils.h b/cpp/libs/gst_nmos_sender_plugin/utils.h deleted file mode 100644 index b84a8a2..0000000 --- a/cpp/libs/gst_nmos_sender_plugin/utils.h +++ /dev/null @@ -1,77 +0,0 @@ -#pragma once -#include -#include -#include - -typedef struct node_fields_t -{ - std::string id; - std::string configuration_location; -} node_fields_t; - -typedef struct device_fields_t -{ - std::string id; - std::string label; - std::string description; -} device_fields_t; - -typedef struct video_media_fields_t -{ - gint width; - gint height; - gint frame_rate_num; - gint frame_rate_den; - std::string sampling; - std::string structure; -} video_media_fields_t; - -typedef struct audio_media_fields_t -{ - std::string format; - gint number_of_channels; - gint sampling_rate; - gint packet_time; -} audio_media_fields_t; - -typedef struct network_fields_t -{ - std::string source_address; - std::string interface_name; - std::string destination_address; - std::string 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; - std::string sender_id; - std::string sender_label; - std::string sender_description; - network_fields_t network; -} config_fields_t; - -void create_default_config_fields(config_fields_t* config); - -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/cpp/libs/ossrf_nmos_api/lib/src/serialization/media_types.h b/cpp/libs/ossrf_nmos_api/lib/src/serialization/media_types.h index 62d0f3a..09ced69 100644 --- a/cpp/libs/ossrf_nmos_api/lib/src/serialization/media_types.h +++ b/cpp/libs/ossrf_nmos_api/lib/src/serialization/media_types.h @@ -20,4 +20,5 @@ namespace ossrf::media_types { constexpr std::string_view VIDEO_RAW = "video/raw"; constexpr std::string_view AUDIO_L24 = "audio/L24"; + constexpr std::string_view AUDIO_L16 = "audio/L16"; } // namespace ossrf::media_types diff --git a/cpp/libs/ossrf_nmos_api/lib/src/serialization/receiver.cpp b/cpp/libs/ossrf_nmos_api/lib/src/serialization/receiver.cpp index d6e9db3..cbbd00a 100644 --- a/cpp/libs/ossrf_nmos_api/lib/src/serialization/receiver.cpp +++ b/cpp/libs/ossrf_nmos_api/lib/src/serialization/receiver.cpp @@ -44,7 +44,20 @@ namespace else if(media_type == "audio/L24") { receiver.media_types = {nmos::media_types::audio_L24}; - receiver.format = nmos::formats::audio; + if(c[1] != "") + { + receiver.media_types.push_back(nmos::media_types::audio_L(16)); + } + receiver.format = nmos::formats::audio; + } + else if(media_type == "audio/L16") + { + receiver.media_types = {nmos::media_types::audio_L(16)}; + if(c[1] != "") + { + receiver.media_types.push_back(nmos::media_types::audio_L24); + } + receiver.format = nmos::formats::audio; } else { diff --git a/cpp/libs/ossrf_nmos_api/lib/src/serialization/sender.cpp b/cpp/libs/ossrf_nmos_api/lib/src/serialization/sender.cpp index 51942e7..8183ff5 100644 --- a/cpp/libs/ossrf_nmos_api/lib/src/serialization/sender.cpp +++ b/cpp/libs/ossrf_nmos_api/lib/src/serialization/sender.cpp @@ -65,6 +65,16 @@ namespace return info; } + expected audio_l16_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 = 16; + 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); @@ -125,6 +135,13 @@ expected ossrf::nmos_sender_from_json(const json& config) sender.format = nmos::formats::audio; sender.grain_rate = info.sampling_rate; } + else if(sender.media_type == media_types::AUDIO_L16) + { + BST_ASSIGN(info, audio_l16_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);