视频流整理

原创
04/17 16:22
阅读数 5.3K

码流的计算

  • 分辨率
  1. x轴的像素个数*y轴的像素个数
  2. 常见的宽高比:16:9    4:3
  3. 360P/720P/1K/2K:这些都是16:9的宽高比,其中360P为640*360;720P为1280*720;1K为1920*1080,即1080P;2K为2560*1440,即1440P。
  • 帧率
  1. 每秒钟采集/播放图像的个数
  2. 常见的帧率:15帧/s,30帧/s,60帧/s
  • 未编码视频的RGB码流

RGB码流=分辨率(宽*高)*3(通道)*帧率(25帧/s)。例如:1280*720*3*25=69120000,约69M

  • YUV

YUV是一种图像格式,具体可以参考OpenCV 计算机视觉整理 中的 YUV。

YUV数据量为:

  1. YUV=Y * 1.5
  2. YUV=RGB / 2

H264编码原理

H264压缩比

条件:

  1. YUV格式为YUV420P
  2. 分辨率为640*480
  3. 帧率为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帧

  • 编码帧的分类
  1. I帧(intraframe frame),关键帧,采用帧内压缩技术。GOP中的第一帧为I帧,且是一种特殊的I帧,称为IDR帧,IDR帧属于I帧,但I帧不一定是IDR帧。一组帧中有很多帧。如果超过了一定范围,对于H264来说,它会强制加入I帧,防止出现错误的时候,错误出现串联。I帧是不依赖于任何参考帧的,它属于帧内压缩技术,它自己编码,自己还原,跟其他帧没有任何关系。
  2. P帧(forward Predicted frame),向前参考帧。压缩时,只参考前面已经处理的帧(前面的帧解码后才能解码P帧,不能单独解码P帧),采用帧间压缩技术。它占I帧的一半大小。
  3. B帧(Bidirectionally predicted frame),双向参考帧。压缩时,即参考前面已经处理的帧,也参考后面的帧,帧间压缩技术。它占I帧\(1\over 4\)大小。
  4. 播放的时候是先播放I帧,B帧,P帧;但是解码的时候是先解码I帧,P帧,B帧。一般在实时通讯场景中,只有I帧和P帧,没有B帧。但是在视频转码的过程中会大量使用B帧,节省空间。
  • IDR帧与I帧的区别与联系
  1. IDR(Instantaneous Decoder Refresh)解码器立即刷新。
  2. 每当遇到IDR帧时,解码器就会清空解码器参考buffer中的内容。在解码器端遇到IDR帧之后,会清空缓冲区,全部重新来过。
  3. 每个GOP中的第一帧就是IDR帧。
  4. 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压缩技术

有损无损压缩

  1. 帧内压缩,解决的是空域数据冗余问题。解决一张图片内的数据压缩问题。
  2. 帧间压缩,解决的是时域数据冗余问题。随着时间的推移,每个时间段都会有一帧数据。每帧数据之间可以做参考。
  3. 整数离散余弦变换(DCT),将空间上的相关性变为频域上无关的数据然后进行量化。
  4. CABAC压缩,根据上下文进行数据压缩。

对于DCT和CABAC都属于无损压缩技术,帧内压缩和帧间压缩都属于有损压缩技术。

宏块

  1. 宏块是视频压缩操作的基本单元。
  2. 无论是帧内压缩还是帧间压缩,它们都以宏块为单位。

这是一张原始图像。

我们在这个原始图像的左上角先切一个宏块。这个宏块是一个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。

帧内压缩技术

  • 帧内压缩的理论:
  1. 相临像素差别不大,所以可以进行宏块预测。
  2. 人们对亮度的敏感度超过色度。
  3. 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的亮度块进行预测。这里我们需要知道,亮度块与色度块是单独进行预测的。在右边的红色方块部分,它是通过上面的数据和左边的数据进行预测的,预测的结果跟左边的图的红色方块的数据几乎是一样的,有一些细微的差别,但是差别不是太大。通过这种预测,我们就可以将数据量大大的减少。

对于这些细微的差别,我们还需要去进行处理。在上图中,右边是原始图,左边是预测图,虽然整体上差不多,但是预测图的清晰度会差很多。这是因为在预测的时候有一些块是模糊的,分不清楚的。

  • 帧内预测残差值

当我们对整个图进行了预测之后,还需要将结果与原始图进行一个差值计算,计算出来的结果就是上图中的灰色图。

  • 预测模式信息与残差值压缩

拿到残差值之后进行压缩的时候,直接就进行两个数据的压缩。第一个是预测模式信息的压缩,就是上图中深灰色的部分;另外一个就是残差值的压缩,就是上图中灰色的部分;这两个值加在一起就是我们压缩后的数据。当传到用户端解码的时候,就可以根据这个模式,先把原来的图像预测出来,预测出来之后再加上残差值,就可以完全还原成原来的图像数据。

上图中其实也表现了空间信息,最左边的是原始图像的容量,压缩后变成深灰色的压缩数据和灰色的残差值数据,我们可以看到它的容量小的可怜。

帧间压缩技术

  • 帧间压缩原理
  1. GOP,所谓的帧间压缩一定是在一个GOP之内的,相邻的帧之间进行帧间压缩。它是不可能进行跨GOP进行帧间压缩的。
  2. 参考帧,后面的帧参考前面的帧进行帧间压缩。
  3. 运动估计(宏块匹配+运动矢量),通过宏块匹配的方式找到运动矢量,矢量是指一个宏块从一个坐标到另外一个坐标,是有方向的。运动矢量是指它有一个轨迹,从一个地方到另外一个地方。
  4. 运动补偿(解码),找到残差值,在解码的时候把残差值给补上去。

参考帧,我们还是以这幅图来说明。后面的帧参考前面的帧来进行编码。由于第一张图是IDR帧,它肯定是采用帧内压缩的。这里需要说明的是,第一张的背景跟后面图的背景其实是一样的,只是因为它是I帧,所以做了一个特殊标记。后面的帧相比于第一帧,大部分都是一样的,最主要的区别在于望远镜这里,我们在进行帧间压缩的时候实际只要存储这个望远镜的移动轨迹就可以了。比如说我们将望远镜这里划分成很多小宏块,对于第二帧来说,实际就是将第一帧望远镜划分出来的小宏块移动到了中间的位置,所以它的运动矢量就是从左到右。解码的时候就可以根据第一帧的基本图像再根据运动矢量就可以很快的恢复出第二帧的图像。所以它存储的数据一定是很小的。

  • 宏块查找

在一个GOP中有很多相似的帧,我们抽出相邻的两个帧,如上图所示,假设抽出的是第一帧(右)和第二帧(左)。在第一帧中有一个黄色的宏块,位于右上的位置(位于第二宏块行)。当我们在第二帧中去匹配第一帧中的该黄色宏块的时候,可以使用逐行扫描的方法,去与第二帧中的每一个宏块进行匹配,找相似度最高的(比方说达到95%),我们就认为它是同一个宏块。当在第二帧中扫描到第三行的时候找到了该宏块。找到之后就在第二帧中将其坐标记录下来。这样就可以计算出第一帧中的宏块到第二帧中的方向矢量。

  • 宏块查找算法
  1. 三步搜索
  2. 二维对数搜索
  3. 四步搜索
  4. 钻石搜索
  • 运动估计

对于上面宏块的查找的整个过程就是运动估计。在上图的右边的图中的红色箭头就是该小黄色宏块的运动轨迹,也就是运动矢量,而我们压缩的时候存储的就是该运动矢量。到解码的时候就可以根据运动矢量数据还原回原来的GOP。

  • 运动矢量与补偿压缩

我们除了要获取的运动矢量之外还有一个残差值。我们知道对于这个小黄宏块的查找,它是有一定的变化的,不是百分之百不变的。每一帧图像宏块变化的这部分就是残差值。在解码的时候必须要有运动矢量和宏块残差,先使用运动矢量把相应的宏块大致放入GOP中,再根据每一帧的残差值对其进行补全,这样才能更完整的还原回原来的数据。

  • 帧间压缩的帧类型
  1. P帧
  2. 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码流分层
  1. NAL层,Network Abstraction Layer,视频数据网络抽象层。方便于在网络上传输视频流。NAL层解决的是网络传输的乱序,丢包,重传的问题。
  2. VCL层,Video Coding Layer,视频数据编码层。这就是之前说的帧内编码,帧间编码,熵编码。
  • VCL结构关系

上图中,最上面的部分是一帧一帧的视频帧,其中每一个视频帧是由slice组成(中间图像的黄色部分),一般情况下一个slice对应整个图像,但是在H264当中,一个图像可以分很多个slice,是想编解码器将图像分成很多的小块,更便于进行网络传输。每一个slice是由很多宏块组成的,因为在压缩之前需要将一张图像划分成很多的宏块再去进行压缩。一个宏块又可以包含几个子块。

  • 码流基本概念
  1. SODB(String Of Data Bits),二进制数据串,原始数据比特流,长度不一定是8的倍数,故需要补齐。它是由VCL层产生的。该数据串都是以“位”进行编码的,目的是为了更好的压缩,每一位都可能代表着某种含义;如果是按照子节来编码,则会造成空间浪费,
  2. RBSP(Raw Byte Sequence Payload),按字节存储的原始数据。SODB+trailing bits,算法是如果SODB最后一个字节不对齐,则补1和多个0。如SODB距离8位(1字节)差5位,则补10000。
  3. 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

  1. H264 Profile,对视频压缩特性的描述,Profile越高,就说明采用了越高级的压缩特性。
  2. 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重要参数

  • 分辨率

  1. pic_width_in_mbs_minus1是宽的记录,但是它记录的并不是像素的多少,而是宏块宽的倍数减1。我们在计算的时候应该是使用pic_width_in_mbs_minus1值加1乘以宏块的宽度,默认的宏块宽度是16*16,所以是(pic_width_in_mbs_minus1+1)*16,这样才能真正就算出图像分辨率的宽;
  2. pic_height_in_mbs_minus1是高的记录,具体含义同上;
  3. frame_mbs_only_flag是编码的时候使用哪一种编码,如果是帧编码就是对图像逐行扫描,如果是场编码就是隔行扫描,实际上将一张完整的图分成了两张图,一张没有偶数行,一张没有奇数行。
  4. frame_crop_left_offset,如果该值为1,则我们需要关注后面的4项:依次是需要裁剪的左边的偏移,右边的偏移,顶部的偏移,底部的偏移。
  • 帧相关
  1. 帧数 log2_max_frame_num_minus4,在一个GOP中解码的帧号,最大帧数。最大帧数是通过该值得到,具体为2的(log2_max_frame_num_minus4+4)次方得到。该值默认情况为0,则最大帧数就是\(2^4=16\)帧。除了最大帧数,还可以知道被解码的序号是多少。
  2. 参考帧数 max_num_ref_frames,在解码的时候,参考帧的缓冲队列为多大。
  3. 显示帧序号 pic_order_cnt_type,当我们将编码后的数据进行解码之后,在一个GOP中帧显示的顺序。在具体计算显示帧序号的时候是根据type来计算的,该type有三个值——0、1、2。对于不同的type有不同的计算公式。
  • 帧率的计算

通过sps还可以计算帧率。

  • 总结

通过sps我们可以拿到的信息:Profile、Level、分辨率、参考帧、GOP中的帧数以及显示的顺序、帧率。

H264 PPS与Slice-Header

  • PPS

  1. entropy_coding_mode_flag,熵编码(无损编码)的模式,1为CABAC,0为VLC。
  2. num_slice_groups_minus1,1帧中的分片数量,本身是减1的,如果是0就是1。
  3. weighted_pred_flag,在P帧中开启权重预测,1就开启,0未开启。
  4. weighted_bipred_idc,B帧中的加权预测,idc表示方法号,对于不同的id方法是不一样的。
  5. pic_init_qp_minus26/pic_init_qs_minus26,这里只是一些初始化参数,真正的初始化是在Slice Header中。
  6. chroma_qp_index_offset,色度量化参数。
  7. deblocking_filter_control_present_flag,滤波器,表示是否在Slice header中是否存在用于去除斑块的滤波信息。如果值为1就表示在Slice header中具有去滤波的相关参数,0则没有。
  8. constrained_intra_pred_flag,如果为1使用帧内预测,如果为0使用帧间预测。
  9. redundant_pic_present_flag,见图,关注的不多。

一般我们会比较关注1、2、3、4、7。

  • Slice Header
  1. 帧类型,Slice中的每一帧的类型(I、P、B)都在这里做了记录。
  2. GOP中解码帧序号——frame-num,解码器会根据这个序号来进行解码。
  3. 预测权重
  4. 滤波

H264分析工具

我这里使用的是stream-eye,下载地址:https://www.elecard.com/products/video-analysis/streameye

打开时界面如下所示(我是mac版的)

打开一个.mp4的文件选择AVC/H.264

会得到这样一个画面

最上面的部分是每帧数值的柱状图,其中红色的是I帧,其他的是P帧,这里没有B帧。

这是软件默认的BarChart选项卡,选择第二个Thumbnails选项卡,是每一帧的画面

最后一个AreaChart选项卡是波形图。

左下的部分是视频流的各项参数

中间的部分是当前帧的画面,它分为四个选项卡

  1. 默认的第一个选项卡Decoded是解码后的画面,当我们把鼠标在画面中移动的时候可以看见它的宏块划分,即上图中的小红块。
  2. 第二个选项卡Predicted是预测的画面,一般会跟解码后的画面有一定的差距。
  3. 第三个选项卡Unfiltered是未过滤的画面。
  4. 第四个选项卡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

安装ffplay所需要的依赖

sudo apt-get install libsdl2-dev
sudo apt-get install libsdl2-2.0-0
sudo apt-get install libsdl2-image-dev
sudo apt-get install libsdl2-image-2.0-0
sudo apt-get install libsdl2-mixer-dev
sudo apt-get install libsdl2-mixer-2.0-0
sudo apt-get install libsdl2-net-dev
sudo apt-get install libsdl2-net-2.0-0

如果安装libsdl2-dev报错

The following packages have unmet dependencies:
 libegl-mesa0 : Depends: libgbm1 (= 22.0.5-0ubuntu0.1) but 22.0.5-0ubuntu0.2 is to be installed
 udev : Breaks: systemd (< 249.11-0ubuntu3.6)
        Breaks: systemd:i386 (< 249.11-0ubuntu3.6)

执行以下命令安装libsdl2-dev

sudo apt-get install aptitude
sudo aptitude install libsdl2-dev

其余组件安装依照顺序继续往后执行即可

进入解压后的文件夹,依次执行

cd ffmpeg-4.1.10
./configure --prefix=/usr/local/ffmpeg --enable-shared --enable-pic --enable-gpl --enable-libx264 --enable-libmp3lame --enable-libopus --enable-ffplay
make
sudo make install

安装完成后设置环境变量

sudo vim /etc/profile

/usr/local/ffmpeg/bin

添加到PATH环境变量中,保存

如果之前没有设置过可以设置如下

export PATH=/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即可。

  • 多媒体文件的基本概念
  1. 多媒体文件其实是个容器,它可以包含很多类型的数据,譬如说音频数据、视频数据、字幕数据。
  2. 在容器里有很多流(Stream/Track),如音频流和视频流,它们是永远不交叉的。流也称为轨。
  3. 每种流是由不同编码器编码的,每一路音频流、视频流在多媒体文件中都是压缩的,音频一般使用的是mp3、aac,视频一般使用的是H264、H265。
  4. 从流中读出的数据称为包。在一个包中包含着一个或多个帧。这些帧数据其实就是未压缩的数据,为了便于传输和存储,需要将这些帧进行压缩,压缩后的数据会打成一个包,这个包中可能是一帧压缩的数据也有可能是多帧压缩的数据。很多包组成流,由多个流组成多媒体文件。
  • 几个重要的结构体(C中,可以理解成C++中的类)
  1. AVFormatContext:格式上下文,是连接多个API之间的桥梁。打开多媒体文件的时候需要创建一个上下文,把一些基本信息放到上下文中,当我们要读多媒体的流的时候,就需要传入该上下文到其他函数中,这样就知道处理的是哪个多媒体。
  2. AVStream:流,打开多媒体文件后,所有的流(音频流、视频流、字幕流)都暴露出来了,通过AVStream就可以将其读取出来。
  3. AVPacket:包,拿到包之后就能拿到被压缩的帧,拿到被压缩的帧之后就能够通过解码器将这些压缩帧解码成原始数据。
  • FFmpeg操作数据的基本步骤

  • 抽取音频数据

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;
    //保存了一些输出的文件格式,如mp4、flv、3gp等的信息以及一些常规设置
    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];

    //打开多媒体文件
    //第三个参数为AVInputFormat类型,表示如果多媒体文件的后缀为.flv,实际存放的数据为mp4,则会按照flv进行解析
    //此时必须设置为mp4解析;如果传入的跟文件格式一致,可以设置为NULL。
    //第四个参数为一个选项,如果没有特殊选项只需要设置为NULL
    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);
    }
    //从多媒体文件中找到音频流
    //第二个参数为媒体类型,是音频、视频还是字幕
    //第三个参数表示当存在多个流的时候,比如说有两路音频流,每路音频流都有自己的stream_number,通过该参数可以指定我们需要的是哪个stream_number
    //当不知道的时候可以设置-1,此时会查找到与流的类型相匹配的第一个流的stream_number
    //第四个参数为related_stream,音视频流是由多个分片组成的,每个分片中音视频流又对应与不同的节目,对于该参数一般都设置为-1
    //第五个参数为优先使用的解码器,如果指定的话就会使用该解码器进行解码,一般设置为NULL,就是不指定解码器
    //第六个参数flag,一般设为0
    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;
    }
    //通过输出的目标文件名可以找到AVOutputFormat,通过该api可以大大减少设置参数的复杂度
    outFmt = av_guess_format(NULL, dst, NULL);
    //将AVOutputFormat设置到oFmtCtx中,这样oFmtCtx中就记录了一些输出的最基本的参数
    oFmtCtx->oformat = outFmt;
    //为目的文件创建一个新的音频流
    outStream = avformat_new_stream(oFmtCtx, NULL);
    //设置音频参数
    //通过查找流时得到的索引获取流
    inStream = pFmtCtx->streams[idx];
    //将输入流的编解码器复制到输出流中
    avcodec_parameters_copy(outStream->codecpar, inStream->codecpar);
    //将该值设置为0可以根据多媒体文件来自动识别编解码器
    outStream->codecpar->codec_tag = 0;
    //将目的文件上下文与目的文件绑定
    //第一个参数为AVIOContext,上下文中的pb就是该结构
    //第二个参数为输出的目的文件
    //第三个参数为指定对输出文件的操作,比如只读、只写或者是读写
    //第四个参数是一个回调函数
    //第五个参数是一些私有协议的选项
    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) {
            //改变时间戳,时间戳包括pts和dts
            //第一个参数是原始的pts
            //第二个参数是输入流的时间基(单位毫秒)
            //第三个参数是输出流的时间基(单位微秒)
            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));
            //音频的dts等于pts,视频的不同
            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/
CFLAGS= -I$(INCLUDE)
LIBS= -lavformat -lavutil -lavcodec
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)

编译完成后执行

./audio ../football.mp4 ./1.aac

执行完成后会得到一个1.aac的文件,就是football.mp4视频文件中的音频数据,并且可以播放。

  • 抽取视频数据

video.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_VIDEO, -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 = av_rescale_q_rnd(pkt.dts, inStream->time_base, outStream->time_base, static_cast<AVRounding>(AV_ROUND_NEAR_INF | AV_ROUND_PASS_MINMAX));
            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名即可,编译后,执行

./video ../football.mp4 ./1.h264

1.h264为输出视频文件,它是没有声音的,可以使用ffplay播放。当然也可以输出成flv格式

./video ../football.mp4 ./1.flv

.h264和.flv格式的不同在于.h264只保存视频的裸数据,其他如帧率的信息都不会记录;而.flv是带源信息的视频,它是可以保存如帧率这些信息的。

FFmpeg编解码

  • 常用数据结构
  1. AVCodec编码器结构体,我们在编码中使用H264还是H265,或者是音频的AAC都记录在该结构体中。
  2. ACCodecContext编码器上下文,是连接多个API之间的桥梁。这样在API中传入该上下文才知道使用的编码器是哪个(H264还是H265等)。
  3. AVFrame解码后的帧
  • 结构体内存的分配与释放
  1. av_frame_alloc()/av_frame_free(),帧的分配与释放。
  2. avcodec_alloc_context3()/avcodec_free_context(),创建与释放编码器上下文。
  • 视频编码

videocoder.cpp

#include <stdio.h>
#define __STDC_CONSTANT_MACROS
// Linux...
#ifdef __cplusplus
extern "C" {
#endif
    #include <libavutil/avutil.h>
    #include <libavutil/opt.h>
    #include <libavcodec/avcodec.h>
#ifdef __cplusplus
};
#endif

static int encode(AVCodecContext *ctx, AVFrame *frame, AVPacket *pkt, FILE *out) {
    int ret;
    //将frame送入编码器进行编码
    ret = avcodec_send_frame(ctx, frame);
    if (ret < 0) {
        av_log(NULL, AV_LOG_ERROR, "Faild to send frame to encoder!\n");
        goto _END;
    }
    while(ret >= 0) {
        //将编码后的数据保存在包里
        ret = avcodec_receive_packet(ctx, pkt);
        //当数据不足时或者缓冲区已经没有数据时,退出
        if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {
            return 0;
        } else if (ret < 0) { //严重错误
            return -1;
        }
        //将包中的数据写入到文件中
        fwrite(pkt->data, 1, pkt->size, out);
        //释放包
        av_packet_unref(pkt);
    }
_END:
    return 0;
}

int main(int argc, char* argv[]) {
    int ret;
    FILE *f;
    char* dst; //目标文件
    char* codecname;  //编码器名称
    char errStr[256] = {0};
    AVCodec *codec = NULL;  //编码器
    AVCodecContext *ctx = NULL;  //编码器上下文
    AVFrame *frame = NULL;  //帧
    AVPacket *pkt = NULL;  //包

    av_log_set_level(AV_LOG_DEBUG);
    if (argc < 3) {
        av_log(NULL, AV_LOG_ERROR, "arguments must be more than 3!\n");
        goto _ERROR;
    }

    dst = argv[1];
    codecname = argv[2];

    //通过编码器名称查找编码器,这里我们使用的编码器名称为libx264
    codec = avcodec_find_encoder_by_name(codecname);
    if (!codec) {
        av_log(NULL, AV_LOG_ERROR, "don't find Codec:%s\n", codecname);
        goto _ERROR;
    }
    //通过编码器创建编码器上下文
    ctx = avcodec_alloc_context3(codec);
    if (!ctx) {
        av_log(NULL, AV_LOG_ERROR, "NO MEMRORY\n");
        goto _ERROR;
    }
    //设置编码器参数
    ctx->width = 640;  //宽
    ctx->height = 480;  //高
    ctx->bit_rate = 500000;  //码率,通常码率越高越清晰,但是有限额,当达到一定限额设置再高也无用
    ctx->time_base = (AVRational){1, 25}; //时间基,1秒25帧
    ctx->framerate = (AVRational){25, 1}; //帧率(与时间基互为相反),25帧1秒
    ctx->gop_size = 10;  //相似帧数量
    ctx->max_b_frames = 1; //b帧最大数量
    ctx->pix_fmt = AV_PIX_FMT_YUV420P; //视频源格式,这里为YUV格式
    if (codec->id == AV_CODEC_ID_H264) {
        //为编码器设置私有属性,preset表示预先设置参数,值为slow,编码视频清晰度会更高
        av_opt_set(ctx->priv_data, "preset", "slow", 0);
    }
    //编码器与编码器上下文绑定
    ret = avcodec_open2(ctx, codec, NULL);
    if (ret < 0) {
        av_strerror(ret, errStr, sizeof(errStr));
        av_log(ctx, AV_LOG_ERROR, "don't open codec: %s\n", errStr);
        goto _ERROR;
    }
    //创建输出文件
    f = fopen(dst, "wb");
    if (!f) {
        av_log(NULL, AV_LOG_ERROR, "Don't open file:%s\n", dst);
        goto _ERROR;
    }
    //创建帧
    frame = av_frame_alloc();
    if (!frame) {
        av_log(NULL, AV_LOG_ERROR, "NO MEMORY\n");
        goto _ERROR;
    }
    frame->width = ctx->width; //设置帧宽
    frame->height = ctx->height;  //设置帧高
    frame->format = ctx->pix_fmt;  //设置帧的视频源格式
    //为frame的data域分配一个buffer
    ret = av_frame_get_buffer(frame, 0);
    if (ret < 0) {
        av_log(NULL, AV_LOG_ERROR, "Could not allocate the video frame \n");
        goto _ERROR;
    }
    //创建包
    pkt = av_packet_alloc();
    if (!pkt) {
        av_log(NULL, AV_LOG_ERROR, "NO MEMORY\n");
        goto _ERROR;
    }
    //生成视频内容,每次循环都产生1帧数据
    for(int i = 0; i < 25; i++) {
        //确保frame中的data域空间有效,在编码时需要将frame传给编码器,
        //编码器拿到frame之后,会锁定frame中的data,然后拿取数据进行编码
        //在其锁定的时候,如果想使用frame再向编码器传送1帧数据,此时会产生冲突
        //该函数会检测frame中的data域,看是否被锁定,如果被锁定会重新为frame的data域
        //分配一个buffer;如果没有锁定,说明现在这个data域是可写的,这样就可以在之后
        //向data域中写入数据
        ret = av_frame_make_writable(frame);
        if (ret < 0) {
            break;
        }
        //Y分量(亮度),对每一帧进行遍历
        //y控制纵轴,x控制横轴
        for(int y = 0; y < ctx->height; y++) {
            for(int x = 0; x < ctx->width; x++) {
                //对图像中的每一个像素进行操控
                //data[0]就是Y数据,frame->linesize[0]表示YUV数据中行的大小
                //+ x表示横向移动,从0到它的宽度
                frame->data[0][y * frame->linesize[0] + x] = x + y + i * 3;
            }
        }
        //UV分量(颜色)
        for(int y = 0; y < ctx->height / 2; y++) {
            for(int x = 0; x < ctx->width / 2; x++) {
                //U分量,128为黑色
                frame->data[1][y * frame->linesize[1] + x] = 128 + y + i * 2;
                //V分量,64为黑色
                frame->data[2][y * frame->linesize[2] + x] = 64 + x + i * 5;
            }
        }
        //设置帧的位置,第一帧为0,第二帧为1,以此类推
        frame->pts = i;
        //编码
        ret = encode(ctx, frame, pkt, f);
        if (ret < 0) {
            goto _ERROR;
        }
    }
    //编码
    encode(ctx, NULL, pkt, f);
_ERROR:
    if (ctx) {
        avcodec_free_context(&ctx);
    }
    if (frame) {
        av_frame_free(&frame);
    }
    if (pkt) {
        av_packet_free(&pkt);
    }
    if (f) {
        fclose(f);
    }
    return 0;
}

Makefile

EXE=videocoder

INCLUDE=/usr/local/ffmpeg/include/
LIBPATH=/usr/local/ffmpeg/lib/
CFLAGS= -I$(INCLUDE)
LIBS= -lavutil -lavcodec
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)

编译后执行

./videocoder 2.h264 libx264

可以看到生成了2.h264的视频文件,可以使用ffplay播放。播放样式如下

  • 解码步骤
  1. 查找解码器(avcodec_find_decoder)
  2. 打开解码器(avcodec_open2)
  3. 解码(avcodec_decode_video2),解码后就可以拿到未压缩到数据,也就是YUV的数据或RGB的数据,就可以通过播放器进行播放。更准确的说拿到未压缩的数据之后可以通过渲染(OpenGL或者其他),将其渲染到屏幕上。
  • 视频解码

videodecoder.cpp

#include <stdio.h>
#define __STDC_CONSTANT_MACROS
// Linux...
#ifdef __cplusplus
extern "C" {
#endif
    #include <libavutil/avutil.h>
    #include <libavformat/avformat.h>
    #include <libavcodec/avcodec.h>
#ifdef __cplusplus
};
#endif

static void savePic(unsigned char* buf, int linesize, int width, int height, char* name) {
    FILE *f;
    //打开图片文件
    f = fopen(name, "wb");
    //向图片文件写入头信息
    fprintf(f, "P5\n%d %d\n%d\n", width, height, 255);
    for(int i = 0; i < height; i++) {
        //将帧的data域写入文件中,每循环一次写一行,一次写一个字节
        //这里的linesize和width一般情况下是相等的,但width一定是大于等于linesize的
        //这里传入的是data[0],所以传入的是Y分量,图片是没有色彩的
        fwrite(buf + i * linesize, 1, width, f);
    }
    fclose(f);
}

static int decode(AVCodecContext *ctx, AVFrame *frame, AVPacket *pkt, const char* filename) {
    int ret;
    char buf[1024];  //图片文件名
    //将包传入解码器进行解码
    ret = avcodec_send_packet(ctx, pkt);
    if (ret < 0) {
        av_log(NULL, AV_LOG_ERROR, "Faild to send packet to decoder!\n");
        goto _END;
    }
    while(ret >= 0) {
        //从解码器中获取已经解码后的一帧,并进行保存
        ret = avcodec_receive_frame(ctx, frame);
        //当数据不足时或者缓冲区已经没有数据时,退出
        if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {
            return 0;
        } else if (ret < 0) {
            return -1;
        }
        //将帧保存为一张一张的图片,这里是为这些图片构造名字
        snprintf(buf, sizeof(buf), "%s-%d", filename, ctx->frame_number);
        //保存图片
        savePic(frame->data[0], frame->linesize[0], frame->width, frame->height, buf);
        if (pkt) {
            //释放包
            av_packet_unref(pkt);
        }
    }
_END:
    return 0;
}

int main(int argc, char *argv[]) {
    int ret;
    int idx;
    char errStr[256] = {0};
    //处理参数
    char* src;  //源视频文件
    char* dst;  //目标图片文件
    AVCodec *codec = NULL;  //解码器
    AVCodecContext *ctx = NULL;  //解码器上下文
    AVFormatContext *pFmtCtx = NULL;  //源视频文件上下文
    AVStream *inStream = NULL;  //流入流
    AVFrame *frame = NULL;  //帧
    AVPacket *pkt = NULL;  //包

    //设置日志级别
    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_VIDEO, -1, -1, NULL, 0);
    if (idx < 0) {
        av_log(pFmtCtx, AV_LOG_ERROR, "Does not include vedio stream!\n");
        goto _ERROR;
    }
    //通过查找流时得到的索引获取流
    inStream = pFmtCtx->streams[idx];
    //通过源文件的解码信息查找解码器
    codec = avcodec_find_decoder(inStream->codecpar->codec_id);
    if (!codec) {
        av_log(NULL, AV_LOG_ERROR, "Could not find libx264 Codec:%s\n");
        goto _ERROR;
    }
    //创建解码器上下文
    ctx = avcodec_alloc_context3(codec);
    if (!ctx) {
        av_log(NULL, AV_LOG_ERROR, "NO MEMRORY\n");
        goto _ERROR;
    }
    avcodec_parameters_to_context(ctx, inStream->codecpar);
    //解码器与解码器上下文绑定
    ret = avcodec_open2(ctx, codec, NULL);
    if (ret < 0) {
        av_strerror(ret, errStr, sizeof(errStr));
        av_log(ctx, AV_LOG_ERROR, "don't open codec: %s\n", errStr);
        goto _ERROR;
    }
    //创建帧
    frame = av_frame_alloc();
    if (!frame) {
        av_log(NULL, AV_LOG_ERROR, "NO MEMORY\n");
        goto _ERROR;
    }
    //创建包
    pkt = av_packet_alloc();
    if (!pkt) {
        av_log(NULL, AV_LOG_ERROR, "NO MEMORY\n");
        goto _ERROR;
    }
    //从源视频文件中读到视频数据进行解码,并将解码后的帧保存为图片
    //av_read_frame是从源文件上下文中获取包
    while(av_read_frame(pFmtCtx, pkt) >= 0) {
        if (pkt->stream_index == idx) {
            //解码
            decode(ctx, frame, pkt, dst);
        }
    }
    //解码
    decode(ctx, frame, NULL, dst);
    //将申请的资源释放掉
_ERROR:
    if (pFmtCtx) {
        avformat_close_input(&pFmtCtx);
        pFmtCtx = NULL;
    }
    if (ctx) {
        avcodec_free_context(&ctx);
    }
    if (frame) {
        av_frame_free(&frame);
    }
    if (pkt) {
        av_packet_free(&pkt);
    }
    return 0;
}

Makefile

EXE=videodecoder

INCLUDE=/usr/local/ffmpeg/include/
LIBPATH=/usr/local/ffmpeg/lib/
CFLAGS= -I$(INCLUDE)
LIBS= -lavformat -lavutil -lavcodec
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)

编译后执行

./videodecoder ../football.mp4 out

这里我们可以看到把一个视频文件给解码成了一张一张的图片。

  • 生成色彩图片

首先,我们需要对需要解码的视频文件进行信息收集,执行

ffprobe ../football.mp4

得到

Input #0, mov,mp4,m4a,3gp,3g2,mj2, from '../football.mp4':
  Metadata:
    major_brand     : isom
    minor_version   : 512
    compatible_brands: isomiso2avc1mp41
    encoder         : Lavf55.33.100
    copyright       : 
    copyright-eng   : 
  Duration: 00:00:10.47, start: 0.000000, bitrate: 956 kb/s
    Stream #0:0(und): Video: h264 (High) (avc1 / 0x31637661), yuv420p(tv, bt709), 576x1024, 897 kb/s, 30 fps, 30 tbr, 12800 tbn, 25600 tbc (default)
    Metadata:
      handler_name    : VideoHandler
    Stream #0:1(und): Audio: aac (LC) (mp4a / 0x6134706D), 44100 Hz, mono, fltp, 48 kb/s (default)
    Metadata:
      handler_name    : SoundHandler

这里我们可以看到该视频的视频流一些信息,h264编码,yuv420p格式的帧,帧大小为576*1024,帧率为30fps。

videodecodercolor.cpp

#include <stdio.h>
#define __STDC_CONSTANT_MACROS
// Linux...
#ifdef __cplusplus
extern "C" {
#endif
    #include <libavutil/avutil.h>
    #include <libavformat/avformat.h>
    #include <libavcodec/avcodec.h>
    #include <libswscale/swscale.h>
#ifdef __cplusplus
};
#endif

#define WORD uint16_t
#define DWORD uint32_t
#define LONG int32_t

typedef struct tagBITMAPFILEHEADER {
    WORD  bfType;
    DWORD bfSize;
    WORD  bfReserved1;
    WORD  bfReserved2;
    DWORD bfOffBits;
} BITMAPFILEHEADER, *LPBITMAPFILEHEADER, *PBITMAPFILEHEADER;

typedef struct tagBITMAPINFOHEADER {
    DWORD biSize;
    LONG  biWidth;
    LONG  biHeight;
    WORD  biPlanes;
    WORD  biBitCount;
    DWORD biCompression;
    DWORD biSizeImage;
    LONG  biXPelsPerMeter;
    LONG  biYPelsPerMeter;
    DWORD biClrUsed;
    DWORD biClrImportant;
} BITMAPINFOHEADER, *LPBITMAPINFOHEADER, *PBITMAPINFOHEADER;

static void saveBMP(struct SwsContext *swsCtx, AVFrame * frame, int width, int height, char* name) {
    int dataSize = width * height * 3;
    FILE *f = NULL;
    //z转换,将YUV frame转换成BGR24 frame
    AVFrame *frameBGR = av_frame_alloc();
    frameBGR->width = width;
    frameBGR->height = height;
    frameBGR->format = AV_PIX_FMT_BGR24;
    av_frame_get_buffer(frameBGR, 0);
    sws_scale(swsCtx, (const uint8_t * const *)frame->data, frame->linesize, 0, frame->height,
                    (uint8_t * const *)frameBGR, frameBGR->linesize);
    //构造BITMAPINFOHEEADER
    BITMAPINFOHEADER infoheader;
    infoheader.biSize = sizeof(BITMAPINFOHEADER);
    infoheader.biWidth = width;
    infoheader.biHeight = height * (-1);
    infoheader.biBitCount = 24;
    infoheader.biCompression = 0;
    infoheader.biSizeImage = 0;
    infoheader.biClrImportant = 0;
    infoheader.biClrUsed = 0;
    infoheader.biXPelsPerMeter = 0;
    infoheader.biYPelsPerMeter = 0;
    infoheader.biPlanes = 0;
    //构造BITMAPFILEHEEEADER
    BITMAPFILEHEADER fileheader;
    fileheader.bfType = 0x4d42;
    fileheader.bfSize = sizeof(BITMAPFILEHEADER) + sizeof(BITMAPINFOHEADER) + dataSize;
    fileheader.bfOffBits = sizeof(BITMAPFILEHEADER) + sizeof(BITMAPINFOHEADER);
    //将数据写到文件
    f = fopen(name, "wb");
    fwrite(&fileheader, sizeof(BITMAPFILEHEADER), 1, f);
    fwrite(&infoheader, sizeof(BITMAPINFOHEADER), 1, f);
    fwrite(frameBGR->data[0], 1, dataSize, f);
    //释放资源
    fclose(f);
    av_freep(&frameBGR->data[0]);
    av_free(frameBGR);
}

static void savePic(unsigned char* buf, int linesize, int width, int height, char* name) {
    FILE *f;
    f = fopen(name, "wb");
    fprintf(f, "P5\n%d %d\n%d\n", width, height, 255);
    for(int i = 0; i < height; i++) {
        fwrite(buf + i * linesize, 1, width, f);
    }
    fclose(f);
}

static int decode(AVCodecContext *ctx, struct SwsContext *swsCtx, AVFrame *frame, AVPacket *pkt, const char* filename) {
    int ret;
    char buf[1024];
    ret = avcodec_send_packet(ctx, pkt);
    if (ret < 0) {
        av_log(NULL, AV_LOG_ERROR, "Faild to send packet to decoder!\n");
        goto _END;
    }
    while(ret >= 0) {
        ret = avcodec_receive_frame(ctx, frame);
        if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {
            return 0;
        } else if (ret < 0) {
            return -1;
        }
        snprintf(buf, sizeof(buf), "%s-%d.bmp", filename, ctx->frame_number);
        saveBMP(swsCtx, frame, frame->width, frame->height, buf);
        //savePic(frame->data[0], frame->linesize[0], frame->width, frame->height, buf);
        if (pkt) {
            av_packet_unref(pkt);
        }
    }
_END:
    return 0;
}

int main(int argc, char *argv[]) {
    int ret;
    int idx;
    char errStr[256] = {0};
    //处理参数
    char* src;
    char* dst;
    AVCodec *codec = NULL;
    AVCodecContext *ctx = NULL;
    AVFormatContext *pFmtCtx = NULL;
    AVStream *inStream = NULL;
    AVFrame *frame = NULL;
    AVPacket *pkt = NULL;
    struct SwsContext *swsCtx = NULL;
    //设置日志级别
    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_VIDEO, -1, -1, NULL, 0);
    if (idx < 0) {
        av_log(pFmtCtx, AV_LOG_ERROR, "Does not include vedio stream!\n");
        goto _ERROR;
    }
    inStream = pFmtCtx->streams[idx];
    //查找解码器
    codec = avcodec_find_decoder(inStream->codecpar->codec_id);
    if (!codec) {
        av_log(NULL, AV_LOG_ERROR, "Could not find libx264 Codec:%s\n");
        goto _ERROR;
    }
    //创建解码器上下文
    ctx = avcodec_alloc_context3(codec);
    if (!ctx) {
        av_log(NULL, AV_LOG_ERROR, "NO MEMRORY\n");
        goto _ERROR;
    }
    avcodec_parameters_to_context(ctx, inStream->codecpar);
    //解码器与解码器上下文绑定
    ret = avcodec_open2(ctx, codec, NULL);
    if (ret < 0) {
        av_strerror(ret, errStr, sizeof(errStr));
        av_log(ctx, AV_LOG_ERROR, "don't open codec: %s\n", errStr);
        goto _ERROR;
    }
    //获得SWS上下文
    //swsCtx = sws_getCachedContext(NULL, ctx->width, ctx->height, AV_PIX_FMT_YUV420P,
//                                      ctx->width, ctx->height, AV_PIX_FMT_BGR24, SWS_BICUBIC, NULL, NULL, NULL);
    swsCtx = sws_getCachedContext(NULL, ctx->width, ctx->height, AV_PIX_FMT_YUV420P,
                    640, 360, AV_PIX_FMT_BGR24, SWS_BICUBIC, NULL, NULL, NULL);
    //创建AVFrame
    frame = av_frame_alloc();
    if (!frame) {
        av_log(NULL, AV_LOG_ERROR, "NO MEMORY\n");
        goto _ERROR;
    }
    //创建AVPacket
    pkt = av_packet_alloc();
    if (!pkt) {
        av_log(NULL, AV_LOG_ERROR, "NO MEMORY\n");
        goto _ERROR;
    }
    //从源多媒体文件中读到音频数据到目的文件中
    while(av_read_frame(pFmtCtx, pkt) >= 0) {
        if (pkt->stream_index == idx) {
            decode(ctx, swsCtx, frame, pkt, dst);
        }
    }
    decode(ctx, swsCtx, frame, NULL, dst);
    //将申请的资源释放掉
_ERROR:
    if (pFmtCtx) {
        avformat_close_input(&pFmtCtx);
        pFmtCtx = NULL;
    }
    if (ctx) {
        avcodec_free_context(&ctx);
    }
    if (swsCtx) {
        sws_freeContext(swsCtx);
    }
    if (frame) {
        av_frame_free(&frame);
    }
    if (pkt) {
        av_packet_free(&pkt);
    }
    return 0;
}

Makefile

EXE=videodecodercolor

INCLUDE=/usr/local/ffmpeg/include/
LIBPATH=/usr/local/ffmpeg/lib/
CFLAGS= -I$(INCLUDE)
LIBS= -lavformat -lavutil -lavcodec -lswscale
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)

视频流传输

RTMP协议

  • 基本概念

RTMP协议是基于TCP连接的基础之上的。TCP可以理解成上面的绿线,在TCP之上要建立一个RTMP连接,就相当于上图中的管道,建立连接需要进行握手,类似于TCP的三次握手。握手成功后就会对应建立RTMP Connection,此时在管道中就可以进行数据传输了,这些数据并不是裸数据,它是以流的形式进行传输的。当RTMP客户端去发布流的时候,就是上图中橙色的虚线;对于RTMP服务端来说,当客户端发布的时候,它就会接收,当有用户对服务端进行订阅的时候,它就会以另一个流传输出去,就是上图中的蓝色的虚线。

  • RTMP创建流的基本流程
  1. socket建立TCP连接
  2. RTMP握手
  3. 建立RTMP连接
  4. 创建RTMP流
  • RTMP协议中的握手

真实的握手

  • 建立RTMP连接

真实的连接

  • RTMP流的创建

真实创建RTMP流

  • 推RTMP流

  • 播RTMP流

RTMP消息格式

RTMP抓包分析

  • Nginx搭建RTMP服务

nginx下载地址:https://nginx.org/en/download.html

我这里下载的是nginx-1.23.4.tar.gz,解压缩

tar -xzvf nginx-1.23.4.tar.gz

下载rtmp-nginx模块:https://github.com/arut/nginx-rtmp-module

进入nginx本身的目录

cd nginx-1.23.4/

执行以下命令进行安装

sudo apt-get update
sudo apt-cache policy libssl-dev
sudo apt-get install libssl-dev
./configure --add-module=../nginx-rtmp-module-master
make
sudo make install

安装完成后,在/usr/local/nginx/conf/nginx.conf中添加以下内容

rtmp {
    server{
        listen 1935;
        chunk_size 4000;

        application live 
        {
            live on;
            allow play all;
        }

    }       
}

在/usr/local/nginx目录下执行

sudo sbin/nginx -c conf/nginx.conf

执行完后可以执行以下命令查看端口是否启动

netstat -anpl | grep 1935

结果为

tcp        0      0 0.0.0.0:1935            0.0.0.0:*               LISTEN  

表示已经启动。

  • 启动一个rtmp服务订阅
ffplay rtmp://localhost/live/room

得到以下内容

ffplay version 4.1.10 Copyright (c) 2003-2022 the FFmpeg developers
  built with gcc 11 (Ubuntu 11.3.0-1ubuntu1~22.04.1)
  configuration: --prefix=/usr/local/ffmpeg --enable-shared --enable-pic --enable-gpl --enable-libx264 --enable-libmp3lame --enable-libopus --enable-ffplay
  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

它表示在应用下订阅了一个叫room的频道。然后就是进行推流

ffmpeg -re -i football.mp4 -c copy -f flv rtmp://localhost/live/room

之后就可以自动使用ffplay进行播放了。

当然这里是同一台电脑的推流,如果是两台不同的电脑,则我们在使用rtmp服务时需要使用局域网中的ip地址,如

ffplay rtmp://192.168.3.249/live/room

同时乌班图系统需要开启防火墙,首先查看防火墙的状态

sudo ufw status

得到

状态: 激活

至                          动作          来自
-                          --          --
22                         ALLOW       Anywhere                  
22 (v6)                    ALLOW       Anywhere (v6) 

这说明只有22端口是打开的,同时我们需要打开1935端口

sudo ufw allow 1935/tcp

 当然还有拒绝端口和删除允许的端口

sudo ufw deny 113  #拒绝UDP 113端口
sudo ufw delete allow 53/tcp   #删除TCP,53端口

整个关闭防火墙

sudo ufw disable
  • Wireshark抓包分析

我这里的Wireshark使用的是mac版本的,界面如下

这上面的各个项就是我们的网卡的标识,这里是存在虚拟项的,而我们要使用的就是Wi-Fi:en0这一项。首先赋予设备可执行权限

sudo chmod 777 /dev/bpf*

双击Wi-Fi:en0,进入如下界面

它会抓取所有连接本机的IP包,但我们并不需要这么多,只需要抓取连接1935端口的包,在命令框中填入

tcp.port == 1935

然后开始推流

ffmpeg -re -i 2022.mp4 -c copy -f flv rtmp://192.168.3.249/live/room

Wireshark界面显示

最上面的4条是TCP/IP协议的三次握手。现在我们选中第5行(Handshake C0+C1),这个就是RTMP协议的握手了,整个信息内容如下

我们可以看到它的源IP是192.168.3.231,端口号是51488,这是mac中ffmpeg推流的端口;目的IP是192.168.3.249,端口号是1935,这个是乌班图中rtmp服务端的端口。首先向服务端发送一个C0和C1。

我们看到第11行(Handshake S0+S1+S2),服务端在收到握手信息后,会回一个S0+S1+S2。

此时我们可以看到它的源端口号变成了1935,目的端口号变成了51488。

我们再看到14行(Handshake C2),它表示客户端在收到服务端的握手信息后,再回一个C2给服务端。

这样整个RTMP的握手就成功了。

我们再看到16行(connect())。这表示开始建立连接了。

这里我们可以看到它是客户端向服务端建立的连接,因为它的源端口是51488,目的端口是1935。在RTMP Header中的format是0,所以它的message header是11位的,它本身占2位,后面的6位包含Chunk Stream ID为3,消息类型Type ID为0x14,换算成十进制为20,Body Size为142,Stream ID为0。我们再来看看Body

它本身就是connect,带了一个Number,还带了几个Object。

我们再看到17行(Window Acknowledgement Size 5000000),它表示服务端返回给客户端的信息,它的滑动窗口大小为5M。再到19行(Set Peer Bandwidth 5000000,Dynamic|Set Chunk Size 4000|_result()),它表示设置带宽为5M,消息块大小(chunk size,在rtmp中每个消息会被切分成很多的小块)为4000。再看到23行(_result()),它表示连接成功。

然后是27行(publish()),这里表示开始推送了。后面的就是推送视频数据和音频数据了。

FLV协议

FLV可以近似的看成是RTMP,但它们也有不同。上图是FLV与RTMP具体的一些差异,它是整个FLV文件的格式。首先它有一个头FLV header,前三个字符就是FLV,然后是版本号Ver,T表示类型,最后是偏移量Offset(4)。后面的是数据,它的格式是先有数据大小pre tagsize,再有数据tag,依次循环。对于每一个tag来说,它又包括一个tag header和tag data,在tag header中第一个是tag type(TT),表示标签类型,是音频还是视频;接着是数据大小(datasize),然后是时间戳(timestamp),它只有3个字节,E是时间戳的扩展位,如果3个字节不够就有1个字节的扩展。SID表示流stream id。

然后就是tag data,它可以保存两种数据类型,一个是音频,一个是视频。对于音频来说,也分为了header和data,视频也一样。对于音频的header来说,它包括了声音的采样率(SF),采值大小(SR)等,但是这些并不重要,重要的是data,data是我们真正传输的数据。如果我们有一个FLV文件,当我们解析到data之后,就知道可以直接把它通过RTMP协议进行传输,在服务端就可以收到这个数据。谁订阅了这个流,就可以将数据转出去。音频的data就是Audio Data,它也是由两部分组成,第一部分是aactype,包括了1个字节的类型,是原始数据还是包含了adts头的数据;第二部分是AAC DATA,它又包括了两个部分,第一部分是配置信息(AudioSpecificConfig),包括采样率大小、通道数;第二部分就是数据了,它有一个ADTS头,分为7字节或9字节,后面是真正的音频数据(aac data)。

视频的header中类型(FT(4b))和编码器的id(CodecID(4b)),每个占4个字节。对于视频data(Video DATA)来说,它包含了头和包,头部包含了类型(PT)和时间戳(timestamp),数据包(AVCVideoPacket)包含了两种类型——sps和pps,这是两种特殊帧。NAL是真正的数据。

FLV协议分析器

由于不同的平台有不同的FLV协议分析工具,我这里是mac,可以使用的是Diffindo,下载地址:https://github.com/xiaoningcn/Diffindo

解压缩后,进入文件夹

cd Diffindo-master/gcc

执行

./flv_compile.sh

此时会产生一个新的文件夹gcc_flv,进入该文件夹

cd gcc_flv/

执行

./FLVParser ../1.flv 2

此时会产生一个名字为2的文件,打印该文件内容

cat 2

得到以下内容(部分)

***********************************
File Name: ../1.flv
File Size: 7836546
***********************************
FLV Version: 1
Video Flag: 1
Audio Flag: 1
Data offset: 9
-----------------------------------
Previous Tag Size: 0
Tag Index: 0
Tag Type: Script
Data Size: 850
TimeStamp: 0
TimeStampExtension: 0
StreamID: 0
-----------------------------------
Previous Tag Size: 861
Tag Index: 1
Tag Type: Video
Data Size: 50
TimeStamp: 0
TimeStampExtension: 0
StreamID: 0
-----------------------------------
Previous Tag Size: 61
Tag Index: 2
Tag Type: Audio
Data Size: 4
TimeStamp: 0
TimeStampExtension: 0
StreamID: 0
-----------------------------------
Previous Tag Size: 15
Tag Index: 3
Tag Type: Video
Data Size: 28526
TimeStamp: 0
TimeStampExtension: 0
StreamID: 0
-----------------------------------
Previous Tag Size: 28537
Tag Index: 4
Tag Type: Audio
Data Size: 10
TimeStamp: 37
TimeStampExtension: 0
StreamID: 0
-----------------------------------
Previous Tag Size: 21
Tag Index: 5
Tag Type: Video
Data Size: 385
TimeStamp: 41
TimeStampExtension: 0
StreamID: 0
-----------------------------------
Previous Tag Size: 396
Tag Index: 6
Tag Type: Audio
Data Size: 10
TimeStamp: 60
TimeStampExtension: 0
StreamID: 0
-----------------------------------
Previous Tag Size: 21
Tag Index: 7
Tag Type: Video
Data Size: 92
TimeStamp: 83
TimeStampExtension: 0
StreamID: 0
-----------------------------------
Previous Tag Size: 103
Tag Index: 8
Tag Type: Audio
Data Size: 10
TimeStamp: 83
TimeStampExtension: 0
StreamID: 0
-----------------------------------
Previous Tag Size: 21
Tag Index: 9
Tag Type: Audio
Data Size: 27
TimeStamp: 106
TimeStampExtension: 0
StreamID: 0
-----------------------------------
Previous Tag Size: 38
Tag Index: 10
Tag Type: Video
Data Size: 97
TimeStamp: 125
TimeStampExtension: 0
StreamID: 0
-----------------------------------

在输出的文件中,我们可以看见每一块的信息,首先第一块是头信息,包括版本号FLV Version: 1,类型有音频和视频Video Flag: 1 Audio Flag: 1,偏移量Data offset: 9;后面的是数据,有前一块的数据大小Previous Tag Size: 0,然后是tag header中标签类型Tag Type,如Tag Type: Video,数据大小Data Size: 50,时间戳TimeStamp: 0,时间戳扩展位TimeStampExtension: 0,流id,StreamID: 0。

librtmp推流程序开发

  • 安装librtmp
sudo apt-get install libssl-dev
sudo apt-get install zlib1g-dev
sudo apt install librtmp-dev
  • 代码

pufo.cpp

#include "librtmp/rtmp.h"
#include <stdio.h>
#include <stdlib.h>
/*
 flv文件 头部有9个字节,
 第一个字节是字母F,
 第二个字节是L,
 第三个字节是V,
 第四个字节是版本号,
 第五个字节 1-5位保留,必须是0,第6位是否有音频tag 第7位保留必须是0,第8位是否有视频tag
 第六到九字节代表header的大小,必须是9
 */
static int status = 1;
void set_status(int state){
    status = state;
}
 
static FILE* open_flv(char* flvaddr){
    FILE* fp = NULL;
    fp = fopen(flvaddr, "rb");
    if(!fp){
        printf("failed to open flv:%s", flvaddr);
        return NULL;
    }
    fseek(fp, 9, SEEK_SET); //跳过flv头 9个字节
    fseek(fp, 4, SEEK_CUR); //跳过pretagsize 4个字节
    return fp;
 
}
 
static RTMP* connect_rtmp_server(char* rtmp_addr){
    RTMP* rtmp = NULL;
    rtmp = RTMP_Alloc();
    if(!rtmp){
        printf("NO Memory");
        goto __ERROR;
    }
    RTMP_Init(rtmp);
    //设置rtmp的超时时间和rtmp的连接地址
    rtmp->Link.timeout = 10;
    RTMP_SetupURL(rtmp, rtmp_addr);
    //设置推流还是拉流,设置开启是推流,不设置是拉流
    RTMP_EnableWrite(rtmp);
 
    //建立链接
    if(!RTMP_Connect(rtmp, NULL)){
        printf("failed to connect");
        goto __ERROR;
    }
    //创建流
    RTMP_ConnectStream(rtmp, 0);
    return rtmp;
__ERROR:
    if(rtmp){
        RTMP_Close(rtmp);
        RTMP_Free(rtmp);
    }
    
    return NULL;
}
static RTMPPacket *alloc_packet(){
    RTMPPacket *pack = NULL;
    pack = (RTMPPacket*)malloc(sizeof(RTMPPacket));
    if(!pack){
        printf("NO Memory alloc_packet");
        return NULL;
    }
    RTMPPacket_Alloc(pack, 64 * 1024);
    RTMPPacket_Reset(pack);
    pack->m_hasAbsTimestamp = 0;
    pack->m_nChannel = 0x4;
    
    return pack;
}
 
static int read_u8(FILE* fp, unsigned int *u8){
    unsigned int tmp;
    if(fread(&tmp, 1, 1, fp) != 1){
        printf("Failed to read_u8!\n");
        return -1;
    }
    
    *u8 = tmp & 0xFF;
    return 0;
}
 
 
 
static int read_u24(FILE* fp, unsigned int *u24){
    unsigned int tmp;
    if(fread(&tmp, 1, 3, fp) != 3){
        printf("Failed to read_u24!\n");
        return -1;
    }
    *u24 = ((tmp >> 16) & 0xFF)| ((tmp << 16) & 0xFF0000) | (tmp &0xFF00);
    return 0;
}
 
 
static int read_u32(FILE* fp, unsigned int *u32){
    unsigned int tmp;
    if(fread(&tmp, 1, 4, fp) != 4){
        printf("Failed to read_u32!\n");
        return -1;
    }
    *u32 = ((tmp >> 24) & 0xFF) ||  ((tmp >> 8) & 0xFF00)| ((tmp << 8) & 0xFF0000) | ((tmp << 24)&0xFF000000);
    return 0;
}
 
static int read_ts(FILE *fp, unsigned int *ts){
    unsigned int tmp;
    if(fread(&tmp, 1, 4, fp) !=4) {
        printf("Failed to read_ts!\n");
        return -1;
    }
    
    *ts = ((tmp >> 16) & 0xFF) | ((tmp << 16) & 0xFF0000) | (tmp & 0xFF00) | (tmp & 0xFF000000);
    
    return 0;
}
 
int  read_data(FILE* fp, RTMPPacket **pack){
    /*
        * tag header
        * 第一个字节 TT(Tag Type), 0x08 音频,0x09 视频, 0x12 script
        * 2-4, Tag body 的长度, PreTagSize - Tag Header size
        * 5-7, 时间戳,单位是毫秒; script 它的时间戳是0
        * 第8个字节,扩展时间戳。真正时间戳结格 [扩展,时间戳] 一共是4字节。
        * 9-11, streamID, 0
        */
    unsigned int tt;
    unsigned int tag_data_size;
    unsigned int ts;
    unsigned int streamId;
    unsigned int tag_pre_size;
 
    int ret = -1;
    size_t data_size = 0;
    if(read_u8(fp, &tt)){
        goto __ERROR;
    }
    
    if(read_u24(fp, &tag_data_size)){
        goto __ERROR;
    }
    
    if(read_ts(fp, &ts)){
        goto __ERROR;
    }
    
    if(read_u24(fp, &streamId)){
        goto __ERROR;
    }
    data_size = fread((*pack)->m_body, 1, tag_data_size, fp);
    if(tag_data_size != data_size){
        printf("read data size error tag_data_size = %d, real data size = %d\n", tag_data_size, data_size);
        goto __ERROR;
    }
    (*pack)->m_headerType = RTMP_PACKET_SIZE_LARGE;
    (*pack)->m_nTimeStamp = ts;
    (*pack)->m_packetType = tt;
    (*pack)->m_nBodySize = tag_data_size;
    read_u32(fp, &tag_pre_size);
    ret = 0;
__ERROR:
    return ret;
    
}
 
static void send_data(FILE* fp, RTMP *rtmp){
    //1、创建RTMPPacket对象
    RTMPPacket *packet = NULL;
    unsigned int pre_ts = 0;
    packet = alloc_packet();
    packet->m_nInfoField2 = rtmp->m_stream_id;
 
    while (1) {
        //从flv读取文件
        //2.从flv文件中读取数据
        if(read_data(fp, &packet)){
            printf("over!\n");
            break;
        }
        //判断rtmp是否还处在链接状态
        if(!RTMP_IsConnected(rtmp)){
            printf("Disconnect....\n");
            break;
        }
 
        unsigned int diff = packet->m_nTimeStamp - pre_ts;
        //usleep(diff * 1000);
        //发送数据
        RTMP_SendPacket(rtmp, packet, 0);
        pre_ts = packet->m_nTimeStamp;
    }
}
 
int main(){
    char* rtmp_addr = "rtmp://localhost/live/room";
    char* flv = "./1.flv";
    //读flv文件
    FILE* fp = open_flv(flv);
    
    //链接RTMP服务器
    RTMP* rtmp = connect_rtmp_server(rtmp_addr);
    
    //推流音视频数据
    send_data(fp, rtmp);
    return 0;
}

Makefile

EXE=pufo

PKGS=librtmp
CFLAGS= `pkg-config --cflags $(PKGS)`
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)

ffmpeg推流开发

ffflow.cpp

#include <stdio.h>
#define __STDC_CONSTANT_MACROS
// Linux...
#ifdef __cplusplus
extern "C" {
#endif
    #include <libavutil/avutil.h>
    #include <libavutil/time.h>
    #include <libavformat/avformat.h>
    #include <libavcodec/avcodec.h>
#ifdef __cplusplus
};
#endif

static double r2d(AVRational r) {
    return r.num == 0 || r.den == 0 ? 0. : (double)r.num / (double)r.den;
}

int main(int argc, char *argv[]) {
    int ret;
    char errStr[256] = {0};
    //输入文件上下文
    AVFormatContext *iCtx = NULL;
    //输出流上下文
    AVFormatContext *oCtx = NULL;
    AVPacket pkt;
    char* in_url = "./1.flv";
    char* out_url = "rtmp://localhost/live/room";
    long long startTime;
    //初始化所有的封装和解封装 flv mp4 mov
    av_register_all();
    //初始化网络库
    avformat_network_init();
    //打开多媒体文件
    ret = avformat_open_input(&iCtx, in_url, 0, 0);
    if (ret < 0) {
        av_strerror(ret, errStr, sizeof(errStr));
        av_log(NULL, AV_LOG_ERROR, "%s\n", errStr);
        goto _ERROR;
    }
    //获取音视频流信息 .h264 flv
    ret = avformat_find_stream_info(iCtx, 0);
    if (ret < 0) {
        av_strerror(ret, errStr, sizeof(errStr));
        av_log(iCtx, AV_LOG_ERROR, "%s\n", errStr);
        goto _ERROR;
    }
    //打印输入流信息
    av_dump_format(iCtx, 0, in_url, 0);
    //打开输出流上下文
    ret = avformat_alloc_output_context2(&oCtx, 0, "flv", out_url);
    if (!oCtx) {
        av_strerror(ret, errStr, sizeof(errStr));
        av_log(NULL, AV_LOG_ERROR, "%s\n", errStr);
        goto _ERROR;
    }
    for(int i = 0; i < iCtx->nb_streams; i++) {
        //创建输出流(这里包含了音频流和视频流)
        AVStream *out = avformat_new_stream(oCtx, iCtx->streams[i]->codec->codec);
        if (!out) {
            av_log(oCtx, AV_LOG_ERROR, "Can't create output stream!");
            goto _ERROR;
        }
        //将输入文件流的编解码器复制到输出流中
        ret = avcodec_parameters_copy(out->codecpar, iCtx->streams[i]->codecpar);
        out->codec->codec_tag = 0;
    }
    //打印输出流信息
    av_dump_format(oCtx, 0, out_url, 1);
    //将输出流上下文与rtmp输出流绑定
    ret = avio_open(&oCtx->pb, out_url, AVIO_FLAG_WRITE);
    if (ret < 0) {
        av_strerror(ret, errStr, sizeof(errStr));
        av_log(oCtx, AV_LOG_ERROR, "%s\n", errStr);
        goto _ERROR;
    }
    //写入rtmp流头信息
    ret = avformat_write_header(oCtx, 0);
    if (ret < 0) {
        av_strerror(ret, errStr, sizeof(errStr));
        av_log(oCtx, AV_LOG_ERROR, "%s\n", errStr);
        goto _ERROR;
    }
    //推流起始真实时间
    startTime = av_gettime();
    //推流每一帧数据(从输入文件中读到的包数据放入输出流中)
    while(av_read_frame(iCtx, &pkt) >= 0) {
        //改变包的时间戳,时间戳包括pts和dts
        pkt.pts = av_rescale_q_rnd(pkt.pts, iCtx->streams[pkt.stream_index]->time_base, oCtx->streams[pkt.stream_index]->time_base, static_cast<AVRounding>(AV_ROUND_NEAR_INF | AV_ROUND_PASS_MINMAX));
        pkt.dts = av_rescale_q_rnd(pkt.dts, iCtx->streams[pkt.stream_index]->time_base, oCtx->streams[pkt.stream_index]->time_base, static_cast<AVRounding>(AV_ROUND_NEAR_INF | AV_ROUND_PASS_MINMAX));
        //设置包的期间
        pkt.duration = av_rescale_q_rnd(pkt.duration, iCtx->streams[pkt.stream_index]->time_base, oCtx->streams[pkt.stream_index]->time_base, static_cast<AVRounding>(AV_ROUND_NEAR_INF | AV_ROUND_PASS_MINMAX));
        //设置包相对位置
        pkt.pos = -1;
        //如果是视频流(防止过快写入无法播放)
        if (iCtx->streams[pkt.stream_index]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {
            //获取该视频流的时间基
            AVRational tb = iCtx->streams[pkt.stream_index]->time_base;
            //获取已经过去的真实时间
            long long now = av_gettime() - startTime;
            //获取在时间戳中应该过去的时间
            long long dts = pkt.dts * (1000 * 1000 * r2d(tb));
            //如果时间戳中的时间还没到,等待
            if (dts > now) {
                av_usleep(dts - now);
            }
            //统一再等待30毫秒
            av_usleep(30 * 1000);
        }
        //将包数据写入到输出流中
        av_interleaved_write_frame(oCtx, &pkt);
        //写入后释放当前包
        av_packet_unref(&pkt);
    }
_ERROR:
    if (iCtx) {
        avformat_close_input(&iCtx);
        iCtx = NULL;
    }
    if (oCtx->pb) {
        avio_close(oCtx->pb);
    }
    if (oCtx) {
        avformat_free_context(oCtx);
        oCtx = NULL;
    }
    return 0;
}

Makefile

EXE=ffflow

INCLUDE=/usr/local/ffmpeg/include/
LIBPATH=/usr/local/ffmpeg/lib/
CFLAGS= -I$(INCLUDE)
LIBS= -lavformat -lavutil -lavcodec
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)

运行结果

Input #0, flv, from './1.flv':
  Metadata:
    creator         : Coded by New Bilibili Transcoder v1.2
    metadatacreator : Yet Another Metadata Injector for FLV - Version 1.9
    hasKeyframes    : true
    hasVideo        : true
    hasAudio        : true
    hasMetadata     : true
    canSeekToEnd    : true
    datasize        : 7835668
    videosize       : 7064970
    audiosize       : 761146
    lasttimestamp   : 35
    lastkeyframetimestamp: 35
    lastkeyframelocation: 7836526
  Duration: 00:00:35.33, start: 0.037000, bitrate: 1774 kb/s
    Stream #0:0: Video: h264 (High), yuv420p(tv, bt709/unknown/unknown, progressive), 1920x1080, 1597 kb/s, 24.09 fps, 24 tbr, 1k tbn, 48 tbc
    Stream #0:1: Audio: aac (LC), 44100 Hz, stereo, fltp, 166 kb/s
Output #0, flv, to 'rtmp://localhost/live/room':
    Stream #0:0: Video: h264 (High), yuv420p(tv, bt709/unknown/unknown, progressive), 1920x1080, q=2-31, 1597 kb/s
    Stream #0:1: Audio: aac (LC), 44100 Hz, stereo, fltp, 166 kb/s

ffmpeg整合opencv

  • rtsp流的接收和rtmp流的推送

这里我们需要知道的是,opencv内部是集成了ffmpeg,所以opencv是可以直接使用rtsp流的。

rtsp2rtmp.cpp

#include <stdio.h>
#define __STDC_CONSTANT_MACROS
#ifdef __cplusplus
extern "C" {
#endif
    #include <libavutil/avutil.h>
    #include <libavutil/time.h>
    #include <libavformat/avformat.h>
    #include <libavcodec/avcodec.h>
    #include <libswscale/swscale.h>
#ifdef __cplusplus
};
#endif
#include "opencv2/highgui/highgui.hpp"
#include "opencv2/imgproc/imgproc.hpp"
#include <iostream>
#include <string>
#include <list>
#include <tuple>
#include <thread>
#include <mutex>
#include <condition_variable>

using namespace cv;
using namespace std;

static list<tuple<int,Mat>> msgs;
static mutex mux;
static condition_variable cva;

static void SendMsg(tuple<int,Mat> msg) {
    unique_lock<mutex> lock(mux);
    msgs.push_back(msg);
    lock.unlock();
    cva.notify_one();
}

static void push_stream(AVFormatContext *oCtx, struct SwsContext *swsCtx, AVFrame *frame, AVCodecContext *codecCtx, AVPacket *pkt, AVStream *stream) {
    int ret;
begin:
    while(1) {
        unique_lock<mutex> lock(mux);
        cva.wait(lock);
        while(!msgs.empty()) {
            tuple<int,Mat> msg = msgs.front();
            int vpts = get<0>(msg);
            Mat mat = get<1>(msg);
            msgs.pop_front();
            uint8_t *indata[AV_NUM_DATA_POINTERS] = {0};
            indata[0] = mat.data;
            int insize[AV_NUM_DATA_POINTERS] = {0};
            insize[0] = mat.cols * mat.elemSize();
            int h = sws_scale(swsCtx, indata, insize, 0, mat.rows, frame->data, frame->linesize);
            if (h < 0) {
                goto begin;
            }
            frame->pts = vpts - 1;
            ret = avcodec_send_frame(codecCtx, frame);
            if (ret < 0) {
                goto begin;
            }
            ret = avcodec_receive_packet(codecCtx, pkt);
            if (ret < 0) {
                goto begin;
            }
            pkt->pts = av_rescale_q(pkt->pts, codecCtx->time_base, stream->time_base);
            pkt->dts = av_rescale_q(pkt->dts, codecCtx->time_base, stream->time_base);
            av_interleaved_write_frame(oCtx, pkt);
            av_packet_unref(pkt);
        }
    }
}

int main() {
    VideoCapture cap;
    string inUrl = "rtsp://admin:1234qwer@192.168.3.166:554/Streaming/Channels/1";
    char* outUrl = "rtmp://192.168.3.249/live/room";
    //注册所有的编解码器
    avcodec_register_all();
    //初始化所有的封装和解封装
    av_register_all();
    //初始化网络库
    avformat_network_init();
    namedWindow("video");
    if (cap.open(inUrl)) {
        cout << "open capture success!" << endl;
    } else {
        cout << "open capture failed!" << endl;
    }
    int ret;
    char errStr[256] = {0};
    int vpts = 0;  //视频帧的时间戳
    long long startTime;
    AVFrame *yuv_frame = NULL; //格式转换后输出流的视频帧
    AVCodec *codec = NULL;  //编码器
    AVCodecContext *codecCtx = NULL;  //编码器上下文
    AVFormatContext *oCtx = NULL; //输出流上下文
    AVStream *outStream = NULL;  //输出视频流
    AVPacket *pkt = NULL; //包
    thread th;
    int width = cap.get(CAP_PROP_FRAME_WIDTH);
    int height = cap.get(CAP_PROP_FRAME_HEIGHT);
    int fps = cap.get(CAP_PROP_FPS);
    //图像像素格式转换上下文,这里是从opencv的BRG转换成YUV
    struct SwsContext *swsCtx = sws_getCachedContext(NULL, width, height, AV_PIX_FMT_BGR24, width, height, 
                    AV_PIX_FMT_YUV420P, SWS_BICUBIC, 0, 0, 0);
    if (!swsCtx) {
        av_log(NULL, AV_LOG_ERROR, "swsCtx created failed\n");
        goto _ERROR;
    }
    //创建视频帧并初始化视频帧的设置
    yuv_frame = av_frame_alloc();
    yuv_frame->format = AV_PIX_FMT_YUV420P;
    yuv_frame->width = width;
    yuv_frame->height = height;
    yuv_frame->pts = 0;
    //为帧的data域分配一个buffer
    ret = av_frame_get_buffer(yuv_frame, 32);
    if (ret < 0) {
        av_strerror(ret, errStr, sizeof(errStr));
        av_log(NULL, AV_LOG_ERROR, "%s\n", errStr);
        goto _ERROR;
    }
    //查找H264编码器
    codec = avcodec_find_encoder(AV_CODEC_ID_H264);
    if (!codec) {
        av_log(NULL, AV_LOG_ERROR, "don't find Codec H264\n");
        goto _ERROR;
    }
    //通过编码器创建编码器上下文
    codecCtx = avcodec_alloc_context3(codec);
    if (!codecCtx) {
        av_log(NULL, AV_LOG_ERROR, "NO MEMRORY\n");
        goto _ERROR;
    }
    //配置编码器上下文参数
    codecCtx->flags |= AV_CODEC_FLAG_GLOBAL_HEADER;  //全局设置
    codecCtx->codec_id = codec->id;  
    codecCtx->thread_count = 4;
    codecCtx->bit_rate = 50 * 1024 * 8; //码率,通常码率越高越清晰,但是有限额,当达到一定限额设置再高也无用
    codecCtx->width = width; //宽
    codecCtx->height = height; //高
    codecCtx->time_base = (AVRational){1,fps};   //时间基
    codecCtx->framerate = (AVRational){fps,1};  //帧率
    codecCtx->gop_size = 50;  //相似帧数量
    codecCtx->max_b_frames = 0;  //b帧最大数量
    codecCtx->pix_fmt = AV_PIX_FMT_YUV420P;  //视频源格式,这里为YUV格式
    //编码器与编码器上下文绑定
    ret = avcodec_open2(codecCtx, codec, NULL);
    if (ret < 0) {
        av_strerror(ret, errStr, sizeof(errStr));
        av_log(codecCtx, AV_LOG_ERROR, "don't open codec: %s\n", errStr);
        goto _ERROR;
    }
    //打开输出流上下文
    ret = avformat_alloc_output_context2(&oCtx, 0, "flv", outUrl);
    if (ret < 0) {
        av_strerror(ret, errStr, sizeof(errStr));
        av_log(NULL, AV_LOG_ERROR, "%s\n", errStr);
        goto _ERROR;
    }
    //创建输出视频流
    outStream = avformat_new_stream(oCtx, NULL);
    outStream->codecpar->codec_tag = 0;
    //从编码器复制参数
    avcodec_parameters_from_context(outStream->codecpar, codecCtx);
    //打印输出流信息a
    av_dump_format(oCtx, 0, outUrl, 1);
    //将输出流上下文与rtmp输出流绑定
    ret = avio_open(&oCtx->pb, outUrl, AVIO_FLAG_WRITE);
    if (ret < 0) {
        av_strerror(ret, errStr, sizeof(errStr));
        av_log(oCtx, AV_LOG_ERROR, "%s\n", errStr);
        goto _ERROR;
    }
    //写入rtmp流头信息
    ret = avformat_write_header(oCtx, 0);
    if (ret < 0) {
        av_strerror(ret, errStr, sizeof(errStr));
        av_log(oCtx, AV_LOG_ERROR, "%s\n", errStr);
        goto _ERROR;
    }
    //创建包
    pkt = av_packet_alloc();
    if (!pkt) {
        av_log(NULL, AV_LOG_ERROR, "NO MEMORY\n");
        goto _ERROR;
    }
    
    th = thread(push_stream, oCtx, swsCtx, yuv_frame, codecCtx, pkt, outStream);
    th.detach();
    startTime = av_gettime();
    while(cap.isOpened()) {
        vpts++;
        Mat frame, out_frame;
        cap >> frame;
        out_frame = frame.clone();
        Point p1(800, 230);
        Point p2(1000, 750);
        rectangle(out_frame, p1, p2, Scalar(0, 255, 255), 3);
        long long now = av_gettime() - startTime;
        if (vpts > now * fps / 1000000) {
            av_usleep(30 * 1000);
            cout << "111" << endl;
            try {
                imshow("video", frame);
            } catch (...) {
                continue;
            }
        } else {
            cout << "222" << endl;
            try {
                imshow("video", frame);
            } catch (...) {
                continue;
            }
        ]
        int key = waitKey(1);
        if (key == 113) {
            break;
        }
        tuple<int,Mat> msg(vpts, out_frame);
        thread thw(SendMsg, msg);
        thw.detach();

        /*uint8_t *indata[AV_NUM_DATA_POINTERS] = {0};
        indata[0] = out_frame.data;
        int insize[AV_NUM_DATA_POINTERS] = {0};
        insize[0] = out_frame.cols * out_frame.elemSize();
        //进行图像格式转换
        //第一个参数,图像格式转换上下文
        //第二个参数,需要转换的图像的全部数据(扁平化的)
        //第三个参数,需要转换的图像的列数(宽)
        //第五个参数,需要转换的图像的行数(高)
        //第六个参数,转换后的帧的data域
        //第七个参数,转换后的帧的列数(宽)
        int h = sws_scale(swsCtx, indata, insize, 0, out_frame.rows, yuv_frame->data, yuv_frame->linesize);
        if (h < 0) {
            continue;
        }
        //设置当前帧的时间戳
        yuv_frame->pts = vpts - 1;
        //将当前帧送入编码器进行H264编码
        ret = avcodec_send_frame(codecCtx, yuv_frame);
        if (ret < 0) {
            continue;    
        }
        //将编码后的数据保存在包里
        ret = avcodec_receive_packet(codecCtx, pkt);
        if (ret != 0 || pkt->size > 0) {
            //cout << "*" << flush;
        } else {
            continue;
        }
        //修改包的pts,dts时间戳
        pkt->pts = av_rescale_q(pkt->pts, codecCtx->time_base, outStream->time_base);
        pkt->dts = av_rescale_q(pkt->dts, codecCtx->time_base, outStream->time_base);
        //将包数据写入到输出流中
        ret = av_interleaved_write_frame(oCtx, pkt);
        if (ret >= 0) {
            cout << "#" << flush;
        }
        //写入后释放当前包
        av_packet_unref(pkt);*/
    }
_ERROR:
    if (swsCtx) {
        sws_freeContext(swsCtx);
        swsCtx = NULL;
    }
    if (codecCtx) {
        avcodec_free_context(&codecCtx);
    }
    if (oCtx->pb) {
        avio_close(oCtx->pb);
    }
    if (oCtx) {
        avformat_free_context(oCtx);
        oCtx = NULL;
    }
    cap.release();
    destroyAllWindows();
    return 0;
}

Makefile

EXE=rtsp2rtmp

PKGS=opencv4
INCLUDE=/usr/local/ffmpeg/include/
LIBPATH=/usr/local/ffmpeg/lib/
CFLAGS= -I$(INCLUDE)
CFLAGS+= `pkg-config --cflags $(PKGS)`
LIBS= -lavformat -lavutil -lavcodec -lswscale -lpthread
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)

RTSP协议

RTSP,RFC2326,实时流传输协议。在体系结构上位于RTP和RTCP之上,它使用TCP或UDP完成数据传输。使用RTSP时,客户机和服务器都可以发出请求,即RTSP可以是双向的。

实时流媒体会话协议:

  1. SDP(会话描述协议)Session Description Protocol
  2. RTP(实时传输协议)RealTime Protocol

RTSP协议是在TCP/IP协议之上的一个应用层协议,该协议定义了一对多应用程序如何有效地通过IP网络传送多媒体文件。

RTSP是基于文本的协议,采用ISO10646字符集,使用UTF-8编码方案。

RTSP建立并控制一个或几个时间同步的连续流媒体。尽管连续流媒体与控制流媒体交换是可能的,通常它本身并不发送连续流。RTSP充当多媒体服务器的网络远程控制。

RTSP连接没有绑定到传输层连接,如TCP。在RTSP连接期间,RTSP用户可打开或关闭多个对服务器的可传输连接以发出RTSP请求。可使用无连接传输协议,如UDP。RTSP流控制的流可能用到RTP,但RTSP操作并不依赖用于携带连续媒体的传输机制。

  • 交互流程

这里第一步是客户端先向服务端请求OPTIONS,询问服务器支持哪些接口,然后服务端响应告之客户端可支持的接口——如DESCRIBE、SET UP、PLAY等等;第二步是客户端请求DESCRIBE,表示请求某一个路流,需要服务器描述其基本信息,服务器会进行响应;第三步是客户端请求SET UP,告诉服务器创建连接通道,准备建立连接,服务器会进行响应告知已经准备好了连接通道;第四步就是客户端请求PLAY,然后服务端开始播放;然后就是RTP和RTCP音视频数据的传输。结束之后会发送一个TEARDOWN,比如播放器点击了暂停按钮,客户端会发送一个这样的指令,之后服务端就不再给该客户端推流。

  • Wireshark 抓包分析

这里我们使用VLC来播放网络摄像头为例来进行抓包

首先可以看到第4行的OPTIONS,然后看到第6行服务端的回复

服务端回复了一个200 OK,可以支持的接口在Public中全部显示了出来。

然后是第8行的DESCRIBE,第9行中服务端的回复如下,描述为网络摄像头(IP Camera(L3062))。

然后是第14行的SET UP,15行服务端的回复,表示通道创建成功,状态为可播放

第20行的PLAY,22行服务端的回复

  • 协议格式

RTSP中所有操作都是通过服务器和客户端的消息应答机制完成的。消息包括请求和应答两种,RTSP是对称的协议。请求消息由请求行,标题行中的各种标题域和主体实体组成,格式如下所示

上图中的方法就是之前说的OPTIONS、DESCRIBE、SET UP、PLAY等。URL就是类似于rtsp://admin:1234qwer@192.168.3.166:554/Streaming/Channels/1这种,版本一般都是RTSP/1.0,换行都是\r\n。首部字段名和值就是一种Key:Value的组合。一般的请求消息中是没有实体主体(body)的。

应答消息的格式如下

这里的版本也是RTSP/1.0,状态码就比如说请求成功,返回200,短语就比如说请求成功返回OK。

  • 消息内容

这里重点说一下SET UP消息,SET UP请求的作用是指明媒体流该以何种方式传输,每个流PLAY之前必须执行SET UP操作。发送SET UP请求时,客户端会指定两个端口,一个端口(偶数端口)用于接收RTP数据,另一个端口(奇数端口)接收RTCP数据,比如32010-32011。

上图中,TRANSPORT表明媒体流的传输方式,具体包含传输协议,如RTP/UDP。CSeq数据包请求序列号。

  • RTP/RTCP协议

RTP(实时传输协议)是一种传输层协议,RTP协议和RTCP(RTP控制协议)一起使用,是建立在UDP协议上的。它是以固定的数据率在网络上发送数据,客户端也是按照这种速度观看影视文件,当影视画面播放过后,不可以重复播放,除非重定向服务端要求数据。RTCP为实时传输控制协议,负责管理传输质量在当前应用进程之间交换控制信息。

在RTP会话期间,各参与者周期性地传送RTCP包,包中含有已发送的数据包的数量、丢失的数据包的数量等统计资料,因此服务器可以利用这些信息动态地改变传输速率,甚至改变有效载荷类型。RTP和RTCP配合使用,能以有效的反馈和最小的开销传输效率最佳化,故特别适合传送网上的实时数据。

  • RTP/RTCP工作机制

当应用程序开始一个rtp会话时将使用两个端口,一个给rtp,一个给rtcp。rtp本身并不能为按顺序传送数据包提供可靠的传送机制,也不能提供流量控制或拥塞控制,它靠rtcp提供这些服务。

在RTCP通信控制中,RTCP协议的功能是通过不同的RTCP数据报来实现的,主要有如下几种类型:

  1. SR:发送端报告,所谓发送端是指发出RTP数据报的应用程序或者终端,发送端同时也可以是接收端;
  2. RR:接收端报告,所谓接收端是指仅接收但不发送RTP数据报的应用程序或者终端;
  3. SDES:源描述,主要功能是作为会话成员有关标识信息的载体,如用户名、邮件地址、电话号码等,此外还具有向会话成员传达会话控制信息的功能;
  4. BYE:通知离开,主要功能是指示某一个或者几个源不再有效,即通知会话中的其他成员自己将退出会话;
  5. APP:由应用程序自己定义,解决了RTCP的扩展性问题,并且为协议的实现者提供了很大的灵活性。

RTSP与RTP最大的区别在于:RTSP是一种双向实时数据传输协议,它允许客户端向服务器发送请求,如回放、快进、倒退等操作。当然RTSP可基于RTP来传送数据,还可以选择TCP、UDP、组播UDP等通道来发送数据,具有很好的扩展性。

RTP的数据报文

RTP同样分成两块,消息头header和消息体body。在上图中,payload之上都是header,payload就是body。在header中V是版本号,2位标识RTP版本,P是填充标识,1位,X是扩展,1位;CC是CSRC计数,4位;M是标记,1位;PT是载荷类型,记录后面使用哪种编码器Codec,接收端找出相应的解码器decoder;sequence number是序列号,16位,随每个RTP包而增加1,由接收端来探测包损失;timestamp是时间戳,32位,反映RTP包中第一个八进制数的采样时刻,采样时刻必须从单调、线性增加的时钟导出,以允许同步和抖动计算,可以让接收端知道在正确的时间将资料播放出来;synchronization source(SSRC)是同步源,此标识不是随机选择的,目的在于使用同一RTP包连接中没有两个同步源有相同的SSRC标识。尽管多个源选择同一个标识的概率很低,所有RTP实现都必须探测并解决冲突。如源改变地址,也必须选择一个新的SSRC标识以避免插入成环形源;contributing source(CSRC)是负载,CSRC列表表示包内的对载荷起作用的源。标识数量由CC段给出。如超出15个作用源,也仅标识15个。CSRC标识由混合器插入,采用作用源的SSRC标识。

搭建rtsp服务器

我们可以把rtsp服务器直接放在开发板中,这样就可以直接建立端到端的连接了

wget https://github.com/bluenviron/mediamtx/releases/download/v0.23.7/mediamtx_v0.23.7_linux_arm64v8.tar.gz
tar -xzvf mediamtx_v0.23.7_linux_arm64v8.tar.gz
./mediamtx

rtsp推流代码

#include <opencv2/highgui.hpp>
#include <stdexcept>
#include <iostream>
extern "C"
{
    #include <libavutil/avutil.h>
    #include <libswscale/swscale.h>
    #include <libavcodec/avcodec.h>
    #include <libavformat/avformat.h>
}
using namespace std;
using namespace cv;
int main(int argc, char *argv[])
{
    VideoCapture cap;
    string inUrl = "rtsp://192.168.3.101:554/stream/av0_0";
    // nginx-rtmp 直播服务器rtmp退流URL
    char *outUrl = "rtsp://192.168.3.142:8554/test";
 
    // 注册所有的解编码器
    avcodec_register_all();
    
    //初始化所有的封装和解封装
    av_register_all();
 
    // 注册所有的网络协议
    avformat_network_init();
    
    if (cap.open(inUrl)) {
        cout << "open capture success!" << endl;
    } else {
        cout << "open capture failed!" << endl;
    }
    //格式转换后输出流的视频帧
    AVFrame *yuv = NULL;
    //编码器
    AVCodec *codec = NULL;
    // 编码器上下文
    AVCodecContext *vc = NULL;
 
    //输出流上下文
    AVFormatContext *ic = NULL;
    //输出视频流
    AVStream *vs = NULL;
    //图像像素格式转换上下文,这里是从opencv的BRG转换成YUV
    SwsContext *vsc = NULL;
    try
    { 
        int inWidth = cap.get(CAP_PROP_FRAME_WIDTH);;
        int inHeight = cap.get(CAP_PROP_FRAME_HEIGHT);
        int fps = cap.get(CAP_PROP_FPS);
        /// 2 初始化格式转换上下文
        vsc = sws_getCachedContext(vsc,
                                   inWidth, inHeight, AV_PIX_FMT_BGR24,   // 源宽、高、像素格式
                                   inWidth, inHeight, AV_PIX_FMT_YUV420P, // 目标宽、高、像素格式
                                   SWS_BICUBIC,                           // 尺寸变化使用算法
                                   0, 0, 0);
        if (!vsc)
        {
            throw logic_error("sws_getCachedContext failed!"); // 转换失败
        }
        ////创建视频帧并初始化视频帧的设置
        yuv = av_frame_alloc();
        yuv->format = AV_PIX_FMT_YUV420P;
        yuv->width = inWidth;
        yuv->height = inHeight;
        yuv->pts = 0;
        //为帧的data域分配一个buffer
        int ret = av_frame_get_buffer(yuv, 32);
        if (ret != 0)
        {
            char buf[1024] = {0};
            av_strerror(ret, buf, sizeof(buf) - 1);
            throw logic_error(buf);
        }
 
        /// 4 初始化编码上下文
        // a 找到编码器
        codec = avcodec_find_encoder(AV_CODEC_ID_H264);
        if (!codec)
        {
            throw logic_error("Can`t find h264 encoder!"); // 找不到264编码器
        }
        //通过编码器创建编码器上下文
        vc = avcodec_alloc_context3(codec);
        if (!vc)
        {
            throw logic_error("avcodec_alloc_context3 failed!"); // 创建编码器失败
        }
        //配置编码器上下文参数
        vc->flags |= AV_CODEC_FLAG_GLOBAL_HEADER; // 全局参数
        vc->codec_id = codec->id;
        vc->thread_count = 4;
 
        vc->bit_rate = 50 * 1024 * 8; // 压缩后每秒视频的bit位大小为50kb
        vc->width = inWidth;
        vc->height = inHeight;
        vc->time_base = (AVRational){1, fps};
        vc->framerate = (AVRational){fps, 1};
 
        // 画面组的大小,多少帧一个关键帧
        vc->gop_size = 50;
        vc->max_b_frames = 0;
        vc->pix_fmt = AV_PIX_FMT_YUV420P;
        //编码器与编码器上下文绑定
        ret = avcodec_open2(vc, 0, 0);
        if (ret != 0)
        {
            char buf[1024] = {0};
            av_strerror(ret, buf, sizeof(buf) - 1);
            throw logic_error(buf);
        }
 
        //打开输出流上下文
        ret = avformat_alloc_output_context2(&ic, 0, "rtsp", outUrl);
        if (ret != 0)
        {
            char buf[1024] = {0};
            av_strerror(ret, buf, sizeof(buf) - 1);
            throw logic_error(buf);
        }
        //创建输出视频流
        vs = avformat_new_stream(ic, codec);
        if (!vs)
        {
            throw logic_error("avformat_new_stream failed");
        }
        vs->codecpar->codec_tag = 0;
        // 从编码器复制参数
        avcodec_parameters_from_context(vs->codecpar, vc);
        //打印输出流信息
        av_dump_format(ic, 0, outUrl, 1);
 
        // 写入rtsp流头信息
        ret = avformat_write_header(ic, NULL);
        if (ret != 0)
        {
            cout << "ret:" << ret << endl;
            char buf[1024] = {0};
            av_strerror(ret, buf, sizeof(buf) - 1);
            throw logic_error(buf);
        }
 
        AVPacket pack;
        memset(&pack, 0, sizeof(pack));
        //视频帧的时间戳
        int vpts = 0;
        // for (;;)
        while (cap.isOpened())
        {
            Mat frame;
            cap >> frame;
            // 输入的数据结构
            uint8_t *indata[AV_NUM_DATA_POINTERS] = {0};
            // indata[0] bgrbgrbgr
            // plane indata[0] bbbbb indata[1]ggggg indata[2]rrrrr
            indata[0] = frame.data;
            int insize[AV_NUM_DATA_POINTERS] = {0};
            // 一行(宽)数据的字节数
            insize[0] = frame.cols * frame.elemSize();
            int h = sws_scale(vsc, indata, insize, 0, frame.rows, // 源数据
                              yuv->data, yuv->linesize);
            if (h <= 0)
            {
                continue;
            }
            // cout << h << " " << flush;
            /// h264编码
            yuv->pts = vpts;
            vpts++;
            //将当前帧送入编码器进行H264编码
            ret = avcodec_send_frame(vc, yuv);
            if (ret != 0)
                continue;
            //将编码后的数据保存在包里
            ret = avcodec_receive_packet(vc, &pack);
            if (ret != 0 || pack.size > 0)
            {
                // cout << "*" << pack.size << flush;
            }
            else
            {
                continue;
            }
 
            int firstFrame = 0;
            if (pack.dts < 0 || pack.pts < 0 || pack.dts > pack.pts || firstFrame)
            {
                firstFrame = 0;
                pack.dts = pack.pts = pack.duration = 0;
            }
 
            // 推流
            pack.pts = av_rescale_q(pack.pts, vc->time_base, vs->time_base); // 显示时间
            pack.dts = av_rescale_q(pack.dts, vc->time_base, vs->time_base); // 解码时间
            pack.duration = av_rescale_q(pack.duration, vc->time_base, vs->time_base); // 数据时长
            //将包数据写入到输出流中
            ret = av_interleaved_write_frame(ic, &pack);
            if (ret == 0)
            {
                // cout << "#" << flush;
            }
            av_packet_unref(&pack);
        }
    }
    catch (logic_error &ex)
    {
        if (cap.isOpened()) {
            cap.release();
        }
        if (vsc)
        {
            sws_freeContext(vsc);
            vsc = NULL;
        }
 
        if (vc)
        {
            avio_closep(&ic->pb);
            avcodec_free_context(&vc);
        }
 
        cerr << ex.what() << endl;
    }
    getchar();
    return 0;
}

Makefile

EXE=rtsp

PKGS=opencv4
INCLUDE=/usr/local/ffmpeg/include/
LIBPATH=/usr/local/ffmpeg/lib/
CFLAGS= -I$(INCLUDE)
CFLAGS+= `pkg-config --cflags $(PKGS)`
LIBS= -lpthread -lavformat -lavutil -lavcodec -lswscale
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)

 

展开阅读全文
加载中
点击引领话题📣 发布并加入讨论🔥
打赏
0 评论
1 收藏
0
分享
返回顶部
顶部