# mp4box
[](https://crates.io/crates/mp4box)
[](https://docs.rs/mp4box)


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`:
```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:
```toml
mp4box = { version = "0.9.0", default-features = false, features = ["edit"] }
```
---
## Quick Start
```rust
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 decoding** — `mvhd`, `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 parsing** — `get_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 tools** — `mp4dump`, `mp4info`, `mp4samples`, `mp4edit`
---
## Example: Build a JSON tree (for GUIs / Tauri)
```rust
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:
```json
[
{
"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:
```rust
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)
```rust
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
```rust
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.
```rust
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
```bash
$ 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)
...
```
```bash
$ 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:
```bash
$ 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.
```bash
$ 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
```bash
$ 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
```bash
$ 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
```bash
$ 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`):
```bash
$ 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.
```rust
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, ®)?;
```
---
## Examples
Runnable examples live in [`examples/`](examples/):
| `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 |
```bash
cargo run --example media_info -- video.mp4
```
---
## License
MIT