Skip to main content

oximedia_transcode/
codec_mapping.rs

1//! Container-to-codec mapping and codec compatibility utilities.
2
3use serde::{Deserialize, Serialize};
4use std::collections::HashMap;
5
6/// A container format identifier (e.g., `"mp4"`, `"webm"`, `"mkv"`).
7pub type ContainerFormat = String;
8
9/// A codec identifier (e.g., `"h264"`, `"vp9"`, `"opus"`).
10pub type CodecId = String;
11
12/// Category of a codec.
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
14pub enum CodecKind {
15    /// Video codec.
16    Video,
17    /// Audio codec.
18    Audio,
19    /// Subtitle codec.
20    Subtitle,
21}
22
23/// Maps container formats to supported video and audio codecs.
24///
25/// # Example
26///
27/// ```
28/// use oximedia_transcode::codec_mapping::{CodecMapping, CodecKind};
29///
30/// let mapping = CodecMapping::default();
31/// let video_codecs = mapping.supported_codecs("mp4", CodecKind::Video);
32/// assert!(video_codecs.contains(&"h264".to_string()));
33/// ```
34#[derive(Debug, Clone)]
35pub struct CodecMapping {
36    /// video codecs per container.
37    video: HashMap<ContainerFormat, Vec<CodecId>>,
38    /// audio codecs per container.
39    audio: HashMap<ContainerFormat, Vec<CodecId>>,
40    /// subtitle codecs per container.
41    subtitle: HashMap<ContainerFormat, Vec<CodecId>>,
42}
43
44impl Default for CodecMapping {
45    fn default() -> Self {
46        Self::new()
47    }
48}
49
50impl CodecMapping {
51    /// Creates a `CodecMapping` populated with standard container/codec combinations.
52    #[must_use]
53    pub fn new() -> Self {
54        let mut video: HashMap<ContainerFormat, Vec<CodecId>> = HashMap::new();
55        let mut audio: HashMap<ContainerFormat, Vec<CodecId>> = HashMap::new();
56        let mut subtitle: HashMap<ContainerFormat, Vec<CodecId>> = HashMap::new();
57
58        // MP4
59        video.insert(
60            "mp4".into(),
61            vec![
62                "h264".into(),
63                "h265".into(),
64                "hevc".into(),
65                "av1".into(),
66                "mpeg4".into(),
67            ],
68        );
69        audio.insert(
70            "mp4".into(),
71            vec![
72                "aac".into(),
73                "mp3".into(),
74                "ac3".into(),
75                "eac3".into(),
76                "flac".into(),
77            ],
78        );
79        subtitle.insert("mp4".into(), vec!["mov_text".into(), "dvdsub".into()]);
80
81        // WebM
82        video.insert(
83            "webm".into(),
84            vec!["vp8".into(), "vp9".into(), "av1".into()],
85        );
86        audio.insert("webm".into(), vec!["opus".into(), "vorbis".into()]);
87        subtitle.insert("webm".into(), vec!["webvtt".into()]);
88
89        // MKV / Matroska
90        video.insert(
91            "mkv".into(),
92            vec![
93                "h264".into(),
94                "h265".into(),
95                "hevc".into(),
96                "av1".into(),
97                "vp9".into(),
98                "vp8".into(),
99                "mpeg4".into(),
100                "theora".into(),
101                "ffv1".into(),
102            ],
103        );
104        audio.insert(
105            "mkv".into(),
106            vec![
107                "aac".into(),
108                "mp3".into(),
109                "opus".into(),
110                "vorbis".into(),
111                "flac".into(),
112                "ac3".into(),
113                "truehd".into(),
114                "dts".into(),
115                "pcm_s16le".into(),
116            ],
117        );
118        subtitle.insert(
119            "mkv".into(),
120            vec![
121                "srt".into(),
122                "ass".into(),
123                "ssa".into(),
124                "webvtt".into(),
125                "dvdsub".into(),
126            ],
127        );
128
129        // MOV
130        video.insert(
131            "mov".into(),
132            vec![
133                "h264".into(),
134                "h265".into(),
135                "prores".into(),
136                "dnxhd".into(),
137                "av1".into(),
138            ],
139        );
140        audio.insert(
141            "mov".into(),
142            vec![
143                "aac".into(),
144                "pcm_s16le".into(),
145                "pcm_s24le".into(),
146                "mp3".into(),
147            ],
148        );
149        subtitle.insert("mov".into(), vec!["mov_text".into()]);
150
151        // AVI
152        video.insert(
153            "avi".into(),
154            vec![
155                "h264".into(),
156                "mpeg4".into(),
157                "xvid".into(),
158                "divx".into(),
159                "wmv2".into(),
160            ],
161        );
162        audio.insert(
163            "avi".into(),
164            vec!["mp3".into(), "aac".into(), "pcm_s16le".into(), "ac3".into()],
165        );
166        subtitle.insert("avi".into(), vec![]);
167
168        // TS (MPEG-TS)
169        video.insert(
170            "ts".into(),
171            vec!["h264".into(), "h265".into(), "mpeg2video".into()],
172        );
173        audio.insert("ts".into(), vec!["aac".into(), "mp3".into(), "ac3".into()]);
174        subtitle.insert("ts".into(), vec!["dvbsub".into()]);
175
176        // FLV
177        video.insert("flv".into(), vec!["h264".into(), "flv1".into()]);
178        audio.insert("flv".into(), vec!["aac".into(), "mp3".into()]);
179        subtitle.insert("flv".into(), vec![]);
180
181        // OGG
182        video.insert("ogg".into(), vec!["theora".into()]);
183        audio.insert(
184            "ogg".into(),
185            vec!["vorbis".into(), "opus".into(), "flac".into()],
186        );
187        subtitle.insert("ogg".into(), vec![]);
188
189        Self {
190            video,
191            audio,
192            subtitle,
193        }
194    }
195
196    /// Returns supported codecs for the given container and kind.
197    ///
198    /// Returns an empty `Vec` if the container is unknown.
199    #[must_use]
200    pub fn supported_codecs(&self, container: &str, kind: CodecKind) -> Vec<CodecId> {
201        let map = match kind {
202            CodecKind::Video => &self.video,
203            CodecKind::Audio => &self.audio,
204            CodecKind::Subtitle => &self.subtitle,
205        };
206        map.get(container).cloned().unwrap_or_default()
207    }
208
209    /// Returns `true` when `codec` is compatible with `container` for the given `kind`.
210    #[must_use]
211    pub fn is_compatible(&self, container: &str, codec: &str, kind: CodecKind) -> bool {
212        self.supported_codecs(container, kind)
213            .contains(&codec.to_string())
214    }
215
216    /// Returns all container formats known to this mapping.
217    #[must_use]
218    pub fn known_containers(&self) -> Vec<ContainerFormat> {
219        let mut containers: std::collections::HashSet<&str> = std::collections::HashSet::new();
220        for k in self.video.keys() {
221            containers.insert(k.as_str());
222        }
223        for k in self.audio.keys() {
224            containers.insert(k.as_str());
225        }
226        let mut result: Vec<ContainerFormat> = containers.into_iter().map(String::from).collect();
227        result.sort_unstable();
228        result
229    }
230
231    /// Finds containers that support both the given video and audio codecs.
232    #[must_use]
233    pub fn find_compatible_containers(
234        &self,
235        video_codec: &str,
236        audio_codec: &str,
237    ) -> Vec<ContainerFormat> {
238        let mut result = Vec::new();
239        for container in self.known_containers() {
240            let has_video = self.is_compatible(&container, video_codec, CodecKind::Video);
241            let has_audio = self.is_compatible(&container, audio_codec, CodecKind::Audio);
242            if has_video && has_audio {
243                result.push(container);
244            }
245        }
246        result
247    }
248
249    /// Adds a custom codec mapping for a container.
250    pub fn add_codec(
251        &mut self,
252        container: impl Into<ContainerFormat>,
253        codec: impl Into<CodecId>,
254        kind: CodecKind,
255    ) {
256        let map = match kind {
257            CodecKind::Video => &mut self.video,
258            CodecKind::Audio => &mut self.audio,
259            CodecKind::Subtitle => &mut self.subtitle,
260        };
261        map.entry(container.into()).or_default().push(codec.into());
262    }
263}
264
265#[cfg(test)]
266mod tests {
267    use super::*;
268
269    #[test]
270    fn test_mp4_video_codecs() {
271        let mapping = CodecMapping::default();
272        let codecs = mapping.supported_codecs("mp4", CodecKind::Video);
273        assert!(codecs.contains(&"h264".to_string()));
274        assert!(codecs.contains(&"h265".to_string()));
275    }
276
277    #[test]
278    fn test_webm_audio_codecs() {
279        let mapping = CodecMapping::default();
280        let codecs = mapping.supported_codecs("webm", CodecKind::Audio);
281        assert!(codecs.contains(&"opus".to_string()));
282        assert!(codecs.contains(&"vorbis".to_string()));
283        assert!(!codecs.contains(&"aac".to_string()));
284    }
285
286    #[test]
287    fn test_is_compatible() {
288        let mapping = CodecMapping::default();
289        assert!(mapping.is_compatible("mp4", "h264", CodecKind::Video));
290        assert!(!mapping.is_compatible("webm", "h264", CodecKind::Video));
291        assert!(mapping.is_compatible("mkv", "opus", CodecKind::Audio));
292    }
293
294    #[test]
295    fn test_unknown_container_returns_empty() {
296        let mapping = CodecMapping::default();
297        let codecs = mapping.supported_codecs("unknown_fmt", CodecKind::Video);
298        assert!(codecs.is_empty());
299    }
300
301    #[test]
302    fn test_known_containers_non_empty() {
303        let mapping = CodecMapping::default();
304        let containers = mapping.known_containers();
305        assert!(!containers.is_empty());
306        assert!(containers.contains(&"mp4".to_string()));
307        assert!(containers.contains(&"webm".to_string()));
308        assert!(containers.contains(&"mkv".to_string()));
309    }
310
311    #[test]
312    fn test_find_compatible_containers() {
313        let mapping = CodecMapping::default();
314        let containers = mapping.find_compatible_containers("vp9", "opus");
315        assert!(containers.contains(&"webm".to_string()));
316        assert!(containers.contains(&"mkv".to_string()));
317        assert!(!containers.contains(&"mp4".to_string()));
318    }
319
320    #[test]
321    fn test_add_custom_codec() {
322        let mut mapping = CodecMapping::new();
323        mapping.add_codec("mp4", "custom_codec", CodecKind::Video);
324        assert!(mapping.is_compatible("mp4", "custom_codec", CodecKind::Video));
325    }
326
327    #[test]
328    fn test_subtitle_codecs_mkv() {
329        let mapping = CodecMapping::default();
330        let subs = mapping.supported_codecs("mkv", CodecKind::Subtitle);
331        assert!(subs.contains(&"ass".to_string()));
332        assert!(subs.contains(&"srt".to_string()));
333    }
334}