Skip to main content

grafton_ndi/
capture.rs

1//! Unified frame capture abstraction via trait-based generics.
2//!
3//! This module provides a single generic RAII guard and capture path for all frame types
4//! (video, audio, metadata), eliminating duplication while maintaining zero-cost abstraction.
5//!
6//! # Architecture
7//!
8//! Two sealed traits split the abstraction along its two real axes:
9//!
10//! - [`FrameFree`] is the *free strategy*: the SDK instance handle type plus the
11//!   `free_*` call that releases one captured frame. The generic RAII guard
12//!   [`Guard<'owner, S>`](Guard) is written once against it, so every owning
13//!   surface — the [`Receiver`](crate::Receiver) capture path and
14//!   [`FrameSync`](crate::FrameSync) — shares one guard rather than hand-rolling
15//!   its own.
16//! - [`CaptureKind`] builds on `FrameFree` for the receiver capture path only,
17//!   adding the `NDIlib_recv_capture_v3` routing, the borrowed/owned frame
18//!   types, and the frame-type discriminant.
19//!
20//! # Example
21//!
22//! ```ignore
23//! // Internal use only - this module is not part of the public API
24//! use crate::capture::{Guard, VideoKind};
25//!
26//! let guard = unsafe { Guard::<VideoKind>::new(instance, frame) };
27//! // Guard automatically calls the correct free function when dropped
28//! ```
29
30use std::{marker::PhantomData, rc::Rc};
31
32use crate::{
33    frames::{
34        AudioFrame, AudioFrameRef, MetadataFrame, MetadataFrameRef, VideoFrame, VideoFrameRef,
35    },
36    ndi_lib::*,
37    Result,
38};
39
40/// Sealed trait module to prevent external implementations of [`FrameFree`] and
41/// [`CaptureKind`].
42mod sealed {
43    pub trait Sealed {}
44
45    impl Sealed for super::VideoKind {}
46    impl Sealed for super::AudioKind {}
47    impl Sealed for super::MetadataKind {}
48    impl Sealed for super::FrameSyncVideoFree {}
49    impl Sealed for super::FrameSyncAudioFree {}
50}
51
52/// The *free strategy* for one captured-frame family: the SDK instance handle
53/// type plus the SDK call that releases a single frame.
54///
55/// This is the single axis along which the RAII `Guard` varies. The receiver
56/// kinds ([`VideoKind`], [`AudioKind`], [`MetadataKind`]) free through
57/// `NDIlib_recv_free_*`; the FrameSync strategies ([`FrameSyncVideoFree`],
58/// [`FrameSyncAudioFree`]) free through `NDIlib_framesync_free_*`. Factoring the
59/// free call out of [`CaptureKind`] lets both families reuse one guard and one
60/// borrowed-reference core instead of maintaining parallel copies.
61///
62/// This is a sealed trait — it cannot be implemented outside this crate.
63///
64/// # Safety
65///
66/// Implementors must ensure [`free`](Self::free) releases a frame that was
67/// populated by the matching SDK capture call through the same `instance`.
68pub trait FrameFree: sealed::Sealed + 'static {
69    /// The SDK instance handle that owns the frame buffers (and through which
70    /// they must be freed).
71    type Instance: Copy;
72
73    /// The raw FFI frame type from the NDI SDK.
74    type RawFrame: Default + Copy;
75
76    /// The `Debug` struct name of the borrowed reference that wraps this guard
77    /// (e.g. `"VideoFrameRef"` or `"FrameSyncVideoRef"`), so the shared generic
78    /// `Debug` impls render the historically-correct type name.
79    const REF_DEBUG_NAME: &'static str;
80
81    /// Free a single captured frame through its owning instance.
82    ///
83    /// # Safety
84    ///
85    /// - `instance` must be the instance that produced `frame` (the FrameSync
86    ///   strategies additionally tolerate a null `instance` by short-circuiting).
87    /// - `frame` must have been populated by a successful capture for this
88    ///   strategy and not already freed.
89    unsafe fn free(instance: Self::Instance, frame: &mut Self::RawFrame);
90}
91
92/// Single source of truth describing one NDI frame kind (video, audio, or
93/// metadata).
94///
95/// Each implementor wires together the FFI frame struct, the borrowed and owned
96/// Rust frame types, and the four SDK calls those entail — capture, the
97/// frame-type discriminant, freeing, and the borrowed/owned conversions. Every
98/// generic capture path (`capture_raw`, [`Receiver`](crate::Receiver)'s
99/// [`Capture`](crate::Capture) view, and the async views) is written once
100/// against this trait, so adding or changing a kind happens in exactly one
101/// place.
102///
103/// This is a sealed trait — it cannot be implemented outside this crate.
104/// Implementations exist for [`VideoKind`], [`AudioKind`], and [`MetadataKind`].
105///
106/// # Safety
107///
108/// Implementors must ensure the members agree on the same
109/// [`RawFrame`](FrameFree::RawFrame): `capture_into` populates it via
110/// `NDIlib_recv_capture_v3`, [`free`](FrameFree::free) frees frames so populated,
111/// and `make_ref` wraps one that `capture_into` reported as
112/// [`FRAME_TYPE`](Self::FRAME_TYPE).
113pub trait CaptureKind: FrameFree<Instance = NDIlib_recv_instance_t> + Sized {
114    /// The borrowed, zero-copy view of a captured frame, tied to the receiver
115    /// that produced it.
116    type Ref<'rx>;
117
118    /// The owned, `'static` frame produced by copying a borrowed view.
119    type Owned;
120
121    /// The frame-type discriminant `NDIlib_recv_capture_v3` returns for this kind.
122    const FRAME_TYPE: NDIlib_frame_type_e;
123
124    /// Run a capture for this kind, routing `frame` into the matching
125    /// `NDIlib_recv_capture_v3` slot and ignoring the others.
126    ///
127    /// # Safety
128    ///
129    /// - `instance` must be a valid NDI receiver instance.
130    /// - `frame` must point to a writable [`RawFrame`](FrameFree::RawFrame).
131    unsafe fn capture_into(
132        instance: NDIlib_recv_instance_t,
133        frame: *mut Self::RawFrame,
134        timeout_ms: u32,
135    ) -> NDIlib_frame_type_e;
136
137    /// Wrap a freshly captured frame guard in its borrowed view, validating any
138    /// kind-specific invariants (e.g. FourCC) during construction.
139    ///
140    /// # Safety
141    ///
142    /// `guard` must own a frame that `capture_into` reported as
143    /// [`FRAME_TYPE`](Self::FRAME_TYPE).
144    unsafe fn make_ref<'rx>(guard: Guard<'rx, Self>) -> Result<Self::Ref<'rx>>;
145
146    /// Copy a borrowed view into an owned, `'static` frame.
147    fn ref_to_owned(frame: &Self::Ref<'_>) -> Result<Self::Owned>;
148}
149
150/// Marker type for video frame capture operations.
151pub struct VideoKind;
152
153impl FrameFree for VideoKind {
154    type Instance = NDIlib_recv_instance_t;
155    type RawFrame = NDIlib_video_frame_v2_t;
156    const REF_DEBUG_NAME: &'static str = "VideoFrameRef";
157
158    unsafe fn free(instance: Self::Instance, frame: &mut Self::RawFrame) {
159        NDIlib_recv_free_video_v2(instance, frame);
160    }
161}
162
163impl CaptureKind for VideoKind {
164    type Ref<'rx> = VideoFrameRef<'rx>;
165    type Owned = VideoFrame;
166    const FRAME_TYPE: NDIlib_frame_type_e = NDIlib_frame_type_e_NDIlib_frame_type_video;
167
168    unsafe fn capture_into(
169        instance: NDIlib_recv_instance_t,
170        frame: *mut Self::RawFrame,
171        timeout_ms: u32,
172    ) -> NDIlib_frame_type_e {
173        NDIlib_recv_capture_v3(
174            instance,
175            frame,
176            std::ptr::null_mut(), // no audio
177            std::ptr::null_mut(), // no metadata
178            timeout_ms,
179        )
180    }
181
182    unsafe fn make_ref<'rx>(guard: Guard<'rx, Self>) -> Result<Self::Ref<'rx>> {
183        VideoFrameRef::new(guard)
184    }
185
186    fn ref_to_owned(frame: &Self::Ref<'_>) -> Result<Self::Owned> {
187        frame.to_owned()
188    }
189}
190
191/// Marker type for audio frame capture operations.
192pub struct AudioKind;
193
194impl FrameFree for AudioKind {
195    type Instance = NDIlib_recv_instance_t;
196    type RawFrame = NDIlib_audio_frame_v3_t;
197    const REF_DEBUG_NAME: &'static str = "AudioFrameRef";
198
199    unsafe fn free(instance: Self::Instance, frame: &mut Self::RawFrame) {
200        NDIlib_recv_free_audio_v3(instance, frame);
201    }
202}
203
204impl CaptureKind for AudioKind {
205    type Ref<'rx> = AudioFrameRef<'rx>;
206    type Owned = AudioFrame;
207    const FRAME_TYPE: NDIlib_frame_type_e = NDIlib_frame_type_e_NDIlib_frame_type_audio;
208
209    unsafe fn capture_into(
210        instance: NDIlib_recv_instance_t,
211        frame: *mut Self::RawFrame,
212        timeout_ms: u32,
213    ) -> NDIlib_frame_type_e {
214        NDIlib_recv_capture_v3(
215            instance,
216            std::ptr::null_mut(), // no video
217            frame,
218            std::ptr::null_mut(), // no metadata
219            timeout_ms,
220        )
221    }
222
223    unsafe fn make_ref<'rx>(guard: Guard<'rx, Self>) -> Result<Self::Ref<'rx>> {
224        AudioFrameRef::new(guard)
225    }
226
227    fn ref_to_owned(frame: &Self::Ref<'_>) -> Result<Self::Owned> {
228        frame.to_owned()
229    }
230}
231
232/// Marker type for metadata frame capture operations.
233pub struct MetadataKind;
234
235impl FrameFree for MetadataKind {
236    type Instance = NDIlib_recv_instance_t;
237    type RawFrame = NDIlib_metadata_frame_t;
238    const REF_DEBUG_NAME: &'static str = "MetadataFrameRef";
239
240    unsafe fn free(instance: Self::Instance, frame: &mut Self::RawFrame) {
241        NDIlib_recv_free_metadata(instance, frame);
242    }
243}
244
245impl CaptureKind for MetadataKind {
246    type Ref<'rx> = MetadataFrameRef<'rx>;
247    type Owned = MetadataFrame;
248    const FRAME_TYPE: NDIlib_frame_type_e = NDIlib_frame_type_e_NDIlib_frame_type_metadata;
249
250    unsafe fn capture_into(
251        instance: NDIlib_recv_instance_t,
252        frame: *mut Self::RawFrame,
253        timeout_ms: u32,
254    ) -> NDIlib_frame_type_e {
255        NDIlib_recv_capture_v3(
256            instance,
257            std::ptr::null_mut(), // no video
258            std::ptr::null_mut(), // no audio
259            frame,
260            timeout_ms,
261        )
262    }
263
264    unsafe fn make_ref<'rx>(guard: Guard<'rx, Self>) -> Result<Self::Ref<'rx>> {
265        MetadataFrameRef::new(guard)
266    }
267
268    fn ref_to_owned(frame: &Self::Ref<'_>) -> Result<Self::Owned> {
269        // Metadata `to_owned` is infallible; lift it into the shared `Result` shape.
270        Ok(frame.to_owned())
271    }
272}
273
274/// Free strategy for video frames captured through [`FrameSync`](crate::FrameSync).
275///
276/// Frees via `NDIlib_framesync_free_video`. Unlike the receiver strategies this
277/// tolerates a null instance handle (the borrowed-frame tests construct guards
278/// with a null instance), short-circuiting the free in that case.
279pub struct FrameSyncVideoFree;
280
281impl FrameFree for FrameSyncVideoFree {
282    type Instance = NDIlib_framesync_instance_t;
283    type RawFrame = NDIlib_video_frame_v2_t;
284    const REF_DEBUG_NAME: &'static str = "FrameSyncVideoRef";
285
286    unsafe fn free(instance: Self::Instance, frame: &mut Self::RawFrame) {
287        if instance.is_null() {
288            return;
289        }
290        NDIlib_framesync_free_video(instance, frame);
291    }
292}
293
294/// Free strategy for audio frames captured through [`FrameSync`](crate::FrameSync).
295///
296/// Frees via `NDIlib_framesync_free_audio_v2`, tolerating a null instance handle
297/// the same way [`FrameSyncVideoFree`] does.
298pub struct FrameSyncAudioFree;
299
300impl FrameFree for FrameSyncAudioFree {
301    type Instance = NDIlib_framesync_instance_t;
302    type RawFrame = NDIlib_audio_frame_v3_t;
303    const REF_DEBUG_NAME: &'static str = "FrameSyncAudioRef";
304
305    unsafe fn free(instance: Self::Instance, frame: &mut Self::RawFrame) {
306        if instance.is_null() {
307            return;
308        }
309        NDIlib_framesync_free_audio_v2(instance, frame);
310    }
311}
312
313/// Generic RAII guard for captured NDI frames.
314///
315/// This guard ensures that captured frames are freed exactly once via the free
316/// strategy `S`'s [`FrameFree::free`] call. The strategy is determined by the
317/// `S: FrameFree` type parameter, enabling compile-time dispatch with zero
318/// runtime cost — receiver captures use `NDIlib_recv_free_*`, FrameSync captures
319/// use `NDIlib_framesync_free_*`, all through the same guard.
320///
321/// The lifetime parameter `'owner` ties this guard to the owner that created it
322/// (a `Receiver` or a `FrameSync`), preventing use-after-free by ensuring the
323/// owner cannot be dropped while this guard is alive.
324///
325/// # Type Parameters
326///
327/// - `'owner`: Lifetime of the owning instance's borrow
328/// - `S`: The free strategy (video/audio/metadata, receiver or FrameSync)
329///
330/// # Safety
331///
332/// This struct stores raw FFI types and must only be created through the `unsafe fn new()`
333/// constructor, which requires the caller to guarantee validity of the instance and frame.
334pub struct Guard<'owner, S: FrameFree> {
335    instance: S::Instance,
336    frame: S::RawFrame,
337    // Ties the guard (and the borrowed refs built on it) to the owning instance's
338    // borrow. The owner type is irrelevant to the borrow checker, so a unit
339    // reference is enough to carry `'owner` covariantly.
340    _owner: PhantomData<&'owner ()>,
341    // SDK-owned buffers must be freed through the originating instance, on the
342    // originating thread. Keep the guard (and borrowed frame refs) deliberately
343    // !Send/!Sync rather than relying on raw-pointer auto-traits from generated
344    // bindings.
345    _thread_affine: PhantomData<Rc<()>>,
346}
347
348impl<'owner, S: FrameFree> Guard<'owner, S> {
349    /// Create a new frame guard.
350    ///
351    /// # Safety
352    ///
353    /// The caller must ensure that:
354    /// - `instance` is a valid instance for the free strategy `S` (or null only
355    ///   for the FrameSync strategies, which short-circuit their free)
356    /// - `frame` was populated by a successful capture for `S` and not yet freed
357    pub(crate) unsafe fn new(instance: S::Instance, frame: S::RawFrame) -> Self {
358        Self {
359            instance,
360            frame,
361            _owner: PhantomData,
362            _thread_affine: PhantomData,
363        }
364    }
365
366    /// Get a reference to the underlying raw frame.
367    pub(crate) fn frame(&self) -> &S::RawFrame {
368        &self.frame
369    }
370}
371
372impl<'owner, S: FrameFree> Drop for Guard<'owner, S> {
373    fn drop(&mut self) {
374        // SAFETY: The constructor guarantees that instance and frame are valid
375        unsafe {
376            S::free(self.instance, &mut self.frame);
377        }
378    }
379}
380
381/// Result of a generic capture operation.
382///
383/// This enum represents the possible outcomes of calling `NDIlib_recv_capture_v3`:
384/// - `Frame`: Successfully captured a frame of the expected type
385/// - `None`: No frame available (timeout)
386/// - `Error`: The SDK returned an error frame
387pub(crate) enum CaptureResult<'rx, K: CaptureKind> {
388    /// Successfully captured a frame of the expected type.
389    Frame(Guard<'rx, K>),
390    /// No frame available within timeout, or a different frame type was returned.
391    None,
392    /// The SDK returned an error frame.
393    Error,
394}
395
396/// Capture a frame of kind `K` from an NDI receiver.
397///
398/// Routes the frame into the matching `NDIlib_recv_capture_v3` slot via
399/// [`CaptureKind::capture_into`] and classifies the result. Other frame types
400/// (e.g. status-change) are treated as [`CaptureResult::None`].
401///
402/// # Safety
403///
404/// `instance` must be a valid NDI receiver instance.
405pub(crate) unsafe fn capture_raw<'rx, K: CaptureKind>(
406    instance: NDIlib_recv_instance_t,
407    timeout_ms: u32,
408) -> CaptureResult<'rx, K> {
409    let mut frame = K::RawFrame::default();
410    let frame_type = K::capture_into(instance, &mut frame, timeout_ms);
411
412    match frame_type {
413        t if t == K::FRAME_TYPE => CaptureResult::Frame(Guard::<K>::new(instance, frame)),
414        NDIlib_frame_type_e_NDIlib_frame_type_none => CaptureResult::None,
415        NDIlib_frame_type_e_NDIlib_frame_type_error => CaptureResult::Error,
416        _ => CaptureResult::None, // Other frame types are ignored
417    }
418}
419
420// Convenience aliases for the receiver guard kinds that have a kind-specific
421// constructor (audio validates a concrete layout; metadata wraps its own ref).
422// Video and the FrameSync families name `Guard<'_, Strategy>` directly.
423/// RAII guard for a captured audio frame.
424///
425/// Automatically calls `NDIlib_recv_free_audio_v3` when dropped.
426pub type RecvAudioGuard<'rx> = Guard<'rx, AudioKind>;
427
428/// RAII guard for a captured metadata frame.
429///
430/// Automatically calls `NDIlib_recv_free_metadata` when dropped.
431pub type RecvMetadataGuard<'rx> = Guard<'rx, MetadataKind>;
432
433#[cfg(test)]
434mod tests {
435    use super::*;
436
437    #[test]
438    fn test_guard_sizes() {
439        use std::mem::size_of;
440
441        // Guards should be compact - instance pointer + frame struct + zero-sized PhantomData
442        assert!(size_of::<RecvAudioGuard>() > 0);
443        assert!(size_of::<RecvMetadataGuard>() > 0);
444
445        // Generic guard with different kinds should have same overhead per kind
446        // (sizes differ only due to RawFrame size differences)
447        assert!(size_of::<Guard<VideoKind>>() > 0);
448        assert!(size_of::<Guard<AudioKind>>() > 0);
449        assert!(size_of::<Guard<MetadataKind>>() > 0);
450        assert!(size_of::<Guard<FrameSyncVideoFree>>() > 0);
451        assert!(size_of::<Guard<FrameSyncAudioFree>>() > 0);
452    }
453
454    #[test]
455    fn test_frame_type_constants() {
456        // Verify frame type constants match expected values
457        assert_eq!(
458            VideoKind::FRAME_TYPE,
459            NDIlib_frame_type_e_NDIlib_frame_type_video
460        );
461        assert_eq!(
462            AudioKind::FRAME_TYPE,
463            NDIlib_frame_type_e_NDIlib_frame_type_audio
464        );
465        assert_eq!(
466            MetadataKind::FRAME_TYPE,
467            NDIlib_frame_type_e_NDIlib_frame_type_metadata
468        );
469    }
470}