Skip to main content

mediadecode_ffmpeg/
demuxer.rs

1//! [`mediadecode::demuxer::Demuxer`] impl backed by `libavformat`.
2//!
3//! Opens a container — from a path, or from any `Read + Seek` reader
4//! through a custom `AVIOContext` — reads its track table once, and
5//! then hands packets out one at a time in interleaved file order.
6//!
7//! # What normalization this layer does
8//!
9//! libavformat's track table is not quite the one the demux tier
10//! promises, and the gap is entirely about attachments:
11//!
12//! - **Cover art is an attachment, not video.** A still image in an
13//!   MP3, FLAC or MP4 arrives as a video stream carrying
14//!   `AV_DISPOSITION_ATTACHED_PIC`. This layer maps it to
15//!   [`TrackKind::Attachment`], so the `Video` arm carries true motion
16//!   video and nothing else.
17//! - **A font's bytes are not in the packet stream at all.** An
18//!   `AVMEDIA_TYPE_ATTACHMENT` stream never produces a packet; its
19//!   payload lives in `AVCodecParameters.extradata`. This layer
20//!   synthesizes the packet at open time.
21//! - **Cover art's packet is hoisted.** libavformat parks the real
22//!   packet in `AVStream.attached_pic`; some demuxers also emit it in
23//!   the packet stream, some do not. This layer takes it from
24//!   `attached_pic` at open time and drops the duplicate if it ever
25//!   arrives, so the count is exactly one either way.
26//!
27//! Both kinds are queued at open — every attachment track, without
28//! exception, or the open fails. That is what makes the face's "exactly
29//! one packet, before any timed packet" true *by construction* here:
30//! the queue is complete and drains before the first `av_read_frame`
31//! call ever runs, so no packet on an attachment track can be anything
32//! but a duplicate, and no seek can move a packet that was never on the
33//! timeline.
34//!
35//! # Seeking
36//!
37//! `seek` converts the target to `AV_TIME_BASE` units and calls
38//! `avformat_seek_file` over the window `[i64::MIN, target]`, which is
39//! FFmpeg's backward convention: the landing point is the nearest
40//! keyframe at or before the target, never after. `avformat_seek_file`
41//! flushes libavformat's own buffers; this layer clears the EOF latch
42//! it set itself, and deliberately does **not** touch the attachment
43//! bookkeeping — an attachment already handed out is never handed out
44//! again, and one not yet handed out is still owed.
45
46use std::{
47  collections::VecDeque,
48  ffi::{CStr, c_int},
49  io::{Read, Seek},
50  mem,
51  num::NonZeroI32,
52  path::Path,
53  ptr::{addr_of, read_unaligned},
54  sync::Arc,
55};
56
57use derive_more::{IsVariant, TryUnwrap, Unwrap};
58use ffmpeg_next::{
59  Packet, Rational,
60  ffi::{
61    AV_DISPOSITION_ATTACHED_PIC, AV_DISPOSITION_TIMED_THUMBNAILS, AV_NOPTS_VALUE, AVDictionary,
62    AVStream, av_dict_get,
63  },
64  format::{self, context::Input},
65};
66use mediadecode::{
67  Timebase, Timestamp,
68  demuxer::{
69    AttachmentPacket, AttachmentTrackPacket, AttachmentTrackParams, AudioTrackPacket,
70    AudioTrackParams, DataTrackPacket, DataTrackParams, DemuxedPacket, Demuxer,
71    SubtitleTrackPacket, SubtitleTrackParams, TrackIndex, TrackInfo, TrackKind, TrackParams,
72    UnknownTrackParams, VideoTrackPacket, VideoTrackParams,
73  },
74};
75use smol_str::SmolStr;
76
77use crate::{
78  Ffmpeg, boundary,
79  buffer::PacketBufferError,
80  codec_id::CodecId,
81  extras::{AttachmentPacketExtra, TrackExtra},
82  limits::DemuxLimits,
83  reader_guard::{GuardedReader, PanicLatch},
84  sample_format::SampleFormat,
85};
86
87/// One microsecond — the timebase `avformat_seek_file` expects when no
88/// reference stream is named (`stream_index == -1`).
89fn av_time_base_q() -> Timebase {
90  Timebase::new(1, NonZeroI32::new(1_000_000).expect("1e6 is non-zero"))
91}
92
93/// `mediadecode::demuxer::Demuxer` impl wrapping `ffmpeg::format::context::Input`.
94///
95/// Construction is deliberately not on the trait — see [`Self::open`]
96/// and [`Self::open_reader`].
97pub struct CarrierDemuxer<C: crate::FfmpegCarrier> {
98  input: Input,
99  tracks: Vec<TrackInfo<Ffmpeg>>,
100  pending: VecDeque<(
101    TrackIndex,
102    AttachmentPacket<AttachmentPacketExtra, C::Buffer>,
103  )>,
104  /// `true` once this session has answered `Ok(None)`. Only then does
105  /// [`Self::seek`] clear the `AVIOContext`'s EOF latch — clearing it
106  /// unconditionally would also erase a genuine sticky I/O error, which
107  /// `Input::seek` goes out of its way to preserve.
108  eof: bool,
109  /// Set for a session opened over a caller's reader: where a panic
110  /// raised inside that reader is recorded. `None` for a path-opened
111  /// session, which runs no caller code.
112  reader_panic: Option<Arc<PanicLatch>>,
113  /// The budgets this session spends: on any one timed packet, and —
114  /// already spent, at open — on the file's attachments.
115  limits: DemuxLimits,
116  /// A packet `av_read_frame` has already handed over and whose
117  /// conversion has **not committed**, with the provenance that was
118  /// observed for it.
119  ///
120  /// `av_read_frame` advances the container: once it returns, that
121  /// packet is off the wire and nothing brings it back. A conversion
122  /// that then fails on an *allocation* — a refcount the view lane
123  /// could not take, a copy the middle row could not make — used to
124  /// drop it, leaving a live session that answered the next pull with
125  /// the **following** packet. Compressed data and subtitle cues went
126  /// missing under memory pressure, quietly.
127  ///
128  /// So the read and the conversion are one transaction with a seat
129  /// between them: a transient refusal parks the packet here and the
130  /// next pull re-attempts *this* packet before reading another. It is
131  /// the same park-then-replay the decode household already runs —
132  /// `CarrierVideoStreamDecoder` holds `sw_replay_frames`, and the
133  /// probe holds its rescue history — for the same reason: a byte C
134  /// has already given up is not re-askable.
135  ///
136  /// The provenance is parked **with** the packet rather than re-probed
137  /// on replay. It is an observation about the moment of delivery, and
138  /// a queue that has moved on could answer it differently.
139  unconverted: Option<(Packet, crate::buffer::PayloadProvenance)>,
140}
141
142// The generic bodies. Crate-private, because their bound is: they are
143// the implementation, and the public faces below are written per lane
144// so that no signature a consumer reads names a trait they cannot.
145impl<C: crate::FfmpegCarrier + crate::CarrierOps> CarrierDemuxer<C> {
146  /// Opens a container from a filesystem path.
147  ///
148  /// Runs `avformat_open_input` followed by
149  /// `avformat_find_stream_info`, then builds the track table and
150  /// captures every attachment payload.
151  ///
152  /// Call [`ffmpeg_next::init`] once before the first open if you want
153  /// FFmpeg's logging and network protocols configured; probing a local
154  /// container does not require it.
155  pub(crate) fn open_impl<P: AsRef<Path> + ?Sized>(path: &P) -> Result<Self, DemuxError> {
156    Self::open_with_impl(path, DemuxLimits::default())
157  }
158
159  /// [`Self::open`], with the session's resource budgets named.
160  ///
161  /// The budgets are taken **at open** rather than through a `with_*`
162  /// builder because the attachment half of them is spent here: every
163  /// attachment payload in the file is captured before this call
164  /// returns, which is what makes the demux tier's "exactly one packet,
165  /// before any timed packet" contract true by construction. A budget
166  /// set afterwards would arrive after the spending.
167  ///
168  /// A file whose attachments exceed the budget **fails to open**, with
169  /// [`DemuxError::AttachmentTooLarge`] or
170  /// [`DemuxError::AttachmentBudgetExhausted`] naming the track that
171  /// crossed the line.
172  pub(crate) fn open_with_impl<P: AsRef<Path> + ?Sized>(
173    path: &P,
174    limits: DemuxLimits,
175  ) -> Result<Self, DemuxError> {
176    // **The probe knobs, set before libavformat reads a byte.** See
177    // [`DemuxLimits::max_probe_bytes`]: `avformat_open_input` and
178    // `avformat_find_stream_info` build the attachment, extradata and
179    // coded-side-data buffers themselves, so every budget that measures
180    // *this crate's* copies arrives after the original allocation. The
181    // instrument that reaches behind that is the one bounding what the
182    // parser is handed in the first place.
183    //
184    // On this entrypoint that is `probesize` / `formatprobesize` /
185    // `max_streams` only: the hard byte meter needs an `AVIOContext`
186    // this crate owns, and a path is opened by libavformat's own
187    // protocol layer. The reader entrypoint gets both.
188    Self::from_input(
189      format::input_with_dictionary(path, probe_options(limits))?,
190      limits,
191    )
192  }
193
194  /// Opens a container from any `Read + Seek` byte source, through a
195  /// custom `AVIOContext`.
196  ///
197  /// `Seek` is mandatory and not negotiable: MP4 files routinely put
198  /// `moov` at the end, so a reader that cannot go backwards cannot be
199  /// probed at all — and the seek law on the face would be
200  /// unimplementable.
201  ///
202  /// `filename` is a probe hint, not a path: libavformat uses its
203  /// extension to break ties between formats whose byte signatures are
204  /// ambiguous. Pass `None` when there is nothing to hint with.
205  ///
206  /// # A panicking reader
207  ///
208  /// libavformat drives the reader from `extern "C"` callbacks, where a
209  /// panic would abort the process rather than unwind. Every call into
210  /// `reader` therefore runs under `catch_unwind`: a panic becomes an
211  /// I/O error for libavformat and surfaces here — or from the next
212  /// [`next_packet`](Demuxer::next_packet) / [`seek`](Demuxer::seek) —
213  /// as [`DemuxError::ReaderPanic`], carrying the panic's message. The
214  /// session is terminal from that point: the `AVIOContext`'s error
215  /// state is sticky and the reader's own state is unknown.
216  pub(crate) fn open_reader_impl<R: Read + Seek + Send + 'static>(
217    reader: R,
218    filename: Option<&str>,
219  ) -> Result<Self, DemuxError> {
220    Self::open_reader_with_impl(reader, filename, DemuxLimits::default())
221  }
222
223  /// [`Self::open_reader`], with the session's resource budgets named.
224  /// See [`Self::open_with`] for why they are taken at open.
225  pub(crate) fn open_reader_with_impl<R: Read + Seek + Send + 'static>(
226    reader: R,
227    filename: Option<&str>,
228    limits: DemuxLimits,
229  ) -> Result<Self, DemuxError> {
230    let (guarded, latch, meter) = GuardedReader::new(reader, limits.max_probe_bytes());
231    let io = format::context::StreamIo::from_read_seek(guarded)?;
232    let input =
233      format::input_from_stream(io, filename, Some(probe_options(limits))).map_err(|e| {
234        // Three ways this can fail, and they must not be confused: a
235        // panicked reader, a probe budget reached, or libavformat's own
236        // verdict. The meter is consulted before the errno because
237        // libavformat folds the reader's I/O error into whatever it was
238        // doing at the time — usually "invalid data" — which would
239        // report a refusal this crate made as a malformed file.
240        reader_panic(&latch)
241          .or_else(|| {
242            meter.tripped().then(|| {
243              DemuxError::ProbeBudgetExhausted(ProbeBudgetExhausted::new(
244                meter.read(),
245                meter.budget(),
246              ))
247            })
248          })
249          .unwrap_or(DemuxError::Ffmpeg(e))
250      })?;
251    if meter.tripped() {
252      return Err(DemuxError::ProbeBudgetExhausted(ProbeBudgetExhausted::new(
253        meter.read(),
254        meter.budget(),
255      )));
256    }
257    // Open and analysed: the seat bounds *probing*, and reading the
258    // media itself afterwards is the caller's business, packet by
259    // packet, already bounded by the packet seats.
260    meter.release();
261    // A panic libavformat tolerated (a failed probe it recovered from)
262    // still poisoned the reader; the session must not open over it.
263    if let Some(panicked) = reader_panic(&latch) {
264      return Err(panicked);
265    }
266    let mut demuxer = Self::from_input(input, limits)?;
267    demuxer.reader_panic = Some(latch);
268    Ok(demuxer)
269  }
270
271  /// Borrows the wrapped `ffmpeg::format::context::Input` — for
272  /// `av_dump_format`, container-level metadata, chapters, and anything
273  /// else the portable track table has no seat for.
274  #[cfg_attr(not(tarpaulin), inline(always))]
275  pub(crate) const fn input_impl(&self) -> &Input {
276    &self.input
277  }
278
279  /// The budgets this session was opened with.
280  #[cfg_attr(not(tarpaulin), inline(always))]
281  pub(crate) const fn limits_impl(&self) -> DemuxLimits {
282    self.limits
283  }
284
285  fn from_input(input: Input, limits: DemuxLimits) -> Result<Self, DemuxError> {
286    let (tracks, pending) = build_tracks::<C>(&input, limits)?;
287    Ok(Self {
288      input,
289      tracks,
290      pending,
291      unconverted: None,
292      eof: false,
293      reader_panic: None,
294      limits,
295    })
296  }
297
298  /// The error a panicked reader owes this session, if one panicked.
299  fn panicked(&self) -> Option<DemuxError> {
300    self.reader_panic.as_deref().and_then(reader_panic)
301  }
302}
303
304/// The libavformat options this crate sets before a container is
305/// opened.
306///
307/// Passed as an `AVDictionary` because that is the only route to these
308/// fields that works for both entrypoints: `avformat_open_input`
309/// applies the dictionary to the context it allocates itself, and the
310/// same names reach the context behind a custom `AVIOContext`.
311///
312/// * `probesize` / `formatprobesize` bound what the format probe and
313///   the stream analysis are allowed to consume;
314/// * `max_streams` bounds the `AVStream` array a header can conjure —
315///   a container claiming a hundred thousand streams is an allocation
316///   this crate's per-track budgets are downstream of.
317fn probe_options(limits: DemuxLimits) -> ffmpeg_next::Dictionary<'static> {
318  let mut options = ffmpeg_next::Dictionary::new();
319  let probe = limits.max_probe_bytes().to_string();
320  options.set("probesize", &probe);
321  options.set("formatprobesize", &probe);
322  options.set("max_streams", &limits.max_streams().to_string());
323  options
324}
325
326/// Payload for [`DemuxError::ProbeBudgetExhausted`].
327///
328/// libavformat wanted more of the file than the probe budget allows.
329///
330/// # What this bounds
331///
332/// This is the only seat in the crate that reaches *behind*
333/// libavformat: `avformat_open_input` and `avformat_find_stream_info`
334/// build the attached picture, the extradata and the coded side data
335/// out of the file themselves, so every budget measuring this crate's
336/// own copies necessarily arrives after those allocations happened.
337///
338/// A parser cannot allocate from bytes it was never handed, so the
339/// input is bounded instead. What is **not** bounded is amplification
340/// inside a parser — a container can describe, in a few bytes, a
341/// structure whose in-memory form is far larger, and nothing outside
342/// libavformat can observe that. Bounding the output of that is the
343/// substrate's own hardening territory; FFmpeg keeps `max_streams`,
344/// `max_index_size` and `max_picture_buffer` for it, and this crate
345/// sets the first.
346#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
347#[error("libavformat read {read} bytes probing the container, over a budget of {budget}")]
348pub struct ProbeBudgetExhausted {
349  read: u64,
350  budget: u64,
351}
352
353impl ProbeBudgetExhausted {
354  /// Constructs a `ProbeBudgetExhausted` payload.
355  #[inline]
356  pub const fn new(read: u64, budget: u64) -> Self {
357    Self { read, budget }
358  }
359  /// Bytes libavformat was handed before the budget was reached.
360  #[inline]
361  pub const fn read(&self) -> u64 {
362    self.read
363  }
364  /// The budget in force.
365  #[inline]
366  pub const fn budget(&self) -> u64 {
367    self.budget
368  }
369}
370
371/// Turns a latched reader panic into the error that names it.
372fn reader_panic(latch: &PanicLatch) -> Option<DemuxError> {
373  latch
374    .message()
375    .map(|message| DemuxError::ReaderPanic(ReaderPanic::new(message)))
376}
377
378impl<C: crate::FfmpegCarrier + crate::CarrierOps> CarrierDemuxer<C> {
379  pub(crate) fn tracks_impl(&self) -> &[TrackInfo<Ffmpeg>] {
380    &self.tracks
381  }
382
383  pub(crate) fn take_tracks_impl(&mut self) -> Vec<TrackInfo<Ffmpeg>> {
384    mem::take(&mut self.tracks)
385  }
386
387  pub(crate) fn next_packet_impl(
388    &mut self,
389  ) -> Result<Option<DemuxedPacket<Ffmpeg, C::Buffer>>, DemuxError> {
390    // A latched reader panic is terminal, and terminal starts here. The
391    // queue is filled at open and owes nothing to the reader, so a pull
392    // that drained it would answer `Ok` to a caller the session has
393    // already told the truth to — `seek` can latch a panic while
394    // attachments are still queued.
395    if let Some(panicked) = self.panicked() {
396      return Err(panicked);
397    }
398
399    // The attachment queue drains first and drains completely, which is
400    // the whole of "exactly one packet, before any timed packet": no
401    // `av_read_frame` has run yet when the last one leaves.
402    if let Some((track, packet)) = self.pending.pop_front() {
403      return Ok(Some(DemuxedPacket::Attachment(AttachmentTrackPacket::new(
404        track, packet,
405      ))));
406    }
407
408    loop {
409      // **A parked packet is re-attempted before another is read.**
410      // See [`Self::unconverted`]: `av_read_frame` has already given
411      // this one up, so reading past it would lose it.
412      let (packet, parked_provenance) = match self.unconverted.take() {
413        Some((packet, provenance)) => (packet, Some(provenance)),
414        None => {
415          let mut packet = Packet::empty();
416          let read = packet.read(&mut self.input);
417          // A panicking reader reported an ordinary I/O error to C, and
418          // libavformat may answer that with the error, with EOF (a
419          // stream it cannot read looks finished), or with a packet it
420          // had already buffered. None of those are the file's word, so
421          // the latch is consulted whatever the outcome was.
422          if let Some(panicked) = self.panicked() {
423            return Err(panicked);
424          }
425          match read {
426            Ok(()) => {}
427            Err(ffmpeg_next::Error::Eof) => {
428              self.eof = true;
429              return Ok(None);
430            }
431            // A demuxer can resync past a corrupt packet, and
432            // `AVERROR_INVALIDDATA` is not latched into the
433            // `AVIOContext`, so reading again makes progress. Every
434            // other error is sticky and is surfaced.
435            Err(ffmpeg_next::Error::InvalidData) => continue,
436            Err(e) => return Err(DemuxError::Ffmpeg(e)),
437          }
438          (packet, None)
439        }
440      };
441
442      let index = packet.stream();
443      // A packet for a stream the table does not describe cannot be
444      // placed. libavformat does not produce these, but the index comes
445      // from C and indexes a `Vec`.
446      let Some(info) = self.tracks.get(index) else {
447        continue;
448      };
449      let track = TrackIndex::new(index);
450      let time_base = info.timebase();
451
452      // A payload that is there and cannot be referenced is an error,
453      // never a silently dropped packet: `Ok(None)` below means the
454      // packet carried nothing, and that is the only thing that reads
455      // the next one.
456      // **Everything this loop delivers is demux-delivered**, whatever
457      // its refcount: libavformat just handed it over, so any other
458      // reference to its buffer is libavformat's own and no
459      // `ffmpeg_next::Packet` wraps one. That is not the hazard a
460      // caller's second handle is — see
461      // [`crate::buffer::PayloadProvenance`].
462      //
463      // Sharing is ordinary here. A queue-backed demuxer — SubRip,
464      // SubViewer and the rest of the `FFDemuxSubtitlesQueue` family —
465      // keeps its parsed cues and delivers `av_packet_ref`s of them,
466      // so *every* packet it produces arrives with two references.
467      //
468      // The one sub-case that is stronger still is the container's
469      // parked picture, which a stream carrying
470      // `ATTACHED_PIC | TIMED_THUMBNAILS` delivers as its first packet:
471      // written once while the container opened, so the view lane may
472      // window it rather than copy. See [`is_streams_attached_pic`] for
473      // the identity proof.
474      //
475      // SAFETY: both the session's `AVFormatContext` and `packet` are
476      // live here.
477      let provenance = match parked_provenance {
478        // Observed when this packet was delivered, and kept with it.
479        Some(provenance) => provenance,
480        None if unsafe { is_streams_attached_pic(&self.input, index, &packet) } => {
481          crate::buffer::PayloadProvenance::AttachedPicture
482        }
483        None => crate::buffer::PayloadProvenance::DemuxDelivered,
484      };
485
486      // The packet this loop just read is **handed over**, not lent: the
487      // view lane's carrier is a window into its buffer, and a source
488      // that survived the conversion would be a mutable alias of it.
489      // Exactly one arm runs, so exactly one move happens.
490      // **The conversion borrows what this session owns.** The packet
491      // stays in hand until the carrier exists, which is what lets a
492      // failure park it instead of dropping it; on success it falls out
493      // of scope at the end of the iteration and the carrier keeps its
494      // buffer alive by refcount, exactly as when the conversion
495      // consumed it. Nothing outside this loop ever sees the packet, so
496      // the borrow cannot become the aliasing shape the public faces
497      // refuse.
498      let converted = match info.kind() {
499        TrackKind::Video => boundary::video_packet_from_borrowed::<C>(
500          &packet,
501          time_base,
502          self.limits.packet(),
503          provenance,
504        )
505        .map(|built| built.map(|p| DemuxedPacket::Video(VideoTrackPacket::new(track, p)))),
506        TrackKind::Audio => boundary::audio_packet_from_borrowed::<C>(
507          &packet,
508          time_base,
509          self.limits.packet(),
510          provenance,
511        )
512        .map(|built| built.map(|p| DemuxedPacket::Audio(AudioTrackPacket::new(track, p)))),
513        TrackKind::Subtitle => boundary::subtitle_packet_from_borrowed::<C>(
514          &packet,
515          time_base,
516          self.limits.packet(),
517          provenance,
518        )
519        .map(|built| built.map(|p| DemuxedPacket::Subtitle(SubtitleTrackPacket::new(track, p)))),
520        TrackKind::Data => boundary::data_packet_from_borrowed::<C>(
521          &packet,
522          time_base,
523          self.limits.packet(),
524          provenance,
525        )
526        .map(|built| built.map(|p| DemuxedPacket::Data(DataTrackPacket::new(track, p)))),
527        // Every attachment track's one packet was queued at open time,
528        // so anything arriving on one now is the duplicate some
529        // demuxers emit for cover art. Drop it — the contract is
530        // exactly one, and the one has already left. Nothing is
531        // converted here, so there is nothing to park.
532        TrackKind::Attachment => continue,
533        // The roster of arms is five; a track nothing can name has no
534        // arm and its packets are not delivered.
535        TrackKind::Unknown => continue,
536      };
537
538      let built = match converted {
539        Ok(built) => built,
540        Err(source) => {
541          // **Park a refusal that another attempt could survive.** An
542          // allocation that failed says nothing about the packet, and
543          // the packet is off the wire either way. Anything else is a
544          // fact about the packet itself — a malformed one is not made
545          // well-formed by retrying, and parking it would answer every
546          // later pull with the same error instead of letting the
547          // session make progress.
548          if source.parks_in_demux() {
549            self.unconverted = Some((packet, provenance));
550          }
551          return Err(DemuxError::PacketBuffer(PacketBuffer::new(index, source)));
552        }
553      };
554
555      // `None` here means the packet carried no payload — an empty
556      // packet, which some demuxers emit as a marker. Nothing to
557      // deliver; read the next one.
558      if let Some(out) = built {
559        return Ok(Some(out));
560      }
561    }
562  }
563
564  pub(crate) fn seek_impl(&mut self, target: Timestamp) -> Result<(), DemuxError> {
565    let ts = target.rescale_to(av_time_base_q()).pts();
566    // Only our own EOF latch is cleared, and only before the seek —
567    // the seek machinery gates on `eof_reached`, so clearing it
568    // afterwards would be too late.
569    if self.eof {
570      self.input.clear_eof();
571      self.eof = false;
572    }
573    // `..ts` is how ffmpeg-next spells the seek window: it reads only
574    // the endpoint, and `avformat_seek_file`'s `max_ts` is inclusive,
575    // so the window is `[i64::MIN, ts]`. FFmpeg picks the closest seek
576    // point inside it — the nearest keyframe at or before the target.
577    // Never after: a decoder started past the target has no reference
578    // frame.
579    let sought = self.input.seek(ts, ..ts);
580    if let Some(panicked) = self.panicked() {
581      return Err(panicked);
582    }
583    sought?;
584    // **The seat is cleared by a seek that happened, not by one that
585    // was attempted.** A parked packet belongs to the position the
586    // session is leaving, so a successful seek discards it. A *failed*
587    // one leaves the session where it was — and that packet is off the
588    // wire, so dropping it here would be the same silent loss the seat
589    // exists to prevent, with no re-read able to recover it.
590    //
591    // FFmpeg does not specify where a container sits after a seek that
592    // returned an error, and this crate does not guess: it keeps a
593    // packet the container really did deliver, and a caller who saw the
594    // seek fail already knows the position is not the one they asked
595    // for. Every timestamp needed to tell is on the packet.
596    self.unconverted = None;
597    Ok(())
598  }
599}
600
601macro_rules! demuxer_lane_face {
602  ($($lane:ty),+ $(,)?) => { $(
603    impl CarrierDemuxer<$lane> {
604      /// Opens a container from a filesystem path.
605      ///
606      /// Runs `avformat_open_input` followed by
607      /// `avformat_find_stream_info`, then builds the track table and
608      /// captures every attachment payload.
609      ///
610      /// Call [`ffmpeg_next::init`] once before the first open if you
611      /// want FFmpeg's logging and network protocols configured;
612      /// probing a local container does not require it.
613      pub fn open<P: AsRef<Path> + ?Sized>(path: &P) -> Result<Self, DemuxError> {
614        Self::open_impl(path)
615      }
616
617      /// [`Self::open`], with the session's resource budgets named.
618      ///
619      /// The budgets are taken **at open** rather than through a
620      /// `with_*` builder because the attachment half of them is spent
621      /// here: every attachment payload is captured during this call.
622      pub fn open_with<P: AsRef<Path> + ?Sized>(
623        path: &P,
624        limits: DemuxLimits,
625      ) -> Result<Self, DemuxError> {
626        Self::open_with_impl(path, limits)
627      }
628
629      /// Opens a container from any `Read + Seek` source.
630      pub fn open_reader<R: Read + Seek + Send + 'static>(
631        reader: R,
632        url: Option<&str>,
633      ) -> Result<Self, DemuxError> {
634        Self::open_reader_impl(reader, url)
635      }
636
637      /// [`Self::open_reader`], with the session's budgets named.
638      pub fn open_reader_with<R: Read + Seek + Send + 'static>(
639        reader: R,
640        url: Option<&str>,
641        limits: DemuxLimits,
642      ) -> Result<Self, DemuxError> {
643        Self::open_reader_with_impl(reader, url, limits)
644      }
645
646      /// The wrapped `AVFormatContext`.
647      pub const fn input(&self) -> &Input {
648        self.input_impl()
649      }
650
651      /// The budgets this session was opened with.
652      pub const fn limits(&self) -> DemuxLimits {
653        self.limits_impl()
654      }
655    }
656
657    impl Demuxer for CarrierDemuxer<$lane> {
658      type Adapter = Ffmpeg;
659      type Buffer = <$lane as crate::FfmpegCarrier>::Buffer;
660      type Error = DemuxError;
661
662      fn tracks(&self) -> &[TrackInfo<Ffmpeg>] {
663        self.tracks_impl()
664      }
665
666      fn take_tracks(&mut self) -> Vec<TrackInfo<Ffmpeg>> {
667        self.take_tracks_impl()
668      }
669
670      /// Pulls the next packet.
671      ///
672      /// **A refusal that another attempt could survive costs no
673      /// packet.** `av_read_frame` advances the container, so a
674      /// conversion that then fails on an allocation would otherwise
675      /// drop bytes nothing can ask for again. Such a packet is parked
676      /// instead, and this method re-attempts *it* before reading
677      /// another — so a caller who pulls again loses nothing. A refusal
678      /// about the packet itself is not parked: retrying a malformed
679      /// packet forever would be worse than passing it by.
680      fn next_packet(
681        &mut self,
682      ) -> Result<Option<DemuxedPacket<Ffmpeg, Self::Buffer>>, DemuxError> {
683        self.next_packet_impl()
684      }
685
686      fn seek(&mut self, target: Timestamp) -> Result<(), DemuxError> {
687        self.seek_impl(target)
688      }
689    }
690  )+ };
691}
692
693demuxer_lane_face!(crate::View, crate::Owned);
694
695/// Payload for [`DemuxError::AttachmentTooLarge`].
696///
697/// One attachment's payload exceeds
698/// [`DemuxLimits::max_attachment_bytes`].
699#[derive(thiserror::Error, Debug, Clone, Copy, PartialEq, Eq)]
700#[error(
701  "the attachment on stream {stream_index} is {bytes} bytes, over the {limit}-byte per-attachment budget"
702)]
703pub struct AttachmentTooLarge {
704  stream_index: usize,
705  bytes: usize,
706  limit: usize,
707}
708
709impl AttachmentTooLarge {
710  /// Constructs an `AttachmentTooLarge` payload.
711  #[cfg_attr(not(tarpaulin), inline(always))]
712  pub const fn new(stream_index: usize, bytes: usize, limit: usize) -> Self {
713    Self {
714      stream_index,
715      bytes,
716      limit,
717    }
718  }
719  /// The `AVStream.index` carrying the oversized attachment.
720  #[cfg_attr(not(tarpaulin), inline(always))]
721  pub const fn stream_index(&self) -> usize {
722    self.stream_index
723  }
724  /// The attachment's payload length.
725  #[cfg_attr(not(tarpaulin), inline(always))]
726  pub const fn bytes(&self) -> usize {
727    self.bytes
728  }
729  /// The per-attachment budget in force.
730  #[cfg_attr(not(tarpaulin), inline(always))]
731  pub const fn limit(&self) -> usize {
732    self.limit
733  }
734}
735
736/// Payload for [`DemuxError::AttachmentBudgetExhausted`].
737///
738/// The file's attachments, together, exceed
739/// [`DemuxLimits::max_total_attachment_bytes`].
740///
741/// Separate from [`AttachmentTooLarge`] because it is a different
742/// attack: every attachment can be modest and there can still be four
743/// hundred of them. This arm names the track that ran the total past
744/// the line, not the track that was individually at fault — there
745/// need not be one.
746#[derive(thiserror::Error, Debug, Clone, Copy, PartialEq, Eq)]
747#[error(
748  "the attachment on stream {stream_index} brings the file's attachments to {total} bytes, over the {limit}-byte budget"
749)]
750pub struct AttachmentBudgetExhausted {
751  stream_index: usize,
752  total: usize,
753  limit: usize,
754}
755
756impl AttachmentBudgetExhausted {
757  /// Constructs an `AttachmentBudgetExhausted` payload.
758  #[cfg_attr(not(tarpaulin), inline(always))]
759  pub const fn new(stream_index: usize, total: usize, limit: usize) -> Self {
760    Self {
761      stream_index,
762      total,
763      limit,
764    }
765  }
766  /// The `AVStream.index` whose attachment crossed the line.
767  #[cfg_attr(not(tarpaulin), inline(always))]
768  pub const fn stream_index(&self) -> usize {
769    self.stream_index
770  }
771  /// The running total, including this attachment.
772  #[cfg_attr(not(tarpaulin), inline(always))]
773  pub const fn total(&self) -> usize {
774    self.total
775  }
776  /// The whole-file budget in force.
777  #[cfg_attr(not(tarpaulin), inline(always))]
778  pub const fn limit(&self) -> usize {
779    self.limit
780  }
781}
782
783/// Payload for [`DemuxError::ParametersTooLarge`].
784///
785/// One stream's codec parameters hold more heap bytes than
786/// [`DemuxLimits::max_codec_parameter_bytes`] allows.
787///
788/// The bytes are `extradata` plus every `coded_side_data` entry plus a
789/// custom channel map — the three seats `AVCodecParameters` reaches the
790/// heap through. A MOV `prof` atom lands in the second of those as an
791/// ICC profile, which is where the honest large values live and where
792/// the forged ones do too.
793#[derive(thiserror::Error, Debug, Clone, Copy, PartialEq, Eq)]
794#[error(
795  "the codec parameters on stream {stream_index} hold {bytes} heap bytes, over the {limit}-byte budget"
796)]
797pub struct ParametersTooLarge {
798  stream_index: usize,
799  bytes: usize,
800  limit: usize,
801}
802
803impl ParametersTooLarge {
804  /// Constructs a `ParametersTooLarge` payload.
805  #[cfg_attr(not(tarpaulin), inline(always))]
806  pub const fn new(stream_index: usize, bytes: usize, limit: usize) -> Self {
807    Self {
808      stream_index,
809      bytes,
810      limit,
811    }
812  }
813  /// The `AVStream.index` whose parameters were refused.
814  #[cfg_attr(not(tarpaulin), inline(always))]
815  pub const fn stream_index(&self) -> usize {
816    self.stream_index
817  }
818  /// The heap bytes the parameters declared.
819  #[cfg_attr(not(tarpaulin), inline(always))]
820  pub const fn bytes(&self) -> usize {
821    self.bytes
822  }
823  /// The budget in force.
824  #[cfg_attr(not(tarpaulin), inline(always))]
825  pub const fn limit(&self) -> usize {
826    self.limit
827  }
828}
829
830/// Payload for [`DemuxError::ParametersBudgetExhausted`].
831///
832/// Every stream's codec parameters, together, hold more heap bytes than
833/// [`DemuxLimits::max_total_codec_parameter_bytes`] allows.
834///
835/// A separate attack from [`ParametersTooLarge`], and separate for the
836/// same reason the attachment pair are: each stream's parameters can be
837/// individually modest and a container can still declare two hundred
838/// streams. The arm names the stream that ran the total past the line,
839/// which need not be one that was individually at fault.
840#[derive(thiserror::Error, Debug, Clone, Copy, PartialEq, Eq)]
841#[error(
842  "the codec parameters on stream {stream_index} bring the file's to {total} heap bytes, over the {limit}-byte budget"
843)]
844pub struct ParametersBudgetExhausted {
845  stream_index: usize,
846  total: usize,
847  limit: usize,
848}
849
850impl ParametersBudgetExhausted {
851  /// Constructs a `ParametersBudgetExhausted` payload.
852  #[cfg_attr(not(tarpaulin), inline(always))]
853  pub const fn new(stream_index: usize, total: usize, limit: usize) -> Self {
854    Self {
855      stream_index,
856      total,
857      limit,
858    }
859  }
860  /// The `AVStream.index` whose parameters crossed the line.
861  #[cfg_attr(not(tarpaulin), inline(always))]
862  pub const fn stream_index(&self) -> usize {
863    self.stream_index
864  }
865  /// The running total, including this stream.
866  #[cfg_attr(not(tarpaulin), inline(always))]
867  pub const fn total(&self) -> usize {
868    self.total
869  }
870  /// The whole-file budget in force.
871  #[cfg_attr(not(tarpaulin), inline(always))]
872  pub const fn limit(&self) -> usize {
873    self.limit
874  }
875}
876
877/// Payload for [`DemuxError::ParametersMissing`].
878///
879/// Codec parameters arrived that were never allocated.
880///
881/// `ffmpeg_next::codec::Parameters` has safe constructors that hand
882/// back a null-backed value when FFmpeg's allocation failed, and they
883/// report nothing. Copying from one dereferences null, so it is
884/// refused where it arrives — at construction, and again in the
885/// copier — rather than crashing later somewhere that has forgotten
886/// the allocator ever failed.
887#[derive(thiserror::Error, Debug, Clone)]
888#[error("the codec parameters for stream {stream_index} were never allocated")]
889pub struct ParametersMissing {
890  stream_index: usize,
891}
892
893impl ParametersMissing {
894  /// Constructs a `ParametersMissing` payload.
895  #[cfg_attr(not(tarpaulin), inline(always))]
896  pub const fn new(stream_index: usize) -> Self {
897    Self { stream_index }
898  }
899  /// The `AVStream.index` the parameters were offered for.
900  #[cfg_attr(not(tarpaulin), inline(always))]
901  pub const fn stream_index(&self) -> usize {
902    self.stream_index
903  }
904}
905
906/// Payload for [`DemuxError::ParametersAlloc`].
907///
908/// Codec parameters for a track could not be allocated.
909#[derive(thiserror::Error, Debug, Clone)]
910#[error("out of memory allocating the codec parameters for stream {stream_index}")]
911pub struct ParametersAlloc {
912  stream_index: usize,
913}
914
915impl ParametersAlloc {
916  /// Constructs a `ParametersAlloc` payload.
917  #[cfg_attr(not(tarpaulin), inline(always))]
918  pub const fn new(stream_index: usize) -> Self {
919    Self { stream_index }
920  }
921  /// The `AVStream.index` whose parameters could not be copied.
922  #[cfg_attr(not(tarpaulin), inline(always))]
923  pub const fn stream_index(&self) -> usize {
924    self.stream_index
925  }
926}
927
928/// Payload for [`DemuxError::ParametersCopy`].
929///
930/// Copying a track's codec parameters failed part way.
931#[derive(thiserror::Error, Debug, Clone)]
932#[error("the codec parameters for stream {stream_index} could not be copied: {source}")]
933pub struct ParametersCopy {
934  stream_index: usize,
935  #[source]
936  source: ffmpeg_next::Error,
937}
938
939impl ParametersCopy {
940  /// Constructs a `ParametersCopy` payload.
941  #[cfg_attr(not(tarpaulin), inline(always))]
942  pub const fn new(stream_index: usize, source: ffmpeg_next::Error) -> Self {
943    Self {
944      stream_index,
945      source,
946    }
947  }
948  /// The `AVStream.index` whose parameters could not be copied.
949  #[cfg_attr(not(tarpaulin), inline(always))]
950  pub const fn stream_index(&self) -> usize {
951    self.stream_index
952  }
953  /// What FFmpeg said.
954  #[cfg_attr(not(tarpaulin), inline(always))]
955  pub const fn source(&self) -> &ffmpeg_next::Error {
956    &self.source
957  }
958}
959
960/// Payload for [`DemuxError::ParametersOpaque`].
961///
962/// A channel layout arrived carrying `opaque` — a raw pointer FFmpeg
963/// documents as "private data of the user".
964///
965/// [`CodecTicket`](crate::ticket::CodecTicket) is an **owned** mirror:
966/// it outlives the `AVCodecParameters` it was read from, and it may
967/// cross threads, so a pointer into somebody else's data is exactly
968/// what it cannot carry. libavformat sets neither
969/// `AVChannelLayout::opaque` nor `AVChannelCustom::opaque`, so no
970/// demuxed stream reaches the mirror with one; if one ever does, the
971/// mirror refuses rather than dropping the pointer in silence. That is
972/// the same fail-closed answer `extras::measure_parameters` gives a
973/// channel order it has never heard of, and for the same reason:
974/// carrying on would be a guess about memory nobody here owns.
975#[derive(thiserror::Error, Debug, Clone)]
976#[error(
977  "the channel layout for stream {stream_index} carries user-private data \
978   ({}) that an owned codec ticket cannot mirror",
979  match channel { Some(i) => format!("custom channel {i}"), None => "the layout".to_owned() },
980)]
981pub struct ParametersOpaque {
982  stream_index: usize,
983  channel: Option<usize>,
984}
985
986impl ParametersOpaque {
987  /// Constructs a `ParametersOpaque` payload. `channel` names the
988  /// custom-map entry when the pointer was on one, and is `None` when
989  /// it was on the layout itself.
990  #[cfg_attr(not(tarpaulin), inline(always))]
991  pub const fn new(stream_index: usize, channel: Option<usize>) -> Self {
992    Self {
993      stream_index,
994      channel,
995    }
996  }
997  /// The `AVStream.index` whose layout carried the pointer.
998  #[cfg_attr(not(tarpaulin), inline(always))]
999  pub const fn stream_index(&self) -> usize {
1000    self.stream_index
1001  }
1002  /// The custom-map entry the pointer was on, or `None` when it was on
1003  /// the layout itself.
1004  #[cfg_attr(not(tarpaulin), inline(always))]
1005  pub const fn channel(&self) -> Option<usize> {
1006    self.channel
1007  }
1008}
1009
1010/// Payload for [`DemuxError::ParametersChannelMap`].
1011///
1012/// A channel layout declared `AV_CHANNEL_ORDER_CUSTOM` without the map
1013/// that order requires.
1014///
1015/// **This one is a crash, not a curiosity.** For a custom order,
1016/// `av_channel_layout_copy` — which is how
1017/// `avcodec_parameters_to_context` moves a layout into a decoder's
1018/// context — does
1019///
1020/// ```c
1021/// dst->u.map = av_malloc_array(src->nb_channels, sizeof(*dst->u.map));
1022/// if (!dst->u.map)
1023///     return AVERROR(ENOMEM);
1024/// memcpy(dst->u.map, src->u.map, src->nb_channels * sizeof(*src->u.map));
1025/// ```
1026///
1027/// with **no null check on `src->u.map`** — verified against FFmpeg
1028/// n9.0. A layout that names channels it has no map for therefore makes
1029/// libavcodec `memcpy` from a null pointer the moment a decoder opens
1030/// from it.
1031///
1032/// So the mirror refuses such a layout at the door rather than
1033/// reproducing it. An earlier draft carried it through, on the argument
1034/// that a malformed layout in should be a malformed layout out — the
1035/// round trip is faithful either way, and the parity comparator agreed.
1036/// That symmetry was the wrong test: faithfully reproducing a shape
1037/// whose only consumer dereferences null is not fidelity, it is
1038/// forwarding a crash. Refusing is the same fail-closed answer
1039/// `extras::measure_parameters` gives a channel order it has never
1040/// heard of.
1041#[derive(thiserror::Error, Debug, Clone)]
1042#[error(
1043  "the custom channel layout for stream {stream_index} declares {channels} channels \
1044   but carries no map for them"
1045)]
1046pub struct ParametersChannelMap {
1047  stream_index: usize,
1048  channels: i32,
1049}
1050
1051impl ParametersChannelMap {
1052  /// Constructs a `ParametersChannelMap` payload.
1053  #[cfg_attr(not(tarpaulin), inline(always))]
1054  pub const fn new(stream_index: usize, channels: i32) -> Self {
1055    Self {
1056      stream_index,
1057      channels,
1058    }
1059  }
1060  /// The `AVStream.index` whose layout was malformed.
1061  #[cfg_attr(not(tarpaulin), inline(always))]
1062  pub const fn stream_index(&self) -> usize {
1063    self.stream_index
1064  }
1065  /// The `nb_channels` the layout declared with no map to describe them.
1066  #[cfg_attr(not(tarpaulin), inline(always))]
1067  pub const fn channels(&self) -> i32 {
1068    self.channels
1069  }
1070}
1071
1072/// Payload for [`DemuxError::PacketBuffer`].
1073///
1074/// A packet's payload could not be referenced — the bytes are there
1075/// and this layer could not carry them.
1076///
1077/// Never raised for a packet that simply has no payload: an empty
1078/// packet is a marker some demuxers emit, and it is skipped in
1079/// silence. Distinguishing the two is what keeps a refcount failure
1080/// under memory pressure from looking like the file's own word and
1081/// dropping real compressed bytes.
1082#[derive(thiserror::Error, Debug, Clone)]
1083#[error("stream {stream_index}: {source}")]
1084pub struct PacketBuffer {
1085  stream_index: usize,
1086  #[source]
1087  source: PacketBufferError,
1088}
1089
1090impl PacketBuffer {
1091  /// Constructs a `PacketBuffer` payload.
1092  #[cfg_attr(not(tarpaulin), inline(always))]
1093  pub const fn new(stream_index: usize, source: PacketBufferError) -> Self {
1094    Self {
1095      stream_index,
1096      source,
1097    }
1098  }
1099  /// The `AVStream.index` the packet belongs to.
1100  #[cfg_attr(not(tarpaulin), inline(always))]
1101  pub const fn stream_index(&self) -> usize {
1102    self.stream_index
1103  }
1104  /// What went wrong.
1105  #[cfg_attr(not(tarpaulin), inline(always))]
1106  pub const fn source(&self) -> &PacketBufferError {
1107    &self.source
1108  }
1109}
1110
1111/// Payload for [`DemuxError::ReaderPanic`].
1112///
1113/// The `Read + Seek` source given to [`FfmpegDemuxer::open_reader`]
1114/// panicked inside a libavformat callback.
1115///
1116/// The panic was caught before it could cross the `extern "C"`
1117/// boundary and abort the process; this is what it said. The session
1118/// is terminal — every later call reports the same panic.
1119#[derive(thiserror::Error, Debug, Clone)]
1120#[error("the reader panicked: {message}")]
1121pub struct ReaderPanic {
1122  message: SmolStr,
1123}
1124
1125impl ReaderPanic {
1126  /// Constructs a `ReaderPanic` payload.
1127  #[cfg_attr(not(tarpaulin), inline(always))]
1128  pub const fn new(message: SmolStr) -> Self {
1129    Self { message }
1130  }
1131  /// What the panic payload said.
1132  #[cfg_attr(not(tarpaulin), inline(always))]
1133  pub fn message(&self) -> &str {
1134    self.message.as_str()
1135  }
1136}
1137
1138/// Errors from [`FfmpegDemuxer`].
1139///
1140/// **Open fault taxonomy, so it is `#[non_exhaustive]`.** New ways to
1141/// fail are discovered — a backend, a ceiling, a corruption a codec
1142/// learns to report — and a consumer that meets one it has never heard
1143/// of should take its generic-fault path. That is exactly what the
1144/// wildcard arm this attribute forces is for. The two status
1145/// vocabularies opposite it,
1146/// [`Sent`](mediadecode::Sent) and [`Received`](mediadecode::Received),
1147/// are exhaustive for the mirror-image reason: their arms are the
1148/// substrate's fixed state set, and there the wildcard would be dead
1149/// weight hiding a state a consumer forgot.
1150#[derive(thiserror::Error, Debug, Clone, IsVariant, Unwrap, TryUnwrap)]
1151#[unwrap(ref, ref_mut)]
1152#[try_unwrap(ref, ref_mut)]
1153#[non_exhaustive]
1154pub enum DemuxError {
1155  /// The wrapped libavformat call reported an error — open, read or
1156  /// seek.
1157  #[error(transparent)]
1158  Ffmpeg(#[from] ffmpeg_next::Error),
1159
1160  /// libavformat asked for more bytes than the probe budget allows
1161  /// while opening and analysing the container. See
1162  /// [`ProbeBudgetExhausted`].
1163  #[error(transparent)]
1164  ProbeBudgetExhausted(#[from] ProbeBudgetExhausted),
1165
1166  /// One attachment's payload is over the per-attachment budget.
1167  /// Refused at open, before the copy.
1168  #[error(transparent)]
1169  AttachmentTooLarge(#[from] AttachmentTooLarge),
1170
1171  /// The file's attachments, together, are over the whole-file budget.
1172  /// Refused at open, before the copy that would have crossed it.
1173  #[error(transparent)]
1174  AttachmentBudgetExhausted(#[from] AttachmentBudgetExhausted),
1175
1176  /// One stream's codec parameters hold more heap bytes than the
1177  /// budget allows. Refused at open, before the clone.
1178  #[error(transparent)]
1179  ParametersTooLarge(#[from] ParametersTooLarge),
1180
1181  /// Every stream's codec parameters together are over the whole-file
1182  /// budget. Refused at open, before the clone that would have crossed
1183  /// it.
1184  #[error(transparent)]
1185  ParametersBudgetExhausted(#[from] ParametersBudgetExhausted),
1186
1187  /// Codec parameters arrived that were never allocated.
1188  #[error(transparent)]
1189  ParametersMissing(#[from] ParametersMissing),
1190
1191  /// Codec parameters for a track could not be allocated.
1192  #[error(transparent)]
1193  ParametersAlloc(#[from] ParametersAlloc),
1194
1195  /// Copying a track's codec parameters failed part way.
1196  #[error(transparent)]
1197  ParametersCopy(#[from] ParametersCopy),
1198
1199  /// A channel layout arrived carrying user-private data an owned
1200  /// codec ticket cannot mirror.
1201  #[error(transparent)]
1202  ParametersOpaque(#[from] ParametersOpaque),
1203
1204  /// A channel layout declared a custom order without the map that
1205  /// order requires — a shape `av_channel_layout_copy` would `memcpy`
1206  /// from null.
1207  #[error(transparent)]
1208  ParametersChannelMap(#[from] ParametersChannelMap),
1209
1210  /// A packet's payload could not be referenced — the bytes are there
1211  /// and this layer could not carry them.
1212  #[error(transparent)]
1213  PacketBuffer(#[from] PacketBuffer),
1214
1215  /// The `Read + Seek` source given to
1216  /// [`FfmpegDemuxer::open_reader`] panicked inside a libavformat
1217  /// callback.
1218  #[error(transparent)]
1219  ReaderPanic(#[from] ReaderPanic),
1220}
1221
1222// ---------------------------------------------------------------------------
1223//  Track-table construction.
1224// ---------------------------------------------------------------------------
1225
1226type BuiltTracks<C> = (
1227  Vec<TrackInfo<Ffmpeg>>,
1228  VecDeque<(
1229    TrackIndex,
1230    AttachmentPacket<AttachmentPacketExtra, <C as crate::FfmpegCarrier>::Buffer>,
1231  )>,
1232);
1233
1234fn build_tracks<C: crate::FfmpegCarrier + crate::CarrierOps>(
1235  input: &Input,
1236  limits: DemuxLimits,
1237) -> Result<BuiltTracks<C>, DemuxError> {
1238  // **Admission before allocation.** Every attachment in the file is
1239  // judged here, in full, before the loop below allocates anything at
1240  // all — see [`admit_streams`] for why the charge cannot live
1241  // inside the capture.
1242  admit_streams(input, limits)?;
1243
1244  let count = input.streams().len();
1245  let mut tracks = Vec::with_capacity(count);
1246  let mut pending = VecDeque::new();
1247
1248  for stream in input.streams() {
1249    let index = stream.index();
1250    // `AVStream.index` is the stream's position in `ic->streams[]` and
1251    // libavformat keeps the two identical. The demux tier makes
1252    // `TrackIndex` mean "position in `tracks()`", so the two agree by
1253    // construction — but only if they really are dense and in order,
1254    // which is cheap to insist on rather than assume.
1255    debug_assert_eq!(
1256      index,
1257      tracks.len(),
1258      "AVStream indices are dense and ordered"
1259    );
1260
1261    let parameters = stream.parameters();
1262    let par = unsafe { parameters.as_ptr() };
1263    // Never read `AVCodecParameters.codec_type` / `.codec_id` as their
1264    // bindgen enums: a value outside this build's discriminant set is
1265    // UB the moment it exists. Both are read as the raw integers they
1266    // are on the wire — the medium through [`boundary::media_kind_of`],
1267    // which folds anything unnamed into `Unknown`.
1268    //
1269    // The medium used to go through `Parameters::medium()` on the
1270    // argument that `AVMediaType`'s set is tiny and stable. It is; that
1271    // made the read unlikely to bite, not sound. The exception is gone
1272    // rather than defended, so no attacker-reachable path in this crate
1273    // forms a bindgen enum out of FFmpeg memory.
1274    let medium = boundary::media_kind_of(&parameters);
1275    let codec =
1276      CodecId::from_raw(unsafe { read_unaligned(addr_of!((*par).codec_id).cast::<i32>()) });
1277
1278    let disposition = unsafe { (*stream.as_ptr()).disposition };
1279    let attached_pic = is_attachment_disposition(disposition);
1280
1281    let time_base = rational_to_timebase(stream.time_base());
1282    let raw_duration = stream.duration();
1283    let duration = (raw_duration != AV_NOPTS_VALUE && raw_duration > 0)
1284      .then(|| Timestamp::new(raw_duration, time_base));
1285    let raw_start = stream.start_time();
1286    let frames = stream.frames();
1287
1288    let params = if attached_pic {
1289      // Cover art. A still image in a video-shaped slot is an
1290      // attachment by every property that matters, and the `Video` arm
1291      // is reserved for motion video.
1292      TrackParams::Attachment(AttachmentTrackParams::new(codec))
1293    } else {
1294      match medium {
1295        boundary::MediaKind::Video => TrackParams::Video(VideoTrackParams::new(
1296          codec,
1297          unsafe { (*par).width }.max(0) as u32,
1298          unsafe { (*par).height }.max(0) as u32,
1299          boundary::from_av_pixel_format(unsafe { (*par).format }),
1300          rate_to_timebase(stream.avg_frame_rate()),
1301        )),
1302        boundary::MediaKind::Audio => {
1303          let ch_layout = unsafe { std::ptr::addr_of!((*par).ch_layout) };
1304          // SAFETY: `par` is a live `*const AVCodecParameters` for the
1305          // life of `parameters`; the helper validates `order` as an
1306          // `i32` before constructing any `AVChannelOrder`.
1307          let channel_layout =
1308            unsafe { crate::channel_layout::channel_layout_description_from_raw_ptr(ch_layout) };
1309          TrackParams::Audio(AudioTrackParams::new(
1310            codec,
1311            unsafe { (*par).sample_rate }.max(0) as u32,
1312            channel_layout.channels().min(255) as u8,
1313            SampleFormat::from_raw(unsafe { (*par).format }),
1314            channel_layout,
1315          ))
1316        }
1317        boundary::MediaKind::Subtitle => TrackParams::Subtitle(SubtitleTrackParams::new(codec)),
1318        boundary::MediaKind::Data => TrackParams::Data(DataTrackParams::new(codec)),
1319        boundary::MediaKind::Attachment => {
1320          TrackParams::Attachment(AttachmentTrackParams::new(codec))
1321        }
1322        boundary::MediaKind::Unknown => TrackParams::Unknown(UnknownTrackParams::new(codec)),
1323      }
1324    };
1325
1326    // The parameter mirror. For an `AVMEDIA_TYPE_ATTACHMENT` stream its
1327    // `extradata` **is** the attachment's payload — the same bytes the
1328    // carrier below already holds — so it is left behind rather than
1329    // copied. Censused before it was: nothing can use it. libavcodec
1330    // has no decoder for a font (`avcodec_find_decoder` answers null
1331    // for `AV_CODEC_ID_TTF` and its siblings), so no road in this crate
1332    // or downstream of it opens a codec context from these parameters;
1333    // the payload reaches a consumer as the attachment packet, which is
1334    // the delivery the demux tier promises.
1335    //
1336    // **Omitted, not stripped.** An earlier shape copied the extradata
1337    // and freed it immediately afterwards, which allocated the payload
1338    // for no reason and — worse — charged it against the *parameter*
1339    // ceiling on the way past. A font between the two ceilings passed
1340    // the admission pass and then failed inside the clone. See
1341    // [`ExtradataPolicy`](crate::extras::ExtradataPolicy).
1342    //
1343    // Cover art keeps its extradata: there the payload is the parked
1344    // `AVPacket`, extradata is *not* a copy of it, and a still codec
1345    // can legitimately need it (MJPEG with an external Huffman table).
1346    // Measured on this build: a cover-art stream carries none anyway.
1347    let extradata_policy = if medium.is_attachment() {
1348      crate::extras::ExtradataPolicy::Omit
1349    } else {
1350      crate::extras::ExtradataPolicy::Copy
1351    };
1352    // Straight from the stream's own parameters into the owned ticket.
1353    // The row used to reach here through an intermediate
1354    // `avcodec_parameters_copy` — one ffmpeg-native deep copy per
1355    // track, whose only purpose was to sever the tie to the format
1356    // context. The mirror severs it by being owned Rust, so that copy
1357    // is gone rather than moved.
1358    let ticket = crate::ticket::CodecTicket::mirror_with(
1359      &parameters,
1360      index,
1361      limits.max_codec_parameter_bytes(),
1362      extradata_policy,
1363    )?;
1364    let extra = TrackExtra::new(index as i32, ticket)
1365      .with_disposition(disposition)
1366      .with_start_time((raw_start != AV_NOPTS_VALUE).then_some(raw_start))
1367      .with_frame_count((frames > 0).then_some(frames));
1368
1369    // SAFETY: `stream` keeps the `AVStream` — and so its metadata
1370    // dictionary — live across both reads. The dictionary is read
1371    // through `av_dict_get` rather than through
1372    // `DictionaryRef::get`: see [`metadata_text`].
1373    let metadata = unsafe { (*stream.as_ptr()).metadata };
1374    let info = TrackInfo::new(time_base, params, extra)
1375      .with_duration(duration)
1376      .with_filename(unsafe { metadata_text(metadata, c"filename") })
1377      .with_mime_type(unsafe { metadata_text(metadata, c"mimetype") });
1378
1379    // Capture the attachment payload now, so the queue is complete
1380    // before a single timed packet has been read. Every attachment
1381    // track leaves this loop with exactly one packet queued, or the
1382    // open fails: that is what makes "exactly one packet, before any
1383    // timed packet" a property of the construction rather than a
1384    // promise the pull loop has to keep.
1385    if info.kind() == TrackKind::Attachment {
1386      let packet = if attached_pic {
1387        // SAFETY: `stream` keeps the format context (and so the
1388        // `AVStream`) live; `attached_pic` is an `AVPacket` embedded by
1389        // value, and `addr_of!` reaches it without forming a reference
1390        // to the stream.
1391        let pkt = unsafe { std::ptr::addr_of!((*stream.as_ptr()).attached_pic) };
1392        unsafe { attached_pic_payload::<C>(pkt, index, limits) }?
1393      } else {
1394        extradata_payload::<C>(&stream, limits)?
1395      };
1396      pending.push_back((TrackIndex::new(index), packet));
1397    }
1398
1399    tracks.push(info);
1400  }
1401
1402  Ok((tracks, pending))
1403}
1404
1405/// Whether `packet`'s payload is the very allocation the container has
1406/// parked in `AVStream.attached_pic` for stream `index`.
1407///
1408/// # Why this exists
1409///
1410/// libavformat queues a stream's attached picture as its **first
1411/// packet** — `read_frame_internal` does `av_packet_ref(pkt,
1412/// &st->attached_pic)` and keeps its own reference — so that packet
1413/// arrives with two references through nobody's fault. A pure cover-art
1414/// stream never reaches this road (it is an attachment, hoisted at
1415/// open), but a stream carrying `ATTACHED_PIC | TIMED_THUMBNAILS` is
1416/// deliberately classified as **video** by
1417/// [`is_attachment_disposition`], so its first pull comes through here
1418/// and would be refused as a shared payload. Every packet after it is
1419/// an ordinary timed one with a buffer of its own.
1420///
1421/// # The probe, and why it is a proof rather than a guess
1422///
1423/// `av_buffer_ref` sets the new reference's `buffer` field to the
1424/// source's, so two `AVBufferRef`s name one allocation **iff** their
1425/// `buffer` pointers are equal — the same identity
1426/// [`crate::FfmpegBuffer::ptr_eq`] rests on. Comparing them therefore
1427/// establishes the fact the carve-out needs: this payload's allocation
1428/// *is* `AVStream.attached_pic`'s, so one of its outstanding references
1429/// is the container's own.
1430///
1431/// The alternatives were heuristics and are not used: the disposition
1432/// bits say a stream *has* an attached picture, not that this packet is
1433/// it; "the first packet on the stream" is an ordering assumption that
1434/// nothing in libavformat's contract fixes.
1435///
1436/// # The soundness argument, restated for this packet
1437///
1438/// It is the same one the hoisted-attachment road rests on, and it
1439/// holds here for the same reason. `AVStream.attached_pic` is written
1440/// once, while the container is being opened, and never again; the
1441/// reference this crate is looking at is the container's, held for the
1442/// lifetime of the `AVFormatContext`, and there is no
1443/// `ffmpeg_next::Packet` wrapping it for anyone to call `data_mut` on.
1444/// What the uniqueness rule guards against is a *safe Rust* handle that
1445/// may write while this crate reads, and the container's reference is
1446/// not one.
1447///
1448/// # Safety
1449///
1450/// `input` and `packet` must both be live for the duration of the call.
1451unsafe fn is_streams_attached_pic(input: &Input, index: usize, packet: &Packet) -> bool {
1452  // SAFETY: `input` owns a live `AVFormatContext`; `streams` is an
1453  // array of `nb_streams` pointers, and `index` is checked against it.
1454  let stream = unsafe {
1455    let context = input.as_ptr();
1456    if index >= (*context).nb_streams as usize {
1457      return false;
1458    }
1459    *(*context).streams.add(index)
1460  };
1461  if stream.is_null() {
1462    return false;
1463  }
1464  // SAFETY: `stream` is one of the context's own live `AVStream`s and
1465  // `packet` is live per this function's contract.
1466  unsafe { packet_is_parked_picture(stream, packet) }
1467}
1468
1469/// The identity itself: whether `packet`'s payload allocation is the
1470/// one `stream` has parked in `attached_pic`.
1471///
1472/// Split out from [`is_streams_attached_pic`] so the comparison can be
1473/// tested against a hand-built pair without forging an
1474/// `AVFormatContext` — see `a_queued_attached_picture_is_recognised`.
1475///
1476/// # Safety
1477///
1478/// `stream` must be a live `AVStream` and `packet` a live `AVPacket`.
1479unsafe fn packet_is_parked_picture(stream: *const AVStream, packet: &Packet) -> bool {
1480  use ffmpeg_next::packet::Ref;
1481
1482  // SAFETY: both are live per the contract; `attached_pic` is an inline
1483  // `AVPacket` and both `buf` fields may be null, which is answered
1484  // before either is read through.
1485  unsafe {
1486    let parked = (*stream).attached_pic.buf;
1487    let carried = (*packet.as_ptr()).buf;
1488    if parked.is_null() || carried.is_null() {
1489      return false;
1490    }
1491    // The shared `AVBuffer`, not the `AVBufferRef`: `av_packet_ref`
1492    // mints a new reference struct around the same allocation, so
1493    // comparing the references themselves would answer "no" to exactly
1494    // the case this is for.
1495    (*parked).buffer == (*carried).buffer
1496  }
1497}
1498
1499/// Whether a stream's disposition makes it an **attachment** — a
1500/// payload with no place on the timeline — rather than a timed track.
1501///
1502/// `AV_DISPOSITION_ATTACHED_PIC` alone says "cover art": one still
1503/// image, parked in `AVStream.attached_pic`, no timeline. But FFmpeg
1504/// pairs it with `AV_DISPOSITION_TIMED_THUMBNAILS` for a different
1505/// thing entirely — "the stream is sparse, and contains thumbnail
1506/// images, often corresponding to chapter markers", a flag its own
1507/// header documents as *only ever* used together with `ATTACHED_PIC`.
1508/// Such a stream has many images and every one of them has a
1509/// timestamp.
1510///
1511/// Classifying that as an attachment loses all but the first: the
1512/// attachment contract is exactly one packet, so the queue takes the
1513/// parked copy and the delivery loop drops every timed packet on the
1514/// track. It goes to the **`Video`** arm instead. That does not
1515/// contradict "cover art is an attachment, not video" — the reason
1516/// behind that ruling is that a single still with no timeline must not
1517/// look like a motion track, and a timed-thumbnail stream *is* on the
1518/// timeline. It is sparse video: a codec id, a frame size, a pixel
1519/// format and packets with timestamps, which is everything a consumer
1520/// needs to decode the images. The `Data` arm was the alternative and
1521/// is worse: it would strand encoded pictures in an arm that names no
1522/// decoder.
1523///
1524/// The bits are tested against the raw `AVStream.disposition` rather
1525/// than through `ffmpeg_next`'s `Disposition`, which mints no
1526/// `TIMED_THUMBNAILS` constant at all — its `from_bits_truncate` drops
1527/// every bit this build of the wrapper has no name for, which is how
1528/// the distinction went missing in the first place.
1529const fn is_attachment_disposition(disposition: c_int) -> bool {
1530  disposition & AV_DISPOSITION_ATTACHED_PIC != 0
1531    && disposition & AV_DISPOSITION_TIMED_THUMBNAILS == 0
1532}
1533
1534/// Upper bound on the NUL search in [`metadata_text`].
1535///
1536/// Generous by four orders of magnitude for a filename or a MIME type,
1537/// and there only so that a value libavutil did not terminate cannot
1538/// turn the walk into an unbounded read — the same discipline
1539/// [`crate::channel_layout`] and the pixel-format namer follow. A value
1540/// longer than this is refused rather than truncated: a truncated
1541/// filename is a different filename.
1542const METADATA_VALUE_MAX_BYTES: usize = 64 * 1024;
1543
1544/// Reads one entry out of a container's metadata dictionary as text
1545/// this crate can own.
1546///
1547/// **Why not `DictionaryRef::get`.** ffmpeg-next 9.0.0 builds its
1548/// `&str` with `from_utf8_unchecked`
1549/// (`src/util/dictionary/immutable.rs`), and FFmpeg does not validate
1550/// demuxed metadata as UTF-8 — an ID3 frame, a Matroska attachment
1551/// name or a MOV atom carries whatever bytes the file carries. A
1552/// `filename` holding a stray `0x80` would therefore have produced a
1553/// `&str` that is not UTF-8: undefined behaviour the moment it exists,
1554/// before `SmolStr` ever copies it.
1555///
1556/// Invalid bytes are replaced (`U+FFFD`), not refused. This is
1557/// *identity* metadata — the name a font was attached under, the MIME
1558/// type declared for a cover — and a file that names its attachment in
1559/// some legacy codepage is still a file worth opening. The replacement
1560/// characters say plainly that the container's bytes were not text.
1561///
1562/// # Safety
1563///
1564/// `dict` must be null or a live `*const AVDictionary` for the
1565/// duration of this call.
1566unsafe fn metadata_text(dict: *const AVDictionary, key: &CStr) -> Option<SmolStr> {
1567  if dict.is_null() {
1568    return None;
1569  }
1570  // SAFETY: `dict` is live per the contract above and `key` is a
1571  // NUL-terminated C string by construction; `av_dict_get` reads both
1572  // and returns a borrowed entry owned by the dictionary.
1573  let entry = unsafe { av_dict_get(dict, key.as_ptr(), std::ptr::null(), 0) };
1574  if entry.is_null() {
1575    return None;
1576  }
1577  // SAFETY: a non-null entry is a live `AVDictionaryEntry` for as long
1578  // as the dictionary is not modified, which it is not here.
1579  let value = unsafe { (*entry).value };
1580  if value.is_null() {
1581    return None;
1582  }
1583  for len in 0..METADATA_VALUE_MAX_BYTES {
1584    // SAFETY: `value` is a NUL-terminated string libavutil allocated
1585    // with `av_strdup`; the walk reads at most one byte past the last
1586    // value byte and stops at the terminator.
1587    if unsafe { *value.add(len).cast::<u8>() } == 0 {
1588      // SAFETY: the `len` bytes below the terminator were just walked,
1589      // so the slice is in bounds and initialised.
1590      let bytes = unsafe { std::slice::from_raw_parts(value.cast::<u8>(), len) };
1591      return Some(SmolStr::new(std::string::String::from_utf8_lossy(bytes)));
1592    }
1593  }
1594  None
1595}
1596
1597/// Wraps `AVStream.attached_pic` — the real packet libavformat parsed
1598/// for a cover-art stream — as this track's one attachment packet.
1599///
1600/// A stream that declares cover art but parks no payload still gets a
1601/// packet: an empty one, marked `synthesized`, because the contract is
1602/// one packet per attachment track and a consumer that sees an empty
1603/// payload learns something true about the file. The alternative shipped
1604/// once — waiting for the payload to arrive as a packet later — and it
1605/// cannot hold: nothing stops a timed packet, or a seek, from coming
1606/// first, so the track's packet would arrive out of order or never.
1607///
1608/// Measured before it was written: across MP3 (ID3 APIC), M4A (`covr`),
1609/// FLAC (`METADATA_BLOCK_PICTURE`) and Matroska (an `image/*`
1610/// attachment), every stream libavformat gives
1611/// `AV_DISPOSITION_ATTACHED_PIC` also carries the parked packet —
1612/// `ff_add_attached_pic` sets the disposition and fills
1613/// `attached_pic` in the same breath. The empty case is the honest
1614/// answer to a state this build's demuxers do not produce, not a
1615/// fallback anything relies on.
1616///
1617/// # Safety
1618///
1619/// `pkt` must be a live `*const AVPacket` — in practice the
1620/// `attached_pic` embedded in the `AVStream` at `index` — for the
1621/// duration of this call.
1622unsafe fn attached_pic_payload<C: crate::FfmpegCarrier + crate::CarrierOps>(
1623  pkt: *const ffmpeg_next::ffi::AVPacket,
1624  index: usize,
1625  limits: DemuxLimits,
1626) -> Result<AttachmentPacket<AttachmentPacketExtra, C::Buffer>, DemuxError> {
1627  // Already admitted: [`admit_streams`] charged this payload — and
1628  // every other attachment in the file — before `build_tracks`
1629  // allocated anything. The per-attachment budget is passed down as
1630  // this packet's own ceiling anyway, so the funnel is guarded even if
1631  // a future caller reaches it without the admission pass.
1632  //
1633  // SAFETY: `pkt` is live per the contract above.
1634  // **The container's own cover art**, whose buffer libavformat also
1635  // holds — see [`crate::buffer::PayloadProvenance`] for why that
1636  // second reference is not the hazard a caller's second `Packet` is.
1637  let captured = unsafe {
1638    crate::buffer::payload_of::<C>(
1639      pkt,
1640      limits.max_attachment_bytes(),
1641      crate::buffer::PayloadProvenance::AttachedPicture,
1642    )
1643  }
1644  .map_err(|source| DemuxError::PacketBuffer(PacketBuffer::new(index, source)))?;
1645  let extra = AttachmentPacketExtra::new(index as i32);
1646  Ok(match captured {
1647    Some(payload) => {
1648      // The hoisted packet's own flags, through the same raw reader the
1649      // five boundary conversions use. FFmpeg marks an attached picture
1650      // `AV_PKT_FLAG_KEY` — a still image is a keyframe if anything is
1651      // — and building this one with empty flags dropped that, along
1652      // with `CORRUPT` and every other bit the packet really carried.
1653      // SAFETY: `pkt` points at the live embedded `AVPacket`.
1654      let flags = unsafe { boundary::md_flags_from_av_packet(pkt) }
1655        .map_err(|source| DemuxError::PacketBuffer(PacketBuffer::new(index, source)))?;
1656      AttachmentPacket::new(payload, extra).with_flags(flags)
1657    }
1658    // Nothing was parked, so there are no flags to read: an empty set
1659    // is the honest answer for a packet this layer invented.
1660    None => AttachmentPacket::new(C::empty(), extra.with_synthesized(true)),
1661  })
1662}
1663
1664/// Builds an attachment payload out of a track's codec extradata — the
1665/// only place a font's bytes ever live, since an
1666/// `AVMEDIA_TYPE_ATTACHMENT` stream produces no packets at all.
1667///
1668/// A track with no extradata still gets a packet, with an empty
1669/// payload: the contract is one packet per attachment track, and a
1670/// consumer that sees an empty one learns something true about the
1671/// file. Only an allocation failure is an error.
1672fn extradata_payload<C: crate::FfmpegCarrier + crate::CarrierOps>(
1673  stream: &ffmpeg_next::format::stream::Stream<'_>,
1674  limits: DemuxLimits,
1675) -> Result<AttachmentPacket<AttachmentPacketExtra, C::Buffer>, DemuxError> {
1676  let index = stream.index();
1677  let parameters = stream.parameters();
1678  // SAFETY: `parameters` keeps the `AVCodecParameters` live;
1679  // `extradata` / `extradata_size` are public fields.
1680  let par = unsafe { parameters.as_ptr() };
1681  let ptr = unsafe { (*par).extradata };
1682  let len = unsafe { (*par).extradata_size }.max(0) as usize;
1683  // Already admitted, exactly as on the hoisted cover-art path — see
1684  // [`admit_streams`]. Re-judged here against the per-attachment
1685  // ceiling alone, so the helper is safe to call on its own.
1686  if len > limits.max_attachment_bytes() {
1687    return Err(DemuxError::AttachmentTooLarge(AttachmentTooLarge::new(
1688      index,
1689      len,
1690      limits.max_attachment_bytes(),
1691    )));
1692  }
1693  let bytes: &[u8] = if ptr.is_null() || len == 0 {
1694    &[]
1695  } else {
1696    // SAFETY: libavformat guarantees `extradata` is readable for
1697    // `extradata_size` bytes (plus its padding) while the parameters
1698    // live, and the slice is consumed before this function returns.
1699    unsafe { std::slice::from_raw_parts(ptr, len) }
1700  };
1701  // Extradata is a plain allocation with no `AVBufferRef` behind it —
1702  // an `AVMEDIA_TYPE_ATTACHMENT` stream produces no packets, so a
1703  // font's bytes never live in a refcounted buffer. **Both** lanes copy
1704  // here, which is what `from_bytes` is for.
1705  Ok(AttachmentPacket::new(
1706    C::from_bytes(bytes).ok_or_else(|| {
1707      DemuxError::PacketBuffer(PacketBuffer::new(
1708        index,
1709        crate::buffer::PacketBufferError::CaptureFailed(crate::buffer::CaptureFailed::new(len)),
1710      ))
1711    })?,
1712    AttachmentPacketExtra::new(index as i32).with_synthesized(true),
1713  ))
1714}
1715
1716/// **The admission pass**: judges every stream in the file before the
1717/// track table allocates anything at all.
1718///
1719/// # Why this cannot live inside the capture
1720///
1721/// It used to, and that was a bypass. `build_tracks` deep-copies each
1722/// stream's `AVCodecParameters` on its way to building a `TrackExtra`,
1723/// and for an `AVMEDIA_TYPE_ATTACHMENT` stream **the extradata inside
1724/// those parameters is the attachment's payload**. So the loop paid for
1725/// the payload — a full `avcodec_parameters_copy` — one statement
1726/// before asking whether it was allowed to. A file declaring a gigabyte
1727/// of "font" allocated the gigabyte and then reported that a gigabyte
1728/// was too much.
1729///
1730/// The fix is not a check moved a few lines earlier: any per-track
1731/// interleaving of judging and paying has the same shape, because the
1732/// aggregate budget is only knowable once every track has been *seen*.
1733/// So the whole file is admitted here, in a pass that allocates
1734/// nothing — it reads two integers per stream — and only a container
1735/// that passes in full reaches the loop that builds carriers and
1736/// parameter copies.
1737///
1738/// # Why it is every stream, not every attachment
1739///
1740/// Because the track table copies **every** stream's codec parameters,
1741/// and `AVCodecParameters` reaches the heap three ways — `extradata`,
1742/// every `coded_side_data` entry, a custom channel map — all of them
1743/// sized by the file. A pass that walked only attachment streams left
1744/// the other road wide open: a MOV puts an ICC profile in
1745/// `coded_side_data`, on an ordinary video track, and the wholesale
1746/// copy took it before anything asked how big it was. That was the same
1747/// class of defect three review rounds running, which is why the copy
1748/// itself is gone (see
1749/// [`bounded_clone_parameters`](crate::extras::bounded_clone_parameters))
1750/// and why this pass sees everything.
1751///
1752/// # What is charged
1753///
1754/// The bytes this session will **retain**, which is not always the
1755/// declared size:
1756///
1757/// - every stream is charged its parameter clone's footprint against
1758///   the per-stream and whole-file codec-parameter budgets;
1759/// - a synthesized attachment's `extradata` is charged to the
1760///   *attachment* budget and left out of the parameter one, because the
1761///   clone strips it and the carrier holds it — one set of bytes, one
1762///   charge;
1763/// - the attachment budgets then see:
1764///
1765/// - a hoisted cover-art track retains its parked `AVPacket`'s payload
1766///   *and* the extradata in its parameter copy, which the still decoder
1767///   may need and which is not a duplicate of the payload;
1768/// - a synthesized `AVMEDIA_TYPE_ATTACHMENT` track retains only the
1769///   carrier, because `build_tracks` strips the duplicate extradata out
1770///   of the parameter copy (see the comment there for the census).
1771///
1772/// Charging residency rather than payload is what keeps the budget an
1773/// honest statement about memory instead of about file structure.
1774///
1775/// The per-attachment ceiling is judged first for each track: when a
1776/// single payload is itself over the line, that is the more specific
1777/// fact, and naming the aggregate instead would send a reader looking
1778/// for four hundred attachments that are not there.
1779fn admit_streams(input: &Input, limits: DemuxLimits) -> Result<(), DemuxError> {
1780  let mut attachment_spent: usize = 0;
1781  let mut parameter_spent: usize = 0;
1782
1783  for stream in input.streams() {
1784    let index = stream.index();
1785    let parameters = stream.parameters();
1786    // SAFETY: `parameters` keeps the `AVCodecParameters` live for this
1787    // measurement, which allocates nothing and dereferences only what
1788    // it counts.
1789    let par = unsafe { parameters.as_ptr() };
1790    if par.is_null() {
1791      return Err(DemuxError::ParametersMissing(ParametersMissing::new(index)));
1792    }
1793    let footprint =
1794      unsafe { crate::extras::measure_parameters(par) }.ok_or(DemuxError::ParametersTooLarge(
1795        ParametersTooLarge::new(index, usize::MAX, limits.max_codec_parameter_bytes()),
1796      ))?;
1797
1798    // SAFETY: `stream` keeps the `AVStream` live; `disposition` is a
1799    // public field.
1800    let disposition = unsafe { (*stream.as_ptr()).disposition };
1801    let cover_art = is_attachment_disposition(disposition);
1802    let synthesized = !cover_art && boundary::media_kind_of(&parameters).is_attachment();
1803
1804    // What the *parameter clone* will retain for this stream. The
1805    // synthesized-attachment road strips `extradata` — the font's
1806    // payload rides the carrier instead — so counting it here would
1807    // charge the same bytes twice and make the budget a statement about
1808    // the file rather than about memory.
1809    let retained_parameters = if synthesized {
1810      footprint.total_without_extradata()
1811    } else {
1812      footprint.total()
1813    }
1814    .ok_or(DemuxError::ParametersTooLarge(ParametersTooLarge::new(
1815      index,
1816      usize::MAX,
1817      limits.max_codec_parameter_bytes(),
1818    )))?;
1819
1820    if retained_parameters > limits.max_codec_parameter_bytes() {
1821      return Err(DemuxError::ParametersTooLarge(ParametersTooLarge::new(
1822        index,
1823        retained_parameters,
1824        limits.max_codec_parameter_bytes(),
1825      )));
1826    }
1827    parameter_spent = parameter_spent.saturating_add(retained_parameters);
1828    if parameter_spent > limits.max_total_codec_parameter_bytes() {
1829      return Err(DemuxError::ParametersBudgetExhausted(
1830        ParametersBudgetExhausted::new(
1831          index,
1832          parameter_spent,
1833          limits.max_total_codec_parameter_bytes(),
1834        ),
1835      ));
1836    }
1837
1838    // And what the *carrier* will hold, for the two attachment roads.
1839    let carrier = if cover_art {
1840      // SAFETY: `attached_pic` is an `AVPacket` embedded in the
1841      // `AVStream` by value; `addr_of!` reaches its `size` without
1842      // forming a reference to the stream.
1843      unsafe {
1844        let pkt = std::ptr::addr_of!((*stream.as_ptr()).attached_pic);
1845        (*pkt).size
1846      }
1847      .max(0) as usize
1848    } else if synthesized {
1849      // The **payload**, not the padded clone figure. The carrier is
1850      // an `FfmpegBytes` over exactly these bytes and the clone omits
1851      // extradata entirely on this road, so nothing here allocates the
1852      // padding — charging it would bill sixty-four bytes that are
1853      // never spent, reject a payload in the last sixty-four below the
1854      // ceiling, and disagree with the image road about the same file
1855      // at exactly the cap.
1856      footprint.extradata_payload()
1857    } else {
1858      // Not an attachment: nothing is captured eagerly for it, so
1859      // nothing more is charged.
1860      continue;
1861    };
1862
1863    charge_attachment(index, carrier, limits, &mut attachment_spent)?;
1864  }
1865  Ok(())
1866}
1867
1868/// Charges `declared` bytes against both attachment budgets, refusing
1869/// before anything is copied. The one place a file's attachment
1870/// spending is decided; see [`admit_streams`] for when it runs.
1871fn charge_attachment(
1872  index: usize,
1873  declared: usize,
1874  limits: DemuxLimits,
1875  spent: &mut usize,
1876) -> Result<(), DemuxError> {
1877  if declared > limits.max_attachment_bytes() {
1878    return Err(DemuxError::AttachmentTooLarge(AttachmentTooLarge::new(
1879      index,
1880      declared,
1881      limits.max_attachment_bytes(),
1882    )));
1883  }
1884  let total = spent.saturating_add(declared);
1885  if total > limits.max_total_attachment_bytes() {
1886    return Err(DemuxError::AttachmentBudgetExhausted(
1887      AttachmentBudgetExhausted::new(index, total, limits.max_total_attachment_bytes()),
1888    ));
1889  }
1890  *spent = total;
1891  Ok(())
1892}
1893
1894/// A stream's `AVRational` timebase as a [`Timebase`]. A zero or
1895/// negative denominator is clamped to 1 rather than refused: a
1896/// malformed timebase makes the track's timestamps meaningless, not the
1897/// file unreadable, and every other track still demuxes.
1898fn rational_to_timebase(value: Rational) -> Timebase {
1899  Timebase::new(
1900    value.numerator(),
1901    NonZeroI32::new(value.denominator().max(1)).expect("clamped to at least 1"),
1902  )
1903}
1904
1905/// A frame *rate* as a rate-shaped [`Timebase`] (`30000/1001` for
1906/// 29.97 fps), or `None` when the container declares none.
1907fn rate_to_timebase(value: Rational) -> Option<Timebase> {
1908  let (num, den) = (value.numerator(), value.denominator());
1909  (num > 0 && den > 0).then(|| Timebase::new(num, NonZeroI32::new(den).expect("checked above")))
1910}
1911
1912#[cfg(test)]
1913mod tests {
1914  use ffmpeg_next::ffi::{av_dict_free, av_dict_set};
1915
1916  use ffmpeg_next::codec::Parameters;
1917
1918  use super::*;
1919  use crate::extras::TrackExtra;
1920
1921  /// Builds a dictionary holding one entry whose *value* is the given
1922  /// raw bytes. The bytes go in as a C string, which is all
1923  /// `av_dict_set` promises to copy — FFmpeg never asks whether they
1924  /// are UTF-8, which is the whole point of the lane below.
1925  fn dict_with(key: &CStr, value: &[u8]) -> *mut AVDictionary {
1926    let mut dict: *mut AVDictionary = std::ptr::null_mut();
1927    let mut terminated = value.to_vec();
1928    terminated.push(0);
1929    let rc = unsafe {
1930      av_dict_set(
1931        &mut dict,
1932        key.as_ptr(),
1933        terminated.as_ptr().cast::<std::ffi::c_char>(),
1934        0,
1935      )
1936    };
1937    assert!(rc >= 0, "av_dict_set failed: {rc}");
1938    dict
1939  }
1940
1941  #[test]
1942  fn metadata_that_is_not_utf8_is_read_lossily_not_unsoundly() {
1943    // The bytes a real container can hold: a Latin-1 "café.ttf" whose
1944    // 0xE9 is not valid UTF-8 on its own. Read through
1945    // `DictionaryRef::get` this produced a `&str` that violates the
1946    // type's invariant — undefined behaviour before `SmolStr` ever
1947    // copied it.
1948    let raw = b"caf\xE9.ttf".to_vec();
1949    assert!(
1950      std::str::from_utf8(&raw).is_err(),
1951      "the source bytes really are not UTF-8",
1952    );
1953    let dict = dict_with(c"filename", &raw);
1954    let text = unsafe { metadata_text(dict, c"filename") }.expect("the entry exists");
1955    assert_eq!(text.as_str(), "caf\u{FFFD}.ttf");
1956    // A key the dictionary does not hold, and a null dictionary, are
1957    // both simply absent.
1958    assert_eq!(unsafe { metadata_text(dict, c"mimetype") }, None);
1959    assert_eq!(
1960      unsafe { metadata_text(std::ptr::null(), c"filename") },
1961      None
1962    );
1963    unsafe { av_dict_free(&mut { dict }) };
1964  }
1965
1966  #[test]
1967  fn valid_metadata_survives_unchanged() {
1968    let dict = dict_with(c"mimetype", b"application/x-truetype-font");
1969    assert_eq!(
1970      unsafe { metadata_text(dict, c"mimetype") }.as_deref(),
1971      Some("application/x-truetype-font"),
1972    );
1973    unsafe { av_dict_free(&mut { dict }) };
1974  }
1975
1976  #[test]
1977  fn an_unterminated_length_is_refused_rather_than_truncated() {
1978    // Nothing libavutil produces is this long; the cap exists so a
1979    // value it did not terminate cannot walk off the end. A value that
1980    // reaches the cap is absent, never a prefix of itself.
1981    let long = vec![b'a'; METADATA_VALUE_MAX_BYTES + 1];
1982    let dict = dict_with(c"filename", &long);
1983    assert_eq!(unsafe { metadata_text(dict, c"filename") }, None);
1984    unsafe { av_dict_free(&mut { dict }) };
1985  }
1986
1987  /// A reader that panics with a payload whose destructor panics in
1988  /// turn. Both panics are safe code; the second one is what used to
1989  /// leave the guard and enter the `extern "C"` AVIO callback.
1990  struct PanicsWithAHostilePayload;
1991
1992  struct PanicOnDrop;
1993
1994  impl Drop for PanicOnDrop {
1995    fn drop(&mut self) {
1996      panic!("and the payload went too");
1997    }
1998  }
1999
2000  impl std::io::Read for PanicsWithAHostilePayload {
2001    fn read(&mut self, _buf: &mut [u8]) -> std::io::Result<usize> {
2002      std::panic::panic_any(PanicOnDrop);
2003    }
2004  }
2005
2006  impl std::io::Seek for PanicsWithAHostilePayload {
2007    fn seek(&mut self, _pos: std::io::SeekFrom) -> std::io::Result<u64> {
2008      std::panic::panic_any(PanicOnDrop);
2009    }
2010  }
2011
2012  #[test]
2013  fn a_reader_panic_with_a_hostile_payload_does_not_abort_the_process() {
2014    // In its own process, because the assertion *is* the process: a
2015    // parent that sees the child exit cleanly has seen the abort not
2016    // happen. The guard caught the reader's panic and then dropped its
2017    // payload outside `catch_unwind`, so a payload whose `Drop` panics
2018    // sent that second panic straight out of `read` and into C —
2019    // through the very guard that exists to stop it.
2020    crate::fault_subprocess::in_subprocess(
2021      "demuxer::tests::a_reader_panic_with_a_hostile_payload_does_not_abort_the_process",
2022      || {
2023        let previous = std::panic::take_hook();
2024        std::panic::set_hook(Box::new(|_| {}));
2025        let opened =
2026          CarrierDemuxer::<crate::Owned>::open_reader(PanicsWithAHostilePayload, Some("x.mkv"));
2027        std::panic::set_hook(previous);
2028        match opened {
2029          Err(DemuxError::ReaderPanic(_)) => {}
2030          Err(other) => panic!("expected ReaderPanic, got {other:?}"),
2031          Ok(_) => panic!("a reader that only panics cannot open a container"),
2032        }
2033      },
2034    );
2035  }
2036
2037  #[test]
2038  fn codec_parameters_that_cannot_be_allocated_are_named() {
2039    // `Parameters::new` does not check `avcodec_parameters_alloc`, and
2040    // `clone_from` dereferences the result immediately: under a failed
2041    // allocation the shipped clone would write through null.
2042    crate::fault_subprocess::in_subprocess(
2043      "demuxer::tests::codec_parameters_that_cannot_be_allocated_are_named",
2044      || {
2045        let source = Parameters::new();
2046        assert!(
2047          !unsafe { source.as_ptr() }.is_null(),
2048          "the source allocates before the cap goes on",
2049        );
2050        crate::fault_subprocess::cap_ffmpeg_allocations(1);
2051        let refused = crate::extras::bounded_clone_parameters(&source, 4, usize::MAX);
2052        crate::fault_subprocess::uncap_ffmpeg_allocations();
2053        assert!(
2054          matches!(
2055            refused,
2056            Err(DemuxError::ParametersAlloc(ref p)) if p.stream_index() == 4
2057          ),
2058          "expected ParametersAlloc, got {:?}",
2059          refused.map(|_| ()),
2060        );
2061        // And with the cap lifted the same copy succeeds, so the
2062        // refusal was the allocator's answer and not a broken helper.
2063        crate::extras::bounded_clone_parameters(&source, 4, usize::MAX).expect("an uncapped copy");
2064      },
2065    );
2066  }
2067
2068  #[test]
2069  fn the_public_track_extra_handoffs_still_answer_the_allocator() {
2070    // The lane this replaces guarded a hazard that no longer exists:
2071    // `TrackExtra` derived `Clone` over `ffmpeg_next`'s `Parameters`,
2072    // whose clone dereferences an unchecked allocation, so safe public
2073    // code reached a SIGSEGV by copying a track row. The row holds no
2074    // `Parameters` at all now, and the two public handoffs have split
2075    // in kind because of it:
2076    //
2077    // * `Clone` allocates **nothing from FFmpeg** — it copies an
2078    //   owned mirror, which is a `Vec` spine and a refcount bump — so
2079    //   it survives a capped allocator rather than reporting through
2080    //   one. That is what makes the derive honest under the carrier
2081    //   law, and a capped allocator is the only way to pin it.
2082    // * `clone_parameters` is the rebuild, and it is where FFmpeg
2083    //   allocation moved to. It still answers.
2084    crate::fault_subprocess::in_subprocess(
2085      "demuxer::tests::the_public_track_extra_handoffs_still_answer_the_allocator",
2086      || {
2087        let source = Parameters::new();
2088        assert!(!unsafe { source.as_ptr() }.is_null(), "allocated uncapped");
2089        let extra = TrackExtra::new(
2090          6,
2091          crate::ticket::CodecTicket::mirror(&source, 6, usize::MAX).expect("uncapped"),
2092        );
2093
2094        crate::fault_subprocess::cap_ffmpeg_allocations(1);
2095        let cloned = extra.clone();
2096        let handed = extra.clone_parameters().map(|_| ());
2097        crate::fault_subprocess::uncap_ffmpeg_allocations();
2098
2099        assert_eq!(
2100          cloned.parameter_bytes(),
2101          extra.parameter_bytes(),
2102          "the row cloned under an allocator that refuses everything",
2103        );
2104        assert!(
2105          matches!(handed, Err(DemuxError::ParametersAlloc(ref p)) if p.stream_index() == 6),
2106          "TrackExtra::clone_parameters: {handed:?}",
2107        );
2108
2109        // And the rebuild works once the allocator does.
2110        extra.clone_parameters().expect("an uncapped handoff");
2111      },
2112    );
2113  }
2114
2115  #[test]
2116  fn parameters_that_never_allocated_are_refused_at_the_door() {
2117    // The route the destination check could not see. A safe
2118    // `Parameters::new()` under a failed allocation hands back a
2119    // null-backed value and says nothing; the copier then allocated its
2120    // own destination happily — the allocator having recovered by
2121    // then — and called `avcodec_parameters_copy(out, NULL)`, which
2122    // dereferences its source. Same crash, one recovery later, still
2123    // from safe public code.
2124    crate::fault_subprocess::in_subprocess(
2125      "demuxer::tests::parameters_that_never_allocated_are_refused_at_the_door",
2126      || {
2127        // The cap is on *while the source is built* — that is the whole
2128        // difference from the destination lane.
2129        crate::fault_subprocess::cap_ffmpeg_allocations(1);
2130        let never_allocated = Parameters::new();
2131        crate::fault_subprocess::uncap_ffmpeg_allocations();
2132        assert!(
2133          unsafe { never_allocated.as_ptr() }.is_null(),
2134          "the safe constructor really does hand back a null-backed value",
2135        );
2136
2137        // The door moved with the handle. `TrackExtra::new` no longer
2138        // takes a `Parameters` at all, so the only way a null-backed
2139        // one reaches a track row is through the mirror — which is
2140        // where the check now lives, and where it belongs: beside the
2141        // raw pointer rather than one type downstream of it.
2142        let refused = crate::ticket::CodecTicket::mirror(&never_allocated, 9, usize::MAX);
2143        let Err(DemuxError::ParametersMissing(p)) = refused.map(|_| ()) else {
2144          panic!("a null-backed source must not become a codec ticket");
2145        };
2146        assert_eq!(p.stream_index(), 9);
2147
2148        // And the copier refuses it too, so the invariant is not the
2149        // only thing standing between this and a null dereference.
2150        let never_allocated = {
2151          crate::fault_subprocess::cap_ffmpeg_allocations(1);
2152          let p = Parameters::new();
2153          crate::fault_subprocess::uncap_ffmpeg_allocations();
2154          p
2155        };
2156        assert!(matches!(
2157          crate::extras::bounded_clone_parameters(&never_allocated, 9, usize::MAX).map(|_| ()),
2158          Err(DemuxError::ParametersMissing(p)) if p.stream_index() == 9,
2159        ));
2160
2161        // A row built over real parameters still hands off both ways,
2162        // so the refusal is about the null and nothing else.
2163        let real = Parameters::new();
2164        let extra = TrackExtra::new(
2165          9,
2166          crate::ticket::CodecTicket::mirror(&real, 9, usize::MAX).expect("real parameters"),
2167        );
2168        let _ = extra.clone();
2169        extra.clone_parameters().expect("handoff");
2170      },
2171    );
2172  }
2173
2174  #[cfg(feature = "resample")]
2175  #[test]
2176  fn a_spec_read_from_parameters_that_never_allocated_is_absent() {
2177    // The same trap at another public door, found by the sweep:
2178    // `ResampleSpec::from_parameters` asks `parameters.medium()`
2179    // first, and *that* dereferences the pointer inside ffmpeg-next
2180    // before any code of ours runs.
2181    crate::fault_subprocess::in_subprocess(
2182      "demuxer::tests::a_spec_read_from_parameters_that_never_allocated_is_absent",
2183      || {
2184        crate::fault_subprocess::cap_ffmpeg_allocations(1);
2185        let never_allocated = Parameters::new();
2186        crate::fault_subprocess::uncap_ffmpeg_allocations();
2187        assert!(unsafe { never_allocated.as_ptr() }.is_null());
2188        assert_eq!(
2189          crate::ResampleSpec::from_parameters(&never_allocated),
2190          None,
2191          "parameters that do not exist describe no audio",
2192        );
2193      },
2194    );
2195  }
2196
2197  #[test]
2198  fn codec_parameters_whose_copy_fails_are_named() {
2199    // The other leg: the destination allocates, and the deep copy of
2200    // the extradata does not. `clone_from` discards that return value,
2201    // so the shipped clone handed back parameters missing the very
2202    // bytes a decoder needs to open — and said nothing.
2203    crate::fault_subprocess::in_subprocess(
2204      "demuxer::tests::codec_parameters_whose_copy_fails_are_named",
2205      || {
2206        const EXTRADATA: usize = 8 * 1024 * 1024;
2207        let mut source = Parameters::new();
2208        // SAFETY: `source` owns a live `AVCodecParameters`; the buffer
2209        // comes from FFmpeg's allocator and is handed to it, so
2210        // `avcodec_parameters_free` releases it with the rest.
2211        unsafe {
2212          let par = source.as_mut_ptr();
2213          let extradata = ffmpeg_next::ffi::av_mallocz(EXTRADATA) as *mut u8;
2214          assert!(!extradata.is_null(), "av_mallocz");
2215          (*par).extradata = extradata;
2216          (*par).extradata_size = EXTRADATA as i32;
2217        }
2218
2219        // Big enough for the destination `AVCodecParameters`, far too
2220        // small for its extradata.
2221        crate::fault_subprocess::cap_ffmpeg_allocations(64 * 1024);
2222        let refused = crate::extras::bounded_clone_parameters(&source, 2, usize::MAX);
2223        crate::fault_subprocess::uncap_ffmpeg_allocations();
2224        match refused {
2225          Err(DemuxError::ParametersCopy(p)) => assert_eq!(p.stream_index(), 2),
2226          Err(other) => panic!("expected ParametersCopy, got {other:?}"),
2227          Ok(_) => panic!("a copy that could not copy the extradata must not succeed"),
2228        }
2229        crate::extras::bounded_clone_parameters(&source, 2, usize::MAX).expect("an uncapped copy");
2230      },
2231    );
2232  }
2233
2234  /// A stream whose `attached_pic` is `parked`, and the packet
2235  /// libavformat would queue for it.
2236  ///
2237  /// # On the fixture road
2238  ///
2239  /// The container shape this guards — a stream carrying
2240  /// `ATTACHED_PIC | TIMED_THUMBNAILS` — **cannot be minted by the
2241  /// ffmpeg CLI**, and that was censused rather than assumed: no muxer
2242  /// has a field for those bits (`-disposition:v
2243  /// attached_pic+timed_thumbnails` round-trips to nothing through
2244  /// mp4, mov and matroska alike), because the mov *demuxer* derives
2245  /// them from a chapter-track reference its own muxer does not write
2246  /// in that direction.
2247  ///
2248  /// What is reproducible, and what actually matters, is the **packet
2249  /// shape**: `read_frame_internal` queues a stream's parked picture
2250  /// with `av_packet_ref` while keeping its own reference, which is
2251  /// exactly what `av_packet_ref` builds here. The classification half
2252  /// — that such a stream is video rather than an attachment — is
2253  /// pinned separately by
2254  /// [`a_timed_thumbnail_stream_is_not_an_attachment`].
2255  fn parked_picture_stream(parked: &Packet) -> (Box<AVStream>, Packet) {
2256    use ffmpeg_next::packet::{Mut, Ref};
2257
2258    let mut stream: Box<AVStream> = Box::new(unsafe { std::mem::zeroed() });
2259    let mut queued = Packet::empty();
2260    // SAFETY: `parked` is a live refcounted packet; `av_packet_ref`
2261    // takes a reference to its buffer, which is precisely what
2262    // libavformat does when it queues an attached picture. The stream
2263    // is zeroed apart from the one field the probe reads.
2264    unsafe {
2265      assert_eq!(
2266        ffmpeg_next::ffi::av_packet_ref(queued.as_mut_ptr(), parked.as_ptr()),
2267        0,
2268      );
2269      stream.attached_pic.buf = (*parked.as_ptr()).buf;
2270      stream.attached_pic.data = (*parked.as_ptr()).data;
2271      stream.attached_pic.size = (*parked.as_ptr()).size;
2272    }
2273    (stream, queued)
2274  }
2275
2276  #[test]
2277  fn a_queued_attached_picture_is_recognised() {
2278    use ffmpeg_next::packet::Ref;
2279
2280    let parked = Packet::copy(&[9u8; 2048]);
2281    let (stream, queued) = parked_picture_stream(&parked);
2282
2283    // The two references are different structs around one allocation —
2284    // which is the whole reason the probe compares `buffer` and not the
2285    // `AVBufferRef`. Asserting the difference is what makes this a test
2286    // of the right comparison rather than of a lucky one.
2287    // SAFETY: both packets are live.
2288    unsafe {
2289      assert_ne!(
2290        (*queued.as_ptr()).buf,
2291        (*parked.as_ptr()).buf,
2292        "av_packet_ref must mint a new reference struct",
2293      );
2294    }
2295    // SAFETY: the stream is a zeroed `AVStream` whose only populated
2296    // fields are the ones the probe reads, and `queued` is live.
2297    assert!(unsafe { packet_is_parked_picture(&*stream, &queued) });
2298
2299    // An ordinary timed packet — the shape every pull after the first
2300    // one has — is not the parked picture.
2301    let ordinary = Packet::copy(&[1u8; 2048]);
2302    // SAFETY: as above.
2303    assert!(!unsafe { packet_is_parked_picture(&*stream, &ordinary) });
2304
2305    // And a stream that parks nothing recognises nothing.
2306    let bare: Box<AVStream> = Box::new(unsafe { std::mem::zeroed() });
2307    // SAFETY: as above.
2308    assert!(!unsafe { packet_is_parked_picture(&*bare, &queued) });
2309  }
2310
2311  #[test]
2312  fn the_queued_picture_is_admitted_and_later_packets_take_the_ordinary_road() {
2313    use crate::buffer::{PacketBufferError, PayloadProvenance, payload_of};
2314    use ffmpeg_next::packet::Ref;
2315
2316    let parked = Packet::copy(&[9u8; 2048]);
2317    let (_stream, queued) = parked_picture_stream(&parked);
2318    // SAFETY: the packet is live; `buf` is a public field.
2319    let parked_buffer = unsafe { (*parked.as_ptr()).buf };
2320
2321    // **The first pull.** Two references, one of them the container's.
2322    // From a *caller* that shape is refused, because a caller's second
2323    // reference may be a `Packet` with a safe `data_mut`.
2324    // SAFETY: `queued` is live for every call in this test.
2325    assert!(matches!(
2326      unsafe {
2327        payload_of::<crate::View>(
2328          queued.as_ptr(),
2329          usize::MAX,
2330          PayloadProvenance::CallerSupplied,
2331        )
2332      },
2333      Err(PacketBufferError::SharedPayload(_)),
2334    ));
2335
2336    // Delivered by the demux loop, the same shape is carried — by copy,
2337    // because a window would outlive the exclusivity the read rests on.
2338    // SAFETY: as above.
2339    let copied = unsafe {
2340      payload_of::<crate::View>(
2341        queued.as_ptr(),
2342        usize::MAX,
2343        PayloadProvenance::DemuxDelivered,
2344      )
2345    }
2346    .expect("a demux-delivered shared payload is carriable")
2347    .expect("it has a payload");
2348    assert_eq!(copied.as_ref(), &[9u8; 2048][..]);
2349    // SAFETY: the packet is live; `data` is a public field.
2350    unsafe {
2351      assert_ne!(
2352        copied.as_ref().as_ptr() as usize,
2353        (*queued.as_ptr()).data as usize,
2354        "a shared demux-delivered payload is copied, not windowed",
2355      );
2356    }
2357
2358    // With the provenance the probe establishes, both lanes carry it.
2359    // SAFETY: as above.
2360    let viewed = unsafe {
2361      payload_of::<crate::View>(
2362        queued.as_ptr(),
2363        usize::MAX,
2364        PayloadProvenance::AttachedPicture,
2365      )
2366    }
2367    .expect("the container's own picture is carriable")
2368    .expect("it has a payload");
2369    assert_eq!(viewed.as_ref(), &[9u8; 2048][..]);
2370    // And on the view lane it is a window into the parked allocation
2371    // rather than a copy of it.
2372    // SAFETY: both are live; `data`/`size` are public fields.
2373    unsafe {
2374      let start = (*parked_buffer).data as usize;
2375      let end = start + (*parked_buffer).size;
2376      let at = viewed.as_ref().as_ptr() as usize;
2377      assert!(
2378        at >= start && at + viewed.len() <= end,
2379        "the queued picture must be viewed, not copied",
2380      );
2381    }
2382    // SAFETY: as above.
2383    let owned = unsafe {
2384      payload_of::<crate::Owned>(
2385        queued.as_ptr(),
2386        usize::MAX,
2387        PayloadProvenance::AttachedPicture,
2388      )
2389    }
2390    .expect("the owned lane carries it too")
2391    .expect("it has a payload");
2392    assert_eq!(owned.as_ref(), &[9u8; 2048][..]);
2393
2394    // **Every pull after it.** A timed packet has a buffer of its own,
2395    // so it stays on the `Delivered` road, is unique, and the view lane
2396    // shares it.
2397    let later = Packet::copy(&[4u8; 1024]);
2398    // SAFETY: `later` is live.
2399    let shared = unsafe {
2400      payload_of::<crate::View>(
2401        later.as_ptr(),
2402        usize::MAX,
2403        PayloadProvenance::DemuxDelivered,
2404      )
2405    }
2406    .expect("an ordinary packet is carriable")
2407    .expect("it has a payload");
2408    // SAFETY: as above.
2409    unsafe {
2410      assert_eq!(
2411        shared.as_ref().as_ptr() as usize,
2412        (*later.as_ptr()).data as usize,
2413        "a uniquely-referenced packet is still shared, not copied",
2414      );
2415    }
2416  }
2417
2418  #[test]
2419  fn a_timed_thumbnail_stream_is_not_an_attachment() {
2420    // `TIMED_THUMBNAILS` is documented as only ever appearing beside
2421    // `ATTACHED_PIC`, so testing the picture bit alone reads a sparse
2422    // chapter-thumbnail track as cover art — and the attachment
2423    // contract then delivers exactly one of its images and drops the
2424    // rest, every one of which had a timestamp.
2425    assert!(
2426      is_attachment_disposition(AV_DISPOSITION_ATTACHED_PIC),
2427      "a plain attached picture is still an attachment",
2428    );
2429    assert!(
2430      !is_attachment_disposition(AV_DISPOSITION_ATTACHED_PIC | AV_DISPOSITION_TIMED_THUMBNAILS),
2431      "a timed-thumbnail stream is a timed track, whatever else it is flagged",
2432    );
2433    // Neither bit, and the other bits that ride along, change nothing.
2434    assert!(!is_attachment_disposition(0));
2435    assert!(!is_attachment_disposition(AV_DISPOSITION_TIMED_THUMBNAILS));
2436    assert!(is_attachment_disposition(
2437      AV_DISPOSITION_ATTACHED_PIC | ffmpeg_next::ffi::AV_DISPOSITION_DEFAULT
2438    ));
2439    // And the reason the raw bits are read at all: the wrapper's own
2440    // flag set cannot express the distinction.
2441    assert!(
2442      ffmpeg_next::format::stream::Disposition::from_bits(AV_DISPOSITION_TIMED_THUMBNAILS)
2443        .is_none(),
2444      "ffmpeg_next mints no TIMED_THUMBNAILS bit — from_bits_truncate would drop it silently",
2445    );
2446  }
2447
2448  #[test]
2449  fn an_uncapturable_cover_still_gets_its_one_packet() {
2450    // The state the shipped `AwaitingPacket` fallback existed for: a
2451    // stream that declares cover art and parks no payload. The fallback
2452    // waited for a packet that may never come, and let timed packets —
2453    // and seeks — go first, which the face forbids. The track now gets
2454    // its one packet at open like every other attachment track: empty,
2455    // and marked as this layer's own work.
2456    //
2457    // Not reachable from a file: across MP3, M4A, FLAC and Matroska,
2458    // every ATTACHED_PIC stream libavformat produces carries the parked
2459    // packet, because `ff_add_attached_pic` sets the disposition and
2460    // fills it in the same call. A zeroed `AVPacket` is exactly what
2461    // `attached_pic` would hold if one ever did not.
2462    let empty: ffmpeg_next::ffi::AVPacket = unsafe { std::mem::zeroed() };
2463    let packet = unsafe { attached_pic_payload::<crate::Owned>(&empty, 7, DemuxLimits::default()) }
2464      .expect("an unparked cover is a degenerate track, not an unreadable file");
2465    assert!(packet.data().as_ref().is_empty());
2466    assert!(
2467      packet.extra().synthesized(),
2468      "nothing in the container handed this payload over",
2469    );
2470    assert_eq!(packet.extra().stream_index(), 7);
2471  }
2472
2473  #[test]
2474  fn a_zero_denominator_timebase_is_clamped_not_refused() {
2475    // A malformed timebase makes one track's timestamps meaningless.
2476    // It must not make the file unreadable — every other track still
2477    // demuxes, and the caller can see the 1/1 for what it is.
2478    let tb = rational_to_timebase(Rational::new(1, 0));
2479    assert_eq!(tb.den().get(), 1);
2480    assert_eq!(tb.num(), 1);
2481  }
2482
2483  #[test]
2484  fn a_declared_frame_rate_becomes_a_rate_shaped_timebase() {
2485    let ntsc = rate_to_timebase(Rational::new(30_000, 1001)).expect("declared");
2486    assert_eq!((ntsc.num(), ntsc.den().get()), (30_000, 1001));
2487    assert_eq!(
2488      rate_to_timebase(Rational::new(0, 1)),
2489      None,
2490      "0 fps is absent"
2491    );
2492    assert_eq!(
2493      rate_to_timebase(Rational::new(30, 0)),
2494      None,
2495      "no denominator"
2496    );
2497  }
2498
2499  #[test]
2500  fn the_seek_timebase_is_microseconds() {
2501    // `avformat_seek_file` with `stream_index == -1` takes AV_TIME_BASE
2502    // units; a target expressed in anything else has to arrive there.
2503    let tb = av_time_base_q();
2504    assert_eq!((tb.num(), tb.den().get()), (1, 1_000_000));
2505    let target = Timestamp::new(1_500, Timebase::new(1, NonZeroI32::new(1000).expect("ms")));
2506    assert_eq!(target.rescale_to(tb).pts(), 1_500_000);
2507  }
2508}