use std::fmt;
#[derive(Clone, Copy, Debug, Default, Hash, PartialEq, Eq)]
pub enum VideoCodec {
H264,
H265,
VP9,
AV1,
#[default]
Copy,
}
impl VideoCodec {
pub fn to_ffmpeg_name(&self) -> &str {
match self {
Self::H264 => "libx264",
Self::H265 => "libx265",
Self::VP9 => "libvpx-vp9",
Self::AV1 => "libaom-av1",
Self::Copy => "copy",
}
}
}
impl fmt::Display for VideoCodec {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::H264 => f.write_str("H264"),
Self::H265 => f.write_str("H265"),
Self::VP9 => f.write_str("VP9"),
Self::AV1 => f.write_str("AV1"),
Self::Copy => f.write_str("Copy"),
}
}
}
#[derive(Clone, Copy, Debug, Default, Hash, PartialEq, Eq)]
pub enum AudioCodec {
AAC,
MP3,
Opus,
Vorbis,
#[default]
Copy,
}
impl AudioCodec {
pub fn to_ffmpeg_name(&self) -> &str {
match self {
Self::AAC => "aac",
Self::MP3 => "libmp3lame",
Self::Opus => "libopus",
Self::Vorbis => "libvorbis",
Self::Copy => "copy",
}
}
}
impl fmt::Display for AudioCodec {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::AAC => f.write_str("AAC"),
Self::MP3 => f.write_str("MP3"),
Self::Opus => f.write_str("Opus"),
Self::Vorbis => f.write_str("Vorbis"),
Self::Copy => f.write_str("Copy"),
}
}
}
#[derive(Clone, Debug, Hash, PartialEq, Eq)]
pub enum Resolution {
UHD8K,
UHD4K,
QHD,
FullHD,
HD,
SD,
Low,
Custom { width: u32, height: u32 },
}
impl Resolution {
pub fn dimensions(&self) -> (u32, u32) {
match self {
Self::UHD8K => (7680, 4320),
Self::UHD4K => (3840, 2160),
Self::QHD => (2560, 1440),
Self::FullHD => (1920, 1080),
Self::HD => (1280, 720),
Self::SD => (854, 480),
Self::Low => (640, 360),
Self::Custom { width, height } => (*width, *height),
}
}
pub fn to_ffmpeg_scale(&self) -> String {
let (width, height) = self.dimensions();
format!("{}:{}", width, height)
}
}
impl fmt::Display for Resolution {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::UHD8K => f.write_str("UHD8K"),
Self::UHD4K => f.write_str("UHD4K"),
Self::QHD => f.write_str("QHD"),
Self::FullHD => f.write_str("FullHD"),
Self::HD => f.write_str("HD"),
Self::SD => f.write_str("SD"),
Self::Low => f.write_str("Low"),
Self::Custom { width, height } => {
write!(f, "Custom(width={}, height={})", width, height)
}
}
}
}
#[derive(Clone, Copy, Debug, Default, Hash, PartialEq, Eq)]
pub enum EncodingPreset {
UltraFast,
SuperFast,
VeryFast,
Fast,
#[default]
Medium,
Slow,
Slower,
VerySlow,
}
impl EncodingPreset {
pub fn to_ffmpeg_name(&self) -> &str {
match self {
Self::UltraFast => "ultrafast",
Self::SuperFast => "superfast",
Self::VeryFast => "veryfast",
Self::Fast => "fast",
Self::Medium => "medium",
Self::Slow => "slow",
Self::Slower => "slower",
Self::VerySlow => "veryslow",
}
}
}
impl fmt::Display for EncodingPreset {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::UltraFast => f.write_str("UltraFast"),
Self::SuperFast => f.write_str("SuperFast"),
Self::VeryFast => f.write_str("VeryFast"),
Self::Fast => f.write_str("Fast"),
Self::Medium => f.write_str("Medium"),
Self::Slow => f.write_str("Slow"),
Self::Slower => f.write_str("Slower"),
Self::VerySlow => f.write_str("VerySlow"),
}
}
}
#[derive(Clone, Debug, Hash, PartialEq, Eq)]
pub enum WatermarkPosition {
TopLeft,
TopRight,
BottomLeft,
BottomRight,
Center,
Custom { x: u32, y: u32 },
}
impl WatermarkPosition {
pub fn to_ffmpeg_position(&self) -> String {
match self {
Self::TopLeft => "x=10:y=10".to_string(),
Self::TopRight => "x=W-w-10:y=10".to_string(),
Self::BottomLeft => "x=10:y=H-h-10".to_string(),
Self::BottomRight => "x=W-w-10:y=H-h-10".to_string(),
Self::Center => "x=(W-w)/2:y=(H-h)/2".to_string(),
Self::Custom { x, y } => format!("x={}:y={}", x, y),
}
}
}
impl fmt::Display for WatermarkPosition {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::TopLeft => f.write_str("TopLeft"),
Self::TopRight => f.write_str("TopRight"),
Self::BottomLeft => f.write_str("BottomLeft"),
Self::BottomRight => f.write_str("BottomRight"),
Self::Center => f.write_str("Center"),
Self::Custom { x, y } => write!(f, "Custom(x={}, y={})", x, y),
}
}
}
#[derive(Clone, Debug, PartialEq)]
pub enum FfmpegFilter {
Crop { width: u32, height: u32, x: u32, y: u32 },
Rotate { angle: i32 },
Watermark { path: String, position: WatermarkPosition },
Brightness { value: f32 },
Contrast { value: f32 },
Saturation { value: f32 },
Blur { radius: u32 },
FlipHorizontal,
FlipVertical,
Denoise,
Sharpen,
Custom { filter: String },
}
impl FfmpegFilter {
pub fn to_ffmpeg_string(&self) -> String {
match self {
Self::Crop { width, height, x, y } => format!("crop={}:{}:{}:{}", width, height, x, y),
Self::Rotate { angle } => {
let radians = (*angle as f64) * std::f64::consts::PI / 180.0;
format!("rotate={}:ow=rotw({}):oh=roth({})", radians, radians, radians)
}
Self::Watermark { path, position } => {
format!("movie={}[wm];[in][wm]overlay={}", path, position.to_ffmpeg_position())
}
Self::Brightness { value } => format!("eq=brightness={}", value),
Self::Contrast { value } => format!("eq=contrast={}", value),
Self::Saturation { value } => format!("eq=saturation={}", value),
Self::Blur { radius } => format!("boxblur={}:{}", radius, radius),
Self::FlipHorizontal => "hflip".to_string(),
Self::FlipVertical => "vflip".to_string(),
Self::Denoise => "hqdn3d".to_string(),
Self::Sharpen => "unsharp=5:5:1.0:5:5:0.0".to_string(),
Self::Custom { filter } => filter.clone(),
}
}
}
impl fmt::Display for FfmpegFilter {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Crop { width, height, x, y } => {
write!(f, "Crop(width={}, height={}, x={}, y={})", width, height, x, y)
}
Self::Rotate { angle } => write!(f, "Rotate(angle={})", angle),
Self::Watermark { position, .. } => write!(f, "Watermark(position={})", position),
Self::Brightness { value } => write!(f, "Brightness(value={})", value),
Self::Contrast { value } => write!(f, "Contrast(value={})", value),
Self::Saturation { value } => write!(f, "Saturation(value={})", value),
Self::Blur { radius } => write!(f, "Blur(radius={})", radius),
Self::FlipHorizontal => f.write_str("FlipHorizontal"),
Self::FlipVertical => f.write_str("FlipVertical"),
Self::Denoise => f.write_str("Denoise"),
Self::Sharpen => f.write_str("Sharpen"),
Self::Custom { filter } => write!(f, "Custom(filter={})", filter),
}
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct PostProcessConfig {
pub video_codec: Option<VideoCodec>,
pub audio_codec: Option<AudioCodec>,
pub video_bitrate: Option<String>,
pub audio_bitrate: Option<String>,
pub resolution: Option<Resolution>,
pub framerate: Option<u32>,
pub preset: Option<EncodingPreset>,
pub filters: Vec<FfmpegFilter>,
}
impl PostProcessConfig {
pub fn new() -> Self {
tracing::debug!("✂️ Created new post-processing configuration");
Self {
video_codec: None,
audio_codec: None,
video_bitrate: None,
audio_bitrate: None,
resolution: None,
framerate: None,
preset: None,
filters: Vec::new(),
}
}
pub fn with_video_codec(mut self, codec: VideoCodec) -> Self {
self.video_codec = Some(codec);
self
}
pub fn with_audio_codec(mut self, codec: AudioCodec) -> Self {
self.audio_codec = Some(codec);
self
}
pub fn with_video_bitrate(mut self, bitrate: impl Into<String>) -> Self {
self.video_bitrate = Some(bitrate.into());
self
}
pub fn with_audio_bitrate(mut self, bitrate: impl Into<String>) -> Self {
self.audio_bitrate = Some(bitrate.into());
self
}
pub fn with_resolution(mut self, resolution: Resolution) -> Self {
self.resolution = Some(resolution);
self
}
pub fn with_framerate(mut self, fps: u32) -> Self {
self.framerate = Some(fps);
self
}
pub fn with_preset(mut self, preset: EncodingPreset) -> Self {
self.preset = Some(preset);
self
}
pub fn add_filter(mut self, filter: FfmpegFilter) -> Self {
tracing::debug!(filter = ?filter, "✂️ Adding FFmpeg filter to post-processing config");
self.filters.push(filter);
self
}
pub fn is_empty(&self) -> bool {
self.video_codec.is_none()
&& self.audio_codec.is_none()
&& self.video_bitrate.is_none()
&& self.audio_bitrate.is_none()
&& self.resolution.is_none()
&& self.framerate.is_none()
&& self.preset.is_none()
&& self.filters.is_empty()
}
}
impl Default for PostProcessConfig {
fn default() -> Self {
Self::new()
}
}
impl fmt::Display for PostProcessConfig {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let video = self.video_codec.as_ref().map_or("None".to_string(), |c| c.to_string());
let audio = self.audio_codec.as_ref().map_or("None".to_string(), |c| c.to_string());
write!(
f,
"PostProcessConfig(video_codec={}, audio_codec={}, filters={})",
video,
audio,
self.filters.len()
)
}
}