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