#[cfg(feature = "amd")]
pub mod amf;
#[cfg(not(feature = "amd"))]
#[path = "amf_stub.rs"]
pub mod amf;
#[cfg(feature = "ffmpeg")]
pub mod ffmpeg_enc;
#[cfg(feature = "nvidia")]
pub mod nvenc;
#[cfg(not(feature = "nvidia"))]
#[path = "nvenc_stub.rs"]
pub mod nvenc;
#[cfg(feature = "qsv")]
pub mod qsv;
#[cfg(not(feature = "qsv"))]
#[path = "qsv_stub.rs"]
pub mod qsv;
pub mod tuning;
use crate::frame::{ColorMetadata, PixelFormat, VideoFrame};
use crate::gpu;
use anyhow::Result;
use bytes::Bytes;
pub use tuning::{QualityTarget, SpeedTier};
fn pick_vendor_device(
gpus: &[gpu::GpuDevice],
vendor: gpu::GpuVendor,
requested: Option<u32>,
) -> Option<&gpu::GpuDevice> {
match requested {
Some(idx) => gpus.iter().find(|g| g.index == idx && g.vendor == vendor),
None => gpus.iter().find(|g| g.vendor == vendor),
}
}
#[cfg(feature = "ffmpeg")]
fn ffmpeg_disable_flag() -> bool {
match std::env::var("DISABLE_FFMPEG") {
Ok(v) => {
let v = v.to_ascii_lowercase();
matches!(v.as_str(), "1" | "true" | "yes" | "on" | "y" | "t")
}
Err(_) => false,
}
}
pub trait Encoder: Send {
fn send_frame(&mut self, frame: &VideoFrame) -> Result<()>;
fn flush(&mut self) -> Result<()>;
fn receive_packet(&mut self) -> Result<Option<EncodedPacket>>;
}
#[derive(Debug, Clone)]
pub struct EncodedPacket {
pub data: Bytes,
pub pts: u64,
pub is_keyframe: bool,
}
#[derive(Debug, Clone)]
pub struct EncoderConfig {
pub width: u32,
pub height: u32,
pub frame_rate: f64,
pub quality: u8,
pub speed_preset: u8,
pub keyframe_interval: u32,
pub target: QualityTarget,
pub tier: SpeedTier,
pub threads: usize,
pub pixel_format: PixelFormat,
pub color_metadata: ColorMetadata,
pub gpu_index: Option<u32>,
pub gpu_vendor: Option<gpu::GpuVendor>,
pub constant_qp: bool,
}
pub const AUTO_FROM_TARGET: u8 = u8::MAX;
impl Default for EncoderConfig {
fn default() -> Self {
Self {
width: 1920,
height: 1080,
frame_rate: 30.0,
quality: AUTO_FROM_TARGET,
speed_preset: AUTO_FROM_TARGET,
keyframe_interval: 240,
target: QualityTarget::Standard,
tier: SpeedTier::Standard,
threads: 0,
pixel_format: PixelFormat::Yuv420p,
color_metadata: ColorMetadata::default(),
gpu_index: None,
gpu_vendor: None,
constant_qp: false,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum EncoderBackend {
Nvenc,
Amf,
Qsv,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct OutputCaps {
pub max_bit_depth: u8,
pub hdr: bool,
}
pub fn backend_output_caps(backend: EncoderBackend) -> OutputCaps {
match backend {
EncoderBackend::Nvenc | EncoderBackend::Amf | EncoderBackend::Qsv => {
OutputCaps { max_bit_depth: 10, hdr: true }
}
}
}
pub fn build_output_caps() -> OutputCaps {
#[cfg(any(
feature = "ffmpeg",
feature = "nvidia",
feature = "amd",
feature = "qsv"
))]
{
return OutputCaps { max_bit_depth: 10, hdr: true };
}
#[allow(unreachable_code)]
OutputCaps { max_bit_depth: 8, hdr: false }
}
pub fn encode_backends() -> Vec<&'static str> {
let mut v = Vec::new();
if cfg!(feature = "nvidia") {
v.push("nvenc");
}
if cfg!(feature = "amd") {
v.push("amf");
}
if cfg!(feature = "qsv") {
v.push("qsv");
}
if cfg!(feature = "ffmpeg") {
v.push("ffmpeg");
}
v
}
fn make_qsv_encoder(config: EncoderConfig, gpu_index: u32) -> Result<Box<dyn Encoder>> {
Ok(Box::new(qsv::QsvEncoder::new(config, gpu_index)?))
}
pub fn select_encoder(
config: EncoderConfig,
preferred: Option<EncoderBackend>,
) -> Result<Box<dyn Encoder>> {
let gpus = gpu::detect_gpus();
if let Some(backend) = preferred {
return create_backend(backend, config, &gpus);
}
#[cfg(feature = "ffmpeg")]
{
if !ffmpeg_disable_flag() {
match ffmpeg_enc::FfmpegEncoder::new(config.clone()) {
Ok(enc) => {
tracing::info!(
backend = "ffmpeg",
av1_encoder = enc.engaged(),
"FFmpeg primary encoder dispatch engaged"
);
return Ok(Box::new(enc));
}
Err(e) => {
tracing::warn!(
error = %e,
"FFmpeg AV1 encoder chain exhausted; falling through to native backends"
);
}
}
} else {
tracing::debug!("DISABLE_FFMPEG set; skipping FFmpeg encoder dispatch");
}
}
if let Some(pinned) = config.gpu_vendor {
if let Some(dev) = pick_vendor_device(&gpus, pinned, config.gpu_index) {
if gpu::supports_av1_encode(dev) {
let attempt = match pinned {
gpu::GpuVendor::Nvidia => nvenc::NvencEncoder::new(config.clone(), dev.index)
.map(|e| Box::new(e) as Box<dyn Encoder>),
gpu::GpuVendor::Amd => amf::AmfEncoder::new(config.clone(), dev.index)
.map(|e| Box::new(e) as Box<dyn Encoder>),
gpu::GpuVendor::Intel => make_qsv_encoder(config.clone(), dev.index),
};
return match attempt {
Ok(enc) => {
tracing::info!(
gpu_name = %dev.name,
gpu_index = dev.index,
vendor = ?pinned,
"using vendor-pinned AV1 hardware encoder (lease-driven dispatch)"
);
Ok(enc)
}
Err(e) => {
Err(anyhow::anyhow!(
"vendor-pinned AV1 encoder init failed (vendor={pinned:?}, gpu={}, idx={}): {e}",
dev.name,
dev.index,
))
}
};
}
return Err(anyhow::anyhow!(
"vendor-pinned GPU lacks AV1 encode silicon (vendor={pinned:?}, gpu={}); \
GPU-only encode policy has no CPU fallback",
dev.name,
));
}
return Err(anyhow::anyhow!(
"vendor-pinned encoder requested (vendor={pinned:?}, gpu_index={:?}) but no matching GPU found",
config.gpu_index,
));
}
if let Some(dev) = pick_vendor_device(&gpus, gpu::GpuVendor::Nvidia, config.gpu_index) {
if gpu::supports_av1_encode(dev) {
match nvenc::NvencEncoder::new(config.clone(), dev.index) {
Ok(enc) => {
tracing::info!(
gpu_name = %dev.name,
gpu_index = dev.index,
"using NVENC AV1 hardware encoder"
);
return Ok(Box::new(enc));
}
Err(e) => {
tracing::warn!(error = %e, "NVENC init failed, falling back to next backend");
}
}
} else {
tracing::info!(
gpu = %dev.name,
"NVIDIA GPU lacks AV1 NVENC silicon — trying other GPU backends"
);
}
}
if let Some(dev) = pick_vendor_device(&gpus, gpu::GpuVendor::Amd, config.gpu_index) {
if gpu::supports_av1_encode(dev) {
match amf::AmfEncoder::new(config.clone(), dev.index) {
Ok(enc) => {
tracing::info!(
gpu_name = %dev.name,
gpu_index = dev.index,
"using AMF AV1 hardware encoder"
);
return Ok(Box::new(enc));
}
Err(e) => {
tracing::warn!(error = %e, "AMF init failed, falling back to next backend");
}
}
} else {
tracing::info!(
gpu = %dev.name,
"AMD GPU predates RDNA3 — no AV1 AMF silicon; trying Intel / CPU"
);
}
}
if let Some(dev) = pick_vendor_device(&gpus, gpu::GpuVendor::Intel, config.gpu_index) {
if gpu::supports_av1_encode(dev) {
match make_qsv_encoder(config.clone(), dev.index) {
Ok(enc) => {
tracing::info!(
gpu_name = %dev.name,
gpu_index = dev.index,
"using QSV AV1 hardware encoder"
);
return Ok(enc);
}
Err(e) => {
tracing::warn!(error = %e, "QSV init failed; chain exhausted");
}
}
} else {
tracing::info!(
gpu = %dev.name,
"Intel GPU predates Arc/Meteor Lake — no AV1 QSV silicon"
);
}
}
Err(anyhow::anyhow!(
"no AV1 GPU encoder available — the host needs NVIDIA Ada+ / AMD RDNA3+ / Intel Arc \
for AV1 hardware encoding. CPU encoding (rav1e) was removed per the GPU-only directive."
))
}
pub fn av1_encode_capable(dev: &gpu::GpuDevice) -> bool {
use std::collections::HashMap;
use std::sync::{Mutex, OnceLock};
static CACHE: OnceLock<Mutex<HashMap<u32, bool>>> = OnceLock::new();
let cache = CACHE.get_or_init(|| Mutex::new(HashMap::new()));
if let Some(&cached) = cache.lock().unwrap().get(&dev.index) {
return cached;
}
let probe = EncoderConfig {
width: 640,
height: 480,
frame_rate: 30.0,
gpu_index: Some(dev.index),
gpu_vendor: Some(dev.vendor),
..Default::default()
};
let capable = match select_encoder(probe, None) {
Ok(_enc) => true, Err(e) => {
tracing::info!(
gpu_index = dev.index,
gpu = %dev.name,
vendor = ?dev.vendor,
error = %e,
"GPU cannot encode AV1 — excluding it from the encode pool (still usable for decode)"
);
false
}
};
cache.lock().unwrap().insert(dev.index, capable);
capable
}
fn create_backend(
backend: EncoderBackend,
config: EncoderConfig,
gpus: &[gpu::GpuDevice],
) -> Result<Box<dyn Encoder>> {
match backend {
EncoderBackend::Nvenc => {
let dev = pick_vendor_device(gpus, gpu::GpuVendor::Nvidia, config.gpu_index)
.ok_or_else(|| match config.gpu_index {
Some(idx) => anyhow::anyhow!(
"NVENC requested on GPU index {idx} but no NVIDIA GPU with that index found"
),
None => anyhow::anyhow!("NVENC requested but no NVIDIA GPU found"),
})?;
Ok(Box::new(nvenc::NvencEncoder::new(config, dev.index)?))
}
EncoderBackend::Amf => {
let dev = pick_vendor_device(gpus, gpu::GpuVendor::Amd, config.gpu_index).ok_or_else(
|| match config.gpu_index {
Some(idx) => anyhow::anyhow!(
"AMF requested on GPU index {idx} but no AMD GPU with that index found"
),
None => anyhow::anyhow!("AMF requested but no AMD GPU found"),
},
)?;
Ok(Box::new(amf::AmfEncoder::new(config, dev.index)?))
}
EncoderBackend::Qsv => {
let dev = pick_vendor_device(gpus, gpu::GpuVendor::Intel, config.gpu_index)
.ok_or_else(|| match config.gpu_index {
Some(idx) => anyhow::anyhow!(
"QSV requested on GPU index {idx} but no Intel GPU with that index found"
),
None => anyhow::anyhow!("QSV requested but no Intel GPU found"),
})?;
Ok(Box::new(qsv::QsvEncoder::new(config, dev.index)?))
}
}
}
#[cfg(test)]
mod gpu_selection_tests {
use super::*;
use crate::gpu::{GpuDevice, GpuVendor};
fn synth(index: u32, vendor: GpuVendor) -> GpuDevice {
GpuDevice {
index,
vendor,
name: format!("synthetic-{index}"),
generation: String::new(),
pci_id: String::new(),
vram_mib: 0,
serial: None,
host_pci_address: String::new(),
vendor_id_hex: String::new(),
}
}
#[test]
fn pick_vendor_device_defaults_to_first_of_vendor_when_no_request() {
let gpus = vec![
synth(0, GpuVendor::Nvidia),
synth(1, GpuVendor::Nvidia),
synth(2, GpuVendor::Amd),
];
let nv = pick_vendor_device(&gpus, GpuVendor::Nvidia, None).unwrap();
assert_eq!(nv.index, 0);
let amd = pick_vendor_device(&gpus, GpuVendor::Amd, None).unwrap();
assert_eq!(amd.index, 2);
}
#[test]
fn pick_vendor_device_honours_explicit_request() {
let gpus = vec![
synth(0, GpuVendor::Nvidia),
synth(1, GpuVendor::Nvidia),
synth(2, GpuVendor::Nvidia),
];
let dev = pick_vendor_device(&gpus, GpuVendor::Nvidia, Some(1)).unwrap();
assert_eq!(dev.index, 1);
let dev2 = pick_vendor_device(&gpus, GpuVendor::Nvidia, Some(2)).unwrap();
assert_eq!(dev2.index, 2);
}
#[test]
fn pick_vendor_device_returns_none_when_index_vendor_mismatch() {
let gpus = vec![synth(0, GpuVendor::Nvidia), synth(2, GpuVendor::Amd)];
assert!(pick_vendor_device(&gpus, GpuVendor::Nvidia, Some(2)).is_none());
let dev = pick_vendor_device(&gpus, GpuVendor::Amd, Some(2)).unwrap();
assert_eq!(dev.index, 2);
}
#[test]
fn pick_vendor_device_no_gpus_returns_none() {
let gpus: Vec<GpuDevice> = vec![];
assert!(pick_vendor_device(&gpus, GpuVendor::Nvidia, None).is_none());
assert!(pick_vendor_device(&gpus, GpuVendor::Nvidia, Some(0)).is_none());
}
#[test]
fn encoder_config_default_has_no_gpu_pin() {
let cfg = EncoderConfig::default();
assert_eq!(cfg.gpu_index, None);
}
}