本文為作者原創,轉載請注明出處:https://www.cnblogs.com/leisure_chn/p/10068490.html
基於 FFmpeg 和 SDL 實現的簡易視頻播放器,主要分為讀取視頻文件解碼和調用 SDL 播放兩大部分。本實驗僅研究音頻播放的實現方式,不考慮視頻。
FFmpeg 簡易播放器系列文章如下:
[1]. FFmpeg簡易播放器的實現1-最簡版
[2]. FFmpeg簡易播放器的實現2-視頻播放
[3]. FFmpeg簡易播放器的實現3-音頻播放
[4]. FFmpeg簡易播放器的實現4-音視頻播放
[5]. FFmpeg簡易播放器的實現5-音視頻同步
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 源碼清單
使用如下命令下載源碼:
svn checkout https://github.com/leichn/exercises/trunk/source/ffmpeg/player_audio
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 編譯
在源碼目錄運行:
./compiler.sh
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 增加音頻重采樣,修復部分音頻格式無法正常播放的問題