use std::process::{Command, Stdio};
use crate::cli::EncoderChoice;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Encoder {
Nvenc,
VideoToolbox,
Qsv,
Amf,
Software,
}
impl Encoder {
const HARDWARE_PRIORITY: [Encoder; 4] = [
Encoder::Nvenc,
Encoder::VideoToolbox,
Encoder::Qsv,
Encoder::Amf,
];
pub fn from_choice(choice: EncoderChoice) -> Option<Self> {
match choice {
EncoderChoice::Auto => None,
EncoderChoice::Nvenc => Some(Self::Nvenc),
EncoderChoice::Qsv => Some(Self::Qsv),
EncoderChoice::Amf => Some(Self::Amf),
EncoderChoice::Videotoolbox => Some(Self::VideoToolbox),
EncoderChoice::Software => Some(Self::Software),
}
}
pub fn codec_name(self) -> &'static str {
match self {
Self::Nvenc => "hevc_nvenc",
Self::VideoToolbox => "hevc_videotoolbox",
Self::Qsv => "hevc_qsv",
Self::Amf => "hevc_amf",
Self::Software => "libx265",
}
}
pub fn input_args(self) -> &'static [&'static str] {
match self {
Self::Nvenc => &["-hwaccel", "cuda"],
_ => &[],
}
}
pub fn video_args(self, cq: u8) -> Vec<String> {
let cq_str = cq.to_string();
let args: Vec<&str> = match self {
Self::Nvenc => vec![
"-c:v",
"hevc_nvenc",
"-preset",
"p7",
"-tune",
"hq",
"-rc",
"vbr",
"-cq",
&cq_str,
"-b:v",
"0",
"-spatial-aq",
"1",
"-temporal-aq",
"1",
"-aq-strength",
"4",
"-bf",
"3",
"-b_ref_mode",
"middle",
"-rc-lookahead",
"32",
],
Self::Qsv => vec![
"-c:v",
"hevc_qsv",
"-preset",
"veryslow",
"-global_quality",
&cq_str,
"-look_ahead",
"1",
],
Self::Amf => vec![
"-c:v", "hevc_amf", "-quality", "quality", "-rc", "cqp", "-qp_i", &cq_str, "-qp_p",
&cq_str,
],
Self::VideoToolbox => {
let q = videotoolbox_quality(cq).to_string();
return ["-c:v", "hevc_videotoolbox", "-q:v", &q]
.iter()
.map(|s| s.to_string())
.collect();
}
Self::Software => vec!["-c:v", "libx265", "-preset", "slow", "-crf", &cq_str],
};
args.into_iter().map(String::from).collect()
}
pub fn is_usable(self) -> bool {
let mut cmd = Command::new("ffmpeg");
cmd.args([
"-hide_banner",
"-loglevel",
"error",
"-nostdin",
"-f",
"lavfi",
"-i",
"color=c=black:s=640x360:r=1:d=1",
"-frames:v",
"1",
]);
cmd.args(self.video_args(30));
cmd.args(["-f", "null", "-"]);
cmd.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null());
cmd.status().map(|s| s.success()).unwrap_or(false)
}
pub fn detect() -> Self {
Self::HARDWARE_PRIORITY
.into_iter()
.find(|e| e.is_usable())
.unwrap_or(Self::Software)
}
}
fn videotoolbox_quality(cq: u8) -> u8 {
(100.0 - f64::from(cq) * 1.5).round().clamp(1.0, 100.0) as u8
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn nvenc_args_carry_the_cq_value() {
let args = Encoder::Nvenc.video_args(34);
let pos = args.iter().position(|a| a == "-cq").unwrap();
assert_eq!(args[pos + 1], "34");
assert_eq!(args[0], "-c:v");
assert_eq!(args[1], "hevc_nvenc");
}
#[test]
fn software_args_use_crf() {
let args = Encoder::Software.video_args(28);
assert!(args.windows(2).any(|w| w == ["-crf", "28"]));
}
#[test]
fn videotoolbox_quality_is_clamped() {
assert_eq!(videotoolbox_quality(0), 100);
assert_eq!(videotoolbox_quality(30), 55);
assert!(videotoolbox_quality(51) >= 1);
}
#[test]
fn only_nvenc_requests_hardware_decoding() {
assert!(!Encoder::Nvenc.input_args().is_empty());
assert!(Encoder::Software.input_args().is_empty());
assert!(Encoder::Qsv.input_args().is_empty());
}
#[test]
fn auto_has_no_concrete_encoder() {
assert_eq!(Encoder::from_choice(EncoderChoice::Auto), None);
assert_eq!(
Encoder::from_choice(EncoderChoice::Software),
Some(Encoder::Software)
);
}
}