ndi: Implement dynamic loading of the NDI SDK

And build the plugin on the CI and via meson.
This commit is contained in:
Sebastian Dröge 2022-10-12 21:14:45 +03:00
parent 16c036e2cc
commit 3fe9e4a207
11 changed files with 321 additions and 127 deletions

View file

@ -57,6 +57,7 @@ default-members = [
"net/reqwest", "net/reqwest",
"net/rtpav1", "net/rtpav1",
"net/webrtc-http", "net/webrtc-http",
"net/ndi",
"text/ahead", "text/ahead",
"text/json", "text/json",
"text/regex", "text/regex",

View file

@ -32,6 +32,7 @@ You will find the following plugins in this repository:
- `aws_transcriber`: an element wrapping the AWS Transcriber service. - `aws_transcriber`: an element wrapping the AWS Transcriber service.
- `raptorq`: Encoder/decoder element for RaptorQ RTP FEC mechanism. - `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` * `audio`
- `audiofx`: Elements to apply audio effects to a stream - `audiofx`: Elements to apply audio effects to a stream

View file

@ -69,6 +69,7 @@ plugins = {
'gst-plugin-tracers': 'libgstrstracers', 'gst-plugin-tracers': 'libgstrstracers',
'gst-plugin-webrtchttp': 'libgstwebrtchttp', 'gst-plugin-webrtchttp': 'libgstwebrtchttp',
'gst-plugin-rtpav1': 'libgstrtpav1', 'gst-plugin-rtpav1': 'libgstrtpav1',
'gst-plugin-ndi': 'libgstndi',
} }
extra_env = {} extra_env = {}

View file

@ -18,6 +18,7 @@ byte-slice-cast = "1"
once_cell = "1.0" once_cell = "1.0"
byteorder = "1.0" byteorder = "1.0"
atomic_refcell = "0.1" atomic_refcell = "0.1"
libloading = "0.7"
[build-dependencies] [build-dependencies]
gst-plugin-version-helper = { path = "../../version-helper" } gst-plugin-version-helper = { path = "../../version-helper" }
@ -47,4 +48,4 @@ install_subdir = "gstreamer-1.0"
versioning = false versioning = false
[package.metadata.capi.pkg_config] [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"

View file

@ -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: 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: Feel free to contribute to this project. Some ways you can contribute are:
* Testing with more hardware and software and reporting bugs * Testing with more hardware and software and reporting bugs
* Doing pull requests. * 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 License
------- -------
This plugin is licensed under the MPL-2 - see the [LICENSE](LICENSE-MPL-2.0) file for details This plugin is licensed under the MPL-2 - see the [LICENSE](LICENSE-MPL-2.0) file for details
Acknowledgments Acknowledgments
------- -------
* University of the Arts London and The University of Manchester. * University of the Arts London and The University of Manchester.

View file

@ -72,6 +72,10 @@ impl DeviceProviderImpl for DeviceProvider {
} }
fn start(&self) -> Result<(), gst::LoggableError> { 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 mut thread_guard = self.thread.lock().unwrap();
let device_provider = self.instance(); let device_provider = self.instance();
if thread_guard.is_some() { if thread_guard.is_some() {

View file

@ -118,10 +118,6 @@ impl From<RecvColorFormat> for NDIlib_recv_color_format_e {
} }
fn plugin_init(plugin: &gst::Plugin) -> Result<(), glib::BoolError> { fn plugin_init(plugin: &gst::Plugin) -> Result<(), glib::BoolError> {
if !ndi::initialize() {
return Err(glib::bool_error!("Cannot initialize NDI"));
}
device_provider::register(plugin)?; device_provider::register(plugin)?;
ndisrc::register(plugin)?; ndisrc::register(plugin)?;

View file

@ -9,8 +9,8 @@ use std::ptr;
use byte_slice_cast::*; use byte_slice_cast::*;
pub fn initialize() -> bool { pub fn load() -> Result<(), glib::BoolError> {
unsafe { NDIlib_initialize() } ndisys::load()
} }
#[derive(Debug)] #[derive(Debug)]

View file

@ -167,6 +167,23 @@ impl ElementImpl for NdiSink {
PAD_TEMPLATES.as_ref() PAD_TEMPLATES.as_ref()
} }
fn change_state(
&self,
transition: gst::StateChange,
) -> Result<gst::StateChangeSuccess, gst::StateChangeError> {
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 { impl BaseSinkImpl for NdiSink {

View file

@ -370,6 +370,12 @@ impl ElementImpl for NdiSrc {
transition: gst::StateChange, transition: gst::StateChange,
) -> Result<gst::StateChangeSuccess, gst::StateChangeError> { ) -> Result<gst::StateChangeSuccess, gst::StateChangeError> {
match transition { 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 => { gst::StateChange::PausedToPlaying => {
if let Some(ref controller) = *self.receiver_controller.lock().unwrap() { if let Some(ref controller) = *self.receiver_controller.lock().unwrap() {
controller.set_playing(true); controller.set_playing(true);

View file

@ -2,80 +2,66 @@
#![allow(non_camel_case_types, non_upper_case_globals, non_snake_case)] #![allow(non_camel_case_types, non_upper_case_globals, non_snake_case)]
#[cfg_attr( #[cfg(unix)]
all(target_arch = "x86_64", target_os = "windows"), use libloading::os::unix::{Library, Symbol};
link(name = "Processing.NDI.Lib.x64") #[cfg(windows)]
)] use libloading::os::windows::{Library, Symbol};
#[cfg_attr(
all(target_arch = "x86", target_os = "windows"), #[cfg(all(target_arch = "x86_64", target_os = "windows"))]
link(name = "Processing.NDI.Lib.x86") const LIBRARY_NAME: &str = "Processing.NDI.Lib.x64.dll";
)] #[cfg(all(target_arch = "x86", target_os = "windows"))]
#[cfg_attr( const LIBRARY_NAME: &str = "Processing.NDI.Lib.x86.dll";
not(any(target_os = "windows", target_os = "macos")), #[cfg(target_os = "linux")]
link(name = "ndi") const LIBRARY_NAME: &str = "libndi.so.5";
)] #[cfg(target_os = "macos")]
extern "C" { const LIBRARY_NAME: &str = "libndi.dylib";
pub fn NDIlib_initialize() -> bool;
pub fn NDIlib_destroy(); struct FFI {
pub fn NDIlib_find_create_v2( _library: Library,
p_create_settings: *const NDIlib_find_create_t, initialize: Symbol<fn() -> bool>,
) -> NDIlib_find_instance_t; destroy: Symbol<fn()>,
pub fn NDIlib_find_destroy(p_instance: NDIlib_find_instance_t); find_create_v2:
pub fn NDIlib_find_wait_for_sources( Symbol<fn(p_create_settings: *const NDIlib_find_create_t) -> NDIlib_find_instance_t>,
p_instance: NDIlib_find_instance_t, find_destroy: Symbol<fn(p_instance: NDIlib_find_instance_t)>,
timeout_in_ms: u32, find_wait_for_sources:
) -> bool; Symbol<fn(p_instance: NDIlib_find_instance_t, timeout_in_ms: u32) -> bool>,
pub fn NDIlib_find_get_current_sources( find_get_current_sources: Symbol<
p_instance: NDIlib_find_instance_t, fn(p_instance: NDIlib_find_instance_t, p_no_sources: *mut u32) -> *const NDIlib_source_t,
p_no_sources: *mut u32, >,
) -> *const NDIlib_source_t; recv_create_v3:
pub fn NDIlib_recv_create_v3( Symbol<fn(p_create_settings: *const NDIlib_recv_create_v3_t) -> NDIlib_recv_instance_t>,
p_create_settings: *const NDIlib_recv_create_v3_t, recv_destroy: Symbol<fn(p_instance: NDIlib_recv_instance_t)>,
) -> NDIlib_recv_instance_t; recv_set_tally:
pub fn NDIlib_recv_destroy(p_instance: NDIlib_recv_instance_t); Symbol<fn(p_instance: NDIlib_recv_instance_t, p_tally: *const NDIlib_tally_t) -> bool>,
pub fn NDIlib_recv_set_tally( recv_send_metadata: Symbol<
p_instance: NDIlib_recv_instance_t, fn(p_instance: NDIlib_recv_instance_t, p_metadata: *const NDIlib_metadata_frame_t) -> bool,
p_tally: *const NDIlib_tally_t, >,
) -> bool; recv_capture_v3: Symbol<
pub fn NDIlib_recv_send_metadata( fn(
p_instance: NDIlib_recv_instance_t, p_instance: NDIlib_recv_instance_t,
p_metadata: *const NDIlib_metadata_frame_t, p_video_data: *mut NDIlib_video_frame_v2_t,
) -> bool; p_audio_data: *mut NDIlib_audio_frame_v3_t,
pub fn NDIlib_recv_capture_v3( p_metadata: *mut NDIlib_metadata_frame_t,
p_instance: NDIlib_recv_instance_t, timeout_in_ms: u32,
p_video_data: *mut NDIlib_video_frame_v2_t, ) -> NDIlib_frame_type_e,
p_audio_data: *mut NDIlib_audio_frame_v3_t, >,
p_metadata: *mut NDIlib_metadata_frame_t, recv_free_video_v2:
timeout_in_ms: u32, Symbol<fn(p_instance: NDIlib_recv_instance_t, p_video_data: *mut NDIlib_video_frame_v2_t)>,
) -> NDIlib_frame_type_e; recv_free_audio_v3:
pub fn NDIlib_recv_free_video_v2( Symbol<fn(p_instance: NDIlib_recv_instance_t, p_audio_data: *mut NDIlib_audio_frame_v3_t)>,
p_instance: NDIlib_recv_instance_t, recv_free_metadata:
p_video_data: *mut NDIlib_video_frame_v2_t, Symbol<fn(p_instance: NDIlib_recv_instance_t, p_metadata: *mut NDIlib_metadata_frame_t)>,
); recv_get_queue:
pub fn NDIlib_recv_free_audio_v3( Symbol<fn(p_instance: NDIlib_recv_instance_t, p_total: *mut NDIlib_recv_queue_t)>,
p_instance: NDIlib_recv_instance_t, send_create:
p_audio_data: *mut NDIlib_audio_frame_v3_t, Symbol<fn(p_create_settings: *const NDIlib_send_create_t) -> NDIlib_send_instance_t>,
); send_destroy: Symbol<fn(p_instance: NDIlib_send_instance_t)>,
pub fn NDIlib_recv_free_metadata( send_send_video_v2: Symbol<
p_instance: NDIlib_recv_instance_t, fn(p_instance: NDIlib_send_instance_t, p_video_data: *const NDIlib_video_frame_v2_t),
p_metadata: *mut NDIlib_metadata_frame_t, >,
); send_send_audio_v3: Symbol<
pub fn NDIlib_recv_get_queue( fn(p_instance: NDIlib_send_instance_t, p_audio_data: *const NDIlib_audio_frame_v3_t),
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,
);
} }
pub type NDIlib_find_instance_t = *mut ::std::os::raw::c_void; 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")] #[cfg(feature = "advanced-sdk")]
pub const NDIlib_compressed_packet_version_0: u32 = 44; pub const NDIlib_compressed_packet_version_0: u32 = 44;
static FFI: once_cell::sync::OnceCell<FFI> = once_cell::sync::OnceCell::new();
pub fn load() -> Result<(), glib::BoolError> {
static ERR: once_cell::sync::OnceCell<Result<(), glib::BoolError>> =
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)
}