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操作,不会感觉到延迟,还减少了开销和主线程卡死的情况。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值