Skip to main content

rivet/
ladder.rs

1//! ABR ladder computation — derive a sensible set of [`Rung`]s from a source
2//! resolution.
3//!
4//! Callers who want full control build [`Rung`]s by hand and skip this module
5//! entirely. [`standard_ladder`] is the convenience path: it snaps to standard
6//! short-side quantizations (2160/1440/1080/720/480/360/240), preserves the
7//! source aspect ratio, even-aligns every dimension, and caps the top rung.
8
9use crate::spec::{Quality, Rung};
10
11/// Standard short-side quantizations, descending. The "p" number always refers
12/// to the **short** side of the frame regardless of orientation.
13const STANDARD_SHORT_SIDES: &[u32] = &[2160, 1440, 1080, 720, 480, 360, 240];
14
15/// Default cap on the short side of any rung. Sources above this have their
16/// top rung clamped down (aspect preserved); rungs above it never appear.
17pub const DEFAULT_MAX_SHORT_SIDE: u32 = 1080;
18
19/// Smallest allowed dimension on a ladder rung (excluding the source rung).
20const MIN_DIMENSION: u32 = 200;
21
22/// Build a standard ladder for a source clip, every rung at default quality.
23///
24/// Pass `max_short_side = None` for the default cap (1080). Lifting the cap
25/// unlocks the corresponding higher rungs (1440 → QHD, 2160 → 4K).
26pub fn standard_ladder(src_width: u32, src_height: u32, max_short_side: Option<u32>) -> Vec<Rung> {
27    dims_for(src_width, src_height, max_short_side)
28        .into_iter()
29        .map(|(w, h)| Rung::new(w, h))
30        .collect()
31}
32
33/// Same as [`standard_ladder`] but stamps every rung with `quality`.
34pub fn standard_ladder_with_quality(
35    src_width: u32,
36    src_height: u32,
37    max_short_side: Option<u32>,
38    quality: Quality,
39) -> Vec<Rung> {
40    dims_for(src_width, src_height, max_short_side)
41        .into_iter()
42        .map(|(w, h)| Rung::new(w, h).with_quality(quality.clone()))
43        .collect()
44}
45
46/// Core algorithm: source-aspect-preserving, even-aligned, capped rung dims.
47fn dims_for(src_width: u32, src_height: u32, max_short_side: Option<u32>) -> Vec<(u32, u32)> {
48    let cap = max_short_side.unwrap_or(DEFAULT_MAX_SHORT_SIDE);
49    if src_width == 0 || src_height == 0 {
50        return Vec::new();
51    }
52
53    let is_landscape = src_width >= src_height;
54    let src_short = src_width.min(src_height);
55    let src_long = src_width.max(src_height);
56    let src_aspect = src_long as f64 / src_short as f64;
57
58    // Source rung — clamp the short side to `cap`, scale the long side
59    // proportionally, even-align both axes.
60    let (top_short, top_long) = if src_short > cap {
61        let s = cap;
62        let l = ((s as f64 * src_aspect).round() as u32) & !1;
63        (s & !1, l)
64    } else {
65        (src_short & !1, src_long & !1)
66    };
67    let (top_w, top_h) = if is_landscape {
68        (top_long, top_short)
69    } else {
70        (top_short, top_long)
71    };
72
73    let mut out: Vec<(u32, u32)> = vec![(top_w, top_h)];
74
75    for &short in STANDARD_SHORT_SIDES {
76        if short >= top_short || short > cap {
77            continue;
78        }
79        let long = ((short as f64 * src_aspect).round() as u32) & !1;
80        let short_even = short & !1;
81        if short_even < MIN_DIMENSION || long < MIN_DIMENSION {
82            continue;
83        }
84        let (w, h) = if is_landscape {
85            (long, short_even)
86        } else {
87            (short_even, long)
88        };
89        if !out.iter().any(|&(pw, ph)| pw == w && ph == h) {
90            out.push((w, h));
91        }
92    }
93
94    out
95}
96
97#[cfg(test)]
98mod tests {
99    use super::*;
100
101    fn dims(rungs: &[Rung]) -> Vec<(u32, u32)> {
102        rungs.iter().map(|r| (r.width, r.height)).collect()
103    }
104
105    #[test]
106    fn ladder_16_9_1080p_source() {
107        let v = standard_ladder(1920, 1080, None);
108        assert_eq!(
109            dims(&v),
110            vec![(1920, 1080), (1280, 720), (852, 480), (640, 360), (426, 240)]
111        );
112    }
113
114    #[test]
115    fn ladder_4k_clamps_to_1080p_by_default() {
116        let v = standard_ladder(3840, 2160, None);
117        assert_eq!(v.first().map(|r| (r.width, r.height)), Some((1920, 1080)));
118    }
119
120    #[test]
121    fn ladder_4k_with_2160_cap_keeps_full_quality() {
122        let v = standard_ladder(3840, 2160, Some(2160));
123        assert_eq!(v.first().map(|r| (r.width, r.height)), Some((3840, 2160)));
124        assert_eq!(v.len(), 7);
125    }
126
127    #[test]
128    fn ladder_portrait_short_side_labels() {
129        let v = standard_ladder(1080, 1920, None);
130        assert_eq!(dims(&v), vec![(1080, 1920), (720, 1280), (480, 852), (360, 640), (240, 426)]);
131        assert_eq!(v[0].label, "1080p");
132        assert_eq!(v[1].label, "720p");
133    }
134
135    #[test]
136    fn ladder_below_floor_keeps_only_source() {
137        assert_eq!(dims(&standard_ladder(320, 240, None)), vec![(320, 240)]);
138    }
139
140    #[test]
141    fn ladder_zero_dims_empty() {
142        assert!(standard_ladder(0, 1080, None).is_empty());
143    }
144
145    #[test]
146    fn every_rung_is_even() {
147        for r in standard_ladder(1921, 1081, None) {
148            assert_eq!(r.width % 2, 0);
149            assert_eq!(r.height % 2, 0);
150        }
151    }
152}