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}