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.
[dependencies]
oxideav-core = "0.1"
oxideav-codec = "0.1"
oxideav-container = "0.1"
oxideav-mp4 = "0.0"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>>(())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>>(())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.
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 asextradata. -
Sample-table expansion:
stts,stsc,stsz/stz2,stco/co64,stss.next_packetserves samples in file-offset order. -
Fragmented MP4 (DASH / HLS / Smooth Streaming / CMAF):
mvex/trexper-track defaults plus zero or more trailingmoof+mdatpairs (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,tfhd.base_data_offset, and the per-traftfhd/trexsample_description_indexare all honoured.stypmarkers are skipped;sidxandmfra/tfraare parsed and drive the seek fast-paths (see "Seek strategy" below). -
Movie extends header (ISO/IEC 14496-12 §8.8.2,
mehd): a sealed fragmented file'smvex/mehdMovieExtendsHeaderBox carries the overall presentationfragment_duration(in the movie timescale, per §8.8.2.3) — including allmooffragments — that an authoring step can write once the fragments are laid down. Both versions are read (v0 32-bit, v1 64-bit widened tou64). When present and non-zero, the value drivesDemuxer::duration_microsand takes precedence overmvhd.duration— sealed fragmented files commonly carrymvhd.duration = 0(the moov has no resident samples that contribute to a non-fragment duration) andmehdis then the only authoritative total. The raw value (in the movie timescale) is also surfaced verbatim viaDemuxer::metadata()as themehd_fragment_durationkey, for tooling wanting the untranslated number (e.g. CMAF live-edge probes confirming a producer's sealed total against their own running tally). Absentmehd, the key is not emitted andduration_microsfalls back tomvhd.durationas before. -
Random-access trailer size check (ISO/IEC 14496-12 §8.8.12,
mfro): themfra's MovieFragmentRandomAccessOffsetBox declares the size of the enclosingmfraso a player can locate the trailer from the last 16 bytes of the file. The declared value is surfaced onDemuxer::metadata()asmfro_size, and when it disagrees with themfrabox actually measured on disk amfro_size_mismatchkey (declared=<d> actual=<a>) flags the broken shortcut — the open and thetfraseek table are unaffected either way. Absentmfro, no keys are emitted. -
Empty-time track fragments (ISO/IEC 14496-12 §8.8.7,
tfhdduration-is-emptyflag 0x010000): atrafmay insert "empty time" into a track instead of samples (§8.8.6.1 — e.g. audio silence suppression). The empty interval's length is the effective default sample duration (thetfhdoverride, else thetrexdefault); the demuxer advances the track's running decode time by it, so a subsequenttfdt-less fragment lands after the gap (atfdtinside the empty traf re-anchors the gap's start first). Per §8.8.8.1 "If the duration-is-empty flag is set in the tf_flags, there are no track runs" — a trun a non-conforming producer left inside an empty-duration traf is ignored rather than fabricating samples or double-counting the interval. Each gap is surfaced throughDemuxer::metadata()asfrag_empty_duration_<n>("track=<t> seq=<s> duration=<d>", moof-walk order, duration in the track's media timescale) and typed viaMp4Demuxer::empty_duration_records()(a slice ofdemux::EmptyDurationRecord), so a remuxer or validator can reconstruct where the gaps were. Absent empty-time inserts, no keys are emitted. -
Edit lists (ISO/IEC 14496-12 §8.6.5–6,
edts/elst): the full explicit timeline map is applied to packet timestamps. Each edit segment contributes a constant presentation delta — a leading empty edit (media_time = -1) pushes the track's start out by its (movie-timescale) duration, an initial trim (media_time > 0) subtracts the trim point, a dwell (media_rate_integer = 0) inserts presentation time without consuming media, and multi-segment lists hop each mapped media range onto its presentation position (segment_durationis rescaled movie→media timescale with saturating 128-bit math; a zero duration is the §8.6.6.1 open-ended "frommedia_timeonwards" form). Media the list never presents — decode pre-roll before the first mapped composition time (e.g. priming samples ahead of a trim) and ranges excised between segments — is still delivered for decoding but carries the packetdiscardflag. Both v0 (32-bit) and v1 (64-bit) entry layouts are read. The mapping applies when the list is media-monotonic (non-decreasingmedia_timeand per-segment delta across segments — every common shape: delay, trim, delay + trim, dwell inserts, padded excisions); a list that re-orders or repeats media falls back to the single leading-shift mapping so DTS stays monotonic. The same mapping covers moov-resident sample tables andmooffragments (the elst lives in the moov and spans the fragments). A muxer-written start-delay elst round-trips: demuxing our own output recovers the original packet pts including the delay. The declared list is also surfaced verbatim: typed viaMp4Demuxer::edit_list(stream)(a slice ofdemux::EditListEntry—segment_duration/media_time/media_rate_integer/media_rate_fraction, withis_empty_edit()/is_dwell()helpers) and flat viaparams.optionsaselst_entry_count+elst_<n>(dur=<d> media_time=<mt> rate=<int>). The standalone byte pairdemux::parse_elst_box/build_elst_boxround-trips the box byte-exact (auto v0/v1 width selection; build rejects §8.6.6.3 violations — a rate outside {0, 1}, a final empty edit, amedia_timebelow the -1 sentinel, an empty entry list), so a remuxer can carry a source's elst across unchanged. -
Seek:
seek_to(stream, pts)lands on the nearest sync-sample ≤ pts (or the first keyframe of the stream if none qualify). -
Metadata: 3GPP
udtaboxes (titl/auth/…) and iTunes-stylemeta/ilstare surfaced viaDemuxer::metadata(). -
HEIF / MIAF item catalogue (ISO/IEC 14496-12 §8.11): a file-level
metabox's item infrastructure is decoded into the publicdemux::MetaItemsrecord —pitm(PrimaryItemBox §8.11.4),iloc(ItemLocationBox §8.11.3, the full v0/v1/v2 field-width-selector + construction-method + per-extent layout),iinf/infe(ItemInfoBox §8.11.6, everyinfeversion 0–3, including the v2/3 32-bititem_typeFourCC) andiref(ItemReferenceBox §8.11.12, typed from→to item-ID groups), plus themeta'shdlrhandler type. The structured records are reachable viaMp4Demuxer::meta_items(); a flat summary appears onDemuxer::metadata()asmeta_handler/meta_primary_item/meta_item_count/meta_item_<n>/meta_iloc_count/meta_iloc_<n>(id=<item_id> method=<construction_method> extents=<n> length=<total>— where each item lives without downcasting) /meta_iref_count/meta_iref_<n>(type=<fourcc> from=<item_id> to=<id,…>— the relationship graph: thumbnailthmb, auxiliaryauxl, derivationdimg, descriptioncdsc, pre-derivedbase, predictivepred, tile-basetbas, scalable-baseexbl, …). A plain iTunesmeta(justhdlr+ilst) carries no §8.11 items and emits nometa_*keys. The standalone parsersdemux::parse_iloc_box/parse_pitm_box/parse_iinf_box/parse_iref_box/parse_meta_itemsare public. This crate surfaces the item catalogue (locate / type / relate the items); it does not decode an item's codec payload. Theidat(ItemDataBox §8.11.11) bytes are captured intoMetaItems::idat;MetaItems::item_byte_ranges(id)resolves an item's extents to(offset, length)pairs (base-offset folded in, relative to the item'sconstruction_methoddata origin) andMetaItems::item_data_from_idat(id)materialises an idat-resident item's concatenated bytes (with bounds checks).meta_idat_lenis surfaced onDemuxer::metadata()when anidatis present. Write counterpartsdemux::build_iloc_box/build_pitm_box/build_iinf_box/build_iref_box/build_idat_boxserialise each box byte-exact (the inverse of the parsers), so a caller can assemble a HEIF-stylemetafromMetaItemsrecords;build_iloc_boxrejects records that would not round-trip (bad field width, a v0 item with a non-zero construction method, etc.). -
HEIF / MIAF item properties (ISO/IEC 23008-12 §9.3 / §6.5,
iprp/ipco/ipma): a file-levelmeta'siprpItemPropertiesBox is decoded into the publicdemux::ItemProperties(reachable viaMetaItems::iprp) — theipcoItemPropertyContainerBox property list (an implicitly 1-indexed sequence of property boxes) plus everyipmaItemPropertyAssociation box merged into per-item(essential, property_index)lists.ItemProperties::properties_for(item_id)resolves an item to its ordered(essential, &ItemProperty)pairs (transformative properties apply in sequence, §6.5.1; index-0 / out-of-range references are skipped) andItemProperties::property(idx)looks up the 1-basedipcoslot. The typeddemux::ItemPropertyenum models the base property set:ispeImageSpatialExtents (§6.5.3),pixiPixelInformation (§6.5.6),rlocRelativeLocation (§6.5.7),auxCAuxiliaryType (§6.5.8, NUL-terminated URN + subtype tail),irotImageRotation (§6.5.10),imirImageMirroring (§6.5.12),lselLayerSelector (§6.5.11),udesUserDescription (§6.5.20, lang/name/description/tags),alttAccessibilityText (§6.5.21),isclImageScaling (§6.5.13, a transformative H/V scale-ratio property),rrefRequiredReferenceTypes (§6.5.17),crtt/mdftCreation / Modification time (§6.5.18–19, µs since 1904-01-01 UTC), pluspasp/clap/colrreusing this crate's existing 14496-12 records (§6.5.4 / §6.5.9 / §6.5.5); any unrecognised property box is preserved verbatim asItemProperty::Otherso its 1-based index slot survives a round-trip (the index anipmareferences must stay stable). Reserved high bits onirot/imirare masked on read, and a property truncated for its declared type falls back toOtherrather than reading past the end. The demuxer surfaces a compact summary onDemuxer::metadata():meta_iprp_property_count,meta_iprp_property_<n>(a<fourcc> <decoded>token), andmeta_iprp_item_<n>(id=<id> props=<idx[,idx*…]>,*marking an essential association). Public byte-exact buildersdemux::build_iprp_box/build_ipco_box/build_ipma_box/build_item_propertyare the inverses of the parsers (parse_iprp_box/parse_ipco_box/parse_ipma_box);build_ipma_boxauto-selects the narrowest item-ID width (v0 16-bit / v1 32-bit) andproperty_indexwidth (flags & 17- / 15-bit), sorts entries by ascendingitem_ID(§9.3.1), and rejects an index past the 15-bit ceiling. Absentiprp, no keys are emitted. This crate surfaces the property catalogue; applying a transform (rotate / mirror / scale / crop) to a decoded item is a renderer concern. -
HEIF / MIAF entity groups (ISO/IEC 23008-12 §9.4,
grpl/EntityToGroupBox): a file-levelmeta'sgrplGroupsListBox is decoded into the publicdemux::EntityGroups(reachable viaMetaItems::grpl) — everyEntityToGroupBox(a FullBox whose FourCC is thegrouping_type:altralternatives where only one entity should be played,stera two-entity stereo pair with entity 0 = left view, …) becomes a typeddemux::EntityToGroupcarrying thegrouping_type,group_id, and theentity_idlist (item IDs, or track IDs at file level).EntityGroups::by_type(grouping_type)filters (e.g. allaltralternative sets) andEntityGroups::by_id(group_id)locates a group. Surfaced onDemuxer::metadata()asmeta_grpl_group_countplusmeta_grpl_group_<n>(type=<fourcc> id=<id> entities=<id,…>). Public byte-exact buildersdemux::build_grpl_box/build_entity_to_group_boxare the inverses ofparse_grpl_box. AnEntityToGroupBoxwhosenum_entities_in_groupoverruns its body is dropped without aborting thegrplwalk. Absentgrpl, no keys are emitted. -
Extended language tag (ISO/IEC 14496-12 §8.4.6,
elng): a track'smdia/elngExtendedLanguageBox carries a NULL-terminated BCP 47 (RFC 4646) tag richer thanmdhd's packed 3-char ISO 639-2 code (region / script / variant subtags). When present it is surfaced onparams.options["language"](e.g.en-US,zh-Hant-HK,es-419) and, per §8.4.6.1, overrides themdhdlanguage. Absentelng, the option is omitted (callers fall back tomdhd). -
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 withMediaType::Subtitleand the per-codec id from the table above; their post-preamble payload (BMFF strings / tx3g header / vttC) is preserved verbatim inparams.extradatafor 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 innersinfto recover the original codec FourCC fromfrmaand the protection scheme fromschm. 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_ciphermodule (key acquisition stays with the caller).tenc(§8.2 TrackEncryptionBox) — discovered insidesinf/schi. v0 capturesdefault_isProtected,default_Per_Sample_IV_Size, anddefault_KID; v1 adds thedefault_crypt_byte_block/default_skip_byte_blockpattern pair (forcens/cbcsschemes) and thedefault_constant_IVused whenisProtected==1 && IV_size==0. Surfaced onparams.optionsascenc_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 opaqueDatablob. Surfaced viaDemuxer::metadata()aspssh_<n>keys with value"<system_id_hex> <kid_count> <data_len>"; structured records are reachable through the publiccenc::PsshBoxtype for callers that downcast.- moof-level
pssh(§8.1.1 — "Container: Movie (moov) or Movie Fragment (moof)"). pssh boxes that live directly inside amoofare collected per fragment and keyed by the enclosingmfhd.sequence_numberso 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 viaDemuxer::metadata()asmoof_pssh_<n>keys with value"systemid=<hex> seq=<mfhd_seq> kids=<n> data=<len>"; structured records are reachable through the publicdemux::MoofPsshRecordtype (one record per box in moof-walk order,version0 + 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 everytrafwhose matching track carried atencdefault (so the per-sample IV width is recoverable per §7.2.3). Capturesflags, the per-sampleInitializationVector, and (whenUseSubSampleEncryptionis set) the{BytesOfClearData, BytesOfProtectedData}subsample map. Surfaced viaDemuxer::metadata()assenc_<n>keys with value"track=<idx> seq=<mfhd_seq> samples=<n> flags=0x<hex>"; structured records are reachable through the publiccenc::SencBoxtype.
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). The write side iscenc::build_tenc_box/build_pssh_box/build_senc_box— each emits a complete[size][fourcc]box whose body is the byte-exact inverse of the matching parser, rejecting records that would not round-trip (a v0tenccarrying a pattern pair, a constant IV whose presence disagrees with the §9.1isProtected/IV_sizerule, a v0psshwith KIDs, asencwith mixed per-sample IV widths or subsample maps under a clearedUseSubSampleEncryptionflag, …).- PIFF legacy
uuidencryption boxes (Protected Interoperable File Format 1.1/1.3 — the pre-CENC predecessors ofsenc/tenc/psshemitted by Smooth Streaming / PlayReady-era packagers, carried as §4.2 extended-type boxes keyed by vendor UUIDs). All three usertypes are parsed and re-emitted: theschi-level TrackEncryptionBox (cenc::PiffTencBox— u24AlgorithmID0 none / 1 AES-CTR / 2 AES-CBC,IV_size,KID; surfaced onparams.optionsaspiff_algorithm_id/piff_iv_size/piff_kidand per track viaMp4Demuxer::piff_tencs), the moov- and moof-level ProtectionSystemSpecificHeaderBox (byte-identical to a v0psshpayload;Mp4Demuxer::piff_psshes/piff_moof_psshes, metadatapiff_pssh_<n>/piff_moof_pssh_<n>), and the traf-level SampleEncryptionBox (cenc::PiffSencBox— inline per-sample IVs + subsample map, plus the PIFF-onlyflags & 1inlineAlgorithmID / IV_size / KIDoverride triple that CENC later replaced withseig;Mp4Demuxer::piff_senc_records, metadatapiff_senc_<n>). A traf whose only sample-encryption carrier is the PIFF form is bridged into the standardsenc_recordssurface (the tables are byte-compatible), andPiffTencBox::to_tenc/scheme_decisionroute legacy tracks into the sameCencSchemeDecisiondecryption path as moderncenc/cbc1files; dual-branded files (both forms present) keep the 4CC boxes authoritative with no double-reporting. Write side:cenc::build_piff_tenc_box/build_piff_senc_box/build_piff_pssh_boxemit completeuuid-wrapped boxes, byte-exact inverses of the parsers with the same round-trip rejection discipline.
-
emsgDASH Event Message Box (ISO/IEC 23009-1 §5.10.3.3) — in-band timed events (SCTE-35 splice, ad markers, application events) carried at the top level of a media segment before the firstmoofthey apply to. Both versions are parsed and written — v0 (strings first, 32-bit segment-relativepresentation_time_delta) and v1 (integers first, 64-bit absolutepresentation_time) — into the typedemsg::EmsgBox(scheme_id_uri,value,timescale,EmsgTimepresentation,event_durationwith the0xFFFFFFFFunknown sentinel,id,message_data). The demuxer collects instances in file order keyed by the index of the followingmoof(Mp4Demuxer::emsgs, metadataemsg_<n>), andMp4Demuxer::emsg_absolute_timeresolves a v0 delta to an absolute presentation time by anchoring it to the earliest presentation time of the moof the box preceded (rescaled into the emsg timescale; v1 passes through); the fragmented muxer queues them per segment viaFragmentedMuxer::set_next_segment_emsg, writing them between thestypand themoofand counting their bytes into thesidxreferenced_size/ssixmetadata range. Standalone byte layer:emsg::parse_emsg_box/build_emsg_box.- Typed scheme decision router (§4.2 + §10). The four
scheme_typeFourCCs 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 typedcenc::CencSchemeenum. The two binary axes a downstream decryptor dispatches on — cipher mode (CTR vs CBC,cenc::CipherMode) and pattern-encryption flag — are surfaced viaCencScheme::cipher_mode()andCencScheme::uses_pattern_encryption(). A scheme value bundled with the parsedtencbecomes acenc::CencSchemeDecision— the typed routing slip a future AES layer can pattern-match against — built viaCencSchemeDecision::new(scheme, tenc)with structural validation only (the scheme'srequired_tenc_version()must matchtenc.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 whenisProtected == 0— is recovered viaCencSchemeDecision::iv_supply()as the typedcenc::IvSupplyenum. The bundle is a static dispatch contract built from container-side bytes only; the AES execution layer that consumes it iscenc_cipher::decrypt_sample_in_place, with key material (looked up by KID from the namedpssh.SystemIDsystem) supplied by the caller. Unknownscheme_typeFourCCs are preserved verbatim throughCencScheme::Unknown([u8; 4])so a caller carrying a private DRM dialect can still route on its own table. - Typed
CencSampleEncryptionInformationGroupEntryparser (§6). Aseigsample-group entry overrides the track-defaulttencparameters 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 existingsgpdgrouping_type = *b"seig"surface already preserves) into the typedcenc::SeigEntry, mirroring the validationparse_tencapplies (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 parsedCencSchemeDecisionplus the per-sample subsample list (fromparse_senc) and returns the orderedVec<CipherStep>partition of the sample plaintext into contiguous(offset, len, kind, iv_restart)runs an AES layer iterates:Clearruns pass through verbatim andEncryptedruns feed the cipher mode picked byCencSchemeDecision::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.7whole-block leave it in the clear, subsample non-pattern isClear(BytesOfClearData) + Encrypted(BytesOfProtectedData)per subsample with a continuous chain across them (§9.5.1), subsample pattern walkscrypt_byte_block * 16encrypted +skip_byte_block * 16clear with the trailing partialcrypt_byte_blockreset to clear (§9.6), and thecbcsIV restart per subsample (§9.5.1) lights up only on the firstEncryptedstep of each subsample forcbcs(notcenc/cbc1/cens). §9.5.1 invariants are enforced —Σ (clear+protected) == sample_len, no both-zero subsample, no subsample running past sample_len.IvSupply::NoneandCencScheme::Unknownare 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 theCencSchemeDecision(per-sample IV length-checked againsttenc; constant IV pulled fromtenc.default_constant_IV; supplying both is a §9.2 error), builds theCipherStepplan, and executes it in place against AES-128 (theaes/ctr/cbcprimitive crates) — all four §10 schemes decrypt. The §9.1 IV expansion (8-byte IV → bytes 0..8, bytes 8..16 zero) iscenc_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;censskip blocks consume no keystream; thecbc1chain crosses clear gaps), whilecbcsreseeds its chain from the constant IV at each subsample'siv_restartstep 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 ascenc_cipher::decrypt_steps_in_place(mode, key, iv, steps, data)forseig-overridden sample groups where the caller assembles its own plan/IV pairing. The write-side duals —cenc_cipher::encrypt_sample_in_place/encrypt_steps_in_place— encrypt plaintext samples under the identical §9.4–§9.7 partition (CTR is the same keystream XOR; CBC runs the cipher forward with the same per-cbcs-subsample IV restart and whole-block discipline), so encrypt followed by decrypt with the same arguments is the identity — a packager producing protected content uses the same routing decision the reader consumes. 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).
- Typed scheme decision router (§4.2 + §10). The four
-
Track references (ISO/IEC 14496-12 §8.3.3,
tref): each typedTrackReferenceTypeBoxinsidetrak/trefis parsed and the resulting(reference_type → track_IDs)pairs are surfaced onparams.optionsastref_<type>keys whose value is a space-separated list of referencedtrack_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 = 0entries (spec-prohibited) are dropped. The write side isdemux::build_tref_box(&[(reference_type, track_IDs)]), the byte-exact inverse of the parser. -
Track groups (ISO/IEC 14496-12 §8.3.4,
trgr): eachTrackGroupTypeBoxchild insidetrak/trgris parsed as a(track_group_type, track_group_id)pair — the child's FourCC names the grouping (msrcis the spec-named example, for multi-source presentations: tracks sharing atrack_group_idunder themsrctype originate from the same source, e.g. one participant's audio + video in a recorded video call) and the 32-bittrack_group_ididentifies 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 (usetreffor those). Each child is surfaced onparams.optionsastrgr_<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 (unliketrefwhich caps at one perreference_type) so both encounter-order copies are preserved. Bytes trailing the 32-bit id inside a child are reserved for per-track_group_typeextensions in derived specs and silently ignored at this layer; a child whoseversionfield is non-zero (§8.3.4.2 pins it to 0) is also silently skipped, so unknown extensions never mis-parse. Absenttrgr, none of the keys are emitted. The write side isdemux::build_trgr_box(&[(track_group_type, track_group_id)]). -
Track Kind (ISO/IEC 14496-12 §8.10.4,
kind): eachKindBoxinside a track-leveludtais parsed as a(schemeURI, value)pair (both NULL-terminated C strings; an absent value is allowed, meaning the URI alone identifies the kind). Multiplekindboxes per track are supported — the spec explicitly allows several schemes co-labelling the same track (e.g. one DASH roleurn:mpeg:dash:role:2011 mainplus one iTunes-scheme tag). Each entry is surfaced onparams.optionsaskind_<n>(0-based encounter index); the value is the URI alone when no name follows, or"URI value"(space-separated, mirroring thetref_<type>convention) when both are present. The write side isdemux::build_kind_box(scheme_uri, value). -
Track copyright (ISO/IEC 14496-12 §8.10.2,
cprt): eachCopyrightBoxinside a track-leveludtais 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. Multiplecprtboxes per track are supported — the spec's "Quantity: Zero or more" covers the multilingual case (one box per language). Each record is surfaced onparams.optionsas the paircopyright_<n>(the notice; key omitted when the notice is empty) andcopyright_<n>_lang(the 3-letter tag; emitted even with an empty notice so callers can tell "empty notice ineng" from "nocprt"). 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.244cprtshape lumped under the generic file-wide metadata channel, where the language code is not surfaced separately. The write side isdemux::build_cprt_box(language, notice). -
Track selection (ISO/IEC 14496-12 §8.10.3,
tsel): the optionalTrackSelectionBoxinside a track-leveludtacarries 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 ontkhd(§8.3.2), andattribute_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 onparams.optionsastsel_switch_group(the signed integer, emitted even when zero so callers can tell present-but-zero from absent) andtsel_attributes(space-separated FourCCs, mirroring thetref_<type>convention; omitted when the list is empty). A body shorter than the 8-byte FullBox +switch_groupminimum or an unknown FullBox version is silently dropped —tselis 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. Absenttsel, no keys are emitted. The write side isdemux::build_tsel_box(switch_group, attribute_list). -
Sub tracks (ISO/IEC 14496-12 §8.14,
strk/stri/strd/stsg): a track-leveludtamay carry zero or morestrkSub 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). Eachstrk's mandatorystri(Sub Track Information, §8.14.4) carriesswitch_group(signed 16-bit),alternate_group(signed 16-bit),sub_track_ID(32-bit), and anattribute_list[]of FourCCs from §8.14.4.3's descriptive (tesc/fgsc/cgsc/spsc/resc/vwsc) and differentiating (bitr/frar/nvws/ …) vocabulary. The mandatorystrd(Sub Track Definition, §8.14.5) is walked one level for itsstsg(Sub Track Sample Group, §8.14.6) children — each names agrouping_typeshared with the track'ssbgp/sgpd(§8.9) plus thesgpddescription indices that make up the sub track. Each sub track is surfaced onparams.optionsassubtrack_<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), theattrs=andstsg=blocks are omitted when empty, and FourCCs with non-printable bytes fall back to 8-digit hex (matching thetsel_attributesconvention). Astrkmissing its mandatorystricontributes no sub track; an unknown FullBox version onstri/stsg, a too-shortstri, or astsgwhose declareditem_countoverruns the body are rejected — the boxes are informational and a malformed entry never aborts the open. Absentstrk, no keys are emitted. The write side isdemux::build_strk_box(switch_group, alternate_group, sub_track_id, attribute_list, sample_groups)(withbuild_stri_box/build_stsg_boxexposed for the individual children). -
Hint media header (ISO/IEC 14496-12 §12.4.2,
hmhd): a hint track'sminf/hmhdHintMediaHeaderBox carries protocol-independent streaming statistics for the packetised stream the hint track describes —maxPDUsize/avgPDUsize(byte sizes of the largest / average Protocol Data Unit) andmaxbitrate/avgbitrate(bits/second over any one-second window / the whole presentation), with a trailing reserved 32-bit word read past but not surfaced. Used by a streaming server to size buffers / pace delivery without parsing every hint sample. The four fields are surfaced onparams.optionsashmhd_max_pdu_size,hmhd_avg_pdu_size,hmhd_max_bitrate, andhmhd_avg_bitrate(decimal). The FullBoxversion(pinned to 0 by §12.4.2.2) and a non-zero reserved word are tolerated rather than dropping a usable header, matching thevmhdposture; a body shorter than the full 20 bytes is rejected (a truncated tail would surface noise as a bitrate). Absenthmhd, none of the keys are emitted. -
QuickTime base media information header (
minf/gmhd/gmin): QuickTime carries a Base Media Information Header Atom (gmhd) insideminfin place of a typed media header (vmhd/smhd) for media types derived from the base media handler — text, timecode, music, and generic tracks. Its required child, the Base Media Info Atom (gmin), is a FullBox whose fixed 12-byte body carries a 16-bitgraphicsmodecomposition (transfer) mode, a three-component 16-bitopcolor, a signed 16-bit stereo soundbalance, and a reserved 16-bit zero. Thegmhdwalk resolvesgminpast any media-specific sibling atoms (text/tmcd/ …). The three fields are surfaced onparams.optionsasgmin_graphicsmode(decimal),gmin_opcolor(three space-separated decimal components), andgmin_balance(signed decimal). The FullBoxversion/flags(specified as 0) are tolerated rather than dropping a usable atom, matching thevmhdposture; a body shorter than the full 16 bytes is rejected. Public byte-exact parse/build helpersdemux::parse_gmin_box/demux::build_gmin_box(thegminatom) anddemux::build_gmhd_box(a minimalgmhdwrapping a singlegmin) let tooling recover or assemble the atom without re-runningopen(). Absentgmhd, none of the keys are emitted. -
QuickTime timecode media info (
minf/gmhd/tcmi): a timecode track (media typetmcd) carries a Timecode Media Information Atom (tcmi) inside itsgmhd, governing how the on-screen timecode text is rendered. Its FullBox body carries a 16-bittext_fontid, atext_facestyle bitmask (Bold0x01/ Italic0x02/ Underline0x04/ Outline0x08/ Shadow0x10/ Condense0x20/ Extend0x40), a 16-bittext_sizepoint size, 48-bit ([u16; 3])textandbackgroundRGB colours, and a Pascal-stringfont_name. Thegmhdwalk resolves bothgminandtcmi(each the first parseable instance wins). Surfaced onparams.optionsastcmi_text_font/tcmi_text_face/tcmi_text_size(decimal),tcmi_text_color/tcmi_background_color(three space-separated 16-bit components), andtcmi_font_name(omitted when empty). A body shorter than the 22-byte fixed prefix is rejected; afont_namelength that overruns the body is clamped to the bytes present rather than dropping the atom. Public byte-exactdemux::parse_tcmi_box/demux::build_tcmi_boxround-trip the atom. Absenttcmi, none of the keys are emitted. -
QuickTime timecode sample description (
stsdtmcdentry): a timecode track (media typetmcd) carries a singletmcdsample entry defining how its timecode samples are interpreted. After the shared 8-byte sample-entry preamble: a reserved u32, a 32-bitflagsword (Drop-frame0x01/ 24-hour-max0x02/ Negative-OK0x04/ Counter0x08), a 32-bittimescale, a 32-bitframe_duration, and an 8-bitnumber_of_frames. Dispatched by FourCC (thetmcdhandler maps to the Data media type). Surfaced onparams.optionsastmcd_flags(decimal), the decoded booleanstmcd_drop_frame/tmcd_24hour_max/tmcd_negative_ok/tmcd_counter, andtmcd_timescale/tmcd_frame_duration/tmcd_number_of_frames(decimal). TheTmcdSampleEntryrecord'sdrop_frame()/twenty_four_hour_max()/negative_times_ok()/counter()helpers decode the flag word. Public byte-exactdemux::parse_tmcd_sample_entry_box/demux::build_tmcd_sample_entryround-trip the entry. Absent atmcdentry, none of the keys are emitted. -
QuickTime text sample description (
stsdtextentry): a QuickTime text track (media typetext) carries atextsample entry defining how its text samples are drawn. After the shared 8-byte preamble: a 32-bitdisplay_flagsword (Don't-auto-scale0x02/ Use-movie-bg0x08/ Scroll-in0x20/ Scroll-out0x40/ Horizontal-scroll0x80/ Reverse-scroll0x100/ Continuous-scroll0x200/ Drop-shadow0x1000/ Anti-alias0x2000/ Key-text0x4000), a signed 32-bittext_justification(0 left / 1 centered / -1 right), a 48-bitbackground_color, a 64-bitdefault_text_box(top/left/bottom/ right), afont_number,font_facestyle, a 48-bitforeground_color, and a Pascal-stringtext_name(the font name). The structured fields are surfaced onparams.optionsastext_display_flags/text_justification/text_background_color/text_foreground_color/text_default_text_box/text_font_number/text_font_face/text_font_name(font name omitted when empty), while the raw post-preamble bytes remain available asparams.extradata. Public byte-exactdemux::parse_text_sample_entry_box/demux::build_text_sample_entryround-trip the entry. Absent a structuredtextentry, none of the keys are emitted. -
QuickTime track load settings (
trak/load): a QuickTimeloadatom indicates how a reader should preload and play the track. Its plain-Box 16-byte body carriespreload_start_time,preload_duration(-1= to end of track),preload_flags(preload-always1/ preload-if- enabled2), anddefault_hints(double-buffer0x0020/ high-quality0x0100), all in the movie timescale. TheLoadSettingsBoxrecord'spreload_always()/preload_if_enabled()/double_buffer()/high_quality()helpers decode the flag / hint words. Surfaced onparams.optionsasload_preload_start_time/load_preload_duration/load_preload_flags/load_default_hints(decimal) plus the decoded booleansload_preload_always/load_preload_if_enabled/load_double_buffer/load_high_quality. Public byte-exactdemux::parse_load_settings_box/demux::build_load_settings_boxround-trip the atom. Absentload, none of the keys are emitted. -
Hint sample entries (ISO/IEC 14496-12 §9.1.2 / §9.3.3.2 / §9.4): a hint track's
stsdentry is decoded when it is an RTP server (rtp), SRTP (srtp), RTP reception (rrtp), or RTCP reception (rtcp) entry — a shared body (maxpacketsize+ theadditionaldataboxestimstimescale,tsrotime offset,snrosequence offset, and forsrtpthesrppSRTPProcessBox) surfaced asrtp_hint_format,rtp_hint_max_packet_size,rtp_hint_timescale,rtp_hint_time_offset,rtp_hint_sequence_offset, andrtp_hint_srtp— or an MPEG-2 TS server (sm2t) / reception (rm2t) entry, whose per-TS-packet wrapper byte counts and precomputed-only flag surface asm2t_hint_format,m2t_hint_preceding_bytes,m2t_hint_trailing_bytes, andm2t_hint_precomputed. The full typed records (with parse + build round-trip) live in thehintmodule. -
Hint statistics (ISO/IEC 14496-12 §9.1.5,
hinf): a hint track'sudta/hinfHint Statistics Box is decoded into delivery totals — thetrpy/nump/tpyl(andtotl/npck/tpayu32 variants) byte/packet counts,dmed/dimm/drepmedia/immediate/repeated byte totals,maxrmax-rate windows,pmaxlargest packet, andpaytpayload-ID rtpmap entries — surfaced ashinf_bytes_sent,hinf_packets_sent,hinf_payload_bytes,hinf_media_bytes,hinf_immediate_bytes,hinf_repeated_bytes,hinf_largest_packet,hinf_maxr_count, andhinf_payload_count(absent sub-boxes omit their keys). Full typed record + parse/build in thehintmodule. -
FD (File Delivery) item information (ISO/IEC 14496-12 §8.13): a file-level
meta'sfiinFDItemInformationBox — thepaenpartition entries (fparFile Partition + optionalfecrFEC Reservoir +fireFile Reservoir boxes), plus optionalsegrFD session-group andgitngroup-name boxes — is parsed intoMetaItems::fiinand summarised asmeta_fiin_partitions/meta_fiin_session_groups/meta_fiin_group_names. The full typed records (with parse + build round-trip, incl. thefeciFEC Information Box, §9.2.4.7) live in thefdmodule. -
Additional metadata container (ISO/IEC 14496-12 §8.11.7 / §8.11.8,
meco/mere): a file-levelmecoAdditionalMetadataContainerBox is parsed into aMecoBoxcarrying its additionalmetaboxes (each a fully-decodedMetaItemswith a distinct handler type) plus themereMetaboxRelationBoxes describing how same-levelmetaboxes relate, surfaced asmeco_meta_count/meco_meta_<n>/meco_relation_count/meco_relation_<n>. Reachable viaMp4Demuxer::meco(). -
Composition-to-decode (ISO/IEC 14496-12 §8.6.1.4,
cslg): a track'sstbl/cslgCompositionToDecodeBox documents the composition↔decode timeline relationship implied by a signed (v1)ctts— the DTS shift that guaranteesCTS ≥ DTS, the least / greatest composition offsets, and the composition start / end times. Both v0 (32-bit) and v1 (64-bit) layouts are read (widened toi64). The five fields are surfaced onparams.optionsascslg_composition_to_dts_shift,cslg_least_decode_to_display_delta,cslg_greatest_decode_to_display_delta,cslg_composition_start_time, andcslg_composition_end_time(decimal strings in the media timescale;composition_end_time = 0means "unknown" per §8.6.1.4.3). Absentcslg, none of the keys are emitted. -
Shadow sync samples (ISO/IEC 14496-12 §8.6.3,
stsh): a track'sstbl/stshShadowSyncSampleBox 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 onparams.optionsasstsh_<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. Absentstsh, none of the keys are emitted. -
Sample degradation priority (ISO/IEC 14496-12 §8.5.3,
stdp): a track'sstbl/stdpDegradationPriorityBox is an optional per-sample table of 16-bitpriorityvalues, one per sample (thesample_countis implicit fromstsz/stz2, mirroringsdtp). The exact meaning and value range ofpriorityare owned by derived specifications (§8.5.3.1 / §8.5.3.3 "Specifications derived from this define the exact meaning and acceptable range of thepriorityfield"), so the container preserves the raw u16 without interpreting it. The demuxer surfaces a small summary onparams.optionsrather 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), andstdp_sum(a u64 — a u16 priority × 2^32 samples fits comfortably; consumers compute the mean fromsum / count). A renderer dropping samples under bitrate / CPU pressure consults the carrying spec for the priority ordering. Absentstdp, none of the keys are emitted. -
Sample padding bits (ISO/IEC 14496-12 §8.7.6,
padb): a track'sstbl/padbPaddingBitsBox is an optional table recording, per sample, how many bits at the tail of the sample's last byte are padding (a value in0..=7). On the wire two samples share a byte: each nibble isbit(1) reserved=0; bit(3) pad. Unlikesdtp/stdpthe box declares its ownsample_count(independent ofstsz/stz2), so the table is self-contained — the trailing unused nibble whensample_countis 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 onparams.optionsrather 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-zeropadbmeans the track is byte-aligned), andpadb_hist(eight-bucket histogramn0:n1:n2:n3:n4:n5:n6:n7wherenKis the number of samples whose pad count isK, decimal). The histogram fully captures the value distribution in one short string; consumers can recover any aggregate without scanning. Absentpadb, none of the keys are emitted. -
Sample dependency hints (ISO/IEC 14496-12 §8.6.4,
sdtp): a track'sstbl/sdtpSampleDependencyTypeBox 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 (thesample_countis implicit fromstsz/stz2). The table feeds trick-mode playback (drop disposable samples on fast-forward) and refines random-access roll-forward (a sample markedsample_depends_on = 2is an I-picture without needing thestssto mark it). The raw per-sample 2-bit values are decoded and stored on the track; the demuxer surfaces a small summary onparams.optionsas five keys —sdtp_count,sdtp_leading_count(samples withis_leading ∈ {1, 3}),sdtp_independent_count(samples withsample_depends_on = 2),sdtp_disposable_count(samples withsample_is_depended_on = 2), andsdtp_redundant_count(samples withsample_has_redundancy = 1). Absentsdtp, none of the keys are emitted (the demuxer falls back tostssfor keyframe detection, as before). -
Multiple sample descriptions (ISO/IEC 14496-12 §8.5.2,
stsd): everySampleEntryin the box is now parsed, not just the first. Entry[0]still drives active decode dispatch (its FourCC is the stream's resolvedcodec), 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-chunkstsc.sample_description_index ≥ 2or a fragment'stfhd/trexsample_description_index— are recorded with their FourCC and §8.5.2.2data_reference_index. Whenentry_count > 1the demuxer surfaces them onparams.options:stsd_count(total entries) plusstsd_<n>for each entry, where<n>is the 1-based index matching the spec'ssample_description_index(so astsc/tfhdindex valueklooks up directly asstsd_<k>). Each value is<fourcc> dref=<data_reference_index>. A single-description track (the overwhelming common case) emits none of these keys — the activecodecalready carries that information. A declaredentry_countlarger 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'sminf/dinf/drefDataReferenceBox is the table of media-data locations that every sample entry's 1-baseddata_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. EachDataEntryBoxis aurl(with the §8.7.2.3 self-contained flag, or a NULL-terminatedlocationURL when external) or aurn(a NULL-terminatednameplus an optionallocation); a non-url/urnchild is non-conforming and dropped. The overwhelmingly common single self-containedurlcase is surfaced compactly onparams.optionsasdref_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), anddref_<n>(1-based, matchingdata_reference_index) =<kind> self=<true|false>[ name=<urn>] [ loc=<url>]. A forgedentry_countcannot 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 malformeddrefis dropped without failing theminfparse (the file still demuxes against the single-source default). Absent / unparseabledref, none of the keys are emitted. -
Sample groups (ISO/IEC 14496-12 §8.9,
sbgp+sgpd): a track'sstbl/sbgp(SampleToGroupBox §8.9.2) run-length map andstbl/sgpd(SampleGroupDescriptionBox §8.9.3) per-group entries are parsed. Several of each are accumulated — one pair pergrouping_type(roll,rap,sync,alst,prol, …). Eachsbgpis surfaced onparams.optionsassbgp_<n>(0-based encounter index): the grouping type, an optionalparam=<P>(v1grouping_type_parameter), then space-separatedcount:indexrun-length pairs (group_description_index0 = "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). Eachsgpdis surfaced assgpd_<n>: the grouping type, an optionaldefault=<D>(v2default_sample_description_index), then the per-group entry payloads as lowercase hex. Entry sizing honours §8.9.3.2 (v1 fixeddefault_length, v1 per-entrydescription_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 thegrouping_typesemantics. Absent both boxes, none of the keys are emitted. -
Typed §10 sample-group description entries (
sample_group_entriesmodule): a typed interpretation layer over the opaquesgpdentry blobs above, covering everygrouping_typethe base ISO/IEC 14496-12 standard defines in §10 —roll/prolRollRecoveryEntry(signedroll_distance, §10.1.1),rashRateShareEntry(the single- and multi-operation-point wire shapes keyed byoperation_point_count, §10.2.2),alstAlternativeStartupEntry(the variableroll_count-longsample_offset[]array plus the optional output-rate tail read "until the end of the structure", §10.3.2),rapVisualRandomAccessEntry(num_leading_samples_known+ 7-bitnum_leading_samples, §10.4.2),teleTemporalLevelEntry(level_independently_decodable, §10.5.2), andsapSAPEntry(dependent_flag+ 4-bitSAP_type, §10.6.2). Each type has aparse_*decoder and abuild_*builder that round-trip byte-exact;decode_sample_group_entry(grouping_type, blob)is the single typed dispatch point — it routes a(grouping_type, blob)pair (the same per-entry bytes thesgpd_<n>metadata renders as hex) to the matching parser and returnsOk(Some(SampleGroupEntry)),Ok(None)for a grouping type the base spec does not define (a codec-binding type likesync, or the CENCseig— handled bycenc::parse_seig), so a caller keeps the verbatim blob for those, andErr(_)only when a recognised §10 type carries a malformed payload. Bit-packed single-byte entries ignore reserved bits on read (§10.6.3 "allow and ignore reserved") and mask sub-byte fields to their declared widths on build; the parsers tolerate trailing bytes from a future edition except where the tail carries meaning (alst's output-rate pieces,rash's point list). This mirrors theseigprecedent — typed grouping-type interpretation layered above the container boundary, leaving the demuxer's opaque-blobsgpd_<n>surface unchanged. -
Compact sample-to-group (ISO/IEC 14496-12:2020 §8.9.5,
csgp): the compact alternative tosbgpis parsed. TheFullBox.flagsfield is overloaded to carry three 2-bit width selectors (index_size_code,count_size_code,pattern_size_code, each mapped to a field width viawidth = 4 << code→ 4/8/16/32 bits) plus agrouping_type_parameter_presentbit (bit 6) and anindex_msb_indicates_fragment_local_descriptionbit (bit 7). The body's variable-width fields are bit-packed (no byte alignment between 4-/8-bit fields) and read MSB-first. The §8.9.5 constraint thatpattern_size_codeandcount_size_codemust agree on whether the 4-bit width (code 0) is used is enforced on both sides: a box that mixes 4-bit and non-4-bit between the two is rejected by the parser, and the builder promotes a 4-bit code off code 0 if the other would be wider so it never emits the invalid mix. The decoded body is apattern_count-long array of(pattern_length, sample_count)followed by the per-patternsample_group_description_indexrun. When bit 7 is set (legal only in atraf), the most-significant bit of each index — at the field's on-disk width — is a fragment-local-vs-globalsgpdsource selector rather than part of the index value;CsgpBoxrecords the flag and the index field width (index_field_bits), andCsgpPattern::resolve_index(n, flag, bits)splits a stored index into aCsgpResolvedIndex { fragment_local, value }while the raw indices stay verbatim. Eachcsgpis accumulated and surfaced onparams.optionsascsgp_<n>: the grouping type, an optionalparam=<P>, an optionalfraglocalmarker (bit 7 set), then onecount*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 sharesgrouping_typewith the matchingsgpd_<m>. Absentcsgp, no keys are emitted. The structured record is reachable via the publicoxideav_mp4::demux::parse_csgp_box(&[u8])entry point (typedCsgpBox/CsgpPattern/CsgpResolvedIndex) for tooling holding the box body; the write side issample_groups::build_csgp(see Muxer below).CsgpBox::resolve_samples(total_samples)materialises the compact patterns into one resolved index per sample in decoding order — the same per-sample viewsbgpalready gives: each pattern's index run is cycled across itssample_count(the cycle may end mid-pattern), trailing samples not covered by any pattern take thesgpddefault (surfaced asvalue == 0, the "no group" sentinel the caller swaps for the box'sdefault_group_description_index), asample_countsum that overruns the track is clamped tototal_samples, and every emitted index is split through the bit-7 fragment-local-vs-global MSB convention. An empty-index pattern maps nothing rather than panicking. -
Sub-sample information (ISO/IEC 14496-12 §8.7.7,
subs): a track'sstbl/subsSubSampleInformationBox 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 storessubsample_sizeas 16-bit; version 1 widens it to 32-bit (both layouts are read and normalised tou32).flagsdistinguishes co-residentsubsboxes with different per-codec semantics (§8.7.7.1). The container preserves the carried codec's interpretation ofsubsample_priority/discardable/codec_specific_parametersverbatim — those small ints are opaque at this layer. Eachsubsencountered on a track is surfaced onparams.optionsassubs_<n>(0-based encounter index); the value starts with"v<version> flags=<f>"and is followed by one space-separateddelta=<d>[:size,priority,discardable,csp[;...]]block per entry (decimal for everything exceptcsp, which is lowercase 8-digit hex). The trailing colon and per-sub-sample list are omitted when an entry hassubsample_count = 0. Absentsubs, no keys are emitted. The typed records (demux::SubsBox/SubsEntry/SubSampleEntry) are public, with ademux::parse_subs_boxdecoder and a byte-exactdemux::build_subs_boxbuilder. -
Sample auxiliary information sizes + offsets (ISO/IEC 14496-12 §8.7.8–9,
saiz+saio): a track'sstbl/saiz+stbl/saiopair 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: whensencis absent, per-sample IVs + subsample maps (ISO/IEC 23001-7) are carried as an auxiliary-information stream of typecenc/cbc1/cens/cbcs, and thesaiz+saiopair points to the bytes in the mdat. Both versions (v0 32-bit / v1 64-bitsaiooffsets) are read; the optionalaux_info_typeblock (gated byflags & 1) is captured when present and surfaced asNone/Noneotherwise so callers can apply §8.7.8.3's implied-value rule against the sample-entry FourCC / sinf scheme themselves.saiz.default_sample_info_sizenon-zero collapses the per-sample table; v1saiooffsets that exceed 32 bits round-trip asu64. Eachsaiz/saioencountered on a track is surfaced onparams.optionsassaiz_<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 FullBoxflags & 1bit was zero on disk; thesizes=block is omitted whendefault_sample_info_size != 0. Offsets are decimal; instblthey are absolute file positions. For movie-fragment carriage (§8.8.14) the parser also surfaces a per-trafsummary asfrag_sai_<n>throughDemuxer::metadata()("track=<t> seq=<s> saiz=<n> saio=<m>"), and the structured per-fragment records (with offsets preserved astfhd.base_data_offset-relative per §8.8.14, plus the resolvedbase_data_offsetitself on eachSaiRecord) are reachable through the publicMp4Demuxer::sai_records()accessor on the demuxer (downcast). For thesenc-less CENC carriage — aux info in the mdat located only bysaiz+saio—Mp4Demuxer::resolve_sai_aux_info()fetches each fragment's contiguous run from the input, parses the per-sample cells (IV of the track'stencwidth, thenu16subsample count +(u16, u32)runs when the cell exceeds the IV; each cell must consume its declared size exactly), and appends synthesised records tosenc_records()— so a decryption layer replayingsenc_recordsconsumes both carriages identically. Conservative skips: fragments that already have asenc(parsed boxes stay authoritative), tracks withouttenc, non-§10 explicitaux_info_types, multi-offsetsaio(the per-trun split), totals over 16 MiB, runs past EOF, and cells that don't parse. Absentsaiz/saio, no keys are emitted andsai_records()is empty. The typed records (demux::SaizBox/SaioBox) are public, withdemux::parse_saiz_box/parse_saio_boxdecoders and byte-exactdemux::build_saiz_box/build_saio_boxbuilders. -
Fragment-local sample groups (ISO/IEC 14496-12 §8.9.2 / §8.9.3 / §8.9.5 inside
traf): a movie fragment may carry its ownsgpddescription table plussbgp/csgpper-sample maps, so a fragment can declare fragment-local group descriptions and map its samples into either those or thetrak-level (global) ones. Thecsgpbit-7index_msb_indicates_fragment_local_descriptionflag — whose whole purpose is thistrafcase (§8.9.5 makes it legal only here) — then selects the source per index. Eachtraf's sample-group boxes are parsed verbatim (the sameparse_sbgp/parse_sgpd/parse_csgpthestbl-level surface uses) and collected into aTrafSampleGroupRecordkeyed by(track_idx, moof_sequence). A per-trafsummary is surfaced throughDemuxer::metadata()asfrag_sample_group_<n>("track=<t> seq=<s> sgpd=<n> sbgp=<m> csgp=<k>"); the structured records (typedSbgpBox/SgpdBox/CsgpBox, on-wire values preserved — the most common use is CENCseigkey rotation across a fragment's samples) are reachable through the publicMp4Demuxer::traf_sample_groups()accessor (downcast). The boxes do not disturb thetrunsample walk. Absent fragment-local sample groups, no keys are emitted andtraf_sample_groups()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). Eachprftencountered during the top-level walk is surfaced onDemuxer::metadata()asprft_<n>(0-based file order); the value is three space-separated decimal integers"reference_track_ID ntp_timestamp media_time". Both v0 (32-bitmedia_time) and v1 (64-bitmedia_time) layouts are read; v1media_timeis widened tou64so callers see one type regardless. The 24-bit FullBoxflagsfield —0in the 2015 edition, named annotation bits in the 2022 edition (encoder_input_output0x1,finalization_time0x2,file_write_time0x4,arbitrary_association0x8, the combinedrealtime_offsetmask 0x18) describing what the NTP time represents — is captured verbatim and, when any bit is set, appended to the surfaced value as trailing space-separated name tokens after the three integers (the integer prefix stays backward-compatible). Absentprft, no keys are emitted. The structured record is also reachable via the publicoxideav_mp4::demux::parse_prft_box(&[u8])entry point for tooling that wants the typedPrftRecord(reference_track_id,ntp_timestamp,media_time,version,flags) directly, withis_encoder_input_output()/is_finalization_time()/is_file_write_time()/is_arbitrary_association()/is_realtime_offset()accessors for the 2022 flag bits. A standalonedemux::build_prft_box(&PrftRecord)builder (byte-exact inverse, v0/v1) complements the fragmented muxer's per-segmentprftemission. -
Progressive download information (ISO/IEC 14496-12 §8.1.3,
pdin): a top-levelFullBox(version = 0, flags = 0)whose body is a sequence of(rate, initial_delay)u32 pairs (big-endian) — for each effective downloadrate(bytes/second) the box hints the suggested initial playbackinitial_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 onDemuxer::metadata()aspdin_count(total number of pairs, decimal) pluspdin_<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") emitspdin_count = 0and nopdin_<n>keys. Absentpdin, 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 publicoxideav_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 viaMp4Demuxer::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), matchingparse_padb/parse_stdpposture for FullBox version-bit slips. The write side isdemux::build_pdin_box(&PdinRecord), the byte-exact inverse. -
Level assignment (ISO/IEC 14496-12 §8.8.13,
leva): aFullBox(0, 0)insidemvexwhose body opens withunsigned int(8) level_countand is followed bylevel_countper-level entries. Each entry maps one fragment level to atrack_id(u32) with apadding_flag(top bit of the packed §8.8.13.2 byte) and a 7-bitassignment_typediscriminator: 0 selects a sample grouping bygrouping_type(u32), 1 addsgrouping_type_parameter(u32), 2/3 carry no further tail (level by track / by track-subsegment), 4 names asub_track_id(u32) for sub-track scope. Levels specify subsets of subsequent movie fragments — samples mapped to levelnmay depend on any levelm ≤ nbut never on levelp > 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 asidx(§8.16.3) is a "fraction" and data for each level shall appear contiguously within it in increasing level order;padding_flag = 1declares that a conforming fraction can be formed by concatenating any positive integer number of levels and padding the lastmdatto its declared size. §8.8.13.1 fixes quantity at zero or one per file; themvexwalker captures the first instance and ignores any subsequent copies. The parsed table is surfaced onDemuxer::metadata()asleva_count(total entry count, decimal) plusleva_<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 theirassignment_typevariant uses them. The structured record is also reachable via the publicoxideav_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 viaMp4Demuxer::leva_entries() -> Option<&[LevaEntry]>(downcast) for callers holding the demuxer trait object. A truncated entry header (less than 5 bytes fortrack_id+ padding/assignment) or a truncated variant tail is rejected at parse time — a partial table would lie about which levels exist. A malformedlevareachingparse_mvexis dropped silently so a producer slip cannot brickopen()(the box is informational — playback proceeds without level scoping). The §8.8.13.3 minimumlevel_count ≥ 2is not enforced (the demuxer carries whatever the producer wrote so a validator can flag the short table). Reservedassignment_typevalues (> 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, matchingparse_pdin/parse_padb/parse_stdpposture for FullBox version-bit slips. -
Track extension properties (ISO/IEC 14496-12 §8.8.15,
trep): aFullBox(0, 0)insidemvexwhose body opens withunsigned int(32) track_idand is followed by "any number of boxes" (§8.8.15.2) — e.g. anassp(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 permvex(zero or one per track); themvexwalker collects every instance in file order. The nested child boxes are recorded by type + payload length (TrepChild), and the one child the base spec defines here —assp(Alternative Startup Sequence Properties Box, §8.8.16) — is additionally decoded into a typedAsspRecordonTrepChild::assp; any other child stays opaque, leaving its semantics to a downstream consumer that recognises it. Eachtrepis surfaced onDemuxer::metadata()astrep_<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. A cleanly-decodedasspchild appends itsmin_initial_alt_startupoffset(s) after theassptoken:assp(off=<i>)for v0, orassp(<grouping_type_parameter>:<offset> ...)for v1. The structured record is reachable via the publicoxideav_mp4::demux::parse_trep_box(&[u8])entry point (for tooling holding the box's payload bytes) and viaMp4Demuxer::treps() -> &[TrepRecord](downcast). Atrepshorter than its 4-byte FullBox preamble or with a truncatedtrack_idis rejected at parse time; a malformedtrepreachingparse_mvexis dropped silently so a producer slip cannot brickopen()(the box is informational). A child box whose declared size overruns the remainingtrepbody 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-bitlargesizechild form (size == 1) is read via its 16-byte header. The version byte is tolerated even if non-zero, matching theparse_levaposture. -
Alternative startup sequence properties (ISO/IEC 14496-12 §8.8.16,
assp): aFullBox('assp', version, 0)nested inside atrep(§8.8.15) that indicates the properties of the alternative startup sequence (alst, §10.3.2) sample groups in the subsequent track fragments of thetrep's track. §8.8.16.1 ties the box version to thesbgpversion used for thealstgrouping: version 0 (one implied entry — a signedmin_initial_alt_startup_offset, nogrouping_type_parameter) when thealstsbgpis v0; version 1 (anum_entries-long list of(grouping_type_parameter, min_initial_alt_startup_offset)pairs, one per alternative grouping) when it is v1.min_initial_alt_startup_offsetis a lower bound onsample_offset[1]of the referredalstdescription entries: no value shall be smaller (§8.8.16.3). The demuxer decodes theasspchild of any parsedtrepinto the typedAsspRecord(onTrepChild::assp) and appends the offset(s) to the flattrep_<n>metadata after theassptoken (assp(off=<i>)for v0;assp(<gtp>:<off> ...)for v1). The structured record is also reachable via the publicoxideav_mp4::demux::parse_assp_box(&[u8])entry point (typedAsspRecord/AsspEntry) for tooling holding the box body. The matching write side isoxideav_mp4::demux::build_assp_box(&AsspRecord)— the byte-exact inverse ofparse_assp_box, emitting a complete[size]['assp']box for a muxer placing it inside atrep; it rejects records that would not round-trip (a v0 entry carrying agrouping_type_parameter, a v0 record without exactly one entry, or a version other than 0/1). Only versions 0 and 1 are defined; a truncated body, a v1num_entriesoverrunning the body, or an unsupported version are rejected at parse time (a malformedasspthen leavesTrepChild::assp = Nonewhile still contributing its type + length). -
Subsegment index (ISO/IEC 14496-12 §8.16.4,
ssix): the top-levelFullBox(version = 0, flags = 0)that maps levels (as assigned by the §8.8.13levabox above) to byte ranges of the subsegments indexed by the immediately precedingsidx(§8.16.4.1 placement: zero or one per leaf-onlysidx, as its next box;subsegment_countshall equal that sidx'sreference_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_countu32, then per subsegment arange_countu32 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 intoSsixRecord→SsixSubsegment→SsixRange. Everyssixmet during the top-level walk is collected in file order and surfaced onDemuxer::metadata()asssix_<n>="<subsegment_count> <total_range_count>"(shape summary — the full table is reachable via the publicoxideav_mp4::demux::parse_ssix_box(&[u8])entry point or theMp4Demuxer::ssixes()accessor, downcast). Absentssix, no keys are emitted; likeleva, a malformed instance is dropped without failingopen()(the box is informational — this demuxer seeks viasidx/tfraand 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-siderange_count ≥ 2minimum is not enforced on read. The matchingbuild_ssix_box(&SsixRecord)builder emits a complete[size]['ssix']box for segment-index emitters pairing it with asidx, rejectingrange_sizevalues above the 24-bit wire field's 0xFF_FFFF ceiling instead of silently masking them. -
Ambient viewing environment (ISO/IEC 14496-12 post-2015 addition,
amve): a track'sVisualSampleEntry(avc1/hvc1/av01…) may carry anAmbientViewingEnvironmentBox— a plainBox(not aFullBox; no version/flags byte) with a fixed 8-byte body signalling the nominal ambient viewing environment for the display of the video. It is the file-format carriage of the same three syntax elements (with the same units and ranges) as theambient_viewing_environmentSEI message and the ISO/IEC 23091-3 (CICP) ambient-viewing-environment parameters:ambient_illuminance(0.0001 lux per unit),ambient_light_x/ambient_light_y(0.00002 CIE 1931 chromaticity per unit). The three raw integers are surfaced verbatim onparams.optionsasamve_ambient_illuminance,amve_ambient_light_x, andamve_ambient_light_y(decimal) — a downstream HDR pipeline applies the unit scaling and can populate the matching SEI field-for-field with no conversion. The firstamvemet across a track's sample entries wins (one nominal environment per track); a body shorter than the fixed 8 bytes is dropped without aborting the open (the box is informational), and trailing bytes beyond byte 8 (reserved for a future edition) are ignored. The structured record is reachable via the publicoxideav_mp4::demux::parse_amve_box(&[u8])entry point (typedAmveRecordwithambient_illuminance,ambient_light_x,ambient_light_y) for tooling holding the box body. Absentamve, none of the keys are emitted. Distinct from and complementary to the mastering-display (mdcv) / content-light-level (clli) metadata, which describe the content's mastering environment rather than the viewer's ambient one. -
Stereo video (ISO/IEC 14496-12 §8.15.4.2,
stvi): a video track that uses the restricted-schemestviSchemeType (stereoscopic video, §8.15.4.1) carries aStereoVideoBoxinside its sample-entrysinf/schi, indicating that decoded frames hold either two spatially packed constituent frames forming a stereo pair (frame packing) or one of two views of a stereo pair (left / right in different tracks). TheFullBox(0, 0)body is a packedint(30) reserved+int(2) single_view_allowed, thenint(32) stereo_scheme,int(32) length, theint(8)[length] stereo_indication_typearray, and optional trailing boxes.single_view_allowedsignals which view(s) may be shown on a monoscopic single-view display (bit 0 = right view, bit 1 = left view);stereo_schemeselects the arrangement vocabulary (1 = the ISO/IEC 14496-10 frame-packing-arrangement SEI scheme, 2 = the ISO/IEC 13818-2 Annex L arrangement type, 3 = the ISO/IEC 23000-11 scheme);stereo_indication_typeis the scheme-specific arrangement code (preserved verbatim — its detailed meaning is owned by the named derived spec). The box is recovered from theschiof both encrypted (enc*) and non-encrypted (sample-entry-residentsinf) video entries; the first one encountered across a track's sample entries wins. Surfaced onparams.optionsasstvi_single_view_allowedandstvi_stereo_scheme(decimal) plusstvi_indication(the rawstereo_indication_typebytes as lowercase hex, omitted when the indication is empty). The first 32-bit word's 30 reserved high bits are masked off so a producer slip on them does not corruptsingle_view_allowed; alengththat overruns the body is rejected rather than reading past the end, and trailing optionalany_boxbytes after the array are ignored. The structured record is reachable via the publicoxideav_mp4::demux::parse_stvi_box(&[u8])entry point (typedStviRecordwithright_view_monoscopic_allowed()/left_view_monoscopic_allowed()accessors) for tooling holding the box body. Absentstvi, none of the keys are emitted. The box version is tolerated even if non-zero (the layout is unambiguous), matching thepdin/padbposture. The matching write side isoxideav_mp4::demux::build_stvi_box(&StviRecord)— the byte-exact inverse ofparse_stvi_box, emitting a complete[size]['stvi']box with the §8.15.4.2.2FullBox(0, 0)body for a restricted-scheme (stviSchemeType) muxer placing it inside a sample entry'ssinf/schi. -
Bit rate (ISO/IEC 14496-12 §8.5.2,
btrt): aSampleEntry(video / audio / metadata / text) may carry, optionally, aBitRateBox— a plainBox(no FullBox version/flags) with a fixed 12-byte body of three big-endianu32s:bufferSizeDB(size of the decoding buffer for the elementary stream, in bytes),maxBitrate(maximum rate in bits/second over any one-second window), andavgBitrate(average rate in bits/second over the whole presentation). It is the codec-agnostic carriage of the same three bandwidth quantities the MPEG-4esdsDecoderConfigDescriptorcarries, usable by sample entries that have noesds(avc1/hvc1/av01video,Opus/fLaCaudio, …). The three raw integers are surfaced verbatim onparams.optionsasbtrt_buffer_size_db,btrt_max_bitrate, andbtrt_avg_bitrate(decimal) — an ABR packager validating a producer's declared rates, or a player sizing its receive buffer, reads them directly. The firstbtrtmet on the active sample entry ([0]) wins (one per elementary stream); a body shorter than the fixed 12 bytes is dropped without aborting the open (the box is informational), and trailing bytes beyond byte 12 are ignored. The structured record is reachable via the publicoxideav_mp4::demux::parse_btrt_box(&[u8])entry point (typedBtrtRecordwithbuffer_size_db,max_bitrate,avg_bitrate) for tooling holding the box body. Absentbtrt, none of the keys are emitted. -
Picture geometry + colour (ISO/IEC 14496-12 §12.1.4–5,
pasp/clap/colr): aVisualSampleEntrymay carry aPixelAspectRatioBox(pasp,hSpacing/vSpacing), aCleanApertureBox(clap, the eight-u32 width/height/horiz-off/vert-offN/Dfractions defining the active picture region), and one or moreColourInformationBoxes (colr). Forcolrthe three base-spec colour types are modelled:nclx(on-screen colours —colour_primaries/transfer/matrix16-bit codes from ISO/IEC 23091-2 +full_range_flag),rICC/prof(restricted / unrestricted ICC profile, raw bytes preserved), and anOtherfall-back keeping an unknown type + payload. Surfaced onparams.optionsaspasp_h_spacing/pasp_v_spacing;clap_width/clap_height/clap_horiz_off/clap_vert_off(each"<N>/<D>"); andcolr_typeplus, fornclx,colr_primaries/colr_transfer/colr_matrix/colr_full_range(orcolr_icc_lenfor an ICC profile). The first of each on the active entry wins (the spec listscolrboxes most-accurate-first). Public standalone parsersdemux::parse_pasp_box/parse_clap_box/parse_colr_boxdecode each box body, and write counterpartsdemux::build_pasp_box/build_clap_box/build_colr_boxemit each box byte-exact (the inverse of the parsers). A renderer reads the display aspect / crop rectangle and an HDR pipeline reads the colour signalling straight from the container. Absent the box, no keys are emitted.
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→sowtpcm_mulaw/pcm_alaw→ulaw/alaw(plain 8-bit AudioSampleEntry)flac→fLaCwithdfLaconfig (requires STREAMINFO extradata)aac→mp4awithesds(requires AudioSpecificConfig extradata)mp3→mp4awithesdsobjectTypeIndication = 0x6B(MPEG-1 audio; no DecoderSpecificInfo — the demuxer's OTI refinement resolves it back tomp3)opus→OpuswithdOps(extradata is the demuxer's surfaced form — the dOps body behind anOpusHeadmagic; the magic is stripped on write and re-prepended on read, byte-exact both ways)alac→alacwithalacmagic-cookie config child (extradata is the cookie; the FullBox version/flags word is added on write / stripped on read)ac3→ac-3withdac3(extradata verbatim as the config-box body)eac3→ec-3withdec3(extradata verbatim)h264→avc1withavcC(requires AVCConfigurationRecord extradata)h265→hvc1withhvcC(requires HEVCDecoderConfigurationRecord)av1→av01withav1C(requires AV1CodecConfigurationRecord)vp9/vp8→vp09/vp08withvpcC(requires VPCodecConfigurationRecord, including its FullBox version/flags word — the demuxer's surfaced form)h263→s263, with ad263config child when extradata is present (opaque symmetric carriage; the demuxer surfaces thed263body)mjpeg→jpegmov_text→tx3g(3GPP TS 26.245 timed text) —texthandler +nmhdwebvtt→wvtt(BMFF §12.6.3.2 XMLSubtitleSampleEntry sibling) —subthandler +sthdttml→stpp(BMFF §12.6.3.2 XMLSubtitleSampleEntry) —subt+sthdsbtt→sbtt(BMFF §12.6.3.2 TextSubtitleSampleEntry) —subt+sthdstxt→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.
Mp4MuxerOptions::track_protection (a list of TrackProtection
records, each keyed by stream_index) wraps the target track's sample
entry into its ISO/IEC 14496-12 §8.12 protected form: the FourCC
becomes encv / enca / enct / encs per the stream's media type
and a sinf box — frma (original format) + schm (scheme_type,
scheme_version) + schi/tenc (ISO/IEC 23001-7 §8.2 track
defaults) — is appended to the entry body, serialised through the
public cenc::build_sinf_box. The (scheme_type, tenc) pair is
validated at open through CencSchemeDecision::new (a §10 scheme
pins the tenc version; pattern schemes need a non-zero pattern pair)
plus build_tenc_box's round-trip rules, so an envelope this crate's
own demuxer would reject cannot be emitted. Mp4MuxerOptions::pssh
emits moov-level pssh boxes (one per DRM system) after the trak
boxes — in fragmented mode, inside the init-segment moov after
mvex. On the plain (non-fragmented) muxer, protection is signalled only:
packet payloads are written as handed in, so the caller encrypts each
sample first (cenc_cipher::encrypt_sample_in_place with a decision
built from the same (scheme_type, tenc) pair) and carries per-sample
IVs / subsample maps through its own channel. On the fragmented muxer
the auxiliary information travels in the file — see the CENC
packaging subsection below. A demux of the produced file recovers the
original codec id via frma and surfaces protection_scheme /
cenc_default_* exactly as for foreign CENC files.
FragmentedMuxer::write_protected_packet(packet, SencSample) queues
each protected sample's ISO/IEC 23001-7 §7.1 auxiliary information
(per-sample IV + optional subsample map) alongside the payload,
validated at queue time (§9.2 IV-supply discipline against the track
tenc, §9.5.1 subsample totals, §7.2.3 all-samples-or-none per
fragment). Each flush emits, per traf: a §7.2 senc
(UseSubSampleEncryption iff any sample maps subsamples), a §8.7.8
saiz (constant-size shortcut when sizes agree, aux_info_type
omitted per the §7.1 SHOULD), and a §8.7.9 saio whose single offset
is patched to the moof-relative position of the first senc entry
(§8.8.14 default-base-is-moof) — so every fragment is independently
decryptable (the §7.2.1 DASH Media Segment posture). Constant-IV
tracks without subsample maps omit all three boxes (§7.1 empty-aux
rule).
write_protected_packet_grouped(packet, senc, Option<SeigEntry>) adds
key rotation: per-sample §6 seig overrides are deduplicated (in
first-use order) into a fragment-local sgpd('seig') plus an
sbgp('seig') run-length map using the §8.9.4 fragment-local index
numbering (0x10001-based; 0 = tenc defaults). The write side is
cenc::build_seig_entry, the byte-exact inverse of parse_seig.
cenc_packager::CencFragmentPackager is the plaintext-in driver on
top: it owns the KID-keyed AES-128 content-key store, generates unique
per-sample IVs (a per-track 64-bit counter in IV bytes 0..8, so §9.3
CTR counter blocks never collide across samples under one key),
encrypts via encrypt_sample_in_place (write_packet for §9.4/§9.7
full-sample shapes, write_packet_with_subsamples for §9.5 NAL-video
shapes), and rotate_key(kid, key, constant_iv?) switches the active
key mid-stream through the seig channel (constant-IV schemes take a
fresh constant IV per key). set_next_segment_pssh scopes a licence
blob to the rotated fragment's moof (§8.1.1). The whole loop is
gated end-to-end: plaintext → packager → own demux
(demux::open_typed exposes the typed Mp4Demuxer with the
structured senc_records / sai_records / traf_sample_groups
accessors) → decrypt driven only by what the file says → byte-exact
plaintext, for every §10 scheme (cenc / cbc1 / cens / cbcs,
full-sample + subsample + pattern + constant-IV shapes) and across
key-rotation boundaries.
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).
Explicit per-track edit lists: Mp4MuxerOptions::track_edit_lists (a
list of TrackEditList records, each a stream_index plus
demux::EditListEntry entries) emits a caller-supplied edts/elst
verbatim, overriding the automatic start-delay form for that track —
and is written even when write_edit_list is false (the flag
governs only the automatic behaviour). Serialised through
demux::build_elst_box, so §8.6.6.3 round-trip violations (a
media_rate_integer outside {0, 1}, a final empty edit, a
media_time below the -1 sentinel, an empty entry list) and
out-of-range stream indices fail at open, never at write_trailer.
This is the write-side dual of Mp4Demuxer::edit_list: a remuxer
carries a source's elst across by feeding the demuxed slice straight
back in (mind the §8.6.6.3 unit split — segment_duration is movie
timescale, which this muxer writes as 1000). The same option drives
the fragmented muxer's init segment: the list is written into the
init-moov trak, where the §8.6.6.1 zero-segment_duration form is
the idiomatic choice ("the edit provides the offset for the movie and
subsequent movie fragments") — e.g. a media_time = 1024, duration = 0 entry cuts AAC priming ahead of the first presented CMAF sample
across every fragment.
Sample groups (sbgp / sgpd / csgp, ISO/IEC 14496-12 §8.9.2 /
§8.9.3 / §8.9.5) are emitted per-track when supplied via
Mp4MuxerOptions::track_sample_groups (a list of TrackSampleGroups,
each keyed by stream_index). Inside each track's stbl, after the
chunk-offset table, the muxer writes all sgpd description boxes first
(so the tables an index box references are declared ahead of it), then
sbgp run-length maps, then csgp compact maps. sbgp and csgp are
alternative encodings of the same per-sample → group mapping — §8.9.5
permits at most one form per grouping_type, so a caller picks one
form per grouping_type and never both, while distinct grouping_types
on the same track may each pick their own form. The csgp width codes
(index / count / pattern) are chosen automatically as the narrowest
§8.9.5 widths that hold every value and packed into FullBox.flags
alongside the grouping_type_parameter_present and
index_msb_indicates_fragment_local_description bits; the
pattern-vs-count 4-bit-agreement constraint is satisfied by promoting a
lone 4-bit code off code 0 if its partner is wider. Each emitted box
round-trips byte-exact back through the demuxer's sbgp_<n> /
sgpd_<n> / csgp_<n> metadata surface. The stateless byte builders
(sample_groups::build_sbgp / build_sgpd / build_csgp) are public
for callers assembling boxes outside the muxer.
Chunk offsets auto-promote from stco (32-bit) to co64 (64-bit) when
any offset exceeds 4 GiB. The mdat box header is 32-bit by default
(byte-identical to the historical output for sub-4-GiB files); set
Mp4MuxerOptions::large_mdat = true to reserve the ISO/IEC 14496-12
§4.2 extended-size header ([size=1]["mdat"][largesize:u64]) so the
media payload may exceed 4 GiB. Because the direct-write path streams
mdat to the output before its final size is known, the header form is
chosen up front from this flag; the faststart path (which buffers the
payload) additionally promotes automatically when the compact 32-bit
size would overflow. Without the flag, a direct-write mdat that
grows past 4 GiB fails at write_trailer with a message pointing at
large_mdat.
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.
One or more pssh (ProtectionSystemSpecificHeaderBox, ISO/IEC 23001-7
§8.1) boxes can be attached to the next fragment's moof via
FragmentedMuxer::set_next_segment_pssh(iter). Per §8.1.1 a pssh may
live in the moov or a moof; a box queued here is written after
that fragment's mfhd and before its traf boxes, scoping its DRM
header to the fragment's samples only (per-fragment key rotation). The
request is consumed per fragment; the demuxer surfaces the box on
Demuxer::metadata() as moof_pssh_<n> keyed by the fragment's
mfhd.sequence_number. This complements the init-segment-level
Mp4MuxerOptions::pssh boxes.
A prft (ProducerReferenceTimeBox, ISO/IEC 14496-12 §8.16.5) can be
attached to the next fragment via
FragmentedMuxer::set_next_segment_prft(reference_track_id, ntp_timestamp, media_time, flags) (or ..._prft_v1(..) to force the
64-bit-media_time version-1 layout). The box is written immediately
before that fragment's moof, after any sidx/styp — the position
§8.16.5 requires since prft relates to the next moof in bitstream
order — and its size is folded into the enclosing sidx
referenced_size so a byte-range fetch still reaches the moof. The
request is consumed per fragment (one prft per moof). ntp_timestamp
is NTP 64-bit 32.32 fixed-point (high 32 bits = seconds since 1900-01-01,
low 32 bits = fractional seconds); media_time is the same instant on
the reference track's media clock (decode time). The box version is
auto-selected (v0 u32 media_time when it fits, else v1 u64). This
is the write-side dual of the read-side parse_prft_box / PrftRecord.
A leva (LevelAssignmentBox, ISO/IEC 14496-12 §8.8.13) is emitted inside
the init-segment mvex (after the trex boxes) when
FragmentedOptions::levels is non-empty — one LevaEntry per declared
level, serialised through demux::build_leva_box. The level order in
that slice is the level number a sibling ssix refers to. The default
(empty) levels writes no leva and keeps the init segment
byte-identical to before. This is the write-side dual of the read-side
parse_leva_box / LevaRecord.
A trep (TrackExtensionPropertiesBox, ISO/IEC 14496-12 §8.8.15) is
emitted inside the init-segment mvex (after the trex boxes and after
any leva) for each record in FragmentedOptions::treps — one box per
record, in slice order, serialised through demux::build_trep_box. Each
record documents characteristics of one track in the subsequent
fragments; the one base-spec-defined child, assp (Alternative Startup
Sequence Properties Box, §8.8.16), is serialised from its typed
AsspRecord when present on a TrepChild. The default (empty) treps
writes no trep and keeps the init segment byte-identical to before.
This is the write-side dual of the read-side parse_trep_box /
TrepRecord (and parse_assp_box / AsspRecord).
Empty time (ISO/IEC 14496-12 §8.8.6.1 "empty inserts" — e.g. audio
silence suppression) is written via
FragmentedMuxer::insert_empty_time(stream_index, duration): pending
samples are flushed, then one standalone gap fragment
moof(mfhd + traf(tfhd + tfdt)) is emitted whose tfhd carries the
§8.8.7.1 duration-is-empty flag naming the interval (in the track's
media timescale) — per §8.8.8.1 the traf has no trun and no mdat
follows. The track's running decode time advances by the gap, so
subsequent fragments' tfdt (and the next sidx's earliest
presentation time) land after it. The gap fragment consumes a
mfhd.sequence_number but gets no styp / sidx / tfra entry (a
timeline artefact, not an addressable segment). Per §8.8.7.1
("It is an error to make a presentation that has both edit lists in
the Movie Box, and empty-duration fragments") the call is rejected
when the muxer was opened with explicit track_edit_lists. This is
the write-side dual of the demuxer's frag_empty_duration_<n> /
empty_duration_records() surface, and a mux → demux round-trip
recovers the gap position and length exactly.
A mehd (MovieExtendsHeaderBox, ISO/IEC 14496-12 §8.8.2) is written as
the first child of the init-segment mvex when
FragmentedOptions::write_mehd is set: a version-1 (64-bit) box is
reserved with fragment_duration = 0 at write_header and the eight
duration bytes are patched in place at write_trailer with the sealed
§8.8.2.3 total — "the duration of the longest track, including movie
fragments", each track's cumulative decode time rescaled media → movie
timescale with ceiling division. A sealed file then demuxes with an
authoritative Demuxer::duration_micros even though mvhd.duration
is 0, and the raw value surfaces as the mehd_fragment_duration
metadata key (the read side documented above). If the trailer is never
reached (truncated live capture) the placeholder 0 is the §8.8.2.1
"compute by examining each fragment" posture readers already handle.
Default false — no mehd, init segment byte-identical to before
(the right choice for live output where the init segment ships before
the stream ends).
An ssix (SubsegmentIndexBox, ISO/IEC 14496-12 §8.16.4) is emitted
immediately after each per-fragment sidx when FragmentedOptions:: emit_ssix is set (and emit_random_access_indexes is on, since the
ssix documents that sidx). Each emitted ssix carries
subsegment_count == 1 — matching the one-reference sidx — and
partitions the fragment's single subsegment (styp? + prft? + moof + mdat) into two level byte ranges that together cover every byte: the
leading styp? + prft? + moof metadata range and the trailing mdat
media range. The two level numbers are FragmentedOptions::ssix_levels
(default (1, 2)). The §8.16.4 range_count ≥ 2 minimum and the
"ranges partition the subsegment" requirement hold by construction; a
range exceeding the 24-bit range_size field is rejected rather than
truncated. This is the write-side dual of the read-side parse_ssix_box
/ SsixRecord, completing leva/ssix partial-subsegment-fetch
read+write symmetry.
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.
The compact form csgp (CompactSampleToGroupBox, ISO/IEC
14496-12:2020 §8.9.5) has a matching builder
oxideav_mp4::sample_groups::build_csgp(&CompactSampleToGroup),
the byte-exact inverse of the demuxer's parse_csgp (round-tripped in
tests through the public oxideav_mp4::demux::parse_csgp_box entry
point). CompactSampleToGroup carries a grouping_type, an optional
grouping_type_parameter, and a list of CompactSampleToGroupPattern
(each a sample_count plus a per-pattern indices run). The three
bit-field width codes (index_size_code / count_size_code /
pattern_size_code) are chosen automatically as the narrowest §8.9.5
widths — width = 4 << code, i.e. 4/8/16/32 bits — that hold every
value present, and packed into the overloaded FullBox.flags field
together with the grouping_type_parameter_present bit. From
pattern_count onward the (pattern_length, sample_count) array and
the flattened index run are bit-packed MSB-first with no byte alignment
between fields (the trailing partial byte is zero-padded). A
sample_group_description_index ≥ 0x8000_0000 (the fragment-local
high bit, §8.9.4) is written verbatim; the builder does not synthesise
it.
seek_to(stream, pts) tries three strategies in order, picking the
first that applies:
tfrafast-path (ISO/IEC 14496-12 §8.8.11). If the file has a trailingmfrawhosetfraindexes the requested track, the demuxer binary-searches thetfratime table for the largesttime ≤ pts, translates the result to amoof_offset, and snaps to the first keyframe at-or-after that offset. O(log N) ontfra+ a one-fragment-bounded sample-list scan.sidxfast-path (ISO/IEC 14496-12 §8.16.3). If notfracovers the track but the file carries one or moresidxboxes whosereference_idmatches the track'strack_ID, the demuxer walks every matchingsidx, expands its references into virtual(EPT, byte_offset)anchors, and picks the latest anchor whose decode-time start is at-or-beforepts(translated from the track's media timescale into the sidx timescale per §8.16.3). Both on-the-wire shapes are handled: a singlesidxindexing every subsegment (DASH on-demand profile) and onesidxper 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 usesu128arithmetic 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).- 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_tostill returns a correct cursor.
- 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_packetstill 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 thepssh.SystemIDblob) is out of scope by design. (Thesenc-less aux-info carriage is no longer a gap:Mp4Demuxer::resolve_sai_aux_infofetches and bridges it — see thesaiz/saiobullet above.) - Automatic mid-stream codec re-dispatch on a sample-description
switch. All
stsdentries are parsed and surfaced (§8.5.2), and the per-packetsample_description_indexis now resolved from the coveringstscentry (§8.7.4) / the fragment'stfhd-or-trexvalue (§8.8.7) and surfaced viaMp4Demuxer::sample_description_index_of_last_packet— so a caller detects the switch (index ≥ 2 = this packet was authored against thestsd_<n>entry, not the active[0]) and re-opens its decoder against that entry's config. What remains out of scope is doing that re-dispatch automatically insidenext_packet.
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.
A cargo-fuzz target exercises the BMFF box-tree walker on
arbitrary bytes:
cd fuzz
cargo +nightly fuzz run demuxThe 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.
The target opens via open_typed, drains up to 256 packets, re-seeks,
and re-walks the input through resolve_sai_aux_info (so the
attacker-controlled saiz/saio seek + fetch + §7.1 cell-parse path
is under fuzz too). The corpus includes muxer-produced fragmented
seeds — a sealed-mehd file with an empty-time gap fragment, and a
senc-stripped CENC file carrying the saiz/saio aux-info form.
Pinned regressions worth calling out:
- Extended-size u64 overflow — a
size=1 largesize=u64::MAXextended box anchored at a non-zero file offset used to overflow every downstreambody_start + payload_sizearithmetic site (the §8.16.3sidxend-anchor computation is the most exposed example).read_box_headernowchecked_addsstart + total_sizeand rejects the header before any caller computes a derived end byte. Replayed bytests/largesize_overflow.rsand two boundary unit tests insrc/boxes.rs. - Unbacked-count allocations (five shapes, one campaign) — a
forged 32-bit count in a structure whose entries consume little or
no wire data used to drive multi-GiB allocations from sub-KiB
inputs: a defaults-only
trun(zero bytes per declared sample), a constant-sizestsz(sample_count × sample_sizebeyond the file size), anext_packetpayload buffer for a sample size the input cannot hold, atfraentry-table reserve ahead of its validation, and a zero-width-entrysenc(IV size 0, no subsample flag). Every materializing count is now backed: fragmented sample counts are charged against a whole-file sample budget (no file can hold more samples than it has bytes), constant-sizestszvalidates against the file size, packet reads validateoffset + sizeagainst the input length, and the moov table parsers clamp their reservations to what the box body can back. Each shape is pinned as aregression_*corpus entry, with matching unit / integration tests.
MIT — see LICENSE.