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}