一.項目介紹:
本項目采用的是易百納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的設置需要調整