成人国产在线小视频_日韩寡妇人妻调教在线播放_色成人www永久在线观看_2018国产精品久久_亚洲欧美高清在线30p_亚洲少妇综合一区_黄色在线播放国产_亚洲另类技巧小说校园_国产主播xx日韩_a级毛片在线免费

資訊專欄INFORMATION COLUMN

Canvas + WebSocket + Redis 實(shí)現(xiàn)一個(gè)視頻彈幕

gekylin / 2155人閱讀

摘要:創(chuàng)建彈幕功能的類及基本參數(shù)處理布局時(shí)需要注意的默認(rèn)寬為,高為,我們要保證完全覆蓋整個(gè)視頻,需要讓與寬高相等。因?yàn)槲覀儾淮_定每一個(gè)使用該功能的視頻的寬高都是一樣的,所以畫布的寬高并沒有通過來設(shè)置,而是通過在類創(chuàng)建實(shí)例初始化屬性的時(shí)候動(dòng)態(tài)設(shè)置。


閱讀原文


頁面布局

首先,我們需要實(shí)現(xiàn)頁面布局,在根目錄創(chuàng)建 index.html 布局中我們需要有一個(gè) video 多媒體標(biāo)簽引入我們的本地視頻,添加輸入彈幕的輸入框、確認(rèn)發(fā)送的按鈕、顏色選擇器、字體大小滑動(dòng)條,創(chuàng)建一個(gè) style.css 來調(diào)整頁面布局的樣式,這里我們順便創(chuàng)建一個(gè) index.js 文件用于后續(xù)實(shí)現(xiàn)我們的核心邏輯,先引入到頁面當(dāng)中。

HTML 布局代碼如下:





    
    
    視頻彈幕


    

Canvas + WebSocket + Redis 實(shí)現(xiàn)視頻彈幕

CSS 樣式代碼如下:

/* 文件:style.css */
#cantainer {
    text-align: center;
}
#content {
    width: 640px;
    margin: 0 auto;
    position: relative;
}
#canvas {
    position: absolute;
}
video {
    width: 640px;
    height: 360px;
}
input {
    vertical-align: middle;
}

布局效果如下圖:


定義接口,構(gòu)造假數(shù)據(jù)

我們彈幕中的彈幕數(shù)據(jù)正常情況下應(yīng)該是通過與后臺(tái)數(shù)據(jù)交互請求回來,所以我們需要先定義數(shù)據(jù)接口,并構(gòu)造假數(shù)據(jù)來實(shí)現(xiàn)前端邏輯。

數(shù)據(jù)字段定義:

value:表示彈幕的內(nèi)容(必填)

time:表示彈幕出現(xiàn)的時(shí)間(必填)

speed:表示彈幕移動(dòng)的速度(選填)

color:表示彈幕文字的顏色(選填)

fontSize:表示彈幕的字體大小(選填)

opacity:表示彈幕文字的透明度(選填)

上面的 valuetime 是必填參數(shù),其他的選填參數(shù)可以在前端設(shè)置默認(rèn)值。

前端定義的假數(shù)據(jù)如下:

// 文件:index.js
let data = [
    {
        value: "這是第一條彈幕",
        speed: 2,
        time: 0,
        color: "red",
        fontSize: 20
    },
    {
        value: "這是第二條彈幕",
        time: 1
    }
];


實(shí)現(xiàn)前端彈幕的邏輯

我們希望是把彈幕封裝成一個(gè)功能,只要有需要的地方就可以使用,從而實(shí)現(xiàn)復(fù)用,那么不同的地方使用這個(gè)功能通常的方式是 new 一個(gè)實(shí)例,傳入當(dāng)前使用該功能對應(yīng)的參數(shù),我們也使用這種方式來實(shí)現(xiàn),所以我們需要封裝一個(gè)統(tǒng)一的構(gòu)造函數(shù)或者類,參數(shù)為當(dāng)前的 canvas 元素、video 元素和一個(gè) options 對象,options 里面的 data 屬性為我們的彈幕數(shù)據(jù),之所以不直接傳入 data 是為了后續(xù)參數(shù)的擴(kuò)展,嚴(yán)格遵循開放封閉原則,這里我們就統(tǒng)一使用 ES6 的 class 類來實(shí)現(xiàn)。

1、創(chuàng)建彈幕功能的類及基本參數(shù)處理

布局時(shí)需要注意 Canvas 的默認(rèn)寬為 300px,高為 150px,我們要保證 Canvas 完全覆蓋整個(gè)視頻,需要讓 Canvas 與 video 寬高相等。
因?yàn)槲覀儾淮_定每一個(gè)使用該功能的視頻的寬高都是一樣的,所以 Canvas 畫布的寬高并沒有通過 CSS 來設(shè)置,而是通過 JS 在類創(chuàng)建實(shí)例初始化屬性的時(shí)候動(dòng)態(tài)設(shè)置。

// 文件:index.js
class CanvasBarrage {
    constructor(canvas, video, options = {}) {
        // 如果沒有傳入 canvas 或者 video 直接跳出
        if (!canvas || !video) return;
        this.canvas = canvas; // 當(dāng)前的 canvas 元素
        this.video = video; // 當(dāng)前的 video 元素

        // 設(shè)置 canvas 與 video 等高
        this.canvas.width = video.clientWidth;
        this.canvas.height = video.clientHeight;

        // 默認(rèn)暫停播放,表示不渲染彈幕
        this.isPaused = true;

        // 沒傳參數(shù)的默認(rèn)值
        let defaultOptions = {
            fontSize: 20,
            color: "gold",
            speed: 2,
            opacity: 0.3,
            data: []
        };

        // 對象的合并,將默認(rèn)參數(shù)對象的屬性和傳入對象的屬性統(tǒng)一放到當(dāng)前實(shí)例上
        Object.assign(this, defaultOptions, options);
    }
}

應(yīng)該掛在實(shí)例上的屬性除了有當(dāng)前的 canvas 元素、video 元素、彈幕數(shù)據(jù)的默認(rèn)選項(xiàng)以及彈幕數(shù)據(jù)之外,還應(yīng)該有一個(gè)代表當(dāng)前是否渲染彈幕的參數(shù),因?yàn)橐曨l暫停的時(shí)候,彈幕也是暫停的,所以沒有重新渲染,因?yàn)槭欠駮和Ec彈幕是否渲染的狀態(tài)是一致的,所以我們這里就用 isPaused 參數(shù)來代表當(dāng)前是否暫?;蛑匦落秩緩椖?,值類型為布爾值。

2、創(chuàng)建構(gòu)造每一條彈幕的類

我們知道,后臺(tái)返回給我們的彈幕數(shù)據(jù)是一個(gè)數(shù)組,這個(gè)數(shù)組里的每一個(gè)彈幕都是一個(gè)對象,而對象上有著這條彈幕的信息,如果我們需要在每一個(gè)彈幕對象上再加一些新的信息或者在每一個(gè)彈幕對象的處理時(shí)用到了當(dāng)前彈幕功能類 CanvasBarrage 實(shí)例的一些屬性值,取值顯然是不太方便的,這樣為了后續(xù)方便擴(kuò)展,遵循開放封閉原則,我們把每一個(gè)彈幕的對象轉(zhuǎn)變成同一個(gè)類的實(shí)例,所以我們創(chuàng)建一個(gè)名為 Barrage 的類,讓我們每一條彈幕的對象進(jìn)入這個(gè)類里面走一遭,掛上一些擴(kuò)展的屬性。

// 文件:index.js
class Barrage {
    constructor(item, ctx) {
        this.value = item.value; // 彈幕的內(nèi)容
        this.time = item.time; // 彈幕出現(xiàn)的時(shí)間
        this.item = item; // 每一個(gè)彈幕的數(shù)據(jù)對象
        this.ctx = ctx; // 彈幕功能類的執(zhí)行上下文
    }
}

在我們的 CanvasBarrage 類上有一個(gè)存儲(chǔ)彈幕數(shù)據(jù)的數(shù)組 data,此時(shí)我們需要給 CanvasBarrage 增加一個(gè)屬性用來存放 “加工” 后的每條彈幕對應(yīng)的實(shí)例。

// 文件:index.js
class CanvasBarrage {
    constructor(canvas, video, options = {}) {
        // 如果沒有傳入 canvas 或者 video 直接跳出
        if (!canvas || !video) return;
        this.canvas = canvas; // 當(dāng)前的 canvas 元素
        this.video = video; // 當(dāng)前的 video 元素

        // 設(shè)置 canvas 與 video 等高
        this.canvas.width = video.clientWidth;
        this.canvas.height = video.clientHeight;

        // 默認(rèn)暫停播放,表示不渲染彈幕
        this.isPaused = true;

        // 沒傳參數(shù)的默認(rèn)值
        let defaultOptions = {
            fontSize: 20,
            color: "gold",
            speed: 2,
            opacity: 0.3,
            data: []
        };

        // 對象的合并,將默認(rèn)參數(shù)對象的屬性和傳入對象的屬性統(tǒng)一放到當(dāng)前實(shí)例上
        Object.assign(this, defaultOptions, options);

        // ********** 以下為新增代碼 **********
        // 存放所有彈幕實(shí)例,Barrage 是創(chuàng)造每一條彈幕的實(shí)例的類
        this.barrages = this.data.map(item => new Barrage(item, this));
        // ********** 以上為新增代碼 **********
    }
}

其實(shí)通過上面操作以后,我們相當(dāng)于把 data 里面的每一條彈幕對象轉(zhuǎn)換成了一個(gè) Barrage 類的一個(gè)實(shí)例,把當(dāng)前的上下文 this 傳入后可以隨時(shí)在每一個(gè)彈幕實(shí)例上獲取 CanvasBarrage 類實(shí)例的屬性,也方便我們后續(xù)擴(kuò)展方法,遵循這種開放封閉原則的方式開發(fā),意義是不言而喻的。

3、在 CanvasBarrage 類實(shí)現(xiàn)渲染所有彈幕的 render 方法

CanvasBarragerender 方法是在創(chuàng)建彈幕功能實(shí)例的時(shí)候應(yīng)該渲染 Canvas 所以應(yīng)該在 CanvasBarrage 中調(diào)用,在 render 內(nèi)部,每一次渲染之前都應(yīng)該先將 Canvas 畫布清空,所以需要給當(dāng)前的 CanvasBarrage 類新增一個(gè)屬性用于存儲(chǔ) Canvas 畫布的內(nèi)容。

// 文件:index.js
class CanvasBarrage {
    constructor(canvas, video, options = {}) {
        // 如果沒有傳入 canvas 或者 video 直接跳出
        if (!canvas || !video) return;
        this.canvas = canvas; // 當(dāng)前的 canvas 元素
        this.video = video; // 當(dāng)前的 video 元素

        // 設(shè)置 canvas 與 video 等高
        this.canvas.width = video.clientWidth;
        this.canvas.height = video.clientHeight;

        // 默認(rèn)暫停播放,表示不渲染彈幕
        this.isPaused = true;

        // 沒傳參數(shù)的默認(rèn)值
        let defaultOptions = {
            fontSize: 20,
            color: "gold",
            speed: 2,
            opacity: 0.3,
            data: []
        };

        // 對象的合并,將默認(rèn)參數(shù)對象的屬性和傳入對象的屬性統(tǒng)一放到當(dāng)前實(shí)例上
        Object.assign(this, defaultOptions, options);

        // 存放所有彈幕實(shí)例,Barrage 是創(chuàng)造每一條彈幕的實(shí)例的類
        this.barrages = this.data.map(item => new Barrage(item, this));

        // ********** 以下為新增代碼 **********
        // Canvas 畫布的內(nèi)容
        this.context = canvas.getContext("2d");

        // 渲染所有的彈幕
        this.render();
        // ********** 以上為新增代碼 **********
    }

    // ********** 以下為新增代碼 **********
    render() {
        // 渲染整個(gè)彈幕
        // 第一次先進(jìn)行清空操作,執(zhí)行渲染彈幕,如果沒有暫停,繼續(xù)渲染
        this.context.clearRect(0, 0, this.canvas.width, this.canvas.height);
        // 渲染彈幕
        this.renderBarrage();
        if (this.isPaused == false) {
            // 遞歸渲染
            requestAnimationFrame(this.render.bind(this));
        }
    }
    // ********** 以上為新增代碼 **********
}

在上面的 CanvasBarragerender 函數(shù)中,清空時(shí)由于 Canvas 性能比較好,所以將整個(gè)畫布清空,所以從坐標(biāo) (0, 0) 點(diǎn),清空的寬高為整個(gè) Canvas 畫布的寬高。

只要視頻是在播放狀態(tài)應(yīng)該不斷的調(diào)用 render 方法實(shí)現(xiàn)清空畫布、渲染彈幕、判斷是否暫停,如果非暫停狀態(tài)繼續(xù)渲染,所以我們用到了遞歸調(diào)用 render 去不斷的實(shí)現(xiàn)渲染,但是遞歸時(shí)如果直接調(diào)用 render,性能特別差,程序甚至?xí)斓簦酝@種情況我們會(huì)在遞歸外層加一個(gè) setTimeout 來定義一個(gè)短暫的遞歸時(shí)間,但是這個(gè)過程類似于動(dòng)畫效果,如果使用 setTimeout 其實(shí)是將同步代碼轉(zhuǎn)成了異步執(zhí)行,會(huì)增加不確定性導(dǎo)致畫面出現(xiàn)卡頓的現(xiàn)象。

這里我們使用 H5 的新 API requestAnimationFrame,可以在平均 1/60 S 內(nèi)幫我執(zhí)行一次該方法傳入的回調(diào),我們直接把 render 函數(shù)作為回調(diào)函數(shù)傳入 requestAnimationFrame,該方法是按照幀的方式執(zhí)行,動(dòng)畫流暢,需要注意的是,render 函數(shù)內(nèi)使用了 this,所以應(yīng)該處理一下 this 指向問題。

由于我們使用面向?qū)ο蟮姆绞?,所以渲染彈幕的具體細(xì)節(jié),我們抽離出一個(gè)多帶帶的方法 renderBarrage,接下來看一下 renderBarrage 的實(shí)現(xiàn)。

4、CanvasBarrage 類 render 內(nèi)部 renderBarrage 的實(shí)現(xiàn)
// 文件:index.js
class CanvasBarrage {
    constructor(canvas, video, options = {}) {
        // 如果沒有傳入 canvas 或者 video 直接跳出
        if (!canvas || !video) return;
        this.canvas = canvas; // 當(dāng)前的 canvas 元素
        this.video = video; // 當(dāng)前的 video 元素

        // 設(shè)置 canvas 與 video 等高
        this.canvas.width = video.clientWidth;
        this.canvas.height = video.clientHeight;

        // 默認(rèn)暫停播放,表示不渲染彈幕
        this.isPaused = true;

        // 沒傳參數(shù)的默認(rèn)值
        let defaultOptions = {
            fontSize: 20,
            color: "gold",
            speed: 2,
            opacity: 0.3,
            data: []
        };

        // 對象的合并,將默認(rèn)參數(shù)對象的屬性和傳入對象的屬性統(tǒng)一放到當(dāng)前實(shí)例上
        Object.assign(this, defaultOptions, options);

        // 存放所有彈幕實(shí)例,Barrage 是創(chuàng)造每一條彈幕的實(shí)例的類
        this.barrages = this.data.map(item => new Barrage(item, this));

        // Canvas 畫布的內(nèi)容
        this.context = canvas.getContext("2d");

        // 渲染所有的彈幕
        this.render();
    }
    render() {
        // 渲染整個(gè)彈幕
        // 第一次先進(jìn)行清空操作,執(zhí)行渲染彈幕,如果沒有暫停,繼續(xù)渲染
        this.context.clearRect(0, 0, this.canvas.width, this.canvas.height);
        // 渲染彈幕
        this.renderBarrage();
        if (this.isPaused == false) {
            // 遞歸渲染
            requestAnimationFrame(this.render.bind(this));
        }
    }

    // ********** 以下為新增代碼 **********
    renderBarrage() {
        // 將數(shù)組的彈幕一個(gè)一個(gè)取出,判斷時(shí)間和視頻的時(shí)間是否符合,符合就執(zhí)行渲染此彈幕
        let time = this.video.currentTime;
        this.barrages.forEach(barrage => {
            // 當(dāng)視頻時(shí)間大于等于了彈幕設(shè)置的時(shí)間,那么開始渲染(時(shí)間都是以秒為單位)
            if (time >= barrage.time) {
                // 初始化彈幕的各個(gè)參數(shù),只有在彈幕將要出現(xiàn)的時(shí)候再去初始化,節(jié)省性能,初始化后再進(jìn)行繪制
                // 如果沒有初始化,先去初始化一下
                if (!barrage.isInited) {
                    // 初始化后下次再渲染就不需要再初始化了,所以創(chuàng)建一個(gè)標(biāo)識 isInited
                    barrage.init();
                    barrage.isInited = true;
                }
            }
        });
    }
    // ********** 以上為新增代碼 **********
}

此處的 renderBarrage 方法內(nèi)部主要對每一條彈幕實(shí)例所設(shè)置的出現(xiàn)時(shí)間和視頻的播放時(shí)間做對比,如果視頻的播放時(shí)間大于等于了彈幕出現(xiàn)的時(shí)間,說明彈幕需要繪制在 Canvas 畫布內(nèi)。

之前我們的每一條彈幕實(shí)例的屬性可能不全,彈幕的其他未傳參數(shù)并沒有初始化,所以為了最大限度的節(jié)省性能,我們在彈幕該第一次繪制的時(shí)候去初始化參數(shù),等到視頻播放的時(shí)間變化再去重新繪制時(shí),不再初始化參數(shù),所以初始化參數(shù)的方法放在了判斷彈幕出現(xiàn)時(shí)間的條件里面執(zhí)行,又設(shè)置了代表彈幕實(shí)例是不是初始化了的參數(shù) isInited,初始化函數(shù) init 執(zhí)行過一次后,馬上修改 isInited 的值,保證只初始化參數(shù)一次。

renderBarrage 方法中我們可以看出來,其實(shí)我們是循環(huán)了專門存放每一條彈幕實(shí)例(Barrage 類的實(shí)例)的數(shù)組,我們在內(nèi)部用實(shí)例去調(diào)用的方法 init 應(yīng)該是在 Barrage 類的原型上,下面我們?nèi)?Barrage 類上實(shí)現(xiàn) init 的邏輯。

5、Barrage 類 init 的實(shí)現(xiàn)
// 文件:index.js
class Barrage {
    constructor(item, ctx) {
        this.value = item.value; // 彈幕的內(nèi)容
        this.time = item.time; // 彈幕出現(xiàn)的時(shí)間
        this.item = item; // 每一個(gè)彈幕的數(shù)據(jù)對象
        this.ctx = ctx; // 彈幕功能類的執(zhí)行上下文
    }

    // ********** 以下為新增代碼 **********
    init() {
        this.opacity = this.item.opacity || this.ctx.opacity;
        this.color = this.item.color || this.ctx.color;
        this.fontSize = this.item.fontSize || this.ctx.fontSize;
        this.speed = this.item.speed || this.ctx.speed;

        // 求自己的寬度,目的是用來校驗(yàn)當(dāng)前是否還要繼續(xù)繪制(邊界判斷)
        let span = document.createElement("span");

        // 能決定寬度的只有彈幕的內(nèi)容和文字的大小,和字體,字體默認(rèn)為微軟雅黑,我們就不做設(shè)置了
        span.innerText = this.value;
        span.style.font = this.fontSize + "px "Microsoft YaHei";

        // span 為行內(nèi)元素,取不到寬度,所以我們通過定位給轉(zhuǎn)換成塊級元素
        span.style.position = "absolute";

        document.body.appendChild(span); // 放入頁面
        this.width = span.clientWidth; // 記錄彈幕的寬度
        document.body.removeChild(span); // 從頁面移除

        // 存儲(chǔ)彈幕出現(xiàn)的橫縱坐標(biāo)
        this.x = this.ctx.canvas.width;
        this.y = this.ctx.canvas.height;

        // 處理彈幕縱向溢出的邊界處理
        if (this.y < this.fontSize) {
            this.y = this.fontSize;
        }
        if (this.y > this.ctx.canvas.height - this.fontSize) {
            this.y = this.ctx.canvas.height - this.fontSize;
        }
    }
    // ********** 以上為新增代碼 **********
}

在上面代碼的 init 方法中我們其實(shí)可以看出,每條彈幕實(shí)例初始化的時(shí)候初始的信息除了之前說的彈幕的基本參數(shù)外,還獲取了每條彈幕的寬度(用于后續(xù)做彈幕是否已經(jīng)完全移出屏幕的邊界判斷)和每一條彈幕的 xy 軸方向的坐標(biāo)并為了防止彈幕在 y 軸顯示不全做了邊界處理。

6、實(shí)現(xiàn)每條彈幕的渲染和彈幕移除屏幕的處理

我們當(dāng)時(shí)在 CanvasBarrage 類的 render 方法中的渲染每個(gè)彈幕的方法 renderBarrage中(原諒這么啰嗦,因?yàn)榈浆F(xiàn)在內(nèi)容已經(jīng)比較多,說的具體一點(diǎn)方便知道是哪個(gè)步驟,哈哈)只做了對每一條彈幕實(shí)例的初始化操作,并沒有渲染在 Canvas 畫布中,這時(shí)我們主要做兩部操作,實(shí)現(xiàn)每條彈幕渲染在畫布中和左側(cè)移出屏幕不再渲染的邊界處理。

// 文件:index.js
class CanvasBarrage {
    constructor(canvas, video, options = {}) {
        // 如果沒有傳入 canvas 或者 video 直接跳出
        if (!canvas || !video) return;
        this.canvas = canvas; // 當(dāng)前的 canvas 元素
        this.video = video; // 當(dāng)前的 video 元素

        // 設(shè)置 canvas 與 video 等高
        this.canvas.width = video.clientWidth;
        this.canvas.height = video.clientHeight;

        // 默認(rèn)暫停播放,表示不渲染彈幕
        this.isPaused = true;

        // 沒傳參數(shù)的默認(rèn)值
        let defaultOptions = {
            fontSize: 20,
            color: "gold",
            speed: 2,
            opacity: 0.3,
            data: []
        };

        // 對象的合并,將默認(rèn)參數(shù)對象的屬性和傳入對象的屬性統(tǒng)一放到當(dāng)前實(shí)例上
        Object.assign(this, defaultOptions, options);

        // 存放所有彈幕實(shí)例,Barrage 是創(chuàng)造每一條彈幕的實(shí)例的類
        this.barrages = this.data.map(item => new Barrage(item, this));

        // Canvas 畫布的內(nèi)容
        this.context = canvas.getContext("2d");

        // 渲染所有的彈幕
        this.render();
    }
    render() {
        // 渲染整個(gè)彈幕
        // 第一次先進(jìn)行清空操作,執(zhí)行渲染彈幕,如果沒有暫停,繼續(xù)渲染
        this.context.clearRect(0, 0, this.canvas.width, this.canvas.height);
        // 渲染彈幕
        this.renderBarrage();
        if (this.isPaused == false) {
            // 遞歸渲染
            requestAnimationFrame(this.render.bind(this));
        }
    }
    renderBarrage() {
        // 將數(shù)組的彈幕一個(gè)一個(gè)取出,判斷時(shí)間和視頻的時(shí)間是否符合,符合就執(zhí)行渲染此彈幕
        let time = this.video.currentTime;
        this.barrages.forEach(barrage => {
            // ********** 以下為改動(dòng)的代碼 **********
            // 當(dāng)視頻時(shí)間大于等于了彈幕設(shè)置的時(shí)間,那么開始渲染(時(shí)間都是以秒為單位)
            if (!barrage.flag && time >= barrage.time) {
                // ********** 以上為改動(dòng)的代碼 **********

                // 初始化彈幕的各個(gè)參數(shù),只有在彈幕將要出現(xiàn)的時(shí)候再去初始化,節(jié)省性能,初始化后再進(jìn)行繪制
                // 如果沒有初始化,先去初始化一下
                if (!barrage.isInited) {
                    // 初始化后下次再渲染就不需要再初始化了,所以創(chuàng)建一個(gè)標(biāo)識 isInited
                    barrage.init();
                    barrage.isInited = true;
                }

                // ********** 以下為新增代碼 **********
                barrage.x -= barrage.speed;
                barrage.render(); // 渲染該條彈幕
                if (barrage.x < barrage.width * -1) {
                    barrage.flag = true; // 是否出去了,出去了,做停止渲染的操作
                }
                // ********** 以上為新增代碼 **********
            }
        });
    }
}

每個(gè)彈幕實(shí)例都有一個(gè) speed 屬性,該屬性代表著彈幕移動(dòng)的速度,換個(gè)說法其實(shí)就是每次減少的 x 軸的差值,所以我們其實(shí)是通過改變 x 軸的值再重新渲染而實(shí)現(xiàn)彈幕的左移,我們創(chuàng)建了一個(gè)標(biāo)識 flag 掛在每個(gè)彈幕實(shí)例下,代表是否已經(jīng)離開屏幕,如果離開則更改 flag 的值,使外層的 CanvasBarrage 類的 render 函數(shù)再次遞歸時(shí)不進(jìn)入渲染程序。

每一條彈幕具體是怎么渲染的,通過代碼可以看出每個(gè)彈幕實(shí)例在 x 坐標(biāo)改變后都調(diào)用了實(shí)例方法 render 函數(shù),注意此 render 非彼 render,該 render 函數(shù)屬于 Barrage 類,目的是為了渲染每一條彈幕,而 CanvasBarrage 類下的 render,是為了在視頻時(shí)間變化時(shí)清空并重新渲染整個(gè) Canvas 畫布。

7、Barrage 類下的 render 方法的實(shí)現(xiàn)
// 文件:index.js
class Barrage {
    constructor(item, ctx) {
        this.value = item.value; // 彈幕的內(nèi)容
        this.time = item.time; // 彈幕出現(xiàn)的時(shí)間
        this.item = item; // 每一個(gè)彈幕的數(shù)據(jù)對象
        this.ctx = ctx; // 彈幕功能類的執(zhí)行上下文
    }
    init() {
        this.opacity = this.item.opacity || this.ctx.opacity;
        this.color = this.item.color || this.ctx.color;
        this.fontSize = this.item.fontSize || this.ctx.fontSize;
        this.speed = this.item.speed || this.ctx.speed;

        // 求自己的寬度,目的是用來校驗(yàn)當(dāng)前是否還要繼續(xù)繪制(邊界判斷)
        let span = document.createElement("span");

        // 能決定寬度的只有彈幕的內(nèi)容和文字的大小,和字體,字體默認(rèn)為微軟雅黑,我們就不做設(shè)置了
        span.innerText = this.value;
        span.style.font = this.fontSize + "px "Microsoft YaHei";

        // span 為行內(nèi)元素,取不到寬度,所以我們通過定位給轉(zhuǎn)換成塊級元素
        span.style.position = "absolute";

        document.body.appendChild(span); // 放入頁面
        this.width = span.clientWidth; // 記錄彈幕的寬度
        document.body.removeChild(span); // 從頁面移除

        // 存儲(chǔ)彈幕出現(xiàn)的橫縱坐標(biāo)
        this.x = this.ctx.canvas.width;
        this.y = this.ctx.canvas.height;

        // 處理彈幕縱向溢出的邊界處理
        if (this.y < this.fontSize) {
            this.y = this.fontSize;
        }
        if (this.y > this.ctx.canvas.height - this.fontSize) {
            this.y = this.ctx.canvas.height - this.fontSize;
        }
    }

    // ********** 以下為新增代碼 **********
    render() {
        this.ctx.context.font = this.fontSize + "px "Microsoft YaHei"";
        this.ctx.context.fillStyle = this.color;
        this.ctx.context.fillText(this.value, this.x, this.y);
    }
    // ********** 以上為新增代碼 **********
}

從上面新增代碼我們可以看出,其實(shí) Barrage 類的 render 方法只是將每一條彈幕的字號、顏色、內(nèi)容、坐標(biāo)等屬性通過 Canvas 的 API 添加到了畫布上。

8、實(shí)現(xiàn)播放、暫停事件

還記得我們的 CanvasBarrage 類里面有一個(gè)屬性 isPaused,屬性值控制了我們是否遞歸渲染,這個(gè)屬性與視頻暫停的狀態(tài)是一致的,我們在播放的時(shí)候,彈幕不斷的清空并重新繪制,當(dāng)暫停的時(shí)候彈幕也應(yīng)該跟著暫停,說白了就是不在調(diào)用 CanvasBarrage 類的 render 方法,其實(shí)就是在暫停、播放的過程中不斷的改變 isPaused 的值即可。

還記得我們之前構(gòu)造的兩條假數(shù)據(jù) data 吧,接下來我們添加播放、暫停事件,來嘗試使用一下我們的彈幕功能。

// 文件:index.js
// 實(shí)現(xiàn)一個(gè)簡易選擇器,方便獲取元素,后面獲取元素直接調(diào)用 $
const $ = document.querySelector.bind(document);

// 獲取 Canvas 元素和 Video 元素
let canvas = $("#canvas");
let video = $("#video");

let canvasBarrage = new CanvasBarrage(canvas, video, {
    data
});

// 添加播放事件
video.addEventListener("play", function() {
    canvasBarrage.isPaused = false;
    canvasBarrage.render();
});

// 添加暫停事件
video.addEventListener("pause", function() {
    canvasBarrage.isPaused = true;
});
9、實(shí)現(xiàn)發(fā)送彈幕事件
// 文件:index.js
$("#add").addEventListener("click", function() {
    let time = video.currentTime; // 發(fā)送彈幕的時(shí)間
    let value = $("#text").value; // 發(fā)送彈幕的文字
    let color = $("#color").value; // 發(fā)送彈幕文字的顏色
    let fontSize = $("#range").value; // 發(fā)送彈幕的字體大小
    let sendObj = { time, value, color, fontSize }; //發(fā)送彈幕的參數(shù)集合
    canvasBarrage.add(sendObj); // 發(fā)送彈幕的方法
});

其實(shí)我們發(fā)送彈幕時(shí),就是向 CanvasBarrage 類的 barrages 數(shù)組里添加了一條彈幕的實(shí)例,我們多帶帶封裝了一個(gè) add 的實(shí)例方法。

// 文件:index.js
class CanvasBarrage {
    constructor(canvas, video, options = {}) {
        // 如果沒有傳入 canvas 或者 video 直接跳出
        if (!canvas || !video) return;
        this.canvas = canvas; // 當(dāng)前的 canvas 元素
        this.video = video; // 當(dāng)前的 video 元素

        // 設(shè)置 canvas 與 video 等高
        this.canvas.width = video.clientWidth;
        this.canvas.height = video.clientHeight;

        // 默認(rèn)暫停播放,表示不渲染彈幕
        this.isPaused = true;

        // 沒傳參數(shù)的默認(rèn)值
        let defaultOptions = {
            fontSize: 20,
            color: "gold",
            speed: 2,
            opacity: 0.3,
            data: []
        };

        // 對象的合并,將默認(rèn)參數(shù)對象的屬性和傳入對象的屬性統(tǒng)一放到當(dāng)前實(shí)例上
        Object.assign(this, defaultOptions, options);

        // 存放所有彈幕實(shí)例,Barrage 是創(chuàng)造每一條彈幕的實(shí)例的類
        this.barrages = this.data.map(item => new Barrage(item, this));

        // Canvas 畫布的內(nèi)容
        this.context = canvas.getContext("2d");

        // 渲染所有的彈幕
        this.render();
    }
    render() {
        // 渲染整個(gè)彈幕
        // 第一次先進(jìn)行清空操作,執(zhí)行渲染彈幕,如果沒有暫停,繼續(xù)渲染
        this.context.clearRect(0, 0, this.canvas.width, this.canvas.height);
        // 渲染彈幕
        this.renderBarrage();
        if (this.isPaused == false) {
            // 遞歸渲染
            requestAnimationFrame(this.render.bind(this));
        }
    }
    renderBarrage() {
        // 將數(shù)組的彈幕一個(gè)一個(gè)取出,判斷時(shí)間和視頻的時(shí)間是否符合,符合就執(zhí)行渲染此彈幕
        let time = this.video.currentTime;
        this.barrages.forEach(barrage => {
            // 當(dāng)視頻時(shí)間大于等于了彈幕設(shè)置的時(shí)間,那么開始渲染(時(shí)間都是以秒為單位)
            if (!barrage.flag && time >= barrage.time) {
                // 初始化彈幕的各個(gè)參數(shù),只有在彈幕將要出現(xiàn)的時(shí)候再去初始化,節(jié)省性能,初始化后再進(jìn)行繪制
                // 如果沒有初始化,先去初始化一下
                if (!barrage.isInited) {
                    // 初始化后下次再渲染就不需要再初始化了,所以創(chuàng)建一個(gè)標(biāo)識 isInited
                    barrage.init();
                    barrage.isInited = true;
                }

                barrage.x -= barrage.speed;
                barrage.render(); // 渲染該條彈幕
                if (barrage.x < barrage.width * -1) {
                    barrage.flag = true; // 是否出去了,出去了,做停止渲染的操作
                }
            }
        });
    }

    // ********** 以下為新增代碼 **********
    add(item) {
        this.barrages.push(new Barrage(item, this));
    }
    // ********** 以上為新增代碼 **********
}
10、拖動(dòng)進(jìn)度條實(shí)現(xiàn)彈幕的前進(jìn)和后退

其實(shí)我們發(fā)現(xiàn),彈幕雖然實(shí)現(xiàn)了正常的播放、暫停以及發(fā)送,但是當(dāng)我們拖動(dòng)進(jìn)度條的時(shí)候彈幕應(yīng)該是跟著視頻時(shí)間同步播放的,現(xiàn)在的彈幕一旦播放過無論怎樣拉動(dòng)進(jìn)度條彈幕都不會(huì)再出現(xiàn),我們現(xiàn)在就來解決這個(gè)問題。

// 文件:index.js
// 拖動(dòng)進(jìn)度條事件
video.addEventListener("seeked", function() {
    canvasBarrage.reset();
});

我們在事件內(nèi)部其實(shí)只是調(diào)用了一下 CanvasBarrage 類的 reset 方法,這個(gè)方法就是在拖動(dòng)進(jìn)度條的時(shí)候來幫我們初始化彈幕的狀態(tài)。

// 文件:index.js
class CanvasBarrage {
    constructor(canvas, video, options = {}) {
        // 如果沒有傳入 canvas 或者 video 直接跳出
        if (!canvas || !video) return;
        this.canvas = canvas; // 當(dāng)前的 canvas 元素
        this.video = video; // 當(dāng)前的 video 元素

        // 設(shè)置 canvas 與 video 等高
        this.canvas.width = video.clientWidth;
        this.canvas.height = video.clientHeight;

        // 默認(rèn)暫停播放,表示不渲染彈幕
        this.isPaused = true;

        // 沒傳參數(shù)的默認(rèn)值
        let defaultOptions = {
            fontSize: 20,
            color: "gold",
            speed: 2,
            opacity: 0.3,
            data: []
        };

        // 對象的合并,將默認(rèn)參數(shù)對象的屬性和傳入對象的屬性統(tǒng)一放到當(dāng)前實(shí)例上
        Object.assign(this, defaultOptions, options);

        // 存放所有彈幕實(shí)例,Barrage 是創(chuàng)造每一條彈幕的實(shí)例的類
        this.barrages = this.data.map(item => new Barrage(item, this));

        // Canvas 畫布的內(nèi)容
        this.context = canvas.getContext("2d");

        // 渲染所有的彈幕
        this.render();
    }
    render() {
        // 渲染整個(gè)彈幕
        // 第一次先進(jìn)行清空操作,執(zhí)行渲染彈幕,如果沒有暫停,繼續(xù)渲染
        this.context.clearRect(0, 0, this.canvas.width, this.canvas.height);
        // 渲染彈幕
        this.renderBarrage();
        if (this.isPaused == false) {
            // 遞歸渲染
            requestAnimationFrame(this.render.bind(this));
        }
    }
    renderBarrage() {
        // 將數(shù)組的彈幕一個(gè)一個(gè)取出,判斷時(shí)間和視頻的時(shí)間是否符合,符合就執(zhí)行渲染此彈幕
        let time = this.video.currentTime;
        this.barrages.forEach(barrage => {
            // 當(dāng)視頻時(shí)間大于等于了彈幕設(shè)置的時(shí)間,那么開始渲染(時(shí)間都是以秒為單位)
            if (!barrage.flag && time >= barrage.time) {
                // 初始化彈幕的各個(gè)參數(shù),只有在彈幕將要出現(xiàn)的時(shí)候再去初始化,節(jié)省性能,初始化后再進(jìn)行繪制
                // 如果沒有初始化,先去初始化一下
                if (!barrage.isInited) {
                    // 初始化后下次再渲染就不需要再初始化了,所以創(chuàng)建一個(gè)標(biāo)識 isInited
                    barrage.init();
                    barrage.isInited = true;
                }

                barrage.x -= barrage.speed;
                barrage.render(); // 渲染該條彈幕
                if (barrage.x < barrage.width * -1) {
                    barrage.flag = true; // 是否出去了,出去了,做停止渲染的操作
                }
            }
        });
    }
    add(item) {
        this.barrages.push(new Barrage(item, this));
    }

    // ********** 以下為新增代碼 **********
    reset() {
        // 先清空 Canvas 畫布
        this.context.clearRect(0, 0, this.canvas.width, this.canvas.height);
        let time = this.video.currentTime;
        // 循環(huán)每一條彈幕實(shí)例
        this.barrages.forEach(barrage => {
            // 更改已經(jīng)移出屏幕的彈幕狀態(tài)
            barrage.flag = false;
            // 當(dāng)拖動(dòng)到的時(shí)間小于等于當(dāng)前彈幕時(shí)間是,重新初始化彈幕的數(shù)據(jù),實(shí)現(xiàn)渲染
            if (time <= barrage.time) {
                barrage.isInited = false;
            } else {
                barrage.flag = true; // 否則將彈幕的狀態(tài)設(shè)置為以移出屏幕
            }
        });
    }
    // ********** 以上為新增代碼 **********
}

其實(shí) reset 方法中值做了幾件事:

清空 Canvas 畫布;

獲取當(dāng)前進(jìn)度條拖動(dòng)位置的時(shí)間;

循環(huán)存儲(chǔ)彈幕實(shí)例的數(shù)組;

將所有彈幕更改為未移出屏幕;

判斷拖動(dòng)時(shí)間和每條彈幕的時(shí)間;

在當(dāng)前時(shí)間以后的彈幕重新初始化數(shù)據(jù);

以前的彈幕更改為已移出屏幕。

從而實(shí)現(xiàn)了拖動(dòng)進(jìn)度條彈幕的 “前進(jìn)” 和 “后退” 功能。


使用 WebSocket 和 Redis 實(shí)現(xiàn)前后端通信及數(shù)據(jù)存儲(chǔ) 1、服務(wù)器代碼的實(shí)現(xiàn)

要使用 WebSocket 和 Redis 首先需要去安裝 ws、redis 依賴,在項(xiàng)目根目錄執(zhí)行下面命令:

npm install ws redis

我們創(chuàng)建一個(gè) server.js 文件,用來寫服務(wù)端的代碼:

// 文件:index.js
const WebSocket = require("ws"); // 引入 WebSocket
const redis = require("redis"); // 引入 redis

// 初始化 WebSocket 服務(wù)器,端口號為 3000
let wss = new WebSocket.Server({
    port: 3000
});

// 創(chuàng)建 redis 客戶端
let client = redis.createClient(); // key value

// 原生的 websocket 就兩個(gè)常用的方法 on("message")、on("send")
wss.on("connection", function(ws) {
    // 監(jiān)聽連接
    // 連接上需要立即把 redis 數(shù)據(jù)庫的數(shù)據(jù)取出返回給前端
    client.lrange("barrages", 0, -1, function(err, applies) {
        // 由于 redis 的數(shù)據(jù)都是字符串,所以需要把數(shù)組中每一項(xiàng)轉(zhuǎn)成對象
        applies = applies.map(item => JSON.parse(item));

        // 使用 websocket 服務(wù)器將 redis 數(shù)據(jù)庫的數(shù)據(jù)發(fā)送給前端
        // 構(gòu)建一個(gè)對象,加入 type 屬性告訴前端當(dāng)前返回?cái)?shù)據(jù)的行為,并將數(shù)據(jù)轉(zhuǎn)換成字符串
        ws.send(
            JSON.stringify({
                type: "INIT",
                data: applies
            })
        );
    });

    // 當(dāng)服務(wù)器收到消息時(shí),將數(shù)據(jù)存入 redis 數(shù)據(jù)庫
    ws.on("message", function(data) {
        // 向數(shù)據(jù)庫存儲(chǔ)時(shí)存的是字符串,存入并打印數(shù)據(jù),用來判斷是否成功存入數(shù)據(jù)庫
        client.rpush("barrages", data, redis.print);

        // 再將當(dāng)前這條數(shù)據(jù)返回給前端,同樣添加 type 字段告訴前端當(dāng)前行為,并將數(shù)據(jù)轉(zhuǎn)換成字符串
        ws.send(
            JSON.stringify({
                type: "ADD",
                data: JSON.parse(data)
            })
        );
    });
});

服務(wù)器的邏輯很清晰,在 WebSocket 連接上時(shí),立即獲取 Redis 數(shù)據(jù)庫的所有彈幕數(shù)據(jù)返回給前端,當(dāng)前端點(diǎn)擊發(fā)送彈幕按鈕發(fā)送數(shù)據(jù)時(shí),接收數(shù)據(jù)存入 Redis 數(shù)據(jù)庫中并打印驗(yàn)證數(shù)據(jù)是否成功存入,再通過 WebSocket 服務(wù)把當(dāng)前這一條數(shù)返回給前端,需要注意一下幾點(diǎn):

從 Redis 數(shù)據(jù)庫中取出全部彈幕數(shù)據(jù)的數(shù)組內(nèi)部都存儲(chǔ)的是字符串,需要使用 JSON.parse 方法進(jìn)行解析;

將數(shù)據(jù)發(fā)送前端時(shí),最外層要使用 JSON.stringify 重新轉(zhuǎn)換成字符串發(fā)送;

在初始化階段 WebSocket 發(fā)送所有數(shù)據(jù)和前端添加新彈幕 WebSocket 將彈幕的單條數(shù)據(jù)重新返回時(shí),需要添加對應(yīng)的 type 值告訴前端,當(dāng)前的操作行為。

2、前端代碼的修改

在沒有實(shí)現(xiàn)后端代碼之前,前端使用的是 data 的假數(shù)據(jù),是在添加彈幕事件中,將獲取的新增彈幕信息通過 CanvasBarrage 類的 add 方法直接創(chuàng)建 Barrage 類的實(shí)例,并加入到存放彈幕實(shí)例的 barrages 數(shù)組中。

現(xiàn)在我們需要更正一下交互邏輯,在發(fā)送彈幕事件觸發(fā)時(shí),我們應(yīng)該先將獲取的單條彈幕數(shù)據(jù)通過 WebSocket 發(fā)送給后端服務(wù)器,在服務(wù)器重新將消息返還給我們的時(shí)候,去將這條數(shù)據(jù)通過 CanvasBarrage 類的 add 方法加入到存放彈幕實(shí)例的 barrages 數(shù)組中。

還有在頁面初始化時(shí),我們之前在創(chuàng)建 CanvasBarrage 類實(shí)例的時(shí)候直接傳入了 data 假數(shù)據(jù),現(xiàn)在需要通過 WebSocket 的連接事件,在監(jiān)聽到連接 WebSocket 服務(wù)時(shí),去創(chuàng)建 CanvasBarrage 類的實(shí)例,并直接把服務(wù)端返回 Redis 數(shù)據(jù)庫真實(shí)的數(shù)據(jù)作為參數(shù)傳入,前端代碼修改如下:

// 文件:index.js
// ********** 下面代碼被刪掉了 **********
// let canvasBarrage = new CanvasBarrage(canvas, video, {
//     data
// });
// ********** 上面代碼被刪掉了 **********

// ********** 以下為新增代碼 **********
let canvasBarrage;

// 創(chuàng)建 WebSocket 連接
let socket = new WebSocket("ws://localhost:3000");

// 監(jiān)聽連接事件
socket.onopen = function() {
    // 監(jiān)聽消息
    socket.onmessage = function(e) {
        // 將收到的消息從字符串轉(zhuǎn)換成對象
        let message = JSON.parse(e.data);

        // 根據(jù)不同情況判斷是初始化還是發(fā)送彈幕
        if (message.type === "INIT") {
            // 創(chuàng)建 CanvasBarrage 的實(shí)例添加彈幕功能,傳入真實(shí)的數(shù)據(jù)
            canvasBarrage = new CanvasBarrage(canvas, video, {
                data: message.data
            });
        } else if (message.type === "ADD") {
            // 如果是添加彈幕直接將 WebSocket 返回的單條彈幕存入 barrages 中
            canvasBarrage.add(message.data);
        }
    };
};
// ********** 以上為新增代碼 **********

$("#add").addEventListener("click", function() {
    let time = video.currentTime; // 發(fā)送彈幕的時(shí)間
    let value = $("#text").value; // 發(fā)送彈幕的文字
    let color = $("#color").value; // 發(fā)送彈幕文字的顏色
    let fontSize = $("#range").value; // 發(fā)送彈幕的字體大小
    let sendObj = { time, value, color, fontSize }; //發(fā)送彈幕的參數(shù)集合

    // ********** 以下為新增代碼 **********
    socket.send(JSON.stringify(sendObj));
    // ********** 以上為新增代碼 **********

    // ********** 下面代碼被刪掉了 **********
    // canvasBarrage.add(sendObj); // 發(fā)送彈幕的方法
    // ********** 上面代碼被刪掉了 **********
});

現(xiàn)在我們可以打開 index.html 文件并啟動(dòng) server.js 服務(wù)器,就可以實(shí)現(xiàn)真實(shí)的視頻彈幕操作了,但是我們還是差了最后一步,當(dāng)前的服務(wù)只能同時(shí)服務(wù)一個(gè)人,但真實(shí)的場景是同時(shí)看視頻的有很多人,而且發(fā)送的彈幕是共享的。

3、實(shí)現(xiàn)多端通信、彈幕共享

我們需要處理兩件事情:

第一件事情是實(shí)現(xiàn)多端通信共享數(shù)據(jù)庫信息;

第二件事情是當(dāng)有人離開的時(shí)候清除關(guān)閉的 WebSocket 對象。

// 文件:server.js
const WebSocket = require("ws"); // 引入 WebSocket
const redis = require("redis"); // 引入 redis

// 初始化 WebSocket 服務(wù)器,端口號為 3000
let wss = new WebSocket.Server({
    port: 3000
});

// 創(chuàng)建 redis 客戶端
let client = redis.createClient(); // key value

// ********** 以下為新增代碼 **********
// 存儲(chǔ)所有 WebSocket 用戶
let clientsArr = [];
// ********** 以上為新增代碼 **********

// 原生的 websocket 就兩個(gè)常用的方法 on("message")、on("send")
wss.on("connection", function(ws) {
    // ********** 以下為新增代碼 **********
    // 將所有通過 WebSocket 連接的用戶存入數(shù)組中
    clientsArr.push(ws);
    // ********** 以上為新增代碼 **********

    // 監(jiān)聽連接
    // 連接上需要立即把 redis 數(shù)據(jù)庫的數(shù)據(jù)取出返回給前端
    client.lrange("barrages", 0, -1, function(err, applies) {
        // 由于 redis 的數(shù)據(jù)都是字符串,所以需要把數(shù)組中每一項(xiàng)轉(zhuǎn)成對象
        applies = applies.map(item => JSON.parse(item));

        // 使用 websocket 服務(wù)器將 redis 數(shù)據(jù)庫的數(shù)據(jù)發(fā)送給前端
        // 構(gòu)建一個(gè)對象,加入 type 屬性告訴前端當(dāng)前返回?cái)?shù)據(jù)的行為,并將數(shù)據(jù)轉(zhuǎn)換成字符串
        ws.send(
            JSON.stringify({
                type: "INIT",
                data: applies
            })
        );
    });

    // 當(dāng)服務(wù)器收到消息時(shí),將數(shù)據(jù)存入 redis 數(shù)據(jù)庫
    ws.on("message", function(data) {
        // 向數(shù)據(jù)庫存儲(chǔ)時(shí)存的是字符串,存入并打印數(shù)據(jù),用來判斷是否成功存入數(shù)據(jù)庫
        client.rpush("barrages", data, redis.print);

        // ********** 以下為修改后的代碼 **********
        // 循環(huán)數(shù)組,將某一個(gè)人新發(fā)送的彈幕在存儲(chǔ)到 Redis 之后返回給所有用戶
        clientsArr.forEach(w => {
            // 再將當(dāng)前這條數(shù)據(jù)返回給前端,同樣添加 type 字段告訴前端當(dāng)前行為,并將數(shù)據(jù)轉(zhuǎn)換成字符串
            w.send(
                JSON.stringify({
                    type: "ADD",
                    data: JSON.parse(data)
                })
            );
        });
        // ********** 以上為修改后的代碼 **********
    });

    // ********** 以下為新增代碼 **********
    // 監(jiān)聽關(guān)閉連接事件
    ws.on("close", function() {
        // 當(dāng)某一個(gè)人關(guān)閉連接離開時(shí),將這個(gè)人從當(dāng)前存儲(chǔ)用戶的數(shù)組中移除
        clientsArr = clientsArr.filter(client => client != ws);
    });
    // ********** 以上為新增代碼 **********
});

上面就是 Canvas + WebSocket + Redis 視頻彈幕的實(shí)現(xiàn),實(shí)現(xiàn)過程可能有些復(fù)雜,但整個(gè)過程寫的還是比較詳細(xì),可能需要一定的耐心慢慢的讀完,并最好一步一步跟著寫一寫,希望這篇文章可以讓讀到的人解決視頻彈幕類似的需求,真正理解整個(gè)過程和開放封閉原則,認(rèn)識到前端面向?qū)ο缶幊趟枷氲拿馈?/p>


文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。

轉(zhuǎn)載請注明本文地址:http://systransis.cn/yun/53053.html

相關(guān)文章

  • Canvas + WebSocket + Redis 實(shí)現(xiàn)一個(gè)視頻彈幕

    摘要:創(chuàng)建彈幕功能的類及基本參數(shù)處理布局時(shí)需要注意的默認(rèn)寬為,高為,我們要保證完全覆蓋整個(gè)視頻,需要讓與寬高相等。因?yàn)槲覀儾淮_定每一個(gè)使用該功能的視頻的寬高都是一樣的,所以畫布的寬高并沒有通過來設(shè)置,而是通過在類創(chuàng)建實(shí)例初始化屬性的時(shí)候動(dòng)態(tài)設(shè)置。 showImg(https://segmentfault.com/img/remote/1460000018998386); 閱讀原文 頁面布...

    WelliJhon 評論0 收藏0
  • Canvas + WebSocket + Redis 實(shí)現(xiàn)一個(gè)視頻彈幕

    摘要:創(chuàng)建彈幕功能的類及基本參數(shù)處理布局時(shí)需要注意的默認(rèn)寬為,高為,我們要保證完全覆蓋整個(gè)視頻,需要讓與寬高相等。因?yàn)槲覀儾淮_定每一個(gè)使用該功能的視頻的寬高都是一樣的,所以畫布的寬高并沒有通過來設(shè)置,而是通過在類創(chuàng)建實(shí)例初始化屬性的時(shí)候動(dòng)態(tài)設(shè)置。 showImg(https://segmentfault.com/img/remote/1460000018998386); 閱讀原文 頁面布...

    wangdai 評論0 收藏0
  • 彈幕,是怎樣練成的?

    showImg(https://segmentfault.com/img/bVbk1Nl?w=1080&h=602); 說起彈幕看過視頻的都不會(huì)陌生,那滿屏充滿著飄逸評論的效果,讓人如癡如醉,無法自拔 最近也是因?yàn)樵趯W(xué)習(xí)關(guān)于 canvas 的知識,所以今天就想和大家分享一個(gè)關(guān)于彈幕的故事 那么究竟彈幕是怎樣煉成的呢? 我們且往下看(look) 看什么?看效果 showImg(https://s...

    lwx12525 評論0 收藏0
  • 全棧開發(fā)——?jiǎng)邮执蛟鞂儆谧约旱闹辈ラg(Vue+SpringBoot+Nginx)

    摘要:經(jīng)過琢磨,其實(shí)是要考慮安全性的。具體在以下幾個(gè)方面跨域連接協(xié)議升級前握手?jǐn)r截器消息信道攔截器對于跨域問題,我們可以通過方法來設(shè)置可連接的域名,防止跨站連接。 前言 大學(xué)的學(xué)習(xí)時(shí)光臨近尾聲,感嘆時(shí)光匆匆,三年一晃而過。同學(xué)們都忙著找工作,我也在這里拋一份簡歷吧,歡迎各位老板和獵手誠邀。我們進(jìn)入正題。直播行業(yè)是當(dāng)前火熱的行業(yè),誰都想從中分得一杯羹,直播養(yǎng)活了一大批人,一個(gè)平臺(tái)主播粗略估計(jì)就...

    e10101 評論0 收藏0

發(fā)表評論

0條評論

最新活動(dòng)
閱讀需要支付1元查看
<