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}