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}