ez_ffmpeg/core/frame_export/frame.rs
1//! The owned, packed [`VideoFrame`] output type.
2
3use super::options::PixelLayout;
4
5/// One exported video frame: tightly packed 8-bit pixels plus metadata.
6///
7/// The byte buffer is owned and has no row padding — `as_bytes().len()` equals
8/// `width * height * layout.bytes_per_pixel()` and the row stride equals
9/// `width * layout.bytes_per_pixel()`. This is the shape ML/CV consumers expect
10/// (feed it straight into an ndarray view, a tensor, or an image encoder).
11pub struct VideoFrame {
12 width: u32,
13 height: u32,
14 layout: PixelLayout,
15 pts_us: Option<i64>,
16 index: u64,
17 data: Vec<u8>,
18 /// Drop-time return path to the producing run's buffer pool. `None` once
19 /// [`into_vec`](VideoFrame::into_vec) detaches the buffer (or when the
20 /// frame was built without a pool). Purely an allocation-recycling
21 /// channel: it never blocks and never affects the data contract.
22 recycle: Option<crossbeam_channel::Sender<Vec<u8>>>,
23}
24
25impl VideoFrame {
26 /// Builds a frame from an already-packed, tight buffer. Crate-internal: the
27 /// sink guarantees `data.len() == width * height * layout.bytes_per_pixel()`.
28 /// `recycle` is the sink pool's return path for the buffer (dropped frames
29 /// hand their allocation back for the next frame to reuse).
30 pub(crate) fn new(
31 width: u32,
32 height: u32,
33 layout: PixelLayout,
34 pts_us: Option<i64>,
35 index: u64,
36 data: Vec<u8>,
37 recycle: Option<crossbeam_channel::Sender<Vec<u8>>>,
38 ) -> Self {
39 debug_assert_eq!(
40 data.len(),
41 width as usize * height as usize * layout.bytes_per_pixel(),
42 "VideoFrame buffer must be tightly packed"
43 );
44 Self {
45 width,
46 height,
47 layout,
48 pts_us,
49 index,
50 data,
51 recycle,
52 }
53 }
54
55 /// Frame width in pixels.
56 pub fn width(&self) -> u32 {
57 self.width
58 }
59
60 /// Frame height in pixels.
61 pub fn height(&self) -> u32 {
62 self.height
63 }
64
65 /// The packed pixel layout of [`as_bytes`](VideoFrame::as_bytes).
66 pub fn layout(&self) -> PixelLayout {
67 self.layout
68 }
69
70 /// Post-filter presentation time in microseconds, normalized to the start
71 /// of the extraction window (the stream start when no `start_time_us` was
72 /// set). Vsync passthrough preserves the source timing one-to-one; `None`
73 /// when the frame carried no usable timestamp.
74 pub fn pts_us(&self) -> Option<i64> {
75 self.pts_us
76 }
77
78 /// 0-based export index (counts delivered frames in order).
79 pub fn index(&self) -> u64 {
80 self.index
81 }
82
83 /// The packed pixel bytes. Length is `width * height * bytes_per_pixel`;
84 /// rows are tight (`row_bytes()` each) and top-down.
85 pub fn as_bytes(&self) -> &[u8] {
86 &self.data
87 }
88
89 /// Consumes the frame and returns the owned packed buffer (no copy).
90 pub fn into_vec(mut self) -> Vec<u8> {
91 // Detach from the buffer pool first: `Drop` still runs for `self`, and
92 // with `recycle` cleared it has nothing to send — the caller owns the
93 // allocation outright and the pool simply allocates fresh next time.
94 self.recycle = None;
95 std::mem::take(&mut self.data)
96 }
97
98 /// Bytes per row: `width * layout.bytes_per_pixel()`.
99 pub fn row_bytes(&self) -> usize {
100 self.width as usize * self.layout.bytes_per_pixel()
101 }
102}
103
104impl Drop for VideoFrame {
105 fn drop(&mut self) {
106 if let Some(recycle) = self.recycle.take() {
107 // Non-blocking by design: a full pool or a finished run (receiver
108 // gone) just means the buffer is freed here instead of reused.
109 let _ = recycle.try_send(std::mem::take(&mut self.data));
110 }
111 }
112}
113
114impl std::fmt::Debug for VideoFrame {
115 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
116 f.debug_struct("VideoFrame")
117 .field("width", &self.width)
118 .field("height", &self.height)
119 .field("layout", &self.layout)
120 .field("pts_us", &self.pts_us)
121 .field("index", &self.index)
122 .field("bytes", &self.data.len())
123 .finish()
124 }
125}