mp4_muxer 0.1.0

Zero-dependency MP4 muxer for AV1 video streams
Documentation
  • Coverage
  • 100%
    12 out of 12 items documented1 out of 7 items with examples
  • Size
  • Source code size: 42.12 kB This is the summed size of all the files inside the crates.io package for this release.
  • Documentation size: 634.96 kB This is the summed size of all files generated by rustdoc for all configured targets
  • Ø build duration
  • this release: 4s Average build duration of successful builds.
  • all releases: 4s Average build duration of successful builds in releases after 2024-10-23.
  • Links
  • NameOfShadow/mp4_muxer
    1 0 0
  • crates.io
  • Dependencies
  • Versions
  • Owners
  • NameOfShadow

crates.io Documentation MSRV License Dependency Status downloads


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 mp4_muxer::Mp4Writer;
use std::fs::File;

let file = File::create("out.mp4")?;
let mut muxer = Mp4Writer::new(file, 320, 240, 30, 1)?;
muxer.write_frame(&av1_packet, true)?;
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, no mp4parse-rust, no bento4, no FFmpeg.
  • Single-pass writermdat precedes moov, so chunk offsets are known when moov is built. No rewind, no second pass.
  • Automatic co64 fallback — 64-bit chunk offsets kick in the moment any frame lands past 4 GiB. stco truncation is not silent here.
  • Drift-free timing — media timescale is fps_num, sample duration is fps_den. 60000/1001 stays exact.
  • Recoverable writerfinish returns W, so a Cursor<Vec<u8>> gives you the bytes back with .into_inner().
  • Explicit errorsError::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

[dependencies]
mp4_muxer = "0.1"

Requires Rust 1.56+.

Quick start

use mp4_muxer::Mp4Writer;
use std::fs::File;

fn mux_av1<I>(packets: I, width: u32, height: u32) -> mp4_muxer::Result<()>
where
    I: IntoIterator<Item = (Vec<u8>, bool)>, // (obu bytes, is_keyframe)
{
    let file  = File::create("out.mp4")?;
    let mut m = Mp4Writer::new(file, width, height, 30, 1)?;

    for (obu, is_key) in packets {
        m.write_frame(&obu, is_key)?;
    }

    // finish() consumes the muxer and returns the underlying writer,
    // so File::sync_all() / Cursor::into_inner() still works after muxing.
    let _file = m.finish()?;
    Ok(())
}

Feed it real AV1 from rav1e:

use mp4_muxer::Mp4Writer;
use rav1e::{prelude::*, Config, EncoderConfig};
use std::fs::File;

let cfg = EncoderConfig { width: 320, height: 240, ..Default::default() };
let mut enc: Context<u8> = Config::new().with_encoder_config(cfg).new_context()?;

let mut m = Mp4Writer::new(File::create("out.mp4")?, 320, 240, 30, 1)?;
while let Ok(pkt) = enc.receive_packet() {
    m.write_frame(&pkt.data, pkt.frame_type == FrameType::KEY)?;
}
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.