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    /// Forced-keyframe request: list-form times, periodic interval, or off.
262    /// List-form strings are parsed at open time (`parse_forced_key_frames`);
263    /// periodic mode is typed. Applies to re-encoded video only.
264    pub(crate) forced_kf_spec: ForcedKeyframeSpec,
265
266    /// Maximum number of **video** frames to encode (equivalent to `-frames:v` in FFmpeg).
267    ///
268    /// This option limits the number of **video** frames processed by the encoder.
269    ///
270    /// **Equivalent FFmpeg Command:**
271    /// ```sh
272    /// ffmpeg -i input.mp4 -frames:v 100 output.mp4
273    /// ```
274    ///
275    /// **Example Usage:**
276    /// ```rust,ignore
277    /// let output = Output::from("some_url")
278    ///     .set_max_video_frames(300);
279    /// ```
280    pub(crate) max_video_frames: Option<i64>,
281
282    /// Maximum number of **audio** frames to encode (equivalent to `-frames:a` in FFmpeg).
283    ///
284    /// This option limits the number of **audio** frames processed by the encoder.
285    ///
286    /// **Equivalent FFmpeg Command:**
287    /// ```sh
288    /// ffmpeg -i input.mp4 -frames:a 500 output.mp4
289    /// ```
290    ///
291    /// **Example Usage:**
292    /// ```rust,ignore
293    /// let output = Output::from("some_url")
294    ///     .set_max_audio_frames(500);
295    /// ```
296    pub(crate) max_audio_frames: Option<i64>,
297
298    /// Maximum number of **subtitle** frames to encode (equivalent to `-frames:s` in FFmpeg).
299    ///
300    /// This option limits the number of **subtitle** frames processed by the encoder.
301    ///
302    /// **Equivalent FFmpeg Command:**
303    /// ```sh
304    /// ffmpeg -i input.mp4 -frames:s 200 output.mp4
305    /// ```
306    ///
307    /// **Example Usage:**
308    /// ```rust,ignore
309    /// let output = Output::from("some_url")
310    ///     .set_max_subtitle_frames(200);
311    /// ```
312    pub(crate) max_subtitle_frames: Option<i64>,
313
314    /// Video encoder-specific options.
315    ///
316    /// This field stores key-value pairs for configuring the **video encoder**.
317    /// These options are passed to the video encoder before encoding begins.
318    ///
319    /// **Common Examples:**
320    /// - `crf=0` (for lossless quality in x264/x265)
321    /// - `preset=ultrafast` (for faster encoding speed in H.264)
322    /// - `tune=zerolatency` (for real-time streaming)
323    pub(crate) video_codec_opts: Option<HashMap<String, String>>,
324
325    /// Audio encoder-specific options.
326    ///
327    /// This field stores key-value pairs for configuring the **audio encoder**.
328    /// These options are passed to the audio encoder before encoding begins.
329    ///
330    /// **Common Examples:**
331    /// - `b=192k` (for setting bitrate in AAC/MP3)
332    /// - `ar=44100` (for setting sample rate)
333    pub(crate) audio_codec_opts: Option<HashMap<String, String>>,
334
335    /// Subtitle encoder-specific options.
336    ///
337    /// This field stores key-value pairs for configuring the **subtitle encoder**.
338    /// These options are passed to the subtitle encoder before encoding begins.
339    ///
340    /// **Common Examples:**
341    /// - `mov_text` (for MP4 subtitles)
342    /// - `srt` (for subtitle format)
343    pub(crate) subtitle_codec_opts: Option<HashMap<String, String>>,
344
345    /// The output format options for the container.
346    ///
347    /// This field stores additional format-specific options that are passed to the FFmpeg muxer.
348    /// It is a collection of key-value pairs that can modify the behavior of the output format.
349    ///
350    /// Common examples include:
351    /// - `movflags=faststart` (for MP4 files)
352    /// - `flvflags=no_duration_filesize` (for FLV files)
353    ///
354    /// These options are used when initializing the FFmpeg output format.
355    ///
356    /// **Example Usage:**
357    /// ```rust,ignore
358    /// let output = Output::from("some_url")
359    ///     .set_format_opt("movflags", "faststart");
360    /// ```
361    pub(crate) format_opts: Option<HashMap<String, String>>,
362
363    // ========== Metadata Fields ==========
364    /// Global metadata for the entire output file
365    pub(crate) global_metadata: Option<HashMap<String, String>>,
366
367    /// Stream-specific metadata with stream specifiers
368    /// Key: stream specifier string (e.g., "v:0", "a", "s:0")
369    /// Value: metadata key-value pairs for matching streams
370    /// During output initialization, each specifier is matched against actual streams
371    pub(crate) stream_metadata: Vec<(String, String, String)>, // (spec, key, value) tuples
372
373    /// Chapter-specific metadata, indexed by chapter index
374    pub(crate) chapter_metadata: HashMap<usize, HashMap<String, String>>,
375
376    /// Program-specific metadata, indexed by program index
377    pub(crate) program_metadata: HashMap<usize, HashMap<String, String>>,
378
379    /// Metadata mappings from input files
380    pub(crate) metadata_map: Vec<crate::core::metadata::MetadataMapping>,
381
382    /// Whether to automatically copy metadata from input files (default: true)
383    /// Replicates FFmpeg's default behavior of copying global and stream metadata
384    pub(crate) auto_copy_metadata: bool,
385
386    // ========== Stream Disable Flags (P1 Features) ==========
387    /// Disable video stream mapping (equivalent to `-vn` in FFmpeg).
388    /// When true, video streams will be excluded from automatic stream mapping.
389    pub(crate) video_disable: bool,
390
391    /// Disable audio stream mapping (equivalent to `-an` in FFmpeg).
392    /// When true, audio streams will be excluded from automatic stream mapping.
393    pub(crate) audio_disable: bool,
394
395    /// Disable subtitle stream mapping (equivalent to `-sn` in FFmpeg).
396    /// When true, subtitle streams will be excluded from automatic stream mapping.
397    pub(crate) subtitle_disable: bool,
398
399    /// Disable data stream mapping (equivalent to `-dn` in FFmpeg).
400    /// When true, data streams will be excluded from automatic stream mapping.
401    /// Data streams include things like timed metadata, chapter markers, etc.
402    pub(crate) data_disable: bool,
403
404    /// Output pixel format (equivalent to `-pix_fmt` in FFmpeg).
405    /// When set, forces the output video to use the specified pixel format.
406    /// Only effective when re-encoding (not when using stream copy).
407    pub(crate) pix_fmt: Option<String>,
408
409    /// CLI-compat only (crate-internal): the hard simple-filter
410    /// prerequisite — when set, context binding fails unless the opened
411    /// input carries exactly one video stream. Set by the `cli` feature's
412    /// lowering for `-vf` commands; never by the public builder API.
413    #[cfg_attr(not(feature = "cli"), allow(dead_code))]
414    pub(crate) require_unique_video_source: bool,
415
416    /// CLI-compat strict mode (crate-internal): leftover AVOptions error
417    /// instead of warning on every component this output drives (muxer,
418    /// encoders). Set only by the `cli` feature's entry points; the default
419    /// builder path keeps today's warn behavior.
420    pub(crate) strict_avoptions: bool,
421
422    /// Per-output simple **video** filter chain (FFmpeg `-vf`), applied to
423    /// this output's re-encoded video stream through the implicit per-output
424    /// filtergraph (it replaces the default `null` chain). Must be a linear
425    /// chain: exactly one video input pad and one video output pad. `None` ⇒
426    /// the passthrough `null` chain. Set via [`Output::set_video_filter`].
427    pub(crate) video_filter: Option<String>,
428
429    /// Per-output simple **audio** filter chain (FFmpeg `-af`), applied to
430    /// this output's re-encoded audio stream through the implicit per-output
431    /// filtergraph (it replaces the default `anull` chain). Must be a linear
432    /// chain: exactly one audio input pad and one audio output pad. `None` ⇒
433    /// the passthrough `anull` chain. Set via [`Output::set_audio_filter`].
434    pub(crate) audio_filter: Option<String>,
435
436    /// Per-type codec FourCC (FFmpeg `-tag:v` / `-tag:a` / `-tag:s`). Stored
437    /// as the user string and parsed at `build()`; `None` ⇒ unset (`0`).
438    pub(crate) video_codec_tag: Option<String>,
439    pub(crate) audio_codec_tag: Option<String>,
440    pub(crate) subtitle_codec_tag: Option<String>,
441
442    /// sws (libswscale) options for the `scale` filters libavfilter
443    /// auto-inserts ahead of this output's encoder. Maps to the graph-level
444    /// `AVFilterGraph.scale_sws_opts`. Default `None`. Set via
445    /// [`Output::set_sws_opts`].
446    pub(crate) sws_opts: Option<String>,
447
448    /// swr (libswresample) options for the `aresample` filters libavfilter
449    /// auto-inserts ahead of this output's encoder. Maps to the graph-level
450    /// `AVFilterGraph.aresample_swr_opts`. Default `None`. Set via
451    /// [`Output::set_swr_opts`].
452    pub(crate) swr_opts: Option<String>,
453
454    /// Files to embed as attachment streams (FFmpeg `-attach`), e.g. fonts or
455    /// cover art. Empty ⇒ no attachments and zero behavior change. Each entry
456    /// is resolved into an `AVMEDIA_TYPE_ATTACHMENT` stream at output build
457    /// time; the file is read then, so a missing/unreadable/empty/oversized
458    /// file surfaces as an `Err` from the context build — never a panic.
459    pub(crate) attachments: Vec<AttachmentSpec>,
460}
461
462#[derive(Copy, Clone, PartialEq)]
463#[non_exhaustive]
464pub enum VSyncMethod {
465    VsyncAuto,
466    VsyncCfr,
467    VsyncVfr,
468    VsyncPassthrough,
469    VsyncVscfr,
470}
471
472impl Output {
473    pub fn new(url: impl Into<String>) -> Self {
474        url.into().into()
475    }
476
477    /// The destination URL, when this output targets one.
478    pub(crate) fn url(&self) -> Option<&str> {
479        match &self.target {
480            OutputTarget::Url(url) => Some(url),
481            _ => None,
482        }
483    }
484
485    /// The single field-literal constructor every public entry point funnels
486    /// through; the target discriminant is the only per-entry difference.
487    fn with_target(target: OutputTarget) -> Self {
488        Self {
489            target,
490            io_buffer_size: None,
491            max_muxing_queue_size: crate::core::context::pre_mux_queue::DEFAULT_PRE_MUX_MAX_PACKETS,
492            muxing_queue_data_threshold:
493                crate::core::context::pre_mux_queue::DEFAULT_PRE_MUX_DATA_THRESHOLD,
494            seek_callback: None,
495            frame_pipelines: None,
496            stream_map_specs: vec![],
497            stream_maps: vec![],
498            format: None,
499            video_codec: None,
500            audio_codec: None,
501            subtitle_codec: None,
502            video_bsf: None,
503            audio_bsf: None,
504            subtitle_bsf: None,
505            start_time_us: None,
506            recording_time_us: None,
507            stop_time_us: None,
508            framerate: None,
509            framerate_max: None,
510            vsync_method: VSyncMethod::VsyncAuto,
511            bits_per_raw_sample: None,
512            audio_sample_rate: None,
513            audio_channels: None,
514            audio_sample_fmt: None,
515            video_qscale: None,
516            audio_qscale: None,
517            forced_kf_spec: ForcedKeyframeSpec::None,
518            max_video_frames: None,
519            max_audio_frames: None,
520            max_subtitle_frames: None,
521            video_codec_opts: None,
522            audio_codec_opts: None,
523            subtitle_codec_opts: None,
524            format_opts: None,
525            global_metadata: None,
526            stream_metadata: Vec::new(),
527            chapter_metadata: HashMap::new(),
528            program_metadata: HashMap::new(),
529            metadata_map: Vec::new(),
530            auto_copy_metadata: true, // FFmpeg default: auto-copy enabled
531            video_disable: false,
532            audio_disable: false,
533            subtitle_disable: false,
534            data_disable: false,
535            pix_fmt: None,
536            require_unique_video_source: false,
537            strict_avoptions: false,
538            video_filter: None,
539            audio_filter: None,
540            video_codec_tag: None,
541            audio_codec_tag: None,
542            subtitle_codec_tag: None,
543            sws_opts: None,
544            swr_opts: None,
545            attachments: Vec::new(),
546            shortest: false,
547            shortest_buf_duration_us: 10_000_000,
548        }
549    }
550
551    /// Creates a new `Output` instance with a custom write callback and format string.
552    ///
553    /// This method initializes an `Output` object that uses a provided `write_callback` function
554    /// to handle the encoded data being written to the output stream. You can optionally specify
555    /// the desired output format via the `format` method.
556    ///
557    /// ### Parameters:
558    /// - `write_callback: fn(buf: &[u8]) -> i32`: A function that processes the provided buffer of
559    ///   encoded data and writes it to the destination. The function should return the number of bytes
560    ///   successfully written (positive value) or a negative value in case of error.
561    ///
562    /// ### Return Value:
563    /// - Returns a new `Output` instance configured with the specified `write_callback` function.
564    ///
565    /// ### Behavior of `write_callback`:
566    /// - **Positive Value**: Indicates the number of bytes successfully written.
567    /// - **Negative Value**: Indicates an error occurred. For example:
568    ///   - `ffmpeg_sys_next::AVERROR(ffmpeg_sys_next::EIO)`: Represents an input/output error.
569    ///   - Other custom-defined error codes can also be returned to signal specific issues.
570    ///
571    /// ### Example:
572    /// ```rust,ignore
573    /// let output = Output::new_by_write_callback(move |buf| {
574    ///     println!("Processing {} bytes of data for output", buf.len());
575    ///     buf.len() as i32 // Return the number of bytes processed
576    /// })
577    /// .set_format("mp4");
578    /// ```
579    pub fn new_by_write_callback<F>(write_callback: F) -> Self
580    where
581        F: FnMut(&[u8]) -> i32 + Send + 'static,
582    {
583        (Box::new(write_callback) as Box<dyn FnMut(&[u8]) -> i32 + Send>).into()
584    }
585
586    /// Creates an `Output` that delivers **encoded packets** to the given
587    /// [`PacketSink`](crate::packet_sink::PacketSink) callbacks instead of
588    /// muxing them into container bytes.
589    ///
590    /// No container is written and no I/O happens: `on_stream_info` fires
591    /// at most once with the finalized stream configuration (valid avcC for
592    /// H.264, AudioSpecificConfig for AAC) — collecting that configuration
593    /// can itself fail, failing the job before any callback runs — then each
594    /// encoded packet is handed to `on_packet` as a borrowed
595    /// [`PacketView`](crate::packet_sink::PacketView).
596    /// See the [`packet_sink`](crate::packet_sink) module docs for the strict
597    /// tier contract, the callback order, and the **blocking backpressure**
598    /// behavior (a slow callback stalls the pipeline; nothing is dropped).
599    ///
600    /// Options a packet sink cannot honor are rejected when the context is
601    /// built, with a typed
602    /// [`PacketSinkError`](crate::error::PacketSinkError). Container-only
603    /// options are rejected because no container is written: `set_format`,
604    /// `set_seek_callback`, `set_io_buffer_size`, `set_format_opt(s)`,
605    /// attachments, and the metadata setters (`add_metadata`,
606    /// `add_stream_metadata`, `add_chapter_metadata`, `add_program_metadata`,
607    /// `map_metadata_from_input`, `disable_auto_copy_metadata`). Pipeline
608    /// features outside the strict tier's delivery contract are rejected as
609    /// policy, not for lack of a container: `set_video_filter`,
610    /// `set_audio_filter`, bitstream
611    /// filters (`set_*_bsf`), `set_subtitle_codec`, stream copy, and the
612    /// `flags` codec option (it could clear the `global_header` flag behind
613    /// the out-of-band configuration). The set tracks the validator and may
614    /// grow. The v1 strict tier accepts only registry-verified encoders
615    /// (video: `libx264`, `h264_nvenc`, `h264_videotoolbox` with `bf=0`,
616    /// `libopenh264`; audio: AAC).
617    ///
618    /// `Output::from(sink)` is the equivalent, crate-conventional spelling
619    /// and the one used throughout the documentation.
620    ///
621    /// ### Example
622    /// ```rust,no_run
623    /// use ez_ffmpeg::packet_sink::PacketSink;
624    /// use ez_ffmpeg::Output;
625    ///
626    /// let sink = PacketSink::builder(|packet| {
627    ///     println!("stream {} pts {}", packet.stream_index(), packet.pts());
628    ///     Ok(())
629    /// })
630    /// .build();
631    /// let output = Output::from(sink).set_video_codec("libx264");
632    /// ```
633    pub fn new_by_packet_sink(sink: crate::core::packet_sink::PacketSink) -> Self {
634        sink.into()
635    }
636
637    /// Sets the AVIO buffer size, in bytes, for a custom `write_callback` output.
638    ///
639    /// FFmpeg hands one buffer-sized chunk per callback, so a larger buffer means
640    /// fewer Rust↔FFmpeg round-trips for sequential or network sinks. Only applies
641    /// when the output is a `write_callback`; ignored for URL outputs, and
642    /// **rejected** on packet-sink outputs (no I/O exists there): building the
643    /// context fails with
644    /// [`PacketSinkError::UnsupportedOption`](crate::error::PacketSinkError::UnsupportedOption).
645    /// The default is 64 KiB, which keeps first-packet latency low for live use.
646    ///
647    /// # Errors
648    /// The value is validated when the context is built:
649    /// `FfmpegContext::builder().build()` fails with
650    /// [`OpenOutputError::InvalidOption`](crate::error::OpenOutputError::InvalidOption)
651    /// if `size` is 0 or exceeds `i32::MAX` (FFmpeg's `avio_alloc_context`
652    /// takes an `int` buffer size).
653    pub fn set_io_buffer_size(mut self, size: usize) -> Self {
654        self.io_buffer_size = Some(size);
655        self
656    }
657
658    /// Sets the per-stream packet cap of the pre-mux queue (FFmpeg
659    /// `-max_muxing_queue_size` parity; default 128).
660    ///
661    /// Until the muxer starts (it waits for every mapped output stream to
662    /// become ready), each encoder parks its packets in a per-stream queue.
663    /// The cap only applies once the queue's byte threshold
664    /// ([`set_muxing_queue_data_threshold`](Output::set_muxing_queue_data_threshold))
665    /// is exceeded — below it, packet count is unlimited. Raise this (or the
666    /// byte threshold) if a job fails with a pre-mux backpressure error, e.g.
667    /// a sparse subtitle stream whose first packet lands deep into a
668    /// high-bitrate file.
669    ///
670    /// # Errors
671    /// Validated when the context is built: `0` fails with
672    /// [`OpenOutputError::InvalidOption`](crate::error::OpenOutputError::InvalidOption).
673    pub fn set_max_muxing_queue_size(mut self, size: usize) -> Self {
674        self.max_muxing_queue_size = size;
675        self
676    }
677
678    /// Sets the per-stream byte threshold below which the pre-mux queue's
679    /// packet cap does not apply (FFmpeg `-muxing_queue_data_threshold`
680    /// parity; default 50 MiB).
681    ///
682    /// This is a trigger, not a hard byte cap: below the threshold the packet
683    /// count is unbounded, and above it admission stops at
684    /// [`max_muxing_queue_size`](Output::set_max_muxing_queue_size). Together
685    /// they bound how much a fast encoder parks before the muxer starts, which
686    /// doubles as the demux read-ahead window: jobs that must read further
687    /// ahead (late first packet on one mapped stream) need a larger threshold
688    /// (and/or packet cap).
689    ///
690    /// # Errors
691    /// Validated when the context is built: `0` fails with
692    /// [`OpenOutputError::InvalidOption`](crate::error::OpenOutputError::InvalidOption).
693    pub fn set_muxing_queue_data_threshold(mut self, bytes: usize) -> Self {
694        self.muxing_queue_data_threshold = bytes;
695        self
696    }
697
698    /// Sets a custom seek callback for the output stream.
699    ///
700    /// This function assigns a user-defined function that handles seeking within the output stream.
701    /// Seeking is required for certain formats (e.g., `mp4`, `mkv`) where metadata or index information
702    /// needs to be updated at specific positions in the file.
703    ///
704    /// **Why is `seek_callback` necessary?**
705    /// - Some formats (e.g., MP4) require `seek` operations to update metadata (`moov`, `mdat`).
706    /// - If no `seek_callback` is provided for formats that require seeking, FFmpeg will fail with:
707    ///   ```text
708    ///   [mp4 @ 0x...] muxer does not support non seekable output
709    ///   ```
710    /// - For streaming formats (`flv`, `ts`, `rtmp`, `hls`), seeking is **not required**.
711    ///
712    /// **FFmpeg may invoke `seek_callback` from different threads.**
713    /// - If using a `File` as the output, **wrap it in `Arc<Mutex<File>>`** to ensure thread-safe access.
714    ///
715    /// ### Parameters:
716    /// - `seek_callback: FnMut(i64, i32) -> i64`
717    ///   - `offset: i64`: The target seek position in the stream.
718    ///   - `whence: i32`: The seek mode determining how `offset` should be interpreted:
719    ///     - `ffmpeg_sys_next::SEEK_SET` (0): Seek to an absolute position.
720    ///     - `ffmpeg_sys_next::SEEK_CUR` (1): Seek relative to the current position.
721    ///     - `ffmpeg_sys_next::SEEK_END` (2): Seek relative to the end of the output.
722    ///     - `ffmpeg_sys_next::AVSEEK_SIZE` (65536): Query the **total size** of the stream
723    ///       instead of seeking.
724    ///
725    ///     `avio_seek` strips `ffmpeg_sys_next::AVSEEK_FORCE` (131072) from `whence` before
726    ///     invoking a custom callback; the example masks it anyway as cheap defense. No
727    ///     other `whence` values reach a custom seek callback.
728    ///
729    /// ### Return Value:
730    /// - **Positive Value**: The new offset position after seeking.
731    /// - **Negative Value**: An error occurred. Common errors include:
732    ///   - `ffmpeg_sys_next::AVERROR(ffmpeg_sys_next::ESPIPE)`: Seek is not supported.
733    ///   - `ffmpeg_sys_next::AVERROR(ffmpeg_sys_next::EIO)`: General I/O error.
734    ///
735    /// ### Example (Thread-safe seek callback using `Arc<Mutex<File>>`):
736    /// Since `FFmpeg` may call `write_callback` and `seek_callback` from different threads,
737    /// **use `Arc<Mutex<File>>` to ensure safe concurrent access.**
738    ///
739    /// ```rust,no_run
740    /// use ez_ffmpeg::Output;
741    /// use std::fs::File;
742    /// use std::io::{Seek, SeekFrom, Write};
743    /// use std::sync::{Arc, Mutex};
744    ///
745    /// // ✅ Create a thread-safe file handle
746    /// let file = Arc::new(Mutex::new(File::create("output.mp4").expect("Failed to create file")));
747    ///
748    /// // ✅ Define the write callback (data writing logic)
749    /// let write_callback = {
750    ///     let file = Arc::clone(&file);
751    ///     move |buf: &[u8]| -> i32 {
752    ///         let mut file = file.lock().unwrap();
753    ///         match file.write_all(buf) {
754    ///             Ok(_) => buf.len() as i32,
755    ///             Err(e) => {
756    ///                 println!("Write error: {}", e);
757    ///                 ffmpeg_sys_next::AVERROR(ffmpeg_sys_next::EIO) as i32
758    ///             }
759    ///         }
760    ///     }
761    /// };
762    ///
763    /// // ✅ Define the seek callback (position adjustment logic)
764    /// let seek_callback = {
765    ///     let file = Arc::clone(&file);
766    ///     Box::new(move |offset: i64, whence: i32| -> i64 {
767    ///         let mut file = file.lock().unwrap();
768    ///
769    ///         // ✅ Handle AVSEEK_SIZE: FFmpeg asks for the total stream size instead of seeking
770    ///         if whence == ffmpeg_sys_next::AVSEEK_SIZE {
771    ///             if let Ok(size) = file.metadata().map(|m| m.len() as i64) {
772    ///                 return size;
773    ///             }
774    ///             return ffmpeg_sys_next::AVERROR(ffmpeg_sys_next::EIO) as i64;
775    ///         }
776    ///
777    ///         // ✅ Defensive: mask AVSEEK_FORCE (avio_seek strips it before a custom
778    ///         // callback). The AVIO layer sends no other whence values (lseek extensions
779    ///         // like SEEK_HOLE/SEEK_DATA never reach a custom callback).
780    ///         match whence & !ffmpeg_sys_next::AVSEEK_FORCE {
781    ///             ffmpeg_sys_next::SEEK_SET => file.seek(SeekFrom::Start(offset as u64)),
782    ///             ffmpeg_sys_next::SEEK_CUR => file.seek(SeekFrom::Current(offset)),
783    ///             ffmpeg_sys_next::SEEK_END => file.seek(SeekFrom::End(offset)),
784    ///             _ => return ffmpeg_sys_next::AVERROR(ffmpeg_sys_next::ESPIPE) as i64,
785    ///         }.map_or(ffmpeg_sys_next::AVERROR(ffmpeg_sys_next::EIO) as i64, |pos| pos as i64)
786    ///     })
787    /// };
788    ///
789    /// // ✅ Create an output with both callbacks
790    /// let output = Output::new_by_write_callback(write_callback)
791    ///     .set_format("mp4")
792    ///     .set_seek_callback(seek_callback);
793    /// ```
794    pub fn set_seek_callback<F>(mut self, seek_callback: F) -> Self
795    where
796        F: FnMut(i64, i32) -> i64 + Send + 'static,
797    {
798        self.seek_callback =
799            Some(Box::new(seek_callback) as Box<dyn FnMut(i64, i32) -> i64 + Send>);
800        self
801    }
802
803    /// Sets the output format for the container.
804    ///
805    /// This method allows you to specify the output format for the container. If no format is specified,
806    /// FFmpeg will attempt to detect it automatically based on the file extension or output URL.
807    ///
808    /// ### Parameters:
809    /// - `format: &str`: A string specifying the desired output format (e.g., `mp4`, `flv`, `mkv`).
810    ///
811    /// ### Return Value:
812    /// - Returns the `Output` instance with the newly set format.
813    pub fn set_format(mut self, format: impl Into<String>) -> Self {
814        self.format = Some(format.into());
815        self
816    }
817
818    /// Sets the **video codec** to be used for encoding.
819    ///
820    /// # Arguments
821    /// * `video_codec` - A string slice representing the desired video codec (e.g., `"h264"`, `"hevc"`).
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_video_codec("h264");
830    /// ```
831    pub fn set_video_codec(mut self, video_codec: impl Into<String>) -> Self {
832        self.video_codec = Some(video_codec.into());
833        self
834    }
835
836    /// Sets the **audio codec** to be used for encoding.
837    ///
838    /// # Arguments
839    /// * `audio_codec` - A string slice representing the desired audio codec (e.g., `"aac"`, `"mp3"`).
840    ///
841    /// # Returns
842    /// * `Self` - Returns the modified `Output` struct, allowing for method chaining.
843    ///
844    /// # Examples
845    /// ```rust,ignore
846    /// let output = Output::from("rtmp://localhost/live/stream")
847    ///     .set_audio_codec("aac");
848    /// ```
849    pub fn set_audio_codec(mut self, audio_codec: impl Into<String>) -> Self {
850        self.audio_codec = Some(audio_codec.into());
851        self
852    }
853
854    /// Sets the **subtitle codec** to be used for encoding.
855    ///
856    /// # Arguments
857    /// * `subtitle_codec` - A string slice representing the desired subtitle codec
858    ///   (e.g., `"mov_text"`, `"webvtt"`).
859    ///
860    /// # Returns
861    /// * `Self` - Returns the modified `Output` struct, allowing for method chaining.
862    ///
863    /// # Examples
864    /// ```rust,ignore
865    /// let output = Output::from("rtmp://localhost/live/stream")
866    ///     .set_subtitle_codec("mov_text");
867    /// ```
868    pub fn set_subtitle_codec(mut self, subtitle_codec: impl Into<String>) -> Self {
869        self.subtitle_codec = Some(subtitle_codec.into());
870        self
871    }
872
873    /// Sets the video stream's codec FourCC, equivalent to FFmpeg `-tag:v`.
874    ///
875    /// The token is either a whole-token integer (decimal or `0x` hex, matching
876    /// FFmpeg's `strtol(..., 0)`) or a four-character tag such as `"hvc1"` /
877    /// `"mp4v"` (little-endian `AV_RL32`). Setting `("tag", ...)` as a codec
878    /// option does **not** reach `codecpar->codec_tag`; this setter is the
879    /// path that does.
880    ///
881    /// The setter is infallible and stores the token as given. An empty string
882    /// fails [`FfmpegContextBuilder::build`](crate::core::context::ffmpeg_context_builder::FfmpegContextBuilder::build) with
883    /// [`OpenOutputError::InvalidOption`]. On stream copy, a user-set tag is
884    /// honored even when the target container cannot represent it (the muxer
885    /// then fails at `write_header`) — the crate does not auto-clear a tag
886    /// the caller asked for.
887    ///
888    /// **Equivalent FFmpeg command:**
889    /// ```sh
890    /// ffmpeg -i input.mp4 -c:v libx265 -tag:v hvc1 output.mp4
891    /// ```
892    ///
893    /// [`OpenOutputError::InvalidOption`]: crate::error::OpenOutputError::InvalidOption
894    pub fn set_video_codec_tag(mut self, tag: impl Into<String>) -> Self {
895        self.video_codec_tag = Some(tag.into());
896        self
897    }
898
899    /// Sets the audio stream's codec FourCC, equivalent to FFmpeg `-tag:a`.
900    ///
901    /// See [`set_video_codec_tag`](Self::set_video_codec_tag) for the token
902    /// grammar and when the value is applied.
903    pub fn set_audio_codec_tag(mut self, tag: impl Into<String>) -> Self {
904        self.audio_codec_tag = Some(tag.into());
905        self
906    }
907
908    /// Sets the subtitle stream's codec FourCC, equivalent to FFmpeg `-tag:s`.
909    ///
910    /// See [`set_video_codec_tag`](Self::set_video_codec_tag) for the token
911    /// grammar and when the value is applied.
912    pub fn set_subtitle_codec_tag(mut self, tag: impl Into<String>) -> Self {
913        self.subtitle_codec_tag = Some(tag.into());
914        self
915    }
916
917    /// Replaces the entire frame-processing pipeline with a new sequence
918    /// of transformations for **pre-encoding** frames on this `Output`.
919    ///
920    /// This method clears any previously set pipelines and replaces them with the provided list.
921    ///
922    /// # Parameters
923    /// * `frame_pipelines` - A list of [`FramePipeline`] instances defining the
924    ///   transformations to apply before encoding.
925    ///
926    /// # Returns
927    /// * `Self` - Returns the modified `Output`, enabling method chaining.
928    ///
929    /// # Example
930    /// ```rust,ignore
931    /// let output = Output::from("some_url")
932    ///     .set_frame_pipelines(vec![
933    ///         FramePipelineBuilder::new(AVMediaType::AVMEDIA_TYPE_VIDEO).filter("opengl", Box::new(my_filter)),
934    ///         // Additional pipelines...
935    ///     ]);
936    /// ```
937    pub fn set_frame_pipelines(mut self, frame_pipelines: Vec<impl Into<FramePipeline>>) -> Self {
938        self.frame_pipelines = Some(
939            frame_pipelines
940                .into_iter()
941                .map(|frame_pipeline| frame_pipeline.into())
942                .collect(),
943        );
944        self
945    }
946
947    /// Adds a single [`FramePipeline`] to the existing pipeline list.
948    ///
949    /// If no pipelines are currently defined, this method creates a new pipeline list.
950    /// Otherwise, it appends the provided pipeline to the existing transformations.
951    ///
952    /// # Parameters
953    /// * `frame_pipeline` - A [`FramePipeline`] defining a transformation.
954    ///
955    /// # Returns
956    /// * `Self` - Returns the modified `Output`, enabling method chaining.
957    ///
958    /// # Example
959    /// ```rust,ignore
960    /// let output = Output::from("some_url")
961    ///     .add_frame_pipeline(FramePipelineBuilder::new(AVMediaType::AVMEDIA_TYPE_VIDEO).filter("opengl", Box::new(my_filter)).build())
962    ///     .add_frame_pipeline(FramePipelineBuilder::new(AVMediaType::AVMEDIA_TYPE_AUDIO).filter("my_custom_filter1", Box::new(...)).filter("my_custom_filter2", Box::new(...)));
963    /// ```
964    pub fn add_frame_pipeline(mut self, frame_pipeline: impl Into<FramePipeline>) -> Self {
965        if self.frame_pipelines.is_none() {
966            self.frame_pipelines = Some(vec![frame_pipeline.into()]);
967        } else {
968            self.frame_pipelines
969                .as_mut()
970                .unwrap()
971                .push(frame_pipeline.into());
972        }
973        self
974    }
975
976    /// Adds a **stream mapping** for a specific stream or stream type,
977    /// **re-encoding** it according to this output’s codec settings.
978    ///
979    /// # Linklabel (FFmpeg-like Specifier)
980    ///
981    /// This string typically follows `"<input_index>:<media_type>"` syntax:
982    /// - **`"0:v"`** – the video stream(s) from input #0.
983    /// - **`"1:a?"`** – audio from input #1, **ignore** if none present (due to `?`).
984    /// - Other possibilities include `"0:s"`, `"0:d"`, etc. for subtitles/data, optionally with `?`.
985    ///
986    /// A plain specifier **re-encodes** the chosen stream(s) with this
987    /// output's codec settings (unless the resolved codec is `"copy"`).
988    /// For a bit-for-bit copy, see
989    /// [`add_stream_map_with_copy`](Self::add_stream_map_with_copy) or
990    /// [`StreamMap::codec`] with `"copy"`.
991    ///
992    /// # Per-map encoder selection
993    ///
994    /// Passing a [`StreamMap`] instead of a plain string attaches a per-map
995    /// encoder and per-map encoder options to the mapped stream(s) — the
996    /// builder equivalent of FFmpeg's indexed `-c:v:0 libx264 -b:v:0 4M`.
997    /// A per-map codec overrides the per-type
998    /// [`set_video_codec`](Self::set_video_codec) /
999    /// [`set_audio_codec`](Self::set_audio_codec) /
1000    /// [`set_subtitle_codec`](Self::set_subtitle_codec) value for exactly
1001    /// the streams the map matches; per-map options merge key by key over
1002    /// the per-type option tables. See [`StreamMap`] for the precedence and
1003    /// granularity rules.
1004    ///
1005    /// # Parameters
1006    /// - `map`: An FFmpeg-style specifier (`"0:v"`, `"1:a?"`, a filter
1007    ///   output label like `"[v0]"`), or a [`StreamMap`] carrying per-map
1008    ///   encoder settings.
1009    ///
1010    /// # Returns
1011    /// * `Self` - for chained method calls.
1012    ///
1013    /// # Example
1014    /// ```rust,ignore
1015    /// // Re-encode the video stream from input #0 (fail if no video).
1016    /// let output = Output::from("output.mp4")
1017    ///     .add_stream_map("0:v");
1018    ///
1019    /// // Two audio tracks of the same input, each with its own encoder —
1020    /// // FFmpeg: -map 0:a:0 -c:a:0 aac -b:a:0 128k -map 0:a:1 -c:a:1 libopus
1021    /// let output = Output::from("output.mkv")
1022    ///     .add_stream_map(StreamMap::new("0:a:0").codec("aac").codec_opt("b", "128k"))
1023    ///     .add_stream_map(StreamMap::new("0:a:1").codec("libopus"));
1024    /// ```
1025    pub fn add_stream_map(mut self, map: impl Into<StreamMap>) -> Self {
1026        self.stream_map_specs.push(map.into());
1027        self
1028    }
1029
1030    /// Adds a **stream mapping** for a specific stream or stream type,
1031    /// **copying** it bit-for-bit from the source without re-encoding.
1032    ///
1033    /// # Linklabel (FFmpeg-like Specifier)
1034    ///
1035    /// Follows the same `"<input_index>:<media_type>"` pattern as [`add_stream_map`](Self::add_stream_map):
1036    /// - **`"0:a"`** – audio stream(s) from input #0.
1037    /// - **`"0:a?"`** – same, but ignore errors if no audio exists.
1038    /// - And so on for video (`v`), subtitles (`s`), attachments (`t`), etc.
1039    ///
1040    /// # Copy vs. Re-encode
1041    ///
1042    /// Here, `copy = true` by default, meaning the chosen stream(s) are passed through
1043    /// **without** decoding/encoding. This generally **only** works if the source’s codec
1044    /// is compatible with the container/format you’re outputting to.
1045    /// If you require re-encoding (e.g., to ensure compatibility or apply filters),
1046    /// use [`add_stream_map`](Self::add_stream_map).
1047    ///
1048    /// # Parameters
1049    /// - `map`: An FFmpeg-style specifier referencing the desired input index and
1050    ///   media type, like `"0:v?"`, or a [`StreamMap`]. The copy flag is
1051    ///   forced on; combining it with a *different* [`StreamMap::codec`]
1052    ///   (anything but `"copy"`, which is redundant and accepted on a
1053    ///   selecting map) or with any [`StreamMap::codec_opt`] entry is
1054    ///   rejected at build time
1055    ///   ([`OpenOutputError::StreamMapCopyConflict`](crate::error::OpenOutputError::StreamMapCopyConflict)).
1056    ///   A negative (disabling) map such as `"-0:v"` rejects ANY per-map
1057    ///   codec — `"copy"` included — with
1058    ///   [`OpenOutputError::InvalidOption`](crate::error::OpenOutputError::InvalidOption):
1059    ///   a disabling map carries no encoder intent to attach it to.
1060    ///
1061    /// Behavior change in 0.16: passing a filter output label (e.g.
1062    /// `"[v0]"`) with copy now also fails at `build()` with the same
1063    /// `StreamMapCopyConflict` — a filter-graph output carries no source
1064    /// packets to copy. Through 0.15 the copy request was silently ignored
1065    /// and the labeled stream re-encoded; the new error matches the FFmpeg
1066    /// CLI, which rejects combining filtergraphs with streamcopy.
1067    ///
1068    /// # Returns
1069    /// * `Self` - for chained method calls.
1070    ///
1071    /// # Example
1072    /// ```rust,ignore
1073    /// // Copy the audio stream(s) from input #0 if present, no re-encode:
1074    /// let output = Output::from("output.mkv")
1075    ///     .add_stream_map_with_copy("0:a?");
1076    /// ```
1077    pub fn add_stream_map_with_copy(mut self, map: impl Into<StreamMap>) -> Self {
1078        let mut map = map.into();
1079        map.copy = true;
1080        self.stream_map_specs.push(map);
1081        self
1082    }
1083
1084    /// Sets the **start time** (in microseconds) for output encoding.
1085    ///
1086    /// If this is set, FFmpeg will attempt to start encoding from the specified
1087    /// timestamp in the input stream. This can be used to skip initial content.
1088    ///
1089    /// # Parameters
1090    /// * `start_time_us` - The start time in microseconds.
1091    ///
1092    /// # Returns
1093    /// * `Self` - The modified `Output`, allowing method chaining.
1094    ///
1095    /// # Example
1096    /// ```rust,ignore
1097    /// let output = Output::from("output.mp4")
1098    ///     .set_start_time_us(2_000_000); // Start at 2 seconds
1099    /// ```
1100    pub fn set_start_time_us(mut self, start_time_us: i64) -> Self {
1101        self.start_time_us = Some(start_time_us);
1102        self
1103    }
1104
1105    /// Sets the **recording time** (in microseconds) for output encoding.
1106    ///
1107    /// This indicates how many microseconds of data should be processed
1108    /// (i.e., maximum duration to encode). Once this time is reached,
1109    /// FFmpeg will stop encoding.
1110    ///
1111    /// # Parameters
1112    /// * `recording_time_us` - The maximum duration (in microseconds) to process.
1113    ///
1114    /// # Returns
1115    /// * `Self` - The modified `Output`, allowing method chaining.
1116    ///
1117    /// # Example
1118    /// ```rust,ignore
1119    /// let output = Output::from("output.mp4")
1120    ///     .set_recording_time_us(5_000_000); // Record for 5 seconds
1121    /// ```
1122    pub fn set_recording_time_us(mut self, recording_time_us: i64) -> Self {
1123        self.recording_time_us = Some(recording_time_us);
1124        self
1125    }
1126
1127    /// Sets a **stop time** (in microseconds) for output encoding.
1128    ///
1129    /// If set, FFmpeg will stop encoding once the input’s timestamp
1130    /// surpasses this value. Effectively, encoding ends at this timestamp
1131    /// regardless of remaining data.
1132    ///
1133    /// # Parameters
1134    /// * `stop_time_us` - The timestamp (in microseconds) at which to stop.
1135    ///
1136    /// # Returns
1137    /// * `Self` - The modified `Output`, allowing method chaining.
1138    ///
1139    /// # Example
1140    /// ```rust,ignore
1141    /// let output = Output::from("output.mp4")
1142    ///     .set_stop_time_us(10_000_000); // Stop at 10 seconds
1143    /// ```
1144    pub fn set_stop_time_us(mut self, stop_time_us: i64) -> Self {
1145        self.stop_time_us = Some(stop_time_us);
1146        self
1147    }
1148
1149    /// Finish the output when its shortest limiting stream ends (FFmpeg `-shortest`).
1150    ///
1151    /// Encoded audio/video are truncated at the **frame** level before encoding
1152    /// (no B-frame stranding); streamcopy / subtitle / data are truncated at the
1153    /// **packet** level — the same presentation-time cut FFmpeg makes, with the
1154    /// same limitation that a copy B-frame near the cut may reference a dropped
1155    /// later packet. Exact when the shortest→longest gap is within the buffering
1156    /// window (see [`set_shortest_buf_duration_us`](Self::set_shortest_buf_duration_us),
1157    /// default 10 s). Default: `false`.
1158    ///
1159    /// # Limitations
1160    /// Any cut stream fed by an input whose read cannot be interrupted mid-packet —
1161    /// a pipe, a custom IO source, a live device, or a readrate-limited (`-re`)
1162    /// input — may keep that demuxer alive until its in-flight read returns,
1163    /// delaying termination. Ordinary seekable file and network inputs are
1164    /// unaffected, as is a single encoded stream (there is nothing to cut it against).
1165    ///
1166    /// When a cut stream also has an output bitstream filter that reorders, buffers,
1167    /// or rewrites packet timestamps (e.g. `setts`, `pgs_frame_merge`), the
1168    /// packet-level cut is decided on the pre-filter timestamps. Timestamp-preserving
1169    /// 1:1 filters (`h264_mp4toannexb`, `aac_adtstoasc`, metadata filters) are unaffected.
1170    ///
1171    /// # Example
1172    /// ```rust,ignore
1173    /// let output = Output::from("output.mp4").set_shortest(true);
1174    /// ```
1175    pub fn set_shortest(mut self, shortest: bool) -> Self {
1176        self.shortest = shortest;
1177        self
1178    }
1179
1180    /// Maximum microseconds one stream is buffered waiting for a lagging peer
1181    /// before it is released anyway (FFmpeg `-shortest_buf_duration`, expressed in
1182    /// seconds upstream, microseconds here). Bounds `-shortest` memory use and
1183    /// precision. Values `<= 0` are ignored. Default: `10_000_000` (10 s).
1184    ///
1185    /// # Example
1186    /// ```rust,ignore
1187    /// let output = Output::from("output.mp4")
1188    ///     .set_shortest(true)
1189    ///     .set_shortest_buf_duration_us(30_000_000); // tolerate a 30 s gap
1190    /// ```
1191    pub fn set_shortest_buf_duration_us(mut self, shortest_buf_duration_us: i64) -> Self {
1192        if shortest_buf_duration_us > 0 {
1193            self.shortest_buf_duration_us = shortest_buf_duration_us;
1194        }
1195        self
1196    }
1197
1198    /// Sets a **target frame rate** for output encoding, as a `num/den`
1199    /// rational (e.g. `30, 1` for 30 FPS).
1200    ///
1201    /// This can force the output to use a specific frame rate (e.g., 30/1 for 30 FPS).
1202    /// If unset, FFmpeg typically preserves the source frame rate or uses defaults
1203    /// based on the selected codec/container.
1204    ///
1205    /// # Parameters
1206    /// * `num`: Frame rate numerator (e.g., 30 for 30fps, 24000 for 23.976fps)
1207    /// * `den`: Frame rate denominator (e.g., 1 for 30fps, 1001 for 23.976fps)
1208    ///
1209    /// # Returns
1210    /// * `Self` - The modified `Output`, allowing method chaining.
1211    ///
1212    /// # Errors
1213    /// The value is validated when the context is built:
1214    /// `FfmpegContext::builder().build()` fails with
1215    /// [`OpenOutputError::InvalidOption`](crate::error::OpenOutputError::InvalidOption)
1216    /// if `num` or `den` is not positive.
1217    ///
1218    /// # Example
1219    /// ```rust,ignore
1220    /// let output = Output::from("output.mp4")
1221    ///     .set_framerate(30, 1);
1222    /// ```
1223    pub fn set_framerate(mut self, num: i32, den: i32) -> Self {
1224        self.framerate = Some(AVRational { num, den });
1225        self
1226    }
1227
1228    /// Sets a **maximum frame rate** cap for output encoding (`-fpsmax`).
1229    ///
1230    /// Unlike [`set_framerate`](Self::set_framerate), this does not force a
1231    /// rate: the output keeps its native frame rate and is only clamped when
1232    /// that rate exceeds the cap or cannot be determined
1233    /// (ffmpeg_filter.c choose_out_timebase).
1234    ///
1235    /// # Parameters
1236    /// * `num`: Upper-bound numerator (e.g., 30 for a 30fps cap)
1237    /// * `den`: Upper-bound denominator
1238    ///
1239    /// # Returns
1240    /// * `Self` - The modified `Output`, allowing method chaining.
1241    ///
1242    /// # Errors
1243    /// Validated when the context is built, like
1244    /// [`set_framerate`](Self::set_framerate): non-positive values fail with
1245    /// [`OpenOutputError::InvalidOption`](crate::error::OpenOutputError::InvalidOption).
1246    ///
1247    /// # Example
1248    /// ```rust,ignore
1249    /// let output = Output::from("output.mp4")
1250    ///     .set_framerate_max(30, 1);
1251    /// ```
1252    pub fn set_framerate_max(mut self, num: i32, den: i32) -> Self {
1253        self.framerate_max = Some(AVRational { num, den });
1254        self
1255    }
1256
1257    /// Sets the **video sync method** to be used during encoding.
1258    ///
1259    /// FFmpeg uses a variety of vsync policies to handle frame presentation times,
1260    /// dropping/duplicating frames as needed. Adjusting this can be useful when
1261    /// you need strict CFR (constant frame rate), or to pass frames through
1262    /// without modification (`VsyncPassthrough`).
1263    ///
1264    /// # Parameters
1265    /// * `method` - A variant of [`VSyncMethod`], such as `VsyncCfr` or `VsyncVfr`.
1266    ///
1267    /// # Returns
1268    /// * `Self` - The modified `Output`, allowing method chaining.
1269    ///
1270    /// # Example
1271    /// ```rust,ignore
1272    /// let output = Output::from("output.mp4")
1273    ///     .set_vsync_method(VSyncMethod::VsyncCfr);
1274    /// ```
1275    pub fn set_vsync_method(mut self, method: VSyncMethod) -> Self {
1276        self.vsync_method = method;
1277        self
1278    }
1279
1280    /// Sets the **bits per raw sample** for video encoding.
1281    ///
1282    /// This value can influence quality or color depth when dealing with
1283    /// certain pixel formats. Commonly used for high-bit-depth workflows
1284    /// or specialized encoding scenarios.
1285    ///
1286    /// # Parameters
1287    /// * `bits` - The bits per raw sample (e.g., 8, 10, 12).
1288    ///
1289    /// # Returns
1290    /// * `Self` - The modified `Output`, allowing method chaining.
1291    ///
1292    /// # Example
1293    /// ```rust,ignore
1294    /// let output = Output::from("output.mkv")
1295    ///     .set_bits_per_raw_sample(10); // e.g., 10-bit
1296    /// ```
1297    pub fn set_bits_per_raw_sample(mut self, bits: i32) -> Self {
1298        self.bits_per_raw_sample = Some(bits);
1299        self
1300    }
1301
1302    /// Sets the **audio sample rate** (in Hz) for output encoding.
1303    ///
1304    /// This method allows you to specify the desired audio sample rate for the output.
1305    /// Common values include 44100 (CD quality), 48000 (standard for digital video),
1306    /// and 22050 or 16000 (for lower bitrate applications).
1307    ///
1308    /// # Parameters
1309    /// * `audio_sample_rate` - The sample rate in Hertz (e.g., 44100, 48000).
1310    ///
1311    /// # Returns
1312    /// * `Self` - The modified `Output`, allowing method chaining.
1313    ///
1314    /// # Example
1315    /// ```rust,ignore
1316    /// let output = Output::from("output.mp4")
1317    ///     .set_audio_sample_rate(48000); // Set to 48kHz
1318    /// ```
1319    pub fn set_audio_sample_rate(mut self, audio_sample_rate: i32) -> Self {
1320        self.audio_sample_rate = Some(audio_sample_rate);
1321        self
1322    }
1323
1324    /// Sets the number of **audio channels** for output encoding.
1325    ///
1326    /// Common values include 1 (mono), 2 (stereo), 5.1 (6 channels), and 7.1 (8 channels).
1327    /// This setting affects the spatial audio characteristics of the output.
1328    ///
1329    /// # Parameters
1330    /// * `audio_channels` - The number of audio channels (e.g., 1 for mono, 2 for stereo).
1331    ///
1332    /// # Returns
1333    /// * `Self` - The modified `Output`, allowing method chaining.
1334    ///
1335    /// # Example
1336    /// ```rust,ignore
1337    /// let output = Output::from("output.mp4")
1338    ///     .set_audio_channels(2); // Set to stereo
1339    /// ```
1340    pub fn set_audio_channels(mut self, audio_channels: i32) -> Self {
1341        self.audio_channels = Some(audio_channels);
1342        self
1343    }
1344
1345    /// Sets the **audio sample format** for output encoding, by FFmpeg
1346    /// format name — the same currency as [`set_pix_fmt`](Self::set_pix_fmt).
1347    ///
1348    /// Common names (see `ffmpeg -sample_fmts`):
1349    /// - `"s16"` (signed 16-bit)
1350    /// - `"s32"` (signed 32-bit)
1351    /// - `"flt"` (32-bit float)
1352    /// - `"fltp"` (32-bit float, planar)
1353    ///
1354    /// # Parameters
1355    /// * `sample_fmt` - The FFmpeg sample format name (e.g., `"s16"`).
1356    ///
1357    /// # Returns
1358    /// * `Self` - The modified `Output`, allowing method chaining.
1359    ///
1360    /// # Errors
1361    /// The name is resolved when the context is built (like
1362    /// [`set_pix_fmt`](Self::set_pix_fmt)): an unknown name fails with
1363    /// [`OpenOutputError::UnknownSampleFormat`](crate::error::OpenOutputError::UnknownSampleFormat).
1364    ///
1365    /// # Example
1366    /// ```rust,ignore
1367    /// let output = Output::from("output.mp4")
1368    ///     .set_audio_sample_fmt("s16"); // signed 16-bit
1369    /// ```
1370    pub fn set_audio_sample_fmt(mut self, sample_fmt: impl Into<String>) -> Self {
1371        self.audio_sample_fmt = Some(sample_fmt.into());
1372        self
1373    }
1374
1375    /// Sets the **video quality scale** (VBR) for encoding.
1376    ///
1377    /// This method configures a fixed quality scale for variable bitrate (VBR) video encoding.
1378    /// Lower values result in higher quality but larger file sizes, while higher values
1379    /// produce lower quality with smaller file sizes.
1380    ///
1381    /// # Note on Modern Usage
1382    /// While still supported, using fixed quality scale (`-q:v`) is generally not recommended
1383    /// for modern video encoding workflows with codecs like H.264 and H.265. Instead, consider:
1384    /// * For H.264/H.265: Use CRF (Constant Rate Factor) via `-crf` parameter
1385    /// * For two-pass encoding: Use target bitrate settings
1386    ///
1387    /// This parameter is primarily useful for older codecs or specific scenarios where
1388    /// direct quality scale control is needed.
1389    ///
1390    /// # Quality Scale Ranges by Codec
1391    /// * **H.264/H.265**: 0-51 (if needed: 17-28)
1392    ///   - 17-18: Visually lossless
1393    ///   - 23: High quality
1394    ///   - 28: Good quality with reasonable file size
1395    /// * **MPEG-4/MPEG-2**: 2-31 (recommended: 2-6)
1396    ///   - Lower values = higher quality
1397    /// * **VP9**: 0-63 (if needed: 15-35)
1398    ///
1399    /// # Parameters
1400    /// * `video_qscale` - The quality scale value for video encoding.
1401    ///
1402    /// # Returns
1403    /// * `Self` - The modified `Output`, allowing method chaining.
1404    ///
1405    /// # Example
1406    /// ```rust,ignore
1407    /// // For MJPEG encoding of image sequences
1408    /// let output = Output::from("output.jpg")
1409    ///     .set_video_qscale(2);  // High quality JPEG images
1410    ///
1411    /// // For legacy image format conversion
1412    /// let output = Output::from("output.png")
1413    ///     .set_video_qscale(3);  // Controls compression level
1414    /// ```
1415    pub fn set_video_qscale(mut self, video_qscale: i32) -> Self {
1416        self.video_qscale = Some(video_qscale);
1417        self
1418    }
1419
1420    /// Force the **video** encoder to emit a keyframe (an IDR request) at the given
1421    /// absolute output times — the list form of FFmpeg's `-force_key_frames "0,5,10.5"`.
1422    ///
1423    /// `spec` is a comma-separated list of times in **seconds** (e.g. `"0,5,10.5"`).
1424    /// Each token is parsed as a decimal number of seconds and converted to
1425    /// microseconds. The list is sorted ascending internally, so input order does not
1426    /// matter; duplicate times are kept (matching FFmpeg).
1427    ///
1428    /// # Semantics and limitations
1429    /// * Times are **absolute** output/encoder presentation timestamps, not offsets
1430    ///   relative to the first frame.
1431    /// * `pict_type = I` is a **request**: software encoders such as `mpeg4`,
1432    ///   `libx264` and `libx265` honor it and emit a keyframe; some hardware encoders
1433    ///   may ignore it or keep their own GOP cadence. ez-ffmpeg can guarantee no more
1434    ///   than the FFmpeg CLI does here.
1435    /// * Applies only to **re-encoded video** streams. Audio, subtitle, and
1436    ///   stream-copy outputs ignore it; there is no effect if it is never set.
1437    /// * MVP grammar: only a comma-separated list of decimal **seconds** is supported.
1438    ///   The `HH:MM:SS`, `expr:`, `source`, and `source_no_drop` forms of FFmpeg's
1439    ///   option are **not** supported and such tokens return an error.
1440    /// * **Negative times are rejected** — an ez-ffmpeg MVP choice; a negative forced
1441    ///   time is meaningless.
1442    ///
1443    /// # Errors
1444    /// The spec is stored as given and validated when the context is built
1445    /// (like every other deferred option): `FfmpegContext::builder().build()`
1446    /// fails with [`OpenOutputError::InvalidOption`](crate::error::OpenOutputError::InvalidOption)
1447    /// if it is empty, contains an empty token, or contains a token that is
1448    /// not a finite, non-negative decimal number (this rejects `NaN`,
1449    /// infinities, and values that would overflow `i64` microseconds).
1450    ///
1451    /// # Example
1452    /// ```rust,ignore
1453    /// let output = Output::from("output.mp4")
1454    ///     .set_video_codec("libx264")
1455    ///     .set_force_key_frames("0,5,10.5");
1456    /// ```
1457    pub fn set_force_key_frames(mut self, spec: impl Into<String>) -> Self {
1458        self.forced_kf_spec = ForcedKeyframeSpec::Times(spec.into());
1459        self
1460    }
1461
1462    /// Request an intra frame at the first video frame with a valid timestamp
1463    /// and every `interval` thereafter, anchored to that first timestamp.
1464    ///
1465    /// Subsequent targets are `origin + k * interval` in microseconds. Each
1466    /// target is applied to the first frame whose PTS is at least that
1467    /// instant. If a frame skips one or more targets (a drop), that frame is
1468    /// forced once and the cursor advances to the first target strictly after
1469    /// its PTS — missed targets are not repaid on later frames.
1470    ///
1471    /// Times are encoder presentation timestamps. `pict_type = I` remains a
1472    /// request: software encoders typically honor it; hardware encoders may
1473    /// not. Applies only to re-encoded video. A zero interval is rejected when
1474    /// the context is built.
1475    ///
1476    /// This is the typed periodic subset of FFmpeg's `-force_key_frames`; it
1477    /// does not parse `expr:` strings. The list form
1478    /// [`set_force_key_frames`](Self::set_force_key_frames) is unchanged.
1479    ///
1480    /// Calling this after [`set_force_key_frames`](Self::set_force_key_frames)
1481    /// (or the reverse) replaces the previous request.
1482    pub fn set_force_key_frames_interval(mut self, interval: std::time::Duration) -> Self {
1483        let interval_us = i64::try_from(interval.as_micros()).unwrap_or(i64::MAX);
1484        self.forced_kf_spec = ForcedKeyframeSpec::Periodic { interval_us };
1485        self
1486    }
1487
1488    /// Sets the **audio quality scale** for encoding.
1489    ///
1490    /// This method configures codec-specific audio quality settings. The range, behavior,
1491    /// and optimal values depend entirely on the audio codec being used.
1492    ///
1493    /// # Quality Scale Ranges by Codec
1494    /// * **MP3 (libmp3lame)**: 0-9 (recommended: 2-5)
1495    ///   - 0: Highest quality
1496    ///   - 2: Near-transparent quality (~190-200 kbps)
1497    ///   - 5: Good quality (~130 kbps)
1498    ///   - 9: Lowest quality
1499    /// * **AAC**: 0.1-255 (recommended: 1-5)
1500    ///   - 1: Highest quality (~250 kbps)
1501    ///   - 3: Good quality (~160 kbps)
1502    ///   - 5: Medium quality (~100 kbps)
1503    /// * **Vorbis**: -1 to 10 (recommended: 3-8)
1504    ///   - 10: Highest quality
1505    ///   - 5: Good quality
1506    ///   - 3: Medium quality
1507    ///
1508    /// # Parameters
1509    /// * `audio_qscale` - The quality scale value for audio encoding.
1510    ///
1511    /// # Returns
1512    /// * `Self` - The modified `Output`, allowing method chaining.
1513    ///
1514    /// # Example
1515    /// ```rust,ignore
1516    /// // For MP3 encoding at high quality
1517    /// let output = Output::from("output.mp3")
1518    ///     .set_audio_codec("libmp3lame")
1519    ///     .set_audio_qscale(2);
1520    ///
1521    /// // For AAC encoding at good quality
1522    /// let output = Output::from("output.m4a")
1523    ///     .set_audio_codec("aac")
1524    ///     .set_audio_qscale(3);
1525    ///
1526    /// // For Vorbis encoding at high quality
1527    /// let output = Output::from("output.ogg")
1528    ///     .set_audio_codec("libvorbis")
1529    ///     .set_audio_qscale(7);
1530    /// ```
1531    pub fn set_audio_qscale(mut self, audio_qscale: i32) -> Self {
1532        self.audio_qscale = Some(audio_qscale);
1533        self
1534    }
1535
1536    /// **Sets the maximum number of video frames to encode (`-frames:v`).**
1537    ///
1538    /// **Equivalent FFmpeg Command:**
1539    /// ```sh
1540    /// ffmpeg -i input.mp4 -frames:v 100 output.mp4
1541    /// ```
1542    ///
1543    /// **Example Usage:**
1544    /// ```rust,ignore
1545    /// let output = Output::from("some_url")
1546    ///     .set_max_video_frames(500);
1547    /// ```
1548    pub fn set_max_video_frames(mut self, max_frames: impl Into<Option<i64>>) -> Self {
1549        self.max_video_frames = max_frames.into();
1550        self
1551    }
1552
1553    /// **Sets the maximum number of audio frames to encode (`-frames:a`).**
1554    ///
1555    /// **Equivalent FFmpeg Command:**
1556    /// ```sh
1557    /// ffmpeg -i input.mp4 -frames:a 500 output.mp4
1558    /// ```
1559    ///
1560    /// **Example Usage:**
1561    /// ```rust,ignore
1562    /// let output = Output::from("some_url")
1563    ///     .set_max_audio_frames(500);
1564    /// ```
1565    pub fn set_max_audio_frames(mut self, max_frames: impl Into<Option<i64>>) -> Self {
1566        self.max_audio_frames = max_frames.into();
1567        self
1568    }
1569
1570    /// **Sets the maximum number of subtitle frames to encode (`-frames:s`).**
1571    ///
1572    /// **Equivalent FFmpeg Command:**
1573    /// ```sh
1574    /// ffmpeg -i input.mp4 -frames:s 200 output.mp4
1575    /// ```
1576    ///
1577    /// **Example Usage:**
1578    /// ```rust,ignore
1579    /// let output = Output::from("some_url")
1580    ///     .set_max_subtitle_frames(200);
1581    /// ```
1582    pub fn set_max_subtitle_frames(mut self, max_frames: impl Into<Option<i64>>) -> Self {
1583        self.max_subtitle_frames = max_frames.into();
1584        self
1585    }
1586
1587    // ========== Stream Disable & Format API Methods (P1 Features) ==========
1588    // These methods replicate FFmpeg's `-vn`, `-an`, `-sn`, `-b:v`, `-b:a`, and `-pix_fmt` options.
1589
1590    /// Disables video stream mapping (equivalent to `-vn` in FFmpeg).
1591    ///
1592    /// Video streams will be excluded from automatic stream mapping.
1593    /// This is useful when you want to extract only audio from a video file.
1594    ///
1595    /// **Equivalent FFmpeg Command:**
1596    /// ```sh
1597    /// ffmpeg -i input.mp4 -vn output.mp3
1598    /// ```
1599    ///
1600    /// # Examples
1601    /// ```rust,ignore
1602    /// // Extract audio only, no video
1603    /// let output = Output::from("output.mp3")
1604    ///     .disable_video();
1605    /// ```
1606    pub fn disable_video(mut self) -> Self {
1607        self.video_disable = true;
1608        self
1609    }
1610
1611    /// Disables audio stream mapping (equivalent to `-an` in FFmpeg).
1612    ///
1613    /// Audio streams will be excluded from automatic stream mapping.
1614    /// This is useful when you want to create a silent video.
1615    ///
1616    /// **Equivalent FFmpeg Command:**
1617    /// ```sh
1618    /// ffmpeg -i input.mp4 -an output.mp4
1619    /// ```
1620    ///
1621    /// # Examples
1622    /// ```rust,ignore
1623    /// // Create video without audio
1624    /// let output = Output::from("output.mp4")
1625    ///     .disable_audio();
1626    /// ```
1627    pub fn disable_audio(mut self) -> Self {
1628        self.audio_disable = true;
1629        self
1630    }
1631
1632    /// Disables subtitle stream mapping (equivalent to `-sn` in FFmpeg).
1633    ///
1634    /// Subtitle streams will be excluded from automatic stream mapping.
1635    ///
1636    /// **Equivalent FFmpeg Command:**
1637    /// ```sh
1638    /// ffmpeg -i input.mkv -sn output.mkv
1639    /// ```
1640    ///
1641    /// # Examples
1642    /// ```rust,ignore
1643    /// // Copy video and audio, but exclude subtitles
1644    /// let output = Output::from("output.mkv")
1645    ///     .disable_subtitle();
1646    /// ```
1647    pub fn disable_subtitle(mut self) -> Self {
1648        self.subtitle_disable = true;
1649        self
1650    }
1651
1652    /// Disables data stream mapping (equivalent to `-dn` in FFmpeg).
1653    ///
1654    /// Data streams (timed metadata, chapter markers, etc.) will be
1655    /// excluded from automatic stream mapping.
1656    ///
1657    /// **Equivalent FFmpeg Command:**
1658    /// ```sh
1659    /// ffmpeg -i input.mkv -dn output.mp4
1660    /// ```
1661    ///
1662    /// # Examples
1663    /// ```rust,ignore
1664    /// // Copy video and audio, but exclude data streams
1665    /// let output = Output::from("output.mp4")
1666    ///     .disable_data();
1667    /// ```
1668    pub fn disable_data(mut self) -> Self {
1669        self.data_disable = true;
1670        self
1671    }
1672
1673    /// Sets the video bitrate (equivalent to `-b:v` in FFmpeg).
1674    ///
1675    /// The bitrate string follows FFmpeg conventions:
1676    /// - `"1M"` or `"1000k"` for 1 Mbps
1677    /// - `"500k"` for 500 Kbps
1678    /// - `"2M"` for 2 Mbps
1679    ///
1680    /// **Equivalent FFmpeg Command:**
1681    /// ```sh
1682    /// ffmpeg -i input.mp4 -b:v 2M output.mp4
1683    /// ```
1684    ///
1685    /// # Examples
1686    /// ```rust,ignore
1687    /// let output = Output::from("output.mp4")
1688    ///     .set_video_bitrate("2M");
1689    /// ```
1690    pub fn set_video_bitrate(self, bitrate: impl Into<String>) -> Self {
1691        self.set_video_codec_opt("b", bitrate)
1692    }
1693
1694    /// Sets the audio bitrate (equivalent to `-b:a` in FFmpeg).
1695    ///
1696    /// The bitrate string follows FFmpeg conventions:
1697    /// - `"128k"` for 128 Kbps
1698    /// - `"192k"` for 192 Kbps
1699    /// - `"320k"` for 320 Kbps
1700    ///
1701    /// **Equivalent FFmpeg Command:**
1702    /// ```sh
1703    /// ffmpeg -i input.mp4 -b:a 192k output.mp4
1704    /// ```
1705    ///
1706    /// # Examples
1707    /// ```rust,ignore
1708    /// let output = Output::from("output.mp4")
1709    ///     .set_audio_bitrate("192k");
1710    /// ```
1711    pub fn set_audio_bitrate(self, bitrate: impl Into<String>) -> Self {
1712        self.set_audio_codec_opt("b", bitrate)
1713    }
1714
1715    /// Sets the output pixel format (equivalent to `-pix_fmt` in FFmpeg).
1716    ///
1717    /// Common pixel formats include:
1718    /// - `"yuv420p"` - Most compatible format for H.264
1719    /// - `"yuv444p"` - Higher quality, less compatible
1720    /// - `"rgb24"` - RGB format
1721    /// - `"nv12"` - Common for hardware encoding
1722    ///
1723    /// To see all available formats, run: `ffmpeg -pix_fmts`
1724    ///
1725    /// # Behavior
1726    ///
1727    /// - **Unknown format name**: Returns [`OpenOutputError::UnknownPixelFormat`] error.
1728    ///   This matches FFmpeg CLI behavior (e.g., `ffmpeg -pix_fmt foobar` also fails).
1729    /// - **Format incompatible with encoder**: The filter graph automatically converts
1730    ///   to a compatible format. For example, specifying `rgb48be` with libx264 will
1731    ///   auto-convert to `yuv420p`.
1732    /// - **Stream copy mode**: This setting has no effect when using `-c:v copy`.
1733    ///
1734    /// **Equivalent FFmpeg Command:**
1735    /// ```sh
1736    /// ffmpeg -i input.mp4 -pix_fmt yuv420p output.mp4
1737    /// ```
1738    ///
1739    /// # Examples
1740    /// ```rust,ignore
1741    /// let output = Output::from("output.mp4")
1742    ///     .set_pix_fmt("yuv420p");
1743    /// ```
1744    ///
1745    /// [`OpenOutputError::UnknownPixelFormat`]: crate::error::OpenOutputError::UnknownPixelFormat
1746    pub fn set_pix_fmt(mut self, pix_fmt: impl Into<String>) -> Self {
1747        self.pix_fmt = Some(pix_fmt.into());
1748        self
1749    }
1750
1751    /// Sets a simple **video** filter chain for this output, equivalent to
1752    /// FFmpeg `-vf` (`-filter:v`).
1753    ///
1754    /// The chain is applied to this output's **re-encoded** video stream: every
1755    /// simple (non-`filter_complex`) video encode already runs through an
1756    /// implicit per-output filtergraph whose description defaults to the
1757    /// passthrough `null` chain, and this method replaces that `null` with the
1758    /// given description. The filter text is passed to FFmpeg verbatim — the
1759    /// same string the CLI accepts after `-vf` works here unchanged, e.g.
1760    /// `"scale=1280:-2"` or `"fps=30,scale=640:360"`.
1761    ///
1762    /// Unlike [`FfmpegContextBuilder::filter_desc`], which creates one
1763    /// context-level graph shared by all outputs, this filter belongs to this
1764    /// `Output` alone: with several outputs, each can carry its own chain (or
1765    /// none), matching how the CLI scopes `-vf` to the output file it precedes.
1766    ///
1767    /// # Contract
1768    /// - **Linear chain only**: the description must have exactly one video
1769    ///   input pad and one video output pad. Splitting/merging descriptions
1770    ///   (e.g. `split`) fail the build with
1771    ///   [`OpenOutputError::SimpleFilterInvalidShape`]; non-video chains (e.g.
1772    ///   `anull`) fail with [`OpenOutputError::SimpleFilterMediaTypeMismatch`].
1773    ///   Use [`FfmpegContextBuilder::filter_desc`] for complex graphs.
1774    /// - **Connected, structurally**: the input pad must be wired into the
1775    ///   flow that feeds the output pad — disconnected sub-graphs and
1776    ///   descriptions that drain the stream into a sink while an unrelated
1777    ///   branch feeds the encoder fail with
1778    ///   [`OpenOutputError::SimpleFilterInvalidShape`]. The check does not
1779    ///   second-guess runtime routing: a filter that may discard the stream
1780    ///   while it runs (e.g. `streamselect` whose `map` currently selects an
1781    ///   embedded generator — a selection `sendcmd` can rewrite mid-stream)
1782    ///   is accepted and runs as declared, exactly like the CLI.
1783    /// - **Re-encode only**: combining this with `set_video_codec("copy")` or
1784    ///   a copy stream map covering a video stream fails the build with
1785    ///   [`OpenOutputError::FilterWithStreamCopy`], matching the CLI's
1786    ///   "Filtering and streamcopy cannot be used together".
1787    /// - **Simple xor complex**: if this output's video is fed by a
1788    ///   context-level filtergraph output, the build fails with
1789    ///   [`OpenOutputError::SimpleAndComplexFilter`], matching the CLI's rule
1790    ///   for `-vf` + `-filter_complex` on the same stream.
1791    /// - **Audio is untouched**: only the video stream runs through this
1792    ///   chain. Use [`set_audio_filter`](Self::set_audio_filter) for `-af`.
1793    /// - **Must be consumed**: if the output ends up with no re-encoded
1794    ///   video stream at all (audio-only input, [`disable_video`], maps that
1795    ///   match no video stream), the build fails with
1796    ///   [`OpenOutputError::VideoFilterUnused`] instead of silently dropping
1797    ///   the chain.
1798    /// - **VideoWriter**: a [`VideoWriter`](crate::VideoWriter) opening this
1799    ///   `Output` honors the chain when no builder-level `filter_desc` is
1800    ///   set; configuring both fails with
1801    ///   [`WriterError::ConflictingFilterDescriptions`](crate::core::writer::WriterError::ConflictingFilterDescriptions).
1802    ///
1803    /// [`disable_video`]: Self::disable_video
1804    /// [`OpenOutputError::VideoFilterUnused`]: crate::error::OpenOutputError::VideoFilterUnused
1805    ///
1806    /// An **empty string is kept** and fails the build like `-vf ""` fails
1807    /// the CLI (an empty graph parses to zero pads); use
1808    /// [`clear_video_filter`](Self::clear_video_filter) to remove a
1809    /// previously set chain. The description itself is validated when the
1810    /// context is built; an invalid filter name surfaces as a
1811    /// [`FilterGraphParseError`](crate::error::FilterGraphParseError) from
1812    /// `build()`, not from this setter.
1813    ///
1814    /// **Equivalent FFmpeg command:**
1815    /// ```sh
1816    /// ffmpeg -i input.mp4 -vf scale=1280:-2 -c:a copy resized.mp4
1817    /// ```
1818    ///
1819    /// # Examples
1820    /// ```rust,ignore
1821    /// let output = Output::from("resized.mp4")
1822    ///     .set_video_filter("scale=1280:-2") // -vf scale=1280:-2
1823    ///     .set_audio_codec("copy");          // -c:a copy
1824    /// ```
1825    ///
1826    /// [`FfmpegContextBuilder::filter_desc`]: crate::core::context::ffmpeg_context_builder::FfmpegContextBuilder::filter_desc
1827    /// [`OpenOutputError::SimpleFilterInvalidShape`]: crate::error::OpenOutputError::SimpleFilterInvalidShape
1828    /// [`OpenOutputError::SimpleFilterMediaTypeMismatch`]: crate::error::OpenOutputError::SimpleFilterMediaTypeMismatch
1829    /// [`OpenOutputError::FilterWithStreamCopy`]: crate::error::OpenOutputError::FilterWithStreamCopy
1830    /// [`OpenOutputError::SimpleAndComplexFilter`]: crate::error::OpenOutputError::SimpleAndComplexFilter
1831    pub fn set_video_filter(mut self, filter_chain: impl Into<String>) -> Self {
1832        self.video_filter = Some(filter_chain.into());
1833        self
1834    }
1835
1836    /// Removes a previously set [`set_video_filter`](Self::set_video_filter)
1837    /// chain, restoring the implicit passthrough (`null`) graph.
1838    pub fn clear_video_filter(mut self) -> Self {
1839        self.video_filter = None;
1840        self
1841    }
1842
1843    /// Sets a simple **audio** filter chain for this output, equivalent to
1844    /// FFmpeg `-af` (`-filter:a`).
1845    ///
1846    /// The chain is applied to this output's **re-encoded** audio stream: every
1847    /// simple (non-`filter_complex`) audio encode already runs through an
1848    /// implicit per-output filtergraph whose description defaults to the
1849    /// passthrough `anull` chain, and this method replaces that `anull` with
1850    /// the given description. The filter text is passed to FFmpeg verbatim —
1851    /// the same string the CLI accepts after `-af` works here unchanged, e.g.
1852    /// `"aformat=sample_rates=16000"` or `"loudnorm"`.
1853    ///
1854    /// Unlike [`FfmpegContextBuilder::filter_desc`], which creates one
1855    /// context-level graph shared by all outputs, this filter belongs to this
1856    /// `Output` alone: with several outputs, each can carry its own chain (or
1857    /// none), matching how the CLI scopes `-af` to the output file it precedes.
1858    ///
1859    /// # Contract
1860    /// - **Linear chain only**: the description must have exactly one audio
1861    ///   input pad and one audio output pad. Splitting/merging descriptions
1862    ///   (e.g. `asplit`) fail the build with
1863    ///   [`OpenOutputError::SimpleFilterInvalidShape`]; non-audio chains (e.g.
1864    ///   `scale`) fail with [`OpenOutputError::SimpleFilterMediaTypeMismatch`].
1865    ///   Use [`FfmpegContextBuilder::filter_desc`] for complex graphs.
1866    /// - **Re-encode only**: combining this with `set_audio_codec("copy")` or
1867    ///   a copy stream map covering an audio stream fails the build with
1868    ///   [`OpenOutputError::FilterWithStreamCopy`], matching the CLI's
1869    ///   "Filtering and streamcopy cannot be used together".
1870    /// - **Simple xor complex**: if this output's audio is fed by a
1871    ///   context-level filtergraph output, the build fails with
1872    ///   [`OpenOutputError::SimpleAndComplexFilter`], matching the CLI's rule
1873    ///   for `-af` + `-filter_complex` on the same stream.
1874    /// - **Video is untouched**: only the audio stream runs through this
1875    ///   chain. Use [`set_video_filter`](Self::set_video_filter) for `-vf`.
1876    /// - **Must be consumed**: if the output ends up with no re-encoded
1877    ///   audio stream at all (video-only input, [`disable_audio`], maps that
1878    ///   match no audio stream), the build fails with
1879    ///   [`OpenOutputError::AudioFilterUnused`] instead of silently dropping
1880    ///   the chain.
1881    /// - **VideoWriter**: a [`VideoWriter`](crate::VideoWriter) opening this
1882    ///   `Output` rejects the setter — a writer job has no audio stream.
1883    ///
1884    /// [`disable_audio`]: Self::disable_audio
1885    /// [`OpenOutputError::AudioFilterUnused`]: crate::error::OpenOutputError::AudioFilterUnused
1886    ///
1887    /// An **empty string is kept** and fails the build like `-af ""` fails
1888    /// the CLI (an empty graph parses to zero pads); use
1889    /// [`clear_audio_filter`](Self::clear_audio_filter) to remove a
1890    /// previously set chain. The description itself is validated when the
1891    /// context is built; an invalid filter name surfaces as a
1892    /// [`FilterGraphParseError`](crate::error::FilterGraphParseError) from
1893    /// `build()`, not from this setter.
1894    ///
1895    /// **Equivalent FFmpeg command:**
1896    /// ```sh
1897    /// ffmpeg -i input.m4a -af aformat=sample_rates=16000 -c:a aac out.m4a
1898    /// ```
1899    ///
1900    /// # Examples
1901    /// ```rust,ignore
1902    /// let output = Output::from("out.m4a")
1903    ///     .set_audio_filter("aformat=sample_rates=16000"); // -af aformat=sample_rates=16000
1904    /// ```
1905    ///
1906    /// [`FfmpegContextBuilder::filter_desc`]: crate::core::context::ffmpeg_context_builder::FfmpegContextBuilder::filter_desc
1907    /// [`OpenOutputError::SimpleFilterInvalidShape`]: crate::error::OpenOutputError::SimpleFilterInvalidShape
1908    /// [`OpenOutputError::SimpleFilterMediaTypeMismatch`]: crate::error::OpenOutputError::SimpleFilterMediaTypeMismatch
1909    /// [`OpenOutputError::FilterWithStreamCopy`]: crate::error::OpenOutputError::FilterWithStreamCopy
1910    /// [`OpenOutputError::SimpleAndComplexFilter`]: crate::error::OpenOutputError::SimpleAndComplexFilter
1911    pub fn set_audio_filter(mut self, filter_chain: impl Into<String>) -> Self {
1912        self.audio_filter = Some(filter_chain.into());
1913        self
1914    }
1915
1916    /// Removes a previously set [`set_audio_filter`](Self::set_audio_filter)
1917    /// chain, restoring the implicit passthrough (`anull`) graph.
1918    pub fn clear_audio_filter(mut self) -> Self {
1919        self.audio_filter = None;
1920        self
1921    }
1922
1923    /// Sets sws (libswscale) options for the `scale` filters libavfilter
1924    /// **auto-inserts** to convert this output's frames to a format/size the
1925    /// encoder accepts (pixel format, resolution, color).
1926    ///
1927    /// This maps to FFmpeg's graph-level `AVFilterGraph.scale_sws_opts`. It only
1928    /// affects *auto-inserted* scaling; if you build the filtergraph yourself
1929    /// with an explicit `scale=...`, that filter's own arguments still apply.
1930    /// Has no effect on stream-copy (`-c:v copy`) outputs, which are not filtered.
1931    ///
1932    /// The string uses FFmpeg option syntax, e.g.
1933    /// `"flags=lanczos+accurate_rnd"`. To see the available flags, run
1934    /// `ffmpeg -h filter=scale`.
1935    ///
1936    /// # Graph-level, not per-output
1937    /// FFmpeg applies these options to the whole filtergraph, not a single
1938    /// output. When one filtergraph drives several outputs, they must not set
1939    /// *different* non-empty values — that conflict is rejected when the graph is
1940    /// configured. An explicit [`FilterComplex::set_sws_opts`](crate::core::context::filter_complex::FilterComplex::set_sws_opts)
1941    /// takes precedence over this per-output value.
1942    ///
1943    /// # Examples
1944    /// ```rust,ignore
1945    /// let output = Output::from("output.mp4")
1946    ///     .set_sws_opts("flags=lanczos+accurate_rnd");
1947    /// ```
1948    pub fn set_sws_opts(mut self, opts: impl Into<String>) -> Self {
1949        self.sws_opts = Some(opts.into());
1950        self
1951    }
1952
1953    /// Sets swr (libswresample) options for the `aresample` filters libavfilter
1954    /// **auto-inserts** to convert this output's audio to a sample
1955    /// format / rate / channel layout the encoder accepts.
1956    ///
1957    /// This maps to FFmpeg's graph-level `AVFilterGraph.aresample_swr_opts`. It
1958    /// only affects *auto-inserted* resampling; an explicit `aresample=...` in a
1959    /// hand-written filtergraph keeps its own arguments. Has no effect on
1960    /// stream-copy outputs.
1961    ///
1962    /// The string uses FFmpeg option syntax, e.g.
1963    /// `"resampler=soxr:precision=28"`.
1964    ///
1965    /// # Graph-level, not per-output
1966    /// See [`set_sws_opts`](Self::set_sws_opts): the value is graph-level and the
1967    /// same precedence / conflict rules apply.
1968    ///
1969    /// # Examples
1970    /// ```rust,ignore
1971    /// let output = Output::from("output.mp4")
1972    ///     .set_swr_opts("resampler=soxr:precision=28");
1973    /// ```
1974    pub fn set_swr_opts(mut self, opts: impl Into<String>) -> Self {
1975        self.swr_opts = Some(opts.into());
1976        self
1977    }
1978}
1979
1980/// Parse a codec FourCC the way FFmpeg's `-tag` does.
1981///
1982/// A whole-token C `strtol` integer (base 0: decimal or `0x` hex) is used
1983/// when every character is consumed; otherwise the first four bytes are
1984/// read as a little-endian `AV_RL32` FourCC (`"hvc1"`, `"mp4v"`). An empty
1985/// string is rejected — unlike the CLI, which treats it as unset (`0`).
1986pub(crate) fn parse_codec_tag(tag: &str) -> std::result::Result<u32, String> {
1987    if tag.is_empty() {
1988        return Err("codec tag must not be empty".into());
1989    }
1990    if let Some(n) = codec_tag_strtol_full(tag) {
1991        return Ok(n);
1992    }
1993    Ok(codec_tag_av_rl32(tag.as_bytes()))
1994}
1995
1996fn codec_tag_av_rl32(bytes: &[u8]) -> u32 {
1997    let mut tag = 0u32;
1998    for (i, &b) in bytes.iter().take(4).enumerate() {
1999        tag |= u32::from(b) << (8 * i);
2000    }
2001    tag
2002}
2003
2004/// FFmpeg `strtol(arg, &tail, 0)` that only succeeds when `*tail == 0`
2005/// (the whole token was numeric). Leading ASCII whitespace is skipped.
2006fn codec_tag_strtol_full(s: &str) -> Option<u32> {
2007    let t = s.trim_start_matches(|c: char| c.is_ascii_whitespace());
2008    if t.is_empty() {
2009        return Some(0);
2010    }
2011    let t = t.strip_prefix('+').unwrap_or(t);
2012    if t.starts_with('-') {
2013        return None;
2014    }
2015    if let Some(hex) = t.strip_prefix("0x").or_else(|| t.strip_prefix("0X")) {
2016        if hex.is_empty() {
2017            return None;
2018        }
2019        return u32::from_str_radix(hex, 16).ok();
2020    }
2021    t.parse::<u32>().ok()
2022}
2023
2024impl From<Box<dyn FnMut(&[u8]) -> i32 + Send>> for Output {
2025    fn from(write_callback: Box<dyn FnMut(&[u8]) -> i32 + Send>) -> Self {
2026        Self::with_target(OutputTarget::CustomIo {
2027            write: write_callback,
2028        })
2029    }
2030}
2031
2032impl From<crate::core::packet_sink::PacketSink> for Output {
2033    fn from(sink: crate::core::packet_sink::PacketSink) -> Self {
2034        Self::with_target(OutputTarget::PacketSink(sink))
2035    }
2036}
2037
2038impl From<String> for Output {
2039    fn from(url: String) -> Self {
2040        Self::with_target(OutputTarget::Url(url))
2041    }
2042}
2043
2044impl From<&str> for Output {
2045    fn from(url: &str) -> Self {
2046        Self::from(String::from(url))
2047    }
2048}
2049
2050/// Final expanded stream map (matches FFmpeg's StreamMap structure)
2051/// Created after parsing and expansion in outputs_bind()
2052/// FFmpeg reference: fftools/ffmpeg.h:134-141
2053///
2054/// The user-input stage is the public [`StreamMap`] parameter object
2055/// (`stream_map.rs`); one of those expands into N of these, each carrying
2056/// the map's resolved per-map encoder request.
2057#[derive(Debug, Clone)]
2058pub(crate) struct ExpandedStreamMap {
2059    /// 1 if this mapping is disabled by a negative map (-map -0:v)
2060    pub(crate) disabled: bool,
2061    /// Input file index
2062    pub(crate) file_index: usize,
2063    /// Input stream index within the file
2064    pub(crate) stream_index: usize,
2065    /// Name of an output link, for mapping lavfi outputs (e.g., "[v]", "myout")
2066    pub(crate) linklabel: Option<String>,
2067    /// Stream copy flag (-c copy)
2068    pub(crate) copy: bool,
2069    /// Per-map encoder request (FFmpeg `-c:<spec>`), already normalized:
2070    /// never `"copy"` (that became the `copy` flag at resolve time).
2071    pub(crate) codec: Option<String>,
2072    /// Per-map encoder options (FFmpeg `-b:<spec>` etc.), converted for the
2073    /// encoder layer. Merged key by key over the per-type tables in
2074    /// `enc_task::set_encoder_opts`.
2075    pub(crate) codec_opts: Option<HashMap<std::ffi::CString, std::ffi::CString>>,
2076}
2077
2078/// Forced-keyframe request stored on [`Output`] until open time.
2079#[derive(Debug, Clone, PartialEq, Eq)]
2080pub(crate) enum ForcedKeyframeSpec {
2081    /// Feature off.
2082    None,
2083    /// Unparsed list-form string (`"0,5,10.5"`), validated at open.
2084    Times(String),
2085    /// Periodic IDR request every `interval_us` microseconds from the first
2086    /// valid frame PTS.
2087    Periodic { interval_us: i64 },
2088}
2089
2090/// Parsed forced-keyframe plan handed to the muxer / encoder thread.
2091#[derive(Debug, Clone, Default, PartialEq, Eq)]
2092pub(crate) enum ForcedKeyframePlan {
2093    #[default]
2094    Off,
2095    Times(Vec<i64>),
2096    Periodic { interval_us: i64 },
2097}
2098
2099/// Mutable periodic-force cursor used while encoding.
2100#[derive(Debug, Clone)]
2101pub(crate) struct PeriodicKeyframeState {
2102    pub interval_us: i64,
2103    pub origin_us: Option<i64>,
2104    pub next_index: u64,
2105}
2106
2107impl PeriodicKeyframeState {
2108    pub(crate) fn new(interval_us: i64) -> Self {
2109        Self {
2110            interval_us,
2111            origin_us: None,
2112            next_index: 0,
2113        }
2114    }
2115}
2116
2117/// Apply one frame to the periodic schedule. `pts_us == None` is `AV_NOPTS_VALUE`
2118/// and never forces. Returns whether this frame should request an intra frame.
2119pub(crate) fn apply_periodic_keyframe(
2120    state: &mut PeriodicKeyframeState,
2121    pts_us: Option<i64>,
2122) -> bool {
2123    let Some(pts) = pts_us else {
2124        return false;
2125    };
2126    if state.interval_us <= 0 {
2127        return false;
2128    }
2129    match state.origin_us {
2130        None => {
2131            state.origin_us = Some(pts);
2132            state.next_index = 1;
2133            true
2134        }
2135        Some(origin) => {
2136            let Some(target) = periodic_target(origin, state.next_index, state.interval_us) else {
2137                return false;
2138            };
2139            if pts < target {
2140                return false;
2141            }
2142            state.next_index = first_index_strictly_after(origin, state.interval_us, pts);
2143            true
2144        }
2145    }
2146}
2147
2148fn periodic_target(origin: i64, index: u64, interval: i64) -> Option<i64> {
2149    i64::try_from(i128::from(origin) + i128::from(index) * i128::from(interval)).ok()
2150}
2151
2152fn first_index_strictly_after(origin: i64, interval: i64, pts: i64) -> u64 {
2153    if interval <= 0 {
2154        return u64::MAX;
2155    }
2156    let delta = i128::from(pts) - i128::from(origin);
2157    if delta < 0 {
2158        return 0;
2159    }
2160    let k = delta / i128::from(interval) + 1;
2161    u64::try_from(k).unwrap_or(u64::MAX)
2162}
2163
2164/// Parse an FFmpeg `-force_key_frames` **list-form** spec (e.g. `"0,5,10.5"`) into a
2165/// sorted `Vec<i64>` of microsecond timestamps (`AV_TIME_BASE_Q` units).
2166///
2167/// This is pure, set-time validation — no FFmpeg handle is required. It rejects empty
2168/// specs, empty tokens, non-numeric tokens (so the `expr:` / `source` forms are
2169/// refused), negative times, `NaN`/infinite values, and values that would overflow
2170/// `i64` microseconds. Overflow is rejected explicitly rather than relying on `as i64`
2171/// saturation. Duplicates are kept; the result is sorted ascending.
2172pub(crate) fn parse_forced_key_frames(spec: &str) -> Result<Vec<i64>, String> {
2173    if spec.trim().is_empty() {
2174        return Err("force_key_frames: empty spec".to_string());
2175    }
2176
2177    let mut pts = Vec::new();
2178    for token in spec.split(',') {
2179        let token = token.trim();
2180        if token.is_empty() {
2181            return Err("force_key_frames: empty time entry".to_string());
2182        }
2183
2184        let secs = token
2185            .parse::<f64>()
2186            .map_err(|_| format!("force_key_frames: invalid time '{token}'"))?;
2187        if !secs.is_finite() || secs < 0.0 {
2188            return Err(format!("force_key_frames: invalid time '{token}'"));
2189        }
2190
2191        // Reject out-of-range values instead of relying on `as i64` saturation.
2192        // `i64::MAX as f64` rounds up to 2^63, so `>=` also rejects the boundary.
2193        let us = (secs * 1_000_000.0).round();
2194        if !us.is_finite() || us < 0.0 || us >= i64::MAX as f64 {
2195            return Err(format!("force_key_frames: time out of range '{token}'"));
2196        }
2197
2198        pts.push(us as i64);
2199    }
2200
2201    pts.sort_unstable();
2202    Ok(pts)
2203}
2204
2205#[cfg(test)]
2206mod tests {
2207    use super::{
2208        apply_periodic_keyframe, parse_codec_tag, parse_forced_key_frames, ForcedKeyframeSpec,
2209        Output, PeriodicKeyframeState,
2210    };
2211
2212    #[test]
2213    fn io_buffer_size_is_unset_until_the_setter_runs() {
2214        // `None` = "never set"; the effective 64 KiB default is applied at
2215        // build time. Packet-sink validation needs the distinction.
2216        assert_eq!(Output::from("out.mp4").io_buffer_size, None);
2217    }
2218
2219    #[test]
2220    fn set_io_buffer_size_valid() {
2221        assert_eq!(
2222            Output::from("out.mp4")
2223                .set_io_buffer_size(1 << 20)
2224                .io_buffer_size,
2225            Some(1 << 20)
2226        );
2227    }
2228
2229    #[test]
2230    fn set_io_buffer_size_stores_invalid_values_for_deferred_validation() {
2231        let output = Output::new_by_write_callback(|_| 0).set_io_buffer_size(0);
2232        assert_eq!(output.io_buffer_size, Some(0));
2233    }
2234
2235    #[test]
2236    fn muxing_queue_knobs_default_to_ffmpeg_parity() {
2237        use crate::core::context::pre_mux_queue::{
2238            DEFAULT_PRE_MUX_DATA_THRESHOLD, DEFAULT_PRE_MUX_MAX_PACKETS,
2239        };
2240        let output = Output::from("out.mp4");
2241        assert_eq!(output.max_muxing_queue_size, DEFAULT_PRE_MUX_MAX_PACKETS);
2242        assert_eq!(
2243            output.muxing_queue_data_threshold,
2244            DEFAULT_PRE_MUX_DATA_THRESHOLD
2245        );
2246    }
2247
2248    #[test]
2249    fn set_muxing_queue_knobs_valid() {
2250        let output = Output::from("out.mp4")
2251            .set_max_muxing_queue_size(1024)
2252            .set_muxing_queue_data_threshold(256 * 1024 * 1024);
2253        assert_eq!(output.max_muxing_queue_size, 1024);
2254        assert_eq!(output.muxing_queue_data_threshold, 256 * 1024 * 1024);
2255    }
2256
2257    #[test]
2258    fn muxing_queue_setters_store_invalid_values_for_deferred_validation() {
2259        let output = Output::from("out.mp4")
2260            .set_max_muxing_queue_size(0)
2261            .set_muxing_queue_data_threshold(0);
2262        assert_eq!(output.max_muxing_queue_size, 0);
2263        assert_eq!(output.muxing_queue_data_threshold, 0);
2264    }
2265
2266    #[test]
2267    fn set_video_filter_stores_chain() {
2268        let output = Output::from("out.mp4").set_video_filter("scale=1280:-2");
2269        assert_eq!(output.video_filter.as_deref(), Some("scale=1280:-2"));
2270    }
2271
2272    #[test]
2273    fn set_video_filter_keeps_empty_string() {
2274        // -vf "" parity: the empty description is preserved and fails the
2275        // build like the CLI's own empty-graph parse failure.
2276        let output = Output::from("out.mp4")
2277            .set_video_filter("scale=1280:-2")
2278            .set_video_filter("");
2279        assert_eq!(output.video_filter.as_deref(), Some(""));
2280    }
2281
2282    #[test]
2283    fn clear_video_filter_resets() {
2284        let output = Output::from("out.mp4")
2285            .set_video_filter("scale=1280:-2")
2286            .clear_video_filter();
2287        assert_eq!(output.video_filter, None);
2288    }
2289
2290    #[test]
2291    fn video_filter_defaults_to_none() {
2292        assert_eq!(Output::from("out.mp4").video_filter, None);
2293        assert_eq!(Output::new_by_write_callback(|_| 0).video_filter, None);
2294    }
2295
2296    #[test]
2297    fn set_audio_filter_stores_chain() {
2298        let output = Output::from("out.m4a").set_audio_filter("aformat=sample_rates=16000");
2299        assert_eq!(
2300            output.audio_filter.as_deref(),
2301            Some("aformat=sample_rates=16000")
2302        );
2303    }
2304
2305    #[test]
2306    fn clear_audio_filter_resets() {
2307        let output = Output::from("out.m4a")
2308            .set_audio_filter("loudnorm")
2309            .clear_audio_filter();
2310        assert_eq!(output.audio_filter, None);
2311    }
2312
2313    #[test]
2314    fn parse_codec_tag_fourcc_is_little_endian() {
2315        fn mktag(tag: &str) -> u32 {
2316            let b = tag.as_bytes();
2317            u32::from(b[0])
2318                | (u32::from(b[1]) << 8)
2319                | (u32::from(b[2]) << 16)
2320                | (u32::from(b[3]) << 24)
2321        }
2322        assert_eq!(parse_codec_tag("hvc1").unwrap(), mktag("hvc1"));
2323        assert_eq!(parse_codec_tag("mp4v").unwrap(), mktag("mp4v"));
2324        assert_eq!(parse_codec_tag("FMP4").unwrap(), mktag("FMP4"));
2325        assert_eq!(parse_codec_tag("16").unwrap(), 16);
2326        assert_eq!(parse_codec_tag("0x10").unwrap(), 16);
2327        assert_eq!(parse_codec_tag("0").unwrap(), 0);
2328        assert!(parse_codec_tag("").is_err());
2329    }
2330
2331    /// Constructor parity for the CLI-only flags: every public construction
2332    /// path funnels through `with_target`, and the compiler only enforces
2333    /// field PRESENCE there, not VALUES. A future field whose `with_target`
2334    /// default were `true` would silently arm strict/uniqueness semantics on
2335    /// every non-CLI pipeline; this pin turns that mistake into a red test.
2336    #[test]
2337    fn cli_only_flags_default_to_off_on_every_construction_path() {
2338        let outputs = [
2339            Output::from("out.mp4"),
2340            Output::new_by_write_callback(|_| 0),
2341            Output::new_by_packet_sink(crate::core::packet_sink::PacketSink::discard()),
2342        ];
2343        for output in outputs {
2344            assert!(!output.strict_avoptions);
2345            assert!(!output.require_unique_video_source);
2346            assert_eq!(output.video_filter, None);
2347            assert_eq!(output.audio_filter, None);
2348            assert_eq!(output.video_codec_tag, None);
2349            assert_eq!(output.audio_codec_tag, None);
2350            assert_eq!(output.subtitle_codec_tag, None);
2351        }
2352    }
2353
2354    #[test]
2355    fn parses_sorted_microseconds() {
2356        assert_eq!(
2357            parse_forced_key_frames("0,5,10.5").unwrap(),
2358            vec![0, 5_000_000, 10_500_000]
2359        );
2360    }
2361
2362    #[test]
2363    fn sorts_unsorted_input() {
2364        assert_eq!(
2365            parse_forced_key_frames("5,0,10").unwrap(),
2366            vec![0, 5_000_000, 10_000_000]
2367        );
2368    }
2369
2370    #[test]
2371    fn rounds_fractional_seconds() {
2372        assert_eq!(parse_forced_key_frames("10.5").unwrap(), vec![10_500_000]);
2373    }
2374
2375    #[test]
2376    fn keeps_duplicates() {
2377        assert_eq!(
2378            parse_forced_key_frames("5,5").unwrap(),
2379            vec![5_000_000, 5_000_000]
2380        );
2381    }
2382
2383    #[test]
2384    fn tolerates_surrounding_whitespace() {
2385        assert_eq!(
2386            parse_forced_key_frames(" 1 , 2 ").unwrap(),
2387            vec![1_000_000, 2_000_000]
2388        );
2389    }
2390
2391    #[test]
2392    fn accepts_zero() {
2393        assert_eq!(parse_forced_key_frames("0").unwrap(), vec![0]);
2394    }
2395
2396    #[test]
2397    fn rejects_garbage_without_panicking() {
2398        for bad in [
2399            "",
2400            "   ",
2401            "5,,10",
2402            "abc",
2403            "expr:gte(t,5)",
2404            "-1",
2405            "5,NaN",
2406            "inf",
2407            "5,-0.5",
2408        ] {
2409            assert!(
2410                parse_forced_key_frames(bad).is_err(),
2411                "expected Err for {bad:?}"
2412            );
2413        }
2414    }
2415
2416    #[test]
2417    fn rejects_overflow_instead_of_saturating() {
2418        assert!(parse_forced_key_frames("1e30").is_err());
2419    }
2420
2421    fn force_seq(origin_pts: &[Option<i64>], interval_us: i64) -> Vec<bool> {
2422        let mut state = PeriodicKeyframeState::new(interval_us);
2423        origin_pts
2424            .iter()
2425            .copied()
2426            .map(|pts| apply_periodic_keyframe(&mut state, pts))
2427            .collect()
2428    }
2429
2430    fn cfr_pts(frames: u64, fps_num: i64, fps_den: i64) -> Vec<Option<i64>> {
2431        (0..frames)
2432            .map(|n| {
2433                Some(
2434                    i64::try_from(
2435                        i128::from(n) * i128::from(fps_den) * 1_000_000 / i128::from(fps_num),
2436                    )
2437                    .unwrap(),
2438                )
2439            })
2440            .collect()
2441    }
2442
2443    #[test]
2444    fn periodic_origin_zero() {
2445        let pts = cfr_pts(90, 30, 1);
2446        let flags = force_seq(&pts, 1_000_000);
2447        // 30 fps, 1s interval: frames 0, 30, 60.
2448        let forced: Vec<usize> = flags
2449            .iter()
2450            .enumerate()
2451            .filter_map(|(i, f)| f.then_some(i))
2452            .collect();
2453        assert_eq!(forced, vec![0, 30, 60]);
2454    }
2455
2456    #[test]
2457    fn periodic_origin_nonzero() {
2458        // First valid PTS is 2_000_000; subsequent targets 3s, 4s, ...
2459        let mut pts = cfr_pts(90, 30, 1);
2460        for v in pts.iter_mut().flatten() {
2461            *v += 2_000_000;
2462        }
2463        let flags = force_seq(&pts, 1_000_000);
2464        let forced: Vec<usize> = flags
2465            .iter()
2466            .enumerate()
2467            .filter_map(|(i, f)| f.then_some(i))
2468            .collect();
2469        assert_eq!(forced, vec![0, 30, 60]);
2470        assert_eq!(pts[0], Some(2_000_000));
2471    }
2472
2473    #[test]
2474    fn periodic_25_fps() {
2475        let flags = force_seq(&cfr_pts(50, 25, 1), 1_000_000);
2476        let forced: Vec<usize> = flags
2477            .iter()
2478            .enumerate()
2479            .filter_map(|(i, f)| f.then_some(i))
2480            .collect();
2481        assert_eq!(forced, vec![0, 25]);
2482    }
2483
2484    #[test]
2485    fn periodic_ntsc_30000_1001() {
2486        let flags = force_seq(&cfr_pts(60, 30000, 1001), 1_000_000);
2487        assert!(flags[0]);
2488        // Targets stay on origin + k*1s, not N/fps, so the second force is
2489        // the first frame whose PTS >= 1_000_000.
2490        let second = flags.iter().skip(1).position(|f| *f).unwrap() + 1;
2491        let pts = i64::try_from(i128::from(second as u64) * 1001 * 1_000_000 / 30000).unwrap();
2492        assert!(pts >= 1_000_000);
2493        let prev =
2494            i64::try_from(i128::from((second as u64) - 1) * 1001 * 1_000_000 / 30000).unwrap();
2495        assert!(prev < 1_000_000);
2496    }
2497
2498    #[test]
2499    fn periodic_non_integer_interval() {
2500        // 30 fps, 1.5s interval: force at 0, then first frame >= 1.5s (frame 45).
2501        let flags = force_seq(&cfr_pts(90, 30, 1), 1_500_000);
2502        let forced: Vec<usize> = flags
2503            .iter()
2504            .enumerate()
2505            .filter_map(|(i, f)| f.then_some(i))
2506            .collect();
2507        assert_eq!(forced, vec![0, 45]);
2508    }
2509
2510    #[test]
2511    fn periodic_frame_drop_skips_missed_target() {
2512        // origin 0, 2s interval. Frames at 0, 1s, then jump to 4.5s.
2513        let pts = vec![Some(0), Some(1_000_000), Some(4_500_000), Some(6_000_000)];
2514        let flags = force_seq(&pts, 2_000_000);
2515        assert_eq!(flags, vec![true, false, true, true]);
2516        // The 2s target was skipped by the jump to 4.5s; next after 4.5s is 6s.
2517    }
2518
2519    #[test]
2520    fn periodic_nopts_never_forces_or_sets_origin() {
2521        let mut state = PeriodicKeyframeState::new(1_000_000);
2522        assert!(!apply_periodic_keyframe(&mut state, None));
2523        assert_eq!(state.origin_us, None);
2524        assert!(apply_periodic_keyframe(&mut state, Some(5_000_000)));
2525        assert_eq!(state.origin_us, Some(5_000_000));
2526        assert!(!apply_periodic_keyframe(&mut state, None));
2527        assert_eq!(state.origin_us, Some(5_000_000));
2528        assert_eq!(state.next_index, 1);
2529    }
2530
2531    #[test]
2532    fn periodic_overflow_stops_forcing() {
2533        let mut state = PeriodicKeyframeState::new(100);
2534        assert!(apply_periodic_keyframe(&mut state, Some(i64::MAX - 10)));
2535        // next target = origin + 100 overflows i64.
2536        assert!(!apply_periodic_keyframe(&mut state, Some(i64::MAX)));
2537    }
2538
2539    #[test]
2540    fn list_mode_setter_still_stores_raw_spec() {
2541        let output = Output::from("out.mp4").set_force_key_frames("0,5,10.5");
2542        assert_eq!(
2543            output.forced_kf_spec,
2544            ForcedKeyframeSpec::Times("0,5,10.5".into())
2545        );
2546    }
2547
2548    #[test]
2549    fn periodic_setter_replaces_list_mode() {
2550        let output = Output::from("out.mp4")
2551            .set_force_key_frames("0,5")
2552            .set_force_key_frames_interval(std::time::Duration::from_secs(2));
2553        assert_eq!(
2554            output.forced_kf_spec,
2555            ForcedKeyframeSpec::Periodic {
2556                interval_us: 2_000_000
2557            }
2558        );
2559    }
2560}