呼叫FFmpeg SDK解析封裝格式的視訊為音訊流和視訊流
我們平常最常用的音視訊檔案通常不是單獨的音訊訊號和視訊訊號,而是一個整體的檔案。這個檔案會在其中包含音訊流和視訊流,並通過某種方式進行同步播放。通常,檔案的音訊和視訊通過某種標準格式進行復用,生成某種封裝格式,而封裝的標誌就是檔案的副檔名,常用的有mp4/avi/flv/mkv等。
從底層考慮,我們可以使用的只有視訊解碼器、音訊解碼器,或者再加上一些附加的字幕解碼等額外資訊,卻不存在所謂的mp4解碼器或者avi解碼器。所以,為了可以正確播放視訊檔案,必須將封裝格式的視訊檔案分離出視訊和音訊資訊分別進行解碼和播放。
事實上,無論是mp4還是avi等檔案格式,都有不同的標準格式,對於不同的格式並沒有一種通用的解析方法。因此,FFMpeg專門定義了一個庫來處理設計檔案封裝格式的功能,即libavformat。涉及檔案的封裝、解封裝的問題,都可以通過呼叫libavformat的API實現。這裡我們實現一個demo來處理音視訊檔案的解複用與解碼的功能。
FFmpeg解複用-解碼器所包含的結構
這一過程實際上包括了封裝檔案的解複用和音訊/視訊解碼兩個步驟,因此需要定義的結構體大致包括用於解碼和解封裝的部分。我們定義下面這樣的一個結構體實現這個功能:
/************************************************* Struct: DemuxingVideoAudioContex Description: 儲存解複用器和解碼器的上下文元件 *************************************************/ typedef struct { AVFormatContext*fmt_ctx; AVCodecContext*video_dec_ctx, *audio_dec_ctx; AVStream*video_stream, *audio_stream; AVFrame *frame; AVPacket pkt; intvideo_stream_idx, audio_stream_idx; int width,height; uint8_t*video_dst_data[4]; intvideo_dst_linesize[4]; intvideo_dst_bufsize; enumAVPixelFormat pix_fmt; } DemuxingVideoAudioContex;
這個結構體中的大部分資料型別我們在前面做編碼/解碼等功能時已經見到過,另外幾個是涉及到視訊檔案的複用的,其中有:
- AVFormatContext:用於處理音視訊封裝格式的上下文資訊。
- AVStream:表示音訊或者視訊流的結構。
- AVPixelFormat:列舉型別,表示影象畫素的格式,最常用的是AV_PIX_FMT_YUV420P
FFmpeg解複用-解碼的過程
1).相關結構的初始化
與使用FFMpeg進行其他操作一樣,首先需註冊FFMpeg元件:
av_register_all();
隨後,我們需要開啟待處理的音視訊檔案。然而在此我們不使用開啟檔案的fopen函式,而是使用avformat_open_input函式。該函式不但會開啟輸入檔案,而且可以根據輸入檔案讀取相應的格式資訊。該函式的宣告如下:
int avformat_open_input(AVFormatContext **ps, const char*url, AVInputFormat *fmt, AVDictionary **options);
該函式的各個引數的作用為:
- ps:根據輸入檔案接收與格式相關的控制代碼資訊;可以指向NULL,那麼AVFormatContext型別的例項將由該函式進行分配。
- url:視訊url或者檔案路徑;
- fmt:強制輸入格式,可設定為NULL以自動檢測;
- options:儲存檔案格式無法識別的資訊;
- 返回值:成功返回0,失敗則返回負的錯誤碼;
該函式的呼叫方式為:
if (avformat_open_input(&(va_ctx.fmt_ctx),files.src_filename, NULL, NULL) < 0){
fprintf(stderr,"Could not open source file %s\n", files.src_filename);
return -1;
}
開啟檔案後,呼叫avformat_find_stream_info函式獲取檔案中的流資訊。該函式的宣告為:
int avformat_find_stream_info(AVFormatContext *ic,AVDictionary **options);
該函式的第一個引數即前面的檔案控制代碼,第二個引數也是用於儲存無法識別的資訊的AVDictionary的結構,通常可設為NULL。呼叫方式如:
/* retrieve stream information */
if (avformat_find_stream_info(va_ctx.fmt_ctx, NULL) <0){
fprintf(stderr,"Could not find stream information\n");
return -1;
}
獲取檔案中的流資訊後,下一步則是獲取檔案中的音訊和視訊流,並準備對音訊和視訊資訊進行解碼。獲取檔案中的流使用av_find_best_stream函式,其宣告如:
int av_find_best_stream(AVFormatContext *ic,
enum AVMediaType type,
int wanted_stream_nb,
int related_stream,
AVCodec **decoder_ret,
int flags);
其中各個引數的意義:
- ic:視訊檔案控制代碼;
- type:表示資料的型別,常用的有AVMEDIA_TYPE_VIDEO表示視訊,AVMEDIA_TYPE_AUDIO表示音訊等;
- wanted_stream_nb:我們期望獲取到的資料流的數量,設定為-1使用自動獲取;
- related_stream:獲取相關的音視訊流,如果沒有則設為-1;
- decoder_ret:返回這一路資料流的解碼器;
- flags:未定義;
- 返回值:函式執行成功返回流的數量,失敗則返回負的錯誤碼;
在函式執行成功後,便可呼叫avcodec_find_decoder和avcodec_open2開啟解碼器準備解碼音視訊流。該部分的程式碼實現如:
static int open_codec_context(IOFileName &files,DemuxingVideoAudioContex &va_ctx, enum AVMediaType type)
{
int ret,stream_index;
AVStream *st;
AVCodecContext*dec_ctx = NULL;
AVCodec *dec =NULL;
AVDictionary*opts = NULL;
ret =av_find_best_stream(va_ctx.fmt_ctx, type, -1, -1, NULL, 0);
if (ret < 0){
fprintf(stderr, "Could not find %s stream in input file'%s'\n", av_get_media_type_string(type), files.src_filename);
return ret;
}
else{
stream_index = ret;
st =va_ctx.fmt_ctx->streams[stream_index];
/* finddecoder for the stream */
dec_ctx =st->codec;
dec =avcodec_find_decoder(dec_ctx->codec_id);
if (!dec){
fprintf(stderr,"Failed to find %s codec\n", av_get_media_type_string(type));
returnAVERROR(EINVAL);
}
/* Init thedecoders, with or without reference counting */
av_dict_set(&opts, "refcounted_frames", files.refcount ?"1" : "0", 0);
if ((ret =avcodec_open2(dec_ctx, dec, &opts)) < 0) {
fprintf(stderr, "Failed to open %s codec\n",av_get_media_type_string(type));
returnret;
}
switch (type){
caseAVMEDIA_TYPE_VIDEO:
va_ctx.video_stream_idx = stream_index;
va_ctx.video_stream = va_ctx.fmt_ctx->streams[stream_index];
va_ctx.video_dec_ctx = va_ctx.video_stream->codec;
break;
caseAVMEDIA_TYPE_AUDIO:
va_ctx.audio_stream_idx = stream_index;
va_ctx.audio_stream = va_ctx.fmt_ctx->streams[stream_index];
va_ctx.audio_dec_ctx = va_ctx.audio_stream->codec;
break;
default:
fprintf(stderr, "Error: unsupported MediaType: %s\n",av_get_media_type_string(type));
return-1;
}
}
return 0;
}
整體初始化的函式程式碼為:
int InitDemuxContext(IOFileName &files,DemuxingVideoAudioContex &va_ctx)
{
int ret = 0,width, height;
/* register allformats and codecs */
av_register_all();
/* open inputfile, and allocate format context */
if(avformat_open_input(&(va_ctx.fmt_ctx), files.src_filename, NULL, NULL)< 0) {
fprintf(stderr, "Could not opensource file %s\n", files.src_filename);
return -1;
}
/* retrievestream information */
if(avformat_find_stream_info(va_ctx.fmt_ctx, NULL) < 0){
fprintf(stderr, "Could not find stream information\n");
return -1;
}
if(open_codec_context(files, va_ctx, AVMEDIA_TYPE_VIDEO) >= 0){
files.video_dst_file = fopen(files.video_dst_filename, "wb");
if(!files.video_dst_file){
fprintf(stderr, "Could not open destination file %s\n",files.video_dst_filename);
return-1;
}
/* allocateimage where the decoded image will be put */
va_ctx.width = va_ctx.video_dec_ctx->width;
va_ctx.height = va_ctx.video_dec_ctx->height;
va_ctx.pix_fmt = va_ctx.video_dec_ctx->pix_fmt;
ret =av_image_alloc(va_ctx.video_dst_data, va_ctx.video_dst_linesize, va_ctx.width,va_ctx.height, va_ctx.pix_fmt, 1);
if (ret < 0) {
fprintf(stderr, "Could not allocate raw video buffer\n");
return-1;
}
va_ctx.video_dst_bufsize = ret;
}
if(open_codec_context(files, va_ctx, AVMEDIA_TYPE_AUDIO) >= 0) {
files.audio_dst_file = fopen(files.audio_dst_filename, "wb");
if(!files.audio_dst_file){
fprintf(stderr, "Could not open destination file %s\n",files.audio_dst_filename);
return-1;
}
}
if(va_ctx.video_stream){
printf("Demuxing video from file '%s' into '%s'\n",files.src_filename, files.video_dst_filename);
}
if(va_ctx.audio_stream){
printf("Demuxing audio from file '%s' into '%s'\n",files.src_filename, files.audio_dst_filename);
}
/* dump inputinformation to stderr */
av_dump_format(va_ctx.fmt_ctx, 0, files.src_filename, 0);
if(!va_ctx.audio_stream && !va_ctx.video_stream){
fprintf(stderr, "Could not find audio or video stream in the input,aborting\n");
return -1;
}
return 0;
}
隨後要做的,是分配AVFrame和初始化AVPacket物件:
va_ctx.frame = av_frame_alloc(); //分配AVFrame結構物件
if (!va_ctx.frame){
fprintf(stderr,"Could not allocate frame\n");
ret =AVERROR(ENOMEM);
goto end;
}
/* initialize packet, set data to NULL, let the demuxerfill it */
av_init_packet(&va_ctx.pkt); //初始化AVPacket物件
va_ctx.pkt.data = NULL;
va_ctx.pkt.size = 0;
2).迴圈解析視訊檔案的包資料
解析視訊檔案的迴圈程式碼段為:
/* read frames from the file */
while (av_read_frame(va_ctx.fmt_ctx, &va_ctx.pkt)>= 0) //從輸入程式中讀取一個包的資料
{
AVPacketorig_pkt = va_ctx.pkt;
do{
ret =Decode_packet(files, va_ctx, &got_frame, 0); //解碼這個包
if (ret< 0)
break;
va_ctx.pkt.data += ret;
va_ctx.pkt.size -= ret;
} while(va_ctx.pkt.size > 0);
av_packet_unref(&orig_pkt);
}
這部分程式碼邏輯上非常簡單,首先呼叫av_read_frame函式,從檔案中讀取一個packet的資料,並實現了一個Decode_packet對這個packet進行解碼。Decode_packet函式的實現如下:
int Decode_packet(IOFileName &files,DemuxingVideoAudioContex &va_ctx, int *got_frame, int cached)
{
int ret = 0;
int decoded =va_ctx.pkt.size;
static intvideo_frame_count = 0;
static intaudio_frame_count = 0;
*got_frame = 0;
if(va_ctx.pkt.stream_index == va_ctx.video_stream_idx){
/* decodevideo frame */
ret =avcodec_decode_video2(va_ctx.video_dec_ctx, va_ctx.frame, got_frame,&va_ctx.pkt);
if (ret< 0) {
printf("Error decoding video frame(%d)\n", ret);
return ret;
}
if(*got_frame){
if(va_ctx.frame->width != va_ctx.width || va_ctx.frame->height !=va_ctx.height || va_ctx.frame->format != va_ctx.pix_fmt){
/*To handle this change, one could call av_image_alloc again and
*decode the following frames into another rawvideo file. */
printf("Error: Width, height and pixel format have to be "
"constant in a rawvideo file, but the width, height or "
"pixel format of the input video changed:\n"
"old: width = %d, height = %d, format = %s\n"
"new: width = %d, height = %d, format = %s\n",
va_ctx.width, va_ctx.height,av_get_pix_fmt_name((AVPixelFormat)(va_ctx.pix_fmt)),
va_ctx.frame->width, va_ctx.frame->height,
av_get_pix_fmt_name((AVPixelFormat)va_ctx.frame->format));
return -1;
}
printf("video_frame%s n:%d coded_n:%d pts:%s\n", cached ?"(cached)" : "", video_frame_count++,va_ctx.frame->coded_picture_number, va_ctx.frame->pts);
/* copy decoded frame to destination buffer:
* this is required since rawvideo expects non aligned data */
av_image_copy(va_ctx.video_dst_data, va_ctx.video_dst_linesize,
(const uint8_t **)(va_ctx.frame->data), va_ctx.frame->linesize,
va_ctx.pix_fmt, va_ctx.width, va_ctx.height);
/*write to rawvideo file */
fwrite(va_ctx.video_dst_data[0],va_ctx.video_dst_bufsize,files.video_dst_file);
}
}
else if (va_ctx.pkt.stream_index ==va_ctx.audio_stream_idx){
/* decodeaudio frame */
ret =avcodec_decode_audio4(va_ctx.audio_dec_ctx, va_ctx.frame, got_frame,&va_ctx.pkt);
if (ret< 0) {
printf("Error decoding audio frame (%s)\n", ret);
return ret;
}
/* Someaudio decoders decode only part of the packet, and have to be
* calledagain with the remainder of the packet data.
* Sample:fate-suite/lossless-audio/luckynight-partial.shn
* Also,some decoders might over-read the packet. */
decoded =FFMIN(ret, va_ctx.pkt.size);
if(*got_frame){
size_tunpadded_linesize = va_ctx.frame->nb_samples * av_get_bytes_per_sample((AVSampleFormat)va_ctx.frame->format);
printf("audio_frame%s n:%d nb_samples:%d pts:%s\n",
cached ? "(cached)" : "",
audio_frame_count++, va_ctx.frame->nb_samples,
va_ctx.frame->pts);
/*Write the raw audio data samples of the first plane. This works
* finefor packed formats (e.g. AV_SAMPLE_FMT_S16). However,
* mostaudio decoders output planar audio, which uses a separate
* planeof audio samples for each channel (e.g. AV_SAMPLE_FMT_S16P).
* Inother words, this code will write only the first audio channel
* inthese cases.
* Youshould use libswresample or libavfilter to convert the frame
* topacked data. */
fwrite(va_ctx.frame->extended_data[0], 1, unpadded_linesize,files.audio_dst_file);
}
}
/* If weuse frame reference counting, we own the data and need
* tode-reference it when we don't use it anymore */
if(*got_frame && files.refcount)
av_frame_unref(va_ctx.frame);
return decoded;
}
在該函式中,首先對讀取到的packet中的stream_index分別於先前獲取的音訊和視訊的stream_index進行對比來確定是音訊還是視訊流。而後分別呼叫相應的解碼函式進行解碼,以視訊流為例,判斷當前stream為視訊流後,呼叫avcodec_decode_video2函式將流資料解碼為畫素資料,並在獲取完整的一幀之後,將其寫出到輸出檔案中。