#[cfg(feature = "amd")]
pub mod amf_dec;
#[cfg(feature = "ffmpeg")]
pub mod ffmpeg;
#[cfg(feature = "nvidia")]
pub mod nvdec;
#[cfg(feature = "qsv")]
pub mod qsv_dec;
use crate::frame::{StreamInfo, VideoFrame};
use crate::gpu;
#[cfg(any(feature = "nvidia", feature = "amd", feature = "qsv"))]
#[allow(dead_code)]
pub(crate) fn nv12_planes_to_yuv420p(
y: &[u8],
y_stride: usize,
uv: &[u8],
uv_stride: usize,
width: usize,
height: usize,
) -> Vec<u8> {
let cw = width / 2;
let ch = height / 2;
let mut out = Vec::with_capacity(width * height + 2 * cw * ch);
for row in 0..height {
let off = row * y_stride;
out.extend_from_slice(&y[off..off + width]);
}
let mut u_plane = Vec::with_capacity(cw * ch);
let mut v_plane = Vec::with_capacity(cw * ch);
for row in 0..ch {
let off = row * uv_stride;
let r = &uv[off..off + cw * 2];
for c in 0..cw {
u_plane.push(r[2 * c]);
v_plane.push(r[2 * c + 1]);
}
}
out.extend_from_slice(&u_plane);
out.extend_from_slice(&v_plane);
out
}
#[cfg(any(feature = "amd", feature = "qsv"))]
#[allow(dead_code)]
pub(crate) fn p010_planes_to_yuv420p10le(
y: &[u8],
y_stride: usize,
uv: &[u8],
uv_stride: usize,
width: usize,
height: usize,
) -> Vec<u8> {
let cw = width.div_ceil(2);
let ch = height.div_ceil(2);
let mut out = Vec::with_capacity((width * height + 2 * cw * ch) * 2);
let rd = |buf: &[u8], off: usize| -> u16 {
if off + 1 < buf.len() {
u16::from_le_bytes([buf[off], buf[off + 1]]) >> 6
} else {
0
}
};
for row in 0..height {
let base = row * y_stride;
for col in 0..width {
out.extend_from_slice(&rd(y, base + col * 2).to_le_bytes());
}
}
for row in 0..ch {
let base = row * uv_stride;
for col in 0..cw {
out.extend_from_slice(&rd(uv, base + col * 4).to_le_bytes());
}
}
for row in 0..ch {
let base = row * uv_stride;
for col in 0..cw {
out.extend_from_slice(&rd(uv, base + col * 4 + 2).to_le_bytes());
}
}
out
}
use anyhow::{Result, bail};
pub trait Decoder: Send {
fn stream_info(&self) -> &StreamInfo;
fn push_sample(&mut self, data: &[u8]) -> Result<()>;
fn finish(&mut self) -> Result<()>;
fn decode_next(&mut self) -> Result<Option<VideoFrame>>;
}
#[cfg(feature = "nvidia")]
fn env_flag_truthy(name: &str) -> bool {
match std::env::var(name) {
Ok(v) => {
let v = v.to_ascii_lowercase();
matches!(v.as_str(), "1" | "true" | "yes" | "on" | "y" | "t")
}
Err(_) => false,
}
}
#[cfg(feature = "nvidia")]
fn nvdec_disabled_for(codec_lower: &str) -> bool {
if env_flag_truthy("DISABLE_NVDEC") {
return true;
}
let codec_canonical = match codec_lower {
"h264" | "avc1" | "avc" => "H264",
"h265" | "hevc" | "hvc1" | "hev1" | "hvc2" | "hev2" => "HEVC",
"vp8" => "VP8",
"vp9" | "vp09" => "VP9",
"av1" | "av01" => "AV1",
"mpeg2" | "mpeg2video" => "MPEG2",
"mpeg4" | "mp4v" => "MPEG4",
_ => return false,
};
env_flag_truthy(&format!("DISABLE_NVDEC_{codec_canonical}"))
}
#[cfg(feature = "nvidia")]
fn nvdec_supports(codec_lower: &str) -> bool {
matches!(
codec_lower,
"h264"
| "avc1"
| "avc"
| "h265"
| "hevc"
| "hvc1"
| "hev1"
| "hvc2"
| "hev2"
| "vp8"
| "vp9"
| "vp09"
| "av1"
| "av01"
| "mpeg2"
| "mpeg2video"
| "mpeg4"
| "mp4v"
)
}
pub fn decode_backends() -> Vec<&'static str> {
let mut v = Vec::new();
if cfg!(feature = "ffmpeg") {
v.push("ffmpeg");
}
if cfg!(feature = "nvidia") {
v.push("nvdec");
}
if cfg!(feature = "amd") {
v.push("amf");
}
if cfg!(feature = "qsv") {
v.push("qsv");
}
v
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DecodeSupport {
pub codec: &'static str,
pub backends: Vec<&'static str>,
}
pub fn decode_capabilities() -> Vec<DecodeSupport> {
const CODECS: &[&str] = &[
"h264", "hevc", "vp8", "vp9", "av1", "mpeg2", "mpeg4", "prores",
];
const FFMPEG: &[&str] = CODECS;
CODECS
.iter()
.map(|&codec| {
let mut backends: Vec<&'static str> = Vec::new();
#[cfg(feature = "ffmpeg")]
if FFMPEG.contains(&codec) {
backends.push("ffmpeg");
}
#[cfg(feature = "nvidia")]
if nvdec_supports(codec) {
backends.push("nvdec");
}
#[cfg(feature = "amd")]
if amf_dec::supports(codec) {
backends.push("amf");
}
#[cfg(feature = "qsv")]
if qsv_dec::probe_decode_caps().contains(&codec) {
backends.push("qsv");
}
let _ = FFMPEG;
DecodeSupport { codec, backends }
})
.collect()
}
pub fn create_decoder(codec: &str, info: StreamInfo) -> Result<Box<dyn Decoder>> {
create_decoder_on(codec, info, None)
}
pub fn create_decoder_on(
codec: &str,
info: StreamInfo,
gpu_index: Option<u32>,
) -> Result<Box<dyn Decoder>> {
let codec_lower = codec.to_ascii_lowercase();
let gpus = gpu::detect_gpus();
#[cfg(feature = "nvidia")]
let nvidia = match gpu_index {
Some(idx) => gpus
.iter()
.find(|g| matches!(g.vendor, gpu::GpuVendor::Nvidia) && g.index == idx),
None => gpus
.iter()
.find(|g| matches!(g.vendor, gpu::GpuVendor::Nvidia)),
};
#[cfg(feature = "nvidia")]
if let Some(dev) = nvidia
&& nvdec_supports(&codec_lower)
&& !nvdec_disabled_for(&codec_lower)
{
tracing::info!(
backend = "nvdec",
codec = %codec_lower,
gpu_index = dev.index,
gpu_name = %dev.name,
"NVDEC decoder engaged (hand-rolled CUVID FFI)"
);
return Ok(nvdec::NvdecDecoder::new(info, dev.index));
}
#[cfg(feature = "amd")]
{
let amd = match gpu_index {
Some(idx) => gpus
.iter()
.find(|g| matches!(g.vendor, gpu::GpuVendor::Amd) && g.index == idx),
None => gpus.iter().find(|g| matches!(g.vendor, gpu::GpuVendor::Amd)),
};
if let Some(dev) = amd
&& amf_dec::supports(&codec_lower)
{
tracing::info!(
backend = "amf",
codec = %codec_lower,
gpu_index = dev.index,
gpu_name = %dev.name,
"AMF decoder engaged (hand-rolled AMF FFI)"
);
return Ok(Box::new(amf_dec::AmfDecoder::new(info, dev.index)?));
}
}
#[cfg(feature = "qsv")]
{
let intel = match gpu_index {
Some(idx) => gpus
.iter()
.find(|g| matches!(g.vendor, gpu::GpuVendor::Intel) && g.index == idx),
None => gpus.iter().find(|g| matches!(g.vendor, gpu::GpuVendor::Intel)),
};
if let Some(dev) = intel
&& qsv_dec::supports(&codec_lower)
{
tracing::info!(
backend = "qsv",
codec = %codec_lower,
gpu_index = dev.index,
gpu_name = %dev.name,
"QSV decoder engaged (hand-rolled oneVPL FFI)"
);
return Ok(Box::new(qsv_dec::QsvDecoder::new(info, dev.index)?));
}
}
bail!(
"no GPU decoder available for codec '{}' on this host \
(NVIDIA GPUs cover h264/h265/vp8/vp9/av1/mpeg2/mpeg4; \
Intel Arc/Meteor Lake+ covers h264/h265/vp9/av1). \
CPU decoders were removed per the GPU-only directive.",
codec_lower
)
}