4. 编解码器参数详解
封装格式这东西,说白了就是个容器。它把视频流、音频流、字幕流打包在一起,再塞一堆元数据。但不同格式对时间戳的管理方式差别很大。我早年做直播项目时,就因为没搞清楚这些差异,导致音画不同步被客户追着骂了三天。嗯,今天咱们就把这事彻底聊透。
4.1 时间戳的基本单位:DTS 与 PTS
在聊封装格式之前,得先统一两个概念——DTS 和 PTS。这两个东西是所有同步逻辑的基石。
- DTS(Decoding Time Stamp):解码时间戳。告诉解码器什么时候该解码这个帧。
- PTS(Presentation Time Stamp):显示时间戳。告诉渲染器什么时候该显示这个帧。
对于大多数视频编码,B 帧的存在会导致 DTS 和 PTS 不一致。解码顺序和显示顺序是两码事。你想想看,如果没有 B 帧,那 DTS 和 PTS 其实是相等的。但一旦引入 B 帧,解码器必须先解码后面的参考帧,才能解码中间的 B 帧。
核心原则:封装格式只负责存储和传递 PTS/DTS,不负责修正它们。如果源头的时间戳就是错的,封装格式也救不了你。
4.2 MP4:基于时间的精确模型
MP4 是我个人最常用的封装格式。它的时间戳模型基于 mdhd(Media Header Box) 中的 timescale 和 duration。每个 track 都有自己的时间基。
MP4 的时间戳存储方式很有意思——它不存绝对时间,而是存 sample 的序号和 duration。举个例子:timescale = 90000,每个 sample 的 duration = 3000,那这个 sample 的时长就是 3000/90000 ≈ 33.33ms。解码器通过累加 duration 来推算 PTS。
// MP4 时间戳计算逻辑(伪代码)
timescale = moov.trak.mdia.mdhd.timescale; // 比如 90000
sample_duration = stts.entries[i].sample_delta; // 比如 3000
pts = cumulative_duration; // 累加得到
cumulative_duration += sample_duration;
我在项目中遇到过一个问题:某些编码器生成的 MP4 文件,stts 表里 sample_delta 写的是 0。这会导致播放器算出来的 PTS 全部归零,画面直接卡死。嗯,遇到这种情况,我一般会手动用 ffmpeg 的 -vsync 0 重新 remux 一遍。
避坑指南:MP4 的 edit list(elst box)经常被忽略。它定义了媒体时间的偏移量。如果你发现视频开头有几帧黑屏或静音,多半是 elst 里的 empty_duration 没处理好。我曾经排查过一个直播录播文件,开头多了 2 秒黑屏,最后发现是 elst 的 media_time 设成了 -1,导致播放器跳过了前两秒的媒体数据。
4.3 TS:为实时传输而生
TS(Transport Stream)是 MPEG-2 标准的一部分,最早用于数字电视广播。它的时间戳模型和 MP4 完全不同——TS 是 流式 的,每个包都携带时间信息。
TS 使用 PCR(Program Clock Reference) 作为系统时钟基准,PTS 和 DTS 则放在 PES(Packetized Elementary Stream)包头里。PCR 的精度是 27MHz,也就是说它的 tick 是 1/27000000 秒。PTS 和 DTS 的精度是 90kHz,tick 是 1/90000 秒。
| 参数 | TS 中的位置 | 精度 | 作用 |
|---|---|---|---|
| PCR | TS 适配域 | 27 MHz | 系统时钟恢复 |
| PTS | PES 包头 | 90 kHz | 显示时间 |
| DTS | PES 包头 | 90 kHz | 解码时间 |
TS 的同步机制有个特点:PCR 必须每隔 100ms 以内出现一次。如果 PCR 间隔太长,解码器的时钟锁相环会失锁,导致音画不同步。我记得有一次做 DVB 接收机调试,发现画面每隔几秒就卡一下,最后定位到是复用器配置的 PCR 间隔超过了 120ms。
注意:TS 的 PTS 和 DTS 是 33 位整数,最大只能表示约 26.5 小时(2^33 / 90000 秒)。如果你的直播流连续运行超过一天,PTS 会回绕。处理回绕逻辑时,一定要用有符号比较,否则时间戳会突然变成负数。
4.4 FLV:简单粗暴的时间戳
FLV 是 Adobe 的格式,在直播领域用得非常多。它的时间戳模型极其简单——每个 tag 头部都有一个 Timestamp 字段,单位是毫秒。没有 PCR,没有 timescale,就是一个整数。
但 FLV 有个坑:它的 Timestamp 字段只有 24 位,最大只能表示 2^24 - 1 = 16777215 毫秒,约 4.66 小时。超过这个值怎么办?FLV 在扩展字段里加了一个 TimestampExtended,作为高 8 位,拼起来变成 32 位整数。
// FLV 时间戳解析
uint32_t timestamp = (tag[7] << 24) | (tag[4] << 16) | (tag[5] << 8) | tag[6];
// 注意:tag[7] 是 TimestampExtended,放在第 8 个字节
我建议做直播推流时,如果使用 FLV 封装,一定要关注时间戳的连续性。很多推流工具在断线重连后会重置时间戳,导致播放端收到的时间戳突然变小。播放器如果按单调递增处理,就会认为新来的包是旧数据,直接丢弃。结果就是画面卡住不动。
个人经验:处理 FLV 时间戳回绕时,我习惯用 int32_t 做差值比较,而不是直接比较大小。比如 int32_t diff = new_ts - last_ts,如果 diff 在合理范围内(比如 0~5000ms),就认为是正常增量;如果 diff 是负数且绝对值很大,说明发生了回绕或重连。
4.5 三种格式的时间戳处理对比
下面这张表是我自己整理的,平时做方案选型时经常参考。
| 特性 | MP4 | TS | FLV |
|---|---|---|---|
| 时间基 | 自定义 timescale | 27 MHz / 90 kHz | 毫秒(固定) |
| 时间戳存储 | sample duration 累加 | PES 包头 + PCR | tag 头部 |
| 精度 | 取决于 timescale | 高(27 MHz) | 低(1 ms) |
| 回绕周期 | 取决于 timescale | 约 26.5 小时 | 约 4.66 小时 |
| 适用场景 | 点播、本地文件 | 广播、实时传输 | 直播、RTMP 推流 |
| 同步依赖 | 容器自身 | PCR 时钟恢复 | 推流端保证 |
4.6 核心流程:封装格式的时间戳处理逻辑
为了让你更直观地理解这三种格式的处理差异,我画了一张流程图。它展示了从原始帧到封装输出的完整时间戳处理路径。
4.7 实际项目中的选择建议
说了这么多,到底该用哪种格式?我个人的经验是这样的:
- 点播场景:无脑选 MP4。它的时间戳模型最精确,seek 也方便。但要注意编码器生成的 MP4 是否包含 moov box 在文件头部,否则需要 faststart 优化。
- 直播推流:FLV 是主流。RTMP 协议原生支持 FLV,延迟低。但推流端必须保证时间戳单调递增,不能回绕或跳变。
- 广播级传输:TS 是唯一选择。它的 PCR 机制可以保证多路节目的时钟同步。但 TS 的 overhead 比较大,每个包只有 188 字节,有效载荷占比低。
曾经踩过的坑:有一次做多机位直播,用了 FLV 封装推流到 CDN。其中一个机位的编码器在断网重连后,时间戳从 0 开始重新计数。CDN 收到的时间戳比主机的还小,直接判定为过期数据丢弃了。最后我在推流端加了一个时间戳偏移量,每次重连后加上当前系统时间,才解决了这个问题。
封装格式的时间戳处理,说白了就是「怎么把时间信息塞进容器里,再让播放器能准确还原出来」。MP4 靠累加,TS 靠 PCR,FLV 靠推流端保证。没有哪种格式是万能的,关键看你的应用场景和容错能力。