#![allow(dead_code)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum MediaFormat {
Mp4,
Mkv,
Avi,
Mov,
Webm,
Flv,
Ts,
M2ts,
Mxf,
Ogg,
Mp3,
Flac,
Wav,
Aac,
Opus,
Vorbis,
Aiff,
Au,
Jpeg,
Png,
Gif,
Webp,
Bmp,
Tiff,
Svg,
Heic,
Avif,
Srt,
Vtt,
Ass,
Zip,
Tar,
Gz,
Bz2,
Xz,
Zstd,
Dpx,
Exr,
Dng,
Jxl,
Y4m,
Ffv1,
Caf,
Ps,
Unknown,
}
impl MediaFormat {
#[must_use]
pub fn is_video(&self) -> bool {
matches!(
self,
MediaFormat::Mp4
| MediaFormat::Mkv
| MediaFormat::Avi
| MediaFormat::Mov
| MediaFormat::Webm
| MediaFormat::Flv
| MediaFormat::Ts
| MediaFormat::M2ts
| MediaFormat::Mxf
| MediaFormat::Ogg
| MediaFormat::Y4m
| MediaFormat::Ffv1
| MediaFormat::Ps
)
}
#[must_use]
pub fn is_audio(&self) -> bool {
matches!(
self,
MediaFormat::Mp3
| MediaFormat::Flac
| MediaFormat::Wav
| MediaFormat::Aac
| MediaFormat::Opus
| MediaFormat::Vorbis
| MediaFormat::Aiff
| MediaFormat::Au
| MediaFormat::Caf
)
}
#[must_use]
pub fn is_image(&self) -> bool {
matches!(
self,
MediaFormat::Jpeg
| MediaFormat::Png
| MediaFormat::Gif
| MediaFormat::Webp
| MediaFormat::Bmp
| MediaFormat::Tiff
| MediaFormat::Svg
| MediaFormat::Heic
| MediaFormat::Avif
| MediaFormat::Dpx
| MediaFormat::Exr
| MediaFormat::Dng
| MediaFormat::Jxl
)
}
#[must_use]
pub fn extension(&self) -> &'static str {
match self {
MediaFormat::Mp4 => "mp4",
MediaFormat::Mkv => "mkv",
MediaFormat::Avi => "avi",
MediaFormat::Mov => "mov",
MediaFormat::Webm => "webm",
MediaFormat::Flv => "flv",
MediaFormat::Ts => "ts",
MediaFormat::M2ts => "m2ts",
MediaFormat::Mxf => "mxf",
MediaFormat::Ogg => "ogg",
MediaFormat::Mp3 => "mp3",
MediaFormat::Flac => "flac",
MediaFormat::Wav => "wav",
MediaFormat::Aac => "aac",
MediaFormat::Opus => "opus",
MediaFormat::Vorbis => "ogg",
MediaFormat::Aiff => "aiff",
MediaFormat::Au => "au",
MediaFormat::Jpeg => "jpg",
MediaFormat::Png => "png",
MediaFormat::Gif => "gif",
MediaFormat::Webp => "webp",
MediaFormat::Bmp => "bmp",
MediaFormat::Tiff => "tiff",
MediaFormat::Svg => "svg",
MediaFormat::Heic => "heic",
MediaFormat::Avif => "avif",
MediaFormat::Srt => "srt",
MediaFormat::Vtt => "vtt",
MediaFormat::Ass => "ass",
MediaFormat::Zip => "zip",
MediaFormat::Tar => "tar",
MediaFormat::Gz => "gz",
MediaFormat::Bz2 => "bz2",
MediaFormat::Xz => "xz",
MediaFormat::Zstd => "zst",
MediaFormat::Dpx => "dpx",
MediaFormat::Exr => "exr",
MediaFormat::Dng => "dng",
MediaFormat::Jxl => "jxl",
MediaFormat::Y4m => "y4m",
MediaFormat::Ffv1 => "ffv1",
MediaFormat::Caf => "caf",
MediaFormat::Ps => "mpg",
MediaFormat::Unknown => "bin",
}
}
#[must_use]
pub fn mime_type(&self) -> &'static str {
match self {
MediaFormat::Mp4 => "video/mp4",
MediaFormat::Mkv => "video/x-matroska",
MediaFormat::Avi => "video/x-msvideo",
MediaFormat::Mov => "video/quicktime",
MediaFormat::Webm => "video/webm",
MediaFormat::Flv => "video/x-flv",
MediaFormat::Ts => "video/mp2t",
MediaFormat::M2ts => "video/mp2t",
MediaFormat::Mxf => "application/mxf",
MediaFormat::Ogg => "video/ogg",
MediaFormat::Mp3 => "audio/mpeg",
MediaFormat::Flac => "audio/flac",
MediaFormat::Wav => "audio/wav",
MediaFormat::Aac => "audio/aac",
MediaFormat::Opus => "audio/ogg; codecs=opus",
MediaFormat::Vorbis => "audio/ogg; codecs=vorbis",
MediaFormat::Aiff => "audio/aiff",
MediaFormat::Au => "audio/basic",
MediaFormat::Jpeg => "image/jpeg",
MediaFormat::Png => "image/png",
MediaFormat::Gif => "image/gif",
MediaFormat::Webp => "image/webp",
MediaFormat::Bmp => "image/bmp",
MediaFormat::Tiff => "image/tiff",
MediaFormat::Svg => "image/svg+xml",
MediaFormat::Heic => "image/heic",
MediaFormat::Avif => "image/avif",
MediaFormat::Srt => "text/plain",
MediaFormat::Vtt => "text/vtt",
MediaFormat::Ass => "text/x-ssa",
MediaFormat::Zip => "application/zip",
MediaFormat::Tar => "application/x-tar",
MediaFormat::Gz => "application/gzip",
MediaFormat::Bz2 => "application/x-bzip2",
MediaFormat::Xz => "application/x-xz",
MediaFormat::Zstd => "application/zstd",
MediaFormat::Dpx => "image/x-dpx",
MediaFormat::Exr => "image/x-exr",
MediaFormat::Dng => "image/x-adobe-dng",
MediaFormat::Jxl => "image/jxl",
MediaFormat::Y4m => "video/x-raw-yuv",
MediaFormat::Ffv1 => "video/x-ffv1",
MediaFormat::Caf => "audio/x-caf",
MediaFormat::Ps => "video/mpeg",
MediaFormat::Unknown => "application/octet-stream",
}
}
#[must_use]
pub fn description(&self) -> &'static str {
match self {
MediaFormat::Mp4 => "MPEG-4 Part 14 video container",
MediaFormat::Mkv => "Matroska video container",
MediaFormat::Avi => "Audio Video Interleave container",
MediaFormat::Mov => "Apple QuickTime movie container",
MediaFormat::Webm => "WebM video container",
MediaFormat::Flv => "Flash Video container",
MediaFormat::Ts => "MPEG-2 Transport Stream",
MediaFormat::M2ts => "Blu-ray MPEG-2 Transport Stream",
MediaFormat::Mxf => "Material Exchange Format",
MediaFormat::Ogg => "Ogg multimedia container",
MediaFormat::Mp3 => "MPEG Audio Layer III",
MediaFormat::Flac => "Free Lossless Audio Codec",
MediaFormat::Wav => "Waveform Audio File Format",
MediaFormat::Aac => "Advanced Audio Coding",
MediaFormat::Opus => "Opus audio codec",
MediaFormat::Vorbis => "Vorbis audio codec",
MediaFormat::Aiff => "Audio Interchange File Format",
MediaFormat::Au => "Sun/NeXT AU audio",
MediaFormat::Jpeg => "JPEG image",
MediaFormat::Png => "Portable Network Graphics image",
MediaFormat::Gif => "Graphics Interchange Format image",
MediaFormat::Webp => "WebP image",
MediaFormat::Bmp => "Windows Bitmap image",
MediaFormat::Tiff => "Tagged Image File Format",
MediaFormat::Svg => "Scalable Vector Graphics",
MediaFormat::Heic => "High Efficiency Image Container",
MediaFormat::Avif => "AV1 Image File Format",
MediaFormat::Srt => "SubRip Text subtitles",
MediaFormat::Vtt => "Web Video Text Tracks",
MediaFormat::Ass => "Advanced SubStation Alpha subtitles",
MediaFormat::Zip => "ZIP archive",
MediaFormat::Tar => "Unix tar archive",
MediaFormat::Gz => "Gzip compressed data",
MediaFormat::Bz2 => "Bzip2 compressed data",
MediaFormat::Xz => "XZ compressed data",
MediaFormat::Zstd => "Zstandard compressed data",
MediaFormat::Dpx => "Digital Picture Exchange image",
MediaFormat::Exr => "OpenEXR high dynamic range image",
MediaFormat::Dng => "Adobe Digital Negative",
MediaFormat::Jxl => "JPEG XL image",
MediaFormat::Y4m => "YUV4MPEG2 raw video",
MediaFormat::Ffv1 => "FFV1 lossless video codec",
MediaFormat::Caf => "Core Audio Format",
MediaFormat::Ps => "MPEG Program Stream",
MediaFormat::Unknown => "Unknown binary data",
}
}
}
#[derive(Debug, Clone)]
pub struct FormatDetection {
pub format: MediaFormat,
pub confidence: f32,
pub mime_type: &'static str,
pub extension: &'static str,
pub description: &'static str,
}
impl FormatDetection {
fn new(format: MediaFormat, confidence: f32) -> Self {
Self {
mime_type: format.mime_type(),
extension: format.extension(),
description: format.description(),
format,
confidence,
}
}
}
#[derive(Debug, Default, Clone)]
pub struct FormatDetector;
impl FormatDetector {
#[must_use]
pub fn new() -> Self {
Self
}
#[must_use]
pub fn detect(data: &[u8]) -> FormatDetection {
if data.is_empty() {
return FormatDetection::new(MediaFormat::Unknown, 0.0);
}
if data.len() >= 2 && data[0] == 0xFF && data[1] == 0xD8 {
return FormatDetection::new(MediaFormat::Jpeg, 1.0);
}
if data.len() >= 8 && data[..8] == [0x89, b'P', b'N', b'G', 0x0D, 0x0A, 0x1A, 0x0A] {
return FormatDetection::new(MediaFormat::Png, 1.0);
}
if data.len() >= 6 && (&data[..6] == b"GIF87a" || &data[..6] == b"GIF89a") {
return FormatDetection::new(MediaFormat::Gif, 1.0);
}
if data.len() >= 4 && &data[..4] == b"fLaC" {
return FormatDetection::new(MediaFormat::Flac, 1.0);
}
if data.len() >= 4 && &data[..4] == b"OggS" {
if data.len() >= 36 && &data[28..36] == b"OpusHead" {
return FormatDetection::new(MediaFormat::Opus, 1.0);
}
if data.len() >= 35 && data[28] == 0x01 && &data[29..35] == b"vorbi" {
return FormatDetection::new(MediaFormat::Vorbis, 1.0);
}
return FormatDetection::new(MediaFormat::Ogg, 0.95);
}
if data.len() >= 12 && &data[..4] == b"RIFF" {
if &data[8..12] == b"WAVE" {
return FormatDetection::new(MediaFormat::Wav, 1.0);
}
if &data[8..12] == b"AVI " {
return FormatDetection::new(MediaFormat::Avi, 1.0);
}
if &data[8..12] == b"WEBP" {
return FormatDetection::new(MediaFormat::Webp, 1.0);
}
return FormatDetection::new(MediaFormat::Unknown, 0.3);
}
if data.len() >= 12 && &data[..4] == b"FORM" {
if &data[8..12] == b"AIFF" || &data[8..12] == b"AIFC" {
return FormatDetection::new(MediaFormat::Aiff, 1.0);
}
}
if data.len() >= 4 && data[..4] == [0x1A, 0x45, 0xDF, 0xA3] {
let scan_end = data.len().min(128);
let doctype_marker = [0x42u8, 0x82];
let mut idx = 4;
let mut found_webm = false;
let mut found_mkv = false;
while idx + 1 < scan_end {
if data[idx] == doctype_marker[0] && data[idx + 1] == doctype_marker[1] {
if idx + 2 < scan_end {
let size = data[idx + 2] as usize;
let str_start = idx + 3;
let str_end = str_start.saturating_add(size).min(scan_end);
if str_end > str_start {
let doctype = &data[str_start..str_end];
if doctype == b"webm" {
found_webm = true;
break;
} else if doctype == b"matroska" {
found_mkv = true;
break;
}
}
}
}
idx += 1;
}
if found_webm {
return FormatDetection::new(MediaFormat::Webm, 1.0);
}
if found_mkv {
return FormatDetection::new(MediaFormat::Mkv, 1.0);
}
return FormatDetection::new(MediaFormat::Mkv, 0.8);
}
if let Some(fmt) = Self::probe_isobmff(data) {
return fmt;
}
if data.len() >= 3 && &data[..3] == b"FLV" {
return FormatDetection::new(MediaFormat::Flv, 1.0);
}
if data.len() >= 192 && data[0] == 0x47 {
if data[188] == 0x47 {
return FormatDetection::new(MediaFormat::Ts, 0.9);
}
if data[4] == 0x47 {
return FormatDetection::new(MediaFormat::M2ts, 0.85);
}
}
if data.len() >= 1 && data[0] == 0x47 {
return FormatDetection::new(MediaFormat::Ts, 0.5);
}
if data.len() >= 4 && data[..4] == [0x06, 0x0E, 0x2B, 0x34] {
return FormatDetection::new(MediaFormat::Mxf, 1.0);
}
if data.len() >= 3 && &data[..3] == b"ID3" {
return FormatDetection::new(MediaFormat::Mp3, 1.0);
}
if data.len() >= 2 && data[0] == 0xFF && (data[1] & 0xE0) == 0xE0 {
let layer = (data[1] >> 1) & 0x03;
if layer == 0x01 {
return FormatDetection::new(MediaFormat::Mp3, 0.9);
}
}
if data.len() >= 2 && data[0] == 0xFF && (data[1] & 0xF6) == 0xF0 {
return FormatDetection::new(MediaFormat::Aac, 0.9);
}
if data.len() >= 2 && &data[..2] == b"BM" {
return FormatDetection::new(MediaFormat::Bmp, 1.0);
}
if data.len() >= 4 {
if data[..4] == [0x49, 0x49, 0x2A, 0x00] {
return FormatDetection::new(MediaFormat::Tiff, 1.0);
}
if data[..4] == [0x4D, 0x4D, 0x00, 0x2A] {
return FormatDetection::new(MediaFormat::Tiff, 1.0);
}
}
if data.len() >= 2 && data[..2] == [0x50, 0x4B] {
return FormatDetection::new(MediaFormat::Zip, 1.0);
}
if data.len() >= 2 && data[..2] == [0x1F, 0x8B] {
return FormatDetection::new(MediaFormat::Gz, 1.0);
}
if data.len() >= 3 && &data[..3] == b"BZh" {
return FormatDetection::new(MediaFormat::Bz2, 1.0);
}
if data.len() >= 6 && data[..6] == [0xFD, b'7', b'z', b'X', b'Z', 0x00] {
return FormatDetection::new(MediaFormat::Xz, 1.0);
}
if data.len() >= 4 && data[..4] == [0xFD, 0x2F, 0xB5, 0x28] {
return FormatDetection::new(MediaFormat::Zstd, 1.0);
}
if data.len() >= 4 && &data[..4] == b".snd" {
return FormatDetection::new(MediaFormat::Au, 1.0);
}
if Self::looks_like_svg(data) {
return FormatDetection::new(MediaFormat::Svg, 0.85);
}
if data.len() >= 4 && (&data[..4] == b"SDPX" || &data[..4] == b"XPDS") {
return FormatDetection::new(MediaFormat::Dpx, 1.0);
}
if data.len() >= 4 && data[..4] == [0x76, 0x2F, 0x31, 0x01] {
return FormatDetection::new(MediaFormat::Exr, 1.0);
}
if data.len() >= 2 && data[0] == 0xFF && data[1] == 0x0A {
return FormatDetection::new(MediaFormat::Jxl, 1.0);
}
if data.len() >= 12 && data[..8] == [0x00, 0x00, 0x00, 0x0C, 0x4A, 0x58, 0x4C, 0x20] {
return FormatDetection::new(MediaFormat::Jxl, 1.0);
}
if data.len() >= 10 && &data[..10] == b"YUV4MPEG2 " {
return FormatDetection::new(MediaFormat::Y4m, 1.0);
}
if data.len() >= 4 && &data[..4] == b"caff" {
return FormatDetection::new(MediaFormat::Caf, 1.0);
}
if data.len() >= 4 && data[..4] == [0x00, 0x00, 0x01, 0xBA] {
return FormatDetection::new(MediaFormat::Ps, 0.9);
}
if Self::looks_like_srt(data) {
return FormatDetection::new(MediaFormat::Srt, 0.75);
}
if data.len() >= 6 && &data[..6] == b"WEBVTT" {
return FormatDetection::new(MediaFormat::Vtt, 1.0);
}
if Self::looks_like_ass(data) {
return FormatDetection::new(MediaFormat::Ass, 0.9);
}
FormatDetection::new(MediaFormat::Unknown, 0.0)
}
fn probe_isobmff(data: &[u8]) -> Option<FormatDetection> {
let scan_end = data.len().min(512);
let mut offset = 0usize;
while offset + 8 <= scan_end {
let size = u32::from_be_bytes([
data[offset],
data[offset + 1],
data[offset + 2],
data[offset + 3],
]) as usize;
let box_type = &data[offset + 4..offset + 8];
if box_type == b"ftyp" {
if offset + 12 <= data.len() {
let brand = &data[offset + 8..offset + 12];
if brand == b"qt " || brand == b"MSNV" {
return Some(FormatDetection::new(MediaFormat::Mov, 1.0));
}
}
return Some(FormatDetection::new(MediaFormat::Mp4, 1.0));
}
if box_type == b"moov" {
return Some(FormatDetection::new(MediaFormat::Mp4, 0.9));
}
if size < 8 {
break;
}
offset = offset.saturating_add(size);
}
None
}
fn looks_like_svg(data: &[u8]) -> bool {
let s = match std::str::from_utf8(data.get(..256).unwrap_or(data)) {
Ok(s) => s,
Err(_) => return false,
};
let trimmed = s.trim_start();
trimmed.starts_with("<?xml") && trimmed.contains("<svg") || trimmed.starts_with("<svg")
}
fn looks_like_ass(data: &[u8]) -> bool {
let s = match std::str::from_utf8(data.get(..256).unwrap_or(data)) {
Ok(s) => s,
Err(_) => return false,
};
let trimmed = s.trim_start();
trimmed.starts_with("[Script Info]")
}
fn looks_like_srt(data: &[u8]) -> bool {
let s = match std::str::from_utf8(data.get(..512).unwrap_or(data)) {
Ok(s) => s,
Err(_) => return false,
};
let mut lines = s.lines().filter(|l| !l.trim().is_empty());
if let Some(first) = lines.next() {
return first.trim().parse::<u64>().is_ok();
}
false
}
#[must_use]
pub fn detect_from_extension(ext: &str) -> MediaFormat {
match ext.to_ascii_lowercase().trim_start_matches('.') {
"mp4" | "m4v" | "m4p" | "m4b" => MediaFormat::Mp4,
"mkv" => MediaFormat::Mkv,
"avi" => MediaFormat::Avi,
"mov" | "qt" => MediaFormat::Mov,
"webm" => MediaFormat::Webm,
"flv" => MediaFormat::Flv,
"ts" | "mts" => MediaFormat::Ts,
"m2ts" | "m2t" => MediaFormat::M2ts,
"mxf" => MediaFormat::Mxf,
"ogg" | "ogv" | "ogx" => MediaFormat::Ogg,
"mp3" => MediaFormat::Mp3,
"flac" => MediaFormat::Flac,
"wav" | "wave" => MediaFormat::Wav,
"aac" => MediaFormat::Aac,
"opus" => MediaFormat::Opus,
"oga" => MediaFormat::Vorbis,
"aiff" | "aif" => MediaFormat::Aiff,
"au" | "snd" => MediaFormat::Au,
"jpg" | "jpeg" | "jfif" => MediaFormat::Jpeg,
"png" => MediaFormat::Png,
"gif" => MediaFormat::Gif,
"webp" => MediaFormat::Webp,
"bmp" | "dib" => MediaFormat::Bmp,
"tiff" | "tif" => MediaFormat::Tiff,
"svg" | "svgz" => MediaFormat::Svg,
"heic" | "heif" => MediaFormat::Heic,
"avif" => MediaFormat::Avif,
"srt" => MediaFormat::Srt,
"vtt" => MediaFormat::Vtt,
"ass" | "ssa" => MediaFormat::Ass,
"zip" => MediaFormat::Zip,
"tar" => MediaFormat::Tar,
"gz" | "gzip" => MediaFormat::Gz,
"bz2" | "bzip2" => MediaFormat::Bz2,
"xz" => MediaFormat::Xz,
"zst" | "zstd" => MediaFormat::Zstd,
"dpx" => MediaFormat::Dpx,
"exr" => MediaFormat::Exr,
"dng" => MediaFormat::Dng,
"jxl" => MediaFormat::Jxl,
"y4m" => MediaFormat::Y4m,
"ffv1" => MediaFormat::Ffv1,
"caf" => MediaFormat::Caf,
"mpg" | "mpeg" | "vob" => MediaFormat::Ps,
_ => MediaFormat::Unknown,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn mp4_ftyp_bytes() -> Vec<u8> {
let mut v = vec![0u8, 0, 0, 20]; v.extend_from_slice(b"ftyp");
v.extend_from_slice(b"isom"); v.extend_from_slice(b"\x00\x00\x02\x00"); v.extend_from_slice(b"isom"); v
}
fn mov_ftyp_bytes() -> Vec<u8> {
let mut v = vec![0u8, 0, 0, 16];
v.extend_from_slice(b"ftyp");
v.extend_from_slice(b"qt "); v.extend_from_slice(b"\x00\x00\x00\x00");
v
}
fn mkv_bytes() -> Vec<u8> {
let mut v: Vec<u8> = vec![0x1A, 0x45, 0xDF, 0xA3]; v.extend_from_slice(&[0x00u8; 10]);
v.push(0x42);
v.push(0x82);
v.push(8); v.extend_from_slice(b"matroska");
v
}
fn webm_bytes() -> Vec<u8> {
let mut v: Vec<u8> = vec![0x1A, 0x45, 0xDF, 0xA3];
v.extend_from_slice(&[0x00u8; 10]);
v.push(0x42);
v.push(0x82);
v.push(4);
v.extend_from_slice(b"webm");
v
}
#[test]
fn test_detect_mp4() {
let data = mp4_ftyp_bytes();
let det = FormatDetector::detect(&data);
assert_eq!(det.format, MediaFormat::Mp4);
assert!(det.confidence >= 0.9);
}
#[test]
fn test_detect_mov() {
let data = mov_ftyp_bytes();
let det = FormatDetector::detect(&data);
assert_eq!(det.format, MediaFormat::Mov);
assert!(det.confidence >= 0.9);
}
#[test]
fn test_detect_mkv() {
let data = mkv_bytes();
let det = FormatDetector::detect(&data);
assert_eq!(det.format, MediaFormat::Mkv);
assert!(det.confidence >= 0.8);
}
#[test]
fn test_detect_webm() {
let data = webm_bytes();
let det = FormatDetector::detect(&data);
assert_eq!(det.format, MediaFormat::Webm);
assert!(det.confidence >= 0.9);
}
#[test]
fn test_detect_avi() {
let mut data = b"RIFF".to_vec();
data.extend_from_slice(&[0x00u8; 4]);
data.extend_from_slice(b"AVI ");
let det = FormatDetector::detect(&data);
assert_eq!(det.format, MediaFormat::Avi);
}
#[test]
fn test_detect_flv() {
let data = b"FLV\x01\x05\x00\x00\x00\x09";
let det = FormatDetector::detect(data);
assert_eq!(det.format, MediaFormat::Flv);
}
#[test]
fn test_detect_mxf() {
let data = [0x06u8, 0x0E, 0x2B, 0x34, 0x02, 0x05, 0x01, 0x01];
let det = FormatDetector::detect(&data);
assert_eq!(det.format, MediaFormat::Mxf);
}
#[test]
fn test_detect_ogg() {
let mut data = b"OggS".to_vec();
data.extend_from_slice(&[0u8; 50]);
let det = FormatDetector::detect(&data);
assert!(
det.format == MediaFormat::Ogg
|| det.format == MediaFormat::Opus
|| det.format == MediaFormat::Vorbis
);
}
#[test]
fn test_detect_flac() {
let data = b"fLaC\x00\x00\x00\x22";
let det = FormatDetector::detect(data);
assert_eq!(det.format, MediaFormat::Flac);
}
#[test]
fn test_detect_wav() {
let mut data = b"RIFF".to_vec();
data.extend_from_slice(&[0x24u8, 0x00, 0x00, 0x00]);
data.extend_from_slice(b"WAVE");
let det = FormatDetector::detect(&data);
assert_eq!(det.format, MediaFormat::Wav);
}
#[test]
fn test_detect_mp3_id3() {
let data = b"ID3\x04\x00\x00";
let det = FormatDetector::detect(data);
assert_eq!(det.format, MediaFormat::Mp3);
}
#[test]
fn test_detect_aiff() {
let mut data = b"FORM".to_vec();
data.extend_from_slice(&[0x00u8; 4]);
data.extend_from_slice(b"AIFF");
let det = FormatDetector::detect(&data);
assert_eq!(det.format, MediaFormat::Aiff);
}
#[test]
fn test_detect_jpeg() {
let data = [0xFF, 0xD8, 0xFF, 0xE0];
let det = FormatDetector::detect(&data);
assert_eq!(det.format, MediaFormat::Jpeg);
}
#[test]
fn test_detect_png() {
let data = [0x89u8, b'P', b'N', b'G', 0x0D, 0x0A, 0x1A, 0x0A, 0x00];
let det = FormatDetector::detect(&data);
assert_eq!(det.format, MediaFormat::Png);
}
#[test]
fn test_detect_gif() {
let data = b"GIF89a\x01\x00\x01\x00";
let det = FormatDetector::detect(data);
assert_eq!(det.format, MediaFormat::Gif);
}
#[test]
fn test_detect_webp() {
let mut data = b"RIFF".to_vec();
data.extend_from_slice(&[0x24u8, 0, 0, 0]);
data.extend_from_slice(b"WEBP");
let det = FormatDetector::detect(&data);
assert_eq!(det.format, MediaFormat::Webp);
}
#[test]
fn test_detect_bmp() {
let data = b"BM\x36\x00\x00\x00";
let det = FormatDetector::detect(data);
assert_eq!(det.format, MediaFormat::Bmp);
}
#[test]
fn test_detect_tiff_little_endian() {
let data = [0x49u8, 0x49, 0x2A, 0x00, 0x08, 0x00, 0x00, 0x00];
let det = FormatDetector::detect(&data);
assert_eq!(det.format, MediaFormat::Tiff);
}
#[test]
fn test_detect_zip() {
let data = [0x50u8, 0x4B, 0x03, 0x04];
let det = FormatDetector::detect(&data);
assert_eq!(det.format, MediaFormat::Zip);
}
#[test]
fn test_detect_gz() {
let data = [0x1Fu8, 0x8B, 0x08, 0x00];
let det = FormatDetector::detect(&data);
assert_eq!(det.format, MediaFormat::Gz);
}
#[test]
fn test_detect_zstd() {
let data = [0xFDu8, 0x2F, 0xB5, 0x28, 0x00];
let det = FormatDetector::detect(&data);
assert_eq!(det.format, MediaFormat::Zstd);
}
#[test]
fn test_extension_fallback_mp4() {
assert_eq!(
FormatDetector::detect_from_extension("mp4"),
MediaFormat::Mp4
);
}
#[test]
fn test_extension_fallback_unknown() {
assert_eq!(
FormatDetector::detect_from_extension("xyz"),
MediaFormat::Unknown
);
}
#[test]
fn test_is_video() {
assert!(MediaFormat::Mp4.is_video());
assert!(!MediaFormat::Mp4.is_audio());
assert!(!MediaFormat::Mp4.is_image());
}
#[test]
fn test_is_audio() {
assert!(MediaFormat::Flac.is_audio());
assert!(!MediaFormat::Flac.is_video());
assert!(!MediaFormat::Flac.is_image());
}
#[test]
fn test_is_image() {
assert!(MediaFormat::Jpeg.is_image());
assert!(!MediaFormat::Jpeg.is_video());
assert!(!MediaFormat::Jpeg.is_audio());
}
#[test]
fn test_detect_unknown() {
let data = [0x00u8, 0x01, 0x02, 0x03, 0x04];
let det = FormatDetector::detect(&data);
assert_eq!(det.format, MediaFormat::Unknown);
}
#[test]
fn test_detect_empty() {
let det = FormatDetector::detect(&[]);
assert_eq!(det.format, MediaFormat::Unknown);
assert_eq!(det.confidence, 0.0);
}
#[test]
fn test_mime_type_jpeg() {
let det = FormatDetector::detect(&[0xFF, 0xD8, 0xFF, 0xE0]);
assert_eq!(det.mime_type, "image/jpeg");
}
#[test]
fn test_mime_type_mp4() {
let data = mp4_ftyp_bytes();
let det = FormatDetector::detect(&data);
assert_eq!(det.mime_type, "video/mp4");
}
#[test]
fn test_detect_opus_in_ogg() {
let mut data = b"OggS".to_vec();
data.extend_from_slice(&[0u8; 24]);
data.extend_from_slice(b"OpusHead");
let det = FormatDetector::detect(&data);
assert_eq!(det.format, MediaFormat::Opus);
}
#[test]
fn test_detect_dpx_big_endian() {
let mut data = b"SDPX".to_vec();
data.extend_from_slice(&[0u8; 20]);
let det = FormatDetector::detect(&data);
assert_eq!(det.format, MediaFormat::Dpx);
assert_eq!(det.confidence, 1.0);
}
#[test]
fn test_detect_dpx_little_endian() {
let mut data = b"XPDS".to_vec();
data.extend_from_slice(&[0u8; 20]);
let det = FormatDetector::detect(&data);
assert_eq!(det.format, MediaFormat::Dpx);
}
#[test]
fn test_detect_exr() {
let data = [0x76u8, 0x2F, 0x31, 0x01, 0x02, 0x00, 0x00, 0x00];
let det = FormatDetector::detect(&data);
assert_eq!(det.format, MediaFormat::Exr);
assert_eq!(det.confidence, 1.0);
}
#[test]
fn test_detect_jxl_codestream() {
let data = [0xFFu8, 0x0A, 0x00, 0x00];
let det = FormatDetector::detect(&data);
assert_eq!(det.format, MediaFormat::Jxl);
}
#[test]
fn test_detect_jxl_container() {
let mut data = vec![0x00u8, 0x00, 0x00, 0x0C, 0x4A, 0x58, 0x4C, 0x20];
data.extend_from_slice(&[0x0D, 0x0A, 0x87, 0x0A]);
let det = FormatDetector::detect(&data);
assert_eq!(det.format, MediaFormat::Jxl);
}
#[test]
fn test_detect_y4m() {
let data = b"YUV4MPEG2 W320 H240 F30:1 Ip A0:0 C420jpeg\n";
let det = FormatDetector::detect(data);
assert_eq!(det.format, MediaFormat::Y4m);
assert_eq!(det.confidence, 1.0);
}
#[test]
fn test_detect_caf() {
let mut data = b"caff".to_vec();
data.extend_from_slice(&[0x00, 0x01, 0x00, 0x00]);
let det = FormatDetector::detect(&data);
assert_eq!(det.format, MediaFormat::Caf);
}
#[test]
fn test_detect_mpeg_ps() {
let data = [0x00u8, 0x00, 0x01, 0xBA, 0x44, 0x00, 0x04, 0x00];
let det = FormatDetector::detect(&data);
assert_eq!(det.format, MediaFormat::Ps);
}
#[test]
fn test_detect_ass_subtitle() {
let data = b"[Script Info]\n; Script generated by Aegisub\nTitle: Test\n";
let det = FormatDetector::detect(data);
assert_eq!(det.format, MediaFormat::Ass);
}
#[test]
fn test_dpx_is_image() {
assert!(MediaFormat::Dpx.is_image());
assert!(!MediaFormat::Dpx.is_video());
assert!(!MediaFormat::Dpx.is_audio());
}
#[test]
fn test_exr_metadata() {
assert_eq!(MediaFormat::Exr.extension(), "exr");
assert_eq!(MediaFormat::Exr.mime_type(), "image/x-exr");
assert!(MediaFormat::Exr.description().contains("OpenEXR"));
}
#[test]
fn test_extension_fallback_dpx() {
assert_eq!(
FormatDetector::detect_from_extension("dpx"),
MediaFormat::Dpx
);
}
#[test]
fn test_extension_fallback_exr() {
assert_eq!(
FormatDetector::detect_from_extension("exr"),
MediaFormat::Exr
);
}
#[test]
fn test_extension_fallback_jxl() {
assert_eq!(
FormatDetector::detect_from_extension("jxl"),
MediaFormat::Jxl
);
}
#[test]
fn test_extension_fallback_y4m() {
assert_eq!(
FormatDetector::detect_from_extension("y4m"),
MediaFormat::Y4m
);
}
#[test]
fn test_extension_fallback_caf() {
assert_eq!(
FormatDetector::detect_from_extension("caf"),
MediaFormat::Caf
);
}
#[test]
fn test_extension_fallback_mpg() {
assert_eq!(
FormatDetector::detect_from_extension("mpg"),
MediaFormat::Ps
);
assert_eq!(
FormatDetector::detect_from_extension("mpeg"),
MediaFormat::Ps
);
}
#[test]
fn test_detect_single_byte() {
let det = FormatDetector::detect(&[0x00]);
assert!(det.confidence <= 1.0);
}
#[test]
fn test_detect_two_bytes_only() {
let det = FormatDetector::detect(&[0xFF, 0xD8]); assert_eq!(det.format, MediaFormat::Jpeg);
}
#[test]
fn test_detect_truncated_png() {
let data = [0x89u8, b'P', b'N', b'G'];
let det = FormatDetector::detect(&data);
assert_ne!(det.format, MediaFormat::Png);
}
#[test]
fn test_detect_truncated_riff() {
let data = b"RIFF\x00\x00";
let det = FormatDetector::detect(data);
assert_ne!(det.format, MediaFormat::Wav);
assert_ne!(det.format, MediaFormat::Avi);
}
#[test]
fn test_new_formats_all_have_extensions() {
let formats = [
MediaFormat::Dpx,
MediaFormat::Exr,
MediaFormat::Dng,
MediaFormat::Jxl,
MediaFormat::Y4m,
MediaFormat::Ffv1,
MediaFormat::Caf,
MediaFormat::Ps,
];
for fmt in formats {
assert!(!fmt.extension().is_empty(), "{:?} has empty extension", fmt);
assert!(!fmt.mime_type().is_empty(), "{:?} has empty mime_type", fmt);
assert!(
!fmt.description().is_empty(),
"{:?} has empty description",
fmt
);
}
}
}