use std::path::PathBuf;
use super::FfmpegInput;
use crate::report::RenderReport;
use crate::utils;
#[allow(clippy::too_many_arguments)]
pub fn render_video(
input_pattern: &str,
output: &str,
fps: u32,
format: &str,
bitrate: Option<&str>,
crf: Option<u32>,
fade_filter: Option<&str>,
background: Option<&str>,
verbose_ffmpeg: bool,
on_frame: &mut dyn FnMut(u32),
) -> Result<RenderReport, String> {
render_video_from(
FfmpegInput::Sequence(input_pattern),
output,
fps,
format,
bitrate,
crf,
fade_filter,
background,
verbose_ffmpeg,
on_frame,
)
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn render_video_from(
input: FfmpegInput,
output: &str,
fps: u32,
format: &str,
bitrate: Option<&str>,
crf: Option<u32>,
fade_filter: Option<&str>,
background: Option<&str>,
verbose_ffmpeg: bool,
on_frame: &mut dyn FnMut(u32),
) -> Result<RenderReport, String> {
if !matches!(format, "webm" | "mp4" | "mov") {
return Err(format!(
"❌ Unsupported format: {}. Use 'webm', 'mp4' or 'mov'.",
format
));
}
let mut args = utils::ffmpeg_base_args(verbose_ffmpeg);
let (input_args, stdin) = input.into_args(fps);
args.extend(input_args);
let background = background.or((format == "mp4").then_some("black"));
let opaque = background.is_some();
let mut filters: Vec<String> = Vec::new();
if let Some(f) = fade_filter.filter(|f| !f.is_empty()) {
filters.push(f.to_string());
}
if let Some(color) = background {
filters.push(utils::background_filter(color));
}
let s = |v: &[&str]| v.iter().map(|x| x.to_string()).collect::<Vec<String>>();
match format {
"webm" => {
args.extend(s(&[
"-c:v",
"libvpx-vp9",
"-pix_fmt",
if opaque { "yuv420p" } else { "yuva420p" },
"-auto-alt-ref",
"0",
"-row-mt",
"1",
"-deadline",
"good",
"-cpu-used",
"2",
]));
match (crf, bitrate) {
(Some(c), _) => args.extend(s(&["-crf", &c.to_string(), "-b:v", "0"])),
(None, Some(b)) => args.extend(s(&["-b:v", b])),
(None, None) => args.extend(s(&["-crf", "30", "-b:v", "0"])),
}
}
"mp4" => {
filters.push(format!(
"pad=ceil(iw/2)*2:ceil(ih/2)*2:color={}",
background.unwrap_or("black")
));
args.extend(s(&[
"-c:v",
"libx264",
"-pix_fmt",
"yuv420p",
"-preset",
"medium",
"-movflags",
"+faststart",
]));
match (crf, bitrate) {
(Some(c), _) => args.extend(s(&["-crf", &c.to_string()])),
(None, Some(b)) => args.extend(s(&["-b:v", b])),
(None, None) => args.extend(s(&["-crf", "23"])),
}
}
_ => {
args.extend(s(&[
"-c:v",
"prores_ks",
"-profile:v",
"4444",
"-pix_fmt",
if opaque {
"yuv444p10le"
} else {
"yuva444p10le"
},
"-vendor",
"apl0",
]));
}
}
let filter_chain = filters.join(",");
if !filter_chain.is_empty() {
args.extend(["-vf".into(), filter_chain]);
}
args.extend(["-y".into(), output.to_string()]);
let video_stderr =
utils::run_ffmpeg_with_input(&args, output, verbose_ffmpeg, on_frame, stdin)?;
let warnings = utils::scan_ffmpeg_stderr(&video_stderr);
Ok(RenderReport {
output_path: PathBuf::from(output),
frames_rendered: None,
ffmpeg_warnings: warnings,
preview: false,
notes: Some("Video render complete.".into()),
})
}