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