本文主要介绍在用FFmpeg进行视频相关开发时涉及到的一些视频基本概念。
一、视频帧
在H264协议里,图像以组(GOP,也就是一个序列)为单位进行组织,一个组是一段图像编码后的数据流,以I帧开始,到下一个I帧结束。一个组就是一段内容差异不太大的图像编码后生成的一串数据流。当运动变化比较少时,一个组可以很长,因为运动变化少就代表图像画面的内容变动很小,所以就可以编一个I帧,然后一直P帧、B帧了。当运动变化多时,可能一个序列就比较短了,比如就包含一个I帧和3、4个P帧。
1.I帧
I帧是帧组的第一帧,在一组中只有一个I帧。I帧是帧内编码帧,是一种自带全部信息的独立帧,无需参考其他图像便可独立进行解码,可以简单理解为一张静态画面。如果传输过程中I真丢失,画面最直接的影响就是会卡顿,因为后面的帧都无法正确解码,只能等待下一个GOP。
2.P帧
P帧是帧间预测编码帧,需要参考其前面的I帧或P帧才能进行编码。P帧没有完整的画面数据,只有与其前一参考帧的画面差别的数据。与I帧相比,P帧通常占用更少的数据位,但不足是,由于P帧对前面的P和I参考帧有着复杂的依耐性,因此对传输错误非常敏感,所以如果P帧丢失,画面会出现马赛克现象,因为前向参考帧错误,补齐的并不是真正运动变化后的数据。
3.B帧
B帧是双向预测编码帧,也就是B帧记录的是本帧与前后帧的差别。也就是说要解码B帧,不仅要取得之前的缓存画面,还要解码之后的画面,通过前后画面的与本帧数据的叠加取得最终的画面。B帧压缩率高,但解码时很耗CPU资源。(B帧以前面的I或P帧和后面的P帧为参考帧,B帧不是参考帧,不会造成解码错误的扩散。)
一般来说,I帧的压缩率是7(跟JPG差不多),P帧是20,B帧可以达到50。虽然B帧压缩率高,但是在直播系统中很少使用B帧,一是因为解码很耗CPU,再就是B帧解码需要等待下一个P帧数据,这就会造成解码延时,而直播系统对延时要求很高,所以一般不用B帧。但对于点播系统就不会有这个问题。
二、PTS和DTS
PTS(Presentation TimeStamp)是渲染用的时间戳,也就是说,我们的视频帧是按照 PTS 的时间戳来展示的。DTS(Decoding TimeStamp)解码时间戳,是用于视频解码的。
为什么会有2个不同的时间戳呢?这和I帧、P帧和B帧有关,我们分2中情况来看:
1.视频中只有I帧和P帧
如下所示,第1帧是I帧,2-8帧是P帧,展示的顺序是12345678。解码时先解码第1帧,第2帧参考第1帧解码,第3帧参考第2帧解码……也就是解码顺序也是12345678。这种情况下PTS和DTS是一样的,所以此时是没有必要区分2个不同的时间戳的。
1 2 3 4 5 6 7 8
I P P P P P P P
2.视频中有I帧、P帧和B帧时
如下所示,第1帧是I帧,第4、8帧是P帧,其余是B帧,展示顺序是12345678。解码时先解第1帧,第2帧是B帧,由于它后面的帧还没解码,所以它也没法解码,第3帧也是一样,所以第二解码是第4号的P帧,然后2号B帧参考其前面的第1帧和其后面的第4帧进行解码,然后再是3号B帧解码,然后567号B帧要等到8号的P帧解码后才能解码。所以解码的顺序是14238567,这种情况下展示顺序和解码顺序就不一样,所以就有了PTS和DTS的区分。
1 2 3 4 5 6 7 8
I B B P B B B P
三、时间基
1.时间基的概念
FFmpeg中用AVPacket结构体来描述解码前或编码后的压缩包,用AVFrame结构体来描述解码后或编码前的信号帧。对于视频来说,AVFrame就是视频的一帧图像。这帧图像什么时候显示给用户,就取决于它的PTS。DTS是AVPacket里的一个成员,表示这个压缩包应该什么时候被解码。那么PTS和DTS的单位是什么呢?
要回答这个问题,我们先引入FFmpeg中时间基的概念,也就是time_base
,它是一个AVRational结构体,其定义如下:
typedef struct AVRational{
int num; ///< 分子
int den; ///< 分母
} AVRational;
time_base是用来度量时间的,比如time_base = {1,25},它的意思是将1秒分成25段,那么每段就是1/25秒,在FFmpeg中函数av_q2d(time_base)就是用来计算一段的时间的,计算结果就是1/25秒。PTS和DTS的单位就是段,比如一个视频中某一帧的pts是800,也就是说有800段,那么它表示多少秒呢,ptsav_q2d(time_base)=800(1/25)=32s,也就是说要在第32秒的时候播放这一帧。
2.时间基的转换
为什么要有时间基转换呢?首先,不同的封装格式时间基是不一样的。其次,在编码前和编码后的时间基也不一致。拿mpegts封装格式25fps来说,非压缩时候的数据(YUV),在ffmpeg中对应的结构体为AVFrame,它的时间基为AVCodecContext 的time_base ,AVRational{1,25}。 压缩后的数据(对应的结构体为AVPacket)对应的时间基为AVStream的time_base,AVRational{1,90000}。
因此数据状态不同,时间基不一样,所以我们必须转换,这就是pts的转换。FFmpeg中有个函数用于时间基的转换,比如time_base1 = {1,25}的时间基下pts1 = 100,那么在time_base2 = {1,90000}的时间基下pts2是多少呢?pts2 = av_rescale_q(pts1,time_base1,time_base2);
在FFmpeg内部还有一个比较常见的时间基,其定义如下:
#define AV_TIME_BASE 1000000
#define AV_TIME_BASE_Q (AVRational){1, AV_TIME_BASE}
另外还有个和pts类似的参数duration,它的单位和pts一样,表示两帧直接的间隔,也就是说一帧持续多少个时间刻度。
3.计算音频的pts
以AAC音频为例,一个AAC原始帧包含一段时间内1024个采样及相关数据,也就是说一帧有1024个样本,如果采样率为44.1kHz(1秒采集44100个样本),所以aac音频1秒有44100/1024帧,每一帧的持续时间是1024/44100秒,由此可以计算出每一帧的pts。(不同的封装格式一帧的样本数是不一样的。)
四、NALU、SPS和PPS
NALU(NALU详解)
我们平时的一帧数据就是一个NALU,它包含一个字节的头信息和帧的视频数据内容。
MALU头信息中第1个bit是forbidden_zero_bit,在 H.264 规范中规定了这一位必须为 0。
第2、3位是nal_ref_idc,指示了这个NALU的重要性,值越大表示越重要,比如为0时NALU解码器可以丢弃它而不影响图像的回放。如果是参考帧的话这个值必须大于0。
第4-8位是nal_unit_type,表这个NALU单元类型,2表示B帧或P帧,5表示I帧,7表示SPS帧,8表示PPS帧。
每个NALU前会添加一个4字节的起始码,每当解码器遇到起始码就表示当前帧结束下一帧开始。
SPS和PPS(SPS PPS详解)
SPS(Sequence Paramater Set),也就是序列参数集,里面保存了一组编码视频序列(也叫组)的全局参数(比如分辨率、码流等参数)。
PPS(Picture Paramater Set),也就是图像参数集,每一帧的编码后数据所依赖的参数保存于图像参数集中。
H264中SPS和PPS分别保存在2个NALU中,SPS的nal_unit_type值是7,PPS的nal_unit_type值是。在封装格式中,PPS通常与SPS一起,保存在视频文件的文件头中,后面的解码都需要用到SPS和PPS中的参数,SPS和PPS丢失会导致无法解码。如果在播放途中切换分辨率,SPS和PPS信息都会发生改变,所以每次切换时都需要把新的SPS和PPS传过来。对于需要从中间开始解码的情况,中间也需要传SPS和PPS过来,否则后面无法解码。
五、H264压缩简介
1.分组:把几帧图像分为一组(GOP,也就是一个序列),每组的帧数不是固定的,运动变化少时一组的帧数就较多,运动变化多时一组帧数就少。
2.定义帧:将每组内各帧图像定义为三种类型,即I帧、B帧和P帧;
3.预测帧:以I帧做为基础帧,以I帧预测P帧,再由I帧和P帧预测B帧;
4.数据传输:最后将I帧数据与预测的差值信息进行存储和传输。
网友评论