//! Choosing an encoder from the ones a machine actually has. use std::sync::LazyLock; use regex::Regex; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; /// A line of an encoder listing: flags, then the name. static LISTED: LazyLock = LazyLock::new(|| Regex::new(r"(?m)^\s*[A-Z.]{6}\s+(\S+)").expect("a valid pattern")); /// An H.264 encoder, in the order they are preferred. /// /// Hardware first, because a re-encode of a two-hour recording is the /// difference between three minutes and forty. The software encoder is last /// and is always there. #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "snake_case")] pub enum Encoder { /// NVIDIA's hardware encoder. H264Nvenc, /// Apple's, on Apple silicon and recent Macs. H264Videotoolbox, /// The one AMD and Intel graphics offer through VAAPI. H264Vaapi, /// Software. Always available, and the slowest by a wide margin. Libx264, /// The stream is already H.264 or HEVC and is copied rather than encoded. Copy, } impl Encoder { /// What ffmpeg calls it. pub fn name(self) -> &'static str { match self { Encoder::H264Nvenc => "h264_nvenc", Encoder::H264Videotoolbox => "h264_videotoolbox", Encoder::H264Vaapi => "h264_vaapi", Encoder::Libx264 => "libx264", Encoder::Copy => "copy", } } /// Whether this one runs on the graphics card. pub fn is_hardware(self) -> bool { matches!( self, Encoder::H264Nvenc | Encoder::H264Videotoolbox | Encoder::H264Vaapi ) } /// The arguments that have to come before the input. /// /// Only VAAPI needs any: the device has to be opened before the file is, /// so a filter can upload frames to it. pub fn before_input(self, vaapi_device: &str) -> Vec { match self { Encoder::H264Vaapi => vec![ "-init_hw_device".into(), format!("vaapi=va:{vaapi_device}"), "-filter_hw_device".into(), "va".into(), ], _ => Vec::new(), } } /// The arguments that set up the video stream. /// /// Each encoder is asked for roughly the same quality, in whichever of /// the four quality dialects it happens to speak. pub fn video_arguments(self) -> Vec { let args: &[&str] = match self { Encoder::Copy => &["-c:v", "copy"], Encoder::H264Nvenc => &[ "-c:v", "h264_nvenc", "-preset", "p5", "-rc", "vbr", "-cq", "23", "-b:v", "0", ], Encoder::H264Videotoolbox => &[ "-c:v", "h264_videotoolbox", "-q:v", "60", "-profile:v", "high", "-pix_fmt", "yuv420p", ], // Software decode and an upload to the card, so that a source the // GPU's own decoder does not understand still encodes. Encoder::H264Vaapi => &[ "-vf", "format=nv12,hwupload", "-c:v", "h264_vaapi", "-qp", "23", ], Encoder::Libx264 => &[ "-c:v", "libx264", "-preset", "veryfast", "-crf", "23", "-pix_fmt", "yuv420p", ], }; args.iter().map(|arg| (*arg).to_string()).collect() } } /// The encoders this knows how to use, best first. pub const PREFERRED: &[Encoder] = &[ Encoder::H264Nvenc, Encoder::H264Videotoolbox, Encoder::H264Vaapi, Encoder::Libx264, ]; /// The best encoder present in an encoder listing. /// /// `None` when the listing has none of them, which means the installed /// ffmpeg cannot produce H.264 at all. pub fn best_available(listing: &str) -> Option { let available: Vec<&str> = LISTED .captures_iter(listing) .map(|found| found.get(1).expect("a name").as_str()) .collect(); PREFERRED .iter() .copied() .find(|encoder| available.contains(&encoder.name())) } #[cfg(test)] mod tests { use super::*; const LISTING: &str = "\ Encoders: V..... = Video ------ V....D libx264 libx264 H.264 / AVC / MPEG-4 AVC V....D libx265 libx265 HEVC V..... h264_videotoolbox VideoToolbox H.264 Encoder A..... aac AAC (Advanced Audio Coding) "; #[test] fn the_best_one_present_is_chosen() { assert_eq!(best_available(LISTING), Some(Encoder::H264Videotoolbox)); } #[test] fn hardware_beats_software() { let with_nvenc = format!("{LISTING} V..... h264_nvenc NVIDIA NVENC H.264\n"); assert_eq!(best_available(&with_nvenc), Some(Encoder::H264Nvenc)); } #[test] fn software_is_the_last_resort_and_not_the_first() { let software_only = " V....D libx264 libx264 H.264\n"; assert_eq!(best_available(software_only), Some(Encoder::Libx264)); } #[test] fn an_ffmpeg_that_cannot_make_h264_says_so() { assert_eq!( best_available(" V....D libx265 libx265 HEVC\n"), None ); assert_eq!(best_available(""), None); } #[test] fn a_name_that_merely_contains_an_encoder_name_is_not_one() { // `libx264rgb` is a different encoder, and matching loosely would // pick it for a plan that then produces the wrong pixel format. let other = " V....D libx264rgb libx264 H.264 RGB\n"; assert_eq!(best_available(other), None); } #[test] fn only_vaapi_needs_the_device_opened_first() { assert_eq!( Encoder::H264Vaapi.before_input("/dev/dri/renderD128"), [ "-init_hw_device", "vaapi=va:/dev/dri/renderD128", "-filter_hw_device", "va" ] ); assert!( Encoder::H264Nvenc .before_input("/dev/dri/renderD128") .is_empty() ); assert!(Encoder::Libx264.before_input("x").is_empty()); } #[test] fn every_encoder_names_itself_in_its_own_arguments() { for encoder in PREFERRED { let args = encoder.video_arguments(); assert!( args.contains(&encoder.name().to_string()), "{:?} does not name itself", encoder ); } } #[test] fn copying_is_not_encoding() { assert!(!Encoder::Copy.is_hardware()); assert_eq!(Encoder::Copy.video_arguments(), ["-c:v", "copy"]); } }