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