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}