Skip to main content

oxideav_core/
frame.rs

1//! Uncompressed audio and video frames.
2
3use crate::blob::{BlobKind, MetadataBlob, decode_blobs, encode_blobs};
4use crate::layer::LayerIdentity;
5use crate::signal::ColorSignal;
6use crate::subtitle::SubtitleCue;
7use crate::vector::VectorFrame;
8
9/// A decoded chunk of uncompressed data: either audio samples, a video
10/// picture, or (for subtitle streams) a single styled cue.
11///
12/// Marked `#[non_exhaustive]` — consumers that match on variants must
13/// include a wildcard arm. This lets the crate add new frame kinds (data
14/// tracks, hap rops, …) without breaking downstream code.
15#[derive(Clone, Debug)]
16#[non_exhaustive]
17pub enum Frame {
18    /// Uncompressed audio samples.
19    Audio(AudioFrame),
20    /// One uncompressed video picture.
21    Video(VideoFrame),
22    /// A single subtitle cue. Timing is carried inside the cue itself
23    /// (`start_us`/`end_us`) so it's independent of container time bases,
24    /// but the enclosing pipeline/muxer can still rescale via `pts` at
25    /// the packet layer.
26    Subtitle(SubtitleCue),
27    /// A resolution-independent vector-graphics frame. Produced by
28    /// vector-format decoders (`oxideav-svg`, the vector path of
29    /// `oxideav-pdf`) and consumed by vector renderers / writers.
30    /// See [`crate::vector`] for the full primitive set.
31    Vector(VectorFrame),
32}
33
34impl Frame {
35    /// Presentation timestamp of the frame in its stream's time base
36    /// (a subtitle cue reports its `start_us`); `None` if unknown.
37    pub fn pts(&self) -> Option<i64> {
38        match self {
39            Self::Audio(a) => a.pts,
40            Self::Video(v) => v.pts,
41            Self::Subtitle(s) => Some(s.start_us),
42            Self::Vector(v) => v.pts,
43        }
44    }
45}
46
47/// Uncompressed audio frame.
48///
49/// Stream-level properties (sample format, channel count, sample rate,
50/// time base) are NOT carried per-frame — read them from the stream's
51/// [`CodecParameters`](crate::CodecParameters). Frames stay lightweight
52/// because real-time playback moves thousands per second per stream.
53///
54/// This is deliberate, not an omission: a property that does not change
55/// from one frame to the next is stated once, on the stream, instead of
56/// being repeated on every frame. The decoder states what it actually
57/// emits through [`Decoder::output_params`](crate::Decoder::output_params)
58/// (sample rate, channels, sample format, layout — authoritative over
59/// the container's claim), and signals a mid-stream change (a chained
60/// stream switching sample rate) through
61/// [`Decoder::output_params_generation`](crate::Decoder::output_params_generation),
62/// one integer compared after `receive_frame`.
63/// [`OutputParamsWatch`](crate::OutputParamsWatch) folds both into the
64/// stream's parameters in one call per frame.
65///
66/// Sample layout is determined by the stream's `SampleFormat`:
67/// - Interleaved formats: `data` has one plane; samples are stored as
68///   `ch0 ch1 ... chN ch0 ch1 ... chN ...`.
69/// - Planar formats: `data` has one plane per channel.
70///
71/// Use [`SampleFormat::plane_count`](crate::SampleFormat::plane_count)
72/// with the stream's channel count to compute the expected `data.len()`.
73#[derive(Clone, Debug)]
74pub struct AudioFrame {
75    /// Number of samples *per channel* in this frame. Variable per-frame
76    /// for VBR codecs and on partial flushes.
77    pub samples: u32,
78    /// Presentation timestamp in the stream's time base; `None` if unknown.
79    pub pts: Option<i64>,
80    /// Raw sample bytes. Length matches `format.plane_count(channels)`
81    /// from the stream's `CodecParameters`.
82    pub data: Vec<Vec<u8>>,
83}
84
85/// Uncompressed video frame.
86///
87/// Stream-level properties (pixel format, width, height, time base) are
88/// NOT carried per-frame — read them from the stream's
89/// [`CodecParameters`](crate::CodecParameters). Frames stay lightweight
90/// because real-time playback moves thousands per second per stream.
91///
92/// This is deliberate, not an omission: a property that does not change
93/// 60 times a second is stated once, on the stream, instead of being
94/// repeated on every frame. The decoder states what it actually emits
95/// through [`Decoder::output_params`](crate::Decoder::output_params)
96/// (the frames' width / height, pixel format, frame rate, colour signal
97/// — authoritative over the container's claim), and signals a
98/// mid-stream change (a resolution switch at a keyframe) through
99/// [`Decoder::output_params_generation`](crate::Decoder::output_params_generation),
100/// one integer compared after `receive_frame`.
101/// [`OutputParamsWatch`](crate::OutputParamsWatch) folds both into the
102/// stream's parameters in one call per frame.
103///
104/// # Side-channels
105///
106/// `VideoFrame` (like [`VideoPlane`]) is a fully-public struct built by
107/// struct literal throughout the codec crates, so per-frame metadata
108/// cannot be added as new fields without breaking every constructor.
109/// Instead, optional metadata rides in-band as *side-channel* entries at
110/// the tail of `planes`: [`VideoPlane`] values whose shape is impossible
111/// for an image plane, which makes them unambiguous. Every record has
112/// non-empty `data` and a `stride` that no image plane can have —
113/// either `0` (an image plane's `data` is `stride × rows` long, so a
114/// zero stride forces empty data) or a value above `isize::MAX`
115/// (`stride × rows` with any non-zero row count would exceed what a
116/// `Vec` can hold). The whole `stride > isize::MAX` band is reserved
117/// for side-channel tags. Six record kinds exist, distinguished by
118/// their `stride` tag:
119///
120/// - **Palette** — `stride == 0`. Carries the color table for
121///   palette-indexed content
122///   ([`PixelFormat::Pal8`](crate::PixelFormat::Pal8)); see
123///   [`palette`](Self::palette) / [`set_palette`](Self::set_palette).
124/// - **Palette alpha** — `stride == usize::MAX - 3`. Carries one
125///   alpha byte per palette entry for tables with transparent or
126///   translucent entries (a GIF transparent index, a PNG `tRNS` chunk
127///   on a colour-type-3 image, TGA / BMP alpha palettes); entries the
128///   record does not cover are opaque. Only meaningful next to a
129///   palette record; see [`palette_alpha`](Self::palette_alpha) /
130///   [`set_palette_alpha`](Self::set_palette_alpha) and the combined
131///   [`palette_rgba`](Self::palette_rgba) /
132///   [`set_palette_rgba`](Self::set_palette_rgba).
133/// - **Per-plane significant bits** — `stride == usize::MAX`. Carries
134///   mixed per-plane bit depths (e.g. 12-bit luma with 10-bit chroma
135///   from a wavelet codec's custom signal range); see
136///   [`significant_bits`](Self::significant_bits) /
137///   [`set_significant_bits`](Self::set_significant_bits).
138/// - **Colour signal** — `stride == usize::MAX - 1`. Carries a
139///   [`ColorSignal`] (sample range + H.273 primaries / transfer /
140///   matrix) for producers whose signal is per-picture or that have no
141///   stream object to put it on; see
142///   [`color_signal`](Self::color_signal) /
143///   [`set_color_signal`](Self::set_color_signal).
144/// - **Layer identity** — `stride == usize::MAX - 2`. Carries a
145///   [`LayerIdentity`] (layer / view / access-unit) for frames of
146///   multi-layer or multi-view streams; see [`layer`](Self::layer) /
147///   [`set_layer`](Self::set_layer).
148/// - **Metadata blobs** — `stride == usize::MAX - 4`. Carries a list
149///   of [`MetadataBlob`]s (ICC profile, Exif, XMP, … — see
150///   [`crate::blob`]) for pictures whose metadata is per-picture: a
151///   multi-page TIFF's per-page Exif, HEIF burst items with their own
152///   profile. Wire form [`encode_blobs`] / [`decode_blobs`]; see
153///   [`blobs`](Self::blobs) / [`set_blobs`](Self::set_blobs). A blob
154///   that applies to the whole stream belongs on
155///   [`CodecParameters::blobs`](crate::CodecParameters::blobs) instead.
156///
157/// The records compose: a frame can carry any subset at once, in any
158/// order, within the trailing run of side-channel-shaped entries. The
159/// typed accessors find each record by its `stride` tag regardless of
160/// order, and [`image_planes`](Self::image_planes) /
161/// [`image_plane_count`](Self::image_plane_count) exclude the whole
162/// trailing run. Frames without any attached side-channel are
163/// byte-for-byte identical to what they always were.
164///
165/// Consumers that index `planes` directly (rather than through
166/// [`image_planes`](Self::image_planes)) see the records as extra
167/// trailing entries; producers attaching a record to every frame of a
168/// stream should therefore prefer the stream-level home
169/// ([`CodecParameters`](crate::CodecParameters)) for data that does not
170/// vary per picture, and attach per-frame records when the value is
171/// genuinely per-picture or no stream object exists.
172#[derive(Clone, Debug)]
173pub struct VideoFrame {
174    /// Presentation timestamp in the stream's time base; `None` if unknown.
175    pub pts: Option<i64>,
176    /// One entry per plane (e.g., 3 for Yuv420P). Each entry is `(stride, bytes)`.
177    ///
178    /// May additionally end with side-channel entries (palette, palette
179    /// alpha, per-plane significant bits, colour signal, layer identity,
180    /// metadata blobs — see the type-level docs). Code that
181    /// wants only pixel planes should iterate
182    /// [`image_planes`](Self::image_planes) instead of this field.
183    pub planes: Vec<VideoPlane>,
184}
185
186/// `stride` tag of the per-plane significant-bits side-channel record.
187/// (The palette record's tag is `0`; see the [`VideoFrame`] docs.)
188const SIGNIFICANT_BITS_STRIDE: usize = usize::MAX;
189
190/// `stride` tag of the colour-signal side-channel record.
191const COLOR_SIGNAL_STRIDE: usize = usize::MAX - 1;
192
193/// `stride` tag of the layer-identity side-channel record.
194const LAYER_IDENTITY_STRIDE: usize = usize::MAX - 2;
195
196/// `stride` tag of the palette-alpha side-channel record.
197const PALETTE_ALPHA_STRIDE: usize = usize::MAX - 3;
198
199/// `stride` tag of the metadata-blobs side-channel record.
200const BLOBS_STRIDE: usize = usize::MAX - 4;
201
202/// Smallest `stride` value that is impossible for an image plane with
203/// at least one row: `stride × rows` would exceed `isize::MAX`, the
204/// largest allocation a `Vec` can hold. Every non-zero side-channel
205/// tag lives at or above this value.
206const SIDE_CHANNEL_STRIDE_FLOOR: usize = isize::MAX as usize + 1;
207
208impl VideoFrame {
209    /// `true` when `plane` has a side-channel record shape: non-empty
210    /// data with an impossible-for-an-image-plane stride (`0`, or any
211    /// value above `isize::MAX`) as described in the type-level docs.
212    fn is_side_channel_entry(plane: &VideoPlane) -> bool {
213        (plane.stride == 0 || plane.stride >= SIDE_CHANNEL_STRIDE_FLOOR) && !plane.data.is_empty()
214    }
215
216    /// Index of the first entry of the trailing side-channel run — equal
217    /// to the number of image planes. Scans backwards from the tail
218    /// while entries have a side-channel shape.
219    fn side_channel_run_start(&self) -> usize {
220        let mut start = self.planes.len();
221        while start > 0 && Self::is_side_channel_entry(&self.planes[start - 1]) {
222            start -= 1;
223        }
224        start
225    }
226
227    /// Index in `planes` of the side-channel record tagged with
228    /// `stride_tag`, searching the trailing side-channel run only (the
229    /// last match wins if a malformed frame carries duplicates).
230    fn side_channel_index(&self, stride_tag: usize) -> Option<usize> {
231        let start = self.side_channel_run_start();
232        self.planes[start..]
233            .iter()
234            .rposition(|p| p.stride == stride_tag)
235            .map(|i| start + i)
236    }
237
238    /// Remove every record tagged `stride_tag` from the trailing
239    /// side-channel run, returning the data of the record the readers
240    /// would have reported (the last match — consistent with
241    /// [`side_channel_index`](Self::side_channel_index)).
242    fn remove_side_channel(&mut self, stride_tag: usize) -> Option<Vec<u8>> {
243        let reported = self
244            .side_channel_index(stride_tag)
245            .map(|i| self.planes.remove(i).data);
246        while let Some(i) = self.side_channel_index(stride_tag) {
247            self.planes.remove(i);
248        }
249        reported
250    }
251
252    /// The frame's attached palette, if any.
253    ///
254    /// Returns the raw bytes of the palette side-channel (see the
255    /// type-level docs): packed 3-byte RGB entries, entry `i` at bytes
256    /// `3*i .. 3*i + 3` in R, G, B order. A full
257    /// [`Pal8`](crate::PixelFormat::Pal8) table is 256 entries
258    /// (768 bytes), but producers may attach fewer when the source
259    /// image declares a shorter table; indices at or beyond
260    /// `len / 3` are undefined by this frame and up to the consumer's
261    /// missing-entry policy (typically black).
262    pub fn palette(&self) -> Option<&[u8]> {
263        self.side_channel_index(0)
264            .map(|i| self.planes[i].data.as_slice())
265    }
266
267    /// The RGB triplet for palette entry `index`, or `None` when no
268    /// palette is attached or the attached table is too short to cover
269    /// `index`. Sugar over [`palette`](Self::palette) for per-pixel
270    /// lookups.
271    pub fn palette_rgb(&self, index: u8) -> Option<[u8; 3]> {
272        let pal = self.palette()?;
273        let at = usize::from(index) * 3;
274        let entry = pal.get(at..at + 3)?;
275        Some([entry[0], entry[1], entry[2]])
276    }
277
278    /// Attach (or replace) the frame's palette side-channel.
279    ///
280    /// `rgb` is packed 3-byte RGB entries — see
281    /// [`palette`](Self::palette) for the exact layout; pass a length
282    /// that is a multiple of 3 (up to 768 bytes for a full 256-entry
283    /// [`Pal8`](crate::PixelFormat::Pal8) table). The bytes are stored
284    /// verbatim. An empty `rgb` removes any attached palette instead
285    /// (the sentinel requires non-empty data), leaving the frame
286    /// exactly as it was before any palette was attached.
287    pub fn set_palette(&mut self, rgb: Vec<u8>) {
288        self.remove_side_channel(0);
289        if !rgb.is_empty() {
290            self.planes.push(VideoPlane {
291                stride: 0,
292                data: rgb,
293            });
294        }
295    }
296
297    /// Builder-style counterpart to [`set_palette`](Self::set_palette)
298    /// for construction chains:
299    /// `VideoFrame { pts, planes }.with_palette(rgb)`.
300    pub fn with_palette(mut self, rgb: Vec<u8>) -> Self {
301        self.set_palette(rgb);
302        self
303    }
304
305    /// Detach and return the frame's palette side-channel, if any.
306    /// Afterwards the frame carries no palette (any other side-channel
307    /// record is left in place — including a palette-alpha record,
308    /// which then reads as `None` until a palette is attached again).
309    pub fn take_palette(&mut self) -> Option<Vec<u8>> {
310        self.remove_side_channel(0)
311    }
312
313    /// Number of entries the attached palette covers (`len / 3`), or
314    /// `0` without a palette.
315    fn palette_entry_count(&self) -> usize {
316        self.palette().map_or(0, |p| p.len() / 3)
317    }
318
319    /// The frame's attached palette-alpha record, if any.
320    ///
321    /// Returns the raw bytes of the palette-alpha side-channel (see the
322    /// type-level docs): byte `i` is the alpha of palette entry `i`
323    /// (`0` transparent, `255` opaque), in the same entry order as
324    /// [`palette`](Self::palette). The record may be shorter than the
325    /// palette — a GIF with one transparent index needs only
326    /// `index + 1` bytes — and every entry it does not cover is opaque.
327    ///
328    /// The record is only meaningful next to a palette: it reads as
329    /// `None` when no palette is attached, and a malformed record
330    /// **longer** than the palette's entry count also reads as `None`
331    /// (consumers fall back to opaque). Validation happens at read
332    /// time, so palette and alpha may be attached in either order.
333    pub fn palette_alpha(&self) -> Option<&[u8]> {
334        let i = self.side_channel_index(PALETTE_ALPHA_STRIDE)?;
335        let alpha = self.planes[i].data.as_slice();
336        let entries = self.palette_entry_count();
337        (entries > 0 && alpha.len() <= entries).then_some(alpha)
338    }
339
340    /// The RGBA quadruplet for palette entry `index`: the RGB triplet
341    /// from the palette record and the alpha from the palette-alpha
342    /// record, or `255` (opaque) when no alpha record is attached, it
343    /// does not cover `index`, or it is malformed. `None` when no
344    /// palette is attached or the palette is too short to cover
345    /// `index` — exactly when [`palette_rgb`](Self::palette_rgb) is
346    /// `None`. The one lookup a `Pal8` → RGBA expander needs.
347    pub fn palette_rgba(&self, index: u8) -> Option<[u8; 4]> {
348        let [r, g, b] = self.palette_rgb(index)?;
349        let a = self
350            .palette_alpha()
351            .and_then(|a| a.get(usize::from(index)).copied())
352            .unwrap_or(u8::MAX);
353        Some([r, g, b, a])
354    }
355
356    /// Attach (or replace) the frame's palette-alpha side-channel.
357    ///
358    /// `alpha` holds one byte per palette entry, in entry order — see
359    /// [`palette_alpha`](Self::palette_alpha) for the semantics (may be
360    /// shorter than the palette; uncovered entries are opaque). The
361    /// bytes are stored verbatim; the length is checked against the
362    /// palette when read, not here, so the two records may be attached
363    /// in either order. An empty `alpha` removes any attached record
364    /// instead (the sentinel requires non-empty data). Other
365    /// side-channel records — the palette included — are unaffected.
366    pub fn set_palette_alpha(&mut self, alpha: Vec<u8>) {
367        self.remove_side_channel(PALETTE_ALPHA_STRIDE);
368        if !alpha.is_empty() {
369            self.planes.push(VideoPlane {
370                stride: PALETTE_ALPHA_STRIDE,
371                data: alpha,
372            });
373        }
374    }
375
376    /// Builder-style counterpart to
377    /// [`set_palette_alpha`](Self::set_palette_alpha) for construction
378    /// chains:
379    /// `VideoFrame { pts, planes }.with_palette(rgb).with_palette_alpha(alpha)`.
380    pub fn with_palette_alpha(mut self, alpha: Vec<u8>) -> Self {
381        self.set_palette_alpha(alpha);
382        self
383    }
384
385    /// Detach and return the frame's palette-alpha side-channel, if
386    /// any — the bytes [`palette_alpha`](Self::palette_alpha) would
387    /// have reported, so a record that read as `None` (no palette, or
388    /// longer than the palette) is removed but returns `None`.
389    /// Afterwards the frame carries no palette alpha (the palette and
390    /// every other record are left in place).
391    pub fn take_palette_alpha(&mut self) -> Option<Vec<u8>> {
392        let valid = self.palette_alpha().is_some();
393        self.remove_side_channel(PALETTE_ALPHA_STRIDE)
394            .filter(|_| valid)
395    }
396
397    /// Attach (or replace) both palette records at once from RGBA
398    /// entries: entry `i` of `rgba` becomes bytes `3*i .. 3*i + 3` of
399    /// the palette record and byte `i` of the palette-alpha record.
400    /// The alpha record is written even when every entry is opaque
401    /// (stored verbatim, no trimming). An empty `rgba` removes both
402    /// records.
403    ///
404    /// ```
405    /// # use oxideav_core::{VideoFrame, VideoPlane};
406    /// let mut f = VideoFrame { pts: None, planes: vec![VideoPlane { stride: 1, data: vec![1] }] };
407    /// f.set_palette_rgba(&[[0, 0, 0, 0], [255, 255, 255, 255]]);
408    /// assert_eq!(f.palette_rgba(0), Some([0, 0, 0, 0]));
409    /// assert_eq!(f.palette_rgba(1), Some([255, 255, 255, 255]));
410    /// assert_eq!(f.palette_rgba(2), None);
411    /// assert_eq!(f.image_plane_count(), 1);
412    /// ```
413    pub fn set_palette_rgba(&mut self, rgba: &[[u8; 4]]) {
414        let rgb = rgba.iter().flat_map(|e| [e[0], e[1], e[2]]).collect();
415        let alpha = rgba.iter().map(|e| e[3]).collect();
416        self.set_palette(rgb);
417        self.set_palette_alpha(alpha);
418    }
419
420    /// Builder-style counterpart to
421    /// [`set_palette_rgba`](Self::set_palette_rgba).
422    pub fn with_palette_rgba(mut self, rgba: &[[u8; 4]]) -> Self {
423        self.set_palette_rgba(rgba);
424        self
425    }
426
427    /// The frame's attached per-plane significant-bits record, if any.
428    ///
429    /// Returns the raw bytes of the significant-bits side-channel (see
430    /// the type-level docs): byte `k` is the number of significant bits
431    /// in the samples of image plane `k`, in plane order. This lets a
432    /// producer express **mixed** per-plane depths that no single
433    /// [`PixelFormat`](crate::PixelFormat) variant can name — e.g. a
434    /// wavelet codec's custom signal range with 12-bit luma and 10-bit
435    /// chroma, stored on a `Yuv444P12Le` surface with an attached
436    /// record of `[12, 10, 10]`.
437    ///
438    /// # Semantics
439    ///
440    /// - Values are **LSB-anchored**: a plane with `b` significant bits
441    ///   keeps its sample values in the low `b` bits of each storage
442    ///   word, with the upper bits zero — the same convention as this
443    ///   crate's partial-depth formats (`Gray10Le`, `Yuv420P10Le`,
444    ///   `Gbrp12Le`, …, each documented as "uses the low N bits of a
445    ///   16-bit word"). Full-scale for `b` significant bits is
446    ///   `(1 << b) - 1`.
447    /// - Each value must satisfy `1 ≤ b ≤ 8 × storage-word-bytes` of
448    ///   the frame's pixel format (so at most 8 for byte-sized planes,
449    ///   16 for LE-16-bit-word planes). The record refines the storage
450    ///   format's *significant* depth; it never changes the storage
451    ///   word size or plane geometry.
452    /// - A record shorter than the image-plane count (or a missing
453    ///   record) leaves the uncovered planes at the pixel format's own
454    ///   documented depth. Bytes are stored verbatim; out-of-range
455    ///   values are a producer bug and consumers may clamp or reject
456    ///   them.
457    pub fn significant_bits(&self) -> Option<&[u8]> {
458        self.side_channel_index(SIGNIFICANT_BITS_STRIDE)
459            .map(|i| self.planes[i].data.as_slice())
460    }
461
462    /// The significant-bit count for image plane `plane`, or `None`
463    /// when no record is attached or the attached record is too short
464    /// to cover `plane` (fall back to the pixel format's own depth).
465    /// Sugar over [`significant_bits`](Self::significant_bits) for
466    /// per-plane lookups.
467    pub fn plane_significant_bits(&self, plane: usize) -> Option<u8> {
468        self.significant_bits()?.get(plane).copied()
469    }
470
471    /// Attach (or replace) the frame's per-plane significant-bits
472    /// side-channel.
473    ///
474    /// `bits` holds one byte per image plane, in plane order — see
475    /// [`significant_bits`](Self::significant_bits) for the exact
476    /// semantics (LSB-anchored values, `1 ≤ b ≤ storage word bits`).
477    /// The bytes are stored verbatim. An empty `bits` removes any
478    /// attached record instead (the sentinel requires non-empty data),
479    /// leaving the frame exactly as it was before any record was
480    /// attached. Any attached palette is unaffected.
481    pub fn set_significant_bits(&mut self, bits: Vec<u8>) {
482        self.remove_side_channel(SIGNIFICANT_BITS_STRIDE);
483        if !bits.is_empty() {
484            self.planes.push(VideoPlane {
485                stride: SIGNIFICANT_BITS_STRIDE,
486                data: bits,
487            });
488        }
489    }
490
491    /// Builder-style counterpart to
492    /// [`set_significant_bits`](Self::set_significant_bits) for
493    /// construction chains:
494    /// `VideoFrame { pts, planes }.with_significant_bits(bits)`.
495    pub fn with_significant_bits(mut self, bits: Vec<u8>) -> Self {
496        self.set_significant_bits(bits);
497        self
498    }
499
500    /// Detach and return the frame's per-plane significant-bits
501    /// side-channel, if any. Afterwards the frame carries no
502    /// significant-bits record (any attached palette is left in place).
503    pub fn take_significant_bits(&mut self) -> Option<Vec<u8>> {
504        self.remove_side_channel(SIGNIFICANT_BITS_STRIDE)
505    }
506
507    /// The frame's attached colour-signal description, if any.
508    ///
509    /// Decoded from the colour-signal side-channel record (see the
510    /// type-level docs; wire form in [`ColorSignal::to_bytes`]). A
511    /// per-frame description refines the stream-level
512    /// [`CodecParameters::color_signal`](crate::CodecParameters::color_signal):
513    /// consumers resolve `frame.color_signal().unwrap_or_default()
514    /// .or(params.color_signal)` and then apply their own policy to
515    /// whatever is still unspecified. A malformed (too short) record
516    /// reads as `None`.
517    pub fn color_signal(&self) -> Option<ColorSignal> {
518        self.side_channel_index(COLOR_SIGNAL_STRIDE)
519            .and_then(|i| ColorSignal::from_bytes(&self.planes[i].data))
520    }
521
522    /// Attach (or replace) the frame's colour-signal side-channel.
523    /// Other side-channel records are unaffected.
524    pub fn set_color_signal(&mut self, signal: ColorSignal) {
525        self.remove_side_channel(COLOR_SIGNAL_STRIDE);
526        self.planes.push(VideoPlane {
527            stride: COLOR_SIGNAL_STRIDE,
528            data: signal.to_bytes().to_vec(),
529        });
530    }
531
532    /// Builder-style counterpart to
533    /// [`set_color_signal`](Self::set_color_signal) for construction
534    /// chains: `VideoFrame { pts, planes }.with_color_signal(sig)`.
535    pub fn with_color_signal(mut self, signal: ColorSignal) -> Self {
536        self.set_color_signal(signal);
537        self
538    }
539
540    /// Detach and return the frame's colour-signal side-channel, if
541    /// any. Afterwards the frame carries no colour signal (other
542    /// records are left in place).
543    pub fn take_color_signal(&mut self) -> Option<ColorSignal> {
544        self.remove_side_channel(COLOR_SIGNAL_STRIDE)
545            .and_then(|d| ColorSignal::from_bytes(&d))
546    }
547
548    /// The frame's attached layer / view identity, if any.
549    ///
550    /// Decoded from the layer-identity side-channel record (see the
551    /// type-level docs; wire form in [`LayerIdentity::to_bytes`]).
552    /// Single-layer decoders attach nothing; consumers treat `None` as
553    /// "base layer, no view". A malformed (too short) record reads as
554    /// `None`.
555    pub fn layer(&self) -> Option<LayerIdentity> {
556        self.side_channel_index(LAYER_IDENTITY_STRIDE)
557            .and_then(|i| LayerIdentity::from_bytes(&self.planes[i].data))
558    }
559
560    /// The frame's layer identity, or the base layer when none is
561    /// attached. Sugar over [`layer`](Self::layer) for consumers that
562    /// handle single- and multi-layer streams alike.
563    pub fn layer_or_base(&self) -> LayerIdentity {
564        self.layer().unwrap_or_default()
565    }
566
567    /// Attach (or replace) the frame's layer-identity side-channel.
568    /// Other side-channel records are unaffected.
569    pub fn set_layer(&mut self, layer: LayerIdentity) {
570        self.remove_side_channel(LAYER_IDENTITY_STRIDE);
571        self.planes.push(VideoPlane {
572            stride: LAYER_IDENTITY_STRIDE,
573            data: layer.to_bytes().to_vec(),
574        });
575    }
576
577    /// Builder-style counterpart to [`set_layer`](Self::set_layer) for
578    /// construction chains:
579    /// `VideoFrame { pts, planes }.with_layer(LayerIdentity::new(1))`.
580    pub fn with_layer(mut self, layer: LayerIdentity) -> Self {
581        self.set_layer(layer);
582        self
583    }
584
585    /// Detach and return the frame's layer-identity side-channel, if
586    /// any. Afterwards the frame carries no layer identity (other
587    /// records are left in place).
588    pub fn take_layer(&mut self) -> Option<LayerIdentity> {
589        self.remove_side_channel(LAYER_IDENTITY_STRIDE)
590            .and_then(|d| LayerIdentity::from_bytes(&d))
591    }
592
593    /// The frame's attached metadata blobs, decoded from the blobs
594    /// side-channel record (see the type-level docs; wire form in
595    /// [`encode_blobs`]). Empty when no record is attached — and when
596    /// the record is malformed, so a consumer never sees half a list.
597    /// Decodes on every call: read it once per frame.
598    ///
599    /// A per-frame blob refines the stream-level
600    /// [`CodecParameters::blobs`](crate::CodecParameters::blobs) of
601    /// the same kind: resolve
602    /// `frame.blob(&k).or_else(|| params.blob(&k).cloned())`.
603    pub fn blobs(&self) -> Vec<MetadataBlob> {
604        self.side_channel_index(BLOBS_STRIDE)
605            .and_then(|i| decode_blobs(&self.planes[i].data))
606            .unwrap_or_default()
607    }
608
609    /// The first attached blob of `kind`, if any (see
610    /// [`blobs`](Self::blobs)).
611    pub fn blob(&self, kind: &BlobKind) -> Option<MetadataBlob> {
612        self.blobs().into_iter().find(|b| b.is(kind))
613    }
614
615    /// Attach (or replace) the frame's metadata-blobs side-channel
616    /// with `blobs`, in the given order. An empty list removes any
617    /// attached record instead (the sentinel requires non-empty data).
618    /// Other side-channel records are unaffected.
619    pub fn set_blobs(&mut self, blobs: Vec<MetadataBlob>) {
620        self.remove_side_channel(BLOBS_STRIDE);
621        if !blobs.is_empty() {
622            self.planes.push(VideoPlane {
623                stride: BLOBS_STRIDE,
624                data: encode_blobs(&blobs),
625            });
626        }
627    }
628
629    /// Append one blob to the frame's metadata-blobs record (creating
630    /// it when absent; a malformed existing record is replaced by the
631    /// single new blob). Blobs of the same kind are kept in insertion
632    /// order; nothing is replaced.
633    pub fn push_blob(&mut self, blob: MetadataBlob) {
634        let mut blobs = self.blobs();
635        blobs.push(blob);
636        self.set_blobs(blobs);
637    }
638
639    /// Builder-style counterpart to [`push_blob`](Self::push_blob) for
640    /// construction chains:
641    /// `VideoFrame { pts, planes }.with_blob(MetadataBlob::new(BlobKind::EXIF, exif))`.
642    pub fn with_blob(mut self, blob: MetadataBlob) -> Self {
643        self.push_blob(blob);
644        self
645    }
646
647    /// Builder-style counterpart to [`set_blobs`](Self::set_blobs).
648    pub fn with_blobs(mut self, blobs: Vec<MetadataBlob>) -> Self {
649        self.set_blobs(blobs);
650        self
651    }
652
653    /// Detach and return the frame's metadata blobs, if any (empty for
654    /// no record or a malformed one — which is removed all the same).
655    /// Afterwards the frame carries no blobs record (other records are
656    /// left in place).
657    pub fn take_blobs(&mut self) -> Vec<MetadataBlob> {
658        self.remove_side_channel(BLOBS_STRIDE)
659            .and_then(|d| decode_blobs(&d))
660            .unwrap_or_default()
661    }
662
663    /// The frame's image planes — `planes` with the trailing
664    /// side-channel entries (palette, palette alpha, significant bits,
665    /// colour signal, layer identity, metadata blobs) excluded.
666    /// Prefer this over indexing `planes` directly in code that
667    /// handles side-channel-capable frames.
668    pub fn image_planes(&self) -> &[VideoPlane] {
669        &self.planes[..self.image_plane_count()]
670    }
671
672    /// Number of image planes (excludes every side-channel entry).
673    /// Matches the stream pixel format's
674    /// [`plane_count`](crate::PixelFormat::plane_count) for well-formed
675    /// frames.
676    pub fn image_plane_count(&self) -> usize {
677        self.side_channel_run_start()
678    }
679}
680
681/// One plane of a [`VideoFrame`]: row-major sample bytes plus the
682/// stride between rows.
683///
684/// An entry with non-empty `data` and a `stride` of `0` or above
685/// `isize::MAX` is not an image plane: it is a side-channel record
686/// (palette, palette alpha, per-plane significant bits, colour signal,
687/// layer identity, metadata blobs) described on [`VideoFrame`] — only
688/// meaningful within the trailing
689/// run of `VideoFrame::planes`.
690#[derive(Clone, Debug)]
691pub struct VideoPlane {
692    /// Bytes per row in `data`.
693    pub stride: usize,
694    /// Raw plane bytes, `stride × rows` long (rows may carry padding
695    /// beyond the visible width).
696    pub data: Vec<u8>,
697}
698
699#[cfg(test)]
700mod tests {
701    use super::*;
702
703    fn gray_frame() -> VideoFrame {
704        // 4×2 Gray8 image plane.
705        VideoFrame {
706            pts: Some(7),
707            planes: vec![VideoPlane {
708                stride: 4,
709                data: vec![0u8; 8],
710            }],
711        }
712    }
713
714    /// A full 256-entry table where entry i is (i, !i, i^0x55).
715    fn full_palette() -> Vec<u8> {
716        (0u16..256)
717            .flat_map(|i| {
718                let i = i as u8;
719                [i, !i, i ^ 0x55]
720            })
721            .collect()
722    }
723
724    #[test]
725    fn frame_without_palette_reports_none_and_full_image_planes() {
726        let f = gray_frame();
727        assert_eq!(f.palette(), None);
728        assert_eq!(f.palette_rgb(0), None);
729        assert_eq!(f.image_plane_count(), 1);
730        assert_eq!(f.image_planes().len(), 1);
731        assert_eq!(f.image_planes()[0].stride, 4);
732    }
733
734    #[test]
735    fn set_palette_round_trips_and_keeps_image_planes_intact() {
736        let mut f = gray_frame();
737        let pal = full_palette();
738        f.set_palette(pal.clone());
739
740        assert_eq!(f.palette(), Some(pal.as_slice()));
741        // Image-plane view is unchanged by the side-channel.
742        assert_eq!(f.image_plane_count(), 1);
743        assert_eq!(f.image_planes()[0].data.len(), 8);
744        // The raw field sees the sentinel entry at the tail.
745        assert_eq!(f.planes.len(), 2);
746        assert_eq!(f.planes[1].stride, 0);
747
748        // Entry lookup: entry i is (i, !i, i ^ 0x55) by construction.
749        assert_eq!(f.palette_rgb(0), Some([0x00, 0xFF, 0x55]));
750        assert_eq!(f.palette_rgb(0xAB), Some([0xAB, 0x54, 0xFE]));
751        assert_eq!(f.palette_rgb(255), Some([0xFF, 0x00, 0xAA]));
752    }
753
754    #[test]
755    fn set_palette_replaces_existing_table() {
756        let mut f = gray_frame();
757        f.set_palette(vec![1, 2, 3]);
758        f.set_palette(vec![9, 8, 7, 6, 5, 4]);
759        // Replacement, not stacking: one image plane + one sentinel.
760        assert_eq!(f.planes.len(), 2);
761        assert_eq!(f.palette(), Some(&[9, 8, 7, 6, 5, 4][..]));
762        assert_eq!(f.palette_rgb(1), Some([6, 5, 4]));
763    }
764
765    #[test]
766    fn short_palette_covers_only_its_entries() {
767        let f = gray_frame().with_palette(vec![10, 20, 30, 40, 50, 60]);
768        assert_eq!(f.palette_rgb(0), Some([10, 20, 30]));
769        assert_eq!(f.palette_rgb(1), Some([40, 50, 60]));
770        // Beyond the table: undefined by the frame → None.
771        assert_eq!(f.palette_rgb(2), None);
772        assert_eq!(f.palette_rgb(255), None);
773    }
774
775    #[test]
776    fn empty_palette_clears_and_take_palette_detaches() {
777        let mut f = gray_frame();
778        f.set_palette(vec![1, 2, 3]);
779        assert!(f.palette().is_some());
780
781        // Empty input removes the side-channel entirely.
782        f.set_palette(Vec::new());
783        assert_eq!(f.palette(), None);
784        assert_eq!(f.planes.len(), 1);
785
786        // take_palette detaches and returns the bytes.
787        f.set_palette(vec![4, 5, 6]);
788        assert_eq!(f.take_palette(), Some(vec![4, 5, 6]));
789        assert_eq!(f.palette(), None);
790        assert_eq!(f.take_palette(), None);
791        assert_eq!(f.planes.len(), 1);
792    }
793
794    #[test]
795    fn zero_stride_empty_plane_is_not_mistaken_for_a_palette() {
796        // stride == 0 with EMPTY data is the degenerate (but
797        // contract-consistent) empty image plane, not the sentinel.
798        let f = VideoFrame {
799            pts: None,
800            planes: vec![
801                VideoPlane {
802                    stride: 4,
803                    data: vec![0u8; 8],
804                },
805                VideoPlane {
806                    stride: 0,
807                    data: Vec::new(),
808                },
809            ],
810        };
811        assert_eq!(f.palette(), None);
812        assert_eq!(f.image_plane_count(), 2);
813    }
814
815    #[test]
816    fn palette_on_frame_without_image_planes() {
817        // A palette can be attached before pixel planes exist (encoder
818        // scaffolding); the image-plane view is then empty.
819        let f = VideoFrame {
820            pts: None,
821            planes: Vec::new(),
822        }
823        .with_palette(vec![1, 2, 3]);
824        assert_eq!(f.palette(), Some(&[1, 2, 3][..]));
825        assert_eq!(f.image_plane_count(), 0);
826        assert!(f.image_planes().is_empty());
827    }
828
829    #[test]
830    fn frame_without_significant_bits_reports_none() {
831        let f = gray_frame();
832        assert_eq!(f.significant_bits(), None);
833        assert_eq!(f.plane_significant_bits(0), None);
834        assert_eq!(f.image_plane_count(), 1);
835    }
836
837    #[test]
838    fn set_significant_bits_round_trips_and_keeps_image_planes_intact() {
839        // A 12-bit-luma / 10-bit-chroma mixed-depth frame (the VC-2
840        // custom-signal-range shape that motivated the record).
841        let mut f = VideoFrame {
842            pts: Some(3),
843            planes: vec![
844                VideoPlane {
845                    stride: 8,
846                    data: vec![0u8; 16],
847                },
848                VideoPlane {
849                    stride: 8,
850                    data: vec![0u8; 16],
851                },
852                VideoPlane {
853                    stride: 8,
854                    data: vec![0u8; 16],
855                },
856            ],
857        };
858        f.set_significant_bits(vec![12, 10, 10]);
859
860        assert_eq!(f.significant_bits(), Some(&[12, 10, 10][..]));
861        assert_eq!(f.plane_significant_bits(0), Some(12));
862        assert_eq!(f.plane_significant_bits(1), Some(10));
863        assert_eq!(f.plane_significant_bits(2), Some(10));
864        // Beyond the record: fall back to the format default → None.
865        assert_eq!(f.plane_significant_bits(3), None);
866
867        // Image-plane view is unchanged by the side-channel.
868        assert_eq!(f.image_plane_count(), 3);
869        assert_eq!(f.image_planes().len(), 3);
870        // The raw field sees the sentinel entry at the tail.
871        assert_eq!(f.planes.len(), 4);
872        assert_eq!(f.planes[3].stride, usize::MAX);
873    }
874
875    #[test]
876    fn set_significant_bits_replaces_and_empty_clears_and_take_detaches() {
877        let mut f = gray_frame();
878        f.set_significant_bits(vec![7]);
879        f.set_significant_bits(vec![6]);
880        // Replacement, not stacking.
881        assert_eq!(f.planes.len(), 2);
882        assert_eq!(f.significant_bits(), Some(&[6][..]));
883
884        // Empty input removes the side-channel entirely.
885        f.set_significant_bits(Vec::new());
886        assert_eq!(f.significant_bits(), None);
887        assert_eq!(f.planes.len(), 1);
888
889        // take_significant_bits detaches and returns the bytes.
890        f.set_significant_bits(vec![5]);
891        assert_eq!(f.take_significant_bits(), Some(vec![5]));
892        assert_eq!(f.significant_bits(), None);
893        assert_eq!(f.take_significant_bits(), None);
894        assert_eq!(f.planes.len(), 1);
895    }
896
897    #[test]
898    fn palette_and_significant_bits_compose_in_either_order() {
899        // Palette first, then depths.
900        let mut f = gray_frame()
901            .with_palette(vec![1, 2, 3])
902            .with_significant_bits(vec![8]);
903        assert_eq!(f.palette(), Some(&[1, 2, 3][..]));
904        assert_eq!(f.significant_bits(), Some(&[8][..]));
905        assert_eq!(f.image_plane_count(), 1);
906        assert_eq!(f.planes.len(), 3);
907
908        // Replacing one record must not disturb the other, regardless
909        // of which currently sits at the tail.
910        f.set_palette(vec![9, 8, 7]);
911        assert_eq!(f.palette(), Some(&[9, 8, 7][..]));
912        assert_eq!(f.significant_bits(), Some(&[8][..]));
913        f.set_significant_bits(vec![7]);
914        assert_eq!(f.palette(), Some(&[9, 8, 7][..]));
915        assert_eq!(f.significant_bits(), Some(&[7][..]));
916        assert_eq!(f.image_plane_count(), 1);
917
918        // Depths first, then palette.
919        let g = gray_frame()
920            .with_significant_bits(vec![4])
921            .with_palette(full_palette());
922        assert_eq!(g.significant_bits(), Some(&[4][..]));
923        assert_eq!(g.palette_rgb(0), Some([0x00, 0xFF, 0x55]));
924        assert_eq!(g.image_plane_count(), 1);
925
926        // Detaching one leaves the other attached.
927        let mut h = g;
928        assert_eq!(h.take_significant_bits(), Some(vec![4]));
929        assert_eq!(h.significant_bits(), None);
930        assert_eq!(h.palette().map(<[u8]>::len), Some(768));
931        assert_eq!(h.take_palette().map(|p| p.len()), Some(768));
932        assert_eq!(h.planes.len(), 1);
933        assert_eq!(h.image_plane_count(), 1);
934    }
935
936    #[test]
937    fn max_stride_empty_plane_is_not_mistaken_for_significant_bits() {
938        // stride == usize::MAX with EMPTY data is not the sentinel
939        // (mirroring the palette rule: sentinels require non-empty
940        // data). Degenerate, but must not be misread as a record.
941        let f = VideoFrame {
942            pts: None,
943            planes: vec![
944                VideoPlane {
945                    stride: 4,
946                    data: vec![0u8; 8],
947                },
948                VideoPlane {
949                    stride: usize::MAX,
950                    data: Vec::new(),
951                },
952            ],
953        };
954        assert_eq!(f.significant_bits(), None);
955        assert_eq!(f.image_plane_count(), 2);
956    }
957
958    #[test]
959    fn significant_bits_on_frame_without_image_planes() {
960        // Like the palette, the record can be attached before pixel
961        // planes exist (encoder scaffolding).
962        let f = VideoFrame {
963            pts: None,
964            planes: Vec::new(),
965        }
966        .with_significant_bits(vec![12, 10, 10]);
967        assert_eq!(f.significant_bits(), Some(&[12, 10, 10][..]));
968        assert_eq!(f.image_plane_count(), 0);
969        assert!(f.image_planes().is_empty());
970    }
971
972    #[test]
973    fn side_channels_survive_clone_and_frame_wrapping() {
974        let f = gray_frame()
975            .with_palette(vec![1, 2, 3])
976            .with_significant_bits(vec![6]);
977        let cloned = f.clone();
978        assert_eq!(cloned.palette(), f.palette());
979        assert_eq!(cloned.significant_bits(), f.significant_bits());
980
981        let wrapped = Frame::Video(cloned);
982        assert_eq!(wrapped.pts(), Some(7));
983        if let Frame::Video(v) = wrapped {
984            assert_eq!(v.palette(), Some(&[1, 2, 3][..]));
985            assert_eq!(v.significant_bits(), Some(&[6][..]));
986        } else {
987            unreachable!("wrapped as Video above");
988        }
989    }
990
991    #[test]
992    fn frame_without_color_signal_or_layer_reports_none() {
993        let f = gray_frame();
994        assert_eq!(f.color_signal(), None);
995        assert_eq!(f.layer(), None);
996        assert_eq!(f.layer_or_base(), LayerIdentity::base());
997        assert_eq!(f.image_plane_count(), 1);
998        assert_eq!(f.planes.len(), 1);
999    }
1000
1001    #[test]
1002    fn set_color_signal_round_trips_and_keeps_image_planes_intact() {
1003        let mut f = gray_frame();
1004        let sig = ColorSignal::from_code_points(9, 16, 9, true);
1005        f.set_color_signal(sig);
1006        assert_eq!(f.color_signal(), Some(sig));
1007        assert_eq!(f.image_plane_count(), 1);
1008        assert_eq!(f.image_planes()[0].data.len(), 8);
1009        assert_eq!(f.planes.len(), 2);
1010        assert_eq!(f.planes[1].stride, usize::MAX - 1);
1011        assert_eq!(f.planes[1].data, sig.to_bytes());
1012
1013        // Replacement, not stacking.
1014        f.set_color_signal(ColorSignal::srgb());
1015        assert_eq!(f.planes.len(), 2);
1016        assert_eq!(f.color_signal(), Some(ColorSignal::srgb()));
1017
1018        // Detach.
1019        assert_eq!(f.take_color_signal(), Some(ColorSignal::srgb()));
1020        assert_eq!(f.color_signal(), None);
1021        assert_eq!(f.take_color_signal(), None);
1022        assert_eq!(f.planes.len(), 1);
1023    }
1024
1025    #[test]
1026    fn set_layer_round_trips_and_keeps_image_planes_intact() {
1027        let mut f = gray_frame();
1028        let id = LayerIdentity::new(1).with_view_id(1).with_access_unit(9);
1029        f.set_layer(id);
1030        assert_eq!(f.layer(), Some(id));
1031        assert_eq!(f.layer_or_base(), id);
1032        assert_eq!(f.image_plane_count(), 1);
1033        assert_eq!(f.planes.len(), 2);
1034        assert_eq!(f.planes[1].stride, usize::MAX - 2);
1035
1036        f.set_layer(LayerIdentity::new(2));
1037        assert_eq!(f.planes.len(), 2);
1038        assert_eq!(f.layer(), Some(LayerIdentity::new(2)));
1039
1040        assert_eq!(f.take_layer(), Some(LayerIdentity::new(2)));
1041        assert_eq!(f.layer(), None);
1042        assert_eq!(f.take_layer(), None);
1043        assert_eq!(f.planes.len(), 1);
1044    }
1045
1046    #[test]
1047    fn all_four_side_channels_compose_in_any_order() {
1048        let sig = ColorSignal::bt709_limited();
1049        let id = LayerIdentity::new(1).with_view_id(1);
1050        let mut f = gray_frame()
1051            .with_layer(id)
1052            .with_palette(vec![1, 2, 3])
1053            .with_color_signal(sig)
1054            .with_significant_bits(vec![8]);
1055        assert_eq!(f.planes.len(), 5);
1056        assert_eq!(f.image_plane_count(), 1);
1057        assert_eq!(f.palette(), Some(&[1, 2, 3][..]));
1058        assert_eq!(f.significant_bits(), Some(&[8][..]));
1059        assert_eq!(f.color_signal(), Some(sig));
1060        assert_eq!(f.layer(), Some(id));
1061
1062        // Replacing one record from the middle of the run leaves the
1063        // others in place.
1064        f.set_color_signal(ColorSignal::srgb());
1065        assert_eq!(f.planes.len(), 5);
1066        assert_eq!(f.palette(), Some(&[1, 2, 3][..]));
1067        assert_eq!(f.significant_bits(), Some(&[8][..]));
1068        assert_eq!(f.layer(), Some(id));
1069        assert_eq!(f.color_signal(), Some(ColorSignal::srgb()));
1070
1071        // Detaching in an arbitrary order.
1072        assert_eq!(f.take_layer(), Some(id));
1073        assert_eq!(f.take_palette(), Some(vec![1, 2, 3]));
1074        assert_eq!(f.color_signal(), Some(ColorSignal::srgb()));
1075        assert_eq!(f.significant_bits(), Some(&[8][..]));
1076        assert_eq!(f.take_color_signal(), Some(ColorSignal::srgb()));
1077        assert_eq!(f.take_significant_bits(), Some(vec![8]));
1078        assert_eq!(f.planes.len(), 1);
1079        assert_eq!(f.image_plane_count(), 1);
1080    }
1081
1082    #[test]
1083    fn huge_stride_empty_plane_is_not_a_side_channel() {
1084        // Any stride in the reserved band with EMPTY data stays an
1085        // (degenerate) image plane — sentinels require non-empty data.
1086        let f = VideoFrame {
1087            pts: None,
1088            planes: vec![
1089                VideoPlane {
1090                    stride: 4,
1091                    data: vec![0u8; 8],
1092                },
1093                VideoPlane {
1094                    stride: usize::MAX - 1,
1095                    data: Vec::new(),
1096                },
1097                VideoPlane {
1098                    stride: usize::MAX - 2,
1099                    data: Vec::new(),
1100                },
1101            ],
1102        };
1103        assert_eq!(f.color_signal(), None);
1104        assert_eq!(f.layer(), None);
1105        assert_eq!(f.image_plane_count(), 3);
1106    }
1107
1108    #[test]
1109    fn largest_real_stride_is_still_an_image_plane() {
1110        // isize::MAX is the largest stride a one-row plane can have;
1111        // it must not be classified as a side-channel tag.
1112        let f = VideoFrame {
1113            pts: None,
1114            planes: vec![VideoPlane {
1115                stride: isize::MAX as usize,
1116                data: vec![0u8; 1],
1117            }],
1118        };
1119        assert_eq!(f.image_plane_count(), 1);
1120        assert_eq!(f.color_signal(), None);
1121        assert_eq!(f.layer(), None);
1122    }
1123
1124    #[test]
1125    fn malformed_short_records_read_as_none() {
1126        let f = VideoFrame {
1127            pts: None,
1128            planes: vec![
1129                VideoPlane {
1130                    stride: 4,
1131                    data: vec![0u8; 8],
1132                },
1133                VideoPlane {
1134                    stride: usize::MAX - 1,
1135                    data: vec![1, 2],
1136                },
1137                VideoPlane {
1138                    stride: usize::MAX - 2,
1139                    data: vec![0; 5],
1140                },
1141            ],
1142        };
1143        // They are side-channel-shaped (excluded from the image planes)
1144        // but decode to nothing.
1145        assert_eq!(f.image_plane_count(), 1);
1146        assert_eq!(f.color_signal(), None);
1147        assert_eq!(f.layer(), None);
1148    }
1149
1150    #[test]
1151    fn color_signal_and_layer_survive_clone_and_frame_wrapping() {
1152        let sig = ColorSignal::from_code_points(1, 13, 0, true);
1153        let id = LayerIdentity::new(1).with_view_id(1).with_access_unit(3);
1154        let f = gray_frame().with_color_signal(sig).with_layer(id);
1155        let cloned = f.clone();
1156        assert_eq!(cloned.color_signal(), Some(sig));
1157        assert_eq!(cloned.layer(), Some(id));
1158        let wrapped = Frame::Video(cloned);
1159        assert_eq!(wrapped.pts(), Some(7));
1160        if let Frame::Video(v) = wrapped {
1161            assert_eq!(v.color_signal(), Some(sig));
1162            assert_eq!(v.layer(), Some(id));
1163        } else {
1164            unreachable!("wrapped as Video above");
1165        }
1166    }
1167
1168    #[test]
1169    fn frame_without_palette_alpha_reads_opaque() {
1170        // No alpha record: palette_alpha is None and every covered
1171        // entry is opaque through palette_rgba.
1172        let f = gray_frame().with_palette(vec![1, 2, 3, 4, 5, 6]);
1173        assert_eq!(f.palette_alpha(), None);
1174        assert_eq!(f.palette_rgba(0), Some([1, 2, 3, 255]));
1175        assert_eq!(f.palette_rgba(1), Some([4, 5, 6, 255]));
1176        // Beyond the palette: None, exactly like palette_rgb.
1177        assert_eq!(f.palette_rgba(2), None);
1178        assert_eq!(f.image_plane_count(), 1);
1179        assert_eq!(f.planes.len(), 2);
1180
1181        // No palette at all: no RGBA either.
1182        let g = gray_frame();
1183        assert_eq!(g.palette_alpha(), None);
1184        assert_eq!(g.palette_rgba(0), None);
1185    }
1186
1187    #[test]
1188    fn set_palette_alpha_round_trips_and_keeps_image_planes_intact() {
1189        // A GIF-style table: entry 2 is the transparent index, the
1190        // record covers only entries 0..=2 and the rest stay opaque.
1191        let mut f = gray_frame().with_palette(full_palette());
1192        f.set_palette_alpha(vec![255, 255, 0]);
1193
1194        assert_eq!(f.palette_alpha(), Some(&[255, 255, 0][..]));
1195        assert_eq!(f.palette_rgba(0), Some([0x00, 0xFF, 0x55, 255]));
1196        assert_eq!(f.palette_rgba(2), Some([0x02, 0xFD, 0x57, 0]));
1197        // Uncovered entries are opaque.
1198        assert_eq!(f.palette_rgba(3), Some([0x03, 0xFC, 0x56, 255]));
1199        assert_eq!(f.palette_rgba(255), Some([0xFF, 0x00, 0xAA, 255]));
1200        // Image-plane view is unchanged; the raw field sees two records.
1201        assert_eq!(f.image_plane_count(), 1);
1202        assert_eq!(f.image_planes()[0].data.len(), 8);
1203        assert_eq!(f.planes.len(), 3);
1204        assert_eq!(f.planes[2].stride, usize::MAX - 3);
1205        // The RGB record is untouched by the alpha record.
1206        assert_eq!(f.palette().map(<[u8]>::len), Some(768));
1207
1208        // Replacement, not stacking.
1209        f.set_palette_alpha(vec![128]);
1210        assert_eq!(f.planes.len(), 3);
1211        assert_eq!(f.palette_alpha(), Some(&[128][..]));
1212        assert_eq!(f.palette_rgba(0), Some([0x00, 0xFF, 0x55, 128]));
1213
1214        // Empty input removes the record; the palette stays.
1215        f.set_palette_alpha(Vec::new());
1216        assert_eq!(f.palette_alpha(), None);
1217        assert_eq!(f.planes.len(), 2);
1218        assert_eq!(f.palette().map(<[u8]>::len), Some(768));
1219
1220        // take_palette_alpha detaches and returns the bytes.
1221        f.set_palette_alpha(vec![0, 255]);
1222        assert_eq!(f.take_palette_alpha(), Some(vec![0, 255]));
1223        assert_eq!(f.palette_alpha(), None);
1224        assert_eq!(f.take_palette_alpha(), None);
1225        assert_eq!(f.planes.len(), 2);
1226    }
1227
1228    #[test]
1229    fn palette_alpha_longer_than_palette_is_malformed_and_reads_none() {
1230        // Two-entry palette, three alpha bytes: malformed → None, and
1231        // palette_rgba falls back to opaque.
1232        let mut f = gray_frame()
1233            .with_palette(vec![1, 2, 3, 4, 5, 6])
1234            .with_palette_alpha(vec![0, 0, 0]);
1235        assert_eq!(f.palette_alpha(), None);
1236        assert_eq!(f.palette_rgba(0), Some([1, 2, 3, 255]));
1237        assert_eq!(f.palette_rgba(1), Some([4, 5, 6, 255]));
1238        // The entry is still side-channel-shaped: excluded from the
1239        // image planes, present in the raw field.
1240        assert_eq!(f.image_plane_count(), 1);
1241        assert_eq!(f.planes.len(), 3);
1242        // Exactly as long as the palette is fine.
1243        f.set_palette_alpha(vec![0, 7]);
1244        assert_eq!(f.palette_alpha(), Some(&[0, 7][..]));
1245        assert_eq!(f.palette_rgba(1), Some([4, 5, 6, 7]));
1246        // Growing the palette afterwards makes a previously malformed
1247        // record valid: validation is at read time.
1248        f.set_palette_alpha(vec![0, 0, 0]);
1249        assert_eq!(f.palette_alpha(), None);
1250        f.set_palette(vec![1, 2, 3, 4, 5, 6, 7, 8, 9]);
1251        assert_eq!(f.palette_alpha(), Some(&[0, 0, 0][..]));
1252        // Taking a malformed record removes it but reports None.
1253        f.set_palette(vec![1, 2, 3]);
1254        assert_eq!(f.palette_alpha(), None);
1255        assert_eq!(f.take_palette_alpha(), None);
1256        assert_eq!(f.planes.len(), 2);
1257        assert_eq!(f.palette(), Some(&[1, 2, 3][..]));
1258    }
1259
1260    #[test]
1261    fn palette_alpha_without_palette_reads_none_in_either_order() {
1262        // Alpha attached first (encoder scaffolding): meaningless until
1263        // the palette arrives, then readable.
1264        let mut f = gray_frame().with_palette_alpha(vec![0]);
1265        assert_eq!(f.palette_alpha(), None);
1266        assert_eq!(f.palette_rgba(0), None);
1267        assert_eq!(f.image_plane_count(), 1);
1268        assert_eq!(f.planes.len(), 2);
1269        f.set_palette(vec![9, 9, 9]);
1270        assert_eq!(f.palette_alpha(), Some(&[0][..]));
1271        assert_eq!(f.palette_rgba(0), Some([9, 9, 9, 0]));
1272
1273        // Detaching the palette orphans the alpha record: None again,
1274        // still side-channel-shaped, and set_palette brings it back.
1275        assert_eq!(f.take_palette(), Some(vec![9, 9, 9]));
1276        assert_eq!(f.palette_alpha(), None);
1277        assert_eq!(f.planes.len(), 2);
1278        assert_eq!(f.image_plane_count(), 1);
1279        f.set_palette(vec![1, 1, 1]);
1280        assert_eq!(f.palette_alpha(), Some(&[0][..]));
1281    }
1282
1283    #[test]
1284    fn set_palette_rgba_writes_both_records() {
1285        let table = [[10, 20, 30, 0], [40, 50, 60, 255], [70, 80, 90, 128]];
1286        let mut f = gray_frame().with_palette_rgba(&table);
1287        assert_eq!(f.palette(), Some(&[10, 20, 30, 40, 50, 60, 70, 80, 90][..]));
1288        assert_eq!(f.palette_alpha(), Some(&[0, 255, 128][..]));
1289        for (i, e) in table.iter().enumerate() {
1290            assert_eq!(f.palette_rgba(i as u8), Some(*e));
1291        }
1292        assert_eq!(f.palette_rgba(3), None);
1293        assert_eq!(f.image_plane_count(), 1);
1294        assert_eq!(f.planes.len(), 3);
1295
1296        // Replaces an existing pair.
1297        f.set_palette_rgba(&[[1, 2, 3, 4]]);
1298        assert_eq!(f.planes.len(), 3);
1299        assert_eq!(f.palette_rgba(0), Some([1, 2, 3, 4]));
1300        assert_eq!(f.palette_rgba(1), None);
1301
1302        // Empty clears both.
1303        f.set_palette_rgba(&[]);
1304        assert_eq!(f.palette(), None);
1305        assert_eq!(f.palette_alpha(), None);
1306        assert_eq!(f.planes.len(), 1);
1307    }
1308
1309    #[test]
1310    fn all_five_side_channels_compose_in_any_order() {
1311        let sig = ColorSignal::bt709_limited();
1312        let id = LayerIdentity::new(1).with_view_id(1);
1313        // Alpha before palette, palette in the middle, every other
1314        // record around them.
1315        let mut f = gray_frame()
1316            .with_palette_alpha(vec![0])
1317            .with_layer(id)
1318            .with_palette(vec![1, 2, 3, 4, 5, 6])
1319            .with_color_signal(sig)
1320            .with_significant_bits(vec![8]);
1321        assert_eq!(f.planes.len(), 6);
1322        assert_eq!(f.image_plane_count(), 1);
1323        assert_eq!(f.palette(), Some(&[1, 2, 3, 4, 5, 6][..]));
1324        assert_eq!(f.palette_alpha(), Some(&[0][..]));
1325        assert_eq!(f.palette_rgba(0), Some([1, 2, 3, 0]));
1326        assert_eq!(f.palette_rgba(1), Some([4, 5, 6, 255]));
1327        assert_eq!(f.significant_bits(), Some(&[8][..]));
1328        assert_eq!(f.color_signal(), Some(sig));
1329        assert_eq!(f.layer(), Some(id));
1330
1331        // Replacing records from the middle of the run leaves the
1332        // others in place.
1333        f.set_palette_alpha(vec![255, 0]);
1334        f.set_color_signal(ColorSignal::srgb());
1335        f.set_palette(vec![7, 8, 9, 10, 11, 12]);
1336        assert_eq!(f.planes.len(), 6);
1337        assert_eq!(f.palette_rgba(1), Some([10, 11, 12, 0]));
1338        assert_eq!(f.significant_bits(), Some(&[8][..]));
1339        assert_eq!(f.layer(), Some(id));
1340        assert_eq!(f.color_signal(), Some(ColorSignal::srgb()));
1341
1342        // Detaching in an arbitrary order.
1343        assert_eq!(f.take_layer(), Some(id));
1344        assert_eq!(f.take_palette_alpha(), Some(vec![255, 0]));
1345        assert_eq!(f.palette_rgba(1), Some([10, 11, 12, 255]));
1346        assert_eq!(f.take_palette(), Some(vec![7, 8, 9, 10, 11, 12]));
1347        assert_eq!(f.take_color_signal(), Some(ColorSignal::srgb()));
1348        assert_eq!(f.take_significant_bits(), Some(vec![8]));
1349        assert_eq!(f.planes.len(), 1);
1350        assert_eq!(f.image_plane_count(), 1);
1351
1352        // Reverse construction order lands on the same answers.
1353        let g = gray_frame()
1354            .with_significant_bits(vec![8])
1355            .with_color_signal(sig)
1356            .with_palette(vec![1, 2, 3])
1357            .with_layer(id)
1358            .with_palette_alpha(vec![9]);
1359        assert_eq!(g.planes.len(), 6);
1360        assert_eq!(g.image_plane_count(), 1);
1361        assert_eq!(g.palette_rgba(0), Some([1, 2, 3, 9]));
1362        assert_eq!(g.significant_bits(), Some(&[8][..]));
1363        assert_eq!(g.color_signal(), Some(sig));
1364        assert_eq!(g.layer(), Some(id));
1365    }
1366
1367    #[test]
1368    fn palette_alpha_tag_with_empty_data_is_not_a_record() {
1369        let f = VideoFrame {
1370            pts: None,
1371            planes: vec![
1372                VideoPlane {
1373                    stride: 4,
1374                    data: vec![0u8; 8],
1375                },
1376                VideoPlane {
1377                    stride: usize::MAX - 3,
1378                    data: Vec::new(),
1379                },
1380            ],
1381        };
1382        assert_eq!(f.palette_alpha(), None);
1383        assert_eq!(f.image_plane_count(), 2);
1384    }
1385
1386    #[test]
1387    fn palette_alpha_survives_clone_and_frame_wrapping() {
1388        let f = gray_frame().with_palette_rgba(&[[1, 2, 3, 0], [4, 5, 6, 255]]);
1389        let cloned = f.clone();
1390        assert_eq!(cloned.palette_alpha(), f.palette_alpha());
1391        let wrapped = Frame::Video(cloned);
1392        assert_eq!(wrapped.pts(), Some(7));
1393        if let Frame::Video(v) = wrapped {
1394            assert_eq!(v.palette_rgba(0), Some([1, 2, 3, 0]));
1395            assert_eq!(v.palette_rgba(1), Some([4, 5, 6, 255]));
1396        } else {
1397            unreachable!("wrapped as Video above");
1398        }
1399    }
1400
1401    #[test]
1402    fn frame_without_blobs_reads_empty() {
1403        let f = gray_frame();
1404        assert!(f.blobs().is_empty());
1405        assert_eq!(f.blob(&BlobKind::ICC), None);
1406        assert_eq!(f.image_plane_count(), 1);
1407        assert_eq!(f.planes.len(), 1);
1408    }
1409
1410    #[test]
1411    fn set_blobs_round_trips_and_keeps_image_planes_intact() {
1412        let icc = MetadataBlob::new(BlobKind::ICC, vec![1, 2, 3, 4]);
1413        let exif = MetadataBlob::new(BlobKind::EXIF, b"II*\0".to_vec());
1414        let mut f = gray_frame();
1415        f.set_blobs(vec![icc.clone(), exif.clone()]);
1416
1417        assert_eq!(f.blobs(), vec![icc.clone(), exif.clone()]);
1418        assert_eq!(f.blob(&BlobKind::ICC), Some(icc.clone()));
1419        assert_eq!(f.blob(&BlobKind::EXIF), Some(exif.clone()));
1420        assert_eq!(f.blob(&BlobKind::XMP), None);
1421        // Image-plane view is unchanged; the raw field sees one record
1422        // holding the whole list, tagged usize::MAX - 4.
1423        assert_eq!(f.image_plane_count(), 1);
1424        assert_eq!(f.image_planes()[0].data.len(), 8);
1425        assert_eq!(f.planes.len(), 2);
1426        assert_eq!(f.planes[1].stride, usize::MAX - 4);
1427        assert_eq!(f.planes[1].data, encode_blobs(&[icc.clone(), exif.clone()]));
1428
1429        // Replacement, not stacking.
1430        f.set_blobs(vec![exif.clone()]);
1431        assert_eq!(f.planes.len(), 2);
1432        assert_eq!(f.blobs(), vec![exif.clone()]);
1433
1434        // Empty input removes the record entirely.
1435        f.set_blobs(Vec::new());
1436        assert!(f.blobs().is_empty());
1437        assert_eq!(f.planes.len(), 1);
1438
1439        // take_blobs detaches and returns the list.
1440        f.set_blobs(vec![icc.clone()]);
1441        assert_eq!(f.take_blobs(), vec![icc]);
1442        assert!(f.blobs().is_empty());
1443        assert!(f.take_blobs().is_empty());
1444        assert_eq!(f.planes.len(), 1);
1445    }
1446
1447    #[test]
1448    fn push_blob_appends_in_order_and_with_blob_chains() {
1449        let a = MetadataBlob::new(BlobKind::COVER_ART, vec![1]).with_mime("image/jpeg");
1450        let b = MetadataBlob::new(BlobKind::COVER_ART, vec![2]).with_mime("image/png");
1451        let c = MetadataBlob::new(BlobKind::custom("exr-attributes"), vec![3]);
1452        let mut f = gray_frame().with_blob(a.clone());
1453        f.push_blob(b.clone());
1454        f.push_blob(c.clone());
1455        // Same kind twice: both kept, first-of-kind lookup is stable.
1456        assert_eq!(f.blobs(), vec![a.clone(), b.clone(), c.clone()]);
1457        assert_eq!(f.blob(&BlobKind::COVER_ART), Some(a));
1458        assert_eq!(f.blob(&BlobKind::custom("exr-attributes")), Some(c));
1459        // Still one record.
1460        assert_eq!(f.planes.len(), 2);
1461        assert_eq!(f.image_plane_count(), 1);
1462    }
1463
1464    #[test]
1465    fn malformed_blobs_record_reads_empty_and_is_replaced_by_push() {
1466        let mut f = VideoFrame {
1467            pts: None,
1468            planes: vec![
1469                VideoPlane {
1470                    stride: 4,
1471                    data: vec![0u8; 8],
1472                },
1473                VideoPlane {
1474                    stride: usize::MAX - 4,
1475                    data: vec![9, 9, 9],
1476                },
1477            ],
1478        };
1479        // Side-channel-shaped (excluded from the image planes) but
1480        // decodes to nothing.
1481        assert_eq!(f.image_plane_count(), 1);
1482        assert!(f.blobs().is_empty());
1483        assert_eq!(f.blob(&BlobKind::ICC), None);
1484        // push replaces the unreadable record with the single new blob.
1485        let icc = MetadataBlob::new(BlobKind::ICC, vec![7]);
1486        f.push_blob(icc.clone());
1487        assert_eq!(f.planes.len(), 2);
1488        assert_eq!(f.blobs(), vec![icc]);
1489        // take on a malformed record removes it and reports empty.
1490        f.planes[1].data = vec![9, 9, 9];
1491        assert!(f.take_blobs().is_empty());
1492        assert_eq!(f.planes.len(), 1);
1493    }
1494
1495    #[test]
1496    fn blobs_tag_with_empty_data_is_not_a_record() {
1497        let f = VideoFrame {
1498            pts: None,
1499            planes: vec![
1500                VideoPlane {
1501                    stride: 4,
1502                    data: vec![0u8; 8],
1503                },
1504                VideoPlane {
1505                    stride: usize::MAX - 4,
1506                    data: Vec::new(),
1507                },
1508            ],
1509        };
1510        assert!(f.blobs().is_empty());
1511        assert_eq!(f.image_plane_count(), 2);
1512    }
1513
1514    #[test]
1515    fn all_six_side_channels_compose_in_any_order() {
1516        let sig = ColorSignal::bt709_limited();
1517        let id = LayerIdentity::new(1).with_view_id(1);
1518        let icc = MetadataBlob::new(BlobKind::ICC, vec![1, 2, 3]);
1519        let xmp = MetadataBlob::new(BlobKind::XMP, b"<x/>".to_vec());
1520        let mut f = gray_frame()
1521            .with_blob(icc.clone())
1522            .with_palette_alpha(vec![0])
1523            .with_layer(id)
1524            .with_palette(vec![1, 2, 3, 4, 5, 6])
1525            .with_color_signal(sig)
1526            .with_significant_bits(vec![8]);
1527        assert_eq!(f.planes.len(), 7);
1528        assert_eq!(f.image_plane_count(), 1);
1529        assert_eq!(f.blobs(), vec![icc.clone()]);
1530        assert_eq!(f.palette_rgba(0), Some([1, 2, 3, 0]));
1531        assert_eq!(f.significant_bits(), Some(&[8][..]));
1532        assert_eq!(f.color_signal(), Some(sig));
1533        assert_eq!(f.layer(), Some(id));
1534
1535        // Growing the blobs list from the middle of the run leaves the
1536        // others in place and keeps one record.
1537        f.push_blob(xmp.clone());
1538        f.set_color_signal(ColorSignal::srgb());
1539        assert_eq!(f.planes.len(), 7);
1540        assert_eq!(f.blobs(), vec![icc.clone(), xmp.clone()]);
1541        assert_eq!(f.palette_rgba(1), Some([4, 5, 6, 255]));
1542        assert_eq!(f.layer(), Some(id));
1543        assert_eq!(f.color_signal(), Some(ColorSignal::srgb()));
1544
1545        // Detaching in an arbitrary order.
1546        assert_eq!(f.take_palette(), Some(vec![1, 2, 3, 4, 5, 6]));
1547        assert_eq!(f.blobs(), vec![icc.clone(), xmp.clone()]);
1548        assert_eq!(f.take_blobs(), vec![icc, xmp]);
1549        assert_eq!(f.take_layer(), Some(id));
1550        assert_eq!(f.take_palette_alpha(), None); // orphaned → None, removed
1551        assert_eq!(f.take_color_signal(), Some(ColorSignal::srgb()));
1552        assert_eq!(f.take_significant_bits(), Some(vec![8]));
1553        assert_eq!(f.planes.len(), 1);
1554        assert_eq!(f.image_plane_count(), 1);
1555    }
1556
1557    #[test]
1558    fn blobs_survive_clone_and_frame_wrapping() {
1559        let exif = MetadataBlob::new(BlobKind::EXIF, b"MM\0*".to_vec());
1560        let f = gray_frame().with_blob(exif.clone());
1561        let cloned = f.clone();
1562        assert_eq!(cloned.blobs(), f.blobs());
1563        let wrapped = Frame::Video(cloned);
1564        assert_eq!(wrapped.pts(), Some(7));
1565        if let Frame::Video(v) = wrapped {
1566            assert_eq!(v.blob(&BlobKind::EXIF), Some(exif));
1567        } else {
1568            unreachable!("wrapped as Video above");
1569        }
1570    }
1571
1572    #[test]
1573    fn palette_survives_clone_and_frame_wrapping() {
1574        let f = gray_frame().with_palette(full_palette());
1575        let cloned = f.clone();
1576        assert_eq!(cloned.palette(), f.palette());
1577
1578        // Through the Frame enum, pts and palette both survive.
1579        let wrapped = Frame::Video(cloned);
1580        assert_eq!(wrapped.pts(), Some(7));
1581        if let Frame::Video(v) = wrapped {
1582            assert_eq!(v.palette().map(<[u8]>::len), Some(768));
1583        } else {
1584            unreachable!("wrapped as Video above");
1585        }
1586    }
1587}