天下视频唯弹幕不破


说起弹幕看过视频的都不会陌生,那满屏充满着飘逸评论的效果,让人如痴如醉,无法自拔

最近也是因为在学习关于canvas的知识,所以今天就想和大家分享一个关于弹幕的故事

那么究竟弹幕是怎样炼成的呢? 我们且往下看(look)

什么?看效果

效果图已经呈现给各位了,那么是不是有点小激动呢?是的,感慨万分,思绪宁乱,无语凝噎

无论以后我们的工作中是否会遇到这样的需求,也请给自己一个增加技能的机会吧!!!

本次弹幕的效果,项目结构如下图所示

项目整体已经给出,那么我们就撸起袖子加油干吧。

让弹幕飞


上面我们提到了canvas的事情,所以呢,这就是制作弹幕的杀手锏了。我们利用canvas绘图来实现弹幕的功能

首先,我们先给出html的结构

// index.html文件
<div class="wrap"><h1>听妈妈的话 - 周杰伦</h1><div class="main"><canvas id="canvas"></canvas><video src="../source/mv.mp4" id="video" controls width="720" height="480"></video></div><div class="content"><input type="text" id="text"><input type="button" value="发弹幕" id="btn"><input type="color" id="color"><input type="range" id="range" max="40" min="20"></div>
</div>
// 引入index.js文件用来实现弹幕功能
<script src="./index.js"></script>

如需要视频资源的,就点这里吧(提取码:tsei)

结构相对来说没什么高级的内容,主要就是写上了canvas标签还有video标签,他们才是视频网站中弹幕的绝佳拍档

那么不再卖关子了,赶紧进行主要活动吧

模拟数据

// index.js文件
let data = [{value: '周杰伦的听妈妈的话,让我反复循环再循环', time: 5, color: 'red', speed: 1, fontSize: 22},{value: '想快快长大,才能保护她', time: 10, color: '#00a1f5', speed: 1, fontSize: 30},{value: '听妈妈的话吧,晚点再恋爱吧!爱呢?', time: 15},
];

数据里代表了什么:

  • value:代表弹幕的内容 (必填)

  • time:代表弹幕展现的时间 (必填)

  • color:代表弹幕文字的颜色

  • speed:代表弹幕飘过的速度

  • fontSize:代表弹幕文字的大小

  • opacity:代表弹幕文字的透明度

除了弹幕的内容和展现的时间外,其他都是可选的,模拟的数据里没有这些参数也没关系的

获取dom元素

// index.js文件
// 模拟数据
...省略// 获取到所有需要的dom元素
let doc = document;
let canvas = doc.getElementById('canvas');
let video = doc.getElementById('video');
let $txt = doc.getElementById('text');
let $btn = doc.getElementById('btn');
let $color = doc.getElementById('color');
let $range = doc.getElementById('range');

Canvas渲染弹幕

下面我们将用面向对象的方式来实现canvas绘制弹幕的功能,之所以选择用这种方式主要是方便复用和后续添加方法

下面我们先来创建一个CanvasBarrage类,主要用做canvas来渲染整个弹幕

在实现之前,我们先来调用一下,看看是如何创建实例的

// index.js文件
// 模拟数据
...省略
// 获取到所有需要的dom元素
...省略// 创建CanvasBarrage类
class CanvasBarrage {// todo
}
// 创建CanvasBarrage实例
let canvasBarrage = new CanvasBarrage(canvas, video, { data });

创建实例很简单,没有对象,只需要new一个就有了,哈哈。接下来,说回正事,我们赶紧完成上面代码中todo的部分,来完善CanvasBarrage类吧

实现CanvasBarrage

// index.js文件
class CanvasBarrage {constructor(canvas, video, opts = {}) { // opts = {}表示如果opts没传就设为{},防止报错,ES6语法// 如果canvas和video都没传,那就直接return掉if (!canvas || !video) return;// 直接挂载到this上this.video = video;this.canvas = canvas;// 设置canvas的宽高和video一致this.canvas.width = video.width;this.canvas.height = video.height;// 获取画布,操作画布this.ctx = canvas.getContext('2d');// 设置默认参数,如果没有传就给带上let defOpts = {color: '#e91e63',speed: 1.5,opacity: 0.5,fontSize: 20,data: []};// 合并对象并全都挂到this实例上Object.assign(this, defOpts, opts);// 添加个属性,用来判断播放状态,默认是true暂停this.isPaused = true;// 得到所有的弹幕消息this.barrages = this.data.map(item => new Barrage(item, this));// 渲染this.render();console.log(this);}// 渲染canvas绘制的弹幕render() {// todo}
}

我们在“得到所有的弹幕消息”那里,通过数组的map方法返回的还是个数组,不过返回的内容是一个Barrage类,这是为什么呢?

还记得之前说过么,用类的好处就是方便扩展,后续再添加方法的话可以直接在该类中添加即可。

所以我们也不推崇直接map方法里直接返回一个{}这种形式

// 不推荐
this.barrages = this.data.map(item => { item });

说到这里我们还要先写一下Barrage这个类,不然接下来的console.log(this)会因为找不到Barrage类而报错

// index.js文件++++++++++++++++++++++
// 创建Barrage类,用来实例化每一个弹幕元素
class Barrage {constructor(obj, ctx) {// todo}
}
++++++++++++++++++++++class CanvasBarrage {...省略
}

Now,通过上面代码中的console.log(this),我们可以看到,所有挂载到this实例上的属性和原型上的方法都呈现眼前了

render一下

接着上面的CanvasBarrage类里render方法继续写,我们来把todo完成

// index.js文件
class CanvasBarrage {constructor(canvas, video, opts = {}) {...省略// 渲染this.render();}render() {// 渲染的第一步是清除原来的画布,方便复用写成clear方法来调用this.clear();// 渲染弹幕this.renderBarrage();// 如果没有暂停的话就继续渲染if (this.isPaused === false) {// 通过raf渲染动画,递归进行渲染requestAnimationFrame(this.render.bind(this));}}clear() {// 清除整个画布this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);}
}

todo都做了什么?

1、清除之前画布所有的绘制,防止绘制重叠的影响

  • this.clear()

2、渲染真正的弹幕数据 (还未实现)

  • this.renderBarrage()

3、判断是否继续渲染弹幕

  • this.isPaused为false时表示为播放状态

4、递归调用render

  • 通过requestAnimationFrame来递归调用render

  • 要比setInterval这样的方式好很多

渲染整个弹幕render方法就完成了,那么要继续写了,应该是刚才未实现的renderBarrage方法了

But,在此之前,我们要先写个别的,它就是Barrage类

因为还需要它来大显身手一下呢,每一个弹幕的实例都由它来制造

创建Barrage类

弹幕制造者来了,下面我们就来实现一下这个Barrage类,看它都具备哪些属性和方法,继续todo吧

// index.js文件
class Barrage {constructor(obj, ctx) {this.value = obj.value; // 弹幕的内容this.time = obj.time;   // 弹幕出现时间// 把obj和ctx都挂载到this上方便获取this.obj = obj;this.context = ctx;}// 初始化弹幕init() {// 如果数据里没有涉及到下面4种参数,就直接取默认参数this.color = this.obj.color || this.context.color;this.speed = this.obj.speed || this.context.speed;this.opacity = this.obj.opacity || this.context.opacity;this.fontSize = this.obj.fontSize || this.context.fontSize;// 为了计算每个弹幕的宽度,我们必须创建一个元素p,然后计算文字的宽度let p = document.createElement('p');p.style.fontSize = this.fontSize + 'px';p.innerHTML = this.value;document.body.appendChild(p);// 把p元素添加到body里了,这样就可以拿到宽度了// 设置弹幕的宽度this.width = p.clientWidth;// 得到了弹幕的宽度后,就把p元素从body中删掉吧document.body.removeChild(p);// 设置弹幕出现的位置this.x = this.context.canvas.width;this.y = this.context.canvas.height * Math.random();// 做下超出范围处理if (this.y < this.fontSize) {this.y = this.fontSize;} else if (this.y > this.context.canvas.height - this.fontSize) {this.y = this.context.canvas.height - this.fontSize;}}// 渲染每个弹幕render() {// 设置画布文字的字号和字体this.context.ctx.font = `${this.fontSize}px Arial`;// 设置画布文字颜色this.context.ctx.fillStyle = this.color;// 绘制文字this.context.ctx.fillText(this.value, thix.x, this.y);}
}

todo都做了什么?

1、从传入的obj中取到必要的value和time

this.value = obj.value; // 内容
this.time = obj.time;   // 时间

2、初始化弹幕

  • 对每个弹幕所需的参数进行设置,如果obj上没有,就取默认参数

  • 计算每个弹幕的宽度

    • 由于不能直接操纵canvas画布里的元素,所以先创建一个p标签

    • p标签的宽度即为弹幕的宽 -> this.width = p.clientWidth

  • 设置每个弹幕的x和y坐标 (起始位置)

    • 横向x坐标起始位置都是从右边进入,即:画布的宽度

    • this.x = this.context.canvas.width

    • 纵向y坐标起始位置是不固定的,选在画布之内的任意位置出现

    • this.y = this.context.canvas.height * Math.random()

  • 处理弹幕超出画布区域

    • canvas是按照字号基线来展示字体的,如果

      小于

      这个

      字号

      大小

    • this.y = this.fontSize

    • 如果

      大于

      画布高度

      -

      字号

      大小

    • this.y = this.context.canvas.height - this.fontSize

3、渲染每个弹幕

  • 绘制文本需要设置文本的字体字号颜色和文本的内容坐标

  • 字体字号api

    • this.context.ctx.font = `${this.value}px Arial`

  • 颜色api

    • this.context.ctx.fillStyle = this.color

  • 内容与坐标api

    • this.context.ctx.fillText(this.value, this.x, this.y)

以上三步就是整个Barrage类所做的事情了。Barrage这个类都已经敲完了,那么接下来开始真正的渲染步骤吧

renderBarrage才是主角

// index.js文件
class CanvasBarrage {...省略renderBarrage() {// 首先拿到当前视频播放的时间// 要根据该时间来和弹幕要展示的时间做比较,来判断是否展示弹幕let time = this.video.currentTime;// 遍历所有的弹幕,每个barrage都是Barrage的实例this.barrages.forEach(barrage => {// 用一个flag来处理是否渲染,默认是false// 并且只有在视频播放时间大于等于当前弹幕的展现时间时才做处理if (!barrage.flag && time >= barrage.time) {// 判断当前弹幕是否有过初始化了// 如果isInit还是false,那就需要先对当前弹幕进行初始化操作if (!barrage.isInit) {barrage.init();barrage.isInit = true;}// 弹幕要从右向左渲染,所以x坐标减去当前弹幕的speed即可barrage.x -= barrage.speed;barrage.render(); // 渲染当前弹幕// 如果当前弹幕的x坐标比自身的宽度还小了,就表示结束渲染了if (barrage.x < -barrage.width) {barrage.flag = true; // 把flag设为true下次就不再渲染}}});}
}

此时我们再添加一个触发弹幕的事件,让弹幕飞起来

// index.js文件
class CanvasBarrage {...省略
}// 创建CanvasBarrage实例
let canvasBarrage = new CanvasBarrage(canvas, video, { data });
++++++++++++++++++++++++++++++++++++++
// 设置video的play事件来调用CanvasBarrage实例的render方法
video.addEventListener('play', () => {canvasBarrage.isPaused = false;canvasBarrage.render(); // 触发弹幕
});
++++++++++++++++++++++++++++++++++++++

大家一起写到了这里,也是时候展示一下成果了,往下看

别急,让弹幕再飞一会儿


渲染弹幕的功能,我们已经完成了,接下来让我们马不停蹄的写下如何发弹幕吧。别犹豫,开撸!!!

发弹幕

// index.js文件
class CanvasBarrage {...省略
}
video.addEventListener('play', ...省略);+++++++++++++++++++++++++++++++++++++++
// 发送弹幕的方法
function send() {let value = $txt.value;  // 输入的内容let time = video.currentTime; // 当前视频时间let color = $color.value;   // 选取的颜色值let fontSize = $range.value; // 选取的字号大小let obj = { value, time, color, fontSize };// 添加弹幕数据canvasBarrage.add(obj);$txt.value = ''; // 清空输入框
}
// 点击按钮发送弹幕
$btn.addEventListener('click', send);
// 回车发送弹幕
$txt.addEventListener('keyup', e => {let key = e.keyCode;key === 13 && send();
});
+++++++++++++++++++++++++++++++++++++++

发弹幕相对来说还是很简单的,获取到value, time, color, fontSize之后把他们当作对象传给CanvasBarrage的add方法进行添加就好了

下面我们再写一下add方法,回到CanvasBarrage类里继续写

// index.js文件
class CanvasBarrage {constructor() { ...省略}render() { ...省略 }renderBarrage() { ...省略 }clear() { ...省略 }+++++++++++++++++++++++++++add(obj) {// 实际上就是往barrages数组里再添加一项Barrage的实例而已this.barrages.push(new Barrage(obj, this));}+++++++++++++++++++++++++++
}

完成,漂亮,看看效果吧

写到这里我们已经完成了视频网站上的弹幕功能了,可喜可贺

下面我们再来完善一下视频播放时对弹幕的播放处理吧

暂停和拖动

  • 暂停就停止渲染弹幕

// index.js文件
...省略
// 播放
video.addEventListener('play', () => {canvasBarrage.isPaused = false;canvasBarrage.render();
});
+++++++++++++++++++++++++++++++++++++++
// 暂停
video.addEventListener('pause', () => {// isPaused设为true表示暂停播放canvasBarrage.isPaused = true;
});
+++++++++++++++++++++++++++++++++++++++
  • 回放时需要重新渲染该时刻的弹幕

// index.js文件// 暂停
video.addEventListener('pause', () => {canvasBarrage.isPaused = true;
});
+++++++++++++++++++++++++++++++++++++++
// 拖动进度条时触发seeked事件
video.addEventListener('seeked', () => {// 调用CanvasBarrage类的replay方法进行回放,重新渲染弹幕canvasBarrage.replay();
});
+++++++++++++++++++++++++++++++++++++++

让我们再次回到CanvasBarrage这个类上

// index.js文件
class CanvasBarrage {constructor() { ...省略}render() { ...省略 }renderBarrage() { ...省略 }clear() { ...省略 }add(obj) { ...省略 }+++++++++++++++++++++++++++replay() {this.clear(); //先清除画布// 获取当前视频播放时间let time = this.video.currentTime;// 遍历barrages弹幕数组this.barrages.forEach(barrage => {// 当前弹幕的flag设为falsebarrage.flag = false;// 并且,当前视频时间小于等于当前弹幕所展现的时间if (time <= barrage.time) {// 就把isInit重设为false,这样才会重新初始化渲染barrage.isInit = false;} else { // 其他时间对比不匹配的,flag还是true不用重新渲染barrage.flag = true;}});}+++++++++++++++++++++++++++
}

尽善尽美一下


OK,写到这里,所有关于弹幕功能的代码就全部结束了!!!如果工作中让你开发弹幕功能,你也可以在多敲几遍以上代码之后,得心应手的保证完成任务了

不过做事总是要做全套比较好,我们接下来再利用WebSocketredis来进行一下较为实战的功能吧

大家之前看到过目录结构,还有一个app.js文件其实是没有写任何东西的,那么接下来我们就开始写写看吧

WebSocket通信和redis存储

久违的app.js文件,开始动手 首先我们需要安装两个包,一个是处理服务端WebSocket通信的ws模块,另一个就是用来储存redis数据的redis模块

npm i ws redis -S

安装完成后可以继续写东西了

// app.js文件
const WebSocket = require('ws');
const redis = require('redis');
const clientRedis = redis.createClient(); // 创建redis客户端
const ws = new WebSocket.Server({ port: 9999 }); // 创建ws服务
// 用来存储不同的socket实例,区分不同用户
let clients = [];
// 监听连接
ws.on('connection', socket => {clients.push(socket); // 把socket实例添加到数组// 通过redis客户端的lrange方法来获取数据库中key为barrages的数据clientRedis.lrange('barrages', 0, -1, (err, data) => {// 由于redis存储的是key value类型,因此需要JSON.parse转成对象data = data.map(item => JSON.parse(item));// 发送给客户端,send方法传递的是字符串需要JSON.stringify// type为init是用来初始化弹幕数据的socket.send(JSON.stringify({type: 'init',data}));});// 监听客户端发来的消息socket.on('message', data => {// redis客户端通过rpush的方法把每个消息都添加到barrages表的最后面clientRedis.rpush('barrages', data);// 每个socket实例(用户)之间都可以发弹幕,并显示在对方的画布上// type为add表示此次操作为添加处理// 你可以打开两个index.html,分别发弹幕试试吧clients.forEach(sk => {sk.send(JSON.stringify({type: 'add',data: JSON.parse(data)}));});});// 当有socket实例断开与ws服务端的连接时// 重新更新一下clients数组,去掉断开的用户socket.on('close', () => {clients = clients.filter(client => client !== socket);});
});

服务端的内容已经全部完事了,接下来我们再稍微改下客户端的代码,回到熟悉的index.js中

// index.js文件
class CanvasBarrage {...省略
}
+++++++++++++++++++++++++++++++
// 创建CanvasBarrage实例
// let canvasBarrage = new CanvasBarrage(canvas, video, { data });
let canvasBarrage;
let ws = new WebSocket('ws://localhost:9999');// 监听与ws服务端的连接
ws.onopen = function () {// 监听ws服务端发来的消息ws.onmessage = function (e) {let msg = JSON.parse(e.data); //e.data里是真正的数据// 判断如果type为init就初始化弹幕的数据if (msg.type === 'init') {canvasBarrage = new CanvasBarrage(canvas, video, { data: msg.data });} else if (msg.type === 'add') { // 添加弹幕数据canvasBarrage.add(msg.data);}}
};
+++++++++++++++++++++++++++++++// 发送弹幕的方法
function send() {let value = $txt.value;let time = video.currentTime;let color = $color.value;let fontSize = $range.value;let obj = { value, time, color, fontSize };// 添加弹幕数据// canvasBarrage.add(obj);+++++++++++++++++++++++++++++++// 把添加的弹幕数据发给ws服务端// 由ws服务端拿到后添加到redis数据库中ws.send(JSON.stringify(obj));+++++++++++++++++++++++++++++++$txt.value = '';
}

前后端都搞定了,那么我们接下来只需要连接一下redis数据库就可以了

连接redis数据库的正确方式

首先无论是windows还是mac都需要先安装一下

windows系统

  • windows:下载redis (提取码:svua)

windows连接redis数据库

进入下载解压好的redis目录,在命令行工具中输入以下指令建立连接

redis-server.exe redis.windows.conf

出现如下图显示的样子就表示已经成功建立了连接

windows下的redis可视化工具(Redis Desktop Manager)

mac系统

  • mac: brew install redis

  • 连接: brew services start redis

redis数据库如果成功的连接了,那么就可以直接启动app.js的服务了,打开index.html文件,会发现可以拿到数据库里存储的弹幕数据了

好了,这下大家满足了吧,很厉害,我们每个人都可以敲出自己的弹幕了。

不断的学习会让我们一点一滴的进步下去,前端的路还很长,我们都在慢慢前行

对了,忘记重要的事情了,如果大家有什么疑问可以看下源码地址进行参考

结束了


之后一段时间打算好好的研究一下canvas绘图的知识点了,也希望在研究后可以很好的梳理一下分享给大家一起来学习

作为大前端来说,我们要学的东西实在太多了,一专多精才是王道,不负好时光,一起努力吧!谢谢大家的观看了

参考


  • HTML5 Video元素介绍

  • Canvas学习教程

  • 珠峰架构培训公开课 实现弹幕系统

  • ES6语法学习

  • WebSocket学习参考

弹幕,你知道是怎样练成的?相关推荐

  1. 在线直播源码评论弹幕是如何“练”成的?

    在线直播源码评论弹幕是如何"练"成的? 提起弹幕(dànmù),大家都会想到「视频弹幕」.视频弹幕是指网友们在观看视频的同时参与评论,即所谓"即时反馈", 评论 ...

  2. 编程高手是如何练成的?

    每个人都有成长的渴望,也都会遇到成长的瓶颈.下面这个问题是一个读者问我的: 如何才能训练成为一个编程高手? 先简单说下这个读者的背景:工作 3 年多,目前在大厂做后台开发,身边有不少编程高手,是他想要 ...

  3. Python百练成钢002-计算自幂数

    这是[Python 百练成钢]系列文章的第 002 篇,计划完成 100 道练习题. 本文环境:python3.8 计算自幂数 什么是自幂数? 自幂数:也叫超完全数字不变数.自恋数.阿姆斯特朗数(Ar ...

  4. 仿站高手是怎么练成的 分析仿站必备知识总结

    仿站高手是怎么练成的 分析仿站必备知识总结 来源:http://hep6.com 作者:和平 现在的网站,真正原创设计的没有几个,都是我抄袭你,你抄袭我,再自己修改下,这可能跟中国的国情有关吧!抄袭网 ...

  5. 【MySQL】MySQL高手是如何练成的?

    MySQL 什么是MySQL呢? 怎样练成MySQL高手? 在Linux安装MySQL 问题处理 Mysql 的用户管理 什么是MySQL呢? Mysql 是开源的,可以定制的,采用了 GPL 协议, ...

  6. 小强怎样练成——读《现代软件工程——构建之法》第三章有感

    小强怎样练成 ----读<现代软件工程--构建之法>第三章有感 一.知道自己吃几碗干饭 先秦时期的"革命家"告诉我们"知人者智,自知者明",&quo ...

  7. 李小龙:超人是这样练成的

    李小龙:超人是这样练成的 李小龙:超人是这样练成的 作者:约翰·立托 本文原载于美国<肌肉与健美>(Muscle. Fitness)杂志1994年3月号.<肌肉与健美>杂志由世 ...

  8. 著名弹跳训练法 练成可提高30cm

    搜狐体育讯 怎样提高弹跳一直是广大篮球迷最关注的话题之一,下面我们就为大家推荐一种弹跳训练方法--美国最著名纵跳训练计划, 练成后预计纵跳能力(也就是原地弹跳)可以提高20到30厘米以上, 不过锻炼过 ...

  9. 老司机写的java代码_老司机告诉你高质量的Java代码是怎么练成的?

    一提起程序员,首先想到的一定是"码农",对,我们是高产量的优质"码农",我们拥有超跃常人的逻辑思维以及不走寻常路的分析.判别能力,当然,我们也有良好的编码规范, ...

  10. 牛逼的C/C++程序员是如何练成的?

    这个题目的噱头太大,要真的写起来, 足够写一本书了. 牛耳人分享一些经验,希望能让初学的小伙伴少走弯路. 每个人的情况不一样,所以下面的描述可能并不适合每一个看到这篇文章的人. 一.C/C++语言 如 ...

最新文章

  1. python怎么做彩票概率_用Python一次性把论文作图与数据处理全部搞定!
  2. 凭啥Java运行环境称虚拟机 Python只能称解释器
  3. Android 将Openfire中的MUC改造成类似QQ群一样的永久群
  4. 放置奇兵 算法 月度活动 破碎时空记录 第七关 阿姨(阿伊达)+暗战(阿斯布)
  5. Zw*与Nt*的区别
  6. Ant在Java项目中的使用(一眼就看会)
  7. connect ECONNREFUSED 151.101.0.133:443 | spawn xxx ENOENT
  8. CSS3动画 - 心脏跳动
  9. 16位和32位微处理器(4)——Pentium的寄存器及相关机制
  10. 特斯拉Model 3本周平均日产约900辆 7000辆周产量有望
  11. .NET开发相关技术
  12. 【转】OUTLOOK签名档中加入写信日期
  13. iOS开发计算工程里面的代码行数
  14. NB-IOT开发实战
  15. win10任务栏透明_几款软件让你的 Win10 与众不同(简洁篇)
  16. RabbitMQ 使用规范
  17. Cisco(思科)远程登录交换机
  18. 【转】以太坊 2.0 中的验证者经济模型
  19. Redis key前缀的设计与使用
  20. 排查cpu feature 缺少x2apic原因

热门文章

  1. html和xhtml和html5一些区别和笔记
  2. 学习动态性能表(10)--v$session_longops
  3. 读书-算法《程序设计导引及在线实践》-简单计算题5:装箱问题
  4. 自定义的命民空间在其他程序集里无法调用
  5. 基于USES_CONVERSION的W2A用法之CString转char
  6. 使用OpenCV 实现matlab的padarray(A, padsize, ‘symmetric’)函数简单实现
  7. new/delete和malloc/free的区别
  8. winsever 2008 r2 管理员账号没有权限_钉钉管理员攻略—主管理员①
  9. ftp协议是一种用于_______的协议_网工知识角|快速理解FTP和TFTP的区别,实用收藏...
  10. 新手必知20点VC技巧【转】