containr
containr is an MP4, MOV, Matroska, and WebM demuxer and muxer for Rust. It does the container work of ffmpeg -c copy as a library, in Rust, with no C library and no Python.
containr holds the container layer of the ml-rust media stack. It parses boxes and EBML elements into tracks, packets, metadata, chapters, and attachments, and it writes them back. vidr (video) and voxr (audio) depend on it. Codecs live in those crates, so containr never decodes or encodes media.
file → demux → packets with integer timestamps → your codec, or mux → file
Status
In development. Version 0.1.0 is on crates.io.
| Format | Demux | Mux | Feature |
|---|---|---|---|
| MP4, QuickTime MOV | Yes. Fragmented MP4 (moof) is planned, and refused today |
Yes | mp4 |
| Matroska, WebM | Yes | Yes. WebM refuses codecs it cannot hold | mkv |
Not supported yet
- Fragmented MP4 (
moof) input. Demux refuses it withError::Fragmented. - Decoding or encoding any codec. containr moves packets only.
- Matroska attachments into MP4. MP4 has no form for them.
- Exact AAC at 44.1 or 48 kHz and 29.97 fps video in Matroska.
StreamCopy::planrefuses them, andStreamCopy::plan_roundedis the opt-in.
Stream copy
| Source track | Into MP4/MOV | Into MKV/WebM |
|---|---|---|
| From MP4/MOV | Any track, by raw sample entry | avc1, avc3, hvc1, hev1, av01, vp09, mp4a, Opus, tx3g |
| From MKV/WebM | H.264, HEVC, AV1, AAC, Opus, S_TEXT/UTF8 |
Any track |
mkv::StreamCopy::plan refuses a timestamp Matroska cannot hold exactly. See what TimestampScale holds.
FFmpeg 8 and older store no duration on Matroska audio blocks, and every mux needs one on the last block. So when a last audio or video packet stores no duration, StreamCopy::plan gives it the gap between the track's two latest distinct timestamps, less its DiscardPadding. A track with one timestamp and a subtitle track get no such duration, and the muxer refuses them with Error::MissingDuration.
Features
Each container is a Cargo feature. The default build holds the shared types only.
The mp4 feature depends on the mp4-atom crate. The mkv feature has no dependency, because the EBML reader and writer are containr's own. The minimum Rust version is 1.95.
| Feature | What it adds |
|---|---|
mp4 |
containr::mp4::Demuxer and Muxer for MP4 and QuickTime MOV |
mkv |
containr::mkv::Demuxer and Muxer for Matroska and WebM |
Usage
The examples need the mp4 and mkv features.
Read an MP4 or MOV file:
use Demuxer;
containr::mkv::Demuxer has the same shape for Matroska and WebM:
use Demuxer;
Copy an MP4 file into a new one, with moov at the front. The muxer rebuilds the chapter track from set_chapters, so the copy skips it:
use HashMap;
use File;
use ;
Copy a Matroska or WebM file into a new one, with one plan per track:
use File;
use ;
Copy an MP4 file with 29.97 fps video or 48 kHz AAC into Matroska, rounded to 1 ms ticks:
use File;
use ;
use Demuxer;
Format details
MP4/MOV demux
moovcan sit before or aftermdat.- Each track carries its id, kind and handler type, timescale, duration, and language.
- Each sample entry keeps its raw bytes for stream copy, plus
avcC,hvcC,av1C,vpcC,esdsordOps. - Video tracks carry width, height, the
paspratio, and the rawtkhdmatrix. Audio tracks carry channels and sample rate. VideoInfo::colourholds the H.273 tags of acolrbox.Track::colourgives the value for either container.nclx: tags and the full-range bit.nclc(QuickTime): tags with no range.prof,rICC: no H.273 values.colourstaysNoneunless anothercolrbox givesnclx.- A code point above 255 is an error.
- Packets carry
dts,pts(signedcttsincluded), duration, sync flag, byte offset, and size. - Sample tables:
stts,ctts,stsc,stsz,stz2,stco,co64,stss. They expand one sample at a time, so memory tracks the table entries, not the sample count. - Edit lists come raw, with the movie timescale. The caller applies them.
- Metadata:
udtachildren andmeta/ilstitems, raw. Chapters come fromchpland from QuickTime chapter tracks. - Fragmented MP4 is refused with
Error::Fragmented. mp4::first_track_timingreads themdhdtimescale and raw edit list of the first track of a kind, frommoovalone, with no sample tables. It accepts a fragmented file.- Malformed input returns an error that names the box path. Every size from the file is checked before allocation.
MP4/MOV mux
-
Muxer::new,add_track,set_metadata,set_chapters,write_packet,finish. The target implementsWriteandSeek, andReadforfinish, since faststart readsmdatback. -
TrackSpeccarries the sample entry as raw stream-copy bytes, or builds it fromavcC,hvcC,av1C,esds, ordOps. -
TrackSpecalso carries timescale, language, matrix,pasp,colour, and an edit list.colourwrites annclxcolrbox into a built visual entry. A raw entry keeps its owncolrbox. -
TrackSpec::copy_frombuilds a spec from an MP4 track. -
TrackSpec::copy_fromalso takes a Matroska H.264, HEVC, AV1, AAC, Opus orS_TEXT/UTF8track.StreamCopy::planadds packet timing and, for Matroska, the decode times and edit list MP4 needs. -
Matroska timing into MP4:
Matroska fact MP4 result Negative timestamps The edit list drops them Padding of the last block The edit list drops it AAC CodecDelayBecomes the edit's media_time. The segment lasts to the end of the last block less that delayA delay the Matroska tick cannot hold The track timescale rises to a multiple that holds it, so no timestamp rounds A delay no tick count matches within 1 ns Refused. The error names Matroska Opus CodecDelayAdds no edit. dOpspre_skipalready carries it, and a second skip drops it twice -
Layout:
ftyp,mdat,moov.MuxOptions::faststartmovesmoovto the front in a second pass on the same target. That pass adds the shift to every chunk offset. -
Chunks last at most
MuxOptions::chunk_duration_msand interleave across tracks by decode time.MuxOptions::max_bufferbounds the bytes held back. -
Sample tables:
stts,ctts,stsc,stsz,stss, andstco. -
co64replacesstcopast 4 GiB. Version 1cttswithcslgholds a negative offset. The last sample's duration comes from its packet, so the track's total duration is exact. -
Refused, each by name:
- a codec MP4 cannot hold (the error names Matroska)
dtsout of order- a composition offset
cttscannot store - a second sample description
- a last packet with no duration and no default
-
Composition offsets range from -231 to 231 - 1, since readers treat
cttsoffsets as signed. -
Known limits:
- Stream copy drops MP4
sgpdandsbgpsample-group boxes. - Matroska attachments have no MP4 form, so the muxer never sees them.
- A caller that copies Matroska to MP4 must check
Demuxer::attachments(). When it lists any, refuse the copy and name MKV as the container that holds them.
- Stream copy drops MP4
MKV/WebM demux
- The EBML reader is containr's own. It checks every size before it allocates, and decodes unknown sizes of any length.
- Each track carries its
TrackNumberas id, kind,TimestampScaletimebase, and language.Track::matroskaholdsTrackUID,CodecID, rawCodecPrivate,LanguageandLanguageBCP47,DefaultDuration,CodecDelay,SeekPreRoll, flags, andContentEncodings. - Video tracks carry pixel and display size,
DisplayUnit, crop, andProjectionwithProjectionPoseRoll. Audio tracks carry sampling frequency, channels, and bit depth. MatroskaVideo::colourholdsMatrixCoefficients,TransferCharacteristics,Primaries, andRange. The rawColourelement also stays inMatroskaVideo::extra.Range1 gives limited, 2 gives full, and 0 or 3 giveNone.- A code point above 255 is an error that names the element path.
- Packets carry
pts(ClusterTimestampplus the block timestamp), duration, keyframe and discardable flags,DiscardPadding, byte offset, and size.dtsisNone: Matroska stores no decode timestamp. - Xiph, EBML and fixed lacing expand into one packet per frame. Each laced packet records its place in the lace.
- Timestamps count in
TimestampScalenanoseconds, as integers.CodecDelayandDiscardPaddingcome raw. The caller applies them. - A QuickTime Macintosh language code stays
Language::Other.Language::iso639givesNonefor it.Language::iso639_mappedmaps it to ISO 639-2/T.- Codes with no table entry give
None:0x7FFF, 95 to 127, and above 138.
- A Cluster of unknown size, as a live WebM writes it, ends at the next Segment child.
Cues,SeekHeadentries, chapters from every edition, andTagscome raw. A tag that targets a track'sTrackUIDsits in that track's metadata.- Attachments expose name, media type, UID, and the byte range of their data.
Demuxer::read_attachmentreads it. - A truncated file keeps every block before the cut.
Demuxer::truncatedreports the cut. mkv::first_track_timingreads theCodecDelayand summed positiveDiscardPaddingof the first track of a kind, with no packet index.- Malformed input returns an error that names the element path, such as
Segment/Cluster[3]/SimpleBlock[7].
MKV/WebM mux
-
Muxer::new,add_track,set_metadata,set_chapters,attach,write_packet,finish. A target with no seek works throughNoSeekwithMkvOptions::seekableset to false. -
TrackSpec::copy_fromandStreamCopy::plancopy a Matroska track with everyMatroskaTrackfact, or map an MP4avc1,avc3,hvc1,hev1,av01,vp09,mp4a,Opusortx3gtrack. -
Colour tags survive every copy:
Copy Result MP4 to MKV A colrnclxornclcbox maps toColourMKV to MP4 Colourmaps to annclxcolrbox. An unset tag writes 2 (unspecified). An unset range writes limitedHand-built spec Sets MatroskaVideo::colourRaw Colourelement that matchescolourThe muxer writes it unchanged Raw Colourelement that differscolourreplaces the four tags. Other children, such asMasteringMetadata, stay -
Languages:
Output Form Example Matroska Bibliographic, because version 2 readers expect it gerMP4 Terminology, as mdhddefines itdeuAn MP4 track's language maps through
Language::iso639_mapped. A code with no table entry writesund. -
An edit list maps to timing: an empty edit and a video
media_timeshiftpts, an audiomedia_timebecomesCodecDelay, and an audio end trim becomesDiscardPadding. -
The default refuses.
StreamCopy::planandMuxer::write_packettake a value only when it is a whole number of nanoseconds and a whole multiple ofTimestampScale.StreamCopy::timestamp_scalepicks a scale that holds a set of tracks. -
A repeated
TrackUID, attachment UID or non-zeroChapterUIDis refused.
Limits: what TimestampScale holds
Matroska stores integer ticks of TimestampScale. It cannot hold 1024/48000 s or 1001/30000 s exactly, at any scale. So StreamCopy::plan refuses AAC at 44.1 or 48 kHz and 29.97 fps video in a 30000 timescale. The error names MP4, which holds them, and StreamCopy::plan_rounded.
StreamCopy::plan_rounded(track, packets, scale) is the opt-in for MP4 tracks. scale is the TimestampScale in nanoseconds:
- Each timestamp rounds on its own, from its exact source value, to the nearest tick, ties to even, in integer arithmetic. Its error stays at most half a tick, and no error accumulates across packets.
- A block's duration is the distance to the next packet's rounded timestamp, in presentation order. The last packet in that order rounds from its exact end. So durations sum to the track's rounded end.
CodecDelay,DiscardPaddingandDefaultDurationcount in nanoseconds, so each rounds to the nearest nanosecond, ties to even. A block's duration can differ fromDefaultDurationby a tick, and then the block states its own.- Decode order and
syncflags stay as the source has them. - Two packets of one track on one tick are refused, and the error names a finer scale.
StreamCopy::roundingreports the scale and the largest absolute error, in nanoseconds, rounded up.StreamCopy::timestamp_scalegives the largest value that divides every plan's scale, so rounded and exact plans share one output.
A chapter time stays exact or refused. plan_rounded does not round chapters.
Minimum Rust version
1.95.
Contributing
Contributions are welcome. See CONTRIBUTING.md for conventions and checks.
Questions and design discussion happen on Discord.
License
Licensed under Apache-2.0. See LICENSE.