[6] ffmpeg + SDL2 實現的視訊播放器「視音訊同步」
阿新 • • 發佈:2019-01-05
日期:2016.10.8
作者:isshe
github:github.com/isshe
郵箱:[email protected]
平臺:ubuntu16.04 64bit
前言
- 這個程式使用的視音訊同步方法是視訊同步音訊。接下來大概還會學習其他方法,不過下一步應該是先完善功能,實現暫停,播放之類的。
- 這個版本中是用的是較新的兩個解碼函式avcodec_send_packet(), avcode_receive_frame()。如果舊版本沒有,就換回avcodec_decode_video2()即可。
以下寫著”問題:”的,如果懂請回答,謝謝。
程式結構圖
- 主執行緒開一個執行緒專門負責讀packet放到兩個佇列。
- 然後主執行緒開音訊裝置。
- 主執行緒開新執行緒給重新整理函式(refresh_func)
- 然後主執行緒回去等待事件發生。
同步的思路
視訊同步音訊的方法的實質是:如果視訊慢了就延時短點,如果快了就延時長點。
大體思路是:獲取音訊時間,獲取視訊幀顯示時間,比較兩個時間判斷快了還是慢了,在上一幀的延時上做相應增減。
以音訊為基準的話,如何獲取音訊的當前播放時間(cur_audio_clock)?
- 用av_p2d()把時基(time_base)轉換成double型別,再乘以當前packet.pts即可得到當前packet的播放時間(audio_clock)。
- 因為一個packet中可能有多幀資料,所以獲取的audio_clock大概只是前面幀的時間戳。
後面的通過計算獲得:
- a.計算每秒播放的位元組(bytes)數x,
- b.獲取當前緩衝區index。
- c. 用index/x即可得到額外時間。
- d. 用audio_clock + index/x 即可算出當前播放時間。
- x的計算方法:樣本率 * 每個樣本大小 * 通道數。
- 圖示:
又如何獲取視訊的當前播放時間(cur_frame_pts)?
- 當前packet.pts * time_base + extra_delay
當前幀的顯示時間戳 * 時基 +額外的延遲時間。
extra_delay = repeat_pict / (2*fps) (從官網看來的)
- 當前packet.pts * time_base + extra_delay
如何調整視訊?
- 用當前幀pts減去上一幀的pts得到上一幀的延時。
- 對比上面獲得的兩個時間(視訊當前時間、音訊當前時間)判斷是快了還是慢了。
當 cur_frame_pts - cur_audio_clock > 0 說明快了,小於就慢了。
設定一個最大容忍值和一個最小容忍值。
以上一幀延時為基礎,當慢太多就減小延時(delay), 當快太多就加大延時(delay)。
以上就是個人對視訊同步音訊方法的大體理解 , 如有不對請指出,感謝!
主要部分分析
1. 獲取音訊的當前播放時間
- 獲取當前packet的時間
if (packet.pts != AV_NOPTS_VALUE)
{
ps->audio_clock = packet.pts * av_q2d(ps->paudio_stream->time_base);
}
1.av_q2d()是Convert rational to double.
AVRational結構有兩個成員:
分子:int nun
分母:int den
- 獲取當前音訊播放時間
double get_audio_clock(PlayerState *ps)
{
long long bytes_per_sec = 0;
double cur_audio_clock = 0.0;
double cur_buf_pos = ps->audio_buf_index;
//每個樣本佔2bytes。16bit
bytes_per_sec = ps->paudio_stream->codec->sample_rate
* ps->paudio_codec_ctx->channels * 2;
cur_audio_clock = ps->audio_clock +
cur_buf_pos / (double)bytes_per_sec;
return cur_audio_clock;
}
- sample_rate:取樣率(例如44100)
- channels:通道數
- 2代表2bytes。每個樣本的大小(format指定)
- 獲取當前音訊播放時間:audio_clock + packet已經播放的時間.
另一種獲取當前時間的方法:
audio_clock+整個packet能播的時間 - 還沒播的時間。 (我參考的程式碼就是這種方法。)
獲取視訊frame的顯示時間
概念:
- PTS:顯示時間戳。
- DTS:解碼時間戳。
- 時基(time_base): 相當於一個單位。(大概是類似與s(秒)之類的)
- ffmpeg中每個階段有不同是時基。
- 程式碼中用到兩個來調整時間,分別是:AVStream、AVCodecContext的time_base(都是以秒為單位,轉換的時候要注意,SDL_Delay用的是毫秒, SDL_AddTimer用的是毫秒, av_gettime()用的是微秒)「1s = 1000ms = 1000000us」.
獲取packet.pts並調整
double get_frame_pts(PlayerState *ps, AVFrame *pframe)
{
double pts = 0.0;
double frame_delay = 0.0;
pts = av_frame_get_best_effort_timestamp(pframe);
if (pts == AV_NOPTS_VALUE) //???
{
pts = 0;
}
pts *= av_q2d(ps->pvideo_stream->time_base);
if (pts != 0)
{
ps->video_clock = pts;
}
else
{
pts = ps->video_clock;
}
//更新video_clock
//這裡用的是AVCodecContext的time_base
//extra_delay = repeat_pict / (2*fps), 這個公式是在ffmpeg官網手冊看的
frame_delay = av_q2d(ps->pvideo_stream->codec->time_base);
frame_delay += pframe->repeat_pict / (frame_delay * 2);
ps->video_clock += frame_delay;
return pts;
}
- 以中間的av_q2d為界,av_q2d以及以上程式碼為計算frame的pts。
當獲取不到的時候,就用av_q2d以下的程式碼來設定。
問題:什麼時候下會獲取不到呢?
同步調整
- 同步就是快了就加延時(delay),慢了就減延時(delay)。
- 下面程式碼中,兩個函式都是自定義的。
- get_frame_pts():獲取當前幀的顯示時間戳。
- get_delay():獲取延時時間。注意返回的是一個double型別,返回的時間是 s(秒)。給SDL_Delay()用的時候要*1000。
- (本來嘗試在get_delay()內部轉換單位,返回一個整型,但是相對比較麻煩,容易出錯。)
pts = get_frame_pts(ps, pframe);
//ps中用cur_frame_pts是為了減少get_delay()的引數
ps->cur_frame_pts = pts; //*(double *)pframe.opaque;
ps->delay = get_delay(ps) * 1000 + 0.5;
- get_delay()函式程式碼
double get_delay(PlayerState *ps)
{
double ret_delay = 0.0;
double frame_delay = 0.0;
double cur_audio_clock = 0.0;
double compare = 0.0;
double threshold = 0.0;
//這裡的delay是秒為單位, 化為毫秒:*1000
//獲取兩幀之間的延時
frame_delay = ps->cur_frame_pts - ps->pre_frame_pts;
if (frame_delay <= 0 || frame_delay >= 1.0)
{
frame_delay = ps->pre_cur_frame_delay;
}
//兩幀之間的延時存到統籌結構
ps->pre_cur_frame_delay = frame_delay;
ps->pre_frame_pts = ps->cur_frame_pts;
cur_audio_clock = get_audio_clock(ps);
//compare < 0 說明慢了, > 0說明快了
compare = ps->cur_frame_pts - cur_audio_clock;
//設定閥值, 是一個正數,最小閥值取它的負數。
//這裡設閥值為兩幀之間的延遲,
threshold = frame_delay;
//SYNC_THRESHOLD ? frame_delay : SYNC_THRESHOLD;
if (compare <= -threshold) //慢, 加快速度,
{
ret_delay = frame_delay / 2; //這裡用移位更好快
}
else if (compare >= threshold) //快了,就在上一幀延時的基礎上加長延時
{
ret_delay = frame_delay * 2; //這裡用移位更好快
}
else
{
ret_delay = frame_delay;
}
return ret_delay;
}
所遇到的問題
1. ffmpeg共享記憶體導致的錯誤
- 本來的實現是用一個新執行緒負責解碼(packet->frame), 解碼後把frame放入佇列,像把packet放入佇列那樣。
這裡會有一個畫面閃爍或者嚴重丟幀的問題。
原因就是每次新建的frame入隊的時候,會共享記憶體, 這一個frame覆蓋前一個frame。
- 解決辦法有:
a. 把佇列長度設為1。每次只解碼一幀到佇列,取了以後再解。
b. 最容易想到的肯定是找不共享記憶體的方法了.- 嘗試過不共享記憶體的方法有:
av_frame_clone(),
av_frame_alloc()以後av_frame_ref()「這個看了手冊,但不理解,只是用了」
av_malloc()後用memcpy()
av_frame_alloc後用memcpy().
都失敗了。然後用a的方法實現了同步後,重寫這部分程式碼,把frame佇列刪了。
參考資料
程式碼下載
- 接觸ffmpeg已經8天了,搭環境用了兩天,因為不知道怎麼才算對,搭好了編譯下載的程式不過,又再搭如此反覆。最後還是看雷神的視訊才懂,真的感謝他。
- 學習這幾個程式,一次次重寫,學習到了統籌結構很重要,也很好用。
- 還學習到,當出現錯誤的時候,
- 如果是有不懂的函式,結構,要去查詢,理解再繼續。
- 如果函式之類都沒問題,是邏輯之類問題,就認真看程式,去想,思考。
- 而不要盲目地一個個去試錯。