Skip to main content

ez_ffmpeg/core/context/
output.rs

1use crate::filter::frame_pipeline::FramePipeline;
2use ffmpeg_sys_next::{AVRational, AVSampleFormat};
3use std::collections::HashMap;
4
5// Note: Output is Send if all callback fields are Send.
6// We require `+ Send` on callback types to ensure this.
7// Output is !Sync because FnMut callbacks require exclusive access.
8
9pub struct Output {
10    /// The URL of the output destination.
11    ///
12    /// This specifies where the output stream will be written. It can be:
13    /// - A local file path (e.g., `file:///path/to/output.mp4`).
14    /// - A network destination (e.g., `rtmp://example.com/live/stream`).
15    /// - Any other URL supported by FFmpeg (e.g., `udp://...`, `http://...`).
16    ///
17    /// The URL must be valid. If the URL is invalid or unsupported, the library will
18    /// return an error when attempting to initialize the output stream.
19    pub(crate) url: Option<String>,
20
21    /// A callback function for custom data writing.
22    ///
23    /// The `write_callback` function allows you to provide custom logic for handling data
24    /// output from the encoding process. This is useful for scenarios where the output is
25    /// not written directly to a standard destination (like a file or URL), but instead to
26    /// a custom data sink, such as an in-memory buffer or a custom network destination.
27    ///
28    /// The callback receives a buffer of encoded data (`buf: &[u8]`) and should return the number of bytes
29    /// successfully written. If an error occurs, a negative value should be returned. For example:
30    /// - `ffmpeg_sys_next::AVERROR(ffmpeg_sys_next::EIO)` indicates an I/O error.
31    ///
32    /// ### Parameters:
33    /// - `buf: &[u8]`: A buffer containing the encoded data to be written.
34    ///
35    /// ### Return Value:
36    /// - **Positive Value**: The number of bytes successfully written.
37    /// - **Negative Value**: Indicates an error occurred, such as:
38    ///   - `ffmpeg_sys_next::AVERROR(ffmpeg_sys_next::EIO)`: General I/O error.
39    ///   - Custom-defined error codes depending on your implementation.
40    ///
41    /// ### Example:
42    /// ```rust,ignore
43    /// fn custom_write_callback(buf: &[u8]) -> i32 {
44    ///     println!("Writing data: {} bytes", buf.len());
45    ///     buf.len() as i32 // Return the number of bytes successfully written
46    /// }
47    /// ```
48    /// ### Note:
49    /// It is recommended to set the `format` field to the desired output format (e.g., `mp4`, `flv`, etc.)
50    /// when using a custom `write_callback`. The `format` ensures that FFmpeg processes the output
51    /// correctly for the specified format.
52    pub(crate) write_callback: Option<Box<dyn FnMut(&[u8]) -> i32 + Send>>,
53
54    /// A callback function for custom seeking within the output stream.
55    ///
56    /// The `seek_callback` function allows custom logic for adjusting the write position in
57    /// the output stream. This is essential for formats that require seeking, such as `mp4`
58    /// and `mkv`, where metadata or index information must be updated at specific positions.
59    ///
60    /// If the output format requires seeking but no `seek_callback` is provided, the operation
61    /// may fail, resulting in errors such as:
62    /// ```text
63    /// [mp4 @ 0x...] muxer does not support non seekable output
64    /// ```
65    ///
66    /// **FFmpeg may invoke `seek_callback` from different threads, so thread safety is required.**
67    /// If the destination is a `File`, **wrap it in `Arc<Mutex<File>>`** to ensure safe access.
68    ///
69    /// ### Parameters:
70    /// - `offset: i64`: The target position in the output stream where seeking should occur.
71    /// - `whence: i32`: The seek mode, which determines how `offset` should be interpreted:
72    ///   - `ffmpeg_sys_next::SEEK_SET` (0) - Seek to an absolute position.
73    ///   - `ffmpeg_sys_next::SEEK_CUR` (1) - Seek relative to the current position.
74    ///   - `ffmpeg_sys_next::SEEK_END` (2) - Seek relative to the end of the output.
75    ///   - `ffmpeg_sys_next::AVSEEK_FLAG_BYTE` (2) - Seek using **byte offset** instead of timestamps.
76    ///   - `ffmpeg_sys_next::AVSEEK_SIZE` (65536) - Query the **total size** of the stream.
77    ///   - `ffmpeg_sys_next::AVSEEK_FORCE` (131072) - **Force seeking, even if normally restricted.**
78    ///
79    /// ### Return Value:
80    /// - **Positive Value**: The new offset position after seeking.
81    /// - **Negative Value**: An error occurred. Common errors include:
82    ///   - `ffmpeg_sys_next::AVERROR(ffmpeg_sys_next::ESPIPE)`: Seek is not supported.
83    ///   - `ffmpeg_sys_next::AVERROR(ffmpeg_sys_next::EIO)`: General I/O error.
84    ///
85    /// ### Example (Thread-safe seek callback using `Arc<Mutex<File>>`):
86    /// Since `FFmpeg` may call `write_callback` and `seek_callback` from different threads,
87    /// **use `Arc<Mutex<File>>` to ensure safe concurrent access.**
88    ///
89    /// ```rust,ignore
90    /// use std::fs::File;
91    /// use std::io::{Seek, SeekFrom};
92    /// use std::sync::{Arc, Mutex};
93    ///
94    /// let file = Arc::new(Mutex::new(File::create("output.mp4").expect("Failed to create file")));
95    ///
96    /// let seek_callback = {
97    ///     let file = Arc::clone(&file);
98    ///     Box::new(move |offset: i64, whence: i32| -> i64 {
99    ///         let mut file = file.lock().unwrap();
100    ///
101    ///         // ✅ Handle AVSEEK_SIZE: Return total file size
102    ///         if whence == ffmpeg_sys_next::AVSEEK_SIZE {
103    ///             if let Ok(size) = file.metadata().map(|m| m.len() as i64) {
104    ///                 println!("FFmpeg requested stream size: {}", size);
105    ///                 return size;
106    ///             }
107    ///             return ffmpeg_sys_next::AVERROR(ffmpeg_sys_next::EIO) as i64;
108    ///         }
109    ///
110    ///         // ✅ Ignore AVSEEK_FORCE flag
111    ///         let actual_whence = whence & !ffmpeg_sys_next::AVSEEK_FORCE;
112    ///
113    ///         // ✅ Handle AVSEEK_FLAG_BYTE: Perform byte-based seek
114    ///         if actual_whence & ffmpeg_sys_next::AVSEEK_FLAG_BYTE != 0 {
115    ///             println!("FFmpeg requested byte-based seeking. Seeking to byte offset: {}", offset);
116    ///             if let Ok(new_pos) = file.seek(SeekFrom::Start(offset as u64)) {
117    ///                 return new_pos as i64;
118    ///             }
119    ///             return ffmpeg_sys_next::AVERROR(ffmpeg_sys_next::EIO) as i64;
120    ///         }
121    ///
122    ///         // ✅ Standard seek modes
123    ///         let seek_result = match actual_whence {
124    ///             ffmpeg_sys_next::SEEK_SET => file.seek(SeekFrom::Start(offset as u64)),
125    ///             ffmpeg_sys_next::SEEK_CUR => file.seek(SeekFrom::Current(offset)),
126    ///             ffmpeg_sys_next::SEEK_END => file.seek(SeekFrom::End(offset)),
127    ///             _ => {
128    ///                 println!("Unsupported seek mode: {}", whence);
129    ///                 return ffmpeg_sys_next::AVERROR(ffmpeg_sys_next::ESPIPE) as i64;
130    ///             }
131    ///         };
132    ///
133    ///         match seek_result {
134    ///             Ok(new_pos) => {
135    ///                 println!("Seek successful, new position: {}", new_pos);
136    ///                 new_pos as i64
137    ///             }
138    ///             Err(e) => {
139    ///                 println!("Seek failed: {}", e);
140    ///                 ffmpeg_sys_next::AVERROR(ffmpeg_sys_next::EIO) as i64
141    ///             }
142    ///         }
143    ///     })
144    /// };
145    /// ```
146    pub(crate) seek_callback: Option<Box<dyn FnMut(i64, i32) -> i64 + Send>>,
147
148    /// A pipeline specifying how frames will be processed **before encoding**.
149    ///
150    /// Once input data is decoded into [`Frame`]s, these frames pass through
151    /// this pipeline on their way to the encoder. The pipeline is composed of
152    /// one or more [`FrameFilter`]s, each providing a specific transformation,
153    /// effect, or filter (e.g., resizing, color correction, OpenGL shader
154    /// effects, etc.).
155    ///
156    /// If set to [`None`], no additional processing is applied — frames
157    /// are sent to the encoder as they are.
158    pub(crate) frame_pipelines: Option<Vec<FramePipeline>>,
159
160    /// Unparsed stream map specifications (user input stage)
161    /// These get parsed and expanded into stream_maps during outputs_bind()
162    pub(crate) stream_map_specs: Vec<StreamMapSpec>,
163
164    /// Expanded stream maps (FFmpeg-compatible, ready for use)
165    /// Each entry maps exactly one input stream to one output stream
166    pub(crate) stream_maps: Vec<StreamMap>,
167
168    /// The output format for the container.
169    ///
170    /// This field specifies the desired output format, such as `mp4`, `flv`, or `mkv`. If `None`, FFmpeg
171    /// will attempt to automatically detect the format based on the output URL or filename extension.
172    ///
173    /// The format can be specified explicitly for scenarios where the format detection is insufficient or
174    /// where you want to force a particular container format regardless of the URL or extension.
175    pub(crate) format: Option<String>,
176
177    /// The codec to be used for **video** encoding.
178    ///
179    /// If this field is `None`, FFmpeg will try to select an appropriate video codec based on the
180    /// output format or other settings. By setting this field to a specific codec (e.g., `"h264"`, `"hevc"`, etc.),
181    /// you can override FFmpeg’s default codec selection. If the specified codec is not available
182    /// in your FFmpeg build, an error will be returned during initialization.
183    pub(crate) video_codec: Option<String>,
184
185    /// The codec to be used for **audio** encoding.
186    ///
187    /// If this field is `None`, FFmpeg will try to select an appropriate audio codec based on the
188    /// output format or other settings. By providing a value (e.g., `"aac"`, `"mp3"`, etc.),
189    /// you override FFmpeg’s default codec choice. If the specified codec is not available
190    /// in your FFmpeg build, an error will be returned during initialization.
191    pub(crate) audio_codec: Option<String>,
192
193    /// The codec to be used for **subtitle** encoding.
194    ///
195    /// If this field is `None`, FFmpeg will try to select an appropriate subtitle codec based on
196    /// the output format or other settings. Setting this field (e.g., `"mov_text"` for MP4 subtitles)
197    /// forces FFmpeg to use the specified codec. If the chosen codec is not supported by your build of FFmpeg,
198    /// an error will be returned during initialization.
199    pub(crate) subtitle_codec: Option<String>,
200    pub(crate) start_time_us: Option<i64>,
201    pub(crate) recording_time_us: Option<i64>,
202    pub(crate) stop_time_us: Option<i64>,
203    pub(crate) framerate: Option<AVRational>,
204    /// Maximum output frame rate cap (`-fpsmax`): the native rate is kept and
205    /// only clamped when it exceeds the cap or is unknown
206    /// (ffmpeg_mux_init.c ms->max_frame_rate).
207    pub(crate) framerate_max: Option<AVRational>,
208    pub(crate) vsync_method: VSyncMethod,
209    pub(crate) bits_per_raw_sample: Option<i32>,
210    pub(crate) audio_sample_rate: Option<i32>,
211    pub(crate) audio_channels: Option<i32>,
212    pub(crate) audio_sample_fmt: Option<AVSampleFormat>,
213
214    // -q:v
215    // use fixed quality scale (VBR)
216    pub(crate) video_qscale: Option<i32>,
217
218    // -q:a
219    // set audio quality (codec-specific)
220    pub(crate) audio_qscale: Option<i32>,
221
222    /// Maximum number of **video** frames to encode (equivalent to `-frames:v` in FFmpeg).
223    ///
224    /// This option limits the number of **video** frames processed by the encoder.
225    ///
226    /// **Equivalent FFmpeg Command:**
227    /// ```sh
228    /// ffmpeg -i input.mp4 -frames:v 100 output.mp4
229    /// ```
230    ///
231    /// **Example Usage:**
232    /// ```rust,ignore
233    /// let output = Output::from("some_url")
234    ///     .set_max_video_frames(300);
235    /// ```
236    pub(crate) max_video_frames: Option<i64>,
237
238    /// Maximum number of **audio** frames to encode (equivalent to `-frames:a` in FFmpeg).
239    ///
240    /// This option limits the number of **audio** frames processed by the encoder.
241    ///
242    /// **Equivalent FFmpeg Command:**
243    /// ```sh
244    /// ffmpeg -i input.mp4 -frames:a 500 output.mp4
245    /// ```
246    ///
247    /// **Example Usage:**
248    /// ```rust,ignore
249    /// let output = Output::from("some_url")
250    ///     .set_max_audio_frames(500);
251    /// ```
252    pub(crate) max_audio_frames: Option<i64>,
253
254    /// Maximum number of **subtitle** frames to encode (equivalent to `-frames:s` in FFmpeg).
255    ///
256    /// This option limits the number of **subtitle** frames processed by the encoder.
257    ///
258    /// **Equivalent FFmpeg Command:**
259    /// ```sh
260    /// ffmpeg -i input.mp4 -frames:s 200 output.mp4
261    /// ```
262    ///
263    /// **Example Usage:**
264    /// ```rust,ignore
265    /// let output = Output::from("some_url")
266    ///     .set_max_subtitle_frames(200);
267    /// ```
268    pub(crate) max_subtitle_frames: Option<i64>,
269
270    /// Video encoder-specific options.
271    ///
272    /// This field stores key-value pairs for configuring the **video encoder**.
273    /// These options are passed to the video encoder before encoding begins.
274    ///
275    /// **Common Examples:**
276    /// - `crf=0` (for lossless quality in x264/x265)
277    /// - `preset=ultrafast` (for faster encoding speed in H.264)
278    /// - `tune=zerolatency` (for real-time streaming)
279    pub(crate) video_codec_opts: Option<HashMap<String, String>>,
280
281    /// Audio encoder-specific options.
282    ///
283    /// This field stores key-value pairs for configuring the **audio encoder**.
284    /// These options are passed to the audio encoder before encoding begins.
285    ///
286    /// **Common Examples:**
287    /// - `b=192k` (for setting bitrate in AAC/MP3)
288    /// - `ar=44100` (for setting sample rate)
289    pub(crate) audio_codec_opts: Option<HashMap<String, String>>,
290
291    /// Subtitle encoder-specific options.
292    ///
293    /// This field stores key-value pairs for configuring the **subtitle encoder**.
294    /// These options are passed to the subtitle encoder before encoding begins.
295    ///
296    /// **Common Examples:**
297    /// - `mov_text` (for MP4 subtitles)
298    /// - `srt` (for subtitle format)
299    pub(crate) subtitle_codec_opts: Option<HashMap<String, String>>,
300
301    /// The output format options for the container.
302    ///
303    /// This field stores additional format-specific options that are passed to the FFmpeg muxer.
304    /// It is a collection of key-value pairs that can modify the behavior of the output format.
305    ///
306    /// Common examples include:
307    /// - `movflags=faststart` (for MP4 files)
308    /// - `flvflags=no_duration_filesize` (for FLV files)
309    ///
310    /// These options are used when initializing the FFmpeg output format.
311    ///
312    /// **Example Usage:**
313    /// ```rust,ignore
314    /// let output = Output::from("some_url")
315    ///     .set_format_opt("movflags", "faststart");
316    /// ```
317    pub(crate) format_opts: Option<HashMap<String, String>>,
318
319    // ========== Metadata Fields ==========
320    /// Global metadata for the entire output file
321    pub(crate) global_metadata: Option<HashMap<String, String>>,
322
323    /// Stream-specific metadata with stream specifiers
324    /// Key: stream specifier string (e.g., "v:0", "a", "s:0")
325    /// Value: metadata key-value pairs for matching streams
326    /// During output initialization, each specifier is matched against actual streams
327    pub(crate) stream_metadata: Vec<(String, String, String)>, // (spec, key, value) tuples
328
329    /// Chapter-specific metadata, indexed by chapter index
330    pub(crate) chapter_metadata: HashMap<usize, HashMap<String, String>>,
331
332    /// Program-specific metadata, indexed by program index
333    pub(crate) program_metadata: HashMap<usize, HashMap<String, String>>,
334
335    /// Metadata mappings from input files
336    pub(crate) metadata_map: Vec<crate::core::metadata::MetadataMapping>,
337
338    /// Whether to automatically copy metadata from input files (default: true)
339    /// Replicates FFmpeg's default behavior of copying global and stream metadata
340    pub(crate) auto_copy_metadata: bool,
341
342    // ========== Stream Disable Flags (P1 Features) ==========
343    /// Disable video stream mapping (equivalent to `-vn` in FFmpeg).
344    /// When true, video streams will be excluded from automatic stream mapping.
345    pub(crate) video_disable: bool,
346
347    /// Disable audio stream mapping (equivalent to `-an` in FFmpeg).
348    /// When true, audio streams will be excluded from automatic stream mapping.
349    pub(crate) audio_disable: bool,
350
351    /// Disable subtitle stream mapping (equivalent to `-sn` in FFmpeg).
352    /// When true, subtitle streams will be excluded from automatic stream mapping.
353    pub(crate) subtitle_disable: bool,
354
355    /// Disable data stream mapping (equivalent to `-dn` in FFmpeg).
356    /// When true, data streams will be excluded from automatic stream mapping.
357    /// Data streams include things like timed metadata, chapter markers, etc.
358    pub(crate) data_disable: bool,
359
360    /// Output pixel format (equivalent to `-pix_fmt` in FFmpeg).
361    /// When set, forces the output video to use the specified pixel format.
362    /// Only effective when re-encoding (not when using stream copy).
363    pub(crate) pix_fmt: Option<String>,
364}
365
366#[derive(Copy, Clone, PartialEq)]
367pub enum VSyncMethod {
368    VsyncAuto,
369    VsyncCfr,
370    VsyncVfr,
371    VsyncPassthrough,
372    VsyncVscfr,
373}
374
375impl Output {
376    pub fn new(url: impl Into<String>) -> Self {
377        url.into().into()
378    }
379
380    /// Creates a new `Output` instance with a custom write callback and format string.
381    ///
382    /// This method initializes an `Output` object that uses a provided `write_callback` function
383    /// to handle the encoded data being written to the output stream. You can optionally specify
384    /// the desired output format via the `format` method.
385    ///
386    /// ### Parameters:
387    /// - `write_callback: fn(buf: &[u8]) -> i32`: A function that processes the provided buffer of
388    ///   encoded data and writes it to the destination. The function should return the number of bytes
389    ///   successfully written (positive value) or a negative value in case of error.
390    ///
391    /// ### Return Value:
392    /// - Returns a new `Output` instance configured with the specified `write_callback` function.
393    ///
394    /// ### Behavior of `write_callback`:
395    /// - **Positive Value**: Indicates the number of bytes successfully written.
396    /// - **Negative Value**: Indicates an error occurred. For example:
397    ///   - `ffmpeg_sys_next::AVERROR(ffmpeg_sys_next::EIO)`: Represents an input/output error.
398    ///   - Other custom-defined error codes can also be returned to signal specific issues.
399    ///
400    /// ### Example:
401    /// ```rust,ignore
402    /// let output = Output::new_by_write_callback(move |buf| {
403    ///     println!("Processing {} bytes of data for output", buf.len());
404    ///     buf.len() as i32 // Return the number of bytes processed
405    /// })
406    /// .set_format("mp4");
407    /// ```
408    pub fn new_by_write_callback<F>(write_callback: F) -> Self
409    where
410        F: FnMut(&[u8]) -> i32 + Send + 'static,
411    {
412        (Box::new(write_callback) as Box<dyn FnMut(&[u8]) -> i32 + Send>).into()
413    }
414
415    /// Sets a custom seek callback for the output stream.
416    ///
417    /// This function assigns a user-defined function that handles seeking within the output stream.
418    /// Seeking is required for certain formats (e.g., `mp4`, `mkv`) where metadata or index information
419    /// needs to be updated at specific positions in the file.
420    ///
421    /// **Why is `seek_callback` necessary?**
422    /// - Some formats (e.g., MP4) require `seek` operations to update metadata (`moov`, `mdat`).
423    /// - If no `seek_callback` is provided for formats that require seeking, FFmpeg will fail with:
424    ///   ```text
425    ///   [mp4 @ 0x...] muxer does not support non seekable output
426    ///   ```
427    /// - For streaming formats (`flv`, `ts`, `rtmp`, `hls`), seeking is **not required**.
428    ///
429    /// **FFmpeg may invoke `seek_callback` from different threads.**
430    /// - If using a `File` as the output, **wrap it in `Arc<Mutex<File>>`** to ensure thread-safe access.
431    ///
432    /// ### Parameters:
433    /// - `seek_callback: FnMut(i64, i32) -> i64`
434    ///   - `offset: i64`: The target seek position in the stream.
435    ///   - `whence: i32`: The seek mode determining how `offset` should be interpreted:
436    ///     - `ffmpeg_sys_next::SEEK_SET` (0): Seek to an absolute position.
437    ///     - `ffmpeg_sys_next::SEEK_CUR` (1): Seek relative to the current position.
438    ///     - `ffmpeg_sys_next::SEEK_END` (2): Seek relative to the end of the output.
439    ///     - `ffmpeg_sys_next::AVSEEK_FLAG_BYTE` (2): Seek using **byte offset** instead of timestamps.
440    ///     - `ffmpeg_sys_next::AVSEEK_SIZE` (65536): Query the **total size** of the stream.
441    ///     - `ffmpeg_sys_next::AVSEEK_FORCE` (131072): **Force seeking, even if normally restricted.**
442    ///
443    /// ### Return Value:
444    /// - **Positive Value**: The new offset position after seeking.
445    /// - **Negative Value**: An error occurred. Common errors include:
446    ///   - `ffmpeg_sys_next::AVERROR(ffmpeg_sys_next::ESPIPE)`: Seek is not supported.
447    ///   - `ffmpeg_sys_next::AVERROR(ffmpeg_sys_next::EIO)`: General I/O error.
448    ///
449    /// ### Example (Thread-safe seek callback using `Arc<Mutex<File>>`):
450    /// Since `FFmpeg` may call `write_callback` and `seek_callback` from different threads,
451    /// **use `Arc<Mutex<File>>` to ensure safe concurrent access.**
452    ///
453    /// ```rust,ignore
454    /// use std::fs::File;
455    /// use std::io::{Seek, SeekFrom, Write};
456    /// use std::sync::{Arc, Mutex};
457    ///
458    /// // ✅ Create a thread-safe file handle
459    /// let file = Arc::new(Mutex::new(File::create("output.mp4").expect("Failed to create file")));
460    ///
461    /// // ✅ Define the write callback (data writing logic)
462    /// let write_callback = {
463    ///     let file = Arc::clone(&file);
464    ///     move |buf: &[u8]| -> i32 {
465    ///         let mut file = file.lock().unwrap();
466    ///         match file.write_all(buf) {
467    ///             Ok(_) => buf.len() as i32,
468    ///             Err(e) => {
469    ///                 println!("Write error: {}", e);
470    ///                 ffmpeg_sys_next::AVERROR(ffmpeg_sys_next::EIO) as i32
471    ///             }
472    ///         }
473    ///     }
474    /// };
475    ///
476    /// // ✅ Define the seek callback (position adjustment logic)
477    /// let seek_callback = {
478    ///     let file = Arc::clone(&file);
479    ///     Box::new(move |offset: i64, whence: i32| -> i64 {
480    ///         let mut file = file.lock().unwrap();
481    ///
482    ///         match whence {
483    ///             // ✅ Handle AVSEEK_SIZE: Return total file size
484    ///             ffmpeg_sys_next::AVSEEK_SIZE => {
485    ///                 if let Ok(size) = file.metadata().map(|m| m.len() as i64) {
486    ///                     println!("FFmpeg requested stream size: {}", size);
487    ///                     return size;
488    ///                 }
489    ///                 return ffmpeg_sys_next::AVERROR(ffmpeg_sys_next::EIO) as i64;
490    ///             }
491    ///
492    ///             // ✅ Handle AVSEEK_FLAG_BYTE: Seek using byte offset
493    ///             ffmpeg_sys_next::AVSEEK_FLAG_BYTE => {
494    ///                 println!("FFmpeg requested byte-based seeking. Seeking to byte offset: {}", offset);
495    ///                 if let Ok(new_pos) = file.seek(SeekFrom::Start(offset as u64)) {
496    ///                     return new_pos as i64;
497    ///                 }
498    ///                 return ffmpeg_sys_next::AVERROR(ffmpeg_sys_next::EIO) as i64;
499    ///             }
500    ///
501    ///             // ✅ Standard seek modes
502    ///             ffmpeg_sys_next::SEEK_SET => file.seek(SeekFrom::Start(offset as u64)),
503    ///             ffmpeg_sys_next::SEEK_CUR => file.seek(SeekFrom::Current(offset)),
504    ///             ffmpeg_sys_next::SEEK_END => file.seek(SeekFrom::End(offset)),
505    ///             _ => Err(std::io::Error::new(std::io::ErrorKind::InvalidInput, "Unsupported seek mode")),
506    ///         }.map_or(ffmpeg_sys_next::AVERROR(ffmpeg_sys_next::EIO) as i64, |pos| pos as i64)
507    ///     })
508    /// };
509    ///
510    /// // ✅ Create an output with both callbacks
511    /// let output = Output::new_by_write_callback(write_callback, "mp4")
512    ///     .set_seek_callback(seek_callback);
513    /// ```
514    pub fn set_seek_callback<F>(mut self, seek_callback: F) -> Self
515    where
516        F: FnMut(i64, i32) -> i64 + Send + 'static,
517    {
518        self.seek_callback = Some(Box::new(seek_callback) as Box<dyn FnMut(i64, i32) -> i64 + Send>);
519        self
520    }
521
522    /// Sets the output format for the container.
523    ///
524    /// This method allows you to specify the output format for the container. If no format is specified,
525    /// FFmpeg will attempt to detect it automatically based on the file extension or output URL.
526    ///
527    /// ### Parameters:
528    /// - `format: &str`: A string specifying the desired output format (e.g., `mp4`, `flv`, `mkv`).
529    ///
530    /// ### Return Value:
531    /// - Returns the `Output` instance with the newly set format.
532    pub fn set_format(mut self, format: impl Into<String>) -> Self {
533        self.format = Some(format.into());
534        self
535    }
536
537    /// Sets the **video codec** to be used for encoding.
538    ///
539    /// # Arguments
540    /// * `video_codec` - A string slice representing the desired video codec (e.g., `"h264"`, `"hevc"`).
541    ///
542    /// # Returns
543    /// * `Self` - Returns the modified `Output` struct, allowing for method chaining.
544    ///
545    /// # Examples
546    /// ```rust,ignore
547    /// let output = Output::from("rtmp://localhost/live/stream")
548    ///     .set_video_codec("h264");
549    /// ```
550    pub fn set_video_codec(mut self, video_codec: impl Into<String>) -> Self {
551        self.video_codec = Some(video_codec.into());
552        self
553    }
554
555    /// Sets the **audio codec** to be used for encoding.
556    ///
557    /// # Arguments
558    /// * `audio_codec` - A string slice representing the desired audio codec (e.g., `"aac"`, `"mp3"`).
559    ///
560    /// # Returns
561    /// * `Self` - Returns the modified `Output` struct, allowing for method chaining.
562    ///
563    /// # Examples
564    /// ```rust,ignore
565    /// let output = Output::from("rtmp://localhost/live/stream")
566    ///     .set_audio_codec("aac");
567    /// ```
568    pub fn set_audio_codec(mut self, audio_codec: impl Into<String>) -> Self {
569        self.audio_codec = Some(audio_codec.into());
570        self
571    }
572
573    /// Sets the **subtitle codec** to be used for encoding.
574    ///
575    /// # Arguments
576    /// * `subtitle_codec` - A string slice representing the desired subtitle codec
577    ///   (e.g., `"mov_text"`, `"webvtt"`).
578    ///
579    /// # Returns
580    /// * `Self` - Returns the modified `Output` struct, allowing for method chaining.
581    ///
582    /// # Examples
583    /// ```rust,ignore
584    /// let output = Output::from("rtmp://localhost/live/stream")
585    ///     .set_subtitle_codec("mov_text");
586    /// ```
587    pub fn set_subtitle_codec(mut self, subtitle_codec: impl Into<String>) -> Self {
588        self.subtitle_codec = Some(subtitle_codec.into());
589        self
590    }
591
592    /// Replaces the entire frame-processing pipeline with a new sequence
593    /// of transformations for **pre-encoding** frames on this `Output`.
594    ///
595    /// This method clears any previously set pipelines and replaces them with the provided list.
596    ///
597    /// # Parameters
598    /// * `frame_pipelines` - A list of [`FramePipeline`] instances defining the
599    ///   transformations to apply before encoding.
600    ///
601    /// # Returns
602    /// * `Self` - Returns the modified `Output`, enabling method chaining.
603    ///
604    /// # Example
605    /// ```rust,ignore
606    /// let output = Output::from("some_url")
607    ///     .set_frame_pipelines(vec![
608    ///         FramePipelineBuilder::new(AVMediaType::AVMEDIA_TYPE_VIDEO).filter("opengl", Box::new(my_filter)),
609    ///         // Additional pipelines...
610    ///     ]);
611    /// ```
612    pub fn set_frame_pipelines(mut self, frame_pipelines: Vec<impl Into<FramePipeline>>) -> Self {
613        self.frame_pipelines = Some(
614            frame_pipelines
615                .into_iter()
616                .map(|frame_pipeline| frame_pipeline.into())
617                .collect(),
618        );
619        self
620    }
621
622    /// Adds a single [`FramePipeline`] to the existing pipeline list.
623    ///
624    /// If no pipelines are currently defined, this method creates a new pipeline list.
625    /// Otherwise, it appends the provided pipeline to the existing transformations.
626    ///
627    /// # Parameters
628    /// * `frame_pipeline` - A [`FramePipeline`] defining a transformation.
629    ///
630    /// # Returns
631    /// * `Self` - Returns the modified `Output`, enabling method chaining.
632    ///
633    /// # Example
634    /// ```rust,ignore
635    /// let output = Output::from("some_url")
636    ///     .add_frame_pipeline(FramePipelineBuilder::new(AVMediaType::AVMEDIA_TYPE_VIDEO).filter("opengl", Box::new(my_filter)).build())
637    ///     .add_frame_pipeline(FramePipelineBuilder::new(AVMediaType::AVMEDIA_TYPE_AUDIO).filter("my_custom_filter1", Box::new(...)).filter("my_custom_filter2", Box::new(...)));
638    /// ```
639    pub fn add_frame_pipeline(mut self, frame_pipeline: impl Into<FramePipeline>) -> Self {
640        if self.frame_pipelines.is_none() {
641            self.frame_pipelines = Some(vec![frame_pipeline.into()]);
642        } else {
643            self.frame_pipelines
644                .as_mut()
645                .unwrap()
646                .push(frame_pipeline.into());
647        }
648        self
649    }
650
651    /// Adds a **stream mapping** for a specific stream or stream type,
652    /// **re-encoding** it according to this output’s codec settings.
653    ///
654    /// # Linklabel (FFmpeg-like Specifier)
655    ///
656    /// This string typically follows `"<input_index>:<media_type>"` syntax:
657    /// - **`"0:v"`** – the video stream(s) from input #0.
658    /// - **`"1:a?"`** – audio from input #1, **ignore** if none present (due to `?`).
659    /// - Other possibilities include `"0:s"`, `"0:d"`, etc. for subtitles/data, optionally with `?`.
660    ///
661    /// By calling `add_stream_map`, **you force re-encoding** of the chosen stream(s).
662    /// If the user wants a bit-for-bit copy, see [`add_stream_map_with_copy`](Self::add_stream_map_with_copy).
663    ///
664    /// # Parameters
665    /// - `linklabel`: An FFmpeg-style specifier referencing the desired input index and
666    ///   media type, like `"0:v"`, `"1:a?"`, etc.
667    ///
668    /// # Returns
669    /// * `Self` - for chained method calls.
670    ///
671    /// # Example
672    /// ```rust,ignore
673    /// // Re-encode the video stream from input #0 (fail if no video).
674    /// let output = Output::from("output.mp4")
675    ///     .add_stream_map("0:v");
676    /// ```
677    pub fn add_stream_map(mut self, linklabel: impl Into<String>) -> Self {
678        self.stream_map_specs.push(linklabel.into().into());
679        self
680    }
681
682    /// Adds a **stream mapping** for a specific stream or stream type,
683    /// **copying** it bit-for-bit from the source without re-encoding.
684    ///
685    /// # Linklabel (FFmpeg-like Specifier)
686    ///
687    /// Follows the same `"<input_index>:<media_type>"` pattern as [`add_stream_map`](Self::add_stream_map):
688    /// - **`"0:a"`** – audio stream(s) from input #0.
689    /// - **`"0:a?"`** – same, but ignore errors if no audio exists.
690    /// - And so on for video (`v`), subtitles (`s`), attachments (`t`), etc.
691    ///
692    /// # Copy vs. Re-encode
693    ///
694    /// Here, `copy = true` by default, meaning the chosen stream(s) are passed through
695    /// **without** decoding/encoding. This generally **only** works if the source’s codec
696    /// is compatible with the container/format you’re outputting to.
697    /// If you require re-encoding (e.g., to ensure compatibility or apply filters),
698    /// use [`add_stream_map`](Self::add_stream_map).
699    ///
700    /// # Parameters
701    /// - `linklabel`: An FFmpeg-style specifier referencing the desired input index and
702    ///   media type, like `"0:v?"`.
703    ///
704    /// # Returns
705    /// * `Self` - for chained method calls.
706    ///
707    /// # Example
708    /// ```rust,ignore
709    /// // Copy the audio stream(s) from input #0 if present, no re-encode:
710    /// let output = Output::from("output.mkv")
711    ///     .add_stream_map_with_copy("0:a?");
712    /// ```
713    pub fn add_stream_map_with_copy(mut self, linklabel: impl Into<String>) -> Self {
714        self.stream_map_specs.push(StreamMapSpec {
715            linklabel: linklabel.into(),
716            copy: true,
717        });
718        self
719    }
720
721    /// Sets the **start time** (in microseconds) for output encoding.
722    ///
723    /// If this is set, FFmpeg will attempt to start encoding from the specified
724    /// timestamp in the input stream. This can be used to skip initial content.
725    ///
726    /// # Parameters
727    /// * `start_time_us` - The start time in microseconds.
728    ///
729    /// # Returns
730    /// * `Self` - The modified `Output`, allowing method chaining.
731    ///
732    /// # Example
733    /// ```rust,ignore
734    /// let output = Output::from("output.mp4")
735    ///     .set_start_time_us(2_000_000); // Start at 2 seconds
736    /// ```
737    pub fn set_start_time_us(mut self, start_time_us: i64) -> Self {
738        self.start_time_us = Some(start_time_us);
739        self
740    }
741
742    /// Sets the **recording time** (in microseconds) for output encoding.
743    ///
744    /// This indicates how many microseconds of data should be processed
745    /// (i.e., maximum duration to encode). Once this time is reached,
746    /// FFmpeg will stop encoding.
747    ///
748    /// # Parameters
749    /// * `recording_time_us` - The maximum duration (in microseconds) to process.
750    ///
751    /// # Returns
752    /// * `Self` - The modified `Output`, allowing method chaining.
753    ///
754    /// # Example
755    /// ```rust,ignore
756    /// let output = Output::from("output.mp4")
757    ///     .set_recording_time_us(5_000_000); // Record for 5 seconds
758    /// ```
759    pub fn set_recording_time_us(mut self, recording_time_us: i64) -> Self {
760        self.recording_time_us = Some(recording_time_us);
761        self
762    }
763
764    /// Sets a **stop time** (in microseconds) for output encoding.
765    ///
766    /// If set, FFmpeg will stop encoding once the input’s timestamp
767    /// surpasses this value. Effectively, encoding ends at this timestamp
768    /// regardless of remaining data.
769    ///
770    /// # Parameters
771    /// * `stop_time_us` - The timestamp (in microseconds) at which to stop.
772    ///
773    /// # Returns
774    /// * `Self` - The modified `Output`, allowing method chaining.
775    ///
776    /// # Example
777    /// ```rust,ignore
778    /// let output = Output::from("output.mp4")
779    ///     .set_stop_time_us(10_000_000); // Stop at 10 seconds
780    /// ```
781    pub fn set_stop_time_us(mut self, stop_time_us: i64) -> Self {
782        self.stop_time_us = Some(stop_time_us);
783        self
784    }
785
786    /// Sets a **target frame rate** (`AVRational`) for output encoding.
787    ///
788    /// This can force the output to use a specific frame rate (e.g., 30/1 for 30 FPS).
789    /// If unset, FFmpeg typically preserves the source frame rate or uses defaults
790    /// based on the selected codec/container.
791    ///
792    /// # Parameters
793    /// * `framerate` - An `AVRational` representing the desired frame rate
794    ///   numerator/denominator (e.g., `AVRational { num: 30, den: 1 }` for 30fps).
795    ///
796    /// # Returns
797    /// * `Self` - The modified `Output`, allowing method chaining.
798    ///
799    /// # Example
800    /// ```rust,ignore
801    /// use ffmpeg_sys_next::AVRational;
802    /// let output = Output::from("output.mp4")
803    ///     .set_framerate(AVRational { num: 30, den: 1 });
804    /// ```
805    pub fn set_framerate(mut self, framerate: AVRational) -> Self {
806        self.framerate = Some(framerate);
807        self
808    }
809
810    /// Sets a **maximum frame rate** cap for output encoding (`-fpsmax`).
811    ///
812    /// Unlike [`set_framerate`](Self::set_framerate), this does not force a
813    /// rate: the output keeps its native frame rate and is only clamped when
814    /// that rate exceeds the cap or cannot be determined
815    /// (ffmpeg_filter.c choose_out_timebase).
816    ///
817    /// # Parameters
818    /// * `framerate_max` - An `AVRational` upper bound (e.g.,
819    ///   `AVRational { num: 30, den: 1 }` for 30fps).
820    ///
821    /// # Returns
822    /// * `Self` - The modified `Output`, allowing method chaining.
823    ///
824    /// # Example
825    /// ```rust,ignore
826    /// use ffmpeg_sys_next::AVRational;
827    /// let output = Output::from("output.mp4")
828    ///     .set_framerate_max(AVRational { num: 30, den: 1 });
829    /// ```
830    pub fn set_framerate_max(mut self, framerate_max: AVRational) -> Self {
831        self.framerate_max = Some(framerate_max);
832        self
833    }
834
835    /// Sets the **video sync method** to be used during encoding.
836    ///
837    /// FFmpeg uses a variety of vsync policies to handle frame presentation times,
838    /// dropping/duplicating frames as needed. Adjusting this can be useful when
839    /// you need strict CFR (constant frame rate), or to pass frames through
840    /// without modification (`VsyncPassthrough`).
841    ///
842    /// # Parameters
843    /// * `method` - A variant of [`VSyncMethod`], such as `VsyncCfr` or `VsyncVfr`.
844    ///
845    /// # Returns
846    /// * `Self` - The modified `Output`, allowing method chaining.
847    ///
848    /// # Example
849    /// ```rust,ignore
850    /// let output = Output::from("output.mp4")
851    ///     .set_vsync_method(VSyncMethod::VsyncCfr);
852    /// ```
853    pub fn set_vsync_method(mut self, method: VSyncMethod) -> Self {
854        self.vsync_method = method;
855        self
856    }
857
858    /// Sets the **bits per raw sample** for video encoding.
859    ///
860    /// This value can influence quality or color depth when dealing with
861    /// certain pixel formats. Commonly used for high-bit-depth workflows
862    /// or specialized encoding scenarios.
863    ///
864    /// # Parameters
865    /// * `bits` - The bits per raw sample (e.g., 8, 10, 12).
866    ///
867    /// # Returns
868    /// * `Self` - The modified `Output`, allowing method chaining.
869    ///
870    /// # Example
871    /// ```rust,ignore
872    /// let output = Output::from("output.mkv")
873    ///     .set_bits_per_raw_sample(10); // e.g., 10-bit
874    /// ```
875    pub fn set_bits_per_raw_sample(mut self, bits: i32) -> Self {
876        self.bits_per_raw_sample = Some(bits);
877        self
878    }
879
880    /// Sets the **audio sample rate** (in Hz) for output encoding.
881    ///
882    /// This method allows you to specify the desired audio sample rate for the output.
883    /// Common values include 44100 (CD quality), 48000 (standard for digital video),
884    /// and 22050 or 16000 (for lower bitrate applications).
885    ///
886    /// # Parameters
887    /// * `audio_sample_rate` - The sample rate in Hertz (e.g., 44100, 48000).
888    ///
889    /// # Returns
890    /// * `Self` - The modified `Output`, allowing method chaining.
891    ///
892    /// # Example
893    /// ```rust,ignore
894    /// let output = Output::from("output.mp4")
895    ///     .set_audio_sample_rate(48000); // Set to 48kHz
896    /// ```
897    pub fn set_audio_sample_rate(mut self, audio_sample_rate: i32) -> Self {
898        self.audio_sample_rate = Some(audio_sample_rate);
899        self
900    }
901
902    /// Sets the number of **audio channels** for output encoding.
903    ///
904    /// Common values include 1 (mono), 2 (stereo), 5.1 (6 channels), and 7.1 (8 channels).
905    /// This setting affects the spatial audio characteristics of the output.
906    ///
907    /// # Parameters
908    /// * `audio_channels` - The number of audio channels (e.g., 1 for mono, 2 for stereo).
909    ///
910    /// # Returns
911    /// * `Self` - The modified `Output`, allowing method chaining.
912    ///
913    /// # Example
914    /// ```rust,ignore
915    /// let output = Output::from("output.mp4")
916    ///     .set_audio_channels(2); // Set to stereo
917    /// ```
918    pub fn set_audio_channels(mut self, audio_channels: i32) -> Self {
919        self.audio_channels = Some(audio_channels);
920        self
921    }
922
923    /// Sets the **audio sample format** for output encoding.
924    ///
925    /// This method allows you to specify the audio sample format, which affects
926    /// how audio samples are represented. Common formats include:
927    /// - `AV_SAMPLE_FMT_S16` (signed 16-bit)
928    /// - `AV_SAMPLE_FMT_S32` (signed 32-bit)
929    /// - `AV_SAMPLE_FMT_FLT` (32-bit float)
930    /// - `AV_SAMPLE_FMT_FLTP` (32-bit float, planar)
931    ///
932    /// The format choice can impact quality, processing requirements, and compatibility.
933    ///
934    /// # Parameters
935    /// * `sample_fmt` - An `AVSampleFormat` enum value specifying the desired sample format.
936    ///
937    /// # Returns
938    /// * `Self` - The modified `Output`, allowing method chaining.
939    ///
940    /// # Example
941    /// ```rust,ignore
942    /// use ffmpeg_sys_next::AVSampleFormat::AV_SAMPLE_FMT_S16;
943    ///
944    /// let output = Output::from("output.mp4")
945    ///     .set_audio_sample_fmt(AV_SAMPLE_FMT_S16); // Set to signed 16-bit
946    /// ```
947    pub fn set_audio_sample_fmt(mut self, sample_fmt: AVSampleFormat) -> Self {
948        self.audio_sample_fmt = Some(sample_fmt);
949        self
950    }
951
952    /// Sets the **video quality scale** (VBR) for encoding.
953    ///
954    /// This method configures a fixed quality scale for variable bitrate (VBR) video encoding.
955    /// Lower values result in higher quality but larger file sizes, while higher values
956    /// produce lower quality with smaller file sizes.
957    ///
958    /// # Note on Modern Usage
959    /// While still supported, using fixed quality scale (`-q:v`) is generally not recommended
960    /// for modern video encoding workflows with codecs like H.264 and H.265. Instead, consider:
961    /// * For H.264/H.265: Use CRF (Constant Rate Factor) via `-crf` parameter
962    /// * For two-pass encoding: Use target bitrate settings
963    ///
964    /// This parameter is primarily useful for older codecs or specific scenarios where
965    /// direct quality scale control is needed.
966    ///
967    /// # Quality Scale Ranges by Codec
968    /// * **H.264/H.265**: 0-51 (if needed: 17-28)
969    ///   - 17-18: Visually lossless
970    ///   - 23: High quality
971    ///   - 28: Good quality with reasonable file size
972    /// * **MPEG-4/MPEG-2**: 2-31 (recommended: 2-6)
973    ///   - Lower values = higher quality
974    /// * **VP9**: 0-63 (if needed: 15-35)
975    ///
976    /// # Parameters
977    /// * `video_qscale` - The quality scale value for video encoding.
978    ///
979    /// # Returns
980    /// * `Self` - The modified `Output`, allowing method chaining.
981    ///
982    /// # Example
983    /// ```rust,ignore
984    /// // For MJPEG encoding of image sequences
985    /// let output = Output::from("output.jpg")
986    ///     .set_video_qscale(2);  // High quality JPEG images
987    ///
988    /// // For legacy image format conversion
989    /// let output = Output::from("output.png")
990    ///     .set_video_qscale(3);  // Controls compression level
991    /// ```
992    pub fn set_video_qscale(mut self, video_qscale: i32) -> Self {
993        self.video_qscale = Some(video_qscale);
994        self
995    }
996
997    /// Sets the **audio quality scale** for encoding.
998    ///
999    /// This method configures codec-specific audio quality settings. The range, behavior,
1000    /// and optimal values depend entirely on the audio codec being used.
1001    ///
1002    /// # Quality Scale Ranges by Codec
1003    /// * **MP3 (libmp3lame)**: 0-9 (recommended: 2-5)
1004    ///   - 0: Highest quality
1005    ///   - 2: Near-transparent quality (~190-200 kbps)
1006    ///   - 5: Good quality (~130 kbps)
1007    ///   - 9: Lowest quality
1008    /// * **AAC**: 0.1-255 (recommended: 1-5)
1009    ///   - 1: Highest quality (~250 kbps)
1010    ///   - 3: Good quality (~160 kbps)
1011    ///   - 5: Medium quality (~100 kbps)
1012    /// * **Vorbis**: -1 to 10 (recommended: 3-8)
1013    ///   - 10: Highest quality
1014    ///   - 5: Good quality
1015    ///   - 3: Medium quality
1016    ///
1017    /// # Parameters
1018    /// * `audio_qscale` - The quality scale value for audio encoding.
1019    ///
1020    /// # Returns
1021    /// * `Self` - The modified `Output`, allowing method chaining.
1022    ///
1023    /// # Example
1024    /// ```rust,ignore
1025    /// // For MP3 encoding at high quality
1026    /// let output = Output::from("output.mp3")
1027    ///     .set_audio_codec("libmp3lame")
1028    ///     .set_audio_qscale(2);
1029    ///
1030    /// // For AAC encoding at good quality
1031    /// let output = Output::from("output.m4a")
1032    ///     .set_audio_codec("aac")
1033    ///     .set_audio_qscale(3);
1034    ///
1035    /// // For Vorbis encoding at high quality
1036    /// let output = Output::from("output.ogg")
1037    ///     .set_audio_codec("libvorbis")
1038    ///     .set_audio_qscale(7);
1039    /// ```
1040    pub fn set_audio_qscale(mut self, audio_qscale: i32) -> Self {
1041        self.audio_qscale = Some(audio_qscale);
1042        self
1043    }
1044
1045    /// **Sets the maximum number of video frames to encode (`-frames:v`).**
1046    ///
1047    /// **Equivalent FFmpeg Command:**
1048    /// ```sh
1049    /// ffmpeg -i input.mp4 -frames:v 100 output.mp4
1050    /// ```
1051    ///
1052    /// **Example Usage:**
1053    /// ```rust,ignore
1054    /// let output = Output::from("some_url")
1055    ///     .set_max_video_frames(500);
1056    /// ```
1057    pub fn set_max_video_frames(mut self, max_frames: impl Into<Option<i64>>) -> Self {
1058        self.max_video_frames = max_frames.into();
1059        self
1060    }
1061
1062    /// **Sets the maximum number of audio frames to encode (`-frames:a`).**
1063    ///
1064    /// **Equivalent FFmpeg Command:**
1065    /// ```sh
1066    /// ffmpeg -i input.mp4 -frames:a 500 output.mp4
1067    /// ```
1068    ///
1069    /// **Example Usage:**
1070    /// ```rust,ignore
1071    /// let output = Output::from("some_url")
1072    ///     .set_max_audio_frames(500);
1073    /// ```
1074    pub fn set_max_audio_frames(mut self, max_frames: impl Into<Option<i64>>) -> Self {
1075        self.max_audio_frames = max_frames.into();
1076        self
1077    }
1078
1079    /// **Sets the maximum number of subtitle frames to encode (`-frames:s`).**
1080    ///
1081    /// **Equivalent FFmpeg Command:**
1082    /// ```sh
1083    /// ffmpeg -i input.mp4 -frames:s 200 output.mp4
1084    /// ```
1085    ///
1086    /// **Example Usage:**
1087    /// ```rust,ignore
1088    /// let output = Output::from("some_url")
1089    ///     .set_max_subtitle_frames(200);
1090    /// ```
1091    pub fn set_max_subtitle_frames(mut self, max_frames: impl Into<Option<i64>>) -> Self {
1092        self.max_subtitle_frames = max_frames.into();
1093        self
1094    }
1095
1096    /// Sets a **video codec-specific option**.
1097    ///
1098    /// These options control **video encoding parameters** such as compression, quality, and speed.
1099    ///
1100    /// **Supported Parameters:**
1101    /// | Parameter | Description |
1102    /// |-----------|-------------|
1103    /// | `crf=0-51` | Quality level for x264/x265, lower means higher quality (`0` is lossless) |
1104    /// | `preset=ultrafast, superfast, fast, medium, slow, veryslow` | Encoding speed, affects compression efficiency |
1105    /// | `tune=film, animation, grain, stillimage, fastdecode, zerolatency` | Optimizations for specific types of content |
1106    /// | `b=4M` | Bitrate (e.g., `4M` for 4 Mbps) |
1107    /// | `g=50` | GOP (Group of Pictures) size, affects keyframe frequency |
1108    ///
1109    /// **Example Usage:**
1110    /// ```rust,ignore
1111    /// let output = Output::from("some_url")
1112    ///     .set_video_codec_opt("crf", "18")
1113    ///     .set_video_codec_opt("preset", "fast");
1114    /// ```
1115    pub fn set_video_codec_opt(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
1116        if let Some(ref mut opts) = self.video_codec_opts {
1117            opts.insert(key.into(), value.into());
1118        } else {
1119            let mut opts = HashMap::new();
1120            opts.insert(key.into(), value.into());
1121            self.video_codec_opts = Some(opts);
1122        }
1123        self
1124    }
1125
1126    /// **Sets multiple video codec options at once.**
1127    ///
1128    /// **Example Usage:**
1129    /// ```rust,ignore
1130    /// let output = Output::from("some_url")
1131    ///     .set_video_codec_opts(vec![
1132    ///         ("crf", "18"),
1133    ///         ("preset", "fast")
1134    ///     ]);
1135    /// ```
1136    pub fn set_video_codec_opts(
1137        mut self,
1138        opts: Vec<(impl Into<String>, impl Into<String>)>,
1139    ) -> Self {
1140        let video_opts = self.video_codec_opts.get_or_insert_with(HashMap::new);
1141        for (key, value) in opts {
1142            video_opts.insert(key.into(), value.into());
1143        }
1144        self
1145    }
1146
1147    /// Sets a **audio codec-specific option**.
1148    ///
1149    /// These options control **audio encoding parameters** such as bitrate, sample rate, and format.
1150    ///
1151    /// **Supported Parameters:**
1152    /// | Parameter | Description |
1153    /// |-----------|-------------|
1154    /// | `b=192k` | Bitrate (e.g., `128k` for 128 Kbps, `320k` for 320 Kbps) |
1155    /// | `compression_level=0-12` | Compression efficiency for formats like FLAC |
1156    ///
1157    /// **Example Usage:**
1158    /// ```rust,ignore
1159    /// let output = Output::from("some_url")
1160    ///     .set_audio_codec_opt("b", "320k")
1161    ///     .set_audio_codec_opt("compression_level", "6");
1162    /// ```
1163    pub fn set_audio_codec_opt(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
1164        if let Some(ref mut opts) = self.audio_codec_opts {
1165            opts.insert(key.into(), value.into());
1166        } else {
1167            let mut opts = HashMap::new();
1168            opts.insert(key.into(), value.into());
1169            self.audio_codec_opts = Some(opts);
1170        }
1171        self
1172    }
1173
1174    /// **Sets multiple audio codec options at once.**
1175    ///
1176    /// **Example Usage:**
1177    /// ```rust,ignore
1178    /// let output = Output::from("some_url")
1179    ///     .set_audio_codec_opts(vec![
1180    ///         ("b", "320k"),
1181    ///         ("compression_level", "6")
1182    ///     ]);
1183    /// ```
1184    pub fn set_audio_codec_opts(
1185        mut self,
1186        opts: Vec<(impl Into<String>, impl Into<String>)>,
1187    ) -> Self {
1188        let audio_opts = self.audio_codec_opts.get_or_insert_with(HashMap::new);
1189        for (key, value) in opts {
1190            audio_opts.insert(key.into(), value.into());
1191        }
1192        self
1193    }
1194
1195    /// Sets a **subtitle codec-specific option**.
1196    ///
1197    /// These options control **subtitle encoding parameters** such as format and character encoding.
1198    ///
1199    /// **Supported Parameters:**
1200    /// | Parameter | Description |
1201    /// |-----------|-------------|
1202    /// | `mov_text` | Subtitle format for MP4 files |
1203    /// | `srt` | Subtitle format for `.srt` files |
1204    /// | `ass` | Advanced SubStation Alpha (ASS) subtitle format |
1205    /// | `forced_subs=1` | Forces the subtitles to always be displayed |
1206    ///
1207    /// **Example Usage:**
1208    /// ```rust,ignore
1209    /// let output = Output::from("some_url")
1210    ///     .set_subtitle_codec_opt("mov_text", "");
1211    /// ```
1212    pub fn set_subtitle_codec_opt(
1213        mut self,
1214        key: impl Into<String>,
1215        value: impl Into<String>,
1216    ) -> Self {
1217        if let Some(ref mut opts) = self.subtitle_codec_opts {
1218            opts.insert(key.into(), value.into());
1219        } else {
1220            let mut opts = HashMap::new();
1221            opts.insert(key.into(), value.into());
1222            self.subtitle_codec_opts = Some(opts);
1223        }
1224        self
1225    }
1226
1227    /// **Sets multiple subtitle codec options at once.**
1228    ///
1229    /// **Example Usage:**
1230    /// ```rust,ignore
1231    /// let output = Output::from("some_url")
1232    ///     .set_subtitle_codec_opts(vec![
1233    ///         ("mov_text", ""),
1234    ///         ("forced_subs", "1")
1235    ///     ]);
1236    /// ```
1237    pub fn set_subtitle_codec_opts(
1238        mut self,
1239        opts: Vec<(impl Into<String>, impl Into<String>)>,
1240    ) -> Self {
1241        let subtitle_opts = self.subtitle_codec_opts.get_or_insert_with(HashMap::new);
1242        for (key, value) in opts {
1243            subtitle_opts.insert(key.into(), value.into());
1244        }
1245        self
1246    }
1247
1248    /// Sets a format-specific option for the output container.
1249    ///
1250    /// FFmpeg supports various format-specific options that can be passed to the muxer.
1251    /// These options allow fine-tuning of the output container’s behavior.
1252    ///
1253    /// **Example Usage:**
1254    /// ```rust,ignore
1255    /// let output = Output::from("some_url")
1256    ///     .set_format_opt("movflags", "faststart")
1257    ///     .set_format_opt("flvflags", "no_duration_filesize");
1258    /// ```
1259    ///
1260    /// ### Common Format Options:
1261    /// | Format | Option | Description |
1262    /// |--------|--------|-------------|
1263    /// | `mp4`  | `movflags=faststart` | Moves moov atom to the beginning of the file for faster playback start |
1264    /// | `flv`  | `flvflags=no_duration_filesize` | Removes duration/size metadata for live streaming |
1265    ///
1266    /// **Parameters:**
1267    /// - `key`: The format option name (e.g., `"movflags"`, `"flvflags"`).
1268    /// - `value`: The value to set (e.g., `"faststart"`, `"no_duration_filesize"`).
1269    ///
1270    /// Returns the modified `Output` struct for chaining.
1271    pub fn set_format_opt(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
1272        if let Some(ref mut opts) = self.format_opts {
1273            opts.insert(key.into(), value.into());
1274        } else {
1275            let mut opts = HashMap::new();
1276            opts.insert(key.into(), value.into());
1277            self.format_opts = Some(opts);
1278        }
1279        self
1280    }
1281
1282    /// Sets multiple format-specific options at once.
1283    ///
1284    /// This method allows setting multiple format options in a single call.
1285    ///
1286    /// **Example Usage:**
1287    /// ```rust,ignore
1288    /// let output = Output::from("some_url")
1289    ///     .set_format_opts(vec![
1290    ///         ("movflags", "faststart"),
1291    ///         ("flvflags", "no_duration_filesize")
1292    ///     ]);
1293    /// ```
1294    ///
1295    /// **Parameters:**
1296    /// - `opts`: A vector of key-value pairs representing format options.
1297    ///
1298    /// Returns the modified `Output` struct for chaining.
1299    pub fn set_format_opts(mut self, opts: Vec<(impl Into<String>, impl Into<String>)>) -> Self {
1300        if let Some(ref mut format_opts) = self.format_opts {
1301            for (key, value) in opts {
1302                format_opts.insert(key.into(), value.into());
1303            }
1304        } else {
1305            let mut format_opts = HashMap::new();
1306            for (key, value) in opts {
1307                format_opts.insert(key.into(), value.into());
1308            }
1309            self.format_opts = Some(format_opts);
1310        }
1311        self
1312    }
1313
1314    // ========== Metadata API Methods ==========
1315    // The following helpers mirror FFmpeg's command-line metadata options as implemented in
1316    // fftools/ffmpeg_opt.c (opt_metadata / opt_map_metadata) and the automatic propagation rules
1317    // in fftools/ffmpeg_mux_init.c:2913-2983. Each method references the corresponding FFmpeg
1318    // behavior so callers can cross-check the C implementation when needed.
1319
1320    /// Add or update global metadata for the output file.
1321    ///
1322    /// If value is empty string, the key will be removed (FFmpeg behavior).
1323    /// Replicates FFmpeg's `-metadata key=value` option.
1324    ///
1325    /// FFmpeg reference: fftools/ffmpeg_opt.c (`opt_metadata()` handles `-metadata key=value`).
1326    ///
1327    /// # Examples
1328    /// ```rust,ignore
1329    /// let output = Output::from("output.mp4")
1330    ///     .add_metadata("title", "My Video")
1331    ///     .add_metadata("author", "John Doe");
1332    /// ```
1333    pub fn add_metadata(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
1334        let key = key.into();
1335        let value = value.into();
1336
1337        if value.is_empty() {
1338            // Empty value means remove the key (FFmpeg behavior)
1339            if let Some(ref mut metadata) = self.global_metadata {
1340                metadata.remove(&key);
1341            }
1342        } else {
1343            self.global_metadata
1344                .get_or_insert_with(HashMap::new)
1345                .insert(key, value);
1346        }
1347        self
1348    }
1349
1350    /// Add multiple global metadata entries at once.
1351    ///
1352    /// FFmpeg reference: fftools/ffmpeg_opt.c (consecutive `-metadata` invocations append to the
1353    /// same dictionary; this helper simply batches the calls on the Rust side).
1354    ///
1355    /// # Examples
1356    /// ```rust,ignore
1357    /// let mut metadata = HashMap::new();
1358    /// metadata.insert("title".to_string(), "My Video".to_string());
1359    /// metadata.insert("author".to_string(), "John Doe".to_string());
1360    ///
1361    /// let output = Output::from("output.mp4")
1362    ///     .add_metadata_map(metadata);
1363    /// ```
1364    pub fn add_metadata_map(mut self, metadata: HashMap<String, String>) -> Self {
1365        for (key, value) in metadata {
1366            self = self.add_metadata(key, value);
1367        }
1368        self
1369    }
1370
1371    /// Remove a global metadata key.
1372    ///
1373    /// FFmpeg reference: fftools/ffmpeg_opt.c (`-metadata key=` deletes the key when value is
1374    /// empty; we follow the same rule by interpreting an empty string as removal).
1375    ///
1376    /// # Examples
1377    /// ```rust,ignore
1378    /// let output = Output::from("output.mp4")
1379    ///     .add_metadata("title", "My Video")
1380    ///     .remove_metadata("title");  // Remove the title
1381    /// ```
1382    pub fn remove_metadata(mut self, key: &str) -> Self {
1383        if let Some(ref mut metadata) = self.global_metadata {
1384            metadata.remove(key);
1385        }
1386        self
1387    }
1388
1389    /// Clear all metadata (global, stream, chapter, program) and mappings.
1390    ///
1391    /// Useful when you want to start fresh without any metadata.
1392    ///
1393    /// FFmpeg reference: fftools/ffmpeg_opt.c (users typically issue `-map_metadata -1` and then
1394    /// reapply `-metadata` options; this helper emulates that workflow programmatically).
1395    ///
1396    /// # Examples
1397    /// ```rust,ignore
1398    /// let output = Output::from("output.mp4")
1399    ///     .add_metadata("title", "My Video")
1400    ///     .clear_all_metadata();  // Remove all metadata
1401    /// ```
1402    pub fn clear_all_metadata(mut self) -> Self {
1403        self.global_metadata = None;
1404        self.stream_metadata.clear();
1405        self.chapter_metadata.clear();
1406        self.program_metadata.clear();
1407        self.metadata_map.clear();
1408        self
1409    }
1410
1411
1412    /// Disable automatic metadata copying from input files.
1413    ///
1414    /// By default, FFmpeg automatically copies global and stream metadata
1415    /// from input files to output. This method disables that behavior,
1416    /// similar to FFmpeg's `-map_metadata -1` option.
1417    /// FFmpeg reference: ffmpeg_mux_init.c (`copy_meta()` sets metadata_global_manual when
1418    /// `-map_metadata -1` is used; `auto_copy_metadata` mirrors the same flag).
1419    ///
1420    /// # Examples
1421    /// ```rust,ignore
1422    /// let output = Output::from("output.mp4")
1423    ///     .disable_auto_copy_metadata()  // Don't copy any metadata from input
1424    ///     .add_metadata("title", "New Title");  // Only use explicitly set metadata
1425    /// ```
1426    pub fn disable_auto_copy_metadata(mut self) -> Self {
1427        self.auto_copy_metadata = false;
1428        self
1429    }
1430
1431    /// Add or update stream-specific metadata.
1432    ///
1433    /// Uses FFmpeg's stream specifier syntax to identify target streams.
1434    /// If value is empty string, the key will be removed (FFmpeg behavior).
1435    /// Replicates FFmpeg's `-metadata:s:spec key=value` option.
1436    ///
1437    /// FFmpeg reference: fftools/ffmpeg_opt.c (`opt_metadata()` with stream specifiers, lines
1438    /// 2465-2520 in FFmpeg 7.x).
1439    ///
1440    /// # Stream Specifier Syntax
1441    /// - `"v:0"` - First video stream
1442    /// - `"a:1"` - Second audio stream
1443    /// - `"s"` - All subtitle streams
1444    /// - `"v"` - All video streams
1445    /// - `"p:0:v"` - Video streams in program 0
1446    /// - `"#0x100"` or `"i:256"` - Stream with specific ID
1447    /// - `"m:language:eng"` - Streams with metadata language=eng
1448    /// - `"u"` - Usable streams only
1449    /// - `"disp:default"` - Streams with default disposition
1450    ///
1451    /// # Examples
1452    /// ```rust,ignore
1453    /// let output = Output::from("output.mp4")
1454    ///     .add_stream_metadata("v:0", "language", "eng")
1455    ///     .add_stream_metadata("a:0", "title", "Main Audio");
1456    /// ```
1457    ///
1458    /// # Errors
1459    /// Returns error if the stream specifier syntax is invalid.
1460    pub fn add_stream_metadata(
1461        mut self,
1462        stream_spec: impl Into<String>,
1463        key: impl Into<String>,
1464        value: impl Into<String>,
1465    ) -> Result<Self, String> {
1466        use crate::core::metadata::StreamSpecifier;
1467
1468        let stream_spec_str = stream_spec.into();
1469        let key = key.into();
1470        let value = value.into();
1471
1472        // Parse and validate stream specifier
1473        let _specifier = StreamSpecifier::parse(&stream_spec_str)?;
1474
1475        // Store as (spec, key, value) tuple
1476        // During output initialization, this will be matched against actual streams
1477        // using StreamSpecifier::matches and applied to all matching streams
1478        // Replicates FFmpeg's of_add_metadata behavior
1479        self.stream_metadata.push((stream_spec_str, key, value));
1480
1481        Ok(self)
1482    }
1483
1484    /// Add or update chapter-specific metadata.
1485    ///
1486    /// Chapters are used for DVD-like navigation points in media files.
1487    /// If value is empty string, the key will be removed (FFmpeg behavior).
1488    /// Replicates FFmpeg's `-metadata:c:N key=value` option.
1489    /// FFmpeg reference: fftools/ffmpeg_opt.c (`opt_metadata()` handles the `c:` target selector).
1490    ///
1491    /// # Examples
1492    /// ```rust,ignore
1493    /// let output = Output::from("output.mp4")
1494    ///     .add_chapter_metadata(0, "title", "Introduction")
1495    ///     .add_chapter_metadata(1, "title", "Main Content");
1496    /// ```
1497    pub fn add_chapter_metadata(
1498        mut self,
1499        chapter_index: usize,
1500        key: impl Into<String>,
1501        value: impl Into<String>,
1502    ) -> Self {
1503        let key = key.into();
1504        let value = value.into();
1505
1506        if value.is_empty() {
1507            // Empty value means remove the key (FFmpeg behavior)
1508            if let Some(metadata) = self.chapter_metadata.get_mut(&chapter_index) {
1509                metadata.remove(&key);
1510            }
1511        } else {
1512            self.chapter_metadata
1513                .entry(chapter_index)
1514                .or_default()
1515                .insert(key, value);
1516        }
1517        self
1518    }
1519
1520    /// Add or update program-specific metadata.
1521    ///
1522    /// Programs are used in multi-program transport streams (e.g., MPEG-TS).
1523    /// If value is empty string, the key will be removed (FFmpeg behavior).
1524    /// Replicates FFmpeg's `-metadata:p:N key=value` option.
1525    /// FFmpeg reference: fftools/ffmpeg_opt.c (`opt_metadata()` with `p:` selector).
1526    ///
1527    /// # Examples
1528    /// ```rust,ignore
1529    /// let output = Output::from("output.ts")
1530    ///     .add_program_metadata(0, "service_name", "Channel 1")
1531    ///     .add_program_metadata(1, "service_name", "Channel 2");
1532    /// ```
1533    pub fn add_program_metadata(
1534        mut self,
1535        program_index: usize,
1536        key: impl Into<String>,
1537        value: impl Into<String>,
1538    ) -> Self {
1539        let key = key.into();
1540        let value = value.into();
1541
1542        if value.is_empty() {
1543            // Empty value means remove the key (FFmpeg behavior)
1544            if let Some(metadata) = self.program_metadata.get_mut(&program_index) {
1545                metadata.remove(&key);
1546            }
1547        } else {
1548            self.program_metadata
1549                .entry(program_index)
1550                .or_default()
1551                .insert(key, value);
1552        }
1553        self
1554    }
1555
1556    /// Map metadata from an input file to this output.
1557    ///
1558    /// Replicates FFmpeg's `-map_metadata [src_file_idx]:src_type:dst_type` option.
1559    /// This allows copying metadata from specific locations in input files to
1560    /// specific locations in the output file.
1561    ///
1562    /// # Type Specifiers
1563    /// - `"g"` or `""` - Global metadata
1564    /// - `"s"` or `"s:spec"` - Stream metadata (with optional stream specifier)
1565    /// - `"c:N"` - Chapter N metadata
1566    /// - `"p:N"` - Program N metadata
1567    ///
1568    /// # Examples
1569    /// ```rust,ignore
1570    /// use ez_ffmpeg::core::metadata::{MetadataType, MetadataMapping};
1571    ///
1572    /// let output = Output::from("output.mp4")
1573    ///     // Copy global metadata from input 0 to output global
1574    ///     .map_metadata_from_input(0, "g", "g")?
1575    ///     // Copy first video stream metadata from input 1 to output first video stream
1576    ///     .map_metadata_from_input(1, "s:v:0", "s:v:0")?;
1577    /// ```
1578    ///
1579    /// # Errors
1580    /// Returns error if the type specifier syntax is invalid.
1581    /// FFmpeg reference: fftools/ffmpeg_opt.c (`opt_map_metadata()` parses the same
1582    /// `[file][:type]` triplet and feeds it into `MetadataMapping`).
1583    pub fn map_metadata_from_input(
1584        mut self,
1585        input_index: usize,
1586        src_type_spec: impl Into<String>,
1587        dst_type_spec: impl Into<String>,
1588    ) -> Result<Self, String> {
1589        use crate::core::metadata::{MetadataMapping, MetadataType};
1590
1591        let src_type = MetadataType::parse(&src_type_spec.into())?;
1592        let dst_type = MetadataType::parse(&dst_type_spec.into())?;
1593
1594        self.metadata_map.push(MetadataMapping {
1595            src_type,
1596            dst_type,
1597            input_index,
1598        });
1599
1600        Ok(self)
1601    }
1602
1603    // ========== Stream Disable & Format API Methods (P1 Features) ==========
1604    // These methods replicate FFmpeg's `-vn`, `-an`, `-sn`, `-b:v`, `-b:a`, and `-pix_fmt` options.
1605
1606    /// Disables video stream mapping (equivalent to `-vn` in FFmpeg).
1607    ///
1608    /// Video streams will be excluded from automatic stream mapping.
1609    /// This is useful when you want to extract only audio from a video file.
1610    ///
1611    /// **Equivalent FFmpeg Command:**
1612    /// ```sh
1613    /// ffmpeg -i input.mp4 -vn output.mp3
1614    /// ```
1615    ///
1616    /// # Examples
1617    /// ```rust,ignore
1618    /// // Extract audio only, no video
1619    /// let output = Output::from("output.mp3")
1620    ///     .disable_video();
1621    /// ```
1622    pub fn disable_video(mut self) -> Self {
1623        self.video_disable = true;
1624        self
1625    }
1626
1627    /// Disables audio stream mapping (equivalent to `-an` in FFmpeg).
1628    ///
1629    /// Audio streams will be excluded from automatic stream mapping.
1630    /// This is useful when you want to create a silent video.
1631    ///
1632    /// **Equivalent FFmpeg Command:**
1633    /// ```sh
1634    /// ffmpeg -i input.mp4 -an output.mp4
1635    /// ```
1636    ///
1637    /// # Examples
1638    /// ```rust,ignore
1639    /// // Create video without audio
1640    /// let output = Output::from("output.mp4")
1641    ///     .disable_audio();
1642    /// ```
1643    pub fn disable_audio(mut self) -> Self {
1644        self.audio_disable = true;
1645        self
1646    }
1647
1648    /// Disables subtitle stream mapping (equivalent to `-sn` in FFmpeg).
1649    ///
1650    /// Subtitle streams will be excluded from automatic stream mapping.
1651    ///
1652    /// **Equivalent FFmpeg Command:**
1653    /// ```sh
1654    /// ffmpeg -i input.mkv -sn output.mkv
1655    /// ```
1656    ///
1657    /// # Examples
1658    /// ```rust,ignore
1659    /// // Copy video and audio, but exclude subtitles
1660    /// let output = Output::from("output.mkv")
1661    ///     .disable_subtitle();
1662    /// ```
1663    pub fn disable_subtitle(mut self) -> Self {
1664        self.subtitle_disable = true;
1665        self
1666    }
1667
1668    /// Disables data stream mapping (equivalent to `-dn` in FFmpeg).
1669    ///
1670    /// Data streams (timed metadata, chapter markers, etc.) will be
1671    /// excluded from automatic stream mapping.
1672    ///
1673    /// **Equivalent FFmpeg Command:**
1674    /// ```sh
1675    /// ffmpeg -i input.mkv -dn output.mp4
1676    /// ```
1677    ///
1678    /// # Examples
1679    /// ```rust,ignore
1680    /// // Copy video and audio, but exclude data streams
1681    /// let output = Output::from("output.mp4")
1682    ///     .disable_data();
1683    /// ```
1684    pub fn disable_data(mut self) -> Self {
1685        self.data_disable = true;
1686        self
1687    }
1688
1689    /// Sets the video bitrate (equivalent to `-b:v` in FFmpeg).
1690    ///
1691    /// The bitrate string follows FFmpeg conventions:
1692    /// - `"1M"` or `"1000k"` for 1 Mbps
1693    /// - `"500k"` for 500 Kbps
1694    /// - `"2M"` for 2 Mbps
1695    ///
1696    /// **Equivalent FFmpeg Command:**
1697    /// ```sh
1698    /// ffmpeg -i input.mp4 -b:v 2M output.mp4
1699    /// ```
1700    ///
1701    /// # Examples
1702    /// ```rust,ignore
1703    /// let output = Output::from("output.mp4")
1704    ///     .set_video_bitrate("2M");
1705    /// ```
1706    pub fn set_video_bitrate(self, bitrate: impl Into<String>) -> Self {
1707        self.set_video_codec_opt("b", bitrate)
1708    }
1709
1710    /// Sets the audio bitrate (equivalent to `-b:a` in FFmpeg).
1711    ///
1712    /// The bitrate string follows FFmpeg conventions:
1713    /// - `"128k"` for 128 Kbps
1714    /// - `"192k"` for 192 Kbps
1715    /// - `"320k"` for 320 Kbps
1716    ///
1717    /// **Equivalent FFmpeg Command:**
1718    /// ```sh
1719    /// ffmpeg -i input.mp4 -b:a 192k output.mp4
1720    /// ```
1721    ///
1722    /// # Examples
1723    /// ```rust,ignore
1724    /// let output = Output::from("output.mp4")
1725    ///     .set_audio_bitrate("192k");
1726    /// ```
1727    pub fn set_audio_bitrate(self, bitrate: impl Into<String>) -> Self {
1728        self.set_audio_codec_opt("b", bitrate)
1729    }
1730
1731    /// Sets the output pixel format (equivalent to `-pix_fmt` in FFmpeg).
1732    ///
1733    /// Common pixel formats include:
1734    /// - `"yuv420p"` - Most compatible format for H.264
1735    /// - `"yuv444p"` - Higher quality, less compatible
1736    /// - `"rgb24"` - RGB format
1737    /// - `"nv12"` - Common for hardware encoding
1738    ///
1739    /// To see all available formats, run: `ffmpeg -pix_fmts`
1740    ///
1741    /// # Behavior
1742    ///
1743    /// - **Unknown format name**: Returns [`OpenOutputError::UnknownPixelFormat`] error.
1744    ///   This matches FFmpeg CLI behavior (e.g., `ffmpeg -pix_fmt foobar` also fails).
1745    /// - **Format incompatible with encoder**: The filter graph automatically converts
1746    ///   to a compatible format. For example, specifying `rgb48be` with libx264 will
1747    ///   auto-convert to `yuv420p`.
1748    /// - **Stream copy mode**: This setting has no effect when using `-c:v copy`.
1749    ///
1750    /// **Equivalent FFmpeg Command:**
1751    /// ```sh
1752    /// ffmpeg -i input.mp4 -pix_fmt yuv420p output.mp4
1753    /// ```
1754    ///
1755    /// # Examples
1756    /// ```rust,ignore
1757    /// let output = Output::from("output.mp4")
1758    ///     .set_pix_fmt("yuv420p");
1759    /// ```
1760    ///
1761    /// [`OpenOutputError::UnknownPixelFormat`]: crate::error::OpenOutputError::UnknownPixelFormat
1762    pub fn set_pix_fmt(mut self, pix_fmt: impl Into<String>) -> Self {
1763        self.pix_fmt = Some(pix_fmt.into());
1764        self
1765    }
1766}
1767
1768impl From<Box<dyn FnMut(&[u8]) -> i32 + Send>> for Output {
1769    fn from(write_callback_and_format: Box<dyn FnMut(&[u8]) -> i32 + Send>) -> Self {
1770        Self {
1771            url: None,
1772            write_callback: Some(write_callback_and_format),
1773            seek_callback: None,
1774            frame_pipelines: None,
1775            stream_map_specs: vec![],
1776            stream_maps: vec![],
1777            format: None,
1778            video_codec: None,
1779            audio_codec: None,
1780            subtitle_codec: None,
1781            start_time_us: None,
1782            recording_time_us: None,
1783            stop_time_us: None,
1784            framerate: None,
1785            framerate_max: None,
1786            vsync_method: VSyncMethod::VsyncAuto,
1787            bits_per_raw_sample: None,
1788            audio_sample_rate: None,
1789            audio_channels: None,
1790            audio_sample_fmt: None,
1791            video_qscale: None,
1792            audio_qscale: None,
1793            max_video_frames: None,
1794            max_audio_frames: None,
1795            max_subtitle_frames: None,
1796            video_codec_opts: None,
1797            audio_codec_opts: None,
1798            subtitle_codec_opts: None,
1799            format_opts: None,
1800            // Metadata fields - initialized with defaults
1801            global_metadata: None,
1802            stream_metadata: Vec::new(),
1803            chapter_metadata: HashMap::new(),
1804            program_metadata: HashMap::new(),
1805            metadata_map: Vec::new(),
1806            auto_copy_metadata: true, // FFmpeg default: auto-copy enabled
1807            // Stream disable flags - initialized to false (all streams enabled)
1808            video_disable: false,
1809            audio_disable: false,
1810            subtitle_disable: false,
1811            data_disable: false,
1812            pix_fmt: None,
1813        }
1814    }
1815}
1816
1817impl From<String> for Output {
1818    fn from(url: String) -> Self {
1819        Self {
1820            url: Some(url),
1821            write_callback: None,
1822            seek_callback: None,
1823            frame_pipelines: None,
1824            stream_map_specs: vec![],
1825            stream_maps: vec![],
1826            format: None,
1827            video_codec: None,
1828            audio_codec: None,
1829            subtitle_codec: None,
1830            start_time_us: None,
1831            recording_time_us: None,
1832            stop_time_us: None,
1833            framerate: None,
1834            framerate_max: None,
1835            vsync_method: VSyncMethod::VsyncAuto,
1836            bits_per_raw_sample: None,
1837            audio_sample_rate: None,
1838            audio_channels: None,
1839            audio_sample_fmt: None,
1840            video_qscale: None,
1841            audio_qscale: None,
1842            max_video_frames: None,
1843            max_audio_frames: None,
1844            max_subtitle_frames: None,
1845            video_codec_opts: None,
1846            audio_codec_opts: None,
1847            subtitle_codec_opts: None,
1848            format_opts: None,
1849            // Metadata fields - initialized with defaults
1850            global_metadata: None,
1851            stream_metadata: Vec::new(),
1852            chapter_metadata: HashMap::new(),
1853            program_metadata: HashMap::new(),
1854            metadata_map: Vec::new(),
1855            auto_copy_metadata: true, // FFmpeg default: auto-copy enabled
1856            // Stream disable flags - initialized to false (all streams enabled)
1857            video_disable: false,
1858            audio_disable: false,
1859            subtitle_disable: false,
1860            data_disable: false,
1861            pix_fmt: None,
1862        }
1863    }
1864}
1865
1866impl From<&str> for Output {
1867    fn from(url: &str) -> Self {
1868        Self::from(String::from(url))
1869    }
1870}
1871
1872/// Temporary storage for unparsed stream map specifications (user input stage)
1873/// Equivalent to FFmpeg's command-line parsing before opt_map() expansion
1874#[derive(Debug, Clone)]
1875pub(crate) struct StreamMapSpec {
1876    /// Stream specifier string: "0:v", "1:a:0", "0:v?", "[label]", etc.
1877    pub(crate) linklabel: String,
1878    /// Stream copy flag (-c copy)
1879    pub(crate) copy: bool,
1880}
1881
1882impl<T: Into<String>> From<T> for StreamMapSpec {
1883    fn from(linklabel: T) -> Self {
1884        Self {
1885            linklabel: linklabel.into(),
1886            copy: false,
1887        }
1888    }
1889}
1890
1891/// Final expanded stream map (matches FFmpeg's StreamMap structure)
1892/// Created after parsing and expansion in outputs_bind()
1893/// FFmpeg reference: fftools/ffmpeg.h:134-141
1894#[derive(Debug, Clone)]
1895pub(crate) struct StreamMap {
1896    /// 1 if this mapping is disabled by a negative map (-map -0:v)
1897    pub(crate) disabled: bool,
1898    /// Input file index
1899    pub(crate) file_index: usize,
1900    /// Input stream index within the file
1901    pub(crate) stream_index: usize,
1902    /// Name of an output link, for mapping lavfi outputs (e.g., "[v]", "myout")
1903    pub(crate) linklabel: Option<String>,
1904    /// Stream copy flag (-c copy)
1905    pub(crate) copy: bool,
1906}