1#![allow(clippy::cast_lossless)]
25#![allow(clippy::cast_precision_loss)]
26
27#[derive(Debug, Clone, PartialEq, Eq, Hash)]
35pub enum Av1Profile {
36 Main,
38 High,
40 Professional,
42}
43
44impl std::fmt::Display for Av1Profile {
45 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
46 match self {
47 Self::Main => write!(f, "Main"),
48 Self::High => write!(f, "High"),
49 Self::Professional => write!(f, "Professional"),
50 }
51 }
52}
53
54#[derive(Debug, Clone, PartialEq, Eq, Hash)]
58pub enum Vp9Profile {
59 Profile0,
61 Profile1,
63 Profile2,
65 Profile3,
67}
68
69impl std::fmt::Display for Vp9Profile {
70 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
71 match self {
72 Self::Profile0 => write!(f, "Profile 0"),
73 Self::Profile1 => write!(f, "Profile 1"),
74 Self::Profile2 => write!(f, "Profile 2"),
75 Self::Profile3 => write!(f, "Profile 3"),
76 }
77 }
78}
79
80#[derive(Debug, Clone, PartialEq, Eq, Hash)]
84pub enum OpusProfile {
85 Voip,
87 Audio,
89 RestrictedLowDelay,
91}
92
93impl std::fmt::Display for OpusProfile {
94 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
95 match self {
96 Self::Voip => write!(f, "VOIP"),
97 Self::Audio => write!(f, "Audio"),
98 Self::RestrictedLowDelay => write!(f, "Restricted Low-Delay"),
99 }
100 }
101}
102
103#[derive(Debug, Clone, PartialEq, Eq)]
113pub struct Av1Level {
114 pub level: u8,
116 pub max_pic_size: u64,
118 pub max_h_size: u32,
120 pub max_v_size: u32,
122 pub max_display_rate: u64,
124 pub max_bitrate_mbps: u32,
126}
127
128impl Av1Level {
129 #[must_use]
133 pub fn from_code(code: u8) -> Option<Self> {
134 AV1_LEVEL_TABLE.iter().find(|l| l.level == code).cloned()
135 }
136
137 #[must_use]
141 pub fn select_for(width: u32, height: u32, fps: f32, bitrate_kbps: u32) -> Option<Self> {
142 let pic_size = width as u64 * height as u64;
143 let display_rate = (pic_size as f64 * fps as f64).ceil() as u64;
144 let bitrate_mbps = (bitrate_kbps as f64 / 1000.0).ceil() as u32;
145
146 AV1_LEVEL_TABLE
147 .iter()
148 .find(|l| {
149 l.max_pic_size >= pic_size
150 && l.max_h_size >= width
151 && l.max_v_size >= height
152 && l.max_display_rate >= display_rate
153 && l.max_bitrate_mbps >= bitrate_mbps
154 })
155 .cloned()
156 }
157
158 #[must_use]
160 pub fn level_str(&self) -> String {
161 format!("{}.{}", self.level / 10, self.level % 10)
162 }
163}
164
165static AV1_LEVEL_TABLE: &[Av1Level] = &[
169 Av1Level {
170 level: 20,
171 max_pic_size: 147_456,
172 max_h_size: 2048,
173 max_v_size: 1152,
174 max_display_rate: 4_423_680,
175 max_bitrate_mbps: 1,
176 },
177 Av1Level {
178 level: 21,
179 max_pic_size: 278_784,
180 max_h_size: 2816,
181 max_v_size: 1584,
182 max_display_rate: 8_363_520,
183 max_bitrate_mbps: 2,
184 },
185 Av1Level {
186 level: 30,
187 max_pic_size: 665_856,
188 max_h_size: 4352,
189 max_v_size: 2448,
190 max_display_rate: 19_975_680,
191 max_bitrate_mbps: 5,
192 },
193 Av1Level {
194 level: 31,
195 max_pic_size: 1_065_024,
196 max_h_size: 5504,
197 max_v_size: 3096,
198 max_display_rate: 31_950_720,
199 max_bitrate_mbps: 10,
200 },
201 Av1Level {
202 level: 40,
203 max_pic_size: 2_359_296,
204 max_h_size: 6144,
205 max_v_size: 3456,
206 max_display_rate: 70_778_880,
207 max_bitrate_mbps: 12,
208 },
209 Av1Level {
210 level: 41,
211 max_pic_size: 2_359_296,
212 max_h_size: 6144,
213 max_v_size: 3456,
214 max_display_rate: 141_557_760,
215 max_bitrate_mbps: 20,
216 },
217 Av1Level {
218 level: 50,
219 max_pic_size: 8_912_896,
220 max_h_size: 8192,
221 max_v_size: 4352,
222 max_display_rate: 267_386_880,
223 max_bitrate_mbps: 30,
224 },
225 Av1Level {
226 level: 51,
227 max_pic_size: 8_912_896,
228 max_h_size: 8192,
229 max_v_size: 4352,
230 max_display_rate: 534_773_760,
231 max_bitrate_mbps: 40,
232 },
233 Av1Level {
234 level: 52,
235 max_pic_size: 8_912_896,
236 max_h_size: 8192,
237 max_v_size: 4352,
238 max_display_rate: 1_069_547_520,
239 max_bitrate_mbps: 60,
240 },
241 Av1Level {
242 level: 53,
243 max_pic_size: 8_912_896,
244 max_h_size: 8192,
245 max_v_size: 4352,
246 max_display_rate: 1_069_547_520,
247 max_bitrate_mbps: 60,
248 },
249 Av1Level {
250 level: 60,
251 max_pic_size: 35_651_584,
252 max_h_size: 16384,
253 max_v_size: 8704,
254 max_display_rate: 1_069_547_520,
255 max_bitrate_mbps: 100,
256 },
257 Av1Level {
258 level: 61,
259 max_pic_size: 35_651_584,
260 max_h_size: 16384,
261 max_v_size: 8704,
262 max_display_rate: 2_139_095_040,
263 max_bitrate_mbps: 160,
264 },
265 Av1Level {
266 level: 62,
267 max_pic_size: 35_651_584,
268 max_h_size: 16384,
269 max_v_size: 8704,
270 max_display_rate: 4_278_190_080,
271 max_bitrate_mbps: 240,
272 },
273 Av1Level {
274 level: 63,
275 max_pic_size: 35_651_584,
276 max_h_size: 16384,
277 max_v_size: 8704,
278 max_display_rate: 4_278_190_080,
279 max_bitrate_mbps: 240,
280 },
281];
282
283#[derive(Debug, Clone)]
294pub struct CodecConstraints {
295 pub codec: String,
297 pub profile: String,
299 pub level: String,
301 pub max_width: u32,
303 pub max_height: u32,
305 pub max_fps: f32,
307 pub max_bitrate_kbps: u32,
309 pub max_sample_rate: u32,
311 pub bit_depths: Vec<u8>,
313 pub color_spaces: Vec<String>,
315}
316
317impl CodecConstraints {
318 #[must_use]
322 pub fn av1_main() -> Self {
323 Self {
324 codec: "AV1".to_owned(),
325 profile: "Main".to_owned(),
326 level: "6.3".to_owned(),
327 max_width: 16384,
328 max_height: 8704,
329 max_fps: 300.0,
330 max_bitrate_kbps: 240_000,
331 max_sample_rate: 0,
332 bit_depths: vec![8, 10],
333 color_spaces: vec![
334 "BT.601".to_owned(),
335 "BT.709".to_owned(),
336 "BT.2020".to_owned(),
337 ],
338 }
339 }
340
341 #[must_use]
343 pub fn vp9_profile0() -> Self {
344 Self {
345 codec: "VP9".to_owned(),
346 profile: "Profile 0".to_owned(),
347 level: "6.2".to_owned(),
348 max_width: 16384,
349 max_height: 8704,
350 max_fps: 240.0,
351 max_bitrate_kbps: 180_000,
352 max_sample_rate: 0,
353 bit_depths: vec![8],
354 color_spaces: vec!["BT.601".to_owned(), "BT.709".to_owned()],
355 }
356 }
357
358 #[must_use]
360 pub fn opus_audio() -> Self {
361 Self {
362 codec: "Opus".to_owned(),
363 profile: "Audio".to_owned(),
364 level: "N/A".to_owned(),
365 max_width: 0,
366 max_height: 0,
367 max_fps: 0.0,
368 max_bitrate_kbps: 512,
369 max_sample_rate: 48_000,
370 bit_depths: vec![16, 24, 32],
371 color_spaces: Vec::new(),
372 }
373 }
374
375 #[must_use]
377 pub fn flac_standard() -> Self {
378 Self {
379 codec: "FLAC".to_owned(),
380 profile: "Standard".to_owned(),
381 level: "N/A".to_owned(),
382 max_width: 0,
383 max_height: 0,
384 max_fps: 0.0,
385 max_bitrate_kbps: 0, max_sample_rate: 655_350,
387 bit_depths: vec![8, 16, 20, 24, 32],
388 color_spaces: Vec::new(),
389 }
390 }
391
392 pub fn validate_video(
399 constraints: &Self,
400 width: u32,
401 height: u32,
402 fps: f32,
403 bitrate_kbps: u32,
404 ) -> Result<(), Vec<String>> {
405 let mut violations = Vec::new();
406
407 if constraints.max_width > 0 && width > constraints.max_width {
408 violations.push(format!(
409 "width {} exceeds maximum {} for {} {}",
410 width, constraints.max_width, constraints.codec, constraints.profile
411 ));
412 }
413
414 if constraints.max_height > 0 && height > constraints.max_height {
415 violations.push(format!(
416 "height {} exceeds maximum {} for {} {}",
417 height, constraints.max_height, constraints.codec, constraints.profile
418 ));
419 }
420
421 if constraints.max_fps > 0.0 && fps > constraints.max_fps {
422 violations.push(format!(
423 "fps {:.2} exceeds maximum {:.2} for {} {}",
424 fps, constraints.max_fps, constraints.codec, constraints.profile
425 ));
426 }
427
428 if constraints.max_bitrate_kbps > 0 && bitrate_kbps > constraints.max_bitrate_kbps {
429 violations.push(format!(
430 "bitrate {} kbps exceeds maximum {} kbps for {} {}",
431 bitrate_kbps, constraints.max_bitrate_kbps, constraints.codec, constraints.profile
432 ));
433 }
434
435 if violations.is_empty() {
436 Ok(())
437 } else {
438 Err(violations)
439 }
440 }
441
442 pub fn validate_audio(
447 constraints: &Self,
448 sample_rate: u32,
449 channels: u8,
450 bitrate_kbps: u32,
451 ) -> Result<(), Vec<String>> {
452 let mut violations = Vec::new();
453
454 if constraints.max_sample_rate > 0 && sample_rate > constraints.max_sample_rate {
455 violations.push(format!(
456 "sample rate {} Hz exceeds maximum {} Hz for {} {}",
457 sample_rate, constraints.max_sample_rate, constraints.codec, constraints.profile
458 ));
459 }
460
461 let max_channels: u8 = if constraints.codec == "FLAC" { 8 } else { 255 };
463 if channels > max_channels {
464 violations.push(format!(
465 "channel count {} exceeds maximum {} for {} {}",
466 channels, max_channels, constraints.codec, constraints.profile
467 ));
468 }
469
470 if constraints.max_bitrate_kbps > 0 && bitrate_kbps > constraints.max_bitrate_kbps {
471 violations.push(format!(
472 "bitrate {} kbps exceeds maximum {} kbps for {} {}",
473 bitrate_kbps, constraints.max_bitrate_kbps, constraints.codec, constraints.profile
474 ));
475 }
476
477 if violations.is_empty() {
478 Ok(())
479 } else {
480 Err(violations)
481 }
482 }
483}
484
485#[derive(Debug, Clone, PartialEq, Eq, Hash)]
491pub enum ChromaSubsampling {
492 Yuv420,
494 Yuv422,
496 Yuv444,
498 Yuv440,
500}
501
502impl std::fmt::Display for ChromaSubsampling {
503 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
504 match self {
505 Self::Yuv420 => write!(f, "4:2:0"),
506 Self::Yuv422 => write!(f, "4:2:2"),
507 Self::Yuv444 => write!(f, "4:4:4"),
508 Self::Yuv440 => write!(f, "4:4:0"),
509 }
510 }
511}
512
513#[derive(Debug, Clone, PartialEq, Eq)]
515pub struct Vp9ProfileDef {
516 pub profile: Vp9Profile,
518 pub bit_depths: Vec<u8>,
520 pub chroma_formats: Vec<ChromaSubsampling>,
522 pub max_width: u32,
524 pub max_height: u32,
526}
527
528impl Vp9ProfileDef {
529 pub fn profile0() -> Self {
531 Self {
532 profile: Vp9Profile::Profile0,
533 bit_depths: vec![8],
534 chroma_formats: vec![ChromaSubsampling::Yuv420],
535 max_width: 16384,
536 max_height: 16384,
537 }
538 }
539
540 pub fn profile1() -> Self {
542 Self {
543 profile: Vp9Profile::Profile1,
544 bit_depths: vec![8],
545 chroma_formats: vec![
546 ChromaSubsampling::Yuv422,
547 ChromaSubsampling::Yuv444,
548 ChromaSubsampling::Yuv440,
549 ],
550 max_width: 16384,
551 max_height: 16384,
552 }
553 }
554
555 pub fn profile2() -> Self {
557 Self {
558 profile: Vp9Profile::Profile2,
559 bit_depths: vec![10, 12],
560 chroma_formats: vec![ChromaSubsampling::Yuv420],
561 max_width: 16384,
562 max_height: 16384,
563 }
564 }
565
566 pub fn profile3() -> Self {
568 Self {
569 profile: Vp9Profile::Profile3,
570 bit_depths: vec![10, 12],
571 chroma_formats: vec![
572 ChromaSubsampling::Yuv422,
573 ChromaSubsampling::Yuv444,
574 ChromaSubsampling::Yuv440,
575 ],
576 max_width: 16384,
577 max_height: 16384,
578 }
579 }
580
581 pub fn validate(
583 &self,
584 width: u32,
585 height: u32,
586 bit_depth: u8,
587 chroma: &ChromaSubsampling,
588 ) -> Result<(), Vec<String>> {
589 let mut violations = Vec::new();
590
591 if !self.bit_depths.contains(&bit_depth) {
592 violations.push(format!(
593 "bit depth {} not allowed in VP9 {} (allowed: {:?})",
594 bit_depth, self.profile, self.bit_depths
595 ));
596 }
597
598 if !self.chroma_formats.contains(chroma) {
599 violations.push(format!(
600 "chroma format {} not allowed in VP9 {} (allowed: {:?})",
601 chroma,
602 self.profile,
603 self.chroma_formats
604 .iter()
605 .map(|c| c.to_string())
606 .collect::<Vec<_>>()
607 ));
608 }
609
610 if width > self.max_width {
611 violations.push(format!(
612 "width {} exceeds max {} for VP9 {}",
613 width, self.max_width, self.profile
614 ));
615 }
616
617 if height > self.max_height {
618 violations.push(format!(
619 "height {} exceeds max {} for VP9 {}",
620 height, self.max_height, self.profile
621 ));
622 }
623
624 if violations.is_empty() {
625 Ok(())
626 } else {
627 Err(violations)
628 }
629 }
630}
631
632#[derive(Debug, Clone, PartialEq, Eq)]
638pub struct Av1ProfileDef {
639 pub profile: Av1Profile,
641 pub bit_depths: Vec<u8>,
643 pub chroma_formats: Vec<ChromaSubsampling>,
645 pub mono_allowed: bool,
647}
648
649impl Av1ProfileDef {
650 pub fn main() -> Self {
652 Self {
653 profile: Av1Profile::Main,
654 bit_depths: vec![8, 10],
655 chroma_formats: vec![ChromaSubsampling::Yuv420],
656 mono_allowed: true,
657 }
658 }
659
660 pub fn high() -> Self {
662 Self {
663 profile: Av1Profile::High,
664 bit_depths: vec![8, 10],
665 chroma_formats: vec![ChromaSubsampling::Yuv420, ChromaSubsampling::Yuv444],
666 mono_allowed: true,
667 }
668 }
669
670 pub fn professional() -> Self {
672 Self {
673 profile: Av1Profile::Professional,
674 bit_depths: vec![8, 10, 12],
675 chroma_formats: vec![
676 ChromaSubsampling::Yuv420,
677 ChromaSubsampling::Yuv422,
678 ChromaSubsampling::Yuv444,
679 ],
680 mono_allowed: true,
681 }
682 }
683
684 pub fn validate(
686 &self,
687 bit_depth: u8,
688 chroma: &ChromaSubsampling,
689 is_monochrome: bool,
690 ) -> Result<(), Vec<String>> {
691 let mut violations = Vec::new();
692
693 if !self.bit_depths.contains(&bit_depth) {
694 violations.push(format!(
695 "bit depth {} not allowed in AV1 {} (allowed: {:?})",
696 bit_depth, self.profile, self.bit_depths
697 ));
698 }
699
700 if !is_monochrome && !self.chroma_formats.contains(chroma) {
701 violations.push(format!(
702 "chroma format {} not allowed in AV1 {}",
703 chroma, self.profile
704 ));
705 }
706
707 if is_monochrome && !self.mono_allowed {
708 violations.push(format!("monochrome not allowed in AV1 {}", self.profile));
709 }
710
711 if violations.is_empty() {
712 Ok(())
713 } else {
714 Err(violations)
715 }
716 }
717}
718
719#[derive(Debug, Clone, PartialEq, Eq)]
725pub struct CodecCapability {
726 pub codec: String,
728 pub profiles: Vec<String>,
730 pub bit_depths: Vec<u8>,
732 pub chroma_formats: Vec<ChromaSubsampling>,
734 pub max_resolution: (u32, u32),
736 pub max_bitrate_kbps: u32,
738}
739
740#[derive(Debug, Clone, PartialEq, Eq)]
742pub struct NegotiatedCapability {
743 pub codec: String,
745 pub profile: String,
747 pub bit_depths: Vec<u8>,
749 pub chroma_formats: Vec<ChromaSubsampling>,
751 pub max_resolution: (u32, u32),
753 pub max_bitrate_kbps: u32,
755}
756
757pub fn negotiate_capabilities(
761 encoder: &CodecCapability,
762 decoder: &CodecCapability,
763) -> Option<NegotiatedCapability> {
764 if encoder.codec != decoder.codec {
765 return None;
766 }
767
768 let common_profiles: Vec<String> = encoder
770 .profiles
771 .iter()
772 .filter(|p| decoder.profiles.contains(p))
773 .cloned()
774 .collect();
775
776 if common_profiles.is_empty() {
777 return None;
778 }
779
780 let common_depths: Vec<u8> = encoder
782 .bit_depths
783 .iter()
784 .filter(|d| decoder.bit_depths.contains(d))
785 .copied()
786 .collect();
787
788 if common_depths.is_empty() {
789 return None;
790 }
791
792 let common_chroma: Vec<ChromaSubsampling> = encoder
794 .chroma_formats
795 .iter()
796 .filter(|c| decoder.chroma_formats.contains(c))
797 .cloned()
798 .collect();
799
800 if common_chroma.is_empty() {
801 return None;
802 }
803
804 let best_profile = common_profiles.last().cloned().unwrap_or_default();
806
807 Some(NegotiatedCapability {
808 codec: encoder.codec.clone(),
809 profile: best_profile,
810 bit_depths: common_depths,
811 chroma_formats: common_chroma,
812 max_resolution: (
813 encoder.max_resolution.0.min(decoder.max_resolution.0),
814 encoder.max_resolution.1.min(decoder.max_resolution.1),
815 ),
816 max_bitrate_kbps: encoder.max_bitrate_kbps.min(decoder.max_bitrate_kbps),
817 })
818}
819
820pub fn is_profile_compatible(codec: &str, decoder_profile: &str, content_profile: &str) -> bool {
830 match codec {
831 "AV1" => {
832 let decoder_rank = av1_profile_rank(decoder_profile);
833 let content_rank = av1_profile_rank(content_profile);
834 decoder_rank >= content_rank
835 }
836 "VP9" => {
837 let decoder_rank = vp9_profile_rank(decoder_profile);
838 let content_rank = vp9_profile_rank(content_profile);
839 let same_chroma_class =
843 (decoder_rank % 2) == (content_rank % 2) || decoder_rank >= content_rank;
844 same_chroma_class && decoder_rank >= content_rank
845 }
846 _ => decoder_profile == content_profile,
847 }
848}
849
850fn av1_profile_rank(profile: &str) -> u8 {
851 match profile {
852 "Main" => 0,
853 "High" => 1,
854 "Professional" => 2,
855 _ => 0,
856 }
857}
858
859fn vp9_profile_rank(profile: &str) -> u8 {
860 match profile {
861 "Profile 0" => 0,
862 "Profile 1" => 1,
863 "Profile 2" => 2,
864 "Profile 3" => 3,
865 _ => 0,
866 }
867}
868
869#[derive(Debug, Clone, PartialEq, Eq, Hash)]
875pub enum HwDecoderTier {
876 Entry,
878 MidRange,
880 HighEnd,
882 Broadcast,
884}
885
886impl std::fmt::Display for HwDecoderTier {
887 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
888 match self {
889 Self::Entry => write!(f, "Entry"),
890 Self::MidRange => write!(f, "Mid-Range"),
891 Self::HighEnd => write!(f, "High-End"),
892 Self::Broadcast => write!(f, "Broadcast"),
893 }
894 }
895}
896
897#[derive(Debug, Clone)]
899pub struct HwDecodeCapability {
900 pub tier: HwDecoderTier,
902 pub av1_profiles: Vec<Av1Profile>,
904 pub vp9_profiles: Vec<Vp9Profile>,
906 pub max_bit_depth: u8,
908 pub max_resolution: (u32, u32),
910 pub hdr_supported: bool,
912}
913
914impl HwDecodeCapability {
915 pub fn entry() -> Self {
917 Self {
918 tier: HwDecoderTier::Entry,
919 av1_profiles: vec![Av1Profile::Main],
920 vp9_profiles: vec![Vp9Profile::Profile0],
921 max_bit_depth: 8,
922 max_resolution: (1920, 1080),
923 hdr_supported: false,
924 }
925 }
926
927 pub fn mid_range() -> Self {
929 Self {
930 tier: HwDecoderTier::MidRange,
931 av1_profiles: vec![Av1Profile::Main, Av1Profile::High],
932 vp9_profiles: vec![Vp9Profile::Profile0, Vp9Profile::Profile2],
933 max_bit_depth: 10,
934 max_resolution: (3840, 2160),
935 hdr_supported: true,
936 }
937 }
938
939 pub fn high_end() -> Self {
941 Self {
942 tier: HwDecoderTier::HighEnd,
943 av1_profiles: vec![Av1Profile::Main, Av1Profile::High, Av1Profile::Professional],
944 vp9_profiles: vec![
945 Vp9Profile::Profile0,
946 Vp9Profile::Profile1,
947 Vp9Profile::Profile2,
948 Vp9Profile::Profile3,
949 ],
950 max_bit_depth: 12,
951 max_resolution: (7680, 4320),
952 hdr_supported: true,
953 }
954 }
955
956 pub fn broadcast() -> Self {
958 Self {
959 tier: HwDecoderTier::Broadcast,
960 av1_profiles: vec![Av1Profile::Main, Av1Profile::High, Av1Profile::Professional],
961 vp9_profiles: vec![
962 Vp9Profile::Profile0,
963 Vp9Profile::Profile1,
964 Vp9Profile::Profile2,
965 Vp9Profile::Profile3,
966 ],
967 max_bit_depth: 12,
968 max_resolution: (16384, 8704),
969 hdr_supported: true,
970 }
971 }
972
973 pub fn can_decode_av1(&self, profile: &Av1Profile, bit_depth: u8) -> bool {
975 self.av1_profiles.contains(profile) && bit_depth <= self.max_bit_depth
976 }
977
978 pub fn can_decode_vp9(&self, profile: &Vp9Profile, bit_depth: u8) -> bool {
980 self.vp9_profiles.contains(profile) && bit_depth <= self.max_bit_depth
981 }
982
983 pub fn can_handle_resolution(&self, width: u32, height: u32) -> bool {
985 width <= self.max_resolution.0 && height <= self.max_resolution.1
986 }
987}
988
989pub fn required_hw_tier(
991 codec: &str,
992 profile: &str,
993 bit_depth: u8,
994 width: u32,
995 height: u32,
996) -> HwDecoderTier {
997 let tiers = [
998 HwDecodeCapability::entry(),
999 HwDecodeCapability::mid_range(),
1000 HwDecodeCapability::high_end(),
1001 HwDecodeCapability::broadcast(),
1002 ];
1003
1004 for cap in &tiers {
1005 let profile_ok = match codec {
1006 "AV1" => {
1007 let av1p = match profile {
1008 "Main" => Some(Av1Profile::Main),
1009 "High" => Some(Av1Profile::High),
1010 "Professional" => Some(Av1Profile::Professional),
1011 _ => None,
1012 };
1013 av1p.map_or(false, |p| cap.can_decode_av1(&p, bit_depth))
1014 }
1015 "VP9" => {
1016 let vp9p = match profile {
1017 "Profile 0" => Some(Vp9Profile::Profile0),
1018 "Profile 1" => Some(Vp9Profile::Profile1),
1019 "Profile 2" => Some(Vp9Profile::Profile2),
1020 "Profile 3" => Some(Vp9Profile::Profile3),
1021 _ => None,
1022 };
1023 vp9p.map_or(false, |p| cap.can_decode_vp9(&p, bit_depth))
1024 }
1025 _ => bit_depth <= cap.max_bit_depth,
1026 };
1027
1028 if profile_ok && cap.can_handle_resolution(width, height) {
1029 return cap.tier.clone();
1030 }
1031 }
1032
1033 HwDecoderTier::Broadcast
1034}
1035
1036#[cfg(test)]
1041mod tests {
1042 use super::*;
1043
1044 #[test]
1047 fn av1_level_from_code_known() {
1048 let l = Av1Level::from_code(40);
1049 assert!(l.is_some(), "level 4.0 (code 40) must exist");
1050 let l = l.expect("level should exist");
1051 assert_eq!(l.level, 40);
1052 assert_eq!(l.level_str(), "4.0");
1053 }
1054
1055 #[test]
1058 fn av1_level_from_code_unknown() {
1059 let l = Av1Level::from_code(99);
1060 assert!(l.is_none(), "code 99 should not correspond to any level");
1061 }
1062
1063 #[test]
1066 fn av1_level_select_1080p30() {
1067 let l = Av1Level::select_for(1920, 1080, 30.0, 10_000);
1068 assert!(l.is_some(), "1080p/30/10Mbps should resolve to some level");
1069 assert!(
1071 l.expect("level should be found").level >= 40,
1072 "should be at least level 4.0"
1073 );
1074 }
1075
1076 #[test]
1079 fn av1_level_select_exceeds_table() {
1080 let l = Av1Level::select_for(7680, 4320, 120.0, 1_000_000);
1082 assert!(
1083 l.is_none(),
1084 "extreme parameters should find no matching level"
1085 );
1086 }
1087
1088 #[test]
1091 fn av1_main_valid_1080p() {
1092 let c = CodecConstraints::av1_main();
1093 let r = CodecConstraints::validate_video(&c, 1920, 1080, 60.0, 10_000);
1094 assert!(
1095 r.is_ok(),
1096 "1080p/60/10Mbps should pass AV1 Main constraints"
1097 );
1098 }
1099
1100 #[test]
1103 fn av1_main_oversized_width_fails() {
1104 let c = CodecConstraints::av1_main();
1105 let r = CodecConstraints::validate_video(&c, 20_000, 1080, 30.0, 5_000);
1106 assert!(r.is_err());
1107 let v = r.err().expect("validation should return errors");
1108 assert!(v.iter().any(|s| s.contains("width")));
1109 }
1110
1111 #[test]
1114 fn vp9_profile0_8bit_accepted() {
1115 let c = CodecConstraints::vp9_profile0();
1116 assert!(c.bit_depths.contains(&8));
1117 }
1118
1119 #[test]
1122 fn vp9_profile0_no_10bit() {
1123 let c = CodecConstraints::vp9_profile0();
1124 assert!(!c.bit_depths.contains(&10));
1125 }
1126
1127 #[test]
1130 fn opus_audio_valid() {
1131 let c = CodecConstraints::opus_audio();
1132 let r = CodecConstraints::validate_audio(&c, 48_000, 2, 128);
1133 assert!(
1134 r.is_ok(),
1135 "48kHz/2ch/128kbps should pass Opus Audio constraints"
1136 );
1137 }
1138
1139 #[test]
1142 fn opus_audio_bitrate_exceeded() {
1143 let c = CodecConstraints::opus_audio();
1144 let r = CodecConstraints::validate_audio(&c, 48_000, 2, 1_000);
1145 assert!(r.is_err());
1146 let v = r.err().expect("validation should return errors");
1147 assert!(v.iter().any(|s| s.contains("bitrate")));
1148 }
1149
1150 #[test]
1153 fn flac_high_sample_rate_passes() {
1154 let c = CodecConstraints::flac_standard();
1155 let r = CodecConstraints::validate_audio(&c, 192_000, 2, 0);
1156 assert!(r.is_ok(), "192kHz FLAC should pass");
1157 }
1158
1159 #[test]
1162 fn validate_video_multiple_violations() {
1163 let c = CodecConstraints::vp9_profile0();
1164 let r = CodecConstraints::validate_video(&c, 99_999, 99_999, 9999.0, 99_999);
1166 assert!(r.is_err());
1167 let v = r.err().expect("validation should return errors");
1168 assert!(
1170 v.len() >= 3,
1171 "expected ≥3 violations, got {}: {:?}",
1172 v.len(),
1173 v
1174 );
1175 }
1176
1177 #[test]
1180 fn vp9_profile0_valid_420_8bit() {
1181 let p = Vp9ProfileDef::profile0();
1182 let r = p.validate(1920, 1080, 8, &ChromaSubsampling::Yuv420);
1183 assert!(r.is_ok());
1184 }
1185
1186 #[test]
1187 fn vp9_profile0_rejects_10bit() {
1188 let p = Vp9ProfileDef::profile0();
1189 let r = p.validate(1920, 1080, 10, &ChromaSubsampling::Yuv420);
1190 assert!(r.is_err());
1191 }
1192
1193 #[test]
1194 fn vp9_profile0_rejects_444() {
1195 let p = Vp9ProfileDef::profile0();
1196 let r = p.validate(1920, 1080, 8, &ChromaSubsampling::Yuv444);
1197 assert!(r.is_err());
1198 }
1199
1200 #[test]
1201 fn vp9_profile2_accepts_10bit_420() {
1202 let p = Vp9ProfileDef::profile2();
1203 let r = p.validate(3840, 2160, 10, &ChromaSubsampling::Yuv420);
1204 assert!(r.is_ok());
1205 }
1206
1207 #[test]
1208 fn vp9_profile3_accepts_12bit_444() {
1209 let p = Vp9ProfileDef::profile3();
1210 let r = p.validate(1920, 1080, 12, &ChromaSubsampling::Yuv444);
1211 assert!(r.is_ok());
1212 }
1213
1214 #[test]
1217 fn av1_main_accepts_420_10bit() {
1218 let p = Av1ProfileDef::main();
1219 let r = p.validate(10, &ChromaSubsampling::Yuv420, false);
1220 assert!(r.is_ok());
1221 }
1222
1223 #[test]
1224 fn av1_main_rejects_444() {
1225 let p = Av1ProfileDef::main();
1226 let r = p.validate(8, &ChromaSubsampling::Yuv444, false);
1227 assert!(r.is_err());
1228 }
1229
1230 #[test]
1231 fn av1_high_accepts_444() {
1232 let p = Av1ProfileDef::high();
1233 let r = p.validate(10, &ChromaSubsampling::Yuv444, false);
1234 assert!(r.is_ok());
1235 }
1236
1237 #[test]
1238 fn av1_professional_accepts_422_12bit() {
1239 let p = Av1ProfileDef::professional();
1240 let r = p.validate(12, &ChromaSubsampling::Yuv422, false);
1241 assert!(r.is_ok());
1242 }
1243
1244 #[test]
1245 fn av1_main_rejects_12bit() {
1246 let p = Av1ProfileDef::main();
1247 let r = p.validate(12, &ChromaSubsampling::Yuv420, false);
1248 assert!(r.is_err());
1249 }
1250
1251 #[test]
1254 fn negotiate_compatible() {
1255 let enc = CodecCapability {
1256 codec: "AV1".to_string(),
1257 profiles: vec!["Main".to_string(), "High".to_string()],
1258 bit_depths: vec![8, 10],
1259 chroma_formats: vec![ChromaSubsampling::Yuv420, ChromaSubsampling::Yuv444],
1260 max_resolution: (3840, 2160),
1261 max_bitrate_kbps: 50_000,
1262 };
1263 let dec = CodecCapability {
1264 codec: "AV1".to_string(),
1265 profiles: vec!["Main".to_string()],
1266 bit_depths: vec![8, 10],
1267 chroma_formats: vec![ChromaSubsampling::Yuv420],
1268 max_resolution: (1920, 1080),
1269 max_bitrate_kbps: 20_000,
1270 };
1271 let result = negotiate_capabilities(&enc, &dec);
1272 assert!(result.is_some());
1273 let neg = result.expect("negotiation should succeed");
1274 assert_eq!(neg.profile, "Main");
1275 assert_eq!(neg.max_resolution, (1920, 1080));
1276 assert_eq!(neg.max_bitrate_kbps, 20_000);
1277 }
1278
1279 #[test]
1280 fn negotiate_incompatible_codec() {
1281 let enc = CodecCapability {
1282 codec: "AV1".to_string(),
1283 profiles: vec!["Main".to_string()],
1284 bit_depths: vec![8],
1285 chroma_formats: vec![ChromaSubsampling::Yuv420],
1286 max_resolution: (1920, 1080),
1287 max_bitrate_kbps: 10_000,
1288 };
1289 let dec = CodecCapability {
1290 codec: "VP9".to_string(),
1291 profiles: vec!["Profile 0".to_string()],
1292 bit_depths: vec![8],
1293 chroma_formats: vec![ChromaSubsampling::Yuv420],
1294 max_resolution: (1920, 1080),
1295 max_bitrate_kbps: 10_000,
1296 };
1297 assert!(negotiate_capabilities(&enc, &dec).is_none());
1298 }
1299
1300 #[test]
1301 fn negotiate_no_common_depth() {
1302 let enc = CodecCapability {
1303 codec: "AV1".to_string(),
1304 profiles: vec!["Main".to_string()],
1305 bit_depths: vec![8],
1306 chroma_formats: vec![ChromaSubsampling::Yuv420],
1307 max_resolution: (1920, 1080),
1308 max_bitrate_kbps: 10_000,
1309 };
1310 let dec = CodecCapability {
1311 codec: "AV1".to_string(),
1312 profiles: vec!["Main".to_string()],
1313 bit_depths: vec![10],
1314 chroma_formats: vec![ChromaSubsampling::Yuv420],
1315 max_resolution: (1920, 1080),
1316 max_bitrate_kbps: 10_000,
1317 };
1318 assert!(negotiate_capabilities(&enc, &dec).is_none());
1319 }
1320
1321 #[test]
1324 fn av1_professional_decodes_main() {
1325 assert!(is_profile_compatible("AV1", "Professional", "Main"));
1326 }
1327
1328 #[test]
1329 fn av1_main_cannot_decode_high() {
1330 assert!(!is_profile_compatible("AV1", "Main", "High"));
1331 }
1332
1333 #[test]
1334 fn vp9_profile2_decodes_profile0() {
1335 assert!(is_profile_compatible("VP9", "Profile 2", "Profile 0"));
1336 }
1337
1338 #[test]
1339 fn vp9_profile0_cannot_decode_profile2() {
1340 assert!(!is_profile_compatible("VP9", "Profile 0", "Profile 2"));
1341 }
1342
1343 #[test]
1346 fn hw_entry_supports_av1_main_8bit() {
1347 let cap = HwDecodeCapability::entry();
1348 assert!(cap.can_decode_av1(&Av1Profile::Main, 8));
1349 assert!(!cap.can_decode_av1(&Av1Profile::High, 10));
1350 }
1351
1352 #[test]
1353 fn hw_mid_range_supports_4k() {
1354 let cap = HwDecodeCapability::mid_range();
1355 assert!(cap.can_handle_resolution(3840, 2160));
1356 assert!(!cap.can_handle_resolution(7680, 4320));
1357 }
1358
1359 #[test]
1360 fn hw_required_tier_1080p_main() {
1361 let tier = required_hw_tier("AV1", "Main", 8, 1920, 1080);
1362 assert_eq!(tier, HwDecoderTier::Entry);
1363 }
1364
1365 #[test]
1366 fn hw_required_tier_4k_hdr() {
1367 let tier = required_hw_tier("AV1", "Main", 10, 3840, 2160);
1368 assert_eq!(tier, HwDecoderTier::MidRange);
1369 }
1370
1371 #[test]
1372 fn hw_required_tier_8k_professional() {
1373 let tier = required_hw_tier("AV1", "Professional", 12, 7680, 4320);
1374 assert_eq!(tier, HwDecoderTier::HighEnd);
1375 }
1376}