1use serde::{Deserialize, Serialize};
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
13pub enum HwAccelBackend {
14 None,
16 Vaapi,
18 Nvenc,
20 Videotoolbox,
22 Qsv,
24 Amf,
26 D3d11Va,
28}
29
30impl HwAccelBackend {
31 #[must_use]
33 pub fn as_str(&self) -> &'static str {
34 match self {
35 Self::None => "none",
36 Self::Vaapi => "vaapi",
37 Self::Nvenc => "nvenc",
38 Self::Videotoolbox => "videotoolbox",
39 Self::Qsv => "qsv",
40 Self::Amf => "amf",
41 Self::D3d11Va => "d3d11va",
42 }
43 }
44
45 #[must_use]
47 pub fn all_hw() -> &'static [HwAccelBackend] {
48 &[
49 Self::Vaapi,
50 Self::Nvenc,
51 Self::Videotoolbox,
52 Self::Qsv,
53 Self::Amf,
54 Self::D3d11Va,
55 ]
56 }
57}
58
59#[derive(Debug, Clone, Serialize, Deserialize)]
63pub struct HwAccelCaps {
64 pub backend: HwAccelBackend,
66 pub can_decode: bool,
68 pub can_encode: bool,
70 pub max_resolution: (u32, u32),
72 pub supported_codecs: Vec<String>,
74 pub max_sessions: u8,
76}
77
78impl HwAccelCaps {
79 #[must_use]
81 pub fn supports_codec(&self, codec: &str) -> bool {
82 let codec_lower = codec.to_lowercase();
83 self.supported_codecs
84 .iter()
85 .any(|c| c.to_lowercase() == codec_lower)
86 }
87
88 #[must_use]
90 pub fn supports_resolution(&self, width: u32, height: u32) -> bool {
91 width <= self.max_resolution.0 && height <= self.max_resolution.1
92 }
93}
94
95#[derive(Debug, Clone, Serialize, Deserialize)]
99pub struct HwAccelConfig {
100 pub backend: HwAccelBackend,
102 pub device_index: u8,
104 pub fallback_to_software: bool,
106}
107
108impl HwAccelConfig {
109 #[must_use]
111 pub fn new(backend: HwAccelBackend) -> Self {
112 Self {
113 backend,
114 device_index: 0,
115 fallback_to_software: true,
116 }
117 }
118
119 #[must_use]
121 pub fn software() -> Self {
122 Self::new(HwAccelBackend::None)
123 }
124
125 #[must_use]
127 pub fn with_device(mut self, index: u8) -> Self {
128 self.device_index = index;
129 self
130 }
131
132 #[must_use]
134 pub fn no_fallback(mut self) -> Self {
135 self.fallback_to_software = false;
136 self
137 }
138}
139
140#[derive(Debug, Clone, Default)]
144pub struct HwCodecMapping;
145
146impl HwCodecMapping {
147 #[must_use]
149 pub fn new() -> Self {
150 Self
151 }
152
153 #[must_use]
156 pub fn get_encoder_name(backend: &HwAccelBackend, codec: &str) -> Option<&'static str> {
157 let codec_lower = codec.to_lowercase();
158 match backend {
159 HwAccelBackend::None => None,
160 HwAccelBackend::Vaapi => match codec_lower.as_str() {
161 "hevc" | "h265" => Some("hevc_vaapi"),
162 "h264" | "avc" => Some("h264_vaapi"),
163 "vp9" => Some("vp9_vaapi"),
164 "av1" => Some("av1_vaapi"),
165 "vp8" => Some("vp8_vaapi"),
166 "mjpeg" => Some("mjpeg_vaapi"),
167 _ => None,
168 },
169 HwAccelBackend::Nvenc => match codec_lower.as_str() {
170 "hevc" | "h265" => Some("hevc_nvenc"),
171 "h264" | "avc" => Some("h264_nvenc"),
172 "av1" => Some("av1_nvenc"),
173 _ => None,
174 },
175 HwAccelBackend::Videotoolbox => match codec_lower.as_str() {
176 "hevc" | "h265" => Some("hevc_videotoolbox"),
177 "h264" | "avc" => Some("h264_videotoolbox"),
178 _ => None,
179 },
180 HwAccelBackend::Qsv => match codec_lower.as_str() {
181 "hevc" | "h265" => Some("hevc_qsv"),
182 "h264" | "avc" => Some("h264_qsv"),
183 "vp9" => Some("vp9_qsv"),
184 "av1" => Some("av1_qsv"),
185 "mjpeg" => Some("mjpeg_qsv"),
186 _ => None,
187 },
188 HwAccelBackend::Amf => match codec_lower.as_str() {
189 "hevc" | "h265" => Some("hevc_amf"),
190 "h264" | "avc" => Some("h264_amf"),
191 "av1" => Some("av1_amf"),
192 _ => None,
193 },
194 HwAccelBackend::D3d11Va => match codec_lower.as_str() {
195 "hevc" | "h265" => Some("hevc_d3d11va"),
196 "h264" | "avc" => Some("h264_d3d11va"),
197 _ => None,
198 },
199 }
200 }
201
202 #[must_use]
204 pub fn encoder_name(backend: &HwAccelBackend, codec: &str) -> Option<&'static str> {
205 Self::get_encoder_name(backend, codec)
206 }
207}
208
209#[must_use]
215pub fn simulate_hw_caps(backend: HwAccelBackend) -> HwAccelCaps {
216 match backend {
217 HwAccelBackend::None => HwAccelCaps {
218 backend,
219 can_decode: true,
220 can_encode: true,
221 max_resolution: (7680, 4320),
222 supported_codecs: vec![
223 "h264".to_string(),
224 "hevc".to_string(),
225 "vp9".to_string(),
226 "av1".to_string(),
227 "vp8".to_string(),
228 "opus".to_string(),
229 "flac".to_string(),
230 ],
231 max_sessions: 32,
232 },
233 HwAccelBackend::Vaapi => HwAccelCaps {
234 backend,
235 can_decode: true,
236 can_encode: true,
237 max_resolution: (4096, 4096),
238 supported_codecs: vec![
239 "h264".to_string(),
240 "hevc".to_string(),
241 "vp9".to_string(),
242 "av1".to_string(),
243 "vp8".to_string(),
244 "mjpeg".to_string(),
245 ],
246 max_sessions: 8,
247 },
248 HwAccelBackend::Nvenc => HwAccelCaps {
249 backend,
250 can_decode: true,
251 can_encode: true,
252 max_resolution: (7680, 4320),
253 supported_codecs: vec!["h264".to_string(), "hevc".to_string(), "av1".to_string()],
254 max_sessions: 3,
255 },
256 HwAccelBackend::Videotoolbox => HwAccelCaps {
257 backend,
258 can_decode: true,
259 can_encode: true,
260 max_resolution: (4096, 2160),
261 supported_codecs: vec!["h264".to_string(), "hevc".to_string()],
262 max_sessions: 4,
263 },
264 HwAccelBackend::Qsv => HwAccelCaps {
265 backend,
266 can_decode: true,
267 can_encode: true,
268 max_resolution: (8192, 8192),
269 supported_codecs: vec![
270 "h264".to_string(),
271 "hevc".to_string(),
272 "vp9".to_string(),
273 "av1".to_string(),
274 "mjpeg".to_string(),
275 ],
276 max_sessions: 6,
277 },
278 HwAccelBackend::Amf => HwAccelCaps {
279 backend,
280 can_decode: true,
281 can_encode: true,
282 max_resolution: (7680, 4320),
283 supported_codecs: vec!["h264".to_string(), "hevc".to_string(), "av1".to_string()],
284 max_sessions: 4,
285 },
286 HwAccelBackend::D3d11Va => HwAccelCaps {
287 backend,
288 can_decode: true,
289 can_encode: false, max_resolution: (4096, 2160),
291 supported_codecs: vec!["h264".to_string(), "hevc".to_string()],
292 max_sessions: 2,
293 },
294 }
295}
296
297#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
301pub enum LatencyMode {
302 Batch,
304 LowLatency {
306 max_delay_ms: u32,
308 },
309 Realtime,
311}
312
313impl LatencyMode {
314 #[must_use]
316 pub fn is_live(&self) -> bool {
317 matches!(self, Self::LowLatency { .. } | Self::Realtime)
318 }
319}
320
321#[derive(Debug, Clone, Serialize, Deserialize)]
325pub struct PipelineHwConfig {
326 pub hw_accel: HwAccelConfig,
328 pub thread_count: u8,
330 pub buffer_count: u8,
332 pub latency_mode: LatencyMode,
334}
335
336impl PipelineHwConfig {
337 #[must_use]
339 pub fn default_software() -> Self {
340 Self {
341 hw_accel: HwAccelConfig::software(),
342 thread_count: 4,
343 buffer_count: 8,
344 latency_mode: LatencyMode::Batch,
345 }
346 }
347
348 #[must_use]
350 pub fn low_latency(backend: HwAccelBackend, max_delay_ms: u32) -> Self {
351 Self {
352 hw_accel: HwAccelConfig::new(backend),
353 thread_count: 2,
354 buffer_count: 4,
355 latency_mode: LatencyMode::LowLatency { max_delay_ms },
356 }
357 }
358}
359
360#[derive(Debug, Clone, Default)]
364pub struct HwAccelSelector;
365
366impl HwAccelSelector {
367 #[must_use]
369 pub fn new() -> Self {
370 Self
371 }
372
373 #[must_use]
381 pub fn select_best(
382 available: &[HwAccelCaps],
383 codec: &str,
384 resolution: (u32, u32),
385 ) -> Option<HwAccelConfig> {
386 let (width, height) = resolution;
387 available
388 .iter()
389 .filter(|caps| {
390 caps.can_encode
391 && caps.supports_codec(codec)
392 && caps.supports_resolution(width, height)
393 && caps.backend != HwAccelBackend::None
394 })
395 .max_by_key(|caps| caps.max_sessions)
396 .map(|caps| HwAccelConfig::new(caps.backend))
397 }
398
399 #[must_use]
401 pub fn select_best_or_software(
402 available: &[HwAccelCaps],
403 codec: &str,
404 resolution: (u32, u32),
405 ) -> HwAccelConfig {
406 Self::select_best(available, codec, resolution).unwrap_or_else(HwAccelConfig::software)
407 }
408}
409
410#[cfg(test)]
413mod tests {
414 use super::*;
415
416 #[test]
419 fn test_backend_as_str() {
420 assert_eq!(HwAccelBackend::None.as_str(), "none");
421 assert_eq!(HwAccelBackend::Vaapi.as_str(), "vaapi");
422 assert_eq!(HwAccelBackend::Nvenc.as_str(), "nvenc");
423 assert_eq!(HwAccelBackend::Videotoolbox.as_str(), "videotoolbox");
424 assert_eq!(HwAccelBackend::Qsv.as_str(), "qsv");
425 assert_eq!(HwAccelBackend::Amf.as_str(), "amf");
426 assert_eq!(HwAccelBackend::D3d11Va.as_str(), "d3d11va");
427 }
428
429 #[test]
430 fn test_all_hw_contains_six_backends() {
431 assert_eq!(HwAccelBackend::all_hw().len(), 6);
432 assert!(!HwAccelBackend::all_hw().contains(&HwAccelBackend::None));
433 }
434
435 #[test]
438 fn test_vaapi_hevc_encoder_name() {
439 assert_eq!(
440 HwCodecMapping::get_encoder_name(&HwAccelBackend::Vaapi, "hevc"),
441 Some("hevc_vaapi")
442 );
443 }
444
445 #[test]
446 fn test_nvenc_hevc_encoder_name() {
447 assert_eq!(
448 HwCodecMapping::get_encoder_name(&HwAccelBackend::Nvenc, "hevc"),
449 Some("hevc_nvenc")
450 );
451 }
452
453 #[test]
454 fn test_videotoolbox_hevc_encoder_name() {
455 assert_eq!(
456 HwCodecMapping::get_encoder_name(&HwAccelBackend::Videotoolbox, "hevc"),
457 Some("hevc_videotoolbox")
458 );
459 }
460
461 #[test]
462 fn test_qsv_h264_encoder_name() {
463 assert_eq!(
464 HwCodecMapping::get_encoder_name(&HwAccelBackend::Qsv, "h264"),
465 Some("h264_qsv")
466 );
467 }
468
469 #[test]
470 fn test_amf_av1_encoder_name() {
471 assert_eq!(
472 HwCodecMapping::get_encoder_name(&HwAccelBackend::Amf, "av1"),
473 Some("av1_amf")
474 );
475 }
476
477 #[test]
478 fn test_none_backend_returns_none() {
479 assert_eq!(
480 HwCodecMapping::get_encoder_name(&HwAccelBackend::None, "h264"),
481 None
482 );
483 }
484
485 #[test]
486 fn test_unsupported_codec_returns_none() {
487 assert_eq!(
488 HwCodecMapping::get_encoder_name(&HwAccelBackend::Nvenc, "vp9"),
489 None
490 );
491 }
492
493 #[test]
496 fn test_simulate_vaapi_can_encode_and_decode() {
497 let caps = simulate_hw_caps(HwAccelBackend::Vaapi);
498 assert!(caps.can_encode);
499 assert!(caps.can_decode);
500 }
501
502 #[test]
503 fn test_simulate_d3d11va_is_decode_only() {
504 let caps = simulate_hw_caps(HwAccelBackend::D3d11Va);
505 assert!(!caps.can_encode);
506 assert!(caps.can_decode);
507 }
508
509 #[test]
510 fn test_simulate_nvenc_supports_hevc() {
511 let caps = simulate_hw_caps(HwAccelBackend::Nvenc);
512 assert!(caps.supports_codec("hevc"));
513 }
514
515 #[test]
516 fn test_simulate_nvenc_does_not_support_vp9() {
517 let caps = simulate_hw_caps(HwAccelBackend::Nvenc);
518 assert!(!caps.supports_codec("vp9"));
519 }
520
521 #[test]
522 fn test_simulate_resolution_check() {
523 let caps = simulate_hw_caps(HwAccelBackend::Videotoolbox);
524 assert!(caps.supports_resolution(1920, 1080));
525 assert!(!caps.supports_resolution(7680, 4320)); }
527
528 #[test]
529 fn test_simulate_none_is_unlimited() {
530 let caps = simulate_hw_caps(HwAccelBackend::None);
531 assert!(caps.supports_resolution(7680, 4320));
532 }
533
534 #[test]
537 fn test_hw_accel_config_new() {
538 let cfg = HwAccelConfig::new(HwAccelBackend::Vaapi);
539 assert_eq!(cfg.backend, HwAccelBackend::Vaapi);
540 assert_eq!(cfg.device_index, 0);
541 assert!(cfg.fallback_to_software);
542 }
543
544 #[test]
545 fn test_hw_accel_config_no_fallback() {
546 let cfg = HwAccelConfig::new(HwAccelBackend::Nvenc).no_fallback();
547 assert!(!cfg.fallback_to_software);
548 }
549
550 #[test]
551 fn test_hw_accel_config_with_device() {
552 let cfg = HwAccelConfig::new(HwAccelBackend::Nvenc).with_device(2);
553 assert_eq!(cfg.device_index, 2);
554 }
555
556 #[test]
559 fn test_latency_mode_is_live() {
560 assert!(!LatencyMode::Batch.is_live());
561 assert!(LatencyMode::LowLatency { max_delay_ms: 100 }.is_live());
562 assert!(LatencyMode::Realtime.is_live());
563 }
564
565 #[test]
566 fn test_latency_mode_equality() {
567 assert_eq!(
568 LatencyMode::LowLatency { max_delay_ms: 200 },
569 LatencyMode::LowLatency { max_delay_ms: 200 }
570 );
571 assert_ne!(
572 LatencyMode::LowLatency { max_delay_ms: 100 },
573 LatencyMode::LowLatency { max_delay_ms: 200 }
574 );
575 }
576
577 #[test]
580 fn test_selector_picks_highest_sessions() {
581 let caps = vec![
582 simulate_hw_caps(HwAccelBackend::Vaapi), simulate_hw_caps(HwAccelBackend::Nvenc), simulate_hw_caps(HwAccelBackend::Qsv), ];
586 let cfg = HwAccelSelector::select_best(&caps, "h264", (1920, 1080));
587 assert!(cfg.is_some());
588 assert_eq!(
589 cfg.expect("should have config").backend,
590 HwAccelBackend::Vaapi
591 );
592 }
593
594 #[test]
595 fn test_selector_filters_unsupported_codec() {
596 let caps = vec![
598 simulate_hw_caps(HwAccelBackend::D3d11Va),
599 simulate_hw_caps(HwAccelBackend::Videotoolbox),
600 ];
601 let cfg = HwAccelSelector::select_best(&caps, "hevc", (1920, 1080));
602 assert!(cfg.is_some());
603 assert_eq!(
604 cfg.expect("should have config").backend,
605 HwAccelBackend::Videotoolbox
606 );
607 }
608
609 #[test]
610 fn test_selector_returns_none_when_no_match() {
611 let caps = vec![simulate_hw_caps(HwAccelBackend::D3d11Va)]; let cfg = HwAccelSelector::select_best(&caps, "h264", (1920, 1080));
613 assert!(cfg.is_none());
614 }
615
616 #[test]
617 fn test_selector_fallback_to_software() {
618 let caps: Vec<HwAccelCaps> = vec![];
619 let cfg = HwAccelSelector::select_best_or_software(&caps, "h264", (1920, 1080));
620 assert_eq!(cfg.backend, HwAccelBackend::None);
621 }
622
623 #[test]
624 fn test_pipeline_hw_config_default_software() {
625 let cfg = PipelineHwConfig::default_software();
626 assert_eq!(cfg.hw_accel.backend, HwAccelBackend::None);
627 assert_eq!(cfg.latency_mode, LatencyMode::Batch);
628 }
629
630 #[test]
631 fn test_pipeline_hw_config_low_latency() {
632 let cfg = PipelineHwConfig::low_latency(HwAccelBackend::Nvenc, 50);
633 assert!(cfg.latency_mode.is_live());
634 assert_eq!(cfg.hw_accel.backend, HwAccelBackend::Nvenc);
635 }
636}