13.QT-ffmpeg4.4顯示視訊影象
阿新 • • 發佈:2020-11-03
在上章12.QT-通過QOpenGLWidget顯示YUV畫面,通過QOpenGLTexture紋理渲染YUV,我們學會了如何硬解碼,但是ffmpeg影象解碼過程還不知道.所以本章主要分析一下FFmpeg視訊影象解碼過程,只有真正瞭解了FFmpeg處理的基本流程,研讀 ffmpeg 原始碼才能事半功倍.筆者使用的ffmpeg版本為4.4.
1.FFmpeg庫簡介
FFmpeg常用庫如下:
注意:在新版本中,AVCodecContext已經和AVStream[]做了改進,已經分解開了,比如推流的時候,接完封裝就傳送了,沒必要解碼,而遠端使用者才是做接收,解碼流程。
- avcodec :用於各種型別聲音/影象編解碼(最重要的庫),該庫是音視訊編解碼核心
- avformat:用於各種音視訊封裝格式的生成和解析,包括獲取解碼所需資訊以生成解碼上下文結構和讀取音視訊幀等功能;音視訊的格式解析協議,為 avcodec分析碼流提供獨立的音訊或視訊碼流源
- avfilter :濾鏡特效處理, 如寬高比 裁剪 格式化 非格式化 伸縮。
- avdevice:各種硬體採集裝置的輸入輸出。
- avutil:工具庫,包括算數運算字元操作(大部分庫都需要這個庫的支援)
- postproc:用於後期效果處理;音視訊應用的後處理,如影象的去塊效應。
- swresample:音訊取樣資料格式轉換。
- swscale:視訊畫素資料格式轉換、如 rgb565、rgb888 等與 yuv420 等之間轉換。
- AVFormatContext :儲存視音訊封裝格式(flv,mp4,rmvb,avi)中包含的所有資訊.通過avformat_open_input()來分配空間並開啟檔案,結束時通過avformat_close_input()來釋放.
- AVIOContext :存在AVFormatContext ->pb中,用來儲存檔案資料的緩衝區,並通過相關標記成員來實現檔案讀寫操作,其中的opaque 成員這是用於關聯 URLContext 結構
- URLContext :存在AVIOContext->opaque中,表示程式執行的當前廣義輸入檔案使用的 context,著重於所有廣義輸入檔案共有的屬性(並且是在程式執行時才能確定其值)和關聯其他結構的欄位.
- URLProtocol :存在URLContext-> prot中,音視訊輸入檔案型別(rtp,rtmp,file, rtmps, udp等),比如file型別的結構體初始化如下:
- AVInputFormat :存在AVFormatContext ->iformat中, 儲存視訊/音訊流的封裝格式(flv、mkv、avi等),其中name成員可以檢視什麼格式
- AVStream:視音訊流,存在AVFormatContext->streams[i], 每個AVStream包含了一個流,一般預設兩個(0為視訊流,1為音訊流).通過avformat_find_stream_info()來獲取.
- AVCodecContext:用來儲存解碼器上下文結構體(儲存解碼相關資訊,主要儲存在程式執行時才能確定的資料),每個AVCodecContext包含了一個AVCodec解碼器(比如h.264解碼器、mpeg4解碼器等),老版本存在AVFormatContext->streams[i] ->codec中,新版本則需要通過avcodec_alloc_context3()和avcodec_parameters_to_context()函式來構造AVCodecContext.
- AVCodec : 存在AVCodecContext->codec中,指定具體的解碼器(比如h.264解碼器、mpeg4解碼器等),。
- AVPacket :解碼前的音訊/視訊資料,需要使用者自己分配空間後才通過av_read_frame()來獲取一幀未解碼的資料
- AVFrame :解碼後的音訊/視訊資料,比如解碼視訊資料則通過avcodec_receive_frame()來獲取一幀AVFrame資料
- 如果avformat_open_input()開啟的是http,rtsp,rtmp,mms網路相關的流媒體,則需要在開頭呼叫avformat_network_init()來為提供支援.
- AVPacket必須使用av_packet_allc()建立好空間後.才能供給fimpeg進行獲取解碼前幀資料,由於解碼前幀資料大小是不固定的(比如I幀資料量最大)所以ffmpeg會在AVPacket的成員裡動態進行建立空間.
- 並且我們每一次使用完AVPacket後(再次呼叫av_read_frame()讀取新幀之前),必須要通過av_packet_unref()引用技術對AVPacket裡的成員來手動清理.
- 解碼完成或者退出播放後,還要呼叫av_packet_free()來釋放AVPacket本身.
- AVFrame必須使用av_frame_alloc()來分配。注意,這只是分配AVFrame本身,緩衝區的資料(解碼成功後的資料)必須通過其他途徑被管理.
- 因為AVFrame通常只分配一次,然後多次複用來儲存不同型別的資料,複用的時候需要呼叫av_frame_unref()將其重置到它前面的原始清潔狀態.
- 注意呼叫avcodec_receive_frame()時會自動引用減1後再獲取frame,所以解碼過程中無需每次呼叫
- 釋放的時候必須用av_frame_free()釋放。
SwsContext *sws_getContext(int srcW, int srcH, enum AVPixelFormat srcFormat, int dstW, int dstH, enum AVPixelFormat dstFormat, int flags, SwsFilter *srcFilter, SwsFilter *dstFilter, const double *param); // sws_getContext通過引數影象轉換格式以及解析度來初始化SwsContext結構體 //srcW, srcH, srcFormat定義輸入影象資訊(寬、高、顏色空間(畫素格式)) //dstW, dstH, dstFormat定義輸出影象資訊(寬、高、顏色空間(畫素格式,比如AV_PIX_FMT_RGB32))。 // flags:轉換演算法(只有當輸入輸出影象大小不同時有效,速度越快精度越差,一般選擇SWS_BICUBIC) // *srcFilter,* dstFilter: 定義輸入/輸出影象濾波器資訊,一般輸入NULL // param定義特定縮放演算法需要的引數,預設為NUL //比如sws_getContext(w, h, AV_PIX_FMT_YUV420P, 2*w, 2*h, AV_PIX_FMT_RGB32, SWS_BICUBIC, NULL, NULL, NULL); 表示YUV420p-> RGB32,並放大4倍 struct SwsContext *sws_getCachedContext(struct SwsContext *context, int srcW, int srcH, enum AVPixelFormat srcFormat, int dstW, int dstH, enum AVPixelFormat dstFormat, int flags, SwsFilter *srcFilter, SwsFilter *dstFilter, const double *param); //檢查context引數是否可以重用,否則重新分配一個新的SwsContext。 比sws_getContext()多了一個context引數 //比如我們當前傳入的srcW、srcH、srcFormat、dstW、dstH、dstFormat、param引數和之前的context一致,則就直接返回複用. //否則的話,釋放context,並重新初始化一個新的SwsContext返回. //如果要轉換的視訊尺寸和格式始終不變(期間不更改),一般使用sws_getContext() int sws_scale(struct SwsContext *c, const uint8_t * const srcSlice[], const int srcStride[], int srcSliceY, int srcSliceH, uint8_t *const dst[], const int dstStride[]); // sws_scale用來進行視訊畫素格式和解析度的轉換.返回值小於0則表示轉換失敗 //注意:使用之前需要呼叫sws_getContext()來設定畫素轉換格式,並SwsContext結構體,並且用後還要呼叫sws_freeContext()來釋放SwsContext結構體. //*c:轉換格式的上下文,裡面儲存了要轉換的格式和解析度 //*srcSlice[]:源影象資料,也就是解碼後的AVFrame-> data[]陣列成員,需要注意的是裡面的每一行畫素並不等於圖片的寬度 // srcStride[]: input的 strid,每一列影象的byte數,也就是AVFrame->linesize成員 // srcSliceY, srcSliceH: srcSliceY是起始位置,srcSliceH是處理多少行,如果srcSliceY=0,srcSliceH=height,表示一次性處理完整個影象。也可以多執行緒並行加快速度顯示,例如第一個執行緒處理 [0, h/2-1]行,第二個執行緒處理 [h/2, h-1]行。 // dst[]:轉換後的影象資料,也就是要轉碼後的另一個AVFrame-> data[]成員 // dstStride[]: input的 strid,每一列影象的byte數,也就是要轉碼後的另一個AVFrame->linesize成員 void sws_freeContext(struct SwsContext *swsContext); //釋放swsContext結構體,避免記憶體洩漏,釋放後用戶還需要手動置NULL7.原始碼
#include "ffmpegtest.h" #include <QTime> #include <QDebug> extern "C"{ #include <libavcodec/avcodec.h> #include <libavformat/avformat.h> #include <libswscale/swscale.h> #include <libavutil/imgutils.h> } FfmpegTest::FfmpegTest(QWidget *parent) : QWidget(parent) { ui->setupUi(this); } void Delay(int msec) { QTime dieTime = QTime::currentTime().addMSecs(msec); while( QTime::currentTime() < dieTime ) { QCoreApplication::processEvents(QEventLoop::AllEvents, 100); } } void debugErr(QString prefix, int err) //根據錯誤編號獲取錯誤資訊並列印 { char errbuf[512]={0}; av_strerror(err,errbuf,sizeof(errbuf)); qDebug()<<prefix<<":"<<errbuf; } void FfmpegTest::on_pushButton_clicked() { AVFormatContext *pFormatCtx; int videoindex; AVCodecContext *pCodecCtx; AVCodec *pCodec; AVFrame *pFrame, *pFrameRGB; unsigned char *out_buffer; AVPacket *packet; int ret; struct SwsContext *img_convert_ctx; char filepath[] = "G:\\testvideo\\ds.mov"; avformat_network_init(); //載入socket庫以及網路加密協議相關的庫,為後續使用網路相關提供支援 pFormatCtx = avformat_alloc_context(); //初始化AVFormatContext 結構 ret=avformat_open_input(&pFormatCtx, filepath, NULL, NULL);//開啟音視訊檔案並初始化AVFormatContext結構體 if (ret != 0) { debugErr("avformat_open_input",ret); return ; } ret=avformat_find_stream_info(pFormatCtx, NULL);//根據AVFormatContext結構體,來獲取視訊上下文資訊,並初始化streams[]成員 if (ret != 0) { debugErr("avformat_find_stream_info",ret); return ; } videoindex = -1; videoindex = av_find_best_stream(pFormatCtx, AVMEDIA_TYPE_VIDEO, -1, -1, NULL, 0);//根據type引數從ic-> streams[]裡獲取使用者要找的流,找到成功後則返回streams[]中對應的序列號,否則返回-1 if (videoindex == -1){ printf("Didn't find a video stream.\n"); return ; } qDebug()<<"視訊寬度:"<<pFormatCtx->streams[videoindex]->codecpar->width; qDebug()<<"視訊高度:"<<pFormatCtx->streams[videoindex]->codecpar->height; qDebug()<<"視訊位元速率:"<<pFormatCtx->streams[videoindex]->codecpar->bit_rate; ui->label->resize(pFormatCtx->streams[videoindex]->codecpar->width,pFormatCtx->streams[videoindex]->codecpar->height); pCodec = avcodec_find_decoder(pFormatCtx->streams[videoindex]->codecpar->codec_id);//通過解碼器編號來遍歷codec_list[]陣列,來找到AVCodec pCodecCtx = avcodec_alloc_context3(pCodec); //構造AVCodecContext ,並將vcodec填入AVCodecContext中 avcodec_parameters_to_context(pCodecCtx, pFormatCtx->streams[videoindex]->codecpar); //初始化AVCodecContext ret = avcodec_open2(pCodecCtx, NULL,NULL); //開啟解碼器 if (ret != 0) { debugErr("avcodec_open2",ret); return ; } //構造AVFrame,而影象資料空間大小則需通過av_malloc動態分配(因為不知道視訊寬高大小) pFrame = av_frame_alloc(); pFrameRGB = av_frame_alloc(); //建立動態記憶體,建立儲存影象資料的空間 //av_image_get_buffer_size():根據畫素格式、影象寬、影象高來獲取一幀影象需要的大小(第4個引數align:表示多少位元組對齊,一般填1,表示以1位元組為單位) //av_malloc():給out_buffer分配一幀RGB32影象顯示的大小 out_buffer = (unsigned char *)av_malloc(av_image_get_buffer_size(AV_PIX_FMT_RGB32, pCodecCtx->width, pCodecCtx->height, 1)); //通過av_image_fill_arrays和out_buffer來初始化pFrameRGB裡的data指標和linesize指標.linesize是每個影象的寬大小(位元組數)。 av_image_fill_arrays(pFrameRGB->data, pFrameRGB->linesize, out_buffer, AV_PIX_FMT_RGB32, pCodecCtx->width, pCodecCtx->height, 1); packet = (AVPacket *)av_malloc(sizeof(AVPacket)); //初始化SwsContext結構體,設定畫素轉換格式規則,將pCodecCtx->pix_fmt格式轉換為AV_PIX_FMT_RGB32格式 img_convert_ctx = sws_getContext(pCodecCtx->width, pCodecCtx->height, pCodecCtx->pix_fmt, pCodecCtx->width, pCodecCtx->height, AV_PIX_FMT_RGB32, SWS_BICUBIC, NULL, NULL, NULL); //av_read_frame讀取一幀未解碼的資料,可能還有幾幀frame未顯示,我們需要在末尾通過avcodec_send_packet()傳入NULL來將最後幾幀取出來 while (av_read_frame(pFormatCtx, packet) >= 0){ //如果是視訊資料 if (packet->stream_index == videoindex){ //解碼一幀視訊資料 ret = avcodec_send_packet(pCodecCtx, packet); av_packet_unref(packet); if (ret != 0) { debugErr("avcodec_send_packet",ret); continue ; } //呼叫avcodec_receive_frame()時會自動引用減1後再獲取frame,所以解碼過程中無需每次呼叫av_frame_unref()來重置AVFrame while( avcodec_receive_frame(pCodecCtx, pFrame) == 0){ qDebug()<<"視訊幀型別(I(1)、B(3)、P(2)):"<<pFrame->pict_type; //進行視訊畫素格式和解析度的轉換 sws_scale(img_convert_ctx, (const unsigned char* const*)pFrame->data, pFrame->linesize, 0, pCodecCtx->height, pFrameRGB->data, pFrameRGB->linesize); QImage img((uchar*)pFrameRGB->data[0],pCodecCtx->width,pCodecCtx->height,QImage::Format_ARGB32); ui->label->setPixmap(QPixmap::fromImage(img)); Delay(40); } } } av_packet_free(&packet); av_frame_free(&pFrameRGB); av_frame_free(&pFrame); sws_freeContext(img_convert_ctx); img_convert_ctx=NULL; avcodec_close(pCodecCtx); avformat_close_input(&pFormatCtx); }其中幾個引數較多的函式宣告解釋如下所示:
int avformat_open_input(AVFormatContext **ps, const char *filename, ff_const59 AVInputFormat *fmt, AVDictionary **options); //開啟一個音視訊檔案,並初始化AVFormatContext **ps(如果初始化失敗,則釋放ps,並返回非0值) //ps:要初始化的AVFormatContext*的指標,如果ps指向NULL,則該函式內部會呼叫avformat_alloc_context()來自動分配空間。 //*url:傳入的地址, 支援http,RTSP,以及普通的本地檔案,初始化後地址會存放在AVFormatContext下的url成員中 //fmt: 指定輸入的封裝格式。預設為NULL,由FFmpeg自行探測。 // options: 其它引數設定,如果開啟的是本地檔案一般為NULL,比如開啟流媒體視訊時,通過av_dict_set(&pOptions, "max_delay", "200", 0)來設定網路延時最大200毫秒,具體參考libavformat/options_table.h下的 avcodec_options陣列 //返回值:0成功,非0失敗,失敗後,則可以通過av_strerror()獲取失敗原因。 int avformat_find_stream_info(AVFormatContext *ic, AVDictionary **options); //根據AVFormatContext結構體,來獲取視訊上下文資訊,並初始化streams[]成員(如果初始化失敗,則釋放ps,並返回非0值) //ic: AVFormatContext。 //options:其它引數設定,一般為NULL int av_find_best_stream(AVFormatContext *ic, enum AVMediaType type, int wanted_stream_nb, int related_stream, AVCodec **decoder_ret, int flags); //根據type引數從ic-> streams[]裡獲取使用者要找的流,找到成功後則返回streams[]中對應的序列號 //ic: AVFormatContext結構體控制代碼 //type:要找的流引數,比如: AVMEDIA_TYPE_VIDEO,AVMEDIA_TYPE_AUDIO,AVMEDIA_TYPE_SUBTITLE等 wanted_stream_nb: 使用者希望請求的流號,設為-1用於自動選擇 related_stream: 試著找到一個相關的流(比如多路視訊時),一般設為-1,表示不需要 decoder_ret:如果非空,則返回所選流的解碼器(相當於呼叫avcodec_find_decoder()函式) flags:未定義
int avcodec_open2(AVCodecContext *avctx, const AVCodec *codec, AVDictionary **options); //通過avctx開啟解碼器, AVCodecContext存在AVFormatContext->streams[i] ->codec中 //如果在這之前呼叫了avcodec_alloc_context3(vcodec)初始化了avctx,那麼codec可以填NULL. // options: 其它引數設定,具體參考libavformat/options_table.h下的 avcodec_options陣列