Skip to main content

pinray_core/
frame.rs

1use crate::audio::AudioFrame;
2
3/// An axis-aligned rectangle in pixels, used for crop regions and damage.
4#[derive(Debug, Clone, Copy, PartialEq, Eq)]
5pub struct Rect {
6    pub x: i32,
7    pub y: i32,
8    pub width: u32,
9    pub height: u32,
10}
11
12/// Pixel layout of [`VideoFrame::data`].
13///
14/// All backends deliver `Bgra8888` natively and can swizzle to `Rgba8888`;
15/// the remaining formats exist for future zero-copy paths.
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17pub enum PixelFormat {
18    /// 8-bit B, G, R, A byte order — the native format everywhere.
19    Bgra8888,
20    /// 8-bit R, G, B, A byte order (CPU swizzle from BGRA).
21    Rgba8888,
22    /// 24-bit packed RGB, no alpha.
23    Rgb888,
24    /// Biplanar 4:2:0 YCbCr (Y plane + interleaved CbCr).
25    Nv12,
26    /// Planar 4:2:0 YCbCr.
27    I420,
28}
29
30impl PixelFormat {
31    /// Bytes per pixel for packed RGB/RGBA formats.
32    ///
33    /// `None` for the planar/biplanar YUV formats, where a single
34    /// `width * bytes_per_pixel` row size doesn't apply (each plane has its
35    /// own stride and subsampling).
36    pub fn bytes_per_pixel(self) -> Option<usize> {
37        match self {
38            PixelFormat::Bgra8888 | PixelFormat::Rgba8888 => Some(4),
39            PixelFormat::Rgb888 => Some(3),
40            PixelFormat::Nv12 | PixelFormat::I420 => None,
41        }
42    }
43}
44
45/// Color space hint for interpreting pixel values.
46#[derive(Debug, Clone, Copy, PartialEq, Eq)]
47pub enum ColorSpace {
48    Srgb,
49    DisplayP3,
50    Bt709,
51    Bt2020,
52}
53
54/// A Linux DMA-BUF plane handle (zero-copy path, not yet produced).
55#[derive(Debug, Clone, PartialEq, Eq)]
56pub struct DmabufFrame {
57    pub fd: i32,
58    pub offset: u32,
59    pub size: u32,
60    pub stride: u32,
61    pub modifier: u64,
62}
63
64/// A macOS `CVPixelBuffer` pointer (zero-copy path, not yet produced).
65#[derive(Debug, Clone, Copy, PartialEq, Eq)]
66pub struct CvPixelBufferHandle {
67    pub ptr: usize,
68}
69
70/// A Windows `ID3D11Texture2D` pointer (zero-copy path, not yet produced).
71#[derive(Debug, Clone, Copy, PartialEq, Eq)]
72pub struct D3D11TextureHandle {
73    pub ptr: usize,
74}
75
76/// Where a frame's pixels live.
77///
78/// Every backend currently produces `Host` (a plain CPU byte buffer); the
79/// native-handle variants are reserved for opt-in zero-copy support.
80#[derive(Debug, Clone, PartialEq)]
81pub enum FrameData {
82    /// CPU memory, rows packed at [`VideoFrame::stride`] bytes.
83    Host(Vec<u8>),
84    Dmabuf(DmabufFrame),
85    CvPixelBuffer(CvPixelBufferHandle),
86    D3D11Texture(D3D11TextureHandle),
87}
88
89/// One captured video frame with complete layout and timing metadata.
90#[derive(Debug, Clone, PartialEq)]
91pub struct VideoFrame {
92    /// Capture timestamp in nanoseconds, monotonic within a session and
93    /// comparable with [`AudioFrame::stream_time_ns`] of the same session.
94    /// The epoch is platform-dependent (boot time on macOS/Windows,
95    /// process-relative on Linux).
96    pub stream_time_ns: i64,
97    /// Per-stream counter advanced once per delivered frame; a jump means
98    /// frames were dropped.
99    pub sequence: u64,
100    pub width: u32,
101    pub height: u32,
102    /// Bytes from the start of one row to the next in [`FrameData::Host`].
103    pub stride: u32,
104    pub pixel_format: PixelFormat,
105    pub color_space: Option<ColorSpace>,
106    pub data: FrameData,
107    /// Changed regions since the previous frame, when the backend knows.
108    pub damage: Option<Vec<Rect>>,
109}
110
111impl VideoFrame {
112    /// Returns [`FrameData::Host`] bytes with row padding stripped, i.e.
113    /// `width * bytes_per_pixel` bytes per row instead of `stride`.
114    ///
115    /// `stride` can be wider than the tightly-packed row size (platform row
116    /// alignment); copying `data` directly into something that assumes tight
117    /// packing (a `rawvideo` pipe, an image encoder) silently skews every row
118    /// after the first. Use this instead of touching `data` directly.
119    ///
120    /// Returns `None` for non-[`FrameData::Host`] variants, or for pixel
121    /// formats without a well-defined bytes-per-pixel (see
122    /// [`PixelFormat::bytes_per_pixel`]).
123    ///
124    /// # Examples
125    ///
126    /// ```
127    /// use pinray_core::{FrameData, PixelFormat, VideoFrame};
128    ///
129    /// let (width, height, stride) = (2u32, 2u32, 12u32); // 4 padding bytes/row
130    /// let mut data = Vec::new();
131    /// for row in 0..height {
132    ///     data.extend(std::iter::repeat_n(row as u8, width as usize * 4));
133    ///     data.extend(std::iter::repeat_n(0xAA, (stride - width * 4) as usize));
134    /// }
135    /// let frame = VideoFrame {
136    ///     stream_time_ns: 0,
137    ///     sequence: 0,
138    ///     width,
139    ///     height,
140    ///     stride,
141    ///     pixel_format: PixelFormat::Bgra8888,
142    ///     color_space: None,
143    ///     data: FrameData::Host(data),
144    ///     damage: None,
145    /// };
146    ///
147    /// let tight = frame.to_tight_bytes().unwrap();
148    /// assert_eq!(tight.len(), (width * height * 4) as usize);
149    /// assert!(tight.iter().all(|&b| b != 0xAA)); // padding is gone
150    /// ```
151    pub fn to_tight_bytes(&self) -> Option<Vec<u8>> {
152        let FrameData::Host(buf) = &self.data else {
153            return None;
154        };
155        let bpp = self.pixel_format.bytes_per_pixel()?;
156        let row_bytes = self.width as usize * bpp;
157        if self.stride as usize == row_bytes {
158            return Some(buf.clone());
159        }
160        let mut out = Vec::with_capacity(row_bytes * self.height as usize);
161        for row in 0..self.height as usize {
162            let start = row * self.stride as usize;
163            out.extend_from_slice(&buf[start..start + row_bytes]);
164        }
165        Some(out)
166    }
167}
168
169/// Why a [`GapEvent`] was emitted.
170#[derive(Debug, Clone, Copy, PartialEq, Eq)]
171pub enum GapReason {
172    /// Frames were dropped because the consumer fell behind.
173    Dropped,
174    /// The stream format changed mid-session.
175    FormatChanged,
176    /// The backend recovered from losing its capture source (e.g. a display
177    /// mode switch invalidating DXGI duplication).
178    BackendRestarted,
179    Unknown,
180}
181
182/// A discontinuity notice: frames were lost or the stream restarted.
183#[derive(Debug, Clone, PartialEq, Eq)]
184pub struct GapEvent {
185    pub stream_time_ns: i64,
186    pub reason: GapReason,
187    pub dropped_frames: Option<u32>,
188}
189
190/// What [`CaptureSession::next_event`](crate::CaptureSession::next_event)
191/// yields.
192#[derive(Debug, Clone, PartialEq)]
193pub enum CaptureEvent {
194    Video(VideoFrame),
195    Audio(AudioFrame),
196    Gap(GapEvent),
197    /// The stream ended and no further events will arrive.
198    End,
199}