use std::fmt;
use std::str::FromStr;
use alpha2_time::FrameRate;
use serde::{Deserialize, Serialize};
#[derive(Copy, Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
#[serde(rename_all = "kebab-case")]
pub enum VideoCodec {
H264,
Hevc,
Vp9,
Av1,
Mpeg4Part2,
}
impl VideoCodec {
pub const ALL: [Self; 5] = [
Self::H264,
Self::Hevc,
Self::Vp9,
Self::Av1,
Self::Mpeg4Part2,
];
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::H264 => "h264",
Self::Hevc => "hevc",
Self::Vp9 => "vp9",
Self::Av1 => "av1",
Self::Mpeg4Part2 => "mpeg4-part2",
}
}
#[must_use]
pub const fn chroma_location(self) -> crate::ChromaLocation {
match self {
Self::H264 | Self::Hevc | Self::Mpeg4Part2 => crate::ChromaLocation::Left,
Self::Vp9 | Self::Av1 => crate::ChromaLocation::Center,
}
}
}
impl FromStr for VideoCodec {
type Err = RateControlError;
fn from_str(value: &str) -> Result<Self, Self::Err> {
Self::ALL
.into_iter()
.find(|codec| codec.as_str() == value)
.ok_or_else(|| RateControlError::UnknownVideoCodec(value.to_owned()))
}
}
#[derive(
Copy, Clone, Debug, Default, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize,
)]
#[serde(rename_all = "kebab-case")]
pub enum QualityLevel {
VeryLow,
Low,
Medium,
#[default]
High,
VeryHigh,
}
impl QualityLevel {
pub const ALL: [Self; 5] = [
Self::VeryLow,
Self::Low,
Self::Medium,
Self::High,
Self::VeryHigh,
];
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::VeryLow => "very-low",
Self::Low => "low",
Self::Medium => "medium",
Self::High => "high",
Self::VeryHigh => "very-high",
}
}
}
impl fmt::Display for QualityLevel {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(self.as_str())
}
}
impl FromStr for QualityLevel {
type Err = RateControlError;
fn from_str(value: &str) -> Result<Self, Self::Err> {
Self::ALL
.into_iter()
.find(|level| level.as_str() == value)
.ok_or_else(|| RateControlError::UnknownLevel(value.to_owned()))
}
}
#[derive(Copy, Clone, Debug, Default, Deserialize, Eq, Hash, PartialEq, Serialize)]
#[serde(rename_all = "kebab-case")]
pub enum BitrateMode {
#[default]
Variable,
Constant,
}
impl BitrateMode {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Variable => "variable",
Self::Constant => "constant",
}
}
}
impl FromStr for BitrateMode {
type Err = RateControlError;
fn from_str(value: &str) -> Result<Self, Self::Err> {
match value {
"variable" => Ok(Self::Variable),
"constant" => Ok(Self::Constant),
other => Err(RateControlError::UnknownMode(other.to_owned())),
}
}
}
#[derive(Copy, Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
pub struct VideoRateControl {
pub bitrate: u64,
pub mode: BitrateMode,
pub quantizer: Option<u32>,
}
impl VideoRateControl {
pub const fn bitrate(bitrate: u64, mode: BitrateMode) -> Result<Self, RateControlError> {
if bitrate == 0 {
return Err(RateControlError::NonPositiveBitrate);
}
Ok(Self {
bitrate,
mode,
quantizer: None,
})
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum RateControlError {
UnknownLevel(String),
UnknownMode(String),
NonPositiveBitrate,
UnknownVideoCodec(String),
UnknownAudioCodec(String),
EmptyFrame,
}
impl fmt::Display for RateControlError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::UnknownLevel(level) => write!(
formatter,
"unknown quality level \"{level}\"; expected one of very-low, low, medium, high, very-high"
),
Self::UnknownMode(mode) => write!(
formatter,
"unknown bitrate mode \"{mode}\"; expected constant or variable"
),
Self::NonPositiveBitrate => formatter.write_str("bitrate must be a positive integer"),
Self::UnknownVideoCodec(codec) => write!(
formatter,
"unknown video codec \"{codec}\"; expected one of h264, hevc, vp9, av1, mpeg4-part2"
),
Self::UnknownAudioCodec(codec) => write!(
formatter,
"unknown audio codec \"{codec}\"; expected aac or opus"
),
Self::EmptyFrame => formatter.write_str("output width and height must be non-zero"),
}
}
}
impl std::error::Error for RateControlError {}
pub const REFERENCE_PIXELS: u64 = 1920 * 1080;
pub const REFERENCE_FPS: f64 = 30.0;
const REFERENCE_BITRATE: [f64; 5] = [1.8e6, 3.5e6, 6.0e6, 10.0e6, 16.0e6];
pub const RESOLUTION_EXPONENT: f64 = 0.5;
pub const FRAME_RATE_EXPONENT: f64 = 0.6;
pub const MIN_VIDEO_BITRATE: u64 = 250_000;
#[must_use]
pub const fn codec_factor(codec: VideoCodec) -> f64 {
match codec {
VideoCodec::H264 => 1.0,
VideoCodec::Hevc | VideoCodec::Vp9 => 0.65,
VideoCodec::Av1 => 0.58,
VideoCodec::Mpeg4Part2 => 2.7,
}
}
#[must_use]
pub const fn level_quantizer(codec: VideoCodec, level: QualityLevel) -> Option<u32> {
match codec {
VideoCodec::H264 => Some(match level {
QualityLevel::VeryLow => 41,
QualityLevel::Low => 35,
QualityLevel::Medium => 31,
QualityLevel::High => 27,
QualityLevel::VeryHigh => 23,
}),
VideoCodec::Hevc | VideoCodec::Vp9 | VideoCodec::Av1 | VideoCodec::Mpeg4Part2 => None,
}
}
pub fn resolve_video_rate_control(
codec: VideoCodec,
width: u32,
height: u32,
frame_rate: FrameRate,
level: QualityLevel,
) -> Result<VideoRateControl, RateControlError> {
if width == 0 || height == 0 {
return Err(RateControlError::EmptyFrame);
}
let pixels = f64::from(width) * f64::from(height);
#[allow(clippy::cast_precision_loss)] let resolution = (pixels / REFERENCE_PIXELS as f64).powf(RESOLUTION_EXPONENT);
let fps = f64::from(frame_rate.numerator()) / f64::from(frame_rate.denominator());
let temporal = (fps / REFERENCE_FPS).powf(FRAME_RATE_EXPONENT);
let bitrate = REFERENCE_BITRATE[level as usize] * resolution * temporal * codec_factor(codec);
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)] let kilobits = (bitrate / 1_000.0).round() as u64;
Ok(VideoRateControl {
bitrate: kilobits.saturating_mul(1_000).max(MIN_VIDEO_BITRATE),
mode: BitrateMode::Variable,
quantizer: level_quantizer(codec, level),
})
}
#[derive(Copy, Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
#[serde(rename_all = "kebab-case")]
pub enum AudioCodec {
Aac,
Opus,
}
impl AudioCodec {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Aac => "aac",
Self::Opus => "opus",
}
}
}
impl FromStr for AudioCodec {
type Err = RateControlError;
fn from_str(value: &str) -> Result<Self, Self::Err> {
match value {
"aac" => Ok(Self::Aac),
"opus" => Ok(Self::Opus),
other => Err(RateControlError::UnknownAudioCodec(other.to_owned())),
}
}
}
#[must_use]
pub const fn resolve_audio_bitrate(codec: AudioCodec, level: QualityLevel) -> u32 {
let table: [u32; 5] = match codec {
AudioCodec::Aac => [64_000, 96_000, 128_000, 192_000, 256_000],
AudioCodec::Opus => [48_000, 64_000, 96_000, 128_000, 160_000],
};
table[level as usize]
}
#[cfg(test)]
#[path = "rate_control_tests.rs"]
mod tests;