ez_ffmpeg/core/context/ffmpeg_context_builder.rs
1use crate::core::context::input::Input;
2use crate::core::context::output::Output;
3use crate::core::context::ffmpeg_context::FfmpegContext;
4use crate::core::context::filter_complex::FilterComplex;
5
6/// A builder for constructing [`FfmpegContext`] objects with customized inputs,
7/// outputs, and filter configurations. Typically, you will start by calling
8/// [`FfmpegContext::builder()`], then chain methods to add inputs, outputs, or
9/// filter descriptions, and finally invoke [`build()`](FfmpegContextBuilder::build) to produce an `FfmpegContext`.
10///
11/// # Examples
12///
13/// ```rust
14/// // 1. Create a builder
15/// let builder = FfmpegContext::builder();
16///
17/// // 2. Add at least one input and one output
18/// let ffmpeg_context = builder
19/// .input("input.mp4")
20/// // 3. Optionally add filters and more inputs/outputs
21/// .filter_desc("hue=s=0") // Example FFmpeg filter
22/// .output("output.mp4")
23/// .build()
24/// .expect("Failed to build FfmpegContext");
25///
26/// // 4. Use ffmpeg_context with FfmpegScheduler (e.g., `ffmpeg_context.start()`).
27/// ```
28#[must_use]
29pub struct FfmpegContextBuilder {
30 independent_readrate: bool,
31 inputs: Vec<Input>,
32 filter_descs: Vec<FilterComplex>,
33 outputs: Vec<Output>,
34 copy_ts: bool,
35}
36
37impl FfmpegContextBuilder {
38
39 /// Creates a new, empty `FfmpegContextBuilder`. Generally, you won't call this
40 /// directly; instead, use [`FfmpegContext::builder()`] as your entry point.
41 ///
42 /// # Example
43 ///
44 /// ```rust
45 /// let builder = FfmpegContextBuilder::new();
46 /// ```
47 pub fn new() -> Self {
48 Self {
49 independent_readrate: false,
50 inputs: vec![],
51 filter_descs: vec![],
52 outputs: vec![],
53 copy_ts: false,
54 }
55 }
56
57 /// Enables independent read rate control for multiple inputs, specifically addressing issues
58 /// with sequential processing filters like 'concat'.
59 ///
60 /// # Core Problem Solved
61 ///
62 /// When processing multiple inputs sequentially with filters like 'concat', FFmpeg's default
63 /// read rate mechanism causes unintended behavior:
64 ///
65 /// 1. By default, FFmpeg initializes a single 'wallclock_start' timestamp at the beginning
66 /// of processing, which serves as the reference for calculating read speeds.
67 ///
68 /// 2. In sequential processing (like with concat filter), inputs are processed one after another:
69 /// - The first input starts immediately and is read at the specified rate
70 /// - Subsequent inputs remain locked until previous inputs finish processing
71 ///
72 /// 3. When later inputs are unlocked (which could be minutes or hours later), their read rate
73 /// is calculated using the original 'wallclock_start' time, causing them to be read far too
74 /// quickly - often at maximum speed regardless of the set readrate.
75 ///
76 /// 4. This rapid reading loads large amounts of data into memory too quickly, potentially
77 /// causing out-of-memory errors with large media files.
78 ///
79 /// # How This Fix Works
80 ///
81 /// When `independent_readrate()` is enabled:
82 /// - Each input gets its own effective 'wallclock_start' reference time when it begins processing
83 /// - This ensures each input maintains the specified read rate, regardless of when in the
84 /// sequence it's processed
85 /// - Memory usage becomes more consistent and predictable throughout the entire processing pipeline
86 ///
87 /// # Practical Example
88 ///
89 /// ```rust
90 /// let result = FfmpegContext::builder()
91 /// .independent_readrate() // Enable independent read rates
92 /// .input(Input::from("file1.mp4").set_readrate(1.0)) // First input at 1x speed
93 /// .input(Input::from("file2.mp4").set_readrate(1.0)) // Second input also at 1x speed
94 /// .input(Input::from("file3.mp4").set_readrate(1.0)) // Third input also at 1x speed
95 /// .filter_desc("[0:v][0:a][1:v][1:a][2:v][2:a]concat=n=3:v=1:a=1") // Concatenate all inputs
96 /// .output(output)
97 /// .build()
98 /// .unwrap();
99 /// ```
100 ///
101 /// In this example, without `independent_readrate()`, the second and third files would be read
102 /// much faster than intended after the first file completes. With this option enabled, each file
103 /// maintains its 1.0x read rate precisely when it begins processing.
104 ///
105 /// # When To Use
106 ///
107 /// This option is essential when:
108 /// - Using the 'concat' filter with multiple inputs
109 /// - Processing long-duration media files sequentially
110 /// - Setting specific read rates for inputs (via `set_readrate()`)
111 /// - Memory usage is a concern in your application
112 pub fn independent_readrate(mut self) -> Self {
113 self.independent_readrate = true;
114 self
115 }
116
117 /// Adds a single [`Input`] to the builder. This can be a file path, a URL,
118 /// or a custom input with callbacks.
119 ///
120 /// Calling this method multiple times adds multiple distinct inputs.
121 ///
122 /// # Parameters
123 /// - `input` - Anything convertible into an [`Input`], such as a `&str`, `String`,
124 /// or a custom callback-based `Input`.
125 ///
126 /// # Returns
127 /// A modified `FfmpegContextBuilder`, allowing method chaining.
128 ///
129 /// # Example
130 /// ```rust
131 /// let context = FfmpegContextBuilder::new()
132 /// .input("video.mp4")
133 /// .build()
134 /// .unwrap();
135 /// ```
136 pub fn input(mut self, input: impl Into<Input>) -> Self {
137 self.inputs.push(input.into());
138 self
139 }
140
141 /// Replaces the current list of inputs with a new collection.
142 ///
143 /// This method takes a `Vec` of items convertible into [`Input`]s and sets them
144 /// as the complete set of inputs for the builder. Any previously added inputs
145 /// will be discarded.
146 ///
147 /// # Parameters
148 /// - `inputs` - A vector of items (e.g. `&str`, `String`, custom callbacks)
149 /// that will be converted into `Input`s.
150 ///
151 /// # Returns
152 /// A modified `FfmpegContextBuilder`, allowing method chaining.
153 ///
154 /// # Example
155 /// ```rust
156 /// let inputs = vec!["input1.mp4", "input2.mp4"];
157 /// let context = FfmpegContextBuilder::new()
158 /// .inputs(inputs)
159 /// .build()
160 /// .unwrap();
161 /// ```
162 pub fn inputs(mut self, inputs: Vec<impl Into<Input>>) -> Self {
163 self.inputs = inputs.into_iter().map(|input| input.into()).collect();
164 self
165 }
166
167 /// Adds a single [`Output`] to the builder, representing a single output
168 /// destination (file path, URL, or custom write callback).
169 ///
170 /// Calling this multiple times adds multiple outputs (e.g., for transcoding
171 /// to different formats simultaneously).
172 ///
173 /// # Parameters
174 /// - `output` - Anything convertible into an [`Output`], such as a `&str`, `String`,
175 /// or a callback-based output.
176 ///
177 /// # Returns
178 /// A modified `FfmpegContextBuilder`, allowing method chaining.
179 ///
180 /// # Example
181 /// ```rust
182 /// let context = FfmpegContextBuilder::new()
183 /// .output("output.mp4")
184 /// .build()
185 /// .unwrap();
186 /// ```
187 pub fn output(mut self, output: impl Into<Output>) -> Self {
188 self.outputs.push(output.into());
189 self
190 }
191
192 /// Replaces the current list of outputs with a new collection.
193 ///
194 /// This method takes a `Vec` of items convertible into [`Output`] and sets them
195 /// as the complete set of outputs for the builder. Any previously added outputs
196 /// will be discarded.
197 ///
198 /// # Parameters
199 /// - `outputs` - A vector of items (e.g. `&str`, `String`, or custom callback-based
200 /// outputs) that will be converted into `Output`s.
201 ///
202 /// # Returns
203 /// A modified `FfmpegContextBuilder`, allowing method chaining.
204 ///
205 /// # Example
206 /// ```rust
207 /// let outputs = vec!["output1.mp4", "output2.mkv"];
208 /// let context = FfmpegContextBuilder::new()
209 /// .outputs(outputs)
210 /// .build()
211 /// .unwrap();
212 /// ```
213 pub fn outputs(mut self, outputs: Vec<impl Into<Output>>) -> Self {
214 self.outputs = outputs.into_iter().map(|output| output.into()).collect();
215 self
216 }
217
218 /// Adds a single filter description (e.g., `"hue=s=0"`, `"scale=1280:720"`)
219 /// that applies to one or more inputs. Each filter description can also
220 /// contain complex filter graphs.
221 ///
222 /// Internally, it's converted into a [`FilterComplex`] object. These filters
223 /// can further manipulate or route media streams before they reach the outputs.
224 ///
225 /// # Parameters
226 /// - `filter_desc` - A string or [`FilterComplex`] describing filter operations
227 /// in FFmpeg's filter syntax.
228 ///
229 /// # Returns
230 /// A modified `FfmpegContextBuilder`, allowing method chaining.
231 ///
232 /// # Example
233 /// ```rust
234 /// let context = FfmpegContextBuilder::new()
235 /// .input("input.mp4")
236 /// .filter_desc("hue=s=0") // Desaturate the video
237 /// .output("output_gray.mp4")
238 /// .build()
239 /// .unwrap();
240 /// ```
241 pub fn filter_desc(mut self, filter_desc: impl Into<FilterComplex>) -> Self {
242 self.filter_descs.push(filter_desc.into());
243 self
244 }
245
246 /// Replaces the current filter descriptions with a new list of them.
247 ///
248 /// This method takes a `Vec` of items convertible into [`FilterComplex`], allowing
249 /// you to specify multiple complex filter graphs or distinct filter operations
250 /// all at once. Any previously added filters will be discarded.
251 ///
252 /// # Parameters
253 /// - `filter_descs` - A vector of strings or [`FilterComplex`] objects that define
254 /// FFmpeg filter operations.
255 ///
256 /// # Returns
257 /// A modified `FfmpegContextBuilder`, allowing method chaining.
258 ///
259 /// # Example
260 /// ```rust
261 /// let filter_chains = vec!["scale=1280:720", "drawtext=fontfile=...:text='Watermark'"];
262 /// let context = FfmpegContextBuilder::new()
263 /// .input("input.mp4")
264 /// .filter_descs(filter_chains)
265 /// .output("output_scaled.mp4")
266 /// .build()
267 /// .unwrap();
268 /// ```
269 pub fn filter_descs(mut self, filter_descs: Vec<impl Into<FilterComplex>>) -> Self {
270 self.filter_descs = filter_descs.into_iter().map(|filter| filter.into()).collect();
271 self
272 }
273
274 /// Enables timestamp copying from input to output
275 ///
276 /// This method sets the `copy_ts` flag to true, which is equivalent to FFmpeg's `-copyts` option.
277 /// When enabled, timestamps from the input stream are preserved in the output stream without modification.
278 /// This is useful when you want to maintain the original timing information from the source media.
279 ///
280 /// # Example
281 /// ```
282 /// let builder = FfmpegContextBuilder::new()
283 /// .copyts();
284 /// ```
285 pub fn copyts(mut self) -> Self {
286 self.copy_ts = true;
287 self
288 }
289
290 /// Finalizes this builder, creating an [`FfmpegContext`] which can then be used
291 /// to run FFmpeg jobs via [`FfmpegContext::start()`](FfmpegContext::start) or by constructing an
292 /// [`FfmpegScheduler`](crate::FfmpegScheduler) yourself.
293 ///
294 /// # Errors
295 /// Returns an error if any configuration issues are found (e.g., invalid URL syntax,
296 /// conflicting filter settings, etc.).
297 /// (The actual validation depends on how [`FfmpegContext::new_with_independent_readrate`](FfmpegContext::new_with_independent_readrate) is implemented.)
298 ///
299 /// # Example
300 /// ```rust
301 /// let context = FfmpegContextBuilder::new()
302 /// .input("input1.mp4")
303 /// .input("input2.mp4")
304 /// .output("combined_output.mkv")
305 /// .build()
306 /// .expect("Failed to build FfmpegContext");
307 ///
308 /// // Use the context to start FFmpeg processing
309 /// let scheduler = context.start().expect("Failed to start FFmpeg job");
310 /// scheduler.wait().unwrap();
311 /// ```
312 pub fn build(self) -> crate::error::Result<FfmpegContext> {
313 FfmpegContext::new_with_options(
314 self.independent_readrate,
315 self.inputs,
316 self.filter_descs,
317 self.outputs,
318 self.copy_ts
319 )
320 }
321}