Documentation: https://docs.rs/mp4_muxer
Source Code: https://github.com/NameOfShadow/mp4_muxer
No FFmpeg. No mp4parse. No shell-out. AV1 OBU packets in, playable .mp4 out — one pass, one Vec<u8> of Rust.
use Mp4Writer;
use File;
let file = create?;
let mut muxer = new?;
muxer.write_frame?;
muxer.finish?;
$ ffprobe -v error -show_streams out.mp4
[STREAM]
codec_type=video
codec_name=av1
width=320
height=240
r_frame_rate=30/1
[/STREAM]
Features
- Zero runtime dependencies — pure
std, nomp4parse-rust, nobento4, no FFmpeg. - Single-pass writer —
mdatprecedesmoov, so chunk offsets are known whenmoovis built. No rewind, no second pass. - Automatic
co64fallback — 64-bit chunk offsets kick in the moment any frame lands past 4 GiB.stcotruncation is not silent here. - Drift-free timing — media timescale is
fps_num, sample duration isfps_den.60000/1001stays exact. - Recoverable writer —
finishreturnsW, so aCursor<Vec<u8>>gives you the bytes back with.into_inner(). - Explicit errors —
Error::Io,Error::NoFrames,Error::InvalidFps. No panics on I/O. - AV1 sync-sample tracking — keyframes go in
stss; seekers work. - Small — ~500 lines, one file.
Installation
[]
= "0.1"
Requires Rust 1.56+.
Quick start
use Mp4Writer;
use File;
Feed it real AV1 from rav1e:
use Mp4Writer;
use ;
use File;
let cfg = EncoderConfig ;
let mut enc: = new.with_encoder_config.new_context?;
let mut m = new?;
while let Ok = enc.receive_packet
m.finish?;
Reference
Mp4Writer<W>
| Method | Description |
|---|---|
new(w, width, height, fps_num, fps_den) |
Write ftyp + placeholder mdat header. Returns Result<Self>. |
write_frame(obu, is_key) |
Append one AV1 OBU. Records offset/size/keyframe flag. |
frames_written() |
Frames written so far. |
finish() |
Patch mdat size, append moov. Consumes self, returns W. |
Error
| Variant | Cause |
|---|---|
Error::Io(io::Error) |
Underlying writer failed. Wraps std::io::Error. |
Error::NoFrames |
finish() called before any write_frame. |
Error::InvalidFps |
fps_num or fps_den was zero. |
FPS encoding
| Rate | fps_num |
fps_den |
|---|---|---|
| 24 fps | 24 |
1 |
| 30 fps | 30 |
1 |
| 29.97 fps | 30000 |
1001 |
| 59.94 fps | 60000 |
1001 |
| 60 fps | 60 |
1 |
Output layout
Every produced file is exactly this box tree:
ftyp — major "isom", compatible "av01"
mdat — raw AV1 OBU bytes
moov
├─ mvhd — movie header, timescale 1000
└─ trak — video track
├─ tkhd — width, height, matrix
└─ mdia
├─ mdhd — media timescale = fps_num
├─ hdlr — handler = "vide"
└─ minf
├─ vmhd
├─ dinf → dref
└─ stbl
├─ stsd → av01 → av1C
├─ stts — one entry, sample_delta = fps_den
├─ stss — 1-based sync sample indices
├─ stsc — 1 sample per chunk
├─ stsz — per-sample sizes
└─ stco | co64 — absolute chunk offsets
Because mdat sits before moov, every offset in stco / co64 is the absolute file position of the corresponding frame. No offsets are patched at finalisation — only the mdat size field.
stco vs co64
build_stco_or_co64 picks per file:
Any offset > u32::MAX? |
Box emitted | Offset width |
|---|---|---|
| no | stco |
32-bit |
| yes | co64 |
64-bit |
The mdat box itself is still capped at 4 GiB of payload — you get Error::Io (InvalidData) instead of a corrupted file if you cross that line.
Timing
Media timescale is set to fps_num and per-sample duration to fps_den. For 60000/1001 (59.94 fps) that gives you (sample_delta / timescale) = 1001/60000 exactly per frame — no accumulated drift over long clips.
What it does not do
| Status | Workaround | |
|---|---|---|
Audio track (mp4a, Opus) |
not supported | mux video only |
| B-frames / DTS ≠ PTS | not supported | encode with pts == dts |
| Fragmented MP4 (fMP4 / DASH) | not supported | use a fragmented muxer |
| Bitstream parsing | not supported | you tag keyframes yourself |
mdat > 4 GiB |
rejected | split output, or send a PR for largesize |
Dependencies
Runtime
| Crate | Purpose |
|---|---|
| — | none. |
Compile-time only
| Crate | Purpose |
|---|---|
| — | none. |
License
MIT — see LICENSE.
Changelog
See CHANGELOG.md.