Skip to main content

alpha2_native/
rate_control.rs

1//! How a resolved rate control reaches one concrete encoder.
2//!
3//! Sinks receive numbers ([`VideoRateControl`]), never a quality level. Encoders differ only in
4//! spelling (`h264_vulkan` takes `rc_mode`, NVENC `rc`, `MediaFoundation` `rate_control`), and
5//! every spelling lives here so the software and GPU-resident sinks agree.
6
7use alpha2_media::{BitrateMode, VideoRateControl};
8use rsmpeg::avutil::AVDictionary;
9use std::ffi::CStr;
10
11/// The private option that puts `encoder_name` into constant-bitrate mode, if its family has one.
12///
13/// Others rely on `minrate`/`maxrate`/`bufsize` alone, as libavcodec's own encoders and `libvpx`
14/// do. `libsvtav1` uses `svtav1-params` instead ([`mechanism_request`]).
15pub(crate) fn constant_mode_option(encoder_name: &str) -> Option<(&'static CStr, &'static CStr)> {
16    if encoder_name.ends_with("_vulkan") {
17        Some((c"rc_mode", c"cbr"))
18    } else if encoder_name.ends_with("_nvenc") || encoder_name.ends_with("_amf") {
19        Some((c"rc", c"cbr"))
20    } else if encoder_name.ends_with("_vaapi") || encoder_name.ends_with("_d3d12va") {
21        Some((c"rc_mode", c"CBR"))
22    } else if encoder_name.ends_with("_mf") {
23        Some((c"rate_control", c"cbr"))
24    } else if encoder_name.ends_with("_videotoolbox") {
25        Some((c"constant_bit_rate", c"1"))
26    } else {
27        None
28    }
29}
30
31/// A hint for an open failure under a constant `rate`: `VideoToolbox` has CBR only from macOS 13,
32/// D3D12 Video Encode only on some devices (others refuse with `HRESULT` 0x887A0020), and `FFmpeg`
33/// reports either as a generic error.
34pub(crate) fn constant_rate_hint(encoder_name: &str, rate: VideoRateControl) -> &'static str {
35    if rate.mode != BitrateMode::Constant {
36        ""
37    } else if encoder_name.ends_with("_videotoolbox") {
38        " (a constant bit rate was asked for, which VideoToolbox has only on macOS 13 and later)"
39    } else if encoder_name.ends_with("_d3d12va") {
40        " (a constant bit rate was asked for, which D3D12 Video Encode has only on some devices; \
41         ask for a quantizer instead)"
42    } else {
43        ""
44    }
45}
46
47/// Adds what `rate` needs beyond `AVCodecContext::bit_rate` to `options`.
48///
49/// A variable rate needs nothing more. A constant rate pins min and max against a
50/// one-second buffer and sets the family's mode switch, so `reject_unused_encode_options`
51/// catches an encoder that ignores it.
52pub(crate) fn rate_control_options(
53    encoder_name: &str,
54    rate: VideoRateControl,
55    options: AVDictionary,
56) -> AVDictionary {
57    match rate.mode {
58        BitrateMode::Variable => options,
59        BitrateMode::Constant => {
60            match mechanism_request(
61                encoder_name,
62                RateControlMechanism::ConstantBitrate,
63                rate.bitrate,
64                0,
65            ) {
66                Some(request) => request.dictionary(options),
67                None => options,
68            }
69        }
70    }
71}
72
73/// A way of controlling an encoder's rate, independent of family spelling.
74#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq)]
75#[non_exhaustive]
76pub enum RateControlMechanism {
77    /// Constant bitrate.
78    ConstantBitrate,
79    /// Variable (average) bitrate.
80    VariableBitrate,
81    /// Constant quantizer: `FFmpeg`'s `cqp`/`constqp`, the family of `crf`.
82    ConstantQuantizer,
83}
84
85impl RateControlMechanism {
86    /// Every mechanism, in the order the probe reports them.
87    pub const ALL: [Self; 3] = [
88        Self::ConstantBitrate,
89        Self::VariableBitrate,
90        Self::ConstantQuantizer,
91    ];
92
93    /// Stable identifier for diagnostics.
94    #[must_use]
95    pub const fn as_str(self) -> &'static str {
96        match self {
97            Self::ConstantBitrate => "cbr",
98            Self::VariableBitrate => "vbr",
99            Self::ConstantQuantizer => "cqp",
100        }
101    }
102}
103
104/// The options that ask one encoder for one mechanism.
105#[derive(Clone, Debug, Eq, PartialEq)]
106pub(crate) struct MechanismRequest {
107    /// Private and generic `AVOption`s, in the order they are set.
108    pub options: Vec<(&'static CStr, String)>,
109    /// `AVCodecContext::bit_rate`; zero for a quantizer mode, so the
110    /// encoder has no target to prefer.
111    pub bit_rate: i64,
112}
113
114impl MechanismRequest {
115    /// The options as `key=value` pairs, for a report.
116    pub fn describe(&self) -> String {
117        let mut parts = self
118            .options
119            .iter()
120            .map(|(key, value)| format!("{}={value}", key.to_string_lossy()))
121            .collect::<Vec<_>>();
122        if self.bit_rate > 0 {
123            parts.insert(0, format!("b={}", self.bit_rate));
124        }
125        parts.join(" ")
126    }
127
128    /// The options added to a dictionary to open a context with.
129    pub fn dictionary(&self, mut options: AVDictionary) -> AVDictionary {
130        for (key, value) in &self.options {
131            options = crate::convert::set_encoder_option(options, key, value);
132        }
133        options
134    }
135}
136
137/// How `encoder_name` is asked for `mechanism`, or `None` where alpha2 knows no spelling.
138///
139/// `quantizer` is on the codec's own scale: 0-51 for H.264/HEVC, 0-255 for NVENC and Vulkan AV1,
140/// 0-52 for every D3D12VA codec. Device support is left to the probe and the open-time fallback.
141pub(crate) fn mechanism_request(
142    encoder_name: &str,
143    mechanism: RateControlMechanism,
144    bitrate: u64,
145    quantizer: u32,
146) -> Option<MechanismRequest> {
147    let target = i64::try_from(bitrate).unwrap_or(i64::MAX);
148    let constant = |switch: Option<(&'static CStr, &'static str)>| {
149        let mut options = vec![
150            (c"maxrate", bitrate.to_string()),
151            (c"minrate", bitrate.to_string()),
152            (c"bufsize", bitrate.to_string()),
153        ];
154        if let Some((key, value)) = switch {
155            options.push((key, value.to_owned()));
156        }
157        MechanismRequest {
158            options,
159            bit_rate: target,
160        }
161    };
162    let switched = |key: &'static CStr, value: &str| MechanismRequest {
163        options: vec![(key, value.to_owned())],
164        bit_rate: target,
165    };
166    let fixed_quantizer = |options: Vec<(&'static CStr, String)>| MechanismRequest {
167        options,
168        bit_rate: 0,
169    };
170    let q = quantizer.to_string();
171    let family = if encoder_name.ends_with("_vulkan") {
172        "vulkan"
173    } else if encoder_name.ends_with("_nvenc") {
174        "nvenc"
175    } else if encoder_name.ends_with("_vaapi") {
176        "vaapi"
177    } else if encoder_name.ends_with("_d3d12va") {
178        "d3d12va"
179    } else if encoder_name.ends_with("_amf") {
180        "amf"
181    } else if encoder_name.ends_with("_mf") {
182        "mf"
183    } else if encoder_name.ends_with("_videotoolbox") {
184        "videotoolbox"
185    } else if encoder_name.ends_with("_qsv") {
186        "qsv"
187    } else if matches!(encoder_name, "libx264" | "libx265") {
188        "x26x"
189    } else if matches!(
190        encoder_name,
191        "libvpx" | "libvpx-vp9" | "libaom-av1" | "libsvtav1"
192    ) {
193        "crf"
194    } else {
195        "libavcodec"
196    };
197    Some(match (family, mechanism) {
198        // SVT-AV1 has CBR only in its low-delay structure and refuses a maximum bitrate
199        // outside its quantizer mode, so the switch is spelled in its own parameters
200        // against a one-second buffer.
201        ("crf", RateControlMechanism::ConstantBitrate) if encoder_name == "libsvtav1" => {
202            MechanismRequest {
203                options: vec![
204                    (c"bufsize", bitrate.to_string()),
205                    (c"svtav1-params", "rc=2:pred-struct=1".to_owned()),
206                ],
207                bit_rate: target,
208            }
209        }
210        (_, RateControlMechanism::ConstantBitrate) => constant(
211            constant_mode_option(encoder_name)
212                .map(|(key, value)| (key, value.to_str().unwrap_or_default())),
213        ),
214        ("vulkan", RateControlMechanism::VariableBitrate) => switched(c"rc_mode", "vbr"),
215        ("nvenc", RateControlMechanism::VariableBitrate) => switched(c"rc", "vbr"),
216        ("vaapi" | "d3d12va", RateControlMechanism::VariableBitrate) => switched(c"rc_mode", "VBR"),
217        ("amf", RateControlMechanism::VariableBitrate) => switched(c"rc", "vbr_peak"),
218        ("mf", RateControlMechanism::VariableBitrate) => switched(c"rate_control", "u_vbr"),
219        (_, RateControlMechanism::VariableBitrate) => MechanismRequest {
220            options: Vec::new(),
221            bit_rate: target,
222        },
223        ("vulkan", RateControlMechanism::ConstantQuantizer) => {
224            fixed_quantizer(vec![(c"rc_mode", "cqp".to_owned()), (c"qp", q)])
225        }
226        ("nvenc", RateControlMechanism::ConstantQuantizer) => {
227            fixed_quantizer(vec![(c"rc", "constqp".to_owned()), (c"qp", q)])
228        }
229        // D3D12VA derives I and B quantizers from `qp` via `i_qfactor`/`b_qfactor` and offsets,
230        // left at the encoder defaults (1 and 0 for HEVC/AV1; 0.8 and 1.25 + 1.25 for H.264).
231        ("vaapi" | "d3d12va", RateControlMechanism::ConstantQuantizer) => {
232            fixed_quantizer(vec![(c"rc_mode", "CQP".to_owned()), (c"qp", q)])
233        }
234        ("amf", RateControlMechanism::ConstantQuantizer) => fixed_quantizer(vec![
235            (c"rc", "cqp".to_owned()),
236            (c"qp_i", q.clone()),
237            (c"qp_p", q),
238        ]),
239        // x264 and x265 take a true constant QP as `qp`; their `crf` is a different scale.
240        ("x26x", RateControlMechanism::ConstantQuantizer) => fixed_quantizer(vec![(c"qp", q)]),
241        ("crf", RateControlMechanism::ConstantQuantizer) => fixed_quantizer(vec![(c"crf", q)]),
242        // `VideoToolbox` has no QP: `flags=+qscale` makes `FFmpeg` pass
243        // `global_quality / (FF_QP2LAMBDA * 100)` as `kVTCompressionPropertyKey_Quality`, so
244        // the quantizer is mapped onto that 0-1 scale ([`videotoolbox_quality`]). `FFmpeg`
245        // refuses `qscale` off Apple silicon, where the bitrate fallback runs. The map is
246        // H.264's; HEVC keeps the bitrate.
247        ("videotoolbox", RateControlMechanism::ConstantQuantizer)
248            if encoder_name == "h264_videotoolbox" =>
249        {
250            fixed_quantizer(videotoolbox_quality_options(quantizer))
251        }
252        // These families accept `flags=+qscale global_quality=N` but none reads N as a QP, so the
253        // quantizer falls back to bitrate visibly. `MediaFoundation` passes it unscaled as
254        // `AVEncVideoEncodeQP`; libavcodec's `mpeg4` reads per-frame `quality`, which no sink sets.
255        _ => return None,
256    })
257}
258
259/// `FFmpeg`'s `FF_QP2LAMBDA`, which `qscale` encoders divide `global_quality` by.
260const FF_QP2LAMBDA: u32 = 118;
261
262/// `VideoToolbox` quality (`kVTCompressionPropertyKey_Quality`, 0-1, higher better) per H.264
263/// quantizer at the levels' quantizers (`alpha2_media::level_quantizer`).
264///
265/// Each point holds `h264_videotoolbox` on Apple silicon within 0.5 dB PSNR-Y of `OpenH264` at the
266/// same level on a complex 1080p30 project (40.6 to 52.0 dB), at 0.66x-0.85x its bitrate. Grainy
267/// content needs about 0.05-0.10 more for the same match.
268const VIDEOTOOLBOX_QUALITY: [(u32, f64); 5] =
269    [(23, 0.65), (27, 0.57), (31, 0.50), (35, 0.40), (41, 0.25)];
270
271/// The `VideoToolbox` quality for an H.264 `quantizer`: [`VIDEOTOOLBOX_QUALITY`] interpolated,
272/// extrapolated along its end segments, rounded to hundredths and clamped to 0.01-1.
273pub(crate) fn videotoolbox_quality(quantizer: u32) -> f64 {
274    let table = &VIDEOTOOLBOX_QUALITY;
275    let segment = table
276        .windows(2)
277        .find(|pair| quantizer <= pair[1].0)
278        .unwrap_or(&table[table.len() - 2..]);
279    let ((q0, v0), (q1, v1)) = (segment[0], segment[1]);
280    let position = (f64::from(quantizer) - f64::from(q0)) / (f64::from(q1) - f64::from(q0));
281    let quality = v0 + (v1 - v0) * position;
282    (quality * 100.0).round().clamp(1.0, 100.0) / 100.0
283}
284
285/// Options that make `FFmpeg` pass [`videotoolbox_quality`]: `global_quality` is the hundredths
286/// times `FF_QP2LAMBDA`, which `FFmpeg` divides back exactly.
287fn videotoolbox_quality_options(quantizer: u32) -> Vec<(&'static CStr, String)> {
288    #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
289    let hundredths = (videotoolbox_quality(quantizer) * 100.0).round() as u32;
290    vec![
291        (c"flags", "+qscale".to_owned()),
292        (c"global_quality", (hundredths * FF_QP2LAMBDA).to_string()),
293    ]
294}
295
296/// Which form of a resolved rate control an encoder actually ran.
297#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq)]
298#[non_exhaustive]
299pub enum AppliedRateControl {
300    /// The constant quantizer.
301    Quantizer,
302    /// The quantizer mapped onto the encoder's own quality scale (`VideoToolbox`'s 0-1 `Quality`).
303    Quality,
304    /// The bitrate: none was asked for, or the encoder refused the quantizer.
305    Bitrate,
306}
307
308impl AppliedRateControl {
309    /// Stable identifier for the export result.
310    #[must_use]
311    pub const fn as_str(self) -> &'static str {
312        match self {
313            Self::Quantizer => "quantizer",
314            Self::Quality => "quality",
315            Self::Bitrate => "bitrate",
316        }
317    }
318
319    /// What `encoder_name` runs when it opened with its quantizer request.
320    pub(crate) fn of_quantizer_request(encoder_name: &str) -> Self {
321        if encoder_name.ends_with("_videotoolbox") {
322            Self::Quality
323        } else {
324            Self::Quantizer
325        }
326    }
327}
328
329/// How `OpenH264` runs `rate`, and which form that is.
330///
331/// `OpenH264` is driven through its own API; with rate control off it encodes every picture at the
332/// layer QP. Without a quantizer it runs the bitrate, also capped when the rate is constant.
333pub(crate) fn openh264_rate(
334    rate: VideoRateControl,
335) -> (crate::openh264::OpenH264Rate, AppliedRateControl) {
336    match rate.quantizer {
337        Some(quantizer) => (
338            crate::openh264::OpenH264Rate::Quantizer(quantizer),
339            AppliedRateControl::Quantizer,
340        ),
341        None => (
342            crate::openh264::OpenH264Rate::Bitrate {
343                target: rate.bitrate,
344                constant: rate.mode == BitrateMode::Constant,
345            },
346            AppliedRateControl::Bitrate,
347        ),
348    }
349}
350
351/// The constant-quantizer request for `encoder_name` when `rate` carries a quantizer; `Err` says
352/// why it cannot be asked for.
353pub(crate) fn quantizer_request(
354    encoder_name: &str,
355    rate: VideoRateControl,
356) -> Option<Result<MechanismRequest, String>> {
357    let quantizer = rate.quantizer?;
358    Some(
359        mechanism_request(
360            encoder_name,
361            RateControlMechanism::ConstantQuantizer,
362            rate.bitrate,
363            quantizer,
364        )
365        .ok_or_else(|| format!("{encoder_name} has no constant-quantizer option alpha2 knows")),
366    )
367}