Skip to main content

ff_filter/graph/
graph.rs

1//! [`FilterGraph`] struct definition and push/pull implementations.
2
3use std::time::Duration;
4
5use ff_format::{AudioFrame, VideoFrame};
6
7use crate::animation::AnimationEntry;
8use crate::error::FilterError;
9use crate::filter_inner::FilterGraphInner;
10
11use super::builder::FilterGraphBuilder;
12
13// FilterGraph
14
15/// An `FFmpeg` libavfilter filter graph.
16///
17/// Constructed via [`FilterGraph::builder()`].  The underlying `AVFilterGraph` is
18/// initialised lazily on the first push call, deriving format information from
19/// the first frame.
20///
21/// # Examples
22///
23/// ```ignore
24/// use ff_filter::FilterGraph;
25///
26/// let mut graph = FilterGraph::builder()
27///     .scale(1280, 720)
28///     .build()?;
29///
30/// // Push decoded frames in …
31/// graph.push_video(0, &video_frame)?;
32///
33/// // … and pull filtered frames out.
34/// while let Some(frame) = graph.pull_video()? {
35///     // use frame
36/// }
37/// ```
38pub struct FilterGraph {
39    pub(crate) inner: FilterGraphInner,
40    pub(crate) output_resolution: Option<(u32, u32)>,
41    /// Animation entries registered via animated builder methods (e.g.
42    /// `crop_animated`, `gblur_animated`, `eq_animated`).
43    ///
44    /// Evaluated on every `push_video` / `push_audio` call and applied to
45    /// the live filter graph via `avfilter_graph_send_command`.
46    pub(crate) pending_animations: Vec<AnimationEntry>,
47}
48
49impl std::fmt::Debug for FilterGraph {
50    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
51        f.debug_struct("FilterGraph").finish_non_exhaustive()
52    }
53}
54
55impl FilterGraph {
56    /// Create a new builder.
57    #[must_use]
58    pub fn builder() -> FilterGraphBuilder {
59        FilterGraphBuilder::new()
60    }
61
62    /// Build a graph from a single `FFmpeg` filter description — the escape
63    /// hatch for reaching filters and graph shapes the typed API does not cover.
64    ///
65    /// ```ignore
66    /// use ff_filter::FilterGraph;
67    ///
68    /// let mut graph = FilterGraph::parse_desc("scale=1280:720,hue=s=0")?;
69    /// ```
70    ///
71    /// **The typed API remains the preferred path.** A description carries **no
72    /// compile-time checking**: filter names, options and their meaning are all
73    /// opaque strings until `FFmpeg` looks at them. Use
74    /// [`FilterGraph::builder`] and its typed methods wherever they cover what
75    /// you need.
76    ///
77    /// Shorthand for `FilterGraph::builder().parse_desc(desc).build()`. Use
78    /// [`FilterGraphBuilder::parse_desc`] instead to mix a description with
79    /// typed steps in one chain; its documentation describes exactly what is
80    /// checked, when, and the one-open-input / one-open-output requirement.
81    ///
82    /// # Errors
83    ///
84    /// Returns [`FilterError::InvalidConfig`] naming the description when it
85    /// cannot be parsed, names a filter this `FFmpeg` build does not have, sets
86    /// an option that filter rejects, or does not leave exactly one open input
87    /// and one open output.
88    pub fn parse_desc(desc: impl Into<String>) -> Result<Self, FilterError> {
89        Self::builder().parse_desc(desc).build()
90    }
91
92    /// Creates a `FilterGraph` from a pre-built [`FilterGraphInner`].
93    ///
94    /// Used by [`MultiTrackComposer`](crate::MultiTrackComposer) and
95    /// [`MultiTrackAudioMixer`](crate::MultiTrackAudioMixer) to wrap
96    /// source-only filter graphs that need no external `buffersrc`.
97    pub(crate) fn from_prebuilt(inner: FilterGraphInner) -> Self {
98        Self {
99            inner,
100            output_resolution: None,
101            pending_animations: Vec::new(),
102        }
103    }
104
105    /// Creates a `FilterGraph` from a pre-built [`FilterGraphInner`] with
106    /// animation entries accumulated during graph construction.
107    ///
108    /// Used by [`MultiTrackAudioMixer`](crate::MultiTrackAudioMixer) when
109    /// one or more tracks have an animated `volume` field.
110    pub(crate) fn from_prebuilt_animated(
111        inner: FilterGraphInner,
112        animations: Vec<AnimationEntry>,
113    ) -> Self {
114        Self {
115            inner,
116            output_resolution: None,
117            pending_animations: animations,
118        }
119    }
120
121    /// Applies all registered animation entries at time `t`.
122    ///
123    /// Call this before each [`pull_video`](Self::pull_video) on source-only
124    /// graphs (e.g. from [`MultiTrackComposer`](crate::MultiTrackComposer)) to
125    /// update animated filter parameters for the next frame.
126    ///
127    /// On graphs that use [`push_video`](Self::push_video), animations are
128    /// applied automatically at the pushed frame's PTS — `tick` is not needed.
129    pub fn tick(&mut self, t: Duration) {
130        if !self.pending_animations.is_empty() {
131            self.inner.apply_animations(&self.pending_animations, t);
132        }
133    }
134
135    /// Returns the output resolution produced by this graph's `scale` filter step,
136    /// if one was configured.
137    ///
138    /// When multiple `scale` steps are chained, the **last** one's dimensions are
139    /// returned. Returns `None` when no `scale` step was added.
140    #[must_use]
141    pub fn output_resolution(&self) -> Option<(u32, u32)> {
142        self.output_resolution
143    }
144
145    /// Push a video frame into input slot `slot`.
146    ///
147    /// On the first call the filter graph is initialised using this frame's
148    /// format, resolution, and time base.
149    ///
150    /// All registered animation entries are evaluated at the frame's PTS and
151    /// applied to the live graph via `avfilter_graph_send_command` before the
152    /// frame is pushed.
153    ///
154    /// # Errors
155    ///
156    /// - [`FilterError::InvalidInput`] if `slot` is out of range.
157    /// - [`FilterError::BuildFailed`] if the graph cannot be initialised.
158    /// - [`FilterError::ProcessFailed`] if the `FFmpeg` push fails.
159    pub fn push_video(&mut self, slot: usize, frame: &VideoFrame) -> Result<(), FilterError> {
160        if !self.pending_animations.is_empty() {
161            let t = frame.timestamp().as_duration();
162            self.inner.apply_animations(&self.pending_animations, t);
163        }
164        self.inner.push_video(slot, frame)
165    }
166
167    /// Pull the next filtered video frame, if one is available.
168    ///
169    /// Returns `None` when the internal `FFmpeg` buffer is empty (EAGAIN) or
170    /// at end-of-stream.
171    ///
172    /// # Errors
173    ///
174    /// Returns [`FilterError::ProcessFailed`] on an unexpected `FFmpeg` error.
175    pub fn pull_video(&mut self) -> Result<Option<VideoFrame>, FilterError> {
176        self.inner.pull_video()
177    }
178
179    /// Push an audio frame into input slot `slot`.
180    ///
181    /// On the first call the audio filter graph is initialised using this
182    /// frame's format, sample rate, and channel count.
183    ///
184    /// All registered animation entries are evaluated at the frame's PTS and
185    /// applied to the live graph via `avfilter_graph_send_command` before the
186    /// frame is pushed.
187    ///
188    /// # Errors
189    ///
190    /// - [`FilterError::InvalidInput`] if `slot` is out of range.
191    /// - [`FilterError::BuildFailed`] if the graph cannot be initialised.
192    /// - [`FilterError::ProcessFailed`] if the `FFmpeg` push fails.
193    pub fn push_audio(&mut self, slot: usize, frame: &AudioFrame) -> Result<(), FilterError> {
194        if !self.pending_animations.is_empty() {
195            let t = frame.timestamp().as_duration();
196            self.inner.apply_animations(&self.pending_animations, t);
197        }
198        self.inner.push_audio(slot, frame)
199    }
200
201    /// Pull the next filtered audio frame, if one is available.
202    ///
203    /// Returns `None` when the internal `FFmpeg` buffer is empty (EAGAIN) or
204    /// at end-of-stream.
205    ///
206    /// # Errors
207    ///
208    /// Returns [`FilterError::ProcessFailed`] on an unexpected `FFmpeg` error.
209    pub fn pull_audio(&mut self) -> Result<Option<AudioFrame>, FilterError> {
210        self.inner.pull_audio()
211    }
212
213    /// Signal end-of-stream to the audio graph, flushing output still buffered
214    /// inside filters such as `atempo` (used by `pitch_shift` / `time_stretch`).
215    ///
216    /// Call this once after the final [`push_audio`](Self::push_audio) and
217    /// before draining the remaining frames with
218    /// [`pull_audio`](Self::pull_audio). A WSOLA filter like `atempo` holds its
219    /// tail until EOF, so without a flush the last frames never emerge. No-op if
220    /// no audio has been pushed yet.
221    pub fn flush_audio(&mut self) {
222        self.inner.flush_audio();
223    }
224}