styx_core/
format.rs

1use std::{fmt, num::NonZeroU32, str::FromStr};
2
3/// Four-character code describing a pixel/stream format.
4///
5/// # Example
6/// ```rust
7/// use styx_core::prelude::FourCc;
8///
9/// let fcc = FourCc::new(*b"MJPG");
10/// assert_eq!(fcc.to_string(), "MJPG");
11/// ```
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
13#[cfg_attr(feature = "schema", derive(utoipa::ToSchema))]
14#[cfg_attr(feature = "schema", schema(value_type = String, example = "MJPG"))]
15pub struct FourCc([u8; 4]);
16
17impl FourCc {
18    /// Construct from raw bytes.
19    pub const fn new(bytes: [u8; 4]) -> Self {
20        Self(bytes)
21    }
22
23    /// Little-endian u32 encoding.
24    pub fn to_u32(self) -> u32 {
25        u32::from_le_bytes(self.0)
26    }
27
28    /// Try to convert to a printable string.
29    pub fn as_str(&self) -> Option<&str> {
30        std::str::from_utf8(&self.0).ok()
31    }
32}
33
34impl From<u32> for FourCc {
35    fn from(value: u32) -> Self {
36        Self(value.to_le_bytes())
37    }
38}
39
40impl fmt::Display for FourCc {
41    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
42        if let Some(s) = self.as_str() {
43            write!(f, "{s}")
44        } else {
45            write!(f, "0x{:08x}", self.to_u32())
46        }
47    }
48}
49
50impl FromStr for FourCc {
51    type Err = String;
52
53    fn from_str(s: &str) -> Result<Self, Self::Err> {
54        let bytes = s.as_bytes();
55        if bytes.len() != 4 {
56            return Err("fourcc must be four ASCII bytes".into());
57        }
58        let mut arr = [0u8; 4];
59        arr.copy_from_slice(bytes);
60        Ok(FourCc(arr))
61    }
62}
63
64/// Resolution of a frame.
65///
66/// # Example
67/// ```rust
68/// use styx_core::prelude::Resolution;
69///
70/// let res = Resolution::new(640, 480).unwrap();
71/// assert_eq!(res.width.get(), 640);
72/// ```
73#[derive(Debug, Clone, Copy, PartialEq, Eq)]
74#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
75#[cfg_attr(feature = "schema", derive(utoipa::ToSchema))]
76pub struct Resolution {
77    /// Width in pixels (non-zero).
78    #[cfg_attr(feature = "schema", schema(value_type = u32, minimum = 1))]
79    pub width: NonZeroU32,
80    /// Height in pixels (non-zero).
81    #[cfg_attr(feature = "schema", schema(value_type = u32, minimum = 1))]
82    pub height: NonZeroU32,
83}
84
85impl Resolution {
86    /// Create a resolution, returning `None` if width or height are zero.
87    pub fn new(width: u32, height: u32) -> Option<Self> {
88        Some(Self {
89            width: NonZeroU32::new(width)?,
90            height: NonZeroU32::new(height)?,
91        })
92    }
93}
94
95/// Frame interval (fps) expressed as a rational.
96///
97/// # Example
98/// ```rust
99/// use std::num::NonZeroU32;
100/// use styx_core::prelude::Interval;
101///
102/// let interval = Interval {
103///     numerator: NonZeroU32::new(1).unwrap(),
104///     denominator: NonZeroU32::new(30).unwrap(),
105/// };
106/// assert!(interval.fps() > 0.0);
107/// ```
108#[derive(Debug, Clone, Copy, PartialEq, Eq)]
109#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
110#[cfg_attr(feature = "schema", derive(utoipa::ToSchema))]
111pub struct Interval {
112    /// Numerator of the fps rational.
113    #[cfg_attr(feature = "schema", schema(value_type = u32, minimum = 1))]
114    pub numerator: NonZeroU32,
115    /// Denominator of the fps rational.
116    #[cfg_attr(feature = "schema", schema(value_type = u32, minimum = 1))]
117    pub denominator: NonZeroU32,
118}
119
120impl Interval {
121    /// Frames per second as floating point.
122    pub fn fps(&self) -> f32 {
123        // V4L2 expresses frame intervals as a fraction of seconds per frame
124        // (numerator / denominator), so fps is the inverse.
125        self.denominator.get() as f32 / self.numerator.get() as f32
126    }
127
128    pub fn within(&self, min: Interval, max: Interval) -> bool {
129        // Compare as rational: self between min and max.
130        let self_num = self.numerator.get() as u64;
131        let self_den = self.denominator.get() as u64;
132        let min_num = min.numerator.get() as u64;
133        let min_den = min.denominator.get() as u64;
134        let max_num = max.numerator.get() as u64;
135        let max_den = max.denominator.get() as u64;
136        self_num * min_den >= min_num * self_den && self_num * max_den <= max_num * self_den
137    }
138}
139
140/// Stepwise interval description (min/max/step).
141///
142/// # Example
143/// ```rust
144/// use std::num::NonZeroU32;
145/// use styx_core::prelude::{Interval, IntervalStepwise};
146///
147/// let make = |n, d| Interval {
148///     numerator: NonZeroU32::new(n).unwrap(),
149///     denominator: NonZeroU32::new(d).unwrap(),
150/// };
151/// let stepwise = IntervalStepwise {
152///     min: make(1, 60),
153///     max: make(1, 30),
154///     step: make(1, 30),
155/// };
156/// assert!(stepwise.contains(make(1, 30)));
157/// ```
158#[derive(Debug, Clone, Copy, PartialEq, Eq)]
159#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
160#[cfg_attr(feature = "schema", derive(utoipa::ToSchema))]
161pub struct IntervalStepwise {
162    pub min: Interval,
163    pub max: Interval,
164    pub step: Interval,
165}
166
167impl IntervalStepwise {
168    pub fn contains(&self, candidate: Interval) -> bool {
169        if !candidate.within(self.min, self.max) {
170            return false;
171        }
172        // Rough step check: compare fps spacing.
173        let step_fps = self.step.fps();
174        if step_fps == 0.0 {
175            return true;
176        }
177        let min_fps = self.min.fps();
178        let cand_fps = candidate.fps();
179        let steps = ((cand_fps - min_fps) / step_fps).round();
180        ((cand_fps - min_fps) - steps * step_fps).abs() < 0.001
181    }
182}
183
184/// Basic color space hints.
185#[derive(Debug, Clone, Copy, PartialEq, Eq)]
186#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
187#[cfg_attr(feature = "schema", derive(utoipa::ToSchema))]
188pub enum ColorSpace {
189    /// Standard sRGB.
190    Srgb,
191    /// Rec. 709.
192    Bt709,
193    /// Rec. 2020.
194    Bt2020,
195    /// Unspecified/unknown.
196    Unknown,
197}
198
199/// Media format including code and geometry.
200///
201/// # Example
202/// ```rust
203/// use styx_core::prelude::{ColorSpace, FourCc, MediaFormat, Resolution};
204///
205/// let res = Resolution::new(1920, 1080).unwrap();
206/// let fmt = MediaFormat::new(FourCc::new(*b"RG24"), res, ColorSpace::Srgb);
207/// assert_eq!(fmt.code.to_string(), "RG24");
208/// ```
209#[derive(Debug, Clone, Copy, PartialEq, Eq)]
210#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
211#[cfg_attr(feature = "schema", derive(utoipa::ToSchema))]
212pub struct MediaFormat {
213    /// FourCc code describing pixel layout.
214    #[cfg_attr(feature = "schema", schema(value_type = String))]
215    pub code: FourCc,
216    /// Resolution of the frame.
217    pub resolution: Resolution,
218    /// Color space hint.
219    pub color: ColorSpace,
220}
221
222impl MediaFormat {
223    /// Build a new format.
224    pub fn new(code: FourCc, resolution: Resolution, color: ColorSpace) -> Self {
225        Self {
226            code,
227            resolution,
228            color,
229        }
230    }
231}
232
233#[cfg(feature = "serde")]
234impl serde::Serialize for FourCc {
235    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
236    where
237        S: serde::Serializer,
238    {
239        // Prefer string encoding so decoding does not rely on `deserialize_any`.
240        let encoded = self.as_str().unwrap_or("FFFF");
241        serializer.serialize_str(encoded)
242    }
243}
244
245#[cfg(feature = "serde")]
246impl<'de> serde::Deserialize<'de> for FourCc {
247    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
248    where
249        D: serde::Deserializer<'de>,
250    {
251        struct FourCcVisitor;
252
253        impl<'de> serde::de::Visitor<'de> for FourCcVisitor {
254            type Value = FourCc;
255
256            fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
257                f.write_str("a 4-character FourCc string")
258            }
259
260            fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
261            where
262                E: serde::de::Error,
263            {
264                FourCc::from_str(v).map_err(E::custom)
265            }
266        }
267
268        deserializer.deserialize_str(FourCcVisitor)
269    }
270}