Skip to main content

oxideav_core/
frame.rs

1//! Uncompressed audio and video frames.
2
3use crate::subtitle::SubtitleCue;
4use crate::vector::VectorFrame;
5
6/// A decoded chunk of uncompressed data: either audio samples, a video
7/// picture, or (for subtitle streams) a single styled cue.
8///
9/// Marked `#[non_exhaustive]` — consumers that match on variants must
10/// include a wildcard arm. This lets the crate add new frame kinds (data
11/// tracks, hap rops, …) without breaking downstream code.
12#[derive(Clone, Debug)]
13#[non_exhaustive]
14pub enum Frame {
15    /// Uncompressed audio samples.
16    Audio(AudioFrame),
17    /// One uncompressed video picture.
18    Video(VideoFrame),
19    /// A single subtitle cue. Timing is carried inside the cue itself
20    /// (`start_us`/`end_us`) so it's independent of container time bases,
21    /// but the enclosing pipeline/muxer can still rescale via `pts` at
22    /// the packet layer.
23    Subtitle(SubtitleCue),
24    /// A resolution-independent vector-graphics frame. Produced by
25    /// vector-format decoders (`oxideav-svg`, the vector path of
26    /// `oxideav-pdf`) and consumed by vector renderers / writers.
27    /// See [`crate::vector`] for the full primitive set.
28    Vector(VectorFrame),
29}
30
31impl Frame {
32    /// Presentation timestamp of the frame in its stream's time base
33    /// (a subtitle cue reports its `start_us`); `None` if unknown.
34    pub fn pts(&self) -> Option<i64> {
35        match self {
36            Self::Audio(a) => a.pts,
37            Self::Video(v) => v.pts,
38            Self::Subtitle(s) => Some(s.start_us),
39            Self::Vector(v) => v.pts,
40        }
41    }
42}
43
44/// Uncompressed audio frame.
45///
46/// Stream-level properties (sample format, channel count, sample rate,
47/// time base) are NOT carried per-frame — read them from the stream's
48/// [`CodecParameters`](crate::CodecParameters). Frames stay lightweight
49/// because real-time playback moves thousands per second per stream.
50///
51/// Sample layout is determined by the stream's `SampleFormat`:
52/// - Interleaved formats: `data` has one plane; samples are stored as
53///   `ch0 ch1 ... chN ch0 ch1 ... chN ...`.
54/// - Planar formats: `data` has one plane per channel.
55///
56/// Use [`SampleFormat::plane_count`](crate::SampleFormat::plane_count)
57/// with the stream's channel count to compute the expected `data.len()`.
58#[derive(Clone, Debug)]
59pub struct AudioFrame {
60    /// Number of samples *per channel* in this frame. Variable per-frame
61    /// for VBR codecs and on partial flushes.
62    pub samples: u32,
63    /// Presentation timestamp in the stream's time base; `None` if unknown.
64    pub pts: Option<i64>,
65    /// Raw sample bytes. Length matches `format.plane_count(channels)`
66    /// from the stream's `CodecParameters`.
67    pub data: Vec<Vec<u8>>,
68}
69
70/// Uncompressed video frame.
71///
72/// Stream-level properties (pixel format, width, height, time base) are
73/// NOT carried per-frame — read them from the stream's
74/// [`CodecParameters`](crate::CodecParameters). Frames stay lightweight
75/// because real-time playback moves thousands per second per stream.
76///
77/// # Side-channels
78///
79/// `VideoFrame` (like [`VideoPlane`]) is a fully-public struct built by
80/// struct literal throughout the codec crates, so per-frame metadata
81/// cannot be added as new fields without breaking every constructor.
82/// Instead, optional metadata rides in-band as *side-channel* entries at
83/// the tail of `planes`: [`VideoPlane`] values whose shape is impossible
84/// for an image plane, which makes them unambiguous. Two side-channel
85/// record kinds exist, distinguished by their `stride` tag:
86///
87/// - **Palette** — `stride == 0`, non-empty `data`. Impossible for an
88///   image plane because an image plane's `data` is `stride × rows`
89///   long, so a zero stride forces empty data. Carries the color table
90///   for palette-indexed content
91///   ([`PixelFormat::Pal8`](crate::PixelFormat::Pal8)); see
92///   [`palette`](Self::palette) / [`set_palette`](Self::set_palette).
93/// - **Per-plane significant bits** — `stride == usize::MAX`, non-empty
94///   `data`. Impossible for an image plane because `stride × rows`
95///   bytes with any non-zero row count would exceed what a `Vec` can
96///   hold. Carries mixed per-plane bit depths (e.g. 12-bit luma with
97///   10-bit chroma from a wavelet codec's custom signal range); see
98///   [`significant_bits`](Self::significant_bits) /
99///   [`set_significant_bits`](Self::set_significant_bits).
100///
101/// The two records compose: a frame can carry both at once, in either
102/// order, within the trailing run of side-channel-shaped entries. The
103/// typed accessors find each record by its `stride` tag regardless of
104/// order, and [`image_planes`](Self::image_planes) /
105/// [`image_plane_count`](Self::image_plane_count) exclude the whole
106/// trailing run. Frames without any attached side-channel are
107/// byte-for-byte identical to what they always were.
108#[derive(Clone, Debug)]
109pub struct VideoFrame {
110    /// Presentation timestamp in the stream's time base; `None` if unknown.
111    pub pts: Option<i64>,
112    /// One entry per plane (e.g., 3 for Yuv420P). Each entry is `(stride, bytes)`.
113    ///
114    /// May additionally end with side-channel entries (palette,
115    /// per-plane significant bits — see the type-level docs). Code that
116    /// wants only pixel planes should iterate
117    /// [`image_planes`](Self::image_planes) instead of this field.
118    pub planes: Vec<VideoPlane>,
119}
120
121/// `stride` tag of the per-plane significant-bits side-channel record.
122/// (The palette record's tag is `0`; see the [`VideoFrame`] docs.)
123const SIGNIFICANT_BITS_STRIDE: usize = usize::MAX;
124
125impl VideoFrame {
126    /// `true` when `plane` has a side-channel record shape: one of the
127    /// two impossible-for-an-image-plane sentinels described in the
128    /// type-level docs.
129    fn is_side_channel_entry(plane: &VideoPlane) -> bool {
130        (plane.stride == 0 || plane.stride == SIGNIFICANT_BITS_STRIDE) && !plane.data.is_empty()
131    }
132
133    /// Index of the first entry of the trailing side-channel run — equal
134    /// to the number of image planes. Scans backwards from the tail
135    /// while entries have a side-channel shape.
136    fn side_channel_run_start(&self) -> usize {
137        let mut start = self.planes.len();
138        while start > 0 && Self::is_side_channel_entry(&self.planes[start - 1]) {
139            start -= 1;
140        }
141        start
142    }
143
144    /// Index in `planes` of the side-channel record tagged with
145    /// `stride_tag`, searching the trailing side-channel run only (the
146    /// last match wins if a malformed frame carries duplicates).
147    fn side_channel_index(&self, stride_tag: usize) -> Option<usize> {
148        let start = self.side_channel_run_start();
149        self.planes[start..]
150            .iter()
151            .rposition(|p| p.stride == stride_tag)
152            .map(|i| start + i)
153    }
154
155    /// Remove every record tagged `stride_tag` from the trailing
156    /// side-channel run, returning the data of the record the readers
157    /// would have reported (the last match — consistent with
158    /// [`side_channel_index`](Self::side_channel_index)).
159    fn remove_side_channel(&mut self, stride_tag: usize) -> Option<Vec<u8>> {
160        let reported = self
161            .side_channel_index(stride_tag)
162            .map(|i| self.planes.remove(i).data);
163        while let Some(i) = self.side_channel_index(stride_tag) {
164            self.planes.remove(i);
165        }
166        reported
167    }
168
169    /// The frame's attached palette, if any.
170    ///
171    /// Returns the raw bytes of the palette side-channel (see the
172    /// type-level docs): packed 3-byte RGB entries, entry `i` at bytes
173    /// `3*i .. 3*i + 3` in R, G, B order. A full
174    /// [`Pal8`](crate::PixelFormat::Pal8) table is 256 entries
175    /// (768 bytes), but producers may attach fewer when the source
176    /// image declares a shorter table; indices at or beyond
177    /// `len / 3` are undefined by this frame and up to the consumer's
178    /// missing-entry policy (typically black).
179    pub fn palette(&self) -> Option<&[u8]> {
180        self.side_channel_index(0)
181            .map(|i| self.planes[i].data.as_slice())
182    }
183
184    /// The RGB triplet for palette entry `index`, or `None` when no
185    /// palette is attached or the attached table is too short to cover
186    /// `index`. Sugar over [`palette`](Self::palette) for per-pixel
187    /// lookups.
188    pub fn palette_rgb(&self, index: u8) -> Option<[u8; 3]> {
189        let pal = self.palette()?;
190        let at = usize::from(index) * 3;
191        let entry = pal.get(at..at + 3)?;
192        Some([entry[0], entry[1], entry[2]])
193    }
194
195    /// Attach (or replace) the frame's palette side-channel.
196    ///
197    /// `rgb` is packed 3-byte RGB entries — see
198    /// [`palette`](Self::palette) for the exact layout; pass a length
199    /// that is a multiple of 3 (up to 768 bytes for a full 256-entry
200    /// [`Pal8`](crate::PixelFormat::Pal8) table). The bytes are stored
201    /// verbatim. An empty `rgb` removes any attached palette instead
202    /// (the sentinel requires non-empty data), leaving the frame
203    /// exactly as it was before any palette was attached.
204    pub fn set_palette(&mut self, rgb: Vec<u8>) {
205        self.remove_side_channel(0);
206        if !rgb.is_empty() {
207            self.planes.push(VideoPlane {
208                stride: 0,
209                data: rgb,
210            });
211        }
212    }
213
214    /// Builder-style counterpart to [`set_palette`](Self::set_palette)
215    /// for construction chains:
216    /// `VideoFrame { pts, planes }.with_palette(rgb)`.
217    pub fn with_palette(mut self, rgb: Vec<u8>) -> Self {
218        self.set_palette(rgb);
219        self
220    }
221
222    /// Detach and return the frame's palette side-channel, if any.
223    /// Afterwards the frame carries no palette (any other side-channel
224    /// record is left in place).
225    pub fn take_palette(&mut self) -> Option<Vec<u8>> {
226        self.remove_side_channel(0)
227    }
228
229    /// The frame's attached per-plane significant-bits record, if any.
230    ///
231    /// Returns the raw bytes of the significant-bits side-channel (see
232    /// the type-level docs): byte `k` is the number of significant bits
233    /// in the samples of image plane `k`, in plane order. This lets a
234    /// producer express **mixed** per-plane depths that no single
235    /// [`PixelFormat`](crate::PixelFormat) variant can name — e.g. a
236    /// wavelet codec's custom signal range with 12-bit luma and 10-bit
237    /// chroma, stored on a `Yuv444P12Le` surface with an attached
238    /// record of `[12, 10, 10]`.
239    ///
240    /// # Semantics
241    ///
242    /// - Values are **LSB-anchored**: a plane with `b` significant bits
243    ///   keeps its sample values in the low `b` bits of each storage
244    ///   word, with the upper bits zero — the same convention as this
245    ///   crate's partial-depth formats (`Gray10Le`, `Yuv420P10Le`,
246    ///   `Gbrp12Le`, …, each documented as "uses the low N bits of a
247    ///   16-bit word"). Full-scale for `b` significant bits is
248    ///   `(1 << b) - 1`.
249    /// - Each value must satisfy `1 ≤ b ≤ 8 × storage-word-bytes` of
250    ///   the frame's pixel format (so at most 8 for byte-sized planes,
251    ///   16 for LE-16-bit-word planes). The record refines the storage
252    ///   format's *significant* depth; it never changes the storage
253    ///   word size or plane geometry.
254    /// - A record shorter than the image-plane count (or a missing
255    ///   record) leaves the uncovered planes at the pixel format's own
256    ///   documented depth. Bytes are stored verbatim; out-of-range
257    ///   values are a producer bug and consumers may clamp or reject
258    ///   them.
259    pub fn significant_bits(&self) -> Option<&[u8]> {
260        self.side_channel_index(SIGNIFICANT_BITS_STRIDE)
261            .map(|i| self.planes[i].data.as_slice())
262    }
263
264    /// The significant-bit count for image plane `plane`, or `None`
265    /// when no record is attached or the attached record is too short
266    /// to cover `plane` (fall back to the pixel format's own depth).
267    /// Sugar over [`significant_bits`](Self::significant_bits) for
268    /// per-plane lookups.
269    pub fn plane_significant_bits(&self, plane: usize) -> Option<u8> {
270        self.significant_bits()?.get(plane).copied()
271    }
272
273    /// Attach (or replace) the frame's per-plane significant-bits
274    /// side-channel.
275    ///
276    /// `bits` holds one byte per image plane, in plane order — see
277    /// [`significant_bits`](Self::significant_bits) for the exact
278    /// semantics (LSB-anchored values, `1 ≤ b ≤ storage word bits`).
279    /// The bytes are stored verbatim. An empty `bits` removes any
280    /// attached record instead (the sentinel requires non-empty data),
281    /// leaving the frame exactly as it was before any record was
282    /// attached. Any attached palette is unaffected.
283    pub fn set_significant_bits(&mut self, bits: Vec<u8>) {
284        self.remove_side_channel(SIGNIFICANT_BITS_STRIDE);
285        if !bits.is_empty() {
286            self.planes.push(VideoPlane {
287                stride: SIGNIFICANT_BITS_STRIDE,
288                data: bits,
289            });
290        }
291    }
292
293    /// Builder-style counterpart to
294    /// [`set_significant_bits`](Self::set_significant_bits) for
295    /// construction chains:
296    /// `VideoFrame { pts, planes }.with_significant_bits(bits)`.
297    pub fn with_significant_bits(mut self, bits: Vec<u8>) -> Self {
298        self.set_significant_bits(bits);
299        self
300    }
301
302    /// Detach and return the frame's per-plane significant-bits
303    /// side-channel, if any. Afterwards the frame carries no
304    /// significant-bits record (any attached palette is left in place).
305    pub fn take_significant_bits(&mut self) -> Option<Vec<u8>> {
306        self.remove_side_channel(SIGNIFICANT_BITS_STRIDE)
307    }
308
309    /// The frame's image planes — `planes` with the trailing
310    /// side-channel entries (palette, significant bits) excluded.
311    /// Prefer this over indexing `planes` directly in code that
312    /// handles side-channel-capable frames.
313    pub fn image_planes(&self) -> &[VideoPlane] {
314        &self.planes[..self.image_plane_count()]
315    }
316
317    /// Number of image planes (excludes every side-channel entry).
318    /// Matches the stream pixel format's
319    /// [`plane_count`](crate::PixelFormat::plane_count) for well-formed
320    /// frames.
321    pub fn image_plane_count(&self) -> usize {
322        self.side_channel_run_start()
323    }
324}
325
326/// One plane of a [`VideoFrame`]: row-major sample bytes plus the
327/// stride between rows.
328///
329/// An entry with non-empty `data` and a `stride` of `0` or `usize::MAX`
330/// is not an image plane: it is a side-channel record (palette and
331/// per-plane significant bits respectively) described on [`VideoFrame`]
332/// — only meaningful within the trailing run of `VideoFrame::planes`.
333#[derive(Clone, Debug)]
334pub struct VideoPlane {
335    /// Bytes per row in `data`.
336    pub stride: usize,
337    /// Raw plane bytes, `stride × rows` long (rows may carry padding
338    /// beyond the visible width).
339    pub data: Vec<u8>,
340}
341
342#[cfg(test)]
343mod tests {
344    use super::*;
345
346    fn gray_frame() -> VideoFrame {
347        // 4×2 Gray8 image plane.
348        VideoFrame {
349            pts: Some(7),
350            planes: vec![VideoPlane {
351                stride: 4,
352                data: vec![0u8; 8],
353            }],
354        }
355    }
356
357    /// A full 256-entry table where entry i is (i, !i, i^0x55).
358    fn full_palette() -> Vec<u8> {
359        (0u16..256)
360            .flat_map(|i| {
361                let i = i as u8;
362                [i, !i, i ^ 0x55]
363            })
364            .collect()
365    }
366
367    #[test]
368    fn frame_without_palette_reports_none_and_full_image_planes() {
369        let f = gray_frame();
370        assert_eq!(f.palette(), None);
371        assert_eq!(f.palette_rgb(0), None);
372        assert_eq!(f.image_plane_count(), 1);
373        assert_eq!(f.image_planes().len(), 1);
374        assert_eq!(f.image_planes()[0].stride, 4);
375    }
376
377    #[test]
378    fn set_palette_round_trips_and_keeps_image_planes_intact() {
379        let mut f = gray_frame();
380        let pal = full_palette();
381        f.set_palette(pal.clone());
382
383        assert_eq!(f.palette(), Some(pal.as_slice()));
384        // Image-plane view is unchanged by the side-channel.
385        assert_eq!(f.image_plane_count(), 1);
386        assert_eq!(f.image_planes()[0].data.len(), 8);
387        // The raw field sees the sentinel entry at the tail.
388        assert_eq!(f.planes.len(), 2);
389        assert_eq!(f.planes[1].stride, 0);
390
391        // Entry lookup: entry i is (i, !i, i ^ 0x55) by construction.
392        assert_eq!(f.palette_rgb(0), Some([0x00, 0xFF, 0x55]));
393        assert_eq!(f.palette_rgb(0xAB), Some([0xAB, 0x54, 0xFE]));
394        assert_eq!(f.palette_rgb(255), Some([0xFF, 0x00, 0xAA]));
395    }
396
397    #[test]
398    fn set_palette_replaces_existing_table() {
399        let mut f = gray_frame();
400        f.set_palette(vec![1, 2, 3]);
401        f.set_palette(vec![9, 8, 7, 6, 5, 4]);
402        // Replacement, not stacking: one image plane + one sentinel.
403        assert_eq!(f.planes.len(), 2);
404        assert_eq!(f.palette(), Some(&[9, 8, 7, 6, 5, 4][..]));
405        assert_eq!(f.palette_rgb(1), Some([6, 5, 4]));
406    }
407
408    #[test]
409    fn short_palette_covers_only_its_entries() {
410        let f = gray_frame().with_palette(vec![10, 20, 30, 40, 50, 60]);
411        assert_eq!(f.palette_rgb(0), Some([10, 20, 30]));
412        assert_eq!(f.palette_rgb(1), Some([40, 50, 60]));
413        // Beyond the table: undefined by the frame → None.
414        assert_eq!(f.palette_rgb(2), None);
415        assert_eq!(f.palette_rgb(255), None);
416    }
417
418    #[test]
419    fn empty_palette_clears_and_take_palette_detaches() {
420        let mut f = gray_frame();
421        f.set_palette(vec![1, 2, 3]);
422        assert!(f.palette().is_some());
423
424        // Empty input removes the side-channel entirely.
425        f.set_palette(Vec::new());
426        assert_eq!(f.palette(), None);
427        assert_eq!(f.planes.len(), 1);
428
429        // take_palette detaches and returns the bytes.
430        f.set_palette(vec![4, 5, 6]);
431        assert_eq!(f.take_palette(), Some(vec![4, 5, 6]));
432        assert_eq!(f.palette(), None);
433        assert_eq!(f.take_palette(), None);
434        assert_eq!(f.planes.len(), 1);
435    }
436
437    #[test]
438    fn zero_stride_empty_plane_is_not_mistaken_for_a_palette() {
439        // stride == 0 with EMPTY data is the degenerate (but
440        // contract-consistent) empty image plane, not the sentinel.
441        let f = VideoFrame {
442            pts: None,
443            planes: vec![
444                VideoPlane {
445                    stride: 4,
446                    data: vec![0u8; 8],
447                },
448                VideoPlane {
449                    stride: 0,
450                    data: Vec::new(),
451                },
452            ],
453        };
454        assert_eq!(f.palette(), None);
455        assert_eq!(f.image_plane_count(), 2);
456    }
457
458    #[test]
459    fn palette_on_frame_without_image_planes() {
460        // A palette can be attached before pixel planes exist (encoder
461        // scaffolding); the image-plane view is then empty.
462        let f = VideoFrame {
463            pts: None,
464            planes: Vec::new(),
465        }
466        .with_palette(vec![1, 2, 3]);
467        assert_eq!(f.palette(), Some(&[1, 2, 3][..]));
468        assert_eq!(f.image_plane_count(), 0);
469        assert!(f.image_planes().is_empty());
470    }
471
472    #[test]
473    fn frame_without_significant_bits_reports_none() {
474        let f = gray_frame();
475        assert_eq!(f.significant_bits(), None);
476        assert_eq!(f.plane_significant_bits(0), None);
477        assert_eq!(f.image_plane_count(), 1);
478    }
479
480    #[test]
481    fn set_significant_bits_round_trips_and_keeps_image_planes_intact() {
482        // A 12-bit-luma / 10-bit-chroma mixed-depth frame (the VC-2
483        // custom-signal-range shape that motivated the record).
484        let mut f = VideoFrame {
485            pts: Some(3),
486            planes: vec![
487                VideoPlane {
488                    stride: 8,
489                    data: vec![0u8; 16],
490                },
491                VideoPlane {
492                    stride: 8,
493                    data: vec![0u8; 16],
494                },
495                VideoPlane {
496                    stride: 8,
497                    data: vec![0u8; 16],
498                },
499            ],
500        };
501        f.set_significant_bits(vec![12, 10, 10]);
502
503        assert_eq!(f.significant_bits(), Some(&[12, 10, 10][..]));
504        assert_eq!(f.plane_significant_bits(0), Some(12));
505        assert_eq!(f.plane_significant_bits(1), Some(10));
506        assert_eq!(f.plane_significant_bits(2), Some(10));
507        // Beyond the record: fall back to the format default → None.
508        assert_eq!(f.plane_significant_bits(3), None);
509
510        // Image-plane view is unchanged by the side-channel.
511        assert_eq!(f.image_plane_count(), 3);
512        assert_eq!(f.image_planes().len(), 3);
513        // The raw field sees the sentinel entry at the tail.
514        assert_eq!(f.planes.len(), 4);
515        assert_eq!(f.planes[3].stride, usize::MAX);
516    }
517
518    #[test]
519    fn set_significant_bits_replaces_and_empty_clears_and_take_detaches() {
520        let mut f = gray_frame();
521        f.set_significant_bits(vec![7]);
522        f.set_significant_bits(vec![6]);
523        // Replacement, not stacking.
524        assert_eq!(f.planes.len(), 2);
525        assert_eq!(f.significant_bits(), Some(&[6][..]));
526
527        // Empty input removes the side-channel entirely.
528        f.set_significant_bits(Vec::new());
529        assert_eq!(f.significant_bits(), None);
530        assert_eq!(f.planes.len(), 1);
531
532        // take_significant_bits detaches and returns the bytes.
533        f.set_significant_bits(vec![5]);
534        assert_eq!(f.take_significant_bits(), Some(vec![5]));
535        assert_eq!(f.significant_bits(), None);
536        assert_eq!(f.take_significant_bits(), None);
537        assert_eq!(f.planes.len(), 1);
538    }
539
540    #[test]
541    fn palette_and_significant_bits_compose_in_either_order() {
542        // Palette first, then depths.
543        let mut f = gray_frame()
544            .with_palette(vec![1, 2, 3])
545            .with_significant_bits(vec![8]);
546        assert_eq!(f.palette(), Some(&[1, 2, 3][..]));
547        assert_eq!(f.significant_bits(), Some(&[8][..]));
548        assert_eq!(f.image_plane_count(), 1);
549        assert_eq!(f.planes.len(), 3);
550
551        // Replacing one record must not disturb the other, regardless
552        // of which currently sits at the tail.
553        f.set_palette(vec![9, 8, 7]);
554        assert_eq!(f.palette(), Some(&[9, 8, 7][..]));
555        assert_eq!(f.significant_bits(), Some(&[8][..]));
556        f.set_significant_bits(vec![7]);
557        assert_eq!(f.palette(), Some(&[9, 8, 7][..]));
558        assert_eq!(f.significant_bits(), Some(&[7][..]));
559        assert_eq!(f.image_plane_count(), 1);
560
561        // Depths first, then palette.
562        let g = gray_frame()
563            .with_significant_bits(vec![4])
564            .with_palette(full_palette());
565        assert_eq!(g.significant_bits(), Some(&[4][..]));
566        assert_eq!(g.palette_rgb(0), Some([0x00, 0xFF, 0x55]));
567        assert_eq!(g.image_plane_count(), 1);
568
569        // Detaching one leaves the other attached.
570        let mut h = g;
571        assert_eq!(h.take_significant_bits(), Some(vec![4]));
572        assert_eq!(h.significant_bits(), None);
573        assert_eq!(h.palette().map(<[u8]>::len), Some(768));
574        assert_eq!(h.take_palette().map(|p| p.len()), Some(768));
575        assert_eq!(h.planes.len(), 1);
576        assert_eq!(h.image_plane_count(), 1);
577    }
578
579    #[test]
580    fn max_stride_empty_plane_is_not_mistaken_for_significant_bits() {
581        // stride == usize::MAX with EMPTY data is not the sentinel
582        // (mirroring the palette rule: sentinels require non-empty
583        // data). Degenerate, but must not be misread as a record.
584        let f = VideoFrame {
585            pts: None,
586            planes: vec![
587                VideoPlane {
588                    stride: 4,
589                    data: vec![0u8; 8],
590                },
591                VideoPlane {
592                    stride: usize::MAX,
593                    data: Vec::new(),
594                },
595            ],
596        };
597        assert_eq!(f.significant_bits(), None);
598        assert_eq!(f.image_plane_count(), 2);
599    }
600
601    #[test]
602    fn significant_bits_on_frame_without_image_planes() {
603        // Like the palette, the record can be attached before pixel
604        // planes exist (encoder scaffolding).
605        let f = VideoFrame {
606            pts: None,
607            planes: Vec::new(),
608        }
609        .with_significant_bits(vec![12, 10, 10]);
610        assert_eq!(f.significant_bits(), Some(&[12, 10, 10][..]));
611        assert_eq!(f.image_plane_count(), 0);
612        assert!(f.image_planes().is_empty());
613    }
614
615    #[test]
616    fn side_channels_survive_clone_and_frame_wrapping() {
617        let f = gray_frame()
618            .with_palette(vec![1, 2, 3])
619            .with_significant_bits(vec![6]);
620        let cloned = f.clone();
621        assert_eq!(cloned.palette(), f.palette());
622        assert_eq!(cloned.significant_bits(), f.significant_bits());
623
624        let wrapped = Frame::Video(cloned);
625        assert_eq!(wrapped.pts(), Some(7));
626        if let Frame::Video(v) = wrapped {
627            assert_eq!(v.palette(), Some(&[1, 2, 3][..]));
628            assert_eq!(v.significant_bits(), Some(&[6][..]));
629        } else {
630            unreachable!("wrapped as Video above");
631        }
632    }
633
634    #[test]
635    fn palette_survives_clone_and_frame_wrapping() {
636        let f = gray_frame().with_palette(full_palette());
637        let cloned = f.clone();
638        assert_eq!(cloned.palette(), f.palette());
639
640        // Through the Frame enum, pts and palette both survive.
641        let wrapped = Frame::Video(cloned);
642        assert_eq!(wrapped.pts(), Some(7));
643        if let Frame::Video(v) = wrapped {
644            assert_eq!(v.palette().map(<[u8]>::len), Some(768));
645        } else {
646            unreachable!("wrapped as Video above");
647        }
648    }
649}