Skip to main content

mediadecode/
frame.rs

1//! Frame types and supporting building blocks.
2//!
3//! The frame structural primitives `Dimensions`, `Rect`, and `Plane<B>`
4//! are re-exported from `mediaframe::frame` — they live in the lowest-
5//! layer crate so colconv, mediadecode, and scenesdetect share a single
6//! canonical definition.
7//!
8//! `VideoFrame<P, E, D>`, `AudioFrame<S, C, E, D>`,
9//! `SubtitleFrame<E, D>` and `ImageFrame<P, E, D>` remain in
10//! mediadecode because they carry timestamp + backend-extras layers
11//! that are mediadecode's domain (`mediaframe` stays the pure
12//! pixel-data layer).
13//!
14//! # The four households
15//!
16//! Three of them are on the timeline and carry `pts` / `duration`.
17//! [`ImageFrame`] is the fourth and carries neither: a still image is
18//! not on the timeline, which is the same fact
19//! [`AttachmentPacket`](crate::demuxer::AttachmentPacket) states one
20//! tier down on the packet side. A household that has no timestamps
21//! has no seats for them either — an `Option<Timestamp>` that is
22//! always `None` is a field that invites a caller to look.
23//!
24//! # `Clone` on a frame is a refcount bump
25//!
26//! All four derive `Clone`, which makes the derive's per-parameter
27//! bound (`D: Clone`, `E: Clone`, …) exactly what cloning the fields
28//! requires. What that costs is the backend's business: the
29//! [D-seat amputation contract](crate::adapter#the-d-seat-amputation-contract)
30//! is what makes it a refcount bump rather than a pixel copy.
31
32pub use mediaframe::frame::{Dimensions, Plane, Rect};
33
34use derive_more::IsVariant;
35use thiserror::Error;
36
37use crate::{Timestamp, color::ColorInfo, subtitle::SubtitlePayload};
38
39/// Errors returned by the `try_new` constructors on the frame types.
40#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, IsVariant, Error)]
41#[non_exhaustive]
42pub enum FrameError {
43  /// `VideoFrame::try_new` was called with `plane_count > 4`. The
44  /// fixed plane array has exactly 4 slots; `plane_count` values
45  /// up to and including 4 are accepted, larger values would
46  /// later panic inside [`VideoFrame::planes`] far from the
47  /// construction site. See [`TooManyVideoPlanes`] for the
48  /// payload details. `#[from]` gives a free
49  /// `impl From<TooManyVideoPlanes> for FrameError`, so inner
50  /// helpers that return `Result<_, TooManyVideoPlanes>` can be
51  /// `?`-propagated into `FrameError` directly.
52  #[error(transparent)]
53  TooManyVideoPlanes(#[from] TooManyVideoPlanes),
54  /// `AudioFrame::try_new` was called with `plane_count > 8`. The
55  /// fixed plane array has exactly 8 slots (matches FFmpeg's
56  /// `AV_NUM_DATA_POINTERS`). See [`TooManyAudioPlanes`] for the
57  /// payload details. `#[from]` gives a free
58  /// `impl From<TooManyAudioPlanes> for FrameError`.
59  #[error(transparent)]
60  TooManyAudioPlanes(#[from] TooManyAudioPlanes),
61  /// `ImageFrame::try_new` was called with `plane_count > 4`. The
62  /// fixed plane array has exactly 4 slots — the same cap the video
63  /// household carries, and for the same reason: packed RGB is 1,
64  /// MJPEG's YUV is 3, and an alpha channel makes 4. See
65  /// [`TooManyImagePlanes`] for the payload details. `#[from]` gives a
66  /// free `impl From<TooManyImagePlanes> for FrameError`.
67  #[error(transparent)]
68  TooManyImagePlanes(#[from] TooManyImagePlanes),
69}
70
71/// Payload for [`FrameError::TooManyVideoPlanes`].
72#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Error)]
73#[error("VideoFrame: plane_count {plane_count} exceeds the fixed 4-plane array")]
74pub struct TooManyVideoPlanes {
75  /// The out-of-range `plane_count` value the caller supplied.
76  plane_count: u8,
77}
78
79impl TooManyVideoPlanes {
80  /// Constructs a new [`TooManyVideoPlanes`] payload.
81  #[inline]
82  pub const fn new(plane_count: u8) -> Self {
83    Self { plane_count }
84  }
85  /// The out-of-range `plane_count` value the caller supplied.
86  #[inline]
87  pub const fn plane_count(&self) -> u8 {
88    self.plane_count
89  }
90}
91
92/// Payload for [`FrameError::TooManyAudioPlanes`].
93#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Error)]
94#[error("AudioFrame: plane_count {plane_count} exceeds the fixed 8-plane array")]
95pub struct TooManyAudioPlanes {
96  /// The out-of-range `plane_count` value the caller supplied.
97  plane_count: u8,
98}
99
100impl TooManyAudioPlanes {
101  /// Constructs a new [`TooManyAudioPlanes`] payload.
102  #[inline]
103  pub const fn new(plane_count: u8) -> Self {
104    Self { plane_count }
105  }
106  /// The out-of-range `plane_count` value the caller supplied.
107  #[inline]
108  pub const fn plane_count(&self) -> u8 {
109    self.plane_count
110  }
111}
112
113/// Payload for [`FrameError::TooManyImagePlanes`].
114#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Error)]
115#[error("ImageFrame: plane_count {plane_count} exceeds the fixed 4-plane array")]
116pub struct TooManyImagePlanes {
117  /// The out-of-range `plane_count` value the caller supplied.
118  plane_count: u8,
119}
120
121impl TooManyImagePlanes {
122  /// Constructs a new [`TooManyImagePlanes`] payload.
123  #[inline]
124  pub const fn new(plane_count: u8) -> Self {
125    Self { plane_count }
126  }
127  /// The out-of-range `plane_count` value the caller supplied.
128  #[inline]
129  pub const fn plane_count(&self) -> u8 {
130    self.plane_count
131  }
132}
133
134/// A decoded video frame.
135///
136/// Generic parameters:
137/// - `P` — pixel-format identifier (e.g. `mediadecode_ffmpeg::PixelFormat`).
138/// - `E` — backend-specific frame extras (HDR mastering display, RAW
139///   sensor metadata, picture type, …).
140/// - `D` — plane data buffer type. Each populated `Plane<D>` carries one
141///   plane's bytes; `D: AsRef<[u8]>` at the use site (e.g. `Bytes`,
142///   `&'a [u8]`, refcounted FFmpeg buffer).
143///
144/// `width` / `height` are the **coded** dimensions; `visible_rect`
145/// (when present) is the displayable subregion (FFmpeg crop /
146/// WebCodecs `visibleRect` / ProRes RAW `CleanAperture`).
147///
148/// `plane_count` is the number of populated entries in `planes`.
149/// Four slots cover every realistic format: NV12 = 2, YUV420P = 3,
150/// YUVA / packed-with-alpha = 4, packed RGB / Bayer CFA = 1.
151///
152/// `Clone` derives: every field is a concrete `Copy` type, a
153/// `ColorInfo`, or one of `P` / `E` / `D` themselves, so the derive's
154/// per-parameter bound is exactly what cloning the fields requires —
155/// the same reasoning [`crate::packet::VideoPacket`]'s own derive
156/// rests on.
157#[derive(Clone)]
158pub struct VideoFrame<P, E, D> {
159  pts: Option<Timestamp>,
160  duration: Option<Timestamp>,
161  dimensions: Dimensions,
162  visible_rect: Option<Rect>,
163  pixel_format: P,
164  plane_count: u8,
165  planes: [Plane<D>; 4],
166  color: ColorInfo,
167  extra: E,
168}
169
170impl<P, E, D> VideoFrame<P, E, D> {
171  /// Constructs a `VideoFrame`. Timestamps default to `None`,
172  /// `visible_rect` to `None`, color to `ColorInfo::UNSPECIFIED`.
173  ///
174  /// `dimensions` is the coded width/height pair (see
175  /// [`Dimensions`] and [`Self::dimensions`] for the visible-vs-
176  /// coded distinction).
177  ///
178  /// # Panics
179  ///
180  /// Panics if `plane_count > 4`. The fixed-size `planes` array
181  /// has four slots; passing a larger `plane_count` would later
182  /// trip the slice indexing inside [`Self::planes`] far from
183  /// the construction site. Asserting here fails fast with a
184  /// clear message instead. Prefer [`Self::try_new`] when
185  /// `plane_count` can't be statically proven `<= 4`.
186  #[cfg_attr(not(tarpaulin), inline(always))]
187  pub const fn new(
188    dimensions: Dimensions,
189    pixel_format: P,
190    planes: [Plane<D>; 4],
191    plane_count: u8,
192    extra: E,
193  ) -> Self {
194    assert!(
195      plane_count as usize <= 4,
196      "VideoFrame::new: plane_count exceeds the fixed 4-plane array",
197    );
198    Self {
199      pts: None,
200      duration: None,
201      dimensions,
202      visible_rect: None,
203      pixel_format,
204      plane_count,
205      planes,
206      color: ColorInfo::UNSPECIFIED,
207      extra,
208    }
209  }
210
211  /// Fallible counterpart to [`Self::new`]. Returns
212  /// [`FrameError::TooManyVideoPlanes`] when `plane_count > 4`
213  /// (the fixed plane array's capacity) rather than panicking.
214  ///
215  /// Not `const fn` — returning `Result<Self, _>` would require
216  /// dropping the moved generic-typed `planes` / `pixel_format` /
217  /// `extra` on the error branch, which the const evaluator
218  /// can't prove safe for arbitrary `P` / `E` / `D`.
219  #[cfg_attr(not(tarpaulin), inline(always))]
220  pub fn try_new(
221    dimensions: Dimensions,
222    pixel_format: P,
223    planes: [Plane<D>; 4],
224    plane_count: u8,
225    extra: E,
226  ) -> Result<Self, FrameError> {
227    if plane_count as usize > 4 {
228      return Err(FrameError::TooManyVideoPlanes(TooManyVideoPlanes::new(
229        plane_count,
230      )));
231    }
232    Ok(Self {
233      pts: None,
234      duration: None,
235      dimensions,
236      visible_rect: None,
237      pixel_format,
238      plane_count,
239      planes,
240      color: ColorInfo::UNSPECIFIED,
241      extra,
242    })
243  }
244
245  /// Returns the presentation timestamp.
246  #[cfg_attr(not(tarpaulin), inline(always))]
247  pub const fn pts(&self) -> Option<Timestamp> {
248    self.pts
249  }
250  /// Returns the duration.
251  #[cfg_attr(not(tarpaulin), inline(always))]
252  pub const fn duration(&self) -> Option<Timestamp> {
253    self.duration
254  }
255  /// Returns the coded dimensions.
256  #[cfg_attr(not(tarpaulin), inline(always))]
257  pub const fn dimensions(&self) -> Dimensions {
258    self.dimensions
259  }
260  /// Returns the coded width.
261  #[cfg_attr(not(tarpaulin), inline(always))]
262  pub const fn width(&self) -> u32 {
263    self.dimensions.width()
264  }
265  /// Returns the coded height.
266  #[cfg_attr(not(tarpaulin), inline(always))]
267  pub const fn height(&self) -> u32 {
268    self.dimensions.height()
269  }
270  /// Returns the visible / clean-aperture rectangle, if any.
271  #[cfg_attr(not(tarpaulin), inline(always))]
272  pub const fn visible_rect(&self) -> Option<Rect> {
273    self.visible_rect
274  }
275  /// Returns a reference to the pixel format identifier.
276  #[cfg_attr(not(tarpaulin), inline(always))]
277  pub const fn pixel_format(&self) -> &P {
278    &self.pixel_format
279  }
280  /// Returns the populated plane count.
281  #[cfg_attr(not(tarpaulin), inline(always))]
282  pub const fn plane_count(&self) -> u8 {
283    self.plane_count
284  }
285  /// Returns the populated planes as a slice.
286  #[cfg_attr(not(tarpaulin), inline(always))]
287  pub fn planes(&self) -> &[Plane<D>] {
288    &self.planes[..self.plane_count as usize]
289  }
290  /// Returns one plane by index, or `None` if out of range.
291  #[cfg_attr(not(tarpaulin), inline(always))]
292  pub fn plane(&self, i: usize) -> Option<&Plane<D>> {
293    if i < self.plane_count as usize {
294      self.planes.get(i)
295    } else {
296      None
297    }
298  }
299  /// Returns the color metadata.
300  ///
301  /// Not `const`: mediaframe 0.3 dropped `Copy` from [`ColorInfo`]
302  /// (its member vocabularies carry an owned `Other` arm at the
303  /// `alloc` tier), so the by-value accessor clones — the shape
304  /// mediaframe's own `Info` accessors use.
305  #[cfg_attr(not(tarpaulin), inline(always))]
306  pub fn color(&self) -> ColorInfo {
307    self.color.clone()
308  }
309  /// Returns the backend extras.
310  #[cfg_attr(not(tarpaulin), inline(always))]
311  pub const fn extra(&self) -> &E {
312    &self.extra
313  }
314  /// Returns a mutable reference to the backend extras.
315  #[cfg_attr(not(tarpaulin), inline(always))]
316  pub const fn extra_mut(&mut self) -> &mut E {
317    &mut self.extra
318  }
319
320  /// Sets the PTS (consuming builder).
321  #[cfg_attr(not(tarpaulin), inline(always))]
322  #[must_use]
323  pub const fn with_pts(mut self, v: Option<Timestamp>) -> Self {
324    self.pts = v;
325    self
326  }
327  /// Sets the duration (consuming builder).
328  #[cfg_attr(not(tarpaulin), inline(always))]
329  #[must_use]
330  pub const fn with_duration(mut self, v: Option<Timestamp>) -> Self {
331    self.duration = v;
332    self
333  }
334  /// Sets the visible rect (consuming builder).
335  #[cfg_attr(not(tarpaulin), inline(always))]
336  #[must_use]
337  pub const fn with_visible_rect(mut self, v: Option<Rect>) -> Self {
338    self.visible_rect = v;
339    self
340  }
341  /// Sets the color metadata (consuming builder).
342  ///
343  /// Not `const`: assigning the field drops the previous
344  /// [`ColorInfo`], and mediaframe 0.3's `Info` has a destructor
345  /// whenever mediaframe's `alloc` feature is on (its member
346  /// vocabularies then carry an owned `Other` arm). A const
347  /// destructor is not evaluable, and feature unification can turn
348  /// that feature on from anywhere in the dependency graph, so the
349  /// `const` cannot be kept at either tier.
350  #[cfg_attr(not(tarpaulin), inline(always))]
351  #[must_use]
352  pub fn with_color(mut self, v: ColorInfo) -> Self {
353    self.color = v;
354    self
355  }
356
357  /// Sets the PTS in place.
358  #[cfg_attr(not(tarpaulin), inline(always))]
359  pub const fn set_pts(&mut self, v: Option<Timestamp>) -> &mut Self {
360    self.pts = v;
361    self
362  }
363  /// Sets the duration in place.
364  #[cfg_attr(not(tarpaulin), inline(always))]
365  pub const fn set_duration(&mut self, v: Option<Timestamp>) -> &mut Self {
366    self.duration = v;
367    self
368  }
369  /// Sets the visible rect in place.
370  #[cfg_attr(not(tarpaulin), inline(always))]
371  pub const fn set_visible_rect(&mut self, v: Option<Rect>) -> &mut Self {
372    self.visible_rect = v;
373    self
374  }
375  /// Sets the color metadata in place.
376  ///
377  /// Not `const`, for the same reason as [`Self::with_color`].
378  #[cfg_attr(not(tarpaulin), inline(always))]
379  pub fn set_color(&mut self, v: ColorInfo) -> &mut Self {
380    self.color = v;
381    self
382  }
383}
384
385/// A decoded audio frame.
386///
387/// Generic parameters:
388/// - `S` — sample-format identifier.
389/// - `C` — channel layout (e.g.
390///   `mediaframe::audio::ChannelLayoutDescription`).
391/// - `E` — backend-specific frame extras.
392/// - `D` — plane data buffer type (`D: AsRef<[u8]>` at the use site).
393///
394/// `nb_samples` is **per channel**. `plane_count` is `1` for packed
395/// (interleaved) formats and `channel_count` for planar; the
396/// `[Plane; 8]` cap mirrors FFmpeg's `AV_NUM_DATA_POINTERS`. Channel
397/// counts above 8 surface their extra channels through `E` (rare in
398/// practice).
399#[derive(Clone)]
400pub struct AudioFrame<S, C, E, D> {
401  pts: Option<Timestamp>,
402  duration: Option<Timestamp>,
403  sample_rate: u32,
404  nb_samples: u32,
405  channel_count: u8,
406  sample_format: S,
407  channel_layout: C,
408  plane_count: u8,
409  planes: [Plane<D>; 8],
410  extra: E,
411}
412
413impl<S, C, E, D> AudioFrame<S, C, E, D> {
414  /// Constructs an `AudioFrame`.
415  ///
416  /// # Panics
417  ///
418  /// Panics if `plane_count > 8`. The fixed-size `planes` array
419  /// has eight slots; passing a larger `plane_count` would
420  /// later trip the slice indexing inside [`Self::planes`] far
421  /// from the construction site.
422  ///
423  /// Prefer [`Self::try_new`] when `plane_count` can't be
424  /// statically proven `<= 8`.
425  #[allow(clippy::too_many_arguments)]
426  #[cfg_attr(not(tarpaulin), inline(always))]
427  pub const fn new(
428    sample_rate: u32,
429    nb_samples: u32,
430    channel_count: u8,
431    sample_format: S,
432    channel_layout: C,
433    planes: [Plane<D>; 8],
434    plane_count: u8,
435    extra: E,
436  ) -> Self {
437    assert!(
438      plane_count as usize <= 8,
439      "AudioFrame::new: plane_count exceeds the fixed 8-plane array",
440    );
441    Self {
442      pts: None,
443      duration: None,
444      sample_rate,
445      nb_samples,
446      channel_count,
447      sample_format,
448      channel_layout,
449      plane_count,
450      planes,
451      extra,
452    }
453  }
454
455  /// Fallible counterpart to [`Self::new`]. Returns
456  /// [`FrameError::TooManyAudioPlanes`] when `plane_count > 8`
457  /// (the fixed plane array's capacity) rather than panicking.
458  ///
459  /// Not `const fn` — see the rationale on
460  /// [`VideoFrame::try_new`].
461  #[allow(clippy::too_many_arguments)]
462  #[cfg_attr(not(tarpaulin), inline(always))]
463  pub fn try_new(
464    sample_rate: u32,
465    nb_samples: u32,
466    channel_count: u8,
467    sample_format: S,
468    channel_layout: C,
469    planes: [Plane<D>; 8],
470    plane_count: u8,
471    extra: E,
472  ) -> Result<Self, FrameError> {
473    if plane_count as usize > 8 {
474      return Err(FrameError::TooManyAudioPlanes(TooManyAudioPlanes::new(
475        plane_count,
476      )));
477    }
478    Ok(Self {
479      pts: None,
480      duration: None,
481      sample_rate,
482      nb_samples,
483      channel_count,
484      sample_format,
485      channel_layout,
486      plane_count,
487      planes,
488      extra,
489    })
490  }
491
492  /// Returns the presentation timestamp.
493  #[cfg_attr(not(tarpaulin), inline(always))]
494  pub const fn pts(&self) -> Option<Timestamp> {
495    self.pts
496  }
497  /// Returns the duration.
498  #[cfg_attr(not(tarpaulin), inline(always))]
499  pub const fn duration(&self) -> Option<Timestamp> {
500    self.duration
501  }
502  /// Returns the sample rate (Hz).
503  #[cfg_attr(not(tarpaulin), inline(always))]
504  pub const fn sample_rate(&self) -> u32 {
505    self.sample_rate
506  }
507  /// Returns the per-channel sample count.
508  #[cfg_attr(not(tarpaulin), inline(always))]
509  pub const fn nb_samples(&self) -> u32 {
510    self.nb_samples
511  }
512  /// Returns the channel count.
513  #[cfg_attr(not(tarpaulin), inline(always))]
514  pub const fn channel_count(&self) -> u8 {
515    self.channel_count
516  }
517  /// Returns a reference to the sample format identifier.
518  #[cfg_attr(not(tarpaulin), inline(always))]
519  pub const fn sample_format(&self) -> &S {
520    &self.sample_format
521  }
522  /// Returns the channel layout identifier.
523  #[cfg_attr(not(tarpaulin), inline(always))]
524  pub const fn channel_layout(&self) -> &C {
525    &self.channel_layout
526  }
527  /// Returns the populated plane count.
528  #[cfg_attr(not(tarpaulin), inline(always))]
529  pub const fn plane_count(&self) -> u8 {
530    self.plane_count
531  }
532  /// Returns the populated planes as a slice.
533  #[cfg_attr(not(tarpaulin), inline(always))]
534  pub fn planes(&self) -> &[Plane<D>] {
535    &self.planes[..self.plane_count as usize]
536  }
537  /// Returns the backend extras.
538  #[cfg_attr(not(tarpaulin), inline(always))]
539  pub const fn extra(&self) -> &E {
540    &self.extra
541  }
542  /// Returns a mutable reference to the backend extras.
543  #[cfg_attr(not(tarpaulin), inline(always))]
544  pub const fn extra_mut(&mut self) -> &mut E {
545    &mut self.extra
546  }
547
548  /// Sets the PTS (consuming builder).
549  #[cfg_attr(not(tarpaulin), inline(always))]
550  #[must_use]
551  pub const fn with_pts(mut self, v: Option<Timestamp>) -> Self {
552    self.pts = v;
553    self
554  }
555  /// Sets the duration (consuming builder).
556  #[cfg_attr(not(tarpaulin), inline(always))]
557  #[must_use]
558  pub const fn with_duration(mut self, v: Option<Timestamp>) -> Self {
559    self.duration = v;
560    self
561  }
562
563  /// Sets the PTS in place.
564  #[cfg_attr(not(tarpaulin), inline(always))]
565  pub const fn set_pts(&mut self, v: Option<Timestamp>) -> &mut Self {
566    self.pts = v;
567    self
568  }
569  /// Sets the duration in place.
570  #[cfg_attr(not(tarpaulin), inline(always))]
571  pub const fn set_duration(&mut self, v: Option<Timestamp>) -> &mut Self {
572    self.duration = v;
573    self
574  }
575}
576
577/// A decoded subtitle frame.
578///
579/// Generic parameters:
580/// - `E` — backend-specific frame extras.
581/// - `D` — payload data buffer type (`D: AsRef<[u8]>` at the use site).
582///
583/// `Clone` derives, on the same reasoning as [`VideoFrame`]'s.
584#[derive(Clone)]
585pub struct SubtitleFrame<E, D> {
586  pts: Option<Timestamp>,
587  duration: Option<Timestamp>,
588  payload: SubtitlePayload<D>,
589  extra: E,
590}
591
592impl<E, D> SubtitleFrame<E, D> {
593  /// Constructs a `SubtitleFrame`.
594  #[cfg_attr(not(tarpaulin), inline(always))]
595  pub const fn new(payload: SubtitlePayload<D>, extra: E) -> Self {
596    Self {
597      pts: None,
598      duration: None,
599      payload,
600      extra,
601    }
602  }
603
604  /// Returns the PTS.
605  #[cfg_attr(not(tarpaulin), inline(always))]
606  pub const fn pts(&self) -> Option<Timestamp> {
607    self.pts
608  }
609  /// Returns the duration.
610  #[cfg_attr(not(tarpaulin), inline(always))]
611  pub const fn duration(&self) -> Option<Timestamp> {
612    self.duration
613  }
614  /// Returns the payload.
615  #[cfg_attr(not(tarpaulin), inline(always))]
616  pub const fn payload(&self) -> &SubtitlePayload<D> {
617    &self.payload
618  }
619  /// Returns the backend extras.
620  #[cfg_attr(not(tarpaulin), inline(always))]
621  pub const fn extra(&self) -> &E {
622    &self.extra
623  }
624  /// Returns a mutable reference to the backend extras.
625  #[cfg_attr(not(tarpaulin), inline(always))]
626  pub const fn extra_mut(&mut self) -> &mut E {
627    &mut self.extra
628  }
629
630  /// Sets the PTS (consuming builder).
631  #[cfg_attr(not(tarpaulin), inline(always))]
632  #[must_use]
633  pub const fn with_pts(mut self, v: Option<Timestamp>) -> Self {
634    self.pts = v;
635    self
636  }
637  /// Sets the duration (consuming builder).
638  #[cfg_attr(not(tarpaulin), inline(always))]
639  #[must_use]
640  pub const fn with_duration(mut self, v: Option<Timestamp>) -> Self {
641    self.duration = v;
642    self
643  }
644
645  /// Sets the PTS in place.
646  #[cfg_attr(not(tarpaulin), inline(always))]
647  pub const fn set_pts(&mut self, v: Option<Timestamp>) -> &mut Self {
648    self.pts = v;
649    self
650  }
651  /// Sets the duration in place.
652  #[cfg_attr(not(tarpaulin), inline(always))]
653  pub const fn set_duration(&mut self, v: Option<Timestamp>) -> &mut Self {
654    self.duration = v;
655    self
656  }
657}
658
659/// A decoded still image. Not on the timeline — no pts, no duration
660/// (the same fact [`AttachmentPacket`](crate::demuxer::AttachmentPacket)
661/// states on the packet side).
662///
663/// The fourth household, and the only one with no timestamps. Cover
664/// art, an embedded thumbnail, a poster frame: one picture, present
665/// for the whole file or not at all. Giving it `pts` / `duration`
666/// seats that are always `None` would invite a caller to read them and
667/// a backend to invent them, which is why they are absent rather than
668/// empty.
669///
670/// Generic parameters — the same three [`VideoFrame`] carries:
671/// - `P` — pixel-format identifier.
672/// - `E` — backend-specific frame extras (EXIF, ICC, side data).
673/// - `D` — plane data buffer type (`D: AsRef<[u8]>` at the use site).
674///
675/// `dimensions` is the **coded** size and `visible_rect` (when
676/// present) the displayable subregion — the distinction is not
677/// academic for a still: a JPEG's coded dimensions are rounded up to
678/// its MCU grid (8 or 16 pixels), so a 30×30 photograph is coded 32×32
679/// and only `visible_rect` says which of those pixels are the picture.
680///
681/// `plane_count` is the number of populated entries in `planes`, and
682/// the four slots are the video household's cap for the same reasons:
683/// packed RGB = 1, MJPEG's YUV = 3, either of those plus alpha = 4.
684#[derive(Clone)]
685pub struct ImageFrame<P, E, D> {
686  dimensions: Dimensions,
687  visible_rect: Option<Rect>,
688  pixel_format: P,
689  plane_count: u8,
690  planes: [Plane<D>; 4],
691  color: ColorInfo,
692  extra: E,
693}
694
695impl<P, E, D> ImageFrame<P, E, D> {
696  /// Constructs an `ImageFrame`. `visible_rect` defaults to `None`,
697  /// color to `ColorInfo::UNSPECIFIED`.
698  ///
699  /// `dimensions` is the coded width/height pair (see
700  /// [`Dimensions`] and [`Self::dimensions`] for the visible-vs-coded
701  /// distinction).
702  ///
703  /// # Panics
704  ///
705  /// Panics if `plane_count > 4`. The fixed-size `planes` array has
706  /// four slots; passing a larger `plane_count` would later trip the
707  /// slice indexing inside [`Self::planes`] far from the construction
708  /// site. Asserting here fails fast with a clear message instead.
709  /// Prefer [`Self::try_new`] when `plane_count` can't be statically
710  /// proven `<= 4`.
711  #[cfg_attr(not(tarpaulin), inline(always))]
712  pub const fn new(
713    dimensions: Dimensions,
714    pixel_format: P,
715    planes: [Plane<D>; 4],
716    plane_count: u8,
717    extra: E,
718  ) -> Self {
719    assert!(
720      plane_count as usize <= 4,
721      "ImageFrame::new: plane_count exceeds the fixed 4-plane array",
722    );
723    Self {
724      dimensions,
725      visible_rect: None,
726      pixel_format,
727      plane_count,
728      planes,
729      color: ColorInfo::UNSPECIFIED,
730      extra,
731    }
732  }
733
734  /// Fallible counterpart to [`Self::new`]. Returns
735  /// [`FrameError::TooManyImagePlanes`] when `plane_count > 4` (the
736  /// fixed plane array's capacity) rather than panicking.
737  ///
738  /// Not `const fn` — see the rationale on [`VideoFrame::try_new`].
739  #[cfg_attr(not(tarpaulin), inline(always))]
740  pub fn try_new(
741    dimensions: Dimensions,
742    pixel_format: P,
743    planes: [Plane<D>; 4],
744    plane_count: u8,
745    extra: E,
746  ) -> Result<Self, FrameError> {
747    if plane_count as usize > 4 {
748      return Err(FrameError::TooManyImagePlanes(TooManyImagePlanes::new(
749        plane_count,
750      )));
751    }
752    Ok(Self {
753      dimensions,
754      visible_rect: None,
755      pixel_format,
756      plane_count,
757      planes,
758      color: ColorInfo::UNSPECIFIED,
759      extra,
760    })
761  }
762
763  /// Returns the coded dimensions.
764  #[cfg_attr(not(tarpaulin), inline(always))]
765  pub const fn dimensions(&self) -> Dimensions {
766    self.dimensions
767  }
768  /// Returns the coded width.
769  #[cfg_attr(not(tarpaulin), inline(always))]
770  pub const fn width(&self) -> u32 {
771    self.dimensions.width()
772  }
773  /// Returns the coded height.
774  #[cfg_attr(not(tarpaulin), inline(always))]
775  pub const fn height(&self) -> u32 {
776    self.dimensions.height()
777  }
778  /// Returns the visible / true-picture rectangle, if any.
779  #[cfg_attr(not(tarpaulin), inline(always))]
780  pub const fn visible_rect(&self) -> Option<Rect> {
781    self.visible_rect
782  }
783  /// Returns a reference to the pixel format identifier.
784  #[cfg_attr(not(tarpaulin), inline(always))]
785  pub const fn pixel_format(&self) -> &P {
786    &self.pixel_format
787  }
788  /// Returns the populated plane count.
789  #[cfg_attr(not(tarpaulin), inline(always))]
790  pub const fn plane_count(&self) -> u8 {
791    self.plane_count
792  }
793  /// Returns the populated planes as a slice.
794  #[cfg_attr(not(tarpaulin), inline(always))]
795  pub fn planes(&self) -> &[Plane<D>] {
796    &self.planes[..self.plane_count as usize]
797  }
798  /// Returns one plane by index, or `None` if out of range.
799  #[cfg_attr(not(tarpaulin), inline(always))]
800  pub fn plane(&self, i: usize) -> Option<&Plane<D>> {
801    if i < self.plane_count as usize {
802      self.planes.get(i)
803    } else {
804      None
805    }
806  }
807  /// Returns the color metadata.
808  ///
809  /// Not `const`, for the same reason as [`VideoFrame::color`].
810  #[cfg_attr(not(tarpaulin), inline(always))]
811  pub fn color(&self) -> ColorInfo {
812    self.color.clone()
813  }
814  /// Returns the backend extras.
815  #[cfg_attr(not(tarpaulin), inline(always))]
816  pub const fn extra(&self) -> &E {
817    &self.extra
818  }
819  /// Returns a mutable reference to the backend extras.
820  #[cfg_attr(not(tarpaulin), inline(always))]
821  pub const fn extra_mut(&mut self) -> &mut E {
822    &mut self.extra
823  }
824
825  /// Sets the visible rect (consuming builder).
826  #[cfg_attr(not(tarpaulin), inline(always))]
827  #[must_use]
828  pub const fn with_visible_rect(mut self, v: Option<Rect>) -> Self {
829    self.visible_rect = v;
830    self
831  }
832  /// Sets the color metadata (consuming builder).
833  ///
834  /// Not `const`, for the same reason as [`VideoFrame::with_color`].
835  #[cfg_attr(not(tarpaulin), inline(always))]
836  #[must_use]
837  pub fn with_color(mut self, v: ColorInfo) -> Self {
838    self.color = v;
839    self
840  }
841
842  /// Sets the visible rect in place.
843  #[cfg_attr(not(tarpaulin), inline(always))]
844  pub const fn set_visible_rect(&mut self, v: Option<Rect>) -> &mut Self {
845    self.visible_rect = v;
846    self
847  }
848  /// Sets the color metadata in place.
849  ///
850  /// Not `const`, for the same reason as [`VideoFrame::with_color`].
851  #[cfg_attr(not(tarpaulin), inline(always))]
852  pub fn set_color(&mut self, v: ColorInfo) -> &mut Self {
853    self.color = v;
854    self
855  }
856}
857
858#[cfg(test)]
859mod tests {
860  use super::*;
861
862  use crate::{
863    color::{ColorInfo, ColorMatrix},
864    subtitle::SubtitlePayload,
865  };
866
867  fn empty_planes() -> [Plane<&'static [u8]>; 4] {
868    [
869      Plane::new(&[][..], 0),
870      Plane::new(&[][..], 0),
871      Plane::new(&[][..], 0),
872      Plane::new(&[][..], 0),
873    ]
874  }
875
876  #[test]
877  fn rect_construct_and_access() {
878    let r = Rect::new(10, 20, 1920, 1080);
879    assert_eq!(r.x(), 10);
880    assert_eq!(r.y(), 20);
881    assert_eq!(r.width(), 1920);
882    assert_eq!(r.height(), 1080);
883  }
884
885  #[test]
886  fn rect_default_is_zero() {
887    let r = Rect::default();
888    assert_eq!((r.x(), r.y(), r.width(), r.height()), (0, 0, 0, 0));
889  }
890
891  #[test]
892  fn rect_builders_chain() {
893    let r = Rect::default()
894      .with_x(1)
895      .with_y(2)
896      .with_width(3)
897      .with_height(4);
898    assert_eq!((r.x(), r.y(), r.width(), r.height()), (1, 2, 3, 4));
899  }
900
901  #[test]
902  fn rect_setters_chain() {
903    let mut r = Rect::default();
904    r.set_x(5).set_y(6).set_width(7).set_height(8);
905    assert_eq!((r.x(), r.y(), r.width(), r.height()), (5, 6, 7, 8));
906  }
907
908  #[test]
909  fn rect_const_construction() {
910    const R: Rect = Rect::new(0, 0, 1920, 1080);
911    assert_eq!(R.width(), 1920);
912  }
913
914  #[test]
915  fn plane_construct_and_access_borrowed() {
916    let buf: [u8; 4] = [1, 2, 3, 4];
917    let p: Plane<&[u8]> = Plane::new(&buf, 4);
918    assert_eq!(p.stride(), 4);
919    assert_eq!(p.data_ref(), &&buf[..]);
920  }
921
922  #[test]
923  fn plane_with_and_set_stride() {
924    let buf: [u8; 0] = [];
925    let p = Plane::new(&buf[..], 16).with_stride(32);
926    assert_eq!(p.stride(), 32);
927    let mut p2 = p;
928    p2.set_stride(64);
929    assert_eq!(p2.stride(), 64);
930  }
931
932  #[test]
933  fn plane_into_data() {
934    let buf: [u8; 4] = [1, 2, 3, 4];
935    let p: Plane<&[u8]> = Plane::new(&buf, 4);
936    let recovered = p.into_data();
937    assert_eq!(recovered, &buf[..]);
938  }
939
940  #[test]
941  fn video_frame_construct_and_access() {
942    // VideoFrame<P, E, D>: P=u32 (PixelFormat), E=VLoop (adapter ZST),
943    // D=&[u8] (plane buffer).
944    let f: VideoFrame<u32, (), &[u8]> = VideoFrame::new(
945      Dimensions::new(1920, 1080),
946      /*pix_fmt=*/ 0u32,
947      empty_planes(),
948      1,
949      (),
950    );
951    assert_eq!(f.width(), 1920);
952    assert_eq!(f.height(), 1080);
953    assert_eq!(f.dimensions(), Dimensions::new(1920, 1080));
954    assert_eq!(f.plane_count(), 1);
955    // mediaframe rename: `Matrix::default()` is now `Unspecified` (was `Bt709`).
956    assert_eq!(f.color().matrix(), crate::color::ColorMatrix::Unspecified);
957    assert_eq!(f.planes().len(), 1);
958  }
959
960  #[test]
961  fn video_frame_plane_index_clamped() {
962    let f: VideoFrame<u32, (), &[u8]> =
963      VideoFrame::new(Dimensions::new(64, 64), 0u32, empty_planes(), 2, ());
964    assert!(f.plane(0).is_some());
965    assert!(f.plane(1).is_some());
966    assert!(f.plane(2).is_none());
967    assert!(f.plane(3).is_none());
968  }
969
970  #[test]
971  fn video_frame_builders_chain() {
972    let ci = ColorInfo::UNSPECIFIED.with_matrix(ColorMatrix::Bt2020Ncl);
973    let f: VideoFrame<u32, (), &[u8]> =
974      VideoFrame::new(Dimensions::new(64, 64), 0u32, empty_planes(), 1, ())
975        .with_color(ci)
976        .with_visible_rect(Some(Rect::new(0, 0, 64, 64)));
977    assert!(f.color().matrix().is_bt_2020_ncl());
978    assert!(f.visible_rect().is_some());
979  }
980
981  fn audio_planes() -> [Plane<&'static [u8]>; 8] {
982    [
983      Plane::new(&[][..], 0),
984      Plane::new(&[][..], 0),
985      Plane::new(&[][..], 0),
986      Plane::new(&[][..], 0),
987      Plane::new(&[][..], 0),
988      Plane::new(&[][..], 0),
989      Plane::new(&[][..], 0),
990      Plane::new(&[][..], 0),
991    ]
992  }
993
994  #[test]
995  #[should_panic(expected = "plane_count exceeds the fixed 4-plane array")]
996  fn video_frame_rejects_plane_count_above_array_size() {
997    let _f: VideoFrame<u32, (), &[u8]> =
998      VideoFrame::new(Dimensions::new(64, 64), 0u32, empty_planes(), 5, ());
999  }
1000
1001  #[test]
1002  fn video_frame_try_new_returns_err_for_too_many_planes() {
1003    let res: Result<VideoFrame<u32, (), &[u8]>, FrameError> =
1004      VideoFrame::try_new(Dimensions::new(64, 64), 0u32, empty_planes(), 5, ());
1005    assert!(matches!(
1006      res,
1007      Err(FrameError::TooManyVideoPlanes(p)) if p.plane_count() == 5,
1008    ));
1009  }
1010
1011  #[test]
1012  fn video_frame_try_new_accepts_valid_plane_count() {
1013    let f: VideoFrame<u32, (), &[u8]> =
1014      VideoFrame::try_new(Dimensions::new(64, 64), 0u32, empty_planes(), 2, ())
1015        .expect("plane_count = 2 is within the 4-slot capacity");
1016    assert_eq!(f.plane_count(), 2);
1017  }
1018
1019  #[test]
1020  #[should_panic(expected = "plane_count exceeds the fixed 8-plane array")]
1021  fn audio_frame_rejects_plane_count_above_array_size() {
1022    let _f: AudioFrame<u32, u32, (), &[u8]> =
1023      AudioFrame::new(48_000, 1024, 2, 0u32, 0u32, audio_planes(), 9, ());
1024  }
1025
1026  #[test]
1027  fn audio_frame_try_new_returns_err_for_too_many_planes() {
1028    let res: Result<AudioFrame<u32, u32, (), &[u8]>, FrameError> =
1029      AudioFrame::try_new(48_000, 1024, 2, 0u32, 0u32, audio_planes(), 9, ());
1030    assert!(matches!(
1031      res,
1032      Err(FrameError::TooManyAudioPlanes(p)) if p.plane_count() == 9,
1033    ));
1034  }
1035
1036  #[test]
1037  fn audio_frame_try_new_accepts_valid_plane_count() {
1038    let f: AudioFrame<u32, u32, (), &[u8]> =
1039      AudioFrame::try_new(48_000, 1024, 2, 0u32, 0u32, audio_planes(), 8, ())
1040        .expect("plane_count = 8 is the 8-slot capacity boundary");
1041    assert_eq!(f.plane_count(), 8);
1042  }
1043
1044  #[test]
1045  fn audio_frame_construct_and_access() {
1046    // AudioFrame<S, C, E, D>: S=u32 (SampleFormat), C=u32 (ChannelLayout),
1047    // E=ALoop (adapter ZST), D=&[u8].
1048    let f: AudioFrame<u32, u32, (), &[u8]> = AudioFrame::new(
1049      48_000,
1050      1024,
1051      2,
1052      /*sf=*/ 0u32,
1053      /*layout=*/ 0u32,
1054      audio_planes(),
1055      2,
1056      (),
1057    );
1058    assert_eq!(f.sample_rate(), 48_000);
1059    assert_eq!(f.nb_samples(), 1024);
1060    assert_eq!(f.channel_count(), 2);
1061    assert_eq!(f.plane_count(), 2);
1062    assert_eq!(f.planes().len(), 2);
1063  }
1064
1065  #[test]
1066  fn image_frame_construct_and_access() {
1067    // ImageFrame<P, E, D>: P=u32 (PixelFormat), E=() (adapter ZST),
1068    // D=&[u8] (plane buffer).
1069    let f: ImageFrame<u32, (), &[u8]> = ImageFrame::new(
1070      Dimensions::new(32, 32),
1071      /*pix_fmt=*/ 0u32,
1072      empty_planes(),
1073      3,
1074      (),
1075    );
1076    assert_eq!(f.width(), 32);
1077    assert_eq!(f.height(), 32);
1078    assert_eq!(f.dimensions(), Dimensions::new(32, 32));
1079    assert_eq!(f.plane_count(), 3);
1080    assert_eq!(f.planes().len(), 3);
1081    assert_eq!(f.color().matrix(), ColorMatrix::Unspecified);
1082    assert!(f.visible_rect().is_none());
1083  }
1084
1085  #[test]
1086  fn image_frame_plane_index_clamped() {
1087    let f: ImageFrame<u32, (), &[u8]> =
1088      ImageFrame::new(Dimensions::new(8, 8), 0u32, empty_planes(), 1, ());
1089    assert!(f.plane(0).is_some());
1090    assert!(f.plane(1).is_none());
1091    assert!(f.plane(3).is_none());
1092  }
1093
1094  #[test]
1095  fn image_frame_builders_and_setters_chain() {
1096    // The MCU story the type's doc tells: a 30x30 JPEG is coded 32x32
1097    // and only the visible rect says which pixels are the picture.
1098    let ci = ColorInfo::UNSPECIFIED.with_matrix(ColorMatrix::Bt601);
1099    let mut f: ImageFrame<u32, (), &[u8]> =
1100      ImageFrame::new(Dimensions::new(32, 32), 0u32, empty_planes(), 1, ())
1101        .with_visible_rect(Some(Rect::new(0, 0, 30, 30)))
1102        .with_color(ci);
1103    assert_eq!(f.visible_rect(), Some(Rect::new(0, 0, 30, 30)));
1104    assert_eq!(f.color().matrix(), ColorMatrix::Bt601);
1105    f.set_visible_rect(None)
1106      .set_color(ColorInfo::UNSPECIFIED.with_matrix(ColorMatrix::Bt709));
1107    assert!(f.visible_rect().is_none());
1108    assert_eq!(f.color().matrix(), ColorMatrix::Bt709);
1109  }
1110
1111  #[test]
1112  #[should_panic(expected = "plane_count exceeds the fixed 4-plane array")]
1113  fn image_frame_rejects_plane_count_above_array_size() {
1114    let _f: ImageFrame<u32, (), &[u8]> =
1115      ImageFrame::new(Dimensions::new(8, 8), 0u32, empty_planes(), 5, ());
1116  }
1117
1118  #[test]
1119  fn image_frame_try_new_returns_err_for_too_many_planes() {
1120    let res: Result<ImageFrame<u32, (), &[u8]>, FrameError> =
1121      ImageFrame::try_new(Dimensions::new(8, 8), 0u32, empty_planes(), 5, ());
1122    assert!(matches!(
1123      res,
1124      Err(FrameError::TooManyImagePlanes(p)) if p.plane_count() == 5,
1125    ));
1126  }
1127
1128  #[test]
1129  fn image_frame_try_new_accepts_the_capacity_boundary() {
1130    let f: ImageFrame<u32, (), &[u8]> =
1131      ImageFrame::try_new(Dimensions::new(8, 8), 0u32, empty_planes(), 4, ())
1132        .expect("plane_count = 4 is the 4-slot capacity boundary");
1133    assert_eq!(f.plane_count(), 4);
1134  }
1135
1136  #[test]
1137  fn image_frame_extra_is_reachable_by_reference_and_by_mutation() {
1138    let mut f: ImageFrame<u32, u8, &[u8]> =
1139      ImageFrame::new(Dimensions::new(1, 1), 0u32, empty_planes(), 1, 7u8);
1140    assert_eq!(*f.extra(), 7);
1141    *f.extra_mut() = 9;
1142    assert_eq!(*f.extra(), 9);
1143  }
1144
1145  #[test]
1146  fn every_frame_household_clones() {
1147    // The amputation contract's consumer-side half: a frame is a
1148    // message, and a message is cloneable. What the clone *costs* is
1149    // the backend's business — see the D-seat contract on `adapter`.
1150    fn clones<T: Clone>(_: &T) {}
1151    let v: VideoFrame<u32, (), &[u8]> =
1152      VideoFrame::new(Dimensions::new(2, 2), 0u32, empty_planes(), 1, ());
1153    let a: AudioFrame<u32, u32, (), &[u8]> =
1154      AudioFrame::new(48_000, 1, 1, 0u32, 0u32, audio_planes(), 1, ());
1155    let s: SubtitleFrame<(), &[u8]> = SubtitleFrame::new(
1156      SubtitlePayload::Text(crate::subtitle::Text::new(&[][..], None)),
1157      (),
1158    );
1159    let i: ImageFrame<u32, (), &[u8]> =
1160      ImageFrame::new(Dimensions::new(2, 2), 0u32, empty_planes(), 1, ());
1161    clones(&v.clone());
1162    clones(&a.clone());
1163    clones(&s.clone());
1164    clones(&i.clone());
1165    assert_eq!(v.clone().plane_count(), v.plane_count());
1166    assert_eq!(i.clone().dimensions(), i.dimensions());
1167  }
1168
1169  #[test]
1170  fn subtitle_frame_text_payload() {
1171    let payload: SubtitlePayload<&[u8]> =
1172      SubtitlePayload::Text(crate::subtitle::Text::new(b"hi", None));
1173    // SubtitleFrame<E, D>: E=SLoop, D=&[u8].
1174    let f: SubtitleFrame<(), &[u8]> = SubtitleFrame::new(payload, ());
1175    match f.payload() {
1176      SubtitlePayload::Text(p) => assert_eq!(p.text(), &&b"hi"[..]),
1177      #[cfg(any(feature = "std", feature = "alloc"))]
1178      _ => panic!("unexpected variant"),
1179    }
1180  }
1181}