Skip to main content

rust_ffmpeg/
filter.rs

1use ffmpeg_common::{utils, Result};
2use std::fmt;
3
4/// Video filter
5#[derive(Debug, Clone)]
6pub struct VideoFilter {
7    name: String,
8    params: Vec<(String, String)>,
9}
10
11impl VideoFilter {
12    /// Create a new video filter
13    pub fn new(name: impl Into<String>) -> Self {
14        Self {
15            name: name.into(),
16            params: Vec::new(),
17        }
18    }
19
20    /// Add a parameter
21    pub fn param(mut self, key: impl Into<String>, value: impl ToString) -> Self {
22        self.params.push((key.into(), value.to_string()));
23        self
24    }
25
26    /// Scale filter
27    pub fn scale(width: i32, height: i32) -> Self {
28        Self::new("scale")
29            .param("w", width)
30            .param("h", height)
31    }
32
33    /// Scale with aspect ratio preservation
34    pub fn scale_aspect(width: i32) -> Self {
35        Self::new("scale")
36            .param("w", width)
37            .param("h", -1)
38    }
39
40    /// Crop filter
41    pub fn crop(width: u32, height: u32, x: u32, y: u32) -> Self {
42        Self::new("crop")
43            .param("w", width)
44            .param("h", height)
45            .param("x", x)
46            .param("y", y)
47    }
48
49    /// Pad filter
50    pub fn pad(width: u32, height: u32) -> Self {
51        Self::new("pad")
52            .param("width", width)
53            .param("height", height)
54            .param("x", "(ow-iw)/2")
55            .param("y", "(oh-ih)/2")
56    }
57
58    /// Rotate filter
59    pub fn rotate(angle: f64) -> Self {
60        Self::new("rotate").param("angle", angle)
61    }
62
63    /// Transpose (90 degree rotations)
64    pub fn transpose(direction: TransposeDirection) -> Self {
65        Self::new("transpose").param("dir", direction as u8)
66    }
67
68    /// Horizontal flip
69    pub fn hflip() -> Self {
70        Self::new("hflip")
71    }
72
73    /// Vertical flip
74    pub fn vflip() -> Self {
75        Self::new("vflip")
76    }
77
78    /// FPS filter
79    pub fn fps(framerate: f64) -> Self {
80        Self::new("fps").param("fps", framerate)
81    }
82
83    /// Deinterlace with yadif
84    pub fn deinterlace() -> Self {
85        Self::new("yadif")
86    }
87
88    /// Denoise with hqdn3d
89    pub fn denoise(strength: f64) -> Self {
90        Self::new("hqdn3d").param("luma_spatial", strength)
91    }
92
93    /// Sharpen with unsharp
94    pub fn sharpen() -> Self {
95        Self::new("unsharp")
96    }
97
98    /// Blur
99    pub fn blur(radius: u32) -> Self {
100        Self::new("boxblur").param("lr", radius).param("lp", 1)
101    }
102
103    /// Overlay filter
104    pub fn overlay(x: impl Into<String>, y: impl Into<String>) -> Self {
105        Self::new("overlay")
106            .param("x", x.into())
107            .param("y", y.into())
108    }
109
110    /// Draw text
111    pub fn drawtext(text: impl Into<String>) -> Self {
112        Self::new("drawtext")
113            .param("text", utils::escape_filter_string(&text.into()))
114    }
115
116    /// Fade in
117    pub fn fade_in(duration: f64) -> Self {
118        Self::new("fade")
119            .param("type", "in")
120            .param("duration", duration)
121    }
122
123    /// Fade out
124    pub fn fade_out(duration: f64, start_time: f64) -> Self {
125        Self::new("fade")
126            .param("type", "out")
127            .param("duration", duration)
128            .param("start_time", start_time)
129    }
130
131    /// Set PTS (presentation timestamp)
132    pub fn setpts(expr: impl Into<String>) -> Self {
133        Self::new("setpts").param("expr", expr.into())
134    }
135
136    /// Select frames
137    pub fn select(expr: impl Into<String>) -> Self {
138        Self::new("select").param("expr", expr.into())
139    }
140
141    /// EQ (brightness/contrast/saturation)
142    pub fn eq() -> Self {
143        Self::new("eq")
144    }
145
146    /// Add brightness adjustment to EQ filter
147    pub fn brightness(mut self, value: f64) -> Self {
148        if self.name == "eq" {
149            self.params.push(("brightness".to_string(), value.to_string()));
150        }
151        self
152    }
153
154    /// Add contrast adjustment to EQ filter
155    pub fn contrast(mut self, value: f64) -> Self {
156        if self.name == "eq" {
157            self.params.push(("contrast".to_string(), value.to_string()));
158        }
159        self
160    }
161
162    /// Add saturation adjustment to EQ filter
163    pub fn saturation(mut self, value: f64) -> Self {
164        if self.name == "eq" {
165            self.params.push(("saturation".to_string(), value.to_string()));
166        }
167        self
168    }
169
170    /// Format conversion
171    pub fn format(pix_fmt: impl Into<String>) -> Self {
172        Self::new("format").param("pix_fmts", pix_fmt.into())
173    }
174}
175
176impl fmt::Display for VideoFilter {
177    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
178        write!(f, "{}", self.name)?;
179        if !self.params.is_empty() {
180            write!(f, "=")?;
181            let params: Vec<String> = self.params
182                .iter()
183                .map(|(k, v)| format!("{}={}", k, v))
184                .collect();
185            write!(f, "{}", params.join(":"))?;
186        }
187        Ok(())
188    }
189}
190
191/// Audio filter
192#[derive(Debug, Clone)]
193pub struct AudioFilter {
194    name: String,
195    params: Vec<(String, String)>,
196}
197
198impl AudioFilter {
199    /// Create a new audio filter
200    pub fn new(name: impl Into<String>) -> Self {
201        Self {
202            name: name.into(),
203            params: Vec::new(),
204        }
205    }
206
207    /// Add a parameter
208    pub fn param(mut self, key: impl Into<String>, value: impl ToString) -> Self {
209        self.params.push((key.into(), value.to_string()));
210        self
211    }
212
213    /// Volume adjustment
214    pub fn volume(level: f64) -> Self {
215        Self::new("volume").param("volume", level)
216    }
217
218    /// Audio normalization
219    pub fn loudnorm() -> Self {
220        Self::new("loudnorm")
221            .param("I", -16)
222            .param("TP", -1.5)
223            .param("LRA", 11)
224    }
225
226    /// Dynamic audio normalizer
227    pub fn dynaudnorm() -> Self {
228        Self::new("dynaudnorm")
229    }
230
231    /// High pass filter
232    pub fn highpass(frequency: u32) -> Self {
233        Self::new("highpass").param("f", frequency)
234    }
235
236    /// Low pass filter
237    pub fn lowpass(frequency: u32) -> Self {
238        Self::new("lowpass").param("f", frequency)
239    }
240
241    /// Audio fade in
242    pub fn afade_in(duration: f64) -> Self {
243        Self::new("afade")
244            .param("type", "in")
245            .param("duration", duration)
246    }
247
248    /// Audio fade out
249    pub fn afade_out(duration: f64, start_time: f64) -> Self {
250        Self::new("afade")
251            .param("type", "out")
252            .param("duration", duration)
253            .param("start_time", start_time)
254    }
255
256    /// Resample audio
257    pub fn aresample(sample_rate: u32) -> Self {
258        Self::new("aresample").param("sample_rate", sample_rate)
259    }
260
261    /// Change tempo without changing pitch
262    pub fn atempo(tempo: f64) -> Self {
263        Self::new("atempo").param("tempo", tempo)
264    }
265
266    /// Audio delay
267    pub fn adelay(delays: impl Into<String>) -> Self {
268        Self::new("adelay").param("delays", delays.into())
269    }
270
271    /// Audio echo
272    pub fn aecho(delays: impl Into<String>, decays: impl Into<String>) -> Self {
273        Self::new("aecho")
274            .param("delays", delays.into())
275            .param("decays", decays.into())
276    }
277
278    /// Compressor
279    pub fn acompressor() -> Self {
280        Self::new("acompressor")
281    }
282
283    /// Limiter
284    pub fn alimiter() -> Self {
285        Self::new("alimiter")
286    }
287
288    /// Gate
289    pub fn agate() -> Self {
290        Self::new("agate")
291    }
292
293    /// EQ band
294    pub fn anequalizer(frequency: u32, width: f64, gain: f64) -> Self {
295        Self::new("anequalizer")
296            .param("frequency", frequency)
297            .param("width", width)
298            .param("gain", gain)
299    }
300
301    /// Channel manipulation
302    pub fn channelmap(map: impl Into<String>) -> Self {
303        Self::new("channelmap").param("map", map.into())
304    }
305
306    /// Mix channels
307    pub fn amerge() -> Self {
308        Self::new("amerge")
309    }
310
311    /// Split channels
312    pub fn channelsplit() -> Self {
313        Self::new("channelsplit")
314    }
315}
316
317impl fmt::Display for AudioFilter {
318    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
319        write!(f, "{}", self.name)?;
320        if !self.params.is_empty() {
321            write!(f, "=")?;
322            let params: Vec<String> = self.params
323                .iter()
324                .map(|(k, v)| format!("{}={}", k, v))
325                .collect();
326            write!(f, "{}", params.join(":"))?;
327        }
328        Ok(())
329    }
330}
331
332/// Transpose direction
333#[derive(Debug, Clone, Copy)]
334#[repr(u8)]
335pub enum TransposeDirection {
336    CounterClockwiseFlip = 0,
337    Clockwise = 1,
338    CounterClockwise = 2,
339    ClockwiseFlip = 3,
340}
341
342/// Complex filter graph builder
343#[derive(Debug, Clone, Default)]
344pub struct FilterGraph {
345    nodes: Vec<FilterNode>,
346    edges: Vec<FilterEdge>,
347}
348
349#[derive(Debug, Clone)]
350struct FilterNode {
351    id: String,
352    filter: String,
353    inputs: Vec<String>,
354    outputs: Vec<String>,
355}
356
357#[derive(Debug, Clone)]
358struct FilterEdge {
359    from: String,
360    to: String,
361}
362
363impl FilterGraph {
364    /// Create a new filter graph
365    pub fn new() -> Self {
366        Self::default()
367    }
368
369    /// Add a filter node
370    pub fn add_filter(
371        mut self,
372        filter: impl Into<String>,
373        inputs: Vec<String>,
374        outputs: Vec<String>,
375    ) -> Self {
376        let id = format!("f{}", self.nodes.len());
377        self.nodes.push(FilterNode {
378            id: id.clone(),
379            filter: filter.into(),
380            inputs,
381            outputs,
382        });
383        self
384    }
385
386    /// Connect two filter nodes
387    pub fn connect(mut self, from: impl Into<String>, to: impl Into<String>) -> Self {
388        self.edges.push(FilterEdge {
389            from: from.into(),
390            to: to.into(),
391        });
392        self
393    }
394
395    /// Build the filter graph string
396    pub fn build(&self) -> String {
397        let mut parts = Vec::new();
398
399        for node in &self.nodes {
400            let mut part = String::new();
401
402            // Inputs
403            if !node.inputs.is_empty() {
404                part.push_str(&node.inputs.join(""));
405            }
406
407            // Filter
408            part.push_str(&node.filter);
409
410            // Outputs
411            if !node.outputs.is_empty() {
412                part.push_str(&node.outputs.join(""));
413            }
414
415            parts.push(part);
416        }
417
418        parts.join(";")
419    }
420}
421
422/// Common filter chains
423pub mod chains {
424    use super::*;
425
426    /// Create a thumbnail extraction filter chain
427    pub fn thumbnail() -> Vec<VideoFilter> {
428        vec![
429            VideoFilter::select("eq(pict_type\\,I)"),
430            VideoFilter::scale(320, -1),
431        ]
432    }
433
434    /// Create a GIF optimization filter chain
435    pub fn gif_optimize(width: u32, fps: f64) -> Vec<VideoFilter> {
436        vec![
437            VideoFilter::fps(fps),
438            VideoFilter::scale(width as i32, -1),
439            VideoFilter::new("split").param("outputs", 2),
440            VideoFilter::new("palettegen"),
441            VideoFilter::new("paletteuse"),
442        ]
443    }
444
445    /// Stabilization filter chain
446    pub fn stabilize() -> Vec<VideoFilter> {
447        vec![
448            VideoFilter::new("vidstabdetect").param("shakiness", 5),
449            VideoFilter::new("vidstabtransform"),
450        ]
451    }
452
453    /// Cinematic look
454    pub fn cinematic() -> Vec<VideoFilter> {
455        vec![
456            VideoFilter::eq()
457                .contrast(1.2)
458                .brightness(-0.05)
459                .saturation(0.8),
460            VideoFilter::new("curves")
461                .param("preset", "vintage"),
462            VideoFilter::new("vignette"),
463        ]
464    }
465
466    /// Upscale with enhancement
467    pub fn upscale_enhance(scale: u32) -> Vec<VideoFilter> {
468        vec![
469            VideoFilter::scale(scale as i32, -1),
470            VideoFilter::sharpen(),
471            VideoFilter::new("hqdn3d").param("luma_spatial", 4),
472        ]
473    }
474
475    /// Audio mastering chain
476    pub fn audio_master() -> Vec<AudioFilter> {
477        vec![
478            AudioFilter::highpass(80),
479            AudioFilter::acompressor()
480                .param("threshold", -20)
481                .param("ratio", 4)
482                .param("attack", 5)
483                .param("release", 50),
484            AudioFilter::anequalizer(100, 100.0, 2.0),
485            AudioFilter::anequalizer(1000, 500.0, -1.0),
486            AudioFilter::anequalizer(10000, 2000.0, 1.0),
487            AudioFilter::alimiter()
488                .param("limit", -0.5),
489            AudioFilter::loudnorm(),
490        ]
491    }
492
493    /// Podcast audio processing
494    pub fn podcast_audio() -> Vec<AudioFilter> {
495        vec![
496            AudioFilter::highpass(100),
497            AudioFilter::lowpass(15000),
498            AudioFilter::agate()
499                .param("threshold", -35)
500                .param("range", -40),
501            AudioFilter::acompressor()
502                .param("threshold", -15)
503                .param("ratio", 3),
504            AudioFilter::loudnorm(),
505        ]
506    }
507}
508
509#[cfg(test)]
510mod tests {
511    use super::*;
512
513    #[test]
514    fn test_video_filters() {
515        let scale = VideoFilter::scale(1920, 1080);
516        assert_eq!(scale.to_string(), "scale=w=1920:h=1080");
517
518        let crop = VideoFilter::crop(640, 480, 100, 50);
519        assert_eq!(crop.to_string(), "crop=w=640:h=480:x=100:y=50");
520
521        let text = VideoFilter::drawtext("Hello, World!");
522        assert!(text.to_string().contains("drawtext=text="));
523    }
524
525    #[test]
526    fn test_audio_filters() {
527        let volume = AudioFilter::volume(0.5);
528        assert_eq!(volume.to_string(), "volume=volume=0.5");
529
530        let tempo = AudioFilter::atempo(1.5);
531        assert_eq!(tempo.to_string(), "atempo=tempo=1.5");
532    }
533
534    #[test]
535    fn test_filter_graph() {
536        let graph = FilterGraph::new()
537            .add_filter("scale=640:480", vec!["[0:v]".to_string()], vec!["[scaled]".to_string()])
538            .add_filter("overlay", vec!["[scaled]".to_string(), "[1:v]".to_string()], vec!["[out]".to_string()]);
539
540        let result = graph.build();
541        assert!(result.contains("[0:v]scale=640:480[scaled]"));
542        assert!(result.contains("[scaled][1:v]overlay[out]"));
543    }
544}