1#![allow(dead_code)]
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
10pub enum OptimizationGoal {
11 MinimizeSize,
13 MaximizeQuality,
15 TargetBitrate,
17 Balanced,
19 RealTime,
21}
22
23impl OptimizationGoal {
24 #[must_use]
26 pub fn description(&self) -> &'static str {
27 match self {
28 Self::MinimizeSize => "Minimize output file size",
29 Self::MaximizeQuality => "Maximize perceptual quality",
30 Self::TargetBitrate => "Hit a specific target bitrate",
31 Self::Balanced => "Balance quality and file size",
32 Self::RealTime => "Optimize for real-time encoding speed",
33 }
34 }
35
36 #[must_use]
38 pub fn default_crf(&self) -> u8 {
39 match self {
40 Self::MinimizeSize => 30,
41 Self::MaximizeQuality => 16,
42 Self::TargetBitrate => 23,
43 Self::Balanced => 23,
44 Self::RealTime => 28,
45 }
46 }
47}
48
49impl Default for OptimizationGoal {
50 fn default() -> Self {
51 Self::Balanced
52 }
53}
54
55#[derive(Debug, Clone)]
57pub struct TranscodeConfig {
58 pub width: u32,
60 pub height: u32,
62 pub fps: f64,
64 pub duration_secs: f64,
66 pub crf: u8,
68 pub audio_kbps: u32,
70 pub target_bitrate_kbps: Option<f64>,
72}
73
74impl TranscodeConfig {
75 #[must_use]
77 pub fn new(width: u32, height: u32, fps: f64, duration_secs: f64, crf: u8) -> Self {
78 Self {
79 width,
80 height,
81 fps,
82 duration_secs,
83 crf,
84 audio_kbps: 192,
85 target_bitrate_kbps: None,
86 }
87 }
88
89 #[allow(clippy::cast_precision_loss)]
93 #[must_use]
94 pub fn estimated_size_mb(&self) -> f64 {
95 let pixels_per_sec = self.width as f64 * self.height as f64 * self.fps;
96 let crf_scale = (-f64::from(self.crf) / 6.0_f64).exp2();
97 let video_kbps = (pixels_per_sec / 500.0) * crf_scale;
98 let total_kbps = video_kbps + f64::from(self.audio_kbps);
99 total_kbps * self.duration_secs / 8.0 / 1024.0
101 }
102
103 #[must_use]
105 pub fn pixel_count(&self) -> u64 {
106 u64::from(self.width) * u64::from(self.height)
107 }
108
109 #[must_use]
111 pub fn is_4k_or_above(&self) -> bool {
112 self.width >= 3840 || self.height >= 2160
113 }
114}
115
116impl Default for TranscodeConfig {
117 fn default() -> Self {
118 Self::new(1920, 1080, 25.0, 60.0, 23)
119 }
120}
121
122#[derive(Debug)]
125pub struct TranscodeOptimizer {
126 goal: OptimizationGoal,
127 max_size_mb: Option<f64>,
129 target_kbps: Option<f64>,
131}
132
133impl TranscodeOptimizer {
134 #[must_use]
136 pub fn new(goal: OptimizationGoal) -> Self {
137 Self {
138 goal,
139 max_size_mb: None,
140 target_kbps: None,
141 }
142 }
143
144 #[must_use]
146 pub fn with_max_size_mb(mut self, mb: f64) -> Self {
147 self.max_size_mb = Some(mb);
148 self
149 }
150
151 #[must_use]
153 pub fn with_target_kbps(mut self, kbps: f64) -> Self {
154 self.target_kbps = Some(kbps);
155 self
156 }
157
158 #[must_use]
160 pub fn goal(&self) -> OptimizationGoal {
161 self.goal
162 }
163
164 #[allow(clippy::cast_precision_loss)]
167 #[must_use]
168 pub fn optimize_for_goal(&self, config: &TranscodeConfig) -> TranscodeConfig {
169 let mut out = config.clone();
170 match self.goal {
171 OptimizationGoal::MinimizeSize => {
172 out.crf = 30; out.audio_kbps = 128;
174 }
175 OptimizationGoal::MaximizeQuality => {
176 out.crf = 16; out.audio_kbps = 320;
178 }
179 OptimizationGoal::TargetBitrate => {
180 if let Some(target) = self.target_kbps {
181 out.crf = self.suggest_crf(config, target);
182 out.target_bitrate_kbps = Some(target);
183 }
184 }
185 OptimizationGoal::Balanced => {
186 out.crf = 23;
187 out.audio_kbps = 192;
188 }
189 OptimizationGoal::RealTime => {
190 out.crf = 28;
191 out.audio_kbps = 128;
192 }
193 }
194 out
195 }
196
197 #[allow(clippy::cast_possible_truncation)]
201 #[allow(clippy::cast_sign_loss)]
202 #[allow(clippy::cast_precision_loss)]
203 #[must_use]
204 pub fn suggest_crf(&self, config: &TranscodeConfig, target_kbps: f64) -> u8 {
205 let duration = config.duration_secs;
206 if duration <= 0.0 {
207 return 23;
208 }
209 let mut best_crf: u8 = 23;
210 let mut best_delta = f64::MAX;
211 for crf in 0u8..=51u8 {
212 let probe = TranscodeConfig {
213 crf,
214 ..config.clone()
215 };
216 let size_mb = probe.estimated_size_mb();
217 let kbps = size_mb * 8.0 * 1024.0 / duration;
218 let delta = (kbps - target_kbps).abs();
219 if delta < best_delta {
220 best_delta = delta;
221 best_crf = crf;
222 }
223 }
224 best_crf
225 }
226
227 #[must_use]
230 pub fn estimated_output_size_mb(&self, config: &TranscodeConfig) -> f64 {
231 self.optimize_for_goal(config).estimated_size_mb()
232 }
233}
234
235#[cfg(test)]
236mod tests {
237 use super::*;
238
239 #[test]
240 fn test_goal_description_non_empty() {
241 for g in [
242 OptimizationGoal::MinimizeSize,
243 OptimizationGoal::MaximizeQuality,
244 OptimizationGoal::TargetBitrate,
245 OptimizationGoal::Balanced,
246 OptimizationGoal::RealTime,
247 ] {
248 assert!(!g.description().is_empty());
249 }
250 }
251
252 #[test]
253 fn test_goal_default_crf_in_range() {
254 for g in [
255 OptimizationGoal::MinimizeSize,
256 OptimizationGoal::MaximizeQuality,
257 OptimizationGoal::Balanced,
258 OptimizationGoal::RealTime,
259 ] {
260 let crf = g.default_crf();
261 assert!(crf <= 51, "CRF {crf} out of range for {g:?}");
262 }
263 }
264
265 #[test]
266 fn test_transcode_config_estimated_size_positive() {
267 let cfg = TranscodeConfig::default();
268 assert!(cfg.estimated_size_mb() > 0.0);
269 }
270
271 #[test]
272 fn test_transcode_config_lower_crf_larger_size() {
273 let low = TranscodeConfig::new(1920, 1080, 25.0, 60.0, 16);
274 let high = TranscodeConfig::new(1920, 1080, 25.0, 60.0, 30);
275 assert!(low.estimated_size_mb() > high.estimated_size_mb());
276 }
277
278 #[test]
279 fn test_transcode_config_pixel_count() {
280 let cfg = TranscodeConfig::new(1920, 1080, 25.0, 60.0, 23);
281 assert_eq!(cfg.pixel_count(), 1920 * 1080);
282 }
283
284 #[test]
285 fn test_transcode_config_is_4k_above() {
286 let uhd = TranscodeConfig::new(3840, 2160, 24.0, 120.0, 23);
287 assert!(uhd.is_4k_or_above());
288 }
289
290 #[test]
291 fn test_transcode_config_not_4k() {
292 let hd = TranscodeConfig::new(1280, 720, 30.0, 60.0, 23);
293 assert!(!hd.is_4k_or_above());
294 }
295
296 #[test]
297 fn test_optimizer_goal_accessor() {
298 let opt = TranscodeOptimizer::new(OptimizationGoal::MinimizeSize);
299 assert_eq!(opt.goal(), OptimizationGoal::MinimizeSize);
300 }
301
302 #[test]
303 fn test_optimize_for_minimize_size_increases_crf() {
304 let cfg = TranscodeConfig::default(); let opt = TranscodeOptimizer::new(OptimizationGoal::MinimizeSize);
306 let out = opt.optimize_for_goal(&cfg);
307 assert!(out.crf > cfg.crf);
308 }
309
310 #[test]
311 fn test_optimize_for_max_quality_decreases_crf() {
312 let cfg = TranscodeConfig::default(); let opt = TranscodeOptimizer::new(OptimizationGoal::MaximizeQuality);
314 let out = opt.optimize_for_goal(&cfg);
315 assert!(out.crf < cfg.crf);
316 }
317
318 #[test]
319 fn test_suggest_crf_in_range() {
320 let cfg = TranscodeConfig::default();
321 let opt = TranscodeOptimizer::new(OptimizationGoal::TargetBitrate);
322 let crf = opt.suggest_crf(&cfg, 2000.0);
323 assert!(crf <= 51);
324 }
325
326 #[test]
327 fn test_suggest_crf_zero_duration_returns_default() {
328 let mut cfg = TranscodeConfig::default();
329 cfg.duration_secs = 0.0;
330 let opt = TranscodeOptimizer::new(OptimizationGoal::TargetBitrate);
331 assert_eq!(opt.suggest_crf(&cfg, 2000.0), 23);
332 }
333
334 #[test]
335 fn test_optimize_for_target_bitrate_sets_crf() {
336 let cfg = TranscodeConfig::default();
337 let opt = TranscodeOptimizer::new(OptimizationGoal::TargetBitrate).with_target_kbps(4000.0);
338 let out = opt.optimize_for_goal(&cfg);
339 assert!(out.crf <= 51);
340 }
341
342 #[test]
343 fn test_estimated_output_size_positive() {
344 let cfg = TranscodeConfig::default();
345 let opt = TranscodeOptimizer::new(OptimizationGoal::Balanced);
346 assert!(opt.estimated_output_size_mb(&cfg) > 0.0);
347 }
348}