use crate::error::{Error, Result};
use crate::tag::{Tag, TagGroup, TagId};
use crate::value::Value;
fn mk_video(name: &str, value: Value, print: String) -> Tag {
Tag {
id: TagId::Text(name.to_string()),
name: name.to_string(),
description: name.to_string(),
group: TagGroup {
family0: "MPEG".into(),
family1: "MPEG".into(),
family2: "Video".into(),
},
raw_value: value,
print_value: print,
priority: 0,
}
}
fn mk_audio(name: &str, value: Value, print: String) -> Tag {
Tag {
id: TagId::Text(name.to_string()),
name: name.to_string(),
description: name.to_string(),
group: TagGroup {
family0: "MPEG".into(),
family1: "MPEG".into(),
family2: "Audio".into(),
},
raw_value: value,
print_value: print,
priority: 0,
}
}
fn format_bitrate(bps: u32) -> String {
if bps >= 1_000_000 {
let mbps = bps as f64 / 1_000_000.0;
if (mbps - mbps.round()).abs() < 0.0001 {
format!("{} Mbps", mbps as u32)
} else {
format!("{:.3} Mbps", mbps)
}
} else if bps >= 1000 {
let kbps = bps as f64 / 1000.0;
if (kbps - kbps.round()).abs() < 0.0001 {
format!("{} kbps", kbps as u32)
} else {
format!("{:.3} kbps", kbps)
}
} else {
format!("{} bps", bps)
}
}
fn process_mpeg_video(data: &[u8]) -> Vec<Tag> {
let mut tags = Vec::new();
if data.len() < 8 {
return tags;
}
let w1 = u32::from_be_bytes([data[0], data[1], data[2], data[3]]);
let w2 = u32::from_be_bytes([data[4], data[5], data[6], data[7]]);
let width = (w1 >> 20) & 0xFFF;
let height = (w1 >> 8) & 0xFFF;
let aspect_code = (w1 >> 4) & 0xF;
let frame_rate_code = w1 & 0xF;
if aspect_code == 0 || aspect_code == 15 || frame_rate_code == 0 || frame_rate_code > 8 {
return tags;
}
let video_bitrate_raw = (w2 >> 14) & 0x3FFFF;
tags.push(mk_video("ImageWidth", Value::U32(width), width.to_string()));
tags.push(mk_video(
"ImageHeight",
Value::U32(height),
height.to_string(),
));
let aspect_str = match aspect_code {
1 => "1:1",
2 => "0.6735",
3 => "16:9, 625 line, PAL",
4 => "0.7615",
5 => "0.8055",
6 => "16:9, 525 line, NTSC",
7 => "0.8935",
8 => "4:3, 625 line, PAL, CCIR601",
9 => "0.9815",
10 => "1.0255",
11 => "1.0695",
12 => "4:3, 525 line, NTSC, CCIR601",
13 => "1.1575",
14 => "1.2015",
_ => "Unknown",
};
let aspect_val = match aspect_code {
1 => 1.0,
2 => 0.6735,
3 => 0.7031,
4 => 0.7615,
5 => 0.8055,
6 => 0.8437,
7 => 0.8935,
8 => 0.9157,
9 => 0.9815,
10 => 1.0255,
11 => 1.0695,
12 => 1.0950,
13 => 1.1575,
14 => 1.2015,
_ => 0.0,
};
tags.push(mk_video(
"AspectRatio",
Value::F64(aspect_val),
aspect_str.to_string(),
));
let frame_rate = match frame_rate_code {
1 => 23.976,
2 => 24.0,
3 => 25.0,
4 => 29.97,
5 => 30.0,
6 => 50.0,
7 => 59.94,
8 => 60.0,
_ => 0.0,
};
tags.push(mk_video(
"FrameRate",
Value::F64(frame_rate),
format!("{} fps", frame_rate),
));
if video_bitrate_raw == 0x3FFFF {
tags.push(mk_video(
"VideoBitrate",
Value::String("Variable".into()),
"Variable".into(),
));
} else {
let bitrate = video_bitrate_raw * 400;
tags.push(mk_video(
"VideoBitrate",
Value::U32(bitrate),
format_bitrate(bitrate),
));
}
tags
}
fn parse_mpeg_audio_header(word: u32) -> Option<Vec<Tag>> {
if (word & 0xFFE00000) != 0xFFE00000 {
return None;
}
if (word & 0x180000) == 0x080000 || (word & 0x060000) == 0x000000 || (word & 0x00F000) == 0x000000 || (word & 0x00F000) == 0x00F000 || (word & 0x000C00) == 0x000C00 || (word & 0x000003) == 0x000002
{
return None;
}
let mut tags = Vec::new();
let version_bits = (word >> 19) & 0x3;
let version_str = match version_bits {
0 => "2.5",
2 => "2",
3 => "1",
_ => return None,
};
tags.push(mk_audio(
"MPEGAudioVersion",
Value::String(version_str.into()),
version_str.into(),
));
let layer_bits = (word >> 17) & 0x3;
let layer = match layer_bits {
1 => 3u8,
2 => 2,
3 => 1,
_ => return None,
};
tags.push(mk_audio("AudioLayer", Value::U8(layer), layer.to_string()));
let bitrate_index = ((word >> 12) & 0xF) as usize;
let bitrate = lookup_audio_bitrate(version_bits, layer_bits, bitrate_index);
if let Some(br) = bitrate {
if br > 0 {
tags.push(mk_audio("AudioBitrate", Value::U32(br), format_bitrate(br)));
}
}
let sample_rate_index = (word >> 10) & 0x3;
let sample_rate = match version_bits {
3 => match sample_rate_index {
0 => Some(44100u32),
1 => Some(48000),
2 => Some(32000),
_ => None,
},
2 => match sample_rate_index {
0 => Some(22050),
1 => Some(24000),
2 => Some(16000),
_ => None,
},
0 => match sample_rate_index {
0 => Some(11025),
1 => Some(12000),
2 => Some(8000),
_ => None,
},
_ => None,
};
if let Some(sr) = sample_rate {
tags.push(mk_audio("SampleRate", Value::U32(sr), sr.to_string()));
}
let channel_mode = (word >> 6) & 0x3;
let channel_str = match channel_mode {
0 => "Stereo",
1 => "Joint Stereo",
2 => "Dual Channel",
3 => "Single Channel",
_ => "Unknown",
};
tags.push(mk_audio(
"ChannelMode",
Value::String(channel_str.into()),
channel_str.into(),
));
let copyright = (word >> 3) & 0x1;
let copyright_str = if copyright == 1 { "True" } else { "False" };
tags.push(mk_audio(
"CopyrightFlag",
Value::String(copyright_str.into()),
copyright_str.into(),
));
let original = (word >> 2) & 0x1;
let original_str = if original == 1 { "True" } else { "False" };
tags.push(mk_audio(
"OriginalMedia",
Value::String(original_str.into()),
original_str.into(),
));
let emphasis = word & 0x3;
let emphasis_str = match emphasis {
0 => "None",
1 => "50/15 ms",
2 => "reserved",
3 => "CCIT J.17",
_ => "Unknown",
};
tags.push(mk_audio(
"Emphasis",
Value::String(emphasis_str.into()),
emphasis_str.into(),
));
Some(tags)
}
fn lookup_audio_bitrate(version_bits: u32, layer_bits: u32, index: usize) -> Option<u32> {
if index == 0 || index >= 15 {
return None;
}
let table: &[u32; 14] = match (version_bits, layer_bits) {
(3, 3) => &[
32000, 64000, 96000, 128000, 160000, 192000, 224000, 256000, 288000, 320000, 352000,
384000, 416000, 448000,
],
(3, 2) => &[
32000, 48000, 56000, 64000, 80000, 96000, 112000, 128000, 160000, 192000, 224000,
256000, 320000, 384000,
],
(3, 1) => &[
32000, 40000, 48000, 56000, 64000, 80000, 96000, 112000, 128000, 160000, 192000,
224000, 256000, 320000,
],
(0 | 2, 3) => &[
32000, 48000, 56000, 64000, 80000, 96000, 112000, 128000, 144000, 160000, 176000,
192000, 224000, 256000,
],
(0 | 2, 1 | 2) => &[
8000, 16000, 24000, 32000, 40000, 48000, 56000, 64000, 80000, 96000, 112000, 128000,
144000, 160000,
],
_ => return None,
};
Some(table[index - 1])
}
fn find_mpeg_audio(data: &[u8]) -> Option<Vec<Tag>> {
let len = data.len();
let mut pos = 0;
while pos + 3 < len {
if data[pos] == 0xFF && (data[pos + 1] & 0xE0) == 0xE0 {
let word = u32::from_be_bytes([data[pos], data[pos + 1], data[pos + 2], data[pos + 3]]);
if let Some(tags) = parse_mpeg_audio_header(word) {
return Some(tags);
}
pos += 1;
} else {
pos += 1;
}
}
None
}
pub fn read_mpeg(data: &[u8]) -> Result<Vec<Tag>> {
if data.len() < 4 {
return Err(Error::InvalidData("not an MPEG file".into()));
}
if !(data[0] == 0x00
&& data[1] == 0x00
&& data[2] == 0x01
&& (data[3] == 0xBA || data[3] == 0xBB || (data[3] & 0xF0) == 0xE0 || data[3] == 0xB3))
{
return Err(Error::InvalidData("not an MPEG file".into()));
}
let mut tags = Vec::new();
let mut found_video = false;
let mut found_audio = false;
let scan_len = data.len().min(262144);
let mut first_start = scan_len;
let mut pos = 0;
while pos + 3 < scan_len {
if data[pos] == 0x00 && data[pos + 1] == 0x00 && data[pos + 2] == 0x01 {
let code = data[pos + 3];
if code == 0xB3 || code == 0xC0 {
first_start = pos;
break;
}
}
pos += 1;
}
if first_start > 3 {
let pre_data = &data[..first_start.min(scan_len)];
if let Some(audio_tags) = find_mpeg_audio(pre_data) {
tags.extend(audio_tags);
found_audio = true;
}
}
pos = 0;
while pos + 3 < scan_len {
if data[pos] == 0x00 && data[pos + 1] == 0x00 && data[pos + 2] == 0x01 {
let code = data[pos + 3];
if code == 0xB3 && !found_video {
let remaining = &data[pos + 4..scan_len.min(pos + 4 + 256)];
let video_tags = process_mpeg_video(remaining);
if !video_tags.is_empty() {
tags.extend(video_tags);
found_video = true;
}
} else if code == 0xC0 && !found_audio {
let end = scan_len.min(pos + 4 + 256);
if pos + 4 < end {
let audio_data = &data[pos + 4..end];
if let Some(audio_tags) = find_mpeg_audio(audio_data) {
tags.extend(audio_tags);
found_audio = true;
}
}
}
if found_video && found_audio {
break;
}
pos += 4;
} else {
pos += 1;
}
}
if tags.is_empty() {
return Err(Error::InvalidData(
"no MPEG video or audio headers found".into(),
));
}
Ok(tags)
}