Skip to main content

oximedia_codec/
stream_info.rs

1//! Stream information parsing and media container metadata.
2//!
3//! Provides types for describing individual elementary streams within a media
4//! container, a parser that reads a simple header, and a `MediaInfo` aggregate
5//! that groups all streams found in a file.
6
7#![allow(dead_code)]
8
9/// Type of elementary stream.
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub enum StreamType {
12    /// Video stream.
13    Video,
14    /// Audio stream.
15    Audio,
16    /// Subtitle or text stream.
17    Subtitle,
18    /// Data / metadata stream.
19    Data,
20    /// Unknown stream type.
21    Unknown,
22}
23
24/// Information record for a single elementary stream.
25#[derive(Debug, Clone)]
26pub struct StreamInfo {
27    /// Zero-based stream index within the container.
28    pub index: u32,
29    /// Stream type.
30    pub stream_type: StreamType,
31    /// Codec identifier string (e.g. `"h264"`, `"aac"`).
32    pub codec_id: String,
33    /// Bitrate in bits per second (0 if unknown).
34    pub bitrate_bps: u64,
35    /// Duration in milliseconds (0 if unknown).
36    pub duration_ms_val: u64,
37    /// Sample rate (audio) or frame rate numerator (video); 0 if unused.
38    pub rate_num: u32,
39    /// Frame rate denominator (video only); 0 if unused.
40    pub rate_den: u32,
41    /// Width in pixels (video only); 0 if unused.
42    pub width: u32,
43    /// Height in pixels (video only); 0 if unused.
44    pub height: u32,
45    /// Channel count (audio only); 0 if unused.
46    pub channels: u32,
47}
48
49impl StreamInfo {
50    /// Create a minimal stream descriptor.
51    pub fn new(index: u32, stream_type: StreamType, codec_id: impl Into<String>) -> Self {
52        Self {
53            index,
54            stream_type,
55            codec_id: codec_id.into(),
56            bitrate_bps: 0,
57            duration_ms_val: 0,
58            rate_num: 0,
59            rate_den: 1,
60            width: 0,
61            height: 0,
62            channels: 0,
63        }
64    }
65
66    /// Returns `true` if this is a video stream.
67    pub fn is_video(&self) -> bool {
68        self.stream_type == StreamType::Video
69    }
70
71    /// Returns `true` if this is an audio stream.
72    pub fn is_audio(&self) -> bool {
73        self.stream_type == StreamType::Audio
74    }
75
76    /// Returns the duration in milliseconds.
77    pub fn duration_ms(&self) -> u64 {
78        self.duration_ms_val
79    }
80
81    /// Returns the frame rate as a float (for video streams).
82    pub fn frame_rate(&self) -> f64 {
83        if self.rate_den == 0 {
84            return 0.0;
85        }
86        self.rate_num as f64 / self.rate_den as f64
87    }
88
89    /// Builder: set bitrate.
90    pub fn with_bitrate(mut self, bps: u64) -> Self {
91        self.bitrate_bps = bps;
92        self
93    }
94
95    /// Builder: set duration.
96    pub fn with_duration_ms(mut self, ms: u64) -> Self {
97        self.duration_ms_val = ms;
98        self
99    }
100
101    /// Builder: set video dimensions.
102    pub fn with_video_dims(mut self, width: u32, height: u32) -> Self {
103        self.width = width;
104        self.height = height;
105        self
106    }
107
108    /// Builder: set frame rate (numerator / denominator).
109    pub fn with_frame_rate(mut self, num: u32, den: u32) -> Self {
110        self.rate_num = num;
111        self.rate_den = den;
112        self
113    }
114
115    /// Builder: set audio properties.
116    pub fn with_audio(mut self, sample_rate: u32, channels: u32) -> Self {
117        self.rate_num = sample_rate;
118        self.channels = channels;
119        self
120    }
121}
122
123/// Parses a minimal text header format into a list of [`StreamInfo`] records.
124///
125/// Expected format per line:
126/// `<index>,<type>,<codec_id>,<bitrate_bps>,<duration_ms>,<rate_num>,<rate_den>,<w>,<h>,<ch>`
127///
128/// Lines starting with `#` are treated as comments and skipped.
129#[derive(Debug, Default)]
130pub struct StreamInfoParser;
131
132impl StreamInfoParser {
133    /// Create a new parser.
134    pub fn new() -> Self {
135        Self
136    }
137
138    /// Parse a multi-line header string into a `Vec<StreamInfo>`.
139    ///
140    /// Malformed lines are silently skipped.
141    pub fn parse_header(&self, header: &str) -> Vec<StreamInfo> {
142        let mut streams = Vec::new();
143        for line in header.lines() {
144            let trimmed = line.trim();
145            if trimmed.is_empty() || trimmed.starts_with('#') {
146                continue;
147            }
148            let parts: Vec<&str> = trimmed.split(',').collect();
149            if parts.len() < 10 {
150                continue;
151            }
152            let index: u32 = match parts[0].trim().parse() {
153                Ok(v) => v,
154                Err(_) => continue,
155            };
156            let stream_type = match parts[1].trim() {
157                "video" => StreamType::Video,
158                "audio" => StreamType::Audio,
159                "subtitle" => StreamType::Subtitle,
160                "data" => StreamType::Data,
161                _ => StreamType::Unknown,
162            };
163            let codec_id = parts[2].trim().to_string();
164            let bitrate_bps: u64 = parts[3].trim().parse().unwrap_or(0);
165            let duration_ms: u64 = parts[4].trim().parse().unwrap_or(0);
166            let rate_num: u32 = parts[5].trim().parse().unwrap_or(0);
167            let rate_den: u32 = parts[6].trim().parse().unwrap_or(1);
168            let width: u32 = parts[7].trim().parse().unwrap_or(0);
169            let height: u32 = parts[8].trim().parse().unwrap_or(0);
170            let channels: u32 = parts[9].trim().parse().unwrap_or(0);
171            streams.push(StreamInfo {
172                index,
173                stream_type,
174                codec_id,
175                bitrate_bps,
176                duration_ms_val: duration_ms,
177                rate_num,
178                rate_den,
179                width,
180                height,
181                channels,
182            });
183        }
184        streams
185    }
186}
187
188/// Aggregated media information for a complete container.
189#[derive(Debug, Default)]
190pub struct MediaInfo {
191    /// All streams present in the container.
192    pub streams: Vec<StreamInfo>,
193    /// Container format string (e.g. `"mp4"`, `"mkv"`).
194    pub container_format: String,
195    /// Total file size in bytes.
196    pub file_size_bytes: u64,
197}
198
199impl MediaInfo {
200    /// Create an empty `MediaInfo`.
201    pub fn new() -> Self {
202        Self::default()
203    }
204
205    /// Returns the total number of streams.
206    pub fn stream_count(&self) -> usize {
207        self.streams.len()
208    }
209
210    /// Returns a slice of video streams only.
211    pub fn video_streams(&self) -> Vec<&StreamInfo> {
212        self.streams.iter().filter(|s| s.is_video()).collect()
213    }
214
215    /// Returns a slice of audio streams only.
216    pub fn audio_streams(&self) -> Vec<&StreamInfo> {
217        self.streams.iter().filter(|s| s.is_audio()).collect()
218    }
219
220    /// Returns the primary video stream (index 0 among video streams), if any.
221    pub fn primary_video(&self) -> Option<&StreamInfo> {
222        self.video_streams().into_iter().next()
223    }
224
225    /// Returns the primary audio stream (index 0 among audio streams), if any.
226    pub fn primary_audio(&self) -> Option<&StreamInfo> {
227        self.audio_streams().into_iter().next()
228    }
229
230    /// Returns the total bitrate across all streams in kbps.
231    pub fn total_bitrate_kbps(&self) -> u64 {
232        self.streams.iter().map(|s| s.bitrate_bps / 1000).sum()
233    }
234}
235
236#[cfg(test)]
237mod tests {
238    use super::*;
239
240    fn make_video_stream() -> StreamInfo {
241        StreamInfo::new(0, StreamType::Video, "h264")
242            .with_video_dims(1920, 1080)
243            .with_frame_rate(30, 1)
244            .with_bitrate(5_000_000)
245            .with_duration_ms(60_000)
246    }
247
248    fn make_audio_stream() -> StreamInfo {
249        StreamInfo::new(1, StreamType::Audio, "aac")
250            .with_audio(48_000, 2)
251            .with_bitrate(128_000)
252            .with_duration_ms(60_000)
253    }
254
255    #[test]
256    fn test_stream_info_is_video() {
257        let s = make_video_stream();
258        assert!(s.is_video());
259        assert!(!s.is_audio());
260    }
261
262    #[test]
263    fn test_stream_info_is_audio() {
264        let s = make_audio_stream();
265        assert!(s.is_audio());
266        assert!(!s.is_video());
267    }
268
269    #[test]
270    fn test_stream_info_duration_ms() {
271        let s = make_video_stream();
272        assert_eq!(s.duration_ms(), 60_000);
273    }
274
275    #[test]
276    fn test_stream_info_frame_rate() {
277        let s = make_video_stream();
278        assert!((s.frame_rate() - 30.0).abs() < 0.001);
279    }
280
281    #[test]
282    fn test_stream_info_zero_den_frame_rate() {
283        let s = StreamInfo::new(0, StreamType::Video, "av1").with_frame_rate(30, 0);
284        assert!((s.frame_rate()).abs() < 0.001);
285    }
286
287    #[test]
288    fn test_parser_parses_video_line() {
289        let header = "0,video,h264,5000000,60000,30,1,1920,1080,0";
290        let parser = StreamInfoParser::new();
291        let streams = parser.parse_header(header);
292        assert_eq!(streams.len(), 1);
293        assert!(streams[0].is_video());
294        assert_eq!(streams[0].codec_id, "h264");
295        assert_eq!(streams[0].width, 1920);
296    }
297
298    #[test]
299    fn test_parser_parses_audio_line() {
300        let header = "1,audio,aac,128000,60000,48000,0,0,0,2";
301        let parser = StreamInfoParser::new();
302        let streams = parser.parse_header(header);
303        assert_eq!(streams.len(), 1);
304        assert!(streams[0].is_audio());
305        assert_eq!(streams[0].channels, 2);
306    }
307
308    #[test]
309    fn test_parser_skips_comment_lines() {
310        let header = "# This is a comment\n0,video,av1,4000000,30000,24,1,1280,720,0";
311        let parser = StreamInfoParser::new();
312        let streams = parser.parse_header(header);
313        assert_eq!(streams.len(), 1);
314    }
315
316    #[test]
317    fn test_parser_skips_malformed_lines() {
318        let header = "bad,line\n0,video,vp9,0,0,30,1,1920,1080,0";
319        let parser = StreamInfoParser::new();
320        let streams = parser.parse_header(header);
321        assert_eq!(streams.len(), 1);
322    }
323
324    #[test]
325    fn test_media_info_stream_count() {
326        let mut info = MediaInfo::new();
327        info.streams.push(make_video_stream());
328        info.streams.push(make_audio_stream());
329        assert_eq!(info.stream_count(), 2);
330    }
331
332    #[test]
333    fn test_media_info_video_streams() {
334        let mut info = MediaInfo::new();
335        info.streams.push(make_video_stream());
336        info.streams.push(make_audio_stream());
337        assert_eq!(info.video_streams().len(), 1);
338    }
339
340    #[test]
341    fn test_media_info_primary_video() {
342        let mut info = MediaInfo::new();
343        info.streams.push(make_video_stream());
344        assert!(info.primary_video().is_some());
345    }
346
347    #[test]
348    fn test_media_info_primary_audio() {
349        let mut info = MediaInfo::new();
350        info.streams.push(make_audio_stream());
351        assert!(info.primary_audio().is_some());
352    }
353
354    #[test]
355    fn test_media_info_total_bitrate_kbps() {
356        let mut info = MediaInfo::new();
357        info.streams.push(make_video_stream()); // 5000 kbps
358        info.streams.push(make_audio_stream()); // 128 kbps
359        assert_eq!(info.total_bitrate_kbps(), 5128);
360    }
361}