1#![doc = include_str!("../README.md")]
2use smallvec::smallvec;
3use std::num::NonZeroU32;
4use std::panic::catch_unwind;
5use styx_capture::prelude::*;
6use styx_core::controls::{Access, ControlKind, ControlValue};
7use v4l::format::Colorspace as V4lColorspace;
8use v4l::{capability::Flags, framesize::FrameSizeEnum, prelude::*, video::Capture};
9
10fn read_node_name(path: &std::path::Path) -> Option<String> {
11 let node = path.file_name()?.to_string_lossy();
12 let sysfs = format!("/sys/class/video4linux/{node}/name");
13 std::fs::read_to_string(sysfs)
14 .ok()
15 .map(|s| s.trim().to_string())
16}
17
18pub struct V4l2DeviceInfo {
20 pub path: String,
21 pub name: Option<String>,
22 pub card: String,
23 pub driver: String,
24 pub bus_info: String,
25 pub properties: Vec<(String, String)>,
26 pub descriptor: CaptureDescriptor,
27}
28
29pub fn probe_devices() -> (Vec<V4l2DeviceInfo>, Vec<String>) {
31 let mut devices = Vec::new();
32 let mut errors = Vec::new();
33 for dev in v4l::context::enum_devices() {
34 match build_info(dev.path()) {
35 Ok(info) => devices.push(info),
36 Err(e) => errors.push(format!("{}: {e}", dev.path().display())),
37 };
38 }
39 (devices, errors)
40}
41
42fn build_info(path: &std::path::Path) -> Result<V4l2DeviceInfo, Box<dyn std::error::Error>> {
43 let dev = Device::with_path(path)?;
44 let caps = dev.query_caps()?;
45 let node_name = read_node_name(path);
46
47 if !(caps.capabilities.contains(Flags::VIDEO_CAPTURE)
48 || caps.capabilities.contains(Flags::VIDEO_CAPTURE_MPLANE))
49 {
50 return Err("not a capture device".into());
52 }
53 let card = caps.card;
54 let driver = caps.driver;
55 let bus_info = caps.bus;
56 let driver_lc = driver.to_ascii_lowercase();
57 let card_lc = card.to_ascii_lowercase();
58
59 let name_lc = node_name
61 .as_deref()
62 .unwrap_or_default()
63 .to_ascii_lowercase();
64 if card_lc.contains("virtual")
65 || driver_lc.contains("virtual")
66 || driver_lc.contains("pispbe")
67 || card_lc.contains("pispbe")
68 || card_lc.contains("pisp")
69 || driver_lc.contains("rp1-cfe")
70 || card_lc.contains("rp1-cfe")
71 || name_lc.contains("rp1-cfe")
72 || name_lc.contains("embedded")
73 || name_lc.contains("config")
74 || name_lc.contains("fe_")
75 || name_lc.contains("stats")
76 {
77 return Err("filtered non-camera node".into());
78 }
79
80 let mut modes = Vec::new();
83 let default_color = dev
84 .format()
85 .ok()
86 .map(|fmt| map_color_space(Some(fmt.colorspace)))
87 .unwrap_or(ColorSpace::Unknown);
88 let formats = dev.enum_formats().unwrap_or_default();
89 for fmt in formats {
90 let fourcc = FourCc::from(u32::from_le_bytes(fmt.fourcc.repr));
91 let color = if default_color != ColorSpace::Unknown {
92 default_color
93 } else {
94 guess_color_space(fourcc)
95 };
96 let framesizes = match dev.enum_framesizes(fmt.fourcc) {
97 Ok(sizes) => sizes,
98 Err(_) => continue,
99 };
100 for size in framesizes {
102 match size.size {
103 FrameSizeEnum::Discrete(fs) => {
104 if let Some(res) = Resolution::new(fs.width, fs.height) {
105 let mut intervals = smallvec![];
106 let ivals = dev
107 .enum_frameintervals(fmt.fourcc, fs.width, fs.height)
108 .unwrap_or_default();
109 for iv in ivals {
110 if let v4l::frameinterval::FrameIntervalEnum::Discrete(discrete) =
111 iv.interval
112 && let (Some(n), Some(d)) = (
113 NonZeroU32::new(discrete.numerator),
114 NonZeroU32::new(discrete.denominator),
115 )
116 {
117 intervals.push(Interval {
118 numerator: n,
119 denominator: d,
120 });
121 }
122 }
123 let format = MediaFormat::new(fourcc, res, color);
124 modes.push(Mode {
125 id: ModeId {
126 format,
127 interval: None,
128 },
129 format,
130 intervals,
131 interval_stepwise: None,
132 });
133 }
134 }
135 FrameSizeEnum::Stepwise(step) => {
136 if let Some(res) = Resolution::new(step.min_width, step.min_height) {
137 let format = MediaFormat::new(fourcc, res, color);
138 modes.push(Mode {
139 id: ModeId {
140 format,
141 interval: None,
142 },
143 format,
144 intervals: smallvec![],
145 interval_stepwise: None,
146 });
147 }
148 }
149 }
150 }
151 }
152
153 let controls = match catch_unwind(|| dev.query_controls()) {
157 Ok(Ok(ctrls)) => ctrls
158 .into_iter()
159 .filter_map(|ctrl| map_control(ctrl).ok())
160 .collect::<Vec<_>>(),
161 Ok(Err(_)) | Err(_) => Vec::new(),
164 };
165
166 let descriptor = CaptureDescriptor { modes, controls };
167 Ok(V4l2DeviceInfo {
168 path: path.display().to_string(),
169 name: node_name.clone(),
170 card: card.clone(),
171 driver: driver.clone(),
172 bus_info: bus_info.clone(),
173 properties: vec![
174 ("path".into(), path.display().to_string()),
175 ("name".into(), node_name.unwrap_or_default()),
176 ("driver".into(), driver),
177 ("card".into(), card),
178 ("bus".into(), bus_info),
179 ],
180 descriptor,
181 })
182}
183
184fn map_control(ctrl: v4l::control::Description) -> Result<ControlMeta, Box<dyn std::error::Error>> {
185 let id = ControlId(ctrl.id);
186 let name = ctrl.name;
187 use v4l::control::Type::*;
188 let (min, max, default) = match ctrl.typ {
189 Integer => (
190 ControlValue::Int(ctrl.minimum as i32),
191 ControlValue::Int(ctrl.maximum as i32),
192 ControlValue::Int(ctrl.default as i32),
193 ),
194 Boolean => (
195 ControlValue::Bool(ctrl.minimum != 0),
196 ControlValue::Bool(ctrl.maximum != 0),
197 ControlValue::Bool(ctrl.default != 0),
198 ),
199 Menu | IntegerMenu => (
200 ControlValue::Uint(ctrl.minimum as u32),
201 ControlValue::Uint(ctrl.maximum as u32),
202 ControlValue::Uint(ctrl.default as u32),
203 ),
204 Bitmask | Integer64 | CtrlClass | Button | String => {
205 return Err("unsupported control type".into());
206 }
207 _ => {
208 return Err("unsupported control type".into());
209 }
210 };
211
212 let access = if ctrl.flags.contains(v4l::control::Flags::READ_ONLY) {
213 Access::ReadOnly
214 } else {
215 Access::ReadWrite
216 };
217
218 let menu = ctrl.items.map(|items| {
219 items
220 .into_iter()
221 .map(|(_, item)| item.to_string())
222 .collect()
223 });
224
225 let step = match ctrl.typ {
226 Integer | Integer64 | Bitmask | IntegerMenu => Some(ControlValue::Uint(ctrl.step as u32)),
227 _ => None,
228 };
229
230 Ok(ControlMeta {
231 id,
232 name,
233 kind: match ctrl.typ {
234 Integer => ControlKind::Int,
235 Boolean => ControlKind::Bool,
236 Menu => ControlKind::Menu,
237 IntegerMenu => ControlKind::IntMenu,
238 Bitmask => ControlKind::Uint,
239 Integer64 => ControlKind::Int,
240 CtrlClass | Button | String => ControlKind::Unknown,
241 _ => ControlKind::Unknown,
242 },
243 access,
244 min,
245 max,
246 default,
247 step,
248 menu,
249 })
250}
251
252fn map_color_space(cs: Option<V4lColorspace>) -> ColorSpace {
253 match cs {
254 Some(V4lColorspace::SRGB) => ColorSpace::Srgb,
255 Some(V4lColorspace::Rec709) => ColorSpace::Bt709,
256 Some(V4lColorspace::Rec2020) => ColorSpace::Bt2020,
257 Some(V4lColorspace::SMPTE170M) => ColorSpace::Bt709,
258 _ => ColorSpace::Unknown,
259 }
260}
261
262fn guess_color_space(fcc: FourCc) -> ColorSpace {
263 match &fcc.to_u32().to_le_bytes() {
264 b"MJPG" | b"JPEG" | b"RG24" | b"RGB3" | b"RGB6" | b"BG24" | b"RGBA" | b"BGRA" | b"XB24"
265 | b"XR24" => ColorSpace::Srgb,
266 b"NV12" | b"NV21" | b"NV16" | b"NV61" | b"NV24" | b"NV42" | b"YUYV" | b"YVYU" | b"UYVY"
267 | b"VYUY" | b"I420" | b"YU12" | b"YV12" | b"YU16" | b"YV16" | b"YU24" | b"YV24" => {
268 ColorSpace::Bt709
269 }
270 _ => ColorSpace::Unknown,
271 }
272}
273
274pub mod prelude {
275 pub use crate::{V4l2DeviceInfo, probe_devices};
276 pub use styx_capture::prelude::*;
277}