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}