use std::time::Duration;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum ContainerFormat {
Gif,
Png,
Jpeg,
WebP,
Qoi,
ImageSequence,
Avi,
Mp4,
Mkv,
Webm,
Unknown,
}
impl ContainerFormat {
#[must_use]
pub fn extension(&self) -> &'static str {
match self {
Self::Gif => "gif",
Self::Png => "png",
Self::Jpeg => "jpg",
Self::WebP => "webp",
Self::Qoi => "qoi",
Self::ImageSequence => "",
Self::Avi => "avi",
Self::Mp4 => "mp4",
Self::Mkv => "mkv",
Self::Webm => "webm",
Self::Unknown => "",
}
}
#[must_use]
pub fn from_path(path: &str) -> Self {
let lower = path.to_lowercase();
if lower.ends_with(".gif") {
Self::Gif
} else if lower.ends_with(".apng") || lower.ends_with(".png") {
Self::Png
} else if lower.ends_with(".jpg") || lower.ends_with(".jpeg") {
Self::Jpeg
} else if lower.ends_with(".webp") {
Self::WebP
} else if lower.ends_with(".qoi") {
Self::Qoi
} else if lower.ends_with(".avi") {
Self::Avi
} else if lower.ends_with(".mp4") || lower.ends_with(".mov") {
Self::Mp4
} else if lower.ends_with(".mkv") {
Self::Mkv
} else if lower.ends_with(".webm") {
Self::Webm
} else {
Self::Unknown
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum PixelFormat {
Rgb8,
Rgba8,
Gray8,
Rgb16,
Rgba16,
Rgb32F,
Rgba32F,
}
impl PixelFormat {
#[must_use]
pub fn channels(&self) -> usize {
match self {
Self::Gray8 => 1,
Self::Rgb8 | Self::Rgb16 | Self::Rgb32F => 3,
Self::Rgba8 | Self::Rgba16 | Self::Rgba32F => 4,
}
}
}
#[derive(Clone, Debug)]
pub struct StreamInfo {
pub index: usize,
pub stream_type: StreamType,
pub codec: String,
pub width: usize,
pub height: usize,
pub fps: f64,
pub duration: Duration,
pub frame_count: usize,
pub pixel_format: PixelFormat,
pub rotation: u32,
pub bit_rate: u64,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum StreamType {
Video,
Audio,
Subtitle,
}
#[derive(Clone, Debug)]
pub struct VideoMetadata {
pub format: ContainerFormat,
pub duration: Duration,
pub fps: f64,
pub width: usize,
pub height: usize,
pub frame_count: usize,
pub video_codec: String,
pub pixel_format: PixelFormat,
pub rotation: u32,
pub bit_rate: u64,
pub streams: Vec<StreamInfo>,
pub has_audio: bool,
pub has_subtitles: bool,
pub file_size: u64,
}
impl VideoMetadata {
#[must_use]
pub fn synthetic(width: usize, height: usize, fps: f64, frame_count: usize) -> Self {
let duration_secs = if fps > 0.0 {
frame_count as f64 / fps
} else {
0.0
};
Self {
format: ContainerFormat::Unknown,
duration: Duration::from_secs_f64(duration_secs),
fps,
width,
height,
frame_count,
video_codec: "unknown".to_string(),
pixel_format: PixelFormat::Rgb8,
rotation: 0,
bit_rate: 0,
streams: Vec::new(),
has_audio: false,
has_subtitles: false,
file_size: 0,
}
}
#[must_use]
pub fn aspect_ratio(&self) -> f64 {
if self.height == 0 {
return 0.0;
}
self.width as f64 / self.height as f64
}
#[must_use]
pub fn video_stream_count(&self) -> usize {
self.streams
.iter()
.filter(|s| s.stream_type == StreamType::Video)
.count()
}
#[must_use]
pub fn audio_stream_count(&self) -> usize {
self.streams
.iter()
.filter(|s| s.stream_type == StreamType::Audio)
.count()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_container_format_detection() {
assert_eq!(ContainerFormat::from_path("test.gif"), ContainerFormat::Gif);
assert_eq!(ContainerFormat::from_path("test.mp4"), ContainerFormat::Mp4);
assert_eq!(ContainerFormat::from_path("test.MKV"), ContainerFormat::Mkv);
assert_eq!(
ContainerFormat::from_path("test.webp"),
ContainerFormat::WebP
);
assert_eq!(
ContainerFormat::from_path("test.unknown"),
ContainerFormat::Unknown
);
}
#[test]
fn test_video_metadata() {
let meta = VideoMetadata::synthetic(1920, 1080, 30.0, 300);
assert_eq!(meta.width, 1920);
assert_eq!(meta.height, 1080);
assert!((meta.fps - 30.0).abs() < 1e-6);
assert_eq!(meta.frame_count, 300);
assert!((meta.aspect_ratio() - 16.0 / 9.0).abs() < 1e-6);
}
#[test]
fn test_pixel_format_channels() {
assert_eq!(PixelFormat::Rgb8.channels(), 3);
assert_eq!(PixelFormat::Rgba8.channels(), 4);
assert_eq!(PixelFormat::Gray8.channels(), 1);
}
}