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.3.0"
placeholder: "0.4.0"
validations:
required: true

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

- Added richer additive probe surfaces for broader codec families, codec-specific details, media-characteristics reporting, and lighter-weight probe controls for large-file inspection
- Added deterministic structured dump and `psshdump` JSON/YAML export, field-level dump payload reporting, and repeatable path or protection filters shared across text and structured output
- Expanded CLI path ergonomics with parsed-path extraction, subtree-scoped dump selection, path-scoped typed edit flows, and richer `psshdump` filtering by box path, system ID, and KID
- Improved `divide` by deriving playlist signaling from probed metadata and adding a first-class validation mode for unsupported fragmented layouts before any output is written
- Added optional `serde` support for reusable report types, including nested probe and dump companion data intended for library-side embedding
- Expanded checked-in fixture coverage for AV1, VP9, AAC, Opus, and PCM, and added dedicated high-level fuzz targets for probe, structured dump, and typed rewrite surfaces
- Refined README guidance, examples, tests, and goldens across the newer higher-level library and CLI workflows while preserving the existing low-level usage paths

# 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
Expand Down
10 changes: 9 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "mp4forge"
version = "0.3.0"
version = "0.4.0"
edition = "2024"
rust-version = "1.88"
authors = ["bakgio"]
Expand All @@ -16,5 +16,13 @@ exclude = [".github/**", "fuzz/**", "tests/**"]
all-features = true
rustdoc-args = ["--cfg", "docsrs"]

[features]
default = []
serde = ["dep:serde"]

[dependencies]
serde = { version = "1", features = ["derive"], optional = true }
terminal_size = "0.4"

[dev-dependencies]
serde_json = "1"
59 changes: 45 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,16 @@
- Low-level traversal, extraction, stringify, probe, and writer APIs
- 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
- Shared-fixture coverage for regular MP4, fragmented MP4, encrypted init segments, QuickTime-style metadata cases, and derived real codec fixtures for additional codec-family coverage

## Installation

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

# With optional features:
# mp4forge = { version = "0.4.0", features = ["serde"] }
```

Install the CLI from crates.io:
Expand All @@ -43,6 +46,17 @@ cargo install --path . --locked

The published crate includes both the library and the `mp4forge` binary from `src/bin/mp4forge.rs`.

## Feature Flags

`mp4forge` keeps the default dependency surface minimal and currently exposes one optional public
feature flag:

- `serde`: derives `Serialize` and `Deserialize` for the reusable public report structs under
`mp4forge::cli::probe` and `mp4forge::cli::dump`, along with their nested public codec-detail,
media-characteristics, `FieldValue`, and `FourCc` data. This is intended for library-side report
embedding and uses the Rust field names of those public structs; the CLI `-format` outputs keep
their existing hand-authored JSON and YAML schemas.

## CLI

```text
Expand All @@ -52,22 +66,39 @@ COMMAND:
divide split a fragmented MP4 into track playlists
dump display the MP4 box tree
edit rewrite selected boxes
extract extract raw boxes by type
extract extract raw boxes by type or path
psshdump summarize pssh boxes
probe summarize an MP4 file
```

For example:

```sh
mp4forge dump input.mp4
mp4forge probe input.mp4
mp4forge psshdump encrypted_init.mp4
```

## Feature Flags

`mp4forge` currently ships without public Cargo feature flags.
`divide` currently targets fragmented inputs with up to one AVC video track and one MP4A audio
track, including encrypted wrappers that preserve those original sample-entry formats. Pass
`-validate` when you want the same probe-driven layout checks without creating any output files.

`dump` defaults to the existing human-readable tree view. Pass `-format json` or `-format yaml` for
deterministic structured tree export with stable `payload_fields` for supported boxes; `-full` and
`-a` still control when large raw or unsupported payloads expand beyond the default summary-oriented
view. Add repeatable `-path <box/path>` filters when you want text or structured output rooted at
only the matched parsed subtrees instead of the whole file.

`edit` keeps the existing global `tfdt` replacement and `-drop` behavior, and now also accepts
repeatable `-path` filters when you want `-base_media_decode_time` to target only matching parsed
box paths.

`psshdump` defaults to the existing human-readable protection summary. Pass `-format json` or
`-format yaml` for deterministic structured reports with box offsets, system IDs, KIDs, `Data`
bytes, and the legacy raw-box base64 payload. Add repeatable `-path <box/path>`, `-system-id
<uuid>`, or `-kid <uuid>` filters when you want text and structured reports to return only the
matching protection boxes.

`probe` defaults to structured JSON output. When the input carries parsed codec-configuration
boxes, the report now includes a nested `codec_details` object per track for families such as AVC,
HEVC, AV1, VP8/VP9, MP4A, Opus, AC-3, PCM, XML subtitles, text subtitles, and WebVTT. When sample
entries carry `btrt`, `colr`, `pasp`, or `fiel`, the richer CLI path also emits nested
`media_characteristics` data such as declared bitrate, colorimetry, pixel aspect ratio, and
field-order hints. Pass `-detail light` for a lighter-weight probe that skips per-sample,
per-chunk, bitrate, and IDR aggregation, or use `mp4forge::probe::ProbeOptions` from the library
when you need the same control programmatically.

> See the [`examples/`](./examples) directory for the crate's low-level and high-level API usage patterns.

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

use mp4forge::cli::dump::{DumpOptions, build_field_structured_report_paths};
use mp4forge::walk::BoxPath;

fn main() -> Result<(), Box<dyn std::error::Error>> {
let input_path = env::args()
.nth(1)
.expect("usage: cargo run --example dump_selected_paths -- <input.mp4>");

let mut file = File::open(input_path)?;
let paths = [BoxPath::parse("moov/trak")?];
let report = build_field_structured_report_paths(&mut file, &DumpOptions::default(), &paths)?;

for entry in report.boxes {
println!("{} children={}", entry.path, entry.children.len());
}

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

use mp4forge::cli::dump::{DumpOptions, build_field_structured_report};

fn main() -> Result<(), Box<dyn std::error::Error>> {
let input_path = env::args().nth(1).ok_or_else(|| {
io::Error::new(
io::ErrorKind::InvalidInput,
"usage: dump_structured_fields INPUT.mp4",
)
})?;

let mut file = File::open(&input_path)?;
let report = build_field_structured_report(&mut file, &DumpOptions::default())?;

for root in &report.boxes {
println!("{} {}", root.path, root.payload_fields.len());
}

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

use mp4forge::probe::probe_codec_detailed;

fn main() -> Result<(), Box<dyn std::error::Error>> {
let input_path = env::args().nth(1).ok_or_else(|| {
io::Error::new(
io::ErrorKind::InvalidInput,
"usage: probe_codec_details INPUT.mp4",
)
})?;

let mut file = File::open(&input_path)?;
let summary = probe_codec_detailed(&mut file)?;

for track in &summary.tracks {
println!(
"track {} family {:?}",
track.summary.summary.track_id, track.summary.codec_family
);
println!(" details: {:?}", track.codec_details);
}

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

use mp4forge::probe::{ProbeOptions, probe_codec_detailed_with_options};

fn main() -> Result<(), Box<dyn std::error::Error>> {
let input_path = env::args().nth(1).ok_or_else(|| {
io::Error::new(
io::ErrorKind::InvalidInput,
"usage: probe_lightweight INPUT.mp4",
)
})?;

let mut file = File::open(&input_path)?;
let summary = probe_codec_detailed_with_options(&mut file, ProbeOptions::lightweight())?;

println!("fast start: {}", summary.fast_start);
println!("track num: {}", summary.tracks.len());
for track in &summary.tracks {
println!(
"track {} family {:?} expanded samples {}",
track.summary.summary.track_id,
track.summary.codec_family,
track.summary.summary.samples.len()
);
}

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

use mp4forge::probe::probe_media_characteristics;

fn main() -> Result<(), Box<dyn std::error::Error>> {
let Some(input_path) = env::args().nth(1) else {
eprintln!("usage: cargo run --example probe_media_characteristics -- <input.mp4>");
std::process::exit(1);
};

let mut file = File::open(input_path)?;
let summary = probe_media_characteristics(&mut file)?;

for track in &summary.tracks {
println!(
"track {} codec_family={:?}",
track.summary.summary.track_id, track.summary.codec_family
);
if let Some(declared) = track.media_characteristics.declared_bitrate.as_ref() {
println!(
" declared bitrate: avg={} max={} buffer={}",
declared.avg_bitrate, declared.max_bitrate, declared.buffer_size_db
);
}
if let Some(color) = track.media_characteristics.color.as_ref() {
println!(" color type: {}", color.colour_type);
}
if let Some(par) = track.media_characteristics.pixel_aspect_ratio.as_ref() {
println!(" pixel aspect ratio: {}/{}", par.h_spacing, par.v_spacing);
}
if let Some(field_order) = track.media_characteristics.field_order.as_ref() {
println!(
" field order: count={} ordering={} interlaced={}",
field_order.field_count, field_order.field_ordering, field_order.interlaced
);
}
}

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

use mp4forge::cli::pssh::build_pssh_report;

fn main() -> Result<(), Box<dyn std::error::Error>> {
let input_path = env::args()
.nth(1)
.expect("usage: cargo run --example pssh_report -- <input.mp4>");

let mut file = File::open(input_path)?;
let report = build_pssh_report(&mut file)?;

for entry in report.entries {
println!(
"{} offset={} system_id={} kid_count={} data_size={}",
entry.path, entry.offset, entry.system_id, entry.kid_count, entry.data_size
);
}

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

use mp4forge::cli::pssh::{PsshReportFilter, build_pssh_report_with_filters};
use mp4forge::walk::BoxPath;

fn main() -> Result<(), Box<dyn std::error::Error>> {
let input_path = env::args()
.nth(1)
.expect("usage: cargo run --example pssh_report_filtered -- <input.mp4>");

let mut file = File::open(input_path)?;
let report = build_pssh_report_with_filters(
&mut file,
&PsshReportFilter {
paths: vec![BoxPath::parse("moov")?],
system_ids: vec![[
0x10, 0x77, 0xef, 0xec, 0xc0, 0xb2, 0x4d, 0x02, 0xac, 0xe3, 0x3c, 0x1e, 0x52, 0xe2,
0xfb, 0x4b,
]],
kids: Vec::new(),
},
)?;

for entry in report.entries {
println!(
"{} system_id={} kid_count={} data_size={}",
entry.path, entry.system_id, entry.kid_count, entry.data_size
);
}

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

use mp4forge::cli::divide::{DivideTrackRole, validate_divide_reader};

fn main() -> Result<(), Box<dyn std::error::Error>> {
let input_path = env::args().nth(1).ok_or_else(|| {
io::Error::new(
io::ErrorKind::InvalidInput,
"usage: validate_divide_layout INPUT.mp4",
)
})?;

let mut file = File::open(&input_path)?;
let report = validate_divide_reader(&mut file)?;

for track in &report.tracks {
let role = match track.role {
DivideTrackRole::Video => "video",
DivideTrackRole::Audio => "audio",
};
let codec = track
.original_format
.or(track.sample_entry_type)
.map(|value| value.to_string())
.unwrap_or_else(|| format!("{:?}", track.codec_family));
println!(
"track {} role {} codec {} segments {}",
track.track_id, role, codec, track.segment_count
);
}

Ok(())
}
21 changes: 21 additions & 0 deletions fuzz/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -46,4 +46,25 @@ test = false
doc = false
bench = false

[[bin]]
name = "probe_reports"
path = "fuzz_targets/probe_reports.rs"
test = false
doc = false
bench = false

[[bin]]
name = "dump_reports"
path = "fuzz_targets/dump_reports.rs"
test = false
doc = false
bench = false

[[bin]]
name = "typed_rewrites"
path = "fuzz_targets/typed_rewrites.rs"
test = false
doc = false
bench = false

[workspace]
Loading
Loading