use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum HwAccelBackend {
None,
Vaapi,
Nvenc,
Videotoolbox,
Qsv,
Amf,
D3d11Va,
}
impl HwAccelBackend {
#[must_use]
pub fn as_str(&self) -> &'static str {
match self {
Self::None => "none",
Self::Vaapi => "vaapi",
Self::Nvenc => "nvenc",
Self::Videotoolbox => "videotoolbox",
Self::Qsv => "qsv",
Self::Amf => "amf",
Self::D3d11Va => "d3d11va",
}
}
#[must_use]
pub fn all_hw() -> &'static [HwAccelBackend] {
&[
Self::Vaapi,
Self::Nvenc,
Self::Videotoolbox,
Self::Qsv,
Self::Amf,
Self::D3d11Va,
]
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HwAccelCaps {
pub backend: HwAccelBackend,
pub can_decode: bool,
pub can_encode: bool,
pub max_resolution: (u32, u32),
pub supported_codecs: Vec<String>,
pub max_sessions: u8,
}
impl HwAccelCaps {
#[must_use]
pub fn supports_codec(&self, codec: &str) -> bool {
let codec_lower = codec.to_lowercase();
self.supported_codecs
.iter()
.any(|c| c.to_lowercase() == codec_lower)
}
#[must_use]
pub fn supports_resolution(&self, width: u32, height: u32) -> bool {
width <= self.max_resolution.0 && height <= self.max_resolution.1
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HwAccelConfig {
pub backend: HwAccelBackend,
pub device_index: u8,
pub fallback_to_software: bool,
}
impl HwAccelConfig {
#[must_use]
pub fn new(backend: HwAccelBackend) -> Self {
Self {
backend,
device_index: 0,
fallback_to_software: true,
}
}
#[must_use]
pub fn software() -> Self {
Self::new(HwAccelBackend::None)
}
#[must_use]
pub fn with_device(mut self, index: u8) -> Self {
self.device_index = index;
self
}
#[must_use]
pub fn no_fallback(mut self) -> Self {
self.fallback_to_software = false;
self
}
}
#[derive(Debug, Clone, Default)]
pub struct HwCodecMapping;
impl HwCodecMapping {
#[must_use]
pub fn new() -> Self {
Self
}
#[must_use]
pub fn get_encoder_name(backend: &HwAccelBackend, codec: &str) -> Option<&'static str> {
let codec_lower = codec.to_lowercase();
match backend {
HwAccelBackend::None => None,
HwAccelBackend::Vaapi => match codec_lower.as_str() {
"hevc" | "h265" => Some("hevc_vaapi"),
"h264" | "avc" => Some("h264_vaapi"),
"vp9" => Some("vp9_vaapi"),
"av1" => Some("av1_vaapi"),
"vp8" => Some("vp8_vaapi"),
"mjpeg" => Some("mjpeg_vaapi"),
_ => None,
},
HwAccelBackend::Nvenc => match codec_lower.as_str() {
"hevc" | "h265" => Some("hevc_nvenc"),
"h264" | "avc" => Some("h264_nvenc"),
"av1" => Some("av1_nvenc"),
_ => None,
},
HwAccelBackend::Videotoolbox => match codec_lower.as_str() {
"hevc" | "h265" => Some("hevc_videotoolbox"),
"h264" | "avc" => Some("h264_videotoolbox"),
_ => None,
},
HwAccelBackend::Qsv => match codec_lower.as_str() {
"hevc" | "h265" => Some("hevc_qsv"),
"h264" | "avc" => Some("h264_qsv"),
"vp9" => Some("vp9_qsv"),
"av1" => Some("av1_qsv"),
"mjpeg" => Some("mjpeg_qsv"),
_ => None,
},
HwAccelBackend::Amf => match codec_lower.as_str() {
"hevc" | "h265" => Some("hevc_amf"),
"h264" | "avc" => Some("h264_amf"),
"av1" => Some("av1_amf"),
_ => None,
},
HwAccelBackend::D3d11Va => match codec_lower.as_str() {
"hevc" | "h265" => Some("hevc_d3d11va"),
"h264" | "avc" => Some("h264_d3d11va"),
_ => None,
},
}
}
#[must_use]
pub fn encoder_name(backend: &HwAccelBackend, codec: &str) -> Option<&'static str> {
Self::get_encoder_name(backend, codec)
}
}
#[must_use]
pub fn simulate_hw_caps(backend: HwAccelBackend) -> HwAccelCaps {
match backend {
HwAccelBackend::None => HwAccelCaps {
backend,
can_decode: true,
can_encode: true,
max_resolution: (7680, 4320),
supported_codecs: vec![
"h264".to_string(),
"hevc".to_string(),
"vp9".to_string(),
"av1".to_string(),
"vp8".to_string(),
"opus".to_string(),
"flac".to_string(),
],
max_sessions: 32,
},
HwAccelBackend::Vaapi => HwAccelCaps {
backend,
can_decode: true,
can_encode: true,
max_resolution: (4096, 4096),
supported_codecs: vec![
"h264".to_string(),
"hevc".to_string(),
"vp9".to_string(),
"av1".to_string(),
"vp8".to_string(),
"mjpeg".to_string(),
],
max_sessions: 8,
},
HwAccelBackend::Nvenc => HwAccelCaps {
backend,
can_decode: true,
can_encode: true,
max_resolution: (7680, 4320),
supported_codecs: vec!["h264".to_string(), "hevc".to_string(), "av1".to_string()],
max_sessions: 3,
},
HwAccelBackend::Videotoolbox => HwAccelCaps {
backend,
can_decode: true,
can_encode: true,
max_resolution: (4096, 2160),
supported_codecs: vec!["h264".to_string(), "hevc".to_string()],
max_sessions: 4,
},
HwAccelBackend::Qsv => HwAccelCaps {
backend,
can_decode: true,
can_encode: true,
max_resolution: (8192, 8192),
supported_codecs: vec![
"h264".to_string(),
"hevc".to_string(),
"vp9".to_string(),
"av1".to_string(),
"mjpeg".to_string(),
],
max_sessions: 6,
},
HwAccelBackend::Amf => HwAccelCaps {
backend,
can_decode: true,
can_encode: true,
max_resolution: (7680, 4320),
supported_codecs: vec!["h264".to_string(), "hevc".to_string(), "av1".to_string()],
max_sessions: 4,
},
HwAccelBackend::D3d11Va => HwAccelCaps {
backend,
can_decode: true,
can_encode: false, max_resolution: (4096, 2160),
supported_codecs: vec!["h264".to_string(), "hevc".to_string()],
max_sessions: 2,
},
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum LatencyMode {
Batch,
LowLatency {
max_delay_ms: u32,
},
Realtime,
}
impl LatencyMode {
#[must_use]
pub fn is_live(&self) -> bool {
matches!(self, Self::LowLatency { .. } | Self::Realtime)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PipelineHwConfig {
pub hw_accel: HwAccelConfig,
pub thread_count: u8,
pub buffer_count: u8,
pub latency_mode: LatencyMode,
}
impl PipelineHwConfig {
#[must_use]
pub fn default_software() -> Self {
Self {
hw_accel: HwAccelConfig::software(),
thread_count: 4,
buffer_count: 8,
latency_mode: LatencyMode::Batch,
}
}
#[must_use]
pub fn low_latency(backend: HwAccelBackend, max_delay_ms: u32) -> Self {
Self {
hw_accel: HwAccelConfig::new(backend),
thread_count: 2,
buffer_count: 4,
latency_mode: LatencyMode::LowLatency { max_delay_ms },
}
}
}
#[derive(Debug, Clone, Default)]
pub struct HwAccelSelector;
impl HwAccelSelector {
#[must_use]
pub fn new() -> Self {
Self
}
#[must_use]
pub fn select_best(
available: &[HwAccelCaps],
codec: &str,
resolution: (u32, u32),
) -> Option<HwAccelConfig> {
let (width, height) = resolution;
available
.iter()
.filter(|caps| {
caps.can_encode
&& caps.supports_codec(codec)
&& caps.supports_resolution(width, height)
&& caps.backend != HwAccelBackend::None
})
.max_by_key(|caps| caps.max_sessions)
.map(|caps| HwAccelConfig::new(caps.backend))
}
#[must_use]
pub fn select_best_or_software(
available: &[HwAccelCaps],
codec: &str,
resolution: (u32, u32),
) -> HwAccelConfig {
Self::select_best(available, codec, resolution).unwrap_or_else(HwAccelConfig::software)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_backend_as_str() {
assert_eq!(HwAccelBackend::None.as_str(), "none");
assert_eq!(HwAccelBackend::Vaapi.as_str(), "vaapi");
assert_eq!(HwAccelBackend::Nvenc.as_str(), "nvenc");
assert_eq!(HwAccelBackend::Videotoolbox.as_str(), "videotoolbox");
assert_eq!(HwAccelBackend::Qsv.as_str(), "qsv");
assert_eq!(HwAccelBackend::Amf.as_str(), "amf");
assert_eq!(HwAccelBackend::D3d11Va.as_str(), "d3d11va");
}
#[test]
fn test_all_hw_contains_six_backends() {
assert_eq!(HwAccelBackend::all_hw().len(), 6);
assert!(!HwAccelBackend::all_hw().contains(&HwAccelBackend::None));
}
#[test]
fn test_vaapi_hevc_encoder_name() {
assert_eq!(
HwCodecMapping::get_encoder_name(&HwAccelBackend::Vaapi, "hevc"),
Some("hevc_vaapi")
);
}
#[test]
fn test_nvenc_hevc_encoder_name() {
assert_eq!(
HwCodecMapping::get_encoder_name(&HwAccelBackend::Nvenc, "hevc"),
Some("hevc_nvenc")
);
}
#[test]
fn test_videotoolbox_hevc_encoder_name() {
assert_eq!(
HwCodecMapping::get_encoder_name(&HwAccelBackend::Videotoolbox, "hevc"),
Some("hevc_videotoolbox")
);
}
#[test]
fn test_qsv_h264_encoder_name() {
assert_eq!(
HwCodecMapping::get_encoder_name(&HwAccelBackend::Qsv, "h264"),
Some("h264_qsv")
);
}
#[test]
fn test_amf_av1_encoder_name() {
assert_eq!(
HwCodecMapping::get_encoder_name(&HwAccelBackend::Amf, "av1"),
Some("av1_amf")
);
}
#[test]
fn test_none_backend_returns_none() {
assert_eq!(
HwCodecMapping::get_encoder_name(&HwAccelBackend::None, "h264"),
None
);
}
#[test]
fn test_unsupported_codec_returns_none() {
assert_eq!(
HwCodecMapping::get_encoder_name(&HwAccelBackend::Nvenc, "vp9"),
None
);
}
#[test]
fn test_simulate_vaapi_can_encode_and_decode() {
let caps = simulate_hw_caps(HwAccelBackend::Vaapi);
assert!(caps.can_encode);
assert!(caps.can_decode);
}
#[test]
fn test_simulate_d3d11va_is_decode_only() {
let caps = simulate_hw_caps(HwAccelBackend::D3d11Va);
assert!(!caps.can_encode);
assert!(caps.can_decode);
}
#[test]
fn test_simulate_nvenc_supports_hevc() {
let caps = simulate_hw_caps(HwAccelBackend::Nvenc);
assert!(caps.supports_codec("hevc"));
}
#[test]
fn test_simulate_nvenc_does_not_support_vp9() {
let caps = simulate_hw_caps(HwAccelBackend::Nvenc);
assert!(!caps.supports_codec("vp9"));
}
#[test]
fn test_simulate_resolution_check() {
let caps = simulate_hw_caps(HwAccelBackend::Videotoolbox);
assert!(caps.supports_resolution(1920, 1080));
assert!(!caps.supports_resolution(7680, 4320)); }
#[test]
fn test_simulate_none_is_unlimited() {
let caps = simulate_hw_caps(HwAccelBackend::None);
assert!(caps.supports_resolution(7680, 4320));
}
#[test]
fn test_hw_accel_config_new() {
let cfg = HwAccelConfig::new(HwAccelBackend::Vaapi);
assert_eq!(cfg.backend, HwAccelBackend::Vaapi);
assert_eq!(cfg.device_index, 0);
assert!(cfg.fallback_to_software);
}
#[test]
fn test_hw_accel_config_no_fallback() {
let cfg = HwAccelConfig::new(HwAccelBackend::Nvenc).no_fallback();
assert!(!cfg.fallback_to_software);
}
#[test]
fn test_hw_accel_config_with_device() {
let cfg = HwAccelConfig::new(HwAccelBackend::Nvenc).with_device(2);
assert_eq!(cfg.device_index, 2);
}
#[test]
fn test_latency_mode_is_live() {
assert!(!LatencyMode::Batch.is_live());
assert!(LatencyMode::LowLatency { max_delay_ms: 100 }.is_live());
assert!(LatencyMode::Realtime.is_live());
}
#[test]
fn test_latency_mode_equality() {
assert_eq!(
LatencyMode::LowLatency { max_delay_ms: 200 },
LatencyMode::LowLatency { max_delay_ms: 200 }
);
assert_ne!(
LatencyMode::LowLatency { max_delay_ms: 100 },
LatencyMode::LowLatency { max_delay_ms: 200 }
);
}
#[test]
fn test_selector_picks_highest_sessions() {
let caps = vec![
simulate_hw_caps(HwAccelBackend::Vaapi), simulate_hw_caps(HwAccelBackend::Nvenc), simulate_hw_caps(HwAccelBackend::Qsv), ];
let cfg = HwAccelSelector::select_best(&caps, "h264", (1920, 1080));
assert!(cfg.is_some());
assert_eq!(
cfg.expect("should have config").backend,
HwAccelBackend::Vaapi
);
}
#[test]
fn test_selector_filters_unsupported_codec() {
let caps = vec![
simulate_hw_caps(HwAccelBackend::D3d11Va),
simulate_hw_caps(HwAccelBackend::Videotoolbox),
];
let cfg = HwAccelSelector::select_best(&caps, "hevc", (1920, 1080));
assert!(cfg.is_some());
assert_eq!(
cfg.expect("should have config").backend,
HwAccelBackend::Videotoolbox
);
}
#[test]
fn test_selector_returns_none_when_no_match() {
let caps = vec![simulate_hw_caps(HwAccelBackend::D3d11Va)]; let cfg = HwAccelSelector::select_best(&caps, "h264", (1920, 1080));
assert!(cfg.is_none());
}
#[test]
fn test_selector_fallback_to_software() {
let caps: Vec<HwAccelCaps> = vec![];
let cfg = HwAccelSelector::select_best_or_software(&caps, "h264", (1920, 1080));
assert_eq!(cfg.backend, HwAccelBackend::None);
}
#[test]
fn test_pipeline_hw_config_default_software() {
let cfg = PipelineHwConfig::default_software();
assert_eq!(cfg.hw_accel.backend, HwAccelBackend::None);
assert_eq!(cfg.latency_mode, LatencyMode::Batch);
}
#[test]
fn test_pipeline_hw_config_low_latency() {
let cfg = PipelineHwConfig::low_latency(HwAccelBackend::Nvenc, 50);
assert!(cfg.latency_mode.is_live());
assert_eq!(cfg.hw_accel.backend, HwAccelBackend::Nvenc);
}
}