RV1126+FFMPEG多路码流项目
本项目采用的是易百纳RV1126开发板和CMOS摄像头,使用的推流框架是FFMPEG开源项目。这个项目的工作流程如下(如下图):通过采集摄像头的VI模块,再通过硬件编码VENC模块进行H264/H265的编码压缩,并把压缩后的数据通过FFMPEG传输到两个流媒体服务器(如同时推送到流媒体服务器:rtmp://xxx.xxx.xx.xxx:1935/live/01和rtmp://xxx.xxx.xx
1、项目介绍:
本项目采用的是易百纳RV1126开发板和CMOS摄像头,使用的推流框架是FFMPEG开源项目。这个项目的工作流程如下(如下图):通过采集摄像头的VI模块,再通过硬件编码VENC模块进行H264/H265的编码压缩,并把压缩后的数据通过FFMPEG传输到两个流媒体服务器(如同时推送到流媒体服务器:rtmp://xxx.xxx.xx.xxx:1935/live/01和rtmp://xxx.xxx.xx.xxx:1935/live/02)。这个项目我们可以学习到:多线程,队列,FFMPEG音视频数据传输等重要知识。

2、项目框架的思维导图:

上面是整个项目思维导图可以看出来,这个项目的main函数是整个项目的入口函数。在这里入口函数里面,需要做四个比较重要的步骤:分别是rkmedia组件和功能的初始化、初始化高分辨率队列HIGH_VIDEO_QUEUE
初始化低分辨率队列LOW_VIDEO_QUEUE、init_rv1126_first_assignment开启RV1126的推流任务。
2.1、VI模块讲解:
思维导图:
![]()
上面思维导图主要是描述VI模块设置的大致流程,首先对RV1126_VI_CONFIG结构体进行参数设置,然后调用rkmedia_vi_init对VI模块进行设置和使能,设置完成后再把VI的模块ID放到VI数组里面(vi_containers)。
代码截图:
1、配置VI模块结构体,这里使用设置的结构体是RV1126_VI_CONFIG。RV1126_VI_CONFIG里面主要包含了VI_CHN_ATTR_S结构体进行设置。


2、kmedia_vi_init这个自定义函数里面,关键是对VI进行初始化和使能,它分别调用了RK_MPI_VI_SetChnAttr的API对VI模块的属性进行初始化,然后再调用RK_MPI_VI_EnableChn对其使能。

3、设置完VI 模块后,把VI模块的ID号设置在容器里面,调用自己封装的函数set_vi_container

在这个自定义的函数里面,最主要是把VI的ID号存放在VI模块数组里面(vi_containers),具体结构如下图:这个函数的功能是江vi模块的ID号存储到这个数组里方便后续的.c进行调用

RV1126_ALL_CONTAINER结构体里面包含了四个模块的数组存储分别是VI模块(vi_contaianers)、AI模块(ai_containers)、VENC模块(venc_containers)、AENC模块(aenc_containers)。这四个模块容器就是分别存储,四个模块的ID号,让其能够更加方便的管理起来。
2.2、高分辨率VENC编码模块设计
VENC编码模块参数的设置是至关重要的,它可以对VI数据进行硬件编码让其可以进行高分辨率编码码流的推流
高分辨率VENC思维导图:


设置完上述VENC编码参数后,我们就要调用自己封装的函数rkmedia_venc_init函数,对VENC模块进行设置,具体的实现如下图:

设置完VENC模块后,就要把VENC模块的ID号设置到VENC容器数组里面,高分辨率VENC的ID号是0,调用自己封装的函数是set_venc_container

set_venc_container具体的实现,在这个自定义的函数里面,最主要是把VENC的ID号存放在VENC模块数组里面(vi_containers),具体结构如下图

这个自定义的函数里面,最主要是把VENC的ID号存放在VENC模块数组里面(venc_containers),具体结构如下图:

2.3、RGA模块:

RGA设置需要对RGA_ATTR_S结构体进行参数设置,设置完之后调用RK_MPI_RGA_CreateChn设置RGA模块。若成功则打印RGA Set Success,失败则打印RGA Set Failed。
代码截图:

RGA_ATTR_S结构体里面包含了两个重要的结构体,分别是stImgIn和stImgOut。stImgIn是视频输入的结构体,stImgOut是处理后的视频结构体。
stImgIn结构体的设置:
stImgIn.u32Width:视频输入的分辨率宽度,这里使用的是1920
stImgIn.u32Height:视频输入的分辨率宽度,这里使用的是1080
stImgIn.u32Width:视频输入的分辨率宽度,这里使用的是1920
stImgIn.u32Height:视频输入的分辨率宽度,这里使用的是1080
stImgIn.u32HorStride:视频输入的分辨率虚宽,虚宽的数值跟分辨率宽度数值一致都是1920
stImgIn.u32VirStride:视频输入的分辨率虚高,虚高的数值跟分辨率高度数值一致都是1080
stImgIn.imgType:视频输入的格式,这里设置的是NV12也就是IMAGE_TYPE_NV12
stImgIn.u32X:设置stImgIn的X坐标
stImgIn.u32Y:设置stImgIn的Y坐标
stImgOut结构体的设置:
stImgOut.u32Width:视频输入的分辨率宽度,这里使用的是1280
stImgOut.u32Height:视频输入的分辨率宽度,这里使用的是720
stImgOut.u32Width:视频输入的分辨率宽度,这里使用的是1280
stImgOut.u32Height:视频输入的分辨率宽度,这里使用的是720
stImgOut.u32HorStride:视频输入的分辨率虚宽,虚宽的数值跟分辨率宽度数值一致都是1280
stImgOut.u32VirStride:视频输入的分辨率虚高,虚高的数值跟分辨率高度数值一致都是720
stImgOut.imgType:视频输入的格式,这里设置的是NV12也就是IMAGE_TYPE_NV12
stImgIn.u32X:设置stImgOut的X坐标
stImgIn.u32Y:设置stImgOut的Y坐标
RGA公共参数
u16BufPoolCnt:缓冲区计数,默认是3
u16Rotation:旋转角度,这里填0,没有旋转角度
enFlip:镜像控制,这里选择RGA_FLIP_H,水平镜像处理
bEnBufPool:使能缓冲池
2.4、低分辨率VENC编码模块设置:

它的流程基本上和高分辨率是一致的。首先对RV1126_VENC_CONFIG结构体进行参数设置,然后调用rkmedia_venc_init对VENC模块进行设置,设置完成后再把VENC的模块ID放到VENC管理数组里面(venc_containers)。
代码截图:

设置完上述VENC编码参数后,我们同样要调用自己封装的函数rkmedia_venc_init函数,对低分辨率VENC模块进行设置,具体的实现如下图:

设置完VENC模块后,就要把VENC模块的ID号设置到VENC容器数组里面,低分辨率VENC的ID号是1,调用自己封装的函数是set_venc_container
其中低分辨率的VENC_ID,其中id=1,对应得高分辨率得id=0;

set_venc_container具体的实现:在这个自定义的函数里面,最主要是把低分辨率VENC的ID号存放在VENC模块数组里面(venc_containers),具体结构如下图:

在这个自定义的函数里面,最主要是把VENC的ID号存放在VENC模块数组里面(venc_containers),具体结构如下图:、

2.5camera_venc_thread线程获取高分辨率编码码流
我们要从VI节点容器和VENC节点容器里面获取到对应的VI节点和VENC节点,然后调用RK_MPI_SYS_Bind这个API绑定VI节点和VENC节点。然后创建camera_venc_thread线程获取高分辨率VENC码流,然后入到HIGH_VIDEO_QUEUE队列。这部分代码的实现在rkmedia_assignment_manage.cpp和rkmedia_data_process.cpp这两个源文件里面。

思维导图:

整个流程最关键是get_vi_container获取VI节点,get_venc_container获取VENC节点,然后调用RK_SYS_MPI_Bind绑定VI和VENC节点,并启动camera_venc_thread线程获取编码数据赋值到video_data_packet_t结构体,然后入队列。
代码截图:



我们首先要通过get_vi_container从VI容器里面获取到VI节点,然后再调用get_venc_container从venc容器里面获取venc节点。利用RK_MPI_SYS_Bind把VI节点和VENC节点绑定起来,绑定起来后创建camera_venc_thread线程,从这个线程里面获取1920 * 1080的编码码流数据。

调用的API是RK_MPI_SYS_GetMediaBuffer,MOD_ID是RK_ID_VENC, CHN_ID是创建的VENC的CHNID来直接获取高分辨率的VENC码流数据,并且把数据拷贝到video_data_packet_t结构体,包括每一帧的视频流数据RK_MPI_GetPtr(mb),还有每一帧的视频长度RK_MPI_GetSize(mb)。然后把整个video_data_packet包入队,high_video_queue->putVideoPacketQueue里面。video_data_packet_t结构体里面有两个成员变量,一个是buffer(视频缓冲区)、video_frame_size是每一帧视频的长度,frame_flag关键帧标识符。下面是RKMEDIA_BUFFER赋值到VIDEO_DATA_PACKET_T的核心代码:
memcpy(video_data_packet->buffer, RK_MPI_MB_GetPtr(mb), RK_MPI_MB_GetSize(mb));
video_data_packet->video_frame_size = RK_MPI_MB_GetSize(mb);
2.6、get_rga_thread线程和low_camera_venc_thread线程获取低分辨率VENC码流数据
由于低分辨率需要通过RGA模块处理获得1280*720得原始数据,并将视频数据发送到VENC模块中获取,所以需要构建两个线程分别进行处理;一个RGA线程用于将1920*1080得视频压缩到1080*720;另一个通过VENC编码器将压缩的数据进行编码得到低分辨率视频流;将最后的视频流数据存放到packet中并发送到队列中;
思维导图:

代码截图:





上面的截图就是如何通过get_rga_thread和low_camera_thread线程的结合获取低分辨率(1280 * 720)的编码码流。首先要通过RGA的节点和VENC的节点进行RK_SYS_MPI_Bind绑定,然后开启get_rga_thread获取每一帧的RGA处理过后的1280 * 720原始数据,并且调用RK_MPI_SYS_SendMediaBuffer这个API把每一帧1280 * 720的原始数据发送到低分辨率的编码器里面,然后再创建low_camera_thread现成获取每一帧1280 * 720的编码视频数据,然后把每一帧低分辨率的编码数据赋值到video_data_packet_t结构体,包括每一帧的视频流数据RK_MPI_GetPtr(mb),还有每一帧的视频长度RK_MPI_GetSize(mb)。然后把整个video_data_packet 包入队,low_video_queue->putVideoPacketQueue里面。video_data_packet_t结构体里面有两个成员变量,一个是buffer(视频缓冲区)、video_frame_size是每一帧视频的长度,frame_flag关键帧标识符。
3、FFMPEG推流器
FFMPEG主要的作用是进行视频推流的功能,就是把RV1126编码的视频码流利用FFMPEG框架推送到流媒体服务器。
FFMPEG中有六个比较重要的结构体,分别是AVFormatContext、AVOutputFormat、 AVStream、AVCodec、AVCodecContext、AVPacket、AVFrame、AVIOContext结构体,这几个结构体是贯穿着整个FFMPEG核心功能,并且也是我们这个项目中最重要的几个结构体。下面我们来重点介绍这几个结构体:
4、FFMPEG模块初始化:
先初始化AV_STREAM结构体并通过、avcodec_find_encoder找出对应的codec编码器,再把codec里的参数传到AVStream里面的参数中,,在初始化FFMPEG得IO结构体,avformat_write_header初始化AVFormatContext。

2.1. 分配FFMPEG AVFormatContext输出的上下文结构体指针

上面这个API是根据我们流媒体类型去分配AVFormatContext结构体。我们传进来的类型会分为FLV_PROTOCOL和TS_PROTOCOL,具体如何配置如下面:
若TS_PROTOCOL类型:avformat_alloc_output_context2(&group->oc, NULL, "mpegts", group->url_addr);
若FLV_PROTOCOL类型:avformat_alloc_output_context2(&group->oc, NULL, "flv", group->url_addr);
注意:TS格式分别可以适配以下流媒体复合流,包括:SRT、UDP、TS本地文件等。flv格式包括:RTMP、FLV本地文件等等。
2.2、配置推流器编码参数和AVStream结构体:
AVStream主要是存储流信息结构体,这个流信息包含音频流和视频流。创建的API是avformat_new_stream,如下图:

2.3. 设置对应的推流器编码器参数

AVCodec *avcodec_find_encoder(enum AVCodecID id); //
第一个传输参数:传递参数AVCodecID结构体指针
avcodec_find_encoder的主要作用是通过codec_id(编码器id )找到对应的AVCodec结构体。在RV1126推流项目中codec_id我们使用两种,分别是AV_CODEC_ID_H264、AV_CODEC_ID_H265。并利用avcodec_alloc_context3去创建AVCodecContext上下文。
初始化完AVStream和编码上下文结构体之后,我们就需要对这些参数进行配置。重点:推流编码器参数和RV1126编码器的参数要完全一样,否则可能会出问题,具体的如下图:
1920 * 1080编码器和FFMPEG推流器的配置:

1280* 720编码器和FFMPEG推流器的配置:

FFMPEG的视频编码参数如:分辨率(WIDTH、HEIGHT)、时间基(time_base)、 帧率(r_frame_rate)、GOP_SIZE等都需要和右边VENC的参数要一一对应起来。其中time_base的值要和视频帧率必须要一致。如RV1126高编码器分辨率是1920 * 1080,则FFMPEG推流器的WIDTH = 1920,HEIGHT = 1080;若RV1126编码器的分辨率是1280 * 720,则FFMPEG推流器的WIDTH = 1280,HEIGHT = 720;若RV1126的GOP的值是25,那右边FFMPEG的gop_size 也等于25;time_base的数值和帧率保持一致

AV_CODEC_FLAG_GLOBAL_HEADER:发送视频数据的时候都会在关键帧前面添加SPS/PPS,这个标识符在FFMPEG初始化的时候都需要添加
2.5. 设置完上述参数之后,拷贝参数到AVStream编解码器,具体的操作如下
拷贝参数到AVStream,我们封装到open_video自定义函数里面,要先调用avcodec_open2打开编码器,然后再调用avcodec_parameters_from_context把编码器参数传输到AVStream里面

2.5.1.int avcodec_open2(AVCodecContext *avctx, const AVCodec *codec, AVDictionary **options);
这个函数的具体作用是,打开编解码器
第一个参数:AVCodecContext结构体指针
第二个参数:AVCodec结构体指针
第三个参数:AVDictionary二级指针
2.5.2. int avcodec_parameters_from_context(AVCodecParameters *par, const AVCodecContext *codec);
这个函数的具体作用是,把AVCodecContext的参数拷贝到AVCodecParameters里面。
第一个参数:AVCodecParameters结构体指针
第二个参数:AVCodecContext结构体指针
2.6. 打开IO文件操作

使用avio_open打开对应的文件,注意这里的文件不仅是指本地的文件也指的是网络流媒体文件,下面是avio_open的定义。

int avio_open(AVIOContext **s, const char *url, int flags);
第一个参数:AVIOContext的结构体指针,它主要是管理数据输入输出的结构体
第二个参数: url地址,这个URL地址既包括本地文件如(xxx.ts、xxx.mp4),也可以是网络流媒体地址,如(rtmp://192.168.22.22:1935/live/01)等
第三个参数:flags标识符
#define AVIO_FLAG_READ 1 /**< read-only */
#define AVIO_FLAG_WRITE 2 /**< write-only */
#define AVIO_FLAG_READ_WRITE (AVIO_FLAG_READ|AVIO_FLAG_WRITE) /**< read-write pseudo flag */
2.7. avformat_write_header对头部进行初始化,输出模块头部进行初始化

int avformat_write_header(AVFormatContext *s, AVDictionary **options);
第一个参数:传递AVFormatContext结构体指针
第二个参数:传递AVDictionary结构体指针的指针
更多推荐


所有评论(0)