grafton_ndi/lib.rs
1//! High-performance Rust bindings for the NDIĀ® 6 SDK (Network Device Interface).
2//!
3//! This crate provides safe, idiomatic Rust bindings for the NDI SDK, enabling
4//! real-time, low-latency video/audio streaming over IP networks. NDI is widely
5//! used in broadcast, live production, and video conferencing applications.
6//!
7//! # Quick Start
8//!
9//! ```no_run
10//! use grafton_ndi::{NDI, FinderOptions, Finder};
11//! use std::time::Duration;
12//!
13//! # fn main() -> Result<(), grafton_ndi::Error> {
14//! // Initialize the NDI runtime
15//! let ndi = NDI::new()?;
16//!
17//! // Find sources on the network
18//! let options = FinderOptions::builder().show_local_sources(true).build();
19//! let finder = Finder::new(&ndi, &options)?;
20//!
21//! // Discover sources
22//! let sources = finder.find_sources(Duration::from_secs(5))?;
23//!
24//! for source in sources {
25//! println!("Found: {}", source);
26//! }
27//! # Ok(())
28//! # }
29//! ```
30//!
31//! # Core Concepts
32//!
33//! ## Runtime Management
34//!
35//! The [`NDI`] struct manages the NDI runtime lifecycle. Create a handle before
36//! constructing SDK resources; those resources retain their own runtime
37//! references. [`SourceCache`] acquires the runtime on cache misses, while its
38//! cached owned source descriptors need no live runtime handle.
39//!
40//! ## Source Discovery
41//!
42//! Use [`Finder`] to discover NDI sources on the network. Sources can be filtered
43//! by groups and additional IP addresses can be specified for discovery.
44//!
45//! ## Receiving
46//!
47//! The [`Receiver`] type handles receiving video, audio, and metadata from NDI
48//! sources. It supports various color formats and bandwidth modes.
49//!
50//! ## Sending
51//!
52//! Use [`Sender`] to transmit video, audio, and metadata as an NDI source.
53//! Senders can be configured with clock settings and group assignments.
54//!
55//! # Thread Safety
56//!
57//! All primary types ([`Finder`], [`Receiver`], [`Sender`]) implement `Send + Sync`
58//! with internal synchronization where required by the SDK. Borrowed receiver
59//! frame references support scoped thread transfer; FrameSync references remain
60//! tied to their owner and cannot be shared or transferred across threads.
61//! Immutable [`VideoPlane`], [`VideoRows`], and [`VideoPlanes`] views are
62//! `Send + Sync` from either capture family. Scoped workers may read those
63//! borrowed pixels while the frame guard stays on its permitted freeing thread.
64//!
65//! ## Async Sending
66//!
67//! [`Sender::send_video_async`] takes ownership of a [`VideoFrame`] without
68//! copying its pixel or metadata allocations. The next submission returns the
69//! previous frame after SDK synchronization; finish recovers the final frame.
70//!
71//! ```no_run
72//! # use grafton_ndi::{NDI, SenderOptions, VideoFrame, PixelFormat};
73//! # fn main() -> Result<(), grafton_ndi::Error> {
74//! # let ndi = NDI::new()?;
75//! # let mut sender = grafton_ndi::Sender::new(&ndi, &SenderOptions::builder("Test").build())?;
76//! let first = VideoFrame::builder().resolution(1920, 1080).pixel_format(PixelFormat::BGRA).build()?;
77//! let second = VideoFrame::builder().resolution(1920, 1080).pixel_format(PixelFormat::BGRA).build()?;
78//! assert!(sender.send_video_async(first).is_none());
79//! let mut reusable = sender.send_video_async(second).unwrap();
80//! reusable.data_mut().fill(0);
81//! let final_frame = sender.finish_video_async().unwrap();
82//! # Ok(())
83//! # }
84//! ```
85//!
86//! [`Sender::send_video_async_copy`] explicitly copies a borrowed buffer into
87//! an owned frame, then uses the same pipeline and returns the previous frame.
88//! Source buffers can be reused immediately. Both paths use default SDK
89//! synchronization, with no SDK completion callback registration. Optional Rust
90//! completion handlers run on the calling
91//! thread after synchronization, outside internal locks.
92//!
93//! Note: Only video supports async sending in the NDI SDK. Audio and metadata are always synchronous.
94//!
95//! # Performance
96//!
97//! - **Borrowed receive buffers**: Read SDK-owned frames without wrapper pixel copies
98//! - **Async completion**: Owned frames are recovered after SDK synchronization
99//! - **Bandwidth control**: Multiple quality levels for different use cases
100//! - **SDK processing**: Encoding and acceleration depend on the installed NDI SDK
101//!
102//! # Platform Support
103//!
104//! Windows, Linux, and macOS are all exercised in CI (build, test, lint, and
105//! semver checks) against the NDI SDK.
106//!
107//! - **Windows**: Links the NDI SDK import library at build time; requires the
108//! NDI runtime DLLs at runtime. Supports x86 and x86_64 MSVC targets.
109//! - **Linux**: Supports both standard and Advanced SDK install directories;
110//! the runtime libraries must be discoverable by the dynamic linker. Uses
111//! GNU/glibc targets for x86, x86_64, ARMv6/ARMv7 hard-float, and ARM64.
112//! - **macOS**: Supports current NDI SDK package layouts used by the CI setup
113//! action and common local install paths.
114//! - **Android**: Cross-build CI links examples for ARM64, ARMv7, x86, and x86_64.
115//! Set `NDI_SDK_DIR` to the Android NDI SDK and configure the Android NDK
116//! linker/sysroot. Device runtime testing is separate from build coverage.
117//!
118//! The current APIs are validated with NDI SDK 6.3.2. Apple mobile targets,
119//! including iOS, are not currently selected by the build script. See the
120//! [SDK validation guide](https://github.com/GrantSparks/grafton-ndi/blob/main/docs/sdk-validation.md)
121//! for exact target coverage and cross-compilation commands.
122
123#![allow(non_upper_case_globals)]
124#![allow(non_camel_case_types)]
125#![allow(clippy::wildcard_imports)]
126#![allow(clippy::must_use_candidate)]
127#![allow(clippy::missing_errors_doc)]
128
129mod capture;
130mod error;
131mod ndi_lib;
132mod recv_guard;
133
134pub mod framesync;
135
136pub mod finder;
137pub mod frames;
138pub mod receiver;
139pub mod runtime;
140pub mod sender;
141
142#[cfg(any(feature = "tokio", feature = "smol"))]
143mod async_runtime;
144
145#[cfg(feature = "tokio")]
146pub use async_runtime::tokio;
147
148#[cfg(feature = "smol")]
149pub use async_runtime::smol;
150
151pub use {
152 capture::{AudioKind, CaptureKind, MetadataKind, VideoKind},
153 error::*,
154 finder::{Finder, FinderOptions, FinderOptionsBuilder, Source, SourceAddress, SourceCache},
155 frames::{
156 AudioFormat, AudioFrame, AudioFrameBuilder, AudioFrameRef, AudioLayout, FormatCategory,
157 LineStrideOrSize, MetadataFrame, MetadataFrameRef, PixelFormat, PixelFormatInfo, ScanType,
158 VideoFrame, VideoFrameBuilder, VideoFrameRef, VideoPlane, VideoPlanes, VideoRows,
159 },
160 framesync::{FrameSync, FrameSyncAudioRef, FrameSyncAudioRequest, FrameSyncVideoRef},
161 receiver::{
162 Capture, ConnectionStats, FrameSyncAttached, RawCapture, Receiver, ReceiverBandwidth,
163 ReceiverColorFormat, ReceiverMode, ReceiverOptions, ReceiverOptionsBuilder, ReceiverStatus,
164 Tally,
165 },
166 runtime::NDI,
167 sender::{
168 BorrowedVideoFrame, BorrowedVideoFrameBuilder, Sender, SenderOptions, SenderOptionsBuilder,
169 },
170};
171
172#[cfg(feature = "image-encoding")]
173pub use frames::ImageFormat;
174
175/// Alias for Result with our Error type
176pub type Result<T> = std::result::Result<T, crate::error::Error>;
177
178/// Maximum timeout duration supported by the NDI SDK (~49.7 days).
179///
180/// The NDI SDK uses `u32` milliseconds for timeout values, which limits the maximum
181/// timeout to approximately 49.7 days. Attempting to use a larger `Duration` will
182/// result in an error.
183pub const MAX_TIMEOUT: std::time::Duration = std::time::Duration::from_millis(u32::MAX as u64);
184
185/// Converts a `Duration` to milliseconds for FFI, checking for overflow.
186///
187/// # Errors
188///
189/// Returns [`Error::InvalidConfiguration`] if the duration exceeds [`MAX_TIMEOUT`].
190///
191pub(crate) fn to_ms_checked(d: std::time::Duration) -> Result<u32> {
192 if d > MAX_TIMEOUT {
193 Err(Error::InvalidConfiguration(format!(
194 "timeout {:?} exceeds MAX_TIMEOUT (~49.7 days)",
195 d
196 )))
197 } else {
198 Ok(d.as_millis() as u32)
199 }
200}
201
202#[cfg(test)]
203#[path = "tests.rs"]
204mod tests;
205
206#[cfg(test)]
207mod test_alloc;