一.项目介绍:
本项目采用的是易百纳RV1126开发板和CMOS摄像头,使用的推流框架是FFMPEG开源项目。这个项目的工作流程如下(如上图):通过采集摄像头的VI模块,再通过硬件编码VENC模块进行H264/H265的编码压缩,并把压缩后的数据通过FFMPEG传输到两个流媒体服务器(如同时推送到流媒体服务器:rtmp://xxx.xxx.xx.xxx:1935/live/01和rtmp://xxx.xxx.xx.xxx:1935/live/02)。
二项目框架思维导图
上面是整个项目思维导图可以看出来,这个项目的main函数是整个项目的入口函数。在这里入口函数里面,需要做四个比较重要的步骤:分别是rkmedia组件和功能的初始化、初始化高分辨率队列HIGH_VIDEO_QUEUE、初始化低分辨率队列LOW_VIDEO_QUEUE、init_rv1126_first_assignment开启RV1126的推流任务。
2.1. init_rkmedia_module_function讲解:
这个函数主要是做RKMEDIA的组件初始化,组件包括:VI模块的初始化、高分辨率VENC模块的初始化、低分辨率VENC模块的初始化、RGA模块初始化。
2.1.1. VI模块初始化:
初始化摄像头模块让其摄像头模块能够正常工作,具体的VI模块初始化在rkmedia_vi_init里面。
int init_rkmedia_module_function()
{rkmedia_function_init();RV1126_VI_CONFIG rkmedia_vi_config;memset(&rkmedia_vi_config, 0, sizeof(rkmedia_vi_config));rkmedia_vi_config.id = 0;rkmedia_vi_config.attr.pcVideoNode = CMOS_DEVICE_NAME; // VIDEO视频节点路径,rkmedia_vi_config.attr.u32BufCnt = 3; // VI捕获视频缓冲区计数,默认是3rkmedia_vi_config.attr.u32Width = 1920; // 视频输入的宽度,一般和CMOS摄像头或者外设的宽度一致rkmedia_vi_config.attr.u32Height = 1080; // 视频输入的高度,一般和CMOS摄像头或者外设的高度一致rkmedia_vi_config.attr.enPixFmt = IMAGE_TYPE_NV12; // 视频输入的图像格式,默认是NV12(IMAGE_TYPE_NV12)rkmedia_vi_config.attr.enBufType = VI_CHN_BUF_TYPE_MMAP; // VI捕捉视频的类型rkmedia_vi_config.attr.enWorkMode = VI_WORK_MODE_NORMAL; // VI的工作模式,默认是NORMAL(VI_WORK_MODE_NORMAL)int ret = rkmedia_vi_init(&rkmedia_vi_config); // 初始化VI工作if (ret != 0){printf("vi init error\n");}else{printf("vi init success\n");RV1126_VI_CONTAINTER vi_container;vi_container.id = 0;vi_container.vi_id = rkmedia_vi_config.id;set_vi_container(0, &vi_container); // 设置VI容器}
填写完配置参数后,就会调用rkmedia_vi_init这个自己封装的函数,这个函数主要是实现VI模块的初始化和使能的具体操作
int rkmedia_vi_init(RV1126_VI_CONFIG *rv1126_vi_config)
{int ret;VI_CHN_ATTR_S vi_attr = rv1126_vi_config->attr;unsigned int id = rv1126_vi_config->id;//vi_attr.pcVideoNode = CMOS_DEVICE_NAME;////初始化VI模块ret = RK_MPI_VI_SetChnAttr(CAMERA_ID, id, &vi_attr);//使能VI模块ret |= RK_MPI_VI_EnableChn(CAMERA_ID, id);if (ret != 0){printf("create vi failed.....\n", ret);return -1;}return 0;
}
设置完VI模块后,就要把VI模块的ID号设置到容器里面,调用自己封装的函数是set_vi_container
set_vi_container的具体实现是:
int set_vi_container(unsigned int index, RV1126_VI_CONTAINTER *vi_container)
{pthread_mutex_lock(&all_containers_mutex);all_containers.vi_containers[index] = *vi_container;pthread_mutex_unlock(&all_containers_mutex);return 0;
}
在这个自定义的函数里面,最主要是把VI的ID号存放在VI模块数组里面(vi_containers),具体结构:
typedef struct
{unsigned int container_id;RV1126_VI_CONTAINTER vi_containers[ALL_CONTAINER_NUM];RV1126_AI_CONTAINTER ai_containers[ALL_CONTAINER_NUM];RV1126_VENC_CONTAINER venc_containers[ALL_CONTAINER_NUM];RV1126_AENC_CONTAINER aenc_containers[ALL_CONTAINER_NUM];}RV1126_ALL_CONTAINER;
RV1126_ALL_CONTAINER结构体里面包含了四个模块的数组存储分别是VI模块(vi_contaianers)、AI模块(ai_containers)、VENC模块(venc_containers)、AENC模块(aenc_containers)。这四个模块容器就是分别存储,四个模块的ID号,让其能够更加方便的管理起来。
2.1.2. RGA模块的初始化:
RGA主要是对VI模块的数据进行缩放操作,把1920 * 1080的视频数据转换成1280 * 720的视频数据。
RGA模块是视频处理模块,这个模块可以对VI视频数据进行缩放、裁剪、格式转换、图片叠加等的功能,在这个项目里面RGA模块最重要的功能是把1920 * 1080的分辨率转换成1280 * 720的分辨率。
// RGARGA_ATTR_S rga_info;/**Image Input ..............*/rga_info.stImgIn.u32Width = 1920; // 设置RGA输入分辨率宽度rga_info.stImgIn.u32Height = 1080; // 设置RGA输入分辨率高度rga_info.stImgIn.u32HorStride = 1920; // 设置RGA输入分辨率虚宽rga_info.stImgIn.u32VirStride = 1080; // 设置RGA输入分辨率虚高rga_info.stImgIn.imgType = IMAGE_TYPE_NV12; // 设置ImageType图像类型rga_info.stImgIn.u32X = 0; // 设置X坐标rga_info.stImgIn.u32Y = 0; // 设置Y坐标/**Image Output......................*/rga_info.stImgOut.u32Width = 1280; // 设置RGA输出分辨率宽度rga_info.stImgOut.u32Height = 720; // 设置RGA输出分辨率高度rga_info.stImgOut.u32HorStride = 1280; // 设置RGA输出分辨率虚宽rga_info.stImgOut.u32VirStride = 720; // 设置RGA输出分辨率虚高rga_info.stImgOut.imgType = IMAGE_TYPE_NV12; // 设置输出ImageType图像类型rga_info.stImgOut.u32X = 0; // 设置X坐标rga_info.stImgOut.u32Y = 0; // 设置Y坐标// RGA Public Parameterrga_info.u16BufPoolCnt = 3; // 缓冲池计数rga_info.u16Rotaion = 0; //rga_info.enFlip = RGA_FLIP_H;rga_info.bEnBufPool = RK_TRUE;ret = RK_MPI_RGA_CreateChn(0, &rga_info);if (ret){printf("RGA Set Failed.....\n");}else{printf("RGA Set Success.....\n");}
RGA_ATTR_S结构体里面包含了两个重要的结构体,分别是stImgIn和stImgOut。stImgIn是视频输入的结构体,stImgOut是处理后的视频结构体。除了这两个重要的结构体外,还有公共参数需要设置设置完上述的参数后,调用RK_MPI_RGA_CreateChn设置RGA模块。
2.1.3. VENC模块初始化(分别是高、低分辨率):
初始化高、低分辨率VENC硬件编码器,这里的编码器主要针对的是1920 * 1080和1280 * 720两种分辨率,具体的高分辨率VENC模块初始化在rkmedia_venc_init里面。
RV1126的高分辨率VENC编码模块的设置
RV1126_VENC_CONFIG rkmedia_venc_config = {0};
memset(&rkmedia_venc_config, 0, sizeof(rkmedia_venc_config));
rkmedia_venc_config.id = 0;
rkmedia_venc_config.attr.stVencAttr.enType = RK_CODEC_TYPE_H264; // 编码器协议类型
rkmedia_venc_config.attr.stVencAttr.imageType = IMAGE_TYPE_NV12; // 输入图像类型
rkmedia_venc_config.attr.stVencAttr.u32PicWidth = 1920; // 编码图像宽度
rkmedia_venc_config.attr.stVencAttr.u32PicHeight = 1080; // 编码图像高度
rkmedia_venc_config.attr.stVencAttr.u32VirWidth = 1920; // 编码图像虚宽度,一般来说u32VirWidth和u32PicWidth是一致的
rkmedia_venc_config.attr.stVencAttr.u32VirHeight = 1080; // 编码图像虚高度,一般来说u32VirHeight和u32PicHeight是一致的
rkmedia_venc_config.attr.stVencAttr.u32Profile = 66; // 编码等级H.264: 66: Baseline; 77:Main Profile; 100:High Profile; H.265: default:Main; Jpege/MJpege: default:Baseline(编码等级的作用主要是改变画面质量,66的画面质量最差利于网络传输,100的质量最好)rkmedia_venc_config.attr.stRcAttr.enRcMode = VENC_RC_MODE_H264CBR; // 编码器码率控制模式
rkmedia_venc_config.attr.stRcAttr.stH264Cbr.u32Gop = 25; // GOPSIZE:关键帧间隔
rkmedia_venc_config.attr.stRcAttr.stH264Cbr.u32BitRate = 1920 * 1080 * 3; // 码率
rkmedia_venc_config.attr.stRcAttr.stH264Cbr.fr32DstFrameRateDen = 1; // 目的帧率分子:填的是1固定
rkmedia_venc_config.attr.stRcAttr.stH264Cbr.fr32DstFrameRateNum = 25; // 目的帧率分母:填的是25固定
rkmedia_venc_config.attr.stRcAttr.stH264Cbr.u32SrcFrameRateDen = 1; // 源头帧率分子:填的是1固定
rkmedia_venc_config.attr.stRcAttr.stH264Cbr.u32SrcFrameRateNum = 25; // 源头帧率分母:填的是25固定ret = rkmedia_venc_init(&rkmedia_venc_config); // VENC模块的初始化
if (ret != 0)
{printf("venc init error\n");
}
else
{RV1126_VENC_CONTAINER venc_container;venc_container.id = 0;venc_container.venc_id = rkmedia_venc_config.id;set_venc_container(0, &venc_container);printf("venc init success\n");
}
设置完上述VENC编码参数后,我们就要调用自己封装的函数rkmedia_venc_init函数,对VENC模块进行设置,具体的实现:
int rkmedia_venc_init(RV1126_VENC_CONFIG *rv1126_venc_config)
{int ret;VENC_CHN_ATTR_S venc_chn_attr = rv1126_venc_config->attr;unsigned int venc_id = rv1126_venc_config->id;ret = RK_MPI_VENC_CreateChn(rv1126_venc_config->id, &venc_chn_attr);if (ret != 0){printf("create rv1126_venc_module failed\n");return -1;}else{printf("create rv1126_venc_module success\n");}return 0;
}
这个自定义函数还是非常简单的,就是把RK_MPI_VENC_CreateChn封装了一层,然后把RV1126_VENC_CONFIG的结构体指针传进去。
设置完VENC模块后,就要把VENC模块的ID号设置到VENC容器数组里面,高分辨率VENC的ID号是0,调用自己封装的函数是set_venc_container,
set_venc_container具体的实现:在这个自定义的函数里面,最主要是把VENC的ID号存放在VENC模块数组里面(vi_containers),具体结构如下:
int set_venc_container(unsigned int index, RV1126_VENC_CONTAINER *venc_container)
{pthread_mutex_lock(&all_containers_mutex);all_containers.venc_containers[index] = *venc_container;pthread_mutex_unlock(&all_containers_mutex);return 0;
}
在这个自定义的函数里面,最主要是把VENC的ID号存放在VENC模块数组里面(venc_containers),具体结构如下:
typedef struct
{unsigned int container_id;RV1126_VI_CONTAINTER vi_containers[ALL_CONTAINER_NUM];RV1126_AI_CONTAINTER ai_containers[ALL_CONTAINER_NUM];RV1126_VENC_CONTAINER venc_containers[ALL_CONTAINER_NUM];RV1126_AENC_CONTAINER aenc_containers[ALL_CONTAINER_NUM];}RV1126_ALL_CONTAINER;
这次VENC的ID号需要存放到venc_containers数组里面,这样更容易管理VENC模块号ID。
RV1126的低分辨率VENC编码模块的设置
低分辨率VENC的设置和高分辨率的设置方法基本上是一致的,唯一的区别在于分辨率要写成1280 * 720。获取低分辨率编码数据的流程,分别是VI模块获取视频数据->RGA模块处理->获取1280*720的原始数据->送到低分辨率编码器处理->获取1280 * 720的编码(h264/h265)压缩数据。
RV1126_VENC_CONFIG low_rkmedia_venc_config = {0};memset(&low_rkmedia_venc_config, 0, sizeof(low_rkmedia_venc_config));low_rkmedia_venc_config.id = 1;low_rkmedia_venc_config.attr.stVencAttr.enType = RK_CODEC_TYPE_H264; // 编码器协议类型low_rkmedia_venc_config.attr.stVencAttr.imageType = IMAGE_TYPE_NV12; // 输入图像类型low_rkmedia_venc_config.attr.stVencAttr.u32PicWidth = 1280; // 编码图像宽度low_rkmedia_venc_config.attr.stVencAttr.u32PicHeight = 720; // 编码图像高度low_rkmedia_venc_config.attr.stVencAttr.u32VirWidth = 1280; // 编码图像虚宽度,一般来说u32VirWidth和u32PicWidth是一致的low_rkmedia_venc_config.attr.stVencAttr.u32VirHeight = 720; // 编码图像虚高度,一般来说u32VirHeight和u32PicHeight是一致的low_rkmedia_venc_config.attr.stVencAttr.u32Profile = 66; // 编码等级H.264: 66: Baseline; 77:Main Profile; 100:High Profile; H.265: default:Main; Jpege/MJpege: default:Baseline(编码等级的作用主要是改变画面质量,66的画面质量最差利于网络传输,100的质量最好)low_rkmedia_venc_config.attr.stRcAttr.enRcMode = VENC_RC_MODE_H264CBR; // 编码器码率控制模式low_rkmedia_venc_config.attr.stRcAttr.stH264Cbr.u32Gop = 30; // GOPSIZE:关键帧间隔low_rkmedia_venc_config.attr.stRcAttr.stH264Cbr.u32BitRate = 1280 * 720 * 3; // 码率low_rkmedia_venc_config.attr.stRcAttr.stH264Cbr.fr32DstFrameRateDen = 1; // 目的帧率分子:填的是1固定low_rkmedia_venc_config.attr.stRcAttr.stH264Cbr.fr32DstFrameRateNum = 25; // 目的帧率分母:填的是25固定low_rkmedia_venc_config.attr.stRcAttr.stH264Cbr.u32SrcFrameRateDen = 1; // 源头帧率分子:填的是1固定low_rkmedia_venc_config.attr.stRcAttr.stH264Cbr.u32SrcFrameRateNum = 25; // 源头帧率分母:填的是25固定 ret = rkmedia_venc_init(&low_rkmedia_venc_config); // VENC模块的初始化if (ret != 0){printf("venc init error\n");}else{RV1126_VENC_CONTAINER low_venc_container;low_venc_container.id = 1;low_venc_container.venc_id = low_rkmedia_venc_config.id;set_venc_container(low_venc_container.id, &low_venc_container);printf("low_venc init success\n");}
设置完上述VENC编码参数后,我们同样要调用自己封装的函数rkmedia_venc_init函数,对低分辨率VENC模块进行设置,具体的实现如与高分辨VENC部分相似。设置完VENC模块后,就要把VENC模块的ID号设置到VENC容器数组里面,低分辨率VENC的ID号是1,调用自己封装的函数是set_venc_container在这个自定义的函数里面,最主要是把低分辨率VENC的ID号存放在VENC模块数组里面(venc_containers),这次VENC的ID号需要存放到venc_containers数组里面,这样更容易管理VENC模块号ID。
2.2. 高分辨率队列的初始化HIGH_VIDEO_QUEUE:
初始化搞分辨率编码数据队列,这个队列主要是存储1920 * 1080编码的视频数据
#include "ffmpeg_video_queue.h"//VIDEO队列的构造器,包含mutex的初始化和条件变量初始化
VIDEO_QUEUE::VIDEO_QUEUE()
{pthread_mutex_init(&videoMutex, NULL);//mutex的初始化pthread_cond_init(&videoCond, NULL);//条件变量初始化
}//VIDEO队列的析构函数,锁的销毁和条件变量的销毁
VIDEO_QUEUE ::~VIDEO_QUEUE()
{pthread_mutex_destroy(&videoMutex);//锁的销毁pthread_cond_destroy(&videoCond);//条件变量的销毁
}//VIDEO_QUEUE的插入视频队列操作
int VIDEO_QUEUE::putVideoPacketQueue(video_data_packet_t *video_packet)
{pthread_mutex_lock(&videoMutex); //上视频锁video_packet_queue.push(video_packet);//向视频队列插入video_data_packet_t包pthread_cond_broadcast(&videoCond);//唤醒视频队列pthread_mutex_unlock(&videoMutex);//解视频锁return 0;
}//VIDEO_QUEUE取出视频包
video_data_packet_t *VIDEO_QUEUE::getVideoPacketQueue()
{pthread_mutex_lock(&videoMutex);//上视频锁while (video_packet_queue.size() == 0){pthread_cond_wait(&videoCond, &videoMutex); //当视频队列没有数据的时候,等待被唤醒}video_data_packet_t *item = video_packet_queue.front();//把视频数据包移到最前面video_packet_queue.pop();//pop取出视频数据并删除pthread_mutex_unlock(&videoMutex);//解视频锁return item;
}//VIDEO_QUEUE视频队列长度
int VIDEO_QUEUE::getVideoQueueSize()
{unsigned int count = 0;pthread_mutex_lock(&videoMutex);//上视频锁count = video_packet_queue.size();//获取视频队列长度pthread_mutex_unlock(&videoMutex);//解视频锁return count;
}
这段代码是视频队列实现的过程,VIDEO_QUEUE是一个类。这个类里面,封装了添加视频队列(putVideoPacketQueue)、获取视频队列数据(getVideoPacketQueue)、获取视频队列长度(getVideoQueueSize)。
2.3. 低分辨率队列的初始化LOW_VIDEO_QUEUE:
初始化搞分辨率编码数据队列,这个队列主要是存储1280* 720编码的视频数据
代码同高分辨率队列的初始化一样
2.4. init_rv1126_first_assignment启动RV1126推流任务讲解:
这个函数主要进行多路码流推流的业务实现,这里面包含了:init_rkmedia_ffmpeg_context分别初始化高分辨率的ffmpeg推流器和低分辨率的ffmpeg推流器、创建camera_venc_thread线程、创建get_rga_thread线程、创建low_camera_venc_thread线程、创建high_video_push_thread线程、创建low_video_push_thread线程。
2.4.1. init_rkmedia_ffmpeg_context初始化高分辨率和低分辨率的推流器:
在这个函数里面主要是对FFMPEG推流器参数进行设置,它需要对高分辨率(1920 * 1080)和低分辨率(1280 * 720)的FFMPEG推流器进行初始化。
FFMPEG输出模块的最大作用是对音视频推流模块进行初始化让其能够正常工作起来,RV1126的码流通过FFMPEG进行推流,输出模块一般由几个步骤。分别由avformat_alloc_output_context2分配AVFormatContext、avformat_new_stream初始化AVStream结构体、avcodec_find_encoder找出对应的codec编码器、利用avcodec_alloc_context3分配AVCodecCotext、设置AVCodecContext结构体参数、利用avcodec_parameters_from_context把codec参数传输到AVStream里面的参数、avio_open初始化FFMPEG的IO结构体、avformat_write_header初始化AVFormatContext。
2.4.1.1分配FFMPEG AVFormatContext输出的上下文结构体指针:
//FLV_PROTOCOL is RTMP TCPif (ffmpeg_config->protocol_type == FLV_PROTOCOL){//初始化一个FLV的AVFormatContextret = avformat_alloc_output_context2(&ffmpeg_config->oc, NULL, "flv", ffmpeg_config->network_addr); if (ret < 0){return -1;}}//TS_PROTOCOL is SRT UDP RTSPelse if (ffmpeg_config->protocol_type == TS_PROTOCOL){//初始化一个TS的AVFormatContextret = avformat_alloc_output_context2(&ffmpeg_config->oc, NULL, "mpegts", ffmpeg_config->network_addr);if (ret < 0){return -1;}}
int avformat_alloc_output_context2(AVFormatContext **ctx, AVOutputFormat *oformat, const char *format_name, const char *filename)
第一个传输参数:AVFormatContext结构体指针的指针,是存储音视频封装格式中包含的信息的结构体,所有对文件的封装、编码都是从这个结构体开始。
第二个传输参数:AVOutputFormat的结构体指针,它主要存储复合流信息的常规配置,默认为设置NULL。
第三个传输参数:format_name指的是复合流的格式,比方说:flv、ts、mp4等等
第四个传输参数:filename是输出地址,输出地址可以是本地文件(如:xxx.mp4、xxx.ts等等)。也可以是网络流地址(如:rtmp://xxx.xxx.xxx.xxx:1935/live/01)
上面这个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.4.1.2. 配置推流器编码参数和AVStream结构体
AVStream主要是存储流信息结构体,这个流信息包含音频流和视频流。创建的API是avformat_new_stream,如下代码:
//创建输出码流的AVStream, AVStream是存储每一个视频/音频流信息的结构体
ost->stream = avformat_new_stream(oc, NULL);
if (!ost->stream)
{printf("Can't not avformat_new_stream\n");return 0;
}
else
{printf("Success avformat_new_stream\n");
}
AVStream * avformat_new_stream(AVFormatContext *s, AVDictionary **options);
第一个传输参数:AVFormatContext的结构体指针
第二个传输参数:AVDictionary结构体指针的指针
返回值:AVStream结构体指针
2.4.1.3. 设置对应的推流器编码器参数
//通过codecid找到CODEC*codec = avcodec_find_encoder(codec_id);if (!(*codec)){printf("Can't not find any encoder");return 0;}else{printf("Success find encoder");}
AVCodec *avcodec_find_encoder(enum AVCodecID id); //
第一个传输参数:传递参数AVCodecID
2.4.1.4. 根据编码器ID分配AVCodecContext结构体
//通过CODEC分配编码器上下文c = avcodec_alloc_context3(*codec);if (!c){printf("Can't not allocate context3\n");return 0;}else{printf("Success allocate context3");}
AVCodecContext *avcodec_alloc_context3(const AVCodec *codec);
第一个参数:传递AVCodec结构体指针
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的数值和帧率保持一致
//在h264头部添加SPS,PPSif (oc->oformat->flags & AVFMT_GLOBALHEADER){c->flags |= AV_CODEC_FLAG_GLOBAL_HEADER;}
AV_CODEC_FLAG_GLOBAL_HEADER:发送视频数据的时候都会在关键帧前面添加SPS/PPS,这个标识符在FFMPEG初始化的时候都需要添加。
2.4.1.5. 设置完上述参数之后,拷贝参数到AVStream编解码器,具体的操作如下:
拷贝参数到AVStream,我们封装到open_video自定义函数里面,要先调用avcodec_open2打开编码器,然后再调用avcodec_parameters_from_context把编码器参数传输到AVStream里面
//使能video编码器
int open_video(AVFormatContext *oc, AVCodec *codec, OutputStream *ost, AVDictionary *opt_arg)
{AVCodecContext *c = ost->enc;//打开编码器avcodec_open2(c, codec, NULL);//分配video avpacket包ost->packet = av_packet_alloc();/* 将AVCodecContext参数复制AVCodecParameters复用器 */avcodec_parameters_from_context(ost->stream->codecpar, c);return 0;
}
int avcodec_open2(AVCodecContext *avctx, const AVCodec *codec, AVDictionary **options);
这个函数的具体作用是,打开编解码器
第一个参数:AVCodecContext结构体指针
第二个参数:AVCodec结构体指针
第三个参数:AVDictionary二级指针
int avcodec_parameters_from_context(AVCodecParameters *par, const AVCodecContext *codec);
这个函数的具体作用是,把AVCodecContext的参数拷贝到AVCodecParameters里面。
第一个参数:AVCodecParameters结构体指针
第二个参数:AVCodecContext结构体指针
2.4.1.6. 打开IO文件操作
if (!(fmt->flags & AVFMT_NOFILE)){//打开输出文件ret = avio_open(&ffmpeg_config->oc->pb, ffmpeg_config->network_addr, AVIO_FLAG_WRITE);if (ret < 0){free_stream(ffmpeg_config->oc, &ffmpeg_config->video_stream);free_stream(ffmpeg_config->oc, &ffmpeg_config->audio_stream);avformat_free_context(ffmpeg_config->oc);return -1;}}avformat_write_header(ffmpeg_config->oc, NULL);
使用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 */
avformat_write_header对头部进行初始化,输出模块头部进行初始化
int avformat_write_header(AVFormatContext *s, AVDictionary **options);
第一个参数:传递AVFormatContext结构体指针
第二个参数:传递AVDictionary结构体指针的指针
2.4.2. 创建camera_venc_thread线程
camera_venc_thread线程最重要的作用是编码1920 * 1080的编码视频数据流并且入到HIGH_VIDEO_QUEUE队列
通过camera_venc_thread线程获取高分辨率(1920 * 1080)的编码码流数据,并且把编码码流插入到高分辨率编码码流队列里面。上图就是camera_venc_thread线程获取高分辨率编码码流的大体流程,我们要从VI节点容器和VENC节点容器里面获取到对应的VI节点和VENC节点,然后调用RK_MPI_SYS_Bind这个API绑定VI节点和VENC节点。然后创建camera_venc_thread线程获取高分辨率VENC码流,然后入到HIGH_VIDEO_QUEUE队列。
//从VI容器里面获取VI_IDRV1126_VI_CONTAINTER vi_container;get_vi_container(0, &vi_container);//从VENC容器里面获取VENC_IDRV1126_VENC_CONTAINER venc_container;get_venc_container(0, &venc_container);vi_channel.enModId = RK_ID_VI; //VI模块IDvi_channel.s32ChnId = vi_container.vi_id;//VI通道IDvenc_channel.enModId = RK_ID_VENC;//VENC模块IDvenc_channel.s32ChnId = venc_container.venc_id;//VENC通道ID//绑定VI和VENC节点ret = RK_MPI_SYS_Bind(&vi_channel, &venc_channel);if (ret != 0){printf("bind venc error\n");return -1;}else{printf("bind venc success\n");}
//VENC线程的参数VENC_PROC_PARAM *venc_arg_params = (VENC_PROC_PARAM *)malloc(sizeof(VENC_PROC_PARAM));if (venc_arg_params == NULL){printf("malloc venc arg error\n");free(venc_arg_params);}venc_arg_params->vencId = venc_channel.s32ChnId;//创建VENC线程,获取摄像头编码数据ret = pthread_create(&pid, NULL, camera_venc_thread, (void *)venc_arg_params);if (ret != 0){printf("create camera_venc_thread failed\n");}
void *camera_venc_thread(void *args)
{pthread_detach(pthread_self());MEDIA_BUFFER mb = NULL;VENC_PROC_PARAM venc_arg = *(VENC_PROC_PARAM *)args;free(args);printf("video_venc_thread...\n");while (1){// 从指定通道中获取VENC数据mb = RK_MPI_SYS_GetMediaBuffer(RK_ID_VENC, venc_arg.vencId, -1);if (!mb){printf("high_get venc media buffer error\n");break;}// int naluType = RK_MPI_MB_GetFlag(mb);// 分配video_data_packet_t结构体video_data_packet_t *video_data_packet = (video_data_packet_t *)malloc(sizeof(video_data_packet_t));// 把VENC视频缓冲区数据传输到video_data_packet的buffer中memcpy(video_data_packet->buffer, RK_MPI_MB_GetPtr(mb), RK_MPI_MB_GetSize(mb));// 把VENC的长度赋值给video_data_packet的video_frame_size中video_data_packet->video_frame_size = RK_MPI_MB_GetSize(mb);// video_data_packet->frame_flag = naluType;// 入到视频压缩队列high_video_queue->putVideoPacketQueue(video_data_packet);// printf("#naluType = %d \n", naluType);// 释放VENC资源RK_MPI_MB_ReleaseBuffer(mb);}MPP_CHN_S vi_channel;MPP_CHN_S venc_channel;vi_channel.enModId = RK_ID_VI;vi_channel.s32ChnId = 0;venc_channel.enModId = RK_ID_VENC;venc_channel.s32ChnId = venc_arg.vencId;int ret;ret = RK_MPI_SYS_UnBind(&vi_channel, &venc_channel);if (ret != 0){printf("VI UnBind failed \n");}else{printf("Vi UnBind success\n");}ret = RK_MPI_VENC_DestroyChn(0);if (ret){printf("Destroy Venc error! ret=%d\n", ret);return 0;}// destroy viret = RK_MPI_VI_DisableChn(0, 0);if (ret){printf("Disable Chn Venc error! ret=%d\n", ret);return 0;}return NULL;
}
上面三段代码就是关于camera_venc_thread整个流程,我们首先要通过get_vi_container从VI容器里面获取到VI节点,然后再调用get_venc_container从venc容器里面获取venc节点。利用RK_MPI_SYS_Bind把VI节点和VENC节点绑定起来,绑定起来后创建camera_venc_thread线程,从这个线程里面获取1920 * 1080的编码码流数据。
typedef struct _video_data_packet_t
{unsigned char buffer[MAX_VIDEO_BUFFER_SIZE];int video_frame_size;int frame_flag;}video_data_packet_t;
调用的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.4.3. 创建get_rga_thread线程和low_camera_venc_thread线程获取低分辨率VENC码流数据
get_rga_thread线程最重要的作用是处理1920 * 1080的摄像头数据,把它的分辨率降低到1280 * 720,并且把1280 * 720的原始码流传输到低分辨率(1280 * 720)的编码器
low_camera_venc_thread线程最重要的作用是获取分辨率1280 * 720的编码数据,并且入到LOW_VIDEO_QUEU队列
通过get_rga_thread线程和low_camera_venc_thread共同获取低分辨率(1280 * 720)的编码码流并且入队列。从上图我们可以看出。我们经过几个步骤首先要调用get_vi_container获取VI节点,然后把VI节点和RGA节点绑定起来,通过get_rga_thread线程获取1280 * 720的原始数据并把1280 * 720的原始数据发送到1280 * 720的VENC低分辨率编码器。
rga_channel.enModId = RK_ID_RGA;rga_channel.s32ChnId = 0;ret = RK_MPI_SYS_Bind(&vi_channel, &rga_channel);if (ret != 0){printf("vi bind rga error\n");return -1;}else{printf("vi bind rga success\n");}
ret = pthread_create(&pid, NULL, get_rga_thread, NULL);if(ret != 0){printf("create get_rga_thread failed\n");}
void * get_rga_thread(void * args)
{MEDIA_BUFFER mb = NULL;while (1){mb = RK_MPI_SYS_GetMediaBuffer(RK_ID_RGA, 0 , -1); //获取RGA的数据if(!mb){break;}RK_MPI_SYS_SendMediaBuffer(RK_ID_VENC, 1, mb); //RK_MPI_MB_ReleaseBuffer(mb);}return NULL;
}
//VENC线程的参数VENC_PROC_PARAM *low_venc_arg_params = (VENC_PROC_PARAM *)malloc(sizeof(VENC_PROC_PARAM));if (venc_arg_params == NULL){printf("malloc venc arg error\n");free(venc_arg_params);}low_venc_arg_params->vencId = low_venc_channel.s32ChnId;//创建VENC线程,获取摄像头编码数据ret = pthread_create(&pid, NULL, low_camera_venc_thread, (void *)low_venc_arg_params);if (ret != 0){printf("create camera_venc_thread failed\n");}
void *low_camera_venc_thread(void *args)
{pthread_detach(pthread_self());MEDIA_BUFFER mb = NULL;VENC_PROC_PARAM venc_arg = *(VENC_PROC_PARAM *)args;free(args);printf("low_video_venc_thread...\n");while (1){// 从指定通道中获取VENC数据//mb = RK_MPI_SYS_GetMediaBuffer(RK_ID_VENC, venc_arg.vencId, -1);mb = RK_MPI_SYS_GetMediaBuffer(RK_ID_VENC, 1, -1);if (!mb){printf("low_venc break....\n");break;}// int naluType = RK_MPI_MB_GetFlag(mb);// 分配video_data_packet_t结构体video_data_packet_t *video_data_packet = (video_data_packet_t *)malloc(sizeof(video_data_packet_t));// 把VENC视频缓冲区数据传输到video_data_packet的buffer中memcpy(video_data_packet->buffer, RK_MPI_MB_GetPtr(mb), RK_MPI_MB_GetSize(mb));// 把VENC的长度赋值给video_data_packet的video_frame_size中video_data_packet->video_frame_size = RK_MPI_MB_GetSize(mb);// video_data_packet->frame_flag = naluType;// 入到视频压缩队列low_video_queue->putVideoPacketQueue(video_data_packet);// printf("#naluType = %d \n", naluType);// 释放VENC资源RK_MPI_MB_ReleaseBuffer(mb);}return NULL;
}
上面的截图就是如何通过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的原始数据发送到低分辨率的编码器里面,核心代码,如下:
while (1){mb = RK_MPI_SYS_GetMediaBuffer(RK_ID_RGA, 0 , -1); //获取每一帧RGA处理过后的数据if(!mb){break;}RK_MPI_SYS_SendMediaBuffer(RK_ID_VENC, 1, mb); //把每一帧RGA数据传输到低分辨率VENC里面RK_MPI_MB_ReleaseBuffer(mb); //释放资源}
然后再创建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关键帧标识符。下面是RKMEDIABUFFER赋值到VIDEO_DATA_PACKET的核心代码:
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.4.4. 创建high_video_push_thread线程
high_video_push_thread线程作用是从HIGH_VIDEO_QUEUE队里取出每一帧1920*1080的视频编码数据,然后利用FFMPEG的推流到对应的流媒体服务器
上面是高分辨率推流的过程,总共分成6个步骤。分别是初始化RKMEDIA_FFMPEG_CONFIG结构体、调用init_rkmedia_ffmpeg_context设置1920 * 1080推流器、创建high_video_push_thread线程、从HIGH_VIDEO_QUEUE队列获取每一帧视频数据 、把每一帧的AVPacket的PTS进行计算和时间基转换、利用FFMPEG的API推送每一帧视频数据到流媒体服务器。
初始化RKMEDIA_FFMPEG_CONFIG结构体
typedef struct
{int width;int height;unsigned int config_id;int protocol_type; //流媒体TYPEchar network_addr[NETWORK_ADDR_LENGTH];//流媒体地址enum AVCodecID video_codec; //视频编码器IDenum AVCodecID audio_codec; //音频编码器IDOutputStream video_stream; //VIDEO的STREAM配置OutputStream audio_stream; //AUDIO的STREAM配置AVFormatContext *oc; //是存储音视频封装格式中包含的信息的结构体,也是FFmpeg中统领全局的结构体,对文件的封装、编码操作从这里开始。} RKMEDIA_FFMPEG_CONFIG; //FFMPEG配置
RKMEDIA_FFMPEG_CONFIG的成员变量
width:推流器的width,width和rv1126编码器的width一致
height:推流器的height,height和rv1126编码器的height一致
config_id:config_id,暂时没用到
protocol_type:流媒体的类型
network_addr:流媒体地址
video_codec:视频编码器ID
audio_codec:音频编码器ID
video_stream:自定义VIDEO的STREAM结构体配置
audio_stream:自定义AUDIO的STREAM结构体配置
上面是高分辨率rkmedia_ffmpeg_config的设置
init_rkmedia_ffmpeg_context是初始化rkmedia_ffmpeg_config的设置
创建high_video_push_thread线程:
void *high_video_push_thread(void *args)
{pthread_detach(pthread_self());RKMEDIA_FFMPEG_CONFIG ffmpeg_config = *(RKMEDIA_FFMPEG_CONFIG *)args;free(args);AVOutputFormat *fmt = NULL;int ret;while (1){ret = deal_high_video_avpacket(ffmpeg_config.oc, &ffmpeg_config.video_stream); // 处理FFMPEG视频数据if (ret == -1){printf("deal_video_avpacket error\n");break;}}av_write_trailer(ffmpeg_config.oc); // 写入AVFormatContext的尾巴free_stream(ffmpeg_config.oc, &ffmpeg_config.video_stream); // 释放VIDEO_STREAM的资源free_stream(ffmpeg_config.oc, &ffmpeg_config.audio_stream); // 释放AUDIO_STREAM的资源avio_closep(&ffmpeg_config.oc->pb); // 释放AVIO资源avformat_free_context(ffmpeg_config.oc); // 释放AVFormatContext资源return NULL;
}
high_video_push_thread最主要作用是在HIGH_VIDEO_QUEUE队列获取每一帧1920 * 1080的H264编码视频流,然后再把每一帧H264的码流数据先赋值到AVPacket,再调用FFMPEG的API把视频流传输到流媒体服务器。
int deal_high_video_avpacket(AVFormatContext *oc, OutputStream *ost)
{int ret;AVCodecContext *c = ost->enc;AVPacket *video_packet = get_high_ffmpeg_video_avpacket(ost->packet); // 从RV1126视频编码数据赋值到FFMPEG的Video AVPacket中if (video_packet != NULL){video_packet->pts = ost->next_timestamp++; // VIDEO_PTS按照帧率进行累加}ret = write_ffmpeg_avpacket(oc, &c->time_base, ost->stream, video_packet); // 向复合流写入视频数据if (ret != 0){printf("write video avpacket error");return -1;}return 0;
}
// 从RV1126视频编码数据赋值到FFMPEG的Video AVPacket中
AVPacket *get_high_ffmpeg_video_avpacket(AVPacket *pkt)
{video_data_packet_t *video_data_packet = high_video_queue->getVideoPacketQueue(); // 从视频队列获取数据if (video_data_packet != NULL){/*重新分配给定的缓冲区1. 如果入参的 AVBufferRef 为空,直接调用 av_realloc 分配一个新的缓存区,并调用 av_buffer_create 返回一个新的 AVBufferRef 结构;2. 如果入参的缓存区长度和入参 size 相等,直接返回 0;3. 如果对应的 AVBuffer 设置了 BUFFER_FLAG_REALLOCATABLE 标志,或者不可写,再或者 AVBufferRef data 字段指向的数据地址和 AVBuffer 的 data 地址不同,递归调用 av_buffer_realloc 分配一个新
的 buffer,并将 data 拷贝过去;4. 不满足上面的条件,直接调用 av_realloc 重新分配缓存区。*/int ret = av_buffer_realloc(&pkt->buf, video_data_packet->video_frame_size + 70);if (ret < 0){return NULL;}pkt->size = video_data_packet->video_frame_size; // rv1126的视频长度赋值到AVPacket Sizememcpy(pkt->buf->data, video_data_packet->buffer, video_data_packet->video_frame_size); // rv1126的视频数据赋值到AVPacket datapkt->data = pkt->buf->data; // 把pkt->buf->data赋值到pkt->datapkt->flags |= AV_PKT_FLAG_KEY; // 默认flags是AV_PKT_FLAG_KEYif (video_data_packet != NULL){free(video_data_packet);video_data_packet = NULL;}return pkt;}else{return NULL;}
}
上面的代码是从HIGH_VIDEO_QUEUE队列里面取出每一帧1920 * 1080的H264数据,并且赋值到AVPacket的过程。整个函数封装到deal_high_video_packet里面。在deal_high_video_packet主要是实现从HIGH_VIDEO_QUEUE队列获取每一帧数据并赋值到AVPacket的具体实现过程,具体如上代码。
这里面有几个比较核心的地方:video_data_packet的视频数据包赋值到AVPacket,这里要赋值两部分:一部分是AVPacket缓冲区数据的赋值,另外一个是AVPacket的长度赋值。
AVPacket缓冲区的赋值:首先用av_buffer_realloc分配每一个缓冲区数据。要注意的是AVPacket中缓冲区的buf是不能直接赋值的,如: memcpy(pkt->data, video_data_packet->buffer, video_data_packet->frame_size)否则程序就会出现core_dump情况。我们需要先把video_data_packet_t的视频数据(video_data_packet->buffer)先拷贝到pkt->buf->data,然后再把pkt->buf->data的数据赋值到pkt->data。
AVPacket缓冲区长度的赋值:把video_data_packet的video_frame_size长度直接赋值给AVPacket的pkt->size。
pkt->flags |= AV_PKT_FLAG_KEY;AVPacket关键帧标识符的赋值:添加了这个标识符后,每个AVPacket中都进行关键帧设置,这个标识符必须要加,否则播放器则无法正常解码出视频。
if (video_packet != NULL){video_packet->pts = ost->next_timestamp++; // VIDEO_PTS按照帧率进行累加}
每一帧AVPacket计算PTS时间戳:根据AVPacket的数据去计算视频的PTS,若AVPacket的数据不为空。则让视频pts = ost->next_timestamp++。
把每一帧视频数据传输到流媒体服务器时间基转换完成之后,就把视频数据写入到复合流文件里面,调用的API是av_interleaved_write_frame (注意:复合流文件可以是本地文件也可以是流媒体地址)。把视频PTS进行时间基的转换,调用av_packet_rescale_ts把采集的视频时间基转换成复合流的时间基。
int write_ffmpeg_avpacket(AVFormatContext *fmt_ctx, const AVRational *time_base, AVStream *st, AVPacket *pkt)
{/*将输出数据包时间戳值从编解码器重新调整为流时基 */av_packet_rescale_ts(pkt, *time_base, st->time_base);pkt->stream_index = st->index;return av_interleaved_write_frame(fmt_ctx, pkt);
}
上面初始化完成之后,我们就需要利用输出模块对流媒体服务器进行推流工作。在FFMPEG中我们基本上使用av_interleaved_write_frame去进行推流。av_interleaved_write_frame的功能是把压缩过后的音频数据(如:aac、mp3)、视频(h264/h265)数据交替地写入到复合流文件里面。这个复合流文件,可以是本地文件、也可以是流媒体数据。需要注意的是,av_interleaved_write_frame将会对AVPacket进行pts合法检查并进行,并进行缓存检查。
int av_interleaved_write_frame(AVFormatContext *s, AVPacket *pkt);
第一个参数:AVFormatContext结构体指针
第二个参数:AVPacket结构体指针,在我们这个项目里面AVPacket存储RV1126的编码数据。
返回值:成功==0,失败-22
2.4.5. 创建low_video_push_thread线程
low_video_push_thread线程作用是从LOW_VIDEO_QUEUE队里取出每一帧1280*720的视频编码数据,然后利用FFMPEG的推流到对应的流媒体服务器
与上述high_video_push_thread线程的步骤基本一致,在低分辨率rkmedia_ffmpeg_config的设置需要调整