別只會(huì)聊天室!用 Spring Boot 3 玩出酷炫實(shí)時(shí)彈幕特效
在當(dāng)今的視頻平臺(tái)和直播場(chǎng)景中,彈幕技術(shù)成為提升用戶參與度與互動(dòng)體驗(yàn)的關(guān)鍵工具。彈幕通過實(shí)時(shí)渲染觀眾評(píng)論在視頻播放界面中橫向滾動(dòng)顯示,不僅增強(qiáng)了沉浸感,也營(yíng)造了社區(qū)式觀影氛圍。本文將基于 Spring Boot 3 構(gòu)建一個(gè)具備實(shí)時(shí)通信、內(nèi)容過濾與歷史記錄能力的彈幕系統(tǒng)。
系統(tǒng)功能概覽
功能定義
彈幕系統(tǒng)允許用戶將文字信息實(shí)時(shí)發(fā)送至正在播放的視頻畫面中。內(nèi)容通常在視頻上層以從右至左方式動(dòng)態(tài)滾動(dòng),呈現(xiàn)同步評(píng)論的視覺效果。
主要特征
- 低延遲推送用戶評(píng)論可在毫秒級(jí)別廣播至所有連接終端;
- 強(qiáng)交互性評(píng)論即時(shí)可見,營(yíng)造出“陪伴觀影”的社交感;
- 內(nèi)容時(shí)間綁定彈幕多與視頻時(shí)間點(diǎn)匹配,有助于信息歸檔與回放分析;
- 視覺層沖擊批量彈幕可呈現(xiàn)獨(dú)特動(dòng)態(tài)視覺表現(xiàn)。
技術(shù)架構(gòu)設(shè)計(jì)
系統(tǒng)構(gòu)成
系統(tǒng)由以下核心模塊組成:
- 前端播放器負(fù)責(zé)視頻呈現(xiàn)與彈幕展示;
- WebSocket 推送引擎實(shí)現(xiàn)低延遲的實(shí)時(shí)消息廣播;
- 持久化存儲(chǔ)模塊記錄用戶彈幕數(shù)據(jù),支持回放及分析;
- 內(nèi)容審查組件確保發(fā)送信息符合平臺(tái)規(guī)范。
協(xié)議選型分析
在實(shí)時(shí)推送技術(shù)方案中,以下協(xié)議可供選擇:
協(xié)議 | 優(yōu)點(diǎn) | 局限性 | 推薦場(chǎng)景 |
WebSocket | 全雙工、低延遲、兼容廣 | 需維持長(zhǎng)連接,資源占用較高 | 高實(shí)時(shí)性場(chǎng)景,如彈幕直播 |
SSE | 實(shí)現(xiàn)簡(jiǎn)單,適合單向推送 | 僅支持服務(wù)器向客戶端 | 新聞推送、股票刷新 |
Long Polling | 通用性強(qiáng) | 實(shí)時(shí)性差,耗資源 | 極端兼容場(chǎng)景或降級(jí)備選方案 |
本項(xiàng)目采用 WebSocket 作為通信協(xié)議以實(shí)現(xiàn)毫秒級(jí)互動(dòng)體驗(yàn)。
后端實(shí)現(xiàn)詳解(Spring Boot 3)
引入依賴(pom.xml)
<groupId>com.icoderoad</groupId>
<artifactId>danmaku-system</artifactId>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
<version>3.5.5</version>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>
WebSocket 配置
路徑: /src/main/java/com/icoderoad/danmaku/config/WebSocketConfig.java
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
registry.enableSimpleBroker("/topic");
registry.setApplicationDestinationPrefixes("/app");
}
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/ws-danmaku").setAllowedOriginPatterns("*").withSockJS();
}
}
彈幕實(shí)體模型
路徑: /src/main/java/com/icoderoad/danmaku/model/Danmaku.java
@Data
@TableName("danmaku")
public class Danmaku {
@TableId(type = IdType.AUTO)
private Long id;
private String content;
private String color;
private Integer fontSize;
private Double time;
private String videoId;
private String userId;
private String username;
private LocalDateTime createdAt;
}
數(shù)據(jù)傳輸結(jié)構(gòu)(DTO)
路徑: /src/main/java/com/icoderoad/danmaku/dto/DanmakuDTO.java
@Data
public class DanmakuDTO {
private String content;
private String color = "#ffffff";
private Integer fontSize = 24;
private Double time;
private String videoId;
private String userId;
private String username;
}
Mapper 接口
路徑: /src/main/java/com/icoderoad/danmaku/mapper/DanmakuMapper.java
@Mapper
public interface DanmakuMapper extends BaseMapper<Danmaku> {
@Select("SELECT * FROM danmaku WHERE video_id = #{videoId} ORDER BY time ASC")
List<Danmaku> findByVideoIdOrderByTimeAsc(@Param("videoId") String videoId);
@Select("SELECT * FROM danmaku WHERE video_id = #{videoId} AND time BETWEEN #{startTime} AND #{endTime} ORDER BY time ASC")
List<Danmaku> findByVideoIdAndTimeBetween(@Param("videoId") String videoId, @Param("startTime") Double start, @Param("endTime") Double end);
}
服務(wù)邏輯
路徑: /src/main/java/com/icoderoad/danmaku/service/DanmakuService.java
@Service
public class DanmakuService {
@Autowired private DanmakuMapper danmakuMapper;
@Autowired private SimpMessagingTemplate messagingTemplate;
public Danmaku saveDanmaku(DanmakuDTO dto) {
String clean = filterContent(dto.getContent());
Danmaku danmaku = new Danmaku();
danmaku.setContent(clean);
danmaku.setColor(dto.getColor());
danmaku.setFontSize(dto.getFontSize());
danmaku.setTime(dto.getTime());
danmaku.setVideoId(dto.getVideoId());
danmaku.setUserId(dto.getUserId());
danmaku.setUsername(dto.getUsername());
danmaku.setCreatedAt(LocalDateTime.now());
danmakuMapper.insert(danmaku);
messagingTemplate.convertAndSend("/topic/video/" + dto.getVideoId(), danmaku);
return danmaku;
}
public List<Danmaku> getDanmakusByVideoId(String videoId) {
return danmakuMapper.findByVideoIdOrderByTimeAsc(videoId);
}
public List<Danmaku> getDanmakusByTimeRange(String videoId, Double start, Double end) {
return danmakuMapper.findByVideoIdAndTimeBetween(videoId, start, end);
}
private String filterContent(String content) {
String[] blocklist = {"敏感詞1", "敏感詞2"};
for (String word : blocklist) {
content = content.replaceAll(word, "***");
}
return content;
}
}
控制器接口
路徑: /src/main/java/com/icoderoad/danmaku/controller/DanmakuController.java
@RestController
@RequestMapping("/api/danmaku")
public class DanmakuController {
@Autowired private DanmakuService service;
@MessageMapping("/danmaku/send")
public Danmaku push(DanmakuDTO dto) {
return service.saveDanmaku(dto);
}
@GetMapping("/video/{videoId}")
public ResponseEntity<List<Danmaku>> list(@PathVariable String videoId) {
return ResponseEntity.ok(service.getDanmakusByVideoId(videoId));
}
@GetMapping("/video/{videoId}/timerange")
public ResponseEntity<List<Danmaku>> range(@PathVariable String videoId,
@RequestParam Double start,
@RequestParam Double end) {
return ResponseEntity.ok(service.getDanmakusByTimeRange(videoId, start, end));
}
}
Thymeleaf + Bootstrap 優(yōu)化版前端頁面示例
將頁面放置于 /src/main/resources/templates/danmaku.html,供 Thymeleaf 渲染
<!DOCTYPE html>
<html lang="zh" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>實(shí)時(shí)彈幕播放器</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- Bootstrap 5 樣式 -->
<link rel="stylesheet">
<style>
#video-container {
position: relative;
width: 100%;
max-width: 900px;
margin: auto;
}
video {
width: 100%;
height: auto;
}
#danmaku-layer {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
}
.danmaku {
position: absolute;
white-space: nowrap;
font-weight: bold;
animation: danmaku-move 8s linear forwards;
}
@keyframes danmaku-move {
0% {
transform: translateX(100%);
}
100% {
transform: translateX(-100%);
}
}
</style>
</head>
<body>
<div class="container py-4">
<h2 class="text-center mb-4">?? 實(shí)時(shí)彈幕播放器</h2>
<div id="video-container" class="mb-3">
<video id="video" controls th:src="@{/videos/sample.mp4}"></video>
<div id="danmaku-layer"></div>
</div>
<!-- 彈幕輸入?yún)^(qū) -->
<form id="danmaku-form" class="row g-2 align-items-center justify-content-center">
<div class="col-md-4">
<input type="text" class="form-control" id="danmaku-content" placeholder="輸入你的彈幕..." required>
</div>
<div class="col-md-2">
<input type="color" class="form-control form-control-color" id="danmaku-color" value="#ffffff" title="選擇顏色">
</div>
<div class="col-md-2">
<input type="number" class="form-control" id="danmaku-size" value="24" min="12" max="48" title="字體大小">
</div>
<div class="col-md-2">
<button type="submit" class="btn btn-primary w-100">發(fā)送彈幕</button>
</div>
</form>
</div>
<!-- SockJS + STOMP -->
<script src="https://cdn.jsdelivr.net/npm/sockjs-client@1.6.1/dist/sockjs.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/stompjs@2.3.3/lib/stomp.min.js"></script>
<script>
const video = document.getElementById("video");
const danmakuLayer = document.getElementById("danmaku-layer");
const stompClient = Stomp.over(new SockJS("/ws-danmaku"));
stompClient.connect({}, function () {
stompClient.subscribe("/topic/video/demo", function (message) {
const danmaku = JSON.parse(message.body);
renderDanmaku(danmaku);
});
});
document.getElementById("danmaku-form").addEventListener("submit", function (e) {
e.preventDefault();
const content = document.getElementById("danmaku-content").value.trim();
const color = document.getElementById("danmaku-color").value;
const fontSize = parseInt(document.getElementById("danmaku-size").value) || 24;
if (!content) return;
const danmaku = {
content: content,
color: color,
fontSize: fontSize,
time: video.currentTime,
videoId: "demo",
userId: "user123",
username: "訪客"
};
stompClient.send("/app/danmaku/send", {}, JSON.stringify(danmaku));
document.getElementById("danmaku-form").reset();
});
function renderDanmaku(d) {
const span = document.createElement("span");
span.className = "danmaku";
span.textContent = d.content;
span.style.color = d.color || "#fff";
span.style.fontSize = (d.fontSize || 24) + "px";
span.style.top = Math.random() * 80 + "%";
danmakuLayer.appendChild(span);
// 清理彈幕
setTimeout(() => danmakuLayer.removeChild(span), 8000);
}
</script>
</body>
</html>
結(jié)合 STOMP 協(xié)議與 SockJS 客戶端即可建立彈幕推送通道。
結(jié)語
通過本項(xiàng)目,我們以 Spring Boot 3 為核心技術(shù)棧,構(gòu)建了支持 WebSocket 實(shí)時(shí)通信的彈幕系統(tǒng)。該系統(tǒng)架構(gòu)清晰、可擴(kuò)展性強(qiáng),適用于視頻平臺(tái)、直播系統(tǒng)、虛擬課堂等多種場(chǎng)景。未來可進(jìn)一步擴(kuò)展彈幕審核、用戶等級(jí)體系、彈幕樣式個(gè)性化等高級(jí)功能,以構(gòu)建更加豐富的互動(dòng)體驗(yàn)。