// server/src/camera.rs #![cfg_attr(coverage, allow(dead_code, unused_imports, unused_variables))] mod selection; use std::sync::{OnceLock, RwLock}; use selection::select_camera_config; #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum CameraOutput { Uvc, Hdmi, } impl CameraOutput { /// Return the canonical string name for the transport. pub fn as_str(self) -> &'static str { match self { CameraOutput::Uvc => "uvc", CameraOutput::Hdmi => "hdmi", } } } #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum CameraCodec { H264, Mjpeg, } impl CameraCodec { /// Return the canonical string name for the codec. pub fn as_str(self) -> &'static str { match self { CameraCodec::H264 => "h264", CameraCodec::Mjpeg => "mjpeg", } } } #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct HdmiMode { pub width: u32, pub height: u32, } #[derive(Clone, Debug)] pub struct HdmiConnector { pub name: String, pub id: Option, pub modes: Vec, } #[derive(Clone, Debug)] pub struct CameraConfig { pub output: CameraOutput, pub codec: CameraCodec, pub width: u32, pub height: u32, pub fps: u32, pub hdmi: Option, } impl CameraConfig { /// Return the HDMI display mode that should be driven on the wire. /// /// Inputs: the selected camera config and optional HDMI mode overrides. /// Outputs: a width/height pair for the physical display pipeline. /// Why: the client webcam uplink can stay at a known-good capture profile /// while HDMI output scales to a mode the capture adapter actually locks. pub fn hdmi_display_size(&self) -> (u32, u32) { if self.output != CameraOutput::Hdmi { return (self.width, self.height); } if let (Some(width), Some(height)) = ( selection::read_u32_from_env("LESAVKA_HDMI_WIDTH"), selection::read_u32_from_env("LESAVKA_HDMI_HEIGHT"), ) { return (width, height); } self.hdmi .as_ref() .and_then(|hdmi| selection::preferred_hdmi_mode(&hdmi.modes)) .map(|mode| (mode.width, mode.height)) .unwrap_or((self.width, self.height)) } } static LAST_CONFIG: OnceLock> = OnceLock::new(); /// Refresh the cached camera config from the current environment. /// /// Inputs: none; the selector consults the current environment and local /// `/sys/class/drm` state. /// Outputs: the newly selected camera configuration, which is also stored as /// the last-known config for future reads. /// Why: callers need a single entrypoint for changing the active output mode /// without re-implementing the selection rules. pub fn update_camera_config() -> CameraConfig { let cfg = select_camera_config(); let lock = LAST_CONFIG.get_or_init(|| RwLock::new(cfg.clone())); *lock.write().unwrap() = cfg.clone(); cfg } #[cfg(coverage)] pub fn current_camera_config() -> CameraConfig { update_camera_config() } /// Return the last selected camera configuration. /// /// Inputs: none. /// Outputs: the cached camera configuration, or a freshly selected one when /// the cache has not been initialized yet. /// Why: call sites can read the active config without worrying about whether /// initialization already happened in this process. #[cfg(not(coverage))] pub fn current_camera_config() -> CameraConfig { if let Some(lock) = LAST_CONFIG.get() { return lock.read().unwrap().clone(); } update_camera_config() } #[cfg(test)] #[path = "tests/camera.rs"] mod tests;