來(lái)源丨搜狐技術(shù)產(chǎn)品丨
介紹
彈幕誕生于日本的視頻平臺(tái)狗准,后來(lái)被B站這種短視頻平臺(tái)引入到國(guó)內(nèi)傻丝,并在國(guó)內(nèi)發(fā)展壯大。后來(lái)逐漸被長(zhǎng)視頻平臺(tái)所接受俄占,現(xiàn)在視頻相關(guān)的應(yīng)用基本上都會(huì)有彈幕管怠。
但是長(zhǎng)視頻彈幕和B站這類的短視頻彈幕還不太一樣,短視頻平臺(tái)有自己特有的彈幕文化颠放,所以彈幕更注重和用戶的互動(dòng)排惨。長(zhǎng)視頻平臺(tái)還是以看劇為主吭敢,彈幕類似于評(píng)論的功能碰凶,所以不能影響用戶看劇,彈幕不能太密集鹿驼,而且相互之間最好不要有遮蓋欲低,否則會(huì)對(duì)視頻內(nèi)容會(huì)有比較明顯的影響。
本篇文章主要從長(zhǎng)視頻平臺(tái)的角度來(lái)講彈幕的實(shí)現(xiàn)原理畜晰,但其實(shí)短視頻平臺(tái)的彈幕也是同樣的原理砾莱,區(qū)別在于短視頻可能彈幕種類會(huì)多一些。
技術(shù)實(shí)現(xiàn)
畫布
以我公司應(yīng)用為例凄鼻,有iPhone
和iPad
兩個(gè)平臺(tái)腊瑟,在iPhone
平臺(tái)上有橫豎屏的概念,都需要展示彈幕块蚌。在iPad
上有大小屏的概念闰非,也需要都展示彈幕。彈幕的技術(shù)方案肯定是兩個(gè)平臺(tái)用一套峭范,但需要考慮跨不同設(shè)備和屏幕的情況财松。
所以,對(duì)于這個(gè)問(wèn)題,我通過(guò)畫布的概念來(lái)解決通用性的問(wèn)題辆毡。畫布并不區(qū)分屏幕大小和比例的概念菜秦,只是單純的用來(lái)展示彈幕,并不處理其他業(yè)務(wù)邏輯舶掖,通過(guò)一個(gè)Render
類來(lái)控制畫布的渲染球昨。對(duì)于不同設(shè)備上的差異,例如iPad
字體大一些眨攘,iPhone
字體小一些這種情況褪尝,通過(guò)config
類來(lái)進(jìn)行控制,畫布內(nèi)部不做判斷期犬。
小屏上畫布會(huì)根據(jù)比例少展示一些河哑,大屏上則多展示一些。字體變大畫布也會(huì)根據(jù)比例和左右間距進(jìn)行控制龟虎,保證展示比例是對(duì)的璃谨,并且在屏幕寬高發(fā)生改變后,自動(dòng)適應(yīng)新的尺寸鲤妥,不會(huì)出現(xiàn)彈幕銜接斷開的問(wèn)題佳吞,例如iPad
上大小屏切換。外部在使用時(shí)棉安,只需要傳入一個(gè)frame
即可底扳,不需要關(guān)注畫布內(nèi)部的調(diào)整。
彈幕軌道
從屏幕上來(lái)看贡耽,可以看到彈幕一般都是一行一行的衷模。為了方便對(duì)彈幕視圖進(jìn)行管理,以及后續(xù)的擴(kuò)展工作蒲赂,我對(duì)彈幕設(shè)計(jì)了“軌道”的概念阱冶。每一行都是一個(gè)軌道,對(duì)彈幕進(jìn)行橫向的管理滥嘴,這一行包括速度木蹬、末端彈幕、高度等參數(shù)若皱,這些參數(shù)適用于這一行的所有彈幕镊叁。軌道是一個(gè)虛擬的概念,并沒有對(duì)應(yīng)的視圖走触。
軌道有對(duì)應(yīng)的類來(lái)實(shí)現(xiàn)晦譬,類中會(huì)包含一個(gè)數(shù)組,數(shù)組中有這一行所有的彈幕饺汹。這個(gè)思路有點(diǎn)像玩過(guò)的一款游戲-節(jié)奏大師蛔添,里面也有音樂(lè)軌道的概念,每個(gè)軌道上對(duì)應(yīng)不同速度和顏色的音符,音符數(shù)量也是不固定的迎瞧,根據(jù)節(jié)奏來(lái)決定夸溶。
軌道還有一個(gè)好處在于,對(duì)于不同速度的彈幕比較好控制凶硅。例如騰訊視頻的彈幕其實(shí)是不同速度的缝裁,但是你仔細(xì)觀察的話,可以發(fā)現(xiàn)他們的彈幕是“奇偶行不同速”足绅,也就是奇數(shù)行一個(gè)速度捷绑,偶數(shù)行一個(gè)速度,讓人從感官上來(lái)覺得所有彈幕的速度都不一樣氢妈。如果通過(guò)軌道的方式就很好實(shí)現(xiàn)粹污,不同的軌道根據(jù)當(dāng)前所在行數(shù),對(duì)發(fā)出的彈幕設(shè)置不同的速度即可首量。
有時(shí)候看視頻過(guò)程中會(huì)從右側(cè)出現(xiàn)一條活動(dòng)彈幕壮吩,可能是視頻中的梗,也可能是類似于廣告的互動(dòng)加缘。但是活動(dòng)彈幕出現(xiàn)時(shí)一般是單行清屏的鸭叙,也就是和普通彈幕是互斥的,展示活動(dòng)彈幕的時(shí)候前后沒有普通彈幕拣宏。這種通過(guò)軌道的方式也比較好實(shí)現(xiàn)沈贝,每條彈幕都對(duì)應(yīng)一個(gè)時(shí)間段,根據(jù)活動(dòng)彈幕的時(shí)間和速度勋乾,將活動(dòng)彈幕展示的前后時(shí)間宋下,將這段時(shí)間軌道暫時(shí)關(guān)閉,只保留活動(dòng)彈幕即可市俊。
輪詢
每條彈幕都對(duì)應(yīng)著一個(gè)展示時(shí)間杨凑,所以需要每隔一段時(shí)間就找一下有沒有需要展示的彈幕滤奈。我設(shè)計(jì)的方案是通過(guò)輪詢摆昧,來(lái)驅(qū)動(dòng)彈幕展示。
通過(guò)CADisplayLink
來(lái)進(jìn)行輪訓(xùn)蜒程,將frameInterval
設(shè)置為60绅你,即每秒輪詢一次。在輪詢的回調(diào)中查找有沒有要展示的彈幕昭躺,有的話就從上到下查找每條軌道忌锯,某條軌道有位置可以展示的話就交給這條軌道展示,如果所有軌道都有正在展示的彈幕领炫,則將此條彈幕丟棄偶垮。是否有位置是根據(jù)屏幕最右側(cè),最后一條彈幕是否已經(jīng)展示完全,并且后面有空余位置來(lái)決定的似舵。
對(duì)于取數(shù)據(jù)的部分脚猾,數(shù)據(jù)和視圖的邏輯是分離的,相互之間并沒有耦合關(guān)系砚哗。取數(shù)據(jù)時(shí)只是從一個(gè)很小的字典中龙助,根據(jù)時(shí)間取出所用的彈幕數(shù)并轉(zhuǎn)化為model
。字典的數(shù)據(jù)很少蛛芥,最多十秒的數(shù)據(jù)提鸟,而且這里并不會(huì)接觸到讀數(shù)據(jù)庫(kù)的操作,也沒有網(wǎng)絡(luò)請(qǐng)求的邏輯仅淑,這些都是獨(dú)立的邏輯称勋,后面會(huì)講到。
彈幕視圖
經(jīng)逞木梗看視頻的同學(xué)應(yīng)該會(huì)知道铣缠,彈幕的展示形式有很多,有帶明星頭像的昆禽、有帶點(diǎn)贊數(shù)的蝗蛙、帶矩形背景色的,很多種展示形態(tài)醉鳖。為了更好的對(duì)視圖進(jìn)行組織捡硅,所以我采用的就是很普通的UIView
的展示形式,并沒有為了性能去做很復(fù)雜的渲染操作盗棵。
用UIView
的好處主要就是方便做布局和子視圖管理壮韭,但在屏幕上做動(dòng)畫時(shí),是對(duì)CALayer
進(jìn)行渲染的纹因。也就是說(shuō)UIView
就是用來(lái)做視圖組織喷屋,并不會(huì)直接參與渲染,這也符合蘋果的設(shè)計(jì)理念瞭恰。
復(fù)用池
彈幕是一個(gè)高頻使用的控件屯曹,所以不能一直頻繁創(chuàng)建,以及添加和移除視圖惊畏,會(huì)對(duì)性能有影響恶耽。所以就像很多同學(xué)設(shè)計(jì)的模塊一樣,我也引入了緩存池的概念颜启,我這里叫復(fù)用池偷俭。
彈幕復(fù)用池和UITableView
的復(fù)用池類似,離開屏幕的彈幕會(huì)被放在復(fù)用池中等待復(fù)用缰盏,下次直接從復(fù)用池中取而不重新創(chuàng)建涌萤。彈幕視圖做的工作就是接收新的model
對(duì)象淹遵,并根據(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)是通過(guò)一個(gè)更高精度的CADisplayLink
進(jìn)行輪詢檢測(cè)镜廉,也就是把frameInterval
設(shè)置的更小弄诲,我這里設(shè)置的是10,也就是每秒檢測(cè)六次娇唯。但是進(jìn)行檢測(cè)時(shí)不能直接用CALayer
進(jìn)行判斷齐遵,需要使用presentationLayer
也就是屏幕上正在展示的layer
進(jìn)行檢測(cè),通過(guò)這個(gè)layer
獲取到的frame
和屏幕上顯示的才是一致的塔插。
這里簡(jiǎn)單介紹一下CALayer
的結(jié)構(gòu)梗摇,我們都知道UIView
是對(duì)CALayer
的一層封裝,實(shí)際上屏幕上的顯示都是通過(guò)layer
來(lái)實(shí)現(xiàn)的想许,而layer
本身也分為以下三層伶授,并有不同的功能。
- presentationLayer伸刃,其本身是當(dāng)前幀的一個(gè)拷貝谎砾,每次獲取都是一個(gè)新的對(duì)象,和動(dòng)畫過(guò)程中屏幕上顯示的位置是一樣的捧颅。
- modelLayer,表示
layer
動(dòng)畫完成后的真實(shí)值较雕,如果打印一下modelLayer
和layer
的話碉哑,發(fā)現(xiàn)二者其實(shí)是一個(gè)對(duì)象挚币。 - renderLayer,渲染幀扣典,應(yīng)用程序會(huì)根據(jù)視圖層級(jí)妆毕,構(gòu)成由
layer
組成的渲染樹,renderLayer
就代表layer
在渲染樹中的對(duì)象贮尖。
炫彩彈幕
在播放彈幕的過(guò)程中笛粘,我們可以看到有漸變顏色的彈幕,我們叫做“炫彩彈幕”湿硝。這種彈幕有一個(gè)很明顯的特征薪前,就是其顏色是漸變的。這時(shí)候要考慮性能的問(wèn)題关斜,因?yàn)椴シ鸥咔逡曨l時(shí)本身性能消耗就很大示括,在彈幕量比較大的情況下,會(huì)造成更多的性能消耗痢畜,所以減少性能消耗就是很重要的垛膝,漸變彈幕可能會(huì)使性能消耗加劇。
對(duì)于漸變文字丁稀,一般都是通過(guò)mask
的方式實(shí)現(xiàn)吼拥,下面放一個(gè)CAGradientLayer
做漸變,上面蓋一個(gè)文字的layer
线衫。但是這種會(huì)觸發(fā)離屏渲染扔罪,會(huì)導(dǎo)致性能下降,并不能用這種方案桶雀。經(jīng)過(guò)我們的嘗試矿酵,決定用設(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è)上下文矗积,用來(lái)進(jìn)行圖片繪制全肮,隨后對(duì)上下文進(jìn)行一個(gè)漸變的繪制,最后獲取到一個(gè)UIImage
棘捣,并將圖片賦值給UILabel
的textColor
即可辜腺。
從離屏檢測(cè)來(lái)看,并未發(fā)生離屏渲染乍恐,fps也始終保持在一個(gè)很高的水平评疗。
暫停和開始
彈幕是隨視頻播放和暫停的,所以需要對(duì)彈幕提供暫停和繼續(xù)的支持茵烈,對(duì)于這塊我采用的CAMediaTiming
協(xié)議來(lái)處理百匆,可以通過(guò)此協(xié)議對(duì)動(dòng)畫的過(guò)程進(jìn)行控制。
代碼中加0.05是為了避免彈幕在暫停時(shí)導(dǎo)致的回跳呜投,所以加上一個(gè)時(shí)間差加匈。具體原因是因?yàn)橥ㄟ^(guò)convertTime:fromLayer:
方法計(jì)算得到的時(shí)間存璃,和屏幕上彈幕的位置依然存在一個(gè)微弱的時(shí)間差,而導(dǎo)致渲染時(shí)視圖位置發(fā)生回跳雕拼,這個(gè)0.05是一個(gè)實(shí)踐得來(lái)的經(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é)議是用來(lái)對(duì)動(dòng)畫過(guò)程控制的一個(gè)協(xié)議偎球,例如通過(guò)CoreAnimation
創(chuàng)建的動(dòng)畫,CALayer
遵守了這個(gè)協(xié)議辑甜。這樣如果需要對(duì)動(dòng)畫進(jìn)行控制的話衰絮,不需要引用一個(gè)CABasicAnimation
對(duì)象,然后再修改動(dòng)畫屬性這種方式對(duì)動(dòng)畫流程進(jìn)行控制栈戳,只需要直接對(duì)layer的屬性進(jìn)行修改即可岂傲。
下面是CAMediaTiming
協(xié)議中一些關(guān)鍵的屬性,在上文中也用到了其中的部分屬性子檀。
- beginTime镊掖,動(dòng)畫開始時(shí)間,可以控制動(dòng)畫延遲展示褂痰。一般是一個(gè)絕對(duì)時(shí)間亩进,為了保證準(zhǔn)確性,最好先對(duì)當(dāng)前
layer
進(jìn)行一個(gè)轉(zhuǎn)換缩歪,延遲展示在后面加對(duì)應(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)程奥务,主要用來(lái)結(jié)合
speed
來(lái)對(duì)動(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í)行完成后,是否按照原先的過(guò)程反向執(zhí)行一次瞻坝。此屬性會(huì)對(duì)
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é)合劇中主角煌抒,以及各種文字顏色讓你去選擇,通過(guò)這些功能也可以帶來(lái)一部分付費(fèi)用戶厕倍。當(dāng)發(fā)送一條彈幕時(shí)寡壮,會(huì)從上到下查找軌道凹联,查找軌道時(shí)是通過(guò)presentationLayer
來(lái)進(jìn)行frame
的判斷采桃,如果layer
的最右邊不在屏幕外,并且距離右側(cè)屏幕還有一定空隙属瓣,項(xiàng)目中寫的是10pt组民,則表示有空位可以插入下一條彈幕棒仍,這條彈幕會(huì)被放在這條軌道上。
如果當(dāng)前軌道沒有空位置邪乍,則從上到下逐條查找軌道降狠,直到找到有空位的軌道。如果當(dāng)前屏幕上彈幕較多庇楞,所有軌道都沒有空位榜配,則這一條彈幕會(huì)被拋棄。
如果是自己發(fā)的彈幕吕晌,這個(gè)是必須要展示出來(lái)的蛋褥,因?yàn)橛脩舭l(fā)的彈幕要在界面上給用戶一個(gè)反饋。對(duì)于自己發(fā)的彈幕睛驳,會(huì)有一個(gè)插隊(duì)操作烙心,優(yōu)先級(jí)比其他彈幕都要高膜廊。自己發(fā)的彈幕并不入本地?cái)?shù)據(jù)庫(kù),只是進(jìn)行一個(gè)網(wǎng)絡(luò)請(qǐng)求傳給服務(wù)器淫茵,以及在界面上進(jìn)行展示爪瓜。
選擇角色
在上面的圖片中可以看到,文本之前會(huì)有角色和角色名匙瘪,這些都是獨(dú)立于輸入文字之外的铆铆。用戶如果刪除完輸入的文字之后,再點(diǎn)擊刪除要把角色也一起刪除掉丹喻。輸入框頁(yè)面構(gòu)成是一個(gè)UITextField
薄货,左邊的角色頭像和角色名是一個(gè)自定義View
,被當(dāng)做textField
的leftView
來(lái)展示碍论。如果刪除的話就是將leftView
置nil
即可谅猾。
問(wèn)題在于,如果使用UIControlEventEditingChanged
的事件鳍悠,只能獲取到文本發(fā)生變化時(shí)的內(nèi)容税娜,如果輸入框的文字已經(jīng)被刪完,而角色是一個(gè)leftView
贼涩,但由于文本已經(jīng)為空巧涧,則無(wú)法再獲取到刪除事件,也就不能把角色刪除掉遥倦。
對(duì)于這個(gè)問(wèn)題谤绳,我們找到了下面的協(xié)議來(lái)實(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)整的断凶,對(duì)于iPhone
和iPad
兩個(gè)平臺(tái)參數(shù)還不一樣,調(diào)整范圍也不一樣巫俺。這些參數(shù)肯定是不能放在業(yè)務(wù)代碼里進(jìn)行判斷的认烁,這樣各種判斷條件散落在項(xiàng)目中,會(huì)導(dǎo)致代碼耦合嚴(yán)重。
對(duì)于這個(gè)問(wèn)題却嗡,我們的實(shí)現(xiàn)方式是通過(guò)BarrageConfig
來(lái)區(qū)分不同平臺(tái)舶沛,將兩個(gè)平臺(tái)的數(shù)值差異都放在這個(gè)類中。業(yè)務(wù)部分直接讀取屬性即可窗价,不需要做任何判斷如庭,包括退出進(jìn)程的持久化也在內(nèi)部完成,這樣就可以讓業(yè)務(wù)部分使用無(wú)感知舌镶,也保證了各個(gè)類中的數(shù)值統(tǒng)一柱彻。
當(dāng)有任何參數(shù)的改動(dòng)豪娜,都可以對(duì)BarrageConfig
進(jìn)行修改餐胀,然后調(diào)用Render
的layoutBarrageSubviews
進(jìn)行渲染即可。因?yàn)檎{(diào)整參數(shù)之后瘤载,屏幕上已經(jīng)顯示的彈幕也需要跟著變否灾,而且變得過(guò)程中還是在動(dòng)畫執(zhí)行過(guò)程中,動(dòng)畫執(zhí)行不能斷掉鸣奔,所以對(duì)動(dòng)畫的處理就很重要墨技。這部分處理起來(lái)比較復(fù)雜,就不詳細(xì)講了挎狸。
點(diǎn)贊
彈幕還會(huì)有點(diǎn)贊和長(zhǎng)按的功能扣汪,點(diǎn)贊一般是點(diǎn)擊屏幕然后出現(xiàn)一個(gè)選擇視圖,點(diǎn)擊點(diǎn)贊后有一個(gè)動(dòng)畫效果锨匆。長(zhǎng)按就是選中一個(gè)彈幕崭别,識(shí)別到手勢(shì)長(zhǎng)按之后,右側(cè)出現(xiàn)一個(gè)舉報(bào)頁(yè)面恐锣。
這兩個(gè)手勢(shì)我用tap
和longPress
兩個(gè)手勢(shì)來(lái)處理茅主,并給longPress
設(shè)置了一個(gè)0.2s的識(shí)別時(shí)間,將這兩種手勢(shì)的識(shí)別交給系統(tǒng)去做土榴,這樣也比較省事诀姚。
這兩個(gè)手勢(shì)都加到Render
上,而不是每個(gè)彈幕視圖對(duì)應(yīng)一個(gè)手勢(shì)玷禽,這樣管理起來(lái)也比較簡(jiǎn)單赫段。這樣在手勢(shì)識(shí)別時(shí),就需要先找到手勢(shì)觸摸點(diǎn)矢赁,再根據(jù)觸摸點(diǎn)查找對(duì)應(yīng)的彈幕視圖糯笙,查找的時(shí)候依然通過(guò)presentationLayer
來(lái)查找區(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;
}
}];
}
彈幕廣告
廣告
對(duì)于這么好的一個(gè)展示位置炬丸,廣告部門必然不會(huì)放過(guò)。在視頻播放過(guò)程中,會(huì)根據(jù)金主爸爸投放要求稠炬,在指定的時(shí)間展示一個(gè)廣告彈幕焕阿,并且這個(gè)彈幕的形態(tài)還是不固定的。也就是說(shuō)大小首启、動(dòng)畫形式都不能確定暮屡,而且這條彈幕還要在最上層展示。
對(duì)于這個(gè)問(wèn)題毅桃,我們采用的方案是褒纲,給廣告專門留了一個(gè)視圖,視圖層級(jí)高于Render
钥飞,在初始化廣告SDK的時(shí)候傳給SDK莺掠,這樣就把廣告彈幕的控制交給SDK,我們不做處理读宙。
圖層管理
播放器上存在很多圖層彻秆,播控、彈幕Render
结闸、廣告之類的唇兑,看得到的和看不到的有很多。對(duì)于這個(gè)問(wèn)題桦锄,播放器創(chuàng)建了一個(gè)繼承自NSObject
的視圖管理器扎附,這個(gè)視圖管理器可以對(duì)視圖進(jìn)行分層管理。
播放器上的視圖结耀,都需要調(diào)用指定的方法留夜,將自己加到對(duì)應(yīng)的圖層上,移除也需要調(diào)用對(duì)應(yīng)的方法饼记。當(dāng)需要調(diào)整前后順序時(shí)香伴,修改定義的枚舉即可。
數(shù)據(jù)分離
前面一直說(shuō)的都是視圖的部分具则,沒有涉及數(shù)據(jù)的部分即纲,這是因?yàn)閁I和數(shù)據(jù)其實(shí)是解耦和的,二者并沒有強(qiáng)耦合博肋,所以可以單獨(dú)拿出來(lái)講。數(shù)據(jù)部分的設(shè)計(jì)匪凡,類似于播放器的local server
方案,將請(qǐng)求數(shù)據(jù)到本地唇跨,和從本地讀取數(shù)據(jù)做了一個(gè)拆分稠通。
請(qǐng)求數(shù)據(jù)
彈幕數(shù)據(jù)量比較大改橘,肯定是不能一次都請(qǐng)求下來(lái)的,這樣很容易造成請(qǐng)求失敗的情況飞主。所以這塊采取的是五分鐘一個(gè)分片數(shù)據(jù)高诺,在當(dāng)前的五分鐘彈幕快播完的前十秒,開始請(qǐng)求下一個(gè)時(shí)間段的彈幕虱而。如果拖動(dòng)進(jìn)度條,則拖動(dòng)完成后開始請(qǐng)求新位置的彈幕胖烛。在每次請(qǐng)求前都會(huì)查一下庫(kù),數(shù)據(jù)是否已存在。
請(qǐng)求數(shù)據(jù)由業(yè)務(wù)部分驅(qū)動(dòng)众旗,請(qǐng)求數(shù)據(jù)后并不會(huì)直接拿來(lái)使用,而是存入本地?cái)?shù)據(jù)庫(kù)贡歧,這部分比較像服務(wù)器往本地寫ts
分片的操作滩租。數(shù)據(jù)庫(kù)存儲(chǔ)的部分,推薦使用WCDB
利朵,彈幕這塊主要都是批量數(shù)據(jù)處理律想,而WCDB
對(duì)于批量數(shù)據(jù)的處理,性能高于FMDB
绍弟。
取數(shù)據(jù)
取數(shù)據(jù)同樣由業(yè)務(wù)層驅(qū)動(dòng)技即,為了減少頻繁進(jìn)行數(shù)據(jù)庫(kù)讀寫,每隔十秒鐘進(jìn)行一次數(shù)據(jù)庫(kù)批量讀取樟遣,并轉(zhuǎn)換為model
返回給上層而叼。彈幕模塊在內(nèi)存中維護(hù)了一個(gè)字典,字典以時(shí)間為key
豹悬,數(shù)組為value
葵陵,因?yàn)橥粫r(shí)間可能會(huì)有多條彈幕。
從數(shù)據(jù)庫(kù)批量獲取的數(shù)據(jù)會(huì)被保存到字典中瞻佛,上層業(yè)務(wù)層在使用數(shù)據(jù)時(shí)脱篙,都是通過(guò)字典來(lái)獲取數(shù)據(jù),這樣也實(shí)現(xiàn)了數(shù)據(jù)層和業(yè)務(wù)層的一個(gè)解耦和。上層業(yè)務(wù)層每隔一秒從字典中讀取一次數(shù)據(jù)绊困,并通過(guò)數(shù)據(jù)找到合適的軌道忍弛,將數(shù)據(jù)傳給合適的軌道來(lái)處理。
彈幕防擋探索
現(xiàn)在很多視頻網(wǎng)站都上線了彈幕防遮擋方案考抄,對(duì)于視頻中的人物细疚,彈幕會(huì)在其下方展示,而不會(huì)遮擋住人物川梅。還有的應(yīng)用針對(duì)彈幕遮擋進(jìn)行了新的探索疯兼,即成為付費(fèi)會(huì)員后,可以選擇只有自己喜歡的愛豆不被遮擋贫途,其他人依然被遮擋吧彪。
語(yǔ)義分割
根據(jù)業(yè)務(wù)場(chǎng)景我們分析,首先需要把人像部分分割出來(lái)丢早,獲取到人像的位置之后才能做后續(xù)的操作姨裸。所以人像分割的部分采取語(yǔ)義分割的方式實(shí)現(xiàn)傀缩,提前對(duì)視頻關(guān)鍵幀進(jìn)行標(biāo)注赡艰,這個(gè)工作量是很龐大的慷垮,所以需要一個(gè)專門的標(biāo)注團(tuán)隊(duì)去完成料身。根據(jù)標(biāo)注后的模型芹血,通過(guò)機(jī)器學(xué)習(xí)的方式祟牲,讓計(jì)算機(jī)可以準(zhǔn)確的識(shí)別出人的位置说贝,并導(dǎo)出多邊形路徑乡恕。
這里面還涉及一個(gè)問(wèn)題,就是近景識(shí)別和遠(yuǎn)景識(shí)別的問(wèn)題运杭,機(jī)器進(jìn)行識(shí)別時(shí)只需要識(shí)別近景人物辆憔,遠(yuǎn)景人物并不需要進(jìn)行識(shí)別虱咧,否則彈幕展示效果會(huì)受到很大影響腕巡。語(yǔ)義分割可以通過(guò)Google的Mask_RCNN
來(lái)實(shí)現(xiàn)绘沉。
客戶端實(shí)現(xiàn)方案
客戶端的實(shí)現(xiàn)方案是通過(guò)人像的多邊形路徑车伞,對(duì)原視頻摳出人像并導(dǎo)出一個(gè)新的視頻帖世。在播放的時(shí)候?qū)嶋H上是前后兩個(gè)播放器在播放,彈幕夾在兩個(gè)播放器中間來(lái)實(shí)現(xiàn)的赂弓。并且前面的人像層需要做邊緣虛化翔怎,讓彈幕的過(guò)渡顯得自然些赤套,否則會(huì)太突兀容握。
這種方案的過(guò)渡效果會(huì)好一些剔氏。因?yàn)閷?duì)每一幀視頻進(jìn)行切割的時(shí)候,每一幀并不能保證相鄰幀切割的邊緣相差都不大羊苟,也就是相鄰近的幀邊緣不能保證很好的銜接蜡励,這樣就容易出現(xiàn)視頻連續(xù)性的問(wèn)題阻桅。前后兩個(gè)播放器疊加的方案占遥,兩個(gè)層的視頻內(nèi)容實(shí)際上是銜接很緊密的输瓜,把彈幕層去掉你根本看不出來(lái)這是兩層播放器尤揣,所以連續(xù)性的問(wèn)題就不明顯了北戏。
前端實(shí)現(xiàn)方案
前端的實(shí)現(xiàn)方案是服務(wù)端將多邊形路徑放在一個(gè)svg文件中嗜愈,并將文件下發(fā)給前端蠕嫁,前端通過(guò)css
的mask?image
遮罩實(shí)現(xiàn)的剃毒。通過(guò)遮罩把人像部分摳出來(lái)赘阀,人像之外依然是黑色區(qū)域基公,黑色是可顯示區(qū)域,和iOS的mask
屬性類似欠痴。
B站是最開始做彈幕防擋的喇辽,現(xiàn)在B站已經(jīng)不局限于真人彈幕防擋了菩咨,現(xiàn)在很多番劇中的動(dòng)漫人物也支持彈幕防擋√卣迹可以看下面的視頻感受一下是目。