要实现的是 “历史弹幕 + 实时弹幕” 混合的完整弹幕系统,类似 B 站的做法。
我们来一步步设计并实现它👇
✅ 场景目标
你的视频播放器要支持:
功能 | 说明 |
---|---|
🎞 历史弹幕 | 视频开始播放时加载过去用户发送的弹幕,按时间同步展示 |
🧑💻 实时弹幕 | 当前用户或其他用户正在发的弹幕,立即出现在屏幕上 |
🧠 核心设计理念
🔁 整体流程图
┌────────────┐
┌────────────▶│ WebSocket │◀────────────┐
│ │ 服务器广播 │ │
│ └─────▲──────┘ │
│ │ │
▼ ┌─────┴──────┐ ▼
[观看者A] [Spring Boot 后端] [观看者B]
│ GET 弹幕历史 ▲ │ GET 弹幕历史 │
│ 数据库 │ │
▼ ▼ ▼
同步播放历史弹幕 接收实时弹幕 接收实时弹幕
🧱 技术实现拆解
🔸 后端
✅ 数据模型(MySQL)
CREATE TABLE danmu (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
video_id BIGINT NOT NULL,
text VARCHAR(255) NOT NULL,
time_in_video DOUBLE NOT NULL, -- 出现的时间(秒)
send_time DATETIME NOT NULL
);
✅ 接口 1:获取历史弹幕
@GetMapping("/api/danmu/history")
public List<DanmuDto> getHistory(@RequestParam Long videoId) {
return danmuRepository.findByVideoIdOrderByTimeInVideo(videoId);
}
✅ 接口 2:WebSocket 实时弹幕服务
@ServerEndpoint("/ws/danmu/{videoId}")
@Component
public class DanmuWebSocket {
private static final Map<String, Session> sessionMap = new ConcurrentHashMap<>();
@OnOpen
public void onOpen(Session session, @PathParam("videoId") String videoId) {
sessionMap.put(session.getId(), session);
}
@OnMessage
public void onMessage(String msgJson, Session session, @PathParam("videoId") String videoId) {
// 解析 JSON 为 Danmu 对象
Danmu danmu = new Gson().fromJson(msgJson, Danmu.class);
danmu.setSendTime(LocalDateTime.now());
// 保存弹幕到数据库
danmuService.save(danmu);
// 广播给所有客户端
sessionMap.values().forEach(s -> {
try {
s.getBasicRemote().sendText(msgJson);
} catch (IOException e) {
e.printStackTrace();
}
});
}
@OnClose
public void onClose(Session session) {
sessionMap.remove(session.getId());
}
}
🔸 前端(HTML + JS)
✅ 加载历史弹幕并同步播放
let historyDanmus = []; // { time: 3.5, text: "弹幕内容" }
fetch("/api/danmu/history?videoId=101")
.then(res => res.json())
.then(data => {
historyDanmus = data.sort((a, b) => a.time - b.time);
});
setInterval(() => {
const currentTime = video.currentTime;
while (historyDanmus.length && historyDanmus[0].time <= currentTime) {
showDanmu(historyDanmus.shift().text);
}
}, 200);
✅ 实时弹幕连接
const socket = new WebSocket("ws://localhost:8080/ws/danmu/101");
socket.onmessage = (event) => {
const danmu = JSON.parse(event.data);
showDanmu(danmu.text);
};
function sendDanmu(text) {
const time = video.currentTime;
socket.send(JSON.stringify({ videoId: 101, text, time }));
}
🔸 弹幕展示动画(CSS + JS)
function showDanmu(text) {
const el = document.createElement("div");
el.className = "danmu";
el.innerText = text;
el.style.top = Math.random() * 80 + "%";
el.style.color = randomColor();
document.getElementById("danmu-area").appendChild(el);
setTimeout(() => el.remove(), 8000);
}
✅ 总结:逻辑分工
功能 | 负责方 | 技术 |
---|---|---|
视频播放进度监听 | 前端 | video.currentTime |
加载历史弹幕 | 后端接口 | GET /api/danmu/history |
弹幕显示同步播放 | 前端 JS + 定时器 | 每 200ms 检查一次 |
实时弹幕收发 | WebSocket | 后端广播 + 前端展示 |
数据存储 | MySQL | timeInVideo + text |
✅ 用户视角体验流程
- 打开视频页面
- 请求并同步播放历史弹幕
- 用户点击发送 → 发到 WebSocket
- 后端广播 + 存库
- 所有在线用户立即看到