alpha2_media/rate_control.rs
1//! Output rate control: the vocabulary a caller asks in, and the numbers a
2//! sink receives.
3//!
4//! A caller names a [`QualityLevel`] or an explicit rate. The engine resolves
5//! it once into a [`VideoRateControl`], and every sink (native software,
6//! native GPU-resident, browser) encodes from that resolved value. No sink
7//! ever sees a level, so no two sinks can disagree about what one means.
8
9use std::fmt;
10use std::str::FromStr;
11
12use alpha2_time::FrameRate;
13use serde::{Deserialize, Serialize};
14
15/// Video codecs the engine resolves a rate for.
16///
17/// One set of spellings for every host: the native sinks key on these
18/// directly, and the browser adapter maps its `avc` onto [`Self::H264`] at its
19/// own edge. `mpeg4-part2` has no browser sink.
20#[derive(Copy, Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
21#[serde(rename_all = "kebab-case")]
22pub enum VideoCodec {
23 /// H.264/AVC.
24 H264,
25 /// H.265/HEVC.
26 Hevc,
27 /// VP9.
28 Vp9,
29 /// AV1.
30 Av1,
31 /// MPEG-4 Part 2.
32 Mpeg4Part2,
33}
34
35impl VideoCodec {
36 /// Every codec, in the order the engine lists them.
37 pub const ALL: [Self; 5] = [
38 Self::H264,
39 Self::Hevc,
40 Self::Vp9,
41 Self::Av1,
42 Self::Mpeg4Part2,
43 ];
44
45 /// Stable identifier used by every protocol and binding.
46 #[must_use]
47 pub const fn as_str(self) -> &'static str {
48 match self {
49 Self::H264 => "h264",
50 Self::Hevc => "hevc",
51 Self::Vp9 => "vp9",
52 Self::Av1 => "av1",
53 Self::Mpeg4Part2 => "mpeg4-part2",
54 }
55 }
56
57 /// Where the codec's 4:2:0 chroma samples sit when a stream does not say,
58 /// which is how every export is written: an export packs its chroma
59 /// there, since a browser encoder cannot tag it otherwise.
60 ///
61 /// H.264, HEVC, and MPEG-4 Part 2 imply H.273 chroma location type 0,
62 /// co-sited with the even luma columns ([`ChromaLocation::Left`]), as
63 /// decoders report them and alpha2 imports them. VP9 and AV1 streams
64 /// leave it unspecified, which alpha2 reads as centred.
65 ///
66 /// [`ChromaLocation::Left`]: crate::ChromaLocation::Left
67 #[must_use]
68 pub const fn chroma_location(self) -> crate::ChromaLocation {
69 match self {
70 Self::H264 | Self::Hevc | Self::Mpeg4Part2 => crate::ChromaLocation::Left,
71 Self::Vp9 | Self::Av1 => crate::ChromaLocation::Center,
72 }
73 }
74}
75
76impl FromStr for VideoCodec {
77 type Err = RateControlError;
78
79 fn from_str(value: &str) -> Result<Self, Self::Err> {
80 Self::ALL
81 .into_iter()
82 .find(|codec| codec.as_str() == value)
83 .ok_or_else(|| RateControlError::UnknownVideoCodec(value.to_owned()))
84 }
85}
86
87/// The five qualitative output levels.
88///
89/// The names are the browser adapter's, so the engine and the adapter share
90/// one vocabulary for the concept. Their meaning is the engine's: see the
91/// resolution functions beside this type.
92#[derive(
93 Copy, Clone, Debug, Default, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize,
94)]
95#[serde(rename_all = "kebab-case")]
96pub enum QualityLevel {
97 /// Smallest output that is still watchable.
98 VeryLow,
99 /// Previews and proxies.
100 Low,
101 /// Streaming-sized delivery.
102 Medium,
103 /// Delivery quality. The default.
104 #[default]
105 High,
106 /// Close to visually transparent; mastering and re-encode sources.
107 VeryHigh,
108}
109
110impl QualityLevel {
111 /// Every level, lowest first.
112 pub const ALL: [Self; 5] = [
113 Self::VeryLow,
114 Self::Low,
115 Self::Medium,
116 Self::High,
117 Self::VeryHigh,
118 ];
119
120 /// Stable identifier used by every protocol and binding.
121 #[must_use]
122 pub const fn as_str(self) -> &'static str {
123 match self {
124 Self::VeryLow => "very-low",
125 Self::Low => "low",
126 Self::Medium => "medium",
127 Self::High => "high",
128 Self::VeryHigh => "very-high",
129 }
130 }
131}
132
133impl fmt::Display for QualityLevel {
134 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
135 formatter.write_str(self.as_str())
136 }
137}
138
139impl FromStr for QualityLevel {
140 type Err = RateControlError;
141
142 fn from_str(value: &str) -> Result<Self, Self::Err> {
143 Self::ALL
144 .into_iter()
145 .find(|level| level.as_str() == value)
146 .ok_or_else(|| RateControlError::UnknownLevel(value.to_owned()))
147 }
148}
149
150/// How an encoder is asked to hold its bitrate.
151#[derive(Copy, Clone, Debug, Default, Deserialize, Eq, Hash, PartialEq, Serialize)]
152#[serde(rename_all = "kebab-case")]
153pub enum BitrateMode {
154 /// Average rate; the encoder may spend more on hard frames. The default.
155 #[default]
156 Variable,
157 /// Constant rate, held against a one-second buffer.
158 Constant,
159}
160
161impl BitrateMode {
162 /// Stable identifier used by every protocol and binding.
163 #[must_use]
164 pub const fn as_str(self) -> &'static str {
165 match self {
166 Self::Variable => "variable",
167 Self::Constant => "constant",
168 }
169 }
170}
171
172impl FromStr for BitrateMode {
173 type Err = RateControlError;
174
175 fn from_str(value: &str) -> Result<Self, Self::Err> {
176 match value {
177 "variable" => Ok(Self::Variable),
178 "constant" => Ok(Self::Constant),
179 other => Err(RateControlError::UnknownMode(other.to_owned())),
180 }
181 }
182}
183
184/// A resolved video rate control: what a sink encodes with.
185///
186/// `bitrate` is always present. With a `quantizer` it is the fallback for an
187/// encoder that cannot hold a constant quantizer; without one it is the
188/// target.
189#[derive(Copy, Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
190pub struct VideoRateControl {
191 /// Target, or fallback, bits per second. Always positive.
192 pub bitrate: u64,
193 /// How the bitrate is held when the encoder runs on it.
194 pub mode: BitrateMode,
195 /// Constant quantizer on the codec's own scale, where the encoder has one.
196 pub quantizer: Option<u32>,
197}
198
199impl VideoRateControl {
200 /// An explicit bitrate with no quantizer.
201 ///
202 /// # Errors
203 /// Returns [`RateControlError::NonPositiveBitrate`] for zero.
204 pub const fn bitrate(bitrate: u64, mode: BitrateMode) -> Result<Self, RateControlError> {
205 if bitrate == 0 {
206 return Err(RateControlError::NonPositiveBitrate);
207 }
208 Ok(Self {
209 bitrate,
210 mode,
211 quantizer: None,
212 })
213 }
214}
215
216/// Why a rate-control request is not one the engine can resolve.
217#[derive(Clone, Debug, Eq, PartialEq)]
218pub enum RateControlError {
219 /// A level name outside the five.
220 UnknownLevel(String),
221 /// A bitrate mode other than `constant` or `variable`.
222 UnknownMode(String),
223 /// A bitrate of zero or less.
224 NonPositiveBitrate,
225 /// A video codec name outside [`VideoCodec::ALL`].
226 UnknownVideoCodec(String),
227 /// An audio codec name other than `aac` or `opus`.
228 UnknownAudioCodec(String),
229 /// A zero output dimension.
230 EmptyFrame,
231}
232
233impl fmt::Display for RateControlError {
234 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
235 match self {
236 Self::UnknownLevel(level) => write!(
237 formatter,
238 "unknown quality level \"{level}\"; expected one of very-low, low, medium, high, very-high"
239 ),
240 Self::UnknownMode(mode) => write!(
241 formatter,
242 "unknown bitrate mode \"{mode}\"; expected constant or variable"
243 ),
244 Self::NonPositiveBitrate => formatter.write_str("bitrate must be a positive integer"),
245 Self::UnknownVideoCodec(codec) => write!(
246 formatter,
247 "unknown video codec \"{codec}\"; expected one of h264, hevc, vp9, av1, mpeg4-part2"
248 ),
249 Self::UnknownAudioCodec(codec) => write!(
250 formatter,
251 "unknown audio codec \"{codec}\"; expected aac or opus"
252 ),
253 Self::EmptyFrame => formatter.write_str("output width and height must be non-zero"),
254 }
255 }
256}
257
258impl std::error::Error for RateControlError {}
259
260// Level to rate:
261//
262// bitrate = REFERENCE_BITRATE[level]
263// * (pixels / REFERENCE_PIXELS) ^ RESOLUTION_EXPONENT
264// * (fps / REFERENCE_FPS) ^ FRAME_RATE_EXPONENT
265// * codec_factor(codec)
266//
267// rounded to 1 kbps and clamped below at MIN_VIDEO_BITRATE. The constants were measured on this
268// project's own export path (PSNR-Y on the 4K showcase), not taken from encoder defaults.
269
270/// Luma samples of the reference output the level table is stated at.
271pub const REFERENCE_PIXELS: u64 = 1920 * 1080;
272/// Frame rate the level table is stated at.
273pub const REFERENCE_FPS: f64 = 30.0;
274
275/// H.264 bits per second at 1920x1080, 30 fps, lowest level first.
276///
277/// Each level is 3 dB of PSNR-Y above the one below it (half the error) on
278/// the harder of the two measured contents: 32, 35, 38, 41 and 44 dB. A
279/// constant rate must hold its level on content harder than the median;
280/// easier content lands higher on the same rate.
281const REFERENCE_BITRATE: [f64; 5] = [1.8e6, 3.5e6, 6.0e6, 10.0e6, 16.0e6];
282
283/// How bitrate grows with pixel count at equal quality.
284///
285/// Measured between 0.16 and 0.54 across the two contents and the level
286/// range; this is the steepest, because both measured contents are soft and
287/// sharper sources scale closer to linearly.
288pub const RESOLUTION_EXPONENT: f64 = 0.5;
289
290/// How bitrate grows with frame rate at equal quality: `(fps / 30) ^ k`.
291///
292/// Not linear: frames at 60 fps are more alike than frames at 24. Measured
293/// 24-vs-60 at equal PSNR, `k` is 0.30 to 0.36 for 24p footage exported at
294/// 60 and 0.87 to 0.98 for true 60 fps motion; this is the middle of that bracket,
295/// which puts 60 fps at 1.52x of 30.
296pub const FRAME_RATE_EXPONENT: f64 = 0.6;
297
298/// No level resolves below this. Below about 300 kbps the measured encoders
299/// stopped holding their target at all (asked for 84 kbps they wrote 170 to
300/// 260), so a lower number would describe a file the encoder does not make.
301pub const MIN_VIDEO_BITRATE: u64 = 250_000;
302
303/// Bits a codec needs, relative to H.264, for the same quality.
304///
305/// HEVC and AV1 were measured on NVENC against `h264_nvenc` at equal PSNR-Y
306/// (0.61 to 0.71 and 0.56 to 0.61 over 40 to 48 dB), MPEG-4 Part 2 on `mpeg4` against
307/// `h264_vulkan` (3.3x at 40 dB falling to 2.1x at 48). The software encoders the
308/// pinned SDK links were measured the same way, against `h264_nvenc` on the colour
309/// clip at 1920x816, at the PSNR-Y `h264_nvenc` reaches at each level's H.264 rate:
310/// `libvpx-vp9` (`cpu-used` 4) needs 0.60x at `very-low` rising to 0.87x at
311/// `very-high`, so VP9 is the middle of that, 0.75. `libsvtav1` (preset 8) needs
312/// 0.47x rising to 0.93x, where `av1_nvenc` on the same frames needs 0.49x to 0.61x;
313/// one AV1 factor serves both encoders, so it is the higher of their middles, 0.70, which
314/// gives `av1_nvenc` more than it needs rather than `libsvtav1` far less. The same runs put HEVC
315/// at 0.60x to 0.73x, in line with its factor. A browser's `WebCodecs` VP9 and AV1
316/// encoders take the same factors.
317#[must_use]
318pub const fn codec_factor(codec: VideoCodec) -> f64 {
319 match codec {
320 VideoCodec::H264 => 1.0,
321 VideoCodec::Hevc => 0.65,
322 VideoCodec::Vp9 => 0.75,
323 VideoCodec::Av1 => 0.70,
324 VideoCodec::Mpeg4Part2 => 2.7,
325 }
326}
327
328/// The constant quantizer a level resolves to for `codec`, where the engine
329/// has measured one.
330///
331/// H.264 only, on its 0-51 QP scale, which `h264_vulkan`, `h264_nvenc` and
332/// `WebCodecs`' `avc` quantizer mode share. Each value is the QP at which
333/// `h264_vulkan` holds the level's own PSNR-Y definition on the harder
334/// measured content at the size the levels are stated at (the colour clip at
335/// 1920x816: 32.5, 35.6, 38.4, 41.3 and 44.4 dB under the bitrate path), rounded
336/// toward the better picture. QP moves PSNR by 0.66 dB a step there and by
337/// about the same at 4K, so one QP per level serves every size. HEVC and AV1
338/// encoders open a quantizer mode too, but what their QPs are worth has not
339/// been measured, so their levels stay bitrate-only rather than borrowing
340/// H.264's; MPEG-4 Part 2's encoder takes its quantizer per frame and not from
341/// the options a sink sets, so it has none either.
342#[must_use]
343pub const fn level_quantizer(codec: VideoCodec, level: QualityLevel) -> Option<u32> {
344 match codec {
345 VideoCodec::H264 => Some(match level {
346 QualityLevel::VeryLow => 41,
347 QualityLevel::Low => 35,
348 QualityLevel::Medium => 31,
349 QualityLevel::High => 27,
350 QualityLevel::VeryHigh => 23,
351 }),
352 VideoCodec::Hevc | VideoCodec::Vp9 | VideoCodec::Av1 | VideoCodec::Mpeg4Part2 => None,
353 }
354}
355
356/// Resolves a quality level into the rate control every sink encodes with.
357///
358/// This is the one definition of what a level means. The native host and the
359/// browser binding both call it, so two sinks asked for the same level at the
360/// same output receive the same numbers.
361///
362/// # Errors
363/// Returns [`RateControlError::EmptyFrame`] for a zero dimension.
364pub fn resolve_video_rate_control(
365 codec: VideoCodec,
366 width: u32,
367 height: u32,
368 frame_rate: FrameRate,
369 level: QualityLevel,
370) -> Result<VideoRateControl, RateControlError> {
371 if width == 0 || height == 0 {
372 return Err(RateControlError::EmptyFrame);
373 }
374 let pixels = f64::from(width) * f64::from(height);
375 #[allow(clippy::cast_precision_loss)] // 2^53 pixels is not an output size.
376 let resolution = (pixels / REFERENCE_PIXELS as f64).powf(RESOLUTION_EXPONENT);
377 let fps = f64::from(frame_rate.numerator()) / f64::from(frame_rate.denominator());
378 let temporal = (fps / REFERENCE_FPS).powf(FRAME_RATE_EXPONENT);
379 let bitrate = REFERENCE_BITRATE[level as usize] * resolution * temporal * codec_factor(codec);
380 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)] // Finite and positive.
381 let kilobits = (bitrate / 1_000.0).round() as u64;
382 // The quantizer, where there is one, is what a sink runs; the bitrate is its fallback on an
383 // encoder that cannot hold a constant quantizer, and the whole of the rate control on a codec
384 // without one.
385 Ok(VideoRateControl {
386 bitrate: kilobits.saturating_mul(1_000).max(MIN_VIDEO_BITRATE),
387 mode: BitrateMode::Variable,
388 quantizer: level_quantizer(codec, level),
389 })
390}
391
392/// Audio codecs the engine resolves a rate for.
393///
394/// AAC and Opus. The native sinks encode Opus with `libopus`, the browser
395/// with `WebCodecs`; which containers offer which is
396/// [`Container::audio_codecs`](crate::Container::audio_codecs).
397#[derive(Copy, Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
398#[serde(rename_all = "kebab-case")]
399pub enum AudioCodec {
400 /// AAC-LC.
401 Aac,
402 /// Opus.
403 Opus,
404}
405
406impl AudioCodec {
407 /// Stable identifier used by every protocol and binding.
408 #[must_use]
409 pub const fn as_str(self) -> &'static str {
410 match self {
411 Self::Aac => "aac",
412 Self::Opus => "opus",
413 }
414 }
415}
416
417impl FromStr for AudioCodec {
418 type Err = RateControlError;
419
420 fn from_str(value: &str) -> Result<Self, Self::Err> {
421 match value {
422 "aac" => Ok(Self::Aac),
423 "opus" => Ok(Self::Opus),
424 other => Err(RateControlError::UnknownAudioCodec(other.to_owned())),
425 }
426 }
427}
428
429/// Resolves a quality level into a stereo audio bitrate in bits per second.
430///
431/// No resolution or duration term: an audio stream's cost depends on its
432/// codec and channel layout, and every alpha2 export is stereo.
433///
434/// AAC-LC: `high`, the default, is 192 kbps. The ladder runs 64 / 96 / 128 /
435/// 192 / 256 kbps: 64 kbps is where AAC-LC stereo is still intelligible and
436/// 256 is past the point where AAC-LC stereo is commonly held to be
437/// transparent, and the steps between are the conventional encoder settings.
438/// Opus reaches the same quality at roughly two thirds of the AAC rate and is
439/// transparent near 128 kbps stereo, so it runs 48 / 64 / 96 / 128 / 160.
440#[must_use]
441pub const fn resolve_audio_bitrate(codec: AudioCodec, level: QualityLevel) -> u32 {
442 let table: [u32; 5] = match codec {
443 AudioCodec::Aac => [64_000, 96_000, 128_000, 192_000, 256_000],
444 AudioCodec::Opus => [48_000, 64_000, 96_000, 128_000, 160_000],
445 };
446 table[level as usize]
447}
448
449#[cfg(test)]
450#[path = "rate_control_tests.rs"]
451mod tests;