Skip to main content

media_pp/elements/filter/
pacer.rs

1use std::{collections::VecDeque, sync::Arc, thread, time::Duration};
2
3use crate::pp_log::{PpLog, pp_info, pp_warn};
4use ffmpeg_next as ffmpeg;
5use thiserror::Error as ThisError;
6
7use crate::{
8    buffer::MediaBuffer,
9    contract::{InputContract, OutputContract},
10    control::ControlMsg,
11    element::{Element, ElementType, Sink, Source, element_pp_log},
12    pad::SrcPad,
13    playback_clock::PlaybackClock,
14    time::{InvalidTimeBase, MediaTimestamp, TimeBase},
15};
16
17/// Errors specific to [`Pacer`].
18#[derive(Debug, ThisError)]
19pub enum PacerError {
20    /// `time_base` came from
21    /// [`crate::elements::FileDemuxer::stream_time_base`]/an encoder's own
22    /// time base — i.e. from a demuxed file or an otherwise externally
23    /// supplied stream, not a value this crate controls. A malformed or
24    /// unusual stream can legitimately have an invalid one.
25    #[error(
26        "invalid time base {numerator}/{denominator}: both numerator and denominator must be positive"
27    )]
28    InvalidTimeBase {
29        /// Invalid rational numerator.
30        numerator: i32,
31        /// Invalid rational denominator.
32        denominator: i32,
33    },
34
35    /// `pts` is external input too (see [`PacerError::InvalidTimeBase`]) —
36    /// an adversarial or corrupt value that cannot be turned into a wait at
37    /// all: it does not fit in nanoseconds at this stream's time base.
38    ///
39    /// Reported rather than swallowed because the alternative is the buffer
40    /// going through *unpaced*, which is a whole stream arriving at once.
41    #[error("pts {pts} cannot be paced against this pipeline's timeline")]
42    UnpaceableTimestamp {
43        /// The timestamp, in its own stream's units.
44        pts: i64,
45    },
46
47    /// A buffer arrived before the pipeline gave this pacer its playback
48    /// clock, which
49    /// cannot happen through ordinary wiring: `attach_context` runs when the
50    /// branch is built and a branch cannot carry buffers before it exists.
51    ///
52    /// Reported rather than passed through, because pacing nothing is a
53    /// whole stream arriving at once and there is no quieter way for that to
54    /// go wrong.
55    #[error("this pacer was never wired into a pipeline, so it has no clock to pace against")]
56    NotAttached,
57}
58
59/// [`TimeBase::new_unchecked`] is fine here — `1/1_000_000_000` is a
60/// hardcoded constant known valid, not external input.
61fn nanoseconds() -> TimeBase {
62    TimeBase::new_unchecked(ffmpeg::Rational::new(1, 1_000_000_000))
63}
64
65/// Maximum time a paced wait sleeps without checking whether a control
66/// request needs the owning worker back.
67const INTERRUPT_POLL_INTERVAL: Duration = Duration::from_millis(10);
68
69/// Delays each buffer until its presentation time, so downstream sees
70/// frames (or, upstream of a decoder, compressed packets) at real playback
71/// speed instead of as fast as demux/decode can produce them. A `Filter`:
72/// receives via `Sink`, waits in short interruptible sleeps inside
73/// `consume`, then pushes the same buffer through its own (single) src pad.
74/// A pending pause/seek/stop interrupts that wait so the owning worker can
75/// process control: pause retains the in-flight buffer for resume, while
76/// seek and stop discard it.
77///
78/// Normally place a [`crate::queue::Queue`] upstream so the paced waits do
79/// not stall the demux/decoder feeding it and those stages can run ahead
80/// into the queue. The type does not enforce that placement; without the
81/// queue, pacing simply blocks the upstream caller on the same thread.
82///
83/// Every `Pacer` in a pipeline (one per stream — video, audio, ...) measures
84/// against the same [`crate::playback_clock::PlaybackClock`], so they agree
85/// on one t=0 instead of each anchoring to its own first frame. That
86/// agreement is what keeps the picture with the sound: the offset a
87/// container gives its streams is part of their sync, and a pacer zeroing on
88/// its own stream would throw it away.
89///
90/// Which clock that is can change while it runs. A pipeline starts on the
91/// pause-aware wall clock and hands the position to an audio renderer once
92/// its endpoint is running, and a pacer follows: what it waits on is where
93/// playback has actually reached, not where a wall-clock deadline computed
94/// at the start says it should be.
95pub struct Pacer {
96    pp_log: PpLog,
97    name: Arc<str>,
98    time_base: TimeBase,
99    /// The pipeline's, given by [`Element::attach_context`] — see there for
100    /// why this is not something the caller supplies.
101    ///
102    /// The playback clock rather than the wall one, and it holds the origin
103    /// too: a container's streams do not start at the same timestamp, and a
104    /// pacer that zeroed on its own first timestamp would play them as
105    /// though they did. Nothing is cached here — not the origin, not an
106    /// anchor — because both can move underneath: a pause shifts the wall
107    /// timeline, a seek clears the origin, and an audio renderer taking the
108    /// clock replaces the rate the position advances at.
109    playback_clock: Option<Arc<PlaybackClock>>,
110    /// The latest pipeline interrupt this pacer has acknowledged through
111    /// `control()`. A newer clock epoch means pause/seek/stop is waiting for
112    /// the current `consume()` call to return. `Queue`'s own worker only
113    /// checks its control channel *between* buffers (see its type docs) —
114    /// it can't preempt a `consume()` call already in flight, and this
115    /// pacer's own wait is exactly that kind of long-running call.
116    interrupt_epoch: u64,
117    /// The longest a single buffer may hold this pacer before its timestamp
118    /// is read as a new timeline rather than a distant one, or `None` for a
119    /// timeline that cannot restart — see
120    /// [`Pacer::with_discontinuity_limit`].
121    discontinuity_limit: Option<Duration>,
122    /// Preroll advances data without consulting the paused pipeline clock.
123    prerolling: bool,
124    /// Buffers whose paced wait was interrupted before the owning worker
125    /// could process pause/seek/stop. Pause retains them for resume; seek
126    /// and stop discard them in `control()`.
127    pending: VecDeque<MediaBuffer>,
128    pad: SrcPad,
129}
130
131impl Pacer {
132    /// Creates a pacer using `time_base` to convert input PTS values to wall
133    /// time.
134    ///
135    /// The clock it paces against is the pipeline's and arrives when this is
136    /// wired into one — see [`Element::attach_context`].
137    pub fn new(name: impl Into<String>, time_base: ffmpeg::Rational) -> Result<Self, PacerError> {
138        let name: Arc<str> = name.into().into();
139        let pp_log = element_pp_log(ElementType::Pacer, &name, None);
140        pp_info!(pp_log: &pp_log, "created: time_base={time_base}");
141        let pad = SrcPad::with_contract(format!("{name}_src"), OutputContract::Passthrough);
142        let time_base = TimeBase::try_new(time_base).map_err(
143            |InvalidTimeBase {
144                 numerator,
145                 denominator,
146             }| PacerError::InvalidTimeBase {
147                numerator,
148                denominator,
149            },
150        )?;
151        Ok(Self {
152            name,
153            pp_log,
154            time_base,
155            playback_clock: None,
156            discontinuity_limit: None,
157            interrupt_epoch: 0,
158            prerolling: false,
159            pending: VecDeque::new(),
160            pad,
161        })
162    }
163
164    /// The same, for a stream whose timeline can restart under it.
165    ///
166    /// A file's timestamps only ever move forward from where they began, so
167    /// a buffer due far ahead is a real gap in the stream and waiting it out
168    /// is the correct thing to do. A live sender is not like that: a camera
169    /// that reboots, or an RTP timestamp base that wraps, hands over a
170    /// timestamp with no relation to the one before it, and a pacer that
171    /// believes it sleeps for as long as the jump says — a still picture,
172    /// no error, and nothing to reconnect from, since as far as the pipeline
173    /// is concerned it is working.
174    ///
175    /// Past `limit` such a jump is read as a new timeline: the origin
176    /// re-anchors so the buffer that carried it is due now, and a warning
177    /// says so. Both branches of one source see the same jump and re-anchor
178    /// within a buffer of each other, so the picture keeps its sound.
179    ///
180    /// Pick a `limit` longer than the longest gap the stream can really
181    /// have — for most cameras a second or two of nothing is already a
182    /// problem, not a pause. Shorter than the spacing between its own
183    /// frames and every ordinary wait reads as a jump, which is this pacer
184    /// no longer pacing at all.
185    pub fn with_discontinuity_limit(
186        name: impl Into<String>,
187        time_base: ffmpeg::Rational,
188        limit: Duration,
189    ) -> Result<Self, PacerError> {
190        let mut pacer = Self::new(name, time_base)?;
191        pp_info!(pp_log: &pacer.pp_log, "timeline jumps beyond {limit:?} re-anchor it");
192        pacer.discontinuity_limit = Some(limit);
193        Ok(pacer)
194    }
195
196    /// Blocks until `pts` is due against the pipeline's playback clock.
197    ///
198    /// Returns `Ok(false)` if pause/seek/stop interrupts the wait; the caller
199    /// retains that in-flight buffer and returns so the owning worker can
200    /// process the pending control request. Frames without a pts (`None`)
201    /// pass straight through. `Err` only for a `pts` too pathological to
202    /// pace against at all (see [`PacerError::UnpaceableTimestamp`]) — the
203    /// caller drops that one buffer rather than treating it as interrupted.
204    ///
205    /// How long is left is asked again on every pass rather than computed
206    /// once into a deadline. Under a wall-clock master the two are the same
207    /// arithmetic; under an audio one they are not, because the position
208    /// advances at the device's rate and a deadline named up front would be
209    /// wrong by however far that rate differs.
210    fn wait_for(&mut self, pts: Option<i64>) -> Result<bool, PacerError> {
211        let playback = self.playback_clock.clone().ok_or(PacerError::NotAttached)?;
212        if playback.interrupt_epoch() != self.interrupt_epoch {
213            return Ok(false);
214        }
215        if self.prerolling {
216            return Ok(true);
217        }
218        let Some(pts) = pts else { return Ok(true) };
219        // Rescaled before anything is compared, because the origin this is
220        // measured against is shared with streams in other units — a
221        // container's audio and video rarely count in the same ticks.
222        // Integer rescale rather than `pts as f64 * f64::from(time_base)`:
223        // the latter loses precision (and the numerator, if computed by
224        // naive division) over a long-running stream; see `MediaTimestamp`'s
225        // own docs.
226        let pts_ns = MediaTimestamp::new_unchecked(pts, self.time_base).rescale(nanoseconds());
227        // `av_rescale_q_rnd` answers a value it cannot represent with
228        // `INT64_MIN`, which is also FFmpeg's "no timestamp". Checked before
229        // the clock is asked, so a pathological first buffer cannot become
230        // the timeline every other stream is measured against.
231        if pts_ns == i64::MIN {
232            return Err(PacerError::UnpaceableTimestamp { pts });
233        }
234        loop {
235            if playback.interrupt_epoch() != self.interrupt_epoch {
236                return Ok(false);
237            }
238            let remaining = playback.remaining(pts_ns);
239            if remaining.is_zero() {
240                return Ok(true);
241            }
242            if let Some(limit) = self.discontinuity_limit
243                && remaining > limit
244            {
245                // A jump this far forward is a sender that restarted its
246                // timeline, not a stream with a gap that long in it — see
247                // `with_discontinuity_limit`.
248                pp_warn!(
249                    self,
250                    "timeline jumped {remaining:?} ahead; re-anchoring on it"
251                );
252                playback.re_anchor(pts_ns);
253                return Ok(true);
254            }
255            thread::sleep(remaining.min(INTERRUPT_POLL_INTERVAL));
256        }
257    }
258}
259
260impl Element for Pacer {
261    fn name(&self) -> Arc<str> {
262        self.name.clone()
263    }
264
265    fn element_type(&self) -> ElementType {
266        ElementType::Pacer
267    }
268
269    fn pp_log(&self) -> &PpLog {
270        &self.pp_log
271    }
272
273    fn pp_log_mut(&mut self) -> &mut PpLog {
274        &mut self.pp_log
275    }
276
277    fn attach_context(&mut self, context: &Arc<crate::element::Context>) {
278        self.interrupt_epoch = context.playback_clock.interrupt_epoch();
279        self.playback_clock = Some(Arc::clone(&context.playback_clock));
280    }
281}
282
283impl Source for Pacer {
284    fn src_pads(&mut self) -> &mut [SrcPad] {
285        std::slice::from_mut(&mut self.pad)
286    }
287}
288
289impl Sink for Pacer {
290    /// Pacing is a delay, not a transform: every kind is held until its
291    /// own PTS comes due and then forwarded unchanged.
292    fn input_contract(&self) -> InputContract {
293        InputContract::Any
294    }
295
296    fn consume(&mut self, buf: MediaBuffer) -> crate::error::Result<()> {
297        self.pending.push_back(buf);
298        while let Some(buf) = self.pending.pop_front() {
299            let ready = match &buf {
300                MediaBuffer::Packet(packet) => self.wait_for(packet.pts())?,
301                MediaBuffer::Video(frame) => self.wait_for(frame.pts())?,
302                MediaBuffer::Audio(frame) => self.wait_for(frame.pts())?,
303                MediaBuffer::Eos => true,
304            };
305            if !ready {
306                self.pending.push_front(buf);
307                return Ok(());
308            }
309            self.pad.push(buf)?;
310        }
311        Ok(())
312    }
313
314    fn control(&mut self, msg: ControlMsg) -> crate::error::Result<()> {
315        // Acknowledge the interrupt that made any in-flight wait return.
316        // Flush discards an interrupted old-timeline buffer; Seek then drops
317        // the origin so the new timeline anchors on whichever stream reaches
318        // its landing place first.
319        // A pacer that was never wired has no clock to acknowledge, and
320        // nothing is going to send it control either — see
321        // `PacerError::NotAttached`.
322        if let Some(playback) = &self.playback_clock {
323            self.interrupt_epoch = playback.interrupt_epoch();
324        }
325        match msg {
326            ControlMsg::Flush => self.pending.clear(),
327            ControlMsg::Seek(_) => {
328                // The wall clock is left alone, which it could not be while
329                // the origin was paired with `Clock::start()`: post-seek
330                // timestamps restart near zero, and against a stale anchor
331                // every one of them was already overdue. The playback clock
332                // holds its origin as an elapsed offset instead, so it
333                // re-anchors itself on the next buffer and the pipeline's
334                // monotonic time — which a seek does not stop — keeps
335                // running for everything else that reads it.
336                if let Some(playback) = &self.playback_clock {
337                    playback.reset_for_seek();
338                }
339            }
340            ControlMsg::Stop => self.pending.clear(),
341            ControlMsg::Preroll(_) => {
342                self.prerolling = true;
343            }
344            ControlMsg::Pause | ControlMsg::Resume => {
345                self.prerolling = false;
346            }
347            ControlMsg::CheckSeek(_) => {}
348        }
349        self.pad.control(msg)
350    }
351}
352
353#[cfg(test)]
354mod tests {
355    use super::*;
356    use crate::clock::Clock;
357    use crate::control::PrerollContext;
358    use std::{
359        sync::mpsc,
360        time::{Duration, Instant},
361    };
362
363    /// One pipeline's context, around a clock the test keeps so it can
364    /// interrupt and pause it.
365    ///
366    /// Shared between every pacer a test wires, because that is what a
367    /// pipeline does and what the origin depends on: two contexts are two
368    /// playback clocks, and two streams measured against separate origins
369    /// have lost the offset between them before the test starts.
370    fn context(clock: &Arc<Clock>) -> Arc<crate::element::Context> {
371        Arc::new(crate::element::Context::for_test_with_clock(
372            crate::bus::Bus::new().0,
373            "test",
374            crate::graph::PipelineGraph::new(),
375            crate::graph::ElementId::for_test(1),
376            Arc::clone(clock),
377        ))
378    }
379
380    /// A pacer wired the way a pipeline wires one.
381    fn paced(
382        name: &str,
383        time_base: ffmpeg::Rational,
384        context: &Arc<crate::element::Context>,
385    ) -> Pacer {
386        let mut pacer = Pacer::new(name, time_base).expect("valid time base");
387        pacer.attach_context(context);
388        pacer
389    }
390
391    /// The same, with a limit on how long one buffer may hold it.
392    fn paced_live(
393        name: &str,
394        time_base: ffmpeg::Rational,
395        context: &Arc<crate::element::Context>,
396        limit: Duration,
397    ) -> Pacer {
398        let mut pacer =
399            Pacer::with_discontinuity_limit(name, time_base, limit).expect("valid time base");
400        pacer.attach_context(context);
401        pacer
402    }
403
404    /// A camera that reboots hands over a timestamp with no relation to the
405    /// one before it. Waited out, that is a still picture for as long as the
406    /// jump says — and nothing reports it, because as far as the pipeline is
407    /// concerned the pacer is doing its job.
408    #[test]
409    fn a_live_timeline_that_jumps_is_re_anchored_rather_than_waited_out() {
410        let clock = Arc::new(Clock::new());
411        let context = context(&clock);
412        // Milliseconds, and a limit longer than the spacing between the
413        // frames below — a limit shorter than that would read the ordinary
414        // wait for the next frame as a jump, which is what it is for the
415        // caller to pick against its own stream.
416        let mut pacer = paced_live(
417            "pacer",
418            ffmpeg::Rational::new(1, 1000),
419            &context,
420            Duration::from_millis(300),
421        );
422        assert!(pacer.wait_for(Some(0)).unwrap(), "the first pts anchors");
423
424        let started = Instant::now();
425        assert!(
426            pacer.wait_for(Some(3_600_000)).unwrap(),
427            "a jumped timestamp is released, not refused"
428        );
429        assert!(
430            started.elapsed() < Duration::from_secs(1),
431            "an hour ahead must not be an hour of waiting: took {:?}",
432            started.elapsed()
433        );
434
435        // And the new timeline is the one it paces against from here: 200ms
436        // after the jump is 200ms of waiting, not another hour.
437        let after = Instant::now();
438        assert!(pacer.wait_for(Some(3_600_200)).unwrap(), "the next frame");
439        let waited = after.elapsed();
440        assert!(
441            waited >= Duration::from_millis(150) && waited < Duration::from_secs(2),
442            "200ms past the re-anchored origin: waited {waited:?}"
443        );
444    }
445
446    /// The limit is not the default, and must not be: a file's timeline does
447    /// not restart, so a gap in it is real and waiting it out is correct.
448    #[test]
449    fn a_pacer_without_a_limit_still_waits_out_a_distant_timestamp() {
450        let clock = Arc::new(Clock::new());
451        let context = context(&clock);
452        let mut pacer = paced("pacer", ffmpeg::Rational::new(1, 1000), &context);
453        assert!(pacer.wait_for(Some(0)).unwrap(), "the first pts anchors");
454
455        let started = Instant::now();
456        assert!(pacer.wait_for(Some(300)).unwrap(), "300ms into the stream");
457        assert!(
458            started.elapsed() >= Duration::from_millis(250),
459            "the gap is the stream's own and has to be waited out: took {:?}",
460            started.elapsed()
461        );
462    }
463
464    fn packet(pts: i64) -> MediaBuffer {
465        let mut packet = ffmpeg::Packet::empty();
466        packet.set_pts(Some(pts));
467        MediaBuffer::Packet(Arc::new(packet))
468    }
469
470    #[test]
471    fn long_wait_returns_promptly_when_control_interrupts_it() {
472        let clock = Arc::new(Clock::new());
473        let context = context(&clock);
474        let mut pacer = paced("pacer", ffmpeg::Rational::new(1, 1), &context);
475        assert!(
476            pacer.wait_for(Some(0)).unwrap(),
477            "first pts should establish the anchor"
478        );
479
480        let (started_tx, started_rx) = mpsc::channel();
481        let worker = thread::spawn(move || {
482            started_tx.send(()).expect("test receiver alive");
483            pacer.wait_for(Some(60))
484        });
485
486        started_rx.recv().expect("paced wait should start");
487        thread::sleep(Duration::from_millis(20));
488        clock.interrupt();
489
490        assert!(
491            !worker
492                .join()
493                .expect("paced wait should return")
494                .expect("interrupted wait is Ok(false), not an error"),
495            "an interrupted paced wait must return before its due time"
496        );
497    }
498
499    #[test]
500    fn pause_retains_interrupted_buffer_but_flush_and_stop_discard_it() {
501        let clock = Arc::new(Clock::new());
502        let context = context(&clock);
503        let mut pacer = paced("pacer", ffmpeg::Rational::new(1, 1), &context);
504
505        clock.interrupt();
506        pacer.consume(packet(0)).expect("interrupted consume");
507        assert_eq!(pacer.pending.len(), 1);
508
509        pacer.control(ControlMsg::Pause).expect("pause");
510        assert_eq!(pacer.pending.len(), 1, "pause must retain the buffer");
511
512        pacer.control(ControlMsg::Flush).expect("flush");
513        assert!(pacer.pending.is_empty(), "flush must discard stale data");
514
515        pacer
516            .control(ControlMsg::Seek(Duration::ZERO))
517            .expect("seek");
518
519        clock.interrupt();
520        pacer.consume(packet(1)).expect("interrupted consume");
521        assert_eq!(pacer.pending.len(), 1);
522        pacer.control(ControlMsg::Stop).expect("stop");
523        assert!(pacer.pending.is_empty(), "stop must abandon pending data");
524    }
525
526    #[test]
527    fn new_rejects_an_invalid_time_base() {
528        for rational in [
529            ffmpeg::Rational::new(0, 1),
530            ffmpeg::Rational::new(1, 0),
531            ffmpeg::Rational::new(-1, 1),
532            ffmpeg::Rational::new(1, -1),
533        ] {
534            assert!(
535                matches!(
536                    Pacer::new("pacer", rational),
537                    Err(PacerError::InvalidTimeBase { .. })
538                ),
539                "expected {rational} to be rejected"
540            );
541        }
542    }
543
544    /// Preroll has to outrun the paused clock — that is the whole reason a
545    /// `Pacer` reacts to it. Suppressing pre-target media is *not* its job:
546    /// that needs the time base a decoder has on every decoded branch, and a
547    /// `Pacer` is only on some of them.
548    /// Preroll has to outrun the paused clock — that is the whole reason a
549    /// `Pacer` reacts to it, and a paused pipeline could otherwise never
550    /// deliver a preview sample. Suppressing pre-target media is *not* its
551    /// job: that needs the time base a decoder has on every decoded branch,
552    /// and a `Pacer` is only on some of them.
553    #[test]
554    fn preroll_forwards_without_waiting_out_the_presentation_time() {
555        let clock = Arc::new(Clock::new());
556        let context = context(&clock);
557        let mut pacer = paced("pacer", ffmpeg::Rational::new(1, 1), &context);
558        let context = Arc::new(PrerollContext::for_seek([], Duration::from_secs(2)));
559        pacer
560            .control(ControlMsg::Preroll(context))
561            .expect("preroll");
562
563        let started = Instant::now();
564        pacer.consume(packet(0)).expect("first preroll packet");
565        pacer.consume(packet(60)).expect("distant preroll packet");
566
567        assert!(
568            started.elapsed() < Duration::from_millis(100),
569            "a minute of presentation time must not be waited out during preroll"
570        );
571    }
572
573    /// A pacer that was never wired into a pipeline has no clock, and the
574    /// one thing it must not do is let the buffer through: unpaced is a
575    /// whole stream arriving at once, and silently.
576    ///
577    /// Unreachable through ordinary wiring — `attach_context` runs when a
578    /// branch is built, and a branch cannot carry buffers before it exists —
579    /// which is exactly why it is worth a typed error rather than a
580    /// debug_assert nobody runs.
581    #[test]
582    fn an_unwired_pacer_refuses_rather_than_passing_a_buffer_through() {
583        let mut pacer = Pacer::new("pacer", ffmpeg::Rational::new(1, 1)).unwrap();
584        assert!(matches!(
585            pacer.consume(packet(0)),
586            Err(crate::error::Error::PacerError(PacerError::NotAttached))
587        ));
588    }
589
590    /// Regression test for the sync this exists to keep. A container's
591    /// streams do not start together — `sample.mp4`'s audio starts at zero
592    /// and its video one frame in, 33 ms later — and that offset is part of
593    /// what puts the picture with the sound.
594    ///
595    /// Each pacer used to zero on its own stream's first timestamp, which
596    /// released both first buffers at once and played the sound 33 ms early
597    /// for the rest of the file. The origin is the *pipeline's* now, so the
598    /// stream that starts later waits for its turn.
599    #[test]
600    fn streams_that_start_apart_stay_apart() {
601        let clock = Arc::new(Clock::new());
602        let context = context(&clock);
603        // Milliseconds, so the numbers below read as what they are.
604        let unit = ffmpeg::Rational::new(1, 1000);
605        let mut audio = paced("audio", unit, &context);
606        let mut video = paced("video", unit, &context);
607
608        let started = Instant::now();
609        assert!(
610            audio.wait_for(Some(0)).unwrap(),
611            "the first stream sets the origin and has nothing to wait for"
612        );
613        assert!(
614            started.elapsed() < Duration::from_millis(20),
615            "it must not wait for itself"
616        );
617
618        assert!(video.wait_for(Some(80)).unwrap(), "paced, not refused");
619        assert!(
620            started.elapsed() >= Duration::from_millis(70),
621            "a stream starting 80ms into the file must be held back by it, \
622             not released alongside the one that starts at zero"
623        );
624    }
625
626    /// Where the position comes from once an audio renderer owns it.
627    ///
628    /// A pacer measures against the pipeline's playback clock rather than
629    /// against the first timestamp it happened to see, so a stream joining a
630    /// pipeline whose audio is already a second in is a second late, not at
631    /// its own zero. Held apart from the wall clock on purpose: with an
632    /// origin of its own this pacer would have released the two buffers
633    /// below 200ms apart, and against the audio position both are already
634    /// past.
635    #[test]
636    fn a_pacer_measures_against_the_audio_master_not_its_own_first_buffer() {
637        let clock = Arc::new(Clock::new());
638        let context = context(&clock);
639        let mut pacer = paced("pacer", ffmpeg::Rational::new(1, 1000), &context);
640
641        let audio = context
642            .playback_clock
643            .register_audio_master()
644            .expect("nothing else holds the clock");
645        // A second of audio played, five submitted, and still running.
646        audio
647            .publish(1_000_000_000, 5_000_000_000, true)
648            .expect("the registration is live");
649
650        let started = Instant::now();
651        assert!(pacer.wait_for(Some(100)).unwrap(), "100ms is already past");
652        assert!(pacer.wait_for(Some(300)).unwrap(), "so is 300ms");
653        assert!(
654            started.elapsed() < Duration::from_millis(100),
655            "both are behind a position already at 1s and neither has \
656             anything to wait for: took {:?}",
657            started.elapsed()
658        );
659    }
660
661    /// An audio master that has taken the clock but not started must not
662    /// hold a pacer.
663    ///
664    /// This is the deadlock a renderer's deferred registration exists to
665    /// avoid, seen from the other side: a branch attached to a running `Tee`
666    /// sits behind a demuxer that cannot reach its first audio packet until
667    /// the video queue drains, and a pacer waiting on a position no one has
668    /// published yet is what would stop that queue draining.
669    #[test]
670    fn priming_does_not_hold_a_pacer() {
671        let clock = Arc::new(Clock::new());
672        let context = context(&clock);
673        let mut pacer = paced("pacer", ffmpeg::Rational::new(1, 1000), &context);
674
675        let _audio = context
676            .playback_clock
677            .register_audio_master()
678            .expect("nothing else holds the clock");
679        assert_eq!(
680            context.playback_clock.master(),
681            crate::playback_clock::PlaybackMaster::AudioPriming
682        );
683
684        let started = Instant::now();
685        assert!(
686            pacer.wait_for(Some(10_000)).unwrap(),
687            "ten seconds ahead, and released anyway"
688        );
689        assert!(
690            started.elapsed() < Duration::from_millis(100),
691            "a priming master says nothing about where it is, and waiting on \
692             that is the stall this is here to rule out: took {:?}",
693            started.elapsed()
694        );
695    }
696
697    /// Regression test: a `pts` this far from the origin used to overflow
698    /// the subtraction silently (a plain `-`) or let the buffer through
699    /// unpaced (an earlier `checked_sub` that swallowed the error). Now
700    /// it's a typed `PacerError` `consume` propagates via `?`, and — since
701    /// `Queue`/a pushing source both treat a `Sink::consume` failure as
702    /// "drop this one buffer, report on the bus, keep going" — a Pacer
703    /// that hits this on one buffer must still pace the next one normally.
704    #[test]
705    fn a_pathological_pts_jump_is_a_typed_error_not_silent_passthrough() {
706        let clock = Arc::new(Clock::new());
707        let context = context(&clock);
708        let mut pacer = paced("pacer", ffmpeg::Rational::new(1, 1), &context);
709
710        assert!(pacer.consume(packet(-1)).is_ok(), "establishes the origin");
711
712        let error = pacer
713            .consume(packet(i64::MAX))
714            .expect_err("pts far enough from the origin to overflow the subtraction");
715        assert!(matches!(
716            error,
717            crate::Error::PacerError(PacerError::UnpaceableTimestamp { pts: i64::MAX })
718        ));
719        assert!(
720            pacer.pending.is_empty(),
721            "the overflowing buffer must not get stuck in `pending`"
722        );
723
724        // The pacer itself must still be usable afterward: a `Some` result
725        // (not a further error) for an ordinary pts relative to the same
726        // origin.
727        assert!(pacer.wait_for(Some(0)).is_ok());
728    }
729
730    /// Packets whose `pts` goes backwards are still paced to their own
731    /// timeline.
732    ///
733    /// A `Pacer` in front of a decoder — where `webrtc_record` and
734    /// `rtsp_serve` both put one — waits on `pts`, and a stream carrying
735    /// B-frames hands it packets in decode order: `pts` jumps forward, then
736    /// back behind a frame already released, over and over. Each of those is
737    /// simply already due, so what comes out is bursty within a frame or two
738    /// and correct across the stream. What must not happen is either end of
739    /// getting that wrong — a wait on a timestamp read as far in the future,
740    /// or an origin that moves and lets the whole stream through at once.
741    #[test]
742    fn a_reordered_packet_stream_is_paced_to_its_own_length() {
743        use crate::elements::FileDemuxer;
744        use crate::pipeline::Pipeline;
745        use std::sync::atomic::{AtomicUsize, Ordering};
746
747        const SECONDS: f64 = 2.0;
748
749        let fixture = crate::test_support::synthesize_reordered("paced-reorder", SECONDS);
750        let path = fixture.path.to_string_lossy().into_owned();
751        let (demuxer, streams) = FileDemuxer::open("demuxer", &path).expect("open the fixture");
752        let video = streams
753            .iter()
754            .find(|stream| stream.kind == ffmpeg::media::Type::Video)
755            .expect("the fixture has video")
756            .index;
757        let time_base = demuxer.stream_time_base(video).expect("video time base");
758
759        let seen = Arc::new(AtomicUsize::new(0));
760        let counter = crate::elements::AppSink::new("paced", {
761            let seen = Arc::clone(&seen);
762            move |_| {
763                seen.fetch_add(1, Ordering::Relaxed);
764                Ok(())
765            }
766        });
767
768        let started = Instant::now();
769        let pipeline = Pipeline::new("paced-reorder", demuxer, move |source, context| {
770            let branch = context
771                .branch()
772                .pipe(Pacer::new("pacer", time_base)?)
773                .to(Box::new(counter))?;
774            context.attach(source, video, branch)?;
775            Ok(())
776        })
777        .expect("wire the paced stream");
778        pipeline.run().expect("run it");
779        for event in pipeline.bus().iter() {
780            if matches!(event, crate::bus::BusEvent::Eos { .. }) {
781                break;
782            }
783        }
784        let elapsed = started.elapsed();
785        pipeline.stop();
786
787        assert!(
788            seen.load(Ordering::Relaxed) > 0,
789            "no packet reached the far side of the pacer"
790        );
791        // Generous on both sides: what this is watching for is a stall or a
792        // whole stream let through at once, not a few milliseconds either
793        // way.
794        let content = Duration::from_secs_f64(SECONDS);
795        assert!(
796            elapsed >= content.mul_f64(0.5),
797            "a reordered stream was let through in {elapsed:?}, well under the \
798             {content:?} it describes"
799        );
800        assert!(
801            elapsed <= content.mul_f64(2.5),
802            "a reordered stream took {elapsed:?} to pace {content:?} of packets"
803        );
804    }
805}