From ccf28b0ea315f422b588c371ad3f8ee86937147f Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Wed, 11 Nov 2026 10:15:21 -0800 Subject: [PATCH] fix(mux): a reset track's arrivals drop the hold behind the old tail A replacement segment held behind the old tail adds that hold to each arrival, so it does not drag the schedule ahead. A reset (a hole too large to fill, or a rewind) kept the hold, so the clock re-anchored that much late and moq play waited out media that was already live, draining the sink. Back-to-back push-to-talk presses accumulate a hold, so after the first eviction the native player stalled repeatedly and lost utterance heads. Co-Authored-By: Claude Opus 5.5 --- rs/moq-mux/src/sync.rs | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/rs/moq-mux/src/sync.rs b/rs/moq-mux/src/sync.rs index 3994bad899..48c5de2996 100644 --- a/rs/moq-mux/src/sync.rs +++ b/rs/moq-mux/src/sync.rs @@ -347,7 +347,12 @@ impl Track { /// End this track's timeline at a discontinuity, such as lost media or a /// rewind: its arrivals stop holding the clock until the next one. + /// + /// Media after the break was not held behind any tail, so its arrivals are + /// the live edge again. Keeping the hold would anchor the clock that much + /// late and make the media wait for it. pub fn reset(&self) { + self.mapping.lock().unwrap().held = Duration::ZERO; if self.clock.state.lock().unwrap().reset(self.role) { self.clock.wake(); } @@ -1083,6 +1088,28 @@ mod tests { assert_eq!(clock.state.lock().unwrap().due(ms(10_500)), unchanged); } + /// Media after a reset was not held behind the tail, so it plays one delay + /// after it arrives rather than waiting out the old hold too. + #[tokio::test] + async fn a_reset_drops_the_hold_behind_a_tail() { + tokio::time::pause(); + let start = kio::clock::now(); + let clock = Arc::new(Clock::new(DELAY)); + let audio = Track::new(clock.clone(), Kind::Audio); + audio.received(ms(10_000), start); + let transition = audio.declare(); + audio.end(ms(10_100)).unwrap(); + audio.plan(transition, ms(0), Some(DELAY)); + let mapping = audio.resume(transition, ms(0), Some(DELAY)).ready().await.unwrap(); + audio.apply(transition, mapping); + + audio.reset(); + let now = kio::clock::now(); + let next = audio.map(ms(1_000)).unwrap(); + audio.received(next, now); + assert_eq!(clock.state.lock().unwrap().due(next), now.checked_add(DELAY)); + } + #[tokio::test] async fn a_late_paired_replacement_keeps_its_media_offset() { tokio::time::pause();