1pub const CONTAINER_OVERHEAD: f64 = 0.02;
14
15pub const DEFAULT_AUDIO_BPS: u64 = 128_000;
17
18pub const ABSOLUTE_MIN_VIDEO_BPS: u64 = 60_000;
21
22#[derive(Debug, Clone, Copy)]
27pub struct Rung {
28 pub height: u32,
29 pub min_video_bps: u64,
30}
31
32pub const LADDER: &[Rung] = &[
34 Rung {
35 height: 1080,
36 min_video_bps: 2_500_000,
37 },
38 Rung {
39 height: 720,
40 min_video_bps: 1_200_000,
41 },
42 Rung {
43 height: 480,
44 min_video_bps: 600_000,
45 },
46 Rung {
47 height: 360,
48 min_video_bps: 350_000,
49 },
50 Rung {
51 height: 240,
52 min_video_bps: 200_000,
53 },
54 Rung {
55 height: 144,
56 min_video_bps: 100_000,
57 },
58];
59
60pub fn video_bitrate_bps(target_bytes: u64, duration_sec: f64, audio_bps: u64) -> Option<u64> {
63 if duration_sec <= 0.0 {
64 return None;
65 }
66 let usable_bits = target_bytes as f64 * 8.0 * (1.0 - CONTAINER_OVERHEAD);
67 let audio_bits = audio_bps as f64 * duration_sec;
68 let video_bits = usable_bits - audio_bits;
69 if video_bits <= 0.0 {
70 return None;
71 }
72 Some((video_bits / duration_sec) as u64)
73}
74
75pub fn reduce_target_bytes(original_bytes: u64, reduce_fraction: f64) -> u64 {
78 let keep = (1.0 - reduce_fraction).clamp(0.0, 1.0);
79 (original_bytes as f64 * keep).round() as u64
80}
81
82pub fn choose_height(src_height: u32, video_bps: u64) -> u32 {
89 for rung in LADDER {
90 if rung.height <= src_height && video_bps >= rung.min_video_bps {
91 return rung.height;
92 }
93 }
94 LADDER
95 .iter()
96 .filter(|r| r.height <= src_height)
97 .map(|r| r.height)
98 .min()
99 .unwrap_or(src_height)
100}
101
102pub fn fit_audio_bps(target_bytes: u64, duration_sec: f64, candidates: &[u64]) -> Option<u64> {
106 candidates.iter().copied().find(|&audio_bps| {
107 video_bitrate_bps(target_bytes, duration_sec, audio_bps)
108 .is_some_and(|v| v >= ABSOLUTE_MIN_VIDEO_BPS)
109 })
110}
111
112pub fn search_crf(target: f64, lo: u8, hi: u8, mut measure: impl FnMut(u8) -> f64) -> (u8, f64) {
125 let base = measure(lo);
126 if base < target {
128 return (lo, base);
129 }
130 let mut best = (lo, base);
131 let (mut a, mut b) = (lo as i32 + 1, hi as i32);
132 while a <= b {
133 let mid = a + (b - a) / 2;
134 let v = measure(mid as u8);
135 if v >= target {
136 best = (mid as u8, v);
137 a = mid + 1;
138 } else {
139 b = mid - 1;
140 }
141 }
142 best
143}
144
145pub const AUDIO_STEPS: &[u64] = &[
150 320_000, 256_000, 192_000, 160_000, 128_000, 96_000, 80_000, 64_000, 48_000, 32_000, 24_000,
151 16_000, 12_000,
152];
153
154pub const ABSOLUTE_MIN_AUDIO_BPS: u64 = 12_000;
157
158pub fn audio_bitrate_bps(target_bytes: u64, duration_sec: f64) -> Option<u64> {
161 if duration_sec <= 0.0 {
162 return None;
163 }
164 let usable_bits = target_bytes as f64 * 8.0 * (1.0 - CONTAINER_OVERHEAD);
165 let bps = usable_bits / duration_sec;
166 if bps <= 0.0 {
167 return None;
168 }
169 Some(bps as u64)
170}
171
172pub fn snap_audio_bitrate(bps: u64) -> u64 {
175 AUDIO_STEPS
176 .iter()
177 .copied()
178 .find(|&step| step <= bps)
179 .unwrap_or_else(|| *AUDIO_STEPS.last().expect("non-empty ladder"))
180}
181
182#[cfg(test)]
183mod tests {
184 use super::*;
185
186 #[test]
187 fn video_bitrate_subtracts_audio_and_overhead() {
188 let v = video_bitrate_bps(8_000_000, 60.0, 128_000).unwrap();
193 assert!((916_000..=918_000).contains(&v), "got {v}");
194 }
195
196 #[test]
197 fn video_bitrate_none_when_audio_eats_budget() {
198 assert_eq!(video_bitrate_bps(100_000, 600.0, 128_000), None);
200 assert_eq!(video_bitrate_bps(1_000_000, 0.0, 0), None);
201 }
202
203 #[test]
204 fn reduce_keeps_complement() {
205 assert_eq!(reduce_target_bytes(1_000_000, 0.70), 300_000);
206 assert_eq!(reduce_target_bytes(1_000_000, 0.0), 1_000_000);
207 assert_eq!(reduce_target_bytes(1_000_000, 1.0), 0);
208 }
209
210 #[test]
211 fn choose_height_picks_best_feasible_rung() {
212 assert_eq!(choose_height(1080, 4_000_000), 1080);
214 assert_eq!(choose_height(1080, 800_000), 480);
216 assert_eq!(choose_height(720, 5_000_000), 720);
218 assert_eq!(choose_height(1080, 10_000), 144);
220 assert_eq!(choose_height(100, 500_000), 100);
222 }
223
224 #[test]
225 fn audio_bitrate_fits_target() {
226 let bps = audio_bitrate_bps(10_000_000, 3480.0).unwrap();
229 assert!((22_000..=23_000).contains(&bps), "got {bps}");
230 assert_eq!(audio_bitrate_bps(1_000_000, 0.0), None);
231 }
232
233 #[test]
234 fn snap_audio_floors_to_a_step() {
235 assert_eq!(snap_audio_bitrate(22_528), 16_000);
236 assert_eq!(snap_audio_bitrate(128_000), 128_000);
237 assert_eq!(snap_audio_bitrate(130_000), 128_000);
238 assert_eq!(snap_audio_bitrate(1_000_000), 320_000); assert_eq!(snap_audio_bitrate(12_000), 12_000);
240 }
241
242 #[test]
243 fn search_crf_picks_highest_meeting_target() {
244 let vmaf = |crf: u8| 100.0 - 1.5 * (crf as f64 - 18.0);
247 let (crf, v) = search_crf(91.0, 18, 32, vmaf);
249 assert_eq!(crf, 24);
250 assert!(v >= 91.0);
251 assert!(vmaf(crf + 1) < 91.0);
253 }
254
255 #[test]
256 fn search_crf_best_effort_when_unreachable() {
257 let vmaf = |crf: u8| 80.0 - (crf as f64 - 18.0);
259 let (crf, v) = search_crf(95.0, 18, 32, vmaf);
260 assert_eq!(crf, 18);
261 assert_eq!(v, 80.0);
262 }
263
264 #[test]
265 fn search_crf_keeps_top_quality_when_all_pass() {
266 let vmaf = |crf: u8| 100.0 - 0.1 * (crf as f64 - 18.0);
268 let (crf, _) = search_crf(50.0, 18, 32, vmaf);
269 assert_eq!(crf, 32);
270 }
271
272 #[test]
273 fn fit_audio_steps_down() {
274 let candidates = [128_000, 96_000, 64_000, 48_000];
275 assert_eq!(fit_audio_bps(8_000_000, 60.0, &candidates), Some(128_000));
277 assert_eq!(fit_audio_bps(1_300_000, 60.0, &candidates), Some(96_000));
279 assert_eq!(fit_audio_bps(50_000, 600.0, &candidates), None);
281 }
282}