Skip to main content

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}