1pub mod capabilities;
55pub mod probe;
56
57#[cfg(target_os = "macos")]
58pub(crate) mod macos;
59
60#[cfg(target_os = "linux")]
61pub(crate) mod linux;
62
63pub use capabilities::{HwAccelCapabilities, HwAccelDevice, HwKind};
66pub use probe::{HwProbe, MockProbe, SystemProbe};
67
68use std::sync::OnceLock;
71
72static HW_CAPS: OnceLock<HwAccelCapabilities> = OnceLock::new();
73
74#[must_use]
82pub fn detect_hw_accel_caps() -> &'static HwAccelCapabilities {
83 HW_CAPS.get_or_init(|| SystemProbe.probe())
84}
85
86#[must_use]
100pub fn detect_hw_accel_with_probe(probe: &dyn HwProbe) -> HwAccelCapabilities {
101 probe.probe()
102}
103
104use serde::{Deserialize, Serialize};
107
108#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
110pub enum HwAccelType {
111 None,
113 Nvenc,
115 Qsv,
117 Amd,
119 VideoToolbox,
121 Vulkan,
123 D3d11,
125 Vaapi,
127 Vdpau,
129}
130
131#[derive(Debug, Clone)]
133pub struct HwEncoder {
134 pub accel_type: HwAccelType,
136 pub codec: String,
138 pub encoder_name: String,
140 pub available: bool,
142 pub max_resolution: (u32, u32),
144 pub features: Vec<HwFeature>,
146}
147
148#[derive(Debug, Clone, Copy, PartialEq, Eq)]
150pub enum HwFeature {
151 TenBit,
153 Hdr,
155 BFrames,
157 Lookahead,
159 TemporalAq,
161 SpatialAq,
163 WeightedPred,
165 CustomQuant,
167}
168
169#[derive(Debug, Clone)]
171pub struct HwAccelConfig {
172 pub preferred_type: HwAccelType,
174 pub allow_fallback: bool,
176 pub decode: bool,
178 pub encode: bool,
180 pub device_id: Option<u32>,
182}
183
184impl Default for HwAccelConfig {
185 fn default() -> Self {
186 Self {
187 preferred_type: HwAccelType::None,
188 allow_fallback: true,
189 decode: false,
190 encode: false,
191 device_id: None,
192 }
193 }
194}
195
196impl HwAccelConfig {
197 #[must_use]
199 pub fn new(accel_type: HwAccelType) -> Self {
200 Self {
201 preferred_type: accel_type,
202 allow_fallback: true,
203 decode: true,
204 encode: true,
205 device_id: None,
206 }
207 }
208
209 #[must_use]
211 pub fn allow_fallback(mut self, allow: bool) -> Self {
212 self.allow_fallback = allow;
213 self
214 }
215
216 #[must_use]
218 pub fn decode(mut self, enable: bool) -> Self {
219 self.decode = enable;
220 self
221 }
222
223 #[must_use]
225 pub fn encode(mut self, enable: bool) -> Self {
226 self.encode = enable;
227 self
228 }
229
230 #[must_use]
232 pub fn device_id(mut self, id: u32) -> Self {
233 self.device_id = Some(id);
234 self
235 }
236}
237
238impl HwAccelType {
239 #[must_use]
241 pub fn platform_name(self) -> &'static str {
242 match self {
243 Self::None => "software",
244 Self::Nvenc => "NVIDIA NVENC",
245 Self::Qsv => "Intel Quick Sync",
246 Self::Amd => "AMD VCE/VCN",
247 Self::VideoToolbox => "Apple VideoToolbox",
248 Self::Vulkan => "Vulkan",
249 Self::D3d11 => "Direct3D 11",
250 Self::Vaapi => "VAAPI",
251 Self::Vdpau => "VDPAU",
252 }
253 }
254
255 #[must_use]
257 pub fn is_available(self) -> bool {
258 match self {
259 Self::None => true,
260 Self::Nvenc => detect_nvenc(),
261 Self::Qsv => detect_qsv(),
262 Self::Amd => detect_amd(),
263 Self::VideoToolbox => detect_videotoolbox(),
264 Self::Vulkan => detect_vulkan(),
265 Self::D3d11 => detect_d3d11(),
266 Self::Vaapi => detect_vaapi(),
267 Self::Vdpau => detect_vdpau(),
268 }
269 }
270
271 #[must_use]
273 pub fn supported_codecs(self) -> Vec<&'static str> {
274 match self {
275 Self::None => vec!["h264", "vp8", "vp9", "av1", "theora"],
276 Self::Nvenc => vec!["h264", "h265", "av1"],
277 Self::Qsv => vec!["h264", "h265", "vp9", "av1"],
278 Self::Amd => vec!["h264", "h265", "av1"],
279 Self::VideoToolbox => vec!["h264", "h265"],
280 Self::Vulkan => vec!["h264", "h265"],
281 Self::D3d11 => vec!["h264", "h265", "vp9"],
282 Self::Vaapi => vec!["h264", "h265", "vp8", "vp9", "av1"],
283 Self::Vdpau => vec!["h264", "h265"],
284 }
285 }
286
287 #[must_use]
289 pub fn encoder_name(self, codec: &str) -> Option<String> {
290 match self {
291 Self::None => Some(codec.to_string()),
292 Self::Nvenc => match codec {
293 "h264" => Some("h264_nvenc".to_string()),
294 "h265" => Some("hevc_nvenc".to_string()),
295 "av1" => Some("av1_nvenc".to_string()),
296 _ => None,
297 },
298 Self::Qsv => match codec {
299 "h264" => Some("h264_qsv".to_string()),
300 "h265" => Some("hevc_qsv".to_string()),
301 "vp9" => Some("vp9_qsv".to_string()),
302 "av1" => Some("av1_qsv".to_string()),
303 _ => None,
304 },
305 Self::Amd => match codec {
306 "h264" => Some("h264_amf".to_string()),
307 "h265" => Some("hevc_amf".to_string()),
308 "av1" => Some("av1_amf".to_string()),
309 _ => None,
310 },
311 Self::VideoToolbox => match codec {
312 "h264" => Some("h264_videotoolbox".to_string()),
313 "h265" => Some("hevc_videotoolbox".to_string()),
314 _ => None,
315 },
316 Self::Vulkan => match codec {
317 "h264" => Some("h264_vulkan".to_string()),
318 "h265" => Some("hevc_vulkan".to_string()),
319 _ => None,
320 },
321 Self::D3d11 => match codec {
322 "h264" => Some("h264_d3d11va".to_string()),
323 "h265" => Some("hevc_d3d11va".to_string()),
324 "vp9" => Some("vp9_d3d11va".to_string()),
325 _ => None,
326 },
327 Self::Vaapi => match codec {
328 "h264" => Some("h264_vaapi".to_string()),
329 "h265" => Some("hevc_vaapi".to_string()),
330 "vp8" => Some("vp8_vaapi".to_string()),
331 "vp9" => Some("vp9_vaapi".to_string()),
332 "av1" => Some("av1_vaapi".to_string()),
333 _ => None,
334 },
335 Self::Vdpau => match codec {
336 "h264" => Some("h264_vdpau".to_string()),
337 "h265" => Some("hevc_vdpau".to_string()),
338 _ => None,
339 },
340 }
341 }
342}
343
344#[must_use]
349pub fn detect_available_hw_accel() -> Vec<HwAccelType> {
350 let mut available = vec![HwAccelType::None];
351
352 for accel_type in &[
353 HwAccelType::Nvenc,
354 HwAccelType::Qsv,
355 HwAccelType::Amd,
356 HwAccelType::VideoToolbox,
357 HwAccelType::Vulkan,
358 HwAccelType::D3d11,
359 HwAccelType::Vaapi,
360 HwAccelType::Vdpau,
361 ] {
362 if accel_type.is_available() {
363 available.push(*accel_type);
364 }
365 }
366
367 available
368}
369
370#[must_use]
372pub fn detect_best_hw_accel_for_codec(codec: &str) -> Option<HwAccelType> {
373 detect_available_hw_accel()
374 .into_iter()
375 .find(|&accel_type| accel_type.supported_codecs().contains(&codec))
376}
377
378#[cfg(target_os = "linux")]
382fn detect_nvenc() -> bool {
383 std::path::Path::new("/dev/nvidia0").exists()
384 && std::path::Path::new("/dev/nvidia-modeset").exists()
385}
386
387#[cfg(not(target_os = "linux"))]
388fn detect_nvenc() -> bool {
389 false
390}
391
392#[cfg(target_os = "linux")]
394fn detect_vaapi() -> bool {
395 use std::path::Path;
396
397 if !Path::new("/dev/dri/renderD128").exists() {
398 return false;
399 }
400
401 if !Path::new("/dev/dri/card0").exists() {
402 if !Path::new("/dev/dri/renderD129").exists() {
403 return false;
404 }
405 }
406
407 let driver_paths = [
408 "/usr/lib/dri/i965_drv_video.so",
409 "/usr/lib/dri/iHD_drv_video.so",
410 "/usr/lib/dri/radeonsi_drv_video.so",
411 "/usr/lib/dri/nouveau_drv_video.so",
412 "/usr/lib/x86_64-linux-gnu/dri/i965_drv_video.so",
413 "/usr/lib/x86_64-linux-gnu/dri/iHD_drv_video.so",
414 "/usr/lib/x86_64-linux-gnu/dri/radeonsi_drv_video.so",
415 ];
416 for p in &driver_paths {
417 if Path::new(p).exists() {
418 return true;
419 }
420 }
421
422 true
423}
424
425#[cfg(not(target_os = "linux"))]
426fn detect_vaapi() -> bool {
427 false
428}
429
430#[cfg(target_os = "linux")]
432fn detect_vdpau() -> bool {
433 use std::path::Path;
434 if !Path::new("/dev/dri/renderD128").exists() {
435 return false;
436 }
437 let vdpau_paths = [
438 "/usr/lib/vdpau/libvdpau_nvidia.so",
439 "/usr/lib/vdpau/libvdpau_r600.so",
440 "/usr/lib/vdpau/libvdpau_radeonsi.so",
441 "/usr/lib/x86_64-linux-gnu/vdpau/libvdpau_nvidia.so",
442 "/usr/lib/x86_64-linux-gnu/vdpau/libvdpau_radeonsi.so",
443 ];
444 vdpau_paths.iter().any(|p| Path::new(p).exists())
445}
446
447#[cfg(not(target_os = "linux"))]
448fn detect_vdpau() -> bool {
449 false
450}
451
452#[cfg(target_os = "linux")]
454fn detect_qsv() -> bool {
455 use std::path::Path;
456 if !Path::new("/dev/dri/renderD128").exists() {
457 return false;
458 }
459 let intel_paths = [
460 "/usr/lib/dri/i965_drv_video.so",
461 "/usr/lib/dri/iHD_drv_video.so",
462 "/usr/lib/x86_64-linux-gnu/dri/i965_drv_video.so",
463 "/usr/lib/x86_64-linux-gnu/dri/iHD_drv_video.so",
464 ];
465 intel_paths.iter().any(|p| Path::new(p).exists())
466}
467
468#[cfg(target_os = "windows")]
469fn detect_qsv() -> bool {
470 false
471}
472
473#[cfg(not(any(target_os = "linux", target_os = "windows")))]
474fn detect_qsv() -> bool {
475 false
476}
477
478#[cfg(target_os = "linux")]
480fn detect_amd() -> bool {
481 use std::path::Path;
482 if !Path::new("/dev/dri/renderD128").exists() {
483 return false;
484 }
485 let amd_paths = [
486 "/usr/lib/dri/radeonsi_drv_video.so",
487 "/usr/lib/x86_64-linux-gnu/dri/radeonsi_drv_video.so",
488 ];
489 amd_paths.iter().any(|p| Path::new(p).exists())
490}
491
492#[cfg(target_os = "windows")]
493fn detect_amd() -> bool {
494 false
495}
496
497#[cfg(not(any(target_os = "linux", target_os = "windows")))]
498fn detect_amd() -> bool {
499 false
500}
501
502#[cfg(target_os = "macos")]
504fn detect_videotoolbox() -> bool {
505 let framework_paths = [
506 "/System/Library/Frameworks/VideoToolbox.framework",
507 "/System/Library/Frameworks/CoreMedia.framework",
508 ];
509 framework_paths
510 .iter()
511 .all(|p| std::path::Path::new(p).exists())
512}
513
514#[cfg(not(target_os = "macos"))]
515fn detect_videotoolbox() -> bool {
516 false
517}
518
519#[cfg(target_os = "windows")]
521fn detect_d3d11() -> bool {
522 true
523}
524
525#[cfg(not(target_os = "windows"))]
526fn detect_d3d11() -> bool {
527 false
528}
529
530fn detect_vulkan() -> bool {
532 #[cfg(target_os = "linux")]
533 {
534 let vulkan_icd_paths = [
535 "/usr/share/vulkan/icd.d",
536 "/etc/vulkan/icd.d",
537 "/usr/local/share/vulkan/icd.d",
538 ];
539 vulkan_icd_paths
540 .iter()
541 .any(|p| std::path::Path::new(p).exists())
542 }
543 #[cfg(not(target_os = "linux"))]
544 {
545 false
546 }
547}
548
549impl HwEncoder {
550 #[must_use]
552 pub fn new(
553 accel_type: HwAccelType,
554 codec: impl Into<String>,
555 encoder_name: impl Into<String>,
556 ) -> Self {
557 Self {
558 accel_type,
559 codec: codec.into(),
560 encoder_name: encoder_name.into(),
561 available: false,
562 max_resolution: (7680, 4320),
563 features: Vec::new(),
564 }
565 }
566
567 #[must_use]
569 pub fn available(mut self, available: bool) -> Self {
570 self.available = available;
571 self
572 }
573
574 #[must_use]
576 pub fn max_resolution(mut self, width: u32, height: u32) -> Self {
577 self.max_resolution = (width, height);
578 self
579 }
580
581 #[must_use]
583 pub fn with_feature(mut self, feature: HwFeature) -> Self {
584 self.features.push(feature);
585 self
586 }
587
588 #[must_use]
590 pub fn supports_feature(&self, feature: HwFeature) -> bool {
591 self.features.contains(&feature)
592 }
593}
594
595#[must_use]
597pub fn get_available_encoders() -> Vec<HwEncoder> {
598 let mut encoders = Vec::new();
599
600 for accel_type in detect_available_hw_accel() {
601 for codec in accel_type.supported_codecs() {
602 if let Some(encoder_name) = accel_type.encoder_name(codec) {
603 let encoder = HwEncoder::new(accel_type, codec, encoder_name).available(true);
604 encoders.push(encoder);
605 }
606 }
607 }
608
609 encoders
610}
611
612#[cfg(test)]
615mod tests {
616 use super::*;
617
618 #[test]
619 fn test_hw_accel_type_platform_name() {
620 assert_eq!(HwAccelType::Nvenc.platform_name(), "NVIDIA NVENC");
621 assert_eq!(HwAccelType::Qsv.platform_name(), "Intel Quick Sync");
622 assert_eq!(HwAccelType::Vaapi.platform_name(), "VAAPI");
623 }
624
625 #[test]
626 fn test_hw_accel_supported_codecs() {
627 let codecs = HwAccelType::Nvenc.supported_codecs();
628 assert!(codecs.contains(&"h264"));
629 assert!(codecs.contains(&"h265"));
630 }
631
632 #[test]
633 fn test_hw_accel_encoder_name() {
634 assert_eq!(
635 HwAccelType::Nvenc.encoder_name("h264"),
636 Some("h264_nvenc".to_string())
637 );
638 assert_eq!(
639 HwAccelType::Qsv.encoder_name("h265"),
640 Some("hevc_qsv".to_string())
641 );
642 }
643
644 #[test]
645 fn test_hw_accel_config() {
646 let config = HwAccelConfig::new(HwAccelType::Nvenc)
647 .allow_fallback(false)
648 .decode(true)
649 .encode(true)
650 .device_id(0);
651
652 assert_eq!(config.preferred_type, HwAccelType::Nvenc);
653 assert!(!config.allow_fallback);
654 assert!(config.decode);
655 assert!(config.encode);
656 assert_eq!(config.device_id, Some(0));
657 }
658
659 #[test]
660 fn test_detect_available_hw_accel() {
661 let available = detect_available_hw_accel();
662 assert!(available.contains(&HwAccelType::None)); }
664
665 #[test]
666 fn test_hw_encoder_creation() {
667 let encoder = HwEncoder::new(HwAccelType::Nvenc, "h264", "h264_nvenc")
668 .available(true)
669 .max_resolution(3840, 2160)
670 .with_feature(HwFeature::TenBit)
671 .with_feature(HwFeature::Lookahead);
672
673 assert_eq!(encoder.accel_type, HwAccelType::Nvenc);
674 assert_eq!(encoder.codec, "h264");
675 assert!(encoder.available);
676 assert_eq!(encoder.max_resolution, (3840, 2160));
677 assert!(encoder.supports_feature(HwFeature::TenBit));
678 assert!(encoder.supports_feature(HwFeature::Lookahead));
679 assert!(!encoder.supports_feature(HwFeature::Hdr));
680 }
681
682 #[test]
685 fn test_mock_probe_empty_returns_empty_caps() {
686 let probe = MockProbe(HwAccelCapabilities::none());
687 let caps = detect_hw_accel_with_probe(&probe);
688 assert!(caps.is_empty());
689 }
690
691 #[test]
692 fn test_mock_probe_with_device_is_non_empty() {
693 use super::capabilities::HwAccelDevice;
694 let device = HwAccelDevice {
695 kind: HwKind::VideoToolbox,
696 driver: None,
697 render_node: None,
698 supported_codecs: vec!["h264".to_string(), "hevc".to_string()],
699 max_width: 8192,
700 max_height: 4320,
701 supports_hdr: true,
702 };
703 let caps = HwAccelCapabilities {
704 devices: vec![device],
705 };
706 let probe = MockProbe(caps);
707 let result = detect_hw_accel_with_probe(&probe);
708 assert!(!result.is_empty());
709 assert_eq!(result.devices.len(), 1);
710 assert!(result.devices[0].supports_codec("hevc"));
711 }
712
713 #[test]
714 fn test_hw_accel_capabilities_none_is_empty() {
715 let caps = HwAccelCapabilities::none();
716 assert!(caps.is_empty());
717 assert!(caps.device_for_codec("h264").is_none());
718 }
719
720 #[test]
721 fn test_hw_kind_debug() {
722 let kind = HwKind::Vaapi;
723 assert!(format!("{kind:?}").contains("Vaapi"));
724 }
725}