Skip to main content

media_pp\elements\filter/
pacer.rs

1use std::{
2    collections::VecDeque,
3    sync::Arc,
4    thread,
5    time::{Duration, Instant},
6};
7
8use crate::pp_log::{PpLog, pp_info};
9use ffmpeg_next as ffmpeg;
10use thiserror::Error as ThisError;
11
12use crate::{
13    buffer::MediaBuffer,
14    clock::Clock,
15    control::ControlMsg,
16    element::{Element, ElementType, Sink, Source, element_pp_log},
17    pad::SrcPad,
18    time::{InvalidTimeBase, MediaTimestamp, TimeBase},
19};
20
21/// Errors specific to [`Pacer`].
22#[derive(Debug, ThisError)]
23pub enum PacerError {
24    /// `time_base` came from
25    /// [`crate::elements::FileDemuxer::stream_time_base`]/an encoder's own
26    /// time base — i.e. from a demuxed file or an otherwise externally
27    /// supplied stream, not a value this crate controls. A malformed or
28    /// unusual stream can legitimately have an invalid one.
29    #[error(
30        "invalid time base {numerator}/{denominator}: both numerator and denominator must be positive"
31    )]
32    InvalidTimeBase { numerator: i32, denominator: i32 },
33
34    /// `pts` is external input too (see [`PacerError::InvalidTimeBase`]) —
35    /// an adversarial or corrupt jump this far from this pacer's own
36    /// `first_pts` overflows the subtraction used to compute how long to
37    /// wait, leaving nothing sane to pace against.
38    #[error("pts {pts} is too far from this pacer's first pts {first_pts} to pace against")]
39    TimestampDeltaOverflow { pts: i64, first_pts: i64 },
40}
41
42/// [`TimeBase::new_unchecked`] is fine here — `1/1_000_000_000` is a
43/// hardcoded constant known valid, not external input.
44fn nanoseconds() -> TimeBase {
45    TimeBase::new_unchecked(ffmpeg::Rational::new(1, 1_000_000_000))
46}
47
48/// Maximum time a paced wait sleeps without checking whether a control
49/// request needs the owning worker back.
50const INTERRUPT_POLL_INTERVAL: Duration = Duration::from_millis(10);
51
52/// Delays each buffer until its presentation time, so downstream sees
53/// frames (or, upstream of a decoder, compressed packets) at real playback
54/// speed instead of as fast as demux/decode can produce them. A `Filter`:
55/// receives via `Sink`, waits in short interruptible sleeps inside
56/// `consume`, then pushes the same buffer through its own (single) src pad.
57/// A pending pause/seek/stop interrupts that wait so the owning worker can
58/// process control: pause retains the in-flight buffer for resume, while
59/// seek and stop discard it.
60///
61/// Normally place a [`crate::queue::Queue`] upstream so the paced waits do
62/// not stall the demux/decoder feeding it and those stages can run ahead
63/// into the queue. The type does not enforce that placement; without the
64/// queue, pacing simply blocks the upstream caller on the same thread.
65///
66/// `clock` is shared across every `Pacer` in the pipeline (one per stream
67/// — video, audio, ...) so they all agree on the same t=0 instead of each
68/// anchoring to its own first frame.
69pub struct Pacer {
70    pp_log: PpLog,
71    name: Arc<str>,
72    time_base: TimeBase,
73    clock: Arc<Clock>,
74    /// This pacer's first timestamped frame's pts — set on first call.
75    /// Deliberately *not* paired with a cached wall-clock anchor: the
76    /// anchor has to come fresh from `clock.start()` on every call
77    /// instead, since [`Clock::pause`]/[`Clock::resume`] can shift it —
78    /// caching it once here would mean a paused-then-resumed pipeline
79    /// blasts through however many frames piled up during the pause
80    /// (their `due` times would all already be in the past relative to a
81    /// stale anchor).
82    first_pts: Option<i64>,
83    /// The latest pipeline interrupt this pacer has acknowledged through
84    /// `control()`. A newer clock epoch means pause/seek/stop is waiting for
85    /// the current `consume()` call to return. `Queue`'s own worker only
86    /// checks its control channel *between* buffers (see its type docs) —
87    /// it can't preempt a `consume()` call already in flight, and this
88    /// pacer's own wait is exactly that kind of long-running call.
89    interrupt_epoch: u64,
90    /// Buffers whose paced wait was interrupted before the owning worker
91    /// could process pause/seek/stop. Pause retains them for resume; seek
92    /// and stop discard them in `control()`.
93    pending: VecDeque<MediaBuffer>,
94    pad: SrcPad,
95}
96
97impl Pacer {
98    pub fn new(
99        name: impl Into<String>,
100        time_base: ffmpeg::Rational,
101        clock: Arc<Clock>,
102    ) -> Result<Self, PacerError> {
103        let name: Arc<str> = name.into().into();
104        let pp_log = element_pp_log(ElementType::Pacer, &name, None);
105        pp_info!(pp_log: &pp_log, "created: time_base={time_base}");
106        let pad = SrcPad::new(format!("{name}_src"));
107        let interrupt_epoch = clock.interrupt_epoch();
108        let time_base = TimeBase::try_new(time_base).map_err(
109            |InvalidTimeBase {
110                 numerator,
111                 denominator,
112             }| PacerError::InvalidTimeBase {
113                numerator,
114                denominator,
115            },
116        )?;
117        Ok(Self {
118            name,
119            pp_log,
120            time_base,
121            clock,
122            first_pts: None,
123            interrupt_epoch,
124            pending: VecDeque::new(),
125            pad,
126        })
127    }
128
129    /// Blocks until `pts` is due, based on this pacer's `first_pts` (set
130    /// here, on the first call) and the shared `clock`'s *current*
131    /// anchor. Returns `Ok(false)` if pause/seek/stop interrupts the wait;
132    /// the caller retains that in-flight buffer and returns so the owning
133    /// worker can process the pending control request. Frames without a
134    /// pts (`None`) pass straight through. `Err` only for a `pts` too
135    /// pathological to pace against at all (see
136    /// [`PacerError::TimestampDeltaOverflow`]) — the caller drops that one
137    /// buffer rather than treating it as interrupted.
138    fn wait_for(&mut self, pts: Option<i64>) -> Result<bool, PacerError> {
139        if self.clock.interrupt_epoch() != self.interrupt_epoch {
140            return Ok(false);
141        }
142        let Some(pts) = pts else { return Ok(true) };
143        let first_pts = *self.first_pts.get_or_insert(pts);
144
145        let elapsed_ticks = pts
146            .checked_sub(first_pts)
147            .ok_or(PacerError::TimestampDeltaOverflow { pts, first_pts })?;
148        if elapsed_ticks <= 0 {
149            return Ok(true);
150        }
151        // Integer rescale straight to nanoseconds rather than
152        // `elapsed_ticks as f64 * f64::from(time_base)` — the latter loses
153        // precision (and the numerator, if computed by naive division)
154        // over a long-running stream; see `MediaTimestamp`'s own docs.
155        let elapsed_ns = MediaTimestamp::new_unchecked(elapsed_ticks, self.time_base)
156            .rescale(nanoseconds())
157            .max(0) as u64;
158
159        let due = self.clock.start() + Duration::from_nanos(elapsed_ns);
160        loop {
161            if self.clock.interrupt_epoch() != self.interrupt_epoch {
162                return Ok(false);
163            }
164            let now = Instant::now();
165            if due <= now {
166                return Ok(true);
167            }
168            thread::sleep((due - now).min(INTERRUPT_POLL_INTERVAL));
169        }
170    }
171}
172
173impl Element for Pacer {
174    fn name(&self) -> Arc<str> {
175        self.name.clone()
176    }
177
178    fn element_type(&self) -> ElementType {
179        ElementType::Pacer
180    }
181
182    fn pp_log(&self) -> &PpLog {
183        &self.pp_log
184    }
185
186    fn pp_log_mut(&mut self) -> &mut PpLog {
187        &mut self.pp_log
188    }
189}
190
191impl Source for Pacer {
192    fn src_pads(&mut self) -> &mut [SrcPad] {
193        std::slice::from_mut(&mut self.pad)
194    }
195}
196
197impl Sink for Pacer {
198    fn consume(&mut self, buf: MediaBuffer) -> crate::error::Result<()> {
199        self.pending.push_back(buf);
200        while let Some(buf) = self.pending.pop_front() {
201            let ready = match &buf {
202                MediaBuffer::Packet(packet) => self.wait_for(packet.pts())?,
203                MediaBuffer::Video(frame) => self.wait_for(frame.pts())?,
204                MediaBuffer::Audio(frame) => self.wait_for(frame.pts())?,
205                MediaBuffer::Eos => true,
206            };
207            if !ready {
208                self.pending.push_front(buf);
209                return Ok(());
210            }
211            self.pad.push(buf)?;
212        }
213        Ok(())
214    }
215
216    fn control(&mut self, msg: ControlMsg) -> crate::error::Result<()> {
217        // Acknowledge the interrupt that made any in-flight wait return.
218        // Seek additionally resets both halves of pacing so the next frame
219        // establishes a fresh pts and wall-clock anchor.
220        self.interrupt_epoch = self.clock.interrupt_epoch();
221        match msg {
222            ControlMsg::Seek(_) => {
223                self.pending.clear();
224                self.first_pts = None;
225                self.clock.reset();
226            }
227            ControlMsg::Stop => self.pending.clear(),
228            ControlMsg::Pause | ControlMsg::Resume => {}
229        }
230        self.pad.control(msg)
231    }
232}
233
234#[cfg(test)]
235mod tests {
236    use super::*;
237    use std::{sync::mpsc, time::Duration};
238
239    fn packet(pts: i64) -> MediaBuffer {
240        let mut packet = ffmpeg::Packet::empty();
241        packet.set_pts(Some(pts));
242        MediaBuffer::Packet(Arc::new(packet))
243    }
244
245    #[test]
246    fn long_wait_returns_promptly_when_control_interrupts_it() {
247        let clock = Arc::new(Clock::new());
248        let mut pacer = Pacer::new("pacer", ffmpeg::Rational::new(1, 1), clock.clone()).unwrap();
249        assert!(
250            pacer.wait_for(Some(0)).unwrap(),
251            "first pts should establish the anchor"
252        );
253
254        let (started_tx, started_rx) = mpsc::channel();
255        let worker = thread::spawn(move || {
256            started_tx.send(()).expect("test receiver alive");
257            pacer.wait_for(Some(60))
258        });
259
260        started_rx.recv().expect("paced wait should start");
261        thread::sleep(Duration::from_millis(20));
262        clock.interrupt();
263
264        assert!(
265            !worker
266                .join()
267                .expect("paced wait should return")
268                .expect("interrupted wait is Ok(false), not an error"),
269            "an interrupted paced wait must return before its due time"
270        );
271    }
272
273    #[test]
274    fn pause_retains_interrupted_buffer_but_seek_and_stop_discard_it() {
275        let clock = Arc::new(Clock::new());
276        let mut pacer = Pacer::new("pacer", ffmpeg::Rational::new(1, 1), clock.clone()).unwrap();
277
278        clock.interrupt();
279        pacer.consume(packet(0)).expect("interrupted consume");
280        assert_eq!(pacer.pending.len(), 1);
281
282        pacer.control(ControlMsg::Pause).expect("pause");
283        assert_eq!(pacer.pending.len(), 1, "pause must retain the buffer");
284
285        pacer
286            .control(ControlMsg::Seek(Duration::ZERO))
287            .expect("seek");
288        assert!(pacer.pending.is_empty(), "seek must discard stale data");
289
290        clock.interrupt();
291        pacer.consume(packet(1)).expect("interrupted consume");
292        assert_eq!(pacer.pending.len(), 1);
293        pacer.control(ControlMsg::Stop).expect("stop");
294        assert!(pacer.pending.is_empty(), "stop must abandon pending data");
295    }
296
297    #[test]
298    fn new_rejects_an_invalid_time_base() {
299        let clock = Arc::new(Clock::new());
300        for rational in [
301            ffmpeg::Rational::new(0, 1),
302            ffmpeg::Rational::new(1, 0),
303            ffmpeg::Rational::new(-1, 1),
304            ffmpeg::Rational::new(1, -1),
305        ] {
306            assert!(
307                matches!(
308                    Pacer::new("pacer", rational, clock.clone()),
309                    Err(PacerError::InvalidTimeBase { .. })
310                ),
311                "expected {rational} to be rejected"
312            );
313        }
314    }
315
316    /// Regression test: a `pts` this far from `first_pts` used to overflow
317    /// `pts - first_pts` silently (a plain `-`) or let the buffer through
318    /// unpaced (an earlier `checked_sub` that swallowed the error). Now
319    /// it's a typed `PacerError` `consume` propagates via `?`, and — since
320    /// `Queue`/a pushing source both treat a `Sink::consume` failure as
321    /// "drop this one buffer, report on the bus, keep going" — a Pacer
322    /// that hits this on one buffer must still pace the next one normally.
323    #[test]
324    fn a_pathological_pts_jump_is_a_typed_error_not_silent_passthrough() {
325        let clock = Arc::new(Clock::new());
326        let mut pacer = Pacer::new("pacer", ffmpeg::Rational::new(1, 1), clock).unwrap();
327
328        assert!(pacer.consume(packet(-1)).is_ok(), "establishes first_pts");
329
330        let error = pacer
331            .consume(packet(i64::MAX))
332            .expect_err("pts far enough from first_pts to overflow the subtraction");
333        assert!(matches!(
334            error,
335            crate::Error::PacerError(PacerError::TimestampDeltaOverflow {
336                pts: i64::MAX,
337                first_pts: -1,
338            })
339        ));
340        assert!(
341            pacer.pending.is_empty(),
342            "the overflowing buffer must not get stuck in `pending`"
343        );
344
345        // The pacer itself must still be usable afterward: a `Some` result
346        // (not a further error) for an ordinary pts relative to the same
347        // `first_pts`.
348        assert!(pacer.wait_for(Some(0)).is_ok());
349    }
350}