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_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的设置需要调整
相关文章:
RV1126+FFMPEG多路码流监控项目
一.项目介绍: 本项目采用的是易百纳RV1126开发板和CMOS摄像头,使用的推流框架是FFMPEG开源项目。这个项目的工作流程如下(如上图):通过采集摄像头的VI模块,再通过硬件编码VENC模块进行H264/H265的编码压缩,并把压缩后的…...
doris: SQL Server
Doris JDBC Catalog 支持通过标准 JDBC 接口连接 SQL Server 数据库。本文档介绍如何配置 SQL Server 数据库连接。 使用须知 要连接到 SQL Server 数据库,您需要 SQL Server 2012 或更高版本,或 Azure SQL 数据库。 SQL Server 数据库的 JDBC 驱动…...
valgrind 检测多线程 bug,检测 并发 bug concurrent bug parallel bug
valgrind --toolhelgrind ./your_program 如果检测的对象是大型程序,可以设定仅在某些函数中开启 valgrind 的检测: Valgrind 提供了一些客户请求(client requests),可以在代码中插入特定的宏来控制 Valgrind 的行为。…...
查看k8s集群的资源使用情况
查看Kubernetes(k8s)集群的资源使用情况有多种方法,以下是一些常见的方式: 使用kubectl命令行工具 查看节点资源使用情况 kubectl top nodes命令可以显示集群中各个节点的CPU和内存使用情况。例如: NAME …...
在 k8s中查看最大 CPU 和内存的极限
在 Kubernetes(k8s)中,你可以从不同层面查看最大 CPU 和内存的极限,下面为你详细介绍从节点和集群层面查看的方法。 查看节点的 CPU 和内存极限 节点的 CPU 和内存极限是指单个节点上可分配的最大资源量,可通过以下几…...
IDC权威认证!永洪科技入选 IDC「GBI图谱」,点亮生成式 BI 价值灯塔
大数据市场正在稳步前进,生成式AI已成为厂商服务的重点方向,其发展离不开数据底座建设和数据工程管理,反过来AI也会帮助开发运维人员、业务人员和管理层更好地使用、查询数据。IDC调研数据显示,在生成式AI的驱动下,未来…...
HarmonyOS 应用程序包结构 (编译态)
不同类型的Module编译后会生成对应的HAP、HAR、HSP等文件,开发态视图与编译态视图的对照关系如下: 从开发态到编译态,Module中的文件会发生如下变更: ets目录:ArkTS源码编译生成.abc文件。resources目录:A…...
C# 程序结构
C#的程序结构大体可以分为: 命名空间、类名、Main方法、标识符及关键字语句注释 C# 文件的后缀为 .cs 以下创建一个HelloWorld的类,可以看一下 using System; namespace HelloWorldApplication //命名空间 {class HelloWorld //类名{static void …...
LLM 学习(二 完结 Multi-Head Attention、Encoder、Decoder)
文章目录 LLM 学习(二 完结 Multi-Head Attention、Encoder、Decoder)Self-Attention (自注意力机制)结构多头注意力 EncoderAdd & Norm 层Feed Forward 层 EncoderDecoder的第一个Multi-Head AttentionMasked 操作Teacher Fo…...
GET3D:从图像中学习的高质量3D纹理形状的生成模型
【摘要】 本文提出了GET3D,这是一种新的生成模型,能够生成具有任意拓扑结构的高质量3D纹理网格,可以直接被3D渲染引擎使用并在下游应用中立即使用。现有的3D生成模型要么缺乏几何细节,要么生成的网格拓扑受限,通常不支持纹理,或者在生成过程中使用神经渲染器,使得它们在…...
JmeterHttp请求头管理出现Unsupported Media Type问题解决
JmeterHttp请求头管理出现Unsupported Media Type问题解决 大多数的app与pc端压测的时候都会出现这种情况 当我们在jemter测试当中当中遇见Unsupported Media Type,有一种可能就是我们请求的网页的content-Type的类型与我们测试的时候的类型不一致 解决方法 可以添…...
Python 性能优化:从入门到精通的实用指南
Langchain系列文章目录 01-玩转LangChain:从模型调用到Prompt模板与输出解析的完整指南 02-玩转 LangChain Memory 模块:四种记忆类型详解及应用场景全覆盖 03-全面掌握 LangChain:从核心链条构建到动态任务分配的实战指南 04-玩转 LangChai…...
Vue23Web 基礎性拉滿的面試題(2025版)還沒更新完...
Vue2&3 基礎性1. 關於Vue2和Vue3生命週期的差別2. Vue2&3組件之間傳參不同點Vue2 傳遞與接收Vue3 傳遞與接收 (使用script setup語法糖)Vue3 傳遞與接收 (不使用script setup語法糖) 3. Vue2&3 keep-alive 組件Vue2 keep-aliveVue3 keep-alive 進階性爲什麽POST請求…...
Python基于Django的医用耗材网上申领系统【附源码、文档说明】
博主介绍:✌Java老徐、7年大厂程序员经历。全网粉丝12w、csdn博客专家、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和毕业项目实战✌ 🍅文末获取源码联系🍅 👇🏻 精彩专栏推荐订阅👇&…...
STM32使用无源蜂鸣器
1.1 介绍: 有源蜂鸣器:内部自带振荡源,将正负极接上直流电压即可持续发声,频率固定 无源蜂鸣器:内部不带振荡源,需要控制器提供振荡脉冲才可发声,调整提供振荡脉冲的频率,可发出不同…...
9.1go结构体
Go不是完全面向对象的,没有类的概念,所以结构体应该承担了更多的责任。 结构体定义 使用 type 和 struct 关键字定义: type Person struct { Name string Age int } 字段可以是任意类型,包括其他结构体或指针。 字段名以大写…...
Ubuntu20.04本地配置IsaacLab 4.2.0的G1训练环境(一)
Ubuntu20.04本地配置IsaacLab的G1训练环境(一) 配置Omniverse环境配置IsaacSim配置IsaacLab 写在前面,如果Ubuntu剩余空间低于60G,则空间不足,除非你不需要资产包。但资产包中却包含了G1模型、Go2模型等机器人模型和代…...
全星FMEA软件:汽车电子行业研发管理高效之选
全星FMEA软件:汽车电子行业研发管理高效之选 在汽车电子行业,FMEA(失效模式与影响分析)是确保产品质量和安全的关键工具。然而,传统的FMEA分析过程往往繁琐复杂,耗费大量时间和精力。 全星FMEA软件应运而生…...
AGI 之 【Dify】 之 使用 Docker 在 Windows 端本地部署 Dify 大语言模型(LLM)应用开发平台
AGI 之 【Dify】 之 使用 Docker 在 Windows 端本地部署 Dify 大语言模型(LLM)应用开发平台 目录 AGI 之 【Dify】 之 使用 Docker 在 Windows 端本地部署 Dify 大语言模型(LLM)应用开发平台 一、简单介绍 二、Docker 下载安…...
OpenCV计算摄影学(18)平滑图像中的纹理区域同时保留边缘信息函数textureFlattening()
操作系统:ubuntu22.04 OpenCV版本:OpenCV4.9 IDE:Visual Studio Code 编程语言:C11 算法描述 cv::textureFlattening 是 OpenCV 中用于图像处理的一个函数,旨在平滑图像中的纹理区域,同时保留边缘信息。该技术特别适…...
RLock类详细介绍、应用场景和示例代码
概述 org.redisson.api.RLock 是 Redisson 提供的 分布式可重入锁(类似 ReentrantLock),基于 Redis 实现,常用于 分布式环境 下的 并发控制。 1. RLock 详解 🔹 特点 基于 Redis 实现,支持 集群环境。可…...
【HeadFirst系列之HeadFirst设计模式】第16天之生成器模式(Builder Pattern):让对象构建更优雅!
🚀 生成器模式(Builder Pattern):让对象构建更优雅! “遇到复杂对象构建?试试生成器模式!” 在日常开发中,我们经常会遇到 创建对象属性过多、构造方法过长、可选参数混乱 的问题。这…...
Browser Use+DeepSeek的使用教程
browser-use webui 主要功能 提供了全新的网页界面,简单好用,方便操作。 支持更多大语言模型,比如 Gemini、OpenAI、Azure 等,还有最近爆火的国产大模型 DeepSeek,未来还会加更多。 支持用自己的浏览器,不用…...
“此电脑”中删除WPS云盘方法(百度网盘通用)
📣此方法适用于卸载WPS云盘后,WPS云盘图标依然在此电脑中显示的问题。 原理:通过注册来进行删除 步骤: WIN键R,打开运行窗口,输入regedit命令,来打开【注册表编辑器】; 从左侧,依…...
1. 树莓派上配置机器人环境(具身智能机器人套件)
1. 安装树莓派系统 镜像下载地址(windows/Mac/Ubuntu),安装Pi5. 2. 环境配置(登录Pi系统) 2.1 启用 SSH From the Preferences menu, launch Raspberry Pi Configuration. Navigate to the Interfaces tab. Select Enable…...
正则表达式(2)匹配规则
正则表达式的匹配规则定义了如何识别字符串中的特定模式。这些规则包括字符类匹配、元字符匹配、数量词、字符转义和分组。 字符类匹配 字符类匹配允许你指定一个字符集合,并匹配该集合中的任意单个字符。这是通过方括号 [] 来实现的。 简单字符类:[abc…...
Golang实践录:go发布版本信息收集
go发布版本信息收集。 背景 本文从官方、网络资料收罗有关go的发布历史概况。主要目的是能快速了解golang不同版本的变更。鉴于官方资料为英文,为方便阅读,使用工具翻译成中文,重要特性参考其它资料补充/修改。由于发布版本内容较多…...
go:windows环境下安装Go语言
1.下载 打开Go语言中文网下载页面: https://studygolang.com/dl 按照对应平台选择下载: https://studygolang.com/dl/golang/go1.19.2.windows-amd64.msi。 2.安装 双击下载好的文件 记下安装路径,删除末尾的反斜杠: C:\Program…...
遵义市招生管理信息系统的开发与实现
近年来,随着信息技术的发展,各类管理系统在教育领域得到广泛应用。本文将介绍如何开发一个基于Web的招生管理信息系统,以提高遵义市招生工作的效率和透明度。 系统架构设计 本系统采用典型的三层架构,包括表示层、业务逻辑层和数…...
Java 大视界 -- 区块链赋能 Java 大数据:数据可信与价值流转(84)
💖亲爱的朋友们,热烈欢迎来到 青云交的博客!能与诸位在此相逢,我倍感荣幸。在这飞速更迭的时代,我们都渴望一方心灵净土,而 我的博客 正是这样温暖的所在。这里为你呈上趣味与实用兼具的知识,也…...
Pytest自动化框架
目录 Pytest简单介绍 第一章:Pytest console命令 1.pytest -v 参数 -h 参数 其他一些参数:仅供参考 第二章. mark标记 pytest.mark.skip pytest.mark.skipif pytest.mark.xfail 编辑 pytest.mark.patametrize 单个参数 多个参数 直接…...
算法之 前缀和
文章目录 前缀和基础3427.变长子数组求和 前缀和与哈希表1524.和为奇数的子数组数目 距离和1685.有序数组中绝对值之和 前缀异或和1177.构建回文串检测 其他一维前缀和1310.子数组异或查询 二维前缀和1314.矩阵区域和 前缀和,就是定义pre[i] 为nums的前i个元素的和值…...
【Windows下Gitbook快速入门使用】
Windows下Gitbook快速入门使用 1 工具安装1.1 Node.js下载安装1.1 环境变量1.2 npm配置1.3 安装gitbook 2 gitbook使用2.1 gitbook 无法执行2.2 gitbook常用命令 Gitbook是一个软件,使用Git和Markdown来编排书本; GitBook helps you pushlish beautiful …...
python网络爬虫开发实战之基本库使用
目录 第二章 基本库的使用 2.1 urllib的使用 1 发送请求 2 处理异常 3 解析链接 4 分析Robots协议 2.2 requests的使用 1 准备工作 2 实例引入 3 GET请求 4 POST请求 5 响应 6 高级用法 2.3 正则表达式 1 实例引入 2 match 3 search 4 findall 5 sub 6 com…...
【hello git】git rebase、git merge、git stash、git cherry-pick
目录 一、git merge:保留了原有分支的提交结构 二、git rebase:提交分支更加整洁 三、git stash 四、git cherry-pick 共同点:将 一个分支的提交 合并到 到另一个上分支上去 一、git merge:保留了原有分支的提交结构 现有一个模型…...
deepseek、腾讯元宝deepseek R1、百度deepseekR1关系
分析与结论 区别与联系 技术基础与定制方向: DeepSeek官网R1版本:作为基础版本,通常保留通用性设计,适用于广泛的AI应用场景(如自然语言处理、数据分析等)。其优势在于技术原生性和官方直接支持。腾讯元宝…...
DeepSeek + 沉浸式翻译 打造智能翻译助手
本文详细介绍如何使用 DeepSeek API 沉浸式翻译插件打造个性化翻译助手。 一、DeepSeek API 配置 基础配置 API 基础地址:https://api.deepseek.com需要申请 API Key支持与 OpenAI SDK 兼容的调用方式 可用模型 deepseek-chat:已升级为 DeepSeek-V3&am…...
iOS 聊天 IM 消息收发管理工具
iOS 聊天 IM 消息收发管理工具 连续疯狂加班告一段落,趁着离职前夕的空闲时间,整理一下重构相关的文档。之前写过两篇文章 iOS 客户端 IM 以及列表 UI 框架 、iOS 客户端 IM 消息卡片插件化,突然发现时间过的真的很快,这都已经是…...
从零开始学习Slam--数学概念
正交矩阵 矩阵的转置等于它的逆矩阵,这样的矩阵称之为正交矩阵 即: Q T Q I Q^T Q I QTQI, 这样的矩阵列向量都是单位向量且两两正交。 旋转矩阵属于特殊的正交群,即SO(n),这里n通常是3,所以SO(3)就是…...
3.使用ElementUI搭建侧边栏及顶部栏
1. 安装ElementUI ElementUI是基于 Vue 2.0 的桌面端组件库。使用之前,需要在项目文件夹中安装ElementUI,在终端中输入以下命令,进行安装。 npm i element-ui -S并在main.js中引入ElementUI 2. 使用elmentUI组件进行页面布局 2.1 清空原…...
pandas-基础(数据结构及文件访问)
1 Pandas的数据结构 1.1 Series 特点:一维的数据型对象,包含一个值序列和数据标签(即索引) 创建Series: pandas.Series(dataNone, indexNone, dtypeNone, nameNone, copyFalse, fastpathFalse) 参数说明: data&a…...
windows下使用msys2编译ffmpeg
三种方法: 1、在msys2中使用gcc编译 2、在msys2中使用visual studio编译(有环境变量) 3、在msys2中使用visual studio编译(无环境变量) 我的环境: 1、msys2-x86_64-20250221 2、vs2015 3、ffmpeg-7.1…...
Linux驱动开发(1.基础创建)
序言:从高层逻辑到底层硬件的回归 在当今的软件开发中,我们习惯于用高级语言构建抽象层——通过框架、库和云服务快速实现功能。这种“软逻辑”的便利性让开发效率倍增,却也逐渐模糊了我们对计算机本质的认知:一切代码终将落地为…...
长短期记忆网络(LSTM)学习指南
长短期记忆网络(LSTM)学习指南 1. 定义和背景 长短期记忆网络(Long Short-Term Memory, LSTM)是一种递归神经网络(RNN)的变体,旨在解决传统RNN在处理长期依赖关系时遇到的梯度消失或爆炸问题。…...
蓝耘智算平台助力阿里万相2.1:文生图创作攻略
声明:非广告,为用户体验文章 目录 引言 一、认识蓝耘智算平台与阿里万相 2.1 (一)蓝耘智算平台概述 (二)阿里万相 2.1 文生图技术解析 二、蓝耘智算平台注册与环境准备 (一)注…...
HarmonyOS ArkTS声明式UI开发实战教程
引言:为何选择ArkTS? 在HarmonyOS生态快速发展的当下,ArkTS作为新一代声明式UI开发框架,正在引发移动应用开发范式的变革。笔者曾在多个跨平台框架开发中经历过"命令式编程之痛",直到接触ArkTS后才发现&…...
电脑总显示串口正在被占用处理方法
1.现象 在嵌入式开发过程中,有很多情况下要使用串口调试,其中485/422/232转usb串口是非常常见的做法。 根据协议,接口芯片不同,需要安装对应的驱动程序,比如ch340,cp2102,CDM212364等驱动。可…...
linux 安装nvidia 驱动所有发行版通用
之前有写过Ubuntu安装驱动的手册: https://blog.csdn.net/qq_50247813/article/details/146062785 到官网寻找合适的驱动: https://www.nvidia.cn/drivers/lookup 直接选择 linux 64-bit 适用于所有x86构架的linux,arm需要选择arm架构。 这次…...
PHP之数组
在你有别的编程语言的基础下,你想学习PHP,可能要了解的一些关于数组的信息。 PHP中的数组不用指定数据类型,同时索引不一定是数字。 与其说是数组,其实更像map。 创建和输出 $arr array(1, 2, 3); $arr1 [1, 2, 3]; var_dump(…...
【js逆向】图灵爬虫练习平台 第十五题
地址:aHR0cHM6Ly9zdHUudHVsaW5ncHl0b24uY24vcHJvYmxlbS1kZXRhaWwvMTUv 不一样的两个值 hook cookie两次执行出现密文v值 最主要是这个函数 直接扣整个js代码,然后补环境即可,可以参考 同花顺那篇文章,有讲 关键代码 var rt;!funct…...