FFmpeg簡易播放器的實現-音頻播放

本文爲做者原創,轉載請註明出處:http://www.javashuo.com/article/p-hkvbjkbw-gw.htmlhtml

基於FFmpeg和SDL實現的簡易視頻播放器,主要分爲讀取視頻文件解碼和調用SDL播放兩大部分。
本實驗僅研究音頻播放的實現方式,不考慮視頻。git

FFmpeg簡易播放器系列文章以下:
[1]. FFmpeg簡易播放器的實現-最簡版
[2]. FFmpeg簡易播放器的實現-視頻播放
[3]. FFmpeg簡易播放器的實現-音頻播放
[4]. FFmpeg簡易播放器的實現-音視頻播放
[5]. FFmpeg簡易播放器的實現-音視頻同步github

1. 視頻播放器基本原理

下圖引用自「雷霄驊,視音頻編解碼技術零基礎學習方法」,因原圖過小,看不太清楚,故從新制做了一張圖片。
播放器基本原理示意圖
以下內容引用自「雷霄驊,視音頻編解碼技術零基礎學習方法」:shell

解協議
將流媒體協議的數據,解析爲標準的相應的封裝格式數據。視音頻在網絡上傳播的時候,經常採用各類流媒體協議,例如HTTP,RTMP,或是MMS等等。這些協議在傳輸視音頻數據的同時,也會傳輸一些信令數據。這些信令數據包括對播放的控制(播放,暫停,中止),或者對網絡狀態的描述等。解協議的過程當中會去除掉信令數據而只保留視音頻數據。例如,採用RTMP協議傳輸的數據,通過解協議操做後,輸出FLV格式的數據。緩存

解封裝
將輸入的封裝格式的數據,分離成爲音頻流壓縮編碼數據和視頻流壓縮編碼數據。封裝格式種類不少,例如MP4,MKV,RMVB,TS,FLV,AVI等等,它的做用就是將已經壓縮編碼的視頻數據和音頻數據按照必定的格式放到一塊兒。例如,FLV格式的數據,通過解封裝操做後,輸出H.264編碼的視頻碼流和AAC編碼的音頻碼流。安全

解碼
將視頻/音頻壓縮編碼數據,解碼成爲非壓縮的視頻/音頻原始數據。音頻的壓縮編碼標準包含AAC,MP3,AC-3等等,視頻的壓縮編碼標準則包含H.264,MPEG2,VC-1等等。解碼是整個系統中最重要也是最複雜的一個環節。經過解碼,壓縮編碼的視頻數據輸出成爲非壓縮的顏色數據,例如YUV420P,RGB等等;壓縮編碼的音頻數據輸出成爲非壓縮的音頻抽樣數據,例如PCM數據。網絡

音視頻同步
根據解封裝模塊處理過程當中獲取到的參數信息,同步解碼出來的視頻和音頻數據,並將視頻音頻數據送至系統的顯卡和聲卡播放出來。數據結構

2. 簡易播放器的實現-音頻播放

2.1 實驗平臺

實驗平臺:openSUSE Leap 42.3
FFmpeg版本:4.1
SDL版本:2.0.9
FFmpeg開發環境搭建可參考「ffmpeg開發環境構建ide

2.2 源碼流程分析

本實驗僅播放視頻文件中的聲音,而不顯示圖像。源碼流程參考以下:
FFmpeg簡易播放器-音頻播放流程圖函數

2.3 源碼清單

代碼已經變得挺長了,不貼完整源碼了,源碼參考:
https://github.com/leichn/exercises/blob/master/source/ffmpeg/player_audio/ffplayer.c

源碼清單中涉及的一些概念簡述以下:
container:
對應數據結構AVFormatContext
封裝器,將流數據封裝爲指定格式的文件,文件格式如AVI、MP4等。
FFmpeg可識別五種流類型:視頻video(v)、音頻audio(a)、attachment(t)、數據data(d)、字幕subtitle。

codec:
對應數據結構AVCodec
編解碼器。編碼器將未壓縮的原始圖像或音頻數據編碼爲壓縮數據。解碼器與之相反。

codec context:
對應數據結構AVCodecContext
編解碼器上下文。此爲很是重要的一個數據結構,後文分析。各API大量使用AVCodecContext來引用編解碼器。

codec par:
對應數據結構AVCodecParameters
編解碼器參數。新版本增長的字段。新版本建議使用AVStream->codepar替代AVStream->codec。

packet:
對應數據結構AVPacket
通過編碼的數據。經過av_read_frame()從媒體文件中獲取獲得的一個packet可能包含多個(整數個)音頻幀或單個
視頻幀,或者其餘類型的流數據。

frame:
對應數據結構AVFrame
解碼後的原始數據。解碼器將packet解碼後生成frame。

2.4 關鍵過程

幾個關鍵函數的說明直接寫在代碼註釋裏:

2.4.1 開啓音頻處理子線程

// B2. 打開音頻設備並建立音頻處理線程
// B2.1 打開音頻設備,獲取SDL設備支持的音頻參數actual_spec(指望的參數是wanted_spec,實際獲得actual_spec)
// 1) SDL提供兩種使音頻設備取得音頻數據方法:
//    a. push,SDL以特定的頻率調用回調函數,在回調函數中取得音頻數據
//    b. pull,用戶程序以特定的頻率調用SDL_QueueAudio(),向音頻設備提供數據。此種狀況wanted_spec.callback=NULL
// 2) 音頻設備打開後播放靜音,不啓動回調,調用SDL_PauseAudio(0)後啓動回調,開始正常播放音頻
wanted_spec.freq = p_codec_ctx->sample_rate;    // 採樣率
wanted_spec.format = AUDIO_S16SYS;              // S錶帶符號,16是採樣深度,SYS表採用系統字節序
wanted_spec.channels = p_codec_ctx->channels;   // 聲道數
wanted_spec.silence = 0;                        // 靜音值
wanted_spec.samples = SDL_AUDIO_BUFFER_SIZE;    // SDL聲音緩衝區尺寸,單位是單聲道採樣點尺寸x通道數
wanted_spec.callback = sdl_audio_callback;      // 回調函數,若爲NULL,則應使用SDL_QueueAudio()機制
wanted_spec.userdata = p_codec_ctx;             // 提供給回調函數的參數
if (SDL_OpenAudio(&wanted_spec, &actual_spec) < 0)
{
    printf("SDL_OpenAudio() failed: %s\n", SDL_GetError());
    goto exit4;
}

// B2.2 根據SDL音頻參數構建音頻重採樣參數
// wanted_spec是指望的參數,actual_spec是實際的參數,wanted_spec和auctual_spec都是SDL中的參數。
// 此處audio_param是FFmpeg中的參數,此參數應保證是SDL播放支持的參數,後面重採樣要用到此參數
// 音頻幀解碼後獲得的frame中的音頻格式未必被SDL支持,好比frame多是planar格式,但SDL2.0並不支持planar格式,
// 若將解碼後的frame直接送入SDL音頻緩衝區,聲音將沒法正常播放。因此須要先將frame重採樣(轉換格式)爲SDL支持的模式,
// 而後送再寫入SDL音頻緩衝區
s_audio_param_tgt.fmt = AV_SAMPLE_FMT_S16;
s_audio_param_tgt.freq = actual_spec.freq;
s_audio_param_tgt.channel_layout = av_get_default_channel_layout(actual_spec.channels);;
s_audio_param_tgt.channels =  actual_spec.channels;
s_audio_param_tgt.frame_size = av_samples_get_buffer_size(NULL, actual_spec.channels, 1, s_audio_param_tgt.fmt, 1);
s_audio_param_tgt.bytes_per_sec = av_samples_get_buffer_size(NULL, actual_spec.channels, actual_spec.freq, s_audio_param_tgt.fmt, 1);
if (s_audio_param_tgt.bytes_per_sec <= 0 || s_audio_param_tgt.frame_size <= 0)
{
    printf("av_samples_get_buffer_size failed\n");
    goto exit4;
}
s_audio_param_src = s_audio_param_tgt;

2.4.2 啓動音頻回調機制

// 暫停/繼續音頻回調處理。參數1表暫停,0表繼續。
// 打開音頻設備後默認未啓動回調處理,經過調用SDL_PauseAudio(0)來啓動回調處理。
// 這樣就能夠在打開音頻設備後先爲回調函數安全初始化數據,一切就緒後再啓動音頻回調。
// 在暫停期間,會將靜音值往音頻設備寫。
SDL_PauseAudio(0);

2.4.3 音頻回調函數

用戶實現的函數,由SDL音頻處理子線程回調

// 音頻處理回調函數。讀隊列獲取音頻包,解碼,播放
// 此函數被SDL按需調用,此函數不在用戶主線程中,所以數據須要保護
// \param[in]  userdata用戶在註冊回調函數時指定的參數
// \param[out] stream 音頻數據緩衝區地址,將解碼後的音頻數據填入此緩衝區
// \param[out] len    音頻數據緩衝區大小,單位字節
// 回調函數返回後,stream指向的音頻緩衝區將變爲無效
// 雙聲道採樣點的順序爲LRLRLR
void audio_callback(void *userdata, uint8_t *stream, int len)
{
    ...
}

2.4.4 音頻包隊列讀寫函數

用戶實現的函數,主線程向隊列尾部寫音頻包,SDL音頻處理子線程(回調函數處理)從隊列頭部取出音頻包

// 寫隊列尾部
int packet_queue_push(packet_queue_t *q, AVPacket *pkt)
{
    ...
}

// 讀隊列頭部
int packet_queue_pop(packet_queue_t *q, AVPacket *pkt, int block)
{
    ...
}

2.4.5 音頻解碼

音頻解碼功能封裝爲一個函數,將一個音頻packet解碼後獲得的聲音數據傳遞給輸出緩衝區。此處的輸出緩衝區audio_buf會由上一級調用函數audio_callback()在返回時將緩衝區數據提供給音頻設備。

int audio_decode_frame(AVCodecContext *p_codec_ctx, AVPacket *p_packet, uint8_t *audio_buf, int buf_size)
{
    AVFrame *p_frame = av_frame_alloc();
    
    int frm_size = 0;
    int res = 0;
    int ret = 0;
    int nb_samples = 0;             // 重採樣輸出樣本數
    uint8_t *p_cp_buf = NULL;
    int cp_len = 0;
    bool need_new = false;

    res = 0;
    while (1)
    {
        need_new = false;
        
        // 1 接收解碼器輸出的數據,每次接收一個frame
        ret = avcodec_receive_frame(p_codec_ctx, p_frame);
        if (ret != 0)
        {
            if (ret == AVERROR_EOF)
            {
                printf("audio avcodec_receive_frame(): the decoder has been fully flushed\n");
                res = 0;
                goto exit;
            }
            else if (ret == AVERROR(EAGAIN))
            {
                //printf("audio avcodec_receive_frame(): output is not available in this state - "
                //       "user must try to send new input\n");
                need_new = true;
            }
            else if (ret == AVERROR(EINVAL))
            {
                printf("audio avcodec_receive_frame(): codec not opened, or it is an encoder\n");
                res = -1;
                goto exit;
            }
            else
            {
                printf("audio avcodec_receive_frame(): legitimate decoding errors\n");
                res = -1;
                goto exit;
            }
        }
        else
        {
            // s_audio_param_tgt是SDL可接受的音頻幀數,是main()中取得的參數
            // 在main()函數中又有「s_audio_param_src = s_audio_param_tgt」
            // 此處表示:若是frame中的音頻參數 == s_audio_param_src == s_audio_param_tgt,那音頻重採樣的過程就免了(所以時s_audio_swr_ctx是NULL)
            //      不然使用frame(源)和s_audio_param_src(目標)中的音頻參數來設置s_audio_swr_ctx,並使用frame中的音頻參數來賦值s_audio_param_src
            if (p_frame->format         != s_audio_param_src.fmt            ||
                p_frame->channel_layout != s_audio_param_src.channel_layout ||
                p_frame->sample_rate    != s_audio_param_src.freq)
            {
                swr_free(&s_audio_swr_ctx);
                // 使用frame(源)和is->audio_tgt(目標)中的音頻參數來設置is->swr_ctx
                s_audio_swr_ctx = swr_alloc_set_opts(NULL,
                                                     s_audio_param_tgt.channel_layout, 
                                                     s_audio_param_tgt.fmt, 
                                                     s_audio_param_tgt.freq,
                                                     p_frame->channel_layout,           
                                                     p_frame->format, 
                                                     p_frame->sample_rate,
                                                     0,
                                                     NULL);
                if (s_audio_swr_ctx == NULL || swr_init(s_audio_swr_ctx) < 0)
                {
                    printf("Cannot create sample rate converter for conversion of %d Hz %s %d channels to %d Hz %s %d channels!\n",
                            p_frame->sample_rate, av_get_sample_fmt_name(p_frame->format), p_frame->channels,
                            s_audio_param_tgt.freq, av_get_sample_fmt_name(s_audio_param_tgt.fmt), s_audio_param_tgt.channels);
                    swr_free(&s_audio_swr_ctx);
                    return -1;
                }
                
                // 使用frame中的參數更新s_audio_param_src,第一次更新後後面基本不用執行此if分支了,由於一個音頻流中各frame通用參數同樣
                s_audio_param_src.channel_layout = p_frame->channel_layout;
                s_audio_param_src.channels       = p_frame->channels;
                s_audio_param_src.freq           = p_frame->sample_rate;
                s_audio_param_src.fmt            = p_frame->format;
            }

            if (s_audio_swr_ctx != NULL)        // 重採樣
            {
                // 重採樣輸入參數1:輸入音頻樣本數是p_frame->nb_samples
                // 重採樣輸入參數2:輸入音頻緩衝區
                const uint8_t **in = (const uint8_t **)p_frame->extended_data;
                // 重採樣輸出參數1:輸出音頻緩衝區尺寸
                // 重採樣輸出參數2:輸出音頻緩衝區
                uint8_t **out = &s_resample_buf;
                // 重採樣輸出參數:輸出音頻樣本數(多加了256個樣本)
                int out_count = (int64_t)p_frame->nb_samples * s_audio_param_tgt.freq / p_frame->sample_rate + 256;
                // 重採樣輸出參數:輸出音頻緩衝區尺寸(以字節爲單位)
                int out_size  = av_samples_get_buffer_size(NULL, s_audio_param_tgt.channels, out_count, s_audio_param_tgt.fmt, 0);
                if (out_size < 0)
                {
                    printf("av_samples_get_buffer_size() failed\n");
                    return -1;
                }
                
                if (s_resample_buf == NULL)
                {
                    av_fast_malloc(&s_resample_buf, &s_resample_buf_len, out_size);
                }
                if (s_resample_buf == NULL)
                {
                    return AVERROR(ENOMEM);
                }
                // 音頻重採樣:返回值是重採樣後獲得的音頻數據中單個聲道的樣本數
                nb_samples = swr_convert(s_audio_swr_ctx, out, out_count, in, p_frame->nb_samples);
                if (nb_samples < 0) {
                    printf("swr_convert() failed\n");
                    return -1;
                }
                if (nb_samples == out_count)
                {
                    printf("audio buffer is probably too small\n");
                    if (swr_init(s_audio_swr_ctx) < 0)
                        swr_free(&s_audio_swr_ctx);
                }
        
                // 重採樣返回的一幀音頻數據大小(以字節爲單位)
                p_cp_buf = s_resample_buf;
                cp_len = nb_samples * s_audio_param_tgt.channels * av_get_bytes_per_sample(s_audio_param_tgt.fmt);
            }
            else    // 不重採樣
            {
                // 根據相應音頻參數,得到所需緩衝區大小
                frm_size = av_samples_get_buffer_size(
                        NULL, 
                        p_codec_ctx->channels,
                        p_frame->nb_samples,
                        p_codec_ctx->sample_fmt,
                        1);
                
                printf("frame size %d, buffer size %d\n", frm_size, buf_size);
                assert(frm_size <= buf_size);

                p_cp_buf = p_frame->data[0];
                cp_len = frm_size;
            }
            
            // 將音頻幀拷貝到函數輸出參數audio_buf
            memcpy(audio_buf, p_cp_buf, cp_len);

            res = cp_len;
            goto exit;
        }

        // 2 向解碼器喂數據,每次喂一個packet
        if (need_new)
        {
            ret = avcodec_send_packet(p_codec_ctx, p_packet);
            if (ret != 0)
            {
                printf("avcodec_send_packet() failed %d\n", ret);
                av_packet_unref(p_packet);
                res = -1;
                goto exit;
            }
        }
    }

exit:
    av_frame_unref(p_frame);
    return res;
}

注意:
[1]. 一個音頻packet中含有多個完整的音頻幀,此函數每次只返回一個frame,當avcodec_receive_frame()指示須要新數據時才調用avcodec_send_packet()向編碼器發送一個packet。
[2]. 音頻frame中的數據格式未必被SDL支持,對於不支持的音頻frame格式,須要進行重採樣,轉換爲SDL支持的格式聲音才能正常播放
[3]. 解碼器內部會有緩衝機制,會緩存必定量的音頻幀,不沖洗(flush)解碼器的話,緩存幀是取不出來的,未沖洗(flush)解碼器狀況下,avcodec_receive_frame()返回AVERROR(EAGAIN),表示解碼器中改取的幀已取完了(固然緩存幀仍是在的),須要用avcodec_send_packet()向解碼器提供新數據。
[4]. 文件播放完畢時,應沖洗(flush)解碼器。沖洗(flush)解碼器的方法就是調用avcodec_send_packet(..., NULL),而後按以前一樣的方式屢次調用avcodec_receive_frame()將緩存幀取盡。緩存幀取完後,avcodec_receive_frame()返回AVERROR_EOF。

3. 編譯與驗證

3.1 編譯

gcc -o ffplayer ffplayer.c -lavutil -lavformat -lavcodec -lavutil -lswscale -lswresample -lSDL2

3.2 驗證

選用clock.avi測試文件,測試文件下載:clock.avi
查看視頻文件格式信息:

ffprobe clock.avi

打印視頻文件信息以下:

[avi @ 0x9286c0] non-interleaved AVI
Input #0, avi, from 'clock.avi':
  Duration: 00:00:12.00, start: 0.000000, bitrate: 42 kb/s
    Stream #0:0: Video: msrle ([1][0][0][0] / 0x0001), pal8, 320x320, 1 fps, 1 tbr, 1 tbn, 1 tbc
    Stream #0:1: Audio: truespeech ([34][0][0][0] / 0x0022), 8000 Hz, mono, s16, 8 kb/s

運行測試命令:

./ffplayer clock.avi

能夠聽到每隔1秒播放一次「嘀」聲,播放12次後播放結束。播放過程只有聲音,沒有圖像窗口。播放正常。

4. 參考資料

[1] 雷霄驊,視音頻編解碼技術零基礎學習方法
[2] 雷霄驊,最簡單的基於FFMPEG+SDL的視頻播放器ver2(採用SDL2.0)
[3] SDL WIKI, https://wiki.libsdl.org/
[4] Martin Bohme, An ffmpeg and SDL Tutorial, Tutorial 03: Playing Sound

5. 修改記錄

2018-12-04 V1.0 初稿 2019-01-06 V1.1 增長音頻重採樣,修復部分音頻格式沒法正常播放的問題

相關文章
相關標籤/搜索