該文章屬于劉小壯原創(chuàng),轉(zhuǎn)載請注明:劉小壯

介紹
彈幕誕生于日本的視頻平臺,后來被B站這種短視頻平臺引入到國內(nèi),并在國內(nèi)發(fā)展壯大。后來逐漸被長視頻平臺所接受,現(xiàn)在視頻相關(guān)的應(yīng)用基本上都會(huì)有彈幕。
但是長視頻彈幕和B站這類的短視頻彈幕還不太一樣,短視頻平臺有自己特有的彈幕文化,所以彈幕更注重和用戶的互動(dòng)。長視頻平臺還是以看劇為主,彈幕類似于評論的功能,所以不能影響用戶看劇,彈幕不能太密集,而且相互之間最好不要有遮蓋,否則會(huì)對視頻內(nèi)容會(huì)有比較明顯的影響。
本篇文章主要從長視頻平臺的角度來講彈幕的實(shí)現(xiàn)原理,但其實(shí)短視頻平臺的彈幕也是同樣的原理,區(qū)別在于短視頻可能彈幕種類會(huì)多一些。
技術(shù)實(shí)現(xiàn)
畫布
以我公司應(yīng)用為例,有iPhone和iPad兩個(gè)平臺,在iPhone平臺上有橫豎屏的概念,都需要展示彈幕。在iPad上有大小屏的概念,也需要都展示彈幕。彈幕的技術(shù)方案肯定是兩個(gè)平臺用一套,但需要考慮跨不同設(shè)備和屏幕的情況。
所以,對于這個(gè)問題,我通過畫布的概念來解決通用性的問題。畫布并不區(qū)分屏幕大小和比例的概念,只是單純的用來展示彈幕,并不處理其他業(yè)務(wù)邏輯,通過一個(gè)Render類來控制畫布的渲染。對于不同設(shè)備上的差異,例如iPad字體大一些,iPhone字體小一些這種情況,通過config類來進(jìn)行控制,畫布內(nèi)部不做判斷。
小屏上畫布會(huì)根據(jù)比例少展示一些,大屏上則多展示一些。字體變大畫布也會(huì)根據(jù)比例和左右間距進(jìn)行控制,保證展示比例是對的,并且在屏幕寬高發(fā)生改變后,自動(dòng)適應(yīng)新的尺寸,不會(huì)出現(xiàn)彈幕銜接斷開的問題,例如iPad上大小屏切換。外部在使用時(shí),只需要傳入一個(gè)frame即可,不需要關(guān)注畫布內(nèi)部的調(diào)整。
彈幕軌道

從屏幕上來看,可以看到彈幕一般都是一行一行的。為了方便對彈幕視圖進(jìn)行管理,以及后續(xù)的擴(kuò)展工作,我對彈幕設(shè)計(jì)了“軌道”的概念。每一行都是一個(gè)軌道,對彈幕進(jìn)行橫向的管理,這一行包括速度、末端彈幕、高度等參數(shù),這些參數(shù)適用于這一行的所有彈幕。軌道是一個(gè)虛擬的概念,并沒有對應(yīng)的視圖。
軌道有對應(yīng)的類來實(shí)現(xiàn),類中會(huì)包含一個(gè)數(shù)組,數(shù)組中有這一行所有的彈幕。這個(gè)思路有點(diǎn)像玩過的一款游戲-節(jié)奏大師,里面也有音樂軌道的概念,每個(gè)軌道上對應(yīng)不同速度和顏色的音符,音符數(shù)量也是不固定的,根據(jù)節(jié)奏來決定。
軌道還有一個(gè)好處在于,對于不同速度的彈幕比較好控制。例如騰訊視頻的彈幕其實(shí)是不同速度的,但是你仔細(xì)觀察的話,可以發(fā)現(xiàn)他們的彈幕是“奇偶行不同速”,也就是奇數(shù)行一個(gè)速度,偶數(shù)行一個(gè)速度,讓人從感官上來覺得所有彈幕的速度都不一樣。如果通過軌道的方式就很好實(shí)現(xiàn),不同的軌道根據(jù)當(dāng)前所在行數(shù),對發(fā)出的彈幕設(shè)置不同的速度即可。

有時(shí)候看視頻過程中會(huì)從右側(cè)出現(xiàn)一條活動(dòng)彈幕,可能是視頻中的梗,也可能是類似于廣告的互動(dòng)。但是活動(dòng)彈幕出現(xiàn)時(shí)一般是單行清屏的,也就是和普通彈幕是互斥的,展示活動(dòng)彈幕的時(shí)候前后沒有普通彈幕。這種通過軌道的方式也比較好實(shí)現(xiàn),每條彈幕都對應(yīng)一個(gè)時(shí)間段,根據(jù)活動(dòng)彈幕的時(shí)間和速度,將活動(dòng)彈幕展示的前后時(shí)間,將這段時(shí)間軌道暫時(shí)關(guān)閉,只保留活動(dòng)彈幕即可。
輪詢
每條彈幕都對應(yīng)著一個(gè)展示時(shí)間,所以需要每隔一段時(shí)間就找一下有沒有需要展示的彈幕。我設(shè)計(jì)的方案是通過輪詢,來驅(qū)動(dòng)彈幕展示。
通過CADisplayLink來進(jìn)行輪訓(xùn),將frameInterval設(shè)置為60,即每秒輪詢一次。在輪詢的回調(diào)中查找有沒有要展示的彈幕,有的話就從上到下查找每條軌道,某條軌道有位置可以展示的話就交給這條軌道展示,如果所有軌道都有正在展示的彈幕,則將此條彈幕丟棄。是否有位置是根據(jù)屏幕最右側(cè),最后一條彈幕是否已經(jīng)展示完全,并且后面有空余位置來決定的。
對于取數(shù)據(jù)的部分,數(shù)據(jù)和視圖的邏輯是分離的,相互之間并沒有耦合關(guān)系。取數(shù)據(jù)時(shí)只是從一個(gè)很小的字典中,根據(jù)時(shí)間取出所用的彈幕數(shù)據(jù),并轉(zhuǎn)化為model。字典的數(shù)據(jù)很少,最多十秒的數(shù)據(jù),而且這里并不會(huì)接觸到讀數(shù)據(jù)庫的操作,也沒有網(wǎng)絡(luò)請求的邏輯,這些都是獨(dú)立的邏輯,后面會(huì)講到。
彈幕視圖
經(jīng)常看視頻的同學(xué)應(yīng)該會(huì)知道,彈幕的展示形式有很多,有帶明星頭像的、有帶點(diǎn)贊數(shù)的、帶矩形背景色的,很多種展示形態(tài)。為了更好的對視圖進(jìn)行組織,所以我采用的就是很普通的UIView的展示形式,并沒有為了性能去做很復(fù)雜的渲染操作。
用UIView的好處主要就是方便做布局和子視圖管理,但在屏幕上做動(dòng)畫時(shí),是對CALayer進(jìn)行渲染的。也就是說UIView就是用來做視圖組織,并不會(huì)直接參與渲染,這也符合蘋果的設(shè)計(jì)理念。
復(fù)用池

彈幕是一個(gè)高頻使用的控件,所以不能一直頻繁創(chuàng)建,以及添加和移除視圖,會(huì)對性能有影響。所以就像很多同學(xué)設(shè)計(jì)的模塊一樣,我也引入了緩存池的概念,我這里叫復(fù)用池。
彈幕復(fù)用池和UITableView的復(fù)用池類似,離開屏幕的彈幕會(huì)被放在復(fù)用池中等待復(fù)用,下次直接從復(fù)用池中取而不重新創(chuàng)建。彈幕視圖做的工作就是接收新的model對象,并根據(jù)彈幕類型進(jìn)行不同的視圖布局。
并且彈幕只會(huì)在創(chuàng)建時(shí)被addSubview一次,當(dāng)彈幕離開屏幕不會(huì)被從父視圖移除,這樣彈幕從復(fù)用池中取出時(shí)也不需要被addSubview。當(dāng)動(dòng)畫執(zhí)行完成后,彈幕就直接留在動(dòng)畫結(jié)束的位置,下次做動(dòng)畫時(shí)彈幕會(huì)自動(dòng)回到fromValue的位置。實(shí)際上視圖結(jié)構(gòu)就如上圖所示,灰色區(qū)域就是可視區(qū)域。
系統(tǒng)彈幕

在視頻剛開始時(shí)會(huì)有引導(dǎo)信息,比如引導(dǎo)用戶發(fā)彈幕,或者提示彈幕有多少條,這個(gè)我們叫做系統(tǒng)彈幕。系統(tǒng)彈幕一般是展示到屏幕中間時(shí),才開始展示后續(xù)彈幕。但是要精確的計(jì)算到彈幕到達(dá)屏幕中間,然后再展示后續(xù)彈幕,這種的采用清除前后特定時(shí)間段的彈幕就不太精確,所以我們采用的是另一套實(shí)現(xiàn)方案。
系統(tǒng)彈幕的實(shí)現(xiàn)是通過一個(gè)更高精度的CADisplayLink進(jìn)行輪詢檢測,也就是把frameInterval設(shè)置的更小,我這里設(shè)置的是10,也就是每秒檢測六次。但是進(jìn)行檢測時(shí)不能直接用CALayer進(jìn)行判斷,需要使用presentationLayer也就是屏幕上正在展示的layer進(jìn)行檢測,通過這個(gè)layer獲取到的frame和屏幕上顯示的才是一致的。
這里簡單介紹一下CALayer的結(jié)構(gòu),我們都知道UIView是對CALayer的一層封裝,實(shí)際上屏幕上的顯示都是通過layer來實(shí)現(xiàn)的,而layer本身也分為以下三層,并有不同的功能。
- presentationLayer,其本身是當(dāng)前幀的一個(gè)拷貝,每次獲取都是一個(gè)新的對象,和動(dòng)畫過程中屏幕上顯示的位置是一樣的。
- modelLayer,表示
layer動(dòng)畫完成后的真實(shí)值,如果打印一下modelLayer和layer的話,發(fā)現(xiàn)二者其實(shí)是一個(gè)對象。 - renderLayer,渲染幀,應(yīng)用程序會(huì)根據(jù)視圖層級,構(gòu)成由
layer組成的渲染樹,renderLayer就代表layer在渲染樹中的對象。
炫彩彈幕

在播放彈幕的過程中,我們可以看到有漸變顏色的彈幕,我們叫做“炫彩彈幕”。這種彈幕有一個(gè)很明顯的特征,就是其顏色是漸變的。這時(shí)候要考慮性能的問題,因?yàn)椴シ鸥咔逡曨l時(shí)本身性能消耗就很大,在彈幕量比較大的情況下,會(huì)造成更多的性能消耗,所以減少性能消耗就是很重要的,漸變彈幕可能會(huì)使性能消耗加劇。
對于漸變文字,一般都是通過mask的方式實(shí)現(xiàn),下面放一個(gè)CAGradientLayer做漸變,上面蓋一個(gè)文字的layer。但是這種會(huì)觸發(fā)離屏渲染,會(huì)導(dǎo)致性能下降,并不能用這種方案。經(jīng)過我們的嘗試,決定用設(shè)置漸變文字顏色的方式解決。
CGFloat scale = [UIScreen mainScreen].scale;
UIGraphicsBeginImageContextWithOptions(imageSize, NO, scale);
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSaveGState(context);
CGColorSpaceRef colorSpace = CGColorGetColorSpace([[colors lastObject] CGColor]);
CGGradientRef gradient = CGGradientCreateWithColors(colorSpace, (CFArrayRef)ar, NULL);
CGPoint start = CGPointMake(0.0, 0.0);
CGPoint end = CGPointMake(imageSize.width, 0.0);
CGContextDrawLinearGradient(context, gradient, start, end, kCGGradientDrawsBeforeStartLocation | kCGGradientDrawsAfterEndLocation);
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
CGGradientRelease(gradient);
CGContextRestoreGState(context);
UIGraphicsEndImageContext();
實(shí)現(xiàn)方式就是先開辟一個(gè)上下文,用來進(jìn)行圖片繪制,隨后對上下文進(jìn)行一個(gè)漸變的繪制,最后獲取到一個(gè)UIImage,并將圖片賦值給UILabel的textColor即可。

從離屏檢測來看,并未發(fā)生離屏渲染,FPS也始終保持在一個(gè)很高的水平。
暫停和開始
彈幕是隨視頻播放和暫停的,所以需要對彈幕提供暫停和繼續(xù)的支持,對于這塊我采用的CAMediaTiming協(xié)議來處理,可以通過此協(xié)議對動(dòng)畫的過程進(jìn)行控制。
代碼中加0.05是為了避免彈幕在暫停時(shí)導(dǎo)致的回跳,所以加上一個(gè)時(shí)間差。具體原因是因?yàn)橥ㄟ^convertTime:fromLayer:方法計(jì)算得到的時(shí)間,和屏幕上彈幕的位置依然存在一個(gè)微弱的時(shí)間差,而導(dǎo)致渲染時(shí)視圖位置發(fā)生回跳,這個(gè)0.05是一個(gè)實(shí)踐得來的經(jīng)驗(yàn)值。
- (void)pauseAnimation {
// 增加判斷條件,避免重復(fù)調(diào)用
if (self.layer.speed == 0.f) {
return;
}
CFTimeInterval pausedTime = [self.layer convertTime:CACurrentMediaTime() fromLayer:nil];
self.layer.speed = 0.f;
self.layer.timeOffset = pausedTime + 0.05f;
}
- (void)resumeAnimation {
// 增加判斷條件,避免重復(fù)調(diào)用
if (self.layer.speed == 1.f) {
return;
}
CFTimeInterval pausedTime = self.layer.timeOffset;
self.layer.speed = 1.0;
self.layer.timeOffset = 0.0;
self.layer.beginTime = 0.0;
CFTimeInterval timeSincePause = [self.layer convertTime:CACurrentMediaTime() fromLayer:nil] - pausedTime;
self.layer.beginTime = timeSincePause;
}
CAMediaTiming協(xié)議是用來對動(dòng)畫過程控制的一個(gè)協(xié)議,例如通過CoreAnimation創(chuàng)建的動(dòng)畫,CALayer遵守了這個(gè)協(xié)議。這樣如果需要對動(dòng)畫進(jìn)行控制的話,不需要引用一個(gè)CABasicAnimation對象,然后再修改動(dòng)畫屬性這種方式對動(dòng)畫流程進(jìn)行控制,只需要直接對layer的屬性進(jìn)行修改即可。
下面是CAMediaTiming協(xié)議中一些關(guān)鍵的屬性,在上文中也用到了其中的部分屬性。
- beginTime,動(dòng)畫開始時(shí)間,可以控制動(dòng)畫延遲展示。一般是一個(gè)絕對時(shí)間,為了保證準(zhǔn)確性,最好先對當(dāng)前
layer進(jìn)行一個(gè)轉(zhuǎn)換,延遲展示在后面加對應(yīng)的時(shí)間即可。 - duration,動(dòng)畫結(jié)束時(shí)間。
- speed,動(dòng)畫執(zhí)行速度,默認(rèn)是1。動(dòng)畫最終執(zhí)行時(shí)間=
duration/speed,也就是duration是3秒,speed是2,最終動(dòng)畫執(zhí)行時(shí)間是1.5秒。 - timeOffset,控制動(dòng)畫進(jìn)程,主要用來結(jié)合
speed來對動(dòng)畫進(jìn)行暫停和開始。 - repeatCount,重復(fù)執(zhí)行次數(shù),和
repeatDuration互斥。 - repeatDuration,重復(fù)執(zhí)行時(shí)間,如果時(shí)間不是
duration的倍數(shù),最后一次的動(dòng)畫會(huì)執(zhí)行不完整。 - autoreverses,動(dòng)畫反轉(zhuǎn),在動(dòng)畫執(zhí)行完成后,是否按照原先的過程反向執(zhí)行一次。此屬性會(huì)對
duration有一個(gè)疊加效果,如果duration是1s,autoreverses設(shè)置為YES后時(shí)間就是2s。 - fillMode,如果想要?jiǎng)赢嬙陂_始時(shí),就停留在
fromValue的位置,就可以設(shè)置為kCAFillModeBackwards。如果想要?jiǎng)赢嫿Y(jié)束時(shí)停留在toValue的位置,就設(shè)置為kCAFillModeForwards,如果兩種都要就設(shè)置為kCAFillModeBoth,默認(rèn)是kCAFillModeRemoved,即動(dòng)畫結(jié)束后移除。
發(fā)送彈幕
插入彈幕

現(xiàn)在彈幕一般都會(huì)結(jié)合劇中主角,以及各種文字顏色讓你去選擇,通過這些功能也可以帶來一部分付費(fèi)用戶。當(dāng)發(fā)送一條彈幕時(shí),會(huì)從上到下查找軌道,查找軌道時(shí)是通過presentationLayer來進(jìn)行frame的判斷,如果layer的最右邊不在屏幕外,并且距離右側(cè)屏幕還有一定空隙,項(xiàng)目中寫的是10pt,則表示有空位可以插入下一條彈幕,這條彈幕會(huì)被放在這條軌道上。
如果當(dāng)前軌道沒有空位置,則從上到下逐條查找軌道,直到找到有空位的軌道。如果當(dāng)前屏幕上彈幕較多,所有軌道都沒有空位,則這一條彈幕會(huì)被拋棄。
如果是自己發(fā)的彈幕,這個(gè)是必須要展示出來的,因?yàn)橛脩舭l(fā)的彈幕要在界面上給用戶一個(gè)反饋。對于自己發(fā)的彈幕,會(huì)有一個(gè)插隊(duì)操作,優(yōu)先級比其他彈幕都要高。自己發(fā)的彈幕并不入本地?cái)?shù)據(jù)庫,只是進(jìn)行一個(gè)網(wǎng)絡(luò)請求傳給服務(wù)器,以及在界面上進(jìn)行展示。
選擇角色
在上面的圖片中可以看到,文本之前會(huì)有角色和角色名,這些都是獨(dú)立于輸入文字之外的。用戶如果刪除完輸入的文字之后,再點(diǎn)擊刪除要把角色也一起刪除掉。輸入框頁面構(gòu)成是一個(gè)UITextField,左邊的角色頭像和角色名是一個(gè)自定義View,被當(dāng)做textField的leftView來展示。如果刪除的話就是將leftView置nil即可。
問題在于,如果使用UIControlEventEditingChanged的事件,只能獲取到文本發(fā)生變化時(shí)的內(nèi)容,如果輸入框的文字已經(jīng)被刪完,而角色是一個(gè)leftView,但由于文本已經(jīng)為空,則無法再獲取到刪除事件,也就不能把角色刪除掉。
對于這個(gè)問題,我們找到了下面的協(xié)議來實(shí)現(xiàn)。UITextField遵守UITextInput協(xié)議,但UITextInput協(xié)議繼承自UIKeyInput協(xié)議,所以也就擁有下面兩個(gè)方法。下面兩個(gè)方法分別在插入文字,以及點(diǎn)擊刪除按鈕時(shí)調(diào)用,即使文本已經(jīng)為空,依然可以收到deleteBackward的回調(diào)。在這個(gè)回調(diào)里就可以判斷文本是否為空,如果為空則刪除角色即可。
@protocol UIKeyInput <UITextInputTraits>
@property(nonatomic, readonly) BOOL hasText;
- (void)insertText:(NSString *)text;
- (void)deleteBackward;
@end
彈幕設(shè)置
參數(shù)調(diào)整

彈幕一般都不是一種形態(tài),很多參數(shù)都是可以調(diào)整的,對于iPhone和iPad兩個(gè)平臺參數(shù)還不一樣,調(diào)整范圍也不一樣。這些參數(shù)肯定是不能放在業(yè)務(wù)代碼里進(jìn)行判斷的,這樣各種判斷條件散落在項(xiàng)目中,會(huì)導(dǎo)致代碼耦合嚴(yán)重。
對于這個(gè)問題,我們的實(shí)現(xiàn)方式是通過BarrageConfig來區(qū)分不同平臺,將兩個(gè)平臺的數(shù)值差異都放在這個(gè)類中。業(yè)務(wù)部分直接讀取屬性即可,不需要做任何判斷,包括退出進(jìn)程的持久化也在內(nèi)部完成,這樣就可以讓業(yè)務(wù)部分使用無感知,也保證了各個(gè)類中的數(shù)值統(tǒng)一。
當(dāng)有任何參數(shù)的改動(dòng),都可以對BarrageConfig進(jìn)行修改,然后調(diào)用Render的layoutBarrageSubviews進(jìn)行渲染即可。因?yàn)檎{(diào)整參數(shù)之后,屏幕上已經(jīng)顯示的彈幕也需要跟著變,而且變得過程中還是在動(dòng)畫執(zhí)行過程中,動(dòng)畫執(zhí)行不能斷掉,所以對動(dòng)畫的處理就很重要。這部分處理起來比較復(fù)雜,就不詳細(xì)講了。
點(diǎn)贊

彈幕還會(huì)有點(diǎn)贊和長按的功能,點(diǎn)贊一般是點(diǎn)擊屏幕然后出現(xiàn)一個(gè)選擇視圖,點(diǎn)擊點(diǎn)贊后有一個(gè)動(dòng)畫效果。長按就是選中一個(gè)彈幕,識別到手勢長按之后,右側(cè)出現(xiàn)一個(gè)舉報(bào)頁面。
這兩個(gè)手勢我用tap和longPress兩個(gè)手勢來處理,并給longPress設(shè)置了一個(gè)0.2s的識別時(shí)間,將這兩種手勢的識別交給系統(tǒng)去做,這樣也比較省事。
這兩個(gè)手勢都加到Render上,而不是每個(gè)彈幕視圖對應(yīng)一個(gè)手勢,這樣管理起來也比較簡單。這樣在手勢識別時(shí),就需要先找到手勢觸摸點(diǎn),再根據(jù)觸摸點(diǎn)查找對應(yīng)的彈幕視圖,查找的時(shí)候依然通過presentationLayer來查找區(qū)域,而不能用視圖做查找。
- (void)singleTapHandle:(UITapGestureRecognizer *)tapGestureRecognizer {
CGPoint touchLocation = [tapGestureRecognizer locationInView:self.barrageRender];
__block BOOL barrageLiked = NO;
weakifyself;
[self enumerateObjectsUsingBlock:^(SVBarrageItemLabelView *itemLabel, NSUInteger index, BOOL *stop) {
strongifyself;
if ([itemLabel.layer.presentationLayer hitTest:touchLocation] && barrageLiked == NO) {
barrageLiked = YES;
[self likeAction:itemLabel withTouchLocation:touchLocation];
*stop = YES;
}
}];
}
彈幕廣告
廣告
對于這么好的一個(gè)展示位置,廣告部門必然不會(huì)放過。在視頻播放過程中,會(huì)根據(jù)金主爸爸投放要求,在指定的時(shí)間展示一個(gè)廣告彈幕,并且這個(gè)彈幕的形態(tài)還是不固定的。也就是說大小、動(dòng)畫形式都不能確定,而且這條彈幕還要在最上層展示。
對于這個(gè)問題,我們采用的方案是,給廣告專門留了一個(gè)視圖,視圖層級高于Render,在初始化廣告SDK的時(shí)候傳給SDK,這樣就把廣告彈幕的控制交給SDK,我們不做處理。
圖層管理
播放器上存在很多圖層,播控、彈幕Render、廣告之類的,看得到的和看不到的有很多。對于這個(gè)問題,播放器創(chuàng)建了一個(gè)繼承自NSObject的視圖管理器,這個(gè)視圖管理器可以對視圖進(jìn)行分層管理。
播放器上的視圖,都需要調(diào)用指定的方法,將自己加到對應(yīng)的圖層上,移除也需要調(diào)用對應(yīng)的方法。當(dāng)需要調(diào)整前后順序時(shí),修改定義的枚舉即可。
數(shù)據(jù)分離
前面一直說的都是視圖的部分,沒有涉及數(shù)據(jù)的部分,這是因?yàn)?code>UI和數(shù)據(jù)其實(shí)是解耦和的,二者并沒有強(qiáng)耦合,所以可以單獨(dú)拿出來講。數(shù)據(jù)部分的設(shè)計(jì),類似于播放器的local server方案,將請求數(shù)據(jù)到本地,和從本地讀取數(shù)據(jù)做了一個(gè)拆分。
請求數(shù)據(jù)
彈幕數(shù)據(jù)量比較大,肯定是不能一次都請求下來的,這樣很容易造成請求失敗的情況。所以這塊采取的是五分鐘一個(gè)分片數(shù)據(jù),在當(dāng)前的五分鐘彈幕快播完的前十秒,開始請求下一個(gè)時(shí)間段的彈幕。如果拖動(dòng)進(jìn)度條,則拖動(dòng)完成后開始請求新位置的彈幕。在每次請求前都會(huì)查一下庫,數(shù)據(jù)是否已存在。
請求數(shù)據(jù)由業(yè)務(wù)部分驅(qū)動(dòng),請求數(shù)據(jù)后并不會(huì)直接拿來使用,而是存入本地?cái)?shù)據(jù)庫,這部分比較像服務(wù)器往本地寫ts分片的操作。數(shù)據(jù)庫存儲(chǔ)的部分,推薦使用WCDB,彈幕這塊主要都是批量數(shù)據(jù)處理,而WCDB對于批量數(shù)據(jù)的處理,性能高于FMDB。
取數(shù)據(jù)
取數(shù)據(jù)同樣由業(yè)務(wù)層驅(qū)動(dòng),為了減少頻繁進(jìn)行數(shù)據(jù)庫讀寫,每隔十秒鐘進(jìn)行一次數(shù)據(jù)庫批量讀取,并轉(zhuǎn)換為model返回給上層。彈幕模塊在內(nèi)存中維護(hù)了一個(gè)字典,字典以時(shí)間為key,數(shù)組為value,因?yàn)橥粫r(shí)間可能會(huì)有多條彈幕。
從數(shù)據(jù)庫批量獲取的數(shù)據(jù)會(huì)被保存到字典中,上層業(yè)務(wù)層在使用數(shù)據(jù)時(shí),都是通過字典來獲取數(shù)據(jù),這樣也實(shí)現(xiàn)了數(shù)據(jù)層和業(yè)務(wù)層的一個(gè)解耦和。上層業(yè)務(wù)層每隔一秒從字典中讀取一次數(shù)據(jù),并通過數(shù)據(jù)找到合適的軌道,將數(shù)據(jù)傳給合適的軌道來處理。
彈幕防擋探索

現(xiàn)在很多視頻網(wǎng)站都上線了彈幕防遮擋方案,對于視頻中的人物,彈幕會(huì)在其下方展示,而不會(huì)遮擋住人物。還有的應(yīng)用針對彈幕遮擋進(jìn)行了新的探索,即成為付費(fèi)會(huì)員后,可以選擇只有自己喜歡的愛豆不被遮擋,其他人依然被遮擋。
語義分割
根據(jù)業(yè)務(wù)場景我們分析,首先需要把人像部分分割出來,獲取到人像的位置之后才能做后續(xù)的操作。所以人像分割的部分采取語義分割的方式實(shí)現(xiàn),提前對視頻關(guān)鍵幀進(jìn)行標(biāo)注,這個(gè)工作量是很龐大的,所以需要一個(gè)專門的標(biāo)注團(tuán)隊(duì)去完成。根據(jù)標(biāo)注后的模型,通過機(jī)器學(xué)習(xí)的方式,讓計(jì)算機(jī)可以準(zhǔn)確的識別出人的位置,并導(dǎo)出多邊形路徑。
這里面還涉及一個(gè)問題,就是近景識別和遠(yuǎn)景識別的問題,機(jī)器進(jìn)行識別時(shí)只需要識別近景人物,遠(yuǎn)景人物并不需要進(jìn)行識別,否則彈幕展示效果會(huì)受到很大影響。語義分割可以通過Google的Mask_RCNN來實(shí)現(xiàn)。
客戶端實(shí)現(xiàn)方案
客戶端的實(shí)現(xiàn)方案是通過人像的多邊形路徑,對原視頻摳出人像并導(dǎo)出一個(gè)新的視頻。在播放的時(shí)候?qū)嶋H上是前后兩個(gè)播放器在播放,彈幕夾在兩個(gè)播放器中間來實(shí)現(xiàn)的。并且前面的人像層需要做邊緣虛化,讓彈幕的過渡顯得自然些,否則會(huì)太突兀。
這種方案的過渡效果會(huì)好一些。因?yàn)閷γ恳粠曨l進(jìn)行切割的時(shí)候,每一幀并不能保證相鄰幀切割的邊緣相差都不大,也就是相鄰近的幀邊緣不能保證很好的銜接,這樣就容易出現(xiàn)視頻連續(xù)性的問題。前后兩個(gè)播放器疊加的方案,兩個(gè)層的視頻內(nèi)容實(shí)際上是銜接很緊密的,把彈幕層去掉你根本看不出來這是兩層播放器,所以連續(xù)性的問題就不明顯了。
前端實(shí)現(xiàn)方案
前端的實(shí)現(xiàn)方案是服務(wù)端將多邊形路徑放在一個(gè)svg文件中,并將文件下發(fā)給前端,前端通過css的mask?image遮罩實(shí)現(xiàn)的。通過遮罩把人像部分摳出來,人像之外依然是黑色區(qū)域,黑色是可顯示區(qū)域,和iOS的mask屬性類似。
B站是最開始做彈幕防擋的,現(xiàn)在B站已經(jīng)不局限于真人彈幕防擋了,現(xiàn)在很多番劇中的動(dòng)漫人物也支持彈幕防擋。簡書不允許發(fā)外鏈,所以我把外鏈刪了,大家可以去B站上看一下最新的防擋技術(shù)。