Skip to main content

media_pp/core/
playback_clock.rs

1//! Which stream currently defines the pipeline's media position.
2//!
3//! [`Clock`](crate::clock::Clock) stays the monotonic control and pause clock.
4//! This module adds the *media* position on top of it, and the handover it
5//! exists for: a pipeline starts on a wall-clock fallback and can pass the
6//! position to one audio renderer once that renderer's endpoint is running,
7//! without ever letting the position jump backwards. [`PlaybackMaster`] is
8//! the state that handover is currently in.
9//!
10//! Only one audio master may hold the clock at a time, which is what
11//! [`PlaybackClockError`] guards. No audio-backend type appears here: a
12//! renderer publishes device-position snapshots through a private
13//! registration, and video scheduling only ever reads the result.
14
15use std::{
16    sync::{Arc, Mutex},
17    time::Duration,
18};
19
20use thiserror::Error as ThisError;
21
22use crate::clock::Clock;
23
24/// Which source currently defines the pipeline's media position.
25#[derive(Debug, Clone, Copy, PartialEq, Eq)]
26pub enum PlaybackMaster {
27    /// No timestamped stream has established a position yet.
28    Unavailable,
29    /// Media position advances from the pipeline's pause-aware wall clock.
30    Wall,
31    /// An audio renderer owns the clock but has not started the endpoint yet.
32    AudioPriming,
33    /// An audio endpoint's played-sample position is the master clock.
34    Audio,
35}
36
37#[derive(Debug, Clone, Copy, PartialEq, Eq, ThisError)]
38/// Why an audio renderer could not take or keep the playback clock.
39///
40/// Both variants mean the caller's registration is not the live one — either
41/// another master already holds the clock, or this registration was superseded.
42/// Neither is fatal to playback: the position simply stays with whoever owns it.
43pub enum PlaybackClockError {
44    /// Another live audio renderer already owns the playback clock.
45    #[error("this pipeline already has an audio playback-clock master")]
46    AudioMasterAlreadyRegistered,
47
48    /// The registration was released or superseded before this operation.
49    #[error("the audio playback-clock registration is stale")]
50    StaleAudioMaster,
51}
52
53/// Pipeline-wide media clock shared by audio output and video scheduling.
54///
55/// [`Clock`] remains the pipeline's monotonic control/pause clock. This
56/// type adds the media-timeline position and can hand that position from a
57/// wall-clock fallback to one audio renderer without letting the position
58/// jump backwards. It deliberately contains no WASAPI types: an audio
59/// backend publishes device-position snapshots through its private
60/// registration, while video scheduling only reads the resulting position.
61pub struct PlaybackClock {
62    wall_clock: Arc<Clock>,
63    state: Mutex<State>,
64}
65
66#[derive(Clone, Copy)]
67enum State {
68    Unavailable {
69        next_registration: u64,
70    },
71    Wall {
72        anchor_ns: i64,
73        anchor_elapsed: Duration,
74        next_registration: u64,
75    },
76    AudioPriming {
77        registration: u64,
78        held_ns: Option<i64>,
79        next_registration: u64,
80    },
81    // Only an audio renderer moves the clock into these two, and the only one
82    // in this crate is behind `wasapi-renderer`. They are dead in a build
83    // without it, but they are the timeline contract `PlaybackClock` exists to
84    // provide — gating them on a backend feature would invert that. See
85    // `AudioMasterRegistration`.
86    #[allow(dead_code)]
87    Audio {
88        registration: u64,
89        position_ns: i64,
90        sampled_elapsed: Duration,
91        submitted_until_ns: i64,
92        running: bool,
93        next_registration: u64,
94    },
95    #[allow(dead_code)]
96    AudioFallback {
97        registration: u64,
98        anchor_ns: i64,
99        anchor_elapsed: Duration,
100        next_registration: u64,
101    },
102}
103
104impl PlaybackClock {
105    pub(crate) fn new(wall_clock: Arc<Clock>) -> Self {
106        Self {
107            wall_clock,
108            state: Mutex::new(State::Unavailable {
109                next_registration: 1,
110            }),
111        }
112    }
113
114    /// Reports which timing source currently defines media position.
115    ///
116    /// This is a lock-protected snapshot; ownership may change immediately
117    /// afterward as an audio renderer starts or stops.
118    pub fn master(&self) -> PlaybackMaster {
119        match *self.state.lock().unwrap() {
120            State::Unavailable { .. } => PlaybackMaster::Unavailable,
121            State::Wall { .. } | State::AudioFallback { .. } => PlaybackMaster::Wall,
122            State::AudioPriming { .. } => PlaybackMaster::AudioPriming,
123            State::Audio { .. } => PlaybackMaster::Audio,
124        }
125    }
126
127    #[cfg(test)]
128    pub(crate) fn position_ns(&self) -> Option<i64> {
129        let state = self.state.lock().unwrap();
130        position_at(*state, self.wall_clock.elapsed())
131    }
132
133    pub(crate) fn interrupt_epoch(&self) -> u64 {
134        self.wall_clock.interrupt_epoch()
135    }
136
137    /// Establishes a wall-clock media origin if no stream owns one yet.
138    /// Returns the current position after doing so.
139    #[cfg(test)]
140    pub(crate) fn ensure_wall_origin(&self, media_ns: i64) -> Option<i64> {
141        let mut state = self.state.lock().unwrap();
142        if let State::Unavailable { next_registration } = *state {
143            self.wall_clock.start();
144            let elapsed = self.wall_clock.elapsed();
145            *state = State::Wall {
146                anchor_ns: media_ns,
147                anchor_elapsed: elapsed,
148                next_registration,
149            };
150        }
151        position_at(*state, self.wall_clock.elapsed())
152    }
153
154    pub(crate) fn video_snapshot(&self, media_ns: i64) -> (PlaybackMaster, Option<i64>) {
155        let mut state = self.state.lock().unwrap();
156        if let State::Unavailable { next_registration } = *state {
157            self.wall_clock.start();
158            let elapsed = self.wall_clock.elapsed();
159            *state = State::Wall {
160                anchor_ns: media_ns,
161                anchor_elapsed: elapsed,
162                next_registration,
163            };
164        }
165        let elapsed = self.wall_clock.elapsed();
166        let master = match *state {
167            State::Unavailable { .. } => PlaybackMaster::Unavailable,
168            State::Wall { .. } | State::AudioFallback { .. } => PlaybackMaster::Wall,
169            State::AudioPriming { .. } => PlaybackMaster::AudioPriming,
170            State::Audio { .. } => PlaybackMaster::Audio,
171        };
172        (master, position_at(*state, elapsed))
173    }
174
175    /// Claims the timeline for one audio renderer. Unused in a build without
176    /// an audio renderer (see `AudioMasterRegistration`), hence the `allow`.
177    #[allow(dead_code)]
178    pub(crate) fn register_audio_master(
179        self: &Arc<Self>,
180    ) -> Result<AudioMasterRegistration, PlaybackClockError> {
181        let mut state = self.state.lock().unwrap();
182        let elapsed = self.wall_clock.elapsed();
183        let (held_ns, registration, next_registration) = match *state {
184            State::Unavailable { next_registration } => {
185                (None, next_registration, next_registration.wrapping_add(1))
186            }
187            State::Wall {
188                next_registration, ..
189            } => (
190                position_at(*state, elapsed),
191                next_registration,
192                next_registration.wrapping_add(1),
193            ),
194            State::AudioPriming { .. } | State::Audio { .. } | State::AudioFallback { .. } => {
195                return Err(PlaybackClockError::AudioMasterAlreadyRegistered);
196            }
197        };
198        *state = State::AudioPriming {
199            registration,
200            held_ns,
201            next_registration,
202        };
203        Ok(AudioMasterRegistration {
204            clock: self.clone(),
205            registration,
206        })
207    }
208
209    /// Resets media state for a seek while retaining the current audio
210    /// renderer's ownership. The next timestamp/device sample establishes
211    /// the post-seek position.
212    pub(crate) fn reset_for_seek(&self) {
213        let mut state = self.state.lock().unwrap();
214        *state = match *state {
215            State::Unavailable { next_registration }
216            | State::Wall {
217                next_registration, ..
218            } => State::Unavailable { next_registration },
219            State::AudioPriming {
220                registration,
221                next_registration,
222                ..
223            }
224            | State::Audio {
225                registration,
226                next_registration,
227                ..
228            }
229            | State::AudioFallback {
230                registration,
231                next_registration,
232                ..
233            } => State::AudioPriming {
234                registration,
235                held_ns: None,
236                next_registration,
237            },
238        };
239    }
240
241    #[allow(dead_code)]
242    fn release_audio_master(&self, registration: u64) {
243        let mut state = self.state.lock().unwrap();
244        let elapsed = self.wall_clock.elapsed();
245        let (matches, next_registration) = match *state {
246            State::AudioPriming {
247                registration: current,
248                next_registration,
249                ..
250            }
251            | State::Audio {
252                registration: current,
253                next_registration,
254                ..
255            }
256            | State::AudioFallback {
257                registration: current,
258                next_registration,
259                ..
260            } => (current == registration, next_registration),
261            State::Unavailable { .. } | State::Wall { .. } => return,
262        };
263        if !matches {
264            return;
265        }
266        *state = match position_at(*state, elapsed) {
267            Some(anchor_ns) => State::Wall {
268                anchor_ns,
269                anchor_elapsed: elapsed,
270                next_registration,
271            },
272            None => State::Unavailable { next_registration },
273        };
274    }
275}
276
277/// Exclusive, generation-checked writer owned by one audio renderer.
278/// Dropping it hands the last known position back to the wall clock.
279///
280/// The only audio renderer in this crate is `WasapiRenderer`, behind the
281/// `wasapi-renderer` feature, so a build without it constructs this nowhere and
282/// every method below is dead. That is why the `allow`s here are deliberate
283/// rather than a `cfg(feature = "wasapi-renderer")` gate: `PlaybackClock` is
284/// the backend-independent timeline every renderer binds to, and teaching it
285/// about one backend's Cargo feature would invert that relationship. The
286/// crate's own tests exercise this path, so it is covered even when no shipped
287/// element uses it.
288#[allow(dead_code)]
289pub(crate) struct AudioMasterRegistration {
290    clock: Arc<PlaybackClock>,
291    registration: u64,
292}
293
294#[allow(dead_code)]
295impl AudioMasterRegistration {
296    pub(crate) fn priming_target_ns(&self) -> Result<Option<i64>, PlaybackClockError> {
297        match *self.clock.state.lock().unwrap() {
298            State::AudioPriming {
299                registration,
300                held_ns,
301                ..
302            } if registration == self.registration => Ok(held_ns),
303            State::Audio { registration, .. } if registration == self.registration => Ok(None),
304            State::AudioFallback { registration, .. } if registration == self.registration => {
305                Ok(None)
306            }
307            _ => Err(PlaybackClockError::StaleAudioMaster),
308        }
309    }
310
311    pub(crate) fn publish(
312        &self,
313        position_ns: i64,
314        submitted_until_ns: i64,
315        running: bool,
316    ) -> Result<(), PlaybackClockError> {
317        let mut state = self.clock.state.lock().unwrap();
318        self.clock.wall_clock.start();
319        let elapsed = self.clock.wall_clock.elapsed();
320        let (held_ns, next_registration) = match *state {
321            State::AudioPriming {
322                registration,
323                held_ns,
324                next_registration,
325            } if registration == self.registration => (held_ns, next_registration),
326            State::Audio {
327                registration,
328                next_registration,
329                ..
330            } if registration == self.registration => (None, next_registration),
331            State::AudioFallback {
332                registration,
333                next_registration,
334                ..
335            } if registration == self.registration => (None, next_registration),
336            _ => return Err(PlaybackClockError::StaleAudioMaster),
337        };
338
339        // A master handoff must never make video scheduling move backwards.
340        let position_ns = held_ns.map_or(position_ns, |held| position_ns.max(held));
341        let submitted_until_ns = submitted_until_ns.max(position_ns);
342        *state = State::Audio {
343            registration: self.registration,
344            position_ns,
345            sampled_elapsed: elapsed,
346            submitted_until_ns,
347            running,
348            next_registration,
349        };
350        Ok(())
351    }
352
353    /// Audio ended before another stream: continue from its final played
354    /// position using the wall clock while retaining this registration so
355    /// a second renderer cannot race the still-attached one.
356    pub(crate) fn finish(&self, position_ns: i64) -> Result<(), PlaybackClockError> {
357        let mut state = self.clock.state.lock().unwrap();
358        let elapsed = self.clock.wall_clock.elapsed();
359        let next_registration = match *state {
360            State::AudioPriming {
361                registration,
362                next_registration,
363                ..
364            }
365            | State::Audio {
366                registration,
367                next_registration,
368                ..
369            } if registration == self.registration => next_registration,
370            _ => return Err(PlaybackClockError::StaleAudioMaster),
371        };
372        *state = State::AudioFallback {
373            registration: self.registration,
374            anchor_ns: position_ns,
375            anchor_elapsed: elapsed,
376            next_registration,
377        };
378        Ok(())
379    }
380
381    pub(crate) fn reset_for_seek(&self) -> Result<(), PlaybackClockError> {
382        let mut state = self.clock.state.lock().unwrap();
383        let next_registration = match *state {
384            State::AudioPriming {
385                registration,
386                next_registration,
387                ..
388            }
389            | State::Audio {
390                registration,
391                next_registration,
392                ..
393            }
394            | State::AudioFallback {
395                registration,
396                next_registration,
397                ..
398            } if registration == self.registration => next_registration,
399            _ => return Err(PlaybackClockError::StaleAudioMaster),
400        };
401        *state = State::AudioPriming {
402            registration: self.registration,
403            held_ns: None,
404            next_registration,
405        };
406        Ok(())
407    }
408}
409
410impl Drop for AudioMasterRegistration {
411    fn drop(&mut self) {
412        self.clock.release_audio_master(self.registration);
413    }
414}
415
416fn position_at(state: State, elapsed: Duration) -> Option<i64> {
417    match state {
418        State::Unavailable { .. } => None,
419        State::Wall {
420            anchor_ns,
421            anchor_elapsed,
422            ..
423        } => Some(add_duration(
424            anchor_ns,
425            elapsed.saturating_sub(anchor_elapsed),
426        )),
427        State::AudioPriming { held_ns, .. } => held_ns,
428        State::Audio {
429            position_ns,
430            sampled_elapsed,
431            submitted_until_ns,
432            running,
433            ..
434        } => {
435            let projected = if running {
436                add_duration(position_ns, elapsed.saturating_sub(sampled_elapsed))
437            } else {
438                position_ns
439            };
440            Some(projected.min(submitted_until_ns))
441        }
442        State::AudioFallback {
443            anchor_ns,
444            anchor_elapsed,
445            ..
446        } => Some(add_duration(
447            anchor_ns,
448            elapsed.saturating_sub(anchor_elapsed),
449        )),
450    }
451}
452
453fn add_duration(value_ns: i64, duration: Duration) -> i64 {
454    let delta = duration.as_nanos().min(i64::MAX as u128) as i64;
455    value_ns.saturating_add(delta)
456}
457
458#[cfg(test)]
459mod tests {
460    use std::{thread, time::Duration};
461
462    use super::*;
463
464    #[test]
465    fn wall_origin_advances_and_freezes_with_pipeline_clock() {
466        let wall = Arc::new(Clock::new());
467        let playback = PlaybackClock::new(wall.clone());
468        assert!(playback.ensure_wall_origin(1_000).unwrap() >= 1_000);
469        thread::sleep(Duration::from_millis(20));
470        assert!(playback.position_ns().unwrap() >= 10_000_000);
471
472        wall.pause();
473        let paused = playback.position_ns().unwrap();
474        thread::sleep(Duration::from_millis(20));
475        assert_eq!(playback.position_ns(), Some(paused));
476    }
477
478    #[test]
479    fn audio_handoff_never_moves_backwards_and_release_continues_on_wall() {
480        let wall = Arc::new(Clock::new());
481        let playback = Arc::new(PlaybackClock::new(wall));
482        playback.ensure_wall_origin(50_000_000);
483        let audio = playback.register_audio_master().unwrap();
484        let held = audio.priming_target_ns().unwrap().unwrap();
485
486        audio
487            .publish(held - 10_000_000, held + 100_000_000, true)
488            .unwrap();
489        assert!(playback.position_ns().unwrap() >= held);
490        drop(audio);
491        let released = playback.position_ns().unwrap();
492        thread::sleep(Duration::from_millis(10));
493        assert!(playback.position_ns().unwrap() >= released);
494        assert_eq!(playback.master(), PlaybackMaster::Wall);
495    }
496
497    #[test]
498    fn only_one_audio_master_can_publish_and_seek_retains_its_generation() {
499        let wall = Arc::new(Clock::new());
500        let playback = Arc::new(PlaybackClock::new(wall));
501        let audio = playback.register_audio_master().unwrap();
502        assert!(matches!(
503            playback.register_audio_master(),
504            Err(PlaybackClockError::AudioMasterAlreadyRegistered)
505        ));
506
507        playback.reset_for_seek();
508        audio.publish(2_000, 3_000, true).unwrap();
509        assert_eq!(playback.master(), PlaybackMaster::Audio);
510    }
511
512    #[test]
513    fn audio_projection_is_capped_at_submitted_media() {
514        let wall = Arc::new(Clock::new());
515        let playback = Arc::new(PlaybackClock::new(wall));
516        let audio = playback.register_audio_master().unwrap();
517        audio.publish(10, 1_000_000, true).unwrap();
518        thread::sleep(Duration::from_millis(5));
519        assert_eq!(playback.position_ns(), Some(1_000_000));
520    }
521
522    #[test]
523    fn finished_audio_continues_on_wall_and_can_reset_for_seek() {
524        let wall = Arc::new(Clock::new());
525        let playback = Arc::new(PlaybackClock::new(wall));
526        let audio = playback.register_audio_master().unwrap();
527        audio.publish(1_000, 2_000, false).unwrap();
528        audio.finish(2_000).unwrap();
529        assert_eq!(playback.master(), PlaybackMaster::Wall);
530        thread::sleep(Duration::from_millis(5));
531        assert!(playback.position_ns().unwrap() > 2_000);
532
533        audio.reset_for_seek().unwrap();
534        assert_eq!(playback.master(), PlaybackMaster::AudioPriming);
535        assert_eq!(playback.position_ns(), None);
536    }
537}