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>

        淺談前端彈幕的設計

        共 12059字,需瀏覽 25分鐘

         ·

        2021-08-25 01:01

         大廠技術(shù)  高級進階  精選好文

        點擊上方 程序員成長指北,關(guān)注公眾號

        回復1,加入高級Node交流群

        背景

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


        形式

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

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

        為什么需要彈幕

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

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

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

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

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

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

        除此之外的好處

        互動性強:點播時讓你覺得不孤獨

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

        互動性強:直播時的互動及時

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

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

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

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

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

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

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

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

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

        彈幕的設計

        首先是整體設計,主要是三個部分:舞臺、軌道、彈幕池。

        舞臺

        舞臺是整個彈幕的主控制,它維護著多個軌道、一個等待隊列、一個彈幕池。舞臺要做的事情是控制整個彈幕的節(jié)奏,當每一幀進行渲染時,都判斷其中的軌道是否有空位,從等待隊列中取合適的彈幕送往合適的軌道。舞臺的能力可以通過實現(xiàn)舞臺基類以及對應的抽象函數(shù),讓具體類型的舞臺去實現(xiàn)對應的舞臺邏輯。從而實現(xiàn)不同渲染能力(Canvas、HTML+CSS)以及不同類型(滾動、頂部固定、底部固定)的彈幕控制。無法復制加載中的內(nèi)容 不管是通過 Canvas 還是 DOM 實現(xiàn)彈幕,需要的方法都是相似的:添加新彈幕到等待隊列、尋找合適的軌道、從等待隊列中抽取彈幕并放入軌道、整體渲染、清空。因此 BaseStage 可以通過編排抽象方法,讓具體的子類去進行具體實現(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[] = [] 
         
          // 添加彈幕到等待隊列 
          abstract add(barrage: T): boolean 
          // 尋找合適的軌道 
          abstract _findTrack(): number 
          // 從等待隊列中抽取彈幕并放入軌道 
          abstract _extractBarrage(): void 
          // 渲染函數(shù) 
          abstract render(): void 
          // 清空 
          abstract reset(): void 

        Canvas 版本

        比如,Canvas的舞臺基類需要傳入Canvas元素,獲取Context。最后通過實現(xiàn) BaseStage 的抽象方法實現(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 版本

        而對于HTML+CSS的實現(xiàn),就需要維護一個彈幕池domPool、彈幕實例與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() 
            }) 
          } 

        彈幕池

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

        軌道


        從我們平常見到的彈幕中可以看到,其實舞臺中間會存在多條平行的軌道,舞臺和軌道之間的關(guān)系是1對多的關(guān)系。當彈幕運行時,依次渲染軌道中的彈幕。所以,軌道中會存在一個彈幕數(shù)組,代表著目前正在軌道上展示的彈幕;以及一個叫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) 
          } 
         
          // 移除第一個(也就是剛剛出去的一個) 
          removeTop() { 
            this.barrages.shift() 
          } 
         
          remove(index: number) { 
            if (index < 0 || index >= this.barrages.length) { 
              return 
            } 
            this.barrages.splice(index, 1
          } 
         
          // 更新 Offset,只需要關(guān)注軌道中最后一個彈幕 
          updateOffset() { 
            const endBarrage = this.barrages[this.barrages.length - 1
            if (endBarrage) { 
              const { speed } = endBarrage 
              this.offset -= speed 
            } 
          } 

        碰撞

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

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

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

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


        DefaultSpeed 第一個彈幕在軌道上的默認速度,它應該根據(jù)實際需求設置成一個合適的值,然后 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


        Node 社群


        我組建了一個氛圍特別好的 Node.js 社群,里面有很多 Node.js小伙伴,如果你對Node.js學習感興趣的話(后續(xù)有計劃也可以),我們可以一起進行Node.js相關(guān)的交流、學習、共建。下方加 考拉 好友回復「Node」即可。


           “分享、點贊、在看” 支持一波 



        瀏覽 63
        點贊
        評論
        收藏
        分享

        手機掃一掃分享

        分享
        舉報
        評論
        圖片
        表情
        推薦
        點贊
        評論
        收藏
        分享

        手機掃一掃分享

        分享
        舉報
        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>
            欧美一级做a爰片久久高潮 | 污片网站 | 狠狠视频 | 91视频污污污 | 午夜激情视频三区在线 | 亚洲精品88欧美一区二区 | 疯狂做爰高潮videossex | 《色戒》汤唯无删减版视频 | 91久久国产视频 | 欧美BBBXXX |