ArrayBuffer、TypedArray 和 DataView 在 MP4 Box 解析中的运用

2026 年 2 月 21 日 星期六(已编辑)
/ ,
160
1
这篇文章上次修改于 2026 年 2 月 21 日 星期六,可能部分内容已经不适用,如有疑问可询问作者。

ArrayBuffer、TypedArray 和 DataView 在 MP4 Box 解析中的运用

JavaScript 处理二进制数据的 API 主要有三种:ArrayBufferTypedArrayDataView。在 MP4 Box 的解析和处理过程中,这些工具非常有用。本文结合实际的 MP4 box 结构,聊聊它们各自的定位和取舍。

三者的关系

ArrayBuffer

ArrayBuffer 是一块原始的、固定长度的二进制内存,你不能直接读写它,必须通过"视图"来操作。当你 fetch 一个 fMP4 文件后,对其解析会先转为 ArrayBuffer。

const response = await fetch('video.m4s');
const buffer = await response.arrayBuffer();
console.log(buffer);
arrayBuffer result

arrayBuffer result

TypedArray

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

const response = await fetch('video.m4s');
const buffer = await response.arrayBuffer();
const uint8 = new Uint8Array(buffer); // 创建视图,不会占用额外内存
console.log(uint8);
typedArray result

typedArray result

DataView

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

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);
dataView result

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     (各种混合类型字段)

这里有几个关键特征:

  1. 字段类型混杂:一个 box 里 uint8、uint16、uint24、uint32、uint64 混着来,没法用单一的 TypedArray 类型映射整个 box
  2. 字段偏移不对齐:比如 1 字节的 version 后面紧跟 3 字节的 flags,再接 uint32 字段——中间穿插了奇数长度的字段后,后续偏移很容易不是 2、4、8 的倍数,TypedArray 的对齐要求就满足不了
  3. 大端字节序:MP4 规范要求所有多字节整数使用大端字节序,而 TypedArray 使用平台原生字节序(绝大多数设备是小端),直接读出来的值是反的

这三条,直接决定了各 API 的适用程度。

Uint8Array 数据搬运和字节级操作

在 MP4 解析中,Uint8Array 是用得最多的 TypedArray。它没有对齐和字节序的问题——每个元素就是一个字节,强项是数据搬运(切片、拷贝、拼接)和逐字节扫描。

// 切出某个 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;
  }
}

Uint32Array 为什么在 MP4 解析中几乎没用

直觉上,MP4 box 里到处是 uint32 字段,Uint32Array 应该很适合?实际上它在 MP4 解析里几乎没有用武之地,最核心的原因是它的字节序不对。

MP4 是大端,而 Uint32Array 使用平台原生字节序。绝大多数设备(x86、ARM)是小端,这意味着直接读出来的值是字节反转的。

举个例子,要把值 23 写入 buffer,按 MP4 大端格式应该是 00 00 00 17

// 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 ✓

把一个简单的 23 转成 385875968 才能写入,这样就太复杂了。

DataView 的灵活性

DataView 完美解决了上述问题。它允许你在任意偏移位置,以任意类型和字节序来读写数据。

DataView 提供了 10 对 getter/setter:

方法字节数说明
getInt8 / getUint81无字节序参数(单字节不需要)
getInt16 / getUint162第二参数控制字节序
getInt32 / getUint324第二参数控制字节序
getFloat32 / getFloat644 / 8IEEE 754 浮点数
getBigInt64 / getBigUint648返回 BigInt

每个 getter 都有对应的 setter,setter 多一个 value 参数。所有多字节方法的最后一个参数 littleEndian 默认为 false(大端),恰好和 MP4 的字节序一致。

总结

APIMP4 解析中的角色典型场景
ArrayBuffer底层数据容器承载所有二进制数据
Uint8Array高频工具切片、拷贝、扫描、读 box type
Uint32Array 等几乎不用字节序/对齐/混合类型三重限制
DataView核心解析工具读写任意类型、任意偏移、可控字节序

写 MP4 parser 时,一个简单的原则:搬运数据用 Uint8Array,解析字段用 DataView

使用社交账号登录

  • Loading...
  • Loading...
  • Loading...
  • Loading...
  • Loading...