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.pswp.cn/pingmian/71652.shtml
繁體地址,請注明出處:http://hk.pswp.cn/pingmian/71652.shtml
英文地址,請注明出處:http://en.pswp.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;成了我在大模型和具身的原始技術積累 如今一轉眼…