Skip to main content

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). VP9 is not
308/// measured: neither measured runtime links a VP9 encoder. It is set to
309/// HEVC's factor, a codec of the same generation, and it is reachable: the
310/// browser exporter encodes VP9 through `WebCodecs`, and a native host whose
311/// `FFmpeg` has `libvpx-vp9` encodes it too. Its levels therefore rest on this
312/// assumption until VP9 is measured as HEVC and AV1 were.
313#[must_use]
314pub const fn codec_factor(codec: VideoCodec) -> f64 {
315    match codec {
316        VideoCodec::H264 => 1.0,
317        VideoCodec::Hevc | VideoCodec::Vp9 => 0.65,
318        VideoCodec::Av1 => 0.58,
319        VideoCodec::Mpeg4Part2 => 2.7,
320    }
321}
322
323/// The constant quantizer a level resolves to for `codec`, where the engine
324/// has measured one.
325///
326/// H.264 only, on its 0-51 QP scale, which `h264_vulkan`, `h264_nvenc` and
327/// `WebCodecs`' `avc` quantizer mode share. Each value is the QP at which
328/// `h264_vulkan` holds the level's own PSNR-Y definition on the harder
329/// measured content at the size the levels are stated at (the colour clip at
330/// 1920x816: 32.5, 35.6, 38.4, 41.3 and 44.4 dB under the bitrate path), rounded
331/// toward the better picture. QP moves PSNR by 0.66 dB a step there and by
332/// about the same at 4K, so one QP per level serves every size. HEVC and AV1
333/// encoders open a quantizer mode too, but what their QPs are worth has not
334/// been measured, so their levels stay bitrate-only rather than borrowing
335/// H.264's; MPEG-4 Part 2's encoder takes its quantizer per frame and not from
336/// the options a sink sets, so it has none either.
337#[must_use]
338pub const fn level_quantizer(codec: VideoCodec, level: QualityLevel) -> Option<u32> {
339    match codec {
340        VideoCodec::H264 => Some(match level {
341            QualityLevel::VeryLow => 41,
342            QualityLevel::Low => 35,
343            QualityLevel::Medium => 31,
344            QualityLevel::High => 27,
345            QualityLevel::VeryHigh => 23,
346        }),
347        VideoCodec::Hevc | VideoCodec::Vp9 | VideoCodec::Av1 | VideoCodec::Mpeg4Part2 => None,
348    }
349}
350
351/// Resolves a quality level into the rate control every sink encodes with.
352///
353/// This is the one definition of what a level means. The native host and the
354/// browser binding both call it, so two sinks asked for the same level at the
355/// same output receive the same numbers.
356///
357/// # Errors
358/// Returns [`RateControlError::EmptyFrame`] for a zero dimension.
359pub fn resolve_video_rate_control(
360    codec: VideoCodec,
361    width: u32,
362    height: u32,
363    frame_rate: FrameRate,
364    level: QualityLevel,
365) -> Result<VideoRateControl, RateControlError> {
366    if width == 0 || height == 0 {
367        return Err(RateControlError::EmptyFrame);
368    }
369    let pixels = f64::from(width) * f64::from(height);
370    #[allow(clippy::cast_precision_loss)] // 2^53 pixels is not an output size.
371    let resolution = (pixels / REFERENCE_PIXELS as f64).powf(RESOLUTION_EXPONENT);
372    let fps = f64::from(frame_rate.numerator()) / f64::from(frame_rate.denominator());
373    let temporal = (fps / REFERENCE_FPS).powf(FRAME_RATE_EXPONENT);
374    let bitrate = REFERENCE_BITRATE[level as usize] * resolution * temporal * codec_factor(codec);
375    #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)] // Finite and positive.
376    let kilobits = (bitrate / 1_000.0).round() as u64;
377    // The quantizer, where there is one, is what a sink runs; the bitrate is its fallback on an
378    // encoder that cannot hold a constant quantizer, and the whole of the rate control on a codec
379    // without one.
380    Ok(VideoRateControl {
381        bitrate: kilobits.saturating_mul(1_000).max(MIN_VIDEO_BITRATE),
382        mode: BitrateMode::Variable,
383        quantizer: level_quantizer(codec, level),
384    })
385}
386
387/// Audio codecs the engine resolves a rate for.
388///
389/// AAC is the only one the native sinks encode. Opus is here because the
390/// browser app falls back to it where a browser cannot encode AAC, and that
391/// sink must receive a number from the engine like every other.
392#[derive(Copy, Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
393#[serde(rename_all = "kebab-case")]
394pub enum AudioCodec {
395    /// AAC-LC.
396    Aac,
397    /// Opus.
398    Opus,
399}
400
401impl AudioCodec {
402    /// Stable identifier used by every protocol and binding.
403    #[must_use]
404    pub const fn as_str(self) -> &'static str {
405        match self {
406            Self::Aac => "aac",
407            Self::Opus => "opus",
408        }
409    }
410}
411
412impl FromStr for AudioCodec {
413    type Err = RateControlError;
414
415    fn from_str(value: &str) -> Result<Self, Self::Err> {
416        match value {
417            "aac" => Ok(Self::Aac),
418            "opus" => Ok(Self::Opus),
419            other => Err(RateControlError::UnknownAudioCodec(other.to_owned())),
420        }
421    }
422}
423
424/// Resolves a quality level into a stereo audio bitrate in bits per second.
425///
426/// No resolution or duration term: an audio stream's cost depends on its
427/// codec and channel layout, and every alpha2 export is stereo.
428///
429/// AAC-LC: `high`, the default, is 192 kbps. The ladder runs 64 / 96 / 128 /
430/// 192 / 256 kbps: 64 kbps is where AAC-LC stereo is still intelligible and
431/// 256 is past the point where AAC-LC stereo is commonly held to be
432/// transparent, and the steps between are the conventional encoder settings.
433/// Opus reaches the same quality at roughly two thirds of the AAC rate and is
434/// transparent near 128 kbps stereo, so it runs 48 / 64 / 96 / 128 / 160.
435#[must_use]
436pub const fn resolve_audio_bitrate(codec: AudioCodec, level: QualityLevel) -> u32 {
437    let table: [u32; 5] = match codec {
438        AudioCodec::Aac => [64_000, 96_000, 128_000, 192_000, 256_000],
439        AudioCodec::Opus => [48_000, 64_000, 96_000, 128_000, 160_000],
440    };
441    table[level as usize]
442}
443
444#[cfg(test)]
445#[path = "rate_control_tests.rs"]
446mod tests;