2026-04-30 08:16:57 -03:00
|
|
|
use super::{UpstreamMediaRuntime, play, runtime_without_offsets};
|
|
|
|
|
use serial_test::serial;
|
2026-04-29 01:25:06 -03:00
|
|
|
use std::time::Duration;
|
|
|
|
|
|
|
|
|
|
fn with_info_tracing<T>(f: impl FnOnce() -> T) -> T {
|
|
|
|
|
let subscriber = tracing_subscriber::fmt()
|
|
|
|
|
.with_max_level(tracing::Level::INFO)
|
|
|
|
|
.with_test_writer()
|
|
|
|
|
.finish();
|
|
|
|
|
tracing::subscriber::with_default(subscriber, f)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
2026-04-30 08:16:57 -03:00
|
|
|
#[serial(upstream_media_runtime)]
|
2026-04-29 01:25:06 -03:00
|
|
|
fn shared_playout_epoch_is_reused_across_audio_and_video() {
|
2026-04-30 08:16:57 -03:00
|
|
|
let runtime = runtime_without_offsets();
|
2026-04-29 01:25:06 -03:00
|
|
|
let _camera = runtime.activate_camera();
|
|
|
|
|
let _microphone = runtime.activate_microphone();
|
|
|
|
|
|
|
|
|
|
assert!(matches!(
|
|
|
|
|
runtime.plan_video_pts(1_000_000, 16_666),
|
|
|
|
|
super::UpstreamPlanDecision::AwaitingPair
|
|
|
|
|
));
|
|
|
|
|
let audio_first = play(runtime.plan_audio_pts(1_000_000));
|
|
|
|
|
let video_first = play(runtime.plan_video_pts(1_000_000, 16_666));
|
|
|
|
|
let audio_next = play(runtime.plan_audio_pts(1_010_000));
|
|
|
|
|
|
|
|
|
|
assert_eq!(video_first.local_pts_us, 0);
|
|
|
|
|
assert_eq!(audio_first.local_pts_us, 0);
|
|
|
|
|
assert_eq!(video_first.due_at, audio_first.due_at);
|
|
|
|
|
assert_eq!(
|
|
|
|
|
audio_next
|
|
|
|
|
.due_at
|
|
|
|
|
.saturating_duration_since(audio_first.due_at),
|
|
|
|
|
Duration::from_micros(10_000)
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
2026-04-30 08:16:57 -03:00
|
|
|
#[serial(upstream_media_runtime)]
|
2026-05-01 12:03:07 -03:00
|
|
|
fn pairing_window_holds_one_sided_playout_by_default() {
|
2026-04-29 01:25:06 -03:00
|
|
|
temp_env::with_var("LESAVKA_UPSTREAM_PLAYOUT_DELAY_MS", Some("0"), || {
|
|
|
|
|
let runtime = UpstreamMediaRuntime::new();
|
|
|
|
|
let _camera = runtime.activate_camera();
|
|
|
|
|
|
2026-05-01 12:03:07 -03:00
|
|
|
assert!(matches!(
|
|
|
|
|
runtime.plan_video_pts(1_000_000, 16_666),
|
|
|
|
|
super::UpstreamPlanDecision::AwaitingPair
|
|
|
|
|
));
|
|
|
|
|
assert!(matches!(
|
|
|
|
|
runtime.plan_video_pts(1_016_666, 16_666),
|
|
|
|
|
super::UpstreamPlanDecision::AwaitingPair
|
|
|
|
|
));
|
|
|
|
|
});
|
|
|
|
|
}
|
2026-04-29 01:25:06 -03:00
|
|
|
|
2026-05-01 12:03:07 -03:00
|
|
|
#[test]
|
|
|
|
|
#[serial(upstream_media_runtime)]
|
|
|
|
|
fn explicit_override_allows_one_sided_playout_for_compatibility() {
|
|
|
|
|
temp_env::with_var("LESAVKA_UPSTREAM_REQUIRE_PAIRED_STARTUP", Some("0"), || {
|
|
|
|
|
temp_env::with_var("LESAVKA_UPSTREAM_PLAYOUT_DELAY_MS", Some("0"), || {
|
|
|
|
|
let runtime = UpstreamMediaRuntime::new();
|
|
|
|
|
let _camera = runtime.activate_camera();
|
|
|
|
|
|
|
|
|
|
let first = play(runtime.plan_video_pts(1_000_000, 16_666));
|
|
|
|
|
let second = play(runtime.plan_video_pts(1_016_666, 16_666));
|
|
|
|
|
|
|
|
|
|
assert_eq!(first.local_pts_us, 0);
|
|
|
|
|
assert_eq!(second.local_pts_us, 16_666);
|
|
|
|
|
});
|
2026-04-29 01:25:06 -03:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-01 12:03:07 -03:00
|
|
|
#[test]
|
|
|
|
|
#[serial(upstream_media_runtime)]
|
|
|
|
|
fn overdue_pairing_refreshes_waiting_anchor_before_late_counterpart_arrives() {
|
|
|
|
|
temp_env::with_var(
|
|
|
|
|
"LESAVKA_UPSTREAM_CAMERA_STARTUP_GRACE_MS",
|
|
|
|
|
Some("0"),
|
|
|
|
|
|| {
|
|
|
|
|
temp_env::with_var("LESAVKA_UPSTREAM_PLAYOUT_DELAY_MS", Some("0"), || {
|
|
|
|
|
let runtime = runtime_without_offsets();
|
|
|
|
|
let _camera = runtime.activate_camera();
|
|
|
|
|
let _microphone = runtime.activate_microphone();
|
|
|
|
|
|
|
|
|
|
assert!(matches!(
|
|
|
|
|
runtime.plan_video_pts(1_000_000, 16_666),
|
|
|
|
|
super::UpstreamPlanDecision::AwaitingPair
|
|
|
|
|
));
|
|
|
|
|
assert!(matches!(
|
|
|
|
|
runtime.plan_video_pts(9_000_000, 16_666),
|
|
|
|
|
super::UpstreamPlanDecision::AwaitingPair
|
|
|
|
|
));
|
|
|
|
|
|
|
|
|
|
let audio = play(runtime.plan_audio_pts(9_010_000));
|
|
|
|
|
assert!(matches!(
|
|
|
|
|
runtime.plan_video_pts(9_000_000, 16_666),
|
|
|
|
|
super::UpstreamPlanDecision::DropBeforeOverlap
|
|
|
|
|
));
|
|
|
|
|
let video = play(runtime.plan_video_pts(9_016_666, 16_666));
|
|
|
|
|
|
|
|
|
|
assert_eq!(audio.local_pts_us, 0);
|
|
|
|
|
assert_eq!(video.local_pts_us, 6_666);
|
|
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-29 01:25:06 -03:00
|
|
|
#[test]
|
2026-04-30 08:16:57 -03:00
|
|
|
#[serial(upstream_media_runtime)]
|
2026-04-29 01:25:06 -03:00
|
|
|
fn map_wrappers_hide_unpaired_and_pre_overlap_packets() {
|
|
|
|
|
let runtime = UpstreamMediaRuntime::new();
|
|
|
|
|
let _camera = runtime.activate_camera();
|
|
|
|
|
let _microphone = runtime.activate_microphone();
|
|
|
|
|
|
|
|
|
|
assert_eq!(runtime.map_video_pts(1_000_000, 16_666), None);
|
|
|
|
|
assert_eq!(runtime.map_audio_pts(1_000_000), Some(0));
|
|
|
|
|
assert_eq!(runtime.map_audio_pts(999_999), None);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
2026-04-30 08:16:57 -03:00
|
|
|
#[serial(upstream_media_runtime)]
|
2026-04-29 01:25:06 -03:00
|
|
|
fn shared_playout_trace_path_keeps_planned_pts_stable() {
|
|
|
|
|
temp_env::with_var("LESAVKA_UPSTREAM_TIMING_TRACE", Some("1"), || {
|
|
|
|
|
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
|
|
|
|
|
));
|
|
|
|
|
let audio = play(runtime.plan_audio_pts(1_000_000));
|
|
|
|
|
let video = play(runtime.plan_video_pts(1_000_000, 16_666));
|
|
|
|
|
|
|
|
|
|
assert_eq!(video.local_pts_us, 0);
|
|
|
|
|
assert_eq!(audio.local_pts_us, 0);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
2026-04-30 08:16:57 -03:00
|
|
|
#[serial(upstream_media_runtime)]
|
2026-04-29 01:25:06 -03:00
|
|
|
fn catastrophic_lateness_reanchors_the_shared_playout_epoch() {
|
|
|
|
|
temp_env::with_var("LESAVKA_UPSTREAM_PLAYOUT_DELAY_MS", Some("20"), || {
|
|
|
|
|
temp_env::with_var("LESAVKA_UPSTREAM_REANCHOR_LATE_MS", Some("5"), || {
|
|
|
|
|
let runtime = UpstreamMediaRuntime::new();
|
2026-04-30 08:16:57 -03:00
|
|
|
runtime.set_playout_offsets(0, 0);
|
2026-04-29 01:25:06 -03:00
|
|
|
let _camera = runtime.activate_camera();
|
|
|
|
|
let _microphone = runtime.activate_microphone();
|
|
|
|
|
|
|
|
|
|
assert!(matches!(
|
|
|
|
|
runtime.plan_video_pts(1_000_000, 16_666),
|
|
|
|
|
super::UpstreamPlanDecision::AwaitingPair
|
|
|
|
|
));
|
|
|
|
|
let _audio_first = play(runtime.plan_audio_pts(1_000_000));
|
|
|
|
|
let _video_first = play(runtime.plan_video_pts(1_000_000, 16_666));
|
|
|
|
|
|
|
|
|
|
std::thread::sleep(Duration::from_millis(30));
|
|
|
|
|
|
|
|
|
|
let recovered_audio = play(runtime.plan_audio_pts(1_000_000));
|
|
|
|
|
assert!(
|
|
|
|
|
recovered_audio.due_at > tokio::time::Instant::now(),
|
|
|
|
|
"recovered packet should be scheduled back into the future"
|
|
|
|
|
);
|
|
|
|
|
assert!(
|
|
|
|
|
recovered_audio.late_by <= Duration::from_millis(1),
|
|
|
|
|
"recovered packet should no longer be catastrophically late"
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
let recovered_video = play(runtime.plan_video_pts(1_016_666, 16_666));
|
|
|
|
|
assert!(
|
|
|
|
|
recovered_video.due_at > tokio::time::Instant::now(),
|
|
|
|
|
"shared epoch recovery should also move video back into the future"
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
2026-04-30 08:16:57 -03:00
|
|
|
#[serial(upstream_media_runtime)]
|
2026-04-29 01:25:06 -03:00
|
|
|
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();
|
2026-04-30 08:16:57 -03:00
|
|
|
runtime.set_playout_offsets(0, 0);
|
2026-04-29 01:25:06 -03:00
|
|
|
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]
|
2026-04-30 08:16:57 -03:00
|
|
|
#[serial(upstream_media_runtime)]
|
2026-05-01 19:16:40 -03:00
|
|
|
fn catastrophic_lateness_reanchors_repeatedly_to_preserve_freshness() {
|
2026-04-29 01:25:06 -03:00
|
|
|
temp_env::with_var("LESAVKA_UPSTREAM_PLAYOUT_DELAY_MS", Some("20"), || {
|
|
|
|
|
temp_env::with_var("LESAVKA_UPSTREAM_REANCHOR_LATE_MS", Some("5"), || {
|
|
|
|
|
let runtime = UpstreamMediaRuntime::new();
|
2026-04-30 08:16:57 -03:00
|
|
|
runtime.set_playout_offsets(0, 0);
|
2026-04-29 01:25:06 -03:00
|
|
|
let _camera = runtime.activate_camera();
|
|
|
|
|
let _microphone = runtime.activate_microphone();
|
|
|
|
|
|
|
|
|
|
assert!(matches!(
|
|
|
|
|
runtime.plan_video_pts(1_000_000, 16_666),
|
|
|
|
|
super::UpstreamPlanDecision::AwaitingPair
|
|
|
|
|
));
|
|
|
|
|
let _audio_first = play(runtime.plan_audio_pts(1_000_000));
|
|
|
|
|
let _video_first = play(runtime.plan_video_pts(1_000_000, 16_666));
|
|
|
|
|
|
|
|
|
|
std::thread::sleep(Duration::from_millis(30));
|
|
|
|
|
let first_recovered = play(runtime.plan_audio_pts(1_000_000));
|
|
|
|
|
assert!(first_recovered.due_at > tokio::time::Instant::now());
|
2026-05-01 19:16:40 -03:00
|
|
|
assert!(first_recovered.late_by <= Duration::from_millis(1));
|
2026-04-29 01:25:06 -03:00
|
|
|
|
|
|
|
|
std::thread::sleep(Duration::from_millis(30));
|
2026-05-01 19:16:40 -03:00
|
|
|
let second_recovered = play(runtime.plan_audio_pts(1_000_001));
|
|
|
|
|
assert!(second_recovered.due_at > tokio::time::Instant::now());
|
2026-04-29 01:25:06 -03:00
|
|
|
assert!(
|
2026-05-01 19:16:40 -03:00
|
|
|
second_recovered.late_by <= Duration::from_millis(1),
|
|
|
|
|
"0.17 planner must keep healing instead of preserving stale timing"
|
|
|
|
|
);
|
|
|
|
|
assert!(
|
|
|
|
|
runtime.snapshot().freshness_reanchors >= 2,
|
|
|
|
|
"repeated freshness reanchors should be counted for diagnostics"
|
2026-04-29 01:25:06 -03:00
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
2026-04-30 08:16:57 -03:00
|
|
|
#[serial(upstream_media_runtime)]
|
2026-05-01 19:16:40 -03:00
|
|
|
fn catastrophic_lateness_reanchors_even_after_startup_window() {
|
2026-04-29 01:25:06 -03:00
|
|
|
temp_env::with_var("LESAVKA_UPSTREAM_PLAYOUT_DELAY_MS", Some("20"), || {
|
|
|
|
|
temp_env::with_var("LESAVKA_UPSTREAM_REANCHOR_LATE_MS", Some("5"), || {
|
|
|
|
|
let runtime = UpstreamMediaRuntime::new();
|
2026-04-30 08:16:57 -03:00
|
|
|
runtime.set_playout_offsets(0, 0);
|
2026-04-29 01:25:06 -03:00
|
|
|
let _camera = runtime.activate_camera();
|
|
|
|
|
let _microphone = runtime.activate_microphone();
|
|
|
|
|
|
|
|
|
|
assert!(matches!(
|
|
|
|
|
runtime.plan_video_pts(1_000_000, 16_666),
|
|
|
|
|
super::UpstreamPlanDecision::AwaitingPair
|
|
|
|
|
));
|
|
|
|
|
let _audio_first = play(runtime.plan_audio_pts(1_000_000));
|
|
|
|
|
let _video_first = play(runtime.plan_video_pts(1_000_000, 16_666));
|
|
|
|
|
std::thread::sleep(Duration::from_millis(130));
|
|
|
|
|
|
|
|
|
|
let late_audio = play(runtime.plan_audio_pts(1_100_000));
|
|
|
|
|
assert_eq!(late_audio.local_pts_us, 100_000);
|
|
|
|
|
assert!(
|
2026-05-01 19:16:40 -03:00
|
|
|
late_audio.late_by <= Duration::from_millis(1),
|
|
|
|
|
"0.17 planner should heal mid-session lateness instead of preserving drift"
|
2026-04-29 01:25:06 -03:00
|
|
|
);
|
|
|
|
|
assert!(
|
2026-05-01 19:16:40 -03:00
|
|
|
late_audio.due_at > tokio::time::Instant::now(),
|
|
|
|
|
"mid-session freshness healing should push due_at back into the live budget"
|
2026-04-29 01:25:06 -03:00
|
|
|
);
|
2026-05-01 19:16:40 -03:00
|
|
|
assert!(runtime.snapshot().freshness_reanchors >= 1);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
#[serial(upstream_media_runtime)]
|
|
|
|
|
fn stale_audio_behind_the_freshest_audio_frontier_is_dropped() {
|
|
|
|
|
temp_env::with_var("LESAVKA_UPSTREAM_PLAYOUT_DELAY_MS", Some("0"), || {
|
|
|
|
|
temp_env::with_var("LESAVKA_UPSTREAM_MAX_LIVE_LAG_MS", Some("50"), || {
|
|
|
|
|
let runtime = runtime_without_offsets();
|
|
|
|
|
let _camera = runtime.activate_camera();
|
|
|
|
|
let _microphone = runtime.activate_microphone();
|
|
|
|
|
|
|
|
|
|
assert!(matches!(
|
|
|
|
|
runtime.plan_video_pts(1_000_000, 16_666),
|
|
|
|
|
super::UpstreamPlanDecision::AwaitingPair
|
|
|
|
|
));
|
|
|
|
|
let _audio = play(runtime.plan_audio_pts(1_000_000));
|
|
|
|
|
let _video = play(runtime.plan_video_pts(1_000_000, 16_666));
|
|
|
|
|
let _fresh_audio = play(runtime.plan_audio_pts(2_000_000));
|
|
|
|
|
|
|
|
|
|
assert!(matches!(
|
|
|
|
|
runtime.plan_audio_pts(1_900_000),
|
|
|
|
|
super::UpstreamPlanDecision::DropStale("packet exceeded max live lag")
|
|
|
|
|
));
|
|
|
|
|
assert_eq!(runtime.snapshot().stale_audio_drops, 1);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
#[serial(upstream_media_runtime)]
|
|
|
|
|
fn stale_video_behind_the_freshest_video_frontier_is_dropped() {
|
|
|
|
|
temp_env::with_var("LESAVKA_UPSTREAM_PLAYOUT_DELAY_MS", Some("0"), || {
|
|
|
|
|
temp_env::with_var("LESAVKA_UPSTREAM_MAX_LIVE_LAG_MS", Some("50"), || {
|
|
|
|
|
let runtime = runtime_without_offsets();
|
|
|
|
|
let _camera = runtime.activate_camera();
|
|
|
|
|
let _microphone = runtime.activate_microphone();
|
|
|
|
|
|
|
|
|
|
assert!(matches!(
|
|
|
|
|
runtime.plan_video_pts(1_000_000, 16_666),
|
|
|
|
|
super::UpstreamPlanDecision::AwaitingPair
|
|
|
|
|
));
|
|
|
|
|
let _audio = play(runtime.plan_audio_pts(1_000_000));
|
|
|
|
|
let _video = play(runtime.plan_video_pts(1_000_000, 16_666));
|
|
|
|
|
let _fresh_video = play(runtime.plan_video_pts(2_000_000, 16_666));
|
|
|
|
|
|
|
|
|
|
assert!(matches!(
|
|
|
|
|
runtime.plan_video_pts(1_900_000, 16_666),
|
|
|
|
|
super::UpstreamPlanDecision::DropStale("packet exceeded max live lag")
|
|
|
|
|
));
|
|
|
|
|
let snapshot = runtime.snapshot();
|
|
|
|
|
assert_eq!(snapshot.stale_video_drops, 1);
|
|
|
|
|
assert_eq!(snapshot.video_freezes, 1);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
#[serial(upstream_media_runtime)]
|
|
|
|
|
fn video_too_far_behind_audio_master_is_dropped_and_counted_as_freeze() {
|
|
|
|
|
temp_env::with_var("LESAVKA_UPSTREAM_PLAYOUT_DELAY_MS", Some("0"), || {
|
|
|
|
|
temp_env::with_var("LESAVKA_UPSTREAM_PAIR_SLACK_US", Some("50000"), || {
|
|
|
|
|
let runtime = runtime_without_offsets();
|
|
|
|
|
let _camera = runtime.activate_camera();
|
|
|
|
|
let _microphone = runtime.activate_microphone();
|
|
|
|
|
|
|
|
|
|
assert!(matches!(
|
|
|
|
|
runtime.plan_video_pts(1_000_000, 16_666),
|
|
|
|
|
super::UpstreamPlanDecision::AwaitingPair
|
|
|
|
|
));
|
2026-05-02 11:04:36 -03:00
|
|
|
let audio = play(runtime.plan_audio_pts(1_000_000));
|
2026-05-01 19:16:40 -03:00
|
|
|
let _video = play(runtime.plan_video_pts(1_000_000, 16_666));
|
2026-05-02 11:04:36 -03:00
|
|
|
runtime.mark_audio_presented(audio.local_pts_us);
|
|
|
|
|
let audio_master = play(runtime.plan_audio_pts(1_200_000));
|
|
|
|
|
|
|
|
|
|
assert!(
|
|
|
|
|
matches!(
|
|
|
|
|
runtime.plan_video_pts(1_100_000, 16_666),
|
|
|
|
|
super::UpstreamPlanDecision::Play(_)
|
|
|
|
|
),
|
|
|
|
|
"future planned audio alone must not freeze video before UAC presentation"
|
|
|
|
|
);
|
|
|
|
|
runtime.mark_audio_presented(audio_master.local_pts_us);
|
2026-05-01 19:16:40 -03:00
|
|
|
|
|
|
|
|
assert!(matches!(
|
2026-05-02 11:04:36 -03:00
|
|
|
runtime.plan_video_pts(1_116_666, 16_666),
|
2026-05-01 19:16:40 -03:00
|
|
|
super::UpstreamPlanDecision::DropStale(
|
|
|
|
|
"video frame was too far behind audio master"
|
|
|
|
|
)
|
|
|
|
|
));
|
|
|
|
|
let snapshot = runtime.snapshot();
|
|
|
|
|
assert_eq!(snapshot.skew_video_drops, 1);
|
|
|
|
|
assert_eq!(snapshot.video_freezes, 1);
|
2026-04-29 01:25:06 -03:00
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-02 01:00:57 -03:00
|
|
|
#[test]
|
|
|
|
|
#[serial(upstream_media_runtime)]
|
|
|
|
|
fn configured_video_delay_does_not_make_the_planner_freeze_video() {
|
|
|
|
|
temp_env::with_var("LESAVKA_UPSTREAM_PLAYOUT_DELAY_MS", Some("0"), || {
|
|
|
|
|
temp_env::with_var("LESAVKA_UPSTREAM_PAIR_SLACK_US", Some("50000"), || {
|
|
|
|
|
let runtime = UpstreamMediaRuntime::new();
|
|
|
|
|
runtime.set_playout_offsets(350_000, 0);
|
|
|
|
|
let _camera = runtime.activate_camera();
|
|
|
|
|
let _microphone = runtime.activate_microphone();
|
|
|
|
|
|
|
|
|
|
assert!(matches!(
|
|
|
|
|
runtime.plan_video_pts(1_000_000, 16_666),
|
|
|
|
|
super::UpstreamPlanDecision::AwaitingPair
|
|
|
|
|
));
|
|
|
|
|
let _audio = play(runtime.plan_audio_pts(1_000_000));
|
|
|
|
|
let _video = play(runtime.plan_video_pts(1_000_000, 16_666));
|
|
|
|
|
let _audio_master = play(runtime.plan_audio_pts(1_300_000));
|
|
|
|
|
|
|
|
|
|
let video = play(runtime.plan_video_pts(1_100_000, 16_666));
|
|
|
|
|
assert_eq!(video.local_pts_us, 100_000);
|
|
|
|
|
let snapshot = runtime.snapshot();
|
|
|
|
|
assert_eq!(snapshot.skew_video_drops, 0);
|
|
|
|
|
assert_eq!(snapshot.video_freezes, 0);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-29 01:25:06 -03:00
|
|
|
#[test]
|
2026-04-30 08:16:57 -03:00
|
|
|
#[serial(upstream_media_runtime)]
|
2026-05-01 19:16:40 -03:00
|
|
|
fn paired_startup_times_out_instead_of_waiting_forever() {
|
|
|
|
|
temp_env::with_var("LESAVKA_UPSTREAM_PLAYOUT_DELAY_MS", Some("0"), || {
|
|
|
|
|
temp_env::with_var("LESAVKA_UPSTREAM_STARTUP_TIMEOUT_MS", Some("1"), || {
|
|
|
|
|
let runtime = UpstreamMediaRuntime::new();
|
|
|
|
|
runtime.set_playout_offsets(0, 0);
|
|
|
|
|
let _camera = runtime.activate_camera();
|
|
|
|
|
|
|
|
|
|
assert!(matches!(
|
|
|
|
|
runtime.plan_video_pts(1_000_000, 16_666),
|
|
|
|
|
super::UpstreamPlanDecision::AwaitingPair
|
|
|
|
|
));
|
|
|
|
|
std::thread::sleep(Duration::from_millis(3));
|
|
|
|
|
assert!(matches!(
|
|
|
|
|
runtime.plan_video_pts(1_016_666, 16_666),
|
|
|
|
|
super::UpstreamPlanDecision::StartupFailed(
|
|
|
|
|
"paired upstream startup did not converge before timeout"
|
|
|
|
|
)
|
|
|
|
|
));
|
|
|
|
|
let snapshot = runtime.snapshot();
|
|
|
|
|
assert_eq!(snapshot.phase, "failed");
|
|
|
|
|
assert_eq!(snapshot.startup_timeouts, 1);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
#[serial(upstream_media_runtime)]
|
|
|
|
|
fn planner_snapshot_tracks_presented_playheads_and_skew() {
|
|
|
|
|
let runtime = runtime_without_offsets();
|
|
|
|
|
let _camera = runtime.activate_camera();
|
|
|
|
|
let _microphone = runtime.activate_microphone();
|
|
|
|
|
|
|
|
|
|
assert!(matches!(
|
|
|
|
|
runtime.plan_video_pts(1_000_000, 16_666),
|
|
|
|
|
super::UpstreamPlanDecision::AwaitingPair
|
|
|
|
|
));
|
|
|
|
|
let audio = play(runtime.plan_audio_pts(1_000_000));
|
|
|
|
|
let video = play(runtime.plan_video_pts(1_000_000, 16_666));
|
|
|
|
|
runtime.mark_audio_presented(audio.local_pts_us);
|
|
|
|
|
runtime.mark_video_presented(video.local_pts_us);
|
|
|
|
|
|
|
|
|
|
let snapshot = runtime.snapshot();
|
|
|
|
|
assert_eq!(snapshot.phase, "live");
|
|
|
|
|
assert_eq!(snapshot.last_audio_presented_pts_us, Some(0));
|
|
|
|
|
assert_eq!(snapshot.last_video_presented_pts_us, Some(0));
|
|
|
|
|
assert_eq!(snapshot.planner_skew_ms, Some(0.0));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
#[serial(upstream_media_runtime)]
|
2026-04-29 01:25:06 -03:00
|
|
|
fn default_runtime_covers_video_map_play_path() {
|
|
|
|
|
let runtime = UpstreamMediaRuntime::default();
|
|
|
|
|
let _camera = runtime.activate_camera();
|
|
|
|
|
let _microphone = runtime.activate_microphone();
|
|
|
|
|
|
|
|
|
|
assert!(matches!(
|
|
|
|
|
runtime.plan_video_pts(1_000_000, 16_666),
|
|
|
|
|
super::UpstreamPlanDecision::AwaitingPair
|
|
|
|
|
));
|
|
|
|
|
let _audio = play(runtime.plan_audio_pts(1_000_000));
|
|
|
|
|
assert_eq!(runtime.map_video_pts(1_000_000, 16_666), Some(0));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[tokio::test(flavor = "current_thread")]
|
2026-04-30 08:16:57 -03:00
|
|
|
#[serial(upstream_media_runtime)]
|
2026-04-29 01:25:06 -03:00
|
|
|
async fn wait_for_audio_master_returns_false_immediately_once_due_time_has_already_passed() {
|
|
|
|
|
let runtime = UpstreamMediaRuntime::new();
|
|
|
|
|
let _camera = runtime.activate_camera();
|
|
|
|
|
let _microphone = runtime.activate_microphone();
|
|
|
|
|
|
2026-04-30 08:16:57 -03:00
|
|
|
assert!(
|
|
|
|
|
!runtime
|
|
|
|
|
.wait_for_audio_master(
|
|
|
|
|
123_456,
|
|
|
|
|
tokio::time::Instant::now()
|
|
|
|
|
.checked_sub(Duration::from_millis(1))
|
|
|
|
|
.unwrap_or_else(tokio::time::Instant::now),
|
|
|
|
|
)
|
|
|
|
|
.await
|
|
|
|
|
);
|
2026-04-29 01:25:06 -03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
2026-04-30 08:16:57 -03:00
|
|
|
#[serial(upstream_media_runtime)]
|
2026-04-29 01:25:06 -03:00
|
|
|
fn timing_trace_paths_emit_overlap_and_dropbeforeoverlap_details() {
|
|
|
|
|
temp_env::with_var("LESAVKA_UPSTREAM_TIMING_TRACE", Some("1"), || {
|
|
|
|
|
with_info_tracing(|| {
|
|
|
|
|
let runtime = UpstreamMediaRuntime::new();
|
|
|
|
|
let _camera = runtime.activate_camera();
|
|
|
|
|
let _microphone = runtime.activate_microphone();
|
|
|
|
|
|
|
|
|
|
assert!(matches!(
|
|
|
|
|
runtime.plan_video_pts(1_300_000, 16_666),
|
|
|
|
|
super::UpstreamPlanDecision::AwaitingPair
|
|
|
|
|
));
|
|
|
|
|
assert!(matches!(
|
|
|
|
|
runtime.plan_audio_pts(1_000_000),
|
|
|
|
|
super::UpstreamPlanDecision::DropBeforeOverlap
|
|
|
|
|
));
|
|
|
|
|
let _video = play(runtime.plan_video_pts(1_300_000, 16_666));
|
|
|
|
|
assert!(matches!(
|
|
|
|
|
runtime.plan_audio_pts(1_000_000),
|
|
|
|
|
super::UpstreamPlanDecision::DropBeforeOverlap
|
|
|
|
|
));
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
}
|