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