use anyhow::{Context, Result}; #[cfg(coverage)] use std::env; #[cfg(coverage)] use std::fs::OpenOptions; #[cfg(coverage)] use std::os::unix::fs::{FileTypeExt, OpenOptionsExt}; #[cfg(coverage)] const STREAM_CTRL_SIZE_11: usize = 26; #[cfg(coverage)] const STREAM_CTRL_SIZE_15: usize = 34; #[cfg(coverage)] const STREAM_CTRL_SIZE_MAX: usize = STREAM_CTRL_SIZE_15; #[cfg(coverage)] const UVC_DATA_SIZE: usize = 60; #[cfg(coverage)] const UVC_STRING_CONTROL_IDX: u8 = 0; #[cfg(coverage)] const UVC_STRING_STREAMING_IDX: u8 = 1; #[cfg(coverage)] const USB_DIR_IN: u8 = 0x80; #[cfg(coverage)] const UVC_SET_CUR: u8 = 0x01; #[cfg(coverage)] const UVC_GET_CUR: u8 = 0x81; #[cfg(coverage)] const UVC_GET_MIN: u8 = 0x82; #[cfg(coverage)] const UVC_GET_MAX: u8 = 0x83; #[cfg(coverage)] const UVC_GET_RES: u8 = 0x84; #[cfg(coverage)] const UVC_GET_LEN: u8 = 0x85; #[cfg(coverage)] const UVC_GET_INFO: u8 = 0x86; #[cfg(coverage)] const UVC_GET_DEF: u8 = 0x87; #[cfg(coverage)] const UVC_VS_PROBE_CONTROL: u8 = 0x01; #[cfg(coverage)] const UVC_VS_COMMIT_CONTROL: u8 = 0x02; #[cfg(coverage)] const UVC_VC_REQUEST_ERROR_CODE_CONTROL: u8 = 0x02; #[cfg(coverage)] const DEFAULT_UVC_BUFFER_COUNT: u32 = 2; #[cfg(coverage)] const DEFAULT_UVC_IDLE_PUMP_MS: u64 = 2; #[cfg(coverage)] const DEFAULT_UVC_FRAME_MAX_AGE_MS: u64 = 1_000; #[cfg(coverage)] const DEFAULT_UVC_MJPEG_BUDGET_BYTES_PER_SEC: u32 = 10_000_000; #[cfg(coverage)] const DEFAULT_UVC_STATS_INTERVAL_MS: u64 = 5_000; #[cfg(coverage)] const DEFAULT_UVC_STATS_PATH: &str = "/run/lesavka-uvc-video-stats.json"; #[cfg(coverage)] const MAX_MJPEG_FRAME_BYTES: usize = 8 * 1024 * 1024; #[cfg(coverage)] const MINIMAL_MJPEG_FRAME: &[u8] = &[0xff, 0xd8, 0xff, 0xd9]; #[cfg(coverage)] const IDLE_MJPEG_FRAME: &[u8] = include_bytes!("idle_1280x720_black.jpg"); #[cfg(coverage)] #[repr(C)] #[derive(Clone, Copy)] struct V4l2Event { _bytes: [u8; 64], } #[cfg(coverage)] #[repr(C)] #[derive(Clone, Copy)] struct UsbCtrlRequest { b_request_type: u8, b_request: u8, w_value: u16, w_index: u16, w_length: u16, } #[cfg(coverage)] #[repr(C)] #[derive(Clone, Copy)] struct UvcRequestData { length: i32, data: [u8; UVC_DATA_SIZE], } #[cfg(coverage)] #[derive(Clone, Copy)] struct UvcConfig { width: u32, height: u32, fps: u32, interval: u32, max_packet: u32, frame_size: u32, } #[cfg(coverage)] struct PayloadCap { limit: u32, pct: u32, source: &'static str, periodic_dw: Option, non_periodic_dw: Option, } #[cfg(coverage)] struct UvcState { cfg: UvcConfig, ctrl_len: usize, default: [u8; STREAM_CTRL_SIZE_MAX], probe: [u8; STREAM_CTRL_SIZE_MAX], commit: [u8; STREAM_CTRL_SIZE_MAX], cfg_snapshot: Option, } #[cfg(coverage)] #[derive(Clone, Copy)] struct PendingRequest { interface: u8, selector: u8, expected_len: usize, } #[cfg(coverage)] #[derive(Clone, Copy)] struct UvcInterfaces { control: u8, streaming: u8, } #[cfg(coverage)] #[derive(Clone, Copy, Debug, PartialEq, Eq)] struct ConfigfsSnapshot { width: u32, height: u32, default_interval: u32, frame_interval: u32, maxpacket: u32, maxburst: u32, } #[cfg(coverage)] struct MmapBuffer { ptr: *mut u8, len: usize, } #[cfg(coverage)] struct UvcVideoStream { buffers: Vec, frame_path: std::path::PathBuf, latest_frame: Vec, frame_max_bytes: usize, } #[cfg(coverage)] #[derive(Default)] struct UvcVideoStats { queued: u64, reloaded: u64, replayed_stale: u64, rejected_oversize: u64, rejected_invalid: u64, fallback_idle: u64, latest_bytes: usize, } #[cfg(coverage)] impl UvcVideoStream { fn new(_fd: i32) -> Self { Self { buffers: Vec::new(), frame_path: frame_spool_path(), latest_frame: IDLE_MJPEG_FRAME.to_vec(), frame_max_bytes: MAX_MJPEG_FRAME_BYTES, } } fn refresh_latest_frame(&mut self) { let stale = frame_spool_is_stale(&self.frame_path, frame_spool_max_age()); if stale && looks_like_mjpeg_frame(&self.latest_frame) { return; } let max_frame_bytes = self.frame_payload_limit(); if let Ok(frame) = std::fs::read(&self.frame_path) && frame.len() <= max_frame_bytes && looks_like_mjpeg_frame(&frame) { self.latest_frame = frame; } else if !looks_like_mjpeg_frame(&self.latest_frame) { self.latest_frame = IDLE_MJPEG_FRAME.to_vec(); } } fn frame_payload_limit(&self) -> usize { self.buffers .iter() .map(|buffer| buffer.len) .min() .unwrap_or(MAX_MJPEG_FRAME_BYTES) .min(self.frame_max_bytes) } fn frame_for_buffer(&self, buffer_len: usize) -> &[u8] { if self.latest_frame.len() <= buffer_len && looks_like_mjpeg_frame(&self.latest_frame) { &self.latest_frame } else if IDLE_MJPEG_FRAME.len() <= buffer_len { IDLE_MJPEG_FRAME } else { MINIMAL_MJPEG_FRAME } } } #[cfg(coverage)] fn frame_spool_path() -> std::path::PathBuf { env::var("LESAVKA_UVC_FRAME_PATH") .map(std::path::PathBuf::from) .unwrap_or_else(|_| std::path::PathBuf::from("/run/lesavka-uvc-frame.mjpg")) } #[cfg(coverage)] fn looks_like_mjpeg_frame(frame: &[u8]) -> bool { frame.len() > MINIMAL_MJPEG_FRAME.len() && frame.starts_with(&[0xff, 0xd8]) && frame.ends_with(&[0xff, 0xd9]) }