2026-04-23 07:00:06 -03:00
|
|
|
|
impl CameraCapture {
|
|
|
|
|
|
/// Fuzzy‑match devices under `/dev/v4l/by-id`, preferring capture nodes
|
|
|
|
|
|
#[cfg(not(coverage))]
|
|
|
|
|
|
fn find_device(substr: &str) -> Option<String> {
|
|
|
|
|
|
let wanted = substr.to_ascii_lowercase();
|
|
|
|
|
|
let mut matches: Vec<_> = std::fs::read_dir("/dev/v4l/by-id")
|
|
|
|
|
|
.ok()?
|
|
|
|
|
|
.flatten()
|
|
|
|
|
|
.filter_map(|e| {
|
|
|
|
|
|
let p = e.path();
|
|
|
|
|
|
let name = p.file_name()?.to_string_lossy().to_ascii_lowercase();
|
|
|
|
|
|
if name.contains(&wanted) {
|
|
|
|
|
|
Some(p)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
None
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
.collect();
|
|
|
|
|
|
|
|
|
|
|
|
// deterministic order
|
|
|
|
|
|
matches.sort();
|
|
|
|
|
|
|
|
|
|
|
|
for p in matches {
|
|
|
|
|
|
if let Ok(target) = std::fs::read_link(&p) {
|
|
|
|
|
|
let dev = format!("/dev/{}", target.file_name()?.to_string_lossy());
|
|
|
|
|
|
if Self::is_capture(&dev) {
|
|
|
|
|
|
return Some(dev);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
None
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[cfg(coverage)]
|
|
|
|
|
|
fn find_device(substr: &str) -> Option<String> {
|
|
|
|
|
|
let wanted = substr.to_ascii_lowercase();
|
|
|
|
|
|
let by_id_dir =
|
|
|
|
|
|
std::env::var("LESAVKA_CAM_BY_ID_DIR").unwrap_or_else(|_| "/dev/v4l/by-id".to_string());
|
|
|
|
|
|
let dev_root = std::env::var("LESAVKA_CAM_DEV_ROOT").unwrap_or_else(|_| "/dev".to_string());
|
|
|
|
|
|
let mut matches: Vec<_> = std::fs::read_dir(by_id_dir)
|
|
|
|
|
|
.ok()?
|
|
|
|
|
|
.flatten()
|
|
|
|
|
|
.filter_map(|e| {
|
|
|
|
|
|
let p = e.path();
|
|
|
|
|
|
let name = p.file_name()?.to_string_lossy().to_ascii_lowercase();
|
|
|
|
|
|
if name.contains(&wanted) {
|
|
|
|
|
|
Some(p)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
None
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
.collect();
|
|
|
|
|
|
matches.sort();
|
|
|
|
|
|
for p in matches {
|
|
|
|
|
|
if let Ok(target) = std::fs::read_link(&p) {
|
|
|
|
|
|
let dev = format!("{}/{}", dev_root, target.file_name()?.to_string_lossy());
|
|
|
|
|
|
if Self::is_capture(&dev) {
|
|
|
|
|
|
return Some(dev);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
None
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[cfg(not(coverage))]
|
|
|
|
|
|
fn is_capture(dev: &str) -> bool {
|
|
|
|
|
|
const V4L2_CAP_VIDEO_CAPTURE: u32 = 0x0000_0001;
|
|
|
|
|
|
const V4L2_CAP_VIDEO_CAPTURE_MPLANE: u32 = 0x0000_1000;
|
|
|
|
|
|
|
|
|
|
|
|
v4l::Device::with_path(dev)
|
|
|
|
|
|
.ok()
|
|
|
|
|
|
.and_then(|d| d.query_caps().ok())
|
|
|
|
|
|
.map(|caps| {
|
|
|
|
|
|
let bits = caps.capabilities.bits();
|
|
|
|
|
|
(bits & V4L2_CAP_VIDEO_CAPTURE != 0) || (bits & V4L2_CAP_VIDEO_CAPTURE_MPLANE != 0)
|
|
|
|
|
|
})
|
|
|
|
|
|
.unwrap_or(false)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#[cfg(coverage)]
|
|
|
|
|
|
fn is_capture(dev: &str) -> bool {
|
|
|
|
|
|
dev.starts_with("/dev/video")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// Cheap stub used when the web‑cam is disabled
|
|
|
|
|
|
pub fn new_stub() -> Self {
|
|
|
|
|
|
let pipeline = gst::Pipeline::new();
|
|
|
|
|
|
let sink: gst_app::AppSink = gst::ElementFactory::make("appsink")
|
|
|
|
|
|
.build()
|
|
|
|
|
|
.expect("appsink")
|
|
|
|
|
|
.downcast::<gst_app::AppSink>()
|
|
|
|
|
|
.unwrap();
|
|
|
|
|
|
Self {
|
|
|
|
|
|
pipeline,
|
|
|
|
|
|
sink,
|
|
|
|
|
|
preview_tap_running: None,
|
2026-04-26 13:22:52 -03:00
|
|
|
|
pts_rebaser: crate::live_capture_clock::DurationPacedSourcePtsRebaser::default(),
|
|
|
|
|
|
frame_duration_us: 1,
|
2026-04-23 07:00:06 -03:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
}
|