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  /// Whether this layout's union arm is the custom map.
262  #[cfg_attr(not(tarpaulin), inline(always))]
263  fn is_custom(&self) -> bool {
264    self.order == AVChannelOrder::AV_CHANNEL_ORDER_CUSTOM as i32
265  }
266}
267
268/// Every seat of one stream's `AVCodecParameters`, owned.
269///
270/// # The roster
271///
272/// All thirty-two fields FFmpeg n9.0 declares, in that struct's own
273/// order. Nothing is elided as "video only" or "audio only":
274/// `avcodec_parameters_to_context` reads a different subset per medium
275/// but the *file* carries whatever it carries, and a mirror that kept
276/// only one medium's subset would lose a seat the moment a container
277/// declared something unusual.
278///
279/// They land in three kinds. Three seats own heap and become owned
280/// Rust: `extradata`, `coded_side_data`, and `ch_layout`'s custom map.
281/// Two are lengths of those — `extradata_size` and
282/// `nb_coded_side_data` — and are **not stored**: a carrier already
283/// knows its own length, and a second copy of it is a second thing to
284/// keep in agreement. The remaining twenty-seven are scalars, held as
285/// the integers (or, for the two `AVRational` seats, the [`Ratio`])
286/// they are.
287///
288/// # `Send + Sync`, by construction
289///
290/// Every field is an integer, an [`FfmpegBytes`](crate::FfmpegBytes)
291/// (an `Arc<[u8]>`), or a `Vec` of those. There is no raw pointer, so
292/// there is no `unsafe impl` and no safety argument to get wrong —
293/// which is exactly the point of the road this type is on. See
294/// `tests::the_ticket_is_send_and_sync`.
295///
296/// # What it does not carry, and why that is a refusal rather than a
297/// loss
298///
299/// `AVChannelLayout::opaque` and `AVChannelCustom::opaque` are
300/// documented as "private data of the user": raw pointers, set by
301/// nobody but the caller who owns them, and unreadable to a mirror
302/// that must outlive the pointer's owner. libavformat never sets
303/// either, so no demuxed stream reaches this type carrying one. If one
304/// ever did, [`CodecTicket::mirror`] refuses with
305/// [`DemuxError::ParametersOpaque`] rather than dropping it in
306/// silence — the same fail-closed answer
307/// [`measure_parameters`](crate::extras) gives a channel order it has
308/// never heard of.
309#[derive(Clone)]
310pub struct CodecTicket {
311  /// The `AVStream.index` this mirror was taken at.
312  ///
313  /// Carried for one reason: so [`Self::rebuild`]'s errors can name
314  /// the stream they are about. A rebuild that reports `ENOMEM`
315  /// without saying which track it was opening is a log line nobody
316  /// can act on, and the mirror is the last place that knows.
317  stream_index: usize,
318  codec_type: i32,
319  codec_id: i32,
320  codec_tag: u32,
321  extradata: FfmpegBytes,
322  coded_side_data: Vec<SideDataEntry>,
323  format: i32,
324  bit_rate: i64,
325  bits_per_coded_sample: i32,
326  bits_per_raw_sample: i32,
327  profile: i32,
328  level: i32,
329  width: i32,
330  height: i32,
331  sample_aspect_ratio: Ratio,
332  framerate: Ratio,
333  field_order: i32,
334  color_range: i32,
335  color_primaries: i32,
336  color_trc: i32,
337  color_space: i32,
338  chroma_location: i32,
339  video_delay: i32,
340  ch_layout: ChannelLayoutTicket,
341  sample_rate: i32,
342  block_align: i32,
343  frame_size: i32,
344  initial_padding: i32,
345  trailing_padding: i32,
346  seek_preroll: i32,
347  alpha_mode: i32,
348  /// What [`Self::rebuild`] will ask FFmpeg's allocator for — see
349  /// [`Self::footprint_bytes`].
350  footprint_bytes: usize,
351}
352
353impl CodecTicket {
354  /// Mirrors a live set of codec parameters into an owned ticket.
355  ///
356  /// `budget` is the ceiling the mirror's heap seats must fit under,
357  /// measured before a byte is copied — the same admission
358  /// [`crate::extras::bounded_clone_parameters`] performs and the same
359  /// number `admit_streams` charges against the session's total. A set
360  /// of parameters over the ceiling is refused with
361  /// [`DemuxError::ParametersTooLarge`], never truncated.
362  ///
363  /// Fails with [`DemuxError::ParametersMissing`] when `source` is
364  /// null-backed — `Parameters::new()` and `Parameters::default()` are
365  /// safe constructors over an unchecked `avcodec_parameters_alloc`,
366  /// so a caller can hold one without ever having been told.
367  pub fn mirror(
368    source: &Parameters,
369    stream_index: usize,
370    budget: usize,
371  ) -> Result<Self, DemuxError> {
372    Self::mirror_with(source, stream_index, budget, ExtradataPolicy::Copy)
373  }
374
375  /// [`Self::mirror`], with the `extradata` policy named.
376  pub(crate) fn mirror_with(
377    source: &Parameters,
378    stream_index: usize,
379    budget: usize,
380    extradata_policy: ExtradataPolicy,
381  ) -> Result<Self, DemuxError> {
382    // SAFETY: reading the pointer without dereferencing it — which is
383    // what the null check exists for.
384    let par = unsafe { source.as_ptr() };
385    if par.is_null() {
386      return Err(DemuxError::ParametersMissing(ParametersMissing::new(
387        stream_index,
388      )));
389    }
390    // SAFETY: `par` is a live `AVCodecParameters` owned by `source`
391    // for the duration of this call.
392    unsafe { Self::from_raw(par, stream_index, budget, extradata_policy) }
393  }
394
395  /// [`Self::mirror`] over a raw pointer.
396  ///
397  /// # Safety
398  ///
399  /// `par` must be a non-null, live `*const AVCodecParameters` for the
400  /// duration of this call.
401  pub(crate) unsafe fn from_raw(
402    par: *const AVCodecParameters,
403    stream_index: usize,
404    budget: usize,
405    extradata_policy: ExtradataPolicy,
406  ) -> Result<Self, DemuxError> {
407    let too_large = |bytes: usize| {
408      DemuxError::ParametersTooLarge(ParametersTooLarge::new(stream_index, bytes, budget))
409    };
410
411    // Measured before a byte is copied, exactly as the bounded clone
412    // does it: the footprint enumerates the same three heap seats this
413    // mirror is about to read, and refusing here is what keeps an
414    // attacker-sized `extradata` or ICC profile from being copied into
415    // Rust memory just to be refused afterwards.
416    //
417    // SAFETY: `par` is live per this function's contract; the
418    // measurement allocates nothing and dereferences only what it
419    // counts.
420    let footprint = unsafe { measure_parameters(par) }.ok_or_else(|| too_large(usize::MAX))?;
421    let footprint_bytes = match extradata_policy {
422      ExtradataPolicy::Copy => footprint.total(),
423      ExtradataPolicy::Omit => footprint.total_without_extradata(),
424    }
425    .ok_or_else(|| too_large(usize::MAX))?;
426    if footprint_bytes > budget {
427      return Err(too_large(footprint_bytes));
428    }
429
430    // SAFETY: `par` is live; every read below either takes a scalar
431    // field by value or reaches one through `addr_of!`, and no enum
432    // field is read as anything but the `i32` pattern it is on the
433    // wire. See the module docs for why that distinction is
434    // load-bearing rather than stylistic.
435    let ticket = unsafe {
436      Self {
437        stream_index,
438        codec_type: read_unaligned(addr_of!((*par).codec_type).cast::<i32>()),
439        codec_id: read_unaligned(addr_of!((*par).codec_id).cast::<i32>()),
440        codec_tag: (*par).codec_tag,
441        extradata: extradata_of(par, extradata_policy),
442        coded_side_data: side_data_of(par),
443        format: (*par).format,
444        bit_rate: (*par).bit_rate,
445        bits_per_coded_sample: (*par).bits_per_coded_sample,
446        bits_per_raw_sample: (*par).bits_per_raw_sample,
447        profile: (*par).profile,
448        level: (*par).level,
449        width: (*par).width,
450        height: (*par).height,
451        sample_aspect_ratio: Ratio::new(
452          (*par).sample_aspect_ratio.num,
453          (*par).sample_aspect_ratio.den,
454        ),
455        framerate: Ratio::new((*par).framerate.num, (*par).framerate.den),
456        field_order: read_unaligned(addr_of!((*par).field_order).cast::<i32>()),
457        color_range: read_unaligned(addr_of!((*par).color_range).cast::<i32>()),
458        color_primaries: read_unaligned(addr_of!((*par).color_primaries).cast::<i32>()),
459        color_trc: read_unaligned(addr_of!((*par).color_trc).cast::<i32>()),
460        color_space: read_unaligned(addr_of!((*par).color_space).cast::<i32>()),
461        chroma_location: read_unaligned(addr_of!((*par).chroma_location).cast::<i32>()),
462        video_delay: (*par).video_delay,
463        ch_layout: channel_layout_of(par, stream_index)?,
464        sample_rate: (*par).sample_rate,
465        block_align: (*par).block_align,
466        frame_size: (*par).frame_size,
467        initial_padding: (*par).initial_padding,
468        trailing_padding: (*par).trailing_padding,
469        seek_preroll: (*par).seek_preroll,
470        alpha_mode: read_unaligned(addr_of!((*par).alpha_mode).cast::<i32>()),
471        footprint_bytes,
472      }
473    };
474    Ok(ticket)
475  }
476
477  /// Rebuilds a live `AVCodecParameters` from the ticket.
478  ///
479  /// **The one ffmpeg-native allocation on the track row's road**, and
480  /// the handoff a decoder is opened from:
481  ///
482  /// ```ignore
483  /// FfmpegAudioStreamDecoder::open(
484  ///   track.extra().clone_parameters()?,
485  ///   track.timebase(),
486  ///   limits,
487  /// )
488  /// ```
489  ///
490  /// Every seat that can hold a non-default value is written, so the
491  /// result depends on the ticket rather than on what
492  /// `avcodec_parameters_alloc` happened to leave behind. Stated
493  /// exactly, because the difference is load-bearing:
494  ///
495  /// * The **twenty-seven scalars** are written unconditionally. Those
496  ///   are the seats `avcodec_parameters_alloc` gives non-zero defaults
497  ///   to — `format` is `-1`, `profile` and `level` are
498  ///   `AV_PROFILE_UNKNOWN` / `AV_LEVEL_UNKNOWN`, both rationals are
499  ///   `0/1`, and so on — so leaving any of them would let a default
500  ///   masquerade as the file's own value.
501  /// * The **four descriptor seats** — `extradata` and
502  ///   `extradata_size`, `coded_side_data` and `nb_coded_side_data` —
503  ///   are written only when the ticket has something to put there. On
504  ///   the empty path they keep the allocator's zero, and that is
505  ///   correct rather than an omission: `codec_parameters_reset`
506  ///   `memset`s the whole struct to zero and then assigns non-zero
507  ///   defaults to a named list that contains none of these four. A
508  ///   null pointer with a zero length is exactly what "no extradata"
509  ///   and "no side data" mean, and it is what the source had.
510  /// * `ch_layout` is always written — order, channel count, and either
511  ///   the mask or the map.
512  ///
513  /// That is what makes field-by-field parity with the original
514  /// provable rather than hopeful, and
515  /// `tests/codec_ticket_parity.rs::every_scalar_seat_is_written_back`
516  /// is the assertion that a seat quietly relying on a default cannot
517  /// pass.
518  ///
519  /// Fallible because allocation is: `ParametersAlloc` when the struct
520  /// itself cannot be allocated, `ParametersCopy` carrying `ENOMEM`
521  /// when one of the heap seats cannot. Nothing here consults a
522  /// budget — the bytes are already resident and were admitted at
523  /// [`Self::mirror`]; what this allocates is exactly
524  /// [`Self::footprint_bytes`].
525  pub fn rebuild(&self) -> Result<Parameters, DemuxError> {
526    let stream_index = self.stream_index;
527    let mut out = Parameters::new();
528    // SAFETY: reading the pointer the constructor stored without
529    // dereferencing it — `Parameters::new` does not check
530    // `avcodec_parameters_alloc` and hands back a null on failure.
531    let dst = unsafe { out.as_mut_ptr() };
532    if dst.is_null() {
533      return Err(DemuxError::ParametersAlloc(ParametersAlloc::new(
534        stream_index,
535      )));
536    }
537
538    // SAFETY: `dst` is a live, freshly allocated `AVCodecParameters`
539    // whose heap seats are still null. Every write below is a scalar
540    // store, or an `addr_of_mut!` store of the same 32-bit pattern the
541    // mirror read, into a struct nothing else holds a reference to.
542    unsafe {
543      write_unaligned(
544        addr_of_mut!((*dst).codec_type).cast::<i32>(),
545        self.codec_type,
546      );
547      write_unaligned(addr_of_mut!((*dst).codec_id).cast::<i32>(), self.codec_id);
548      (*dst).codec_tag = self.codec_tag;
549      (*dst).format = self.format;
550      (*dst).bit_rate = self.bit_rate;
551      (*dst).bits_per_coded_sample = self.bits_per_coded_sample;
552      (*dst).bits_per_raw_sample = self.bits_per_raw_sample;
553      (*dst).profile = self.profile;
554      (*dst).level = self.level;
555      (*dst).width = self.width;
556      (*dst).height = self.height;
557      (*dst).sample_aspect_ratio.num = self.sample_aspect_ratio.num();
558      (*dst).sample_aspect_ratio.den = self.sample_aspect_ratio.den();
559      (*dst).framerate.num = self.framerate.num();
560      (*dst).framerate.den = self.framerate.den();
561      write_unaligned(
562        addr_of_mut!((*dst).field_order).cast::<i32>(),
563        self.field_order,
564      );
565      write_unaligned(
566        addr_of_mut!((*dst).color_range).cast::<i32>(),
567        self.color_range,
568      );
569      write_unaligned(
570        addr_of_mut!((*dst).color_primaries).cast::<i32>(),
571        self.color_primaries,
572      );
573      write_unaligned(addr_of_mut!((*dst).color_trc).cast::<i32>(), self.color_trc);
574      write_unaligned(
575        addr_of_mut!((*dst).color_space).cast::<i32>(),
576        self.color_space,
577      );
578      write_unaligned(
579        addr_of_mut!((*dst).chroma_location).cast::<i32>(),
580        self.chroma_location,
581      );
582      (*dst).video_delay = self.video_delay;
583      (*dst).sample_rate = self.sample_rate;
584      (*dst).block_align = self.block_align;
585      (*dst).frame_size = self.frame_size;
586      (*dst).initial_padding = self.initial_padding;
587      (*dst).trailing_padding = self.trailing_padding;
588      (*dst).seek_preroll = self.seek_preroll;
589      write_unaligned(
590        addr_of_mut!((*dst).alpha_mode).cast::<i32>(),
591        self.alpha_mode,
592      );
593    }
594
595    // The three heap seats, each allocated from FFmpeg's allocator so
596    // `avcodec_parameters_free` releases them with the struct — the
597    // same allocator discipline `bounded_clone_parameters` uses, and
598    // the same one `avcodec_parameters_copy` would have used.
599    //
600    // SAFETY: `dst` is live and its heap seats are null; each
601    // allocation below is checked, and each is attached to `dst`
602    // before the next one is attempted, so a failure part way leaves
603    // `out`'s own destructor a well-formed struct to free.
604    unsafe {
605      write_extradata(dst, self.extradata.as_slice(), stream_index)?;
606      write_side_data(dst, &self.coded_side_data, stream_index)?;
607      write_channel_layout(dst, &self.ch_layout, stream_index)?;
608    }
609
610    Ok(out)
611  }
612
613  /// What [`Self::rebuild`] asks FFmpeg's allocator for: `extradata`
614  /// with the `AV_INPUT_BUFFER_PADDING_SIZE` decoders read past the
615  /// end into, the `coded_side_data` descriptor array and every
616  /// entry's payload, and a custom channel map.
617  ///
618  /// The number the session admitted this stream at, and the number
619  /// `DemuxLimits::max_codec_parameter_bytes` was judged against — so
620  /// a row that opened is a row whose every rebuild fits the ceiling
621  /// it opened under.
622  ///
623  /// Not the ticket's own residency: the owned mirror holds the
624  /// payload without FFmpeg's trailing padding, and shares its buffers
625  /// by refcount.
626  #[cfg_attr(not(tarpaulin), inline(always))]
627  pub const fn footprint_bytes(&self) -> usize {
628    self.footprint_bytes
629  }
630
631  /// The `AVStream.index` this mirror was taken at — what
632  /// [`Self::rebuild`]'s errors name.
633  #[cfg_attr(not(tarpaulin), inline(always))]
634  pub const fn stream_index(&self) -> usize {
635    self.stream_index
636  }
637  /// The raw `AVMediaType`.
638  #[cfg_attr(not(tarpaulin), inline(always))]
639  pub const fn codec_type(&self) -> i32 {
640    self.codec_type
641  }
642  /// The raw `AVCodecID`.
643  #[cfg_attr(not(tarpaulin), inline(always))]
644  pub const fn codec_id(&self) -> i32 {
645    self.codec_id
646  }
647  /// The codec tag — the AVI FOURCC, when the container carries one.
648  #[cfg_attr(not(tarpaulin), inline(always))]
649  pub const fn codec_tag(&self) -> u32 {
650    self.codec_tag
651  }
652  /// The decoder-initialisation bytes — SPS/PPS for H.264, the
653  /// `AudioSpecificConfig` for AAC, a font's payload for an
654  /// attachment. Empty when the stream carries none, or when the row
655  /// was built on the attachment road that leaves them to the carrier.
656  #[cfg_attr(not(tarpaulin), inline(always))]
657  pub fn extradata(&self) -> &[u8] {
658    self.extradata.as_slice()
659  }
660  /// The extradata's carrier, for a consumer that wants the bytes
661  /// without copying them again.
662  #[cfg_attr(not(tarpaulin), inline(always))]
663  pub const fn extradata_ref(&self) -> &FfmpegBytes {
664    &self.extradata
665  }
666  /// Stream-level side data — where a MOV `prof` atom's ICC profile
667  /// arrives, among others.
668  #[cfg_attr(not(tarpaulin), inline(always))]
669  pub fn coded_side_data(&self) -> &[SideDataEntry] {
670    self.coded_side_data.as_slice()
671  }
672  /// The Dolby Vision configuration record — profile number and base-
673  /// layer compatibility id — from the container's `dvcC` / `dvvC` /
674  /// `dwvC` box, when the stream carries one.
675  ///
676  /// `None` when [`Self::coded_side_data`] holds no
677  /// `AV_PKT_DATA_DOVI_CONF` entry (an ordinary, non-Dolby-Vision
678  /// stream — the overwhelming majority) or the entry's payload is too
679  /// short to hold both seats. Absent configuration answers absent,
680  /// same as every other seat this crate exposes as an `Option`.
681  ///
682  /// This is the **configuration-record** half of Dolby Vision — the
683  /// two numbers a consumer routes base-layer-vs-refuse on before a
684  /// single frame decodes. The **per-frame** half — the RPU buffer
685  /// (`AV_FRAME_DATA_DOVI_RPU_BUFFER`) and parsed dynamic metadata
686  /// (`AV_FRAME_DATA_DOVI_METADATA`), plus HDR10+ dynamic metadata
687  /// (`AV_FRAME_DATA_DYNAMIC_HDR_PLUS`) — is not exposed by this crate
688  /// yet: [mediadecode#54](https://github.com/findit-studio/mediadecode/issues/54).
689  #[cfg_attr(not(tarpaulin), inline(always))]
690  pub fn dolby_vision_config(&self) -> Option<DolbyVisionConfig> {
691    let kind = AVPacketSideDataType::AV_PKT_DATA_DOVI_CONF as i32;
692    self
693      .coded_side_data
694      .iter()
695      .find(|entry| entry.kind() == kind)
696      .and_then(|entry| parse_dolby_vision_config(entry.data()))
697  }
698  /// The pixel format (video) or sample format (audio), as the raw
699  /// integer both enums share this seat as.
700  #[cfg_attr(not(tarpaulin), inline(always))]
701  pub const fn format(&self) -> i32 {
702    self.format
703  }
704  /// Average bitrate in bits per second.
705  #[cfg_attr(not(tarpaulin), inline(always))]
706  pub const fn bit_rate(&self) -> i64 {
707    self.bit_rate
708  }
709  /// Bits per sample in the coded bitstream.
710  #[cfg_attr(not(tarpaulin), inline(always))]
711  pub const fn bits_per_coded_sample(&self) -> i32 {
712    self.bits_per_coded_sample
713  }
714  /// Valid bits in each output sample.
715  #[cfg_attr(not(tarpaulin), inline(always))]
716  pub const fn bits_per_raw_sample(&self) -> i32 {
717    self.bits_per_raw_sample
718  }
719  /// The codec profile.
720  #[cfg_attr(not(tarpaulin), inline(always))]
721  pub const fn profile(&self) -> i32 {
722    self.profile
723  }
724  /// The codec level.
725  #[cfg_attr(not(tarpaulin), inline(always))]
726  pub const fn level(&self) -> i32 {
727    self.level
728  }
729  /// Frame width in pixels — video, and the subtitle canvas.
730  #[cfg_attr(not(tarpaulin), inline(always))]
731  pub const fn width(&self) -> i32 {
732    self.width
733  }
734  /// Frame height in pixels — video, and the subtitle canvas.
735  #[cfg_attr(not(tarpaulin), inline(always))]
736  pub const fn height(&self) -> i32 {
737    self.height
738  }
739  /// The sample aspect ratio. A zero numerator means unknown.
740  #[cfg_attr(not(tarpaulin), inline(always))]
741  pub const fn sample_aspect_ratio(&self) -> Ratio {
742    self.sample_aspect_ratio
743  }
744  /// The codec-level frame rate. `0/1` when frames differ in duration
745  /// or the value is not known.
746  #[cfg_attr(not(tarpaulin), inline(always))]
747  pub const fn framerate(&self) -> Ratio {
748    self.framerate
749  }
750  /// The raw `AVFieldOrder`.
751  #[cfg_attr(not(tarpaulin), inline(always))]
752  pub const fn field_order(&self) -> i32 {
753    self.field_order
754  }
755  /// The raw `AVColorRange`.
756  #[cfg_attr(not(tarpaulin), inline(always))]
757  pub const fn color_range(&self) -> i32 {
758    self.color_range
759  }
760  /// The raw `AVColorPrimaries`.
761  #[cfg_attr(not(tarpaulin), inline(always))]
762  pub const fn color_primaries(&self) -> i32 {
763    self.color_primaries
764  }
765  /// The raw `AVColorTransferCharacteristic`.
766  #[cfg_attr(not(tarpaulin), inline(always))]
767  pub const fn color_trc(&self) -> i32 {
768    self.color_trc
769  }
770  /// The raw `AVColorSpace`.
771  #[cfg_attr(not(tarpaulin), inline(always))]
772  pub const fn color_space(&self) -> i32 {
773    self.color_space
774  }
775  /// The raw `AVChromaLocation`.
776  #[cfg_attr(not(tarpaulin), inline(always))]
777  pub const fn chroma_location(&self) -> i32 {
778    self.chroma_location
779  }
780  /// Number of delayed frames — the decoder's `has_b_frames`.
781  #[cfg_attr(not(tarpaulin), inline(always))]
782  pub const fn video_delay(&self) -> i32 {
783    self.video_delay
784  }
785  /// The channel layout.
786  #[cfg_attr(not(tarpaulin), inline(always))]
787  pub const fn ch_layout(&self) -> &ChannelLayoutTicket {
788    &self.ch_layout
789  }
790  /// Audio samples per second.
791  #[cfg_attr(not(tarpaulin), inline(always))]
792  pub const fn sample_rate(&self) -> i32 {
793    self.sample_rate
794  }
795  /// Bytes per coded audio frame — `nBlockAlign` in `WAVEFORMATEX`.
796  #[cfg_attr(not(tarpaulin), inline(always))]
797  pub const fn block_align(&self) -> i32 {
798    self.block_align
799  }
800  /// Audio frame size, when the format fixes one.
801  #[cfg_attr(not(tarpaulin), inline(always))]
802  pub const fn frame_size(&self) -> i32 {
803    self.frame_size
804  }
805  /// Leading padding samples the encoder inserted.
806  #[cfg_attr(not(tarpaulin), inline(always))]
807  pub const fn initial_padding(&self) -> i32 {
808    self.initial_padding
809  }
810  /// Trailing padding samples the encoder appended.
811  #[cfg_attr(not(tarpaulin), inline(always))]
812  pub const fn trailing_padding(&self) -> i32 {
813    self.trailing_padding
814  }
815  /// Samples to skip after a discontinuity.
816  #[cfg_attr(not(tarpaulin), inline(always))]
817  pub const fn seek_preroll(&self) -> i32 {
818    self.seek_preroll
819  }
820  /// The raw `AVAlphaMode` — how an alpha channel relates to the
821  /// colour values, and the last field `AVCodecParameters` declares.
822  ///
823  /// New in FFmpeg n9.0, and the seat this mirror's first draft
824  /// dropped: video-only, left at its zero by every fixture the corpus
825  /// can mint, and therefore reading back identically whether it is
826  /// mirrored or forgotten. The parity comparator names every field
827  /// for exactly that reason.
828  #[cfg_attr(not(tarpaulin), inline(always))]
829  pub const fn alpha_mode(&self) -> i32 {
830    self.alpha_mode
831  }
832}
833
834impl core::fmt::Debug for CodecTicket {
835  /// Sizes rather than payloads. An `extradata` blob and an ICC
836  /// profile are both megabyte-scale and neither is readable; what a
837  /// reader of a log wants is the stream's identity and whether the
838  /// heap seats are populated.
839  fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
840    f.debug_struct("CodecTicket")
841      .field(
842        "medium",
843        &crate::boundary::media_kind_from_raw(self.codec_type),
844      )
845      .field("codec_id", &self.codec_id)
846      .field("codec_tag", &format_args!("{:#010x}", self.codec_tag))
847      .field("format", &self.format)
848      .field("width", &self.width)
849      .field("height", &self.height)
850      .field("sample_rate", &self.sample_rate)
851      .field("channels", &self.ch_layout.channels)
852      .field("extradata_len", &self.extradata.len())
853      .field("coded_side_data", &self.coded_side_data.len())
854      .field("footprint_bytes", &self.footprint_bytes)
855      .finish_non_exhaustive()
856  }
857}
858
859// ---------------------------------------------------------------------------
860//  Reading the three heap seats.
861// ---------------------------------------------------------------------------
862
863/// Copies `extradata` into an owned carrier — the payload only.
864///
865/// FFmpeg's `AV_INPUT_BUFFER_PADDING_SIZE` trailing zeroes are an
866/// allocation contract, not content: decoders read past the end of the
867/// buffer and the padding is what makes that defined. Carrying them in
868/// the mirror would store zeroes an owned `Arc<[u8]>` needs no reader
869/// to be safe past; [`write_extradata`] mints them again on the way
870/// back out, which is where they mean something.
871///
872/// # Safety
873///
874/// `par` must be a live `*const AVCodecParameters`.
875unsafe fn extradata_of(par: *const AVCodecParameters, policy: ExtradataPolicy) -> FfmpegBytes {
876  if matches!(policy, ExtradataPolicy::Omit) {
877    return FfmpegBytes::empty();
878  }
879  // SAFETY: `par` is live per the contract; both fields are a pointer
880  // and an integer.
881  let (ptr, size) = unsafe { ((*par).extradata, (*par).extradata_size) };
882  let Ok(len) = usize::try_from(size) else {
883    return FfmpegBytes::empty();
884  };
885  if ptr.is_null() || len == 0 {
886    return FfmpegBytes::empty();
887  }
888  // SAFETY: libavformat guarantees `extradata` is readable for
889  // `extradata_size` bytes while the parameters live, and the slice is
890  // consumed before this function returns.
891  FfmpegBytes::copy_from_slice(unsafe { core::slice::from_raw_parts(ptr, len) })
892}
893
894/// Copies every `coded_side_data` entry into owned entries.
895///
896/// # Safety
897///
898/// `par` must be a live `*const AVCodecParameters`.
899unsafe fn side_data_of(par: *const AVCodecParameters) -> Vec<SideDataEntry> {
900  // SAFETY: `par` is live per the contract.
901  let (array, count) = unsafe { ((*par).coded_side_data, (*par).nb_coded_side_data) };
902  let Ok(count) = usize::try_from(count) else {
903    return Vec::new();
904  };
905  if array.is_null() || count == 0 {
906    return Vec::new();
907  }
908  let mut entries = Vec::with_capacity(count);
909  for index in 0..count {
910    // **Never `&*entry`.** `AVPacketSideData::type` is an open C enum
911    // and an ABI-compatible FFmpeg newer than these bindings emits
912    // kinds absent from the generated Rust enum; forming a typed
913    // reference asserts every field inhabits its declared type, which
914    // is undefined behaviour before a single field is read.
915    //
916    // SAFETY: the array is valid for `nb_coded_side_data` contiguous
917    // entries per FFmpeg's contract, `index` is below that count, and
918    // `addr_of!` computes a field address without forming a reference
919    // to the struct containing it.
920    let (kind, data, size) = unsafe {
921      let entry = array.add(index);
922      (
923        read_unaligned(addr_of!((*entry).type_).cast::<i32>()),
924        read_unaligned(addr_of!((*entry).data)),
925        read_unaligned(addr_of!((*entry).size)),
926      )
927    };
928    let payload = if data.is_null() || size == 0 {
929      FfmpegBytes::empty()
930    } else {
931      // SAFETY: the descriptor declares `size` readable bytes at
932      // `data`, and the slice is consumed before the loop advances.
933      FfmpegBytes::copy_from_slice(unsafe { core::slice::from_raw_parts(data, size) })
934    };
935    entries.push(SideDataEntry::new(kind, payload));
936  }
937  entries
938}
939
940/// Mirrors the embedded `AVChannelLayout`.
941///
942/// # Safety
943///
944/// `par` must be a live `*const AVCodecParameters`.
945unsafe fn channel_layout_of(
946  par: *const AVCodecParameters,
947  stream_index: usize,
948) -> Result<ChannelLayoutTicket, DemuxError> {
949  // SAFETY: `ch_layout` is embedded by value; `addr_of!` reaches each
950  // field without forming a reference to the layout, and `order` has
951  // the layout of a `c_int`.
952  let (order, channels, opaque) = unsafe {
953    (
954      read_unaligned(addr_of!((*par).ch_layout.order).cast::<i32>()),
955      (*par).ch_layout.nb_channels,
956      (*par).ch_layout.opaque,
957    )
958  };
959  if !opaque.is_null() {
960    return Err(DemuxError::ParametersOpaque(ParametersOpaque::new(
961      stream_index,
962      None,
963    )));
964  }
965
966  let mut layout = ChannelLayoutTicket {
967    order,
968    channels,
969    mask: 0,
970    map: Vec::new(),
971  };
972  if !layout.is_custom() {
973    // Every order but `CUSTOM` describes its channels with the union's
974    // `mask` arm. Reading it for `CUSTOM` would read a pointer.
975    //
976    // SAFETY: the union is eight bytes either way and this arm is the
977    // one the order names.
978    layout.mask = unsafe { (*par).ch_layout.u.mask };
979    return Ok(layout);
980  }
981
982  // A custom order without a full map is **refused**, not reproduced.
983  // `av_channel_layout_copy` — the call
984  // `avcodec_parameters_to_context` moves this field through —
985  // allocates `nb_channels` entries and then `memcpy`s from
986  // `src->u.map` with no null check of its own, so a layout that names
987  // channels it has no map for makes libavcodec read from null the
988  // moment a decoder opens. Carrying it across would be a faithful
989  // round trip of a crash. See
990  // [`DemuxError::ParametersChannelMap`](crate::DemuxError).
991  //
992  // Refusing here is also what lets [`write_channel_layout`] rely on
993  // `map.len() == nb_channels` for a custom order.
994  //
995  // SAFETY: the order names the `map` arm.
996  let map = unsafe { (*par).ch_layout.u.map };
997  let malformed = || {
998    Err(DemuxError::ParametersChannelMap(ParametersChannelMap::new(
999      stream_index,
1000      channels,
1001    )))
1002  };
1003  let Ok(count) = usize::try_from(channels) else {
1004    return malformed();
1005  };
1006  if map.is_null() || count == 0 {
1007    return malformed();
1008  }
1009  layout.map.reserve_exact(count);
1010  for index in 0..count {
1011    // Field pointers again, never `&AVChannelCustom`: `id` is an open
1012    // enum with the same hazard as a side-data type id.
1013    //
1014    // SAFETY: FFmpeg's contract makes the map `nb_channels` entries
1015    // long, `index` is below that count, and every read goes through
1016    // `addr_of!`.
1017    let (id, name, opaque) = unsafe {
1018      let entry = map.add(index);
1019      (
1020        read_unaligned(addr_of!((*entry).id).cast::<i32>()),
1021        read_unaligned(addr_of!((*entry).name).cast::<[u8; 16]>()),
1022        read_unaligned(addr_of!((*entry).opaque)),
1023      )
1024    };
1025    if !opaque.is_null() {
1026      return Err(DemuxError::ParametersOpaque(ParametersOpaque::new(
1027        stream_index,
1028        Some(index),
1029      )));
1030    }
1031    layout.map.push(CustomChannel::new(id, name));
1032  }
1033  Ok(layout)
1034}
1035
1036// ---------------------------------------------------------------------------
1037//  Writing the three heap seats.
1038// ---------------------------------------------------------------------------
1039
1040/// The `ENOMEM` a heap seat's allocation reports when it fails.
1041fn seat_alloc_failed(stream_index: usize) -> DemuxError {
1042  DemuxError::ParametersCopy(ParametersCopy::new(
1043    stream_index,
1044    ffmpeg_next::Error::Other {
1045      errno: libc::ENOMEM,
1046    },
1047  ))
1048}
1049
1050/// Allocates `extradata` and its padding, and copies the payload in.
1051///
1052/// # Safety
1053///
1054/// `dst` must be a live `*mut AVCodecParameters` whose `extradata` is
1055/// null.
1056unsafe fn write_extradata(
1057  dst: *mut AVCodecParameters,
1058  payload: &[u8],
1059  stream_index: usize,
1060) -> Result<(), DemuxError> {
1061  if payload.is_empty() {
1062    return Ok(());
1063  }
1064  let size = i32::try_from(payload.len()).map_err(|_| seat_alloc_failed(stream_index))?;
1065  let padded = payload
1066    .len()
1067    .checked_add(AV_INPUT_BUFFER_PADDING_SIZE as usize)
1068    .ok_or_else(|| seat_alloc_failed(stream_index))?;
1069  // SAFETY: `av_mallocz` returns zeroed memory or null; the copy
1070  // writes exactly `payload.len()` bytes into an allocation that is
1071  // `AV_INPUT_BUFFER_PADDING_SIZE` longer, leaving the padding zero —
1072  // which is the contract decoders read past the end under.
1073  unsafe {
1074    let buffer = av_mallocz(padded).cast::<u8>();
1075    if buffer.is_null() {
1076      return Err(seat_alloc_failed(stream_index));
1077    }
1078    copy_nonoverlapping(payload.as_ptr(), buffer, payload.len());
1079    (*dst).extradata = buffer;
1080    (*dst).extradata_size = size;
1081  }
1082  Ok(())
1083}
1084
1085/// Allocates the `coded_side_data` descriptor array and each payload.
1086///
1087/// # Safety
1088///
1089/// `dst` must be a live `*mut AVCodecParameters` whose
1090/// `coded_side_data` is null.
1091unsafe fn write_side_data(
1092  dst: *mut AVCodecParameters,
1093  entries: &[SideDataEntry],
1094  stream_index: usize,
1095) -> Result<(), DemuxError> {
1096  if entries.is_empty() {
1097    return Ok(());
1098  }
1099  let count = i32::try_from(entries.len()).map_err(|_| seat_alloc_failed(stream_index))?;
1100  let bytes = entries
1101    .len()
1102    .checked_mul(core::mem::size_of::<AVPacketSideData>())
1103    .ok_or_else(|| seat_alloc_failed(stream_index))?;
1104
1105  // SAFETY: `dst` is live with a null `coded_side_data`. The array is
1106  // attached before any payload is filled in, so a failure part way
1107  // leaves the destructor a well-formed array to walk: the entries it
1108  // has not reached are zeroed, and freeing a null payload is a no-op.
1109  unsafe {
1110    let array = av_mallocz(bytes).cast::<AVPacketSideData>();
1111    if array.is_null() {
1112      return Err(seat_alloc_failed(stream_index));
1113    }
1114    (*dst).coded_side_data = array;
1115    (*dst).nb_coded_side_data = count;
1116
1117    for (index, entry) in entries.iter().enumerate() {
1118      let into = array.add(index);
1119      // The type id travels as the raw bits it is on the wire, for the
1120      // reason the read did: a kind these bindings cannot name is
1121      // still a kind the file carries and a decoder may want.
1122      write_unaligned(addr_of_mut!((*into).type_).cast::<i32>(), entry.kind());
1123      let payload = entry.data();
1124      if payload.is_empty() {
1125        continue;
1126      }
1127      let buffer = av_mallocz(payload.len()).cast::<u8>();
1128      if buffer.is_null() {
1129        return Err(seat_alloc_failed(stream_index));
1130      }
1131      copy_nonoverlapping(payload.as_ptr(), buffer, payload.len());
1132      write_unaligned(addr_of_mut!((*into).data), buffer);
1133      write_unaligned(addr_of_mut!((*into).size), payload.len());
1134    }
1135  }
1136  Ok(())
1137}
1138
1139/// Writes the channel layout, allocating a custom map when the order
1140/// names one.
1141///
1142/// Written field by field rather than through
1143/// `av_channel_layout_copy`, because the source it would copy from is
1144/// the thing this road has abolished: there is no live
1145/// `AVChannelLayout` to copy, only owned Rust. The one allocation is
1146/// the custom map, which [`CodecTicket::mirror`] already measured and
1147/// admitted.
1148///
1149/// # Safety
1150///
1151/// `dst` must be a live `*mut AVCodecParameters` whose `ch_layout` is
1152/// the zeroed (`AV_CHANNEL_ORDER_UNSPEC`) state
1153/// `avcodec_parameters_alloc` leaves, owning no map.
1154unsafe fn write_channel_layout(
1155  dst: *mut AVCodecParameters,
1156  layout: &ChannelLayoutTicket,
1157  stream_index: usize,
1158) -> Result<(), DemuxError> {
1159  // SAFETY: `dst` is live and its layout owns nothing yet; `order` is
1160  // written as the same 32-bit pattern the mirror read.
1161  unsafe {
1162    write_unaligned(
1163      addr_of_mut!((*dst).ch_layout.order).cast::<i32>(),
1164      layout.order(),
1165    );
1166    (*dst).ch_layout.opaque = core::ptr::null_mut();
1167  }
1168
1169  if !layout.is_custom() {
1170    // SAFETY: the order names the `mask` arm.
1171    unsafe {
1172      (*dst).ch_layout.nb_channels = layout.channels();
1173      (*dst).ch_layout.u.mask = layout.mask();
1174    }
1175    return Ok(());
1176  }
1177
1178  // **`nb_channels` comes from the map, not from the stored field.**
1179  // The two are equal — [`channel_layout_of`] refuses a custom layout
1180  // it cannot map in full, and the fields are private, so no other
1181  // value can exist. Writing the count from the array anyway is what
1182  // makes that structural rather than remembered: the layout handed to
1183  // libavcodec can never declare more channels than the array it points
1184  // at, which is precisely the shape `av_channel_layout_copy` would
1185  // `memcpy` past the end of.
1186  debug_assert_eq!(
1187    layout.map().len(),
1188    layout.channels().max(0) as usize,
1189    "a custom layout's map length is its channel count",
1190  );
1191  let count = layout.map().len();
1192  if count == 0 {
1193    // Unreachable through `mirror`, and fail-closed if a future
1194    // constructor ever makes it reachable.
1195    return Err(DemuxError::ParametersChannelMap(ParametersChannelMap::new(
1196      stream_index,
1197      layout.channels(),
1198    )));
1199  }
1200
1201  let declared = i32::try_from(count).map_err(|_| seat_alloc_failed(stream_index))?;
1202  let bytes = count
1203    .checked_mul(core::mem::size_of::<AVChannelCustom>())
1204    .ok_or_else(|| seat_alloc_failed(stream_index))?;
1205  // SAFETY: `av_mallocz` returns zeroed memory or null. The map and the
1206  // count it describes are attached together, before the entries are
1207  // filled in, so a later failure leaves a well-formed (zeroed) map for
1208  // the destructor to free, and every write goes through
1209  // `addr_of_mut!` rather than a typed reference.
1210  unsafe {
1211    let map = av_mallocz(bytes).cast::<AVChannelCustom>();
1212    if map.is_null() {
1213      return Err(seat_alloc_failed(stream_index));
1214    }
1215    (*dst).ch_layout.u.map = map;
1216    (*dst).ch_layout.nb_channels = declared;
1217    for (index, channel) in layout.map().iter().enumerate() {
1218      let into = map.add(index);
1219      write_unaligned(addr_of_mut!((*into).id).cast::<i32>(), channel.id());
1220      write_unaligned(
1221        addr_of_mut!((*into).name).cast::<[u8; 16]>(),
1222        *channel.name_bytes(),
1223      );
1224      write_unaligned(addr_of_mut!((*into).opaque), core::ptr::null_mut());
1225    }
1226  }
1227  Ok(())
1228}
1229
1230#[cfg(test)]
1231mod tests;