Skip to main content

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