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.7.0"
placeholder: "0.8.0"
validations:
required: true

Expand Down
36 changes: 36 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,39 @@
# 0.8.0 (May 26, 2026)

- Added the feature-gated mux release surface, including `MuxRequest`, path-first
`MuxTrackSpec` parsing with selector suffixes, flat and fragmented output layout selection,
segment or fragment duration modes, sync `mux_to_path`, `mux_into_path`, and
`mux_fragmented_to_paths` helpers, Tokio async companions behind `mux` plus `async`, and the
sync-only `mux` CLI route
- Added real MP4 mux writing for both flat and fragmented layouts on one shared mux event graph,
with deterministic payload planning, chunk interleaving, duration-based chunking, sync-aligned
fragment or segment boundaries, split init/media fragmented outputs, destination update or
create-new behavior, preserved flat destination metadata where supported, and explicit
validation for unsupported duration-mode or fragmented multi-video requests
- Added broad mux import coverage for MP4 track import with retained sample-entry bytes, selected
AVI, MPEG-PS, MPEG-TS, DASH, NHML, NHNT, SAF, and VobSub-style inputs, plus demux-backed
elementary or containerized audio, video, text, subtitle, image, raw-video, and PCM families
across the public path-first request shape
- Added `mp4forge::mux::inspect`, `mp4forge::mux::sample_reader`, and
`mp4forge::mux::rewrite` helpers for direct-ingest reporting, packet-focused exports,
one-sample-at-a-time seekable or progressive reading, and elementary sample rewrite/export
flows for AVC, HEVC, VVC, AV1, AAC ADTS, and MHAS data
- Expanded typed box and sample-entry coverage needed by the mux, probe, divide, and rewrite
paths, including additional DTS, Dolby, IAMF, FLAC, MPEG-H, PCM, uncompressed-visual,
codec-configuration, timing, and metadata structures
- Broadened `divide` and probe behavior for more video and audio sample-entry families while
keeping the existing validation-first workflow for unsupported fragmented output shapes
- Reworked file-backed decrypt rewrite execution to use streaming root rewrite plans for the
supported sync and async surfaces, refreshed affected top-level `sidx` data after clear-output
rewrites, and kept progress-enabled decrypt surfaces byte-aligned with their non-progress
companions
- Added mux-focused examples, expanded retained fixture coverage, moved test output helpers onto
auto-cleaned temporary paths, and added fuzz targets for mux demuxing, sample rewrites, async
API parity, decrypt byte-surface parity, extra box roundtrips, bit I/O boundaries, and `sidx`
plan application
- Removed the deprecated `dump` command `-mdat` and `-free` shorthand options; use
`-full mdat` or `-full free,skip` explicitly instead

# 0.7.0 (April 28, 2026)

- Added the feature-gated decryption release surface across sync library helpers, Tokio async
Expand Down
7 changes: 5 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
[package]
name = "mp4forge"
version = "0.7.0"
version = "0.8.0"
edition = "2024"
rust-version = "1.88"
authors = ["bakgio"]
license = "MIT OR Apache-2.0"
description = "Rust library and CLI for inspecting, probing, extracting, and rewriting MP4 box structures"
description = "Rust library and CLI for inspecting, probing, extracting, muxing, and rewriting MP4 structures"
repository = "https://github.com/bakgio/mp4forge"
readme = "README.md"
keywords = ["mp4", "isobmff", "parser", "video", "cli"]
Expand All @@ -20,14 +20,17 @@ rustdoc-args = ["--cfg", "docsrs"]
default = []
async = ["dep:tokio"]
decrypt = ["dep:aes"]
mux = []
serde = ["dep:serde"]

[dependencies]
aes = { version = "0.8", optional = true }
miniz_oxide = "0.8"
serde = { version = "1", features = ["derive"], optional = true }
terminal_size = "0.4"
tokio = { version = "1.52.1", features = ["fs", "io-util", "rt", "rt-multi-thread", "macros"], optional = true }

[dev-dependencies]
aes = "0.8"
serde_json = "1"
tempfile = "3"
64 changes: 52 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<p align="center">
<h1 align="center">mp4forge</h1>
<p align="center">
Rust library and CLI for inspecting, extracting, probing, and rewriting MP4 box structures.
Rust library and CLI for inspecting, extracting, probing, muxing, and rewriting MP4 structures.
</p>
<p align="center">
<a href="https://crates.io/crates/mp4forge"><img src="https://img.shields.io/crates/v/mp4forge.svg" alt="Crates.io"></a>
Expand All @@ -21,20 +21,22 @@
- 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
- Feature-gated decryption APIs and a sync-only `decrypt` CLI for the supported protected MP4 families
- Built-in CLI for `decrypt`, `dump`, `extract`, `probe`, `psshdump`, `edit`, and `divide`
- Built-in CLI for `decrypt`, `divide`, `dump`, `edit`, `extract`, `mux`, `probe`, and `psshdump`
- 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.7.0"
mp4forge = "0.8.0"

# With optional features:
# mp4forge = { version = "0.7.0", features = ["async"] }
# mp4forge = { version = "0.7.0", features = ["decrypt"] }
# mp4forge = { version = "0.7.0", features = ["decrypt", "async"] }
# mp4forge = { version = "0.7.0", features = ["serde"] }
# mp4forge = { version = "0.8.0", features = ["async"] }
# mp4forge = { version = "0.8.0", features = ["decrypt"] }
# mp4forge = { version = "0.8.0", features = ["decrypt", "async"] }
# mp4forge = { version = "0.8.0", features = ["mux"] }
# mp4forge = { version = "0.8.0", features = ["mux", "async"] }
# mp4forge = { version = "0.8.0", features = ["serde"] }
```

Install the CLI from crates.io:
Expand Down Expand Up @@ -68,6 +70,14 @@ feature flags:
IPMP ACBC and ACGK OD-track movies, and the retained IAEC protected-movie path. When combined
with `async`, it also enables the additive file-backed Tokio async decrypt companions, while the
CLI remains on the synchronous path.
- `mux`: enables the additive mux task surface and the retained low-level helpers underneath it.
The library path covers the narrow public `MuxRequest` model with repeated track specs plus
optional `segment_duration` or `fragment_duration`, real `ftyp`/`moov`/`mdat` writing for sync
callers, additive async real-container writing when combined with `async`, internal chunk and
duration coordination on one mux event graph, the retained low-level seekable and progressive
payload assembly helpers, and one-sample-at-a-time seekable or progressive readers. It also
enables the sync-only `mux` CLI route for one output MP4 built from repeated
path-first `--track` inputs.
- `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
Expand All @@ -85,6 +95,7 @@ COMMAND:
dump display the MP4 box tree
edit rewrite selected boxes
extract extract raw boxes by type or path
mux merge one video track plus audio, text, and subtitle tracks into one MP4
psshdump summarize pssh boxes
probe summarize an MP4 file
```
Expand All @@ -94,9 +105,37 @@ sync-only, accepts repeated `--key ID:KEY`, optional `--fragments-info FILE`, an
`--show-progress`, and reuses the same library decryption surface that backs the feature-gated
sync and async APIs.

`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.
`mux` is available when the crate is built with `--features mux`. The CLI route stays sync-only
and accepts repeated `--track` inputs, one required positional output path, and at most one of
`--segment_duration` or `--fragment_duration`. The current public `--track` grammar is path-first:
`PATH` imports one raw source or every supported track from one MP4 source, while
`PATH#video`, `PATH#audio`, `PATH#audio:N`, `PATH#text`, `PATH#text:N`, and `PATH#track:ID`
select one specific track from a containerized source. The landed path-only auto-detection
currently covers MP4, supported AVI audio streams plus H.263/JPEG/PNG/MPEG-4 Part 2/H.264/AVC1 video streams, supported
MPEG-PS MPEG audio streams plus LPCM audio plus MPEG-4 Part 2/H.264/H.265/VVC video streams, supported MPEG-TS
MPEG audio streams plus AAC LATM/MHAS plus AC-3/E-AC-3/AC-4/DTS/TrueHD audio plus MPEG-2/AV1/AVS3/MPEG-4 Part 2/H.264/H.265/VVC video streams, AAC
ADTS, MP3, AC-3, E-AC-3, AC-4, AMR, AMR-WB, QCP voice audio, DTS core audio, AAC LATM, Dolby
TrueHD, leading-sync MHAS MPEG-H, IAMF, H.263 elementary video, MPEG-2 elementary video, MPEG-4 Part 2 elementary video,
H.264 Annex B, H.265 Annex B, VVC Annex B, raw AV1 OBU, raw AV1 Annex B, IVF-backed AV1, IVF-backed VP8, IVF-backed VP9,
JPEG still images, PNG still images, WAVE/AIFF/AIFC PCM, native FLAC, Ogg-backed FLAC,
Ogg-backed Opus, Ogg-backed Vorbis, Ogg-backed Speex, Ogg-backed Theora, and CAF-backed ALAC.
Broader DTS-family
sample-entry variants remain supported through MP4 track import, and the broader demux-backed
path-only families continue to move over behind the same public shape.
MP4-track merges continue to cover the broader registered sample-entry families because they
preserve encoded sample-entry bytes from the source file, and mixed video/audio/text/subtitle jobs
retain imported handler names and languages on the real MP4 path. The matching sync and async
library entry points use the same `MuxRequest` surface, while the retained lower-level mux helpers
remain available separately when you need staged planning or payload-copy behavior without the
task-level request layer. The public `mp4forge::mux::sample_reader` helpers can also expose stable
text or subtitle track identity when you construct them with companion `MuxTrackConfig` values.

`divide` currently targets fragmented inputs with up to one video track from AVC, HEVC, Dolby
Vision on HEVC, AV1, VP8, or VP9 and one audio track from MP4A-based audio, Opus, AC-3,
E-AC-3, AC-4, ALAC, DTS-family entries, FLAC, IAMF, MPEG-H, or PCM, including encrypted wrappers
that preserve those original sample-entry formats. Subtitle and text tracks remain unsupported in
the current divide output model. 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
Expand Down Expand Up @@ -124,8 +163,9 @@ per-chunk, bitrate, and IDR aggregation, or use `mp4forge::probe::ProbeOptions`
when you need the same control programmatically.

> See the [`examples/`](./examples) directory for the crate's low-level and high-level API usage
> patterns, including the feature-gated decrypt example and the Tokio-based async library example
> behind the optional `async` feature.
> patterns, including the feature-gated decrypt example, the feature-gated real-mux and
> mux/sample-reader examples, and the Tokio-based async library example behind the optional
> `async` feature.

## License

Expand Down
29 changes: 29 additions & 0 deletions examples/inspect_mux_boundaries.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
#[cfg(feature = "mux")]
fn main() {
use mp4forge::mux::{MuxInterleavePolicy, MuxStagedMediaItem, plan_staged_media_items};

let plan = plan_staged_media_items(
vec![
MuxStagedMediaItem::new(0, 1, 0, 1024, 4096, 2048).with_sync_sample(true),
MuxStagedMediaItem::new(1, 2, 512, 512, 2048, 1024),
],
MuxInterleavePolicy::DecodeTime,
)
.unwrap();

for item in plan.planned_items() {
println!(
"track {} decode [{}..{}) output [{}..{})",
item.staged().track_id(),
item.staged().decode_time(),
item.decode_end_time(),
item.output_offset(),
item.output_end_offset()
);
}
}

#[cfg(not(feature = "mux"))]
fn main() {
eprintln!("enable the `mux` feature to run this example");
}
38 changes: 38 additions & 0 deletions examples/mux_fragment_duration.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
#[cfg(feature = "mux")]
#[path = "support/mux_example_support.rs"]
mod mux_example_support;

#[cfg(feature = "mux")]
use std::error::Error;

#[cfg(feature = "mux")]
use mp4forge::mux::{
MuxDurationMode, MuxMp4TrackSelector, MuxOutputLayout, MuxRequest, MuxTrackSpec, mux_to_path,
};

#[cfg(feature = "mux")]
fn main() -> Result<(), Box<dyn Error>> {
let audio_input = mux_example_support::build_audio_input_file(
"example-fragment-audio",
mux_example_support::fourcc("dash"),
"mp4a",
&[b"one", b"two", b"three"],
);
let output_path = mux_example_support::write_temp_file("example-fragment-output", "mp4", &[]);

let request = MuxRequest::new(vec![MuxTrackSpec::mp4(
&audio_input,
MuxMp4TrackSelector::Audio { occurrence: 1 },
)])
.with_output_layout(MuxOutputLayout::Fragmented)
.with_duration_mode(MuxDurationMode::Fragment { seconds: 0.05 });

mux_to_path(&request, &output_path)?;
println!("wrote {}", output_path.display());
Ok(())
}

#[cfg(not(feature = "mux"))]
fn main() {
eprintln!("Enable the `mux` feature to run this example.");
}
38 changes: 38 additions & 0 deletions examples/mux_raw_tracks.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
#[cfg(feature = "mux")]
#[path = "support/mux_example_support.rs"]
mod mux_example_support;

#[cfg(feature = "mux")]
use std::error::Error;

#[cfg(feature = "mux")]
use mp4forge::mux::{MuxRequest, MuxTrackSpec, mux_to_path};

#[cfg(feature = "mux")]
fn main() -> Result<(), Box<dyn Error>> {
let audio_input = mux_example_support::write_test_flac_file("example-raw-audio", b"flac-frame");
let first_video_frame = mux_example_support::build_test_av1_sequence_header_obu(640, 360);
let second_video_frame = mux_example_support::build_test_av1_sequence_header_obu(640, 360);
let video_input = mux_example_support::write_test_av1_ivf_file(
"example-raw-video",
640,
360,
&[0, 1],
&[&first_video_frame, &second_video_frame],
);
let output_path = mux_example_support::write_temp_file("example-raw-output", "mp4", &[]);

let request = MuxRequest::new(vec![
MuxTrackSpec::path(&audio_input),
MuxTrackSpec::path(&video_input),
]);

mux_to_path(&request, &output_path)?;
println!("wrote {}", output_path.display());
Ok(())
}

#[cfg(not(feature = "mux"))]
fn main() {
eprintln!("Enable the `mux` feature to run this example.");
}
38 changes: 38 additions & 0 deletions examples/mux_segment_duration.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
#[cfg(feature = "mux")]
#[path = "support/mux_example_support.rs"]
mod mux_example_support;

#[cfg(feature = "mux")]
use std::error::Error;

#[cfg(feature = "mux")]
use mp4forge::mux::{
MuxDurationMode, MuxMp4TrackSelector, MuxOutputLayout, MuxRequest, MuxTrackSpec, mux_to_path,
};

#[cfg(feature = "mux")]
fn main() -> Result<(), Box<dyn Error>> {
let audio_input = mux_example_support::build_audio_input_file(
"example-segment-audio",
mux_example_support::fourcc("dash"),
"mp4a",
&[b"one", b"two", b"three"],
);
let output_path = mux_example_support::write_temp_file("example-segment-output", "mp4", &[]);

let request = MuxRequest::new(vec![MuxTrackSpec::mp4(
&audio_input,
MuxMp4TrackSelector::Audio { occurrence: 1 },
)])
.with_output_layout(MuxOutputLayout::Fragmented)
.with_duration_mode(MuxDurationMode::Segment { seconds: 0.05 });

mux_to_path(&request, &output_path)?;
println!("wrote {}", output_path.display());
Ok(())
}

#[cfg(not(feature = "mux"))]
fn main() {
eprintln!("Enable the `mux` feature to run this example.");
}
48 changes: 48 additions & 0 deletions examples/mux_subtitle_tracks.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
#[cfg(feature = "mux")]
#[path = "support/mux_example_support.rs"]
mod mux_example_support;

#[cfg(feature = "mux")]
use std::error::Error;

#[cfg(feature = "mux")]
use mp4forge::mux::{MuxMp4TrackSelector, MuxRequest, MuxTrackSpec, mux_to_path};

#[cfg(feature = "mux")]
fn main() -> Result<(), Box<dyn Error>> {
let video_input = mux_example_support::build_video_input_file(
"example-subtitle-video",
mux_example_support::fourcc("isom"),
"avc1",
&[b"video"],
);
let audio_input = mux_example_support::build_audio_input_file_with_timing(
"example-subtitle-audio",
mux_example_support::fourcc("dash"),
"mp4a",
1_000,
1_000,
&[b"aud"],
);
let text_input = mux_example_support::build_text_input_file(
"example-subtitle-text",
mux_example_support::fourcc("mp42"),
);
let output_path = mux_example_support::write_temp_file("example-subtitle-output", "mp4", &[]);

let request = MuxRequest::new(vec![
MuxTrackSpec::mp4(&video_input, MuxMp4TrackSelector::Video),
MuxTrackSpec::mp4(&audio_input, MuxMp4TrackSelector::Audio { occurrence: 1 }),
MuxTrackSpec::mp4(&text_input, MuxMp4TrackSelector::Text { occurrence: 1 }),
MuxTrackSpec::mp4(&text_input, MuxMp4TrackSelector::Text { occurrence: 2 }),
]);

mux_to_path(&request, &output_path)?;
println!("wrote {}", output_path.display());
Ok(())
}

#[cfg(not(feature = "mux"))]
fn main() {
eprintln!("Enable the `mux` feature to run this example.");
}
Loading
Loading