1use crate::running_stats::BitrateRunningAnalyzer;
9
10#[derive(Debug)]
37pub struct BitrateEstimator {
38 base_bpp: f64,
40 decay: f64,
42 analyzer: BitrateRunningAnalyzer,
44}
45
46impl Default for BitrateEstimator {
47 fn default() -> Self {
48 Self::new()
49 }
50}
51
52impl BitrateEstimator {
53 #[must_use]
59 pub fn new() -> Self {
60 Self {
61 base_bpp: 0.10, decay: 0.065, analyzer: BitrateRunningAnalyzer::new(30.0, 60),
64 }
65 }
66
67 #[must_use]
72 pub fn with_params(base_bpp: f64, decay: f64) -> Self {
73 Self {
74 base_bpp,
75 decay,
76 analyzer: BitrateRunningAnalyzer::new(30.0, 60),
77 }
78 }
79
80 #[must_use]
85 pub fn with_params_and_fps(base_bpp: f64, decay: f64, fps: f64, window_frames: usize) -> Self {
86 Self {
87 base_bpp,
88 decay,
89 analyzer: BitrateRunningAnalyzer::new(fps, window_frames),
90 }
91 }
92
93 pub fn record_frame_bits(&mut self, bits_per_frame: u64) {
98 self.analyzer.push_frame(bits_per_frame);
99 }
100
101 #[must_use]
105 pub fn running_summary(&self) -> crate::running_stats::BitrateSummary {
106 self.analyzer.summary()
107 }
108
109 pub fn reset_running_stats(&mut self) {
111 self.analyzer.reset();
112 }
113
114 #[must_use]
121 pub fn estimate_from_crf(&self, crf: u8, width: u32, height: u32, frame_rate: f64) -> u64 {
122 self.estimate_from_qp(f64::from(crf), width, height, frame_rate)
123 }
124
125 #[must_use]
132 pub fn estimate_from_qp(&self, qp: f64, width: u32, height: u32, frame_rate: f64) -> u64 {
133 if frame_rate <= 0.0 || width == 0 || height == 0 {
134 return 0;
135 }
136 let pixels = f64::from(width) * f64::from(height);
137 let quality_factor = (-self.decay * qp).exp();
138 let bps = self.base_bpp * pixels * frame_rate * quality_factor;
139 bps.round() as u64
140 }
141
142 #[must_use]
147 pub fn estimate_from_vmaf(&self, vmaf: f64, width: u32, height: u32, frame_rate: f64) -> u64 {
148 let vmaf_clamped = vmaf.clamp(0.0, 100.0);
149 let qp = 51.0 * (1.0 - vmaf_clamped / 100.0);
150 self.estimate_from_qp(qp, width, height, frame_rate)
151 }
152
153 #[must_use]
157 pub fn crf_for_target_bitrate(
158 &self,
159 target_bps: u64,
160 width: u32,
161 height: u32,
162 frame_rate: f64,
163 ) -> Option<u8> {
164 if frame_rate <= 0.0 || width == 0 || height == 0 || target_bps == 0 {
165 return None;
166 }
167 let pixels = f64::from(width) * f64::from(height);
168 let denominator = self.base_bpp * pixels * frame_rate;
171 if denominator <= 0.0 {
172 return None;
173 }
174 let qp = -(target_bps as f64 / denominator).ln() / self.decay;
175 if !(0.0..=63.0).contains(&qp) {
176 return None;
177 }
178 Some(qp.round() as u8)
179 }
180
181 #[must_use]
183 pub fn estimate_file_size(
184 &self,
185 crf: u8,
186 width: u32,
187 height: u32,
188 frame_rate: f64,
189 duration_secs: f64,
190 ) -> u64 {
191 let bps = self.estimate_from_crf(crf, width, height, frame_rate);
192 ((bps as f64 * duration_secs) / 8.0).round() as u64
193 }
194}
195
196#[derive(Debug, Clone, Copy)]
198pub struct VideoParams {
199 pub width: u32,
201 pub height: u32,
203 pub frame_rate: f64,
205 pub crf: u8,
207}
208
209impl VideoParams {
210 #[must_use]
212 pub fn new(width: u32, height: u32, frame_rate: f64, crf: u8) -> Self {
213 Self {
214 width,
215 height,
216 frame_rate,
217 crf,
218 }
219 }
220
221 #[must_use]
223 pub fn estimate_bitrate(&self, estimator: &BitrateEstimator) -> u64 {
224 estimator.estimate_from_crf(self.crf, self.width, self.height, self.frame_rate)
225 }
226}
227
228#[cfg(test)]
229mod tests {
230 use super::*;
231
232 #[test]
233 fn test_estimate_from_crf_positive() {
234 let est = BitrateEstimator::new();
235 let bps = est.estimate_from_crf(23, 1920, 1080, 30.0);
236 assert!(bps > 0, "Expected positive bitrate, got {bps}");
237 }
238
239 #[test]
240 fn test_lower_crf_higher_bitrate() {
241 let est = BitrateEstimator::new();
242 let high_quality = est.estimate_from_crf(18, 1920, 1080, 30.0);
243 let low_quality = est.estimate_from_crf(28, 1920, 1080, 30.0);
244 assert!(
245 high_quality > low_quality,
246 "CRF 18 should yield more bits than CRF 28"
247 );
248 }
249
250 #[test]
251 fn test_higher_resolution_higher_bitrate() {
252 let est = BitrateEstimator::new();
253 let fhd = est.estimate_from_crf(23, 1920, 1080, 30.0);
254 let uhd = est.estimate_from_crf(23, 3840, 2160, 30.0);
255 assert!(uhd > fhd, "4K should require more bits than 1080p");
256 }
257
258 #[test]
259 fn test_higher_fps_higher_bitrate() {
260 let est = BitrateEstimator::new();
261 let fps30 = est.estimate_from_crf(23, 1920, 1080, 30.0);
262 let fps60 = est.estimate_from_crf(23, 1920, 1080, 60.0);
263 assert!(fps60 > fps30, "60 fps should require more bits than 30 fps");
264 assert!(
265 (fps60 as f64 / fps30 as f64 - 2.0).abs() < 0.01,
266 "Should scale linearly with fps"
267 );
268 }
269
270 #[test]
271 fn test_zero_dimensions_returns_zero() {
272 let est = BitrateEstimator::new();
273 assert_eq!(est.estimate_from_crf(23, 0, 1080, 30.0), 0);
274 assert_eq!(est.estimate_from_crf(23, 1920, 0, 30.0), 0);
275 assert_eq!(est.estimate_from_crf(23, 1920, 1080, 0.0), 0);
276 }
277
278 #[test]
279 fn test_vmaf_estimate_high_quality() {
280 let est = BitrateEstimator::new();
281 let high = est.estimate_from_vmaf(95.0, 1920, 1080, 30.0);
282 let low = est.estimate_from_vmaf(50.0, 1920, 1080, 30.0);
283 assert!(high > low, "VMAF 95 should need more bits than VMAF 50");
284 }
285
286 #[test]
287 fn test_crf_for_target_bitrate_roundtrip() {
288 let est = BitrateEstimator::new();
289 let target_crf: u8 = 23;
290 let bps = est.estimate_from_crf(target_crf, 1920, 1080, 30.0);
291 if let Some(inferred_crf) = est.crf_for_target_bitrate(bps, 1920, 1080, 30.0) {
292 assert!(
294 (inferred_crf as i16 - target_crf as i16).abs() <= 1,
295 "Expected CRF ~{target_crf}, got {inferred_crf}"
296 );
297 }
298 }
299
300 #[test]
301 fn test_estimate_file_size() {
302 let est = BitrateEstimator::new();
303 let bytes = est.estimate_file_size(23, 1920, 1080, 30.0, 60.0); assert!(bytes > 0);
305 let bps = est.estimate_from_crf(23, 1920, 1080, 30.0);
307 let expected = (bps as f64 * 60.0 / 8.0).round() as u64;
308 assert_eq!(bytes, expected);
309 }
310
311 #[test]
312 fn test_video_params_estimate_bitrate() {
313 let params = VideoParams::new(1920, 1080, 30.0, 23);
314 let est = BitrateEstimator::new();
315 let bps = params.estimate_bitrate(&est);
316 assert_eq!(bps, est.estimate_from_crf(23, 1920, 1080, 30.0));
317 }
318
319 #[test]
320 fn test_custom_params() {
321 let est = BitrateEstimator::with_params(0.2, 0.05);
322 let bps = est.estimate_from_crf(20, 1280, 720, 25.0);
323 assert!(bps > 0);
324 }
325
326 #[test]
331 fn test_running_stats_matches_batch_computation() {
332 use crate::running_stats::RunningStats;
333
334 let samples = [
336 10_000.0_f64,
337 12_500.0,
338 8_700.0,
339 15_000.0,
340 9_800.0,
341 11_300.0,
342 13_600.0,
343 7_900.0,
344 14_200.0,
345 10_500.0,
346 ];
347
348 let mut stats = RunningStats::new();
350 for &s in &samples {
351 stats.push(s);
352 }
353
354 let n = samples.len() as f64;
356 let batch_mean = samples.iter().sum::<f64>() / n;
357 let batch_var = samples
358 .iter()
359 .map(|&x| (x - batch_mean).powi(2))
360 .sum::<f64>()
361 / (n - 1.0); let tol = 1e-6;
364 assert!(
365 (stats.mean() - batch_mean).abs() < tol,
366 "mean mismatch: welford={}, batch={}",
367 stats.mean(),
368 batch_mean
369 );
370 assert!(
371 (stats.variance() - batch_var).abs() < tol,
372 "variance mismatch: welford={}, batch={}",
373 stats.variance(),
374 batch_var
375 );
376 }
377
378 #[test]
381 fn test_running_stats_incremental_update() {
382 use crate::running_stats::RunningStats;
383
384 let all_samples = [1.0_f64, 4.0, 9.0, 16.0, 25.0, 36.0];
385
386 let mut batch = RunningStats::new();
388 for &s in &all_samples {
389 batch.push(s);
390 }
391
392 let mut incremental = RunningStats::new();
394 let (first_half, second_half) = all_samples.split_at(3);
395 for &s in first_half {
396 incremental.push(s);
397 }
398 let expected_mid_mean = first_half.iter().sum::<f64>() / first_half.len() as f64;
400 assert!(
401 (incremental.mean() - expected_mid_mean).abs() < 1e-10,
402 "mid-point mean mismatch: got {}, expected {}",
403 incremental.mean(),
404 expected_mid_mean
405 );
406
407 for &s in second_half {
408 incremental.push(s);
409 }
410
411 let tol = 1e-10;
413 assert_eq!(incremental.count(), batch.count());
414 assert!(
415 (incremental.mean() - batch.mean()).abs() < tol,
416 "final mean mismatch"
417 );
418 assert!(
419 (incremental.variance() - batch.variance()).abs() < tol,
420 "final variance mismatch"
421 );
422 assert_eq!(incremental.min(), batch.min());
423 assert_eq!(incremental.max(), batch.max());
424 }
425}