Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions quest/m1/push-to-talk-resume-latency.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# [S] Back-to-back push-to-talk presses keep their latency

## Goal

A press that starts while the previous press is still playing continues on the old schedule, so `just test push-to-talk`'s delay stays flat across back-to-back presses instead of growing by about 250 ms per press.

## Plan

Today each resume in `rs/moq-mux/src/sync.rs` (`Track::plan`/`resume`) starts one delay after the old tail drains, even when no gap separates the presses. The added hold (`held`) only shrinks at an eviction or a long mute.

Recommendation: when the new segment's first media is due no later than the old tail ends, plus one frame, map it onto the old schedule with no added hold. Re-buffer one delay only after a real gap, where the sink would underrun anyway. Mirror the same rule in `js/watch` if its audio sync has the same resume. Make `just test push-to-talk` fail when the delay grows across back-to-back presses.

Public API: none expected. Wire: none.

## Related

- [Gapless audio switch](/quest/m3/gapless-audio-switch.md) - the same schedule continuity across a track change
27 changes: 27 additions & 0 deletions rs/moq-mux/src/sync.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
Expand Down Expand Up @@ -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();
Expand Down