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,
            ffmpeg_child: None,
            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()
}
