Skip to main content

oximedia_transcode/
codec_profile.rs

1//! Codec profile definitions and selector utilities.
2//!
3//! Provides `CodecLevel`, `CodecProfile`, and `CodecProfileSelector`
4//! for choosing the best encoding profile for a given resolution.
5
6#![allow(dead_code)]
7
8/// H.264/HEVC codec conformance level.
9#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
10pub enum CodecLevel {
11    /// Level 3.0 – up to 720p30.
12    Level30,
13    /// Level 3.1 – up to 720p60 / 1080p30.
14    Level31,
15    /// Level 4.0 – up to 1080p30 high-quality.
16    Level40,
17    /// Level 4.1 – up to 1080p60.
18    Level41,
19    /// Level 5.0 – up to 2160p30.
20    Level50,
21    /// Level 5.1 – up to 2160p60.
22    Level51,
23    /// Level 5.2 – 2160p120 / 8K30.
24    Level52,
25}
26
27impl CodecLevel {
28    /// Returns the level as a floating-point number (e.g. 4.1).
29    #[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    /// Returns the maximum pixel count this level supports per frame.
44    #[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/// An encoding profile combining codec name, level, and capability flags.
59#[derive(Debug, Clone)]
60pub struct CodecProfile {
61    /// Codec identifier (e.g. "h264", "hevc", "av1").
62    pub codec: String,
63    /// Conformance level.
64    pub level: CodecLevel,
65    /// Maximum bitrate in Megabits per second.
66    max_bitrate_mbps_val: f64,
67    /// Whether hardware encoders are typically available for this profile.
68    hw_encodable: bool,
69    /// Whether this profile supports 10-bit colour.
70    supports_10bit: bool,
71}
72
73impl CodecProfile {
74    /// Creates a new codec profile.
75    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    /// Enables 10-bit colour support for this profile.
91    #[must_use]
92    pub fn with_10bit(mut self) -> Self {
93        self.supports_10bit = true;
94        self
95    }
96
97    /// Returns the maximum bitrate in Megabits per second.
98    #[must_use]
99    pub fn max_bitrate_mbps(&self) -> f64 {
100        self.max_bitrate_mbps_val
101    }
102
103    /// Returns `true` if this profile is typically hardware-encodable.
104    #[must_use]
105    pub fn is_hardware_encodable(&self) -> bool {
106        self.hw_encodable
107    }
108
109    /// Returns `true` if this profile supports 10-bit depth.
110    #[must_use]
111    pub fn supports_10bit(&self) -> bool {
112        self.supports_10bit
113    }
114
115    /// Returns the maximum pixel count for this profile's level.
116    #[must_use]
117    pub fn max_pixels(&self) -> u64 {
118        self.level.max_pixels()
119    }
120
121    /// Returns `true` if this profile can encode the given resolution.
122    #[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/// Selects an appropriate codec profile based on resolution requirements.
130#[derive(Debug, Default)]
131pub struct CodecProfileSelector {
132    profiles: Vec<CodecProfile>,
133}
134
135impl CodecProfileSelector {
136    /// Creates an empty selector.
137    #[must_use]
138    pub fn new() -> Self {
139        Self::default()
140    }
141
142    /// Creates a selector pre-loaded with common H.264 profiles.
143    #[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    /// Creates a selector pre-loaded with common HEVC profiles.
153    #[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    /// Adds a profile to the selector.
163    pub fn add(&mut self, profile: CodecProfile) {
164        self.profiles.push(profile);
165    }
166
167    /// Selects the lowest-level (most compatible) profile that supports the
168    /// given resolution. Returns `None` if no profile is sufficient.
169    #[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    /// Returns all profiles that support the given resolution.
178    #[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    /// Returns the number of profiles registered.
187    #[must_use]
188    pub fn profile_count(&self) -> usize {
189        self.profiles.len()
190    }
191}
192
193#[cfg(test)]
194mod tests {
195    use super::*;
196
197    #[test]
198    fn test_codec_level_ordering() {
199        assert!(CodecLevel::Level30 < CodecLevel::Level31);
200        assert!(CodecLevel::Level41 < CodecLevel::Level50);
201        assert!(CodecLevel::Level51 < CodecLevel::Level52);
202    }
203
204    #[test]
205    fn test_codec_level_as_f32() {
206        let v = CodecLevel::Level41.as_f32();
207        assert!((v - 4.1_f32).abs() < 0.001);
208    }
209
210    #[test]
211    fn test_codec_level_max_pixels_1080p() {
212        assert_eq!(CodecLevel::Level41.max_pixels(), 1_920 * 1_080);
213    }
214
215    #[test]
216    fn test_codec_level_max_pixels_4k() {
217        assert_eq!(CodecLevel::Level51.max_pixels(), 3_840 * 2_160);
218    }
219
220    #[test]
221    fn test_profile_max_bitrate() {
222        let p = CodecProfile::new("h264", CodecLevel::Level41, 50.0, true);
223        assert!((p.max_bitrate_mbps() - 50.0).abs() < f64::EPSILON);
224    }
225
226    #[test]
227    fn test_profile_hw_encodable() {
228        let p = CodecProfile::new("h264", CodecLevel::Level41, 50.0, true);
229        assert!(p.is_hardware_encodable());
230        let p2 = CodecProfile::new("av1", CodecLevel::Level51, 100.0, false);
231        assert!(!p2.is_hardware_encodable());
232    }
233
234    #[test]
235    fn test_profile_10bit_default_false() {
236        let p = CodecProfile::new("h264", CodecLevel::Level41, 50.0, true);
237        assert!(!p.supports_10bit());
238    }
239
240    #[test]
241    fn test_profile_with_10bit() {
242        let p = CodecProfile::new("hevc", CodecLevel::Level51, 160.0, true).with_10bit();
243        assert!(p.supports_10bit());
244    }
245
246    #[test]
247    fn test_profile_supports_resolution_1080p() {
248        let p = CodecProfile::new("h264", CodecLevel::Level41, 50.0, true);
249        assert!(p.supports_resolution(1920, 1080));
250        assert!(!p.supports_resolution(3840, 2160));
251    }
252
253    #[test]
254    fn test_selector_h264_defaults_count() {
255        let sel = CodecProfileSelector::with_h264_defaults();
256        assert_eq!(sel.profile_count(), 3);
257    }
258
259    #[test]
260    fn test_selector_select_for_1080p_returns_lowest_level() {
261        let sel = CodecProfileSelector::with_h264_defaults();
262        let p = sel
263            .select_for_resolution(1920, 1080)
264            .expect("should succeed in test");
265        // Level31 max_pixels is 1280*720, so Level41 should be selected
266        assert_eq!(p.level, CodecLevel::Level41);
267    }
268
269    #[test]
270    fn test_selector_select_for_720p() {
271        let sel = CodecProfileSelector::with_h264_defaults();
272        let p = sel
273            .select_for_resolution(1280, 720)
274            .expect("should succeed in test");
275        assert_eq!(p.level, CodecLevel::Level31);
276    }
277
278    #[test]
279    fn test_selector_select_for_8k_returns_none_h264() {
280        let sel = CodecProfileSelector::with_h264_defaults();
281        // 8K exceeds all H.264 profiles in the default set
282        assert!(sel.select_for_resolution(7680, 4320).is_none());
283    }
284
285    #[test]
286    fn test_selector_hevc_supports_4k() {
287        let sel = CodecProfileSelector::with_hevc_defaults();
288        let p = sel
289            .select_for_resolution(3840, 2160)
290            .expect("should succeed in test");
291        assert_eq!(p.codec, "hevc");
292        assert!(p.supports_10bit());
293    }
294}