別只會聊天室!用 Spring Boot 3 玩出酷炫實時彈幕特效
在當今的視頻平臺和直播場景中,彈幕技術成為提升用戶參與度與互動體驗的關鍵工具。彈幕通過實時渲染觀眾評論在視頻播放界面中橫向滾動顯示,不僅增強了沉浸感,也營造了社區式觀影氛圍。本文將基于 Spring Boot 3 構建一個具備實時通信、內容過濾與歷史記錄能力的彈幕系統。
系統功能概覽
功能定義
彈幕系統允許用戶將文字信息實時發送至正在播放的視頻畫面中。內容通常在視頻上層以從右至左方式動態滾動,呈現同步評論的視覺效果。
主要特征
- 低延遲推送用戶評論可在毫秒級別廣播至所有連接終端;
- 強交互性評論即時可見,營造出“陪伴觀影”的社交感;
- 內容時間綁定彈幕多與視頻時間點匹配,有助于信息歸檔與回放分析;
- 視覺層沖擊批量彈幕可呈現獨特動態視覺表現。
技術架構設計
系統構成
系統由以下核心模塊組成:
- 前端播放器負責視頻呈現與彈幕展示;
- WebSocket 推送引擎實現低延遲的實時消息廣播;
- 持久化存儲模塊記錄用戶彈幕數據,支持回放及分析;
- 內容審查組件確保發送信息符合平臺規范。
協議選型分析
在實時推送技術方案中,以下協議可供選擇:
協議 | 優點 | 局限性 | 推薦場景 |
WebSocket | 全雙工、低延遲、兼容廣 | 需維持長連接,資源占用較高 | 高實時性場景,如彈幕直播 |
SSE | 實現簡單,適合單向推送 | 僅支持服務器向客戶端 | 新聞推送、股票刷新 |
Long Polling | 通用性強 | 實時性差,耗資源 | 極端兼容場景或降級備選方案 |
本項目采用 WebSocket 作為通信協議以實現毫秒級互動體驗。
后端實現詳解(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();
}
}
彈幕實體模型
路徑: /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;
}
數據傳輸結構(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);
}
服務邏輯
路徑: /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 優化版前端頁面示例
將頁面放置于 /src/main/resources/templates/danmaku.html,供 Thymeleaf 渲染
<!DOCTYPE html>
<html lang="zh" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>實時彈幕播放器</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">?? 實時彈幕播放器</h2>
<div id="video-container" class="mb-3">
<video id="video" controls th:src="@{/videos/sample.mp4}"></video>
<div id="danmaku-layer"></div>
</div>
<!-- 彈幕輸入區 -->
<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">發送彈幕</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>
結合 STOMP 協議與 SockJS 客戶端即可建立彈幕推送通道。
結語
通過本項目,我們以 Spring Boot 3 為核心技術棧,構建了支持 WebSocket 實時通信的彈幕系統。該系統架構清晰、可擴展性強,適用于視頻平臺、直播系統、虛擬課堂等多種場景。未來可進一步擴展彈幕審核、用戶等級體系、彈幕樣式個性化等高級功能,以構建更加豐富的互動體驗。