Skip to main content

mediadecode_ffmpeg/
extras.rs

1//! Backend-specific `*Extra` carriers used as the
2//! `mediadecode::*Adapter::*Extra` associated types.
3//!
4//! Fields are private; values are read through getters and set through
5//! `with_*` (consuming builders) / `set_*` (in-place mutators) — the
6//! crate-wide encapsulation convention. `const fn` is used wherever
7//! the field type permits (i.e. anything but `Vec`).
8//!
9//! # The extras obey the amputation contract too
10//!
11//! [`SideDataEntry`] carries its payload as an `FfmpegBytes`, not a
12//! `Vec<u8>`. Those bytes ride on packets and frames that a graph fans
13//! out, and the [D-seat contract][law] says a clone of a message is a
14//! refcount bump — a `Vec` in the extras would have made a frame's
15//! *payload* cheap to clone and its metadata expensive, which is the
16//! kind of asymmetry nobody remembers while profiling.
17//! `smpte_timecode` stays a `Vec<u32>` on [`VideoFrameExtra`]: those
18//! are parsed values, a handful of words, not bytes crossing a
19//! boundary.
20//!
21//! [law]: mediadecode::adapter#the-d-seat-amputation-contract
22
23use std::vec::Vec;
24
25use mediaframe::frame::Rotation;
26
27use crate::FfmpegBytes;
28
29use derive_more::IsVariant;
30use ffmpeg_next::codec::Parameters;
31
32use crate::{
33  demuxer::{DemuxError, ParametersAlloc, ParametersCopy, ParametersMissing, ParametersTooLarge},
34  ticket::CodecTicket,
35};
36
37/// Per-`VideoPacket` extras.
38#[derive(Clone, Debug, Default)]
39pub struct VideoPacketExtra {
40  stream_index: i32,
41  byte_pos: Option<i64>,
42  side_data: Vec<SideDataEntry>,
43}
44
45impl VideoPacketExtra {
46  /// Constructs a `VideoPacketExtra` with the given stream index.
47  /// `byte_pos` defaults to `None` and `side_data` to empty.
48  #[cfg_attr(not(tarpaulin), inline(always))]
49  pub const fn new(stream_index: i32) -> Self {
50    Self {
51      stream_index,
52      byte_pos: None,
53      side_data: Vec::new(),
54    }
55  }
56
57  /// Returns the source `AVStream.index`.
58  #[cfg_attr(not(tarpaulin), inline(always))]
59  pub const fn stream_index(&self) -> i32 {
60    self.stream_index
61  }
62
63  /// Returns the byte position of the packet in the input file, or
64  /// `None` if unknown.
65  #[cfg_attr(not(tarpaulin), inline(always))]
66  pub const fn byte_pos(&self) -> Option<i64> {
67    self.byte_pos
68  }
69
70  /// Returns the raw side-data entries from `AVPacket.side_data`.
71  #[cfg_attr(not(tarpaulin), inline(always))]
72  pub fn side_data(&self) -> &[SideDataEntry] {
73    self.side_data.as_slice()
74  }
75
76  /// Sets the stream index (consuming builder).
77  #[cfg_attr(not(tarpaulin), inline(always))]
78  #[must_use]
79  pub const fn with_stream_index(mut self, value: i32) -> Self {
80    self.stream_index = value;
81    self
82  }
83  /// Sets the byte position (consuming builder).
84  #[cfg_attr(not(tarpaulin), inline(always))]
85  #[must_use]
86  pub const fn with_byte_pos(mut self, value: Option<i64>) -> Self {
87    self.byte_pos = value;
88    self
89  }
90  /// Sets the side-data list (consuming builder).
91  #[cfg_attr(not(tarpaulin), inline(always))]
92  #[must_use]
93  pub fn with_side_data(mut self, value: Vec<SideDataEntry>) -> Self {
94    self.side_data = value;
95    self
96  }
97
98  /// Sets the stream index in place.
99  #[cfg_attr(not(tarpaulin), inline(always))]
100  pub const fn set_stream_index(&mut self, value: i32) -> &mut Self {
101    self.stream_index = value;
102    self
103  }
104  /// Sets the byte position in place.
105  #[cfg_attr(not(tarpaulin), inline(always))]
106  pub const fn set_byte_pos(&mut self, value: Option<i64>) -> &mut Self {
107    self.byte_pos = value;
108    self
109  }
110  /// Sets the side-data list in place.
111  #[cfg_attr(not(tarpaulin), inline(always))]
112  pub fn set_side_data(&mut self, value: Vec<SideDataEntry>) -> &mut Self {
113    self.side_data = value;
114    self
115  }
116}
117
118/// Per-`VideoFrame` extras carrying everything the unified
119/// `mediadecode::ColorInfo` doesn't already cover.
120#[derive(Clone, Debug, Default)]
121pub struct VideoFrameExtra {
122  sample_aspect_ratio: Option<(u32, u32)>,
123  picture_type: PictureType,
124  key_frame: bool,
125  interlaced: bool,
126  top_field_first: bool,
127  best_effort_timestamp: Option<i64>,
128  mastering_display: Option<MasteringDisplay>,
129  content_light_level: Option<ContentLightLevel>,
130  smpte_timecode: Vec<u32>,
131  side_data: Vec<SideDataEntry>,
132}
133
134impl VideoFrameExtra {
135  /// Constructs an empty `VideoFrameExtra` (all fields at default).
136  #[cfg_attr(not(tarpaulin), inline(always))]
137  pub const fn new() -> Self {
138    Self {
139      sample_aspect_ratio: None,
140      picture_type: PictureType::Unspecified,
141      key_frame: false,
142      interlaced: false,
143      top_field_first: false,
144      best_effort_timestamp: None,
145      mastering_display: None,
146      content_light_level: None,
147      smpte_timecode: Vec::new(),
148      side_data: Vec::new(),
149    }
150  }
151
152  /// Sample aspect ratio (par numerator / denominator), `None` if 1:1
153  /// or unspecified.
154  #[cfg_attr(not(tarpaulin), inline(always))]
155  pub const fn sample_aspect_ratio(&self) -> Option<(u32, u32)> {
156    self.sample_aspect_ratio
157  }
158  /// Frame picture type (I/P/B/etc.).
159  #[cfg_attr(not(tarpaulin), inline(always))]
160  pub const fn picture_type(&self) -> PictureType {
161    self.picture_type
162  }
163  /// `True` if this frame is a key frame.
164  #[cfg_attr(not(tarpaulin), inline(always))]
165  pub const fn key_frame(&self) -> bool {
166    self.key_frame
167  }
168  /// `True` if the frame is interlaced.
169  #[cfg_attr(not(tarpaulin), inline(always))]
170  pub const fn interlaced(&self) -> bool {
171    self.interlaced
172  }
173  /// `True` if the top field is first (only meaningful with `interlaced`).
174  #[cfg_attr(not(tarpaulin), inline(always))]
175  pub const fn top_field_first(&self) -> bool {
176    self.top_field_first
177  }
178  /// FFmpeg's heuristic best-effort PTS, or `None` if unknown.
179  #[cfg_attr(not(tarpaulin), inline(always))]
180  pub const fn best_effort_timestamp(&self) -> Option<i64> {
181    self.best_effort_timestamp
182  }
183  /// HDR10 mastering-display metadata, if present on the source frame.
184  #[cfg_attr(not(tarpaulin), inline(always))]
185  pub const fn mastering_display(&self) -> Option<MasteringDisplay> {
186    self.mastering_display
187  }
188  /// HDR10 content-light-level.
189  #[cfg_attr(not(tarpaulin), inline(always))]
190  pub const fn content_light_level(&self) -> Option<ContentLightLevel> {
191    self.content_light_level
192  }
193  /// SMPTE ST 12-M timecode entries (raw 32-bit BCD-packed values).
194  #[cfg_attr(not(tarpaulin), inline(always))]
195  pub fn smpte_timecode(&self) -> &[u32] {
196    self.smpte_timecode.as_slice()
197  }
198  /// Raw side-data entries from `AVFrame.side_data`.
199  #[cfg_attr(not(tarpaulin), inline(always))]
200  pub fn side_data(&self) -> &[SideDataEntry] {
201    self.side_data.as_slice()
202  }
203
204  /// Sets the sample aspect ratio (consuming builder).
205  #[cfg_attr(not(tarpaulin), inline(always))]
206  pub const fn with_sample_aspect_ratio(mut self, value: Option<(u32, u32)>) -> Self {
207    self.sample_aspect_ratio = value;
208    self
209  }
210  /// Sets the picture type (consuming builder).
211  #[cfg_attr(not(tarpaulin), inline(always))]
212  #[must_use]
213  pub const fn with_picture_type(mut self, value: PictureType) -> Self {
214    self.picture_type = value;
215    self
216  }
217  /// Sets the key-frame flag (consuming builder).
218  #[cfg_attr(not(tarpaulin), inline(always))]
219  #[must_use]
220  pub const fn with_key_frame(mut self, value: bool) -> Self {
221    self.key_frame = value;
222    self
223  }
224  /// Sets the interlaced flag (consuming builder).
225  #[cfg_attr(not(tarpaulin), inline(always))]
226  #[must_use]
227  pub const fn with_interlaced(mut self, value: bool) -> Self {
228    self.interlaced = value;
229    self
230  }
231  /// Sets the top-field-first flag (consuming builder).
232  #[cfg_attr(not(tarpaulin), inline(always))]
233  #[must_use]
234  pub const fn with_top_field_first(mut self, value: bool) -> Self {
235    self.top_field_first = value;
236    self
237  }
238  /// Sets the best-effort timestamp (consuming builder).
239  #[cfg_attr(not(tarpaulin), inline(always))]
240  #[must_use]
241  pub const fn with_best_effort_timestamp(mut self, value: Option<i64>) -> Self {
242    self.best_effort_timestamp = value;
243    self
244  }
245  /// Sets the mastering-display metadata (consuming builder).
246  #[cfg_attr(not(tarpaulin), inline(always))]
247  #[must_use]
248  pub const fn with_mastering_display(mut self, value: Option<MasteringDisplay>) -> Self {
249    self.mastering_display = value;
250    self
251  }
252  /// Sets the content-light-level metadata (consuming builder).
253  #[cfg_attr(not(tarpaulin), inline(always))]
254  #[must_use]
255  pub const fn with_content_light_level(mut self, value: Option<ContentLightLevel>) -> Self {
256    self.content_light_level = value;
257    self
258  }
259  /// Sets the SMPTE timecode list (consuming builder).
260  #[cfg_attr(not(tarpaulin), inline(always))]
261  #[must_use]
262  pub fn with_smpte_timecode(mut self, value: Vec<u32>) -> Self {
263    self.smpte_timecode = value;
264    self
265  }
266  /// Sets the side-data list (consuming builder).
267  #[cfg_attr(not(tarpaulin), inline(always))]
268  #[must_use]
269  pub fn with_side_data(mut self, value: Vec<SideDataEntry>) -> Self {
270    self.side_data = value;
271    self
272  }
273
274  /// Sets the sample aspect ratio in place.
275  #[cfg_attr(not(tarpaulin), inline(always))]
276  pub const fn set_sample_aspect_ratio(&mut self, value: Option<(u32, u32)>) -> &mut Self {
277    self.sample_aspect_ratio = value;
278    self
279  }
280  /// Sets the picture type in place.
281  #[cfg_attr(not(tarpaulin), inline(always))]
282  pub const fn set_picture_type(&mut self, value: PictureType) -> &mut Self {
283    self.picture_type = value;
284    self
285  }
286  /// Sets the key-frame flag in place.
287  #[cfg_attr(not(tarpaulin), inline(always))]
288  pub const fn set_key_frame(&mut self, value: bool) -> &mut Self {
289    self.key_frame = value;
290    self
291  }
292  /// Sets the interlaced flag in place.
293  #[cfg_attr(not(tarpaulin), inline(always))]
294  pub const fn set_interlaced(&mut self, value: bool) -> &mut Self {
295    self.interlaced = value;
296    self
297  }
298  /// Sets the top-field-first flag in place.
299  #[cfg_attr(not(tarpaulin), inline(always))]
300  pub const fn set_top_field_first(&mut self, value: bool) -> &mut Self {
301    self.top_field_first = value;
302    self
303  }
304  /// Sets the best-effort timestamp in place.
305  #[cfg_attr(not(tarpaulin), inline(always))]
306  pub const fn set_best_effort_timestamp(&mut self, value: Option<i64>) -> &mut Self {
307    self.best_effort_timestamp = value;
308    self
309  }
310  /// Sets the mastering-display metadata in place.
311  #[cfg_attr(not(tarpaulin), inline(always))]
312  pub const fn set_mastering_display(&mut self, value: Option<MasteringDisplay>) -> &mut Self {
313    self.mastering_display = value;
314    self
315  }
316  /// Sets the content-light-level metadata in place.
317  #[cfg_attr(not(tarpaulin), inline(always))]
318  pub const fn set_content_light_level(&mut self, value: Option<ContentLightLevel>) -> &mut Self {
319    self.content_light_level = value;
320    self
321  }
322  /// Sets the SMPTE timecode list in place.
323  #[cfg_attr(not(tarpaulin), inline(always))]
324  pub fn set_smpte_timecode(&mut self, value: Vec<u32>) -> &mut Self {
325    self.smpte_timecode = value;
326    self
327  }
328  /// Sets the side-data list in place.
329  #[cfg_attr(not(tarpaulin), inline(always))]
330  pub fn set_side_data(&mut self, value: Vec<SideDataEntry>) -> &mut Self {
331    self.side_data = value;
332    self
333  }
334}
335
336/// Per-`AudioPacket` extras.
337#[derive(Clone, Debug, Default)]
338pub struct AudioPacketExtra {
339  stream_index: i32,
340  byte_pos: Option<i64>,
341  side_data: Vec<SideDataEntry>,
342}
343
344impl AudioPacketExtra {
345  /// Constructs an `AudioPacketExtra` with the given stream index.
346  #[cfg_attr(not(tarpaulin), inline(always))]
347  pub const fn new(stream_index: i32) -> Self {
348    Self {
349      stream_index,
350      byte_pos: None,
351      side_data: Vec::new(),
352    }
353  }
354
355  /// Returns the source `AVStream.index`.
356  #[cfg_attr(not(tarpaulin), inline(always))]
357  pub const fn stream_index(&self) -> i32 {
358    self.stream_index
359  }
360  /// Returns the byte position, or `None` if unknown.
361  #[cfg_attr(not(tarpaulin), inline(always))]
362  pub const fn byte_pos(&self) -> Option<i64> {
363    self.byte_pos
364  }
365  /// Returns the raw side-data entries.
366  #[cfg_attr(not(tarpaulin), inline(always))]
367  pub fn side_data(&self) -> &[SideDataEntry] {
368    self.side_data.as_slice()
369  }
370
371  /// Sets the stream index (consuming builder).
372  #[cfg_attr(not(tarpaulin), inline(always))]
373  #[must_use]
374  pub const fn with_stream_index(mut self, value: i32) -> Self {
375    self.stream_index = value;
376    self
377  }
378  /// Sets the byte position (consuming builder).
379  #[cfg_attr(not(tarpaulin), inline(always))]
380  #[must_use]
381  pub const fn with_byte_pos(mut self, value: Option<i64>) -> Self {
382    self.byte_pos = value;
383    self
384  }
385  /// Sets the side-data list (consuming builder).
386  #[cfg_attr(not(tarpaulin), inline(always))]
387  #[must_use]
388  pub fn with_side_data(mut self, value: Vec<SideDataEntry>) -> Self {
389    self.side_data = value;
390    self
391  }
392
393  /// Sets the stream index in place.
394  #[cfg_attr(not(tarpaulin), inline(always))]
395  pub const fn set_stream_index(&mut self, value: i32) -> &mut Self {
396    self.stream_index = value;
397    self
398  }
399  /// Sets the byte position in place.
400  #[cfg_attr(not(tarpaulin), inline(always))]
401  pub const fn set_byte_pos(&mut self, value: Option<i64>) -> &mut Self {
402    self.byte_pos = value;
403    self
404  }
405  /// Sets the side-data list in place.
406  #[cfg_attr(not(tarpaulin), inline(always))]
407  pub fn set_side_data(&mut self, value: Vec<SideDataEntry>) -> &mut Self {
408    self.side_data = value;
409    self
410  }
411}
412
413/// Per-`AudioFrame` extras.
414#[derive(Clone, Debug, Default)]
415pub struct AudioFrameExtra {
416  best_effort_timestamp: Option<i64>,
417  side_data: Vec<SideDataEntry>,
418}
419
420impl AudioFrameExtra {
421  /// Constructs an empty `AudioFrameExtra`.
422  #[cfg_attr(not(tarpaulin), inline(always))]
423  pub const fn new() -> Self {
424    Self {
425      best_effort_timestamp: None,
426      side_data: Vec::new(),
427    }
428  }
429
430  /// FFmpeg's heuristic best-effort PTS, or `None` if unknown.
431  #[cfg_attr(not(tarpaulin), inline(always))]
432  pub const fn best_effort_timestamp(&self) -> Option<i64> {
433    self.best_effort_timestamp
434  }
435  /// Returns the raw side-data entries.
436  #[cfg_attr(not(tarpaulin), inline(always))]
437  pub fn side_data(&self) -> &[SideDataEntry] {
438    self.side_data.as_slice()
439  }
440
441  /// Sets the best-effort timestamp (consuming builder).
442  #[cfg_attr(not(tarpaulin), inline(always))]
443  #[must_use]
444  pub const fn with_best_effort_timestamp(mut self, value: Option<i64>) -> Self {
445    self.best_effort_timestamp = value;
446    self
447  }
448  /// Sets the side-data list (consuming builder).
449  #[cfg_attr(not(tarpaulin), inline(always))]
450  #[must_use]
451  pub fn with_side_data(mut self, value: Vec<SideDataEntry>) -> Self {
452    self.side_data = value;
453    self
454  }
455
456  /// Sets the best-effort timestamp in place.
457  #[cfg_attr(not(tarpaulin), inline(always))]
458  pub const fn set_best_effort_timestamp(&mut self, value: Option<i64>) -> &mut Self {
459    self.best_effort_timestamp = value;
460    self
461  }
462  /// Sets the side-data list in place.
463  #[cfg_attr(not(tarpaulin), inline(always))]
464  pub fn set_side_data(&mut self, value: Vec<SideDataEntry>) -> &mut Self {
465    self.side_data = value;
466    self
467  }
468}
469
470/// Per-`SubtitlePacket` extras.
471#[derive(Clone, Debug, Default)]
472pub struct SubtitlePacketExtra {
473  stream_index: i32,
474  language: Option<[u8; 3]>,
475  forced: bool,
476  side_data: Vec<SideDataEntry>,
477}
478
479impl SubtitlePacketExtra {
480  /// Constructs a `SubtitlePacketExtra` with the given stream index.
481  /// `side_data` defaults to empty.
482  #[cfg_attr(not(tarpaulin), inline(always))]
483  pub const fn new(stream_index: i32) -> Self {
484    Self {
485      stream_index,
486      language: None,
487      forced: false,
488      side_data: Vec::new(),
489    }
490  }
491
492  /// Returns the source `AVStream.index`.
493  #[cfg_attr(not(tarpaulin), inline(always))]
494  pub const fn stream_index(&self) -> i32 {
495    self.stream_index
496  }
497  /// Returns the ISO 639-2/T language tag, or `None` if unspecified.
498  #[cfg_attr(not(tarpaulin), inline(always))]
499  pub const fn language(&self) -> Option<[u8; 3]> {
500    self.language
501  }
502  /// Returns whether this subtitle stream is marked "forced".
503  #[cfg_attr(not(tarpaulin), inline(always))]
504  pub const fn forced(&self) -> bool {
505    self.forced
506  }
507  /// Returns the raw side-data entries from `AVPacket.side_data`.
508  ///
509  /// A subtitle packet's side data is rare but not absent — and a
510  /// packet that carries *nothing else* is exactly the case this seat
511  /// exists for: with no seat, a side-data-only packet has nowhere to
512  /// put its only content.
513  #[cfg_attr(not(tarpaulin), inline(always))]
514  pub fn side_data(&self) -> &[SideDataEntry] {
515    self.side_data.as_slice()
516  }
517
518  /// Sets the stream index (consuming builder).
519  #[cfg_attr(not(tarpaulin), inline(always))]
520  #[must_use]
521  pub const fn with_stream_index(mut self, value: i32) -> Self {
522    self.stream_index = value;
523    self
524  }
525  /// Sets the language tag (consuming builder).
526  #[cfg_attr(not(tarpaulin), inline(always))]
527  #[must_use]
528  pub const fn with_language(mut self, value: Option<[u8; 3]>) -> Self {
529    self.language = value;
530    self
531  }
532  /// Sets the side-data list (consuming builder).
533  #[cfg_attr(not(tarpaulin), inline(always))]
534  #[must_use]
535  pub fn with_side_data(mut self, value: Vec<SideDataEntry>) -> Self {
536    self.side_data = value;
537    self
538  }
539  /// Sets the side-data list in place.
540  #[cfg_attr(not(tarpaulin), inline(always))]
541  pub fn set_side_data(&mut self, value: Vec<SideDataEntry>) -> &mut Self {
542    self.side_data = value;
543    self
544  }
545  /// Sets the forced flag (consuming builder).
546  #[cfg_attr(not(tarpaulin), inline(always))]
547  #[must_use]
548  pub const fn with_forced(mut self, value: bool) -> Self {
549    self.forced = value;
550    self
551  }
552
553  /// Sets the stream index in place.
554  #[cfg_attr(not(tarpaulin), inline(always))]
555  pub const fn set_stream_index(&mut self, value: i32) -> &mut Self {
556    self.stream_index = value;
557    self
558  }
559  /// Sets the language tag in place.
560  #[cfg_attr(not(tarpaulin), inline(always))]
561  pub const fn set_language(&mut self, value: Option<[u8; 3]>) -> &mut Self {
562    self.language = value;
563    self
564  }
565  /// Sets the forced flag in place.
566  #[cfg_attr(not(tarpaulin), inline(always))]
567  pub const fn set_forced(&mut self, value: bool) -> &mut Self {
568    self.forced = value;
569    self
570  }
571}
572
573/// Per-`SubtitleFrame` extras.
574#[derive(Clone, Debug, Default)]
575pub struct SubtitleFrameExtra {
576  start_display_time: u32,
577  end_display_time: u32,
578}
579
580impl SubtitleFrameExtra {
581  /// Constructs a `SubtitleFrameExtra`.
582  #[cfg_attr(not(tarpaulin), inline(always))]
583  pub const fn new(start_display_time: u32, end_display_time: u32) -> Self {
584    Self {
585      start_display_time,
586      end_display_time,
587    }
588  }
589
590  /// `AVSubtitle.start_display_time` — milliseconds from `pts`.
591  #[cfg_attr(not(tarpaulin), inline(always))]
592  pub const fn start_display_time(&self) -> u32 {
593    self.start_display_time
594  }
595  /// `AVSubtitle.end_display_time` — milliseconds from `pts`.
596  #[cfg_attr(not(tarpaulin), inline(always))]
597  pub const fn end_display_time(&self) -> u32 {
598    self.end_display_time
599  }
600
601  /// Sets the start display time (consuming builder).
602  #[cfg_attr(not(tarpaulin), inline(always))]
603  #[must_use]
604  pub const fn with_start_display_time(mut self, value: u32) -> Self {
605    self.start_display_time = value;
606    self
607  }
608  /// Sets the end display time (consuming builder).
609  #[cfg_attr(not(tarpaulin), inline(always))]
610  #[must_use]
611  pub const fn with_end_display_time(mut self, value: u32) -> Self {
612    self.end_display_time = value;
613    self
614  }
615
616  /// Sets the start display time in place.
617  #[cfg_attr(not(tarpaulin), inline(always))]
618  pub const fn set_start_display_time(&mut self, value: u32) -> &mut Self {
619    self.start_display_time = value;
620    self
621  }
622  /// Sets the end display time in place.
623  #[cfg_attr(not(tarpaulin), inline(always))]
624  pub const fn set_end_display_time(&mut self, value: u32) -> &mut Self {
625    self.end_display_time = value;
626    self
627  }
628}
629
630/// Where the first stored row and column of a still belong on screen —
631/// the eight orientations EXIF names, tags 1 through 8.
632///
633/// # How it reaches us
634///
635/// Not through `AVFrame.metadata`, which is where an earlier reading of
636/// libavcodec put it. **Measured**, against this build, by feeding
637/// mjpeg a JPEG whose EXIF IFD carries each orientation in turn: for a
638/// recognised tag (1..=8) the decoder emits an
639/// `AV_FRAME_DATA_DISPLAYMATRIX` frame side-data entry and puts
640/// *nothing* in the metadata dictionary; only an out-of-range tag (0,
641/// 9, …) is left to fall through to the dictionary, as the string
642/// `"      9"`. So the display matrix is the road, and the metadata
643/// dictionary is where malformed tags go to be ignored.
644///
645/// The matrix that arrives is the one below, in units of 65536 (the
646/// 16.16 fixed point `libavutil/display.h` specifies), with
647/// `(a, b, c, d)` the entries at indices 0, 1, 3, 4:
648///
649/// | tag | variant | a | b | c | d |
650/// |-----|---------|---|---|---|---|
651/// | 1 | [`TopLeft`](Self::TopLeft)         |  1 |  0 |  0 |  1 |
652/// | 2 | [`TopRight`](Self::TopRight)       | -1 |  0 |  0 |  1 |
653/// | 3 | [`BottomRight`](Self::BottomRight) | -1 |  0 |  0 | -1 |
654/// | 4 | [`BottomLeft`](Self::BottomLeft)   |  1 |  0 |  0 | -1 |
655/// | 5 | [`LeftTop`](Self::LeftTop)         |  0 |  1 |  1 |  0 |
656/// | 6 | [`RightTop`](Self::RightTop)       |  0 |  1 | -1 |  0 |
657/// | 7 | [`RightBottom`](Self::RightBottom) |  0 | -1 | -1 |  0 |
658/// | 8 | [`LeftBottom`](Self::LeftBottom)   |  0 | -1 |  1 |  0 |
659///
660/// # Why a vocabulary of its own
661///
662/// [`mediaframe::frame::Rotation`] is this crate's home for a quarter
663/// turn and is reused by [`Self::rotation`] — but it names four values
664/// and there are eight. The other four are *mirrored*, and a rotation
665/// vocabulary cannot hold a reflection. The same gap shows up one
666/// level down in FFmpeg's own API: `av_display_rotation_get` answers
667/// `-180` for **both** tag 2 and tag 3, `-90` for both 5 and 6, and
668/// `90` for both 7 and 8 — reading only the angle loses the mirror on
669/// half the vocabulary. This type is the eight-value reading that does
670/// not.
671#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Hash, IsVariant)]
672#[non_exhaustive]
673pub enum ImageOrientation {
674  /// Tag 1 — stored as displayed. The default and the overwhelming
675  /// majority of files.
676  #[default]
677  TopLeft,
678  /// Tag 2 — mirrored horizontally.
679  TopRight,
680  /// Tag 3 — turned half way round.
681  BottomRight,
682  /// Tag 4 — mirrored vertically.
683  BottomLeft,
684  /// Tag 5 — transposed (mirrored, then a quarter turn clockwise).
685  LeftTop,
686  /// Tag 6 — a quarter turn clockwise.
687  RightTop,
688  /// Tag 7 — transverse (mirrored, then three quarter turns
689  /// clockwise).
690  RightBottom,
691  /// Tag 8 — three quarter turns clockwise.
692  LeftBottom,
693  /// A display matrix none of the eight names, carried verbatim as
694  /// **all nine** of its words.
695  ///
696  /// Reachable: the display matrix is a general affine transform, and
697  /// a container (a MOV `tkhd`, a hand-built stream) may carry an
698  /// arbitrary one. Rather than answer `TopLeft` for a transform this
699  /// vocabulary cannot name — the silent-loss failure this crate
700  /// refuses everywhere else — the whole matrix rides along and
701  /// [`Self::to_exif_code`] admits it has no tag for it.
702  ///
703  /// **All nine words, not the four that carry the orientation.** The
704  /// escape's job is to lose nothing: a matrix with the right linear
705  /// part and a translation, or a perspective term, is *not* one of
706  /// the eight, and carrying only `[a, b, c, d]` would have thrown
707  /// away the very words that made it different. That is the same
708  /// collapse the escape exists to prevent, one level in.
709  Other([i32; 9]),
710}
711
712impl ImageOrientation {
713  /// One 16.16 fixed-point unit — the value a display-matrix entry
714  /// takes for ±1.
715  const UNIT: i32 = 1 << 16;
716
717  /// One 2.30 fixed-point unit — the value the matrix's `w` term takes
718  /// for 1, and the only value it takes for a picture that is merely
719  /// turned rather than projected.
720  const PERSPECTIVE_UNIT: i32 = 1 << 30;
721
722  /// The number of bytes an `AV_FRAME_DATA_DISPLAYMATRIX` entry
723  /// carries: nine `int32_t`, per `libavutil/display.h`.
724  pub const DISPLAY_MATRIX_BYTES: usize = 9 * core::mem::size_of::<i32>();
725
726  /// The nine-word matrix a named orientation stands for — the exact
727  /// inverse of what [`Self::from_display_matrix`] reads, and for
728  /// [`Self::Other`] the words it was handed, unchanged.
729  ///
730  /// `libavutil/display.h` lays the matrix out row-major as
731  ///
732  /// ```text
733  /// | a b u |     | 0 1 2 |
734  /// | c d v |  =  | 3 4 5 |
735  /// | x y w |     | 6 7 8 |
736  /// ```
737  ///
738  /// where `a b c d x y` are 16.16 fixed point and `u v w` are 2.30.
739  /// A named orientation puts the rotation-or-reflection in `a b c d`,
740  /// no translation in `x y`, no perspective in `u v`, and unity in
741  /// `w`.
742  #[cfg_attr(not(tarpaulin), inline(always))]
743  pub const fn matrix(&self) -> [i32; 9] {
744    match self {
745      Self::Other(matrix) => *matrix,
746      named => {
747        let [a, b, c, d] = named.linear();
748        [a, b, 0, c, d, 0, 0, 0, Self::PERSPECTIVE_UNIT]
749      }
750    }
751  }
752
753  /// Reads an orientation out of the raw bytes of an
754  /// `AV_FRAME_DATA_DISPLAYMATRIX` side-data entry.
755  ///
756  /// `None` when `bytes` is not exactly
757  /// [`Self::DISPLAY_MATRIX_BYTES`] long — a malformed entry is not an
758  /// orientation, and guessing one from a truncated matrix would be
759  /// the invention this seat exists to avoid.
760  ///
761  /// The entries are `int32_t` in **native** byte order: the side data
762  /// is a C array as it sits in memory, not a wire format.
763  pub fn from_display_matrix(bytes: &[u8]) -> Option<Self> {
764    if bytes.len() != Self::DISPLAY_MATRIX_BYTES {
765      return None;
766    }
767    let mut matrix = [0i32; 9];
768    for (index, word) in matrix.iter_mut().enumerate() {
769      let mut raw = [0u8; 4];
770      raw.copy_from_slice(&bytes[index * 4..index * 4 + 4]);
771      *word = i32::from_ne_bytes(raw);
772    }
773    Some(Self::from_matrix(matrix))
774  }
775
776  /// The orientation a whole nine-word matrix names. Anything but the
777  /// eight rides [`Self::Other`], whole.
778  ///
779  /// **A named variant requires the other five words to be canonical**
780  /// — no translation in `x`/`y`, no perspective in `u`/`v`, and unity
781  /// in `w` — not merely a linear part that matches. A matrix that
782  /// turns the picture *and* shifts it is not "turned"; answering
783  /// `RightTop` for it would drop the shift on the floor, which is the
784  /// collapse the escape exists to prevent.
785  fn from_matrix(matrix: [i32; 9]) -> Self {
786    const P: i32 = ImageOrientation::UNIT;
787    const N: i32 = -ImageOrientation::UNIT;
788    const W: i32 = ImageOrientation::PERSPECTIVE_UNIT;
789    match matrix {
790      [P, 0, 0, 0, P, 0, 0, 0, W] => Self::TopLeft,
791      [N, 0, 0, 0, P, 0, 0, 0, W] => Self::TopRight,
792      [N, 0, 0, 0, N, 0, 0, 0, W] => Self::BottomRight,
793      [P, 0, 0, 0, N, 0, 0, 0, W] => Self::BottomLeft,
794      [0, P, 0, P, 0, 0, 0, 0, W] => Self::LeftTop,
795      [0, P, 0, N, 0, 0, 0, 0, W] => Self::RightTop,
796      [0, N, 0, N, 0, 0, 0, 0, W] => Self::RightBottom,
797      [0, N, 0, P, 0, 0, 0, 0, W] => Self::LeftBottom,
798      other => Self::Other(other),
799    }
800  }
801
802  /// The EXIF tag value, 1 through 8.
803  ///
804  /// `None` for [`Self::Other`]: it names a transform EXIF has no tag
805  /// for, and there is no number to invent for it.
806  #[cfg_attr(not(tarpaulin), inline(always))]
807  pub const fn to_exif_code(&self) -> Option<u16> {
808    Some(match self {
809      Self::TopLeft => 1,
810      Self::TopRight => 2,
811      Self::BottomRight => 3,
812      Self::BottomLeft => 4,
813      Self::LeftTop => 5,
814      Self::RightTop => 6,
815      Self::RightBottom => 7,
816      Self::LeftBottom => 8,
817      Self::Other(_) => return None,
818    })
819  }
820
821  /// Decodes an EXIF tag value. `None` outside 1..=8 — never a silent
822  /// collapse onto [`Self::TopLeft`], which is what a viewer that
823  /// clamps an out-of-range tag ends up showing.
824  #[cfg_attr(not(tarpaulin), inline(always))]
825  pub const fn from_exif_code(code: u16) -> Option<Self> {
826    Some(match code {
827      1 => Self::TopLeft,
828      2 => Self::TopRight,
829      3 => Self::BottomRight,
830      4 => Self::BottomLeft,
831      5 => Self::LeftTop,
832      6 => Self::RightTop,
833      7 => Self::RightBottom,
834      8 => Self::LeftBottom,
835      _ => return None,
836    })
837  }
838
839  /// `true` when displaying the picture correctly requires a
840  /// reflection as well as a turn — EXIF tags 2, 4, 5 and 7.
841  ///
842  /// Read off the sign of the linear part's determinant, which is what
843  /// a reflection *is*, so [`Self::Other`] answers too.
844  #[cfg_attr(not(tarpaulin), inline(always))]
845  pub const fn is_mirrored(&self) -> bool {
846    let [a, b, c, d] = self.linear();
847    // Every entry is 0 or ±65536 for the eight named orientations, and
848    // an arbitrary matrix's entries are bounded by `i32`; widening to
849    // `i64` keeps the products exact for both.
850    (a as i64) * (d as i64) - (b as i64) * (c as i64) < 0
851  }
852
853  /// The quarter turn, clockwise, in the EXIF specification's
854  /// mirror-then-rotate decomposition — expressed in this workspace's
855  /// existing rotation vocabulary.
856  ///
857  /// `None` for [`Self::Other`], whose transform need not be a
858  /// multiple of 90° at all.
859  ///
860  /// This is the specification's decomposition, not a measurement:
861  /// what was measured here is which matrix carries which *tag* (see
862  /// the type's own docs), and the tag's meaning is EXIF's to define.
863  /// [`Self::is_mirrored`] supplies the half a [`Rotation`] cannot
864  /// hold.
865  #[cfg_attr(not(tarpaulin), inline(always))]
866  pub const fn rotation(&self) -> Option<Rotation> {
867    Some(match self {
868      Self::TopLeft | Self::TopRight => Rotation::D0,
869      Self::RightTop | Self::LeftTop => Rotation::D90,
870      Self::BottomRight | Self::BottomLeft => Rotation::D180,
871      Self::LeftBottom | Self::RightBottom => Rotation::D270,
872      Self::Other(_) => return None,
873    })
874  }
875
876  /// The linear part of the display matrix this orientation stands
877  /// for, `[a, b, c, d]` in 16.16 fixed point — matrix indices 0, 1, 3
878  /// and 4.
879  ///
880  /// A **projection**, not the whole value: for [`Self::Other`] it
881  /// drops the five words that made the matrix unnameable. Use
882  /// [`Self::matrix`] when nothing may be lost; this is for the
883  /// rotation-and-reflection question, which the four words answer on
884  /// their own.
885  #[cfg_attr(not(tarpaulin), inline(always))]
886  pub const fn linear(&self) -> [i32; 4] {
887    const P: i32 = ImageOrientation::UNIT;
888    const N: i32 = -ImageOrientation::UNIT;
889    match self {
890      Self::TopLeft => [P, 0, 0, P],
891      Self::TopRight => [N, 0, 0, P],
892      Self::BottomRight => [N, 0, 0, N],
893      Self::BottomLeft => [P, 0, 0, N],
894      Self::LeftTop => [0, P, P, 0],
895      Self::RightTop => [0, P, N, 0],
896      Self::RightBottom => [0, N, N, 0],
897      Self::LeftBottom => [0, N, P, 0],
898      Self::Other(matrix) => [matrix[0], matrix[1], matrix[3], matrix[4]],
899    }
900  }
901}
902
903/// Per-[`ImageFrame`](mediadecode::frame::ImageFrame) extras — cover
904/// art, an embedded thumbnail, a poster frame.
905///
906/// **One seat, deliberately.** This household is minimal at birth
907/// because a still image's `AVFrame` carries almost nothing a motion
908/// frame's does: no picture type, no field order, no best-effort
909/// timestamp, no SAR worth a seat. What it does carry is side data,
910/// and that is what is here.
911///
912/// **EXIF orientation takes a seat here**, read off the frame's
913/// `AV_FRAME_DATA_DISPLAYMATRIX` side data — see [`ImageOrientation`]
914/// for the measurement that establishes that road, and for why an
915/// earlier reading of libavcodec (which put orientation in
916/// `AVFrame.metadata`) was wrong about where it arrives.
917///
918/// **An ICC profile** arrives as side data too
919/// (`AV_FRAME_DATA_ICC_PROFILE`) and does *not* get a seat: it is a
920/// payload, not a fact — kilobytes of colour transform this crate has
921/// no vocabulary for and no business parsing. [`Self::side_data`]
922/// carries it unparsed and whole for a consumer that does. That is the
923/// line: a fact a picture cannot be displayed correctly without is
924/// typed; a payload only a specialist reads rides raw.
925#[derive(Clone, Debug, Default)]
926pub struct ImageFrameExtra {
927  orientation: Option<ImageOrientation>,
928  side_data: Vec<SideDataEntry>,
929}
930
931impl ImageFrameExtra {
932  /// Constructs an `ImageFrameExtra` with no orientation and no side
933  /// data.
934  #[cfg_attr(not(tarpaulin), inline(always))]
935  pub const fn new() -> Self {
936    Self {
937      orientation: None,
938      side_data: Vec::new(),
939    }
940  }
941
942  /// How the picture should be turned for display, when the file said.
943  ///
944  /// `None` means the frame carried no display matrix — the ordinary
945  /// case for a still with no EXIF orientation tag, and also what an
946  /// **out-of-range** tag produces, because libavcodec emits no matrix
947  /// for one. A malformed matrix (not nine `int32_t`) is `None` too.
948  /// In every one of those cases the raw entry, if there was one, is
949  /// still in [`Self::side_data`].
950  ///
951  /// A consumer that ignores this displays a sideways photograph.
952  #[cfg_attr(not(tarpaulin), inline(always))]
953  pub const fn orientation(&self) -> Option<ImageOrientation> {
954    self.orientation
955  }
956
957  /// Returns the raw side-data entries from `AVFrame.side_data` —
958  /// `AV_FRAME_DATA_ICC_PROFILE` and the display matrix among them.
959  #[cfg_attr(not(tarpaulin), inline(always))]
960  pub fn side_data(&self) -> &[SideDataEntry] {
961    self.side_data.as_slice()
962  }
963
964  /// Sets the orientation (consuming builder).
965  #[cfg_attr(not(tarpaulin), inline(always))]
966  #[must_use]
967  pub const fn with_orientation(mut self, value: Option<ImageOrientation>) -> Self {
968    self.orientation = value;
969    self
970  }
971
972  /// Sets the side-data list (consuming builder).
973  #[cfg_attr(not(tarpaulin), inline(always))]
974  #[must_use]
975  pub fn with_side_data(mut self, value: Vec<SideDataEntry>) -> Self {
976    self.side_data = value;
977    self
978  }
979
980  /// Sets the orientation in place.
981  #[cfg_attr(not(tarpaulin), inline(always))]
982  pub const fn set_orientation(&mut self, value: Option<ImageOrientation>) -> &mut Self {
983    self.orientation = value;
984    self
985  }
986
987  /// Sets the side-data list in place.
988  #[cfg_attr(not(tarpaulin), inline(always))]
989  pub fn set_side_data(&mut self, value: Vec<SideDataEntry>) -> &mut Self {
990    self.side_data = value;
991    self
992  }
993}
994
995/// Picture type per `AVFrame.pict_type`.
996#[derive(Copy, Clone, Debug, Default, Eq, PartialEq, Hash, IsVariant)]
997#[non_exhaustive]
998pub enum PictureType {
999  /// Unspecified / unset.
1000  #[default]
1001  Unspecified,
1002  /// Intra (I-frame).
1003  I,
1004  /// Predicted (P-frame).
1005  P,
1006  /// Bi-directional predicted (B-frame).
1007  B,
1008  /// S(GMC)-VOP from MPEG-4.
1009  S,
1010  /// Switching Intra (H.264).
1011  Si,
1012  /// Switching Predicted (H.264).
1013  Sp,
1014  /// Bi-predicted intra (BI-frame).
1015  Bi,
1016}
1017
1018/// Raw side-data entry carrying the FFmpeg type id and the unparsed
1019/// byte buffer. Type ids correspond to FFmpeg's
1020/// `AV_FRAME_DATA_*` / `AV_PKT_DATA_*` constants — see
1021/// `libavutil/frame.h` and `libavcodec/packet.h`.
1022///
1023/// The payload is an `FfmpegBytes` — see the [module docs](self) for the
1024/// reason it is not a `Vec<u8>`.
1025#[derive(Clone, Debug)]
1026pub struct SideDataEntry {
1027  kind: i32,
1028  data: FfmpegBytes,
1029}
1030
1031impl SideDataEntry {
1032  /// Constructs a `SideDataEntry`.
1033  #[cfg_attr(not(tarpaulin), inline(always))]
1034  pub const fn new(kind: i32, data: FfmpegBytes) -> Self {
1035    Self { kind, data }
1036  }
1037
1038  /// FFmpeg side-data type id.
1039  #[cfg_attr(not(tarpaulin), inline(always))]
1040  pub const fn kind(&self) -> i32 {
1041    self.kind
1042  }
1043  /// Side-data payload as raw bytes.
1044  #[cfg_attr(not(tarpaulin), inline(always))]
1045  pub fn data(&self) -> &[u8] {
1046    self.data.as_slice()
1047  }
1048  /// The payload's carrier, for a consumer that wants to keep the
1049  /// bytes without copying them again.
1050  #[cfg_attr(not(tarpaulin), inline(always))]
1051  pub const fn data_ref(&self) -> &FfmpegBytes {
1052    &self.data
1053  }
1054
1055  /// Sets the type id (consuming builder).
1056  #[cfg_attr(not(tarpaulin), inline(always))]
1057  #[must_use]
1058  pub const fn with_kind(mut self, value: i32) -> Self {
1059    self.kind = value;
1060    self
1061  }
1062  /// Sets the payload (consuming builder).
1063  #[cfg_attr(not(tarpaulin), inline(always))]
1064  #[must_use]
1065  pub fn with_data(mut self, value: FfmpegBytes) -> Self {
1066    self.data = value;
1067    self
1068  }
1069
1070  /// Sets the type id in place.
1071  #[cfg_attr(not(tarpaulin), inline(always))]
1072  pub const fn set_kind(&mut self, value: i32) -> &mut Self {
1073    self.kind = value;
1074    self
1075  }
1076  /// Sets the payload in place.
1077  #[cfg_attr(not(tarpaulin), inline(always))]
1078  pub fn set_data(&mut self, value: FfmpegBytes) -> &mut Self {
1079    self.data = value;
1080    self
1081  }
1082}
1083
1084/// HDR10 mastering display metadata.
1085#[derive(Copy, Clone, Debug, PartialEq)]
1086pub struct MasteringDisplay {
1087  display_primaries: [(u32, u32); 3],
1088  white_point: (u32, u32),
1089  max_luminance: (u32, u32),
1090  min_luminance: (u32, u32),
1091}
1092
1093impl MasteringDisplay {
1094  /// Constructs a `MasteringDisplay`.
1095  #[cfg_attr(not(tarpaulin), inline(always))]
1096  pub const fn new(
1097    display_primaries: [(u32, u32); 3],
1098    white_point: (u32, u32),
1099    max_luminance: (u32, u32),
1100    min_luminance: (u32, u32),
1101  ) -> Self {
1102    Self {
1103      display_primaries,
1104      white_point,
1105      max_luminance,
1106      min_luminance,
1107    }
1108  }
1109
1110  /// Display primary chromaticities `(x, y)` for R, G, B in CIE 1931,
1111  /// each coordinate normalized to SMPTE ST 2086 fixed-point units —
1112  /// 0.00002 increments, so a floating value is `raw / 50000.0`. The
1113  /// same units [`mediaframe::color::ChromaCoord`] uses, so a `(x, y)`
1114  /// pair here constructs one directly.
1115  ///
1116  /// A tuple has room for one denominator, not two, so it cannot hold
1117  /// `x` and `y` each as an independent `AVRational` the way the
1118  /// source `AVMasteringDisplayMetadata` does — `parse_mastering_
1119  /// display` (`mediadecode-ffmpeg`'s `convert` module) is the one
1120  /// place that resolves each coordinate's own rational to this
1121  /// shared fixed-point unit, exactly rather than by rounding, for
1122  /// every producer this crate has observed (which all emit a
1123  /// denominator of `50000` already).
1124  #[cfg_attr(not(tarpaulin), inline(always))]
1125  pub const fn display_primaries(&self) -> [(u32, u32); 3] {
1126    self.display_primaries
1127  }
1128  /// White-point chromaticity `(x, y)`, in the same SMPTE ST 2086
1129  /// fixed-point units as [`Self::display_primaries`].
1130  #[cfg_attr(not(tarpaulin), inline(always))]
1131  pub const fn white_point(&self) -> (u32, u32) {
1132    self.white_point
1133  }
1134  /// Maximum luminance in `0.0001 cd/m²` units (rational `(num, den)`).
1135  #[cfg_attr(not(tarpaulin), inline(always))]
1136  pub const fn max_luminance(&self) -> (u32, u32) {
1137    self.max_luminance
1138  }
1139  /// Minimum luminance in `0.0001 cd/m²` units.
1140  #[cfg_attr(not(tarpaulin), inline(always))]
1141  pub const fn min_luminance(&self) -> (u32, u32) {
1142    self.min_luminance
1143  }
1144
1145  /// Sets the display primaries (consuming builder).
1146  #[cfg_attr(not(tarpaulin), inline(always))]
1147  pub const fn with_display_primaries(mut self, value: [(u32, u32); 3]) -> Self {
1148    self.display_primaries = value;
1149    self
1150  }
1151  /// Sets the white point (consuming builder).
1152  #[cfg_attr(not(tarpaulin), inline(always))]
1153  pub const fn with_white_point(mut self, value: (u32, u32)) -> Self {
1154    self.white_point = value;
1155    self
1156  }
1157  /// Sets the max luminance (consuming builder).
1158  #[cfg_attr(not(tarpaulin), inline(always))]
1159  pub const fn with_max_luminance(mut self, value: (u32, u32)) -> Self {
1160    self.max_luminance = value;
1161    self
1162  }
1163  /// Sets the min luminance (consuming builder).
1164  #[cfg_attr(not(tarpaulin), inline(always))]
1165  pub const fn with_min_luminance(mut self, value: (u32, u32)) -> Self {
1166    self.min_luminance = value;
1167    self
1168  }
1169
1170  /// Sets the display primaries in place.
1171  #[cfg_attr(not(tarpaulin), inline(always))]
1172  pub const fn set_display_primaries(&mut self, value: [(u32, u32); 3]) -> &mut Self {
1173    self.display_primaries = value;
1174    self
1175  }
1176  /// Sets the white point in place.
1177  #[cfg_attr(not(tarpaulin), inline(always))]
1178  pub const fn set_white_point(&mut self, value: (u32, u32)) -> &mut Self {
1179    self.white_point = value;
1180    self
1181  }
1182  /// Sets the max luminance in place.
1183  #[cfg_attr(not(tarpaulin), inline(always))]
1184  pub const fn set_max_luminance(&mut self, value: (u32, u32)) -> &mut Self {
1185    self.max_luminance = value;
1186    self
1187  }
1188  /// Sets the min luminance in place.
1189  #[cfg_attr(not(tarpaulin), inline(always))]
1190  pub const fn set_min_luminance(&mut self, value: (u32, u32)) -> &mut Self {
1191    self.min_luminance = value;
1192    self
1193  }
1194}
1195
1196/// HDR10 content light level (`AV_FRAME_DATA_CONTENT_LIGHT_LEVEL`).
1197#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Default)]
1198pub struct ContentLightLevel {
1199  max_cll: u32,
1200  max_fall: u32,
1201}
1202
1203impl ContentLightLevel {
1204  /// Constructs a `ContentLightLevel`.
1205  #[cfg_attr(not(tarpaulin), inline(always))]
1206  pub const fn new(max_cll: u32, max_fall: u32) -> Self {
1207    Self { max_cll, max_fall }
1208  }
1209
1210  /// Maximum content light level (cd/m²).
1211  #[cfg_attr(not(tarpaulin), inline(always))]
1212  pub const fn max_cll(&self) -> u32 {
1213    self.max_cll
1214  }
1215  /// Maximum frame-average light level (cd/m²).
1216  #[cfg_attr(not(tarpaulin), inline(always))]
1217  pub const fn max_fall(&self) -> u32 {
1218    self.max_fall
1219  }
1220
1221  /// Sets `max_cll` (consuming builder).
1222  #[cfg_attr(not(tarpaulin), inline(always))]
1223  #[must_use]
1224  pub const fn with_max_cll(mut self, value: u32) -> Self {
1225    self.max_cll = value;
1226    self
1227  }
1228  /// Sets `max_fall` (consuming builder).
1229  #[cfg_attr(not(tarpaulin), inline(always))]
1230  #[must_use]
1231  pub const fn with_max_fall(mut self, value: u32) -> Self {
1232    self.max_fall = value;
1233    self
1234  }
1235
1236  /// Sets `max_cll` in place.
1237  #[cfg_attr(not(tarpaulin), inline(always))]
1238  pub const fn set_max_cll(&mut self, value: u32) -> &mut Self {
1239    self.max_cll = value;
1240    self
1241  }
1242  /// Sets `max_fall` in place.
1243  #[cfg_attr(not(tarpaulin), inline(always))]
1244  pub const fn set_max_fall(&mut self, value: u32) -> &mut Self {
1245    self.max_fall = value;
1246    self
1247  }
1248}
1249
1250// ---------------------------------------------------------------------------
1251//  The demux tier's carriers.
1252// ---------------------------------------------------------------------------
1253
1254/// Per-`DataPacket` extras — timecode, KLV, timed ID3.
1255///
1256/// The same three seats as [`VideoPacketExtra`]. The side-data list was
1257/// left off at first — data demuxers carry their whole payload in the
1258/// packet body — and then earned its place: a packet with no body and
1259/// only side data is a real packet, and without this seat its only
1260/// content would have nowhere to go.
1261#[derive(Clone, Debug, Default)]
1262pub struct DataPacketExtra {
1263  stream_index: i32,
1264  byte_pos: Option<i64>,
1265  side_data: Vec<SideDataEntry>,
1266}
1267
1268impl DataPacketExtra {
1269  /// Constructs a `DataPacketExtra` with the given stream index.
1270  /// `byte_pos` defaults to `None` and `side_data` to empty.
1271  #[cfg_attr(not(tarpaulin), inline(always))]
1272  pub const fn new(stream_index: i32) -> Self {
1273    Self {
1274      stream_index,
1275      byte_pos: None,
1276      side_data: Vec::new(),
1277    }
1278  }
1279
1280  /// Returns the source `AVStream.index`.
1281  #[cfg_attr(not(tarpaulin), inline(always))]
1282  pub const fn stream_index(&self) -> i32 {
1283    self.stream_index
1284  }
1285  /// Returns the byte position of the packet in the input file, or
1286  /// `None` if unknown.
1287  #[cfg_attr(not(tarpaulin), inline(always))]
1288  pub const fn byte_pos(&self) -> Option<i64> {
1289    self.byte_pos
1290  }
1291  /// Returns the raw side-data entries from `AVPacket.side_data`.
1292  #[cfg_attr(not(tarpaulin), inline(always))]
1293  pub fn side_data(&self) -> &[SideDataEntry] {
1294    self.side_data.as_slice()
1295  }
1296
1297  /// Sets the stream index (consuming builder).
1298  #[cfg_attr(not(tarpaulin), inline(always))]
1299  #[must_use]
1300  pub const fn with_stream_index(mut self, value: i32) -> Self {
1301    self.stream_index = value;
1302    self
1303  }
1304  /// Sets the byte position (consuming builder).
1305  #[cfg_attr(not(tarpaulin), inline(always))]
1306  #[must_use]
1307  pub const fn with_byte_pos(mut self, value: Option<i64>) -> Self {
1308    self.byte_pos = value;
1309    self
1310  }
1311  /// Sets the side-data list (consuming builder).
1312  #[cfg_attr(not(tarpaulin), inline(always))]
1313  #[must_use]
1314  pub fn with_side_data(mut self, value: Vec<SideDataEntry>) -> Self {
1315    self.side_data = value;
1316    self
1317  }
1318
1319  /// Sets the stream index in place.
1320  #[cfg_attr(not(tarpaulin), inline(always))]
1321  pub const fn set_stream_index(&mut self, value: i32) -> &mut Self {
1322    self.stream_index = value;
1323    self
1324  }
1325  /// Sets the byte position in place.
1326  #[cfg_attr(not(tarpaulin), inline(always))]
1327  pub const fn set_byte_pos(&mut self, value: Option<i64>) -> &mut Self {
1328    self.byte_pos = value;
1329    self
1330  }
1331  /// Sets the side-data list in place.
1332  #[cfg_attr(not(tarpaulin), inline(always))]
1333  pub fn set_side_data(&mut self, value: Vec<SideDataEntry>) -> &mut Self {
1334    self.side_data = value;
1335    self
1336  }
1337}
1338
1339/// Per-`AttachmentPacket` extras — fonts, cover art.
1340///
1341/// `synthesized` records where the payload came from, which is not a
1342/// detail: an attachment track's single packet is either a real packet
1343/// the container stores (cover art, which libavformat parks in
1344/// `AVStream.attached_pic`) or one this crate builds out of the
1345/// track's codec extradata (fonts, whose bytes never appear in the
1346/// packet stream at all). A consumer chasing a payload that looks
1347/// wrong needs to know which.
1348#[derive(Clone, Debug, Default)]
1349pub struct AttachmentPacketExtra {
1350  stream_index: i32,
1351  synthesized: bool,
1352}
1353
1354impl AttachmentPacketExtra {
1355  /// Constructs an `AttachmentPacketExtra` with the given stream index.
1356  /// `synthesized` defaults to `false`.
1357  #[cfg_attr(not(tarpaulin), inline(always))]
1358  pub const fn new(stream_index: i32) -> Self {
1359    Self {
1360      stream_index,
1361      synthesized: false,
1362    }
1363  }
1364
1365  /// Returns the source `AVStream.index`.
1366  #[cfg_attr(not(tarpaulin), inline(always))]
1367  pub const fn stream_index(&self) -> i32 {
1368    self.stream_index
1369  }
1370  /// `true` when the payload was built from the track's codec
1371  /// extradata rather than taken from a packet the container stores.
1372  #[cfg_attr(not(tarpaulin), inline(always))]
1373  pub const fn synthesized(&self) -> bool {
1374    self.synthesized
1375  }
1376
1377  /// Sets the stream index (consuming builder).
1378  #[cfg_attr(not(tarpaulin), inline(always))]
1379  #[must_use]
1380  pub const fn with_stream_index(mut self, value: i32) -> Self {
1381    self.stream_index = value;
1382    self
1383  }
1384  /// Sets the synthesized flag (consuming builder).
1385  #[cfg_attr(not(tarpaulin), inline(always))]
1386  #[must_use]
1387  pub const fn with_synthesized(mut self, value: bool) -> Self {
1388    self.synthesized = value;
1389    self
1390  }
1391
1392  /// Sets the stream index in place.
1393  #[cfg_attr(not(tarpaulin), inline(always))]
1394  pub const fn set_stream_index(&mut self, value: i32) -> &mut Self {
1395    self.stream_index = value;
1396    self
1397  }
1398  /// Sets the synthesized flag in place.
1399  #[cfg_attr(not(tarpaulin), inline(always))]
1400  pub const fn set_synthesized(&mut self, value: bool) -> &mut Self {
1401    self.synthesized = value;
1402    self
1403  }
1404}
1405
1406/// The heap bytes an `AVCodecParameters` holds — measured, never
1407/// allocated.
1408///
1409/// See [`bounded_clone_parameters`] for the rule this measurement
1410/// serves and for the field-by-field inventory it is derived from.
1411#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
1412pub struct ParameterFootprint {
1413  extradata: usize,
1414  extradata_payload: usize,
1415  coded_side_data: usize,
1416  channel_map: usize,
1417}
1418
1419impl ParameterFootprint {
1420  /// Bytes a copy of `extradata` allocates — the payload **plus** the
1421  /// `AV_INPUT_BUFFER_PADDING_SIZE` trailing zeroes decoders read past
1422  /// the end into.
1423  ///
1424  /// The padding is counted because it is allocated: a ceiling that
1425  /// measured only the payload admitted a copy sixty-four bytes larger
1426  /// than it agreed to, once per stream. Zero when there is no
1427  /// extradata at all — no payload, no padding.
1428  #[cfg_attr(not(tarpaulin), inline(always))]
1429  pub const fn extradata(&self) -> usize {
1430    self.extradata
1431  }
1432
1433  /// The `extradata_size` the parameters declare — the payload alone,
1434  /// with no padding.
1435  ///
1436  /// [`Self::extradata`] is what a *parameter clone* costs, padding
1437  /// included. This is what a **carrier** costs: the synthesized
1438  /// attachment road copies these bytes into an [`FfmpegBytes`], which
1439  /// allocates exactly them, so charging the padded figure against the
1440  /// attachment budget would bill sixty-four bytes nobody allocates —
1441  /// and reject a payload sitting in the last sixty-four bytes below
1442  /// the ceiling that the image road, judging the same bytes, accepts.
1443  #[cfg_attr(not(tarpaulin), inline(always))]
1444  pub const fn extradata_payload(&self) -> usize {
1445    self.extradata_payload
1446  }
1447  /// Bytes across every `coded_side_data` entry, plus the array of
1448  /// descriptors itself.
1449  #[cfg_attr(not(tarpaulin), inline(always))]
1450  pub const fn coded_side_data(&self) -> usize {
1451    self.coded_side_data
1452  }
1453  /// Bytes in a custom channel map, or zero for every other layout
1454  /// order.
1455  #[cfg_attr(not(tarpaulin), inline(always))]
1456  pub const fn channel_map(&self) -> usize {
1457    self.channel_map
1458  }
1459  /// Everything a clone of these parameters would retain.
1460  ///
1461  /// `None` on overflow — a set of declared sizes that cannot be added
1462  /// up is not a set anything should try to copy.
1463  #[cfg_attr(not(tarpaulin), inline(always))]
1464  pub const fn total(&self) -> Option<usize> {
1465    match self.extradata.checked_add(self.coded_side_data) {
1466      Some(sum) => sum.checked_add(self.channel_map),
1467      None => None,
1468    }
1469  }
1470
1471  /// The same total with `extradata` left out — what a clone retains
1472  /// when the caller is going to strip it.
1473  ///
1474  /// The synthesized-attachment road does exactly that: a font's
1475  /// extradata *is* its payload, the carrier already holds it, and
1476  /// counting it twice would make the budget describe the file rather
1477  /// than the memory.
1478  #[cfg_attr(not(tarpaulin), inline(always))]
1479  pub const fn total_without_extradata(&self) -> Option<usize> {
1480    self.coded_side_data.checked_add(self.channel_map)
1481  }
1482}
1483
1484/// `AVCodecParameters` has thirty-two fields in FFmpeg n9.0 and
1485/// exactly three of them reach the heap. This tripwire fires if the
1486/// struct changes shape, because [`measure_parameters`] and
1487/// [`bounded_clone_parameters`] enumerate those three **by hand** and a
1488/// fourth would silently go uncounted and uncopied.
1489///
1490/// [`CodecTicket`](crate::ticket::CodecTicket) raises the stake: it
1491/// enumerates **every** field by hand, heap and scalar alike, so a new
1492/// *scalar* — which the bounded clone's bytewise sweep would have
1493/// carried for free — is now a seat the mirror would drop. A field
1494/// that is removed fails the build on its own; a field that is added
1495/// fails it here.
1496///
1497/// A size assertion is a tripwire, not a proof: it catches a struct
1498/// that grew or was reordered, which is how a new seat arrives in
1499/// practice. Gated to 64-bit because the number is pointer-width
1500/// dependent and every target this crate links FFmpeg on is 64-bit;
1501/// elsewhere the hand-written clone still runs, it just loses the
1502/// alarm.
1503#[cfg(target_pointer_width = "64")]
1504const _: () = {
1505  assert!(
1506    core::mem::size_of::<ffmpeg_next::ffi::AVCodecParameters>() == 184,
1507    "AVCodecParameters changed shape — re-census its fields against \
1508     `measure_parameters`, `bounded_clone_parameters` and `ticket::CodecTicket` \
1509     before raising this",
1510  );
1511};
1512
1513/// Measures an `AVCodecParameters`' heap seats without allocating
1514/// anything.
1515///
1516/// `None` when a declared size cannot be added up — a count or length
1517/// whose arithmetic overflows is malformed, and refusing beats
1518/// saturating into a number that then passes a budget.
1519///
1520/// # Safety
1521///
1522/// `par` must be a live `*const AVCodecParameters` for the duration of
1523/// this call.
1524pub(crate) unsafe fn measure_parameters(
1525  par: *const ffmpeg_next::ffi::AVCodecParameters,
1526) -> Option<ParameterFootprint> {
1527  // SAFETY: `par` is live per the contract; `extradata` and
1528  // `extradata_size` are a pointer and an integer.
1529  let extradata_payload = if unsafe { (*par).extradata }.is_null() {
1530    0
1531  } else {
1532    usize::try_from(unsafe { (*par).extradata_size }).ok()?
1533  };
1534  // The padding a *clone* allocates is part of what a clone costs. A
1535  // carrier of the same bytes allocates only the payload, which is why
1536  // both numbers are kept.
1537  let extradata = if extradata_payload == 0 {
1538    0
1539  } else {
1540    extradata_payload.checked_add(ffmpeg_next::ffi::AV_INPUT_BUFFER_PADDING_SIZE as usize)?
1541  };
1542
1543  let side_data_ptr = unsafe { (*par).coded_side_data };
1544  let side_data_count = unsafe { (*par).nb_coded_side_data };
1545  let coded_side_data = if side_data_ptr.is_null() || side_data_count <= 0 {
1546    0
1547  } else {
1548    let count = usize::try_from(side_data_count).ok()?;
1549    let mut total = count.checked_mul(core::mem::size_of::<ffmpeg_next::ffi::AVPacketSideData>())?;
1550    for index in 0..count {
1551      // **Never `&*entry`.** `AVPacketSideData` carries a `type` field
1552      // of an *open* C enum: FFmpeg adds side-data kinds between
1553      // releases, and an ABI-compatible library newer than the
1554      // bindings this crate was built against will emit values absent
1555      // from the generated Rust enum. Forming a typed reference to such
1556      // a struct asserts every field inhabits its declared type, which
1557      // is undefined behaviour before a single field is read. Only the
1558      // `size` field is needed here, and it is reached through
1559      // `addr_of!` and read as the plain `usize` it is.
1560      //
1561      // SAFETY: the array is valid for `nb_coded_side_data` contiguous
1562      // entries per FFmpeg's contract, `index` is below that count, and
1563      // `addr_of!` computes a field address without forming a reference
1564      // to the struct that contains it.
1565      let size =
1566        unsafe { core::ptr::read_unaligned(core::ptr::addr_of!((*side_data_ptr.add(index)).size)) };
1567      total = total.checked_add(size)?;
1568    }
1569    total
1570  };
1571
1572  let channel_map = {
1573    // The channel order is an open enum too, and this one decides
1574    // whether the layout owns a heap allocation at all. Read as the
1575    // integer it is on the wire and compared against the orders whose
1576    // heap semantics this crate has censused; anything else **fails
1577    // closed**, because a future order might own memory nobody here
1578    // knows how to measure and guessing zero would admit it unbudgeted.
1579    //
1580    // SAFETY: `ch_layout` is embedded by value; `addr_of!` reaches
1581    // `order` without forming a reference to the layout, and the field
1582    // has the layout of a `c_int`.
1583    let order = unsafe {
1584      core::ptr::read_unaligned(core::ptr::addr_of!((*par).ch_layout.order).cast::<i32>())
1585    };
1586    const UNSPEC: i32 = ffmpeg_next::ffi::AVChannelOrder::AV_CHANNEL_ORDER_UNSPEC as i32;
1587    const NATIVE: i32 = ffmpeg_next::ffi::AVChannelOrder::AV_CHANNEL_ORDER_NATIVE as i32;
1588    const CUSTOM: i32 = ffmpeg_next::ffi::AVChannelOrder::AV_CHANNEL_ORDER_CUSTOM as i32;
1589    const AMBISONIC: i32 = ffmpeg_next::ffi::AVChannelOrder::AV_CHANNEL_ORDER_AMBISONIC as i32;
1590    match order {
1591      // These three describe their channels with the union's `mask`
1592      // arm, which owns nothing.
1593      UNSPEC | NATIVE | AMBISONIC => 0,
1594      // Only this one owns a heap map.
1595      CUSTOM => {
1596        let channels = usize::try_from(unsafe { (*par).ch_layout.nb_channels }).ok()?;
1597        channels.checked_mul(core::mem::size_of::<ffmpeg_next::ffi::AVChannelCustom>())?
1598      }
1599      // An order this build has never heard of. Refusing is the only
1600      // honest answer: its union arm is unknown, so both "it owns
1601      // nothing" and "it owns `nb_channels` of something" are guesses.
1602      _ => return None,
1603    }
1604  };
1605
1606  Some(ParameterFootprint {
1607    extradata,
1608    extradata_payload,
1609    coded_side_data,
1610    channel_map,
1611  })
1612}
1613
1614/// Whether a parameter clone carries `extradata` across.
1615///
1616/// The synthesized-attachment road omits it: a font's `extradata`
1617/// **is** the attachment payload, the carrier already holds it, and a
1618/// second copy is residency nobody asked for.
1619///
1620/// **Omitted from the outset, never stripped afterwards.** Stripping
1621/// was the shape that shipped first and it was wrong twice over: the
1622/// bytes were allocated before being freed, and — worse — the clone
1623/// measured and charged them against the *parameter* ceiling on the
1624/// way past. A payload sitting between the two ceilings (over the
1625/// 16 MiB parameter one, under the 64 MiB attachment one) therefore
1626/// passed the session's admission and then failed deterministically
1627/// inside the clone, refusing a file the budgets had already agreed to
1628/// open. Omitting from the outset makes the clone's accounting the
1629/// same accounting the admission did.
1630#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Hash)]
1631pub(crate) enum ExtradataPolicy {
1632  /// Copy `extradata` across — every road but one.
1633  #[default]
1634  Copy,
1635  /// Leave `extradata` behind: never allocated, never charged.
1636  Omit,
1637}
1638
1639/// The `ENOMEM` a heap seat's copy reports when its allocation fails.
1640///
1641/// [`DemuxError::ParametersAlloc`] names the *destination struct*
1642/// failing to allocate; a seat that fails after that is the copy
1643/// failing part way, which is what
1644/// [`DemuxError::ParametersCopy`] was minted for and what
1645/// `avcodec_parameters_copy` itself would have returned here.
1646fn seat_copy_failed(stream_index: usize) -> DemuxError {
1647  DemuxError::ParametersCopy(ParametersCopy::new(
1648    stream_index,
1649    ffmpeg_next::Error::Other {
1650      errno: libc::ENOMEM,
1651    },
1652  ))
1653}
1654
1655/// A deep copy of codec parameters in which **every heap field passes a
1656/// budget before it is copied**.
1657///
1658/// # The rule this exists to enforce
1659///
1660/// > **No code path in this crate hands attacker-sized parameter data
1661/// > to a wholesale FFI copy.**
1662///
1663/// `avcodec_parameters_copy` is that wholesale copy, and it was the
1664/// same defect three review rounds running. It deep-copies every heap
1665/// field an `AVCodecParameters` has — `extradata`, every
1666/// `coded_side_data` entry, a custom channel map — and it does so
1667/// before anything has asked how big they are. The first two rounds
1668/// patched the field that had been noticed; the third found another
1669/// (`coded_side_data`, where a MOV `prof` atom puts an ICC profile),
1670/// which is the signal that patching fields is not a fix. So the
1671/// wholesale copy is gone from every attacker-facing path and this
1672/// function replaces it.
1673///
1674/// The shape that makes the class unable to recur: the heap seats are
1675/// enumerated **by hand**, each is measured before it is copied, and
1676/// the whole footprint is admitted against a budget first. A field
1677/// nobody enumerated is a field that is not copied — which loses data
1678/// rather than allocating it — and the compile-time size tripwire above
1679/// fires when one appears.
1680///
1681/// # What it copies
1682///
1683/// Every scalar, by a bytewise copy of the struct — so a *scalar* field
1684/// a future FFmpeg adds travels for free. Then the three heap seats are
1685/// nulled on the destination (the bytewise copy left them aliasing the
1686/// source) and rebuilt one at a time:
1687///
1688/// 1. `extradata`, with the `AV_INPUT_BUFFER_PADDING_SIZE` trailing
1689///    zeroes decoders read past the end into;
1690/// 2. `coded_side_data` — the descriptor array, then each entry's
1691///    payload;
1692/// 3. `ch_layout`, through `av_channel_layout_copy`, whose one
1693///    allocation is the custom map this function has already measured.
1694///
1695/// Nothing a decoder consumes is dropped: the SPS/PPS in `extradata`,
1696/// the side data codecs read, and the channel layout all survive, which
1697/// is what the decode, resample and image suites prove by continuing to
1698/// pass unchanged.
1699pub(crate) fn bounded_clone_parameters(
1700  source: &Parameters,
1701  stream_index: usize,
1702  budget: usize,
1703) -> Result<Parameters, DemuxError> {
1704  bounded_clone_parameters_with(source, stream_index, budget, ExtradataPolicy::Copy)
1705}
1706
1707/// [`bounded_clone_parameters`], with the `extradata` policy named.
1708pub(crate) fn bounded_clone_parameters_with(
1709  source: &Parameters,
1710  stream_index: usize,
1711  budget: usize,
1712  extradata_policy: ExtradataPolicy,
1713) -> Result<Parameters, DemuxError> {
1714  // The *source* first. `Parameters::new()` and `Parameters::default()`
1715  // hand back a value whose pointer is null when
1716  // `avcodec_parameters_alloc` failed — safe code, no error, no way to
1717  // tell — and every read below dereferences it.
1718  // SAFETY: reading the pointer without dereferencing it.
1719  let src = unsafe { source.as_ptr() };
1720  if src.is_null() {
1721    return Err(DemuxError::ParametersMissing(ParametersMissing::new(
1722      stream_index,
1723    )));
1724  }
1725
1726  // SAFETY: `src` is a live `AVCodecParameters` owned by `source` for
1727  // the duration of this call.
1728  let footprint = unsafe { measure_parameters(src) }.ok_or_else(|| {
1729    DemuxError::ParametersTooLarge(ParametersTooLarge::new(stream_index, usize::MAX, budget))
1730  })?;
1731  // Charged for what this clone will actually retain, which is what
1732  // the session's admission pass charged for too — see
1733  // [`ExtradataPolicy`] for the interval that disagreeing about this
1734  // used to lose.
1735  let total = match extradata_policy {
1736    ExtradataPolicy::Copy => footprint.total(),
1737    ExtradataPolicy::Omit => footprint.total_without_extradata(),
1738  }
1739  .ok_or_else(|| {
1740    DemuxError::ParametersTooLarge(ParametersTooLarge::new(stream_index, usize::MAX, budget))
1741  })?;
1742  if total > budget {
1743    return Err(DemuxError::ParametersTooLarge(ParametersTooLarge::new(
1744      stream_index,
1745      total,
1746      budget,
1747    )));
1748  }
1749
1750  let mut out = Parameters::new();
1751  // SAFETY: reading the pointer the constructor stored without
1752  // dereferencing it — which is exactly what the check is for.
1753  let dst = unsafe { out.as_mut_ptr() };
1754  if dst.is_null() {
1755    return Err(DemuxError::ParametersAlloc(ParametersAlloc::new(
1756      stream_index,
1757    )));
1758  }
1759
1760  // SAFETY: `src` and `dst` are both live, non-null, distinct
1761  // `AVCodecParameters` allocations. The bytewise copy carries every
1762  // scalar across — including any this crate has never heard of — and
1763  // leaves the three pointer seats aliasing `src`, which the very next
1764  // statements overwrite before anything can observe or free them.
1765  unsafe {
1766    core::ptr::copy_nonoverlapping(src, dst, 1);
1767    (*dst).extradata = core::ptr::null_mut();
1768    (*dst).extradata_size = 0;
1769    (*dst).coded_side_data = core::ptr::null_mut();
1770    (*dst).nb_coded_side_data = 0;
1771    // `ch_layout` is zeroed rather than nulled field-by-field:
1772    // `AV_CHANNEL_ORDER_UNSPEC` is `0`, so an all-zero layout is the
1773    // valid "nothing here yet" state `av_channel_layout_copy` expects
1774    // to be handed, and it owns no map.
1775    (*dst).ch_layout = core::mem::zeroed();
1776  }
1777
1778  // 1. extradata, with the padding decoders read into — unless the
1779  // caller asked for it to be left behind, in which case nothing is
1780  // allocated and the destination keeps the null the cleanup above put
1781  // there. `footprint.extradata()` already counts that padding.
1782  if footprint.extradata() > 0 && matches!(extradata_policy, ExtradataPolicy::Copy) {
1783    let padded = footprint.extradata();
1784    // SAFETY: `av_mallocz` returns zeroed memory or null; the copy
1785    // writes exactly the measured length into an allocation that is
1786    // `AV_INPUT_BUFFER_PADDING_SIZE` longer, leaving the padding zero.
1787    unsafe {
1788      let buffer = ffmpeg_next::ffi::av_mallocz(padded) as *mut u8;
1789      if buffer.is_null() {
1790        return Err(seat_copy_failed(stream_index));
1791      }
1792      let payload =
1793        usize::try_from((*src).extradata_size).map_err(|_| seat_copy_failed(stream_index))?;
1794      core::ptr::copy_nonoverlapping((*src).extradata, buffer, payload);
1795      (*dst).extradata = buffer;
1796      (*dst).extradata_size = (*src).extradata_size;
1797    }
1798  }
1799
1800  // 2. coded_side_data — the descriptor array, then each payload.
1801  //
1802  // SAFETY: the count and array were measured above; every entry is
1803  // read within the declared count, and each payload is copied at the
1804  // length its own descriptor declares.
1805  unsafe {
1806    let count = (*src).nb_coded_side_data;
1807    if count > 0 && !(*src).coded_side_data.is_null() {
1808      let entries = usize::try_from(count)
1809        .ok()
1810        .and_then(|c| c.checked_mul(core::mem::size_of::<ffmpeg_next::ffi::AVPacketSideData>()))
1811        .ok_or_else(|| seat_copy_failed(stream_index))?;
1812      let array = ffmpeg_next::ffi::av_mallocz(entries) as *mut ffmpeg_next::ffi::AVPacketSideData;
1813      if array.is_null() {
1814        return Err(seat_copy_failed(stream_index));
1815      }
1816      // Attached before the payloads are filled in, so a failure part
1817      // way leaves `out`'s own destructor a well-formed array to walk:
1818      // the entries it has not reached are zeroed, and freeing a null
1819      // payload is a no-op.
1820      (*dst).coded_side_data = array;
1821      (*dst).nb_coded_side_data = count;
1822      for index in 0..count as usize {
1823        // Field pointers, never `&AVPacketSideData` — the `type` field
1824        // is an open C enum and a newer-but-ABI-compatible FFmpeg emits
1825        // kinds these bindings do not name. See `measure_parameters`
1826        // for the whole argument.
1827        let from = (*src).coded_side_data.add(index);
1828        let into = array.add(index);
1829        // The type id travels as the **raw bits it is on the wire**.
1830        // Reading it as the Rust enum would be the very UB this avoids,
1831        // and a kind this build cannot name is still a kind the file
1832        // carries and a decoder may want.
1833        let kind = core::ptr::read_unaligned(core::ptr::addr_of!((*from).type_).cast::<i32>());
1834        core::ptr::write_unaligned(core::ptr::addr_of_mut!((*into).type_).cast::<i32>(), kind);
1835
1836        let size = core::ptr::read_unaligned(core::ptr::addr_of!((*from).size));
1837        let data = core::ptr::read_unaligned(core::ptr::addr_of!((*from).data));
1838        if size > 0 && !data.is_null() {
1839          let payload = ffmpeg_next::ffi::av_mallocz(size) as *mut u8;
1840          if payload.is_null() {
1841            return Err(seat_copy_failed(stream_index));
1842          }
1843          core::ptr::copy_nonoverlapping(data, payload, size);
1844          core::ptr::write_unaligned(core::ptr::addr_of_mut!((*into).data), payload);
1845          core::ptr::write_unaligned(core::ptr::addr_of_mut!((*into).size), size);
1846        } else {
1847          core::ptr::write_unaligned(core::ptr::addr_of_mut!((*into).size), 0);
1848        }
1849      }
1850    }
1851  }
1852
1853  // 3. ch_layout. One FFmpeg call, over one field whose only allocation
1854  // is the custom map this function measured and admitted above.
1855  //
1856  // SAFETY: both layouts are live; the destination is the zeroed
1857  // (`AV_CHANNEL_ORDER_UNSPEC`) state this function put it in, which is
1858  // what `av_channel_layout_copy` requires of a destination it may
1859  // overwrite.
1860  let rc = unsafe {
1861    ffmpeg_next::ffi::av_channel_layout_copy(
1862      core::ptr::addr_of_mut!((*dst).ch_layout),
1863      core::ptr::addr_of!((*src).ch_layout),
1864    )
1865  };
1866  if rc < 0 {
1867    return Err(DemuxError::ParametersCopy(ParametersCopy::new(
1868      stream_index,
1869      ffmpeg_next::Error::from(rc),
1870    )));
1871  }
1872
1873  Ok(out)
1874}
1875
1876/// Per-`TrackInfo` extras — the FFmpeg side of one track-table row.
1877///
1878/// Carries the stream's [`CodecTicket`] — the owned mirror of its
1879/// `AVCodecParameters` — which is what opens a decoder for the track,
1880/// through [`Self::clone_parameters`]. That call rebuilds a fresh
1881/// `AVCodecParameters` from the ticket with no tie back to the format
1882/// context, so a decoder outlives the demuxer that named it.
1883///
1884/// # The handle is gone, and `Sync` arrived with it
1885///
1886/// This type used to hold an `ffmpeg_next::codec::Parameters` — a
1887/// `*mut AVCodecParameters` behind a `Send`-but-not-`Sync` wrapper.
1888/// It was the row's only non-`Sync` field, and through it the whole
1889/// track table was `!Sync`: `TrackInfo<Ffmpeg>` could not be shared,
1890/// `Arc<TrackInfo<Ffmpeg>>` was not `Send`, and every consumer that
1891/// hands a track row to more than one task stopped compiling — for a
1892/// struct FFmpeg documents as a plain descriptor with no thread
1893/// affinity at all.
1894///
1895/// The answer is the mirror this crate already lives by rather than an
1896/// `unsafe impl` over FFI: [`CodecTicket`] holds every seat as owned
1897/// bytes and plain integers, so `Send + Sync` are structural facts and
1898/// there is no safety argument to get wrong. See
1899/// `crate::ticket::tests::the_ticket_is_send_and_sync` for the pins,
1900/// and the ticket's own docs for the parity the rebuild is held to.
1901///
1902/// # `Clone`, and the ruling that banned it
1903///
1904/// This type refused `Clone` for two releases, on two arguments. Both
1905/// were about the raw handle, and both died with it.
1906///
1907/// The first was safety. A derived `Clone` went through
1908/// `ffmpeg_next`'s `Clone` for `Parameters`, which checks neither the
1909/// allocation nor the copy — so safe public code that merely copied a
1910/// track row could dereference a null destination or receive
1911/// parameters that were quietly incomplete, and `Clone` has no way to
1912/// report either. That derive really did ship once, and really was
1913/// reachable.
1914///
1915/// The second was the message-carrier law: a `Clone` is a refcount
1916/// bump, never a deep copy, and `avcodec_parameters_copy` is not that.
1917///
1918/// Neither survives the mirror. There is no `Parameters` left to clone
1919/// unchecked, so there is nothing for a `Clone` to fail to report; and
1920/// a copy is now plain owned Rust — a `Vec` spine and refcount bumps
1921/// over `Arc<[u8]>` — with no FFmpeg allocator anywhere near it, which
1922/// is exactly what the carrier law asks of a `Clone`. A ban whose
1923/// whole rationale is spent is ceremony, so the ban is gone, together
1924/// with the fallible `try_clone` and the infallible `duplicate` that
1925/// stood in for it.
1926///
1927/// **This does not make a track row cheap to copy by accident.**
1928/// [`TrackInfo`](mediadecode::demuxer::TrackInfo) and its
1929/// `TrackParams` still have no `Clone` of their own, so the
1930/// row-sharing law is untouched: a consumer that needs to share a row
1931/// still wraps it in `Arc` once, at the door — which is now a thing it
1932/// can actually do, because the row finally became `Sync`.
1933///
1934/// No `Default`, though. A track row with no codec parameters
1935/// describes no track.
1936///
1937/// `disposition` is the raw `AV_DISPOSITION_*` bit set, not
1938/// `ffmpeg_next::format::stream::Disposition`. That type's
1939/// `from_bits_truncate` drops bits the linked build has no constant
1940/// for, and this crate's stance on bit sets is that every pattern is a
1941/// value — the same reason `PacketFlags` reaches the wire as a number.
1942///
1943/// [`CodecTicket`]: crate::ticket::CodecTicket
1944#[derive(Clone)]
1945pub struct TrackExtra {
1946  stream_index: i32,
1947  disposition: i32,
1948  start_time: Option<i64>,
1949  frame_count: Option<i64>,
1950  ticket: CodecTicket,
1951}
1952
1953impl TrackExtra {
1954  /// Constructs a `TrackExtra` from the stream index and the owned
1955  /// codec ticket. Everything else starts absent.
1956  ///
1957  /// **Infallible, and that is the news.** The old constructor
1958  /// returned a `Result` for one reason: `Parameters::new()` and
1959  /// `Parameters::default()` are safe constructors over an unchecked
1960  /// `avcodec_parameters_alloc`, so a caller could hand this type a
1961  /// null-backed value having never been told. A [`CodecTicket`] has
1962  /// no such state — it cannot be null-backed and it cannot be
1963  /// unmeasurable, because [`CodecTicket::mirror`] refused both before
1964  /// it existed. The check moved to where the raw pointer is, and the
1965  /// row stopped carrying an error it could no longer raise.
1966  ///
1967  /// Not `const fn`: the ticket owns heap.
1968  ///
1969  /// [`CodecTicket`]: crate::ticket::CodecTicket
1970  /// [`CodecTicket::mirror`]: crate::ticket::CodecTicket::mirror
1971  #[cfg_attr(not(tarpaulin), inline(always))]
1972  pub fn new(stream_index: i32, ticket: CodecTicket) -> Self {
1973    Self {
1974      stream_index,
1975      disposition: 0,
1976      start_time: None,
1977      frame_count: None,
1978      ticket,
1979    }
1980  }
1981
1982  /// The heap bytes a rebuild of this row's codec parameters allocates
1983  /// — `extradata` with its padding, the `coded_side_data` descriptor
1984  /// array and every entry's payload, and a custom channel map.
1985  ///
1986  /// The number the session admitted this stream at, and the ceiling
1987  /// `DemuxLimits::max_codec_parameter_bytes` was judged against — so
1988  /// a row that opened is a row whose every
1989  /// [`Self::clone_parameters`] fits the ceiling it opened under.
1990  ///
1991  /// Not the row's own residency: the ticket holds the payload without
1992  /// FFmpeg's trailing padding and shares its buffers by refcount.
1993  #[cfg_attr(not(tarpaulin), inline(always))]
1994  pub const fn parameter_bytes(&self) -> usize {
1995    self.ticket.footprint_bytes()
1996  }
1997
1998  /// A live `AVCodecParameters`, rebuilt from the ticket — the handoff
1999  /// that opens a decoder for this track.
2000  ///
2001  /// `FfmpegAudioStreamDecoder::open(track.extra().clone_parameters()?,
2002  /// track.timebase(), limits)`. Fallible because allocation is: a
2003  /// failure here is the difference between a decoder that is not
2004  /// opened and one opened on parameters that are not the file's.
2005  ///
2006  /// Every seat the file declared is written back, so what a decoder
2007  /// receives is what the demuxer read — proved field by field, per
2008  /// codec, by `tests/codec_ticket_parity.rs`.
2009  #[cfg_attr(not(tarpaulin), inline(always))]
2010  pub fn clone_parameters(&self) -> Result<Parameters, DemuxError> {
2011    self.ticket.rebuild()
2012  }
2013
2014  /// Returns the source `AVStream.index`.
2015  #[cfg_attr(not(tarpaulin), inline(always))]
2016  pub const fn stream_index(&self) -> i32 {
2017    self.stream_index
2018  }
2019  /// Returns the raw `AVStream.disposition` bit set.
2020  #[cfg_attr(not(tarpaulin), inline(always))]
2021  pub const fn disposition(&self) -> i32 {
2022    self.disposition
2023  }
2024  /// Returns the stream's start time in the track's timebase, or
2025  /// `None` when the container does not carry one.
2026  #[cfg_attr(not(tarpaulin), inline(always))]
2027  pub const fn start_time(&self) -> Option<i64> {
2028    self.start_time
2029  }
2030  /// Returns `AVStream.nb_frames` when the container carries it.
2031  #[cfg_attr(not(tarpaulin), inline(always))]
2032  pub const fn frame_count(&self) -> Option<i64> {
2033    self.frame_count
2034  }
2035  /// Returns the stream's owned codec ticket — every seat of its
2036  /// `AVCodecParameters`, and what [`Self::clone_parameters`] rebuilds
2037  /// a decoder's parameters from.
2038  #[cfg_attr(not(tarpaulin), inline(always))]
2039  pub const fn ticket(&self) -> &CodecTicket {
2040    &self.ticket
2041  }
2042
2043  /// Sets the disposition bits (consuming builder).
2044  #[cfg_attr(not(tarpaulin), inline(always))]
2045  #[must_use]
2046  pub const fn with_disposition(mut self, value: i32) -> Self {
2047    self.disposition = value;
2048    self
2049  }
2050  /// Sets the start time (consuming builder).
2051  #[cfg_attr(not(tarpaulin), inline(always))]
2052  #[must_use]
2053  pub const fn with_start_time(mut self, value: Option<i64>) -> Self {
2054    self.start_time = value;
2055    self
2056  }
2057  /// Sets the frame count (consuming builder).
2058  #[cfg_attr(not(tarpaulin), inline(always))]
2059  #[must_use]
2060  pub const fn with_frame_count(mut self, value: Option<i64>) -> Self {
2061    self.frame_count = value;
2062    self
2063  }
2064
2065  /// Sets the disposition bits in place.
2066  #[cfg_attr(not(tarpaulin), inline(always))]
2067  pub const fn set_disposition(&mut self, value: i32) -> &mut Self {
2068    self.disposition = value;
2069    self
2070  }
2071  /// Sets the start time in place.
2072  #[cfg_attr(not(tarpaulin), inline(always))]
2073  pub const fn set_start_time(&mut self, value: Option<i64>) -> &mut Self {
2074    self.start_time = value;
2075    self
2076  }
2077  /// Sets the frame count in place.
2078  #[cfg_attr(not(tarpaulin), inline(always))]
2079  pub const fn set_frame_count(&mut self, value: Option<i64>) -> &mut Self {
2080    self.frame_count = value;
2081    self
2082  }
2083}
2084
2085impl std::fmt::Debug for TrackExtra {
2086  /// Hand-written so the ticket prints as its own summary rather than
2087  /// as a wall of bytes — see [`CodecTicket`](crate::ticket::CodecTicket)'s
2088  /// `Debug`, which prints sizes for the same reason.
2089  fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2090    f.debug_struct("TrackExtra")
2091      .field("stream_index", &self.stream_index)
2092      .field("disposition", &format_args!("{:#x}", self.disposition))
2093      .field("start_time", &self.start_time)
2094      .field("frame_count", &self.frame_count)
2095      .field("ticket", &self.ticket)
2096      .finish()
2097  }
2098}
2099
2100#[cfg(test)]
2101mod tests {
2102  use super::*;
2103
2104  #[test]
2105  fn defaults_construct() {
2106    let v = VideoPacketExtra::default();
2107    assert_eq!(v.stream_index(), 0);
2108    assert!(v.side_data().is_empty());
2109
2110    let f = VideoFrameExtra::default();
2111    assert_eq!(f.picture_type(), PictureType::Unspecified);
2112    assert!(!f.key_frame());
2113    assert!(f.mastering_display().is_none());
2114
2115    let s = SubtitleFrameExtra::default();
2116    assert_eq!(s.start_display_time(), 0);
2117    assert_eq!(s.end_display_time(), 0);
2118  }
2119
2120  #[test]
2121  fn picture_type_default_is_unspecified() {
2122    assert_eq!(PictureType::default(), PictureType::Unspecified);
2123  }
2124
2125  /// Builds an `AVCodecParameters` with the heap seats a file
2126  /// controls, so the preflight and the bounded clone can be driven
2127  /// without a container.
2128  ///
2129  /// The same discipline as the EXIF fixture: a shape the `ffmpeg` CLI
2130  /// cannot mint is built here, by hand, beside the assertions it
2131  /// feeds. A MOV `prof` atom lands in `coded_side_data` as an
2132  /// `AV_PKT_DATA_ICC_PROFILE` entry — that is the road this
2133  /// constructs, at whatever size the test asks for.
2134  fn parameters_with(extradata: usize, icc_profile: usize) -> Parameters {
2135    let mut out = Parameters::new();
2136    // SAFETY: `out` owns a live `AVCodecParameters`. Every buffer below
2137    // comes from FFmpeg's allocator and is handed to it, so
2138    // `avcodec_parameters_free` releases all of them with the struct.
2139    unsafe {
2140      let par = out.as_mut_ptr();
2141      if extradata > 0 {
2142        let buffer = ffmpeg_next::ffi::av_mallocz(extradata) as *mut u8;
2143        assert!(!buffer.is_null(), "av_mallocz extradata");
2144        (*par).extradata = buffer;
2145        (*par).extradata_size = extradata as i32;
2146      }
2147      if icc_profile > 0 {
2148        let array = ffmpeg_next::ffi::av_mallocz(core::mem::size_of::<
2149          ffmpeg_next::ffi::AVPacketSideData,
2150        >()) as *mut ffmpeg_next::ffi::AVPacketSideData;
2151        assert!(!array.is_null(), "av_mallocz side-data array");
2152        let payload = ffmpeg_next::ffi::av_mallocz(icc_profile) as *mut u8;
2153        assert!(!payload.is_null(), "av_mallocz icc profile");
2154        (*array).data = payload;
2155        (*array).size = icc_profile;
2156        (*array).type_ = ffmpeg_next::ffi::AVPacketSideDataType::AV_PKT_DATA_ICC_PROFILE;
2157        (*par).coded_side_data = array;
2158        (*par).nb_coded_side_data = 1;
2159      }
2160    }
2161    out
2162  }
2163
2164  fn footprint_of(parameters: &Parameters) -> ParameterFootprint {
2165    // SAFETY: `parameters` owns a live `AVCodecParameters`.
2166    unsafe { measure_parameters(parameters.as_ptr()) }.expect("measurable")
2167  }
2168
2169  #[test]
2170  fn the_measurement_counts_every_heap_seat_and_allocates_nothing() {
2171    // The inventory the bounded clone is written against: `extradata`,
2172    // `coded_side_data` (payload *and* descriptor array), and a custom
2173    // channel map. A seat the measurement misses is a seat the budget
2174    // never sees, which is how this class kept coming back.
2175    const PAD: usize = ffmpeg_next::ffi::AV_INPUT_BUFFER_PADDING_SIZE as usize;
2176    const DESCRIPTOR: usize = core::mem::size_of::<ffmpeg_next::ffi::AVPacketSideData>();
2177    let parameters = parameters_with(4_096, 64 * 1024);
2178    let footprint = footprint_of(&parameters);
2179    // The padding a copy allocates is part of what a copy costs: a
2180    // ceiling that counted only the payload admitted an allocation
2181    // `AV_INPUT_BUFFER_PADDING_SIZE` larger than it agreed to.
2182    assert_eq!(footprint.extradata(), 4_096 + PAD);
2183    assert_eq!(
2184      footprint.coded_side_data(),
2185      64 * 1024 + DESCRIPTOR,
2186      "the descriptor array is an allocation too",
2187    );
2188    assert_eq!(footprint.channel_map(), 0, "no custom layout here");
2189    assert_eq!(
2190      footprint.total(),
2191      Some(4_096 + PAD + 64 * 1024 + DESCRIPTOR),
2192    );
2193    // The omit-aware total leaves extradata *and its padding* out,
2194    // which is what keeps a synthesized attachment from being charged
2195    // for bytes the clone never allocates.
2196    assert_eq!(
2197      footprint.total_without_extradata(),
2198      Some(64 * 1024 + DESCRIPTOR),
2199    );
2200    // And no extradata at all means no padding either.
2201    assert_eq!(footprint_of(&parameters_with(0, 8)).extradata(), 0);
2202  }
2203
2204  #[test]
2205  fn an_oversized_coded_side_data_entry_is_refused_before_the_clone() {
2206    // The R3 finding, at unit level: `avcodec_parameters_copy` deep-copies
2207    // every `coded_side_data` entry, and a MOV `prof` atom is where an
2208    // attacker-sized one arrives. The bounded clone measures first.
2209    let parameters = parameters_with(0, 8 * 1024 * 1024);
2210    let declared = footprint_of(&parameters).total().expect("measurable");
2211
2212    match bounded_clone_parameters(&parameters, 3, 64 * 1024) {
2213      Err(DemuxError::ParametersTooLarge(p)) => {
2214        assert_eq!(p.stream_index(), 3);
2215        assert_eq!(p.bytes(), declared);
2216        assert_eq!(p.limit(), 64 * 1024);
2217      }
2218      Err(other) => panic!("expected ParametersTooLarge, got {other:?}"),
2219      Ok(_) => panic!("an 8 MiB ICC profile passed a 64 KiB ceiling"),
2220    }
2221
2222    // Exactly at the line is not over it, and the copy is faithful.
2223    let cloned =
2224      bounded_clone_parameters(&parameters, 3, declared).expect("at the cap is not over it");
2225    assert_eq!(footprint_of(&cloned), footprint_of(&parameters));
2226  }
2227
2228  #[test]
2229  fn a_legitimate_multi_megabyte_icc_profile_is_admitted_by_default() {
2230    // The honest end of the range this budget has to clear: a real
2231    // camera or display ICC profile. A ceiling that refused these would
2232    // be a ceiling nobody could ship behind.
2233    let parameters = parameters_with(1_024, 4 * 1024 * 1024);
2234    let cloned = bounded_clone_parameters(
2235      &parameters,
2236      0,
2237      crate::limits::DEFAULT_MAX_CODEC_PARAMETER_BYTES,
2238    )
2239    .expect("a 4 MiB ICC profile is real media");
2240    // And it arrived whole — a clone that silently dropped the profile
2241    // would pass a size assertion and fail a consumer.
2242    assert_eq!(footprint_of(&cloned), footprint_of(&parameters));
2243  }
2244
2245  #[test]
2246  fn the_bounded_clone_keeps_every_field_a_decoder_consumes() {
2247    // Decode-capability parity, asserted directly as well as by the
2248    // decode suites passing unchanged: scalars, extradata bytes (with
2249    // the padding decoders read into), and each side-data entry's type
2250    // and payload all survive.
2251    let parameters = parameters_with(32, 128);
2252    // SAFETY: `parameters` owns a live `AVCodecParameters`; the writes
2253    // below are plain scalar fields.
2254    unsafe {
2255      let par = parameters.as_ptr() as *mut ffmpeg_next::ffi::AVCodecParameters;
2256      (*par).codec_id = ffmpeg_next::ffi::AVCodecID::AV_CODEC_ID_H264;
2257      (*par).width = 1920;
2258      (*par).height = 1080;
2259      (*par).bit_rate = 5_000_000;
2260      (*par).sample_rate = 48_000;
2261      core::ptr::write_bytes((*par).extradata, 0xAB, 32);
2262      core::ptr::write_bytes((*(*par).coded_side_data).data, 0xCD, 128);
2263    }
2264
2265    let cloned = bounded_clone_parameters(&parameters, 0, usize::MAX).expect("clone");
2266    // SAFETY: both own live `AVCodecParameters`.
2267    unsafe {
2268      let src = parameters.as_ptr();
2269      let dst = cloned.as_ptr();
2270      assert_eq!((*dst).codec_id, (*src).codec_id, "the scalar sweep");
2271      assert_eq!(((*dst).width, (*dst).height), (1920, 1080));
2272      assert_eq!((*dst).bit_rate, 5_000_000);
2273      assert_eq!((*dst).sample_rate, 48_000);
2274
2275      assert_eq!((*dst).extradata_size, 32);
2276      assert_ne!(
2277        (*dst).extradata,
2278        (*src).extradata,
2279        "it is a copy, not an alias"
2280      );
2281      let extradata = core::slice::from_raw_parts((*dst).extradata, 32);
2282      assert!(extradata.iter().all(|&b| b == 0xAB), "SPS/PPS survived");
2283      // The padding a decoder reads past the end into is present and
2284      // zeroed — `avcodec_parameters_copy` guarantees it and so must we.
2285      let padded = core::slice::from_raw_parts(
2286        (*dst).extradata.add(32),
2287        ffmpeg_next::ffi::AV_INPUT_BUFFER_PADDING_SIZE as usize,
2288      );
2289      assert!(padded.iter().all(|&b| b == 0), "the read-past padding");
2290
2291      assert_eq!((*dst).nb_coded_side_data, 1);
2292      let entry = &*(*dst).coded_side_data;
2293      assert_eq!(entry.size, 128);
2294      assert_eq!(
2295        entry.type_,
2296        ffmpeg_next::ffi::AVPacketSideDataType::AV_PKT_DATA_ICC_PROFILE,
2297      );
2298      assert_ne!(entry.data, (*(*src).coded_side_data).data, "a copy");
2299      let payload = core::slice::from_raw_parts(entry.data, 128);
2300      assert!(
2301        payload.iter().all(|&b| b == 0xCD),
2302        "the ICC profile survived"
2303      );
2304    }
2305  }
2306
2307  #[test]
2308  fn side_data_entry_carries_bytes() {
2309    let entry = SideDataEntry::new(12345, FfmpegBytes::copy_from_slice(&[1, 2, 3, 4]));
2310    assert_eq!(entry.kind(), 12345);
2311    assert_eq!(entry.data(), &[1, 2, 3, 4]);
2312  }
2313
2314  #[test]
2315  fn side_data_entry_clone_shares_its_payload() {
2316    // The amputation contract's consumer-side half, one tier in: a
2317    // frame's metadata clones as cheaply as its pixels do.
2318    let entry = SideDataEntry::new(7, FfmpegBytes::copy_from_slice(&[9u8; 64]));
2319    let cloned = entry.clone();
2320    assert!(
2321      entry.data_ref().ptr_eq(cloned.data_ref()),
2322      "cloning a side-data entry copied its bytes",
2323    );
2324    assert_eq!(cloned.data(), entry.data());
2325  }
2326
2327  /// The eight matrices, exactly as this build of libavcodec emits
2328  /// them — measured by feeding mjpeg a JPEG whose EXIF IFD carries
2329  /// each tag in turn, then reading the frame's
2330  /// `AV_FRAME_DATA_DISPLAYMATRIX` entry back.
2331  const MEASURED: [(u16, ImageOrientation, [i32; 4]); 8] = [
2332    (1, ImageOrientation::TopLeft, [65536, 0, 0, 65536]),
2333    (2, ImageOrientation::TopRight, [-65536, 0, 0, 65536]),
2334    (3, ImageOrientation::BottomRight, [-65536, 0, 0, -65536]),
2335    (4, ImageOrientation::BottomLeft, [65536, 0, 0, -65536]),
2336    (5, ImageOrientation::LeftTop, [0, 65536, 65536, 0]),
2337    (6, ImageOrientation::RightTop, [0, 65536, -65536, 0]),
2338    (7, ImageOrientation::RightBottom, [0, -65536, -65536, 0]),
2339    (8, ImageOrientation::LeftBottom, [0, -65536, 65536, 0]),
2340  ];
2341
2342  /// A full nine-word display matrix with `linear` in the four
2343  /// load-bearing slots and canonical values everywhere else, laid out
2344  /// as `libavutil/display.h` specifies and in native byte order, as
2345  /// the side data really is.
2346  fn display_matrix(linear: [i32; 4]) -> Vec<u8> {
2347    words_to_bytes([
2348      linear[0],
2349      linear[1],
2350      0,
2351      linear[2],
2352      linear[3],
2353      0,
2354      0,
2355      0,
2356      1 << 30,
2357    ])
2358  }
2359
2360  fn words_to_bytes(words: [i32; 9]) -> Vec<u8> {
2361    words.iter().flat_map(|w| w.to_ne_bytes()).collect()
2362  }
2363
2364  #[test]
2365  fn every_measured_display_matrix_reads_back_as_its_exif_tag() {
2366    for (tag, expected, linear) in MEASURED {
2367      let read = ImageOrientation::from_display_matrix(&display_matrix(linear))
2368        .expect("a nine-word matrix is readable");
2369      assert_eq!(read, expected, "tag {tag}");
2370      assert_eq!(read.to_exif_code(), Some(tag));
2371      assert_eq!(ImageOrientation::from_exif_code(tag), Some(read));
2372      // The inverse: the variant knows the matrix it came from.
2373      assert_eq!(read.linear(), linear, "tag {tag}");
2374    }
2375  }
2376
2377  #[test]
2378  fn the_four_mirrored_tags_are_the_ones_exif_says_they_are() {
2379    // 2, 4, 5, 7 — and this is exactly the half `av_display_rotation_get`
2380    // cannot distinguish, which is why the seat is eight-valued.
2381    for (tag, orientation, _) in MEASURED {
2382      assert_eq!(
2383        orientation.is_mirrored(),
2384        matches!(tag, 2 | 4 | 5 | 7),
2385        "tag {tag}",
2386      );
2387    }
2388  }
2389
2390  #[test]
2391  fn the_quarter_turn_lands_in_the_workspace_rotation_vocabulary() {
2392    use ImageOrientation::*;
2393    assert_eq!(TopLeft.rotation(), Some(Rotation::D0));
2394    assert_eq!(TopRight.rotation(), Some(Rotation::D0));
2395    assert_eq!(RightTop.rotation(), Some(Rotation::D90));
2396    assert_eq!(LeftTop.rotation(), Some(Rotation::D90));
2397    assert_eq!(BottomRight.rotation(), Some(Rotation::D180));
2398    assert_eq!(BottomLeft.rotation(), Some(Rotation::D180));
2399    assert_eq!(LeftBottom.rotation(), Some(Rotation::D270));
2400    assert_eq!(RightBottom.rotation(), Some(Rotation::D270));
2401    // Four rotations, eight orientations: the rotation alone cannot
2402    // tell 1 from 2, which is the whole reason this type exists.
2403    assert_eq!(TopLeft.rotation(), TopRight.rotation());
2404    assert_ne!(TopLeft, TopRight);
2405  }
2406
2407  #[test]
2408  fn a_transform_the_vocabulary_cannot_name_is_carried_not_collapsed() {
2409    // An arbitrary affine linear part — a MOV `tkhd` can carry one.
2410    // Answering `TopLeft` here is the silent loss this crate refuses.
2411    let odd = [46_341, 46_341, -46_341, 46_341]; // ~45 degrees
2412    let words: [i32; 9] = [odd[0], odd[1], 0, odd[2], odd[3], 0, 0, 0, 1 << 30];
2413    let read =
2414      ImageOrientation::from_display_matrix(&display_matrix(odd)).expect("readable, just unnamed");
2415    assert_eq!(read, ImageOrientation::Other(words));
2416    assert_eq!(read.to_exif_code(), None, "there is no tag to invent");
2417    assert_eq!(read.rotation(), None, "it is not a quarter turn");
2418    assert_eq!(read.linear(), odd, "the linear projection still answers");
2419    assert_eq!(read.matrix(), words, "and nothing was dropped");
2420    // The determinant still answers, because a reflection is a
2421    // determinant sign whatever the angle.
2422    assert!(!read.is_mirrored());
2423    assert!(ImageOrientation::Other([65536, 0, 0, 0, -65536, 0, 0, 0, 1 << 30]).is_mirrored());
2424  }
2425
2426  #[test]
2427  fn a_noncanonical_word_keeps_a_matrix_out_of_the_named_variants() {
2428    // The escape-carries-never-collapses law, at the exact place it was
2429    // being broken: a matrix whose *linear* four say "quarter turn
2430    // clockwise" but which also translates, or projects, is not tag 6.
2431    // Reading it as tag 6 throws away the word that made it different.
2432    //
2433    // Every non-linear word, one at a time, against a linear part that
2434    // would otherwise be named.
2435    let named = ImageOrientation::RightTop;
2436    let canonical = named.matrix();
2437    assert_eq!(
2438      ImageOrientation::from_display_matrix(&words_to_bytes(canonical)),
2439      Some(named),
2440      "the canonical matrix must still be named",
2441    );
2442
2443    for index in [2usize, 5, 6, 7, 8] {
2444      let mut forged = canonical;
2445      // A value that is wrong for that slot: any non-zero for the
2446      // translation and perspective terms, anything but unity for `w`.
2447      forged[index] = if index == 8 { 1 << 29 } else { 4_096 };
2448      let read = ImageOrientation::from_display_matrix(&words_to_bytes(forged))
2449        .expect("nine words are readable");
2450      assert_eq!(
2451        read,
2452        ImageOrientation::Other(forged),
2453        "word {index} was collapsed into a named variant",
2454      );
2455      assert_eq!(read.to_exif_code(), None, "word {index}");
2456      assert_eq!(read.matrix(), forged, "word {index} round-trips whole");
2457      // The four that do carry the orientation are still projectable.
2458      assert_eq!(read.linear(), named.linear(), "word {index}");
2459    }
2460  }
2461
2462  #[test]
2463  fn the_escape_round_trips_every_word_losslessly() {
2464    // Nine distinct, deliberately hostile words: negative, extreme,
2465    // and nothing canonical anywhere.
2466    let words: [i32; 9] = [1, -2, 3, -4, 5, -6, i32::MIN, i32::MAX, 0];
2467    let read = ImageOrientation::from_display_matrix(&words_to_bytes(words)).expect("readable");
2468    assert_eq!(read, ImageOrientation::Other(words));
2469    assert_eq!(read.matrix(), words);
2470    // And back through the bytes again: the escape is a fixed point.
2471    let again =
2472      ImageOrientation::from_display_matrix(&words_to_bytes(read.matrix())).expect("readable");
2473    assert_eq!(again, read);
2474  }
2475
2476  #[test]
2477  fn every_named_orientation_reconstructs_its_canonical_matrix() {
2478    for (tag, orientation, linear) in MEASURED {
2479      let matrix = orientation.matrix();
2480      assert_eq!(
2481        [matrix[0], matrix[1], matrix[3], matrix[4]],
2482        linear,
2483        "tag {tag}",
2484      );
2485      assert_eq!(
2486        [matrix[2], matrix[5], matrix[6], matrix[7]],
2487        [0, 0, 0, 0],
2488        "tag {tag}: no translation, no perspective",
2489      );
2490      assert_eq!(matrix[8], 1 << 30, "tag {tag}: unity `w`");
2491      // Round-trip: the reconstruction reads back as the same value.
2492      assert_eq!(
2493        ImageOrientation::from_display_matrix(&words_to_bytes(matrix)),
2494        Some(orientation),
2495        "tag {tag}",
2496      );
2497    }
2498  }
2499
2500  #[test]
2501  fn a_malformed_matrix_is_no_orientation_rather_than_a_guessed_one() {
2502    assert_eq!(ImageOrientation::from_display_matrix(&[]), None);
2503    assert_eq!(ImageOrientation::from_display_matrix(&[0u8; 16]), None);
2504    assert_eq!(ImageOrientation::from_display_matrix(&[0u8; 40]), None);
2505    // The one length that is right.
2506    assert_eq!(
2507      ImageOrientation::DISPLAY_MATRIX_BYTES,
2508      36,
2509      "nine int32, per libavutil/display.h",
2510    );
2511    assert!(ImageOrientation::from_display_matrix(&[0u8; 36]).is_some());
2512  }
2513
2514  #[test]
2515  fn an_out_of_range_exif_tag_is_refused_not_clamped() {
2516    for code in [0u16, 9, 255, u16::MAX] {
2517      assert_eq!(ImageOrientation::from_exif_code(code), None, "code {code}");
2518    }
2519  }
2520
2521  #[test]
2522  fn the_orientation_seat_rides_the_image_extras() {
2523    let extra = ImageFrameExtra::default();
2524    assert_eq!(extra.orientation(), None, "absent until a file says");
2525
2526    let carried = ImageFrameExtra::new().with_orientation(Some(ImageOrientation::RightTop));
2527    assert_eq!(carried.orientation(), Some(ImageOrientation::RightTop));
2528
2529    let mut mutated = carried.clone();
2530    mutated.set_orientation(None);
2531    assert_eq!(mutated.orientation(), None);
2532    assert_eq!(carried.orientation(), Some(ImageOrientation::RightTop));
2533  }
2534
2535  #[test]
2536  fn the_image_household_is_one_seat() {
2537    let extra = ImageFrameExtra::default();
2538    assert!(extra.side_data().is_empty());
2539    let carried = ImageFrameExtra::new().with_side_data(vec![SideDataEntry::new(
2540      3,
2541      FfmpegBytes::copy_from_slice(&[1]),
2542    )]);
2543    assert_eq!(carried.side_data().len(), 1);
2544    assert_eq!(carried.side_data()[0].kind(), 3);
2545    let mut mutated = carried.clone();
2546    mutated.set_side_data(Vec::new());
2547    assert!(mutated.side_data().is_empty());
2548    assert_eq!(carried.side_data().len(), 1);
2549  }
2550
2551  #[test]
2552  fn content_light_level_default_is_zero() {
2553    let cll = ContentLightLevel::default();
2554    assert_eq!(cll.max_cll(), 0);
2555    assert_eq!(cll.max_fall(), 0);
2556  }
2557
2558  #[test]
2559  fn builders_chain() {
2560    let v = VideoPacketExtra::new(7)
2561      .with_byte_pos(Some(1234))
2562      .with_side_data(vec![SideDataEntry::new(
2563        1,
2564        FfmpegBytes::copy_from_slice(&[0xAB]),
2565      )]);
2566    assert_eq!(v.stream_index(), 7);
2567    assert_eq!(v.byte_pos(), Some(1234));
2568    assert_eq!(v.side_data().len(), 1);
2569  }
2570
2571  #[test]
2572  fn setters_chain() {
2573    let mut v = VideoPacketExtra::default();
2574    v.set_stream_index(3).set_byte_pos(Some(99));
2575    assert_eq!(v.stream_index(), 3);
2576    assert_eq!(v.byte_pos(), Some(99));
2577  }
2578}