use std::path::PathBuf; use std::sync::{Arc, Mutex}; use anyhow::{Context, Result}; use chrono::Utc; use lesavka_common::lesavka::{ CalibrationAction, CalibrationRequest, CalibrationState as ProtoCalibrationState, }; use crate::upstream_media_runtime::UpstreamMediaRuntime; pub const FACTORY_MJPEG_AUDIO_OFFSET_US: i64 = 0; pub const FACTORY_MJPEG_VIDEO_OFFSET_1280X720_20_US: i64 = 162_659; pub const FACTORY_MJPEG_VIDEO_OFFSET_1280X720_30_US: i64 = 135_090; pub const FACTORY_MJPEG_VIDEO_OFFSET_1920X1080_20_US: i64 = 160_045; pub const FACTORY_MJPEG_VIDEO_OFFSET_1920X1080_30_US: i64 = 127_952; pub const FACTORY_MJPEG_AUDIO_MODE_OFFSETS_US: &str = "1280x720@20=0,1280x720@30=0,1920x1080@20=0,1920x1080@30=0"; pub const FACTORY_MJPEG_VIDEO_MODE_OFFSETS_US: &str = "1280x720@20=162659,1280x720@30=135090,1920x1080@20=160045,1920x1080@30=127952"; // Direct UVC/UAC output-delay probes against the lab RC target showed a // per-mode sync center for MJPEG/UVC video. This is output-path compensation, // not a freshness buffer. The scalar fallback follows the default UVC mode. pub const FACTORY_MJPEG_VIDEO_OFFSET_US: i64 = FACTORY_MJPEG_VIDEO_OFFSET_1280X720_30_US; const LEGACY_FACTORY_MJPEG_AUDIO_OFFSET_US: i64 = -45_000; const PREVIOUS_FACTORY_MJPEG_AUDIO_OFFSET_US: i64 = 720_000; const PREVIOUS_TUNED_MJPEG_AUDIO_OFFSET_US: i64 = 1_260_000; const PREVIOUS_FACTORY_MJPEG_VIDEO_OFFSET_US: i64 = 0; const PREVIOUS_DELAYED_FACTORY_MJPEG_VIDEO_OFFSET_US: i64 = 350_000; const PREVIOUS_BROWSER_FACTORY_MJPEG_VIDEO_OFFSET_US: i64 = 130_000; const PREVIOUS_SCALAR_FACTORY_MJPEG_VIDEO_OFFSET_US: i64 = 170_000; const PREVIOUS_OVERSHOT_FACTORY_MJPEG_VIDEO_OFFSET_US: i64 = 1_090_000; const PROFILE: &str = "mjpeg"; const FACTORY_CONFIDENCE: &str = "factory"; const PREVIOUS_OFFSET_LIMIT_US: i64 = 500_000; const OFFSET_LIMIT_US: i64 = 1_500_000; #[derive(Debug, Clone, PartialEq, Eq)] struct CalibrationSnapshot { profile: String, factory_audio_offset_us: i64, factory_video_offset_us: i64, default_audio_offset_us: i64, default_video_offset_us: i64, active_audio_offset_us: i64, active_video_offset_us: i64, source: String, confidence: String, updated_at: String, detail: String, } #[derive(Debug)] pub struct CalibrationStore { path: PathBuf, runtime: Arc, state: Mutex, } impl CalibrationStore { /// Keeps `load` explicit because it sits on calibration state, where persisted and factory offsets must stay auditable. /// Inputs are the typed parameters; output is the return value or side effect. pub fn load(runtime: Arc) -> Self { let path = calibration_path(); let state = std::fs::read_to_string(&path) .ok() .map(|raw| migrate_legacy_snapshot(parse_snapshot(&raw))) .unwrap_or_else(snapshot_from_env); runtime.set_playout_offsets(state.active_video_offset_us, state.active_audio_offset_us); Self { path, runtime, state: Mutex::new(state), } } pub fn current(&self) -> ProtoCalibrationState { self.state .lock() .expect("calibration mutex poisoned") .to_proto() } /// Keeps `apply` explicit because it sits on calibration state, where persisted and factory offsets must stay auditable. /// Inputs are the typed parameters; output is the return value or side effect. pub fn apply(&self, request: CalibrationRequest) -> Result { let mut state = self.state.lock().expect("calibration mutex poisoned"); let action = CalibrationAction::try_from(request.action).unwrap_or(CalibrationAction::Unspecified); match action { CalibrationAction::Unspecified => {} CalibrationAction::RestoreDefault => { state.active_audio_offset_us = state.default_audio_offset_us; state.active_video_offset_us = state.default_video_offset_us; state.source = "default".to_string(); state.confidence = "saved-default".to_string(); state.detail = "restored saved upstream A/V calibration".to_string(); touch(&mut state); } CalibrationAction::RestoreFactory => { state.active_audio_offset_us = state.factory_audio_offset_us; state.active_video_offset_us = state.factory_video_offset_us; state.source = "factory".to_string(); state.confidence = FACTORY_CONFIDENCE.to_string(); state.detail = "restored release-shipped MJPEG upstream calibration".to_string(); touch(&mut state); } CalibrationAction::AdjustActive => { state.active_audio_offset_us = clamp_offset( state .active_audio_offset_us .saturating_add(request.audio_delta_us), ); state.active_video_offset_us = clamp_offset( state .active_video_offset_us .saturating_add(request.video_delta_us), ); state.source = "manual".to_string(); state.confidence = "manual".to_string(); state.detail = format!( "manual upstream A/V calibration nudge: audio {:+.1}ms, video {:+.1}ms", request.audio_delta_us as f64 / 1000.0, request.video_delta_us as f64 / 1000.0 ); touch(&mut state); } CalibrationAction::BlindEstimate => { state.active_audio_offset_us = clamp_offset( state .active_audio_offset_us .saturating_add(request.audio_delta_us), ); state.active_video_offset_us = clamp_offset( state .active_video_offset_us .saturating_add(request.video_delta_us), ); state.source = "blind".to_string(); state.confidence = "estimated".to_string(); state.detail = if request.note.trim().is_empty() { format!( "blind estimate applied from relay telemetry: delivery skew {:.1}ms, enqueue skew {:.1}ms", request.observed_delivery_skew_ms, request.observed_enqueue_skew_ms ) } else { request.note }; touch(&mut state); } CalibrationAction::SaveActiveAsDefault => { state.default_audio_offset_us = state.active_audio_offset_us; state.default_video_offset_us = state.active_video_offset_us; state.source = "default".to_string(); state.confidence = "measured".to_string(); state.detail = "saved current upstream A/V calibration as site default".to_string(); touch(&mut state); } } self.runtime .set_playout_offsets(state.active_video_offset_us, state.active_audio_offset_us); persist_snapshot(&self.path, &state)?; Ok(state.to_proto()) } /// Keeps `apply_transient_blind_estimate` explicit because it sits on calibration state, where persisted and factory offsets must stay auditable. /// Inputs are the typed parameters; output is the return value or side effect. pub fn apply_transient_blind_estimate( &self, audio_delta_us: i64, video_delta_us: i64, observed_delivery_skew_ms: f32, observed_enqueue_skew_ms: f32, note: impl Into, ) -> ProtoCalibrationState { let mut state = self.state.lock().expect("calibration mutex poisoned"); state.active_audio_offset_us = clamp_offset(state.active_audio_offset_us.saturating_add(audio_delta_us)); state.active_video_offset_us = clamp_offset(state.active_video_offset_us.saturating_add(video_delta_us)); state.source = "blind".to_string(); state.confidence = "runtime-estimated".to_string(); let note = note.into(); state.detail = if note.trim().is_empty() { format!( "transient blind estimate from relay telemetry: delivery skew {:.1}ms, enqueue skew {:.1}ms", observed_delivery_skew_ms, observed_enqueue_skew_ms ) } else { note }; touch(&mut state); self.runtime .set_playout_offsets(state.active_video_offset_us, state.active_audio_offset_us); state.to_proto() } } impl CalibrationSnapshot { /// Keeps `to_proto` explicit because it sits on calibration state, where persisted and factory offsets must stay auditable. /// Inputs are the typed parameters; output is the return value or side effect. fn to_proto(&self) -> ProtoCalibrationState { ProtoCalibrationState { profile: self.profile.clone(), factory_audio_offset_us: self.factory_audio_offset_us, factory_video_offset_us: self.factory_video_offset_us, default_audio_offset_us: self.default_audio_offset_us, default_video_offset_us: self.default_video_offset_us, active_audio_offset_us: self.active_audio_offset_us, active_video_offset_us: self.active_video_offset_us, source: self.source.clone(), confidence: self.confidence.clone(), updated_at: self.updated_at.clone(), detail: self.detail.clone(), } } } pub fn calibration_path() -> PathBuf { std::env::var("LESAVKA_CALIBRATION_PATH") .ok() .filter(|path| !path.trim().is_empty()) .map(PathBuf::from) .unwrap_or_else(|| PathBuf::from("/var/lib/lesavka/calibration.toml")) } /// Keeps `snapshot_from_env` explicit because it sits on calibration state, where persisted and factory offsets must stay auditable. /// Inputs are the typed parameters; output is the return value or side effect. fn snapshot_from_env() -> CalibrationSnapshot { let mode = current_uvc_mode(); let factory_audio_offset_us = mode .as_deref() .and_then(|mode| lookup_mode_offset_us(FACTORY_MJPEG_AUDIO_MODE_OFFSETS_US, mode)) .unwrap_or(FACTORY_MJPEG_AUDIO_OFFSET_US); let factory_video_offset_us = mode .as_deref() .and_then(|mode| lookup_mode_offset_us(FACTORY_MJPEG_VIDEO_MODE_OFFSETS_US, mode)) .unwrap_or(FACTORY_MJPEG_VIDEO_OFFSET_US); let env_audio = configured_offset_us( "LESAVKA_UPSTREAM_AUDIO_PLAYOUT_MODE_OFFSETS_US", "LESAVKA_UPSTREAM_AUDIO_PLAYOUT_OFFSET_US", mode.as_deref(), is_stale_audio_offset_us, ); let env_video = configured_offset_us( "LESAVKA_UPSTREAM_VIDEO_PLAYOUT_MODE_OFFSETS_US", "LESAVKA_UPSTREAM_VIDEO_PLAYOUT_OFFSET_US", mode.as_deref(), is_stale_video_offset_us, ); let default_audio_offset_us = env_audio.unwrap_or(factory_audio_offset_us); let default_video_offset_us = env_video.unwrap_or(factory_video_offset_us); let source = if env_audio.is_some() || env_video.is_some() { "env".to_string() } else { "factory".to_string() }; let confidence = if source == "factory" { FACTORY_CONFIDENCE.to_string() } else { "configured".to_string() }; CalibrationSnapshot { profile: PROFILE.to_string(), factory_audio_offset_us, factory_video_offset_us, default_audio_offset_us, default_video_offset_us, active_audio_offset_us: default_audio_offset_us, active_video_offset_us: default_video_offset_us, source, confidence, updated_at: Utc::now().to_rfc3339(), detail: "loaded upstream A/V calibration defaults".to_string(), } } /// Keeps `parse_snapshot` explicit because it sits on calibration state, where persisted and factory offsets must stay auditable. /// Inputs are the typed parameters; output is the return value or side effect. fn parse_snapshot(raw: &str) -> CalibrationSnapshot { let fallback = snapshot_from_env(); let value = |key: &str| -> Option { raw.lines().find_map(|line| { let trimmed = line.trim(); let (left, right) = trimmed.split_once('=')?; (left.trim() == key).then(|| right.trim().trim_matches('"').to_string()) }) }; let number = |key: &str, default: i64| -> i64 { value(key) .and_then(|raw| raw.parse::().ok()) .map(clamp_offset) .unwrap_or(default) }; CalibrationSnapshot { profile: value("profile").unwrap_or(fallback.profile), factory_audio_offset_us: fallback.factory_audio_offset_us, factory_video_offset_us: fallback.factory_video_offset_us, default_audio_offset_us: number( "default_audio_offset_us", fallback.default_audio_offset_us, ), default_video_offset_us: number( "default_video_offset_us", fallback.default_video_offset_us, ), active_audio_offset_us: number("active_audio_offset_us", fallback.active_audio_offset_us), active_video_offset_us: number("active_video_offset_us", fallback.active_video_offset_us), source: value("source").unwrap_or(fallback.source), confidence: value("confidence").unwrap_or(fallback.confidence), updated_at: value("updated_at").unwrap_or(fallback.updated_at), detail: value("detail").unwrap_or(fallback.detail), } } /// Keeps `migrate_legacy_snapshot` explicit because it sits on calibration state, where persisted and factory offsets must stay auditable. /// Inputs are the typed parameters; output is the return value or side effect. fn migrate_legacy_snapshot(mut state: CalibrationSnapshot) -> CalibrationSnapshot { let source_allows_migration = matches!(state.source.as_str(), "factory" | "env"); let confidence_allows_migration = matches!(state.confidence.as_str(), "factory" | "configured"); let clamped_previous_baseline = matches!( state.default_audio_offset_us, PREVIOUS_OFFSET_LIMIT_US | OFFSET_LIMIT_US ) && state .detail .contains("loaded upstream A/V calibration defaults"); let untouched_legacy_audio = (is_stale_audio_offset_us(state.default_audio_offset_us) || state.default_audio_offset_us == state.factory_audio_offset_us || clamped_previous_baseline) && state.active_audio_offset_us == state.default_audio_offset_us; let untouched_legacy_video = is_stale_video_offset_us(state.default_video_offset_us) && state.active_video_offset_us == state.default_video_offset_us; if state.profile == PROFILE && source_allows_migration && confidence_allows_migration && untouched_legacy_audio && untouched_legacy_video { let old_audio_offset_us = state.default_audio_offset_us; let old_video_offset_us = state.default_video_offset_us; state.default_audio_offset_us = state.factory_audio_offset_us; state.active_audio_offset_us = state.factory_audio_offset_us; state.default_video_offset_us = state.factory_video_offset_us; state.active_video_offset_us = state.factory_video_offset_us; state.source = "factory".to_string(); state.confidence = FACTORY_CONFIDENCE.to_string(); state.detail = format!( "migrated legacy MJPEG upstream A/V baseline from audio {:+.1}ms/video {:+.1}ms to audio {:+.1}ms/video {:+.1}ms", old_audio_offset_us as f64 / 1000.0, old_video_offset_us as f64 / 1000.0, state.factory_audio_offset_us as f64 / 1000.0, state.factory_video_offset_us as f64 / 1000.0 ); touch(&mut state); } state } /// Keeps `persist_snapshot` explicit because it sits on calibration state, where persisted and factory offsets must stay auditable. /// Inputs are the typed parameters; output is the return value or side effect. fn persist_snapshot(path: &PathBuf, state: &CalibrationSnapshot) -> Result<()> { if let Some(parent) = path.parent() { std::fs::create_dir_all(parent) .with_context(|| format!("creating calibration directory {}", parent.display()))?; } std::fs::write(path, serialize_snapshot(state)) .with_context(|| format!("writing calibration state {}", path.display())) } /// Keeps `serialize_snapshot` explicit because it sits on calibration state, where persisted and factory offsets must stay auditable. /// Inputs are the typed parameters; output is the return value or side effect. fn serialize_snapshot(state: &CalibrationSnapshot) -> String { format!( "profile=\"{}\"\ndefault_audio_offset_us={}\ndefault_video_offset_us={}\nactive_audio_offset_us={}\nactive_video_offset_us={}\nsource=\"{}\"\nconfidence=\"{}\"\nupdated_at=\"{}\"\ndetail=\"{}\"\n", escape_value(&state.profile), state.default_audio_offset_us, state.default_video_offset_us, state.active_audio_offset_us, state.active_video_offset_us, escape_value(&state.source), escape_value(&state.confidence), escape_value(&state.updated_at), escape_value(&state.detail), ) } fn touch(state: &mut CalibrationSnapshot) { state.updated_at = Utc::now().to_rfc3339(); } fn configured_offset_us( mode_map_name: &str, scalar_name: &str, mode: Option<&str>, is_stale_scalar: impl Fn(i64) -> bool, ) -> Option { mode.and_then(|mode| env_mode_offset_us(mode_map_name, mode)) .or_else(|| env_i64(scalar_name).filter(|offset| !is_stale_scalar(*offset))) } /// Keeps `current_uvc_mode` explicit because it sits on calibration state, where persisted and factory offsets must stay auditable. /// Inputs are the typed parameters; output is the return value or side effect. fn current_uvc_mode() -> Option { env_mode("UVC_MODE") .or_else(|| env_mode("LESAVKA_UVC_MODE")) .or_else(|| { let width = env_u32("LESAVKA_UVC_WIDTH")?; let height = env_u32("LESAVKA_UVC_HEIGHT")?; let fps = env_u32("LESAVKA_UVC_FPS") .or_else(|| { env_u32("LESAVKA_UVC_INTERVAL") .and_then(|interval| (interval > 0).then_some(10_000_000 / interval)) })? .max(1); Some(format!("{width}x{height}@{fps}")) }) .or_else(|| { let width = env_u32("LESAVKA_CAM_WIDTH")?; let height = env_u32("LESAVKA_CAM_HEIGHT")?; let fps = env_u32("LESAVKA_CAM_FPS")?.max(1); Some(format!("{width}x{height}@{fps}")) }) } /// Keeps `env_mode` explicit because it sits on calibration state, where persisted and factory offsets must stay auditable. /// Inputs are the typed parameters; output is the return value or side effect. fn env_mode(name: &str) -> Option { std::env::var(name).ok().and_then(|value| { let trimmed = value.trim(); let valid = trimmed.split_once('@').and_then(|(size, fps)| { let (width, height) = size.split_once('x')?; width.parse::().ok()?; height.parse::().ok()?; fps.parse::().ok()?; Some(()) }); valid.map(|()| trimmed.to_string()) }) } fn env_mode_offset_us(name: &str, mode: &str) -> Option { std::env::var(name) .ok() .and_then(|map| lookup_mode_offset_us(&map, mode)) } fn lookup_mode_offset_us(map: &str, mode: &str) -> Option { map.split(',').find_map(|entry| { let (key, value) = entry.trim().split_once('=')?; (key.trim() == mode) .then(|| value.trim().parse::().ok().map(clamp_offset)) .flatten() }) } fn env_i64(name: &str) -> Option { std::env::var(name) .ok() .and_then(|value| value.trim().parse::().ok()) .map(clamp_offset) } fn env_u32(name: &str) -> Option { std::env::var(name) .ok() .and_then(|value| value.trim().parse::().ok()) } fn is_stale_audio_offset_us(offset: i64) -> bool { matches!( offset, LEGACY_FACTORY_MJPEG_AUDIO_OFFSET_US | PREVIOUS_FACTORY_MJPEG_AUDIO_OFFSET_US | PREVIOUS_TUNED_MJPEG_AUDIO_OFFSET_US ) } fn is_stale_video_offset_us(offset: i64) -> bool { matches!( offset, PREVIOUS_FACTORY_MJPEG_VIDEO_OFFSET_US | PREVIOUS_DELAYED_FACTORY_MJPEG_VIDEO_OFFSET_US | PREVIOUS_BROWSER_FACTORY_MJPEG_VIDEO_OFFSET_US | PREVIOUS_SCALAR_FACTORY_MJPEG_VIDEO_OFFSET_US | PREVIOUS_OVERSHOT_FACTORY_MJPEG_VIDEO_OFFSET_US ) } fn clamp_offset(value: i64) -> i64 { value.clamp(-OFFSET_LIMIT_US, OFFSET_LIMIT_US) } fn escape_value(value: &str) -> String { value.replace('\\', "\\\\").replace('"', "\\\"") } #[cfg(test)] #[path = "calibration/tests/mod.rs"] mod tests;