Skip to main content

oximedia_container/
stream.rs

1//! Stream information types.
2
3use bytes::Bytes;
4use oximedia_core::{CodecId, MediaType, Rational};
5
6use crate::demux::matroska::matroska_v4::BlockAdditionMapping;
7
8/// Information about a stream in a container.
9///
10/// Each stream in a container has associated metadata including codec,
11/// timebase, and format-specific parameters.
12#[derive(Clone, Debug)]
13pub struct StreamInfo {
14    /// Stream index (0-based).
15    pub index: usize,
16
17    /// Codec used for this stream.
18    pub codec: CodecId,
19
20    /// Type of media in this stream.
21    pub media_type: MediaType,
22
23    /// Timebase for interpreting timestamps.
24    ///
25    /// Timestamps are expressed as multiples of this rational number.
26    /// For example, a timebase of 1/1000 means timestamps are in milliseconds.
27    pub timebase: Rational,
28
29    /// Duration of the stream in timebase units, if known.
30    pub duration: Option<i64>,
31
32    /// Codec-specific parameters.
33    pub codec_params: CodecParams,
34
35    /// Stream metadata (title, language, etc.).
36    pub metadata: Metadata,
37}
38
39impl StreamInfo {
40    /// Creates a new `StreamInfo` with the given parameters.
41    #[must_use]
42    pub fn new(index: usize, codec: CodecId, timebase: Rational) -> Self {
43        Self {
44            index,
45            codec,
46            media_type: codec.media_type(),
47            timebase,
48            duration: None,
49            codec_params: CodecParams::default(),
50            metadata: Metadata::default(),
51        }
52    }
53
54    /// Returns the duration in seconds, if known.
55    #[must_use]
56    #[allow(clippy::cast_precision_loss)]
57    pub fn duration_seconds(&self) -> Option<f64> {
58        self.duration.map(|d| {
59            if self.timebase.den == 0 {
60                0.0
61            } else {
62                (d as f64 * self.timebase.num as f64) / self.timebase.den as f64
63            }
64        })
65    }
66
67    /// Returns true if this is a video stream.
68    #[must_use]
69    pub const fn is_video(&self) -> bool {
70        matches!(self.media_type, MediaType::Video)
71    }
72
73    /// Returns true if this is an audio stream.
74    #[must_use]
75    pub const fn is_audio(&self) -> bool {
76        matches!(self.media_type, MediaType::Audio)
77    }
78
79    /// Returns true if this is a subtitle stream.
80    #[must_use]
81    pub const fn is_subtitle(&self) -> bool {
82        matches!(self.media_type, MediaType::Subtitle)
83    }
84}
85
86/// Codec-specific parameters.
87///
88/// Contains format-specific information needed for decoding,
89/// such as video dimensions or audio sample rate.
90#[derive(Clone, Debug, Default)]
91pub struct CodecParams {
92    /// Video width in pixels.
93    pub width: Option<u32>,
94
95    /// Video height in pixels.
96    pub height: Option<u32>,
97
98    /// Audio sample rate in Hz.
99    pub sample_rate: Option<u32>,
100
101    /// Number of audio channels.
102    pub channels: Option<u8>,
103
104    /// Codec-specific extra data (e.g., SPS/PPS for video, codec headers).
105    pub extradata: Option<Bytes>,
106
107    /// Matroska BlockAdditionMapping metadata for this stream, if any.
108    pub block_addition_mappings: Vec<BlockAdditionMapping>,
109}
110
111impl CodecParams {
112    /// Creates video codec parameters.
113    #[must_use]
114    pub const fn video(width: u32, height: u32) -> Self {
115        Self {
116            width: Some(width),
117            height: Some(height),
118            sample_rate: None,
119            channels: None,
120            extradata: None,
121            block_addition_mappings: Vec::new(),
122        }
123    }
124
125    /// Creates audio codec parameters.
126    #[must_use]
127    pub const fn audio(sample_rate: u32, channels: u8) -> Self {
128        Self {
129            width: None,
130            height: None,
131            sample_rate: Some(sample_rate),
132            channels: Some(channels),
133            extradata: None,
134            block_addition_mappings: Vec::new(),
135        }
136    }
137
138    /// Sets the extradata.
139    #[must_use]
140    pub fn with_extradata(mut self, extradata: Bytes) -> Self {
141        self.extradata = Some(extradata);
142        self
143    }
144
145    /// Returns true if video dimensions are set.
146    #[must_use]
147    pub const fn has_video_params(&self) -> bool {
148        self.width.is_some() && self.height.is_some()
149    }
150
151    /// Returns true if audio parameters are set.
152    #[must_use]
153    pub const fn has_audio_params(&self) -> bool {
154        self.sample_rate.is_some() && self.channels.is_some()
155    }
156}
157
158/// Stream and container metadata.
159///
160/// Contains textual metadata such as title, artist, and custom key-value pairs.
161#[derive(Clone, Debug, Default)]
162pub struct Metadata {
163    /// Stream or container title.
164    pub title: Option<String>,
165
166    /// Artist or author.
167    pub artist: Option<String>,
168
169    /// Album name (for audio).
170    pub album: Option<String>,
171
172    /// Additional metadata entries.
173    pub entries: Vec<(String, String)>,
174}
175
176impl Metadata {
177    /// Creates empty metadata.
178    #[must_use]
179    pub const fn new() -> Self {
180        Self {
181            title: None,
182            artist: None,
183            album: None,
184            entries: Vec::new(),
185        }
186    }
187
188    /// Sets the title.
189    #[must_use]
190    pub fn with_title(mut self, title: impl Into<String>) -> Self {
191        self.title = Some(title.into());
192        self
193    }
194
195    /// Sets the artist.
196    #[must_use]
197    pub fn with_artist(mut self, artist: impl Into<String>) -> Self {
198        self.artist = Some(artist.into());
199        self
200    }
201
202    /// Sets the album.
203    #[must_use]
204    pub fn with_album(mut self, album: impl Into<String>) -> Self {
205        self.album = Some(album.into());
206        self
207    }
208
209    /// Adds a custom metadata entry.
210    #[must_use]
211    pub fn with_entry(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
212        self.entries.push((key.into(), value.into()));
213        self
214    }
215
216    /// Gets a metadata value by key.
217    ///
218    /// First checks the common fields (title, artist, album),
219    /// then searches the custom entries.
220    #[must_use]
221    pub fn get(&self, key: &str) -> Option<&str> {
222        let key_lower = key.to_lowercase();
223        match key_lower.as_str() {
224            "title" => self.title.as_deref(),
225            "artist" | "author" => self.artist.as_deref(),
226            "album" => self.album.as_deref(),
227            _ => self
228                .entries
229                .iter()
230                .find(|(k, _)| k.eq_ignore_ascii_case(key))
231                .map(|(_, v)| v.as_str()),
232        }
233    }
234
235    /// Returns true if the metadata is empty.
236    #[must_use]
237    pub fn is_empty(&self) -> bool {
238        self.title.is_none()
239            && self.artist.is_none()
240            && self.album.is_none()
241            && self.entries.is_empty()
242    }
243}
244
245#[cfg(test)]
246mod tests {
247    use super::*;
248
249    #[test]
250    fn test_stream_info() {
251        let stream = StreamInfo::new(0, CodecId::Av1, Rational::new(1, 1000));
252        assert_eq!(stream.index, 0);
253        assert_eq!(stream.codec, CodecId::Av1);
254        assert!(stream.is_video());
255        assert!(!stream.is_audio());
256    }
257
258    #[test]
259    fn test_stream_duration() {
260        let mut stream = StreamInfo::new(0, CodecId::Opus, Rational::new(1, 48000));
261        stream.duration = Some(480_000); // 10 seconds at 48kHz
262
263        let duration = stream.duration_seconds().expect("operation should succeed");
264        assert!((duration - 10.0).abs() < 0.001);
265    }
266
267    #[test]
268    fn test_codec_params_video() {
269        let params = CodecParams::video(1920, 1080);
270        assert!(params.has_video_params());
271        assert!(!params.has_audio_params());
272        assert_eq!(params.width, Some(1920));
273        assert_eq!(params.height, Some(1080));
274    }
275
276    #[test]
277    fn test_codec_params_audio() {
278        let params = CodecParams::audio(48000, 2);
279        assert!(!params.has_video_params());
280        assert!(params.has_audio_params());
281        assert_eq!(params.sample_rate, Some(48000));
282        assert_eq!(params.channels, Some(2));
283    }
284
285    #[test]
286    fn test_metadata() {
287        let metadata = Metadata::new()
288            .with_title("Test Title")
289            .with_artist("Test Artist")
290            .with_entry("language", "en");
291
292        assert_eq!(metadata.get("title"), Some("Test Title"));
293        assert_eq!(metadata.get("artist"), Some("Test Artist"));
294        assert_eq!(metadata.get("language"), Some("en"));
295        assert_eq!(metadata.get("nonexistent"), None);
296    }
297
298    #[test]
299    fn test_metadata_is_empty() {
300        assert!(Metadata::new().is_empty());
301        assert!(!Metadata::new().with_title("Test").is_empty());
302    }
303}