1#![allow(dead_code)]
7#![allow(clippy::cast_precision_loss)]
8
9#[derive(Debug, Clone, PartialEq)]
11pub struct AbrRungConfig {
12 pub label: String,
14 pub width: u32,
16 pub height: u32,
18 pub video_bitrate_bps: u64,
20 pub audio_bitrate_bps: u64,
22 pub fps_num: u32,
24 pub fps_den: u32,
26 pub crf: Option<u8>,
28 pub profile: Option<String>,
30 pub bufsize_bits: Option<u64>,
32}
33
34impl AbrRungConfig {
35 #[must_use]
37 pub fn new(
38 label: impl Into<String>,
39 width: u32,
40 height: u32,
41 video_bitrate_bps: u64,
42 audio_bitrate_bps: u64,
43 ) -> Self {
44 Self {
45 label: label.into(),
46 width,
47 height,
48 video_bitrate_bps,
49 audio_bitrate_bps,
50 fps_num: 30,
51 fps_den: 1,
52 crf: None,
53 profile: None,
54 bufsize_bits: None,
55 }
56 }
57
58 #[must_use]
60 pub fn with_fps(mut self, num: u32, den: u32) -> Self {
61 self.fps_num = num;
62 self.fps_den = den;
63 self
64 }
65
66 #[must_use]
68 pub fn with_crf(mut self, crf: u8) -> Self {
69 self.crf = Some(crf);
70 self
71 }
72
73 #[must_use]
75 pub fn with_profile(mut self, profile: impl Into<String>) -> Self {
76 self.profile = Some(profile.into());
77 self
78 }
79
80 #[must_use]
82 pub fn with_bufsize(mut self, bufsize_bits: u64) -> Self {
83 self.bufsize_bits = Some(bufsize_bits);
84 self
85 }
86
87 #[must_use]
89 pub fn total_bitrate_bps(&self) -> u64 {
90 self.video_bitrate_bps + self.audio_bitrate_bps
91 }
92
93 #[must_use]
95 pub fn fps_f64(&self) -> f64 {
96 f64::from(self.fps_num) / f64::from(self.fps_den)
97 }
98
99 #[must_use]
101 pub fn pixel_count(&self) -> u64 {
102 u64::from(self.width) * u64::from(self.height)
103 }
104}
105
106#[derive(Debug, Clone, Copy, PartialEq, Eq)]
108pub enum LadderSelectionStrategy {
109 BandwidthFit,
111 ResolutionMatch,
113 Conservative,
115 Aggressive,
117}
118
119#[derive(Debug, Clone)]
121pub struct SwitchRule {
122 pub switch_up_bandwidth_bps: u64,
124 pub switch_down_bandwidth_bps: u64,
126 pub switch_up_samples: u32,
128 pub allow_multi_rung_up: bool,
130}
131
132impl SwitchRule {
133 #[must_use]
135 pub fn new(switch_up_bps: u64, switch_down_bps: u64) -> Self {
136 Self {
137 switch_up_bandwidth_bps: switch_up_bps,
138 switch_down_bandwidth_bps: switch_down_bps,
139 switch_up_samples: 3,
140 allow_multi_rung_up: false,
141 }
142 }
143
144 #[must_use]
146 pub fn with_switch_up_samples(mut self, samples: u32) -> Self {
147 self.switch_up_samples = samples;
148 self
149 }
150}
151
152#[derive(Debug, Clone)]
154pub struct AbrLadderConfig {
155 pub rungs: Vec<AbrRungConfig>,
157 pub strategy: LadderSelectionStrategy,
159 pub switch_rules: Vec<SwitchRule>,
161 pub segment_duration_secs: f64,
163 pub codec: String,
165}
166
167impl AbrLadderConfig {
168 #[must_use]
170 pub fn new(codec: impl Into<String>) -> Self {
171 Self {
172 rungs: Vec::new(),
173 strategy: LadderSelectionStrategy::BandwidthFit,
174 switch_rules: Vec::new(),
175 segment_duration_secs: 6.0,
176 codec: codec.into(),
177 }
178 }
179
180 #[must_use]
182 pub fn add_rung(mut self, rung: AbrRungConfig) -> Self {
183 self.rungs.push(rung);
184 self.rungs.sort_by_key(|r| r.video_bitrate_bps);
185 self
186 }
187
188 #[must_use]
190 pub fn with_strategy(mut self, strategy: LadderSelectionStrategy) -> Self {
191 self.strategy = strategy;
192 self
193 }
194
195 #[must_use]
197 pub fn with_segment_duration(mut self, secs: f64) -> Self {
198 self.segment_duration_secs = secs;
199 self
200 }
201
202 #[must_use]
204 pub fn standard_hls_h264() -> Self {
205 Self::new("h264")
206 .add_rung(
207 AbrRungConfig::new("240p", 426, 240, 400_000, 64_000).with_profile("baseline"),
208 )
209 .add_rung(AbrRungConfig::new("360p", 640, 360, 800_000, 96_000).with_profile("main"))
210 .add_rung(AbrRungConfig::new("480p", 854, 480, 1_400_000, 128_000).with_profile("main"))
211 .add_rung(
212 AbrRungConfig::new("720p", 1280, 720, 2_800_000, 128_000).with_profile("high"),
213 )
214 .add_rung(
215 AbrRungConfig::new("1080p", 1920, 1080, 5_000_000, 192_000).with_profile("high"),
216 )
217 .add_rung(
218 AbrRungConfig::new("4K", 3840, 2160, 15_000_000, 192_000).with_profile("high"),
219 )
220 }
221
222 #[must_use]
224 pub fn select_rung(&self, available_bps: u64) -> Option<&AbrRungConfig> {
225 match self.strategy {
226 LadderSelectionStrategy::BandwidthFit => self
227 .rungs
228 .iter()
229 .rfind(|r| r.total_bitrate_bps() <= available_bps),
230 LadderSelectionStrategy::Conservative => {
231 let fitting: Vec<&AbrRungConfig> = self
232 .rungs
233 .iter()
234 .filter(|r| r.total_bitrate_bps() <= available_bps)
235 .collect();
236 if fitting.len() > 1 {
237 fitting.get(fitting.len() - 2).copied()
238 } else {
239 fitting.into_iter().last()
240 }
241 }
242 LadderSelectionStrategy::Aggressive => self
243 .rungs
244 .iter()
245 .rfind(|r| r.total_bitrate_bps() <= available_bps * 3 / 2),
246 LadderSelectionStrategy::ResolutionMatch => {
247 self.rungs
249 .iter()
250 .rfind(|r| r.total_bitrate_bps() <= available_bps)
251 }
252 }
253 }
254
255 #[must_use]
257 pub fn rung_count(&self) -> usize {
258 self.rungs.len()
259 }
260
261 #[must_use]
263 pub fn lowest_rung(&self) -> Option<&AbrRungConfig> {
264 self.rungs.first()
265 }
266
267 #[must_use]
269 pub fn highest_rung(&self) -> Option<&AbrRungConfig> {
270 self.rungs.last()
271 }
272
273 pub fn generate_switch_rules(&mut self) {
275 self.switch_rules.clear();
276 for window in self.rungs.windows(2) {
277 let lower = &window[0];
278 let upper = &window[1];
279 let switch_up = upper.total_bitrate_bps() * 120 / 100;
281 let switch_down = lower.total_bitrate_bps();
283 self.switch_rules
284 .push(SwitchRule::new(switch_up, switch_down));
285 }
286 }
287}
288
289#[cfg(test)]
290mod tests {
291 use super::*;
292
293 #[test]
294 fn test_rung_total_bitrate() {
295 let rung = AbrRungConfig::new("720p", 1280, 720, 2_800_000, 128_000);
296 assert_eq!(rung.total_bitrate_bps(), 2_928_000);
297 }
298
299 #[test]
300 fn test_rung_fps_f64() {
301 let rung = AbrRungConfig::new("1080p", 1920, 1080, 5_000_000, 192_000).with_fps(60, 1);
302 assert!((rung.fps_f64() - 60.0).abs() < f64::EPSILON);
303 }
304
305 #[test]
306 fn test_rung_pixel_count() {
307 let rung = AbrRungConfig::new("1080p", 1920, 1080, 5_000_000, 192_000);
308 assert_eq!(rung.pixel_count(), 1920 * 1080);
309 }
310
311 #[test]
312 fn test_rung_with_crf() {
313 let rung = AbrRungConfig::new("720p", 1280, 720, 2_800_000, 128_000).with_crf(23);
314 assert_eq!(rung.crf, Some(23));
315 }
316
317 #[test]
318 fn test_rung_with_profile() {
319 let rung = AbrRungConfig::new("1080p", 1920, 1080, 5_000_000, 192_000).with_profile("high");
320 assert_eq!(rung.profile.as_deref(), Some("high"));
321 }
322
323 #[test]
324 fn test_rung_with_bufsize() {
325 let rung = AbrRungConfig::new("480p", 854, 480, 1_400_000, 128_000).with_bufsize(2_800_000);
326 assert_eq!(rung.bufsize_bits, Some(2_800_000));
327 }
328
329 #[test]
330 fn test_ladder_rung_count() {
331 let ladder = AbrLadderConfig::standard_hls_h264();
332 assert_eq!(ladder.rung_count(), 6);
333 }
334
335 #[test]
336 fn test_ladder_sorted_by_bitrate() {
337 let ladder = AbrLadderConfig::standard_hls_h264();
338 let bitrates: Vec<u64> = ladder.rungs.iter().map(|r| r.video_bitrate_bps).collect();
339 let mut sorted = bitrates.clone();
340 sorted.sort_unstable();
341 assert_eq!(bitrates, sorted);
342 }
343
344 #[test]
345 fn test_select_rung_bandwidth_fit() {
346 let ladder = AbrLadderConfig::standard_hls_h264();
347 let rung = ladder
349 .select_rung(3_000_000)
350 .expect("should succeed in test");
351 assert_eq!(rung.label, "720p");
352 }
353
354 #[test]
355 fn test_select_rung_conservative() {
356 let ladder = AbrLadderConfig::standard_hls_h264()
357 .with_strategy(LadderSelectionStrategy::Conservative);
358 let rung = ladder
359 .select_rung(3_000_000)
360 .expect("should succeed in test");
361 assert_eq!(rung.label, "480p");
363 }
364
365 #[test]
366 fn test_select_rung_no_fit() {
367 let ladder = AbrLadderConfig::standard_hls_h264();
368 let rung = ladder.select_rung(100_000);
370 assert!(rung.is_none());
371 }
372
373 #[test]
374 fn test_lowest_highest_rung() {
375 let ladder = AbrLadderConfig::standard_hls_h264();
376 assert_eq!(
377 ladder.lowest_rung().expect("should succeed in test").label,
378 "240p"
379 );
380 assert_eq!(
381 ladder.highest_rung().expect("should succeed in test").label,
382 "4K"
383 );
384 }
385
386 #[test]
387 fn test_generate_switch_rules() {
388 let mut ladder = AbrLadderConfig::standard_hls_h264();
389 ladder.generate_switch_rules();
390 assert_eq!(ladder.switch_rules.len(), ladder.rung_count() - 1);
392 }
393
394 #[test]
395 fn test_switch_rule_new() {
396 let rule = SwitchRule::new(5_000_000, 2_000_000).with_switch_up_samples(5);
397 assert_eq!(rule.switch_up_bandwidth_bps, 5_000_000);
398 assert_eq!(rule.switch_down_bandwidth_bps, 2_000_000);
399 assert_eq!(rule.switch_up_samples, 5);
400 }
401
402 #[test]
403 fn test_ladder_segment_duration() {
404 let ladder = AbrLadderConfig::new("vp9").with_segment_duration(4.0);
405 assert!((ladder.segment_duration_secs - 4.0).abs() < f64::EPSILON);
406 }
407
408 #[test]
409 fn test_ladder_codec() {
410 let ladder = AbrLadderConfig::new("av1");
411 assert_eq!(ladder.codec, "av1");
412 }
413}