use std::{path::PathBuf, process};
use ff_encode::VideoEncoder;
use ff_filter::{
AnimatedValue, AudioTrack, LayerSource, MultiTrackAudioMixer, MultiTrackComposer, VideoLayer,
};
use ff_format::{AudioCodec, ChannelLayout, VideoCodec};
struct Args {
base: PathBuf,
overlay: PathBuf,
output: PathBuf,
audio_a: Option<PathBuf>,
audio_b: Option<PathBuf>,
width: u32,
height: u32,
fps: f64,
}
fn parse_args() -> Args {
let args: Vec<String> = std::env::args().skip(1).collect();
let get = |flag: &str| -> Option<String> {
args.windows(2).find(|w| w[0] == flag).map(|w| w[1].clone())
};
let base = if let Some(p) = get("--base") {
PathBuf::from(p)
} else {
eprintln!("error: --base <path> is required");
process::exit(1);
};
let overlay = if let Some(p) = get("--overlay") {
PathBuf::from(p)
} else {
eprintln!("error: --overlay <path> is required");
process::exit(1);
};
let output = if let Some(p) = get("--output") {
PathBuf::from(p)
} else {
eprintln!("error: --output <path> is required");
process::exit(1);
};
Args {
base,
overlay,
output,
audio_a: get("--audio-a").map(PathBuf::from),
audio_b: get("--audio-b").map(PathBuf::from),
width: get("--width").and_then(|v| v.parse().ok()).unwrap_or(1280),
height: get("--height").and_then(|v| v.parse().ok()).unwrap_or(720),
fps: get("--fps").and_then(|v| v.parse().ok()).unwrap_or(30.0),
}
}
fn main() {
let args = parse_args();
let overlay_x = args.width / 2;
let mut video_graph = match MultiTrackComposer::new(args.width, args.height)
.add_layer(VideoLayer {
source: LayerSource::File(args.base.clone()),
proxy: None,
x: AnimatedValue::Static(0.0),
y: AnimatedValue::Static(0.0),
scale_x: AnimatedValue::Static(1.0),
scale_y: AnimatedValue::Static(1.0),
rotation: AnimatedValue::Static(0.0),
opacity: AnimatedValue::Static(1.0),
blend_mode: ff_filter::BlendMode::Normal,
composite_op: ff_filter::CompositeOp::Over,
effects: vec![],
})
.add_layer(VideoLayer {
source: LayerSource::File(args.overlay.clone()),
proxy: None,
x: AnimatedValue::Static(f64::from(overlay_x)),
y: AnimatedValue::Static(0.0),
scale_x: AnimatedValue::Static(0.5),
scale_y: AnimatedValue::Static(0.5),
rotation: AnimatedValue::Static(0.0),
opacity: AnimatedValue::Static(0.85),
blend_mode: ff_filter::BlendMode::Normal,
composite_op: ff_filter::CompositeOp::Over,
effects: vec![],
})
.build()
{
Ok(g) => g,
Err(e) => {
eprintln!("error: failed to build video composition graph: {e}");
process::exit(1);
}
};
let has_audio = args.audio_a.is_some() && args.audio_b.is_some();
let mut audio_graph = if let (Some(audio_a), Some(audio_b)) = (&args.audio_a, &args.audio_b) {
match MultiTrackAudioMixer::new(48_000, ChannelLayout::Stereo)
.add_track(AudioTrack {
source: audio_a.clone(),
volume: AnimatedValue::Static(0.0),
pan: AnimatedValue::Static(-0.2), effects: vec![],
sample_rate: 48_000,
channel_layout: ChannelLayout::Stereo,
})
.add_track(AudioTrack {
source: audio_b.clone(),
volume: AnimatedValue::Static(0.0),
pan: AnimatedValue::Static(0.2), effects: vec![],
sample_rate: 48_000,
channel_layout: ChannelLayout::Stereo,
})
.build()
{
Ok(g) => Some(g),
Err(e) => {
eprintln!("error: failed to build audio mix graph: {e}");
process::exit(1);
}
}
} else {
None
};
let mut enc_builder = VideoEncoder::create(&args.output)
.video(args.width, args.height, args.fps)
.video_codec(VideoCodec::H264);
if has_audio {
enc_builder = enc_builder
.audio(48_000, 2)
.audio_codec(AudioCodec::Aac)
.audio_bitrate(192_000);
}
let mut encoder = match enc_builder.build() {
Ok(e) => e,
Err(e) => {
eprintln!("error: failed to create encoder: {e}");
process::exit(1);
}
};
loop {
match video_graph.pull_video() {
Ok(Some(frame)) => {
if let Err(e) = encoder.push_video(&frame) {
eprintln!("error: push_video failed: {e}");
process::exit(1);
}
}
Ok(None) => break,
Err(e) => {
eprintln!("error: pull_video failed: {e}");
process::exit(1);
}
}
}
if let Some(ref mut agraph) = audio_graph {
loop {
match agraph.pull_audio() {
Ok(Some(frame)) => {
if let Err(e) = encoder.push_audio(&frame) {
eprintln!("error: push_audio failed: {e}");
process::exit(1);
}
}
Ok(None) => break,
Err(e) => {
eprintln!("error: pull_audio failed: {e}");
process::exit(1);
}
}
}
}
if let Err(e) = encoder.finish() {
eprintln!("error: encoder.finish() failed: {e}");
process::exit(1);
}
println!(
"composed: {} → {} ({}x{} @ {:.0}fps{})",
args.base.display(),
args.output.display(),
args.width,
args.height,
args.fps,
if has_audio { " + audio mix" } else { "" },
);
}