sync: recalibrate fresh uac to uvc timing

This commit is contained in:
Brad Stein 2026-05-01 16:57:55 -03:00
parent dc8559f764
commit c960df7400
12 changed files with 74 additions and 39 deletions

View File

@ -140,8 +140,8 @@ Context: the mirrored browser probe finally reproduced the real failure class on
### Phase 2: Bound UAC Freshness
- [x] Configure UAC `appsrc` as non-blocking and bounded.
- [x] Log and drop UAC appsrc push failures instead of treating enqueue as guaranteed playback.
- [x] Raise calibration offset limits to cover one-second healing without rejecting measured probe corrections.
- [x] Update the MJPEG/UVC factory audio baseline from `-45ms` to `+720ms` based on the first trustworthy mirrored browser probe artifact.
- [x] Raise calibration offset limits enough to cover the measured MJPEG/UVC path delta without rejecting probe corrections.
- [x] Update the MJPEG/UVC factory audio baseline from the old `-45ms`/`+720ms` values to `+1260ms` as the mirrored probe exposes the fresh UAC-vs-UVC path delta.
- [x] Migrate untouched legacy `-45ms` factory/env calibration files on load so old installs actually receive the new baseline.
- [x] Make the video/audio-master wait offset-aware so a positive audio playout delay does not freeze UVC video while UAC sleeps before emission.
- [ ] Flush/stop UAC cleanly on session close, replacement, and recovery.
@ -176,5 +176,6 @@ Context: the mirrored browser probe finally reproduced the real failure class on
- 0.16.23 local validation passed for fresh-queue behavior, uplink/probe freshness contracts, sync analyzer tests, client/server binary checks, and whitespace checks.
- 0.16.23 live mirrored run improved to p95 `215.2 ms`, median `+142.2 ms`, 13 paired coded pulses, and raw activity alignment within `6.6 ms` of coded pairs. Patch 0.16.24 makes the probe print local client and remote server versions before capture so every run records what was actually tested.
- 0.16.24 live mirrored run improved again to p95 `168.4 ms`, median `-19.1 ms`, 11 paired coded pulses, but still failed because individual paired pulses bounced between about `-168 ms` and `+45 ms`. Client logs showed the microphone uplink queue still accumulating depth `16`; patch 0.16.25 makes microphone uplink queues latest-only too so stale audio PTS cannot continue acting as the server timing master under backpressure.
- 0.16.25 removed the client mic backlog but exposed a stable hardware/browser path delta: p95 `557.3 ms`, median `-540.5 ms`, drift `+9.0 ms`, and fresh mic delivery ages around `2-10 ms`. Patch 0.16.26 raises the MJPEG/UVC factory audio delay to `+1260 ms` and expands the calibration clamp so this stable offset can actually be corrected instead of rejected.
- [ ] Re-run the mirrored browser probe after the pre-start false-positive fix.
- [ ] Run Google Meet manual validation.

6
Cargo.lock generated
View File

@ -1652,7 +1652,7 @@ checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
[[package]]
name = "lesavka_client"
version = "0.16.25"
version = "0.16.26"
dependencies = [
"anyhow",
"async-stream",
@ -1686,7 +1686,7 @@ dependencies = [
[[package]]
name = "lesavka_common"
version = "0.16.25"
version = "0.16.26"
dependencies = [
"anyhow",
"base64",
@ -1698,7 +1698,7 @@ dependencies = [
[[package]]
name = "lesavka_server"
version = "0.16.25"
version = "0.16.26"
dependencies = [
"anyhow",
"base64",

View File

@ -4,7 +4,7 @@ path = "src/main.rs"
[package]
name = "lesavka_client"
version = "0.16.25"
version = "0.16.26"
edition = "2024"
[dependencies]

View File

@ -346,11 +346,11 @@ impl Default for CalibrationStatus {
Self {
available: false,
profile: "mjpeg".to_string(),
factory_audio_offset_us: 720_000,
factory_audio_offset_us: 1_260_000,
factory_video_offset_us: 0,
default_audio_offset_us: 720_000,
default_audio_offset_us: 1_260_000,
default_video_offset_us: 0,
active_audio_offset_us: 720_000,
active_audio_offset_us: 1_260_000,
active_video_offset_us: 0,
source: "unknown".to_string(),
confidence: "unknown".to_string(),

View File

@ -405,7 +405,7 @@ fn capture_power_status_updates_snapshot_state() {
fn calibration_status_tracks_proto_unavailable_and_status_line() {
let mut state = LauncherState::new();
assert!(!state.calibration.available);
assert_eq!(state.calibration.active_audio_offset_us, 720_000);
assert_eq!(state.calibration.active_audio_offset_us, 1_260_000);
let unavailable = CalibrationStatus::unavailable("server unreachable");
assert!(!unavailable.available);
@ -414,7 +414,7 @@ fn calibration_status_tracks_proto_unavailable_and_status_line() {
state.set_calibration(CalibrationStatus::from_proto(
lesavka_common::lesavka::CalibrationState {
profile: "mjpeg".to_string(),
factory_audio_offset_us: 720_000,
factory_audio_offset_us: 1_260_000,
factory_video_offset_us: 0,
default_audio_offset_us: -40_000,
default_video_offset_us: 1_000,
@ -429,7 +429,7 @@ fn calibration_status_tracks_proto_unavailable_and_status_line() {
assert!(state.calibration.available);
assert_eq!(state.calibration.profile, "mjpeg");
assert_eq!(state.calibration.factory_audio_offset_us, 720_000);
assert_eq!(state.calibration.factory_audio_offset_us, 1_260_000);
assert_eq!(state.calibration.factory_video_offset_us, 0);
assert_eq!(state.calibration.default_audio_offset_us, -40_000);
assert_eq!(state.calibration.default_video_offset_us, 1_000);

View File

@ -1,6 +1,6 @@
[package]
name = "lesavka_common"
version = "0.16.25"
version = "0.16.26"
edition = "2024"
build = "build.rs"

View File

@ -14,8 +14,9 @@ INSTALL_UVC_CODEC=${LESAVKA_INSTALL_UVC_CODEC:-mjpeg}
INSTALL_SERVER_BIND_ADDR=${LESAVKA_INSTALL_SERVER_BIND_ADDR:-0.0.0.0:50051}
LESAVKA_TLS_DIR=${LESAVKA_TLS_DIR:-/etc/lesavka/pki}
LESAVKA_CLIENT_BUNDLE=${LESAVKA_CLIENT_BUNDLE:-/etc/lesavka/lesavka-client-pki.tar.gz}
DEFAULT_MJPEG_UPSTREAM_AUDIO_PLAYOUT_OFFSET_US=720000
DEFAULT_MJPEG_UPSTREAM_AUDIO_PLAYOUT_OFFSET_US=1260000
LEGACY_MJPEG_UPSTREAM_AUDIO_PLAYOUT_OFFSET_US=-45000
PREVIOUS_MJPEG_UPSTREAM_AUDIO_PLAYOUT_OFFSET_US=720000
resolve_upstream_audio_playout_offset_us() {
if [[ -n ${LESAVKA_INSTALL_UPSTREAM_AUDIO_PLAYOUT_OFFSET_US:-} ]]; then
@ -23,9 +24,9 @@ resolve_upstream_audio_playout_offset_us() {
return 0
fi
if [[ ${LESAVKA_UPSTREAM_AUDIO_PLAYOUT_OFFSET_US:-} == "$LEGACY_MJPEG_UPSTREAM_AUDIO_PLAYOUT_OFFSET_US" ]]; then
echo "⚠️ migrating legacy upstream audio playout offset -45ms to +720ms for MJPEG/UVC." >&2
echo " Use LESAVKA_INSTALL_UPSTREAM_AUDIO_PLAYOUT_OFFSET_US=-45000 only if you intentionally need the old value." >&2
if [[ ${LESAVKA_UPSTREAM_AUDIO_PLAYOUT_OFFSET_US:-} == "$LEGACY_MJPEG_UPSTREAM_AUDIO_PLAYOUT_OFFSET_US" || ${LESAVKA_UPSTREAM_AUDIO_PLAYOUT_OFFSET_US:-} == "$PREVIOUS_MJPEG_UPSTREAM_AUDIO_PLAYOUT_OFFSET_US" ]]; then
echo "⚠️ migrating stale upstream audio playout offset to +1260ms for MJPEG/UVC." >&2
echo " Use LESAVKA_INSTALL_UPSTREAM_AUDIO_PLAYOUT_OFFSET_US to intentionally keep an older value." >&2
printf '%s\n' "$DEFAULT_MJPEG_UPSTREAM_AUDIO_PLAYOUT_OFFSET_US"
return 0
fi

View File

@ -10,7 +10,7 @@ bench = false
[package]
name = "lesavka_server"
version = "0.16.25"
version = "0.16.26"
edition = "2024"
autobins = false

View File

@ -9,12 +9,13 @@ use lesavka_common::lesavka::{
use crate::upstream_media_runtime::UpstreamMediaRuntime;
pub const FACTORY_MJPEG_AUDIO_OFFSET_US: i64 = 720_000;
pub const FACTORY_MJPEG_AUDIO_OFFSET_US: i64 = 1_260_000;
pub const FACTORY_MJPEG_VIDEO_OFFSET_US: i64 = 0;
const LEGACY_FACTORY_MJPEG_AUDIO_OFFSET_US: i64 = -45_000;
const PREVIOUS_FACTORY_MJPEG_AUDIO_OFFSET_US: i64 = 720_000;
const PROFILE: &str = "mjpeg";
const FACTORY_CONFIDENCE: &str = "factory";
const OFFSET_LIMIT_US: i64 = 1_000_000;
const OFFSET_LIMIT_US: i64 = 1_500_000;
#[derive(Debug, Clone, PartialEq, Eq)]
struct CalibrationSnapshot {
@ -236,9 +237,10 @@ fn parse_snapshot(raw: &str) -> CalibrationSnapshot {
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 untouched_legacy_audio = state.default_audio_offset_us
== LEGACY_FACTORY_MJPEG_AUDIO_OFFSET_US
&& state.active_audio_offset_us == LEGACY_FACTORY_MJPEG_AUDIO_OFFSET_US;
let untouched_legacy_audio = matches!(
state.default_audio_offset_us,
LEGACY_FACTORY_MJPEG_AUDIO_OFFSET_US | PREVIOUS_FACTORY_MJPEG_AUDIO_OFFSET_US
) && state.active_audio_offset_us == state.default_audio_offset_us;
let untouched_legacy_video = state.default_video_offset_us == FACTORY_MJPEG_VIDEO_OFFSET_US
&& state.active_video_offset_us == FACTORY_MJPEG_VIDEO_OFFSET_US;
if state.profile == PROFILE
@ -247,13 +249,14 @@ fn migrate_legacy_snapshot(mut state: CalibrationSnapshot) -> CalibrationSnapsho
&& untouched_legacy_audio
&& untouched_legacy_video
{
let old_audio_offset_us = state.default_audio_offset_us;
state.default_audio_offset_us = FACTORY_MJPEG_AUDIO_OFFSET_US;
state.active_audio_offset_us = FACTORY_MJPEG_AUDIO_OFFSET_US;
state.source = "factory".to_string();
state.confidence = FACTORY_CONFIDENCE.to_string();
state.detail = format!(
"migrated legacy MJPEG upstream A/V baseline from {:+.1}ms to {:+.1}ms",
LEGACY_FACTORY_MJPEG_AUDIO_OFFSET_US as f64 / 1000.0,
old_audio_offset_us as f64 / 1000.0,
FACTORY_MJPEG_AUDIO_OFFSET_US as f64 / 1000.0
);
touch(&mut state);
@ -318,7 +321,7 @@ mod tests {
],
|| {
let state = snapshot_from_env();
assert_eq!(state.default_audio_offset_us, 720_000);
assert_eq!(state.default_audio_offset_us, 1_260_000);
assert_eq!(state.active_video_offset_us, 0);
assert_eq!(state.source, "factory");
},
@ -342,10 +345,10 @@ mod tests {
note: String::new(),
})
.expect("manual adjust applies");
assert_eq!(state.active_audio_offset_us, 715_000);
assert_eq!(runtime.playout_offsets(), (0, 715_000));
assert_eq!(state.active_audio_offset_us, 1_255_000);
assert_eq!(runtime.playout_offsets(), (0, 1_255_000));
let raw = std::fs::read_to_string(file.path()).expect("persisted calibration");
assert!(raw.contains("active_audio_offset_us=715000"));
assert!(raw.contains("active_audio_offset_us=1255000"));
});
}
@ -368,7 +371,7 @@ mod tests {
],
|| {
let state = snapshot_from_env();
assert_eq!(state.default_audio_offset_us, -1_000_000);
assert_eq!(state.default_audio_offset_us, -1_500_000);
assert_eq!(state.default_video_offset_us, 12_345);
assert_eq!(state.source, "env");
assert_eq!(state.confidence, "configured");
@ -396,7 +399,7 @@ mod tests {
);
assert_eq!(state.default_audio_offset_us, FACTORY_MJPEG_AUDIO_OFFSET_US);
assert_eq!(state.default_video_offset_us, 2_500);
assert_eq!(state.active_audio_offset_us, -1_000_000);
assert_eq!(state.active_audio_offset_us, -1_500_000);
assert_eq!(state.active_video_offset_us, FACTORY_MJPEG_VIDEO_OFFSET_US);
assert_eq!(state.source, "saved");
assert_eq!(state.confidence, FACTORY_CONFIDENCE);
@ -426,14 +429,44 @@ mod tests {
let runtime = Arc::new(UpstreamMediaRuntime::new());
let store = CalibrationStore::load(runtime.clone());
let state = store.current();
assert_eq!(state.active_audio_offset_us, 720_000);
assert_eq!(state.default_audio_offset_us, 720_000);
assert_eq!(state.active_audio_offset_us, 1_260_000);
assert_eq!(state.default_audio_offset_us, 1_260_000);
assert_eq!(state.source, "factory");
assert_eq!(runtime.playout_offsets(), (0, 720_000));
assert_eq!(runtime.playout_offsets(), (0, 1_260_000));
assert!(state.detail.contains("migrated legacy MJPEG"));
});
}
#[test]
fn load_migrates_untouched_previous_factory_mjpeg_baseline() {
let file = NamedTempFile::new().expect("temp calibration file");
std::fs::write(
file.path(),
r#"
profile="mjpeg"
default_audio_offset_us=720000
default_video_offset_us=0
active_audio_offset_us=720000
active_video_offset_us=0
source="env"
confidence="configured"
detail="loaded upstream A/V calibration defaults"
"#,
)
.expect("previous calibration seed");
let path = file.path().to_string_lossy().to_string();
temp_env::with_var("LESAVKA_CALIBRATION_PATH", Some(path.as_str()), || {
let runtime = Arc::new(UpstreamMediaRuntime::new());
let store = CalibrationStore::load(runtime.clone());
let state = store.current();
assert_eq!(state.active_audio_offset_us, 1_260_000);
assert_eq!(state.default_audio_offset_us, 1_260_000);
assert_eq!(state.source, "factory");
assert_eq!(runtime.playout_offsets(), (0, 1_260_000));
assert!(state.detail.contains("from +720.0ms to +1260.0ms"));
});
}
#[test]
fn load_keeps_manual_legacy_sized_calibration() {
let file = NamedTempFile::new().expect("temp calibration file");
@ -486,7 +519,7 @@ mod tests {
.expect("blind estimate");
assert_eq!(blind.source, "blind");
assert!(blind.detail.contains("delivery skew 44.0ms"));
assert_eq!(runtime.playout_offsets(), (-2_000, 725_000));
assert_eq!(runtime.playout_offsets(), (-2_000, 1_265_000));
let manual = store
.apply(CalibrationRequest {
@ -498,7 +531,7 @@ mod tests {
note: String::new(),
})
.expect("manual clamp");
assert_eq!(manual.active_audio_offset_us, 1_000_000);
assert_eq!(manual.active_audio_offset_us, 1_500_000);
let saved = store
.apply(CalibrationRequest {

View File

@ -43,7 +43,7 @@ fn upstream_playout_offsets_default_to_mjpeg_calibration_and_accept_overrides()
temp_env::with_var_unset("LESAVKA_UPSTREAM_VIDEO_PLAYOUT_OFFSET_US", || {
assert_eq!(
super::upstream_playout_offset_us(UpstreamMediaKind::Microphone),
720_000
1_260_000
);
assert_eq!(
super::upstream_playout_offset_us(UpstreamMediaKind::Camera),

View File

@ -50,14 +50,14 @@ fn server_install_pins_hdmi_camera_and_display_defaults() {
assert!(SERVER_INSTALL.contains("${LESAVKA_HDMI_HEIGHT:-1080}"));
assert!(SERVER_INSTALL.contains("${LESAVKA_HDMI_SINK:-fbdevsink}"));
assert!(SERVER_INSTALL.contains("${LESAVKA_UPSTREAM_PLAYOUT_DELAY_MS:-1000}"));
assert!(SERVER_INSTALL.contains("DEFAULT_MJPEG_UPSTREAM_AUDIO_PLAYOUT_OFFSET_US=720000"));
assert!(SERVER_INSTALL.contains("DEFAULT_MJPEG_UPSTREAM_AUDIO_PLAYOUT_OFFSET_US=1260000"));
assert!(SERVER_INSTALL.contains("LEGACY_MJPEG_UPSTREAM_AUDIO_PLAYOUT_OFFSET_US=-45000"));
assert!(
SERVER_INSTALL.contains("LESAVKA_INSTALL_UPSTREAM_AUDIO_PLAYOUT_OFFSET_US"),
"install-specific offset override should bypass stale ambient runtime env"
);
assert!(
SERVER_INSTALL.contains("migrating legacy upstream audio playout offset -45ms to +720ms"),
SERVER_INSTALL.contains("migrating stale upstream audio playout offset to +1260ms"),
"installer should not preserve the old MJPEG/UVC sync baseline accidentally"
);
assert!(SERVER_INSTALL.contains("${LESAVKA_UPSTREAM_PAIR_SLACK_US:-80000}"));

View File

@ -467,7 +467,7 @@ mod server_main_rpc {
.expect("initial calibration")
.into_inner();
assert_eq!(initial.profile, "mjpeg");
assert_eq!(initial.active_audio_offset_us, 720_000);
assert_eq!(initial.active_audio_offset_us, 1_260_000);
let adjusted = rt
.block_on(async {