ffmpeg深入理解H264中的时间戳( DTS和PTS)


一、视频的播放过程?

视频的播放过程可以简单理解为一帧一帧的画面按照时间顺序呈现出来的过程,就像在一个本子的每一页画上画,然后快速翻动的感觉。但是在实际应用中,并不是每一帧都是完整的画面,因为如果每一帧画面都是完整的图片,那么一个视频的体积就会很大,这样对于网络传输或者视频数据存储来说成本太高,所以通常会对视频流中的一部分画面进行压缩(编码)处理。由于压缩处理的方式不同,视频中的画面帧就分为了不同的类别,其中包括:I 帧、P 帧、B 帧。

在这里插入图片描述

二、I、P、B 帧的区别

  • I 帧( 帧内编码帧 ):I 帧图像采用帧内编码方式,即只利用了单帧图像内的空间相关性,而没有利用时间相关性。I帧使用帧内压缩,不使用运动补偿,由于 I 帧不依赖其它帧,所以是随机存取的入点,同时是解码的基准帧。I帧主要用于接收机的初始化和信道的获取,以及节目的切换和插入,I 帧图像的压缩倍数相对较低。I帧图像是周期性出现在图像序列中的,出现频率可由编码器选择。
  • P 帧(前向预测编码帧 ):P 帧和 B 帧图像采用帧间编码方式,即同时利用了空间和时间上的相关性。P帧图像只采用前向时间预测,可以提高压缩效率和图像质量。P 帧图像中可以包含帧内编码的部分,即 P帧中的每一个宏块可以是前向预测,也可以是帧内编码。
  • B 帧(双向预测内插编码帧 ):B帧图像采用双向时间预测,可以大大提高压缩倍数。值得注意的是,由于 B 帧图像采用了未来帧作为参考,因此 MPEG-2编码码流中图像帧的传输顺序和显示顺序是不同的。

也就是说,一个 I 帧可以不依赖其他帧就解码出一幅完整的图像,而 P 帧、B 帧不行。P 帧需要依赖视频流中排在它前面的帧才能解码出图像。B 帧则需要依赖视频流中排在它前面或后面的帧才能解码出图像。

这就带来一个问题:在视频流中,先到来的 B 帧无法立即解码,需要等待它依赖的后面的 I、P 帧先解码完成,这样一来播放时间与解码时间不一致了,顺序打乱了,那这些帧该如何播放呢?这时就需要我们来了解另外两个概念:DTS 和 PTS。

三、DTS、PTS 的概念

  • DTS(Decoding Time Stamp):即解码时间戳,这个时间戳的意义在于告诉播放器该在什么时候解码这一帧的数据。
  • PTS(Presentation Time Stamp):即显示时间戳,这个时间戳用来告诉播放器该在什么时候显示这一帧的数据。

需要注意的是:
虽然 DTS、PTS 是用于指导播放端的行为,但它们是在编码的时候由编码器生成的。

当视频流中没有 B 帧时,通常 DTS 和 PTS 的顺序是一致的。但如果有 B 帧时,就回到了我们前面说的问题:解码顺序和播放顺序不一致了。

比如一个视频中,帧的显示顺序是:I B B P,现在我们需要在解码 B 帧时知道 P 帧中信息,因此这几帧在视频流中的顺序可能是:I P B B,这时候就体现出每帧都有 DTS 和 PTS 的作用了。DTS 告诉我们该按什么顺序解码这几帧图像,PTS 告诉我们该按什么顺序显示这几帧图像。顺序大概如下:

PTS: 1 4 2 3
DTS: 1 2 3 4
Stream: I P B B

在这里插入图片描述

四、PTS和DTS的时间基

PST和DTS的单位是什么?

FFmpeg中时间基的概念,也就是time_base。它也是用来度量时间的。 如果把1秒分为25等份,你可以理解就是一把尺,那么每一格表示的就是1/25秒。此时的time_base={1,25}
pts的值就是占多少个时间刻度(占多少个格子)。它的单位不是秒,而是时间刻度。只有pts加上time_base两者同时在一起,才能表达出时间是多少。

pts=20个刻度 
time_base={
    
    1,10} 每一个刻度是1/10厘米 
所以物体的长度=pts*time_base=20*1/10 厘米
我们当前的时间是 100, 时间基是 1/25,那么转成秒的时间是多少呢? 100*时音基(1/25),也就是100 * 1/25 = 4秒。

duration和pts单位一样,duration表示当前帧的持续时间占多少格。或者理解是两帧的间隔时间是占多少格。

五、FFMPEG的AVRational time_base

内部时间基 AV_TIME_BASE

// Internal time base represented as integer
#define AV_TIME_BASE            1000000
// Internal time base represented as fractional value
#define AV_TIME_BASE_Q          (AVRational){
      
      1, AV_TIME_BASE}
typedef struct AVRational{
    
    
    int num; ///< numerator
    int den; ///< denominator
} AVRational;

AVRational这个结构标识一个分数,num为分数,den为分母。
pts:格子数

av_q2d(st->time_base):/

计算视频长度

time() = st->duration * av_q2d(st->time_base)

帧的显示时间戳

pts*av_q2d(time_base)

时间值形式转换

av_q2d()将时间从 AVRational 形式转换为 double 形式。AVRational 是分数类型,double 是双精度浮点数类型,转换的结果单位是秒。

/**
 * Convert an AVRational to a `double`.
 * @param a AVRational to convert
 * @return `a` in floating-point form
 * @see av_d2q()
 */
static inline double av_q2d(AVRational a){
    
    
    return a.num / (double) a.den;
}

av_q2d()使用方法如下:

AVStream stream;
AVPacket packet;
packet 播放时刻值:timestamp(单位秒) = packet.pts × av_q2d(stream.time_base);
packet 播放时长值:duration(单位秒) = packet.duration × av_q2d(stream.time_base);

转码过程中的时间基转换

视频解码过程中的时间基转换处理:

AVFormatContext *ifmt_ctx;
AVStream *in_stream;
AVCodecContext *dec_ctx;
AVPacket packet;
AVFrame *frame;

// 从输入文件中读取编码帧
av_read_frame(ifmt_ctx, &packet);

// 时间基转换
int raw_video_time_base = av_inv_q(dec_ctx->framerate);
av_packet_rescale_ts(packet, in_stream->time_base, raw_video_time_base);

// 解码
avcodec_send_packet(dec_ctx, packet)
avcodec_receive_frame(dec_ctx, frame);

视频编码过程中的时间基转换处理:

AVFormatContext *ofmt_ctx;
AVStream *out_stream;
AVCodecContext *dec_ctx;
AVCodecContext *enc_ctx;
AVPacket packet;
AVFrame *frame;

// 编码
avcodec_send_frame(enc_ctx, frame);
avcodec_receive_packet(enc_ctx, packet);

// 时间基转换
packet.stream_index = out_stream_idx;
enc_ctx->time_base = av_inv_q(dec_ctx->framerate);
av_packet_rescale_ts(&opacket, enc_ctx->time_base, out_stream->time_base);

// 将编码帧写入输出媒体文件
av_interleaved_write_frame(o_fmt_ctx, &packet);

案例

/*
*知识点1:av_q2d(AVRational a)函数
    av_q2d(AVRational);该函数负责把AVRational结构转换成double,通过这个函数可以计算出某一帧在视频中的时间位置
    timestamp(秒) = pts * av_q2d(st->time_base);
    计算视频长度的方法:
    time(秒) = st->duration * av_q2d(st->time_base);

*知识点2:av_rescale_q(int64_t a, AVRational bq, AVRational cq)函数
    这个函数的作用是计算a*bq / cq来把时间戳从一个时间基调整到另外一个时间基。在进行时间基转换的时候,应该首先这个函数,因为它可以避免溢出的情况发生

*知识点3:ffmpeg内部的时间与标准的时间转换方法:
    timestamp(ffmpeg内部的时间戳) = AV_TIME_BASE * time(秒)
    time(秒) = AV_TIME_BASE_Q * timestamp(ffmpeg内部的时间戳)

*知识点4:ts格式文件中3600间隔是什么意思?
    它是25fps帧率的ts媒体文件,每个视频帧的间隔时间。
    ts文件的封装时基是90kHz为单位,timebase是AVRational{1,90000},简单的理解就是把1秒分成了90000等分,拿25帧率ts文件来分析
    按标准时间来计算每帧的间隔:
    公式为:1 / 25 = 0.04(秒) = 40毫秒
    按ffmpeg中的1秒(即90000)来计算每帧的间隔(单位好像没有明确的定义,暂且使用ffmpeg吧):
    90000 / 25 = 3600(ffmpeg)
    用时间转换公式可能会更清楚一些:
    1(s) = 90000(ffmpeg)
    40(ms) = 3600(ffmpeg)

*知识点5:不同的时间基
    现实中不同的封装格式,timebase是不一样的。另外,整个转码过程,不同的数据状态对应的时间基也不一致。还是拿mpegts封装格式25fps来
    说(只说视频,音频大致一样,但也略有不同)。非压缩时候的数据(即YUV或者其它),在ffmpeg中对应的结构体为AVFrame,它的时间基为AVRational{1,25}。
    压缩后的数据(对应的结构体为AVPacket)对应的时间基为AVRational{1,90000}
*/

#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include <libavfilter/avfiltergraph.h>
#include <libavfilter/buffersink.h>
#include <libavfilter/buffersrc.h>
#include <libavutil/time.h>
#include <stdlib.h>
#include <unistd.h>

int main(int argc, char **argv)
{
    
    
    int i = 0;
    av_register_all();
    int64_t start = av_gettime_relative();
    AVRational tb = (AVRational){
    
    1,90000};
    for(i=1; i < 100; i++)
    {
    
    
    #if 1
        usleep(1000*40);//等待40毫秒
        int64_t time = av_gettime_relative();//单位:AV_TIME_BASE,即ffmpeg内部使用的时间单位 
        int64_t timestamp = time - start;
    #else
        int64_t timestamp = i*0.04 * AV_TIME_BASE;//把实际的时间单位转换成AV_TIME_BASE
    #endif

        //时间基转换
        int64_t pts = av_rescale_q(timestamp, AV_TIME_BASE_Q, tb);
        printf("timestamp:%"PRId64" pts:%"PRId64"\n", timestamp, pts);
    }
    return 0;
}

猜你喜欢

转载自blog.csdn.net/qq_34623621/article/details/125069417