Skip to main content

oximedia_codec/
codec_profile.rs

1//! Codec profile and level definitions, and constraint validation.
2//!
3//! This module provides:
4//! - Profile enumerations for AV1, VP9, and Opus.
5//! - The AV1 level table (levels 2.0 – 6.3) with decoded capability limits.
6//! - [`CodecConstraints`] — a codec-agnostic constraints bag and validation
7//!   helpers for both video and audio streams.
8//!
9//! # Example
10//!
11//! ```rust
12//! use oximedia_codec::codec_profile::{Av1Level, CodecConstraints};
13//!
14//! // Find the lowest AV1 level sufficient for 4K @ 60 fps / 20 Mbps.
15//! let level = Av1Level::select_for(3840, 2160, 60.0, 20_000);
16//! assert!(level.is_some());
17//!
18//! // Validate a stream against AV1 Main profile constraints.
19//! let c = CodecConstraints::av1_main();
20//! let result = CodecConstraints::validate_video(&c, 1920, 1080, 60.0, 10_000);
21//! assert!(result.is_ok());
22//! ```
23
24#![allow(clippy::cast_lossless)]
25#![allow(clippy::cast_precision_loss)]
26
27// ──────────────────────────────────────────────
28// Profile enumerations
29// ──────────────────────────────────────────────
30
31/// AV1 codec profile.
32///
33/// Defined in the AV1 specification, Section 6.4.1.
34#[derive(Debug, Clone, PartialEq, Eq, Hash)]
35pub enum Av1Profile {
36    /// Main profile — 4:2:0 chroma subsampling, 8-bit and 10-bit.
37    Main,
38    /// High profile — 4:4:4 chroma subsampling, 8-bit and 10-bit.
39    High,
40    /// Professional profile — 4:2:2, 4:4:4, up to 12-bit.
41    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/// VP9 codec profile.
55///
56/// Defined in the VP9 bitstream specification.
57#[derive(Debug, Clone, PartialEq, Eq, Hash)]
58pub enum Vp9Profile {
59    /// Profile 0 — 4:2:0, 8-bit.
60    Profile0,
61    /// Profile 1 — 4:2:2 / 4:4:4 / 4:4:0, 8-bit.
62    Profile1,
63    /// Profile 2 — 4:2:0, 10-bit or 12-bit.
64    Profile2,
65    /// Profile 3 — 4:2:2 / 4:4:4 / 4:4:0, 10-bit or 12-bit.
66    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/// Opus application / profile mode.
81///
82/// Controls the encoder's optimisation target.
83#[derive(Debug, Clone, PartialEq, Eq, Hash)]
84pub enum OpusProfile {
85    /// VOIP — optimised for speech, allows aggressive packet loss concealment.
86    Voip,
87    /// Audio — optimised for music and general audio, fullband.
88    Audio,
89    /// Restricted Low Delay — no look-ahead, lowest latency.
90    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// ──────────────────────────────────────────────
104// AV1 level table
105// ──────────────────────────────────────────────
106
107/// An entry in the AV1 level table.
108///
109/// Level numbers are encoded as `major * 10 + minor`; e.g., level 5.1 is
110/// encoded as `51`.  The table is derived from the AV1 specification,
111/// Annex A.
112#[derive(Debug, Clone, PartialEq, Eq)]
113pub struct Av1Level {
114    /// Level code: `major * 10 + minor`.  Level 4.0 → `40`, 6.3 → `63`.
115    pub level: u8,
116    /// Maximum picture size in pixels (width × height).
117    pub max_pic_size: u64,
118    /// Maximum horizontal picture dimension in pixels.
119    pub max_h_size: u32,
120    /// Maximum vertical picture dimension in pixels.
121    pub max_v_size: u32,
122    /// Maximum display sample rate in samples per second.
123    pub max_display_rate: u64,
124    /// Maximum bitrate in Mbps (megabits per second).
125    pub max_bitrate_mbps: u32,
126}
127
128impl Av1Level {
129    /// Return the AV1 level entry for the given `code` (e.g., `20` = level 2.0).
130    ///
131    /// Returns `None` if the code does not correspond to a defined level.
132    #[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    /// Find the lowest AV1 level sufficient for the specified video parameters.
138    ///
139    /// Returns `None` if no defined level can accommodate the request.
140    #[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    /// Return a human-readable level string such as `"4.1"`.
159    #[must_use]
160    pub fn level_str(&self) -> String {
161        format!("{}.{}", self.level / 10, self.level % 10)
162    }
163}
164
165/// AV1 level table derived from the AV1 specification, Annex A.
166///
167/// Levels 2.0 through 6.3 are listed in ascending order of capability.
168static 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// ──────────────────────────────────────────────
284// CodecConstraints
285// ──────────────────────────────────────────────
286
287/// A codec-agnostic constraints record used for profile/level validation.
288///
289/// Callers obtain pre-filled instances via the associated constructor functions
290/// (e.g., [`CodecConstraints::av1_main`]) and then call
291/// [`CodecConstraints::validate_video`] or [`CodecConstraints::validate_audio`]
292/// to collect any violations.
293#[derive(Debug, Clone)]
294pub struct CodecConstraints {
295    /// Codec name (e.g., `"AV1"`, `"VP9"`, `"Opus"`, `"FLAC"`).
296    pub codec: String,
297    /// Profile name (e.g., `"Main"`, `"Profile 0"`, `"Audio"`).
298    pub profile: String,
299    /// Level name (e.g., `"4.0"`, `"N/A"`).
300    pub level: String,
301    /// Maximum picture width in pixels (0 = unlimited).
302    pub max_width: u32,
303    /// Maximum picture height in pixels (0 = unlimited).
304    pub max_height: u32,
305    /// Maximum frames per second (0.0 = unlimited).
306    pub max_fps: f32,
307    /// Maximum video / audio bitrate in kbps (0 = unlimited).
308    pub max_bitrate_kbps: u32,
309    /// Maximum audio sample rate in Hz (0 = unlimited).
310    pub max_sample_rate: u32,
311    /// Supported bit depths (empty = all).
312    pub bit_depths: Vec<u8>,
313    /// Supported colour space names (empty = all).
314    pub color_spaces: Vec<String>,
315}
316
317impl CodecConstraints {
318    // ── Preset constructors ─────────────────────────────────────────────────
319
320    /// Constraints for AV1 Main profile (4:2:0, 8-bit or 10-bit).
321    #[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    /// Constraints for VP9 Profile 0 (4:2:0, 8-bit).
342    #[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    /// Constraints for Opus Audio profile.
359    #[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    /// Constraints for FLAC lossless audio.
376    #[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, // lossless — no fixed cap
386            max_sample_rate: 655_350,
387            bit_depths: vec![8, 16, 20, 24, 32],
388            color_spaces: Vec::new(),
389        }
390    }
391
392    // ── Validation helpers ──────────────────────────────────────────────────
393
394    /// Validate video stream parameters against `constraints`.
395    ///
396    /// Returns `Ok(())` when all constraints are satisfied, or
397    /// `Err(violations)` with a list of human-readable violation messages.
398    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    /// Validate audio stream parameters against `constraints`.
443    ///
444    /// Returns `Ok(())` when all constraints are satisfied, or
445    /// `Err(violations)` with a list of human-readable violation messages.
446    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        // Opus supports up to 255 channels; FLAC up to 8 in standard mode.
462        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// ──────────────────────────────────────────────
486// VP9 Profile Definitions
487// ──────────────────────────────────────────────
488
489/// Chroma subsampling format.
490#[derive(Debug, Clone, PartialEq, Eq, Hash)]
491pub enum ChromaSubsampling {
492    /// 4:2:0 — both chroma channels halved in both dimensions.
493    Yuv420,
494    /// 4:2:2 — chroma halved horizontally only.
495    Yuv422,
496    /// 4:4:4 — no chroma subsampling.
497    Yuv444,
498    /// 4:4:0 — chroma halved vertically only.
499    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/// Full VP9 profile definition with bit depth and chroma constraints.
514#[derive(Debug, Clone, PartialEq, Eq)]
515pub struct Vp9ProfileDef {
516    /// Profile enum variant.
517    pub profile: Vp9Profile,
518    /// Allowed bit depths.
519    pub bit_depths: Vec<u8>,
520    /// Allowed chroma subsampling formats.
521    pub chroma_formats: Vec<ChromaSubsampling>,
522    /// Maximum resolution width.
523    pub max_width: u32,
524    /// Maximum resolution height.
525    pub max_height: u32,
526}
527
528impl Vp9ProfileDef {
529    /// VP9 Profile 0: 4:2:0, 8-bit only.
530    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    /// VP9 Profile 1: 4:2:2, 4:4:4, 4:4:0, 8-bit.
541    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    /// VP9 Profile 2: 4:2:0, 10-bit or 12-bit.
556    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    /// VP9 Profile 3: 4:2:2, 4:4:4, 4:4:0, 10-bit or 12-bit.
567    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    /// Validate that a given stream matches this profile's constraints.
582    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// ──────────────────────────────────────────────
633// AV1 Profile Constraints Validation
634// ──────────────────────────────────────────────
635
636/// Full AV1 profile definition with chroma and bit depth constraints.
637#[derive(Debug, Clone, PartialEq, Eq)]
638pub struct Av1ProfileDef {
639    /// Profile enum variant.
640    pub profile: Av1Profile,
641    /// Allowed bit depths.
642    pub bit_depths: Vec<u8>,
643    /// Allowed chroma subsampling formats.
644    pub chroma_formats: Vec<ChromaSubsampling>,
645    /// Whether monochrome is allowed.
646    pub mono_allowed: bool,
647}
648
649impl Av1ProfileDef {
650    /// AV1 Main profile: 4:2:0, 8/10-bit, mono allowed.
651    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    /// AV1 High profile: 4:2:0, 4:4:4, 8/10-bit, mono allowed.
661    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    /// AV1 Professional profile: all formats, 8/10/12-bit.
671    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    /// Validate a stream against this profile's constraints.
685    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// ──────────────────────────────────────────────
720// Codec Capability Negotiation
721// ──────────────────────────────────────────────
722
723/// Codec capability descriptor for negotiation.
724#[derive(Debug, Clone, PartialEq, Eq)]
725pub struct CodecCapability {
726    /// Codec name (e.g., "AV1", "VP9").
727    pub codec: String,
728    /// Supported profiles.
729    pub profiles: Vec<String>,
730    /// Supported bit depths.
731    pub bit_depths: Vec<u8>,
732    /// Supported chroma formats.
733    pub chroma_formats: Vec<ChromaSubsampling>,
734    /// Maximum resolution (width, height).
735    pub max_resolution: (u32, u32),
736    /// Maximum bitrate in kbps.
737    pub max_bitrate_kbps: u32,
738}
739
740/// Result of capability negotiation.
741#[derive(Debug, Clone, PartialEq, Eq)]
742pub struct NegotiatedCapability {
743    /// Codec name.
744    pub codec: String,
745    /// Best common profile (highest tier both support).
746    pub profile: String,
747    /// Common bit depths.
748    pub bit_depths: Vec<u8>,
749    /// Common chroma formats.
750    pub chroma_formats: Vec<ChromaSubsampling>,
751    /// Minimum of both max resolutions.
752    pub max_resolution: (u32, u32),
753    /// Minimum of both max bitrates.
754    pub max_bitrate_kbps: u32,
755}
756
757/// Negotiate the best common capability between an encoder and decoder.
758///
759/// Returns `None` if the two sides have no compatible configuration.
760pub fn negotiate_capabilities(
761    encoder: &CodecCapability,
762    decoder: &CodecCapability,
763) -> Option<NegotiatedCapability> {
764    if encoder.codec != decoder.codec {
765        return None;
766    }
767
768    // Find common profiles.
769    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    // Find common bit depths.
781    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    // Find common chroma formats.
793    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    // Use the last (highest) common profile.
805    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
820// ──────────────────────────────────────────────
821// Profile Compatibility Matrix
822// ──────────────────────────────────────────────
823
824/// Check if a decoder supporting `decoder_profile` can handle content
825/// encoded with `content_profile`.
826///
827/// AV1 profiles form a strict hierarchy: Professional > High > Main.
828/// VP9 profiles: Profile 2/3 can decode 0/1 respectively (same chroma, higher bit depth).
829pub 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            // VP9 profiles are only compatible within the same chroma class:
840            // Profile 0 ↔ Profile 2 (both 4:2:0)
841            // Profile 1 ↔ Profile 3 (both 4:2:2/4:4:4)
842            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// ──────────────────────────────────────────────
870// Hardware Decode Profile Detection
871// ──────────────────────────────────────────────
872
873/// Hardware decoder tier.
874#[derive(Debug, Clone, PartialEq, Eq, Hash)]
875pub enum HwDecoderTier {
876    /// Entry-level (mobile, embedded): AV1 Main 8-bit, VP9 Profile 0.
877    Entry,
878    /// Mid-range (laptop, desktop): AV1 Main/High 10-bit, VP9 Profile 0/2.
879    MidRange,
880    /// High-end (workstation, pro): AV1 all profiles, VP9 all profiles.
881    HighEnd,
882    /// Broadcast / professional (studio hardware).
883    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/// Hardware decode capability for a specific decoder tier.
898#[derive(Debug, Clone)]
899pub struct HwDecodeCapability {
900    /// Tier of the hardware.
901    pub tier: HwDecoderTier,
902    /// Supported AV1 profiles.
903    pub av1_profiles: Vec<Av1Profile>,
904    /// Supported VP9 profiles.
905    pub vp9_profiles: Vec<Vp9Profile>,
906    /// Maximum supported bit depth.
907    pub max_bit_depth: u8,
908    /// Maximum resolution (width, height).
909    pub max_resolution: (u32, u32),
910    /// Whether HDR / HLG is supported.
911    pub hdr_supported: bool,
912}
913
914impl HwDecodeCapability {
915    /// Entry-level hardware capability.
916    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    /// Mid-range hardware capability.
928    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    /// High-end hardware capability.
940    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    /// Broadcast / professional hardware capability.
957    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    /// Check if this hardware can decode the given AV1 profile and bit depth.
974    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    /// Check if this hardware can decode the given VP9 profile and bit depth.
979    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    /// Check if resolution is within hardware limits.
984    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
989/// Determine the minimum hardware tier required for a given stream.
990pub 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// ──────────────────────────────────────────────
1037// Tests
1038// ──────────────────────────────────────────────
1039
1040#[cfg(test)]
1041mod tests {
1042    use super::*;
1043
1044    // ── 1. Av1Level::from_code: known level ──────────────────────────────────
1045
1046    #[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    // ── 2. Av1Level::from_code: unknown level ────────────────────────────────
1056
1057    #[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    // ── 3. Av1Level::select_for: 1080p/30 should fit in level 4.0 ───────────
1064
1065    #[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        // Level 4.0 has max_bitrate_mbps=12, which is ≥ 10 Mbps.
1070        assert!(
1071            l.expect("level should be found").level >= 40,
1072            "should be at least level 4.0"
1073        );
1074    }
1075
1076    // ── 4. Av1Level::select_for: 8K/120 should exceed level 6.3 ────────────
1077
1078    #[test]
1079    fn av1_level_select_exceeds_table() {
1080        // 8K at 120fps with 1Gbps — no defined level covers this.
1081        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    // ── 5. CodecConstraints::av1_main: valid 1080p passes ───────────────────
1089
1090    #[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    // ── 6. CodecConstraints::av1_main: oversized width fails ─────────────────
1101
1102    #[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    // ── 7. CodecConstraints::vp9_profile0: 8-bit accepted ───────────────────
1112
1113    #[test]
1114    fn vp9_profile0_8bit_accepted() {
1115        let c = CodecConstraints::vp9_profile0();
1116        assert!(c.bit_depths.contains(&8));
1117    }
1118
1119    // ── 8. CodecConstraints::vp9_profile0: 10-bit not in bit_depths ─────────
1120
1121    #[test]
1122    fn vp9_profile0_no_10bit() {
1123        let c = CodecConstraints::vp9_profile0();
1124        assert!(!c.bit_depths.contains(&10));
1125    }
1126
1127    // ── 9. CodecConstraints::opus_audio: valid parameters pass ──────────────
1128
1129    #[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    // ── 10. CodecConstraints::opus_audio: bitrate over limit fails ───────────
1140
1141    #[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    // ── 11. CodecConstraints::flac_standard: high sample rate passes ─────────
1151
1152    #[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    // ── 12. validate_video returns all violations at once ────────────────────
1160
1161    #[test]
1162    fn validate_video_multiple_violations() {
1163        let c = CodecConstraints::vp9_profile0();
1164        // Width + height + fps all exceed VP9 Profile 0 limits.
1165        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        // Expect at least three violations (width, height, fps or bitrate).
1169        assert!(
1170            v.len() >= 3,
1171            "expected ≥3 violations, got {}: {:?}",
1172            v.len(),
1173            v
1174        );
1175    }
1176
1177    // ── VP9 Profile Definitions ─────────────────────────────────────────────
1178
1179    #[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    // ── AV1 Profile Constraints Validation ───────────────────────────────────
1215
1216    #[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    // ── Capability Negotiation ───────────────────────────────────────────────
1252
1253    #[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    // ── Profile Compatibility Matrix ─────────────────────────────────────────
1322
1323    #[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    // ── Hardware Decode Profile Detection ────────────────────────────────────
1344
1345    #[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}