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, AVPacketSideDataType, 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/// The Dolby Vision decoder configuration record's two routing seats —
113/// the profile number and the base-layer signal-compatibility id —
114/// read from the container's `dvcC`/`dvvC`/`dwvC` box when present.
115///
116/// **Numbers, not interpretation.** This crate does not map `profile`
117/// onto a named Dolby Vision profile (5, 7, 8.1, …) or `compatibility_id`
118/// onto "HDR10-compatible" / "SDR-compatible" / etc. — those tables are
119/// Dolby's own and change independently of this crate's release cycle;
120/// the consumer that already routes base-layer-vs-refuse on this value
121/// (per the sealed ground this type answers to) owns that table. What
122/// crosses here is exactly what the box declared, unchanged.
123///
124/// `Copy`: two bytes, nothing owned.
125#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
126pub struct DolbyVisionConfig {
127  profile: u8,
128  compatibility_id: u8,
129}
130
131impl DolbyVisionConfig {
132  /// Constructs a `DolbyVisionConfig` from its two routing numbers.
133  #[cfg_attr(not(tarpaulin), inline(always))]
134  pub const fn new(profile: u8, compatibility_id: u8) -> Self {
135    Self {
136      profile,
137      compatibility_id,
138    }
139  }
140  /// The Dolby Vision profile number (`dv_profile`), verbatim.
141  #[cfg_attr(not(tarpaulin), inline(always))]
142  pub const fn profile(&self) -> u8 {
143    self.profile
144  }
145  /// The base-layer signal-compatibility id (`dv_bl_signal_
146  /// compatibility_id`), verbatim — what the consumer this record was
147  /// sealed for routes base-layer-vs-refuse on.
148  #[cfg_attr(not(tarpaulin), inline(always))]
149  pub const fn compatibility_id(&self) -> u8 {
150    self.compatibility_id
151  }
152}
153
154/// Byte offset of `dv_profile` in FFmpeg's in-process
155/// `AVDOVIDecoderConfigurationRecord` (`libavutil/dovi_meta.h`): two
156/// leading version bytes, then the profile.
157const DOVI_CONFIG_PROFILE_OFFSET: usize = 2;
158/// Byte offset of `dv_bl_signal_compatibility_id`: version (2) +
159/// profile (1) + level (1) + three one-byte presence flags (3).
160const DOVI_CONFIG_COMPATIBILITY_ID_OFFSET: usize = 7;
161/// Minimum payload length [`parse_dolby_vision_config`] needs — enough
162/// to read the compatibility id, the later of the two seats. The full
163/// struct FFmpeg n9.0 allocates is nine bytes (a ninth,
164/// `dv_md_compression`, follows); this function reads neither that
165/// byte nor relies on the struct's total size, which its own header
166/// documents as **not** part of the public ABI.
167const DOVI_CONFIG_MIN_BYTES: usize = DOVI_CONFIG_COMPATIBILITY_ID_OFFSET + 1;
168
169/// Parses an `AV_PKT_DATA_DOVI_CONF` payload — a byte-for-byte copy of
170/// FFmpeg's `AVDOVIDecoderConfigurationRecord` (`dv_version_major,
171/// dv_version_minor, dv_profile, dv_level, rpu_present_flag,
172/// el_present_flag, bl_present_flag, dv_bl_signal_compatibility_id,
173/// [dv_md_compression]`, every seat one byte) — into the two routing
174/// numbers. `None` when the payload is shorter than
175/// [`DOVI_CONFIG_MIN_BYTES`] — a version-skew or corrupt entry.
176fn parse_dolby_vision_config(bytes: &[u8]) -> Option<DolbyVisionConfig> {
177  if bytes.len() < DOVI_CONFIG_MIN_BYTES {
178    return None;
179  }
180  Some(DolbyVisionConfig::new(
181    bytes[DOVI_CONFIG_PROFILE_OFFSET],
182    bytes[DOVI_CONFIG_COMPATIBILITY_ID_OFFSET],
183  ))
184}
185
186/// One entry of a custom channel map — the `AV_CHANNEL_ORDER_CUSTOM`
187/// arm of [`ChannelLayoutTicket`].
188///
189/// `name` is FFmpeg's inline `char[16]`, carried as the sixteen bytes
190/// it is. It is a NUL-padded label, not a Rust string: FFmpeg's own
191/// contract is "may be filled with a 0-terminated string … otherwise
192/// it must be zeroed", so the bytes cross verbatim and any decoding
193/// into text is the consumer's choice, not the mirror's.
194#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Hash)]
195pub struct CustomChannel {
196  id: i32,
197  name: [u8; 16],
198}
199
200impl CustomChannel {
201  /// Constructs a `CustomChannel` from a raw `AVChannel` id and the
202  /// sixteen name bytes.
203  #[cfg_attr(not(tarpaulin), inline(always))]
204  pub const fn new(id: i32, name: [u8; 16]) -> Self {
205    Self { id, name }
206  }
207  /// The raw `AVChannel` id. Negative values are real: `AV_CHAN_NONE`
208  /// is `-1`.
209  #[cfg_attr(not(tarpaulin), inline(always))]
210  pub const fn id(&self) -> i32 {
211    self.id
212  }
213  /// The sixteen name bytes, NUL-padded, exactly as FFmpeg holds them.
214  #[cfg_attr(not(tarpaulin), inline(always))]
215  pub const fn name_bytes(&self) -> &[u8; 16] {
216    &self.name
217  }
218}
219
220/// The owned mirror of an `AVChannelLayout`.
221///
222/// The union is discriminated by `order`, and this type keeps that
223/// discrimination honest: `mask` is read and written **only** for the
224/// orders whose union arm is the bitmask, and `map` **only** for
225/// `AV_CHANNEL_ORDER_CUSTOM`, whose arm is a pointer. Reading the
226/// pointer arm as a mask would put a raw address in an owned mirror,
227/// which is the whole thing this type exists to stop.
228#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)]
229pub struct ChannelLayoutTicket {
230  order: i32,
231  channels: i32,
232  mask: u64,
233  map: Vec<CustomChannel>,
234}
235
236impl ChannelLayoutTicket {
237  /// The raw `AVChannelOrder`.
238  #[cfg_attr(not(tarpaulin), inline(always))]
239  pub const fn order(&self) -> i32 {
240    self.order
241  }
242  /// `nb_channels`, verbatim.
243  #[cfg_attr(not(tarpaulin), inline(always))]
244  pub const fn channels(&self) -> i32 {
245    self.channels
246  }
247  /// The channel bitmask, meaningful for every order but
248  /// `AV_CHANNEL_ORDER_CUSTOM`, where it reads zero and [`Self::map`]
249  /// carries the layout instead.
250  #[cfg_attr(not(tarpaulin), inline(always))]
251  pub const fn mask(&self) -> u64 {
252    self.mask
253  }
254  /// The custom channel map — empty for every order but
255  /// `AV_CHANNEL_ORDER_CUSTOM`.
256  #[cfg_attr(not(tarpaulin), inline(always))]
257  pub fn map(&self) -> &[CustomChannel] {
258    self.map.as_slice()
259  }
260
261  // `is_custom` used to live here, and its removal is the point rather
262  // than tidying. Every caller asked it "is this custom?" and treated
263  // `false` as "mask-backed" — which is how an unspecified layout's
264  // undefined union came to be read into `mask`. The question has three
265  // answers, not two, and
266  // [`LayoutArm`](crate::channel_layout::LayoutArm) is the only thing
267  // that answers it now.
268}
269
270/// Every seat of one stream's `AVCodecParameters`, owned.
271///
272/// # The roster
273///
274/// All thirty-two fields FFmpeg n9.0 declares, in that struct's own
275/// order. Nothing is elided as "video only" or "audio only":
276/// `avcodec_parameters_to_context` reads a different subset per medium
277/// but the *file* carries whatever it carries, and a mirror that kept
278/// only one medium's subset would lose a seat the moment a container
279/// declared something unusual.
280///
281/// They land in three kinds. Three seats own heap and become owned
282/// Rust: `extradata`, `coded_side_data`, and `ch_layout`'s custom map.
283/// Two are lengths of those — `extradata_size` and
284/// `nb_coded_side_data` — and are **not stored**: a carrier already
285/// knows its own length, and a second copy of it is a second thing to
286/// keep in agreement. The remaining twenty-seven are scalars, held as
287/// the integers (or, for the two `AVRational` seats, the [`Ratio`])
288/// they are.
289///
290/// # `Send + Sync`, by construction
291///
292/// Every field is an integer, an [`FfmpegBytes`](crate::FfmpegBytes)
293/// (an `Arc<[u8]>`), or a `Vec` of those. There is no raw pointer, so
294/// there is no `unsafe impl` and no safety argument to get wrong —
295/// which is exactly the point of the road this type is on. See
296/// `tests::the_ticket_is_send_and_sync`.
297///
298/// # What it does not carry, and why that is a refusal rather than a
299/// loss
300///
301/// `AVChannelLayout::opaque` and `AVChannelCustom::opaque` are
302/// documented as "private data of the user": raw pointers, set by
303/// nobody but the caller who owns them, and unreadable to a mirror
304/// that must outlive the pointer's owner. libavformat never sets
305/// either, so no demuxed stream reaches this type carrying one. If one
306/// ever did, [`CodecTicket::mirror`] refuses with
307/// [`DemuxError::ParametersOpaque`] rather than dropping it in
308/// silence — the same fail-closed answer
309/// [`measure_parameters`](crate::extras) gives a channel order it has
310/// never heard of.
311#[derive(Clone)]
312pub struct CodecTicket {
313  /// The `AVStream.index` this mirror was taken at.
314  ///
315  /// Carried for one reason: so [`Self::rebuild`]'s errors can name
316  /// the stream they are about. A rebuild that reports `ENOMEM`
317  /// without saying which track it was opening is a log line nobody
318  /// can act on, and the mirror is the last place that knows.
319  stream_index: usize,
320  codec_type: i32,
321  codec_id: i32,
322  codec_tag: u32,
323  extradata: FfmpegBytes,
324  coded_side_data: Vec<SideDataEntry>,
325  format: i32,
326  bit_rate: i64,
327  bits_per_coded_sample: i32,
328  bits_per_raw_sample: i32,
329  profile: i32,
330  level: i32,
331  width: i32,
332  height: i32,
333  sample_aspect_ratio: Ratio,
334  framerate: Ratio,
335  field_order: i32,
336  color_range: i32,
337  color_primaries: i32,
338  color_trc: i32,
339  color_space: i32,
340  chroma_location: i32,
341  video_delay: i32,
342  ch_layout: ChannelLayoutTicket,
343  sample_rate: i32,
344  block_align: i32,
345  frame_size: i32,
346  initial_padding: i32,
347  trailing_padding: i32,
348  seek_preroll: i32,
349  alpha_mode: i32,
350  /// What [`Self::rebuild`] will ask FFmpeg's allocator for — see
351  /// [`Self::footprint_bytes`].
352  footprint_bytes: usize,
353}
354
355impl CodecTicket {
356  /// Mirrors a live set of codec parameters into an owned ticket.
357  ///
358  /// `budget` is the ceiling the mirror's heap seats must fit under,
359  /// measured before a byte is copied — the same admission
360  /// [`crate::extras::bounded_clone_parameters`] performs and the same
361  /// number `admit_streams` charges against the session's total. A set
362  /// of parameters over the ceiling is refused with
363  /// [`DemuxError::ParametersTooLarge`], never truncated.
364  ///
365  /// Fails with [`DemuxError::ParametersMissing`] when `source` is
366  /// null-backed — `Parameters::new()` and `Parameters::default()` are
367  /// safe constructors over an unchecked `avcodec_parameters_alloc`,
368  /// so a caller can hold one without ever having been told.
369  pub fn mirror(
370    source: &Parameters,
371    stream_index: usize,
372    budget: usize,
373  ) -> Result<Self, DemuxError> {
374    Self::mirror_with(source, stream_index, budget, ExtradataPolicy::Copy)
375  }
376
377  /// [`Self::mirror`], with the `extradata` policy named.
378  pub(crate) fn mirror_with(
379    source: &Parameters,
380    stream_index: usize,
381    budget: usize,
382    extradata_policy: ExtradataPolicy,
383  ) -> Result<Self, DemuxError> {
384    // SAFETY: reading the pointer without dereferencing it — which is
385    // what the null check exists for.
386    let par = unsafe { source.as_ptr() };
387    if par.is_null() {
388      return Err(DemuxError::ParametersMissing(ParametersMissing::new(
389        stream_index,
390      )));
391    }
392    // SAFETY: `par` is a live `AVCodecParameters` owned by `source`
393    // for the duration of this call.
394    unsafe { Self::from_raw(par, stream_index, budget, extradata_policy) }
395  }
396
397  /// [`Self::mirror`] over a raw pointer.
398  ///
399  /// # Safety
400  ///
401  /// `par` must be a non-null, live `*const AVCodecParameters` for the
402  /// duration of this call.
403  pub(crate) unsafe fn from_raw(
404    par: *const AVCodecParameters,
405    stream_index: usize,
406    budget: usize,
407    extradata_policy: ExtradataPolicy,
408  ) -> Result<Self, DemuxError> {
409    let too_large = |bytes: usize| {
410      DemuxError::ParametersTooLarge(ParametersTooLarge::new(stream_index, bytes, budget))
411    };
412
413    // **Structure first, before the measurement and long before the
414    // copies.** The demux road already ran this during admission, but
415    // this constructor is reachable on its own, and it used to copy
416    // `extradata` and every coded-side-data payload — the two
417    // attacker-sized seats — and only then reach
418    // [`channel_layout_of`], which is where a malformed layout was
419    // discovered. A deterministic refusal derivable from the struct's
420    // own declared fields has no business arriving after two payload
421    // copies, whichever caller asked for the mirror.
422    //
423    // It costs a pointer walk and allocates nothing. See
424    // [`validate_channel_layout`], which is the whole of what can be
425    // refused here from structure alone.
426    //
427    // SAFETY: this function's own contract, forwarded — `par` is a
428    // non-null, live `*const AVCodecParameters`.
429    unsafe { validate_channel_layout(par, stream_index) }?;
430
431    // Measured before a byte is copied, exactly as the bounded clone
432    // does it: the footprint enumerates the same three heap seats this
433    // mirror is about to read, and refusing here is what keeps an
434    // attacker-sized `extradata` or ICC profile from being copied into
435    // Rust memory just to be refused afterwards.
436    //
437    // SAFETY: `par` is live per this function's contract; the
438    // measurement allocates nothing and dereferences only what it
439    // counts.
440    let footprint = unsafe { measure_parameters(par) }.ok_or_else(|| too_large(usize::MAX))?;
441    let footprint_bytes = match extradata_policy {
442      ExtradataPolicy::Copy => footprint.total(),
443      ExtradataPolicy::Omit => footprint.total_without_extradata(),
444    }
445    .ok_or_else(|| too_large(usize::MAX))?;
446    if footprint_bytes > budget {
447      return Err(too_large(footprint_bytes));
448    }
449
450    // SAFETY: `par` is live; every read below either takes a scalar
451    // field by value or reaches one through `addr_of!`, and no enum
452    // field is read as anything but the `i32` pattern it is on the
453    // wire. See the module docs for why that distinction is
454    // load-bearing rather than stylistic.
455    let ticket = unsafe {
456      Self {
457        stream_index,
458        codec_type: read_unaligned(addr_of!((*par).codec_type).cast::<i32>()),
459        codec_id: read_unaligned(addr_of!((*par).codec_id).cast::<i32>()),
460        codec_tag: (*par).codec_tag,
461        extradata: extradata_of(par, extradata_policy, stream_index)?,
462        coded_side_data: side_data_of(par, stream_index)?,
463        format: (*par).format,
464        bit_rate: (*par).bit_rate,
465        bits_per_coded_sample: (*par).bits_per_coded_sample,
466        bits_per_raw_sample: (*par).bits_per_raw_sample,
467        profile: (*par).profile,
468        level: (*par).level,
469        width: (*par).width,
470        height: (*par).height,
471        sample_aspect_ratio: Ratio::new(
472          (*par).sample_aspect_ratio.num,
473          (*par).sample_aspect_ratio.den,
474        ),
475        framerate: Ratio::new((*par).framerate.num, (*par).framerate.den),
476        field_order: read_unaligned(addr_of!((*par).field_order).cast::<i32>()),
477        color_range: read_unaligned(addr_of!((*par).color_range).cast::<i32>()),
478        color_primaries: read_unaligned(addr_of!((*par).color_primaries).cast::<i32>()),
479        color_trc: read_unaligned(addr_of!((*par).color_trc).cast::<i32>()),
480        color_space: read_unaligned(addr_of!((*par).color_space).cast::<i32>()),
481        chroma_location: read_unaligned(addr_of!((*par).chroma_location).cast::<i32>()),
482        video_delay: (*par).video_delay,
483        ch_layout: channel_layout_of(par, stream_index)?,
484        sample_rate: (*par).sample_rate,
485        block_align: (*par).block_align,
486        frame_size: (*par).frame_size,
487        initial_padding: (*par).initial_padding,
488        trailing_padding: (*par).trailing_padding,
489        seek_preroll: (*par).seek_preroll,
490        alpha_mode: read_unaligned(addr_of!((*par).alpha_mode).cast::<i32>()),
491        footprint_bytes,
492      }
493    };
494    Ok(ticket)
495  }
496
497  /// Rebuilds a live `AVCodecParameters` from the ticket.
498  ///
499  /// **The one ffmpeg-native allocation on the track row's road**, and
500  /// the handoff a decoder is opened from:
501  ///
502  /// ```ignore
503  /// FfmpegAudioStreamDecoder::open(
504  ///   track.extra().clone_parameters()?,
505  ///   track.timebase(),
506  ///   limits,
507  /// )
508  /// ```
509  ///
510  /// Every seat that can hold a non-default value is written, so the
511  /// result depends on the ticket rather than on what
512  /// `avcodec_parameters_alloc` happened to leave behind. Stated
513  /// exactly, because the difference is load-bearing:
514  ///
515  /// * The **twenty-seven scalars** are written unconditionally. Those
516  ///   are the seats `avcodec_parameters_alloc` gives non-zero defaults
517  ///   to — `format` is `-1`, `profile` and `level` are
518  ///   `AV_PROFILE_UNKNOWN` / `AV_LEVEL_UNKNOWN`, both rationals are
519  ///   `0/1`, and so on — so leaving any of them would let a default
520  ///   masquerade as the file's own value.
521  /// * The **four descriptor seats** — `extradata` and
522  ///   `extradata_size`, `coded_side_data` and `nb_coded_side_data` —
523  ///   are written only when the ticket has something to put there. On
524  ///   the empty path they keep the allocator's zero, and that is
525  ///   correct rather than an omission: `codec_parameters_reset`
526  ///   `memset`s the whole struct to zero and then assigns non-zero
527  ///   defaults to a named list that contains none of these four. A
528  ///   null pointer with a zero length is exactly what "no extradata"
529  ///   and "no side data" mean, and it is what the source had.
530  /// * `ch_layout` is always written — order, channel count, and either
531  ///   the mask or the map.
532  ///
533  /// That is what makes field-by-field parity with the original
534  /// provable rather than hopeful, and
535  /// `tests/codec_ticket_parity.rs::every_scalar_seat_is_written_back`
536  /// is the assertion that a seat quietly relying on a default cannot
537  /// pass.
538  ///
539  /// Fallible because allocation is: `ParametersAlloc` when the struct
540  /// itself cannot be allocated, `ParametersCopy` carrying `ENOMEM`
541  /// when one of the heap seats cannot. Nothing here consults a
542  /// budget — the bytes are already resident and were admitted at
543  /// [`Self::mirror`]; what this allocates is exactly
544  /// [`Self::footprint_bytes`].
545  pub fn rebuild(&self) -> Result<Parameters, DemuxError> {
546    let stream_index = self.stream_index;
547    let mut out = Parameters::new();
548    // SAFETY: reading the pointer the constructor stored without
549    // dereferencing it — `Parameters::new` does not check
550    // `avcodec_parameters_alloc` and hands back a null on failure.
551    let dst = unsafe { out.as_mut_ptr() };
552    if dst.is_null() {
553      return Err(DemuxError::ParametersAlloc(ParametersAlloc::new(
554        stream_index,
555      )));
556    }
557
558    // SAFETY: `dst` is a live, freshly allocated `AVCodecParameters`
559    // whose heap seats are still null. Every write below is a scalar
560    // store, or an `addr_of_mut!` store of the same 32-bit pattern the
561    // mirror read, into a struct nothing else holds a reference to.
562    unsafe {
563      write_unaligned(
564        addr_of_mut!((*dst).codec_type).cast::<i32>(),
565        self.codec_type,
566      );
567      write_unaligned(addr_of_mut!((*dst).codec_id).cast::<i32>(), self.codec_id);
568      (*dst).codec_tag = self.codec_tag;
569      (*dst).format = self.format;
570      (*dst).bit_rate = self.bit_rate;
571      (*dst).bits_per_coded_sample = self.bits_per_coded_sample;
572      (*dst).bits_per_raw_sample = self.bits_per_raw_sample;
573      (*dst).profile = self.profile;
574      (*dst).level = self.level;
575      (*dst).width = self.width;
576      (*dst).height = self.height;
577      (*dst).sample_aspect_ratio.num = self.sample_aspect_ratio.num();
578      (*dst).sample_aspect_ratio.den = self.sample_aspect_ratio.den();
579      (*dst).framerate.num = self.framerate.num();
580      (*dst).framerate.den = self.framerate.den();
581      write_unaligned(
582        addr_of_mut!((*dst).field_order).cast::<i32>(),
583        self.field_order,
584      );
585      write_unaligned(
586        addr_of_mut!((*dst).color_range).cast::<i32>(),
587        self.color_range,
588      );
589      write_unaligned(
590        addr_of_mut!((*dst).color_primaries).cast::<i32>(),
591        self.color_primaries,
592      );
593      write_unaligned(addr_of_mut!((*dst).color_trc).cast::<i32>(), self.color_trc);
594      write_unaligned(
595        addr_of_mut!((*dst).color_space).cast::<i32>(),
596        self.color_space,
597      );
598      write_unaligned(
599        addr_of_mut!((*dst).chroma_location).cast::<i32>(),
600        self.chroma_location,
601      );
602      (*dst).video_delay = self.video_delay;
603      (*dst).sample_rate = self.sample_rate;
604      (*dst).block_align = self.block_align;
605      (*dst).frame_size = self.frame_size;
606      (*dst).initial_padding = self.initial_padding;
607      (*dst).trailing_padding = self.trailing_padding;
608      (*dst).seek_preroll = self.seek_preroll;
609      write_unaligned(
610        addr_of_mut!((*dst).alpha_mode).cast::<i32>(),
611        self.alpha_mode,
612      );
613    }
614
615    // The three heap seats, each allocated from FFmpeg's allocator so
616    // `avcodec_parameters_free` releases them with the struct — the
617    // same allocator discipline `bounded_clone_parameters` uses, and
618    // the same one `avcodec_parameters_copy` would have used.
619    //
620    // SAFETY: `dst` is live and its heap seats are null; each
621    // allocation below is checked, and each is attached to `dst`
622    // before the next one is attempted, so a failure part way leaves
623    // `out`'s own destructor a well-formed struct to free.
624    unsafe {
625      write_extradata(dst, self.extradata.as_slice(), stream_index)?;
626      write_side_data(dst, &self.coded_side_data, stream_index)?;
627      write_channel_layout(dst, &self.ch_layout, stream_index)?;
628    }
629
630    Ok(out)
631  }
632
633  /// What [`Self::rebuild`] asks FFmpeg's allocator for: `extradata`
634  /// with the `AV_INPUT_BUFFER_PADDING_SIZE` decoders read past the
635  /// end into, the `coded_side_data` descriptor array and every
636  /// entry's payload, and a custom channel map.
637  ///
638  /// The number the session admitted this stream at, and the number
639  /// `DemuxLimits::max_codec_parameter_bytes` was judged against — so
640  /// a row that opened is a row whose every rebuild fits the ceiling
641  /// it opened under.
642  ///
643  /// Not the ticket's own residency: the owned mirror holds the
644  /// payload without FFmpeg's trailing padding, and shares its buffers
645  /// by refcount.
646  #[cfg_attr(not(tarpaulin), inline(always))]
647  pub const fn footprint_bytes(&self) -> usize {
648    self.footprint_bytes
649  }
650
651  /// The `AVStream.index` this mirror was taken at — what
652  /// [`Self::rebuild`]'s errors name.
653  #[cfg_attr(not(tarpaulin), inline(always))]
654  pub const fn stream_index(&self) -> usize {
655    self.stream_index
656  }
657  /// The raw `AVMediaType`.
658  #[cfg_attr(not(tarpaulin), inline(always))]
659  pub const fn codec_type(&self) -> i32 {
660    self.codec_type
661  }
662  /// The raw `AVCodecID`.
663  #[cfg_attr(not(tarpaulin), inline(always))]
664  pub const fn codec_id(&self) -> i32 {
665    self.codec_id
666  }
667  /// The codec tag — the AVI FOURCC, when the container carries one.
668  #[cfg_attr(not(tarpaulin), inline(always))]
669  pub const fn codec_tag(&self) -> u32 {
670    self.codec_tag
671  }
672  /// The decoder-initialisation bytes — SPS/PPS for H.264, the
673  /// `AudioSpecificConfig` for AAC, a font's payload for an
674  /// attachment. Empty when the stream carries none, or when the row
675  /// was built on the attachment road that leaves them to the carrier.
676  #[cfg_attr(not(tarpaulin), inline(always))]
677  pub fn extradata(&self) -> &[u8] {
678    self.extradata.as_slice()
679  }
680  /// The extradata's carrier, for a consumer that wants the bytes
681  /// without copying them again.
682  #[cfg_attr(not(tarpaulin), inline(always))]
683  pub const fn extradata_ref(&self) -> &FfmpegBytes {
684    &self.extradata
685  }
686  /// Stream-level side data — where a MOV `prof` atom's ICC profile
687  /// arrives, among others.
688  #[cfg_attr(not(tarpaulin), inline(always))]
689  pub fn coded_side_data(&self) -> &[SideDataEntry] {
690    self.coded_side_data.as_slice()
691  }
692  /// The Dolby Vision configuration record — profile number and base-
693  /// layer compatibility id — from the container's `dvcC` / `dvvC` /
694  /// `dwvC` box, when the stream carries one.
695  ///
696  /// `None` when [`Self::coded_side_data`] holds no
697  /// `AV_PKT_DATA_DOVI_CONF` entry (an ordinary, non-Dolby-Vision
698  /// stream — the overwhelming majority) or the entry's payload is too
699  /// short to hold both seats. Absent configuration answers absent,
700  /// same as every other seat this crate exposes as an `Option`.
701  ///
702  /// This is the **configuration-record** half of Dolby Vision — the
703  /// two numbers a consumer routes base-layer-vs-refuse on before a
704  /// single frame decodes. The **per-frame** half — the RPU buffer
705  /// (`AV_FRAME_DATA_DOVI_RPU_BUFFER`) and parsed dynamic metadata
706  /// (`AV_FRAME_DATA_DOVI_METADATA`), plus HDR10+ dynamic metadata
707  /// (`AV_FRAME_DATA_DYNAMIC_HDR_PLUS`) — is not exposed by this crate
708  /// yet: [mediadecode#54](https://github.com/findit-studio/mediadecode/issues/54).
709  #[cfg_attr(not(tarpaulin), inline(always))]
710  pub fn dolby_vision_config(&self) -> Option<DolbyVisionConfig> {
711    let kind = AVPacketSideDataType::AV_PKT_DATA_DOVI_CONF as i32;
712    self
713      .coded_side_data
714      .iter()
715      .find(|entry| entry.kind() == kind)
716      .and_then(|entry| parse_dolby_vision_config(entry.data()))
717  }
718  /// The pixel format (video) or sample format (audio), as the raw
719  /// integer both enums share this seat as.
720  #[cfg_attr(not(tarpaulin), inline(always))]
721  pub const fn format(&self) -> i32 {
722    self.format
723  }
724  /// Average bitrate in bits per second.
725  #[cfg_attr(not(tarpaulin), inline(always))]
726  pub const fn bit_rate(&self) -> i64 {
727    self.bit_rate
728  }
729  /// Bits per sample in the coded bitstream.
730  #[cfg_attr(not(tarpaulin), inline(always))]
731  pub const fn bits_per_coded_sample(&self) -> i32 {
732    self.bits_per_coded_sample
733  }
734  /// Valid bits in each output sample.
735  #[cfg_attr(not(tarpaulin), inline(always))]
736  pub const fn bits_per_raw_sample(&self) -> i32 {
737    self.bits_per_raw_sample
738  }
739  /// The codec profile.
740  #[cfg_attr(not(tarpaulin), inline(always))]
741  pub const fn profile(&self) -> i32 {
742    self.profile
743  }
744  /// The codec level.
745  #[cfg_attr(not(tarpaulin), inline(always))]
746  pub const fn level(&self) -> i32 {
747    self.level
748  }
749  /// Frame width in pixels — video, and the subtitle canvas.
750  #[cfg_attr(not(tarpaulin), inline(always))]
751  pub const fn width(&self) -> i32 {
752    self.width
753  }
754  /// Frame height in pixels — video, and the subtitle canvas.
755  #[cfg_attr(not(tarpaulin), inline(always))]
756  pub const fn height(&self) -> i32 {
757    self.height
758  }
759  /// The sample aspect ratio. A zero numerator means unknown.
760  #[cfg_attr(not(tarpaulin), inline(always))]
761  pub const fn sample_aspect_ratio(&self) -> Ratio {
762    self.sample_aspect_ratio
763  }
764  /// The codec-level frame rate. `0/1` when frames differ in duration
765  /// or the value is not known.
766  #[cfg_attr(not(tarpaulin), inline(always))]
767  pub const fn framerate(&self) -> Ratio {
768    self.framerate
769  }
770  /// The raw `AVFieldOrder`.
771  #[cfg_attr(not(tarpaulin), inline(always))]
772  pub const fn field_order(&self) -> i32 {
773    self.field_order
774  }
775  /// The raw `AVColorRange`.
776  #[cfg_attr(not(tarpaulin), inline(always))]
777  pub const fn color_range(&self) -> i32 {
778    self.color_range
779  }
780  /// The raw `AVColorPrimaries`.
781  #[cfg_attr(not(tarpaulin), inline(always))]
782  pub const fn color_primaries(&self) -> i32 {
783    self.color_primaries
784  }
785  /// The raw `AVColorTransferCharacteristic`.
786  #[cfg_attr(not(tarpaulin), inline(always))]
787  pub const fn color_trc(&self) -> i32 {
788    self.color_trc
789  }
790  /// The raw `AVColorSpace`.
791  #[cfg_attr(not(tarpaulin), inline(always))]
792  pub const fn color_space(&self) -> i32 {
793    self.color_space
794  }
795  /// The raw `AVChromaLocation`.
796  #[cfg_attr(not(tarpaulin), inline(always))]
797  pub const fn chroma_location(&self) -> i32 {
798    self.chroma_location
799  }
800  /// Number of delayed frames — the decoder's `has_b_frames`.
801  #[cfg_attr(not(tarpaulin), inline(always))]
802  pub const fn video_delay(&self) -> i32 {
803    self.video_delay
804  }
805  /// The channel layout.
806  #[cfg_attr(not(tarpaulin), inline(always))]
807  pub const fn ch_layout(&self) -> &ChannelLayoutTicket {
808    &self.ch_layout
809  }
810  /// Audio samples per second.
811  #[cfg_attr(not(tarpaulin), inline(always))]
812  pub const fn sample_rate(&self) -> i32 {
813    self.sample_rate
814  }
815  /// Bytes per coded audio frame — `nBlockAlign` in `WAVEFORMATEX`.
816  #[cfg_attr(not(tarpaulin), inline(always))]
817  pub const fn block_align(&self) -> i32 {
818    self.block_align
819  }
820  /// Audio frame size, when the format fixes one.
821  #[cfg_attr(not(tarpaulin), inline(always))]
822  pub const fn frame_size(&self) -> i32 {
823    self.frame_size
824  }
825  /// Leading padding samples the encoder inserted.
826  #[cfg_attr(not(tarpaulin), inline(always))]
827  pub const fn initial_padding(&self) -> i32 {
828    self.initial_padding
829  }
830  /// Trailing padding samples the encoder appended.
831  #[cfg_attr(not(tarpaulin), inline(always))]
832  pub const fn trailing_padding(&self) -> i32 {
833    self.trailing_padding
834  }
835  /// Samples to skip after a discontinuity.
836  #[cfg_attr(not(tarpaulin), inline(always))]
837  pub const fn seek_preroll(&self) -> i32 {
838    self.seek_preroll
839  }
840  /// The raw `AVAlphaMode` — how an alpha channel relates to the
841  /// colour values, and the last field `AVCodecParameters` declares.
842  ///
843  /// New in FFmpeg n9.0, and the seat this mirror's first draft
844  /// dropped: video-only, left at its zero by every fixture the corpus
845  /// can mint, and therefore reading back identically whether it is
846  /// mirrored or forgotten. The parity comparator names every field
847  /// for exactly that reason.
848  #[cfg_attr(not(tarpaulin), inline(always))]
849  pub const fn alpha_mode(&self) -> i32 {
850    self.alpha_mode
851  }
852}
853
854impl core::fmt::Debug for CodecTicket {
855  /// Sizes rather than payloads. An `extradata` blob and an ICC
856  /// profile are both megabyte-scale and neither is readable; what a
857  /// reader of a log wants is the stream's identity and whether the
858  /// heap seats are populated.
859  fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
860    f.debug_struct("CodecTicket")
861      .field(
862        "medium",
863        &crate::boundary::media_kind_from_raw(self.codec_type),
864      )
865      .field("codec_id", &self.codec_id)
866      .field("codec_tag", &format_args!("{:#010x}", self.codec_tag))
867      .field("format", &self.format)
868      .field("width", &self.width)
869      .field("height", &self.height)
870      .field("sample_rate", &self.sample_rate)
871      .field("channels", &self.ch_layout.channels)
872      .field("extradata_len", &self.extradata.len())
873      .field("coded_side_data", &self.coded_side_data.len())
874      .field("footprint_bytes", &self.footprint_bytes)
875      .finish_non_exhaustive()
876  }
877}
878
879// ---------------------------------------------------------------------------
880//  Reading the three heap seats.
881// ---------------------------------------------------------------------------
882
883/// Copies `extradata` into an owned carrier — the payload only.
884///
885/// FFmpeg's `AV_INPUT_BUFFER_PADDING_SIZE` trailing zeroes are an
886/// allocation contract, not content: decoders read past the end of the
887/// buffer and the padding is what makes that defined. Carrying them in
888/// the mirror would store zeroes an owned `Arc<[u8]>` needs no reader
889/// to be safe past; [`write_extradata`] mints them again on the way
890/// back out, which is where they mean something.
891///
892/// # Safety
893///
894/// `par` must be a live `*const AVCodecParameters`.
895unsafe fn extradata_of(
896  par: *const AVCodecParameters,
897  policy: ExtradataPolicy,
898  stream_index: usize,
899) -> Result<FfmpegBytes, DemuxError> {
900  if matches!(policy, ExtradataPolicy::Omit) {
901    return Ok(FfmpegBytes::empty());
902  }
903  // SAFETY: `par` is live per the contract; both fields are a pointer
904  // and an integer.
905  let (ptr, size) = unsafe { ((*par).extradata, (*par).extradata_size) };
906  let Ok(len) = usize::try_from(size) else {
907    return Ok(FfmpegBytes::empty());
908  };
909  if ptr.is_null() || len == 0 {
910    return Ok(FfmpegBytes::empty());
911  }
912  // SAFETY: libavformat guarantees `extradata` is readable for
913  // `extradata_size` bytes while the parameters live, and the slice is
914  // consumed before this function returns.
915  // **Fallibly**, because the size is the container's: the footprint
916  // above admitted it against the caller's ceiling, and an admitted
917  // size that then aborts the process is a budget that did not do its
918  // job.
919  FfmpegBytes::try_copy_from_slice(unsafe { core::slice::from_raw_parts(ptr, len) })
920    .ok_or_else(|| DemuxError::ParametersAlloc(ParametersAlloc::new(stream_index)))
921}
922
923/// Copies every `coded_side_data` entry into owned entries.
924///
925/// # Safety
926///
927/// `par` must be a live `*const AVCodecParameters`.
928unsafe fn side_data_of(
929  par: *const AVCodecParameters,
930  stream_index: usize,
931) -> Result<Vec<SideDataEntry>, DemuxError> {
932  // SAFETY: `par` is live per the contract.
933  let (array, count) = unsafe { ((*par).coded_side_data, (*par).nb_coded_side_data) };
934  let alloc = || DemuxError::ParametersAlloc(ParametersAlloc::new(stream_index));
935  let Ok(count) = usize::try_from(count) else {
936    return Ok(Vec::new());
937  };
938  if array.is_null() || count == 0 {
939    return Ok(Vec::new());
940  }
941  // The descriptor table, reserved fallibly: `nb_coded_side_data` is
942  // the container's number too.
943  let mut entries = Vec::new();
944  entries.try_reserve_exact(count).map_err(|_| alloc())?;
945  for index in 0..count {
946    // **Never `&*entry`.** `AVPacketSideData::type` is an open C enum
947    // and an ABI-compatible FFmpeg newer than these bindings emits
948    // kinds absent from the generated Rust enum; forming a typed
949    // reference asserts every field inhabits its declared type, which
950    // is undefined behaviour before a single field is read.
951    //
952    // SAFETY: the array is valid for `nb_coded_side_data` contiguous
953    // entries per FFmpeg's contract, `index` is below that count, and
954    // `addr_of!` computes a field address without forming a reference
955    // to the struct containing it.
956    let (kind, data, size) = unsafe {
957      let entry = array.add(index);
958      (
959        read_unaligned(addr_of!((*entry).type_).cast::<i32>()),
960        read_unaligned(addr_of!((*entry).data)),
961        read_unaligned(addr_of!((*entry).size)),
962      )
963    };
964    let payload = if data.is_null() || size == 0 {
965      FfmpegBytes::empty()
966    } else {
967      // SAFETY: the descriptor declares `size` readable bytes at
968      // `data`, and the slice is consumed before the loop advances.
969      FfmpegBytes::try_copy_from_slice(unsafe { core::slice::from_raw_parts(data, size) })
970        .ok_or_else(alloc)?
971    };
972    entries.push(SideDataEntry::new(kind, payload));
973  }
974  Ok(entries)
975}
976
977/// **Every refusal a channel layout can earn from the container's own
978/// declared facts — and not one byte allocated deciding them.**
979///
980/// Split out of [`channel_layout_of`] so the judging can happen where
981/// the paying has not: `admit_streams` calls this for every stream
982/// before the track table reserves a row, so a malformed layout on the
983/// *last* stream refuses the open before the *first* stream's ticket is
984/// copied. `channel_layout_of` calls it again as its own first
985/// statement, which is what lets the build below carry no refusals at
986/// all and makes the two passes one rule rather than two.
987///
988/// Judged here, in this order:
989///
990/// - a non-null `ch_layout.opaque`, which this crate has no way to
991///   mirror and will not silently drop;
992/// - every structural fault of a `CUSTOM` map —a negative or zero
993///   channel count, a null `u.map`, or a sixteen-byte name with no NUL
994///   inside it — through
995///   [`custom_map_fault`](crate::channel_layout::custom_map_fault),
996///   which is the single place that rule is written and which the
997///   describe road applies too. `av_channel_layout_copy` (which
998///   `avcodec_parameters_to_context` moves this field through)
999///   allocates `nb_channels` entries and `memcpy`s from `src->u.map`
1000///   with no null check of its own, and `av_channel_layout_describe`
1001///   hands each fixed `name` array to a `%s` conversion; carrying
1002///   either across would be a faithful round trip of a crash;
1003/// - a non-null `opaque` on any entry of that map, which only this road
1004///   cares about because only this road mirrors it.
1005///
1006/// Refusing the incomplete map here is also what lets
1007/// [`write_channel_layout`] rely on `map.len() == nb_channels` for a
1008/// custom order.
1009///
1010/// # Safety
1011///
1012/// `par` must be a live `*const AVCodecParameters`, and for a `CUSTOM`
1013/// order with a non-null `u.map` that map must hold `nb_channels` live
1014/// `AVChannelCustom` entries — FFmpeg's own contract for a layout it
1015/// filled, which is the only kind this crate hands in.
1016pub(crate) unsafe fn validate_channel_layout(
1017  par: *const AVCodecParameters,
1018  stream_index: usize,
1019) -> Result<(), DemuxError> {
1020  // SAFETY: `ch_layout` is embedded by value; `addr_of!` reaches each
1021  // field without forming a reference to the layout, and `order` has
1022  // the layout of a `c_int`.
1023  let (order, channels, opaque) = unsafe {
1024    (
1025      read_unaligned(addr_of!((*par).ch_layout.order).cast::<i32>()),
1026      (*par).ch_layout.nb_channels,
1027      (*par).ch_layout.opaque,
1028    )
1029  };
1030  if !opaque.is_null() {
1031    return Err(DemuxError::ParametersOpaque(ParametersOpaque::new(
1032      stream_index,
1033      None,
1034    )));
1035  }
1036  // **The layout's whole structure, through the one function that
1037  // decides it — every order, not only `CUSTOM`.**
1038  //
1039  // The custom map's rules (null map, non-positive count, a name with
1040  // no NUL) and the other orders' (a count outside FFmpeg's arithmetic
1041  // range, a `NATIVE` mask that disagrees with its count, an
1042  // `AMBISONIC` layout whose channels form no ambisonic order) are one
1043  // preflight, and the describe road, the decoder and the resampler
1044  // apply exactly this one — so the pass that admits and the road that
1045  // materialises cannot come to different answers about a layout.
1046  //
1047  // An earlier round stopped here for every order but `CUSTOM`, on the
1048  // argument that a `uint64_t` mask cannot be malformed. The mask is
1049  // not the only field.
1050  //
1051  // SAFETY: this function's own contract, forwarded: `ch_layout` is
1052  // embedded in a live `AVCodecParameters`, `order` was read as the
1053  // `c_int` it is, and for a custom order its map holds `nb_channels`
1054  // entries.
1055  unsafe { crate::channel_layout::layout_preflight(addr_of!((*par).ch_layout)) }
1056    .map_err(|fault| crate::demuxer::layout_fault_to_demux(stream_index, fault))?;
1057
1058  if order != AVChannelOrder::AV_CHANNEL_ORDER_CUSTOM as i32 {
1059    return Ok(());
1060  }
1061  // The map is sound to walk now; what remains is the one thing the
1062  // describe road has no opinion about, because it never mirrors it.
1063  //
1064  // SAFETY: the order names the `map` arm, and the preflight above
1065  // proved it non-null with a positive count.
1066  let map = unsafe { (*par).ch_layout.u.map };
1067  let count = channels.max(0) as usize;
1068  for index in 0..count {
1069    // Field pointers, never `&AVChannelCustom`: `id` is an open enum
1070    // with the same hazard as a side-data type id.
1071    //
1072    // SAFETY: the contract above makes the map `count` entries long,
1073    // `index` is below that count, and the read goes through
1074    // `addr_of!`.
1075    let opaque = unsafe { read_unaligned(addr_of!((*map.add(index)).opaque)) };
1076    if !opaque.is_null() {
1077      return Err(DemuxError::ParametersOpaque(ParametersOpaque::new(
1078        stream_index,
1079        Some(index),
1080      )));
1081    }
1082  }
1083  Ok(())
1084}
1085
1086/// Mirrors the embedded `AVChannelLayout`.
1087///
1088/// Carries no refusals of its own beyond an allocator's: everything
1089/// this layout can be refused for is decided by
1090/// [`validate_channel_layout`], which runs first here and has already
1091/// run during admission.
1092///
1093/// # Safety
1094///
1095/// `par` must be a live `*const AVCodecParameters`, with
1096/// [`validate_channel_layout`]'s contract for a custom map.
1097unsafe fn channel_layout_of(
1098  par: *const AVCodecParameters,
1099  stream_index: usize,
1100) -> Result<ChannelLayoutTicket, DemuxError> {
1101  // SAFETY: the caller's contract, forwarded unchanged.
1102  unsafe { validate_channel_layout(par, stream_index) }?;
1103
1104  // SAFETY: as in the validation above — field reads through
1105  // `addr_of!`, with `order` read as the `c_int` it is.
1106  let (order, channels) = unsafe {
1107    (
1108      read_unaligned(addr_of!((*par).ch_layout.order).cast::<i32>()),
1109      (*par).ch_layout.nb_channels,
1110    )
1111  };
1112
1113  let mut layout = ChannelLayoutTicket {
1114    order,
1115    channels,
1116    mask: 0,
1117    map: Vec::new(),
1118  };
1119  match crate::channel_layout::LayoutArm::of(order) {
1120    // SAFETY: `NATIVE` and `AMBISONIC` are the two orders whose
1121    // contract defines `u.mask`, and this arm names exactly them.
1122    crate::channel_layout::LayoutArm::Mask => {
1123      layout.mask = unsafe { (*par).ch_layout.u.mask };
1124      return Ok(layout);
1125    }
1126    // **`UNSPEC`, and any order this build has never heard of: the
1127    // union is not read at all.** FFmpeg's own header declares it
1128    // undefined for an unspecified layout, and this mirror used to
1129    // treat "not custom" as "mask-backed" — importing whatever bytes
1130    // happened to sit there into a field that `mask()` hands out and
1131    // that the derived `Debug`, `PartialEq` and `Hash` then expose and
1132    // compare. The stored zero is a value this crate chose, not one it
1133    // read.
1134    crate::channel_layout::LayoutArm::Undefined => return Ok(layout),
1135    crate::channel_layout::LayoutArm::Map => {}
1136  }
1137
1138  // SAFETY: the order names the `map` arm, and the validation above
1139  // proved it non-null with a positive count.
1140  let map = unsafe { (*par).ch_layout.u.map };
1141  let count = channels.max(0) as usize;
1142  layout
1143    .map
1144    .try_reserve_exact(count)
1145    .map_err(|_| DemuxError::ParametersAlloc(ParametersAlloc::new(stream_index)))?;
1146  for index in 0..count {
1147    // SAFETY: FFmpeg's contract makes the map `count` entries long,
1148    // `index` is below that count, and every read goes through
1149    // `addr_of!`.
1150    let (id, name) = unsafe {
1151      let entry = map.add(index);
1152      (
1153        read_unaligned(addr_of!((*entry).id).cast::<i32>()),
1154        read_unaligned(addr_of!((*entry).name).cast::<[u8; 16]>()),
1155      )
1156    };
1157    layout.map.push(CustomChannel::new(id, name));
1158  }
1159  Ok(layout)
1160}
1161
1162// ---------------------------------------------------------------------------
1163//  Writing the three heap seats.
1164// ---------------------------------------------------------------------------
1165
1166/// The `ENOMEM` a heap seat's allocation reports when it fails.
1167fn seat_alloc_failed(stream_index: usize) -> DemuxError {
1168  DemuxError::ParametersCopy(ParametersCopy::new(
1169    stream_index,
1170    ffmpeg_next::Error::Other {
1171      errno: libc::ENOMEM,
1172    },
1173  ))
1174}
1175
1176/// Allocates `extradata` and its padding, and copies the payload in.
1177///
1178/// # Safety
1179///
1180/// `dst` must be a live `*mut AVCodecParameters` whose `extradata` is
1181/// null.
1182unsafe fn write_extradata(
1183  dst: *mut AVCodecParameters,
1184  payload: &[u8],
1185  stream_index: usize,
1186) -> Result<(), DemuxError> {
1187  if payload.is_empty() {
1188    return Ok(());
1189  }
1190  let size = i32::try_from(payload.len()).map_err(|_| seat_alloc_failed(stream_index))?;
1191  let padded = payload
1192    .len()
1193    .checked_add(AV_INPUT_BUFFER_PADDING_SIZE as usize)
1194    .ok_or_else(|| seat_alloc_failed(stream_index))?;
1195  // SAFETY: `av_mallocz` returns zeroed memory or null; the copy
1196  // writes exactly `payload.len()` bytes into an allocation that is
1197  // `AV_INPUT_BUFFER_PADDING_SIZE` longer, leaving the padding zero —
1198  // which is the contract decoders read past the end under.
1199  unsafe {
1200    let buffer = av_mallocz(padded).cast::<u8>();
1201    if buffer.is_null() {
1202      return Err(seat_alloc_failed(stream_index));
1203    }
1204    copy_nonoverlapping(payload.as_ptr(), buffer, payload.len());
1205    (*dst).extradata = buffer;
1206    (*dst).extradata_size = size;
1207  }
1208  Ok(())
1209}
1210
1211/// Allocates the `coded_side_data` descriptor array and each payload.
1212///
1213/// # Safety
1214///
1215/// `dst` must be a live `*mut AVCodecParameters` whose
1216/// `coded_side_data` is null.
1217unsafe fn write_side_data(
1218  dst: *mut AVCodecParameters,
1219  entries: &[SideDataEntry],
1220  stream_index: usize,
1221) -> Result<(), DemuxError> {
1222  if entries.is_empty() {
1223    return Ok(());
1224  }
1225  let count = i32::try_from(entries.len()).map_err(|_| seat_alloc_failed(stream_index))?;
1226  let bytes = entries
1227    .len()
1228    .checked_mul(core::mem::size_of::<AVPacketSideData>())
1229    .ok_or_else(|| seat_alloc_failed(stream_index))?;
1230
1231  // SAFETY: `dst` is live with a null `coded_side_data`. The array is
1232  // attached before any payload is filled in, so a failure part way
1233  // leaves the destructor a well-formed array to walk: the entries it
1234  // has not reached are zeroed, and freeing a null payload is a no-op.
1235  unsafe {
1236    let array = av_mallocz(bytes).cast::<AVPacketSideData>();
1237    if array.is_null() {
1238      return Err(seat_alloc_failed(stream_index));
1239    }
1240    (*dst).coded_side_data = array;
1241    (*dst).nb_coded_side_data = count;
1242
1243    for (index, entry) in entries.iter().enumerate() {
1244      let into = array.add(index);
1245      // The type id travels as the raw bits it is on the wire, for the
1246      // reason the read did: a kind these bindings cannot name is
1247      // still a kind the file carries and a decoder may want.
1248      write_unaligned(addr_of_mut!((*into).type_).cast::<i32>(), entry.kind());
1249      let payload = entry.data();
1250      if payload.is_empty() {
1251        continue;
1252      }
1253      let buffer = av_mallocz(payload.len()).cast::<u8>();
1254      if buffer.is_null() {
1255        return Err(seat_alloc_failed(stream_index));
1256      }
1257      copy_nonoverlapping(payload.as_ptr(), buffer, payload.len());
1258      write_unaligned(addr_of_mut!((*into).data), buffer);
1259      write_unaligned(addr_of_mut!((*into).size), payload.len());
1260    }
1261  }
1262  Ok(())
1263}
1264
1265/// Writes the channel layout, allocating a custom map when the order
1266/// names one.
1267///
1268/// Written field by field rather than through
1269/// `av_channel_layout_copy`, because the source it would copy from is
1270/// the thing this road has abolished: there is no live
1271/// `AVChannelLayout` to copy, only owned Rust. The one allocation is
1272/// the custom map, which [`CodecTicket::mirror`] already measured and
1273/// admitted.
1274///
1275/// # Safety
1276///
1277/// `dst` must be a live `*mut AVCodecParameters` whose `ch_layout` is
1278/// the zeroed (`AV_CHANNEL_ORDER_UNSPEC`) state
1279/// `avcodec_parameters_alloc` leaves, owning no map.
1280unsafe fn write_channel_layout(
1281  dst: *mut AVCodecParameters,
1282  layout: &ChannelLayoutTicket,
1283  stream_index: usize,
1284) -> Result<(), DemuxError> {
1285  // SAFETY: `dst` is live and its layout owns nothing yet; `order` is
1286  // written as the same 32-bit pattern the mirror read.
1287  unsafe {
1288    write_unaligned(
1289      addr_of_mut!((*dst).ch_layout.order).cast::<i32>(),
1290      layout.order(),
1291    );
1292    (*dst).ch_layout.opaque = core::ptr::null_mut();
1293  }
1294
1295  match crate::channel_layout::LayoutArm::of(layout.order()) {
1296    // SAFETY: the order names the `mask` arm.
1297    crate::channel_layout::LayoutArm::Mask => {
1298      unsafe {
1299        (*dst).ch_layout.nb_channels = layout.channels();
1300        (*dst).ch_layout.u.mask = layout.mask();
1301      }
1302      return Ok(());
1303    }
1304    // **The union is left as `avcodec_parameters_alloc` zeroed it.**
1305    // An order that defines no arm gets no arm written: handing
1306    // libavcodec a mask for an unspecified layout would be inventing a
1307    // fact about a field FFmpeg says means nothing there, and the
1308    // mirror does not read one either.
1309    crate::channel_layout::LayoutArm::Undefined => {
1310      // SAFETY: `dst` is live and the count is a plain `int` field.
1311      unsafe {
1312        (*dst).ch_layout.nb_channels = layout.channels();
1313      }
1314      return Ok(());
1315    }
1316    crate::channel_layout::LayoutArm::Map => {}
1317  }
1318
1319  // **`nb_channels` comes from the map, not from the stored field.**
1320  // The two are equal — [`channel_layout_of`] refuses a custom layout
1321  // it cannot map in full, and the fields are private, so no other
1322  // value can exist. Writing the count from the array anyway is what
1323  // makes that structural rather than remembered: the layout handed to
1324  // libavcodec can never declare more channels than the array it points
1325  // at, which is precisely the shape `av_channel_layout_copy` would
1326  // `memcpy` past the end of.
1327  debug_assert_eq!(
1328    layout.map().len(),
1329    layout.channels().max(0) as usize,
1330    "a custom layout's map length is its channel count",
1331  );
1332  let count = layout.map().len();
1333  if count == 0 {
1334    // Unreachable through `mirror`, and fail-closed if a future
1335    // constructor ever makes it reachable.
1336    return Err(DemuxError::ParametersChannelMap(ParametersChannelMap::new(
1337      stream_index,
1338      layout.channels(),
1339    )));
1340  }
1341
1342  let declared = i32::try_from(count).map_err(|_| seat_alloc_failed(stream_index))?;
1343  let bytes = count
1344    .checked_mul(core::mem::size_of::<AVChannelCustom>())
1345    .ok_or_else(|| seat_alloc_failed(stream_index))?;
1346  // SAFETY: `av_mallocz` returns zeroed memory or null. The map and the
1347  // count it describes are attached together, before the entries are
1348  // filled in, so a later failure leaves a well-formed (zeroed) map for
1349  // the destructor to free, and every write goes through
1350  // `addr_of_mut!` rather than a typed reference.
1351  unsafe {
1352    let map = av_mallocz(bytes).cast::<AVChannelCustom>();
1353    if map.is_null() {
1354      return Err(seat_alloc_failed(stream_index));
1355    }
1356    (*dst).ch_layout.u.map = map;
1357    (*dst).ch_layout.nb_channels = declared;
1358    for (index, channel) in layout.map().iter().enumerate() {
1359      let into = map.add(index);
1360      write_unaligned(addr_of_mut!((*into).id).cast::<i32>(), channel.id());
1361      write_unaligned(
1362        addr_of_mut!((*into).name).cast::<[u8; 16]>(),
1363        *channel.name_bytes(),
1364      );
1365      write_unaligned(addr_of_mut!((*into).opaque), core::ptr::null_mut());
1366    }
1367  }
1368  Ok(())
1369}
1370
1371#[cfg(test)]
1372mod tests;