1use std::time::Duration;
4
5use ff_format::{AudioFrame, VideoFrame};
6
7use crate::error::FilterError;
8use crate::filter_inner::FilterGraphInner;
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub enum ToneMap {
17 Hable,
19 Reinhard,
21 Mobius,
23}
24
25impl ToneMap {
26 #[must_use]
28 pub const fn as_str(&self) -> &'static str {
29 match self {
30 Self::Hable => "hable",
31 Self::Reinhard => "reinhard",
32 Self::Mobius => "mobius",
33 }
34 }
35}
36
37#[derive(Debug, Clone, Copy, PartialEq, Eq)]
43pub enum HwAccel {
44 Cuda,
46 VideoToolbox,
48 Vaapi,
50}
51
52#[derive(Debug, Clone)]
59pub(crate) enum FilterStep {
60 Trim { start: f64, end: f64 },
62 Scale { width: u32, height: u32 },
64 Crop {
66 x: u32,
67 y: u32,
68 width: u32,
69 height: u32,
70 },
71 Overlay { x: i32, y: i32 },
73 FadeIn(Duration),
75 FadeOut(Duration),
77 Rotate(f64),
79 ToneMap(ToneMap),
81 Volume(f64),
83 Amix(usize),
85 Equalizer { band_hz: f64, gain_db: f64 },
87}
88
89impl FilterStep {
90 pub(crate) fn filter_name(&self) -> &'static str {
92 match self {
93 Self::Trim { .. } => "trim",
94 Self::Scale { .. } => "scale",
95 Self::Crop { .. } => "crop",
96 Self::Overlay { .. } => "overlay",
97 Self::FadeIn(_) | Self::FadeOut(_) => "fade",
98 Self::Rotate(_) => "rotate",
99 Self::ToneMap(_) => "tonemap",
100 Self::Volume(_) => "volume",
101 Self::Amix(_) => "amix",
102 Self::Equalizer { .. } => "equalizer",
103 }
104 }
105
106 pub(crate) fn args(&self) -> String {
108 match self {
109 Self::Trim { start, end } => format!("start={start}:end={end}"),
110 Self::Scale { width, height } => format!("w={width}:h={height}"),
111 Self::Crop {
112 x,
113 y,
114 width,
115 height,
116 } => {
117 format!("x={x}:y={y}:w={width}:h={height}")
118 }
119 Self::Overlay { x, y } => format!("x={x}:y={y}"),
120 Self::FadeIn(d) => format!("type=in:duration={}", d.as_secs_f64()),
121 Self::FadeOut(d) => format!("type=out:duration={}", d.as_secs_f64()),
122 Self::Rotate(degrees) => {
123 format!("angle={}", degrees.to_radians())
124 }
125 Self::ToneMap(algorithm) => format!("tonemap={}", algorithm.as_str()),
126 Self::Volume(db) => format!("volume={db}dB"),
127 Self::Amix(inputs) => format!("inputs={inputs}"),
128 Self::Equalizer { band_hz, gain_db } => {
129 format!("f={band_hz}:width_type=o:width=2:g={gain_db}")
130 }
131 }
132 }
133}
134
135#[derive(Debug, Default)]
153pub struct FilterGraphBuilder {
154 steps: Vec<FilterStep>,
155 hw: Option<HwAccel>,
156}
157
158impl FilterGraphBuilder {
159 #[must_use]
161 pub fn new() -> Self {
162 Self::default()
163 }
164
165 #[must_use]
169 pub fn trim(mut self, start: f64, end: f64) -> Self {
170 self.steps.push(FilterStep::Trim { start, end });
171 self
172 }
173
174 #[must_use]
176 pub fn scale(mut self, width: u32, height: u32) -> Self {
177 self.steps.push(FilterStep::Scale { width, height });
178 self
179 }
180
181 #[must_use]
183 pub fn crop(mut self, x: u32, y: u32, width: u32, height: u32) -> Self {
184 self.steps.push(FilterStep::Crop {
185 x,
186 y,
187 width,
188 height,
189 });
190 self
191 }
192
193 #[must_use]
195 pub fn overlay(mut self, x: i32, y: i32) -> Self {
196 self.steps.push(FilterStep::Overlay { x, y });
197 self
198 }
199
200 #[must_use]
202 pub fn fade_in(mut self, duration: Duration) -> Self {
203 self.steps.push(FilterStep::FadeIn(duration));
204 self
205 }
206
207 #[must_use]
209 pub fn fade_out(mut self, duration: Duration) -> Self {
210 self.steps.push(FilterStep::FadeOut(duration));
211 self
212 }
213
214 #[must_use]
216 pub fn rotate(mut self, degrees: f64) -> Self {
217 self.steps.push(FilterStep::Rotate(degrees));
218 self
219 }
220
221 #[must_use]
223 pub fn tone_map(mut self, algorithm: ToneMap) -> Self {
224 self.steps.push(FilterStep::ToneMap(algorithm));
225 self
226 }
227
228 #[must_use]
232 pub fn volume(mut self, gain_db: f64) -> Self {
233 self.steps.push(FilterStep::Volume(gain_db));
234 self
235 }
236
237 #[must_use]
239 pub fn amix(mut self, inputs: usize) -> Self {
240 self.steps.push(FilterStep::Amix(inputs));
241 self
242 }
243
244 #[must_use]
246 pub fn equalizer(mut self, band_hz: f64, gain_db: f64) -> Self {
247 self.steps.push(FilterStep::Equalizer { band_hz, gain_db });
248 self
249 }
250
251 #[must_use]
258 pub fn hardware(mut self, hw: HwAccel) -> Self {
259 self.hw = Some(hw);
260 self
261 }
262
263 pub fn build(self) -> Result<FilterGraph, FilterError> {
274 if self.steps.is_empty() {
275 return Err(FilterError::BuildFailed);
276 }
277 crate::filter_inner::validate_filter_steps(&self.steps)?;
278 Ok(FilterGraph {
279 inner: FilterGraphInner::new(self.steps, self.hw),
280 })
281 }
282}
283
284pub struct FilterGraph {
310 inner: FilterGraphInner,
311}
312
313impl std::fmt::Debug for FilterGraph {
314 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
315 f.debug_struct("FilterGraph").finish_non_exhaustive()
316 }
317}
318
319impl FilterGraph {
320 #[must_use]
322 pub fn builder() -> FilterGraphBuilder {
323 FilterGraphBuilder::new()
324 }
325
326 pub fn push_video(&mut self, slot: usize, frame: &VideoFrame) -> Result<(), FilterError> {
337 self.inner.push_video(slot, frame)
338 }
339
340 pub fn pull_video(&mut self) -> Result<Option<VideoFrame>, FilterError> {
349 self.inner.pull_video()
350 }
351
352 pub fn push_audio(&mut self, slot: usize, frame: &AudioFrame) -> Result<(), FilterError> {
363 self.inner.push_audio(slot, frame)
364 }
365
366 pub fn pull_audio(&mut self) -> Result<Option<AudioFrame>, FilterError> {
375 self.inner.pull_audio()
376 }
377}
378
379#[cfg(test)]
382mod tests {
383 use super::*;
384
385 #[test]
386 fn filter_step_scale_should_produce_correct_args() {
387 let step = FilterStep::Scale {
388 width: 1280,
389 height: 720,
390 };
391 assert_eq!(step.filter_name(), "scale");
392 assert_eq!(step.args(), "w=1280:h=720");
393 }
394
395 #[test]
396 fn filter_step_trim_should_produce_correct_args() {
397 let step = FilterStep::Trim {
398 start: 10.0,
399 end: 30.0,
400 };
401 assert_eq!(step.filter_name(), "trim");
402 assert_eq!(step.args(), "start=10:end=30");
403 }
404
405 #[test]
406 fn filter_step_volume_should_produce_correct_args() {
407 let step = FilterStep::Volume(-6.0);
408 assert_eq!(step.filter_name(), "volume");
409 assert_eq!(step.args(), "volume=-6dB");
410 }
411
412 #[test]
413 fn tone_map_variants_should_have_correct_names() {
414 assert_eq!(ToneMap::Hable.as_str(), "hable");
415 assert_eq!(ToneMap::Reinhard.as_str(), "reinhard");
416 assert_eq!(ToneMap::Mobius.as_str(), "mobius");
417 }
418
419 #[test]
420 fn builder_empty_steps_should_return_error() {
421 let result = FilterGraph::builder().build();
422 assert!(
423 matches!(result, Err(FilterError::BuildFailed)),
424 "expected BuildFailed, got {result:?}"
425 );
426 }
427
428 #[test]
429 fn filter_step_overlay_should_produce_correct_args() {
430 let step = FilterStep::Overlay { x: 10, y: 20 };
431 assert_eq!(step.filter_name(), "overlay");
432 assert_eq!(step.args(), "x=10:y=20");
433 }
434
435 #[test]
436 fn filter_step_crop_should_produce_correct_args() {
437 let step = FilterStep::Crop {
438 x: 0,
439 y: 0,
440 width: 640,
441 height: 360,
442 };
443 assert_eq!(step.filter_name(), "crop");
444 assert_eq!(step.args(), "x=0:y=0:w=640:h=360");
445 }
446
447 #[test]
448 fn filter_step_fade_in_should_produce_correct_args() {
449 let step = FilterStep::FadeIn(Duration::from_secs(1));
450 assert_eq!(step.filter_name(), "fade");
451 assert_eq!(step.args(), "type=in:duration=1");
452 }
453
454 #[test]
455 fn filter_step_fade_out_should_produce_correct_args() {
456 let step = FilterStep::FadeOut(Duration::from_secs(2));
457 assert_eq!(step.filter_name(), "fade");
458 assert_eq!(step.args(), "type=out:duration=2");
459 }
460
461 #[test]
462 fn filter_step_rotate_should_produce_correct_args() {
463 let step = FilterStep::Rotate(90.0);
464 assert_eq!(step.filter_name(), "rotate");
465 assert_eq!(step.args(), format!("angle={}", 90_f64.to_radians()));
466 }
467
468 #[test]
469 fn filter_step_tone_map_should_produce_correct_args() {
470 let step = FilterStep::ToneMap(ToneMap::Hable);
471 assert_eq!(step.filter_name(), "tonemap");
472 assert_eq!(step.args(), "tonemap=hable");
473 }
474
475 #[test]
476 fn filter_step_amix_should_produce_correct_args() {
477 let step = FilterStep::Amix(3);
478 assert_eq!(step.filter_name(), "amix");
479 assert_eq!(step.args(), "inputs=3");
480 }
481
482 #[test]
483 fn filter_step_equalizer_should_produce_correct_args() {
484 let step = FilterStep::Equalizer {
485 band_hz: 1000.0,
486 gain_db: 3.0,
487 };
488 assert_eq!(step.filter_name(), "equalizer");
489 assert_eq!(step.args(), "f=1000:width_type=o:width=2:g=3");
490 }
491
492 #[test]
493 fn builder_steps_should_accumulate_in_order() {
494 let result = FilterGraph::builder()
495 .trim(0.0, 5.0)
496 .scale(1280, 720)
497 .volume(-3.0)
498 .build();
499 assert!(
500 result.is_ok(),
501 "builder with multiple valid steps must succeed, got {result:?}"
502 );
503 }
504
505 #[test]
506 fn builder_with_valid_steps_should_succeed() {
507 let result = FilterGraph::builder().scale(1280, 720).build();
508 assert!(
509 result.is_ok(),
510 "builder with a known filter step must succeed, got {result:?}"
511 );
512 }
513}