mp4box 0.11.0

MP4/ISOBMFF parser and editor: box trees, typed decoding, sample tables, fragmented MP4, and non-destructive editing
Documentation

mp4box

Crates.io Docs.rs License Rust Version

A minimal, dependency-light MP4/ISOBMFF parser and editor for Rust. Parses the full box tree (including codec configuration inside stsd), decodes known boxes into typed structures, extracts per-sample tables from both progressive and fragmented (fMP4/DASH/CMAF) files, reads iTunes metadata, and performs non-destructive box editing with automatic size and chunk-offset fixup. Suitable for CLIs, Tauri backends, media inspectors, and developer tooling.


Installation

Add this to your Cargo.toml:

[dependencies]
mp4box = "0.9.0"
anyhow = "1.0"  # For error handling in examples

The library core depends only on anyhow and serde. Two default-on features add the rest:

  • edit — non-destructive box editing (no extra dependencies)
  • cli — the mp4dump/mp4info/mp4samples/mp4edit binaries (pulls in clap and serde_json)

For the lightest library-only build:

mp4box = { version = "0.9.0", default-features = false, features = ["edit"] }

Quick Start

use mp4box::get_boxes;
use std::fs::File;

fn main() -> anyhow::Result<()> {
    let mut file = File::open("video.mp4")?;
    let size = file.metadata()?.len();

    // Parse all boxes without decoding
    let boxes = get_boxes(&mut file, size, false)?;
    println!("Found {} top-level boxes", boxes.len());

    // Parse with decoding for known box types
    let mut file = File::open("video.mp4")?;
    let decoded_boxes = get_boxes(&mut file, size, true)?;

    // Print decoded info for ftyp box
    if let Some(ftyp) = decoded_boxes.iter().find(|b| b.typ == "ftyp") {
        println!("File type: {}", ftyp.decoded.as_ref().unwrap_or(&"unknown".to_string()));
    }

    Ok(())
}

Features

  • Zero-dependency core parser (Read + Seek)
  • Full MP4/ISOBMFF box tree
    • Leaf, FullBox (version/flags), Container, FullBox containers (meta, stsd), UUID
    • Large-size (64-bit) boxes; recursion into stsd sample entries and their codec configuration children (avcC, hvcC, esds, dOps, ...)
  • Known-box registry — hundreds of ISO/HEIF/MPEG boxes with full names
  • Typed structured decodingmvhd, tkhd, mdhd, stsd, the whole sample table family, elst, sidx, and the fragment boxes (tfhd/tfdt/trun/trex) decode to serializable structs
  • Sample tables — per-sample DTS/PTS, duration, size, file offset, and keyframe flags for progressive and fragmented (fMP4/DASH/CMAF) files
  • iTunes metadata — read tags with get_itunes_tags, write them with the edit API
  • Non-destructive editing (edit feature) — remove/insert/replace boxes, patch header fields, set tags; box sizes and stco/co64 chunk offsets are fixed up automatically
  • Tolerant parsingget_boxes_tolerant recovers from malformed boxes, returning the partial tree plus located issues instead of an error
  • Custom decoders — attach your own parser for any 4CC or UUID
  • Frontend-friendly JSON — works perfectly in Tauri or WebView apps
  • CLI toolsmp4dump, mp4info, mp4samples, mp4edit

Example: Build a JSON tree (for GUIs / Tauri)

use mp4box::get_boxes;
use std::fs::File;

let mut file = File::open("video.mp4")?;
let size = file.metadata()?.len();
let boxes = get_boxes(&mut file, size, /*decode=*/ true)?;
println!("{} top-level boxes", boxes.len());

This returns a serializable tree:

[
  {
    "offset": 0,
    "size": 32,
    "typ": "ftyp",
    "full_name": "File Type Box",
    "kind": "leaf",
    "decoded": "major=isom minor=512 compatible=[...]",
    "children": null
  }
]

Known boxes also carry structured_data — typed values instead of strings:

use mp4box::registry::StructuredData;

if let Some(StructuredData::MovieHeader(mvhd)) = &boxes[1].children.as_ref().unwrap()[0].structured_data {
    println!("duration: {:.2}s", mvhd.duration as f64 / mvhd.timescale as f64);
}

Example: Per-sample tables (progressive and fragmented)

use mp4box::track_samples_from_path;

for track in track_samples_from_path("video.mp4")? {
    let keyframes = track.samples.iter().filter(|s| s.is_sync).count();
    println!(
        "track {} ({}): {} samples, {} keyframes, {:.2}s",
        track.track_id,
        track.handler_type,
        track.sample_count,
        keyframes,
        track.duration as f64 / track.timescale as f64,
    );
    // each sample: dts, pts, duration, size, file_offset, is_sync
}

Fragmented files (moof/traf/trun) are handled transparently: samples are assembled across all fragments using the tfhd/trex defaulting rules, with byte offsets and keyframe flags matching what ffprobe reports.


Example: iTunes metadata

use mp4box::get_itunes_tags;
use std::fs::File;

let mut file = File::open("video.mp4")?;
let size = file.metadata()?.len();
let tags = get_itunes_tags(&mut file, size)?;
// {"title": "...", "artist": "...", "encoder": "...", "track": "3/12", ...}

Example: Editing (requires the edit feature)

Editing is non-destructive: the source file is never modified, untouched bytes are streamed through verbatim (an mdat is never loaded into memory), and ancestor box sizes plus stco/co64 chunk offsets are recomputed automatically. Re-serializing with no edits reproduces the input byte for byte.

use mp4box::edit::Editor;

let mut editor = Editor::new();
editor.set_tag("title", "My Movie")?;
editor.set_field("moov/mvhd", "creation_time", "0");
editor.remove("moov/udta/meta");           // paths support indices: moov/trak[1]/...
editor.remove_all("free");                 // strip every `free` box
editor.faststart();                        // move moov before mdat

let stats = editor.process_file("in.mp4", "out.mp4")?;
println!("{} chunk offsets adjusted", stats.chunk_offsets_adjusted);

Fragmented (moof/sidx) and HEIF (iloc) files are refused with a clear error rather than corrupted — their internal offsets are not covered by the fixup pass yet.


Command-line tools

mp4dump — box tree explorer

$ mp4dump input.mp4
   0x0         32 ftyp (File Type Box)
  0x20          8 free (Free Space Box)
  0x28    3374542 mdat (Media Data Box)
0x337df6     170458 moov (Movie Box) (container)
  0x337dfe        108 mvhd (Movie Header Box) (ver=0, flags=0x000000)
  ...
$ mp4dump input.mp4 --decode
   0x0         32 ftyp (File Type Box)
        -> major=isom minor=512 compatible=["isom", "iso2", "avc1", "mp41"]
...
           0x191        174 stsd (Sample Description Box) (container, ver=0, flags=0x000000)
        -> codec=avc1 1920x1080 entries=1
             0x1a1        158 avc1 (AVC Video Sample Entry) (container)
               0x1f7         56 avcC (AVC Decoder Configuration Box)
        -> configurationVersion=1 profile=100 compat=0x00 level=4.0 nal_length_size=4

Filter a subtree, emit JSON, or dump raw payload bytes:

$ mp4dump input.mp4 --filter 'moov/trak[0]/mdia/minf/stbl'
$ mp4dump input.mp4 --decode --json
$ mp4dump input.mp4 --raw stsd --bytes 256

Damaged files are handled gracefully by default: parsing recovers past malformed boxes, prints the partial tree, reports what was wrong and where on stderr, and exits with code 2. Use --strict to fail on the first malformed box instead.

$ mp4dump damaged.mp4
...
2 parse issue(s):
  0x294b4a2: box 'mdia' declares size 284285 which overruns its container by 84287 bytes; clamped
  0x2960979: box 'stsz' declares size 126484 which overruns its container by 13741 bytes; clamped
$ echo $?
2

mp4info — media summary

$ mp4info input.mp4
File: input.mp4
Major brand: isom
Minor version: 512
Compatible brands: isom, iso2, avc1, mp41
Movie duration: 600000 ticks @ 1000 -> 600.000 s
Tracks:
  Track 1:
    type: video
    codec: avc1
    size: 320x180
    timescale: 12800
    duration: 7680000 ticks -> 600.000 s
    language: und

mp4samples — per-sample tables

$ mp4samples input.mp4 --limit 3
Track 1 (vide) timescale=12800 duration=7680000 sample_count=15000
idx    start(s)   dur(ts)  size   offset      sync
----------------------------------------------------
    0     0.0800      512   3417         48 *
    1     0.1600      512    264       3465
    2     0.1200      512    159       3729

mp4edit — non-destructive editor

$ mp4edit --tag title="My Movie" --tag artist="Me" \
          --set moov/mvhd.creation_time=0 \
          --remove-all free \
          input.mp4 output.mp4
wrote output.mp4 (158008454 bytes, 2336 chunk offsets adjusted)

Optimize for progressive playback (moov before mdat, like qt-faststart):

$ mp4edit --faststart input.mp4 output.mp4

Adding Custom Box Decoders

Extend the default registry with a decoder for any 4CC or UUID; the decoded value shows up in the same tree as the built-ins.

use mp4box::{BoxHeader, BoxKey, FourCC, get_boxes_with_registry};
use mp4box::registry::{BoxDecoder, BoxValue, default_registry};
use std::fs::File;
use std::io::Read;

struct MyDecoder;

impl BoxDecoder for MyDecoder {
    fn decode(
        &self,
        r: &mut dyn Read,
        _hdr: &BoxHeader,
        _version: Option<u8>,
        _flags: Option<u32>,
    ) -> anyhow::Result<BoxValue> {
        let mut buf = Vec::new();
        r.read_to_end(&mut buf)?;
        Ok(BoxValue::Text(format!("{} bytes", buf.len())))
    }
}

let reg = default_registry().with_decoder(
    BoxKey::FourCC(FourCC(*b"xyz ")),
    "xyz ",
    Box::new(MyDecoder),
);

let mut file = File::open("video.mp4")?;
let size = file.metadata()?.len();
let boxes = get_boxes_with_registry(&mut file, size, true, &reg)?;

Examples

Runnable examples live in examples/:

Example Shows
boxes Walking the box tree with names and version/flags
media_info Typed structured data: movie/track headers, codec details, edit lists, tags
samples Sample table analysis
fragments Fragmented MP4 inspection and assembled per-track samples
edit Setting tags and zeroing timestamps with the edit API
simple Hex-dumping a byte range
cargo run --example media_info -- video.mp4

License

MIT