RV1126+FFMPEG多路码流监控项目

一.项目介绍:

本项目采用的是易百纳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_QUEUEinit_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分配AVFormatContextavformat_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_PROTOCOLTS_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_H264AV_CODEC_ID_H265并利用avcodec_alloc_context3去创建AVCodecContext上下文。

初始化完AVStream和编码上下文结构体之后,我们就需要对这些参数进行配置。重点:推流编码器参数和RV1126编码器的参数要完全一样,否则可能会出问题,具体的如下图:

1920 * 1080编码器和FFMPEG推流器的配置

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

FFMPEG的视频编码参数如:分辨率(WIDTHHEIGHT)、时间基(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的设置需要调整

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/29824.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Python组合数据类型(一)

目录 一、数据类型 1、基本数据类型 2、组合数据类型 二、介绍两个函数 1、 isinstance函数 2、len函数 三、Python指针 1、指针 2、is运算符和的区别 3、列表的指针 四、函数参数的传递 1、例子一 2、例子二 五、字符串详解 1、转义字符 2、字符串的切片 3、字…

Doris vs ClickHouse 企业级实时分析引擎怎么选?

Apache Doris 与 ClickHouse 同作为OLAP领域的佼佼者&#xff0c;在企业级实时分析引擎该如何选择呢。本文将详细介绍 Doris 的优势&#xff0c;并通过直观对比展示两者的关键差异&#xff0c;同时分享一个企业成功用 Doris 替换 ClickHouse 的实践案例&#xff0c;帮助您做出明…

【ThreeJS Basics 09】Debug

文章目录 简介从 dat.GUI 到 lil-gui例子安装 lil-gui 并实例化不同类型的调整改变位置针对非属性的调整复选框颜色 功能/按钮调整几何形状文件夹调整 GUI宽度标题关闭文件夹隐藏按键切换 结论 简介 每一个创意项目的一个基本方面是能够轻松调整。开发人员和参与项目的其他参与…

Android Native 之 文件系统挂载

一、文件系统挂载流程概述 二、文件系统挂载流程细节 1、Init启动阶段 众所周知&#xff0c;init进程为android系统的第一个进程&#xff0c;也是native世界的开端&#xff0c;要想让整个android世界能够稳定的运行&#xff0c;文件系统的创建和初始化是必不可少的&#xff…

Chain of Draft: 借鉴人类草稿思维让大型语言模型更快地思考

这个研究探讨了大型语言模型&#xff08;LLMs&#xff09;在执行复杂推理任务时面临的计算资源消耗与响应延迟问题。研究特别聚焦于思维链&#xff08;Chain-of-Thought, CoT&#xff09;提示范式的效率局限性。CoT虽然有效&#xff0c;但在推理过程中需要生成冗长、详尽的逐步…

《A++ 敏捷开发》- 18 软件需求

需求并不是关于需求 (Requirements are not really about requirements) 大家去公共图书馆寄存物品&#xff0c;以前都是扫二维码开箱&#xff0c;有些图书馆升级了使用指纹识别。 “是否新方法比以前好&#xff1f;”我问年轻的开发人员。 “当然用指纹识别好。新技术&#x…

【智能体架构:Agent】LangChain智能体类型ReAct、Self-ASK的区别

1. 什么是智能体 将大语言模型作为一个推理引擎。给定一个任务&#xff0c; 智能体自动生成完成任务所需步骤&#xff0c; 执行相应动作&#xff08;例如选择并调用工具&#xff09;&#xff0c; 直到任务完成。 2. 先定义工具&#xff1a;Tools 可以是一个函数或三方 API也…

Vue进阶之Vue3源码解析(一)

Vue3源码解析 目录结构编译compiler-corepackage.jsonsrc/index.ts 入口文件src/compile.ts生成ASTsrc/parse.ts 代码转换src/transform.ts几种策略模式src/transforms/transformElement.tssrc/transforms/transformText.tssrc/transforms/transformExpression.ts 代码生成src/…

servlet tomcat

在spring-mvc demo程序运行到DispatcherServlet的mvc处理 一文中&#xff0c;我们实践了浏览器输入一个请求&#xff0c;然后到SpringMvc的DispatcherServlet处理的整个流程. 设计上这些都是tomcat servlet的处理 那么究竟这是怎么到DispatcherServlet处理的&#xff0c;本文将…

【我的待办(MyTodolists)-免费无内购的 IOS 应用】

我的待办&#xff08;MyTodolists&#xff09; 我的待办&#xff1a;智能任务管理助手应用说明主要功能为什么选择"我的待办"&#xff1f;隐私保障使用截图 我的待办&#xff1a;智能任务管理助手 应用说明 "我的待办"是一款智能化的任务管理应用&#x…

GCC RISCV 后端 -- C语言语法分析过程

在 GCC 编译一个 C 源代码时&#xff0c;先会通过宏处理&#xff0c;形成 一个叫转译单元&#xff08;translation_unit&#xff09;&#xff0c;接着进行语法分析&#xff0c;C 的语法分析入口是 static void c_parser_translation_unit(c_parser *parser); 接着就通过类似递…

Vim复制内容到系统剪切板

参考链接 【Vim】Vim 中将文件内容复制到系统剪切板的方法_vi 复制到系统剪贴板-CSDN博客 [转]vim如何复制到系统剪贴板 - biiigwang - 博客园 1. 确定Vim是否支持复制到系统剪切板 输入命令 vim --version | grep clipboard 如果是开头&#xff0c;说明支持系统剪切板&…

测试用大模型组词

已经把hanzi-writer的js的调用、hanzi-writer调用的数千个汉字的json文件&#xff0c;全都放在本地了。虽然用的办法还是比较笨的。我注意到 大模型也可以部署本地&#xff0c;虽然使用频率低的情况下不划算。 尝试直接通过html的javascript通过api key调用大语言模型&#x…

华为eNSP:配置单区域OSPF

一、什么是OSPF&#xff1f; OSPF&#xff08;Open Shortest Path First&#xff0c;开放最短路径优先&#xff09;是一种链路状态路由协议&#xff0c;属于内部网关协议&#xff08;IGP&#xff09;&#xff0c;主要用于在单一自治系统&#xff08;AS&#xff09;内部动态发现…

P62 线程

这篇文章我们来讲一下线程。截止到目前&#xff0c;我们的代码都是在单线程上运行的&#xff0c;现在看起来没有什么问题&#xff0c;但是目前所有的计算机几乎都不只有一个逻辑线程&#xff0c;所以如果我们一直使用单线程运行&#xff0c;这样的话效率会很低。尤其是如果我们…

Android AudioFlinger(五)—— 揭开AudioMixer面纱

前言&#xff1a; 在 Android 音频系统中&#xff0c;AudioMixer 是音频框架中一个关键的组件&#xff0c;用于处理多路音频流的混音操作。它主要存在于音频回放路径中&#xff0c;是 AudioFlinger 服务的一部分。 上一节我们讲threadloop的时候&#xff0c;提到了一个函数pr…

im即时聊天客服系统SaaS还是私有化部署:成本、安全与定制化的权衡策略

随着即时通讯技术的不断发展&#xff0c;IM即时聊天客服系统已经成为企业与客户沟通、解决问题、提升用户体验的重要工具。在选择IM即时聊天客服系统时&#xff0c;企业面临一个重要决策&#xff1a;选择SaaS&#xff08;软件即服务&#xff09;解决方案&#xff0c;还是进行私…

DeepSeek系列模型技术报告的阅读笔记

DeepSeek系列模型技术报告的阅读笔记 之前仔细阅读了DeepSeek系列模型的主要技术方面内容与发展脉络&#xff0c;以下是DeepSeek系列模型技术报告的笔记&#xff0c;有错误的地方欢迎指正&#xff01; 文章目录 DeepSeek系列模型技术报告的阅读笔记GQADeepseek MoEAbstractIn…

【VUE】第二期——生命周期及工程化

目录 1 生命周期 1.1 介绍 1.2 钩子 2 可视化图表库 3 脚手架Vue CLI 3.1 使用步骤 3.2 项目目录介绍 3.3 main.js入口文件代码介绍 4 组件化开发 4.1 组件 4.2 普通组件注册 4.2.1 局部注册 4.2.2 全局注册 1 生命周期 1.1 介绍 Vue生命周期&#xff1a;就是…

Spring-framework源码编译

版本统一&#xff08;搭配其他版本会遇到不可知错误&#xff09;&#xff1a; 1&#xff09;spring 5.2.X&#xff08;5.5.26&#xff09; 2&#xff09;JDK8 3&#xff09;Gradle:5.6.4 可以在gradle-wrapper.properties中修改 https\://services.gradle.org/distribution…