avplan: whether a recording plays, and the one call that fixes it

This commit is contained in:
funkpower
2026-09-19 15:07:33 -04:00
parent ea0a3694b1
commit 49f12c9718
16 changed files with 1370 additions and 4 deletions
+224
View File
@@ -0,0 +1,224 @@
//! 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<Regex> =
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<String> {
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<String> {
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<Encoder> {
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"]);
}
}