FFMPEG——时间戳、时间基、时间转换

时间基与时间戳是 FFmpeg 音视频同步、有序播放的核心机制,也是推流开发中最容易踩坑的环节。所有音视频帧的播放顺序、音画对齐、封装传输,都依赖统一的时间刻度规则与转换逻辑。

一、FFMPEG 时间基、 时间戳

1.1 时间基(time_base):

时间基也称之为时间基准,它代表的是每个刻度是多少秒。比方说:视频帧率是**25FPS,**那它的时间刻度是{1,25}。相当于1s内划分出25个等分,也就是每隔1/25秒后显示一帧视频数据。具体的如下图所示:

在FFMPEG中时间基准都是用AVRational结构体来表示:

复制代码
typedef struct AVRational{
    int num; // 分子
    int den; // 分母
} AVRational;

num 它是numerator的缩小,代表的是分子

den 它是denominator的缩小,代表的是分母

它的物理含义是:1 个时间刻度 = num/den 秒

在视频时间基都是以帧率为单位,比方说25帧。FFMPEG就以AVRational video_timebase = {1, 25}来表示。

在音频时间基都是以采样率为单位,比方说音频采样率是48000HZ。FFMPEG就以AVRationalaudio_timebase = {1, 48000}来表示。

对于封装格式来说:flv 封装格式的 time_base 为{1,1000},ts 封装格式的 time_base 为{1,90000}

从上图ffplay的信息我们可以看到有很多关于时间基的信息:

tbr **:**表示帧率,该帧率是一个基准,通常来说tbr和fps是一致的

tbn **:**表示视频流timebase(时间基),比方说:TS格式的数据timebase是90000,flv格式的视频流timebase为1000

tbc **:**表示视频流codec timebase,这个值一般为帧率的两倍。比方说:帧率是25fps,则tbc是50

核心原则:时间戳的数值本身没有意义,必须搭配对应的时间基才能换算出真实的时间。同一个真实时刻,用不同的时间基表示,数值完全不同。

1.2 时间戳(PTS、DTS):

首先时间戳它指的是在时间轴里面占了多少个格子,时间戳的单位不是具体的秒数,而是时间刻度。只有当时间基和时间戳结合在一起的时候,才能够真正表达出来时间是多少。

比方说:

有一把尺子pts = 25个刻度,time_base = {1,25} 每一个刻度是1/25厘米

所以这把尺子的长度 = pts * time_base = 25* 1/25= 1厘米

FFmpeg 中每个音视频帧(AVPacket/AVFrame)都带有两个核心时间戳,单位是对应模块的时间基刻度:

1.2.1 PTS

全称是Presentation Time Stamp(显示时间戳),它主要的作用是度量解码后的视频帧什么时候显示出来。标记这一帧应该被渲染 / 播放的真实时刻,决定了用户看到画面、听到声音的时间点。

视频PTS计算:n为第n帧视频帧,timebase是{1,framerate},fps是framerate

pts = n *(( 1 / timebase) / fps):

pts = pts++;

举例子:n = 1, pts = 1

n = 2, pts = 2

n =3, pts = 3

音频PTS计算:n为第n帧音频帧,nb_samples指的是采样个数(AAC默认1024),timebase是{1,samplerate},samplerate是采样率

Samplerate = 48000, nb_sample=1024, timebase = {1,48000}

num_pkt = samplerate/nb_samples

pts = n * ( ( 1/ timebase) / num_pkt)

pts = pts+1024

举例子:n = 1, pts = 1024

n = 2, pts = 2048

n = 3, pts = 3072

1.2.2 DTS

解码时间戳,标记这一帧应该被送入解码器解码的时刻,决定了解码器的工作顺序。在没有B帧的情况下PTS 等于 DTS。假设编码的里面引入了B帧,则还要计算B帧的时间。

没有B帧:dts = pts

存在B帧:dts = pts + b_time

  • 无 B 帧的场景(绝大多数监控、直播场景) :解码顺序和播放顺序完全一致,因此 PTS = DTS,这也是你 RV1126 硬件编码场景的默认情况。
  • 有 B 帧的场景:B 帧需要参考前后帧才能解码,解码顺序和播放顺序不一致,因此 DTS 和 PTS 会有差值,需要额外处理

二、FFmpeg 不同层级的时间基

FFmpeg 的不同模块有各自独立的时间基,这是新手最容易混淆的点。在推流场景下,重点关注三个层级的时间基:

1. 编码器层:AVCodecContext->time_base

  • 所属模块:编码器内部使用的时间刻度,是编码侧计算帧间隔、控制码率的基准。
  • 软编码场景下,送入编码器的 AVFrame 的 pts 必须以这个时间基为单位。
  • 本项目是硬件编码场景,编码由 VENC 硬件完成,FFmpeg 不参与编码,因此这个时间基不需要关注。

2. 流层:AVStream->time_base

  • 所属模块:封装层(AVFormat/AVStream)使用的时间刻度,是推流开发最核心的时间基。
  • 硬性规则:所有写入封装器的 AVPacket,它的 pts/dts 必须以对应流的 time_base 为单位
  • 这是我们开发中需要重点对齐的参数:硬件输出的时间戳必须转换为该时间基后,再赋值给 pkt->pts/dts。

3. 容器 / 协议层

  • 所属模块:具体封装格式或传输协议自带的时间刻度,比如 FLV/RTMP 固定为毫秒精度、MPEG-TS 固定为 90kHz。
  • 开发者一般不需要手动处理:FFmpeg 的封装器内部会自动把 AVStream->time_base 的时间戳,转换成容器 / 协议要求的刻度,不需要手动换算。

工程简化原则:推流开发时,我们只需要保证 AVPacket 的 pts/dts 和 AVStream->time_base 对应即可,底层协议的时间转换由 FFmpeg 自动完成。

三、时间转换的原理

时间基转换不能手写乘除,直接计算容易出现整数溢出、精度误差。FFmpeg 提供了标准转换函数 av_rescale_q,是所有时间基转换的唯一标准写法。

在FFMPEG中由于不同的复合流,时间基是不同的,比方说:flv的时间基time_base= {1,1000 },假设一个视频time_base = {1,25 },我们需要合成mpegts文件,它就需要把time_base = {1,25} 占的格子转换成time_base = {1,1000}占的格子。

3.1 在FFMPEG中用以下的API进行时间基转换:

void av_packet_rescale_ts(AVPacket *pkt, AVRational tb_src, AVRational tb_dst);

**第一个参数:**AVPacket结构体指针

**第二个参数:**源时间基

**第三个参数:**目的时间基

上面这个api的用法是,把AVPacket的时间基tb_src 转换成时间基tb_dst。下面我们用H264和AAC时间基TS转换的例子来说明这个转换时间基的用法:

3.2 视频H264时间基转换成MPEGTS时间基:

**DST_VIDEO_PTS = VIDEO_PTS * VIDEO_TIME_BASE / DST_TIME_BASE

H264 {1,25} flv{1,1000}

pts = 1 pts = 40

pts = 2 av_packet_rescale_ts pts = 80

pts = 3 pts = 120

pts = 4 pts = 160

3.3 音频AAC时间基转换成FLV时间基:

**DST_AUDIO_PTS = AUDIO_PTS * AUDIO_TIME_BASE / DST_TIME_BASE

AAC {1,48000} FLV{1,1000}

pts =1024 pts ~= 21.3

pts =2048 av_packet_rescale_ts pts ~= 42.6

pts =3072 pts ~= 64

pts =4096 pts ~= 85.3

从上述推导的结果可以看出来,如果使用av_packet_rescale_ts 的API对视频时间基进行转换,实际上是使用DST_VIDEO_PTS = VIDEO_PTS * VIDEO_TIME_BASE / DST_TIME_BASE去计算推流的视频时间戳。

同理用av_packet_rescale_ts 对音频时间基进行转换,实际上是使用DST_AUDIO_PTS = AUDIO_PTS * AUDIO_TIME_BASE / DST_TIME_BASE去计算我们真实推流的音频时间戳。