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}