RTSP视频流播放系统

RTSP视频流播放系统

📋 目录

  1. 系统架构概述
  2. 核心技术原理
  3. 数据流转过程
  4. 后端接口设计
  5. 前端实现
  6. 关键技术细节
  7. 问题与解决方案

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 播放视频

  1. 选择设备
  2. 选择通道
  3. 点击"开始播放"
  4. 观看实时画面

10. 总结

10.1 核心技术栈

复制代码
摄像头(RTSP/H.264)
    ↓
Spring Boot + iot-communication (RTSP客户端 + FMP4转换)
    ↓
WebSocket (二进制流传输)
    ↓
JavaScript + MSE (队列机制 + SourceBuffer)
    ↓
HTML5 Video (实时播放)

10.2 关键创新点

  1. Token预授权机制 - 简化前端流程,保护敏感信息
  2. 队列驱动架构 - 解决MSE并发问题
  3. 智能延迟控制 - 保证实时性
  4. 自动错误恢复 - 提高稳定性

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: 当前实现仅支持实时流。录像需要在后端添加存储模块,回放需要支持时间轴查询。


相关推荐
gelald9 小时前
ReentrantLock 学习笔记
java·后端
计算机学姐9 小时前
基于SpringBoot的校园资源共享系统【个性化推荐算法+数据可视化统计】
java·vue.js·spring boot·后端·mysql·spring·信息可视化
一条咸鱼_SaltyFish9 小时前
[Day15] 若依框架二次开发改造记录:定制化之旅 contract-security-ruoyi
java·大数据·经验分享·分布式·微服务·架构·ai编程
跟着珅聪学java9 小时前
JavaScript 底层原理
java·开发语言
Mr. Cao code9 小时前
Docker数据管理:持久化存储最佳实践
java·docker·容器
强子感冒了9 小时前
Java 学习笔记:File类核心API详解与使用指南
java·笔记·学习
spencer_tseng9 小时前
eclipse ALT+SHIFT+A
java·ide·eclipse
vyuvyucd9 小时前
C++排序算法全解析
java·数据结构·算法
juelianhuayao9 小时前
Git错误提交后如何快速删除本次commit
git