1. 程式人生 > >FFMPEG音視訊解碼流程&MP4音視訊檔案流讀取(轉)

FFMPEG音視訊解碼流程&MP4音視訊檔案流讀取(轉)

1.播放多媒體檔案步驟

通常情況下,我們下載的視訊檔案如MP4,MKV、FLV等都屬於封裝格式,就是把音視訊資料按照相應的規範,打包成一個文字檔案。我們可以使用MediaInfo這個工具檢視媒體檔案的相關資訊。

所以當我們播放一個媒體檔案時,通常需要經過以下幾個步驟

①解封裝(Demuxing):就是將輸入的封裝格式的資料,分離成為音訊流壓縮編碼資料和視訊流壓縮編碼資料。封裝格式種類很多,例如MP4,MKV,RMVB,TS,FLV,AVI等等,它的作用就是將已經壓縮編碼的視訊資料和音訊資料按照一定的格式放到一起。例如,FLV格式的資料,經過解封裝操作後,輸出H.264編碼的視訊碼流和AAC編碼的音訊碼流。

②解碼(Decode):就是將視訊/音訊壓縮編碼資料,解碼成為非壓縮的視訊/音訊原始資料。音訊的壓縮編碼標準包含AAC,MP3等,視訊的壓縮編碼標準則包含H.264,MPEG2等。解碼是整個系統中最重要也是最複雜的一個環節。通過解碼,壓縮編碼的視訊資料輸出成為非壓縮的顏色資料,例如YUV、RGB等等;壓縮編碼的音訊資料輸出成為非壓縮的音訊抽樣資料,例如PCM資料。

③音視訊同步:就是根據解封裝模組處理過程中獲取到的引數資訊,同步解碼出來的音訊和視訊資料,並將音視訊頻資料送至系統的顯示卡和音效卡播放出來(Render)。

2.FFMPEG音視訊解碼

通過上面對媒體檔案播放步驟的瞭解,我們在解碼多媒體檔案的時候需要經過兩個步驟,即解封裝(Demuxing)和解碼(Decode)。下面就來看一下FFMPEG解碼媒體檔案的時候是怎樣做這兩個步驟的。

在使用FFMPEG解碼媒體檔案之前,我們首先需要註冊FFMPEG的各種元件,通過

av_register_all();

這個函式,可以註冊所有支援的容器和對應的codec。之後我們通過

AVFormatContext *pFormatCtx = avformat_alloc_context();
avformat_open_input(&pFormatCtx,input_cstr,NULL,NULL);
avformat_find_stream_info(pFormatCtx,NULL);

來開啟一個媒體檔案,並獲得媒體檔案封裝格式的上下文。之後我們就可以通過遍歷定義在libavformat/avformat.h裡儲存著媒體檔案中封裝的流數量的nb_streams在媒體檔案中分離出音視訊流。

分離出音視訊流之後,就可以對音視訊流分別進行解碼了,這裡先以視訊解碼為例,我們可以遍歷AVStream找到codec_type為AVMEDIA_TYPE_VIDEO的的AVStream即為視訊流的索引值。

複製程式碼

//視訊解碼,需要找到視訊對應的AVStream所在pFormatCtx->streams的索引位置
    int video_stream_idx = -1;
    int i = 0;
    for(; i < pFormatCtx->nb_streams;i++){
        //根據型別判斷,是否是視訊流
        if(pFormatCtx->streams[i]->codec->codec_type == AVMEDIA_TYPE_VIDEO){
            video_stream_idx = i;
            break;
        }
    }

複製程式碼

然後我們就可以通過AVStream來找到對應的AVCodecContext即編解碼器的上下文。之後就可以通過這個上下文,使用

avcodec_find_decoder()

來找到對應的解碼器,再通過

avcodec_open2()

來開啟解碼器,AVFormatContext、AVStream、AVCodecContext、AVCodec四者之間的關係為

開啟解碼器之後,就可以迴圈的將一幀待解碼的資料AVPacket送給

avcodec_decode_video2()

進行解碼,解碼之後的資料存放在AVFrame裡面。

3.示例程式碼

3.1.視訊解碼

複製程式碼

#include <stdio.h>
#include <stdlib.h>
//編碼
#include "libavcodec/avcodec.h"
//封裝格式處理
#include "libavformat/avformat.h"
//畫素處理
#include "libswscale/swscale.h"

int main()
{
    //獲取輸入輸出檔名
    const char *input = "test.mp4";
    const char *output = "test.yuv";

    //1.註冊所有元件
    av_register_all();

    //封裝格式上下文,統領全域性的結構體,儲存了視訊檔案封裝格式的相關資訊
    AVFormatContext *pFormatCtx = avformat_alloc_context();

    //2.開啟輸入視訊檔案
    if (avformat_open_input(&pFormatCtx, input, NULL, NULL) != 0)
    {
        printf("%s","無法開啟輸入視訊檔案");
        return;
    }

    //3.獲取視訊檔案資訊
    if (avformat_find_stream_info(pFormatCtx,NULL) < 0)
    {
        printf("%s","無法獲取視訊檔案資訊");
        return;
    }

    //獲取視訊流的索引位置
    //遍歷所有型別的流(音訊流、視訊流、字幕流),找到視訊流
    int v_stream_idx = -1;
    int i = 0;
    //number of streams
    for (; i < pFormatCtx->nb_streams; i++)
    {
        //流的型別
        if (pFormatCtx->streams[i]->codec->codec_type == AVMEDIA_TYPE_VIDEO)
        {
            v_stream_idx = i;
            break;
        }
    }

    if (v_stream_idx == -1)
    {
        printf("%s","找不到視訊流\n");
        return;
    }

    //只有知道視訊的編碼方式,才能夠根據編碼方式去找到解碼器
    //獲取視訊流中的編解碼上下文
    AVCodecContext *pCodecCtx = pFormatCtx->streams[v_stream_idx]->codec;
    //4.根據編解碼上下文中的編碼id查詢對應的解碼
    AVCodec *pCodec = avcodec_find_decoder(pCodecCtx->codec_id);
    if (pCodec == NULL)
    {
        printf("%s","找不到解碼器\n");
        return;
    }

    //5.開啟解碼器
    if (avcodec_open2(pCodecCtx,pCodec,NULL)<0)
    {
        printf("%s","解碼器無法開啟\n");
        return;
    }

    //輸出視訊資訊
    printf("視訊的檔案格式:%s",pFormatCtx->iformat->name);
    printf("視訊時長:%d", (pFormatCtx->duration)/1000000);
    printf("視訊的寬高:%d,%d",pCodecCtx->width,pCodecCtx->height);
    printf("解碼器的名稱:%s",pCodec->name);

    //準備讀取
    //AVPacket用於儲存一幀一幀的壓縮資料(H264)
    //緩衝區,開闢空間
    AVPacket *packet = (AVPacket*)av_malloc(sizeof(AVPacket));

    //AVFrame用於儲存解碼後的畫素資料(YUV)
    //記憶體分配
    AVFrame *pFrame = av_frame_alloc();
    //YUV420
    AVFrame *pFrameYUV = av_frame_alloc();
    //只有指定了AVFrame的畫素格式、畫面大小才能真正分配記憶體
    //緩衝區分配記憶體
    uint8_t *out_buffer = (uint8_t *)av_malloc(avpicture_get_size(AV_PIX_FMT_YUV420P, pCodecCtx->width, pCodecCtx->height));
    //初始化緩衝區
    avpicture_fill((AVPicture *)pFrameYUV, out_buffer, AV_PIX_FMT_YUV420P, pCodecCtx->width, pCodecCtx->height);

    //用於轉碼(縮放)的引數,轉之前的寬高,轉之後的寬高,格式等
    struct SwsContext *sws_ctx = sws_getContext(pCodecCtx->width,pCodecCtx->height,pCodecCtx->pix_fmt,
                                                pCodecCtx->width, pCodecCtx->height, AV_PIX_FMT_YUV420P,
                                                SWS_BICUBIC, NULL, NULL, NULL);
    int got_picture, ret;

    FILE *fp_yuv = fopen(output, "wb+");

    int frame_count = 0;

    //6.一幀一幀的讀取壓縮資料
    while (av_read_frame(pFormatCtx, packet) >= 0)
    {
        //只要視訊壓縮資料(根據流的索引位置判斷)
        if (packet->stream_index == v_stream_idx)
        {
            //7.解碼一幀視訊壓縮資料,得到視訊畫素資料
            ret = avcodec_decode_video2(pCodecCtx, pFrame, &got_picture, packet);
            if (ret < 0)
            {
                printf("%s","解碼錯誤");
                return;
            }

            //為0說明解碼完成,非0正在解碼
            if (got_picture)
            {
                //AVFrame轉為畫素格式YUV420,寬高
                //2 6輸入、輸出資料
                //3 7輸入、輸出畫面一行的資料的大小 AVFrame 轉換是一行一行轉換的
                //4 輸入資料第一列要轉碼的位置 從0開始
                //5 輸入畫面的高度
                sws_scale(sws_ctx, pFrame->data, pFrame->linesize, 0, pCodecCtx->height,
                          pFrameYUV->data, pFrameYUV->linesize);

                //輸出到YUV檔案
                //AVFrame畫素幀寫入檔案
                //data解碼後的影象畫素資料(音訊取樣資料)
                //Y 亮度 UV 色度(壓縮了) 人對亮度更加敏感
                //U V 個數是Y的1/4
                int y_size = pCodecCtx->width * pCodecCtx->height;
                fwrite(pFrameYUV->data[0], 1, y_size, fp_yuv);
                fwrite(pFrameYUV->data[1], 1, y_size / 4, fp_yuv);
                fwrite(pFrameYUV->data[2], 1, y_size / 4, fp_yuv);

                frame_count++;
                printf("解碼第%d幀\n",frame_count);
            }
        }

        //釋放資源
        av_free_packet(packet);
    }

    fclose(fp_yuv);

    av_frame_free(&pFrame);

    avcodec_close(pCodecCtx);

    avformat_free_context(pFormatCtx);

}

複製程式碼

3.2.音訊解碼

#include <stdlib.h> #include <stdio.h> #include <unistd.h>

//封裝格式 #include "libavformat/avformat.h" //解碼 #include "libavcodec/avcodec.h" //縮放 #include "libswscale/swscale.h" #include "libswresample/swresample.h"

int main (void) {

    //1.註冊元件     av_register_all();     //封裝格式上下文     AVFormatContext *pFormatCtx = avformat_alloc_context();

    //2.開啟輸入音訊檔案     if (avformat_open_input(&pFormatCtx, "test.mp3", NULL, NULL) != 0) {         printf("%s", "開啟輸入音訊檔案失敗");         return;     }     //3.獲取音訊資訊     if (avformat_find_stream_info(pFormatCtx, NULL) < 0) {         printf("%s", "獲取音訊資訊失敗");         return;     }

    //音訊解碼,需要找到對應的AVStream所在的pFormatCtx->streams的索引位置     int audio_stream_idx = -1;     int i = 0;     for (; i < pFormatCtx->nb_streams; i++) {         //根據型別判斷是否是音訊流         if (pFormatCtx->streams[i]->codec->codec_type == AVMEDIA_TYPE_AUDIO) {             audio_stream_idx = i;             break;         }     }     //4.獲取解碼器     //根據索引拿到對應的流,根據流拿到解碼器上下文     AVCodecContext *pCodeCtx = pFormatCtx->streams[audio_stream_idx]->codec;     //再根據上下文拿到編解碼id,通過該id拿到解碼器     AVCodec *pCodec = avcodec_find_decoder(pCodeCtx->codec_id);     if (pCodec == NULL) {         printf("%s", "無法解碼");         return;     }     //5.開啟解碼器     if (avcodec_open2(pCodeCtx, pCodec, NULL) < 0) {         printf("%s", "編碼器無法開啟");         return;     }     //編碼資料     AVPacket *packet = av_malloc(sizeof(AVPacket));     //解壓縮資料     AVFrame *frame = av_frame_alloc();          //frame->16bit 44100 PCM 統一音訊取樣格式與取樣率     SwrContext *swrCtx = swr_alloc();     //重取樣設定選項-----------------------------------------------------------start     //輸入的取樣格式     enum AVSampleFormat in_sample_fmt = pCodeCtx->sample_fmt;     //輸出的取樣格式 16bit PCM     enum AVSampleFormat out_sample_fmt = AV_SAMPLE_FMT_S16;     //輸入的取樣率     int in_sample_rate = pCodeCtx->sample_rate;     //輸出的取樣率     int out_sample_rate = 44100;     //輸入的聲道佈局     uint64_t in_ch_layout = pCodeCtx->channel_layout;     //輸出的聲道佈局     uint64_t out_ch_layout = AV_CH_LAYOUT_MONO;

    swr_alloc_set_opts(swrCtx, out_ch_layout, out_sample_fmt, out_sample_rate, in_ch_layout, in_sample_fmt,             in_sample_rate, 0, NULL);     swr_init(swrCtx);     //重取樣設定選項-----------------------------------------------------------end     //獲取輸出的聲道個數     int out_channel_nb = av_get_channel_layout_nb_channels(out_ch_layout);     //儲存pcm資料     uint8_t *out_buffer = (uint8_t *) av_malloc(2 * 44100);     FILE *fp_pcm = fopen("out.pcm", "wb");     int ret, got_frame, framecount = 0;     //6.一幀一幀讀取壓縮的音訊資料AVPacket     while (av_read_frame(pFormatCtx, packet) >= 0) {         if (packet->stream_index == audio_stream_idx) {             //解碼AVPacket->AVFrame             ret = avcodec_decode_audio4(pCodeCtx, frame, &got_frame, packet);             if (ret < 0) {                 printf("%s", "解碼完成");             }             //非0,正在解碼             if (got_frame) {                 printf("解碼%d幀", framecount++);                 swr_convert(swrCtx, &out_buffer, 2 * 44100, frame->data, frame->nb_samples);                 //獲取sample的size                 int out_buffer_size = av_samples_get_buffer_size(NULL, out_channel_nb, frame->nb_samples,                         out_sample_fmt, 1);                 //寫入檔案進行測試                 fwrite(out_buffer, 1, out_buffer_size, fp_pcm);             }         }         av_free_packet(packet);     }     fclose(fp_pcm);     av_frame_free(&frame);     av_free(out_buffer);     swr_free(&swrCtx);     avcodec_close(pCodeCtx);     avformat_close_input(&pFormatCtx);     return 0; }