ffmpeg_light/
transcode.rs

1//! Transcoding helpers built on top of the CLI `ffmpeg` binary.
2
3use std::ffi::OsString;
4use std::path::{Path, PathBuf};
5
6use crate::command::{FfmpegBinaryPaths, FfmpegCommand};
7use crate::config::FfmpegLocator;
8use crate::error::{Error, Result};
9use crate::filter::VideoFilter;
10
11/// Builder-style API for spinning up simple ffmpeg jobs.
12#[derive(Debug, Default)]
13pub struct TranscodeBuilder {
14    binaries: Option<FfmpegBinaryPaths>,
15    input: Option<PathBuf>,
16    output: Option<PathBuf>,
17    video_codec: Option<String>,
18    audio_codec: Option<String>,
19    video_bitrate: Option<u32>,
20    audio_bitrate: Option<u32>,
21    frame_rate: Option<f64>,
22    preset: Option<String>,
23    filters: Vec<VideoFilter>,
24    extra_args: Vec<OsString>,
25    overwrite: bool,
26}
27
28impl TranscodeBuilder {
29    /// Create a new builder with sensible defaults (overwrite enabled).
30    pub fn new() -> Self {
31        Self {
32            overwrite: true,
33            ..Self::default()
34        }
35    }
36
37    /// Use pre-discovered binaries instead of searching PATH every call.
38    pub fn with_binaries(mut self, binaries: &FfmpegBinaryPaths) -> Self {
39        self.binaries = Some(binaries.clone());
40        self
41    }
42
43    /// Pin the builder to a specific locator.
44    pub fn with_locator(mut self, locator: &FfmpegLocator) -> Self {
45        self.binaries = Some(locator.binaries().clone());
46        self
47    }
48
49    /// Input media path.
50    pub fn input<P: AsRef<Path>>(mut self, path: P) -> Self {
51        self.input = Some(path.as_ref().to_path_buf());
52        self
53    }
54
55    /// Output media path.
56    pub fn output<P: AsRef<Path>>(mut self, path: P) -> Self {
57        self.output = Some(path.as_ref().to_path_buf());
58        self
59    }
60
61    /// Desired video codec (e.g. `libx264`).
62    pub fn video_codec(mut self, codec: impl Into<String>) -> Self {
63        self.video_codec = Some(codec.into());
64        self
65    }
66
67    /// Desired audio codec (e.g. `aac`).
68    pub fn audio_codec(mut self, codec: impl Into<String>) -> Self {
69        self.audio_codec = Some(codec.into());
70        self
71    }
72
73    /// Target video bitrate in kbps.
74    pub fn video_bitrate(mut self, kbps: u32) -> Self {
75        self.video_bitrate = Some(kbps);
76        self
77    }
78
79    /// Target audio bitrate in kbps.
80    pub fn audio_bitrate(mut self, kbps: u32) -> Self {
81        self.audio_bitrate = Some(kbps);
82        self
83    }
84
85    /// Target frame rate.
86    pub fn frame_rate(mut self, fps: f64) -> Self {
87        self.frame_rate = Some(fps);
88        self
89    }
90
91    /// Apply a named preset (maps to `-preset`).
92    pub fn preset(mut self, preset: impl Into<String>) -> Self {
93        self.preset = Some(preset.into());
94        self
95    }
96
97    /// Convenience helper to scale output.
98    pub fn size(self, width: u32, height: u32) -> Self {
99        self.add_filter(VideoFilter::Scale { width, height })
100    }
101
102    /// Push a filter into the video filter graph.
103    pub fn add_filter(mut self, filter: VideoFilter) -> Self {
104        self.filters.push(filter);
105        self
106    }
107
108    /// Pass a raw argument for advanced cases.
109    pub fn extra_arg(mut self, arg: impl Into<OsString>) -> Self {
110        self.extra_args.push(arg.into());
111        self
112    }
113
114    /// Control whether ffmpeg should overwrite the output file.
115    pub fn overwrite(mut self, enabled: bool) -> Self {
116        self.overwrite = enabled;
117        self
118    }
119
120    fn resolve_binaries(binaries: Option<FfmpegBinaryPaths>) -> Result<FfmpegBinaryPaths> {
121        if let Some(paths) = binaries {
122            return Ok(paths);
123        }
124        Ok(FfmpegLocator::system()?.binaries().clone())
125    }
126
127    fn validate(self) -> Result<ValidatedTranscode> {
128        let Self {
129            binaries,
130            input,
131            output,
132            video_codec,
133            audio_codec,
134            video_bitrate,
135            audio_bitrate,
136            frame_rate,
137            preset,
138            filters,
139            extra_args,
140            overwrite,
141        } = self;
142
143        let input = input.ok_or_else(|| Error::InvalidInput("input path is required".into()))?;
144        let output = output.ok_or_else(|| Error::InvalidInput("output path is required".into()))?;
145
146        Ok(ValidatedTranscode {
147            binaries: Self::resolve_binaries(binaries)?,
148            input,
149            output,
150            video_codec,
151            audio_codec,
152            video_bitrate,
153            audio_bitrate,
154            frame_rate,
155            preset,
156            filters,
157            extra_args,
158            overwrite,
159        })
160    }
161
162    /// Execute ffmpeg with the configured arguments.
163    pub fn run(self) -> Result<()> {
164        let validated = self.validate()?;
165        validated.run()
166    }
167}
168
169struct ValidatedTranscode {
170    binaries: FfmpegBinaryPaths,
171    input: PathBuf,
172    output: PathBuf,
173    video_codec: Option<String>,
174    audio_codec: Option<String>,
175    video_bitrate: Option<u32>,
176    audio_bitrate: Option<u32>,
177    frame_rate: Option<f64>,
178    preset: Option<String>,
179    filters: Vec<VideoFilter>,
180    extra_args: Vec<OsString>,
181    overwrite: bool,
182}
183
184impl ValidatedTranscode {
185    fn run(self) -> Result<()> {
186        let mut cmd = FfmpegCommand::new(self.binaries.ffmpeg());
187        cmd.arg(if self.overwrite { "-y" } else { "-n" });
188        cmd.arg("-i").arg(&self.input);
189
190        if let Some(codec) = self.video_codec {
191            cmd.arg("-c:v").arg(codec);
192        }
193        if let Some(codec) = self.audio_codec {
194            cmd.arg("-c:a").arg(codec);
195        }
196        if let Some(kbps) = self.video_bitrate {
197            cmd.arg("-b:v").arg(format!("{kbps}k"));
198        }
199        if let Some(kbps) = self.audio_bitrate {
200            cmd.arg("-b:a").arg(format!("{kbps}k"));
201        }
202        if let Some(fps) = self.frame_rate {
203            cmd.arg("-r").arg(format!("{fps}"));
204        }
205        if let Some(preset) = self.preset {
206            cmd.arg("-preset").arg(preset);
207        }
208
209        let mut filter_strings: Vec<String> = Vec::new();
210        for filter in self.filters {
211            filter_strings.push(filter.to_filter_string());
212        }
213        if !filter_strings.is_empty() {
214            cmd.arg("-vf").arg(filter_strings.join(","));
215        }
216
217        for arg in self.extra_args {
218            cmd.arg(arg);
219        }
220
221        cmd.arg(&self.output);
222        cmd.run()
223    }
224}