1#![allow(dead_code)]
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
10pub enum CodecLevel {
11 Level30,
13 Level31,
15 Level40,
17 Level41,
19 Level50,
21 Level51,
23 Level52,
25}
26
27impl CodecLevel {
28 #[allow(clippy::cast_precision_loss)]
30 #[must_use]
31 pub fn as_f32(self) -> f32 {
32 match self {
33 CodecLevel::Level30 => 3.0,
34 CodecLevel::Level31 => 3.1,
35 CodecLevel::Level40 => 4.0,
36 CodecLevel::Level41 => 4.1,
37 CodecLevel::Level50 => 5.0,
38 CodecLevel::Level51 => 5.1,
39 CodecLevel::Level52 => 5.2,
40 }
41 }
42
43 #[must_use]
45 pub fn max_pixels(self) -> u64 {
46 match self {
47 CodecLevel::Level30 => 1_280 * 720,
48 CodecLevel::Level31 => 1_280 * 720,
49 CodecLevel::Level40 => 1_920 * 1_080,
50 CodecLevel::Level41 => 1_920 * 1_080,
51 CodecLevel::Level50 => 3_840 * 2_160,
52 CodecLevel::Level51 => 3_840 * 2_160,
53 CodecLevel::Level52 => 7_680 * 4_320,
54 }
55 }
56}
57
58#[derive(Debug, Clone)]
60pub struct CodecProfile {
61 pub codec: String,
63 pub level: CodecLevel,
65 max_bitrate_mbps_val: f64,
67 hw_encodable: bool,
69 supports_10bit: bool,
71}
72
73impl CodecProfile {
74 pub fn new(
76 codec: impl Into<String>,
77 level: CodecLevel,
78 max_bitrate_mbps: f64,
79 hw_encodable: bool,
80 ) -> Self {
81 Self {
82 codec: codec.into(),
83 level,
84 max_bitrate_mbps_val: max_bitrate_mbps,
85 hw_encodable,
86 supports_10bit: false,
87 }
88 }
89
90 #[must_use]
92 pub fn with_10bit(mut self) -> Self {
93 self.supports_10bit = true;
94 self
95 }
96
97 #[must_use]
99 pub fn max_bitrate_mbps(&self) -> f64 {
100 self.max_bitrate_mbps_val
101 }
102
103 #[must_use]
105 pub fn is_hardware_encodable(&self) -> bool {
106 self.hw_encodable
107 }
108
109 #[must_use]
111 pub fn supports_10bit(&self) -> bool {
112 self.supports_10bit
113 }
114
115 #[must_use]
117 pub fn max_pixels(&self) -> u64 {
118 self.level.max_pixels()
119 }
120
121 #[must_use]
123 pub fn supports_resolution(&self, width: u32, height: u32) -> bool {
124 let pixels = u64::from(width) * u64::from(height);
125 pixels <= self.level.max_pixels()
126 }
127}
128
129#[derive(Debug, Default)]
131pub struct CodecProfileSelector {
132 profiles: Vec<CodecProfile>,
133}
134
135impl CodecProfileSelector {
136 #[must_use]
138 pub fn new() -> Self {
139 Self::default()
140 }
141
142 #[must_use]
144 pub fn with_h264_defaults() -> Self {
145 let mut s = Self::new();
146 s.add(CodecProfile::new("h264", CodecLevel::Level31, 10.0, true));
147 s.add(CodecProfile::new("h264", CodecLevel::Level41, 50.0, true));
148 s.add(CodecProfile::new("h264", CodecLevel::Level51, 240.0, true));
149 s
150 }
151
152 #[must_use]
154 pub fn with_hevc_defaults() -> Self {
155 let mut s = Self::new();
156 s.add(CodecProfile::new("hevc", CodecLevel::Level41, 40.0, true).with_10bit());
157 s.add(CodecProfile::new("hevc", CodecLevel::Level51, 160.0, true).with_10bit());
158 s.add(CodecProfile::new("hevc", CodecLevel::Level52, 640.0, false).with_10bit());
159 s
160 }
161
162 pub fn add(&mut self, profile: CodecProfile) {
164 self.profiles.push(profile);
165 }
166
167 #[must_use]
170 pub fn select_for_resolution(&self, width: u32, height: u32) -> Option<&CodecProfile> {
171 self.profiles
172 .iter()
173 .filter(|p| p.supports_resolution(width, height))
174 .min_by(|a, b| a.level.cmp(&b.level))
175 }
176
177 #[must_use]
179 pub fn all_for_resolution(&self, width: u32, height: u32) -> Vec<&CodecProfile> {
180 self.profiles
181 .iter()
182 .filter(|p| p.supports_resolution(width, height))
183 .collect()
184 }
185
186 #[must_use]
188 pub fn profile_count(&self) -> usize {
189 self.profiles.len()
190 }
191}
192
193#[derive(Debug, Clone, PartialEq, Eq)]
198pub struct CodecTunePreset {
199 pub codec: String,
201 pub tune_name: String,
203 pub options: Vec<(String, String)>,
205 pub description: String,
207}
208
209impl CodecTunePreset {
210 #[must_use]
216 pub fn av1_film() -> Self {
217 Self {
218 codec: "av1".to_string(),
219 tune_name: "film".to_string(),
220 options: vec![
221 ("enable-film-grain".to_string(), "1".to_string()),
222 ("film-grain-denoise".to_string(), "1".to_string()),
223 ("film-grain-table".to_string(), "auto".to_string()),
224 ("aq-mode".to_string(), "2".to_string()),
225 ("deltaq-mode".to_string(), "3".to_string()),
226 ("enable-qm".to_string(), "1".to_string()),
227 ("qm-min".to_string(), "5".to_string()),
228 ("tune".to_string(), "ssim".to_string()),
229 ("arnr-maxframes".to_string(), "7".to_string()),
230 ("arnr-strength".to_string(), "4".to_string()),
231 ],
232 description: "Film: grain synthesis, perceptual quality, natural motion".to_string(),
233 }
234 }
235
236 #[must_use]
241 pub fn av1_animation() -> Self {
242 Self {
243 codec: "av1".to_string(),
244 tune_name: "animation".to_string(),
245 options: vec![
246 ("enable-film-grain".to_string(), "0".to_string()),
247 ("aq-mode".to_string(), "0".to_string()),
248 ("deltaq-mode".to_string(), "0".to_string()),
249 ("enable-qm".to_string(), "1".to_string()),
250 ("qm-min".to_string(), "0".to_string()),
251 ("qm-max".to_string(), "8".to_string()),
252 ("tune".to_string(), "psnr".to_string()),
253 ("enable-keyframe-filtering".to_string(), "0".to_string()),
254 ("arnr-maxframes".to_string(), "15".to_string()),
255 ("arnr-strength".to_string(), "6".to_string()),
256 ("enable-smooth-interintra".to_string(), "1".to_string()),
257 ],
258 description: "Animation: flat areas, sharp edges, PSNR-optimized".to_string(),
259 }
260 }
261
262 #[must_use]
268 pub fn av1_grain() -> Self {
269 Self {
270 codec: "av1".to_string(),
271 tune_name: "grain".to_string(),
272 options: vec![
273 ("enable-film-grain".to_string(), "1".to_string()),
274 ("film-grain-denoise".to_string(), "0".to_string()),
275 ("aq-mode".to_string(), "1".to_string()),
276 ("deltaq-mode".to_string(), "3".to_string()),
277 ("enable-qm".to_string(), "1".to_string()),
278 ("qm-min".to_string(), "0".to_string()),
279 ("tune".to_string(), "ssim".to_string()),
280 ("arnr-maxframes".to_string(), "4".to_string()),
281 ("arnr-strength".to_string(), "2".to_string()),
282 ("noise-sensitivity".to_string(), "0".to_string()),
283 ],
284 description: "Grain: preserve film grain and high-frequency detail".to_string(),
285 }
286 }
287
288 #[must_use]
292 pub fn vp9_film() -> Self {
293 Self {
294 codec: "vp9".to_string(),
295 tune_name: "film".to_string(),
296 options: vec![
297 ("aq-mode".to_string(), "2".to_string()),
298 ("lag-in-frames".to_string(), "25".to_string()),
299 ("auto-alt-ref".to_string(), "6".to_string()),
300 ("arnr-maxframes".to_string(), "7".to_string()),
301 ("arnr-strength".to_string(), "4".to_string()),
302 ("arnr-type".to_string(), "3".to_string()),
303 ("tune".to_string(), "ssim".to_string()),
304 ("row-mt".to_string(), "1".to_string()),
305 ],
306 description: "Film: temporal filtering, perceptual quality for live-action".to_string(),
307 }
308 }
309
310 #[must_use]
314 pub fn vp9_animation() -> Self {
315 Self {
316 codec: "vp9".to_string(),
317 tune_name: "animation".to_string(),
318 options: vec![
319 ("aq-mode".to_string(), "0".to_string()),
320 ("lag-in-frames".to_string(), "25".to_string()),
321 ("auto-alt-ref".to_string(), "6".to_string()),
322 ("arnr-maxframes".to_string(), "15".to_string()),
323 ("arnr-strength".to_string(), "6".to_string()),
324 ("arnr-type".to_string(), "3".to_string()),
325 ("tune".to_string(), "psnr".to_string()),
326 ("row-mt".to_string(), "1".to_string()),
327 ],
328 description: "Animation: flat areas, sharp edges, PSNR-optimized for VP9".to_string(),
329 }
330 }
331
332 #[must_use]
334 pub fn vp9_grain() -> Self {
335 Self {
336 codec: "vp9".to_string(),
337 tune_name: "grain".to_string(),
338 options: vec![
339 ("aq-mode".to_string(), "0".to_string()),
340 ("lag-in-frames".to_string(), "25".to_string()),
341 ("auto-alt-ref".to_string(), "2".to_string()),
342 ("arnr-maxframes".to_string(), "4".to_string()),
343 ("arnr-strength".to_string(), "1".to_string()),
344 ("arnr-type".to_string(), "3".to_string()),
345 ("tune".to_string(), "ssim".to_string()),
346 ("noise-sensitivity".to_string(), "0".to_string()),
347 ("row-mt".to_string(), "1".to_string()),
348 ],
349 description: "Grain: minimal temporal filtering to preserve texture".to_string(),
350 }
351 }
352
353 #[must_use]
355 pub fn presets_for_codec(codec: &str) -> Vec<Self> {
356 match codec.to_lowercase().as_str() {
357 "av1" | "libaom-av1" | "svt-av1" | "rav1e" => {
358 vec![Self::av1_film(), Self::av1_animation(), Self::av1_grain()]
359 }
360 "vp9" | "libvpx-vp9" => {
361 vec![Self::vp9_film(), Self::vp9_animation(), Self::vp9_grain()]
362 }
363 _ => Vec::new(),
364 }
365 }
366
367 #[must_use]
369 pub fn find(codec: &str, tune_name: &str) -> Option<Self> {
370 Self::presets_for_codec(codec)
371 .into_iter()
372 .find(|p| p.tune_name == tune_name)
373 }
374
375 #[must_use]
377 pub fn option_count(&self) -> usize {
378 self.options.len()
379 }
380
381 pub fn apply_to_options(&self, options: &mut Vec<(String, String)>) {
385 for (key, value) in &self.options {
386 options.retain(|(k, _)| k != key);
388 options.push((key.clone(), value.clone()));
389 }
390 }
391}
392
393#[cfg(test)]
394mod tests {
395 use super::*;
396
397 #[test]
398 fn test_codec_level_ordering() {
399 assert!(CodecLevel::Level30 < CodecLevel::Level31);
400 assert!(CodecLevel::Level41 < CodecLevel::Level50);
401 assert!(CodecLevel::Level51 < CodecLevel::Level52);
402 }
403
404 #[test]
405 fn test_codec_level_as_f32() {
406 let v = CodecLevel::Level41.as_f32();
407 assert!((v - 4.1_f32).abs() < 0.001);
408 }
409
410 #[test]
411 fn test_codec_level_max_pixels_1080p() {
412 assert_eq!(CodecLevel::Level41.max_pixels(), 1_920 * 1_080);
413 }
414
415 #[test]
416 fn test_codec_level_max_pixels_4k() {
417 assert_eq!(CodecLevel::Level51.max_pixels(), 3_840 * 2_160);
418 }
419
420 #[test]
421 fn test_profile_max_bitrate() {
422 let p = CodecProfile::new("h264", CodecLevel::Level41, 50.0, true);
423 assert!((p.max_bitrate_mbps() - 50.0).abs() < f64::EPSILON);
424 }
425
426 #[test]
427 fn test_profile_hw_encodable() {
428 let p = CodecProfile::new("h264", CodecLevel::Level41, 50.0, true);
429 assert!(p.is_hardware_encodable());
430 let p2 = CodecProfile::new("av1", CodecLevel::Level51, 100.0, false);
431 assert!(!p2.is_hardware_encodable());
432 }
433
434 #[test]
435 fn test_profile_10bit_default_false() {
436 let p = CodecProfile::new("h264", CodecLevel::Level41, 50.0, true);
437 assert!(!p.supports_10bit());
438 }
439
440 #[test]
441 fn test_profile_with_10bit() {
442 let p = CodecProfile::new("hevc", CodecLevel::Level51, 160.0, true).with_10bit();
443 assert!(p.supports_10bit());
444 }
445
446 #[test]
447 fn test_profile_supports_resolution_1080p() {
448 let p = CodecProfile::new("h264", CodecLevel::Level41, 50.0, true);
449 assert!(p.supports_resolution(1920, 1080));
450 assert!(!p.supports_resolution(3840, 2160));
451 }
452
453 #[test]
454 fn test_selector_h264_defaults_count() {
455 let sel = CodecProfileSelector::with_h264_defaults();
456 assert_eq!(sel.profile_count(), 3);
457 }
458
459 #[test]
460 fn test_selector_select_for_1080p_returns_lowest_level() {
461 let sel = CodecProfileSelector::with_h264_defaults();
462 let p = sel
463 .select_for_resolution(1920, 1080)
464 .expect("should succeed in test");
465 assert_eq!(p.level, CodecLevel::Level41);
467 }
468
469 #[test]
470 fn test_selector_select_for_720p() {
471 let sel = CodecProfileSelector::with_h264_defaults();
472 let p = sel
473 .select_for_resolution(1280, 720)
474 .expect("should succeed in test");
475 assert_eq!(p.level, CodecLevel::Level31);
476 }
477
478 #[test]
479 fn test_selector_select_for_8k_returns_none_h264() {
480 let sel = CodecProfileSelector::with_h264_defaults();
481 assert!(sel.select_for_resolution(7680, 4320).is_none());
483 }
484
485 #[test]
486 fn test_selector_hevc_supports_4k() {
487 let sel = CodecProfileSelector::with_hevc_defaults();
488 let p = sel
489 .select_for_resolution(3840, 2160)
490 .expect("should succeed in test");
491 assert_eq!(p.codec, "hevc");
492 assert!(p.supports_10bit());
493 }
494
495 #[test]
498 fn test_av1_film_preset() {
499 let p = CodecTunePreset::av1_film();
500 assert_eq!(p.codec, "av1");
501 assert_eq!(p.tune_name, "film");
502 assert!(p.option_count() > 0);
503 assert!(p
504 .options
505 .iter()
506 .any(|(k, v)| k == "enable-film-grain" && v == "1"));
507 assert!(p.options.iter().any(|(k, v)| k == "tune" && v == "ssim"));
508 }
509
510 #[test]
511 fn test_av1_animation_preset() {
512 let p = CodecTunePreset::av1_animation();
513 assert_eq!(p.codec, "av1");
514 assert_eq!(p.tune_name, "animation");
515 assert!(p
516 .options
517 .iter()
518 .any(|(k, v)| k == "enable-film-grain" && v == "0"));
519 assert!(p.options.iter().any(|(k, v)| k == "tune" && v == "psnr"));
520 }
521
522 #[test]
523 fn test_av1_grain_preset() {
524 let p = CodecTunePreset::av1_grain();
525 assert_eq!(p.codec, "av1");
526 assert_eq!(p.tune_name, "grain");
527 assert!(p
529 .options
530 .iter()
531 .any(|(k, v)| k == "enable-film-grain" && v == "1"));
532 assert!(p
533 .options
534 .iter()
535 .any(|(k, v)| k == "film-grain-denoise" && v == "0"));
536 }
537
538 #[test]
539 fn test_vp9_film_preset() {
540 let p = CodecTunePreset::vp9_film();
541 assert_eq!(p.codec, "vp9");
542 assert_eq!(p.tune_name, "film");
543 assert!(p
544 .options
545 .iter()
546 .any(|(k, v)| k == "lag-in-frames" && v == "25"));
547 assert!(p.options.iter().any(|(k, v)| k == "tune" && v == "ssim"));
548 }
549
550 #[test]
551 fn test_vp9_animation_preset() {
552 let p = CodecTunePreset::vp9_animation();
553 assert_eq!(p.codec, "vp9");
554 assert_eq!(p.tune_name, "animation");
555 assert!(p.options.iter().any(|(k, v)| k == "tune" && v == "psnr"));
556 }
557
558 #[test]
559 fn test_vp9_grain_preset() {
560 let p = CodecTunePreset::vp9_grain();
561 assert_eq!(p.codec, "vp9");
562 assert_eq!(p.tune_name, "grain");
563 assert!(p
565 .options
566 .iter()
567 .any(|(k, v)| k == "arnr-strength" && v == "1"));
568 }
569
570 #[test]
571 fn test_presets_for_av1() {
572 let presets = CodecTunePreset::presets_for_codec("av1");
573 assert_eq!(presets.len(), 3);
574 let names: Vec<&str> = presets.iter().map(|p| p.tune_name.as_str()).collect();
575 assert!(names.contains(&"film"));
576 assert!(names.contains(&"animation"));
577 assert!(names.contains(&"grain"));
578 }
579
580 #[test]
581 fn test_presets_for_vp9() {
582 let presets = CodecTunePreset::presets_for_codec("vp9");
583 assert_eq!(presets.len(), 3);
584 }
585
586 #[test]
587 fn test_presets_for_av1_alias() {
588 let presets = CodecTunePreset::presets_for_codec("libaom-av1");
589 assert_eq!(presets.len(), 3);
590 }
591
592 #[test]
593 fn test_presets_for_vp9_alias() {
594 let presets = CodecTunePreset::presets_for_codec("libvpx-vp9");
595 assert_eq!(presets.len(), 3);
596 }
597
598 #[test]
599 fn test_presets_for_unknown_codec() {
600 let presets = CodecTunePreset::presets_for_codec("h264");
601 assert!(presets.is_empty());
602 }
603
604 #[test]
605 fn test_find_av1_film() {
606 let p = CodecTunePreset::find("av1", "film");
607 assert!(p.is_some());
608 let p = p.expect("should find av1 film preset");
609 assert_eq!(p.tune_name, "film");
610 }
611
612 #[test]
613 fn test_find_vp9_animation() {
614 let p = CodecTunePreset::find("vp9", "animation");
615 assert!(p.is_some());
616 }
617
618 #[test]
619 fn test_find_nonexistent() {
620 assert!(CodecTunePreset::find("av1", "nonexistent").is_none());
621 assert!(CodecTunePreset::find("h264", "film").is_none());
622 }
623
624 #[test]
625 fn test_apply_to_options() {
626 let preset = CodecTunePreset::av1_film();
627 let mut options = vec![
628 ("tune".to_string(), "psnr".to_string()),
629 ("custom".to_string(), "value".to_string()),
630 ];
631 preset.apply_to_options(&mut options);
632 let tune_val = options
634 .iter()
635 .find(|(k, _)| k == "tune")
636 .map(|(_, v)| v.as_str());
637 assert_eq!(tune_val, Some("ssim"));
638 assert!(options.iter().any(|(k, _)| k == "custom"));
640 assert!(options
642 .iter()
643 .any(|(k, v)| k == "enable-film-grain" && v == "1"));
644 }
645
646 #[test]
647 fn test_apply_to_empty_options() {
648 let preset = CodecTunePreset::vp9_grain();
649 let mut options = Vec::new();
650 preset.apply_to_options(&mut options);
651 assert_eq!(options.len(), preset.option_count());
652 }
653
654 #[test]
655 fn test_description_not_empty() {
656 let presets = [
657 CodecTunePreset::av1_film(),
658 CodecTunePreset::av1_animation(),
659 CodecTunePreset::av1_grain(),
660 CodecTunePreset::vp9_film(),
661 CodecTunePreset::vp9_animation(),
662 CodecTunePreset::vp9_grain(),
663 ];
664 for p in &presets {
665 assert!(
666 !p.description.is_empty(),
667 "Description for {}:{} should not be empty",
668 p.codec,
669 p.tune_name
670 );
671 }
672 }
673}