Pure-Rust Matroska (MKV) and WebM container — demuxer + muxer built on the EBML primitives from RFC 8794. 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-mkv = "0.0"Register both containers ("matroska" and "webm") and let the probe
pick which DocType the file carries:
use oxideav_container::ContainerRegistry;
let mut containers = ContainerRegistry::new();
oxideav_mkv::register(&mut containers);
let input: Box<dyn oxideav_container::ReadSeek> = Box::new(
std::fs::File::open("movie.mkv")?,
);
let mut dmx = containers.open_demuxer("matroska", input)?;
for s in dmx.streams() {
println!("track {}: {}", s.index, s.params.codec_id.as_str());
}
loop {
match dmx.next_packet() {
Ok(p) => { /* feed p into a decoder from oxideav-codec */ }
Err(oxideav_core::Error::Eof) => break,
Err(e) => return Err(e.into()),
}
}
# Ok::<(), Box<dyn std::error::Error>>(())The demuxer returns raw Packet bytes — pair it with a decoder crate
(e.g. oxideav-opus,
oxideav-flac,
oxideav-vp9) or go through
the unified oxideav aggregator to wire decoding automatically.
- EBML header parse, DocType validation (
matroska/webm). - Typed EBML header accessor (RFC 8794 §11.2):
MkvDemuxer::ebml_header() -> &EbmlHeadersurfaces the full parsed header —ebml_version/ebml_read_version/ebml_max_id_length/ebml_max_size_length(§11.2.2..§11.2.5, spec defaults1/1/4/8materialised when absent),doc_type,doc_type_version/doc_type_read_version(spec default1materialised when the element was absent), and every well-formedDocTypeExtension(§11.2.9..§11.2.11) declaration in document order. EachDocTypeExtensionpairs a per-header-uniquename(DocTypeExtensionName, §11.2.10, length>0) with a non-zeroversion(DocTypeExtensionVersion, §11.2.11) — the experimental / out-of-band element-set declarations a reader checks before relying on extension elements. A malformed extension missing either mandatory child (or carrying an empty name / zero version) is dropped at parse time. The common file declares none and surfaces an empty list. - Segment walk:
Info,Tracks,Tags,Cues,Cluster. Known- and unknown-size Segment/Cluster both supported. - Clusters:
SimpleBlockandBlockGroup -> Block, all three lacing modes (Xiph, fixed, EBML-signed-delta). - Typed
BlockGroupmeta (demux::open_typed):MkvDemuxer::block_group_meta() -> Option<&BlockGroupMeta>surfaces the fourBlockGroupchildren thePackettype has no slot for —ReferenceBlock(RFC 9559 §5.1.3.5.5, every value in on-disk order),ReferencePriority(§5.1.3.5.4, default0materialised),CodecState(§5.1.3.5.6), andDiscardPadding(§5.1.3.5.7) — alongside the existingblock_additions()side channel (same read-after-next_packetcall discipline, cleared on seek). The same record also surfaces the reclaimed DivX trick-track / old-lacingBlockGroupchildren (RFC 9559 Appendix A.3..A.14) for a faithful re-mux:block_virtual()(BlockVirtual, A.3),reference_virtual()(ReferenceVirtual, A.4),slices()(everySlices > TimeSlicemaster, A.5..A.11 — eachTimeSlicefoldsLaceNumber/FrameNumber/BlockAdditionID/Delay/SliceDuration), andreference_frame()(ReferenceFrame, A.12..A.14 —ReferenceOffset+ReferenceTimestamp). Every reclaimed field is a pure on-disk projection (None/empty = absent, present0=Some(0)); none is interpreted by the container. The muxer writes them throughBlockGroupOptions(block_virtual/reference_virtual/slices/reference_frame), so a mux→demux pipeline round-trips every populated child. SilentTracks(RFC 9559 Appendix A.1 / A.2): per-ClusterSilentTrackNumberlists surface onClusterRecord::silent_track_numbersin on-disk order (deprecated element, but read for faithful re-mux).EncryptedBlock(RFC 9559 Appendix A.15, id0xAF): the reclaimed Cluster-level element whose body is structurally aSimpleBlockbut with its contents Transformed (encrypted and/or signed). Its raw, still- Transformed payload surfaces onClusterRecord::encrypted_blocksin on-disk order rather than being skipped silently — it yields noPacket(the track-number header lives inside the Transformed region), so a caller that decrypts itself or a re-muxer copying a legacy stream recovers the bytes verbatim. The container performs no decryption and exposes no track binding.- Metadata lift: title, muxer, encoder, date (Matroska
DateUTC-> ISO-8601), TagsSimpleTagname/value pairs with target-scope resolution (Tags.Targets.TagTrackUID->tag:track:N:<name>,TagChapterUID->tag:chapter:N:<name>,TagAttachmentUID->tag:attachment:N:<name>,TagEditionUID->tag:edition:N:<name>; all-zero UIDs -> bare<name>global key; unresolved non-zero UIDs are dropped per RFC 9559 §5.1.8.1.1.x "MUST match"),Chapters(chapter:N:start_ms/:end_ms/:title, ns→ms), andAttachments(attachment:N:filename/:mime_type/:size_bytes; payload is skipped, only the index surfaces). - Typed
Tagaccessor:demux::open_typedreturns the concreteMkvDemuxer, whose.tags() -> &[Tag]exposes RFC 9559 §5.1.8.1 fields the flat metadata view drops —TargetType/TargetTypeValueinformational hints, multi-UIDTargetsmasters (oneTagcan scope to several tracks/chapters at once), per-SimpleTagTagLanguage/TagLanguageBCP47/TagDefault, and binaryTagBinarypayloads (e.g. embedded cover-art bytes). The reclaimedTagDefaultBogusid (RFC 9559 Appendix A.43,0x44B4) — a mis-encoded variant some historical Writers emitted — is read as a synonym forTagDefaultso aSimpleTagcarrying it still surfaces its default flag. Tags with only dangling non-zero UIDs are filtered out per §5.1.8.1.1.3..§5.1.8.1.1.6; mixed Targets keep their resolvable UIDs. NestedSimpleTags (§5.1.8.1.2recursive: True) surface throughSimpleTag::children— a hierarchical tag (e.g. aTITLEcarrying aSORT_WITHsub-tag, or a name-only parent grouping severalARTISTleaves) is preserved as a tree rather than being flattened or dropped, parsed up to a 16-level depth cap with name-less children dropped per the §5.1.8.1.2.1minOccurs: 1rule. The flatmetadata()view still surfaces only top-level descriptors. Targets::target_level()typed hierarchy (RFC 9559 §5.1.8.1.1.1, Table 33):Targets::target_level() -> Option<TargetLevel>resolves the rawtarget_type_valueinteger into the typedTargetLevelenum (Shot=10/Subtrack=20/Track=30/Part=40/Album=50/Edition=60/Collection=70, plusOther(u64)for values registered under the §27.13 "Matroska Tags Target Types" registry after RFC 9559). The enum derivesOrdin spec-containment order so a player can walk the album → track → subtrack hierarchy without re-comparing raw integers — the §5.1.8.1.1.1 usage note ("Higher values MUST correspond to a logical level that contains the lower logical level TargetTypeValue values") falls straight out ofOrd.Other(_)sorts after every named level so a future entry doesn't break the comparison rule for the named ones. ReturnsNonewhen theTargetTypeValueelement was absent on disk — distinguishable fromSome(TargetLevel::Album)(the spec default50materialised by a writer). InverseTargetLevel::to_raw()round-trips every named variant + theOther(u64)forward-compat passthrough. CompanionTargetLevel::canonical_label()returns the leftmost / most common Table 33 label for the level (e.g.ALBUMfor value50, not the alternateOPERA/CONCERT/MOVIE/EPISODElabels); the file's ownTargetTypeinformational string stays on the existingTargets::target_typefield — the typed level helper doesn't overwrite it.- Typed
TrackAudienceFlagsaccessor (RFC 9559 §5.1.4.1.6..§5.1.4.1.11):MkvDemuxer::track_audience_flags(stream_index) -> Option<&TrackAudienceFlags>(and the per-streamall_track_audience_flags()slice) folds the six per-TrackEntryaudience hints —FlagForced(id0x55AA),FlagHearingImpaired(id0x55AB),FlagVisualImpaired(id0x55AC),FlagTextDescriptions(id0x55AD),FlagOriginal(id0x55AE),FlagCommentary(id0x55AF) — into one typed record per stream. Spec defaults are materialised asymmetrically:forced()returns a bareboolwith the §5.1.4.1.6 default0always reflected (aTrackEntrywith noFlagForcedchild decodesfalse); the fiveminver: 4flags carry no spec default and surface asOption<bool>so callers can distinguish "writer was silent" (None) from "writer explicitly cleared the flag" (Some(false)) — the §5.1.4.1.7..§5.1.4.1.11 wording ("Set to 1 if and only if …") makes that distinction load-bearing. Convenience predicatesis_default_presentation()(no flag isSome(true)) andis_accessibility()(any ofhearing_impaired/visual_impaired/text_descriptionsisSome(true)) cover the common filter cases. Every track surfaces a record —FlagForced's spec wording "applies only to subtitles" does not suppress the surface on audio / video tracks because the spec puts the elements onTrackEntryitself withminOccurs: 1forFlagForced; the typed surface trusts the caller to apply each flag where it makes sense for the track'sTrackType/CodecID. - Typed
TrackAudioaccessor (RFC 9559 §5.1.4.1.29.1..§5.1.4.1.29.4):MkvDemuxer::track_audio(stream_index) -> Option<&TrackAudio>(and the per-streamall_track_audio()slice) folds the fourAudiosub-master children —SamplingFrequency(id0xB5, §5.1.4.1.29.1),OutputSamplingFrequency(id0x78B5, §5.1.4.1.29.2),Channels(id0x9F, §5.1.4.1.29.3),BitDepth(id0x6264, §5.1.4.1.29.4) — into one typed record. Spec defaults are materialised asymmetrically:sampling_frequency()returns a baref64with the §5.1.4.1.29.1 default0x1.f4p+12=8000.0always reflected (anAudiomaster with no explicit child still surfaces 8000.0 Hz, never0.0);channels()returns a bareu64with the §5.1.4.1.29.3 default1(mono) always reflected;output_sampling_frequency()folds Table 19's derived default (=sampling_frequency()when the element was absent) butoutput_sampling_frequency_explicit()preserves the on-disk presence asOption<f64>so a re-muxer doesn't materialise an element that wasn't in the source.bit_depth()staysOption<u64>— §5.1.4.1.29.4 defines no default, so absence is observable. Convenience predicateis_sbr()returnstrueexactly when the writer emitted an explicitOutputSamplingFrequencystrictly greater thanSamplingFrequency(the canonical SBR-doubling signal for HE-AAC and similar tracks). Records surface only forTrackEntrys that carried anAudiomaster at all: video / subtitle / button tracks (where the master ismaxOccurs: 1but carries nominOccursat theTrackEntrylevel) returnNone, as does a malformed audio track that emitted noAudiochild — the typed surface never synthesises a record from the spec defaults alone. - Typed
TrackTimingaccessor (RFC 9559 §5.1.4.1.13..§5.1.4.1.15):MkvDemuxer::track_timing(stream_index) -> Option<&TrackTiming>(and the per-streamall_track_timing()slice) folds the threeTrackEntry-level timing elements —DefaultDuration(id0x23E383, §5.1.4.1.13),DefaultDecodedFieldDuration(id0x234E7A, §5.1.4.1.14), andTrackTimestampScale(id0x23314F, §5.1.4.1.15) — into one record per track. The elements sit directly onTrackEntry(no gating master), so every valid track surfaces a record;track_timingreturnsNoneonly for an out-of-range stream index.default_duration()is the container's nominal nanoseconds-per-frame source —TrackTiming::nominal_frame_rate()derives fps (1e9 / ns), so e.g. a41708333ns track yields~23.976. Both nanosecond durations carry a "not 0" range and no spec default, so they stayOption<u64>and a spec-illegal explicit0is dropped at parse time.track_timestamp_scale()materialises the §5.1.4.1.15 default1.0whiletrack_timestamp_scale_explicit()preserves the on-disk presence (a non-finite / non-positive payload is dropped, since the spec range is> 0x0p+0).TrackTiming::is_empty()reports the all-absent state — a track that carried none of the three elements. - Typed
TrackIdentityaccessor (RFC 9559 §5.1.4.1.18 / .19 / .20 / .23 / .4 / .5 / .12 / .24):MkvDemuxer::track_identity(stream_index) -> Option<&TrackIdentity>(and the per-streamall_track_identity()slice) folds the eightTrackEntry-level identity / selection elements —Name(id0x536E, §5.1.4.1.18),Language(id0x22B59C, §5.1.4.1.19),LanguageBCP47(id0x22B59D, §5.1.4.1.20),CodecName(id0x258688, §5.1.4.1.23),FlagEnabled(id0xB9, §5.1.4.1.4),FlagDefault(id0x88, §5.1.4.1.5),FlagLacing(id0x9C, §5.1.4.1.12), andAttachmentLink(id0x7446, §5.1.4.1.24) — into one record per track. The elements sit directly onTrackEntry(no gating master), so every valid track surfaces a record;track_identityreturnsNoneonly for an out-of-range stream index. The four strings carry no spec default and stayOption(name()/codec_name()/language_matroska()/language_bcp47());language()returns the effective language honouring the §5.1.4.1.20 precedence (LanguageBCP47supersedesLanguage— "any Language elements ... MUST be ignored"), anduses_bcp47()reports it. The three selection flags carry the spec default1:enabled()/default()/lacing_allowed()materialise it while*_explicit()preserve the on-disk presence so a re-muxer can distinguish "writer was silent" from "writer explicitly cleared the flag".attachment_link()surfaces theFileUIDof an attachment the track's codec uses (e.g. a font for an ASS/SSA subtitle track), matching anAttachment::uidfromattachments(); a spec-illegal0(range "not 0") is dropped at parse time, and per RFC 9559 errata ID 8615 (the printedmaxOccurs: 1is bogus) the full multi-occurrence list surfaces onattachment_links()in on-disk order, the singular accessor returning the first.TrackIdentity::is_default()reports the all-absent state. The effective language also lifts onto the flatStreamInfoview (BCP-47 preferred). - Typed
TrackCodecTimingaccessor (RFC 9559 §5.1.4.1.25 + §5.1.4.1.26):MkvDemuxer::track_codec_timing(stream_index) -> Option<&TrackCodecTiming>(and the per-streamall_track_codec_timing()slice) folds the twoTrackEntry-level codec-timing elements —CodecDelay(id0x56AA, §5.1.4.1.25) andSeekPreRoll(id0x56BB, §5.1.4.1.26), both nanosecond (Matroska Tick)uintegers — into one record per track. The elements sit directly onTrackEntry(no gating master), so every valid track surfaces a record;track_codec_timingreturnsNoneonly for an out-of-range stream index.codec_delay()is the encoder's built-in delay (Opus pre-skip) the player MUST subtract from each frame timestamp;seek_pre_roll()is the audio the decoder MUST decode after a seek before its output is valid (Opus convention 80 ms). Unlike theTrackTimingdurations, both elements carry spec default0and no "not 0" range, so an explicit on-disk0is a legal value distinct from "absent": the plain accessors materialise the0default whilecodec_delay_explicit()/seek_pre_roll_explicit()preserve the on-disk presence (a re-muxer can avoid emitting an element the source omitted).TrackCodecTiming::is_empty()reports the both-absent state — a track that emitted an explicit0for either element is not empty. The mux side is now fully symmetric:MkvMuxer::set_track_codec_timing(stream_index, MkvTrackCodecTiming)writes both elements on any track (eachSome(v)field explicit, eachNoneoff-disk), and an explicit hint overrides the auto-derived Opus values (CodecDelay=OpusHeadpre-skip in ns,SeekPreRoll= 80 ms) per field — so a re-mux preserves a source'sCodecDelay/SeekPreRollverbatim rather than being limited to the Opus recommendation. - Typed
TrackTranslateaccessor (RFC 9559 §5.1.4.1.27):MkvDemuxer::track_translates(stream_index) -> &[TrackTranslate](and the per-streamall_track_translates()slice) returns eachTracks > TrackEntry > TrackTranslatemaster the file carries, in on-disk order.TrackTranslatemaps a track to the value a Chapter Codec (DVD-menu, Matroska Script) uses to name it, so a file can be remuxed (acquiring newTrackNumber/TrackUIDvalues) without rewriting the opaque chapter-codec command data — only the mapping changes. It is theTrackEntry-level twin of theInfo > ChapterTranslate(§5.1.2.8) master already surfaced throughsegment_linking(), and is unbounded (a singleTrackEntrymay carry several). Each record exposestrack_id(TrackTranslateTrackID, §5.1.4.1.27.1 — surfaced verbatim, not a MatroskaTrackUID; its format is defined by the chapter codec),codec(TrackTranslateCodec, §5.1.4.1.27.2 — same value space asChapProcessCodecID, Table 31:0= Matroska Script,1= DVD-menu), and the unboundededition_uidslist (TrackTranslateEditionUID, §5.1.4.1.27.3 — an empty list means "all editions using the codec"). A master missing a mandatory child is surfaced verbatim (empty bytes /0codec) so callers can inspect a malformed file, mirroring the tolerantChapterTranslateparse. Returns an empty slice for a track with noTrackTranslatechild (the common case) or an out-of-rangestream_index. - Typed
TrackLegacyaccessor (RFC 9559 Appendix A.16..A.27 + A.28..A.32):MkvDemuxer::track_legacy(stream_index) -> Option<&TrackLegacy>(and the per-streamall_track_legacy()slice) folds the reclaimedTrackEntry-level legacy elements — the ones the RFC 9559 core body no longer documents but whose Element IDs stay reserved in the registry, and which historical Writers still emit — into one typed record per track. Three families share the record: the codec-description metadata (CodecSettingsA.19 utf-8,CodecInfoURLA.20 +CodecDownloadURLA.21 string lists,CodecDecodeAllA.22 uinteger —can_decode_damaged()predicate), the cache / offset hints (MinCacheA.16,MaxCacheA.17, signedTrackOffsetA.18 — a Matroska-Tick playback-offset the container surfaces but does not apply), the reclaimed Video/Audio-master children (GammaValueA.25 +FrameRateA.26 floats nested inVideo;ChannelPositionsA.27 binary nested inAudio— all surfaced verbatim, none applied), the orderedTrackOverlayfallback list (A.23 — "the order of multiple TrackOverlay matters", surfaced verbatim so the preference chain is preserved), and the DivXTrickTrack Smooth-FF/RW pairing quintet (TrickTrackUIDA.28,TrickTrackSegmentUIDA.29,TrickTrackFlagA.30 —is_trick_track(),TrickMasterTrackUIDA.31,TrickMasterTrackSegmentUIDA.32). None of the appendix entries carries a spec default or range, so every field is a pure on-disk projection: absence is always observable (None/ emptyVec), a present0round-trips asSome(0)distinct from omission, and an off-length SegmentUID binary is preserved verbatim for inspection. The accessor returnsNone(never a hollow record) when theTrackEntrycarried none of these — the common case for a modern file — or for an out-of-rangestream_index;TrackLegacy::is_empty()reports the all-absent state. The container surfaces these for a faithful re-mux and never interprets them. The mux side writes the populated fields viaMkvMuxer::set_track_legacy(see the Muxer section) — a mux→demux pipeline round-trips every populated field verbatim. - Typed per-Cluster
Position/PrevSizerecords (RFC 9559 §5.1.3.2 / §5.1.3.3):MkvDemuxer::cluster_records() -> &[ClusterRecord]surfaces each Cluster's optionalPosition(id0xA7,uinteger) andPrevSize(id0xAB,uinteger) children as they're walked. Records are appended in first-encounter order throughnext_packet/seek_to, withbody_offset(the absolute file offset of the byte right after the Cluster's id+size header) as the dedup key — a back-then-forward seek that revisits the same Cluster doesn't push a duplicate row. Both typed fields areOption<u64>:Nonewhen the on-disk child was absent (common forPrevSizeon the first Cluster of a Segment, and for both fields when a writer omitted them entirely),Some(v)when present. TheSome(0)Positioncase is the §5.1.3.2 spec convention for live streams (Cluster offset not determined ahead of time) and is distinct fromNone. Consumers can verify a recordedPositionmatches the actual on-disk offset by subtractingsegment_data_start- the Cluster's header length from
body_offset(the §16 Segment-Position definition), build a reverse walker on top ofPrevSizewithout re-scanning the SeekHead, or detect a live stream by seeingSome(0)Positionvalues. The slice grows incrementally as the demuxer walks the Segment — callers wanting the full per-Cluster set should drain the file vianext_packetfirst.
- the Cluster's header length from
- Typed
SeekHeadaccessor (RFC 9559 §5.1.1, including §5.1.1.1..§5.1.1.1.2):MkvDemuxer::seek_entries() -> &[SeekEntry]surfaces the MetaSeek index — theSeekHead > Seekrows that point each Top-Level Element to its Segment Position — in document order. Besides inspection / re-mux, the open path now navigates by it (RFC 9559 §6.3): a SeekHead entry referencing aTags/Chapters/Attachments/Cuesmaster stored after the Cluster run (the common single-pass-mux layout) is followed at open time, so late masters surface through their typed accessors, the flat metadata view, and the seek index — including on-demandattachment_datareads — without draining the stream. Trust-but-verify: the target must carry exactly the promised element ID with a bounded body inside the Segment, the parse lands in temporaries merged only on success (a hostile or staleSeekPositioncan never leave partial state), and masters the pre-Cluster walk already parsed are never chased again. A leadingCRC-32on a followed master validates ontocrc_status()like every other Top-Level master. EachSeekEntrypairs aSeekID(§5.1.1.1.1, the 4-byte binary EBML ID of the referenced element) with aSeekPosition(§5.1.1.1.2, a Segment Position per Section 16 — relative to the first Segment-data byte, not an absolute file offset).seek_id() -> Option<u32>decodes the big-endian EBML id for the common case (compare againstids::CUES/ids::TRACKS/ …);seek_id_bytes()keeps the raw bytes verbatim so aSeekIDreferencing an element this build doesn't recognise still round-trips through a re-mux. A malformedSeekmissing its mandatorySeekPosition(minOccurs: 1) is surfaced for inspection withseek_position() == 0andhas_position() == falserather than dropped. Files using the §6.3 two-SeekHeadlayout (maxOccurs: 2, the first referencing the second) accumulate both SeekHeads' entries onto the one slice in document order. The in-tree muxer's emittedSeekHead(Info / Tracks / Cues) reads back through this accessor with everySeekPosition + segment_data_startlanding on the matching on-disk element header. Returns an empty slice when the file carries noSeekHead(legal — §6.3 only RECOMMENDS it). - Typed
Attachmentsaccessor (RFC 9559 §5.1.6):MkvDemuxer::attachments() -> &[Attachment]returns one [Attachment] perAttachedFileparsed from the Segment, in document order. Each entry carries the 1-basedindex(matching theattachment:N:*flat metadata keys and anytag:attachment:N:<name>Tag scope),filename(FileName, §5.1.6.2),mime_type(FileMimeType, §5.1.6.3),description(FileDescription, §5.1.6.1),uid(FileUID, §5.1.6.5), and the on-disk byte range (data_offset+data_size) of theFileDatapayload. The payload bytes are not read up front — a multi-megabyte embedded font stays on disk untilMkvDemuxer::attachment_data(index)is called, at which point exactlydata_sizebytes are read fromdata_offsetand returned; the demuxer's reader position is preserved across the fetch so calling it betweennext_packetcalls is safe. The flatmetadata()view also gains anattachment:N:descriptionkey when the source element was present. EachAttachmentalso surfaces the three reclaimed DivX-font children (RFC 9559 Appendix A.40..A.42) —referral(FileReferral, A.40, binary),used_start_time(FileUsedStartTime, A.41) andused_end_time(FileUsedEndTime, A.42) — asOptions (None= absent; a present empty referral staysSome(vec![])and a present0used-time staysSome(0)), read and written verbatim by the muxer (MkvAttachmentcarries the same three fields) so a mux→demux pipeline round-trips a legacy font attachment. - Typed
Chaptersaccessor (RFC 9559 §5.1.7):MkvDemuxer::chapters() -> &[Edition]exposes the structured chapter tree the flatchapter:N:*metadata view collapses — everyEditionEntrykeeps itsEditionUID,EditionFlagDefaultandEditionFlagOrderedflags; everyChapterAtomkeeps itsChapterUID,ChapterStringUID(e.g. WebVTT cue id), full-precisionChapterTimeStart/ChapterTimeEndnanoseconds,ChapterFlagHidden,ChapterFlagEnabled(id0x4598— a legacy pre-RFC element the RFC 9559 registry leaves unassigned; historical default1materialised astrue), Medium-Linking fieldsChapterSegmentUUID(raw 16 B) +ChapterSegmentEditionUID(zero suppressed per spec "range: not 0"),ChapterPhysicalEquiv(DVD/SIDE physical mapping per §20.4), all multilingualChapterDisplayrows (each withChapString,ChapLanguage+ChapLanguageBCP47,ChapCountry), theChapProcesssub-tree (RFC 9559 §5.1.7.1.4.14–19 —ChapProcessCodecID,ChapProcessPrivate, and zero or moreChapProcessCommandrows each withChapProcessTime+ rawChapProcessData; payloads surfaced verbatim, never executed), and any nested child atoms (the spec marksChapterAtomas recursive). Atoms are 1-indexed depth-first in document order — the same index the flatchapter:N:*keys andTagChapterUID-resolved tags use, now extended to nested chapters. Returns an empty slice when the file has noChapterselement. - Legacy pre-registry elements (staged
legacy-element-ids.mdmapping — three groups Matroska dropped before RFC 9559 without registry rows, their IDs formally unassigned in the IANA registry):Edition::hiddenreads the legacyEditionFlagHidden(id0x45BD, historical default0materialised);Chapter::track_uidsreads the legacyChapterTrack(id0x8F) >ChapterTrackUID(id0x89— the element old schemas spellChapterTrackNumber) list in on-disk order with spec-illegal zeros dropped, empty = "all tracks apply"; and the removed global Signature family (SignatureSlot0x1B538667+ seven children) is recognised and skipped as known-legacy — a strict open steps over it, a resilient open records noDamageEventfor it, and the resync scanner can re-anchor on its four-octet ID. All three groups are read-side only: the muxer never emits them (an unassigned ID could collide with a future registry assignment). - Matroska v5 elements (staged
post-rfc9559-elements.md— the six elements the CELLAR schema carries withminver: 5that no RFC and no IANA registry row define; parsed unconditionally per the staged doc's "parse all six" posture, mirror-image of the legacy set above):EditionDisplay(0x4520) +EditionString(0x4521) +EditionLanguageIETF(0x45E4): edition-level display names — theEditionEntryanalogue ofChapterDisplay— surface onEdition::displaysin on-disk order, each pairing one name string with its BCP 47 tag list (note the upstream…IETFspelling; every other BCP 47 element is named…BCP47). A master missing its mandatoryEditionStringis dropped; a present-but-empty string is kept (the schema doesn't prohibit it); noundfallback is synthesised for a language-less entry (none is specified). The full list is surfaced — the schema names no selection rule among multiple displays, so the caller picks by language.ChapterSkipType(0x4588): per-atom skip classification onChapter::skip_typeasOption<ChapterSkipType>— the element has no default, so absence stays observable (Nonecarries no assertion, distinct fromSome(NoSkipping)'s positive "do not skip this"). The closed 0..=7 enumeration (incl. the post-RFCIntermission = 7addition) decodes to named variants; out-of-enumeration values degrade toUnknown(v)("unrecognised, do not skip" —is_skippable()never fires for them). The v5 nested-atom rule (a nested atom MUST NOT repeat its nearest ancestor's value) is checked by the schema validator (ChapterSkipTypeNesting), ancestor-chain-aware and independent of on-disk child order.Edition::skip_type_at(ns)resolves "the skip classification at timestamp T" applying the staged implicit-range rule — an end-less governing atom classifies until the next atom carrying aChapterSkipType(skip-less atoms neither classify nor terminate) or EOF, which needs a forward sibling scan rather than an interval lookup; the overlap tie-break (latest-starting governing atom wins) is documented as this Reader's deterministic choice, not spec.Emphasis(0x52F1): per-Audio-master emphasis filter on the typedTrackAudiorecord —emphasis()returns a bareAudioEmphasiswith the mandatory-but-defaulted0materialised (RFC 8794 §11.1.5),emphasis_explicit()preserves the on-disk presence (an explicit0is distinct from absence — load-bearing for re-mux, since the element carries the CELLARstream copy keep="1"marker: dropping it on a stream copy is a correctness bug, the stored samples stay emphasised). The enumeration is deliberately non-contiguous (0..=5 + 10..=16; 2 isreserved, 6..=9 and 17+ unassigned) and closed — no registry, no FCFS path;needs_deemphasis()reports the values whose inverse filter the player MUST apply.TagBlockAddIDValue(0x63C7— the only one of the six that genuinely postdates RFC 9559):Targets::block_add_id_valuessurfaces the repeatable selector verbatim (default0= wildcard; empty list ≡ single0). It is the onlyTargetschild whose scope is qualified by a sibling child:Targets::applies_to_block_addition (stream_index, mapping_value)resolves the joint 2×2TagBlockAddIDValue×TagTrackUIDmatrix (wildcard/wildcard = all mappings in the Segment; the both-concrete cell is the v5 MUST-match case), andMkvDemuxer::tags_for_block_addition_mappingfilters the tag list through it. A selector of1matches nothing (the referentBlockAddIDValueis ranged>= 2) — surfaced, not errored.
- RFC 9559 errata surfaced (both
Reportederrata transcribed in the stagedpost-rfc9559-elements.md§8): per errata ID 8615 the printedAttachmentLinkmaxOccurs: 1is bogus, soTrackIdentitymodels the element as a list —attachment_links()returns every non-zero value in on-disk order while the historical singularattachment_link()keeps returning the first. (Errata ID 8616 —CueTime= Cluster Timestamp + Block Timestamp,CodecDelay/DiscardPadding/SeekPreRollexcluded — matches the arithmeticseek_toalready uses.) - Damage-resilient open (
demux::open_resilient/demux::open_resilient_typed): RFC 9559 §26 leaves error handling to the Reader ("Matroska Readers decide how to handle the errors whether or not they are recoverable in their code"); this Reader recovers where the strictopenfails. A known-size Segment whose declared size runs past the input end is clamped (truncated-file recovery); a damaged Top-Level master before the first Cluster (Tags,Chapters,Cues, ...) is skipped keeping whatever parsed before the error; garbage between Top-Level elements is stepped over by scanning for the next well-formed 4-byte Top-Level element ID; and a corrupt element inside the Cluster stream makesnext_packetresynchronise on the next plausible Cluster — the §5.1.3.2 damaged-stream resynchronisation unit — instead of ending the stream (a candidate must header-parse and its first child must carry a legal Cluster-child ID per the §5.1.3.1 usage note; a resync floor guarantees forward progress). A truncated final Cluster yields the packets that physically fit, then a cleanError::Eof— the resilientnext_packetnever surfaces any other error class. Every recovery is recorded as a typedDamageEvent(DamagedMaster(id)/GarbageData/ClusterStream/SegmentTruncated/UnrecoverableTail, each carrying the damage offset, resume offset, and bytes skipped) onMkvDemuxer::damage_events()— empty exactly when the file needed no recovery, so strict-minded callers can reject after the fact. The strictopen/open_typedbehaviour is unchanged byte-for-byte. - Resilient Cues-less seek fallback: on an
open_resilientdemuxer,seek_toon a file with no usableCues(absent — RFC 9559 §22.1 only RECOMMENDS the element — or damaged and skipped at open) linearly scans ClusterTimestamps (§5.1.3.1) and lands on the last Cluster at or before the target, stopping early on the §11.1 ascending-Cluster-time order. Unknown-size Clusters are stepped over with the same vetted Top-Level scan the resync path uses; targets before the first / past the last Cluster snap to the first / last Cluster. The strict path still returnsError::Unsupported— the RFC 9559 §23.2 "neither SeekHead nor Cues at the start SHOULD be considered non-seekable" signal stays observable. - Cues that LIE — trust-but-verify seek + whole-index audit: the
index is pure metadata (every
CueClusterPosition/CueTime/CueRelativePosition/CueBlockNumberclaim restates information about bytes that exist elsewhere in the Segment — RFC 9559 §5.1.5.1, Section 16), so every claim is checkable against the Segment itself. The real-world damage class is a stale index (the file was edited or truncated after theCueselement was written); the hostile class is a forged one. Two surfaces:- On a resilient open,
seek_toverifies the chosen cue's landing before committing: the promised offset must carry a parseableClusterheader inside the Segment (§5.1.5.1.2.2 — "the Segment Position of the Cluster containing the associated Block"), and the landed Cluster'sTimestampmust not prove the promisedCueTimeimpossible (§11.2 stores Block timestamps as 16-bit signed Track-Tick offsets from the ClusterTimestamp, so aTimestampbeyondCueTime + 32768 × TrackTimestampScalecannot contain the promised Block; the check usesmax(scale, 1.0)— the global maximum across tracks, since a §18.8 virtual-track seek resolves through source-track cues — so it can never false-positive on a spec-legal file). A lying cue records aDamageKind::CueLieevent (lying target offset, fallback landing,bytes_skipped: 0) and the seek falls back to the linear Cluster-Timestampscan — landing correctly instead of feedingnext_packetgarbage (packet loss through resynchronisation) or silently overshooting the target. The fine-grainedCueRelativePosition/CueBlockNumberlies are not seek lies — those already degrade to a cluster-start walk, which lands correctly. A truncated-but-present final Cluster is not a lie either: its surviving prefix is still the right landing. The strict path stays byte-for-byte unchanged — it trusts the index and surfaces whatever the landing yields (RFC 9559 §26 leaves the choice to the Reader). MkvDemuxer::audit_cues() -> CueAuditReportaudits the whole index on strict and resilient opens alike, one entry perCueTrackPositions(the same denormalised tableseek_toconsults), typed per-claim findings (CueLieKind):UnknownTrack(danglingCueTrackafter a track was dropped),TargetOutOfSegment,TargetNotCluster,ClusterTruncated(the declared Cluster body runs past the Segment end — a truthful index whose file lost its tail),TimestampImpossible(the §11.2 bound above;detail()carries the landedTimestamp),RelativePositionInvalid(doesn't resolve to aSimpleBlock/BlockGroupheader inside the body), andBlockNumberOutOfRange(1-based count unreachable, or the spec-illegal0;detail()carries the Blocks found). Read-only with respect to demux state (reader position restored, no packet / CRC / damage bookkeeping touched — callable betweennext_packetcalls), findings capped at 4096 with an exact uncappedfindings_total()counter and a per-offset probe cache against hostile fan-in,is_truthful()as the headline verdict.Erronly on input-level I/O failure — hostile content becomes findings, never errors. The in-tree muxer's own emitted index audits truthful, pinned in CI (tests/seek_cues_lies.rs, 18 tests: stale/forged offsets, void and mid-element targets, truncation both sides of the last Cluster header,TrackTimestampScaleslack widening, findings-cap flood, per-seek event logging, fuzz-corpus + byte-soup no-panic sweeps, strict-path trust pin, and theseed_cue_lies.mkvcorpus-seed pin — one lie of every checkable class from a well-formed start).MkvDemuxer::audit_seek_head() -> SeekHeadAuditReportextends the same treatment to the file's other self-referential index: every MetaSeekSeekentry'sSeekID/SeekPositionpair (RFC 9559 §5.1.1.1) is resolved and must land on an element header carrying exactly the promised ID. TypedSeekLieKindfindings:MissingPosition(theminOccurs: 1child omitted),MalformedId(payload not a 1..=4-octet EBML ID),TargetOutOfSegment, andTargetMismatch(with the ID that actually parsed onfound_id()). The open path already trust-but-verifies the entries it follows (late post-Cluster masters); the audit checks every entry, including the ones navigation never needed. Same contract asaudit_cues(read-only, both open modes, 4096-cap + exact counter,Erronly on I/O). The in-tree muxer's emitted SeekHead audits truthful — pinned along with mismatch/garbage/malformed shapes intests/seek_head_lies.rs(8 tests).
- On a resilient open,
- Zero-Cluster (metadata-only) Segments open — the schema gives
ClusternominOccurs, so a Segment carrying only Info / Tracks / Chapters / Tags / Attachments (a chapters-only sidecar, or the in-tree muxer's own zero-packet output) is legal: strict and resilient opens both succeed with zeroDamageEvents, every non-Cluster master surfaces through its usual accessor,next_packetreports a cleanError::Eoffrom the first call, andseek_toreturnsError::Unsupported(nothing to land on) in both modes. - Duration:
Segment\Info\Durationtranslated to microseconds. - Linked-Segment
Infometadata (RFC 9559 §5.1.2.1..§5.1.2.8 + Section 17) viaMkvDemuxer::segment_linking()→ [SegmentLinking]: the Segment's ownSegmentUUID, the previous / next Segment UIDs of a Hard-Linked chain (PrevUUID/NextUUID) with their display*Filenames, the unboundedSegmentFamilyUID list, and theChapterTranslatesub-tree ([ChapterTranslate]:ChapterTranslateIDChapterTranslateCodec+ optionalChapterTranslateEditionUIDlist). UID binaries are surfaced verbatim — off-length values round-trip for inspection rather than being truncated.is_empty()reports the common standalone Segment;is_hard_linked()reports a chain member. Pure container surface: no neighbouring-file resolution.
- Seek:
seek_to(stream, pts)uses the Cues index. Handles Cues at either end of the Segment, and walks an unknown-size final Cluster to find Cues that sit past it. CueRelativePositionhonoured on seek (RFC 9559 §5.1.5.1.2.3): when a Cues entry carries theCueRelativePositionelement,seek_toopens the target Cluster, captures itsTimestamp(RFC 9559 §5.1.3.1 — SHOULD be the first child), and then repositions the reader directly at the byte offset of the referencedSimpleBlock/BlockGroup(0being the first possible element position inside that Cluster). The next packet emitted is the cue's exact block, not the first block in the Cluster — finer seek granularity than the legacy "scan from cluster start" path, which is preserved as a fallback when the cue has noCueRelativePositionor the encoded position is out of range.CueBlockNumberseek fallback (RFC 9559 §5.1.5.1.2.5): when a Cues entry carriesCueBlockNumber("Number of the Block in the specified Cluster") but noCueRelativePosition— common for files indexed by older tools —seek_towalks the Cluster body countingSimpleBlock/BlockGroupelements and lands the reader on the exact 1-based n-th Block instead of scanning from the Cluster start. Out-of-range or malformed block numbers degrade gracefully to the cluster-start walk.- Typed
Cuesaccessor (RFC 9559 §5.1.5.1, including §5.1.5.1.1..§5.1.5.1.2.8 and the reclaimed Appendix A.37..A.39CueReferencechildren):MkvDemuxer::cue_points() -> &[CuePoint]surfaces the full on-disk seek-index tree in document order. Theseek_topath consumes a denormalised, sorted projection internally (track, time, cluster offset, relative position);cue_pointsinstead preserves everything that projection collapses, so callers can read per-cueCueDuration(§5.1.5.1.2.4) andCueBlockNumber(§5.1.5.1.2.5), theCueCodecState(§5.1.5.1.2.6, spec default0materialised —0meaning "taken from the initialTrackEntry"), and walk the nestedCueReferencerows (§5.1.5.1.2.7 — each carryingCueRefTimeplus the reclaimedCueRefCluster/CueRefNumber/CueRefCodecState), or re-mux theCueselement sub-element-for-sub- element. EachCuePointpairs an absoluteCueTime(in Segment Ticks — the file'sTimestampScale, not microseconds) with one or moreCueTrackPositions(the spec gives the latterminOccurs: 1with nomaxOccurs, so a single timestamp can index blocks on several tracks). Populated whetherCuessits before the first Cluster or after the last (the late best-effort rescan feeds the same typed collector); optional children surface asOption<u64>(absent vs present),0-but- present and0-by-defaultCueCodecStateare observationally identical per the spec default. Unknown children insideCueTrackPositionsare skipped (forward-compat). Returns an empty slice when the file has noCueselement. - An unknown-size Cluster is terminated cleanly when a sibling Segment- child element follows it (no more "Cues silently eaten as payload").
- CRC-32 validation (RFC 8794 §11.3.1, RFC 9559 §6.2): when a Top-Level
master element (
Info,Tracks,Tags,Cues,Chapters,Attachments,SeekHead) or aClustercarries a leadingCRC-32child, the demuxer recomputes the IEEE CRC-32 (reflected poly0xEDB88320, init0xFFFFFFFF, final XOR, little-endian storage) over the rest of the element and records the result.MkvDemuxer::crc_status() -> &[CrcStatus]exposes each{element_id, stored, computed}triple with anis_valid()helper. Up-front masters are checked at open time in segment order; Cluster checks land lazily on the firstnext_packet/seek_tothat opens each Cluster (the element id on a Cluster status isids::CLUSTER), with a body-offset dedup so a back-then-forward seek revisiting the same Cluster never produces two statuses for it. The late best-effort Cues rescan (the path the demuxer uses whenCuessits after the finalCluster— the common single-pass-mux layout, and the one our own muxer emits) also validates a leadingCRC-32on the rediscoveredCueselement and pushes its status, so a Cues CRC mismatch surfaces regardless of whether theCueswas placed before or after Clusters. A Cluster declared with the unknown-size VINT can't be CRC-checked (the spec requires a bounded body) and produces no status. Validation is informational — a mismatch does not abort the open (RFC 8794 §12: a reader MAY ignore the data); strict callers reject any non- valid status. Elements with noCRC-32child produce no status (omission is spec-legal). TrackOperationtyped decode (RFC 9559 §5.1.4.1.30): a virtual track assembled from other tracks.MkvDemuxer::track_operation(stream_index)(and the per-streamtrack_operations()slice) returns a typedTrackOperationfor anyTrackEntrycarrying the element,Nonefor an ordinary track.TrackCombinePlanes(§5.1.4.1.30.1) surfaces as aVec<TrackPlane>— each pairs a referenced track with itsTrackPlaneType(LeftEye/RightEye/Background, withOther(u64)preserving FCFS-registry values per §27.17) — andTrackJoinBlocks(§5.1.4.1.30.5) surfaces as aVec<TrackRef>. EveryTrackPlaneUID/TrackJoinUIDis resolved back to aTrackRefcarrying both the on-diskTrackUIDand the matching 0-indexed stream index (Nonefor a dangling reference, kept rather than dropped). ATrackPlanemissing its mandatoryTrackPlaneUIDand a zeroTrackJoinUID("not 0" per spec) are dropped.TrackOperationapplication (RFC 9559 §18.8):MkvDemuxer::set_apply_track_operations(true)turns the decoded recipe into an actual packet stream, so a reader opens the virtual track like any real one. Every packet whose stream is referenced by a virtual track's operation is followed by a synthesised copy re-tagged with the virtual track's stream index — §18.8's "the Block elements (from BlockGroup and SimpleBlock) of all the tracks SHOULD be used as if they were defined for this new virtual Track", applied to both mechanisms: aTrackJoinBlocksvirtual stream is the merged source stream, and aTrackCombinePlanesstereo-3D virtual stream carries every plane's frames, each copy tagged with its plane role. Copies preserve the source Block's bytes, timestamps, duration, flags, and per-Block side channels (BlockAdditions/BlockGroupmeta), and are queued directly after their source frame so the virtual stream keeps the writer's Block interleave — storage order is coding order (§10), which a PTS re-sort would break; overlapping-timestamp joins forward both Blocks (§18.8 leaves the handling "up to the underlying system").virtual_packet_origin()reports each synthesised packet's provenance (VirtualPacketOrigin: virtual stream, source stream,VirtualPacketRole::Plane(TrackPlaneType)/Joined) under the same read-after-next_packetdiscipline asblock_additions(), so a caller routes each plane packet to its own decoder — the virtual track's own codec ID is meaningless per §18.8, and the container assembles the packet stream, never the pixels. Dangling and self-references synthesise nothing; source tracks keep emitting their own packets; the toggle works on strict and resilient opens alike and is off by default (the pre-application behaviour, byte-for-byte). Seeking the virtual stream works too: a virtual track with no Cues rows of its own resolvesseek_tothrough the §18.8 Cues union of its source tracks (per-source best cue, earliest cluster offset wins — the conservative landing that cannot skip any source's Blocks at or after the target; the landed Cluster is walked from its start). The mux→demux round trip is validated end-to-end: our own muxedTrackOperationfiles re-apply with correct per-packet roles and seek through their own emitted Cues (tests/mux_track_operation_apply.rs), andexamples/gen_track_operation.rsgenerates join / stereo files for black-box cross-checks.BlockAdditionMappingtyped decode (RFC 9559 §5.1.4.1.17):MkvDemuxer::block_addition_mappings(stream_index)(and the per-streamall_block_addition_mappings()slice) returns eachTracks > TrackEntry > BlockAdditionMappingmaster the file carries, in on-disk order, as a typedBlockAdditionMappingrecord exposingvalue(BlockAddIDValue, §5.1.4.1.17.1,Option<u64>— spec range>=2, no default),name(BlockAddIDName, §5.1.4.1.17.2,Option<String>),addid_type(BlockAddIDType, §5.1.4.1.17.3,u64— spec default0(codec-defined) materialised), andextra_data(BlockAddIDExtraData, §5.1.4.1.17.4,Option<Vec<u8>>— opaque per-track binary state the type interpreter consults). The helperis_codec_defined()reports whetheraddid_type == 0(the §5.1.4.1.17.3 usage-note case in which the matchingBlockAddIDmust be1). Unknown child elements inside the master are skipped — the spec allows additions to the registry. Tracks with noBlockAdditionMappingchild surface as an empty slice (the common case — the element only appears on tracks that useBlockAdditionalto extend their on-disk format). The typed view declares the shape of the side channel; the per-frameBlockAdditionalpayload bytes themselves surface through the per-packetblock_additions()accessor below, and payload semantics stay with the codec / track-format extension that owns eachBlockAddIDTypevalue. The mux side is symmetric:MkvMuxer::set_block_addition_mappings(stream_index, Vec<BlockAdditionMapping>)takes the same demux-side records and writes eachBlockAdditionMappingmaster into the carryingTrackEntryin slice order, so a mux→demux pipeline round-trips every mapping element-for-element. Per-field omission mirrors the decode:value/name/extra_dataemit their child only whenSome, andBlockAddIDTypeis emitted only when non-zero (the §5.1.4.1.17.3 default0stays off-disk and still round-trips as0).- Per-Block
BlockAdditionstyped decode (RFC 9559 §5.1.3.5.2, including §5.1.3.5.2.1..§5.1.3.5.2.3) +MaxBlockAdditionID(§5.1.4.1.16):MkvDemuxer::block_additions() -> &[BlockAddition]surfaces the side-channel payloads attached to the most recently returned packet — one typedBlockAdditionperBlockMorein on-disk order, each pairingblock_add_id()(BlockAddID, §5.1.3.5.2.3, spec default1= codec-defined materialised on omission) with the verbatimdata()bytes (BlockAdditional, §5.1.3.5.2.2, never interpreted by the container — id1is e.g. the WebM alpha plane when the track'sAlphaModeisPresent; ids>= 2are described by the track'sBlockAdditionMapping). The slice is empty forSimpleBlockpackets (the element only exists onBlockGroup), forBlockGroups without the master (the common case), before the firstnext_packet, and after a seek; every frame de-laced from one laced Block shares the Block's additions (the spec attaches the master to the Block as a whole). MalformedBlockMores are dropped: a missing mandatoryBlockAdditional, aBlockAddIDof0(range "not 0"), and a duplicateBlockAddID(uniqueness MUST — first occurrence kept). The per-track declaration surfaces throughMkvDemuxer::max_block_addition_id(stream_index)with the §5.1.4.1.16 spec default0("there is no BlockAdditions for this track") materialised on absence. ContentEncodingstyped decode (RFC 9559 §5.1.4.1.31):MkvDemuxer::content_encodings(stream_index)(and the per-streamall_content_encodings()slice) returns the track's transformation chain — compression and/or encryption applied to frame data /CodecPrivatebefore the bytes hit Blocks — as typedContentEncodings,Nonefor an ordinary track. EachContentEncodingcarries itsContentEncodingOrder,ContentEncodingScopebit field (block()/private()/next()accessors), and aContentEncodingTransformenum:Compression(ContentCompAlgo→Zlib/Bzlib/Lzo1x/HeaderStripping/Other(u64), plus theContentCompSettingsstripped bytes) orEncryption(ContentEncAlgo→None/Des/TripleDes/Twofish/Blowfish/Aes/Other(u64), theContentEncKeyID, the nestedContentEncAESSettings→AESSettingsCipherModeasCtr/Cbc/Other(u64), and the reclaimed content-signing quartet (RFC 9559 Appendix A.33..A.36) as a typedContentSigningrecord —ContentSignature(id0x47E3, binary) +ContentSigKeyID(0x47E4, binary) +ContentSigAlgo(0x47E5, uinteger) +ContentSigHashAlgo(0x47E6, uinteger). Each signing field is anOptionwhoseNonemeans "absent on disk" — the appendix names no values and no defaults, so a present0round-trips asSome(0)distinct from absence, mirroring the reclaimed Appendix-AAspectRatioTyperaw-value rule;ContentSigning::is_empty()reports the all-absent state). The list is pre-sorted into decode order (highestContentEncodingOrderfirst, per §5.1.4.1.31.2). Element defaults are honoured (order 0, scope 0x1 Block, type 0 compression, comp-algo 0 zlib). The headers are surfaced; zlib/bzlib/lzo1x and encryption are never decompressed or decrypted (out of container scope).- Header-Stripping applied on read (RFC 9559 §5.1.4.1.31.6 algo 3,
§5.1.4.1.31.7): Header Stripping is the one
ContentEncodingtransform the container can reverse without a codec — theContentCompSettingsbytes were removed from the front of each frame on write, so the demuxer prepends them back to every de-laced frame, andnext_packetreturns the original (un-stripped) frame data. Block scope (§5.1.4.1.31.3 bit 0x1) is honoured per-frame (the prefix lands on each laced sub-frame, not the Block once); a chain of several Header-Stripping steps is combined in decode order. If the Block-scoped chain contains any step the container can't undo (zlib/bzlib/lzo1x compression or encryption), packets pass through encoded — the demuxer never partially strips. Private-scope (CodecPrivate-only) Header Stripping leaves frame data untouched. Videogeometry quartet typed decode (RFC 9559 §5.1.4.1.28.8..§5.1.4.1.28.14):MkvDemuxer::video_geometry(stream_index)(and the per-streamvideo_geometries()slice) folds thePixelCrop{Top,Bottom,Left,Right}hide-window plus theDisplayWidth/DisplayHeight/DisplayUnitrender-size triple into a single typedVideoGeometry.DisplayUnitsurfaces as theDisplayUnitenum (Pixels/Centimeters/Inches/DisplayAspectRatio/Unknown/Other(u64)for forward-compat with the §27.9 "Matroska Display Units" registry).display_width()/display_height()returnOption<u64>: the explicit element when the file carries it, otherwise the §5.1.4.1.28.12 / §5.1.4.1.28.13 derived default (PixelWidth - PixelCropLeft - PixelCropRight/PixelHeight - PixelCropTop - PixelCropBottom) — but only whenDisplayUnit == 0(pixels), since the spec explicitly states "If the DisplayUnit of the same TrackEntry is 0, then the default value for DisplayWidth is ...; else, there is no default value". For any otherDisplayUnitan absent element resolves toNone. The PixelCrop defaults (0, §5.1.4.1.28.8..11) and DisplayUnit default (0, §5.1.4.1.28.14) are always materialised. Non-video tracks (and video tracks with noVideomaster) returnNone; a derivation that would underflow (malformed file with crops larger than the encoded width or height on the same axis) returnsNoneon that axis rather than wrapping.Video > Colourtyped decode (RFC 9559 §5.1.4.1.28.16, including §5.1.4.1.28.17..§5.1.4.1.28.40 sub-elements and the SMPTE 2086 / CTA-861.3 HDRMasteringMetadata):MkvDemuxer::video_colour(stream_index)(and the per-streamvideo_colours()slice) folds theColourmaster's children into a single typedVideoColour. Each ofMatrixCoefficients,TransferCharacteristics,Primaries,ColourRange,ChromaSitingHorzandChromaSitingVertsurfaces as a typed enum; forward-compat values outside the registered tables pass through via anOther(u64)variant (§27 leaves registries open for future additions).BitsPerChannel,ChromaSubsampling{Horz,Vert},CbSubsampling{Horz,Vert},MaxCLL/MaxFALLsurface as the raw unsigned integer (Optional when the spec doesn't define a default). The nestedMasteringMetadata(§5.1.4.1.28.30..§5.1.4.1.28.40) surfaces asOption<&MasteringMetadata>with the sixPrimary{R,G,B}Chromaticity{X,Y}floats, the twoWhitePointChromaticity{X,Y}floats and theLuminance{Max,Min}cd/m² pair — each independently optional, since the spec does not require all-or-nothing. Spec defaults are materialised on the typed surface so an emptyColourmaster decodes as fully-typed unspecified (§5.1.4.1.28.17 / .26 / .27 default2; §5.1.4.1.28.23..25 default0). Non-video tracks (and video tracks with noColourchild) returnNone.Video > StereoModetyped decode (RFC 9559 §5.1.4.1.28.3):MkvDemuxer::video_stereo_mode(stream_index) -> Option<StereoMode>(and the per-streamvideo_stereo_modes()slice) returns the single-track stereo-3D packing —Mono/SideBySide{Left,Right}First/TopBottom{Left,Right}First/Checkboard{Left,Right}First/RowInterleaved{Left,Right}First/ColumnInterleaved{Left,Right}First/Anaglyph{CyanRed,GreenMagenta}/BothEyesLaced{Left,Right}First(the full §5.1.4.1.28.3 Table 5 set) plusOther(u64)for values registered after RFC 9559 (§27.7 leaves the registry open). The §5.1.4.1.28.3 default0(Mono) is materialised: aVideomaster with no explicitStereoModedecodes asSome(StereoMode::Mono), distinguishable fromNone(which means "noVideomaster at all"). Multi-track stereo (TrackOperation > TrackCombinePlanes, §5.1.4.1.30.1) is independent and surfaces throughtrack_operation; a single track MAY carry both. A convenienceStereoMode::is_stereo()returnstruefor any non-Monopacking.Video > OldStereoModetyped decode (RFC 9559 §5.1.4.1.28.5, id0x53B9,maxver 2):MkvDemuxer::video_old_stereo_mode(stream_index) -> Option<OldStereoMode>(and the per-streamvideo_old_stereo_modes()slice) surfaces the "bogus" stereo-3D mode value that [libmatroska] prior to 0.9.0 wrote at the wrong Element ID (0x53B9instead of0x53B8, §18.10). The spec marks the elementmaxver 2and says a Writer MUST NOT use it, but a Reader MAY support legacy files by reading it — which this accessor does. Its value space (Table 7) is incompatible with the modernStereoMode(Table 5): onlyMono(0) /RightEye(1) /LeftEye(2) /BothEyes(3) appear here, plusOther(u64)for any out-of-Table-7 value, so the surface is deliberately kept separate fromvideo_stereo_mode. UnlikeStereoMode, no spec default is materialised — a modern file with noOldStereoModeelement returnsNone, never a synthesisedMono; a track MAY carry both (a transitional file aware of the libmatroska bug), and the two surfaces report independently.OldStereoMode::is_stereo()/to_raw()mirror theStereoModehelpers. This closes the last RFC 9559 element-registry entry the crate had not yet handled.Video > Projectiontyped decode (RFC 9559 §5.1.4.1.28.41, including §5.1.4.1.28.42..§5.1.4.1.28.46):MkvDemuxer::video_projection(stream_index)(and the per-streamvideo_projections()slice) folds theProjectionmaster's children into a single typedProjection.ProjectionTypesurfaces as a typed enum (Rectangular/Equirectangular/Cubemap/Mesh/Other(u64)for values registered after RFC 9559 — §27.15 leaves the registry open).ProjectionPrivate(the verbatim ISOBMFF box body —equi/cbmp/mshp— that pairs with the projection type) surfaces verbatim asOption<&[u8]>and is never parsed or validated by the container; that's a renderer concern. The yaw / pitch / roll pose triple (degrees, ranges±180 / ±90 / ±180per §5.1.4.1.28.44..46) surfaces as threef64s with the spec default0.0materialised. An emptyProjectionmaster decodes as a fully-typed identity projection (rectangular + zero pose), distinguishable fromNone(which means "noProjectionmaster at all" — the common case for ordinary 2D video). The §5.1.4.1.28.46 worked example<Projection><ProjectionPoseRoll>90</ProjectionPoseRoll></Projection>(signalling a 90° counter-clockwise rotation) round-trips withprojection_type == Rectangular,pose_roll == 90.0, and the other pose components at their defaults. Convenience helpersProjectionType::is_spherical()andProjection::is_rotated()provide the headline yes/no answers. Non-video tracks (and video tracks with noProjectionchild) returnNone.Video > AlphaModetyped decode (RFC 9559 §5.1.4.1.28.4):MkvDemuxer::video_alpha_mode(stream_index) -> Option<AlphaMode>(and the per-streamvideo_alpha_modes()slice) folds the per-track WebM-alpha hint into a typed enum (None/Present/Other(u64)for values registered after RFC 9559 — §27.8 leaves the registry open). The §5.1.4.1.28.4 default0(None) is materialised: aVideomaster with no explicitAlphaModedecodes asSome(AlphaMode::None), distinguishable fromNone(which means "noVideomaster at all").AlphaMode::Present(value1) signals that the track'sBlockAdditionalelement withBlockAddID=1carries alpha-channel data per the codec mapping forCodecID(the WebM VP8/VP9 alpha extension is the canonical user). A convenienceAlphaMode::has_alpha()returnstrueexactly for thePresentvariant — values outside Table 6 are conservatively treated as "no alpha" because the spec leaves their semantics implementation-defined.Video > AspectRatioTypetyped decode (RFC 9559 Appendix A.24, reclaimed):MkvDemuxer::video_aspect_ratio_type(stream_index) -> Option<u64>(and the per-streamvideo_aspect_ratio_types()slice) surfaces the rawu64value rather than synthesising an enum — the reclaimed appendix says only "Specifies the possible modifications to the aspect ratio" and enumerates no values. ReturnsNonewhenever the file did not carry the element (the appendix specifies no default, so absence is not materialised).Video > UncompressedFourCCtyped decode (RFC 9559 §5.1.4.1.28.15):MkvDemuxer::video_uncompressed_fourcc(stream_index) -> Option<&UncompressedFourCC>(and the per-streamvideo_uncompressed_fourccs()slice) surfaces the 4-byte FourCC that identifies the uncompressed pixel layout. Spec-mandatory only whenCodecID == "V_UNCOMPRESSED"(Table 11); the typed surface carries the verbatim on-disk bytes viaas_bytes(), plus conveniencefourcc() -> Option<[u8; 4]>andas_str() -> Option<String>(UTF-8 lossy) accessors that returnNonewhenever the on-disk payload isn't exactly 4 bytes. A malformed non-4-byte payload is preserved verbatim rather than being dropped, so callers debugging a malformed file can still see what the writer emitted. Absence on any track is legal — the spec specifies no default — and returnsNone.Video > FlagInterlaced+FieldOrdertyped decode (RFC 9559 §5.1.4.1.28.1 + §5.1.4.1.28.2):MkvDemuxer::video_interlacing(stream_index)(and the per-streamvideo_interlacings()slice) folds both elements into a typedVideoInterlacing—flag()returns aFlagInterlacedenum (Undetermined/Interlaced/Progressive/Other(u64)) andfield_order()returnsSome(FieldOrder)(Progressive/Tff/Undetermined/Bff/TffInterleaved/BffInterleaved/Other(u64)) only when the track is actually interlaced. §5.1.4.1.28.2's "If FlagInterlaced is not set to 1, this element MUST be ignored" is honoured by the typed surface: a strayFieldOrderon a progressive / undetermined track silently resolves toNone. Spec defaults materialised — bareVideomaster with noFlagInterlacedchild decodes asUndetermined(default0); an interlaced track with no explicitFieldOrderdecodes asSome(FieldOrder::Undetermined)(default2). Non-video tracks (and video tracks with noVideomaster) returnNone.
- EBML header + Segment (unknown size) for a streaming-friendly layout.
DocTypeExtensionon write (RFC 8794 §11.2.9..§11.2.11):MkvMuxer::set_doc_type_extensions(Vec<DocTypeExtension>)queues the EBML-header extension declarations, emitted afterDocTypeReadVersionin the header atwrite_headertime. Takes the same demux-sideDocTypeExtensionrecordMkvDemuxer::ebml_headersurfaces, so a header→header copy round-trips every extension verbatim. Queue-time validation rejects post-write_headeruse, an emptyDocTypeExtensionName(§11.2.10 length>0), a zeroDocTypeExtensionVersion(§11.2.11 "not 0"), and a duplicate name (§11.2.10 "MUST be unique within the EBML Header"). Omitting the call (the default) keeps the header free of extensions — the common case. Pairs symmetrically withMkvDemuxer::ebml_header.- Fixed-size
SeekHeadat the start of the Segment with Seek entries forInfo,Tracks, andCues- so players that pre-walk the SeekHead (mpv, Chromium) jump straight to Cues without scanning. The CuesSeekPositionis patched inwrite_trailer; if no packets were written, the Cues entry is rewritten as a Void filler. Info(1 msTimecodeScale),Tracks, rollingClusters withSimpleBlockpayload, budgeted per RFC 9559 §25.1 ("no more than five seconds or five megabytes of content"): a new Cluster starts when the open one would pass 5 s or once its Block content reaches 5 MB (so a high-bitrate stream rotates on bytes long before the duration budget; a Cluster exceeds the byte budget by at most one Block).MkvMuxer::with_cluster_limits(max_duration_ms, max_bytes)tunes both budgets (duration capped ati16::MAXms — the Section 10 signed-16-bit Block-timestamp bound; bytes floored at 1024);cluster_limits()reads them back. Smaller budgets buy finer seek granularity and a smaller damage blast radius per broken Cluster at the cost of per-Cluster overhead.Cueselement emitted inwrite_trailer- index entries for every video keyframe and every audio cluster-start, so the resulting file is seekable without a second pass. Each entry carries the fullCueTrackPositionssub-tree, symmetric with the demux read surface:CueRelativePosition(RFC 9559 §5.1.5.1.2.3, recommended by §22.1) so seek-aware readers jump straight to the indexedSimpleBlockinside the Cluster;CueBlockNumber(§5.1.5.1.2.5) — the 1-based ordinal of the indexed Block within its Cluster (every block counted across all tracks in write order,range: not 0honoured); andCueDuration(§5.1.5.1.2.4) when the indexed packet carried a usable duration. Subtitle tracks follow §22.1's stronger recommendation ("each subtitle frame SHOULD be referenced by a CuePoint element with a CueDuration element"): aMediaType::Subtitletrack is indexed once per frame (each carrying itsCueDuration), while audio/video keep the once-per-cluster cadence.- Codec-specific fields:
CodecPrivatenormalisation for FLAC (fLaCmagic prepended), OpusCodecDelayderived from theOpusHeadpre-skip plus an 80 msSeekPreRollper the WebM spec. Chapters(RFC 9559 §5.1.7):MkvMuxer::add_chapter(start_ns, end_ns, title)queues a single English-languageChapterAtom;add_chapter_full(MkvChapter)takes a fully-specified record with multilingualChapterDisplayrows (ChapString+ChapLanguage- optional
ChapCountry+ optionalChapLanguageBCP47, §5.1.7.1.4.11 — when the BCP-47 tag is set the muxer writes it in place ofChapLanguage, which the spec says MUST be ignored when the BCP-47 form is present) plus the completeChapterAtomfield set —ChapterUID(§5.1.7.1.4.1, auto-derived non-zero whenNone),ChapterStringUID(.2),ChapterFlagHidden(.5),ChapterFlagEnabled(default1, written only when cleared),ChapterSegmentUUID(.6, 16-byte),ChapterSegmentEditionUID(.7),ChapterPhysicalEquiv(.8), and theChapProcess > ChapProcessCommandchapter-codec command tree (§5.1.7.1.4.14–19) viaMkvChapProcess/MkvChapProcessCommand(write-side mirror of the demuxChapProcesssurface).add_chapter_fullrejectsChapterUID 0/ChapterSegmentEditionUID 0(range "not 0") and a non-16-byteChapterSegmentUUID. Chapters must be added beforewrite_header; the muxer emits a singleEditionEntrybetween Tracks and the first Cluster and patches the SeekHeadChaptersslot to point at it (slot is voided if no chapters were queued).
- optional
Attachments(RFC 9559 §5.1.6):MkvMuxer::add_attachment(MkvAttachment { filename, mime_type, data, uid, description })queues oneAttachedFile. Attachments must be added beforewrite_header; the muxer emits theAttachmentsmaster right afterChapters(or directly afterTrackswhen no chapters are queued) and patches the SeekHeadAttachmentsslot to point at it (slot is voided if no attachments were queued). Field handling matches the demux side field-for-field so an end-to-end demux→mux pipeline preserves attachments:FileName(§5.1.6.1.2) +FileMediaType(§5.1.6.1.3) are mandatory and rejected up front when empty;FileUID(§5.1.6.1.5,range: not 0) auto-derives from the 1-based attachment index when the caller passesNone, and an explicitSome(0)is rejected;FileDescription(§5.1.6.1.1) is omitted on disk whenNoneor empty.MkvAttachment::new(filename, mime_type, data)is a convenience constructor mirroring the demux-side typed surface.Tags(RFC 9559 §5.1.8):MkvMuxer::add_tag(MkvTag)queues oneTagper call, emitted as the file's singleTagsmaster (§5.1.8,maxOccurs: 1) sandwiched afterAttachmentsand before the firstClusterso the demuxer's single-pass header walk catches it. Tags must be added beforewrite_header; the SeekHead grew a sixth slot (Tags, betweenAttachmentsandCues) that is patched to the emitted master's offset or voided when no tags were queued, and the master carries a leadingCRC-32child like the other Top-Level masters (§6.2).MkvTagpairs anMkvTagTargetsscope with one or moreMkvSimpleTag(name, value)descriptors — the write-side mirror of the demuxtags()surface, field-for-field, so a demux→mux pipeline preserves tags.MkvTagTargetscarriesTargetTypeValue(§5.1.8.1.1.1, omitted whenNone,Some(0)rejected perrange: not 0),TargetType(§5.1.8.1.1.2, informational), and the fourTagTrackUID/TagEditionUID/TagChapterUID/TagAttachmentUIDlists (§5.1.8.1.1.3..§5.1.8.1.1.6 — multi-UID scoping, a0UID dropped since "all of that kind" is expressed by omission). The muxer assigns each trackTrackUID == track_number(1-based), so a tag scoped viaMkvTagTargets::track(N)resolves back to stream indexN - 1on read.MkvSimpleTagcarriesTagName(§5.1.8.1.2.1, mandatory),TagLanguage(§5.1.8.1.2.2, default"und"left off-disk),TagLanguageBCP47(§5.1.8.1.2.3, written instead ofTagLanguagewhen present per the spec's "MUST be ignored" rule),TagDefault(§5.1.8.1.2.4, default1written only when cleared), aTagString(§5.1.8.1.2.5) /TagBinary(§5.1.8.1.2.6) payload enum (MkvSimpleTagValue), and achildrenlist for the spec'srecursive: Truenesting — symmetric with the demux-sideSimpleTag::children. Queue-time validation rejects an emptysimple_tagslist (§5.1.8.1.2minOccurs: 1), an emptyTagNameat any nesting depth (§5.1.8.1.2.1),TargetTypeValue 0, and any call afterwrite_header. Convenience constructors:MkvTag::global(name, value),MkvTagTargets::track(uid),MkvSimpleTag::new(name, value)/MkvSimpleTag::binary(name, data).- Cluster
Position/PrevSizehints (RFC 9559 §5.1.3.2 + §5.1.3.3): opt-in viaMkvMuxer::with_cluster_position_hints(). Every Cluster gains aPositionchild (its own Segment Position — the element the spec offers as a damaged-stream resync hint) right after itsTimestamp, and every Cluster from the second on gains aPrevSizechild (the previous Cluster's full element size in octets — the backward-play jump: a Cluster's start minus itsPrevSizelands on the previous Cluster's ID). Off by default, so muxer output stays byte-identical with prior releases. Round-trip tests verify the exact offset semantics (including on the surviving Clusters after a damage resync); black-box validated against a widely-deployed reader. - Front-
Cueslayout (RFC 9559 §25.3.3 "Optimum Layout with Cues at the Front"): opt-in viaMkvMuxer::with_front_cues(reserved_bytes).write_headerreserves a caller-sizedVoidbetween the last pre-Cluster master and the first Cluster;write_trailerwrites the finishedCuesinto the slot (fillerVoidover the remainder; a 1-byte remainder is absorbed by widening theCuessize VINT) and patches the SeekHeadCuesentry to it — so seek-aware players get the whole index without first seeking to the end of the file. If the finished index doesn't fit the reservation, the muxer falls back to the ordinary end placement (theVoidstays, the file stays valid, the SeekHead points at the endCues). Conflicts withwith_live_streamingin both directions (§25.3.4: a live stream writes no Cues). Works in strict WebM mode (Cues + Void are both in-profile). Black-box validated with a widely-deployed prober. - SeekHead-expansion
Void(RFC 9559 §25.2 "It is RECOMMENDED that the first SeekHead element be followed by a Void element to allow for the SeekHead element to be expanded"): opt-in viaMkvMuxer::with_seek_head_expansion_void(reserved_bytes).write_headerwrites aVoidof exactly the requested size (>= 2, the smallest encodableVoid) immediately after the SeekHead, beforeInfo, so a downstream editor can grow the SeekHead in place — §25.2 sizes the reservation "depending on the Tags, Chapters, and Attachments elements", hence caller-sized (one extra fixed-width Seek entry costs 21 bytes). The muxer itself never grows its SeekHead (all six slots are reserved up front), so its own output keeps theVoidintact; the patched SeekPositions still land on their targets with theVoidin between. Conflicts withwith_live_streamingin both directions (§25.3.4 writes no SeekHead). In-profile under strict WebM (Voidis guidelines-Supported). - Two-pass
Durationfinalization (RFC 9559 §5.1.2.10): opt-in viaMkvMuxer::with_duration_finalization().write_headerreserves aDuration-sizedVoidinsideInfo(at the §5.1.2 element-order position) andwrite_trailerpatches it in place with the measured duration — the maximum packet end time (pts + duration) across all streams — then rewrites the InfoCRC-32payload over the patched body (RFC 8794 §11.3.1; skipped in strict WebM mode where no CRC child exists). Failure modes are truthful: a crashed producer (no trailer) or a zero-packet mux leaves a harmlessVoid, never a bogus duration. Conflicts are rejected in both directions withset_duration(explicit vs measured) andwith_live_streaming(forward-only output has no known end to patch). Black-box validated: a widely-deployed prober reads the patched duration. - Livestreaming layout (RFC 9559 §25.3.4 + §23.2): opt-in via
MkvMuxer::with_live_streaming(). Omits the up-frontSeekHeadand writes noCuesinwrite_trailer("SeekHead and Cues are useless" in a live stream; §23.2 says a stream with neither at its start SHOULD be considered non-seekable — the signal a live producer wants to send). Everything other than Clusters still lands before the Clusters, and the Segment / Cluster unknown-size VINTs §23.2 mandates were already the muxer's default, so the output can be cut at any Cluster boundary. Combined withwith_cluster_position_hints(), each Cluster'sPositionis written as0— the §5.1.3.2 live-stream convention — whilePrevSizestays real. Pairs with the resilient demuxer: a live capture cut at an arbitrary byte still demuxes its packet prefix, andseek_toworks through the Cues-less Cluster-Timestamp fallback. - §23.2 live tagging (
MkvMuxer::write_live_tags): on a livestreaming muxer, emits aTagselement between Clusters ("The Tags element can be placed between Clusters each time it is necessary"), flushing in-flight laces and ending the open unknown-size Cluster. The demux side applies the §23.2 MUST-reset when its walk crosses a mid-streamTags:tags()is replaced wholesale and the tag-derived flat-metadata entries are swapped in place (Info- / Chapters- / Attachments-derived metadata untouched), with UID scopes resolved against the open-time maps and a leadingCRC-32validated ontocrc_status(). The same read path surfaces a trailingTagselement placed after the last Cluster — a common Writer layout the single-pass open walk never reaches — once the stream is drained. - WebM profile:
mux::open_webmpinsDocType="webm"and rejects any stream whose codec isn't VP8/VP9/AV1 video or Vorbis/Opus audio withError::Unsupported. - Strict WebM element gating (default on a WebM muxer): emission is
gated to the WebM guidelines' supported subset (the same table the
webmmodule scans against), sowebm::scan(output).is_conformant()holds. Queue-time setters whose elements are guidelines-UnsupportedreturnError::Unsupportednaming the element and the opt-out —Attachments,TrackOperation,TrackTranslate, the legacyTrackEntryset, Linked-Segment Info metadata,MaxBlockAdditionID,DefaultDecodedFieldDuration/TrackTimestampScale,AttachmentLink,ContentCompression(encryption stays in-profile; the signing quartet doesn't), off-profile chapter fields (ChapterFlagHidden/ legacy enabled flag / Medium-Linking pair /ChapterPhysicalEquiv/ChapProcess), and edition / chapter / attachment Tag scopes (track + global scopes stay). Emission-level gates: Top-Level masters carry noCRC-32child (guidelines- unsupported), the per-ClusterPositionhint is suppressed whilePrevSizesurvives, the chaptersEditionEntryomitsEditionUID,BlockGroupextras (ReferencePriority/CodecState/ reclaimed children) are rejected per call, and a queuedSilentTrackslist errors at the next Cluster open.MkvMuxer::with_webm_lenient()restores the full Matroska surface under thewebmDocType;webm_strict()reports the mode. Matroska muxers are unaffected. - CRC-32 on Top-Level masters (RFC 8794 §11.3.1, RFC 9559 §6.2):
the muxer prepends a 6-byte
CRC-32child (id0xBF, fixed size 4, little-endian IEEE CRC-32 of the rest of the element's data) to every Top-Level master it buffers end-to-end before flushing —Info,Tracks,Cues, plusChaptersandAttachmentswhen those are queued. RFC 9559 §6.2 says "all Top-Level Elements of an EBML Document SHOULD include a CRC-32 element as their first Child Element," and the in-tree demuxer'svalidate_top_level_crcpeel-off-leading-CRC rule verifies every emitted master round-trips to a matching stored / computed pair.SeekHeadis deliberately not CRC'd — its Cues entry is patched inwrite_trailer, which would invalidate any CRC computed up front.Clusteris not CRC'd because the muxer streams Clusters with the unknown-size VINT and RFC 8794 §11.3.1 requires a bounded body for CRC. Video > FlagInterlaced+FieldOrderon write (RFC 9559 §5.1.4.1.28.1 + §5.1.4.1.28.2):MkvMuxer::set_video_interlacing( stream_index, FlagInterlaced, Option<FieldOrder>)queues a per-track interlacing hint that lands inside the track'sVideomaster atwrite_headertime, alongside the existingPixelWidth/PixelHeight. The demux-sideFlagInterlaced/FieldOrderenums gainedto_raw()inverses so every Table 3 / Table 4 value round-trips, including theOther(u64)forward-compat variant on both. Spec rules enforced at queue time: the call rejects post-write_headeruse, out-of-rangestream_index, non-video tracks, andFieldOrderpaired with anything other thanFlagInterlaced::Interlaced(the §5.1.4.1.28.2 "If FlagInterlaced is not set to 1, this element MUST be ignored" rule applied symmetrically on write). Omitting the call leaves both elements off-disk so the demuxer materialises the §5.1.4.1.28.1 default0/ §5.1.4.1.28.2 default2(Undetermined). Pairs symmetrically with the existingMkvDemuxer::video_interlacingtyped accessor — a mux→demux pipeline preserves the interlacing pair bit-exactly.Videogeometry quartet on write (RFC 9559 §5.1.4.1.28.8..§5.1.4.1.28.14):MkvMuxer::set_video_geometry(stream_index, MkvVideoGeometry)queues a per-track hint that lands inside the track'sVideomaster atwrite_headertime, alongsidePixelWidth/PixelHeight. The hint carriesPixelCrop{Top,Bottom,Left,Right}(§5.1.4.1.28.8..11),DisplayWidth/DisplayHeight(§5.1.4.1.28.12 / .13), andDisplayUnit(§5.1.4.1.28.14). The demux-sideDisplayUnitenum gained ato_raw()inverse so every Table 10 value round-trips, including theOther(u64)forward-compat variant (§27.9 leaves the "Matroska Display Units" registry open). Per-element omission rules: zero crops stay off-disk (spec default0);DisplayWidth/DisplayHeightare written whenSomeand skipped whenNone;DisplayUnitis written explicitly only for non-Pixelsvalues (omitting it lets the demuxer materialise the §5.1.4.1.28.14 spec default). Spec rules enforced at queue time: rejects post-write_headeruse, out-of-rangestream_index, calls on non-video tracks, andSome(0)on eitherdisplay_width/display_heightper the §5.1.4.1.28.12 / .13range: not 0pin. Convenience constructorsMkvVideoGeometry::cropped(top, bottom, left, right)(RFC 9559 §11.1 pillar-box / letterbox shape, no display-size override,Pixelsunit) andMkvVideoGeometry::aspect_ratio(num, den)(DisplayUnit::DisplayAspectRatio+ the ratio encoded asDisplayWidth/DisplayHeight) cover the two common shapes. Pairs symmetrically with the existingMkvDemuxer::video_geometrytyped accessor — a mux→demux pipeline preserves the quartet bit-exactly, including the §5.1.4.1.28.12 / .13 derived-default behaviour when display dimensions were omitted on write andDisplayUnit == Pixels.Video > StereoMode+AlphaModeon write (RFC 9559 §5.1.4.1.28.3 + §5.1.4.1.28.4):MkvMuxer::set_video_stereo_mode(stream_index, StereoMode)andMkvMuxer::set_video_alpha_mode(stream_index, AlphaMode)queue per-track hints that land inside the track'sVideomaster atwrite_headertime. The demux-sideStereoModeandAlphaModeenums gainedto_raw()inverses so every Table 5 / Table 6 value round-trips, including theOther(u64)forward-compat variant on both (§27.7 / §27.8 leave the "Matroska Stereo Modes" / "Matroska Alpha Modes" registries open). Spec rules enforced at queue time: both setters reject post-write_headeruse, out-of-rangestream_index, and calls on non-video tracks. The two settings are independent — setting one does not affect the other. Omitting the call leaves the element off-disk so the demuxer materialises the §5.1.4.1.28.3 default0(Mono) / §5.1.4.1.28.4 default0(None). Callingset_video_stereo_mode(_, StereoMode::Mono)/set_video_alpha_mode(_, AlphaMode::None)explicitly still writes the element on disk — that is the way for a producer to override a downstream tool that might infer something else. Pairs symmetrically with the existingMkvDemuxer::video_stereo_mode/MkvDemuxer::video_alpha_modetyped accessors.Video > OldStereoModeon write (RFC 9559 §5.1.4.1.28.5, id0x53B9):MkvMuxer::set_video_old_stereo_mode(stream_index, OldStereoMode)queues the legacy libmatroska-bug element, written inside the track'sVideomaster atwrite_headertime. This is a legacy / re-mux-only surface: the spec marks the elementmaxver 2and says a Writer MUST NOT use it for new files (modern stereo-3D belongs inset_video_stereo_modeorset_track_operation). It exists solely so a faithful re-mux of a Matroska v2 / libmatroska-bug source can round-trip the element the demuxer surfaced throughvideo_old_stereo_mode.OldStereoMode::to_raw()round-trips every Table 7 value plus theOther(u64)forward-compat variant. Omitting the call (the default) keeps the element off-disk — the correct behaviour for every modern file. Spec rules enforced at queue time: rejects post-write_headeruse, out-of-rangestream_index, and calls on non-video tracks. Pairs symmetrically withMkvDemuxer::video_old_stereo_mode— a mux→demux pipeline preserves the legacy value bit-exactly. With this, every element in the RFC 9559 element-ID registry is now both read and written.Video > UncompressedFourCCon write (RFC 9559 §5.1.4.1.28.15):MkvMuxer::set_video_uncompressed_fourcc(stream_index, [u8; 4])queues a per-track FourCC hint that lands inside the track'sVideomaster atwrite_headertime (id0x2EB524,binarytype, schema-fixedlength: 4). The setter takes a[u8; 4]array directly, so the schema's fixed length is enforced at the type system; every byte (including high bytes and0x00) is written verbatim — the element isbinary, notstring, and the muxer never interprets the payload as text. Spec rules enforced at queue time: the setter rejects post-write_headeruse, out-of-rangestream_index, and calls on non-video tracks. Omitting the call leaves the element off-disk so the demuxer'sMkvDemuxer::video_uncompressed_fourccsurfacesNone— §5.1.4.1.28.15 defines no default, and Table 11'sminOccurs=1only fires forCodecID == "V_UNCOMPRESSED", which the muxer does not presently emit. Pairs symmetrically with the existingMkvDemuxer::video_uncompressed_fourcctyped accessor — a mux→demux pipeline preserves the four-byte FourCC bit-exactly.Video > AspectRatioTypeon write (RFC 9559 Appendix A.24, reclaimed, id0x54B3):MkvMuxer::set_video_aspect_ratio_type( stream_index, u64)queues a per-track hint that lands inside the track'sVideomaster atwrite_headertime as a plainuintegerelement. The reclaimed appendix documents the element only as "Specifies the possible modifications to the aspect ratio" and enumerates no values and no default, so the setter takes the rawu64verbatim — mirroring the demux side, which deliberately surfaces it as a rawOption<u64>rather than a synthesised enum. Per-element omission rule: the element is written only when the caller opts in; an explicit0is written and round-trips asSome(0)(distinct from absence, since the appendix defines no default). Spec rules enforced at queue time: the setter rejects post-write_headeruse, out-of-rangestream_index, and calls on non-video tracks. Omitting the call leaves the element off-disk so the demuxer'sMkvDemuxer::video_aspect_ratio_typesurfacesNone. Pairs symmetrically with the existingMkvDemuxer::video_aspect_ratio_typetyped accessor — a mux→demux pipeline preserves the raw value bit-exactly. This closes the last remainingVideosub-element that the demux side read but the mux side could not write.Video > Colourscalar children on write (RFC 9559 §5.1.4.1.28.16, §5.1.4.1.28.17..§5.1.4.1.28.29):MkvMuxer::set_video_colour(stream_index, MkvVideoColour)queues a per-track colour-description hint that lands inside the track'sVideomaster atwrite_headertime as aColourmaster (id0x55B0) carrying the eleven scalar children:MatrixCoefficients/BitsPerChannel/ChromaSubsampling{Horz,Vert}/CbSubsampling{Horz,Vert}/ChromaSiting{Horz,Vert}/Range/TransferCharacteristics/Primaries/MaxCLL/MaxFALL. Convenience constructorsMkvVideoColour::bt709()(matrix1/ transfer1/ primaries1/ broadcast range — the canonical SDR HD shape) andMkvVideoColour::bt2020_pq()(matrix9/ transfer16/ primaries9/ full range / 10 bpc — the canonical HDR10 shape) cover the two everyday cases; every field can be overridden on the returned value for one-off departures. Per-element omission rules apply at write time: every scalar that equals its §5.1.4.1.28 spec default is left off-disk so the demuxer materialises the spec default; everyOption<u64>(the four chroma-subsampling integers +MaxCLL/MaxFALL) is written whenSome(v)and skipped whenNone. As a result, queueingMkvVideoColour::default()writes an empty 3-byteColourmaster (id0x55B0+ size VINT0x80), which the demuxer parses intoSome(VideoColour::default())with every getter returning the materialised spec default — distinguishable on disk from the call-was-omitted case, which keeps theColourmaster off-disk entirely so the demuxer surfacesNonefromvideo_colour. Spec rules enforced at queue time: the setter rejects post-write_headeruse, out-of-rangestream_index, and calls on non-video tracks. TheColour > MasteringMetadatasub-master (§5.1.4.1.28.30..§5.1.4.1.28.40, id0x55D0) is emitted whenever the queued hint carriesmastering_metadata: Some(MkvMasteringMetadata); inside that master each chromaticity / luminance child (PrimaryRChromaticityX/Y/PrimaryGChromaticityX/Y/PrimaryBChromaticityX/Y/WhitePointChromaticityX/Y/LuminanceMax/LuminanceMin, ids0x55D1..0x55DA) is written as an 8-byte big-endianf64only when its ownOption<f64>slot isSome(v)— mirroring the per-child omission rules above. ASome(MkvMasteringMetadata::default())(every slotNone) serialises as an empty 3-byteMasteringMetadatamaster that the demuxer parses intoSome(MasteringMetadata::default()); settingmastering_metadata: Nonekeeps the entire sub-master off-disk so the demuxer surfacesNonefrommastering_metadata(). The convenienceMkvMasteringMetadata::bt2020_d65_hdr10()populates the ten-child set with BT.2020 primaries + D65 white point + 1000 cd/m² peak / 0.005 cd/m² floor — the canonical HDR10 mastering display. Pairs symmetrically with the existingMkvDemuxer::video_colourtyped accessor — a mux→demux pipeline preserves every scalar child verbatim, including theOther(u64)forward-compat variants on each of the six enum-typed children, plus every populatedMasteringMetadatachromaticity / luminance child.Video > Projectionmaster on write (RFC 9559 §5.1.4.1.28.41, including §5.1.4.1.28.42..§5.1.4.1.28.46):MkvMuxer::set_video_projection(stream_index, MkvProjection)queues a per-track hint that lands inside the track'sVideomaster atwrite_headertime, after theColourmaster, as aProjectionmaster (id0x7670). The demux-sideProjectionTypeenum gained ato_raw()inverse so every Table 18 value round-trips, including theOther(u64)forward-compat variant (§27.15 leaves the registry open). Per-element omission rules:ProjectionTypeis written only for non-Rectangulartypes (the §5.1.4.1.28.42 default0stays off-disk); eachProjectionPose{Yaw,Pitch,Roll}child is written as an 8-byte big-endianf64only when non-zero (the §5.1.4.1.28.44..46 default0.0stays off-disk);ProjectionPrivate(the verbatim ISOBMFF box body —equi/cbmp/mshp) is written only whenSome(_)and is never interpreted by the muxer. QueueingMkvProjection::default()writes an emptyProjectionmaster that the demuxer parses intoSome(Projection::default()); omitting the call keeps the master off-disk so the demuxer surfacesNone. Convenience constructorsMkvProjection::equirectangular(private)(the 360°-VR shape) andMkvProjection::rotated(roll_degrees)(the §5.1.4.1.28.46 worked example) cover the two common shapes. Spec rules enforced at queue time: rejects post-write_headeruse, out-of-rangestream_index, and calls on non-video tracks. Pairs symmetrically with the existingMkvDemuxer::video_projectiontyped accessor — a mux→demux pipeline preserves the projection record (type, pose, and verbatimProjectionPrivatepayload) bit-exactly.- TrackEntry audience flags on write (RFC 9559
§5.1.4.1.6..§5.1.4.1.11):
MkvMuxer::set_track_audience_flags(stream_index, MkvTrackAudienceFlags)queues a per-track hint whose sixOption<bool>slots —forced(FlagForced, id0x55AA),hearing_impaired(FlagHearingImpaired, id0x55AB),visual_impaired(FlagVisualImpaired, id0x55AC),text_descriptions(FlagTextDescriptions, id0x55AD),original(FlagOriginal, id0x55AE),commentary(FlagCommentary, id0x55AF) — land directly inside theTrackEntry(the elements sit onTrackEntryitself, not in a sub-master) atwrite_headertime, afterFlagLacing, in numerical-id order. Per-element omission rule: eachSome(v)slot writes the element explicitly as0/1; eachNoneslot stays off-disk. ForFlagForced(the only one with a spec default), omission andSome(false)decode identically (false) but differ on disk — the explicit write is the way to override a downstream tool. For the five default-lessminver: 4flags the distinction is semantic: omission decodes asNonewhileSome(false)round-trips asSome(false), preserving the §5.1.4.1.7..§5.1.4.1.11 "set to 1 if and only if …" explicit-zero signal. Unlike theset_video_*family there is no track-type restriction — the spec carries all six elements on everyTrackEntry, so audio / video / subtitle tracks all accept the call (mirroring the demux side, which surfaces a record for every track). The muxer already pinsDocTypeVersionto4, so emitting theminver: 4elements never violates the declared document version. Convenience constructorsMkvTrackAudienceFlags::forced_subtitle()/hearing_impaired_track()/visual_impaired_track()/commentary_track()cover the common single-flag shapes. Rejects post-write_headeruse and out-of-rangestream_index. Pairs symmetrically with the existingMkvDemuxer::track_audience_flagstyped accessor — a mux→demux pipeline preserves every explicit flag, including theSome(false)-vs-absent distinction. - Per-Block
BlockAdditionson write (RFC 9559 §5.1.3.5.2 + §5.1.4.1.16):MkvMuxer::write_packet_with_additions(&packet, &[MkvBlockAddition])emits the packet as aBlockGroup(§5.1.3.5) instead of aSimpleBlock—Block(frame bytes, unlaced; any pending same-track lace is flushed first so Block order is preserved),BlockAdditionswith oneBlockMoreper addition in slice order (each writingBlockAdditionalverbatim andBlockAddIDonly when it differs from the §5.1.3.5.2.3 default1),BlockDuration(§5.1.3.5.3) when the packet carries a duration (aSimpleBlockcould not have carried it), andReferenceBlock(§5.1.3.5.5) when the packet is not a keyframe (a plainBlockhas no KEY flag bit; keyframe-ness is the element's absence — the relative value points at the track's most recently written Block, falling back to the spec-sanctioned0"reference unknown" when there is none). Prerequisite: declare the track's maximum id viaMkvMuxer::set_max_block_addition_id(stream_index, max)beforewrite_header— it lands as theMaxBlockAdditionIDTrackEntry element, andwrite_packet_with_additionsrejects an undeclared stream (§5.1.4.1.16's default0means "no BlockAdditions for this track"), aBlockAddIDof0(range "not 0"), an id above the declared maximum, and duplicate ids within one call (§5.1.3.5.2.3 uniqueness MUST) — all before any byte is written. An empty additions slice degrades to plainwrite_packetbehaviour (BlockMoreis mandatory inside the master, so an emptyBlockAdditionswould be malformed). The convenience constructorMkvBlockAddition::codec_defined(data)covers theBlockAddID = 1shape (e.g. WebM alpha — pair withset_video_alpha_mode). Pairs symmetrically with the newMkvDemuxer::block_additions/max_block_addition_idtyped accessors — a mux→demux pipeline preserves every addition byte-for-byte, plus the packet's keyframe flag and duration. - Full
BlockGroupchild set on write (RFC 9559 §5.1.3.5.4–§5.1.3.5.7):MkvMuxer::write_packet_with_block_group(&packet, &BlockGroupOptions)wraps a packet in aBlockGroupcarrying any combination ofBlockAdditions, an explicit multi-ReferenceBlocklist (§5.1.3.5.5),ReferencePriority(§5.1.3.5.4, written only when non-zero),CodecState(§5.1.3.5.6) andDiscardPadding(§5.1.3.5.7), emitted in §5.1.3.5 order, plus the reclaimed DivX trick-track / old-lacing children (RFC 9559 Appendix A.3..A.14):block_virtual(BlockVirtual),reference_virtual(ReferenceVirtual),slices(aVec<TimeSlice>emitted as oneSlicesmaster, eachTimeSlicebuilt viaTimeSlice::from_fields), andreference_frame(ReferenceFrame::from_fields). The write-side mirror ofMkvDemuxer::block_group_meta— a mux→demux round-trip preserves every child value, including an emptyTimeSlice(the on-disk element count is preserved). A group needing no additions skips theMaxBlockAdditionIDrequirement. SilentTrackson write (RFC 9559 Appendix A.1 / A.2):MkvMuxer::set_next_cluster_silent_tracks(&[track_numbers])queues aSilentTrackNumberlist for the next Cluster the muxer opens (drained after one Cluster to match the element's per-Cluster scope);MkvMuxer::track_number(stream_index)maps a stream index to its on-wireTrackNumber.- Opt-in block lacing on write (RFC 9559 §5.1.4.5.5, §10.3):
MkvMuxer::with_block_lacing(LacingMode::{Xiph,Ebml,FixedSize})beforewrite_headeraggregates same-track, same-keyframe-status consecutive frames (up to 8 per Block, never crossing a cluster boundary) into a single lacedSimpleBlock. Default staysLacingMode::None(one frame per Block,FlagLacing = 0) for byte-identical back-compat. When lacing is on, the muxer writesTrackEntry.FlagLacing = 1, sets the LACING bits in the SimpleBlock flags byte to the requested mode, and encodes the per-frame size header (Xiph 255-additive octets, EBML signed-VINT deltas, or no header for fixed-size). For fixed-size mode, a frame whose size differs from the buffered run flushes the lace and starts a new one. Demuxer side already handles all three modes — the new write path completes the round-trip in-tree. Audiomaster children on write (RFC 9559 §5.1.4.1.29, §5.1.4.1.29.1..§5.1.4.1.29.4):MkvMuxer::set_track_audio(stream_index, MkvTrackAudio)queues a per-track hint that lands inside the track'sAudiomaster (id0xE1) atwrite_headertime. The muxer already derives a minimalAudiomaster from the stream'sStreamInfo(sample_rate→SamplingFrequency,channels→Channels, sample-format bit width →BitDepth); this hint lets a caller override those derived children and supply the one child theStreamInfo-derived path cannot express:OutputSamplingFrequency(id0x78B5, §5.1.4.1.29.2), the Spectral Band Replication (SBR) output rate the demux-sidetrack_audio/TrackAudio::is_sbr()accessor already reads back. Per-field rule: aSome(v)overrides theStreamInfo-derived child; aNonedefers to theStreamInfovalue (and foroutput_sampling_frequency, simply omits the element). Children that resolve to nothing stay off-disk so the demuxer materialises the §5.1.4.1.29.1 default8000.0/ §5.1.4.1.29.3 default1(mono);BitDepthhas no spec default, so its absence surfaces asNone. The convenience constructorMkvTrackAudio::sbr(core)produces the canonical HE-AAC pair (core,2*core). Spec range checks enforced at queue time:SamplingFrequency/OutputSamplingFrequencyranged> 0x0p+0(aSome(v)<= 0.0/ non-finite is rejected),Channels/BitDepthrangednot 0(aSome(0)is rejected). Track-type restriction mirrors the demux side (which returnsNonefor non-audio tracks): the setter rejects non-Audiostreams plus post-write_headeruse and out-of-rangestream_index; repeated calls are last-write-wins; the read-backMkvMuxer::track_audio(stream_index)accessor returns the queued hint pre-write_header. Pairs symmetrically with the existingMkvDemuxer::track_audiotyped accessor — a mux→demux pipeline preserves every supplied child bit-exactly, including theOutputSamplingFrequencySBR signal.TrackEntrytiming trio on write (RFC 9559 §5.1.4.1.13..§5.1.4.1.15):MkvMuxer::set_track_timing(stream_index, MkvTrackTiming)queues a per-track hint whose threeOptionslots —default_duration(DefaultDuration, id0x23E383),default_decoded_field_duration(DefaultDecodedFieldDuration, id0x234E7A), andtrack_timestamp_scale(TrackTimestampScale, id0x23314F) — land directly inside theTrackEntry(no gating master) atwrite_headertime, afterMaxBlockAdditionID. Per-field omission rule: eachSome(v)writes the element explicitly, eachNonestays off-disk (the demuxer surfacesNonefor the two durations and materialises the §5.1.4.1.15TrackTimestampScaledefault1.0). There is no track-type restriction — the spec carries all three on everyTrackEntry. Spec range checks enforced at queue time: the two durations are rangednot 0(aSome(0)is rejected) andTrackTimestampScaleis ranged> 0x0p+0(a non-finite / non-positiveSome(v)is rejected); the setter also rejects post-write_headeruse and out-of-rangestream_index. The convenience constructorMkvTrackTiming::from_frame_rate(fps)rounds1e9 / fpsto the nanosecondDefaultDurationinterval (rejecting non-finite / non-positive fps). Repeated calls are last-write-wins; the read-backMkvMuxer::track_timing(stream_index)accessor returns the queued hint pre-write_header. Pairs symmetrically with the newMkvDemuxer::track_timingtyped accessor — a mux→demux pipeline preserves every supplied child bit-exactly, including theDefaultDuration-derived nominal frame rate.TrackEntryidentity / selection on write (RFC 9559 §5.1.4.1.18 / .19 / .20 / .23 / .4 / .5 / .12 / .24):MkvMuxer::set_track_identity(stream_index, MkvTrackIdentity)queues a per-track hint whose eightOptionslots —name(Name, id0x536E),codec_name(CodecName, id0x258688),language(Language, id0x22B59C),language_bcp47(LanguageBCP47, id0x22B59D),flag_enabled(FlagEnabled, id0xB9),flag_default(FlagDefault, id0x88),flag_lacing(FlagLacing, id0x9C), andattachment_link(AttachmentLink, id0x7446) — land directly inside theTrackEntry(no gating master) atwrite_headertime. Per-field omission rule: eachSome(v)writes the element explicitly, eachNonestays off-disk (the demuxer materialises the §default1for the three selection flags andNonefor the strings / link). Thelanguageslot, whenSome, overrides theStreamInfo-derivedLanguage; theflag_lacingslot, whenSome, overrides the muxer's auto-derivedFlagLacing. Per §5.1.4.1.20, when bothlanguageandlanguage_bcp47areSomethe muxer writes onlyLanguageBCP47(the spec saysLanguageMUST be ignored when BCP-47 is present), mirroring theTagLanguageBCP47handling. There is no track-type restriction — the spec carries all eight on everyTrackEntry. Spec checks enforced at queue time:attachment_linkis rangednot 0(aSome(0)is rejected, §5.1.4.1.24); an emptyName/CodecName/Language/LanguageBCP47string is rejected; plus post-write_headeruse and out-of-rangestream_index. Convenience constructorsMkvTrackIdentity::named(name)/MkvTrackIdentity::language_bcp47(lang)/MkvTrackIdentity::non_default()cover the common shapes; a read-backMkvMuxer::track_identity(stream_index)returns the queued hint pre-write_header. Pairs symmetrically with the newMkvDemuxer::track_identitytyped accessor — a mux→demux pipeline preserves every supplied element, including the BCP-47 precedence and the explicit-0-vs-absent flag distinction.TrackOperationon write (RFC 9559 §5.1.4.1.30):MkvMuxer::set_track_operation(stream_index, MkvTrackOperation)queues a per-track virtual-track recipe that lands as aTrackOperationmaster (id0xE2) directly inside the carryingTrackEntry(sibling toVideo/Audio) atwrite_headertime.MkvTrackOperationcarries acombine_planes: Vec<MkvTrackPlane>(TrackCombinePlanes, §5.1.4.1.30.1 — eachMkvTrackPlanepairs a 0-indexed sourcestream_indexwith aTrackPlaneType, §5.1.4.1.30.4) and ajoin_tracks: Vec<usize>(TrackJoinBlocks, §5.1.4.1.30.5). Each plane / join reference's stream index is resolved to the source track's on-diskTrackUIDat write time — the symmetric inverse of the demux side, which resolves eachTrackPlaneUID(§5.1.4.1.30.3) /TrackJoinUID(§5.1.4.1.30.6) back to a stream index. The demux-sideTrackPlaneTypeenum gained ato_raw()inverse so every Table 20 value (LeftEye/RightEye/Background) round-trips, including theOther(u64)forward-compat variant (§27.17 leaves the "Matroska Track Plane Types" registry open). Both operation kinds may coexist on one track. Convenience constructorsMkvTrackOperation::stereo_3d(left, right)(the canonical left/right-eye 3D recipe) andMkvTrackOperation::join(streams)cover the two common shapes. Spec rules enforced at queue time: rejects post-write_headeruse, out-of-rangestream_index, an empty operation (TrackCombinePlanes/TrackJoinBlocksexist only to carry references), and any plane / join reference pointing at a non-existent stream (theTrackPlaneUID/TrackJoinUID"not 0" pins fall out of the stream-index→TrackUIDmapping). Unlike theset_video_*family there is no track-type restriction — the spec carriesTrackOperationon everyTrackEntry, so aTrackJoinBlocksaudio virtual track is accepted. Omitting the call keeps the master off-disk so the demuxer surfacesNonefromtrack_operation. Pairs symmetrically with the existingMkvDemuxer::track_operationtyped accessor — a mux→demux pipeline preserves every plane (with its type) and every join reference — and the demuxer can now re-apply the written structures (set_apply_track_operations, see the Demuxer section): a muxed stereo-3D file synthesises its virtual stream with correct per-packet plane roles, a muxed join synthesises the merged stream, and the muxed virtual track seeks through the muxer's own emitted Cues via the §18.8 union (tests/mux_track_operation_apply.rs, including aschema::validatezero-findings pin on TrackOperation-carrying output).TrackTranslateon write (RFC 9559 §5.1.4.1.27):MkvMuxer::set_track_translates(stream_index, Vec<MkvTrackTranslate>)queues zero or more chapter-codec track-mapping masters that land directly inside the carryingTrackEntry(after theTrackOperation/ContentEncodingsmasters) atwrite_headertime, in slice order. EachMkvTrackTranslatecarries the mandatorytrack_id(TrackTranslateTrackID, binary, written verbatim — its format is defined by the chapter codec, not by Matroska) andcodec(TrackTranslateCodec, Table 31), plus zero or moreedition_uids(TrackTranslateEditionUID, an empty list meaning "all editions using the codec"). Unlike theset_video_*family there is no track-type restriction — the spec carriesTrackTranslateon everyTrackEntry. Spec rules enforced at queue time: rejects post-write_headeruse, out-of-rangestream_index, an emptytrack_id(minOccurs: 1), and a zeroTrackTranslateEditionUID("not 0"). Calling with an empty slice clears any previously-queued mappings; repeated calls are last-write-wins. The convenience constructorMkvTrackTranslate::new(track_id, codec)covers the all-editions shape. Omitting the call keeps every master off-disk so the demuxer surfaces an emptytrack_translatesslice. Pairs symmetrically with the newMkvDemuxer::track_translatestyped accessor — a mux→demux pipeline round-trips every mapping field-for-field.- Reclaimed Appendix-A
TrackLegacyon write (RFC 9559 Appendix A.16..A.27 + A.28..A.32):MkvMuxer::set_track_legacy(stream_index, MkvTrackLegacy)queues the historicalTrackEntry-level legacy elements —CodecSettings/CodecInfoURL/CodecDownloadURL/CodecDecodeAllcodec-description metadata, theMinCache/MaxCache/ signedTrackOffsetcache-and-offset hints (A.16..A.18), theGammaValue/FrameRatefloats (A.25 / A.26, written inside theVideomaster) andChannelPositionsbinary (A.27, written inside theAudiomaster), the orderedTrackOverlayfallback list, and the DivXTrickTrack pairing quintet — which land inside the carryingTrackEntry(after theTrackTranslatemasters) atwrite_headertime. Only populated fields reach the disk: an absent field (None/ emptyVec) keeps its element off-disk, so the demuxer observes the same absence. The appendix specifies no defaults, so aSome(0)decode_all/trick_track_flagis written as an explicit0distinct from omission. Spec rules enforced at queue time: rejects post-write_headeruse, an out-of-rangestream_index, and aTrickTrackSegmentUID/TrickMasterTrackSegmentUIDwhose length is not the canonical 16 bytes (aSegmentUUIDis a 128-bit value). Calling with an all-absent record clears the queue; repeated calls are last-write-wins. Pairs symmetrically with theMkvDemuxer::track_legacytyped accessor — a mux→demux pipeline round-trips every populated field verbatim. - Linked-Segment
Infoon write (RFC 9559 §5.1.2.1..§5.1.2.8 + Section 17):MkvMuxer::set_segment_linking(SegmentLinking)queues the Linked-Segment metadata that lands inside theInfomaster atwrite_headertime, in the §5.1.2 element order (beforeTimestampScale):SegmentUUID(§5.1.2.1),SegmentFilename(§5.1.2.2),PrevUUID(§5.1.2.3),PrevFilename(§5.1.2.4),NextUUID(§5.1.2.5),NextFilename(§5.1.2.6), everySegmentFamily(§5.1.2.7), and everyChapterTranslate(§5.1.2.8 —ChapterTranslateID+ChapterTranslateCodec+ zero or moreChapterTranslateEditionUID). The setter takes the same demux-sideSegmentLinkingrecordMkvDemuxer::segment_linking()produces, so a mux→demux pipeline round-trips every UID / filename / family / translate field byte-for-byte — the Segment-level twin of theset_track_translatessurface. Spec rules enforced at queue time (before any byte is written): rejects post-write_headeruse; an off-length UID (§5.1.2.1 / .3 / .5 / .7 are alllength: 16); aPrevUUID/NextUUIDequal toSegmentUUID(§5.1.2.3 / .5 "MUST NOT be equal"); aChapterTranslatewithout the REQUIREDSegmentFamily(§5.1.2.7 usage note); and an emptyChapterTranslateID(§5.1.2.8.1,minOccurs: 1). The read-onlysegment_linking()accessor exposes the queued record before sealing. An all-default record (or omitting the call) writes nothing — the common standalone Segment, which the demuxer surfaces as an emptySegmentLinking. Infometadata on write (RFC 9559 §5.1.2.11 / §5.1.2.12):MkvMuxer::set_title(impl Into<String>)writes the SegmentTitle(§5.1.2.12, the general name), andMkvMuxer::set_date_utc_ns(i64)writes the SegmentDateUTC(§5.1.2.11) as signed nanoseconds since the Matroska epoch (2001-01-01T00:00:00 UTC — thedateelement type), at a fixed 8-byte on-disk width.MkvMuxer::set_date_utc_unix_secs(i64)is a convenience that rebases a Unix timestamp onto that epoch (a pre-2001 instant produces a negative, still-validDateUTC). Both elements land in theInfomaster in §5.1.2 element order (afterTimestampScale, beforeMuxingApp) and round-trip onto the demuxer's flat metadata view under the"title"/"date"keys (the latter formatted back to ISO-8601). All three setters reject post-write_headeruse; omitting them writes neither element.MkvMuxer::set_duration(std::time::Duration)writes the SegmentDuration(§5.1.2.10, id0x4489) — the total Segment length as afloatinTimestampScaleticks (= ms at the muxer's fixed 1 ms scale) — into theInfobody betweenTimestampScaleandDateUTC, inside the CRC-validated master. The muxer never auto-derives it:Clusters stream with the unknown-size VINT and theInfomaster is sealed with itsCRC-32at header time, so a trailer patch would invalidate that CRC — a caller that knows the total length ahead of time supplies it, and it round-trips through the demuxer'sduration_micros(). The §5.1.2.10> 0x0p+0range is enforced (a zero or non-finite length is rejected); omitting the call keepsDurationoff-disk (the demuxer surfacesNone).ContentEncodingson write (RFC 9559 §5.1.4.1.31):MkvMuxer::set_track_content_encodings(stream_index, ContentEncodings)queues a per-track transformation chain that lands as aContentEncodingsmaster (id0x6D80) directly inside the carryingTrackEntryatwrite_headertime. The setter takes the same demux-sideContentEncodingsrecordMkvDemuxer::content_encodingsproduces, so a mux→demux pipeline round-trips the whole chain element-for-element. EachContentEncodingwritesContentEncodingOrder(§5.1.4.1.31.2),ContentEncodingScope(§5.1.4.1.31.3), and theContentEncodingType-keyed sub-master:ContentCompression(§5.1.4.1.31.5 —ContentCompAlgo+ContentCompSettings, the latter written only when non-empty) orContentEncryption(§5.1.4.1.31.8 —ContentEncAlgo,ContentEncKeyIDwhen non-empty, the nestedContentEncAESSettings > AESSettingsCipherModewritten only on AES, and the reclaimed content-signing quartet (RFC 9559 Appendix A.33..A.36 —ContentSignature/ContentSigKeyID/ContentSigAlgo/ContentSigHashAlgo) carried by the demux-sideContentSigningrecord, each of whose four children is written only when itsOptionslot isSomeso an emptyContentSigningadds no bytes and round-trips toNoneon every field). The demux-sideContentCompAlgo/ContentEncAlgo/AesCipherModeenums gainedto_raw()inverses so every Table 23 / 24 / 26 value round-trips, including eachOther(u64)forward-compat variant (§27.2 / §27.3 / §27.4 leave the registries open). The container is a pure carrier — it does not compress or encrypt the frame bytes; the caller handswrite_packetpayloads already matching the declared chain. Spec rules enforced at queue time (before any byte is written): rejects post-write_headeruse, out-of-rangestream_index, an empty chain (ContentEncodingisminOccurs: 1), a duplicateContentEncodingOrder(§5.1.4.1.31.2 "MUST be unique"), a zeroContentEncodingScope(§5.1.4.1.31.3 "not 0"), anAESSettingsCipherModepaired with a non-AESContentEncAlgo(Table 25 forbids it), and a zeroAESSettingsCipherMode(§5.1.4.1.31.12 "not 0"). Chain steps are written ascending by order on disk; the demuxer re-sorts into descending decode order on read, so either layout parses identically. Omitting the call keeps the master off-disk so the demuxer surfacesNonefromcontent_encodings. Pairs symmetrically with the existingMkvDemuxer::content_encodingstyped accessor.- Matroska v5 write surface — explicit opt-in,
DocTypeVersion 5auto-declared (stagedpost-rfc9559-elements.md; the conservative default is "write none of them"): the demux-side v5 surface is mux-symmetric, but only when the caller queues it —MkvMuxer::set_edition_displays(Vec<MkvEditionDisplay>)writes theEditionDisplaymasters into the single emittedEditionEntry(rejected atwrite_headerwhen no chapters are queued —EditionEntryneeds aChapterAtom, and silent dropping is not an option;EditionLanguageIETFtags are validated printable-ASCII at queue time),MkvChapter::skip_typewritesChapterSkipType,MkvTrackAudio::emphasiswritesEmphasis, andMkvTagTargets::block_add_id_valueswrites the non-zeroTagBlockAddIDValueselectors (zeros are wildcards expressed by omission, mirroring the UID lists). Queuing any of them flips the emitted EBML header fromDocTypeVersion 4to5(the v5 draft rule: files carryingminver: 5elements MUST declare 5+);DocTypeReadVersionstays2— every v5 element the muxer can emit is skippable by an older Reader.Emphasisqueued asNoEmphasisbehaves like omission on disk and does not force v5 (the staged note: emittingEmphasis=0needlessly upgrades an otherwise-v4 file). Closed enumerations are enforced at queue time (Reserved/ unassignedEmphasisvalues andChapterSkipType8+ are rejected — the restrictions bind writers; readers degrade). All four surfaces are rejected on a WebM muxer with no lenient opt-out — none of the six is a WebM element (ChapterSkipTypecarries an explicitwebm="0"marker) andDocTypeVersion 5is undefined for thewebmDocType. The full mux→demux round trip and the muxer-output schema validation (zero findings, v5 header) are pinned intests/mux_v5_elements.rs.
- Guidelines support table (
webm::webm_element_support(id) -> WebmSupport): the staged WebM container guidelines tabulate, element by element, whether a WebM reader supports it. The table is transcribed by Element ID — 250 rows: 137Supported, 109Unsupported, 4Deprecated(BlockVirtual/TimeSlice/LaceNumber/FrameRate). Elements newer than the table (e.g. theProjectionmaster,LanguageBCP47,BlockAdditionMapping) returnUnlisted. Every guideline row is keyed to a concrete ID: the 11 rows naming elements RFC 9559 dropped without a registry entry (the old Signature family,EditionFlagHidden,ChapterTrack,ChapterTrackNumber) are keyed via the staged legacy-element-ID mapping and classifyUnsupportedexactly as the guidelines say — the guidelines'ChapterTrackNumberrow is the schema'sChapterTrackUID(same ID0x89; its payload has always been a TrackUID, so the schema renamed it without reassigning the ID). - Whole-file conformance scan (
webm::scan(reader) -> WebmConformanceReport): a pure structural EBML walk (headers + master descent, leaf bodies skipped — O(file) time, O(depth) memory, allocation bounded on hostile input) that classifies every element occurrence, capturing theDocType, per-status occurrence counts, everyUnsupported/Deprecatedoccurrence with its absolute offset (capped at 4096 findings), the distinctUnlistedIDs, and the offset of the first structurally-unwalkable byte if the document is damaged (unknown-size on a non-Segment/Cluster master, a child overrunning its parent, a torn header).is_conformant()gives the headline verdict:DocType == "webm", zeroUnsupported/Deprecatedoccurrences, clean walk —Unlistedis informational only. Unknown-size Segment and Cluster walk with the same sibling-termination rule the demuxer uses, so live-streaming layouts scan fine.tests/webm_conformance.rspins the table shape, the conformant / off-profile verdicts with exact finding offsets, and the hostile shapes (every-truncation-point sweep, 4000-deep nesting, findings flood, arbitrary-byte soup).
- Full schema table (
schema::SCHEMA,schema::element_def(id)): the staged IETF CELLAR EBML Schema for Matroska (ebml_matroska.xml) transcribed as 262ElementDefrows — per element: name, schema path, derived parent ID, EBML type,minOccurs/maxOccurs, verbatimrange/length/defaultconstraint strings, theminver/maxverversion window (the 43maxver: 0rows are the RFC 9559 "Reclaimed" set;is_deprecated()reads the window), therecursive/recurring/unknownsizeallowedstructural markers, and thewebmproject.orgWebM-usability extension marker (133 rows carry it). A superset of the RFC 9559 registry: the six post-RFCminver: 5elements and the legacy chapter elements are present; the removed Signature family is absent by design (its recognition lives inids+ the demuxer).schema::EBML_SUPPLEMENTadds the RFC 8794 EBML-header elements and theVoid/CRC-32globals soelement_defresolves everything a well-formed document carries.tests/schema_census.rspins the table shape, every row's path/parent consistency, both directions of theids.rscross-census, and the corroboration between schemawebmmarkers and the guidelines support table (zero marker-vs-Unsupported conflicts; exactly three explainable Supported-without-marker rows). - Whole-document validator (
schema::validate(reader) -> SchemaReport): a pure structural walk (O(file) time, O(depth) memory, leaf reads bounded at 8 bytes) that checks every element occurrence against the table — identity (UnknownId, informational), placement (WrongParent, with recursion and theVoid/CRC-32globals exempt), type shape +lengthattributes (BadLength: uint/int over 8 octets, float not 0/4/8, date not 0/8,SeekID4, UUIDs 16), decoded-valuerangeconstraints (OutOfRange, full schema range grammar incl. C hex-floats), occurrence counts per parent instance (TooManyOccurrences, andMissingMandatoryforminOccurs >= 1children with no declared default on cleanly-walked masters), the RFC 8794 first-child rule forCRC-32(MisplacedCrc32),unknownsizeallowedgating (UnknownSizeNotAllowed), the version window (Deprecated+VersionMismatchagainst the header'sDocTypeVersion, informational), and the Matroska-v5ChapterSkipTypenesting rule (ChapterSkipTypeNesting, violation — a nestedChapterAtomrepeating its nearest ancestor's skip value; checked with a dedicated ancestor-aware pass per top-level atom, transitive across atoms that omit the element and independent of where the parent's ownChapterSkipTypesits among its children). The removed legacy Signature family classifiesKnownLegacy(informational, masters descended) rather thanUnknownId, per the staged mapping doc's recognise-and-skip recommendation.is_valid()= zero violations + clean walk; findings carry absolute offsets, capped at 4096 with exact counters. The in-tree muxer's own output validates with zero violations and zero informational findings — pinned in CI (tests/schema_validate.rs, 16 tests, including a no-panic sweep over byte soup and every truncation prefix).
Matroska CodecID string <-> oxideav CodecId. Both directions are
implemented for roundtrip:
- Audio:
A_FLAC,A_OPUS,A_VORBIS,A_PCM/INT/LIT,A_PCM/INT/BIG,A_PCM/FLOAT/IEEE,A_AAC(+MPEG4/LC/MPEG2/LCaliases),A_MPEG/L3,A_AC3,A_EAC3. - Video:
V_VP8,V_VP9,V_AV1,V_MPEG4/ISO/AVC,V_MPEGH/ISO/HEVC,V_FFV1,V_THEORA, plusV_MS/VFW/FOURCCwith BITMAPINFOHEADER fourcc extraction (e.g.FFV1). - Subtitle:
S_TEXT/UTF8(subrip),S_TEXT/SSA,S_TEXT/ASS,S_TEXT/WEBVTT,S_TEXT/USF,S_VOBSUB(DVD),S_HDMV/PGS/S_HDMV/TEXTST(Blu-ray),S_DVBSUB,S_KATE. Subtitle tracks surface withMediaType::Subtitle; their payload bytes pass through unchanged.
Unknown MKV codec IDs fall back to a pass-through mkv:<raw-id> form
so the demuxer never hides an unrecognised track.
- Registers both
"matroska"and"webm"with the container registry. - Extensions:
.mkv,.mka,.mks->matroska;.webm->webm. - Probe scoring: DocType=webm scores 100 on
probe_webmand 0 onprobe_matroska(so.mkvnever masquerades aswebm). DocType= matroska scores 100 onprobe_matroskaand 0 onprobe_webm. Files with an ambiguous DocType fall through to the matroska entry.
- CRC-32 validation covers Top-Level master elements parsed up front and
every
Clusterthe demuxer opens throughnext_packet/seek_to; the late best-effort Cues rescan (when Cues sit after the final Cluster) is now checksummed too — a leadingCRC-32child on the late-CuesCueselement validates and surfaces throughcrc_status()exactly the same way the up-front masters do. AClusterdeclared with the unknown-size VINT still produces no status (RFC 8794 §11.3.1 needs a bounded body). The muxer writes a leadingCRC-32child on every Top-Level master it buffers end-to-end before flushing —Info,Tracks,Cues, plusChaptersandAttachmentswhen those are queued.SeekHeadandClusterare deliberately not CRC'd on the mux side: theSeekHeadCues entry is patched inwrite_trailer(which would invalidate any CRC computed up front), andClusteris streamed with the unknown-size VINT (RFC 8794 §11.3.1's bounded-body requirement). - The reclaimed content-signing quartet (RFC 9559 Appendix A.33..A.36 —
ContentSignature0x47E3,ContentSigKeyID0x47E4,ContentSigAlgo0x47E5,ContentSigHashAlgo0x47E6) is now decoded and written on both sides (see theContentEncodingsentries below). The container surfaces and round-trips the four values verbatim; it never computes or verifies a signature (out of container scope). TrackOperationis decoded, written, and now applied (RFC 9559 §18.8):MkvDemuxer::set_apply_track_operations(true)synthesises the virtual track's packet stream from its source tracks — bothTrackCombinePlanes(stereo 3D, per-packet plane roles) andTrackJoinBlocks(block joining) — with virtual-track seek via the §18.8 Cues union and a mux→demux re-apply round trip pinned in CI. The remaining boundary is deliberate: combining the decoded pictures of the planes into one 3D frame is a compositor/decoder concern (§18.8 makes the virtual track's codec ID meaningless — each sub track decodes with its own decoder), so the container assembles the packet stream, never the pixels. Application is a typed-surface feature (open_typed/open_resilient_typed); the boxed traitopenkeeps the default off behaviour.ContentEncodingsis decoded and surfaced (compression / encryption headers). The demuxer undoes a Block-scoped Header-Stripping chain (algo 3) on read — packets carry the original frame bytes — but the generic compression algorithms (zlib / bzlib / lzo1x) and encryption are not reversed: for those a caller that wants raw codec bytes must apply the reported encoding chain itself. zlib/bzlib/lzo1x decompression and decryption are out of container scope. The mux side now writes theContentEncodingsmaster (MkvMuxer::set_track_content_encodings, see the Muxer section) — it carries the declared chain but does not itself compress or encrypt the frame bytes; the caller supplies already-encoded payloads.Videosub-element coverage is now complete on the demux side:PixelWidth/PixelHeight(§5.1.4.1.28.6 / §5.1.4.1.28.7) feed theStreamInfodimensions;FlagInterlaced/FieldOrder(§5.1.4.1.28.1 / §5.1.4.1.28.2) surface throughvideo_interlacing; thePixelCrop{Top,Bottom,Left,Right}+DisplayWidth/DisplayHeight/DisplayUnitquartet (§5.1.4.1.28.8..§5.1.4.1.28.14) surfaces throughvideo_geometry; the fullColourmaster (§5.1.4.1.28.16) — including HDR metadata (MaxCLL/MaxFALL/MasteringMetadata) — surfaces throughvideo_colour;StereoMode(§5.1.4.1.28.3) surfaces throughvideo_stereo_modeand the legacyOldStereoMode(§5.1.4.1.28.5, id0x53B9) throughvideo_old_stereo_mode; theProjectionmaster (§5.1.4.1.28.41) — includingProjectionType, the verbatim ISOBMFF-mirroredProjectionPrivatepayload, and the yaw / pitch / roll pose triple — surfaces throughvideo_projection;AlphaMode(§5.1.4.1.28.4) surfaces throughvideo_alpha_mode; the reclaimed Appendix-AAspectRatioTypeelement surfaces throughvideo_aspect_ratio_type; andUncompressedFourCC(§5.1.4.1.28.15) surfaces throughvideo_uncompressed_fourcc. On the mux side,PixelWidth/PixelHeight, theFlagInterlaced/FieldOrderpair (MkvMuxer::set_video_interlacing, §5.1.4.1.28.1 + §5.1.4.1.28.2), theStereoMode/AlphaModepair (MkvMuxer::set_video_stereo_mode/MkvMuxer::set_video_alpha_mode, §5.1.4.1.28.3 + §5.1.4.1.28.4), thePixelCrop{Top,Bottom,Left,Right}+DisplayWidth/DisplayHeight/DisplayUnitquartet (MkvMuxer::set_video_geometry, §5.1.4.1.28.8..§5.1.4.1.28.14),UncompressedFourCC(MkvMuxer::set_video_uncompressed_fourcc, §5.1.4.1.28.15), the eleven scalar children of theColourmaster (MkvMuxer::set_video_colour, §5.1.4.1.28.16, §5.1.4.1.28.17..§5.1.4.1.28.29 —MatrixCoefficients,BitsPerChannel,ChromaSubsampling{Horz,Vert},CbSubsampling{Horz,Vert},ChromaSiting{Horz,Vert},Range,TransferCharacteristics,Primaries,MaxCLL,MaxFALL; the convenience constructorsMkvVideoColour::bt709()andMkvVideoColour::bt2020_pq()cover the SDR HD and HDR10 PQ shapes), and the ten chromaticity / luminance children of theColour > MasteringMetadatasub-master (MkvVideoColour::mastering_metadata = Some(MkvMasteringMetadata), §5.1.4.1.28.30..§5.1.4.1.28.40 —Primary{R,G,B}Chromaticity{X,Y},WhitePointChromaticity{X,Y},Luminance{Max,Min}; the convenience constructorMkvMasteringMetadata::bt2020_d65_hdr10()covers the canonical HDR10 shape), and theProjectionmaster (MkvMuxer::set_video_projection, §5.1.4.1.28.41 —ProjectionType, the verbatimProjectionPrivatepayload, and the yaw / pitch / roll pose triple; the convenience constructorsMkvProjection::equirectangular()andMkvProjection::rotated()cover the 360°-VR and roll-only shapes), and the reclaimed Appendix-AAspectRatioTypeelement (MkvMuxer::set_video_aspect_ratio_type, Appendix A.24, id0x54B3), and the legacyOldStereoMode(MkvMuxer::set_video_old_stereo_mode, §5.1.4.1.28.5, id0x53B9) are written. TheVideosub-element set is now fully symmetric — every element the demux side reads, the mux side can write.
tests/rfc9559_element_census.rs transcribes the full RFC 9559 Table 53
"Matroska Element IDs" registry — 250 named entries (43 of them
Reclaimed), the 4 all-ones Reserved placeholders excluded — and
cross-checks src/ids.rs in CI, both directions: every registry element
has a const, and every numeric const is a registry entry, one of the 13
RFC 8794 EBML-header / EBML-global IDs, one of the six staged post-RFC
Matroska v5 elements (EditionDisplay / EditionString /
EditionLanguageIETF / ChapterSkipType / Emphasis /
TagBlockAddIDValue — no RFC and no IANA row define them; provenance
pinned in post-rfc9559-elements.md), or one of the 12 documented
legacy exceptions — ChapterFlagEnabled (0x4598) plus the
legacy-element-ID mapping set (EditionFlagHidden 0x45BD,
ChapterTrack 0x8F, ChapterTrackUID 0x89, and the eight-element
Signature family under SignatureSlot 0x1B538667), all of which
Matroska dropped before RFC 9559 without carrying the IDs forward but
historical files still carry. Const names must agree with the
registry's Element Names, no Reserved ID may be defined, and every ID
must sit inside the Section 27.1 valid VINT ID classes. The census is
what backs the "every element in the RFC 9559 element-ID registry is
read and written" claim.
tests/injection_robustness.rs pins eighteen attacker-shaped byte
patterns against the open / next_packet / seek_to / attachment_data
surface: a skip helper that previously cast u64 as i64 and could
seek the reader backwards on a forged Size field; demux-open
rejection of an empty input, an EBML-magic with a truncated header, an
oversize EBML-header Size, oversize DocType / CodecID / TagString
strings, and a Segment declared size that runs past EoF; cluster-time
handling of an oversize SimpleBlock, a Xiph-laced SimpleBlock whose
declared sub-frame sizes overrun the body, and a fixed-laced
SimpleBlock with n_frames = 5 over an empty payload; on-demand
attachment_data short-read on a forged 4 GiB FileData size and a
forged 2 GiB FileName; an out-of-range CueRelativePosition in
seek_to; a 4000-level-deep nested SimpleTag chain that must parse
without a stack overflow (the §5.1.8.1.2 recursive depth cap), a
name-less nested SimpleTag that must be dropped (§5.1.8.1.2.1
minOccurs: 1) while its named parent survives; and an inline
fuzz-corpus replay of five malformed seed shapes. All checks land as
standard cargo test targets so a regression on any one surfaces in CI
without waiting for a fuzz cycle.
tests/damage_resilience.rs pins the resilient Reader's recovery
decisions end-to-end: clean-file parity with the strict path (zero
DamageEvents), Cluster-stream resync past a corrupt Cluster header and
past a forged SimpleBlock size, an every-truncation-point sweep
asserting each cut of a multi-cluster file yields a packet prefix and
a clean Error::Eof (never a panic, never a non-Eof error), the
known-size-Segment-past-EoF clamp with its SegmentTruncated event,
damaged-Tags / damaged-Cues master skips, the unrecoverable-tail
drop, and the Cues-less seek fallback (including across an unknown-size
Cluster). tests/mux_live_streaming.rs re-runs the cut-anywhere prefix
property over the §25.3.4 live layout.
tests/ebml_walker_property.rs adds deterministic property-style coverage
for the EBML element walker (RFC 8794) that backs the whole demuxer — a
seeded splitmix64 PRNG drives ~100k generated cases per run (no
proptest/quickcheck dependency): VINT write_vint↔read_vint
round-trips with min_width honoured across the full 56-bit range, the
unknown-size sentinel at every width, read_element_header round-trips, a
sequential read_element_header + skip walk that recovers every element
id and lands exactly on the buffer end, and a no-panic / no-backward-seek
guarantee over arbitrary byte streams and every prefix of a well-formed
tree.
A cargo-fuzz harness for the demuxer lives in fuzz/. It drives
demux::open, drains up to 256 packets via next_packet, and exercises
the seek_to cluster pre-open path — over arbitrary bytes — against the
contract that no call panics, aborts, integer-overflows (in a debug
build), or attempts an attacker-controlled allocation that exceeds what
the input can back. A second pass through open_typed additionally
fuzzes the typed-accessor surface — the per-Block block_additions /
block_group_meta side channels, the ClusterRecord SilentTrackNumber
lists, and the Chapters / SeekHead trees — with TrackOperation
application enabled, asserting every reported virtual_packet_origin
is self-consistent (matches the synthesised packet's stream, never a
self-reference, both indices in range). A fourth pass runs the
webm::scan conformance walker over the same bytes, asserting its
per-status counters always sum to elements_scanned (the seed corpus
also replays through it as a plain cargo test). A fifth pass runs
the schema::validate whole-document validator, asserting its
violation / informational counters stay consistent with the capped
findings list and that is_valid() agrees with the counters (corpus
replay as a plain cargo test too). A third pass drives
open_resilient_typed with a contract stronger than no-panic: a
resilient next_packet may only fail with the clean Error::Eof (any
other error class panics the harness), damage-event bookkeeping must
never move backwards, and both seek shapes (Cues index — now
trust-but-verified, so a lying cue exercises the CueLie fallback —
plus the Cues-less cluster-scan fallback) run post-drain, followed by a
whole-index audit_cues pass asserting its capped findings list stays
consistent with the exact counter and is_truthful() agrees — so the
recovery loop's forward-progress guarantee is fuzz-checked. The seed corpus in
fuzz/corpus/demux/ covers a
minimal valid Matroska file, a minimal valid WebM file, an EBML-header-
only stream, six regression inputs (an EBML size-overflow, a
zero-frame-size fixed-lacing SimpleBlock, the 2026-07 fuzz-found
unknown-size-Colour add-overflow, and the 2026-08 fuzz-found
hostile-TimestampScale seek-conversion overflow, forged
FileReferral-size capacity overflow, and forged fixed-8
CueClusterPosition seek add-overflow — found within seconds of the
lying-Cues seed entering the corpus, fixed by saturating the strict
seek's absolute-offset computation), a lying-Cues seed
(seed_cue_lies.mkv — one lie of every CueLieKind class beside
truthful entries, so mutation reaches the trust-but-verify seek and
audit_cues arms from a well-formed start; builder-match + findings
pinned by a test), and two
corrupted-file seeds (mid-file zeroed bytes, 60% truncation). Every corpus seed also replays
through the resilient path as a plain cargo test
(injection_robustness::fuzz_corpus_files_replay_through_resilient_path).
Run locally with a nightly toolchain:
cd fuzz
cargo +nightly fuzz run demux # libFuzzer drives indefinitely
cargo +nightly fuzz run demux -- -max_total_time=60 # boundedCI runs a 30-minute fuzz cycle daily via
.github/workflows/fuzz.yml (the OxideAV org-level reusable
crate-fuzz.yml).
MIT - see LICENSE.