mod presets;
mod validation;
use ff_format::{PixelFormat, VideoCodec};
use crate::video::codec_options::VideoCodecOptions;
use crate::{AudioCodec, BitrateMode, EncodeError, VideoEncoderBuilder};
#[derive(Debug, Clone)]
pub struct VideoEncoderConfig {
pub codec: VideoCodec,
pub width: Option<u32>,
pub height: Option<u32>,
pub fps: Option<f64>,
pub bitrate_mode: BitrateMode,
pub pixel_format: Option<PixelFormat>,
pub codec_options: Option<VideoCodecOptions>,
}
#[derive(Debug, Clone)]
pub struct AudioEncoderConfig {
pub codec: AudioCodec,
pub sample_rate: u32,
pub channels: u32,
pub bitrate: u64,
}
#[derive(Debug, Clone)]
pub struct ExportPreset {
pub name: String,
pub video: Option<VideoEncoderConfig>,
pub audio: AudioEncoderConfig,
}
impl ExportPreset {
#[must_use]
pub fn youtube_1080p() -> Self {
presets::youtube_1080p()
}
#[must_use]
pub fn youtube_4k() -> Self {
presets::youtube_4k()
}
#[must_use]
pub fn twitter() -> Self {
presets::twitter()
}
#[must_use]
pub fn instagram_square() -> Self {
presets::instagram_square()
}
#[must_use]
pub fn instagram_reels() -> Self {
presets::instagram_reels()
}
#[must_use]
pub fn bluray_1080p() -> Self {
presets::bluray_1080p()
}
#[must_use]
pub fn podcast_mono() -> Self {
presets::podcast_mono()
}
#[must_use]
pub fn lossless_rgb() -> Self {
presets::lossless_rgb()
}
#[must_use]
pub fn web_h264() -> Self {
presets::web_h264()
}
pub fn validate(&self) -> Result<(), EncodeError> {
validation::validate_preset(self)
}
#[must_use]
pub fn apply_video(&self, builder: VideoEncoderBuilder) -> VideoEncoderBuilder {
let Some(ref v) = self.video else {
return builder;
};
let mut b = builder
.video_codec(v.codec)
.bitrate_mode(v.bitrate_mode.clone());
if let (Some(w), Some(h), Some(fps)) = (v.width, v.height, v.fps) {
b = b.video(w, h, fps);
}
if let Some(pf) = v.pixel_format {
b = b.pixel_format(pf);
}
if let Some(opts) = v.codec_options.clone() {
b = b.codec_options(opts);
}
b
}
#[must_use]
pub fn apply_audio(&self, builder: VideoEncoderBuilder) -> VideoEncoderBuilder {
builder
.audio(self.audio.sample_rate, self.audio.channels)
.audio_codec(self.audio.codec)
.audio_bitrate(self.audio.bitrate)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn youtube_1080p_preset_should_have_correct_codec() {
let preset = ExportPreset::youtube_1080p();
let video = preset
.video
.as_ref()
.expect("youtube_1080p must have video");
assert_eq!(video.codec, VideoCodec::H264);
}
#[test]
fn youtube_1080p_preset_should_have_correct_resolution() {
let preset = ExportPreset::youtube_1080p();
let video = preset
.video
.as_ref()
.expect("youtube_1080p must have video");
assert_eq!(video.width, Some(1920));
assert_eq!(video.height, Some(1080));
}
#[test]
fn youtube_1080p_preset_audio_should_be_aac_192kbps() {
let preset = ExportPreset::youtube_1080p();
assert_eq!(preset.audio.codec, AudioCodec::Aac);
assert_eq!(preset.audio.bitrate, 192_000);
}
#[test]
fn youtube_4k_preset_should_have_h265_codec() {
let preset = ExportPreset::youtube_4k();
let video = preset.video.as_ref().expect("youtube_4k must have video");
assert_eq!(video.codec, VideoCodec::H265);
}
#[test]
fn youtube_4k_preset_should_have_correct_resolution() {
let preset = ExportPreset::youtube_4k();
let video = preset.video.as_ref().expect("youtube_4k must have video");
assert_eq!(video.width, Some(3840));
assert_eq!(video.height, Some(2160));
}
#[test]
fn youtube_4k_preset_audio_should_be_aac_256kbps() {
let preset = ExportPreset::youtube_4k();
assert_eq!(preset.audio.codec, AudioCodec::Aac);
assert_eq!(preset.audio.bitrate, 256_000);
}
#[test]
fn lossless_rgb_preset_should_have_ffv1_codec() {
let preset = ExportPreset::lossless_rgb();
let video = preset.video.as_ref().expect("lossless_rgb must have video");
assert_eq!(video.codec, VideoCodec::Ffv1);
}
#[test]
fn lossless_rgb_preset_should_preserve_source_resolution() {
let preset = ExportPreset::lossless_rgb();
let video = preset.video.as_ref().expect("lossless_rgb must have video");
assert!(video.width.is_none(), "lossless_rgb should not fix width");
assert!(video.height.is_none(), "lossless_rgb should not fix height");
assert!(video.fps.is_none(), "lossless_rgb should not fix fps");
}
#[test]
fn podcast_mono_preset_should_have_no_video() {
let preset = ExportPreset::podcast_mono();
assert!(
preset.video.is_none(),
"podcast_mono must be audio-only (video=None)"
);
}
#[test]
fn podcast_mono_preset_should_have_mono_audio() {
let preset = ExportPreset::podcast_mono();
assert_eq!(preset.audio.channels, 1);
}
#[test]
fn web_h264_preset_should_use_vp9_codec() {
let preset = ExportPreset::web_h264();
let video = preset.video.as_ref().expect("web_h264 must have video");
assert_eq!(video.codec, VideoCodec::Vp9);
}
#[test]
fn all_presets_should_have_non_empty_names() {
let presets = [
ExportPreset::youtube_1080p(),
ExportPreset::youtube_4k(),
ExportPreset::twitter(),
ExportPreset::instagram_square(),
ExportPreset::instagram_reels(),
ExportPreset::bluray_1080p(),
ExportPreset::podcast_mono(),
ExportPreset::lossless_rgb(),
ExportPreset::web_h264(),
];
for preset in &presets {
assert!(!preset.name.is_empty(), "preset name must not be empty");
}
}
#[test]
fn custom_preset_should_apply_codec_to_builder() {
use crate::VideoEncoder;
let preset = ExportPreset {
name: "custom".to_string(),
video: Some(VideoEncoderConfig {
codec: VideoCodec::H264,
width: Some(1280),
height: Some(720),
fps: Some(24.0),
bitrate_mode: BitrateMode::Crf(23),
pixel_format: None,
codec_options: None,
}),
audio: AudioEncoderConfig {
codec: AudioCodec::Aac,
sample_rate: 44100,
channels: 2,
bitrate: 128_000,
},
};
let builder = preset.apply_video(VideoEncoder::create("out.mp4"));
assert_eq!(builder.video_codec, VideoCodec::H264);
assert_eq!(builder.video_width, Some(1280));
assert_eq!(builder.video_height, Some(720));
}
#[test]
fn preset_with_no_resolution_should_not_override_builder_resolution() {
use crate::VideoEncoder;
let preset = ExportPreset {
name: "no-res".to_string(),
video: Some(VideoEncoderConfig {
codec: VideoCodec::Ffv1,
width: None,
height: None,
fps: None,
bitrate_mode: BitrateMode::Crf(0),
pixel_format: None,
codec_options: None,
}),
audio: AudioEncoderConfig {
codec: AudioCodec::Flac,
sample_rate: 48000,
channels: 2,
bitrate: 0,
},
};
let builder = preset.apply_video(VideoEncoder::create("out.mkv"));
assert!(
builder.video_width.is_none(),
"apply_video should not set width when VideoEncoderConfig.width is None"
);
assert!(
builder.video_height.is_none(),
"apply_video should not set height when VideoEncoderConfig.height is None"
);
}
#[test]
fn audio_only_preset_apply_video_should_return_builder_unchanged() {
use crate::VideoEncoder;
let preset = ExportPreset::podcast_mono();
let builder_before = VideoEncoder::create("out.m4a");
let builder_after = preset.apply_video(VideoEncoder::create("out.m4a"));
assert_eq!(
builder_before.video_codec, builder_after.video_codec,
"apply_video on audio-only preset must leave builder unchanged"
);
}
#[test]
fn apply_audio_should_set_sample_rate_and_bitrate() {
use crate::VideoEncoder;
let preset = ExportPreset::youtube_1080p();
let builder = preset.apply_audio(VideoEncoder::create("out.mp4"));
assert_eq!(builder.audio_sample_rate, Some(48000));
assert_eq!(builder.audio_bitrate, Some(192_000));
}
#[test]
fn youtube_1080p_preset_should_pass_validation() {
assert!(ExportPreset::youtube_1080p().validate().is_ok());
}
#[test]
fn youtube_4k_preset_should_pass_validation() {
assert!(ExportPreset::youtube_4k().validate().is_ok());
}
#[test]
fn instagram_square_preset_should_pass_validation() {
assert!(ExportPreset::instagram_square().validate().is_ok());
}
#[test]
fn instagram_reels_preset_should_pass_validation() {
assert!(ExportPreset::instagram_reels().validate().is_ok());
}
}