JavaScript 处理二进制数据的 API 主要有三种:ArrayBuffer、TypedArray 和 DataView。在 MP4 Box 的解析和处理过程中,这些工具非常有用。本文结合实际的 MP4 box 结构,聊聊它们各自的定位和取舍。
三者的关系
ArrayBuffer
ArrayBuffer 是一块原始的、固定长度的二进制内存,你不能直接读写它,必须通过"视图"来操作。当你 fetch 一个 fMP4 文件后,对其解析会先转为 ArrayBuffer。
typescript
arrayBuffer resultTypedArray
TypedArray 是一组类型化数组视图(Uint8Array、Uint16Array、Uint32Array、Float32Array 等),它把 ArrayBuffer 当作同构的数组来访问——所有元素类型相同、等宽排列。它只是在已有的 ArrayBuffer 上建立一个视图,本身不复制也不额外分配数据内存。
typescript
typedArray resultDataView
DataView 也是建立在 ArrayBuffer 上的视图,同样不复制数据。与 TypedArray 不同的是,它不把 buffer 当数组看,而是提供了一组方法让你在任意偏移位置、以任意类型和字节序来读写数据——没有对齐限制,也不要求字段类型统一。
typescript
dataView result三者的关系可以这样理解:ArrayBuffer 是仓库,TypedArray 和 DataView 是两种不同的取货方式——前者像传送带,只能运同一规格的货物;后者像叉车,想取什么取什么。
MP4 Box 的结构特点
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 (各种混合类型字段)
这里有几个关键特征:
- 字段类型混杂:一个 box 里 uint8、uint16、uint24、uint32、uint64 混着来,没法用单一的 TypedArray 类型映射整个 box
- 字段偏移不对齐:比如 1 字节的 version 后面紧跟 3 字节的 flags,再接 uint32 字段——中间穿插了奇数长度的字段后,后续偏移很容易不是 2、4、8 的倍数,TypedArray 的对齐要求就满足不了
- 大端字节序:MP4 规范要求所有多字节整数使用大端字节序,而 TypedArray 使用平台原生字节序(绝大多数设备是小端),直接读出来的值是反的
这三条,直接决定了各 API 的适用程度。
Uint8Array 数据搬运和字节级操作
在 MP4 解析中,Uint8Array 是用得最多的 TypedArray。它没有对齐和字节序的问题——每个元素就是一个字节,强项是数据搬运(切片、拷贝、拼接)和逐字节扫描。
js
Uint32Array 为什么在 MP4 解析中几乎没用
直觉上,MP4 box 里到处是 uint32 字段,Uint32Array 应该很适合?实际上它在 MP4 解析里几乎没有用武之地,最核心的原因是它的字节序不对。
MP4 是大端,而 Uint32Array 使用平台原生字节序。绝大多数设备(x86、ARM)是小端,这意味着直接读出来的值是字节反转的。
举个例子,要把值 23 写入 buffer,按 MP4 大端格式应该是 00 00 00 17:
typescript
把一个简单的 23 转成 385875968 才能写入,这样就太复杂了。
DataView 的灵活性
DataView 完美解决了上述问题。它允许你在任意偏移位置,以任意类型和字节序来读写数据。
DataView 提供了 10 对 getter/setter:
每个 getter 都有对应的 setter,setter 多一个 value 参数。所有多字节方法的最后一个参数 littleEndian 默认为 false(大端),恰好和 MP4 的字节序一致。
总结
写 MP4 parser 时,一个简单的原则:搬运数据用 Uint8Array,解析字段用 DataView。