// mp4_ilst.mbt
//
// MP4/M4A 的 iTunes 风格标签(`moov/udta/meta/ilst`)读写。
//
// 路径:moov → udta → meta(FullBox,4 字节 version/flags 之后才是子盒)→ ilst
//
// ilst 的每个条目是"用标签名当盒类型"的容器盒:
// ilst → ©nam → data(FullBox:type + locale + 载荷)
//
// `meta` 是唯一一个"盒子套盒子但中间多了 4 字节"的结构——这是 MP4 解析
// 最常见的坑,漏掉这 4 字节会让后面全部错位。
///|
/// 一个 MP4 盒:类型码 + 在文件中的区间。
pub struct Mp4Box {
/// 4 字符类型码(如 `moov`、`©nam`)。
kind : String
/// 盒载荷起点(不含 8 字节头)。
offset : Int32
/// 盒载荷长度。
length : Int32
/// 盒在文件中的起点(含头)。
start : Int32
}
///|
/// 读 4 字节类型码。
fn mp4_type(data : BytesView, pos : Int32) -> String {
let buf = StringBuilder()
for i in 0..<4 {
buf.write_char(Int32::unsafe_to_char(data.at(pos + i).to_int()))
}
buf.to_string()
}
///|
/// 在 `[start, end)` 区间内枚举同级盒。
fn list_boxes(
data : BytesView,
start : Int32,
end : Int32,
) -> Array[Mp4Box] raise TagError {
let out : Array[Mp4Box] = []
let mut pos = start
while pos + 8 <= end {
let size = read_be(data, pos, 4)
let kind = mp4_type(data, pos + 4)
// size==0 表示"到文件结尾";size==1 表示 64 位长度(本实现不支持)
let (actual_size, header) = if size == 0 {
(end - pos, 8)
} else if size == 1 {
raise Unsupported(what="MP4 盒使用 64 位长度")
} else {
(size, 8)
}
if actual_size < header || pos + actual_size > end {
raise BadHeader(reason="MP4 盒 \{kind} 尺寸 \{actual_size} 越界")
}
out.push({
kind,
offset: pos + header,
length: actual_size - header,
start: pos,
})
pos += actual_size
}
out
}
///|
/// 在盒列表里找第一个指定类型;找不到返回 `None`。
fn find_box(boxes : Array[Mp4Box], kind : String) -> Mp4Box? {
for b in boxes {
if b.kind == kind {
return Some(b)
}
}
None
}
///|
/// 定位 ilst 盒;文件没有返回 `None`。
pub fn find_ilst(data : BytesView) -> Mp4Box? raise TagError {
let top = list_boxes(data, 0, data.length())
let moov = match find_box(top, "moov") {
None => return None
Some(b) => b
}
let moov_children = list_boxes(data, moov.offset, moov.offset + moov.length)
let udta = match find_box(moov_children, "udta") {
None => return None
Some(b) => b
}
let udta_children = list_boxes(data, udta.offset, udta.offset + udta.length)
let meta = match find_box(udta_children, "meta") {
None => return None
Some(b) => b
}
// meta 是 FullBox:载荷前 4 字节是 version(1) + flags(3),子盒从 +4 开始
let meta_children = list_boxes(
data,
meta.offset + 4,
meta.offset + meta.length,
)
find_box(meta_children, "ilst")
}
///|
/// 一个 iTunes 标签条目。
pub struct Mp4Tag {
/// 键(如 `©nam`,其中 © 是 0xA9)。
key : String
/// 值字节(data 盒里去掉 type/locale 之后的部分)。
value : Bytes
/// data 盒的 type 字段(1=UTF-8 文本,13/14=JPEG/PNG 封面等)。
data_type : Int32
}
///|
/// 解析 ilst 下全部条目。
pub fn parse_ilst(
data : BytesView,
ilst : Mp4Box,
) -> Array[Mp4Tag] raise TagError {
let entries = list_boxes(data, ilst.offset, ilst.offset + ilst.length)
let out : Array[Mp4Tag] = []
for e in entries {
// 每个条目是容器盒,里面应当有一个 data 盒
let children = list_boxes(data, e.offset, e.offset + e.length) catch {
_ => continue
}
let data_box = match find_box(children, "data") {
None => continue
Some(b) => b
}
// data 盒:version(1)+flags(3) 共 4 字节(type 就在 flags 里),
// 然后是 locale(4),之后才是值。
if data_box.length < 8 {
continue
}
let data_type = read_be(data, data_box.offset + 1, 3)
let value_start = data_box.offset + 8
let value = slice_bytes(
data,
value_start,
data_box.offset + data_box.length,
)
out.push({ key: e.kind, value, data_type, })
}
out
}
///|
/// 取文本类型的标签值。
pub fn text_of(entries : Array[Mp4Tag], key : String) -> String? {
for e in entries {
if e.key == key {
return Some(@utf8.decode_lossy(e.value.exact_view()))
}
}
None
}
///|
/// 取第一个封面(data type 13=JPEG、14=PNG)。
pub fn cover_of(entries : Array[Mp4Tag]) -> Bytes? {
for e in entries {
if e.key == "covr" {
return Some(e.value)
}
}
None
}
///|
/// 读 MP4 文件的全部 ilst 条目;没有则返回空数组。
pub fn read_mp4_tags(data : BytesView) -> Array[Mp4Tag] raise TagError {
match find_ilst(data) {
None => []
Some(ilst) => parse_ilst(data, ilst)
}
}