Skip to main content

oximedia_transcode/
filters.rs

1//! Video and audio filter chains for transcode operations.
2
3use std::fmt;
4
5/// Video filter chain.
6#[derive(Debug, Clone)]
7pub struct VideoFilter {
8    filters: Vec<FilterNode>,
9}
10
11/// Audio filter chain.
12#[derive(Debug, Clone)]
13pub struct AudioFilter {
14    filters: Vec<FilterNode>,
15}
16
17/// A single filter node in a filter chain.
18#[derive(Debug, Clone)]
19pub struct FilterNode {
20    /// Filter name.
21    pub name: String,
22    /// Filter parameters.
23    pub params: Vec<(String, String)>,
24}
25
26impl VideoFilter {
27    /// Creates a new empty video filter chain.
28    #[must_use]
29    pub fn new() -> Self {
30        Self {
31            filters: Vec::new(),
32        }
33    }
34
35    /// Adds a scale filter.
36    #[must_use]
37    pub fn scale(mut self, width: u32, height: u32) -> Self {
38        self.filters.push(FilterNode {
39            name: "scale".to_string(),
40            params: vec![
41                ("width".to_string(), width.to_string()),
42                ("height".to_string(), height.to_string()),
43            ],
44        });
45        self
46    }
47
48    /// Adds a deinterlace filter.
49    #[must_use]
50    pub fn deinterlace(mut self) -> Self {
51        self.filters.push(FilterNode {
52            name: "yadif".to_string(),
53            params: vec![("mode".to_string(), "1".to_string())],
54        });
55        self
56    }
57
58    /// Adds a crop filter.
59    #[must_use]
60    pub fn crop(mut self, width: u32, height: u32, x: u32, y: u32) -> Self {
61        self.filters.push(FilterNode {
62            name: "crop".to_string(),
63            params: vec![
64                ("width".to_string(), width.to_string()),
65                ("height".to_string(), height.to_string()),
66                ("x".to_string(), x.to_string()),
67                ("y".to_string(), y.to_string()),
68            ],
69        });
70        self
71    }
72
73    /// Adds a pad filter.
74    #[must_use]
75    pub fn pad(mut self, width: u32, height: u32, x: u32, y: u32, color: &str) -> Self {
76        self.filters.push(FilterNode {
77            name: "pad".to_string(),
78            params: vec![
79                ("width".to_string(), width.to_string()),
80                ("height".to_string(), height.to_string()),
81                ("x".to_string(), x.to_string()),
82                ("y".to_string(), y.to_string()),
83                ("color".to_string(), color.to_string()),
84            ],
85        });
86        self
87    }
88
89    /// Adds a denoise filter.
90    #[must_use]
91    pub fn denoise(mut self, strength: f32) -> Self {
92        self.filters.push(FilterNode {
93            name: "hqdn3d".to_string(),
94            params: vec![("luma_spatial".to_string(), strength.to_string())],
95        });
96        self
97    }
98
99    /// Adds a sharpen filter.
100    #[must_use]
101    pub fn sharpen(mut self, amount: f32) -> Self {
102        self.filters.push(FilterNode {
103            name: "unsharp".to_string(),
104            params: vec![("luma_amount".to_string(), amount.to_string())],
105        });
106        self
107    }
108
109    /// Adds a color correction filter.
110    #[must_use]
111    pub fn color_correct(mut self, brightness: f32, contrast: f32, saturation: f32) -> Self {
112        self.filters.push(FilterNode {
113            name: "eq".to_string(),
114            params: vec![
115                ("brightness".to_string(), brightness.to_string()),
116                ("contrast".to_string(), contrast.to_string()),
117                ("saturation".to_string(), saturation.to_string()),
118            ],
119        });
120        self
121    }
122
123    /// Adds a framerate conversion filter.
124    #[must_use]
125    pub fn framerate(mut self, fps: f64) -> Self {
126        self.filters.push(FilterNode {
127            name: "fps".to_string(),
128            params: vec![("fps".to_string(), fps.to_string())],
129        });
130        self
131    }
132
133    /// Adds a rotate filter.
134    #[must_use]
135    pub fn rotate(mut self, degrees: f64) -> Self {
136        self.filters.push(FilterNode {
137            name: "rotate".to_string(),
138            params: vec![("angle".to_string(), degrees.to_string())],
139        });
140        self
141    }
142
143    /// Adds a flip filter (vertical).
144    #[must_use]
145    pub fn vflip(mut self) -> Self {
146        self.filters.push(FilterNode {
147            name: "vflip".to_string(),
148            params: Vec::new(),
149        });
150        self
151    }
152
153    /// Adds a flip filter (horizontal).
154    #[must_use]
155    pub fn hflip(mut self) -> Self {
156        self.filters.push(FilterNode {
157            name: "hflip".to_string(),
158            params: Vec::new(),
159        });
160        self
161    }
162
163    /// Adds a custom filter.
164    #[must_use]
165    pub fn custom(mut self, name: impl Into<String>, params: Vec<(String, String)>) -> Self {
166        self.filters.push(FilterNode {
167            name: name.into(),
168            params,
169        });
170        self
171    }
172
173    /// Converts the filter chain to a filter string.
174    #[must_use]
175    pub fn to_string(&self) -> String {
176        self.filters
177            .iter()
178            .map(std::string::ToString::to_string)
179            .collect::<Vec<_>>()
180            .join(",")
181    }
182
183    /// Gets the number of filters in the chain.
184    #[must_use]
185    pub fn len(&self) -> usize {
186        self.filters.len()
187    }
188
189    /// Checks if the filter chain is empty.
190    #[must_use]
191    pub fn is_empty(&self) -> bool {
192        self.filters.is_empty()
193    }
194}
195
196impl Default for VideoFilter {
197    fn default() -> Self {
198        Self::new()
199    }
200}
201
202impl AudioFilter {
203    /// Creates a new empty audio filter chain.
204    #[must_use]
205    pub fn new() -> Self {
206        Self {
207            filters: Vec::new(),
208        }
209    }
210
211    /// Adds a volume filter.
212    #[must_use]
213    pub fn volume(mut self, volume: f32) -> Self {
214        self.filters.push(FilterNode {
215            name: "volume".to_string(),
216            params: vec![("volume".to_string(), volume.to_string())],
217        });
218        self
219    }
220
221    /// Adds a resample filter.
222    #[must_use]
223    pub fn resample(mut self, sample_rate: u32) -> Self {
224        self.filters.push(FilterNode {
225            name: "aresample".to_string(),
226            params: vec![("sample_rate".to_string(), sample_rate.to_string())],
227        });
228        self
229    }
230
231    /// Adds a channel layout filter.
232    #[must_use]
233    pub fn channel_layout(mut self, layout: &str) -> Self {
234        self.filters.push(FilterNode {
235            name: "aformat".to_string(),
236            params: vec![("channel_layouts".to_string(), layout.to_string())],
237        });
238        self
239    }
240
241    /// Adds an audio normalization filter.
242    #[must_use]
243    pub fn normalize(mut self, target_lufs: f64) -> Self {
244        self.filters.push(FilterNode {
245            name: "loudnorm".to_string(),
246            params: vec![("I".to_string(), target_lufs.to_string())],
247        });
248        self
249    }
250
251    /// Adds a low-pass filter.
252    #[must_use]
253    pub fn lowpass(mut self, frequency: f64) -> Self {
254        self.filters.push(FilterNode {
255            name: "lowpass".to_string(),
256            params: vec![("frequency".to_string(), frequency.to_string())],
257        });
258        self
259    }
260
261    /// Adds a high-pass filter.
262    #[must_use]
263    pub fn highpass(mut self, frequency: f64) -> Self {
264        self.filters.push(FilterNode {
265            name: "highpass".to_string(),
266            params: vec![("frequency".to_string(), frequency.to_string())],
267        });
268        self
269    }
270
271    /// Adds an equalizer filter.
272    #[must_use]
273    pub fn equalizer(mut self, frequency: f64, width: f64, gain: f64) -> Self {
274        self.filters.push(FilterNode {
275            name: "equalizer".to_string(),
276            params: vec![
277                ("frequency".to_string(), frequency.to_string()),
278                ("width_type".to_string(), "h".to_string()),
279                ("width".to_string(), width.to_string()),
280                ("gain".to_string(), gain.to_string()),
281            ],
282        });
283        self
284    }
285
286    /// Adds a compressor filter.
287    #[must_use]
288    pub fn compress(mut self, threshold: f64, ratio: f64) -> Self {
289        self.filters.push(FilterNode {
290            name: "acompressor".to_string(),
291            params: vec![
292                ("threshold".to_string(), threshold.to_string()),
293                ("ratio".to_string(), ratio.to_string()),
294            ],
295        });
296        self
297    }
298
299    /// Adds a delay filter.
300    #[must_use]
301    pub fn delay(mut self, milliseconds: u32) -> Self {
302        self.filters.push(FilterNode {
303            name: "adelay".to_string(),
304            params: vec![("delays".to_string(), format!("{milliseconds}ms"))],
305        });
306        self
307    }
308
309    /// Adds a fade-in filter.
310    #[must_use]
311    pub fn fade_in(mut self, duration: f64) -> Self {
312        self.filters.push(FilterNode {
313            name: "afade".to_string(),
314            params: vec![
315                ("type".to_string(), "in".to_string()),
316                ("duration".to_string(), duration.to_string()),
317            ],
318        });
319        self
320    }
321
322    /// Adds a fade-out filter.
323    #[must_use]
324    pub fn fade_out(mut self, start: f64, duration: f64) -> Self {
325        self.filters.push(FilterNode {
326            name: "afade".to_string(),
327            params: vec![
328                ("type".to_string(), "out".to_string()),
329                ("start_time".to_string(), start.to_string()),
330                ("duration".to_string(), duration.to_string()),
331            ],
332        });
333        self
334    }
335
336    /// Adds a custom filter.
337    #[must_use]
338    pub fn custom(mut self, name: impl Into<String>, params: Vec<(String, String)>) -> Self {
339        self.filters.push(FilterNode {
340            name: name.into(),
341            params,
342        });
343        self
344    }
345
346    /// Converts the filter chain to a filter string.
347    #[must_use]
348    pub fn to_string(&self) -> String {
349        self.filters
350            .iter()
351            .map(std::string::ToString::to_string)
352            .collect::<Vec<_>>()
353            .join(",")
354    }
355
356    /// Gets the number of filters in the chain.
357    #[must_use]
358    pub fn len(&self) -> usize {
359        self.filters.len()
360    }
361
362    /// Checks if the filter chain is empty.
363    #[must_use]
364    pub fn is_empty(&self) -> bool {
365        self.filters.is_empty()
366    }
367}
368
369impl Default for AudioFilter {
370    fn default() -> Self {
371        Self::new()
372    }
373}
374
375impl FilterNode {
376    /// Creates a new filter node.
377    #[must_use]
378    pub fn new(name: impl Into<String>) -> Self {
379        Self {
380            name: name.into(),
381            params: Vec::new(),
382        }
383    }
384
385    /// Adds a parameter to the filter.
386    #[must_use]
387    pub fn param(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
388        self.params.push((key.into(), value.into()));
389        self
390    }
391}
392
393impl fmt::Display for FilterNode {
394    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
395        if self.params.is_empty() {
396            write!(f, "{}", self.name)
397        } else {
398            let params = self
399                .params
400                .iter()
401                .map(|(k, v)| format!("{k}={v}"))
402                .collect::<Vec<_>>()
403                .join(":");
404            write!(f, "{}={}", self.name, params)
405        }
406    }
407}
408
409#[cfg(test)]
410mod tests {
411    use super::*;
412
413    #[test]
414    fn test_video_filter_scale() {
415        let filter = VideoFilter::new().scale(1920, 1080);
416        assert_eq!(filter.len(), 1);
417        assert!(!filter.is_empty());
418    }
419
420    #[test]
421    fn test_video_filter_chain() {
422        let filter = VideoFilter::new()
423            .scale(1920, 1080)
424            .deinterlace()
425            .denoise(3.0)
426            .sharpen(1.5);
427
428        assert_eq!(filter.len(), 4);
429        let filter_str = filter.to_string();
430        assert!(filter_str.contains("scale"));
431        assert!(filter_str.contains("yadif"));
432    }
433
434    #[test]
435    fn test_video_filter_crop() {
436        let filter = VideoFilter::new().crop(1920, 1080, 0, 0);
437        assert_eq!(filter.len(), 1);
438    }
439
440    #[test]
441    fn test_video_filter_color_correct() {
442        let filter = VideoFilter::new().color_correct(0.1, 1.2, 1.0);
443        assert_eq!(filter.len(), 1);
444    }
445
446    #[test]
447    fn test_video_filter_rotate() {
448        let filter = VideoFilter::new().rotate(90.0);
449        assert_eq!(filter.len(), 1);
450    }
451
452    #[test]
453    fn test_video_filter_flip() {
454        let filter = VideoFilter::new().vflip().hflip();
455        assert_eq!(filter.len(), 2);
456    }
457
458    #[test]
459    fn test_audio_filter_volume() {
460        let filter = AudioFilter::new().volume(1.5);
461        assert_eq!(filter.len(), 1);
462    }
463
464    #[test]
465    fn test_audio_filter_resample() {
466        let filter = AudioFilter::new().resample(48000);
467        assert_eq!(filter.len(), 1);
468    }
469
470    #[test]
471    fn test_audio_filter_normalize() {
472        let filter = AudioFilter::new().normalize(-23.0);
473        assert_eq!(filter.len(), 1);
474    }
475
476    #[test]
477    fn test_audio_filter_chain() {
478        let filter = AudioFilter::new()
479            .volume(1.0)
480            .resample(48000)
481            .normalize(-23.0)
482            .compress(-20.0, 3.0);
483
484        assert_eq!(filter.len(), 4);
485    }
486
487    #[test]
488    fn test_audio_filter_eq() {
489        let filter = AudioFilter::new()
490            .equalizer(100.0, 200.0, 5.0)
491            .equalizer(1000.0, 200.0, -3.0);
492
493        assert_eq!(filter.len(), 2);
494    }
495
496    #[test]
497    fn test_audio_filter_fade() {
498        let filter = AudioFilter::new().fade_in(2.0).fade_out(58.0, 2.0);
499
500        assert_eq!(filter.len(), 2);
501    }
502
503    #[test]
504    fn test_filter_node_display() {
505        let node = FilterNode {
506            name: "scale".to_string(),
507            params: vec![
508                ("width".to_string(), "1920".to_string()),
509                ("height".to_string(), "1080".to_string()),
510            ],
511        };
512
513        let display = format!("{node}");
514        assert!(display.contains("scale"));
515        assert!(display.contains("width=1920"));
516        assert!(display.contains("height=1080"));
517    }
518
519    #[test]
520    fn test_empty_filter_chain() {
521        let video_filter = VideoFilter::new();
522        assert!(video_filter.is_empty());
523        assert_eq!(video_filter.len(), 0);
524
525        let audio_filter = AudioFilter::new();
526        assert!(audio_filter.is_empty());
527        assert_eq!(audio_filter.len(), 0);
528    }
529}