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::PacketBuffer`].
961///
962/// A packet's payload could not be referenced — the bytes are there
963/// and this layer could not carry them.
964///
965/// Never raised for a packet that simply has no payload: an empty
966/// packet is a marker some demuxers emit, and it is skipped in
967/// silence. Distinguishing the two is what keeps a refcount failure
968/// under memory pressure from looking like the file's own word and
969/// dropping real compressed bytes.
970#[derive(thiserror::Error, Debug, Clone)]
971#[error("stream {stream_index}: {source}")]
972pub struct PacketBuffer {
973 stream_index: usize,
974 #[source]
975 source: PacketBufferError,
976}
977
978impl PacketBuffer {
979 /// Constructs a `PacketBuffer` payload.
980 #[cfg_attr(not(tarpaulin), inline(always))]
981 pub const fn new(stream_index: usize, source: PacketBufferError) -> Self {
982 Self {
983 stream_index,
984 source,
985 }
986 }
987 /// The `AVStream.index` the packet belongs to.
988 #[cfg_attr(not(tarpaulin), inline(always))]
989 pub const fn stream_index(&self) -> usize {
990 self.stream_index
991 }
992 /// What went wrong.
993 #[cfg_attr(not(tarpaulin), inline(always))]
994 pub const fn source(&self) -> &PacketBufferError {
995 &self.source
996 }
997}
998
999/// Payload for [`DemuxError::ReaderPanic`].
1000///
1001/// The `Read + Seek` source given to [`FfmpegDemuxer::open_reader`]
1002/// panicked inside a libavformat callback.
1003///
1004/// The panic was caught before it could cross the `extern "C"`
1005/// boundary and abort the process; this is what it said. The session
1006/// is terminal — every later call reports the same panic.
1007#[derive(thiserror::Error, Debug, Clone)]
1008#[error("the reader panicked: {message}")]
1009pub struct ReaderPanic {
1010 message: SmolStr,
1011}
1012
1013impl ReaderPanic {
1014 /// Constructs a `ReaderPanic` payload.
1015 #[cfg_attr(not(tarpaulin), inline(always))]
1016 pub const fn new(message: SmolStr) -> Self {
1017 Self { message }
1018 }
1019 /// What the panic payload said.
1020 #[cfg_attr(not(tarpaulin), inline(always))]
1021 pub fn message(&self) -> &str {
1022 self.message.as_str()
1023 }
1024}
1025
1026/// Errors from [`FfmpegDemuxer`].
1027///
1028/// **Open fault taxonomy, so it is `#[non_exhaustive]`.** New ways to
1029/// fail are discovered — a backend, a ceiling, a corruption a codec
1030/// learns to report — and a consumer that meets one it has never heard
1031/// of should take its generic-fault path. That is exactly what the
1032/// wildcard arm this attribute forces is for. The two status
1033/// vocabularies opposite it,
1034/// [`Sent`](mediadecode::Sent) and [`Received`](mediadecode::Received),
1035/// are exhaustive for the mirror-image reason: their arms are the
1036/// substrate's fixed state set, and there the wildcard would be dead
1037/// weight hiding a state a consumer forgot.
1038#[derive(thiserror::Error, Debug, Clone, IsVariant, Unwrap, TryUnwrap)]
1039#[unwrap(ref, ref_mut)]
1040#[try_unwrap(ref, ref_mut)]
1041#[non_exhaustive]
1042pub enum DemuxError {
1043 /// The wrapped libavformat call reported an error — open, read or
1044 /// seek.
1045 #[error(transparent)]
1046 Ffmpeg(#[from] ffmpeg_next::Error),
1047
1048 /// libavformat asked for more bytes than the probe budget allows
1049 /// while opening and analysing the container. See
1050 /// [`ProbeBudgetExhausted`].
1051 #[error(transparent)]
1052 ProbeBudgetExhausted(#[from] ProbeBudgetExhausted),
1053
1054 /// One attachment's payload is over the per-attachment budget.
1055 /// Refused at open, before the copy.
1056 #[error(transparent)]
1057 AttachmentTooLarge(#[from] AttachmentTooLarge),
1058
1059 /// The file's attachments, together, are over the whole-file budget.
1060 /// Refused at open, before the copy that would have crossed it.
1061 #[error(transparent)]
1062 AttachmentBudgetExhausted(#[from] AttachmentBudgetExhausted),
1063
1064 /// One stream's codec parameters hold more heap bytes than the
1065 /// budget allows. Refused at open, before the clone.
1066 #[error(transparent)]
1067 ParametersTooLarge(#[from] ParametersTooLarge),
1068
1069 /// Every stream's codec parameters together are over the whole-file
1070 /// budget. Refused at open, before the clone that would have crossed
1071 /// it.
1072 #[error(transparent)]
1073 ParametersBudgetExhausted(#[from] ParametersBudgetExhausted),
1074
1075 /// Codec parameters arrived that were never allocated.
1076 #[error(transparent)]
1077 ParametersMissing(#[from] ParametersMissing),
1078
1079 /// Codec parameters for a track could not be allocated.
1080 #[error(transparent)]
1081 ParametersAlloc(#[from] ParametersAlloc),
1082
1083 /// Copying a track's codec parameters failed part way.
1084 #[error(transparent)]
1085 ParametersCopy(#[from] ParametersCopy),
1086
1087 /// A packet's payload could not be referenced — the bytes are there
1088 /// and this layer could not carry them.
1089 #[error(transparent)]
1090 PacketBuffer(#[from] PacketBuffer),
1091
1092 /// The `Read + Seek` source given to
1093 /// [`FfmpegDemuxer::open_reader`] panicked inside a libavformat
1094 /// callback.
1095 #[error(transparent)]
1096 ReaderPanic(#[from] ReaderPanic),
1097}
1098
1099// ---------------------------------------------------------------------------
1100// Track-table construction.
1101// ---------------------------------------------------------------------------
1102
1103type BuiltTracks<C> = (
1104 Vec<TrackInfo<Ffmpeg>>,
1105 VecDeque<(
1106 TrackIndex,
1107 AttachmentPacket<AttachmentPacketExtra, <C as crate::FfmpegCarrier>::Buffer>,
1108 )>,
1109);
1110
1111fn build_tracks<C: crate::FfmpegCarrier + crate::CarrierOps>(
1112 input: &Input,
1113 limits: DemuxLimits,
1114) -> Result<BuiltTracks<C>, DemuxError> {
1115 // **Admission before allocation.** Every attachment in the file is
1116 // judged here, in full, before the loop below allocates anything at
1117 // all — see [`admit_streams`] for why the charge cannot live
1118 // inside the capture.
1119 admit_streams(input, limits)?;
1120
1121 let count = input.streams().len();
1122 let mut tracks = Vec::with_capacity(count);
1123 let mut pending = VecDeque::new();
1124
1125 for stream in input.streams() {
1126 let index = stream.index();
1127 // `AVStream.index` is the stream's position in `ic->streams[]` and
1128 // libavformat keeps the two identical. The demux tier makes
1129 // `TrackIndex` mean "position in `tracks()`", so the two agree by
1130 // construction — but only if they really are dense and in order,
1131 // which is cheap to insist on rather than assume.
1132 debug_assert_eq!(
1133 index,
1134 tracks.len(),
1135 "AVStream indices are dense and ordered"
1136 );
1137
1138 let parameters = stream.parameters();
1139 let par = unsafe { parameters.as_ptr() };
1140 // Never read `AVCodecParameters.codec_type` / `.codec_id` as their
1141 // bindgen enums: a value outside this build's discriminant set is
1142 // UB the moment it exists. Both are read as the raw integers they
1143 // are on the wire — the medium through [`boundary::media_kind_of`],
1144 // which folds anything unnamed into `Unknown`.
1145 //
1146 // The medium used to go through `Parameters::medium()` on the
1147 // argument that `AVMediaType`'s set is tiny and stable. It is; that
1148 // made the read unlikely to bite, not sound. The exception is gone
1149 // rather than defended, so no attacker-reachable path in this crate
1150 // forms a bindgen enum out of FFmpeg memory.
1151 let medium = boundary::media_kind_of(¶meters);
1152 let codec =
1153 CodecId::from_raw(unsafe { read_unaligned(addr_of!((*par).codec_id).cast::<i32>()) });
1154
1155 let disposition = unsafe { (*stream.as_ptr()).disposition };
1156 let attached_pic = is_attachment_disposition(disposition);
1157
1158 let time_base = rational_to_timebase(stream.time_base());
1159 let raw_duration = stream.duration();
1160 let duration = (raw_duration != AV_NOPTS_VALUE && raw_duration > 0)
1161 .then(|| Timestamp::new(raw_duration, time_base));
1162 let raw_start = stream.start_time();
1163 let frames = stream.frames();
1164
1165 let params = if attached_pic {
1166 // Cover art. A still image in a video-shaped slot is an
1167 // attachment by every property that matters, and the `Video` arm
1168 // is reserved for motion video.
1169 TrackParams::Attachment(AttachmentTrackParams::new(codec))
1170 } else {
1171 match medium {
1172 boundary::MediaKind::Video => TrackParams::Video(VideoTrackParams::new(
1173 codec,
1174 unsafe { (*par).width }.max(0) as u32,
1175 unsafe { (*par).height }.max(0) as u32,
1176 boundary::from_av_pixel_format(unsafe { (*par).format }),
1177 rate_to_timebase(stream.avg_frame_rate()),
1178 )),
1179 boundary::MediaKind::Audio => {
1180 let ch_layout = unsafe { std::ptr::addr_of!((*par).ch_layout) };
1181 // SAFETY: `par` is a live `*const AVCodecParameters` for the
1182 // life of `parameters`; the helper validates `order` as an
1183 // `i32` before constructing any `AVChannelOrder`.
1184 let channel_layout =
1185 unsafe { crate::channel_layout::channel_layout_description_from_raw_ptr(ch_layout) };
1186 TrackParams::Audio(AudioTrackParams::new(
1187 codec,
1188 unsafe { (*par).sample_rate }.max(0) as u32,
1189 channel_layout.channels().min(255) as u8,
1190 SampleFormat::from_raw(unsafe { (*par).format }),
1191 channel_layout,
1192 ))
1193 }
1194 boundary::MediaKind::Subtitle => TrackParams::Subtitle(SubtitleTrackParams::new(codec)),
1195 boundary::MediaKind::Data => TrackParams::Data(DataTrackParams::new(codec)),
1196 boundary::MediaKind::Attachment => {
1197 TrackParams::Attachment(AttachmentTrackParams::new(codec))
1198 }
1199 boundary::MediaKind::Unknown => TrackParams::Unknown(UnknownTrackParams::new(codec)),
1200 }
1201 };
1202
1203 // The parameter copy. For an `AVMEDIA_TYPE_ATTACHMENT` stream its
1204 // `extradata` **is** the attachment's payload — the same bytes the
1205 // carrier below already holds — so it is left behind rather than
1206 // copied. Censused before it was: nothing can use it. libavcodec
1207 // has no decoder for a font (`avcodec_find_decoder` answers null
1208 // for `AV_CODEC_ID_TTF` and its siblings), so no road in this crate
1209 // or downstream of it opens a codec context from these parameters;
1210 // the payload reaches a consumer as the attachment packet, which is
1211 // the delivery the demux tier promises.
1212 //
1213 // **Omitted, not stripped.** An earlier shape copied the extradata
1214 // and freed it immediately afterwards, which allocated the payload
1215 // for no reason and — worse — charged it against the *parameter*
1216 // ceiling on the way past. A font between the two ceilings passed
1217 // the admission pass and then failed inside the clone. See
1218 // [`ExtradataPolicy`](crate::extras::ExtradataPolicy).
1219 //
1220 // Cover art keeps its extradata: there the payload is the parked
1221 // `AVPacket`, extradata is *not* a copy of it, and a still codec
1222 // can legitimately need it (MJPEG with an external Huffman table).
1223 // Measured on this build: a cover-art stream carries none anyway.
1224 let extradata_policy = if medium.is_attachment() {
1225 crate::extras::ExtradataPolicy::Omit
1226 } else {
1227 crate::extras::ExtradataPolicy::Copy
1228 };
1229 let parameters_copy = crate::extras::bounded_clone_parameters_with(
1230 ¶meters,
1231 index,
1232 limits.max_codec_parameter_bytes(),
1233 extradata_policy,
1234 )?;
1235 let extra = TrackExtra::new(index as i32, parameters_copy)?
1236 .with_disposition(disposition)
1237 .with_start_time((raw_start != AV_NOPTS_VALUE).then_some(raw_start))
1238 .with_frame_count((frames > 0).then_some(frames));
1239
1240 // SAFETY: `stream` keeps the `AVStream` — and so its metadata
1241 // dictionary — live across both reads. The dictionary is read
1242 // through `av_dict_get` rather than through
1243 // `DictionaryRef::get`: see [`metadata_text`].
1244 let metadata = unsafe { (*stream.as_ptr()).metadata };
1245 let info = TrackInfo::new(time_base, params, extra)
1246 .with_duration(duration)
1247 .with_filename(unsafe { metadata_text(metadata, c"filename") })
1248 .with_mime_type(unsafe { metadata_text(metadata, c"mimetype") });
1249
1250 // Capture the attachment payload now, so the queue is complete
1251 // before a single timed packet has been read. Every attachment
1252 // track leaves this loop with exactly one packet queued, or the
1253 // open fails: that is what makes "exactly one packet, before any
1254 // timed packet" a property of the construction rather than a
1255 // promise the pull loop has to keep.
1256 if info.kind() == TrackKind::Attachment {
1257 let packet = if attached_pic {
1258 // SAFETY: `stream` keeps the format context (and so the
1259 // `AVStream`) live; `attached_pic` is an `AVPacket` embedded by
1260 // value, and `addr_of!` reaches it without forming a reference
1261 // to the stream.
1262 let pkt = unsafe { std::ptr::addr_of!((*stream.as_ptr()).attached_pic) };
1263 unsafe { attached_pic_payload::<C>(pkt, index, limits) }?
1264 } else {
1265 extradata_payload::<C>(&stream, limits)?
1266 };
1267 pending.push_back((TrackIndex::new(index), packet));
1268 }
1269
1270 tracks.push(info);
1271 }
1272
1273 Ok((tracks, pending))
1274}
1275
1276/// Whether `packet`'s payload is the very allocation the container has
1277/// parked in `AVStream.attached_pic` for stream `index`.
1278///
1279/// # Why this exists
1280///
1281/// libavformat queues a stream's attached picture as its **first
1282/// packet** — `read_frame_internal` does `av_packet_ref(pkt,
1283/// &st->attached_pic)` and keeps its own reference — so that packet
1284/// arrives with two references through nobody's fault. A pure cover-art
1285/// stream never reaches this road (it is an attachment, hoisted at
1286/// open), but a stream carrying `ATTACHED_PIC | TIMED_THUMBNAILS` is
1287/// deliberately classified as **video** by
1288/// [`is_attachment_disposition`], so its first pull comes through here
1289/// and would be refused as a shared payload. Every packet after it is
1290/// an ordinary timed one with a buffer of its own.
1291///
1292/// # The probe, and why it is a proof rather than a guess
1293///
1294/// `av_buffer_ref` sets the new reference's `buffer` field to the
1295/// source's, so two `AVBufferRef`s name one allocation **iff** their
1296/// `buffer` pointers are equal — the same identity
1297/// [`crate::FfmpegBuffer::ptr_eq`] rests on. Comparing them therefore
1298/// establishes the fact the carve-out needs: this payload's allocation
1299/// *is* `AVStream.attached_pic`'s, so one of its outstanding references
1300/// is the container's own.
1301///
1302/// The alternatives were heuristics and are not used: the disposition
1303/// bits say a stream *has* an attached picture, not that this packet is
1304/// it; "the first packet on the stream" is an ordering assumption that
1305/// nothing in libavformat's contract fixes.
1306///
1307/// # The soundness argument, restated for this packet
1308///
1309/// It is the same one the hoisted-attachment road rests on, and it
1310/// holds here for the same reason. `AVStream.attached_pic` is written
1311/// once, while the container is being opened, and never again; the
1312/// reference this crate is looking at is the container's, held for the
1313/// lifetime of the `AVFormatContext`, and there is no
1314/// `ffmpeg_next::Packet` wrapping it for anyone to call `data_mut` on.
1315/// What the uniqueness rule guards against is a *safe Rust* handle that
1316/// may write while this crate reads, and the container's reference is
1317/// not one.
1318///
1319/// # Safety
1320///
1321/// `input` and `packet` must both be live for the duration of the call.
1322unsafe fn is_streams_attached_pic(input: &Input, index: usize, packet: &Packet) -> bool {
1323 // SAFETY: `input` owns a live `AVFormatContext`; `streams` is an
1324 // array of `nb_streams` pointers, and `index` is checked against it.
1325 let stream = unsafe {
1326 let context = input.as_ptr();
1327 if index >= (*context).nb_streams as usize {
1328 return false;
1329 }
1330 *(*context).streams.add(index)
1331 };
1332 if stream.is_null() {
1333 return false;
1334 }
1335 // SAFETY: `stream` is one of the context's own live `AVStream`s and
1336 // `packet` is live per this function's contract.
1337 unsafe { packet_is_parked_picture(stream, packet) }
1338}
1339
1340/// The identity itself: whether `packet`'s payload allocation is the
1341/// one `stream` has parked in `attached_pic`.
1342///
1343/// Split out from [`is_streams_attached_pic`] so the comparison can be
1344/// tested against a hand-built pair without forging an
1345/// `AVFormatContext` — see `a_queued_attached_picture_is_recognised`.
1346///
1347/// # Safety
1348///
1349/// `stream` must be a live `AVStream` and `packet` a live `AVPacket`.
1350unsafe fn packet_is_parked_picture(stream: *const AVStream, packet: &Packet) -> bool {
1351 use ffmpeg_next::packet::Ref;
1352
1353 // SAFETY: both are live per the contract; `attached_pic` is an inline
1354 // `AVPacket` and both `buf` fields may be null, which is answered
1355 // before either is read through.
1356 unsafe {
1357 let parked = (*stream).attached_pic.buf;
1358 let carried = (*packet.as_ptr()).buf;
1359 if parked.is_null() || carried.is_null() {
1360 return false;
1361 }
1362 // The shared `AVBuffer`, not the `AVBufferRef`: `av_packet_ref`
1363 // mints a new reference struct around the same allocation, so
1364 // comparing the references themselves would answer "no" to exactly
1365 // the case this is for.
1366 (*parked).buffer == (*carried).buffer
1367 }
1368}
1369
1370/// Whether a stream's disposition makes it an **attachment** — a
1371/// payload with no place on the timeline — rather than a timed track.
1372///
1373/// `AV_DISPOSITION_ATTACHED_PIC` alone says "cover art": one still
1374/// image, parked in `AVStream.attached_pic`, no timeline. But FFmpeg
1375/// pairs it with `AV_DISPOSITION_TIMED_THUMBNAILS` for a different
1376/// thing entirely — "the stream is sparse, and contains thumbnail
1377/// images, often corresponding to chapter markers", a flag its own
1378/// header documents as *only ever* used together with `ATTACHED_PIC`.
1379/// Such a stream has many images and every one of them has a
1380/// timestamp.
1381///
1382/// Classifying that as an attachment loses all but the first: the
1383/// attachment contract is exactly one packet, so the queue takes the
1384/// parked copy and the delivery loop drops every timed packet on the
1385/// track. It goes to the **`Video`** arm instead. That does not
1386/// contradict "cover art is an attachment, not video" — the reason
1387/// behind that ruling is that a single still with no timeline must not
1388/// look like a motion track, and a timed-thumbnail stream *is* on the
1389/// timeline. It is sparse video: a codec id, a frame size, a pixel
1390/// format and packets with timestamps, which is everything a consumer
1391/// needs to decode the images. The `Data` arm was the alternative and
1392/// is worse: it would strand encoded pictures in an arm that names no
1393/// decoder.
1394///
1395/// The bits are tested against the raw `AVStream.disposition` rather
1396/// than through `ffmpeg_next`'s `Disposition`, which mints no
1397/// `TIMED_THUMBNAILS` constant at all — its `from_bits_truncate` drops
1398/// every bit this build of the wrapper has no name for, which is how
1399/// the distinction went missing in the first place.
1400const fn is_attachment_disposition(disposition: c_int) -> bool {
1401 disposition & AV_DISPOSITION_ATTACHED_PIC != 0
1402 && disposition & AV_DISPOSITION_TIMED_THUMBNAILS == 0
1403}
1404
1405/// Upper bound on the NUL search in [`metadata_text`].
1406///
1407/// Generous by four orders of magnitude for a filename or a MIME type,
1408/// and there only so that a value libavutil did not terminate cannot
1409/// turn the walk into an unbounded read — the same discipline
1410/// [`crate::channel_layout`] and the pixel-format namer follow. A value
1411/// longer than this is refused rather than truncated: a truncated
1412/// filename is a different filename.
1413const METADATA_VALUE_MAX_BYTES: usize = 64 * 1024;
1414
1415/// Reads one entry out of a container's metadata dictionary as text
1416/// this crate can own.
1417///
1418/// **Why not `DictionaryRef::get`.** ffmpeg-next 9.0.0 builds its
1419/// `&str` with `from_utf8_unchecked`
1420/// (`src/util/dictionary/immutable.rs`), and FFmpeg does not validate
1421/// demuxed metadata as UTF-8 — an ID3 frame, a Matroska attachment
1422/// name or a MOV atom carries whatever bytes the file carries. A
1423/// `filename` holding a stray `0x80` would therefore have produced a
1424/// `&str` that is not UTF-8: undefined behaviour the moment it exists,
1425/// before `SmolStr` ever copies it.
1426///
1427/// Invalid bytes are replaced (`U+FFFD`), not refused. This is
1428/// *identity* metadata — the name a font was attached under, the MIME
1429/// type declared for a cover — and a file that names its attachment in
1430/// some legacy codepage is still a file worth opening. The replacement
1431/// characters say plainly that the container's bytes were not text.
1432///
1433/// # Safety
1434///
1435/// `dict` must be null or a live `*const AVDictionary` for the
1436/// duration of this call.
1437unsafe fn metadata_text(dict: *const AVDictionary, key: &CStr) -> Option<SmolStr> {
1438 if dict.is_null() {
1439 return None;
1440 }
1441 // SAFETY: `dict` is live per the contract above and `key` is a
1442 // NUL-terminated C string by construction; `av_dict_get` reads both
1443 // and returns a borrowed entry owned by the dictionary.
1444 let entry = unsafe { av_dict_get(dict, key.as_ptr(), std::ptr::null(), 0) };
1445 if entry.is_null() {
1446 return None;
1447 }
1448 // SAFETY: a non-null entry is a live `AVDictionaryEntry` for as long
1449 // as the dictionary is not modified, which it is not here.
1450 let value = unsafe { (*entry).value };
1451 if value.is_null() {
1452 return None;
1453 }
1454 for len in 0..METADATA_VALUE_MAX_BYTES {
1455 // SAFETY: `value` is a NUL-terminated string libavutil allocated
1456 // with `av_strdup`; the walk reads at most one byte past the last
1457 // value byte and stops at the terminator.
1458 if unsafe { *value.add(len).cast::<u8>() } == 0 {
1459 // SAFETY: the `len` bytes below the terminator were just walked,
1460 // so the slice is in bounds and initialised.
1461 let bytes = unsafe { std::slice::from_raw_parts(value.cast::<u8>(), len) };
1462 return Some(SmolStr::new(std::string::String::from_utf8_lossy(bytes)));
1463 }
1464 }
1465 None
1466}
1467
1468/// Wraps `AVStream.attached_pic` — the real packet libavformat parsed
1469/// for a cover-art stream — as this track's one attachment packet.
1470///
1471/// A stream that declares cover art but parks no payload still gets a
1472/// packet: an empty one, marked `synthesized`, because the contract is
1473/// one packet per attachment track and a consumer that sees an empty
1474/// payload learns something true about the file. The alternative shipped
1475/// once — waiting for the payload to arrive as a packet later — and it
1476/// cannot hold: nothing stops a timed packet, or a seek, from coming
1477/// first, so the track's packet would arrive out of order or never.
1478///
1479/// Measured before it was written: across MP3 (ID3 APIC), M4A (`covr`),
1480/// FLAC (`METADATA_BLOCK_PICTURE`) and Matroska (an `image/*`
1481/// attachment), every stream libavformat gives
1482/// `AV_DISPOSITION_ATTACHED_PIC` also carries the parked packet —
1483/// `ff_add_attached_pic` sets the disposition and fills
1484/// `attached_pic` in the same breath. The empty case is the honest
1485/// answer to a state this build's demuxers do not produce, not a
1486/// fallback anything relies on.
1487///
1488/// # Safety
1489///
1490/// `pkt` must be a live `*const AVPacket` — in practice the
1491/// `attached_pic` embedded in the `AVStream` at `index` — for the
1492/// duration of this call.
1493unsafe fn attached_pic_payload<C: crate::FfmpegCarrier + crate::CarrierOps>(
1494 pkt: *const ffmpeg_next::ffi::AVPacket,
1495 index: usize,
1496 limits: DemuxLimits,
1497) -> Result<AttachmentPacket<AttachmentPacketExtra, C::Buffer>, DemuxError> {
1498 // Already admitted: [`admit_streams`] charged this payload — and
1499 // every other attachment in the file — before `build_tracks`
1500 // allocated anything. The per-attachment budget is passed down as
1501 // this packet's own ceiling anyway, so the funnel is guarded even if
1502 // a future caller reaches it without the admission pass.
1503 //
1504 // SAFETY: `pkt` is live per the contract above.
1505 // **The container's own cover art**, whose buffer libavformat also
1506 // holds — see [`crate::buffer::PayloadProvenance`] for why that
1507 // second reference is not the hazard a caller's second `Packet` is.
1508 let captured = unsafe {
1509 crate::buffer::payload_of::<C>(
1510 pkt,
1511 limits.max_attachment_bytes(),
1512 crate::buffer::PayloadProvenance::AttachedPicture,
1513 )
1514 }
1515 .map_err(|source| DemuxError::PacketBuffer(PacketBuffer::new(index, source)))?;
1516 let extra = AttachmentPacketExtra::new(index as i32);
1517 Ok(match captured {
1518 Some(payload) => {
1519 // The hoisted packet's own flags, through the same raw reader the
1520 // five boundary conversions use. FFmpeg marks an attached picture
1521 // `AV_PKT_FLAG_KEY` — a still image is a keyframe if anything is
1522 // — and building this one with empty flags dropped that, along
1523 // with `CORRUPT` and every other bit the packet really carried.
1524 // SAFETY: `pkt` points at the live embedded `AVPacket`.
1525 let flags = unsafe { boundary::md_flags_from_av_packet(pkt) }
1526 .map_err(|source| DemuxError::PacketBuffer(PacketBuffer::new(index, source)))?;
1527 AttachmentPacket::new(payload, extra).with_flags(flags)
1528 }
1529 // Nothing was parked, so there are no flags to read: an empty set
1530 // is the honest answer for a packet this layer invented.
1531 None => AttachmentPacket::new(C::empty(), extra.with_synthesized(true)),
1532 })
1533}
1534
1535/// Builds an attachment payload out of a track's codec extradata — the
1536/// only place a font's bytes ever live, since an
1537/// `AVMEDIA_TYPE_ATTACHMENT` stream produces no packets at all.
1538///
1539/// A track with no extradata still gets a packet, with an empty
1540/// payload: the contract is one packet per attachment track, and a
1541/// consumer that sees an empty one learns something true about the
1542/// file. Only an allocation failure is an error.
1543fn extradata_payload<C: crate::FfmpegCarrier + crate::CarrierOps>(
1544 stream: &ffmpeg_next::format::stream::Stream<'_>,
1545 limits: DemuxLimits,
1546) -> Result<AttachmentPacket<AttachmentPacketExtra, C::Buffer>, DemuxError> {
1547 let index = stream.index();
1548 let parameters = stream.parameters();
1549 // SAFETY: `parameters` keeps the `AVCodecParameters` live;
1550 // `extradata` / `extradata_size` are public fields.
1551 let par = unsafe { parameters.as_ptr() };
1552 let ptr = unsafe { (*par).extradata };
1553 let len = unsafe { (*par).extradata_size }.max(0) as usize;
1554 // Already admitted, exactly as on the hoisted cover-art path — see
1555 // [`admit_streams`]. Re-judged here against the per-attachment
1556 // ceiling alone, so the helper is safe to call on its own.
1557 if len > limits.max_attachment_bytes() {
1558 return Err(DemuxError::AttachmentTooLarge(AttachmentTooLarge::new(
1559 index,
1560 len,
1561 limits.max_attachment_bytes(),
1562 )));
1563 }
1564 let bytes: &[u8] = if ptr.is_null() || len == 0 {
1565 &[]
1566 } else {
1567 // SAFETY: libavformat guarantees `extradata` is readable for
1568 // `extradata_size` bytes (plus its padding) while the parameters
1569 // live, and the slice is consumed before this function returns.
1570 unsafe { std::slice::from_raw_parts(ptr, len) }
1571 };
1572 // Extradata is a plain allocation with no `AVBufferRef` behind it —
1573 // an `AVMEDIA_TYPE_ATTACHMENT` stream produces no packets, so a
1574 // font's bytes never live in a refcounted buffer. **Both** lanes copy
1575 // here, which is what `from_bytes` is for.
1576 Ok(AttachmentPacket::new(
1577 C::from_bytes(bytes).ok_or_else(|| {
1578 DemuxError::PacketBuffer(PacketBuffer::new(
1579 index,
1580 crate::buffer::PacketBufferError::CaptureFailed(crate::buffer::CaptureFailed::new(len)),
1581 ))
1582 })?,
1583 AttachmentPacketExtra::new(index as i32).with_synthesized(true),
1584 ))
1585}
1586
1587/// **The admission pass**: judges every stream in the file before the
1588/// track table allocates anything at all.
1589///
1590/// # Why this cannot live inside the capture
1591///
1592/// It used to, and that was a bypass. `build_tracks` deep-copies each
1593/// stream's `AVCodecParameters` on its way to building a `TrackExtra`,
1594/// and for an `AVMEDIA_TYPE_ATTACHMENT` stream **the extradata inside
1595/// those parameters is the attachment's payload**. So the loop paid for
1596/// the payload — a full `avcodec_parameters_copy` — one statement
1597/// before asking whether it was allowed to. A file declaring a gigabyte
1598/// of "font" allocated the gigabyte and then reported that a gigabyte
1599/// was too much.
1600///
1601/// The fix is not a check moved a few lines earlier: any per-track
1602/// interleaving of judging and paying has the same shape, because the
1603/// aggregate budget is only knowable once every track has been *seen*.
1604/// So the whole file is admitted here, in a pass that allocates
1605/// nothing — it reads two integers per stream — and only a container
1606/// that passes in full reaches the loop that builds carriers and
1607/// parameter copies.
1608///
1609/// # Why it is every stream, not every attachment
1610///
1611/// Because the track table copies **every** stream's codec parameters,
1612/// and `AVCodecParameters` reaches the heap three ways — `extradata`,
1613/// every `coded_side_data` entry, a custom channel map — all of them
1614/// sized by the file. A pass that walked only attachment streams left
1615/// the other road wide open: a MOV puts an ICC profile in
1616/// `coded_side_data`, on an ordinary video track, and the wholesale
1617/// copy took it before anything asked how big it was. That was the same
1618/// class of defect three review rounds running, which is why the copy
1619/// itself is gone (see
1620/// [`bounded_clone_parameters`](crate::extras::bounded_clone_parameters))
1621/// and why this pass sees everything.
1622///
1623/// # What is charged
1624///
1625/// The bytes this session will **retain**, which is not always the
1626/// declared size:
1627///
1628/// - every stream is charged its parameter clone's footprint against
1629/// the per-stream and whole-file codec-parameter budgets;
1630/// - a synthesized attachment's `extradata` is charged to the
1631/// *attachment* budget and left out of the parameter one, because the
1632/// clone strips it and the carrier holds it — one set of bytes, one
1633/// charge;
1634/// - the attachment budgets then see:
1635///
1636/// - a hoisted cover-art track retains its parked `AVPacket`'s payload
1637/// *and* the extradata in its parameter copy, which the still decoder
1638/// may need and which is not a duplicate of the payload;
1639/// - a synthesized `AVMEDIA_TYPE_ATTACHMENT` track retains only the
1640/// carrier, because `build_tracks` strips the duplicate extradata out
1641/// of the parameter copy (see the comment there for the census).
1642///
1643/// Charging residency rather than payload is what keeps the budget an
1644/// honest statement about memory instead of about file structure.
1645///
1646/// The per-attachment ceiling is judged first for each track: when a
1647/// single payload is itself over the line, that is the more specific
1648/// fact, and naming the aggregate instead would send a reader looking
1649/// for four hundred attachments that are not there.
1650fn admit_streams(input: &Input, limits: DemuxLimits) -> Result<(), DemuxError> {
1651 let mut attachment_spent: usize = 0;
1652 let mut parameter_spent: usize = 0;
1653
1654 for stream in input.streams() {
1655 let index = stream.index();
1656 let parameters = stream.parameters();
1657 // SAFETY: `parameters` keeps the `AVCodecParameters` live for this
1658 // measurement, which allocates nothing and dereferences only what
1659 // it counts.
1660 let par = unsafe { parameters.as_ptr() };
1661 if par.is_null() {
1662 return Err(DemuxError::ParametersMissing(ParametersMissing::new(index)));
1663 }
1664 let footprint =
1665 unsafe { crate::extras::measure_parameters(par) }.ok_or(DemuxError::ParametersTooLarge(
1666 ParametersTooLarge::new(index, usize::MAX, limits.max_codec_parameter_bytes()),
1667 ))?;
1668
1669 // SAFETY: `stream` keeps the `AVStream` live; `disposition` is a
1670 // public field.
1671 let disposition = unsafe { (*stream.as_ptr()).disposition };
1672 let cover_art = is_attachment_disposition(disposition);
1673 let synthesized = !cover_art && boundary::media_kind_of(¶meters).is_attachment();
1674
1675 // What the *parameter clone* will retain for this stream. The
1676 // synthesized-attachment road strips `extradata` — the font's
1677 // payload rides the carrier instead — so counting it here would
1678 // charge the same bytes twice and make the budget a statement about
1679 // the file rather than about memory.
1680 let retained_parameters = if synthesized {
1681 footprint.total_without_extradata()
1682 } else {
1683 footprint.total()
1684 }
1685 .ok_or(DemuxError::ParametersTooLarge(ParametersTooLarge::new(
1686 index,
1687 usize::MAX,
1688 limits.max_codec_parameter_bytes(),
1689 )))?;
1690
1691 if retained_parameters > limits.max_codec_parameter_bytes() {
1692 return Err(DemuxError::ParametersTooLarge(ParametersTooLarge::new(
1693 index,
1694 retained_parameters,
1695 limits.max_codec_parameter_bytes(),
1696 )));
1697 }
1698 parameter_spent = parameter_spent.saturating_add(retained_parameters);
1699 if parameter_spent > limits.max_total_codec_parameter_bytes() {
1700 return Err(DemuxError::ParametersBudgetExhausted(
1701 ParametersBudgetExhausted::new(
1702 index,
1703 parameter_spent,
1704 limits.max_total_codec_parameter_bytes(),
1705 ),
1706 ));
1707 }
1708
1709 // And what the *carrier* will hold, for the two attachment roads.
1710 let carrier = if cover_art {
1711 // SAFETY: `attached_pic` is an `AVPacket` embedded in the
1712 // `AVStream` by value; `addr_of!` reaches its `size` without
1713 // forming a reference to the stream.
1714 unsafe {
1715 let pkt = std::ptr::addr_of!((*stream.as_ptr()).attached_pic);
1716 (*pkt).size
1717 }
1718 .max(0) as usize
1719 } else if synthesized {
1720 // The **payload**, not the padded clone figure. The carrier is
1721 // an `FfmpegBytes` over exactly these bytes and the clone omits
1722 // extradata entirely on this road, so nothing here allocates the
1723 // padding — charging it would bill sixty-four bytes that are
1724 // never spent, reject a payload in the last sixty-four below the
1725 // ceiling, and disagree with the image road about the same file
1726 // at exactly the cap.
1727 footprint.extradata_payload()
1728 } else {
1729 // Not an attachment: nothing is captured eagerly for it, so
1730 // nothing more is charged.
1731 continue;
1732 };
1733
1734 charge_attachment(index, carrier, limits, &mut attachment_spent)?;
1735 }
1736 Ok(())
1737}
1738
1739/// Charges `declared` bytes against both attachment budgets, refusing
1740/// before anything is copied. The one place a file's attachment
1741/// spending is decided; see [`admit_streams`] for when it runs.
1742fn charge_attachment(
1743 index: usize,
1744 declared: usize,
1745 limits: DemuxLimits,
1746 spent: &mut usize,
1747) -> Result<(), DemuxError> {
1748 if declared > limits.max_attachment_bytes() {
1749 return Err(DemuxError::AttachmentTooLarge(AttachmentTooLarge::new(
1750 index,
1751 declared,
1752 limits.max_attachment_bytes(),
1753 )));
1754 }
1755 let total = spent.saturating_add(declared);
1756 if total > limits.max_total_attachment_bytes() {
1757 return Err(DemuxError::AttachmentBudgetExhausted(
1758 AttachmentBudgetExhausted::new(index, total, limits.max_total_attachment_bytes()),
1759 ));
1760 }
1761 *spent = total;
1762 Ok(())
1763}
1764
1765/// A stream's `AVRational` timebase as a [`Timebase`]. A zero or
1766/// negative denominator is clamped to 1 rather than refused: a
1767/// malformed timebase makes the track's timestamps meaningless, not the
1768/// file unreadable, and every other track still demuxes.
1769fn rational_to_timebase(value: Rational) -> Timebase {
1770 Timebase::new(
1771 value.numerator(),
1772 NonZeroI32::new(value.denominator().max(1)).expect("clamped to at least 1"),
1773 )
1774}
1775
1776/// A frame *rate* as a rate-shaped [`Timebase`] (`30000/1001` for
1777/// 29.97 fps), or `None` when the container declares none.
1778fn rate_to_timebase(value: Rational) -> Option<Timebase> {
1779 let (num, den) = (value.numerator(), value.denominator());
1780 (num > 0 && den > 0).then(|| Timebase::new(num, NonZeroI32::new(den).expect("checked above")))
1781}
1782
1783#[cfg(test)]
1784mod tests {
1785 use ffmpeg_next::ffi::{av_dict_free, av_dict_set};
1786
1787 use ffmpeg_next::codec::Parameters;
1788
1789 use super::*;
1790 use crate::extras::TrackExtra;
1791
1792 /// Builds a dictionary holding one entry whose *value* is the given
1793 /// raw bytes. The bytes go in as a C string, which is all
1794 /// `av_dict_set` promises to copy — FFmpeg never asks whether they
1795 /// are UTF-8, which is the whole point of the lane below.
1796 fn dict_with(key: &CStr, value: &[u8]) -> *mut AVDictionary {
1797 let mut dict: *mut AVDictionary = std::ptr::null_mut();
1798 let mut terminated = value.to_vec();
1799 terminated.push(0);
1800 let rc = unsafe {
1801 av_dict_set(
1802 &mut dict,
1803 key.as_ptr(),
1804 terminated.as_ptr().cast::<std::ffi::c_char>(),
1805 0,
1806 )
1807 };
1808 assert!(rc >= 0, "av_dict_set failed: {rc}");
1809 dict
1810 }
1811
1812 #[test]
1813 fn metadata_that_is_not_utf8_is_read_lossily_not_unsoundly() {
1814 // The bytes a real container can hold: a Latin-1 "café.ttf" whose
1815 // 0xE9 is not valid UTF-8 on its own. Read through
1816 // `DictionaryRef::get` this produced a `&str` that violates the
1817 // type's invariant — undefined behaviour before `SmolStr` ever
1818 // copied it.
1819 let raw = b"caf\xE9.ttf".to_vec();
1820 assert!(
1821 std::str::from_utf8(&raw).is_err(),
1822 "the source bytes really are not UTF-8",
1823 );
1824 let dict = dict_with(c"filename", &raw);
1825 let text = unsafe { metadata_text(dict, c"filename") }.expect("the entry exists");
1826 assert_eq!(text.as_str(), "caf\u{FFFD}.ttf");
1827 // A key the dictionary does not hold, and a null dictionary, are
1828 // both simply absent.
1829 assert_eq!(unsafe { metadata_text(dict, c"mimetype") }, None);
1830 assert_eq!(
1831 unsafe { metadata_text(std::ptr::null(), c"filename") },
1832 None
1833 );
1834 unsafe { av_dict_free(&mut { dict }) };
1835 }
1836
1837 #[test]
1838 fn valid_metadata_survives_unchanged() {
1839 let dict = dict_with(c"mimetype", b"application/x-truetype-font");
1840 assert_eq!(
1841 unsafe { metadata_text(dict, c"mimetype") }.as_deref(),
1842 Some("application/x-truetype-font"),
1843 );
1844 unsafe { av_dict_free(&mut { dict }) };
1845 }
1846
1847 #[test]
1848 fn an_unterminated_length_is_refused_rather_than_truncated() {
1849 // Nothing libavutil produces is this long; the cap exists so a
1850 // value it did not terminate cannot walk off the end. A value that
1851 // reaches the cap is absent, never a prefix of itself.
1852 let long = vec![b'a'; METADATA_VALUE_MAX_BYTES + 1];
1853 let dict = dict_with(c"filename", &long);
1854 assert_eq!(unsafe { metadata_text(dict, c"filename") }, None);
1855 unsafe { av_dict_free(&mut { dict }) };
1856 }
1857
1858 /// A reader that panics with a payload whose destructor panics in
1859 /// turn. Both panics are safe code; the second one is what used to
1860 /// leave the guard and enter the `extern "C"` AVIO callback.
1861 struct PanicsWithAHostilePayload;
1862
1863 struct PanicOnDrop;
1864
1865 impl Drop for PanicOnDrop {
1866 fn drop(&mut self) {
1867 panic!("and the payload went too");
1868 }
1869 }
1870
1871 impl std::io::Read for PanicsWithAHostilePayload {
1872 fn read(&mut self, _buf: &mut [u8]) -> std::io::Result<usize> {
1873 std::panic::panic_any(PanicOnDrop);
1874 }
1875 }
1876
1877 impl std::io::Seek for PanicsWithAHostilePayload {
1878 fn seek(&mut self, _pos: std::io::SeekFrom) -> std::io::Result<u64> {
1879 std::panic::panic_any(PanicOnDrop);
1880 }
1881 }
1882
1883 #[test]
1884 fn a_reader_panic_with_a_hostile_payload_does_not_abort_the_process() {
1885 // In its own process, because the assertion *is* the process: a
1886 // parent that sees the child exit cleanly has seen the abort not
1887 // happen. The guard caught the reader's panic and then dropped its
1888 // payload outside `catch_unwind`, so a payload whose `Drop` panics
1889 // sent that second panic straight out of `read` and into C —
1890 // through the very guard that exists to stop it.
1891 crate::fault_subprocess::in_subprocess(
1892 "demuxer::tests::a_reader_panic_with_a_hostile_payload_does_not_abort_the_process",
1893 || {
1894 let previous = std::panic::take_hook();
1895 std::panic::set_hook(Box::new(|_| {}));
1896 let opened =
1897 CarrierDemuxer::<crate::Owned>::open_reader(PanicsWithAHostilePayload, Some("x.mkv"));
1898 std::panic::set_hook(previous);
1899 match opened {
1900 Err(DemuxError::ReaderPanic(_)) => {}
1901 Err(other) => panic!("expected ReaderPanic, got {other:?}"),
1902 Ok(_) => panic!("a reader that only panics cannot open a container"),
1903 }
1904 },
1905 );
1906 }
1907
1908 #[test]
1909 fn codec_parameters_that_cannot_be_allocated_are_named() {
1910 // `Parameters::new` does not check `avcodec_parameters_alloc`, and
1911 // `clone_from` dereferences the result immediately: under a failed
1912 // allocation the shipped clone would write through null.
1913 crate::fault_subprocess::in_subprocess(
1914 "demuxer::tests::codec_parameters_that_cannot_be_allocated_are_named",
1915 || {
1916 let source = Parameters::new();
1917 assert!(
1918 !unsafe { source.as_ptr() }.is_null(),
1919 "the source allocates before the cap goes on",
1920 );
1921 crate::fault_subprocess::cap_ffmpeg_allocations(1);
1922 let refused = crate::extras::bounded_clone_parameters(&source, 4, usize::MAX);
1923 crate::fault_subprocess::uncap_ffmpeg_allocations();
1924 assert!(
1925 matches!(
1926 refused,
1927 Err(DemuxError::ParametersAlloc(ref p)) if p.stream_index() == 4
1928 ),
1929 "expected ParametersAlloc, got {:?}",
1930 refused.map(|_| ()),
1931 );
1932 // And with the cap lifted the same copy succeeds, so the
1933 // refusal was the allocator's answer and not a broken helper.
1934 crate::extras::bounded_clone_parameters(&source, 4, usize::MAX).expect("an uncapped copy");
1935 },
1936 );
1937 }
1938
1939 #[test]
1940 fn the_public_track_extra_copies_are_checked_too() {
1941 // The helper protected `build_tracks` and nothing else: `TrackExtra`
1942 // derived `Clone` and `Default` over `ffmpeg_next`'s `Parameters`,
1943 // whose clone dereferences an unchecked allocation — so safe public
1944 // code could still reach the SIGSEGV by copying a track row. The
1945 // derives are gone; what replaces them answers.
1946 crate::fault_subprocess::in_subprocess(
1947 "demuxer::tests::the_public_track_extra_copies_are_checked_too",
1948 || {
1949 let source = Parameters::new();
1950 assert!(!unsafe { source.as_ptr() }.is_null(), "allocated uncapped");
1951 let extra = TrackExtra::new(
1952 6,
1953 crate::extras::bounded_clone_parameters(&source, 6, usize::MAX).expect("uncapped"),
1954 )
1955 .expect("real parameters");
1956
1957 crate::fault_subprocess::cap_ffmpeg_allocations(1);
1958 let cloned = extra.try_clone().map(|_| ());
1959 let handed = extra.clone_parameters().map(|_| ());
1960 crate::fault_subprocess::uncap_ffmpeg_allocations();
1961
1962 assert!(
1963 matches!(cloned, Err(DemuxError::ParametersAlloc(ref p)) if p.stream_index() == 6),
1964 "TrackExtra::try_clone: {cloned:?}",
1965 );
1966 assert!(
1967 matches!(handed, Err(DemuxError::ParametersAlloc(ref p)) if p.stream_index() == 6),
1968 "TrackExtra::clone_parameters: {handed:?}",
1969 );
1970
1971 // And both work once the allocator does.
1972 extra.try_clone().expect("an uncapped row copy");
1973 extra.clone_parameters().expect("an uncapped handoff");
1974 },
1975 );
1976 }
1977
1978 #[test]
1979 fn parameters_that_never_allocated_are_refused_at_the_door() {
1980 // The route the destination check could not see. A safe
1981 // `Parameters::new()` under a failed allocation hands back a
1982 // null-backed value and says nothing; the copier then allocated its
1983 // own destination happily — the allocator having recovered by
1984 // then — and called `avcodec_parameters_copy(out, NULL)`, which
1985 // dereferences its source. Same crash, one recovery later, still
1986 // from safe public code.
1987 crate::fault_subprocess::in_subprocess(
1988 "demuxer::tests::parameters_that_never_allocated_are_refused_at_the_door",
1989 || {
1990 // The cap is on *while the source is built* — that is the whole
1991 // difference from the destination lane.
1992 crate::fault_subprocess::cap_ffmpeg_allocations(1);
1993 let never_allocated = Parameters::new();
1994 crate::fault_subprocess::uncap_ffmpeg_allocations();
1995 assert!(
1996 unsafe { never_allocated.as_ptr() }.is_null(),
1997 "the safe constructor really does hand back a null-backed value",
1998 );
1999
2000 // The door: a `TrackExtra` cannot exist over it, so the copy
2001 // methods have nothing to be asked on.
2002 let refused = TrackExtra::new(9, never_allocated);
2003 let Err(DemuxError::ParametersMissing(p)) = refused.map(|_| ()) else {
2004 panic!("a null-backed source must not become a track row");
2005 };
2006 assert_eq!(p.stream_index(), 9);
2007
2008 // And the copier refuses it too, so the invariant is not the
2009 // only thing standing between this and a null dereference.
2010 let never_allocated = {
2011 crate::fault_subprocess::cap_ffmpeg_allocations(1);
2012 let p = Parameters::new();
2013 crate::fault_subprocess::uncap_ffmpeg_allocations();
2014 p
2015 };
2016 assert!(matches!(
2017 crate::extras::bounded_clone_parameters(&never_allocated, 9, usize::MAX).map(|_| ()),
2018 Err(DemuxError::ParametersMissing(p)) if p.stream_index() == 9,
2019 ));
2020
2021 // A row built over real parameters still copies both ways, so
2022 // the refusal is about the null and nothing else.
2023 let real = Parameters::new();
2024 let extra = TrackExtra::new(9, real).expect("real parameters");
2025 extra.try_clone().expect("row copy");
2026 extra.clone_parameters().expect("handoff");
2027 },
2028 );
2029 }
2030
2031 #[cfg(feature = "resample")]
2032 #[test]
2033 fn a_spec_read_from_parameters_that_never_allocated_is_absent() {
2034 // The same trap at another public door, found by the sweep:
2035 // `ResampleSpec::from_parameters` asks `parameters.medium()`
2036 // first, and *that* dereferences the pointer inside ffmpeg-next
2037 // before any code of ours runs.
2038 crate::fault_subprocess::in_subprocess(
2039 "demuxer::tests::a_spec_read_from_parameters_that_never_allocated_is_absent",
2040 || {
2041 crate::fault_subprocess::cap_ffmpeg_allocations(1);
2042 let never_allocated = Parameters::new();
2043 crate::fault_subprocess::uncap_ffmpeg_allocations();
2044 assert!(unsafe { never_allocated.as_ptr() }.is_null());
2045 assert_eq!(
2046 crate::ResampleSpec::from_parameters(&never_allocated),
2047 None,
2048 "parameters that do not exist describe no audio",
2049 );
2050 },
2051 );
2052 }
2053
2054 #[test]
2055 fn codec_parameters_whose_copy_fails_are_named() {
2056 // The other leg: the destination allocates, and the deep copy of
2057 // the extradata does not. `clone_from` discards that return value,
2058 // so the shipped clone handed back parameters missing the very
2059 // bytes a decoder needs to open — and said nothing.
2060 crate::fault_subprocess::in_subprocess(
2061 "demuxer::tests::codec_parameters_whose_copy_fails_are_named",
2062 || {
2063 const EXTRADATA: usize = 8 * 1024 * 1024;
2064 let mut source = Parameters::new();
2065 // SAFETY: `source` owns a live `AVCodecParameters`; the buffer
2066 // comes from FFmpeg's allocator and is handed to it, so
2067 // `avcodec_parameters_free` releases it with the rest.
2068 unsafe {
2069 let par = source.as_mut_ptr();
2070 let extradata = ffmpeg_next::ffi::av_mallocz(EXTRADATA) as *mut u8;
2071 assert!(!extradata.is_null(), "av_mallocz");
2072 (*par).extradata = extradata;
2073 (*par).extradata_size = EXTRADATA as i32;
2074 }
2075
2076 // Big enough for the destination `AVCodecParameters`, far too
2077 // small for its extradata.
2078 crate::fault_subprocess::cap_ffmpeg_allocations(64 * 1024);
2079 let refused = crate::extras::bounded_clone_parameters(&source, 2, usize::MAX);
2080 crate::fault_subprocess::uncap_ffmpeg_allocations();
2081 match refused {
2082 Err(DemuxError::ParametersCopy(p)) => assert_eq!(p.stream_index(), 2),
2083 Err(other) => panic!("expected ParametersCopy, got {other:?}"),
2084 Ok(_) => panic!("a copy that could not copy the extradata must not succeed"),
2085 }
2086 crate::extras::bounded_clone_parameters(&source, 2, usize::MAX).expect("an uncapped copy");
2087 },
2088 );
2089 }
2090
2091 /// A stream whose `attached_pic` is `parked`, and the packet
2092 /// libavformat would queue for it.
2093 ///
2094 /// # On the fixture road
2095 ///
2096 /// The container shape this guards — a stream carrying
2097 /// `ATTACHED_PIC | TIMED_THUMBNAILS` — **cannot be minted by the
2098 /// ffmpeg CLI**, and that was censused rather than assumed: no muxer
2099 /// has a field for those bits (`-disposition:v
2100 /// attached_pic+timed_thumbnails` round-trips to nothing through
2101 /// mp4, mov and matroska alike), because the mov *demuxer* derives
2102 /// them from a chapter-track reference its own muxer does not write
2103 /// in that direction.
2104 ///
2105 /// What is reproducible, and what actually matters, is the **packet
2106 /// shape**: `read_frame_internal` queues a stream's parked picture
2107 /// with `av_packet_ref` while keeping its own reference, which is
2108 /// exactly what `av_packet_ref` builds here. The classification half
2109 /// — that such a stream is video rather than an attachment — is
2110 /// pinned separately by
2111 /// [`a_timed_thumbnail_stream_is_not_an_attachment`].
2112 fn parked_picture_stream(parked: &Packet) -> (Box<AVStream>, Packet) {
2113 use ffmpeg_next::packet::{Mut, Ref};
2114
2115 let mut stream: Box<AVStream> = Box::new(unsafe { std::mem::zeroed() });
2116 let mut queued = Packet::empty();
2117 // SAFETY: `parked` is a live refcounted packet; `av_packet_ref`
2118 // takes a reference to its buffer, which is precisely what
2119 // libavformat does when it queues an attached picture. The stream
2120 // is zeroed apart from the one field the probe reads.
2121 unsafe {
2122 assert_eq!(
2123 ffmpeg_next::ffi::av_packet_ref(queued.as_mut_ptr(), parked.as_ptr()),
2124 0,
2125 );
2126 stream.attached_pic.buf = (*parked.as_ptr()).buf;
2127 stream.attached_pic.data = (*parked.as_ptr()).data;
2128 stream.attached_pic.size = (*parked.as_ptr()).size;
2129 }
2130 (stream, queued)
2131 }
2132
2133 #[test]
2134 fn a_queued_attached_picture_is_recognised() {
2135 use ffmpeg_next::packet::Ref;
2136
2137 let parked = Packet::copy(&[9u8; 2048]);
2138 let (stream, queued) = parked_picture_stream(&parked);
2139
2140 // The two references are different structs around one allocation —
2141 // which is the whole reason the probe compares `buffer` and not the
2142 // `AVBufferRef`. Asserting the difference is what makes this a test
2143 // of the right comparison rather than of a lucky one.
2144 // SAFETY: both packets are live.
2145 unsafe {
2146 assert_ne!(
2147 (*queued.as_ptr()).buf,
2148 (*parked.as_ptr()).buf,
2149 "av_packet_ref must mint a new reference struct",
2150 );
2151 }
2152 // SAFETY: the stream is a zeroed `AVStream` whose only populated
2153 // fields are the ones the probe reads, and `queued` is live.
2154 assert!(unsafe { packet_is_parked_picture(&*stream, &queued) });
2155
2156 // An ordinary timed packet — the shape every pull after the first
2157 // one has — is not the parked picture.
2158 let ordinary = Packet::copy(&[1u8; 2048]);
2159 // SAFETY: as above.
2160 assert!(!unsafe { packet_is_parked_picture(&*stream, &ordinary) });
2161
2162 // And a stream that parks nothing recognises nothing.
2163 let bare: Box<AVStream> = Box::new(unsafe { std::mem::zeroed() });
2164 // SAFETY: as above.
2165 assert!(!unsafe { packet_is_parked_picture(&*bare, &queued) });
2166 }
2167
2168 #[test]
2169 fn the_queued_picture_is_admitted_and_later_packets_take_the_ordinary_road() {
2170 use crate::buffer::{PacketBufferError, PayloadProvenance, payload_of};
2171 use ffmpeg_next::packet::Ref;
2172
2173 let parked = Packet::copy(&[9u8; 2048]);
2174 let (_stream, queued) = parked_picture_stream(&parked);
2175 // SAFETY: the packet is live; `buf` is a public field.
2176 let parked_buffer = unsafe { (*parked.as_ptr()).buf };
2177
2178 // **The first pull.** Two references, one of them the container's.
2179 // From a *caller* that shape is refused, because a caller's second
2180 // reference may be a `Packet` with a safe `data_mut`.
2181 // SAFETY: `queued` is live for every call in this test.
2182 assert!(matches!(
2183 unsafe {
2184 payload_of::<crate::View>(
2185 queued.as_ptr(),
2186 usize::MAX,
2187 PayloadProvenance::CallerSupplied,
2188 )
2189 },
2190 Err(PacketBufferError::SharedPayload(_)),
2191 ));
2192
2193 // Delivered by the demux loop, the same shape is carried — by copy,
2194 // because a window would outlive the exclusivity the read rests on.
2195 // SAFETY: as above.
2196 let copied = unsafe {
2197 payload_of::<crate::View>(
2198 queued.as_ptr(),
2199 usize::MAX,
2200 PayloadProvenance::DemuxDelivered,
2201 )
2202 }
2203 .expect("a demux-delivered shared payload is carriable")
2204 .expect("it has a payload");
2205 assert_eq!(copied.as_ref(), &[9u8; 2048][..]);
2206 // SAFETY: the packet is live; `data` is a public field.
2207 unsafe {
2208 assert_ne!(
2209 copied.as_ref().as_ptr() as usize,
2210 (*queued.as_ptr()).data as usize,
2211 "a shared demux-delivered payload is copied, not windowed",
2212 );
2213 }
2214
2215 // With the provenance the probe establishes, both lanes carry it.
2216 // SAFETY: as above.
2217 let viewed = unsafe {
2218 payload_of::<crate::View>(
2219 queued.as_ptr(),
2220 usize::MAX,
2221 PayloadProvenance::AttachedPicture,
2222 )
2223 }
2224 .expect("the container's own picture is carriable")
2225 .expect("it has a payload");
2226 assert_eq!(viewed.as_ref(), &[9u8; 2048][..]);
2227 // And on the view lane it is a window into the parked allocation
2228 // rather than a copy of it.
2229 // SAFETY: both are live; `data`/`size` are public fields.
2230 unsafe {
2231 let start = (*parked_buffer).data as usize;
2232 let end = start + (*parked_buffer).size;
2233 let at = viewed.as_ref().as_ptr() as usize;
2234 assert!(
2235 at >= start && at + viewed.len() <= end,
2236 "the queued picture must be viewed, not copied",
2237 );
2238 }
2239 // SAFETY: as above.
2240 let owned = unsafe {
2241 payload_of::<crate::Owned>(
2242 queued.as_ptr(),
2243 usize::MAX,
2244 PayloadProvenance::AttachedPicture,
2245 )
2246 }
2247 .expect("the owned lane carries it too")
2248 .expect("it has a payload");
2249 assert_eq!(owned.as_ref(), &[9u8; 2048][..]);
2250
2251 // **Every pull after it.** A timed packet has a buffer of its own,
2252 // so it stays on the `Delivered` road, is unique, and the view lane
2253 // shares it.
2254 let later = Packet::copy(&[4u8; 1024]);
2255 // SAFETY: `later` is live.
2256 let shared = unsafe {
2257 payload_of::<crate::View>(
2258 later.as_ptr(),
2259 usize::MAX,
2260 PayloadProvenance::DemuxDelivered,
2261 )
2262 }
2263 .expect("an ordinary packet is carriable")
2264 .expect("it has a payload");
2265 // SAFETY: as above.
2266 unsafe {
2267 assert_eq!(
2268 shared.as_ref().as_ptr() as usize,
2269 (*later.as_ptr()).data as usize,
2270 "a uniquely-referenced packet is still shared, not copied",
2271 );
2272 }
2273 }
2274
2275 #[test]
2276 fn a_timed_thumbnail_stream_is_not_an_attachment() {
2277 // `TIMED_THUMBNAILS` is documented as only ever appearing beside
2278 // `ATTACHED_PIC`, so testing the picture bit alone reads a sparse
2279 // chapter-thumbnail track as cover art — and the attachment
2280 // contract then delivers exactly one of its images and drops the
2281 // rest, every one of which had a timestamp.
2282 assert!(
2283 is_attachment_disposition(AV_DISPOSITION_ATTACHED_PIC),
2284 "a plain attached picture is still an attachment",
2285 );
2286 assert!(
2287 !is_attachment_disposition(AV_DISPOSITION_ATTACHED_PIC | AV_DISPOSITION_TIMED_THUMBNAILS),
2288 "a timed-thumbnail stream is a timed track, whatever else it is flagged",
2289 );
2290 // Neither bit, and the other bits that ride along, change nothing.
2291 assert!(!is_attachment_disposition(0));
2292 assert!(!is_attachment_disposition(AV_DISPOSITION_TIMED_THUMBNAILS));
2293 assert!(is_attachment_disposition(
2294 AV_DISPOSITION_ATTACHED_PIC | ffmpeg_next::ffi::AV_DISPOSITION_DEFAULT
2295 ));
2296 // And the reason the raw bits are read at all: the wrapper's own
2297 // flag set cannot express the distinction.
2298 assert!(
2299 ffmpeg_next::format::stream::Disposition::from_bits(AV_DISPOSITION_TIMED_THUMBNAILS)
2300 .is_none(),
2301 "ffmpeg_next mints no TIMED_THUMBNAILS bit — from_bits_truncate would drop it silently",
2302 );
2303 }
2304
2305 #[test]
2306 fn an_uncapturable_cover_still_gets_its_one_packet() {
2307 // The state the shipped `AwaitingPacket` fallback existed for: a
2308 // stream that declares cover art and parks no payload. The fallback
2309 // waited for a packet that may never come, and let timed packets —
2310 // and seeks — go first, which the face forbids. The track now gets
2311 // its one packet at open like every other attachment track: empty,
2312 // and marked as this layer's own work.
2313 //
2314 // Not reachable from a file: across MP3, M4A, FLAC and Matroska,
2315 // every ATTACHED_PIC stream libavformat produces carries the parked
2316 // packet, because `ff_add_attached_pic` sets the disposition and
2317 // fills it in the same call. A zeroed `AVPacket` is exactly what
2318 // `attached_pic` would hold if one ever did not.
2319 let empty: ffmpeg_next::ffi::AVPacket = unsafe { std::mem::zeroed() };
2320 let packet = unsafe { attached_pic_payload::<crate::Owned>(&empty, 7, DemuxLimits::default()) }
2321 .expect("an unparked cover is a degenerate track, not an unreadable file");
2322 assert!(packet.data().as_ref().is_empty());
2323 assert!(
2324 packet.extra().synthesized(),
2325 "nothing in the container handed this payload over",
2326 );
2327 assert_eq!(packet.extra().stream_index(), 7);
2328 }
2329
2330 #[test]
2331 fn a_zero_denominator_timebase_is_clamped_not_refused() {
2332 // A malformed timebase makes one track's timestamps meaningless.
2333 // It must not make the file unreadable — every other track still
2334 // demuxes, and the caller can see the 1/1 for what it is.
2335 let tb = rational_to_timebase(Rational::new(1, 0));
2336 assert_eq!(tb.den().get(), 1);
2337 assert_eq!(tb.num(), 1);
2338 }
2339
2340 #[test]
2341 fn a_declared_frame_rate_becomes_a_rate_shaped_timebase() {
2342 let ntsc = rate_to_timebase(Rational::new(30_000, 1001)).expect("declared");
2343 assert_eq!((ntsc.num(), ntsc.den().get()), (30_000, 1001));
2344 assert_eq!(
2345 rate_to_timebase(Rational::new(0, 1)),
2346 None,
2347 "0 fps is absent"
2348 );
2349 assert_eq!(
2350 rate_to_timebase(Rational::new(30, 0)),
2351 None,
2352 "no denominator"
2353 );
2354 }
2355
2356 #[test]
2357 fn the_seek_timebase_is_microseconds() {
2358 // `avformat_seek_file` with `stream_index == -1` takes AV_TIME_BASE
2359 // units; a target expressed in anything else has to arrive there.
2360 let tb = av_time_base_q();
2361 assert_eq!((tb.num(), tb.den().get()), (1, 1_000_000));
2362 let target = Timestamp::new(1_500, Timebase::new(1, NonZeroI32::new(1000).expect("ms")));
2363 assert_eq!(target.rescale_to(tb).pts(), 1_500_000);
2364 }
2365}