Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/ISSUE_TEMPLATE/bug_report.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ body:
attributes:
label: mp4forge Version
description: Which version are you using?
placeholder: "0.2.0"
placeholder: "0.3.0"
validations:
required: true

Expand Down
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
# 0.3.0 (April 22, 2026)

- Added byte-slice convenience helpers for typed extract, rewrite, and probe workflows so higher-level integrations can stay in-memory without dropping to the lower-level APIs
- Added exact raw box-byte extraction helpers for full-box and payload-only reads, including registry-aware variants for custom box decoding workflows
- Added additive `BoxPath` string parsing with `BoxPath::parse`, `FromStr`, and `TryFrom<&str>` so ergonomic path construction can build on the existing low-level API
- Expanded examples, tests, and comparison coverage around the new ergonomic helpers while preserving the existing low-level usage paths
- Refined public docs and README guidance for the new helper surface

# 0.2.0 (April 21, 2026)

- Added typed path-based extraction helpers for common read flows: `extract_box_as`, `extract_boxes_as`, and `extract_boxes_as_with_registry`
Expand Down
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "mp4forge"
version = "0.2.0"
version = "0.3.0"
edition = "2024"
rust-version = "1.88"
authors = ["bakgio"]
Expand Down
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,15 @@

- Typed MP4 and ISOBMFF box model with registry-backed custom box support
- Low-level traversal, extraction, stringify, probe, and writer APIs
- Thin typed path-based helpers for common extraction and rewrite flows
- Thin typed path-based helpers and byte-slice convenience wrappers for common extraction, rewrite, and probe flows
- Built-in CLI for `dump`, `extract`, `probe`, `psshdump`, `edit`, and `divide`
- Shared-fixture coverage for regular MP4, fragmented MP4, encrypted init segments, and QuickTime-style metadata cases

## Installation

```toml
[dependencies]
mp4forge = "0.2.0"
mp4forge = "0.3.0"
```

Install the CLI from crates.io:
Expand Down Expand Up @@ -69,7 +69,7 @@ mp4forge psshdump encrypted_init.mp4

`mp4forge` currently ships without public Cargo feature flags.

> See the [`examples/`](./examples) directory for both the low-level and high-level public API story, including typed extraction in `extract_track_ids_typed.rs`, typed rewrite in `rewrite_emsg.rs`, structure walking, probing, writer-backed rewrite, and custom box registration.
> See the [`examples/`](./examples) directory for the crate's low-level and high-level API usage patterns.

## License

Expand Down
41 changes: 41 additions & 0 deletions examples/extract_mdhd_bytes.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
use std::env;
use std::error::Error;
use std::fs::File;

use mp4forge::FourCc;
use mp4forge::extract::{extract_box_bytes, extract_box_payload_bytes};
use mp4forge::walk::BoxPath;

fn main() {
if let Err(error) = run() {
eprintln!("{error}");
std::process::exit(1);
}
}

fn run() -> Result<(), Box<dyn Error>> {
let Some(input_path) = env::args().nth(1) else {
return Err("usage: cargo run --example extract_mdhd_bytes -- <input.mp4>".into());
};

let box_path = BoxPath::from([
FourCc::from_bytes(*b"moov"),
FourCc::from_bytes(*b"trak"),
FourCc::from_bytes(*b"mdia"),
FourCc::from_bytes(*b"mdhd"),
]);

let mut file = File::open(input_path)?;
let boxes = extract_box_bytes(&mut file, None, box_path.clone())?;
let payloads = extract_box_payload_bytes(&mut file, None, box_path)?;

for (index, (box_bytes, payload_bytes)) in boxes.iter().zip(payloads.iter()).enumerate() {
println!(
"match {index}: total_bytes={} payload_bytes={}",
box_bytes.len(),
payload_bytes.len()
);
}

Ok(())
}
39 changes: 39 additions & 0 deletions examples/extract_track_ids_typed_bytes.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
use std::env;
use std::error::Error;
use std::fs;

use mp4forge::FourCc;
use mp4forge::boxes::iso14496_12::Tkhd;
use mp4forge::extract::extract_box_as_bytes;
use mp4forge::walk::BoxPath;

fn main() {
if let Err(error) = run() {
eprintln!("{error}");
std::process::exit(1);
}
}

fn run() -> Result<(), Box<dyn Error>> {
let Some(path) = env::args().nth(1) else {
return Err(
"usage: cargo run --example extract_track_ids_typed_bytes -- <input.mp4>".into(),
);
};

let input = fs::read(path)?;
let headers = extract_box_as_bytes::<Tkhd>(
&input,
BoxPath::from([
FourCc::from_bytes(*b"moov"),
FourCc::from_bytes(*b"trak"),
FourCc::from_bytes(*b"tkhd"),
]),
)?;

for tkhd in headers {
println!("track ID: {}", tkhd.track_id);
}

Ok(())
}
31 changes: 31 additions & 0 deletions examples/extract_track_ids_typed_parsed_path.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
use std::env;
use std::error::Error;
use std::fs::File;

use mp4forge::boxes::iso14496_12::Tkhd;
use mp4forge::extract::extract_box_as;
use mp4forge::walk::BoxPath;

fn main() {
if let Err(error) = run() {
eprintln!("{error}");
std::process::exit(1);
}
}

fn run() -> Result<(), Box<dyn Error>> {
let Some(path) = env::args().nth(1) else {
return Err(
"usage: cargo run --example extract_track_ids_typed_parsed_path -- <input.mp4>".into(),
);
};

let mut file = File::open(path)?;
let headers = extract_box_as::<_, Tkhd>(&mut file, None, BoxPath::parse("moov/trak/tkhd")?)?;

for tkhd in headers {
println!("track ID: {}", tkhd.track_id);
}

Ok(())
}
23 changes: 23 additions & 0 deletions examples/probe_track_count_bytes.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
use std::env;
use std::error::Error;
use std::fs;

use mp4forge::probe::probe_bytes;

fn main() {
if let Err(error) = run() {
eprintln!("{error}");
std::process::exit(1);
}
}

fn run() -> Result<(), Box<dyn Error>> {
let Some(path) = env::args().nth(1) else {
return Err("usage: cargo run --example probe_track_count_bytes -- <input.mp4>".into());
};

let input = fs::read(path)?;
let info = probe_bytes(&input)?;
println!("track num: {}", info.tracks.len());
Ok(())
}
67 changes: 67 additions & 0 deletions examples/rewrite_emsg_bytes.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
use std::env;
use std::error::Error;
use std::fs;

use mp4forge::FourCc;
use mp4forge::boxes::iso14496_12::Emsg;
use mp4forge::rewrite::rewrite_box_as_bytes;
use mp4forge::walk::BoxPath;

fn main() {
if let Err(error) = run() {
eprintln!("{error}");
std::process::exit(1);
}
}

fn run() -> Result<(), Box<dyn Error>> {
let Some(output_path) = env::args().nth(1) else {
return Err("usage: cargo run --example rewrite_emsg_bytes -- <output.mp4>".into());
};

let input = sample_emsg_file();
let output = rewrite_box_as_bytes::<Emsg, _>(
&input,
BoxPath::from([FourCc::from_bytes(*b"emsg")]),
|emsg| {
emsg.message_data = b"hello world".to_vec();
},
)?;
fs::write(output_path, output)?;

Ok(())
}

fn sample_emsg_file() -> Vec<u8> {
let mut emsg_payload = vec![0x00, 0x00, 0x00, 0x00];
append_null_terminated_string(&mut emsg_payload, "urn:test");
append_null_terminated_string(&mut emsg_payload, "demo");
append_u32(&mut emsg_payload, 1000);
append_u32(&mut emsg_payload, 0);
append_u32(&mut emsg_payload, 5);
append_u32(&mut emsg_payload, 1);
emsg_payload.extend_from_slice(b"hello");

let mut file = Vec::new();
file.extend_from_slice(&box_bytes("free", &[0x01, 0x02, 0x03]));
file.extend_from_slice(&box_bytes("emsg", &emsg_payload));
file.extend_from_slice(&box_bytes("free", &[0x04, 0x05]));
file
}

fn append_null_terminated_string(dst: &mut Vec<u8>, value: &str) {
dst.extend_from_slice(value.as_bytes());
dst.push(0x00);
}

fn append_u32(dst: &mut Vec<u8>, value: u32) {
dst.extend_from_slice(&value.to_be_bytes());
}

fn box_bytes(box_type: &str, payload: &[u8]) -> Vec<u8> {
let mut box_bytes = Vec::with_capacity(8 + payload.len());
box_bytes.extend_from_slice(&((payload.len() + 8) as u32).to_be_bytes());
box_bytes.extend_from_slice(box_type.as_bytes());
box_bytes.extend_from_slice(payload);
box_bytes
}
Loading
Loading