1#![doc = include_str!("../README.md")]
2
3#[cfg(any(feature = "v4l2", feature = "libcamera"))]
4use std::collections::HashSet;
5#[cfg(feature = "v4l2")]
6use std::panic::{AssertUnwindSafe, catch_unwind};
7#[cfg(feature = "file-backend")]
8use std::path::PathBuf;
9
10pub use styx_capture as capture;
11pub use styx_codec as codec;
12pub use styx_core as core;
13#[cfg(feature = "libcamera")]
14pub use styx_libcamera as libcamera;
15#[cfg(feature = "v4l2")]
16pub use styx_v4l2 as v4l2;
17#[cfg(feature = "preview-window")]
18pub mod preview;
19
20pub use thiserror;
21
22pub mod capture_api;
23mod metrics;
24pub mod session;
25
26#[derive(Debug, Clone)]
37#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
38#[cfg_attr(feature = "schema", derive(utoipa::ToSchema))]
39pub struct ProbedDevice {
40 pub identity: DeviceIdentity,
41 pub backends: Vec<ProbedBackend>,
42}
43
44#[derive(Debug, Clone)]
56#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
57#[cfg_attr(feature = "schema", derive(utoipa::ToSchema))]
58pub struct ProbedBackend {
59 pub kind: BackendKind,
60 pub handle: BackendHandle,
61 pub descriptor: styx_capture::CaptureDescriptor,
62 pub properties: Vec<(String, String)>,
63}
64
65#[derive(Debug, Clone, Copy, PartialEq, Eq)]
70#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
71#[cfg_attr(feature = "schema", derive(utoipa::ToSchema))]
72pub enum BackendKind {
73 V4l2,
74 Libcamera,
75 Virtual,
76 Netcam,
77 File,
78}
79
80#[derive(Debug, Clone)]
91#[cfg_attr(feature = "schema", derive(utoipa::ToSchema))]
92pub enum BackendHandle {
93 #[cfg(feature = "v4l2")]
94 V4l2 {
95 path: String,
96 },
97 #[cfg(feature = "libcamera")]
98 Libcamera {
99 id: String,
100 },
101 Virtual,
102 #[cfg(feature = "netcam")]
103 Netcam {
104 url: String,
105 width: u32,
106 height: u32,
107 fps: u32,
108 },
109 #[cfg(feature = "file-backend")]
110 File {
111 #[cfg_attr(feature = "schema", schema(value_type = Vec<String>))]
112 paths: Vec<PathBuf>,
113 fps: u32,
114 loop_forever: bool,
115 },
116}
117
118impl BackendHandle {
119 pub fn kind(&self) -> BackendKind {
121 match self {
122 #[cfg(feature = "v4l2")]
123 BackendHandle::V4l2 { .. } => BackendKind::V4l2,
124 #[cfg(feature = "libcamera")]
125 BackendHandle::Libcamera { .. } => BackendKind::Libcamera,
126 BackendHandle::Virtual => BackendKind::Virtual,
127 #[cfg(feature = "netcam")]
128 BackendHandle::Netcam { .. } => BackendKind::Netcam,
129 #[cfg(feature = "file-backend")]
130 BackendHandle::File { .. } => BackendKind::File,
131 }
132 }
133}
134
135#[cfg(feature = "serde")]
136impl serde::Serialize for BackendHandle {
137 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
138 where
139 S: serde::Serializer,
140 {
141 if serializer.is_human_readable() {
142 #[derive(serde::Serialize)]
143 #[serde(tag = "type", rename_all = "snake_case")]
144 enum HumanHandle<'a> {
145 #[cfg(feature = "v4l2")]
146 V4l2 {
147 path: &'a str,
148 },
149 #[cfg(feature = "libcamera")]
150 Libcamera {
151 id: &'a str,
152 },
153 Virtual,
154 #[cfg(feature = "netcam")]
155 Netcam {
156 url: &'a str,
157 width: u32,
158 height: u32,
159 fps: u32,
160 },
161 #[cfg(feature = "file-backend")]
162 File {
163 paths: Vec<String>,
164 fps: u32,
165 loop_forever: bool,
166 },
167 }
168
169 let human = match self {
170 #[cfg(feature = "v4l2")]
171 BackendHandle::V4l2 { path } => HumanHandle::V4l2 { path },
172 #[cfg(feature = "libcamera")]
173 BackendHandle::Libcamera { id } => HumanHandle::Libcamera { id },
174 BackendHandle::Virtual => HumanHandle::Virtual,
175 #[cfg(feature = "netcam")]
176 BackendHandle::Netcam {
177 url,
178 width,
179 height,
180 fps,
181 } => HumanHandle::Netcam {
182 url,
183 width: *width,
184 height: *height,
185 fps: *fps,
186 },
187 #[cfg(feature = "file-backend")]
188 BackendHandle::File {
189 paths,
190 fps,
191 loop_forever,
192 } => HumanHandle::File {
193 paths: paths
194 .iter()
195 .map(|p| p.to_string_lossy().to_string())
196 .collect(),
197 fps: *fps,
198 loop_forever: *loop_forever,
199 },
200 };
201 human.serialize(serializer)
202 } else {
203 #[derive(serde::Serialize)]
204 enum BinaryHandle<'a> {
205 #[cfg(feature = "v4l2")]
206 V4l2(&'a str),
207 #[cfg(feature = "libcamera")]
208 Libcamera(&'a str),
209 Virtual,
210 #[cfg(feature = "netcam")]
211 Netcam {
212 url: &'a str,
213 width: u32,
214 height: u32,
215 fps: u32,
216 },
217 #[cfg(feature = "file-backend")]
218 File {
219 paths: Vec<String>,
220 fps: u32,
221 loop_forever: bool,
222 },
223 }
224 let bin = match self {
225 #[cfg(feature = "v4l2")]
226 BackendHandle::V4l2 { path } => BinaryHandle::V4l2(path),
227 #[cfg(feature = "libcamera")]
228 BackendHandle::Libcamera { id } => BinaryHandle::Libcamera(id),
229 BackendHandle::Virtual => BinaryHandle::Virtual,
230 #[cfg(feature = "netcam")]
231 BackendHandle::Netcam {
232 url,
233 width,
234 height,
235 fps,
236 } => BinaryHandle::Netcam {
237 url,
238 width: *width,
239 height: *height,
240 fps: *fps,
241 },
242 #[cfg(feature = "file-backend")]
243 BackendHandle::File {
244 paths,
245 fps,
246 loop_forever,
247 } => BinaryHandle::File {
248 paths: paths
249 .iter()
250 .map(|p| p.to_string_lossy().to_string())
251 .collect(),
252 fps: *fps,
253 loop_forever: *loop_forever,
254 },
255 };
256 bin.serialize(serializer)
257 }
258 }
259}
260
261#[cfg(feature = "serde")]
262impl<'de> serde::Deserialize<'de> for BackendHandle {
263 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
264 where
265 D: serde::Deserializer<'de>,
266 {
267 if deserializer.is_human_readable() {
268 #[derive(serde::Deserialize)]
269 #[serde(tag = "type", rename_all = "snake_case")]
270 enum HumanHandle {
271 #[cfg(feature = "v4l2")]
272 V4l2 {
273 path: String,
274 },
275 #[cfg(feature = "libcamera")]
276 Libcamera {
277 id: String,
278 },
279 Virtual,
280 #[cfg(feature = "netcam")]
281 Netcam {
282 url: String,
283 width: u32,
284 height: u32,
285 fps: u32,
286 },
287 #[cfg(feature = "file-backend")]
288 File {
289 paths: Vec<String>,
290 fps: u32,
291 loop_forever: bool,
292 },
293 }
294 let human = HumanHandle::deserialize(deserializer)?;
295 let handle = match human {
296 #[cfg(feature = "v4l2")]
297 HumanHandle::V4l2 { path } => BackendHandle::V4l2 { path },
298 #[cfg(feature = "libcamera")]
299 HumanHandle::Libcamera { id } => BackendHandle::Libcamera { id },
300 HumanHandle::Virtual => BackendHandle::Virtual,
301 #[cfg(feature = "netcam")]
302 HumanHandle::Netcam {
303 url,
304 width,
305 height,
306 fps,
307 } => BackendHandle::Netcam {
308 url,
309 width,
310 height,
311 fps,
312 },
313 #[cfg(feature = "file-backend")]
314 HumanHandle::File {
315 paths,
316 fps,
317 loop_forever,
318 } => BackendHandle::File {
319 paths: paths.into_iter().map(PathBuf::from).collect(),
320 fps,
321 loop_forever,
322 },
323 };
324 Ok(handle)
325 } else {
326 #[derive(serde::Deserialize)]
327 enum BinaryHandle {
328 #[cfg(feature = "v4l2")]
329 V4l2(String),
330 #[cfg(feature = "libcamera")]
331 Libcamera(String),
332 Virtual,
333 #[cfg(feature = "netcam")]
334 Netcam {
335 url: String,
336 width: u32,
337 height: u32,
338 fps: u32,
339 },
340 #[cfg(feature = "file-backend")]
341 File {
342 paths: Vec<String>,
343 fps: u32,
344 loop_forever: bool,
345 },
346 }
347 let bin = BinaryHandle::deserialize(deserializer)?;
348 let handle = match bin {
349 #[cfg(feature = "v4l2")]
350 BinaryHandle::V4l2(path) => BackendHandle::V4l2 { path },
351 #[cfg(feature = "libcamera")]
352 BinaryHandle::Libcamera(id) => BackendHandle::Libcamera { id },
353 BinaryHandle::Virtual => BackendHandle::Virtual,
354 #[cfg(feature = "netcam")]
355 BinaryHandle::Netcam {
356 url,
357 width,
358 height,
359 fps,
360 } => BackendHandle::Netcam {
361 url,
362 width,
363 height,
364 fps,
365 },
366 #[cfg(feature = "file-backend")]
367 BinaryHandle::File {
368 paths,
369 fps,
370 loop_forever,
371 } => BackendHandle::File {
372 paths: paths.into_iter().map(PathBuf::from).collect(),
373 fps,
374 loop_forever,
375 },
376 };
377 Ok(handle)
378 }
379 }
380}
381
382#[derive(Debug, Clone)]
387#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
388#[cfg_attr(feature = "schema", derive(utoipa::ToSchema))]
389pub struct DeviceIdentity {
390 pub display: String,
392 pub keys: Vec<String>,
394}
395
396#[derive(Debug, Clone)]
408#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
409pub struct ProbeResult {
410 pub devices: Vec<ProbedDevice>,
411 pub errors: Vec<String>,
412}
413
414pub fn probe_all() -> Vec<ProbedDevice> {
426 probe_all_with_errors().devices
427}
428
429pub fn probe_all_with_errors() -> ProbeResult {
433 #[allow(unused_mut)]
434 let mut devices: Vec<ProbedDevice> = Vec::new();
435 #[allow(unused_mut)]
436 let mut errors: Vec<String> = Vec::new();
437
438 #[cfg(feature = "v4l2")]
439 {
440 let (v4l2_devices, v4l2_errors) =
441 match catch_unwind(AssertUnwindSafe(|| styx_v4l2::probe_devices())) {
442 Ok(res) => res,
443 Err(_) => (Vec::new(), vec!["v4l2 probe panicked".to_string()]),
444 };
445 errors.extend(v4l2_errors);
446 for dev in v4l2_devices {
447 let backend = ProbedBackend {
448 kind: BackendKind::V4l2,
449 handle: BackendHandle::V4l2 {
450 path: dev.path.clone(),
451 },
452 descriptor: dev.descriptor,
453 properties: dev.properties,
454 };
455 merge_backend(&mut devices, dev.path.clone(), backend);
456 }
457 }
458 #[cfg(feature = "libcamera")]
459 {
460 for dev in styx_libcamera::probe_devices() {
461 let backend = ProbedBackend {
462 kind: BackendKind::Libcamera,
463 handle: BackendHandle::Libcamera { id: dev.id.clone() },
464 descriptor: dev.descriptor,
465 properties: dev.properties,
466 };
467 merge_backend(&mut devices, dev.id.clone(), backend);
468 }
469 }
470 ProbeResult { devices, errors }
471}
472
473#[cfg(any(feature = "v4l2", feature = "libcamera"))]
474fn merge_backend(devices: &mut Vec<ProbedDevice>, id: String, backend: ProbedBackend) {
475 let new_keys: HashSet<String> = derive_keys(&id, &backend.properties).into_iter().collect();
476 let new_keys_vec: Vec<String> = new_keys.iter().cloned().collect();
477 if let Some(existing) = devices
478 .iter_mut()
479 .find(|d| d.identity.keys.iter().any(|k| new_keys.contains(k)))
480 {
481 existing.backends.push(backend);
482 for k in new_keys {
483 if existing.identity.keys.iter().any(|ek| ek == &k) {
484 continue;
485 }
486 existing.identity.keys.push(k);
487 }
488 } else {
489 devices.push(ProbedDevice {
490 identity: DeviceIdentity {
491 display: pick_id(&id, &backend.properties),
492 keys: new_keys_vec,
493 },
494 backends: vec![backend],
495 });
496 }
497}
498
499#[cfg(any(feature = "v4l2", feature = "libcamera"))]
500fn derive_keys(id: &str, props: &[(String, String)]) -> Vec<String> {
501 let mut keys = Vec::new();
502 if !id.starts_with("/dev/video") {
503 keys.push(id.to_string());
504 }
505 for (k, v) in props {
506 let v_trimmed = v.trim();
507 let v_lower = v_trimmed.to_ascii_lowercase();
508 if v_lower == "rp1-cfe" {
509 continue;
510 }
511 if k.eq_ignore_ascii_case("bus")
513 || k.eq_ignore_ascii_case("card")
514 || k.eq_ignore_ascii_case("driver")
515 || k.eq_ignore_ascii_case("model")
516 {
517 keys.push(v_trimmed.to_string());
518 }
519 if let Some(vidpid) = extract_vid_pid(v_trimmed) {
520 keys.push(vidpid);
521 }
522 }
523 if let Some(vidpid) = extract_vid_pid(id) {
524 keys.push(vidpid);
525 }
526 keys
527}
528
529#[cfg(any(feature = "v4l2", feature = "libcamera"))]
530fn pick_id(id: &str, props: &[(String, String)]) -> String {
531 if let Some(model) = props
532 .iter()
533 .find(|(k, _)| k.eq_ignore_ascii_case("model"))
534 .map(|(_, v)| v.trim())
535 && !model.is_empty()
536 && !model.eq_ignore_ascii_case("rp1-cfe")
537 {
538 return model.to_string();
539 }
540 if let Some(vidpid) = props.iter().find_map(|(_, v)| extract_vid_pid(v)) {
541 return vidpid;
542 }
543 if let Some(bus) = props
544 .iter()
545 .find(|(k, _)| k.eq_ignore_ascii_case("bus"))
546 .map(|(_, v)| v.clone())
547 {
548 return bus;
549 }
550 if let Some(card) = props
551 .iter()
552 .find(|(k, _)| k.eq_ignore_ascii_case("card"))
553 .map(|(_, v)| v.clone())
554 {
555 return card;
556 }
557 id.to_string()
558}
559
560#[cfg(any(feature = "v4l2", feature = "libcamera"))]
561fn extract_vid_pid(s: &str) -> Option<String> {
562 let bytes = s.as_bytes();
563 for i in 0..bytes.len().saturating_sub(8) {
564 let slice = &bytes[i..i + 9];
565 if slice[4] != b':' {
566 continue;
567 }
568 if slice[..4].iter().all(|b| b.is_ascii_hexdigit())
569 && slice[5..].iter().all(|b| b.is_ascii_hexdigit())
570 {
571 return Some(String::from_utf8_lossy(slice).to_string());
572 }
573 }
574 None
575}
576
577pub mod prelude {
578 #[cfg(feature = "file-backend")]
579 pub use crate::capture_api::make_file_device;
580 #[cfg(feature = "netcam")]
581 pub use crate::capture_api::make_netcam_device;
582 pub use crate::capture_api::{
583 CaptureError, CaptureHandle, CaptureRequest, CaptureTunables, StyxConfig,
584 set_capture_tunables, start_capture,
585 };
586 pub use crate::metrics::{PipelineMetrics, StageMetrics};
587 #[cfg(feature = "preview-window")]
588 pub use crate::preview::PreviewWindow;
589 pub use crate::probe_all;
590 pub use crate::session::{MediaPipeline, MediaPipelineBuilder};
591 pub use crate::{BackendHandle, BackendKind, ProbedBackend, ProbedDevice};
592 pub use styx_capture::prelude::*;
593 pub use styx_codec::prelude::*;
594 #[allow(unused_imports)]
595 pub use styx_core::prelude::*;
596 #[cfg(feature = "libcamera")]
597 pub use styx_libcamera::prelude::{
598 LibcameraCapture, LibcameraDeviceInfo, probe_devices as probe_libcamera,
599 };
600 #[cfg(feature = "v4l2")]
601 pub use styx_v4l2::prelude::{V4l2DeviceInfo, probe_devices as probe_v4l2};
602}