Skip to main content

media_pp\core/
clock.rs

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