码流的计算
- 分辨率
- x轴的像素个数*y轴的像素个数
- 常见的宽高比:16:9 4:3
- 360P/720P/1K/2K:这些都是16:9的宽高比,其中360P为640*360;720P为1280*720;1K为1920*1080,即1080P;2K为2560*1440,即1440P。
- 帧率
- 每秒钟采集/播放图像的个数
- 常见的帧率:15帧/s,30帧/s,60帧/s
- 未编码视频的RGB码流
RGB码流=分辨率(宽*高)*3(通道)*帧率(25帧/s)。例如:1280*720*3*25=69120000,约69M
- YUV
YUV是一种图像格式,具体可以参考OpenCV 计算机视觉整理 中的 YUV。
YUV数据量为:
- YUV=Y * 1.5
- YUV=RGB / 2
H264编码原理
H264压缩比
条件:
- YUV格式为YUV420P
- 分辨率为640*480
- 帧率为15帧/s
它的码流为:640*480*1.5*15=6912000,单位为字节,换算比特为6912000*8=55296000,将近55M。
在网上传播的视频建议的码流为500kpbs,那么它的压缩比约为1/100.
这个500kpbs的参考值是一个经验值,来源于https://docs.agora.io/cn
GOP
上图是一个帧率为25帧/s的视频,帧与帧之间的间隔为40ms。整段视频只有1s,现在我们将这1s钟的视频变为10min。
这样就意味着帧数就非常多了,25*10*60=15000帧。对于这么多的帧,那么压缩起来就会比较困难。
这样,我们就会依据帧与帧之间的相关性进行分组。如上图中就是分成了两组,一组是人看望远镜的相关动作,它们可能看望远镜的角度不同;另一组是人使用计算机,只是敲键盘的动作不同。这样我们把相关的组称为GOP(group of picture)。GOP中帧与帧之间的差别小。
在同一个GOP中,以上图为例,我们可以看见,人的头发基本是相同的,可以放在一张图中;另外,不同的地方在于镜头、身体,对于这些差值再重新分组,通过这一个GOP之后,GOP的这一组帧进行压缩的时候,会压缩的非常小,只需要存很少的数据就可以将原来的一组帧还原回来。
I/P/B帧
- 编码帧的分类
- I帧(intraframe frame),关键帧,采用帧内压缩技术。GOP中的第一帧为I帧,且是一种特殊的I帧,称为IDR帧,IDR帧属于I帧,但I帧不一定是IDR帧。一组帧中有很多帧。如果超过了一定范围,对于H264来说,它会强制加入I帧,防止出现错误的时候,错误出现串联。I帧是不依赖于任何参考帧的,它属于帧内压缩技术,它自己编码,自己还原,跟其他帧没有任何关系。
- P帧(forward Predicted frame),向前参考帧。压缩时,只参考前面已经处理的帧(前面的帧解码后才能解码P帧,不能单独解码P帧),采用帧间压缩技术。它占I帧的一半大小。
- B帧(Bidirectionally predicted frame),双向参考帧。压缩时,即参考前面已经处理的帧,也参考后面的帧,帧间压缩技术。它占I帧\(1\over 4\)大小。
- 播放的时候是先播放I帧,B帧,P帧;但是解码的时候是先解码I帧,P帧,B帧。一般在实时通讯场景中,只有I帧和P帧,没有B帧。但是在视频转码的过程中会大量使用B帧,节省空间。
- IDR帧与I帧的区别与联系
- IDR(Instantaneous Decoder Refresh)解码器立即刷新。
- 每当遇到IDR帧时,解码器就会清空解码器参考buffer中的内容。在解码器端遇到IDR帧之后,会清空缓冲区,全部重新来过。
- 每个GOP中的第一帧就是IDR帧。
- IDR帧是一种特殊的I帧。
IDR帧起到防止错误传播的作用。
帧与分组的关系
上图的视频帧分成了两个GOP,GOP的第一帧一定是I帧也是IDR帧,对于H264来说,I帧后面会跟3个B帧,然后接1个P帧,以此往复。在解码的过程中,第一个肯定是解码I帧,对于第2、3、4的B帧来说,它们都依赖于I帧和后面的P帧,所以先解码的一定是P帧,然后才能解码中间的3个B帧。而B帧与B帧之间是没有任何参考的。前面的IBBBP帧解码完成之后,第二组的3个B帧则依赖于前面的P帧和后面的P帧,而后面的P帧则是依赖于前面的P帧,以此往复。
这里需要说明的是,无论是I、B、P一旦解码完成后就都是完整的图像了,所以播放的时候一定是按照顺序播放的,不再有I、B、P之分。
SPS与PPS
除了I、B、P之外,还有两个特殊的帧SPS和PPS,它们是参数数据,属于I帧的一部分,在每个IDR帧之前都会有SPS和PPS这两个帧,它们是同时出现的,不会单独出现。
- SPS(Sequence Parameter Set)
序列参数集,对GOP的参数设置。作用于一串连续的视频图像。如seq_parameter_set_id、帧数及POC(picture order count)的约束、参考帧数目、解码图像尺寸和帧场编码模式选择标识等。
- PPS(Picture Parameter Set)
图像数据集,对于GOP这一组中每一幅图像的参数约束。作用于视频序列中的图像。如pic_parameter_set_id、熵编码模式选择标识、片组数目、初始量化参数和去方块滤波系数调整标识等。
H264压缩技术
有损无损压缩
- 帧内压缩,解决的是空域数据冗余问题。解决一张图片内的数据压缩问题。
- 帧间压缩,解决的是时域数据冗余问题。随着时间的推移,每个时间段都会有一帧数据。每帧数据之间可以做参考。
- 整数离散余弦变换(DCT),将空间上的相关性变为频域上无关的数据然后进行量化。
- CABAC压缩,根据上下文进行数据压缩。
对于DCT和CABAC都属于无损压缩技术,帧内压缩和帧间压缩都属于有损压缩技术。
宏块
- 宏块是视频压缩操作的基本单元。
- 无论是帧内压缩还是帧间压缩,它们都以宏块为单位。
这是一张原始图像。
我们在这个原始图像的左上角先切一个宏块。这个宏块是一个8*8的区域块。
这个8*8的宏块中的每一个像素都有一个具体的表现。每个像素都有一定的值,一定的颜色。最后我们会按照这些像素值进行压缩。
当我们将整张原始图像划分完宏块之后,就变成了上面那张图的样子。看上去整体和原始图像差不多,但是它的色彩、平滑度都不如原始图像。
- 子块划分
对于宏块来说还可以划分成很多的子块。在上图的左边的图是H264的一个16*16的宏块,再进行一个平均分配,变成4个8*8的子快。在每一个8*8的子快中又可以划分成4*8、8*4和4*4的子快。宏块的大与小对于编码有着非常重要的关系,如果宏块越小,压缩的时候控制力就更强;宏块越大,控制力就越弱。但是对于色彩单一的背景图来说,如之前原始图像的蓝天背景,宏块划的大,处理速度就快。对于细节丰富,纹理特别多的图,就需要划分成很小的宏块,那么处理起来的压缩比会更高。
上图的右边的图,对于MPEG2格式来说,它就是将16*16的宏块直接划分成4个8*8的子块,处理之后每一个子块的数据量都非常多。而H264则对宏块的划分做了非常大的灵活性,将宏块划分小了之后,我们可以看到,原来的数据量非常多,经过重新划分之后,再去压缩的时候每一块都非常小。整个的背景几乎都不需要存什么数据,只要存一些特定的纹理数据就可以了。
- 宏块的尺寸
对于H264最常见的就是16*16,经过不断的切割,可以划分成8*16、16*8、8*8、4*4、4*8、8*4。
帧内压缩技术
- 帧内压缩的理论:
- 相临像素差别不大,所以可以进行宏块预测。
- 人们对亮度的敏感度超过色度。
- YUV很容易将亮度与色度分开。
- 帧内预测
H264对帧内预测提供了9种模式,见上图的左图。这9种模式使用的时候会做一个预判,从某一个宏块为基础,对其周围的宏块进行推算的时候就是这9种模式进行推算。至于选择哪一种模式,在H264中有一种算法可以快速的定位使用哪一种模式。哪一种模式最接近于原来的宏块,则选择哪种模式,对于每一个宏块的预测都不同。通过这种方式就可以把所有的宏块进行一个处理,在上图的右图中,处理之后宏块都变成了数字,它代表着该宏块使用的是第几种模式进行预测的。
上图就是这9种模式,每一种模式中带颜色的部分(红色、橙色)都是已经预测出来的宏块,白色的部分是待预测部分。第一种模式,就是使用橙色部分的A、B、C、D的值来进行直接的纵向填充;第二种模式就是使用红色的I、J、K、L的值进行直接的横向填充;第三种模式是求平均值,就是将A、B、C、D和I、J、K、L求一个平均值填充进去;第四、五、六、七、八、九种模式都是斜向填充,具体的填充方式见上图即可。
- 不同模式的预测结果
在上图中的第一幅图像使用的就是第一种模式,直接使用A、B、C、D的像素值对下面的部分进行填充;第二幅图像使用的是第二种模式,直接使用I、J、K、L的像素值对右边的部分进行填充;第三幅图像使用的是第三种模式,将A、B、C、D和I、J、K、L的像素值求一个平均值填充进所有的部分,每一个填充的部分的像素值都是相同的。
- 帧内预测举例
在上图中,我们需要对红色方块内的部分进行一个预测
上图中左边是原始数据,右边是按照4*4的亮度块进行预测。这里我们需要知道,亮度块与色度块是单独进行预测的。在右边的红色方块部分,它是通过上面的数据和左边的数据进行预测的,预测的结果跟左边的图的红色方块的数据几乎是一样的,有一些细微的差别,但是差别不是太大。通过这种预测,我们就可以将数据量大大的减少。
对于这些细微的差别,我们还需要去进行处理。在上图中,右边是原始图,左边是预测图,虽然整体上差不多,但是预测图的清晰度会差很多。这是因为在预测的时候有一些块是模糊的,分不清楚的。
- 帧内预测残差值
当我们对整个图进行了预测之后,还需要将结果与原始图进行一个差值计算,计算出来的结果就是上图中的灰色图。
- 预测模式信息与残差值压缩
拿到残差值之后进行压缩的时候,直接就进行两个数据的压缩。第一个是预测模式信息的压缩,就是上图中深灰色的部分;另外一个就是残差值的压缩,就是上图中灰色的部分;这两个值加在一起就是我们压缩后的数据。当传到用户端解码的时候,就可以根据这个模式,先把原来的图像预测出来,预测出来之后再加上残差值,就可以完全还原成原来的图像数据。
上图中其实也表现了空间信息,最左边的是原始图像的容量,压缩后变成深灰色的压缩数据和灰色的残差值数据,我们可以看到它的容量小的可怜。
帧间压缩技术
- 帧间压缩原理
- GOP,所谓的帧间压缩一定是在一个GOP之内的,相邻的帧之间进行帧间压缩。它是不可能进行跨GOP进行帧间压缩的。
- 参考帧,后面的帧参考前面的帧进行帧间压缩。
- 运动估计(宏块匹配+运动矢量),通过宏块匹配的方式找到运动矢量,矢量是指一个宏块从一个坐标到另外一个坐标,是有方向的。运动矢量是指它有一个轨迹,从一个地方到另外一个地方。
- 运动补偿(解码),找到残差值,在解码的时候把残差值给补上去。
参考帧,我们还是以这幅图来说明。后面的帧参考前面的帧来进行编码。由于第一张图是IDR帧,它肯定是采用帧内压缩的。这里需要说明的是,第一张的背景跟后面图的背景其实是一样的,只是因为它是I帧,所以做了一个特殊标记。后面的帧相比于第一帧,大部分都是一样的,最主要的区别在于望远镜这里,我们在进行帧间压缩的时候实际只要存储这个望远镜的移动轨迹就可以了。比如说我们将望远镜这里划分成很多小宏块,对于第二帧来说,实际就是将第一帧望远镜划分出来的小宏块移动到了中间的位置,所以它的运动矢量就是从左到右。解码的时候就可以根据第一帧的基本图像再根据运动矢量就可以很快的恢复出第二帧的图像。所以它存储的数据一定是很小的。
- 宏块查找
在一个GOP中有很多相似的帧,我们抽出相邻的两个帧,如上图所示,假设抽出的是第一帧(右)和第二帧(左)。在第一帧中有一个黄色的宏块,位于右上的位置(位于第二宏块行)。当我们在第二帧中去匹配第一帧中的该黄色宏块的时候,可以使用逐行扫描的方法,去与第二帧中的每一个宏块进行匹配,找相似度最高的(比方说达到95%),我们就认为它是同一个宏块。当在第二帧中扫描到第三行的时候找到了该宏块。找到之后就在第二帧中将其坐标记录下来。这样就可以计算出第一帧中的宏块到第二帧中的方向矢量。
- 宏块查找算法
- 三步搜索
- 二维对数搜索
- 四步搜索
- 钻石搜索
- 运动估计
对于上面宏块的查找的整个过程就是运动估计。在上图的右边的图中的红色箭头就是该小黄色宏块的运动轨迹,也就是运动矢量,而我们压缩的时候存储的就是该运动矢量。到解码的时候就可以根据运动矢量数据还原回原来的GOP。
- 运动矢量与补偿压缩
我们除了要获取的运动矢量之外还有一个残差值。我们知道对于这个小黄宏块的查找,它是有一定的变化的,不是百分之百不变的。每一帧图像宏块变化的这部分就是残差值。在解码的时候必须要有运动矢量和宏块残差,先使用运动矢量把相应的宏块大致放入GOP中,再根据每一帧的残差值对其进行补全,这样才能更完整的还原回原来的数据。
- 帧间压缩的帧类型
- P帧
- B帧
- 视频花屏原因
如果GOP分组中有帧(B、P帧)丢失,会造成解码端的图像发生错误,这会出现马赛克(花屏),当然如果I帧丢失,整个视频都无法解码。
- 视频卡顿原因
为了避免花屏问题的发生,当发现有帧丢失时,就丢弃GOP内的所有帧,直到下一个IDR帧重新刷新图像。
I帧是按照帧周期来的,需要一个比较长的时间周期,如果在下一个I帧来之前不显示后来的图像,那么视频就静止不动了,这就是出现了所谓的卡顿现象。
花屏和卡顿是不兼容的两种问题,如果一定会有问题出现的话,只能根据具体的业务来进行取舍。
H264无损压缩及编解码处理流程
之前的帧内压缩和帧间压缩都属于有损压缩,但对于H264来说,经过了有损压缩,依然会觉得还不够小,这就有了无损压缩技术继续压缩。
- DCT变换
数据经过有损压缩之后,会分散在一个二维矩阵中。当数据比较分散的时候,进行压缩就比较困难,通过DCT变换后形成了一个滤波,会将分散的数据集中到一块,再进行无损压缩的时候,就会非常的方便。比方说上图中的一个8*8的宏块,经过了DCT变换之后就变成了如下的形式
- VLC压缩
当所有的数据通过DCT变换完成之后,就可以进行无损压缩了,一般的无损压缩有两种,一种是VLC压缩,它是MPEG2的压缩方式。
VLC是可变长的编码,如上图所示,表示26个字母的编码。由于A的使用率比较高,就会使用一个短码,而Z的使用率比较低,就会使用一个长码。对于宏块也是一样,经常出现的宏块就使用短码,不常出现的宏块则使用长码。当解码的时候使用规则进行反向操作就可以得到原始数据。
- CABAC压缩
CABAC是H264的编码方式,它的压缩比更高。CABAC是一种带上下文的压缩方式,在上图中我们可以看到相同的图片帧进行VLC进行压缩的时候,它几乎是等大小的压缩块,而进入CABAC之后,前面的压缩块可能跟VLC差不多,但经过一段时间后,由于有上下文的关系,它的压缩块就会变的更小。
- H264编码流程
上图中青色部分的\(F_n\)代表当前要编码的帧,它是一个IDR帧,需要使用帧内编码,首先选择帧内预测模式(Choose Intra prediction),选好帧内预测模式后进行帧内预测(Intra prediction),将每一个宏块的预测模式给计算出来。计算完成之后的数据会与当前帧\(F_n\)进行一个差值计算,将残差值与每一个宏块的预测数据加在一起\(D_n\),再经过一个无损变换T,转换成无损编码Q,再进行拆包X打成NAL头进行数据的分发,这是帧内编码的流程。
对于帧间编码,主要是以\(F'_{n-1}\)为参考,首先要经过运动评估(ME),对每一个宏块进行匹配查找,找到之后得到运动矢量(MC),根据运动矢量推算出整个运动评估之后的帧值,之后再与当前帧\(F_n\)做残差值计算,用当前帧\(F_n\)减去运动估算得到残差值,再使用运动矢量数据再进行转换T,量化Q,最后生成NAL。
- H264解码流程
如果是网络传输的话,是通过NAL一个一个数据包过来的,过来之后再经过反量化\(Q^{-1}\),反转换\(T^{-1}\),再根据前面已经解码后的参考帧,还原回图像数据\(F'_n\)。
H264码流结构
在整个的H264编码结束之后,它输出的结果是H264码流,得到这个码流之后既可以保存成多媒体文件,也可以直接通过网络进行传输,传输到终端后进行组包、解码还原回原始的数据进行播放。
- H264码流分层
- NAL层,Network Abstraction Layer,视频数据网络抽象层。方便于在网络上传输视频流。NAL层解决的是网络传输的乱序,丢包,重传的问题。
- VCL层,Video Coding Layer,视频数据编码层。这就是之前说的帧内编码,帧间编码,熵编码。
- VCL结构关系
上图中,最上面的部分是一帧一帧的视频帧,其中每一个视频帧是由slice组成(中间图像的黄色部分),一般情况下一个slice对应整个图像,但是在H264当中,一个图像可以分很多个slice,是想编解码器将图像分成很多的小块,更便于进行网络传输。每一个slice是由很多宏块组成的,因为在压缩之前需要将一张图像划分成很多的宏块再去进行压缩。一个宏块又可以包含几个子块。
- 码流基本概念
- SODB(String Of Data Bits),二进制数据串,原始数据比特流,长度不一定是8的倍数,故需要补齐。它是由VCL层产生的。该数据串都是以“位”进行编码的,目的是为了更好的压缩,每一位都可能代表着某种含义;如果是按照子节来编码,则会造成空间浪费,
- RBSP(Raw Byte Sequence Payload),按字节存储的原始数据。SODB+trailing bits,算法是如果SODB最后一个字节不对齐,则补1和多个0。如SODB距离8位(1字节)差5位,则补10000。
- NALU,NAL单元,NAL Header(1B)+RBSP,实际就是一个字节的Header加上RBSP。多个NALU组成了H264码流。
- NAL Unit
在上图中,最上面的就是NALU,由NALU Header和NALU Body组成,NALU Body就是RBSP,RBSP是对SODB进行1字节的补位而成,SODB则是由Slice Header以及Slice Data组成,Slice Data则是由宏块组成。上面的就叫NAL层,下面的就叫VCL层。
- Slice与MacroBlock
我们知道Slice Data是由宏块组成的,宏块就是上图中的MB,而宏块又是由宏块类型(mb_type,九种模式之一),宏块的预测值(mb_pred,九种模式中的一种进行的预测)以及残差值(coded residual,预测值与原始图像的差值)。
- 整体格式
除了NAL Unit的结构外,H264的码流包含了两种格式,一种是在文件中保存的Annexb格式,每一个NAL Unit前面都加一个StartCode,这个StartCode就是00000001或者是000001开头,叫起始码。如果只是在网上传输的时候就是RTP格式,是不包含StartCode的,这里的RTP Packet就是NAL Unit。
视频编解码
H264 SPS中的profile和level
- H264 Profile,对视频压缩特性的描述,Profile越高,就说明采用了越高级的压缩特性。
- H264 Level,是对视频的描述,Level越高,视频的码率、分辨率、fps(帧率)越高。
- H264 Profile
从上图中我们可以看到,Profile分成了两级,第一级是从CONSSTRAINED BASELINE为核心,发展出来的MAIN Profile;另一级是以CONSSTRAINED BASELINE发展出的BASELINE以及EXTEND Profile。在CONSSTRAINED BASELINE中包括了帧内压缩I帧(I Slice)、帧间压缩P帧(P Slice),但是B帧(B Slice)是在MAIN Profile才出现的。由于B帧是前后帧参考的,所以它的压缩率更高,则MAIN Profile的压缩率比CONSSTRAINED BASELINE更高。
对于无损压缩来说,CONSSTRAINED BASELINE使用的是比较老的CAVLC,而MAIN Profile使用的是比较新的CABAC,压缩率也就更高。实际上在MAIN Profile之上还有更多的分层。
在MAIN Profile之上有HIGH Profile,HIGH10 Profile,HIGH422 Profile,HIGH444 Profile。每一层都是在原来的基础之上增加了更新的压缩特性,压缩比更高。HIGO Profile中支持了对颜色、色彩的变化(QP for Cr/Cb),支持了8*8的转换(8*8 transport),8*8的帧内预测(8*8 intra predict),图像格式不再是4:2:0,支持了4:0:0;HIGH10中采样率的位数增加了(9/10),这样清晰度就会更高,因为位数越多,能够存储的信息量就越大;HIGH422中增加了一种图像格式4:2:2;HIGH444中增加了一种格式4:4:4,并且增加了采样率的位数(11/14),颜色分层(color plane),更低的损失(loseless)。
- H264 Level
对于不同的Level,都有不同的码流大小,分辨率。比如说对于level 1来说,它的最大码流只支持64k,分辨率很小128*96,30帧/s或者是176*144,15帧/s。在我们常用的分辨率,比如说640*480,使用的Level为2.2。
H264 SPS中的重要参数
之前说的Profile描述的是与压缩相关的特性,Level描述的是与图像相关的特性。
SPS重要参数
- 分辨率
- pic_width_in_mbs_minus1是宽的记录,但是它记录的并不是像素的多少,而是宏块宽的倍数减1。我们在计算的时候应该是使用pic_width_in_mbs_minus1值加1乘以宏块的宽度,默认的宏块宽度是16*16,所以是(pic_width_in_mbs_minus1+1)*16,这样才能真正就算出图像分辨率的宽;
- pic_height_in_mbs_minus1是高的记录,具体含义同上;
- frame_mbs_only_flag是编码的时候使用哪一种编码,如果是帧编码就是对图像逐行扫描,如果是场编码就是隔行扫描,实际上将一张完整的图分成了两张图,一张没有偶数行,一张没有奇数行。
- frame_crop_left_offset,如果该值为1,则我们需要关注后面的4项:依次是需要裁剪的左边的偏移,右边的偏移,顶部的偏移,底部的偏移。
- 帧相关
- 帧数 log2_max_frame_num_minus4,在一个GOP中解码的帧号,最大帧数。最大帧数是通过该值得到,具体为2的(log2_max_frame_num_minus4+4)次方得到。该值默认情况为0,则最大帧数就是\(2^4=16\)帧。除了最大帧数,还可以知道被解码的序号是多少。
- 参考帧数 max_num_ref_frames,在解码的时候,参考帧的缓冲队列为多大。
- 显示帧序号 pic_order_cnt_type,当我们将编码后的数据进行解码之后,在一个GOP中帧显示的顺序。在具体计算显示帧序号的时候是根据type来计算的,该type有三个值——0、1、2。对于不同的type有不同的计算公式。
- 帧率的计算
通过sps还可以计算帧率。
- 总结
通过sps我们可以拿到的信息:Profile、Level、分辨率、参考帧、GOP中的帧数以及显示的顺序、帧率。
H264 PPS与Slice-Header
- PPS
- entropy_coding_mode_flag,熵编码(无损编码)的模式,1为CABAC,0为VLC。
- num_slice_groups_minus1,1帧中的分片数量,本身是减1的,如果是0就是1。
- weighted_pred_flag,在P帧中开启权重预测,1就开启,0未开启。
- weighted_bipred_idc,B帧中的加权预测,idc表示方法号,对于不同的id方法是不一样的。
- pic_init_qp_minus26/pic_init_qs_minus26,这里只是一些初始化参数,真正的初始化是在Slice Header中。
- chroma_qp_index_offset,色度量化参数。
- deblocking_filter_control_present_flag,滤波器,表示是否在Slice header中是否存在用于去除斑块的滤波信息。如果值为1就表示在Slice header中具有去滤波的相关参数,0则没有。
- constrained_intra_pred_flag,如果为1使用帧内预测,如果为0使用帧间预测。
- redundant_pic_present_flag,见图,关注的不多。
一般我们会比较关注1、2、3、4、7。
- Slice Header
- 帧类型,Slice中的每一帧的类型(I、P、B)都在这里做了记录。
- GOP中解码帧序号——frame-num,解码器会根据这个序号来进行解码。
- 预测权重
- 滤波
H264分析工具
我这里使用的是stream-eye,下载地址:https://www.elecard.com/products/video-analysis/streameye
打开时界面如下所示(我是mac版的)
打开一个.mp4的文件选择AVC/H.264
会得到这样一个画面
最上面的部分是每帧数值的柱状图,其中红色的是I帧,其他的是P帧,这里没有B帧。
这是软件默认的BarChart选项卡,选择第二个Thumbnails选项卡,是每一帧的画面
最后一个AreaChart选项卡是波形图。
左下的部分是视频流的各项参数
中间的部分是当前帧的画面,它分为四个选项卡
- 默认的第一个选项卡Decoded是解码后的画面,当我们把鼠标在画面中移动的时候可以看见它的宏块划分,即上图中的小红块。
- 第二个选项卡Predicted是预测的画面,一般会跟解码后的画面有一定的差距。
- 第三个选项卡Unfiltered是未过滤的画面。
- 第四个选项卡Residual是残差值画面,一般是一张灰度图。
右边的部分是划分的宏块信息
最下面的部分是视频流的二进制编码数据
工具栏中有几个比较重要的工具
第一个按钮包含的菜单如下
它主要是对哪些内容进行显示,有类型、运动矢量、工具集等等。
第二个按钮的菜单如下
它代表的就是在一帧中有多少个Slices。
第三个按钮是宏块的划分,当点击的时候就会把一帧中所有的宏块显示出来。
另外第三个按钮还可以关闭打开一些值,如预测值,转换值等
第四个按钮是一些帧内的预测
另外它也可以开关一些数据
第五个按钮是背景
第六个按钮是宏块的具体划分,这里需要跟第三个按钮同时按下
当然也可以同时加入背景
当然第六个按钮也有一些挑选的选项,如宏块、预测和转换和文本。
还有一个YUV的按钮,我们可以让其只显示其中一个值
在左下的部分的第三个选项卡就是SPS、PPS以及slice-header。
- 在SPS中
第一个profile_idc就是指定profile是多少,我这里是High100 Profile。再然后的constraint_setx_flag都是一些限制值,默认为0;level_idc的值为51,代表的就是5.1的Level;seq_parameter_set_id是SPS本身的id,所有的PPS都会跟该值有关联;再然后是根据profile的多少来进行设置
如果profile是High100 Profile,chroma_format_idc就是视频的YUV为4:2:0;bit_depth_luma_minus8代表位深的色度为8位,bit_depth_chroma_minus8代表亮度为8位。
再下来的log2_max_frame_num_minus4代表在一个GOP中最大的帧数量,我这里为\(2^{12}\)=4096帧;pic_order_cnt_type代表的是显示帧的顺序,我这里使用的type值为0,表示使用第0种计算方式来显示;max_num_ref_frames代表最大参考帧数,我这里是2;pic_width_in_mbs_minus1和pic_height_in_mbs_minus1代表分辨率为1280*1024;frame_mbs_only_flag这里为1表示使用的是帧编码而不是场编码;frame_cropping_flag为0表示视频未进行裁剪。
最后一个项目展开,它是vui的参数,可以用来计算帧数。具体的计算是在timing_info_present_flag中计算的。由于我这里是0,如果是1的话会有如下的展示
它的计算方式是time_scale/num_units_in_tick/2=44/1/2=22,所以它这里的帧率为22。
- 在PPS中
seq_parameter_set_id为PPS对应的SPS的id为0;entropy_coding_mode_flag为采用的熵编码的模式为CABAC;num_slice_groups_minus1代表每一帧中Slice的个数,这里为1,就是只有一个Slice;weighted_pred_flag代表预测的权重值是否开启,这里为0未开启;weighted_bipred_idc代表B帧的预测方法号为0;pic_init_qp_minus26/pic_init_qs_minus26为量化的初始化参数,我这里为25和26。
- 在slice-header中
slice_type为帧的类型,我这里为I帧;frame_num为解码帧数;if (eeblocking_filter_control_present_flag)为滤波,我们将其展开
disable_deblocking_filter_idc为是否显示滤波,这里为0不显示。
FFmpeg H264代码开发
编译ffmpeg
源码地址:http://ffmpeg.org/download.html#releases
选择4.1版本,下载后的文件为
ffmpeg-4.1.10.tar.bz2,解压
bzip2 -d ffmpeg-4.1.10.tar.bz2
tar -xvf ffmpeg-4.1.10.tar
依次执行以下命令安装依赖
sudo apt-get install yasm
sudo apt-get install libx264-dev
sudo apt-get install libmp3lame-dev
sudo apt-get install libopus-dev
进入解压后的文件夹,依次执行
cd ffmpeg-4.1.10
./configure --prefix=/usr/local/ffmpeg --enable-shared --enable-pic --enable-gpl --enable-libx264 --enable-libmp3lame --enable-libopus
make
sudo make install
安装完成后设置环境变量
sudo vim /etc/profile
将
/usr/local/ffmpeg/bin
添加到PATH环境变量中,保存,执行
source /etc/profile
这样我们在任意位置执行
ffmpeg
可以得到输出打印
ffmpeg version 4.1.10 Copyright (c) 2000-2022 the FFmpeg developers
built with gcc 11 (Ubuntu 11.3.0-1ubuntu1~22.04)
configuration: --prefix=/usr/local/ffmpeg --enable-shared --enable-pic --enable-gpl --enable-libx264 --enable-libmp3lame --enable-libopus
libavutil 56. 22.100 / 56. 22.100
libavcodec 58. 35.100 / 58. 35.100
libavformat 58. 20.100 / 58. 20.100
libavdevice 58. 5.100 / 58. 5.100
libavfilter 7. 40.101 / 7. 40.101
libswscale 5. 3.100 / 5. 3.100
libswresample 3. 3.100 / 3. 3.100
libpostproc 55. 3.100 / 55. 3.100
Hyper fast Audio and Video encoder
usage: ffmpeg [options] [[infile options] -i infile]... {[outfile options] outfile}...
建立库文件的使用链接
sudo vim /etc/ld.so.conf.d/ffmpeg.conf
添加如下内容
/usr/local/ffmpeg/lib
执行
sudo ldconfig
- 第一个HelloWorld代码
ff.cpp
#include <stdio.h>
#define __STDC_CONSTANT_MACROS
// Linux...
#ifdef __cplusplus
extern "C" {
#endif
#include <libavutil/avutil.h>
#ifdef __cplusplus
};
#endif
int main() {
av_log_set_level(AV_LOG_DEBUG);
av_log(NULL, AV_LOG_DEBUG, "hello world!\n");
return 0;
}
Makefile
EXE=ff
INCLUDE=/usr/local/ffmpeg/include/
LIBPATH=/usr/local/ffmpeg/lib/
CFLAGS= -I$(INCLUDE)
LIBS= -lavcodec -lswscale -lswresample -lavutil -lavfilter
LIBS+= -L$(LIBPATH)
CXX_OBJECTS := $(patsubst %.cpp,%.o,$(shell find . -name "*.cpp"))
DEP_FILES =$(patsubst %.o, %.d, $(CXX_OBJECTS))
$(EXE): $(CXX_OBJECTS)
$(CXX) $(CXX_OBJECTS) -o $(EXE) $(LIBS)
%.o: %.cpp
$(CXX) -c -o $@ $(CFLAGS) $(LIBS) $<
clean:
rm -rf $(CXX_OBJECTS) $(DEP_FILES) $(EXE)
test:
echo $(CXX_OBJECTS
执行make,运行
./ff
得到结果
hello world!
ffmpeg简单操作开发
- 文件操作
file.cpp
#include <stdio.h>
#define __STDC_CONSTANT_MACROS
// Linux...
#ifdef __cplusplus
extern "C" {
#endif
#include <libavformat/avio.h>
#include <libavutil/avutil.h>
#ifdef __cplusplus
};
#endif
int main(int argc, char *argv[]) {
int ret;
//重命名文件
ret = avpriv_io_move("1111.txt", "2222.txt");
if (ret < 0) {
av_log(NULL, AV_LOG_ERROR, "Failed to rename 1111.txt\n");
return -1;
}
av_log(NULL, AV_LOG_INFO, "Success to rename 1111.txt\n");
//删除文件
ret = avpriv_io_delete("./test.txt");
if (ret < 0) {
av_log(NULL, AV_LOG_ERROR, "Failed to delete file %s", "test.txt\n");
return -1;
}
av_log(NULL, AV_LOG_INFO, "Success to deelete file %s", "test.txt\n");
return 0;
}
Makefile
EXE=file
INCLUDE=/usr/local/ffmpeg/include/
LIBPATH=/usr/local/ffmpeg/lib/
CFLAGS= -I$(INCLUDE)
LIBS= -lavformat -lavutil
LIBS+= -L$(LIBPATH)
CXX_OBJECTS := $(patsubst %.cpp,%.o,$(shell find . -name "*.cpp"))
DEP_FILES =$(patsubst %.o, %.d, $(CXX_OBJECTS))
$(EXE): $(CXX_OBJECTS)
$(CXX) $(CXX_OBJECTS) -o $(EXE) $(LIBS)
%.o: %.cpp
$(CXX) -c -o $@ $(CFLAGS) $(LIBS) $<
clean:
rm -rf $(CXX_OBJECTS) $(DEP_FILES) $(EXE)
test:
echo $(CXX_OBJECTS)
- 目录文件夹操作
dir.cpp
#include <stdio.h>
#define __STDC_CONSTANT_MACROS
// Linux...
#ifdef __cplusplus
extern "C" {
#endif
#include <libavformat/avio.h>
#include <libavutil/avutil.h>
#ifdef __cplusplus
};
#endif
int main(int argc, char *argv[]) {
int ret;
char errStr[256] = {0};
//上下文
AVIODirContext *ctx = NULL;
AVIODirEntry *entry = NULL;
//设置日志级别
av_log_set_level(AV_LOG_INFO);
//访问目录
ret = avio_open_dir(&ctx, "./", NULL);
if (ret < 0) {
av_strerror(ret, errStr, sizeof(errStr));
av_log(NULL, AV_LOG_ERROR, "Can't open dir:%s\n", errStr);
return -1;
}
while(1) {
ret = avio_read_dir(ctx, &entry);
if (ret < 0) {
av_strerror(ret, errStr, sizeof(errStr));
av_log(NULL, AV_LOG_ERROR, "Can't read dir:%s\n", errStr);
goto __fail;
}
//如果到达目录的末尾
if (!entry) {
break;
}
av_log(NULL, AV_LOG_INFO, "%+12" PRId64 " %s\n", entry->size, entry->name);
avio_free_directory_entry(&entry);
}
__fail:
avio_close_dir(&ctx);
return 0;
}
Makefile只需要将EXE=file改成EXE=dir即可。
- 抽取音频数据
audio.cpp
#include <stdio.h>
#define __STDC_CONSTANT_MACROS
// Linux...
#ifdef __cplusplus
extern "C" {
#endif
#include <libavutil/avutil.h>
#include <libavformat/avformat.h>
#ifdef __cplusplus
};
#endif
int main(int argc, char *argv[]) {
int ret;
int idx;
char errStr[256] = {0};
//处理参数
char* src;
char* dst;
AVFormatContext *pFmtCtx = NULL;
AVFormatContext *oFmtCtx = NULL;
AVOutputFormat *outFmt = NULL;
AVStream *outStream = NULL;
AVStream *inStream = NULL;
AVPacket pkt;
//设置日志级别
av_log_set_level(AV_LOG_DEBUG);
if (argc < 3) {
av_log(NULL, AV_LOG_INFO, "arguments must be more than 3!\n");
exit(-1);
}
src = argv[1];
dst = argv[2];
//打开多媒体文件
ret = avformat_open_input(&pFmtCtx, src, NULL, NULL);
if (ret < 0) {
av_strerror(ret, errStr, sizeof(errStr));
av_log(NULL, AV_LOG_ERROR, "%s\n", errStr);
exit(-1);
}
//从多媒体文件中找到音频流
idx = av_find_best_stream(pFmtCtx, AVMEDIA_TYPE_AUDIO, -1, -1, NULL, 0);
if (idx < 0) {
av_log(pFmtCtx, AV_LOG_ERROR, "Does not include audio stream!\n");
goto _ERROR;
}
//打开目的文件的上下文
oFmtCtx = avformat_alloc_context();
if (!oFmtCtx) {
av_log(NULL, AV_LOG_ERROR, "NO Memory!\n");
goto _ERROR;
}
outFmt = av_guess_format(NULL, dst, NULL);
oFmtCtx->oformat = outFmt;
//为目的文件创建一个新的音频流
outStream = avformat_new_stream(oFmtCtx, NULL);
//设置音频参数
inStream = pFmtCtx->streams[idx];
avcodec_parameters_copy(outStream->codecpar, inStream->codecpar);
outStream->codecpar->codec_tag = 0;
//绑定
ret = avio_open2(&oFmtCtx->pb, dst, AVIO_FLAG_WRITE, NULL, NULL);
if (ret < 0) {
av_strerror(ret, errStr, sizeof(errStr));
av_log(oFmtCtx, AV_LOG_ERROR, "%s\n", errStr);
goto _ERROR;
}
//写多媒体文件头到目的文件
ret = avformat_write_header(oFmtCtx, NULL);
if (ret < 0) {
av_strerror(ret, errStr, sizeof(errStr));
av_log(oFmtCtx, AV_LOG_ERROR, "%s\n", errStr);
goto _ERROR;
}
//从源多媒体文件中读到音频数据到目的文件中
while(av_read_frame(pFmtCtx, &pkt) >= 0) {
if (pkt.stream_index == idx) {
pkt.pts = av_rescale_q_rnd(pkt.pts, inStream->time_base, outStream->time_base, static_cast<AVRounding>(AV_ROUND_NEAR_INF | AV_ROUND_PASS_MINMAX));
pkt.dts = pkt.pts;
pkt.duration = av_rescale_q(pkt.duration, inStream->time_base, outStream->time_base);
pkt.stream_index = 0;
pkt.pos = -1;
av_interleaved_write_frame(oFmtCtx, &pkt);
av_packet_unref(&pkt);
}
}
//写多媒体文件尾到文件中
av_write_trailer(oFmtCtx);
//将申请的资源释放掉
_ERROR:
if (pFmtCtx) {
avformat_close_input(&pFmtCtx);
pFmtCtx = NULL;
}
if (oFmtCtx->pb) {
avio_close(oFmtCtx->pb);
}
if (oFmtCtx) {
avformat_free_context(oFmtCtx);
oFmtCtx = NULL;
}
return 0;
}
Makefile
EXE=audio
INCLUDE=/usr/local/ffmpeg/include/
LIBPATH=/usr/local/ffmpeg/lib/
#SUBDIR=src object
#PKGS=json-glib-1.0 opencv4
CFLAGS= -I$(INCLUDE)
#CFLAGS= `pkg-config --cflags $(PKGS)`
LIBS= -lavformat -lavutil -lavcodec
LIBS+= -L$(LIBPATH)
#LIBS+= `pkg-config --libs $(PKGS)`
CXX_OBJECTS := $(patsubst %.cpp,%.o,$(shell find . -name "*.cpp"))
DEP_FILES =$(patsubst %.o, %.d, $(CXX_OBJECTS))
$(EXE): $(CXX_OBJECTS)
$(CXX) $(CXX_OBJECTS) -o $(EXE) $(LIBS)
%.o: %.cpp
$(CXX) -c -o $@ $(CFLAGS) $(LIBS) $<
clean:
rm -rf $(CXX_OBJECTS) $(DEP_FILES) $(EXE)
test:
echo $(CXX_OBJECTS)