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)]
24pub enum ToneMap {
25 Hable,
30 Reinhard,
35 Mobius,
40}
41
42impl ToneMap {
43 #[must_use]
45 pub const fn as_str(&self) -> &'static str {
46 match self {
47 Self::Hable => "hable",
48 Self::Reinhard => "reinhard",
49 Self::Mobius => "mobius",
50 }
51 }
52}
53
54#[derive(Debug, Clone, Copy, PartialEq, Eq)]
60pub enum HwAccel {
61 Cuda,
63 VideoToolbox,
65 Vaapi,
67}
68
69#[derive(Debug, Clone)]
76pub(crate) enum FilterStep {
77 Trim { start: f64, end: f64 },
79 Scale { width: u32, height: u32 },
81 Crop {
83 x: u32,
84 y: u32,
85 width: u32,
86 height: u32,
87 },
88 Overlay { x: i32, y: i32 },
90 FadeIn(Duration),
92 FadeOut(Duration),
94 Rotate(f64),
96 ToneMap(ToneMap),
98 Volume(f64),
100 Amix(usize),
102 Equalizer { band_hz: f64, gain_db: f64 },
104}
105
106impl FilterStep {
107 pub(crate) fn filter_name(&self) -> &'static str {
109 match self {
110 Self::Trim { .. } => "trim",
111 Self::Scale { .. } => "scale",
112 Self::Crop { .. } => "crop",
113 Self::Overlay { .. } => "overlay",
114 Self::FadeIn(_) | Self::FadeOut(_) => "fade",
115 Self::Rotate(_) => "rotate",
116 Self::ToneMap(_) => "tonemap",
117 Self::Volume(_) => "volume",
118 Self::Amix(_) => "amix",
119 Self::Equalizer { .. } => "equalizer",
120 }
121 }
122
123 pub(crate) fn args(&self) -> String {
125 match self {
126 Self::Trim { start, end } => format!("start={start}:end={end}"),
127 Self::Scale { width, height } => format!("w={width}:h={height}"),
128 Self::Crop {
129 x,
130 y,
131 width,
132 height,
133 } => {
134 format!("x={x}:y={y}:w={width}:h={height}")
135 }
136 Self::Overlay { x, y } => format!("x={x}:y={y}"),
137 Self::FadeIn(d) => format!("type=in:duration={}", d.as_secs_f64()),
138 Self::FadeOut(d) => format!("type=out:duration={}", d.as_secs_f64()),
139 Self::Rotate(degrees) => {
140 format!("angle={}", degrees.to_radians())
141 }
142 Self::ToneMap(algorithm) => format!("tonemap={}", algorithm.as_str()),
143 Self::Volume(db) => format!("volume={db}dB"),
144 Self::Amix(inputs) => format!("inputs={inputs}"),
145 Self::Equalizer { band_hz, gain_db } => {
146 format!("f={band_hz}:width_type=o:width=2:g={gain_db}")
147 }
148 }
149 }
150}
151
152#[derive(Debug, Default)]
170pub struct FilterGraphBuilder {
171 steps: Vec<FilterStep>,
172 hw: Option<HwAccel>,
173}
174
175impl FilterGraphBuilder {
176 #[must_use]
178 pub fn new() -> Self {
179 Self::default()
180 }
181
182 #[must_use]
186 pub fn trim(mut self, start: f64, end: f64) -> Self {
187 self.steps.push(FilterStep::Trim { start, end });
188 self
189 }
190
191 #[must_use]
193 pub fn scale(mut self, width: u32, height: u32) -> Self {
194 self.steps.push(FilterStep::Scale { width, height });
195 self
196 }
197
198 #[must_use]
200 pub fn crop(mut self, x: u32, y: u32, width: u32, height: u32) -> Self {
201 self.steps.push(FilterStep::Crop {
202 x,
203 y,
204 width,
205 height,
206 });
207 self
208 }
209
210 #[must_use]
212 pub fn overlay(mut self, x: i32, y: i32) -> Self {
213 self.steps.push(FilterStep::Overlay { x, y });
214 self
215 }
216
217 #[must_use]
219 pub fn fade_in(mut self, duration: Duration) -> Self {
220 self.steps.push(FilterStep::FadeIn(duration));
221 self
222 }
223
224 #[must_use]
226 pub fn fade_out(mut self, duration: Duration) -> Self {
227 self.steps.push(FilterStep::FadeOut(duration));
228 self
229 }
230
231 #[must_use]
233 pub fn rotate(mut self, degrees: f64) -> Self {
234 self.steps.push(FilterStep::Rotate(degrees));
235 self
236 }
237
238 #[must_use]
240 pub fn tone_map(mut self, algorithm: ToneMap) -> Self {
241 self.steps.push(FilterStep::ToneMap(algorithm));
242 self
243 }
244
245 #[must_use]
249 pub fn volume(mut self, gain_db: f64) -> Self {
250 self.steps.push(FilterStep::Volume(gain_db));
251 self
252 }
253
254 #[must_use]
256 pub fn amix(mut self, inputs: usize) -> Self {
257 self.steps.push(FilterStep::Amix(inputs));
258 self
259 }
260
261 #[must_use]
263 pub fn equalizer(mut self, band_hz: f64, gain_db: f64) -> Self {
264 self.steps.push(FilterStep::Equalizer { band_hz, gain_db });
265 self
266 }
267
268 #[must_use]
275 pub fn hardware(mut self, hw: HwAccel) -> Self {
276 self.hw = Some(hw);
277 self
278 }
279
280 pub fn build(self) -> Result<FilterGraph, FilterError> {
291 if self.steps.is_empty() {
292 return Err(FilterError::BuildFailed);
293 }
294
295 for step in &self.steps {
300 if let FilterStep::Overlay { x, y } = step
301 && (*x < 0 || *y < 0)
302 {
303 return Err(FilterError::InvalidConfig {
304 reason: format!(
305 "overlay position ({x}, {y}) is off-screen; \
306 ensure the watermark fits within the video dimensions"
307 ),
308 });
309 }
310 }
311
312 crate::filter_inner::validate_filter_steps(&self.steps)?;
313 let output_resolution = self.steps.iter().rev().find_map(|s| {
314 if let FilterStep::Scale { width, height } = s {
315 Some((*width, *height))
316 } else {
317 None
318 }
319 });
320 Ok(FilterGraph {
321 inner: FilterGraphInner::new(self.steps, self.hw),
322 output_resolution,
323 })
324 }
325}
326
327pub struct FilterGraph {
353 inner: FilterGraphInner,
354 output_resolution: Option<(u32, u32)>,
355}
356
357impl std::fmt::Debug for FilterGraph {
358 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
359 f.debug_struct("FilterGraph").finish_non_exhaustive()
360 }
361}
362
363impl FilterGraph {
364 #[must_use]
366 pub fn builder() -> FilterGraphBuilder {
367 FilterGraphBuilder::new()
368 }
369
370 #[must_use]
376 pub fn output_resolution(&self) -> Option<(u32, u32)> {
377 self.output_resolution
378 }
379
380 pub fn push_video(&mut self, slot: usize, frame: &VideoFrame) -> Result<(), FilterError> {
391 self.inner.push_video(slot, frame)
392 }
393
394 pub fn pull_video(&mut self) -> Result<Option<VideoFrame>, FilterError> {
403 self.inner.pull_video()
404 }
405
406 pub fn push_audio(&mut self, slot: usize, frame: &AudioFrame) -> Result<(), FilterError> {
417 self.inner.push_audio(slot, frame)
418 }
419
420 pub fn pull_audio(&mut self) -> Result<Option<AudioFrame>, FilterError> {
429 self.inner.pull_audio()
430 }
431}
432
433#[cfg(test)]
436mod tests {
437 use super::*;
438
439 #[test]
440 fn filter_step_scale_should_produce_correct_args() {
441 let step = FilterStep::Scale {
442 width: 1280,
443 height: 720,
444 };
445 assert_eq!(step.filter_name(), "scale");
446 assert_eq!(step.args(), "w=1280:h=720");
447 }
448
449 #[test]
450 fn filter_step_trim_should_produce_correct_args() {
451 let step = FilterStep::Trim {
452 start: 10.0,
453 end: 30.0,
454 };
455 assert_eq!(step.filter_name(), "trim");
456 assert_eq!(step.args(), "start=10:end=30");
457 }
458
459 #[test]
460 fn filter_step_volume_should_produce_correct_args() {
461 let step = FilterStep::Volume(-6.0);
462 assert_eq!(step.filter_name(), "volume");
463 assert_eq!(step.args(), "volume=-6dB");
464 }
465
466 #[test]
467 fn tone_map_variants_should_have_correct_names() {
468 assert_eq!(ToneMap::Hable.as_str(), "hable");
469 assert_eq!(ToneMap::Reinhard.as_str(), "reinhard");
470 assert_eq!(ToneMap::Mobius.as_str(), "mobius");
471 }
472
473 #[test]
474 fn builder_empty_steps_should_return_error() {
475 let result = FilterGraph::builder().build();
476 assert!(
477 matches!(result, Err(FilterError::BuildFailed)),
478 "expected BuildFailed, got {result:?}"
479 );
480 }
481
482 #[test]
483 fn filter_step_overlay_should_produce_correct_args() {
484 let step = FilterStep::Overlay { x: 10, y: 20 };
485 assert_eq!(step.filter_name(), "overlay");
486 assert_eq!(step.args(), "x=10:y=20");
487 }
488
489 #[test]
490 fn filter_step_crop_should_produce_correct_args() {
491 let step = FilterStep::Crop {
492 x: 0,
493 y: 0,
494 width: 640,
495 height: 360,
496 };
497 assert_eq!(step.filter_name(), "crop");
498 assert_eq!(step.args(), "x=0:y=0:w=640:h=360");
499 }
500
501 #[test]
502 fn filter_step_fade_in_should_produce_correct_args() {
503 let step = FilterStep::FadeIn(Duration::from_secs(1));
504 assert_eq!(step.filter_name(), "fade");
505 assert_eq!(step.args(), "type=in:duration=1");
506 }
507
508 #[test]
509 fn filter_step_fade_out_should_produce_correct_args() {
510 let step = FilterStep::FadeOut(Duration::from_secs(2));
511 assert_eq!(step.filter_name(), "fade");
512 assert_eq!(step.args(), "type=out:duration=2");
513 }
514
515 #[test]
516 fn filter_step_rotate_should_produce_correct_args() {
517 let step = FilterStep::Rotate(90.0);
518 assert_eq!(step.filter_name(), "rotate");
519 assert_eq!(step.args(), format!("angle={}", 90_f64.to_radians()));
520 }
521
522 #[test]
523 fn filter_step_tone_map_should_produce_correct_args() {
524 let step = FilterStep::ToneMap(ToneMap::Hable);
525 assert_eq!(step.filter_name(), "tonemap");
526 assert_eq!(step.args(), "tonemap=hable");
527 }
528
529 #[test]
530 fn filter_step_amix_should_produce_correct_args() {
531 let step = FilterStep::Amix(3);
532 assert_eq!(step.filter_name(), "amix");
533 assert_eq!(step.args(), "inputs=3");
534 }
535
536 #[test]
537 fn filter_step_equalizer_should_produce_correct_args() {
538 let step = FilterStep::Equalizer {
539 band_hz: 1000.0,
540 gain_db: 3.0,
541 };
542 assert_eq!(step.filter_name(), "equalizer");
543 assert_eq!(step.args(), "f=1000:width_type=o:width=2:g=3");
544 }
545
546 #[test]
547 fn builder_steps_should_accumulate_in_order() {
548 let result = FilterGraph::builder()
549 .trim(0.0, 5.0)
550 .scale(1280, 720)
551 .volume(-3.0)
552 .build();
553 assert!(
554 result.is_ok(),
555 "builder with multiple valid steps must succeed, got {result:?}"
556 );
557 }
558
559 #[test]
560 fn builder_with_valid_steps_should_succeed() {
561 let result = FilterGraph::builder().scale(1280, 720).build();
562 assert!(
563 result.is_ok(),
564 "builder with a known filter step must succeed, got {result:?}"
565 );
566 }
567
568 #[test]
569 fn output_resolution_should_return_scale_dimensions() {
570 let fg = FilterGraph::builder().scale(1280, 720).build().unwrap();
571 assert_eq!(fg.output_resolution(), Some((1280, 720)));
572 }
573
574 #[test]
575 fn output_resolution_should_return_last_scale_when_chained() {
576 let fg = FilterGraph::builder()
577 .scale(1920, 1080)
578 .scale(1280, 720)
579 .build()
580 .unwrap();
581 assert_eq!(fg.output_resolution(), Some((1280, 720)));
582 }
583
584 #[test]
585 fn output_resolution_should_return_none_when_no_scale() {
586 let fg = FilterGraph::builder().trim(0.0, 5.0).build().unwrap();
587 assert_eq!(fg.output_resolution(), None);
588 }
589}