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 核心流程:封装格式的时间戳处理逻辑

为了让你更直观地理解这三种格式的处理差异,我画了一张流程图。它展示了从原始帧到封装输出的完整时间戳处理路径。

封装格式时间戳处理流程对比 原始帧(PTS/DTS) 选择封装格式 MP4:计算 sample duration 写入 stts / stsz box TS:生成 PCR + PES 封装为 TS 包(188 字节) FLV:写入 tag 头部 处理回绕(24→32 位) 封装文件输出 关键差异:MP4 依赖累加 duration,TS 依赖 PCR 时钟恢复,FLV 依赖推流端保证时间戳连续性

4.7 实际项目中的选择建议

说了这么多,到底该用哪种格式?我个人的经验是这样的:

  • 点播场景:无脑选 MP4。它的时间戳模型最精确,seek 也方便。但要注意编码器生成的 MP4 是否包含 moov box 在文件头部,否则需要 faststart 优化。
  • 直播推流:FLV 是主流。RTMP 协议原生支持 FLV,延迟低。但推流端必须保证时间戳单调递增,不能回绕或跳变。
  • 广播级传输:TS 是唯一选择。它的 PCR 机制可以保证多路节目的时钟同步。但 TS 的 overhead 比较大,每个包只有 188 字节,有效载荷占比低。

曾经踩过的坑:有一次做多机位直播,用了 FLV 封装推流到 CDN。其中一个机位的编码器在断网重连后,时间戳从 0 开始重新计数。CDN 收到的时间戳比主机的还小,直接判定为过期数据丢弃了。最后我在推流端加了一个时间戳偏移量,每次重连后加上当前系统时间,才解决了这个问题。

封装格式的时间戳处理,说白了就是「怎么把时间信息塞进容器里,再让播放器能准确还原出来」。MP4 靠累加,TS 靠 PCR,FLV 靠推流端保证。没有哪种格式是万能的,关键看你的应用场景和容错能力。