Web前端最全Canvas + WebSocket实现视频弹幕(2),校园Web前端面试题

最后

喜欢的话别忘了关注、点赞哦~

开源分享:【大厂前端面试题解析+核心总结学习笔记+真实项目实战+最新讲解视频】

前端校招面试题精编解析大全

// 设置 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));

}

}

// ********** 以上为新增代码 **********

}

在上面的 CanvasBarrage 的 render 函数中,清空时由于 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.fontSize) {

this.y = this.fontSize;

}

if (this.y > this.ctx.canvas.height - this.fontSize) {

this.y = this.ctx.canvas.height - this.fontSize;

}

}

// ********** 以上为新增代码 **********

}

在上面代码的 init 方法中我们其实可以看出,每条弹幕实例初始化的时候初始的信息除了之前说的弹幕的基本参数外,还获取了每条弹幕的宽度(用于后续做弹幕是否已经完全移出屏幕的边界判断)和每一条弹幕的 x 和 y 轴方向的坐标并为了防止弹幕在 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 < barrage.width * -1) {

barrage.flag = true; // 是否出去了,出去了,做停止渲染的操作

}

// ********** 以上为新增代码 **********

}

});

}

}

每个弹幕实例都有一个 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.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);

}

// ********** 以上为新增代码 **********

}

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

8、实现播放、暂停事件

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

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

// 文件:index.js

// 实现一个简易选择器,方便获取元素,后面获取元素直接调用 $

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、实现发送弹幕事件

// 文件: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 < barrage.width * -1) {

barrage.flag = true; // 是否出去了,出去了,做停止渲染的操作

}

}

});

}

// ********** 以下为新增代码 **********

add(item) {

this.barrages.push(new Barrage(item, this));

}

// ********** 以上为新增代码 **********

}

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

// 循环每一条弹幕实例

this.barrages.forEach(barrage => {

// 更改已经移出屏幕的弹幕状态

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 文件,用来写服务端的代码:

// 文件:index.js

const WebSocket = require(“ws”); // 引入 WebSocket

const redis = require(“redis”); // 引入 redis

// 初始化 WebSocket 服务器,端口号为 3000

最后

四轮技术面+一轮hr面结束,学习到了不少,面试也是一个学习检测自己的过程,面试前大概复习了 一周的时间,把以前的代码看了一下,字节跳动比较注重算法,面试前刷了下leetcode和剑指offer, 也刷了些在牛客网上的面经。大概就说这些了,写代码去了~

祝大家都能收获大厂offer~

开源分享:【大厂前端面试题解析+核心总结学习笔记+真实项目实战+最新讲解视频】

篇幅有限,仅展示部分内容

  • 5
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值