上一篇我们把硬件模块都初始化完成了,接下来我们就用多线程来获取码流
一、多线程获取高分辨率码流

本章节主要介绍如何通过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队列。这部分代码的实现在rkmedia_assignment_manage.cpp 和rkmedia_data_process.cpp这两个源文件里面。
1.1 camera_venc_thread线程的思维导图

上图是获取1920 * 1080视频编码数据的思维导图,整个流程最关键是get_vi_container获取VI节点,get_venc_container获取VENC节点,然后调用RK_SYS_MPI_Bind绑定VI和VENC节点,并启动camera_venc_thread线程获取编码数据赋值到video_data_packet_t结构体,然后入队列。
1.2 通道绑定 + 创建高清编码读取线程代码
在rkmedia_assignment_manage.cpp这个文件中编写通道绑定代码
// 定义MPP媒体通道结构体,分别用来标识VI采集通道、VENC编码通道
MPP_CHN_S vi_channel;
MPP_CHN_S venc_channel;
//从VI容器里面获取VI_ID
RV1126_VI_CONTAINTER vi_container;
get_vi_container(0, &vi_container);
//从VENC容器里面获取VENC_ID
RV1126_VENC_CONTAINER venc_container;
get_venc_container(0, &venc_container);
// 指定当前通道所属模块为VI视频采集模块
vi_channel.enModId = RK_ID_VI; //VI模块ID
// 赋值从全局容器取出的VI硬件通道编号
vi_channel.s32ChnId = vi_container.vi_id;//VI通道ID
// 指定当前通道所属模块为VENC硬件编码模块
venc_channel.enModId = RK_ID_VENC;//VENC模块ID
// 赋值从全局容器取出的高清VENC硬件通道编号
venc_channel.s32ChnId = venc_container.venc_id;//VENC通道ID
// 系统绑定VI采集通道与高清VENC编码通道,硬件直通传输画面,减少CPU拷贝
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");
}
// 线程句柄,用于存放创建的编码读取线程ID
pthread_t pid;
//VENC线程的参数,自定义结构体用于向子线程传递VENC通道ID
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通道ID存入线程参数,供子线程内部使用
venc_arg_params->vencId = venc_channel.s32ChnId;
// 创建高清编码读取线程,入口函数camera_venc_thread,传入通道参数
ret = pthread_create(&pid, NULL, camera_venc_thread, (void *)venc_arg_params);
if (ret != 0)
{
printf("create camera_venc_thread failed\n");
}
这段代码是对通道进行绑定,从全局容器读取提前初始化完成的 VI、高清 VENC 通道 ID,调用RK_MPI_SYS_Bind完成硬件直通绑定,VI 原始画面直接硬件传输至 VENC 编码器,低延迟无 CPU 拷贝,然后创建camera_venc_thread线程
1.3 camera_venc_thread线程函数
在rkmedia_data_process.cpp这个文件中写线程函数
// 高清VENC码流读取子线程入口函数
void *camera_venc_thread(void *args)
{
// 设置线程为分离态,线程退出后系统自动回收资源,无需主线程join
pthread_detach(pthread_self());
// MPP媒体缓冲区,用来接收VENC输出的一帧编码码流
MEDIA_BUFFER mb = NULL;
// 拷贝主线程传递过来的VENC通道参数至局部变量
VENC_PROC_PARAM venc_arg = *(VENC_PROC_PARAM *)args;
// 释放主线程malloc分配的参数内存,防止内存泄漏
free(args);
printf("video_venc_thread...\n");
// 死循环持续读取硬件编码码流,程序运行期间持续工作
while (1)
{
// 阻塞式读取指定VENC通道的编码缓冲区,-1代表无数据时永久阻塞等待
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 = (video_data_packet_t *)malloc(sizeof(video_data_packet_t));
// 拷贝VENC缓冲区中的H264码流数据到自定义数据包buffer数组
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);
// video_data_packet->frame_flag = naluType;
// 将封装完成的高清码流数据包存入高清全局队列,供推流线程读取
high_video_queue->putVideoPacketQueue(video_data_packet);
// printf("#naluType = %d \n", naluType);
// 释放MPP媒体缓冲区,硬件才能继续输出下一帧编码数据,不释放会阻塞编码
RK_MPI_MB_ReleaseBuffer(mb);
}
// 线程异常退出,解绑VI与高清VENC硬件通道
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 vi
ret = RK_MPI_VI_DisableChn(0, 0);
if (ret)
{
printf("Disable Chn Venc error! ret=%d\n", ret);
return 0;
}
return NULL;
}
线程开头和死循环,前面的博客都讲了,这里不再赘述。
video_data_packet_t *video_data_packet = (video_data_packet_t *)malloc(sizeof(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);
high_video_queue->putVideoPacketQueue(video_data_packet);
这段代码是上面线程函数中的一部分,它的作用是将码流打包封装
1.video_data_packet_t:自定义数据包,把一帧码流、长度打包成统一格式,方便队列统一管理。
2.RK_MPI_MB_GetPtr(mb) 获取硬件缓冲区数据指针;RK_MPI_MB_GetSize(mb) 获取当前帧字节大小。
3.memcpy 把硬件码流拷贝到自定义包:
- 为什么拷贝:
mb是硬件临时缓冲区,用完必须释放,不能长期持有;拷贝到堆内存后,队列可以长期保存、交给推流线程使用。
4.putVideoPacketQueue:把数据包推入高清队列。
- 核心设计目的:解耦编码读取和 FFmpeg 推流 读取码流速度、网络推流速度不一样,如果不用队列,网络卡顿会直接阻塞硬件编码,造成花屏、丢帧;队列充当缓冲,两边互不影响。
1.4 数据包结构体定义
在ffmpeg_video_queue.cpp中对数据包结构体进行定义
// 自定义视频数据包结构体,用于封装单帧编码后的码流数据
typedef struct _video_data_packet_t
{
// 码流数据存储缓冲区,定义最大缓存长度
unsigned char buffer[MAX_VIDEO_BUFFER_SIZE];
// 当前帧码流有效数据长度
int video_frame_size;
// 帧标记位,区分I关键帧 / P预测帧
int frame_flag;
}video_data_packet_t;
二、多线程获取低分辨率码流

本章节主要介绍如何通过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低分辨率编码器。
2.1 低分辨率码流获取的思维导图

上图是获取低分辨率1280 * 720编码码流的思维导图,从上面的思维导图可以看出来,整个流程最关键是get_vi_container获取VI节点,然后调用RK_SYS_MPI_Bind绑定VI和RGA节点。然后创建get_rga_thread线程获取每一帧rga的1280 * 720的视频原始数据,调用的API是RK_MPI_SYS_GetMediaBuffer ,获取每一帧1280 * 720原始数据后再调用RK_MPI_SYS_SendMediaBuffer发送每一帧原始数据到1280 * 720的VENC编码器。
创建low_camera_venc_thread 线程获取每一帧1280 * 720的编码码流数据,调用的API是RK_MPI_SYS_GetMediaBuffer 。 获取完每一帧1280 * 720编码视频数据后**,**存放到low_video_queue里面,调用的函数是putVideoPacketQueue。
2.2 创建get_rga_thread线程
在rkmedia_assignment_manage.cpp这个文件中接着上面的高分辨旅通道绑定来继续编写
// 定义RGA通道句柄结构体,用于存储RGA模块通道编号、模块ID
MPP_CHN_S rga_channel;
// 指定该通道归属硬件模块:RGA图像处理模块
rga_channel.enModId = RK_ID_RGA;
// 设置RGA使用通道号为0通道
rga_channel.s32ChnId = 0;
// 系统绑定接口:将VI视频输入通道 绑定到 RGA图像处理通道
ret = RK_MPI_SYS_Bind(&vi_channel, &rga_channel);
// 判断绑定返回值,RK MPI接口返回0代表成功,非0为异常
if (ret != 0)
{
// VI绑定RGA通道失败,打印错误日志并退出当前函数,返回错误码-1
printf("vi bind rga error\n");
return -1;
}
else
{
// VI与RGA通道绑定成功打印提示
printf("vi bind rga success\n");
}
// 创建独立子线程,线程执行函数为get_rga_thread,线程入参传NULL
ret = pthread_create(&pid, NULL, get_rga_thread, NULL);
// 判断线程创建结果,非0代表创建失败
if(ret != 0)
{
// 打印RGA数据读取线程创建失败日志
printf("create get_rga_thread failed\n");
}
这段代码的作用是VI与RGA的通道绑定,创建rga线程
2.3 get_rga_thread线程
在rkmedia_data_process.cpp这个文件中写线程函数
// RGA数据读取线程入口函数,args为线程传入参数(本业务未使用)
void * get_rga_thread(void * args)
{
// 媒体缓冲区句柄,用于存放RGA输出的图像帧数据
MEDIA_BUFFER mb = NULL;
// 无限循环,持续读取RGA输出图像帧
while (1)
{
// 从RGA模块0通道读取媒体帧,-1代表永久阻塞等待,直到有图像数据
mb = RK_MPI_SYS_GetMediaBuffer(RK_ID_RGA, 0 , -1); //获取RGA的数据
// 判断缓冲区是否获取失败(mb为空指针)
if(!mb)
{
// 获取帧失败,跳出循环,线程退出
break;
}
// 将RGA处理完成的图像帧,发送到VENC编码模块1通道进行编码
RK_MPI_SYS_SendMediaBuffer(RK_ID_VENC, 1, mb); //
// 释放媒体缓冲区,归还MPP内存池,防止内存泄漏
RK_MPI_MB_ReleaseBuffer(mb);
}
// 线程正常退出返回空指针
return NULL;
}
这段代码的作用是不断读取经过RGA硬件处理后的图像,转发给编码器进行视频编码。
2.4 创建低分辨率编码读取线程
在rkmedia_assignment_manage.cpp这个文件中编写代码
// 定义MPP标准通道结构体,用于标识低码率编码器VENC通道
MPP_CHN_S low_venc_channel;
//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");
// 释放内存(此处代码存在逻辑bug,分配失败时指针本就是NULL,free无效)
free(venc_arg_params);
}
// 将编码器通道号赋值给线程参数结构体,线程内部可获取当前操作的VENC通道
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");
}
前面RGA已经发送数据了,这段代码的作用就是创建low_camera_thread线程获取低分辨率(1280 * 720)的编码码流。
2.5 low_camera_venc_thread线程函数
在rkmedia_data_process.cpp这个文件中写线程函数
// 低码率编码码流读取线程入口函数,args为主线程传入的编码器通道参数堆指针
void *low_camera_venc_thread(void *args)
{
// 将当前线程设置为分离态:线程退出后自动释放自身资源,无需主线程pthread_join回收
pthread_detach(pthread_self());
// MPP媒体缓冲区句柄,用来存放VENC输出的一帧H264/H265码流
MEDIA_BUFFER mb = NULL;
// 将主线程malloc出来的参数结构体拷贝一份到栈变量,脱离堆内存依赖
VENC_PROC_PARAM venc_arg = *(VENC_PROC_PARAM *)args;
// 主线程分配的参数堆内存已拷贝完成,直接释放,防止内存泄漏
free(args);
printf("low_video_venc_thread...\n");
// 无限循环持续读取编码器输出码流
while (1)
{
// 从指定通道中获取VENC数据
// 原代码:根据传入参数venc_arg.vencId动态获取对应编码通道码流(被注释弃用)
//mb = RK_MPI_SYS_GetMediaBuffer(RK_ID_VENC, venc_arg.vencId, -1);
// 固定读取VENC模块1通道的码流缓冲区,-1表示永久阻塞等待码流,无数据时休眠不占CPU
mb = RK_MPI_SYS_GetMediaBuffer(RK_ID_VENC, 1, -1);
// 判断缓冲区获取失败(通道销毁、硬件异常、解绑时返回NULL)
if (!mb)
{
printf("low_venc break....\n");
// 跳出循环,线程结束
break;
}
// 获取帧标志位,用于区分I帧/P帧(代码注释暂未启用)
// int naluType = RK_MPI_MB_GetFlag(mb);
// 分配一包视频数据包结构体,用于存放一帧完整编码码流,投递业务队列
video_data_packet_t *video_data_packet = (video_data_packet_t *)malloc(sizeof(video_data_packet_t));
// 获取VENC缓冲区原始码流数据指针,拷贝码流数据到自定义数据包buffer内存中
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);
// 保存帧类型(I/P帧标记,代码注释暂未启用)
// video_data_packet->frame_flag = naluType;
// 将封装好的一帧视频数据包写入全局视频缓存队列,供推流/录像模块消费
low_video_queue->putVideoPacketQueue(video_data_packet);
// 打印NALU帧类型日志(注释关闭)
// printf("#naluType = %d \n", naluType);
// 释放MPP媒体缓冲区,归还MPP内存池,防止媒体内存泄漏
RK_MPI_MB_ReleaseBuffer(mb);
}
// 线程正常退出返回空指针
return NULL;
}
这段代码是获取每一帧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关键帧标识符。
以上就是获取高低分辨率码流的线程。