Skip to main content

mediaframe/frame/
mod.rs

1//! Frame primitives + the typed source-format `*Frame<'a, BE>` borrow types.
2//!
3//! ## Always-available primitives
4//!
5//! - [`Dimensions`] — a `(width, height)` pair in pixels.
6//! - [`Rect`] — an axis-aligned integer rectangle (used for visible-region
7//!   crops on `VideoFrame`).
8//! - [`Rotation`] — display rotation (0 / 90 / 180 / 270).
9//! - [`SampleAspectRatio`] — pixel aspect ratio (SAR).
10//! - [`Plane<B>`] — one plane of pixel data, generic over the buffer type.
11//! - [`VideoFrame<P, B>`] — runtime-tagged frame (no timestamp).
12//! - [`TimestampedFrame<F>`] — orthogonal time-carrying wrapper.
13//!
14//! ## Typed `*Frame<'a, BE>` borrow types (feature-gated)
15//!
16//! Each pixel-format family is gated behind its own feature flag so
17//! consumers compile only the formats they need. Enable an individual
18//! family (e.g. `yuv-planar`) or the `frame` umbrella to opt in.
19//!
20//! | Feature           | Formats                                              |
21//! |-------------------|------------------------------------------------------|
22//! | `yuv-planar`      | Yuv420p / 422p / 444p / 440p / 411p / 410p + 9-16bit |
23//! | `yuv-semi-planar` | NV12 / 16 / 21 / 24 / 42, P010 / 210 / 410 families  |
24//! | `yuva`            | YUVA planar 8-bit + high-bit                         |
25//! | `yuv-packed`      | YUYV422, UYVY422, YVYU422, UYYVYY411                 |
26//! | `yuv-444-packed`  | V410, XV30, XV36, AYUV64, VUYA, VUYX, V30X           |
27//! | `y2xx`            | Y210 / Y212 / Y216                                   |
28//! | `v210`            | V210                                                 |
29//! | `rgb`             | Rgb24/Bgr24/Rgba/Bgra + 16-bit family                |
30//! | `rgb-float`       | Rgbf32 / Rgbf16                                      |
31//! | `rgb-legacy`      | Rgb444/555/565 + Bgr counterparts                    |
32//! | `gbr`             | Gbrp / Gbrap + 9-16bit + float                       |
33//! | `gray`            | Gray8-16, Grayf32, Ya8/16                            |
34//! | `bayer`           | Bayer 8-16bit, 4 patterns                            |
35//! | `xyz`             | Xyz12                                                |
36//! | `mono`            | Monoblack / Monowhite / Pal8                         |
37//! | `frame`           | umbrella — enables every sub-feature above           |
38
39// === Primitives (always available) ===
40
41// ---- Shared error payload structs (used by per-family `*FrameError` enums) ----
42//
43// Variant names carry the per-plane / per-axis semantics
44// (`InsufficientYStride`, `InsufficientUPlane`, …); the payload carries the
45// shape-only data (the offending number + the reference number).
46// Each payload has:
47//   - private fields,
48//   - a `pub const fn new(...)` constructor,
49//   - one `pub const fn field(&self) -> T` getter per field,
50//   - `#[inline]` on all methods.
51// thiserror `#[error("...", .0.field())]` routes Display lookups
52// through the getters so the original messages are preserved
53// verbatim.
54
55/// `width × height` carried by zero-dimension errors.
56#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, thiserror::Error)]
57#[error("width ({width}) or height ({height}) is zero")]
58pub struct ZeroDimension {
59  width: u32,
60  height: u32,
61}
62
63impl ZeroDimension {
64  /// Constructs a `ZeroDimension` payload.
65  #[inline]
66  pub const fn new(width: u32, height: u32) -> Self {
67    Self { width, height }
68  }
69  /// Returns the supplied width.
70  #[inline]
71  pub const fn width(&self) -> u32 {
72    self.width
73  }
74  /// Returns the supplied height.
75  #[inline]
76  pub const fn height(&self) -> u32 {
77    self.height
78  }
79}
80
81/// `width × height` carried by dimension-overflow errors.
82#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, thiserror::Error)]
83#[error("dimensions {width} × {height} overflow")]
84pub struct DimensionOverflow {
85  width: u32,
86  height: u32,
87}
88
89impl DimensionOverflow {
90  /// Constructs a `DimensionOverflow` payload.
91  #[inline]
92  pub const fn new(width: u32, height: u32) -> Self {
93    Self { width, height }
94  }
95  /// Returns the supplied width.
96  #[inline]
97  pub const fn width(&self) -> u32 {
98    self.width
99  }
100  /// Returns the supplied height.
101  #[inline]
102  pub const fn height(&self) -> u32 {
103    self.height
104  }
105}
106
107/// Plane stride is smaller than what the declared geometry requires.
108/// The variant name (e.g. `InsufficientYStride` vs `InsufficientUvStride`)
109/// tells the caller which plane and what unit.
110#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, thiserror::Error)]
111#[error("stride ({stride}) is smaller than minimum ({min})")]
112pub struct InsufficientStride {
113  stride: u32,
114  min: u32,
115}
116
117impl InsufficientStride {
118  /// Constructs a `InsufficientStride` payload.
119  #[inline]
120  pub const fn new(stride: u32, min: u32) -> Self {
121    Self { stride, min }
122  }
123  /// Returns the caller-supplied stride.
124  #[inline]
125  pub const fn stride(&self) -> u32 {
126    self.stride
127  }
128  /// Returns the required minimum.
129  #[inline]
130  pub const fn min(&self) -> u32 {
131    self.min
132  }
133}
134
135/// Plane buffer is shorter than the declared geometry requires.
136#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, thiserror::Error)]
137#[error("plane has {actual} bytes/samples but at least {expected} are required")]
138pub struct InsufficientPlane {
139  expected: usize,
140  actual: usize,
141}
142
143impl InsufficientPlane {
144  /// Constructs a `InsufficientPlane` payload.
145  #[inline]
146  pub const fn new(expected: usize, actual: usize) -> Self {
147    Self { expected, actual }
148  }
149  /// Returns the minimum required length.
150  #[inline]
151  pub const fn expected(&self) -> usize {
152    self.expected
153  }
154  /// Returns the actual length supplied.
155  #[inline]
156  pub const fn actual(&self) -> usize {
157    self.actual
158  }
159}
160
161/// Declared geometry (`stride × rows`) doesn't fit in `usize`.
162#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, thiserror::Error)]
163#[error("declared geometry overflows usize: stride={stride} * rows={rows}")]
164pub struct GeometryOverflow {
165  stride: u32,
166  rows: u32,
167}
168
169impl GeometryOverflow {
170  /// Constructs a `GeometryOverflow` payload.
171  #[inline]
172  pub const fn new(stride: u32, rows: u32) -> Self {
173    Self { stride, rows }
174  }
175  /// Returns the stride that overflowed.
176  #[inline]
177  pub const fn stride(&self) -> u32 {
178    self.stride
179  }
180  /// Returns the row count that overflowed.
181  #[inline]
182  pub const fn rows(&self) -> u32 {
183    self.rows
184  }
185}
186
187/// Width-alignment violation.
188#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, thiserror::Error)]
189#[error("width ({width}) {required}")]
190pub struct WidthAlignment {
191  /// Sink's configured width.
192  width: usize,
193  /// The alignment requirement that was violated.
194  required: WidthAlignmentRequirement,
195}
196
197impl WidthAlignment {
198  /// Constructs a new `WidthAlignment` payload.
199  #[inline]
200  const fn new(width: usize, required: WidthAlignmentRequirement) -> Self {
201    Self { width, required }
202  }
203
204  /// Constructs a `WidthAlignment` payload for odd widths.
205  #[inline]
206  pub const fn odd(width: usize) -> Self {
207    Self::new(width, WidthAlignmentRequirement::Even)
208  }
209
210  /// Constructs a `WidthAlignment` payload for widths that are not a
211  #[inline]
212  pub const fn multiple_of_four(width: usize) -> Self {
213    Self::new(width, WidthAlignmentRequirement::MultipleOfFour)
214  }
215
216  /// Sink's configured width.
217  #[inline]
218  pub const fn width(&self) -> usize {
219    self.width
220  }
221
222  /// The alignment requirement that was violated.
223  #[inline]
224  pub const fn required(&self) -> WidthAlignmentRequirement {
225    self.required
226  }
227}
228
229/// Discriminates which width-alignment rule was violated.
230#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, IsVariant, Display)]
231#[non_exhaustive]
232pub enum WidthAlignmentRequirement {
233  /// Width must be even — 4:2:0 / 4:2:2 chroma-pair stride.
234  #[display("is odd")]
235  Even,
236  /// Width must be a multiple of 4. Fired by planar 4:1:0
237  /// ([`Yuv410p`](crate::source::Yuv410p)) and packed 4:1:1
238  /// ([`Uyyvyy411`](crate::source::Uyyvyy411)). Note: planar 4:1:1
239  /// ([`Yuv411p`](crate::source::Yuv411p)) accepts non-4-aligned
240  /// widths via `width.div_ceil(4)` for the chroma row and is NOT
241  /// covered by this discriminant.
242  #[display("is not a multiple of 4")]
243  MultipleOfFour,
244}
245
246/// Frame `width` value carried by per-row width-overflow errors.
247#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, thiserror::Error)]
248#[error("width ({width}) overflow")]
249pub struct WidthOverflow {
250  width: u32,
251}
252
253impl WidthOverflow {
254  /// Constructs a `WidthOverflow` payload.
255  #[inline]
256  pub const fn new(width: u32) -> Self {
257    Self { width }
258  }
259  /// Returns the supplied width.
260  #[inline]
261  pub const fn width(&self) -> u32 {
262    self.width
263  }
264}
265
266/// `BITS` const-generic value carried by unsupported-bits errors.
267#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, thiserror::Error)]
268#[error("unsupported BITS ({bits})")]
269pub struct UnsupportedBits {
270  bits: u32,
271}
272
273impl UnsupportedBits {
274  /// Constructs an `UnsupportedBits` payload.
275  #[inline]
276  pub const fn new(bits: u32) -> Self {
277    Self { bits }
278  }
279  /// Returns the supplied `BITS` value.
280  #[inline]
281  pub const fn bits(&self) -> u32 {
282    self.bits
283  }
284}
285
286/// A `(width, height)` pair in pixels.
287///
288/// Lives alongside the rest of the frame primitives because the same
289/// pair shows up everywhere a video stream is described — the coded
290/// dimensions of a `VideoFrame`, the `coded_*` parameters a backend
291/// adapter takes when opening a decoder, the per-plane layout helpers
292/// in a WebCodecs adapter, etc. Passing it as a single struct rather
293/// than two separate `u32` arguments removes a long-running footgun
294/// (silent argument swap) and gives a natural place to hang helpers
295/// like [`Self::is_zero`] or `Display`.
296///
297/// `u32` width / height matches WebCodecs' `coded_width` /
298/// `coded_height` typing in `web_sys` and FFmpeg's
299/// `AVCodecContext::width` / `height`. 65535×65535 (the smaller `u16`
300/// packing some adjacent crates use) covers every realistic
301/// resolution; the `u32` choice here keeps the public API plug-
302/// compatible with both adapter typings.
303#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
304#[cfg_attr(
305  feature = "quickcheck",
306  derive(::quickcheck_richderive::Arbitrary),
307  quickcheck(arbitrary = "crate::quickcheck_helpers::coded::dimensions")
308)]
309#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
310pub struct Dimensions {
311  width: u32,
312  height: u32,
313}
314
315impl Dimensions {
316  /// Constructs a `Dimensions` with the specified width and height
317  /// in pixels.
318  #[cfg_attr(not(tarpaulin), inline(always))]
319  pub const fn new(width: u32, height: u32) -> Self {
320    Self { width, height }
321  }
322
323  /// Returns the width in pixels.
324  #[cfg_attr(not(tarpaulin), inline(always))]
325  pub const fn width(&self) -> u32 {
326    self.width
327  }
328
329  /// Returns the height in pixels.
330  #[cfg_attr(not(tarpaulin), inline(always))]
331  pub const fn height(&self) -> u32 {
332    self.height
333  }
334
335  /// Sets the width (consuming builder).
336  #[must_use]
337  #[cfg_attr(not(tarpaulin), inline(always))]
338  pub const fn with_width(mut self, width: u32) -> Self {
339    self.width = width;
340    self
341  }
342
343  /// Sets the width in place.
344  #[cfg_attr(not(tarpaulin), inline(always))]
345  pub const fn set_width(&mut self, width: u32) -> &mut Self {
346    self.width = width;
347    self
348  }
349
350  /// Sets the height (consuming builder).
351  #[must_use]
352  #[cfg_attr(not(tarpaulin), inline(always))]
353  pub const fn with_height(mut self, height: u32) -> Self {
354    self.height = height;
355    self
356  }
357
358  /// Sets the height in place.
359  #[cfg_attr(not(tarpaulin), inline(always))]
360  pub const fn set_height(&mut self, height: u32) -> &mut Self {
361    self.height = height;
362    self
363  }
364
365  /// Returns `true` when both width and height are zero — typically
366  /// the default-constructed / unset state.
367  #[cfg_attr(not(tarpaulin), inline(always))]
368  pub const fn is_zero(&self) -> bool {
369    self.width == 0 && self.height == 0
370  }
371}
372
373impl core::fmt::Display for Dimensions {
374  fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
375    write!(f, "{}x{}", self.width, self.height)
376  }
377}
378
379/// An axis-aligned integer rectangle.
380///
381/// Used for `VideoFrame::visible_rect` (FFmpeg crop /
382/// WebCodecs `visibleRect` / ProRes RAW `CleanAperture`).
383#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
384#[cfg_attr(
385  feature = "quickcheck",
386  derive(::quickcheck_richderive::Arbitrary),
387  quickcheck(arbitrary = "crate::quickcheck_helpers::coded::rect")
388)]
389#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
390pub struct Rect {
391  x: u32,
392  y: u32,
393  width: u32,
394  height: u32,
395}
396
397impl Rect {
398  /// Constructs a `Rect` at `(x, y)` with the given size.
399  #[cfg_attr(not(tarpaulin), inline(always))]
400  pub const fn new(x: u32, y: u32, width: u32, height: u32) -> Self {
401    Self {
402      x,
403      y,
404      width,
405      height,
406    }
407  }
408
409  /// Returns the X coordinate of the top-left corner.
410  #[cfg_attr(not(tarpaulin), inline(always))]
411  pub const fn x(&self) -> u32 {
412    self.x
413  }
414
415  /// Returns the Y coordinate of the top-left corner.
416  #[cfg_attr(not(tarpaulin), inline(always))]
417  pub const fn y(&self) -> u32 {
418    self.y
419  }
420
421  /// Returns the width.
422  #[cfg_attr(not(tarpaulin), inline(always))]
423  pub const fn width(&self) -> u32 {
424    self.width
425  }
426
427  /// Returns the height.
428  #[cfg_attr(not(tarpaulin), inline(always))]
429  pub const fn height(&self) -> u32 {
430    self.height
431  }
432
433  /// Sets the X coordinate (consuming builder).
434  #[must_use]
435  #[cfg_attr(not(tarpaulin), inline(always))]
436  pub const fn with_x(mut self, x: u32) -> Self {
437    self.x = x;
438    self
439  }
440  /// Sets the Y coordinate (consuming builder).
441  #[must_use]
442  #[cfg_attr(not(tarpaulin), inline(always))]
443  pub const fn with_y(mut self, y: u32) -> Self {
444    self.y = y;
445    self
446  }
447  /// Sets the width (consuming builder).
448  #[must_use]
449  #[cfg_attr(not(tarpaulin), inline(always))]
450  pub const fn with_width(mut self, w: u32) -> Self {
451    self.width = w;
452    self
453  }
454  /// Sets the height (consuming builder).
455  #[must_use]
456  #[cfg_attr(not(tarpaulin), inline(always))]
457  pub const fn with_height(mut self, h: u32) -> Self {
458    self.height = h;
459    self
460  }
461
462  /// Sets the X coordinate in place.
463  #[cfg_attr(not(tarpaulin), inline(always))]
464  pub const fn set_x(&mut self, x: u32) -> &mut Self {
465    self.x = x;
466    self
467  }
468  /// Sets the Y coordinate in place.
469  #[cfg_attr(not(tarpaulin), inline(always))]
470  pub const fn set_y(&mut self, y: u32) -> &mut Self {
471    self.y = y;
472    self
473  }
474  /// Sets the width in place.
475  #[cfg_attr(not(tarpaulin), inline(always))]
476  pub const fn set_width(&mut self, w: u32) -> &mut Self {
477    self.width = w;
478    self
479  }
480  /// Sets the height in place.
481  #[cfg_attr(not(tarpaulin), inline(always))]
482  pub const fn set_height(&mut self, h: u32) -> &mut Self {
483    self.height = h;
484    self
485  }
486}
487
488/// Display rotation applied to the decoded picture before presentation.
489///
490/// Read from the FFmpeg display matrix side data
491/// (`AV_FRAME_DATA_DISPLAYMATRIX` → `av_display_rotation_get`, which
492/// returns a counter-clockwise angle in degrees) and from the
493/// WebCodecs `VideoFrame` rotation attribute. Only the four
494/// axis-aligned multiples of 90° are representable — every container
495/// rotation tag in practice is one of these. Any other / future /
496/// corrupt wire value is preserved verbatim as [`Self::Unknown`]
497/// rather than silently collapsed to a valid rotation (mirrors the
498/// lossless `Unknown(u32)` convention of the colour enums).
499///
500/// The angle is the **clockwise** rotation to apply for display
501/// (matching WebCodecs' `rotation`); callers normalising FFmpeg's
502/// counter-clockwise convention negate accordingly. [`Self::D0`] is
503/// the default (no rotation / square presentation).
504#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash, Display, IsVariant)]
505#[display("{}", self.as_str())]
506#[non_exhaustive]
507#[cfg_attr(
508  feature = "quickcheck",
509  derive(::quickcheck_richderive::Arbitrary),
510  quickcheck(arbitrary = "crate::quickcheck_helpers::coded::rotation")
511)]
512pub enum Rotation {
513  /// Unknown / unrecognised rotation wire value. The wrapped `u32`
514  /// is the original value passed to [`Self::from_u32`] — preserved
515  /// so the round-trip is lossless (no silent collapse to `D0`).
516  Unknown(u32),
517  /// No rotation.
518  #[default]
519  D0,
520  /// 90° clockwise.
521  D90,
522  /// 180°.
523  D180,
524  /// 270° clockwise (= 90° counter-clockwise).
525  D270,
526}
527
528impl Rotation {
529  /// Degree string for this rotation (`"0"` / `"90"` / `"180"` /
530  /// `"270"`); [`Self::Unknown`] renders as `"unknown"`.
531  #[cfg_attr(not(tarpaulin), inline(always))]
532  pub const fn as_str(&self) -> &'static str {
533    match self {
534      Self::Unknown(_) => "unknown",
535      Self::D0 => "0",
536      Self::D90 => "90",
537      Self::D180 => "180",
538      Self::D270 => "270",
539    }
540  }
541
542  /// Stable `u32` wire id: `0`/`1`/`2`/`3` for
543  /// `D0`/`D90`/`D180`/`D270`; [`Self::Unknown`] carries its
544  /// original value through unchanged so `from_u32(to_u32(x)) == x`
545  /// for every unrecognised `x`. Stable and append-only.
546  #[cfg_attr(not(tarpaulin), inline(always))]
547  pub const fn to_u32(&self) -> u32 {
548    match self {
549      Self::Unknown(v) => *v,
550      Self::D0 => 0,
551      Self::D90 => 1,
552      Self::D180 => 2,
553      Self::D270 => 3,
554    }
555  }
556
557  /// Decodes from the stable `u32` wire id produced by
558  /// [`Self::to_u32`]. Unrecognised values are preserved as
559  /// [`Self::Unknown`] (lossless) rather than mapped to a default.
560  #[cfg_attr(not(tarpaulin), inline(always))]
561  pub const fn from_u32(v: u32) -> Self {
562    match v {
563      0 => Self::D0,
564      1 => Self::D90,
565      2 => Self::D180,
566      3 => Self::D270,
567      _ => Self::Unknown(v),
568    }
569  }
570}
571
572/// Pixel (sample) aspect ratio — the ratio of a pixel's display
573/// width to its display height.
574///
575/// Read from `AVStream.sample_aspect_ratio` /
576/// `AVFrame.sample_aspect_ratio` (an FFmpeg `AVRational`) and from
577/// the WebCodecs display-size derivation. A `0:1` numerator in
578/// FFmpeg means "unknown"; callers normalise that to the `1:1`
579/// default (square pixels) before constructing this type.
580///
581/// `den` is a [`core::num::NonZeroU32`] so a SAR can never have a
582/// zero denominator; the manual [`Default`] is `1:1` (square),
583/// mirroring `mediatime::Timebase`'s non-proto-zero default.
584///
585/// Represented as a newtype over [`Rational`] — the single source of
586/// truth for "exact ratio with a non-zero denominator". The fields
587/// are private; the entire public method API (and the `buffa` wire
588/// format) is unchanged, delegating to the inner `Rational`.
589#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
590#[cfg_attr(
591  feature = "quickcheck",
592  derive(::quickcheck_richderive::Arbitrary),
593  quickcheck(arbitrary = "crate::quickcheck_helpers::coded::sample_aspect_ratio")
594)]
595#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
596pub struct SampleAspectRatio(Rational);
597
598impl Default for SampleAspectRatio {
599  /// `1:1` — square pixels.
600  #[cfg_attr(not(tarpaulin), inline(always))]
601  fn default() -> Self {
602    Self(Rational::default())
603  }
604}
605
606impl SampleAspectRatio {
607  /// Constructs a `SampleAspectRatio` from an explicit
608  /// numerator / (non-zero) denominator.
609  #[cfg_attr(not(tarpaulin), inline(always))]
610  pub const fn new(num: u32, den: core::num::NonZeroU32) -> Self {
611    Self(Rational::new(num, den))
612  }
613
614  /// Returns the numerator (display-width units).
615  #[cfg_attr(not(tarpaulin), inline(always))]
616  pub const fn num(&self) -> u32 {
617    self.0.num()
618  }
619
620  /// Returns the (non-zero) denominator (display-height units).
621  #[cfg_attr(not(tarpaulin), inline(always))]
622  pub const fn den(&self) -> core::num::NonZeroU32 {
623    self.0.den()
624  }
625
626  /// `true` when the pixels are square (`num == den`).
627  #[cfg_attr(not(tarpaulin), inline(always))]
628  pub const fn is_square(&self) -> bool {
629    self.0.num() == self.0.den().get()
630  }
631
632  /// Returns this SAR as a generic [`Rational`] — the underlying
633  /// representation. Purely additive interop; `SampleAspectRatio`'s
634  /// public method API is unchanged.
635  #[cfg_attr(not(tarpaulin), inline(always))]
636  pub const fn rational(&self) -> Rational {
637    self.0
638  }
639
640  /// Alias of [`Self::rational`] — views this SAR as a generic
641  /// [`Rational`].
642  #[cfg_attr(not(tarpaulin), inline(always))]
643  pub const fn as_rational(&self) -> Rational {
644    self.rational()
645  }
646
647  /// Sets the numerator (consuming builder).
648  #[must_use]
649  #[cfg_attr(not(tarpaulin), inline(always))]
650  pub const fn with_num(mut self, num: u32) -> Self {
651    self.0 = self.0.with_num(num);
652    self
653  }
654
655  /// Sets the denominator (consuming builder).
656  #[must_use]
657  #[cfg_attr(not(tarpaulin), inline(always))]
658  pub const fn with_den(mut self, den: core::num::NonZeroU32) -> Self {
659    self.0 = self.0.with_den(den);
660    self
661  }
662
663  /// Sets the numerator in place.
664  #[cfg_attr(not(tarpaulin), inline(always))]
665  pub const fn set_num(&mut self, num: u32) -> &mut Self {
666    self.0.set_num(num);
667    self
668  }
669
670  /// Sets the denominator in place.
671  #[cfg_attr(not(tarpaulin), inline(always))]
672  pub const fn set_den(&mut self, den: core::num::NonZeroU32) -> &mut Self {
673    self.0.set_den(den);
674    self
675  }
676}
677
678impl core::fmt::Display for SampleAspectRatio {
679  fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
680    write!(f, "{}:{}", self.0.num(), self.0.den())
681  }
682}
683
684impl From<SampleAspectRatio> for Rational {
685  /// Unwraps the inner [`Rational`] — `SampleAspectRatio` is a newtype
686  /// over `Rational`. Additive interop; `SampleAspectRatio`'s own
687  /// public method API is unchanged.
688  #[cfg_attr(not(tarpaulin), inline(always))]
689  fn from(sar: SampleAspectRatio) -> Self {
690    sar.0
691  }
692}
693
694impl From<Rational> for SampleAspectRatio {
695  /// Wraps a generic [`Rational`] as a pixel/sample aspect ratio.
696  #[cfg_attr(not(tarpaulin), inline(always))]
697  fn from(rate: Rational) -> Self {
698    Self(rate)
699  }
700}
701
702/// A generic exact ratio `num / den`.
703///
704/// The reusable rational primitive the rest of the frame layer builds
705/// on (e.g. [`FrameRate`]). `den` is a [`core::num::NonZeroU32`] so a
706/// ratio can never have a zero denominator; the manual [`Default`] is
707/// `1/1` (the multiplicative identity), mirroring
708/// [`SampleAspectRatio`]'s non-proto-zero default and
709/// `mediatime::Timebase`'s convention.
710///
711/// This is the format-agnostic numerator/denominator pair; semantic
712/// wrappers ([`SampleAspectRatio`] for pixel aspect, [`FrameRate`] for
713/// frames-per-second) carry the domain meaning. A `0` numerator is a
714/// valid representable state (e.g. an "unknown" FFmpeg `AVRational`
715/// `0/1`) — see [`Self::is_zero`].
716#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
717#[cfg_attr(
718  feature = "quickcheck",
719  derive(::quickcheck_richderive::Arbitrary),
720  quickcheck(arbitrary = "crate::quickcheck_helpers::coded::rational")
721)]
722#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
723pub struct Rational {
724  num: u32,
725  den: core::num::NonZeroU32,
726}
727
728impl Default for Rational {
729  /// `1/1` — the multiplicative identity.
730  #[cfg_attr(not(tarpaulin), inline(always))]
731  fn default() -> Self {
732    Self {
733      num: 1,
734      den: core::num::NonZeroU32::MIN,
735    }
736  }
737}
738
739impl Rational {
740  /// Constructs a `Rational` from an explicit
741  /// numerator / (non-zero) denominator.
742  #[cfg_attr(not(tarpaulin), inline(always))]
743  pub const fn new(num: u32, den: core::num::NonZeroU32) -> Self {
744    Self { num, den }
745  }
746
747  /// Returns the numerator.
748  #[cfg_attr(not(tarpaulin), inline(always))]
749  pub const fn num(&self) -> u32 {
750    self.num
751  }
752
753  /// Returns the (non-zero) denominator.
754  #[cfg_attr(not(tarpaulin), inline(always))]
755  pub const fn den(&self) -> core::num::NonZeroU32 {
756    self.den
757  }
758
759  /// `true` when the numerator is `0` (the ratio is exactly zero —
760  /// e.g. an "unknown" `0/1` FFmpeg `AVRational`).
761  #[cfg_attr(not(tarpaulin), inline(always))]
762  pub const fn is_zero(&self) -> bool {
763    self.num == 0
764  }
765
766  /// Sets the numerator (consuming builder).
767  #[must_use]
768  #[cfg_attr(not(tarpaulin), inline(always))]
769  pub const fn with_num(mut self, num: u32) -> Self {
770    self.num = num;
771    self
772  }
773
774  /// Sets the denominator (consuming builder).
775  #[must_use]
776  #[cfg_attr(not(tarpaulin), inline(always))]
777  pub const fn with_den(mut self, den: core::num::NonZeroU32) -> Self {
778    self.den = den;
779    self
780  }
781
782  /// Sets the numerator in place.
783  #[cfg_attr(not(tarpaulin), inline(always))]
784  pub const fn set_num(&mut self, num: u32) -> &mut Self {
785    self.num = num;
786    self
787  }
788
789  /// Sets the denominator in place.
790  #[cfg_attr(not(tarpaulin), inline(always))]
791  pub const fn set_den(&mut self, den: core::num::NonZeroU32) -> &mut Self {
792    self.den = den;
793    self
794  }
795}
796
797impl core::fmt::Display for Rational {
798  fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
799    write!(f, "{}/{}", self.num, self.den)
800  }
801}
802
803/// The frame rate of a video stream as an exact [`Rational`]
804/// (frames per second) plus a variable-frame-rate marker.
805///
806/// `rate` is the nominal frames-per-second ratio (e.g. `30000/1001`
807/// for NTSC, `25/1` for PAL). `is_vfr` records that the stream is
808/// variable-frame-rate, in which case `rate` is the average / nominal
809/// rate only and per-frame timing must be taken from the timestamps.
810///
811/// This is deliberately **not** [`mediatime::Timebase`]: a frame rate
812/// is *not* a presentation-timestamp timebase. They are reciprocal-ish
813/// but distinct concepts (a 30000/1001 fps stream is commonly carried
814/// on a 1/90000 or 1/1000 PTS timebase) — `mediatime` documents that
815/// distinction and intentionally models only the PTS timebase, so the
816/// frame-rate concept lives here as its own type.
817///
818/// The [`Default`] is `{ rate: Rational::default() (1/1),
819/// is_vfr: false }`.
820#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
821#[cfg_attr(
822  feature = "quickcheck",
823  derive(::quickcheck_richderive::Arbitrary),
824  quickcheck(arbitrary = "crate::quickcheck_helpers::coded::frame_rate")
825)]
826#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
827pub struct FrameRate {
828  rate: Rational,
829  is_vfr: bool,
830}
831
832impl FrameRate {
833  /// Constructs a `FrameRate` from an exact frames-per-second
834  /// [`Rational`] and a variable-frame-rate flag.
835  #[cfg_attr(not(tarpaulin), inline(always))]
836  pub const fn new(rate: Rational, is_vfr: bool) -> Self {
837    Self { rate, is_vfr }
838  }
839
840  /// Returns the nominal frames-per-second ratio.
841  #[cfg_attr(not(tarpaulin), inline(always))]
842  pub const fn rate(&self) -> Rational {
843    self.rate
844  }
845
846  /// `true` when the stream is variable-frame-rate (the [`Self::rate`]
847  /// is then an average / nominal value only).
848  #[cfg_attr(not(tarpaulin), inline(always))]
849  pub const fn is_vfr(&self) -> bool {
850    self.is_vfr
851  }
852
853  /// Sets the rate (consuming builder).
854  #[must_use]
855  #[cfg_attr(not(tarpaulin), inline(always))]
856  pub const fn with_rate(mut self, rate: Rational) -> Self {
857    self.rate = rate;
858    self
859  }
860
861  /// Marks the stream variable-frame-rate (`is_vfr = true`; consuming
862  /// builder).
863  #[must_use]
864  #[cfg_attr(not(tarpaulin), inline(always))]
865  pub const fn with_is_vfr(mut self) -> Self {
866    self.is_vfr = true;
867    self
868  }
869
870  /// Assigns the raw VFR flag (consuming builder).
871  #[must_use]
872  #[cfg_attr(not(tarpaulin), inline(always))]
873  pub const fn maybe_is_vfr(mut self, is_vfr: bool) -> Self {
874    self.is_vfr = is_vfr;
875    self
876  }
877
878  /// Sets the rate in place.
879  #[cfg_attr(not(tarpaulin), inline(always))]
880  pub const fn set_rate(&mut self, rate: Rational) -> &mut Self {
881    self.rate = rate;
882    self
883  }
884
885  /// Marks the stream variable-frame-rate (`is_vfr = true`) in place.
886  #[cfg_attr(not(tarpaulin), inline(always))]
887  pub const fn set_is_vfr(&mut self) -> &mut Self {
888    self.is_vfr = true;
889    self
890  }
891
892  /// Assigns the raw VFR flag in place.
893  #[cfg_attr(not(tarpaulin), inline(always))]
894  pub const fn update_is_vfr(&mut self, is_vfr: bool) -> &mut Self {
895    self.is_vfr = is_vfr;
896    self
897  }
898
899  /// Clears the VFR flag (`is_vfr = false`).
900  #[cfg_attr(not(tarpaulin), inline(always))]
901  pub const fn clear_is_vfr(&mut self) -> &mut Self {
902    self.is_vfr = false;
903    self
904  }
905}
906
907/// Interlacing / field order of a video stream.
908///
909/// Mirrors FFmpeg `AVFieldOrder`
910/// (`AVCodecContext::field_order` / `AVFrame` derived state) with the
911/// exact numeric code points: `AV_FIELD_UNKNOWN = 0`,
912/// `AV_FIELD_PROGRESSIVE = 1`, `AV_FIELD_TT = 2`,
913/// `AV_FIELD_BB = 3`, `AV_FIELD_TB = 4`, `AV_FIELD_BT = 5`. Any
914/// other / future / corrupt wire value is preserved verbatim as
915/// [`Self::Unknown`] rather than collapsed (mirrors the lossless
916/// `Unknown(u32)` convention of [`Rotation`] / the colour enums).
917///
918/// FFmpeg's own `AV_FIELD_UNKNOWN` sentinel is code `0`, so the
919/// [`Default`] is `Unknown(0)` — the same default-is-`Unknown(0)`
920/// precedent as [`PixelFormat`](crate::pixel_format::PixelFormat).
921#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Display, IsVariant)]
922#[display("{}", self.as_str())]
923#[non_exhaustive]
924#[cfg_attr(
925  feature = "quickcheck",
926  derive(::quickcheck_richderive::Arbitrary),
927  quickcheck(arbitrary = "crate::quickcheck_helpers::coded::field_order")
928)]
929pub enum FieldOrder {
930  /// Unknown / unrecognised field-order wire value. The wrapped
931  /// `u32` is the original value passed to [`Self::from_u32`] —
932  /// preserved so the round-trip is lossless. Also the [`Default`]
933  /// (`Unknown(0)`), since FFmpeg's `AV_FIELD_UNKNOWN` is code `0`.
934  Unknown(u32),
935  /// Progressive (not interlaced) — `AV_FIELD_PROGRESSIVE`.
936  Progressive,
937  /// Top coded first, top displayed first — `AV_FIELD_TT`.
938  Tt,
939  /// Bottom coded first, bottom displayed first — `AV_FIELD_BB`.
940  Bb,
941  /// Top coded first, bottom displayed first — `AV_FIELD_TB`.
942  Tb,
943  /// Bottom coded first, top displayed first — `AV_FIELD_BT`.
944  Bt,
945}
946
947impl Default for FieldOrder {
948  /// `Unknown(0)` — FFmpeg's `AV_FIELD_UNKNOWN` is code `0`.
949  #[cfg_attr(not(tarpaulin), inline(always))]
950  fn default() -> Self {
951    Self::Unknown(0)
952  }
953}
954
955impl FieldOrder {
956  /// Lowercase slug for this field order (`"progressive"` / `"tt"` /
957  /// `"bb"` / `"tb"` / `"bt"`); [`Self::Unknown`] renders as
958  /// `"unknown"`.
959  #[cfg_attr(not(tarpaulin), inline(always))]
960  pub const fn as_str(&self) -> &'static str {
961    match self {
962      Self::Unknown(_) => "unknown",
963      Self::Progressive => "progressive",
964      Self::Tt => "tt",
965      Self::Bb => "bb",
966      Self::Tb => "tb",
967      Self::Bt => "bt",
968    }
969  }
970
971  /// Stable `u32` wire id = the FFmpeg `AVFieldOrder` code
972  /// (`Unknown`→its carried value, `Progressive`=1, `Tt`=2, `Bb`=3,
973  /// `Tb`=4, `Bt`=5). [`Self::Unknown`] carries its original value
974  /// through unchanged so `from_u32(to_u32(x)) == x` for every
975  /// unrecognised `x`.
976  #[cfg_attr(not(tarpaulin), inline(always))]
977  pub const fn to_u32(&self) -> u32 {
978    match self {
979      Self::Unknown(v) => *v,
980      Self::Progressive => 1,
981      Self::Tt => 2,
982      Self::Bb => 3,
983      Self::Tb => 4,
984      Self::Bt => 5,
985    }
986  }
987
988  /// Decodes from the FFmpeg `AVFieldOrder` code produced by
989  /// [`Self::to_u32`]. The canonical `AV_FIELD_UNKNOWN` code `0`
990  /// (and any other unrecognised id) maps to [`Self::Unknown`]
991  /// carrying the original value, so the round-trip is lossless.
992  #[cfg_attr(not(tarpaulin), inline(always))]
993  pub const fn from_u32(v: u32) -> Self {
994    match v {
995      1 => Self::Progressive,
996      2 => Self::Tt,
997      3 => Self::Bb,
998      4 => Self::Tb,
999      5 => Self::Bt,
1000      _ => Self::Unknown(v),
1001    }
1002  }
1003}
1004
1005/// Stereoscopic-3D packing mode of a video stream.
1006///
1007/// Mirrors FFmpeg `AVStereo3DType` (the `AV_FRAME_DATA_STEREO3D`
1008/// side-data `type`) with the exact numeric code points:
1009/// `AV_STEREO3D_2D = 0` (named [`Self::Mono`]),
1010/// `AV_STEREO3D_SIDEBYSIDE = 1`, `AV_STEREO3D_TOPBOTTOM = 2`,
1011/// `AV_STEREO3D_FRAMESEQUENCE = 3`, `AV_STEREO3D_CHECKERBOARD = 4`,
1012/// `AV_STEREO3D_SIDEBYSIDE_QUINCUNX = 5`, `AV_STEREO3D_LINES = 6`,
1013/// `AV_STEREO3D_COLUMNS = 7`. Any other / future / corrupt wire
1014/// value is preserved verbatim as [`Self::Unknown`] (lossless
1015/// `Unknown(u32)` convention shared with [`Rotation`] / the colour
1016/// enums).
1017///
1018/// The [`Default`] is [`Self::Mono`] — a *real* code (value `0`,
1019/// FFmpeg `AV_STEREO3D_2D`, plain monoscopic video), so the default
1020/// is a named variant rather than `Unknown(0)` (the colour-enum
1021/// named-default precedent, e.g. `DcpTargetGamut::DciP3`), distinct
1022/// from [`FieldOrder`] whose `0` *is* FFmpeg's UNKNOWN sentinel.
1023#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Display, IsVariant)]
1024#[display("{}", self.as_str())]
1025#[non_exhaustive]
1026#[cfg_attr(
1027  feature = "quickcheck",
1028  derive(::quickcheck_richderive::Arbitrary),
1029  quickcheck(arbitrary = "crate::quickcheck_helpers::coded::stereo_mode")
1030)]
1031pub enum StereoMode {
1032  /// Unknown / unrecognised wire value. The wrapped `u32` is the
1033  /// original value passed to [`Self::from_u32`] — preserved so the
1034  /// round-trip is lossless.
1035  Unknown(u32),
1036  /// Plain monoscopic (non-stereo) video — `AV_STEREO3D_2D` (code
1037  /// `0`). The [`Default`].
1038  Mono,
1039  /// Side-by-side — `AV_STEREO3D_SIDEBYSIDE`.
1040  SideBySide,
1041  /// Top-bottom — `AV_STEREO3D_TOPBOTTOM`.
1042  TopBottom,
1043  /// Frame-sequential — `AV_STEREO3D_FRAMESEQUENCE`.
1044  FrameSequence,
1045  /// Checkerboard — `AV_STEREO3D_CHECKERBOARD`.
1046  Checkerboard,
1047  /// Side-by-side quincunx — `AV_STEREO3D_SIDEBYSIDE_QUINCUNX`.
1048  SideBySideQuincunx,
1049  /// Interleaved by rows — `AV_STEREO3D_LINES`.
1050  Lines,
1051  /// Interleaved by columns — `AV_STEREO3D_COLUMNS`.
1052  Columns,
1053}
1054
1055impl Default for StereoMode {
1056  /// [`Self::Mono`] — FFmpeg `AV_STEREO3D_2D` (code `0`), plain
1057  /// monoscopic video. A named variant (not `Unknown(0)`), the
1058  /// colour-enum named-default precedent.
1059  #[cfg_attr(not(tarpaulin), inline(always))]
1060  fn default() -> Self {
1061    Self::Mono
1062  }
1063}
1064
1065impl StereoMode {
1066  /// Lowercase slug for this stereo mode; [`Self::Unknown`] renders
1067  /// as `"unknown"`.
1068  #[cfg_attr(not(tarpaulin), inline(always))]
1069  pub const fn as_str(&self) -> &'static str {
1070    match self {
1071      Self::Unknown(_) => "unknown",
1072      Self::Mono => "mono",
1073      Self::SideBySide => "side-by-side",
1074      Self::TopBottom => "top-bottom",
1075      Self::FrameSequence => "frame-sequence",
1076      Self::Checkerboard => "checkerboard",
1077      Self::SideBySideQuincunx => "side-by-side-quincunx",
1078      Self::Lines => "lines",
1079      Self::Columns => "columns",
1080    }
1081  }
1082
1083  /// Stable `u32` wire id = the FFmpeg `AVStereo3DType` code
1084  /// (`Mono`=0, `SideBySide`=1, `TopBottom`=2, `FrameSequence`=3,
1085  /// `Checkerboard`=4, `SideBySideQuincunx`=5, `Lines`=6,
1086  /// `Columns`=7). [`Self::Unknown`] carries its original value
1087  /// through unchanged so `from_u32(to_u32(x)) == x` for every
1088  /// unrecognised `x`.
1089  #[cfg_attr(not(tarpaulin), inline(always))]
1090  pub const fn to_u32(&self) -> u32 {
1091    match self {
1092      Self::Unknown(v) => *v,
1093      Self::Mono => 0,
1094      Self::SideBySide => 1,
1095      Self::TopBottom => 2,
1096      Self::FrameSequence => 3,
1097      Self::Checkerboard => 4,
1098      Self::SideBySideQuincunx => 5,
1099      Self::Lines => 6,
1100      Self::Columns => 7,
1101    }
1102  }
1103
1104  /// Decodes from the FFmpeg `AVStereo3DType` code produced by
1105  /// [`Self::to_u32`]. The canonical codes map to their named
1106  /// variants (so a decoded value always round-trips); any other id
1107  /// maps to [`Self::Unknown`] carrying the original value, so the
1108  /// round-trip is lossless.
1109  #[cfg_attr(not(tarpaulin), inline(always))]
1110  pub const fn from_u32(v: u32) -> Self {
1111    match v {
1112      0 => Self::Mono,
1113      1 => Self::SideBySide,
1114      2 => Self::TopBottom,
1115      3 => Self::FrameSequence,
1116      4 => Self::Checkerboard,
1117      5 => Self::SideBySideQuincunx,
1118      6 => Self::Lines,
1119      7 => Self::Columns,
1120      _ => Self::Unknown(v),
1121    }
1122  }
1123}
1124
1125/// One plane of pixel data.
1126///
1127/// Generic over the buffer type `B` so the same `Plane` shape works
1128/// for owned (`Vec<u8>`, `bytes::Bytes`), borrowed (`&'a [u8]`), or
1129/// custom backend-supplied buffers. The bound `B: AsRef<[u8]>` lives
1130/// at the use site (`VideoFrame<P, B: AsRef<[u8]>, …>`); `Plane` itself
1131/// is unbounded so it can be used in const contexts.
1132///
1133/// `stride` is bytes per row for video planes, or total plane size
1134/// in bytes for audio planar formats.
1135#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
1136pub struct Plane<B> {
1137  data: B,
1138  stride: u32,
1139}
1140
1141impl<B> Plane<B> {
1142  /// Constructs a `Plane` from a buffer and a stride.
1143  #[cfg_attr(not(tarpaulin), inline(always))]
1144  pub const fn new(data: B, stride: u32) -> Self {
1145    Self { data, stride }
1146  }
1147
1148  /// Returns the stride in bytes.
1149  #[cfg_attr(not(tarpaulin), inline(always))]
1150  pub const fn stride(&self) -> u32 {
1151    self.stride
1152  }
1153
1154  /// Borrows the underlying buffer.
1155  #[cfg_attr(not(tarpaulin), inline(always))]
1156  pub const fn data_ref(&self) -> &B {
1157    &self.data
1158  }
1159
1160  /// Mutably borrows the underlying buffer.
1161  #[cfg_attr(not(tarpaulin), inline(always))]
1162  pub fn data_mut(&mut self) -> &mut B {
1163    &mut self.data
1164  }
1165
1166  /// Consumes the plane and returns the underlying buffer.
1167  #[cfg_attr(not(tarpaulin), inline(always))]
1168  pub fn into_data(self) -> B {
1169    self.data
1170  }
1171
1172  /// Sets the stride (consuming builder).
1173  #[must_use]
1174  #[cfg_attr(not(tarpaulin), inline(always))]
1175  pub const fn with_stride(mut self, stride: u32) -> Self {
1176    self.stride = stride;
1177    self
1178  }
1179
1180  /// Sets the stride in place.
1181  #[cfg_attr(not(tarpaulin), inline(always))]
1182  pub const fn set_stride(&mut self, stride: u32) -> &mut Self {
1183    self.stride = stride;
1184    self
1185  }
1186}
1187
1188/// A runtime-tagged video frame.
1189///
1190/// Generic parameters:
1191/// - `P` — pixel-format identifier. Typically [`crate::pixel_format::PixelFormat`]
1192///   in mediadecode-style runtime-tagged pipelines, but `P` is left unbounded
1193///   so backends can substitute a richer type (e.g. an FFmpeg
1194///   `AVPixelFormat` newtype that round-trips to `PixelFormat`).
1195/// - `B` — plane data buffer type. Each populated `Plane<B>` carries one
1196///   plane's bytes; `B: AsRef<[u8]>` at the consumer (e.g. `&'a [u8]`,
1197///   `Vec<u8>`, `bytes::Bytes`, refcounted FFmpeg buffer).
1198///
1199/// `dimensions` is the **coded** width / height; [`Self::visible_rect`]
1200/// (when present) is the displayable subregion (FFmpeg crop /
1201/// WebCodecs `visibleRect` / ProRes RAW `CleanAperture`).
1202///
1203/// `plane_count` is the number of populated entries in `planes`. Four
1204/// slots cover every realistic format: NV12 = 2, YUV420P = 3, YUVA /
1205/// packed-with-alpha = 4, packed RGB / Bayer CFA = 1.
1206///
1207/// **No timestamp.** PTS / duration ride on the orthogonal
1208/// [`TimestampedFrame<F>`] wrapper so the pixel-data layer stays
1209/// independent of the timekeeping layer.
1210#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
1211pub struct VideoFrame<P, B> {
1212  dimensions: Dimensions,
1213  visible_rect: Option<Rect>,
1214  pixel_format: P,
1215  plane_count: u8,
1216  planes: [Plane<B>; 4],
1217  color: crate::color::Info,
1218}
1219
1220impl<P, B> VideoFrame<P, B> {
1221  /// Constructs a `VideoFrame`. `visible_rect` defaults to `None`,
1222  /// color to `Info::UNSPECIFIED`.
1223  ///
1224  /// # Panics
1225  ///
1226  /// Panics if `plane_count > 4`. The fixed-size `planes` array has
1227  /// four slots; passing a larger `plane_count` would later trip
1228  /// slice indexing inside [`Self::planes`] far from the
1229  /// construction site. Asserting here fails fast.
1230  #[cfg_attr(not(tarpaulin), inline(always))]
1231  pub const fn new(
1232    dimensions: Dimensions,
1233    pixel_format: P,
1234    planes: [Plane<B>; 4],
1235    plane_count: u8,
1236  ) -> Self {
1237    assert!(
1238      plane_count as usize <= 4,
1239      "VideoFrame::new: plane_count exceeds the fixed 4-plane array",
1240    );
1241    Self {
1242      dimensions,
1243      visible_rect: None,
1244      pixel_format,
1245      plane_count,
1246      planes,
1247      color: crate::color::Info::UNSPECIFIED,
1248    }
1249  }
1250
1251  /// Returns the coded dimensions.
1252  #[cfg_attr(not(tarpaulin), inline(always))]
1253  pub const fn dimensions(&self) -> Dimensions {
1254    self.dimensions
1255  }
1256
1257  /// Returns the coded width (shortcut for `dimensions().width()`).
1258  #[cfg_attr(not(tarpaulin), inline(always))]
1259  pub const fn width(&self) -> u32 {
1260    self.dimensions.width()
1261  }
1262
1263  /// Returns the coded height (shortcut for `dimensions().height()`).
1264  #[cfg_attr(not(tarpaulin), inline(always))]
1265  pub const fn height(&self) -> u32 {
1266    self.dimensions.height()
1267  }
1268
1269  /// Returns the visible / clean-aperture rectangle, if any.
1270  #[cfg_attr(not(tarpaulin), inline(always))]
1271  pub const fn visible_rect(&self) -> Option<Rect> {
1272    self.visible_rect
1273  }
1274
1275  /// Returns a reference to the pixel-format identifier.
1276  #[cfg_attr(not(tarpaulin), inline(always))]
1277  pub const fn pixel_format_ref(&self) -> &P {
1278    &self.pixel_format
1279  }
1280
1281  /// Returns the populated plane count.
1282  #[cfg_attr(not(tarpaulin), inline(always))]
1283  pub const fn plane_count(&self) -> u8 {
1284    self.plane_count
1285  }
1286
1287  /// Returns the populated planes as a slice.
1288  #[cfg_attr(not(tarpaulin), inline(always))]
1289  pub fn planes(&self) -> &[Plane<B>] {
1290    &self.planes[..self.plane_count as usize]
1291  }
1292
1293  /// Returns one plane by index, or `None` if out of range.
1294  #[cfg_attr(not(tarpaulin), inline(always))]
1295  pub fn plane(&self, i: usize) -> Option<&Plane<B>> {
1296    if i < self.plane_count as usize {
1297      self.planes.get(i)
1298    } else {
1299      None
1300    }
1301  }
1302
1303  /// Returns the color metadata.
1304  #[cfg_attr(not(tarpaulin), inline(always))]
1305  pub const fn color(&self) -> crate::color::Info {
1306    self.color
1307  }
1308
1309  /// Sets the visible rect to `Some(v)` (consuming builder).
1310  #[must_use]
1311  #[cfg_attr(not(tarpaulin), inline(always))]
1312  pub const fn with_visible_rect(mut self, v: Rect) -> Self {
1313    self.visible_rect = Some(v);
1314    self
1315  }
1316
1317  /// Assigns the raw visible-rect wrapper (consuming builder).
1318  #[must_use]
1319  #[cfg_attr(not(tarpaulin), inline(always))]
1320  pub const fn maybe_visible_rect(mut self, v: Option<Rect>) -> Self {
1321    self.visible_rect = v;
1322    self
1323  }
1324
1325  /// Sets the color metadata (consuming builder).
1326  #[must_use]
1327  #[cfg_attr(not(tarpaulin), inline(always))]
1328  pub const fn with_color(mut self, v: crate::color::Info) -> Self {
1329    self.color = v;
1330    self
1331  }
1332
1333  /// Sets the visible rect to `Some(v)` in place.
1334  #[cfg_attr(not(tarpaulin), inline(always))]
1335  pub const fn set_visible_rect(&mut self, v: Rect) -> &mut Self {
1336    self.visible_rect = Some(v);
1337    self
1338  }
1339
1340  /// Assigns the raw visible-rect wrapper in place.
1341  #[cfg_attr(not(tarpaulin), inline(always))]
1342  pub const fn update_visible_rect(&mut self, v: Option<Rect>) -> &mut Self {
1343    self.visible_rect = v;
1344    self
1345  }
1346
1347  /// Clears the visible rect (`None`).
1348  #[cfg_attr(not(tarpaulin), inline(always))]
1349  pub const fn clear_visible_rect(&mut self) -> &mut Self {
1350    self.visible_rect = None;
1351    self
1352  }
1353
1354  /// Sets the color metadata in place.
1355  #[cfg_attr(not(tarpaulin), inline(always))]
1356  pub const fn set_color(&mut self, v: crate::color::Info) -> &mut Self {
1357    self.color = v;
1358    self
1359  }
1360}
1361
1362/// Wraps any inner `F` with optional PTS + duration timestamps.
1363///
1364/// This is the orthogonal time-carrying layer. The inner `F` stays
1365/// pure pixel data — `VideoFrame<P, B>` for runtime-tagged decoder
1366/// output, or a colconv-typed `Yuv420pFrame<'a, BE>` borrow type for
1367/// zero-copy conversion pipelines. Composition rather than inheritance
1368/// keeps the mediaframe data layer independent of any timekeeping
1369/// convention.
1370///
1371/// Timestamps use [`mediatime::Timestamp`], a rational-time type from
1372/// the `mediatime` crate (no_std, zero deps, exact arithmetic). Both
1373/// PTS and duration are `Option` because backends do not always know
1374/// them.
1375///
1376/// `duration` is deliberately the **same** `mediatime::Timestamp`
1377/// (timebase ticks) as `pts`, mirroring FFmpeg's `AVFrame.duration`
1378/// — an `int64` in the stream `time_base`, *not* a wall-clock value.
1379/// It is intentionally **not** a `core::time::Duration`: that would
1380/// lose exact rational-timebase precision and diverge from the
1381/// FFmpeg / `mediatime` model this crate faithfully mirrors. (Codex
1382/// adversarial-review F2 — reviewed and intentionally kept as-is.)
1383#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
1384pub struct TimestampedFrame<F> {
1385  pts: Option<mediatime::Timestamp>,
1386  // Timebase ticks, like FFmpeg `AVFrame.duration` — see type doc.
1387  duration: Option<mediatime::Timestamp>,
1388  frame: F,
1389}
1390
1391impl<F> TimestampedFrame<F> {
1392  /// Constructs a `TimestampedFrame`. PTS and duration default to
1393  /// `None`.
1394  #[cfg_attr(not(tarpaulin), inline(always))]
1395  pub const fn new(frame: F) -> Self {
1396    Self {
1397      pts: None,
1398      duration: None,
1399      frame,
1400    }
1401  }
1402
1403  /// Returns the presentation timestamp, if any.
1404  #[cfg_attr(not(tarpaulin), inline(always))]
1405  pub const fn pts(&self) -> Option<mediatime::Timestamp> {
1406    self.pts
1407  }
1408
1409  /// Returns the duration, if any.
1410  #[cfg_attr(not(tarpaulin), inline(always))]
1411  pub const fn duration(&self) -> Option<mediatime::Timestamp> {
1412    self.duration
1413  }
1414
1415  /// Borrows the inner frame.
1416  #[cfg_attr(not(tarpaulin), inline(always))]
1417  pub const fn frame_ref(&self) -> &F {
1418    &self.frame
1419  }
1420
1421  /// Mutably borrows the inner frame.
1422  #[cfg_attr(not(tarpaulin), inline(always))]
1423  pub const fn frame_mut(&mut self) -> &mut F {
1424    &mut self.frame
1425  }
1426
1427  /// Consumes the wrapper and returns the inner frame.
1428  #[cfg_attr(not(tarpaulin), inline(always))]
1429  pub fn into_frame(self) -> F {
1430    self.frame
1431  }
1432
1433  /// Sets the PTS to `Some(v)` (consuming builder).
1434  #[must_use]
1435  #[cfg_attr(not(tarpaulin), inline(always))]
1436  pub const fn with_pts(mut self, v: mediatime::Timestamp) -> Self {
1437    self.pts = Some(v);
1438    self
1439  }
1440
1441  /// Assigns the raw PTS wrapper (consuming builder).
1442  #[must_use]
1443  #[cfg_attr(not(tarpaulin), inline(always))]
1444  pub const fn maybe_pts(mut self, v: Option<mediatime::Timestamp>) -> Self {
1445    self.pts = v;
1446    self
1447  }
1448
1449  /// Sets the duration to `Some(v)` (consuming builder).
1450  #[must_use]
1451  #[cfg_attr(not(tarpaulin), inline(always))]
1452  pub const fn with_duration(mut self, v: mediatime::Timestamp) -> Self {
1453    self.duration = Some(v);
1454    self
1455  }
1456
1457  /// Assigns the raw duration wrapper (consuming builder).
1458  #[must_use]
1459  #[cfg_attr(not(tarpaulin), inline(always))]
1460  pub const fn maybe_duration(mut self, v: Option<mediatime::Timestamp>) -> Self {
1461    self.duration = v;
1462    self
1463  }
1464
1465  /// Sets the PTS to `Some(v)` in place.
1466  #[cfg_attr(not(tarpaulin), inline(always))]
1467  pub const fn set_pts(&mut self, v: mediatime::Timestamp) -> &mut Self {
1468    self.pts = Some(v);
1469    self
1470  }
1471
1472  /// Assigns the raw PTS wrapper in place.
1473  #[cfg_attr(not(tarpaulin), inline(always))]
1474  pub const fn update_pts(&mut self, v: Option<mediatime::Timestamp>) -> &mut Self {
1475    self.pts = v;
1476    self
1477  }
1478
1479  /// Clears the PTS (`None`).
1480  #[cfg_attr(not(tarpaulin), inline(always))]
1481  pub const fn clear_pts(&mut self) -> &mut Self {
1482    self.pts = None;
1483    self
1484  }
1485
1486  /// Sets the duration to `Some(v)` in place.
1487  #[cfg_attr(not(tarpaulin), inline(always))]
1488  pub const fn set_duration(&mut self, v: mediatime::Timestamp) -> &mut Self {
1489    self.duration = Some(v);
1490    self
1491  }
1492
1493  /// Assigns the raw duration wrapper in place.
1494  #[cfg_attr(not(tarpaulin), inline(always))]
1495  pub const fn update_duration(&mut self, v: Option<mediatime::Timestamp>) -> &mut Self {
1496    self.duration = v;
1497    self
1498  }
1499
1500  /// Clears the duration (`None`).
1501  #[cfg_attr(not(tarpaulin), inline(always))]
1502  pub const fn clear_duration(&mut self) -> &mut Self {
1503    self.duration = None;
1504    self
1505  }
1506}
1507
1508// === Per-family Frame modules (feature-gated) ===
1509
1510#[cfg(feature = "yuv-planar")]
1511#[cfg_attr(docsrs, doc(cfg(feature = "yuv-planar")))]
1512mod planar_8bit;
1513#[cfg(feature = "yuv-planar")]
1514#[cfg_attr(docsrs, doc(cfg(feature = "yuv-planar")))]
1515mod subsampled_high_bit_planar;
1516use derive_more::{Display, IsVariant};
1517#[cfg(feature = "yuv-planar")]
1518pub use planar_8bit::*;
1519#[cfg(feature = "yuv-planar")]
1520pub use subsampled_high_bit_planar::*;
1521
1522#[cfg(feature = "yuv-semi-planar")]
1523#[cfg_attr(docsrs, doc(cfg(feature = "yuv-semi-planar")))]
1524mod semi_planar_8bit;
1525#[cfg(feature = "yuv-semi-planar")]
1526#[cfg_attr(docsrs, doc(cfg(feature = "yuv-semi-planar")))]
1527mod subsampled_high_bit_pn;
1528#[cfg(feature = "yuv-semi-planar")]
1529pub use semi_planar_8bit::*;
1530#[cfg(feature = "yuv-semi-planar")]
1531pub use subsampled_high_bit_pn::*;
1532
1533#[cfg(feature = "yuva")]
1534#[cfg_attr(docsrs, doc(cfg(feature = "yuva")))]
1535mod yuva;
1536#[cfg(feature = "yuva")]
1537pub use yuva::*;
1538
1539#[cfg(feature = "yuv-packed")]
1540#[cfg_attr(docsrs, doc(cfg(feature = "yuv-packed")))]
1541mod packed_yuv_4_1_1;
1542#[cfg(feature = "yuv-packed")]
1543#[cfg_attr(docsrs, doc(cfg(feature = "yuv-packed")))]
1544mod packed_yuv_8bit;
1545#[cfg(feature = "yuv-packed")]
1546pub use packed_yuv_4_1_1::*;
1547#[cfg(feature = "yuv-packed")]
1548pub use packed_yuv_8bit::*;
1549
1550#[cfg(feature = "yuv-444-packed")]
1551#[cfg_attr(docsrs, doc(cfg(feature = "yuv-444-packed")))]
1552mod packed_yuv_4_4_4;
1553#[cfg(feature = "yuv-444-packed")]
1554pub use packed_yuv_4_4_4::*;
1555
1556#[cfg(feature = "y2xx")]
1557#[cfg_attr(docsrs, doc(cfg(feature = "y2xx")))]
1558mod y2xx;
1559#[cfg(feature = "y2xx")]
1560pub use y2xx::*;
1561
1562#[cfg(feature = "v210")]
1563#[cfg_attr(docsrs, doc(cfg(feature = "v210")))]
1564mod v210;
1565#[cfg(feature = "v210")]
1566pub use v210::*;
1567
1568#[cfg(feature = "rgb")]
1569#[cfg_attr(docsrs, doc(cfg(feature = "rgb")))]
1570mod packed_rgb_10bit;
1571#[cfg(feature = "rgb")]
1572#[cfg_attr(docsrs, doc(cfg(feature = "rgb")))]
1573mod packed_rgb_16bit;
1574#[cfg(feature = "rgb")]
1575#[cfg_attr(docsrs, doc(cfg(feature = "rgb")))]
1576mod packed_rgb_8bit;
1577#[cfg(feature = "rgb")]
1578pub use packed_rgb_8bit::*;
1579#[cfg(feature = "rgb")]
1580pub use packed_rgb_10bit::*;
1581#[cfg(feature = "rgb")]
1582pub use packed_rgb_16bit::*;
1583
1584#[cfg(feature = "rgb-float")]
1585#[cfg_attr(docsrs, doc(cfg(feature = "rgb-float")))]
1586mod packed_rgb_f16;
1587#[cfg(feature = "rgb-float")]
1588#[cfg_attr(docsrs, doc(cfg(feature = "rgb-float")))]
1589mod packed_rgb_float;
1590#[cfg(feature = "rgb-float")]
1591pub use packed_rgb_f16::*;
1592#[cfg(feature = "rgb-float")]
1593pub use packed_rgb_float::*;
1594
1595#[cfg(feature = "rgb-legacy")]
1596#[cfg_attr(docsrs, doc(cfg(feature = "rgb-legacy")))]
1597mod legacy_rgb;
1598#[cfg(feature = "rgb-legacy")]
1599pub use legacy_rgb::*;
1600
1601#[cfg(feature = "gbr")]
1602#[cfg_attr(docsrs, doc(cfg(feature = "gbr")))]
1603mod planar_gbr_8bit;
1604#[cfg(feature = "gbr")]
1605#[cfg_attr(docsrs, doc(cfg(feature = "gbr")))]
1606mod planar_gbr_float;
1607#[cfg(feature = "gbr")]
1608#[cfg_attr(docsrs, doc(cfg(feature = "gbr")))]
1609mod planar_gbr_high_bit;
1610#[cfg(feature = "gbr")]
1611pub use planar_gbr_8bit::*;
1612#[cfg(feature = "gbr")]
1613pub use planar_gbr_float::*;
1614#[cfg(feature = "gbr")]
1615pub use planar_gbr_high_bit::*;
1616
1617#[cfg(feature = "gray")]
1618#[cfg_attr(docsrs, doc(cfg(feature = "gray")))]
1619mod gray;
1620#[cfg(feature = "gray")]
1621pub use gray::*;
1622
1623#[cfg(feature = "bayer")]
1624#[cfg_attr(docsrs, doc(cfg(feature = "bayer")))]
1625mod bayer;
1626#[cfg(feature = "bayer")]
1627pub use bayer::*;
1628
1629#[cfg(feature = "xyz")]
1630#[cfg_attr(docsrs, doc(cfg(feature = "xyz")))]
1631mod xyz12;
1632#[cfg(feature = "xyz")]
1633pub use xyz12::*;
1634
1635#[cfg(feature = "mono")]
1636#[cfg_attr(docsrs, doc(cfg(feature = "mono")))]
1637mod mono1bit;
1638#[cfg(feature = "mono")]
1639#[cfg_attr(docsrs, doc(cfg(feature = "mono")))]
1640mod pal8;
1641#[cfg(feature = "mono")]
1642pub use mono1bit::*;
1643#[cfg(feature = "mono")]
1644pub use pal8::*;
1645
1646// === Tests ===
1647
1648#[cfg(test)]
1649mod tests_primitives {
1650  use super::*;
1651
1652  #[test]
1653  fn dimensions_construction_and_accessors() {
1654    let d = Dimensions::new(1920, 1080);
1655    assert_eq!(d.width(), 1920);
1656    assert_eq!(d.height(), 1080);
1657    assert!(!d.is_zero());
1658    assert!(Dimensions::default().is_zero());
1659  }
1660
1661  #[test]
1662  fn dimensions_builder() {
1663    let d = Dimensions::new(0, 0).with_width(640).with_height(480);
1664    assert_eq!(d.width(), 640);
1665    assert_eq!(d.height(), 480);
1666  }
1667
1668  #[cfg(feature = "std")]
1669  #[test]
1670  fn dimensions_display() {
1671    assert_eq!(std::format!("{}", Dimensions::new(1920, 1080)), "1920x1080");
1672  }
1673
1674  #[test]
1675  fn rect_construction_and_accessors() {
1676    let r = Rect::new(10, 20, 1280, 720);
1677    assert_eq!(r.x(), 10);
1678    assert_eq!(r.y(), 20);
1679    assert_eq!(r.width(), 1280);
1680    assert_eq!(r.height(), 720);
1681  }
1682
1683  #[test]
1684  fn rect_builder_chains() {
1685    let r = Rect::default()
1686      .with_x(8)
1687      .with_y(8)
1688      .with_width(640)
1689      .with_height(360);
1690    assert_eq!((r.x(), r.y(), r.width(), r.height()), (8, 8, 640, 360));
1691  }
1692
1693  #[test]
1694  fn rotation_defaults_and_as_str() {
1695    assert!(matches!(Rotation::default(), Rotation::D0));
1696    assert_eq!(Rotation::D0.as_str(), "0");
1697    assert_eq!(Rotation::D90.as_str(), "90");
1698    assert_eq!(Rotation::D180.as_str(), "180");
1699    assert_eq!(Rotation::D270.as_str(), "270");
1700    assert!(Rotation::D90.is_d_90());
1701  }
1702
1703  #[test]
1704  fn rotation_u32_round_trip_and_unknown() {
1705    for r in [
1706      Rotation::D0,
1707      Rotation::D90,
1708      Rotation::D180,
1709      Rotation::D270,
1710      Rotation::Unknown(99),
1711      Rotation::Unknown(4242),
1712    ] {
1713      assert_eq!(Rotation::from_u32(r.to_u32()), r);
1714    }
1715    assert_eq!(Rotation::from_u32(0), Rotation::D0);
1716    assert_eq!(Rotation::from_u32(3), Rotation::D270);
1717    // Unrecognised → preserved losslessly (no silent collapse to D0).
1718    assert_eq!(Rotation::from_u32(99), Rotation::Unknown(99));
1719    assert_eq!(Rotation::from_u32(99).to_u32(), 99);
1720  }
1721
1722  #[test]
1723  fn sample_aspect_ratio_default_is_square() {
1724    let s = SampleAspectRatio::default();
1725    assert_eq!(s.num(), 1);
1726    assert_eq!(s.den().get(), 1);
1727    assert!(s.is_square());
1728  }
1729
1730  #[test]
1731  fn sample_aspect_ratio_construction_and_builders() {
1732    let nz = |n: u32| core::num::NonZeroU32::new(n).unwrap();
1733    let s = SampleAspectRatio::new(40, nz(33));
1734    assert_eq!(s.num(), 40);
1735    assert_eq!(s.den().get(), 33);
1736    assert!(!s.is_square());
1737    let s2 = SampleAspectRatio::default().with_num(16).with_den(nz(9));
1738    assert_eq!((s2.num(), s2.den().get()), (16, 9));
1739    let mut s3 = SampleAspectRatio::default();
1740    s3.set_num(4).set_den(nz(3));
1741    assert_eq!((s3.num(), s3.den().get()), (4, 3));
1742  }
1743
1744  #[cfg(feature = "std")]
1745  #[test]
1746  fn sample_aspect_ratio_display() {
1747    let nz = core::num::NonZeroU32::new(11).unwrap();
1748    assert_eq!(std::format!("{}", SampleAspectRatio::new(10, nz)), "10:11");
1749  }
1750
1751  #[test]
1752  fn plane_holds_owned_buffer() {
1753    let p: Plane<[u8; 4]> = Plane::new([1, 2, 3, 4], 4);
1754    assert_eq!(p.stride(), 4);
1755    assert_eq!(p.data_ref(), &[1, 2, 3, 4]);
1756    let raw = p.into_data();
1757    assert_eq!(raw, [1, 2, 3, 4]);
1758  }
1759
1760  #[test]
1761  fn plane_holds_borrowed_buffer() {
1762    let backing = [10u8, 20, 30, 40];
1763    let p: Plane<&[u8]> = Plane::new(&backing[..], 2);
1764    assert_eq!(p.stride(), 2);
1765    assert_eq!(*p.data_ref(), &[10, 20, 30, 40][..]);
1766  }
1767
1768  #[test]
1769  fn plane_with_stride_builder() {
1770    let p = Plane::new([0u8; 2], 0).with_stride(64);
1771    assert_eq!(p.stride(), 64);
1772  }
1773
1774  // ---------- VideoFrame -------------------------------------------------
1775
1776  use crate::{color::Info, pixel_format::PixelFormat};
1777
1778  #[test]
1779  fn video_frame_construction_defaults() {
1780    let planes: [Plane<&[u8]>; 4] = [
1781      Plane::new(&[][..], 16),
1782      Plane::new(&[][..], 8),
1783      Plane::new(&[][..], 8),
1784      Plane::new(&[][..], 0),
1785    ];
1786    let vf = VideoFrame::new(Dimensions::new(16, 16), PixelFormat::Yuv420p, planes, 3);
1787    assert_eq!(vf.dimensions(), Dimensions::new(16, 16));
1788    assert_eq!(vf.width(), 16);
1789    assert_eq!(vf.height(), 16);
1790    assert_eq!(*vf.pixel_format_ref(), PixelFormat::Yuv420p);
1791    assert_eq!(vf.plane_count(), 3);
1792    assert!(vf.visible_rect().is_none());
1793    assert_eq!(vf.color(), Info::UNSPECIFIED);
1794  }
1795
1796  #[test]
1797  fn video_frame_planes_slice_uses_plane_count() {
1798    let planes: [Plane<u32>; 4] = [
1799      Plane::new(1, 0),
1800      Plane::new(2, 0),
1801      Plane::new(3, 0),
1802      Plane::new(4, 0),
1803    ];
1804    let vf = VideoFrame::new(Dimensions::new(2, 2), PixelFormat::Yuv420p, planes, 2);
1805    assert_eq!(vf.planes().len(), 2);
1806    assert_eq!(*vf.plane(0).unwrap().data_ref(), 1);
1807    assert_eq!(*vf.plane(1).unwrap().data_ref(), 2);
1808    assert!(vf.plane(2).is_none());
1809    assert!(vf.plane(7).is_none());
1810  }
1811
1812  #[test]
1813  #[should_panic(expected = "plane_count exceeds the fixed 4-plane array")]
1814  fn video_frame_new_panics_on_plane_count_over_4() {
1815    let planes: [Plane<()>; 4] = [Plane::new((), 0); 4];
1816    let _ = VideoFrame::new(Dimensions::new(1, 1), PixelFormat::Yuv420p, planes, 5);
1817  }
1818
1819  #[test]
1820  fn video_frame_with_visible_rect_and_color_chain() {
1821    let planes: [Plane<()>; 4] = [Plane::new((), 0); 4];
1822    let vf = VideoFrame::new(Dimensions::new(8, 8), PixelFormat::Yuv420p, planes, 3)
1823      .with_visible_rect(Rect::new(0, 0, 6, 6));
1824    assert_eq!(vf.visible_rect(), Some(Rect::new(0, 0, 6, 6)));
1825  }
1826
1827  // ---------- TimestampedFrame ------------------------------------------
1828
1829  #[test]
1830  fn timestamped_frame_construction_defaults() {
1831    let tf: TimestampedFrame<&'static str> = TimestampedFrame::new("payload");
1832    assert!(tf.pts().is_none());
1833    assert!(tf.duration().is_none());
1834    assert_eq!(*tf.frame_ref(), "payload");
1835  }
1836
1837  #[test]
1838  fn timestamped_frame_into_frame_consumes() {
1839    let tf = TimestampedFrame::new(42u32);
1840    let raw = tf.into_frame();
1841    assert_eq!(raw, 42);
1842  }
1843
1844  #[test]
1845  fn timestamped_frame_pts_builder() {
1846    let tb = mediatime::Timebase::new(1, core::num::NonZeroU32::new(1000).unwrap());
1847    let ts = mediatime::Timestamp::new(1000, tb);
1848    let tf = TimestampedFrame::new(0u8).with_pts(ts).with_duration(ts);
1849    assert_eq!(tf.pts(), Some(ts));
1850    assert_eq!(tf.duration(), Some(ts));
1851  }
1852
1853  #[test]
1854  fn timestamped_frame_wraps_video_frame() {
1855    let planes: [Plane<()>; 4] = [Plane::new((), 0); 4];
1856    let vf = VideoFrame::new(Dimensions::new(4, 4), PixelFormat::Yuv420p, planes, 3);
1857    let tf = TimestampedFrame::new(vf);
1858    assert_eq!(tf.frame_ref().dimensions(), Dimensions::new(4, 4));
1859  }
1860
1861  // ---------- Rational --------------------------------------------------
1862
1863  #[test]
1864  fn rational_default_is_one_over_one() {
1865    let r = Rational::default();
1866    assert_eq!(r.num(), 1);
1867    assert_eq!(r.den().get(), 1);
1868    assert!(!r.is_zero());
1869  }
1870
1871  #[test]
1872  fn rational_construction_builders_and_is_zero() {
1873    let nz = |n: u32| core::num::NonZeroU32::new(n).unwrap();
1874    let r = Rational::new(30000, nz(1001));
1875    assert_eq!(r.num(), 30000);
1876    assert_eq!(r.den().get(), 1001);
1877    assert!(!r.is_zero());
1878    let z = Rational::new(0, nz(1));
1879    assert!(z.is_zero());
1880    let r2 = Rational::default().with_num(24).with_den(nz(1));
1881    assert_eq!((r2.num(), r2.den().get()), (24, 1));
1882    let mut r3 = Rational::default();
1883    r3.set_num(16).set_den(nz(9));
1884    assert_eq!((r3.num(), r3.den().get()), (16, 9));
1885  }
1886
1887  #[cfg(feature = "std")]
1888  #[test]
1889  fn rational_display() {
1890    let nz = core::num::NonZeroU32::new(1001).unwrap();
1891    assert_eq!(std::format!("{}", Rational::new(30000, nz)), "30000/1001");
1892  }
1893
1894  // ---------- SampleAspectRatio ↔ Rational interop ----------------------
1895
1896  #[test]
1897  fn sample_aspect_ratio_rational_interop() {
1898    let nz = |n: u32| core::num::NonZeroU32::new(n).unwrap();
1899    let sar = SampleAspectRatio::new(40, nz(33));
1900    let via_method: Rational = sar.as_rational();
1901    let via_from: Rational = Rational::from(sar);
1902    let via_into: Rational = sar.into();
1903    assert_eq!(via_method, Rational::new(40, nz(33)));
1904    assert_eq!(via_method, via_from);
1905    assert_eq!(via_from, via_into);
1906    // Default 1:1 SAR maps to the 1/1 Rational default.
1907    assert_eq!(
1908      SampleAspectRatio::default().as_rational(),
1909      Rational::default()
1910    );
1911  }
1912
1913  #[test]
1914  fn sample_aspect_ratio_rational_round_trip_both_ways() {
1915    let nz = |n: u32| core::num::NonZeroU32::new(n).unwrap();
1916    // SAR -> Rational -> SAR
1917    let sar = SampleAspectRatio::new(40, nz(33));
1918    let r: Rational = sar.into();
1919    let back: SampleAspectRatio = r.into();
1920    assert_eq!(back, sar);
1921    assert_eq!(sar.rational(), r);
1922    assert_eq!(sar.rational(), sar.as_rational());
1923    // Rational -> SAR -> Rational
1924    let r2 = Rational::new(16, nz(9));
1925    let s2 = SampleAspectRatio::from(r2);
1926    assert_eq!((s2.num(), s2.den().get()), (16, 9));
1927    assert_eq!(Rational::from(s2), r2);
1928  }
1929
1930  #[test]
1931  fn sample_aspect_ratio_default_is_one_to_one() {
1932    let d = SampleAspectRatio::default();
1933    assert_eq!((d.num(), d.den().get()), (1, 1));
1934    assert!(d.is_square());
1935    assert_eq!(d, SampleAspectRatio::new(1, core::num::NonZeroU32::MIN));
1936  }
1937
1938  #[test]
1939  fn sample_aspect_ratio_eq_and_hash_parity() {
1940    use core::hash::{Hash, Hasher};
1941    let nz = |n: u32| core::num::NonZeroU32::new(n).unwrap();
1942    let a = SampleAspectRatio::new(40, nz(33));
1943    let b = SampleAspectRatio::default().with_num(40).with_den(nz(33));
1944    assert_eq!(a, b);
1945
1946    fn h(s: &SampleAspectRatio) -> u64 {
1947      // `no_std`-friendly deterministic hasher (FNV-1a).
1948      struct Fnv(u64);
1949      impl Hasher for Fnv {
1950        fn finish(&self) -> u64 {
1951          self.0
1952        }
1953        fn write(&mut self, bytes: &[u8]) {
1954          for &x in bytes {
1955            self.0 = (self.0 ^ x as u64).wrapping_mul(0x0100_0000_01b3);
1956          }
1957        }
1958      }
1959      let mut hasher = Fnv(0xcbf2_9ce4_8422_2325);
1960      s.hash(&mut hasher);
1961      hasher.finish()
1962    }
1963    assert_eq!(h(&a), h(&b));
1964  }
1965
1966  // ---------- FrameRate -------------------------------------------------
1967
1968  #[test]
1969  fn frame_rate_default_is_one_over_one_cfr() {
1970    let fr = FrameRate::default();
1971    assert_eq!(fr.rate(), Rational::default());
1972    assert!(!fr.is_vfr());
1973  }
1974
1975  #[test]
1976  fn frame_rate_construction_and_builders() {
1977    let nz = |n: u32| core::num::NonZeroU32::new(n).unwrap();
1978    let ntsc = Rational::new(30000, nz(1001));
1979    let fr = FrameRate::new(ntsc, false);
1980    assert_eq!(fr.rate(), ntsc);
1981    assert!(!fr.is_vfr());
1982    let vfr = FrameRate::default().with_rate(ntsc).with_is_vfr();
1983    assert_eq!(vfr.rate(), ntsc);
1984    assert!(vfr.is_vfr());
1985    let mut fr3 = FrameRate::default();
1986    fr3.set_rate(Rational::new(25, nz(1))).set_is_vfr();
1987    assert_eq!(fr3.rate(), Rational::new(25, nz(1)));
1988    assert!(fr3.is_vfr());
1989    // raw-wrapper + clear forms
1990    let fr4 = FrameRate::default().maybe_is_vfr(true);
1991    assert!(fr4.is_vfr());
1992    let mut fr5 = FrameRate::default();
1993    fr5.update_is_vfr(true);
1994    assert!(fr5.is_vfr());
1995    fr5.clear_is_vfr();
1996    assert!(!fr5.is_vfr());
1997  }
1998
1999  // ---------- FieldOrder ------------------------------------------------
2000
2001  #[test]
2002  fn field_order_default_is_unknown_zero_and_as_str() {
2003    assert_eq!(FieldOrder::default(), FieldOrder::Unknown(0));
2004    assert_eq!(FieldOrder::Unknown(0).as_str(), "unknown");
2005    assert_eq!(FieldOrder::Progressive.as_str(), "progressive");
2006    assert_eq!(FieldOrder::Tt.as_str(), "tt");
2007    assert_eq!(FieldOrder::Bb.as_str(), "bb");
2008    assert_eq!(FieldOrder::Tb.as_str(), "tb");
2009    assert_eq!(FieldOrder::Bt.as_str(), "bt");
2010    assert!(FieldOrder::Progressive.is_progressive());
2011  }
2012
2013  #[test]
2014  fn field_order_u32_round_trip_and_unknown() {
2015    for f in [
2016      FieldOrder::Progressive,
2017      FieldOrder::Tt,
2018      FieldOrder::Bb,
2019      FieldOrder::Tb,
2020      FieldOrder::Bt,
2021      FieldOrder::Unknown(0),
2022      FieldOrder::Unknown(99),
2023      FieldOrder::Unknown(4242),
2024    ] {
2025      assert_eq!(FieldOrder::from_u32(f.to_u32()), f);
2026    }
2027    assert_eq!(FieldOrder::from_u32(1), FieldOrder::Progressive);
2028    assert_eq!(FieldOrder::from_u32(5), FieldOrder::Bt);
2029    // FFmpeg's own UNKNOWN sentinel (0) decodes to Unknown(0).
2030    assert_eq!(FieldOrder::from_u32(0), FieldOrder::Unknown(0));
2031    assert_eq!(FieldOrder::from_u32(99), FieldOrder::Unknown(99));
2032    assert_eq!(FieldOrder::from_u32(99).to_u32(), 99);
2033  }
2034
2035  // ---------- StereoMode ------------------------------------------------
2036
2037  #[test]
2038  fn stereo_mode_default_is_mono_and_as_str() {
2039    assert_eq!(StereoMode::default(), StereoMode::Mono);
2040    assert_eq!(StereoMode::Mono.as_str(), "mono");
2041    assert_eq!(StereoMode::SideBySide.as_str(), "side-by-side");
2042    assert_eq!(StereoMode::Columns.as_str(), "columns");
2043    assert_eq!(StereoMode::Unknown(0).as_str(), "unknown");
2044    assert!(StereoMode::Mono.is_mono());
2045  }
2046
2047  #[test]
2048  fn stereo_mode_u32_round_trip_and_unknown() {
2049    for s in [
2050      StereoMode::Mono,
2051      StereoMode::SideBySide,
2052      StereoMode::TopBottom,
2053      StereoMode::FrameSequence,
2054      StereoMode::Checkerboard,
2055      StereoMode::SideBySideQuincunx,
2056      StereoMode::Lines,
2057      StereoMode::Columns,
2058      StereoMode::Unknown(99),
2059      StereoMode::Unknown(4242),
2060    ] {
2061      assert_eq!(StereoMode::from_u32(s.to_u32()), s);
2062    }
2063    assert_eq!(StereoMode::from_u32(0), StereoMode::Mono);
2064    assert_eq!(StereoMode::from_u32(7), StereoMode::Columns);
2065    // Unrecognised → preserved losslessly.
2066    assert_eq!(StereoMode::from_u32(99), StereoMode::Unknown(99));
2067    assert_eq!(StereoMode::from_u32(99).to_u32(), 99);
2068  }
2069}
2070
2071// === Frame-family tests (feature-gated) ===
2072
2073#[cfg(all(test, any(feature = "std", feature = "alloc")))]
2074mod tests;