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: moq_net::bandwidth::Rate,
17}
18
19impl Rung {
20	/// A rung at `height` pixels and `bitrate` bits per second.
21	pub fn new(height: u32, bitrate: moq_net::bandwidth::Rate) -> 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.as_bps() == 0 {
92				return Err(Error::Empty {
93					height: rung.height,
94					bitrate: rung.bitrate.as_bps(),
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.as_bps(),
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.as_bps(),
116					below_height: below.height,
117					below_bitrate: below.bitrate.as_bps(),
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, moq_net::bandwidth::Rate::from_bps(350_000)),
137			Rung::new(360, moq_net::bandwidth::Rate::from_bps(600_000)),
138			Rung::new(480, moq_net::bandwidth::Rate::from_bps(1_200_000)),
139			Rung::new(720, moq_net::bandwidth::Rate::from_bps(2_500_000)),
140			Rung::new(1080, moq_net::bandwidth::Rate::from_bps(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, moq_net::bandwidth::Rate::from_bps(2_500_000)),
166			Rung::new(240, moq_net::bandwidth::Rate::from_bps(350_000)),
167			Rung::new(480, moq_net::bandwidth::Rate::from_bps(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.as_bps(), 1_200_000);
174		assert_eq!(ladder.rungs()[0].bitrate.as_bps(), 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([
181			Rung::new(720, moq_net::bandwidth::Rate::from_bps(2_500_000)),
182			Rung::new(480, moq_net::bandwidth::Rate::from_bps(2_500_000)),
183		])
184		.unwrap_err();
185		assert_eq!(
186			err,
187			Error::DuplicateBitrate {
188				bitrate: 2_500_000,
189				first: 480,
190				second: 720,
191			}
192		);
193	}
194
195	/// Odd heights round to even, so a ladder can collide on a height it never
196	/// literally wrote. Silently dropping one rung would move its neighbour's
197	/// "next lower rendition", so it is refused too.
198	#[test]
199	fn duplicate_height_is_refused() {
200		let err = Ladder::new([
201			Rung::new(721, moq_net::bandwidth::Rate::from_bps(2_500_000)),
202			Rung::new(720, moq_net::bandwidth::Rate::from_bps(1_200_000)),
203		])
204		.unwrap_err();
205		assert_eq!(
206			err,
207			Error::Unordered {
208				height: 720,
209				bitrate: 2_500_000,
210				below_height: 720,
211				below_bitrate: 1_200_000,
212			}
213		);
214	}
215
216	/// Paying more for a smaller picture: bitrate and resolution disagree about
217	/// which rendition is lower, and guessing either one mis-ranks the ladder.
218	#[test]
219	fn resolution_inversion_is_refused() {
220		let err = Ladder::new([
221			Rung::new(1080, moq_net::bandwidth::Rate::from_bps(1_000_000)),
222			Rung::new(360, moq_net::bandwidth::Rate::from_bps(3_000_000)),
223		])
224		.unwrap_err();
225		assert_eq!(
226			err,
227			Error::Unordered {
228				height: 360,
229				bitrate: 3_000_000,
230				below_height: 1080,
231				below_bitrate: 1_000_000,
232			}
233		);
234	}
235
236	#[test]
237	fn rung_without_a_rendition_is_refused() {
238		assert_eq!(
239			Ladder::new([Rung::new(1, moq_net::bandwidth::Rate::from_bps(350_000))]).unwrap_err(),
240			Error::Empty {
241				height: 1,
242				bitrate: 350_000
243			}
244		);
245		assert_eq!(
246			Ladder::new([Rung::new(240, moq_net::bandwidth::Rate::from_bps(0))]).unwrap_err(),
247			Error::Empty {
248				height: 240,
249				bitrate: 0
250			}
251		);
252	}
253
254	#[test]
255	fn empty_ladder_is_allowed() {
256		assert!(Ladder::new([]).unwrap().rungs().is_empty());
257	}
258}