oxideav-mp4 0.0.9

Pure-Rust MP4 / ISO base media file container for oxideav
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
# oxideav-mp4

Pure-Rust **MP4 / ISO Base Media File Format** container — demuxer
(probe + sample-table expansion + seek) and muxer (moov-at-end by
default, optional faststart rewrite). Three brand presets share one
implementation: `mp4`, `mov` (QuickTime), and `ismv` (Smooth Streaming
ftyp, non-fragmented layout). Zero C dependencies.

Part of the [oxideav](https://github.com/OxideAV/oxideav-workspace)
framework but usable standalone.

## Installation

```toml
[dependencies]
oxideav-core = "0.1"
oxideav-codec = "0.1"
oxideav-container = "0.1"
oxideav-mp4 = "0.0"
```

## Quick use

### Demux an MP4 and feed packets into a codec

```rust
use oxideav_codec::CodecRegistry;
use oxideav_container::ContainerRegistry;

let mut codecs = CodecRegistry::new();
let mut containers = ContainerRegistry::new();
oxideav_mp4::register(&mut containers);
// ... register whichever codecs you care about (aac, flac, h264, mjpeg, ...)

let input: Box<dyn oxideav_container::ReadSeek> =
    Box::new(std::fs::File::open("clip.mp4")?);
let mut dmx = containers.open("mp4", input)?;

// Sample entries are resolved to concrete codec ids. For `mp4a`/`mp4v`
// tracks the esds `objectTypeIndication` is honoured, so MP3-in-mp4
// comes out as "mp3", MPEG-1 video as "mpeg1video", AAC as "aac", etc.
let stream = &dmx.streams()[0];
let mut dec = codecs.make_decoder(&stream.params)?;

loop {
    match dmx.next_packet() {
        Ok(pkt) => {
            dec.send_packet(&pkt)?;
            while let Ok(frame) = dec.receive_frame() {
                // ... use frame ...
                let _ = frame;
            }
        }
        Err(oxideav_core::Error::Eof) => break,
        Err(e) => return Err(e.into()),
    }
}
# Ok::<(), Box<dyn std::error::Error>>(())
```

### Mux packets into an MP4

```rust
use oxideav_container::WriteSeek;

let f = std::fs::File::create("out.mp4")?;
let ws: Box<dyn WriteSeek> = Box::new(f);
let mut mux = oxideav_mp4::muxer::open(ws, &streams)?;
mux.write_header()?;
for pkt in packets { mux.write_packet(&pkt)?; }
mux.write_trailer()?;
# Ok::<(), Box<dyn std::error::Error>>(())
```

### Faststart (moov-at-front) layout

```rust
use oxideav_mp4::{BrandPreset, Mp4MuxerOptions};

let opts = Mp4MuxerOptions {
    brand: BrandPreset::Mp4,
    faststart: true,
    ..Mp4MuxerOptions::default()
};
let mut mux = oxideav_mp4::muxer::open_with_options(ws, &streams, opts)?;
```

In faststart mode the muxer buffers mdat in memory and writes
`[ftyp][moov][mdat]` at `write_trailer` time, patching chunk offsets so
the file is streamable from the first byte.

## Scope

### Demuxer

Sample-entry FourCCs resolve to these codec ids:

| FourCC                   | Codec id                                |
|--------------------------|-----------------------------------------|
| `mp4a`                   | `aac` (default); esds OTI refines to `mp3` (0x69/0x6B), `ac3` (0xA5), `eac3` (0xA6), `dts` (0xA9) |
| `mp4v`                   | `mpeg4video` (default); esds OTI refines to `mpeg1video`, `mpeg2video`, `h264`, `h265`, `mjpeg` |
| `alac`                   | `alac`                                  |
| `fLaC` / `flac`          | `flac`                                  |
| `Opus` / `opus`          | `opus`                                  |
| `ac-3` / `AC-3`          | `ac3` (Dolby Digital, ETSI TS 102 366 Annex F) |
| `ec-3` / `EC-3`          | `eac3` (Dolby Digital Plus, ETSI TS 102 366 Annex G) |
| `dtsc` / `dtsh` / `dtsl` / `dtse` | `dts` (DTS Coherent Acoustics / DTS-HD HR / DTS-HD MA / DTS Express, ETSI TS 102 114) |
| `ulaw` / `alaw`          | `pcm_mulaw` / `pcm_alaw` (G.711)        |
| `avc1` / `avc3`          | `h264`                                  |
| `hvc1` / `hev1`          | `h265`                                  |
| `vp08`                   | `vp8`                                   |
| `vp09`                   | `vp9`                                   |
| `av01`                   | `av1`                                   |
| `jpeg` / `mjpa` / `mjpb` | `mjpeg`                                 |
| `s263` / `h263`          | `h263`                                  |
| `lpcm` / `sowt` / `twos` | `pcm_s16le` (endianness of `twos` is not re-swapped) |
| `tx3g`                   | `mov_text` (3GPP TS 26.245 timed text — "movtext") |
| `text`                   | `text` (QuickTime plain text)            |
| `wvtt`                   | `webvtt` (W3C WebVTT-in-ISOBMFF)         |
| `stpp`                   | `ttml` (XML / TTML subtitle)             |
| `sbtt` / `stxt`          | `sbtt` / `stxt` (BMFF §12.5–6 text)      |
| `c608` / `c708`          | `eia_608` / `eia_708` (closed captions)  |
| `encv` / `enca` / `enct` / `encs` | original FourCC recovered from `sinf/frma`; `params.options["protection_scheme"]` carries the `schm.scheme_type` (e.g. `cenc`, `cbcs`) |
| any other                | `mp4:<fourcc>` — callers can register their own decoder |

- Codec-specific config records (`avcC`, `hvcC`, `av1C`, `vpcC`,
  `dfLa`, `dOps`, `dac3`, `dec3`, esds DSI) are forwarded as
  `extradata`.
- Sample-table expansion: `stts`, `stsc`, `stsz`/`stz2`, `stco`/`co64`,
  `stss`. `next_packet` serves samples in file-offset order.
- Fragmented MP4 (DASH / HLS / Smooth Streaming / CMAF): `mvex/trex`
  per-track defaults plus zero or more trailing `moof`+`mdat` pairs
  (`mfhd`, `traf`, `tfhd`, `tfdt`, `trun`) are stitched onto the
  initial sample table. `default-base-is-moof`, per-sample
  size/duration/flags/composition-time-offset overrides, and
  `tfhd.base_data_offset` are all honoured. `styp`, `sidx`, and `mfra`
  segment-index boxes are skipped (segment-precision seek hint
  consumption is a follow-up).
- Movie extends header (ISO/IEC 14496-12 §8.8.2, `mehd`): a sealed
  fragmented file's `mvex/mehd` MovieExtendsHeaderBox carries the
  overall presentation `fragment_duration` (in the movie timescale,
  per §8.8.2.3) — including all `moof` fragments — that an authoring
  step can write once the fragments are laid down. Both versions are
  read (v0 32-bit, v1 64-bit widened to `u64`). When present and
  non-zero, the value drives `Demuxer::duration_micros` and takes
  precedence over `mvhd.duration` — sealed fragmented files commonly
  carry `mvhd.duration = 0` (the moov has no resident samples that
  contribute to a non-fragment duration) and `mehd` is then the only
  authoritative total. The raw value (in the movie timescale) is also
  surfaced verbatim via `Demuxer::metadata()` as the
  `mehd_fragment_duration` key, for tooling wanting the untranslated
  number (e.g. CMAF live-edge probes confirming a producer's sealed
  total against their own running tally). Absent `mehd`, the key is
  not emitted and `duration_micros` falls back to `mvhd.duration` as
  before.
- Seek: `seek_to(stream, pts)` lands on the nearest sync-sample ≤ pts
  (or the first keyframe of the stream if none qualify).
- Metadata: 3GPP `udta` boxes (`titl`/`auth`/…) and iTunes-style
  `meta`/`ilst` are surfaced via `Demuxer::metadata()`.
- Extended language tag (ISO/IEC 14496-12 §8.4.6, `elng`): a track's
  `mdia/elng` ExtendedLanguageBox carries a NULL-terminated BCP 47
  (RFC 4646) tag richer than `mdhd`'s packed 3-char ISO 639-2 code
  (region / script / variant subtags). When present it is surfaced on
  `params.options["language"]` (e.g. `en-US`, `zh-Hant-HK`,
  `es-419`) and, per §8.4.6.1, overrides the `mdhd` language. Absent
  `elng`, the option is omitted (callers fall back to `mdhd`).
- Handler-type recognition (ISO/IEC 14496-12 §8.4.3): `soun`  Audio, `vide` → Video, `subt` / `sbtl` / `text` → Subtitle,
  `meta` → Data. Subtitle sample entries (`tx3g`, `text`, `wvtt`,
  `stpp`, `sbtt`, `stxt`, `c608`, `c708`) come out with
  `MediaType::Subtitle` and the per-codec id from the table above;
  their post-preamble payload (BMFF strings / tx3g header / vttC)
  is preserved verbatim in `params.extradata` for downstream
  renderers.
- Protected sample-entry unwrap (ISO/IEC 14496-12 §8.12): when the
  outer FourCC is `encv` / `enca` / `enct` / `encs`, the demuxer
  walks the inner `sinf` to recover the original codec FourCC
  from `frma` and the protection scheme from `schm`. The stream
  surfaces the un-transformed codec id so downstream decoders can
  be set up normally; `params.options["protection_scheme"]`
  carries the four-char scheme type (e.g. `cenc`, `cbcs`) so
  callers know packet payloads are still ciphertext.
- CENC metadata parsing (ISO/IEC 23001-7:2016): the three boxes
  that carry encryption framing on top of the §8.12 envelope are
  parsed structurally and surfaced to callers; the AES-128
  CTR / CBC decryption step itself lives in the `cenc_cipher`
  module (key acquisition stays with the caller).
  - `tenc` (§8.2 TrackEncryptionBox) — discovered inside
    `sinf/schi`. v0 captures `default_isProtected`,
    `default_Per_Sample_IV_Size`, and `default_KID`; v1 adds the
    `default_crypt_byte_block` / `default_skip_byte_block` pattern
    pair (for `cens` / `cbcs` schemes) and the
    `default_constant_IV` used when `isProtected==1 && IV_size==0`.
    Surfaced on `params.options` as `cenc_default_kid` (lowercase
    hex), `cenc_default_is_protected`, `cenc_default_iv_size`,
    `cenc_tenc_version`, and (v1 only) `cenc_default_crypt_byte_block`
    / `cenc_default_skip_byte_block` / `cenc_default_constant_iv`.
  - `pssh` (§8.1 ProtectionSystemSpecificHeaderBox) — collected at
    moov level. Each entry captures the 16-byte SystemID UUID,
    optional v1 KID list, and the DRM-system-specific opaque
    `Data` blob. Surfaced via `Demuxer::metadata()` as `pssh_<n>`
    keys with value `"<system_id_hex> <kid_count> <data_len>"`;
    structured records are reachable through the public
    `cenc::PsshBox` type for callers that downcast.
  - moof-level `pssh` (§8.1.1 — "Container: Movie (`moov`) or
    Movie Fragment (`moof`)"). pssh boxes that live directly inside
    a `moof` are collected per fragment and keyed by the enclosing
    `mfhd.sequence_number` so a downstream DRM layer can honour the
    §8.1.1 reader rule "examine all Protection System Specific
    Header boxes in the Movie Box and in the Movie Fragment Box
    associated with the sample (but not those in other Movie
    Fragment Boxes)". Surfaced via `Demuxer::metadata()` as
    `moof_pssh_<n>` keys with value
    `"systemid=<hex> seq=<mfhd_seq> kids=<n> data=<len>"`;
    structured records are reachable through the public
    `demux::MoofPsshRecord` type (one record per box in moof-walk
    order, `version` 0 + v1 with KID list both supported). A
    malformed pssh inside a moof is dropped without aborting the
    fragment, mirroring the moov-level recovery policy.
  - `senc` (§7.2 SampleEncryptionBox) — collected from every `traf`
    whose matching track carried a `tenc` default (so the
    per-sample IV width is recoverable per §7.2.3). Captures
    `flags`, the per-sample `InitializationVector`, and (when
    `UseSubSampleEncryption` is set) the
    `{BytesOfClearData, BytesOfProtectedData}` subsample map.
    Surfaced via `Demuxer::metadata()` as `senc_<n>` keys with
    value `"track=<idx> seq=<mfhd_seq> samples=<n>
    flags=0x<hex>"`; structured records are reachable through the
    public `cenc::SencBox` type.

  The standalone parsers (`cenc::parse_tenc` / `cenc::parse_pssh`
  / `cenc::parse_senc`) are public for callers that already have
  the box body in hand from another path (e.g. a non-MP4 carrier
  of CENC framing).
  - Typed scheme decision router (§4.2 + §10). The four `scheme_type`
    FourCCs defined in ISO/IEC 23001-7:2016 §10 — `cenc` (AES-CTR
    full / NAL-subsample), `cbc1` (AES-CBC full / NAL-subsample),
    `cens` (AES-CTR pattern subsample), `cbcs` (AES-CBC pattern
    subsample) — are exposed as the typed `cenc::CencScheme` enum.
    The two binary axes a downstream decryptor dispatches on —
    cipher mode (CTR vs CBC, `cenc::CipherMode`) and pattern-encryption
    flag — are surfaced via `CencScheme::cipher_mode()` and
    `CencScheme::uses_pattern_encryption()`. A scheme value bundled
    with the parsed `tenc` becomes a `cenc::CencSchemeDecision`    the typed routing slip a future AES layer can pattern-match
    against — built via `CencSchemeDecision::new(scheme, tenc)`
    with structural validation only (the scheme's
    `required_tenc_version()` must match `tenc.version`; pattern
    schemes must carry a non-zero `(crypt_byte_block,
    skip_byte_block)` pair per §9.6). The track-default IV-supply
    discipline (§9.1) — per-sample 8/16-byte IV vs constant IV vs
    no IV when `isProtected == 0` — is recovered via
    `CencSchemeDecision::iv_supply()` as the typed `cenc::IvSupply`
    enum. The bundle is a static dispatch contract built from
    container-side bytes only; the AES execution layer that
    consumes it is `cenc_cipher::decrypt_sample_in_place`, with
    key material (looked up by KID from the named `pssh.SystemID`
    system) supplied by the caller.
    Unknown `scheme_type` FourCCs are preserved verbatim through
    `CencScheme::Unknown([u8; 4])` so a caller carrying a private
    DRM dialect can still route on its own table.
  - Typed `CencSampleEncryptionInformationGroupEntry` parser (§6).
    A `seig` sample-group entry overrides the track-default `tenc`
    parameters for a group of samples — `(crypt_byte_block,
    skip_byte_block, isProtected, Per_Sample_IV_Size, KID,
    constant_IV?)` — and is the spec's mechanism for mixing
    encrypted and unencrypted samples in one track, or for
    rotating keys per scene. `cenc::parse_seig(body)` decodes one
    entry's payload (the opaque blob the existing `sgpd`
    `grouping_type = *b"seig"` surface already preserves) into the
    typed `cenc::SeigEntry`, mirroring the validation `parse_tenc`
    applies (constant-IV size ∈ {8, 16}, per-sample IV size ∈ {0,
    8, 16}). `SeigEntry::iv_supply()` and `::uses_pattern_encryption()`
    answer the same routing questions as the track-default
    accessors but at the per-group resolution. §6's
    "clients SHALL ignore additional bytes after the fields
    defined" rule is honoured — trailing bytes from a future
    edition do not fail the parse.
  - Per-sample cipher walker (§9.4 / §9.5 / §9.6).
    `cenc::plan_sample_cipher(decision, subsamples, sample_len)`
    consumes a parsed `CencSchemeDecision` plus the per-sample
    subsample list (from `parse_senc`) and returns the ordered
    `Vec<CipherStep>` partition of the sample plaintext into
    contiguous `(offset, len, kind, iv_restart)` runs an AES layer
    iterates: `Clear` runs pass through verbatim and `Encrypted` runs
    feed the cipher mode picked by `CencSchemeDecision::cipher_mode`
    (CTR vs CBC). The walker bakes in every §10-registered
    structural rule — full-sample CTR (§9.4.2) keeps the trailing
    partial block encrypted, full-sample CBC (§9.4.3) and `§9.7`
    whole-block leave it in the clear, subsample non-pattern is
    `Clear(BytesOfClearData) + Encrypted(BytesOfProtectedData)` per
    subsample with a continuous chain across them (§9.5.1),
    subsample pattern walks `crypt_byte_block * 16` encrypted +
    `skip_byte_block * 16` clear with the trailing partial
    `crypt_byte_block` reset to clear (§9.6), and the `cbcs` IV
    restart per subsample (§9.5.1) lights up only on the first
    `Encrypted` step of each subsample for `cbcs` (not `cenc` /
    `cbc1` / `cens`). §9.5.1 invariants are enforced —
    `Σ (clear+protected) == sample_len`, no both-zero subsample,
    no subsample running past sample_len. `IvSupply::None` and
    `CencScheme::Unknown` are rejected so a caller routes through
    its own "no cipher" / private-dialect path explicitly.
  - AES-128 CTR / CBC cipher driver (§9.3 / §9.4–§9.7, module
    `cenc_cipher`). `cenc_cipher::decrypt_sample_in_place(decision,
    key, per_sample_iv, subsamples, data)` resolves the IV
    discipline from the `CencSchemeDecision` (per-sample IV
    length-checked against `tenc`; constant IV pulled from
    `tenc.default_constant_IV`; supplying both is a §9.2 error),
    builds the `CipherStep` plan, and executes it in place against
    AES-128 (the `aes` / `ctr` / `cbc` primitive crates) — all
    four §10 schemes decrypt. The §9.1 IV expansion (8-byte IV →
    bytes 0..8, bytes 8..16 zero) is `cenc_cipher::expand_iv`; the
    §9.3 counter is the low 64 bits big-endian, starting at the
    expanded-IV value and wrapping to zero without carrying into
    bytes 0..8. §9.5.1 continuity is honoured: one continuous
    keystream / cipher chain spans the concatenated protected
    runs (a partial CTR block ending one subsample's protected
    range continues in the next; `cens` skip blocks consume no
    keystream; the `cbc1` chain crosses clear gaps), while `cbcs`
    reseeds its chain from the constant IV at each subsample's
    `iv_restart` step and chains across skip runs within the
    subsample. Encrypted CBC runs that are not a multiple of 16
    bytes are rejected (§9.4.3 / §10.2 — partial blocks are never
    CBC-encrypted). The step-level engine is public as
    `cenc_cipher::decrypt_steps_in_place(mode, key, iv, steps,
    data)` for `seig`-overridden sample groups where the caller
    assembles its own plan/IV pairing. Tested against a FIPS-197
    known-answer AES block anchor, first-principles ECB-built §9.3
    keystreams (including the 64-bit counter wrap), and synthetic
    known-key round-trips for every scheme × subsample × pattern
    shape (cens 2:1 counter continuity, cbcs 1:9 stride with
    chain-over-skip + per-subsample restart, §9.7 whole-block
    audio).
- Track references (ISO/IEC 14496-12 §8.3.3, `tref`): each typed
  `TrackReferenceTypeBox` inside `trak/tref` is parsed and the
  resulting `(reference_type → track_IDs)` pairs are surfaced on
  `params.options` as `tref_<type>` keys whose value is a
  space-separated list of referenced `track_ID`s (e.g.
  `tref_chap = "3"`, `tref_subt = "10 11"`, `tref_cdsc = "2"`).
  Useful for wiring subtitle→video (`subt`), chapter (`chap`),
  content description (`cdsc`), font (`font`), hint (`hint`),
  depth / parallax auxiliary video (`vdep` / `vplx`), and hint
  dependency (`hind`) relationships. `track_ID = 0` entries
  (spec-prohibited) are dropped.
- Track groups (ISO/IEC 14496-12 §8.3.4, `trgr`): each
  `TrackGroupTypeBox` child inside `trak/trgr` is parsed as a
  `(track_group_type, track_group_id)` pair — the child's FourCC
  names the grouping (`msrc` is the spec-named example, for
  multi-source presentations: tracks sharing a `track_group_id`
  under the `msrc` type originate from the same source, e.g. one
  participant's audio + video in a recorded video call) and the
  32-bit `track_group_id` identifies the group within the file.
  Two tracks carrying the same `(type, id)` pair belong to the
  same group (§8.3.4.3); track groups are **not** dependency
  relationships (use `tref` for those). Each child is surfaced on
  `params.options` as `trgr_<n>` (0-based encounter index) with
  value `"<type> <id>"`. The spec leaves the door open for two
  children of the same type on one track (unlike `tref` which
  caps at one per `reference_type`) so both encounter-order copies
  are preserved. Bytes trailing the 32-bit id inside a child are
  reserved for per-`track_group_type` extensions in derived specs
  and silently ignored at this layer; a child whose `version` field
  is non-zero (§8.3.4.2 pins it to 0) is also silently skipped, so
  unknown extensions never mis-parse. Absent `trgr`, none of the
  keys are emitted.
- Track Kind (ISO/IEC 14496-12 §8.10.4, `kind`): each `KindBox`
  inside a track-level `udta` is parsed as a `(schemeURI, value)`
  pair (both NULL-terminated C strings; an absent value is allowed,
  meaning the URI alone identifies the kind). Multiple `kind` boxes
  per track are supported — the spec explicitly allows several
  schemes co-labelling the same track (e.g. one DASH role
  `urn:mpeg:dash:role:2011 main` plus one iTunes-scheme tag). Each
  entry is surfaced on `params.options` as `kind_<n>` (0-based
  encounter index); the value is the URI alone when no name
  follows, or `"URI value"` (space-separated, mirroring the
  `tref_<type>` convention) when both are present.
- Track copyright (ISO/IEC 14496-12 §8.10.2, `cprt`): each
  `CopyrightBox` inside a track-level `udta` is parsed as a typed
  record carrying a 3-letter ISO 639-2/T language code and a decoded
  notice string. The 16-bit packed language word (§8.10.2.3 — bit 15
  pad + three 5-bit characters at `(ASCII - 0x60)`) is decoded back
  to lowercase ASCII; the notice C string is UTF-8 by default and
  UTF-16BE when it opens with the byte-order mark (0xFE 0xFF), with
  the trailing NUL terminator stripped. Multiple `cprt` boxes per
  track are supported — the spec's "Quantity: Zero or more" covers
  the multilingual case (one box per language). Each record is
  surfaced on `params.options` as the pair `copyright_<n>` (the
  notice; key omitted when the notice is empty) and `copyright_<n>_lang`
  (the 3-letter tag; emitted even with an empty notice so callers can
  tell "empty notice in `eng`" from "no `cprt`"). A box body shorter
  than the 6-byte FullBox + language word minimum or an unknown
  FullBox version (§8.10.2.2 pins it to 0) is silently dropped — the
  box is informational and a malformed entry never aborts the open.
  Distinct from the 3GPP TS 26.244 `cprt` shape lumped under the
  generic file-wide metadata channel, where the language code is
  not surfaced separately.
- Track selection (ISO/IEC 14496-12 §8.10.3, `tsel`): the optional
  `TrackSelectionBox` inside a track-level `udta` carries two
  media-selection signals — `switch_group` (signed 32-bit, §8.10.3.4)
  groups tracks that are interchangeable *during* playback (e.g.
  bitrate-adaptive renditions of the same stream) within the
  alternate group declared on `tkhd` (§8.3.2), and `attribute_list`
  (§8.10.3.5) is a list of FourCC tags drawn from the descriptive
  set (`tesc` = temporal scalability, `fgsc` / `cgsc` = SNR
  scalability, `spsc` = spatial, `resc` = region-of-interest, `vwsc`
  = view) and differentiating set (`bitr` = bitrate, `cdec` = codec,
  `lang` = language, …) describing what the track offers. Surfaced
  on `params.options` as `tsel_switch_group` (the signed integer,
  emitted even when zero so callers can tell present-but-zero from
  absent) and `tsel_attributes` (space-separated FourCCs, mirroring
  the `tref_<type>` convention; omitted when the list is empty). A
  body shorter than the 8-byte FullBox + `switch_group` minimum or
  an unknown FullBox version is silently dropped — `tsel` is
  informational and a malformed entry never aborts the open.
  Container preserves the raw FourCCs; consumer mapping (e.g. a
  player selecting by language vs. bitrate within an alternate
  group) is delegated. Absent `tsel`, no keys are emitted.
- Sub tracks (ISO/IEC 14496-12 §8.14, `strk` / `stri` / `strd` /
  `stsg`): a track-level `udta` may carry zero or more `strk` Sub
  Track boxes (§8.14.3), each assigning *part* of the track to the
  same alternate / switch groups that whole tracks use (§8.3.2 /
  §8.10.3) — the mechanism for selecting among layered-codec
  alternatives (SVC / MVC temporal, spatial, SNR, or view layers)
  that don't map onto track boundaries (§8.14.1). Each `strk`'s
  mandatory `stri` (Sub Track Information, §8.14.4) carries
  `switch_group` (signed 16-bit), `alternate_group` (signed 16-bit),
  `sub_track_ID` (32-bit), and an `attribute_list[]` of FourCCs from
  §8.14.4.3's descriptive (`tesc` / `fgsc` / `cgsc` / `spsc` /
  `resc` / `vwsc`) and differentiating (`bitr` / `frar` / `nvws` /
  …) vocabulary. The mandatory `strd` (Sub Track Definition,
  §8.14.5) is walked one level for its `stsg` (Sub Track Sample
  Group, §8.14.6) children — each names a `grouping_type` shared with
  the track's `sbgp` / `sgpd` (§8.9) plus the `sgpd` description
  indices that make up the sub track. Each sub track is surfaced on
  `params.options` as `subtrack_<n>` (0-based encounter index) with
  value `"id=<sub_track_ID> switch=<switch_group> alt=<alternate_group>
  [attrs=<fourcc...>] [stsg=<grouping_type>:<idx>,<idx>;...]"` — the
  three fixed fields are always present (0 is meaningful — it
  distinguishes present-but-unassigned from a missing field), the
  `attrs=` and `stsg=` blocks are omitted when empty, and FourCCs
  with non-printable bytes fall back to 8-digit hex (matching the
  `tsel_attributes` convention). A `strk` missing its mandatory
  `stri` contributes no sub track; an unknown FullBox version on
  `stri` / `stsg`, a too-short `stri`, or a `stsg` whose declared
  `item_count` overruns the body are rejected — the boxes are
  informational and a malformed entry never aborts the open. Absent
  `strk`, no keys are emitted.
- Composition-to-decode (ISO/IEC 14496-12 §8.6.1.4, `cslg`): a
  track's `stbl/cslg` CompositionToDecodeBox documents the
  composition↔decode timeline relationship implied by a signed
  (v1) `ctts` — the DTS shift that guarantees `CTS ≥ DTS`, the
  least / greatest composition offsets, and the composition
  start / end times. Both v0 (32-bit) and v1 (64-bit) layouts are
  read (widened to `i64`). The five fields are surfaced on
  `params.options` as `cslg_composition_to_dts_shift`,
  `cslg_least_decode_to_display_delta`,
  `cslg_greatest_decode_to_display_delta`,
  `cslg_composition_start_time`, and `cslg_composition_end_time`
  (decimal strings in the media timescale; `composition_end_time = 0`
  means "unknown" per §8.6.1.4.3). Absent `cslg`, none of the keys
  are emitted.
- Shadow sync samples (ISO/IEC 14496-12 §8.6.3, `stsh`): a track's
  `stbl/stsh` ShadowSyncSampleBox is an optional seek hint — a table
  of `(shadowed_sample_number, sync_sample_number)` pairs naming a
  sync sample (key frame) that can be decoded in place of a non-sync
  sample when seeking to or before it. Each pair is surfaced on
  `params.options` as `stsh_<n>` (0-based encounter index) with value
  `"shadowed sync"` (both 1-based sample numbers, space-separated). The
  table is purely a seek optimisation — it is ignored in normal
  forward play and a track decodes correctly without it. Absent
  `stsh`, none of the keys are emitted.
- Sample degradation priority (ISO/IEC 14496-12 §8.5.3, `stdp`): a
  track's `stbl/stdp` DegradationPriorityBox is an optional per-sample
  table of 16-bit `priority` values, one per sample (the
  `sample_count` is implicit from `stsz` / `stz2`, mirroring `sdtp`).
  The exact meaning and value range of `priority` are owned by
  derived specifications (§8.5.3.1 / §8.5.3.3 "Specifications derived
  from this define the exact meaning and acceptable range of the
  `priority` field"), so the container preserves the raw u16 without
  interpreting it. The demuxer surfaces a small summary on
  `params.options` rather than the per-sample table (which would
  dominate the options map for a typical track) — four keys per
  track-with-`stdp`: `stdp_count` (total entries), `stdp_min` /
  `stdp_max` (value spread), and `stdp_sum` (a u64 — a u16 priority ×
  2^32 samples fits comfortably; consumers compute the mean from
  `sum / count`). A renderer dropping samples under bitrate / CPU
  pressure consults the carrying spec for the priority ordering.
  Absent `stdp`, none of the keys are emitted.
- Sample padding bits (ISO/IEC 14496-12 §8.7.6, `padb`): a track's
  `stbl/padb` PaddingBitsBox is an optional table recording, per
  sample, how many bits at the tail of the sample's last byte are
  padding (a value in `0..=7`). On the wire two samples share a byte:
  each nibble is `bit(1) reserved=0; bit(3) pad`. Unlike `sdtp` /
  `stdp` the box declares its own `sample_count` (independent of
  `stsz` / `stz2`), so the table is self-contained — the trailing
  unused nibble when `sample_count` is odd is dropped during parse.
  The reserved high bit of each nibble (required zero by §8.7.6.2) is
  masked off so a producer slip on the reserved bit does not corrupt
  the surfaced pad count. The demuxer exposes a summary on
  `params.options` rather than the per-sample table — four keys per
  track-with-`padb`: `padb_count` (total entries), `padb_max` (largest
  pad count seen, `0..=7`), `padb_nonzero_count` (samples whose pad
  count is non-zero — the count that actually matters to a bitstream
  consumer; an all-zero `padb` means the track is byte-aligned), and
  `padb_hist` (eight-bucket histogram `n0:n1:n2:n3:n4:n5:n6:n7` where
  `nK` is the number of samples whose pad count is `K`, decimal). The
  histogram fully captures the value distribution in one short string;
  consumers can recover any aggregate without scanning. Absent `padb`,
  none of the keys are emitted.
- Sample dependency hints (ISO/IEC 14496-12 §8.6.4, `sdtp`): a
  track's `stbl/sdtp` SampleDependencyTypeBox is a per-sample table
  of four 2-bit fields — `is_leading`, `sample_depends_on`,
  `sample_is_depended_on`, `sample_has_redundancy` — packed one
  byte per sample (the `sample_count` is implicit from `stsz` /
  `stz2`). The table feeds trick-mode playback (drop disposable
  samples on fast-forward) and refines random-access roll-forward
  (a sample marked `sample_depends_on = 2` is an I-picture without
  needing the `stss` to mark it). The raw per-sample 2-bit values
  are decoded and stored on the track; the demuxer surfaces a small
  summary on `params.options` as five keys — `sdtp_count`,
  `sdtp_leading_count` (samples with `is_leading ∈ {1, 3}`),
  `sdtp_independent_count` (samples with `sample_depends_on = 2`),
  `sdtp_disposable_count` (samples with `sample_is_depended_on = 2`),
  and `sdtp_redundant_count` (samples with `sample_has_redundancy =
  1`). Absent `sdtp`, none of the keys are emitted (the demuxer
  falls back to `stss` for keyframe detection, as before).
- Multiple sample descriptions (ISO/IEC 14496-12 §8.5.2, `stsd`):
  every `SampleEntry` in the box is now parsed, not just the first.
  Entry `[0]` still drives active decode dispatch (its FourCC is the
  stream's resolved `codec`), and its codec-specific tail (audio /
  video preamble + child config boxes) is parsed as before. The
  additional entries — present when a track switches description
  mid-stream via a per-chunk `stsc.sample_description_index ≥ 2` or a
  fragment's `tfhd` / `trex` `sample_description_index` — are recorded
  with their FourCC and §8.5.2.2 `data_reference_index`. When
  `entry_count > 1` the demuxer surfaces them on `params.options`:
  `stsd_count` (total entries) plus `stsd_<n>` for each entry, where
  `<n>` is the **1-based** index matching the spec's
  `sample_description_index` (so a `stsc` / `tfhd` index value `k`
  looks up directly as `stsd_<k>`). Each value is
  `<fourcc> dref=<data_reference_index>`. A single-description track
  (the overwhelming common case) emits none of these keys — the active
  `codec` already carries that information. A declared `entry_count`
  larger than the bytes actually present stops at what could be read
  rather than failing the box or inventing entries.
- Data references (ISO/IEC 14496-12 §8.7.1–2, `dinf` / `dref` /
  `url ` / `urn `): a track's `minf/dinf/dref` DataReferenceBox is the
  table of media-data locations that every sample entry's 1-based
  `data_reference_index` (§8.5.2.2) indexes into — it declares whether a
  description's samples are self-contained in this file or stored in an
  external resource. Each `DataEntryBox` is a `url ` (with the
  §8.7.2.3 self-contained flag, or a NULL-terminated `location` URL when
  external) or a `urn ` (a NULL-terminated `name` plus an optional
  `location`); a non-`url `/`urn ` child is non-conforming and dropped.
  The overwhelmingly common single self-contained `url ` case is
  surfaced compactly on `params.options` as `dref_self_contained = "true"`
  with no per-entry keys (a one-lookup "no external resources" check).
  When the table has more than one entry, or any entry is *not*
  self-contained (a split-source track), the full table surfaces:
  `dref_count` (total entries), `dref_self_contained` ("true" only when
  *every* entry is self-contained), and `dref_<n>` (1-based, matching
  `data_reference_index`) = `<kind> self=<true|false>[ name=<urn>]
  [ loc=<url>]`. A forged `entry_count` cannot over-allocate (the
  smallest child is its 8-byte box header) and a child overrunning the
  body ends the walk at what was read; an unknown FullBox version is
  tolerated. A malformed `dref` is dropped without failing the `minf`
  parse (the file still demuxes against the single-source default).
  Absent / unparseable `dref`, none of the keys are emitted.
- Sample groups (ISO/IEC 14496-12 §8.9, `sbgp` + `sgpd`): a track's
  `stbl/sbgp` (SampleToGroupBox §8.9.2) run-length map and
  `stbl/sgpd` (SampleGroupDescriptionBox §8.9.3) per-group entries
  are parsed. Several of each are accumulated — one pair per
  `grouping_type` (`roll`, `rap `, `sync`, `alst`, `prol`, …). Each
  `sbgp` is surfaced on `params.options` as `sbgp_<n>` (0-based
  encounter index): the grouping type, an optional `param=<P>` (v1
  `grouping_type_parameter`), then space-separated `count:index`
  run-length pairs (`group_description_index` 0 = "no group of this
  type"; an index ≥ 0x10001 is a movie-fragment-local reference per
  §8.9.4, kept verbatim — the demuxer does not resolve fragment-local
  groups). Each `sgpd` is surfaced as `sgpd_<n>`: the grouping type,
  an optional `default=<D>` (v2 `default_sample_description_index`),
  then the per-group entry payloads as lowercase hex. Entry sizing
  honours §8.9.3.2 (v1 fixed `default_length`, v1 per-entry
  `description_length`, or the v0 deprecated no-length-signalling case
  captured as one combined blob). The entry payloads are
  grouping-type-specific and **not** interpreted by the container —
  they are surfaced verbatim for a layer that knows the
  `grouping_type` semantics. Absent both boxes, none of the keys are
  emitted.
- Compact sample-to-group (ISO/IEC 14496-12:2020 §8.9.5, `csgp`): the
  compact alternative to `sbgp` is parsed. The `FullBox.flags` field is
  overloaded to carry three 2-bit width selectors (`index_size_code`,
  `count_size_code`, `pattern_size_code`, each mapped to a field width
  via `width = 4 << code` → 4/8/16/32 bits) plus a
  `grouping_type_parameter_present` bit. The body's variable-width
  fields are bit-packed (no byte alignment between 4-/8-bit fields) and
  read MSB-first: a `pattern_count`-long array of
  `(pattern_length, sample_count)` followed by the per-pattern
  `sample_group_description_index` run. Each `csgp` is accumulated and
  surfaced on `params.options` as `csgp_<n>`: the grouping type, an
  optional `param=<P>`, then one `count*idx0,idx1,…` token per pattern
  (`count` = `sample_count`, the index list = the pattern's per-sample
  indices; 0 = "no group", fragment-local high-bit kept verbatim). It
  shares `grouping_type` with the matching `sgpd_<m>`. Absent `csgp`,
  no keys are emitted.
- Sub-sample information (ISO/IEC 14496-12 §8.7.7, `subs`): a track's
  `stbl/subs` SubSampleInformationBox is an optional sparse table
  describing how selected samples decompose into smaller,
  semantically-meaningful sub-samples (e.g. NAL units / parameter sets
  for H.264 per ISO/IEC 14496-15, or arbitrary segment boundaries for
  codecs that define their own sub-sample contract). Each entry carries
  a sample-number delta from the previous entry, then a list of
  `(subsample_size, subsample_priority, discardable,
  codec_specific_parameters)` rows. Version 0 stores `subsample_size`
  as 16-bit; version 1 widens it to 32-bit (both layouts are read and
  normalised to `u32`). `flags` distinguishes co-resident `subs` boxes
  with different per-codec semantics (§8.7.7.1). The container
  preserves the carried codec's interpretation of
  `subsample_priority` / `discardable` / `codec_specific_parameters`
  verbatim — those small ints are opaque at this layer. Each `subs`
  encountered on a track is surfaced on `params.options` as
  `subs_<n>` (0-based encounter index); the value starts with
  `"v<version> flags=<f>"` and is followed by one space-separated
  `delta=<d>[:size,priority,discardable,csp[;...]]` block per entry
  (decimal for everything except `csp`, which is lowercase 8-digit
  hex). The trailing colon and per-sub-sample list are omitted when an
  entry has `subsample_count = 0`. Absent `subs`, no keys are emitted.
- Sample auxiliary information sizes + offsets (ISO/IEC 14496-12
  §8.7.8–9, `saiz` + `saio`): a track's `stbl/saiz` + `stbl/saio` pair
  documents where per-sample auxiliary-information records live in the
  file, keyed by `(aux_info_type, aux_info_type_parameter)`. The most
  common consumer is CENC: when `senc` is absent, per-sample IVs +
  subsample maps (ISO/IEC 23001-7) are carried as an
  auxiliary-information stream of type `cenc` / `cbc1` / `cens` /
  `cbcs`, and the `saiz`+`saio` pair points to the bytes in the mdat.
  Both versions (v0 32-bit / v1 64-bit `saio` offsets) are read; the
  optional `aux_info_type` block (gated by `flags & 1`) is captured
  when present and surfaced as `None`/`None` otherwise so callers can
  apply §8.7.8.3's implied-value rule against the sample-entry FourCC
  / sinf scheme themselves. `saiz.default_sample_info_size` non-zero
  collapses the per-sample table; v1 `saio` offsets that exceed 32
  bits round-trip as `u64`. Each `saiz` / `saio` encountered on a
  track is surfaced on `params.options` as `saiz_<n>` / `saio_<n>`
  (0-based encounter index):
  - `saiz_<n> = "[type=<fourcc>] [param=<P>] default_size=<D> count=<N> [sizes=<s0>,<s1>,…]"`
  - `saio_<n> = "v<version> [type=<fourcc>] [param=<P>] offsets=<o0>,<o1>,…"`

  The `type=` / `param=` blocks are omitted when the FullBox `flags &
  1` bit was zero on disk; the `sizes=` block is omitted when
  `default_sample_info_size != 0`. Offsets are decimal; in `stbl` they
  are absolute file positions. For movie-fragment carriage (§8.8.14)
  the parser also surfaces a per-`traf` summary as `frag_sai_<n>`
  through `Demuxer::metadata()` (`"track=<t> seq=<s> saiz=<n>
  saio=<m>"`), and the structured per-fragment records (with offsets
  preserved as `tfhd.base_data_offset`-relative per §8.8.14) are
  reachable through the public `Mp4Demuxer::sai_records()` accessor on
  the demuxer (downcast). Absent `saiz` / `saio`, no keys are emitted
  and `sai_records()` is empty.
- Producer reference time (ISO/IEC 14496-12 §8.16.5, `prft`): a
  top-level FullBox carrying a UTC wall-clock instant in NTP 64-bit
  format (RFC 5905 — high 32 bits = seconds since 1900-01-01 UTC,
  low 32 bits = fractional seconds) correlated with a media time on
  one reference track's media clock. Used by low-latency DASH / CMAF
  live streams so a consumer can match production wall-clock against
  media presentation time (and bound buffer occupancy without
  out-of-band timing signals). Each `prft` encountered during the
  top-level walk is surfaced on `Demuxer::metadata()` as `prft_<n>`
  (0-based file order); the value is three space-separated decimal
  integers `"reference_track_ID ntp_timestamp media_time"`. Both v0
  (32-bit `media_time`) and v1 (64-bit `media_time`) layouts are
  read; v1 `media_time` is widened to `u64` so callers see one type
  regardless. Absent `prft`, no keys are emitted. The structured
  record is also reachable via the public
  `oxideav_mp4::demux::parse_prft_box(&[u8])` entry point for tooling
  that wants the typed `PrftRecord` (`reference_track_id`,
  `ntp_timestamp`, `media_time`, `version`) directly.
- Progressive download information (ISO/IEC 14496-12 §8.1.3, `pdin`): a
  top-level `FullBox(version = 0, flags = 0)` whose body is a sequence
  of `(rate, initial_delay)` u32 pairs (big-endian) — for each effective
  download `rate` (bytes/second) the box hints the suggested initial
  playback `initial_delay` (milliseconds) that lets the rest of the
  file arrive ahead of its playback deadline (§8.1.3.3). A receiver
  estimates its observed throughput, finds the bracketing pair, and
  linearly interpolates the delay (or extrapolates from the first /
  last entry when the observed rate sits outside the recorded range).
  §8.1.3.1 fixes quantity at zero or one per file and recommends it
  be placed as early as possible; the top-level walker captures the
  first instance and ignores any subsequent copies. The parsed table
  is surfaced on `Demuxer::metadata()` as `pdin_count` (total number of
  pairs, decimal) plus `pdin_<n>` (one per pair, `"rate initial_delay"`
  decimal space-separated, 0-based in file order); an explicitly empty
  box (preamble only, zero pairs — a producer signalling "no hints
  available") emits `pdin_count = 0` and no `pdin_<n>` keys. Absent
  `pdin`, none of the keys are emitted (the demuxer signals "no box"
  by omission rather than by a zero count). The structured record is
  also reachable via the public
  `oxideav_mp4::demux::parse_pdin_box(&[u8])` entry point for tooling
  that already has the box's payload bytes in hand (a DASH packager,
  a manifest emitter, a fixture validator), and via
  `Mp4Demuxer::pdin_entries()` (downcast) for callers holding the
  demuxer trait object. A body whose post-preamble length is not a
  multiple of 8 (one u32 pair per entry) is rejected outright per
  §8.1.3.2 — silently rounding to the nearest pair would mask a
  producer-side framing bug; a body shorter than the 4-byte FullBox
  preamble likewise fails the open. The version byte is tolerated even
  if the producer wrote a non-zero value (the spec pins it to 0 but
  the payload layout is unambiguous), matching `parse_padb` /
  `parse_stdp` posture for FullBox version-bit slips.
- Level assignment (ISO/IEC 14496-12 §8.8.13, `leva`): a `FullBox(0, 0)`
  inside `mvex` whose body opens with `unsigned int(8) level_count`
  and is followed by `level_count` per-level entries. Each entry maps
  one fragment level to a `track_id` (u32) with a `padding_flag` (top
  bit of the packed §8.8.13.2 byte) and a 7-bit `assignment_type`
  discriminator: 0 selects a sample grouping by `grouping_type` (u32),
  1 adds `grouping_type_parameter` (u32), 2/3 carry no further tail
  (level by track / by track-subsegment), 4 names a `sub_track_id`
  (u32) for sub-track scope. Levels specify subsets of subsequent
  movie fragments — samples mapped to level `n` may depend on any
  level `m ≤ n` but never on level `p > n` (§8.8.13.1). The §8.8.13
  level table cannot be specified for the initial movie; the box
  applies to all moof fragments that follow. In DASH (ISO/IEC 23009-1)
  each subsegment indexed by a `sidx` (§8.16.3) is a "fraction" and
  data for each level shall appear contiguously within it in
  increasing level order; `padding_flag = 1` declares that a
  conforming fraction can be formed by concatenating any positive
  integer number of levels and padding the last `mdat` to its
  declared size. §8.8.13.1 fixes quantity at zero or one per file;
  the `mvex` walker captures the first instance and ignores any
  subsequent copies. The parsed table is surfaced on
  `Demuxer::metadata()` as `leva_count` (total entry count, decimal)
  plus `leva_<n>` per-entry strings of the shape `"<track_id>
  pad=<0|1> at=<assignment_type> [grouping_type=<u32>]
  [grouping_type_parameter=<u32>] [sub_track_id=<u32>]"` — the
  trailing tokens are emitted only when their `assignment_type`
  variant uses them. The structured record is also reachable via the
  public `oxideav_mp4::demux::parse_leva_box(&[u8])` entry point for
  tooling that already has the box's payload bytes in hand (a DASH
  packager, a manifest emitter, a scalable-bitstream layer picker),
  and via `Mp4Demuxer::leva_entries() -> Option<&[LevaEntry]>`
  (downcast) for callers holding the demuxer trait object. A
  truncated entry header (less than 5 bytes for `track_id` +
  padding/assignment) or a truncated variant tail is rejected at
  parse time — a partial table would lie about which levels exist.
  A malformed `leva` reaching `parse_mvex` is dropped silently so a
  producer slip cannot brick `open()` (the box is informational —
  playback proceeds without level scoping). The §8.8.13.3 minimum
  `level_count ≥ 2` is **not** enforced (the demuxer carries
  whatever the producer wrote so a validator can flag the short
  table). Reserved `assignment_type` values (> 4) are surfaced as
  5-byte header entries with the variant-specific fields zero — the
  spec leaves their tail undefined so consuming any extra bytes would
  desynchronise the loop. The version byte is tolerated even if the
  producer wrote a non-zero value, matching `parse_pdin` / `parse_padb`
  / `parse_stdp` posture for FullBox version-bit slips.
- Track extension properties (ISO/IEC 14496-12 §8.8.15, `trep`): a
  `FullBox(0, 0)` inside `mvex` whose body opens with
  `unsigned int(32) track_id` and is followed by "any number of boxes"
  (§8.8.15.2) — e.g. an `assp` (Alternative Startup Sequence Properties
  Box, §8.8.16). The box documents or summarises characteristics of one
  track in the subsequent movie fragments. §8.8.15.1 fixes quantity at
  zero or more per `mvex` (zero or one per track); the `mvex` walker
  collects every instance in file order. The nested child boxes are
  recorded by type + payload length only (`TrepChild`) — this crate does
  not recurse into them, leaving their semantics to a downstream consumer
  that recognises the specific child. Each `trep` is surfaced on
  `Demuxer::metadata()` as `trep_<n>` (0-based file order) with value
  `"<track_id> children=<k>[ <fourcc>...]"` — the track ID, the child
  count, and each child's four-character type in order. The structured
  record is reachable via the public
  `oxideav_mp4::demux::parse_trep_box(&[u8])` entry point (for tooling
  holding the box's payload bytes) and via
  `Mp4Demuxer::treps() -> &[TrepRecord]` (downcast). A `trep` shorter
  than its 4-byte FullBox preamble or with a truncated `track_id` is
  rejected at parse time; a malformed `trep` reaching `parse_mvex` is
  dropped silently so a producer slip cannot brick `open()` (the box is
  informational). A child box whose declared size overruns the remaining
  `trep` body is clamped to the bytes available and ends the child walk;
  a trailing partial child header (fewer than 8 bytes) ends the walk
  cleanly. The 64-bit `largesize` child form (`size == 1`) is read via
  its 16-byte header. The version byte is tolerated even if non-zero,
  matching the `parse_leva` posture.
- Subsegment index (ISO/IEC 14496-12 §8.16.4, `ssix`): the top-level
  `FullBox(version = 0, flags = 0)` that maps levels (as assigned by
  the §8.8.13 `leva` box above) to byte ranges of the subsegments
  indexed by the immediately preceding `sidx` (§8.16.4.1 placement:
  zero or one per leaf-only `sidx`, as its next box;
  `subsegment_count` shall equal that sidx's `reference_count`), so a
  client can fetch a partial subsegment — e.g. only the lower temporal
  levels — by byte range. The §8.16.4.2 body (`subsegment_count` u32,
  then per subsegment a `range_count` u32 followed by packed
  `(level u8, range_size u24)` records that partition every byte of
  the subsegment, level-contiguous and in increasing level order for
  leaf subsegments) decodes into `SsixRecord``SsixSubsegment`  `SsixRange`. Every `ssix` met during the top-level walk is collected
  in file order and surfaced on `Demuxer::metadata()` as `ssix_<n>` =
  `"<subsegment_count> <total_range_count>"` (shape summary — the full
  table is reachable via the public
  `oxideav_mp4::demux::parse_ssix_box(&[u8])` entry point or the
  `Mp4Demuxer::ssixes()` accessor, downcast). Absent `ssix`, no keys
  are emitted; like `leva`, a malformed instance is dropped without
  failing `open()` (the box is informational — this demuxer seeks via
  `sidx` / `tfra` and never fetches partial subsegments). Only
  version 0 is defined and any other version byte is rejected rather
  than mis-read; both u32 counts are validated against the bytes
  actually present before any allocation. The §8.16.4.1 writer-side
  `range_count ≥ 2` minimum is **not** enforced on read. The matching
  `build_ssix_box(&SsixRecord)` builder emits a complete
  `[size]['ssix']` box for segment-index emitters pairing it with a
  `sidx`, rejecting `range_size` values above the 24-bit wire field's
  0xFF_FFFF ceiling instead of silently masking them.

### Muxer

Only codecs with an `mp4` sample-entry packaging are accepted. Codec
knowledge is confined to `sample_entries::sample_entry_for`; the rest
of the muxer appends opaque packet bytes.

Supported encode codec ids (produced sample entry FourCC in
parentheses):

- `pcm_s16le``sowt`
- `flac``fLaC` with `dfLa` config (requires STREAMINFO extradata)
- `aac``mp4a` with `esds` (requires AudioSpecificConfig extradata)
- `h264``avc1` with `avcC` (requires AVCConfigurationRecord extradata)
- `mjpeg``jpeg`
- `mov_text``tx3g` (3GPP TS 26.245 timed text) — `text` handler + `nmhd`
- `webvtt``wvtt` (BMFF §12.6.3.2 XMLSubtitleSampleEntry sibling) — `subt` handler + `sthd`
- `ttml``stpp` (BMFF §12.6.3.2 XMLSubtitleSampleEntry) — `subt` + `sthd`
- `sbtt``sbtt` (BMFF §12.6.3.2 TextSubtitleSampleEntry) — `subt` + `sthd`
- `stxt``stxt` (BMFF §12.5.3.2 SimpleTextSampleEntry) — `subt` + `sthd`

For the subtitle codecs the muxer accepts the demuxer's surfaced
`extradata` verbatim (the post-preamble sample-entry payload: tx3g's
18-byte header, vttC config, stpp namespace strings, sbtt/stxt MIME
strings), so a demux → mux round-trip preserves the inner config.

Other codec ids fail with `Error::Unsupported` at `open`, never at
`write_packet` time.

Edit lists (`edts`/`elst`, ISO/IEC 14496-12 §8.6.5–6) are emitted
per-track when the first packet has a positive presentation timestamp:
a leading empty edit (`media_time = -1`) of the start delay (in the
movie timescale) followed by a `media_time = 0` segment for the track
duration, so a player offsets the track start instead of beginning at
presentation time 0. Version 0 (32-bit) by default, auto-promoting to
version 1 (64-bit) for over-32-bit durations. Tracks starting at PTS 0
get no `edts`. Controlled by `Mp4MuxerOptions::write_edit_list`
(default `true`).

Chunk offsets auto-promote from `stco` (32-bit) to `co64` (64-bit) when
any offset exceeds 4 GiB. The mdat box header stays 32-bit — files
whose mdat payload exceeds 4 GiB fail at `write_trailer`.

#### Fragmented / DASH / CMAF segment writing

The `dash`, `cmaf`, and `ismv` registry entries select the fragmented
muxer (`oxideav_mp4::frag::open_fragmented_typed`). It emits an init
segment (`ftyp` + `moov` with `mvex`/`trex` per track) followed by one
`styp? + sidx? + moof + mdat` segment per fragment cadence boundary, and
a trailing `mfra`/`tfra`/`mfro` random-access index. The per-segment
`styp` (ISO/IEC 14496-12 §8.16.2 Segment Type Box) is controlled by
`FragmentedOptions::styp`.

For caller-driven per-segment control, the
`FragmentedMuxer::write_fragmented_segment_with_styp(major_brand,
compat_brands)` inherent method marks the *next* emitted segment's
`styp` to use the given DASH/CMAF `(major, compat)` pair, overriding the
preset for one segment (then consumed). The stateless byte builder is
also exposed via the public `oxideav_mp4::styp` module —
`build_styp(major, compat)` / `write_styp(writer, major, compat)` —
mirroring the read-side `parse_styp` in oxideav-mov so a producer
round-tripping a parsed `Styp` can emit the same byte sequence.

#### Sample-group muxing

Sample groups (`sbgp` / `sgpd`, ISO/IEC 14496-12 §8.9.2 / §8.9.3) are
emitted per track via `Mp4MuxerOptions::track_sample_groups`. Each
entry's `sbgp` and `sgpd` Vecs are placed at the end of the target
track's `stbl` body after the chunk-offset table; `sgpd` is written
before `sbgp` so the description table the per-sample index references
is declared first (§8.5.1 ordering). The
`oxideav_mp4::sample_groups::{SampleToGroup, SampleGroupDescription,
build_sbgp, build_sgpd}` API also stands alone for callers that want
to assemble the raw boxes themselves.

The version pick for `sgpd` is automatic per §8.9.3.2: a `Some(_)`
`default_sample_description_index` with shared-length entries → v2
(no per-entry length); shared-length entries alone → v1 with fixed
`default_length`; mixed-length entries → v1 with per-entry
`description_length`. The deprecated version-0 "no length signalling"
form is not emitted. The grouping-type-specific entry payload itself
is opaque to the container — callers supply already-serialised
`Vec<u8>` per entry.

`sbgp` chooses v0 (no `grouping_type_parameter`) or v1 (`Some(_)`) per
§8.9.2; a `group_description_index` ≥ `0x10001` (movie-fragment-local
per §8.9.4) is written verbatim, the muxer does not resolve it.

### Seek strategy

`seek_to(stream, pts)` tries three strategies in order, picking the
first that applies:

1. **`tfra` fast-path (ISO/IEC 14496-12 §8.8.11).** If the file has
   a trailing `mfra` whose `tfra` indexes the requested track, the
   demuxer binary-searches the `tfra` time table for the largest
   `time ≤ pts`, translates the result to a `moof_offset`, and
   snaps to the first keyframe at-or-after that offset. O(log N) on
   `tfra` + a one-fragment-bounded sample-list scan.
2. **`sidx` fast-path (ISO/IEC 14496-12 §8.16.3).** If no `tfra`
   covers the track but the file carries one or more `sidx` boxes
   whose `reference_id` matches the track's `track_ID`, the demuxer
   walks every matching `sidx`, expands its references into virtual
   `(EPT, byte_offset)` anchors, and picks the latest anchor whose
   decode-time start is at-or-before `pts` (translated from the
   track's media timescale into the sidx timescale per §8.16.3).
   Both on-the-wire shapes are handled: a single `sidx` indexing
   every subsegment (DASH on-demand profile) and one `sidx` per
   subsegment (DASH live profile / what our own muxer emits).
   Hierarchical (nested) sidx references are walked for byte-range
   accounting only — they don't carry a media-time anchor we can
   land on. Timescale conversion uses `u128` arithmetic so the
   multiply doesn't overflow for long-duration tracks even when the
   track's media timescale and the sidx's timescale differ (per the
   spec-permitted but DASH-IF-deprecated case).
3. **Linear scan fallback.** Walks the sample table picking the
   last keyframe at-or-before `pts`. This is the unconditional
   safety net — when neither index applies, or when the indexed
   offset doesn't resolve cleanly (corrupt index, mdat layout the
   file lied about), `seek_to` still returns a correct cursor.

### Not (yet) supported

- Fragmented-MP4 *muxing* — the demuxer reads `moof`+`mdat`
  segments, but the muxer only emits a single moov-at-end (or
  faststart) shape.
- In-pipeline CENC decryption — the AES-128 CTR / CBC driver
  exists (`cenc_cipher::decrypt_sample_in_place`, all four §10
  schemes), but the demuxer does not invoke it automatically:
  `read_packet` still yields ciphertext payloads and the caller —
  the party holding the content key for the sample's KID — calls
  the driver per sample. Key acquisition (DRM license exchange
  against the `pssh.SystemID` blob) is out of scope by design. The
  mdat-resident auxiliary-information bytes that the `saio`
  offsets name are not pre-fetched — a CENC consumer reading them
  seeks the input itself using the surfaced offsets.
- Per-sample *selection* of a non-active sample description. All
  `stsd` entries are now parsed and surfaced (see §8.5.2 above), but
  active decode always uses entry `[0]`: a per-chunk
  `stsc.sample_description_index ≥ 2` or a fragment's
  `tfhd.sample_description_index` override is recorded/surfaced but not
  yet honoured to re-dispatch the codec mid-stream.
- mdat payloads larger than 4 GiB (the 32-bit box header is not
  promoted to `largesize`).

## Container registry

```rust
let mut reg = oxideav_container::ContainerRegistry::new();
oxideav_mp4::register(&mut reg);
```

Registers:

- Demuxer `"mp4"` (also serving `.mp4`, `.m4a`, `.m4v`, `.3gp`, `.mov`,
  `.ismv`).
- Muxers `"mp4"`, `"mov"`, `"ismv"`.
- A content probe that recognises `ftyp` / `wide`+`ftyp` / `moov`.

## Fuzzing

A `cargo-fuzz` target exercises the BMFF box-tree walker on
arbitrary bytes:

```sh
cd fuzz
cargo +nightly fuzz run demux
```

The target opens, drains up to 256 packets, and re-seeks; it asserts
nothing panics, aborts, or OOMs. Seed corpus + regression artefacts
live at `fuzz/corpus/demux/`. The fuzz crate has its own `[workspace]`
and a committed `Cargo.lock` for reproducibility.

Pinned regressions worth calling out:

* **Extended-size u64 overflow** — a `size=1 largesize=u64::MAX`
  extended box anchored at a non-zero file offset used to overflow
  every downstream `body_start + payload_size` arithmetic site
  (the §8.16.3 `sidx` end-anchor computation is the most exposed
  example). `read_box_header` now `checked_add`s `start + total_size`
  and rejects the header before any caller computes a derived end
  byte. Replayed by `tests/largesize_overflow.rs` and two boundary
  unit tests in `src/boxes.rs`. Companion to oxideav-mov's round 187
  fix on the QTFF atom walker.

## License

MIT — see [LICENSE](LICENSE).