Skip to main content

mediadecode_ffmpeg/
ticket.rs

1//! The owned codec ticket — one stream's `AVCodecParameters`, mirrored
2//! into plain Rust and rebuilt on demand.
3//!
4//! # Why the mirror exists
5//!
6//! [`ffmpeg_next::codec::Parameters`] is a `*mut AVCodecParameters`
7//! behind a `Send`-but-not-`Sync` wrapper. A track row that stores one
8//! is `!Sync`, an `Arc` of that row is `!Send`, and every consumer that
9//! shares a track table across tasks stops compiling — for a struct
10//! FFmpeg documents as a plain *descriptor* with no thread affinity at
11//! all. The auto-trait is missing, not the safety.
12//!
13//! The crate already answers that class one way: it mirrors what a
14//! consumer needs into owned Rust —
15//! [`TrackParams`](mediadecode::demuxer::TrackParams) mirrors the
16//! common seats, [`SideDataEntry`] mirrors a frame's metadata,
17//! [`FfmpegBytes`] mirrors its pixels. [`CodecTicket`] walks that road
18//! its last mile: **every** seat of an `AVCodecParameters`, held as
19//! owned bytes and plain integers, with `Sync` arriving by
20//! construction rather than by an `unsafe impl` over FFI.
21//!
22//! # The two halves
23//!
24//! * [`CodecTicket::mirror`] reads a live `AVCodecParameters` into the
25//!   ticket. It is the only place that reads one.
26//! * [`CodecTicket::rebuild`] allocates a fresh `AVCodecParameters` and
27//!   writes every seat back. It is the only place in this crate's
28//!   track-row road that allocates one, and what it hands back is what
29//!   `avcodec_parameters_to_context` is fed — unchanged from before the
30//!   mirror existed.
31//!
32//! The pair is proved in `tests/codec_ticket_parity.rs`: for every
33//! stream of every fixture the corpus can mint, the rebuilt struct is
34//! compared with the original **field by field**, including
35//! `extradata` bytes, the `AV_INPUT_BUFFER_PADDING_SIZE` zeroes past
36//! their end, every `coded_side_data` entry's type id and payload, and
37//! the channel layout down to a custom map's per-channel names. The
38//! shapes no container will hand over — a custom map, an unnamed
39//! side-data kind, every scalar set off its default — are built by
40//! hand in the same file, and a decoder is opened through a rebuilt
41//! ticket and made to produce a frame.
42//!
43//! # The reading discipline, inherited
44//!
45//! Not one bindgen enum is materialised out of FFmpeg memory. Every
46//! open C enum seat — the media type, the codec id, the field order,
47//! the five colour seats, the alpha mode, the channel order, a custom
48//! channel's id, a side-data type id — travels as **the raw 32-bit
49//! pattern it is on the wire**, read and written through the same
50//! `i32` cast, so a value this build's bindings cannot name is still a
51//! value this ticket carries. That is the same rule
52//! [`crate::extras::bounded_clone_parameters`] is written to, for the
53//! same reason: forming a typed reference to a struct whose enum field
54//! holds an unnamed discriminant is undefined behaviour before a
55//! single field is read.
56//!
57//! [`SideDataEntry`]: crate::extras::SideDataEntry
58//! [`FfmpegBytes`]: crate::FfmpegBytes
59
60use core::ptr::{addr_of, addr_of_mut, copy_nonoverlapping, read_unaligned, write_unaligned};
61
62use ffmpeg_next::{
63  codec::Parameters,
64  ffi::{
65    AV_INPUT_BUFFER_PADDING_SIZE, AVChannelCustom, AVChannelOrder, AVCodecParameters,
66    AVPacketSideData, av_mallocz,
67  },
68};
69
70use crate::{
71  FfmpegBytes,
72  demuxer::{
73    DemuxError, ParametersAlloc, ParametersChannelMap, ParametersCopy, ParametersMissing,
74    ParametersOpaque, ParametersTooLarge,
75  },
76  extras::{ExtradataPolicy, SideDataEntry, measure_parameters},
77};
78
79/// A verbatim `AVRational` seat.
80///
81/// Its own type rather than a [`mediatime::Timebase`] because the two
82/// seats it carries — `sample_aspect_ratio` and `framerate` — are not
83/// timebases and are not always valid ratios: FFmpeg spells "unknown"
84/// as a zero numerator (`sample_aspect_ratio`) or as `0/1`
85/// (`framerate`), and a mirror that normalised either would fail its
86/// own parity test. Nothing here reduces, validates or interprets;
87/// the numbers cross unchanged.
88#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Hash)]
89pub struct Ratio {
90  num: i32,
91  den: i32,
92}
93
94impl Ratio {
95  /// Constructs a `Ratio` from a numerator and denominator, verbatim.
96  #[cfg_attr(not(tarpaulin), inline(always))]
97  pub const fn new(num: i32, den: i32) -> Self {
98    Self { num, den }
99  }
100  /// The numerator.
101  #[cfg_attr(not(tarpaulin), inline(always))]
102  pub const fn num(&self) -> i32 {
103    self.num
104  }
105  /// The denominator.
106  #[cfg_attr(not(tarpaulin), inline(always))]
107  pub const fn den(&self) -> i32 {
108    self.den
109  }
110}
111
112/// One entry of a custom channel map — the `AV_CHANNEL_ORDER_CUSTOM`
113/// arm of [`ChannelLayoutTicket`].
114///
115/// `name` is FFmpeg's inline `char[16]`, carried as the sixteen bytes
116/// it is. It is a NUL-padded label, not a Rust string: FFmpeg's own
117/// contract is "may be filled with a 0-terminated string … otherwise
118/// it must be zeroed", so the bytes cross verbatim and any decoding
119/// into text is the consumer's choice, not the mirror's.
120#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Hash)]
121pub struct CustomChannel {
122  id: i32,
123  name: [u8; 16],
124}
125
126impl CustomChannel {
127  /// Constructs a `CustomChannel` from a raw `AVChannel` id and the
128  /// sixteen name bytes.
129  #[cfg_attr(not(tarpaulin), inline(always))]
130  pub const fn new(id: i32, name: [u8; 16]) -> Self {
131    Self { id, name }
132  }
133  /// The raw `AVChannel` id. Negative values are real: `AV_CHAN_NONE`
134  /// is `-1`.
135  #[cfg_attr(not(tarpaulin), inline(always))]
136  pub const fn id(&self) -> i32 {
137    self.id
138  }
139  /// The sixteen name bytes, NUL-padded, exactly as FFmpeg holds them.
140  #[cfg_attr(not(tarpaulin), inline(always))]
141  pub const fn name_bytes(&self) -> &[u8; 16] {
142    &self.name
143  }
144}
145
146/// The owned mirror of an `AVChannelLayout`.
147///
148/// The union is discriminated by `order`, and this type keeps that
149/// discrimination honest: `mask` is read and written **only** for the
150/// orders whose union arm is the bitmask, and `map` **only** for
151/// `AV_CHANNEL_ORDER_CUSTOM`, whose arm is a pointer. Reading the
152/// pointer arm as a mask would put a raw address in an owned mirror,
153/// which is the whole thing this type exists to stop.
154#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)]
155pub struct ChannelLayoutTicket {
156  order: i32,
157  channels: i32,
158  mask: u64,
159  map: Vec<CustomChannel>,
160}
161
162impl ChannelLayoutTicket {
163  /// The raw `AVChannelOrder`.
164  #[cfg_attr(not(tarpaulin), inline(always))]
165  pub const fn order(&self) -> i32 {
166    self.order
167  }
168  /// `nb_channels`, verbatim.
169  #[cfg_attr(not(tarpaulin), inline(always))]
170  pub const fn channels(&self) -> i32 {
171    self.channels
172  }
173  /// The channel bitmask, meaningful for every order but
174  /// `AV_CHANNEL_ORDER_CUSTOM`, where it reads zero and [`Self::map`]
175  /// carries the layout instead.
176  #[cfg_attr(not(tarpaulin), inline(always))]
177  pub const fn mask(&self) -> u64 {
178    self.mask
179  }
180  /// The custom channel map — empty for every order but
181  /// `AV_CHANNEL_ORDER_CUSTOM`.
182  #[cfg_attr(not(tarpaulin), inline(always))]
183  pub fn map(&self) -> &[CustomChannel] {
184    self.map.as_slice()
185  }
186
187  /// Whether this layout's union arm is the custom map.
188  #[cfg_attr(not(tarpaulin), inline(always))]
189  fn is_custom(&self) -> bool {
190    self.order == AVChannelOrder::AV_CHANNEL_ORDER_CUSTOM as i32
191  }
192}
193
194/// Every seat of one stream's `AVCodecParameters`, owned.
195///
196/// # The roster
197///
198/// All thirty-two fields FFmpeg n9.0 declares, in that struct's own
199/// order. Nothing is elided as "video only" or "audio only":
200/// `avcodec_parameters_to_context` reads a different subset per medium
201/// but the *file* carries whatever it carries, and a mirror that kept
202/// only one medium's subset would lose a seat the moment a container
203/// declared something unusual.
204///
205/// They land in three kinds. Three seats own heap and become owned
206/// Rust: `extradata`, `coded_side_data`, and `ch_layout`'s custom map.
207/// Two are lengths of those — `extradata_size` and
208/// `nb_coded_side_data` — and are **not stored**: a carrier already
209/// knows its own length, and a second copy of it is a second thing to
210/// keep in agreement. The remaining twenty-seven are scalars, held as
211/// the integers (or, for the two `AVRational` seats, the [`Ratio`])
212/// they are.
213///
214/// # `Send + Sync`, by construction
215///
216/// Every field is an integer, an [`FfmpegBytes`](crate::FfmpegBytes)
217/// (an `Arc<[u8]>`), or a `Vec` of those. There is no raw pointer, so
218/// there is no `unsafe impl` and no safety argument to get wrong —
219/// which is exactly the point of the road this type is on. See
220/// `tests::the_ticket_is_send_and_sync`.
221///
222/// # What it does not carry, and why that is a refusal rather than a
223/// loss
224///
225/// `AVChannelLayout::opaque` and `AVChannelCustom::opaque` are
226/// documented as "private data of the user": raw pointers, set by
227/// nobody but the caller who owns them, and unreadable to a mirror
228/// that must outlive the pointer's owner. libavformat never sets
229/// either, so no demuxed stream reaches this type carrying one. If one
230/// ever did, [`CodecTicket::mirror`] refuses with
231/// [`DemuxError::ParametersOpaque`] rather than dropping it in
232/// silence — the same fail-closed answer
233/// [`measure_parameters`](crate::extras) gives a channel order it has
234/// never heard of.
235#[derive(Clone)]
236pub struct CodecTicket {
237  /// The `AVStream.index` this mirror was taken at.
238  ///
239  /// Carried for one reason: so [`Self::rebuild`]'s errors can name
240  /// the stream they are about. A rebuild that reports `ENOMEM`
241  /// without saying which track it was opening is a log line nobody
242  /// can act on, and the mirror is the last place that knows.
243  stream_index: usize,
244  codec_type: i32,
245  codec_id: i32,
246  codec_tag: u32,
247  extradata: FfmpegBytes,
248  coded_side_data: Vec<SideDataEntry>,
249  format: i32,
250  bit_rate: i64,
251  bits_per_coded_sample: i32,
252  bits_per_raw_sample: i32,
253  profile: i32,
254  level: i32,
255  width: i32,
256  height: i32,
257  sample_aspect_ratio: Ratio,
258  framerate: Ratio,
259  field_order: i32,
260  color_range: i32,
261  color_primaries: i32,
262  color_trc: i32,
263  color_space: i32,
264  chroma_location: i32,
265  video_delay: i32,
266  ch_layout: ChannelLayoutTicket,
267  sample_rate: i32,
268  block_align: i32,
269  frame_size: i32,
270  initial_padding: i32,
271  trailing_padding: i32,
272  seek_preroll: i32,
273  alpha_mode: i32,
274  /// What [`Self::rebuild`] will ask FFmpeg's allocator for — see
275  /// [`Self::footprint_bytes`].
276  footprint_bytes: usize,
277}
278
279impl CodecTicket {
280  /// Mirrors a live set of codec parameters into an owned ticket.
281  ///
282  /// `budget` is the ceiling the mirror's heap seats must fit under,
283  /// measured before a byte is copied — the same admission
284  /// [`crate::extras::bounded_clone_parameters`] performs and the same
285  /// number `admit_streams` charges against the session's total. A set
286  /// of parameters over the ceiling is refused with
287  /// [`DemuxError::ParametersTooLarge`], never truncated.
288  ///
289  /// Fails with [`DemuxError::ParametersMissing`] when `source` is
290  /// null-backed — `Parameters::new()` and `Parameters::default()` are
291  /// safe constructors over an unchecked `avcodec_parameters_alloc`,
292  /// so a caller can hold one without ever having been told.
293  pub fn mirror(
294    source: &Parameters,
295    stream_index: usize,
296    budget: usize,
297  ) -> Result<Self, DemuxError> {
298    Self::mirror_with(source, stream_index, budget, ExtradataPolicy::Copy)
299  }
300
301  /// [`Self::mirror`], with the `extradata` policy named.
302  pub(crate) fn mirror_with(
303    source: &Parameters,
304    stream_index: usize,
305    budget: usize,
306    extradata_policy: ExtradataPolicy,
307  ) -> Result<Self, DemuxError> {
308    // SAFETY: reading the pointer without dereferencing it — which is
309    // what the null check exists for.
310    let par = unsafe { source.as_ptr() };
311    if par.is_null() {
312      return Err(DemuxError::ParametersMissing(ParametersMissing::new(
313        stream_index,
314      )));
315    }
316    // SAFETY: `par` is a live `AVCodecParameters` owned by `source`
317    // for the duration of this call.
318    unsafe { Self::from_raw(par, stream_index, budget, extradata_policy) }
319  }
320
321  /// [`Self::mirror`] over a raw pointer.
322  ///
323  /// # Safety
324  ///
325  /// `par` must be a non-null, live `*const AVCodecParameters` for the
326  /// duration of this call.
327  pub(crate) unsafe fn from_raw(
328    par: *const AVCodecParameters,
329    stream_index: usize,
330    budget: usize,
331    extradata_policy: ExtradataPolicy,
332  ) -> Result<Self, DemuxError> {
333    let too_large = |bytes: usize| {
334      DemuxError::ParametersTooLarge(ParametersTooLarge::new(stream_index, bytes, budget))
335    };
336
337    // Measured before a byte is copied, exactly as the bounded clone
338    // does it: the footprint enumerates the same three heap seats this
339    // mirror is about to read, and refusing here is what keeps an
340    // attacker-sized `extradata` or ICC profile from being copied into
341    // Rust memory just to be refused afterwards.
342    //
343    // SAFETY: `par` is live per this function's contract; the
344    // measurement allocates nothing and dereferences only what it
345    // counts.
346    let footprint = unsafe { measure_parameters(par) }.ok_or_else(|| too_large(usize::MAX))?;
347    let footprint_bytes = match extradata_policy {
348      ExtradataPolicy::Copy => footprint.total(),
349      ExtradataPolicy::Omit => footprint.total_without_extradata(),
350    }
351    .ok_or_else(|| too_large(usize::MAX))?;
352    if footprint_bytes > budget {
353      return Err(too_large(footprint_bytes));
354    }
355
356    // SAFETY: `par` is live; every read below either takes a scalar
357    // field by value or reaches one through `addr_of!`, and no enum
358    // field is read as anything but the `i32` pattern it is on the
359    // wire. See the module docs for why that distinction is
360    // load-bearing rather than stylistic.
361    let ticket = unsafe {
362      Self {
363        stream_index,
364        codec_type: read_unaligned(addr_of!((*par).codec_type).cast::<i32>()),
365        codec_id: read_unaligned(addr_of!((*par).codec_id).cast::<i32>()),
366        codec_tag: (*par).codec_tag,
367        extradata: extradata_of(par, extradata_policy),
368        coded_side_data: side_data_of(par),
369        format: (*par).format,
370        bit_rate: (*par).bit_rate,
371        bits_per_coded_sample: (*par).bits_per_coded_sample,
372        bits_per_raw_sample: (*par).bits_per_raw_sample,
373        profile: (*par).profile,
374        level: (*par).level,
375        width: (*par).width,
376        height: (*par).height,
377        sample_aspect_ratio: Ratio::new(
378          (*par).sample_aspect_ratio.num,
379          (*par).sample_aspect_ratio.den,
380        ),
381        framerate: Ratio::new((*par).framerate.num, (*par).framerate.den),
382        field_order: read_unaligned(addr_of!((*par).field_order).cast::<i32>()),
383        color_range: read_unaligned(addr_of!((*par).color_range).cast::<i32>()),
384        color_primaries: read_unaligned(addr_of!((*par).color_primaries).cast::<i32>()),
385        color_trc: read_unaligned(addr_of!((*par).color_trc).cast::<i32>()),
386        color_space: read_unaligned(addr_of!((*par).color_space).cast::<i32>()),
387        chroma_location: read_unaligned(addr_of!((*par).chroma_location).cast::<i32>()),
388        video_delay: (*par).video_delay,
389        ch_layout: channel_layout_of(par, stream_index)?,
390        sample_rate: (*par).sample_rate,
391        block_align: (*par).block_align,
392        frame_size: (*par).frame_size,
393        initial_padding: (*par).initial_padding,
394        trailing_padding: (*par).trailing_padding,
395        seek_preroll: (*par).seek_preroll,
396        alpha_mode: read_unaligned(addr_of!((*par).alpha_mode).cast::<i32>()),
397        footprint_bytes,
398      }
399    };
400    Ok(ticket)
401  }
402
403  /// Rebuilds a live `AVCodecParameters` from the ticket.
404  ///
405  /// **The one ffmpeg-native allocation on the track row's road**, and
406  /// the handoff a decoder is opened from:
407  ///
408  /// ```ignore
409  /// FfmpegAudioStreamDecoder::open(
410  ///   track.extra().clone_parameters()?,
411  ///   track.timebase(),
412  ///   limits,
413  /// )
414  /// ```
415  ///
416  /// Every seat that can hold a non-default value is written, so the
417  /// result depends on the ticket rather than on what
418  /// `avcodec_parameters_alloc` happened to leave behind. Stated
419  /// exactly, because the difference is load-bearing:
420  ///
421  /// * The **twenty-seven scalars** are written unconditionally. Those
422  ///   are the seats `avcodec_parameters_alloc` gives non-zero defaults
423  ///   to — `format` is `-1`, `profile` and `level` are
424  ///   `AV_PROFILE_UNKNOWN` / `AV_LEVEL_UNKNOWN`, both rationals are
425  ///   `0/1`, and so on — so leaving any of them would let a default
426  ///   masquerade as the file's own value.
427  /// * The **four descriptor seats** — `extradata` and
428  ///   `extradata_size`, `coded_side_data` and `nb_coded_side_data` —
429  ///   are written only when the ticket has something to put there. On
430  ///   the empty path they keep the allocator's zero, and that is
431  ///   correct rather than an omission: `codec_parameters_reset`
432  ///   `memset`s the whole struct to zero and then assigns non-zero
433  ///   defaults to a named list that contains none of these four. A
434  ///   null pointer with a zero length is exactly what "no extradata"
435  ///   and "no side data" mean, and it is what the source had.
436  /// * `ch_layout` is always written — order, channel count, and either
437  ///   the mask or the map.
438  ///
439  /// That is what makes field-by-field parity with the original
440  /// provable rather than hopeful, and
441  /// `tests/codec_ticket_parity.rs::every_scalar_seat_is_written_back`
442  /// is the assertion that a seat quietly relying on a default cannot
443  /// pass.
444  ///
445  /// Fallible because allocation is: `ParametersAlloc` when the struct
446  /// itself cannot be allocated, `ParametersCopy` carrying `ENOMEM`
447  /// when one of the heap seats cannot. Nothing here consults a
448  /// budget — the bytes are already resident and were admitted at
449  /// [`Self::mirror`]; what this allocates is exactly
450  /// [`Self::footprint_bytes`].
451  pub fn rebuild(&self) -> Result<Parameters, DemuxError> {
452    let stream_index = self.stream_index;
453    let mut out = Parameters::new();
454    // SAFETY: reading the pointer the constructor stored without
455    // dereferencing it — `Parameters::new` does not check
456    // `avcodec_parameters_alloc` and hands back a null on failure.
457    let dst = unsafe { out.as_mut_ptr() };
458    if dst.is_null() {
459      return Err(DemuxError::ParametersAlloc(ParametersAlloc::new(
460        stream_index,
461      )));
462    }
463
464    // SAFETY: `dst` is a live, freshly allocated `AVCodecParameters`
465    // whose heap seats are still null. Every write below is a scalar
466    // store, or an `addr_of_mut!` store of the same 32-bit pattern the
467    // mirror read, into a struct nothing else holds a reference to.
468    unsafe {
469      write_unaligned(
470        addr_of_mut!((*dst).codec_type).cast::<i32>(),
471        self.codec_type,
472      );
473      write_unaligned(addr_of_mut!((*dst).codec_id).cast::<i32>(), self.codec_id);
474      (*dst).codec_tag = self.codec_tag;
475      (*dst).format = self.format;
476      (*dst).bit_rate = self.bit_rate;
477      (*dst).bits_per_coded_sample = self.bits_per_coded_sample;
478      (*dst).bits_per_raw_sample = self.bits_per_raw_sample;
479      (*dst).profile = self.profile;
480      (*dst).level = self.level;
481      (*dst).width = self.width;
482      (*dst).height = self.height;
483      (*dst).sample_aspect_ratio.num = self.sample_aspect_ratio.num();
484      (*dst).sample_aspect_ratio.den = self.sample_aspect_ratio.den();
485      (*dst).framerate.num = self.framerate.num();
486      (*dst).framerate.den = self.framerate.den();
487      write_unaligned(
488        addr_of_mut!((*dst).field_order).cast::<i32>(),
489        self.field_order,
490      );
491      write_unaligned(
492        addr_of_mut!((*dst).color_range).cast::<i32>(),
493        self.color_range,
494      );
495      write_unaligned(
496        addr_of_mut!((*dst).color_primaries).cast::<i32>(),
497        self.color_primaries,
498      );
499      write_unaligned(addr_of_mut!((*dst).color_trc).cast::<i32>(), self.color_trc);
500      write_unaligned(
501        addr_of_mut!((*dst).color_space).cast::<i32>(),
502        self.color_space,
503      );
504      write_unaligned(
505        addr_of_mut!((*dst).chroma_location).cast::<i32>(),
506        self.chroma_location,
507      );
508      (*dst).video_delay = self.video_delay;
509      (*dst).sample_rate = self.sample_rate;
510      (*dst).block_align = self.block_align;
511      (*dst).frame_size = self.frame_size;
512      (*dst).initial_padding = self.initial_padding;
513      (*dst).trailing_padding = self.trailing_padding;
514      (*dst).seek_preroll = self.seek_preroll;
515      write_unaligned(
516        addr_of_mut!((*dst).alpha_mode).cast::<i32>(),
517        self.alpha_mode,
518      );
519    }
520
521    // The three heap seats, each allocated from FFmpeg's allocator so
522    // `avcodec_parameters_free` releases them with the struct — the
523    // same allocator discipline `bounded_clone_parameters` uses, and
524    // the same one `avcodec_parameters_copy` would have used.
525    //
526    // SAFETY: `dst` is live and its heap seats are null; each
527    // allocation below is checked, and each is attached to `dst`
528    // before the next one is attempted, so a failure part way leaves
529    // `out`'s own destructor a well-formed struct to free.
530    unsafe {
531      write_extradata(dst, self.extradata.as_slice(), stream_index)?;
532      write_side_data(dst, &self.coded_side_data, stream_index)?;
533      write_channel_layout(dst, &self.ch_layout, stream_index)?;
534    }
535
536    Ok(out)
537  }
538
539  /// What [`Self::rebuild`] asks FFmpeg's allocator for: `extradata`
540  /// with the `AV_INPUT_BUFFER_PADDING_SIZE` decoders read past the
541  /// end into, the `coded_side_data` descriptor array and every
542  /// entry's payload, and a custom channel map.
543  ///
544  /// The number the session admitted this stream at, and the number
545  /// `DemuxLimits::max_codec_parameter_bytes` was judged against — so
546  /// a row that opened is a row whose every rebuild fits the ceiling
547  /// it opened under.
548  ///
549  /// Not the ticket's own residency: the owned mirror holds the
550  /// payload without FFmpeg's trailing padding, and shares its buffers
551  /// by refcount.
552  #[cfg_attr(not(tarpaulin), inline(always))]
553  pub const fn footprint_bytes(&self) -> usize {
554    self.footprint_bytes
555  }
556
557  /// The `AVStream.index` this mirror was taken at — what
558  /// [`Self::rebuild`]'s errors name.
559  #[cfg_attr(not(tarpaulin), inline(always))]
560  pub const fn stream_index(&self) -> usize {
561    self.stream_index
562  }
563  /// The raw `AVMediaType`.
564  #[cfg_attr(not(tarpaulin), inline(always))]
565  pub const fn codec_type(&self) -> i32 {
566    self.codec_type
567  }
568  /// The raw `AVCodecID`.
569  #[cfg_attr(not(tarpaulin), inline(always))]
570  pub const fn codec_id(&self) -> i32 {
571    self.codec_id
572  }
573  /// The codec tag — the AVI FOURCC, when the container carries one.
574  #[cfg_attr(not(tarpaulin), inline(always))]
575  pub const fn codec_tag(&self) -> u32 {
576    self.codec_tag
577  }
578  /// The decoder-initialisation bytes — SPS/PPS for H.264, the
579  /// `AudioSpecificConfig` for AAC, a font's payload for an
580  /// attachment. Empty when the stream carries none, or when the row
581  /// was built on the attachment road that leaves them to the carrier.
582  #[cfg_attr(not(tarpaulin), inline(always))]
583  pub fn extradata(&self) -> &[u8] {
584    self.extradata.as_slice()
585  }
586  /// The extradata's carrier, for a consumer that wants the bytes
587  /// without copying them again.
588  #[cfg_attr(not(tarpaulin), inline(always))]
589  pub const fn extradata_ref(&self) -> &FfmpegBytes {
590    &self.extradata
591  }
592  /// Stream-level side data — where a MOV `prof` atom's ICC profile
593  /// arrives, among others.
594  #[cfg_attr(not(tarpaulin), inline(always))]
595  pub fn coded_side_data(&self) -> &[SideDataEntry] {
596    self.coded_side_data.as_slice()
597  }
598  /// The pixel format (video) or sample format (audio), as the raw
599  /// integer both enums share this seat as.
600  #[cfg_attr(not(tarpaulin), inline(always))]
601  pub const fn format(&self) -> i32 {
602    self.format
603  }
604  /// Average bitrate in bits per second.
605  #[cfg_attr(not(tarpaulin), inline(always))]
606  pub const fn bit_rate(&self) -> i64 {
607    self.bit_rate
608  }
609  /// Bits per sample in the coded bitstream.
610  #[cfg_attr(not(tarpaulin), inline(always))]
611  pub const fn bits_per_coded_sample(&self) -> i32 {
612    self.bits_per_coded_sample
613  }
614  /// Valid bits in each output sample.
615  #[cfg_attr(not(tarpaulin), inline(always))]
616  pub const fn bits_per_raw_sample(&self) -> i32 {
617    self.bits_per_raw_sample
618  }
619  /// The codec profile.
620  #[cfg_attr(not(tarpaulin), inline(always))]
621  pub const fn profile(&self) -> i32 {
622    self.profile
623  }
624  /// The codec level.
625  #[cfg_attr(not(tarpaulin), inline(always))]
626  pub const fn level(&self) -> i32 {
627    self.level
628  }
629  /// Frame width in pixels — video, and the subtitle canvas.
630  #[cfg_attr(not(tarpaulin), inline(always))]
631  pub const fn width(&self) -> i32 {
632    self.width
633  }
634  /// Frame height in pixels — video, and the subtitle canvas.
635  #[cfg_attr(not(tarpaulin), inline(always))]
636  pub const fn height(&self) -> i32 {
637    self.height
638  }
639  /// The sample aspect ratio. A zero numerator means unknown.
640  #[cfg_attr(not(tarpaulin), inline(always))]
641  pub const fn sample_aspect_ratio(&self) -> Ratio {
642    self.sample_aspect_ratio
643  }
644  /// The codec-level frame rate. `0/1` when frames differ in duration
645  /// or the value is not known.
646  #[cfg_attr(not(tarpaulin), inline(always))]
647  pub const fn framerate(&self) -> Ratio {
648    self.framerate
649  }
650  /// The raw `AVFieldOrder`.
651  #[cfg_attr(not(tarpaulin), inline(always))]
652  pub const fn field_order(&self) -> i32 {
653    self.field_order
654  }
655  /// The raw `AVColorRange`.
656  #[cfg_attr(not(tarpaulin), inline(always))]
657  pub const fn color_range(&self) -> i32 {
658    self.color_range
659  }
660  /// The raw `AVColorPrimaries`.
661  #[cfg_attr(not(tarpaulin), inline(always))]
662  pub const fn color_primaries(&self) -> i32 {
663    self.color_primaries
664  }
665  /// The raw `AVColorTransferCharacteristic`.
666  #[cfg_attr(not(tarpaulin), inline(always))]
667  pub const fn color_trc(&self) -> i32 {
668    self.color_trc
669  }
670  /// The raw `AVColorSpace`.
671  #[cfg_attr(not(tarpaulin), inline(always))]
672  pub const fn color_space(&self) -> i32 {
673    self.color_space
674  }
675  /// The raw `AVChromaLocation`.
676  #[cfg_attr(not(tarpaulin), inline(always))]
677  pub const fn chroma_location(&self) -> i32 {
678    self.chroma_location
679  }
680  /// Number of delayed frames — the decoder's `has_b_frames`.
681  #[cfg_attr(not(tarpaulin), inline(always))]
682  pub const fn video_delay(&self) -> i32 {
683    self.video_delay
684  }
685  /// The channel layout.
686  #[cfg_attr(not(tarpaulin), inline(always))]
687  pub const fn ch_layout(&self) -> &ChannelLayoutTicket {
688    &self.ch_layout
689  }
690  /// Audio samples per second.
691  #[cfg_attr(not(tarpaulin), inline(always))]
692  pub const fn sample_rate(&self) -> i32 {
693    self.sample_rate
694  }
695  /// Bytes per coded audio frame — `nBlockAlign` in `WAVEFORMATEX`.
696  #[cfg_attr(not(tarpaulin), inline(always))]
697  pub const fn block_align(&self) -> i32 {
698    self.block_align
699  }
700  /// Audio frame size, when the format fixes one.
701  #[cfg_attr(not(tarpaulin), inline(always))]
702  pub const fn frame_size(&self) -> i32 {
703    self.frame_size
704  }
705  /// Leading padding samples the encoder inserted.
706  #[cfg_attr(not(tarpaulin), inline(always))]
707  pub const fn initial_padding(&self) -> i32 {
708    self.initial_padding
709  }
710  /// Trailing padding samples the encoder appended.
711  #[cfg_attr(not(tarpaulin), inline(always))]
712  pub const fn trailing_padding(&self) -> i32 {
713    self.trailing_padding
714  }
715  /// Samples to skip after a discontinuity.
716  #[cfg_attr(not(tarpaulin), inline(always))]
717  pub const fn seek_preroll(&self) -> i32 {
718    self.seek_preroll
719  }
720  /// The raw `AVAlphaMode` — how an alpha channel relates to the
721  /// colour values, and the last field `AVCodecParameters` declares.
722  ///
723  /// New in FFmpeg n9.0, and the seat this mirror's first draft
724  /// dropped: video-only, left at its zero by every fixture the corpus
725  /// can mint, and therefore reading back identically whether it is
726  /// mirrored or forgotten. The parity comparator names every field
727  /// for exactly that reason.
728  #[cfg_attr(not(tarpaulin), inline(always))]
729  pub const fn alpha_mode(&self) -> i32 {
730    self.alpha_mode
731  }
732}
733
734impl core::fmt::Debug for CodecTicket {
735  /// Sizes rather than payloads. An `extradata` blob and an ICC
736  /// profile are both megabyte-scale and neither is readable; what a
737  /// reader of a log wants is the stream's identity and whether the
738  /// heap seats are populated.
739  fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
740    f.debug_struct("CodecTicket")
741      .field(
742        "medium",
743        &crate::boundary::media_kind_from_raw(self.codec_type),
744      )
745      .field("codec_id", &self.codec_id)
746      .field("codec_tag", &format_args!("{:#010x}", self.codec_tag))
747      .field("format", &self.format)
748      .field("width", &self.width)
749      .field("height", &self.height)
750      .field("sample_rate", &self.sample_rate)
751      .field("channels", &self.ch_layout.channels)
752      .field("extradata_len", &self.extradata.len())
753      .field("coded_side_data", &self.coded_side_data.len())
754      .field("footprint_bytes", &self.footprint_bytes)
755      .finish_non_exhaustive()
756  }
757}
758
759// ---------------------------------------------------------------------------
760//  Reading the three heap seats.
761// ---------------------------------------------------------------------------
762
763/// Copies `extradata` into an owned carrier — the payload only.
764///
765/// FFmpeg's `AV_INPUT_BUFFER_PADDING_SIZE` trailing zeroes are an
766/// allocation contract, not content: decoders read past the end of the
767/// buffer and the padding is what makes that defined. Carrying them in
768/// the mirror would store zeroes an owned `Arc<[u8]>` needs no reader
769/// to be safe past; [`write_extradata`] mints them again on the way
770/// back out, which is where they mean something.
771///
772/// # Safety
773///
774/// `par` must be a live `*const AVCodecParameters`.
775unsafe fn extradata_of(par: *const AVCodecParameters, policy: ExtradataPolicy) -> FfmpegBytes {
776  if matches!(policy, ExtradataPolicy::Omit) {
777    return FfmpegBytes::empty();
778  }
779  // SAFETY: `par` is live per the contract; both fields are a pointer
780  // and an integer.
781  let (ptr, size) = unsafe { ((*par).extradata, (*par).extradata_size) };
782  let Ok(len) = usize::try_from(size) else {
783    return FfmpegBytes::empty();
784  };
785  if ptr.is_null() || len == 0 {
786    return FfmpegBytes::empty();
787  }
788  // SAFETY: libavformat guarantees `extradata` is readable for
789  // `extradata_size` bytes while the parameters live, and the slice is
790  // consumed before this function returns.
791  FfmpegBytes::copy_from_slice(unsafe { core::slice::from_raw_parts(ptr, len) })
792}
793
794/// Copies every `coded_side_data` entry into owned entries.
795///
796/// # Safety
797///
798/// `par` must be a live `*const AVCodecParameters`.
799unsafe fn side_data_of(par: *const AVCodecParameters) -> Vec<SideDataEntry> {
800  // SAFETY: `par` is live per the contract.
801  let (array, count) = unsafe { ((*par).coded_side_data, (*par).nb_coded_side_data) };
802  let Ok(count) = usize::try_from(count) else {
803    return Vec::new();
804  };
805  if array.is_null() || count == 0 {
806    return Vec::new();
807  }
808  let mut entries = Vec::with_capacity(count);
809  for index in 0..count {
810    // **Never `&*entry`.** `AVPacketSideData::type` is an open C enum
811    // and an ABI-compatible FFmpeg newer than these bindings emits
812    // kinds absent from the generated Rust enum; forming a typed
813    // reference asserts every field inhabits its declared type, which
814    // is undefined behaviour before a single field is read.
815    //
816    // SAFETY: the array is valid for `nb_coded_side_data` contiguous
817    // entries per FFmpeg's contract, `index` is below that count, and
818    // `addr_of!` computes a field address without forming a reference
819    // to the struct containing it.
820    let (kind, data, size) = unsafe {
821      let entry = array.add(index);
822      (
823        read_unaligned(addr_of!((*entry).type_).cast::<i32>()),
824        read_unaligned(addr_of!((*entry).data)),
825        read_unaligned(addr_of!((*entry).size)),
826      )
827    };
828    let payload = if data.is_null() || size == 0 {
829      FfmpegBytes::empty()
830    } else {
831      // SAFETY: the descriptor declares `size` readable bytes at
832      // `data`, and the slice is consumed before the loop advances.
833      FfmpegBytes::copy_from_slice(unsafe { core::slice::from_raw_parts(data, size) })
834    };
835    entries.push(SideDataEntry::new(kind, payload));
836  }
837  entries
838}
839
840/// Mirrors the embedded `AVChannelLayout`.
841///
842/// # Safety
843///
844/// `par` must be a live `*const AVCodecParameters`.
845unsafe fn channel_layout_of(
846  par: *const AVCodecParameters,
847  stream_index: usize,
848) -> Result<ChannelLayoutTicket, DemuxError> {
849  // SAFETY: `ch_layout` is embedded by value; `addr_of!` reaches each
850  // field without forming a reference to the layout, and `order` has
851  // the layout of a `c_int`.
852  let (order, channels, opaque) = unsafe {
853    (
854      read_unaligned(addr_of!((*par).ch_layout.order).cast::<i32>()),
855      (*par).ch_layout.nb_channels,
856      (*par).ch_layout.opaque,
857    )
858  };
859  if !opaque.is_null() {
860    return Err(DemuxError::ParametersOpaque(ParametersOpaque::new(
861      stream_index,
862      None,
863    )));
864  }
865
866  let mut layout = ChannelLayoutTicket {
867    order,
868    channels,
869    mask: 0,
870    map: Vec::new(),
871  };
872  if !layout.is_custom() {
873    // Every order but `CUSTOM` describes its channels with the union's
874    // `mask` arm. Reading it for `CUSTOM` would read a pointer.
875    //
876    // SAFETY: the union is eight bytes either way and this arm is the
877    // one the order names.
878    layout.mask = unsafe { (*par).ch_layout.u.mask };
879    return Ok(layout);
880  }
881
882  // A custom order without a full map is **refused**, not reproduced.
883  // `av_channel_layout_copy` — the call
884  // `avcodec_parameters_to_context` moves this field through —
885  // allocates `nb_channels` entries and then `memcpy`s from
886  // `src->u.map` with no null check of its own, so a layout that names
887  // channels it has no map for makes libavcodec read from null the
888  // moment a decoder opens. Carrying it across would be a faithful
889  // round trip of a crash. See
890  // [`DemuxError::ParametersChannelMap`](crate::DemuxError).
891  //
892  // Refusing here is also what lets [`write_channel_layout`] rely on
893  // `map.len() == nb_channels` for a custom order.
894  //
895  // SAFETY: the order names the `map` arm.
896  let map = unsafe { (*par).ch_layout.u.map };
897  let malformed = || {
898    Err(DemuxError::ParametersChannelMap(ParametersChannelMap::new(
899      stream_index,
900      channels,
901    )))
902  };
903  let Ok(count) = usize::try_from(channels) else {
904    return malformed();
905  };
906  if map.is_null() || count == 0 {
907    return malformed();
908  }
909  layout.map.reserve_exact(count);
910  for index in 0..count {
911    // Field pointers again, never `&AVChannelCustom`: `id` is an open
912    // enum with the same hazard as a side-data type id.
913    //
914    // SAFETY: FFmpeg's contract makes the map `nb_channels` entries
915    // long, `index` is below that count, and every read goes through
916    // `addr_of!`.
917    let (id, name, opaque) = unsafe {
918      let entry = map.add(index);
919      (
920        read_unaligned(addr_of!((*entry).id).cast::<i32>()),
921        read_unaligned(addr_of!((*entry).name).cast::<[u8; 16]>()),
922        read_unaligned(addr_of!((*entry).opaque)),
923      )
924    };
925    if !opaque.is_null() {
926      return Err(DemuxError::ParametersOpaque(ParametersOpaque::new(
927        stream_index,
928        Some(index),
929      )));
930    }
931    layout.map.push(CustomChannel::new(id, name));
932  }
933  Ok(layout)
934}
935
936// ---------------------------------------------------------------------------
937//  Writing the three heap seats.
938// ---------------------------------------------------------------------------
939
940/// The `ENOMEM` a heap seat's allocation reports when it fails.
941fn seat_alloc_failed(stream_index: usize) -> DemuxError {
942  DemuxError::ParametersCopy(ParametersCopy::new(
943    stream_index,
944    ffmpeg_next::Error::Other {
945      errno: libc::ENOMEM,
946    },
947  ))
948}
949
950/// Allocates `extradata` and its padding, and copies the payload in.
951///
952/// # Safety
953///
954/// `dst` must be a live `*mut AVCodecParameters` whose `extradata` is
955/// null.
956unsafe fn write_extradata(
957  dst: *mut AVCodecParameters,
958  payload: &[u8],
959  stream_index: usize,
960) -> Result<(), DemuxError> {
961  if payload.is_empty() {
962    return Ok(());
963  }
964  let size = i32::try_from(payload.len()).map_err(|_| seat_alloc_failed(stream_index))?;
965  let padded = payload
966    .len()
967    .checked_add(AV_INPUT_BUFFER_PADDING_SIZE as usize)
968    .ok_or_else(|| seat_alloc_failed(stream_index))?;
969  // SAFETY: `av_mallocz` returns zeroed memory or null; the copy
970  // writes exactly `payload.len()` bytes into an allocation that is
971  // `AV_INPUT_BUFFER_PADDING_SIZE` longer, leaving the padding zero —
972  // which is the contract decoders read past the end under.
973  unsafe {
974    let buffer = av_mallocz(padded).cast::<u8>();
975    if buffer.is_null() {
976      return Err(seat_alloc_failed(stream_index));
977    }
978    copy_nonoverlapping(payload.as_ptr(), buffer, payload.len());
979    (*dst).extradata = buffer;
980    (*dst).extradata_size = size;
981  }
982  Ok(())
983}
984
985/// Allocates the `coded_side_data` descriptor array and each payload.
986///
987/// # Safety
988///
989/// `dst` must be a live `*mut AVCodecParameters` whose
990/// `coded_side_data` is null.
991unsafe fn write_side_data(
992  dst: *mut AVCodecParameters,
993  entries: &[SideDataEntry],
994  stream_index: usize,
995) -> Result<(), DemuxError> {
996  if entries.is_empty() {
997    return Ok(());
998  }
999  let count = i32::try_from(entries.len()).map_err(|_| seat_alloc_failed(stream_index))?;
1000  let bytes = entries
1001    .len()
1002    .checked_mul(core::mem::size_of::<AVPacketSideData>())
1003    .ok_or_else(|| seat_alloc_failed(stream_index))?;
1004
1005  // SAFETY: `dst` is live with a null `coded_side_data`. The array is
1006  // attached before any payload is filled in, so a failure part way
1007  // leaves the destructor a well-formed array to walk: the entries it
1008  // has not reached are zeroed, and freeing a null payload is a no-op.
1009  unsafe {
1010    let array = av_mallocz(bytes).cast::<AVPacketSideData>();
1011    if array.is_null() {
1012      return Err(seat_alloc_failed(stream_index));
1013    }
1014    (*dst).coded_side_data = array;
1015    (*dst).nb_coded_side_data = count;
1016
1017    for (index, entry) in entries.iter().enumerate() {
1018      let into = array.add(index);
1019      // The type id travels as the raw bits it is on the wire, for the
1020      // reason the read did: a kind these bindings cannot name is
1021      // still a kind the file carries and a decoder may want.
1022      write_unaligned(addr_of_mut!((*into).type_).cast::<i32>(), entry.kind());
1023      let payload = entry.data();
1024      if payload.is_empty() {
1025        continue;
1026      }
1027      let buffer = av_mallocz(payload.len()).cast::<u8>();
1028      if buffer.is_null() {
1029        return Err(seat_alloc_failed(stream_index));
1030      }
1031      copy_nonoverlapping(payload.as_ptr(), buffer, payload.len());
1032      write_unaligned(addr_of_mut!((*into).data), buffer);
1033      write_unaligned(addr_of_mut!((*into).size), payload.len());
1034    }
1035  }
1036  Ok(())
1037}
1038
1039/// Writes the channel layout, allocating a custom map when the order
1040/// names one.
1041///
1042/// Written field by field rather than through
1043/// `av_channel_layout_copy`, because the source it would copy from is
1044/// the thing this road has abolished: there is no live
1045/// `AVChannelLayout` to copy, only owned Rust. The one allocation is
1046/// the custom map, which [`CodecTicket::mirror`] already measured and
1047/// admitted.
1048///
1049/// # Safety
1050///
1051/// `dst` must be a live `*mut AVCodecParameters` whose `ch_layout` is
1052/// the zeroed (`AV_CHANNEL_ORDER_UNSPEC`) state
1053/// `avcodec_parameters_alloc` leaves, owning no map.
1054unsafe fn write_channel_layout(
1055  dst: *mut AVCodecParameters,
1056  layout: &ChannelLayoutTicket,
1057  stream_index: usize,
1058) -> Result<(), DemuxError> {
1059  // SAFETY: `dst` is live and its layout owns nothing yet; `order` is
1060  // written as the same 32-bit pattern the mirror read.
1061  unsafe {
1062    write_unaligned(
1063      addr_of_mut!((*dst).ch_layout.order).cast::<i32>(),
1064      layout.order(),
1065    );
1066    (*dst).ch_layout.opaque = core::ptr::null_mut();
1067  }
1068
1069  if !layout.is_custom() {
1070    // SAFETY: the order names the `mask` arm.
1071    unsafe {
1072      (*dst).ch_layout.nb_channels = layout.channels();
1073      (*dst).ch_layout.u.mask = layout.mask();
1074    }
1075    return Ok(());
1076  }
1077
1078  // **`nb_channels` comes from the map, not from the stored field.**
1079  // The two are equal — [`channel_layout_of`] refuses a custom layout
1080  // it cannot map in full, and the fields are private, so no other
1081  // value can exist. Writing the count from the array anyway is what
1082  // makes that structural rather than remembered: the layout handed to
1083  // libavcodec can never declare more channels than the array it points
1084  // at, which is precisely the shape `av_channel_layout_copy` would
1085  // `memcpy` past the end of.
1086  debug_assert_eq!(
1087    layout.map().len(),
1088    layout.channels().max(0) as usize,
1089    "a custom layout's map length is its channel count",
1090  );
1091  let count = layout.map().len();
1092  if count == 0 {
1093    // Unreachable through `mirror`, and fail-closed if a future
1094    // constructor ever makes it reachable.
1095    return Err(DemuxError::ParametersChannelMap(ParametersChannelMap::new(
1096      stream_index,
1097      layout.channels(),
1098    )));
1099  }
1100
1101  let declared = i32::try_from(count).map_err(|_| seat_alloc_failed(stream_index))?;
1102  let bytes = count
1103    .checked_mul(core::mem::size_of::<AVChannelCustom>())
1104    .ok_or_else(|| seat_alloc_failed(stream_index))?;
1105  // SAFETY: `av_mallocz` returns zeroed memory or null. The map and the
1106  // count it describes are attached together, before the entries are
1107  // filled in, so a later failure leaves a well-formed (zeroed) map for
1108  // the destructor to free, and every write goes through
1109  // `addr_of_mut!` rather than a typed reference.
1110  unsafe {
1111    let map = av_mallocz(bytes).cast::<AVChannelCustom>();
1112    if map.is_null() {
1113      return Err(seat_alloc_failed(stream_index));
1114    }
1115    (*dst).ch_layout.u.map = map;
1116    (*dst).ch_layout.nb_channels = declared;
1117    for (index, channel) in layout.map().iter().enumerate() {
1118      let into = map.add(index);
1119      write_unaligned(addr_of_mut!((*into).id).cast::<i32>(), channel.id());
1120      write_unaligned(
1121        addr_of_mut!((*into).name).cast::<[u8; 16]>(),
1122        *channel.name_bytes(),
1123      );
1124      write_unaligned(addr_of_mut!((*into).opaque), core::ptr::null_mut());
1125    }
1126  }
1127  Ok(())
1128}
1129
1130#[cfg(test)]
1131mod tests;