天天看點

ffmpeg音視訊同步,seek政策總結。

作者:音視訊流媒體技術

上一篇音視訊同步政策和視訊seek政策講過一些方法,但是總視存在一些小問題,這裡花費了近三天的時間對整個 音視訊同步,以及seek測率進行較大的調整,使得整個程式更健壯,使用者在界面胡亂操作,seek和pause都不會引起程式卡頓和崩潰了。

音視訊seek政策最簡單的方法,就是一個大鎖,将音頻解碼 和 視訊解碼播放 各用同一個鎖鎖住,然後,将seek部分用同一個鎖鎖住,這樣seek的時候清空資料就不會導緻緩沖區有資料,或者死鎖問題,但是這樣效率很低,且看似音視訊各 一個線程,其實同時隻有一個線程能跑。這裡将自己的心血總結一些。大緻是對上一篇的優化。

結構圖

ffmpeg音視訊同步,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 等等)有需要的可以點選加群免費領取~

ffmpeg音視訊同步,seek政策總結。

下面給出 三個線程的:

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部落格