Skip to main content

media_pp/core/
clock.rs

1//! The pipeline's shared, pause-aware wall-clock reference.
2//!
3//! One [`Clock`] per [`Pipeline`](crate::pipeline::Pipeline), shared with
4//! every [`Pacer`](crate::elements::Pacer) at wiring time. Whichever branch
5//! processes a frame first anchors t=0 and the others read that same anchor,
6//! which is what keeps video and audio from each drifting away from their own
7//! first frame.
8
9use std::{
10    sync::{
11        Mutex,
12        atomic::{AtomicU64, Ordering},
13    },
14    time::{Duration, Instant},
15};
16
17/// Shared wall-clock reference for pacing decoded frames to their
18/// presentation time. Whichever branch (video, audio, ...) processes a
19/// frame first sets the anchor; every other branch reads the same one, so
20/// they agree on t=0 instead of each drifting from its own first frame.
21///
22/// Owned by [`crate::pipeline::Pipeline`] (one per pipeline, shared with
23/// every [`crate::elements::Pacer`] via the `wire` closure) so
24/// `Pipeline::pause`/`resume` can keep it in sync with the rest of the
25/// pipeline — see those for why a `Pacer`, mid-playback, needs this to be
26/// pause-aware and not just a fixed anchor.
27pub struct Clock {
28    state: Mutex<State>,
29    /// Incremented before a control request starts cascading through the
30    /// pipeline. A `Pacer` compares this with the last generation it
31    /// acknowledged in `control()` so a long presentation-time wait can
32    /// return promptly and let the owning worker process that request.
33    interrupt_epoch: AtomicU64,
34}
35
36#[derive(Clone, Copy)]
37enum State {
38    /// Never started — `start()` anchors to *now* on first call.
39    Unset,
40    Running {
41        start: Instant,
42    },
43    Paused {
44        start: Instant,
45        paused_at: Instant,
46    },
47}
48
49impl Default for Clock {
50    fn default() -> Self {
51        Self::new()
52    }
53}
54
55impl Clock {
56    /// Creates an unstarted clock whose first [`Self::start`] call establishes
57    /// the shared playback anchor.
58    pub fn new() -> Self {
59        Self {
60            state: Mutex::new(State::Unset),
61            interrupt_epoch: AtomicU64::new(0),
62        }
63    }
64
65    /// Signals paced waits to return without changing the clock's playback
66    /// anchor. The actual pause/seek/stop state change still happens through
67    /// the ordinary synchronous control cascade.
68    pub(crate) fn interrupt(&self) {
69        self.interrupt_epoch.fetch_add(1, Ordering::Release);
70    }
71
72    pub(crate) fn interrupt_epoch(&self) -> u64 {
73        self.interrupt_epoch.load(Ordering::Acquire)
74    }
75
76    /// The instant playback started, set on first call — shifted forward
77    /// on every [`Clock::resume`] by however long the clock spent paused,
78    /// so `now - start()` stays continuous across a pause/resume cycle
79    /// instead of jumping by the pause's real duration. Callers that pace
80    /// against this (see `Pacer::wait_for`) need to
81    /// call it fresh each time, not cache the first result — the whole
82    /// point is that it can move.
83    pub fn start(&self) -> Instant {
84        let mut state = self.state.lock().unwrap();
85        match *state {
86            State::Unset => {
87                let now = Instant::now();
88                *state = State::Running { start: now };
89                now
90            }
91            State::Running { start } => start,
92            State::Paused { start, .. } => start,
93        }
94    }
95
96    /// Pause-aware time elapsed since this clock was first anchored.
97    pub(crate) fn elapsed(&self) -> Duration {
98        let state = self.state.lock().unwrap();
99        match *state {
100            State::Unset => Duration::ZERO,
101            State::Running { start } => Instant::now().saturating_duration_since(start),
102            State::Paused { start, paused_at } => paused_at.saturating_duration_since(start),
103        }
104    }
105
106    /// Freezes the clock in place. No-op if unset (nothing running yet)
107    /// or already paused.
108    pub fn pause(&self) {
109        let mut state = self.state.lock().unwrap();
110        if let State::Running { start } = *state {
111            *state = State::Paused {
112                start,
113                paused_at: Instant::now(),
114            };
115        }
116    }
117
118    /// Undoes [`Clock::pause`] by shifting `start` forward by however long
119    /// this pause lasted. No-op if not currently paused.
120    pub fn resume(&self) {
121        let mut state = self.state.lock().unwrap();
122        if let State::Paused { start, paused_at } = *state {
123            let shift = Instant::now().saturating_duration_since(paused_at);
124            *state = State::Running {
125                start: start + shift,
126            };
127        }
128    }
129
130    /// Back to the same "never started" state as a freshly constructed
131    /// `Clock` — the next [`Clock::start`] call re-anchors t=0 to
132    /// *that* moment, same lazy-first-caller-wins semantics as initial
133    /// startup (see the type docs). Unconditional, regardless of current
134    /// state.
135    ///
136    /// Called on [`crate::control::ControlMsg::Seek`]
137    /// (see [`crate::pipeline::Pipeline::seek`]): the old anchor measured
138    /// real time elapsed *for the pre-seek position* — after a jump, a
139    /// `Pacer`'s `elapsed_secs` (relative to its own now-reset
140    /// `first_pts`) starts over from ~0 too, so pairing it with the
141    /// stale anchor would compute a `due` far in the past and skip
142    /// sleeping entirely, dumping every post-seek frame with no pacing.
143    /// This is the wall-clock half of that same fix — `Pacer::first_pts`
144    /// resetting is the pts half; both are needed together.
145    pub fn reset(&self) {
146        let mut state = self.state.lock().unwrap();
147        *state = State::Unset;
148    }
149}
150
151#[cfg(test)]
152mod tests {
153    use std::time::Duration;
154
155    use super::*;
156
157    #[test]
158    fn pause_shifts_start_forward_by_the_pause_duration() {
159        let clock = Clock::new();
160        let first = clock.start();
161
162        clock.pause();
163        std::thread::sleep(Duration::from_millis(30));
164        clock.resume();
165
166        let after = clock.start();
167        assert!(
168            after >= first + Duration::from_millis(20),
169            "expected start() to shift forward by roughly the pause duration"
170        );
171    }
172
173    /// Regression test for the bug found manually testing `seek_render`:
174    /// without `reset()`, a `Pacer` re-anchoring only its `first_pts` (not
175    /// the shared `Clock`) after a seek computed `due` times far in the
176    /// past — `start()` kept returning the *original* anchor no matter
177    /// how long ago that was — so every post-seek frame skipped its sleep
178    /// entirely. `reset()` must make the next `start()` anchor to a fresh
179    /// "now", not the original one.
180    #[test]
181    fn reset_makes_the_next_start_anchor_to_a_fresh_now() {
182        let clock = Clock::new();
183        let original = clock.start();
184
185        std::thread::sleep(Duration::from_millis(30));
186        clock.reset();
187        let after_reset = clock.start();
188
189        assert!(
190            after_reset >= original + Duration::from_millis(20),
191            "expected start() after reset() to anchor to a fresh instant, \
192             not keep returning the original one"
193        );
194    }
195}