Skip to main content

alpha2_native/
rate_control.rs

1//! How a resolved rate control reaches one concrete encoder.
2//!
3//! The sinks receive numbers ([`VideoRateControl`]) and never a quality
4//! level. What differs between encoders is only the spelling: `h264_vulkan`
5//! takes `rc_mode`, NVENC takes `rc`, `MediaFoundation` takes `rate_control`.
6//! Everything that spells it lives here, so the software sink and the
7//! GPU-resident sink hold a mode the same way.
8
9use alpha2_media::{BitrateMode, VideoRateControl};
10use rsmpeg::avutil::AVDictionary;
11use std::ffi::CStr;
12
13/// The private option that puts `encoder_name` into constant-bitrate mode,
14/// when its family has one.
15///
16/// Encoders without one are held to a constant rate by the generic
17/// `minrate`/`maxrate`/`bufsize` alone, which is how libavcodec's own
18/// encoders (`mpeg4` among them) implement it, and which `libvpx` takes as
19/// its own CBR switch. `libsvtav1` sets it through `svtav1-params` instead
20/// ([`mechanism_request`]).
21pub(crate) fn constant_mode_option(encoder_name: &str) -> Option<(&'static CStr, &'static CStr)> {
22    if encoder_name.ends_with("_vulkan") {
23        Some((c"rc_mode", c"cbr"))
24    } else if encoder_name.ends_with("_nvenc") || encoder_name.ends_with("_amf") {
25        Some((c"rc", c"cbr"))
26    } else if encoder_name.ends_with("_vaapi") || encoder_name.ends_with("_d3d12va") {
27        Some((c"rc_mode", c"CBR"))
28    } else if encoder_name.ends_with("_mf") {
29        Some((c"rate_control", c"cbr"))
30    } else if encoder_name.ends_with("_videotoolbox") {
31        Some((c"constant_bit_rate", c"1"))
32    } else {
33        None
34    }
35}
36
37/// What to add to an open failure of `encoder_name` under `rate`, when the
38/// constant mode is the likely reason: `VideoToolbox` has a constant bit rate
39/// only from macOS 13, D3D12 Video Encode only on some devices (an Intel Iris
40/// Xe refuses it with `HRESULT` 0x887A0020 and takes a constant QP), and
41/// `FFmpeg` reports either absence as a generic error.
42pub(crate) fn constant_rate_hint(encoder_name: &str, rate: VideoRateControl) -> &'static str {
43    if rate.mode != BitrateMode::Constant {
44        ""
45    } else if encoder_name.ends_with("_videotoolbox") {
46        " (a constant bit rate was asked for, which VideoToolbox has only on macOS 13 and later)"
47    } else if encoder_name.ends_with("_d3d12va") {
48        " (a constant bit rate was asked for, which D3D12 Video Encode has only on some devices; \
49         ask for a quantizer instead)"
50    } else {
51        ""
52    }
53}
54
55/// Adds what `rate` needs beyond `AVCodecContext::bit_rate` to `options`.
56///
57/// A variable rate is the encoder's own bitrate-driven default and needs
58/// nothing more. A constant rate pins the rate from both sides against a
59/// one-second buffer and, where the encoder family has a mode switch, sets it:
60/// `reject_unused_encode_options` then names any encoder that ignored it,
61/// rather than letting a constant request quietly run variable.
62pub(crate) fn rate_control_options(
63    encoder_name: &str,
64    rate: VideoRateControl,
65    options: AVDictionary,
66) -> AVDictionary {
67    match rate.mode {
68        BitrateMode::Variable => options,
69        BitrateMode::Constant => {
70            match mechanism_request(
71                encoder_name,
72                RateControlMechanism::ConstantBitrate,
73                rate.bitrate,
74                0,
75            ) {
76                Some(request) => request.dictionary(options),
77                None => options,
78            }
79        }
80    }
81}
82
83/// A way of controlling an encoder's rate, independent of how a family
84/// spells it.
85#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq)]
86#[non_exhaustive]
87pub enum RateControlMechanism {
88    /// Constant bitrate.
89    ConstantBitrate,
90    /// Variable (average) bitrate.
91    VariableBitrate,
92    /// Constant quantizer: `FFmpeg`'s `cqp`/`constqp`, the family of `crf`.
93    ConstantQuantizer,
94}
95
96impl RateControlMechanism {
97    /// Every mechanism, in the order the probe reports them.
98    pub const ALL: [Self; 3] = [
99        Self::ConstantBitrate,
100        Self::VariableBitrate,
101        Self::ConstantQuantizer,
102    ];
103
104    /// Stable identifier for diagnostics.
105    #[must_use]
106    pub const fn as_str(self) -> &'static str {
107        match self {
108            Self::ConstantBitrate => "cbr",
109            Self::VariableBitrate => "vbr",
110            Self::ConstantQuantizer => "cqp",
111        }
112    }
113}
114
115/// The options that ask one encoder for one mechanism.
116#[derive(Clone, Debug, Eq, PartialEq)]
117pub(crate) struct MechanismRequest {
118    /// Private and generic `AVOption`s, in the order they are set.
119    pub options: Vec<(&'static CStr, String)>,
120    /// `AVCodecContext::bit_rate`; zero for a quantizer mode, which must not
121    /// carry a target it would then be free to prefer.
122    pub bit_rate: i64,
123}
124
125impl MechanismRequest {
126    /// The options as `key=value` pairs, for a report.
127    pub fn describe(&self) -> String {
128        let mut parts = self
129            .options
130            .iter()
131            .map(|(key, value)| format!("{}={value}", key.to_string_lossy()))
132            .collect::<Vec<_>>();
133        if self.bit_rate > 0 {
134            parts.insert(0, format!("b={}", self.bit_rate));
135        }
136        parts.join(" ")
137    }
138
139    /// The options added to a dictionary to open a context with.
140    pub fn dictionary(&self, mut options: AVDictionary) -> AVDictionary {
141        for (key, value) in &self.options {
142            options = crate::convert::set_encoder_option(options, key, value);
143        }
144        options
145    }
146}
147
148/// How `encoder_name` is asked for `mechanism`, or `None` where alpha2 knows no
149/// spelling for it on that family.
150///
151/// `quantizer` is on the codec's own scale: 0-51 for H.264/HEVC, 0-255 for
152/// NVENC and Vulkan AV1, 0-52 for every D3D12VA codec, AV1 included. Whether
153/// the device accepts the mode is not decided here: that is what the probe
154/// and the open-time fallback find out.
155pub(crate) fn mechanism_request(
156    encoder_name: &str,
157    mechanism: RateControlMechanism,
158    bitrate: u64,
159    quantizer: u32,
160) -> Option<MechanismRequest> {
161    let target = i64::try_from(bitrate).unwrap_or(i64::MAX);
162    let constant = |switch: Option<(&'static CStr, &'static str)>| {
163        let mut options = vec![
164            (c"maxrate", bitrate.to_string()),
165            (c"minrate", bitrate.to_string()),
166            (c"bufsize", bitrate.to_string()),
167        ];
168        if let Some((key, value)) = switch {
169            options.push((key, value.to_owned()));
170        }
171        MechanismRequest {
172            options,
173            bit_rate: target,
174        }
175    };
176    let switched = |key: &'static CStr, value: &str| MechanismRequest {
177        options: vec![(key, value.to_owned())],
178        bit_rate: target,
179    };
180    let fixed_quantizer = |options: Vec<(&'static CStr, String)>| MechanismRequest {
181        options,
182        bit_rate: 0,
183    };
184    let q = quantizer.to_string();
185    let family = if encoder_name.ends_with("_vulkan") {
186        "vulkan"
187    } else if encoder_name.ends_with("_nvenc") {
188        "nvenc"
189    } else if encoder_name.ends_with("_vaapi") {
190        "vaapi"
191    } else if encoder_name.ends_with("_d3d12va") {
192        "d3d12va"
193    } else if encoder_name.ends_with("_amf") {
194        "amf"
195    } else if encoder_name.ends_with("_mf") {
196        "mf"
197    } else if encoder_name.ends_with("_videotoolbox") {
198        "videotoolbox"
199    } else if encoder_name.ends_with("_qsv") {
200        "qsv"
201    } else if matches!(encoder_name, "libx264" | "libx265") {
202        "x26x"
203    } else if matches!(
204        encoder_name,
205        "libvpx" | "libvpx-vp9" | "libaom-av1" | "libsvtav1"
206    ) {
207        "crf"
208    } else {
209        "libavcodec"
210    };
211    Some(match (family, mechanism) {
212        // SVT-AV1 has a constant rate only in its low-delay prediction structure, and refuses a
213        // maximum bitrate outside its quantizer mode, which `FFmpeg` would pass on from `maxrate`.
214        // So the switch is spelled in its own parameters, against the same one-second buffer.
215        ("crf", RateControlMechanism::ConstantBitrate) if encoder_name == "libsvtav1" => {
216            MechanismRequest {
217                options: vec![
218                    (c"bufsize", bitrate.to_string()),
219                    (c"svtav1-params", "rc=2:pred-struct=1".to_owned()),
220                ],
221                bit_rate: target,
222            }
223        }
224        (_, RateControlMechanism::ConstantBitrate) => constant(
225            constant_mode_option(encoder_name)
226                .map(|(key, value)| (key, value.to_str().unwrap_or_default())),
227        ),
228        ("vulkan", RateControlMechanism::VariableBitrate) => switched(c"rc_mode", "vbr"),
229        ("nvenc", RateControlMechanism::VariableBitrate) => switched(c"rc", "vbr"),
230        ("vaapi" | "d3d12va", RateControlMechanism::VariableBitrate) => switched(c"rc_mode", "VBR"),
231        ("amf", RateControlMechanism::VariableBitrate) => switched(c"rc", "vbr_peak"),
232        ("mf", RateControlMechanism::VariableBitrate) => switched(c"rate_control", "u_vbr"),
233        (_, RateControlMechanism::VariableBitrate) => MechanismRequest {
234            options: Vec::new(),
235            bit_rate: target,
236        },
237        ("vulkan", RateControlMechanism::ConstantQuantizer) => {
238            fixed_quantizer(vec![(c"rc_mode", "cqp".to_owned()), (c"qp", q)])
239        }
240        ("nvenc", RateControlMechanism::ConstantQuantizer) => {
241            fixed_quantizer(vec![(c"rc", "constqp".to_owned()), (c"qp", q)])
242        }
243        // D3D12VA derives its I and B quantizers from `qp` through `i_qfactor`/`b_qfactor` and
244        // their offsets, which stay at the encoder's defaults: 1 and 0 for HEVC and AV1, and
245        // libavcodec's 0.8 and 1.25 + 1.25 for H.264.
246        ("vaapi" | "d3d12va", RateControlMechanism::ConstantQuantizer) => {
247            fixed_quantizer(vec![(c"rc_mode", "CQP".to_owned()), (c"qp", q)])
248        }
249        ("amf", RateControlMechanism::ConstantQuantizer) => fixed_quantizer(vec![
250            (c"rc", "cqp".to_owned()),
251            (c"qp_i", q.clone()),
252            (c"qp_p", q),
253        ]),
254        // x264 and x265 take a true constant QP as `qp`; their `crf` is another scale (libx264 at
255        // `crf` 23-41 ran 0.8-2.1 dB below `qp` at the same number on the 1080p showcase).
256        ("x26x", RateControlMechanism::ConstantQuantizer) => fixed_quantizer(vec![(c"qp", q)]),
257        ("crf", RateControlMechanism::ConstantQuantizer) => fixed_quantizer(vec![(c"crf", q)]),
258        // These families open with `flags=+qscale global_quality=N` but none reads N as a QP, so a
259        // quantizer visibly falls back to bitrate. VideoToolbox reads it as 0-1 quality, higher
260        // better (an M4 wrote `very-high` smaller than `very-low`); MediaFoundation passes it
261        // unscaled as `AVEncVideoEncodeQP`; libavcodec's `mpeg4` reads each frame's `quality`,
262        // which no sink sets.
263        _ => return None,
264    })
265}
266
267/// Which form of a resolved rate control an encoder actually ran.
268#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq)]
269#[non_exhaustive]
270pub enum AppliedRateControl {
271    /// The constant quantizer.
272    Quantizer,
273    /// The bitrate: either none was asked for, or the encoder refused it.
274    Bitrate,
275}
276
277impl AppliedRateControl {
278    /// Stable identifier for the export result.
279    #[must_use]
280    pub const fn as_str(self) -> &'static str {
281        match self {
282            Self::Quantizer => "quantizer",
283            Self::Bitrate => "bitrate",
284        }
285    }
286}
287
288/// The constant-quantizer request for `encoder_name`, when `rate` carries a
289/// quantizer; `Err` names why it cannot be asked for at all.
290pub(crate) fn quantizer_request(
291    encoder_name: &str,
292    rate: VideoRateControl,
293) -> Option<Result<MechanismRequest, String>> {
294    let quantizer = rate.quantizer?;
295    Some(
296        mechanism_request(
297            encoder_name,
298            RateControlMechanism::ConstantQuantizer,
299            rate.bitrate,
300            quantizer,
301        )
302        .ok_or_else(|| format!("{encoder_name} has no constant-quantizer option alpha2 knows")),
303    )
304}