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.mzph.cn/pingmian/71652.shtml

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

相关文章

13.IIC-EEPROM(AT24C02)

1.为什么需要EEPROM? 在单片机开发中&#xff0c;断电数据保存是常见的需求。例如&#xff0c;智能家居设备的用户设置、电子秤的校准参数等都需要在断电后仍能保留。AT24C02作为一款IIC接口的EEPROM芯片&#xff0c;具备以下优势&#xff1a; 非易失性存储&#xff1a;断电后…

ubuntu22.04安装P104-100一些经验(非教程)

一、版本&#xff1a; 系统&#xff1a;ubuntu-22.04.5-desktop-amd64.iso Nvidia 驱动&#xff1a;NVIDIA-Linux-x86_64-570.124.04.run。官网下载即可 二、经验 1、通用教程⭐ 直接关键词搜“ubuntu p104”会有一些教程&#xff0c;比如禁用nouveau等 安装参考&#xff1a…

TCP7680端口是什么服务

WAF上看到有好多tcp7680端口的访问信息 于是上网搜索了一下&#xff0c;确认TCP7680端口是Windows系统更新“传递优化”功能的服务端口&#xff0c;个人理解应该是Windows利用这个TCP7680端口&#xff0c;直接从内网已经具备更新包的主机上共享下载该升级包&#xff0c;无需从微…

OSI七大模型 --- 发送邮件

我想通过电子邮件发送一张照片给我的朋友。从我开始写邮件到发送成功&#xff0c;按照这个顺序讲一下我都经历了OSI模型的哪一层&#xff0c;对应的层使用了什么样的协议&#xff1f; 完整流程示例&#xff08;补充物理层细节&#xff09; 假设你通过Wi-Fi发送邮件&#xff1a…

LINUX网络基础 [一] - 初识网络,理解网络协议

目录 前言 一. 计算机网络背景 1.1 发展历程 1.1.1 独立模式 1.1.2 网络互联 1.1.3 局域网LAN 1.1.4 广域网WAN 1.2 总结 二. "协议" 2.1 什么是协议 2.2 网络协议的理解 2.3 网络协议的分层结构 三. OSI七层模型&#xff08;理论标准&#xff09; …

【LLms】关键词提取

1. 停用词 在文本处理和信息检索领域&#xff0c;停用词&#xff08;Stop Words&#xff09;是指在文本中出现频率较高&#xff0c;但通常不包含实际语义信息或对语义理解贡献较小的词汇。这些词汇通常是一些常见的功能词&#xff0c;如冠词、介词、连词、代词、感叹词、助动词…

1998-2022年各地级市三次产业占比/地级市国内生产总值构成/地级市第一产业占比、第二产业占比、第三产业占比数据(全市)

1998-2022年各地级市三次产业占比/地级市国内生产总值构成/地级市第一产业占比、第二产业占比、第三产业占比数据&#xff08;全市&#xff09; 1、时间&#xff1a;1998-2022年 2、指标&#xff1a;第一产业占比、第二产业占比、第三产业占比 3、来源&#xff1a;城市统计年…

基于STM32的简易出租车计费设计(Proteus仿真+程序+设计报告+原理图PCB+讲解视频)

这里写目录标题 1.主要功能资料下载链接&#xff1a;2.仿真3. 程序4. 原理图PCB5. 实物图6. 设计报告7. 下载链接 基于STM32的简易出租车计费设计(Proteus仿真程序设计报告原理图PCB讲解视频&#xff09; 仿真图proteus 8.9 程序编译器&#xff1a;keil 5 编程语言&#xff1…

HAL库启动ADC的三个函数的区别

HAL_ADC_Start 应该是启动ADC转换的最基本函数。只是启动一次转换&#xff0c;然后需要用户自己去查询转换是否完成&#xff0c;或者可能只是单次转换。比如&#xff0c;当调用这个函数后&#xff0c;ADC开始转换&#xff0c;但程序需要不断检查某个标志位来看转换是否完成&am…

EXIT原理和使用

要用到的控制器NVIC(中断总控制器)、EXIT&#xff08;外部中断控制器&#xff09; (EXIT是NVIC是下属) GPIO外部中断简图 EXIT的基本概念 EXIT主要特性 EXTI工作原理框图&#xff08;从输入线开始看&#xff09; 6个寄存器 EXTI和IO的映射关系 AFIO简介 EXTI与IO对应关系 如…

经典核密度估计(Kernel Density Estimation):从直觉到数学

经典核密度估计&#xff08;Kernel Density Estimation&#xff09;&#xff1a;从直觉到数学 作为一名在大模型时代进入深度学习领域的研究者&#xff0c;你可能对 Transformer、扩散模型等现代技术驾轻就熟。然而&#xff0c;在阅读一些生成模型的文献&#xff08;如 Explic…

Halcon 算子 一维码检测识别、项目案例

首先我们要明白码的识别思路 把窗口全部关闭读取新的图片图像预处理创建条码模型设置模型参数搜索模型获取条码结果显示条码结果 图像预处理和条码增强 对比度太低&#xff1a; scale_image&#xff08;或使用外部程序scale_image_range&#xff09;,增强图像的对比度图像模糊…

vue-cli3+vue2+elementUI+avue升级到vite+vue3+elementPlus+avue总结

上一个新公司接手了一个vue-cli3vue2vue-router3.0elementUI2.15avue2.6的后台管理项目&#xff0c;因为vue2在2023年底已经不更新维护了&#xff0c;elementUI也只支持到vue2&#xff0c;然后总结了一下vue3的优势&#xff0c;最后批准升级成为了vitevue3vue-router4.5element…

SpringBoot实战(三十五)微服务集成OAuth2.0(UAA)

目录 一、知识回顾1.1 什么是 OAuth2 协议&#xff1f;1.2 OAuth2 的4个角色1.3 OAuth2 的3种令牌1.4 OAuth2 的5种认证方式1.5 OAuth2 内置接口地址 二、UAA介绍2.1 概述2.2 UAA的主要功能2.3 UAA 的应用场景 三、微服务集成3.1 集成示例介绍3.2 集成测试 一、知识回顾 在进行…

红果短剧安卓+IOS双端源码,专业短剧开发公司

给大家拆解一下红果短剧/河马短剧&#xff0c;这种看光解锁视频&#xff0c;可以挣金币的短剧APP。给大家分享一个相似的短剧APP源码&#xff0c;这个系统已接入穿山甲广告、百度广告、快手广告、腾讯广告等&#xff0c;类似红果短剧的玩法&#xff0c;可以看剧赚钱&#xff0c…

从0开始的操作系统手搓教程23:构建输入子系统——实现键盘驱动1——热身驱动

目录 所以&#xff0c;键盘是如何工作的 说一说我们的8042 输出缓冲区寄存器 状态寄存器 控制寄存器 动手&#xff01; 注册中断 简单整个键盘驱动 Reference ScanCode Table 我们下一步就是准备进一步完善我们系统的交互性。基于这个&#xff0c;我们想到的第一个可以…

百度SEO关键词布局从堆砌到场景化的转型指南

百度SEO关键词布局&#xff1a;从“堆砌”到“场景化”的转型指南 引言 在搜索引擎优化&#xff08;SEO&#xff09;领域&#xff0c;关键词布局一直是核心策略之一。然而&#xff0c;随着搜索引擎算法的不断升级和用户需求的多样化&#xff0c;传统的“关键词堆砌”策略已经…

Python ❀ Unix时间戳转日期或日期转时间戳工具分享

设计一款Unix时间戳和日期转换工具&#xff0c;其代码如下&#xff1a; from datetime import datetimeclass Change_Date_Time(object):def __init__(self, date_strNone, date_numNone):self.date_str date_strself.date_num date_num# 转时间戳def datetime2timestamp(s…

【目标检测】【NeuralPS 2023】Gold-YOLO:通过收集与分发机制实现的高效目标检测器

Gold-YOLO&#xff1a; Efficient Object Detector via Gather-and-Distribute Mechanism Gold-YOLO&#xff1a;通过收集与分发机制实现的高效目标检测器 0.论文摘要 在过去的几年中&#xff0c;YOLO系列模型已成为实时目标检测领域的领先方法。许多研究通过修改架构、增强数…

π0源码解析——一个模型控制7种机械臂:对开源VLA sota之π0源码的全面分析,含我司的部分落地实践

前言 ChatGPT出来后的两年多&#xff0c;也是我疯狂写博的两年多(年初deepseek更引爆了下)&#xff0c;比如从创业起步时的15年到后来22年之间 每年2-6篇的&#xff0c;干到了23年30篇、24年65篇、25年前两月18篇&#xff0c;成了我在大模型和具身的原始技术积累 如今一转眼…