【FFMpeg視頻開發與應用基礎】四、調用FFmpeg SDK解析封裝格式的視頻爲音頻流和視頻流

《FFMpeg視頻開發與應用基礎——使用FFMpeg工具與SDK》視頻教程已經在“CSDN學院”上線,視頻中包含了從0開始逐行代碼實現FFMpeg視頻開發的過程,歡迎觀看!鏈接地址:FFMpeg視頻開發與應用基礎——使用FFMpeg工具與SDK

工程代碼地址:FFmpeg_Tutorial


我們平常最常用的音視頻文件通常不是單獨的音頻信號和視頻信號,而是一個整體的文件。這個文件會在其中包含音頻流和視頻流,並通過某種方式進行同步播放。通常,文件的音頻和視頻通過某種標準格式進行復用,生成某種封裝格式,而封裝的標誌就是文件的擴展名,常用的有mp4/avi/flv/mkv等。

從底層考慮,我們可以使用的只有視頻解碼器、音頻解碼器,或者再加上一些附加的字幕解碼等額外信息,卻不存在所謂的mp4解碼器或者avi解碼器。所以,爲了可以正確播放視頻文件,必須將封裝格式的視頻文件分離出視頻和音頻信息分別進行解碼和播放。

事實上,無論是mp4還是avi等文件格式,都有不同的標準格式,對於不同的格式並沒有一種通用的解析方法。因此,FFMpeg專門定義了一個庫來處理設計文件封裝格式的功能,即libavformat。涉及文件的封裝、解封裝的問題,都可以通過調用libavformat的API實現。這裏我們實現一個demo來處理音視頻文件的解複用與解碼的功能。


1. 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;

    int video_stream_idx, audio_stream_idx;
    int width, height;

    uint8_t *video_dst_data[4];
    int video_dst_linesize[4];
    int video_dst_bufsize;
    enum AVPixelFormat pix_fmt;
} DemuxingVideoAudioContex;

這個結構體中的大部分數據類型我們在前面做編碼/解碼等功能時已經見到過,另外幾個是涉及到視頻文件的複用的,其中有:

  • AVFormatContext:用於處理音視頻封裝格式的上下文信息。
  • AVStream:表示音頻或者視頻流的結構。
  • AVPixelFormat:枚舉類型,表示圖像像素的格式,最常用的是AV_PIX_FMT_YUV420P

2、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];

        /* find decoder 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));
            return AVERROR(EINVAL);
        }

        /* Init the decoders, 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));
            return ret;
        }

        switch (type)
        {
        case AVMEDIA_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;
        case AVMEDIA_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 all formats and codecs */
    av_register_all();

    /* open input file, and allocate format context */
    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;
    }

    /* retrieve stream 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;
        }

        /* allocate image 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 input information 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 demuxer fill 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)     //從輸入程序中讀取一個包的數據
{
    AVPacket orig_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 int video_frame_count = 0;
    static int audio_frame_count = 0;

    *got_frame = 0;

    if (va_ctx.pkt.stream_index == va_ctx.video_stream_idx)
    {
        /* decode video 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], 1, va_ctx.video_dst_bufsize, files.video_dst_file);
        }
    }
    else if (va_ctx.pkt.stream_index == va_ctx.audio_stream_idx)
    {
        /* decode audio 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;
        }
        /* Some audio decoders decode only part of the packet, and have to be
        * called again 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_t unpadded_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
            * fine for packed formats (e.g. AV_SAMPLE_FMT_S16). However,
            * most audio decoders output planar audio, which uses a separate
            * plane of audio samples for each channel (e.g. AV_SAMPLE_FMT_S16P).
            * In other words, this code will write only the first audio channel
            * in these cases.
            * You should use libswresample or libavfilter to convert the frame
            * to packed data. */
            fwrite(va_ctx.frame->extended_data[0], 1, unpadded_linesize, files.audio_dst_file);
        }
    }

        /* If we use frame reference counting, we own the data and need
        * to de-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函數將流數據解碼爲像素數據,並在獲取完整的一幀之後,將其寫出到輸出文件中。


3、總結

相對於前文講述過的解碼H.264格式裸碼流,解封裝+解碼過程看似多了一個步驟,然而在實現起來實際上並無過多差別。這主要是由於FFMpeg中的多個API已經很好地實現了封裝文件的解析和讀取過程,如打開文件我們使用avformat_open_input代替fopen,讀取數據包使用av_read_frame代替fread,其他方面只需要多一步判斷封裝文件中數據流的類型即可,剩餘部分與裸碼流的解碼並無太多差別。

發佈了185 篇原創文章 · 獲贊 125 · 訪問量 45萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章