是 LNDanmakuMaster 时间计算问题的QA
LNDanmakuMaster与通常的弹幕框架的区别之一是:在它的体系中没有速度概念,所有运动的进度都是直接从 时间 -> percent 的转换,进度都是用时间直接控制的,所以对时间进行了特殊的区分。
时间含义
一条弹幕从屏幕中"出现一点"到"完全消失"的总时间,称为弹幕存活时间,这个时间通常被分为两部分:弹幕时间和轨道时间;这两段时间不都是必须的,例如:在通常的移动轨道中我们认为 弹幕从屏幕右侧出现一点开始 到 弹幕完全露出的这段时间为弹幕时间,弹幕从完全露出到完全消失的这段时间为轨道时间;再例如:在中间出现一列那种定点轨道中,我们认为没有弹幕时间,只有轨道时间,因为弹幕立刻就出现了;所以,这两段时间的定义不是绝对的,根据轨道特性随机应变,我们认为通常弹幕的整体时间会受到轨道和弹幕自身两个方面影响,所以进行这样的划分。
这两段时间都存在Attributes中:弹幕时间是danmakuTime;轨道时间是:trackTime;二者之和是totalTime;这三个时间都是弹幕插入轨道前规定的,还有一个是currentTime,这个时间是弹幕已经存活了的时间,会在弹幕运行过程中不断更新的,currentTime/totalTime得到的percent就是弹幕动画运行的百分比,一个条形轨道中的一个弹幕的percent从0.f变化到1.f的过程就是这条弹幕从右侧移动到左侧的过程,如果是圆形轨道就是一圈,如果是sin轨道就是一个周期(或几个)。
还有一个时间是:totalTime - currentTime ,遇到“剩余存活时间”可以等价替换成这两个数相减。
弹幕碰撞检测
我画了两个图片来大致描述确保弹幕不会发生碰撞的两个临界条件,以条形轨道为例,我们称一个条形轨道中末尾的弹幕时弹幕A,称想要上轨道的那个弹幕为B,有以下两种情况可能发生碰撞(先不考虑弹幕间隔):
情况1:B高速
B弹幕比A弹幕速度快,那么B弹幕差点就追上A弹幕时的情况是:B的左侧和A的右侧同时到达了屏幕左侧。
也就是说:A的剩余存活时间 = =B的轨道时间
A.totalTime - A.currentTime == B.trackTime
A剩余时间越少,越不容易追上,所以这个条件改成:
A.totalTime - A.currentTime <= B.trackTime
如果让A、B保持一定的最小间距,这个条件就是:
A.totalTime - A.currentTime <= B.trackTime - spacingTimeInterval
情况2:B低速
如果我们只考虑末状态B不会追上A的单一条件,在B低速的情况下,A、B同时出发也会达到相同的末状态,在这个状态之前,A、B一直都是重叠的,所以我们还要增加初状态的判断。
B至少要等到A完全露出了才能上轨道,A的当前时间要大于自己的弹幕时间:
A.currentTime >= A.danmakuTime
如果要保持一定的距离:
A.currentTime >= A.danmakuTime + spacingTimeInterval
以上就是这个框架避免弹幕碰撞的判断方法,B在不满足这两个条件的时候会待在轨道的候补位上,只有同时满足两个条件了才会真正上轨道。
轨道状态估算
轨道状态估算方法意在向外界反馈轨道当前的状态信息,类似:忙不忙?下一个弹幕还要等多久?这样的信息。
繁忙度
- (NSTimeInterval)estimatedFinishTimeForCurrentAttributes;
这个函数返回的数字可以代表一条轨道的繁忙程度,条形轨道的繁忙程度定义为:要播放完目前轨道上已有的所有弹幕需要的时间。这个时间越久,说明这个轨道越繁忙,如果这个时间是0,说明这个轨道是空轨道。
计算方法如下:
每次轨道刷新时都会记录一个末尾的弹幕,同时每个轨道也会有一个等待中的候补位,所以一共是三种情况。
- 没有末尾弹幕,这个时候肯定也不会有候补弹幕,如果有候补弹幕就会变成末尾弹幕:所以是空轨道,总是返回0。
- 有末尾弹幕,但没有候补弹幕:这个时候播放完所有弹幕需要的时间就是播放完末尾弹幕的时间,所以就是末尾弹幕总存活时间 - 末尾弹幕已存活时间。
- 有末尾弹幕,也有候补弹幕:这个时候不光要播放完末尾弹幕还要等候补弹幕播放完,所以是:候补弹幕需要等待的时间 + 候补弹幕播放总时间。
这里解释3里面那个取MAX:
如果候补弹幕不能立刻播,说明他需要等待一段时间,等待的这段时间在上面那个碰撞的地方解释过了,末尾弹幕的剩余时间恰好等于候补弹幕的轨道时间才能播放: lastAttributes.totalTime - lastAttributes.currentTime == candidateAttributes.trackTime 所以左侧减去右侧的时间就是候补弹幕需要等待的时间: waitTime = lastAttributes.totalTime - lastAttributes.currentTime - candidateAttributes.trackTime;
然后把候补弹幕的等待时间和候补弹幕的总运动时间加载一起,会发现,候补弹幕的轨道时间被消掉了:
waitTime + candidateAttributes.totalTime = lastAttributes.totalTime - lastAttributes.currentTime - candidateAttributes.trackTime(和后面那个消掉了) + candidateAttributes.trackTime(和前面那个消掉了) + candidateAttributes.danmakuTime = lastAttributes.totalTime - lastAttributes.currentTime + candidateAttributes.danmakuTime.
所以这个算式就成了: restTime = MAX(self.candidateAttributes.trackTime, self.lastAttributes.totalAliveTime - self.lastAttributes.currentAliveTime [遗漏的+ spacingTimeInterval]) + self.candidateAttributes.danmakuTime ;
这个MAX的意思就是:如果需要等lastAttributes再走一会儿就走MAX(B),如果不需要等,就走MAX(A)候补弹幕的总时间。第二个条件还应该加一个spacingTimeInterval表示末尾弹幕和候补弹幕之间的间隔,这块应该是之前考虑的时候被遗漏了,spacingTime通常比弹幕时间小很多,所以影响不大。
最快显示时间
- (NSTimeInterval)estimatedMinDisplayWaitTimeFor:(LNDanmakuAbstractAttributes *)attributes
这个函数的意思是:传入一个attributes,估算它在这条轨道上最快显示出来需要等多久。这个条件是比繁忙度更加宽泛一些的条件,例如:两个都比较空闲的轨道,如果用繁忙度计算数值,一定有一个是更空闲的;如果用最快显示时间计算,弹幕加到两个轨道上都能立刻显示出来,所以都是等待0s,这两个轨道是等价的。
(再举个不恰当的例子,Jack和Tony如果硬算肯定有个是更富的;但如果让我估算一下,两个人是一样的,繁忙度就是硬算的,最快显示时间就是我估算的,所以后者是个更宽泛的范围:只要到了我无法想象的程度,都一样)
这个算法也分了三种情况考虑:
- 有候补弹幕:不论如何也无法插入,return 1000.f, 这个认为是弹幕存活时间最大值,一般不会有弹幕在屏幕上存活超过1000s,如果有,我就改成10000。
- 没有候补弹幕,也没有末尾弹幕:空轨道,等待时间是0.f。
- 没有候补弹幕,有末尾弹幕:我们计算了两个值:lowSpeedMaxWaitTime/highSpeedMaxWaitTime, 这个low和high就指的是之前检测碰撞的时候说的那个B低速、B高速。我们把即将插入的这个弹幕当做是candidate弹幕计算它还要等多久,要想两个条件都满足,只要尽量保证多等一会就好了,所以总是取较大的值。
额。。这里分了三种情况考虑,其实计算的都是同一回事,low/high数值的算式化简一下是一样的;我也有点忘记了具体的思想历程,应该是之前想的时候分了三种情况考虑,每个情况返回不同的值,后来总结成:总是取这三个值里最大的就行了,所以这里其实不必分成三种,取第一个判断里面的结果就可以,这个之后整理一下。
最大已存活时间
- (NSTimeInterval)estimatedMaxAliveTimeFor:(LNDanmakuAbstractAttributes *)attributes
这个函数的意思是:传入一个attributes,估算它在不超过当前的末尾弹幕情况下,可以设置的最大存活时间是多少。
这个函数用来做视频seek时候使用到的弹幕恢复功能,恢复功能目前已经支持了,但是Demo中的例子还没有给出,就是这个:VideoDanmakuDemoViewController,这个VC用来模拟做一个跟视频联动的例子,里面包括seek操作,因为我自己的计划转去整理另外一个库了,所以还没有写完这个VC。
弹幕恢复功能是指在seek时,瞬间从一个状态切换到另一个状态,而不是清空弹幕从右侧重新开始播放,这个功能根据我调研的一些主流平台来看只有B站的是这样的,而且每次恢复出来的状态总能一致,我没有在B站工作过不清楚B站是怎么实现的,我谨慎地猜测他们的弹幕信息应该是内嵌在视频流中,像字幕一样,所以总是恢复得很准确。
LNDanmakuMaster的弹幕seek过程是:清空Player + 再重新塞入对应时间点附近的弹幕,例如:如果你从10s seek到 20s,就会把当前DanmakuPlayer的、队列、轨道、屏幕上所有弹幕都清楚,然后在一个大致的相关区间内找到所有相关的弹幕,在这段时间起播所有弹幕的队列中找到满足这样条件的弹幕:20s在这条弹幕的起播时间和结束时间之间(如果你已知弹幕最久存活时间是5s,这个范围就是1520,如果是6s,这个范围就是1420,以此类推),找到这些弹幕后,会按照顺序将他们插入:屏幕(轨道)、候补位和Dispatcher的队列中,如果多出来了,就按照Dispatcher的溢出流程走。
所以这个函数的用处就是恢复过程中,将弹幕塞入轨道中时让这个弹幕顶到最前面,算出的这个时间就是不超过前面那个弹幕的最早时间点,这个时间会和(20-弹幕的起播时间)进行比较,如果比后者小,说明这个弹幕没有受前面弹幕影响,是正常起播时间播出的,就取后者;如果比后者大,说明这个弹幕被前面的弹幕推迟了,就取前者。
总结一下就是让弹幕顶到最前面的时候用的。。。
具体计算步骤和之前两个估算类似,就不赘述了。
最后
重新阅读代码确实能发现很多自己之前没有意识到的问题!