use std::ffi::OsString;
use std::path::{Path, PathBuf};
use crate::command::{FfmpegBinaryPaths, FfmpegCommand};
use crate::config::FfmpegLocator;
use crate::error::{Error, Result};
use crate::filter::{AudioFilter, VideoFilter};
#[derive(Debug, Default)]
pub struct TranscodeBuilder {
binaries: Option<FfmpegBinaryPaths>,
input: Option<PathBuf>,
output: Option<PathBuf>,
video_codec: Option<String>,
audio_codec: Option<String>,
video_bitrate: Option<u32>,
audio_bitrate: Option<u32>,
frame_rate: Option<f64>,
preset: Option<String>,
video_filters: Vec<VideoFilter>,
audio_filters: Vec<AudioFilter>,
extra_args: Vec<OsString>,
overwrite: bool,
}
impl TranscodeBuilder {
pub fn new() -> Self {
Self {
overwrite: true,
..Self::default()
}
}
pub fn with_binaries(mut self, binaries: &FfmpegBinaryPaths) -> Self {
self.binaries = Some(binaries.clone());
self
}
pub fn with_locator(mut self, locator: &FfmpegLocator) -> Self {
self.binaries = Some(locator.binaries().clone());
self
}
pub fn input<P: AsRef<Path>>(mut self, path: P) -> Self {
self.input = Some(path.as_ref().to_path_buf());
self
}
pub fn output<P: AsRef<Path>>(mut self, path: P) -> Self {
self.output = Some(path.as_ref().to_path_buf());
self
}
pub fn video_codec(mut self, codec: impl Into<String>) -> Self {
self.video_codec = Some(codec.into());
self
}
pub fn audio_codec(mut self, codec: impl Into<String>) -> Self {
self.audio_codec = Some(codec.into());
self
}
pub fn video_bitrate(mut self, kbps: u32) -> Self {
self.video_bitrate = Some(kbps);
self
}
pub fn audio_bitrate(mut self, kbps: u32) -> Self {
self.audio_bitrate = Some(kbps);
self
}
pub fn frame_rate(mut self, fps: f64) -> Self {
self.frame_rate = Some(fps);
self
}
pub fn preset(mut self, preset: impl Into<String>) -> Self {
self.preset = Some(preset.into());
self
}
pub fn size(self, width: u32, height: u32) -> Self {
self.add_video_filter(VideoFilter::Scale { width, height })
}
pub fn add_video_filter(mut self, filter: VideoFilter) -> Self {
self.video_filters.push(filter);
self
}
pub fn add_audio_filter(mut self, filter: AudioFilter) -> Self {
self.audio_filters.push(filter);
self
}
#[deprecated(since = "0.2.0", note = "use add_video_filter() instead")]
pub fn add_filter(self, filter: VideoFilter) -> Self {
self.add_video_filter(filter)
}
pub fn extra_arg(mut self, arg: impl Into<OsString>) -> Self {
self.extra_args.push(arg.into());
self
}
pub fn overwrite(mut self, enabled: bool) -> Self {
self.overwrite = enabled;
self
}
pub fn input_path(&self) -> Option<&Path> {
self.input.as_deref()
}
pub fn output_path(&self) -> Option<&Path> {
self.output.as_deref()
}
pub fn video_codec_ref(&self) -> Option<&str> {
self.video_codec.as_deref()
}
pub fn audio_codec_ref(&self) -> Option<&str> {
self.audio_codec.as_deref()
}
pub fn video_bitrate_value(&self) -> Option<u32> {
self.video_bitrate
}
pub fn audio_bitrate_value(&self) -> Option<u32> {
self.audio_bitrate
}
pub fn frame_rate_value(&self) -> Option<f64> {
self.frame_rate
}
pub fn preset_value(&self) -> Option<&str> {
self.preset.as_deref()
}
pub fn overwrite_enabled(&self) -> bool {
self.overwrite
}
pub fn video_filters(&self) -> &[VideoFilter] {
&self.video_filters
}
pub fn audio_filters(&self) -> &[AudioFilter] {
&self.audio_filters
}
fn resolve_binaries(binaries: Option<FfmpegBinaryPaths>) -> Result<FfmpegBinaryPaths> {
if let Some(paths) = binaries {
return Ok(paths);
}
Ok(FfmpegLocator::system()?.binaries().clone())
}
fn validate(self) -> Result<ValidatedTranscode> {
let Self {
binaries,
input,
output,
video_codec,
audio_codec,
video_bitrate,
audio_bitrate,
frame_rate,
preset,
video_filters,
audio_filters,
extra_args,
overwrite,
} = self;
let input = input.ok_or_else(|| Error::InvalidInput("input path is required".into()))?;
let output = output.ok_or_else(|| Error::InvalidInput("output path is required".into()))?;
Ok(ValidatedTranscode {
binaries: Self::resolve_binaries(binaries)?,
input,
output,
video_codec,
audio_codec,
video_bitrate,
audio_bitrate,
frame_rate,
preset,
video_filters,
audio_filters,
extra_args,
overwrite,
})
}
pub fn run(self) -> Result<()> {
let validated = self.validate()?;
validated.run()
}
}
struct ValidatedTranscode {
binaries: FfmpegBinaryPaths,
input: PathBuf,
output: PathBuf,
video_codec: Option<String>,
audio_codec: Option<String>,
video_bitrate: Option<u32>,
audio_bitrate: Option<u32>,
frame_rate: Option<f64>,
preset: Option<String>,
video_filters: Vec<VideoFilter>,
audio_filters: Vec<AudioFilter>,
extra_args: Vec<OsString>,
overwrite: bool,
}
impl ValidatedTranscode {
fn run(self) -> Result<()> {
let mut cmd = FfmpegCommand::new(self.binaries.ffmpeg());
cmd.arg(if self.overwrite { "-y" } else { "-n" });
cmd.arg("-i").arg(&self.input);
if let Some(codec) = self.video_codec {
cmd.arg("-c:v").arg(codec);
}
if let Some(codec) = self.audio_codec {
cmd.arg("-c:a").arg(codec);
}
if let Some(kbps) = self.video_bitrate {
cmd.arg("-b:v").arg(format!("{kbps}k"));
}
if let Some(kbps) = self.audio_bitrate {
cmd.arg("-b:a").arg(format!("{kbps}k"));
}
if let Some(fps) = self.frame_rate {
cmd.arg("-r").arg(format!("{fps}"));
}
if let Some(preset) = self.preset {
cmd.arg("-preset").arg(preset);
}
let mut vf_strings: Vec<String> = Vec::new();
for filter in self.video_filters {
vf_strings.push(filter.to_filter_string());
}
if !vf_strings.is_empty() {
cmd.arg("-vf").arg(vf_strings.join(","));
}
let mut af_strings: Vec<String> = Vec::new();
for filter in self.audio_filters {
af_strings.push(filter.to_filter_string());
}
if !af_strings.is_empty() {
cmd.arg("-af").arg(af_strings.join(","));
}
for arg in self.extra_args {
cmd.arg(arg);
}
cmd.arg(&self.output);
cmd.run()
}
}