音视频基础系列(二):上一篇是啊鸡入坑音视频之《音频格式篇》,下一篇是啊鸡入坑音视频之《视频帧与时间戳篇》。
Muxer 通常可以一个 Packet 接一个 Packet 地接收编码数据。既然数据能持续写进去,为什么传统 MP4 仍常被说“不适合直播”?
关键在于:能增量写入,不等于消费者能增量解析、随机加入并在出错后恢复。 Muxer 的输入方式只描述数据怎样交给封装器;Container 的组织方式,才决定输出更适合完整文件、渐进下载还是实时流。
先把三种场景分开:
| 场景 | 数据条件 | 播放要求 |
|---|---|---|
| 本地文件 | 文件已经完整存在 | 可读取完整索引并随机 Seek |
| Progressive Download | 文件还在下载,但有明确结束点 | 下载前半段时就能从头播放 |
| Live Streaming | 数据持续产生,结束时间未知 | 能持续解析、从中途加入并从错误中恢复 |
同一个 Container 可能很适合其中一种,却不适合另一种。

1. Muxer 的增量输入不等于流式输出
一条常见的封装链路是:
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 是:
MP4
├── moov 轨道、时序、Sample Table 等元数据
└── mdat 实际媒体数据
mdat 保存 H.264、H.265、AAC 等编码数据;moov 则告诉播放器有哪些 Track、每个 Sample 在哪里、持续多久,以及怎样映射到时间轴。
很多录制流程会先持续写 mdat,直到录制结束后才得到完整 Sample 数量和偏移,再生成 moov:
持续写入 mdat
↓
录制结束,完整布局确定
↓
生成 moov
最终文件可能是:
┌──────────────┐
│ mdat │
│ H.264/AAC... │
│ ... │
├──────────────┤
│ moov │
└──────────────┘
如果播放器只下载了前 20%,而 moov 位于文件末尾,它虽然已经拿到部分媒体字节,却可能还不知道 Track 结构和 Sample 映射,因此无法开始播放。
这个问题并不说明 MP4 不能顺序写入,而是说明普通 MP4 的播放初始化可能依赖一个尚未完成的全局索引。
3. Fast Start 解决的是渐进下载
对已经完成的 MP4,可以把 moov 移到 mdat 前面:
MP4
├── moov
└── mdat
这通常称为 Fast Start 或 Web Optimized MP4。FFmpeg 中常见的处理方式是:
ffmpeg -i input.mp4 -c copy -movflags +faststart output.mp4
-c copy 表示复制原有码流,不重新编码;+faststart 在输出完成后把 moov 调整到文件前部。
浏览器于是可以:
先取得 moov
↓
建立 Track 与 Sample 映射
↓
边下载 mdat,边从头播放
Fast Start 很适合 Progressive Download,但它没有把传统 MP4 变成无限直播格式。文件依旧有明确的结束点,完整索引仍针对一个最终会完成的媒体对象。
4. fMP4 怎样把全局文件变成 Fragment
fMP4(Fragmented MP4)保留 MP4 的初始化信息,但把后续媒体拆成可独立处理的 Fragment:
初始化段
└── moov
Fragment 1
├── moof
└── mdat
Fragment 2
├── moof
└── mdat
Fragment 3
├── moof
└── mdat
moof 描述当前 Fragment 中的 Sample,紧随其后的 mdat 保存对应媒体数据。播放器不必等待整个内容结束:
取得初始化段
↓
知道 Track 与 Codec 配置
↓
Fragment 到达
↓
解析并播放这一段
↓
等待下一个 Fragment
这使 fMP4 很适合 HLS、MPEG-DASH、CMAF 和浏览器 MSE 等分段媒体体系。HLS 中常见的组织方式就是:
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 连续组成:
188 bytes
188 bytes
188 bytes
188 bytes
...
每个 Packet 都带有同步字节和 PID 等字段。节目与流的映射信息会周期性出现:
PAT
PMT
Video Packets
Audio Packets
...
PAT
PMT
...
PAT(Program Association Table)帮助找到节目对应的 PMT;PMT(Program Map Table)描述该节目包含哪些 Elementary Stream,以及它们分别使用哪个 PID。
周期重复这些信息带来两个重要能力。
第一,播放器可以从流中间加入:
10:15 加入直播
↓
找到 Packet 边界
↓
等待下一组 PAT / PMT
↓
识别音视频 PID
↓
等待可随机接入的视频关键帧和 Codec 参数
↓
开始播放
第二,出现丢包或损坏后,解析器更容易重新找到同步字节、Packet 边界和后续节目表,恢复处理。固定 Packet 和重复元数据增加了开销,却换来了传输场景需要的可恢复性。
传统 MP4 更像一本带目录和页码的书;MPEG-TS 更像持续广播的电视信号。一本书擅长完整结构和随机查阅,广播则允许听众随时打开接收设备,等待必要信息后加入。


6. WAV、Ogg 与 WebM 分别站在哪里
“是否适合流式”不是 MP4 和 TS 的二选一。其他 Container 也体现了不同设计取向。
WAV:头信息偏向完整文件
传统 WAV 使用 RIFF 结构:
RIFF
├── fmt
└── data
└── PCM...
Header 中通常包含 RIFF Chunk 和 Data Chunk 的大小。开始一段无限直播时,最终 PCM 数据量并不知道,因此标准文件长度无法在开头准确填写。录音程序可以先占位、结束时回写,也存在面向大文件或流式场景的扩展,但传统 WAV 的核心模型仍更偏向有结束点的文件。
Ogg:Page 化便于持续追加
Ogg 将数据组织为连续 Page:
Ogg Page
Ogg Page
Ogg Page
...
Page 包含序号、校验和、Payload 等信息,能够持续产生和解析。Ogg 与 Opus 的组合因此常用于音频流。
Matroska / WebM:Cluster 支持增量组织
Matroska 通过 Segment 和 Cluster 组织媒体:
Segment
├── Cluster
├── Cluster
└── ...
Cluster 可以持续产生。WebM 是 Matroska 的受限子集,常见 VP9 / AV1 与 Opus 等组合,适合 Web 媒体。但“格式允许”仍不等于“目标平台可用”,实际选型还取决于浏览器、硬件解码、播放器与协议生态。
7. 评估 Container 流式能力的五个问题
面对一种新 Container,与其背诵“支持或不支持 Streaming”,不如依次问五个问题。
7.1 是否依赖完整文件信息
如果初始化必须知道总大小、总时长或完整索引,它通常更偏向有结束点的文件。能够延期、分片或周期重建这些信息,才更适合持续流。
7.2 是否支持增量解析
消费者能否做到:
收到一点 → 解析一点 → 播放一点
如果必须等待完整文件才能建立结构,就不适合低启动延迟的播放。
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 不在同一层
媒体链路至少可以拆成:
Codec
↓
Container / Payload Format
↓
Streaming / Transport Protocol
↓
Network Transport
例如早期 HLS 常见:
H.264 + AAC
↓
MPEG-TS Segments
↓
HLS Playlist
↓
HTTP
现代 HLS 也常见:
H.264 / H.265 + AAC
↓
fMP4 Segments
↓
HLS Playlist
↓
HTTP
另一条实时链路可能是:
H.264 + AAC
↓
RTP Packetization
↓
RTSP 负责会话控制
↓
UDP / TCP
RTP 与 MP4 并不是可以直接互换的同类对象。RTP 更关注实时媒体在网络中的分包、序号和时间戳;RTSP 负责播放、暂停等会话控制;MP4 则是媒体组织格式。工程讨论中必须先说清楚比较的是哪一层。

9. flv.js:一次只换 Container、不换 Codec 的浏览器适配
flv.js 是理解这几层职责的典型案例。它面对的常见输入不是裸 H.264,而是带有明确封装结构的 FLV 字节流:
HTTP 长连接
↓
FLV Header
↓
Script / Video / Audio Tags 持续到达
FLV Tag 可以承载脚本元数据、视频或音频。flv.js 官方 README 给出的典型媒体组合是:
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 的核心链路是:
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 转换:
FLV
↓ Demux
H.264 + AAC
↓ Remux
fMP4
前后仍然是 H.264 与 AAC,没有把 H.264 解成 YUV,也没有把 AAC 解成 PCM,更没有再编码成另一种 Codec。因此它不是 Transcode,而是 Transmux / Remux。flv.js 更接近一个 FLV Demuxer 加 fMP4 Muxer,而不是 H.264 / AAC Decoder。
真正的 Codec 解码仍由浏览器媒体栈完成。MSE 提供的是 JavaScript 管理编码媒体片段的接口:程序创建 MediaSource 与 SourceBuffer,把初始化段和媒体段逐步 appendBuffer() 给媒体元素。MSE 本身不实现 H.264 或 AAC Decoder,也不会让浏览器原本不支持的 Codec 自动变得可播放。

9.2 为什么浏览器前面需要这层适配
服务器选择 HTTP-FLV,是因为 FLV Tag 可以沿一个 HTTP 长连接持续发送;浏览器侧则通常不把 HTTP-FLV 直接作为 <video> / MSE 的通用输入。浏览器更常见的组合是 MSE 加 ISO BMFF / fMP4,以及它自身支持的 Codec。
于是 flv.js 在两套生态之间做了适配:
服务器侧喜欢: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:
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。
不同规则形成不同消费模型:
传统 MP4
Packet 持续写入 + 全局文件索引
→ 偏完整文件
fMP4
一批 Packet → 一个 Fragment
→ 偏分段流式
MPEG-TS
媒体数据 → 固定长度 Transport Packet
→ 偏持续传输
因此,判断一个 Container 是否适合 Streaming,不能停在“Muxer 能不能一包一包写”。还要继续追问:播放器何时拿到必要上下文,用户能否从中间加入,网络损坏后怎样恢复,以及格式、协议与播放器生态是否共同支持目标场景。
Discussion
留言与讨论
想法、补充和不同意见都欢迎。