Skip to main content

ffcine_render/
encode.rs

1//! Encoder settings, backend-neutral (names follow ffmpeg's encoder options).
2
3use std::fmt;
4use std::str::FromStr;
5
6use crate::{ColorSpace, Error};
7use ffcine_core::color::named_enum;
8
9named_enum!(
10    /// Video encoder.
11    Codec {
12        HevcNvenc = "hevc_nvenc",
13        H264Nvenc = "h264_nvenc",
14        Av1Nvenc = "av1_nvenc",
15        HevcVaapi = "hevc_vaapi",
16        /// Vulkan video encode: takes the rendered Vulkan frames with no copy (no presets).
17        HevcVulkan = "hevc_vulkan",
18        H264Vulkan = "h264_vulkan",
19        Av1Vulkan = "av1_vulkan",
20        Libx264 = "libx264",
21        Libx265 = "libx265",
22        /// PNG stills (`ffcine still`), with [`PixelFormat::Rgba`].
23        Png = "png",
24    }
25);
26
27impl Codec {
28    /// Hardware encoders that can take GPU frames without a download.
29    pub const fn is_hardware(self) -> bool {
30        matches!(
31            self,
32            Self::HevcNvenc | Self::H264Nvenc | Self::Av1Nvenc | Self::HevcVaapi
33        ) || self.is_vulkan()
34    }
35
36    /// Vulkan video encoders (fed Vulkan frames directly).
37    pub const fn is_vulkan(self) -> bool {
38        matches!(self, Self::HevcVulkan | Self::H264Vulkan | Self::Av1Vulkan)
39    }
40}
41
42named_enum!(
43    /// Pixel format handed to the encoder.
44    PixelFormat {
45        /// 10-bit 4:2:0, semi-planar (NVENC main10).
46        P010 = "p010le",
47        Nv12 = "nv12",
48        Yuv420p = "yuv420p",
49        Yuv420p10 = "yuv420p10le",
50        Gbrp16 = "gbrp16le",
51        /// 8-bit packed RGBA (PNG stills).
52        Rgba = "rgba",
53    }
54);
55
56/// Rate control.
57#[derive(Debug, Clone, Copy, PartialEq)]
58pub enum RateControl {
59    /// Constant QP (NVENC `-rc constqp -qp N`).
60    ConstQp(u8),
61    /// Constant rate factor / constant quality (`-crf N` or NVENC `-cq N`).
62    Crf(f32),
63    /// Average bitrate in bits/s.
64    Bitrate(u64),
65}
66
67impl FromStr for RateControl {
68    type Err = Error;
69    /// `qp=12`, `crf=18` or `bitrate=50000000`.
70    fn from_str(s: &str) -> Result<Self, Error> {
71        let invalid = || Error::Invalid {
72            what: "rate control (qp=N, crf=N, bitrate=N)",
73            value: s.to_owned(),
74        };
75        let (k, v) = s.split_once('=').ok_or_else(invalid)?;
76        match k {
77            "qp" => v.parse().map(Self::ConstQp).map_err(|_| invalid()),
78            "crf" => v.parse().map(Self::Crf).map_err(|_| invalid()),
79            "bitrate" => v.parse().map(Self::Bitrate).map_err(|_| invalid()),
80            _ => Err(invalid()),
81        }
82    }
83}
84
85impl fmt::Display for RateControl {
86    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
87        match self {
88            Self::ConstQp(q) => write!(f, "qp={q}"),
89            Self::Crf(c) => write!(f, "crf={c}"),
90            Self::Bitrate(b) => write!(f, "bitrate={b}"),
91        }
92    }
93}
94
95/// How to encode the graded video.
96#[derive(Debug, Clone, PartialEq)]
97pub struct EncoderSettings {
98    pub codec: Codec,
99    pub preset: Option<String>,
100    pub tune: Option<String>,
101    pub profile: Option<String>,
102    pub rate: RateControl,
103    pub pixel_format: PixelFormat,
104    /// Keyframe interval in frames.
105    pub gop: Option<u32>,
106    /// Tags written to the stream; should match [`crate::Grade::output`].
107    pub color: ColorSpace,
108    /// Raw encoder options passed through untouched (`key`, `value`).
109    pub extra: Vec<(String, String)>,
110}
111
112impl EncoderSettings {
113    /// 10-bit HEVC NVENC intermediate/master: `hevc_nvenc -preset p6 -tune hq -rc constqp -qp 12
114    /// -profile:v main10 -pix_fmt p010le -g 24`, as in the prototype.
115    pub fn nvenc_hevc_master() -> Self {
116        Self {
117            codec: Codec::HevcNvenc,
118            preset: Some("p6".into()),
119            tune: Some("hq".into()),
120            profile: Some("main10".into()),
121            rate: RateControl::ConstQp(12),
122            pixel_format: PixelFormat::P010,
123            gop: Some(24),
124            color: ColorSpace::BT709_TV,
125            extra: Vec::new(),
126        }
127    }
128
129    /// 8-bit H.264 preview: `libx264 -preset veryfast -crf 10 -pix_fmt yuv420p -g 24`.
130    pub fn x264_preview() -> Self {
131        Self {
132            codec: Codec::Libx264,
133            preset: Some("veryfast".into()),
134            tune: None,
135            profile: None,
136            rate: RateControl::Crf(10.0),
137            pixel_format: PixelFormat::Yuv420p,
138            gop: Some(24),
139            color: ColorSpace::BT709_TV,
140            extra: Vec::new(),
141        }
142    }
143}
144
145impl EncoderSettings {
146    /// A full-range RGB PNG frame, for stills; the grade's `output` must be
147    /// [`ColorSpace::BT709_RGB`].
148    pub fn png_still() -> Self {
149        Self {
150            codec: Codec::Png,
151            preset: None,
152            tune: None,
153            profile: None,
154            rate: RateControl::ConstQp(0),
155            pixel_format: PixelFormat::Rgba,
156            gop: None,
157            color: ColorSpace::BT709_RGB,
158            extra: Vec::new(),
159        }
160    }
161}
162
163impl Default for EncoderSettings {
164    fn default() -> Self {
165        Self::nvenc_hevc_master()
166    }
167}
168
169#[cfg(test)]
170mod tests {
171    use super::*;
172
173    #[test]
174    fn rate_control_round_trip() {
175        for s in ["qp=12", "crf=18.5", "bitrate=50000000"] {
176            assert_eq!(s.parse::<RateControl>().unwrap().to_string(), s);
177        }
178        assert!("vbr".parse::<RateControl>().is_err());
179    }
180
181    #[test]
182    fn master_preset() {
183        let e = EncoderSettings::default();
184        assert!(e.codec.is_hardware());
185        assert_eq!(e.pixel_format.as_str(), "p010le");
186    }
187}