Skip to main content

oximedia_transcode/
adaptive_bitrate.rs

1//! Adaptive bitrate ladder generation for HLS/DASH streaming.
2//!
3//! This module provides tools for generating Netflix-style ABR ladders,
4//! per-title encoding optimization, and bandwidth estimation.
5
6use std::collections::VecDeque;
7
8/// A single rendition in an ABR ladder.
9#[derive(Debug, Clone, PartialEq, Eq)]
10pub struct ResolutionBitrate {
11    /// Video width in pixels.
12    pub width: u32,
13    /// Video height in pixels.
14    pub height: u32,
15    /// Target bitrate in kilobits per second.
16    pub bitrate_kbps: u32,
17    /// Codec name (e.g., "h264", "vp9", "av1").
18    pub codec: String,
19}
20
21impl ResolutionBitrate {
22    /// Creates a new resolution/bitrate rendition.
23    #[must_use]
24    pub fn new(width: u32, height: u32, bitrate_kbps: u32, codec: impl Into<String>) -> Self {
25        Self {
26            width,
27            height,
28            bitrate_kbps,
29            codec: codec.into(),
30        }
31    }
32
33    /// Returns the pixel count for this rendition.
34    #[must_use]
35    pub fn pixel_count(&self) -> u64 {
36        u64::from(self.width) * u64::from(self.height)
37    }
38
39    /// Returns the aspect ratio as a float.
40    #[must_use]
41    pub fn aspect_ratio(&self) -> f32 {
42        self.width as f32 / self.height as f32
43    }
44}
45
46/// An adaptive bitrate ladder containing multiple renditions.
47#[derive(Debug, Clone)]
48pub struct AbrLadder {
49    /// Renditions sorted by bitrate descending.
50    pub renditions: Vec<ResolutionBitrate>,
51}
52
53impl AbrLadder {
54    /// Creates a new ABR ladder.
55    #[must_use]
56    pub fn new(mut renditions: Vec<ResolutionBitrate>) -> Self {
57        // Sort descending by bitrate
58        renditions.sort_by(|a, b| b.bitrate_kbps.cmp(&a.bitrate_kbps));
59        Self { renditions }
60    }
61
62    /// Selects the optimal rendition for a given available bandwidth.
63    ///
64    /// Returns the highest-bitrate rendition whose bitrate is ≤ 80% of available bandwidth.
65    #[must_use]
66    pub fn optimal_bitrate_kbps(&self, bandwidth_kbps: u32) -> Option<&ResolutionBitrate> {
67        let threshold = (f64::from(bandwidth_kbps) * 0.8) as u32;
68        self.renditions.iter().find(|r| r.bitrate_kbps <= threshold)
69    }
70
71    /// Returns the number of renditions in the ladder.
72    #[must_use]
73    pub fn len(&self) -> usize {
74        self.renditions.len()
75    }
76
77    /// Returns true if the ladder has no renditions.
78    #[must_use]
79    pub fn is_empty(&self) -> bool {
80        self.renditions.is_empty()
81    }
82
83    /// Returns the highest-quality rendition.
84    #[must_use]
85    pub fn highest_quality(&self) -> Option<&ResolutionBitrate> {
86        self.renditions.first()
87    }
88
89    /// Returns the lowest-quality rendition.
90    #[must_use]
91    pub fn lowest_quality(&self) -> Option<&ResolutionBitrate> {
92        self.renditions.last()
93    }
94}
95
96/// Generator for ABR ladders targeting specific resolutions and codecs.
97#[derive(Debug, Clone, Default)]
98pub struct AbrLadderGenerator;
99
100impl AbrLadderGenerator {
101    /// Creates a new `AbrLadderGenerator`.
102    #[must_use]
103    pub fn new() -> Self {
104        Self
105    }
106
107    /// Generates a Netflix-style ABR ladder for the given target resolution and codec.
108    ///
109    /// Standard ladder: 1080p/8000, 720p/4500, 540p/2000, 360p/800, 240p/300 kbps.
110    /// Only renditions at or below the target resolution are included.
111    #[must_use]
112    pub fn generate(&self, target_resolution: (u32, u32), codec: &str) -> AbrLadder {
113        let (_target_w, target_h) = target_resolution;
114
115        // Netflix-style standard ladder
116        let standard_rungs: &[(u32, u32, u32)] = &[
117            (1920, 1080, 8000),
118            (1280, 720, 4500),
119            (960, 540, 2000),
120            (640, 360, 800),
121            (426, 240, 300),
122        ];
123
124        let renditions = standard_rungs
125            .iter()
126            .filter(|(_, h, _)| *h <= target_h)
127            .map(|(w, h, kbps)| ResolutionBitrate::new(*w, *h, *kbps, codec))
128            .collect();
129
130        AbrLadder::new(renditions)
131    }
132}
133
134/// Per-title encoding optimization for content-aware ABR ladders.
135#[derive(Debug, Clone, Default)]
136pub struct PerTitleEncoding;
137
138impl PerTitleEncoding {
139    /// Creates a new `PerTitleEncoding` optimizer.
140    #[must_use]
141    pub fn new() -> Self {
142        Self
143    }
144
145    /// Analyzes content complexity from frame variance values.
146    ///
147    /// Returns the mean variance as a complexity score.
148    #[must_use]
149    pub fn analyze_complexity(frame_variance: &[f32]) -> f32 {
150        if frame_variance.is_empty() {
151            return 0.0;
152        }
153        let sum: f32 = frame_variance.iter().sum();
154        sum / frame_variance.len() as f32
155    }
156
157    /// Optimizes a base ABR ladder based on content complexity.
158    ///
159    /// Complexity factor is clamped to 0.5–2.0. Bitrates are scaled accordingly.
160    #[must_use]
161    pub fn optimize_ladder(base_ladder: &AbrLadder, complexity: f32) -> AbrLadder {
162        // Clamp complexity factor to [0.5, 2.0]
163        let factor = complexity.clamp(0.5, 2.0);
164
165        let adjusted = base_ladder
166            .renditions
167            .iter()
168            .map(|r| {
169                let new_bitrate = ((r.bitrate_kbps as f32) * factor).round() as u32;
170                ResolutionBitrate::new(r.width, r.height, new_bitrate, r.codec.clone())
171            })
172            .collect();
173
174        AbrLadder::new(adjusted)
175    }
176}
177
178/// A bandwidth measurement at a point in time.
179#[derive(Debug, Clone, Copy, PartialEq, Eq)]
180pub struct BandwidthPoint {
181    /// Timestamp in milliseconds.
182    pub timestamp_ms: u64,
183    /// Measured bandwidth in kilobits per second.
184    pub kbps: u32,
185}
186
187/// Ring buffer of recent bandwidth samples (last 30 samples).
188#[derive(Debug, Clone)]
189pub struct BandwidthEstimator {
190    samples: VecDeque<BandwidthPoint>,
191    capacity: usize,
192}
193
194impl BandwidthEstimator {
195    /// Creates a new bandwidth estimator with a 30-sample buffer.
196    #[must_use]
197    pub fn new() -> Self {
198        Self {
199            samples: VecDeque::with_capacity(30),
200            capacity: 30,
201        }
202    }
203
204    /// Adds a bandwidth sample, evicting oldest if at capacity.
205    pub fn add_sample(&mut self, point: BandwidthPoint) {
206        if self.samples.len() >= self.capacity {
207            self.samples.pop_front();
208        }
209        self.samples.push_back(point);
210    }
211
212    /// Returns the number of samples in the buffer.
213    #[must_use]
214    pub fn sample_count(&self) -> usize {
215        self.samples.len()
216    }
217
218    /// Returns the estimated bandwidth as the average of recent samples.
219    #[must_use]
220    pub fn estimated_kbps(&self) -> Option<u32> {
221        if self.samples.is_empty() {
222            return None;
223        }
224        let sum: u64 = self.samples.iter().map(|s| u64::from(s.kbps)).sum();
225        Some((sum / self.samples.len() as u64) as u32)
226    }
227
228    /// Returns all samples in the buffer.
229    #[must_use]
230    pub fn samples(&self) -> &VecDeque<BandwidthPoint> {
231        &self.samples
232    }
233}
234
235impl Default for BandwidthEstimator {
236    fn default() -> Self {
237        Self::new()
238    }
239}
240
241#[cfg(test)]
242mod tests {
243    use super::*;
244
245    #[test]
246    fn test_resolution_bitrate_new() {
247        let r = ResolutionBitrate::new(1920, 1080, 8000, "h264");
248        assert_eq!(r.width, 1920);
249        assert_eq!(r.height, 1080);
250        assert_eq!(r.bitrate_kbps, 8000);
251        assert_eq!(r.codec, "h264");
252    }
253
254    #[test]
255    fn test_resolution_bitrate_pixel_count() {
256        let r = ResolutionBitrate::new(1920, 1080, 8000, "h264");
257        assert_eq!(r.pixel_count(), 1920 * 1080);
258    }
259
260    #[test]
261    fn test_resolution_bitrate_aspect_ratio() {
262        let r = ResolutionBitrate::new(1920, 1080, 8000, "h264");
263        let ar = r.aspect_ratio();
264        assert!((ar - 16.0 / 9.0).abs() < 0.01);
265    }
266
267    #[test]
268    fn test_abr_ladder_sorted_descending() {
269        let renditions = vec![
270            ResolutionBitrate::new(640, 360, 800, "h264"),
271            ResolutionBitrate::new(1920, 1080, 8000, "h264"),
272            ResolutionBitrate::new(1280, 720, 4500, "h264"),
273        ];
274        let ladder = AbrLadder::new(renditions);
275        assert_eq!(ladder.renditions[0].bitrate_kbps, 8000);
276        assert_eq!(ladder.renditions[1].bitrate_kbps, 4500);
277        assert_eq!(ladder.renditions[2].bitrate_kbps, 800);
278    }
279
280    #[test]
281    fn test_abr_ladder_optimal_bitrate() {
282        let gen = AbrLadderGenerator::new();
283        let ladder = gen.generate((1920, 1080), "h264");
284
285        // 80% of 10000 = 8000 → exactly 8000 qualifies
286        let opt = ladder.optimal_bitrate_kbps(10000);
287        assert!(opt.is_some());
288        assert_eq!(opt.expect("should succeed in test").bitrate_kbps, 8000);
289
290        // 80% of 5000 = 4000 → best is 2000
291        let opt2 = ladder.optimal_bitrate_kbps(5000);
292        assert!(opt2.is_some());
293        assert_eq!(opt2.expect("should succeed in test").bitrate_kbps, 2000);
294    }
295
296    #[test]
297    fn test_abr_ladder_optimal_bitrate_too_low() {
298        let gen = AbrLadderGenerator::new();
299        let ladder = gen.generate((1920, 1080), "h264");
300
301        // 80% of 100 = 80 → none qualify
302        let opt = ladder.optimal_bitrate_kbps(100);
303        assert!(opt.is_none());
304    }
305
306    #[test]
307    fn test_abr_ladder_empty_check() {
308        let ladder = AbrLadder::new(vec![]);
309        assert!(ladder.is_empty());
310        assert_eq!(ladder.len(), 0);
311        assert!(ladder.highest_quality().is_none());
312        assert!(ladder.lowest_quality().is_none());
313    }
314
315    #[test]
316    fn test_abr_ladder_generator_full_1080p() {
317        let gen = AbrLadderGenerator::new();
318        let ladder = gen.generate((1920, 1080), "h264");
319        assert_eq!(ladder.len(), 5);
320        assert_eq!(
321            ladder
322                .highest_quality()
323                .expect("should succeed in test")
324                .bitrate_kbps,
325            8000
326        );
327        assert_eq!(
328            ladder
329                .lowest_quality()
330                .expect("should succeed in test")
331                .bitrate_kbps,
332            300
333        );
334    }
335
336    #[test]
337    fn test_abr_ladder_generator_720p_limit() {
338        let gen = AbrLadderGenerator::new();
339        let ladder = gen.generate((1280, 720), "vp9");
340        // Should only include 720p, 540p, 360p, 240p (not 1080p)
341        for r in &ladder.renditions {
342            assert!(r.height <= 720);
343        }
344    }
345
346    #[test]
347    fn test_per_title_analyze_complexity_empty() {
348        let c = PerTitleEncoding::analyze_complexity(&[]);
349        assert_eq!(c, 0.0);
350    }
351
352    #[test]
353    fn test_per_title_analyze_complexity() {
354        let variances = vec![1.0, 2.0, 3.0, 4.0];
355        let c = PerTitleEncoding::analyze_complexity(&variances);
356        assert!((c - 2.5).abs() < 1e-5);
357    }
358
359    #[test]
360    fn test_per_title_optimize_ladder_clamping() {
361        let gen = AbrLadderGenerator::new();
362        let base = gen.generate((1920, 1080), "h264");
363
364        // Complexity 3.0 should be clamped to 2.0
365        let optimized = PerTitleEncoding::optimize_ladder(&base, 3.0);
366        let base_highest = base
367            .highest_quality()
368            .expect("should succeed in test")
369            .bitrate_kbps;
370        let opt_highest = optimized
371            .highest_quality()
372            .expect("should succeed in test")
373            .bitrate_kbps;
374        assert_eq!(opt_highest, (base_highest as f32 * 2.0).round() as u32);
375    }
376
377    #[test]
378    fn test_per_title_optimize_ladder_lower() {
379        let gen = AbrLadderGenerator::new();
380        let base = gen.generate((1920, 1080), "h264");
381
382        // Complexity 0.5 halves bitrates
383        let optimized = PerTitleEncoding::optimize_ladder(&base, 0.5);
384        let base_highest = base
385            .highest_quality()
386            .expect("should succeed in test")
387            .bitrate_kbps;
388        let opt_highest = optimized
389            .highest_quality()
390            .expect("should succeed in test")
391            .bitrate_kbps;
392        assert_eq!(opt_highest, (base_highest as f32 * 0.5).round() as u32);
393    }
394
395    #[test]
396    fn test_bandwidth_estimator_add_sample() {
397        let mut est = BandwidthEstimator::new();
398        assert_eq!(est.sample_count(), 0);
399        assert!(est.estimated_kbps().is_none());
400
401        est.add_sample(BandwidthPoint {
402            timestamp_ms: 0,
403            kbps: 5000,
404        });
405        assert_eq!(est.sample_count(), 1);
406        assert_eq!(est.estimated_kbps(), Some(5000));
407    }
408
409    #[test]
410    fn test_bandwidth_estimator_ring_buffer() {
411        let mut est = BandwidthEstimator::new();
412        for i in 0..35u64 {
413            est.add_sample(BandwidthPoint {
414                timestamp_ms: i * 1000,
415                kbps: 1000,
416            });
417        }
418        // Should only keep last 30
419        assert_eq!(est.sample_count(), 30);
420    }
421
422    #[test]
423    fn test_bandwidth_estimator_average() {
424        let mut est = BandwidthEstimator::new();
425        est.add_sample(BandwidthPoint {
426            timestamp_ms: 0,
427            kbps: 4000,
428        });
429        est.add_sample(BandwidthPoint {
430            timestamp_ms: 1000,
431            kbps: 6000,
432        });
433        assert_eq!(est.estimated_kbps(), Some(5000));
434    }
435}