1use serde::{Deserialize, Serialize};
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
12pub enum LadderPreset {
13 Broadcast,
15 WebVod,
17 Mobile,
19 Ultra4k,
21 Archive,
23 Preview,
25}
26
27impl LadderPreset {
28 #[must_use]
30 pub fn label(&self) -> &'static str {
31 match self {
32 Self::Broadcast => "Broadcast",
33 Self::WebVod => "WebVOD",
34 Self::Mobile => "Mobile",
35 Self::Ultra4k => "Ultra4K",
36 Self::Archive => "Archive",
37 Self::Preview => "Preview",
38 }
39 }
40}
41
42#[derive(Debug, Clone, Serialize, Deserialize)]
46pub struct BitrateRung {
47 pub height: u32,
49 pub width: u32,
51 pub bitrate_kbps: u32,
53 pub codec: String,
55 pub audio_kbps: u32,
57}
58
59impl BitrateRung {
60 #[must_use]
62 pub fn new(
63 height: u32,
64 width: u32,
65 bitrate_kbps: u32,
66 codec: impl Into<String>,
67 audio_kbps: u32,
68 ) -> Self {
69 Self {
70 height,
71 width,
72 bitrate_kbps,
73 codec: codec.into(),
74 audio_kbps,
75 }
76 }
77
78 #[must_use]
80 pub fn total_kbps(&self) -> u32 {
81 self.bitrate_kbps.saturating_add(self.audio_kbps)
82 }
83
84 #[must_use]
86 pub fn pixels(&self) -> u64 {
87 self.height as u64 * self.width as u64
88 }
89}
90
91#[derive(Debug, Clone, Serialize, Deserialize)]
95pub struct LadderSpec {
96 pub preset: LadderPreset,
98 pub rungs: Vec<BitrateRung>,
100 pub min_rungs: u8,
102 pub max_rungs: u8,
104}
105
106impl LadderSpec {
107 #[must_use]
109 pub fn rung_count(&self) -> usize {
110 self.rungs.len()
111 }
112
113 #[must_use]
115 pub fn top_rung(&self) -> Option<&BitrateRung> {
116 self.rungs.first()
117 }
118
119 #[must_use]
121 pub fn bottom_rung(&self) -> Option<&BitrateRung> {
122 self.rungs.last()
123 }
124}
125
126#[must_use]
135pub fn vmaf_estimate_for_bitrate(height: u32, bitrate_kbps: u32) -> f32 {
136 if height == 0 || bitrate_kbps == 0 {
137 return 0.0;
138 }
139 let reference_bitrate = (height as f64 * height as f64) / 100.0;
140 let exponent = -(bitrate_kbps as f64 / reference_bitrate);
141 let vmaf = 95.0 * (1.0 - exponent.exp());
142 vmaf.clamp(0.0, 100.0) as f32
143}
144
145#[derive(Debug, Clone, Default)]
149pub struct LadderGenerator;
150
151struct CandidateRung {
153 height: u32,
154 width: u32,
155 bitrate_kbps: u32,
156 audio_kbps: u32,
157}
158
159impl LadderGenerator {
160 #[must_use]
162 pub fn new() -> Self {
163 Self
164 }
165
166 #[must_use]
170 pub fn generate(
171 &self,
172 input_height: u32,
173 input_width: u32,
174 preset: LadderPreset,
175 ) -> LadderSpec {
176 let codec = self.default_codec(preset);
177 let candidates = self.candidate_rungs(preset);
178
179 let rungs: Vec<BitrateRung> = candidates
181 .into_iter()
182 .filter(|r| r.height <= input_height)
183 .map(|r| {
184 let canonical_width = r.height * 16 / 9;
187 let effective_width = if input_width < canonical_width {
188 input_width
189 } else {
190 r.width
191 };
192 BitrateRung::new(
193 r.height,
194 effective_width,
195 r.bitrate_kbps,
196 codec,
197 r.audio_kbps,
198 )
199 })
200 .collect();
201
202 let (min_rungs, max_rungs) = self.rung_limits(preset);
203
204 LadderSpec {
205 preset,
206 rungs,
207 min_rungs,
208 max_rungs,
209 }
210 }
211
212 fn default_codec(&self, preset: LadderPreset) -> &'static str {
214 match preset {
215 LadderPreset::Archive => "av1",
216 LadderPreset::Ultra4k => "av1",
217 LadderPreset::Preview => "vp9",
218 _ => "vp9",
219 }
220 }
221
222 fn rung_limits(&self, preset: LadderPreset) -> (u8, u8) {
224 match preset {
225 LadderPreset::Mobile => (2, 4),
226 LadderPreset::Preview => (1, 2),
227 LadderPreset::Ultra4k => (3, 6),
228 LadderPreset::Archive => (2, 5),
229 _ => (2, 5),
230 }
231 }
232
233 fn candidate_rungs(&self, preset: LadderPreset) -> Vec<CandidateRung> {
235 match preset {
236 LadderPreset::Broadcast => vec![
237 CandidateRung {
238 height: 2160,
239 width: 3840,
240 bitrate_kbps: 20_000,
241 audio_kbps: 320,
242 },
243 CandidateRung {
244 height: 1080,
245 width: 1920,
246 bitrate_kbps: 8_000,
247 audio_kbps: 192,
248 },
249 CandidateRung {
250 height: 720,
251 width: 1280,
252 bitrate_kbps: 4_000,
253 audio_kbps: 128,
254 },
255 CandidateRung {
256 height: 540,
257 width: 960,
258 bitrate_kbps: 2_000,
259 audio_kbps: 128,
260 },
261 CandidateRung {
262 height: 360,
263 width: 640,
264 bitrate_kbps: 800,
265 audio_kbps: 96,
266 },
267 ],
268 LadderPreset::WebVod => vec![
269 CandidateRung {
270 height: 1080,
271 width: 1920,
272 bitrate_kbps: 4_500,
273 audio_kbps: 192,
274 },
275 CandidateRung {
276 height: 720,
277 width: 1280,
278 bitrate_kbps: 2_500,
279 audio_kbps: 128,
280 },
281 CandidateRung {
282 height: 480,
283 width: 854,
284 bitrate_kbps: 1_200,
285 audio_kbps: 128,
286 },
287 CandidateRung {
288 height: 360,
289 width: 640,
290 bitrate_kbps: 600,
291 audio_kbps: 96,
292 },
293 CandidateRung {
294 height: 240,
295 width: 426,
296 bitrate_kbps: 300,
297 audio_kbps: 64,
298 },
299 ],
300 LadderPreset::Mobile => vec![
301 CandidateRung {
302 height: 720,
303 width: 1280,
304 bitrate_kbps: 2_000,
305 audio_kbps: 128,
306 },
307 CandidateRung {
308 height: 480,
309 width: 854,
310 bitrate_kbps: 1_000,
311 audio_kbps: 96,
312 },
313 CandidateRung {
314 height: 360,
315 width: 640,
316 bitrate_kbps: 500,
317 audio_kbps: 64,
318 },
319 CandidateRung {
320 height: 240,
321 width: 426,
322 bitrate_kbps: 200,
323 audio_kbps: 48,
324 },
325 ],
326 LadderPreset::Ultra4k => vec![
327 CandidateRung {
328 height: 2160,
329 width: 3840,
330 bitrate_kbps: 35_000,
331 audio_kbps: 320,
332 },
333 CandidateRung {
334 height: 1440,
335 width: 2560,
336 bitrate_kbps: 16_000,
337 audio_kbps: 256,
338 },
339 CandidateRung {
340 height: 1080,
341 width: 1920,
342 bitrate_kbps: 8_000,
343 audio_kbps: 192,
344 },
345 CandidateRung {
346 height: 720,
347 width: 1280,
348 bitrate_kbps: 4_000,
349 audio_kbps: 128,
350 },
351 CandidateRung {
352 height: 480,
353 width: 854,
354 bitrate_kbps: 1_500,
355 audio_kbps: 128,
356 },
357 ],
358 LadderPreset::Archive => vec![
359 CandidateRung {
360 height: 2160,
361 width: 3840,
362 bitrate_kbps: 15_000,
363 audio_kbps: 256,
364 },
365 CandidateRung {
366 height: 1080,
367 width: 1920,
368 bitrate_kbps: 6_000,
369 audio_kbps: 192,
370 },
371 CandidateRung {
372 height: 720,
373 width: 1280,
374 bitrate_kbps: 3_000,
375 audio_kbps: 128,
376 },
377 CandidateRung {
378 height: 480,
379 width: 854,
380 bitrate_kbps: 1_200,
381 audio_kbps: 96,
382 },
383 ],
384 LadderPreset::Preview => vec![
385 CandidateRung {
386 height: 480,
387 width: 854,
388 bitrate_kbps: 400,
389 audio_kbps: 64,
390 },
391 CandidateRung {
392 height: 240,
393 width: 426,
394 bitrate_kbps: 150,
395 audio_kbps: 32,
396 },
397 ],
398 }
399 }
400}
401
402#[derive(Debug, Clone)]
407pub struct LadderOptimizer {
408 pub vmaf_equivalence_threshold: f32,
410 pub vmaf_gap_threshold: f32,
412}
413
414impl Default for LadderOptimizer {
415 fn default() -> Self {
416 Self {
417 vmaf_equivalence_threshold: 5.0,
418 vmaf_gap_threshold: 10.0,
419 }
420 }
421}
422
423impl LadderOptimizer {
424 #[must_use]
426 pub fn new() -> Self {
427 Self::default()
428 }
429
430 #[must_use]
432 pub fn with_thresholds(equivalence: f32, gap: f32) -> Self {
433 Self {
434 vmaf_equivalence_threshold: equivalence,
435 vmaf_gap_threshold: gap,
436 }
437 }
438
439 #[must_use]
442 pub fn optimize(&self, spec: LadderSpec) -> LadderSpec {
443 if spec.rungs.is_empty() {
444 return spec;
445 }
446
447 let optimized_rungs = self.remove_equivalent_rungs(spec.rungs);
448 let optimized_rungs = self.fill_large_gaps(optimized_rungs);
449
450 LadderSpec {
451 rungs: optimized_rungs,
452 ..spec
453 }
454 }
455
456 fn remove_equivalent_rungs(&self, rungs: Vec<BitrateRung>) -> Vec<BitrateRung> {
458 if rungs.is_empty() {
459 return rungs;
460 }
461
462 let mut kept: Vec<BitrateRung> = Vec::with_capacity(rungs.len());
463 kept.push(rungs.into_iter().next().expect("non-empty"));
464
465 kept
472 }
473
474 fn fill_large_gaps(&self, rungs: Vec<BitrateRung>) -> Vec<BitrateRung> {
476 if rungs.len() < 2 {
477 return rungs;
478 }
479
480 let mut result: Vec<BitrateRung> = Vec::with_capacity(rungs.len() * 2);
481 let mut iter = rungs.into_iter().peekable();
482
483 while let Some(rung) = iter.next() {
484 if let Some(next) = iter.peek() {
485 let v_current = vmaf_estimate_for_bitrate(rung.height, rung.bitrate_kbps);
486 let v_next = vmaf_estimate_for_bitrate(next.height, next.bitrate_kbps);
487 let gap = (v_current - v_next).abs();
488
489 if gap > self.vmaf_gap_threshold {
490 let mid_height = (rung.height + next.height) / 2;
492 let mid_bitrate = (rung.bitrate_kbps + next.bitrate_kbps) / 2;
493 let mid_width = (rung.width + next.width) / 2;
494 let mid_audio = (rung.audio_kbps + next.audio_kbps) / 2;
495 let codec = rung.codec.clone();
496 result.push(rung);
497 result.push(BitrateRung::new(
498 mid_height,
499 mid_width,
500 mid_bitrate,
501 codec,
502 mid_audio,
503 ));
504 continue;
505 }
506 }
507 result.push(rung);
508 }
509 result
510 }
511
512 #[must_use]
514 pub fn optimize_full(&self, spec: LadderSpec) -> LadderSpec {
515 if spec.rungs.is_empty() {
516 return spec;
517 }
518
519 let filtered = self.filter_equivalent(spec.rungs);
520 let filled = self.fill_large_gaps(filtered);
521
522 LadderSpec {
523 rungs: filled,
524 ..spec
525 }
526 }
527
528 fn filter_equivalent(&self, rungs: Vec<BitrateRung>) -> Vec<BitrateRung> {
531 let mut kept: Vec<BitrateRung> = Vec::with_capacity(rungs.len());
532 for rung in rungs {
533 if let Some(last) = kept.last() {
534 let v_last = vmaf_estimate_for_bitrate(last.height, last.bitrate_kbps);
535 let v_cur = vmaf_estimate_for_bitrate(rung.height, rung.bitrate_kbps);
536 let diff = (v_last - v_cur).abs();
537 if diff < self.vmaf_equivalence_threshold {
538 continue;
540 }
541 }
542 kept.push(rung);
543 }
544 kept
545 }
546}
547
548#[derive(Debug, Clone, Default)]
552pub struct LadderValidator;
553
554#[derive(Debug, Clone, PartialEq, Eq)]
556pub enum LadderValidationError {
557 TooFewRungs {
559 count: usize,
561 },
562 DuplicateHeight {
564 height: u32,
566 },
567 NonMonotonicBitrate {
569 index: usize,
571 bitrate: u32,
573 prev_bitrate: u32,
575 },
576 NonMonotonicHeight {
578 index: usize,
580 height: u32,
582 prev_height: u32,
584 },
585}
586
587impl std::fmt::Display for LadderValidationError {
588 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
589 match self {
590 Self::TooFewRungs { count } => write!(f, "Ladder has {count} rung(s); minimum is 2"),
591 Self::DuplicateHeight { height } => write!(f, "Duplicate height {height}p in ladder"),
592 Self::NonMonotonicBitrate {
593 index,
594 bitrate,
595 prev_bitrate,
596 } => {
597 write!(
598 f,
599 "Rung {index}: bitrate {bitrate} >= previous {prev_bitrate}"
600 )
601 }
602 Self::NonMonotonicHeight {
603 index,
604 height,
605 prev_height,
606 } => {
607 write!(f, "Rung {index}: height {height} >= previous {prev_height}")
608 }
609 }
610 }
611}
612
613impl LadderValidator {
614 #[must_use]
616 pub fn new() -> Self {
617 Self
618 }
619
620 #[must_use]
624 pub fn validate(&self, spec: &LadderSpec) -> Vec<LadderValidationError> {
625 let mut errors = Vec::new();
626
627 if spec.rungs.len() < 2 {
628 errors.push(LadderValidationError::TooFewRungs {
629 count: spec.rungs.len(),
630 });
631 return errors; }
633
634 for (i, rung) in spec.rungs.iter().enumerate().skip(1) {
635 let prev = &spec.rungs[i - 1];
636
637 if rung.bitrate_kbps >= prev.bitrate_kbps {
639 errors.push(LadderValidationError::NonMonotonicBitrate {
640 index: i,
641 bitrate: rung.bitrate_kbps,
642 prev_bitrate: prev.bitrate_kbps,
643 });
644 }
645
646 if rung.height >= prev.height {
648 errors.push(LadderValidationError::NonMonotonicHeight {
649 index: i,
650 height: rung.height,
651 prev_height: prev.height,
652 });
653 }
654 }
655
656 let mut seen_heights = std::collections::HashSet::new();
658 for rung in &spec.rungs {
659 if !seen_heights.insert(rung.height) {
660 errors.push(LadderValidationError::DuplicateHeight {
661 height: rung.height,
662 });
663 }
664 }
665
666 errors
667 }
668
669 #[must_use]
671 pub fn is_valid(&self, spec: &LadderSpec) -> bool {
672 self.validate(spec).is_empty()
673 }
674}
675
676#[cfg(test)]
679mod tests {
680 use super::*;
681
682 #[test]
685 fn test_generate_webvod_1080p_source() {
686 let gen = LadderGenerator::new();
687 let spec = gen.generate(1080, 1920, LadderPreset::WebVod);
688 assert!(!spec.rungs.is_empty());
689 assert!(spec.rungs.iter().all(|r| r.height <= 1080));
691 }
692
693 #[test]
694 fn test_generate_broadcast_strips_above_source() {
695 let gen = LadderGenerator::new();
696 let spec = gen.generate(720, 1280, LadderPreset::Broadcast);
697 assert!(spec.rungs.iter().all(|r| r.height <= 720));
698 }
699
700 #[test]
701 fn test_generate_mobile_ladder_has_enough_rungs() {
702 let gen = LadderGenerator::new();
703 let spec = gen.generate(720, 1280, LadderPreset::Mobile);
704 assert!(spec.rungs.len() >= 2);
705 }
706
707 #[test]
708 fn test_generate_ultra4k_includes_4k_for_4k_source() {
709 let gen = LadderGenerator::new();
710 let spec = gen.generate(2160, 3840, LadderPreset::Ultra4k);
711 assert!(spec.rungs.iter().any(|r| r.height == 2160));
712 }
713
714 #[test]
715 fn test_generate_no_rung_exceeds_source_height() {
716 let gen = LadderGenerator::new();
717 for preset in [
718 LadderPreset::WebVod,
719 LadderPreset::Mobile,
720 LadderPreset::Broadcast,
721 ] {
722 let spec = gen.generate(480, 854, preset);
723 for rung in &spec.rungs {
724 assert!(
725 rung.height <= 480,
726 "{preset:?}: rung {}p exceeds source 480p",
727 rung.height
728 );
729 }
730 }
731 }
732
733 #[test]
734 fn test_ladder_spec_top_bottom_rungs() {
735 let gen = LadderGenerator::new();
736 let spec = gen.generate(1080, 1920, LadderPreset::WebVod);
737 assert!(spec.top_rung().is_some());
738 assert!(spec.bottom_rung().is_some());
739 let top = spec.top_rung().expect("top rung");
740 let bottom = spec.bottom_rung().expect("bottom rung");
741 assert!(top.bitrate_kbps >= bottom.bitrate_kbps);
742 }
743
744 #[test]
745 fn test_bitrate_rung_total_kbps() {
746 let rung = BitrateRung::new(720, 1280, 2500, "vp9", 128);
747 assert_eq!(rung.total_kbps(), 2628);
748 }
749
750 #[test]
751 fn test_bitrate_rung_pixels() {
752 let rung = BitrateRung::new(1080, 1920, 4500, "vp9", 192);
753 assert_eq!(rung.pixels(), 1080 * 1920);
754 }
755
756 #[test]
759 fn test_vmaf_zero_inputs() {
760 assert_eq!(vmaf_estimate_for_bitrate(0, 1000), 0.0);
761 assert_eq!(vmaf_estimate_for_bitrate(1080, 0), 0.0);
762 }
763
764 #[test]
765 fn test_vmaf_approaches_95_at_high_bitrate() {
766 let score = vmaf_estimate_for_bitrate(1080, 1_000_000);
767 assert!(score > 94.0, "Expected near-95 VMAF, got {score}");
768 }
769
770 #[test]
771 fn test_vmaf_increases_with_bitrate() {
772 let low = vmaf_estimate_for_bitrate(720, 500);
773 let high = vmaf_estimate_for_bitrate(720, 5000);
774 assert!(high > low);
775 }
776
777 #[test]
778 fn test_vmaf_lower_resolution_higher_score_at_same_bitrate() {
779 let score_240 = vmaf_estimate_for_bitrate(240, 500);
780 let score_1080 = vmaf_estimate_for_bitrate(1080, 500);
781 assert!(
782 score_240 > score_1080,
783 "Lower res should have higher VMAF at same bitrate"
784 );
785 }
786
787 #[test]
790 fn test_optimizer_does_not_increase_rung_count_on_similar_ladder() {
791 let gen = LadderGenerator::new();
792 let spec = gen.generate(1080, 1920, LadderPreset::WebVod);
793 let original_count = spec.rungs.len();
794 let opt = LadderOptimizer::new();
795 let optimized = opt.optimize_full(spec);
796 assert!(optimized.rungs.len() <= original_count * 2 + 1);
799 }
800
801 #[test]
802 fn test_optimizer_empty_spec_passthrough() {
803 let spec = LadderSpec {
804 preset: LadderPreset::WebVod,
805 rungs: vec![],
806 min_rungs: 2,
807 max_rungs: 5,
808 };
809 let opt = LadderOptimizer::new();
810 let result = opt.optimize(spec);
811 assert!(result.rungs.is_empty());
812 }
813
814 #[test]
815 fn test_optimizer_with_thresholds() {
816 let opt = LadderOptimizer::with_thresholds(3.0, 15.0);
817 assert!((opt.vmaf_equivalence_threshold - 3.0).abs() < 1e-6);
818 assert!((opt.vmaf_gap_threshold - 15.0).abs() < 1e-6);
819 }
820
821 #[test]
824 fn test_validator_valid_webvod_ladder() {
825 let gen = LadderGenerator::new();
826 let spec = gen.generate(1080, 1920, LadderPreset::WebVod);
827 let validator = LadderValidator::new();
828 let errors = validator.validate(&spec);
829 assert!(
830 errors.is_empty(),
831 "WebVOD 1080p ladder should be valid; errors: {errors:?}"
832 );
833 }
834
835 #[test]
836 fn test_validator_too_few_rungs() {
837 let spec = LadderSpec {
838 preset: LadderPreset::WebVod,
839 rungs: vec![BitrateRung::new(1080, 1920, 4500, "vp9", 128)],
840 min_rungs: 2,
841 max_rungs: 5,
842 };
843 let validator = LadderValidator::new();
844 let errors = validator.validate(&spec);
845 assert!(errors
846 .iter()
847 .any(|e| matches!(e, LadderValidationError::TooFewRungs { .. })));
848 }
849
850 #[test]
851 fn test_validator_duplicate_height() {
852 let spec = LadderSpec {
853 preset: LadderPreset::WebVod,
854 rungs: vec![
855 BitrateRung::new(1080, 1920, 4500, "vp9", 128),
856 BitrateRung::new(1080, 1920, 2000, "vp9", 128),
857 ],
858 min_rungs: 2,
859 max_rungs: 5,
860 };
861 let validator = LadderValidator::new();
862 let errors = validator.validate(&spec);
863 assert!(errors
864 .iter()
865 .any(|e| matches!(e, LadderValidationError::DuplicateHeight { height: 1080 })));
866 }
867
868 #[test]
869 fn test_validator_non_monotonic_bitrate() {
870 let spec = LadderSpec {
871 preset: LadderPreset::WebVod,
872 rungs: vec![
873 BitrateRung::new(1080, 1920, 1000, "vp9", 128), BitrateRung::new(720, 1280, 4500, "vp9", 128), ],
876 min_rungs: 2,
877 max_rungs: 5,
878 };
879 let validator = LadderValidator::new();
880 let errors = validator.validate(&spec);
881 assert!(errors
882 .iter()
883 .any(|e| matches!(e, LadderValidationError::NonMonotonicBitrate { .. })));
884 }
885
886 #[test]
887 fn test_validator_is_valid_helper() {
888 let gen = LadderGenerator::new();
889 let spec = gen.generate(720, 1280, LadderPreset::Mobile);
890 let validator = LadderValidator::new();
891 assert!(
892 validator.is_valid(&spec),
893 "Generated Mobile 720p ladder should be valid"
894 );
895 }
896
897 #[test]
898 fn test_ladder_preset_labels_are_non_empty() {
899 for preset in [
900 LadderPreset::Broadcast,
901 LadderPreset::WebVod,
902 LadderPreset::Mobile,
903 LadderPreset::Ultra4k,
904 LadderPreset::Archive,
905 LadderPreset::Preview,
906 ] {
907 assert!(!preset.label().is_empty());
908 }
909 }
910
911 #[test]
912 fn test_archive_uses_av1_codec() {
913 let gen = LadderGenerator::new();
914 let spec = gen.generate(1080, 1920, LadderPreset::Archive);
915 for rung in &spec.rungs {
916 assert_eq!(rung.codec, "av1");
917 }
918 }
919
920 #[test]
921 fn test_preview_ladder_has_at_most_2_rungs_for_480p() {
922 let gen = LadderGenerator::new();
923 let spec = gen.generate(480, 854, LadderPreset::Preview);
924 assert!(spec.rungs.len() <= 2);
925 }
926}