#[derive(Debug, Clone)]
pub struct BitrateEstimator {
base_bpp: f64,
decay: f64,
}
impl Default for BitrateEstimator {
fn default() -> Self {
Self::new()
}
}
impl BitrateEstimator {
#[must_use]
pub fn new() -> Self {
Self {
base_bpp: 0.10, decay: 0.065, }
}
#[must_use]
pub fn with_params(base_bpp: f64, decay: f64) -> Self {
Self { base_bpp, decay }
}
#[must_use]
pub fn estimate_from_crf(&self, crf: u8, width: u32, height: u32, frame_rate: f64) -> u64 {
self.estimate_from_qp(f64::from(crf), width, height, frame_rate)
}
#[must_use]
pub fn estimate_from_qp(&self, qp: f64, width: u32, height: u32, frame_rate: f64) -> u64 {
if frame_rate <= 0.0 || width == 0 || height == 0 {
return 0;
}
let pixels = f64::from(width) * f64::from(height);
let quality_factor = (-self.decay * qp).exp();
let bps = self.base_bpp * pixels * frame_rate * quality_factor;
bps.round() as u64
}
#[must_use]
pub fn estimate_from_vmaf(&self, vmaf: f64, width: u32, height: u32, frame_rate: f64) -> u64 {
let vmaf_clamped = vmaf.clamp(0.0, 100.0);
let qp = 51.0 * (1.0 - vmaf_clamped / 100.0);
self.estimate_from_qp(qp, width, height, frame_rate)
}
#[must_use]
pub fn crf_for_target_bitrate(
&self,
target_bps: u64,
width: u32,
height: u32,
frame_rate: f64,
) -> Option<u8> {
if frame_rate <= 0.0 || width == 0 || height == 0 || target_bps == 0 {
return None;
}
let pixels = f64::from(width) * f64::from(height);
let denominator = self.base_bpp * pixels * frame_rate;
if denominator <= 0.0 {
return None;
}
let qp = -(target_bps as f64 / denominator).ln() / self.decay;
if !(0.0..=63.0).contains(&qp) {
return None;
}
Some(qp.round() as u8)
}
#[must_use]
pub fn estimate_file_size(
&self,
crf: u8,
width: u32,
height: u32,
frame_rate: f64,
duration_secs: f64,
) -> u64 {
let bps = self.estimate_from_crf(crf, width, height, frame_rate);
((bps as f64 * duration_secs) / 8.0).round() as u64
}
}
#[derive(Debug, Clone, Copy)]
pub struct VideoParams {
pub width: u32,
pub height: u32,
pub frame_rate: f64,
pub crf: u8,
}
impl VideoParams {
#[must_use]
pub fn new(width: u32, height: u32, frame_rate: f64, crf: u8) -> Self {
Self {
width,
height,
frame_rate,
crf,
}
}
#[must_use]
pub fn estimate_bitrate(&self, estimator: &BitrateEstimator) -> u64 {
estimator.estimate_from_crf(self.crf, self.width, self.height, self.frame_rate)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_estimate_from_crf_positive() {
let est = BitrateEstimator::new();
let bps = est.estimate_from_crf(23, 1920, 1080, 30.0);
assert!(bps > 0, "Expected positive bitrate, got {bps}");
}
#[test]
fn test_lower_crf_higher_bitrate() {
let est = BitrateEstimator::new();
let high_quality = est.estimate_from_crf(18, 1920, 1080, 30.0);
let low_quality = est.estimate_from_crf(28, 1920, 1080, 30.0);
assert!(
high_quality > low_quality,
"CRF 18 should yield more bits than CRF 28"
);
}
#[test]
fn test_higher_resolution_higher_bitrate() {
let est = BitrateEstimator::new();
let fhd = est.estimate_from_crf(23, 1920, 1080, 30.0);
let uhd = est.estimate_from_crf(23, 3840, 2160, 30.0);
assert!(uhd > fhd, "4K should require more bits than 1080p");
}
#[test]
fn test_higher_fps_higher_bitrate() {
let est = BitrateEstimator::new();
let fps30 = est.estimate_from_crf(23, 1920, 1080, 30.0);
let fps60 = est.estimate_from_crf(23, 1920, 1080, 60.0);
assert!(fps60 > fps30, "60 fps should require more bits than 30 fps");
assert!(
(fps60 as f64 / fps30 as f64 - 2.0).abs() < 0.01,
"Should scale linearly with fps"
);
}
#[test]
fn test_zero_dimensions_returns_zero() {
let est = BitrateEstimator::new();
assert_eq!(est.estimate_from_crf(23, 0, 1080, 30.0), 0);
assert_eq!(est.estimate_from_crf(23, 1920, 0, 30.0), 0);
assert_eq!(est.estimate_from_crf(23, 1920, 1080, 0.0), 0);
}
#[test]
fn test_vmaf_estimate_high_quality() {
let est = BitrateEstimator::new();
let high = est.estimate_from_vmaf(95.0, 1920, 1080, 30.0);
let low = est.estimate_from_vmaf(50.0, 1920, 1080, 30.0);
assert!(high > low, "VMAF 95 should need more bits than VMAF 50");
}
#[test]
fn test_crf_for_target_bitrate_roundtrip() {
let est = BitrateEstimator::new();
let target_crf: u8 = 23;
let bps = est.estimate_from_crf(target_crf, 1920, 1080, 30.0);
if let Some(inferred_crf) = est.crf_for_target_bitrate(bps, 1920, 1080, 30.0) {
assert!(
(inferred_crf as i16 - target_crf as i16).abs() <= 1,
"Expected CRF ~{target_crf}, got {inferred_crf}"
);
}
}
#[test]
fn test_estimate_file_size() {
let est = BitrateEstimator::new();
let bytes = est.estimate_file_size(23, 1920, 1080, 30.0, 60.0); assert!(bytes > 0);
let bps = est.estimate_from_crf(23, 1920, 1080, 30.0);
let expected = (bps as f64 * 60.0 / 8.0).round() as u64;
assert_eq!(bytes, expected);
}
#[test]
fn test_video_params_estimate_bitrate() {
let params = VideoParams::new(1920, 1080, 30.0, 23);
let est = BitrateEstimator::new();
let bps = params.estimate_bitrate(&est);
assert_eq!(bps, est.estimate_from_crf(23, 1920, 1080, 30.0));
}
#[test]
fn test_custom_params() {
let est = BitrateEstimator::with_params(0.2, 0.05);
let bps = est.estimate_from_crf(20, 1280, 720, 25.0);
assert!(bps > 0);
}
}