Skip to main content

yt_dlp/download/config/
postprocess.rs

1//! Post-processing configuration for video and audio processing.
2//!
3//! This module provides comprehensive post-processing options using FFmpeg,
4//! including codec conversion, bitrate adjustment, video filters, and more.
5
6use std::fmt;
7
8/// Video codec options for encoding
9#[derive(Clone, Copy, Debug, Default, Hash, PartialEq, Eq)]
10pub enum VideoCodec {
11    /// H.264/AVC codec (libx264)
12    H264,
13    /// H.265/HEVC codec (libx265)
14    H265,
15    /// VP9 codec (libvpx-vp9)
16    VP9,
17    /// AV1 codec (libaom-av1)
18    AV1,
19    /// Copy video stream without re-encoding
20    #[default]
21    Copy,
22}
23
24impl VideoCodec {
25    /// Converts to FFmpeg codec name
26    ///
27    /// # Returns
28    ///
29    /// The FFmpeg codec name string
30    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/// Audio codec options for encoding
54#[derive(Clone, Copy, Debug, Default, Hash, PartialEq, Eq)]
55pub enum AudioCodec {
56    /// AAC codec    
57    AAC,
58    /// MP3 codec (libmp3lame)
59    MP3,
60    /// Opus codec
61    Opus,
62    /// Vorbis codec
63    Vorbis,
64    /// Copy audio stream without re-encoding
65    #[default]
66    Copy,
67}
68
69impl AudioCodec {
70    /// Converts to FFmpeg codec name
71    ///
72    /// # Returns
73    ///
74    /// The FFmpeg codec name string
75    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/// Video resolution preset
99#[derive(Clone, Debug, Hash, PartialEq, Eq)]
100pub enum Resolution {
101    /// 7680x4320 (8K)
102    UHD8K,
103    /// 3840x2160 (4K)
104    UHD4K,
105    /// 2560x1440 (2K/QHD)
106    QHD,
107    /// 1920x1080 (Full HD)
108    FullHD,
109    /// 1280x720 (HD)
110    HD,
111    /// 854x480 (SD)
112    SD,
113    /// 640x360
114    Low,
115    /// Custom resolution
116    Custom { width: u32, height: u32 },
117}
118
119impl Resolution {
120    /// Returns the width and height for this resolution
121    ///
122    /// # Returns
123    ///
124    /// A tuple (width, height) in pixels
125    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    /// Converts to FFmpeg scale filter format
139    ///
140    /// # Returns
141    ///
142    /// FFmpeg scale filter string (e.g., "1920:1080")
143    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/// Encoding preset for quality/speed trade-off
167#[derive(Clone, Copy, Debug, Default, Hash, PartialEq, Eq)]
168pub enum EncodingPreset {
169    /// Ultra fast encoding (lowest quality)
170    UltraFast,
171    /// Super fast encoding
172    SuperFast,
173    /// Very fast encoding
174    VeryFast,
175    /// Fast encoding
176    Fast,
177    /// Medium encoding (balanced)
178    #[default]
179    Medium,
180    /// Slow encoding (better quality)
181    Slow,
182    /// Slower encoding
183    Slower,
184    /// Very slow encoding (best quality)
185    VerySlow,
186}
187
188impl EncodingPreset {
189    /// Converts to FFmpeg preset name
190    ///
191    /// # Returns
192    ///
193    /// The FFmpeg preset name string
194    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/// Watermark position on the video
224#[derive(Clone, Debug, Hash, PartialEq, Eq)]
225pub enum WatermarkPosition {
226    /// Top left corner
227    TopLeft,
228    /// Top right corner
229    TopRight,
230    /// Bottom left corner
231    BottomLeft,
232    /// Bottom right corner
233    BottomRight,
234    /// Center
235    Center,
236    /// Custom position (x, y coordinates)
237    Custom { x: u32, y: u32 },
238}
239
240impl WatermarkPosition {
241    /// Converts to FFmpeg overlay position
242    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/// Video filter options
268#[derive(Clone, Debug, PartialEq)]
269pub enum FfmpegFilter {
270    /// Crop video to specific dimensions
271    Crop { width: u32, height: u32, x: u32, y: u32 },
272    /// Rotate video by degrees
273    Rotate { angle: i32 },
274    /// Add watermark image
275    Watermark { path: String, position: WatermarkPosition },
276    /// Adjust brightness (-1.0 to 1.0)
277    Brightness { value: f32 },
278    /// Adjust contrast (0.0 to 4.0)
279    Contrast { value: f32 },
280    /// Adjust saturation (0.0 to 3.0)
281    Saturation { value: f32 },
282    /// Apply blur effect
283    Blur { radius: u32 },
284    /// Flip horizontally
285    FlipHorizontal,
286    /// Flip vertically
287    FlipVertical,
288    /// Denoise video
289    Denoise,
290    /// Sharpen video
291    Sharpen,
292    /// Custom FFmpeg filter string
293    Custom { filter: String },
294}
295
296impl FfmpegFilter {
297    /// Converts filter to FFmpeg filter string
298    ///
299    /// # Returns
300    ///
301    /// The FFmpeg filter string
302    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/// Comprehensive post-processing configuration
347#[derive(Clone, Debug, PartialEq)]
348pub struct PostProcessConfig {
349    /// Video codec to use for encoding
350    pub video_codec: Option<VideoCodec>,
351    /// Audio codec to use for encoding
352    pub audio_codec: Option<AudioCodec>,
353    /// Video bitrate (e.g., "2M", "5M")
354    pub video_bitrate: Option<String>,
355    /// Audio bitrate (e.g., "128k", "192k", "320k")
356    pub audio_bitrate: Option<String>,
357    /// Target resolution for scaling
358    pub resolution: Option<Resolution>,
359    /// Target framerate
360    pub framerate: Option<u32>,
361    /// Encoding preset (quality/speed trade-off)
362    pub preset: Option<EncodingPreset>,
363    /// Video filters to apply
364    pub filters: Vec<FfmpegFilter>,
365}
366
367impl PostProcessConfig {
368    /// Creates a new post-processing configuration
369    ///
370    /// # Returns
371    ///
372    /// An empty PostProcessConfig with all options set to None
373    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    /// Sets the video codec
389    ///
390    /// # Arguments
391    ///
392    /// * `codec` - Video codec to use
393    ///
394    /// # Returns
395    ///
396    /// Self for method chaining
397    pub fn with_video_codec(mut self, codec: VideoCodec) -> Self {
398        self.video_codec = Some(codec);
399        self
400    }
401
402    /// Sets the audio codec
403    ///
404    /// # Arguments
405    ///
406    /// * `codec` - Audio codec to use
407    ///
408    /// # Returns
409    ///
410    /// Self for method chaining
411    pub fn with_audio_codec(mut self, codec: AudioCodec) -> Self {
412        self.audio_codec = Some(codec);
413        self
414    }
415
416    /// Sets the video bitrate
417    pub fn with_video_bitrate(mut self, bitrate: impl Into<String>) -> Self {
418        self.video_bitrate = Some(bitrate.into());
419        self
420    }
421
422    /// Sets the audio bitrate
423    pub fn with_audio_bitrate(mut self, bitrate: impl Into<String>) -> Self {
424        self.audio_bitrate = Some(bitrate.into());
425        self
426    }
427
428    /// Sets the target resolution
429    pub fn with_resolution(mut self, resolution: Resolution) -> Self {
430        self.resolution = Some(resolution);
431        self
432    }
433
434    /// Sets the target framerate
435    pub fn with_framerate(mut self, fps: u32) -> Self {
436        self.framerate = Some(fps);
437        self
438    }
439
440    /// Sets the encoding preset
441    pub fn with_preset(mut self, preset: EncodingPreset) -> Self {
442        self.preset = Some(preset);
443        self
444    }
445
446    /// Adds a filter to the processing pipeline
447    ///
448    /// # Arguments
449    ///
450    /// * `filter` - FFmpeg filter to add
451    ///
452    /// # Returns
453    ///
454    /// Self for method chaining
455    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    /// Checks if any post-processing is configured
463    ///
464    /// # Returns
465    ///
466    /// true if no post-processing options are set, false otherwise
467    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}