热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

Canvas+WebSocket+Redis实现一个视频弹幕

首先,我们需要实现页面布局,在根目录创建##定义接口,构造假数据我们弹幕中的弹幕数据正常情况下应该是
Canvas + WebSocket + Redis 实现一个视频弹幕

页面布局

首先,我们需要实现页面布局,在根目录创建 index.html 布局中我们需要有一个 video 多媒体标签引入我们的本地视频,添加输入弹幕的输入框、确认发送的按钮、颜色选择器、字体大小滑动条,创建一个 style.css 来调整页面布局的样式,这里我们顺便创建一个 index.js 文件用于后续实现我们的核心逻辑,先引入到页面当中。

HTML 布局代码如下:




    
    
    


    

Canvas + WebSocket + Redis 实现视频弹幕

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;
}

布局效果如下图:

Canvas + WebSocket + Redis 实现一个视频弹幕

## 定义接口,构造假数据 我们弹幕中的弹幕数据正常情况下应该是通过与后台数据交互请求回来,所以我们需要先定义数据接口,并构造假数据来实现前端逻辑。

数据字段定义:

  • value:表示弹幕的内容(必填)
  • time:表示弹幕出现的时间(必填)
  • speed:表示弹幕移动的速度(选填)
  • color:表示弹幕文字的颜色(选填)
  • fontSize:表示弹幕的字体大小(选填)
  • opacity:表示弹幕文字的透明度(选填)

上面的 valuetime 是必填参数,其他的选填参数可以在前端设置默认值。

前端定义的假数据如下:

// index.js
let data = [
    {
        value: "这是第一条弹幕",
        speed: 2,
        time: 0,
        color: "red",
        fontSize: 20
    },
    {
        value: "这是第二条弹幕",
        time: 1
    }
];

实现前端弹幕的逻辑

我们希望是把弹幕封装成一个功能,只要有需要的地方就可以使用,从而实现复用,那么不同的地方使用这个功能通常的方式是 new 一个实例,传入当前使用该功能对应的参数,我们也使用这种方式来实现,所以我们需要封装一个统一的构造函数或者类,参数为当前的 canvas 元素、 video 元素和一个 options 对象, options 里面的 data 属性为我们的弹幕数据,之所以不直接传入 data 是为了后续参数的扩展,严格遵循开放封闭原则,这里我们就统一使用 ES6 的 class 类来实现。

1、创建弹幕功能的类及基本参数处理

布局时需要注意 Canvas 的默认宽为 300px ,高为 150px ,我们要保证 Canvas 完全覆盖整个视频,需要让 Canvas 与 video 宽高相等。 因为我们不确定每一个使用该功能的视频的宽高都是一样的,所以 Canvas 画布的宽高并没有通过 CSS 来设置,而是通过 JS 在类创建实例初始化属性的时候动态设置。

// index.js
class CanvasBarrage {
    constructor(canvas, video, optiOns= {}) {
        // 如果没有传入 canvas 或者 video 直接跳出
        if (!canvas || !video) return;
        this.canvas = canvas; // 当前的 canvas 元素
        this.video = video; // 当前的 video 元素

        // 设置 canvas 与 video 等高
        this.canvas.width = video.clientWidth;
        this.canvas.height = video.clientHeight;

        // 默认暂停播放,表示不渲染弹幕
        this.isPaused = true;

        // 没传参数的默认值
        let defaultOptiOns= {
            fontSize: 20,
            color: "gold",
            speed: 2,
            opacity: 0.3,
            data: []
        };

        // 对象的合并,将默认参数对象的属性和传入对象的属性统一放到当前实例上
        Object.assign(this, defaultOptions, options);
    }
}

应该挂在实例上的属性除了有当前的 canvas 元素、 video 元素、弹幕数据的默认选项以及弹幕数据之外,还应该有一个代表当前是否渲染弹幕的参数,因为视频暂停的时候,弹幕也是暂停的,所以没有重新渲染,因为是否暂停与弹幕是否渲染的状态是一致的,所以我们这里就用 isPaused 参数来代表当前是否暂停或重新渲染弹幕,值类型为布尔值。

2、创建构造每一条弹幕的类

我们知道,后台返回给我们的弹幕数据是一个数组,这个数组里的每一个弹幕都是一个对象,而对象上有着这条弹幕的信息,如果我们需要在每一个弹幕对象上再加一些新的信息或者在每一个弹幕对象的处理时用到了当前弹幕功能类 CanvasBarrage 实例的一些属性值,取值显然是不太方便的,这样为了后续方便扩展,遵循开放封闭原则,我们把每一个弹幕的对象转变成同一个类的实例,所以我们创建一个名为 Barrage 的类,让我们每一条弹幕的对象进入这个类里面走一遭,挂上一些扩展的属性。

// index.js
class Barrage {
    constructor(item, ctx) {
        this.value = item.value; // 弹幕的内容
        this.time = item.time; // 弹幕出现的时间
        this.item = item; // 每一个弹幕的数据对象
        this.ctx = ctx; // 弹幕功能类的执行上下文
    }
}

在我们的 CanvasBarrage 类上有一个存储弹幕数据的数组 data ,此时我们需要给 CanvasBarrage 增加一个属性用来存放 “加工” 后的每条弹幕对应的实例。

// index.js
class CanvasBarrage {
    constructor(canvas, video, optiOns= {}) {
        // 如果没有传入 canvas 或者 video 直接跳出
        if (!canvas || !video) return;
        this.canvas = canvas; // 当前的 canvas 元素
        this.video = video; // 当前的 video 元素

        // 设置 canvas 与 video 等高
        this.canvas.width = video.clientWidth;
        this.canvas.height = video.clientHeight;

        // 默认暂停播放,表示不渲染弹幕
        this.isPaused = true;

        // 没传参数的默认值
        let defaultOptiOns= {
            fontSize: 20,
            color: "gold",
            speed: 2,
            opacity: 0.3,
            data: []
        };

        // 对象的合并,将默认参数对象的属性和传入对象的属性统一放到当前实例上
        Object.assign(this, defaultOptions, options);

        // ********** 以下为新增代码 **********
        // 存放所有弹幕实例,Barrage 是创造每一条弹幕的实例的类
        this.barrages = this.data.map(item => new Barrage(item, this));
        // ********** 以上为新增代码 **********
    }
}

其实通过上面操作以后,我们相当于把 data 里面的每一条弹幕对象转换成了一个 Barrage 类的一个实例,把当前的上下文 this 传入后可以随时在每一个弹幕实例上获取 CanvasBarrage 类实例的属性,也方便我们后续扩展方法,遵循这种开放封闭原则的方式开发,意义是不言而喻的。

3、在 CanvasBarrage 类实现渲染所有弹幕的 render 方法

CanvasBarragerender 方法是在创建弹幕功能实例的时候应该渲染 Canvas 所以应该在 CanvasBarrage 中调用,在 render 内部,每一次渲染之前都应该先将 Canvas 画布清空,所以需要给当前的 CanvasBarrage 类新增一个属性用于存储 Canvas 画布的内容。

// index.js
class CanvasBarrage {
    constructor(canvas, video, optiOns= {}) {
        // 如果没有传入 canvas 或者 video 直接跳出
        if (!canvas || !video) return;
        this.canvas = canvas; // 当前的 canvas 元素
        this.video = video; // 当前的 video 元素

        // 设置 canvas 与 video 等高
        this.canvas.width = video.clientWidth;
        this.canvas.height = video.clientHeight;

        // 默认暂停播放,表示不渲染弹幕
        this.isPaused = true;

        // 没传参数的默认值
        let defaultOptiOns= {
            fontSize: 20,
            color: "gold",
            speed: 2,
            opacity: 0.3,
            data: []
        };

        // 对象的合并,将默认参数对象的属性和传入对象的属性统一放到当前实例上
        Object.assign(this, defaultOptions, options);

        // 存放所有弹幕实例,Barrage 是创造每一条弹幕的实例的类
        this.barrages = this.data.map(item => new Barrage(item, this));

        // ********** 以下为新增代码 **********
        // Canvas 画布的内容
        this.cOntext= canvas.getContext("2d");

        // 渲染所有的弹幕
        this.render();
        // ********** 以上为新增代码 **********
    }

    // ********** 以下为新增代码 **********
    render() {
        // 渲染整个弹幕
        // 第一次先进行清空操作,执行渲染弹幕,如果没有暂停,继续渲染
        this.context.clearRect(0, 0, this.canvas.width, this.canvas.height);
        // 渲染弹幕
        this.renderBarrage();
        if (this.isPaused == false) {
            // 递归渲染
            requestAnimationFrame(this.render.bind(this));
        }
    }
    // ********** 以上为新增代码 **********
}

在上面的 CanvasBarragerender 函数中,清空时由于 Canvas 性能比较好,所以将整个画布清空,所以从坐标 (0, 0) 点,清空的宽高为整个 Canvas 画布的宽高。

只要视频是在播放状态应该不断的调用 render 方法实现清空画布、渲染弹幕、判断是否暂停,如果非暂停状态继续渲染,所以我们用到了递归调用 render 去不断的实现渲染,但是递归时如果直接调用 render ,性能特别差,程序甚至会挂掉,以往这种情况我们会在递归外层加一个 setTimeout 来定义一个短暂的递归时间,但是这个过程类似于动画效果,如果使用 setTimeout 其实是将同步代码转成了异步执行,会增加不确定性导致画面出现卡顿的现象。

这里我们使用 H5 的新 API requestAnimationFrame ,可以在平均 1/60 S 内帮我执行一次该方法传入的回调,我们直接把 render 函数作为回调函数传入 requestAnimationFrame ,该方法是按照帧的方式执行,动画流畅,需要注意的是, render 函数内使用了 this ,所以应该处理一下 this 指向问题。

由于我们使用面向对象的方式,所以渲染弹幕的具体细节,我们抽离出一个单独的方法 renderBarrage ,接下来看一下 renderBarrage 的实现。

4、CanvasBarrage 类 render 内部 renderBarrage 的实现

// index.js
class CanvasBarrage {
    constructor(canvas, video, optiOns= {}) {
        // 如果没有传入 canvas 或者 video 直接跳出
        if (!canvas || !video) return;
        this.canvas = canvas; // 当前的 canvas 元素
        this.video = video; // 当前的 video 元素

        // 设置 canvas 与 video 等高
        this.canvas.width = video.clientWidth;
        this.canvas.height = video.clientHeight;

        // 默认暂停播放,表示不渲染弹幕
        this.isPaused = true;

        // 没传参数的默认值
        let defaultOptiOns= {
            fontSize: 20,
            color: "gold",
            speed: 2,
            opacity: 0.3,
            data: []
        };

        // 对象的合并,将默认参数对象的属性和传入对象的属性统一放到当前实例上
        Object.assign(this, defaultOptions, options);

        // 存放所有弹幕实例,Barrage 是创造每一条弹幕的实例的类
        this.barrages = this.data.map(item => new Barrage(item, this));

        // Canvas 画布的内容
        this.cOntext= canvas.getContext("2d");

        // 渲染所有的弹幕
        this.render();
    }
    render() {
        // 渲染整个弹幕
        // 第一次先进行清空操作,执行渲染弹幕,如果没有暂停,继续渲染
        this.context.clearRect(0, 0, this.canvas.width, this.canvas.height);
        // 渲染弹幕
        this.renderBarrage();
        if (this.isPaused == false) {
            // 递归渲染
            requestAnimationFrame(this.render.bind(this));
        }
    }

    // ********** 以下为新增代码 **********
    renderBarrage() {
        // 将数组的弹幕一个一个取出,判断时间和视频的时间是否符合,符合就执行渲染此弹幕
        let time = this.video.currentTime;
        this.barrages.forEach(barrage => {
            // 当视频时间大于等于了弹幕设置的时间,那么开始渲染(时间都是以秒为单位)
            if (time >= barrage.time) {
                // 初始化弹幕的各个参数,只有在弹幕将要出现的时候再去初始化,节省性能,初始化后再进行绘制
                // 如果没有初始化,先去初始化一下
                if (!barrage.isInited) {
                    // 初始化后下次再渲染就不需要再初始化了,所以创建一个标识 isInited
                    barrage.init();
                    barrage.isInited = true;
                }
            }
        });
    }
    // ********** 以上为新增代码 **********
}

此处的 renderBarrage 方法内部主要对每一条弹幕实例所设置的出现时间和视频的播放时间做对比,如果视频的播放时间大于等于了弹幕出现的时间,说明弹幕需要绘制在 Canvas 画布内。

之前我们的每一条弹幕实例的属性可能不全,弹幕的其他未传参数并没有初始化,所以为了最大限度的节省性能,我们在弹幕该第一次绘制的时候去初始化参数,等到视频播放的时间变化再去重新绘制时,不再初始化参数,所以初始化参数的方法放在了判断弹幕出现时间的条件里面执行,又设置了代表弹幕实例是不是初始化了的参数 isInited ,初始化函数 init 执行过一次后,马上修改 isInited 的值,保证只初始化参数一次。

renderBarrage 方法中我们可以看出来,其实我们是循环了专门存放每一条弹幕实例( Barrage 类的实例)的数组,我们在内部用实例去调用的方法 init 应该是在 Barrage 类的原型上,下面我们去 Barrage 类上实现 init 的逻辑。

5、Barrage 类 init 的实现

// index.js
class Barrage {
    constructor(item, ctx) {
        this.value = item.value; // 弹幕的内容
        this.time = item.time; // 弹幕出现的时间
        this.item = item; // 每一个弹幕的数据对象
        this.ctx = ctx; // 弹幕功能类的执行上下文
    }

    // ********** 以下为新增代码 **********
    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;

        // 求自己的宽度,目的是用来校验当前是否还要继续绘制(边界判断)
        let span = document.createElement("span");

        // 能决定宽度的只有弹幕的内容和文字的大小,和字体,字体默认为微软雅黑,我们就不做设置了
        span.innerText = this.value;
        span.style.fOnt= this.fontSize + 'px "Microsoft YaHei';

        // span 为行内元素,取不到宽度,所以我们通过定位给转换成块级元素
        span.style.position = "absolute";

        document.body.appendChild(span); // 放入页面
        this.width = span.clientWidth; // 记录弹幕的宽度
        document.body.removeChild(span); // 从页面移除

        // 存储弹幕出现的横纵坐标
        this.x = this.ctx.canvas.width;
        this.y = this.ctx.canvas.height;

        // 处理弹幕纵向溢出的边界处理
        if (this.y  this.ctx.canvas.height - this.fontSize) {
            this.y = this.ctx.canvas.height - this.fontSize;
        }
    }
    // ********** 以上为新增代码 **********
}

在上面代码的 init 方法中我们其实可以看出,每条弹幕实例初始化的时候初始的信息除了之前说的弹幕的基本参数外,还获取了每条弹幕的宽度(用于后续做弹幕是否已经完全移出屏幕的边界判断)和每一条弹幕的 xy 轴方向的坐标并为了防止弹幕在 y 轴显示不全做了边界处理。

6、实现每条弹幕的渲染和弹幕移除屏幕的处理

我们当时在 CanvasBarrage 类的 render 方法中的渲染每个弹幕的方法 renderBarrage 中(原谅这么啰嗦,因为到现在内容已经比较多,说的具体一点方便知道是哪个步骤,哈哈)只做了对每一条弹幕实例的初始化操作,并没有渲染在 Canvas 画布中,这时我们主要做两部操作,实现每条弹幕渲染在画布中和左侧移出屏幕不再渲染的边界处理。

// index.js
class CanvasBarrage {
    constructor(canvas, video, optiOns= {}) {
        // 如果没有传入 canvas 或者 video 直接跳出
        if (!canvas || !video) return;
        this.canvas = canvas; // 当前的 canvas 元素
        this.video = video; // 当前的 video 元素

        // 设置 canvas 与 video 等高
        this.canvas.width = video.clientWidth;
        this.canvas.height = video.clientHeight;

        // 默认暂停播放,表示不渲染弹幕
        this.isPaused = true;

        // 没传参数的默认值
        let defaultOptiOns= {
            fontSize: 20,
            color: "gold",
            speed: 2,
            opacity: 0.3,
            data: []
        };

        // 对象的合并,将默认参数对象的属性和传入对象的属性统一放到当前实例上
        Object.assign(this, defaultOptions, options);

        // 存放所有弹幕实例,Barrage 是创造每一条弹幕的实例的类
        this.barrages = this.data.map(item => new Barrage(item, this));

        // Canvas 画布的内容
        this.cOntext= canvas.getContext("2d");

        // 渲染所有的弹幕
        this.render();
    }
    render() {
        // 渲染整个弹幕
        // 第一次先进行清空操作,执行渲染弹幕,如果没有暂停,继续渲染
        this.context.clearRect(0, 0, this.canvas.width, this.canvas.height);
        // 渲染弹幕
        this.renderBarrage();
        if (this.isPaused == false) {
            // 递归渲染
            requestAnimationFrame(this.render.bind(this));
        }
    }
    renderBarrage() {
        // 将数组的弹幕一个一个取出,判断时间和视频的时间是否符合,符合就执行渲染此弹幕
        let time = this.video.currentTime;
        this.barrages.forEach(barrage => {
            // ********** 以下为改动的代码 **********
            // 当视频时间大于等于了弹幕设置的时间,那么开始渲染(时间都是以秒为单位)
            if (!barrage.flag && time >= barrage.time) {
                // ********** 以上为改动的代码 **********

                // 初始化弹幕的各个参数,只有在弹幕将要出现的时候再去初始化,节省性能,初始化后再进行绘制
                // 如果没有初始化,先去初始化一下
                if (!barrage.isInited) {
                    // 初始化后下次再渲染就不需要再初始化了,所以创建一个标识 isInited
                    barrage.init();
                    barrage.isInited = true;
                }

                // ********** 以下为新增代码 **********
                barrage.x -= barrage.speed;
                barrage.render(); // 渲染该条弹幕
                if (barrage.x  
 

每个弹幕实例都有一个 speed 属性,该属性代表着弹幕移动的速度,换个说法其实就是每次减少的 x 轴的差值,所以我们其实是通过改变 x 轴的值再重新渲染而实现弹幕的左移,我们创建了一个标识 flag 挂在每个弹幕实例下,代表是否已经离开屏幕,如果离开则更改 flag 的值,使外层的 CanvasBarrage 类的 render 函数再次递归时不进入渲染程序。

每一条弹幕具体是怎么渲染的,通过代码可以看出每个弹幕实例在 x 坐标改变后都调用了实例方法 render 函数,注意此 render 非彼 render ,该 render 函数属于 Barrage 类,目的是为了渲染每一条弹幕,而 CanvasBarrage 类下的 render ,是为了在视频时间变化时清空并重新渲染整个 Canvas 画布。

7、Barrage 类下的 render 方法的实现

// index.js
class Barrage {
    constructor(item, ctx) {
        this.value = item.value; // 弹幕的内容
        this.time = item.time; // 弹幕出现的时间
        this.item = item; // 每一个弹幕的数据对象
        this.ctx = ctx; // 弹幕功能类的执行上下文
    }
    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;

        // 求自己的宽度,目的是用来校验当前是否还要继续绘制(边界判断)
        let span = document.createElement("span");

        // 能决定宽度的只有弹幕的内容和文字的大小,和字体,字体默认为微软雅黑,我们就不做设置了
        span.innerText = this.value;
        span.style.fOnt= this.fontSize + 'px "Microsoft YaHei';

        // span 为行内元素,取不到宽度,所以我们通过定位给转换成块级元素
        span.style.position = "absolute";

        document.body.appendChild(span); // 放入页面
        this.width = span.clientWidth; // 记录弹幕的宽度
        document.body.removeChild(span); // 从页面移除

        // 存储弹幕出现的横纵坐标
        this.x = this.ctx.canvas.width;
        this.y = this.ctx.canvas.height;

        // 处理弹幕纵向溢出的边界处理
        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);
    }
    // ********** 以上为新增代码 **********
}

从上面新增代码我们可以看出,其实 Barrage 类的 render 方法只是将每一条弹幕的字号、颜色、内容、坐标等属性通过 Canvas 的 API 添加到了画布上。

8、实现播放、暂停事件

还记得我们的 CanvasBarrage 类里面有一个属性 isPaused ,属性值控制了我们是否递归渲染,这个属性与视频暂停的状态是一致的,我们在播放的时候,弹幕不断的清空并重新绘制,当暂停的时候弹幕也应该跟着暂停,说白了就是不在调用 CanvasBarrage 类的 render 方法,其实就是在暂停、播放的过程中不断的改变 isPaused 的值即可。

还记得我们之前构造的两条假数据 data 吧,接下来我们添加播放、暂停事件,来尝试使用一下我们的弹幕功能。

// index.js
// 实现一个简易选择器,方便获取元素,后面获取元素直接调用 $
let $ = document.querySelector.bind(document);

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

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

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

// 添加暂停事件
// index.js
video.addEventListener("pause", function() {
    canvasBarrage.isPaused = true;
});

9、实现发送弹幕事件

// index.js
$("#add").addEventListener("click", function() {
    let time = video.currentTime; // 发送弹幕的时间
    let value = $("#text").value; // 发送弹幕的文字
    let color = $("#color").value; // 发送弹幕文字的颜色
    let fOntSize= $("#range").value; // 发送弹幕的字体大小
    let sendObj = { time, value, color, fontSize }; //发送弹幕的参数集合
    canvasBarrage.add(sendObj); // 发送弹幕的方法
});

其实我们发送弹幕时,就是向 CanvasBarrage 类的 barrages 数组里添加了一条弹幕的实例,我们单独封装了一个 add 的实例方法。

// index.js
class CanvasBarrage {
    constructor(canvas, video, optiOns= {}) {
        // 如果没有传入 canvas 或者 video 直接跳出
        if (!canvas || !video) return;
        this.canvas = canvas; // 当前的 canvas 元素
        this.video = video; // 当前的 video 元素

        // 设置 canvas 与 video 等高
        this.canvas.width = video.clientWidth;
        this.canvas.height = video.clientHeight;

        // 默认暂停播放,表示不渲染弹幕
        this.isPaused = true;

        // 没传参数的默认值
        let defaultOptiOns= {
            fontSize: 20,
            color: "gold",
            speed: 2,
            opacity: 0.3,
            data: []
        };

        // 对象的合并,将默认参数对象的属性和传入对象的属性统一放到当前实例上
        Object.assign(this, defaultOptions, options);

        // 存放所有弹幕实例,Barrage 是创造每一条弹幕的实例的类
        this.barrages = this.data.map(item => new Barrage(item, this));

        // Canvas 画布的内容
        this.cOntext= canvas.getContext("2d");

        // 渲染所有的弹幕
        this.render();
    }
    render() {
        // 渲染整个弹幕
        // 第一次先进行清空操作,执行渲染弹幕,如果没有暂停,继续渲染
        this.context.clearRect(0, 0, this.canvas.width, this.canvas.height);
        // 渲染弹幕
        this.renderBarrage();
        if (this.isPaused == false) {
            // 递归渲染
            requestAnimationFrame(this.render.bind(this));
        }
    }
    renderBarrage() {
        // 将数组的弹幕一个一个取出,判断时间和视频的时间是否符合,符合就执行渲染此弹幕
        let time = this.video.currentTime;
        this.barrages.forEach(barrage => {
            // 当视频时间大于等于了弹幕设置的时间,那么开始渲染(时间都是以秒为单位)
            if (!barrage.flag && time >= barrage.time) {
                // 初始化弹幕的各个参数,只有在弹幕将要出现的时候再去初始化,节省性能,初始化后再进行绘制
                // 如果没有初始化,先去初始化一下
                if (!barrage.isInited) {
                    // 初始化后下次再渲染就不需要再初始化了,所以创建一个标识 isInited
                    barrage.init();
                    barrage.isInited = true;
                }

                barrage.x -= barrage.speed;
                barrage.render(); // 渲染该条弹幕
                if (barrage.x  
 

10、拖动进度条实现弹幕的前进和后退

其实我们发现,弹幕虽然实现了正常的播放、暂停以及发送,但是当我们拖动进度条的时候弹幕应该是跟着视频时间同步播放的,现在的弹幕一旦播放过无论怎样拉动进度条弹幕都不会再出现,我们现在就来解决这个问题。

// index.js
// 拖动进度条事件
video.addEventListener("seeked", function() {
    canvasBarrage.reset();
});

我们在事件内部其实只是调用了一下 CanvasBarrage 类的 reset 方法,这个方法就是在拖动进度条的时候来帮我们初始化弹幕的状态。

// index.js
class CanvasBarrage {
    constructor(canvas, video, optiOns= {}) {
        // 如果没有传入 canvas 或者 video 直接跳出
        if (!canvas || !video) return;
        this.canvas = canvas; // 当前的 canvas 元素
        this.video = video; // 当前的 video 元素

        // 设置 canvas 与 video 等高
        this.canvas.width = video.clientWidth;
        this.canvas.height = video.clientHeight;

        // 默认暂停播放,表示不渲染弹幕
        this.isPaused = true;

        // 没传参数的默认值
        let defaultOptiOns= {
            fontSize: 20,
            color: "gold",
            speed: 2,
            opacity: 0.3,
            data: []
        };

        // 对象的合并,将默认参数对象的属性和传入对象的属性统一放到当前实例上
        Object.assign(this, defaultOptions, options);

        // 存放所有弹幕实例,Barrage 是创造每一条弹幕的实例的类
        this.barrages = this.data.map(item => new Barrage(item, this));

        // Canvas 画布的内容
        this.cOntext= canvas.getContext("2d");

        // 渲染所有的弹幕
        this.render();
    }
    render() {
        // 渲染整个弹幕
        // 第一次先进行清空操作,执行渲染弹幕,如果没有暂停,继续渲染
        this.context.clearRect(0, 0, this.canvas.width, this.canvas.height);
        // 渲染弹幕
        this.renderBarrage();
        if (this.isPaused == false) {
            // 递归渲染
            requestAnimationFrame(this.render.bind(this));
        }
    }
    renderBarrage() {
        // 将数组的弹幕一个一个取出,判断时间和视频的时间是否符合,符合就执行渲染此弹幕
        let time = this.video.currentTime;
        this.barrages.forEach(barrage => {
            // 当视频时间大于等于了弹幕设置的时间,那么开始渲染(时间都是以秒为单位)
            if (!barrage.flag && time >= barrage.time) {
                // 初始化弹幕的各个参数,只有在弹幕将要出现的时候再去初始化,节省性能,初始化后再进行绘制
                // 如果没有初始化,先去初始化一下
                if (!barrage.isInited) {
                    // 初始化后下次再渲染就不需要再初始化了,所以创建一个标识 isInited
                    barrage.init();
                    barrage.isInited = true;
                }

                barrage.x -= barrage.speed;
                barrage.render(); // 渲染该条弹幕
                if (barrage.x  {
            // 更改已经移出屏幕的弹幕状态
            barrage.flag = false;
            // 当拖动到的时间小于等于当前弹幕时间是,重新初始化弹幕的数据,实现渲染
            if (time <= barrage.time) {
                barrage.isInited = false;
            } else {
                barrage.flag = true; // 否则将弹幕的状态设置为以移出屏幕
            }
        });
    }
    // ********** 以上为新增代码 **********
}

其实 reset 方法中值做了几件事:

  • 清空 Canvas 画布;
  • 获取当前进度条拖动位置的时间;
  • 循环存储弹幕实例的数组;
  • 将所有弹幕更改为未移出屏幕;
  • 判断拖动时间和每条弹幕的时间;
  • 在当前时间以后的弹幕重新初始化数据;
  • 以前的弹幕更改为已移出屏幕。

从而实现了拖动进度条弹幕的 “前进” 和 “后退” 功能。

使用 WebSocket 和 Redis 实现前后端通信及数据存储

1、服务器代码的实现

要使用 WebSocket 和 Redis 首先需要去安装 wsredis 依赖,在项目根目录执行下面命令:

npm install ws redis

我们创建一个 server.js 文件,用来写服务端的代码:

// server.js
let WebSocket = require("ws"); // 引入 WebSocket
let redis = require("redis"); // 引入 redis

// 初始化 WebSocket 服务器,端口号为 3000
let wss = new WebSocket.Server({
    port: 3000
});

// 创建 redis 客户端
let client = redis.createClient(); // key value

// 原生的 websocket 就两个常用的方法 on('message')、on('send')
wss.on("connection", function(ws) {
    // 监听连接
    // 连接上需要立即把 redis 数据库的数据取出返回给前端
    client.lrange("barrages", 0, -1, function(err, applies) {
        // 由于 redis 的数据都是字符串,所以需要把数组中每一项转成对象
        applies = applies.map(item => JSON.parse(item));

        // 使用 websocket 服务器将 redis 数据库的数据发送给前端
        // 构建一个对象,加入 type 属性告诉前端当前返回数据的行为,并将数据转换成字符串
        ws.send(
            JSON.stringify({
                type: "INIT",
                data: applies
            })
        );
    });

    // 当服务器收到消息时,将数据存入 redis 数据库
    ws.on("message", function(data) {
        // 向数据库存储时存的是字符串,存入并打印数据,用来判断是否成功存入数据库
        client.rpush("barrages", data, redis.print);

        // 再将当前这条数据返回给前端,同样添加 type 字段告诉前端当前行为,并将数据转换成字符串
        ws.send(
            JSON.stringify({
                type: "ADD",
                data: JSON.parse(data)
            })
        );
    });
});

服务器的逻辑很清晰,在 WebSocket 连接上时,立即获取 Redis 数据库的所有弹幕数据返回给前端,当前端点击发送弹幕按钮发送数据时,接收数据存入 Redis 数据库中并打印验证数据是否成功存入,再通过 WebSocket 服务把当前这一条数返回给前端,需要注意一下几点:

JSON.parse
JSON.stringify
type

2、前端代码的修改

在没有实现后端代码之前,前端使用的是 data 的假数据,是在添加弹幕事件中,将获取的新增弹幕信息通过 CanvasBarrage 类的 add 方法直接创建 Barrage 类的实例,并加入到存放弹幕实例的 barrages 数组中。

现在我们需要更正一下交互逻辑,在发送弹幕事件触发时,我们应该先将获取的单条弹幕数据通过 WebSocket 发送给后端服务器,在服务器重新将消息返还给我们的时候,去将这条数据通过 CanvasBarrage 类的 add 方法加入到存放弹幕实例的 barrages 数组中。

还有在页面初始化时,我们之前在创建 CanvasBarrage 类实例的时候直接传入了 data 假数据,现在需要通过 WebSocket 的连接事件,在监听到连接 WebSocket 服务时,去创建 CanvasBarrage 类的实例,并直接把服务端返回 Redis 数据库真实的数据作为参数传入,前端代码修改如下:

// index.js
// ********** 下面代码被删掉了 **********
// let canvasBarrage = new CanvasBarrage(canvas, video, {
//     data
// });
// ********** 上面代码被删掉了 **********

// ********** 以下为新增代码 **********
let canvasBarrage;

// 创建 WebSocket 连接
let socket = new WebSocket("ws://localhost:3000");

// 监听连接事件
socket.Onopen= function() {
    // 监听消息
    socket.Onmessage= function(e) {
        // 将收到的消息从字符串转换成对象
        let message = JSON.parse(e.data);

        // 根据不同情况判断是初始化还是发送弹幕
        if (message.type === "INIT") {
            // 创建 CanvasBarrage 的实例添加弹幕功能,传入真实的数据
            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; // 发送弹幕的时间
    let value = $("#text").value; // 发送弹幕的文字
    let color = $("#color").value; // 发送弹幕文字的颜色
    let fOntSize= $("#range").value; // 发送弹幕的字体大小
    let sendObj = { time, value, color, fontSize }; //发送弹幕的参数集合

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

    // ********** 下面代码被删掉了 **********
    // canvasBarrage.add(sendObj); // 发送弹幕的方法
    // ********** 上面代码被删掉了 **********
});

现在我们可以打开 index.html 文件并启动 server.js 服务器,就可以实现真实的视频弹幕操作了,但是我们还是差了最后一步,当前的服务只能同时服务一个人,但真实的场景是同时看视频的有很多人,而且发送的弹幕是共享的。

3、实现多端通信、弹幕共享

我们需要处理两件事情:

  • 第一件事情是实现多端通信共享数据库信息;
  • 第二件事情是当有人离开的时候清除关闭的 WebSocket 对象。
// server.js
let WebSocket = require("ws"); // 引入 WebSocket
let redis = require("redis"); // 引入 redis

// 初始化 WebSocket 服务器,端口号为 3000
let wss = new WebSocket.Server({
    port: 3000
});

// 创建 redis 客户端
let client = redis.createClient(); // key value

// ********** 以下为新增代码 **********
// 存储所有 WebSocket 用户
let clientsArr = [];
// ********** 以上为新增代码 **********

// 原生的 websocket 就两个常用的方法 on('message')、on('send')
wss.on("connection", function(ws) {
    // ********** 以下为新增代码 **********
    // 将所有通过 WebSocket 连接的用户存入数组中
    clientsArr.push(ws);
    // ********** 以上为新增代码 **********

    // 监听连接
    // 连接上需要立即把 redis 数据库的数据取出返回给前端
    client.lrange("barrages", 0, -1, function(err, applies) {
        // 由于 redis 的数据都是字符串,所以需要把数组中每一项转成对象
        applies = applies.map(item => JSON.parse(item));

        // 使用 websocket 服务器将 redis 数据库的数据发送给前端
        // 构建一个对象,加入 type 属性告诉前端当前返回数据的行为,并将数据转换成字符串
        ws.send(
            JSON.stringify({
                type: "INIT",
                data: applies
            })
        );
    });

    // 当服务器收到消息时,将数据存入 redis 数据库
    ws.on("message", function(data) {
        // 向数据库存储时存的是字符串,存入并打印数据,用来判断是否成功存入数据库
        client.rpush("barrages", data, redis.print);

        // ********** 以下为修改后的代码 **********
        // 循环数组,将某一个人新发送的弹幕在存储到 Redis 之后返回给所有用户
        clientsArr.forEach(w => {
            // 再将当前这条数据返回给前端,同样添加 type 字段告诉前端当前行为,并将数据转换成字符串
            w.send(
                JSON.stringify({
                    type: "ADD",
                    data: JSON.parse(data)
                })
            );
        });
        // ********** 以上为修改后的代码 **********
    });

    // ********** 以下为新增代码 **********
    // 监听关闭连接事件
    ws.on("close", function() {
        // 当某一个人关闭连接离开时,将这个人从当前存储用户的数组中移除
        clientsArr = clientsArr.filter(client => client != ws);
    });
    // ********** 以上为新增代码 **********
});

上面就是 Canvas + WebSocket + Redis 视频弹幕的实现,实现过程可能有些复杂,但整个过程写的还是比较详细,可能需要一定的耐心慢慢的读完,并最好一步一步跟着写一写,希望这篇文章可以让读到的人解决视频弹幕类似的需求,真正理解整个过程和开放封闭原则,认识到前端面向对象编程思想的美。


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 我们


推荐阅读
  • Webpack5内置处理图片资源的配置方法
    本文介绍了在Webpack5中处理图片资源的配置方法。在Webpack4中,我们需要使用file-loader和url-loader来处理图片资源,但是在Webpack5中,这两个Loader的功能已经被内置到Webpack中,我们只需要简单配置即可实现图片资源的处理。本文还介绍了一些常用的配置方法,如匹配不同类型的图片文件、设置输出路径等。通过本文的学习,读者可以快速掌握Webpack5处理图片资源的方法。 ... [详细]
  • android listview OnItemClickListener失效原因
    最近在做listview时发现OnItemClickListener失效的问题,经过查找发现是因为button的原因。不仅listitem中存在button会影响OnItemClickListener事件的失效,还会导致单击后listview每个item的背景改变,使得item中的所有有关焦点的事件都失效。本文给出了一个范例来说明这种情况,并提供了解决方法。 ... [详细]
  • 在说Hibernate映射前,我们先来了解下对象关系映射ORM。ORM的实现思想就是将关系数据库中表的数据映射成对象,以对象的形式展现。这样开发人员就可以把对数据库的操作转化为对 ... [详细]
  • Voicewo在线语音识别转换jQuery插件的特点和示例
    本文介绍了一款名为Voicewo的在线语音识别转换jQuery插件,该插件具有快速、架构、风格、扩展和兼容等特点,适合在互联网应用中使用。同时还提供了一个快速示例供开发人员参考。 ... [详细]
  • 1,关于死锁的理解死锁,我们可以简单的理解为是两个线程同时使用同一资源,两个线程又得不到相应的资源而造成永无相互等待的情况。 2,模拟死锁背景介绍:我们创建一个朋友 ... [详细]
  • 后台获取视图对应的字符串
    1.帮助类后台获取视图对应的字符串publicclassViewHelper{将View输出为字符串(注:不会执行对应的ac ... [详细]
  • 《数据结构》学习笔记3——串匹配算法性能评估
    本文主要讨论串匹配算法的性能评估,包括模式匹配、字符种类数量、算法复杂度等内容。通过借助C++中的头文件和库,可以实现对串的匹配操作。其中蛮力算法的复杂度为O(m*n),通过随机取出长度为m的子串作为模式P,在文本T中进行匹配,统计平均复杂度。对于成功和失败的匹配分别进行测试,分析其平均复杂度。详情请参考相关学习资源。 ... [详细]
  • 本文介绍了南邮ctf-web的writeup,包括签到题和md5 collision。在CTF比赛和渗透测试中,可以通过查看源代码、代码注释、页面隐藏元素、超链接和HTTP响应头部来寻找flag或提示信息。利用PHP弱类型,可以发现md5('QNKCDZO')='0e830400451993494058024219903391'和md5('240610708')='0e462097431906509019562988736854'。 ... [详细]
  • 本文介绍了解决IE678伪类不兼容问题的方法,包括少用CSS3和HTML5独有的属性,使用CSS hacker,使用last-child清除浮动、批量添加标签、去掉list item最后一个的border-right等技巧。同时还介绍了使用after清除浮动时加上IE独有属性zoom:1的处理方法。另外,本文还提到可以使用jQuery代替批量添加标签的功能,以及使用负边距和CSS2选择器element+element去掉list item最后一个的border-right的方法。 ... [详细]
  • 深入理解CSS中的margin属性及其应用场景
    本文主要介绍了CSS中的margin属性及其应用场景,包括垂直外边距合并、padding的使用时机、行内替换元素与费替换元素的区别、margin的基线、盒子的物理大小、显示大小、逻辑大小等知识点。通过深入理解这些概念,读者可以更好地掌握margin的用法和原理。同时,文中提供了一些相关的文档和规范供读者参考。 ... [详细]
  • CSS3选择器的使用方法详解,提高Web开发效率和精准度
    本文详细介绍了CSS3新增的选择器方法,包括属性选择器的使用。通过CSS3选择器,可以提高Web开发的效率和精准度,使得查找元素更加方便和快捷。同时,本文还对属性选择器的各种用法进行了详细解释,并给出了相应的代码示例。通过学习本文,读者可以更好地掌握CSS3选择器的使用方法,提升自己的Web开发能力。 ... [详细]
  • 本文讨论了Alink回归预测的不完善问题,指出目前主要针对Python做案例,对其他语言支持不足。同时介绍了pom.xml文件的基本结构和使用方法,以及Maven的相关知识。最后,对Alink回归预测的未来发展提出了期待。 ... [详细]
  • 本文介绍了C#中数据集DataSet对象的使用及相关方法详解,包括DataSet对象的概述、与数据关系对象的互联、Rows集合和Columns集合的组成,以及DataSet对象常用的方法之一——Merge方法的使用。通过本文的阅读,读者可以了解到DataSet对象在C#中的重要性和使用方法。 ... [详细]
  • XML介绍与使用的概述及标签规则
    本文介绍了XML的基本概念和用途,包括XML的可扩展性和标签的自定义特性。同时还详细解释了XML标签的规则,包括标签的尖括号和合法标识符的组成,标签必须成对出现的原则以及特殊标签的使用方法。通过本文的阅读,读者可以对XML的基本知识有一个全面的了解。 ... [详细]
  • 前景:当UI一个查询条件为多项选择,或录入多个条件的时候,比如查询所有名称里面包含以下动态条件,需要模糊查询里面每一项时比如是这样一个数组条件:newstring[]{兴业银行, ... [详细]
author-avatar
tianziqizhi
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有