SpringBoot JavaCV Minio 实现视频流转码

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 等。

前端技术架构

目录结构

│ 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 部署

源码怎么找到我- (请注明来意)

  • QQ : 1835334431
评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值