FFmpeg时间戳详解

简介: I 帧:I 帧(Intra-coded picture, 帧内编码帧,常称为关键帧)包含一幅完整的图像信息,属于帧内编码图像,不含运动矢量,在解码时不需要参考其他帧图像。因此在 I 帧图像处可以切换频道,而不会导致图像丢失或无法解码。I 帧图像用于阻止误差的累积和扩散。在闭合式 GOP 中,每个 GOP 的第一个帧一定是 I 帧,且当前 GOP 的数据不会参考前后 GOP 的数据。

作者:叶余

来源:https://www.cnblogs.com/leisure_chn/p/10584910.html


1. I 帧/P 帧/B 帧

I 帧:I 帧(Intra-coded picture, 帧内编码帧,常称为关键帧)包含一幅完整的图像信息,属于帧内编码图像,不含运动矢量,在解码时不需要参考其他帧图像。因此在 I 帧图像处可以切换频道,而不会导致图像丢失或无法解码。I 帧图像用于阻止误差的累积和扩散。在闭合式 GOP 中,每个 GOP 的第一个帧一定是 I 帧,且当前 GOP 的数据不会参考前后 GOP 的数据。

P 帧:P 帧(Predictive-coded picture, 预测编码图像帧)是帧间编码帧,利用之前的 I 帧或 P 帧进行预测编码。

B 帧:B 帧(Bi-directionally predicted picture, 双向预测编码图像帧)是帧间编码帧,利用之前和(或)之后的 I 帧或 P 帧进行双向预测编码。B 帧不可以作为参考帧。

B 帧具有更高的压缩率,但需要更多的缓冲时间以及更高的 CPU 占用率,因此 B 帧适合本地存储以及视频点播,而不适用对实时性要求较高的直播系统。

2. DTS 和 PTS

DTS(Decoding Time Stamp, 解码时间戳),表示压缩帧的解码时间。

PTS(Presentation Time Stamp, 显示时间戳),表示将压缩帧解码后得到的原始帧的显示时间。

音频中 DTS 和 PTS 是相同的。视频中由于 B 帧需要双向预测,B 帧依赖于其前和其后的帧,因此含 B 帧的视频解码顺序与显示顺序不同,即 DTS 与 PTS 不同。当然,不含 B 帧的视频,其 DTS 和 PTS 是相同的。下图以一个开放式 GOP 示意图为例,说明视频流的解码顺序和显示顺序

image.png

采集顺序指图像传感器采集原始信号得到图像帧的顺序。

编码顺序指编码器编码后图像帧的顺序。存储到磁盘的本地视频文件中图像帧的顺序与编码顺序相同。

传输顺序指编码后的流在网络中传输过程中图像帧的顺序。

解码顺序指解码器解码图像帧的顺序。

显示顺序指图像帧在显示器上显示的顺序。

采集顺序与显示顺序相同。编码顺序、传输顺序和解码顺序相同。

以图中“B[1]”帧为例进行说明,“B[1]”帧解码时需要参考“I[0]”帧和“P[3]”帧,因此“P[3]”帧必须比“B[1]”帧先解码。这就导致了解码顺序和显示顺序的不一致,后显示的帧需要先解码。

3. FFmpeg 中的时间基与时间戳

3.1 时间基与时间戳的概念

在 FFmpeg 中,时间基(time_base)是时间戳(timestamp)的单位,时间戳值乘以时间基,可以得到实际的时刻值(以秒等为单位)。例如,如果一个视频帧的 dts 是 40,pts 是 160,其 time_base 是 1/1000 秒,那么可以计算出此视频帧的解码时刻是 40 毫秒(40/1000),显示时刻是 160 毫秒(160/1000)。FFmpeg 中时间戳(pts/dts)的类型是 int64_t 类型,把一个 time_base 看作一个时钟脉冲,则可把 dts/pts 看作时钟脉冲的计数。

3.2 三种时间基 tbr、tbn 和 tbc

不同的封装格式具有不同的时间基。在 FFmpeg 处理音视频过程中的不同阶段,也会采用不同的时间基。

FFmepg 中有三种时间基,命令行中 tbr、tbn 和 tbc 的打印值就是这三种时间基的倒数:

tbn:对应容器中的时间基。值是 AVStream.time_base 的倒数

tbc:对应编解码器中的时间基。值是 AVCodecContext.time_base 的倒数

tbr:从视频流中猜算得到,可能是帧率或场率(帧率的 2 倍)

测试文件下载(右键另存为):tnmil3.flv

使用 ffprobe 探测媒体文件格式,如下:

think@opensuse> ffprobe tnmil3.flv 
ffprobe version 4.1 Copyright (c) 2007-2018 the FFmpeg developers
Input #0, flv, from 'tnmil3.flv':
  Metadata:
    encoder         : Lavf58.20.100
  Duration: 00:00:03.60, start: 0.017000, bitrate: 513 kb/s
    Stream #0:0: Video: h264 (High), yuv420p(progressive), 784x480, 25 fps, 25 tbr, 1k tbn, 50 tbc
    Stream #0:1: Audio: aac (LC), 44100 Hz, stereo, fltp, 128 kb/s

关于 tbr、tbn 和 tbc 的说明,原文如下,来自 FFmpeg 邮件列表:

There are three different time bases for time stamps in FFmpeg. The

values printed are actually reciprocals of these, i.e. 1/tbr, 1/tbn and

1/tbc.

tbn is the time base in AVStream that has come from the container, I

think. It is used for all AVStream time stamps.

tbc is the time base in AVCodecContext for the codec used for a

particular stream. It is used for all AVCodecContext and related time

stamps.

tbr is guessed from the video stream and is the value users want to see

when they look for the video frame rate, except sometimes it is twice

what one would expect because of field rate versus frame rate.

3.3 内部时间基 AV_TIME_BASE

除以上三种时间基外,FFmpeg 还有一个内部时间基 AV_TIME_BASE(以及分数形式的 AV_TIME_BASE_Q)

// 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}

AV_TIME_BASE 及 AV_TIME_BASE_Q 用于 FFmpeg 内部函数处理,使用此时间基计算得到时间值表示的是微秒。

3.4 时间值形式转换

av_q2d()将时间从 AVRational 形式转换为 double 形式。AVRational 是分数类型,double 是双精度浮点数类型,转换的结果单位是秒。转换前后的值基于同一时间基,仅仅是数值的表现形式不同而已。

av_q2d()实现如下:

/**
 * 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);

3.5 时间基转换函数

av_rescale_q()用于不同时间基的转换,用于将时间值从一种时间基转换为另一种时间基。

/**
 * Rescale a 64-bit integer by 2 rational numbers.
 *
 * The operation is mathematically equivalent to `a × bq / cq`.
 *
 * This function is equivalent to av_rescale_q_rnd() with #AV_ROUND_NEAR_INF.
 *
 * @see av_rescale(), av_rescale_rnd(), av_rescale_q_rnd()
 */
int64_t av_rescale_q(int64_t a, AVRational bq, AVRational cq) av_const;

av_packet_rescale_ts()用于将 AVPacket 中各种时间值从一种时间基转换为另一种时间基。

/**
 * Convert valid timing fields (timestamps / durations) in a packet from one
 * timebase to another. Timestamps with unknown values (AV_NOPTS_VALUE) will be
 * ignored.
 *
 * @param pkt packet on which the conversion will be performed
 * @param tb_src source timebase, in which the timing fields in pkt are
 *               expressed
 * @param tb_dst destination timebase, to which the timing fields will be
 *               converted
 */
void av_packet_rescale_ts(AVPacket *pkt, AVRational tb_src, AVRational tb_dst);

3.6 转封装过程中的时间基转换

容器中的时间基(AVStream.time_base,3.2 节中的 tbn)定义如下:

typedef struct AVStream {
    ......
    /**
     * This is the fundamental unit of time (in seconds) in terms
     * of which frame timestamps are represented.
     *
     * decoding: set by libavformat
     * encoding: May be set by the caller before avformat_write_header() to
     *           provide a hint to the muxer about the desired timebase. In
     *           avformat_write_header(), the muxer will overwrite this field
     *           with the timebase that will actually be used for the timestamps
     *           written into the file (which may or may not be related to the
     *           user-provided one, depending on the format).
     */
    AVRational time_base;
    ......
}

AVStream.time_base 是 AVPacket 中 pts 和 dts 的时间单位,输入流与输出流中 time_base 按如下方式确定:

对于输入流:打开输入文件后,调用 avformat_find_stream_info()可获取到每个流中的 time_base

对于输出流:打开输出文件后,调用 avformat_write_header()可根据输出文件封装格式确定每个流的 time_base 并写入输出文件中

不同封装格式具有不同的时间基,在转封装(将一种封装格式转换为另一种封装格式)过程中,时间基转换相关代码如下:

av_read_frame(ifmt_ctx, &pkt);
pkt.pts = av_rescale_q_rnd(pkt.pts, in_stream->time_base, out_stream->time_base, AV_ROUND_NEAR_INF|AV_ROUND_PASS_MINMAX);
pkt.dts = av_rescale_q_rnd(pkt.dts, in_stream->time_base, out_stream->time_base, AV_ROUND_NEAR_INF|AV_ROUND_PASS_MINMAX);
pkt.duration = av_rescale_q(pkt.duration, in_stream->time_base, out_stream->time_base);

下面的代码具有和上面代码相同的效果:

// 从输入文件中读取 packet
av_read_frame(ifmt_ctx, &pkt);
// 将 packet 中的各时间值从输入流封装格式时间基转换到输出流封装格式时间基
av_packet_rescale_ts(&pkt, in_stream->time_base, out_stream->time_base);

这里流里的时间基in_stream->time_baseout_stream->time_base,是容器中的时间基,就是 3.2 节中的 tbn。

例如,flv 封装格式的 time_base 为{1,1000},ts 封装格式的 time_base 为{1,90000}

我们编写程序将 flv 封装格式转换为 ts 封装格式,抓取原文件(flv)的前四帧显示时间戳:

think@opensuse> ffprobe -show_frames -select_streams v tnmil3.flv | grep pkt_pts  
ffprobe version 4.1 Copyright (c) 2007-2018 the FFmpeg developers
Input #0, flv, from 'tnmil3.flv':
  Metadata:
    encoder         : Lavf58.20.100
  Duration: 00:00:03.60, start: 0.017000, bitrate: 513 kb/s
    Stream #0:0: Video: h264 (High), yuv420p(progressive), 784x480, 25 fps, 25 tbr, 1k tbn, 50 tbc
    Stream #0:1: Audio: aac (LC), 44100 Hz, stereo, fltp, 128 kb/s
pkt_pts=80
pkt_pts_time=0.080000
pkt_pts=120
pkt_pts_time=0.120000
pkt_pts=160
pkt_pts_time=0.160000
pkt_pts=200
pkt_pts_time=0.200000

再抓取转换的文件(ts)的前四帧显示时间戳:

think@opensuse> ffprobe -show_frames -select_streams v tnmil3.ts | grep pkt_pts  
ffprobe version 4.1 Copyright (c) 2007-2018 the FFmpeg developers
Input #0, mpegts, from 'tnmil3.ts':
  Duration: 00:00:03.58, start: 0.017000, bitrate: 619 kb/s
  Program 1 
    Metadata:
      service_name    : Service01
      service_provider: FFmpeg
    Stream #0:0[0x100]: Video: h264 (High) ([27][0][0][0] / 0x001B), yuv420p(progressive), 784x480, 25 fps, 25 tbr, 90k tbn, 50 tbc
    Stream #0:1[0x101]: Audio: aac (LC) ([15][0][0][0] / 0x000F), 44100 Hz, stereo, fltp, 127 kb/s
pkt_pts=7200
pkt_pts_time=0.080000
pkt_pts=10800
pkt_pts_time=0.120000
pkt_pts=14400
pkt_pts_time=0.160000
pkt_pts=18000
pkt_pts_time=0.200000

可以发现,对于同一个视频帧,它们时间基(tbn)不同因此时间戳(pkt_pts)也不同,但是计算出来的时刻值(pkt_pts_time)是相同的。

看第一帧的时间戳,计算关系:80×{1,1000} == 7200×{1,90000} == 0.080000

3.7 转码过程中的时间基转换

编解码器中的时间基(AVCodecContext.time_base,3.2 节中的 tbc)定义如下:

typedef struct AVCodecContext {
    ......
    /**
     * This is the fundamental unit of time (in seconds) in terms
     * of which frame timestamps are represented. For fixed-fps content,
     * timebase should be 1/framerate and timestamp increments should be
     * identically 1.
     * This often, but not always is the inverse of the frame rate or field rate
     * for video. 1/time_base is not the average frame rate if the frame rate is not
     * constant.
     *
     * Like containers, elementary streams also can store timestamps, 1/time_base
     * is the unit in which these timestamps are specified.
     * As example of such codec time base see ISO/IEC 14496-2:2001(E)
     * vop_time_increment_resolution and fixed_vop_rate
     * (fixed_vop_rate == 0 implies that it is different from the framerate)
     *
     * - encoding: MUST be set by user.
     * - decoding: the use of this field for decoding is deprecated.
     *             Use framerate instead.
     */
    AVRational time_base;
    ......
}

上述注释指出,AVCodecContext.time_base 是帧率(视频帧)的倒数,每帧时间戳递增 1,那么 tbc 就等于帧率。编码过程中,应由用户设置好此参数。解码过程中,此参数已过时,建议直接使用帧率倒数用作时间基。

这里有一个问题:按照此处注释说明,帧率为 25 的视频流,tbc 理应为 25,但实际值却为 50,不知作何解释?是否 tbc 已经过时,不具参考意义?

根据注释中的建议,实际使用时,在视频解码过程中,我们不使用 AVCodecContext.time_base,而用帧率倒数作时间基,在视频编码过程中,我们将 AVCodecContext.time_base 设置为帧率的倒数。

3.7.1 视频流

视频按帧播放,所以解码后的原始视频帧时间基为 1/framerate。

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

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);

3.7.2 音频流

音频按采样点播放,所以解码后的原始音频帧时间基为 1/sample_rate

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

AVFormatContext *ifmt_ctx;
AVStream *in_stream;
AVCodecContext *dec_ctx;
AVPacket packet;
AVFrame *frame;
// 从输入文件中读取编码帧
av_read_frame(ifmt_ctx, &packet);
// 时间基转换
int raw_audio_time_base = av_inv_q(dec_ctx->sample_rate);
av_packet_rescale_ts(packet, in_stream->time_base, raw_audio_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->sample_rate);
av_packet_rescale_ts(&opacket, enc_ctx->time_base, out_stream->time_base);
// 将编码帧写入输出媒体文件
av_interleaved_write_frame(o_fmt_ctx, &packet);

4. 参考资料

[1]. What does the output of ffmpeg mean? tbr tbn tbc etc?

[2]. 视频编解码基础概念, https://www.cnblogs.com/leisure_chn/p/10285829.html

[3]. 对 ffmpeg 的时间戳的理解笔记, https://blog.csdn.net/topsluo/article/details/76239136

[4]. ffmpeg 中的时间戳与时间基, http://www.imooc.com/article/91381

[5]. ffmpeg 编解码中涉及到的 pts 详解, http://www.52ffmpeg.com/article/353.html

[6]. 音视频录入的 pts 和 dts 问题, https://blog.csdn.net/zhouyongku/article/details/38510747

5. 修改记录

2019-03-16 V1.0 初稿

2019-03-23 V1.1 增加 3.7 节


「视频云技术」你最值得关注的音视频技术公众号,每周推送来自阿里云一线的实践技术文章,在这里与音视频领域一流工程师交流切磋。

image.png

相关文章
|
6月前
|
存储 编解码 调度
剖析ffmpeg视频解码播放:时间戳的处理
剖析ffmpeg视频解码播放:时间戳的处理
587 0
|
24天前
|
Linux 开发工具 Android开发
FFmpeg开发笔记(六十)使用国产的ijkplayer播放器观看网络视频
ijkplayer是由Bilibili基于FFmpeg3.4研发并开源的播放器,适用于Android和iOS,支持本地视频及网络流媒体播放。本文详细介绍如何在新版Android Studio中导入并使用ijkplayer库,包括Gradle版本及配置更新、导入编译好的so文件以及添加直播链接播放代码等步骤,帮助开发者顺利进行App调试与开发。更多FFmpeg开发知识可参考《FFmpeg开发实战:从零基础到短视频上线》。
93 2
FFmpeg开发笔记(六十)使用国产的ijkplayer播放器观看网络视频
|
1月前
|
编解码 语音技术 内存技术
FFmpeg开发笔记(五十八)把32位采样的MP3转换为16位的PCM音频
《FFmpeg开发实战:从零基础到短视频上线》一书中的“5.1.2 把音频流保存为PCM文件”章节介绍了将媒体文件中的音频流转换为原始PCM音频的方法。示例代码直接保存解码后的PCM数据,保留了原始音频的采样频率、声道数量和采样位数。但在实际应用中,有时需要特定规格的PCM音频。例如,某些语音识别引擎仅接受16位PCM数据,而标准MP3音频通常采用32位采样,因此需将32位MP3音频转换为16位PCM音频。
48 0
FFmpeg开发笔记(五十八)把32位采样的MP3转换为16位的PCM音频
|
1月前
|
XML 开发工具 Android开发
FFmpeg开发笔记(五十六)使用Media3的Exoplayer播放网络视频
ExoPlayer最初是为了解决Android早期MediaPlayer控件对网络视频兼容性差的问题而推出的。现在,Android官方已将其升级并纳入Jetpack的Media3库,使其成为音视频操作的统一引擎。新版ExoPlayer支持多种协议,解决了设备和系统碎片化问题,可在整个Android生态中一致运行。通过修改`build.gradle`文件、布局文件及Activity代码,并添加必要的权限,即可集成并使用ExoPlayer进行网络视频播放。具体步骤包括引入依赖库、配置播放界面、编写播放逻辑以及添加互联网访问权限。
126 1
FFmpeg开发笔记(五十六)使用Media3的Exoplayer播放网络视频
|
1月前
|
Web App开发 安全 程序员
FFmpeg开发笔记(五十五)寒冬里的安卓程序员可进阶修炼的几种姿势
多年的互联网寒冬在今年尤为凛冽,坚守安卓开发愈发不易。面对是否转行或学习新技术的迷茫,安卓程序员可从三个方向进阶:1)钻研谷歌新技术,如Kotlin、Flutter、Jetpack等;2)拓展新功能应用,掌握Socket、OpenGL、WebRTC等专业领域技能;3)结合其他行业,如汽车、游戏、安全等,拓宽职业道路。这三个方向各有学习难度和保饭碗指数,助你在安卓开发领域持续成长。
69 1
FFmpeg开发笔记(五十五)寒冬里的安卓程序员可进阶修炼的几种姿势
|
2月前
|
XML Java Android开发
FFmpeg开发笔记(五十二)移动端的国产视频播放器GSYVideoPlayer
GSYVideoPlayer是一款国产移动端视频播放器,支持弹幕、滤镜、广告等功能,采用IJKPlayer、Media3(EXOPlayer)、MediaPlayer及AliPlayer多种内核。截至2024年8月,其GitHub星标数达2万。集成时需使用新版Android Studio,并按特定步骤配置依赖与权限。提供了NormalGSYVideoPlayer、GSYADVideoPlayer及ListGSYVideoPlayer三种控件,支持HLS、RTMP等多种直播链接。
93 18
FFmpeg开发笔记(五十二)移动端的国产视频播放器GSYVideoPlayer
|
27天前
|
Linux API 开发工具
FFmpeg开发笔记(五十九)Linux编译ijkplayer的Android平台so库
ijkplayer是由B站研发的移动端播放器,基于FFmpeg 3.4,支持Android和iOS。其源码托管于GitHub,截至2024年9月15日,获得了3.24万星标和0.81万分支,尽管已停止更新6年。本文档介绍了如何在Linux环境下编译ijkplayer的so库,以便在较新的开发环境中使用。首先需安装编译工具并调整/tmp分区大小,接着下载并安装Android SDK和NDK,最后下载ijkplayer源码并编译。详细步骤包括环境准备、工具安装及库编译等。更多FFmpeg开发知识可参考相关书籍。
75 0
FFmpeg开发笔记(五十九)Linux编译ijkplayer的Android平台so库
|
2月前
|
Linux 开发工具 Android开发
FFmpeg开发笔记(五十三)移动端的国产直播录制工具EasyPusher
EasyPusher是一款国产RTSP直播录制推流客户端工具,支持Windows、Linux、Android及iOS等系统。尽管其GitHub仓库(安卓版:https://github.com/EasyDarwin/EasyPusher-Android)已多年未更新,但通过一系列改造,如升级SDK版本、迁移到AndroidX、指定本地NDK版本及更新Gradle版本等,仍可在最新Android Studio上运行。以下是针对Android Studio Dolphin版本的具体改造步骤。
59 3
FFmpeg开发笔记(五十三)移动端的国产直播录制工具EasyPusher
|
1月前
|
Android开发 开发者
FFmpeg开发笔记(五十七)使用Media3的Transformer加工视频文件
谷歌推出的Transformer,作为Jetpack Media3架构的一部分,助力开发者实现音视频格式转换与编辑。Media3简化了媒体处理流程,提升了定制性和可靠性。Transformer可用于剪辑、添加滤镜等操作,其示例代码可在指定GitHub仓库中找到。要使用Transformer,需在`build.gradle`中添加相关依赖,并按文档编写处理逻辑,最终完成音视频转换任务。具体步骤包括配置剪辑参数、设置空间效果以及监听转换事件等。
49 0
FFmpeg开发笔记(五十七)使用Media3的Transformer加工视频文件
|
1月前
|
Linux 视频直播
FFmpeg开发笔记(五十四)使用EasyPusher实现移动端的RTSP直播
本文介绍了如何使用EasyPusher-Android实现RTSP直播流程。首先对比了RTSP、RTMP、SRT和RIST四种流媒体协议,并以RTSP为例,详细说明了使用EasyPusher-Android向流媒体服务器进行RTSP直播推流的方法。文中还提供了OBS Studio配置RTSP插件及ZLMediaKit云服务器部署的相关信息,通过修改EasyPusher-Android源码使其支持通用RTSP地址,最终验证了直播功能的成功实现。
50 0
FFmpeg开发笔记(五十四)使用EasyPusher实现移动端的RTSP直播

热门文章

最新文章