| Crates.io | muxide |
| lib.rs | muxide |
| version | 0.2.0 |
| created_at | 2025-12-17 23:51:22.830042+00 |
| updated_at | 2026-01-01 20:53:44.876856+00 |
| description | Minimal-dependency pure-Rust MP4 muxer for recording applications. Includes CLI tool and library API. |
| homepage | https://github.com/Michael-A-Kuykendall/muxide |
| repository | https://github.com/Michael-A-Kuykendall/muxide |
| max_upload_size | |
| id | 1991352 |
| size | 1,512,996 |

The last mile from encoder to playable MP4.
cargo add muxide
Muxide takes correctly-timestamped, already-encoded audio/video frames and produces a standards-compliant MP4 — pure Rust, minimal external dependencies, no FFmpeg.
| Your Encoder H.264 / HEVC / AV1 AAC / Opus |
➡️ | Muxide Pure Rust Minimal external deps |
➡️ | playable.mp4 Standards-compliant Fast-start ready |
If you're building a recording pipeline in Rust, you know the tradeoffs:
| Approach | Tradeoff |
|---|---|
| FFmpeg CLI/libs | External binary, GPL licensing concerns, "which build is this?" |
| GStreamer | Complex plugin system, C dependencies, heavy runtime |
| Raw MP4 writing | ISO-BMFF expertise required (sample tables, interleaving, moov layout) |
| "Minimal" crates | Often missing fast-start, strict validation, or production ergonomics |
Muxide solves one job cleanly:
Take already-encoded frames with correct timestamps → produce a standards-compliant, immediately-playable MP4 → using pure Rust.
Nothing more. Nothing less.
cargo add muxide
use muxide::api::{MuxerBuilder, VideoCodec};
let mut muxer = MuxerBuilder::new(file)
.video(VideoCodec::H264, 1920, 1080, 30.0)?
.build()?;
// Write your encoded frames...
muxer.write_video(0.0, &h264_frame, true)?;
muxer.finish()?;
# Install globally
cargo install muxide
# Or download pre-built binary from releases
# Then use:
muxide --help
# Quick examples:
muxide mux --video frames/ --output output.mp4 --width 1920 --height 1080 --fps 30
muxide mux --video video.h264 --audio audio.aac --output output.mp4
muxide validate --video frames/ --audio audio.aac
muxide info input.mp4
The CLI tool accepts raw encoded frames from stdin or files and produces MP4 output.
Muxide enforces a strict contract:
| Your Responsibility | Muxide's Guarantee |
|---|---|
| ✓ Frames are already encoded | ✓ Valid ISO-BMFF (MP4) |
| ✓ Timestamps are monotonic | ✓ Correct sample tables |
| ✓ DTS provided for B-frames | ✓ Fast-start layout |
| ✓ Codec headers in keyframes | ✓ No post-processing needed |
If input violates the contract, Muxide fails fast with explicit errors—no silent corruption, no guessing.
| Category | Supported | Notes |
|---|---|---|
| Video | H.264/AVC | Annex B format |
| H.265/HEVC | Annex B with VPS/SPS/PPS | |
| AV1 | OBU stream format | |
| VP9 | Frame header parsing, resolution/bit-depth/color config extraction | |
| Audio | AAC | All profiles: LC, Main, SSR, LTP, HE, HEv2 |
| Opus | Raw packets, 48kHz | |
| Container | Fast-start | moov before mdat for web playback |
| B-frames | Explicit PTS/DTS support | |
| Fragmented MP4 | For DASH/HLS streaming | |
| Metadata | Title, creation time, language | |
| Quality | World-class errors | Detailed diagnostics, hex dumps, JSON output |
| Production tested | FFmpeg compatibility verified | |
| Comprehensive testing | 80+ tests, property-based validation |
| Principle | Implementation |
|---|---|
| 🦀 Pure Rust | No unsafe, no FFI, no C bindings |
| 📦 Minimal deps | Only essential Rust crates — no external binaries |
| 🧵 Thread-safe | Send + Sync when writer is |
| ✅ Well-tested | Unit, integration, property tests |
| 📜 MIT license | No GPL, no copyleft concerns |
| 🚨 Developer-friendly | Exceptional error messages make debugging 10x faster |
Note:
no_stdis not supported. Muxide requiresstd::io::Write.
use muxide::api::{MuxerBuilder, VideoCodec, AudioCodec, Metadata};
use std::fs::File;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let file = File::create("recording.mp4")?;
let mut muxer = MuxerBuilder::new(file)
.video(VideoCodec::H264, 1920, 1080, 30.0)
.audio(AudioCodec::Aac, 48000, 2)
.with_metadata(Metadata::new().with_title("My Recording"))
.with_fast_start(true)
.build()?;
// Write encoded frames (from your encoder)
// muxer.write_video(pts_seconds, h264_annex_b_bytes, is_keyframe)?;
// muxer.write_audio(pts_seconds, aac_adts_bytes)?;
let stats = muxer.finish_with_stats()?;
println!("Wrote {} frames, {} bytes", stats.video_frames, stats.bytes_written);
Ok(())
}
// Requires VPS, SPS, PPS in first keyframe
let mut muxer = MuxerBuilder::new(file)
.video(VideoCodec::H265, 3840, 2160, 30.0)
.build()?;
muxer.write_video(0.0, &hevc_annexb_with_vps_sps_pps, true)?;
// Requires Sequence Header OBU in first keyframe
let mut muxer = MuxerBuilder::new(file)
.video(VideoCodec::Av1, 1920, 1080, 60.0)
.build()?;
muxer.write_video(0.0, &av1_obu_with_sequence_header, true)?;
// Opus always uses 48kHz internally (per spec)
let mut muxer = MuxerBuilder::new(file)
.video(VideoCodec::H264, 1920, 1080, 30.0)
.audio(AudioCodec::Opus, 48000, 2)
.build()?;
muxer.write_audio(0.0, &opus_packet)?;
use muxide::codec::vp9::Vp9Config;
// H.264
let sps_bytes = vec![0x67, 0x42, 0x00, 0x1e, 0xda, 0x02, 0x80, 0x2d, 0x8b, 0x11];
let pps_bytes = vec![0x68, 0xce, 0x38, 0x80];
let mut muxer = MuxerBuilder::new(file)
.video(VideoCodec::H264, 1920, 1080, 30.0)
.with_sps(sps_bytes)
.with_pps(pps_bytes)
.new_with_fragment()?;
// H.265
let vps_bytes = vec![0x40, 0x01, 0x0c, 0x01, 0xff, 0xff, 0x01, 0x60, 0x00];
let sps_bytes = vec![0x42, 0x01, 0x01, 0x01, 0x60, 0x00, 0x00, 0x03, 0x00, 0x90, 0x00];
let pps_bytes = vec![0x44, 0x01, 0xc0, 0x73, 0xc0, 0x4c, 0x90];
let mut muxer = MuxerBuilder::new(file)
.video(VideoCodec::H265, 1920, 1080, 30.0)
.with_vps(vps_bytes)
.with_sps(sps_bytes)
.with_pps(pps_bytes)
.new_with_fragment()?;
// AV1
let seq_header_bytes = vec![
0x0A, 0x10, // OBU header + size (example)
0x00, 0x00, 0x00, 0x00,
];
let mut muxer = MuxerBuilder::new(file)
.video(VideoCodec::Av1, 1920, 1080, 30.0)
.with_av1_sequence_header(seq_header_bytes)
.new_with_fragment()?;
// VP9
let vp9_config = Vp9Config {
width: 1920,
height: 1080,
profile: 0,
bit_depth: 8,
color_space: 0,
transfer_function: 0,
matrix_coefficients: 0,
level: 0,
full_range_flag: 0,
};
let mut muxer = MuxerBuilder::new(file)
.video(VideoCodec::Vp9, 1920, 1080, 30.0)
.with_vp9_config(vp9_config)
.new_with_fragment()?;
// Get init segment (ftyp + moov)
let init_segment = muxer.init_segment();
// Write frames...
muxer.write_video(0, 0, &frame, true)?;
// Get media segments (moof + mdat)
if let Some(segment) = muxer.flush_segment() {
// Send segment to client
}
// When encoder produces B-frames, provide both PTS and DTS
muxer.write_video_with_dts(
pts_seconds, // Presentation timestamp
dts_seconds, // Decode timestamp (for B-frame ordering)
&frame_data,
is_keyframe
)?;
Muxide includes a command-line tool for quick testing and development workflows:
# Install the CLI tool
cargo install muxide
# Basic video-only muxing
muxide mux \
--video keyframes.h264 \
--width 1920 --height 1080 --fps 30 \
--output recording.mp4
# Video + audio with metadata
muxide mux \
--video stream.h264 \
--audio stream.aac \
--video-codec h264 \
--audio-codec aac-he \
--width 1920 --height 1080 --fps 30 \
--sample-rate 44100 --channels 2 \
--title "My Recording" \
--language eng \
--output final.mp4
# JSON output for automation
muxide mux --json [args...] > stats.json
# Validate input files without muxing
muxide validate --video input.h264 --audio input.aac
# Get info about supported codecs
muxide info
Supported Codecs:
Features:
--verboseMuxide is intentionally focused. It does not:
| Not Supported | Why |
|---|---|
| Encoding/decoding | Use openh264, x264, rav1e, etc. |
| Transcoding | Not a codec library |
| Demuxing/reading MP4 | Write-only by design |
| Timestamp correction | Garbage in = error out |
| Non-MP4 containers | MKV, WebM, AVI not supported |
| DRM/encryption | Out of scope |
Muxide is the last mile: encoder output → playable file.
Muxide is a great fit for:
Probably not a fit if you need encoding, demuxing, or legacy codecs (MPEG-2, etc.).
The faststart_proof example demonstrates a structural MP4 invariant:
$ cargo run --example faststart_proof --release
output: recording_faststart.mp4
layout invariant: moov before mdat = YES
output: recording_normal.mp4
layout invariant: moov before mdat = NO
When served over HTTP, the fast-start file can begin playback without waiting for the full download (player behavior varies, but the layout property is deterministic).
This example is intentionally minimal:
Muxide is designed for minimal overhead. Muxing should never be your bottleneck.
| Scenario | Time | Throughput |
|---|---|---|
| 1000 H.264 frames | 264 µs | 3.7M frames/sec |
| 1000 H.264 + fast-start | 362 µs | 2.8M frames/sec |
| 1000 video + 1500 audio | 457 µs | 2.2M frames/sec |
| 100 4K frames (~6.5 MB) | 14 ms | 464 MB/sec |
Note: Benchmarks are based on development hardware. Encoding is typically the bottleneck—muxing overhead is negligible. Run
cargo benchfor your environment (dev-only benchmarks available).AVC
00 00 00 01 or 00 00 01)| Resource | Description |
|---|---|
| 📚 API Reference | Complete API documentation |
| 📜 Design Charter | Architecture decisions and rationale |
| 📋 API Contract | Input/output guarantees |
FFmpeg is excellent, but:
Muxide is a single cargo add with minimal external dependencies.
No. Muxide is muxing only. For encoding, use:
openh264 — H.264 encoding (BSD)rav1e — AV1 encoding (BSD)x264/x265 — H.264/HEVC (GPL, via FFI)Muxide will reject non-monotonic timestamps with a clear error. It does not attempt to "fix" broken input — this is by design to ensure predictable output.
Yes. Muxide has an extensive test suite (unit, integration, property-based tests) and is designed for predictable, deterministic behavior.
MIT — no GPL, no copyleft, no surprises.
Muxide is designed to be boring in the best way:
predictable, strict, fast, and invisible once integrated.