java接口方式调用海康大华摄像机预览。

客户有海康和大华的监控设备,没有买各类安防平台,国标方式需要预留给其他需要接入的系统,得兼容高版本chrome,询问了大华的客服人员,记录下曲折的过程。延迟大约10秒的样子,应该还能通过设置参数在优化,CPU占用率较高,不适合高并发,小项目用的少可以。如果高并发场景高的还是别用,可以使用ZLMediaKit开源(尝试了下ffmpeg占用率是要低很多,好东西太优秀的国产开源),或者SRS等一些开源的去改吧改吧用。

海康大华摄像机NVR接口方式在高版本chrome浏览器预览的解决方案

  1. rtsp+nginx转m3u8播放

  1. rtsp+nginx转flv播放

  1. ZLMediaKit+wvp拉流

  1. rtsp转flv(简单方式)

  2. 有安装安防平台使用安防平台提供的前后端demo

对接海康综合安防管理平台经验总结_artemis-http-client-CSDN博客

javacv实现流媒体服务-阿里云开发者社区

方案一(目前使用方式,记录下整个过程):

一、ffmpeg+nginx搭建过程支持h264和h265


#前置安装一些后面需要的
yum -y install git
yum install -y bzip2
yum install -y cmake
yum install unzip -y
yum install gcc-c++ -y
yum install pcre pcre-devel -y 
yum install -y  libarchive
yum install zlib zlib-devel -y
yum install openssl openssl-devel -y
#sudo yum -y install cmake   #3.0版本以上
yum install wget -y
cd /usr/local/
sudo wget https://cmake.org/files/v3.22/cmake-3.22.0-rc1-linux-x86_64.tar.gz
tar -zxvf cmake-3.22.0-rc1-linux-x86_64.tar.gz
sudo mv cmake-3.22.0-rc1-linux-x86_64 /opt/cmake-3.22.0
sudo ln -sf /opt/cmake-3.22.0/bin/* /usr/bin/
cmake --version
#下载安装nginx http://nginx.org/en/download.html
cd /usr/local/
tar -zxvf nginx-1.23.3.tar.gz

cd nginx-1.23.3
linux 安装flv模块已包含了rtmp
wget https://github.com/winshining/nginx-http-flv-module/archive/master.zip
unzip master.zip
./configure  --prefix=/usr/local/nginx --with-http_ssl_module --with-http_stub_status_module --add-module=/usr/local/nginx-1.23.3/nginx-http-flv-module-master
make && make install
#nginx的配置,修改完重启下nginx

#nginx配置rtmp详解参考

#需要通过nginx简单负载均衡参考

ffmpeg通过nginx负载均衡后端和静态文件_ffmpeg proxy-CSDN博客

#直播相关的配置的详细介绍实时监控、直播流、流媒体、视频网站开发方案流媒体服务器搭建及配置详解:使用nginx搭建rtmp直播、rtmp点播、,hls直播服务配置详解_51CTO博客_视频流媒体平台

二、对外网nginx的配置如下


user  root root;
worker_processes  2;#设置为内核数*2

#error_log  logs/error.log;
#error_log  logs/error.log  notice;
#error_log  logs/error.log  info;

#pid        logs/nginx.pid;


events {
    worker_connections  1024;
}


http {
    include       mime.types;
    default_type  application/octet-stream;

    #log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
    #                  '$status $body_bytes_sent "$http_referer" '
    #                  '"$http_user_agent" "$http_x_forwarded_for"';

    #access_log  logs/access.log  main;

    sendfile        on;
    #tcp_nopush     on;

    #keepalive_timeout  0;
    keepalive_timeout  65;    
    client_max_body_size 100m;
    #gzip  on;

    server {
        listen       8099;
        #配置安装服务器的Ip地址,与下面rtmp的配置一致
        server_name  172.16.121.75;

        #charset koi8-r;

        #access_log  logs/host.access.log  main;

       # location / {
       #     index  index.html index.htm;
       #     proxy_pass http://webservers;
       #     add_header Access-Control-Allow-Origin *;
       #     add_header Access-Control-Allow-Credentials true;
       #     add_header Access-Control-Allow-Headers Authorization,Content-Type,Accept,Origin,User-Agent,DNT,Cache-Control,X-Mx-ReqToken,X-Requested-With;
       #     add_header Access-Control-Allow-Methods GET,POST,OPTIONS;
       #     try_files $uri $uri/ @router; 
       # }


            # ffmpeg直播地址
       location /live {
            flv_live on;
            chunked_transfer_encoding  on; #open 'Transfer-Encoding: chunked' response
            add_header 'Access-Control-Allow-Credentials' 'true'; #add additional HTTP header
            add_header 'Access-Control-Allow-Origin' '*'; #add additional HTTP header
            add_header Access-Control-Allow-Headers X-Requested-With;
            add_header Access-Control-Allow-Methods GET,POST,OPTIONS;
            add_header 'Cache-Control' 'no-cache';
        }

        # This URL provides RTMP statistics in XML
                location /stat {
                                rtmp_stat all;
                                # Use this stylesheet to view XML as web page
                                # in browser
                                rtmp_stat_stylesheet stat.xsl;
                }

                location /stat.xsl {
                                # XML stylesheet to view RTMP stats.
                                # Copy stat.xsl wherever you want
                                # and put the full directory path here
                                root /path/to/stat.xsl/;
                }
              #使用转发
                location /hls {
                                # Serve HLS fragments
                                types {
                                                application/vnd.apple.mpegurl m3u8;
                                                video/mp2t ts;
                                }
                                root html;
                                add_header Cache-Control no-cache;
                }

                location /dash {
                                # Serve DASH fragments
                                root /tmp;
                                add_header Cache-Control no-cache;
                }

        error_page   500 502 503 504  /50x.html;
        location = /50x.html {
            root   html;
        }

                error_page  404 403               /404.html;
                location = /404.html {
                        root /usr/local/nginx/html;
                } 

                # redirect server error pages to the static page /50x.html
                #
                error_page   500 502 503 504  /500.html;
                location = /500.html {
                        root   /usr/local/nginx/html;
                }

    }

}
#rtmp设置
rtmp {
    out_queue               4096;
    out_cork                 8;
    max_streams             128;
    timeout                 15s;
    drop_idle_publisher     15s;
    log_interval 5s;
    log_size     1m;
    server {
        listen 1935;      #监听的端口号
        server_name 172.16.121.75; #与上面监听的8099端口的server名字一致
        application live {      #自定义的名字
            live on;
       }
       #直播hls配置
        application hls {
            live on;
            hls on;
            hls_path /usr/local/nginx/html/hls;#直播缓存路径window环境的配置为绝对地址,参考上传的
              hls_fragment 2s;#设置HLS片段长度。 默认为5秒。
            hls_playlist_length 5s;#设置HLS播放列表长度。 默认为30秒。
            hls_continuous on; #连续模式。
            hls_nested on;     #嵌套模式就是允许如localhost:8099/hts/目录1/目录2/文件名.m3u8;
            hls_cleanup off; # 切换HLS清理。 默认情况下,该功能处于打开状态。 在这种模式下,nginx缓存管理器进程从HLS目录中删除旧的HLS片段和播放列表,必须关闭才能按目录去删除,如果访问的url没有层级要求,把m3u8的名字取好点就可以没有必要按层级去删除目录或者建目录
       }
    }
}

#下载包甩到/home/village/ffmpeg 下根据实际需自己放,或者放到/usr/local下

三、linux环境程序部署相关下载包分享和安装过程

#链接:百度网盘 请输入提取码

#提取码:0824

#大致的安装过程


cd /home/village/ffmpeg
tar jxvf nasm-2.15.tar.bz2
#安装h264 和 h265时候所需
cd nasm-2.15/
./configure  --prefix=/usr/local
make && make install
nasm --version    # 查看版本号  如果提示命令找不到 配下环境变量
cd /home/village/ffmpeg
tar -zxvf yasm-1.3.0.tar.gz
cd yasm-1.3.0
./configure
make && make install
#安装pkg-config
cd /home/village/ffmpeg
#wget https://pkg-config.freedesktop.org/releases/pkg-config-0.29.2.tar.gz
sudo tar -zxvf pkg-config-0.29.2.tar.gz
cd pkg-config-0.29.2/
sudo ./configure --with-internal-glib
make
make check
make install
export PATH=/usr/local/lib/pkgconfig:$PATH 
#查看版本
pkg-config --version
#安装h264
cd /home/village/ffmpeg
tar -jxvf x264-master.tar.bz2
cd x264-master
mkdir /usr/local/x264
./configure --prefix=/usr/local/x264 --enable-shared --enable-static
make && make install 
#添加环境变量
vim /etc/profile
#在文件末尾添加
export PATH=/usr/local/x264/bin:$PATH
export PATH=/usr/local/x264/include:$PATH
export PATH=/usr/local/x264/lib:$PATH
source /etc/profile
安装h265
cd /home/village/ffmpeg
tar -zxvf x265_3.2.tar.gz
cd x265_3.2/build/linux
./make-Makefiles.bash
#选择时候将选择ENABLE_HDR10_PLUS和HIGH_BIT_DEPTH通过回车设置ON,然后q键退出
vi /home/village/ffmpeg/x265_3.2/build/linux/x265.pc
#找到Libs.private: -lstdc++ -lm -lrt -ldl 末尾添加-lpthread
make && make install
pkg-config --list-all   # 查看是否有x265 x264
#没有vim /etc/profile  
export PKG_CONFIG_PATH=/usr/local/lib/pkgconfig:$PKG_CONFIG_PATH 
export PKG_CONFIG_PATH=/usr/local/x264/lib/pkgconfig:$PKG_CONFIG_PATH 
export PKG_CONFIG_PATH=/usr/local/lib:$PKG_CONFIG_PATH
source /etc/profile
pkg-config --list-all # 查看是否有x265 x264
#https://blog.csdn.net/angl129/article/details/122339796
# 支持10bit
cd /home/village/ffmpeg/x265_3.2/source
vim CMakeLists.txt
#/HIGH_BIT_DEPTH 修改option(HIGH_BIT_DEPTH "Store pixel samples as 16bit values (Main10/Main12)" OFF)  OFF改为ON
cd /home/village/ffmpeg/x265_3.2/build/linux
cmake -G "Unix Makefiles" -DCMAKE_INSTALL_PREFIX=/usr/local -DENABLE_SHARED=OFF ../../source
#安装
make
make install
#ffmpeg
cd /home/village/ffmpeg
tar -xzvf ffmpeg-4.1.tar.gz
cd ffmpeg-4.1
./configure --prefix=/usr/local/ffmpeg --enable-shared --enable-yasm --enable-libx264 --enable-libx265 --enable-gpl --enable-pthreads --extra-cflags=-I/usr/local/x264/include --extra-ldflags=-L/usr/local/x264/lib --disable-x86asm --pkg-config="pkg-config --static"
# vim /etc/ld.so.conf
#在文件末尾加上
/usr/local/ffmpeg/lib
/usr/local/lib
/usr/local/x264/lib
#让配置生效
 sudo ldconfig
make && make install
cp /usr/local/ffmpeg/bin/* /usr/bin/
vi /etc/profile
#末尾加入
export PATH=/usr/local/ffmpeg/bin:$PATH
export LD_LIBRARY_PATH=/usr/local/ffmpeg/lib:$LD_LIBRARY_PATH
#生效
source /etc/profile
ffmpeg -version

#处理大华rtsp到m3u8测试


#linux测试
ffmpeg -rtsp_transport tcp -i "rtsp://admin:admin123@IP:554/cam/realmonitor?channel=1&subtype=0" -strict -2 -c:v libx264 -vsync 2 -c:a aac -f hls -hls_time 4 -hls_list_size 3 -hls_wrap 10 -y /usr/local/nginx/html/hls/channel1.m3u8
#多级目录测试,目录结构/hls/用户名/自定的视频表ID/自定的视频表ID.m3u8
#简单处理就是hls_cleanup开启,定义好m3u8的文件名,通过文件名解析去关掉相应的ffmpeg
ffmpeg -rtsp_transport tcp -i "rtsp://admin:admin123@IP:554/cam/realmonitor?channel=1&subtype=0" -strict -2 -c:v libx264 -vsync 2 -c:a aac -f hls -hls_time 4 -hls_list_size 3 -hls_wrap 10 -y /usr/local/nginx/html/hls/ea6f791673c640998e31dd29082621f1/3E4DC4ECEA2847ED8E5D88BB2BA3BFCC/3E4DC4ECEA2847ED8E5D88BB2BA3BFCC.m3u8
#window下测试
ffmpeg -rtsp_transport tcp -i "rtsp://admin:admin123@IP:554/cam/realmonitor?channel=1&subtype=0" -strict -2 -c:v libx264 -vsync 2 -c:a aac -f hls -hls_time 4 -hls_list_size 3 -hls_wrap 10 -y D:\tools\nginx\vedioCache\hls\ea6f791673c640998e31dd29082621f1\3E4DC4ECEA2847ED8E5D88BB2BA3BFCC\3E4DC4ECEA2847ED8E5D88BB2BA3BFCC.m3u8
#http m3u8格式访问地址
http://localhost:8099/hls/ea6f791673c640998e31dd29082621f1/3E4DC4ECEA2847ED8E5D88BB2BA3BFCC/3E4DC4ECEA2847ED8E5D88BB2BA3BFCC.m3u8
http://172.16.121.75:8099/hls/ea6f791673c640998e31dd29082621f1/3E4DC4ECEA2847ED8E5D88BB2BA3BFCC/3E4DC4ECEA2847ED8E5D88BB2BA3BFCC.m3u8

#vlc播发器串流播放测试

#本地window测试环境就需要在本地安装ffmpeg和nginx

#window 下测试用,已编译好的安装了rtmp的nginx共享地址

链接:百度网盘 请输入提取码

提取码:0824

#java通过接口调用方式调用ffmpeg参考以下博文进行改造,按海康和大华rtsp的格式设计和维护一张表,也可以通过集成官方的SDK去获取相应的设备列表信息

海康视频回放,rtsp视频接口转换成.m3u8格式文件_rtsp转m3u8-CSDN博客

Nginx+ffmpeg+java+hls+videojs实现RTSP转HLS - 灰信网(软件开发博客聚合)

四、代码改造

ffmpeg java调用上面涉及的一些代码下载地址

http://github.com/eguid/FFCH4J

#代码和配置实现接口调用预览和关闭预览

#POM引入


 <!--javacv基础包,包含javacv和javacpp,必须-->
        <dependency>
            <groupId>org.bytedeco</groupId>
            <artifactId>javacv</artifactId>
            <version>1.5.4</version>
        </dependency>
        <!-- ffmpeg依赖 -->
        <dependency>
            <groupId>org.bytedeco</groupId>
            <artifactId>ffmpeg-platform</artifactId>
            <version>4.3.1-1.5.4</version>
        </dependency>
       <!-- redis依赖 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>

#yml配置加入


#ffmpeg相关自定义的设置  
ffmpeg:
  #macOs系统转流文件缓存地址
  macOsTempPath: /Users/jiangsha/Documents/upload
  #linux系统转流文件缓存地址,rtmp如何配置需要一致
  linuxTempPath: /usr/local/nginx/html
  #本地系统转流文件缓存地址,rtmp直播缓存地址
  windowTempPath: D:\tools\nginx\vedioCache
  # 映射出来的流媒体所在端口
  livePort: 8099
  # 映射出来的流媒体所在服务,nginx配置的/hls用于转发流的
  liveApp: hls
  #httpHeader http或者https
  urlHead: http
  #流媒体所在服务器
  liveServiceUrl: localhost
  #一次在线观看的有效时间,系统资源有限,无法一直推流消耗内存,默认给定30分钟
  expireMinutes: 30
  #延迟关闭最大时间 20 关闭后20秒内重新打开不重复调用
  expireMinutesDelay: 20
 #配置文件中找到 notify-keyspace-events ""  将其改成   notify-keyspace-events "Ex" 用于回调
spring: 
 redis:
    host: redis的IP
    password: 密码
    database: 4
    port: 6379

#redis配置类和回调类


@Configuration
public class RedisConfig {
    @Bean
    //标红别管他
    RedisMessageListenerContainer container(RedisConnectionFactory connectionFactory) {
        RedisMessageListenerContainer container = new RedisMessageListenerContainer();
        container.setConnectionFactory(connectionFactory);
        return container;
    }
}

@Component
@Slf4j
public class RedisKeyExpirationListener extends KeyExpirationEventMessageListener {

    public RedisKeyExpirationListener(RedisMessageListenerContainer listenerContainer) {

        super(listenerContainer);
    }

    @Autowired
    CameraSecretInfoService cameraSecretInfoService;

    /**
     * key过期触发的事件
     */
    @SneakyThrows
    @Override
    public void onMessage(Message message, byte[] pattern) {
        String channel = new String(message.getChannel(), StandardCharsets.UTF_8);
        String key = new String(message.getBody(), StandardCharsets.UTF_8);
        boolean contains = key.contains(Constants.FFMPEG_USER_KEY);
        if (contains) {
            log.info("redis key 过期:pattern={},channel={},key={}", new String(pattern), channel, key);
            // key 形式 bladeFile:userId:pkId
            String[] params = key.split(":");
            cameraSecretInfoService.removeExpiredVideo(params[1], params[2]);
        }

    }
}

#定时关闭意外未关闭的ffmpeg


@Slf4j
@Component
public class FfmpegSchedule {

    @Resource
    CommandManager manager;

    /**
     * 每天晚上2点清理一次ffmpeg
     */
    @Scheduled(cron = "0 00 2 * * ?")
    private void configureTasks() {
        log.info("开始定时清理未正常关闭的ffmpeg进程");
        manager.stopAll();
    }
}

#ffmpeg自定义配置类


@Data
@Configuration
@ConfigurationProperties(value = "ffmpeg")
public class FfmpegConfig {

    /**
     * 本地测试流文件路径缓存地址
     */
    private String macOsTempPath;

    /**
     * window流文件路径存地址,配置在yml文件里面
     */
    private String windowTempPath;

    /**
     * linux流文件路径存地址,配置在yml文件里面
     */
    private String linuxTempPath;

    /**
     * 映射出来的流媒体所在端口
     */
    private String livePort;

    /**
     * 映射出来的流媒体所在服务,nginx配置的勇于转发流的
     */
    private String liveApp;

    /**
     * httpHeader
     */
    private String urlHead;

    /**
     * 流媒体服务地址,设置为流媒体所在地址,也就是服务所地址
     */
    private String liveServiceUrl;

    /**
     * 一次观看系统监控的时间,过期后会自动释放,单位分钟
     */
    private int expireMinutes;


    /**
     *  延迟关闭的10秒
     */
    private int expireMinutesDelay;

    /**
     * 默认命令行执行根路径
     */
    private String path;
    /**
     * 是否开启debug模式
     */
    private boolean debug;
    /**
     * 任务池大小
     */
    private Integer size;
    /**
     * 回调通知地址
     */
    private String callback;
    /**
     * 是否开启保活
     */
    private boolean keepalive;

    /**
     * Description 获取返回3um8的地址 如http://221.178.132.15:8099/live/userId/vedioId/vedioId.m3u8
     *
     * @param userId
     *            userId
     * @param vedioId
     *            vedioId
     * @return java.lang.String
     * @author
     * @date 19:33 2023/1/7
     **/
    public String getRealLiveUrl(String userId, String vedioId) {
        StringBuilder stringBuilder = new StringBuilder();
        stringBuilder.append(this.urlHead);
        stringBuilder.append("://");
        stringBuilder.append(this.liveServiceUrl);
        stringBuilder.append(":");
        stringBuilder.append(this.livePort + "/");
        stringBuilder.append(this.liveApp + "/");
        stringBuilder.append(userId + "/");
        stringBuilder.append(vedioId + "/");
        stringBuilder.append(vedioId);
        stringBuilder.append(".m3u8");
        return stringBuilder.toString();
    }

    public String getRealLiveUrl(String vedioId) {
        StringBuilder stringBuilder = new StringBuilder();
        stringBuilder.append(this.urlHead);
        stringBuilder.append("://");
        stringBuilder.append(this.liveServiceUrl);
        stringBuilder.append(":");
        stringBuilder.append(this.livePort + "/");
        stringBuilder.append(this.liveApp + "/");
        stringBuilder.append(vedioId + "/");
        stringBuilder.append(vedioId);
        stringBuilder.append(".m3u8");
        return stringBuilder.toString();
    }

    /**
     * Description ffmpeg服务存储m3u8的地址
     * 
     * @param userId
     *            userId
     * @param vedioId
     *            vedioId
     * @return java.lang.String
     * @author
     * @date 20:03 2023/1/7
     **/
    public String getSaveTempFileName(String userId, String vedioId) {
        return FilePathUtil.getFfmpegTmpPath() + File.separator + this.getLiveApp() + File.separator + userId + File.separator + vedioId + File.separator + vedioId + ".m3u8";
    }

    /**
     * Description ffmpeg服务存储m3u8的地址
     *
     * @param vedioId
     *            vedioId
     * @return java.lang.String
     * @author
     * @date 20:03 2023/1/7
     **/
    public String getSaveTempFileName(String vedioId) {
        return FilePathUtil.getFfmpegTmpPath() + File.separator + this.getLiveApp() + File.separator + vedioId + File.separator + vedioId + ".m3u8";
    }

    /**
     * Description ffmpeg服务存储m3u8的地址
     *
     * @param userId
     *            userId
     * @param vedioId
     *            vedioId
     * @return java.lang.String
     * @author
     * @date 20:03 2023/1/7
     **/
    public String getBaseSavePath(String userId, String vedioId) {
        return FilePathUtil.getFfmpegTmpPath() + File.separator + this.getLiveApp() + File.separator + userId + File.separator + vedioId + File.separator;
    }

    /**
     * Description ffmpeg服务存储m3u8的地址
     *
     * @param vedioId
     *            vedioId
     * @return java.lang.String
     * @author
     * @date 20:03 2023/1/7
     **/
    public String getBaseSavePath(String vedioId) {
        return FilePathUtil.getFfmpegTmpPath() + File.separator + this.getLiveApp() + File.separator + vedioId + File.separator;
    }
}

#文件路径工具类兼容各系统


public class FilePathUtil {

    private FilePathUtil() {}

    private static final FfmpegConfig ffmpegConfig = SpringUtils.getBean(FfmpegConfig.class);

    public static String getFfmpegTmpPath() {
        String tempPath = "";
        if (SystemUtils.isMacOs()) {
            tempPath = ffmpegConfig.getMacOsTempPath();
        } else if (SystemUtils.isWindows()) {
            tempPath = ffmpegConfig.getWindowTempPath();
            FileUtil.makeDir(tempPath);
        } else {
            tempPath = ffmpegConfig.getLinuxTempPath();
        }
        return tempPath;
    }

    public static String getFilePath(String fileName) {
        return getFilePath() + File.separator + fileName;
    }

}

#API接口和DTO


@Api(value = "大华监控视频预览API", tags = "大华监控视频预览API")
@RequestMapping("/village/live")
@Validated
public interface CameraSecretInfoApi {

    /**
     * Description 预览大华视频
     *
     * @param villageMonitorDTO
     *            villageMonitorDTO
     * @return com.gsww.village.common.base.OpenResponse<java.lang.String>
     * @author
     * @date 15:55 2023/1/9
     **/
    @ApiOperation(value = "预览某个视频返回m3u8地址", notes = "预览某个视频返回m3u8地址")
    @PostMapping("/ffmpegOpen")
    @BusinessOperateLog(operateModule = "通过ffmpeg预览某个视频", operateType = OperateType.QUERY, operateDesc = "预览摄像头")
    OpenResponse<VillageMonitorDTO>
        ffmpegOpen(@Validated(ValidatedGroup.CreateGroup.class) @RequestBody VillageMonitorDTO villageMonitorDTO);

    /**
     * Description 关闭预览
     *
     * @param villageMonitorParamDTO
     *            villageMonitorParamDTO
     * @return com.gsww.village.common.base.OpenResponse<java.lang.Boolean>
     * @author
     * @date 15:55 2023/1/9
     **/
    @ApiOperation(value = "关闭某个视频预览", notes = "关闭某个视频预览")
    @PostMapping("/ffmpegClose")
    @BusinessOperateLog(operateModule = "通过ffmpeg关闭某个视频预览", operateType = OperateType.QUERY, operateDesc = "关闭预览摄像头")
    OpenResponse<Boolean> ffmpegOff(@RequestBody VillageMonitorParamDTO villageMonitorParamDTO);

}
#DTO类自己定义的表接各类摄像头数据进来方便前端拼接展示,CURD工程师最爱的
@Data
@ApiModel(description = "VillageMonitorDTO")
public class VillageMonitorDTO {


    /**
     * 网络摄像机地址
     */
    @ApiModelProperty(value = "网络摄像机地址")
    @Trimmed
    @Length(max = 25, message = "网络摄像机地址不能超过25个字符",
        groups = {ValidatedGroup.CreateGroup.class, ValidatedGroup.UpdateGroup.class})
    @NotBlank(message = "网络摄像机地址不能为空")
    private String vedioAdress;
    /**
     * 端口
     */
    @ApiModelProperty(value = "端口")
    @Trimmed
    @Length(max = 10, message = "端口不能超过10个字符",
        groups = {ValidatedGroup.CreateGroup.class, ValidatedGroup.UpdateGroup.class})
    @NotEmpty(message = "端口不能为空")
    private String vedioPort;

    /**
     * 摄像机类型, 预留后面去实现 1大华0海康
     */
    @ApiModelProperty(value = "摄像机类型")
    private String vedioType;

    /**
     * 通道号
     */
    @ApiModelProperty(value = "通道号")
    @Trimmed
    @Length(max = 2, message = "通道号不能超过2个字符",
        groups = {ValidatedGroup.CreateGroup.class, ValidatedGroup.UpdateGroup.class})
    @NotEmpty(message = "通道号不能为空")
    private String channelNo;
    /**
     * 通道名称或摄像头名称
     */
    @ApiModelProperty(value = "通道名称或摄像头名称")
    @Trimmed
    @Length(max = 20, message = "通道名称或摄像头名称不能超过20个字符",
        groups = {ValidatedGroup.CreateGroup.class, ValidatedGroup.UpdateGroup.class})
    @NotEmpty(message = "通道名称或摄像头名称不能为空")
    private String channelName;
    /**
     * 码流类型 大华0 主流 1辅流 海康1主流0辅流
     */
    @ApiModelProperty(value = "码流类型")
    @Trimmed
    @Length(max = 10, message = "码流类型不能超过10个字符",
        groups = {ValidatedGroup.CreateGroup.class, ValidatedGroup.UpdateGroup.class})
    @NotEmpty(message = "码流类型不能为空")
    private String subType;
    /**
     * 账户名称
     */
    @ApiModelProperty(value = "账户名称")
    @Trimmed
    @Length(max = 20, message = "账户名称不能超过20个字符",
        groups = {ValidatedGroup.CreateGroup.class, ValidatedGroup.UpdateGroup.class})
    @NotEmpty(message = "账户名称不能为空")
    private String accountName;
    /**
     * 账户密码
     */
    @ApiModelProperty(value = "账户密码")
    @Trimmed
    @Length(max = 100, message = "账户密码不能超过100个字符",
        groups = {ValidatedGroup.CreateGroup.class, ValidatedGroup.UpdateGroup.class})
    @NotEmpty(message = "账户密码不能为空")
    private String accountPass;
    /**
     * 数据所属区划代码
     */
    @ApiModelProperty(value = "数据所属区划代码")
    @Trimmed
    @Length(max = 20, message = "数据所属区划代码不能超过20个字符",
        groups = {ValidatedGroup.CreateGroup.class, ValidatedGroup.UpdateGroup.class})
    private String areaCode;
    /**
     * 数据所属区划名称
     */
    @ApiModelProperty(value = "数据所属区划名称")
    @Trimmed
    @Length(max = 20, message = "数据所属区划名称不能超过20个字符",
        groups = {ValidatedGroup.CreateGroup.class, ValidatedGroup.UpdateGroup.class})
    private String areaName;
    /**
     * 经度
     */
    @ApiModelProperty(value = "经度")
    @Trimmed
    private Double longitude;
    /**
     * 纬度
     */
    @ApiModelProperty(value = "纬度")
    @Trimmed
    private Double latitude;
    /**
     * 备注
     */
    @ApiModelProperty(value = "备注")
    @Trimmed
    @Length(max = 20, message = "备注不能超过20个字符",
        groups = {ValidatedGroup.CreateGroup.class, ValidatedGroup.UpdateGroup.class})
    private String note;

    /**
     * m3u8Url 返回的地址
     */
    private String m3u8Url;
    
    @ApiModelProperty(value = "主键")
    private String pkId;

    /**
     * 创建人id
     */
    @ApiModelProperty(value = "创建人id")
    private String createUserId;

    /**
     * 标识位
     */
    private String flagEemp;

    /**
     * 创建人
     */
    @ApiModelProperty(value = "创建人")
    private String createUserName;

    /**
     * 更新人id
     */
    @ApiModelProperty(value = "更新人id")
    private String updateUserId;

    /**
     * 更新人
     */
    @ApiModelProperty(value = "更新人")
    private String updateUserName;

    /**
     * 创建时间
     */
    @ApiModelProperty(value = "创建时间")
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private Date createTime;

    /**
     * 更新时间
     */
    @ApiModelProperty(value = "更新时间")
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private Date updateTime;

    /**
     * 1、新增,2、修改,3、删除
     */
    @ApiModelProperty(value = "操作标志")
    private String statusEemp;

    /**
     * 逻辑删除标志位
     */
    @ApiModelProperty(value = "逻辑删除0未删除1已删除")
    private String deleted;

}

#接视频的存储在自己系统的业务表设计


DROP TABLE IF EXISTS `t_village_monitor`;
CREATE TABLE `t_village_monitor`  (
  `PK_ID` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '主键',
  `VEDIO_ADRESS` varchar(25) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '网络摄像机IP地址',
  `VEDIO_PORT` varchar(10) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT 'RTSP端口',
  `CHANNEL_NO` varchar(2) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '通道号',
  `CHANNEL_NAME` varchar(20) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '通道名称',
  `VEDIO_TYPE` varchar(10) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '设备类型',
  `SUB_TYPE` varchar(10) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '码流类型',
  `ACCOUNT_NAME` varchar(20) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '账户名称',
  `ACCOUNT_PASS` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '账户密码',
  `AREA_CODE` varchar(20) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '数据所属区划代码',
  `AREA_NAME` varchar(20) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '数据所属区划名称',
  `LONGITUDE` decimal(10, 6) NULL DEFAULT NULL COMMENT '经度',
  `LATITUDE` decimal(10, 6) NULL DEFAULT NULL COMMENT '纬度',
  `NOTE` varchar(20) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '备注',
  `FLAG_EEMP` varchar(1) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '标识位',
  `CREATE_USER_ID` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '创建用户ID',
  `CREATE_USER_NAME` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '创建用户名称',
  `UPDATE_USER_ID` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '修改用户ID',
  `UPDATE_USER_NAME` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '修改用户姓名',
  `STATUS_EEMP` varchar(1) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '逻辑标识位:(1:新增,2:修改,3:删除)',
  `CREATE_TIME` timestamp(0) NOT NULL DEFAULT CURRENT_TIMESTAMP(0) ON UPDATE CURRENT_TIMESTAMP(0) COMMENT '创建时间',
  `UPDATE_TIME` timestamp(0) NOT NULL DEFAULT CURRENT_TIMESTAMP(0) ON UPDATE CURRENT_TIMESTAMP(0) COMMENT '修改时间',
  `DELETED` char(1) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '删除标识(0:否,1:是)',
  PRIMARY KEY (`PK_ID`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '视频监控表' ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of t_village_monitor
-- ----------------------------
INSERT INTO `t_village_monitor` VALUES ('3E4DC4ECEA2847ED8E5D88BB2BA3BFCC', '61.171.182.292', '554', '8', '高清球机17', '1', '0', 'admin', 'qUReS8rEJLdHkqqRRg8uOg==', '340323202000', '仲兴镇', 117.195874, 33.204568, NULL, NULL, 'ea6f791673c640998e31dd29082621f1', 'ljp', 'ea6f791673c640998e31dd29082621f1', 'ljp', '2', '2023-01-10 10:50:50', '2023-01-10 11:38:59', '0');
INSERT INTO `t_village_monitor` VALUES ('3E4DC4ECEA2847ED8E5D88BB2BA3BFC2', '62.172.182.291', '554', '6', '高清球机15', '1', '0', 'admin', 'qUReS8rEJLdHkqqRRg8uOg==', '340323103000', '连城镇', 117.368578, 33.292745, NULL, NULL, 'ea6f791673c640998e31dd29082621f1', 'ljp', 'ea6f791673c640998e31dd29082621f1', 'ljp', '2', '2023-01-10 10:50:32', '2023-01-10 11:38:41', '0');

#Controller


@RestController
public class CameraSecretInfoController implements CameraSecretInfoApi {

    @Autowired
    CameraSecretInfoService cmeraSecretInfoService;

    @Override
    public OpenResponse<VillageMonitorDTO> ffmpegOpen(VillageMonitorDTO villageMonitorDTO) {
        if (StrUtil.isEmpty(villageMonitorDTO.getPkId())) {
            throw new BusinessException("摄像ID不能为空");
        }
        villageMonitorDTO.setAccountPass(AesEncryptUtil.desEncrypt(villageMonitorDTO.getAccountPass()));
        VillageMonitorDTO returnDto = cmeraSecretInfoService.ffmpegOpen(villageMonitorDTO);
        // 休息3秒钟先加载一下视频,体验好些就不加
        if (!returnDto.getReplayFlag()) {
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        return OpenResponse.success(returnDto);
    }

    @Override
    public OpenResponse<Boolean> ffmpegOff(VillageMonitorParamDTO villageMonitorParamDTO) {
        if (StrUtil.isEmpty(villageMonitorParamDTO.getPkId())) {
            throw new BusinessException("摄像ID不能为空");
        }
        cmeraSecretInfoService.ffmpegOff(villageMonitorParamDTO.getPkId());
        return OpenResponse.success();
    }

    @Override
    public OpenResponse<List<VillageMonitorDTO>> ffmpegOpenList(VillageMonitorListParamDTO villageMonitorListParamDTO) {
        List<VillageMonitorDTO> list = new ArrayList<>();
        List<Boolean> booleanList = new ArrayList<>();
        int size = villageMonitorListParamDTO.getListVideos().size();
        villageMonitorListParamDTO.getListVideos().stream().forEach(dto -> {
            dto.setAccountPass(AesEncryptUtil.desEncrypt(dto.getAccountPass()));
            VillageMonitorDTO returnDto = cmeraSecretInfoService.ffmpegOpen(dto);
            returnDto.setAccountPass("");
            list.add(returnDto);
            if (returnDto.getReplayFlag()) {
                booleanList.add(returnDto.getReplayFlag());
            }
        });
        // 休息3秒钟先加载一下视频,体验好些就不加
        if (!(booleanList.size() == size)) {
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        return OpenResponse.success(list);
    }

    @Override
    public OpenResponse<Boolean> ffmpegOffList(IdsDTO<String> ids) {
        ids.getIds().stream().forEach(id -> cmeraSecretInfoService.ffmpegOff(id));
        return OpenResponse.success();
    }
}

#service接口和实现类


public interface CameraSecretInfoService {

    /**
     * Description 返回M3u8地址
     *
     * @param villageMonitorDTO
     *            villageMonitorParamDTO
     * @return java.lang.String
     * @author
     * @date 16:04 2023/1/7
     **/
    VillageMonitorDTO ffmpegOpen(VillageMonitorDTO villageMonitorDTO);

    /**
     * Description 关闭预览
     *
     * @param pkId
     *            pkId
     * @author
     * @date 16:04 2023/1/7
     **/
    void ffmpegOff(String pkId);

    /**
     * Description 关闭预览
     *
     * @param pkId
     *            pkId
     * @author
     * @date 16:04 2023/1/7
     **/
    void stopBackVideoDelay(String pkId);

    /**
     * Description 停止并删除过期的预览
     *
     * @param userId
     *            token1
     * @return void
     * @author
     * @date 15:56 2023/1/7
     **/
    void removeExpiredVideo(String userId, String vedioId) throws IOException;

    /**
     * Description 删除文件夹下面所有文件
     *
     * @param basePath
     *            basePath
     * @return void
     * @author
     * @date 15:57 2023/1/7
     **/
    void deleteDir(String basePath) throws IOException;

    void stopBackVideo(String vedioId) throws IOException;
}

#// 过期删除关闭视频流的KEY用于redis的回调
    public static final String FFMPEG_USER_KEY = "bladeFile:";
#视频预览实现
@Service
@Slf4j
public class CameraSecretInfoServiceImpl implements CameraSecretInfoService {

    @Resource
    private StringRedisTemplate stringRedisTemplate;

    @Autowired
    FfmpegConfig ffmpegConfig;

    @Autowired
    CommandManager manager;

    /**
     * Description 开启预览,启动ffmpeg的key规则为用户ID+视频监控表ID,用户失效的时候按用户全部删除
     *
     * @param villageMonitorDTO
     * @return java.lang.String
     * @author
     * @date 17:44 2023/1/7
     **/
    @Override
    public VillageMonitorDTO ffmpegOpen(VillageMonitorDTO villageMonitorDTO) {
        // 当前用户ID
        String userId = AbsExtDomainUtil.getUserInfo().getUserId();
        // 存放的m3u8文件夹地址 root/appUrl/userId /pkId/pkId.m3u8
        FileUtil.makeDir(ffmpegConfig.getBaseSavePath(userId, villageMonitorDTO.getPkId()));
        String codeId = userId + ":" + villageMonitorDTO.getPkId();
        if (ObjectUtil.isEmpty(manager.query(codeId))
            && Boolean.FALSE.equals(stringRedisTemplate.hasKey(Constants.FFMPEG_USER_KEY + codeId))) {
            manager.stop(codeId); // 先停止视频
            manager.start(codeId,
                    CommandBuidlerFactory.createBuidler().add("ffmpeg").add("-rtsp_transport", "tcp")
                            .add("-i", getRtspUrl(villageMonitorDTO)) // 取videoUrl
                            .add("-strict", "-2")
                            //转成h264国标
                            .add("-vcodec", "libx264").add("-vsync", "2")
                            .add("-preset:v").add("ultrafast").add("-tune:v").add("zerolatency")
                            // 不要声音
                            //.add("-an")
                            // 音频
                            .add("-c:a", "aac")
                            .add("-hls_time", "3").add("-hls_list_size", "2").add("-hls_wrap", "3").add("-y")
                                  //如果使用http-flv把下面的地址换成类似rtmp://172.16.121.75:1935/live/test
                            .add(ffmpegConfig.getSaveTempFileName(userId, villageMonitorDTO.getPkId())));
            CommandTasker info = manager.query(codeId);
            villageMonitorDTO.setReplayFlag(false);
            log.info("启动ffmpeg:" + info.toString());
        }  else if (ObjectUtil.isEmpty(manager.query(codeId))
            && Boolean.TRUE.equals(stringRedisTemplate.hasKey(Constants.FFMPEG_USER_KEY + codeId))) {
            try {
                removeDir(userId, villageMonitorDTO.getPkId());
            } catch (IOException e) {
                e.printStackTrace();
            }
            manager.stop(codeId); // 先停止视频
            manager.start(codeId,
                CommandBuidlerFactory.createBuidler().add("ffmpeg").add("-rtsp_transport", "tcp")
                    .add("-i", getRtspUrl(villageMonitorDTO)) // 取videoUrl
                    .add("-strict", "-2")
                    // 转成h264
                    .add("-vcodec", "libx264").add("-vsync", "2").add("-preset:v").add("ultrafast").add("-tune:v")
                    .add("zerolatency")
                    // 不要声音
                    // .add("-an")
                    // 音频
                    .add("-c:a", "aac").add("-hls_time", "5").add("-hls_list_size", "2").add("-hls_wrap", "3").add("-y")
                    .add(ffmpegConfig.getSaveTempFileName(userId, villageMonitorDTO.getPkId())));
            CommandTasker info = manager.query(codeId);
            villageMonitorDTO.setReplayFlag(false);
            log.info("启动ffmpeg:" + info.toString());

        } else {
            villageMonitorDTO.setReplayFlag(true);
        }
        // 设置ffmpeg视频失效时间,避免长时间占用资源,造成内存溢出
        stringRedisTemplate.opsForValue().set(Constants.FFMPEG_USER_KEY + codeId, codeId,
            ffmpegConfig.getExpireMinutes(), TimeUnit.MINUTES);
        // 返回路径根据ffmpeg存放视频路径+nginx代理灵活配置
        //如果使用http-flv的话实际返回地址换成类似http://172.16.121.75:8099/live?port=1935&app=live&stream=test     
        villageMonitorDTO.setM3u8Url(ffmpegConfig.getRealLiveUrl(userId, villageMonitorDTO.getPkId()));
        return villageMonitorDTO;
    }

    @Override
    public void ffmpegOff(String pkId) {
        stopBackVideoDelay(pkId);
    }

    @Override
    public void removeExpiredVideo(String userId, String vedioId) throws IOException {
        String basePathAll = ffmpegConfig.getBaseSavePath(userId, vedioId);
        File fileExist = new File(basePathAll);
        // 文件夹文件夹存在,则停止后删除
        if (fileExist.exists()) {
            stopBackVideo(userId, vedioId);
            if (fileExist.exists()) {
                deleteDir(basePathAll);
            }
        }
    }

    @Override
    public void deleteDir(String basePath) throws IOException {
        Path path = Paths.get(basePath);
        Files.walkFileTree(path, new SimpleFileVisitor<Path>() {
            // 先去遍历删除文件
            @Override
            public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
                Files.delete(file);
                log.warn("文件被删除 : %s%n", file);
                return FileVisitResult.CONTINUE;
            }

            // 再去遍历删除目录
            @Override
            public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
                Files.delete(dir);
                log.warn("文件夹被删除: %s%n", dir);
                return FileVisitResult.CONTINUE;
            }

        });
    }

    @Override
    public void stopBackVideo(String vedioId) throws IOException {
        String userId = AbsExtDomainUtil.getUserInfo().getUserId();
        String basePath = ffmpegConfig.getBaseSavePath(userId, vedioId);
        File fileExist = new File(basePath);
        String codeId = userId + ":" + vedioId;
        // 停止ffmpeg转码
        manager.stop(codeId);
        manager.start(codeId, CommandBuidlerFactory.createBuidler().add("rm -rf", basePath));
        // 对文件夹进行删除操作
        if (fileExist.exists()) {
            deleteDir(basePath);
        }
        // 清除相应的redis
        stringRedisTemplate.delete(Constants.FFMPEG_USER_KEY + codeId);
    }

    @Override
    public void stopBackVideoDelay(String vedioId) {
        String userId = AbsExtDomainUtil.getUserInfo().getUserId();
        String codeId = userId + ":" + vedioId;
        // 延迟10秒关闭,免得重复拉
        log.info(codeId + " 将在" + ffmpegConfig.getExpireMinutesDelay() + "秒后关闭流");
        stringRedisTemplate.opsForValue().set(Constants.FFMPEG_USER_KEY + codeId, codeId,
            ffmpegConfig.getExpireMinutesDelay(), TimeUnit.SECONDS);
    }

    public void stopBackVideo(String userId, String vedioId) throws IOException {
        // 解析jwt的token值,拿到最后面一截,这个也是不会重复
        String basePath = FilePathUtil.getFfmpegTmpPath() + File.separator + ffmpegConfig.getLiveApp() + File.separator
            + userId + File.separator + vedioId + File.separator;
        File fileExist = new File(basePath);
        String codeId = userId + ":" + vedioId;
        // 停止ffmpeg转码
        manager.stop(codeId);
        manager.start(codeId, CommandBuidlerFactory.createBuidler().add("rm -rf", basePath));
        // 对文件夹进行删除操作
        if (fileExist.exists()) {
            deleteDir(basePath);
        }
        // 清除相应的redis
        stringRedisTemplate.delete(Constants.FFMPEG_USER_KEY + codeId);
    }

    /**
     * 得到文件名称
     *
     * @param file
     *            文件
     * @param fileNames
     *            文件名
     * @return {@link List}<{@link String}>
     */
    private List<String> getFileNames(File file, List<String> fileNames) {
        File[] files = file.listFiles();
        for (File f : files) {
            if (f.isDirectory()) {
                fileNames.add(f.getName());
            }
        }
        return fileNames;
    }

    /**
     * Description 获取到设备的rtsp流地址
     *
     * @param villageMonitorDTO
     *            villageMonitorDTO
     * @return java.lang.String
     * @author
     * @date 13:45 2023/1/10
     **/
    private String getRtspUrl(VillageMonitorDTO villageMonitorDTO) {
        StringBuilder stringBuild = new StringBuilder("rtsp://");
        stringBuild.append(villageMonitorDTO.getAccountName() + ":");
        stringBuild.append(villageMonitorDTO.getAccountPass() + "@");
        stringBuild.append(villageMonitorDTO.getVedioAdress() + ":");
        stringBuild.append(villageMonitorDTO.getVedioPort());
        // 默认大华
        if (StrUtil.isEmpty(villageMonitorDTO.getVedioType())
            || Constants.ONE.equals(villageMonitorDTO.getVedioType())) {
            stringBuild.append("/cam/realmonitor?");
            stringBuild.append("channel=" + villageMonitorDTO.getChannelNo());
            stringBuild.append("&subtype=" + villageMonitorDTO.getSubType());
        } else {
            stringBuild.append("/Streaming/Channels/");
            stringBuild.append(villageMonitorDTO.getChannelNo() + villageMonitorDTO.getSubType());
        }
        // stringBuild.append("\\\"");
        return stringBuild.toString();
    }
}

#开源保活处理下避免内存溢出造成应用程序奔溃


public class KeepAliveHandler extends Thread{
    /**待处理队列*/
    private static Queue<String> queue=null;
    
    public int err_index=0;//错误计数 5次调用后自动停止

    public volatile int stop_index=0;//安全停止线程标记
    
    /** 任务持久化器*/
    private TaskDao taskDao = null;
    
    public KeepAliveHandler(TaskDao taskDao) {
        super();
        this.taskDao=taskDao;
        queue=new ConcurrentLinkedQueue<>();
    }

    public static void add(String id ) {
        if(queue!=null) {
            queue.offer(id);
        }
    }
    
    public boolean stop(Process process) {
        if (process != null) {
            process.destroy();
            return true;
        }
        return false;
    }
    
    @Override
    public void run() {
        for(;stop_index==0;) {
            if(queue==null) {
                continue;
            }
            String id=null;
            CommandTasker task=null;
            
            try {
                while(queue.peek() != null) {
                    System.err.println("准备重启任务:"+queue);
                    id=queue.poll();
                    task=taskDao.get(id);
                    //重启任务
                    ExecUtil.restart(task);
                }
            }catch(Exception e) {

                //重启任务失败,5次后自动停止避免内存溢出
                err_index++;
                System.err.println(id+" 任务重启失败" + err_index + "次,详情:"+task);
                if (err_index >=5) {
                    stop_index=1;
                    System.err.println(id+" 任务重启失败,保活关闭");
                }
            }
        }
    }
    
    @Override
    public void interrupt() {
        stop_index=1;
    }
    
}

#开源代码配置工具类linux路径问题修改
PropertiesUtil类修改
/**
     * 获取对应文件路径下的文件流 兼容linux打包读取路径
     * 
     * @param path
     * @return
     * @throws FileNotFoundException
     */
    public static InputStream getInputStream(String path) throws IOException {
        return new ClassPathResource(path).getInputStream();
    }

接口测试

#前端测试html ,src修改为输出的m3u8地址

#前端部分代码

链接:百度网盘 请输入提取码

提取码:0824

<!DOCTYPE html>
<html lang="zh-CN">

<head>
    <meta charset="UTF-8">
    <title>前端播放m3u8格式视频</title>
    <link href="https://vjs.zencdn.net/7.4.1/video-js.css" rel="stylesheet">
    <script src='https://vjs.zencdn.net/7.4.1/video.js'></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/videojs-contrib-hls/5.15.0/videojs-contrib-hls.min.js"
        type="text/javascript"></script>
    <!-- videojs-contrib-hls 用于在电脑端播放 如果只需手机播放可以不引入 -->
</head>

<body>
    <style>
        .video-js .vjs-tech {
            position: relative !important;
        }
    </style>
    <div>
        <video id="myVideo" class="video-js vjs-default-skin vjs-big-play-centered" controls preload="auto"
            data-setup='{}' style='width: 100%;height: auto'>
            <source id="source"
                src="http://IP:8099/hls/channel1.m3u8"
                type="application/x-mpegURL">
            </source>
        </video>
    </div>
    <div class="qiehuan"
        style="width:100px;height: 100px;background: red;margin:0 auto;line-height: 100px;color:#fff;text-align: center">
        切换视频</div>
</body>

<script>
    // videojs 简单使用
    var myVideo = videojs('myVideo', {
        bigPlayButton: true,
        textTrackDisplay: false,
        posterImage: false,
        errorDisplay: false,
    })
    myVideo.play()
    var changeVideo = function (vdoSrc) {
        if (/\.m3u8$/.test(vdoSrc)) { //判断视频源是否是m3u8的格式
            myVideo.src({
                src: vdoSrc,
                type: 'application/x-mpegURL' //在重新添加视频源的时候需要给新的type的值
            })
        } else {
            myVideo.src(vdoSrc)
        }
        myVideo.load();
        myVideo.play();
    }
    var src = 'http://IP:8099/hls/channel1.m3u8';
    document.querySelector('.qiehuan').addEventListener('click', function () {
        changeVideo(src);
    })
</script>

方案二

在方案一的代码中把ffmpeg组装的地址换成

ffmpeg -re -rtsp_transport tcp -i "rtsp://admin:admin123@IP:554/cam/realmonitor?channel=3&subtype=0" -f flv -vcodec h264 -vprofile baseline -acodec aac -ar 44100 -strict -2 -ac 1 -f flv -s 640*360 -q 10 "rtmp://172.16.121.75(nginxIP):1935(rtmp端口)/live(服务名称)/test"

通过vlc打开

http://172.16.121.75:8099/live?port=1939&app=live&stream=test

前端可以通过flv.js访问,vue的代码自行改造,以下代码保存为html直接打开


<!doctype html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport"
        content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>播放http-flv</title>
</head>
<body>
<video id="videoElement"></video>
<script src="https://cdn.bootcdn.net/ajax/libs/flv.js/1.6.2/flv.min.js"></script>
<script>
  if (flvjs.isSupported()) {
      const videoElement = document.getElementById('videoElement');
      const flvPlayer = flvjs.createPlayer({
        type: 'flv',
        url: 'http://172.16.121.75:8099/live?port=1935&app=live&stream=test'
      });
      flvPlayer.attachMediaElement(videoElement);
      flvPlayer.load();
      flvPlayer.play();
    }
</script>
</body>
</html>

效果如下

参考博文Java 监控直播流rtsp协议转rtmp、hls、httpflv协议返回浏览器_rtsp 转httpflv-CSDN博客

方案三:

通过ZLM免费开源国标平台搭建接入的示例,看看拉流方式的实现,适合并发较高情况,不过需要搭建个流媒体服务,有支持http-flv可以开箱就用:

参考WVP+ZLMediaKit+MediaServerUI实现摄像头GB28181推流播放录制 - 我的技术展示

前端如果想分离出来简单调整下代码实现前后端分离

如果想支持拉流拉成m3u8格式可以简单调整下如下,没有深入改会有很多隐患

/home/village/ZLMediaKit/release/linux/Debug/config.ini

cmd添加flv和m3u8格式支持

代码调整下


 if ("ffmpeg".equals(param.getType()) && "ffmpeg.cmd2".equals(param.getFfmpeg_cmd_key())) {
            dstUrl = String.format("/usr/local/nginx/html/hls/%s-%s%s", param.getApp(),
                    param.getStream(),".m3u8");
        }

配置文件更改

前端分离需要更改的地方如下

index.js更改替换下build


build: {
    // Template for index.html
    index: path.resolve(__dirname, '../dist/index.html'),

    // Paths
    assetsRoot: path.resolve(__dirname, '../dist'),
    assetsSubDirectory: 'static',
    assetsPublicPath: './',
    proxyTable: {
      '/zlm': {
        target:'http://172.16.121.75:18080',            //请求的目标地址的BaseURL
        ws:true,
        changeOrigin:true,                            //是否开启跨域
        pathRewrite:{
          '^/zlm':''                                    //重新路径,把EzaYun开头的,替换成 ''
        }
      },
      '/static/snap': {
        target: 'http://127.16.121.75:18080',
        changeOrigin: true,
        // pathRewrite: {
        //   '^/static/snap': '/static/snap'
        // }
      }
    },

这样前端打包完成后就和一般vue打包一样生成个dist,这样通过nginx就可以简单部署前端代码

配置调整下

nginx加上流媒体服务访问m3u8


#流媒体前端代理 
location /zlm-server/ {
                alias /home/village/wvp/web/dist/;
                index  index.html index.htm;
            }
        #流媒体接口代理
        location /zlm/api/ {
           proxy_pass  http://172.16.121.75:18080/api/;

        }
        location /zlm/api/user/login {
           proxy_pass http://172.16.121.75:18080/api/user/login;
        }
        #前端获取流媒体 
        location /zlmhls {
                                # Serve HLS fragments
                                types {
                                                application/vnd.apple.mpegurl m3u8;
                                                video/mp2t ts;
                                }
                    #root html;
                    alias /usr/local/nginx/html/hls;

                }

拉流占用情况

  • 3
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
在使用VB调用海康网络摄像机的demo时,我们首先需要确保计算机上已安装海康网络摄像机的相关驱动和SDK。 接下来,我们可以按照以下步骤进行调用: 1. 导入海康网络摄像机SDK的相关库文件。这些文件包括SDK的DLL文件和相关的引用库文件。可以将这些文件复制到项目的工作目录下,并在VB中通过项目属性中的引用设置添加进去。 2. 在VB代码中声明与SDK相关的类型和函数。根据海康网络摄像机SDK提供的接口文档,确定需要使用的类型和函数,并在VB代码中进行声明。例如,可以使用Declare语句声明函数的原型,以及使用Structure语句声明相关的结构体。 3. 初始化摄像机SDK。在VB程序的合适位置,调用SDK提供的初始化函数,以便确保SDK的正常使用。可以根据需要设置一些初始化参数,如IP地址、端口号等。 4. 创建预览窗口并显示图像。在VB窗体上添加一个用于显示图像的控件,如PictureBox控件。在初始化完成后,调用SDK提供的预览函数,即可将摄像机的图像显示在预览窗口上。 5. 控制摄像机的其他功能。根据需要,可以调用SDK提供的其他函数来实现摄像机的其他功能,如云台控制、录像、截图等。这些函数的调用方式与预览函数类似,只需根据接口文档来确定参数和返回值的使用方式。 总之,通过以上步骤可以在VB中调用海康网络摄像机的demo,实现图像预览和控制等功能。需要注意的是,在调用SDK时需要确保与SDK版本对应的函数和类型的正确使用,以及正确处理返回值和错误信息,以保证程序的稳定性和功能的正确实现。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值