2. FFmpeg时间戳处理:解析ffprobe输出的时间戳信息,手动计算音画延迟
时间戳,说白了就是音画同步的「命根子」。
我在刚接触FFmpeg那会儿,总觉得时间戳是个很玄乎的东西。直到有一次,我拿到一个视频文件,画面和声音差了将近两秒,怎么调都调不回来。后来才发现,是我根本没读懂ffprobe输出的时间戳信息。
嗯,这节课我们就来彻底搞懂它。
2.1 时间戳的基本单位:时间基(time_base)
先问一个问题:FFmpeg内部用什么来表示时间?
不是秒,不是毫秒,而是一个叫「时间基」的东西。每个流都有自己的时间基,说白了就是一个分数。比如 1/90000 表示每个时间单位是 1/90000 秒,1/1000 就是 1 毫秒。
核心公式:
实际时间(秒) = 时间戳值 × 时间基
举个例子:
- 视频时间戳 PTS = 3600,时间基 = 1/90000
- 实际时间 = 3600 × (1/90000) = 0.04 秒 = 40 毫秒
我个人习惯把时间基想象成「一把尺子的刻度」。刻度越细,精度越高。视频流常用 1/90000,音频流常用 1/48000 或 1/44100。
2.2 用ffprobe抓取时间戳信息
先跑一条命令看看原始数据长什么样:
ffprobe -v quiet -print_format json -show_streams -show_packets input.mp4
输出会很长,我们只关注关键字段。我截取一段实际项目中的输出:
{
"streams": [
{
"index": 0,
"codec_type": "video",
"time_base": "1/90000",
"start_pts": 0,
"start_time": "0.000000",
"duration_ts": 540000,
"duration": "6.000000",
"nb_frames": 180
},
{
"index": 1,
"codec_type": "audio",
"time_base": "1/48000",
"start_pts": 0,
"start_time": "0.000000",
"duration_ts": 288000,
"duration": "6.000000",
"nb_frames": 282
}
],
"packets": [
{
"codec_type": "video",
"pts": 0,
"pts_time": "0.000000",
"dts": 0,
"dts_time": "0.000000",
"duration": 3000,
"duration_time": "0.033333"
},
{
"codec_type": "audio",
"pts": 0,
"pts_time": "0.000000",
"dts": 0,
"dts_time": "0.000000",
"duration": 1024,
"duration_time": "0.021333"
}
]
}
小技巧:用 -print_format json 输出 JSON 格式,方便程序解析。我写脚本时经常配合 jq 命令做过滤,比如 ffprobe ... | jq '.streams[] | select(.codec_type=="video")'。
2.3 手动计算音画延迟
好了,现在我们来干正事——算延迟。
音画不同步,本质上是音频和视频的 PTS 没有对齐。我们需要找到「同一时刻」的音视频包,看它们的 PTS 差值。
2.3.1 提取第一个视频包和第一个音频包
用这条命令只抓前几个包:
ffprobe -v quiet -print_format json -show_packets -read_intervals "%+#10" input.mp4
-read_intervals "%+#10" 的意思是只读前 10 个包。输出类似这样:
{
"packets": [
{"codec_type": "video", "pts": 0, "pts_time": "0.000000"},
{"codec_type": "audio", "pts": 1920, "pts_time": "0.040000"},
{"codec_type": "video", "pts": 3000, "pts_time": "0.033333"},
{"codec_type": "audio", "pts": 2944, "pts_time": "0.061333"}
]
}
注意看:第一个视频包 pts_time = 0.000000,第一个音频包 pts_time = 0.040000。
注意:音频的 PTS 不一定从 0 开始。很多编码器会给音频加一个初始偏移,这是正常的。真正的延迟要看「稳定后」的差值。
2.3.2 计算差值
取第 5 个视频包和第 5 个音频包:
| 包序号 | 视频 pts_time | 音频 pts_time | 差值(音频-视频) |
|---|---|---|---|
| 1 | 0.000000 | 0.040000 | +0.040000 |
| 2 | 0.033333 | 0.061333 | +0.028000 |
| 3 | 0.066667 | 0.082667 | +0.016000 |
| 4 | 0.100000 | 0.104000 | +0.004000 |
| 5 | 0.133333 | 0.125333 | -0.008000 |
看到没?差值在逐渐变小,到第 5 个包时已经变成负的了。这说明音频比视频快了 8 毫秒。
我曾经遇到过一个极端案例:某个直播流的音频比视频快了整整 3 秒。排查了半天,发现是推流端的时间基设置错了,音频用了 1/1000 而视频用了 1/90000,导致 PTS 换算后完全对不上。
2.4 用脚本自动化计算
手动算太累?写个脚本吧。我常用的 Python 片段:
import json
import subprocess
def get_pts_diff(filename, stream_index=0):
cmd = [
'ffprobe', '-v', 'quiet',
'-print_format', 'json',
'-show_packets',
'-read_intervals', '%+#50',
filename
]
result = subprocess.run(cmd, capture_output=True, text=True)
data = json.loads(result.stdout)
video_pts = []
audio_pts = []
for pkt in data['packets']:
if pkt['codec_type'] == 'video':
video_pts.append(float(pkt['pts_time']))
elif pkt['codec_type'] == 'audio':
audio_pts.append(float(pkt['pts_time']))
# 取中间段的包做比较,避开开头
if len(video_pts) > 10 and len(audio_pts) > 10:
v = video_pts[10]
a = audio_pts[10]
diff = a - v
return diff
return None
diff = get_pts_diff('input.mp4')
print(f"音画延迟: {diff*1000:.2f} ms")
# 正数表示音频落后,负数表示音频超前
经验之谈:不要只看前几个包。开头可能有编码器初始化数据,PTS 不稳定。我一般取第 10 到第 30 个包的平均值,这样更可靠。
2.5 时间戳知识体系
下面这张图总结了时间戳的核心概念和计算流程:
2.6 常见问题与避坑
- 时间基不一致:视频用 1/90000,音频用 1/48000,直接比 PTS 原始值没有意义。必须先换算成秒。
- B帧导致 PTS 和 DTS 不同:B帧的 PTS 小于后面的 I帧,但 DTS 是递增的。计算延迟时用 PTS,不要用 DTS。
- 开头有黑帧或静音:有些视频开头会插入黑帧或静音段,导致前几个包的 PTS 异常。我一般跳过前 5% 的包。
我曾经踩过的坑:有一次用 -read_intervals 只抓了前 5 个包,算出来延迟 200ms。后来发现前 5 个包全是视频关键帧,音频包还没开始。正确的做法是抓足够多的包,取中间段做统计。
2.7 小结
时间戳计算音画延迟,说白了就是三步:
- 用 ffprobe 拿到 PTS 和时间基
- 换算成秒,取中间段的包
- 算差值,正负决定谁快谁慢
掌握了这个基本功,后面讲「用 setpts 和 atempo 做精准对齐」时,你就能理解为什么那些参数要那么写了。