Skip to main content

oxideav_core/
stream.rs

1//! Stream metadata shared between containers and codecs.
2
3use crate::format::{MediaType, PixelFormat, SampleFormat};
4use crate::rational::Rational;
5use crate::time::TimeBase;
6
7/// A stable identifier for a codec. Codec crates register a `CodecId` so the
8/// codec registry can look them up by name.
9#[derive(Clone, Debug, PartialEq, Eq, Hash)]
10pub struct CodecId(pub String);
11
12impl CodecId {
13    pub fn new(s: impl Into<String>) -> Self {
14        Self(s.into())
15    }
16
17    pub fn as_str(&self) -> &str {
18        &self.0
19    }
20}
21
22impl From<&str> for CodecId {
23    fn from(s: &str) -> Self {
24        Self(s.to_owned())
25    }
26}
27
28impl std::fmt::Display for CodecId {
29    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
30        write!(f, "{}", self.0)
31    }
32}
33
34/// A codec identifier scoped to a container format — the thing a
35/// demuxer reads out of the file to name a codec. Resolved to a
36/// [`CodecId`] by the codec registry.
37///
38/// Centralising these in the registry (instead of each container
39/// hand-rolling its own FourCC → CodecId table) lets:
40///
41/// * a codec crate declare its own tag claims in `register()`, keeping
42///   ownership co-located with the decoder;
43/// * multiple codecs claim the same tag with priority ordering;
44/// * optional per-claim probes disambiguate the tag-collision cases
45///   that happen everywhere in the wild (DIV3 that's actually MPEG-4
46///   Part 2, XVID that's actually MS-MPEG4v3, audio wFormatTag=0x0055
47///   that could be MP3 or — very rarely — something else, etc.).
48#[derive(Clone, Debug, PartialEq, Eq, Hash)]
49pub enum CodecTag {
50    /// Four-character code used by AVI's `bmih.biCompression`, MP4 /
51    /// QuickTime sample-entry type, Matroska V_/A_ tags built around
52    /// FourCC, and many others. Always stored with alphabetic bytes
53    /// upper-cased so lookups are case-insensitive; non-alphabetic
54    /// bytes are preserved as-is.
55    Fourcc([u8; 4]),
56
57    /// AVI / WAV `WAVEFORMATEX::wFormatTag` (e.g. 0x0001 = PCM,
58    /// 0x0055 = MP3, 0x00FF = "raw" AAC, 0x1610 = AAC ADTS).
59    WaveFormat(u16),
60
61    /// MP4 ObjectTypeIndication (ISO/IEC 14496-1 Table 5 / the values
62    /// in an MP4 `esds` `DecoderConfigDescriptor`). e.g. 0x40 = MPEG-4
63    /// AAC, 0x20 = MPEG-4 Visual, 0x69 = MP3.
64    Mp4ObjectType(u8),
65
66    /// Matroska `CodecID` element (full string, e.g.
67    /// `"V_MPEG4/ISO/AVC"`, `"A_AAC"`, `"A_VORBIS"`).
68    Matroska(String),
69}
70
71impl CodecTag {
72    /// Build a FourCC tag, upper-casing alphabetic bytes.
73    pub fn fourcc(raw: &[u8; 4]) -> Self {
74        let mut out = [0u8; 4];
75        for i in 0..4 {
76            out[i] = raw[i].to_ascii_uppercase();
77        }
78        Self::Fourcc(out)
79    }
80
81    pub fn wave_format(tag: u16) -> Self {
82        Self::WaveFormat(tag)
83    }
84
85    pub fn mp4_object_type(oti: u8) -> Self {
86        Self::Mp4ObjectType(oti)
87    }
88
89    pub fn matroska(id: impl Into<String>) -> Self {
90        Self::Matroska(id.into())
91    }
92}
93
94impl std::fmt::Display for CodecTag {
95    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
96        match self {
97            Self::Fourcc(fcc) => {
98                // Print as bytes when ASCII-printable, else as hex.
99                if fcc.iter().all(|b| b.is_ascii_graphic() || *b == b' ') {
100                    write!(f, "fourcc({})", std::str::from_utf8(fcc).unwrap_or("????"))
101                } else {
102                    write!(
103                        f,
104                        "fourcc(0x{:02X}{:02X}{:02X}{:02X})",
105                        fcc[0], fcc[1], fcc[2], fcc[3]
106                    )
107                }
108            }
109            Self::WaveFormat(t) => write!(f, "wFormatTag(0x{t:04X})"),
110            Self::Mp4ObjectType(o) => write!(f, "mp4_oti(0x{o:02X})"),
111            Self::Matroska(s) => write!(f, "matroska({s})"),
112        }
113    }
114}
115
116/// Codec-level parameters shared between demuxer/muxer and en/decoder.
117#[derive(Clone, Debug)]
118pub struct CodecParameters {
119    pub codec_id: CodecId,
120    pub media_type: MediaType,
121
122    // Audio-specific
123    pub sample_rate: Option<u32>,
124    pub channels: Option<u16>,
125    pub sample_format: Option<SampleFormat>,
126
127    // Video-specific
128    pub width: Option<u32>,
129    pub height: Option<u32>,
130    pub pixel_format: Option<PixelFormat>,
131    pub frame_rate: Option<Rational>,
132
133    /// Per-codec setup bytes (e.g., SPS/PPS, OpusHead). Format defined by codec.
134    pub extradata: Vec<u8>,
135
136    pub bit_rate: Option<u64>,
137}
138
139impl CodecParameters {
140    pub fn audio(codec_id: CodecId) -> Self {
141        Self {
142            codec_id,
143            media_type: MediaType::Audio,
144            sample_rate: None,
145            channels: None,
146            sample_format: None,
147            width: None,
148            height: None,
149            pixel_format: None,
150            frame_rate: None,
151            extradata: Vec::new(),
152            bit_rate: None,
153        }
154    }
155
156    /// True when `self` and `other` have the same codec_id and core
157    /// format parameters (sample_rate/channels/sample_format for audio,
158    /// width/height/pixel_format for video). Extradata and bitrate
159    /// differences are tolerated — many containers rewrite extradata
160    /// losslessly during a copy operation.
161    pub fn matches_core(&self, other: &CodecParameters) -> bool {
162        self.codec_id == other.codec_id
163            && self.sample_rate == other.sample_rate
164            && self.channels == other.channels
165            && self.sample_format == other.sample_format
166            && self.width == other.width
167            && self.height == other.height
168            && self.pixel_format == other.pixel_format
169    }
170
171    pub fn video(codec_id: CodecId) -> Self {
172        Self {
173            codec_id,
174            media_type: MediaType::Video,
175            sample_rate: None,
176            channels: None,
177            sample_format: None,
178            width: None,
179            height: None,
180            pixel_format: None,
181            frame_rate: None,
182            extradata: Vec::new(),
183            bit_rate: None,
184        }
185    }
186}
187
188/// Description of a single stream inside a container.
189#[derive(Clone, Debug)]
190pub struct StreamInfo {
191    pub index: u32,
192    pub time_base: TimeBase,
193    pub duration: Option<i64>,
194    pub start_time: Option<i64>,
195    pub params: CodecParameters,
196}
197
198#[cfg(test)]
199mod codec_tag_tests {
200    use super::*;
201
202    #[test]
203    fn fourcc_uppercases_on_construction() {
204        let t = CodecTag::fourcc(b"div3");
205        assert_eq!(t, CodecTag::Fourcc(*b"DIV3"));
206        // Non-alphabetic bytes preserved unchanged.
207        let t2 = CodecTag::fourcc(b"MP42");
208        assert_eq!(t2, CodecTag::Fourcc(*b"MP42"));
209        let t3 = CodecTag::fourcc(&[0xFF, b'a', 0x00, b'1']);
210        assert_eq!(t3, CodecTag::Fourcc([0xFF, b'A', 0x00, b'1']));
211    }
212
213    #[test]
214    fn fourcc_equality_case_insensitive_via_ctor() {
215        assert_eq!(CodecTag::fourcc(b"xvid"), CodecTag::fourcc(b"XVID"));
216        assert_eq!(CodecTag::fourcc(b"DiV3"), CodecTag::fourcc(b"div3"));
217    }
218
219    #[test]
220    fn display_printable_fourcc() {
221        assert_eq!(CodecTag::fourcc(b"XVID").to_string(), "fourcc(XVID)");
222    }
223
224    #[test]
225    fn display_non_printable_fourcc_as_hex() {
226        let t = CodecTag::Fourcc([0x00, 0x00, 0x00, 0x01]);
227        assert_eq!(t.to_string(), "fourcc(0x00000001)");
228    }
229
230    #[test]
231    fn display_wave_format() {
232        assert_eq!(
233            CodecTag::wave_format(0x0055).to_string(),
234            "wFormatTag(0x0055)"
235        );
236    }
237
238    #[test]
239    fn display_mp4_oti() {
240        assert_eq!(CodecTag::mp4_object_type(0x40).to_string(), "mp4_oti(0x40)");
241    }
242
243    #[test]
244    fn display_matroska() {
245        assert_eq!(
246            CodecTag::matroska("V_MPEG4/ISO/AVC").to_string(),
247            "matroska(V_MPEG4/ISO/AVC)",
248        );
249    }
250}