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 /// Creates a `FilterGraph` from a pre-built [`FilterGraphInner`].
63 ///
64 /// Used by [`MultiTrackComposer`](crate::MultiTrackComposer) and
65 /// [`MultiTrackAudioMixer`](crate::MultiTrackAudioMixer) to wrap
66 /// source-only filter graphs that need no external `buffersrc`.
67 pub(crate) fn from_prebuilt(inner: FilterGraphInner) -> Self {
68 Self {
69 inner,
70 output_resolution: None,
71 pending_animations: Vec::new(),
72 }
73 }
74
75 /// Creates a `FilterGraph` from a pre-built [`FilterGraphInner`] with
76 /// animation entries accumulated during graph construction.
77 ///
78 /// Used by [`MultiTrackAudioMixer`](crate::MultiTrackAudioMixer) when
79 /// one or more tracks have an animated `volume` field.
80 pub(crate) fn from_prebuilt_animated(
81 inner: FilterGraphInner,
82 animations: Vec<AnimationEntry>,
83 ) -> Self {
84 Self {
85 inner,
86 output_resolution: None,
87 pending_animations: animations,
88 }
89 }
90
91 /// Applies all registered animation entries at time `t`.
92 ///
93 /// Call this before each [`pull_video`](Self::pull_video) on source-only
94 /// graphs (e.g. from [`MultiTrackComposer`](crate::MultiTrackComposer)) to
95 /// update animated filter parameters for the next frame.
96 ///
97 /// On graphs that use [`push_video`](Self::push_video), animations are
98 /// applied automatically at the pushed frame's PTS — `tick` is not needed.
99 pub fn tick(&mut self, t: Duration) {
100 if !self.pending_animations.is_empty() {
101 self.inner.apply_animations(&self.pending_animations, t);
102 }
103 }
104
105 /// Returns the output resolution produced by this graph's `scale` filter step,
106 /// if one was configured.
107 ///
108 /// When multiple `scale` steps are chained, the **last** one's dimensions are
109 /// returned. Returns `None` when no `scale` step was added.
110 #[must_use]
111 pub fn output_resolution(&self) -> Option<(u32, u32)> {
112 self.output_resolution
113 }
114
115 /// Push a video frame into input slot `slot`.
116 ///
117 /// On the first call the filter graph is initialised using this frame's
118 /// format, resolution, and time base.
119 ///
120 /// All registered animation entries are evaluated at the frame's PTS and
121 /// applied to the live graph via `avfilter_graph_send_command` before the
122 /// frame is pushed.
123 ///
124 /// # Errors
125 ///
126 /// - [`FilterError::InvalidInput`] if `slot` is out of range.
127 /// - [`FilterError::BuildFailed`] if the graph cannot be initialised.
128 /// - [`FilterError::ProcessFailed`] if the `FFmpeg` push fails.
129 pub fn push_video(&mut self, slot: usize, frame: &VideoFrame) -> Result<(), FilterError> {
130 if !self.pending_animations.is_empty() {
131 let t = frame.timestamp().as_duration();
132 self.inner.apply_animations(&self.pending_animations, t);
133 }
134 self.inner.push_video(slot, frame)
135 }
136
137 /// Pull the next filtered video frame, if one is available.
138 ///
139 /// Returns `None` when the internal `FFmpeg` buffer is empty (EAGAIN) or
140 /// at end-of-stream.
141 ///
142 /// # Errors
143 ///
144 /// Returns [`FilterError::ProcessFailed`] on an unexpected `FFmpeg` error.
145 pub fn pull_video(&mut self) -> Result<Option<VideoFrame>, FilterError> {
146 self.inner.pull_video()
147 }
148
149 /// Push an audio frame into input slot `slot`.
150 ///
151 /// On the first call the audio filter graph is initialised using this
152 /// frame's format, sample rate, and channel count.
153 ///
154 /// All registered animation entries are evaluated at the frame's PTS and
155 /// applied to the live graph via `avfilter_graph_send_command` before the
156 /// frame is pushed.
157 ///
158 /// # Errors
159 ///
160 /// - [`FilterError::InvalidInput`] if `slot` is out of range.
161 /// - [`FilterError::BuildFailed`] if the graph cannot be initialised.
162 /// - [`FilterError::ProcessFailed`] if the `FFmpeg` push fails.
163 pub fn push_audio(&mut self, slot: usize, frame: &AudioFrame) -> Result<(), FilterError> {
164 if !self.pending_animations.is_empty() {
165 let t = frame.timestamp().as_duration();
166 self.inner.apply_animations(&self.pending_animations, t);
167 }
168 self.inner.push_audio(slot, frame)
169 }
170
171 /// Pull the next filtered audio frame, if one is available.
172 ///
173 /// Returns `None` when the internal `FFmpeg` buffer is empty (EAGAIN) or
174 /// at end-of-stream.
175 ///
176 /// # Errors
177 ///
178 /// Returns [`FilterError::ProcessFailed`] on an unexpected `FFmpeg` error.
179 pub fn pull_audio(&mut self) -> Result<Option<AudioFrame>, FilterError> {
180 self.inner.pull_audio()
181 }
182}