ffmpeg簡易播放器的實現-音訊播放
基於FFmpeg和SDL實現的簡易視訊播放器,主要分為讀取視訊檔案解碼和呼叫SDL顯示兩大部分。詳細流程可參考程式碼註釋。
本篇實驗筆記主要參考如下兩篇文章:
[1]. 最簡單的基於FFMPEG+SDL的視訊播放器ver2(採用SDL2.0)
[2]. An ffmpeg and SDL Tutorial
1. 視訊播放器基本原理
下圖引用自“雷霄驊,視音訊編解碼技術零基礎學習方法”,因原圖太小,看不太清楚,故重新制作了一張圖片。
如下內容引用自“雷霄驊,視音訊編解碼技術零基礎學習方法”:
解協議
將流媒體協議的資料,解析為標準的相應的封裝格式資料。視音訊在網路上傳播的時候,常常採用各種流媒體協議,例如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開發環境構建”
2.2 原始碼流程分析
本實驗僅播放視訊檔案中的聲音,而不顯示影象。原始碼流程參考如下:
2.3 關鍵函式
幾個關鍵函式的說明直接寫在程式碼註釋裡:
2.3.1 開啟音訊處理子執行緒
// 開啟音訊裝置並建立音訊處理執行緒。期望的引數是wanted_spec,實際得到的硬體引數是actual_spec // 1) SDL提供兩種使音訊裝置取得音訊資料方法: // a. push,SDL以特定的頻率呼叫回撥函式,在回撥函式中取得音訊資料 // b. pull,使用者程式以特定的頻率呼叫SDL_QueueAudio(),向音訊裝置提供資料。此種情況wanted_spec.callback=NULL // 2) 音訊裝置開啟後播放靜音,不啟動回撥,呼叫SDL_PauseAudio(0)後啟動回撥,開始正常播放音訊 SDL_AudioSpec wanted_spec; SDL_AudioSpec actual_spec; 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 = audio_callback; // 回撥函式,若為NULL,則應使用SDL_QueueAudio()機制 wanted_spec.userdata = p_codec_ctx; // 提供給回撥函式的引數 SDL_OpenAudio(&wanted_spec, &actual_spec);
2.3.2 啟動音訊回撥機制
// 暫停/繼續音訊回撥處理。引數1表暫停,0表繼續。
// 開啟音訊裝置後預設未啟動回撥處理,通過呼叫SDL_PauseAudio(0)來啟動回撥處理。
// 這樣就可以在開啟音訊裝置後先為回撥函式安全初始化資料,一切就緒後再啟動音訊回撥。
// 在暫停期間,會將靜音值往音訊裝置寫。
SDL_PauseAudio(0);
2.3.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.3.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.3.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 ret_size = 0;
int ret;
// 1 向解碼器喂資料,每次喂一個packet
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);
return -1;
}
ret_size = 0;
while (1)
{
// 2 接收解碼器輸出的資料,每次接收一個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");
return 0;
}
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");
break;
}
else if (ret == AVERROR(EINVAL))
{
printf("audio avcodec_receive_frame(): codec not opened, or it is an encoder\n");
}
else
{
printf("audio avcodec_receive_frame(): legitimate decoding errors\n");
}
}
// 3. 根據相應音訊引數,獲得所需緩衝區大小
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);
// 4. 將音訊幀拷貝到函式輸出引數audio_buf
memcpy(audio_buf, p_frame->data[0], frm_size);
if (frm_size > 0)
{
ret_size += frm_size;
}
}
av_frame_unref(p_frame);
return ret_size;
}
注意:
[1]. 一個音訊packet中含有多個完整的音訊幀,因此一次avcodec_send_packet()後,會多次呼叫avcodec_receive_frame()來將這一個packet解碼後的資料接收完。
[2]. 解碼器內部會有緩衝機制,會快取一定量的音訊幀,不沖洗(flush)解碼器的話,快取幀是取不出來的,未沖洗(flush)解碼器情況下,avcodec_receive_frame()返回AVERROR(EAGAIN),表示解碼器中改取的幀已取完了(當然快取幀還是在的),需要用avcodec_send_packet()向解碼器提供新資料。
[3]. 檔案播放完畢時,應沖洗(flush)解碼器。沖洗(flush)解碼器的方法就是呼叫avcodec_send_packet(..., NULL),然後按之前同樣的方式多次呼叫avcodec_receive_frame()將快取幀取盡。快取幀取完後,avcodec_receive_frame()返回AVERROR_EOF。
2.4 原始碼清單
程式碼已經變得挺長了,不貼完整原始碼了,原始碼參考:
https://github.com/leihl/leihl.github.io/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.3 編譯
gcc -o ffplayer ffplayer.c -lavutil -lavformat -lavcodec -lavutil -lswscale -lSDL2
2.4 測試
選用clock_320.avi測試檔案,此檔案
ffprobe clock_320.avi
列印視訊檔案資訊如下:
[avi @ 0x9286c0] non-interleaved AVI
Input #0, avi, from 'clock_320.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_320.avi
可以聽到每隔1秒播放一次“嘀”聲,播放12次後播放結束。播放過程只有聲音,沒有影象視窗。播放正常。
3. 參考資料
[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
4. 修改記錄
2018-12-04 V1.0 初稿