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
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
use super::Output;
use std::collections::HashMap;
impl Output {
// ========== Metadata API Methods ==========
// The following helpers mirror FFmpeg's command-line metadata options as implemented in
// fftools/ffmpeg_opt.c (opt_metadata / opt_map_metadata) and the automatic propagation rules
// in fftools/ffmpeg_mux_init.c:2913-2983. Each method references the corresponding FFmpeg
// behavior so callers can cross-check the C implementation when needed.
/// Add or update global metadata for the output file.
///
/// If value is empty string, the key will be removed (FFmpeg behavior).
/// Replicates FFmpeg's `-metadata key=value` option.
///
/// FFmpeg reference: fftools/ffmpeg_opt.c (`opt_metadata()` handles `-metadata key=value`).
///
/// # Examples
/// ```rust,ignore
/// let output = Output::from("output.mp4")
/// .add_metadata("title", "My Video")
/// .add_metadata("author", "John Doe");
/// ```
pub fn add_metadata(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
let key = key.into();
let value = value.into();
if value.is_empty() {
// Empty value means remove the key (FFmpeg behavior)
if let Some(ref mut metadata) = self.global_metadata {
metadata.remove(&key);
}
} else {
self.global_metadata
.get_or_insert_with(HashMap::new)
.insert(key, value);
}
self
}
/// Add multiple global metadata entries at once.
///
/// FFmpeg reference: fftools/ffmpeg_opt.c (consecutive `-metadata` invocations append to the
/// same dictionary; this helper simply batches the calls on the Rust side).
///
/// # Examples
/// ```rust,ignore
/// let mut metadata = HashMap::new();
/// metadata.insert("title".to_string(), "My Video".to_string());
/// metadata.insert("author".to_string(), "John Doe".to_string());
///
/// let output = Output::from("output.mp4")
/// .add_metadata_map(metadata);
/// ```
pub fn add_metadata_map(mut self, metadata: HashMap<String, String>) -> Self {
for (key, value) in metadata {
self = self.add_metadata(key, value);
}
self
}
/// Remove a global metadata key.
///
/// FFmpeg reference: fftools/ffmpeg_opt.c (`-metadata key=` deletes the key when value is
/// empty; we follow the same rule by interpreting an empty string as removal).
///
/// # Examples
/// ```rust,ignore
/// let output = Output::from("output.mp4")
/// .add_metadata("title", "My Video")
/// .remove_metadata("title"); // Remove the title
/// ```
pub fn remove_metadata(mut self, key: &str) -> Self {
if let Some(ref mut metadata) = self.global_metadata {
metadata.remove(key);
}
self
}
/// Clear all metadata (global, stream, chapter, program) and mappings.
///
/// Useful when you want to start fresh without any metadata.
///
/// FFmpeg reference: fftools/ffmpeg_opt.c (users typically issue `-map_metadata -1` and then
/// reapply `-metadata` options; this helper emulates that workflow programmatically).
///
/// # Examples
/// ```rust,ignore
/// let output = Output::from("output.mp4")
/// .add_metadata("title", "My Video")
/// .clear_all_metadata(); // Remove all metadata
/// ```
pub fn clear_all_metadata(mut self) -> Self {
self.global_metadata = None;
self.stream_metadata.clear();
self.chapter_metadata.clear();
self.program_metadata.clear();
self.metadata_map.clear();
self
}
/// Disable automatic metadata copying from input files.
///
/// By default, FFmpeg automatically copies global and stream metadata
/// from input files to output. This method disables that behavior,
/// similar to FFmpeg's `-map_metadata -1` option.
/// FFmpeg reference: ffmpeg_mux_init.c (`copy_meta()` sets metadata_global_manual when
/// `-map_metadata -1` is used; `auto_copy_metadata` mirrors the same flag).
///
/// # Examples
/// ```rust,ignore
/// let output = Output::from("output.mp4")
/// .disable_auto_copy_metadata() // Don't copy any metadata from input
/// .add_metadata("title", "New Title"); // Only use explicitly set metadata
/// ```
pub fn disable_auto_copy_metadata(mut self) -> Self {
self.auto_copy_metadata = false;
self
}
/// Add or update stream-specific metadata.
///
/// Uses FFmpeg's stream specifier syntax to identify target streams.
/// If value is empty string, the key will be removed (FFmpeg behavior).
/// Replicates FFmpeg's `-metadata:s:spec key=value` option.
///
/// FFmpeg reference: fftools/ffmpeg_opt.c (`opt_metadata()` with stream specifiers, lines
/// 2465-2520 in FFmpeg 7.x).
///
/// # Stream Specifier Syntax
/// - `"v:0"` - First video stream
/// - `"a:1"` - Second audio stream
/// - `"s"` - All subtitle streams
/// - `"v"` - All video streams
/// - `"p:0:v"` - Video streams in program 0
/// - `"#0x100"` or `"i:256"` - Stream with specific ID
/// - `"m:language:eng"` - Streams with metadata language=eng
/// - `"u"` - Usable streams only
/// - `"disp:default"` - Streams with default disposition
///
/// # Examples
/// ```rust,ignore
/// let output = Output::from("output.mp4")
/// .add_stream_metadata("v:0", "language", "eng")
/// .add_stream_metadata("a:0", "title", "Main Audio");
/// ```
///
/// # Errors
/// Returns error if the stream specifier syntax is invalid.
pub fn add_stream_metadata(
mut self,
stream_spec: impl Into<String>,
key: impl Into<String>,
value: impl Into<String>,
) -> Result<Self, String> {
use crate::core::metadata::StreamSpecifier;
let stream_spec_str = stream_spec.into();
let key = key.into();
let value = value.into();
// Parse and validate stream specifier
let _specifier = StreamSpecifier::parse(&stream_spec_str)?;
// Store as (spec, key, value) tuple
// During output initialization, this will be matched against actual streams
// using StreamSpecifier::matches and applied to all matching streams
// Replicates FFmpeg's of_add_metadata behavior
self.stream_metadata.push((stream_spec_str, key, value));
Ok(self)
}
/// Add or update chapter-specific metadata.
///
/// Chapters are used for DVD-like navigation points in media files.
/// If value is empty string, the key will be removed (FFmpeg behavior).
/// Replicates FFmpeg's `-metadata:c:N key=value` option.
/// FFmpeg reference: fftools/ffmpeg_opt.c (`opt_metadata()` handles the `c:` target selector).
///
/// # Examples
/// ```rust,ignore
/// let output = Output::from("output.mp4")
/// .add_chapter_metadata(0, "title", "Introduction")
/// .add_chapter_metadata(1, "title", "Main Content");
/// ```
pub fn add_chapter_metadata(
mut self,
chapter_index: usize,
key: impl Into<String>,
value: impl Into<String>,
) -> Self {
let key = key.into();
let value = value.into();
if value.is_empty() {
// Empty value means remove the key (FFmpeg behavior)
if let Some(metadata) = self.chapter_metadata.get_mut(&chapter_index) {
metadata.remove(&key);
}
} else {
self.chapter_metadata
.entry(chapter_index)
.or_default()
.insert(key, value);
}
self
}
/// Add or update program-specific metadata.
///
/// Programs are used in multi-program transport streams (e.g., MPEG-TS).
/// If value is empty string, the key will be removed (FFmpeg behavior).
/// Replicates FFmpeg's `-metadata:p:N key=value` option.
/// FFmpeg reference: fftools/ffmpeg_opt.c (`opt_metadata()` with `p:` selector).
///
/// # Examples
/// ```rust,ignore
/// let output = Output::from("output.ts")
/// .add_program_metadata(0, "service_name", "Channel 1")
/// .add_program_metadata(1, "service_name", "Channel 2");
/// ```
pub fn add_program_metadata(
mut self,
program_index: usize,
key: impl Into<String>,
value: impl Into<String>,
) -> Self {
let key = key.into();
let value = value.into();
if value.is_empty() {
// Empty value means remove the key (FFmpeg behavior)
if let Some(metadata) = self.program_metadata.get_mut(&program_index) {
metadata.remove(&key);
}
} else {
self.program_metadata
.entry(program_index)
.or_default()
.insert(key, value);
}
self
}
/// Map metadata from an input file to this output.
///
/// Replicates FFmpeg's `-map_metadata [src_file_idx]:src_type:dst_type` option.
/// This allows copying metadata from specific locations in input files to
/// specific locations in the output file.
///
/// # Type Specifiers
/// - `"g"` or `""` - Global metadata
/// - `"s"` or `"s:spec"` - Stream metadata (with optional stream specifier)
/// - `"c:N"` - Chapter N metadata
/// - `"p:N"` - Program N metadata
///
/// # Examples
/// ```rust,ignore
/// use ez_ffmpeg::core::metadata::{MetadataType, MetadataMapping};
///
/// let output = Output::from("output.mp4")
/// // Copy global metadata from input 0 to output global
/// .map_metadata_from_input(0, "g", "g")?
/// // Copy first video stream metadata from input 1 to output first video stream
/// .map_metadata_from_input(1, "s:v:0", "s:v:0")?;
/// ```
///
/// # Errors
/// Returns error if the type specifier syntax is invalid.
/// FFmpeg reference: fftools/ffmpeg_opt.c (`opt_map_metadata()` parses the same
/// `[file][:type]` triplet and feeds it into `MetadataMapping`).
pub fn map_metadata_from_input(
mut self,
input_index: usize,
src_type_spec: impl Into<String>,
dst_type_spec: impl Into<String>,
) -> Result<Self, String> {
use crate::core::metadata::{MetadataMapping, MetadataType};
let src_type = MetadataType::parse(&src_type_spec.into())?;
let dst_type = MetadataType::parse(&dst_type_spec.into())?;
self.metadata_map.push(MetadataMapping {
src_type,
dst_type,
input_index,
});
Ok(self)
}
}