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

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

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

  1. rtsp+nginx转m3u8播放

  1. rtsp+nginx转flv播放

  1. ZLMediaKit+wvp拉流

  1. rtsp转flv(简单方式)

https://developer.aliyun.com/article/867004?scm=20140722.ID_community@@article@@867004.P_121.MO_938-ST_5186-V_1-ID_community@@article@@867004-OR_rec

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

一、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简单负载均衡参考

https://blog.csdn.net/weixin_37530941/article/details/128702616

#直播相关的配置的详细介绍https://blog.51cto.com/eguid/5100113

二、对外网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环境程序部署相关下载包分享和安装过程

#链接:https://pan.baidu.com/s/1gTB0Hjxa7mqnBjPMKe-YQw

#提取码: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共享地址

链接:https://pan.baidu.com/s/1F9QKRXpubngbd7X1xypkwA

提取码:0824

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

https://blog.csdn.net/weixin_43288858/article/details/128253490?spm=1001.2014.3001.5502

https://www.freesion.com/article/5775913700/

四、代码改造

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

http://github.com/eguid/FFCH4J

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

#POM引入

 
        
            org.bytedeco
            javacv
            1.5.4
        
        
        
            org.bytedeco
            ffmpeg-platform
            4.3.1-1.5.4
        
       
        
            org.springframework.boot
            spring-boot-starter-data-redis
        

#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
     * @author
     * @date 15:55 2023/1/9
     **/
    @ApiOperation(value = "预览某个视频返回m3u8地址", notes = "预览某个视频返回m3u8地址")
    @PostMapping("/ffmpegOpen")
    @BusinessOperateLog(operateModule = "通过ffmpeg预览某个视频", operateType = OperateType.QUERY, operateDesc = "预览摄像头")
    OpenResponse
        ffmpegOpen(@Validated(ValidatedGroup.CreateGroup.class) @RequestBody VillageMonitorDTO villageMonitorDTO);

    /**
     * Description 关闭预览
     *
     * @param villageMonitorParamDTO
     *            villageMonitorParamDTO
     * @return com.gsww.village.common.base.OpenResponse
     * @author
     * @date 15:55 2023/1/9
     **/
    @ApiOperation(value = "关闭某个视频预览", notes = "关闭某个视频预览")
    @PostMapping("/ffmpegClose")
    @BusinessOperateLog(operateModule = "通过ffmpeg关闭某个视频预览", operateType = OperateType.QUERY, operateDesc = "关闭预览摄像头")
    OpenResponse 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 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 ffmpegOff(VillageMonitorParamDTO villageMonitorParamDTO) {
        if (StrUtil.isEmpty(villageMonitorParamDTO.getPkId())) {
            throw new BusinessException("摄像ID不能为空");
        }
        cmeraSecretInfoService.ffmpegOff(villageMonitorParamDTO.getPkId());
        return OpenResponse.success();
    }

    @Override
    public OpenResponse> ffmpegOpenList(VillageMonitorListParamDTO villageMonitorListParamDTO) {
        List list = new ArrayList<>();
        List 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 ffmpegOffList(IdsDTO 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() {
            // 先去遍历删除文件
            @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 getFileNames(File file, List 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 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();
    }

接口测试

java接口方式调用海康大华摄像机预览。_第1张图片

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

#前端部分代码

链接:https://pan.baidu.com/s/1Ev-1cuA5WRSC_vbCSKA-Wg

提取码:0824





    
    前端播放m3u8格式视频
    
    
    
    



    
    
切换视频
java接口方式调用海康大华摄像机预览。_第2张图片
java接口方式调用海康大华摄像机预览。_第3张图片

方案二

在方案一的代码中把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直接打开




  
  
  
  播放http-flv






效果如下

java接口方式调用海康大华摄像机预览。_第4张图片

参考博文https://blog.csdn.net/Candyz7/article/details/126741970

方案三:

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

参考https://notemi.cn/wvp---zlmedia-kit---mediaserverui-to-realize-streaming-playback-and-recording-of-camera-gb28181.html

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

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

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

cmd添加flv和m3u8格式支持

代码调整下

java接口方式调用海康大华摄像机预览。_第5张图片
 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");
        }

配置文件更改

java接口方式调用海康大华摄像机预览。_第6张图片

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

java接口方式调用海康大华摄像机预览。_第7张图片

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就可以简单部署前端代码

java接口方式调用海康大华摄像机预览。_第8张图片

配置调整下

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;

                }
java接口方式调用海康大华摄像机预览。_第9张图片
java接口方式调用海康大华摄像机预览。_第10张图片

拉流占用情况

java接口方式调用海康大华摄像机预览。_第11张图片

你可能感兴趣的:(笔记,ffmpeg,nginx)