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}