Skip to main content

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}