www.Walzer.cn - Tech & Management Blog

Focus on mobile dev
本博客文章,未在标题中写明转载的, 均为原创.
所谓高手,也就是熟悉别人制定的游戏规则、并且能在规则内跳舞的人。
  博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

自创视频解码前丢帧的“挂档算法”

Posted on 2007-05-01 10:47  Walzer  阅读(2069)  评论(4编辑  收藏  举报


上一篇文章《播放器上音频断续问题的原因》提到了最后通过在解码前丢掉一半数量视频帧的方法,保证了音频解码通路有足够的时间片来运转,以实现高码率解码时音频连续。当然最终产品上不可能强制丢弃定量的帧数了,需要做一个动态的丢帧算法,根据播放i当前的状态自动调整丢帧的幅度。由于做丢帧算法的前一晚我玩了会儿极品飞车,最近又在关注上海车展,所以丢帧的算法也就沾了沾灵感,做成类似汽车加速换档和刹车的处理逻辑啦。我觉得看播放器运转情况来决定丢帧到什么程度,与看路况决定汽车开多快,这两点在逻辑上是十分类似的。一共有几个要点:

1、察看播放器当前运转情况,主要判断依据是DECODER之前拿到的FRAME时间戳,与当前播放时间相减,得到的提前量。这个提前量就表示了播放器卡或者不卡。比如我这里放行几帧之后,DECODER解得比较吃力,于是DECODER线程抢占了DEMUX线程的时间片,那么下面几帧到来的提前量会更小了。

1、汽车加速是一个渐变过程,不可能直接从1档换到4档跳变。同样地,播放器的运行状态也应该理解成一个渐变过程,尤其是对于CBR恒定码率的播放。如果做成只根据当前帧的时间提前量来判断的跳变,可以预想到会出现这样的情况;在高码率上,一开始由于解码来不及,我立刻丢帧,连丢若干帧之后,下面几帧到达DECODER的时间大大提前,所以放行,结果DEOCDER连续解这几帧显得非常吃力,并且挤占了其他线程的时间片,播放器进入最恶劣的情况,于是音频就断了;下几个视频帧到达时判断状态不佳,被连续丢弃,由于连续丢弃造成时间空闲,那么再下几帧的到达时间又大大提前……播放器就进入良性-恶性的不断摇摆中,音频还是要断的

2、汽车的刹车应该理解为一个跳变过程。事实上只能说是比较快的渐变,但我们总希望刹车越灵越好,最好直接从140kmh直接跳到0kmh,这样的车才是最安全的。播放器上面一样,一旦播放情况恶化,希望它能立刻作出反应,把当前到达DECODER的帧丢弃,以在最短时间内改善播放性能,否则只要恶化的情况持续片刻,比如几百毫秒,那么脆弱的音频就会断掉了。

3、两档之间应该有个用来确认加速有效的时间段,汽车不可能挂了二档后立刻就挂三档,而是应该在挂二档后确认一下这次提速是安全的,然后看看是否还有提速的空间,再挂三档。

4、车能开多快,应该是看当前路况上还有多少提速空间,而不是当前行驶速度。车速和路况有一个“最佳适合点”。比如我现在开到80kmh,和路况正好适合,那么就没必要提速到90kmh,如果一提速,可能就要撞车了。同样地,比如视频帧到达时间提前了300ms, 这是一个比较合适的提前量区间,如果我认为既然可以提前到300ms,那不妨放行更多帧去解码,那么很可能你会在2秒后看到提前量已经降到了50ms,这时就必须丢弃更多的帧来调整了。

5、越低的档,其加速过程越短;比如从1档加速到2档只需要很短的时间;而从4档换到5档,就需要比较长的加速时间。播放器上面也一样,在放行更多帧之前,需要越来越长的时间用以观察是否符合提档的标准。

说了这么多,我还是贴上代码和注释吧。

  1bool JudgeDrop(UINT16 FrameSeqNum, s64 FrameTimestamp)  //FrameSeqNum是当前帧在整个视频文件中处于第几帧的序号,FrameTimestamp是当前帧的时间戳
  2{
  3    static int DropLevel = 5;  //我设置从1档到5档,5档是播放性能最快的情况,所有帧全部放行。比如解QVGA的视频时,就能走到最快档。我们以放行所有帧开局
  4    static unsigned int DropInertia = 0;  //掉帧的惯性。每换一次档,需要观察DropInertia数量的帧后,才能再次决定是否换档。阀值越高播放器越稳定,适合CBR,阀值越低则播放器越灵敏,适合VBR,具体阀值的设置就凭经验了,对不同平台显然是不同的。
  5    static s64 TotalSkew = 0;  //在DropInertia数量的帧里,统计总的时间提前量
  6    static int CanKickUp = 0;  //必须CanKickUp次数地证明可以挂更高档后,才允许升档。这时用来实现如上面第5点描述的,后面可以看到CanKickUp的值和当前档数成正比
  7    s64 skew;    
  8
  9#define UP_SHIFT         400000  //提前400毫秒才可以证明当前播放足够流畅,满足提档要求
 10#define NORMAL_SHIFT         300000  //正常播放时提前大概300毫秒。如果提前量落在300毫秒~400毫秒,那就不用提档了。
 11#define WORSE_SHIFT         250000  //情况不太好咯,需要丢帧咯
 12#define WORST_SHIFT        200000    //吃力啊,多丢一些
 13#define DEAD_SHIFT        150000    //去死吧,彻底搞不定
 14#define CAN_KCIKUP_TIMES     2    //CanKickUp和当前档数的比值。这个CAN_KCIKUP_TIMES越高,提速越不容易
 15
 16
 17
 18
 19    if( FrameSeqNum > pRvInfoM->ufFramesPerSecond  )
 20    {
 21        skew = FrameTimestamp - (pClkM->GetTimeMicroseconds()); //计算当前到达帧和当前播放时间的提前量
 22        TotalSkew += skew;    //加到累计提前量里头
 23        DropInertia++;        //掉帧的惯性。
 24    }

 25    else    //第一秒里的帧全部放行,我们先试探一下能放到什么程度。另一方面,第一帧到达frame render后,才能知道播放时间。
 26    {
 27        return false;
 28    }

 29
 30    //printf("skew = %lld, DropInertia = %d\n", skew, DropInertia);
 31
 32    if( DropInertia >= (pRvInfoM->ufFramesPerSecond >> 1) )  //我们已经统计了半秒钟内所有帧的提前量情况,应该判断是否挂档了
 33    {
 34        int DestLevel;
 35        s64 AverageSkew = TotalSkew / DropInertia;    //取得平均提前量
 36
 37        //printf("AverageSkew = %lld\n", AverageSkew);
 38
 39        TotalSkew = 0;      //完成任务,让统计提前量的工作,从下一帧重新开始
 40        DropInertia = 0;  //同上
 41
 42        if( AverageSkew > UP_SHIFT )   //真强,播放够快
 43        {
 44            DestLevel = DropLevel + 1;  //我们可以考虑提一档
 45        }

 46        else if( AverageSkew < DEAD_SHIFT )  //最慢最慢的情况了,慢得跟蜗牛似的
 47        {
 48            DestLevel = 1;        //去死吧,挂最低档,除了参考帧和关键帧,全部给我扔掉
 49        }

 50        else if( AverageSkew < WORST_SHIFT )  //不是吧,这么慢
 51        {
 52            DestLevel = 2;        //丢掉1/2的帧数
 53        }

 54        else if( AverageSkew < WORSE_SHIFT )  //偏慢了
 55        {
 56            DestLevel = 3;        //丢掉1/3的帧看看
 57        }

 58        else if( AverageSkew < NORMAL_SHIFT )
 59        {
 60            DestLevel = 4;        //没事丢帧玩,丢个1/4意思意思
 61        }

 62        else  // NORMAL_SHIFT ~ UP_SHIFT,正常播放性能
 63        {
 64            DestLevel = DropLevel;    //维持现有档数,不换了。
 65        }

 66
 67        if( DestLevel > DropLevel )  //情况比现在要好些,考虑提档
 68        {
 69            CanKickUp++;    //你证明了自己的实力,不错不错,但要继续证明实力才可以。
 70
 71            if( CanKickUp > CAN_KCIKUP_TIMES * DropLevel )  //恭喜恭喜,终于可以提档了。在这里可以看到换越高档越难。1到2档只要连续达到提档条件2次,2到3需要达到条件4次,3到4需要6次,4到5需要8次。而连续8次统计就是需要4秒的时间,其间只要有一次没达到提档标准,就重新来过,要求很严格哦。
 72            {
 73                CanKickUp = 0;  //准备计算下一次提档条件
 74                DropLevel++;  //修成正果,提档!
 75            }

 76            
 77            //printf("new drop level = %d\n", DropLevel);
 78        }

 79        else if( DestLevel < DropLevel ) //不行嘛,菜菜的,要降级咯
 80        {
 81            CanKickUp = 0;  //不论前面满足提档标准多少次,只要一次不成,就重新来过。
 82            DropLevel = DestLevel; //刹车是跳变
 83            //printf("new drop level = %d\n", DropLevel);
 84        }

 85        else
 86        {
 87            CanKickUp = 0;  //DestLevel == DropLevel, 情况正常,没必要给你提档了。
 88        }

 89
 90    }

 91
 92
 93    if(    ( DropLevel >= 2 )&& ( DropLevel <= 4 ) &&  //2~4档都需要丢帧的
 94        ( FrameSeqNum % DropLevel == 0 ) &&    //根据帧的序号抽取这几分几的帧来丢弃
 95        (!pDecodeM->bInputFrameIsReference )  ) //如果是参考帧,比如B帧,那是不能丢的,不然后面的B帧全部解不出来了
 96    {
 97        //printf("drop frame seq = %d, DropLevel = %d\n", FrameSeqNum, DropLevel);
 98        return true;  //是的,丢!
 99    }

100    else if( ( ( DropLevel == 1 ) || ( skew < 0 )) &&  //在最坏情况上,或者当前帧到达时已经延迟了,那么就丢弃除了参考帧外其他的所有帧
101             (!pDecodeM->bInputFrameIsReference ) )
102    {
103        return true;  //
104    }

105    else
106    {
107        return false//真不容易,通过了严格的盘查,放行~~
108    }

109
110}

111


这段代码的运行效果良好,让video decoder腾出更多时间来给audio decoder运作,保证了音频输出的流畅连贯。