基于FFmpeg库和RV1126的视频推流

        这是个音视频的入门项目,实现的功能是:将RV1126采集到的音视频数据通过FFmpeg库以FLV协议复合流的形式推流至RTMP服务器(nginx搭建的)。

        整个项目大致细节如下图所示,具体细节在下文会一一提及:

第一部分:RV1126采集及编码部分

        整个项目的开头从VI模块开始,当然如果有音频就还需要AI,这里先讲视频部分的。我们先介绍一下RV1126的VI模块。

1.1 VI模块:

        OK,可以看到下方是一个VI的初始化代码,步骤就是:1、设置VI模块属性;2、EnableVI模块通道;3、开启VI流。

#include <stdio.h>
#include "stdint.h"
#include "rkmedia_api.h"
#include "rkmedia_venc.h"

#define VI_PIPE_ID              0
#define VI_CHN_ID               0


typedef struct rv1126_vi_devMsg{
    uint8_t chn_id;
    VI_CHN_ATTR_S vi_attr; 

}rv1126_vi_devMsg_t;



int rv1126_vi_init(rv1126_vi_devMsg_t* vi){
    if(!vi){
        return -1;
    }
    vi->vi_attr.enBufType = VI_CHN_BUF_TYPE_MMAP;
    vi->vi_attr.enPixFmt = IMAGE_TYPE_NV12;
    vi->vi_attr.enWorkMode = VI_WORK_MODE_NORMAL;
    vi->vi_attr.pcVideoNode = "rkispp_scale0";
    vi->vi_attr.u32BufCnt = 3;
    vi->vi_attr.u32Width = 1920;
    vi->vi_attr.u32Height = 1080;
   
    int ret = RK_MPI_VI_SetChnAttr(VI_PIPE_ID, vi->chn_id, &(vi->vi_attr));
    if(ret){
        printf("VI set failed!\n");
        return -1;
    }
    printf("VI set success....\n");

    ret = RK_MPI_VI_EnableChn(VI_PIPE_ID, vi->chn_id);
    if(ret){
        printf("VI enable failed!\n");
        return -1;
    }
    printf("VI enable success....\n");

    ret = RK_MPI_VI_StartStream(VI_PIPE_ID, vi->chn_id);
    if(ret){
        printf("VI start failed!\n");
        return -1;
    }
    printf("VI start success....\n");
    return 0;
}

          我们首先看到 VENC_CHN_ATTR_S 这个结构体,它是用来描述我的VI模块(视频输入模块)的器件属性。其中:

        1、Width、Height为当前你的摄像头输入视频分辨率的宽和高

        2、pcVideoNode,视频的输入节点,如下图所示:

        3、enworkMode,VI的工作模式,有VI_WORK_MODE_NORMAL 和 VI_WORK_MODE_LUMA_ONLY两种工作类型,VI_WORK_MODE_LUMA_ONLY模式下,VI 模块只捕捉图像的亮度信息 (luma),而忽略色度信息 (chroma)。所以一般我们没有特殊需要,就使用VI_WORK_MODE_NORMAL即可。

        4、enPixfmt,VI模块YUV存储方式的选择,我们这里选择YUV NV12的Semi-Planar存储方式,即四个pixel的Y(亮度)值对应一组UV(色度)。由于人类对色度不像亮度敏感,因此这样的采集方式是可以保证图片在人看起来没什么差异的情况下还能压缩一部分存储空间的。

有关YUV的采集格式分类和存储形式分类参考:YUV格式详解【全】-CSDN博客

        5、enBufType,这个是VI模块的一个缓冲区类型选项共有两个选项,VI_CHN_BUF_TYPE_DMA 和 VI_CHN_BUF_TYPE_MMAP。后者的好处在于:当使用 MMAP 模式时,内核空间的缓冲区会直接映射到用户空间,应用程序可以直接访问这些内存区域。减少了数据复制的开销,提高了数据传输的效率。所以这里选择用MMAP存储映射的缓冲方式。

        6、u32BufCnt,帧缓冲区数量,这个和帧缓存的概念差不多,他会在这里缓存一些帧内容,防止掉帧等问题。这里数量我们默认选择3。

        经过上述步骤,可以把VI模块初始化成功。

1.2 RGA模块:

        RGA(Raster Graphic Acceleration Unit),这个模块的作用其实就是实现对图像的裁剪、缩放、旋转、格式转换(如NV12转NV21)、图片叠加则需要单独调用librga.so库。

#include <stdio.h>
#include "stdint.h"
#include "rkmedia_api.h"
#include "rkmedia_venc.h"
#define RGA_CHN_ID              0


typedef struct rv1126_rga_devMsg{
    uint8_t chn_id;
    RGA_ATTR_S rga_attr; 

}rv1126_rga_devMsg_t;

int rv1126_rga_init(rv1126_rga_devMsg_t* rga){
    if(!rga){
        return -1;
    }

    rga->rga_attr.stImgIn.imgType = IMAGE_TYPE_NV12;
    rga->rga_attr.stImgIn.u32Width = 1920;
    rga->rga_attr.stImgIn.u32Height = 1080;
    rga->rga_attr.stImgIn.u32HorStride = 1920;
    rga->rga_attr.stImgIn.u32VirStride = 1080;
    rga->rga_attr.stImgIn.u32X = 0;
    rga->rga_attr.stImgIn.u32Y = 0;

    rga->rga_attr.stImgOut.u32Y = 0;
    rga->rga_attr.stImgOut.imgType = IMAGE_TYPE_NV12;
    rga->rga_attr.stImgOut.u32Width = 1280;
    rga->rga_attr.stImgOut.u32Height = 720;
    rga->rga_attr.stImgOut.u32HorStride = 1280;
    rga->rga_attr.stImgOut.u32VirStride = 720;
    rga->rga_attr.stImgOut.u32X = 0;

    rga->rga_attr.bEnBufPool = RK_TRUE;
    rga->rga_attr.u16BufPoolCnt = 3;
    rga->rga_attr.u16Rotaion = 0;

    int ret = RK_MPI_RGA_CreateChn(rga->chn_id, &(rga->rga_attr));
    if(ret){
        printf("rga create failed!\n");
        return -1;
    }
    printf("rga create success....\n"); 
    return 0;
}

        设置 RGA 模块,就需要设置 RGA_ATTR_S 结构体属性:

        1、stImgIn 和 stImgOut:分别对应RGA模块输入视频的各个参数和输出视频的各个参数。每个都有(1)u32X,u32Y,坐标原点;(2)Width 和 Height,当前视频的分辨率宽和高(3)u32HorStride 和 u32VirStride,虚宽 和 虚高,。这里虚宽虚高必须要大于等于Width 和 Height,这个虚宽高如果设定的比Width 和 Height大,目的可能是为了字节对齐或是减少cache miss的情况等。一般虚宽高设置和宽高一致即可(4)imgType,视频存储形式。

        2、u16BufPoolCnt 和 bEnBufPool缓冲区数量及是否使能缓冲区的标志。

        3、Rotation,旋转角度,取值范围:0,90,180,270

        设置完成 RGA 的属性后,开启通道即可,但是后续需要 Bind VI 和 RGA的通道,这样才能正常得到视频流。

1.3VENC模块:

        VENC模块是RV1126的视频编码模块,支持 H264/H265/MJPEG/JPEG 编码。在本项目中我们使用 H.264/AVC 压缩视频数据。

        我们先简单讲述以下H.264压缩的实现:

1.3.1 H.264/AVC:

VAL层:

        首先,我们会将一个图片分割成一个个小的blocks,如图:

在H.264中,一般是16x16的blocks,并且根据图片细节,我们会将这些个16x16的小blocks再切分成16x8,8x16,4x4等更小的blocks。

        接着,这些被细分的blcoks都会通过一个系统,如下图:

其中:

        (1)input就是那些一个个的small blocks;这里有个error,代表的就是帧内预测图像和帧间预测图像和原始图像的残差信息。

        (2)T、Q是DCT变换、量化的过程,T-1、Q-1就是反变换和反量化过程。DCT将图像分成由不同频率组成的小块,然后进行量化。在量化过程中,舍弃高频分量,剩下的低频分量被保存下来用于后面的图像重建。效果如下图:

所以其实这一部分实际是有损压缩,将人眼并不敏感的高频分量舍弃以获取最大的压缩比。

        (3)Intra Prediction:帧内预测,通过已经编码好的像素块,以不同方向预测其他像素块数据的方式。看下面的动态图,H.264的话只支持九种方向预测(这里有些忘记了)。通过这样的方式,我们就可以减少原始数据量的传输和存储,从而压缩帧内的空间冗余。

实际例子(这里用一下别的大佬的原图),可以看见预测的基本不差:

        (4)Inter Prediction:帧间预测,减少了视频的时间冗余。第一步,编码器在参考帧中寻找与当前块最相似的区域。这个过程称为运动估计(Motion Estimation)。第二步,找到最匹配的区域后,编码器计算该区域与当前块之间的位移,称为运动矢量(Motion Vector)。第三步,使用运动矢量对参考帧进行补偿,生成预测块。这个过程称为运动补偿(Motion Compensation)。通过这种方法,编码器不需要重新存储当前块的完整图像信息,只需要传输运动矢量和后续对比实际块的残差,大大减少数据量。

        (5)Entropy Coding:熵编码,数据压缩中的一种无损编码技术,用于去除冗余信息,从而减少数据的表示长度。

        到最后其实一个个small blocks都会以下图形式进行循环,熵编码记录帧的残差信息,预测好的块用于下一次的帧内预测和进入图片缓冲区组合成一个完整的帧供帧间预测的使用

NAL层:

        所有熵编码的bit流和Picture Buffer中的图像数据都会在该层转化成一个个NALU单元,最后按下述格式传输(当然,可以没有B frame):

        每个NALU = [start code] + [NALU header] + [NALU payload]

        start code:一般分为0x000001和0x00000001两种

        NALU header:NALU类型(5bit)、重要性位(2bit)、禁止位(1bit)

        

        下图是NALU的TYPE类型对应值:

        SPS(Sequence Parameter Set):SPS为解码器提供了解码整个视频序列所需的全局信息。图像的宽度和高度、帧率(时间信息)、编码配置文件和级别、颜色格式和色度、取样参考帧的最大数量、视频格式(如帧结构或场结构)。

        PPS(Picture Parameter Set):PPS包含描述特定图片或一组图片(如一个GOP)的参数,这些参数可以在SPS定义的全局参数基础上进一步细化。量化参数、去块效应滤波器参数、变换系数和预测模式、参考帧的使用方式、局部帧率调整。

        IDR(Instantaneous Decoding Refresh):特殊I帧, IDR帧的作用是立刻刷新,使错误不致传播,从IDR帧开始,重新算一个新的序列开始编码。

        I帧(Intra-coded picture)(帧内编码图像帧),不参考其他图像帧,只利用本帧的信息进行编码。
        P帧(Predictive-codedPicture)(预测编码图像帧),利用之前的I帧或P帧,采用运动预测的方式进行帧间预测编码。
        B帧(Bidirectionallypredicted picture)(双向预测编码图像帧),提供最高的压缩比,它既需要之前的图像帧(I帧或P帧),也需要后来的图像帧(P帧),采用运动预测的方式进行帧间双向预测编码。

         I、P、B帧的表现形式:上述H.264系统,Picture Buffer + 残差bit流信息。对于I帧就是帧内预测块和实际块的残差和所有块都预测完的完整帧。P、B就是帧间预测.....

        以上就是大致的H.264的编码过程,若有错误请大佬纠正。原动图来源视频请看https://www.youtube.com/watch?v=LDeL7-49qm4&t=2041s 这位专门做音视频编码工作的外国老哥的高质量视频!

1.3.2 VENC模块初始化:

        讲完H.264编码,那么以下是RV1126的VENC模块初始化代码:      

#include <stdio.h>
#include "stdint.h"
#include "rkmedia_api.h"
#include "rkmedia_venc.h"

#define HIGH_RS_VENC_CHN_ID     0
#define LOW_RS_VENC_CHN_ID      1

typedef struct rv1126_venc_devMsg{
    uint8_t chn_id;
    VENC_CHN_ATTR_S venc_attr; 

}rv1126_venc_devMsg_t;

int rv1126_venc_init(rv1126_venc_devMsg_t* low_rs_venc, rv1126_venc_devMsg_t* high_rs_venc){
    if(!low_rs_venc || !high_rs_venc){
        return -1;
    }

    /*low resolution venc*/
    low_rs_venc->venc_attr.stVencAttr.enType = RK_CODEC_TYPE_H264;
    low_rs_venc->venc_attr.stVencAttr.enRotation = VENC_ROTATION_0;
    low_rs_venc->venc_attr.stVencAttr.imageType = IMAGE_TYPE_NV12;
    low_rs_venc->venc_attr.stVencAttr.u32PicWidth = 1280;
    low_rs_venc->venc_attr.stVencAttr.u32PicHeight = 720;
    low_rs_venc->venc_attr.stVencAttr.u32VirWidth = 1280;
    low_rs_venc->venc_attr.stVencAttr.u32VirHeight = 720;
    low_rs_venc->venc_attr.stVencAttr.u32Profile = 66;

    low_rs_venc->venc_attr.stRcAttr.enRcMode = VENC_RC_MODE_H264CBR;
    low_rs_venc->venc_attr.stRcAttr.stH264Cbr.u32BitRate = 1280 * 720 * 3;
    low_rs_venc->venc_attr.stRcAttr.stH264Cbr.u32Gop = 25;
    low_rs_venc->venc_attr.stRcAttr.stH264Cbr.u32SrcFrameRateDen = 1;
    low_rs_venc->venc_attr.stRcAttr.stH264Cbr.u32SrcFrameRateNum = 25;
    low_rs_venc->venc_attr.stRcAttr.stH264Cbr.fr32DstFrameRateDen = 1;
    low_rs_venc->venc_attr.stRcAttr.stH264Cbr.fr32DstFrameRateNum = 25;

    low_rs_venc->venc_attr.stGopAttr.enGopMode = VENC_GOPMODE_NORMALP;
    low_rs_venc->venc_attr.stGopAttr.u32GopSize = 25;
    low_rs_venc->venc_attr.stGopAttr.s32IPQpDelta = 6;

    int ret = RK_MPI_VENC_CreateChn(low_rs_venc->chn_id, &(low_rs_venc->venc_attr));
    if(ret){
        printf("low resolution venc create failed!\n");
        return -1;
    }
    printf("low resolution venc create success....\n");

    /*high resolution venc*/
        /*low resolution venc*/
    high_rs_venc->venc_attr.stVencAttr.enType = RK_CODEC_TYPE_H264;
    high_rs_venc->venc_attr.stVencAttr.enRotation = VENC_ROTATION_0;
    high_rs_venc->venc_attr.stVencAttr.imageType = IMAGE_TYPE_NV12;
    high_rs_venc->venc_attr.stVencAttr.u32PicWidth = 1920;
    high_rs_venc->venc_attr.stVencAttr.u32PicHeight = 1080;
    high_rs_venc->venc_attr.stVencAttr.u32VirWidth = 1920;
    high_rs_venc->venc_attr.stVencAttr.u32VirHeight = 1080;
    high_rs_venc->venc_attr.stVencAttr.u32Profile = 66;

    high_rs_venc->venc_attr.stRcAttr.enRcMode = VENC_RC_MODE_H264CBR;
    high_rs_venc->venc_attr.stRcAttr.stH264Cbr.u32BitRate = 1920 * 1080 * 3;
    high_rs_venc->venc_attr.stRcAttr.stH264Cbr.u32Gop = 25;
    high_rs_venc->venc_attr.stRcAttr.stH264Cbr.u32SrcFrameRateDen = 1;
    high_rs_venc->venc_attr.stRcAttr.stH264Cbr.u32SrcFrameRateNum = 25;
    high_rs_venc->venc_attr.stRcAttr.stH264Cbr.fr32DstFrameRateDen = 1;
    high_rs_venc->venc_attr.stRcAttr.stH264Cbr.fr32DstFrameRateNum = 25;

    high_rs_venc->venc_attr.stGopAttr.enGopMode = VENC_GOPMODE_NORMALP;
    high_rs_venc->venc_attr.stGopAttr.u32GopSize = 25;
    high_rs_venc->venc_attr.stGopAttr.s32IPQpDelta = 6;

    ret = RK_MPI_VENC_CreateChn(high_rs_venc->chn_id, &(high_rs_venc->venc_attr));
    if(ret){
        printf("high resolution venc create failed!\n");
        return -1;
    }
    printf("high resolution venc create success....\n");
    return 0;
}

         由于这里有高分辨率(1920x1080)和低分辨率(1280x720)两种分辨率视频流,所以我们需要两个通道对两种分辨率的视频流分别编码。这里拿高分辨率的 VENC 通道做讲解。

        设置 VENC 通道属性其实就是设置 VENC_CHN_ATTR_S 结构体。VENC_CHN_ATTR_S 结构体中包含3个成员变量:

        1、stVencAttr:设置编码器属性相关的结构体。其中包含:

        (1)宽、高、虚宽、虚高,这里不在重复;

        (2)Profile,编码等级,可供选择的等级如下图:

我们使用的Baseline编码等级是不支持B帧,只支持关键I帧和P帧。有关于H.264三个编码等级的区别看下图:

        (3)enType:视频数据编码类型,这里选择H.264的视频压缩编码;

        (4)imgType:视频输入的存储形式,这里选择NV12;

        (5)Rotation:视频的旋转角度,这里默认不旋转VENC_ROTATION_0。

        2、stRcAttr:设置码率控制器,这里就要提一嘴码率是什么?码率,每秒钟传输或处理的数据量,通常用比特率(bps)来表示。码率的大小会影响视频的质量及大小,设定码率大那么视频的细节更多、质量越好但是所需要的网络带宽就大,设定码率小那么视频质量就会有所下降,若设定过小,就会导致一些丢帧、图像失真等问题。

typedef struct rkVENC_RC_ATTR_S {
/* RW; the type of rc*/
VENC_RC_MODE_E enRcMode;
union {
VENC_H264_CBR_S stH264Cbr;
VENC_H264_VBR_S stH264Vbr;
VENC_H264_AVBR_S stH264Avbr;
VENC_MJPEG_CBR_S stMjpegCbr;
VENC_MJPEG_VBR_S stMjpegVbr;
VENC_H265_CBR_S stH265Cbr;
VENC_H265_VBR_S stH265Vbr;
VENC_H265_AVBR_S stH265Avbr;
};
} VENC_RC_ATTR_S;

        (1)enRcMode:编码协议,这里的编码协议选择如下:

typedef enum rkVENC_RC_MODE_E {
// H264
VENC_RC_MODE_H264CBR = 1,
VENC_RC_MODE_H264VBR,
VENC_RC_MODE_H264AVBR,
// MJPEG
VENC_RC_MODE_MJPEGCBR,
VENC_RC_MODE_MJPEGVBR,
// H265
VENC_RC_MODE_H265CBR,
VENC_RC_MODE_H265VBR,
VENC_RC_MODE_H265AVBR,
VENC_RC_MODE_BUTT,
} VENC_RC_MODE_E;

       可以看到这里对H.264压缩有三种CBR、VBR、AVBR。那么CBR简单来说就是固定码率,使码率维持在一个比较稳定的值,但是这样做的坏处就是如果一个视频背景比较单调,可能这个视频不需要相对高的码率去传输也能拥有一个不错的质量,那么这样的话会造成一定程度的资源浪费。所以为了考虑这种情况,那么VBR就被创造出来,它会根据视频的复杂程度,所需多少码率,来动态调整码率大小,在设置中的体现就是他会有一个Max码率和Average码率的设置。AVBR的话,我是把他理解成增强版AVBR,没怎么用过,这里就不在详细展开。

        (2)stH264Cbr(这里根据你选择的编码模式来使用的结构体,一定要保证你用什么模式,设定什么结构体):编码模式

        u32SrcFrameRateNum / u32SrcFrameRateDen 与 fr32DstFrameRateNum / fr32DstFrameRateDen :视频帧率 = u32SrcFrameRateNum / u32SrcFrameRateDen (fps)

        这里视频源就是输入视频流的帧率,默认25帧。目标视频流(Dst)的帧率我们保持不变,这里Src的帧率必须 ≥ Dst的帧率,否则RV1126内部编码器会报错 rc: Assertion rate_in >= rate_out failed at rc_frm_check_drop:154。

        u32BitRate码率,注意码率不要过大也不要过小。码率过大会给网络带来严重负担。网络带宽好比一条马路的宽度,码率过大就好比这条马路上我这个音视频占用网络资源的车很大很宽,路变得拥堵,其他需要网络资源的进程会被强制堵车。显然这样的情况是我们不想看到的。码率过小,视频质量就会下降。

        u32GOP:GOP,Group of picture,两个关键I帧之间的距离。在码率不变的前提下,GOP值越大,P、B帧的数量会越多,画面细节更多,也就更容易获取较好的图像质量。但是在强烈变化的场景下GOP值过大会导致编码器画面处理不过来,导致画质的下降。

        3、stGopAttr:GOP参数的设置,GOP和Qp量化参数都是用来调节视频质量的

        enGopMode:可供选择的有VENC_GOPMODE_NORMALP、 VENC_GOPMODE_TSVC、 VENC_GOPMODE_SMARTP、 VENC_GOPMODE_BUTT这四种,我们这里只讲VENC_GOPMODE_NORMALP与 VENC_GOPMODE_SMARTP这两种模式。VENC_GOPMODE_NORMALP,GOP 的结构通常是 I 帧和 P 帧的固定序列。P 帧只参考前面的 I 帧或 P 帧,没有使用任何复杂的参考策略。VENC_GOPMODE_SMARTP

        u32GopSize:与上文u32GOP一致,这里不再赘述。

        s32IPQpDelta:Qp值的调节,Qp值越小,画面月细节清晰,这是由于量化压缩的数据少了,保留了更多细节,相对应码率的需求也越高,取值范围为[0,51]下图是Qp对应Qp step的关系图:

QPQstep
00.625
10.6875
20.8125
30.875
41
51.125
61.25
71.375
81.625
91.75
102
112.25
122.5
132.75
143.25
153.5
164
174.5
185
195.5
206.5
217
228
239
2410
2511
2613
2714
2816
2918
3020
3122
3226
3328
3432
3536
3640
3744
3852
3956
4064
4172
4280
4388
44104
45112
46128
47144
48160
49176
50208
51224

        以上就可以完成对 VENC 编码器的初始化步骤。

1.4 模块之间的相互绑定:

int rv1126_dev_init(rv1126_vi_devMsg_t* vi,
                    rv1126_rga_devMsg_t* rga, 
                    rv1126_venc_devMsg_t* low_rs_venc,
                    rv1126_venc_devMsg_t* high_rs_venc){

    RK_MPI_SYS_Init();

    if(rv1126_vi_init(vi) == -1){
        printf("vi init failed!\n");
        return -1;
    }
    printf("VI init success and start streaming....\n");

    if(rv1126_rga_init(rga) == -1){
        printf("rga init failed!\n");
        return -1;
    }
    printf("rga init success....\n");

    if(rv1126_venc_init(low_rs_venc, high_rs_venc) == -1){
        printf("venc init failed!\n");
        return -1;
    }
    printf("venc init success....\n");

    MPP_CHN_S vi_chn_s;
    vi_chn_s.enModId = RK_ID_VI;
    vi_chn_s.s32ChnId = vi->chn_id;

    MPP_CHN_S rga_chn_s;
    rga_chn_s.enModId = RK_ID_RGA;
    rga_chn_s.s32ChnId = rga->chn_id;

    MPP_CHN_S high_rs_venc_chn_s;
    high_rs_venc_chn_s.enModId = RK_ID_VENC;
    high_rs_venc_chn_s.s32ChnId = high_rs_venc->chn_id;

    MPP_CHN_S low_rs_venc_chn_s;
    low_rs_venc_chn_s.enModId = RK_ID_VENC;
    low_rs_venc_chn_s.s32ChnId = low_rs_venc->chn_id;

    int ret = RK_MPI_SYS_Bind(&vi_chn_s, &high_rs_venc_chn_s);
    if(ret){
        return -1;
        printf("vi_chn and high_rs_venc_chn bind failed!\n");
    }

    ret = RK_MPI_SYS_Bind(&vi_chn_s, &rga_chn_s);
    if(ret){
        return -1;
        printf("vi_chn and rga_chn bind failed!\n");
    }

    ret = RK_MPI_SYS_Bind(&rga_chn_s, &low_rs_venc_chn_s);
    if(ret){
        return -1;
        printf("rga_chn and low_rs_venc_chn bind failed!\n");
    }
    printf("All chns bind success....\n");
    return 0;
}

        那么当我把前面所有模块都初始化完成后,我们就需要把这些模块之间绑定起来以传输稳定视频流。具体连接方式在文章开头就已经呈现,这里不再赘述。

第二部分:使用FFmpeg库对两种分辨率Stream流进行推流

        首先我们先讲一下如果我们要进行推流,我们所需要使用的FFmpeg框架。

2.1 FFmpeg的初始化工作:

        第一步,我们需要创建一个AVFormatContext 结构体对象并绑定流媒体地址,这个对象就好比一个通道框架,到时候我需要往这个通道里面加入各个视频、音频、字幕流组成的复合流来达到推流的目的。

        第二步,我们需要创建并初始化一个个需要的 AVStream 结构体对象,即 xx 流(视频、音频、字幕流等),那么在这里面,每个流都会有一个对应的编码器属性 AVCodecParameters,那么我们就需要初始化一个我们需要的编码器属性 AVCodecContext 对象内容复制到 AVCodecParameters对应对象中(这样做的好处是避免手动复制带来的一系列问题),完成对流的编码器的初始化。

        第三步,打开传输通道的文件(和打开Socket文件传输差不多意思)。

        第四步,向文件种先写入一段你定义的复合流的头部内容。

        具体细节看下面的代码与注释内容:

typedef enum{
    VEDIO_STREAM = 0,
    AUDIO_STREAM,
    SUBTITLE_STREAM
}STREAM_TYPE;


typedef enum{
    PROTOCOL_RMTP = 0,
    PROTOCOL_TS, 
}PROTOCOL_TYPE;



typedef struct Nake_Stream{

    //vedio, audio, subtitle stream
    STREAM_TYPE type;                 
    AVStream *stream;
    AVCodecContext *avcodeccontext;
    AVPacket *packet;
    int time_stamp;
}Nake_Stream;


typedef struct AV_Framework{

    PROTOCOL_TYPE protocol_type;
    char netWork_addr[500];
    AVFormatContext *avformatcontext;

    //vedio stream
    Nake_Stream vedio_stream;
    int vedio_width;
    int vedio_height;
    AVCodecID vedio_codec;

    //audio stream
    Nake_Stream audio_stream;
    int audio_sample_rate;
    AVCodecID audio_codec;

}AV_Framework;


int rkmedia_ffmpeg_context_init(AV_Framework *fw){

    if(!fw){
        fw = (AV_Framework *)malloc(sizeof(AV_Framework));
    }

    /*
    First step: Build a AVFormatContext for streaming,
                including mutiple streams, such as multiple vedio streams, audio streams, subtitle streams etc.
                avformat_alloc_output_context2(...), the API is to create a framework of streaming channel.
    */
    int ret = avformat_alloc_output_context2(&fw->avformatcontext, NULL, "flv", fw->netWork_addr);
    
    if(ret < 0){
        printf("Create AVFormatContext framework failed!\n");
        return -1;
    }
    printf("Create AVFormatContext framework success....\n");

    //Second step: Set type of video_codec and audio_codec of output format.
    //This means that when you don't manually specify a codec for a stream, it will use the default codec specified in oformat.
    fw->avformatcontext->oformat->video_codec = fw->vedio_codec;
    fw->avformatcontext->oformat->audio_codec = fw->audio_codec;

    //Third step: Create a new stream, like vedio stream or audio's etc.
    if(fw->vedio_codec != AV_CODEC_ID_NONE){
        if(add_newStream(fw, fw->vedio_codec, VEDIO_STREAM)){
            printf("add stream failed!\n");
            return -1;
        }
        printf("Add stream success....\n");
    }

#if 0
    if(fw->audio_codec != AV_CODEC_ID_NONE){

        if(add_newStream(fw, AV_CODEC_ID_AAC, AUDIO_STREAM)){
            printf("add stream failed!\n");        
            avformat_free_context(fw->avformatcontext);
            avcodec_free_context(&fw->audio_stream.AVCodecContext);
            av_packet_free(&fw->audio_stream.packet);
            return -1;
        }
        printf("Add stream success....\n");
    }
#endif

    //Fourth step: Open AVFormatContext->pb (IO) for writing stream data.
    if (!(fw->avformatcontext->oformat->flags & AVFMT_NOFILE)){

        //打开输出文件
        ret = avio_open(&fw->avformatcontext->pb, fw->netWork_addr, AVIO_FLAG_WRITE);
        if (ret < 0){
            printf("Open stream file failed!\n");
            return -1;
        }
    }

    /*
    Fifth step: Write the file format header that you defined, like if you set format is "flv",
                then it will write flv header and some metadata like "Script Data Tag".
     */
    ret = avformat_write_header(fw->avformatcontext, NULL);
    if(ret){
        printf("AVFormat write header failed!\n");
        return -1;
    }
    return 0;
}

        这部分代码是添加流的操作,具体步骤为:

        (1)创建一个stream流并将其添入至你对应的AVFormatContext中。

        (2)创建并选择对应编码器并设置编码器具体参数。

        (3)将编码器属性复制到 AVStream 的 AVCodecParameter 参数中。

        备注:在过程中你可以申请AVPacket的空间并记录下来,因为后续你视频流的内容都是存入AVPacket的Buffer中。

int add_newStream(AV_Framework *fw, AVCodecID CodecID, STREAM_TYPE stream_type){

    //First step: Create a new stream, use avformat_new_stream(...) API and bind it to corresponding AVFormatContext.
    AVStream *newStream = avformat_new_stream(fw->avformatcontext, NULL);
    if(!newStream){
        printf("Create new stream failed!\n");
        return -1;
    }
    printf("Create new stream success....\n");

    newStream->id = fw->avformatcontext->nb_streams - 1;

    //Second step: According to CodecID, select corresponding AVCodec and initialize it as you want. 
    AVCodec *codec_tmp =  avcodec_find_encoder(CodecID);
    if(!codec_tmp){
        printf("Find encoder failed!\n");      
        return -1;
    }
    printf("Find encoder success!\n");

    AVCodecContext *codecContext_tmp = avcodec_alloc_context3(codec_tmp);
    if(!codecContext_tmp){
        printf("Malloc codec failed!\n");
        return -1;
    }
    printf("Malloc codec success....\n");

    /*vedio stream*/
    if(stream_type == VEDIO_STREAM){
        //Set stream attribute
        newStream->r_frame_rate.den = 1;
        newStream->r_frame_rate.num = 25;
        newStream->time_base = (AVRational){1, 25};

        //Set codec of the stream's attribute
        codecContext_tmp->bit_rate = fw->vedio_width * fw->vedio_height * 3;
        codecContext_tmp->width = fw->vedio_width;
        codecContext_tmp->height = fw->vedio_height;
        codecContext_tmp->time_base = newStream->time_base;
        codecContext_tmp->gop_size = 25;
        codecContext_tmp->pix_fmt = AV_PIX_FMT_NV12;

        fw->vedio_stream.type = VEDIO_STREAM;
        fw->vedio_stream.stream = newStream; 
        fw->vedio_stream.avcodeccontext = codecContext_tmp;
        fw->vedio_stream.packet = av_packet_alloc();
        fw->vedio_stream.time_stamp = 0;
    }
    /*audio stream*/
    else if(stream_type == AUDIO_STREAM){
        codecContext_tmp->bit_rate = AUDIO_BIT_RATE;
        codecContext_tmp->sample_rate = fw->audio_sample_rate;
        //Dual Channel
        codecContext_tmp->channel_layout = AV_CH_LAYOUT_STEREO;         
        codecContext_tmp->channels = av_get_channel_layout_nb_channels(codecContext_tmp->channel_layout);

        newStream->time_base = (AVRational){1, codecContext_tmp->sample_rate};

        fw->audio_stream.type = AUDIO_STREAM;
        fw->audio_stream.stream = newStream;
        fw->audio_stream.avcodeccontext = codecContext_tmp;
        fw->audio_stream.packet = av_packet_alloc();
    }


    //Third step: Sync AVcodecContext settings to stream AVCodecParameter
    int ret = avcodec_parameters_from_context(newStream->codecpar, codecContext_tmp);
    if(ret){
        printf("Set avcodec for stream failed!\n");
        return -1;
    }
    printf("Set avcodec for stream success...\n");

    return 0;
}

        至此,这个项目中用到的FFmpeg库的框架就初始化完毕。

2.2 FFmpeg推流的具体实现:

        我们需要做的就是把RV1126部分的H.264视频压缩数据push在一个queue中,在从queue中pop出来放到 AVPacket 中并写入对应 AVFormatContext 框架中。中间加一级queue的好处就是,防止读写速度不一致,即防止RV1126采集并压缩的视频流 和 视频流输出至RTMP服务器端 速度不一致问题。尤其是写比读快的情况,这时加一级FIFO去当蓄水池是一个十分不错的选择。

        具体操作如下,一共四个线程去处理,前两个负责把RV1126 的H.264数据放入我们创建的queue中,后两个负责把数据从queue中取出,并写入 AVFormatContext 对应的文件中进行视频流传输(FLV格式):

using namespace std;

#include <queue>
#include <pthread.h>

#define V_DATA_MAX_BUF_SIZE     1024 * 1024 * 3

typedef struct v_queue_node{
    unsigned char v_data[V_DATA_MAX_BUF_SIZE];
    int v_data_size;
}v_queue_node_t;


class VedioH264_Queue{

    public:
        std::queue<v_queue_node_t*> vedioH264_queue;
        pthread_mutex_t videoMutex;
        pthread_cond_t videoCond;

    //class init api
    public:
        VedioH264_Queue();
        ~VedioH264_Queue();
    
    //queue operation api
    int get_queue_size();

    void vedio_frame_push(v_queue_node_t* node);
    v_queue_node_t* vedio_frame_pop();

};

extern VedioH264_Queue high_rs_vH264_queue;
extern VedioH264_Queue low_rs_vH264_queue;


//Process: H264 frame data to queue //
void* get_high_rsH264_frame(void *arg){
    pthread_detach(pthread_self());
    MEDIA_BUFFER mb;
    for(;;){
        mb = RK_MPI_SYS_GetMediaBuffer(RK_ID_VENC, HIGH_RS_VENC_CHN_ID, -1);
        if(!mb){
            printf("mbbuf get failed!\n");
            continue;
        }
        v_queue_node_t *node = (v_queue_node_t*)malloc(sizeof(v_queue_node_t));

        node->v_data_size = RK_MPI_MB_GetSize(mb);
        memcpy(node->v_data, RK_MPI_MB_GetPtr(mb), RK_MPI_MB_GetSize(mb));
        high_rs_vH264_queue.vedio_frame_push(node);
        // printf("High resolution frame push success....\n");
        RK_MPI_MB_ReleaseBuffer(mb);
    }


    return 0;
}



void* get_low_rsH264_frame(void *arg){
    pthread_detach(pthread_self());
    MEDIA_BUFFER mb = NULL;

    for(;;){
        mb = RK_MPI_SYS_GetMediaBuffer(RK_ID_VENC, LOW_RS_VENC_CHN_ID, -1);
        if(!mb){
            printf("mbbuf get failed!\n");
            continue;
        }
        
        v_queue_node_t *node = (v_queue_node_t*)malloc(sizeof(v_queue_node_t));
        
        node->v_data_size = RK_MPI_MB_GetSize(mb);
        memcpy(node->v_data, RK_MPI_MB_GetPtr(mb), RK_MPI_MB_GetSize(mb));
        low_rs_vH264_queue.vedio_frame_push(node);
        // printf("Low resolution frame push success....\n");
        RK_MPI_MB_ReleaseBuffer(mb);
    }

    return 0;
}



//Process: Streaming FLV stream to RTMP server//
void* push_high_rsFLV_stream(void *arg){
    pthread_detach(pthread_self());
    AV_Framework* fw = (AV_Framework *)arg;

    int ret; 
    for(;;){

        ret = high_rsFLV_streaming(fw); 
        if (ret == -1){
            printf("deal_video_avpacket error\n");
            break;
        }
    }

    av_write_trailer(fw->avformatcontext);                  
    avio_closep(&fw->avformatcontext->pb);                      
    //Vedio
    avcodec_free_context(&fw->vedio_stream.avcodeccontext);
    av_packet_free(&fw->vedio_stream.packet);
    av_buffer_unref(&fw->vedio_stream.packet->buf);
    //Audio
    // avcodec_free_context(&fw.audio_stream.avcodeccontext);
    // av_packet_free(&fw.audio_stream.packet);
    // av_buffer_unref(&fw.audio_stream.packet->buf);
    avformat_free_context(fw->avformatcontext);
    return NULL;

}


void* push_low_rsFLV_stream(void *arg){
    pthread_detach(pthread_self());
    AV_Framework* fw = (AV_Framework *)arg;

    int ret; 
    for(;;){
        ret = low_rsFLV_streaming(fw);
        if (ret == -1){
            printf("deal_video_avpacket error\n");
            break;
        }
    }

    av_write_trailer(fw->avformatcontext);                     
    avio_closep(&fw->avformatcontext->pb);                          
    //Vedio
    avcodec_free_context(&fw->vedio_stream.avcodeccontext);
    av_packet_free(&fw->vedio_stream.packet);
    av_buffer_unref(&fw->vedio_stream.packet->buf);
    //Audio
    // avcodec_free_context(&fw.audio_stream.avcodeccontext);
    // av_packet_free(&fw.audio_stream.packet);
    // av_buffer_unref(&fw.audio_stream.packet->buf);
    avformat_free_context(fw->avformatcontext);
    return NULL;

}

        

        队列的基本操作封装,这里会有用到锁去保证同一分辨率对应的两个线程(分别是Push H.264数据入队 和 Pop数据出队)的同步问题,条件变量在这里的作用是为了防止队列空的情况

VedioH264_Queue high_rs_vH264_queue;
VedioH264_Queue low_rs_vH264_queue;

VedioH264_Queue ::VedioH264_Queue(){
    
    pthread_mutex_init(&videoMutex, NULL);
    pthread_cond_init(&videoCond, NULL);
}

VedioH264_Queue::~VedioH264_Queue(){
    pthread_mutex_destroy(&videoMutex);
    pthread_cond_destroy(&videoCond);
}


int VedioH264_Queue::get_queue_size(){
    pthread_mutex_lock(&videoMutex);

    int count = vedioH264_queue.size();

    pthread_mutex_unlock(&videoMutex);
    return count;
}

void VedioH264_Queue::vedio_frame_push(v_queue_node_t* node){
    pthread_mutex_lock(&videoMutex);

    //broadcast all pthreads consume
    pthread_cond_broadcast(&videoCond);
    vedioH264_queue.push(node);

    pthread_mutex_unlock(&videoMutex);

}


v_queue_node_t* VedioH264_Queue::vedio_frame_pop(){
    pthread_mutex_lock(&videoMutex);

    //when there's no frame data in the queue
    //we should unlock the mutex for getting new frame data
    //and use condition to stop frame poping operation
    while(vedioH264_queue.size() == 0){
        pthread_cond_wait(&videoCond, &videoMutex);
    }

    v_queue_node_t* dest = vedioH264_queue.front();
    vedioH264_queue.pop();

    pthread_mutex_unlock(&videoMutex);

    return dest;
}

        复合流填充过程如下:

        (1)确保 AVPacket 已经有内存空间。

        (2)每接收一帧,记录当前 AVPacket 的 pts,并将AVPacket 中的 pts++。这里简述一下pts和dts的概念:

        PTS(Presentation Time Stamp):显示时间戳,这个时间戳用来告诉播放器该在什么时候显示这一帧的数据。

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

        在之前对 stream 中的 AVCodec 中的 timebase 我们已经设置了timebase是(AVRational){1,25}。这里累加 pts 就是说明当前我是第几帧。

        (3)确保 AVPacket 数据的时间基和 stream中AVCodec编解码器的时间基相同。

        (4)AVPacket,通过stream_index匹配对应你创建的流,并且将数据写入stream,并且内部会根据 AVFormatContext 设定的格式进行格式封装(估计内部也会自动将AVPacket的时间基转化到 AVFormatContext 设定的格式的时间基,这里我们设定的格式是"FLV")。

Streaming Operation API

int high_rsFLV_streaming(AV_Framework *fw){
    v_queue_node_t *vedio_frame = high_rs_vH264_queue.vedio_frame_pop();

    if(vedio_frame){  
        //First step: Get H264 or ACC data from  corresponding queue.     
        int ret = av_buffer_realloc(&fw->vedio_stream.packet->buf, vedio_frame->v_data_size);
        if(ret){
            printf("AVBufferRef malloc failed!\n");
            return -1;
        }
        fw->vedio_stream.packet->size = vedio_frame->v_data_size;                                      
        memcpy(fw->vedio_stream.packet->buf->data, vedio_frame->v_data, vedio_frame->v_data_size); 
        fw->vedio_stream.packet->data = fw->vedio_stream.packet->buf->data;                                                             

        free(vedio_frame);
        vedio_frame = NULL;

        //Second step: Record orignal timestamp.
        fw->vedio_stream.packet->pts = fw->vedio_stream.time_stamp++;

        //Third step: 
        av_packet_rescale_ts(fw->vedio_stream.packet, fw->vedio_stream.avcodeccontext->time_base, fw->vedio_stream.stream->time_base);

        fw->vedio_stream.packet->stream_index = fw->vedio_stream.stream->index;

        //Fourth step: Write frame to AVFormatContext's pb (IO).
        ret = av_interleaved_write_frame(fw->avformatcontext, fw->vedio_stream.packet);   
        if(ret){
            printf("Write frame (Streaming) failed!\n");
            return -1;
        }   
    }else{
        printf("Get data from high_rs_vH264_queue failed!\n");
        return -1;
    }

    return 0;

}




int low_rsFLV_streaming(AV_Framework *fw){
        v_queue_node_t *vedio_frame = low_rs_vH264_queue.vedio_frame_pop();

    if(vedio_frame){   
        //First step: Get H264 or ACC data from  corresponding queue. 
        int ret = av_buffer_realloc(&fw->vedio_stream.packet->buf, vedio_frame->v_data_size);
        if(ret){
            printf("AVBufferRef malloc failed!\n");
            return -1;
        }
        fw->vedio_stream.packet->size = vedio_frame->v_data_size;                                      
        memcpy(fw->vedio_stream.packet->buf->data, vedio_frame->v_data, vedio_frame->v_data_size); 
        fw->vedio_stream.packet->data = fw->vedio_stream.packet->buf->data;                                                             

        free(vedio_frame);
        vedio_frame = NULL;

        //Second step: Record orignal timestamp and put it into AVPacket pts.
        fw->vedio_stream.packet->pts = fw->vedio_stream.time_stamp++;

        //Third step: 
        av_packet_rescale_ts(fw->vedio_stream.packet, fw->vedio_stream.avcodeccontext->time_base, fw->vedio_stream.stream->time_base);

        

        //Fourth step: Write frame to AVFormatContext's pb (IO).
        //This index is to match corresponding stream.
        fw->vedio_stream.packet->stream_index = fw->vedio_stream.stream->index;
        ret = av_interleaved_write_frame(fw->avformatcontext, fw->vedio_stream.packet);   
        if(ret){
            printf("Write frame (Streaming) failed!\n");
            return -1;
        }   
    }else{
        printf("Get data from high_rs_vH264_queue failed!\n");
        return -1;
    }

    return 0;

}

        完成以上步骤,视频流的推流便已经全部完成,那么再加上一个音频流流程也是基本一致的。之后会继续补充....(未完待续)

第三部分:补充

FLV格式:

        FLV格式详解-CSDN博客,这个博主写的挺详细的,可以直接参考他的文章。

评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值