Skip to main content

oxideav_core/
frame.rs

1//! Uncompressed audio and video frames.
2
3use crate::layer::LayerIdentity;
4use crate::signal::ColorSignal;
5use crate::subtitle::SubtitleCue;
6use crate::vector::VectorFrame;
7
8/// A decoded chunk of uncompressed data: either audio samples, a video
9/// picture, or (for subtitle streams) a single styled cue.
10///
11/// Marked `#[non_exhaustive]` — consumers that match on variants must
12/// include a wildcard arm. This lets the crate add new frame kinds (data
13/// tracks, hap rops, …) without breaking downstream code.
14#[derive(Clone, Debug)]
15#[non_exhaustive]
16pub enum Frame {
17    /// Uncompressed audio samples.
18    Audio(AudioFrame),
19    /// One uncompressed video picture.
20    Video(VideoFrame),
21    /// A single subtitle cue. Timing is carried inside the cue itself
22    /// (`start_us`/`end_us`) so it's independent of container time bases,
23    /// but the enclosing pipeline/muxer can still rescale via `pts` at
24    /// the packet layer.
25    Subtitle(SubtitleCue),
26    /// A resolution-independent vector-graphics frame. Produced by
27    /// vector-format decoders (`oxideav-svg`, the vector path of
28    /// `oxideav-pdf`) and consumed by vector renderers / writers.
29    /// See [`crate::vector`] for the full primitive set.
30    Vector(VectorFrame),
31}
32
33impl Frame {
34    /// Presentation timestamp of the frame in its stream's time base
35    /// (a subtitle cue reports its `start_us`); `None` if unknown.
36    pub fn pts(&self) -> Option<i64> {
37        match self {
38            Self::Audio(a) => a.pts,
39            Self::Video(v) => v.pts,
40            Self::Subtitle(s) => Some(s.start_us),
41            Self::Vector(v) => v.pts,
42        }
43    }
44}
45
46/// Uncompressed audio frame.
47///
48/// Stream-level properties (sample format, channel count, sample rate,
49/// time base) are NOT carried per-frame — read them from the stream's
50/// [`CodecParameters`](crate::CodecParameters). Frames stay lightweight
51/// because real-time playback moves thousands per second per stream.
52///
53/// Sample layout is determined by the stream's `SampleFormat`:
54/// - Interleaved formats: `data` has one plane; samples are stored as
55///   `ch0 ch1 ... chN ch0 ch1 ... chN ...`.
56/// - Planar formats: `data` has one plane per channel.
57///
58/// Use [`SampleFormat::plane_count`](crate::SampleFormat::plane_count)
59/// with the stream's channel count to compute the expected `data.len()`.
60#[derive(Clone, Debug)]
61pub struct AudioFrame {
62    /// Number of samples *per channel* in this frame. Variable per-frame
63    /// for VBR codecs and on partial flushes.
64    pub samples: u32,
65    /// Presentation timestamp in the stream's time base; `None` if unknown.
66    pub pts: Option<i64>,
67    /// Raw sample bytes. Length matches `format.plane_count(channels)`
68    /// from the stream's `CodecParameters`.
69    pub data: Vec<Vec<u8>>,
70}
71
72/// Uncompressed video frame.
73///
74/// Stream-level properties (pixel format, width, height, time base) are
75/// NOT carried per-frame — read them from the stream's
76/// [`CodecParameters`](crate::CodecParameters). Frames stay lightweight
77/// because real-time playback moves thousands per second per stream.
78///
79/// # Side-channels
80///
81/// `VideoFrame` (like [`VideoPlane`]) is a fully-public struct built by
82/// struct literal throughout the codec crates, so per-frame metadata
83/// cannot be added as new fields without breaking every constructor.
84/// Instead, optional metadata rides in-band as *side-channel* entries at
85/// the tail of `planes`: [`VideoPlane`] values whose shape is impossible
86/// for an image plane, which makes them unambiguous. Every record has
87/// non-empty `data` and a `stride` that no image plane can have —
88/// either `0` (an image plane's `data` is `stride × rows` long, so a
89/// zero stride forces empty data) or a value above `isize::MAX`
90/// (`stride × rows` with any non-zero row count would exceed what a
91/// `Vec` can hold). The whole `stride > isize::MAX` band is reserved
92/// for side-channel tags. Four record kinds exist, distinguished by
93/// their `stride` tag:
94///
95/// - **Palette** — `stride == 0`. Carries the color table for
96///   palette-indexed content
97///   ([`PixelFormat::Pal8`](crate::PixelFormat::Pal8)); see
98///   [`palette`](Self::palette) / [`set_palette`](Self::set_palette).
99/// - **Per-plane significant bits** — `stride == usize::MAX`. Carries
100///   mixed per-plane bit depths (e.g. 12-bit luma with 10-bit chroma
101///   from a wavelet codec's custom signal range); see
102///   [`significant_bits`](Self::significant_bits) /
103///   [`set_significant_bits`](Self::set_significant_bits).
104/// - **Colour signal** — `stride == usize::MAX - 1`. Carries a
105///   [`ColorSignal`] (sample range + H.273 primaries / transfer /
106///   matrix) for producers whose signal is per-picture or that have no
107///   stream object to put it on; see
108///   [`color_signal`](Self::color_signal) /
109///   [`set_color_signal`](Self::set_color_signal).
110/// - **Layer identity** — `stride == usize::MAX - 2`. Carries a
111///   [`LayerIdentity`] (layer / view / access-unit) for frames of
112///   multi-layer or multi-view streams; see [`layer`](Self::layer) /
113///   [`set_layer`](Self::set_layer).
114///
115/// The records compose: a frame can carry any subset at once, in any
116/// order, within the trailing run of side-channel-shaped entries. The
117/// typed accessors find each record by its `stride` tag regardless of
118/// order, and [`image_planes`](Self::image_planes) /
119/// [`image_plane_count`](Self::image_plane_count) exclude the whole
120/// trailing run. Frames without any attached side-channel are
121/// byte-for-byte identical to what they always were.
122///
123/// Consumers that index `planes` directly (rather than through
124/// [`image_planes`](Self::image_planes)) see the records as extra
125/// trailing entries; producers attaching a record to every frame of a
126/// stream should therefore prefer the stream-level home
127/// ([`CodecParameters`](crate::CodecParameters)) for data that does not
128/// vary per picture, and attach per-frame records when the value is
129/// genuinely per-picture or no stream object exists.
130#[derive(Clone, Debug)]
131pub struct VideoFrame {
132    /// Presentation timestamp in the stream's time base; `None` if unknown.
133    pub pts: Option<i64>,
134    /// One entry per plane (e.g., 3 for Yuv420P). Each entry is `(stride, bytes)`.
135    ///
136    /// May additionally end with side-channel entries (palette,
137    /// per-plane significant bits, colour signal, layer identity — see
138    /// the type-level docs). Code that
139    /// wants only pixel planes should iterate
140    /// [`image_planes`](Self::image_planes) instead of this field.
141    pub planes: Vec<VideoPlane>,
142}
143
144/// `stride` tag of the per-plane significant-bits side-channel record.
145/// (The palette record's tag is `0`; see the [`VideoFrame`] docs.)
146const SIGNIFICANT_BITS_STRIDE: usize = usize::MAX;
147
148/// `stride` tag of the colour-signal side-channel record.
149const COLOR_SIGNAL_STRIDE: usize = usize::MAX - 1;
150
151/// `stride` tag of the layer-identity side-channel record.
152const LAYER_IDENTITY_STRIDE: usize = usize::MAX - 2;
153
154/// Smallest `stride` value that is impossible for an image plane with
155/// at least one row: `stride × rows` would exceed `isize::MAX`, the
156/// largest allocation a `Vec` can hold. Every non-zero side-channel
157/// tag lives at or above this value.
158const SIDE_CHANNEL_STRIDE_FLOOR: usize = isize::MAX as usize + 1;
159
160impl VideoFrame {
161    /// `true` when `plane` has a side-channel record shape: non-empty
162    /// data with an impossible-for-an-image-plane stride (`0`, or any
163    /// value above `isize::MAX`) as described in the type-level docs.
164    fn is_side_channel_entry(plane: &VideoPlane) -> bool {
165        (plane.stride == 0 || plane.stride >= SIDE_CHANNEL_STRIDE_FLOOR) && !plane.data.is_empty()
166    }
167
168    /// Index of the first entry of the trailing side-channel run — equal
169    /// to the number of image planes. Scans backwards from the tail
170    /// while entries have a side-channel shape.
171    fn side_channel_run_start(&self) -> usize {
172        let mut start = self.planes.len();
173        while start > 0 && Self::is_side_channel_entry(&self.planes[start - 1]) {
174            start -= 1;
175        }
176        start
177    }
178
179    /// Index in `planes` of the side-channel record tagged with
180    /// `stride_tag`, searching the trailing side-channel run only (the
181    /// last match wins if a malformed frame carries duplicates).
182    fn side_channel_index(&self, stride_tag: usize) -> Option<usize> {
183        let start = self.side_channel_run_start();
184        self.planes[start..]
185            .iter()
186            .rposition(|p| p.stride == stride_tag)
187            .map(|i| start + i)
188    }
189
190    /// Remove every record tagged `stride_tag` from the trailing
191    /// side-channel run, returning the data of the record the readers
192    /// would have reported (the last match — consistent with
193    /// [`side_channel_index`](Self::side_channel_index)).
194    fn remove_side_channel(&mut self, stride_tag: usize) -> Option<Vec<u8>> {
195        let reported = self
196            .side_channel_index(stride_tag)
197            .map(|i| self.planes.remove(i).data);
198        while let Some(i) = self.side_channel_index(stride_tag) {
199            self.planes.remove(i);
200        }
201        reported
202    }
203
204    /// The frame's attached palette, if any.
205    ///
206    /// Returns the raw bytes of the palette side-channel (see the
207    /// type-level docs): packed 3-byte RGB entries, entry `i` at bytes
208    /// `3*i .. 3*i + 3` in R, G, B order. A full
209    /// [`Pal8`](crate::PixelFormat::Pal8) table is 256 entries
210    /// (768 bytes), but producers may attach fewer when the source
211    /// image declares a shorter table; indices at or beyond
212    /// `len / 3` are undefined by this frame and up to the consumer's
213    /// missing-entry policy (typically black).
214    pub fn palette(&self) -> Option<&[u8]> {
215        self.side_channel_index(0)
216            .map(|i| self.planes[i].data.as_slice())
217    }
218
219    /// The RGB triplet for palette entry `index`, or `None` when no
220    /// palette is attached or the attached table is too short to cover
221    /// `index`. Sugar over [`palette`](Self::palette) for per-pixel
222    /// lookups.
223    pub fn palette_rgb(&self, index: u8) -> Option<[u8; 3]> {
224        let pal = self.palette()?;
225        let at = usize::from(index) * 3;
226        let entry = pal.get(at..at + 3)?;
227        Some([entry[0], entry[1], entry[2]])
228    }
229
230    /// Attach (or replace) the frame's palette side-channel.
231    ///
232    /// `rgb` is packed 3-byte RGB entries — see
233    /// [`palette`](Self::palette) for the exact layout; pass a length
234    /// that is a multiple of 3 (up to 768 bytes for a full 256-entry
235    /// [`Pal8`](crate::PixelFormat::Pal8) table). The bytes are stored
236    /// verbatim. An empty `rgb` removes any attached palette instead
237    /// (the sentinel requires non-empty data), leaving the frame
238    /// exactly as it was before any palette was attached.
239    pub fn set_palette(&mut self, rgb: Vec<u8>) {
240        self.remove_side_channel(0);
241        if !rgb.is_empty() {
242            self.planes.push(VideoPlane {
243                stride: 0,
244                data: rgb,
245            });
246        }
247    }
248
249    /// Builder-style counterpart to [`set_palette`](Self::set_palette)
250    /// for construction chains:
251    /// `VideoFrame { pts, planes }.with_palette(rgb)`.
252    pub fn with_palette(mut self, rgb: Vec<u8>) -> Self {
253        self.set_palette(rgb);
254        self
255    }
256
257    /// Detach and return the frame's palette side-channel, if any.
258    /// Afterwards the frame carries no palette (any other side-channel
259    /// record is left in place).
260    pub fn take_palette(&mut self) -> Option<Vec<u8>> {
261        self.remove_side_channel(0)
262    }
263
264    /// The frame's attached per-plane significant-bits record, if any.
265    ///
266    /// Returns the raw bytes of the significant-bits side-channel (see
267    /// the type-level docs): byte `k` is the number of significant bits
268    /// in the samples of image plane `k`, in plane order. This lets a
269    /// producer express **mixed** per-plane depths that no single
270    /// [`PixelFormat`](crate::PixelFormat) variant can name — e.g. a
271    /// wavelet codec's custom signal range with 12-bit luma and 10-bit
272    /// chroma, stored on a `Yuv444P12Le` surface with an attached
273    /// record of `[12, 10, 10]`.
274    ///
275    /// # Semantics
276    ///
277    /// - Values are **LSB-anchored**: a plane with `b` significant bits
278    ///   keeps its sample values in the low `b` bits of each storage
279    ///   word, with the upper bits zero — the same convention as this
280    ///   crate's partial-depth formats (`Gray10Le`, `Yuv420P10Le`,
281    ///   `Gbrp12Le`, …, each documented as "uses the low N bits of a
282    ///   16-bit word"). Full-scale for `b` significant bits is
283    ///   `(1 << b) - 1`.
284    /// - Each value must satisfy `1 ≤ b ≤ 8 × storage-word-bytes` of
285    ///   the frame's pixel format (so at most 8 for byte-sized planes,
286    ///   16 for LE-16-bit-word planes). The record refines the storage
287    ///   format's *significant* depth; it never changes the storage
288    ///   word size or plane geometry.
289    /// - A record shorter than the image-plane count (or a missing
290    ///   record) leaves the uncovered planes at the pixel format's own
291    ///   documented depth. Bytes are stored verbatim; out-of-range
292    ///   values are a producer bug and consumers may clamp or reject
293    ///   them.
294    pub fn significant_bits(&self) -> Option<&[u8]> {
295        self.side_channel_index(SIGNIFICANT_BITS_STRIDE)
296            .map(|i| self.planes[i].data.as_slice())
297    }
298
299    /// The significant-bit count for image plane `plane`, or `None`
300    /// when no record is attached or the attached record is too short
301    /// to cover `plane` (fall back to the pixel format's own depth).
302    /// Sugar over [`significant_bits`](Self::significant_bits) for
303    /// per-plane lookups.
304    pub fn plane_significant_bits(&self, plane: usize) -> Option<u8> {
305        self.significant_bits()?.get(plane).copied()
306    }
307
308    /// Attach (or replace) the frame's per-plane significant-bits
309    /// side-channel.
310    ///
311    /// `bits` holds one byte per image plane, in plane order — see
312    /// [`significant_bits`](Self::significant_bits) for the exact
313    /// semantics (LSB-anchored values, `1 ≤ b ≤ storage word bits`).
314    /// The bytes are stored verbatim. An empty `bits` removes any
315    /// attached record instead (the sentinel requires non-empty data),
316    /// leaving the frame exactly as it was before any record was
317    /// attached. Any attached palette is unaffected.
318    pub fn set_significant_bits(&mut self, bits: Vec<u8>) {
319        self.remove_side_channel(SIGNIFICANT_BITS_STRIDE);
320        if !bits.is_empty() {
321            self.planes.push(VideoPlane {
322                stride: SIGNIFICANT_BITS_STRIDE,
323                data: bits,
324            });
325        }
326    }
327
328    /// Builder-style counterpart to
329    /// [`set_significant_bits`](Self::set_significant_bits) for
330    /// construction chains:
331    /// `VideoFrame { pts, planes }.with_significant_bits(bits)`.
332    pub fn with_significant_bits(mut self, bits: Vec<u8>) -> Self {
333        self.set_significant_bits(bits);
334        self
335    }
336
337    /// Detach and return the frame's per-plane significant-bits
338    /// side-channel, if any. Afterwards the frame carries no
339    /// significant-bits record (any attached palette is left in place).
340    pub fn take_significant_bits(&mut self) -> Option<Vec<u8>> {
341        self.remove_side_channel(SIGNIFICANT_BITS_STRIDE)
342    }
343
344    /// The frame's attached colour-signal description, if any.
345    ///
346    /// Decoded from the colour-signal side-channel record (see the
347    /// type-level docs; wire form in [`ColorSignal::to_bytes`]). A
348    /// per-frame description refines the stream-level
349    /// [`CodecParameters::color_signal`](crate::CodecParameters::color_signal):
350    /// consumers resolve `frame.color_signal().unwrap_or_default()
351    /// .or(params.color_signal)` and then apply their own policy to
352    /// whatever is still unspecified. A malformed (too short) record
353    /// reads as `None`.
354    pub fn color_signal(&self) -> Option<ColorSignal> {
355        self.side_channel_index(COLOR_SIGNAL_STRIDE)
356            .and_then(|i| ColorSignal::from_bytes(&self.planes[i].data))
357    }
358
359    /// Attach (or replace) the frame's colour-signal side-channel.
360    /// Other side-channel records are unaffected.
361    pub fn set_color_signal(&mut self, signal: ColorSignal) {
362        self.remove_side_channel(COLOR_SIGNAL_STRIDE);
363        self.planes.push(VideoPlane {
364            stride: COLOR_SIGNAL_STRIDE,
365            data: signal.to_bytes().to_vec(),
366        });
367    }
368
369    /// Builder-style counterpart to
370    /// [`set_color_signal`](Self::set_color_signal) for construction
371    /// chains: `VideoFrame { pts, planes }.with_color_signal(sig)`.
372    pub fn with_color_signal(mut self, signal: ColorSignal) -> Self {
373        self.set_color_signal(signal);
374        self
375    }
376
377    /// Detach and return the frame's colour-signal side-channel, if
378    /// any. Afterwards the frame carries no colour signal (other
379    /// records are left in place).
380    pub fn take_color_signal(&mut self) -> Option<ColorSignal> {
381        self.remove_side_channel(COLOR_SIGNAL_STRIDE)
382            .and_then(|d| ColorSignal::from_bytes(&d))
383    }
384
385    /// The frame's attached layer / view identity, if any.
386    ///
387    /// Decoded from the layer-identity side-channel record (see the
388    /// type-level docs; wire form in [`LayerIdentity::to_bytes`]).
389    /// Single-layer decoders attach nothing; consumers treat `None` as
390    /// "base layer, no view". A malformed (too short) record reads as
391    /// `None`.
392    pub fn layer(&self) -> Option<LayerIdentity> {
393        self.side_channel_index(LAYER_IDENTITY_STRIDE)
394            .and_then(|i| LayerIdentity::from_bytes(&self.planes[i].data))
395    }
396
397    /// The frame's layer identity, or the base layer when none is
398    /// attached. Sugar over [`layer`](Self::layer) for consumers that
399    /// handle single- and multi-layer streams alike.
400    pub fn layer_or_base(&self) -> LayerIdentity {
401        self.layer().unwrap_or_default()
402    }
403
404    /// Attach (or replace) the frame's layer-identity side-channel.
405    /// Other side-channel records are unaffected.
406    pub fn set_layer(&mut self, layer: LayerIdentity) {
407        self.remove_side_channel(LAYER_IDENTITY_STRIDE);
408        self.planes.push(VideoPlane {
409            stride: LAYER_IDENTITY_STRIDE,
410            data: layer.to_bytes().to_vec(),
411        });
412    }
413
414    /// Builder-style counterpart to [`set_layer`](Self::set_layer) for
415    /// construction chains:
416    /// `VideoFrame { pts, planes }.with_layer(LayerIdentity::new(1))`.
417    pub fn with_layer(mut self, layer: LayerIdentity) -> Self {
418        self.set_layer(layer);
419        self
420    }
421
422    /// Detach and return the frame's layer-identity side-channel, if
423    /// any. Afterwards the frame carries no layer identity (other
424    /// records are left in place).
425    pub fn take_layer(&mut self) -> Option<LayerIdentity> {
426        self.remove_side_channel(LAYER_IDENTITY_STRIDE)
427            .and_then(|d| LayerIdentity::from_bytes(&d))
428    }
429
430    /// The frame's image planes — `planes` with the trailing
431    /// side-channel entries (palette, significant bits, colour signal,
432    /// layer identity) excluded.
433    /// Prefer this over indexing `planes` directly in code that
434    /// handles side-channel-capable frames.
435    pub fn image_planes(&self) -> &[VideoPlane] {
436        &self.planes[..self.image_plane_count()]
437    }
438
439    /// Number of image planes (excludes every side-channel entry).
440    /// Matches the stream pixel format's
441    /// [`plane_count`](crate::PixelFormat::plane_count) for well-formed
442    /// frames.
443    pub fn image_plane_count(&self) -> usize {
444        self.side_channel_run_start()
445    }
446}
447
448/// One plane of a [`VideoFrame`]: row-major sample bytes plus the
449/// stride between rows.
450///
451/// An entry with non-empty `data` and a `stride` of `0` or above
452/// `isize::MAX` is not an image plane: it is a side-channel record
453/// (palette, per-plane significant bits, colour signal, layer identity)
454/// described on [`VideoFrame`] — only meaningful within the trailing
455/// run of `VideoFrame::planes`.
456#[derive(Clone, Debug)]
457pub struct VideoPlane {
458    /// Bytes per row in `data`.
459    pub stride: usize,
460    /// Raw plane bytes, `stride × rows` long (rows may carry padding
461    /// beyond the visible width).
462    pub data: Vec<u8>,
463}
464
465#[cfg(test)]
466mod tests {
467    use super::*;
468
469    fn gray_frame() -> VideoFrame {
470        // 4×2 Gray8 image plane.
471        VideoFrame {
472            pts: Some(7),
473            planes: vec![VideoPlane {
474                stride: 4,
475                data: vec![0u8; 8],
476            }],
477        }
478    }
479
480    /// A full 256-entry table where entry i is (i, !i, i^0x55).
481    fn full_palette() -> Vec<u8> {
482        (0u16..256)
483            .flat_map(|i| {
484                let i = i as u8;
485                [i, !i, i ^ 0x55]
486            })
487            .collect()
488    }
489
490    #[test]
491    fn frame_without_palette_reports_none_and_full_image_planes() {
492        let f = gray_frame();
493        assert_eq!(f.palette(), None);
494        assert_eq!(f.palette_rgb(0), None);
495        assert_eq!(f.image_plane_count(), 1);
496        assert_eq!(f.image_planes().len(), 1);
497        assert_eq!(f.image_planes()[0].stride, 4);
498    }
499
500    #[test]
501    fn set_palette_round_trips_and_keeps_image_planes_intact() {
502        let mut f = gray_frame();
503        let pal = full_palette();
504        f.set_palette(pal.clone());
505
506        assert_eq!(f.palette(), Some(pal.as_slice()));
507        // Image-plane view is unchanged by the side-channel.
508        assert_eq!(f.image_plane_count(), 1);
509        assert_eq!(f.image_planes()[0].data.len(), 8);
510        // The raw field sees the sentinel entry at the tail.
511        assert_eq!(f.planes.len(), 2);
512        assert_eq!(f.planes[1].stride, 0);
513
514        // Entry lookup: entry i is (i, !i, i ^ 0x55) by construction.
515        assert_eq!(f.palette_rgb(0), Some([0x00, 0xFF, 0x55]));
516        assert_eq!(f.palette_rgb(0xAB), Some([0xAB, 0x54, 0xFE]));
517        assert_eq!(f.palette_rgb(255), Some([0xFF, 0x00, 0xAA]));
518    }
519
520    #[test]
521    fn set_palette_replaces_existing_table() {
522        let mut f = gray_frame();
523        f.set_palette(vec![1, 2, 3]);
524        f.set_palette(vec![9, 8, 7, 6, 5, 4]);
525        // Replacement, not stacking: one image plane + one sentinel.
526        assert_eq!(f.planes.len(), 2);
527        assert_eq!(f.palette(), Some(&[9, 8, 7, 6, 5, 4][..]));
528        assert_eq!(f.palette_rgb(1), Some([6, 5, 4]));
529    }
530
531    #[test]
532    fn short_palette_covers_only_its_entries() {
533        let f = gray_frame().with_palette(vec![10, 20, 30, 40, 50, 60]);
534        assert_eq!(f.palette_rgb(0), Some([10, 20, 30]));
535        assert_eq!(f.palette_rgb(1), Some([40, 50, 60]));
536        // Beyond the table: undefined by the frame → None.
537        assert_eq!(f.palette_rgb(2), None);
538        assert_eq!(f.palette_rgb(255), None);
539    }
540
541    #[test]
542    fn empty_palette_clears_and_take_palette_detaches() {
543        let mut f = gray_frame();
544        f.set_palette(vec![1, 2, 3]);
545        assert!(f.palette().is_some());
546
547        // Empty input removes the side-channel entirely.
548        f.set_palette(Vec::new());
549        assert_eq!(f.palette(), None);
550        assert_eq!(f.planes.len(), 1);
551
552        // take_palette detaches and returns the bytes.
553        f.set_palette(vec![4, 5, 6]);
554        assert_eq!(f.take_palette(), Some(vec![4, 5, 6]));
555        assert_eq!(f.palette(), None);
556        assert_eq!(f.take_palette(), None);
557        assert_eq!(f.planes.len(), 1);
558    }
559
560    #[test]
561    fn zero_stride_empty_plane_is_not_mistaken_for_a_palette() {
562        // stride == 0 with EMPTY data is the degenerate (but
563        // contract-consistent) empty image plane, not the sentinel.
564        let f = VideoFrame {
565            pts: None,
566            planes: vec![
567                VideoPlane {
568                    stride: 4,
569                    data: vec![0u8; 8],
570                },
571                VideoPlane {
572                    stride: 0,
573                    data: Vec::new(),
574                },
575            ],
576        };
577        assert_eq!(f.palette(), None);
578        assert_eq!(f.image_plane_count(), 2);
579    }
580
581    #[test]
582    fn palette_on_frame_without_image_planes() {
583        // A palette can be attached before pixel planes exist (encoder
584        // scaffolding); the image-plane view is then empty.
585        let f = VideoFrame {
586            pts: None,
587            planes: Vec::new(),
588        }
589        .with_palette(vec![1, 2, 3]);
590        assert_eq!(f.palette(), Some(&[1, 2, 3][..]));
591        assert_eq!(f.image_plane_count(), 0);
592        assert!(f.image_planes().is_empty());
593    }
594
595    #[test]
596    fn frame_without_significant_bits_reports_none() {
597        let f = gray_frame();
598        assert_eq!(f.significant_bits(), None);
599        assert_eq!(f.plane_significant_bits(0), None);
600        assert_eq!(f.image_plane_count(), 1);
601    }
602
603    #[test]
604    fn set_significant_bits_round_trips_and_keeps_image_planes_intact() {
605        // A 12-bit-luma / 10-bit-chroma mixed-depth frame (the VC-2
606        // custom-signal-range shape that motivated the record).
607        let mut f = VideoFrame {
608            pts: Some(3),
609            planes: vec![
610                VideoPlane {
611                    stride: 8,
612                    data: vec![0u8; 16],
613                },
614                VideoPlane {
615                    stride: 8,
616                    data: vec![0u8; 16],
617                },
618                VideoPlane {
619                    stride: 8,
620                    data: vec![0u8; 16],
621                },
622            ],
623        };
624        f.set_significant_bits(vec![12, 10, 10]);
625
626        assert_eq!(f.significant_bits(), Some(&[12, 10, 10][..]));
627        assert_eq!(f.plane_significant_bits(0), Some(12));
628        assert_eq!(f.plane_significant_bits(1), Some(10));
629        assert_eq!(f.plane_significant_bits(2), Some(10));
630        // Beyond the record: fall back to the format default → None.
631        assert_eq!(f.plane_significant_bits(3), None);
632
633        // Image-plane view is unchanged by the side-channel.
634        assert_eq!(f.image_plane_count(), 3);
635        assert_eq!(f.image_planes().len(), 3);
636        // The raw field sees the sentinel entry at the tail.
637        assert_eq!(f.planes.len(), 4);
638        assert_eq!(f.planes[3].stride, usize::MAX);
639    }
640
641    #[test]
642    fn set_significant_bits_replaces_and_empty_clears_and_take_detaches() {
643        let mut f = gray_frame();
644        f.set_significant_bits(vec![7]);
645        f.set_significant_bits(vec![6]);
646        // Replacement, not stacking.
647        assert_eq!(f.planes.len(), 2);
648        assert_eq!(f.significant_bits(), Some(&[6][..]));
649
650        // Empty input removes the side-channel entirely.
651        f.set_significant_bits(Vec::new());
652        assert_eq!(f.significant_bits(), None);
653        assert_eq!(f.planes.len(), 1);
654
655        // take_significant_bits detaches and returns the bytes.
656        f.set_significant_bits(vec![5]);
657        assert_eq!(f.take_significant_bits(), Some(vec![5]));
658        assert_eq!(f.significant_bits(), None);
659        assert_eq!(f.take_significant_bits(), None);
660        assert_eq!(f.planes.len(), 1);
661    }
662
663    #[test]
664    fn palette_and_significant_bits_compose_in_either_order() {
665        // Palette first, then depths.
666        let mut f = gray_frame()
667            .with_palette(vec![1, 2, 3])
668            .with_significant_bits(vec![8]);
669        assert_eq!(f.palette(), Some(&[1, 2, 3][..]));
670        assert_eq!(f.significant_bits(), Some(&[8][..]));
671        assert_eq!(f.image_plane_count(), 1);
672        assert_eq!(f.planes.len(), 3);
673
674        // Replacing one record must not disturb the other, regardless
675        // of which currently sits at the tail.
676        f.set_palette(vec![9, 8, 7]);
677        assert_eq!(f.palette(), Some(&[9, 8, 7][..]));
678        assert_eq!(f.significant_bits(), Some(&[8][..]));
679        f.set_significant_bits(vec![7]);
680        assert_eq!(f.palette(), Some(&[9, 8, 7][..]));
681        assert_eq!(f.significant_bits(), Some(&[7][..]));
682        assert_eq!(f.image_plane_count(), 1);
683
684        // Depths first, then palette.
685        let g = gray_frame()
686            .with_significant_bits(vec![4])
687            .with_palette(full_palette());
688        assert_eq!(g.significant_bits(), Some(&[4][..]));
689        assert_eq!(g.palette_rgb(0), Some([0x00, 0xFF, 0x55]));
690        assert_eq!(g.image_plane_count(), 1);
691
692        // Detaching one leaves the other attached.
693        let mut h = g;
694        assert_eq!(h.take_significant_bits(), Some(vec![4]));
695        assert_eq!(h.significant_bits(), None);
696        assert_eq!(h.palette().map(<[u8]>::len), Some(768));
697        assert_eq!(h.take_palette().map(|p| p.len()), Some(768));
698        assert_eq!(h.planes.len(), 1);
699        assert_eq!(h.image_plane_count(), 1);
700    }
701
702    #[test]
703    fn max_stride_empty_plane_is_not_mistaken_for_significant_bits() {
704        // stride == usize::MAX with EMPTY data is not the sentinel
705        // (mirroring the palette rule: sentinels require non-empty
706        // data). Degenerate, but must not be misread as a record.
707        let f = VideoFrame {
708            pts: None,
709            planes: vec![
710                VideoPlane {
711                    stride: 4,
712                    data: vec![0u8; 8],
713                },
714                VideoPlane {
715                    stride: usize::MAX,
716                    data: Vec::new(),
717                },
718            ],
719        };
720        assert_eq!(f.significant_bits(), None);
721        assert_eq!(f.image_plane_count(), 2);
722    }
723
724    #[test]
725    fn significant_bits_on_frame_without_image_planes() {
726        // Like the palette, the record can be attached before pixel
727        // planes exist (encoder scaffolding).
728        let f = VideoFrame {
729            pts: None,
730            planes: Vec::new(),
731        }
732        .with_significant_bits(vec![12, 10, 10]);
733        assert_eq!(f.significant_bits(), Some(&[12, 10, 10][..]));
734        assert_eq!(f.image_plane_count(), 0);
735        assert!(f.image_planes().is_empty());
736    }
737
738    #[test]
739    fn side_channels_survive_clone_and_frame_wrapping() {
740        let f = gray_frame()
741            .with_palette(vec![1, 2, 3])
742            .with_significant_bits(vec![6]);
743        let cloned = f.clone();
744        assert_eq!(cloned.palette(), f.palette());
745        assert_eq!(cloned.significant_bits(), f.significant_bits());
746
747        let wrapped = Frame::Video(cloned);
748        assert_eq!(wrapped.pts(), Some(7));
749        if let Frame::Video(v) = wrapped {
750            assert_eq!(v.palette(), Some(&[1, 2, 3][..]));
751            assert_eq!(v.significant_bits(), Some(&[6][..]));
752        } else {
753            unreachable!("wrapped as Video above");
754        }
755    }
756
757    #[test]
758    fn frame_without_color_signal_or_layer_reports_none() {
759        let f = gray_frame();
760        assert_eq!(f.color_signal(), None);
761        assert_eq!(f.layer(), None);
762        assert_eq!(f.layer_or_base(), LayerIdentity::base());
763        assert_eq!(f.image_plane_count(), 1);
764        assert_eq!(f.planes.len(), 1);
765    }
766
767    #[test]
768    fn set_color_signal_round_trips_and_keeps_image_planes_intact() {
769        let mut f = gray_frame();
770        let sig = ColorSignal::from_code_points(9, 16, 9, true);
771        f.set_color_signal(sig);
772        assert_eq!(f.color_signal(), Some(sig));
773        assert_eq!(f.image_plane_count(), 1);
774        assert_eq!(f.image_planes()[0].data.len(), 8);
775        assert_eq!(f.planes.len(), 2);
776        assert_eq!(f.planes[1].stride, usize::MAX - 1);
777        assert_eq!(f.planes[1].data, sig.to_bytes());
778
779        // Replacement, not stacking.
780        f.set_color_signal(ColorSignal::srgb());
781        assert_eq!(f.planes.len(), 2);
782        assert_eq!(f.color_signal(), Some(ColorSignal::srgb()));
783
784        // Detach.
785        assert_eq!(f.take_color_signal(), Some(ColorSignal::srgb()));
786        assert_eq!(f.color_signal(), None);
787        assert_eq!(f.take_color_signal(), None);
788        assert_eq!(f.planes.len(), 1);
789    }
790
791    #[test]
792    fn set_layer_round_trips_and_keeps_image_planes_intact() {
793        let mut f = gray_frame();
794        let id = LayerIdentity::new(1).with_view_id(1).with_access_unit(9);
795        f.set_layer(id);
796        assert_eq!(f.layer(), Some(id));
797        assert_eq!(f.layer_or_base(), id);
798        assert_eq!(f.image_plane_count(), 1);
799        assert_eq!(f.planes.len(), 2);
800        assert_eq!(f.planes[1].stride, usize::MAX - 2);
801
802        f.set_layer(LayerIdentity::new(2));
803        assert_eq!(f.planes.len(), 2);
804        assert_eq!(f.layer(), Some(LayerIdentity::new(2)));
805
806        assert_eq!(f.take_layer(), Some(LayerIdentity::new(2)));
807        assert_eq!(f.layer(), None);
808        assert_eq!(f.take_layer(), None);
809        assert_eq!(f.planes.len(), 1);
810    }
811
812    #[test]
813    fn all_four_side_channels_compose_in_any_order() {
814        let sig = ColorSignal::bt709_limited();
815        let id = LayerIdentity::new(1).with_view_id(1);
816        let mut f = gray_frame()
817            .with_layer(id)
818            .with_palette(vec![1, 2, 3])
819            .with_color_signal(sig)
820            .with_significant_bits(vec![8]);
821        assert_eq!(f.planes.len(), 5);
822        assert_eq!(f.image_plane_count(), 1);
823        assert_eq!(f.palette(), Some(&[1, 2, 3][..]));
824        assert_eq!(f.significant_bits(), Some(&[8][..]));
825        assert_eq!(f.color_signal(), Some(sig));
826        assert_eq!(f.layer(), Some(id));
827
828        // Replacing one record from the middle of the run leaves the
829        // others in place.
830        f.set_color_signal(ColorSignal::srgb());
831        assert_eq!(f.planes.len(), 5);
832        assert_eq!(f.palette(), Some(&[1, 2, 3][..]));
833        assert_eq!(f.significant_bits(), Some(&[8][..]));
834        assert_eq!(f.layer(), Some(id));
835        assert_eq!(f.color_signal(), Some(ColorSignal::srgb()));
836
837        // Detaching in an arbitrary order.
838        assert_eq!(f.take_layer(), Some(id));
839        assert_eq!(f.take_palette(), Some(vec![1, 2, 3]));
840        assert_eq!(f.color_signal(), Some(ColorSignal::srgb()));
841        assert_eq!(f.significant_bits(), Some(&[8][..]));
842        assert_eq!(f.take_color_signal(), Some(ColorSignal::srgb()));
843        assert_eq!(f.take_significant_bits(), Some(vec![8]));
844        assert_eq!(f.planes.len(), 1);
845        assert_eq!(f.image_plane_count(), 1);
846    }
847
848    #[test]
849    fn huge_stride_empty_plane_is_not_a_side_channel() {
850        // Any stride in the reserved band with EMPTY data stays an
851        // (degenerate) image plane — sentinels require non-empty data.
852        let f = VideoFrame {
853            pts: None,
854            planes: vec![
855                VideoPlane {
856                    stride: 4,
857                    data: vec![0u8; 8],
858                },
859                VideoPlane {
860                    stride: usize::MAX - 1,
861                    data: Vec::new(),
862                },
863                VideoPlane {
864                    stride: usize::MAX - 2,
865                    data: Vec::new(),
866                },
867            ],
868        };
869        assert_eq!(f.color_signal(), None);
870        assert_eq!(f.layer(), None);
871        assert_eq!(f.image_plane_count(), 3);
872    }
873
874    #[test]
875    fn largest_real_stride_is_still_an_image_plane() {
876        // isize::MAX is the largest stride a one-row plane can have;
877        // it must not be classified as a side-channel tag.
878        let f = VideoFrame {
879            pts: None,
880            planes: vec![VideoPlane {
881                stride: isize::MAX as usize,
882                data: vec![0u8; 1],
883            }],
884        };
885        assert_eq!(f.image_plane_count(), 1);
886        assert_eq!(f.color_signal(), None);
887        assert_eq!(f.layer(), None);
888    }
889
890    #[test]
891    fn malformed_short_records_read_as_none() {
892        let f = VideoFrame {
893            pts: None,
894            planes: vec![
895                VideoPlane {
896                    stride: 4,
897                    data: vec![0u8; 8],
898                },
899                VideoPlane {
900                    stride: usize::MAX - 1,
901                    data: vec![1, 2],
902                },
903                VideoPlane {
904                    stride: usize::MAX - 2,
905                    data: vec![0; 5],
906                },
907            ],
908        };
909        // They are side-channel-shaped (excluded from the image planes)
910        // but decode to nothing.
911        assert_eq!(f.image_plane_count(), 1);
912        assert_eq!(f.color_signal(), None);
913        assert_eq!(f.layer(), None);
914    }
915
916    #[test]
917    fn color_signal_and_layer_survive_clone_and_frame_wrapping() {
918        let sig = ColorSignal::from_code_points(1, 13, 0, true);
919        let id = LayerIdentity::new(1).with_view_id(1).with_access_unit(3);
920        let f = gray_frame().with_color_signal(sig).with_layer(id);
921        let cloned = f.clone();
922        assert_eq!(cloned.color_signal(), Some(sig));
923        assert_eq!(cloned.layer(), Some(id));
924        let wrapped = Frame::Video(cloned);
925        assert_eq!(wrapped.pts(), Some(7));
926        if let Frame::Video(v) = wrapped {
927            assert_eq!(v.color_signal(), Some(sig));
928            assert_eq!(v.layer(), Some(id));
929        } else {
930            unreachable!("wrapped as Video above");
931        }
932    }
933
934    #[test]
935    fn palette_survives_clone_and_frame_wrapping() {
936        let f = gray_frame().with_palette(full_palette());
937        let cloned = f.clone();
938        assert_eq!(cloned.palette(), f.palette());
939
940        // Through the Frame enum, pts and palette both survive.
941        let wrapped = Frame::Video(cloned);
942        assert_eq!(wrapped.pts(), Some(7));
943        if let Frame::Video(v) = wrapped {
944            assert_eq!(v.palette().map(<[u8]>::len), Some(768));
945        } else {
946            unreachable!("wrapped as Video above");
947        }
948    }
949}