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). 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;