Skip to main content

ez_ffmpeg/core/context/output/
mod.rs

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