1#[cfg(feature = "amd")]
16pub mod amf_dec;
17#[cfg(feature = "ffmpeg")]
18pub mod ffmpeg;
19#[cfg(feature = "nvidia")]
20pub mod nvdec;
21#[cfg(feature = "qsv")]
22pub mod qsv_dec;
23
24use crate::frame::{StreamInfo, VideoFrame};
25use crate::gpu;
26
27#[cfg(any(feature = "nvidia", feature = "amd", feature = "qsv"))]
31#[allow(dead_code)]
32pub(crate) fn nv12_planes_to_yuv420p(
33 y: &[u8],
34 y_stride: usize,
35 uv: &[u8],
36 uv_stride: usize,
37 width: usize,
38 height: usize,
39) -> Vec<u8> {
40 let cw = width / 2;
41 let ch = height / 2;
42 let mut out = Vec::with_capacity(width * height + 2 * cw * ch);
43 for row in 0..height {
44 let off = row * y_stride;
45 out.extend_from_slice(&y[off..off + width]);
46 }
47 let mut u_plane = Vec::with_capacity(cw * ch);
49 let mut v_plane = Vec::with_capacity(cw * ch);
50 for row in 0..ch {
51 let off = row * uv_stride;
52 let r = &uv[off..off + cw * 2];
53 for c in 0..cw {
54 u_plane.push(r[2 * c]);
55 v_plane.push(r[2 * c + 1]);
56 }
57 }
58 out.extend_from_slice(&u_plane);
59 out.extend_from_slice(&v_plane);
60 out
61}
62
63#[cfg(any(feature = "amd", feature = "qsv"))]
67#[allow(dead_code)]
68pub(crate) fn p010_planes_to_yuv420p10le(
69 y: &[u8],
70 y_stride: usize,
71 uv: &[u8],
72 uv_stride: usize,
73 width: usize,
74 height: usize,
75) -> Vec<u8> {
76 let cw = width.div_ceil(2);
77 let ch = height.div_ceil(2);
78 let mut out = Vec::with_capacity((width * height + 2 * cw * ch) * 2);
79 let rd = |buf: &[u8], off: usize| -> u16 {
80 if off + 1 < buf.len() {
81 u16::from_le_bytes([buf[off], buf[off + 1]]) >> 6
82 } else {
83 0
84 }
85 };
86 for row in 0..height {
87 let base = row * y_stride;
88 for col in 0..width {
89 out.extend_from_slice(&rd(y, base + col * 2).to_le_bytes());
90 }
91 }
92 for row in 0..ch {
93 let base = row * uv_stride;
94 for col in 0..cw {
95 out.extend_from_slice(&rd(uv, base + col * 4).to_le_bytes());
96 }
97 }
98 for row in 0..ch {
99 let base = row * uv_stride;
100 for col in 0..cw {
101 out.extend_from_slice(&rd(uv, base + col * 4 + 2).to_le_bytes());
102 }
103 }
104 out
105}
106use anyhow::{Result, bail};
107
108pub trait Decoder: Send {
109 fn stream_info(&self) -> &StreamInfo;
110
111 fn push_sample(&mut self, data: &[u8]) -> Result<()>;
116
117 fn finish(&mut self) -> Result<()>;
120
121 fn decode_next(&mut self) -> Result<Option<VideoFrame>>;
122}
123
124#[cfg(feature = "nvidia")]
128fn env_flag_truthy(name: &str) -> bool {
129 match std::env::var(name) {
130 Ok(v) => {
131 let v = v.to_ascii_lowercase();
132 matches!(v.as_str(), "1" | "true" | "yes" | "on" | "y" | "t")
133 }
134 Err(_) => false,
135 }
136}
137
138#[cfg(feature = "nvidia")]
144fn nvdec_disabled_for(codec_lower: &str) -> bool {
145 if env_flag_truthy("DISABLE_NVDEC") {
146 return true;
147 }
148 let codec_canonical = match codec_lower {
149 "h264" | "avc1" | "avc" => "H264",
150 "h265" | "hevc" | "hvc1" | "hev1" | "hvc2" | "hev2" => "HEVC",
151 "vp8" => "VP8",
152 "vp9" | "vp09" => "VP9",
153 "av1" | "av01" => "AV1",
154 "mpeg2" | "mpeg2video" => "MPEG2",
155 "mpeg4" | "mp4v" => "MPEG4",
156 _ => return false,
157 };
158 env_flag_truthy(&format!("DISABLE_NVDEC_{codec_canonical}"))
159}
160
161#[cfg(feature = "nvidia")]
163fn nvdec_supports(codec_lower: &str) -> bool {
164 matches!(
165 codec_lower,
166 "h264"
167 | "avc1"
168 | "avc"
169 | "h265"
170 | "hevc"
171 | "hvc1"
172 | "hev1"
173 | "hvc2"
174 | "hev2"
175 | "vp8"
176 | "vp9"
177 | "vp09"
178 | "av1"
179 | "av01"
180 | "mpeg2"
181 | "mpeg2video"
182 | "mpeg4"
183 | "mp4v"
184 )
185}
186
187pub fn decode_backends() -> Vec<&'static str> {
189 let mut v = Vec::new();
190 if cfg!(feature = "ffmpeg") {
191 v.push("ffmpeg");
192 }
193 if cfg!(feature = "nvidia") {
194 v.push("nvdec");
195 }
196 if cfg!(feature = "amd") {
197 v.push("amf");
198 }
199 if cfg!(feature = "qsv") {
200 v.push("qsv");
201 }
202 v
203}
204
205#[derive(Debug, Clone, PartialEq, Eq)]
207pub struct DecodeSupport {
208 pub codec: &'static str,
210 pub backends: Vec<&'static str>,
213}
214
215pub fn decode_capabilities() -> Vec<DecodeSupport> {
217 const CODECS: &[&str] = &[
218 "h264", "hevc", "vp8", "vp9", "av1", "mpeg2", "mpeg4", "prores",
219 ];
220 const FFMPEG: &[&str] = CODECS;
222 CODECS
223 .iter()
224 .map(|&codec| {
225 let mut backends: Vec<&'static str> = Vec::new();
226 #[cfg(feature = "ffmpeg")]
227 if FFMPEG.contains(&codec) {
228 backends.push("ffmpeg");
229 }
230 #[cfg(feature = "nvidia")]
231 if nvdec_supports(codec) {
232 backends.push("nvdec");
233 }
234 #[cfg(feature = "amd")]
235 if amf_dec::supports(codec) {
236 backends.push("amf");
237 }
238 #[cfg(feature = "qsv")]
239 if qsv_dec::supports(codec) {
240 backends.push("qsv");
241 }
242 let _ = FFMPEG;
243 DecodeSupport { codec, backends }
244 })
245 .collect()
246}
247
248pub fn create_decoder(codec: &str, info: StreamInfo) -> Result<Box<dyn Decoder>> {
255 create_decoder_on(codec, info, None)
256}
257
258pub fn create_decoder_on(
269 codec: &str,
270 info: StreamInfo,
271 gpu_index: Option<u32>,
272) -> Result<Box<dyn Decoder>> {
273 let codec_lower = codec.to_ascii_lowercase();
274 let gpus = gpu::detect_gpus();
275
276 #[cfg(feature = "nvidia")]
281 let nvidia = match gpu_index {
282 Some(idx) => gpus
283 .iter()
284 .find(|g| matches!(g.vendor, gpu::GpuVendor::Nvidia) && g.index == idx),
285 None => gpus
286 .iter()
287 .find(|g| matches!(g.vendor, gpu::GpuVendor::Nvidia)),
288 };
289
290 #[cfg(feature = "nvidia")]
294 if let Some(dev) = nvidia
295 && nvdec_supports(&codec_lower)
296 && !nvdec_disabled_for(&codec_lower)
297 {
298 tracing::info!(
299 backend = "nvdec",
300 codec = %codec_lower,
301 gpu_index = dev.index,
302 gpu_name = %dev.name,
303 "NVDEC decoder engaged (hand-rolled CUVID FFI)"
304 );
305 return Ok(nvdec::NvdecDecoder::new(info, dev.index));
306 }
307
308 #[cfg(feature = "amd")]
310 {
311 let amd = match gpu_index {
312 Some(idx) => gpus
313 .iter()
314 .find(|g| matches!(g.vendor, gpu::GpuVendor::Amd) && g.index == idx),
315 None => gpus.iter().find(|g| matches!(g.vendor, gpu::GpuVendor::Amd)),
316 };
317 if let Some(dev) = amd
318 && amf_dec::supports(&codec_lower)
319 {
320 tracing::info!(
321 backend = "amf",
322 codec = %codec_lower,
323 gpu_index = dev.index,
324 gpu_name = %dev.name,
325 "AMF decoder engaged (hand-rolled AMF FFI)"
326 );
327 return Ok(Box::new(amf_dec::AmfDecoder::new(info, dev.index)?));
328 }
329 }
330
331 #[cfg(feature = "qsv")]
333 {
334 let intel = match gpu_index {
335 Some(idx) => gpus
336 .iter()
337 .find(|g| matches!(g.vendor, gpu::GpuVendor::Intel) && g.index == idx),
338 None => gpus.iter().find(|g| matches!(g.vendor, gpu::GpuVendor::Intel)),
339 };
340 if let Some(dev) = intel
341 && qsv_dec::supports(&codec_lower)
342 {
343 tracing::info!(
344 backend = "qsv",
345 codec = %codec_lower,
346 gpu_index = dev.index,
347 gpu_name = %dev.name,
348 "QSV decoder engaged (hand-rolled oneVPL FFI)"
349 );
350 return Ok(Box::new(qsv_dec::QsvDecoder::new(info, dev.index)?));
351 }
352 }
353
354 bail!(
355 "no GPU decoder available for codec '{}' on this host \
356 (NVIDIA GPUs cover h264/h265/vp8/vp9/av1/mpeg2/mpeg4; \
357 Intel Arc/Meteor Lake+ covers h264/h265/vp9/av1). \
358 CPU decoders were removed per the GPU-only directive.",
359 codec_lower
360 )
361}