styx_capture/
lib.rs

1#![doc = include_str!("../README.md")]
2
3use smallvec::SmallVec;
4
5use styx_core::prelude::*;
6
7/// Identifier for a capture mode keyed by its format and optional interval.
8///
9/// # Example
10/// ```rust
11/// use styx_capture::prelude::*;
12///
13/// let res = Resolution::new(640, 480).unwrap();
14/// let format = MediaFormat::new(FourCc::new(*b"RG24"), res, ColorSpace::Srgb);
15/// let id = ModeId { format, interval: None };
16/// assert_eq!(id.format.code.to_string(), "RG24");
17/// ```
18#[derive(Debug, Clone, PartialEq, Eq)]
19#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
20#[cfg_attr(feature = "schema", derive(utoipa::ToSchema))]
21pub struct ModeId {
22    /// Pixel format and resolution for this mode.
23    pub format: MediaFormat,
24    /// Optional interval associated with this mode (if the mode is interval-specific).
25    pub interval: Option<Interval>,
26}
27
28/// Descriptor for a single capture mode (format + intervals).
29///
30/// # Example
31/// ```rust
32/// use styx_capture::prelude::*;
33///
34/// let res = Resolution::new(320, 240).unwrap();
35/// let format = MediaFormat::new(FourCc::new(*b"RG24"), res, ColorSpace::Srgb);
36/// let mode = Mode {
37///     id: ModeId { format, interval: None },
38///     format,
39///     intervals: smallvec::smallvec![],
40///     interval_stepwise: None,
41/// };
42/// assert_eq!(mode.format.code.to_string(), "RG24");
43/// ```
44#[derive(Debug, Clone)]
45#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
46#[cfg_attr(feature = "schema", derive(utoipa::ToSchema))]
47pub struct Mode {
48    /// Identifier (format + optional interval) for this mode.
49    pub id: ModeId,
50    /// Media format associated with the mode.
51    pub format: MediaFormat,
52    /// Supported frame intervals.
53    #[cfg_attr(feature = "schema", schema(value_type = Vec<Interval>))]
54    pub intervals: SmallVec<[Interval; 4]>,
55    /// Optional stepwise interval range.
56    #[cfg_attr(feature = "schema", schema(value_type = Option<IntervalStepwise>))]
57    pub interval_stepwise: Option<IntervalStepwise>,
58}
59
60/// Descriptor for a capture device/source.
61///
62/// # Example
63/// ```rust
64/// use styx_capture::prelude::*;
65///
66/// let res = Resolution::new(320, 240).unwrap();
67/// let format = MediaFormat::new(FourCc::new(*b"RG24"), res, ColorSpace::Srgb);
68/// let mode = Mode {
69///     id: ModeId { format, interval: None },
70///     format,
71///     intervals: smallvec::smallvec![],
72///     interval_stepwise: None,
73/// };
74/// let descriptor = CaptureDescriptor { modes: vec![mode], controls: Vec::new() };
75/// assert_eq!(descriptor.modes.len(), 1);
76/// ```
77#[derive(Debug, Clone)]
78#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
79#[cfg_attr(feature = "schema", derive(utoipa::ToSchema))]
80pub struct CaptureDescriptor {
81    /// Supported modes.
82    pub modes: Vec<Mode>,
83    /// Supported controls.
84    pub controls: Vec<ControlMeta>,
85}
86
87/// User-selected configuration validated against a descriptor.
88///
89/// # Example
90/// ```rust
91/// use styx_capture::prelude::*;
92///
93/// let res = Resolution::new(320, 240).unwrap();
94/// let format = MediaFormat::new(FourCc::new(*b"RG24"), res, ColorSpace::Srgb);
95/// let mode = Mode {
96///     id: ModeId { format, interval: None },
97///     format,
98///     intervals: smallvec::smallvec![],
99///     interval_stepwise: None,
100/// };
101/// let descriptor = CaptureDescriptor { modes: vec![mode.clone()], controls: Vec::new() };
102/// let cfg = CaptureConfig { mode: mode.id.clone(), interval: None, controls: vec![] };
103/// assert!(cfg.validate(&descriptor).is_ok());
104/// ```
105#[derive(Debug, Clone)]
106#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
107#[cfg_attr(feature = "schema", derive(utoipa::ToSchema))]
108pub struct CaptureConfig {
109    /// Selected mode.
110    pub mode: ModeId,
111    /// Optional interval override.
112    pub interval: Option<Interval>,
113    /// Control assignments.
114    pub controls: Vec<(ControlId, ControlValue)>,
115}
116
117impl CaptureConfig {
118    /// Validate a config against a descriptor.
119    pub fn validate(&self, descriptor: &CaptureDescriptor) -> Result<(), String> {
120        let mode = descriptor
121            .modes
122            .iter()
123            .find(|m| m.id.format == self.mode.format)
124            .ok_or_else(|| "mode not found".to_string())?;
125
126        let interval = self.mode.interval.or(self.interval);
127        if let Some(interval) = &interval {
128            // Some backends (notably libcamera) may not advertise frame interval data even though
129            // they can accept an interval request via controls. If the mode provides no interval
130            // metadata at all, allow any interval through validation.
131            let has_interval_metadata =
132                !mode.intervals.is_empty() || mode.interval_stepwise.is_some();
133            if has_interval_metadata {
134                let supported = mode.intervals.iter().any(|iv| iv == interval)
135                    || mode
136                        .interval_stepwise
137                        .as_ref()
138                        .map(|sw| sw.contains(*interval))
139                        .unwrap_or(false);
140                if !supported {
141                    return Err("interval not supported by mode".into());
142                }
143            }
144        }
145
146        for (id, value) in &self.controls {
147            let Some(meta) = descriptor.controls.iter().find(|c| c.id == *id) else {
148                return Err(format!("control {:?} not supported by descriptor", id));
149            };
150            if matches!(meta.access, Access::ReadOnly) {
151                return Err(format!("control {} is read-only", meta.name));
152            }
153            if !meta.validate(value) {
154                return Err(format!("control {} rejected value", meta.name));
155            }
156        }
157
158        Ok(())
159    }
160}
161
162/// Trait implemented by capture backends that yield zero-copy frames.
163///
164/// # Example
165/// ```rust,ignore
166/// use styx_capture::prelude::*;
167///
168/// struct MySource;
169/// impl CaptureSource for MySource {
170///     fn descriptor(&self) -> &CaptureDescriptor { unimplemented!() }
171///     fn next_frame(&self) -> Option<FrameLease> { None }
172/// }
173/// ```
174pub trait CaptureSource: Send + Sync {
175    /// Descriptor for this source.
176    fn descriptor(&self) -> &CaptureDescriptor;
177
178    /// Pull the next frame; concrete backends decide how to block/yield.
179    fn next_frame(&self) -> Option<FrameLease>;
180}
181
182/// Helper to construct a simple frame from a pooled buffer.
183///
184/// # Example
185/// ```rust
186/// use styx_capture::prelude::*;
187///
188/// let pool = BufferPool::with_capacity(1, 64);
189/// let res = Resolution::new(2, 2).unwrap();
190/// let format = MediaFormat::new(FourCc::new(*b"RG24"), res, ColorSpace::Srgb);
191/// let frame = build_frame_from_pool(format, &pool, 0, 3);
192/// assert_eq!(frame.meta().format.code.to_string(), "RG24");
193/// ```
194pub fn build_frame_from_pool(
195    format: MediaFormat,
196    pool: &BufferPool,
197    timestamp: u64,
198    bytes_per_pixel: usize,
199) -> FrameLease {
200    let layout = plane_layout_from_dims(
201        format.resolution.width,
202        format.resolution.height,
203        bytes_per_pixel,
204    );
205    let meta = FrameMeta::new(format, timestamp);
206    FrameLease::single_plane(meta, pool.lease(), layout.len, layout.stride)
207}
208
209/// Utility to create a mode id list from formats.
210///
211/// # Example
212/// ```rust
213/// use styx_capture::prelude::*;
214///
215/// let res = Resolution::new(2, 2).unwrap();
216/// let formats = [MediaFormat::new(FourCc::new(*b"RG24"), res, ColorSpace::Srgb)];
217/// let modes = modes_from_formats(formats);
218/// assert_eq!(modes.len(), 1);
219/// ```
220pub fn modes_from_formats(formats: impl IntoIterator<Item = MediaFormat>) -> Vec<Mode> {
221    formats
222        .into_iter()
223        .map(|format| Mode {
224            id: ModeId {
225                format,
226                interval: None,
227            },
228            format,
229            intervals: SmallVec::new(),
230            interval_stepwise: None,
231        })
232        .collect()
233}
234
235pub mod virtual_backend;
236
237pub mod prelude {
238    pub use crate::{
239        CaptureConfig, CaptureDescriptor, CaptureSource, Mode, ModeId, build_frame_from_pool,
240        modes_from_formats, virtual_backend::VirtualCapture,
241    };
242    pub use styx_core::prelude::*;
243}