Skip to main content

oximedia_transcode/
abr.rs

1//! Adaptive Bitrate (ABR) ladder generation for HLS/DASH streaming.
2
3use serde::{Deserialize, Serialize};
4
5/// A single rung in an ABR ladder.
6#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
7pub struct AbrRung {
8    /// Video width in pixels.
9    pub width: u32,
10    /// Video height in pixels.
11    pub height: u32,
12    /// Target video bitrate in bits per second.
13    pub video_bitrate: u64,
14    /// Target audio bitrate in bits per second.
15    pub audio_bitrate: u64,
16    /// Frame rate as (numerator, denominator).
17    pub frame_rate: (u32, u32),
18    /// Codec to use for this rung.
19    pub codec: String,
20    /// Profile name for this rung (e.g., "720p", "1080p").
21    pub profile_name: String,
22}
23
24/// Strategy for generating ABR ladder rungs.
25#[derive(Debug, Clone, Copy, PartialEq, Eq)]
26pub enum AbrStrategy {
27    /// Apple HLS recommendations.
28    AppleHls,
29    /// `YouTube` recommendations.
30    YouTube,
31    /// Netflix-style ladder.
32    Netflix,
33    /// Conservative ladder (fewer rungs).
34    Conservative,
35    /// Aggressive ladder (more rungs).
36    Aggressive,
37    /// Custom strategy.
38    Custom,
39}
40
41/// ABR ladder configuration.
42#[derive(Debug, Clone)]
43pub struct AbrLadder {
44    /// The rungs in the ladder, sorted by bitrate (lowest to highest).
45    pub rungs: Vec<AbrRung>,
46    /// Strategy used to generate the ladder.
47    pub strategy: AbrStrategy,
48    /// Maximum resolution to include.
49    pub max_resolution: (u32, u32),
50    /// Minimum resolution to include.
51    pub min_resolution: (u32, u32),
52}
53
54impl AbrRung {
55    /// Creates a new ABR rung.
56    #[must_use]
57    pub fn new(
58        width: u32,
59        height: u32,
60        video_bitrate: u64,
61        audio_bitrate: u64,
62        codec: impl Into<String>,
63        profile_name: impl Into<String>,
64    ) -> Self {
65        Self {
66            width,
67            height,
68            video_bitrate,
69            audio_bitrate,
70            frame_rate: (30, 1),
71            codec: codec.into(),
72            profile_name: profile_name.into(),
73        }
74    }
75
76    /// Sets the frame rate.
77    #[must_use]
78    pub fn with_frame_rate(mut self, num: u32, den: u32) -> Self {
79        self.frame_rate = (num, den);
80        self
81    }
82
83    /// Gets the total bitrate (video + audio).
84    #[must_use]
85    pub fn total_bitrate(&self) -> u64 {
86        self.video_bitrate + self.audio_bitrate
87    }
88
89    /// Gets the resolution as a string (e.g., "1920x1080").
90    #[must_use]
91    pub fn resolution_string(&self) -> String {
92        format!("{}x{}", self.width, self.height)
93    }
94
95    /// Checks if this rung is HD quality or higher (720p+).
96    #[must_use]
97    pub fn is_hd(&self) -> bool {
98        self.height >= 720
99    }
100
101    /// Checks if this rung is Full HD quality or higher (1080p+).
102    #[must_use]
103    pub fn is_full_hd(&self) -> bool {
104        self.height >= 1080
105    }
106
107    /// Checks if this rung is 4K quality or higher (2160p+).
108    #[must_use]
109    pub fn is_4k(&self) -> bool {
110        self.height >= 2160
111    }
112}
113
114impl AbrLadder {
115    /// Creates a new empty ABR ladder.
116    #[must_use]
117    pub fn new(strategy: AbrStrategy) -> Self {
118        Self {
119            rungs: Vec::new(),
120            strategy,
121            max_resolution: (3840, 2160), // 4K
122            min_resolution: (426, 240),   // 240p
123        }
124    }
125
126    /// Adds a rung to the ladder.
127    pub fn add_rung(&mut self, rung: AbrRung) {
128        self.rungs.push(rung);
129        // Keep sorted by total bitrate
130        self.rungs.sort_by_key(AbrRung::total_bitrate);
131    }
132
133    /// Sets the maximum resolution.
134    #[must_use]
135    pub fn with_max_resolution(mut self, width: u32, height: u32) -> Self {
136        self.max_resolution = (width, height);
137        self
138    }
139
140    /// Sets the minimum resolution.
141    #[must_use]
142    pub fn with_min_resolution(mut self, width: u32, height: u32) -> Self {
143        self.min_resolution = (width, height);
144        self
145    }
146
147    /// Generates a standard HLS ladder based on Apple recommendations.
148    #[must_use]
149    pub fn hls_standard() -> Self {
150        let mut ladder = Self::new(AbrStrategy::AppleHls);
151
152        // Apple HLS recommendations
153        ladder.add_rung(AbrRung::new(426, 240, 400_000, 64_000, "h264", "240p"));
154        ladder.add_rung(AbrRung::new(640, 360, 800_000, 96_000, "h264", "360p"));
155        ladder.add_rung(AbrRung::new(854, 480, 1_400_000, 128_000, "h264", "480p"));
156        ladder.add_rung(AbrRung::new(1280, 720, 2_800_000, 128_000, "h264", "720p"));
157        ladder.add_rung(AbrRung::new(
158            1920, 1080, 5_000_000, 192_000, "h264", "1080p",
159        ));
160
161        ladder
162    }
163
164    /// Generates a YouTube-style ABR ladder.
165    #[must_use]
166    pub fn youtube_standard() -> Self {
167        let mut ladder = Self::new(AbrStrategy::YouTube);
168
169        // YouTube recommendations
170        ladder.add_rung(AbrRung::new(426, 240, 300_000, 64_000, "vp9", "240p"));
171        ladder.add_rung(AbrRung::new(640, 360, 700_000, 96_000, "vp9", "360p"));
172        ladder.add_rung(AbrRung::new(854, 480, 1_000_000, 128_000, "vp9", "480p"));
173        ladder.add_rung(AbrRung::new(1280, 720, 2_500_000, 128_000, "vp9", "720p"));
174        ladder.add_rung(AbrRung::new(1920, 1080, 4_500_000, 192_000, "vp9", "1080p"));
175        ladder.add_rung(AbrRung::new(2560, 1440, 9_000_000, 192_000, "vp9", "1440p"));
176
177        ladder
178    }
179
180    /// Generates a conservative ladder (fewer rungs for bandwidth savings).
181    #[must_use]
182    pub fn conservative() -> Self {
183        let mut ladder = Self::new(AbrStrategy::Conservative);
184
185        ladder.add_rung(AbrRung::new(640, 360, 600_000, 96_000, "h264", "360p"));
186        ladder.add_rung(AbrRung::new(1280, 720, 2_000_000, 128_000, "h264", "720p"));
187        ladder.add_rung(AbrRung::new(
188            1920, 1080, 4_000_000, 192_000, "h264", "1080p",
189        ));
190
191        ladder
192    }
193
194    /// Generates an aggressive ladder (more rungs for quality).
195    #[must_use]
196    pub fn aggressive() -> Self {
197        let mut ladder = Self::new(AbrStrategy::Aggressive);
198
199        ladder.add_rung(AbrRung::new(426, 240, 400_000, 64_000, "h264", "240p"));
200        ladder.add_rung(AbrRung::new(640, 360, 800_000, 96_000, "h264", "360p"));
201        ladder.add_rung(AbrRung::new(854, 480, 1_400_000, 128_000, "h264", "480p"));
202        ladder.add_rung(AbrRung::new(960, 540, 2_000_000, 128_000, "h264", "540p"));
203        ladder.add_rung(AbrRung::new(1280, 720, 3_000_000, 128_000, "h264", "720p"));
204        ladder.add_rung(AbrRung::new(
205            1920, 1080, 5_500_000, 192_000, "h264", "1080p",
206        ));
207        ladder.add_rung(AbrRung::new(
208            2560, 1440, 10_000_000, 192_000, "h264", "1440p",
209        ));
210        ladder.add_rung(AbrRung::new(
211            3840, 2160, 20_000_000, 256_000, "h264", "2160p",
212        ));
213
214        ladder
215    }
216
217    /// Filters rungs based on source resolution.
218    ///
219    /// Only includes rungs at or below the source resolution.
220    #[must_use]
221    pub fn filter_by_source(mut self, source_width: u32, source_height: u32) -> Self {
222        self.rungs
223            .retain(|rung| rung.width <= source_width && rung.height <= source_height);
224        self
225    }
226
227    /// Gets the number of rungs in the ladder.
228    #[must_use]
229    pub fn rung_count(&self) -> usize {
230        self.rungs.len()
231    }
232
233    /// Gets a rung by index.
234    #[must_use]
235    pub fn get_rung(&self, index: usize) -> Option<&AbrRung> {
236        self.rungs.get(index)
237    }
238
239    /// Gets the highest quality rung.
240    #[must_use]
241    pub fn highest_quality(&self) -> Option<&AbrRung> {
242        self.rungs.last()
243    }
244
245    /// Gets the lowest quality rung.
246    #[must_use]
247    pub fn lowest_quality(&self) -> Option<&AbrRung> {
248        self.rungs.first()
249    }
250}
251
252/// Builder for creating custom ABR ladders.
253pub struct AbrLadderBuilder {
254    ladder: AbrLadder,
255}
256
257impl AbrLadderBuilder {
258    /// Creates a new builder with the specified strategy.
259    #[must_use]
260    pub fn new(strategy: AbrStrategy) -> Self {
261        Self {
262            ladder: AbrLadder::new(strategy),
263        }
264    }
265
266    /// Adds a rung to the ladder.
267    #[must_use]
268    pub fn add_rung(mut self, rung: AbrRung) -> Self {
269        self.ladder.add_rung(rung);
270        self
271    }
272
273    /// Adds a rung with the specified parameters.
274    #[must_use]
275    pub fn add(
276        mut self,
277        width: u32,
278        height: u32,
279        video_bitrate: u64,
280        audio_bitrate: u64,
281        codec: impl Into<String>,
282        profile_name: impl Into<String>,
283    ) -> Self {
284        let rung = AbrRung::new(
285            width,
286            height,
287            video_bitrate,
288            audio_bitrate,
289            codec,
290            profile_name,
291        );
292        self.ladder.add_rung(rung);
293        self
294    }
295
296    /// Sets the maximum resolution.
297    #[must_use]
298    pub fn max_resolution(mut self, width: u32, height: u32) -> Self {
299        self.ladder.max_resolution = (width, height);
300        self
301    }
302
303    /// Sets the minimum resolution.
304    #[must_use]
305    pub fn min_resolution(mut self, width: u32, height: u32) -> Self {
306        self.ladder.min_resolution = (width, height);
307        self
308    }
309
310    /// Builds the ABR ladder.
311    #[must_use]
312    pub fn build(self) -> AbrLadder {
313        self.ladder
314    }
315}
316
317#[cfg(test)]
318mod tests {
319    use super::*;
320
321    #[test]
322    fn test_abr_rung_creation() {
323        let rung = AbrRung::new(1920, 1080, 5_000_000, 192_000, "h264", "1080p");
324
325        assert_eq!(rung.width, 1920);
326        assert_eq!(rung.height, 1080);
327        assert_eq!(rung.video_bitrate, 5_000_000);
328        assert_eq!(rung.audio_bitrate, 192_000);
329        assert_eq!(rung.total_bitrate(), 5_192_000);
330        assert_eq!(rung.codec, "h264");
331        assert_eq!(rung.profile_name, "1080p");
332    }
333
334    #[test]
335    fn test_abr_rung_quality_checks() {
336        let rung_240p = AbrRung::new(426, 240, 400_000, 64_000, "h264", "240p");
337        assert!(!rung_240p.is_hd());
338        assert!(!rung_240p.is_full_hd());
339        assert!(!rung_240p.is_4k());
340
341        let rung_720p = AbrRung::new(1280, 720, 2_800_000, 128_000, "h264", "720p");
342        assert!(rung_720p.is_hd());
343        assert!(!rung_720p.is_full_hd());
344        assert!(!rung_720p.is_4k());
345
346        let rung_1080p = AbrRung::new(1920, 1080, 5_000_000, 192_000, "h264", "1080p");
347        assert!(rung_1080p.is_hd());
348        assert!(rung_1080p.is_full_hd());
349        assert!(!rung_1080p.is_4k());
350
351        let rung_4k = AbrRung::new(3840, 2160, 20_000_000, 256_000, "h264", "2160p");
352        assert!(rung_4k.is_hd());
353        assert!(rung_4k.is_full_hd());
354        assert!(rung_4k.is_4k());
355    }
356
357    #[test]
358    fn test_abr_rung_resolution_string() {
359        let rung = AbrRung::new(1920, 1080, 5_000_000, 192_000, "h264", "1080p");
360        assert_eq!(rung.resolution_string(), "1920x1080");
361    }
362
363    #[test]
364    fn test_hls_standard_ladder() {
365        let ladder = AbrLadder::hls_standard();
366        assert_eq!(ladder.rung_count(), 5);
367        assert_eq!(ladder.strategy, AbrStrategy::AppleHls);
368
369        let lowest = ladder.lowest_quality().expect("should succeed in test");
370        assert_eq!(lowest.profile_name, "240p");
371
372        let highest = ladder.highest_quality().expect("should succeed in test");
373        assert_eq!(highest.profile_name, "1080p");
374    }
375
376    #[test]
377    fn test_youtube_standard_ladder() {
378        let ladder = AbrLadder::youtube_standard();
379        assert_eq!(ladder.rung_count(), 6);
380        assert_eq!(ladder.strategy, AbrStrategy::YouTube);
381
382        let highest = ladder.highest_quality().expect("should succeed in test");
383        assert_eq!(highest.profile_name, "1440p");
384    }
385
386    #[test]
387    fn test_conservative_ladder() {
388        let ladder = AbrLadder::conservative();
389        assert_eq!(ladder.rung_count(), 3);
390        assert_eq!(ladder.strategy, AbrStrategy::Conservative);
391    }
392
393    #[test]
394    fn test_aggressive_ladder() {
395        let ladder = AbrLadder::aggressive();
396        assert_eq!(ladder.rung_count(), 8);
397        assert_eq!(ladder.strategy, AbrStrategy::Aggressive);
398    }
399
400    #[test]
401    fn test_ladder_filtering() {
402        let ladder = AbrLadder::hls_standard();
403        let filtered = ladder.filter_by_source(1280, 720);
404
405        assert_eq!(filtered.rung_count(), 4); // 240p, 360p, 480p, 720p
406        let highest = filtered.highest_quality().expect("should succeed in test");
407        assert_eq!(highest.profile_name, "720p");
408    }
409
410    #[test]
411    fn test_ladder_builder() {
412        let ladder = AbrLadderBuilder::new(AbrStrategy::Custom)
413            .add(640, 360, 800_000, 96_000, "h264", "360p")
414            .add(1280, 720, 2_800_000, 128_000, "h264", "720p")
415            .add(1920, 1080, 5_000_000, 192_000, "h264", "1080p")
416            .max_resolution(1920, 1080)
417            .min_resolution(640, 360)
418            .build();
419
420        assert_eq!(ladder.rung_count(), 3);
421        assert_eq!(ladder.strategy, AbrStrategy::Custom);
422    }
423
424    #[test]
425    fn test_ladder_sorting() {
426        let mut ladder = AbrLadder::new(AbrStrategy::Custom);
427
428        // Add rungs in reverse order
429        ladder.add_rung(AbrRung::new(
430            1920, 1080, 5_000_000, 192_000, "h264", "1080p",
431        ));
432        ladder.add_rung(AbrRung::new(640, 360, 800_000, 96_000, "h264", "360p"));
433        ladder.add_rung(AbrRung::new(1280, 720, 2_800_000, 128_000, "h264", "720p"));
434
435        // Should be sorted by bitrate
436        assert_eq!(ladder.rungs[0].profile_name, "360p");
437        assert_eq!(ladder.rungs[1].profile_name, "720p");
438        assert_eq!(ladder.rungs[2].profile_name, "1080p");
439    }
440}