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}