一、引言
DPlayer官网:DPlayer
官方弹幕后端服务:DPlayer-node
MoePlayer/DPlayer-node:使用 Docker for DPlayer Node.js 后端(https://github.com/DIYgod/DPlayer)
本来想直接使用官网提供的DPlayer-node直接搭建的,折腾了一天,在Windows上又是装Docker,又是Redis,又是Mongodb,可能是我的环境或者Windows的原因,启动就报错,遂自己做一个简单的弹幕后端服务。
当然,为了让DPlayer能够成功接收自己做的接口,还要看一下DPlayer 的官方文档与DPlayer-node的源码,返回DPplayer能够接受的接口数据。
二、环境
系统:Windowsjs
数据库:Mysql
后端:Node.js
IDE:Vscode
接口测试:postman(可选)
三、核心逻辑
1.视频开始前查询弹幕
- 客户端请求:当用户打开一个视频时,前端(如 DPlayer)会向弹幕服务器发送一个请求,获取与该视频相关的所有弹幕信息。请求中通常会包含视频的唯一标识(如 videoId)。
- 服务器查询:弹幕服务器接收到请求后,会根据视频 ID 从数据库中查询所有相关的弹幕数据。查询结果包括弹幕的文本内容和出现时间(time)等信息。
- 返回弹幕数据:服务器将查询到的弹幕数据返回给客户端,客户端接收到数据后将其存储在内存中。
2.视频播放过程中的弹幕显示
- DPlayer 处理弹幕:在视频播放过程中,DPlayer 会根据每个弹幕的出现时间(time)来控制弹幕的显示。当视频播放时间达到某个弹幕的 time 值时,DPlayer 会在屏幕上显示该弹幕。
- 弹幕的实时显示:DPlayer 会持续监测视频的播放时间,并在合适的时间点显示对应的弹幕,从而实现弹幕的实时滚动效果。
3.添加新弹幕
- 客户端发送请求:当用户发送一条新的弹幕时,前端会向弹幕服务器发送一个 POST 请求,请求体中包含弹幕的文本内容、出现时间、颜色、类型等信息。
- 服务器处理请求:弹幕服务器接收到请求后,会将新的弹幕信息存储到数据库中。
- 更新弹幕数据:新的弹幕数据会被添加到数据库中,以便其他客户端在请求弹幕数据时能够获取到最新的弹幕信息。
4.系统架构图
+-------------------+ +-------------------+ +-------------------+
| | | | | |
| 客户端 (DPlayer) | HTTP | 弹幕服务器 | SQL | 数据库 (MySQL) |
| | <-------> | | <-------> | |
| - 视频播放 | | - 处理请求 | | - 存储弹幕数据 |
| - 显示弹幕 | | - 返回弹幕数据 | | - 查询弹幕数据 |
| | | | | |
+-------------------+ +-------------------+ +-------------------+
四、数据库&接口分析
1.网络请求分析
直接在Vscode新建一个HTML文件,引入DPlayer:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>视频弹幕演示</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/dplayer/dist/DPlayer.min.css">
</head>
<body>
<div id="dplayer"></div>
<script src="https://cdn.jsdelivr.net/npm/dplayer/dist/DPlayer.min.js"></script>
<script>
const videoId = 'test'; // ❗ 替换为实际视频 ID(如 1028,需与后端一致)
const dp = new DPlayer({
container: document.getElementById('dplayer'),
video: {
url: 'video/test.mp4', // 视频路径
type: 'mp4',
name: '演示视频'
},
danmaku: {
id: videoId, // 必须与后端路由中的 :id 一致
api: 'http://127.0.0.1:3000/danmaku/', // 后端弹幕服务根路径
addition: [
// 外挂弹幕
//`http://127.0.0.1:3000/danmaku/list/${videoId}`
]
}
});
// 监听 DPlayer 的弹幕发送事件
dp.on('danmaku_send', (danmaku) => {
console.log('发送请求')
console.log('视频ID:' + videoId)
});
</script>
</body>
</html>
视频路径改为本地的视频,里面的api和addition就随便写一个接口,不管能不能请求成功,重点是看DPlayer请求的接口;
- 在Vscode中安装Live Server
- 在新建的HTML页面上按快捷键:Alt+L+O,自动打开浏览器,然后按F12打开开发者工具:
- 按F5或点击刷新按钮刷新本页面,筛选Fetch/XHR,这个就是发送的请求
可以看到这个请求是404,没有请求到数据,因为我还没建这个网络请求
点击会显示请求信息:
http://127.0.0.1:3000/danmaku/v3/?id=test
我写的请求明明是:“api: 'http://127.0.0.1:3000/danmaku/'”;
但浏览器发出的请求是在我写的基础上增加了“v3/?id=test”,说明这个接口被DPlayer.min.js修改了,所以我需要专门提供这个接口;
- 增加弹幕
随便发送一个弹幕,浏览器会捕获到发送弹幕的请求
http://127.0.0.1:3000/danmaku/v3/
和第一个接口相比没有了参数?id,所以我要为这个接口提供get还有post请求,get请求根据id返回数据,post请求插入数据。
- 切换到负载
这里可以看到请求所需的数据:
{
"id": "test",
"author": "DIYgod",
"time": 21.778242,
"text": "dsfsd",
"color": 16777215,
"type": 0
}
这就是数据表所需的数据。
2.源码分析
根据官方提供的后端服务推测所需的数据表结构和接口
DPlayer-node/routes at master · MoePlayer/DPlayer-node
查看根目录下的路由文件:
DPlayer-node/router.js
const Router = require('koa-router');
const router = new Router();
router.get('/v3', require('./routes/get'));
router.post('/v3', require('./routes/post'));
router.get('/v3/bilibili', require('./routes/bilibili'));
module.exports = router;
该服务一共接收三个请求:post、get、bilibili,关联至DPlayer-node/routes/目录下的三个js文件:
bilibili.js | set cache time | 7 years ago |
get.js | compatible with empty author | 7 years ago |
post.js | fix redis del | 7 years ago |
bilibili.js顾名思义是B站的弹幕接口,不知现在还有没有用,这里也不需要用到B站的弹幕,故跳过;
主要重点是负责get/post请求的这两个js文件,觉得麻烦直接问AI一步到位。
post.js
const logger = require('../utils/logger');
module.exports = async (ctx) => {
const body = ctx.request.body;
const dan = new ctx.mongodb({
player: body.id,
author: body.author,
time: body.time,
text: body.text,
color: body.color,
type: body.type,
ip: ctx.ips[0] || ctx.ip,
referer: ctx.headers.referer,
date: +new Date(),
});
try {
const data = await dan.save();
ctx.body = JSON.stringify({
code: 0,
data,
});
ctx.redis.del(`danmaku${data.player}`);
}
catch (err) {
logger.error(err);
ctx.body = JSON.stringify({
code: 1,
msg: `Database error: ${err}`,
});
}
};
-
接收前端发送的弹幕数据。
-
将弹幕数据保存到数据库中。
-
如果保存成功,删除对应的 Redis 缓存并返回成功响应。
-
如果保存失败,记录错误日志并返回错误响应。
get.js
function htmlEncode (str) {
return str ? str.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''')
.replace(/\//g, '/') : '';
}
module.exports = async (ctx) => {
const { id, limit } = ctx.request.query;
let data = await ctx.redis.get(`danmaku${id}`);
if (data) {
data = JSON.parse(data);
if (limit) {
data = data.slice(-1 * parseInt(limit));
}
ctx.response.set('X-Koa-Redis', 'true');
} else {
data = await ctx.mongodb.find({ player: id }) || [];
ctx.redis.set(`danmaku${id}`, JSON.stringify(data));
if (limit) {
data = data.slice(-1 * parseInt(limit));
}
ctx.response.set('X-Koa-Mongodb', 'true');
}
ctx.body = JSON.stringify({
code: 0,
data: data.map((item) => [item.time || 0, item.type || 0, item.color || 16777215, htmlEncode(item.author) || 'DPlayer', htmlEncode(item.text) || '']),
});
};
-
接收前端发送的视频 ID 和弹幕数量限制参数。
-
尝试从 Redis 缓存中获取弹幕数据,如果缓存存在则直接返回。
-
如果缓存不存在,从 MongoDB 数据库中查询弹幕数据,并将结果存入 Redis 缓存。
-
根据
limit
参数对数据进行截取,只返回最近的弹幕数据。 -
对弹幕的作者和文本内容进行 HTML 编码,防止 XSS 攻击。
-
返回格式化后的弹幕数据给客户端。
五、分析结果
1.数据库表结构
表:danmaku(弹幕表)
字段名 | 类型 | 描述 |
---|---|---|
id | INT PRIMARY KEY | 弹幕唯一标识 |
player | VARCHAR(255) | 关联的视频唯一标识 |
author | VARCHAR(255) | 弹幕作者 |
time | DECIMAL(10, 2) | 弹幕出现的时间(秒) |
text | TEXT | 弹幕文本内容 |
color | VARCHAR(7) | 弹幕颜色(十六进制) |
type | INT | 弹幕类型(如滚动、顶部、底部) |
ip | VARCHAR(45) | 发送弹幕的用户IP地址 |
referer | TEXT | 请求来源页面 |
date | TIMESTAMP | 弹幕发送的时间戳 |
2.接口格式
2.1获取弹幕
-
接口地址:
GET /v3
-
请求参数:
-
videoId
:视频唯一标识 -
limit
:限制返回的弹幕数量(可选)
-
-
响应格式:
{ "code": 0, "data": [ [ 10.5, // 弹幕出现的时间(秒) 1, // 弹幕类型 16777215, // 弹幕颜色(十六进制转十进制) "DPlayer", // 弹幕作者(HTML编码) "这是一条测试弹幕" // 弹幕文本内容(HTML编码) ] ] }
2.2添加弹幕
-
接口地址:
POST /v3
-
请求体:
{ "id": "test", // 视频唯一标识 "author": "user1", // 弹幕作者 "time": 20.0, // 弹幕出现的时间(秒) "text": "这是一条新弹幕", // 弹幕文本内容 "color": "#FFFFFF", // 弹幕颜色(十六进制) "type": 1 // 弹幕类型 }
-
响应格式:
{ "code": 0, "data": { "id": 2, "player": "test", "author": "user1", "time": 20.0, "text": "这是一条新弹幕", "color": "#FFFFFF", "type": 1, "ip": "192.168.1.1", "referer": "http://example.com", "date": 1714234800000 } }
六、创建数据表
-- 创建数据库
CREATE DATABASE danmaku_db;
-- 使用创建的数据库
USE danmaku_db;
-- 创建弹幕表
CREATE TABLE danmaku (
id INT PRIMARY KEY AUTO_INCREMENT,
player VARCHAR(255) NOT NULL,
author VARCHAR(255) NOT NULL,
time DECIMAL(10, 2) NOT NULL,
text TEXT NOT NULL,
color VARCHAR(20) NOT NULL,
type INT NOT NULL,
ip VARCHAR(45) NOT NULL,
referer TEXT,
date TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
七、编写接口
1. 环境验证
以下需要Node环境,cmd输入以下命令检查是否含有所需环境:
node -v
npm -v
返回版本号证明安装成功。
2. 创建项目
mkdir danmaku-server
cd danmaku-server
npm init -y
3. 安装依赖
npm install express mysql2 body-parser morgan cors
4. 数据库连接文件
在项目根目录下新建文件夹models,在models中新建danmaku.js文件
const mysql = require('mysql2/promise');
const pool = mysql.createPool({
host: 'localhost',
user: 'root',
password: 'your_password',
database: 'danmaku_db',
waitForConnections: true,
connectionLimit: 10,
queueLimit: 0
});
module.exports = pool;
5. 获取弹幕接口
在项目根目录下新建文件夹routes,在routes中新建get.js文件
const express = require('express');
const router = express.Router(); // 使用 express.Router() 创建路由实例
const pool = require('../models/danmaku');
// 处理 GET 请求
router.get('/', async (req, res) => {
console.log('GET请求');
const { videoId, limit } = req.query; // 从查询参数获取 videoId 和 limit
// 参数验证
if (!videoId) {
return res.status(400).json({ code: 1, msg: 'videoId 参数是必需的' });
}
try {
const [rows] = await pool.query(
'SELECT time, type, color, author, text FROM danmaku WHERE player = ? ORDER BY time',
[videoId]
);
let data = rows.map(item => [
parseFloat(item.time),
parseInt(item.type),
parseInt(item.color.replace('#', ''), 16),
item.author,
item.text
]);
if (limit) {
const parsedLimit = parseInt(limit);
if (isNaN(parsedLimit) || parsedLimit <= 0) {
return res.status(400).json({ code: 1, msg: 'limit 参数必须是正整数' });
}
data = data.slice(-parsedLimit);
}
res.json({
code: 0,
data
});
} catch (error) {
console.error('获取弹幕失败:', error);
res.status(500).json({ code: 1, msg: '获取弹幕失败' });
}
});
module.exports = router;
6. 添加弹幕接口
在routes中新建post.js文件
const express = require('express');
const router = express.Router();
const pool = require('../models/danmaku');
const bodyParser = require('body-parser');
router.use(bodyParser.json());
router.post('/', async (req, res) => {
console.log('POST请求');
const { id, author, time, text, color, type } = req.body;
const ip = req.ip;
const referer = req.headers.referer || '';
try {
const [result] = await pool.query(
'INSERT INTO danmaku (player, author, time, text, color, type, ip, referer) VALUES (?, ?, ?, ?, ?, ?, ?, ?)',
[id, author, time, text, color, type, ip, referer]
);
res.json({
code: 0,
data: {
id: result.insertId,
player: id,
author,
time,
text,
color,
type,
ip,
referer,
date: new Date().getTime()
}
});
} catch (error) {
console.error('添加弹幕失败:', error);
res.status(500).json({ code: 1, msg: '添加弹幕失败' });
}
});
module.exports = router;
7. 主应用文件
在项目根目录下创建app.js文件
const express = require('express');
const app = express();
const getRouter = require('./routes/get');
const postRouter = require('./routes/post');
const morgan = require('morgan');
const cors = require('cors'); // 引入 cors 中间件
// 使用 cors 中间件处理跨域请求
app.use(cors());
// 使用 morgan 作为日志中间件,记录更详细的请求信息
app.use(morgan('combined'));
app.use(express.json());
// 使用 /v3 作为基础路径
app.use('/v3', getRouter);
app.use('/v3', postRouter);
// 全局错误处理中间件
app.use((err, req, res, next) => {
console.error('发生未捕获的异常:', err);
res.status(500).json({ code: 1, msg: '服务器内部错误' });
});
const PORT = 3000;
app.listen(PORT, (err) => {
if (err) {
console.error(`无法启动服务器:`, err);
} else {
console.log(`弹幕服务器运行在 http://localhost:${PORT}`);
}
});
8. 目录结构
9. 运行服务器
在终端内运行命令:
node app.js
出现以下界面说明运行成功
八、接口测试
1. get测试
在浏览器中输入链接:http://localhost:3000/v3?videoId=test&limit=10
出现以上界面说明请求接口成功,获取弹幕失败为数据库问题,把数据库配置改成自己本地的就可以了:
const mysql = require('mysql2/promise');
const pool = mysql.createPool({
host: '127.0.0.1', //使用localhost可能会连接失败
user: 'root',
password: '123456',
database: 'danmaku_db',
waitForConnections: true,
connectionLimit: 10,
queueLimit: 0
});
module.exports = pool;
最终的效果应是这样的:
2.post测试
post测试就不能直接使用浏览器,这里需要用到接口测试软件postman,如果没有就要自己在HTML里面写一个接口请求,比较麻烦,后面会直接写一个DPlayer的引用测试。
打开 Postman:
-
在 Postman 中,选择
POST
方法。 -
在请求 URL 输入框中输入
http://localhost:3000/v3
。
设置请求头:
-
点击
Headers
标签。 -
添加一个请求头,
Content-Type
设置为application/json
。
设置请求体:
-
点击
Body
标签。 -
选择
raw
单选按钮,并从下拉列表中选择JSON
。 -
在文本框中输入以下 JSON 数据:
{ "id": "test", "author": "user1", "time": 20.0, "text": "这是一条新弹幕", "color": "#FFFFFF", "type": 1 }
发送请求:
-
点击
Send
按钮发送请求。 -
检查响应,确保服务器返回了正确的结果。
检查数据库是否成功插入了数据
九、在DPlayer中使用
1.新建HTML文件
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>视频弹幕演示</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/dplayer/dist/DPlayer.min.css">
</head>
<body>
<div id="dplayer"></div>
<script src="https://cdn.jsdelivr.net/npm/dplayer/dist/DPlayer.min.js"></script>
<script>
const videoId = 'test'; // ❗ 替换为实际视频 ID(如 1028,需与后端一致)
const dp = new DPlayer({
container: document.getElementById('dplayer'),
video: {
url: 'video/test.mp4', // 视频路径
type: 'mp4',
name: '演示视频'
},
danmaku: {
id: videoId, // 必须与后端路由中的 :id 一致
api: 'http://127.0.0.1:3000/', // 后端弹幕服务根路径
addition: [
// 外挂弹幕
//`http://127.0.0.1:3000/`
]
}
});
// 监听 DPlayer 的弹幕发送事件
dp.on('danmaku_send', (danmaku) => {
console.log('发送请求')
console.log('视频ID:' + videoId)
});
</script>
</body>
</html>
将视频路径“video/test.mp4”换成你自己项目视频所在的路径,比如我的项目结构如下:
2. 弹幕加载
直接在新建的HTML页面上按快捷键:“Alt+L+O”或右键菜单Open with Live Server,没有该选项需要先安装插件“Live Server”,将自动打开默认浏览器。
打开开发者工具,切换到网络页面,刷新当前网页,网络请求没有变红,并且返回相应数据说明弹幕查询成功;
出现相应的弹幕说明DPlayer解析我们的接口数据成功。
3.发送弹幕测试
- 该请求没有变红,并且返回了相应的数据;
- 数据库也增加了这条数据;
- 视频上也有相应的弹幕;
以上情况都出现了,说明弹幕发送成功。
十、服务器日志
如果其中某个操作没有成功,就需要查看服务器的日志,重复没有成功操作,查看终端是否输出错误信息,根据错误信息定位错误。
比如,我执行发送弹幕的操作没有成功,查看日志输出为:“Data too long for column 'color' at row 1”,插入数据库的长度太长,需要将color字段的长度从varchar(10)改成varchar(20)。
十一、总结
一个非常简陋的Node.js后端服务项目,基本都是用别人写好的东西,感觉还是挺适合初次接触Node.js的同学的。
涉及的技术也很浅:
1.前端:HTML、Javascript;
2.后端:Node.js;
3.接口测试:postman ;