1. <strong id="7actg"></strong>
    2. <table id="7actg"></table>

    3. <address id="7actg"></address>
      <address id="7actg"></address>
      1. <object id="7actg"><tt id="7actg"></tt></object>

        淺談彈幕的設(shè)計(jì)

        共 11960字,需瀏覽 24分鐘

         ·

        2021-08-18 06:53

         大廠技術(shù)  堅(jiān)持周更  精選好文


        背景

        為了創(chuàng)造更好的多媒體體驗(yàn),許多視頻網(wǎng)站都添加了社交機(jī)制,使用戶可以在媒體時(shí)間軸上的特定點(diǎn)發(fā)布評(píng)論和查看其他人的評(píng)論,其中一種機(jī)制被稱為彈幕(dàn mù),在日語中也稱為コメント(comment)或者弾幕(danmaku),在播放過程中,可能會(huì)出現(xiàn)大量評(píng)論和注釋,并且直接渲染在視頻上。彈幕最初是由日本視頻網(wǎng)站Niconico(ニコニコ)引入的。在中國,除了在Bilibili和AcFun等彈幕視頻網(wǎng)站中使用之外,其他主流視頻網(wǎng)站(例如騰訊視頻,愛奇藝視頻,優(yōu)酷視頻和咪咕視頻)中的視頻播放器也支持彈幕。


        形式

        單條彈幕的基本模式有三種:

        1. 滾動(dòng)彈幕:自右向左滾動(dòng)過屏幕的彈幕,以自上而下的優(yōu)先度展示。
        2. 頂部彈幕:自上而下靜止居中的彈幕、以自上而下的優(yōu)先度展示。
        3. 底部彈幕:自下而上靜止居中的彈幕、以自下而上的優(yōu)先度展示。

        為什么需要彈幕

        從用戶體驗(yàn)角度出發(fā)——沒有彈幕之前

        在沒有彈幕之前,我們一般是通過評(píng)論或者聊天室的方式去進(jìn)行互動(dòng):(如上,左邊視頻,右邊互動(dòng)區(qū))

        傳統(tǒng)互動(dòng)方式帶來的問題是,當(dāng)我們的人眼的關(guān)注點(diǎn)在視頻上時(shí),是沒辦法進(jìn)行“一眼二用”的,簡單的來說就是,你沒辦法讓你的兩顆眼珠子往不同的方向看。這樣帶來的弊端是,當(dāng)用戶專注于視頻時(shí),互動(dòng)區(qū)的交互效果是很差的;而當(dāng)用戶在看互動(dòng)區(qū)的評(píng)論時(shí),又沒辦法去關(guān)注整件事的主體內(nèi)容,顧此失彼。

        (你沒辦法“一眼二用”) 與此同時(shí),對(duì)于世界上大多數(shù)的人來說,自小養(yǎng)成的習(xí)慣就是從左往右的閱讀習(xí)慣。像這種互動(dòng)區(qū)的評(píng)論,通常都是從下往上進(jìn)行自動(dòng)滾動(dòng)的,兩個(gè)方向的合起來的話整個(gè)文字就形成了一個(gè)傾斜的運(yùn)動(dòng)方向,使得用戶的閱讀產(chǎn)生了障礙。(傾斜向上的文字移動(dòng),讓人沒辦法好好看字)

        從用戶體驗(yàn)角度出發(fā)——彈幕出現(xiàn)之后

        彈幕出現(xiàn)后,我們的視角就集中到視頻主體上,當(dāng)彈幕出現(xiàn)時(shí),如果是滾動(dòng)彈幕,那么一般都是從右往左出發(fā),非常適合我們的從左往右的閱讀習(xí)慣,并且,文字的移動(dòng)方向只有一個(gè),不會(huì)給我們的閱讀產(chǎn)生障礙。

        除此之外的好處

        互動(dòng)性強(qiáng):點(diǎn)播時(shí)讓你覺得不孤獨(dú)

        在觀看視頻網(wǎng)站提供視頻時(shí),觀看者在觀看視頻內(nèi)容過程中根據(jù)內(nèi)容啟發(fā)會(huì)有一些想法或者吐槽點(diǎn),就想要發(fā)表出來和更多的人分享,這時(shí)就需要彈幕來滿足這個(gè)需求。通過彈幕,可以把同一時(shí)間觀看者的評(píng)論通過固定方向滾動(dòng)的方式顯示在視頻區(qū)域中,或者靜止的顯示在視頻區(qū)域的頂部或底部,這樣可以增加觀看者和視頻的互動(dòng)特性以及觀看者之間的互動(dòng)。在相同時(shí)刻發(fā)送的彈幕基本上也具有相同的主題。

        互動(dòng)性強(qiáng):直播時(shí)的互動(dòng)及時(shí)

        彈幕在視頻直播場景中也能夠成為主播與觀眾直接互動(dòng)的方式。比起傳統(tǒng)的實(shí)時(shí)評(píng)論,主播能夠根據(jù)屏幕上彈幕的展現(xiàn)更直觀了解觀眾的需求和反饋,更方便地調(diào)整接下來的行動(dòng)和處理,也能夠根據(jù)用戶的輸入進(jìn)行交互操作。

        氣氛渲染好:“前方高能”

        當(dāng)看一些比較恐怖、懸疑的內(nèi)容時(shí),“前方高能”可能會(huì)避免你心里落下童年陰影[手動(dòng)狗頭]。

        彈幕的實(shí)現(xiàn)方式

        現(xiàn)如今,從B站、愛奇藝、騰訊視頻等各大媒體網(wǎng)站上按下 F12 時(shí),很容易發(fā)現(xiàn)是通過 HTML+CSS 的方式實(shí)現(xiàn)的。另外,也有一小部分具備 Canvas 實(shí)現(xiàn)的彈幕,比如之前的B站(不過在截稿前好像找不到切換按鈕了)。

        假如通過 HTML+CSS 實(shí)現(xiàn)

        通過 DOM 元素實(shí)現(xiàn)彈幕,前端同學(xué)可以很方便地通過 CSS 修改彈幕樣式。同時(shí),得益于瀏覽器原生的 DOM 事件機(jī)制,借助這個(gè)可以很快捷實(shí)現(xiàn)一系列彈幕交互功能:個(gè)性化、點(diǎn)贊、舉報(bào)等,以滿足產(chǎn)品的各種互動(dòng)需求。很容易看到,目前像騰訊視頻、愛奇藝等都是通過 DOM 元素實(shí)現(xiàn)彈幕,這是目前主流的實(shí)現(xiàn)方式。

        假如通過 Canvas 實(shí)現(xiàn)

        Canvas 為動(dòng)畫而生,但是基于 Canvas 實(shí)現(xiàn)一個(gè)彈幕系統(tǒng),會(huì)比基于 DOM 實(shí)現(xiàn)要復(fù)雜。暫且不說對(duì)于大部分前端同學(xué)而言,對(duì) Canvas 的熟悉程度遠(yuǎn)比 DOM 要低,更何況,Canvas 并沒有一套原生的事件系統(tǒng),這意味著,如果要實(shí)現(xiàn)一些互動(dòng)功能,你必須要自己實(shí)現(xiàn)一套 Canvas 的事件機(jī)制……

        彈幕的設(shè)計(jì)

        首先是整體設(shè)計(jì),主要是三個(gè)部分:舞臺(tái)、軌道、彈幕池。

        舞臺(tái)

        舞臺(tái)是整個(gè)彈幕的主控制,它維護(hù)著多個(gè)軌道、一個(gè)等待隊(duì)列、一個(gè)彈幕池。舞臺(tái)要做的事情是控制整個(gè)彈幕的節(jié)奏,當(dāng)每一幀進(jìn)行渲染時(shí),都判斷其中的軌道是否有空位,從等待隊(duì)列中取合適的彈幕送往合適的軌道。舞臺(tái)的能力可以通過實(shí)現(xiàn)舞臺(tái)基類以及對(duì)應(yīng)的抽象函數(shù),讓具體類型的舞臺(tái)去實(shí)現(xiàn)對(duì)應(yīng)的舞臺(tái)邏輯。從而實(shí)現(xiàn)不同渲染能力(Canvas、HTML+CSS)以及不同類型(滾動(dòng)、頂部固定、底部固定)的彈幕控制。無法復(fù)制加載中的內(nèi)容 不管是通過 Canvas 還是 DOM 實(shí)現(xiàn)彈幕,需要的方法都是相似的:添加新彈幕到等待隊(duì)列、尋找合適的軌道、從等待隊(duì)列中抽取彈幕并放入軌道、整體渲染、清空。因此 BaseStage 可以通過編排抽象方法,讓具體的子類去進(jìn)行具體實(shí)現(xiàn)。

        export default abstract class BaseStage<T extends BarrageObjectextends EventEmitter 
          protected trackWidth: number 
          protected trackHeight: number 
          protected duration: number 
          protected maxTrack: number 
          protected tracks: Track<T>[] = [] 
          waitingQueue: T[] = [] 
         
          // 添加彈幕到等待隊(duì)列 
          abstract add(barrage: T): boolean 
          // 尋找合適的軌道 
          abstract _findTrack(): number 
          // 從等待隊(duì)列中抽取彈幕并放入軌道 
          abstract _extractBarrage(): void 
          // 渲染函數(shù) 
          abstract render(): void 
          // 清空 
          abstract reset(): void 

        Canvas 版本

        比如,Canvas的舞臺(tái)基類需要傳入Canvas元素,獲取Context。最后通過實(shí)現(xiàn) BaseStage 的抽象方法實(shí)現(xiàn)具體的邏輯。

        export default abstract class BaseCanvasStage<T extends BarrageObjectextends BaseStage
          T 

          protected canvas: HTMLCanvasElement 
          protected ctx: CanvasRenderingContext2D 
         
          constructor(canvas: HTMLCanvasElement, config: Config) { 
            super(config) 
            this.canvas = canvas 
            this.ctx = canvas.getContext('2d')! 
          } 

        HTML + CSS 版本

        而對(duì)于HTML+CSS的實(shí)現(xiàn),就需要維護(hù)一個(gè)彈幕池domPool、彈幕實(shí)例與DOM的映射關(guān)系(objToElm、elmToObj)以及一些必要的事件處理方法(_mouseMoveEventHandler 、_mouseClickEventHandler)。

        export default abstract class BaseCssStage<T extends BarrageObjectextends BaseStage<T
          el: HTMLDivElement 
          objToElmWeakMap<T, HTMLElement> = new WeakMap() 
          elmToObjWeakMap<HTMLElement, T> = new WeakMap() 
          freezeBarrage: T | null = null 
          domPoolArray<HTMLElement> = [] 
         
          constructor(el: HTMLDivElement, config: Config) { 
            super(config) 
         
            this.el = el 
         
            const wrapper = config.wrapper 
            if (wrapper && config.interactive) { 
              wrapper.addEventListener('mousemove'this._mouseMoveEventHandler.bind(this)) 
              wrapper.addEventListener('click'this._mouseClickEventHandler.bind(this)) 
            } 
          } 
         
          createBarrage(text: string, color: string, fontSize: string, left: string) { 
            if (this.domPool.length) { 
              const el = this.domPool.pop() 
              return _createBarrage(text, color, fontSize, left, el) 
            } else { 
              return _createBarrage(text, color, fontSize, left) 
            } 
          } 
         
          removeElement(target: HTMLElement) { 
            if (this.domPool.length < this.poolSize) { 
              this.domPool.push(target) 
              return 
            } 
            this.el.removeChild(target) 
          } 
         
          _mouseMoveEventHandler(e: Event) { 
            const target = e.target 
            if (!target) { 
              return 
            } 
         
            const newFreezeBarrage = this.elmToObj.get(target as HTMLElement) 
            const oldFreezeBarrage = this.freezeBarrage 
         
            if (newFreezeBarrage === oldFreezeBarrage) { 
              return 
            } 
         
            this.freezeBarrage = null 
         
            if (newFreezeBarrage) { 
              this.freezeBarrage = newFreezeBarrage 
              newFreezeBarrage.freeze = true 
              setHoverStyle(target as HTMLElement) 
              this.$emit('hover', newFreezeBarrage, target as HTMLElement) 
            } 
         
            if (oldFreezeBarrage) { 
              oldFreezeBarrage.freeze = false 
              const oldFreezeElm = this.objToElm.get(oldFreezeBarrage) 
              oldFreezeElm && setBlurStyle(oldFreezeElm) 
              this.$emit('blur', oldFreezeBarrage, oldFreezeElm) 
            } 
          } 
         
          _mouseClickEventHandler(e: Event) { 
            const target = e.target 
            const barrageObject = this.elmToObj.get(target as HTMLElement) 
            if (barrageObject) { 
              this.$emit('click', barrageObject, target) 
            } 
          } 
         
          reset() { 
            this.forEach(track => { 
              track.forEach(barrage => { 
                const el = this.objToElm.get(barrage) 
                if (!el) { 
                  return 
                } 
                this.removeElement(el) 
              }) 
              track.reset() 
            }) 
          } 

        彈幕池

        無法復(fù)制加載中的內(nèi)容 通過HTML+CSS實(shí)現(xiàn)的彈幕,每一個(gè)彈幕會(huì)對(duì)應(yīng)一個(gè) DOM 元素,為了減少頻繁的創(chuàng)建,會(huì)在屏幕的左側(cè)把上一輪已經(jīng)滾出舞臺(tái)的彈幕存到池子中,當(dāng)有新彈幕時(shí)會(huì)重新復(fù)用。

        軌道


        從我們平常見到的彈幕中可以看到,其實(shí)舞臺(tái)中間會(huì)存在多條平行的軌道,舞臺(tái)和軌道之間的關(guān)系是1對(duì)多的關(guān)系。當(dāng)彈幕運(yùn)行時(shí),依次渲染軌道中的彈幕。所以,軌道中會(huì)存在一個(gè)彈幕數(shù)組,代表著目前正在軌道上展示的彈幕;以及一個(gè)叫offset的變量,代表著目前軌道已被占據(jù)的寬度。

        class BarrageTrack<T extends BarrageObject
          barrages: T[] = [] 
          offset: number = 0 
         
          forEach(handler: TrackForEachHandler<T>) { 
            for (let i = 0; i < this.barrages.length; ++i) { 
              handler(this.barrages[i], i, this.barrages) 
            } 
          } 
         
          // 重置 
          reset() { 
            this.barrages = [] 
            this.offset = 0 
          } 
         
          // 加入新彈幕 
          push(...items: T[]) { 
            this.barrages.push(...items) 
          } 
         
          // 移除第一個(gè)(也就是剛剛出去的一個(gè)) 
          removeTop() { 
            this.barrages.shift() 
          } 
         
          remove(index: number) { 
            if (index < 0 || index >= this.barrages.length) { 
              return 
            } 
            this.barrages.splice(index, 1
          } 
         
          // 更新 Offset,只需要關(guān)注軌道中最后一個(gè)彈幕 
          updateOffset() { 
            const endBarrage = this.barrages[this.barrages.length - 1
            if (endBarrage) { 
              const { speed } = endBarrage 
              this.offset -= speed 
            } 
          } 

        碰撞

        彈幕的碰撞控制以及彈幕的呈現(xiàn)方式,其實(shí)全憑產(chǎn)品需求和個(gè)人喜好決定。以大多數(shù)彈幕為例,除了 B站的實(shí)現(xiàn)比較多樣化之外,更多的實(shí)現(xiàn)是通過平行軌道的方式實(shí)現(xiàn)。如果需要考慮彈幕的碰撞問題,一般有兩種方法:

        1. 每個(gè)彈幕的速度都是相同的,所以也就不存在碰撞問題,但是效果非常死板。
        2. 每個(gè)彈幕的速度都是不一樣的,但是需要解決碰撞問題。

        為了實(shí)現(xiàn)不同的速度,最簡單有效的方式其實(shí)就是通過『追及問題』求出彈幕的最大速度。通過『追及問題』,很容易求出彈幕B的最大速度 VB 。但是 VB 不應(yīng)該是彈幕的最終速度,考慮到距離 S 可能會(huì)比較大,那么 VB 的速度就會(huì)很大。于此同時(shí),應(yīng)該給彈幕的速度增加一點(diǎn)隨機(jī)性。因此,彈幕的速度比較好的呈現(xiàn)方式是:

        S = Math.max(VB, Random * DefaultSpeed) 


        DefaultSpeed 第一個(gè)彈幕在軌道上的默認(rèn)速度,它應(yīng)該根據(jù)實(shí)際需求設(shè)置成一個(gè)合適的值,然后 VB 的最大值不能超過它,不然的話彈幕只能在軌道上『一閃而過』。

        Demo

        https://logcas.github.io/a-barrage/example/css3.html 

        https://logcas.github.io/a-barrage/example/canvas.html

        參考資料

        https://w3c.github.io/danmaku/usecase.zh.html

        https://juejin.cn/post/6867689680670818317

        ?? 謝謝支持

        以上便是本次分享的全部內(nèi)容,希望對(duì)你有所幫助^_^

        喜歡的話別忘了 分享、點(diǎn)贊、收藏 三連哦~。

        歡迎關(guān)注公眾號(hào) 前端Sharing 收貨大廠一手好文章~



        瀏覽 199
        點(diǎn)贊
        評(píng)論
        收藏
        分享

        手機(jī)掃一掃分享

        分享
        舉報(bào)
        評(píng)論
        圖片
        表情
        推薦
        點(diǎn)贊
        評(píng)論
        收藏
        分享

        手機(jī)掃一掃分享

        分享
        舉報(bào)
        1. <strong id="7actg"></strong>
        2. <table id="7actg"></table>

        3. <address id="7actg"></address>
          <address id="7actg"></address>
          1. <object id="7actg"><tt id="7actg"></tt></object>
            国产精品自拍一 | 免费无码精品久久久嫩青 | 激情床戏电影视频,大尺度 | 国产精品视频一二三区 | 中国操逼免费视频 | 约炮操女人aV | 少妇放荡的呻吟干柴烈火视频 | 手机操逼网站 | 蝌蚪久久 | 一级少妇婬高潮免费全看 |