From 71b44521ef84fafd5c4bb7c7905b68c3bc7542ad Mon Sep 17 00:00:00 2001 From: Brad Stein Date: Sun, 26 Apr 2026 12:42:17 -0300 Subject: [PATCH] fix(sync): refresh playout budget after pairing --- Cargo.lock | 6 ++--- client/Cargo.toml | 2 +- common/Cargo.toml | 2 +- server/Cargo.toml | 2 +- server/src/upstream_media_runtime.rs | 9 +++++-- server/src/upstream_media_runtime/tests.rs | 28 ++++++++++++++++++++++ 6 files changed, 41 insertions(+), 8 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0d4045c..5d4b999 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1642,7 +1642,7 @@ checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" [[package]] name = "lesavka_client" -version = "0.14.1" +version = "0.14.2" dependencies = [ "anyhow", "async-stream", @@ -1676,7 +1676,7 @@ dependencies = [ [[package]] name = "lesavka_common" -version = "0.14.1" +version = "0.14.2" dependencies = [ "anyhow", "base64", @@ -1688,7 +1688,7 @@ dependencies = [ [[package]] name = "lesavka_server" -version = "0.14.1" +version = "0.14.2" dependencies = [ "anyhow", "base64", diff --git a/client/Cargo.toml b/client/Cargo.toml index 2b42fca..53f79cb 100644 --- a/client/Cargo.toml +++ b/client/Cargo.toml @@ -4,7 +4,7 @@ path = "src/main.rs" [package] name = "lesavka_client" -version = "0.14.1" +version = "0.14.2" edition = "2024" [dependencies] diff --git a/common/Cargo.toml b/common/Cargo.toml index 7922c62..2a2473e 100644 --- a/common/Cargo.toml +++ b/common/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "lesavka_common" -version = "0.14.1" +version = "0.14.2" edition = "2024" build = "build.rs" diff --git a/server/Cargo.toml b/server/Cargo.toml index 78b123c..c57ee84 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -10,7 +10,7 @@ bench = false [package] name = "lesavka_server" -version = "0.14.1" +version = "0.14.2" edition = "2024" autobins = false diff --git a/server/src/upstream_media_runtime.rs b/server/src/upstream_media_runtime.rs index f4d1204..5893ef0 100644 --- a/server/src/upstream_media_runtime.rs +++ b/server/src/upstream_media_runtime.rs @@ -326,6 +326,7 @@ impl UpstreamMediaRuntime { let pairing_deadline = *state .pairing_anchor_deadline .get_or_insert_with(|| now + upstream_playout_delay()); + let playout_delay = upstream_playout_delay(); if state.session_base_remote_pts_us.is_none() { if state.first_camera_remote_pts_us.is_some() @@ -337,7 +338,9 @@ impl UpstreamMediaRuntime { state.first_microphone_remote_pts_us.unwrap_or_default(); state.session_base_remote_pts_us = Some(first_camera_remote_pts_us.max(first_microphone_remote_pts_us)); - state.playout_epoch = Some(pairing_deadline); + let overlap_epoch = now + playout_delay; + state.playout_epoch = Some(overlap_epoch); + state.pairing_anchor_deadline = Some(overlap_epoch); if !state.startup_anchor_logged { let startup_delta_us = first_camera_remote_pts_us as i128 - first_microphone_remote_pts_us as i128; @@ -377,7 +380,9 @@ impl UpstreamMediaRuntime { .unwrap_or(remote_pts_us), }; state.session_base_remote_pts_us = Some(single_stream_base_remote_pts_us); - state.playout_epoch = Some(pairing_deadline); + let one_sided_epoch = now + playout_delay; + state.playout_epoch = Some(one_sided_epoch); + state.pairing_anchor_deadline = Some(one_sided_epoch); info!( session_id, ?kind, diff --git a/server/src/upstream_media_runtime/tests.rs b/server/src/upstream_media_runtime/tests.rs index 5b1deea..480ed8f 100644 --- a/server/src/upstream_media_runtime/tests.rs +++ b/server/src/upstream_media_runtime/tests.rs @@ -339,6 +339,34 @@ fn catastrophic_lateness_reanchors_the_shared_playout_epoch() { }); } +#[test] +fn overlap_anchor_gets_a_fresh_playout_budget_when_pairing_finishes_late() { + temp_env::with_var("LESAVKA_UPSTREAM_PLAYOUT_DELAY_MS", Some("20"), || { + let runtime = UpstreamMediaRuntime::new(); + let _camera = runtime.activate_camera(); + let _microphone = runtime.activate_microphone(); + + assert!(matches!( + runtime.plan_video_pts(1_000_000, 16_666), + super::UpstreamPlanDecision::AwaitingPair + )); + + std::thread::sleep(Duration::from_millis(15)); + let before_pair = tokio::time::Instant::now(); + let audio_first = play(runtime.plan_audio_pts(1_000_000)); + let video_first = play(runtime.plan_video_pts(1_000_000, 16_666)); + + assert!( + audio_first.due_at.saturating_duration_since(before_pair) >= Duration::from_millis(15), + "audio should keep most of the configured playout budget after late pairing" + ); + assert!( + video_first.due_at.saturating_duration_since(before_pair) >= Duration::from_millis(15), + "video should keep most of the configured playout budget after late pairing" + ); + }); +} + #[test] fn catastrophic_lateness_reanchors_only_once_per_session() { temp_env::with_var("LESAVKA_UPSTREAM_PLAYOUT_DELAY_MS", Some("20"), || {