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 ### Phase 2: Bound UAC Freshness
- [x] Configure UAC `appsrc` as non-blocking and bounded. - [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] 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] 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 `-45ms` to `+720ms` based on the first trustworthy mirrored browser probe artifact. - [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] 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. - [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. - [ ] 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 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.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.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. - [ ] Re-run the mirrored browser probe after the pre-start false-positive fix.
- [ ] Run Google Meet manual validation. - [ ] Run Google Meet manual validation.

6
Cargo.lock generated
View File

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

View File

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

View File

@ -346,11 +346,11 @@ impl Default for CalibrationStatus {
Self { Self {
available: false, available: false,
profile: "mjpeg".to_string(), profile: "mjpeg".to_string(),
factory_audio_offset_us: 720_000, factory_audio_offset_us: 1_260_000,
factory_video_offset_us: 0, factory_video_offset_us: 0,
default_audio_offset_us: 720_000, default_audio_offset_us: 1_260_000,
default_video_offset_us: 0, default_video_offset_us: 0,
active_audio_offset_us: 720_000, active_audio_offset_us: 1_260_000,
active_video_offset_us: 0, active_video_offset_us: 0,
source: "unknown".to_string(), source: "unknown".to_string(),
confidence: "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() { fn calibration_status_tracks_proto_unavailable_and_status_line() {
let mut state = LauncherState::new(); let mut state = LauncherState::new();
assert!(!state.calibration.available); 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"); let unavailable = CalibrationStatus::unavailable("server unreachable");
assert!(!unavailable.available); assert!(!unavailable.available);
@ -414,7 +414,7 @@ fn calibration_status_tracks_proto_unavailable_and_status_line() {
state.set_calibration(CalibrationStatus::from_proto( state.set_calibration(CalibrationStatus::from_proto(
lesavka_common::lesavka::CalibrationState { lesavka_common::lesavka::CalibrationState {
profile: "mjpeg".to_string(), profile: "mjpeg".to_string(),
factory_audio_offset_us: 720_000, factory_audio_offset_us: 1_260_000,
factory_video_offset_us: 0, factory_video_offset_us: 0,
default_audio_offset_us: -40_000, default_audio_offset_us: -40_000,
default_video_offset_us: 1_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!(state.calibration.available);
assert_eq!(state.calibration.profile, "mjpeg"); 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.factory_video_offset_us, 0);
assert_eq!(state.calibration.default_audio_offset_us, -40_000); assert_eq!(state.calibration.default_audio_offset_us, -40_000);
assert_eq!(state.calibration.default_video_offset_us, 1_000); assert_eq!(state.calibration.default_video_offset_us, 1_000);

View File

@ -1,6 +1,6 @@
[package] [package]
name = "lesavka_common" name = "lesavka_common"
version = "0.16.25" version = "0.16.26"
edition = "2024" edition = "2024"
build = "build.rs" 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} INSTALL_SERVER_BIND_ADDR=${LESAVKA_INSTALL_SERVER_BIND_ADDR:-0.0.0.0:50051}
LESAVKA_TLS_DIR=${LESAVKA_TLS_DIR:-/etc/lesavka/pki} LESAVKA_TLS_DIR=${LESAVKA_TLS_DIR:-/etc/lesavka/pki}
LESAVKA_CLIENT_BUNDLE=${LESAVKA_CLIENT_BUNDLE:-/etc/lesavka/lesavka-client-pki.tar.gz} 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 LEGACY_MJPEG_UPSTREAM_AUDIO_PLAYOUT_OFFSET_US=-45000
PREVIOUS_MJPEG_UPSTREAM_AUDIO_PLAYOUT_OFFSET_US=720000
resolve_upstream_audio_playout_offset_us() { resolve_upstream_audio_playout_offset_us() {
if [[ -n ${LESAVKA_INSTALL_UPSTREAM_AUDIO_PLAYOUT_OFFSET_US:-} ]]; then if [[ -n ${LESAVKA_INSTALL_UPSTREAM_AUDIO_PLAYOUT_OFFSET_US:-} ]]; then
@ -23,9 +24,9 @@ resolve_upstream_audio_playout_offset_us() {
return 0 return 0
fi fi
if [[ ${LESAVKA_UPSTREAM_AUDIO_PLAYOUT_OFFSET_US:-} == "$LEGACY_MJPEG_UPSTREAM_AUDIO_PLAYOUT_OFFSET_US" ]]; then 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 legacy upstream audio playout offset -45ms to +720ms for MJPEG/UVC." >&2 echo "⚠️ migrating stale upstream audio playout offset to +1260ms for MJPEG/UVC." >&2
echo " Use LESAVKA_INSTALL_UPSTREAM_AUDIO_PLAYOUT_OFFSET_US=-45000 only if you intentionally need the old value." >&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" printf '%s\n' "$DEFAULT_MJPEG_UPSTREAM_AUDIO_PLAYOUT_OFFSET_US"
return 0 return 0
fi fi

View File

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

View File

@ -9,12 +9,13 @@ use lesavka_common::lesavka::{
use crate::upstream_media_runtime::UpstreamMediaRuntime; 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; pub const FACTORY_MJPEG_VIDEO_OFFSET_US: i64 = 0;
const LEGACY_FACTORY_MJPEG_AUDIO_OFFSET_US: i64 = -45_000; 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 PROFILE: &str = "mjpeg";
const FACTORY_CONFIDENCE: &str = "factory"; 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)] #[derive(Debug, Clone, PartialEq, Eq)]
struct CalibrationSnapshot { struct CalibrationSnapshot {
@ -236,9 +237,10 @@ fn parse_snapshot(raw: &str) -> CalibrationSnapshot {
fn migrate_legacy_snapshot(mut state: CalibrationSnapshot) -> CalibrationSnapshot { fn migrate_legacy_snapshot(mut state: CalibrationSnapshot) -> CalibrationSnapshot {
let source_allows_migration = matches!(state.source.as_str(), "factory" | "env"); let source_allows_migration = matches!(state.source.as_str(), "factory" | "env");
let confidence_allows_migration = matches!(state.confidence.as_str(), "factory" | "configured"); let confidence_allows_migration = matches!(state.confidence.as_str(), "factory" | "configured");
let untouched_legacy_audio = state.default_audio_offset_us let untouched_legacy_audio = matches!(
== LEGACY_FACTORY_MJPEG_AUDIO_OFFSET_US state.default_audio_offset_us,
&& state.active_audio_offset_us == LEGACY_FACTORY_MJPEG_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 let untouched_legacy_video = state.default_video_offset_us == FACTORY_MJPEG_VIDEO_OFFSET_US
&& state.active_video_offset_us == FACTORY_MJPEG_VIDEO_OFFSET_US; && state.active_video_offset_us == FACTORY_MJPEG_VIDEO_OFFSET_US;
if state.profile == PROFILE if state.profile == PROFILE
@ -247,13 +249,14 @@ fn migrate_legacy_snapshot(mut state: CalibrationSnapshot) -> CalibrationSnapsho
&& untouched_legacy_audio && untouched_legacy_audio
&& untouched_legacy_video && untouched_legacy_video
{ {
let old_audio_offset_us = state.default_audio_offset_us;
state.default_audio_offset_us = FACTORY_MJPEG_AUDIO_OFFSET_US; state.default_audio_offset_us = FACTORY_MJPEG_AUDIO_OFFSET_US;
state.active_audio_offset_us = FACTORY_MJPEG_AUDIO_OFFSET_US; state.active_audio_offset_us = FACTORY_MJPEG_AUDIO_OFFSET_US;
state.source = "factory".to_string(); state.source = "factory".to_string();
state.confidence = FACTORY_CONFIDENCE.to_string(); state.confidence = FACTORY_CONFIDENCE.to_string();
state.detail = format!( state.detail = format!(
"migrated legacy MJPEG upstream A/V baseline from {:+.1}ms to {:+.1}ms", "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 FACTORY_MJPEG_AUDIO_OFFSET_US as f64 / 1000.0
); );
touch(&mut state); touch(&mut state);
@ -318,7 +321,7 @@ mod tests {
], ],
|| { || {
let state = snapshot_from_env(); 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.active_video_offset_us, 0);
assert_eq!(state.source, "factory"); assert_eq!(state.source, "factory");
}, },
@ -342,10 +345,10 @@ mod tests {
note: String::new(), note: String::new(),
}) })
.expect("manual adjust applies"); .expect("manual adjust applies");
assert_eq!(state.active_audio_offset_us, 715_000); assert_eq!(state.active_audio_offset_us, 1_255_000);
assert_eq!(runtime.playout_offsets(), (0, 715_000)); assert_eq!(runtime.playout_offsets(), (0, 1_255_000));
let raw = std::fs::read_to_string(file.path()).expect("persisted calibration"); 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(); 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.default_video_offset_us, 12_345);
assert_eq!(state.source, "env"); assert_eq!(state.source, "env");
assert_eq!(state.confidence, "configured"); 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_audio_offset_us, FACTORY_MJPEG_AUDIO_OFFSET_US);
assert_eq!(state.default_video_offset_us, 2_500); 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.active_video_offset_us, FACTORY_MJPEG_VIDEO_OFFSET_US);
assert_eq!(state.source, "saved"); assert_eq!(state.source, "saved");
assert_eq!(state.confidence, FACTORY_CONFIDENCE); assert_eq!(state.confidence, FACTORY_CONFIDENCE);
@ -426,14 +429,44 @@ mod tests {
let runtime = Arc::new(UpstreamMediaRuntime::new()); let runtime = Arc::new(UpstreamMediaRuntime::new());
let store = CalibrationStore::load(runtime.clone()); let store = CalibrationStore::load(runtime.clone());
let state = store.current(); let state = store.current();
assert_eq!(state.active_audio_offset_us, 720_000); assert_eq!(state.active_audio_offset_us, 1_260_000);
assert_eq!(state.default_audio_offset_us, 720_000); assert_eq!(state.default_audio_offset_us, 1_260_000);
assert_eq!(state.source, "factory"); 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")); 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] #[test]
fn load_keeps_manual_legacy_sized_calibration() { fn load_keeps_manual_legacy_sized_calibration() {
let file = NamedTempFile::new().expect("temp calibration file"); let file = NamedTempFile::new().expect("temp calibration file");
@ -486,7 +519,7 @@ mod tests {
.expect("blind estimate"); .expect("blind estimate");
assert_eq!(blind.source, "blind"); assert_eq!(blind.source, "blind");
assert!(blind.detail.contains("delivery skew 44.0ms")); 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 let manual = store
.apply(CalibrationRequest { .apply(CalibrationRequest {
@ -498,7 +531,7 @@ mod tests {
note: String::new(), note: String::new(),
}) })
.expect("manual clamp"); .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 let saved = store
.apply(CalibrationRequest { .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", || { temp_env::with_var_unset("LESAVKA_UPSTREAM_VIDEO_PLAYOUT_OFFSET_US", || {
assert_eq!( assert_eq!(
super::upstream_playout_offset_us(UpstreamMediaKind::Microphone), super::upstream_playout_offset_us(UpstreamMediaKind::Microphone),
720_000 1_260_000
); );
assert_eq!( assert_eq!(
super::upstream_playout_offset_us(UpstreamMediaKind::Camera), 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_HEIGHT:-1080}"));
assert!(SERVER_INSTALL.contains("${LESAVKA_HDMI_SINK:-fbdevsink}")); assert!(SERVER_INSTALL.contains("${LESAVKA_HDMI_SINK:-fbdevsink}"));
assert!(SERVER_INSTALL.contains("${LESAVKA_UPSTREAM_PLAYOUT_DELAY_MS:-1000}")); 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("LEGACY_MJPEG_UPSTREAM_AUDIO_PLAYOUT_OFFSET_US=-45000"));
assert!( assert!(
SERVER_INSTALL.contains("LESAVKA_INSTALL_UPSTREAM_AUDIO_PLAYOUT_OFFSET_US"), SERVER_INSTALL.contains("LESAVKA_INSTALL_UPSTREAM_AUDIO_PLAYOUT_OFFSET_US"),
"install-specific offset override should bypass stale ambient runtime env" "install-specific offset override should bypass stale ambient runtime env"
); );
assert!( 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" "installer should not preserve the old MJPEG/UVC sync baseline accidentally"
); );
assert!(SERVER_INSTALL.contains("${LESAVKA_UPSTREAM_PAIR_SLACK_US:-80000}")); assert!(SERVER_INSTALL.contains("${LESAVKA_UPSTREAM_PAIR_SLACK_US:-80000}"));

View File

@ -467,7 +467,7 @@ mod server_main_rpc {
.expect("initial calibration") .expect("initial calibration")
.into_inner(); .into_inner();
assert_eq!(initial.profile, "mjpeg"); 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 let adjusted = rt
.block_on(async { .block_on(async {