Skip to content
safewordsPublic

About

NDI® discovery, receive and send in Rust through hand-rolled FFI that loads the NDI runtime at run time — no SDK, bindgen or link step. rivet's NDI input and output (crate rivet-ndi).

Resources

Stars

1 star

Watchers

0 watching

Forks

Latest commit

 

History

2 Commits

Folders and files

Repository files navigation

rivet-ndi

CI

NDI® source discovery, receive and send in Rust, through FFI written by hand against the NDI SDK's public C headers. The NDI runtime is loaded the first time it is asked for (LoadLibrary / dlopen), so nothing about NDI is needed to build: no SDK, no bindgen, no link step, no build script. A host without the runtime gets an error saying how to install it, not a binary that will not start.

Written for the rivet transcoder, where it is the ndi feature: ndi://NAME is an input or an output wherever rivet takes a path — a live source recorded to files or a live HLS package, a file played out as an NDI stream — on the same job spec as any file (see rivet's docs/ndi.md). Usable on its own by anything that wants NDI pictures and sound in, or out.

Published as rivet-ndi; imported as ndi (use ndi::…). Two dependencies (libloading, thiserror), no features, no build script.

[dependencies]
ndi = { package = "rivet-ndi", git = "https://github.com/safewords/rivet-ndi", branch = "develop" }

Use

use std::time::Duration;

let ndi = ndi::Ndi::load()?;
println!("{}", ndi.version());

// Discovery.
for source in ndi.sources(&ndi::FindOptions::default(), Duration::from_secs(2))? {
    println!("{}  {:?}", source.name, source.url);
}

// Receive: the one source a name (or a unique part of one) names.
let source = ndi.find_source("Camera 1", &ndi::FindOptions::default(), Duration::from_secs(5))?;
let mut receiver = ndi.receiver(&source, &ndi::ReceiverOptions::default())?;
match receiver.capture(Duration::from_millis(500))? {
    ndi::Capture::Video(frame) => {
        let picture = frame.to_picture()?; // planar, no row padding
        println!("{}x{} {:?} at {:?}", picture.width, picture.height, picture.layout, frame.frame_rate());
    }
    ndi::Capture::Audio(audio) => {
        let interleaved = audio.interleaved(); // f32, L R L R ...
        println!("{} samples x {} channels", audio.samples(), audio.channels());
    }
    _ => {}
}

// Send: announce a source, paced by the runtime at the frame rate given.
let mut sender = ndi.sender(&ndi::SenderOptions::new("My output"))?;
let picture = ndi::Picture {
    layout: ndi::Layout::Yuv420p,
    width: 1280,
    height: 720,
    data: vec![128; 1280 * 720 * 3 / 2],
};
sender.send_picture(&picture, (30000, 1001), None)?;
sender.send_audio(48_000, 2, &vec![0.0; 1601 * 2], None)?;
# Ok::<(), ndi::Error>(())

Frames are the runtime's until dropped: a received VideoFrame or AudioFrame hands its buffer back to the runtime on drop, and borrows nothing, so it can be moved to another thread first.

Pictures

NDI carries video uncompressed in a few fourccs, rows stride bytes apart. VideoFrame::to_picture (or convert::to_picture on raw bytes) packs them into a planar Picture; Picture::to_ndi goes the other way:

NDI fourcc Picture layout Back out as
UYVY, UYVA (alpha dropped) Yuv422p UYVY
P216, PA16 (alpha dropped) Yuv422p10le (16 → 10 bits, rounded) P216
I420, YV12 Yuv420p I420
NV12 Nv12 NV12
BGRA, BGRX, RGBA, RGBX Rgba (X opaque) RGBA
— Yuv420p10le P216 (chroma rows repeated)

Only samples move: nothing is re-matrixed. NDI's YUV is studio range, BT.601 below 720 lines and BT.709 from 720 up, unless the frame's metadata says otherwise — VideoFrame::color_info reads NDI 6's <ndi_color_info transfer=… matrix=… primaries=…/>. Odd widths and heights are handled (chroma (n + 1) / 2); a short buffer or stride is an error, never a panic.

The runtime

Looked for, in order: RIVET_NDI_LIB (a full path), the directories the NDI installers set in NDI_RUNTIME_DIR_V6 / _V5 / _V4, the installers' default directories on Windows, then the platform loader's own search for Processing.NDI.Lib.x64.dll (Windows) or libndi.so.6 / .5 (Linux). NDI 4 or later; the v2 frame structs every runtime since NDI 3 exports are the ones declared.

The runtime is Vizrt's, installed by the user under NDI's licence (ndi.video/tools); this crate redistributes no part of the NDI SDK or runtime.

How it is checked

cargo test: every struct's size and field offsets against the SDK headers' 64-bit layout, each fourcc conversion (padded strides, odd sizes, 16 → 10-bit rounding, round trips), colour metadata parsing, and source-name matching. No runtime is needed. rivet's ndi_loopback test goes through a real runtime: a sender and a receiver on one machine, pictures and a tone, recorded and checked.

Licence

Source-available under the Open Encoding Attribution License 1.0 (LICENSE.md); see NOTICE.

NDI® is a registered trademark of Vizrt NDI AB. This crate is not affiliated with or endorsed by Vizrt.

About

NDI® discovery, receive and send in Rust through hand-rolled FFI that loads the NDI runtime at run time — no SDK, bindgen or link step. rivet's NDI input and output (crate rivet-ndi).

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages