使用社交账号登录
JavaScript 处理二进制数据的 API 主要有三种:ArrayBuffer、TypedArray 和 DataView。在 MP4 Box 的解析和处理过程中,这些工具非常有用。本文结合实际的 MP4 box 结构,聊聊它们各自的定位和取舍。
ArrayBuffer 是一块原始的、固定长度的二进制内存,你不能直接读写它,必须通过"视图"来操作。当你 fetch 一个 fMP4 文件后,对其解析会先转为 ArrayBuffer。
const response = await fetch('video.m4s');
const buffer = await response.arrayBuffer();
console.log(buffer);

TypedArray 是一组类型化数组视图(Uint8Array、Uint16Array、Uint32Array、Float32Array 等),它把 ArrayBuffer 当作同构的数组来访问——所有元素类型相同、等宽排列。它只是在已有的 ArrayBuffer 上建立一个视图,本身不复制也不额外分配数据内存。

DataView 也是建立在 ArrayBuffer 上的视图,同样不复制数据。与 TypedArray 不同的是,它不把 buffer 当数组看,而是提供了一组方法让你在任意偏移位置、以任意类型和字节序来读写数据——没有对齐限制,也不要求字段类型统一。

三者的关系可以这样理解:ArrayBuffer 是仓库,TypedArray 和 DataView 是两种不同的取货方式——前者像传送带,只能运同一规格的货物;后者像叉车,想取什么取什么。
MP4 文件遵循 ISO 14496-12(ISOBMFF)规范,整个文件由嵌套的 box 组成。每个 box 的基本结构是:
[4 bytes] size (uint32)
[4 bytes] type (4个ASCII字符)
[可选] largesize (uint64,当 size==1 时)
[可选] version (uint8) + flags (uint24)
[...] payload (各种混合类型字段)
这里有几个关键特征:
这三条,直接决定了各 API 的适用程度。
在 MP4 解析中,Uint8Array 是用得最多的 TypedArray。它没有对齐和字节序的问题——每个元素就是一个字节,强项是数据搬运(切片、拷贝、拼接)和逐字节扫描。
直觉上,MP4 box 里到处是 uint32 字段,Uint32Array 应该很适合?实际上它在 MP4 解析里几乎没有用武之地,最核心的原因是它的字节序不对。
MP4 是大端,而 Uint32Array 使用平台原生字节序。绝大多数设备(x86、ARM)是小端,这意味着直接读出来的值是字节反转的。
举个例子,要把值 23 写入 buffer,按 MP4 大端格式应该是 00 00 00 17:
把一个简单的 23 转成 385875968 才能写入,这样就太复杂了。
DataView 完美解决了上述问题。它允许你在任意偏移位置,以任意类型和字节序来读写数据。
DataView 提供了 10 对 getter/setter:
| 方法 | 字节数 | 说明 |
|---|---|---|
getInt8 / getUint8 | 1 | 无字节序参数(单字节不需要) |
getInt16 / getUint16 | 2 | 第二参数控制字节序 |
getInt32 / getUint32 | 4 | 第二参数控制字节序 |
getFloat32 / getFloat64 | 4 / 8 | IEEE 754 浮点数 |
getBigInt64 / getBigUint64 | 8 | 返回 BigInt |
每个 getter 都有对应的 setter,setter 多一个 value 参数。所有多字节方法的最后一个参数 littleEndian 默认为 false(大端),恰好和 MP4 的字节序一致。
| API | MP4 解析中的角色 | 典型场景 |
|---|---|---|
| ArrayBuffer | 底层数据容器 | 承载所有二进制数据 |
| Uint8Array | 高频工具 | 切片、拷贝、扫描、读 box type |
| Uint32Array 等 | 几乎不用 | 字节序/对齐/混合类型三重限制 |
| DataView | 核心解析工具 | 读写任意类型、任意偏移、可控字节序 |
写 MP4 parser 时,一个简单的原则:搬运数据用 Uint8Array,解析字段用 DataView。
const response = await fetch('video.m4s');
const buffer = await response.arrayBuffer();
const uint8 = new Uint8Array(buffer); // 创建视图,不会占用额外内存
console.log(uint8);
const response = await fetch('video.m4s');
const buffer = await response.arrayBuffer();
const view = new DataView(buffer);
const size = view.getUint32(0);
const type = String.fromCharCode(
view.getUint8(4), view.getUint8(5),
view.getUint8(6), view.getUint8(7)
);
console.log(size, type);
// 切出某个 box 的 payload
const boxPayload = new Uint8Array(buffer, boxOffset + 8, boxSize - 8);
// 拼接两段 segment 数据
const merged = new Uint8Array(a.length + b.length);
merged.set(a, 0);
merged.set(b, a.length);
// 逐字节匹配 box type,找到 mdat box 的位置
const bytes = new Uint8Array(buffer);
for (let i = 0; i < bytes.length - 7; i++) {
if (bytes[i+4] === 0x6D && bytes[i+5] === 0x64 &&
bytes[i+6] === 0x61 && bytes[i+7] === 0x74) {
console.log('mdat box at offset', i);
break;
}
}
// DataView:直接写 23,指定大端
const view = new DataView(buf);
view.setUint32(0, 23, false); // buffer: 00 00 00 17 ✓
// Uint32Array:写 23 会变成小端排列
const arr = new Uint32Array(buf);
arr[0] = 23; // 小端机器上 buffer: 17 00 00 00 ✗
// 要得到正确的字节排列,你得手动翻转
arr[0] = 0x17000000; // 即 23 × 2²⁴ = 385875968
// buffer: 00 00 00 17 ✓