3、视频容器与封装:MP4、MKV、WebM 容器格式对比

聊到视频文件,很多人第一反应是「这是个 MP4 文件」。但严格来说,MP4 只是一个「盒子」,里面装的是编码后的视频流和音频流。这个盒子怎么设计,直接决定了你的播放器能不能快进、能不能挂字幕、能不能在网页上直接播。

我最早做播放器开发时,被容器格式坑过好几次。明明 H.264 码流没问题,封装成 MP4 就卡顿,换成 MKV 反而流畅。后来才明白——封装层和编码层是两套逻辑,不能混为一谈。

3.1 封装层与编码层的关系

先理清一个基本概念:编码层负责压缩图像和声音,输出的是纯码流(比如 H.264 的 NAL Unit 流、AAC 的 ADTS 流)。封装层负责把这些码流打包成一个文件,加上时间戳、索引、元数据,让播放器知道「第 10 秒的视频数据在哪一段」。

打个比方:编码层是「食材」,封装层是「餐盒」。同样的红烧肉(H.264 视频),可以装进塑料饭盒(MP4),也可以装进保温饭盒(MKV)。餐盒好不好用,不影响肉的味道,但影响你吃饭的体验。

核心原则:封装格式不改变编码数据本身。一个 H.264 码流,无论封装成 MP4、MKV 还是 TS,里面的 NAL Unit 字节是完全一样的。区别在于容器如何组织这些字节。

我在项目中遇到过一种情况:客户提供的 MP4 文件,用 FFmpeg 转封装成 MKV 后,文件大小几乎没变,但播放器能正常快进了。原因就是原 MP4 的 moov box 位置不对,导致播放器需要下载完整个文件才能开始播。换成 MKV 后,Cues 索引结构更灵活,问题就解决了。

3.2 主流容器格式对比

目前最常用的三种容器:MP4、MKV、WebM。我列个表,方便你对比。

特性 MP4 MKV WebM
标准组织 ISO/IEC 14496-12 开放社区(Matroska) Google(基于 MKV)
底层结构 Box(原子) EBML(可扩展二进制标记语言) EBML(同 MKV)
支持的视频编码 H.264、H.265、AV1 等 几乎所有编码 VP8、VP9、AV1
支持的音频编码 AAC、MP3、AC-3 等 几乎所有编码 Opus、Vorbis
字幕支持 有限(需嵌入视频流) 原生支持(SRT、ASS 等) 有限(WebVTT)
流式播放 优秀(需 moov box 前置) 一般(需 Cues 索引) 优秀(专为 Web 设计)
容错性 低(损坏后难恢复) 高(分段索引) 中

你想想看,为什么 MP4 这么流行?因为它被几乎所有设备原生支持。但 MKV 在「折腾党」圈子里更受欢迎——能塞任意编码、任意字幕、任意章节。WebM 则是 Google 为 Web 推的格式,完全开源,没有专利费。

我的建议:如果你做的是跨平台产品,优先用 MP4(H.264 + AAC)。如果是本地播放器或视频编辑工具,MKV 更灵活。Web 端直播或点播,WebM + AV1 是未来的方向。

3.3 音视频复用与解复用

复用(Muxing)就是把视频流、音频流、字幕流按时间交错写入容器。解复用(Demuxing)则是反过来,从容器中分离出各条流。

复用时的关键问题是「时间同步」。视频帧和音频帧的时长不一样,播放器需要根据时间戳(PTS/DTS)来对齐。我见过一个坑:某 SDK 在复用 MP4 时,把音频的 PTS 写成了视频的 DTS,结果播放时音画不同步,差了整整 2 秒。

复用流程大致如下:

  1. 从编码器拿到压缩后的视频帧(如 H.264 NAL Unit)和音频帧(如 AAC 包)。
  2. 为每一帧分配 PTS(显示时间戳)和 DTS(解码时间戳)。
  3. 按 PTS 顺序交错写入容器:视频帧、音频帧、视频帧、音频帧……
  4. 写入索引信息(MP4 的 moov box 或 MKV 的 Cues)。
  5. 写入元数据(分辨率、帧率、语言等)。

解复用就是反过来:读取容器头,找到各条流的索引,然后按时间戳顺序读取帧数据,交给解码器。

注意:解复用器必须正确处理 B 帧的 DTS 重排序。如果直接按 PTS 顺序送解码器,B 帧会解码失败。我早期写播放器时,就因为这个 bug 调试了三天。

3.4 码流结构:NAL Unit 与 Annex B 格式

说到封装,就绕不开 H.264/H.265 的码流结构。编码器输出的不是「视频帧」,而是一串 NAL Unit(网络抽象层单元)。每个 NAL Unit 包含一个字节的头和负载数据。

NAL Unit 的类型由头部的 5 位字段决定:

NAL 类型 名称 说明
1 非 IDR 图像片 普通视频帧数据
5 IDR 图像片 关键帧,解码器重置参考帧
7 SPS(序列参数集) 分辨率、帧率等全局参数
8 PPS(图像参数集) 编码模式、熵编码方式等
9 访问单元分隔符 标记一个帧的开始

Annex B 格式是 H.264 最原始的码流组织方式。它用起始码(Start Code)来分隔 NAL Unit:

  • 0x00 0x00 0x01(3 字节起始码)
  • 0x00 0x00 0x00 0x01(4 字节起始码,用于流边界)

一个典型的 Annex B 码流长这样:

0x00 0x00 0x00 0x01 0x67 0x42 0x00 0x1E ...  // SPS (NAL type 7)
0x00 0x00 0x00 0x01 0x68 0xCE 0x3C 0x80 ...  // PPS (NAL type 8)
0x00 0x00 0x00 0x01 0x65 0x88 0x84 0x00 ...  // IDR (NAL type 5)
0x00 0x00 0x01 0x41 0x9A 0x22 0x10 ...       // 非 IDR 片 (NAL type 1)

封装 MP4 时,容器不会直接存起始码,而是用 AVCC 格式:每个 NAL Unit 前面加 4 字节长度字段。这样解析更快,不需要扫描起始码。MKV 和 WebM 则用类似的方式,但结构不同。

避坑指南:我曾经在转封装时,把 Annex B 格式的 H.264 流直接塞进 MP4,没转换成 AVCC 格式。结果播放器解析 NAL Unit 长度出错,画面花屏。记住:MP4 要求 NAL Unit 前 4 字节是长度,不是起始码。

3.5 核心结构图:容器与码流的关系

下面这张图展示了从编码到封装的完整链路,以及 NAL Unit 在容器中的组织方式。

视频容器与码流结构图 视频封装与码流结构 编码层:NAL Unit 流(Annex B 格式) 起始码 NAL 头 RBSP 数据(SPS/PPS/片数据) 起始码 NAL 头 RBSP 复用(Muxing) 封装层:容器格式(MP4 / MKV / WebM) ftyp box 文件类型标识 moov box 索引 + 元数据 mdat box 音视频帧数据(交错) 其他 box 字幕、章节等 解复用(Demuxing) 解复用输出:分离的码流 视频流(H.264 NAL Unit) 送入视频解码器 音频流(AAC / Opus) 送入音频解码器

从图中可以看到:编码层输出的是连续的 NAL Unit 流,用起始码分隔。封装层把这些 NAL Unit 重新组织成容器格式的「盒子」结构。解复用层再把这些盒子拆开,还原出原始的 NAL Unit 流,送给解码器。

嗯,这里要注意:不同容器对 NAL Unit 的存储方式不同。MP4 用 AVCC 格式(4 字节长度前缀),MKV 用 BlockGroup 结构,WebM 则直接复用 MKV 的方式。但无论哪种容器,最终解码器拿到的都是完整的 NAL Unit。

一句话总结:容器是壳,码流是核。壳可以换,核不变。理解了这个关系,你就能灵活处理各种封装格式的转换问题。