上一篇文章《播放器上音频断续问题的原因》提到了最后通过在解码前丢掉一半数量视频帧的方法,保证了音频解码通路有足够的时间片来运转,以实现高码率解码时音频连续。当然最终产品上不可能强制丢弃定量的帧数了,需要做一个动态的丢帧算法,根据播放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档,就需要比较长的加速时间。播放器上面也一样,在放行更多帧之前,需要越来越长的时间用以观察是否符合提档的标准。
说了这么多,我还是贴上代码和注释吧。
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运作,保证了音频输出的流畅连贯。