1mod fixed;
11
12pub use fixed::*;
13
14use serde::{Deserialize, Serialize};
15use viser_ffmpeg::Resolution;
16use viser_hull::{Hull, Point};
17
18#[derive(Debug, Clone, Serialize, Deserialize)]
20pub struct Rung {
21 #[serde(flatten)]
23 pub point: Point,
24 pub index: i32, }
27
28#[derive(Debug, Clone, Serialize, Deserialize)]
30pub struct Ladder {
31 pub rungs: Vec<Rung>,
33}
34
35#[derive(Debug, Clone, Serialize, Deserialize)]
37pub struct Opts {
38 pub num_rungs: i32, pub min_bitrate: f64, pub max_bitrate: f64, pub min_vmaf: f64, pub max_vmaf: f64, pub audio_bitrate_kbps: f64, }
51
52impl Default for Opts {
53 fn default() -> Self {
54 Self {
55 num_rungs: 6,
56 min_bitrate: 200.0,
57 max_bitrate: 8000.0,
58 min_vmaf: 40.0,
59 max_vmaf: 97.0,
60 audio_bitrate_kbps: 0.0,
61 }
62 }
63}
64
65pub fn select(h: &Hull, opts: &Opts) -> Ladder {
67 if h.points.is_empty() || opts.num_rungs <= 0 {
68 return Ladder { rungs: vec![] };
69 }
70
71 let crossover_min = build_crossover_map(h);
73
74 let mut candidates: Vec<Point> = Vec::new();
76 for p in &h.points {
77 if p.bitrate < opts.min_bitrate || p.bitrate > opts.max_bitrate - opts.audio_bitrate_kbps {
78 continue;
79 }
80 if p.vmaf < opts.min_vmaf {
81 continue;
82 }
83 if let Some(&min_br) = crossover_min.get(&p.resolution)
84 && p.bitrate < min_br
85 {
86 continue;
87 }
88 candidates.push(p.clone());
89 }
90
91 if candidates.is_empty() {
92 return Ladder { rungs: vec![] };
93 }
94
95 if candidates.len() <= opts.num_rungs as usize {
96 return to_ladder(candidates);
97 }
98
99 let min_q = candidates.first().map(|p| p.vmaf).unwrap_or(0.0);
101 let mut max_q = candidates.last().map(|p| p.vmaf).unwrap_or(100.0);
102 if opts.max_vmaf > 0.0 && max_q > opts.max_vmaf {
103 max_q = opts.max_vmaf;
104 }
105 let min_q = min_q.min(max_q);
106
107 let num = opts.num_rungs as usize;
109 let targets: Vec<f64> = if num == 1 {
110 vec![(min_q + max_q) / 2.0]
111 } else {
112 let step = (max_q - min_q) / (num - 1) as f64;
113 (0..num).map(|i| min_q + step * i as f64).collect()
114 };
115
116 let mut used = vec![false; candidates.len()];
118 let mut selected = Vec::new();
119
120 for target in &targets {
121 let mut best_idx = None;
122 let mut best_dist = f64::MAX;
123
124 for (i, p) in candidates.iter().enumerate() {
125 if used[i] {
126 continue;
127 }
128 let dist = (p.vmaf - target).abs();
129 if dist < best_dist {
130 best_dist = dist;
131 best_idx = Some(i);
132 }
133 }
134
135 if let Some(idx) = best_idx {
136 used[idx] = true;
137 selected.push(candidates[idx].clone());
138 }
139 }
140
141 to_ladder(selected)
142}
143
144fn build_crossover_map(h: &Hull) -> std::collections::HashMap<Resolution, f64> {
145 let mut crossovers = std::collections::HashMap::new();
146 for co in h.crossovers() {
147 crossovers.insert(co.to, co.bitrate);
148 }
149 crossovers
150}
151
152fn to_ladder(mut points: Vec<Point>) -> Ladder {
153 points.sort_by(|a, b| a.bitrate.partial_cmp(&b.bitrate).unwrap());
154 let rungs =
155 points.into_iter().enumerate().map(|(i, p)| Rung { point: p, index: i as i32 }).collect();
156 Ladder { rungs }
157}
158
159impl Ladder {
160 pub fn bitrate_range(&self) -> (f64, f64) {
162 if self.rungs.is_empty() {
163 return (0.0, 0.0);
164 }
165 (self.rungs.first().unwrap().point.bitrate, self.rungs.last().unwrap().point.bitrate)
166 }
167
168 pub fn quality_range(&self) -> (f64, f64) {
170 if self.rungs.is_empty() {
171 return (0.0, 0.0);
172 }
173 (self.rungs.first().unwrap().point.vmaf, self.rungs.last().unwrap().point.vmaf)
174 }
175
176 pub fn savings(&self, fixed_bitrate: f64) -> f64 {
180 if self.rungs.is_empty() || fixed_bitrate <= 0.0 {
181 return 0.0;
182 }
183 let top = &self.rungs.last().unwrap().point;
184 if top.bitrate >= fixed_bitrate {
185 return 0.0;
186 }
187 (1.0 - top.bitrate / fixed_bitrate) * 100.0
188 }
189}
190
191#[cfg(test)]
192mod tests {
193 use super::*;
194 use viser_ffmpeg::{Codec, Resolution};
195 use viser_hull::{Hull, Point};
196
197 fn point(bitrate: f64, vmaf: f64) -> Point {
198 Point {
199 resolution: Resolution::new(1920, 1080),
200 codec: Codec::X264,
201 crf: 23,
202 bitrate,
203 vmaf,
204 psnr: 0.0,
205 ssim: 0.0,
206 }
207 }
208
209 fn hull_for(points: Vec<Point>) -> Hull {
210 viser_hull::compute_upper(&points)
211 }
212
213 #[cfg(test)]
215 mod proptests {
216 use super::*;
217 use proptest::prelude::*;
218
219 fn arb_point() -> impl Strategy<Value = Point> {
220 let res = prop_oneof![
221 Just(Resolution::new(640, 360)),
222 Just(Resolution::new(1280, 720)),
223 Just(Resolution::new(1920, 1080)),
224 ];
225 (0.0f64..10000.0f64, 0.0f64..100.0f64, res, 0i32..51i32).prop_map(
226 |(bitrate, vmaf, res, crf)| Point {
227 resolution: res,
228 codec: Codec::X264,
229 crf,
230 bitrate,
231 vmaf,
232 psnr: 0.0,
233 ssim: 0.0,
234 },
235 )
236 }
237
238 fn arb_opts() -> impl Strategy<Value = Opts> {
239 (
240 1i32..12i32,
241 0.0f64..2000.0f64,
242 2000.0f64..20000.0f64,
243 0.0f64..60.0f64,
244 70.0f64..100.0f64,
245 0.0f64..500.0f64,
246 )
247 .prop_map(|(num_rungs, min_br, max_br, min_q, max_q, audio)| Opts {
248 num_rungs,
249 min_bitrate: min_br,
250 max_bitrate: max_br.max(min_br + 100.0),
251 min_vmaf: min_q,
252 max_vmaf: max_q.max(min_q + 1.0),
253 audio_bitrate_kbps: audio,
254 })
255 }
256
257 proptest! {
258 #[test]
260 fn ladder_rungs_sorted_by_bitrate(
261 points in proptest::collection::vec(arb_point(), 0..60),
262 opts in arb_opts(),
263 ) {
264 let hull = viser_hull::compute_upper(&points);
265 let ladder = select(&hull, &opts);
266 for w in ladder.rungs.windows(2) {
267 assert!(w[0].point.bitrate <= w[1].point.bitrate,
268 "ladder not sorted: {} kbps before {} kbps",
269 w[0].point.bitrate, w[1].point.bitrate);
270 }
271 }
272
273 #[test]
275 fn ladder_rung_count_within_limit(
276 points in proptest::collection::vec(arb_point(), 0..60),
277 opts in arb_opts(),
278 ) {
279 let hull = viser_hull::compute_upper(&points);
280 let ladder = select(&hull, &opts);
281 assert!(ladder.rungs.len() <= opts.num_rungs as usize,
282 "got {} rungs, asked for {}", ladder.rungs.len(), opts.num_rungs);
283 }
284
285 #[test]
287 fn ladder_rungs_within_bitrate_bounds(
288 points in proptest::collection::vec(arb_point(), 0..60),
289 opts in arb_opts(),
290 ) {
291 let hull = viser_hull::compute_upper(&points);
292 let ladder = select(&hull, &opts);
293 let effective_max = opts.max_bitrate - opts.audio_bitrate_kbps;
294 for rung in &ladder.rungs {
295 assert!(rung.point.bitrate >= opts.min_bitrate - 1e-9,
296 "rung bitrate {:.1} below min {:.1}",
297 rung.point.bitrate, opts.min_bitrate);
298 assert!(rung.point.bitrate <= effective_max + 1e-9,
299 "rung bitrate {:.1} above max {:.1} (effective after audio {:.1})",
300 rung.point.bitrate, effective_max, opts.max_bitrate);
301 }
302 }
303
304 #[test]
306 fn ladder_rungs_within_vmaf_bounds(
307 points in proptest::collection::vec(arb_point(), 0..60),
308 opts in arb_opts(),
309 ) {
310 let hull = viser_hull::compute_upper(&points);
311 let ladder = select(&hull, &opts);
312 for rung in &ladder.rungs {
313 assert!(rung.point.vmaf >= opts.min_vmaf - 1e-9,
314 "rung vmaf {:.1} below min {:.1}", rung.point.vmaf, opts.min_vmaf);
315 }
316 }
317
318 #[test]
320 fn ladder_rung_indices_contiguous(
321 points in proptest::collection::vec(arb_point(), 0..60),
322 opts in arb_opts(),
323 ) {
324 let hull = viser_hull::compute_upper(&points);
325 let ladder = select(&hull, &opts);
326 let indices: Vec<i32> = ladder.rungs.iter().map(|r| r.index).collect();
327 let expected: Vec<i32> = (0..ladder.rungs.len() as i32).collect();
328 assert_eq!(indices, expected, "rung indices not contiguous");
329 }
330
331 #[test]
333 fn ladder_bitrate_range_ordered(
334 points in proptest::collection::vec(arb_point(), 1..60),
335 ) {
336 let mut sorted = points;
337 sorted.sort_by(|a, b| a.bitrate.partial_cmp(&b.bitrate).unwrap());
338 let ladder = Ladder {
339 rungs: sorted.iter().enumerate().map(|(i, p)| Rung {
340 point: p.clone(), index: i as i32
341 }).collect()
342 };
343 let (lo, hi) = ladder.bitrate_range();
344 assert!(lo <= hi, "bitrate range out of order: {lo} > {hi}");
345 }
346 #[test]
347 fn ladder_savings_bounded(
348 points in proptest::collection::vec(arb_point(), 1..60),
349 fixed_bitrate in 1000.0f64..20000.0f64,
350 ) {
351 let ladder = Ladder {
352 rungs: points.iter().enumerate().map(|(i, p)| Rung {
353 point: p.clone(), index: i as i32
354 }).collect()
355 };
356 let s = ladder.savings(fixed_bitrate);
357 assert!(s >= 0.0, "savings negative: {s}");
358 assert!(s <= 100.0, "savings > 100%: {s}");
359 }
360
361 }
362 }
363
364 #[test]
365 fn test_select_empty_hull() {
366 let h = Hull { points: vec![] };
367 let ladder = select(&h, &Opts::default());
368 assert!(ladder.rungs.is_empty());
369 }
370
371 #[test]
372 fn test_select_zero_rungs() {
373 let h = hull_for(vec![point(500.0, 80.0), point(1000.0, 90.0)]);
374 let ladder = select(&h, &Opts { num_rungs: 0, ..Opts::default() });
375 assert!(ladder.rungs.is_empty());
376 }
377
378 #[test]
379 fn test_select_fewer_candidates_than_rungs() {
380 let h = hull_for(vec![point(500.0, 80.0), point(1000.0, 90.0)]);
381 let ladder = select(&h, &Opts { num_rungs: 6, ..Opts::default() });
382 assert!(!ladder.rungs.is_empty());
383 assert!(ladder.rungs.len() <= 2);
384 }
385
386 #[test]
387 fn test_select_filters_outside_bitrate_range() {
388 let h = hull_for(vec![
389 point(100.0, 50.0),
390 point(500.0, 80.0),
391 point(1000.0, 90.0),
392 point(10000.0, 98.0),
393 ]);
394 let opts =
395 Opts { num_rungs: 4, min_bitrate: 200.0, max_bitrate: 5000.0, ..Opts::default() };
396 let ladder = select(&h, &opts);
397 for rung in &ladder.rungs {
398 assert!(rung.point.bitrate >= 200.0);
399 assert!(rung.point.bitrate <= 5000.0);
400 }
401 }
402
403 #[test]
404 fn test_select_filters_below_min_vmaf() {
405 let h = hull_for(vec![
406 point(200.0, 30.0),
407 point(500.0, 60.0),
408 point(1000.0, 85.0),
409 point(2000.0, 95.0),
410 ]);
411 let opts = Opts { num_rungs: 4, min_vmaf: 50.0, ..Opts::default() };
412 let ladder = select(&h, &opts);
413 for rung in &ladder.rungs {
414 assert!(rung.point.vmaf >= 50.0);
415 }
416 }
417
418 #[test]
419 fn test_select_output_sorted() {
420 let h = hull_for(vec![
421 point(500.0, 70.0),
422 point(1000.0, 85.0),
423 point(2000.0, 93.0),
424 point(5000.0, 98.0),
425 ]);
426 let ladder = select(&h, &Opts::default());
427 assert!(ladder.rungs.windows(2).all(|w| w[0].point.bitrate <= w[1].point.bitrate));
428 }
429
430 #[test]
431 fn test_select_rung_indices() {
432 let h = hull_for(vec![point(500.0, 70.0), point(1000.0, 85.0), point(2000.0, 93.0)]);
433 let ladder = select(&h, &Opts { num_rungs: 3, ..Opts::default() });
434 for (i, rung) in ladder.rungs.iter().enumerate() {
435 assert_eq!(rung.index as usize, i);
436 }
437 }
438
439 #[test]
440 fn test_bitrate_range_empty() {
441 let ladder = Ladder { rungs: vec![] };
442 assert_eq!(ladder.bitrate_range(), (0.0, 0.0));
443 }
444
445 #[test]
446 fn test_bitrate_range() {
447 let rungs = vec![
448 Rung { point: point(500.0, 70.0), index: 0 },
449 Rung { point: point(2000.0, 93.0), index: 1 },
450 ];
451 let ladder = Ladder { rungs };
452 assert_eq!(ladder.bitrate_range(), (500.0, 2000.0));
453 }
454
455 #[test]
456 fn test_quality_range_empty() {
457 let ladder = Ladder { rungs: vec![] };
458 assert_eq!(ladder.quality_range(), (0.0, 0.0));
459 }
460
461 #[test]
462 fn test_quality_range() {
463 let rungs = vec![
464 Rung { point: point(500.0, 70.0), index: 0 },
465 Rung { point: point(2000.0, 93.0), index: 1 },
466 ];
467 let ladder = Ladder { rungs };
468 assert_eq!(ladder.quality_range(), (70.0, 93.0));
469 }
470
471 #[test]
472 fn test_savings_empty() {
473 let ladder = Ladder { rungs: vec![] };
474 assert_eq!(ladder.savings(8000.0), 0.0);
475 }
476
477 #[test]
478 fn test_savings_zero_fixed() {
479 let rungs = vec![Rung { point: point(2000.0, 93.0), index: 0 }];
480 let ladder = Ladder { rungs };
481 assert_eq!(ladder.savings(0.0), 0.0);
482 }
483
484 #[test]
485 fn test_savings_no_savings() {
486 let rungs = vec![Rung { point: point(8000.0, 93.0), index: 0 }];
487 let ladder = Ladder { rungs };
488 assert_eq!(ladder.savings(8000.0), 0.0);
489 }
490
491 #[test]
492 fn test_savings_calculated() {
493 let rungs = vec![Rung { point: point(4000.0, 93.0), index: 0 }];
494 let ladder = Ladder { rungs };
495 let s = ladder.savings(8000.0);
496 assert!((s - 50.0).abs() < 1e-9);
497 }
498
499 #[test]
500 fn test_netflix_old_ladder() {
501 let ladder = netflix_old();
502 assert_eq!(ladder.name, "Netflix Fixed (2015)");
503 assert_eq!(ladder.rungs.len(), 10);
504 assert!((ladder.total_bitrate() - 20170.0).abs() < 1e-9);
505 assert!((ladder.top_bitrate() - 5800.0).abs() < 1e-9);
506 }
507
508 #[test]
509 fn test_apple_hls_ladder() {
510 let ladder = apple_hls();
511 assert_eq!(ladder.name, "Apple HLS (2024)");
512 assert_eq!(ladder.rungs.len(), 9);
513 assert!((ladder.total_bitrate() - 25640.0).abs() < 1e-9);
514 assert!((ladder.top_bitrate() - 7800.0).abs() < 1e-9);
515 }
516
517 #[test]
518 fn test_select_respects_max_vmaf() {
519 let h = hull_for(vec![
520 point(500.0, 70.0),
521 point(1000.0, 85.0),
522 point(2000.0, 90.0),
523 point(3000.0, 93.0),
524 point(5000.0, 98.0),
525 ]);
526 let opts = Opts { num_rungs: 3, max_vmaf: 90.0, ..Opts::default() };
527 let ladder = select(&h, &opts);
528 assert!(!ladder.rungs.is_empty());
531 assert!(ladder.rungs.last().unwrap().point.vmaf <= 90.0 + 1e-9);
533 }
534
535 #[test]
536 fn test_opts_default() {
537 let opts = Opts::default();
538 assert_eq!(opts.num_rungs, 6);
539 assert!((opts.min_bitrate - 200.0).abs() < 1e-9);
540 assert!((opts.max_bitrate - 8000.0).abs() < 1e-9);
541 assert!((opts.min_vmaf - 40.0).abs() < 1e-9);
542 assert!((opts.max_vmaf - 97.0).abs() < 1e-9);
543 }
544}