// mp4_write.mbt
//
// MP4 ilst 写入。
//
// 难点在于 moov 的 `udta/meta/ilst` 可能根本不存在:
// 这时要新建整条路径,并把每一层父盒的尺寸重新算对。
// 本实现的做法是"自底向上重算":只重建从 ilst 到 moov 这条链上的盒,
// 其它同级盒(如 trak、mdat)的原始字节原样保留。
///|
/// 拼一个盒:类型 + 载荷(尺寸自动计算)。
fn make_box(kind : String, payload : Bytes) -> Bytes {
let total = payload.length() + 8
let out = @buffer.Buffer()
out.write_bytes(write_u32be(total))
for c in kind {
out.write_byte(c.to_int().to_byte())
}
out.write_bytes(payload)
out.to_bytes()
}
///|
/// 对一个 ilst 条目(如 `©nam`)构造完整字节。
///
/// `data` 盒结构:version(1 字节) + **flags(3 字节,高 8 位存 data type)**
/// + locale(4 字节) + 值。注意 type 在 flags 的**最低字节**,不是单独的 4 字节
/// ——多写 4 字节会让后续所有解析错位,且播放器读出空值。
fn make_ilst_entry(key : String, data_type : Int32, value : Bytes) -> Bytes {
let data_box = @buffer.Buffer()
// version = 0,flags 低 8 位 = data type
data_box.write_byte(b'\x00')
data_box.write_byte(b'\x00')
data_box.write_byte(b'\x00')
data_box.write_byte((data_type & 0xFF).to_byte())
data_box.write_bytes(write_u32be(0)) // locale
data_box.write_bytes(value)
let data_bytes = make_box("data", data_box.to_bytes())
make_box(key, data_bytes)
}
///|
/// 拼一个新的 ilst 盒。
fn make_ilst(entries : Array[Bytes]) -> Bytes {
let inner = @buffer.Buffer()
for e in entries {
inner.write_bytes(e)
}
make_box("ilst", inner.to_bytes())
}
///|
/// 用一批条目重写 MP4 文件的 ilst。
///
/// `updates` 是按 key 的完整替换(同 key 会被整体替换,不会叠加);
/// 未在 `updates` 里出现的既有条目原样保留。
/// 返回完整的新文件字节。
pub fn set_mp4_tags(
data : BytesView,
updates : Array[Mp4Tag],
) -> Bytes raise TagError {
// 收集既有条目
let existing = read_mp4_tags(data)
let final_entries : Array[Mp4Tag] = []
for e in existing {
let mut replaced = false
for u in updates {
if u.key == e.key {
replaced = true
}
}
if !replaced {
final_entries.push(e)
}
}
for u in updates {
final_entries.push(u)
}
// 拼新 ilst
let entry_bytes : Array[Bytes] = []
for e in final_entries {
entry_bytes.push(make_ilst_entry(e.key, e.data_type, e.value))
}
let new_ilst = make_ilst(entry_bytes)
// 重建 moov/udta/meta 链
rebuild_moov(data, new_ilst)
}
///|
/// 重建 moov:替换其中的 udta/meta/ilst,其余盒原样保留。
///
/// **媒体定位保持**:若 moov 在 mdat 之前(faststart 布局),moov 变长会让
/// mdat 后移,此时 stco/co64 里的绝对 chunk offset 必须同步增加 delta,
/// 否则播放器读到错位字节(实测:标签增长后 ffmpeg 报
/// `Error submitting packet to decoder`)。moov 在 mdat 之后时 mdat 不动,
/// 无需修正。
fn rebuild_moov(data : BytesView, new_ilst : Bytes) -> Bytes raise TagError {
let top = list_boxes(data, 0, data.length())
// fragmented MP4 的 chunk 位置记在 moof/traf/trun(及 tfhd 的 base_data_offset),
// 不在 stco/co64 里。本实现不维护这些偏移,必须**明确拒绝**——静默改写会产出
// 能通过解析、但音频已损坏的文件(实测 ffmpeg 报 "Error submitting packet to decoder")。
for b in top {
if b.kind == "moof" {
raise Unsupported(what="fragmented MP4(moof)的标签改写")
}
}
// 找 mdat 位置(判断 moov 是否在 mdat 之前)
let mut first_mdat_start = -1
for b in top {
if b.kind == "mdat" {
first_mdat_start = b.start
break
}
}
let out = @buffer.Buffer()
let mut found_moov = false
for b in top {
if b.kind == "moov" {
found_moov = true
let old_moov_total = b.length + 8
let moov_children = list_boxes(data, b.offset, b.offset + b.length)
// moov 里有 mvex 也是 fragmented MP4 的标志(空 moov + moof 序列)
if find_box(moov_children, "mvex") is Some(_) {
raise Unsupported(what="fragmented MP4(mvex)的标签改写")
}
// 处理 udta
let new_udta = match find_box(moov_children, "udta") {
None => make_box("udta", make_meta_with_ilst(new_ilst))
Some(udta) => {
let udta_children = list_boxes(
data,
udta.offset,
udta.offset + udta.length,
)
let meta_bytes = match find_box(udta_children, "meta") {
None => make_meta_with_ilst(new_ilst)
Some(meta) => {
let meta_children = list_boxes(
data,
meta.offset + 4,
meta.offset + meta.length,
)
// 保留 meta 内除 ilst 外的子盒
let inner = @buffer.Buffer()
for c in meta_children {
if c.kind != "ilst" {
inner.write_bytes(
slice_bytes(data, c.start, c.start + 8 + c.length),
)
}
}
inner.write_bytes(new_ilst)
// meta 是 FullBox:version/flags 4 字节 + 子盒
let payload = @buffer.Buffer()
payload.write_bytes(Bytes::from_array([0x00, 0x00, 0x00, 0x00]))
payload.write_bytes(inner.to_bytes())
make_box("meta", payload.to_bytes())
}
}
// udta 内除 meta 外的子盒保留
let udta_inner = @buffer.Buffer()
for c in udta_children {
if c.kind != "meta" {
udta_inner.write_bytes(
slice_bytes(data, c.start, c.start + 8 + c.length),
)
}
}
udta_inner.write_bytes(meta_bytes)
make_box("udta", udta_inner.to_bytes())
}
}
// moov 内除 udta 外的子盒保留
let moov_inner = @buffer.Buffer()
for c in moov_children {
if c.kind != "udta" {
moov_inner.write_bytes(
slice_bytes(data, c.start, c.start + 8 + c.length),
)
}
}
moov_inner.write_bytes(new_udta)
let new_moov = make_box("moov", moov_inner.to_bytes())
let delta = new_moov.length() - old_moov_total
let moov_before_mdat = first_mdat_start > b.start
if delta != 0 && moov_before_mdat {
// moov 变长且在 mdat 之前:修正 stco/co64 的绝对偏移
let payload = slice_bytes(new_moov.exact_view(), 8, new_moov.length())
let fixed = fix_offsets_in_tree(
payload.exact_view(),
0,
payload.length(),
delta,
)
out.write_bytes(make_box("moov", fixed))
} else {
out.write_bytes(new_moov)
}
} else {
out.write_bytes(slice_bytes(data, b.start, b.start + 8 + b.length))
}
}
if !found_moov {
raise BadHeader(reason="MP4 文件缺少 moov 盒")
}
out.to_bytes()
}
///|
/// 容器盒(可递归进入寻找 stco/co64)。
fn is_container_box(kind : String) -> Bool {
kind == "moov" ||
kind == "trak" ||
kind == "mdia" ||
kind == "minf" ||
kind == "stbl"
}
///|
/// 递归重建盒树,遇到 stco/co64 时把每个 chunk offset += delta。
fn fix_offsets_in_tree(
data : BytesView,
start : Int32,
end : Int32,
delta : Int32,
) -> Bytes raise TagError {
let boxes = list_boxes(data, start, end)
let out = @buffer.Buffer()
for b in boxes {
if b.kind == "stco" || b.kind == "co64" {
out.write_bytes(fix_stco_box(data, b, delta))
} else if is_container_box(b.kind) {
let inner = fix_offsets_in_tree(
data,
b.offset,
b.offset + b.length,
delta,
)
out.write_bytes(make_box(b.kind, inner))
} else {
out.write_bytes(slice_bytes(data, b.start, b.start + 8 + b.length))
}
}
out.to_bytes()
}
///|
/// 修正单个 stco/co64 盒内的偏移条目。
///
/// 结构:size(4) + type(4) + version/flags(4) + entry_count(4) + entries。
/// stco 每条 4 字节 BE u32,co64 每条 8 字节 BE u64。
fn fix_stco_box(
data : BytesView,
b : Mp4Box,
delta : Int32,
) -> Bytes raise TagError {
let raw = slice_bytes(data, b.start, b.start + 8 + b.length)
let view = raw.exact_view()
let entry_count = read_be(view, 12, 4)
let out = @buffer.Buffer()
// 复制盒头(size + type + version/flags + entry_count = 16 字节)
out.write_bytes(slice_bytes(view, 0, 16))
if b.kind == "stco" {
for i in 0.. Bytes {
let payload = @buffer.Buffer()
payload.write_bytes(Bytes::from_array([0x00, 0x00, 0x00, 0x00]))
payload.write_bytes(make_box("hdlr", mp4_hdlr_payload()))
payload.write_bytes(ilst)
make_box("meta", payload.to_bytes())
}
///|
/// 标准的 `mdir` handler 载荷(meta 盒要求有 hdlr)。
fn mp4_hdlr_payload() -> Bytes {
let out = @buffer.Buffer()
out.write_bytes(Bytes::from_array([0x00, 0x00, 0x00, 0x00])) // version+flags
out.write_bytes(Bytes::from_array([0x00, 0x00, 0x00, 0x00])) // pre_defined
out.write_bytes(Bytes::from_array([0x6D, 0x64, 0x69, 0x72])) // 'mdir'
out.write_bytes(Bytes::from_array([0x61, 0x70, 0x70, 0x6C])) // 'appl'
out.write_bytes(Bytes::from_array([0x00, 0x00, 0x00, 0x00])) // reserved
out.write_bytes(Bytes::from_array([0x00, 0x00, 0x00, 0x00])) // reserved
out.write_bytes(Bytes::from_array([0x00])) // name(空串 + 终止)
out.to_bytes()
}
///|
/// 构造一个文本条目。
pub fn mp4_text_tag(key : String, value : String) -> Mp4Tag {
{ key, value: @utf8.encode(value), data_type: 1, }
}
///|
/// 构造一个封面条目(`covr`);data type:13=JPEG,14=PNG。
pub fn mp4_cover_tag(image : Bytes, is_png : Bool) -> Mp4Tag {
{ key: "covr", value: image, data_type: if is_png { 14 } else { 13 }, }
}