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;