#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CodecFamily {
ProRes,
DNxHR,
HEVC,
H264,
AV1,
VP9,
}
impl CodecFamily {
pub fn name(&self) -> &'static str {
match self {
CodecFamily::ProRes => "ProRes",
CodecFamily::DNxHR => "DNxHR",
CodecFamily::HEVC => "HEVC",
CodecFamily::H264 => "H.264",
CodecFamily::AV1 => "AV1",
CodecFamily::VP9 => "VP9",
}
}
pub fn all() -> &'static [CodecFamily] {
&[
CodecFamily::ProRes,
CodecFamily::DNxHR,
CodecFamily::HEVC,
CodecFamily::H264,
CodecFamily::AV1,
CodecFamily::VP9,
]
}
pub fn next(self) -> Self {
let all = Self::all();
let pos = all.iter().position(|&x| x == self).unwrap_or(0);
all[(pos + 1) % all.len()]
}
pub fn prev(self) -> Self {
let all = Self::all();
let pos = all.iter().position(|&x| x == self).unwrap_or(0);
all[(pos + all.len() - 1) % all.len()]
}
pub fn to_ffmpeg_args(
&self,
hevc_encoder: &str,
h264_encoder: &str,
av1_encoder: &str,
prores_encoder: &str,
prores: ProResProfile,
dnxhr: DnxhrProfile,
hevc: HevcProfile,
h264: H264Profile,
av1: Av1Profile,
vp9: Vp9Profile,
rate_control: &RateControl,
is_wide_gamut: bool,
) -> (String, String, Vec<String>) {
let mut base_codec_name: String = String::new();
let mut base_pix_fmt: String = String::new();
let mut base_extra: Vec<&'static str> = Vec::new();
match self {
CodecFamily::ProRes => {
let (profile_v, base_pix) = match prores {
ProResProfile::Proxy => ("0", "yuv422p10le"),
ProResProfile::LT => ("1", "yuv422p10le"),
ProResProfile::Standard => ("2", "yuv422p10le"),
ProResProfile::HQ => ("3", "yuv422p10le"),
ProResProfile::P4444 => ("4", "yuva444p10le"),
ProResProfile::XQ4444 => ("5", "yuva444p12le"),
};
let pix_fmt = match (is_wide_gamut, prores) {
(true, ProResProfile::P4444 | ProResProfile::XQ4444) => base_pix,
(true, _) => "gbrp10le",
(false, _) => base_pix,
};
base_codec_name = prores_encoder.to_string();
base_pix_fmt = pix_fmt.to_string();
base_extra = vec!["-profile:v", profile_v];
}
CodecFamily::DNxHR => {
let (profile_str, pix_fmt) = match dnxhr {
DnxhrProfile::SQ => ("dnxhr_sq", "yuv422p10le"),
DnxhrProfile::HD => ("dnxhr_hd", "yuv422p10le"),
DnxhrProfile::HDX => ("dnxhr_hdx", "yuv422p10le"),
DnxhrProfile::HQX => ("dnxhr_hqx", "yuv422p10le"),
DnxhrProfile::P444 => ("dnxhr_444", "yuv444p10le"),
};
base_codec_name = "dnxhd".to_string();
base_pix_fmt = pix_fmt.to_string();
base_extra = vec!["-profile:v", profile_str];
}
CodecFamily::HEVC => {
match hevc_encoder {
"libx265" => {
if is_wide_gamut {
base_codec_name = "libx265".to_string();
base_pix_fmt = "gbrp10le".to_string();
base_extra = vec![];
} else {
let pix_fmt = match hevc {
HevcProfile::Main10_420 => "yuv420p10le",
HevcProfile::Main10_444 => "yuv444p10le",
};
base_codec_name = "libx265".to_string();
base_pix_fmt = pix_fmt.to_string();
base_extra = vec!["-preset", "slow"];
}
}
"hevc_nvenc" => {
base_codec_name = "hevc_nvenc".to_string();
base_pix_fmt = "p010le".to_string();
base_extra = vec!["-preset", "p6"];
}
"hevc_amf" => {
base_codec_name = "hevc_amf".to_string();
base_pix_fmt = "p010le".to_string();
base_extra = vec!["-quality", "quality"];
}
"hevc_qsv" => {
base_codec_name = "hevc_qsv".to_string();
base_pix_fmt = "p010le".to_string();
}
"hevc_videotoolbox" => {
base_codec_name = "hevc_videotoolbox".to_string();
base_pix_fmt = "p010le".to_string();
base_extra = vec!["-realtime", "true"];
}
_ => {
if is_wide_gamut {
base_codec_name = "libx265".to_string();
base_pix_fmt = "gbrp10le".to_string();
base_extra = vec!["-preset", "slow"];
} else {
let pix_fmt = match hevc {
HevcProfile::Main10_420 => "yuv420p10le",
HevcProfile::Main10_444 => "yuv444p10le",
};
base_codec_name = "libx265".to_string();
base_pix_fmt = pix_fmt.to_string();
base_extra = vec!["-pix_fmt", pix_fmt, "-preset", "slow"];
}
}
}
}
CodecFamily::H264 => {
if is_wide_gamut {
base_codec_name = "libx265".to_string();
base_pix_fmt = "gbrp10le".to_string();
base_extra = vec!["-pix_fmt", "gbrp10le"];
} else {
match h264_encoder {
"h264_nvenc" => {
let (pf, ext) = match h264 {
H264Profile::High10bit => ("p010le", vec!["-preset", "p6", "-profile:v", "high10"]),
H264Profile::Main8bit => ("yuv420p", vec!["-preset", "p6"]),
};
base_codec_name = "h264_nvenc".to_string();
base_pix_fmt = pf.to_string();
base_extra = ext;
}
"h264_amf" => {
let (pf, ext) = match h264 {
H264Profile::High10bit => ("p010le", vec!["-quality", "quality"]),
H264Profile::Main8bit => ("yuv420p", vec!["-quality", "quality"]),
};
base_codec_name = "h264_amf".to_string();
base_pix_fmt = pf.to_string();
base_extra = ext;
}
"h264_qsv" => {
let pf = match h264 {
H264Profile::High10bit => "p010le",
H264Profile::Main8bit => "yuv420p",
};
base_codec_name = "h264_qsv".to_string();
base_pix_fmt = pf.to_string();
}
"h264_videotoolbox" => {
let pf = match h264 {
H264Profile::High10bit => "p010le",
H264Profile::Main8bit => "yuv420p",
};
base_codec_name = "h264_videotoolbox".to_string();
base_pix_fmt = pf.to_string();
base_extra = vec!["-realtime", "true"];
}
_ => {
let (pf, ext) = match h264 {
H264Profile::Main8bit => ("yuv420p", vec!["-preset", "slow"]),
H264Profile::High10bit => ("yuv422p10le", vec!["-preset", "slow"]),
};
base_codec_name = "libx264".to_string();
base_pix_fmt = pf.to_string();
base_extra = ext;
}
}
}
}
CodecFamily::AV1 => {
match av1_encoder {
"libsvtav1" => {
base_codec_name = "libsvtav1".to_string();
base_pix_fmt = match av1 {
Av1Profile::Profile0_420_10bit => "yuv420p10le",
Av1Profile::Profile1_444_10bit => "yuv444p10le",
}.to_string();
base_extra = vec!["-preset", "8"];
}
"av1_nvenc" => {
base_codec_name = "av1_nvenc".to_string();
base_pix_fmt = match av1 {
Av1Profile::Profile0_420_10bit => "p010le",
Av1Profile::Profile1_444_10bit => "yuv444p10le",
}.to_string();
base_extra = vec!["-preset", "p6"];
}
"av1_amf" => {
base_codec_name = "av1_amf".to_string();
base_pix_fmt = match av1 {
Av1Profile::Profile0_420_10bit => "p010le",
Av1Profile::Profile1_444_10bit => "yuv444p10le",
}.to_string();
base_extra = vec!["-quality", "quality"];
}
"av1_qsv" => {
base_codec_name = "av1_qsv".to_string();
base_pix_fmt = match av1 {
Av1Profile::Profile0_420_10bit => "p010le",
Av1Profile::Profile1_444_10bit => "yuv444p10le",
}.to_string();
}
_ => {
base_codec_name = "libsvtav1".to_string();
base_pix_fmt = match av1 {
Av1Profile::Profile0_420_10bit => "yuv420p10le",
Av1Profile::Profile1_444_10bit => "yuv444p10le",
}.to_string();
base_extra = vec!["-preset", "8"];
}
}
}
CodecFamily::VP9 => {
base_codec_name = "libvpx-vp9".to_string();
base_pix_fmt = match vp9 {
Vp9Profile::Profile2_420_10bit => "yuv420p10le".to_string(),
Vp9Profile::Profile3_444_10bit => "yuv444p10le".to_string(),
};
base_extra = vec![];
}
}
let mut extra: Vec<String> = base_extra.iter().map(|&s| s.to_string()).collect();
if is_wide_gamut && !base_pix_fmt.starts_with("gbrp") && !base_pix_fmt.starts_with("rgb") {
extra.push("-vf".into());
extra.push(format!("scale=flags=accurate_rnd+full_chroma_int:out_color_matrix=bt2020nc:out_range=full,format={}", base_pix_fmt));
}
match self {
CodecFamily::HEVC => {
extra.extend(rate_control_args(rate_control, hevc_encoder));
}
CodecFamily::H264 => {
extra.extend(rate_control_args(rate_control, h264_encoder));
}
CodecFamily::AV1 => {
extra.extend(rate_control_args(rate_control, av1_encoder));
}
CodecFamily::VP9 => {
extra.extend(rate_control_args(rate_control, "libvpx-vp9"));
}
CodecFamily::ProRes | CodecFamily::DNxHR => {}
}
tracing::debug!("ffmpeg args: codec={} pix_fmt={} extra={:?}",
base_codec_name, base_pix_fmt, extra);
(base_codec_name, base_pix_fmt, extra)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ProResProfile {
Proxy,
LT,
Standard,
HQ,
P4444,
XQ4444,
}
impl ProResProfile {
pub fn name(&self) -> &'static str {
match self {
ProResProfile::Proxy => "Proxy",
ProResProfile::LT => "LT",
ProResProfile::Standard => "Standard",
ProResProfile::HQ => "HQ",
ProResProfile::P4444 => "4444",
ProResProfile::XQ4444 => "4444 XQ",
}
}
pub fn all() -> &'static [ProResProfile] {
&[
ProResProfile::Proxy,
ProResProfile::LT,
ProResProfile::Standard,
ProResProfile::HQ,
ProResProfile::P4444,
ProResProfile::XQ4444,
]
}
pub fn next(self) -> Self {
let all = Self::all();
let pos = all.iter().position(|&x| x == self).unwrap_or(0);
all[(pos + 1) % all.len()]
}
pub fn prev(self) -> Self {
let all = Self::all();
let pos = all.iter().position(|&x| x == self).unwrap_or(0);
all[(pos + all.len() - 1) % all.len()]
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DnxhrProfile {
SQ,
HD,
HDX,
HQX,
P444,
}
impl DnxhrProfile {
pub fn name(&self) -> &'static str {
match self {
DnxhrProfile::SQ => "SQ",
DnxhrProfile::HD => "HD",
DnxhrProfile::HDX => "HDX",
DnxhrProfile::HQX => "HQX",
DnxhrProfile::P444 => "444",
}
}
pub fn all() -> &'static [DnxhrProfile] {
&[DnxhrProfile::SQ, DnxhrProfile::HD, DnxhrProfile::HDX, DnxhrProfile::HQX, DnxhrProfile::P444]
}
pub fn next(self) -> Self {
let all = Self::all();
let pos = all.iter().position(|&x| x == self).unwrap_or(0);
all[(pos + 1) % all.len()]
}
pub fn prev(self) -> Self {
let all = Self::all();
let pos = all.iter().position(|&x| x == self).unwrap_or(0);
all[(pos + all.len() - 1) % all.len()]
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum HevcProfile {
Main10_420,
Main10_444,
}
impl HevcProfile {
pub fn name(&self) -> &'static str {
match self {
HevcProfile::Main10_420 => "Main 10 4:2:0",
HevcProfile::Main10_444 => "Main 10 4:4:4",
}
}
pub fn is_8bit(&self) -> bool {
false
}
pub fn all() -> &'static [HevcProfile] {
&[HevcProfile::Main10_420, HevcProfile::Main10_444]
}
pub fn next(self) -> Self {
let all = Self::all();
let pos = all.iter().position(|&x| x == self).unwrap_or(0);
all[(pos + 1) % all.len()]
}
pub fn prev(self) -> Self {
let all = Self::all();
let pos = all.iter().position(|&x| x == self).unwrap_or(0);
all[(pos + all.len() - 1) % all.len()]
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum H264Profile {
Main8bit,
High10bit,
}
impl H264Profile {
pub fn name(&self) -> &'static str {
match self {
H264Profile::Main8bit => "Main 8-bit",
H264Profile::High10bit => "High 10-bit",
}
}
pub fn is_8bit(&self) -> bool {
matches!(self, H264Profile::Main8bit)
}
pub fn all() -> &'static [H264Profile] {
&[H264Profile::Main8bit, H264Profile::High10bit]
}
pub fn next(self) -> Self {
let all = Self::all();
let pos = all.iter().position(|&x| x == self).unwrap_or(0);
all[(pos + 1) % all.len()]
}
pub fn prev(self) -> Self {
let all = Self::all();
let pos = all.iter().position(|&x| x == self).unwrap_or(0);
all[(pos + all.len() - 1) % all.len()]
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Av1Profile {
Profile0_420_10bit,
Profile1_444_10bit,
}
impl Av1Profile {
pub fn name(&self) -> &'static str {
match self {
Av1Profile::Profile0_420_10bit => "Profile 0 4:2:0 10-bit",
Av1Profile::Profile1_444_10bit => "Profile 1 4:4:4 10-bit",
}
}
pub fn is_8bit(&self) -> bool {
false
}
pub fn all() -> &'static [Av1Profile] {
&[Av1Profile::Profile0_420_10bit, Av1Profile::Profile1_444_10bit]
}
pub fn next(self) -> Self {
let all = Self::all();
let pos = all.iter().position(|&x| x == self).unwrap_or(0);
all[(pos + 1) % all.len()]
}
pub fn prev(self) -> Self {
let all = Self::all();
let pos = all.iter().position(|&x| x == self).unwrap_or(0);
all[(pos + all.len() - 1) % all.len()]
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Vp9Profile {
Profile2_420_10bit,
Profile3_444_10bit,
}
impl Vp9Profile {
pub fn name(&self) -> &'static str {
match self {
Vp9Profile::Profile2_420_10bit => "Profile 2 4:2:0 10-bit",
Vp9Profile::Profile3_444_10bit => "Profile 3 4:4:4 10-bit",
}
}
pub fn is_8bit(&self) -> bool {
false
}
pub fn all() -> &'static [Vp9Profile] {
&[Vp9Profile::Profile2_420_10bit, Vp9Profile::Profile3_444_10bit]
}
pub fn next(self) -> Self {
let all = Self::all();
let pos = all.iter().position(|&x| x == self).unwrap_or(0);
all[(pos + 1) % all.len()]
}
pub fn prev(self) -> Self {
let all = Self::all();
let pos = all.iter().position(|&x| x == self).unwrap_or(0);
all[(pos + all.len() - 1) % all.len()]
}
}
#[derive(Debug, Clone)]
pub enum RateControl {
Lossless,
High,
Standard,
Master400M,
Standard150M,
Custom(String),
}
impl RateControl {
pub fn name(&self) -> String {
match self {
RateControl::Lossless => "Lossless".to_string(),
RateControl::High => "High Quality".to_string(),
RateControl::Standard => "Standard".to_string(),
RateControl::Master400M => "Master 400M".to_string(),
RateControl::Standard150M => "Standard 150M".to_string(),
RateControl::Custom(v) => {
if v.is_empty() {
"Custom: []".to_string()
} else {
format!("Custom: [{}]", v)
}
}
}
}
pub fn next(&self) -> Self {
match self {
RateControl::Lossless => RateControl::High,
RateControl::High => RateControl::Standard,
RateControl::Standard => RateControl::Master400M,
RateControl::Master400M => RateControl::Standard150M,
RateControl::Standard150M => RateControl::Custom(String::new()),
RateControl::Custom(_) => RateControl::Lossless,
}
}
pub fn prev(&self) -> Self {
match self {
RateControl::Lossless => RateControl::Custom(String::new()),
RateControl::High => RateControl::Lossless,
RateControl::Standard => RateControl::High,
RateControl::Master400M => RateControl::Standard,
RateControl::Standard150M => RateControl::Master400M,
RateControl::Custom(_) => RateControl::Standard150M,
}
}
}
pub fn rate_control_args(rc: &RateControl, encoder_name: &str) -> Vec<String> {
let is_hw = !encoder_name.starts_with("lib");
let is_videotoolbox = encoder_name.ends_with("_videotoolbox");
let is_nvenc = encoder_name.ends_with("_nvenc");
let needs_bv0_for_crf = matches!(encoder_name, "libvpx-vp9" | "libaom-av1");
let cq = |value: &str| -> Vec<String> {
if is_videotoolbox {
vec!["-quality".into(), value.into()]
} else if is_hw {
vec!["-cq".into(), value.into()]
} else if needs_bv0_for_crf {
vec!["-crf".into(), value.into(), "-b:v".into(), "0".into()]
} else {
vec!["-crf".into(), value.into()]
}
};
let bitrate = |value: &str| -> Vec<String> {
let mut v = vec![
"-b:v".into(), value.into(),
"-maxrate".into(), value.into(),
];
if is_nvenc {
v.push("-rc:v".into());
v.push("vbr".into());
}
v
};
match rc {
RateControl::Lossless => {
if is_videotoolbox {
vec!["-quality".into(), "lossless".into()]
} else {
cq("16")
}
}
RateControl::High => {
if is_videotoolbox {
vec!["-quality".into(), "max".into()]
} else {
cq("20")
}
}
RateControl::Standard => {
if is_videotoolbox {
vec!["-quality".into(), "high".into()]
} else {
cq("24")
}
}
RateControl::Master400M => bitrate("400M"),
RateControl::Standard150M => bitrate("150M"),
RateControl::Custom(val) => {
if val.is_empty() {
return vec![];
}
let upper = val.to_uppercase();
if upper.ends_with('M') || upper.ends_with('K') {
bitrate(val)
} else if val.parse::<f64>().is_ok() {
cq(val)
} else {
vec![val.clone()]
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn rate_control_lossless_software_uses_crf() {
let args = rate_control_args(&RateControl::Lossless, "libx265");
assert_eq!(args, vec!["-crf", "16"]);
}
#[test]
fn rate_control_lossless_nvenc_uses_cq() {
let args = rate_control_args(&RateControl::Lossless, "hevc_nvenc");
assert_eq!(args, vec!["-cq", "16"]);
}
#[test]
fn rate_control_lossless_videotoolbox_uses_quality_lossless() {
let args = rate_control_args(&RateControl::Lossless, "hevc_videotoolbox");
assert_eq!(args, vec!["-quality", "lossless"]);
}
#[test]
fn rate_control_nvenc_bitrate_mode_pins_rc_to_vbr() {
let args = rate_control_args(&RateControl::Master400M, "hevc_nvenc");
assert!(args.contains(&"-b:v".to_string()));
assert!(args.contains(&"-maxrate".to_string()));
assert!(args.contains(&"-rc:v".to_string()));
assert!(args.contains(&"vbr".to_string()));
}
#[test]
fn rate_control_vp9_crf_adds_bv0() {
let args = rate_control_args(&RateControl::Standard, "libvpx-vp9");
assert!(args.contains(&"-crf".to_string()));
assert!(args.contains(&"24".to_string()));
assert!(args.contains(&"-b:v".to_string()));
assert!(args.contains(&"0".to_string()));
}
#[test]
fn rate_control_libaom_av1_crf_adds_bv0() {
let args = rate_control_args(&RateControl::High, "libaom-av1");
assert!(args.contains(&"-crf".to_string()));
assert!(args.contains(&"20".to_string()));
assert!(args.contains(&"-b:v".to_string()));
assert!(args.contains(&"0".to_string()));
}
#[test]
fn rate_control_custom_numeric_routes_to_cq() {
let args = rate_control_args(&RateControl::Custom("18".into()), "libx265");
assert_eq!(args, vec!["-crf", "18"]);
}
#[test]
fn rate_control_custom_bitrate_routes_to_bv() {
let args = rate_control_args(&RateControl::Custom("50M".into()), "libx265");
assert!(args.contains(&"-b:v".to_string()));
assert!(args.contains(&"50M".to_string()));
}
#[test]
fn rate_control_custom_empty_returns_empty() {
let args = rate_control_args(&RateControl::Custom(String::new()), "libx265");
assert!(args.is_empty());
}
}