1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
//! Metadata, container, and miscellaneous settings for [`VideoEncoderBuilder`].
use super::VideoEncoderBuilder;
use crate::OutputContainer;
impl VideoEncoderBuilder {
/// Set container format explicitly (usually auto-detected from file extension).
#[must_use]
pub fn container(mut self, container: OutputContainer) -> Self {
self.container = Some(container);
self
}
/// Set a closure as the progress callback.
#[must_use]
pub fn on_progress<F>(mut self, callback: F) -> Self
where
F: FnMut(&crate::EncodeProgress) + Send + 'static,
{
self.progress_callback = Some(Box::new(callback));
self
}
/// Set a [`crate::EncodeProgressCallback`] trait object (supports cancellation).
#[must_use]
pub fn progress_callback<C: crate::EncodeProgressCallback + 'static>(
mut self,
callback: C,
) -> Self {
self.progress_callback = Some(Box::new(callback));
self
}
/// Enable two-pass encoding for more accurate bitrate distribution.
///
/// Two-pass encoding is video-only and is incompatible with audio streams.
#[must_use]
pub fn two_pass(mut self) -> Self {
self.two_pass = true;
self
}
/// Embed a metadata tag in the output container.
///
/// Calls `av_dict_set` on `AVFormatContext->metadata` before the header
/// is written. Multiple calls accumulate entries; duplicate keys use the
/// last value.
#[must_use]
pub fn metadata(mut self, key: &str, value: &str) -> Self {
self.metadata.push((key.to_string(), value.to_string()));
self
}
/// Add a chapter to the output container.
///
/// Allocates an `AVChapter` entry on `AVFormatContext` before the header
/// is written. Multiple calls accumulate chapters in the order added.
#[must_use]
pub fn chapter(mut self, chapter: ff_format::chapter::ChapterInfo) -> Self {
self.chapters.push(chapter);
self
}
/// Copy a subtitle stream from an existing file into the output container.
///
/// Opens `source_path`, locates the stream at `stream_index`, and registers it
/// as a passthrough stream in the output. Packets are copied verbatim using
/// `av_interleaved_write_frame` without re-encoding.
///
/// `stream_index` is the zero-based index of the subtitle stream inside
/// `source_path`. For files with a single subtitle track this is typically `0`
/// (or whichever index `ffprobe` reports).
///
/// If the source cannot be opened or the stream index is invalid, a warning is
/// logged and encoding continues without subtitles.
#[must_use]
pub fn subtitle_passthrough(mut self, source_path: &str, stream_index: usize) -> Self {
self.subtitle_passthrough = Some((source_path.to_string(), stream_index));
self
}
/// Set per-codec encoding options.
///
/// Applied via `av_opt_set` before `avcodec_open2` during [`build()`](Self::build).
/// This is additive — omitting it leaves codec defaults unchanged.
/// Any option that the chosen encoder does not support is logged as a
/// warning and skipped; it never causes `build()` to return an error.
///
/// The [`crate::VideoCodecOptions`] variant should match the codec selected via
/// [`video_codec()`](Self::video_codec). A mismatch is silently ignored.
#[must_use]
pub fn codec_options(mut self, opts: crate::VideoCodecOptions) -> Self {
self.codec_options = Some(opts);
self
}
/// Embed a binary attachment in the output container.
///
/// Attachments are supported in MKV/WebM containers and are used for
/// fonts (required by ASS/SSA subtitle rendering), cover art, or other
/// binary files that consumers of the file may need.
///
/// - `data` — raw bytes of the attachment
/// - `mime_type` — MIME type string (e.g. `"application/x-truetype-font"`,
/// `"image/jpeg"`)
/// - `filename` — the name reported inside the container (e.g. `"Arial.ttf"`)
///
/// Multiple calls accumulate entries; each attachment becomes its own stream
/// with `AVMEDIA_TYPE_ATTACHMENT` codec parameters.
#[must_use]
pub fn add_attachment(mut self, data: Vec<u8>, mime_type: &str, filename: &str) -> Self {
self.attachments
.push((data, mime_type.to_string(), filename.to_string()));
self
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;
#[test]
fn builder_container_should_be_stored() {
let builder = VideoEncoderBuilder::new(PathBuf::from("output.mp4"))
.video(1920, 1080, 30.0)
.container(OutputContainer::Mp4);
assert_eq!(builder.container, Some(OutputContainer::Mp4));
}
#[test]
fn two_pass_flag_should_be_stored_in_builder() {
let builder = VideoEncoderBuilder::new(PathBuf::from("output.mp4"))
.video(640, 480, 30.0)
.two_pass();
assert!(builder.two_pass);
}
#[test]
fn add_attachment_should_accumulate_entries() {
let builder = VideoEncoderBuilder::new(PathBuf::from("output.mkv"))
.video(320, 240, 30.0)
.add_attachment(vec![1, 2, 3], "application/x-truetype-font", "font.ttf")
.add_attachment(vec![4, 5, 6], "image/jpeg", "cover.jpg");
assert_eq!(builder.attachments.len(), 2);
assert_eq!(builder.attachments[0].0, vec![1u8, 2, 3]);
assert_eq!(builder.attachments[0].1, "application/x-truetype-font");
assert_eq!(builder.attachments[0].2, "font.ttf");
assert_eq!(builder.attachments[1].1, "image/jpeg");
assert_eq!(builder.attachments[1].2, "cover.jpg");
}
#[test]
fn add_attachment_with_no_attachments_should_start_empty() {
let builder = VideoEncoderBuilder::new(PathBuf::from("output.mkv")).video(320, 240, 30.0);
assert!(builder.attachments.is_empty());
}
}