阅读原文
页面布局
首先,我们需要实现页面布局,在根目录创建 index.html
布局中我们需要有一个 video
多媒体标签引入我们的本地视频,添加输入弹幕的输入框、确认发送的按钮、颜色选择器、字体大小滑动条,创建一个 style.css
来调整页面布局的样式,这里我们顺便创建一个 index.js
文件用于后续实现我们的核心逻辑,先引入到页面当中。
HTML 布局代码如下:
<!-- 文件:index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<link rel="stylesheet" href="style.css">
<title>视频弹幕</title>
</head>
<body>
<div id="cantainer">
<h2>Canvas + WebSocket + Redis 实现视频弹幕</h2>
<div id="content">
<canvas id="canvas"></canvas>
<video id="video" src="./barrage.mp4" controls></video>
</div>
<!-- 输入弹幕内容 -->
<input type="text" id="text">
<!-- 添加弹幕按钮 -->
<button id="add">发送</button>
<!-- 选择文字颜色 -->
<input type="color" id="color">
<!-- 调整字体大小 -->
<input type="range" max="40" min="20" id="range">
</div>
<script src="./index.js"></script>
</body>
</html>
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;
}
布局效果如下图:
定义接口,构造假数据
我们弹幕中的弹幕数据正常情况下应该是通过与后台数据交互请求回来,所以我们需要先定义数据接口,并构造假数据来实现前端逻辑。
数据字段定义:
- value:表示弹幕的内容(必填)
- time:表示弹幕出现的时间(必填)
- speed:表示弹幕移动的速度(选填)
- color:表示弹幕文字的颜色(选填)
- fontSize:表示弹幕的字体大小(选填)
- opacity:表示弹幕文字的透明度(选填)
上面的 value
和 time
是必填参数,其他的选填参数可以在前端设置默认值。
前端定义的假数据如下:
// 文件: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 方法
CanvasBarrage
的 render
方法是在创建弹幕功能实例的时候应该渲染 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 =