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    /// How long from now until `media_ns` is its turn.
155    ///
156    /// The other direction of what [`PlaybackClock::video_snapshot`] reads:
157    /// that answers *what media time is it*, this answers *when is this media
158    /// time*. Both are the same relation between the media timeline and this
159    /// pipeline's own, and keeping them in one place is what stops a paced
160    /// element and a synchronized one from scheduling the same picture
161    /// against two authorities that drift apart.
162    ///
163    /// A `Duration` rather than an `Instant`, because under an audio master
164    /// there is no wall-clock moment to name: the position advances at the
165    /// device's own rate, so the honest answer is how much is left *as of
166    /// now*. A caller sleeps some of it and asks again, which is what
167    /// [`crate::elements::Pacer`] already did with its own arithmetic.
168    ///
169    /// Establishes the origin from the first caller to ask, exactly as
170    /// `video_snapshot` does — whichever of the two arrives first speaks for
171    /// the pipeline, which is the point: a container's streams do not start
172    /// at the same timestamp, and that offset is part of their sync.
173    ///
174    /// `Duration::ZERO` while an audio master is priming and has said
175    /// nothing about where it is. Holding a caller there would be waiting on
176    /// audio that has not started, which is the stall a renderer's deferred
177    /// registration exists to avoid.
178    pub(crate) fn remaining(&self, media_ns: i64) -> Duration {
179        let mut state = self.state.lock().unwrap();
180        if let State::Unavailable { next_registration } = *state {
181            self.wall_clock.start();
182            let elapsed = self.wall_clock.elapsed();
183            *state = State::Wall {
184                anchor_ns: media_ns,
185                anchor_elapsed: elapsed,
186                next_registration,
187            };
188        }
189        let Some(position) = position_at(*state, self.wall_clock.elapsed()) else {
190            return Duration::ZERO;
191        };
192        let ahead = media_ns.saturating_sub(position);
193        if ahead <= 0 {
194            return Duration::ZERO;
195        }
196        Duration::from_nanos(ahead as u64)
197    }
198
199    /// Moves the origin so that `media_ns` is due now.
200    ///
201    /// For a sender whose timeline restarts under the pipeline — a camera
202    /// rebooting, an RTP timestamp base that wraps — where the timestamps
203    /// that follow have no relation to the ones before them. Paced literally
204    /// such a jump is a still picture for as long as it claims to be worth.
205    ///
206    /// A no-op while an audio master owns the position: what it is playing is
207    /// what the pipeline is at, and a stream that jumped is the stream's
208    /// problem to reconcile, not the clock's.
209    pub(crate) fn re_anchor(&self, media_ns: i64) {
210        let mut state = self.state.lock().unwrap();
211        let next_registration = match *state {
212            State::Unavailable {
213                next_registration, ..
214            }
215            | State::Wall {
216                next_registration, ..
217            } => next_registration,
218            State::AudioPriming { .. } | State::Audio { .. } | State::AudioFallback { .. } => {
219                return;
220            }
221        };
222        self.wall_clock.start();
223        *state = State::Wall {
224            anchor_ns: media_ns,
225            anchor_elapsed: self.wall_clock.elapsed(),
226            next_registration,
227        };
228    }
229
230    pub(crate) fn video_snapshot(&self, media_ns: i64) -> (PlaybackMaster, Option<i64>) {
231        let mut state = self.state.lock().unwrap();
232        if let State::Unavailable { next_registration } = *state {
233            self.wall_clock.start();
234            let elapsed = self.wall_clock.elapsed();
235            *state = State::Wall {
236                anchor_ns: media_ns,
237                anchor_elapsed: elapsed,
238                next_registration,
239            };
240        }
241        let elapsed = self.wall_clock.elapsed();
242        let master = match *state {
243            State::Unavailable { .. } => PlaybackMaster::Unavailable,
244            State::Wall { .. } | State::AudioFallback { .. } => PlaybackMaster::Wall,
245            State::AudioPriming { .. } => PlaybackMaster::AudioPriming,
246            State::Audio { .. } => PlaybackMaster::Audio,
247        };
248        (master, position_at(*state, elapsed))
249    }
250
251    /// Claims the timeline for one audio renderer. Unused in a build without
252    /// an audio renderer (see `AudioMasterRegistration`), hence the `allow`.
253    #[allow(dead_code)]
254    pub(crate) fn register_audio_master(
255        self: &Arc<Self>,
256    ) -> Result<AudioMasterRegistration, PlaybackClockError> {
257        let mut state = self.state.lock().unwrap();
258        let elapsed = self.wall_clock.elapsed();
259        let (held_ns, registration, next_registration) = match *state {
260            State::Unavailable { next_registration } => {
261                (None, next_registration, next_registration.wrapping_add(1))
262            }
263            State::Wall {
264                next_registration, ..
265            } => (
266                position_at(*state, elapsed),
267                next_registration,
268                next_registration.wrapping_add(1),
269            ),
270            State::AudioPriming { .. } | State::Audio { .. } | State::AudioFallback { .. } => {
271                return Err(PlaybackClockError::AudioMasterAlreadyRegistered);
272            }
273        };
274        *state = State::AudioPriming {
275            registration,
276            held_ns,
277            next_registration,
278        };
279        Ok(AudioMasterRegistration {
280            clock: self.clone(),
281            registration,
282        })
283    }
284
285    /// Resets media state for a seek while retaining the current audio
286    /// renderer's ownership. The next timestamp/device sample establishes
287    /// the post-seek position.
288    pub(crate) fn reset_for_seek(&self) {
289        let mut state = self.state.lock().unwrap();
290        *state = match *state {
291            State::Unavailable { next_registration }
292            | State::Wall {
293                next_registration, ..
294            } => State::Unavailable { next_registration },
295            State::AudioPriming {
296                registration,
297                next_registration,
298                ..
299            }
300            | State::Audio {
301                registration,
302                next_registration,
303                ..
304            }
305            | State::AudioFallback {
306                registration,
307                next_registration,
308                ..
309            } => State::AudioPriming {
310                registration,
311                held_ns: None,
312                next_registration,
313            },
314        };
315    }
316
317    #[allow(dead_code)]
318    fn release_audio_master(&self, registration: u64) {
319        let mut state = self.state.lock().unwrap();
320        let elapsed = self.wall_clock.elapsed();
321        let (matches, next_registration) = match *state {
322            State::AudioPriming {
323                registration: current,
324                next_registration,
325                ..
326            }
327            | State::Audio {
328                registration: current,
329                next_registration,
330                ..
331            }
332            | State::AudioFallback {
333                registration: current,
334                next_registration,
335                ..
336            } => (current == registration, next_registration),
337            State::Unavailable { .. } | State::Wall { .. } => return,
338        };
339        if !matches {
340            return;
341        }
342        *state = match position_at(*state, elapsed) {
343            Some(anchor_ns) => State::Wall {
344                anchor_ns,
345                anchor_elapsed: elapsed,
346                next_registration,
347            },
348            None => State::Unavailable { next_registration },
349        };
350    }
351}
352
353/// Exclusive, generation-checked writer owned by one audio renderer.
354/// Dropping it hands the last known position back to the wall clock.
355///
356/// The only audio renderer in this crate is `WasapiRenderer`, behind the
357/// `wasapi-renderer` feature, so a build without it constructs this nowhere and
358/// every method below is dead. That is why the `allow`s here are deliberate
359/// rather than a `cfg(feature = "wasapi-renderer")` gate: `PlaybackClock` is
360/// the backend-independent timeline every renderer binds to, and teaching it
361/// about one backend's Cargo feature would invert that relationship. The
362/// crate's own tests exercise this path, so it is covered even when no shipped
363/// element uses it.
364#[allow(dead_code)]
365pub(crate) struct AudioMasterRegistration {
366    clock: Arc<PlaybackClock>,
367    registration: u64,
368}
369
370#[allow(dead_code)]
371impl AudioMasterRegistration {
372    pub(crate) fn priming_target_ns(&self) -> Result<Option<i64>, PlaybackClockError> {
373        match *self.clock.state.lock().unwrap() {
374            State::AudioPriming {
375                registration,
376                held_ns,
377                ..
378            } if registration == self.registration => Ok(held_ns),
379            State::Audio { registration, .. } if registration == self.registration => Ok(None),
380            State::AudioFallback { registration, .. } if registration == self.registration => {
381                Ok(None)
382            }
383            _ => Err(PlaybackClockError::StaleAudioMaster),
384        }
385    }
386
387    pub(crate) fn publish(
388        &self,
389        position_ns: i64,
390        submitted_until_ns: i64,
391        running: bool,
392    ) -> Result<(), PlaybackClockError> {
393        let mut state = self.clock.state.lock().unwrap();
394        self.clock.wall_clock.start();
395        let elapsed = self.clock.wall_clock.elapsed();
396        let (held_ns, next_registration) = match *state {
397            State::AudioPriming {
398                registration,
399                held_ns,
400                next_registration,
401            } if registration == self.registration => (held_ns, next_registration),
402            State::Audio {
403                registration,
404                next_registration,
405                ..
406            } if registration == self.registration => (None, next_registration),
407            State::AudioFallback {
408                registration,
409                next_registration,
410                ..
411            } if registration == self.registration => (None, next_registration),
412            _ => return Err(PlaybackClockError::StaleAudioMaster),
413        };
414
415        // A master handoff must never make video scheduling move backwards.
416        let position_ns = held_ns.map_or(position_ns, |held| position_ns.max(held));
417        let submitted_until_ns = submitted_until_ns.max(position_ns);
418        *state = State::Audio {
419            registration: self.registration,
420            position_ns,
421            sampled_elapsed: elapsed,
422            submitted_until_ns,
423            running,
424            next_registration,
425        };
426        Ok(())
427    }
428
429    /// Audio ended before another stream: continue from its final played
430    /// position using the wall clock while retaining this registration so
431    /// a second renderer cannot race the still-attached one.
432    pub(crate) fn finish(&self, position_ns: i64) -> Result<(), PlaybackClockError> {
433        let mut state = self.clock.state.lock().unwrap();
434        let elapsed = self.clock.wall_clock.elapsed();
435        let next_registration = match *state {
436            State::AudioPriming {
437                registration,
438                next_registration,
439                ..
440            }
441            | State::Audio {
442                registration,
443                next_registration,
444                ..
445            } if registration == self.registration => next_registration,
446            _ => return Err(PlaybackClockError::StaleAudioMaster),
447        };
448        *state = State::AudioFallback {
449            registration: self.registration,
450            anchor_ns: position_ns,
451            anchor_elapsed: elapsed,
452            next_registration,
453        };
454        Ok(())
455    }
456
457    pub(crate) fn reset_for_seek(&self) -> Result<(), PlaybackClockError> {
458        let mut state = self.clock.state.lock().unwrap();
459        let next_registration = match *state {
460            State::AudioPriming {
461                registration,
462                next_registration,
463                ..
464            }
465            | State::Audio {
466                registration,
467                next_registration,
468                ..
469            }
470            | State::AudioFallback {
471                registration,
472                next_registration,
473                ..
474            } if registration == self.registration => next_registration,
475            _ => return Err(PlaybackClockError::StaleAudioMaster),
476        };
477        *state = State::AudioPriming {
478            registration: self.registration,
479            held_ns: None,
480            next_registration,
481        };
482        Ok(())
483    }
484}
485
486impl Drop for AudioMasterRegistration {
487    fn drop(&mut self) {
488        self.clock.release_audio_master(self.registration);
489    }
490}
491
492fn position_at(state: State, elapsed: Duration) -> Option<i64> {
493    match state {
494        State::Unavailable { .. } => None,
495        State::Wall {
496            anchor_ns,
497            anchor_elapsed,
498            ..
499        } => Some(add_duration(
500            anchor_ns,
501            elapsed.saturating_sub(anchor_elapsed),
502        )),
503        State::AudioPriming { held_ns, .. } => held_ns,
504        State::Audio {
505            position_ns,
506            sampled_elapsed,
507            submitted_until_ns,
508            running,
509            ..
510        } => {
511            let projected = if running {
512                add_duration(position_ns, elapsed.saturating_sub(sampled_elapsed))
513            } else {
514                position_ns
515            };
516            Some(projected.min(submitted_until_ns))
517        }
518        State::AudioFallback {
519            anchor_ns,
520            anchor_elapsed,
521            ..
522        } => Some(add_duration(
523            anchor_ns,
524            elapsed.saturating_sub(anchor_elapsed),
525        )),
526    }
527}
528
529fn add_duration(value_ns: i64, duration: Duration) -> i64 {
530    let delta = duration.as_nanos().min(i64::MAX as u128) as i64;
531    value_ns.saturating_add(delta)
532}
533
534#[cfg(test)]
535mod tests {
536    use std::{thread, time::Duration};
537
538    use super::*;
539
540    #[test]
541    fn wall_origin_advances_and_freezes_with_pipeline_clock() {
542        let wall = Arc::new(Clock::new());
543        let playback = PlaybackClock::new(wall.clone());
544        assert!(playback.ensure_wall_origin(1_000).unwrap() >= 1_000);
545        thread::sleep(Duration::from_millis(20));
546        assert!(playback.position_ns().unwrap() >= 10_000_000);
547
548        wall.pause();
549        let paused = playback.position_ns().unwrap();
550        thread::sleep(Duration::from_millis(20));
551        assert_eq!(playback.position_ns(), Some(paused));
552    }
553
554    #[test]
555    fn audio_handoff_never_moves_backwards_and_release_continues_on_wall() {
556        let wall = Arc::new(Clock::new());
557        let playback = Arc::new(PlaybackClock::new(wall));
558        playback.ensure_wall_origin(50_000_000);
559        let audio = playback.register_audio_master().unwrap();
560        let held = audio.priming_target_ns().unwrap().unwrap();
561
562        audio
563            .publish(held - 10_000_000, held + 100_000_000, true)
564            .unwrap();
565        assert!(playback.position_ns().unwrap() >= held);
566        drop(audio);
567        let released = playback.position_ns().unwrap();
568        thread::sleep(Duration::from_millis(10));
569        assert!(playback.position_ns().unwrap() >= released);
570        assert_eq!(playback.master(), PlaybackMaster::Wall);
571    }
572
573    #[test]
574    fn only_one_audio_master_can_publish_and_seek_retains_its_generation() {
575        let wall = Arc::new(Clock::new());
576        let playback = Arc::new(PlaybackClock::new(wall));
577        let audio = playback.register_audio_master().unwrap();
578        assert!(matches!(
579            playback.register_audio_master(),
580            Err(PlaybackClockError::AudioMasterAlreadyRegistered)
581        ));
582
583        playback.reset_for_seek();
584        audio.publish(2_000, 3_000, true).unwrap();
585        assert_eq!(playback.master(), PlaybackMaster::Audio);
586    }
587
588    #[test]
589    fn audio_projection_is_capped_at_submitted_media() {
590        let wall = Arc::new(Clock::new());
591        let playback = Arc::new(PlaybackClock::new(wall));
592        let audio = playback.register_audio_master().unwrap();
593        audio.publish(10, 1_000_000, true).unwrap();
594        thread::sleep(Duration::from_millis(5));
595        assert_eq!(playback.position_ns(), Some(1_000_000));
596    }
597
598    #[test]
599    fn finished_audio_continues_on_wall_and_can_reset_for_seek() {
600        let wall = Arc::new(Clock::new());
601        let playback = Arc::new(PlaybackClock::new(wall));
602        let audio = playback.register_audio_master().unwrap();
603        audio.publish(1_000, 2_000, false).unwrap();
604        audio.finish(2_000).unwrap();
605        assert_eq!(playback.master(), PlaybackMaster::Wall);
606        thread::sleep(Duration::from_millis(5));
607        assert!(playback.position_ns().unwrap() > 2_000);
608
609        audio.reset_for_seek().unwrap();
610        assert_eq!(playback.master(), PlaybackMaster::AudioPriming);
611        assert_eq!(playback.position_ns(), None);
612    }
613}