4、封装格式对同步的影响:MP4、TS、FLV、MKV 等格式的时间戳处理差异
封装格式这东西,说白了就是音视频数据的「容器」。不同的容器,对时间戳的存储方式、精度、甚至语义都不一样。我刚开始做流媒体的时候,就吃过这个亏——同样的源文件,换个封装格式,音画就不同步了。你想想看,问题出在哪?
嗯,核心就在于时间戳的「坐标系」和「存储粒度」。今天咱们就把 MP4、TS、FLV、MKV 这四种最常见的格式,一个一个拆开看。
4.1 MP4:基于「样本」的精确时间戳
MP4 是我个人用得最多的封装格式。它的时间戳体系非常严谨,基于 MDAT(媒体数据)和 MOOV(元数据)两个核心 box 来组织。
在 MP4 里,每个音频帧或视频帧都是一个「样本」(sample)。时间戳不是直接存数值,而是通过 stts(时间到样本)和 ctts(合成时间到样本)这两个 box 来推算的。
关键点:MP4 的时间戳单位是「时间尺度」(timescale),通常视频用 90000,音频用 48000 或 44100。PTS 和 DTS 的差值通过 ctts box 记录,支持 B 帧。
我在项目中遇到过一个问题:用 FFmpeg 转码 MP4 时,如果 -vsync 参数设置不对,时间戳会跳变。比如 -vsync vfr 会丢弃重复帧,导致时间戳不连续,播放器就可能卡顿或音画错位。
# 查看 MP4 的时间戳信息
ffprobe -v error -select_streams v:0 -show_entries packet=pts_time,dts_time,duration_time input.mp4
我的习惯:处理 MP4 时,尽量保持 -vsync cfr(恒定帧率),除非你明确知道源文件是 VFR。另外,MP4 不支持「全局时间戳偏移」,所以拼接多个 MP4 时一定要用 concat 协议,否则时间戳会乱。
4.2 TS:基于「时钟」的实时同步
TS(Transport Stream)是广播电视和直播的标配。它的时间戳体系跟 MP4 完全不同——TS 不依赖样本索引,而是靠 PCR(节目时钟参考)和 PTS/DTS 包来维持同步。
TS 的每个包固定 188 字节,时间戳信息分散在 PES 包头部。PCR 每 100ms 至少出现一次,用来恢复编码器时钟。说白了,TS 是为「实时传输」设计的,时间戳的精度取决于 PCR 的抖动。
| 参数 | MP4 | TS |
|---|---|---|
| 时间戳存储 | stts/ctts box 推算 | PES 包头 + PCR |
| 精度 | 取决于 timescale(通常 90000) | 27MHz 时钟派生,90kHz 基准 |
| B 帧支持 | 完整支持(ctts) | 支持,但 DTS 必须单调递增 |
| 拼接难度 | 需要重写 moov | 直接拼接,但 PCR 可能不连续 |
我记得有一次做直播录制,TS 文件播放时音画不同步。排查了半天,发现是编码器 PCR 间隔太大,超过了 100ms。播放器恢复时钟时产生了漂移。解决办法很简单:在 FFmpeg 里强制设置 PCR 间隔。
# 强制 PCR 间隔为 30ms
ffmpeg -i input.ts -c copy -mpegts_flags +initial_discontinuity -mpegts_pcr_period 30 output.ts
避坑指南:我曾经在拼接多个 TS 文件时,直接用了 cat 命令。结果 PCR 不连续,播放器直接卡死。正确做法是用 concat 协议,或者用 -mpegts_flags +resend_headers 重置 PCR。
4.3 FLV:简单粗暴的「绝对时间戳」
FLV 是 RTMP 直播的御用格式。它的时间戳设计非常「直白」——每个音视频 tag 都带一个 绝对时间戳(单位毫秒),从第一个 tag 开始累加。
但这里有个坑:FLV 的时间戳是 uint32 类型,最大值只有 4,294,967,295 毫秒,约 49.7 天。超过这个值就会溢出。我见过一些长时间直播的录制文件,时间戳溢出后直接归零,音画瞬间错位。
FLV 时间戳特点:
- 音频和视频的时间戳是独立的,但播放器需要对齐
- 不支持 B 帧(或者说,FLV 的 DTS 和 PTS 必须相同)
- 时间戳单位固定为毫秒,精度不如 MP4/TS
FFmpeg 在处理 FLV 时,会自动把时间戳转换为毫秒。如果你从 MP4 转 FLV,要注意帧率转换带来的时间戳误差。比如 29.97fps 的视频,每帧间隔约 33.367ms,但 FLV 只能存整数毫秒,累积误差就会越来越大。
# 从 MP4 转 FLV,注意时间戳精度损失
ffmpeg -i input.mp4 -c copy -f flv output.flv
我的建议:如果源文件是 VFR(可变帧率),转 FLV 前最好先转成 CFR。否则时间戳抖动会让你怀疑人生。另外,FLV 不支持全局时间戳偏移,所以做延迟播放时,需要在应用层处理。
4.4 MKV:灵活但复杂的「时间码」体系
MKV(Matroska)是我最喜欢的封装格式,没有之一。它的时间戳体系非常灵活——每个 block 可以独立设置时间戳,支持 时间码缩放(TimecodeScale),甚至允许负时间戳。
MKV 的时间戳单位是纳秒(通过 TimecodeScale 缩放),精度极高。但灵活性也带来了复杂性:MKV 允许「无序时间戳」(比如 B 帧的 DTS 和 PTS 顺序不同),有些播放器处理不好就会出问题。
| 特性 | MKV | MP4 | TS | FLV |
|---|---|---|---|---|
| 时间戳精度 | 纳秒级 | 微秒级(取决于 timescale) | 微秒级(90kHz) | 毫秒级 |
| B 帧支持 | 完整支持 | 完整支持 | 支持 | 不支持 |
| 时间戳偏移 | 支持全局偏移 | 不支持 | 不支持 | 不支持 |
| 拼接难度 | 中等(需重写 Cues) | 高(需重写 moov) | 低(但 PCR 需处理) | 低(直接追加) |
我在做 MKV 封装时,最常遇到的问题是 Cues(索引)损坏。如果 Cues 里的时间戳和实际 block 不一致,播放器拖动进度条时就会音画不同步。FFmpeg 默认生成的 Cues 有时不够精确,我习惯手动指定。
# 生成精确的 Cues 索引
ffmpeg -i input.mkv -c copy -cues_to_front true output.mkv
注意:MKV 的 TimecodeScale 默认是 1000000(即 1ms),但如果你用 -timecode_scale 改了它,所有时间戳都会按新比例缩放。我曾经把一个 25fps 的视频转成 MKV,忘了设置 TimecodeScale,结果播放器以为时间戳是纳秒级,画面直接快进了 1000 倍。
4.5 四种格式的时间戳处理对比
说了这么多,咱们用一张图来总结。下面这张 SVG 展示了四种封装格式的时间戳处理核心逻辑:
4.6 格式转换时的时间戳陷阱
格式转换是音画同步问题的高发区。我总结了几条经验:
- MP4 → TS:注意 B 帧的 DTS 顺序。MP4 允许 DTS 不连续,但 TS 要求 DTS 严格递增。FFmpeg 会自动重排,但会引入延迟。
- TS → FLV:TS 的 90kHz 时间戳转成毫秒时,会有截断误差。建议用
-r参数强制帧率,减少累积误差。 - MKV → MP4:MKV 的负时间戳在 MP4 里不支持。FFmpeg 会整体偏移时间戳,但偏移量可能不是整数帧,导致音画错位。
- FLV → MKV:FLV 的毫秒精度太低,转成 MKV 后时间戳抖动明显。我一般先转成 TS 做一次时间戳平滑,再封装成 MKV。
# 一个稳妥的格式转换命令(MP4 → MKV,保持时间戳精度)
ffmpeg -i input.mp4 -c copy -timecode_scale 1000000 -cues_to_front true output.mkv
最后说一句:不管你用哪种封装格式,-fflags +genpts 这个参数都值得记住。它能强制 FFmpeg 在解封装时重新生成 PTS,很多时间戳问题都能靠它「救回来」。