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}