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::{AudioFilter, 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    video_filters: Vec<VideoFilter>,
24    audio_filters: Vec<AudioFilter>,
25    extra_args: Vec<OsString>,
26    overwrite: bool,
27}
28
29impl TranscodeBuilder {
30    /// Create a new builder with sensible defaults (overwrite enabled).
31    pub fn new() -> Self {
32        Self {
33            overwrite: true,
34            ..Self::default()
35        }
36    }
37
38    /// Use pre-discovered binaries instead of searching PATH every call.
39    pub fn with_binaries(mut self, binaries: &FfmpegBinaryPaths) -> Self {
40        self.binaries = Some(binaries.clone());
41        self
42    }
43
44    /// Pin the builder to a specific locator.
45    pub fn with_locator(mut self, locator: &FfmpegLocator) -> Self {
46        self.binaries = Some(locator.binaries().clone());
47        self
48    }
49
50    /// Input media path.
51    pub fn input<P: AsRef<Path>>(mut self, path: P) -> Self {
52        self.input = Some(path.as_ref().to_path_buf());
53        self
54    }
55
56    /// Output media path.
57    pub fn output<P: AsRef<Path>>(mut self, path: P) -> Self {
58        self.output = Some(path.as_ref().to_path_buf());
59        self
60    }
61
62    /// Desired video codec (e.g. `libx264`).
63    pub fn video_codec(mut self, codec: impl Into<String>) -> Self {
64        self.video_codec = Some(codec.into());
65        self
66    }
67
68    /// Desired audio codec (e.g. `aac`).
69    pub fn audio_codec(mut self, codec: impl Into<String>) -> Self {
70        self.audio_codec = Some(codec.into());
71        self
72    }
73
74    /// Target video bitrate in kbps.
75    pub fn video_bitrate(mut self, kbps: u32) -> Self {
76        self.video_bitrate = Some(kbps);
77        self
78    }
79
80    /// Target audio bitrate in kbps.
81    pub fn audio_bitrate(mut self, kbps: u32) -> Self {
82        self.audio_bitrate = Some(kbps);
83        self
84    }
85
86    /// Target frame rate.
87    pub fn frame_rate(mut self, fps: f64) -> Self {
88        self.frame_rate = Some(fps);
89        self
90    }
91
92    /// Apply a named preset (maps to `-preset`).
93    pub fn preset(mut self, preset: impl Into<String>) -> Self {
94        self.preset = Some(preset.into());
95        self
96    }
97
98    /// Convenience helper to scale output.
99    pub fn size(self, width: u32, height: u32) -> Self {
100        self.add_video_filter(VideoFilter::Scale { width, height })
101    }
102
103    /// Add a video filter to the processing chain.
104    pub fn add_video_filter(mut self, filter: VideoFilter) -> Self {
105        self.video_filters.push(filter);
106        self
107    }
108
109    /// Add an audio filter to the processing chain.
110    pub fn add_audio_filter(mut self, filter: AudioFilter) -> Self {
111        self.audio_filters.push(filter);
112        self
113    }
114
115    /// Backward compatibility: alias for `add_video_filter`.
116    #[deprecated(since = "0.2.0", note = "use add_video_filter() instead")]
117    pub fn add_filter(self, filter: VideoFilter) -> Self {
118        self.add_video_filter(filter)
119    }
120
121    /// Pass a raw argument for advanced cases.
122    pub fn extra_arg(mut self, arg: impl Into<OsString>) -> Self {
123        self.extra_args.push(arg.into());
124        self
125    }
126
127    /// Control whether ffmpeg should overwrite the output file.
128    pub fn overwrite(mut self, enabled: bool) -> Self {
129        self.overwrite = enabled;
130        self
131    }
132
133    /// Accessor for the configured input path.
134    pub fn input_path(&self) -> Option<&Path> {
135        self.input.as_deref()
136    }
137
138    /// Accessor for the configured output path.
139    pub fn output_path(&self) -> Option<&Path> {
140        self.output.as_deref()
141    }
142
143    /// Accessor for the configured video codec.
144    pub fn video_codec_ref(&self) -> Option<&str> {
145        self.video_codec.as_deref()
146    }
147
148    /// Accessor for the configured audio codec.
149    pub fn audio_codec_ref(&self) -> Option<&str> {
150        self.audio_codec.as_deref()
151    }
152
153    /// Accessor for the configured video bitrate.
154    pub fn video_bitrate_value(&self) -> Option<u32> {
155        self.video_bitrate
156    }
157
158    /// Accessor for the configured audio bitrate.
159    pub fn audio_bitrate_value(&self) -> Option<u32> {
160        self.audio_bitrate
161    }
162
163    /// Accessor for the configured frame rate.
164    pub fn frame_rate_value(&self) -> Option<f64> {
165        self.frame_rate
166    }
167
168    /// Accessor for the configured preset.
169    pub fn preset_value(&self) -> Option<&str> {
170        self.preset.as_deref()
171    }
172
173    /// Returns whether overwriting outputs is enabled.
174    pub fn overwrite_enabled(&self) -> bool {
175        self.overwrite
176    }
177
178    /// Accessor for the configured video filter chain.
179    pub fn video_filters(&self) -> &[VideoFilter] {
180        &self.video_filters
181    }
182
183    /// Accessor for the configured audio filter chain.
184    pub fn audio_filters(&self) -> &[AudioFilter] {
185        &self.audio_filters
186    }
187
188    fn resolve_binaries(binaries: Option<FfmpegBinaryPaths>) -> Result<FfmpegBinaryPaths> {
189        if let Some(paths) = binaries {
190            return Ok(paths);
191        }
192        Ok(FfmpegLocator::system()?.binaries().clone())
193    }
194
195    fn validate(self) -> Result<ValidatedTranscode> {
196        let Self {
197            binaries,
198            input,
199            output,
200            video_codec,
201            audio_codec,
202            video_bitrate,
203            audio_bitrate,
204            frame_rate,
205            preset,
206            video_filters,
207            audio_filters,
208            extra_args,
209            overwrite,
210        } = self;
211
212        let input = input.ok_or_else(|| Error::InvalidInput("input path is required".into()))?;
213        let output = output.ok_or_else(|| Error::InvalidInput("output path is required".into()))?;
214
215        Ok(ValidatedTranscode {
216            binaries: Self::resolve_binaries(binaries)?,
217            input,
218            output,
219            video_codec,
220            audio_codec,
221            video_bitrate,
222            audio_bitrate,
223            frame_rate,
224            preset,
225            video_filters,
226            audio_filters,
227            extra_args,
228            overwrite,
229        })
230    }
231
232    /// Execute ffmpeg with the configured arguments.
233    pub fn run(self) -> Result<()> {
234        let validated = self.validate()?;
235        validated.run()
236    }
237}
238
239struct ValidatedTranscode {
240    binaries: FfmpegBinaryPaths,
241    input: PathBuf,
242    output: PathBuf,
243    video_codec: Option<String>,
244    audio_codec: Option<String>,
245    video_bitrate: Option<u32>,
246    audio_bitrate: Option<u32>,
247    frame_rate: Option<f64>,
248    preset: Option<String>,
249    video_filters: Vec<VideoFilter>,
250    audio_filters: Vec<AudioFilter>,
251    extra_args: Vec<OsString>,
252    overwrite: bool,
253}
254
255impl ValidatedTranscode {
256    fn run(self) -> Result<()> {
257        let mut cmd = FfmpegCommand::new(self.binaries.ffmpeg());
258        cmd.arg(if self.overwrite { "-y" } else { "-n" });
259        cmd.arg("-i").arg(&self.input);
260
261        if let Some(codec) = self.video_codec {
262            cmd.arg("-c:v").arg(codec);
263        }
264        if let Some(codec) = self.audio_codec {
265            cmd.arg("-c:a").arg(codec);
266        }
267        if let Some(kbps) = self.video_bitrate {
268            cmd.arg("-b:v").arg(format!("{kbps}k"));
269        }
270        if let Some(kbps) = self.audio_bitrate {
271            cmd.arg("-b:a").arg(format!("{kbps}k"));
272        }
273        if let Some(fps) = self.frame_rate {
274            cmd.arg("-r").arg(format!("{fps}"));
275        }
276        if let Some(preset) = self.preset {
277            cmd.arg("-preset").arg(preset);
278        }
279
280        // Build video filter chain
281        let mut vf_strings: Vec<String> = Vec::new();
282        for filter in self.video_filters {
283            vf_strings.push(filter.to_filter_string());
284        }
285        if !vf_strings.is_empty() {
286            cmd.arg("-vf").arg(vf_strings.join(","));
287        }
288
289        // Build audio filter chain
290        let mut af_strings: Vec<String> = Vec::new();
291        for filter in self.audio_filters {
292            af_strings.push(filter.to_filter_string());
293        }
294        if !af_strings.is_empty() {
295            cmd.arg("-af").arg(af_strings.join(","));
296        }
297
298        for arg in self.extra_args {
299            cmd.arg(arg);
300        }
301
302        cmd.arg(&self.output);
303        cmd.run()
304    }
305}
306