在互联网视频娱乐蓬勃发展的当下,实时弹幕系统早已从锦上添花的附加功能,进化为现代视频网站和直播平台不可或缺的核心组件。
它打破了传统单向的观看模式,让观众在沉浸于视频内容的同时,能即时发送评论。这些承载着用户观点、情绪与共鸣的弹幕,以横向滚动的形式穿梭于视频画面之上,不仅显著提升了内容的互动性,更让每个观看者都能感受到 “不是一个人在追剧 / 看直播” 的社区参与感,形成独特的群体观看氛围。
跟着我学习完之后你能获得如下成果:
接下来,我将带领大家进行技术实战,详细演示如何运用 SpringBoot 框架,一步步搭建起一个实时弹幕系统。
一、什么是实时弹幕系统
在弹幕系统中,用户发送的评论会即时呈现在视频画面上。这些评论从右向左横向滚动,让所有观众都能直观看到他人的实时反馈,实现高效的互动交流。
- 实时性:弹幕毫秒级推送至客户端,实现即时显示。
- 互动性:用户评论实时可见,支持观点即时回应,构建集体观看场景。
- 时间关联性:弹幕携带时间戳,通过算法精准对应视频播放时间点。
- 视觉冲击力:系统控制弹幕密度与流速,形成视觉效果的同时避免遮挡画面。
二、技术设计
2.1 整体架构
我们本次开发的实时弹幕系统主要包含以下:
- 前端播放器:实现视频播放,通过 HTML5 Canvas 或 CSS 动画展示弹幕。
- WebSocket 服务:基于 WebSocket 协议,实现弹幕消息实时双向传输。
- 弹幕存储:使用数据库或分布式存储,保存弹幕内容、时间及关联时间戳。
- 内容过滤组件:利用 NLP 技术和关键词算法,实时过滤不良弹幕内容。
2.2 通信协议
想要实现本次的实时弹幕系统,首先需要选择一个适合的通信协议。例如:
| 协议 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| WebSocket | 全双工通信,低延迟,广泛支持 | 需要服务器保持连接,资源消耗较大 | 实时性要求高的场景 |
| SSE (Server-Sent Events) | 服务器推送,简单实现 | 只支持服务器到客户端的单向通信 | 服务器推送更新场景 |
| 长轮询 (Long Polling) | 兼容性好,实现简单 | 效率低,延迟高 | 兼容性要求高的场景 |
最后我选择使用 SpringBoot 和 WebSocket 技术去实现。
三、后端代码实现
3.1 添加 maven 依赖
在 pom.xml 中添加:
<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>
<optional>true</optional>
</dependency>
</dependencies>
3.2 创建 WebSocket 配置类
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;
/**
* WebSocket 配置类
* 配置 STOMP 协议和消息代理,支持弹幕功能
*/
@Configuration
@EnableWebSocketMessageBroker // 启用基于 WebSocket 的消息代理
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
// 消息代理主题前缀(客户端订阅用)
private static final String BROKER_TOPIC_PREFIX = "/topic";
// 应用目标前缀(服务端消息处理用)
private static final String APPLICATION_DESTINATION_PREFIX = "/app";
// WebSocket 连接端点路径
private static final String WEBSOCKET_CONNECTION_ENDPOINT = "/ws-barrage";
/**
* 配置消息代理设置
* @param registry 消息代理注册器
*/
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
// 启用简单内存消息代理,客户端可订阅这些主题接收消息
registry.enableSimpleBroker(BROKER_TOPIC_PREFIX);
// 设置应用目标前缀,过滤需要服务端处理的消息
registry.setApplicationDestinationPrefixes(APPLICATION_DESTINATION_PREFIX);
}
/**
* 注册 STOMP 协议端点
* @param registry STOMP 端点注册器
*/
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint(WEBSOCKET_CONNECTION_ENDPOINT)
.setAllowedOriginPatterns("*") // 生产环境应限制为特定来源
.withSockJS(); // 启用 SockJS 回退支持
}
}
3.3 定义弹幕消息模型类
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.time.LocalDateTime;
/**
* 弹幕信息实体类
* 对应数据库中的 Barrage 表
*/
@Data
@TableName("barrage")
public class Barrage {
/**
* 主键ID,自增
*/
@TableId(type = IdType.AUTO)
private Long id;
/**
* 弹幕内容,不允许为空
*/
@TableField(value = "content", strategy = FieldStrategy.NOT_EMPTY)
private String content;
/**
* 弹幕颜色,十六进制颜色码(如:#FFFFFF)
*/
@TableField("color")
private String color;
/**
* 字体大小(单位:px)
*/
@TableField("font_size")
private Integer fontSize;
/**
* 弹幕出现的时间点(单位:秒,支持小数)
*/
@TableField("time")
private Double time;
/**
* 关联的视频ID
*/
@TableField("video_id")
private String videoId;
/**
* 发送用户ID
*/
@TableField("user_id")
private String userId;
/**
* 用户名(冗余存储,避免频繁联表查询)
*/
@TableField("username")
private String username;
/**
* 创建时间,自动填充
*/
@TableField(value = "created_time", fill = FieldFill.INSERT)
private LocalDateTime createdTime;
}
3.4 定义弹幕消息传输对象类
import lombok.Data;
/**
* 弹幕消息传输对象(DTO)
* 用于客户端与服务端之间的弹幕数据传输
*/
@Data
public class BarrageDTO {
/**
* 弹幕文本内容(必填)
*/
private String content;
/**
* 弹幕颜色(十六进制颜色码)
* 默认值:白色(#FFFFFF)
*/
private String color = "#FFFFFF";
/**
* 弹幕字体大小(单位:像素)
* 默认值:25px
*/
private Integer fontSize = 25;
/**
* 弹幕出现时间点(单位:秒,精确到小数点后3位)
*/
private Double time;
/**
* 关联的视频ID(必填)
*/
private String videoId;
/**
* 发送用户ID(必填)
*/
private String userId;
/**
* 发送用户昵称/用户名
*/
private String username;
}
3.5 定义 Mapper 接口
/**
* 弹幕数据访问接口
* 提供弹幕数据的CRUD及自定义查询操作
*/
@Mapper
public interface BarrageMapper extends BaseMapper<Barrage> {
/**
* 根据视频ID查询所有弹幕(按时间升序排序)
*
* @param videoId 视频ID(不能为空)
* @return 按时间排序的弹幕列表
*/
@Select("SELECT id, content, color, font_size, time, video_id, user_id, username, created_time " +
"FROM barrage " +
"WHERE video_id = #{videoId} " +
"ORDER BY time ASC")
List<Barrage> selectByVideoIdOrderByTimeAsc(@Param("videoId") String videoId);
/**
* 根据视频ID和时间范围查询弹幕(按时间升序排序)
*
* @param videoId 视频ID(不能为空)
* @param startTime 起始时间(单位:秒)
* @param endTime 结束时间(单位:秒)
* @return 指定时间范围内的弹幕列表
*/
@Select("SELECT id, content, color, font_size, time, video_id, user_id, username, created_time " +
"FROM barrage " +
"WHERE video_id = #{videoId} " +
"AND time BETWEEN #{startTime} AND #{endTime} " +
"ORDER BY time ASC")
List<Barrage> selectByVideoIdAndTimeRange(
@Param("videoId") String videoId,
@Param("startTime") Double startTime,
@Param("endTime") Double endTime);
}
3.6 弹幕服务实现类
/**
* 弹幕服务类
* 处理弹幕相关的业务逻辑,包括保存、查询和实时推送
*/
@Service
public class BarrageService {
private final BarrageMapper barrageMapper;
private final SimpMessagingTemplate messagingTemplate;
@Autowired
public BarrageService(BarrageMapper barrageMapper, SimpMessagingTemplate messagingTemplate) {
this.barrageMapper = barrageMapper;
this.messagingTemplate = messagingTemplate;
}
/**
* 保存并广播弹幕
* @param barrageDto 弹幕数据传输对象
* @return 保存后的弹幕实体
*/
public Barrage saveAndBroadcastBarrage(BarrageDTO barrageDto) {
// 内容过滤(简单示例)
String filteredContent = filterContent(barrageDto.getContent());
// 创建弹幕实体
Barrage barrage = new Barrage();
barrage.setContent(filteredContent);
barrage.setColor(barrageDto.getColor());
barrage.setFontSize(barrageDto.getFontSize());
barrage.setTime(barrageDto.getTime());
barrage.setVideoId(barrageDto.getVideoId());
barrage.setUserId(barrageDto.getUserId());
barrage.setUsername(barrageDto.getUsername());
barrage.setCreatedTime(LocalDateTime.now());
// 保存到数据库
barrageMapper.insert(barrage);
// 通过WebSocket广播到客户端
messagingTemplate.convertAndSend("/topic/video/" + barrage.getVideoId(), barrage);
return barrage;
}
/**
* 根据视频ID获取弹幕列表(按时间升序排序)
* @param videoId 视频ID
* @return 弹幕列表
*/
public List<Barrage> getBarragesByVideoId(String videoId) {
return barrageMapper.selectByVideoIdOrderByTimeAsc(videoId);
}
/**
* 根据视频ID和时间范围获取弹幕列表
* @param videoId 视频ID
* @param startTime 起始时间(秒)
* @param endTime 结束时间(秒)
* @return 弹幕列表
*/
public List<Barrage> getBarragesByVideoIdAndTimeRange(
String videoId, Double startTime, Double endTime) {
return barrageMapper.selectByVideoIdAndTimeRange(videoId, startTime, endTime);
}
/**
* 弹幕内容过滤
* @param content 原始内容
* @return 过滤后的内容
*/
private String filterContent(String content) {
// 实际应用中这里可能会有更复杂的过滤逻辑
String[] sensitiveWords = {"敏感词1", "敏感词2", "敏感词3"};
String filtered = content;
for (String word : sensitiveWords) {
filtered = filtered.replaceAll(word, "***");
}
return filtered;
}
}
3.7 弹幕接口控制器类
/**
* 弹幕控制器
* 提供弹幕相关的WebSocket和REST API接口
*/
@RestController
@RequestMapping("/api/barrage")
public class BarrageController {
private final BarrageService barrageService;
@Autowired
public BarrageController(BarrageService barrageService) {
this.barrageService = barrageService;
}
/**
* 发送弹幕(WebSocket接口)
* @param barrageDto 弹幕数据传输对象
* @return 保存后的弹幕实体
*/
@MessageMapping("/barrage/send")
public Barrage sendBarrage(@RequestBody BarrageDTO barrageDto) {
return barrageService.saveAndBroadcastBarrage(barrageDto);
}
/**
* 获取视频的所有弹幕(REST API)
* @param videoId 视频ID
* @return 弹幕列表响应实体
*/
@GetMapping("/video/{videoId}")
public ResponseEntity<List<Barrage>> getBarragesByVideoId(
@PathVariable String videoId) {
List<Barrage> barrages = barrageService.getBarragesByVideoId(videoId);
return ResponseEntity.ok(barrages);
}
/**
* 获取视频指定时间范围内的弹幕(REST API)
* @param videoId 视频ID
* @param start 起始时间(秒)
* @param end 结束时间(秒)
* @return 弹幕列表响应实体
*/
@GetMapping("/video/{videoId}/time-range")
public ResponseEntity<List<Barrage>> getBarragesByTimeRange(
@PathVariable String videoId,
@RequestParam Double start,
@RequestParam Double end) {
List<Barrage> barrages = barrageService.getBarragesByVideoIdAndTimeRange(videoId, start, end);
return ResponseEntity.ok(barrages);
}
}
四、前端代码实现
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>弹幕视频播放器</title>
<style>
/* 基础样式重置 */
body {
margin: 0;
padding: 0;
min-height: 100vh;
background: linear-gradient(135deg, #232526 0%, #414345 100%);
font-family: 'Segoe UI', 'Microsoft YaHei', sans-serif;
}
/* 视频容器样式 - 包含视频和弹幕层 */
#video-container {
position: relative;
width: 800px;
height: 450px;
margin: 40px auto 0 auto;
background-color: #000;
overflow: hidden;
border-radius: 18px;
box-shadow: 0 8px 32px 0 rgba(31, 38, 135, 0.37);
}
/* 视频播放器样式 */
#video-player {
width: 100%;
height: 100%;
border-radius: 18px;
}
/* 弹幕容器 - 覆盖在视频上方但不影响视频操作 */
#barrage-container {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none; /* 允许点击穿透到视频 */
}
/* 单条弹幕样式 */
.barrage {
position: absolute;
white-space: nowrap;
font-family: "Microsoft YaHei", sans-serif;
font-weight: bold;
text-shadow:
-1px -1px 0 #000,
1px -1px 0 #000,
-1px 1px 0 #000,
1px 1px 0 #000;
animation-name: barrage-move;
animation-timing-function: linear;
animation-fill-mode: forwards;
will-change: transform; /* 优化动画性能 */
}
/* 弹幕移动动画 */
@keyframes barrage-move {
from {
transform: translateX(100%);
}
to {
transform: translateX(-100%);
}
}
/* 弹幕表单容器 */
.barrage-form-wrapper {
width: 800px;
max-width: 95vw;
margin: 28px auto 0 auto;
display: flex;
justify-content: center;
}
/* 弹幕输入表单样式 */
#barrage-form {
width: 100%;
display: flex;
justify-content: center;
align-items: center;
gap: 12px;
background: rgba(255, 255, 255, 0.10);
box-shadow: 0 4px 16px 0 rgba(31, 38, 135, 0.17);
border-radius: 12px;
padding: 16px 12px 12px 12px;
backdrop-filter: blur(4px); /* 毛玻璃效果 */
}
/* 弹幕输入框 */
#barrage-input {
width: 40%;
min-width: 120px;
max-width: 320px;
padding: 10px 14px;
border-radius: 6px;
border: 1px solid #d1d5db;
outline: none;
font-size: 16px;
background: rgba(255, 255, 255, 0.85);
transition: border 0.2s;
}
/* 输入框聚焦状态 */
#barrage-input:focus {
border: 1.5px solid #1890ff;
}
/* 颜色选择器 */
#color-picker {
width: 38px;
height: 38px;
border: none;
background: none;
cursor: pointer;
border-radius: 6px;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08);
}
/* 字体大小选择器 */
#font-size {
padding: 8px 10px;
border-radius: 6px;
border: 1px solid #d1d5db;
background: rgba(255, 255, 255, 0.85);
font-size: 16px;
outline: none;
transition: border 0.2s;
}
/* 发送按钮样式 */
#send-btn {
padding: 10px 24px;
background: linear-gradient(90deg, #1890ff 0%, #40a9ff 100%);
color: white;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 16px;
font-weight: bold;
box-shadow: 0 2px 8px rgba(24, 144, 255, 0.15);
transition: background 0.2s, transform 0.1s;
}
/* 发送按钮悬停效果 */
#send-btn:hover {
background: linear-gradient(90deg, #40a9ff 0%, #1890ff 100%);
transform: translateY(-2px) scale(1.04);
}
/* 响应式设计 - 平板及以下设备 */
@media (max-width: 900px) {
#video-container,
.barrage-form-wrapper {
width: 98vw;
min-width: 0;
}
}
/* 响应式设计 - 手机设备 */
@media (max-width: 600px) {
#video-container,
.barrage-form-wrapper {
width: 100vw;
min-width: 0;
border-radius: 0;
}
#barrage-form {
flex-direction: column;
gap: 8px;
padding: 10px 2vw 8px 2vw;
}
#barrage-input {
width: 90vw;
max-width: 98vw;
}
}
</style>
</head>
<body>
<!-- 视频播放区域 -->
<div id="video-container">
<!-- 视频播放器 -->
<video id="video-player" controls>
<source src="code.mp4" type="video/mp4">
您的浏览器不支持 video 标签。
</video>
<!-- 弹幕显示层 -->
<div id="barrage-container"></div>
</div>
<!-- 弹幕控制表单 -->
<div class="barrage-form-wrapper">
<form id="barrage-form" onsubmit="return false;">
<!-- 弹幕内容输入框 -->
<input type="text" id="barrage-input" placeholder="发送弹幕..." maxlength="100">
<!-- 弹幕颜色选择器 -->
<input type="color" id="color-picker" value="#ffffff" title="选择弹幕颜色">
<!-- 弹幕字体大小选择 -->
<select id="font-size">
<option value="18">小</option>
<option value="24" selected>中</option>
<option value="30">大</option>
</select>
<!-- 发送按钮 -->
<button id="send-btn" type="submit">发送</button>
</form>
</div>
<!-- 引入WebSocket相关库 -->
<script src="https://cdn.jsdelivr.net/npm/sockjs-client@1.5.0/dist/sockjs.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/stompjs@2.3.3/lib/stomp.min.js"></script>
<!-- 弹幕业务逻辑脚本 -->
<script src="barrage.js"></script>
</body>
</html>
由于篇幅有限,本文只展示了部分核心代码,想要完整代码学习的同学 可以找我免费领取。