Skip to main content

ff_encode/video/builder/
meta.rs

1//! Metadata, container, and miscellaneous settings for [`VideoEncoderBuilder`].
2
3use super::VideoEncoderBuilder;
4use crate::OutputContainer;
5
6impl VideoEncoderBuilder {
7    /// Set container format explicitly (usually auto-detected from file extension).
8    #[must_use]
9    pub fn container(mut self, container: OutputContainer) -> Self {
10        self.container = Some(container);
11        self
12    }
13
14    /// Mux into `sink` instead of writing a file.
15    ///
16    /// The path given to [`VideoEncoder::create`](crate::VideoEncoder::create) is
17    /// then only what the muxer is guessed from -- nothing is created on disk --
18    /// so it still needs a usable extension, or an explicit
19    /// [`container`](Self::container).
20    ///
21    /// `sink` is anything that writes and seeks and can move to the encoder's
22    /// thread. Seeking is not optional: MP4 rewrites its header once the sizes
23    /// are known, and a sink that cannot seek would produce an unplayable file.
24    ///
25    /// Incompatible with [`two_pass`](Self::two_pass), which opens the output
26    /// again for the second pass; that combination is rejected by `build`.
27    ///
28    /// # Examples
29    ///
30    /// ```ignore
31    /// use std::io::Cursor;
32    /// use ff_encode::VideoEncoder;
33    ///
34    /// let mut encoder = VideoEncoder::create("out.mp4")
35    ///     .video_size(640, 360)
36    ///     .output_sink(Cursor::new(Vec::new()))
37    ///     .build()?;
38    /// # Ok::<(), ff_encode::EncodeError>(())
39    /// ```
40    #[must_use]
41    pub fn output_sink(mut self, sink: impl ff_sys::IoSink + 'static) -> Self {
42        self.sink = Some(Box::new(sink));
43        self
44    }
45
46    /// Set a closure as the progress callback.
47    #[must_use]
48    pub fn on_progress<F>(mut self, callback: F) -> Self
49    where
50        F: FnMut(&crate::EncodeProgress) + Send + 'static,
51    {
52        self.progress_callback = Some(Box::new(callback));
53        self
54    }
55
56    /// Set a [`crate::EncodeProgressCallback`] trait object (supports cancellation).
57    #[must_use]
58    pub fn progress_callback<C: crate::EncodeProgressCallback + 'static>(
59        mut self,
60        callback: C,
61    ) -> Self {
62        self.progress_callback = Some(Box::new(callback));
63        self
64    }
65
66    /// Enable two-pass encoding for more accurate bitrate distribution.
67    ///
68    /// Two-pass encoding is video-only and is incompatible with audio streams.
69    #[must_use]
70    pub fn two_pass(mut self) -> Self {
71        self.two_pass = true;
72        self
73    }
74
75    /// Relocate the `moov` atom to the front of MP4/MOV output (`movflags=+faststart`).
76    ///
77    /// This makes the file playable via progressive download / streaming before it
78    /// is fully fetched, the standard requirement for web-delivered files. It has no
79    /// effect on non-MP4/MOV containers or on fragmented MP4 (which already streams
80    /// via its own movflags). Because `FFmpeg` relocates the atom by rewriting the
81    /// file at finalize, faststart adds a second pass over the output, so it is not
82    /// free for very large files.
83    #[must_use]
84    pub fn faststart(mut self) -> Self {
85        self.faststart = true;
86        self
87    }
88
89    /// Embed a metadata tag in the output container.
90    ///
91    /// Calls `av_dict_set` on `AVFormatContext->metadata` before the header
92    /// is written. Multiple calls accumulate entries; duplicate keys use the
93    /// last value.
94    #[must_use]
95    pub fn metadata(mut self, key: &str, value: &str) -> Self {
96        self.metadata.push((key.to_string(), value.to_string()));
97        self
98    }
99
100    /// Add a chapter to the output container.
101    ///
102    /// Allocates an `AVChapter` entry on `AVFormatContext` before the header
103    /// is written. Multiple calls accumulate chapters in the order added.
104    #[must_use]
105    pub fn chapter(mut self, chapter: ff_format::chapter::ChapterInfo) -> Self {
106        self.chapters.push(chapter);
107        self
108    }
109
110    /// Copy a subtitle stream from an existing file into the output container.
111    ///
112    /// Opens `source_path`, locates the stream at `stream_index`, and registers it
113    /// as a passthrough stream in the output.  Packets are copied verbatim using
114    /// `av_interleaved_write_frame` without re-encoding.
115    ///
116    /// `stream_index` is the zero-based index of the subtitle stream inside
117    /// `source_path`.  For files with a single subtitle track this is typically `0`
118    /// (or whichever index `ffprobe` reports).
119    ///
120    /// If the source cannot be opened or the stream index is invalid, a warning is
121    /// logged and encoding continues without subtitles.
122    #[must_use]
123    pub fn subtitle_passthrough(mut self, source_path: &str, stream_index: usize) -> Self {
124        self.subtitle_passthrough = Some((source_path.to_string(), stream_index));
125        self
126    }
127
128    /// Set a codec-*private* option by name, for the long tail that has no typed
129    /// builder (`x264-params`, `aq-mode`, `psy-rd`, ...).
130    ///
131    /// Applies to the **video** codec only. A video output's audio track opens
132    /// its own codec context, which this does not reach; use
133    /// [`AudioEncoder`](crate::AudioEncoder) for audio-only output that needs
134    /// the same escape hatch.
135    ///
136    /// Repeatable, and applied in call order via `av_opt_set` on the codec's
137    /// `priv_data` before `avcodec_open2`, **after**
138    /// [`codec_options()`](Self::codec_options) — so a key named here overrides
139    /// the same key set through the typed API.
140    ///
141    /// # Escape-hatch semantics
142    ///
143    /// Prefer [`codec_options()`](Self::codec_options): it is validated at
144    /// compile time and portable across encoders. Nothing here is checked until
145    /// `FFmpeg` sees it, and keys are codec-specific.
146    ///
147    /// Unlike the typed options, which log and continue when an encoder does not
148    /// support them, an option rejected here fails
149    /// [`build()`](Self::build) with [`crate::EncodeError::InvalidConfig`] — the
150    /// key was named explicitly, so dropping it silently would defeat the
151    /// purpose. The consequence is worth planning for: a configuration carrying
152    /// libx264 keys will fail once the caller switches to a hardware encoder.
153    ///
154    /// ```no_run
155    /// # use ff_encode::VideoEncoder;
156    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
157    /// let encoder = VideoEncoder::create("out.mp4")
158    ///     .video(1920, 1080, 30.0)
159    ///     .codec_opt("x264-params", "keyint=48:min-keyint=48")
160    ///     .build()?;
161    /// # Ok(())
162    /// # }
163    /// ```
164    #[must_use]
165    pub fn codec_opt(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
166        self.codec_opts.push((key.into(), value.into()));
167        self
168    }
169
170    /// Set per-codec encoding options.
171    ///
172    /// Applied via `av_opt_set` before `avcodec_open2` during [`build()`](Self::build).
173    /// This is additive — omitting it leaves codec defaults unchanged.
174    /// Any option that the chosen encoder does not support is logged as a
175    /// warning and skipped; it never causes `build()` to return an error.
176    ///
177    /// The [`crate::VideoCodecOptions`] variant should match the codec selected via
178    /// [`video_codec()`](Self::video_codec).  A mismatch is silently ignored.
179    #[must_use]
180    pub fn codec_options(mut self, opts: crate::VideoCodecOptions) -> Self {
181        self.codec_options = Some(opts);
182        self
183    }
184
185    /// Embed a binary attachment in the output container.
186    ///
187    /// Attachments are supported in MKV/WebM containers and are used for
188    /// fonts (required by ASS/SSA subtitle rendering), cover art, or other
189    /// binary files that consumers of the file may need.
190    ///
191    /// - `data` — raw bytes of the attachment
192    /// - `mime_type` — MIME type string (e.g. `"application/x-truetype-font"`,
193    ///   `"image/jpeg"`)
194    /// - `filename` — the name reported inside the container (e.g. `"Arial.ttf"`)
195    ///
196    /// Multiple calls accumulate entries; each attachment becomes its own stream
197    /// with `AVMEDIA_TYPE_ATTACHMENT` codec parameters.
198    #[must_use]
199    pub fn add_attachment(mut self, data: Vec<u8>, mime_type: &str, filename: &str) -> Self {
200        self.attachments
201            .push((data, mime_type.to_string(), filename.to_string()));
202        self
203    }
204}
205
206#[cfg(test)]
207mod tests {
208    use super::*;
209    use std::path::PathBuf;
210
211    #[test]
212    fn builder_container_should_be_stored() {
213        let builder = VideoEncoderBuilder::new(PathBuf::from("output.mp4"))
214            .video(1920, 1080, 30.0)
215            .container(OutputContainer::Mp4);
216        assert_eq!(builder.container, Some(OutputContainer::Mp4));
217    }
218
219    #[test]
220    fn two_pass_flag_should_be_stored_in_builder() {
221        let builder = VideoEncoderBuilder::new(PathBuf::from("output.mp4"))
222            .video(640, 480, 30.0)
223            .two_pass();
224        assert!(builder.two_pass);
225    }
226
227    #[test]
228    fn faststart_flag_should_be_stored_in_builder() {
229        let builder = VideoEncoderBuilder::new(PathBuf::from("output.mp4"))
230            .video(640, 480, 30.0)
231            .faststart();
232        assert!(builder.faststart);
233    }
234
235    #[test]
236    fn add_attachment_should_accumulate_entries() {
237        let builder = VideoEncoderBuilder::new(PathBuf::from("output.mkv"))
238            .video(320, 240, 30.0)
239            .add_attachment(vec![1, 2, 3], "application/x-truetype-font", "font.ttf")
240            .add_attachment(vec![4, 5, 6], "image/jpeg", "cover.jpg");
241        assert_eq!(builder.attachments.len(), 2);
242        assert_eq!(builder.attachments[0].0, vec![1u8, 2, 3]);
243        assert_eq!(builder.attachments[0].1, "application/x-truetype-font");
244        assert_eq!(builder.attachments[0].2, "font.ttf");
245        assert_eq!(builder.attachments[1].1, "image/jpeg");
246        assert_eq!(builder.attachments[1].2, "cover.jpg");
247    }
248
249    #[test]
250    fn add_attachment_with_no_attachments_should_start_empty() {
251        let builder = VideoEncoderBuilder::new(PathBuf::from("output.mkv")).video(320, 240, 30.0);
252        assert!(builder.attachments.is_empty());
253    }
254}