lvqr_transcode/rendition.rs
1//! [`RenditionSpec`] plus preset constructors for the default
2//! LVQR ABR ladder.
3
4use serde::{Deserialize, Serialize};
5
6/// One rendition in an ABR ladder. Carries the target geometry +
7/// bitrates a downstream encoder uses to produce output fragments.
8///
9/// Session 104 A captures only the minimum set every software +
10/// hardware encoder consumes. Session 105 B extends this with
11/// codec-specific knobs (x264 profile / tune / keyint, NVENC
12/// quality preset, VideoToolbox pixel format) layered on
13/// rather than replacing these fields, so existing consumers stay
14/// source-compatible.
15#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
16pub struct RenditionSpec {
17 /// Short human-readable identifier (`"720p"` / `"480p"` /
18 /// `"240p"`). Used as:
19 ///
20 /// * The rendition suffix on the output broadcast name
21 /// (`<source>/<name>`).
22 /// * A Prometheus metric label
23 /// (`lvqr_transcode_fragments_total{rendition="720p"}`).
24 /// * The HLS master-playlist `NAME=` attribute (landed in
25 /// session 106 C).
26 ///
27 /// Pick something short, lowercase, no slashes. Validation is
28 /// the operator's responsibility for now; session 106 C's
29 /// CLI flag will enforce the character set.
30 pub name: String,
31
32 /// Target frame width in pixels. Downstream encoders use this
33 /// to configure the `videoscale` element (or the hardware
34 /// encoder's equivalent).
35 pub width: u32,
36
37 /// Target frame height in pixels.
38 pub height: u32,
39
40 /// Target video bitrate in kilobits / second. Upstream to the
41 /// encoder's `bitrate` property. Typical 720p h264 lands at
42 /// 2-3 Mb/s; 480p at 1-1.5 Mb/s; 240p at 300-500 kb/s.
43 pub video_bitrate_kbps: u32,
44
45 /// Target audio bitrate in kilobits / second. Upstream to the
46 /// audio encoder (AAC in 105 B) or passed through when the
47 /// rendition reuses the source audio track. 96-128 kb/s at
48 /// 48 kHz stereo is the typical range.
49 pub audio_bitrate_kbps: u32,
50}
51
52impl RenditionSpec {
53 /// Construct a custom rendition with the supplied fields.
54 pub fn new(
55 name: impl Into<String>,
56 width: u32,
57 height: u32,
58 video_bitrate_kbps: u32,
59 audio_bitrate_kbps: u32,
60 ) -> Self {
61 Self {
62 name: name.into(),
63 width,
64 height,
65 video_bitrate_kbps,
66 audio_bitrate_kbps,
67 }
68 }
69
70 /// 720p preset: `1280x720` at 2.5 Mb/s video + 128 kb/s audio.
71 /// Matches the `tracking/TIER_4_PLAN.md` section 4.6 default.
72 pub fn preset_720p() -> Self {
73 Self::new("720p", 1280, 720, 2_500, 128)
74 }
75
76 /// 480p preset: `854x480` at 1.2 Mb/s video + 96 kb/s audio.
77 pub fn preset_480p() -> Self {
78 Self::new("480p", 854, 480, 1_200, 96)
79 }
80
81 /// 240p preset: `426x240` at 400 kb/s video + 64 kb/s audio.
82 pub fn preset_240p() -> Self {
83 Self::new("240p", 426, 240, 400, 64)
84 }
85
86 /// Default 3-rung LVQR ladder, ordered highest-to-lowest so
87 /// operators reading logs or admin output see the ladder's
88 /// top rung first. HLS master-playlist composition in
89 /// session 106 C sorts independently by `BANDWIDTH`.
90 pub fn default_ladder() -> Vec<Self> {
91 vec![Self::preset_720p(), Self::preset_480p(), Self::preset_240p()]
92 }
93}
94
95#[cfg(test)]
96mod tests {
97 use super::*;
98
99 #[test]
100 fn presets_match_plan_defaults() {
101 let r = RenditionSpec::preset_720p();
102 assert_eq!(r.name, "720p");
103 assert_eq!((r.width, r.height), (1280, 720));
104 assert_eq!(r.video_bitrate_kbps, 2_500);
105 assert_eq!(r.audio_bitrate_kbps, 128);
106
107 let r = RenditionSpec::preset_480p();
108 assert_eq!((r.width, r.height), (854, 480));
109
110 let r = RenditionSpec::preset_240p();
111 assert_eq!((r.width, r.height), (426, 240));
112 }
113
114 #[test]
115 fn default_ladder_is_highest_to_lowest() {
116 let ladder = RenditionSpec::default_ladder();
117 assert_eq!(ladder.len(), 3);
118 // Monotonically decreasing video bitrate from top rung to
119 // bottom: the ordering convention operators rely on when
120 // scanning admin output.
121 for pair in ladder.windows(2) {
122 assert!(
123 pair[0].video_bitrate_kbps > pair[1].video_bitrate_kbps,
124 "ladder must be highest-to-lowest; got {} before {}",
125 pair[0].name,
126 pair[1].name,
127 );
128 }
129 }
130
131 #[test]
132 fn rendition_spec_round_trips_through_json() {
133 let r = RenditionSpec::new("custom", 1920, 1080, 5_000, 192);
134 let j = serde_json::to_string(&r).expect("serialize");
135 let parsed: RenditionSpec = serde_json::from_str(&j).expect("deserialize");
136 assert_eq!(parsed, r);
137 }
138}