學習FFmpeg API – 解碼視頻

原文地址: 地址不詳

視頻播放過程

首先簡單介紹以下視頻文件的相關(guān)知識症副。我們平時看到的視頻文件有許多格式贞铣,比如 avi, mkv窍奋, rmvb酱畅, mov圣贸, mp4等等,這些被稱為\color{blue}{[容器][Container]}, 不同的容器格式規(guī)定了其中音視頻數(shù)據(jù)的組織方式(也包括其他數(shù)據(jù)用含,比如字幕等)帮匾。容器中一般會封裝有視頻和音頻軌,也稱為視頻流(stream)和音頻 流缸夹,播放視頻文件的第一步就是根據(jù)視頻文件的格式虽惭,解析(demux)出其中封裝的視頻流蛇尚、音頻流以及字幕(如果有的話),解析的數(shù)據(jù)讀到包 (packet)中匆笤,每個包里保存的是視頻幀(frame)或音頻幀炮捧,然后分別對視頻幀和音頻幀調(diào)用相應的解碼器(decoder)進行解碼惦银,比如使用 H.264編碼的視頻和MP3編碼的音頻,會相應的調(diào)用H.264解碼器和MP3解碼器傀蚌,解碼之后得到的就是原始的圖像(YUV or RGB)和聲音(PCM)數(shù)據(jù)善炫,然后根據(jù)同步好的時間將圖像顯示到屏幕上,將聲音輸出到聲卡窜醉,最終就是我們看到的視頻艺谆。

FFmpeg的API就是根據(jù)這個過程設(shè)計的静汤,因此使用FFmpeg來處理視頻文件的方法非常直觀簡單。下面就一步一步介紹從視頻文件中解碼出圖片的過程藤抡。

聲明變量

首先定義整個過程中需要使用到的變量:

int main(int argc, const char *argv[])
{
  AVFormatContext *pFormatCtx = NULL;
  int             i, videoStream;
  AVCodecContext  *pCodecCtx;
  AVCodec         *pCodec;
  AVFrame         *pFrame;
  AVFrame         *pFrameRGB;
  AVPacket        packet;
  int             frameFinished;
  int             numBytes;
  uint8_t         *buffer;
}
  • AVFormatContext:保存需要讀入的文件的格式信息缠黍,比如流的個數(shù)以及流數(shù)據(jù)等
  • AVCodecCotext:保存了相應流的詳細編碼信息药蜻,比如視頻的寬语泽、高,編碼類型等瓤漏。
  • pCodec:真正的編解碼器颊埃,其中有編解碼需要調(diào)用的函數(shù)
  • AVFrame:用于保存數(shù)據(jù)幀的數(shù)據(jù)結(jié)構(gòu)班利,這里的兩個幀分別是保存顏色轉(zhuǎn)換前后的兩幀圖像
  • AVPacket:解析文件時會將音/視頻幀讀入到packet中

打開文件

接下來我們打開一個視頻文件。

av_register_all();

av_register_all 定義在 libavformat 里庸队,調(diào)用它用以注冊所有支持的文件格式以及編解碼器彻消,從其實現(xiàn)代碼里可以看到它會調(diào)用 avcodec_register_all,因此之后就可以用所有ffmpeg支持的codec了丙笋。

if( avformat_open_input(&pFormatCtx, argv[1], NULL, NULL) != 0 )
{
  return 0;
}

使用新的API avformat_open_input來打開一個文件御板,第一個參數(shù)是一個AVFormatContext指針變量的地址牛郑,它會根據(jù)打開的文件信息填充AVFormatContext淹朋,需要注意的是,此處的pFormatContext必須為NULL或由avformat_alloc_context分配得到酪惭,這也是上一節(jié)中將其初始化為NULL的原因者甲,否則此函數(shù)調(diào)用會出問題虏缸。第二個參數(shù)是打開的文件名嫩实,通過argv[1]指定甲献,也就是命令行的第一個參數(shù)。后兩個參數(shù)分別用于指定特定的輸入格式(AVInputFormat)以及指定文件打開額外參數(shù)的AVDictionary結(jié)構(gòu)慨灭,這里均留作NULL氧骤。

if( avformat_find_stream_info(pFormatCtx, NULL ) < 0 )
{
   return -1;
}

av_dump_format(pFormatCtx, -1, argv[1], 0);

avformat_open_input函數(shù)只是讀文件頭吃引,并不會填充流信息,因此我們需要接下來調(diào)用avformat_find_stream_info獲取文件中的流信息并思,此函數(shù)會讀取packet语稠,并確定文件中所有的流信息颅筋,設(shè)置pFormatCtx->streams指向文件中的流,但此函數(shù)并不會改變文件指針占贫,讀取的packet會給后面的解碼進行處理先口。
最后調(diào)用一個幫助函數(shù)av_dump_format碉京,輸出文件的信息,也就是我們在使用ffmpeg時能看到的文件詳細信息烫葬。第二個參數(shù)指定輸出哪條流的信息搭综,-1表示給ffmpeg自己選擇划栓。最后一個參數(shù)用于指定dump的是不是輸出文件忠荞,我們dump的是輸入文件,因此一定要是0堂油。

現(xiàn)在 pFormatCtx->streams 中已經(jīng)有所有流了素标,因此現(xiàn)在我們遍歷它找到第一條視頻流:

videoStream = -1;

for( i = 0; i < pFormatCtx->nb_streams; i++ )
{
  if( pFormatCtx->streams[i]->codec->codec_type == AVMEDIA_TYPE_VIDEO)     
  {

    videoStream = i;

    break;

  }
}
 
if( videoStream == -1 )
{
  return -1;
}

codec_type 的宏定義已經(jīng)由以前的 CODEC_TYPE_VIDEO 改為 AVMEDIA_TYPE_VIDEO 了头遭。接下來我們通過這條 video stream 的編解碼信息打開相應的解碼器:

pCodecCtx = pFormatCtx->streams[videoStream]->codec;

pCodec = avcodec_find_decoder(pCodecCtx->codec_id);

if( pCodec == NULL )
{
  return -1;
}

if( avcodec_open2(pCodecCtx, pCodec, NULL) < 0 )
{
  return -1;
}

分配圖像緩存

接下來我們準備給即將解碼的圖片分配內(nèi)存空間。

pFrame = avcodec_alloc_frame();

if( pFrame == NULL )
{
  return -1;
}

pFrameRGB = avcodec_alloc_frame();

if( pFrameRGB == NULL )
{
  return -1;
}

調(diào)用 avcodec_alloc_frame 分配幀撕予,因為最后我們會將圖像寫成 24-bits RGB 的 PPM 文件实抡,因此這里需要兩個 AVFrame欢策,pFrame用于存儲解碼后的數(shù)據(jù)踩寇,pFrameRGB用于存儲轉(zhuǎn)換后的數(shù)據(jù):

numBytes = avpicture_get_size(PIX_FMT_RGB24, pCodecCtx->width,  pCodecCtx->height);

這里調(diào)用 avpicture_get_size,根據(jù) pCodecCtx 中原始圖像的寬高計算 RGB24 格式的圖像需要占用的空間大小俺孙,這是為了之后給 pFrameRGB 分配空間:

buffer = av_malloc(numBytes);

avpicture_fill( (AVPicture *)pFrameRGB, buffer, PIX_FMT_RGB24, 
                pCodecCtx->width, pCodecCtx->height);

接著上面的辣卒,首先是用 av_malloc 分配上面計算大小的內(nèi)存空間,然后調(diào)用 avpicture_fill 將 pFrameRGB 跟 buffer 指向的內(nèi)存關(guān)聯(lián)起來睛榄。

獲取圖像

OK荣茫,一切準備好就可以開始從文件中讀取視頻幀并解碼得到圖像了。

i = 0;
while( av_read_frame(pFormatCtx, &packet) >= 0 ) 
{
  if( packet.stream_index == videoStream ) 
  {
    avcodec_decode_video2(pCodecCtx, pFrame, &frameFinished, &packet);

    if( frameFinished )
    {
      struct SwsContext *img_convert_ctx = NULL;
      img_convert_ctx =sws_getCachedContext(img_convert_ctx, pCodecCtx->width,
                             pCodecCtx->height, pCodecCtx->pix_fmt,
                             pCodecCtx->width, pCodecCtx->height,
                             PIX_FMT_RGB24, SWS_BICUBIC,
                             NULL, NULL, NULL);

     if( !img_convert_ctx ) 
     {
      fprintf(stderr, "Cannot initialize sws conversion context\n");
      exit(1);
     }

     sws_scale(img_convert_ctx, (const uint8_t* const*)pFrame->data,
                  pFrame->linesize, 0, pCodecCtx->height, pFrameRGB->data,
                  pFrameRGB->linesize);

     if( i++ < 50 )
     {
      SaveFrame(pFrameRGB, pCodecCtx->width, pCodecCtx->height, i);
     }

    }
  }
  av_free_packet(&packet);
}

av_read_frame 從文件中讀取一個packet场靴,對于視頻來說一個packet里面包含一幀圖像數(shù)據(jù)啡莉,音頻可能包含多個幀(當音頻幀長度固定時),讀到這一幀后旨剥,如果是視頻幀票罐,則使用 avcodec_decode_video2 對packet中的幀進行解碼泞边,有時候解碼器并不能從一個packet中解碼得到一幀圖像數(shù)據(jù)(比如在需要其他參考幀的情況下),因此會設(shè)置 frameFinished疗杉,如果已經(jīng)得到下一幀圖像則設(shè)置 frameFinished 非零阵谚,否則為零。所以這里我們判斷 frameFinished 是否為零來確定 pFrame 中是否已經(jīng)得到解碼的圖像烟具。注意在每次處理完后需要調(diào)用av_free_packet 釋放讀取的packet梢什。

解碼得到圖像后,很有可能不是我們想要的 RGB24 格式朝聋,因此需要使用 swscale 來做轉(zhuǎn)換嗡午,調(diào)用sws_getCachedContext 得到轉(zhuǎn)換上下文,使用 sws_scale 將圖形從解碼后的格式轉(zhuǎn)換為 RGB24冀痕,最后將前50幀寫人 ppm 文件荔睹。最后釋放圖像以及關(guān)閉文件:

av_free(buffer);
av_free(pFrameRGB);
av_free(pFrame);
avcodec_close(pCodecCtx);
avformat_close_input(&pFormatCtx);
static void SaveFrame(AVFrame *pFrame, int width, int height, int iFrame)
{
  FILE *pFile;
  char szFilename[32];
  int y;
  sprintf(szFilename, "frame%d.ppm", iFrame);
  pFile = fopen(szFilename, "wb");
  if( !pFile )
  {
    return;
  }
  fprintf(pFile, "P6\n%d %d\n255\n", width, height);

  for( y = 0; y < height; y++ )
  {
    fwrite(pFrame->data[0] + y * pFrame->linesize[0], 1, width * 3, pFile);
  }
  fclose(pFile);
}
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末狸演,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子僻他,更是在濱河造成了極大的恐慌宵距,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,284評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件吨拗,死亡現(xiàn)場離奇詭異满哪,居然都是意外死亡,警方通過查閱死者的電腦和手機劝篷,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,115評論 3 395
  • 文/潘曉璐 我一進店門哨鸭,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人娇妓,你說我怎么就攤上這事像鸡。” “怎么了峡蟋?”我有些...
    開封第一講書人閱讀 164,614評論 0 354
  • 文/不壞的土叔 我叫張陵坟桅,是天一觀的道長。 經(jīng)常有香客問我蕊蝗,道長仅乓,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,671評論 1 293
  • 正文 為了忘掉前任蓬戚,我火速辦了婚禮夸楣,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘子漩。我一直安慰自己豫喧,他們只是感情好,可當我...
    茶點故事閱讀 67,699評論 6 392
  • 文/花漫 我一把揭開白布幢泼。 她就那樣靜靜地躺著紧显,像睡著了一般。 火紅的嫁衣襯著肌膚如雪缕棵。 梳的紋絲不亂的頭發(fā)上孵班,一...
    開封第一講書人閱讀 51,562評論 1 305
  • 那天,我揣著相機與錄音招驴,去河邊找鬼篙程。 笑死,一個胖子當著我的面吹牛别厘,可吹牛的內(nèi)容都是我干的虱饿。 我是一名探鬼主播,決...
    沈念sama閱讀 40,309評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼氮发!你這毒婦竟也來了渴肉?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,223評論 0 276
  • 序言:老撾萬榮一對情侶失蹤折柠,失蹤者是張志新(化名)和其女友劉穎宾娜,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體扇售,經(jīng)...
    沈念sama閱讀 45,668評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡前塔,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,859評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了承冰。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片华弓。...
    茶點故事閱讀 39,981評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖困乒,靈堂內(nèi)的尸體忽然破棺而出寂屏,到底是詐尸還是另有隱情,我是刑警寧澤娜搂,帶...
    沈念sama閱讀 35,705評論 5 347
  • 正文 年R本政府宣布迁霎,位于F島的核電站,受9級特大地震影響百宇,放射性物質(zhì)發(fā)生泄漏考廉。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,310評論 3 330
  • 文/蒙蒙 一携御、第九天 我趴在偏房一處隱蔽的房頂上張望昌粤。 院中可真熱鬧,春花似錦啄刹、人聲如沸涮坐。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,904評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽袱讹。三九已至,卻和暖如春昵时,著一層夾襖步出監(jiān)牢的瞬間廓译,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,023評論 1 270
  • 我被黑心中介騙來泰國打工债查, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人瓜挽。 一個月前我還...
    沈念sama閱讀 48,146評論 3 370
  • 正文 我出身青樓盹廷,卻偏偏與公主長得像,于是被迫代替她去往敵國和親久橙。 傳聞我的和親對象是個殘疾皇子俄占,可洞房花燭夜當晚...
    茶點故事閱讀 44,933評論 2 355

推薦閱讀更多精彩內(nèi)容