在上章12.QT-通過QOpenGLWidget顯示YUV畫面,通過QOpenGLTexture紋理渲染YUV,我們學會了如何硬解碼,但是ffmpeg圖像解碼過程還不知道.所以
本章主要分析一下FFmpeg視頻圖像解碼過程,只有真正了解了FFmpeg處理的基本流程,研讀 ffmpeg 源代碼才能事半功倍.筆者使用的ffmpeg版本為4.4.
1.FFmpeg庫簡介
FFmpeg常用庫如下:
- avcodec : 用於各種類型聲音/圖像編解碼(最重要的庫),該庫是音視頻編解碼核心
- avformat:用於各種音視頻封裝格式的生成和解析,包括獲取解碼所需信息以生成解碼上下文結構和讀取音視頻幀等功能;音視頻的格式解析協議,為 avcodec分析碼流提供獨立的音頻或視頻碼流源
- avfilter : 濾鏡特效處理, 如寬高比 裁剪 格式化 非格式化 伸縮。
- avdevice:各種硬件采集設備的輸入輸出。
- avutil:工具庫,包括算數運算字符操作(大部分庫都需要這個庫的支持)
- postproc:用於后期效果處理;音視頻應用的后處理,如圖像的去塊效應。
- swresample:音頻采樣數據格式轉換。
- swscale:視頻像素數據格式轉換、如 rgb565、rgb888 等與 yuv420 等之間轉換。
2.FFmpeg結構體
ffmpeg結構體關系圖如下所示,用的雷神圖,可以看到很多結構體都與AVFormatContext有關系,所以很多函數也經常使用AVFormatContext作為參數使用(FFmpeg舊版本):
注意:在新版本中,AVCodecContext已經和AVStream[]做了改進,已經分解開了,比如推流的時候,接完封裝就發送了,沒必要解碼,而遠端用戶才是做接收,解碼流程。
- 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數據
上面部分的結構體封裝參考連接如下:
剩下的還有AVIOContext、URLContext、URLProtocol 、結構體未分析,后續使用時補充
3.解碼流程(針對只顯示一個簡單的視頻畫面,后續同步聲音)
如下圖所示,針對ffmpeg4.4版本:
- 如果avformat_open_input()打開的是http,rtsp,rtmp,mms網絡相關的流媒體,則需要在開頭調用avformat_network_init()來為提供支持.
4.AVPacket使用注意
- AVPacket必須使用av_packet_allc()創建好空間后.才能供給fimpeg進行獲取解碼前幀數據,由於解碼前幀數據大小是不固定的(比如I幀數據量最大)所以ffmpeg會在AVPacket的成員里動態進行創建空間.
- 並且我們每一次使用完AVPacket后(再次調用av_read_frame()讀取新幀之前),必須要通過av_packet_unref()引用技術對AVPacket里的成員來手動清理.
- 解碼完成或者退出播放后,還要調用av_packet_free()來釋放AVPacket本身.
5.AVFrame使用注意
- AVFrame必須使用av_frame_alloc()來分配。注意,這只是分配AVFrame本身,緩沖區的數據(解碼成功后的數據)必須通過其他途徑被管理.
- 因為AVFrame通常只分配一次,然后多次復用來保存不同類型的數據,復用的時候需要調用av_frame_unref()將其重置到它前面的原始清潔狀態.
- 注意調用avcodec_receive_frame()時會自動引用減1后再獲取frame,所以解碼過程中無需每次調用
- 釋放的時候必須用av_frame_free()釋放。
6.SwsContext結構體介紹(轉換格式與尺寸)
由於我們是軟解,需要的圖像格式是RGB類型的,所以我們需要將YUV格式進行轉換,而ffmpeg庫中提供了一個SwsContext類,該類主要用於圖片像素格式的轉換, 圖片的尺寸改變.
SwsContext常用相關函數如下所示:
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結構體,避免內存泄漏,釋放后用戶還需要手動置NULL
7.源碼
#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數組
