Skip to main content

iris/video/
metadata.rs

1use std::time::Duration;
2
3/// Supported video container formats.
4#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
5pub enum ContainerFormat {
6    /// Animated GIF
7    Gif,
8    /// Portable Network Graphics (animated/APNG)
9    Png,
10    /// JPEG (animated)
11    Jpeg,
12    /// WebP (animated)
13    WebP,
14    /// QOI format
15    Qoi,
16    /// Image sequence (directory of numbered images)
17    ImageSequence,
18    /// AVI container
19    Avi,
20    /// MP4/MOV container
21    Mp4,
22    /// MKV/Matroska container
23    Mkv,
24    /// WebM container
25    Webm,
26    /// Unknown format
27    Unknown,
28}
29
30impl ContainerFormat {
31    /// Returns the typical file extension for this format.
32    #[must_use]
33    pub fn extension(&self) -> &'static str {
34        match self {
35            Self::Gif => "gif",
36            Self::Png => "png",
37            Self::Jpeg => "jpg",
38            Self::WebP => "webp",
39            Self::Qoi => "qoi",
40            Self::ImageSequence => "",
41            Self::Avi => "avi",
42            Self::Mp4 => "mp4",
43            Self::Mkv => "mkv",
44            Self::Webm => "webm",
45            Self::Unknown => "",
46        }
47    }
48
49    /// Detects the container format from a file path extension.
50    #[must_use]
51    pub fn from_path(path: &str) -> Self {
52        let lower = path.to_lowercase();
53        if lower.ends_with(".gif") {
54            Self::Gif
55        } else if lower.ends_with(".apng") || lower.ends_with(".png") {
56            Self::Png
57        } else if lower.ends_with(".jpg") || lower.ends_with(".jpeg") {
58            Self::Jpeg
59        } else if lower.ends_with(".webp") {
60            Self::WebP
61        } else if lower.ends_with(".qoi") {
62            Self::Qoi
63        } else if lower.ends_with(".avi") {
64            Self::Avi
65        } else if lower.ends_with(".mp4") || lower.ends_with(".mov") {
66            Self::Mp4
67        } else if lower.ends_with(".mkv") {
68            Self::Mkv
69        } else if lower.ends_with(".webm") {
70            Self::Webm
71        } else {
72            Self::Unknown
73        }
74    }
75}
76
77/// Pixel format of video frames.
78#[derive(Clone, Copy, Debug, PartialEq, Eq)]
79pub enum PixelFormat {
80    /// 8-bit RGB (3 channels)
81    Rgb8,
82    /// 8-bit RGBA (4 channels)
83    Rgba8,
84    /// 8-bit Grayscale (1 channel)
85    Gray8,
86    /// 16-bit RGB
87    Rgb16,
88    /// 16-bit RGBA
89    Rgba16,
90    /// 32-bit float RGB
91    Rgb32F,
92    /// 32-bit float RGBA
93    Rgba32F,
94}
95
96impl PixelFormat {
97    /// Returns the number of channels for this pixel format.
98    #[must_use]
99    pub fn channels(&self) -> usize {
100        match self {
101            Self::Gray8 => 1,
102            Self::Rgb8 | Self::Rgb16 | Self::Rgb32F => 3,
103            Self::Rgba8 | Self::Rgba16 | Self::Rgba32F => 4,
104        }
105    }
106}
107
108/// Information about a single stream within a video file.
109#[derive(Clone, Debug)]
110pub struct StreamInfo {
111    /// Stream index (0-based).
112    pub index: usize,
113    /// Stream type.
114    pub stream_type: StreamType,
115    /// Codec name.
116    pub codec: String,
117    /// Width in pixels (for video streams).
118    pub width: usize,
119    /// Height in pixels (for video streams).
120    pub height: usize,
121    /// Frame rate in frames per second.
122    pub fps: f64,
123    /// Duration of the stream.
124    pub duration: Duration,
125    /// Total number of frames.
126    pub frame_count: usize,
127    /// Pixel format.
128    pub pixel_format: PixelFormat,
129    /// Rotation angle in degrees.
130    pub rotation: u32,
131    /// Bit rate in bits per second.
132    pub bit_rate: u64,
133}
134
135/// Type of stream within a video file.
136#[derive(Clone, Copy, Debug, PartialEq, Eq)]
137pub enum StreamType {
138    /// Video stream.
139    Video,
140    /// Audio stream.
141    Audio,
142    /// Subtitle stream.
143    Subtitle,
144}
145
146/// Complete metadata for a video file.
147#[derive(Clone, Debug)]
148pub struct VideoMetadata {
149    /// Container format.
150    pub format: ContainerFormat,
151    /// Video duration.
152    pub duration: Duration,
153    /// Frame rate in frames per second.
154    pub fps: f64,
155    /// Width in pixels.
156    pub width: usize,
157    /// Height in pixels.
158    pub height: usize,
159    /// Total number of frames.
160    pub frame_count: usize,
161    /// Video codec.
162    pub video_codec: String,
163    /// Pixel format.
164    pub pixel_format: PixelFormat,
165    /// Rotation angle in degrees.
166    pub rotation: u32,
167    /// Total bit rate.
168    pub bit_rate: u64,
169    /// All streams in the file.
170    pub streams: Vec<StreamInfo>,
171    /// Whether the video has audio.
172    pub has_audio: bool,
173    /// Whether the video has subtitles.
174    pub has_subtitles: bool,
175    /// File size in bytes.
176    pub file_size: u64,
177}
178
179impl VideoMetadata {
180    /// Creates metadata for a synthetic/test video.
181    #[must_use]
182    pub fn synthetic(width: usize, height: usize, fps: f64, frame_count: usize) -> Self {
183        let duration_secs = if fps > 0.0 {
184            frame_count as f64 / fps
185        } else {
186            0.0
187        };
188        Self {
189            format: ContainerFormat::Unknown,
190            duration: Duration::from_secs_f64(duration_secs),
191            fps,
192            width,
193            height,
194            frame_count,
195            video_codec: "unknown".to_string(),
196            pixel_format: PixelFormat::Rgb8,
197            rotation: 0,
198            bit_rate: 0,
199            streams: Vec::new(),
200            has_audio: false,
201            has_subtitles: false,
202            file_size: 0,
203        }
204    }
205
206    /// Returns the aspect ratio (width / height).
207    #[must_use]
208    pub fn aspect_ratio(&self) -> f64 {
209        if self.height == 0 {
210            return 0.0;
211        }
212        self.width as f64 / self.height as f64
213    }
214
215    /// Returns the total number of video streams.
216    #[must_use]
217    pub fn video_stream_count(&self) -> usize {
218        self.streams
219            .iter()
220            .filter(|s| s.stream_type == StreamType::Video)
221            .count()
222    }
223
224    /// Returns the total number of audio streams.
225    #[must_use]
226    pub fn audio_stream_count(&self) -> usize {
227        self.streams
228            .iter()
229            .filter(|s| s.stream_type == StreamType::Audio)
230            .count()
231    }
232}
233
234#[cfg(test)]
235mod tests {
236    use super::*;
237
238    #[test]
239    fn test_container_format_detection() {
240        assert_eq!(ContainerFormat::from_path("test.gif"), ContainerFormat::Gif);
241        assert_eq!(ContainerFormat::from_path("test.mp4"), ContainerFormat::Mp4);
242        assert_eq!(ContainerFormat::from_path("test.MKV"), ContainerFormat::Mkv);
243        assert_eq!(
244            ContainerFormat::from_path("test.webp"),
245            ContainerFormat::WebP
246        );
247        assert_eq!(
248            ContainerFormat::from_path("test.unknown"),
249            ContainerFormat::Unknown
250        );
251    }
252
253    #[test]
254    fn test_video_metadata() {
255        let meta = VideoMetadata::synthetic(1920, 1080, 30.0, 300);
256        assert_eq!(meta.width, 1920);
257        assert_eq!(meta.height, 1080);
258        assert!((meta.fps - 30.0).abs() < 1e-6);
259        assert_eq!(meta.frame_count, 300);
260        assert!((meta.aspect_ratio() - 16.0 / 9.0).abs() < 1e-6);
261    }
262
263    #[test]
264    fn test_pixel_format_channels() {
265        assert_eq!(PixelFormat::Rgb8.channels(), 3);
266        assert_eq!(PixelFormat::Rgba8.channels(), 4);
267        assert_eq!(PixelFormat::Gray8.channels(), 1);
268    }
269}