1#[derive(Debug, Clone)]
8pub struct QualityTarget {
9 pub min_psnr_db: f32,
11 pub min_ssim: f32,
13 pub max_bitrate_kbps: u32,
15}
16
17impl QualityTarget {
18 #[must_use]
20 pub fn new(min_psnr_db: f32, min_ssim: f32, max_bitrate_kbps: u32) -> Self {
21 Self {
22 min_psnr_db,
23 min_ssim,
24 max_bitrate_kbps,
25 }
26 }
27}
28
29#[derive(Debug, Clone, Copy, PartialEq, Eq)]
31pub struct CrfRange {
32 pub min_crf: u8,
34 pub max_crf: u8,
36}
37
38impl CrfRange {
39 #[must_use]
41 pub fn new(min_crf: u8, max_crf: u8) -> Self {
42 Self { min_crf, max_crf }
43 }
44
45 #[must_use]
47 pub fn h264_range() -> Self {
48 Self {
49 min_crf: 17,
50 max_crf: 51,
51 }
52 }
53
54 #[must_use]
56 pub fn av1_range() -> Self {
57 Self {
58 min_crf: 0,
59 max_crf: 63,
60 }
61 }
62
63 #[must_use]
65 pub fn midpoint(&self) -> u8 {
66 self.min_crf + (self.max_crf - self.min_crf) / 2
67 }
68
69 #[must_use]
71 pub fn span(&self) -> u8 {
72 self.max_crf - self.min_crf
73 }
74}
75
76#[derive(Debug, Clone)]
78pub struct CrfOptimizerResult {
79 pub optimal_crf: u8,
81 pub estimated_bitrate_kbps: u32,
83 pub estimated_psnr: f32,
85}
86
87#[derive(Debug, Clone, Default)]
89pub struct CrfOptimizer;
90
91impl CrfOptimizer {
92 #[must_use]
94 pub fn new() -> Self {
95 Self
96 }
97
98 #[must_use]
104 pub fn find_optimal(
105 target: &QualityTarget,
106 crf_range: CrfRange,
107 content_complexity: f32,
108 ) -> CrfOptimizerResult {
109 let mut lo = crf_range.min_crf;
114 let mut hi = crf_range.max_crf;
115 let mut best_crf = crf_range.max_crf;
116
117 while lo <= hi {
119 let mid = lo + (hi - lo) / 2;
120 let bitrate = BitrateModel::predict(mid, content_complexity, "h264");
121 if bitrate <= target.max_bitrate_kbps {
122 best_crf = mid;
123 if mid == 0 {
125 break;
126 }
127 hi = mid.saturating_sub(1);
128 } else {
129 lo = mid.saturating_add(1);
130 if lo > hi {
131 break;
132 }
133 }
134 }
135
136 let estimated_bitrate_kbps = BitrateModel::predict(best_crf, content_complexity, "h264");
137 let estimated_psnr = Self::estimate_psnr(best_crf, content_complexity);
138
139 CrfOptimizerResult {
140 optimal_crf: best_crf,
141 estimated_bitrate_kbps,
142 estimated_psnr,
143 }
144 }
145
146 #[must_use]
150 pub fn estimate_psnr(crf: u8, _complexity: f32) -> f32 {
151 50.0 - (f32::from(crf) * 20.0 / 51.0)
152 }
153}
154
155#[derive(Debug, Clone, Default)]
157pub struct BitrateModel;
158
159impl BitrateModel {
160 #[must_use]
171 pub fn predict(crf: u8, complexity: f32, codec: &str) -> u32 {
172 let base_kbps = match codec {
173 "h264" | "libx264" => 2000.0_f32,
174 "vp9" | "libvpx-vp9" => 1500.0,
175 "av1" | "libaom-av1" | "libsvtav1" => 1200.0,
176 "hevc" | "h265" | "libx265" => 1400.0,
177 _ => 2000.0,
178 };
179
180 let exponent = (28.0 - f32::from(crf)) / 6.0;
182 let scale = 2.0_f32.powf(exponent);
183 let bitrate = complexity * base_kbps * scale;
184
185 bitrate.clamp(10.0, 100_000.0).round() as u32
187 }
188
189 #[must_use]
191 pub fn crf_for_bitrate(target_kbps: u32, complexity: f32, codec: &str) -> u8 {
192 let base_kbps = match codec {
193 "h264" | "libx264" => 2000.0_f32,
194 "vp9" | "libvpx-vp9" => 1500.0,
195 "av1" | "libaom-av1" | "libsvtav1" => 1200.0,
196 "hevc" | "h265" | "libx265" => 1400.0,
197 _ => 2000.0,
198 };
199
200 if complexity <= 0.0 || base_kbps <= 0.0 {
201 return 28;
202 }
203
204 let ratio = target_kbps as f32 / (complexity * base_kbps);
208 if ratio <= 0.0 {
209 return 51;
210 }
211 let crf_f = 28.0 - 6.0 * ratio.log2();
212 (crf_f.round() as i32).clamp(0, 63) as u8
213 }
214}
215
216#[cfg(test)]
217mod tests {
218 use super::*;
219
220 #[test]
221 fn test_quality_target_new() {
222 let t = QualityTarget::new(35.0, 0.95, 4000);
223 assert_eq!(t.min_psnr_db, 35.0);
224 assert_eq!(t.min_ssim, 0.95);
225 assert_eq!(t.max_bitrate_kbps, 4000);
226 }
227
228 #[test]
229 fn test_crf_range_h264() {
230 let r = CrfRange::h264_range();
231 assert_eq!(r.min_crf, 17);
232 assert_eq!(r.max_crf, 51);
233 }
234
235 #[test]
236 fn test_crf_range_av1() {
237 let r = CrfRange::av1_range();
238 assert_eq!(r.min_crf, 0);
239 assert_eq!(r.max_crf, 63);
240 }
241
242 #[test]
243 fn test_crf_range_midpoint() {
244 let r = CrfRange::new(0, 63);
245 assert_eq!(r.midpoint(), 31);
246 }
247
248 #[test]
249 fn test_crf_range_span() {
250 let r = CrfRange::h264_range();
251 assert_eq!(r.span(), 34);
252 }
253
254 #[test]
255 fn test_bitrate_model_predict_h264() {
256 let b = BitrateModel::predict(28, 1.0, "h264");
258 assert_eq!(b, 2000);
259 }
260
261 #[test]
262 fn test_bitrate_model_predict_higher_crf_lower_bitrate() {
263 let b_low = BitrateModel::predict(20, 1.0, "h264");
264 let b_high = BitrateModel::predict(35, 1.0, "h264");
265 assert!(b_low > b_high, "Lower CRF should produce higher bitrate");
266 }
267
268 #[test]
269 fn test_bitrate_model_predict_av1_lower_than_h264() {
270 let h264 = BitrateModel::predict(28, 1.0, "h264");
271 let av1 = BitrateModel::predict(28, 1.0, "av1");
272 assert!(av1 < h264, "AV1 should have lower base bitrate");
273 }
274
275 #[test]
276 fn test_bitrate_model_complexity_scaling() {
277 let b1 = BitrateModel::predict(28, 1.0, "h264");
278 let b2 = BitrateModel::predict(28, 2.0, "h264");
279 assert_eq!(b2, b1 * 2);
280 }
281
282 #[test]
283 fn test_crf_optimizer_finds_within_budget() {
284 let target = QualityTarget::new(30.0, 0.9, 5000);
285 let crf_range = CrfRange::h264_range();
286 let result = CrfOptimizer::find_optimal(&target, crf_range, 1.0);
287 assert!(
288 result.estimated_bitrate_kbps <= 5000,
289 "Bitrate {} should be <= 5000",
290 result.estimated_bitrate_kbps
291 );
292 assert!(result.optimal_crf >= crf_range.min_crf);
293 assert!(result.optimal_crf <= crf_range.max_crf);
294 }
295
296 #[test]
297 fn test_crf_optimizer_result_fields() {
298 let target = QualityTarget::new(30.0, 0.9, 4000);
299 let result = CrfOptimizer::find_optimal(&target, CrfRange::h264_range(), 1.0);
300 assert!(result.estimated_psnr > 0.0);
301 assert!(result.estimated_bitrate_kbps > 0);
302 }
303
304 #[test]
305 fn test_estimate_psnr_decreases_with_crf() {
306 let psnr_low = CrfOptimizer::estimate_psnr(17, 1.0);
307 let psnr_high = CrfOptimizer::estimate_psnr(51, 1.0);
308 assert!(psnr_low > psnr_high);
309 }
310}