1mod presets;
21mod validation;
22
23use ff_format::{PixelFormat, VideoCodec};
24
25use crate::video::codec_options::VideoCodecOptions;
26use crate::{AudioCodec, BitrateMode, EncodeError, VideoEncoderBuilder};
27
28#[derive(Debug, Clone)]
33pub struct VideoEncoderConfig {
34 pub codec: VideoCodec,
36 pub width: Option<u32>,
38 pub height: Option<u32>,
40 pub fps: Option<f64>,
42 pub bitrate_mode: BitrateMode,
44 pub pixel_format: Option<PixelFormat>,
46 pub codec_options: Option<VideoCodecOptions>,
48}
49
50#[derive(Debug, Clone)]
52pub struct AudioEncoderConfig {
53 pub codec: AudioCodec,
55 pub sample_rate: u32,
57 pub channels: u32,
59 pub bitrate: u64,
61}
62
63#[derive(Debug, Clone)]
84pub struct ExportPreset {
85 pub name: String,
87 pub video: Option<VideoEncoderConfig>,
89 pub audio: AudioEncoderConfig,
91}
92
93impl ExportPreset {
94 #[must_use]
98 pub fn youtube_1080p() -> Self {
99 presets::youtube_1080p()
100 }
101
102 #[must_use]
104 pub fn youtube_4k() -> Self {
105 presets::youtube_4k()
106 }
107
108 #[must_use]
110 pub fn twitter() -> Self {
111 presets::twitter()
112 }
113
114 #[must_use]
116 pub fn instagram_square() -> Self {
117 presets::instagram_square()
118 }
119
120 #[must_use]
122 pub fn instagram_reels() -> Self {
123 presets::instagram_reels()
124 }
125
126 #[must_use]
128 pub fn bluray_1080p() -> Self {
129 presets::bluray_1080p()
130 }
131
132 #[must_use]
134 pub fn podcast_mono() -> Self {
135 presets::podcast_mono()
136 }
137
138 #[must_use]
140 pub fn lossless_rgb() -> Self {
141 presets::lossless_rgb()
142 }
143
144 #[must_use]
146 pub fn web_h264() -> Self {
147 presets::web_h264()
148 }
149
150 pub fn validate(&self) -> Result<(), EncodeError> {
164 validation::validate_preset(self)
165 }
166
167 #[must_use]
178 pub fn apply_video(&self, builder: VideoEncoderBuilder) -> VideoEncoderBuilder {
179 let Some(ref v) = self.video else {
180 return builder;
181 };
182 let mut b = builder
183 .video_codec(v.codec)
184 .bitrate_mode(v.bitrate_mode.clone());
185 if let (Some(w), Some(h), Some(fps)) = (v.width, v.height, v.fps) {
186 b = b.video(w, h, fps);
187 }
188 if let Some(pf) = v.pixel_format {
189 b = b.pixel_format(pf);
190 }
191 if let Some(opts) = v.codec_options.clone() {
192 b = b.codec_options(opts);
193 }
194 b
195 }
196
197 #[must_use]
201 pub fn apply_audio(&self, builder: VideoEncoderBuilder) -> VideoEncoderBuilder {
202 builder
203 .audio(self.audio.sample_rate, self.audio.channels)
204 .audio_codec(self.audio.codec)
205 .audio_bitrate(self.audio.bitrate)
206 }
207}
208
209#[cfg(test)]
210mod tests {
211 use super::*;
212
213 #[test]
216 fn youtube_1080p_preset_should_have_correct_codec() {
217 let preset = ExportPreset::youtube_1080p();
218 let video = preset
219 .video
220 .as_ref()
221 .expect("youtube_1080p must have video");
222 assert_eq!(video.codec, VideoCodec::H264);
223 }
224
225 #[test]
226 fn youtube_1080p_preset_should_have_correct_resolution() {
227 let preset = ExportPreset::youtube_1080p();
228 let video = preset
229 .video
230 .as_ref()
231 .expect("youtube_1080p must have video");
232 assert_eq!(video.width, Some(1920));
233 assert_eq!(video.height, Some(1080));
234 }
235
236 #[test]
237 fn youtube_1080p_preset_audio_should_be_aac_192kbps() {
238 let preset = ExportPreset::youtube_1080p();
239 assert_eq!(preset.audio.codec, AudioCodec::Aac);
240 assert_eq!(preset.audio.bitrate, 192_000);
241 }
242
243 #[test]
246 fn youtube_4k_preset_should_have_h265_codec() {
247 let preset = ExportPreset::youtube_4k();
248 let video = preset.video.as_ref().expect("youtube_4k must have video");
249 assert_eq!(video.codec, VideoCodec::H265);
250 }
251
252 #[test]
253 fn youtube_4k_preset_should_have_correct_resolution() {
254 let preset = ExportPreset::youtube_4k();
255 let video = preset.video.as_ref().expect("youtube_4k must have video");
256 assert_eq!(video.width, Some(3840));
257 assert_eq!(video.height, Some(2160));
258 }
259
260 #[test]
261 fn youtube_4k_preset_audio_should_be_aac_256kbps() {
262 let preset = ExportPreset::youtube_4k();
263 assert_eq!(preset.audio.codec, AudioCodec::Aac);
264 assert_eq!(preset.audio.bitrate, 256_000);
265 }
266
267 #[test]
270 fn lossless_rgb_preset_should_have_ffv1_codec() {
271 let preset = ExportPreset::lossless_rgb();
272 let video = preset.video.as_ref().expect("lossless_rgb must have video");
273 assert_eq!(video.codec, VideoCodec::Ffv1);
274 }
275
276 #[test]
277 fn lossless_rgb_preset_should_preserve_source_resolution() {
278 let preset = ExportPreset::lossless_rgb();
279 let video = preset.video.as_ref().expect("lossless_rgb must have video");
280 assert!(video.width.is_none(), "lossless_rgb should not fix width");
281 assert!(video.height.is_none(), "lossless_rgb should not fix height");
282 assert!(video.fps.is_none(), "lossless_rgb should not fix fps");
283 }
284
285 #[test]
288 fn podcast_mono_preset_should_have_no_video() {
289 let preset = ExportPreset::podcast_mono();
290 assert!(
291 preset.video.is_none(),
292 "podcast_mono must be audio-only (video=None)"
293 );
294 }
295
296 #[test]
297 fn podcast_mono_preset_should_have_mono_audio() {
298 let preset = ExportPreset::podcast_mono();
299 assert_eq!(preset.audio.channels, 1);
300 }
301
302 #[test]
305 fn web_h264_preset_should_use_vp9_codec() {
306 let preset = ExportPreset::web_h264();
307 let video = preset.video.as_ref().expect("web_h264 must have video");
308 assert_eq!(video.codec, VideoCodec::Vp9);
309 }
310
311 #[test]
314 fn all_presets_should_have_non_empty_names() {
315 let presets = [
316 ExportPreset::youtube_1080p(),
317 ExportPreset::youtube_4k(),
318 ExportPreset::twitter(),
319 ExportPreset::instagram_square(),
320 ExportPreset::instagram_reels(),
321 ExportPreset::bluray_1080p(),
322 ExportPreset::podcast_mono(),
323 ExportPreset::lossless_rgb(),
324 ExportPreset::web_h264(),
325 ];
326 for preset in &presets {
327 assert!(!preset.name.is_empty(), "preset name must not be empty");
328 }
329 }
330
331 #[test]
334 fn custom_preset_should_apply_codec_to_builder() {
335 use crate::VideoEncoder;
336
337 let preset = ExportPreset {
338 name: "custom".to_string(),
339 video: Some(VideoEncoderConfig {
340 codec: VideoCodec::H264,
341 width: Some(1280),
342 height: Some(720),
343 fps: Some(24.0),
344 bitrate_mode: BitrateMode::Crf(23),
345 pixel_format: None,
346 codec_options: None,
347 }),
348 audio: AudioEncoderConfig {
349 codec: AudioCodec::Aac,
350 sample_rate: 44100,
351 channels: 2,
352 bitrate: 128_000,
353 },
354 };
355
356 let builder = preset.apply_video(VideoEncoder::create("out.mp4"));
357 assert_eq!(builder.video_codec, VideoCodec::H264);
358 assert_eq!(builder.video_width, Some(1280));
359 assert_eq!(builder.video_height, Some(720));
360 }
361
362 #[test]
363 fn preset_with_no_resolution_should_not_override_builder_resolution() {
364 use crate::VideoEncoder;
365
366 let preset = ExportPreset {
367 name: "no-res".to_string(),
368 video: Some(VideoEncoderConfig {
369 codec: VideoCodec::Ffv1,
370 width: None,
371 height: None,
372 fps: None,
373 bitrate_mode: BitrateMode::Crf(0),
374 pixel_format: None,
375 codec_options: None,
376 }),
377 audio: AudioEncoderConfig {
378 codec: AudioCodec::Flac,
379 sample_rate: 48000,
380 channels: 2,
381 bitrate: 0,
382 },
383 };
384
385 let builder = preset.apply_video(VideoEncoder::create("out.mkv"));
387 assert!(
388 builder.video_width.is_none(),
389 "apply_video should not set width when VideoEncoderConfig.width is None"
390 );
391 assert!(
392 builder.video_height.is_none(),
393 "apply_video should not set height when VideoEncoderConfig.height is None"
394 );
395 }
396
397 #[test]
398 fn audio_only_preset_apply_video_should_return_builder_unchanged() {
399 use crate::VideoEncoder;
400
401 let preset = ExportPreset::podcast_mono();
402 let builder_before = VideoEncoder::create("out.m4a");
403 let builder_after = preset.apply_video(VideoEncoder::create("out.m4a"));
404 assert_eq!(
406 builder_before.video_codec, builder_after.video_codec,
407 "apply_video on audio-only preset must leave builder unchanged"
408 );
409 }
410
411 #[test]
412 fn apply_audio_should_set_sample_rate_and_bitrate() {
413 use crate::VideoEncoder;
414
415 let preset = ExportPreset::youtube_1080p();
416 let builder = preset.apply_audio(VideoEncoder::create("out.mp4"));
417 assert_eq!(builder.audio_sample_rate, Some(48000));
418 assert_eq!(builder.audio_bitrate, Some(192_000));
419 }
420
421 #[test]
424 fn youtube_1080p_preset_should_pass_validation() {
425 assert!(ExportPreset::youtube_1080p().validate().is_ok());
426 }
427
428 #[test]
429 fn youtube_4k_preset_should_pass_validation() {
430 assert!(ExportPreset::youtube_4k().validate().is_ok());
431 }
432
433 #[test]
434 fn instagram_square_preset_should_pass_validation() {
435 assert!(ExportPreset::instagram_square().validate().is_ok());
436 }
437
438 #[test]
439 fn instagram_reels_preset_should_pass_validation() {
440 assert!(ExportPreset::instagram_reels().validate().is_ok());
441 }
442}