lesavka/server/src/camera.rs

133 lines
3.6 KiB
Rust
Raw Normal View History

2026-01-28 17:52:00 -03:00
// server/src/camera.rs
#![cfg_attr(coverage, allow(dead_code, unused_imports, unused_variables))]
mod selection;
2026-01-28 17:52:00 -03:00
use std::sync::{OnceLock, RwLock};
use selection::select_camera_config;
2026-01-28 17:52:00 -03:00
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum CameraOutput {
Uvc,
Hdmi,
}
impl CameraOutput {
/// Return the canonical string name for the transport.
2026-01-28 17:52:00 -03:00
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.
2026-01-28 17:52:00 -03:00
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,
}
2026-01-28 17:52:00 -03:00
#[derive(Clone, Debug)]
pub struct HdmiConnector {
pub name: String,
pub id: Option<u32>,
pub modes: Vec<HdmiMode>,
2026-01-28 17:52:00 -03:00
}
#[derive(Clone, Debug)]
pub struct CameraConfig {
pub output: CameraOutput,
pub codec: CameraCodec,
pub width: u32,
pub height: u32,
pub fps: u32,
pub hdmi: Option<HdmiConnector>,
}
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))
}
}
2026-01-28 17:52:00 -03:00
static LAST_CONFIG: OnceLock<RwLock<CameraConfig>> = 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.
2026-01-28 17:52:00 -03:00
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))]
2026-01-28 17:52:00 -03:00
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;