1use std::fmt;
7
8#[derive(Clone, Copy, Debug, Default, Hash, PartialEq, Eq)]
10pub enum VideoCodec {
11 H264,
13 H265,
15 VP9,
17 AV1,
19 #[default]
21 Copy,
22}
23
24impl VideoCodec {
25 pub fn to_ffmpeg_name(&self) -> &str {
31 match self {
32 Self::H264 => "libx264",
33 Self::H265 => "libx265",
34 Self::VP9 => "libvpx-vp9",
35 Self::AV1 => "libaom-av1",
36 Self::Copy => "copy",
37 }
38 }
39}
40
41impl fmt::Display for VideoCodec {
42 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
43 match self {
44 Self::H264 => f.write_str("H264"),
45 Self::H265 => f.write_str("H265"),
46 Self::VP9 => f.write_str("VP9"),
47 Self::AV1 => f.write_str("AV1"),
48 Self::Copy => f.write_str("Copy"),
49 }
50 }
51}
52
53#[derive(Clone, Copy, Debug, Default, Hash, PartialEq, Eq)]
55pub enum AudioCodec {
56 AAC,
58 MP3,
60 Opus,
62 Vorbis,
64 #[default]
66 Copy,
67}
68
69impl AudioCodec {
70 pub fn to_ffmpeg_name(&self) -> &str {
76 match self {
77 Self::AAC => "aac",
78 Self::MP3 => "libmp3lame",
79 Self::Opus => "libopus",
80 Self::Vorbis => "libvorbis",
81 Self::Copy => "copy",
82 }
83 }
84}
85
86impl fmt::Display for AudioCodec {
87 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
88 match self {
89 Self::AAC => f.write_str("AAC"),
90 Self::MP3 => f.write_str("MP3"),
91 Self::Opus => f.write_str("Opus"),
92 Self::Vorbis => f.write_str("Vorbis"),
93 Self::Copy => f.write_str("Copy"),
94 }
95 }
96}
97
98#[derive(Clone, Debug, Hash, PartialEq, Eq)]
100pub enum Resolution {
101 UHD8K,
103 UHD4K,
105 QHD,
107 FullHD,
109 HD,
111 SD,
113 Low,
115 Custom { width: u32, height: u32 },
117}
118
119impl Resolution {
120 pub fn dimensions(&self) -> (u32, u32) {
126 match self {
127 Self::UHD8K => (7680, 4320),
128 Self::UHD4K => (3840, 2160),
129 Self::QHD => (2560, 1440),
130 Self::FullHD => (1920, 1080),
131 Self::HD => (1280, 720),
132 Self::SD => (854, 480),
133 Self::Low => (640, 360),
134 Self::Custom { width, height } => (*width, *height),
135 }
136 }
137
138 pub fn to_ffmpeg_scale(&self) -> String {
144 let (width, height) = self.dimensions();
145 format!("{}:{}", width, height)
146 }
147}
148
149impl fmt::Display for Resolution {
150 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
151 match self {
152 Self::UHD8K => f.write_str("UHD8K"),
153 Self::UHD4K => f.write_str("UHD4K"),
154 Self::QHD => f.write_str("QHD"),
155 Self::FullHD => f.write_str("FullHD"),
156 Self::HD => f.write_str("HD"),
157 Self::SD => f.write_str("SD"),
158 Self::Low => f.write_str("Low"),
159 Self::Custom { width, height } => {
160 write!(f, "Custom(width={}, height={})", width, height)
161 }
162 }
163 }
164}
165
166#[derive(Clone, Copy, Debug, Default, Hash, PartialEq, Eq)]
168pub enum EncodingPreset {
169 UltraFast,
171 SuperFast,
173 VeryFast,
175 Fast,
177 #[default]
179 Medium,
180 Slow,
182 Slower,
184 VerySlow,
186}
187
188impl EncodingPreset {
189 pub fn to_ffmpeg_name(&self) -> &str {
195 match self {
196 Self::UltraFast => "ultrafast",
197 Self::SuperFast => "superfast",
198 Self::VeryFast => "veryfast",
199 Self::Fast => "fast",
200 Self::Medium => "medium",
201 Self::Slow => "slow",
202 Self::Slower => "slower",
203 Self::VerySlow => "veryslow",
204 }
205 }
206}
207
208impl fmt::Display for EncodingPreset {
209 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
210 match self {
211 Self::UltraFast => f.write_str("UltraFast"),
212 Self::SuperFast => f.write_str("SuperFast"),
213 Self::VeryFast => f.write_str("VeryFast"),
214 Self::Fast => f.write_str("Fast"),
215 Self::Medium => f.write_str("Medium"),
216 Self::Slow => f.write_str("Slow"),
217 Self::Slower => f.write_str("Slower"),
218 Self::VerySlow => f.write_str("VerySlow"),
219 }
220 }
221}
222
223#[derive(Clone, Debug, Hash, PartialEq, Eq)]
225pub enum WatermarkPosition {
226 TopLeft,
228 TopRight,
230 BottomLeft,
232 BottomRight,
234 Center,
236 Custom { x: u32, y: u32 },
238}
239
240impl WatermarkPosition {
241 pub fn to_ffmpeg_position(&self) -> String {
243 match self {
244 Self::TopLeft => "x=10:y=10".to_string(),
245 Self::TopRight => "x=W-w-10:y=10".to_string(),
246 Self::BottomLeft => "x=10:y=H-h-10".to_string(),
247 Self::BottomRight => "x=W-w-10:y=H-h-10".to_string(),
248 Self::Center => "x=(W-w)/2:y=(H-h)/2".to_string(),
249 Self::Custom { x, y } => format!("x={}:y={}", x, y),
250 }
251 }
252}
253
254impl fmt::Display for WatermarkPosition {
255 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
256 match self {
257 Self::TopLeft => f.write_str("TopLeft"),
258 Self::TopRight => f.write_str("TopRight"),
259 Self::BottomLeft => f.write_str("BottomLeft"),
260 Self::BottomRight => f.write_str("BottomRight"),
261 Self::Center => f.write_str("Center"),
262 Self::Custom { x, y } => write!(f, "Custom(x={}, y={})", x, y),
263 }
264 }
265}
266
267#[derive(Clone, Debug, PartialEq)]
269pub enum FfmpegFilter {
270 Crop { width: u32, height: u32, x: u32, y: u32 },
272 Rotate { angle: i32 },
274 Watermark { path: String, position: WatermarkPosition },
276 Brightness { value: f32 },
278 Contrast { value: f32 },
280 Saturation { value: f32 },
282 Blur { radius: u32 },
284 FlipHorizontal,
286 FlipVertical,
288 Denoise,
290 Sharpen,
292 Custom { filter: String },
294}
295
296impl FfmpegFilter {
297 pub fn to_ffmpeg_string(&self) -> String {
303 match self {
304 Self::Crop { width, height, x, y } => format!("crop={}:{}:{}:{}", width, height, x, y),
305 Self::Rotate { angle } => {
306 let radians = (*angle as f64) * std::f64::consts::PI / 180.0;
307 format!("rotate={}:ow=rotw({}):oh=roth({})", radians, radians, radians)
308 }
309 Self::Watermark { path, position } => {
310 format!("movie={}[wm];[in][wm]overlay={}", path, position.to_ffmpeg_position())
311 }
312 Self::Brightness { value } => format!("eq=brightness={}", value),
313 Self::Contrast { value } => format!("eq=contrast={}", value),
314 Self::Saturation { value } => format!("eq=saturation={}", value),
315 Self::Blur { radius } => format!("boxblur={}:{}", radius, radius),
316 Self::FlipHorizontal => "hflip".to_string(),
317 Self::FlipVertical => "vflip".to_string(),
318 Self::Denoise => "hqdn3d".to_string(),
319 Self::Sharpen => "unsharp=5:5:1.0:5:5:0.0".to_string(),
320 Self::Custom { filter } => filter.clone(),
321 }
322 }
323}
324
325impl fmt::Display for FfmpegFilter {
326 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
327 match self {
328 Self::Crop { width, height, x, y } => {
329 write!(f, "Crop(width={}, height={}, x={}, y={})", width, height, x, y)
330 }
331 Self::Rotate { angle } => write!(f, "Rotate(angle={})", angle),
332 Self::Watermark { position, .. } => write!(f, "Watermark(position={})", position),
333 Self::Brightness { value } => write!(f, "Brightness(value={})", value),
334 Self::Contrast { value } => write!(f, "Contrast(value={})", value),
335 Self::Saturation { value } => write!(f, "Saturation(value={})", value),
336 Self::Blur { radius } => write!(f, "Blur(radius={})", radius),
337 Self::FlipHorizontal => f.write_str("FlipHorizontal"),
338 Self::FlipVertical => f.write_str("FlipVertical"),
339 Self::Denoise => f.write_str("Denoise"),
340 Self::Sharpen => f.write_str("Sharpen"),
341 Self::Custom { filter } => write!(f, "Custom(filter={})", filter),
342 }
343 }
344}
345
346#[derive(Clone, Debug, PartialEq)]
348pub struct PostProcessConfig {
349 pub video_codec: Option<VideoCodec>,
351 pub audio_codec: Option<AudioCodec>,
353 pub video_bitrate: Option<String>,
355 pub audio_bitrate: Option<String>,
357 pub resolution: Option<Resolution>,
359 pub framerate: Option<u32>,
361 pub preset: Option<EncodingPreset>,
363 pub filters: Vec<FfmpegFilter>,
365}
366
367impl PostProcessConfig {
368 pub fn new() -> Self {
374 tracing::debug!("✂️ Created new post-processing configuration");
375
376 Self {
377 video_codec: None,
378 audio_codec: None,
379 video_bitrate: None,
380 audio_bitrate: None,
381 resolution: None,
382 framerate: None,
383 preset: None,
384 filters: Vec::new(),
385 }
386 }
387
388 pub fn with_video_codec(mut self, codec: VideoCodec) -> Self {
398 self.video_codec = Some(codec);
399 self
400 }
401
402 pub fn with_audio_codec(mut self, codec: AudioCodec) -> Self {
412 self.audio_codec = Some(codec);
413 self
414 }
415
416 pub fn with_video_bitrate(mut self, bitrate: impl Into<String>) -> Self {
418 self.video_bitrate = Some(bitrate.into());
419 self
420 }
421
422 pub fn with_audio_bitrate(mut self, bitrate: impl Into<String>) -> Self {
424 self.audio_bitrate = Some(bitrate.into());
425 self
426 }
427
428 pub fn with_resolution(mut self, resolution: Resolution) -> Self {
430 self.resolution = Some(resolution);
431 self
432 }
433
434 pub fn with_framerate(mut self, fps: u32) -> Self {
436 self.framerate = Some(fps);
437 self
438 }
439
440 pub fn with_preset(mut self, preset: EncodingPreset) -> Self {
442 self.preset = Some(preset);
443 self
444 }
445
446 pub fn add_filter(mut self, filter: FfmpegFilter) -> Self {
456 tracing::debug!(filter = ?filter, "✂️ Adding FFmpeg filter to post-processing config");
457
458 self.filters.push(filter);
459 self
460 }
461
462 pub fn is_empty(&self) -> bool {
468 self.video_codec.is_none()
469 && self.audio_codec.is_none()
470 && self.video_bitrate.is_none()
471 && self.audio_bitrate.is_none()
472 && self.resolution.is_none()
473 && self.framerate.is_none()
474 && self.preset.is_none()
475 && self.filters.is_empty()
476 }
477}
478
479impl Default for PostProcessConfig {
480 fn default() -> Self {
481 Self::new()
482 }
483}
484
485impl fmt::Display for PostProcessConfig {
486 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
487 let video = self.video_codec.as_ref().map_or("None".to_string(), |c| c.to_string());
488 let audio = self.audio_codec.as_ref().map_or("None".to_string(), |c| c.to_string());
489 write!(
490 f,
491 "PostProcessConfig(video_codec={}, audio_codec={}, filters={})",
492 video,
493 audio,
494 self.filters.len()
495 )
496 }
497}