SpringBoot JavaCV Minio 实现RTSP 实时推送,历史回播,切片存储,实时抓拍
介绍
监控视频流转换,用于将rtsp等协议的视频流转换为rtmp或hls协议的视频流,前端使用flv.js渲染,是一个通用的开源项目,开箱即用,目前已集成知名设备厂家的摄像头(宇视,海康,大华)无缝连接,技术栈使用java + javacv +minio 实现该项目的三大功能模块:实时播放,切分存储回放,点时抓拍,播放拓展结合nginx 代理分发实现并发预览播放效果
后端技术架构
-
基础框架:Spring Boot 2.0.5.RELEASE
-
计算机视觉封装库: javacv 1.5.5
-
持久层框架:mybatis plus
-
安全框架:spring security
-
数据库连接池: Druid 1.1.22
-
缓存框架:redis
-
分布式对象存储系统: minio 8.2.0
-
日志打印:logback
-
其他:fastjson、commons-lang3、netty、lombok 等。
前端技术架构
-
基础框架:框架随意
-
播放器:flv.js https://cdn.bootcss.com/flv.js/1.4.0/flv.min.js
目录结构
│ pom.xml
│ README.en.md
│ README.md
│ wnvideo.png
├─dll
│ ├─x64
│ │ opencv_java451.dll
│ └─x86
│ opencv_java451.dll
├─lib
│ opencv-451.jar
└─src
└─main
├─java
│ └─com
│ └─coyee
│ └─stream
│ │ StreamApplication.java
│ │
│ ├─annotate
│ │ MyAnnotation.java
│ │ MyAnnotationAspect.java
│ │
│ ├─bean
│ │ JsonResult.java
│ │
│ ├─config
│ │ AppConfig.java
│ │ AsyncConfig.java
│ │ HttpClientConfigs.java
│ │ MinIoClientConfig.java
│ │ RedisConfig.java
│ │ ResourceConfig.java
│ │ SchedulerConfig.java
│ │ SpringMvcConfig.java
│ │ StreamServerConfig.java
│ │
│ ├─controller
│ │ │ StreamController.java
│ │ │
│ │ └─advice
│ │ GlobleExceptionHandler.java
│ │
│ ├─converter
│ │ Converter.java
│ │ ConverterFactory.java
│ │ FlvConverter.java
│ │ HlsConverter.java
│ │ ImageConverter.java
│ │
│ ├─enums
│ │ │ AppHttpCodeEnum.java
│ │ │ HttpStatusEnum.java
│ │ │ IResultEnum.java
│ │ │ ResultEnum.java
│ │ │
│ │ └─equipment
│ │ EqStatusEnum.java
│ │ EquipmentTypeEnum.java
│ │
│ ├─exception
│ │ ApplicationException.java
│ │ Assert.java
│ │ BaseException.java
│ │ BusinessException.java
│ │ DataAccessException.java
│ │ IBusinessExceptionAssert.java
│ │ ServiceException.java
│ │
│ ├─service
│ │ │ IMinioFileService.java
│ │ │ ThreadFileSaveService.java
│ │ │
│ │ └─impl
│ │ IMinioFileServiceImpl.java
│ │ ThreadFileSaveServiceImpl.java
│ │
│ ├─task
│ │ VideRegisterTask.java
│ │
│ ├─transcribe
│ │ TConverter.java
│ │ TransferConverter.java
│ │ TransferConverterFactory.java
│ │ TransferToFlv.java
│ │ TStreamServerConfig.java
│ │
│ └─util
│ BodyRequestConfig.java
│ Des.java
│ EnvironmentUtils.java
│ FastJsonRedisSerializerUtil.java
│ FormRequestConfig.java
│ HttpClientUtil.java
│ HttpServletRequestUtil.java
│ MinioUtil.java
│ RedisUtil.java
│ Result.java
│ SliceStorageVideoFlv.java
│
└─resources
│ application.yml
│ banner.txt
│ logback-spring.xml
│ run.sh
│
└─static
favicon.ico
flv.html
hls.html
开发环境
-
语言:Java 8
-
IDEA 2023
-
依赖管理:Maven
-
缓存:Redis
-
MINIO 8.2.0
-
前端播放器:flv.js(源码中附有)
功能描述
三大功能
- 实时转流拉流
- 流切片存储flv-回放效果
- 点时抓拍
快速开始
环境准备
- clone代码到本地(尽量避免放在中文路径之下)
- 检查
java
环境,minio
版本,Redis
,没有的请自行搭建,注意javacv和java版本
后端
- 确保环境准备都完成并且没问题,直接使用
idea
安装好maven
依赖就可以直接运行 - 操作之前请阅读一下注意事项第一点
代码核心
1、实时转码拉流
private void transFlv() throws FrameRecorder.Exception, FrameGrabber.Exception, InterruptedException {
log.info("FLV(complex)转换任务启动,可以立即播放:{}。", endpoint);
//创建录制器
if (Objects.isNull(recorder)){
createTransterOrRecodeRecorder();
}
if (headers == null) {
headers = stream.toByteArray();
stream.reset();
writeResponse(headers);
}
long startTime = System.currentTimeMillis();
// 获取当前线程的ID
long threadId = Thread.currentThread().getId();
log.info("开始转码-拉流-推流-key={}-线程id={}-时间: {} ",key,threadId,startTime);
int nullNumber = 0;
while (running) {
// 抓取一帧
Frame f = grabber.grab();
if (f != null) {
try {
recorder.record(f);
} catch (Exception e) {
}
if (stream.size() > 0) {
byte[] b = stream.toByteArray();
stream.reset();
writeResponse(b);
if (outEntitys.isEmpty()) {
log.info("没有输出退出");
break;
}
}
} else {
nullNumber++;
if (nullNumber > 200) {
break;
}
}
Thread.sleep(1);
}
}
2、分片存储
public void saveStreamToSegments(String rtsp_url,String outputDir) {
logger.info("the TransferToFlv-saveStreamToSegments 开始存储:{}",this);
//TODO 每次存储时长 单位毫秒 分片持续时间 1 分钟
long segmentDurationMillis = 60000;
long startTimeMillis= System.currentTimeMillis();
System.out.println("the TransferToFlv-saveStreamToSegments ------------- 开始时间戳:"+startTimeMillis);
FFmpegFrameGrabber grabber = new FFmpegFrameGrabber(rtsp_url);
try {
createGrabber(grabber,startTimeMillis); // 从指定时间戳开始拉流
long segmentStartTime = System.currentTimeMillis();
running =true;
while (running) {
long segmentIndex = System.currentTimeMillis() / segmentDurationMillis; // 起始分片的索引
String segmentFile=null;
if (EnvironmentUtils.isWindows()){
segmentFile = outputDir + "\\segment_" + segmentIndex + ".flv";
}else{
segmentFile = outputDir + "/segment_"+ segmentIndex + ".flv";
}
FFmpegFrameRecorder recorder = new FFmpegFrameRecorder(segmentFile, grabber.getImageWidth(), grabber.getImageHeight(), grabber.getAudioChannels());
recorder.setFormat("flv");
//设置录制器参数
setRecorderParams(recorder,grabber);
recorder.startUnsafe();
while (System.currentTimeMillis() - segmentStartTime < segmentDurationMillis && running) {
Frame frame = grabber.grab();
if (frame != null) {
recorder.record(frame);
}
}
recorder.close();
segmentStartTime = System.currentTimeMillis();
System.out.println("the TransferToFlv-saveStreamToSegments ------------- 获取下一个文件创建时间戳:"+startTimeMillis);
segmentIndex++;
}
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
try {
grabber.close();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
3、拉取本地分片历史视频文件
public void playFromSegments() {
try {
//TODO 起始文件分片的索引
long segmentIndex = startTimeMillis*1000 / segmentDurationMillis;
//TODO 起始分片内的偏移量
long segmentStartOffset =startTimeMillis*1000 % segmentDurationMillis;
// TODO 结束分片索引
long endmentIndex = endTimeMillis*1000 /segmentDurationMillis;
while (running) {
String segmentFile =null;
if (EnvironmentUtils.isWindows()){
segmentFile = "D:\\home\\"+key+"\\segment_" + segmentIndex + ".flv"; // 当前分片的文件路径
}else{
segmentFile = "/home" + "/segment_"+ segmentIndex + ".flv";
}
File file = new File(segmentFile);
if (file.exists() && (segmentIndex < endmentIndex)){
FFmpegFrameGrabber grabber = new FFmpegFrameGrabber(segmentFile);
grabber.startUnsafe();
stream = new ByteArrayOutputStream();
setRecorderParams(stream,grabber);
// 如果是从指定时间点开始播放的第一个分片,设置grabber的时间戳从segmentStartOffset开始读取
if (segmentIndex == startTimeMillis*1000 / segmentDurationMillis) {
grabber.setTimestamp(segmentStartOffset); // 设置开始时间(单位:微秒)
}
if (headers == null) {
headers = stream.toByteArray();
stream.reset();
writeResponse(headers);
}
int nullNumber = 0;
// 循环读取每一帧,并通过recorder将其录制
while (running ) {
Frame f = grabber.grab();
if (f != null) {
try {
recorder.record(f);
} catch (Exception e) {
}
if (stream.size() > 0) {
byte[] b = stream.toByteArray();
stream.reset();
writeResponse(b);
if (outEntitys.isEmpty()) {
logger.info("没有输出退出");
break;
}
}
} else {
nullNumber++;
if (nullNumber > 200) {
break;
}
}
Thread.sleep(1);
}
// 关闭当前分片
recorder.close();
grabber.close();
}
// 移动到下一个分片
segmentIndex++;
Thread.sleep(5); // 稍作延迟,避免过快切换
}
} catch (Exception e) {
logger.error("Error playing segments: " + e.getMessage());
throw new RuntimeException(e);
} finally {
try {
if (recorder != null) {
recorder.close();
}
if (grabber != null) {
grabber.close();
}
} catch (Exception e) {
logger.error("Error closing resources: " + e.getMessage());
throw new RuntimeException(e);
}
}
}
跑不起來?
- 文末加联系方式,可提供运行教程视频以及更多资源(数据库逻辑结构、ER图、功能详细说明)
LINUX 部署
- 源码提供 sh脚本部署
- 源码地址
源码怎么找到我- (请注明来意)
- QQ : 1835334431