ndi_sdk_sys/
sender.rs

1//! NDI Sender
2
3use std::{
4    ffi::CString,
5    fmt::Debug,
6    ptr::NonNull,
7    sync::{Arc, Mutex},
8    time::Duration,
9};
10
11use static_assertions::assert_impl_all;
12
13use crate::{
14    bindings,
15    blocking_update::BlockingUpdate,
16    enums::NDIRecvError,
17    frame::{
18        audio::AudioFrame,
19        generic::{AsFFIReadable, AsFFIWritable, FFIReadablePtrError},
20        metadata::MetadataFrame,
21        video::VideoFrame,
22    },
23    source::{NDISourceLike, NDISourceRef},
24    tally::Tally,
25    util::{SourceNameError, duration_to_ms, validate_source_name},
26};
27
28/// Builder for [NDISender]
29#[non_exhaustive]
30#[derive(Debug, Clone)]
31pub struct NDISenderBuilder {
32    pub name: Option<CString>,
33    pub groups: Option<CString>,
34    pub clock_video: bool,
35    pub clock_audio: bool,
36}
37
38#[allow(clippy::derivable_impls)]
39impl Default for NDISenderBuilder {
40    fn default() -> Self {
41        Self {
42            name: None,
43            groups: None,
44            clock_video: false,
45            clock_audio: false,
46        }
47    }
48}
49
50impl NDISenderBuilder {
51    pub fn new() -> Self {
52        Self::default()
53    }
54
55    /// Sets the name for the sender.
56    ///
57    /// The total length of an NDI source name should be limited to 253 characters. The following characters
58    /// are considered invalid: \ / : * ? " < > |. If any of these characters are found in the name, they will
59    /// be replaced with a space. These characters are reserved according to Windows file system naming conventions
60    pub fn name(mut self, name: &str) -> Result<Self, SourceNameError> {
61        self.name = Some(validate_source_name(name)?);
62        Ok(self)
63    }
64
65    /// Sets the groups for the sender.
66    ///
67    /// This parameter represents the groups that this NDI sender should place itself into. Groups are sets of
68    /// NDI sources. Any source can be part of any number of groups, and groups are comma-separated.
69    /// For instance, "cameras,studio 1,10am show" would place a source in the three groups named.
70    /// On the finding side, you can specify which groups to look for and look in multiple groups.
71    /// If the group is not set then the system default groups will be used.
72    /// <https://docs.ndi.video/all/developing-with-ndi/sdk/ndi-send#parameters>
73    pub fn groups(mut self, groups: &str) -> Self {
74        self.groups = Some(CString::new(groups).unwrap());
75        self
76    }
77
78    /// When enabled the SDK will limit the frame rate for video frames.
79    /// The send function will block until the next frame is ready to be sent.
80    /// <https://docs.ndi.video/all/developing-with-ndi/sdk/ndi-send#parameters>
81    pub fn clock_video(mut self, clock_video: bool) -> Self {
82        self.clock_video = clock_video;
83        self
84    }
85
86    /// When enabled the SDK will limit the frame rate for audio frames.
87    /// The send function will block until the next frame is ready to be sent.
88    /// <https://docs.ndi.video/all/developing-with-ndi/sdk/ndi-send#parameters>
89    pub fn clock_audio(mut self, clock_audio: bool) -> Self {
90        self.clock_audio = clock_audio;
91        self
92    }
93}
94
95impl NDISenderBuilder {
96    pub fn build(self) -> Result<NDISender, NDISenderBuilderError> {
97        let options = bindings::NDIlib_send_create_t {
98            p_ndi_name: self.name.as_ref().map_or(std::ptr::null(), |s| s.as_ptr()),
99            p_groups: self
100                .groups
101                .as_ref()
102                .map_or(std::ptr::null(), |s| s.as_ptr()),
103            clock_video: self.clock_video,
104            clock_audio: self.clock_audio,
105        };
106
107        let handle = unsafe { bindings::NDIlib_send_create(&options) };
108
109        if let Some(handle) = NonNull::new(handle) {
110            Ok(NDISender {
111                handle: Arc::new(RawSender { handle }),
112                in_transmission: Mutex::new(None),
113            })
114        } else {
115            Err(NDISenderBuilderError::CreationFailed)
116        }
117    }
118}
119
120#[non_exhaustive]
121#[derive(Debug, Clone, Copy, PartialEq, Eq)]
122pub enum NDISenderBuilderError {
123    CreationFailed,
124}
125
126#[derive(PartialEq, Eq)]
127pub(crate) struct RawSender {
128    handle: NonNull<bindings::NDIlib_send_instance_type>,
129}
130impl RawSender {
131    pub(crate) fn raw_ptr(&self) -> bindings::NDIlib_send_instance_t {
132        self.handle.as_ptr()
133    }
134}
135
136impl Drop for RawSender {
137    fn drop(&mut self) {
138        unsafe { bindings::NDIlib_send_destroy(self.raw_ptr()) };
139    }
140}
141
142impl Debug for RawSender {
143    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
144        f.debug_struct("RawSender")
145            .field("raw_ptr", &self.raw_ptr())
146            .finish()
147    }
148}
149
150unsafe impl Send for RawSender {}
151unsafe impl Sync for RawSender {}
152
153/// A NDI sender that can send frames to a receiver.
154///
155/// Please note that the sender handle will not be dropped until all metadata frames
156/// that were received from it are dropped or have their buffers deallocated. If you
157/// dynamically create and destroy senders take into consideration that metadata frames
158/// may prevent the sender resources from being released.
159pub struct NDISender {
160    handle: Arc<RawSender>,
161    in_transmission: Mutex<Option<Arc<VideoFrame>>>,
162}
163
164assert_impl_all!(
165    NDISender: Send, Sync,
166);
167
168impl NDISender {
169    /// Sends a video frame
170    ///
171    /// This will block until the frame is sent.
172    pub fn send_video_sync(&self, frame: &VideoFrame) -> Result<(), SendFrameError> {
173        let ptr = frame.to_ffi_send_frame_ptr().map_err(|err| match err {
174            crate::frame::generic::FFIReadablePtrError::NotReadable(desc) => {
175                SendFrameError::NotSendable(desc)
176            }
177        })?;
178
179        unsafe {
180            bindings::NDIlib_send_send_video_v2(self.handle.raw_ptr(), ptr);
181            self.write_in_transmission(None);
182        }
183
184        Ok(())
185    }
186
187    /// This updates the Arc reference for the frame that is held in the background of async transmissions
188    ///
189    /// # Safety
190    ///
191    /// This function may drop the frame of a previous transmission through the Arc. If this is called to
192    /// early, it may lead to a use-after-free error and frame glitches.
193    ///
194    /// <https://docs.ndi.video/all/developing-with-ndi/sdk/ndi-send#asynchronous-sending>
195    unsafe fn write_in_transmission(&self, frame: Option<Arc<VideoFrame>>) {
196        match self.in_transmission.lock() {
197            Ok(mut guard) => {
198                *guard = frame;
199            }
200            Err(mut e) => {
201                **e.get_mut() = frame;
202                self.in_transmission.clear_poison();
203            }
204        }
205    }
206
207    /// Sends a video frame asynchronously.
208    ///
209    /// > <https://docs.ndi.video/all/developing-with-ndi/sdk/ndi-send#asynchronous-sending>
210    /// >
211    /// > This function will return immediately and will perform all required operations (including color conversion,
212    /// > compression, and network transmission) asynchronously with the call.
213    /// > Because NDI takes full advantage of asynchronous OS behavior when available, this will normally result in
214    /// > improved performance (as compared to creating your own thread and submitting frames asynchronously with rendering).
215    ///
216    /// # Blocking
217    ///
218    /// This function does not block by default, but it will block in the following cases:
219    /// - The previous frame is still in transmission
220    /// - The sender uses [NDISenderBuilder::clock_video]
221    ///
222    /// This will hold a strong reference to the frame until it is guaranteed to be safely mutated again. This is:
223    /// - After a call to `flush_async_video`
224    /// - After the next call to `send_video_async`
225    /// - After a call to `send_video_sync`
226    /// - When the `NDISender` is dropped (this is not affected by delayed dropping of the sender handle)
227    pub fn send_video_async(&self, frame: Arc<VideoFrame>) -> Result<(), SendFrameError> {
228        let ptr = frame.to_ffi_send_frame_ptr().map_err(|err| match err {
229            FFIReadablePtrError::NotReadable(desc) => SendFrameError::NotSendable(desc),
230        })?;
231
232        unsafe {
233            bindings::NDIlib_send_send_video_async_v2(self.handle.raw_ptr(), ptr);
234            self.write_in_transmission(Some(frame));
235        }
236
237        Ok(())
238    }
239
240    /// Blocks until the current async video transmission is finished.
241    pub fn flush_async_video(&self) {
242        unsafe {
243            bindings::NDIlib_send_send_video_async_v2(self.handle.raw_ptr(), std::ptr::null_mut());
244            self.write_in_transmission(None);
245        }
246    }
247
248    pub fn send_audio(&self, frame: &AudioFrame) -> Result<(), SendFrameError> {
249        let ptr = frame.to_ffi_send_frame_ptr().map_err(|err| match err {
250            FFIReadablePtrError::NotReadable(desc) => SendFrameError::NotSendable(desc),
251        })?;
252
253        unsafe {
254            bindings::NDIlib_send_send_audio_v3(self.handle.raw_ptr(), ptr);
255        }
256
257        Ok(())
258    }
259
260    pub fn send_metadata(&self, frame: &MetadataFrame) -> Result<(), SendFrameError> {
261        let ptr = frame.to_ffi_send_frame_ptr().map_err(|err| match err {
262            FFIReadablePtrError::NotReadable(desc) => SendFrameError::NotSendable(desc),
263        })?;
264
265        unsafe {
266            bindings::NDIlib_send_send_metadata(self.handle.raw_ptr(), ptr);
267        }
268
269        Ok(())
270    }
271
272    /// Receives a metadata frame, works exactly like [super::receiver::NDIReceiver::recv]
273    pub fn recv_metadata(
274        &self,
275        meta: &mut MetadataFrame,
276        timeout: Duration,
277    ) -> Result<Option<()>, NDIRecvError> {
278        let ptr = meta.to_ffi_recv_frame_ptr();
279
280        if ptr.is_null() {
281            eprintln!(
282                "[Warning] The frame given to NDISender::recv_metadata is not writable, ignoring"
283            );
284            return Ok(None);
285        }
286
287        let timeout: u32 = timeout.as_millis().try_into().unwrap_or(u32::MAX);
288
289        let recv_type =
290            unsafe { bindings::NDIlib_send_capture(self.handle.raw_ptr(), ptr, timeout) };
291
292        match recv_type {
293            bindings::NDIlib_frame_type_e_NDIlib_frame_type_video
294            | bindings::NDIlib_frame_type_e_NDIlib_frame_type_audio => {
295                panic!("[Fatal FFI Error] Invalid enum discriminant");
296            }
297            bindings::NDIlib_frame_type_e_NDIlib_frame_type_metadata => {
298                meta.alloc.update_from_sender(self.handle.clone());
299                Ok(Some(()))
300            }
301            bindings::NDIlib_frame_type_e_NDIlib_frame_type_none => Ok(None),
302            discriminant => {
303                eprintln!("NDI SDK returned an unknown frame type: {:?}", discriminant);
304
305                meta.assert_unwritten();
306
307                Err(NDIRecvError::UnknownType)
308            }
309        }
310    }
311
312    /// Returns the current tally state.
313    pub fn get_tally(&self) -> Tally {
314        let mut tally = bindings::NDIlib_tally_t {
315            on_program: false,
316            on_preview: false,
317        };
318
319        unsafe { bindings::NDIlib_send_get_tally(self.handle.raw_ptr(), &mut tally, 0) };
320
321        Tally::from_ffi(&tally)
322    }
323
324    /// Blocks until the tally state changes or the timeout is reached.
325    pub fn get_tally_update(&self, timeout: Duration) -> BlockingUpdate<Tally> {
326        let timeout: u32 = duration_to_ms(timeout);
327
328        let mut tally = bindings::NDIlib_tally_t {
329            on_program: false,
330            on_preview: false,
331        };
332
333        let changed = unsafe {
334            // NDI Docs:
335            // Determine the current tally sate. If you specify a timeout then it will wait until it has changed,
336            // otherwise it will simply poll it and return the current tally immediately. The return value is whether
337            // anything has actually change (true) or whether it timed out (false)
338            bindings::NDIlib_send_get_tally(self.handle.raw_ptr(), &mut tally, timeout)
339        };
340
341        BlockingUpdate::new(Tally::from_ffi(&tally), changed)
342    }
343
344    /// Blocks until the number of connections changes or the timeout is reached.
345    pub fn get_num_connections_update(&self, timeout: Duration) -> usize {
346        let timeout: u32 = duration_to_ms(timeout);
347
348        let no_conns =
349            unsafe { bindings::NDIlib_send_get_no_connections(self.handle.raw_ptr(), timeout) };
350
351        no_conns
352            .try_into()
353            .expect("[Fatal FFI Error] NDI SDK returned an invalid number of connections")
354    }
355
356    /// Sets the failover source, which is used when the receiver cannot receive frames from this source anymore.
357    ///
358    /// <https://docs.ndi.video/all/developing-with-ndi/sdk/ndi-send#failsafe>
359    pub fn set_failover(&self, failover_src: impl NDISourceLike) {
360        failover_src.with_descriptor(|src_ptr| {
361            unsafe { bindings::NDIlib_send_set_failover(self.handle.raw_ptr(), src_ptr) };
362        });
363    }
364
365    /// Get this sources description (includes the name)
366    pub fn get_source<'a>(&'a self) -> NDISourceRef<'a> {
367        let source = unsafe { bindings::NDIlib_send_get_source_name(self.handle.raw_ptr()) };
368
369        unsafe {
370            NDISourceRef::from(
371                *source
372                    .as_ref()
373                    .expect("[Fatal FFI Error] NDI SDK returned nullptr for source descriptor"),
374            )
375        }
376    }
377
378    /// Adds connection metadata which will be sent to every connection in the future.
379    pub fn add_connection_metadata(&self, meta: &MetadataFrame) -> Result<(), SendFrameError> {
380        let ptr = meta.to_ffi_send_frame_ptr().map_err(|err| match err {
381            crate::frame::generic::FFIReadablePtrError::NotReadable(desc) => {
382                SendFrameError::NotSendable(desc)
383            }
384        })?;
385
386        unsafe {
387            bindings::NDIlib_send_add_connection_metadata(self.handle.raw_ptr(), ptr);
388        }
389
390        Ok(())
391    }
392
393    /// Removes all connection metadata that was previously added.
394    pub fn clear_connection_metadata(&self) {
395        unsafe { bindings::NDIlib_send_clear_connection_metadata(self.handle.raw_ptr()) };
396    }
397}
398
399impl Drop for NDISender {
400    fn drop(&mut self) {
401        // This is required because dropping the sender will also (potentially) drop the in-transmission frame,
402        // but the sender handle may outlive this if it has to wait for a metadata frame to be dropped.
403        self.flush_async_video();
404    }
405}
406
407#[derive(Debug, PartialEq, Eq)]
408pub enum SendFrameError {
409    NotSendable(&'static str),
410}