264編碼基本概念 FFMpeg的解碼流程
1、NAL、Slice與frame意思及相互關係 NAL指網路提取層,裡面放一些與網路相關的資訊Slice是片的意思,264中把影象分成一幀(frame)或兩場(field),而幀又可以分成一個或幾個片(Slilce);片由巨集塊(MB)組成。巨集塊是編碼處理的基本單元。2、NAL nal_unit_type中的1(非IDR影象的編碼條帶)、2(編碼條帶資料分割塊A)、3(編碼條帶資料分割塊B)、4(編碼條帶資料分割塊C)、5(IDR影象的編碼條帶)種類型 與 Slice種的三種編碼模式:I_slice、P_slice、B_slice NAL nal_unit_type 裡的五種型別,代表接下來資料是表示啥資訊的和具體如何分塊。I_slice、P_slice、B_slice 表示I型別的片、P型別的片,B型別的片.其中I_slice為幀內預測模式編碼;P_slice為單向預測編碼或幀內模式;B_slice 中為雙向預測或幀內模式。3、還有frame的3種類型:I frame、P frame、 B frame之間有什麼對映關係麼? I frame、P frame、 B frame關係同 I_slice、P_slice、B_slice,slice和frame區別在問題1中已經講明白。4、最後,NAL nal_unit_type中的6(SEI)、7(SPS)、8(PPS)屬於什麼幀呢? NAL nal_unit_type 為序列引數集(SPS)、影象引數集(PPS)、增強資訊(SEI)不屬於啥幀的概念。表示後面的資料資訊為序列引數集(SPS)、影象引數集(PPS)、增強資訊(SEI)。
====================================================================================
NAL單元中首先會有一個H.264 NAL type,根據這個可以判斷是啥資訊。如果是H264NT_SLICE_DPA,H264NT_SLICE_DPB,H264NT_SLICE_DPC, H264NT_SLICE_IDR視訊資料相關的,裡面還會有Slice head頭資訊,根據這個頭資訊,可以判斷屬於I-Slice(P-Slice或B-Slice),之後對於每個巨集塊,都會有MB head 資訊,根據巨集塊頭資訊可以判斷塊模式。H264就是這樣以分層的方式組織資訊的。不知道你理解沒有。
====================================================================================
x264_encoder_encode每次會以引數送入一幀待編碼的幀pic_in,函式首先會從空閒佇列中取出一幀用於承載該新幀,而它的i_frame被設定為播放順序計數,如:fenc->i_frame = h->frames.i_input++。
FFMpeg的解碼流程
1. 從基礎談起先給出幾個概念,以在後面的分析中方便理解Container:在音視訊中的容器,一般指的是一種特定的檔案格式,裡面指明瞭所包含的 音視訊,字幕等相關資訊Stream:這個詞有些微妙,很多地方都用到,比如TCP,SVR4系統等,其實在音視訊,你 可以理解為單純的音訊資料或者視訊資料等Frames:這個概念不是很好明確的表示,指的是Stream中的一個數據單元,要真正對這 個概念有所理解,可能需要看一些音視訊編碼解碼的理論知識Packet:是Stream的raw資料Codec:Coded + Decoded其實這些概念在在FFmpeg中都有很好的體現,我們在後續分析中會慢慢看到
2.解碼的基本流程我很懶,於是還是選擇了從<An ffmpeg and SDL Tutorial>中的流程概述:
10 OPEN video_stream FROM video.avi20 READ packet FROM video_stream INTO frame30 IF frame NOT COMPLETE GOTO 2040 DO SOMETHING WITH frame50 GOTO 20
這就是解碼的全過程,一眼看去,是不是感覺不過如此:),不過,事情有深有淺,從淺到深,然後從深回到淺可能才是一個有意思的過程,我們的故事,就從這裡開始,展開來講。
3.例子程式碼在<An ffmpeg and SDL Tutorial 1>中,給出了一個陽春版的解碼器,我們來仔細看看陽春後面的故事,為了方便講述,我先貼出程式碼:
#include <ffmpeg/avcodec.h>#include <ffmpeg/avformat.h>
#include <stdio.h>
void SaveFrame(AVFrame *pFrame, int width, int height, int iFrame) {FILE *pFile;char szFilename[32];int y;// Open filesprintf(szFilename, "frame%d.ppm", iFrame);pFile=fopen(szFilename, "wb");if(pFile==NULL) return;// Write headerfprintf(pFile, "P6/n%d %d/n255/n", width, height);// Write pixel datafor(y=0; y<height; y++) fwrite(pFrame->data[0]+y*pFrame->linesize[0], 1, width*3, pFile);// Close filefclose(pFile);}
int main(int argc, char *argv[]) {AVFormatContext *pFormatCtx;int i, videoStream;AVCodecContext *pCodecCtx;AVCodec *pCodec;AVFrame *pFrame; AVFrame *pFrameRGB;AVPacket packet;int frameFinished;int numBytes;uint8_t *buffer;if(argc < 2) { printf("Please provide a movie file/n"); return -1;}// Register all formats and codecs########################################[1]########################################av_register_all();// Open video file########################################[2]########################################if(av_open_input_file(&pFormatCtx, argv[1], NULL, 0, NULL)!=0) return -1; // Couldn't open file// Retrieve stream information########################################[3]########################################if(av_find_stream_info(pFormatCtx)<0) return -1; // Couldn't find stream information// Dump information about file onto standard errordump_format(pFormatCtx, 0, argv[1], 0);// Find the first video streamvideoStream=-1;for(i=0; i<pFormatCtx->nb_streams; i++) if(pFormatCtx->streams[i]->codec->codec_type==CODEC_TYPE_VIDEO) { videoStream=i; break; }if(videoStream==-1) return -1; // Didn't find a video stream// Get a pointer to the codec context for the video streampCodecCtx=pFormatCtx->streams[videoStream]->codec;// Find the decoder for the video streampCodec=avcodec_find_decoder(pCodecCtx->codec_id);if(pCodec==NULL) { fprintf(stderr, "Unsupported codec!/n"); return -1; // Codec not found}// Open codecif(avcodec_open(pCodecCtx, pCodec)<0) return -1; // Could not open codec// Allocate video framepFrame=avcodec_alloc_frame();// Allocate an AVFrame structurepFrameRGB=avcodec_alloc_frame();if(pFrameRGB==NULL) return -1; // Determine required buffer size and allocate buffernumBytes=avpicture_get_size(PIX_FMT_RGB24, pCodecCtx->width, pCodecCtx->height);buffer=(uint8_t *)av_malloc(numBytes*sizeof(uint8_t));// Assign appropriate parts of buffer to image planes in pFrameRGB// Note that pFrameRGB is an AVFrame, but AVFrame is a superset// of AVPictureavpicture_fill((AVPicture *)pFrameRGB, buffer, PIX_FMT_RGB24, pCodecCtx->width, pCodecCtx->height);// Read frames and save first five frames to disk########################################[4]########################################i=0;while(av_read_frame(pFormatCtx, &packet)>=0) { // Is this a packet from the video stream? if(packet.stream_index==videoStream) { // Decode video frame avcodec_decode_video(pCodecCtx, pFrame, &frameFinished, packet.data, packet.size); // Did we get a video frame? if(frameFinished) { // Convert the image from its native format to RGB img_convert((AVPicture *)pFrameRGB, PIX_FMT_RGB24, (AVPicture*)pFrame, pCodecCtx->pix_fmt, pCodecCtx->width, pCodecCtx->height); // Save the frame to disk if(++i<=5) SaveFrame(pFrameRGB, pCodecCtx->width, pCodecCtx->height, i); } } // Free the packet that was allocated by av_read_frame av_free_packet(&packet);}// Free the RGB imageav_free(buffer);av_free(pFrameRGB);// Free the YUV frameav_free(pFrame);// Close the codecavcodec_close(pCodecCtx);// Close the video fileav_close_input_file(pFormatCtx);return 0;}
程式碼註釋得很清楚,沒什麼過多需要講解的,關於其中的什麼YUV420,RGB,PPM等格式,如果不理解,麻煩還是google一下,也可以參考:http://barrypopy.cublog.cn/裡面的相關文章
其實這部分程式碼,很好了Demo了怎麼樣去抓屏功能的實現,但我們得去看看魔術師在後臺的一些手法,而不只是簡單的享受其表演。
4.背後的故事真正的難度,其實就是上面的[1],[2],[3],[4],其他部分,都是資料結構之間的轉換,如果你認真看程式碼的話,不難理解其他部分。
[1]:沒什麼太多好說的,如果不明白,看我轉載的關於FFmepg框架的文章
[2]:先說說裡面的AVFormatContext *pFormatCtx結構,字面意思理解AVFormatContext就是關於AVFormat(其實就是我們上面說的Container格式)的所處的Context(場景),自然是儲存Container資訊的總控結構了,後面你也可以看到,基本上所有的資訊,都可以從它出發而獲取到 我們來看看av_open_input_file()都做了些什麼:[libavformat/utils.c]int av_open_input_file(AVFormatContext **ic_ptr, const char *filename, AVInputFormat *fmt, int buf_size, AVFormatParameters *ap){ ...... if (!fmt) { /* guess format if no file can be opened */ fmt = av_probe_input_format(pd, 0); }
...... err = av_open_input_stream(ic_ptr, pb, filename, fmt, ap); ......}
這樣看來,只是做了兩件事情:1). 偵測容器檔案格式2). 從容器檔案獲取Stream的資訊
這兩件事情,實際上就是呼叫特定檔案的demuxer以分離Stream的過程:
具體流程如下:
av_open_input_file | +---->av_probe_input_format從first_iformat中遍歷註冊的所有demuxer以 | 呼叫相應的probe函式 | +---->av_open_input_stream呼叫指定demuxer的read_header函式以獲取相關 流的資訊ic->iformat->read_header
如果反過來再參考我轉貼的關於ffmpeg框架的文章,是否清楚一些了呢:)
[3]:簡單從AVFormatContext獲取Stream的資訊,沒什麼好多說的
[4]:先簡單說一些ffmpeg方面的東西,從理論角度說過來,Packet可以包含frame的部分資料,但ffmpeg為了實現上的方便,使得對於視訊來說,每個Packet至少包含一frame,對於音訊也是相應處理,這是實現方面的考慮,而非協議要求.因此,在上面的程式碼實際上是這樣的: 從檔案中讀取packet,從Packet中解碼相應的frame; 從幀中解碼; if(解碼幀完成) do something();
我們來看看如何獲取Packet,又如何從Packet中解碼frame的。
av_read_frame | +---->av_read_frame_internal | +---->av_parser_parse呼叫的是指定解碼器的s->parser->parser_parse函式以從raw packet中重構frame
avcodec_decode_video | +---->avctx->codec->decode呼叫指定Codec的解碼函式 因此,從上面的過程可以看到,實際上分為了兩部分:
一部分是解複用(demuxer),然後是解碼(decode)
使用的分別是:av_open_input_file() ---->解複用
av_read_frame() | | ---->解碼 avcodec_decode_video() |
5.後面該做些什麼結合這部分和轉貼的ffmepg框架的文章,應該可以基本打通解碼的流程了,後面的問題則是針對具體容器格式和具體編碼解碼器的分析,後面我們繼續