1use bytes::Bytes;
4use oximedia_core::{CodecId, MediaType, Rational};
5
6use crate::demux::matroska::matroska_v4::BlockAdditionMapping;
7
8#[derive(Clone, Debug)]
13pub struct StreamInfo {
14 pub index: usize,
16
17 pub codec: CodecId,
19
20 pub media_type: MediaType,
22
23 pub timebase: Rational,
28
29 pub duration: Option<i64>,
31
32 pub codec_params: CodecParams,
34
35 pub metadata: Metadata,
37}
38
39impl StreamInfo {
40 #[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 #[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 #[must_use]
69 pub const fn is_video(&self) -> bool {
70 matches!(self.media_type, MediaType::Video)
71 }
72
73 #[must_use]
75 pub const fn is_audio(&self) -> bool {
76 matches!(self.media_type, MediaType::Audio)
77 }
78
79 #[must_use]
81 pub const fn is_subtitle(&self) -> bool {
82 matches!(self.media_type, MediaType::Subtitle)
83 }
84}
85
86#[derive(Clone, Debug, Default)]
91pub struct CodecParams {
92 pub width: Option<u32>,
94
95 pub height: Option<u32>,
97
98 pub sample_rate: Option<u32>,
100
101 pub channels: Option<u8>,
103
104 pub extradata: Option<Bytes>,
106
107 pub block_addition_mappings: Vec<BlockAdditionMapping>,
109}
110
111impl CodecParams {
112 #[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 #[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 #[must_use]
140 pub fn with_extradata(mut self, extradata: Bytes) -> Self {
141 self.extradata = Some(extradata);
142 self
143 }
144
145 #[must_use]
147 pub const fn has_video_params(&self) -> bool {
148 self.width.is_some() && self.height.is_some()
149 }
150
151 #[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#[derive(Clone, Debug, Default)]
162pub struct Metadata {
163 pub title: Option<String>,
165
166 pub artist: Option<String>,
168
169 pub album: Option<String>,
171
172 pub entries: Vec<(String, String)>,
174}
175
176impl Metadata {
177 #[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 #[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 #[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 #[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 #[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 #[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 #[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); 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}