oxideav-mkv
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.
Installation
[]
= "0.1"
= "0.1"
= "0.1"
= "0.0"
Quick use
Register both containers ("matroska" and "webm") and let the probe
pick which DocType the file carries:
use ContainerRegistry;
let mut containers = new;
register;
let input: = Boxnew;
let mut dmx = containers.open_demuxer?;
for s in dmx.streams
loop
# Ok::
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.
What's implemented
Demuxer (demux::open)
- EBML header parse, DocType validation (
matroska/webm). - 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). - 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). 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. 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
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 already writes both on the Opus path (CodecDelay=OpusHeadpre-skip in ns,SeekPreRoll= 80 ms). - 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
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. - 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(spec 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. - Duration:
Segment\Info\Durationtranslated to microseconds. - 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.- 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.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.- 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, and the nestedContentEncAESSettings→AESSettingsCipherModeasCtr/Cbc/Other(u64)). 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 > 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.
Muxer (mux::open and mux::open_webm)
- EBML header + Segment (unknown size) for a streaming-friendly layout.
- 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, rolling ~5 sClusters withSimpleBlockpayload.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 carriesCueRelativePosition(RFC 9559 §5.1.5.1.2.3, recommended by §22.1) so seek-aware readers jump straight to the indexedSimpleBlockinside the Cluster instead of scanning from the cluster header.- 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). 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.- WebM profile:
mux::open_webmpinsDocType="webm"and rejects any stream whose codec isn't VP8/VP9/AV1 video or Vorbis/Opus audio withError::Unsupported. - 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 > 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. - 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.
Codec ID mapping (codec_id module)
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.
Probes + registration
- 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.
What's NOT implemented
- 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). - ContentSignature (RFC 9559 §A.33 reclaimed
0x47E3) is parsed by neither side. The element is reserved for a future per-segment signature scheme. TrackOperationis decoded and surfaced (left/right-eye plane combining, block joining) but the demuxer does not yet apply it — virtual tracks are reported alongside their source tracks rather than being synthesised into a single combined output stream.TrackOperationis never written on the mux side.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;ContentEncodingsis never written on the mux side.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_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) are written. TheVideosub-element set is now fully symmetric — every element the demux side reads, the mux side can write.
Robustness
tests/injection_robustness.rs pins sixteen 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; 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.
Fuzzing
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. The seed corpus in fuzz/corpus/demux/ covers a
minimal valid Matroska file, a minimal valid WebM file, an EBML-header-
only stream, and two regression inputs (one for an EBML size-overflow,
one for a zero-frame-size fixed-lacing SimpleBlock).
Run locally with a nightly toolchain:
CI runs a 30-minute fuzz cycle daily via
.github/workflows/fuzz.yml (the OxideAV org-level reusable
crate-fuzz.yml).
License
MIT - see LICENSE.