返回学习

学习 2026

啊鸡入坑音视频之《流媒体封装篇》

从 Muxer 可以逐包写入却不代表容器适合直播说起,对比 MP4、fMP4 与 MPEG-TS,并用 flv.js 串起 HTTP-FLV、Remux、MSE 和浏览器解码链路。

音视频流媒体Container
本文目录23
  1. 1. Muxer 的增量输入不等于流式输出
  2. 2. 传统 MP4 为什么偏向完整文件
  3. 3. Fast Start 解决的是渐进下载
  4. 4. fMP4 怎样把全局文件变成 Fragment
  5. 5. MPEG-TS 为什么天然面向传输
  6. 6. WAV、Ogg 与 WebM 分别站在哪里
  7. WAV:头信息偏向完整文件
  8. Ogg:Page 化便于持续追加
  9. Matroska / WebM:Cluster 支持增量组织
  10. 7. 评估 Container 流式能力的五个问题
  11. 7.1 是否依赖完整文件信息
  12. 7.2 是否支持增量解析
  13. 7.3 Metadata 是否能提前取得或周期重复
  14. 7.4 是否支持随机接入
  15. 7.5 损坏后能否重新同步
  16. 8. Container 和 Streaming Protocol 不在同一层
  17. 9. flv.js:一次只换 Container、不换 Codec 的浏览器适配
  18. 9.1 它不是 H.264 Decoder
  19. 9.2 为什么浏览器前面需要这层适配
  20. 9.3 FLV Tag 里仍然是 Codec 数据
  21. 10. 一张表看清常见取向
  22. 11. 回到最初的问题
  23. References

音视频基础系列(二):上一篇是啊鸡入坑音视频之《音频格式篇》,下一篇是啊鸡入坑音视频之《视频帧与时间戳篇》

Muxer 通常可以一个 Packet 接一个 Packet 地接收编码数据。既然数据能持续写进去,为什么传统 MP4 仍常被说“不适合直播”?

关键在于:能增量写入,不等于消费者能增量解析、随机加入并在出错后恢复。 Muxer 的输入方式只描述数据怎样交给封装器;Container 的组织方式,才决定输出更适合完整文件、渐进下载还是实时流。

先把三种场景分开:

场景数据条件播放要求
本地文件文件已经完整存在可读取完整索引并随机 Seek
Progressive Download文件还在下载,但有明确结束点下载前半段时就能从头播放
Live Streaming数据持续产生,结束时间未知能持续解析、从中途加入并从错误中恢复

同一个 Container 可能很适合其中一种,却不适合另一种。

本地文件、渐进下载与直播的播放模型

1. Muxer 的增量输入不等于流式输出

一条常见的封装链路是:

text
Encoder
   ↓
Encoded Packet / Access Unit
   ↓
Muxer
   ↓
按 Container 规则组织的数据

Muxer 可以持续接收 Packet,但它如何落盘或发送,受目标 Container 约束:

  • 是否需要最终文件大小或总时长;
  • 是否依赖完整 Sample Table;
  • 播放器何时能够取得 Track 与 Codec 参数;
  • 用户能否从数据流中间加入;
  • 网络损坏后能否重新找到边界。

所以,“Muxer 支持逐包写入”只回答了生产端 API 的问题,没有回答消费端何时具备播放条件。

2. 传统 MP4 为什么偏向完整文件

MP4 是 ISO Base Media File Format 家族中的容器。用一个简化模型看,普通 MP4 里最关键的两类 Box 是:

text
MP4
├── moov   轨道、时序、Sample Table 等元数据
└── mdat   实际媒体数据

mdat 保存 H.264、H.265、AAC 等编码数据;moov 则告诉播放器有哪些 Track、每个 Sample 在哪里、持续多久,以及怎样映射到时间轴。

很多录制流程会先持续写 mdat,直到录制结束后才得到完整 Sample 数量和偏移,再生成 moov

text
持续写入 mdat
      ↓
录制结束,完整布局确定
      ↓
生成 moov

最终文件可能是:

text
┌──────────────┐
│ mdat         │
│ H.264/AAC... │
│ ...          │
├──────────────┤
│ moov         │
└──────────────┘

如果播放器只下载了前 20%,而 moov 位于文件末尾,它虽然已经拿到部分媒体字节,却可能还不知道 Track 结构和 Sample 映射,因此无法开始播放。

这个问题并不说明 MP4 不能顺序写入,而是说明普通 MP4 的播放初始化可能依赖一个尚未完成的全局索引

3. Fast Start 解决的是渐进下载

对已经完成的 MP4,可以把 moov 移到 mdat 前面:

text
MP4
├── moov
└── mdat

这通常称为 Fast Start 或 Web Optimized MP4。FFmpeg 中常见的处理方式是:

bash
ffmpeg -i input.mp4 -c copy -movflags +faststart output.mp4

-c copy 表示复制原有码流,不重新编码;+faststart 在输出完成后把 moov 调整到文件前部。

浏览器于是可以:

text
先取得 moov
    ↓
建立 Track 与 Sample 映射
    ↓
边下载 mdat,边从头播放

Fast Start 很适合 Progressive Download,但它没有把传统 MP4 变成无限直播格式。文件依旧有明确的结束点,完整索引仍针对一个最终会完成的媒体对象。

4. fMP4 怎样把全局文件变成 Fragment

fMP4(Fragmented MP4)保留 MP4 的初始化信息,但把后续媒体拆成可独立处理的 Fragment:

text
初始化段
└── moov

Fragment 1
├── moof
└── mdat

Fragment 2
├── moof
└── mdat

Fragment 3
├── moof
└── mdat

moof 描述当前 Fragment 中的 Sample,紧随其后的 mdat 保存对应媒体数据。播放器不必等待整个内容结束:

text
取得初始化段
      ↓
知道 Track 与 Codec 配置
      ↓
Fragment 到达
      ↓
解析并播放这一段
      ↓
等待下一个 Fragment

这使 fMP4 很适合 HLS、MPEG-DASH、CMAF 和浏览器 MSE 等分段媒体体系。HLS 中常见的组织方式就是:

text
playlist.m3u8
init.mp4
001.m4s
002.m4s
003.m4s

这里的 .m4s 是媒体 Segment,和传统完整 .mp4 文件不是同一种消费模型。

不过 Fragment 化也不意味着零延迟。Fragment 或 Segment 持续多久、播放器缓冲多少、清单何时更新,都会影响首帧时间和直播延迟。Container 提供能力,协议与播放器策略决定最终体验。

5. MPEG-TS 为什么天然面向传输

MPEG-TS 的全称是 MPEG Transport Stream。它从设计上就面向广播和不可靠传输环境,而不是完整文件归档。

TS 数据由固定长度的 188-byte Transport Packet 连续组成:

text
188 bytes
188 bytes
188 bytes
188 bytes
...

每个 Packet 都带有同步字节和 PID 等字段。节目与流的映射信息会周期性出现:

text
PAT
PMT
Video Packets
Audio Packets
...
PAT
PMT
...

PAT(Program Association Table)帮助找到节目对应的 PMT;PMT(Program Map Table)描述该节目包含哪些 Elementary Stream,以及它们分别使用哪个 PID。

周期重复这些信息带来两个重要能力。

第一,播放器可以从流中间加入:

text
10:15 加入直播
      ↓
找到 Packet 边界
      ↓
等待下一组 PAT / PMT
      ↓
识别音视频 PID
      ↓
等待可随机接入的视频关键帧和 Codec 参数
      ↓
开始播放

第二,出现丢包或损坏后,解析器更容易重新找到同步字节、Packet 边界和后续节目表,恢复处理。固定 Packet 和重复元数据增加了开销,却换来了传输场景需要的可恢复性。

传统 MP4 更像一本带目录和页码的书;MPEG-TS 更像持续广播的电视信号。一本书擅长完整结构和随机查阅,广播则允许听众随时打开接收设备,等待必要信息后加入。

传统 MP4、fMP4 与 MPEG-TS 的数据组织方式

Fast Start MP4、fMP4 与 MPEG-TS 的能力演进

6. WAV、Ogg 与 WebM 分别站在哪里

“是否适合流式”不是 MP4 和 TS 的二选一。其他 Container 也体现了不同设计取向。

WAV:头信息偏向完整文件

传统 WAV 使用 RIFF 结构:

text
RIFF
├── fmt
└── data
    └── PCM...

Header 中通常包含 RIFF Chunk 和 Data Chunk 的大小。开始一段无限直播时,最终 PCM 数据量并不知道,因此标准文件长度无法在开头准确填写。录音程序可以先占位、结束时回写,也存在面向大文件或流式场景的扩展,但传统 WAV 的核心模型仍更偏向有结束点的文件。

Ogg:Page 化便于持续追加

Ogg 将数据组织为连续 Page:

text
Ogg Page
Ogg Page
Ogg Page
...

Page 包含序号、校验和、Payload 等信息,能够持续产生和解析。Ogg 与 Opus 的组合因此常用于音频流。

Matroska / WebM:Cluster 支持增量组织

Matroska 通过 Segment 和 Cluster 组织媒体:

text
Segment
├── Cluster
├── Cluster
└── ...

Cluster 可以持续产生。WebM 是 Matroska 的受限子集,常见 VP9 / AV1 与 Opus 等组合,适合 Web 媒体。但“格式允许”仍不等于“目标平台可用”,实际选型还取决于浏览器、硬件解码、播放器与协议生态。

7. 评估 Container 流式能力的五个问题

面对一种新 Container,与其背诵“支持或不支持 Streaming”,不如依次问五个问题。

7.1 是否依赖完整文件信息

如果初始化必须知道总大小、总时长或完整索引,它通常更偏向有结束点的文件。能够延期、分片或周期重建这些信息,才更适合持续流。

7.2 是否支持增量解析

消费者能否做到:

text
收到一点 → 解析一点 → 播放一点

如果必须等待完整文件才能建立结构,就不适合低启动延迟的播放。

7.3 Metadata 是否能提前取得或周期重复

fMP4 通过初始化段提供 Track 与 Codec 配置,MPEG-TS 周期发送 PAT / PMT。两种方式不同,目标都是让消费者在有限数据到达后获得必要上下文。

7.4 是否支持随机接入

直播观众不会都从第一个字节开始观看。中途加入至少需要:

  • Container / Transport 边界;
  • Track 或 PID 信息;
  • Codec 初始化参数;
  • 视频可随机接入点,通常是合适的关键帧;
  • 对应的时间戳与播放时钟。

Container 只解决其中一部分,Encoder 的 GOP 设计与协议的分段边界同样重要。

7.5 损坏后能否重新同步

真实网络会丢包、重排或产生局部损坏。面向传输的格式通常提供稳定边界、同步标记、连续性信息或周期元数据,使解析器不用从头重来。

这五个问题比“Muxer 能不能边收边写”更接近真正的 Streaming 能力。

8. Container 和 Streaming Protocol 不在同一层

媒体链路至少可以拆成:

text
Codec
  ↓
Container / Payload Format
  ↓
Streaming / Transport Protocol
  ↓
Network Transport

例如早期 HLS 常见:

text
H.264 + AAC
      ↓
MPEG-TS Segments
      ↓
HLS Playlist
      ↓
HTTP

现代 HLS 也常见:

text
H.264 / H.265 + AAC
          ↓
fMP4 Segments
          ↓
HLS Playlist
          ↓
HTTP

另一条实时链路可能是:

text
H.264 + AAC
      ↓
RTP Packetization
      ↓
RTSP 负责会话控制
      ↓
UDP / TCP

RTP 与 MP4 并不是可以直接互换的同类对象。RTP 更关注实时媒体在网络中的分包、序号和时间戳;RTSP 负责播放、暂停等会话控制;MP4 则是媒体组织格式。工程讨论中必须先说清楚比较的是哪一层。

Codec、Container、协议与网络传输的分层关系

9. flv.js:一次只换 Container、不换 Codec 的浏览器适配

flv.js 是理解这几层职责的典型案例。它面对的常见输入不是裸 H.264,而是带有明确封装结构的 FLV 字节流:

text
HTTP 长连接
      ↓
FLV Header
      ↓
Script / Video / Audio Tags 持续到达

FLV Tag 可以承载脚本元数据、视频或音频。flv.js 官方 README 给出的典型媒体组合是:

text
FLV Container
├── Video: H.264 / AVC
└── Audio: AAC / MP3

因此,回答“flv.js 支持什么格式”时,至少要分成两层:它解析的是 FLV Container,其中常见的 Video Codec 是 H.264 / AVC,Audio Codec 是 AAC 或 MP3。只说“它支持 H.264”会遗漏最关键的输入契约——裸 H.264 并不是这个库的核心输入形式。

9.1 它不是 H.264 Decoder

flv.js 的核心链路是:

text
HTTP-FLV / WebSocket-FLV
            ↓
        FLV Demux
            ↓
     H.264 + AAC / MP3
            ↓
       Transmux / Remux
            ↓
     ISO BMFF / fMP4 Segments
            ↓
Media Source Extensions(MSE)
            ↓
        HTML <video>
            ↓
      Browser Media Pipeline

官方 README 将这项工作描述为:把 FLV stream transmux 成 ISO BMFF(Fragmented MP4)segments,再通过 MSE 交给 HTML5 <video>

这里真正发生的是两步 Container 转换:

text
FLV
 ↓ Demux
H.264 + AAC
 ↓ Remux
fMP4

前后仍然是 H.264 与 AAC,没有把 H.264 解成 YUV,也没有把 AAC 解成 PCM,更没有再编码成另一种 Codec。因此它不是 Transcode,而是 Transmux / Remuxflv.js 更接近一个 FLV Demuxer 加 fMP4 Muxer,而不是 H.264 / AAC Decoder。

真正的 Codec 解码仍由浏览器媒体栈完成。MSE 提供的是 JavaScript 管理编码媒体片段的接口:程序创建 MediaSourceSourceBuffer,把初始化段和媒体段逐步 appendBuffer() 给媒体元素。MSE 本身不实现 H.264 或 AAC Decoder,也不会让浏览器原本不支持的 Codec 自动变得可播放。

flv.js 从 HTTP-FLV 到浏览器解码与渲染的 Transmux 链路

9.2 为什么浏览器前面需要这层适配

服务器选择 HTTP-FLV,是因为 FLV Tag 可以沿一个 HTTP 长连接持续发送;浏览器侧则通常不把 HTTP-FLV 直接作为 <video> / MSE 的通用输入。浏览器更常见的组合是 MSE 加 ISO BMFF / fMP4,以及它自身支持的 Codec。

于是 flv.js 在两套生态之间做了适配:

text
服务器侧喜欢:HTTP-FLV
          ↓ flv.js
浏览器侧接受:fMP4 Segments + MSE

这也再次说明“支持流式”不是一个格式单独决定的布尔值。FLV 负责连续组织 Tag,HTTP 提供字节传输,flv.js 负责 Demux 与 Remux,MSE 负责把编码片段接入媒体元素,浏览器媒体栈负责 Decode 与 Render。缺少其中任一层,链路都不完整。

9.3 FLV Tag 里仍然是 Codec 数据

当 FLV 承载 H.264 时,Video Tag 中仍包含 AVC Packet 与 H.264 NAL Unit;初始化阶段还需要 Decoder Configuration,例如 SPS、PPS 等信息。AAC 同样需要对应的 AudioSpecificConfig。

因此从 FLV 切换到 RTP,并不一定要更换底层 Codec:

text
HTTP-FLV:H.264 NALU → FLV Tags → HTTP / TCP
RTSP / RTP:H.264 NALU → RTP Packets → UDP / TCP

两条链路都可能传 H.264,差别在于上一层怎样组织、标时、分包和传输。FLV、RTP 和 H.264 不是三个互相替代的“视频格式”,而是处在不同职责层。

flv.js README 已说明项目将很少维护,并建议 FLV 直播场景考虑基于它继续发展的 mpegts.js。后者除了 FLV,还扩展了 MPEG-TS 等输入与更多 Codec;但在常见 MSE 播放路径中,核心思想仍是把输入媒体重新组织成浏览器可以追加和播放的 fMP4 segments。

10. 一张表看清常见取向

下面是一种工程上的粗略分类,不代表所有实现、扩展和播放器都具有相同行为:

Container文件播放渐进播放Live Streaming主要取向
传统 MP4很好取决于 moov 位置一般完整文件与随机 Seek
Fast Start MP4很好很好一般提前提供全局索引
fMP4很好很好很好Fragment / Segment
MPEG-TS可以可以很好广播、随机加入与恢复
Matroska / WebM很好可以到很好可以到较好Cluster 化、生态依实现而异
Ogg很好很好较好Page 化连续流
传统 WAV很好有限较差有结束点的音频文件

真正选型时,还要叠加 Codec 兼容性、浏览器支持、硬件能力、DRM、延迟、带宽开销与现有协议栈,不能只看 Container 的理论结构。

11. 回到最初的问题

封装既不是简单的“按文件一次写完”,也不是“每个编码帧原样套一个壳”。更准确的描述是:

Muxer 持续接收编码 Packet,再按目标 Container 的规则组织 Track、时间戳、索引、Fragment、Page、Cluster 或 Transport Packet。

不同规则形成不同消费模型:

text
传统 MP4
Packet 持续写入 + 全局文件索引
→ 偏完整文件

fMP4
一批 Packet → 一个 Fragment
→ 偏分段流式

MPEG-TS
媒体数据 → 固定长度 Transport Packet
→ 偏持续传输

因此,判断一个 Container 是否适合 Streaming,不能停在“Muxer 能不能一包一包写”。还要继续追问:播放器何时拿到必要上下文,用户能否从中间加入,网络损坏后怎样恢复,以及格式、协议与播放器生态是否共同支持目标场景。

References

返回首页
上一篇啊鸡入坑音视频之《音频格式篇》下一篇啊鸡入坑音视频之《视频帧与时间戳篇》

Discussion

留言与讨论

想法、补充和不同意见都欢迎。