突然發現又有好久沒有寫技術blog了,主要原因是最近時間都用來研究libav去了(因為api極類似ffmpeg,雖然出自同一份代碼的另外一個分支,因項目選用libav,故下文均用libav代替),其實要從知道這個庫的時候已經很久了,早在加入avplayer開源社區的已經略有耳聞,看着他們討論我卻一直不知這個庫能具體幫我做到哪些功能,插不上嘴呢,更強迫了我學習它的熱情,下面就來一一解惑,希望就能幫到類似幾個月前的我那樣的同行。
1、提供API解碼、編碼市面上主流幾乎全部的視頻、音頻格式文件。
2、通用視頻轉換命令行工具ffmpeg、avconv,可以幫我們快速將媒體文件格式進行轉換,且做一些簡單的resize或者resample,其工具提供了非常強大的filter,各種變幻根據參數都能實現,沒有你想不到的,只有你找不到的。
3、簡單的播放器avplay命令,這個播放器支持libav所有能夠支持的video codec,算個簡單的萬能播放器了,雖然seek功能弱爆了,並且還沒有pause、stop、顯示時間等等功能,不過有些應急時候絕對首選它了。
4、libav還提供avprobe命令,可以讓你瞬間了解這個媒體文件其中真實的video/audio編碼(不會受到文件擴展名的誤導)、擁有哪些stream(一般MP4分為視頻、音頻、字幕),一目了然。
讀到此你一定會很感激我,不像大多數技術博客那樣直接貼上很多大段大段的代碼一下嚇走好多初學者....我不能保證接下來一定不貼代碼上來,但是我會盡量克制自己的....
本文主要將以第1點API解碼編碼的介紹為主。因為libav是基於C實現的,調用習慣全是基於函數式的,這樣的優點就是跨平台好吧,缺點就是會使client代碼比較臃腫,到處充斥着free、alloc等等。如果你是一個純面向對象發燒支持者,請不要往下看,以免傷身且葯還不能停。
libav提供一個函數avformat_open_input,即打開一個媒體文件,用AVFormatContext指針接受返回結果,代碼看起來就是這樣:
AVFormatContext* pformat_context = avformat_alloc_context(); if(avformat_open_input(&pformat_context, file.c_str(), nullptr, 0) != 0) { printf("can't open the file %s\n", file.c_str()); return false; }
然后你要做的是將所打開的FormatContext讀取其中的stream,其中會有各種各樣的stream類型,你需要做的事情就是將這個stream的index記錄下來。
shared_ptr<AVFormatContext> format_context(pformat_context, [](AVFormatContext*& p){ avformat_close_input(&p); });
if(avformat_find_stream_info(format_context.get(), nullptr) < 0)
{
printf("can't find suitable codec parameters\n");
return false;
}
// find out the audio and video stream
int video_stream_index = -1, audio_stream_index = -1;
for(unsigned int i = 0; i < format_context->nb_streams && (video_stream_index == -1 || audio_stream_index == -1); i++)
{
if(format_context->streams[i]->codec->codec_type == AVMEDIA_TYPE_VIDEO)
{
video_stream_index = i;
}
else if (format_context->streams[i]->codec->codec_type == AVMEDIA_TYPE_AUDIO)
{
audio_stream_index = i;
}
}
if(video_stream_index == -1 && audio_stream_index == -1)
{
printf("input file contains no video stream or audio stream.\n");
return false;
}
對照stream可以使用avprobe命令查看視頻文件本身的內容。
獲取到stream信息之后,你就需要創建decoder來解碼視頻啦~ libav提供一個函數avcodec_find_decoder根據你自己找到的video index和audio index去尋找codec_id作為參數得到AVCodec指針,再使用函數avcodec_open2傳入這個指針即可。
// open the video decoder AVCodecContext* video_codec_context = nullptr; if (video_stream_index != -1) { video_codec_context = format_context->streams[video_stream_index]->codec; AVCodec* video_codec = avcodec_find_decoder(video_codec_context->codec_id); if(video_codec == nullptr) { printf("can't find suitable video decoder\n"); return false; } if(avcodec_open2(video_codec_context, video_codec, nullptr) < 0) { printf("can't open the video decoder\n"); return false; } } // open the audio decoder AVCodecContext* audio_codec_context = nullptr; if (audio_stream_index != -1) { audio_codec_context = format_context->streams[audio_stream_index]->codec; AVCodec* audio_codec = avcodec_find_decoder(audio_codec_context->codec_id); if (audio_codec == nullptr) { printf("can't find suitable audio decoder\n"); return false; } if (avcodec_open2(audio_codec_context, audio_codec, nullptr) < 0) { printf("can't open the audio decoder\n"); return false; } }
注意,open之后一定要調用對應的api close,比方avformat_close_input這些都是必備的,就不全貼出來了。
接下來講這課程最重要的部分——decode video,先創建用於接收av_read_frame讀出來的數據包,
AVPacket packet = {0};
av_init_packet(&packet);
然后使用一個循環調用av_read_frame,查注釋你會知道return>=0為成功,然后判斷packet的stream_index是video_stream_index還是audio_stream_index,從而使用不同的decode函數(avcodec_decode_video2 / avcodec_decode_audio4)做解碼,視頻如果是MP4將得到AV_PIX_FMT_YUV420P數據,音頻將得到原始音頻AV_SAMPLE_FMT_FLTP采樣數據。
但是我們一般不會使用YUV420P進行圖像、視頻處理,而是使用bitmap來進行處理,所以需要在這里借助另外一個函數sws_scale,第一個參數查看源碼了解到是一個結構體struct,並不需要手動填充它,而且你也沒辦法手動填充它,libav並不希望你這么做(沒有將細節寫在include中),因此有一個sws_getContext函數是專門做這件事情的。
struct 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);
看到參數你就能很容易的猜到,你需要提供原視頻的尺寸和格式,可以在已打開的視頻的codec中獲得,目標視頻尺寸你自己隨便設置都可以,dstFormat可以設置為:AV_PIX_FMT_BGRA,更多可以參見:pixfmt.h 中的 enum AVPixelFormat,如果是BGRA,圖片則為32位,包含透明通道,方便之后疊加圖層處理。如果讀者跟着我的步驟走,應該就能達到連續輸出圖片的功能了,再加入圖像識別的更多功能:臉譜識別、手勢識別、車牌識別,就直接可以用了,是不是很激動?
