1#![doc = include_str!("../README.md")]
2use styx_capture::prelude::*;
3
4#[cfg(feature = "probe")]
5use libcamera::{
6 camera::Camera,
7 camera_manager::CameraManager,
8 color_space::{ColorSpace as LcColorSpace, Primaries as LcPrimaries, Range as LcRange},
9 control,
10 control_value::{ControlType, ControlValue as LcValue},
11 controls::ControlId,
12 properties::PropertyId,
13 stream::StreamRole,
14};
15#[cfg(feature = "probe")]
16use smallvec::smallvec;
17#[cfg(feature = "probe")]
18use std::cell::UnsafeCell;
19#[cfg(feature = "probe")]
20use std::sync::{Mutex, OnceLock};
21#[cfg(feature = "probe")]
22use std::time::{Duration, Instant};
23#[cfg(feature = "probe")]
24use styx_core::controls::{Access, ControlKind, ControlValue};
25
26#[derive(Clone)]
28pub struct LibcameraDeviceInfo {
29 pub id: String,
30 pub properties: Vec<(String, String)>,
31 pub descriptor: CaptureDescriptor,
32}
33
34#[cfg(feature = "probe")]
36pub fn probe_devices() -> Vec<LibcameraDeviceInfo> {
37 if let Some(cached) = read_probe_cache() {
38 return cached;
39 }
40
41 let mut devices = Vec::new();
42
43 let manager = match manager() {
44 Ok(mgr) => mgr,
45 Err(err) => {
46 if debug_enabled() {
47 eprintln!("libcamera manager init failed: {err}");
48 }
49 write_probe_cache(&devices);
50 return devices;
51 }
52 };
53
54 {
55 let cameras = manager.cameras();
56 if debug_enabled() {
57 let ids: Vec<String> = cameras.iter().map(|c| c.id().to_string()).collect();
58 eprintln!("libcamera probe: discovered {} camera(s): {:?}", ids.len(), ids);
59 }
60
61 for camera in cameras.iter() {
62 match build_info(&camera) {
63 Ok(info) => devices.push(info),
64 Err(err) => {
65 if debug_enabled() {
66 eprintln!(
67 "libcamera probe: failed to build descriptor for {}: {err}",
68 camera.id()
69 );
70 }
71 }
72 }
73 }
74 }
75 if stop_when_idle_enabled() {
76 let _ = try_stop_if_idle();
77 }
78 write_probe_cache(&devices);
79 devices
80}
81
82#[cfg(feature = "probe")]
83static MANAGER: OnceLock<SharedManager> = OnceLock::new();
84#[cfg(feature = "probe")]
85static INIT_GUARD: Mutex<()> = Mutex::new(());
86
87#[cfg(feature = "probe")]
88static PROBE_CACHE: OnceLock<Mutex<ProbeCache>> = OnceLock::new();
89
90#[cfg(feature = "probe")]
91#[derive(Default)]
92struct ProbeCache {
93 last_probe_at: Option<Instant>,
94 cached_devices: Vec<LibcameraDeviceInfo>,
95}
96
97#[cfg(feature = "probe")]
98fn probe_cache_ttl() -> Duration {
99 const DEFAULT_MS: u64 = 1_000;
100 let ms = std::env::var("STYX_LIBCAMERA_PROBE_CACHE_MS")
101 .ok()
102 .and_then(|v| v.parse::<u64>().ok())
103 .unwrap_or(DEFAULT_MS);
104 Duration::from_millis(ms.max(0))
105}
106
107#[cfg(feature = "probe")]
108fn debug_enabled() -> bool {
109 std::env::var_os("STYX_LIBCAMERA_DEBUG").is_some()
110}
111
112#[cfg(feature = "probe")]
113fn stop_when_idle_enabled() -> bool {
114 matches!(
115 std::env::var("STYX_LIBCAMERA_STOP_WHEN_IDLE")
116 .ok()
117 .as_deref()
118 .map(str::to_ascii_lowercase)
119 .as_deref(),
120 Some("1") | Some("true") | Some("yes") | Some("on")
121 )
122}
123
124#[cfg(feature = "probe")]
125fn read_probe_cache() -> Option<Vec<LibcameraDeviceInfo>> {
126 let cache = PROBE_CACHE.get_or_init(|| Mutex::new(ProbeCache::default()));
127 let ttl = probe_cache_ttl();
128 let guard = cache.lock().ok()?;
129 let Some(last) = guard.last_probe_at else {
130 return None;
131 };
132 if last.elapsed() <= ttl {
133 return Some(guard.cached_devices.clone());
134 }
135 None
136}
137
138#[cfg(feature = "probe")]
139fn write_probe_cache(devices: &[LibcameraDeviceInfo]) {
140 let cache = PROBE_CACHE.get_or_init(|| Mutex::new(ProbeCache::default()));
141 if let Ok(mut guard) = cache.lock() {
142 guard.last_probe_at = Some(Instant::now());
143 guard.cached_devices = devices.to_vec();
144 }
145}
146
147#[cfg(feature = "probe")]
148struct SharedManager {
149 manager: UnsafeCell<CameraManager>,
150 lock: Mutex<()>,
151}
152
153#[cfg(feature = "probe")]
154unsafe impl Send for SharedManager {}
155#[cfg(feature = "probe")]
156unsafe impl Sync for SharedManager {}
157
158#[cfg(feature = "probe")]
159pub fn manager() -> Result<&'static CameraManager, String> {
160 if let Some(mgr) = MANAGER.get() {
161 return Ok(unsafe { &*mgr.manager.get() });
162 }
163
164 let _guard = INIT_GUARD.lock().map_err(|e| e.to_string())?;
166 if let Some(mgr) = MANAGER.get() {
167 return Ok(unsafe { &*mgr.manager.get() });
168 }
169
170 let mgr = CameraManager::new().map_err(|e| e.to_string())?;
171 MANAGER
172 .set(SharedManager {
173 manager: UnsafeCell::new(mgr),
174 lock: Mutex::new(()),
175 })
176 .map_err(|_| "failed to set libcamera manager".to_string())?;
177 MANAGER
178 .get()
179 .map(|m| unsafe { &*m.manager.get() })
180 .ok_or_else(|| "failed to init libcamera manager".to_string())
181}
182
183#[cfg(feature = "probe")]
188pub fn with_manager_mut<R>(f: impl FnOnce(&mut CameraManager) -> R) -> Result<R, String> {
189 let shared = MANAGER.get().or_else(|| {
190 let _ = manager();
191 MANAGER.get()
192 }).ok_or_else(|| "failed to init libcamera manager".to_string())?;
193 let _guard = shared.lock.lock().map_err(|e| e.to_string())?;
194 let mgr = unsafe { &mut *shared.manager.get() };
195 Ok(f(mgr))
196}
197
198#[cfg(feature = "probe")]
202pub fn try_stop_if_idle() -> Result<(), String> {
203 let enabled = std::env::var("STYX_LIBCAMERA_STOP_IF_IDLE")
208 .ok()
209 .map(|v| v == "1" || v.eq_ignore_ascii_case("true"))
210 .unwrap_or(false);
211 if !enabled {
212 return Ok(());
213 }
214
215 with_manager_mut(|mgr| {
216 let _ = mgr.try_stop();
217 })
218}
219
220#[cfg(feature = "probe")]
221fn build_info(camera: &Camera) -> Result<LibcameraDeviceInfo, Box<dyn std::error::Error>> {
222 let mut modes = Vec::new();
223 let mut seen = std::collections::HashSet::<(FourCc, u32, u32)>::new();
224 let is_pisp = is_rpi_pisp_sensor_i2c(camera.id());
225
226 for role in [
230 StreamRole::ViewFinder,
231 StreamRole::VideoRecording,
232 StreamRole::StillCapture,
233 StreamRole::Raw,
234 ] {
235 if let Some(cfg) = camera.generate_configuration(&[role])
236 && let Some(view_cfg) = cfg.get(0)
237 {
238 let color = map_color_space(view_cfg.get_color_space());
239 let formats = view_cfg.formats();
240 for pf in formats.pixel_formats().into_iter() {
241 let fourcc = map_pixel_format_to_fourcc(pf);
242 if is_pisp && pisp_disallowed_fourcc(fourcc) {
243 continue;
244 }
245 for size in formats.sizes(pf) {
246 let Some(res) = Resolution::new(size.width, size.height) else {
247 continue;
248 };
249 if !seen.insert((fourcc, size.width, size.height)) {
250 continue;
251 }
252 let format = MediaFormat::new(fourcc, res, color);
253 modes.push(Mode {
254 id: ModeId {
255 format: format.clone(),
256 interval: None,
257 },
258 format,
259 intervals: smallvec![],
260 interval_stepwise: None,
261 });
262 }
263 }
264 }
265 }
266 let controls = map_controls(camera.controls());
267 let mut properties = map_properties(camera.properties());
268 properties.push(("id".into(), camera.id().to_string()));
269 let descriptor = CaptureDescriptor { modes, controls };
270 Ok(LibcameraDeviceInfo {
271 id: camera.id().to_string(),
272 properties,
273 descriptor,
274 })
275}
276
277#[cfg(feature = "probe")]
278fn is_rpi_pisp_sensor_i2c(id: &str) -> bool {
279 id.starts_with("/base/") && id.contains("/i2c@")
280}
281
282#[cfg(feature = "probe")]
283fn pisp_disallowed_fourcc(code: FourCc) -> bool {
284 matches!(
286 &code.to_u32().to_le_bytes(),
287 b"YV12" | b"XB24" | b"XR24" | b"YU16" | b"YV16" | b"YU24" | b"YV24" | b"YVYU" | b"VYUY"
288 )
289}
290
291#[cfg(feature = "probe")]
292fn map_pixel_format_to_fourcc(pf: libcamera::pixel_format::PixelFormat) -> FourCc {
293 let base = FourCc::from(pf.fourcc());
294 match base.to_u32().to_le_bytes() {
295 bytes if bytes == *b"RGB3" => return FourCc::new(*b"RG24"),
298 bytes if bytes == *b"BGR3" => return FourCc::new(*b"BG24"),
299 bytes if bytes == *b"RGB0" => return FourCc::new(*b"XR24"),
300 bytes if bytes == *b"BGR0" => return FourCc::new(*b"XB24"),
301 _ => {}
302 }
303 let Some(info) = pf.info() else {
304 return base;
305 };
306 if !info.packed || info.colour_encoding != libcamera::pixel_format::ColourEncoding::Raw {
307 return base;
308 }
309
310 const RG10: [u8; 4] = *b"RG10";
311 const BG10: [u8; 4] = *b"BG10";
312 const GB10: [u8; 4] = *b"GB10";
313 const BA10: [u8; 4] = *b"BA10";
314 const RG12: [u8; 4] = *b"RG12";
315 const BG12: [u8; 4] = *b"BG12";
316 const GB12: [u8; 4] = *b"GB12";
317 const BA12: [u8; 4] = *b"BA12";
318
319 match (base.to_u32().to_le_bytes(), info.bits_per_pixel) {
320 (RG10, 10) => FourCc::new(*b"pRAA"),
322 (BG10, 10) => FourCc::new(*b"pBAA"),
323 (GB10, 10) => FourCc::new(*b"pGAA"),
324 (BA10, 10) => FourCc::new(*b"pgAA"),
325
326 (RG12, 12) => FourCc::new(*b"pRCC"),
328 (BG12, 12) => FourCc::new(*b"pBCC"),
329 (GB12, 12) => FourCc::new(*b"pGCC"),
330 (BA12, 12) => FourCc::new(*b"pgCC"),
331
332 _ => base,
333 }
334}
335
336#[cfg(feature = "probe")]
337fn map_controls(map: &control::ControlInfoMap) -> Vec<ControlMeta> {
338 fn kind_from_type(control_type: ControlType) -> ControlKind {
339 match control_type {
340 ControlType::Bool => ControlKind::Bool,
341 ControlType::Byte | ControlType::Uint16 | ControlType::Uint32 => ControlKind::Uint,
342 ControlType::Int32 | ControlType::Int64 => ControlKind::Int,
343 ControlType::Float => ControlKind::Float,
344 ControlType::None
345 | ControlType::String
346 | ControlType::Rectangle
347 | ControlType::Size
348 | ControlType::Point => ControlKind::Unknown,
349 }
350 }
351
352 fn as_nonneg_i64(v: &ControlValue) -> Option<i64> {
353 match v {
354 ControlValue::Uint(n) => Some(*n as i64),
355 ControlValue::Int(n) if *n >= 0 => Some(*n as i64),
356 _ => None,
357 }
358 }
359
360 let mut out = Vec::new();
361 for (id, info) in map.into_iter() {
362 let name = ControlId::from_id(id)
365 .map(|cid| cid.name().to_string())
366 .or_else(|| ControlId::try_from(id).ok().map(|cid| cid.name().to_string()))
367 .unwrap_or_else(|| format!("ctrl_{id}"));
368 let min = convert_value(&info.min());
369 let max = convert_value(&info.max());
370 let default = convert_value(&info.def());
371 let control_type = ControlType::from(&info.def());
372 let mut kind = kind_from_type(control_type);
373
374 let mut menu: Option<Vec<String>> = None;
378 let values = info.values();
379 if !values.is_empty() {
380 let mut allowed = values
381 .iter()
382 .map(convert_value)
383 .filter_map(|v| as_nonneg_i64(&v))
384 .collect::<Vec<_>>();
385 allowed.sort_unstable();
386 allowed.dedup();
387 let contiguous = allowed.first().is_some_and(|first| *first == 0)
388 && allowed.iter().enumerate().all(|(idx, v)| *v == idx as i64);
389
390 if contiguous {
391 let enumerators = ControlId::from_id(id)
392 .map(|cid| cid.enumerators_map())
393 .unwrap_or_default();
394 menu = Some(
395 allowed
396 .iter()
397 .map(|v| enumerators.get(&(*v as i32)).cloned().unwrap_or_default())
398 .collect(),
399 );
400 kind = match kind {
401 ControlKind::Int => ControlKind::IntMenu,
402 ControlKind::Uint => ControlKind::Menu,
403 other => other,
404 };
405 }
406 }
407
408 let (name, menu) = match (id, name.as_str(), menu.as_ref()) {
411 (10002, "ctrl_10002", Some(existing)) if existing.iter().all(|s| s.is_empty()) => (
413 "NoiseReductionMode".to_string(),
414 Some(vec![
415 "NoiseReductionModeOff".into(),
416 "NoiseReductionModeFast".into(),
417 "NoiseReductionModeHighQuality".into(),
418 "NoiseReductionModeMinimal".into(),
419 "NoiseReductionModeZSL".into(),
420 ]),
421 ),
422 (10002, "ctrl_10002", None) => (
423 "NoiseReductionMode".to_string(),
424 Some(vec![
425 "NoiseReductionModeOff".into(),
426 "NoiseReductionModeFast".into(),
427 "NoiseReductionModeHighQuality".into(),
428 "NoiseReductionModeMinimal".into(),
429 "NoiseReductionModeZSL".into(),
430 ]),
431 ),
432 _ => (name, menu),
433 };
434
435 if matches!(kind, ControlKind::Unknown) {
437 continue;
438 }
439 out.push(ControlMeta {
440 id: ControlId(id),
441 name,
442 kind,
443 access: Access::ReadWrite,
444 min,
445 max,
446 default,
447 step: None,
448 menu,
449 });
450 }
451 out
452}
453
454#[cfg(feature = "probe")]
455fn map_properties(props: &control::PropertyList) -> Vec<(String, String)> {
456 let mut out = Vec::new();
457 for (id, val) in props.into_iter() {
458 let name = PropertyId::try_from(id)
459 .map(|pid| pid.name().to_string())
460 .unwrap_or_else(|_| format!("prop_{id}"));
461 out.push((name, format_property_value(&val)));
462 }
463 out
464}
465
466#[cfg(feature = "probe")]
467fn format_property_value(val: &LcValue) -> String {
468 match val {
469 LcValue::None => String::new(),
470 LcValue::Bool(v) => v.first().map(|n| n.to_string()).unwrap_or_default(),
471 LcValue::Byte(v) => v.first().map(|n| n.to_string()).unwrap_or_default(),
472 LcValue::Uint16(v) => v.first().map(|n| n.to_string()).unwrap_or_default(),
473 LcValue::Uint32(v) => v.first().map(|n| n.to_string()).unwrap_or_default(),
474 LcValue::Int32(v) => v.first().map(|n| n.to_string()).unwrap_or_default(),
475 LcValue::Int64(v) => v.first().map(|n| n.to_string()).unwrap_or_default(),
476 LcValue::Float(v) => v.first().map(|n| n.to_string()).unwrap_or_default(),
477 LcValue::String(v) => v.first().cloned().unwrap_or_default(),
478 other => format!("{other:?}"),
479 }
480}
481
482#[cfg(feature = "probe")]
483fn map_color_space(cs: Option<LcColorSpace>) -> ColorSpace {
484 let Some(cs) = cs else {
485 return ColorSpace::Unknown;
486 };
487 let primaries = cs.primaries;
488 let transfer = cs.transfer_function;
489 let range = cs.range;
490 let full = matches!(range, LcRange::Full);
491 match (primaries, transfer) {
492 (LcPrimaries::Rec2020, _) => {
493 if full {
494 ColorSpace::Srgb
495 } else {
496 ColorSpace::Bt2020
497 }
498 }
499 (LcPrimaries::Rec709 | LcPrimaries::Smpte170m, _)
500 | (_, libcamera::color_space::TransferFunction::Srgb) => {
501 if full {
502 ColorSpace::Srgb
503 } else {
504 ColorSpace::Bt709
505 }
506 }
507 _ => {
508 if full {
509 ColorSpace::Srgb
510 } else {
511 ColorSpace::Unknown
512 }
513 }
514 }
515}
516
517#[cfg(feature = "probe")]
518fn convert_value(val: &LcValue) -> ControlValue {
519 match val {
520 LcValue::None => ControlValue::None,
521 LcValue::Bool(v) => v
522 .first()
523 .copied()
524 .map(ControlValue::Bool)
525 .unwrap_or(ControlValue::None),
526 LcValue::Byte(v) => v
527 .first()
528 .copied()
529 .map(|b| ControlValue::Uint(b as u32))
530 .unwrap_or(ControlValue::None),
531 LcValue::Uint16(v) => v
532 .first()
533 .copied()
534 .map(|b| ControlValue::Uint(b as u32))
535 .unwrap_or(ControlValue::None),
536 LcValue::Uint32(v) => v
537 .first()
538 .copied()
539 .map(ControlValue::Uint)
540 .unwrap_or(ControlValue::None),
541 LcValue::Int32(v) => v
542 .first()
543 .copied()
544 .map(ControlValue::Int)
545 .unwrap_or(ControlValue::None),
546 LcValue::Int64(v) => v
547 .first()
548 .copied()
549 .map(|i| ControlValue::Int(i.clamp(i32::MIN as i64, i32::MAX as i64) as i32))
550 .unwrap_or(ControlValue::None),
551 LcValue::Float(v) => v
552 .first()
553 .copied()
554 .map(ControlValue::Float)
555 .unwrap_or(ControlValue::None),
556 _ => ControlValue::None,
557 }
558}
559
560pub struct LibcameraCapture {
562 descriptor: CaptureDescriptor,
563}
564
565impl LibcameraCapture {
566 pub fn new(descriptor: CaptureDescriptor) -> Self {
568 Self { descriptor }
569 }
570}
571
572impl CaptureSource for LibcameraCapture {
573 fn descriptor(&self) -> &CaptureDescriptor {
574 &self.descriptor
575 }
576
577 fn next_frame(&self) -> Option<FrameLease> {
578 None
580 }
581}
582
583pub mod prelude {
584 #[cfg(feature = "probe")]
585 pub use crate::probe_devices;
586 pub use crate::{LibcameraCapture, LibcameraDeviceInfo};
587 pub use styx_capture::prelude::*;
588}