//! Live media feed controls shared by the launcher and relay child. use std::{ fs, path::{Path, PathBuf}, sync::{Arc, Mutex}, time::{SystemTime, UNIX_EPOCH}, }; pub const MEDIA_CONTROL_ENV: &str = "LESAVKA_MEDIA_CONTROL"; pub const DEFAULT_MEDIA_CONTROL_PATH: &str = "/tmp/lesavka-media.control"; #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub(crate) struct MediaControlState { pub camera: bool, pub microphone: bool, pub audio: bool, } impl MediaControlState { #[must_use] pub const fn new(camera: bool, microphone: bool, audio: bool) -> Self { Self { camera, microphone, audio, } } } #[derive(Clone, Debug)] pub(crate) struct LiveMediaControls { path: PathBuf, inner: Arc>, } #[derive(Debug)] struct LiveMediaControlsInner { state: MediaControlState, } impl LiveMediaControls { #[must_use] pub fn from_env(initial: MediaControlState) -> Self { let path = std::env::var(MEDIA_CONTROL_ENV) .map(PathBuf::from) .unwrap_or_else(|_| PathBuf::from(DEFAULT_MEDIA_CONTROL_PATH)); let controls = Self { path, inner: Arc::new(Mutex::new(LiveMediaControlsInner { state: initial })), }; let _ = controls.refresh(); controls } /// Reloads the launcher-written soft-pause switches, falling back safely on read errors. pub fn refresh(&self) -> MediaControlState { let Ok(mut inner) = self.inner.lock() else { return MediaControlState::new(true, true, true); }; if let Ok(raw) = fs::read_to_string(&self.path) && let Some(state) = parse_media_control_state(&raw) { inner.state = state; } inner.state } } /// Writes one atomic-ish soft-pause request for the running relay child to poll. pub(crate) fn write_media_control_request( path: &Path, state: MediaControlState, ) -> std::io::Result<()> { fs::write( path, format!( "camera={} microphone={} audio={} {}\n", bool_flag(state.camera), bool_flag(state.microphone), bool_flag(state.audio), control_request_nonce() ), ) } /// Parses the small launcher control-file grammar used across process boundaries. fn parse_media_control_state(raw: &str) -> Option { let mut camera = None; let mut microphone = None; let mut audio = None; for token in raw.split_ascii_whitespace() { let Some((key, value)) = token.split_once('=') else { continue; }; match key { "camera" => camera = Some(parse_bool_flag(value)?), "microphone" | "mic" => microphone = Some(parse_bool_flag(value)?), "audio" | "speaker" => audio = Some(parse_bool_flag(value)?), _ => {} } } Some(MediaControlState { camera: camera?, microphone: microphone?, audio: audio?, }) } fn parse_bool_flag(value: &str) -> Option { match value.trim().to_ascii_lowercase().as_str() { "1" | "true" | "on" | "yes" => Some(true), "0" | "false" | "off" | "no" => Some(false), _ => None, } } const fn bool_flag(enabled: bool) -> &'static str { if enabled { "1" } else { "0" } } fn control_request_nonce() -> u128 { SystemTime::now() .duration_since(UNIX_EPOCH) .map(|duration| duration.as_nanos()) .unwrap_or_default() } #[cfg(test)] mod tests { use super::*; #[test] fn parses_media_control_state_from_launcher_file() { assert_eq!( parse_media_control_state("camera=1 microphone=0 audio=true 123"), Some(MediaControlState::new(true, false, true)) ); } #[test] fn live_media_controls_refresh_after_file_changes() { let dir = tempfile::tempdir().expect("tempdir"); let path = dir.path().join("media.control"); write_media_control_request(&path, MediaControlState::new(true, true, false)) .expect("write initial controls"); let controls = LiveMediaControls { path: path.clone(), inner: Arc::new(Mutex::new(LiveMediaControlsInner { state: MediaControlState::new(false, false, false), })), }; assert_eq!( controls.refresh(), MediaControlState::new(true, true, false) ); std::thread::sleep(std::time::Duration::from_millis(5)); write_media_control_request(&path, MediaControlState::new(false, true, true)) .expect("write updated controls"); assert_eq!( controls.refresh(), MediaControlState::new(false, true, true) ); } #[test] fn from_env_default_path_fallback_is_safe() { let controls = LiveMediaControls::from_env(MediaControlState::new(true, false, true)); let _ = controls.refresh(); } #[test] fn parser_tolerates_unknown_tokens_and_rejects_invalid_flags() { assert_eq!( parse_media_control_state("camera=on extra=ignored microphone=no audio=off"), Some(MediaControlState::new(true, false, false)) ); assert_eq!( parse_media_control_state("camera=maybe microphone=1 audio=1"), None ); } #[test] fn refresh_falls_back_to_all_enabled_if_lock_is_poisoned() { let controls = LiveMediaControls { path: PathBuf::from("/definitely/not/a/real/lesavka-media.control"), inner: Arc::new(Mutex::new(LiveMediaControlsInner { state: MediaControlState::new(false, false, false), })), }; let inner = Arc::clone(&controls.inner); let _ = std::panic::catch_unwind(move || { let _guard = inner.lock().expect("lock"); panic!("poison media controls lock"); }); assert_eq!(controls.refresh(), MediaControlState::new(true, true, true)); } }