上一篇音視訊同步政策和視訊seek政策講過一些方法,但是總視存在一些小問題,這裡花費了近三天的時間對整個 音視訊同步,以及seek測率進行較大的調整,使得整個程式更健壯,使用者在界面胡亂操作,seek和pause都不會引起程式卡頓和崩潰了。
音視訊seek政策最簡單的方法,就是一個大鎖,将音頻解碼 和 視訊解碼播放 各用同一個鎖鎖住,然後,将seek部分用同一個鎖鎖住,這樣seek的時候清空資料就不會導緻緩沖區有資料,或者死鎖問題,但是這樣效率很低,且看似音視訊各 一個線程,其實同時隻有一個線程能跑。這裡将自己的心血總結一些。大緻是對上一篇的優化。
結構圖
如圖:有四個線程,橙色為條件判斷和指派。
demux 解封裝出來,分别為videoPacket 和 audioPacket,分别存入一個 list裡面。
audioThread 不停的從audioPacket list 取audioPacket 進行decode 和resample,并且将frame的pts和 重采樣資料data存儲在 audio data list 和 pts list中
videoThread 不停從 videoPacket list 取出 videoPacket 進行decode,然後于目前播放音頻 pts比較,小于就進行顯示
音頻播放線程,openSELS進行播放,在回調函數中取 audio data 和 pts 進行播放,并且将目前pts(curAudioPts) 設定為播放的pts
node: 為了友善播放器資源的管理,圖中其實還有個dataManager類沒有畫出,這個類是所有對象的成員變量,并且一個播放器隻能有一個,所有的資料都在dataManager 對象。一些 list 資料和播放器狀态都在 這個類對象當中,包括ffmpeg的一些解碼器 和 上下文 都存儲在裡面,當對播放器操作,seek 和 pause 和close的時候對資料的清理和通信,都是通過公用的dataManager來進行的
音視訊同步政策
同步測率沒有什麼變化,同上一篇一樣:因為視訊解碼後很大,不建議緩存,隻能緩存packet,然後與目前音頻比較,如果小于音頻的pts就顯示和播放。沒有多大的變化。
Pause政策
通過将音頻 線程 和 demux線程分開,現在音頻 、 視訊 、demux這三個線程都是完全獨立的,除了 同步那裡會阻塞其他地方都不會堵塞了。并且,在decode 和 resample的時候 不要用while(!isExit)沒取到資料就睡2ms然後繼續取資料。因為在過程中盡量不要堵塞,友善再後面暫停播放器。
pause:當我們每個線程的一個周期執行完畢後,再進行暫停,因為每個線程都是一秒至少30次,是以人是感覺不到這個暫停的延遲的,即線上程開通進行沉睡(2ms),然後看播放器狀态,選擇是否繼續睡眠。
node
如果用glsurfaceView的時候,可能繪制視訊的那個線程是主線程,可能不能堵塞哦。
demux的packet 必須存入後才能暫停,是以,需要用到while(!isExit),可能暫停會堵塞在這裡,是以 需要在堵塞的時候判斷是否isPauing (正在進行暫停操作),若是,則直接傳回不等待,在暫停和seek的時候,丢一幀是感覺不到的。
openSELS有資料就播放,沒資料就沒聲音,是以主要是在擷取音頻data的時候去控制播放于否。
相關學習資料推薦,點選下方連結免費報名,先碼住不迷路~】
【免費分享】音視訊學習資料包、大廠面試題、技術視訊和學習路線圖,資料包括(C/C++,Linux,FFmpeg webRTC rtmp hls rtsp ffplay srs 等等)有需要的可以點選加群免費領取~
下面給出 三個線程的:
demux線程:
void LammyOpenglVideoPlayer::demuxThreadMain()
{
while(!dataManager->isExit)
{
while(dataManager->demuxPauseReady)
{
LSleep(2);
continue;
}
/********************* 解封裝部分****************************/
int mode = ffmDemux->demux();
/********************* 解封裝部分結束****************************/
if(dataManager->isPausing)
{
LOGI("pause demuxPauseReady .open....");
dataManager->demuxPauseReady = true;
}
}
}
因為demux中要等待存入到packet list當中,才能進行下一次循環,是以暫停的時候,會卡在這裡,是以如果在暫停的時候就不存,直接傳回:
if(dataManager->isPausing)
{
dataManager->videoLock.unlock();
return 0;
}
視訊線程
視訊的播放在主線程,是以開頭沒有用while而是if:
void LammyOpenglVideoPlayer::videoThreadMain()
{
if(!dataManager->isExit)
{
if(dataManager->videoPauseReady){
LSleep(2);
LOGI("pause videoPauseReady .....");
return;
}
AVFrame * avFrame = ffMdecode->decode(0);
if(avFrame != 0 && avFrame != nullptr){
LSleep(2); LOGI("avFrame video show .....");
openglVideoShow->show(avFrame);
}
else if(avFrame == 0){
LSleep(2);
}
if(dataManager->isPausing)//&&dataManager->demuxPauseReady
{
LOGI("pause videoPauseReady .open....");
dataManager->videoPauseReady = true;
}
}else{
dataManager->isVideoRunning = false;
}
}
因為decode的時候取不到資料也不會堵塞,show的時候也不會堵塞,整個過程很快。 videoPauseReady 很快就會true,然後在開頭的地方為了不顯示并且不堵塞主線程,堵塞2ms後隻能直接傳回。
音頻線程
void LammyOpenglVideoPlayer::audioThreadMain()
{
while(!dataManager->isExit)
{
while(dataManager->audioPauseReady)
{
LSleep(2);
//continue;
}
/********************* 解碼重采樣部分****************************/
AVFrame * avFrame = ffMdecode->decode(1);
if(avFrame != 0 && avFrame != nullptr)
{
// LOGI("pause resample ..........");
ffmResample->resample(avFrame);
}else{
LSleep(2);
}
/********************* 解碼重采樣部分結束****************************/
if(dataManager->isPausing )
{
LOGI("pause audioPauseReady .open....");
dataManager->audioPauseReady = true;
}
}
音頻不在主線程,是以背景不停的解碼 和重采樣,存入到緩沖區
openSELS獲得資料
void OpenSLESAudioPlayer::getAudioData()
{
// 當暫停後,就等待
while ((dataManager->isPause)&&!dataManager->isExit){
LOGE("音頻暫停中。。。。。。。。");
LSleep(10);
continue;
}
char *data = nullptr;
while (!dataManager->isExit) {
dataManager->audioLock.lock();
if (dataManager->audioData.size() > 0 && dataManager->audioPts.size()>0) {
data = (char *) (dataManager->audioData.front());
dataManager->currentAudioPts = dataManager->audioPts.front();
dataManager->audioPts.pop_front();
dataManager->audioData.pop_front();
memcpy(buf,data,dataManager->audioDateSize);
free(data);
dataManager->audioLock.unlock();
return ;
}
LOGE("沒有資料了,等等");
dataManager->audioLock.unlock();
LSleep(2);
continue;
}
}
pause函數:
void LammyOpenglVideoPlayer::pauseOrContinue()
{
if(!dataManager->isPause)
{
dataManager->isPausing = true;
while(true)
{
if( dataManager->videoPauseReady &&dataManager->audioPauseReady&&dataManager->demuxPauseReady )
{
dataManager->isPause =true;
dataManager->isPausing =false;
// 隻有 取消暫停的時候才能 将下面置為true
// dataManager->videoPauseReady =false;
// dataManager->audioPauseReady =false;
// dataManager->demuxPauseReady=false;
LOGE("pause success");
return;
}else{
LSleep(20);
continue;
}
}
}
else
{
dataManager->isPause =false;
dataManager->isPausing =false;
dataManager->videoPauseReady =false;
dataManager->audioPauseReady =false;
dataManager->demuxPauseReady=false;
LOGE("un pause success");
return;
}
}
隻有當三個線程都準備完畢後,isPausing 完畢置為false,isPause為true。
這樣pause的政策就完成了,這個政策這樣設計主要是友善後面的seek操作。
seek政策
上面pause政策可以看出,暫停後,線程都會停留線上程的開頭,不會對解碼器或者重采樣等ffmpeg的資料進行操作,這樣可以省去不進行pause 和 seek的時候 大量的鎖操作,大大減少了開銷,并且 音頻 和 視訊的解碼完全獨立開來,不會解碼音頻的時候視訊就無法進行解碼。
seek政策:seek操作是在主線程,上一篇中講到無法快速點選seek,這會seek操作延遲會很嚴重,是以這裡進行了改進:
将seek操作放入子線程進行操作,防止堵塞主線程。
為了減小開銷和延遲,當使用者進行seek操作時,如果清理資料等一切操作完畢,而沒有進行 ffmpeg的seek操作時候,我們隻需要将seek的seekPos修改為最新的使用者點選的seekPos,前面的seekPos就不執行了。
增加的seekLock隻在 ffmpeg的seek的時候鎖住 和 點選seek鍵的時候判斷是否正在seeking當中這2步同步,這2個操作都很短,并且保證了程序中隻有一個seek線程。使用者點選seek鍵存在2種情況 :1、 一旦 進入了seekTo函數,下面的ffmpeg線程就無法seek操作,等修改好了seePos,直接seek到新點選的pos點,不建立線程。2、無法進入seekTo函數,等待 seek完畢,再建立線程進行seek。
先給出seek的函數:
float progress = 0;
void LammyOpenglVideoPlayer::seekTo(float seekPos)
{
LOGE("seekPos = %f", seekPos);
dataManager->seekLock.lock();
if (dataManager->isSeeking){
progress = seekPos;
LOGE(" progress = seekPos = %f", seekPos);
dataManager->seekLock.unlock();
return;
}else{
progress = seekPos;
std::thread seek_th(&LammyOpenglVideoPlayer::seekThreadMain,this);
seek_th.detach();
}
dataManager->seekLock.unlock();
}
void LammyOpenglVideoPlayer::seekThreadMain()
{
dataManager->isSeeking = true;
if(!dataManager-> isPause)
{
pauseOrContinue();
}
dataManager->clearData();
dataManager->seekLock.lock();
long long pos2 = dataManager->avFormatContext->streams[dataManager->videoStreamIndex]->duration* progress;
av_seek_frame(dataManager->avFormatContext, dataManager->videoStreamIndex,
pos2, AVSEEK_FLAG_FRAME|AVSEEK_FLAG_BACKWARD);
ffmDemux->seekTo(progress);
dataManager->currentAudioPts =LLONG_MAX;
pauseOrContinue();
dataManager->isSeeking = false;
dataManager->seekLock.unlock();
}
新的seek操作是異步的,并且隻會執行最新的seek操作,不會感覺到延遲,還減少了開銷和主線程卡死的情況。
原文 ffmpeg音視訊同步,seek政策總結。_ffmpeg seek_Lammyzp的部落格-CSDN部落格