ffmpeg音視頻同步,seek策略總結。

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

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

結構圖

如圖:有四個線程,橙色爲條件判斷和賦值。

  1. demux 解封裝出來,分別爲videoPacket 和 audioPacket,分別存入一個 list裏面。
  2. audioThread 不停的從audioPacket list 取audioPacket  進行decode 和resample,並且將frame的pts和 重採樣數據data存儲在 audio data list 和 pts list中
  3. videoThread 不停從 videoPacket list 取出 videoPacket 進行decode,然後於當前播放音頻 pts比較,小於就進行顯示
  4. 音頻播放線程,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

  1. 如果用glsurfaceView的時候,可能繪製視頻的那個線程是主線程,可能不能堵塞哦。
  2. demux的packet 必須存入後才能暫停,因此,需要用到while(!isExit),可能暫停會堵塞在這裏,因此 需要在堵塞的時候判斷是否isPauing (正在進行暫停操作),若是,則直接返回不等待,在暫停和seek的時候,丟一幀是感覺不到的。
  3. 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操作延遲會很嚴重,因此這裏進行了改進:

  1. 將seek操作放入子線程進行操作,防止堵塞主線程。
  2. 爲了減小開銷和延遲,當用戶進行seek操作時,如果清理數據等一切操作完畢,而沒有進行 ffmpeg的seek操作時候,我們只需要將seek的seekPos修改爲最新的用戶點擊的seekPos,前面的seekPos就不執行了。
  3. 增加的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操作,不會感覺到延遲,還減少了開銷和主線程卡死的情況。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章