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 时间戳知识体系

下面这张图总结了时间戳的核心概念和计算流程:

FFmpeg 时间戳知识体系 时间基 time_base PTS 显示时间戳 DTS 解码时间戳 ffprobe 关键字段 pts / pts_time / dts / dts_time / duration / duration_time 手动计算音画延迟 步骤1:提取视频/音频包 PTS 步骤2:跳过前 N 个不稳定包 步骤3:计算差值 = 音频PTS - 视频PTS 结果:差值 ≈ 0 → 同步 | 差值 > 0 → 音频落后 | 差值 < 0 → 音频超前 注意:音画延迟 < 20ms 人耳基本无感知,> 100ms 明显不同步

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 小结

时间戳计算音画延迟,说白了就是三步:

  1. 用 ffprobe 拿到 PTS 和时间基
  2. 换算成秒,取中间段的包
  3. 算差值,正负决定谁快谁慢

掌握了这个基本功,后面讲「用 setpts 和 atempo 做精准对齐」时,你就能理解为什么那些参数要那么写了。