在視頻解碼前,先了解以下幾個基本的概念:
- 編解碼器(CODEC):能夠進行視頻和音頻壓縮(CO)與解壓縮(DEC),是視頻編解碼的核心部分。
- 容器/多媒體文件(Container/File):沒有了解視頻的編解碼之前,總是錯誤的認為平常下載的電影的文件的后綴(avi,mkv,rmvb等)就是視頻的編碼方式。事實上,剛才提到的幾種文件的后綴
並不是視頻的編碼方式,只是其封裝的方式。一個視頻文件通常有視頻數據、音頻數據以及字幕等,封裝的格式決定這些數據在文件中是如何的存放的,封裝在一起音頻、視頻等數據組成的多媒體文件,也可以叫做容器(其中包含了視音頻數據)。所以,只看多媒體文件的后綴名是難以知道視音頻的編碼方式的。 - 流數據 Stream,例如視頻流(Video Stream),音頻流(Audio Stream)。流中的數據元素被稱為幀Frame。
FFmpeg視頻解碼過程
通常來說,FFmpeg的視頻解碼過程有以下幾個步驟:
- 注冊所支持的所有的文件(容器)格式及其對應的CODEC
av_register_all()
- 打開文件
avformat_open_input()
- 從文件中提取流信息
avformat_find_stream_info()
- 在多個數據流中找到視頻流 video stream(類型為
MEDIA_TYPE_VIDEO
) - 查找video stream 相對應的解碼器
avcodec_find_decoder
- 打開解碼器
avcodec_open2()
- 為解碼幀分配內存
av_frame_alloc()
- 從流中讀取讀取數據到Packet中
av_read_frame()
- 對video 幀進行解碼,調用
avcodec_decode_video2()
解碼過程的具體說明
1. 注冊
av_register_all
該函數注冊支持的所有的文件格式(容器)及其對應的CODEC,只需要調用一次,故一般放在main函數中。也可以注冊某個特定的容器格式,但通常來說不需要這么做。
2. 打開文件
avformat_open_input
該函數讀取文件的頭信息,並將其信息保存到AVFormatContext
結構體中。其調用如下
AVFormatContext* pFormatCtx = nullptr;
avformat_open_input(&pFormatCtx, filenName, nullptr, nullptr)
第一個參數是AVFormatContext結構體的指針,第二個參數為文件路徑;第三個參數用來設定輸入文件的格式,如果設為null,將自動檢測文件格式;第四個參數用來填充AVFormatContext一些字段以及Demuxer的private選項。
AVFormatContext包含有較多的碼流信息參數,通常由avformat_open_input
創建並填充關鍵字段。
3. 獲取必要的CODEC參數
avformat_open_input
通過解析多媒體文件或流的頭信息及其他的輔助數據,能夠獲取到足夠多的關於文件、流和CODEC的信息,並將這些信息填充到AVFormatContext
結構體中。但任何一種多媒體格式(容器)提供的信息都是有限的,而且不同的多媒體制作軟件對頭信息的設置也不盡相同,在制作多媒體文件的時候難免會引入一些錯誤。也就是說,僅僅通過avformat_open_input
並不能保證能夠獲取所需要的信息,所以一般要使用
avformat_find_stream_info(AVFormatContext *ic, AVDictionary **options)
avformat_find_stream_info
主要用來獲取必要的CODEC參數,設置到ic->streams[i]->codec
。
在解碼的過程中,首先要獲取到各個stream所對應的CODEC類型和id,CODEC的類型和id是兩個枚舉值,其定義如下:
enum AVMediaType {
AVMEDIA_TYPE_UNKNOWN = -1,
AVMEDIA_TYPE_VIDEO,
AVMEDIA_TYPE_AUDIO,
AVMEDIA_TYPE_DATA,
AVMEDIA_TYPE_SUBTITLE,
AVMEDIA_TYPE_ATTACHMENT,
AVMEDIA_TYPE_NB
};
enum CodecID {
CODEC_ID_NONE, /* video codecs */
CODEC_ID_MPEG1VIDEO,
CODEC_ID_MPEG2VIDEO, ///< preferred ID for MPEG-1/2 video decoding
CODEC_ID_MPEG2VIDEO_XVMC,
CODEC_ID_H261,
CODEC_ID_H263,
...
}
通常,如果多媒體文件具有完整而正確的頭信息,通過avformat_open_input
即可用獲得這兩個參數。
4. 打開解碼器
經過上面的步驟,已經將文件格式信息讀取到了AVFormatContext中,要打開流數據相應的CODEC需要經過下面幾個步驟
- 找到視頻流 video stream
一個多媒體文件包含有多個原始流,例如 movie.mkv這個多媒體文件可能包含下面的流數據 - 原始流 1 h.264 video
- 原始流 2 aac audio for Chinese
- 原始流 3 aac audio for English
- 原始流 4 Chinese Subtitle
- 原始流 5 English Subtitle
要解碼視頻,首先要在AVFormatContext包含的多個流中找到CODEC類型為AVMEDIA_TYPE_VIDEO,代碼如下:
//查找視頻流 video stream
int videoStream = -1;
for (int 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; // 沒有找到視頻流video stream
結構體AVFormatContext中的streams字段是一個AVStream指針的數組,包含了文件所有流的描述,上述上述代碼在該數組中查找CODEC類型為
AVMEDIA_TYPE_VIDEO的流的下標。
- 根據codec_id找到相應的CODEC,並打開
結構體AVCodecContext描述了CODEC上下文,包含了眾多CODEC所需要的參數信息。
AVCodecContext* pCodecCtxOrg = nullptr;
AVCodec* pCodec = nullptr;
pCodecCtxOrg = pFormatCtx->streams[videoStream]->codec; // codec context
// 找到video stream的 decoder
pCodec = avcodec_find_decoder(pCodecCtxOrg->codec_id);
// open codec
if (avcodec_open2(pCodecCtxOrg , pCodec, nullptr) < 0)
return -1; // Could open codec
上述代碼,首先通過codec_id找到相應的CODEC,然后調用avcodec_open2
打開相應的CODEC。
5. 讀取數據幀並解碼
已經有了相應的解碼器,下面的工作就是將數據從流中讀出,並解碼為沒有壓縮的原始數據
AVPacket packet;
while (av_read_frame(pFormatCtx, &packet) >= 0)
{
if (packet.stream_index == videoStream)
{
int frameFinished = 0;
avcodec_decode_video2(pCodecCtx, pFrame, &frameFinished, &packet);
if (frameFinished)
{
doSomething();
}
}
}
上述代碼調用av_read_frame
將數據從流中讀取數據到packet中,並調用avcodec_decode_video2
對讀取的數據進行解碼。
6. 關閉
需要關閉avformat_open_input
打開的輸入流,avcodec_open2
打開的CODEC
avcodec_close(pCodecCtxOrg);
avformat_close_input(&pFormatCtx);
補充
在配置好FFmpeg的開發環境后,在C++中使用FFmpeg的庫函數,會出現解析不出函數的名稱鏈接錯誤,這是由於FFmpeg庫是C語言實現,要在C++調用C函數需要 extern "C"
的聲明。
extern "C"
{
# include <libavcodec\avcodec.h>
# include <libavformat\avformat.h>
# include <libswscale\swscale.h>
}
哎,博客園的Markdown用着好不方便啊,不知道怎么畫流程圖....