RV1126+FFMPEG多路码流监控项目——多线程获取高低分辨率码流

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

一、多线程获取高分辨率码流

本章节主要介绍如何通过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.cpprkmedia_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关键帧标识符。

以上就是获取高低分辨率码流的线程。