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 知识体系:时间戳分析流程
下面这张图是我自己总结的时间戳分析流程,每次遇到音画不同步的问题,我都会按这个步骤来排查:
这个流程我用了好几年,基本上能解决 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,那你就知道问题出在哪了。
我的习惯:每次拿到一个新的音视频文件,第一件事就是跑一遍这个脚本。就像医生看病先量体温一样,这是最基本的「体检」。