Skip to main content

media_pp/elements/source/
file_demuxer.rs

1use std::{
2    collections::VecDeque,
3    path::Path,
4    sync::{
5        Arc,
6        atomic::{AtomicBool, AtomicI64, Ordering},
7    },
8    time::Duration,
9};
10
11use crate::pp_log::{PpLog, pp_debug, pp_error, pp_info};
12use ffmpeg_next::{self as ffmpeg, Rescale};
13use thiserror::Error as ThisError;
14
15use crate::{
16    buffer::MediaBuffer,
17    bus::{Bus, BusEvent},
18    contract::{MediaKind, OutputContract, PortContract},
19    control::{ControlReceiver, drain_control},
20    element::{Element, ElementType, Source, SourceElement, element_pp_log},
21    pad::SrcPad,
22};
23
24/// Errors specific to `FileDemuxer`. Converts into the crate-wide `Error`
25/// via `?` (see [`crate::error::Error`]).
26#[derive(Debug, ThisError)]
27pub enum FileDemuxError {
28    /// FFmpeg rejected opening, reading, or seeking the input container.
29    #[error("ffmpeg error: {0}")]
30    Ffmpeg(#[from] ffmpeg::Error),
31}
32
33/// Metadata about one stream in an opened container, reported up front so
34/// callers can decide what to build downstream before the pipeline runs.
35#[derive(Debug, Clone, Copy)]
36pub struct StreamInfo {
37    /// Zero-based stream index used by the matching source pad.
38    pub index: usize,
39    /// Media kind reported by the container, such as audio or video.
40    pub kind: ffmpeg::media::Type,
41}
42
43/// Runtime control for a [`FileDemuxer`], taken with
44/// [`FileDemuxer::looping_handle`] before the demuxer is moved into its
45/// pipeline.
46///
47/// Cheap to clone and safe to share: it holds one atomic flag and nothing
48/// else, so it keeps neither the demuxer, its file, nor its pipeline alive.
49/// No call blocks or does any work beyond that store. A call after the
50/// source has finished is simply never read.
51#[derive(Clone)]
52pub struct FileDemuxerHandle {
53    looping: Arc<AtomicBool>,
54    published_offset: Arc<AtomicI64>,
55}
56
57impl FileDemuxerHandle {
58    /// Whether reaching the end of the file starts it again instead of
59    /// ending the stream. Off unless this says otherwise.
60    ///
61    /// Read once per lap, at the end of the file — never mid-file. So
62    /// turning it off part way through means "play this lap out and then
63    /// finish", not "stop now", and the stream still ends with a real
64    /// `Eos` rather than being abandoned the way [`ControlMsg::Stop`]
65    /// abandons it. Turning it on part way through takes effect at the end
66    /// the source was already heading for.
67    ///
68    /// [`ControlMsg::Stop`]: crate::control::ControlMsg::Stop
69    pub fn set_looping(&self, looping: bool) {
70        self.looping.store(looping, Ordering::Relaxed);
71    }
72
73    /// What [`FileDemuxerHandle::set_looping`] last set.
74    pub fn is_looping(&self) -> bool {
75        self.looping.load(Ordering::Relaxed)
76    }
77
78    /// How far this source's output timeline has been carried past the
79    /// file's own — the sum of every lap already played.
80    ///
81    /// Zero until the first wrap, so a source that never loops never needs
82    /// this. What it is for is reading a timestamp *back*: subtract it from
83    /// a packet's or frame's timestamp and the result is a position in the
84    /// file, which is what a progress bar means by one. See
85    /// [`FileDemuxer`]'s own docs on why the two are not the same number.
86    ///
87    /// It moves once per lap, so a reader that samples it beside a timestamp
88    /// from the same moment can be one lap out for the instant either side of
89    /// a wrap. Nothing here can close that window, and a progress bar that is
90    /// wrong for one frame at the moment it jumps back to zero is not wrong
91    /// in a way anyone can see.
92    pub fn lap_offset(&self) -> Duration {
93        Duration::from_micros(self.published_offset.load(Ordering::Relaxed).max(0) as u64)
94    }
95}
96
97/// Demuxes a file, exposing one src pad per container stream (indexed the
98/// same way as `StreamInfo::index`). Linking a pad "selects" that stream;
99/// leaving it unlinked just drops its packets. Real demuxer I/O is
100/// blocking, so this is meant to be run as the pipeline's source thread.
101///
102/// Fan-out (e.g. routing video and audio to separate branches) needs no
103/// separate "Tee" element here — it's just a matter of linking more than
104/// one of these pads.
105///
106/// Set to loop through [`FileDemuxer::looping_handle`] and the end of the
107/// file rewinds to the start instead of ending the stream. Timestamps then
108/// keep climbing across the join rather than restarting: what a lap already
109/// reached is added to every later one, so a `Pacer` still paces, a muxer
110/// still sees its timestamps advance, and nothing downstream has to know a
111/// join happened. The consequence is that a looping source's timestamps are
112/// no longer positions *in the file* — one second into the third lap is at
113/// twice the file's length plus a second — and [`FileDemuxer::seek`] stays
114/// the way to speak in the file's own timeline.
115///
116/// [`FileDemuxer::seek`]: crate::element::SourceElement::seek
117pub struct FileDemuxer {
118    pp_log: PpLog,
119    name: Arc<str>,
120    input: ffmpeg::format::context::Input,
121    pads: Vec<SrcPad>,
122    /// Packets read but not yet delivered, in file order.
123    ///
124    /// `seek` puts one here: peeking a packet right after `Input::seek` is how
125    /// it learns where playback actually landed (see `seek`'s docs), and that
126    /// packet is real data that still has to be delivered.
127    ///
128    /// `run` also parks a packet here when its own pad cannot accept one yet.
129    /// A container interleaves every stream into one read cursor, so refusing
130    /// to read at all while a single pad is blocked stalls the streams that
131    /// *are* ready — during preroll that starves whichever branch has not yet
132    /// taken its sample, and the seek times out waiting for it. Holding the
133    /// blocked pad's packets keeps the cursor moving; per-pad order is what
134    /// matters and each pad's packets stay in the order they were read.
135    pending: VecDeque<(usize, ffmpeg::Rational, ffmpeg::Packet)>,
136    /// Total payload parked in `pending`.
137    pending_bytes: usize,
138    /// How many parked packets each pad owes, so a freshly read one never
139    /// overtakes them.
140    parked_per_pad: Vec<usize>,
141    /// Whether a preroll is running. Parking is only correct then: outside
142    /// one, a blocked pad is ordinary backpressure this source must wait on.
143    prerolling: bool,
144    /// Set through [`FileDemuxerHandle::set_looping`], read only where the
145    /// container runs out.
146    looping: Arc<AtomicBool>,
147    /// `loop_offset`, published for [`FileDemuxerHandle::lap_offset`].
148    ///
149    /// A copy rather than the field itself: the offset is read and written
150    /// once per packet on this thread, and making that an atomic to serve a
151    /// reader that looks a few times a second is the wrong way round. This
152    /// is stored only where the offset moves, which is once per lap.
153    published_offset: Arc<AtomicI64>,
154    /// How far this source's output timeline has been carried past the
155    /// file's own, in microseconds: the sum of every lap already played.
156    /// Zero until the first wrap, so a source that never loops emits the
157    /// file's timestamps untouched.
158    ///
159    /// Microseconds because one lap has to be one length for every stream.
160    /// Measuring each stream's own end separately would let audio and video
161    /// restart at different points and drift apart by that difference on
162    /// every lap.
163    loop_offset: i64,
164    /// The furthest into the file, in the same units, any packet read this
165    /// lap reaches — what `loop_offset` grows by at the next wrap.
166    ///
167    /// A running maximum that only the wrap resets. A seek backwards does
168    /// not un-deliver what already went downstream, so the lap stays as long
169    /// as its furthest packet; growing the offset by what was *played*
170    /// instead would drop the next lap on top of timestamps a muxer has
171    /// already written.
172    lap_end: i64,
173}
174
175impl FileDemuxer {
176    /// Opens the file and returns it alongside every stream it contains,
177    /// so the caller can inspect them (count, media type, ...) before
178    /// deciding which of `src_pads()` to link.
179    pub fn open(
180        name: impl Into<String>,
181        path: impl AsRef<Path>,
182    ) -> Result<(Self, Vec<StreamInfo>), FileDemuxError> {
183        let input = ffmpeg::format::input(&path)?;
184
185        let streams: Vec<StreamInfo> = input
186            .streams()
187            .map(|s| StreamInfo {
188                index: s.index(),
189                kind: s.parameters().medium(),
190            })
191            .collect();
192
193        let pads: Vec<SrcPad> = streams
194            .iter()
195            .map(|s| {
196                // Per stream, from the medium the container announced:
197                // both pads emit `MediaBuffer::Packet`, so only this tells
198                // an audio stream apart from a video one. A medium this
199                // crate does not model (subtitles, data) declares nothing
200                // and is left to the runtime check.
201                match MediaKind::packet_for(s.kind) {
202                    Some(kind) => SrcPad::with_contract(
203                        format!("src_{}", s.index),
204                        OutputContract::Fixed(PortContract::packet(kind)),
205                    ),
206                    None => SrcPad::new(format!("src_{}", s.index)),
207                }
208            })
209            .collect();
210
211        let name: Arc<str> = name.into().into();
212        let pp_log = element_pp_log(ElementType::FileDemuxer, &name, None);
213        pp_info!(
214            pp_log: &pp_log,
215            "opened: path={}, {} stream(s)",
216            path.as_ref().display(),
217            streams.len()
218        );
219        Ok((
220            Self {
221                name,
222                pp_log,
223                input,
224                parked_per_pad: vec![0; pads.len()],
225                prerolling: false,
226                pads,
227                pending: VecDeque::new(),
228                pending_bytes: 0,
229                looping: Arc::new(AtomicBool::new(false)),
230                published_offset: Arc::new(AtomicI64::new(0)),
231                loop_offset: 0,
232                lap_end: 0,
233            },
234            streams,
235        ))
236    }
237
238    /// The control endpoint for looping this file, valid for as long as the
239    /// demuxer runs — take it here, before moving the demuxer into its
240    /// pipeline, and keep it for as long as the loop is meant to be
241    /// switchable. See [`FileDemuxerHandle::set_looping`].
242    pub fn looping_handle(&self) -> FileDemuxerHandle {
243        FileDemuxerHandle {
244            looping: self.looping.clone(),
245            published_offset: self.published_offset.clone(),
246        }
247    }
248
249    /// Codec parameters for one of this file's streams — what you need to
250    /// construct a matching [`crate::elements::SwDecoder`] for it.
251    pub fn stream_parameters(&self, index: usize) -> Option<ffmpeg::codec::Parameters> {
252        self.stream(index).map(|s| s.parameters())
253    }
254
255    /// The unit decoded frame timestamps for this stream are expressed in —
256    /// what you need to construct a matching [`crate::elements::Pacer`] for
257    /// it.
258    pub fn stream_time_base(&self, index: usize) -> Option<ffmpeg::Rational> {
259        self.stream(index).map(|s| s.time_base())
260    }
261
262    fn stream(&self, index: usize) -> Option<ffmpeg::format::stream::Stream<'_>> {
263        self.input.streams().find(|s| s.index() == index)
264    }
265
266    /// Puts a freshly read packet on this source's output timeline, and
267    /// records how far into the file this lap has now reached.
268    ///
269    /// Called at each of the two places a packet is read out of the
270    /// container — `run`'s cursor and `seek`'s read-ahead — rather than
271    /// where they are delivered. Stamping a time base is idempotent and
272    /// `deliver_or_park` can do it to the same packet twice; shifting a
273    /// timestamp is not.
274    ///
275    /// Only a linked pad's stream counts towards the lap's length. An
276    /// unlinked one is dropped rather than delivered, so letting a longer
277    /// audio track nobody selected decide where the video restarts would
278    /// only open a gap at every join.
279    fn stamp_lap(
280        &mut self,
281        index: usize,
282        time_base: ffmpeg::Rational,
283        packet: &mut ffmpeg::Packet,
284    ) {
285        if let Some(start) = packet.pts().or_else(|| packet.dts())
286            && self.pads.get(index).is_some_and(SrcPad::is_linked)
287        {
288            // A packet carrying no duration of its own still ends after it
289            // starts, and one tick is the least that keeps the next lap's
290            // first timestamp past this one's rather than equal to it.
291            let end = start.saturating_add(packet.duration().max(1));
292            self.lap_end = self.lap_end.max(end.rescale(time_base, microseconds()));
293        }
294        if self.loop_offset == 0 {
295            return;
296        }
297        let shift = self.loop_offset.rescale(microseconds(), time_base);
298        packet.set_pts(packet.pts().map(|pts| pts.saturating_add(shift)));
299        packet.set_dts(packet.dts().map(|dts| dts.saturating_add(shift)));
300    }
301
302    /// Starts the file again: carries the output timeline past the lap that
303    /// just ended, then rewinds the container.
304    ///
305    /// Ordering matters. The offset moves first so that the read-ahead
306    /// packet `seek` parks — the new lap's first — is stamped onto the new
307    /// timeline like every packet after it.
308    fn wrap(&mut self) -> crate::error::Result<()> {
309        self.loop_offset = self.loop_offset.saturating_add(self.lap_end);
310        self.published_offset
311            .store(self.loop_offset, Ordering::Relaxed);
312        self.lap_end = 0;
313        let landed = self.seek(Duration::ZERO)?;
314        pp_debug!(
315            self,
316            "looped: restarted at {landed:?}, timeline now {}us past the file's own",
317            self.loop_offset
318        );
319        Ok(())
320    }
321
322    /// Pushes `item` if its pad can take one now, otherwise parks it.
323    ///
324    /// A downstream failure drops just that one packet — the same
325    /// "report, don't die" contract `Queue`'s worker gives a failing `Sink` —
326    /// rather than ending this whole source thread over it. `Pipeline::stop`
327    /// is how a caller who decides an error is fatal actually ends things.
328    fn deliver_or_park(
329        &mut self,
330        item: (usize, ffmpeg::Rational, ffmpeg::Packet),
331        bus: &Bus,
332    ) -> crate::error::Result<()> {
333        let (index, time_base, mut packet) = item;
334        // `AVCodecParameters` does not carry the container stream's timestamp
335        // unit, and FFmpeg does not guarantee that demuxers populate
336        // `AVPacket::time_base`. Stamping it here — the one place every packet
337        // leaves this source through, parked or not — means a packet held for
338        // a blocked pad arrives downstream describing itself the same way an
339        // immediately delivered one does.
340        packet.set_time_base(time_base);
341        if self.pads.get(index).is_none() {
342            // No pad for this stream: nobody selected it, so it is dropped
343            // rather than parked. Parking it would grow without bound.
344            return Ok(());
345        }
346        // Anything already parked for this pad was read first and must stay
347        // first. Overtaking it would hand a decoder its stream out of decode
348        // order.
349        let blocked = self.parked_per_pad[index] > 0 || !self.pads[index].ready_consume();
350        if blocked && self.prerolling {
351            self.park((index, time_base, packet));
352            return Ok(());
353        }
354        // Outside a preroll, a blocked pad is ordinary backpressure and the
355        // right answer is to wait on it: the push below blocks until the
356        // downstream `Queue` has room, which paces this whole source to the
357        // branch that is furthest behind. Holding the packet and reading on
358        // instead would let the source run away from playback and buffer the
359        // file here — measured at 67 MB parked within 1.5 s of paced playback,
360        // after which the backlog ceiling stopped the read cursor for *every*
361        // pad and starved the branches that were still keeping up.
362        self.push_to_pad(index, packet, bus);
363        Ok(())
364    }
365
366    fn push_to_pad(&mut self, index: usize, packet: ffmpeg::Packet, bus: &Bus) {
367        if let Err(error) = self.pads[index].push(MediaBuffer::Packet(Arc::new(packet))) {
368            bus.post(
369                &self.pp_log,
370                BusEvent::Error {
371                    element_type: ElementType::FileDemuxer,
372                    name: self.name.clone(),
373                    error,
374                },
375            );
376        }
377    }
378
379    /// Holds one packet back, keeping the totals that bound the backlog in
380    /// step with the queue. The one place anything enters `pending`, so it is
381    /// also where the stream time base is guaranteed onto a packet that will
382    /// be delivered later — `seek` parks its read-ahead packet directly.
383    fn park(&mut self, item: (usize, ffmpeg::Rational, ffmpeg::Packet)) {
384        let (index, time_base, mut packet) = item;
385        packet.set_time_base(time_base);
386        self.pending_bytes = self.pending_bytes.saturating_add(packet.size());
387        self.parked_per_pad[index] += 1;
388        self.pending.push_back((index, time_base, packet));
389    }
390
391    /// Delivers every parked packet whose pad can take one, oldest first,
392    /// leaving the rest in place.
393    ///
394    /// Once a pad has held one packet back, every later packet of *that* pad
395    /// is held too, even if the pad reports itself ready again a moment later.
396    /// Readiness here is a `Queue`'s "not full", which its worker changes on
397    /// another thread while this loop runs — so re-asking per packet would let
398    /// the second overtake the first the instant a slot opened, and a decoder
399    /// handed its stream out of order produces garbage and a flood of
400    /// `co located POCs unavailable`. Other pads are unaffected: keeping the
401    /// read cursor moving for them is the whole reason parking exists.
402    fn drain_pending(&mut self, bus: &Bus) -> crate::error::Result<()> {
403        // The ordinary case, now that holding back is scoped to preroll: this
404        // runs once per packet read, so it must not allocate to find nothing.
405        if self.pending.is_empty() {
406            return Ok(());
407        }
408        let mut deferred = VecDeque::with_capacity(self.pending.len());
409        let mut blocked = vec![false; self.pads.len()];
410        while let Some((index, time_base, packet)) = self.pending.pop_front() {
411            // Once the preroll that justified holding these is over, waiting
412            // on the pad is what empties the backlog; deferring again would
413            // leave it parked for as long as playback keeps that pad busy.
414            let hold = self.prerolling && (blocked[index] || !self.pads[index].ready_consume());
415            if hold {
416                blocked[index] = true;
417                deferred.push_back((index, time_base, packet));
418                continue;
419            }
420            self.pending_bytes = self.pending_bytes.saturating_sub(packet.size());
421            self.parked_per_pad[index] -= 1;
422            self.push_to_pad(index, packet, bus);
423        }
424        self.pending = deferred;
425        Ok(())
426    }
427
428    /// Whether reading another packet would only deepen the parked backlog.
429    ///
430    /// Not a tuning knob: a branch that is briefly behind parks a handful of
431    /// packets and clears them within milliseconds. These ceilings only bound
432    /// a pad that has stopped accepting altogether, so the read cursor cannot
433    /// pull an arbitrary amount of the file into memory waiting for it. Both
434    /// are needed — a few large keyframes reach the byte limit at a packet
435    /// count that would never trip on its own.
436    fn pending_blocked(&mut self) -> bool {
437        if !self.prerolling {
438            // Nothing is parked outside a preroll, and a blocked pad is
439            // waited on rather than skipped.
440            return false;
441        }
442        const MAX_PENDING_PACKETS: usize = 4_096;
443        const MAX_PENDING_BYTES: usize = 64 * 1024 * 1024;
444
445        self.pending.len() >= MAX_PENDING_PACKETS
446            || self.pending_bytes >= MAX_PENDING_BYTES
447            || !self.pads.iter_mut().any(SrcPad::ready_consume)
448    }
449}
450
451impl Element for FileDemuxer {
452    fn name(&self) -> Arc<str> {
453        self.name.clone()
454    }
455
456    fn element_type(&self) -> ElementType {
457        ElementType::FileDemuxer
458    }
459
460    fn pp_log(&self) -> &PpLog {
461        &self.pp_log
462    }
463
464    fn pp_log_mut(&mut self) -> &mut PpLog {
465        &mut self.pp_log
466    }
467}
468
469impl Source for FileDemuxer {
470    fn src_pads(&mut self) -> &mut [SrcPad] {
471        &mut self.pads
472    }
473}
474
475impl SourceElement for FileDemuxer {
476    fn is_live(&self) -> bool {
477        false
478    }
479
480    fn is_seekable(&self) -> bool {
481        true
482    }
483
484    fn run(&mut self, control: &ControlReceiver, bus: &Bus) -> crate::error::Result<()> {
485        pp_info!(self, "started");
486        // Deliberately re-creates `self.input.packets()` fresh every
487        // iteration (cheap — it's just a short-lived wrapper, not a
488        // stateful cursor of its own) instead of holding one `for` loop's
489        // iterator across the whole function, the way this used to read.
490        // That iterator borrows `input` for as long as it's alive; `Seek`
491        // needs `drain_control` to be able to call `self.seek()` — a
492        // *second* mutable borrow of `input` — in between reads, which a
493        // single loop-spanning iterator would rule out.
494        loop {
495            if drain_control(control, self, bus)?.stopped {
496                // Stop: abandon in place, no final Eos.
497                pp_info!(self, "stopped");
498                return Ok(());
499            }
500            // Deliver whatever is parked and now accepted, oldest first.
501            // Skipping a still-blocked pad's entry to reach a later one is
502            // safe: only each pad's own order has to hold, and this preserves
503            // it because entries for one pad are never reordered against each
504            // other.
505            self.drain_pending(bus)?;
506            // Read on unless everything is blocked, or the parked backlog has
507            // grown past what one branch briefly falling behind can explain.
508            // Sleeping is the only option then: the container cannot hand out
509            // a different stream's packet without reading this one.
510            if self.pending_blocked() {
511                std::thread::sleep(Duration::from_millis(1));
512                continue;
513            }
514            let next = self
515                .input
516                .packets()
517                .next()
518                .map(|(s, p)| (s.index(), s.time_base(), p));
519            let Some((index, time_base, mut packet)) = next else {
520                if !self.pending.is_empty() {
521                    // Nothing left to read, but a blocked pad still owes
522                    // delivery.
523                    std::thread::sleep(Duration::from_millis(1));
524                    continue;
525                }
526                // The end of the file, and the one place the loop flag is
527                // read: a change made mid-file lands here, at the end the
528                // source was already heading for.
529                if self.looping.load(Ordering::Relaxed) {
530                    match self.wrap() {
531                        Ok(()) => continue,
532                        // A file that cannot be rewound cannot be looped,
533                        // but it has been fully read — so report why the
534                        // loop stopped and end the stream properly, rather
535                        // than failing a source that delivered everything
536                        // it was asked for.
537                        Err(error) => bus.post(
538                            &self.pp_log,
539                            BusEvent::Error {
540                                element_type: ElementType::FileDemuxer,
541                                name: self.name.clone(),
542                                error,
543                            },
544                        ),
545                    }
546                }
547                break;
548            };
549            self.stamp_lap(index, time_base, &mut packet);
550            // `deliver_or_park` stamps the stream time base; every packet
551            // leaves this source through it, parked or not.
552            self.deliver_or_park((index, time_base, packet), bus)?;
553        }
554        for pad in self.pads.iter_mut() {
555            pad.push_eos(&self.pp_log)?;
556        }
557        pp_info!(self, "event=eos phase=source_completed outcome=ok");
558        Ok(())
559    }
560
561    fn on_control(&mut self, msg: &crate::control::ControlMsg) {
562        use crate::control::ControlMsg;
563        match msg {
564            // Those packets were read from the timeline being left behind,
565            // and this source is the only place they exist — every downstream
566            // stage discards its own on the same `Flush`, so releasing these
567            // afterwards would be the one way old media could reach a decoder
568            // that had already reset for the new position.
569            ControlMsg::Flush => {
570                self.pending.clear();
571                self.pending_bytes = 0;
572                self.parked_per_pad.fill(0);
573            }
574            // Holding a blocked pad's packets is only correct while a preroll
575            // is running; see `deliver_or_park`.
576            ControlMsg::Preroll(_) => self.prerolling = true,
577            ControlMsg::Pause | ControlMsg::Resume | ControlMsg::Stop => self.prerolling = false,
578            ControlMsg::CheckSeek(_) | ControlMsg::Seek(_) => {}
579        }
580    }
581
582    fn seek(&mut self, target: Duration) -> crate::error::Result<Duration> {
583        // `Input::seek` takes microseconds (`AV_TIME_BASE` units) when
584        // seeking the whole container (stream index -1, which is what it
585        // uses internally) rather than one specific stream — an unbounded
586        // range (`..`) just means "as close to `ts` as ffmpeg can manage",
587        // no extra min/max constraint. In practice that means *backward*
588        // to the nearest keyframe at or before `target`: never forward,
589        // and never onto a non-keyframe, since either would leave nothing
590        // downstream can decode/remux from. A sparse-keyframe file can
591        // make that keyframe well before `target` — e.g. a single
592        // 10-second file with keyframes only at 0s and 8.3s means every
593        // `target` under 8.3s lands back at 0s.
594        let ts = target.as_micros().min(i64::MAX as u128) as i64;
595        self.input.seek(ts, ..).inspect_err(|error| {
596            pp_error!(self, "seek to {target:?} failed: {error}");
597        })?;
598
599        // `avformat_seek_file` only reports success/failure, not where it
600        // landed — the one way to find out is to read the next packet and
601        // look at its own timestamp. That packet is real data (not a
602        // probe to throw away), so it's stashed in `pending` for `run`'s
603        // next iteration instead of being dropped here.
604        let landed_packet = self
605            .input
606            .packets()
607            .next()
608            .map(|(stream, packet)| (stream.index(), stream.time_base(), packet));
609        match landed_packet {
610            Some((index, time_base, mut packet)) => {
611                // Read before `stamp_lap` moves it: where a seek landed is a
612                // position in the *file*, which a loop's accumulated offset
613                // must not be added to.
614                let landed = packet
615                    .pts()
616                    .or_else(|| packet.dts())
617                    .map(|ts| ts_to_duration(ts, time_base))
618                    .unwrap_or(Duration::ZERO);
619                self.stamp_lap(index, time_base, &mut packet);
620                self.park((index, time_base, packet));
621                Ok(landed)
622            }
623            // Nothing left to read right after seeking (`target` at/past
624            // EOF) — there's no packet to learn a real position from, so
625            // just report the request back as-is.
626            None => Ok(target),
627        }
628    }
629}
630
631/// The unit a lap's length is kept in, so it can be one length for every
632/// stream. Also what `Input::seek` takes, which is why the wrap needs no
633/// conversion of its own.
634///
635/// A hardcoded constant, not external input, so there is nothing to
636/// validate.
637fn microseconds() -> ffmpeg::Rational {
638    ffmpeg::Rational::new(1, 1_000_000)
639}
640
641fn ts_to_duration(ts: i64, time_base: ffmpeg::Rational) -> Duration {
642    let secs = ts as f64 * f64::from(time_base.numerator()) / f64::from(time_base.denominator());
643    Duration::from_secs_f64(secs.max(0.0))
644}
645
646#[cfg(test)]
647mod tests {
648    use std::sync::Mutex;
649    use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
650
651    use super::*;
652    use crate::control;
653    use crate::test_support::try_test_video;
654
655    struct CountingSink {
656        pp_log: PpLog,
657        count: Arc<AtomicUsize>,
658        saw_eos: Arc<AtomicBool>,
659        expected_time_base: ffmpeg::Rational,
660        time_base_matches: Arc<AtomicBool>,
661    }
662
663    impl Element for CountingSink {
664        fn name(&self) -> Arc<str> {
665            "counting-sink".into()
666        }
667
668        fn element_type(&self) -> ElementType {
669            ElementType::Other
670        }
671
672        fn pp_log(&self) -> &PpLog {
673            &self.pp_log
674        }
675
676        fn pp_log_mut(&mut self) -> &mut PpLog {
677            &mut self.pp_log
678        }
679    }
680
681    impl crate::element::Sink for CountingSink {
682        fn consume(&mut self, buf: MediaBuffer) -> crate::error::Result<()> {
683            match buf {
684                MediaBuffer::Eos => self.saw_eos.store(true, Ordering::SeqCst),
685                MediaBuffer::Packet(packet) => {
686                    if packet.time_base() != self.expected_time_base {
687                        self.time_base_matches.store(false, Ordering::SeqCst);
688                    }
689                    self.count.fetch_add(1, Ordering::SeqCst);
690                }
691                _ => {}
692            }
693            Ok(())
694        }
695
696        fn control(&mut self, _msg: crate::control::ControlMsg) -> crate::error::Result<()> {
697            Ok(())
698        }
699    }
700
701    #[test]
702    fn open_reports_stream_parameters_for_a_valid_index_and_none_out_of_range() {
703        let Some(path) = try_test_video() else { return };
704        let (demuxer, streams) = FileDemuxer::open("demux", &path).expect("open test video");
705        let video = streams
706            .iter()
707            .find(|s| s.kind == ffmpeg::media::Type::Video)
708            .expect("test video has a video stream");
709
710        assert!(demuxer.stream_parameters(video.index).is_some());
711        assert!(demuxer.stream_time_base(video.index).is_some());
712
713        let out_of_range = streams.len() + 1;
714        assert!(
715            demuxer.stream_parameters(out_of_range).is_none(),
716            "an out-of-range stream index must report nothing, not panic"
717        );
718        assert!(demuxer.stream_time_base(out_of_range).is_none());
719    }
720
721    /// Follows the output timeline across a loop's join, and switches the
722    /// loop off once the file has been through once so `run` finishes
723    /// instead of going round forever.
724    struct LoopSink {
725        pp_log: PpLog,
726        count: Arc<AtomicUsize>,
727        saw_eos: Arc<AtomicBool>,
728        /// Cleared the first time a decode timestamp goes backwards.
729        climbing: Arc<AtomicBool>,
730        last_dts: Arc<Mutex<Option<i64>>>,
731        /// How many packets one pass of this stream delivers.
732        lap: usize,
733        handle: FileDemuxerHandle,
734    }
735
736    impl Element for LoopSink {
737        fn name(&self) -> Arc<str> {
738            "loop-sink".into()
739        }
740
741        fn element_type(&self) -> ElementType {
742            ElementType::Other
743        }
744
745        fn pp_log(&self) -> &PpLog {
746            &self.pp_log
747        }
748
749        fn pp_log_mut(&mut self) -> &mut PpLog {
750            &mut self.pp_log
751        }
752    }
753
754    impl crate::element::Sink for LoopSink {
755        fn consume(&mut self, buf: MediaBuffer) -> crate::error::Result<()> {
756            match buf {
757                MediaBuffer::Eos => self.saw_eos.store(true, Ordering::SeqCst),
758                MediaBuffer::Packet(packet) => {
759                    if let Some(dts) = packet.dts().or_else(|| packet.pts()) {
760                        let mut last = self.last_dts.lock().unwrap();
761                        if last.is_some_and(|last| dts < last) {
762                            self.climbing.store(false, Ordering::SeqCst);
763                        }
764                        *last = Some(dts);
765                    }
766                    // Past a whole pass of the file: the source is into its
767                    // second lap, so let that one play out and then end.
768                    if self.count.fetch_add(1, Ordering::SeqCst) + 1 > self.lap {
769                        self.handle.set_looping(false);
770                    }
771                }
772                _ => {}
773            }
774            Ok(())
775        }
776
777        fn control(&mut self, _msg: crate::control::ControlMsg) -> crate::error::Result<()> {
778            Ok(())
779        }
780    }
781
782    /// The index of this file's video stream, and the time base its packets
783    /// carry.
784    fn video_stream(demuxer: &FileDemuxer, streams: &[StreamInfo]) -> (usize, ffmpeg::Rational) {
785        let index = streams
786            .iter()
787            .find(|s| s.kind == ffmpeg::media::Type::Video)
788            .expect("test video has a video stream")
789            .index;
790        let time_base = demuxer
791            .stream_time_base(index)
792            .expect("video stream has a time base");
793        (index, time_base)
794    }
795
796    /// How many packets one pass of this file's video stream delivers, so a
797    /// test can tell a second lap has begun without assuming anything about
798    /// the fixture.
799    fn packets_in_one_pass(path: impl AsRef<Path>) -> usize {
800        let (mut demuxer, streams) = FileDemuxer::open("demux", path).expect("open test video");
801        let (index, expected_time_base) = video_stream(&demuxer, &streams);
802        let count = Arc::new(AtomicUsize::new(0));
803        demuxer.src_pads()[index].link(Box::new(CountingSink {
804            count: count.clone(),
805            saw_eos: Arc::new(AtomicBool::new(false)),
806            expected_time_base,
807            time_base_matches: Arc::new(AtomicBool::new(true)),
808            pp_log: element_pp_log(ElementType::Other, "counting-sink", None),
809        }));
810        let (bus, _bus_rx) = Bus::new();
811        let (_tx, rx) = control::channel();
812        demuxer.run(&rx, &bus).expect("run must reach eos cleanly");
813        count.load(Ordering::SeqCst)
814    }
815
816    /// Looping puts the start of the file where its end was, and the
817    /// timestamps that come out keep climbing across that join instead of
818    /// restarting at zero.
819    ///
820    /// That is the whole point of the offset. A `Pacer` downstream anchors
821    /// on the first timestamp it sees and waits for each later one to come
822    /// due; hand it a second lap starting back at zero and every frame of it
823    /// is already overdue, so the lap is emitted as fast as it can be read
824    /// rather than played. A muxer refuses it outright.
825    ///
826    /// Also covers when the flag is read: it is switched off part way into
827    /// the second lap, and that lap still plays out and ends with a real
828    /// `Eos`.
829    #[test]
830    fn looping_restarts_the_file_and_carries_the_timeline_past_the_join() {
831        let Some(path) = try_test_video() else { return };
832        let lap = packets_in_one_pass(&path);
833        assert!(lap > 0, "the fixture must deliver something to loop");
834
835        let (mut demuxer, streams) = FileDemuxer::open("demux", &path).expect("open test video");
836        let (index, _) = video_stream(&demuxer, &streams);
837        let handle = demuxer.looping_handle();
838        assert!(
839            !handle.is_looping(),
840            "a file plays once unless asked not to"
841        );
842        handle.set_looping(true);
843
844        let count = Arc::new(AtomicUsize::new(0));
845        let saw_eos = Arc::new(AtomicBool::new(false));
846        let climbing = Arc::new(AtomicBool::new(true));
847        demuxer.src_pads()[index].link(Box::new(LoopSink {
848            count: count.clone(),
849            saw_eos: saw_eos.clone(),
850            climbing: climbing.clone(),
851            last_dts: Arc::new(Mutex::new(None)),
852            lap,
853            handle: handle.clone(),
854            pp_log: element_pp_log(ElementType::Other, "loop-sink", None),
855        }));
856
857        let (bus, bus_rx) = Bus::new();
858        let (_tx, rx) = control::channel();
859        demuxer
860            .run(&rx, &bus)
861            .expect("run must reach eos cleanly, not error");
862
863        assert!(
864            count.load(Ordering::SeqCst) > lap,
865            "the end of the file must start it again, not end the stream"
866        );
867        assert!(
868            climbing.load(Ordering::SeqCst),
869            "timestamps must not fall back to the file's own at the join"
870        );
871        assert!(
872            saw_eos.load(Ordering::SeqCst),
873            "switching looping off must end the lap it is in with an Eos"
874        );
875        // What a reader needs to turn one of those climbing timestamps back
876        // into a position in the file. It only moves at a wrap, so a run that
877        // wrapped has one and a run that did not has zero.
878        assert!(
879            handle.lap_offset() > Duration::ZERO,
880            "a lap that has been stepped over must be reported"
881        );
882        drop(bus);
883        assert!(
884            bus_rx.iter().all(|e| !matches!(e, BusEvent::Error { .. })),
885            "looping a well-formed file must not report any errors"
886        );
887    }
888
889    /// Regression test for how far a wrap carries the timeline. It has to be
890    /// how far into the file the lap *reached*, not how much of it was
891    /// played: a seek backwards does not un-deliver the packets that already
892    /// went downstream, so a shorter step would drop the next lap on top of
893    /// timestamps a muxer has already written.
894    #[test]
895    fn a_backward_seek_leaves_the_lap_as_long_as_its_furthest_packet() {
896        let Some(path) = try_test_video() else { return };
897        let (mut demuxer, streams) = FileDemuxer::open("demux", &path).expect("open test video");
898
899        // Every pad linked, because only a linked pad's stream counts
900        // towards the lap — and `seek` parks whichever stream's packet it
901        // happens to read first.
902        let time_bases: Vec<ffmpeg::Rational> = (0..streams.len())
903            .map(|index| {
904                demuxer
905                    .stream_time_base(index)
906                    .expect("every stream has a time base")
907            })
908            .collect();
909        for (index, expected_time_base) in time_bases.into_iter().enumerate() {
910            demuxer.src_pads()[index].link(Box::new(CountingSink {
911                count: Arc::new(AtomicUsize::new(0)),
912                saw_eos: Arc::new(AtomicBool::new(false)),
913                expected_time_base,
914                time_base_matches: Arc::new(AtomicBool::new(true)),
915                pp_log: element_pp_log(ElementType::Other, "counting-sink", None),
916            }));
917        }
918
919        demuxer
920            .seek(Duration::ZERO)
921            .expect("seek to the start of the test video");
922        let at_start = demuxer.lap_end;
923
924        // Half way in, so the keyframe this lands on is somewhere past the
925        // first one for any file that has more than one — which is what the
926        // guard below checks rather than assumes.
927        let half = Duration::from_micros((demuxer.input.duration().max(0) / 2) as u64);
928        demuxer
929            .seek(half)
930            .expect("seek half way into the test video");
931        let reached = demuxer.lap_end;
932        if reached <= at_start {
933            // Nothing in this fixture is reachable past its own start, so
934            // there is no reach for a seek back to lose.
935            return;
936        }
937
938        demuxer
939            .seek(Duration::ZERO)
940            .expect("seek back to the start of the test video");
941
942        assert_eq!(
943            demuxer.lap_end, reached,
944            "going back must not shorten the lap the next wrap steps over"
945        );
946    }
947
948    /// Drives `FileDemuxer::run` directly (no `Pipeline`) to prove the
949    /// basic contract on its own: every packet on a linked pad's stream
950    /// arrives, and running off the end of the file delivers a final
951    /// `Eos` rather than just stopping silently.
952    #[test]
953    fn run_delivers_every_packet_on_a_linked_pad_then_eos() {
954        let Some(path) = try_test_video() else { return };
955        let (mut demuxer, streams) = FileDemuxer::open("demux", &path).expect("open test video");
956        let video = streams
957            .iter()
958            .find(|s| s.kind == ffmpeg::media::Type::Video)
959            .expect("test video has a video stream");
960
961        let count = Arc::new(AtomicUsize::new(0));
962        let saw_eos = Arc::new(AtomicBool::new(false));
963        let time_base_matches = Arc::new(AtomicBool::new(true));
964        let expected_time_base = demuxer
965            .stream_time_base(video.index)
966            .expect("video stream has a time base");
967        demuxer.src_pads()[video.index].link(Box::new(CountingSink {
968            count: count.clone(),
969            saw_eos: saw_eos.clone(),
970            expected_time_base,
971            time_base_matches: time_base_matches.clone(),
972            pp_log: element_pp_log(ElementType::Other, "counting-sink", None),
973        }));
974
975        let (bus, bus_rx) = Bus::new();
976        let (_tx, rx) = control::channel();
977        demuxer
978            .run(&rx, &bus)
979            .expect("run must reach eos cleanly, not error");
980
981        assert!(
982            count.load(Ordering::SeqCst) > 0,
983            "expected at least one packet delivered to the linked pad"
984        );
985        assert!(
986            saw_eos.load(Ordering::SeqCst),
987            "expected an Eos once the file is exhausted"
988        );
989        assert!(
990            time_base_matches.load(Ordering::SeqCst),
991            "every delivered packet must carry its stream time base"
992        );
993        drop(bus);
994        assert!(
995            bus_rx.iter().all(|e| !matches!(e, BusEvent::Error { .. })),
996            "run must not report any errors demuxing a well-formed file"
997        );
998    }
999
1000    #[test]
1001    fn seek_read_ahead_packet_carries_its_stream_time_base_when_delivered() {
1002        let Some(path) = try_test_video() else { return };
1003        let (mut demuxer, _) = FileDemuxer::open("demux", &path).expect("open test video");
1004
1005        demuxer
1006            .seek(Duration::from_secs(1))
1007            .expect("seek within the test video");
1008        let (pending_index, expected_time_base, _) = demuxer
1009            .pending
1010            .front()
1011            .expect("seek must retain the first packet at or after the target");
1012        let pending_index = *pending_index;
1013        let expected_time_base = *expected_time_base;
1014
1015        let count = Arc::new(AtomicUsize::new(0));
1016        let saw_eos = Arc::new(AtomicBool::new(false));
1017        let time_base_matches = Arc::new(AtomicBool::new(true));
1018        demuxer.src_pads()[pending_index].link(Box::new(CountingSink {
1019            count: count.clone(),
1020            saw_eos,
1021            expected_time_base,
1022            time_base_matches: time_base_matches.clone(),
1023            pp_log: element_pp_log(ElementType::Other, "counting-sink", None),
1024        }));
1025
1026        let (bus, _bus_rx) = Bus::new();
1027        let (_tx, rx) = control::channel();
1028        demuxer
1029            .run(&rx, &bus)
1030            .expect("run after seek must reach eos cleanly");
1031
1032        assert!(
1033            count.load(Ordering::SeqCst) > 0,
1034            "the packet retained by seek must be delivered"
1035        );
1036        assert!(
1037            time_base_matches.load(Ordering::SeqCst),
1038            "the packet retained by seek must carry its stream time base"
1039        );
1040    }
1041
1042    /// A seek starts a new timeline. Anything the demuxer had already read and
1043    /// parked for a blocked pad belongs to the old one, so delivering it after the
1044    /// reposition feeds pre-seek packets to a decoder whose reference state was
1045    /// just flushed — corrupt output, and a stream of `co located POCs
1046    /// unavailable` from libavcodec.
1047    #[test]
1048    fn seeking_discards_packets_parked_before_the_jump() {
1049        let Some(path) = try_test_video() else { return };
1050        let (mut demuxer, streams) = FileDemuxer::open("demux", &path).expect("open test video");
1051        let video = streams
1052            .iter()
1053            .find(|stream| stream.kind == ffmpeg::media::Type::Video)
1054            .expect("test video has a video stream");
1055        let time_base = demuxer
1056            .stream_time_base(video.index)
1057            .expect("video stream disappeared");
1058
1059        // Read a little of the start into the parked queue by hand, the way `run`
1060        // does when a pad cannot accept a packet yet.
1061        let mut input = ffmpeg::format::input(&path).expect("second handle");
1062        let mut parked = 0usize;
1063        for (stream, packet) in input.packets() {
1064            if stream.index() != video.index {
1065                continue;
1066            }
1067            demuxer.park((stream.index(), time_base, packet));
1068            parked += 1;
1069            if parked == 4 {
1070                break;
1071            }
1072        }
1073        assert_eq!(parked, 4, "the fixture must have packets to park");
1074
1075        // The order a pipeline uses: Flush marks the timeline boundary, Seek
1076        // then moves the cursor and reads one packet ahead to learn where it
1077        // landed. Counting rather than comparing timestamps, because a seek
1078        // that lands back at the start legitimately re-reads a packet with
1079        // the same pts as one of the discarded ones.
1080        demuxer.on_control(&crate::control::ControlMsg::Flush);
1081        demuxer
1082            .seek(Duration::from_secs(3))
1083            .expect("seek within the test video");
1084
1085        assert_eq!(
1086            demuxer.pending.len(),
1087            1,
1088            "only the seek's own read-ahead packet may survive the flush"
1089        );
1090        let counted: usize = demuxer
1091            .pending
1092            .iter()
1093            .map(|(_, _, packet)| packet.size())
1094            .sum();
1095        assert_eq!(
1096            demuxer.pending_bytes, counted,
1097            "the parked byte total must stay in step with the queue"
1098        );
1099    }
1100
1101    /// Reports "full" until asked a given number of times, then reports room
1102    /// again — a `Queue` whose worker frees a slot partway through a drain.
1103    /// Readiness flipping *during* the loop is the real behaviour: the worker
1104    /// runs on its own thread, so the answer is not stable across the packets
1105    /// of one pass.
1106    struct FlipToReadySink {
1107        refusals_left: Arc<AtomicUsize>,
1108        seen: Arc<Mutex<Vec<i64>>>,
1109        pp_log: PpLog,
1110    }
1111
1112    impl Element for FlipToReadySink {
1113        fn name(&self) -> Arc<str> {
1114            "flip-to-ready".into()
1115        }
1116        fn element_type(&self) -> ElementType {
1117            ElementType::Other
1118        }
1119        fn pp_log(&self) -> &PpLog {
1120            &self.pp_log
1121        }
1122        fn pp_log_mut(&mut self) -> &mut PpLog {
1123            &mut self.pp_log
1124        }
1125    }
1126
1127    impl crate::element::Sink for FlipToReadySink {
1128        fn ready_consume(&mut self) -> bool {
1129            let left = self.refusals_left.load(Ordering::SeqCst);
1130            if left > 0 {
1131                self.refusals_left.store(left - 1, Ordering::SeqCst);
1132                return false;
1133            }
1134            true
1135        }
1136
1137        fn consume(&mut self, buf: MediaBuffer) -> crate::error::Result<()> {
1138            if let MediaBuffer::Packet(packet) = &buf
1139                && let Some(pts) = packet.pts()
1140            {
1141                self.seen.lock().unwrap().push(pts);
1142            }
1143            Ok(())
1144        }
1145
1146        fn control(&mut self, _msg: crate::control::ControlMsg) -> crate::error::Result<()> {
1147            Ok(())
1148        }
1149    }
1150
1151    /// A pad that says "full" for the first packet of a drain and "ready" for
1152    /// the next must not let the second overtake the first.
1153    ///
1154    /// This is what a `Queue` does: its worker frees a slot on another thread
1155    /// while the drain loop is running, so asking again per packet gives a
1156    /// different answer mid-pass. Re-asking let the later packet through
1157    /// first, handing the decoder its stream out of decode order — libavcodec
1158    /// answers with a flood of `co located POCs unavailable`, and the picture
1159    /// is wrong. Reproduced by launching `av_playback` with no seek at all:
1160    /// 45 warnings against 0 before this branch.
1161    #[test]
1162    fn a_pad_that_becomes_ready_mid_drain_does_not_reorder_its_stream() {
1163        let Some(path) = try_test_video() else { return };
1164        let (mut demuxer, streams) = FileDemuxer::open("demux", &path).expect("open");
1165        let video = streams
1166            .iter()
1167            .find(|stream| stream.kind == ffmpeg::media::Type::Video)
1168            .expect("video stream");
1169        let time_base = demuxer.stream_time_base(video.index).expect("time base");
1170
1171        // Refuse once: the first packet of the drain is held back, and every
1172        // later one finds the pad ready again.
1173        let seen = Arc::new(Mutex::new(Vec::new()));
1174        demuxer.pads[video.index].link(Box::new(FlipToReadySink {
1175            refusals_left: Arc::new(AtomicUsize::new(1)),
1176            seen: Arc::clone(&seen),
1177            pp_log: element_pp_log(ElementType::Other, "flip-to-ready", None),
1178        }));
1179
1180        let mut input = ffmpeg::format::input(&path).expect("second handle");
1181        let mut order = Vec::new();
1182        for (stream, packet) in input.packets() {
1183            if stream.index() != video.index {
1184                continue;
1185            }
1186            order.push(packet.pts().expect("fixture packets carry a pts"));
1187            demuxer.park((stream.index(), time_base, packet));
1188            if order.len() == 4 {
1189                break;
1190            }
1191        }
1192
1193        let (bus, _bus_rx) = Bus::new();
1194        demuxer.drain_pending(&bus).expect("first drain");
1195        demuxer.drain_pending(&bus).expect("second drain");
1196
1197        assert_eq!(
1198            *seen.lock().unwrap(),
1199            order,
1200            "a packet overtook one still parked for the same pad"
1201        );
1202    }
1203
1204    /// Accepts a bounded number of buffers, then blocks — the shape of a
1205    /// `Queue` filling up mid-drain.
1206    struct BoundedSink {
1207        remaining: Arc<AtomicUsize>,
1208        seen: Arc<Mutex<Vec<i64>>>,
1209        pp_log: PpLog,
1210    }
1211
1212    impl Element for BoundedSink {
1213        fn name(&self) -> Arc<str> {
1214            "bounded".into()
1215        }
1216        fn element_type(&self) -> ElementType {
1217            ElementType::Other
1218        }
1219        fn pp_log(&self) -> &PpLog {
1220            &self.pp_log
1221        }
1222        fn pp_log_mut(&mut self) -> &mut PpLog {
1223            &mut self.pp_log
1224        }
1225    }
1226
1227    impl crate::element::Sink for BoundedSink {
1228        fn ready_consume(&mut self) -> bool {
1229            self.remaining.load(Ordering::SeqCst) > 0
1230        }
1231
1232        fn consume(&mut self, buf: MediaBuffer) -> crate::error::Result<()> {
1233            if let MediaBuffer::Packet(packet) = &buf
1234                && let Some(pts) = packet.pts()
1235            {
1236                self.seen.lock().unwrap().push(pts);
1237            }
1238            self.remaining.fetch_sub(1, Ordering::SeqCst);
1239            Ok(())
1240        }
1241
1242        fn control(&mut self, _msg: crate::control::ControlMsg) -> crate::error::Result<()> {
1243            Ok(())
1244        }
1245    }
1246
1247    /// A stream's packets must reach its pad in the order they were read.
1248    /// Parking exists so a blocked pad does not stall the read cursor; it must
1249    /// not reorder that pad's own packets, or the decoder is handed frames out
1250    /// of decode order and produces `co located POCs unavailable` and garbage.
1251    #[test]
1252    fn parked_packets_keep_their_per_pad_order_when_the_pad_blocks_mid_drain() {
1253        let Some(path) = try_test_video() else { return };
1254        let (mut demuxer, streams) = FileDemuxer::open("demux", &path).expect("open");
1255        let video = streams
1256            .iter()
1257            .find(|stream| stream.kind == ffmpeg::media::Type::Video)
1258            .expect("video stream");
1259        let time_base = demuxer.stream_time_base(video.index).expect("time base");
1260
1261        // Room for one buffer only, so the pad blocks partway through the
1262        // drain and the rest have to be parked again.
1263        let remaining = Arc::new(AtomicUsize::new(1));
1264        let seen = Arc::new(Mutex::new(Vec::new()));
1265        demuxer.pads[video.index].link(Box::new(BoundedSink {
1266            remaining: Arc::clone(&remaining),
1267            seen: Arc::clone(&seen),
1268            pp_log: element_pp_log(ElementType::Other, "bounded", None),
1269        }));
1270
1271        let mut input = ffmpeg::format::input(&path).expect("second handle");
1272        let mut order = Vec::new();
1273        for (stream, packet) in input.packets() {
1274            if stream.index() != video.index {
1275                continue;
1276            }
1277            order.push(packet.pts().expect("fixture packets carry a pts"));
1278            demuxer.park((stream.index(), time_base, packet));
1279            if order.len() == 5 {
1280                break;
1281            }
1282        }
1283
1284        let (bus, _bus_rx) = Bus::new();
1285        // Drain repeatedly, opening one slot at a time, until everything has
1286        // been delivered.
1287        for _ in 0..order.len() {
1288            demuxer.drain_pending(&bus).expect("drain");
1289            remaining.fetch_add(1, Ordering::SeqCst);
1290        }
1291        demuxer.drain_pending(&bus).expect("final drain");
1292
1293        assert_eq!(
1294            *seen.lock().unwrap(),
1295            order,
1296            "packets reached the pad out of the order they were read"
1297        );
1298    }
1299
1300    /// Never has room, but accepts what is pushed — a pad reporting
1301    /// backpressure without a `Queue`'s blocking behind it.
1302    struct NeverReadyRecorder {
1303        seen: Arc<AtomicUsize>,
1304        pp_log: PpLog,
1305    }
1306
1307    impl Element for NeverReadyRecorder {
1308        fn name(&self) -> Arc<str> {
1309            "never-ready".into()
1310        }
1311        fn element_type(&self) -> ElementType {
1312            ElementType::Other
1313        }
1314        fn pp_log(&self) -> &PpLog {
1315            &self.pp_log
1316        }
1317        fn pp_log_mut(&mut self) -> &mut PpLog {
1318            &mut self.pp_log
1319        }
1320    }
1321
1322    impl crate::element::Sink for NeverReadyRecorder {
1323        fn ready_consume(&mut self) -> bool {
1324            false
1325        }
1326        fn consume(&mut self, _buf: MediaBuffer) -> crate::error::Result<()> {
1327            self.seen.fetch_add(1, Ordering::SeqCst);
1328            Ok(())
1329        }
1330        fn control(&mut self, _msg: crate::control::ControlMsg) -> crate::error::Result<()> {
1331            Ok(())
1332        }
1333    }
1334
1335    /// Holding a blocked pad's packets is a preroll measure, not a playback
1336    /// one.
1337    ///
1338    /// Preroll needs it: a terminal closes after its one sample, and refusing
1339    /// to read while that pad is shut would starve the branches still owed
1340    /// theirs. Playback must not have it — a blocked pad there is ordinary
1341    /// backpressure, and waiting on it is what paces this source to the
1342    /// slowest branch. Holding instead let the source run away from playback
1343    /// and buffer the file: 67 MB parked within 1.5 s, after which the backlog
1344    /// ceiling stopped the read cursor for *every* pad. Attaching an audio
1345    /// branch then never received a packet, so it never primed the playback
1346    /// clock, and the picture froze.
1347    #[test]
1348    fn packets_are_only_held_back_while_a_preroll_is_running() {
1349        let Some(path) = try_test_video() else { return };
1350        let (mut demuxer, streams) = FileDemuxer::open("demux", &path).expect("open");
1351        let index = streams.first().expect("at least one stream").index;
1352        let time_base = demuxer.stream_time_base(index).expect("time base");
1353        let seen = Arc::new(AtomicUsize::new(0));
1354        demuxer.pads[index].link(Box::new(NeverReadyRecorder {
1355            seen: Arc::clone(&seen),
1356            pp_log: element_pp_log(ElementType::Other, "never-ready", None),
1357        }));
1358
1359        let (bus, _bus_rx) = Bus::new();
1360        let packet = || {
1361            let mut packet = ffmpeg::Packet::empty();
1362            packet.set_pts(Some(0));
1363            (index, time_base, packet)
1364        };
1365
1366        demuxer
1367            .deliver_or_park(packet(), &bus)
1368            .expect("playback delivery");
1369        assert!(
1370            demuxer.pending.is_empty(),
1371            "playback must wait on a blocked pad, not buffer behind it"
1372        );
1373        assert_eq!(seen.load(Ordering::SeqCst), 1);
1374
1375        demuxer.on_control(&crate::control::ControlMsg::Preroll(Arc::new(
1376            crate::control::PrerollContext::new([]),
1377        )));
1378        demuxer
1379            .deliver_or_park(packet(), &bus)
1380            .expect("preroll delivery");
1381        assert_eq!(
1382            demuxer.pending.len(),
1383            1,
1384            "preroll must hold a blocked pad's packet so its siblings keep flowing"
1385        );
1386        assert_eq!(seen.load(Ordering::SeqCst), 1);
1387
1388        demuxer.on_control(&crate::control::ControlMsg::Resume);
1389        demuxer.drain_pending(&bus).expect("drain after preroll");
1390        assert!(
1391            demuxer.pending.is_empty(),
1392            "the backlog must not outlive the preroll that justified it"
1393        );
1394        assert_eq!(seen.load(Ordering::SeqCst), 2);
1395    }
1396}