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