sync: recalibrate fresh uac to uvc timing
This commit is contained in:
parent
dc8559f764
commit
c960df7400
@ -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
6
Cargo.lock
generated
@ -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",
|
||||
|
||||
@ -4,7 +4,7 @@ path = "src/main.rs"
|
||||
|
||||
[package]
|
||||
name = "lesavka_client"
|
||||
version = "0.16.25"
|
||||
version = "0.16.26"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
|
||||
@ -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(),
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "lesavka_common"
|
||||
version = "0.16.25"
|
||||
version = "0.16.26"
|
||||
edition = "2024"
|
||||
build = "build.rs"
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -10,7 +10,7 @@ bench = false
|
||||
|
||||
[package]
|
||||
name = "lesavka_server"
|
||||
version = "0.16.25"
|
||||
version = "0.16.26"
|
||||
edition = "2024"
|
||||
autobins = false
|
||||
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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),
|
||||
|
||||
@ -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}"));
|
||||
|
||||
@ -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 {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user