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