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.4.0"
placeholder: "0.5.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.5.0 (April 25, 2026)

- Added first-class encrypted metadata coverage for typed `senc`, typed `sgpd(seig)`, resolved sample-encryption helpers, and broader encrypted fragmented fixture coverage across extraction, rewrite, and probe flows
- Added additive top-level `sidx` analysis, planning, rewrite, documentation, and example support for the supported fragmented-file layouts
- Expanded typed box coverage across fragmented timing, metadata, and codec families, including `clap`, `SmDm`, `CoLL`, `dec3`, `dac4`, `vvcC`, AVS3, FLAC, MPEG-H, `subs`, `elng`, `ssix`, `leva`, `evte`, `silb`, `emib`, `emeb`, `ID32`, loudness boxes, `prft`, typed `tref` children, `sthd`, `nmhd`, `kind`, `mime`, `cdat`, and selected legacy `uuid` payloads
- Improved low-level robustness by preserving legal trailing bytes in `VisualSampleEntry` layouts and carrying those bytes cleanly through traversal and rewrite paths
- Added `prft` timestamp and flag helpers, richer examples, and broader regression coverage for fragmented, encrypted, metadata-rich, and legacy MP4 layouts

# 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
Expand Down
4 changes: 2 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "mp4forge"
version = "0.4.0"
version = "0.5.0"
edition = "2024"
rust-version = "1.88"
authors = ["bakgio"]
Expand All @@ -9,7 +9,7 @@ description = "Rust library and CLI for inspecting, probing, extracting, and rew
repository = "https://github.com/bakgio/mp4forge"
readme = "README.md"
keywords = ["mp4", "isobmff", "parser", "video", "cli"]
categories = ["command-line-utilities", "multimedia::video", "parser-implementations"]
categories = ["command-line-utilities", "multimedia::video"]
exclude = [".github/**", "fuzz/**", "tests/**"]

[package.metadata.docs.rs]
Expand Down
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,17 +19,18 @@
- 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 and byte-slice convenience wrappers for common extraction, rewrite, and probe flows
- Fragmented top-level `sidx` analysis, planning, and rewrite APIs for supported layouts
- Built-in CLI for `dump`, `extract`, `probe`, `psshdump`, `edit`, and `divide`
- 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.4.0"
mp4forge = "0.5.0"

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

Install the CLI from crates.io:
Expand Down
47 changes: 47 additions & 0 deletions examples/probe_extended_media_characteristics.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
use std::env;
use std::fs::File;

use mp4forge::probe::probe_extended_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_extended_media_characteristics -- <input.mp4>");
std::process::exit(1);
};

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

for track in &summary.tracks {
println!(
"track {} sample_entry_type={:?}",
track.summary.summary.track_id, track.summary.sample_entry_type
);
if let Some(aperture) = track.visual_metadata.clean_aperture.as_ref() {
println!(
" clean aperture: {}/{} x {}/{}",
aperture.width_numerator,
aperture.width_denominator,
aperture.height_numerator,
aperture.height_denominator
);
}
if let Some(light) = track.visual_metadata.content_light_level.as_ref() {
println!(
" content light level: max_cll={} max_fall={}",
light.max_cll, light.max_fall
);
}
if let Some(display) = track.visual_metadata.mastering_display.as_ref() {
println!(
" mastering display: white_point=({}, {}) luminance={}..{}",
display.white_point_chromaticity_x,
display.white_point_chromaticity_y,
display.luminance_min,
display.luminance_max
);
}
}

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

use mp4forge::sidx::{
TopLevelSidxPlanAction, TopLevelSidxPlanOptions, apply_top_level_sidx_plan,
plan_top_level_sidx_update_bytes,
};

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

fn run() -> Result<(), Box<dyn Error>> {
let args = env::args().skip(1).collect::<Vec<_>>();
let (input_path, output_path, non_zero_ept) = match args.as_slice() {
[input_path, output_path] => (input_path.as_str(), output_path.as_str(), false),
[input_path, output_path, flag] if flag == "--non-zero-ept" => {
(input_path.as_str(), output_path.as_str(), true)
}
_ => {
return Err(
"usage: cargo run --example refresh_top_level_sidx -- <input.mp4> <output.mp4> [--non-zero-ept]"
.into(),
);
}
};

let input = std::fs::read(input_path)?;
let Some(plan) = plan_top_level_sidx_update_bytes(
&input,
TopLevelSidxPlanOptions {
add_if_not_exists: true,
non_zero_ept,
},
)?
else {
return Err("no top-level sidx change was needed".into());
};

let action = match &plan.action {
TopLevelSidxPlanAction::Insert => "inserted",
TopLevelSidxPlanAction::Replace { .. } => "updated",
};

let mut output = Vec::with_capacity(input.len().saturating_add(plan.encoded_box_size as usize));
let applied = apply_top_level_sidx_plan(&mut Cursor::new(&input), &mut output, &plan)?;
std::fs::write(output_path, &output)?;

println!(
"{action} top-level sidx at offset {} with {} references",
applied.info.offset(),
applied.sidx.reference_count
);

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

use mp4forge::FourCc;
use mp4forge::boxes::iso14496_12::{Saiz, Sbgp, Sgpd};
use mp4forge::boxes::iso23001_7::{Senc, Tenc};
use mp4forge::encryption::{
ResolvedSampleEncryptionSource, SampleEncryptionContext, resolve_sample_encryption,
};
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 resolve_sample_encryption -- <input.mp4>".into());
};

let input = std::fs::read(path)?;
let tenc = extract_box_as_bytes::<Tenc>(
&input,
BoxPath::from([
FourCc::from_bytes(*b"moov"),
FourCc::from_bytes(*b"trak"),
FourCc::from_bytes(*b"mdia"),
FourCc::from_bytes(*b"minf"),
FourCc::from_bytes(*b"stbl"),
FourCc::from_bytes(*b"stsd"),
FourCc::ANY,
FourCc::from_bytes(*b"sinf"),
FourCc::from_bytes(*b"schi"),
FourCc::from_bytes(*b"tenc"),
]),
)?
.into_iter()
.next();

let senc = extract_box_as_bytes::<Senc>(
&input,
BoxPath::from([
FourCc::from_bytes(*b"moof"),
FourCc::from_bytes(*b"traf"),
FourCc::from_bytes(*b"senc"),
]),
)?
.into_iter()
.next()
.ok_or("no senc box found")?;

let sgpd = extract_box_as_bytes::<Sgpd>(
&input,
BoxPath::from([
FourCc::from_bytes(*b"moof"),
FourCc::from_bytes(*b"traf"),
FourCc::from_bytes(*b"sgpd"),
]),
)?
.into_iter()
.next();
let sbgp = extract_box_as_bytes::<Sbgp>(
&input,
BoxPath::from([
FourCc::from_bytes(*b"moof"),
FourCc::from_bytes(*b"traf"),
FourCc::from_bytes(*b"sbgp"),
]),
)?
.into_iter()
.next();
let saiz = extract_box_as_bytes::<Saiz>(
&input,
BoxPath::from([
FourCc::from_bytes(*b"moof"),
FourCc::from_bytes(*b"traf"),
FourCc::from_bytes(*b"saiz"),
]),
)?
.into_iter()
.next();

let resolved = resolve_sample_encryption(
&senc,
SampleEncryptionContext {
tenc: tenc.as_ref(),
sgpd: sgpd.as_ref(),
sbgp: sbgp.as_ref(),
saiz: saiz.as_ref(),
},
)?;

for sample in resolved.samples {
let source = match sample.metadata_source {
ResolvedSampleEncryptionSource::TrackEncryptionBox => "tenc".to_string(),
ResolvedSampleEncryptionSource::SampleGroupDescription {
group_description_index,
description_index,
fragment_local,
} => format!(
"sgpd(seig) group_description_index={} description_index={} fragment_local={}",
group_description_index, description_index, fragment_local
),
};

println!(
"sample {} source={} protected={} iv_len={} aux_size={}",
sample.sample_index,
source,
sample.is_protected,
sample.effective_initialization_vector().len(),
sample.auxiliary_info_size
);
}

Ok(())
}
Loading
Loading