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
16 changes: 16 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@ description = "Minimal MP4/ISOBMFF parser with JSON output, box decoding, and UU
license = "MIT"
repository = "https://github.com/alfg/mp4box"

[features]
default = ["edit"]
# Non-destructive box editing (Editor, mp4edit). No extra dependencies;
# opt out with default-features = false for a parse-only build.
edit = []

[lib]
name = "mp4box"
path = "src/lib.rs"
Expand All @@ -22,6 +28,11 @@ path = "src/bin/mp4info.rs"
name = "mp4samples"
path = "src/bin/mp4samples.rs"

[[bin]]
name = "mp4edit"
path = "src/bin/mp4edit.rs"
required-features = ["edit"]

[[example]]
name = "simple"
path = "examples/simple.rs"
Expand All @@ -30,6 +41,11 @@ path = "examples/simple.rs"
name = "boxes"
path = "examples/boxes.rs"

[[example]]
name = "edit"
path = "examples/edit.rs"
required-features = ["edit"]

[dependencies]
anyhow = "1.0"
byteorder = "1.5"
Expand Down
35 changes: 35 additions & 0 deletions examples/edit.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
use mp4box::edit::Editor;
use std::env;

// This example demonstrates the edit API: it copies an MP4 while setting
// iTunes tags (creating moov/udta/meta/ilst if needed) and zeroing the
// movie timestamps. Box sizes and stco/co64 chunk offsets are fixed up
// automatically; the input file is never modified.
//
// Requires the "edit" feature (enabled by default):
// cargo run --example edit -- in.mp4 out.mp4
fn main() -> anyhow::Result<()> {
let args: Vec<String> = env::args().collect();
if args.len() != 3 {
eprintln!("Usage: {} <input.mp4> <output.mp4>", args[0]);
std::process::exit(1);
}

let mut editor = Editor::new();
editor.set_tag("title", "Edited with mp4box")?;
editor.set_tag("encoder", "mp4box edit example")?;
editor.set_field("moov/mvhd", "creation_time", "0");
editor.set_field("moov/mvhd", "modification_time", "0");

let stats = editor.process_file(&args[1], &args[2])?;

println!("wrote {} bytes to {}", stats.bytes_written, args[2]);
println!("chunk offsets adjusted: {}", stats.chunk_offsets_adjusted);
if stats.chunk_offsets_unmapped > 0 {
eprintln!(
"warning: {} chunk offsets point at removed data",
stats.chunk_offsets_unmapped
);
}
Ok(())
}
36 changes: 36 additions & 0 deletions src/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,42 @@ pub fn get_boxes<R: Read + Seek>(r: &mut R, size: u64, decode: bool) -> anyhow::

/// Like [`get_boxes`], but decodes with a caller-supplied [`Registry`]
/// instead of the default one.
///
/// To *extend* the default registry with custom decoders (rather than
/// replace it), build on [`default_registry`]:
///
/// ```no_run
/// use mp4box::{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: &mp4box::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!("{} payload 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, &reg)?;
/// # Ok::<(), anyhow::Error>(())
/// ```
pub fn get_boxes_with_registry<R: Read + Seek>(
r: &mut R,
size: u64,
Expand Down
128 changes: 128 additions & 0 deletions src/bin/mp4edit.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
use clap::Parser;
use mp4box::edit::{Command, Editor};

/// mp4edit — non-destructive MP4/ISOBMFF box editor.
///
/// Commands apply in order to a single parsed tree; the output is written as
/// a new file with all box sizes and chunk offsets fixed up automatically.
/// Box paths are slash-delimited fourccs with optional indices for repeated
/// boxes, e.g. `moov/trak[1]/mdia/mdhd`.
#[derive(Parser, Debug)]
#[command(version, about = "Non-destructive MP4/ISOBMFF box editor")]
struct Args {
/// Remove a box: --remove moov/udta
#[arg(long = "remove", value_name = "PATH")]
remove: Vec<String>,

/// Remove every box with this fourcc, anywhere: --remove-all free
#[arg(long = "remove-all", value_name = "FOURCC")]
remove_all: Vec<String>,

/// Insert a raw box file (header + payload) as a child.
/// Format: PARENT_PATH:FILE[:POSITION]
#[arg(long = "insert", value_name = "PATH:FILE[:POS]")]
insert: Vec<String>,

/// Replace a box wholesale with a raw box file. Format: PATH:FILE
#[arg(long = "replace", value_name = "PATH:FILE")]
replace: Vec<String>,

/// Set a field of a known box (mvhd/tkhd/mdhd) in place.
/// Format: PATH.FIELD=VALUE, e.g. --set moov/mvhd.creation_time=0
#[arg(long = "set", value_name = "PATH.FIELD=VALUE")]
set: Vec<String>,

/// Set an iTunes tag (creates moov/udta/meta/ilst as needed).
/// Format: NAME=VALUE with a friendly name (title, artist, album, ...)
/// or a raw fourcc, e.g. --tag title="My Movie"
#[arg(long = "tag", value_name = "NAME=VALUE")]
tag: Vec<String>,

/// Input MP4/ISOBMFF file (never modified)
input: String,

/// Output file path
output: String,
}

fn main() -> anyhow::Result<()> {
let args = Args::parse();
let mut editor = Editor::new();

for path in &args.remove {
editor.add_command(Command::Remove { path: path.clone() });
}

for fourcc in &args.remove_all {
editor.add_command(Command::RemoveAll {
fourcc: fourcc.clone(),
});
}

for spec in &args.insert {
let parts: Vec<&str> = spec.splitn(3, ':').collect();
anyhow::ensure!(
parts.len() >= 2,
"--insert format is PATH:FILE or PATH:FILE:POSITION, got '{}'",
spec
);
let position =
match parts.get(2) {
Some(p) => Some(p.parse::<usize>().map_err(|_| {
anyhow::anyhow!("--insert position must be a number, got '{}'", p)
})?),
None => None,
};
editor.add_command(Command::Insert {
parent: parts[0].to_string(),
bytes: std::fs::read(parts[1])?,
position,
});
}

for spec in &args.replace {
let (path, file) = spec
.split_once(':')
.ok_or_else(|| anyhow::anyhow!("--replace format is PATH:FILE, got '{}'", spec))?;
editor.add_command(Command::Replace {
path: path.to_string(),
bytes: std::fs::read(file)?,
});
}

for spec in &args.set {
// PATH.FIELD=VALUE — the field name never contains '.' or '='.
let (lhs, value) = spec
.split_once('=')
.ok_or_else(|| anyhow::anyhow!("--set format is PATH.FIELD=VALUE, got '{}'", spec))?;
let (path, field) = lhs
.rsplit_once('.')
.ok_or_else(|| anyhow::anyhow!("--set format is PATH.FIELD=VALUE, got '{}'", spec))?;
editor.add_command(Command::Set {
path: path.to_string(),
field: field.to_string(),
value: value.to_string(),
});
}

for spec in &args.tag {
let (name, value) = spec
.split_once('=')
.ok_or_else(|| anyhow::anyhow!("--tag format is NAME=VALUE, got '{}'", spec))?;
editor.set_tag(name, value)?;
}

let stats = editor.process_file(&args.input, &args.output)?;
println!(
"wrote {} ({} bytes, {} chunk offsets adjusted{})",
args.output,
stats.bytes_written,
stats.chunk_offsets_adjusted,
if stats.chunk_offsets_unmapped > 0 {
format!(", {} unmapped!", stats.chunk_offsets_unmapped)
} else {
String::new()
}
);
Ok(())
}
Loading
Loading