上一篇音視頻同步策略和視頻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的時候去控制播放於否。
下面給出 三個線程的:
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操作,不會感覺到延遲,還減少了開銷和主線程卡死的情況。