3、音视频流的时间戳分析:使用 ffprobe 分析音视频流的时间戳信息

时间戳,说白了就是音视频数据的「时钟」。没有它,画面和声音就是两匹脱缰的野马,各跑各的。我刚开始接触音视频开发时,总觉得时间戳就是个简单的数字,直到被一个 200ms 的延迟折磨了整整三天……嗯,从那以后我再也不敢小看它了。

这一节,我们就用 ffprobe 这把瑞士军刀,把时间戳的底裤扒个干净。

3.1 时间戳的基本概念

在 FFmpeg 的世界里,时间戳主要分三种。我习惯把它们比作「三兄弟」:

缩写 全称 中文 我的理解
DTS Decoding Time Stamp 解码时间戳 告诉解码器「什么时候该干活」
PTS Presentation Time Stamp 显示时间戳 告诉显示器「什么时候该亮出来」
CTS Composition Time Stamp 合成时间戳 其实就是 PTS - DTS 的差值

关键点:对于大多数视频流,DTS 和 PTS 是相等的。只有在存在 B 帧(双向预测帧)时,两者才会出现差异。音频流通常只有 PTS,没有 DTS 的概念。

你想想看,为什么要有 DTS 和 PTS 两套时间?因为解码顺序和显示顺序不一定一样。比如一个 GOP(图像组)的帧顺序是 I-B-B-P,解码器必须先解 I 帧,再解 P 帧,最后才能解 B 帧。但显示的时候,B 帧要插在 I 和 P 之间。这就是 DTS 和 PTS 存在的意义。

3.2 使用 ffprobe 查看时间戳

ffprobe 是 FFmpeg 家族里的「侦察兵」。我个人习惯用它来快速诊断音视频流的时间戳问题。最基本的命令长这样:

ffprobe -v quiet -print_format json -show_streams input.mp4

这条命令会输出一个 JSON,里面包含了每个流(stream)的详细信息。我们重点关注以下几个字段:

  • codec_type:流类型(video/audio)
  • time_base:时间基,时间戳的「刻度」
  • start_pts:起始 PTS 值
  • start_time:起始时间(秒)
  • duration_ts:时长(以时间基为单位)
  • duration:时长(秒)
  • nb_frames:帧数

小技巧:如果只想看时间戳相关的信息,可以用 -show_entries 参数过滤。比如:ffprobe -v quiet -show_entries stream=index,codec_type,start_pts,start_time,duration_ts,duration,nb_frames input.mp4

3.3 实战:分析一个音视频流

我们来跑一个真实的例子。假设有一个 test.mp4 文件,执行:

ffprobe -v quiet -print_format json -show_streams test.mp4

输出结果(简化版)大概长这样:

{
  "streams": [
    {
      "index": 0,
      "codec_type": "video",
      "codec_name": "h264",
      "time_base": "1/90000",
      "start_pts": 0,
      "start_time": "0.000000",
      "duration_ts": 180000,
      "duration": "2.000000",
      "nb_frames": 60
    },
    {
      "index": 1,
      "codec_type": "audio",
      "codec_name": "aac",
      "time_base": "1/44100",
      "start_pts": 0,
      "start_time": "0.000000",
      "duration_ts": 88200,
      "duration": "2.000000",
      "nb_frames": 86
    }
  ]
}

看到这里,你可能会问:「为什么视频的时间基是 1/90000,音频却是 1/44100?」

嗯,这是个好问题。视频的 90000 是 MPEG 标准里定义的「90kHz 时钟」,说白了就是为了让时间戳有足够高的精度。音频的 44100 就是采样率,一个时间基对应一个采样点。

注意:如果 start_time 不是 0,说明这个文件有「前导空白」。我曾经遇到过一些录播文件,视频从 0 开始,音频却从 0.5 秒开始,结果播放时前 0.5 秒只有画面没有声音。这种问题用 ffprobe 一眼就能看出来。

3.4 时间戳的换算方法

时间戳和时间(秒)之间的换算,其实就是一个简单的乘法:

时间(秒) = 时间戳值 × 时间基

举个例子,上面视频流的 duration_ts 是 180000,时间基是 1/90000,那么:

duration = 180000 × (1/90000) = 2.0 秒

反过来,如果你想把 2 秒换算成时间戳:

时间戳值 = 2.0 / (1/90000) = 180000

是不是很简单?但我在项目中遇到过一种坑:有些封装格式(比如 TS)的时间基是 1/90000,但内部流的实际时间基可能不同。这时候如果直接拿时间戳去算,结果会差很多。

3.5 音视频时间戳的同步分析

现在我们来点硬核的。用 ffprobe 逐帧分析时间戳:

ffprobe -v quiet -print_format json -show_frames test.mp4

这个命令会输出每一帧的详细信息。我们重点关注:

  • pkt_pts:该帧的 PTS 值
  • pkt_dts:该帧的 DTS 值
  • pkt_duration:该帧的持续时间
  • pkt_pos:该帧在文件中的位置

我习惯把视频帧和音频帧的 PTS 拉出来对比。如果发现某个时间点,视频帧的 PTS 和音频帧的 PTS 差距超过一个阈值(比如 100ms),那就说明音画不同步了。

经验值:人耳对音画不同步的敏感度大约是 100ms。超过这个值,观众就会觉得「嘴型对不上」。专业广播级的要求是 ±40ms 以内。

3.6 常见的时间戳问题

用 ffprobe 分析时间戳时,我总结了几种常见的「病状」:

症状 可能原因 ffprobe 怎么看
视频流 start_time 不为 0 文件裁剪或拼接后未重置时间戳 查看 start_time 字段
音频流 start_time 与视频不一致 录制时音视频设备启动时间不同 对比两个流的 start_time
PTS 出现回退(非单调递增) B 帧导致 DTS 与 PTS 顺序不同 查看 pkt_pts 序列
时间戳跳变(gap) 丢帧或编码器异常 计算相邻帧的 PTS 差值
duration_ts 为 0 流信息不完整或损坏 查看 duration_ts 字段

我曾经遇到过一个直播流,视频的 PTS 每隔几秒就跳变一次,导致播放器反复缓冲。用 ffprobe 一查,发现是推流端的编码器时间基设置错了,把 1/1000 写成了 1/10000。这种问题,光靠看画面是看不出来的,必须用工具分析时间戳。

3.7 知识体系:时间戳分析流程

下面这张图是我自己总结的时间戳分析流程,每次遇到音画不同步的问题,我都会按这个步骤来排查:

时间戳分析流程 步骤1:ffprobe -show_streams 步骤2:检查 time_base 是否一致 步骤3:对比 start_time 是否对齐 步骤4:ffprobe -show_frames 逐帧分析 步骤5:计算音视频 PTS 差值 差值 < 100ms ✓ 差值 ≥ 100ms ✗ 说明:差值 = |视频帧 PTS - 对应音频帧 PTS|,需换算为秒后再比较

这个流程我用了好几年,基本上能解决 90% 的时间戳问题。剩下的 10%,往往是封装格式的「潜规则」导致的,比如某些 MP4 文件的 edit list 会影响时间戳的起始点。

3.8 一个实用的脚本

最后,分享一个我常用的脚本。它可以快速提取音视频流的时间戳信息,并计算两者的偏移量:

#!/bin/bash
# 用法:./check_av_sync.sh input.mp4

INPUT=$1

echo "=== 视频流信息 ==="
ffprobe -v quiet -show_entries stream=index,codec_type,start_pts,start_time,duration_ts,duration,nb_frames \
  -of csv=p=0 "$INPUT" | grep video

echo ""
echo "=== 音频流信息 ==="
ffprobe -v quiet -show_entries stream=index,codec_type,start_pts,start_time,duration_ts,duration,nb_frames \
  -of csv=p=0 "$INPUT" | grep audio

echo ""
echo "=== 前10帧 PTS 对比 ==="
ffprobe -v quiet -show_entries frame=media_type,pkt_pts,pkt_dts_time \
  -of csv=p=0 "$INPUT" | head -20

这个脚本的输出很直观。如果视频的 start_time 是 0.000,音频的 start_time 是 0.500,那你就知道问题出在哪了。

我的习惯:每次拿到一个新的音视频文件,第一件事就是跑一遍这个脚本。就像医生看病先量体温一样,这是最基本的「体检」。