111 lines
3.5 KiB
Rust
111 lines
3.5 KiB
Rust
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 = normalize_device_fragment(substr);
|
||
let mut matches: Vec<_> = std::fs::read_dir("/dev/v4l/by-id")
|
||
.ok()?
|
||
.flatten()
|
||
.filter_map(|e| {
|
||
let p = e.path();
|
||
let name = normalize_device_fragment(&p.file_name()?.to_string_lossy());
|
||
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 = normalize_device_fragment(substr);
|
||
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 = normalize_device_fragment(&p.file_name()?.to_string_lossy());
|
||
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,
|
||
pts_rebaser: crate::live_capture_clock::DurationPacedSourcePtsRebaser::default(),
|
||
frame_duration_us: 1,
|
||
}
|
||
}
|
||
|
||
}
|
||
|
||
fn normalize_device_fragment(value: &str) -> String {
|
||
value
|
||
.chars()
|
||
.filter(|ch| ch.is_ascii_alphanumeric())
|
||
.flat_map(char::to_lowercase)
|
||
.collect()
|
||
}
|