基于rtsp视频流的监控系统实现方案,包含Java后端和Vue前端代码
时间: 2025-07-24 18:31:32 浏览: 2
<think>我们正在设计一个基于RTSP视频流的监控系统,包含Java后端和Vue前端。主要思路是:后端使用Spring Boot,通过FFmpeg将RTSP流转换为WebSocket或HTTP-FLV流,前端使用flv.js或类似库播放转换后的流。
步骤:
1. 后端:使用Java调用FFmpeg将RTSP流转码为HTTP-FLV流(或WebSocket传输FLV)。
2. 后端:提供HTTP-FLV流服务(或者通过WebSocket发送FLV数据)。
3. 前端:使用flv.js播放HTTP-FLV流。
注意:由于浏览器不支持直接播放RTSP流,因此需要转换。
另一种方案:使用WebRTC,但这里我们采用FFmpeg转码为FLV的方案,因为flv.js兼容性较好且实现相对简单。
具体步骤:
后端(Spring Boot):
1. 引入依赖:Spring Boot Web,以及用于执行FFmpeg命令的库(如commons-exec)。
2. 编写一个服务,启动FFmpeg进程,将RTSP流转换为FLV并通过HTTP-FLV输出。
3. 提供接口管理这些转换进程(启动、停止)。
前端(Vue):
1. 使用flv.js库创建播放器。
2. 将视频流地址指向后端转换后的HTTP-FLV地址。
由于FFmpeg转换需要一定时间,且对服务器资源消耗较大,实际生产环境可能需要使用更专业的流媒体服务器(如SRS),但这里我们演示用Java调用FFmpeg的方式。
代码示例:
后端Java代码:
1. 添加依赖(pom.xml):
```xml
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-exec</artifactId>
<version>1.3</version>
</dependency>
</dependencies>
```
2. 编写FFmpeg命令执行服务:
假设我们有一个RTSP流地址:rtsp://admin:[email protected]:554/Streaming/Channels/101
转换命令示例(将RTSP转成HTTP-FLV):
```bash
ffmpeg -i rtsp://... -c copy -f flv http://localhost:8081/live/stream.flv
```
但是,我们需要动态管理不同的流。因此,我们设计一个服务来管理这些进程。
3. 创建FFmpeg服务类:
```java
import org.apache.commons.exec.CommandLine;
import org.apache.commons.exec.DefaultExecutor;
import org.apache.commons.exec.ExecuteException;
import org.apache.commons.exec.PumpStreamHandler;
import org.springframework.stereotype.Service;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
@Service
public class FFmpegService {
private Map<String, Process> processMap = new HashMap<>();
public void startStream(String streamKey, String rtspUrl, String outputUrl) throws IOException {
// 停止已存在的相同key的流
stopStream(streamKey);
// 构建命令
CommandLine cmdLine = new CommandLine("ffmpeg");
cmdLine.addArgument("-i");
cmdLine.addArgument(rtspUrl);
cmdLine.addArgument("-c:v");
cmdLine.addArgument("copy");
cmdLine.addArgument("-an"); // 去掉音频(可选)
cmdLine.addArgument("-f");
cmdLine.addArgument("flv");
cmdLine.addArgument(outputUrl);
DefaultExecutor executor = new DefaultExecutor();
// 防止等待命令执行完成(异步执行)
executor.setStreamHandler(new PumpStreamHandler(null, null, null));
// 启动进程
Process process = executor.execute(cmdLine);
processMap.put(streamKey, process);
}
public void stopStream(String streamKey) {
Process process = processMap.get(streamKey);
if (process != null) {
process.destroy();
processMap.remove(streamKey);
}
}
}
```
4. 创建REST控制器来启动/停止流:
```java
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class StreamController {
@Autowired
private FFmpegService ffmpegService;
@GetMapping("/start")
public String startStream(@RequestParam String streamKey, @RequestParam String rtspUrl) {
// 假设输出地址是本地的一个HTTP服务器,这里我们使用一个简单的HTTP服务器(如netty)来接收FLV流并发布
// 但为了简化,我们假设有一个HTTP服务器在8081端口接收FLV流
String outputUrl = "http://localhost:8081/live/" + streamKey + ".flv";
try {
ffmpegService.startStream(streamKey, rtspUrl, outputUrl);
return "Stream started";
} catch (IOException e) {
e.printStackTrace();
return "Start failed";
}
}
@GetMapping("/stop")
public String stopStream(@RequestParam String streamKey) {
ffmpegService.stopStream(streamKey);
return "Stream stopped";
}
}
```
注意:上述代码中,我们假设有一个HTTP服务器在8081端口接收FFmpeg推送的FLV流。我们可以使用一个简单的Netty服务器来接收并让前端访问,或者使用现成的支持HTTP-FLV的流媒体服务器(如SRS)。这里为了简化,我们使用一个现成的方案:在Spring Boot中嵌入一个Netty服务器来接收FLV流并支持HTTP-FLV播放。但这样会复杂,因此我们可以使用已有的流媒体服务器(如Nginx+rtmp模块)来接收FFmpeg的推流,然后提供HTTP-FLV服务。
由于时间限制,我们采用另一种方式:使用一个简单的Java HTTP服务器来接收FFmpeg的推流,然后保存为文件,同时支持HTTP访问这些文件。但是这样无法实时,因为FLV文件会不断被覆盖?所以不推荐。
因此,我们调整方案:使用WebSocket将FFmpeg转换后的FLV数据发送到前端。但这样改动较大。
考虑到复杂度,我们改为:使用FFmpeg将RTSP流转码为HLS(m3u8+ts切片),然后前端使用hls.js播放。这样更简单,因为HLS可以直接通过HTTP访问文件。
但用户要求RTSP实时流,HLS有延迟。因此我们还是回到FLV方案,但需要搭建一个支持HTTP-FLV的流媒体服务器。这里我们使用一个开源的Java流媒体服务器:https://github.com/Red5/red5-server 或者使用SRS(但SRS是C++的)。
由于项目要求Java,我们可以使用Red5。但这样会增加部署复杂度。
因此,我们折中:使用FFmpeg将RTSP流转换为RTMP流,推送到本地Nginx(带rtmp模块)服务器,然后Nginx将RTMP流转换为HTTP-FLV流。这样前端就可以通过flv.js播放HTTP-FLV流。
但是,这需要安装Nginx和配置rtmp模块。对于纯Java方案,我们可以考虑使用一个Java的RTMP服务器,然后将其转换为HTTP-FLV。但这样的库不多。
因此,我们改变后端方案:不直接使用FFmpeg推HTTP-FLV(因为需要接收服务器),而是使用WebSocket传输。我们可以使用netty-websocket-spring-boot-starter来建立WebSocket服务器,然后让FFmpeg输出到标准输出,Java读取FFmpeg的输出并通过WebSocket发送给前端。前端使用flv.js的WebSocket模式。
步骤:
1. 启动FFmpeg进程,将其输出设置为管道(输出到stdout)。
2. Java读取FFmpeg进程的stdout,将FLV数据通过WebSocket发送给所有连接的客户端。
3. 前端通过WebSocket接收FLV数据,用flv.js播放。
这样我们就避免了中间服务器。
我们调整FFmpeg命令:
```bash
ffmpeg -i rtsp://... -c copy -an -f flv -
```
注意最后的“-”表示输出到stdout。
然后Java读取这个进程的输出流,将数据发送给WebSocket客户端。
由于FLV数据是流式的,我们需要实时读取并发送。
我们重新设计:
后端:
1. 创建WebSocket端点。
2. 对于每个RTSP流,启动一个FFmpeg进程,读取其输出流,将数据发送给所有订阅该流的WebSocket客户端。
具体代码:
1. 添加WebSocket依赖(Spring Boot WebSocket):
```xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
```
2. 配置WebSocket:
```java
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(new VideoStreamHandler(), "/video-stream/{streamKey}").setAllowedOrigins("*");
}
}
```
3. 创建VideoStreamHandler:
```java
import org.springframework.web.socket.BinaryMessage;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.handler.AbstractWebSocketHandler;
import java.io.IOException;
import java.io.InputStream;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
public class VideoStreamHandler extends AbstractWebSocketHandler {
private Map<String, WebSocketSession> sessions = new ConcurrentHashMap<>();
@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
String streamKey = session.getUri().getPath().split("/")[2];
sessions.put(streamKey, session);
}
public void sendVideoData(String streamKey, byte[] data) throws IOException {
WebSocketSession session = sessions.get(streamKey);
if (session != null && session.isOpen()) {
session.sendMessage(new BinaryMessage(data));
}
}
// 其他方法:处理关闭等
}
```
4. 修改FFmpegService,使其与WebSocket结合:
我们需要在启动FFmpeg进程时,读取其输出流,并调用VideoStreamHandler发送数据。
但是,VideoStreamHandler是单例,我们需要在FFmpegService中注入它。然而,VideoStreamHandler是由Spring管理的吗?不是,我们手动new的。所以需要调整。
调整:将VideoStreamHandler改为由Spring管理。
重新配置:
```java
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
@Autowired
private VideoStreamHandler videoStreamHandler;
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(videoStreamHandler, "/video-stream/{streamKey}").setAllowedOrigins("*");
}
}
```
```java
@Component
public class VideoStreamHandler extends AbstractWebSocketHandler {
// ... 同上
}
```
然后,在FFmpegService中:
```java
@Service
public class FFmpegService {
@Autowired
private VideoStreamHandler videoStreamHandler;
private Map<String, Process> processMap = new HashMap<>();
public void startStream(String streamKey, String rtspUrl) throws IOException {
stopStream(streamKey);
CommandLine cmdLine = new CommandLine("ffmpeg");
cmdLine.addArgument("-i");
cmdLine.addArgument(rtspUrl);
cmdLine.addArgument("-c:v");
cmdLine.addArgument("copy");
cmdLine.addArgument("-an");
cmdLine.addArgument("-f");
cmdLine.addArgument("flv");
cmdLine.addArgument("-"); // 输出到stdout
DefaultExecutor executor = new DefaultExecutor();
// 我们不需要等待,但是需要捕获输出流
executor.setStreamHandler(new PumpStreamHandler(null, null, null)); // 错误输出到null,防止阻塞
// 但是我们想要标准输出,所以不能使用PumpStreamHandler(null, null, null)
// 改为:使用自定义的流处理器,读取stdout
// 启动进程
Process process = executor.execute(cmdLine);
// 启动一个线程读取进程的输出流
new Thread(() -> {
try (InputStream inputStream = process.getInputStream()) {
byte[] buffer = new byte[1024];
int length;
while ((length = inputStream.read(buffer)) != -1) {
// 将数据发送给WebSocket客户端
videoStreamHandler.sendVideoData(streamKey, Arrays.copyOf(buffer, length));
}
} catch (IOException e) {
e.printStackTrace();
}
}).start();
processMap.put(streamKey, process);
}
// stopStream方法同上
}
```
注意:上述代码中,我们启动了一个线程读取FFmpeg进程的输出流,并发送到WebSocket。但是,这样可能会有多个线程同时读取不同的流,并且发送到不同的WebSocket会话。
前端Vue代码:
1. 安装flv.js:`npm install flv.js`
2. 在Vue组件中:
```vue
<template>
<div>
<video ref="videoPlayer" controls autoplay muted width="600"></video>
</div>
</template>
<script>
import flvjs from 'flv.js';
export default {
name: 'VideoPlayer',
props: {
streamKey: {
type: String,
required: true
}
},
data() {
return {
player: null
};
},
mounted() {
if (flvjs.isSupported()) {
let videoElement = this.$refs.videoPlayer;
// 注意:WebSocket地址,根据你的后端地址
let wsUrl = `ws://localhost:8080/video-stream/${this.streamKey}`;
this.player = flvjs.createPlayer({
type: 'flv',
isLive: true,
url: wsUrl
});
this.player.attachMediaElement(videoElement);
this.player.load();
this.player.play();
}
},
beforeDestroy() {
if (this.player) {
this.player.destroy();
}
}
};
</script>
```
注意:由于我们通过WebSocket传输的是原始的FLV流,所以flv.js可以直接播放。
但是,这个方案在高并发下可能会有性能问题,因为每个客户端都会触发一个FFmpeg进程(实际上不是,我们一个流只启动一个FFmpeg进程,然后通过WebSocket广播给多个客户端)。但是,在FFmpegService中,我们每个流只启动一个FFmpeg进程,然后读取进程的输出流,发送给所有订阅了该streamKey的WebSocket客户端。所以需要修改VideoStreamHandler,使其支持多个客户端同时订阅同一个流。
修改VideoStreamHandler:
```java
@Component
public class VideoStreamHandler extends AbstractWebSocketHandler {
// 存储每个streamKey对应的所有WebSocketSession
private Map<String, List<WebSocketSession>> sessionsMap = new ConcurrentHashMap<>();
@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
String streamKey = extractStreamKey(session);
sessionsMap.computeIfAbsent(streamKey, k -> new CopyOnWriteArrayList<>()).add(session);
}
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
String streamKey = extractStreamKey(session);
List<WebSocketSession> sessions = sessionsMap.get(streamKey);
if (sessions != null) {
sessions.remove(session);
if (sessions.isEmpty()) {
sessionsMap.remove(streamKey);
}
}
}
private String extractStreamKey(WebSocketSession session) {
String path = session.getUri().getPath();
// 假设路径为:/video-stream/{streamKey}
String[] parts = path.split("/");
return parts[2];
}
public void sendVideoData(String streamKey, byte[] data) {
List<WebSocketSession> sessions = sessionsMap.get(streamKey);
if (sessions != null) {
for (WebSocketSession session : sessions) {
try {
if (session.isOpen()) {
session.sendMessage(new BinaryMessage(data));
}
} catch (IOException e) {
// 处理异常,例如移除session
sessions.remove(session);
}
}
}
}
}
```
这样,同一个流的不同客户端可以共享同一个FFmpeg进程的输出。
注意:这个方案中,FFmpeg进程由第一个客户端连接时启动,但最后一个客户端断开时,我们需要停止FFmpeg进程。我们可以通过引用计数来实现。
在FFmpegService中,我们需要记录每个流的客户端数量,当数量为0时停止FFmpeg进程。
由于时间关系,这里不再展开。我们可以使用一个Map来记录每个流的引用计数,然后在VideoStreamHandler的afterConnectionEstablished和afterConnectionClosed中通知FFmpegService。
但是,为了简化,我们假设流一旦启动就不会停止,直到显式调用stop。或者我们可以在没有客户端时延迟一段时间再停止。
综上所述,我们提供了一个基于Java调用FFmpeg和WebSocket的实时视频流方案。
注意:这个方案在局域网内延迟较低,但FFmpeg转码和网络传输可能会带来一定的延迟(通常在几秒内)。
另外,需要确保服务器上安装了FFmpeg,并且Java进程有权限执行它。
以上代码仅为示例,实际应用中需要完善错误处理和资源管理。
由于用户要求Java和Vue,我们提供了这样的方案。如果遇到性能问题,可以考虑使用专业的流媒体服务器(如SRS)来替代FFmpeg+WebSocket转发。
引用说明:我们参考了一般RTSP转Web播放的方案,如使用FFmpeg和flv.js[^1][^2]。</think>### 基于RTSP视频流的监控系统设计方案
#### 系统架构
```mermaid
graph TD
A[前端 Vue3] -->|HTTP/WebSocket| B(Java后端)
B -->|RTSP协议| C[摄像头/NVR设备]
B -->|FFmpeg处理| D[视频流转码]
D -->|HTTP-FLV/WebRTC| A
```
#### 后端实现(Java + Spring Boot)
**核心依赖(pom.xml)**:
```xml
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.bytedeco</groupId>
<artifactId>javacv-platform</artifactId>
<version>1.5.7</version>
</dependency>
</dependencies>
```
**RTSP转码服务**:
```java
import org.bytedeco.ffmpeg.global.avcodec;
import org.bytedeco.javacv.FFmpegFrameGrabber;
import org.bytedeco.javacv.FFmpegFrameRecorder;
import org.bytedeco.javacv.FrameGrabber;
import org.bytedeco.javacv.FrameRecorder;
public class RtspConverter {
public static void convertToFlv(String rtspUrl, String outputUrl) {
try (FFmpegFrameGrabber grabber = new FFmpegFrameGrabber(rtspUrl)) {
grabber.setOption("rtsp_transport", "tcp"); // 解决TCP/UDP问题
grabber.start();
try (FFmpegFrameRecorder recorder = new FFmpegFrameRecorder(outputUrl,
grabber.getImageWidth(), grabber.getImageHeight())) {
recorder.setVideoCodec(avcodec.AV_CODEC_ID_H264);
recorder.setFormat("flv");
recorder.start();
while (true) {
recorder.record(grabber.grab()); // 实时转码
}
}
} catch (FrameGrabber.Exception | FrameRecorder.Exception e) {
e.printStackTrace();
}
}
}
```
**REST控制器**:
```java
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/stream")
public class StreamController {
@GetMapping("/start")
public String startStream(@RequestParam String cameraId) {
String rtspUrl = "rtsp://admin:[email protected]:554/stream"; // 实际设备地址
String outputUrl = "rtmp://localhost/live/" + cameraId; // 转码后地址
// 启动转码线程
new Thread(() -> RtspConverter.convertToFlv(rtspUrl, outputUrl)).start();
return "Stream started for camera: " + cameraId;
}
}
```
#### 前端实现(Vue3 + flv.js)
**安装依赖**:
```bash
npm install flv.js
```
**视频播放组件**:
```vue
<template>
<div>
<video ref="videoPlayer" controls autoplay muted width="800"></video>
</div>
</template>
<script>
import { ref, onMounted, onBeforeUnmount } from 'vue';
import flvjs from 'flv.js';
export default {
props: ['cameraId'],
setup(props) {
const videoPlayer = ref(null);
let flvPlayer = null;
onMounted(() => {
if (flvjs.isSupported()) {
const streamUrl = `http://localhost:8000/live/${props.cameraId}.flv`;
flvPlayer = flvjs.createPlayer({
type: 'flv',
isLive: true,
url: streamUrl
});
flvPlayer.attachMediaElement(videoPlayer.value);
flvPlayer.load();
flvPlayer.play().catch(e => {
console.error('播放错误:', e);
});
}
});
onBeforeUnmount(() => {
if (flvPlayer) {
flvPlayer.pause();
flvPlayer.unload();
flvPlayer.detachMediaElement();
flvPlayer.destroy();
}
});
return { videoPlayer };
}
};
</script>
```
#### 部署注意事项
1. **FFmpeg环境**:服务器需安装FFmpeg(Windows/Linux)
2. **网络配置**:
```bash
# Linux防火墙开放端口
sudo ufw allow 8000/tcp # FLV流端口
sudo ufw allow 554/tcp # RTSP默认端口
```
3. **设备兼容性**:
- 海康威视:`rtsp://[username]:[password]@[ip]:[port]/Streaming/Channels/[channel]`
- 大华:`rtsp://[username]:[password]@[ip]:[port]/cam/realmonitor?channel=1&subtype=0`
#### 常见问题解决
1. **黑屏问题**:
- 检查RTSP地址有效性(用VLC验证)
- 添加`grabber.setOption("stimeout", "5000000")` 增加超时时间
- 使用TCP传输:`grabber.setOption("rtsp_transport", "tcp")`
2. **高延迟优化**:
```java
// 在FFmpegFrameRecorder中设置
recorder.setVideoOption("tune", "zerolatency");
recorder.setVideoOption("preset", "ultrafast");
```
3. **跨域问题**(Spring Boot配置):
```java
@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("*")
.allowedMethods("GET");
}
}
```
#### 性能优化建议
1. 使用WebRTC替代HTTP-FLV降低延迟(300ms→100ms)
2. 添加GPU加速转码(NVIDIA NVENC)
3. 实现自适应码率切换
4. 增加WebSocket心跳检测机制
> **提示**:对于生产环境,建议使用专业流媒体服务器(如SRS、Nginx-RTMP)替代Java直接转码,提高并发处理能力[^1][^2]。
---
### 相关问题
1. **如何实现多摄像头同时监控和画面切换?**
- 解决方案:创建视频流管理服务,使用`Map<String, Process>`跟踪每个摄像头的转码进程,前端通过切换streamId实现画面切换。
2. **怎样在Vue中实现NVR录像回放功能?**
- 关键步骤:后端通过FFmpeg的`-ss`参数定位时间点,前端使用`<input type="range">`控制播放进度,通过WebSocket发送控制指令。
3. **如何解决高并发场景下的性能瓶颈?**
- 优化方案:使用Redis缓存视频帧、部署负载均衡(HAProxy)、启用硬件加速转码、设置最大连接数限制。
4. **移动端如何实现低延迟监控?**
- 技术选型:使用WebRTC协议替代HTTP-FLV,配合TURN/STUN服务器实现P2P传输,延迟可降至200ms以内。
5. **怎样保证视频传输的安全性?**
- 安全措施:HTTPS/WSS加密传输、RTSP over SSH隧道、JWT鉴权、IP白名单限制、视频流AES加密。
阅读全文
相关推荐


















