Skip to main content

moq_transcode/
ladder.rs

1//! Validated output renditions in ascending bitrate and height order.
2
3/// One candidate output rendition: a target resolution (by height) and bitrate.
4///
5/// The width is derived from the source aspect ratio at runtime, and a rung is
6/// only offered when it is strictly below the source (see [`Ladder`]).
7#[derive(Clone, Copy, Debug, PartialEq, Eq)]
8#[non_exhaustive]
9pub struct Rung {
10	/// Output height in pixels. Rounded down to even when it enters a [`Ladder`]
11	/// (I420 chroma is 2x2).
12	pub height: u32,
13
14	/// The configured maximum in bits per second: the CBR target advertised in the
15	/// derivative catalog.
16	pub bitrate: u64,
17}
18
19impl Rung {
20	/// A rung at `height` pixels and `bitrate` bits per second.
21	pub fn new(height: u32, bitrate: u64) -> Self {
22		Self { height, bitrate }
23	}
24}
25
26/// Why a set of rungs is not a ladder.
27#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
28#[non_exhaustive]
29pub enum Error {
30	/// A rung that encodes nothing: a height that rounds to zero pixels, or no
31	/// bitrate to spend.
32	#[error("rung {height}p at {bitrate} bps encodes nothing")]
33	Empty {
34		/// The configured height, before rounding to even.
35		height: u32,
36		/// The configured maximum, in bits per second.
37		bitrate: u64,
38	},
39
40	/// Two rungs claim the same maximum bitrate, so neither one is the lower of
41	/// the pair.
42	#[error("rungs {first}p and {second}p share a maximum of {bitrate} bps")]
43	DuplicateBitrate {
44		/// The shared maximum, in bits per second.
45		bitrate: u64,
46		/// The shorter rung's height, in pixels.
47		first: u32,
48		/// The taller rung's height, in pixels.
49		second: u32,
50	},
51
52	/// Resolution runs backwards against bitrate: a rung costs more than the one
53	/// below it without being taller, so bitrate and picture disagree on which
54	/// rendition is lower.
55	#[error("rung {height}p at {bitrate} bps does not rise above {below_height}p at {below_bitrate} bps")]
56	Unordered {
57		/// The more expensive rung's height, in pixels.
58		height: u32,
59		/// The more expensive rung's maximum, in bits per second.
60		bitrate: u64,
61		/// The cheaper rung's height, in pixels.
62		below_height: u32,
63		/// The cheaper rung's maximum, in bits per second.
64		below_bitrate: u64,
65	},
66}
67
68/// The output ladder, in canonical order: strictly ascending maximum bitrate,
69/// and strictly ascending height with it.
70///
71/// Construction validates the order; [`Ladder::rungs`] exposes a read-only slice. The
72/// rungs are still filtered against the source at runtime (nothing above it
73/// survives), which drops rungs but never reorders them.
74#[derive(Clone, Debug, PartialEq, Eq)]
75pub struct Ladder {
76	// Ascending by bitrate, and by height with it. Heights are even.
77	rungs: Vec<Rung>,
78}
79
80impl Ladder {
81	/// Resolve `rungs` into a ladder, in any order.
82	///
83	/// Heights round down to even, and the result is sorted by maximum bitrate.
84	/// An ambiguous ladder is an [`Error`] rather than a guess: two rungs at the
85	/// same maximum have no lower one, and a rung that costs more without being
86	/// taller means bitrate and picture disagree about which rendition is lower.
87	/// An empty ladder is fine; it just offers nothing.
88	pub fn new(rungs: impl IntoIterator<Item = Rung>) -> Result<Self, Error> {
89		let mut rungs: Vec<Rung> = rungs.into_iter().collect();
90		for rung in &mut rungs {
91			if rung.height < 2 || rung.bitrate == 0 {
92				return Err(Error::Empty {
93					height: rung.height,
94					bitrate: rung.bitrate,
95				});
96			}
97			// Normalize before checking uniqueness: odd heights can share a track name.
98			rung.height &= !1;
99		}
100
101		rungs.sort_by_key(|rung| (rung.bitrate, rung.height));
102
103		for pair in rungs.windows(2) {
104			let (below, rung) = (pair[0], pair[1]);
105			if below.bitrate == rung.bitrate {
106				return Err(Error::DuplicateBitrate {
107					bitrate: rung.bitrate,
108					first: below.height,
109					second: rung.height,
110				});
111			}
112			if rung.height <= below.height {
113				return Err(Error::Unordered {
114					height: rung.height,
115					bitrate: rung.bitrate,
116					below_height: below.height,
117					below_bitrate: below.bitrate,
118				});
119			}
120		}
121
122		Ok(Self { rungs })
123	}
124
125	/// The rungs, lowest first: ascending maximum bitrate and ascending height.
126	pub fn rungs(&self) -> &[Rung] {
127		&self.rungs
128	}
129}
130
131impl Default for Ladder {
132	/// The default ladder: 240p to 1080p, filtered against the source at runtime
133	/// so only strictly-lower renditions are offered.
134	fn default() -> Self {
135		Self::new([
136			Rung::new(240, 350_000),
137			Rung::new(360, 600_000),
138			Rung::new(480, 1_200_000),
139			Rung::new(720, 2_500_000),
140			Rung::new(1080, 5_000_000),
141		])
142		.expect("the default ladder is ordered")
143	}
144}
145
146#[cfg(test)]
147mod tests {
148	use super::*;
149
150	fn heights(ladder: &Ladder) -> Vec<u32> {
151		ladder.rungs().iter().map(|rung| rung.height).collect()
152	}
153
154	#[test]
155	fn default_ladder_is_ordered() {
156		let ladder = Ladder::default();
157		assert_eq!(heights(&ladder), [240, 360, 480, 720, 1080]);
158	}
159
160	/// The operator writes the ladder top-down (or in whatever order), and it
161	/// still resolves to one canonical bottom-up ranking.
162	#[test]
163	fn custom_ladder_out_of_order() {
164		let ladder = Ladder::new([
165			Rung::new(720, 2_500_000),
166			Rung::new(240, 350_000),
167			Rung::new(480, 1_200_000),
168		])
169		.unwrap();
170		assert_eq!(heights(&ladder), [240, 480, 720]);
171		// The neighbour is the next rendition down, which is what the band
172		// formula reads.
173		assert_eq!(ladder.rungs()[1].bitrate, 1_200_000);
174		assert_eq!(ladder.rungs()[0].bitrate, 350_000);
175	}
176
177	/// Two rungs at one maximum: neither is the lower, so there is no ladder.
178	#[test]
179	fn duplicate_ceiling_is_refused() {
180		let err = Ladder::new([Rung::new(720, 2_500_000), Rung::new(480, 2_500_000)]).unwrap_err();
181		assert_eq!(
182			err,
183			Error::DuplicateBitrate {
184				bitrate: 2_500_000,
185				first: 480,
186				second: 720,
187			}
188		);
189	}
190
191	/// Odd heights round to even, so a ladder can collide on a height it never
192	/// literally wrote. Silently dropping one rung would move its neighbour's
193	/// "next lower rendition", so it is refused too.
194	#[test]
195	fn duplicate_height_is_refused() {
196		let err = Ladder::new([Rung::new(721, 2_500_000), Rung::new(720, 1_200_000)]).unwrap_err();
197		assert_eq!(
198			err,
199			Error::Unordered {
200				height: 720,
201				bitrate: 2_500_000,
202				below_height: 720,
203				below_bitrate: 1_200_000,
204			}
205		);
206	}
207
208	/// Paying more for a smaller picture: bitrate and resolution disagree about
209	/// which rendition is lower, and guessing either one mis-ranks the ladder.
210	#[test]
211	fn resolution_inversion_is_refused() {
212		let err = Ladder::new([Rung::new(1080, 1_000_000), Rung::new(360, 3_000_000)]).unwrap_err();
213		assert_eq!(
214			err,
215			Error::Unordered {
216				height: 360,
217				bitrate: 3_000_000,
218				below_height: 1080,
219				below_bitrate: 1_000_000,
220			}
221		);
222	}
223
224	#[test]
225	fn rung_without_a_rendition_is_refused() {
226		assert_eq!(
227			Ladder::new([Rung::new(1, 350_000)]).unwrap_err(),
228			Error::Empty {
229				height: 1,
230				bitrate: 350_000
231			}
232		);
233		assert_eq!(
234			Ladder::new([Rung::new(240, 0)]).unwrap_err(),
235			Error::Empty {
236				height: 240,
237				bitrate: 0
238			}
239		);
240	}
241
242	#[test]
243	fn empty_ladder_is_allowed() {
244		assert!(Ladder::new([]).unwrap().rungs().is_empty());
245	}
246}