oxideav_mp4/sample_groups.rs
1//! Sample group muxing — write side of `sbgp` (SampleToGroupBox,
2//! ISO/IEC 14496-12 §8.9.2) and `sgpd` (SampleGroupDescriptionBox,
3//! §8.9.3).
4//!
5//! These two boxes always travel as a pair: an `sgpd` declares a table
6//! of per-group descriptive entries for a given four-byte
7//! `grouping_type` (e.g. `roll`, `rap `, `sync`, `alst`, `prol`), and
8//! an `sbgp` of the same `grouping_type` maps a track's samples into
9//! those groups via a run-length `(sample_count,
10//! group_description_index)` table. An index of `0` means "sample is a
11//! member of no group of this type"; an index ≥ `0x10001` is a
12//! movie-fragment-local reference into a fragment's own `sgpd`
13//! (§8.9.4) and is preserved verbatim — this builder does not resolve
14//! fragment-local groups (consistent with the demuxer side).
15//!
16//! The per-group entry payload inside `sgpd` is **grouping-type-specific
17//! and opaque to the container**: this crate carries it as `Vec<u8>`
18//! and never inspects it, mirroring the demuxer behaviour. A layer
19//! that knows the `grouping_type` semantics (for example a `roll`-aware
20//! codec consumer that wants to encode a signed `roll_distance`)
21//! supplies the bytes pre-serialised.
22//!
23//! # Layouts
24//!
25//! ## `sbgp` (SampleToGroupBox, §8.9.2)
26//!
27//! ```text
28//! FullBox('sbgp', version, 0)
29//! unsigned int(32) grouping_type
30//! if (version == 1)
31//! unsigned int(32) grouping_type_parameter
32//! unsigned int(32) entry_count
33//! entry_count × {
34//! unsigned int(32) sample_count
35//! unsigned int(32) group_description_index
36//! }
37//! ```
38//!
39//! Version is `1` iff `grouping_type_parameter` is `Some(_)`, else `0`.
40//!
41//! ## `sgpd` (SampleGroupDescriptionBox, §8.9.3)
42//!
43//! ```text
44//! FullBox('sgpd', version, 0)
45//! unsigned int(32) grouping_type
46//! if (version == 1) unsigned int(32) default_length
47//! if (version >= 2) unsigned int(32) default_sample_description_index
48//! unsigned int(32) entry_count
49//! entry_count × {
50//! if (version == 1 && default_length == 0)
51//! unsigned int(32) description_length
52//! SampleGroupEntry(grouping_type) // grouping-type-specific blob
53//! }
54//! ```
55//!
56//! The builder picks the version automatically per §8.9.3.2:
57//!
58//! * If `default_sample_description_index` is `Some(_)` and all entries
59//! share a common non-zero byte length → **version 2** with the
60//! `default_sample_description_index` field; the entries are emitted
61//! back-to-back without per-entry length prefixes (callers must know
62//! the entry size from the `grouping_type` semantics on the read
63//! side).
64//! * If all entries share a common non-zero byte length → **version 1**
65//! with `default_length = <that length>`; entries are packed
66//! back-to-back.
67//! * Otherwise → **version 1** with `default_length = 0`; each entry is
68//! preceded by its own `u32 description_length`.
69//!
70//! The version-0 "no per-entry length signalling" form (§8.9.3.3 NOTE,
71//! deprecated) is intentionally not emitted; the spec recommends
72//! against it precisely because it cannot be scanned, and round-tripping
73//! it against the demuxer side reduces to "one combined blob" which is
74//! not a faithful reconstruction.
75//!
76//! # Pairing
77//!
78//! For a sample-group pair to be meaningful on read, the writer should
79//! ensure the `sbgp.grouping_type` matches some `sgpd.grouping_type` in
80//! the same track's `stbl` and that the cumulative
81//! `sum(sample_count)` matches the track's sample count (§8.9.2.1).
82//! Mismatches are not rejected — a producer that intentionally writes a
83//! partial map (e.g. a stream-friendly format that only labels the
84//! first few seconds) is free to do so. The demuxer just surfaces the
85//! pair verbatim.
86
87use crate::boxes::{CSGP, SBGP, SGPD};
88
89/// One `sbgp` (SampleToGroupBox) to emit on a track.
90///
91/// `grouping_type` is the four-byte selector that pairs this box with
92/// an `sgpd` of the same type. The common types in ISO/IEC 14496-12
93/// are `roll` (audio roll-back distance), `rap ` (random-access
94/// points), `sync` (sync samples), `alst` (alternate startup
95/// sequence), `prol` (audio preroll); other groupings are defined in
96/// codec-binding specs.
97///
98/// `grouping_type_parameter` selects an alternative grouping of the
99/// same type when `Some(_)` (version 1 of `sbgp`); `None` emits
100/// version 0.
101///
102/// `entries` is the run-length table — each pair `(sample_count,
103/// group_description_index)` says "the next `sample_count` samples
104/// belong to group `group_description_index`". `group_description_index
105/// = 0` means "no group of this type" for the run; an index ≥
106/// `0x10001` is a movie-fragment-local reference and is written
107/// verbatim (the muxer does not resolve fragment-local groups). The
108/// cumulative `sum(sample_count)` should match the track's total
109/// sample count per §8.9.2.1 — the builder does not validate this.
110#[derive(Clone, Debug, Default)]
111pub struct SampleToGroup {
112 /// Four-byte grouping type (e.g. `*b"roll"`, `*b"rap "`).
113 pub grouping_type: [u8; 4],
114 /// `grouping_type_parameter` — selects an alternative grouping of
115 /// the same type. `Some(_)` emits version 1; `None` emits version 0.
116 pub grouping_type_parameter: Option<u32>,
117 /// Run-length entries `(sample_count, group_description_index)`.
118 /// Empty is a legal "zero-entry" `sbgp`.
119 pub entries: Vec<(u32, u32)>,
120}
121
122/// One `sgpd` (SampleGroupDescriptionBox) to emit on a track.
123///
124/// Pairs with an `sbgp` of the same `grouping_type`. The per-group
125/// entry payload is grouping-type-specific and opaque to the container —
126/// callers supply `entries` as already-serialised `Vec<u8>`s.
127///
128/// `default_sample_description_index` (if `Some(_)`) requests the
129/// version-2 layout: the box header carries a
130/// `default_sample_description_index` to apply to samples not mapped
131/// by any `sbgp` of this type, and entries are emitted back-to-back
132/// without per-entry length prefixes (all entries must share a common
133/// non-zero length).
134#[derive(Clone, Debug, Default)]
135pub struct SampleGroupDescription {
136 /// Four-byte grouping type matching the paired `sbgp`.
137 pub grouping_type: [u8; 4],
138 /// `default_sample_description_index` (version 2). `Some(0)` means
139 /// "no group of this type" per §8.9.3 (the default value).
140 pub default_sample_description_index: Option<u32>,
141 /// Per-group entry payloads, grouping-type-specific and opaque.
142 /// Empty is a legal "zero-entry" `sgpd`.
143 pub entries: Vec<Vec<u8>>,
144}
145
146/// Serialise an [`SampleToGroup`] into a complete `sbgp` box ready to
147/// append to a track's `stbl` body.
148///
149/// Picks version 0 if `grouping_type_parameter` is `None`, version 1
150/// otherwise. The returned slice includes the 8-byte ISO BMFF box
151/// header (`size:u32 + type='sbgp'`).
152pub fn build_sbgp(s: &SampleToGroup) -> Vec<u8> {
153 let version: u8 = if s.grouping_type_parameter.is_some() {
154 1
155 } else {
156 0
157 };
158 // header (FullBox: 1B version + 3B flags) + grouping_type(4) +
159 // (v1: grouping_type_parameter(4)) + entry_count(4) + entries*8
160 let mut body =
161 Vec::with_capacity(4 + 4 + if version == 1 { 4 } else { 0 } + 4 + s.entries.len() * 8);
162 body.push(version);
163 body.extend_from_slice(&[0, 0, 0]); // flags
164 body.extend_from_slice(&s.grouping_type);
165 if let Some(p) = s.grouping_type_parameter {
166 body.extend_from_slice(&p.to_be_bytes());
167 }
168 body.extend_from_slice(&(s.entries.len() as u32).to_be_bytes());
169 for (count, idx) in &s.entries {
170 body.extend_from_slice(&count.to_be_bytes());
171 body.extend_from_slice(&idx.to_be_bytes());
172 }
173 wrap(&SBGP, &body)
174}
175
176/// Serialise a [`SampleGroupDescription`] into a complete `sgpd` box.
177///
178/// Version is chosen automatically per §8.9.3.2:
179///
180/// * `default_sample_description_index = Some(_)` and entries share a
181/// common non-zero length → **version 2**.
182/// * Entries share a common non-zero length → **version 1** with
183/// `default_length = <that length>`.
184/// * Otherwise → **version 1** with `default_length = 0` (each entry
185/// carries its own `u32 description_length`).
186///
187/// Empty entry list → version 1 with `default_length = 0` and
188/// `entry_count = 0`.
189pub fn build_sgpd(s: &SampleGroupDescription) -> Vec<u8> {
190 // Decide common-length / per-entry-length / v2 layout.
191 let common_len = entries_common_length(&s.entries);
192 let want_v2 = s.default_sample_description_index.is_some() && common_len.is_some();
193 let (version, default_length): (u8, u32) = if want_v2 {
194 (2, 0)
195 } else if let Some(cl) = common_len {
196 (1, cl as u32)
197 } else {
198 (1, 0)
199 };
200
201 let entries_payload_len = match (version, default_length) {
202 (1, 0) => s.entries.iter().map(|e| 4 + e.len()).sum::<usize>(),
203 (1, dl) => s.entries.len() * dl as usize,
204 (2, _) => s.entries.iter().map(|e| e.len()).sum::<usize>(),
205 _ => unreachable!(),
206 };
207
208 // header sizing: 4 (full) + 4 (grouping_type)
209 // + 4 (default_length if v1)
210 // + 4 (default_sample_description_index if v2)
211 // + 4 (entry_count)
212 // + entries
213 let extra = match version {
214 1 => 4,
215 2 => 4,
216 _ => unreachable!(),
217 };
218 let mut body = Vec::with_capacity(4 + 4 + extra + 4 + entries_payload_len);
219 body.push(version);
220 body.extend_from_slice(&[0, 0, 0]); // flags
221 body.extend_from_slice(&s.grouping_type);
222 match version {
223 1 => body.extend_from_slice(&default_length.to_be_bytes()),
224 2 => body.extend_from_slice(
225 &s.default_sample_description_index
226 .unwrap_or(0)
227 .to_be_bytes(),
228 ),
229 _ => unreachable!(),
230 }
231 body.extend_from_slice(&(s.entries.len() as u32).to_be_bytes());
232
233 match (version, default_length) {
234 (1, 0) => {
235 for e in &s.entries {
236 body.extend_from_slice(&(e.len() as u32).to_be_bytes());
237 body.extend_from_slice(e);
238 }
239 }
240 (1, _) => {
241 for e in &s.entries {
242 body.extend_from_slice(e);
243 }
244 }
245 (2, _) => {
246 for e in &s.entries {
247 body.extend_from_slice(e);
248 }
249 }
250 _ => unreachable!(),
251 }
252 wrap(&SGPD, &body)
253}
254
255/// `Some(len)` iff all entries share the same non-zero byte length.
256/// `None` for an empty list (no fixed length to claim) or for a list
257/// with heterogeneous lengths.
258fn entries_common_length(entries: &[Vec<u8>]) -> Option<usize> {
259 let first = entries.first()?;
260 if first.is_empty() {
261 return None;
262 }
263 let len = first.len();
264 if entries.iter().all(|e| e.len() == len) {
265 Some(len)
266 } else {
267 None
268 }
269}
270
271/// One pattern of a [`CompactSampleToGroup`] (`csgp`, §8.9.5): a run of
272/// `indices.len()` per-sample `sample_group_description_index` values
273/// (the *pattern*), replicated across `sample_count` consecutive groups
274/// of that length. The pattern therefore covers `sample_count *
275/// indices.len()` samples in total.
276///
277/// `pattern_length` is implicit from `indices.len()` (the builder writes
278/// it), matching the read side which reconstructs it the same way.
279#[derive(Clone, Debug, Default)]
280pub struct CompactSampleToGroupPattern {
281 /// `sample_count[i]` — number of consecutive groups (each
282 /// `indices.len()` samples long) that replay this pattern.
283 pub sample_count: u32,
284 /// `sample_group_description_index[i][1..=pattern_length]` — one
285 /// index per sample of the pattern. `0` means "member of no group of
286 /// this type"; in a `traf` the index's most-significant bit (for the
287 /// chosen field width) distinguishes a fragment-local description
288 /// (set) from a global one (clear). The raw value is written
289 /// verbatim — the builder does not synthesise the fragment-local bit.
290 pub indices: Vec<u32>,
291}
292
293/// One `csgp` (CompactSampleToGroupBox, ISO/IEC 14496-12:2020 §8.9.5) to
294/// emit on a track — the compact alternative to [`SampleToGroup`].
295///
296/// Where `sbgp` emits one `(sample_count, group_description_index)` pair
297/// per run, `csgp` groups samples into a small set of **patterns** that
298/// are each replicated across the track: pattern `i` replays its
299/// `indices` for `sample_count[i]` consecutive groups. A track whose
300/// per-sample group membership is periodic (the common reason to pick the
301/// compact form) shrinks dramatically — the index run is bit-packed at
302/// the narrowest width that fits.
303///
304/// Pairs with an `sgpd` of the same `grouping_type` exactly like `sbgp`.
305#[derive(Clone, Debug, Default)]
306pub struct CompactSampleToGroup {
307 /// Four-byte grouping type matching the paired `sgpd`.
308 pub grouping_type: [u8; 4],
309 /// `grouping_type_parameter` — selects an alternative grouping of the
310 /// same type. `Some(_)` sets the flag-layout presence bit and emits
311 /// the optional `u32` field; `None` omits it.
312 pub grouping_type_parameter: Option<u32>,
313 /// `index_msb_indicates_fragment_local_description` — flag-layout
314 /// **bit 7** (§8.9.5). Set it (only legal when emitting into a `traf`)
315 /// to declare that the most-significant bit of each index is a
316 /// fragment-local-vs-global `sgpd` source selector. The builder writes
317 /// each index value verbatim; it does not synthesise the selector bit,
318 /// so a caller that sets this must pre-set the high bit on indices that
319 /// should reference the fragment-local `sgpd`. Defaults to `false`
320 /// (`stbl` form / no MSB special-casing).
321 pub index_msb_indicates_fragment_local_description: bool,
322 /// The repeating index patterns, in emit order.
323 pub patterns: Vec<CompactSampleToGroupPattern>,
324}
325
326/// Map a maximum field value to the narrowest §8.9.5 2-bit size code.
327///
328/// The width function is `width = 4 << code` (code 0→4, 1→8, 2→16,
329/// 3→32 bits). The smallest code whose width holds `max` is chosen so a
330/// `csgp` is as compact as the data allows; an all-zero column still
331/// picks code 0 (4 bits), the spec's minimum width.
332fn size_code_for(max: u32) -> u8 {
333 if max <= 0xF {
334 0
335 } else if max <= 0xFF {
336 1
337 } else if max <= 0xFFFF {
338 2
339 } else {
340 3
341 }
342}
343
344/// MSB-first big-endian bit writer — the inverse of the demuxer's
345/// `BitCursor`. Accumulates bits into a byte buffer; the final partial
346/// byte is zero-padded on `finish` so the box body stays byte-aligned
347/// (§8.9.5 leaves no defined meaning for trailing pad bits, matching the
348/// read side which simply stops once every declared field is consumed).
349struct BitWriter {
350 out: Vec<u8>,
351 /// Bits already filled in the in-progress final byte (0..=7); `0`
352 /// means the buffer is byte-aligned and a fresh byte starts the next
353 /// write.
354 bits_filled: u8,
355}
356
357impl BitWriter {
358 fn new() -> Self {
359 BitWriter {
360 out: Vec::new(),
361 bits_filled: 0,
362 }
363 }
364
365 /// Write the low `n` bits (0..=32) of `value`, MSB-first. `n == 0`
366 /// writes nothing.
367 fn write(&mut self, value: u32, n: u32) {
368 debug_assert!(n <= 32);
369 for i in (0..n).rev() {
370 let bit = ((value >> i) & 1) as u8;
371 if self.bits_filled == 0 {
372 self.out.push(0);
373 }
374 let last = self.out.len() - 1;
375 self.out[last] |= bit << (7 - self.bits_filled);
376 self.bits_filled = (self.bits_filled + 1) & 7;
377 }
378 }
379
380 /// Zero-pad the final partial byte and return the buffer.
381 fn finish(self) -> Vec<u8> {
382 self.out
383 }
384}
385
386/// Serialise a [`CompactSampleToGroup`] into a complete `csgp` box ready
387/// to append to a track's `stbl` (or a `traf`) body.
388///
389/// The three bit-field width codes (for `sample_group_description_index`,
390/// `sample_count`, and `pattern_length`) are chosen automatically as the
391/// narrowest §8.9.5 widths that hold every value present, and packed into
392/// the `FullBox.flags` field together with the
393/// `grouping_type_parameter_present` bit:
394///
395/// ```text
396/// index_size_code = flags[0..1] (2 bits)
397/// count_size_code = flags[2..3] (2 bits)
398/// pattern_size_code = flags[4..5] (2 bits)
399/// grouping_type_parameter_present = flags[6] (1 bit)
400/// index_msb_indicates_fragment_local_description = flags[7] (1 bit)
401/// ```
402///
403/// The fixed-width header fields (`grouping_type`, optional
404/// `grouping_type_parameter`, `pattern_count`) are byte-aligned `u32`s;
405/// from `pattern_count` onward the `(pattern_length, sample_count)` array
406/// and then the flattened index run are bit-packed MSB-first at the
407/// chosen widths (no byte alignment between fields), the exact inverse of
408/// `demux::parse_csgp`. The returned slice includes the 8-byte ISO BMFF
409/// box header (`size:u32 + type='csgp'`).
410pub fn build_csgp(c: &CompactSampleToGroup) -> Vec<u8> {
411 let max_pattern_length = c
412 .patterns
413 .iter()
414 .map(|p| p.indices.len() as u32)
415 .max()
416 .unwrap_or(0);
417 let max_sample_count = c.patterns.iter().map(|p| p.sample_count).max().unwrap_or(0);
418 let max_index = c
419 .patterns
420 .iter()
421 .flat_map(|p| p.indices.iter().copied())
422 .max()
423 .unwrap_or(0);
424
425 let mut pattern_size_code = size_code_for(max_pattern_length);
426 let mut count_size_code = size_code_for(max_sample_count);
427 let index_size_code = size_code_for(max_index);
428 // §8.9.5 constraint: `pattern_size_code` and `count_size_code` must
429 // agree on whether the 4-bit width (code 0) is used — a 4-bit/non-4-bit
430 // mix is an invalid file. When exactly one of the two would pick code 0
431 // (4 bits) while the other needs a wider field, promote the 4-bit one to
432 // code 1 (8 bits) so the emitted box satisfies the constraint. (Both at
433 // code 0, or both ≥ code 1, already agree and are left untouched.)
434 if (pattern_size_code == 0) != (count_size_code == 0) {
435 pattern_size_code = pattern_size_code.max(1);
436 count_size_code = count_size_code.max(1);
437 }
438 let pattern_w = 4u32 << pattern_size_code;
439 let count_w = 4u32 << count_size_code;
440 let index_w = 4u32 << index_size_code;
441
442 let gtpp = c.grouping_type_parameter.is_some();
443 // FullBox flags: index[0..1], count[2..3], pattern[4..5], gtpp[6],
444 // index_msb_indicates_fragment_local_description[7] (§8.9.5).
445 let flags: u32 = (index_size_code as u32)
446 | ((count_size_code as u32) << 2)
447 | ((pattern_size_code as u32) << 4)
448 | (if gtpp { 1 } else { 0 } << 6)
449 | (if c.index_msb_indicates_fragment_local_description {
450 1
451 } else {
452 0
453 } << 7);
454
455 let mut body = Vec::new();
456 body.push(0); // version 0
457 body.extend_from_slice(&flags.to_be_bytes()[1..]); // 24-bit flags
458 body.extend_from_slice(&c.grouping_type);
459 if let Some(p) = c.grouping_type_parameter {
460 body.extend_from_slice(&p.to_be_bytes());
461 }
462 body.extend_from_slice(&(c.patterns.len() as u32).to_be_bytes());
463
464 // Bit-packed region: all (pattern_length, sample_count) pairs first,
465 // then every pattern's index run flattened in order.
466 let mut bits = BitWriter::new();
467 for p in &c.patterns {
468 bits.write(p.indices.len() as u32, pattern_w);
469 bits.write(p.sample_count, count_w);
470 }
471 for p in &c.patterns {
472 for &idx in &p.indices {
473 bits.write(idx, index_w);
474 }
475 }
476 body.extend_from_slice(&bits.finish());
477
478 wrap(&CSGP, &body)
479}
480
481fn wrap(kind: &[u8; 4], body: &[u8]) -> Vec<u8> {
482 let total = (8 + body.len()) as u32;
483 let mut out = Vec::with_capacity(total as usize);
484 out.extend_from_slice(&total.to_be_bytes());
485 out.extend_from_slice(kind);
486 out.extend_from_slice(body);
487 out
488}
489
490#[cfg(test)]
491mod tests {
492 use super::*;
493
494 #[test]
495 fn sbgp_v0_two_runs_byte_exact() {
496 let s = SampleToGroup {
497 grouping_type: *b"roll",
498 grouping_type_parameter: None,
499 entries: vec![(10, 1), (5, 0)],
500 };
501 let b = build_sbgp(&s);
502 // header: size = 8 + 4 (full) + 4 (gt) + 4 (count) + 2*8 = 36
503 assert_eq!(b.len(), 36);
504 assert_eq!(&b[0..4], &36u32.to_be_bytes());
505 assert_eq!(&b[4..8], b"sbgp");
506 assert_eq!(&b[8..12], &[0, 0, 0, 0]); // version 0 + flags
507 assert_eq!(&b[12..16], b"roll");
508 assert_eq!(&b[16..20], &2u32.to_be_bytes()); // entry_count
509 assert_eq!(&b[20..24], &10u32.to_be_bytes());
510 assert_eq!(&b[24..28], &1u32.to_be_bytes());
511 assert_eq!(&b[28..32], &5u32.to_be_bytes());
512 assert_eq!(&b[32..36], &0u32.to_be_bytes());
513 }
514
515 #[test]
516 fn sbgp_v1_with_parameter_byte_exact() {
517 let s = SampleToGroup {
518 grouping_type: *b"rap ",
519 grouping_type_parameter: Some(7),
520 entries: vec![(3, 2)],
521 };
522 let b = build_sbgp(&s);
523 // size = 8 + 4 + 4 + 4 + 4 + 8 = 32
524 assert_eq!(b.len(), 32);
525 assert_eq!(&b[8..12], &[1, 0, 0, 0]); // version 1 + flags
526 assert_eq!(&b[12..16], b"rap ");
527 assert_eq!(&b[16..20], &7u32.to_be_bytes()); // grouping_type_parameter
528 assert_eq!(&b[20..24], &1u32.to_be_bytes()); // entry_count
529 assert_eq!(&b[24..28], &3u32.to_be_bytes());
530 assert_eq!(&b[28..32], &2u32.to_be_bytes());
531 }
532
533 #[test]
534 fn sbgp_zero_entries_legal() {
535 let s = SampleToGroup {
536 grouping_type: *b"roll",
537 grouping_type_parameter: None,
538 entries: vec![],
539 };
540 let b = build_sbgp(&s);
541 // size = 8 + 4 + 4 + 4 = 20
542 assert_eq!(b.len(), 20);
543 assert_eq!(&b[16..20], &0u32.to_be_bytes()); // entry_count = 0
544 }
545
546 #[test]
547 fn sbgp_fragment_local_index_preserved() {
548 let s = SampleToGroup {
549 grouping_type: *b"sync",
550 grouping_type_parameter: None,
551 entries: vec![(1, 0x1_0001)],
552 };
553 let b = build_sbgp(&s);
554 assert_eq!(&b[24..28], &0x1_0001u32.to_be_bytes());
555 }
556
557 #[test]
558 fn sgpd_v1_fixed_length_when_entries_share_size() {
559 let s = SampleGroupDescription {
560 grouping_type: *b"roll",
561 default_sample_description_index: None,
562 entries: vec![vec![0xFF, 0xFB], vec![0x00, 0x05]],
563 };
564 let b = build_sgpd(&s);
565 // size = 8 + 4 + 4 + 4 (default_length) + 4 (entry_count) + 2*2 = 28
566 assert_eq!(b.len(), 28);
567 assert_eq!(&b[8..12], &[1, 0, 0, 0]); // version 1
568 assert_eq!(&b[12..16], b"roll");
569 assert_eq!(&b[16..20], &2u32.to_be_bytes()); // default_length = 2
570 assert_eq!(&b[20..24], &2u32.to_be_bytes()); // entry_count
571 assert_eq!(&b[24..26], &[0xFF, 0xFB]);
572 assert_eq!(&b[26..28], &[0x00, 0x05]);
573 }
574
575 #[test]
576 fn sgpd_v1_variable_length_when_entries_differ() {
577 let s = SampleGroupDescription {
578 grouping_type: *b"prol",
579 default_sample_description_index: None,
580 entries: vec![vec![0xAA, 0xBB, 0xCC], vec![0xDD]],
581 };
582 let b = build_sgpd(&s);
583 // size = 8 + 4 + 4 + 4 (default_length = 0) + 4 (entry_count)
584 // + (4 + 3) + (4 + 1) = 36
585 assert_eq!(b.len(), 36);
586 assert_eq!(&b[8..12], &[1, 0, 0, 0]); // version 1
587 assert_eq!(&b[16..20], &0u32.to_be_bytes()); // default_length = 0
588 assert_eq!(&b[20..24], &2u32.to_be_bytes()); // entry_count
589 assert_eq!(&b[24..28], &3u32.to_be_bytes()); // description_length 0
590 assert_eq!(&b[28..31], &[0xAA, 0xBB, 0xCC]);
591 assert_eq!(&b[31..35], &1u32.to_be_bytes()); // description_length 1
592 assert_eq!(&b[35..36], &[0xDD]);
593 }
594
595 #[test]
596 fn sgpd_v2_when_default_sample_description_index_set() {
597 let s = SampleGroupDescription {
598 grouping_type: *b"alst",
599 default_sample_description_index: Some(3),
600 entries: vec![vec![0x01, 0x02], vec![0x03, 0x04]],
601 };
602 let b = build_sgpd(&s);
603 // size = 8 + 4 + 4 + 4 (default_sample_description_index) + 4 (entry_count) + 4 (entries) = 28
604 assert_eq!(b.len(), 28);
605 assert_eq!(&b[8..12], &[2, 0, 0, 0]); // version 2
606 assert_eq!(&b[12..16], b"alst");
607 assert_eq!(&b[16..20], &3u32.to_be_bytes()); // default_sample_description_index
608 assert_eq!(&b[20..24], &2u32.to_be_bytes()); // entry_count
609 assert_eq!(&b[24..26], &[0x01, 0x02]);
610 assert_eq!(&b[26..28], &[0x03, 0x04]);
611 }
612
613 #[test]
614 fn sgpd_v2_falls_back_to_v1_when_entries_differ() {
615 // default_sample_description_index is set, but entries don't
616 // share a length → can't use v2's no-length form. Falls back
617 // to v1 with per-entry length.
618 let s = SampleGroupDescription {
619 grouping_type: *b"alst",
620 default_sample_description_index: Some(3),
621 entries: vec![vec![0x01], vec![0x02, 0x03]],
622 };
623 let b = build_sgpd(&s);
624 assert_eq!(b[8], 1); // version 1, not 2
625 assert_eq!(&b[16..20], &0u32.to_be_bytes()); // default_length = 0
626 }
627
628 #[test]
629 fn sgpd_empty_entries_legal() {
630 let s = SampleGroupDescription {
631 grouping_type: *b"roll",
632 default_sample_description_index: None,
633 entries: vec![],
634 };
635 let b = build_sgpd(&s);
636 // size = 8 + 4 + 4 + 4 (default_length=0) + 4 (entry_count=0) = 24
637 assert_eq!(b.len(), 24);
638 assert_eq!(&b[8..12], &[1, 0, 0, 0]); // version 1
639 assert_eq!(&b[20..24], &0u32.to_be_bytes()); // entry_count = 0
640 }
641
642 #[test]
643 fn entries_common_length_helper() {
644 assert_eq!(entries_common_length(&[]), None);
645 assert_eq!(entries_common_length(&[vec![]]), None);
646 assert_eq!(entries_common_length(&[vec![1, 2]]), Some(2));
647 assert_eq!(entries_common_length(&[vec![1, 2], vec![3, 4]]), Some(2));
648 assert_eq!(entries_common_length(&[vec![1], vec![2, 3]]), None);
649 }
650
651 #[test]
652 fn size_code_for_picks_narrowest_width() {
653 assert_eq!(size_code_for(0), 0); // all-zero still 4 bits
654 assert_eq!(size_code_for(0xF), 0); // 4-bit boundary
655 assert_eq!(size_code_for(0x10), 1); // needs 8 bits
656 assert_eq!(size_code_for(0xFF), 1);
657 assert_eq!(size_code_for(0x100), 2); // needs 16 bits
658 assert_eq!(size_code_for(0xFFFF), 2);
659 assert_eq!(size_code_for(0x1_0000), 3); // needs 32 bits
660 assert_eq!(size_code_for(u32::MAX), 3);
661 }
662
663 #[test]
664 fn bit_writer_msb_first() {
665 let mut w = BitWriter::new();
666 // 0b101 then 0b01 → 0b10101 padded to 0b1010_1000 = 0xA8.
667 w.write(0b101, 3);
668 w.write(0b01, 2);
669 let out = w.finish();
670 assert_eq!(out, vec![0xA8]);
671 }
672
673 #[test]
674 fn csgp_4bit_widths_byte_exact() {
675 // One pattern: pattern_length = 2, sample_count = 3, indices
676 // [1, 2]. All values ≤ 0xF → every size code is 0 (4-bit width),
677 // so flags = 0 and the bit-packed region is:
678 // pattern_length=2 (4b) sample_count=3 (4b) → 0x23
679 // idx[0]=1 (4b) idx[1]=2 (4b) → 0x12
680 let c = CompactSampleToGroup {
681 grouping_type: *b"roll",
682 grouping_type_parameter: None,
683 index_msb_indicates_fragment_local_description: false,
684 patterns: vec![CompactSampleToGroupPattern {
685 sample_count: 3,
686 indices: vec![1, 2],
687 }],
688 };
689 let b = build_csgp(&c);
690 // size = 8 (hdr) + 4 (full) + 4 (gt) + 4 (pattern_count) + 2 (bits)
691 assert_eq!(b.len(), 22);
692 assert_eq!(&b[0..4], &22u32.to_be_bytes());
693 assert_eq!(&b[4..8], b"csgp");
694 assert_eq!(&b[8..12], &[0, 0, 0, 0]); // version 0 + flags 0
695 assert_eq!(&b[12..16], b"roll");
696 assert_eq!(&b[16..20], &1u32.to_be_bytes()); // pattern_count
697 assert_eq!(b[20], 0x23); // pattern_length=2 | sample_count=3
698 assert_eq!(b[21], 0x12); // idx 1 | idx 2
699 }
700
701 #[test]
702 fn csgp_flags_encode_size_codes() {
703 // Force distinct codes where pattern and count already AGREE on
704 // not-4-bit (both ≥ code 1), so no §8.9.5 promotion applies:
705 // index needs 8 bits (0x10 → code 1), count needs 16 bits (0x100
706 // → code 2), pattern_length is 0x20 → needs 8 bits (code 1).
707 let c = CompactSampleToGroup {
708 grouping_type: *b"sync",
709 grouping_type_parameter: None,
710 index_msb_indicates_fragment_local_description: false,
711 patterns: vec![CompactSampleToGroupPattern {
712 sample_count: 0x100,
713 // 0x20 entries → pattern_length = 0x20 needs 8-bit (code 1).
714 indices: vec![0x10; 0x20],
715 }],
716 };
717 let b = build_csgp(&c);
718 // flags: index_size_code=1 (bits0..1), count_size_code=2
719 // (bits2..3), pattern_size_code=1 (bits4..5), gtpp=0.
720 let flags = u32::from_be_bytes([0, b[9], b[10], b[11]]);
721 assert_eq!(flags & 0x3, 1); // index_size_code
722 assert_eq!((flags >> 2) & 0x3, 2); // count_size_code
723 assert_eq!((flags >> 4) & 0x3, 1); // pattern_size_code (not 4-bit)
724 assert_eq!((flags >> 6) & 0x1, 0); // gtpp
725 }
726
727 /// §8.9.5 constraint: `pattern_size_code` and `count_size_code` must
728 /// agree on 4-bit usage. Here `pattern_length` fits in 4 bits (max 2)
729 /// but `sample_count` needs 8 bits (0x100 → 16-bit, actually code 2),
730 /// so the builder must NOT leave `pattern_size_code` at 0 (4-bit) — it
731 /// promotes it off 4-bit so the emitted box is valid, and the result
732 /// re-parses through the now-strict reader.
733 #[test]
734 fn csgp_builder_avoids_mixed_4bit_width() {
735 let c = CompactSampleToGroup {
736 grouping_type: *b"roll",
737 grouping_type_parameter: None,
738 index_msb_indicates_fragment_local_description: false,
739 patterns: vec![CompactSampleToGroupPattern {
740 sample_count: 0x100, // needs 16-bit (code 2)
741 indices: vec![1, 2], // pattern_length = 2 fits 4-bit
742 }],
743 };
744 let b = build_csgp(&c);
745 let flags = u32::from_be_bytes([0, b[9], b[10], b[11]]);
746 let pattern_code = (flags >> 4) & 0x3;
747 let count_code = (flags >> 2) & 0x3;
748 // count needs ≥ code 2; pattern must NOT stay at 4-bit (code 0).
749 assert!(count_code >= 2);
750 assert_ne!(pattern_code, 0, "pattern_size_code must not stay 4-bit");
751 // And the produced box round-trips through the strict parser.
752 let parsed = crate::demux::parse_csgp_box(&b[8..]).expect("must re-parse");
753 assert_eq!(parsed.patterns.len(), 1);
754 assert_eq!(parsed.patterns[0].sample_count, 0x100);
755 assert_eq!(parsed.patterns[0].indices, vec![1, 2]);
756 }
757
758 #[test]
759 fn csgp_with_grouping_type_parameter_sets_presence_bit() {
760 let c = CompactSampleToGroup {
761 grouping_type: *b"rap ",
762 grouping_type_parameter: Some(7),
763 index_msb_indicates_fragment_local_description: false,
764 patterns: vec![CompactSampleToGroupPattern {
765 sample_count: 1,
766 indices: vec![1],
767 }],
768 };
769 let b = build_csgp(&c);
770 let flags = u32::from_be_bytes([0, b[9], b[10], b[11]]);
771 assert_eq!((flags >> 6) & 0x1, 1); // gtpp bit set
772 assert_eq!(&b[12..16], b"rap ");
773 assert_eq!(&b[16..20], &7u32.to_be_bytes()); // grouping_type_parameter
774 assert_eq!(&b[20..24], &1u32.to_be_bytes()); // pattern_count
775 }
776
777 #[test]
778 fn csgp_empty_patterns_legal() {
779 let c = CompactSampleToGroup {
780 grouping_type: *b"roll",
781 grouping_type_parameter: None,
782 index_msb_indicates_fragment_local_description: false,
783 patterns: vec![],
784 };
785 let b = build_csgp(&c);
786 // size = 8 + 4 + 4 + 4 = 20, pattern_count = 0, no bit region.
787 assert_eq!(b.len(), 20);
788 assert_eq!(&b[16..20], &0u32.to_be_bytes());
789 }
790
791 /// Build → parse round-trip through the canonical demuxer reader for
792 /// a spread of widths, the grouping_type_parameter, multiple
793 /// patterns, and the fragment-local high bit.
794 #[test]
795 fn csgp_roundtrip_through_parser() {
796 let cases = vec![
797 CompactSampleToGroup {
798 grouping_type: *b"roll",
799 grouping_type_parameter: None,
800 index_msb_indicates_fragment_local_description: false,
801 patterns: vec![CompactSampleToGroupPattern {
802 sample_count: 3,
803 indices: vec![1, 2],
804 }],
805 },
806 CompactSampleToGroup {
807 grouping_type: *b"rap ",
808 grouping_type_parameter: Some(42),
809 index_msb_indicates_fragment_local_description: false,
810 patterns: vec![
811 CompactSampleToGroupPattern {
812 sample_count: 0x100,
813 indices: vec![0x10, 0, 0xFF],
814 },
815 CompactSampleToGroupPattern {
816 sample_count: 1,
817 indices: vec![0x1_0000],
818 },
819 ],
820 },
821 CompactSampleToGroup {
822 grouping_type: *b"sync",
823 grouping_type_parameter: None,
824 index_msb_indicates_fragment_local_description: false,
825 // fragment-local high bit set on an 8-bit-wide index.
826 patterns: vec![CompactSampleToGroupPattern {
827 sample_count: 5,
828 indices: vec![0x8000_0001],
829 }],
830 },
831 ];
832 for c in &cases {
833 let bytes = build_csgp(c);
834 // Strip the 8-byte box header — parse_csgp_box takes the body.
835 let parsed = crate::demux::parse_csgp_box(&bytes[8..]).unwrap();
836 assert_eq!(parsed.grouping_type, c.grouping_type);
837 assert_eq!(parsed.grouping_type_parameter, c.grouping_type_parameter);
838 assert_eq!(parsed.patterns.len(), c.patterns.len());
839 for (pp, cp) in parsed.patterns.iter().zip(&c.patterns) {
840 assert_eq!(pp.sample_count, cp.sample_count);
841 assert_eq!(pp.indices, cp.indices);
842 }
843 }
844 }
845}