RecursiveAI.OneMedia.Matroska.MatroskaMuxer writes Matroska and WebM files from packets in decode order. It implements IMuxer (declare tracks with AddTrack, give packets with WritePacket, end with Finish, which returns a MuxReport). Payload bytes are copied to the output unchanged. Specifications: RFC 8794 (EBML), RFC 9559 (Matroska), the Matroska codec mapping and the WebM container guidelines; section numbers are in the code comments.
Layout
Seekable output (the default):
EBML header | Segment (size patched at Finish) SeekHead (Info, Tracks, Cues, two spare slots as Void) Info (TimestampScale, Duration, MuxingApp, WritingApp, SegmentUUID [not in WebM]) Tracks Void (ReserveBytes, default 2048: room for tags added in place later) Cluster ... Cues
- The header is written when the first Cluster is closed, so a track's
DefaultDurationcan be stated when the first packets show one (the same duration on every packet but the last, at least three packets). OtherwiseDefaultDurationcomes from the declared frame rate (video) or a codec's fixed frame length (AAC, AC-3 …). - The Segment is first written with an unknown size;
Finishseeks back and patches the SeekHead (Cues position), Info (Duration, a fixed 8-byte float) and the Segment size. A file cut short stays readable. - A packet is a SimpleBlock unless it needs a BlockGroup: a duration the track's
DefaultDurationdoes not give (BlockDuration), aDiscardPadding, or a subtitle. A non-key BlockGroup carries aReferenceBlock, because a BlockGroup without one is a key frame (RFC 9559 §10.3). - Packets of one audio track that share a timestamp and state no duration (the frames of a laced block in the file they came from) are written back as one Xiph-laced block; written as separate blocks they would overlap. Lacing happens only in seekable mode, and the track's
FlagLacingis left at its default for audio (video isFlagLacing 0). - Clusters start on a video key frame (with no video, on a packet of the first track) once the current one is at least
MinClusterDuration(2 s) long or holdsMaxClusterBytes(4 MiB). They are cut inside a group of pictures only when block offsets would overflow 16 bits (about 32 s at 1 ms units) or the payload passes four timesMaxClusterBytes. The Cluster Timestamp is the earliest block time, so offsets are small. - Cues (RFC 9559 §11): one per video key frame (no video: one per Cluster) with
CueClusterPosition(from the Segment data start) andCueRelativePosition(from the Cluster data start). - CRC-32 (RFC 8794 §11.3.1), when
Crc32is set: first child of SeekHead, Info, Tracks, Cues and every Cluster.
Live mode
MatroskaMuxOptions.Live writes to a stream that cannot seek: the Segment and every Cluster have an unknown size (RFC 8794 §6.2), each block is written when it arrives (two writes: block header, payload), and there are no Cues, Duration, SeekHead patch, lacing or CRC. DefaultDuration comes only from the declared frame rate or a codec's frame length; any other packet with a duration is a BlockGroup. Read back with the forward-only reader and with ffprobe. Frames that came from a laced block are written as separate blocks that share a timestamp (a lace cannot be formed once earlier frames are on the wire); ffmpeg warns about the equal decode times when it reads them, and the packets still match.
Memory and allocations
- Seekable mode holds at most one Cluster's payload (
MaxClusterBytes, up to four times that inside a long group of pictures, plus one packet) and the Cue list:MatroskaMuxer.CueBytesPerPoint= 24 bytes in memory per cue point, about 25 bytes when written. - Live mode holds no payload.
- After the first Cluster the write path allocates nothing per packet (tests measure 0 bytes over 20,000 live packets and under 4 KiB over 10,000 seekable ones, which is the Cue list doubling).
Tracks
CodecID and CodecPrivate come from the neutral track and the codec layer:
| Source | CodecID | CodecPrivate |
|---|---|---|
| H.264 / HEVC / VVC | V_MPEG4/ISO/AVC / V_MPEGH/ISO/HEVC / V_MPEGI/ISO/VVC | avcC / hvcC / vvcC as stored (Annex B tracks are refused) |
| AV1, VP9, VP8 | V_AV1, V_VP9, V_VP8 | av1C; VP9: vpcC → Matroska VP9 features (when present); none for VP8 |
| MPEG-1/2 video, MJPEG, MPEG-4 ASP, Theora | V_MPEG1, V_MPEG2, V_MJPEG, V_MPEG4/ISO/ASP, V_THEORA | MPEG-4: the decoder-specific info of the esds |
| AAC | A_AAC | AudioSpecificConfig (esds unwrapped); implicit SBR (CodecID ending /SBR) writes half the output rate as SamplingFrequency |
| Opus | A_OPUS | OpusHead (dOps converted); CodecDelay = pre-skip, SeekPreRoll 80 ms |
| Vorbis, FLAC | A_VORBIS, A_FLAC | Xiph-laced headers; fLaC + metadata blocks (dfLa converted) |
| MP3, MP2, AC-3, E-AC-3 | A_MPEG/L3, A_MPEG/L2, A_AC3, A_EAC3 | none |
| SubRip, ASS | S_TEXT/UTF8, S_TEXT/ASS | ASS header |
| A track read from Matroska | its own CodecID (CodecTag) | its CodecPrivate unchanged; CodecDelay restated; BitDepth kept |
bitmapinfoheader / waveformatex (from Matroska VFW/ACM or AVI) | V_MS/VFW/FOURCC / A_MS/ACM | the structure |
Also written: track number (index + 1), UID, flags (FlagDefault only when not default, forced, hearing/visual impaired, original, commentary, enabled), name, language (Language for three lowercase letters, otherwise und plus LanguageBCP47), pixel size, display size from the pixel aspect ratio, Colour (matrix, transfer, primaries, range) when the codec headers state it, channels, sampling frequency, bit depth for FLAC, PCM wrappers and tracks read from Matroska.
WebM (MatroskaProfile.WebM) takes VP8, VP9, AV1, Vorbis and Opus; any other track is refused with MuxRefusalReason.UnsupportedCodec naming the CodecID. mkvalidator 0.6.0, the validator used here, rejects SegmentUUID and the whole Colour element in its webm profile (the WebM guidelines list Colour), so the WebM profile leaves both out.
Refusals are typed MuxRefusedException with a MuxRefusalReason, raised before anything of the offending track or packet is written: UnsupportedCodec, MissingConfiguration (no CodecPrivate where the codec needs it), InvalidConfiguration (setup bytes in a form that cannot be repackaged), NonMonotonicDts, MissingTimestamp, InvalidTrack, InvalidState, LimitExceeded, OutputNotSuitable.
Time
Block timestamps are in TimestampScale units (default 1 ms; a Matroska source whose tracks all count 1,000–10,000 ticks per second keeps its time base). Times are rescaled exactly with nearest rounding; MuxReport.Notes says when any rounding happened. Matroska stores no decode time: blocks are written in the order given, with presentation times, so a B-frame stream needs no extra field. DTS only gates the order (a decrease within a track is refused).
Tracks whose packet times already exclude CodecDelay (TrackEditInfo.PacketTimesIncludePriming: Matroska and Ogg Opus) get it added back to the Block timestamp; an ISO Opus track (packet times start at 0, the pre-skip is discarded by the decoder) is written with its times as they are, and CodecDelay states the pre-skip. AAC priming from an edit list is not carried (Matroska readers do not apply CodecDelay to AAC consistently).
A packet with an unknown duration (or a zero duration on audio or video, which states nothing) gets the distance to the next decode time of its track, as the neutral contract says, when that packet is still held: in seekable mode, within the open Cluster, and the previous packet is completed before a Cluster is cut. It is then a BlockDuration, or covered by a learned DefaultDuration. Left unknown: the last packet of a track; every packet in live mode (a block is on the wire before the next packet exists; the duration comes from the declared frame rate or the codec's frame length instead); members of a lace (they share one time); and subtitle packets (their duration is their meaning).
Not carried
Rotation (ProjectionPoseRoll), interlacing and field order, stereo mode, content encoding/compression (frames are written plain), attachments, chapters and tags (tag writing is separate), BlockAdditions, CodecState, and the discardable flag on a packet that needs a BlockGroup.
How it is checked
tools/OneMedia.MatroskaMuxCheck copy-muxes a file with our reader and muxer, then runs ffprobe -show_packets -show_data_hash adler32 on the output and compares, per stream, size, payload hash, presentation time, duration and key flag with what our reader found in the input; for a Matroska input it also compares ffprobe's listing of the input and of the output field by field (decode times included). It then reads the output back with our own reader (flags included). tools/matroska/run-mux-checks.sh runs a list of files and adds mkvalidator, mkvinfo and ffmpeg -f null. Every difference is one of these explained classes, counted per file:
| Class | What differs | Why |
|---|---|---|
lace-shared-pts | Frames of one laced block carry one timestamp in our reader | A lace has one timestamp; ffprobe spaces the frames by the codec's frame length, both for the input and for our re-laced output |
rounded-to-the-output-time-base | Times differ by under half an output unit | The source's TimestampScale is finer or does not divide a second; ffprobe's own integer time base for such a scale is approximate (1e-4) |
pts-as-ffprobe-gives-for-the-input / duration-as-ffprobe-gives-for-the-input | Our reader and ffprobe disagree, but ffprobe says the same for input and output | A documented reader/ffprobe difference (docs/matroska-differential.md) |
pts-not-stated-by-ffprobe-for-the-input | ffprobe gives no presentation time for packets of the input (MPEG-4 with B-frames) and does for the output | ffprobe derives them from our explicit block times; ffmpeg's null muxer then warns about the dts it generates |
duration-derived-from-next-dts | ffprobe shows a duration for the output where the input stated none | The muxer derived it from the next decode time (checked numerically by the tool) |
duration-truncated-from-default-duration-by-ffprobe | Durations differ by one millisecond | ffprobe truncates a nanosecond DefaultDuration (41.67 ms) where we round to the nearest unit (42) |
ffprobe-gives-the-default-duration-for-a-shorter-last-block | The last packet is shorter than the others (an edit-list trim) | We write its BlockDuration (checked with mkvinfo); ffprobe 7.0.2 reports DefaultDuration |
key-as-ffprobe-gives-for-the-input | ffprobe marks a key frame the container does not | ffmpeg's MPEG-4 part 2 parser marks I-VOPs itself, for the input as well |
ffprobe-reframes-the-input-the-same-way | ffprobe lists other packet counts than the container has | A QuickTime MP3 track stored one byte per sample is re-framed by ffmpeg's parser for the input too; we copy the samples as stored |
zero-duration-treated-as-unknown | The source states a zero duration on audio or video (a fragment without sample durations) and ffprobe shows one for the output | The muxer treats it as unknown and derives it from the next decode time |
duration-from-default-duration | ffprobe shows a duration for the output where the input stated none | We state a DefaultDuration the input lacked (learned or from the codec) |
ffprobe-splits-blocks(-of-the-input-too) | ffprobe lists more packets than blocks | RealAudio de-interleaving, identical for the input |
| invisible flag | ffprobe shows no D for a block's invisible flag | Checked with our reader instead: the flag survives |
The results table with file and packet counts is at the end of the unit's report; rerun with tools/matroska/run-mux-checks.sh <work-dir> <file-list>.