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