oxideav-mp4 0.0.9

Pure-Rust MP4 / ISO base media file container for oxideav
Documentation

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 framework but usable standalone.

Installation

[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

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

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

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_IDs (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 SsixRecordSsixSubsegmentSsixRange. 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_s16lesowt
  • flacfLaC with dfLa config (requires STREAMINFO extradata)
  • aacmp4a with esds (requires AudioSpecificConfig extradata)
  • h264avc1 with avcC (requires AVCConfigurationRecord extradata)
  • mjpegjpeg
  • mov_texttx3g (3GPP TS 26.245 timed text) — text handler + nmhd
  • webvttwvtt (BMFF §12.6.3.2 XMLSubtitleSampleEntry sibling) — subt handler + sthd
  • ttmlstpp (BMFF §12.6.3.2 XMLSubtitleSampleEntry) — subt + sthd
  • sbttsbtt (BMFF §12.6.3.2 TextSubtitleSampleEntry) — subt + sthd
  • stxtstxt (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_index0x10001 (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

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:

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_adds 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.