1#[derive(Debug, Clone)]
20pub struct BitrateEstimator {
21 base_bpp: f64,
23 decay: f64,
25}
26
27impl Default for BitrateEstimator {
28 fn default() -> Self {
29 Self::new()
30 }
31}
32
33impl BitrateEstimator {
34 #[must_use]
36 pub fn new() -> Self {
37 Self {
38 base_bpp: 0.10, decay: 0.065, }
41 }
42
43 #[must_use]
48 pub fn with_params(base_bpp: f64, decay: f64) -> Self {
49 Self { base_bpp, decay }
50 }
51
52 #[must_use]
59 pub fn estimate_from_crf(&self, crf: u8, width: u32, height: u32, frame_rate: f64) -> u64 {
60 self.estimate_from_qp(f64::from(crf), width, height, frame_rate)
61 }
62
63 #[must_use]
70 pub fn estimate_from_qp(&self, qp: f64, width: u32, height: u32, frame_rate: f64) -> u64 {
71 if frame_rate <= 0.0 || width == 0 || height == 0 {
72 return 0;
73 }
74 let pixels = f64::from(width) * f64::from(height);
75 let quality_factor = (-self.decay * qp).exp();
76 let bps = self.base_bpp * pixels * frame_rate * quality_factor;
77 bps.round() as u64
78 }
79
80 #[must_use]
85 pub fn estimate_from_vmaf(&self, vmaf: f64, width: u32, height: u32, frame_rate: f64) -> u64 {
86 let vmaf_clamped = vmaf.clamp(0.0, 100.0);
87 let qp = 51.0 * (1.0 - vmaf_clamped / 100.0);
88 self.estimate_from_qp(qp, width, height, frame_rate)
89 }
90
91 #[must_use]
95 pub fn crf_for_target_bitrate(
96 &self,
97 target_bps: u64,
98 width: u32,
99 height: u32,
100 frame_rate: f64,
101 ) -> Option<u8> {
102 if frame_rate <= 0.0 || width == 0 || height == 0 || target_bps == 0 {
103 return None;
104 }
105 let pixels = f64::from(width) * f64::from(height);
106 let denominator = self.base_bpp * pixels * frame_rate;
109 if denominator <= 0.0 {
110 return None;
111 }
112 let qp = -(target_bps as f64 / denominator).ln() / self.decay;
113 if !(0.0..=63.0).contains(&qp) {
114 return None;
115 }
116 Some(qp.round() as u8)
117 }
118
119 #[must_use]
121 pub fn estimate_file_size(
122 &self,
123 crf: u8,
124 width: u32,
125 height: u32,
126 frame_rate: f64,
127 duration_secs: f64,
128 ) -> u64 {
129 let bps = self.estimate_from_crf(crf, width, height, frame_rate);
130 ((bps as f64 * duration_secs) / 8.0).round() as u64
131 }
132}
133
134#[derive(Debug, Clone, Copy)]
136pub struct VideoParams {
137 pub width: u32,
139 pub height: u32,
141 pub frame_rate: f64,
143 pub crf: u8,
145}
146
147impl VideoParams {
148 #[must_use]
150 pub fn new(width: u32, height: u32, frame_rate: f64, crf: u8) -> Self {
151 Self {
152 width,
153 height,
154 frame_rate,
155 crf,
156 }
157 }
158
159 #[must_use]
161 pub fn estimate_bitrate(&self, estimator: &BitrateEstimator) -> u64 {
162 estimator.estimate_from_crf(self.crf, self.width, self.height, self.frame_rate)
163 }
164}
165
166#[cfg(test)]
167mod tests {
168 use super::*;
169
170 #[test]
171 fn test_estimate_from_crf_positive() {
172 let est = BitrateEstimator::new();
173 let bps = est.estimate_from_crf(23, 1920, 1080, 30.0);
174 assert!(bps > 0, "Expected positive bitrate, got {bps}");
175 }
176
177 #[test]
178 fn test_lower_crf_higher_bitrate() {
179 let est = BitrateEstimator::new();
180 let high_quality = est.estimate_from_crf(18, 1920, 1080, 30.0);
181 let low_quality = est.estimate_from_crf(28, 1920, 1080, 30.0);
182 assert!(
183 high_quality > low_quality,
184 "CRF 18 should yield more bits than CRF 28"
185 );
186 }
187
188 #[test]
189 fn test_higher_resolution_higher_bitrate() {
190 let est = BitrateEstimator::new();
191 let fhd = est.estimate_from_crf(23, 1920, 1080, 30.0);
192 let uhd = est.estimate_from_crf(23, 3840, 2160, 30.0);
193 assert!(uhd > fhd, "4K should require more bits than 1080p");
194 }
195
196 #[test]
197 fn test_higher_fps_higher_bitrate() {
198 let est = BitrateEstimator::new();
199 let fps30 = est.estimate_from_crf(23, 1920, 1080, 30.0);
200 let fps60 = est.estimate_from_crf(23, 1920, 1080, 60.0);
201 assert!(fps60 > fps30, "60 fps should require more bits than 30 fps");
202 assert!(
203 (fps60 as f64 / fps30 as f64 - 2.0).abs() < 0.01,
204 "Should scale linearly with fps"
205 );
206 }
207
208 #[test]
209 fn test_zero_dimensions_returns_zero() {
210 let est = BitrateEstimator::new();
211 assert_eq!(est.estimate_from_crf(23, 0, 1080, 30.0), 0);
212 assert_eq!(est.estimate_from_crf(23, 1920, 0, 30.0), 0);
213 assert_eq!(est.estimate_from_crf(23, 1920, 1080, 0.0), 0);
214 }
215
216 #[test]
217 fn test_vmaf_estimate_high_quality() {
218 let est = BitrateEstimator::new();
219 let high = est.estimate_from_vmaf(95.0, 1920, 1080, 30.0);
220 let low = est.estimate_from_vmaf(50.0, 1920, 1080, 30.0);
221 assert!(high > low, "VMAF 95 should need more bits than VMAF 50");
222 }
223
224 #[test]
225 fn test_crf_for_target_bitrate_roundtrip() {
226 let est = BitrateEstimator::new();
227 let target_crf: u8 = 23;
228 let bps = est.estimate_from_crf(target_crf, 1920, 1080, 30.0);
229 if let Some(inferred_crf) = est.crf_for_target_bitrate(bps, 1920, 1080, 30.0) {
230 assert!(
232 (inferred_crf as i16 - target_crf as i16).abs() <= 1,
233 "Expected CRF ~{target_crf}, got {inferred_crf}"
234 );
235 }
236 }
237
238 #[test]
239 fn test_estimate_file_size() {
240 let est = BitrateEstimator::new();
241 let bytes = est.estimate_file_size(23, 1920, 1080, 30.0, 60.0); assert!(bytes > 0);
243 let bps = est.estimate_from_crf(23, 1920, 1080, 30.0);
245 let expected = (bps as f64 * 60.0 / 8.0).round() as u64;
246 assert_eq!(bytes, expected);
247 }
248
249 #[test]
250 fn test_video_params_estimate_bitrate() {
251 let params = VideoParams::new(1920, 1080, 30.0, 23);
252 let est = BitrateEstimator::new();
253 let bps = params.estimate_bitrate(&est);
254 assert_eq!(bps, est.estimate_from_crf(23, 1920, 1080, 30.0));
255 }
256
257 #[test]
258 fn test_custom_params() {
259 let est = BitrateEstimator::with_params(0.2, 0.05);
260 let bps = est.estimate_from_crf(20, 1280, 720, 25.0);
261 assert!(bps > 0);
262 }
263}