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}