RTSP视频流播放系统
📋 目录
1. 系统架构概述
1.1 整体架构图
┌─────────────┐ ┌──────────────┐ ┌─────────────┐ ┌──────────────┐
│ 摄像头 │─────→│ Spring Boot │─────→│ WebSocket │─────→│ 浏览器 │
│ (RTSP) │ RTSP │ 后端服务 │ FMP4 │ 传输层 │ MSE │ (HTML5) │
└─────────────┘ └──────────────┘ └─────────────┘ └──────────────┘
↓ ↓ ↓ ↓
H.264编码 RTSP→FMP4转换 二进制流传输 MSE解码播放
1.2 技术栈
后端:
- Spring Boot 2.5.15
- WebSocket (javax.websocket)
- iot-communication 1.5.3 (RTSP客户端)
- Spring Security (认证授权)
前端:
- HTML5 + JavaScript (ES6)
- MediaSource Extensions (MSE) API
- WebSocket API
- Fetch API (REST调用)
2. 核心技术原理
2.1 RTSP协议
RTSP (Real-Time Streaming Protocol) 是应用层协议,用于控制流媒体服务器。
rtsp://username:password@ip:port/path
例如: rtsp://admin:QJvcms2003@192.168.1.64:554/Streaming/Channels/101
RTSP握手流程:
客户端 → 服务器: OPTIONS (查询支持的方法)
客户端 → 服务器: DESCRIBE (获取媒体描述SDP)
客户端 → 服务器: SETUP (建立传输会话)
客户端 → 服务器: PLAY (开始播放)
服务器 → 客户端: RTP数据包 (H.264视频流)
2.2 H.264编码
摄像头输出的视频格式,包含:
- I帧 (关键帧): 完整图像
- P帧 (预测帧): 参考前一帧的差异
- B帧 (双向帧): 参考前后帧
2.3 FMP4格式
FMP4 (Fragmented MP4) 是适合流媒体传输的MP4格式。
结构:
┌─────────────────────┐
│ ftyp (文件类型) │
├─────────────────────┤
│ moov (初始化段) │ ← 一次性发送
│ - mvhd (头信息) │
│ - trak (轨道信息) │
├─────────────────────┤
│ moof (媒体片段) │ ← 持续发送
│ - mfhd │
│ - traf │
├─────────────────────┤
│ mdat (媒体数据) │
└─────────────────────┘
2.4 MSE (Media Source Extensions)
浏览器API,允许JavaScript生成媒体流供<video>播放。
核心组件:
- MediaSource: 媒体源对象
- SourceBuffer: 数据缓冲区
- Video Element: HTML5视频元素
3. 数据流转过程
3.1 完整流程图
┌─────────┐
│ 1. 用户 │
│ 点击播放 │
└────┬────┘
│
▼
┌─────────────────────────────────────────────┐
│ 2. 前端调用 REST API │
│ GET /equipment/channel/stream/{id}/{ch} │
└────┬────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────┐
│ 3. 后端生成 Token 和 WebSocket URL │
│ - 创建 streamToken │
│ - 返回 ws://host/rtsp/stream?token=xxx │
└────┬────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────┐
│ 4. 前端建立 WebSocket 连接 │
│ - 连接到 ws://host/rtsp/stream?token=xxx │
│ - 后端验证 Token │
└────┬────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────┐
│ 5. 后端启动 RTSP 客户端 │
│ - 连接摄像头 (RTSP) │
│ - 认证 (Digest Auth) │
│ - 接收 H.264/RTP 流 │
└────┬────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────┐
│ 6. 后端转换 RTSP → FMP4 │
│ - RtspFMp4Proxy 处理 │
│ - 生成编码信息 (JSON) │
│ - 生成 FMP4 片段 (二进制) │
└────┬────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────┐
│ 7. WebSocket 推送数据 │
│ - 文本消息: 编码信息 (一次) │
│ {"mimeType":"video/mp4; codecs=..."} │
│ - 二进制消息: FMP4 数据 (持续) │
└────┬────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────┐
│ 8. 前端接收并处理 │
│ - 解析编码信息 → 创建 SourceBuffer │
│ - FMP4 数据 → 放入队列 │
│ - 队列 → appendBuffer │
└────┬────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────┐
│ 9. MSE 解码播放 │
│ - SourceBuffer 解码 │
│ - 渲染到 <video> 元素 │
│ - 用户看到实时画面 │
└─────────────────────────────────────────────┘
3.2 详细时序图
摄像头 后端 前端 用户 摄像头 后端 前端 用户 1. 点击播放 2. GET /equipment/channel/stream/12/1 3. 创建Token + 构建RTSP URL 4. 返回 {wsUrl, token} 5. 创建MediaSource + SourceBuffer 6. WebSocket连接 (带Token) 7. 验证Token 8. RTSP连接请求 9. RTSP响应 + RTP流 10. H.264/RTP → FMP4转换 11. 发送编码信息 (文本) 12. 创建SourceBuffer 13. 持续发送FMP4数据 (二进制) 14. 队列 → appendBuffer → 播放 15. 看到实时画面 ✓
4. 后端接口设计
4.1 REST API 接口
4.1.1 获取设备列表
http
GET /dev-api/equipment/list?pageNum=1&pageSize=10
Authorization: {token}
响应:
json
{
"code": 200,
"msg": "查询成功",
"data": {
"list": [
{
"id": 12,
"deviceName": "监控摄像头1",
"ipAddress": "192.168.1.64",
"port": 554,
"channelList": ["1", "2"]
}
]
}
}
4.1.2 获取设备详情
http
GET /dev-api/equipment/detail/{id}
Authorization: {token}
响应:
json
{
"code": 200,
"data": {
"id": 12,
"deviceName": "监控摄像头1",
"ipAddress": "192.168.1.64",
"port": 554,
"accountName": "admin",
"password": "加密后的密码",
"channelList": ["1", "2"]
}
}
4.1.3 获取流地址(核心接口)
http
GET /dev-api/equipment/channel/stream/{equipmentId}/{channelCode}
GET /dev-api/equipment/channel/stream/{equipmentId}
Authorization: {token}
请求示例:
GET /dev-api/equipment/channel/stream/12/1
响应:
json
{
"code": 200,
"msg": "查询成功",
"data": {
"wsUrl": "ws://localhost:8080/dev-api/rtsp/stream?token=stream_12_1_1736473692348",
"streamToken": "stream_12_1_1736473692348",
"equipmentId": 12,
"channelCode": "1",
"expireTime": 600
}
}
4.1.4 停止流
http
POST /dev-api/equipment/rtsp/stop/{equipmentId}
Authorization: {token}
响应:
json
{
"code": 200,
"msg": "成功停止2个流"
}
4.2 WebSocket 接口
4.2.1 连接端点
ws://host:port/dev-api/rtsp/stream?token={streamToken}
4.2.2 消息格式
服务端 → 客户端 (文本消息):
json
{
"mimeType": "video/mp4; codecs=\"avc1.4d0032\""
}
服务端 → 客户端 (二进制消息):
ArrayBuffer: FMP4 媒体数据
大小: 通常 1KB - 100KB 每个片段
频率: 持续发送,根据视频帧率
客户端 → 服务端:
"stop" - 停止推流
5. 前端实现
5.1 核心类: RtspPlayer
javascript
class RtspPlayer {
constructor(videoElement) {
this.videoElement = videoElement;
this.ws = null;
this.mediaSource = null;
this.sourceBuffer = null;
this.queue = []; // 数据队列 (关键!)
this.canFeed = false; // 控制标志
}
async play(wsUrl) {
// 1. 连接WebSocket
await this.connectWebSocket(wsUrl);
// 2. 等待编码信息
// 3. 创建MediaSource
// 4. 开始接收数据
}
handleCodecInfo(codecJson) {
// 1. 解析编码信息
// 2. 创建MediaSource和SourceBuffer
// 3. 监听updateend事件
}
feedNext() {
// 1. 从队列取数据
// 2. appendBuffer到SourceBuffer
// 3. 等待updateend事件
}
stop() {
// 1. 关闭WebSocket
// 2. 清理MediaSource
// 3. 重置状态
}
}
5.2 关键代码片段
5.2.1 创建MediaSource
javascript
this.mediaSource = new MediaSource();
this.mediaSource.addEventListener('sourceopen', () => {
this.sourceBuffer = this.mediaSource.addSourceBuffer(mimeType);
// 核心: updateend事件驱动
this.sourceBuffer.addEventListener('updateend', () => {
this.removeBuffer(); // 清理旧数据
this.processDelay(); // 处理延迟
this.canFeed = true; // 允许喂数据
this.feedNext(); // 处理下一包
});
this.canFeed = true;
});
this.videoElement.src = URL.createObjectURL(this.mediaSource);
5.2.2 数据队列处理
javascript
// WebSocket接收数据
ws.onmessage = (event) => {
if (typeof event.data === 'string') {
// 编码信息
this.handleCodecInfo(event.data);
} else {
// 视频数据 - 放入队列
const data = new Uint8Array(event.data);
this.queue.push(data);
// 立即尝试处理
if (this.canFeed) {
this.feedNext();
}
}
};
// 喂数据方法
feedNext() {
if (!this.queue.length) return;
if (!this.sourceBuffer || this.sourceBuffer.updating) return;
if (!this.canFeed) return;
const data = this.queue.shift();
this.sourceBuffer.appendBuffer(data);
this.canFeed = false; // 等待updateend
}
5.2.3 延迟处理
javascript
processDelay() {
if (!this.sourceBuffer || !this.sourceBuffer.buffered.length) return;
const end = this.sourceBuffer.buffered.end(this.sourceBuffer.buffered.length - 1);
const current = this.videoElement.currentTime;
// 延迟超过1.8秒,跳转到最新位置
if (Math.abs(end - current) >= 1.8) {
this.videoElement.currentTime = end - 0.01;
console.log('调整播放位置以减少延迟');
}
}
6. 关键技术细节
6.1 Token预授权机制
目的: 简化前端流程,将RTSP URL等敏感信息保留在后端。
流程:
java
// 1. 前端请求流地址
GET /equipment/channel/stream/12/1
// 2. 后端生成Token
String token = "stream_12_1_" + System.currentTimeMillis();
StreamInfo info = new StreamInfo(rtspUrl, equipmentId, channelCode);
STREAM_TOKEN_MAP.put(token, info); // 存储10分钟
// 3. 返回WebSocket URL
ws://host/rtsp/stream?token=stream_12_1_xxx
// 4. WebSocket连接时验证Token
@OnOpen
public void onOpen(Session session) {
String token = getQueryParam(session, "token");
StreamInfo info = STREAM_TOKEN_MAP.remove(token);
if (info != null && !expired(info)) {
startStream(session, info.getRtspUrl(), info.getEquipmentId());
}
}
6.2 RTSP到FMP4转换
使用的库 : iot-communication 1.5.3
java
// 1. 创建RTSP客户端
RtspClient rtspClient = new RtspClient(
rtspUri,
authenticator, // Digest认证
ERtspTransportProtocol.TCP // TCP传输
);
// 2. 创建FMP4代理
RtspFMp4Proxy rtspFMp4Proxy = new RtspFMp4Proxy(rtspClient);
// 3. 监听FMP4数据
rtspFMp4Proxy.onFmp4DataHandle(data -> {
session.getBasicRemote().sendBinary(ByteBuffer.wrap(data));
});
// 4. 监听编码信息
rtspFMp4Proxy.onCodecHandle(codec -> {
String json = String.format(
"{\"mimeType\":\"video/mp4; codecs=\\\"%s\\\"\"}",
codec
);
session.getBasicRemote().sendText(json);
});
// 5. 启动
rtspFMp4Proxy.start();
6.3 队列机制原理
为什么需要队列?
MSE的SourceBuffer在处理数据时是异步的:
appendBuffer()调用后,updating标志变为true- 在
updating=true期间,不能再次调用appendBuffer() - 必须等待
updateend事件触发
队列解决方案:
数据到达 → 放入队列 → 等待canFeed=true → feedNext() → appendBuffer()
↑ ↓
└──────────── updateend事件 ← canFeed=true ←────────┘
6.4 Spring Security配置
java
@Bean
protected SecurityFilterChain filterChain(HttpSecurity httpSecurity) {
return httpSecurity
.authorizeHttpRequests((requests) -> {
// WebSocket端点允许匿名访问
requests.antMatchers("/rtsp/stream", "/rtsp/stream/**").permitAll()
// 其他请求需要认证
.anyRequest().authenticated();
})
.build();
}
6.5 WebSocket配置
java
@Configuration
public class WebSocketConfig {
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}
7. 问题与解决方案
7.1 问题列表
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 401认证失败 | 前端未携带token | 添加TokenManager和login.js |
| 404 WebSocket | Security拦截 | permitAll /rtsp/stream |
| 编码信息JSON解析失败 | 后端发送纯字符串 | 包装为JSON格式 |
| SourceBuffer错误 | 数据处理并发问题 | 实现队列机制 |
| 视频卡顿/延迟 | buffer未清理 | 智能清理+延迟调整 |
7.2 性能优化
7.2.1 传输协议选择
java
// UDP: 快但可能丢包
ERtspTransportProtocol.UDP
// TCP: 慢但稳定可靠 (推荐)
ERtspTransportProtocol.TCP ✓
7.2.2 Buffer管理
javascript
// 清理超过120秒的旧数据
if (currentTime - firstStart > 120 && lastEnd > currentTime) {
this.sourceBuffer.remove(firstStart, lastEnd - 10);
}
// 时间戳溢出检测 (47小时)
if (Math.abs(firstStart - lastEnd) > 47000) {
this.sourceBuffer.remove(firstEnd + 10, lastEnd);
}
7.2.3 延迟控制
javascript
// 实时流延迟不应超过1.8秒
if (Math.abs(end - current) >= 1.8) {
this.videoElement.currentTime = end - 0.01;
}
7.3 兼容性
浏览器支持:
- ✅ Chrome 85+
- ✅ Edge 85+
- ✅ Safari 14+
- ❌ Firefox (MSE H.264支持有限)
- ❌ IE 11 (不支持MSE)
编码格式:
- ✅ H.264 (AVC)
- ❌ H.265 (HEVC) - 浏览器支持有限
- ❌ VP8/VP9 - 需要WebRTC
8. 文件清单
8.1 后端文件
aibox-framework/
├── config/
│ ├── WebSocketConfig.java # WebSocket配置
│ └── SecurityConfig.java # Security配置
└── ...
aibox-common/
└── utils/
└── RtspUtil/
└── RtspStreamManager.java # RTSP流管理核心
aibox-admin/
└── controller/
├── equipment/
│ └── EquipmentManagementController.java # 设备管理API
└── rtsp/
└── RtspWebSocketServer.java # WebSocket端点
8.2 前端文件
static/
├── js/
│ ├── api.js # API封装 + Token管理
│ ├── login.js # 登录组件
│ └── rtsp-player.js # 播放器核心 (队列机制)
├── css/
│ └── rtsp-player.css # 样式
├── rtsp-player.html # 单路播放器
├── multi-view.html # 多路监控
└── rtsp-diagnosis.html # 诊断工具
9. 使用指南
9.1 启动应用
bash
cd e:/ProjectOne/AiBox/aibox
mvn clean install -DskipTests
cd aibox-admin
mvn spring-boot:run
9.2 访问页面
http://localhost:8080/dev-api/rtsp-player.html
9.3 登录
- 用户名:
admin - 密码:
admin123 - 无需验证码
9.4 播放视频
- 选择设备
- 选择通道
- 点击"开始播放"
- 观看实时画面
10. 总结
10.1 核心技术栈
摄像头(RTSP/H.264)
↓
Spring Boot + iot-communication (RTSP客户端 + FMP4转换)
↓
WebSocket (二进制流传输)
↓
JavaScript + MSE (队列机制 + SourceBuffer)
↓
HTML5 Video (实时播放)
10.2 关键创新点
- Token预授权机制 - 简化前端流程,保护敏感信息
- 队列驱动架构 - 解决MSE并发问题
- 智能延迟控制 - 保证实时性
- 自动错误恢复 - 提高稳定性
10.3 性能指标
- 延迟: < 2秒
- 帧率: 25-30 FPS
- 带宽: 1-4 Mbps (取决于分辨率)
- 并发: 支持多路同时播放
B. 常见问题
Q: 为什么不直接在video标签src使用RTSP地址?
A: 浏览器原生不支持RTSP协议,必须转换为HTTP流式传输格式(如FMP4)。
Q: 为什么使用WebSocket而不是HTTP?
A: WebSocket是全双工协议,延迟更低,更适合实时流媒体传输。
Q: 可以支持录像和回放吗?
A: 当前实现仅支持实时流。录像需要在后端添加存储模块,回放需要支持时间轴查询。
文档版本 : v1.0
最后更新 : 2026-01-09
作者 : AI Assistant
指标
- 延迟: < 2秒
- 帧率: 25-30 FPS
- 带宽: 1-4 Mbps (取决于分辨率)
- 并发: 支持多路同时播放
附录
A. 参考资料
B. 常见问题
Q: 为什么不直接在video标签src使用RTSP地址?
A: 浏览器原生不支持RTSP协议,必须转换为HTTP流式传输格式(如FMP4)。
Q: 为什么使用WebSocket而不是HTTP?
A: WebSocket是全双工协议,延迟更低,更适合实时流媒体传输。
Q: 可以支持录像和回放吗?
A: 当前实现仅支持实时流。录像需要在后端添加存储模块,回放需要支持时间轴查询。