oximedia_transcode/
resolution_select.rs1#![allow(dead_code)]
7
8use serde::{Deserialize, Serialize};
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
12pub enum ResolutionTier {
13 Sd,
15 Hd,
17 FullHd,
19 Uhd4k,
21}
22
23impl ResolutionTier {
24 #[must_use]
26 pub fn pixel_count(self) -> u64 {
27 let (w, h) = self.dimensions();
28 u64::from(w) * u64::from(h)
29 }
30
31 #[must_use]
33 pub fn dimensions(self) -> (u32, u32) {
34 match self {
35 Self::Sd => (854, 480),
36 Self::Hd => (1280, 720),
37 Self::FullHd => (1920, 1080),
38 Self::Uhd4k => (3840, 2160),
39 }
40 }
41
42 #[must_use]
44 pub fn recommended_bitrate_bps(self) -> u64 {
45 match self {
46 Self::Sd => 1_500_000,
47 Self::Hd => 4_000_000,
48 Self::FullHd => 8_000_000,
49 Self::Uhd4k => 40_000_000,
50 }
51 }
52
53 #[must_use]
55 pub fn label(self) -> &'static str {
56 match self {
57 Self::Sd => "480p",
58 Self::Hd => "720p",
59 Self::FullHd => "1080p",
60 Self::Uhd4k => "2160p",
61 }
62 }
63
64 #[must_use]
66 pub fn all_tiers() -> [Self; 4] {
67 [Self::Sd, Self::Hd, Self::FullHd, Self::Uhd4k]
68 }
69}
70
71#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
73pub enum SelectionStrategy {
74 BandwidthFit,
76 MaxQuality,
78 MinBandwidth,
80 Exact(ResolutionTier),
82}
83
84#[derive(Debug, Clone, Serialize, Deserialize)]
86pub struct ResolutionSelector {
87 pub max_bandwidth_bps: u64,
89 pub min_tier: ResolutionTier,
91 pub max_tier: ResolutionTier,
93 pub headroom_factor: f64,
96}
97
98impl Default for ResolutionSelector {
99 fn default() -> Self {
100 Self {
101 max_bandwidth_bps: 10_000_000,
102 min_tier: ResolutionTier::Sd,
103 max_tier: ResolutionTier::Uhd4k,
104 headroom_factor: 1.2,
105 }
106 }
107}
108
109impl ResolutionSelector {
110 #[must_use]
112 pub fn new(max_bandwidth_bps: u64) -> Self {
113 Self {
114 max_bandwidth_bps,
115 ..Self::default()
116 }
117 }
118
119 #[must_use]
121 pub fn with_min_tier(mut self, tier: ResolutionTier) -> Self {
122 self.min_tier = tier;
123 self
124 }
125
126 #[must_use]
128 pub fn with_max_tier(mut self, tier: ResolutionTier) -> Self {
129 self.max_tier = tier;
130 self
131 }
132
133 #[must_use]
135 pub fn with_headroom(mut self, factor: f64) -> Self {
136 self.headroom_factor = factor.max(1.0);
137 self
138 }
139
140 #[allow(clippy::cast_precision_loss)]
144 #[must_use]
145 pub fn select_for_bandwidth(&self) -> Option<ResolutionTier> {
146 let budget = self.max_bandwidth_bps as f64 / self.headroom_factor;
147 let mut best: Option<ResolutionTier> = None;
148 for tier in ResolutionTier::all_tiers() {
149 if tier < self.min_tier || tier > self.max_tier {
150 continue;
151 }
152 if tier.recommended_bitrate_bps() as f64 <= budget {
153 best = Some(tier);
154 }
155 }
156 best
157 }
158
159 #[must_use]
161 pub fn select_for_quality(&self) -> ResolutionTier {
162 ResolutionTier::all_tiers()
163 .iter()
164 .filter(|&&t| t >= self.min_tier && t <= self.max_tier)
165 .copied()
166 .last()
167 .unwrap_or(self.min_tier)
168 }
169
170 #[must_use]
172 pub fn all_tiers(&self) -> Vec<ResolutionTier> {
173 ResolutionTier::all_tiers()
174 .iter()
175 .filter(|&&t| t >= self.min_tier && t <= self.max_tier)
176 .copied()
177 .collect()
178 }
179
180 #[allow(clippy::cast_precision_loss)]
182 #[must_use]
183 pub fn abr_ladder(&self) -> Vec<ResolutionTier> {
184 let budget = self.max_bandwidth_bps as f64 / self.headroom_factor;
185 ResolutionTier::all_tiers()
186 .iter()
187 .filter(|&&t| {
188 t >= self.min_tier
189 && t <= self.max_tier
190 && t.recommended_bitrate_bps() as f64 <= budget
191 })
192 .copied()
193 .collect()
194 }
195}
196
197#[cfg(test)]
198mod tests {
199 use super::*;
200
201 #[test]
202 fn test_pixel_count_ordering() {
203 assert!(ResolutionTier::Sd.pixel_count() < ResolutionTier::Hd.pixel_count());
204 assert!(ResolutionTier::Hd.pixel_count() < ResolutionTier::FullHd.pixel_count());
205 assert!(ResolutionTier::FullHd.pixel_count() < ResolutionTier::Uhd4k.pixel_count());
206 }
207
208 #[test]
209 fn test_sd_dimensions() {
210 assert_eq!(ResolutionTier::Sd.dimensions(), (854, 480));
211 }
212
213 #[test]
214 fn test_uhd4k_dimensions() {
215 assert_eq!(ResolutionTier::Uhd4k.dimensions(), (3840, 2160));
216 }
217
218 #[test]
219 fn test_recommended_bitrate_increases_with_tier() {
220 let tiers = ResolutionTier::all_tiers();
221 for window in tiers.windows(2) {
222 assert!(window[0].recommended_bitrate_bps() < window[1].recommended_bitrate_bps());
223 }
224 }
225
226 #[test]
227 fn test_labels_non_empty() {
228 for t in ResolutionTier::all_tiers() {
229 assert!(!t.label().is_empty());
230 }
231 }
232
233 #[test]
234 fn test_all_tiers_count() {
235 assert_eq!(ResolutionTier::all_tiers().len(), 4);
236 }
237
238 #[test]
239 fn test_select_for_bandwidth_high_budget() {
240 let sel = ResolutionSelector::new(100_000_000);
241 assert_eq!(sel.select_for_bandwidth(), Some(ResolutionTier::Uhd4k));
242 }
243
244 #[test]
245 fn test_select_for_bandwidth_low_budget() {
246 let sel = ResolutionSelector::new(1_000_000).with_headroom(1.0);
248 assert!(sel.select_for_bandwidth().is_none());
249 }
250
251 #[test]
252 fn test_select_for_bandwidth_mid_budget() {
253 let sel = ResolutionSelector::new(5_000_000).with_headroom(1.0);
255 assert_eq!(sel.select_for_bandwidth(), Some(ResolutionTier::Hd));
256 }
257
258 #[test]
259 fn test_select_for_quality_returns_max_tier() {
260 let sel = ResolutionSelector::default().with_max_tier(ResolutionTier::FullHd);
261 assert_eq!(sel.select_for_quality(), ResolutionTier::FullHd);
262 }
263
264 #[test]
265 fn test_all_tiers_bounded() {
266 let sel = ResolutionSelector::default()
267 .with_min_tier(ResolutionTier::Hd)
268 .with_max_tier(ResolutionTier::FullHd);
269 let tiers = sel.all_tiers();
270 assert_eq!(tiers, vec![ResolutionTier::Hd, ResolutionTier::FullHd]);
271 }
272
273 #[test]
274 fn test_abr_ladder_large_budget() {
275 let sel = ResolutionSelector::new(100_000_000).with_headroom(1.0);
276 assert_eq!(sel.abr_ladder().len(), 4);
277 }
278
279 #[test]
280 fn test_abr_ladder_small_budget() {
281 let sel = ResolutionSelector::new(4_000_000).with_headroom(1.0);
283 let ladder = sel.abr_ladder();
284 assert!(ladder.contains(&ResolutionTier::Hd));
285 assert!(!ladder.contains(&ResolutionTier::FullHd));
286 }
287
288 #[test]
289 fn test_tier_ordering() {
290 assert!(ResolutionTier::Sd < ResolutionTier::Hd);
291 assert!(ResolutionTier::Hd < ResolutionTier::FullHd);
292 assert!(ResolutionTier::FullHd < ResolutionTier::Uhd4k);
293 }
294}