From 3fe9e4a207f8da5d5f3996160a09a12ed19a2a3c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Dr=C3=B6ge?= Date: Wed, 12 Oct 2022 21:14:45 +0300 Subject: [PATCH] ndi: Implement dynamic loading of the NDI SDK And build the plugin on the CI and via meson. --- Cargo.toml | 1 + README.md | 1 + meson.build | 1 + net/ndi/Cargo.toml | 3 +- net/ndi/README.md | 63 ++---- net/ndi/src/device_provider/imp.rs | 4 + net/ndi/src/lib.rs | 4 - net/ndi/src/ndi.rs | 4 +- net/ndi/src/ndisink/imp.rs | 17 ++ net/ndi/src/ndisrc/imp.rs | 6 + net/ndi/src/ndisys.rs | 344 ++++++++++++++++++++++------- 11 files changed, 321 insertions(+), 127 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 78574627..711e9c4e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -57,6 +57,7 @@ default-members = [ "net/reqwest", "net/rtpav1", "net/webrtc-http", + "net/ndi", "text/ahead", "text/json", "text/regex", diff --git a/README.md b/README.md index aea88723..8a341e8d 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,7 @@ You will find the following plugins in this repository: - `aws_transcriber`: an element wrapping the AWS Transcriber service. - `raptorq`: Encoder/decoder element for RaptorQ RTP FEC mechanism. + - `ndi`: An [NDI](https://www.newtek.com/ndi/) plugin containing a source, sink and device provider. * `audio` - `audiofx`: Elements to apply audio effects to a stream diff --git a/meson.build b/meson.build index 3d8ac24f..c9d10bf5 100644 --- a/meson.build +++ b/meson.build @@ -69,6 +69,7 @@ plugins = { 'gst-plugin-tracers': 'libgstrstracers', 'gst-plugin-webrtchttp': 'libgstwebrtchttp', 'gst-plugin-rtpav1': 'libgstrtpav1', + 'gst-plugin-ndi': 'libgstndi', } extra_env = {} diff --git a/net/ndi/Cargo.toml b/net/ndi/Cargo.toml index d0b1c39e..c9a20441 100644 --- a/net/ndi/Cargo.toml +++ b/net/ndi/Cargo.toml @@ -18,6 +18,7 @@ byte-slice-cast = "1" once_cell = "1.0" byteorder = "1.0" atomic_refcell = "0.1" +libloading = "0.7" [build-dependencies] gst-plugin-version-helper = { path = "../../version-helper" } @@ -47,4 +48,4 @@ install_subdir = "gstreamer-1.0" versioning = false [package.metadata.capi.pkg_config] -requires_private = "gstreamer-1.0, gstreamer-base-1.0, gstreamer-audio-1.0, gstreamer-video-1.0, gobject-2.0, glib-2.0, gmodule-2.0, ndi" +requires_private = "gstreamer-1.0, gstreamer-base-1.0, gstreamer-audio-1.0, gstreamer-video-1.0, gobject-2.0, glib-2.0, gmodule-2.0" diff --git a/net/ndi/README.md b/net/ndi/README.md index 2446cae3..c783e336 100644 --- a/net/ndi/README.md +++ b/net/ndi/README.md @@ -1,11 +1,23 @@ -GStreamer NDI Plugin for Linux +GStreamer NDI Plugin ==================== -*Compiled and tested with NDI SDK 4.0, 4.1 and 5.0* +*Compatible with NDI SDK 5.x* -This is a plugin for the [GStreamer](https://gstreamer.freedesktop.org/) multimedia framework that allows GStreamer to receive a stream from a [NDI](https://www.newtek.com/ndi/) source. This plugin has been developed by [Teltek](http://teltek.es/) and was funded by the [University of the Arts London](https://www.arts.ac.uk/) and [The University of Manchester](https://www.manchester.ac.uk/). +This is a plugin for the [GStreamer](https://gstreamer.freedesktop.org/) +multimedia framework that allows GStreamer to receive or send an +[NDI](https://www.newtek.com/ndi/) stream. -Currently the plugin has a source element for receiving from NDI sources, a sink element to provide an NDI source and a device provider for discovering NDI sources on the network. +This plugin has been initially developed by [Teltek](http://teltek.es/) and +was funded by the [University of the Arts London](https://www.arts.ac.uk/) and +[The University of Manchester](https://www.manchester.ac.uk/). + +Currently the plugin has a source element for receiving from NDI sources, a +sink element to provide an NDI source and a device provider for discovering +NDI sources on the network. + +The plugin is loading the NDI SDK at runtime, either from the default library +path or, if set, from the directory given by the `NDI_RUNTIME_DIR_V5` +environment variable. Some examples of how to use these elements from the command line: @@ -26,55 +38,14 @@ $ gst-launch-1.0 videotestsrc is-live=true ! video/x-raw,format=UYVY ! ndisinkco ``` Feel free to contribute to this project. Some ways you can contribute are: + * Testing with more hardware and software and reporting bugs * Doing pull requests. -Compilation of the NDI element -------- -To compile the NDI element it's necessary to install Rust, the NDI SDK and the following packages for gstreamer: - -```console -$ apt-get install libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev \ - gstreamer1.0-plugins-base - -``` -To install the required NDI library there are two options: -1. Download NDI SDK from NDI website and move the library to the correct location. -2. Use a [deb package](https://github.com/Palakis/obs-ndi/releases/download/4.5.2/libndi3_3.5.1-1_amd64.deb) made by the community. Thanks to [NDI plugin for OBS](https://github.com/Palakis/obs-ndi). - -To install Rust, you can follow their documentation: https://www.rust-lang.org/en-US/install.html - -Once all requirements are met, you can build the plugin by executing the following command from the project root folder: - -``` -cargo build -export GST_PLUGIN_PATH=`pwd`/target/debug -gst-inspect-1.0 ndi -``` - -By default GStreamer 1.18 is required, to use an older version. You can build with `$ cargo build --no-default-features --features whatever_you_want_to_enable_of_the_above_features` - - -If all went ok, you should see info related to the NDI element. To make the plugin available without using `GST_PLUGIN_PATH` it's necessary to copy the plugin to the gstreamer plugins folder. - -```console -$ cargo build --release -$ sudo install -o root -g root -m 644 target/release/libgstndi.so /usr/lib/x86_64-linux-gnu/gstreamer-1.0/ -$ sudo ldconfig -$ gst-inspect-1.0 ndi -``` - -More info about GStreamer plugins written in Rust: ----------------------------------- -https://gitlab.freedesktop.org/gstreamer/gstreamer-rs -https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs - - License ------- This plugin is licensed under the MPL-2 - see the [LICENSE](LICENSE-MPL-2.0) file for details - Acknowledgments ------- * University of the Arts London and The University of Manchester. diff --git a/net/ndi/src/device_provider/imp.rs b/net/ndi/src/device_provider/imp.rs index 6fba8c73..7cc4bd69 100644 --- a/net/ndi/src/device_provider/imp.rs +++ b/net/ndi/src/device_provider/imp.rs @@ -72,6 +72,10 @@ impl DeviceProviderImpl for DeviceProvider { } fn start(&self) -> Result<(), gst::LoggableError> { + if let Err(err) = crate::ndi::load() { + return Err(gst::loggable_error!(CAT, "{}", err)); + } + let mut thread_guard = self.thread.lock().unwrap(); let device_provider = self.instance(); if thread_guard.is_some() { diff --git a/net/ndi/src/lib.rs b/net/ndi/src/lib.rs index 2a168b8f..cf4ce9bb 100644 --- a/net/ndi/src/lib.rs +++ b/net/ndi/src/lib.rs @@ -118,10 +118,6 @@ impl From for NDIlib_recv_color_format_e { } fn plugin_init(plugin: &gst::Plugin) -> Result<(), glib::BoolError> { - if !ndi::initialize() { - return Err(glib::bool_error!("Cannot initialize NDI")); - } - device_provider::register(plugin)?; ndisrc::register(plugin)?; diff --git a/net/ndi/src/ndi.rs b/net/ndi/src/ndi.rs index b9fe48bd..ecbc648c 100644 --- a/net/ndi/src/ndi.rs +++ b/net/ndi/src/ndi.rs @@ -9,8 +9,8 @@ use std::ptr; use byte_slice_cast::*; -pub fn initialize() -> bool { - unsafe { NDIlib_initialize() } +pub fn load() -> Result<(), glib::BoolError> { + ndisys::load() } #[derive(Debug)] diff --git a/net/ndi/src/ndisink/imp.rs b/net/ndi/src/ndisink/imp.rs index eb3c7dc1..24eb1694 100644 --- a/net/ndi/src/ndisink/imp.rs +++ b/net/ndi/src/ndisink/imp.rs @@ -167,6 +167,23 @@ impl ElementImpl for NdiSink { PAD_TEMPLATES.as_ref() } + + fn change_state( + &self, + transition: gst::StateChange, + ) -> Result { + match transition { + gst::StateChange::NullToReady => { + if let Err(err) = crate::ndi::load() { + gst::element_imp_error!(self, gst::LibraryError::Init, ("{}", err)); + return Err(gst::StateChangeError); + } + } + _ => (), + } + + self.parent_change_state(transition) + } } impl BaseSinkImpl for NdiSink { diff --git a/net/ndi/src/ndisrc/imp.rs b/net/ndi/src/ndisrc/imp.rs index d19c049d..5bd90dfc 100644 --- a/net/ndi/src/ndisrc/imp.rs +++ b/net/ndi/src/ndisrc/imp.rs @@ -370,6 +370,12 @@ impl ElementImpl for NdiSrc { transition: gst::StateChange, ) -> Result { match transition { + gst::StateChange::NullToReady => { + if let Err(err) = crate::ndi::load() { + gst::element_imp_error!(self, gst::LibraryError::Init, ("{}", err)); + return Err(gst::StateChangeError); + } + } gst::StateChange::PausedToPlaying => { if let Some(ref controller) = *self.receiver_controller.lock().unwrap() { controller.set_playing(true); diff --git a/net/ndi/src/ndisys.rs b/net/ndi/src/ndisys.rs index 86053abd..b5c813fe 100644 --- a/net/ndi/src/ndisys.rs +++ b/net/ndi/src/ndisys.rs @@ -2,80 +2,66 @@ #![allow(non_camel_case_types, non_upper_case_globals, non_snake_case)] -#[cfg_attr( - all(target_arch = "x86_64", target_os = "windows"), - link(name = "Processing.NDI.Lib.x64") -)] -#[cfg_attr( - all(target_arch = "x86", target_os = "windows"), - link(name = "Processing.NDI.Lib.x86") -)] -#[cfg_attr( - not(any(target_os = "windows", target_os = "macos")), - link(name = "ndi") -)] -extern "C" { - pub fn NDIlib_initialize() -> bool; - pub fn NDIlib_destroy(); - pub fn NDIlib_find_create_v2( - p_create_settings: *const NDIlib_find_create_t, - ) -> NDIlib_find_instance_t; - pub fn NDIlib_find_destroy(p_instance: NDIlib_find_instance_t); - pub fn NDIlib_find_wait_for_sources( - p_instance: NDIlib_find_instance_t, - timeout_in_ms: u32, - ) -> bool; - pub fn NDIlib_find_get_current_sources( - p_instance: NDIlib_find_instance_t, - p_no_sources: *mut u32, - ) -> *const NDIlib_source_t; - pub fn NDIlib_recv_create_v3( - p_create_settings: *const NDIlib_recv_create_v3_t, - ) -> NDIlib_recv_instance_t; - pub fn NDIlib_recv_destroy(p_instance: NDIlib_recv_instance_t); - pub fn NDIlib_recv_set_tally( - p_instance: NDIlib_recv_instance_t, - p_tally: *const NDIlib_tally_t, - ) -> bool; - pub fn NDIlib_recv_send_metadata( - p_instance: NDIlib_recv_instance_t, - p_metadata: *const NDIlib_metadata_frame_t, - ) -> bool; - pub fn NDIlib_recv_capture_v3( - p_instance: NDIlib_recv_instance_t, - p_video_data: *mut NDIlib_video_frame_v2_t, - p_audio_data: *mut NDIlib_audio_frame_v3_t, - p_metadata: *mut NDIlib_metadata_frame_t, - timeout_in_ms: u32, - ) -> NDIlib_frame_type_e; - pub fn NDIlib_recv_free_video_v2( - p_instance: NDIlib_recv_instance_t, - p_video_data: *mut NDIlib_video_frame_v2_t, - ); - pub fn NDIlib_recv_free_audio_v3( - p_instance: NDIlib_recv_instance_t, - p_audio_data: *mut NDIlib_audio_frame_v3_t, - ); - pub fn NDIlib_recv_free_metadata( - p_instance: NDIlib_recv_instance_t, - p_metadata: *mut NDIlib_metadata_frame_t, - ); - pub fn NDIlib_recv_get_queue( - p_instance: NDIlib_recv_instance_t, - p_total: *mut NDIlib_recv_queue_t, - ); - pub fn NDIlib_send_create( - p_create_settings: *const NDIlib_send_create_t, - ) -> NDIlib_send_instance_t; - pub fn NDIlib_send_destroy(p_instance: NDIlib_send_instance_t); - pub fn NDIlib_send_send_video_v2( - p_instance: NDIlib_send_instance_t, - p_video_data: *const NDIlib_video_frame_v2_t, - ); - pub fn NDIlib_send_send_audio_v3( - p_instance: NDIlib_send_instance_t, - p_audio_data: *const NDIlib_audio_frame_v3_t, - ); +#[cfg(unix)] +use libloading::os::unix::{Library, Symbol}; +#[cfg(windows)] +use libloading::os::windows::{Library, Symbol}; + +#[cfg(all(target_arch = "x86_64", target_os = "windows"))] +const LIBRARY_NAME: &str = "Processing.NDI.Lib.x64.dll"; +#[cfg(all(target_arch = "x86", target_os = "windows"))] +const LIBRARY_NAME: &str = "Processing.NDI.Lib.x86.dll"; +#[cfg(target_os = "linux")] +const LIBRARY_NAME: &str = "libndi.so.5"; +#[cfg(target_os = "macos")] +const LIBRARY_NAME: &str = "libndi.dylib"; + +struct FFI { + _library: Library, + initialize: Symbol bool>, + destroy: Symbol, + find_create_v2: + Symbol NDIlib_find_instance_t>, + find_destroy: Symbol, + find_wait_for_sources: + Symbol bool>, + find_get_current_sources: Symbol< + fn(p_instance: NDIlib_find_instance_t, p_no_sources: *mut u32) -> *const NDIlib_source_t, + >, + recv_create_v3: + Symbol NDIlib_recv_instance_t>, + recv_destroy: Symbol, + recv_set_tally: + Symbol bool>, + recv_send_metadata: Symbol< + fn(p_instance: NDIlib_recv_instance_t, p_metadata: *const NDIlib_metadata_frame_t) -> bool, + >, + recv_capture_v3: Symbol< + fn( + p_instance: NDIlib_recv_instance_t, + p_video_data: *mut NDIlib_video_frame_v2_t, + p_audio_data: *mut NDIlib_audio_frame_v3_t, + p_metadata: *mut NDIlib_metadata_frame_t, + timeout_in_ms: u32, + ) -> NDIlib_frame_type_e, + >, + recv_free_video_v2: + Symbol, + recv_free_audio_v3: + Symbol, + recv_free_metadata: + Symbol, + recv_get_queue: + Symbol, + send_create: + Symbol NDIlib_send_instance_t>, + send_destroy: Symbol, + send_send_video_v2: Symbol< + fn(p_instance: NDIlib_send_instance_t, p_video_data: *const NDIlib_video_frame_v2_t), + >, + send_send_audio_v3: Symbol< + fn(p_instance: NDIlib_send_instance_t, p_audio_data: *const NDIlib_audio_frame_v3_t), + >, } pub type NDIlib_find_instance_t = *mut ::std::os::raw::c_void; @@ -326,3 +312,213 @@ pub const NDIlib_compressed_packet_flags_keyframe: u32 = 1; #[cfg(feature = "advanced-sdk")] pub const NDIlib_compressed_packet_version_0: u32 = 44; + +static FFI: once_cell::sync::OnceCell = once_cell::sync::OnceCell::new(); + +pub fn load() -> Result<(), glib::BoolError> { + static ERR: once_cell::sync::OnceCell> = + once_cell::sync::OnceCell::new(); + + ERR.get_or_init(|| unsafe { + use std::env; + use std::path; + + let library_directory = env::var_os("NDI_RUNTIME_DIR_V5"); + let library_path = if let Some(library_directory) = library_directory { + let mut path = path::PathBuf::from(library_directory); + path.push(LIBRARY_NAME); + path + } else { + path::PathBuf::from(LIBRARY_NAME) + }; + + let library = Library::new(library_path) + .map_err(|err| glib::bool_error!("Failed to load NDI SDK: {}", err))?; + + macro_rules! load_symbol { + ($name:ident) => {{ + #[cfg(unix)] + { + library + .get_singlethreaded(stringify!($name).as_bytes()) + .map_err(|err| { + glib::bool_error!( + concat!( + "Failed to load function '", + stringify!($name), + "' from NDI SDK: {}" + ), + err + ) + })? + } + #[cfg(windows)] + { + library.get(stringify!($name).as_bytes()).map_err(|err| { + glib::bool_error!( + concat!( + "Failed to load function '", + stringify!($name), + "' from NDI SDK: {}" + ), + err + ) + })? + } + }}; + } + + let ffi = FFI { + initialize: load_symbol!(NDIlib_initialize), + destroy: load_symbol!(NDIlib_destroy), + find_create_v2: load_symbol!(NDIlib_find_create_v2), + find_destroy: load_symbol!(NDIlib_find_destroy), + find_wait_for_sources: load_symbol!(NDIlib_find_wait_for_sources), + find_get_current_sources: load_symbol!(NDIlib_find_get_current_sources), + recv_create_v3: load_symbol!(NDIlib_recv_create_v3), + recv_destroy: load_symbol!(NDIlib_recv_destroy), + recv_set_tally: load_symbol!(NDIlib_recv_set_tally), + recv_send_metadata: load_symbol!(NDIlib_recv_send_metadata), + recv_capture_v3: load_symbol!(NDIlib_recv_capture_v3), + recv_free_video_v2: load_symbol!(NDIlib_recv_free_video_v2), + recv_free_audio_v3: load_symbol!(NDIlib_recv_free_audio_v3), + recv_free_metadata: load_symbol!(NDIlib_recv_free_metadata), + recv_get_queue: load_symbol!(NDIlib_recv_get_queue), + send_create: load_symbol!(NDIlib_send_create), + send_destroy: load_symbol!(NDIlib_send_destroy), + send_send_video_v2: load_symbol!(NDIlib_send_send_video_v2), + send_send_audio_v3: load_symbol!(NDIlib_send_send_audio_v3), + _library: library, + }; + + if FFI.set(ffi).is_err() { + unreachable!("NDI SDK loaded twice"); + } + + Ok(()) + }) + .to_owned() +} + +pub unsafe fn NDIlib_initialize() -> bool { + (FFI.get_unchecked().initialize)() +} + +pub unsafe fn NDIlib_destroy() { + (FFI.get_unchecked().destroy)() +} + +pub unsafe fn NDIlib_find_create_v2( + p_create_settings: *const NDIlib_find_create_t, +) -> NDIlib_find_instance_t { + (FFI.get_unchecked().find_create_v2)(p_create_settings) +} +pub unsafe fn NDIlib_find_destroy(p_instance: NDIlib_find_instance_t) { + (FFI.get_unchecked().find_destroy)(p_instance) +} + +pub unsafe fn NDIlib_find_wait_for_sources( + p_instance: NDIlib_find_instance_t, + timeout_in_ms: u32, +) -> bool { + (FFI.get_unchecked().find_wait_for_sources)(p_instance, timeout_in_ms) +} + +pub unsafe fn NDIlib_find_get_current_sources( + p_instance: NDIlib_find_instance_t, + p_no_sources: *mut u32, +) -> *const NDIlib_source_t { + (FFI.get_unchecked().find_get_current_sources)(p_instance, p_no_sources) +} + +pub unsafe fn NDIlib_recv_create_v3( + p_create_settings: *const NDIlib_recv_create_v3_t, +) -> NDIlib_recv_instance_t { + (FFI.get_unchecked().recv_create_v3)(p_create_settings) +} + +pub unsafe fn NDIlib_recv_destroy(p_instance: NDIlib_recv_instance_t) { + (FFI.get_unchecked().recv_destroy)(p_instance) +} + +pub unsafe fn NDIlib_recv_set_tally( + p_instance: NDIlib_recv_instance_t, + p_tally: *const NDIlib_tally_t, +) -> bool { + (FFI.get_unchecked().recv_set_tally)(p_instance, p_tally) +} + +pub unsafe fn NDIlib_recv_send_metadata( + p_instance: NDIlib_recv_instance_t, + p_metadata: *const NDIlib_metadata_frame_t, +) -> bool { + (FFI.get_unchecked().recv_send_metadata)(p_instance, p_metadata) +} + +pub unsafe fn NDIlib_recv_capture_v3( + p_instance: NDIlib_recv_instance_t, + p_video_data: *mut NDIlib_video_frame_v2_t, + p_audio_data: *mut NDIlib_audio_frame_v3_t, + p_metadata: *mut NDIlib_metadata_frame_t, + timeout_in_ms: u32, +) -> NDIlib_frame_type_e { + (FFI.get_unchecked().recv_capture_v3)( + p_instance, + p_video_data, + p_audio_data, + p_metadata, + timeout_in_ms, + ) +} + +pub unsafe fn NDIlib_recv_free_video_v2( + p_instance: NDIlib_recv_instance_t, + p_video_data: *mut NDIlib_video_frame_v2_t, +) { + (FFI.get_unchecked().recv_free_video_v2)(p_instance, p_video_data) +} + +pub unsafe fn NDIlib_recv_free_audio_v3( + p_instance: NDIlib_recv_instance_t, + p_audio_data: *mut NDIlib_audio_frame_v3_t, +) { + (FFI.get_unchecked().recv_free_audio_v3)(p_instance, p_audio_data) +} + +pub unsafe fn NDIlib_recv_free_metadata( + p_instance: NDIlib_recv_instance_t, + p_metadata: *mut NDIlib_metadata_frame_t, +) { + (FFI.get_unchecked().recv_free_metadata)(p_instance, p_metadata) +} + +pub unsafe fn NDIlib_recv_get_queue( + p_instance: NDIlib_recv_instance_t, + p_total: *mut NDIlib_recv_queue_t, +) { + (FFI.get_unchecked().recv_get_queue)(p_instance, p_total) +} + +pub unsafe fn NDIlib_send_create( + p_create_settings: *const NDIlib_send_create_t, +) -> NDIlib_send_instance_t { + (FFI.get_unchecked().send_create)(p_create_settings) +} + +pub unsafe fn NDIlib_send_destroy(p_instance: NDIlib_send_instance_t) { + (FFI.get_unchecked().send_destroy)(p_instance) +} + +pub unsafe fn NDIlib_send_send_video_v2( + p_instance: NDIlib_send_instance_t, + p_video_data: *const NDIlib_video_frame_v2_t, +) { + (FFI.get_unchecked().send_send_video_v2)(p_instance, p_video_data) +} + +pub unsafe fn NDIlib_send_send_audio_v3( + p_instance: NDIlib_send_instance_t, + p_audio_data: *const NDIlib_audio_frame_v3_t, +) { + (FFI.get_unchecked().send_send_audio_v3)(p_instance, p_audio_data) +}