| |
|
开发:
C++知识库
Java知识库
JavaScript
Python
PHP知识库
人工智能
区块链
大数据
移动开发
嵌入式
开发工具
数据结构与算法
开发测试
游戏开发
网络协议
系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程 数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁 |
-> 网络协议 -> Canvas + WebSocket实现视频弹幕 -> 正文阅读 |
|
[网络协议]Canvas + WebSocket实现视频弹幕 |
页面布局首先,我们需要实现页面布局,在根目录创建? 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; } 布局效果如下图: ? 定义接口,构造假数据我们弹幕中的弹幕数据正常情况下应该是通过与后台数据交互请求回来,所以我们需要先定义数据接口,并构造假数据来实现前端逻辑。 数据字段定义:
上面的? 前端定义的假数据如下: // 文件:index.js let data = [ { value: "这是第一条弹幕", speed: 2, time: 0, color: "red", fontSize: 20 }, { value: "这是第二条弹幕", time: 1 } ]; 实现前端弹幕的逻辑我们希望是把弹幕封装成一个功能,只要有需要的地方就可以使用,从而实现复用,那么不同的地方使用这个功能通常的方式是? 1、创建弹幕功能的类及基本参数处理布局时需要注意 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); } } 应该挂在实例上的属性除了有当前的? 2、创建构造每一条弹幕的类我们知道,后台返回给我们的弹幕数据是一个数组,这个数组里的每一个弹幕都是一个对象,而对象上有着这条弹幕的信息,如果我们需要在每一个弹幕对象上再加一些新的信息或者在每一个弹幕对象的处理时用到了当前弹幕功能类? // 文件:index.js class Barrage { constructor(item, ctx) { this.value = item.value; // 弹幕的内容 this.time = item.time; // 弹幕出现的时间 this.item = item; // 每一个弹幕的数据对象 this.ctx = ctx; // 弹幕功能类的执行上下文 } } 在我们的? // 文件: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)); // ********** 以上为新增代码 ********** } } 其实通过上面操作以后,我们相当于把? 3、在 CanvasBarrage 类实现渲染所有弹幕的 render 方法
// 文件: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)); } } // ********** 以上为新增代码 ********** } 在上面的? 只要视频是在播放状态应该不断的调用? 这里我们使用 H5 的新 API? 由于我们使用面向对象的方式,所以渲染弹幕的具体细节,我们抽离出一个单独的方法? 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; } } }); } // ********** 以上为新增代码 ********** } 此处的? 之前我们的每一条弹幕实例的属性可能不全,弹幕的其他未传参数并没有初始化,所以为了最大限度的节省性能,我们在弹幕该第一次绘制的时候去初始化参数,等到视频播放的时间变化再去重新绘制时,不再初始化参数,所以初始化参数的方法放在了判断弹幕出现时间的条件里面执行,又设置了代表弹幕实例是不是初始化了的参数? 在? 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; } } // ********** 以上为新增代码 ********** } 在上面代码的? 6、实现每条弹幕的渲染和弹幕移除屏幕的处理我们当时在? // 文件: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; // 是否出去了,出去了,做停止渲染的操作 } // ********** 以上为新增代码 ********** } }); } } 每个弹幕实例都有一个? 每一条弹幕具体是怎么渲染的,通过代码可以看出每个弹幕实例在? 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); } // ********** 以上为新增代码 ********** } 从上面新增代码我们可以看出,其实? 8、实现播放、暂停事件还记得我们的? 还记得我们之前构造的两条假数据? // 文件: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); // 发送弹幕的方法 }); 其实我们发送弹幕时,就是向? // 文件: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(); }); 我们在事件内部其实只是调用了一下? // 文件: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; // 否则将弹幕的状态设置为以移出屏幕 } }); } // ********** 以上为新增代码 ********** } 其实?
从而实现了拖动进度条弹幕的 “前进” 和 “后退” 功能。 使用 WebSocket 和 Redis 实现前后端通信及数据存储1、服务器代码的实现要使用 WebSocket 和 Redis 首先需要去安装? npm install ws redis 我们创建一个? // 文件:index.js const WebSocket = require("ws"); // 引入 WebSocket const 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 服务把当前这一条数返回给前端,需要注意一下几点:
2、前端代码的修改在没有实现后端代码之前,前端使用的是? 现在我们需要更正一下交互逻辑,在发送弹幕事件触发时,我们应该先将获取的单条弹幕数据通过 WebSocket 发送给后端服务器,在服务器重新将消息返还给我们的时候,去将这条数据通过? 还有在页面初始化时,我们之前在创建? // 文件: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); // 发送弹幕的方法 // ********** 上面代码被删掉了 ********** }); 现在我们可以打开? 3、实现多端通信、弹幕共享我们需要处理两件事情:
// 文件:server.js const WebSocket = require("ws"); // 引入 WebSocket const 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 视频弹幕的实现,实现过程可能有些复杂,但整个过程写的还是比较详细,可能需要一定的耐心慢慢的读完,并最好一步一步跟着写一写,希望这篇文章可以让读到的人解决视频弹幕类似的需求,真正理解整个过程和开放封闭原则,认识到前端面向对象编程思想的美。 |
|
网络协议 最新文章 |
使用Easyswoole 搭建简单的Websoket服务 |
常见的数据通信方式有哪些? |
Openssl 1024bit RSA算法---公私钥获取和处 |
HTTPS协议的密钥交换流程 |
《小白WEB安全入门》03. 漏洞篇 |
HttpRunner4.x 安装与使用 |
2021-07-04 |
手写RPC学习笔记 |
K8S高可用版本部署 |
mySQL计算IP地址范围 |
|
上一篇文章 下一篇文章 查看所有文章 |
|
开发:
C++知识库
Java知识库
JavaScript
Python
PHP知识库
人工智能
区块链
大数据
移动开发
嵌入式
开发工具
数据结构与算法
开发测试
游戏开发
网络协议
系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程 数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁 |
360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年12日历 | -2024/12/28 21:26:02- |
|
网站联系: qq:121756557 email:121756557@qq.com IT数码 |
数据统计 |