Jelajahi Sumber

Merge branch 'wvp-28181-2.0' into wvp-28181-2.0

648540858 2 tahun lalu
induk
melakukan
bdf799fb64
26 mengubah file dengan 1071 tambahan dan 917 penghapusan
  1. 59 100
      README.md
  2. 2 1
      doc/_content/introduction/config.md
  3. TEMPAT SAMPAH
      doc/_media/2.png
  4. TEMPAT SAMPAH
      doc/_media/3-1.png
  5. TEMPAT SAMPAH
      doc/_media/3-2.png
  6. TEMPAT SAMPAH
      doc/_media/3-3.png
  7. TEMPAT SAMPAH
      doc/_media/3.png
  8. TEMPAT SAMPAH
      doc/_media/index.png
  9. 1 0
      sql/mysql.sql
  10. 10 4
      src/main/java/com/genersoft/iot/vmp/gb28181/session/CatalogDataCatch.java
  11. 151 113
      src/main/java/com/genersoft/iot/vmp/gb28181/transmit/cmd/impl/SIPCommanderFroPlatform.java
  12. 3 62
      src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/SIPRequestProcessorParent.java
  13. 1 1
      src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/InviteRequestProcessor.java
  14. 3 3
      src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/query/cmd/CatalogQueryMessageHandler.java
  15. 631 619
      src/main/java/com/genersoft/iot/vmp/media/zlm/ZLMHttpHookListener.java
  16. 36 0
      src/main/java/com/genersoft/iot/vmp/media/zlm/dto/hook/HookResult.java
  17. 44 0
      src/main/java/com/genersoft/iot/vmp/media/zlm/dto/hook/HookResultForOnPublish.java
  18. 4 0
      src/main/java/com/genersoft/iot/vmp/service/impl/GbStreamServiceImpl.java
  19. 3 1
      src/main/java/com/genersoft/iot/vmp/service/impl/StreamPushServiceImpl.java
  20. 2 1
      src/main/java/com/genersoft/iot/vmp/storager/IVideoManagerStorage.java
  21. 1 1
      src/main/java/com/genersoft/iot/vmp/storager/dao/UserMapper.java
  22. 113 0
      src/main/java/com/genersoft/iot/vmp/storager/impl/VideoManagerStorageImpl.java
  23. 3 4
      src/main/java/com/genersoft/iot/vmp/utils/redis/RedisUtil.java
  24. 1 5
      src/main/java/com/genersoft/iot/vmp/vmanager/user/UserController.java
  25. 1 1
      src/main/resources/all-application.yml
  26. 2 1
      web_src/src/components/GBRecordDetail.vue

+ 59 - 100
README.md

@@ -1,4 +1,4 @@
-![logo](https://raw.githubusercontent.com/648540858/wvp-GB28181-pro/wvp-28181-2.0/web_src/static/logo.png)
+![logo](doc/_media/logo.png)
 # 开箱即用的28181协议视频平台
 
 [![Build Status](https://travis-ci.org/xia-chu/ZLMediaKit.svg?branch=master)](https://travis-ci.org/xia-chu/ZLMediaKit)
@@ -17,7 +17,7 @@ WEB VIDEO PLATFORM是一个基于GB28181-2016标准实现的开箱即用的网
 # 应用场景:
 支持浏览器无插件播放摄像头视频。  
 支持摄像机、平台、NVR等设备接入。 
-支持国标级联。  
+支持国标级联。多平台级联。跨网视频预览。
 支持rtsp/rtmp等视频流转发到国标平台。  
 支持rtsp/rtmp等推流转发到国标平台。  
 
@@ -31,62 +31,49 @@ WEB VIDEO PLATFORM是一个基于GB28181-2016标准实现的开箱即用的网
 https://gitee.com/pan648540858/wvp-GB28181-pro.git
 
 # 截图
-![build_1.png](https://images.gitee.com/uploads/images/2022/0304/101513_79632720_1018729.png "2022-03-04_09-51.png")
-![build_1.png](https://images.gitee.com/uploads/images/2022/0304/103025_5df016f9_1018729.png "2022-03-04_10-27.png")
-![build_1.png](https://images.gitee.com/uploads/images/2022/0304/101706_088fbafa_1018729.png "2022-03-04_09-52_1.png")
-![build_1.png](https://images.gitee.com/uploads/images/2022/0304/101756_3d662828_1018729.png "2022-03-04_10-00_1.png")
-![build_1.png](https://images.gitee.com/uploads/images/2022/0304/101823_19050c66_1018729.png "2022-03-04_10-12_1.png")
-![build_1.png](https://images.gitee.com/uploads/images/2022/0304/101848_e5a39557_1018729.png "2022-03-04_10-12_2.png")
-![build_1.png](https://images.gitee.com/uploads/images/2022/0304/101919_ee5b8c79_1018729.png "2022-03-04_10-13.png")
-
-# 1.0 基础特性  
-1. 视频预览;  
-2. 云台控制(方向、缩放控制);  
-3. 视频设备信息同步;   
-4. 离在线监控;  
-5. 录像查询与回放(基于NVR\DVR,暂不支持快进、seek操作);  
-6. 无人观看自动断流;    
-7. 支持UDP和TCP两种国标信令传输模式; 
-8. 集成web界面, 不需要单独部署前端服务, 直接利用wvp内置文件服务部署, 随wvp一起部署;   
-9. 支持平台接入, 针对大平台大量设备的情况进行优化;  
-10. 支持检索,通道筛选;  
-11. 支持自动配置ZLM媒体服务, 减少因配置问题所出现的问题;  
-12. 支持启用udp多端口模式, 提高udp模式下媒体传输性能;  
-13. 支持通道是否含有音频的设置;  
-14. 支持通道子目录查询;  
-15. 支持udp/tcp国标流传输模式;  
-16. 支持直接输出RTSP、RTMP、HTTP-FLV、Websocket-FLV、HLS多种协议流地址  
-17. 支持国标网络校时  
-18. 支持公网部署, 支持wvp与zlm分开部署   
-19. 支持播放h265, g.711格式的流(需要将closeWaitRTPInfo设为false)
-20. 报警信息处理,支持向前端推送报警信息
-
-# 1.0 新支持特性  
-1. 集成web界面, 不需要单独部署前端服务, 直接利用wvp内置文件服务部署, 随wvp一起部署;   
-2. 支持平台接入, 针对大平台大量设备的情况进行优化;  
-3. 支持检索,通道筛选;  
-4. 支持自动配置ZLM媒体服务, 减少因配置问题所出现的问题;  
-5. 支持启用udp多端口模式, 提高udp模式下媒体传输性能;  
-6. 支持通道是否含有音频的设置;  
-7. 支持通道子目录查询;  
-8. 支持udp/tcp国标流传输模式;  
-9. 支持直接输出RTSP、RTMP、HTTP-FLV、Websocket-FLV、HLS多种协议流地址  
-10. 支持国标网络校时  
-11. 支持公网部署, 支持wvp与zlm分开部署   
-12. 支持播放h265, g.711格式的流   
-13. 支持固定流地址和自动点播,同时支持未点播时直接播放流地址,代码自动发起点播.  ( [查看WIKI](https://github.com/648540858/wvp-GB28181-pro/wiki/%E5%A6%82%E4%BD%95%E4%BD%BF%E7%94%A8%E5%9B%BA%E5%AE%9A%E6%92%AD%E6%94%BE%E5%9C%B0%E5%9D%80%E4%B8%8E%E8%87%AA%E5%8A%A8%E7%82%B9%E6%92%AD))
-14. 报警信息处理,支持向前端推送报警信息
-15. 支持订阅与通知方法
-   -  [X] 移动位置订阅
-   -  [X] 移动位置通知处理
-   -  [X] 报警事件订阅
-   -  [X] 报警事件通知处理
-   -  [X] 设备目录订阅
-   -  [X] 设备目录通知处理
-16. 移动位置查询和显示,可通过配置文件设置移动位置历史是否存储
-
-# 2.0 支持特性
-- [X] 国标通道向上级联
+![index](doc/_media/index.png "index.png")
+![2](doc/_media/2.png "2.png")
+![3](doc/_media/3.png "3.png")
+![3-1](doc/_media/3-1.png "3-1.png")
+![3-2](doc/_media/3-2.png "3-2.png")
+![3-3](doc/_media/3-3.png "3-3.png")
+![build_1](https://images.gitee.com/uploads/images/2022/0304/101919_ee5b8c79_1018729.png "2022-03-04_10-13.png")
+
+# 功能特性 
+- [X] 集成web界面
+- [X] 兼容性良好
+- [X] 支持电子地图,支持接入WGS84和GCJ02两种坐标系,并且自动转化为合适的坐标系进行展示和分发
+- [X] 接入设备
+  - [X] 视频预览
+  - [X] 无限制接入路数,能接入多少设备只取决于你的服务器性能
+  - [X] 云台控制,控制设备转向,拉近,拉远
+  - [X] 预置位查询,使用与设置
+  - [X] 查询NVR/IPC上的录像与播放,支持指定时间播放与下载
+  - [X] 无人观看自动断流,节省流量
+  - [X] 视频设备信息同步
+  - [X] 离在线监控
+  - [X] 支持直接输出RTSP、RTMP、HTTP-FLV、Websocket-FLV、HLS多种协议流地址
+  - [X] 支持通过一个流地址直接观看摄像头,无需登录以及调用任何接口
+  - [X] 支持UDP和TCP两种国标信令传输模式
+  - [X] 支持UDP和TCP两种国标流传输模式
+  - [X] 支持检索,通道筛选
+  - [X] 支持通道子目录查询
+  - [X] 支持过滤音频,防止杂音影响观看
+  - [X] 支持国标网络校时
+  - [X] 支持播放H264和H265
+  - [X] 报警信息处理,支持向前端推送报警信息
+  - [X] 支持订阅与通知方法
+    - [X] 移动位置订阅
+    - [X] 移动位置通知处理
+    - [X] 报警事件订阅
+    - [X] 报警事件通知处理
+    - [X] 设备目录订阅
+    - [X] 设备目录通知处理
+  -  [X] 移动位置查询和显示
+  - [X] 支持手动添加设备和给设备设置单独的密码
+-  [X] 支持平台对接接入
+-  [X] 支持国标级联
+  - [X] 国标通道向上级联
     - [X] WEB添加上级平台
     - [X] 注册
     - [X] 心跳保活
@@ -101,61 +88,33 @@ https://gitee.com/pan648540858/wvp-GB28181-pro.git
     - [X] 目录订阅与通知
     - [X] 录像查看与播放
     - [X] GPS订阅与通知(直播推流)
-- [X] 支持手动添加设备和给设备设置单独的密码
-- [X] 添加RTSP视频
-- [X] 添加接口鉴权
-- [X] 添加RTMP视频
-- [X] 云端录像(需要部署单独服务配合使用)
+- [X] 支持自动配置ZLM媒体服务, 减少因配置问题所出现的问题;  
 - [X] 多流媒体节点,自动选择负载最低的节点使用。
-- [X] WEB端支持播放H264与H265,音频支持G.711A/G.711U/AAC,覆盖国标常用编码格式。
-- [X] 支持电子地图。
-- [X] 支持接入WGS84和GCJ02两种坐标系。
-
-[//]: # (# docker快速体验)
-
-[//]: # (目前作者的docker-compose因为时间有限维护不及时,这里提供第三方提供的供大家使用,维护不易,大家记得给这位小伙伴点个star。  )
-
-[//]: # (https://github.com/SaltFish001/wvp_pro_compose)
-
-[//]: # ([https://github.com/SaltFish001/wvp_pro_compose](https://github.com/SaltFish001/wvp_pro_compose))
-
-[//]: # (这是作者维护的一个镜像,可能存在不及时的问题。)
-
-[//]: # (```shell)
-
-[//]: # (docker pull 648540858/wvp_pro)
-
-[//]: # ()
-[//]: # (docker run  --env WVP_IP="你的IP" -it -p 18080:18080 -p 30000-30500:30000-30500/udp -p 30000-30500:30000-30500/tcp -p 80:80 -p 5060:5060 -p 5060:5060/udp 648540858/wvp_pro)
-
-[//]: # (```)
-
-[//]: # (docker使用详情查看:[https://hub.docker.com/r/648540858/wvp_pro](https://hub.docker.com/r/648540858/wvp_pro))
-
-# gitee同步仓库
-https://gitee.com/pan648540858/wvp-GB28181-pro.git  
-
-# 遇到问题
+- [X] 支持启用udp多端口模式, 提高udp模式下媒体传输性能;
+- [X] 支持公网部署; 
+- [X] 支持wvp与zlm分开部署,提升平台并发能力
+- [X] 支持拉流RTSP/RTMP,分发为各种流格式,或者推送到其他国标平台
+- [X] 支持推流RTSP/RTMP,分发为各种流格式,或者推送到其他国标平台
+- [X] 支持推流鉴权
+- [X] 支持接口鉴权
+- [X] 云端录像,推流/代理/国标视频均可以录制在云端服务器,支持预览和下载
+ 
+
+# 遇到问题如何解决
 国标最麻烦的地方在于设备的兼容性,所以需要大量的设备来测试,目前作者手里的设备有限,再加上作者水平有限,所以遇到问题在所难免;
 1. 查看wiki,仔细的阅读可以帮你避免几乎所有的问题
 2. 搜索issues,这里有大部分的答案
-3. 加QQ群,这里有大量热心的小伙伴,但是前提新希望你已经仔细阅读了wiki和搜索了issues。
+3. 加QQ群(901799015),这里有大量热心的小伙伴,但是前提新希望你已经仔细阅读了wiki和搜索了issues。
 4. 你可以请作者为你解答,但是我不是免费的。
 5. 你可以把遇到问题的设备寄给我,可以更容易的复现问题。
 
-
-# 合作
-目前很多打着合作的幌子来私聊的,其实大家大可不必,目前作者没有精力,你有问题可以付费找我解答,也可以提PR
-,如果对代码有建议可以提ISSUE;也可以加群一起聊聊。我们欢迎所有有兴趣参与到项目中来的人。
-
-
-
 # 使用帮助
 QQ群: 901799015, ZLM使用文档[https://github.com/ZLMediaKit/ZLMediaKit](https://github.com/ZLMediaKit/ZLMediaKit)  
 QQ私信一般不回, 精力有限.欢迎大家在群里讨论.觉得项目对你有帮助,欢迎star和提交pr。
 
 # 授权协议
 本项目自有代码使用宽松的MIT协议,在保留版权信息的情况下可以自由应用于各自商用、非商业的项目。 但是本项目也零碎的使用了一些其他的开源代码,在商用的情况下请自行替代或剔除; 由于使用本项目而产生的商业纠纷或侵权行为一概与本项目及开发者无关,请自行承担法律风险。 在使用本项目代码时,也应该在授权协议中同时表明本项目依赖的第三方库的协议
+
 # 致谢
 感谢作者[夏楚](https://github.com/xia-chu) 提供这么棒的开源流媒体服务框架,并在开发过程中给予支持与帮助。     
 感谢作者[dexter langhuihui](https://github.com/langhuihui) 开源这么好用的WEB播放器。     

+ 2 - 1
doc/_content/introduction/config.md

@@ -31,6 +31,7 @@ java -jar wvp-pro-*.jar
 ## 2 配置WVP-PRO
 ### 2.1 Mysql数据库配置
 首先你需要创建一个名为wvp(也可使用其他名字)的数据库,并使用sql/mysql.sql导入数据库,初始化数据库结构。
+(这里注意,取决于版本,新版的sql文件夹下有update.sql,补丁包,一定要注意运行导入)
 在application-dev.yml中配置(使用1.2方式的是在jar包的同级目录的application.yml)配置数据库连接,包括数据库连接信息,密码。
 ### 2.2 Redis数据库配置
 配置wvp中的redis连接信息,建议wvp自己单独使用一个db。
@@ -116,4 +117,4 @@ user-settings:
 
 
 如果配置信息无误,你可以启动zlm,再启动wvp来测试了,启动成功的话,你可以在wvp的日志下看到zlm已连接的提示。
-接下来[部署到服务器](./_content/introduction/deployment.md), 如何你只是本地运行直接再本地运行即可。
+接下来[部署到服务器](./_content/introduction/deployment.md), 如何你只是本地运行直接再本地运行即可。

TEMPAT SAMPAH
doc/_media/2.png


TEMPAT SAMPAH
doc/_media/3-1.png


TEMPAT SAMPAH
doc/_media/3-2.png


TEMPAT SAMPAH
doc/_media/3-3.png


TEMPAT SAMPAH
doc/_media/3.png


TEMPAT SAMPAH
doc/_media/index.png


+ 1 - 0
sql/mysql.sql

@@ -39,6 +39,7 @@ CREATE TABLE `device` (
                           `updateTime` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
                           `port` int DEFAULT NULL,
                           `expires` int DEFAULT NULL,
+                          `keepaliveIntervalTime` int DEFAULT NULL,
                           `subscribeCycleForCatalog` int DEFAULT NULL,
                           `hostAddress` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,
                           `charset` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,

+ 10 - 4
src/main/java/com/genersoft/iot/vmp/gb28181/session/CatalogDataCatch.java

@@ -109,12 +109,18 @@ public class CatalogDataCatch {
 
         for (String deviceId : keys) {
             CatalogData catalogData = data.get(deviceId);
-            if ( catalogData.getLastTime().isBefore(instantBefore5S)) { // 超过五秒收不到消息任务超时, 只更新这一部分数据
+            if ( catalogData.getLastTime().isBefore(instantBefore5S)) {
+                // 超过五秒收不到消息任务超时, 只更新这一部分数据, 收到数据与声明的总数一致,则重置通道数据,数据不全则只对收到的数据做更新操作
                 if (catalogData.getStatus().equals(CatalogData.CatalogDataStatus.runIng)) {
-                    storager.resetChannels(catalogData.getDevice().getDeviceId(), catalogData.getChannelList());
+                    if (catalogData.getTotal() == catalogData.getChannelList().size()) {
+                        storager.resetChannels(catalogData.getDevice().getDeviceId(), catalogData.getChannelList());
+                    }else {
+                        storager.updateChannels(catalogData.getDevice().getDeviceId(), catalogData.getChannelList());
+                    }
+                    String errorMsg = "更新成功,共" + catalogData.getTotal() + "条,已更新" + catalogData.getChannelList().size() + "条";
+                    catalogData.setErrorMsg(errorMsg);
                     if (catalogData.getTotal() != catalogData.getChannelList().size()) {
-                        String errorMsg = "更新成功,共" + catalogData.getTotal() + "条,已更新" + catalogData.getChannelList().size() + "条";
-                        catalogData.setErrorMsg(errorMsg);
+
                     }
                 }else if (catalogData.getStatus().equals(CatalogData.CatalogDataStatus.ready)) {
                     String errorMsg = "同步失败,等待回复超时";

+ 151 - 113
src/main/java/com/genersoft/iot/vmp/gb28181/transmit/cmd/impl/SIPCommanderFroPlatform.java

@@ -1,6 +1,7 @@
 package com.genersoft.iot.vmp.gb28181.transmit.cmd.impl;
 
 import com.alibaba.fastjson2.JSON;
+import com.genersoft.iot.vmp.conf.DynamicTask;
 import com.genersoft.iot.vmp.gb28181.SipLayer;
 import com.genersoft.iot.vmp.gb28181.bean.*;
 import com.genersoft.iot.vmp.gb28181.event.SipSubscribe;
@@ -61,6 +62,9 @@ public class SIPCommanderFroPlatform implements ISIPCommanderForPlatform {
     @Autowired
     private SIPSender sipSender;
 
+    @Autowired
+    private DynamicTask dynamicTask;
+
     @Override
     public void register(ParentPlatform parentPlatform, SipSubscribe.Event errorEvent , SipSubscribe.Event okEvent) throws InvalidArgumentException, ParseException, SipException {
         register(parentPlatform, null, null, errorEvent, okEvent, false, true);
@@ -109,13 +113,14 @@ public class SIPCommanderFroPlatform implements ISIPCommanderForPlatform {
     public String keepalive(ParentPlatform parentPlatform,SipSubscribe.Event errorEvent , SipSubscribe.Event okEvent) throws SipException, InvalidArgumentException, ParseException {
             String characterSet = parentPlatform.getCharacterSet();
             StringBuffer keepaliveXml = new StringBuffer(200);
-            keepaliveXml.append("<?xml version=\"1.0\" encoding=\"" + characterSet + "\"?>\r\n");
-            keepaliveXml.append("<Notify>\r\n");
-            keepaliveXml.append("<CmdType>Keepalive</CmdType>\r\n");
-            keepaliveXml.append("<SN>" + (int)((Math.random()*9+1)*100000) + "</SN>\r\n");
-            keepaliveXml.append("<DeviceID>" + parentPlatform.getDeviceGBId() + "</DeviceID>\r\n");
-            keepaliveXml.append("<Status>OK</Status>\r\n");
-            keepaliveXml.append("</Notify>\r\n");
+            keepaliveXml.append("<?xml version=\"1.0\" encoding=\"")
+                    .append(characterSet).append("\"?>\r\n")
+                    .append("<Notify>\r\n")
+                    .append("<CmdType>Keepalive</CmdType>\r\n")
+                    .append("<SN>" + (int)((Math.random()*9+1)*100000) + "</SN>\r\n")
+                    .append("<DeviceID>" + parentPlatform.getDeviceGBId() + "</DeviceID>\r\n")
+                    .append("<Status>OK</Status>\r\n")
+                    .append("</Notify>\r\n");
 
         CallIdHeader callIdHeader = sipSender.getNewCallIdHeader(parentPlatform.getDeviceIp(),parentPlatform.getTransport());
 
@@ -133,7 +138,6 @@ public class SIPCommanderFroPlatform implements ISIPCommanderForPlatform {
      * 向上级回复通道信息
      * @param channel 通道信息
      * @param parentPlatform 平台信息
-     * @return
      */
     @Override
     public void catalogQuery(DeviceChannel channel, ParentPlatform parentPlatform, String sn, String fromTag, int size) throws SipException, InvalidArgumentException, ParseException {
@@ -160,18 +164,18 @@ public class SIPCommanderFroPlatform implements ISIPCommanderForPlatform {
         if ( parentPlatform ==null) {
             return ;
         }
-        sendCatalogResponse(channels, parentPlatform, sn, fromTag, 0);
+        sendCatalogResponse(channels, parentPlatform, sn, fromTag, 0, true);
     }
     private String getCatalogXml(List<DeviceChannel> channels, String sn, ParentPlatform parentPlatform, int size) {
         String characterSet = parentPlatform.getCharacterSet();
         StringBuffer catalogXml = new StringBuffer(600);
-        catalogXml.append("<?xml version=\"1.0\" encoding=\"" + characterSet +"\"?>\r\n");
-        catalogXml.append("<Response>\r\n");
-        catalogXml.append("<CmdType>Catalog</CmdType>\r\n");
-        catalogXml.append("<SN>" +sn + "</SN>\r\n");
-        catalogXml.append("<DeviceID>" + parentPlatform.getDeviceGBId() + "</DeviceID>\r\n");
-        catalogXml.append("<SumNum>" + size + "</SumNum>\r\n");
-        catalogXml.append("<DeviceList Num=\"" + channels.size() +"\">\r\n");
+        catalogXml.append("<?xml version=\"1.0\" encoding=\"" + characterSet +"\"?>\r\n")
+                .append("<Response>\r\n")
+                .append("<CmdType>Catalog</CmdType>\r\n")
+                .append("<SN>" +sn + "</SN>\r\n")
+                .append("<DeviceID>" + parentPlatform.getDeviceGBId() + "</DeviceID>\r\n")
+                .append("<SumNum>" + size + "</SumNum>\r\n")
+                .append("<DeviceList Num=\"" + channels.size() +"\">\r\n");
         if (channels.size() > 0) {
             for (DeviceChannel channel : channels) {
                 if (parentPlatform.getServerGBId().equals(channel.getParentId())) {
@@ -222,7 +226,7 @@ public class SIPCommanderFroPlatform implements ISIPCommanderForPlatform {
         return catalogXml.toString();
     }
 
-    private void sendCatalogResponse(List<DeviceChannel> channels, ParentPlatform parentPlatform, String sn, String fromTag, int index) throws SipException, InvalidArgumentException, ParseException {
+    private void sendCatalogResponse(List<DeviceChannel> channels, ParentPlatform parentPlatform, String sn, String fromTag, int index, boolean sendAfterResponse) throws SipException, InvalidArgumentException, ParseException {
         if (index >= channels.size()) {
             return;
         }
@@ -236,15 +240,49 @@ public class SIPCommanderFroPlatform implements ISIPCommanderForPlatform {
         // callid
         CallIdHeader callIdHeader = sipSender.getNewCallIdHeader(parentPlatform.getDeviceIp(),parentPlatform.getTransport());
 
-        Request request = headerProviderPlatformProvider.createMessageRequest(parentPlatform, catalogXml, fromTag, SipUtils.getNewViaTag(), callIdHeader);
-        sipSender.transmitRequest(parentPlatform.getDeviceIp(), request, null, eventResult -> {
-            int indexNext = index + parentPlatform.getCatalogGroup();
-            try {
-                sendCatalogResponse(channels, parentPlatform, sn, fromTag, indexNext);
-            } catch (SipException | InvalidArgumentException | ParseException e) {
-                logger.error("[命令发送失败] 国标级联 目录查询回复: {}", e.getMessage());
-            }
-        });
+        SIPRequest request = (SIPRequest)headerProviderPlatformProvider.createMessageRequest(parentPlatform, catalogXml, fromTag, SipUtils.getNewViaTag(), callIdHeader);
+
+        String timeoutTaskKey = "catalog_task_" + parentPlatform.getServerGBId() + sn;
+
+        String callId = request.getCallIdHeader().getCallId();
+
+        if (sendAfterResponse) {
+            // 默认按照收到200回复后发送下一条, 如果超时收不到回复,就以30毫秒的间隔直接发送。
+            dynamicTask.startDelay(timeoutTaskKey, ()->{
+                sipSubscribe.removeOkSubscribe(callId);
+                int indexNext = index + parentPlatform.getCatalogGroup();
+                try {
+                    sendCatalogResponse(channels, parentPlatform, sn, fromTag, indexNext, false);
+                } catch (SipException | InvalidArgumentException | ParseException e) {
+                    logger.error("[命令发送失败] 国标级联 目录查询回复: {}", e.getMessage());
+                }
+            }, 3000);
+            sipSender.transmitRequest(parentPlatform.getDeviceIp(), request, eventResult -> {
+                logger.error("[目录推送失败] 国标级联 platform : {}, code: {}, msg: {}, 停止发送", parentPlatform.getServerGBId(), eventResult.statusCode, eventResult.msg);
+                dynamicTask.stop(timeoutTaskKey);
+            }, eventResult -> {
+                dynamicTask.stop(timeoutTaskKey);
+                int indexNext = index + parentPlatform.getCatalogGroup();
+                try {
+                    sendCatalogResponse(channels, parentPlatform, sn, fromTag, indexNext, true);
+                } catch (SipException | InvalidArgumentException | ParseException e) {
+                    logger.error("[命令发送失败] 国标级联 目录查询回复: {}", e.getMessage());
+                }
+            });
+        }else {
+            sipSender.transmitRequest(parentPlatform.getDeviceIp(), request, eventResult -> {
+                logger.error("[目录推送失败] 国标级联 platform : {}, code: {}, msg: {}, 停止发送", parentPlatform.getServerGBId(), eventResult.statusCode, eventResult.msg);
+                dynamicTask.stop(timeoutTaskKey);
+            }, null);
+            dynamicTask.startDelay(timeoutTaskKey, ()->{
+                int indexNext = index + parentPlatform.getCatalogGroup();
+                try {
+                    sendCatalogResponse(channels, parentPlatform, sn, fromTag, indexNext, false);
+                } catch (SipException | InvalidArgumentException | ParseException e) {
+                    logger.error("[命令发送失败] 国标级联 目录查询回复: {}", e.getMessage());
+                }
+            }, 30);
+        }
     }
 
     /**
@@ -294,15 +332,15 @@ public class SIPCommanderFroPlatform implements ISIPCommanderForPlatform {
         String statusStr = (status==1)?"ONLINE":"OFFLINE";
         String characterSet = parentPlatform.getCharacterSet();
         StringBuffer deviceStatusXml = new StringBuffer(600);
-        deviceStatusXml.append("<?xml version=\"1.0\" encoding=\"" + characterSet + "\"?>\r\n");
-        deviceStatusXml.append("<Response>\r\n");
-        deviceStatusXml.append("<CmdType>DeviceStatus</CmdType>\r\n");
-        deviceStatusXml.append("<SN>" +sn + "</SN>\r\n");
-        deviceStatusXml.append("<DeviceID>" + channelId + "</DeviceID>\r\n");
-        deviceStatusXml.append("<Result>OK</Result>\r\n");
-        deviceStatusXml.append("<Online>"+statusStr+"</Online>\r\n");
-        deviceStatusXml.append("<Status>OK</Status>\r\n");
-        deviceStatusXml.append("</Response>\r\n");
+        deviceStatusXml.append("<?xml version=\"1.0\" encoding=\"" + characterSet + "\"?>\r\n")
+                .append("<Response>\r\n")
+                .append("<CmdType>DeviceStatus</CmdType>\r\n")
+                .append("<SN>" +sn + "</SN>\r\n")
+                .append("<DeviceID>" + channelId + "</DeviceID>\r\n")
+                .append("<Result>OK</Result>\r\n")
+                .append("<Online>"+statusStr+"</Online>\r\n")
+                .append("<Status>OK</Status>\r\n")
+                .append("</Response>\r\n");
 
         CallIdHeader callIdHeader = sipSender.getNewCallIdHeader(parentPlatform.getDeviceIp(),parentPlatform.getTransport());
 
@@ -321,18 +359,18 @@ public class SIPCommanderFroPlatform implements ISIPCommanderForPlatform {
 
         String characterSet = parentPlatform.getCharacterSet();
         StringBuffer deviceStatusXml = new StringBuffer(600);
-        deviceStatusXml.append("<?xml version=\"1.0\" encoding=\"" + characterSet + "\"?>\r\n");
-        deviceStatusXml.append("<Notify>\r\n");
-        deviceStatusXml.append("<CmdType>MobilePosition</CmdType>\r\n");
-        deviceStatusXml.append("<SN>" + (int)((Math.random()*9+1)*100000) + "</SN>\r\n");
-        deviceStatusXml.append("<DeviceID>" + gpsMsgInfo.getId() + "</DeviceID>\r\n");
-        deviceStatusXml.append("<Time>" + gpsMsgInfo.getTime() + "</Time>\r\n");
-        deviceStatusXml.append("<Longitude>" + gpsMsgInfo.getLng() + "</Longitude>\r\n");
-        deviceStatusXml.append("<Latitude>" + gpsMsgInfo.getLat() + "</Latitude>\r\n");
-        deviceStatusXml.append("<Speed>" + gpsMsgInfo.getSpeed() + "</Speed>\r\n");
-        deviceStatusXml.append("<Direction>" + gpsMsgInfo.getDirection() + "</Direction>\r\n");
-        deviceStatusXml.append("<Altitude>" + gpsMsgInfo.getAltitude() + "</Altitude>\r\n");
-        deviceStatusXml.append("</Notify>\r\n");
+        deviceStatusXml.append("<?xml version=\"1.0\" encoding=\"" + characterSet + "\"?>\r\n")
+                .append("<Notify>\r\n")
+                .append("<CmdType>MobilePosition</CmdType>\r\n")
+                .append("<SN>" + (int)((Math.random()*9+1)*100000) + "</SN>\r\n")
+                .append("<DeviceID>" + gpsMsgInfo.getId() + "</DeviceID>\r\n")
+                .append("<Time>" + gpsMsgInfo.getTime() + "</Time>\r\n")
+                .append("<Longitude>" + gpsMsgInfo.getLng() + "</Longitude>\r\n")
+                .append("<Latitude>" + gpsMsgInfo.getLat() + "</Latitude>\r\n")
+                .append("<Speed>" + gpsMsgInfo.getSpeed() + "</Speed>\r\n")
+                .append("<Direction>" + gpsMsgInfo.getDirection() + "</Direction>\r\n")
+                .append("<Altitude>" + gpsMsgInfo.getAltitude() + "</Altitude>\r\n")
+                .append("</Notify>\r\n");
 
        sendNotify(parentPlatform, deviceStatusXml.toString(), subscribeInfo, eventResult -> {
             logger.error("发送NOTIFY通知消息失败。错误:{} {}", eventResult.statusCode, eventResult.msg);
@@ -349,21 +387,21 @@ public class SIPCommanderFroPlatform implements ISIPCommanderForPlatform {
                 deviceAlarm.getLongitude(), deviceAlarm.getLatitude(), JSON.toJSONString(deviceAlarm));
         String characterSet = parentPlatform.getCharacterSet();
         StringBuffer deviceStatusXml = new StringBuffer(600);
-        deviceStatusXml.append("<?xml version=\"1.0\" encoding=\"" + characterSet + "\"?>\r\n");
-        deviceStatusXml.append("<Notify>\r\n");
-        deviceStatusXml.append("<CmdType>Alarm</CmdType>\r\n");
-        deviceStatusXml.append("<SN>" + (int)((Math.random()*9+1)*100000) + "</SN>\r\n");
-        deviceStatusXml.append("<DeviceID>" + deviceAlarm.getChannelId() + "</DeviceID>\r\n");
-        deviceStatusXml.append("<AlarmPriority>" + deviceAlarm.getAlarmPriority() + "</AlarmPriority>\r\n");
-        deviceStatusXml.append("<AlarmMethod>" + deviceAlarm.getAlarmMethod() + "</AlarmMethod>\r\n");
-        deviceStatusXml.append("<AlarmTime>" + deviceAlarm.getAlarmTime() + "</AlarmTime>\r\n");
-        deviceStatusXml.append("<AlarmDescription>" + deviceAlarm.getAlarmDescription() + "</AlarmDescription>\r\n");
-        deviceStatusXml.append("<Longitude>" + deviceAlarm.getLongitude() + "</Longitude>\r\n");
-        deviceStatusXml.append("<Latitude>" + deviceAlarm.getLatitude() + "</Latitude>\r\n");
-        deviceStatusXml.append("<info>\r\n");
-        deviceStatusXml.append("<AlarmType>" + deviceAlarm.getAlarmType() + "</AlarmType>\r\n");
-        deviceStatusXml.append("</info>\r\n");
-        deviceStatusXml.append("</Notify>\r\n");
+        deviceStatusXml.append("<?xml version=\"1.0\" encoding=\"" + characterSet + "\"?>\r\n")
+                .append("<Notify>\r\n")
+                .append("<CmdType>Alarm</CmdType>\r\n")
+                .append("<SN>" + (int)((Math.random()*9+1)*100000) + "</SN>\r\n")
+                .append("<DeviceID>" + deviceAlarm.getChannelId() + "</DeviceID>\r\n")
+                .append("<AlarmPriority>" + deviceAlarm.getAlarmPriority() + "</AlarmPriority>\r\n")
+                .append("<AlarmMethod>" + deviceAlarm.getAlarmMethod() + "</AlarmMethod>\r\n")
+                .append("<AlarmTime>" + deviceAlarm.getAlarmTime() + "</AlarmTime>\r\n")
+                .append("<AlarmDescription>" + deviceAlarm.getAlarmDescription() + "</AlarmDescription>\r\n")
+                .append("<Longitude>" + deviceAlarm.getLongitude() + "</Longitude>\r\n")
+                .append("<Latitude>" + deviceAlarm.getLatitude() + "</Latitude>\r\n")
+                .append("<info>\r\n")
+                .append("<AlarmType>" + deviceAlarm.getAlarmType() + "</AlarmType>\r\n")
+                .append("</info>\r\n")
+                .append("</Notify>\r\n");
 
         CallIdHeader callIdHeader = sipSender.getNewCallIdHeader(parentPlatform.getDeviceIp(),parentPlatform.getTransport());
 
@@ -422,13 +460,13 @@ public class SIPCommanderFroPlatform implements ISIPCommanderForPlatform {
         StringBuffer catalogXml = new StringBuffer(600);
 
         String characterSet = parentPlatform.getCharacterSet();
-        catalogXml.append("<?xml version=\"1.0\" encoding=\"" + characterSet + "\"?>\r\n");
-        catalogXml.append("<Notify>\r\n");
-        catalogXml.append("<CmdType>Catalog</CmdType>\r\n");
-        catalogXml.append("<SN>" + (int) ((Math.random() * 9 + 1) * 100000) + "</SN>\r\n");
-        catalogXml.append("<DeviceID>" + parentPlatform.getDeviceGBId() + "</DeviceID>\r\n");
-        catalogXml.append("<SumNum>1</SumNum>\r\n");
-        catalogXml.append("<DeviceList Num=\"" + channels.size() + "\">\r\n");
+        catalogXml.append("<?xml version=\"1.0\" encoding=\"" + characterSet + "\"?>\r\n")
+                .append("<Notify>\r\n")
+                .append("<CmdType>Catalog</CmdType>\r\n")
+                .append("<SN>" + (int) ((Math.random() * 9 + 1) * 100000) + "</SN>\r\n")
+                .append("<DeviceID>" + parentPlatform.getDeviceGBId() + "</DeviceID>\r\n")
+                .append("<SumNum>1</SumNum>\r\n")
+                .append("<DeviceList Num=\"" + channels.size() + "\">\r\n");
         if (channels.size() > 0) {
             for (DeviceChannel channel : channels) {
                 if (parentPlatform.getServerGBId().equals(channel.getParentId())) {
@@ -449,16 +487,16 @@ public class SIPCommanderFroPlatform implements ISIPCommanderForPlatform {
                 catalogXml.append("<Parental>" + channel.getParental() + "</Parental>\r\n");
                 if (channel.getParental() == 0) {
                     // 通道项
-                    catalogXml.append("<Manufacturer>" + channel.getManufacture() + "</Manufacturer>\r\n");
-                    catalogXml.append("<Secrecy>" + channel.getSecrecy() + "</Secrecy>\r\n");
-                    catalogXml.append("<RegisterWay>" + channel.getRegisterWay() + "</RegisterWay>\r\n");
-                    catalogXml.append("<Status>" + (channel.getStatus() == 0 ? "OFF" : "ON") + "</Status>\r\n");
+                    catalogXml.append("<Manufacturer>" + channel.getManufacture() + "</Manufacturer>\r\n")
+                            .append("<Secrecy>" + channel.getSecrecy() + "</Secrecy>\r\n")
+                            .append("<RegisterWay>" + channel.getRegisterWay() + "</RegisterWay>\r\n")
+                            .append("<Status>" + (channel.getStatus() == 0 ? "OFF" : "ON") + "</Status>\r\n");
 
                     if (channel.getChannelType() != 2) {  // 业务分组/虚拟组织/行政区划 不设置以下属性
-                        catalogXml.append("<Model>" + channel.getModel() + "</Model>\r\n");
-                        catalogXml.append("<Owner> " + channel.getOwner()+ "</Owner>\r\n");
-                        catalogXml.append("<CivilCode>" + channel.getCivilCode() + "</CivilCode>\r\n");
-                        catalogXml.append("<Address>" + channel.getAddress() + "</Address>\r\n");
+                        catalogXml.append("<Model>" + channel.getModel() + "</Model>\r\n")
+                                .append("<Owner> " + channel.getOwner()+ "</Owner>\r\n")
+                                .append("<CivilCode>" + channel.getCivilCode() + "</CivilCode>\r\n")
+                                .append("<Address>" + channel.getAddress() + "</Address>\r\n");
                     }
                     if (!"presence".equals(subscribeInfo.getEventType())) {
                         catalogXml.append("<Event>" + type + "</Event>\r\n");
@@ -468,8 +506,8 @@ public class SIPCommanderFroPlatform implements ISIPCommanderForPlatform {
                 catalogXml.append("</Item>\r\n");
             }
         }
-        catalogXml.append("</DeviceList>\r\n");
-        catalogXml.append("</Notify>\r\n");
+        catalogXml.append("</DeviceList>\r\n")
+                .append("</Notify>\r\n");
         return catalogXml.toString();
     }
 
@@ -515,26 +553,26 @@ public class SIPCommanderFroPlatform implements ISIPCommanderForPlatform {
 
         String characterSet = parentPlatform.getCharacterSet();
         StringBuffer catalogXml = new StringBuffer(600);
-        catalogXml.append("<?xml version=\"1.0\" encoding=\"" + characterSet + "\"?>\r\n");
-        catalogXml.append("<Notify>\r\n");
-        catalogXml.append("<CmdType>Catalog</CmdType>\r\n");
-        catalogXml.append("<SN>" + (int) ((Math.random() * 9 + 1) * 100000) + "</SN>\r\n");
-        catalogXml.append("<DeviceID>" + parentPlatform.getDeviceGBId() + "</DeviceID>\r\n");
-        catalogXml.append("<SumNum>1</SumNum>\r\n");
-        catalogXml.append("<DeviceList Num=\" " + channels.size() + " \">\r\n");
+        catalogXml.append("<?xml version=\"1.0\" encoding=\"" + characterSet + "\"?>\r\n")
+                .append("<Notify>\r\n")
+                .append("<CmdType>Catalog</CmdType>\r\n")
+                .append("<SN>" + (int) ((Math.random() * 9 + 1) * 100000) + "</SN>\r\n")
+                .append("<DeviceID>" + parentPlatform.getDeviceGBId() + "</DeviceID>\r\n")
+                .append("<SumNum>1</SumNum>\r\n")
+                .append("<DeviceList Num=\" " + channels.size() + " \">\r\n");
         if (channels.size() > 0) {
             for (DeviceChannel channel : channels) {
                 if (parentPlatform.getServerGBId().equals(channel.getParentId())) {
                     channel.setParentId(parentPlatform.getDeviceGBId());
                 }
-                catalogXml.append("<Item>\r\n");
-                catalogXml.append("<DeviceID>" + channel.getChannelId() + "</DeviceID>\r\n");
-                catalogXml.append("<Event>" + type + "</Event>\r\n");
-                catalogXml.append("</Item>\r\n");
+                catalogXml.append("<Item>\r\n")
+                        .append("<DeviceID>" + channel.getChannelId() + "</DeviceID>\r\n")
+                        .append("<Event>" + type + "</Event>\r\n")
+                        .append("</Item>\r\n");
             }
         }
-        catalogXml.append("</DeviceList>\r\n");
-        catalogXml.append("</Notify>\r\n");
+        catalogXml.append("</DeviceList>\r\n")
+                .append("</Notify>\r\n");
         return catalogXml.toString();
     }
     @Override
@@ -544,12 +582,12 @@ public class SIPCommanderFroPlatform implements ISIPCommanderForPlatform {
         }
         String characterSet = parentPlatform.getCharacterSet();
         StringBuffer recordXml = new StringBuffer(600);
-        recordXml.append("<?xml version=\"1.0\" encoding=\"" + characterSet + "\"?>\r\n");
-        recordXml.append("<Response>\r\n");
-        recordXml.append("<CmdType>RecordInfo</CmdType>\r\n");
-        recordXml.append("<SN>" +recordInfo.getSn() + "</SN>\r\n");
-        recordXml.append("<DeviceID>" + recordInfo.getDeviceId() + "</DeviceID>\r\n");
-        recordXml.append("<SumNum>" + recordInfo.getSumNum() + "</SumNum>\r\n");
+        recordXml.append("<?xml version=\"1.0\" encoding=\"" + characterSet + "\"?>\r\n")
+                .append("<Response>\r\n")
+                .append("<CmdType>RecordInfo</CmdType>\r\n")
+                .append("<SN>" +recordInfo.getSn() + "</SN>\r\n")
+                .append("<DeviceID>" + recordInfo.getDeviceId() + "</DeviceID>\r\n")
+                .append("<SumNum>" + recordInfo.getSumNum() + "</SumNum>\r\n");
         if (recordInfo.getRecordList() == null ) {
             recordXml.append("<RecordList Num=\"0\">\r\n");
         }else {
@@ -558,12 +596,12 @@ public class SIPCommanderFroPlatform implements ISIPCommanderForPlatform {
                 for (RecordItem recordItem : recordInfo.getRecordList()) {
                     recordXml.append("<Item>\r\n");
                     if (deviceChannel != null) {
-                        recordXml.append("<DeviceID>" + recordItem.getDeviceId() + "</DeviceID>\r\n");
-                        recordXml.append("<Name>" + recordItem.getName() + "</Name>\r\n");
-                        recordXml.append("<StartTime>" + DateUtil.yyyy_MM_dd_HH_mm_ssToISO8601(recordItem.getStartTime()) + "</StartTime>\r\n");
-                        recordXml.append("<EndTime>" + DateUtil.yyyy_MM_dd_HH_mm_ssToISO8601(recordItem.getEndTime()) + "</EndTime>\r\n");
-                        recordXml.append("<Secrecy>" + recordItem.getSecrecy() + "</Secrecy>\r\n");
-                        recordXml.append("<Type>" + recordItem.getType() + "</Type>\r\n");
+                        recordXml.append("<DeviceID>" + recordItem.getDeviceId() + "</DeviceID>\r\n")
+                                .append("<Name>" + recordItem.getName() + "</Name>\r\n")
+                                .append("<StartTime>" + DateUtil.yyyy_MM_dd_HH_mm_ssToISO8601(recordItem.getStartTime()) + "</StartTime>\r\n")
+                                .append("<EndTime>" + DateUtil.yyyy_MM_dd_HH_mm_ssToISO8601(recordItem.getEndTime()) + "</EndTime>\r\n")
+                                .append("<Secrecy>" + recordItem.getSecrecy() + "</Secrecy>\r\n")
+                                .append("<Type>" + recordItem.getType() + "</Type>\r\n");
                         if (!ObjectUtils.isEmpty(recordItem.getFileSize())) {
                             recordXml.append("<FileSize>" + recordItem.getFileSize() + "</FileSize>\r\n");
                         }
@@ -576,8 +614,8 @@ public class SIPCommanderFroPlatform implements ISIPCommanderForPlatform {
             }
         }
 
-        recordXml.append("</RecordList>\r\n");
-        recordXml.append("</Response>\r\n");
+        recordXml.append("</RecordList>\r\n")
+                .append("</Response>\r\n");
 
         // callid
         CallIdHeader callIdHeader = sipSender.getNewCallIdHeader(parentPlatform.getDeviceIp(),parentPlatform.getTransport());
@@ -596,13 +634,13 @@ public class SIPCommanderFroPlatform implements ISIPCommanderForPlatform {
 
         String characterSet = parentPlatform.getCharacterSet();
         StringBuffer mediaStatusXml = new StringBuffer(200);
-        mediaStatusXml.append("<?xml version=\"1.0\" encoding=\"" + characterSet + "\"?>\r\n");
-        mediaStatusXml.append("<Notify>\r\n");
-        mediaStatusXml.append("<CmdType>MediaStatus</CmdType>\r\n");
-        mediaStatusXml.append("<SN>" + (int)((Math.random()*9+1)*100000) + "</SN>\r\n");
-        mediaStatusXml.append("<DeviceID>" + sendRtpItem.getChannelId() + "</DeviceID>\r\n");
-        mediaStatusXml.append("<NotifyType>121</NotifyType>\r\n");
-        mediaStatusXml.append("</Notify>\r\n");
+        mediaStatusXml.append("<?xml version=\"1.0\" encoding=\"" + characterSet + "\"?>\r\n")
+                .append("<Notify>\r\n")
+                .append("<CmdType>MediaStatus</CmdType>\r\n")
+                .append("<SN>" + (int)((Math.random()*9+1)*100000) + "</SN>\r\n")
+                .append("<DeviceID>" + sendRtpItem.getChannelId() + "</DeviceID>\r\n")
+                .append("<NotifyType>121</NotifyType>\r\n")
+                .append("</Notify>\r\n");
 
         SIPRequest messageRequest = (SIPRequest)headerProviderPlatformProvider.createMessageRequest(parentPlatform, mediaStatusXml.toString(),
                 sendRtpItem);

+ 3 - 62
src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/SIPRequestProcessorParent.java

@@ -3,7 +3,6 @@ package com.genersoft.iot.vmp.gb28181.transmit.event.request;
 import com.genersoft.iot.vmp.gb28181.bean.ParentPlatform;
 import com.genersoft.iot.vmp.gb28181.transmit.SIPSender;
 import com.genersoft.iot.vmp.gb28181.utils.SipUtils;
-import gov.nist.javax.sip.SipProviderImpl;
 import gov.nist.javax.sip.message.SIPRequest;
 import gov.nist.javax.sip.message.SIPResponse;
 import org.apache.commons.lang3.ArrayUtils;
@@ -14,19 +13,17 @@ import org.dom4j.io.SAXReader;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.beans.factory.annotation.Qualifier;
 
 import javax.sip.*;
 import javax.sip.address.Address;
-import javax.sip.address.AddressFactory;
 import javax.sip.address.SipURI;
-import javax.sip.header.*;
+import javax.sip.header.ContentTypeHeader;
+import javax.sip.header.ExpiresHeader;
+import javax.sip.header.HeaderFactory;
 import javax.sip.message.MessageFactory;
 import javax.sip.message.Request;
 import javax.sip.message.Response;
 import java.io.ByteArrayInputStream;
-import java.nio.ByteBuffer;
-import java.nio.charset.StandardCharsets;
 import java.text.ParseException;
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -44,15 +41,6 @@ public abstract class SIPRequestProcessorParent {
 	@Autowired
 	private SIPSender sipSender;
 
-	public AddressFactory getAddressFactory() {
-		try {
-			return SipFactory.getInstance().createAddressFactory();
-		} catch (PeerUnavailableException e) {
-			e.printStackTrace();
-		}
-		return null;
-	}
-
 	public HeaderFactory getHeaderFactory() {
 		try {
 			return SipFactory.getInstance().createHeaderFactory();
@@ -93,53 +81,6 @@ public abstract class SIPRequestProcessorParent {
 		return responseAck(sipRequest, statusCode, msg, null);
 	}
 
-//	public SIPResponse responseAck(ServerTransaction serverTransaction, int statusCode, String msg, ResponseAckExtraParam responseAckExtraParam) throws SipException, InvalidArgumentException, ParseException {
-//		if (serverTransaction == null) {
-//			logger.warn("[回复消息] ServerTransaction 为null");
-//			return null;
-//		}
-//		ToHeader toHeader = (ToHeader) serverTransaction.getRequest().getHeader(ToHeader.NAME);
-//		if (toHeader.getTag() == null) {
-//			toHeader.setTag(SipUtils.getNewTag());
-//		}
-//		SIPResponse response = (SIPResponse)getMessageFactory().createResponse(statusCode, serverTransaction.getRequest());
-//		if (msg != null) {
-//			response.setReasonPhrase(msg);
-//		}
-//		if (responseAckExtraParam != null) {
-//			if (responseAckExtraParam.sipURI != null && serverTransaction.getRequest().getMethod().equals(Request.INVITE)) {
-//				logger.debug("responseSdpAck SipURI: {}:{}", responseAckExtraParam.sipURI.getHost(), responseAckExtraParam.sipURI.getPort());
-//				Address concatAddress = SipFactory.getInstance().createAddressFactory().createAddress(
-//						SipFactory.getInstance().createAddressFactory().createSipURI(responseAckExtraParam.sipURI.getUser(),  responseAckExtraParam.sipURI.getHost()+":"+responseAckExtraParam.sipURI.getPort()
-//						));
-//				response.addHeader(SipFactory.getInstance().createHeaderFactory().createContactHeader(concatAddress));
-//			}
-//			if (responseAckExtraParam.contentTypeHeader != null) {
-//				response.setContent(responseAckExtraParam.content, responseAckExtraParam.contentTypeHeader);
-//			}
-//
-//			if (serverTransaction.getRequest().getMethod().equals(Request.SUBSCRIBE)) {
-//				if (responseAckExtraParam.expires == -1) {
-//					logger.error("[参数不全] 2xx的SUBSCRIBE回复,必须设置Expires header");
-//				}else {
-//					ExpiresHeader expiresHeader = SipFactory.getInstance().createHeaderFactory().createExpiresHeader(responseAckExtraParam.expires);
-//					response.addHeader(expiresHeader);
-//				}
-//			}
-//		}else {
-//			if (serverTransaction.getRequest().getMethod().equals(Request.SUBSCRIBE)) {
-//				logger.error("[参数不全] 2xx的SUBSCRIBE回复,必须设置Expires header");
-//			}
-//		}
-//		serverTransaction.sendResponse(response);
-//		if (statusCode >= 200 && !"NOTIFY".equalsIgnoreCase(serverTransaction.getRequest().getMethod())) {
-//			if (serverTransaction.getDialog() != null) {
-//				serverTransaction.getDialog().delete();
-//			}
-//		}
-//		return response;
-//	}
-
 	public SIPResponse responseAck(SIPRequest sipRequest, int statusCode, String msg, ResponseAckExtraParam responseAckExtraParam) throws SipException, InvalidArgumentException, ParseException {
 		if (sipRequest.getToHeader().getTag() == null) {
 			sipRequest.getToHeader().setTag(SipUtils.getNewTag());

+ 1 - 1
src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/InviteRequestProcessor.java

@@ -228,7 +228,7 @@ public class InviteRequestProcessor extends SIPRequestProcessorParent implements
                     }
                     return;
                 } else {
-                    logger.info("通道不存在,返回404");
+                    logger.info("通道不存在,返回404: {}", channelId);
                     try {
                         // 通道不存在,发404,资源不存在
                         responseAck(request, Response.NOT_FOUND);

+ 3 - 3
src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/query/cmd/CatalogQueryMessageHandler.java

@@ -1,9 +1,10 @@
 package com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.query.cmd;
 
 import com.genersoft.iot.vmp.conf.SipConfig;
-import com.genersoft.iot.vmp.gb28181.bean.*;
+import com.genersoft.iot.vmp.gb28181.bean.Device;
+import com.genersoft.iot.vmp.gb28181.bean.DeviceChannel;
+import com.genersoft.iot.vmp.gb28181.bean.ParentPlatform;
 import com.genersoft.iot.vmp.gb28181.event.EventPublisher;
-import com.genersoft.iot.vmp.gb28181.transmit.callback.DeferredResultHolder;
 import com.genersoft.iot.vmp.gb28181.transmit.cmd.impl.SIPCommanderFroPlatform;
 import com.genersoft.iot.vmp.gb28181.transmit.event.request.SIPRequestProcessorParent;
 import com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.IMessageHandler;
@@ -63,7 +64,6 @@ public class CatalogQueryMessageHandler extends SIPRequestProcessorParent implem
     @Override
     public void handForPlatform(RequestEvent evt, ParentPlatform parentPlatform, Element rootElement) {
 
-        String key = DeferredResultHolder.CALLBACK_CMD_CATALOG + parentPlatform.getServerGBId();
         FromHeader fromHeader = (FromHeader) evt.getRequest().getHeader(FromHeader.NAME);
         try {
             // 回复200 OK

+ 631 - 619
src/main/java/com/genersoft/iot/vmp/media/zlm/ZLMHttpHookListener.java

@@ -8,6 +8,8 @@ import com.genersoft.iot.vmp.conf.exception.SsrcTransactionNotFoundException;
 import com.genersoft.iot.vmp.gb28181.bean.*;
 import com.genersoft.iot.vmp.gb28181.event.EventPublisher;
 import com.genersoft.iot.vmp.gb28181.session.VideoStreamSessionManager;
+import com.genersoft.iot.vmp.gb28181.transmit.callback.DeferredResultHolder;
+import com.genersoft.iot.vmp.gb28181.transmit.callback.RequestMessage;
 import com.genersoft.iot.vmp.gb28181.transmit.cmd.impl.SIPCommander;
 import com.genersoft.iot.vmp.gb28181.transmit.cmd.impl.SIPCommanderFroPlatform;
 import com.genersoft.iot.vmp.media.zlm.dto.HookType;
@@ -18,7 +20,10 @@ import com.genersoft.iot.vmp.media.zlm.dto.hook.*;
 import com.genersoft.iot.vmp.service.*;
 import com.genersoft.iot.vmp.storager.IRedisCatchStorage;
 import com.genersoft.iot.vmp.storager.IVideoManagerStorage;
+import com.genersoft.iot.vmp.vmanager.bean.DeferredResultEx;
+import com.genersoft.iot.vmp.vmanager.bean.ErrorCode;
 import com.genersoft.iot.vmp.vmanager.bean.StreamContent;
+import com.genersoft.iot.vmp.vmanager.bean.WVPResult;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.beans.factory.annotation.Autowired;
@@ -26,6 +31,7 @@ import org.springframework.beans.factory.annotation.Qualifier;
 import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
 import org.springframework.util.ObjectUtils;
 import org.springframework.web.bind.annotation.*;
+import org.springframework.web.context.request.async.DeferredResult;
 
 import javax.servlet.http.HttpServletRequest;
 import javax.sip.InvalidArgumentException;
@@ -34,638 +40,644 @@ import java.text.ParseException;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.UUID;
 
-/**    
+/**
  * @description:针对 ZLMediaServer的hook事件监听
  * @author: swwheihei
- * @date:   2020年5月8日 上午10:46:48     
+ * @date: 2020年5月8日 上午10:46:48
  */
 @RestController
 @RequestMapping("/index/hook")
 public class ZLMHttpHookListener {
 
-	private final static Logger logger = LoggerFactory.getLogger(ZLMHttpHookListener.class);
+    private final static Logger logger = LoggerFactory.getLogger(ZLMHttpHookListener.class);
 
-	@Autowired
-	private SIPCommander cmder;
-
-	@Autowired
-	private SIPCommanderFroPlatform commanderFroPlatform;
-
-	@Autowired
-	private IPlayService playService;
-
-	@Autowired
-	private IVideoManagerStorage storager;
-
-	@Autowired
-	private IRedisCatchStorage redisCatchStorage;
-
-	@Autowired
-	private IDeviceService deviceService;
-
-	@Autowired
-	private IMediaServerService mediaServerService;
-
-	@Autowired
-	private IStreamProxyService streamProxyService;
-
-	@Autowired
-	private IStreamPushService streamPushService;
-
-	@Autowired
-	private IMediaService mediaService;
-
-	@Autowired
-	private EventPublisher eventPublisher;
-
-	 @Autowired
-	 private ZLMMediaListManager zlmMediaListManager;
-
-	@Autowired
-	private ZlmHttpHookSubscribe subscribe;
-
-	@Autowired
-	private UserSetting userSetting;
-
-	@Autowired
-	private IUserService userService;
-
-	@Autowired
-	private VideoStreamSessionManager sessionManager;
-
-	@Autowired
-	private AssistRESTfulUtils assistRESTfulUtils;
-
-	@Qualifier("taskExecutor")
-	@Autowired
-	private ThreadPoolTaskExecutor taskExecutor;
-
-	/**
-	 * 服务器定时上报时间,上报间隔可配置,默认10s上报一次
-	 *
-	 */
-	@ResponseBody
-	@PostMapping(value = "/on_server_keepalive", produces = "application/json;charset=UTF-8")
-	public JSONObject onServerKeepalive(@RequestBody OnServerKeepaliveHookParam param){
-
-		logger.info("[ZLM HOOK] 收到zlm心跳:" + param.getMediaServerId());
-
-		taskExecutor.execute(()->{
-			List<ZlmHttpHookSubscribe.Event> subscribes = this.subscribe.getSubscribes(HookType.on_server_keepalive);
-			JSONObject json = (JSONObject) JSON.toJSON(param);
-			if (subscribes != null  && subscribes.size() > 0) {
-				for (ZlmHttpHookSubscribe.Event subscribe : subscribes) {
-					subscribe.response(null, json);
-				}
-			}
-		});
-		mediaServerService.updateMediaServerKeepalive(param.getMediaServerId(), param.getData());
-
-		JSONObject ret = new JSONObject();
-		ret.put("code", 0);
-		ret.put("msg", "success");
-
-		return ret;
-	}
-	
-	/**
-	 * 播放器鉴权事件,rtsp/rtmp/http-flv/ws-flv/hls的播放都将触发此鉴权事件。
-	 *  
-	 */
-	@ResponseBody
-	@PostMapping(value = "/on_play", produces = "application/json;charset=UTF-8")
-	public JSONObject onPlay(@RequestBody OnPlayHookParam param){
-		if (logger.isDebugEnabled()) {
-			logger.debug("[ZLM HOOK] 播放鉴权:{}->{}" + param.getMediaServerId(), param);
-		}
-		String mediaServerId = param.getMediaServerId();
-
-		taskExecutor.execute(()->{
-			JSONObject json = (JSONObject) JSON.toJSON(param);
-			ZlmHttpHookSubscribe.Event subscribe = this.subscribe.sendNotify(HookType.on_play, json);
-			if (subscribe != null ) {
-				MediaServerItem mediaInfo = mediaServerService.getOne(mediaServerId);
-				if (mediaInfo != null) {
-					subscribe.response(mediaInfo, json);
-				}
-			}
-		});
-		JSONObject ret = new JSONObject();
-		if (!"rtp".equals(param.getApp())) {
-			Map<String, String> paramMap = urlParamToMap(param.getParams());
-			StreamAuthorityInfo streamAuthorityInfo = redisCatchStorage.getStreamAuthorityInfo(param.getApp(), param.getStream());
-			if (streamAuthorityInfo != null && streamAuthorityInfo.getCallId() != null && !streamAuthorityInfo.getCallId().equals(paramMap.get("callId"))) {
-				ret.put("code", 401);
-				ret.put("msg", "Unauthorized");
-				return ret;
-			}
-		}
-
-		ret.put("code", 0);
-		ret.put("msg", "success");
-		return ret;
-	}
-	
-	/**
-	 * rtsp/rtmp/rtp推流鉴权事件。
-	 *  
-	 */
-	@ResponseBody
-	@PostMapping(value = "/on_publish", produces = "application/json;charset=UTF-8")
-	public JSONObject onPublish(@RequestBody OnPublishHookParam param) {
-
-		JSONObject json = (JSONObject) JSON.toJSON(param);
-
-		logger.info("[ZLM HOOK]推流鉴权:{}->{}",  param.getMediaServerId(), param);
-		JSONObject ret = new JSONObject();
-		String mediaServerId = json.getString("mediaServerId");
-		MediaServerItem mediaInfo = mediaServerService.getOne(mediaServerId);
-
-		if (!"rtp".equals(param.getApp())) {
-			if (userSetting.getPushAuthority()) {
-				// 推流鉴权
-				if (param.getParams() == null) {
-					logger.info("推流鉴权失败: 缺少不要参数:sign=md5(user表的pushKey)");
-					ret.put("code", 401);
-					ret.put("msg", "Unauthorized");
-					return ret;
-				}
-				Map<String, String> paramMap = urlParamToMap(param.getParams());
-				String sign = paramMap.get("sign");
-				if (sign == null) {
-					logger.info("推流鉴权失败: 缺少不要参数:sign=md5(user表的pushKey)");
-					ret.put("code", 401);
-					ret.put("msg", "Unauthorized");
-					return ret;
-				}
-				// 推流自定义播放鉴权码
-				String callId = paramMap.get("callId");
-				// 鉴权配置
-				boolean hasAuthority = userService.checkPushAuthority(callId, sign);
-				if (!hasAuthority) {
-					logger.info("推流鉴权失败: sign 无权限: callId={}. sign={}", callId, sign);
-					ret.put("code", 401);
-					ret.put("msg", "Unauthorized");
-					return ret;
-				}
-				StreamAuthorityInfo streamAuthorityInfo = StreamAuthorityInfo.getInstanceByHook(param);
-				streamAuthorityInfo.setCallId(callId);
-				streamAuthorityInfo.setSign(sign);
-				// 鉴权通过
-				redisCatchStorage.updateStreamAuthorityInfo(param.getApp(), param.getStream(), streamAuthorityInfo);
-				// 通知assist新的callId
-				if (mediaInfo != null && mediaInfo.getRecordAssistPort() > 0) {
-					taskExecutor.execute(()->{
-						assistRESTfulUtils.addStreamCallInfo(mediaInfo, param.getApp(), param.getStream(), callId, null);
-					});
-				}
-			}
-		}else {
-			zlmMediaListManager.sendStreamEvent(param.getApp(),param.getStream(), param.getMediaServerId());
-		}
-
-		ret.put("code", 0);
-		ret.put("msg", "success");
-
-		if (!"rtp".equals(param.getApp())) {
-			ret.put("enable_audio", true);
-		}
-
-		taskExecutor.execute(()->{
-			ZlmHttpHookSubscribe.Event subscribe = this.subscribe.sendNotify(HookType.on_publish, json);
-			if (subscribe != null) {
-				if (mediaInfo != null) {
-					subscribe.response(mediaInfo, json);
-				}else {
-					ret.put("code", 1);
-					ret.put("msg", "zlm not register");
-				}
-			}
-		});
-
-		if ("rtp".equals(param.getApp())) {
-			ret.put("enable_mp4", userSetting.getRecordSip());
-		}else {
-			ret.put("enable_mp4", userSetting.isRecordPushLive());
-		}
-		List<SsrcTransaction> ssrcTransactionForAll = sessionManager.getSsrcTransactionForAll(null, null, null, param.getStream());
-		if (ssrcTransactionForAll != null && ssrcTransactionForAll.size() == 1) {
-			String deviceId = ssrcTransactionForAll.get(0).getDeviceId();
-			String channelId = ssrcTransactionForAll.get(0).getChannelId();
-			DeviceChannel deviceChannel = storager.queryChannel(deviceId, channelId);
-			if (deviceChannel != null) {
-				ret.put("enable_audio", deviceChannel.isHasAudio());
-			}
-			// 如果是录像下载就设置视频间隔十秒
-			if (ssrcTransactionForAll.get(0).getType() == VideoStreamSessionManager.SessionType.download) {
-				ret.put("mp4_max_second", 10);
-				ret.put("enable_mp4", true);
-				ret.put("enable_audio", true);
-			}
-		}
-		return ret;
-	}
-	
-	/**
-	 * rtsp/rtmp流注册或注销时触发此事件;此事件对回复不敏感。
-	 *  
-	 */
-	@ResponseBody
-	@PostMapping(value = "/on_stream_changed", produces = "application/json;charset=UTF-8")
-	public JSONObject onStreamChanged(@RequestBody OnStreamChangedHookParam param){
-
-		if (param.isRegist()) {
-			logger.info("[ZLM HOOK] 流注册, {}->{}->{}/{}", param.getMediaServerId(), param.getSchema(), param.getApp(), param.getStream());
-		}else {
-			logger.info("[ZLM HOOK] 流注销, {}->{}->{}/{}", param.getMediaServerId(), param.getSchema(), param.getApp(), param.getStream());
-		}
-
-
-		JSONObject json = (JSONObject) JSON.toJSON(param);
-		taskExecutor.execute(()->{
-			ZlmHttpHookSubscribe.Event subscribe = this.subscribe.sendNotify(HookType.on_stream_changed, json);
-			if (subscribe != null ) {
-				MediaServerItem mediaInfo = mediaServerService.getOne(param.getMediaServerId());
-				if (mediaInfo != null) {
-					subscribe.response(mediaInfo, json);
-				}
-			}
-			// 流消失移除redis play
-			List<OnStreamChangedHookParam.MediaTrack> tracks = param.getTracks();
-			if (param.isRegist()) {
-				if (param.getOriginType() == OriginType.RTMP_PUSH.ordinal()
-						|| param.getOriginType() == OriginType.RTSP_PUSH.ordinal()
-						|| param.getOriginType() == OriginType.RTC_PUSH.ordinal()) {
-
-					StreamAuthorityInfo streamAuthorityInfo = redisCatchStorage.getStreamAuthorityInfo(param.getApp(), param.getStream());
-					if (streamAuthorityInfo == null) {
-						streamAuthorityInfo = StreamAuthorityInfo.getInstanceByHook(param);
-					}else {
-						streamAuthorityInfo.setOriginType(param.getOriginType());
-						streamAuthorityInfo.setOriginTypeStr(param.getOriginTypeStr());
-					}
-					redisCatchStorage.updateStreamAuthorityInfo(param.getApp(), param.getStream(), streamAuthorityInfo);
-				}
-			}else {
-				redisCatchStorage.removeStreamAuthorityInfo(param.getApp(), param.getStream());
-			}
-
-			if ("rtsp".equals(param.getSchema())){
-				if (param.isRegist()) {
-					mediaServerService.addCount(param.getMediaServerId());
-				}else {
-					mediaServerService.removeCount(param.getMediaServerId());
-				}
-				if (param.getOriginType() == OriginType.PULL.ordinal()
-						|| param.getOriginType() == OriginType.FFMPEG_PULL.ordinal()) {
-					// 设置拉流代理上线/离线
-					streamProxyService.updateStatus(param.isRegist(), param.getApp(), param.getStream());
-				}
-				if ("rtp".equals(param.getApp()) && !param.isRegist() ) {
-					StreamInfo streamInfo = redisCatchStorage.queryPlayByStreamId(param.getStream());
-					if (streamInfo!=null){
-						redisCatchStorage.stopPlay(streamInfo);
-						storager.stopPlay(streamInfo.getDeviceID(), streamInfo.getChannelId());
-					}else{
-						streamInfo = redisCatchStorage.queryPlayback(null, null, param.getStream(), null);
-						if (streamInfo != null) {
-							redisCatchStorage.stopPlayback(streamInfo.getDeviceID(), streamInfo.getChannelId(),
-									streamInfo.getStream(), null);
-						}
-					}
-				}else {
-					if (!"rtp".equals(param.getApp())){
-						String type = OriginType.values()[param.getOriginType()].getType();
-						MediaServerItem mediaServerItem = mediaServerService.getOne(param.getMediaServerId());
-
-						if (mediaServerItem != null){
-							if (param.isRegist()) {
-								StreamAuthorityInfo streamAuthorityInfo = redisCatchStorage.getStreamAuthorityInfo(param.getApp(), param.getStream());
-								String callId = null;
-								if (streamAuthorityInfo != null) {
-									callId = streamAuthorityInfo.getCallId();
-								}
-								StreamInfo streamInfoByAppAndStream = mediaService.getStreamInfoByAppAndStream(mediaServerItem,
-										param.getApp(), param.getStream(), tracks, callId);
-								param.setStreamInfo(new StreamContent(streamInfoByAppAndStream));
-								redisCatchStorage.addStream(mediaServerItem, type, param.getApp(), param.getStream(), param);
-								if (param.getOriginType() == OriginType.RTSP_PUSH.ordinal()
-										|| param.getOriginType() == OriginType.RTMP_PUSH.ordinal()
-										|| param.getOriginType() == OriginType.RTC_PUSH.ordinal() ) {
-									param.setSeverId(userSetting.getServerId());
-									zlmMediaListManager.addPush(param);
-								}
-							}else {
-								// 兼容流注销时类型从redis记录获取
-								OnStreamChangedHookParam onStreamChangedHookParam = redisCatchStorage.getStreamInfo(param.getApp(), param.getStream(), param.getMediaServerId());
-								if (onStreamChangedHookParam != null) {
-									type = OriginType.values()[onStreamChangedHookParam.getOriginType()].getType();
-									redisCatchStorage.removeStream(mediaServerItem.getId(), type, param.getApp(), param.getStream());
-								}
-								GbStream gbStream = storager.getGbStream(param.getApp(), param.getStream());
-								if (gbStream != null) {
+    @Autowired
+    private SIPCommander cmder;
+
+    @Autowired
+    private SIPCommanderFroPlatform commanderFroPlatform;
+
+    @Autowired
+    private IPlayService playService;
+
+    @Autowired
+    private IVideoManagerStorage storager;
+
+    @Autowired
+    private IRedisCatchStorage redisCatchStorage;
+
+    @Autowired
+    private IDeviceService deviceService;
+
+    @Autowired
+    private IMediaServerService mediaServerService;
+
+    @Autowired
+    private IStreamProxyService streamProxyService;
+
+    @Autowired
+    private DeferredResultHolder resultHolder;
+
+    @Autowired
+    private IMediaService mediaService;
+
+    @Autowired
+    private EventPublisher eventPublisher;
+
+    @Autowired
+    private ZLMMediaListManager zlmMediaListManager;
+
+    @Autowired
+    private ZlmHttpHookSubscribe subscribe;
+
+    @Autowired
+    private UserSetting userSetting;
+
+    @Autowired
+    private IUserService userService;
+
+    @Autowired
+    private VideoStreamSessionManager sessionManager;
+
+    @Autowired
+    private AssistRESTfulUtils assistRESTfulUtils;
+
+    @Qualifier("taskExecutor")
+    @Autowired
+    private ThreadPoolTaskExecutor taskExecutor;
+
+    /**
+     * 服务器定时上报时间,上报间隔可配置,默认10s上报一次
+     */
+    @ResponseBody
+    @PostMapping(value = "/on_server_keepalive", produces = "application/json;charset=UTF-8")
+    public HookResult onServerKeepalive(@RequestBody OnServerKeepaliveHookParam param) {
+
+        logger.info("[ZLM HOOK] 收到zlm心跳:" + param.getMediaServerId());
+
+        taskExecutor.execute(() -> {
+            List<ZlmHttpHookSubscribe.Event> subscribes = this.subscribe.getSubscribes(HookType.on_server_keepalive);
+            JSONObject json = (JSONObject) JSON.toJSON(param);
+            if (subscribes != null && subscribes.size() > 0) {
+                for (ZlmHttpHookSubscribe.Event subscribe : subscribes) {
+                    subscribe.response(null, json);
+                }
+            }
+        });
+        mediaServerService.updateMediaServerKeepalive(param.getMediaServerId(), param.getData());
+
+        return HookResult.SUCCESS();
+    }
+
+    /**
+     * 播放器鉴权事件,rtsp/rtmp/http-flv/ws-flv/hls的播放都将触发此鉴权事件。
+     */
+    @ResponseBody
+    @PostMapping(value = "/on_play", produces = "application/json;charset=UTF-8")
+    public HookResult onPlay(@RequestBody OnPlayHookParam param) {
+        if (logger.isDebugEnabled()) {
+            logger.debug("[ZLM HOOK] 播放鉴权:{}->{}" + param.getMediaServerId(), param);
+        }
+        String mediaServerId = param.getMediaServerId();
+
+        taskExecutor.execute(() -> {
+            JSONObject json = (JSONObject) JSON.toJSON(param);
+            ZlmHttpHookSubscribe.Event subscribe = this.subscribe.sendNotify(HookType.on_play, json);
+            if (subscribe != null) {
+                MediaServerItem mediaInfo = mediaServerService.getOne(mediaServerId);
+                if (mediaInfo != null) {
+                    subscribe.response(mediaInfo, json);
+                }
+            }
+        });
+        if (!"rtp".equals(param.getApp())) {
+            Map<String, String> paramMap = urlParamToMap(param.getParams());
+            StreamAuthorityInfo streamAuthorityInfo = redisCatchStorage.getStreamAuthorityInfo(param.getApp(), param.getStream());
+            if (streamAuthorityInfo != null && streamAuthorityInfo.getCallId() != null && !streamAuthorityInfo.getCallId().equals(paramMap.get("callId"))) {
+                return new HookResult(401, "Unauthorized");
+            }
+        }
+
+        return HookResult.SUCCESS();
+    }
+
+    /**
+     * rtsp/rtmp/rtp推流鉴权事件。
+     */
+    @ResponseBody
+    @PostMapping(value = "/on_publish", produces = "application/json;charset=UTF-8")
+    public HookResultForOnPublish onPublish(@RequestBody OnPublishHookParam param) {
+
+        JSONObject json = (JSONObject) JSON.toJSON(param);
+
+        logger.info("[ZLM HOOK]推流鉴权:{}->{}", param.getMediaServerId(), param);
+
+        String mediaServerId = json.getString("mediaServerId");
+        MediaServerItem mediaInfo = mediaServerService.getOne(mediaServerId);
+
+        if (!"rtp".equals(param.getApp())) {
+            if (userSetting.getPushAuthority()) {
+                // 推流鉴权
+                if (param.getParams() == null) {
+                    logger.info("推流鉴权失败: 缺少不要参数:sign=md5(user表的pushKey)");
+                    return new HookResultForOnPublish(401, "Unauthorized");
+                }
+                Map<String, String> paramMap = urlParamToMap(param.getParams());
+                String sign = paramMap.get("sign");
+                if (sign == null) {
+                    logger.info("推流鉴权失败: 缺少不要参数:sign=md5(user表的pushKey)");
+                    return new HookResultForOnPublish(401, "Unauthorized");
+                }
+                // 推流自定义播放鉴权码
+                String callId = paramMap.get("callId");
+                // 鉴权配置
+                boolean hasAuthority = userService.checkPushAuthority(callId, sign);
+                if (!hasAuthority) {
+                    logger.info("推流鉴权失败: sign 无权限: callId={}. sign={}", callId, sign);
+                    return new HookResultForOnPublish(401, "Unauthorized");
+                }
+                StreamAuthorityInfo streamAuthorityInfo = StreamAuthorityInfo.getInstanceByHook(param);
+                streamAuthorityInfo.setCallId(callId);
+                streamAuthorityInfo.setSign(sign);
+                // 鉴权通过
+                redisCatchStorage.updateStreamAuthorityInfo(param.getApp(), param.getStream(), streamAuthorityInfo);
+                // 通知assist新的callId
+                if (mediaInfo != null && mediaInfo.getRecordAssistPort() > 0) {
+                    taskExecutor.execute(() -> {
+                        assistRESTfulUtils.addStreamCallInfo(mediaInfo, param.getApp(), param.getStream(), callId, null);
+                    });
+                }
+            }
+        } else {
+            zlmMediaListManager.sendStreamEvent(param.getApp(), param.getStream(), param.getMediaServerId());
+        }
+
+
+        HookResultForOnPublish result = HookResultForOnPublish.SUCCESS();
+        if (!"rtp".equals(param.getApp())) {
+            result.setEnable_audio(true);
+        }
+
+        taskExecutor.execute(() -> {
+            ZlmHttpHookSubscribe.Event subscribe = this.subscribe.sendNotify(HookType.on_publish, json);
+            if (subscribe != null) {
+                if (mediaInfo != null) {
+                    subscribe.response(mediaInfo, json);
+                } else {
+                    new HookResultForOnPublish(1, "zlm not register");
+                }
+            }
+        });
+
+        if ("rtp".equals(param.getApp())) {
+            result.setEnable_mp4(userSetting.getRecordSip());
+        } else {
+            result.setEnable_mp4(userSetting.isRecordPushLive());
+        }
+        List<SsrcTransaction> ssrcTransactionForAll = sessionManager.getSsrcTransactionForAll(null, null, null, param.getStream());
+        if (ssrcTransactionForAll != null && ssrcTransactionForAll.size() == 1) {
+            String deviceId = ssrcTransactionForAll.get(0).getDeviceId();
+            String channelId = ssrcTransactionForAll.get(0).getChannelId();
+            DeviceChannel deviceChannel = storager.queryChannel(deviceId, channelId);
+            if (deviceChannel != null) {
+                result.setEnable_audio(deviceChannel.isHasAudio());
+            }
+            // 如果是录像下载就设置视频间隔十秒
+            if (ssrcTransactionForAll.get(0).getType() == VideoStreamSessionManager.SessionType.download) {
+                result.setMp4_max_second(10);
+                result.setEnable_audio(true);
+                result.setEnable_mp4(true);
+            }
+        }
+        return result;
+    }
+
+    /**
+     * rtsp/rtmp流注册或注销时触发此事件;此事件对回复不敏感。
+     */
+    @ResponseBody
+    @PostMapping(value = "/on_stream_changed", produces = "application/json;charset=UTF-8")
+    public HookResult onStreamChanged(@RequestBody OnStreamChangedHookParam param) {
+
+        if (param.isRegist()) {
+            logger.info("[ZLM HOOK] 流注册, {}->{}->{}/{}", param.getMediaServerId(), param.getSchema(), param.getApp(), param.getStream());
+        } else {
+            logger.info("[ZLM HOOK] 流注销, {}->{}->{}/{}", param.getMediaServerId(), param.getSchema(), param.getApp(), param.getStream());
+        }
+
+
+        JSONObject json = (JSONObject) JSON.toJSON(param);
+        taskExecutor.execute(() -> {
+            ZlmHttpHookSubscribe.Event subscribe = this.subscribe.sendNotify(HookType.on_stream_changed, json);
+            if (subscribe != null) {
+                MediaServerItem mediaInfo = mediaServerService.getOne(param.getMediaServerId());
+                if (mediaInfo != null) {
+                    subscribe.response(mediaInfo, json);
+                }
+            }
+            // 流消失移除redis play
+            List<OnStreamChangedHookParam.MediaTrack> tracks = param.getTracks();
+            if (param.isRegist()) {
+                if (param.getOriginType() == OriginType.RTMP_PUSH.ordinal()
+                        || param.getOriginType() == OriginType.RTSP_PUSH.ordinal()
+                        || param.getOriginType() == OriginType.RTC_PUSH.ordinal()) {
+
+                    StreamAuthorityInfo streamAuthorityInfo = redisCatchStorage.getStreamAuthorityInfo(param.getApp(), param.getStream());
+                    if (streamAuthorityInfo == null) {
+                        streamAuthorityInfo = StreamAuthorityInfo.getInstanceByHook(param);
+                    } else {
+                        streamAuthorityInfo.setOriginType(param.getOriginType());
+                        streamAuthorityInfo.setOriginTypeStr(param.getOriginTypeStr());
+                    }
+                    redisCatchStorage.updateStreamAuthorityInfo(param.getApp(), param.getStream(), streamAuthorityInfo);
+                }
+            } else {
+                redisCatchStorage.removeStreamAuthorityInfo(param.getApp(), param.getStream());
+            }
+
+            if ("rtsp".equals(param.getSchema())) {
+                if (param.isRegist()) {
+                    mediaServerService.addCount(param.getMediaServerId());
+                } else {
+                    mediaServerService.removeCount(param.getMediaServerId());
+                }
+                if (param.getOriginType() == OriginType.PULL.ordinal()
+                        || param.getOriginType() == OriginType.FFMPEG_PULL.ordinal()) {
+                    // 设置拉流代理上线/离线
+                    streamProxyService.updateStatus(param.isRegist(), param.getApp(), param.getStream());
+                }
+                if ("rtp".equals(param.getApp()) && !param.isRegist()) {
+                    StreamInfo streamInfo = redisCatchStorage.queryPlayByStreamId(param.getStream());
+                    if (streamInfo != null) {
+                        redisCatchStorage.stopPlay(streamInfo);
+                        storager.stopPlay(streamInfo.getDeviceID(), streamInfo.getChannelId());
+                    } else {
+                        streamInfo = redisCatchStorage.queryPlayback(null, null, param.getStream(), null);
+                        if (streamInfo != null) {
+                            redisCatchStorage.stopPlayback(streamInfo.getDeviceID(), streamInfo.getChannelId(),
+                                    streamInfo.getStream(), null);
+                        }
+                    }
+                } else {
+                    if (!"rtp".equals(param.getApp())) {
+                        String type = OriginType.values()[param.getOriginType()].getType();
+                        MediaServerItem mediaServerItem = mediaServerService.getOne(param.getMediaServerId());
+
+                        if (mediaServerItem != null) {
+                            if (param.isRegist()) {
+                                StreamAuthorityInfo streamAuthorityInfo = redisCatchStorage.getStreamAuthorityInfo(param.getApp(), param.getStream());
+                                String callId = null;
+                                if (streamAuthorityInfo != null) {
+                                    callId = streamAuthorityInfo.getCallId();
+                                }
+                                StreamInfo streamInfoByAppAndStream = mediaService.getStreamInfoByAppAndStream(mediaServerItem,
+                                        param.getApp(), param.getStream(), tracks, callId);
+                                param.setStreamInfo(new StreamContent(streamInfoByAppAndStream));
+                                redisCatchStorage.addStream(mediaServerItem, type, param.getApp(), param.getStream(), param);
+                                if (param.getOriginType() == OriginType.RTSP_PUSH.ordinal()
+                                        || param.getOriginType() == OriginType.RTMP_PUSH.ordinal()
+                                        || param.getOriginType() == OriginType.RTC_PUSH.ordinal()) {
+                                    param.setSeverId(userSetting.getServerId());
+                                    zlmMediaListManager.addPush(param);
+                                }
+                            } else {
+                                // 兼容流注销时类型从redis记录获取
+                                OnStreamChangedHookParam onStreamChangedHookParam = redisCatchStorage.getStreamInfo(param.getApp(), param.getStream(), param.getMediaServerId());
+                                if (onStreamChangedHookParam != null) {
+                                    type = OriginType.values()[onStreamChangedHookParam.getOriginType()].getType();
+                                    redisCatchStorage.removeStream(mediaServerItem.getId(), type, param.getApp(), param.getStream());
+                                }
+                                GbStream gbStream = storager.getGbStream(param.getApp(), param.getStream());
+                                if (gbStream != null) {
 //									eventPublisher.catalogEventPublishForStream(null, gbStream, CatalogEvent.OFF);
-								}
-								zlmMediaListManager.removeMedia(param.getApp(), param.getStream());
-							}
-							if (type != null) {
-								// 发送流变化redis消息
-								JSONObject jsonObject = new JSONObject();
-								jsonObject.put("serverId", userSetting.getServerId());
-								jsonObject.put("app", param.getApp());
-								jsonObject.put("stream", param.getStream());
-								jsonObject.put("register", param.isRegist());
-								jsonObject.put("mediaServerId", param.getMediaServerId());
-								redisCatchStorage.sendStreamChangeMsg(type, jsonObject);
-							}
-						}
-					}
-				}
-				if (!param.isRegist()) {
-					List<SendRtpItem> sendRtpItems = redisCatchStorage.querySendRTPServerByStream(param.getStream());
-					if (sendRtpItems.size() > 0) {
-						for (SendRtpItem sendRtpItem : sendRtpItems) {
-							if (sendRtpItem.getApp().equals(param.getApp())) {
-								String platformId = sendRtpItem.getPlatformId();
-								ParentPlatform platform = storager.queryParentPlatByServerGBId(platformId);
-								Device device = deviceService.getDevice(platformId);
-
-								try {
-									if (platform != null) {
-										commanderFroPlatform.streamByeCmd(platform, sendRtpItem);
-									}else {
-										cmder.streamByeCmd(device, sendRtpItem.getChannelId(), param.getStream(), sendRtpItem.getCallId());
-									}
-								} catch (SipException | InvalidArgumentException | ParseException | SsrcTransactionNotFoundException e) {
-									logger.error("[命令发送失败] 国标级联 发送BYE: {}", e.getMessage());
-								}
-							}
-						}
-					}
-				}
-			}
-		});
-
-		JSONObject ret = new JSONObject();
-		ret.put("code", 0);
-		ret.put("msg", "success");
-		return ret;
-	}
-	
-	/**
-	 * 流无人观看时事件,用户可以通过此事件选择是否关闭无人看的流。
-	 *  
-	 */
-	@ResponseBody
-	@PostMapping(value = "/on_stream_none_reader", produces = "application/json;charset=UTF-8")
-	public JSONObject onStreamNoneReader(@RequestBody OnStreamNoneReaderHookParam param){
-
-		logger.info("[ZLM HOOK]流无人观看:{]->{}->{}/{}" + param.getMediaServerId(), param.getSchema(), param.getApp(), param.getStream());
-		JSONObject ret = new JSONObject();
-		ret.put("code", 0);
-		// 国标类型的流
-		if ("rtp".equals(param.getApp())){
-			ret.put("close", userSetting.getStreamOnDemand());
-			// 国标流, 点播/录像回放/录像下载
-			StreamInfo streamInfoForPlayCatch = redisCatchStorage.queryPlayByStreamId(param.getStream());
-			// 点播
-			if (streamInfoForPlayCatch != null) {
-				// 收到无人观看说明流也没有在往上级推送
-				if (redisCatchStorage.isChannelSendingRTP(streamInfoForPlayCatch.getChannelId())) {
-					List<SendRtpItem> sendRtpItems = redisCatchStorage.querySendRTPServerByChnnelId(streamInfoForPlayCatch.getChannelId());
-					if (sendRtpItems.size() > 0) {
-						for (SendRtpItem sendRtpItem : sendRtpItems) {
-							ParentPlatform parentPlatform = storager.queryParentPlatByServerGBId(sendRtpItem.getPlatformId());
-							try {
-								commanderFroPlatform.streamByeCmd(parentPlatform, sendRtpItem.getCallId());
-							} catch (SipException | InvalidArgumentException | ParseException e) {
-								logger.error("[命令发送失败] 国标级联 发送BYE: {}", e.getMessage());
-							}
-							redisCatchStorage.deleteSendRTPServer(parentPlatform.getServerGBId(), sendRtpItem.getChannelId(),
-									sendRtpItem.getCallId(), sendRtpItem.getStreamId());
-						}
-					}
-				}
-				Device device = deviceService.getDevice(streamInfoForPlayCatch.getDeviceID());
-				if (device != null) {
-					try {
-						cmder.streamByeCmd(device, streamInfoForPlayCatch.getChannelId(),
-								streamInfoForPlayCatch.getStream(), null);
-					} catch (InvalidArgumentException | ParseException | SipException | SsrcTransactionNotFoundException e) {
-						logger.error("[无人观看]点播, 发送BYE失败 {}", e.getMessage());
-					}
-				}
-
-				redisCatchStorage.stopPlay(streamInfoForPlayCatch);
-				storager.stopPlay(streamInfoForPlayCatch.getDeviceID(), streamInfoForPlayCatch.getChannelId());
-				return ret;
-			}
-			// 录像回放
-			StreamInfo streamInfoForPlayBackCatch = redisCatchStorage.queryPlayback(null, null, param.getStream(), null);
-			if (streamInfoForPlayBackCatch != null ) {
-				if (streamInfoForPlayBackCatch.isPause()) {
-					ret.put("close", false);
-				}else {
-					Device device = deviceService.getDevice(streamInfoForPlayBackCatch.getDeviceID());
-					if (device != null) {
-						try {
-							cmder.streamByeCmd(device,streamInfoForPlayBackCatch.getChannelId(),
-									streamInfoForPlayBackCatch.getStream(), null);
-						} catch (InvalidArgumentException | ParseException | SipException |
-								 SsrcTransactionNotFoundException e) {
-							logger.error("[无人观看]回放, 发送BYE失败 {}", e.getMessage());
-						}
-					}
-					redisCatchStorage.stopPlayback(streamInfoForPlayBackCatch.getDeviceID(),
-							streamInfoForPlayBackCatch.getChannelId(), streamInfoForPlayBackCatch.getStream(), null);
-				}
-				return ret;
-			}
-			// 录像下载
-			StreamInfo streamInfoForDownload = redisCatchStorage.queryDownload(null, null, param.getStream(), null);
-			// 进行录像下载时无人观看不断流
-			if (streamInfoForDownload != null) {
-				ret.put("close", false);
-				return ret;
-			}
-		}else {
-			// 非国标流 推流/拉流代理
-			// 拉流代理
-			StreamProxyItem streamProxyItem = streamProxyService.getStreamProxyByAppAndStream(param.getApp(), param.getStream());
-			if (streamProxyItem != null ) {
-				if (streamProxyItem.isEnable_remove_none_reader()) {
-					// 无人观看自动移除
-					ret.put("close", true);
-					streamProxyService.del(param.getApp(), param.getStream());
-					String url = streamProxyItem.getUrl() != null?streamProxyItem.getUrl():streamProxyItem.getSrc_url();
-					logger.info("[{}/{}]<-[{}] 拉流代理无人观看已经移除",  param.getApp(), param.getStream(), url);
-				}else if (streamProxyItem.isEnable_disable_none_reader()) {
-					// 无人观看停用
-					ret.put("close", true);
-					// 修改数据
-					streamProxyService.stop(param.getApp(), param.getStream());
-				}else {
-					// 无人观看不做处理
-					ret.put("close", false);
-				}
-				return ret;
-			}
-			// 推流具有主动性,暂时不做处理
+                                }
+                                zlmMediaListManager.removeMedia(param.getApp(), param.getStream());
+                            }
+                            if (type != null) {
+                                // 发送流变化redis消息
+                                JSONObject jsonObject = new JSONObject();
+                                jsonObject.put("serverId", userSetting.getServerId());
+                                jsonObject.put("app", param.getApp());
+                                jsonObject.put("stream", param.getStream());
+                                jsonObject.put("register", param.isRegist());
+                                jsonObject.put("mediaServerId", param.getMediaServerId());
+                                redisCatchStorage.sendStreamChangeMsg(type, jsonObject);
+                            }
+                        }
+                    }
+                }
+                if (!param.isRegist()) {
+                    List<SendRtpItem> sendRtpItems = redisCatchStorage.querySendRTPServerByStream(param.getStream());
+                    if (sendRtpItems.size() > 0) {
+                        for (SendRtpItem sendRtpItem : sendRtpItems) {
+                            if (sendRtpItem.getApp().equals(param.getApp())) {
+                                String platformId = sendRtpItem.getPlatformId();
+                                ParentPlatform platform = storager.queryParentPlatByServerGBId(platformId);
+                                Device device = deviceService.getDevice(platformId);
+
+                                try {
+                                    if (platform != null) {
+                                        commanderFroPlatform.streamByeCmd(platform, sendRtpItem);
+                                    } else {
+                                        cmder.streamByeCmd(device, sendRtpItem.getChannelId(), param.getStream(), sendRtpItem.getCallId());
+                                    }
+                                } catch (SipException | InvalidArgumentException | ParseException |
+                                         SsrcTransactionNotFoundException e) {
+                                    logger.error("[命令发送失败] 国标级联 发送BYE: {}", e.getMessage());
+                                }
+                            }
+                        }
+                    }
+                }
+            }
+        });
+
+        return HookResult.SUCCESS();
+    }
+
+    /**
+     * 流无人观看时事件,用户可以通过此事件选择是否关闭无人看的流。
+     */
+    @ResponseBody
+    @PostMapping(value = "/on_stream_none_reader", produces = "application/json;charset=UTF-8")
+    public JSONObject onStreamNoneReader(@RequestBody OnStreamNoneReaderHookParam param) {
+
+        logger.info("[ZLM HOOK]流无人观看:{]->{}->{}/{}" + param.getMediaServerId(), param.getSchema(), param.getApp(), param.getStream());
+        JSONObject ret = new JSONObject();
+        ret.put("code", 0);
+        // 国标类型的流
+        if ("rtp".equals(param.getApp())) {
+            ret.put("close", userSetting.getStreamOnDemand());
+            // 国标流, 点播/录像回放/录像下载
+            StreamInfo streamInfoForPlayCatch = redisCatchStorage.queryPlayByStreamId(param.getStream());
+            // 点播
+            if (streamInfoForPlayCatch != null) {
+                // 收到无人观看说明流也没有在往上级推送
+                if (redisCatchStorage.isChannelSendingRTP(streamInfoForPlayCatch.getChannelId())) {
+                    List<SendRtpItem> sendRtpItems = redisCatchStorage.querySendRTPServerByChnnelId(streamInfoForPlayCatch.getChannelId());
+                    if (sendRtpItems.size() > 0) {
+                        for (SendRtpItem sendRtpItem : sendRtpItems) {
+                            ParentPlatform parentPlatform = storager.queryParentPlatByServerGBId(sendRtpItem.getPlatformId());
+                            try {
+                                commanderFroPlatform.streamByeCmd(parentPlatform, sendRtpItem.getCallId());
+                            } catch (SipException | InvalidArgumentException | ParseException e) {
+                                logger.error("[命令发送失败] 国标级联 发送BYE: {}", e.getMessage());
+                            }
+                            redisCatchStorage.deleteSendRTPServer(parentPlatform.getServerGBId(), sendRtpItem.getChannelId(),
+                                    sendRtpItem.getCallId(), sendRtpItem.getStreamId());
+                        }
+                    }
+                }
+                Device device = deviceService.getDevice(streamInfoForPlayCatch.getDeviceID());
+                if (device != null) {
+                    try {
+                        cmder.streamByeCmd(device, streamInfoForPlayCatch.getChannelId(),
+                                streamInfoForPlayCatch.getStream(), null);
+                    } catch (InvalidArgumentException | ParseException | SipException |
+                             SsrcTransactionNotFoundException e) {
+                        logger.error("[无人观看]点播, 发送BYE失败 {}", e.getMessage());
+                    }
+                }
+
+                redisCatchStorage.stopPlay(streamInfoForPlayCatch);
+                storager.stopPlay(streamInfoForPlayCatch.getDeviceID(), streamInfoForPlayCatch.getChannelId());
+                return ret;
+            }
+            // 录像回放
+            StreamInfo streamInfoForPlayBackCatch = redisCatchStorage.queryPlayback(null, null, param.getStream(), null);
+            if (streamInfoForPlayBackCatch != null) {
+                if (streamInfoForPlayBackCatch.isPause()) {
+                    ret.put("close", false);
+                } else {
+                    Device device = deviceService.getDevice(streamInfoForPlayBackCatch.getDeviceID());
+                    if (device != null) {
+                        try {
+                            cmder.streamByeCmd(device, streamInfoForPlayBackCatch.getChannelId(),
+                                    streamInfoForPlayBackCatch.getStream(), null);
+                        } catch (InvalidArgumentException | ParseException | SipException |
+                                 SsrcTransactionNotFoundException e) {
+                            logger.error("[无人观看]回放, 发送BYE失败 {}", e.getMessage());
+                        }
+                    }
+                    redisCatchStorage.stopPlayback(streamInfoForPlayBackCatch.getDeviceID(),
+                            streamInfoForPlayBackCatch.getChannelId(), streamInfoForPlayBackCatch.getStream(), null);
+                }
+                return ret;
+            }
+            // 录像下载
+            StreamInfo streamInfoForDownload = redisCatchStorage.queryDownload(null, null, param.getStream(), null);
+            // 进行录像下载时无人观看不断流
+            if (streamInfoForDownload != null) {
+                ret.put("close", false);
+                return ret;
+            }
+        } else {
+            // 非国标流 推流/拉流代理
+            // 拉流代理
+            StreamProxyItem streamProxyItem = streamProxyService.getStreamProxyByAppAndStream(param.getApp(), param.getStream());
+            if (streamProxyItem != null) {
+                if (streamProxyItem.isEnable_remove_none_reader()) {
+                    // 无人观看自动移除
+                    ret.put("close", true);
+                    streamProxyService.del(param.getApp(), param.getStream());
+                    String url = streamProxyItem.getUrl() != null ? streamProxyItem.getUrl() : streamProxyItem.getSrc_url();
+                    logger.info("[{}/{}]<-[{}] 拉流代理无人观看已经移除", param.getApp(), param.getStream(), url);
+                } else if (streamProxyItem.isEnable_disable_none_reader()) {
+                    // 无人观看停用
+                    ret.put("close", true);
+                    // 修改数据
+                    streamProxyService.stop(param.getApp(), param.getStream());
+                } else {
+                    // 无人观看不做处理
+                    ret.put("close", false);
+                }
+                return ret;
+            }
+            // 推流具有主动性,暂时不做处理
 //			StreamPushItem streamPushItem = streamPushService.getPush(app, streamId);
 //			if (streamPushItem != null) {
 //				// TODO 发送停止
 //
 //			}
-		}
-		return ret;
-	}
-	
-	/**
-	 * 流未找到事件,用户可以在此事件触发时,立即去拉流,这样可以实现按需拉流;此事件对回复不敏感。
-	 *  
-	 */
-	@ResponseBody
-	@PostMapping(value = "/on_stream_not_found", produces = "application/json;charset=UTF-8")
-	public JSONObject onStreamNotFound(@RequestBody OnStreamNotFoundHookParam param){
-		logger.info("[ZLM HOOK] 流未找到:{}->{}->{}/{}", param.getMediaServerId(), param.getSchema(), param.getApp(), param.getStream());
-		taskExecutor.execute(()->{
-			MediaServerItem mediaInfo = mediaServerService.getOne(param.getMediaServerId());
-			if (userSetting.isAutoApplyPlay() && mediaInfo != null) {
-				if ("rtp".equals(param.getApp())) {
-					if (mediaInfo.isRtpEnable()) {
-						String[] s = param.getStream().split("_");
-						if (s.length == 2) {
-							String deviceId = s[0];
-							String channelId = s[1];
-							Device device = redisCatchStorage.getDevice(deviceId);
-							if (device != null) {
-								playService.play(mediaInfo,deviceId, channelId, null, null, null);
-							}
-						}
-					}
-				}else {
-					// 拉流代理
-					StreamProxyItem streamProxyByAppAndStream = streamProxyService.getStreamProxyByAppAndStream(param.getApp(), param.getStream());
-					if (streamProxyByAppAndStream != null && streamProxyByAppAndStream.isEnable_disable_none_reader()) {
-						streamProxyService.start(param.getApp(), param.getStream());
-					}
-				}
-			}
-		});
-
-
-		JSONObject ret = new JSONObject();
-		ret.put("code", 0);
-		ret.put("msg", "success");
-		return ret;
-	}
-	
-	/**
-	 * 服务器启动事件,可以用于监听服务器崩溃重启;此事件对回复不敏感。
-	 *  
-	 */
-	@ResponseBody
-	@PostMapping(value = "/on_server_started", produces = "application/json;charset=UTF-8")
-	public JSONObject onServerStarted(HttpServletRequest request, @RequestBody JSONObject jsonObject){
-
-		jsonObject.put("ip", request.getRemoteAddr());
-		ZLMServerConfig zlmServerConfig = JSON.to(ZLMServerConfig.class, jsonObject);
-		zlmServerConfig.setIp(request.getRemoteAddr());
-		logger.info("[ZLM HOOK] zlm 启动 " + zlmServerConfig.getGeneralMediaServerId());
-		taskExecutor.execute(()->{
-			List<ZlmHttpHookSubscribe.Event> subscribes = this.subscribe.getSubscribes(HookType.on_server_started);
-			if (subscribes != null  && subscribes.size() > 0) {
-				for (ZlmHttpHookSubscribe.Event subscribe : subscribes) {
-					subscribe.response(null, jsonObject);
-				}
-			}
-			mediaServerService.zlmServerOnline(zlmServerConfig);
-		});
-
-		JSONObject ret = new JSONObject();
-		ret.put("code", 0);
-		ret.put("msg", "success");
-		return ret;
-	}
-
-	/**
-	 * 发送rtp(startSendRtp)被动关闭时回调
-	 */
-	@ResponseBody
-	@PostMapping(value = "/on_send_rtp_stopped", produces = "application/json;charset=UTF-8")
-	public JSONObject onSendRtpStopped(HttpServletRequest request, @RequestBody OnSendRtpStoppedHookParam param){
-
-		logger.info("[ZLM HOOK] rtp发送关闭:{}->{}/{}", param.getMediaServerId(), param.getApp(), param.getStream());
-
-		JSONObject ret = new JSONObject();
-		ret.put("code", 0);
-		ret.put("msg", "success");
-
-		// 查找对应的上级推流,发送停止
-		if (!"rtp".equals(param.getApp())) {
-			return ret;
-		}
-		taskExecutor.execute(()->{
-			List<SendRtpItem> sendRtpItems = redisCatchStorage.querySendRTPServerByStream(param.getStream());
-			if (sendRtpItems.size() > 0) {
-				for (SendRtpItem sendRtpItem : sendRtpItems) {
-					ParentPlatform parentPlatform = storager.queryParentPlatByServerGBId(sendRtpItem.getPlatformId());
-					try {
-						commanderFroPlatform.streamByeCmd(parentPlatform, sendRtpItem.getCallId());
-					} catch (SipException | InvalidArgumentException | ParseException e) {
-						logger.error("[命令发送失败] 国标级联 发送BYE: {}", e.getMessage());
-					}
-					redisCatchStorage.deleteSendRTPServer(parentPlatform.getServerGBId(), sendRtpItem.getChannelId(),
-							sendRtpItem.getCallId(), sendRtpItem.getStreamId());
-				}
-			}
-		});
-
-
-		return ret;
-	}
-
-	/**
-	 * rtpServer收流超时
-	 */
-	@ResponseBody
-	@PostMapping(value = "/on_rtp_server_timeout", produces = "application/json;charset=UTF-8")
-	public JSONObject onRtpServerTimeout(HttpServletRequest request, @RequestBody OnRtpServerTimeoutHookParam param){
-		logger.info("[ZLM HOOK] rtpServer收流超时:{}->{}({})", param.getMediaServerId(), param.getStream_id(), param.getSsrc());
-
-		JSONObject ret = new JSONObject();
-		ret.put("code", 0);
-		ret.put("msg", "success");
-
-		taskExecutor.execute(()->{
-			JSONObject json = (JSONObject) JSON.toJSON(param);
-			List<ZlmHttpHookSubscribe.Event> subscribes = this.subscribe.getSubscribes(HookType.on_rtp_server_timeout);
-			if (subscribes != null  && subscribes.size() > 0) {
-				for (ZlmHttpHookSubscribe.Event subscribe : subscribes) {
-					subscribe.response(null, json);
-				}
-			}
-		});
-
-		return ret;
-	}
-
-	private Map<String, String> urlParamToMap(String params) {
-		HashMap<String, String> map = new HashMap<>();
-		if (ObjectUtils.isEmpty(params)) {
-			return map;
-		}
-		String[] paramsArray = params.split("&");
-		if (paramsArray.length == 0) {
-			return map;
-		}
-		for (String param : paramsArray) {
-			String[] paramArray = param.split("=");
-			if (paramArray.length == 2){
-				map.put(paramArray[0], paramArray[1]);
-			}
-		}
-		return map;
-	}
+        }
+        return ret;
+    }
+
+    /**
+     * 流未找到事件,用户可以在此事件触发时,立即去拉流,这样可以实现按需拉流;此事件对回复不敏感。
+     */
+    @ResponseBody
+    @PostMapping(value = "/on_stream_not_found", produces = "application/json;charset=UTF-8")
+    public DeferredResult<HookResult> onStreamNotFound(@RequestBody OnStreamNotFoundHookParam param) {
+        logger.info("[ZLM HOOK] 流未找到:{}->{}->{}/{}", param.getMediaServerId(), param.getSchema(), param.getApp(), param.getStream());
+
+        DeferredResult<HookResult> defaultResult = new DeferredResult<>();
+
+        MediaServerItem mediaInfo = mediaServerService.getOne(param.getMediaServerId());
+        if (!userSetting.isAutoApplyPlay() || mediaInfo == null) {
+            defaultResult.setResult(new HookResult(ErrorCode.ERROR404.getCode(), ErrorCode.ERROR404.getMsg()));
+            return defaultResult;
+        }
+
+        if ("rtp".equals(param.getApp())) {
+            String[] s = param.getStream().split("_");
+            if (!mediaInfo.isRtpEnable() || s.length != 2) {
+                defaultResult.setResult(HookResult.SUCCESS());
+                return defaultResult;
+            }
+            String deviceId = s[0];
+            String channelId = s[1];
+            Device device = redisCatchStorage.getDevice(deviceId);
+            if (device == null) {
+                defaultResult.setResult(new HookResult(ErrorCode.ERROR404.getCode(), ErrorCode.ERROR404.getMsg()));
+                return defaultResult;
+            }
+            DeviceChannel deviceChannel = storager.queryChannel(deviceId, channelId);
+            if (deviceChannel == null) {
+                defaultResult.setResult(new HookResult(ErrorCode.ERROR404.getCode(), ErrorCode.ERROR404.getMsg()));
+                return defaultResult;
+            }
+            logger.info("[ZLM HOOK] 流未找到, 发起自动点播:{}->{}->{}/{}", param.getMediaServerId(), param.getSchema(), param.getApp(), param.getStream());
+            RequestMessage msg = new RequestMessage();
+            String key = DeferredResultHolder.CALLBACK_CMD_PLAY + deviceId + channelId;
+            boolean exist = resultHolder.exist(key, null);
+            msg.setKey(key);
+            String uuid = UUID.randomUUID().toString();
+            msg.setId(uuid);
+            DeferredResult<HookResult> result = new DeferredResult<>(userSetting.getPlayTimeout().longValue());
+            DeferredResultEx<HookResult> deferredResultEx = new DeferredResultEx<>(result);
+
+            result.onTimeout(() -> {
+                logger.info("点播接口等待超时");
+                // 释放rtpserver
+                msg.setData(new HookResult(ErrorCode.ERROR100.getCode(), "点播超时"));
+                resultHolder.invokeResult(msg);
+            });
+            // TODO 在点播未成功的情况下在此调用接口点播会导致返回的流地址ip错误
+            deferredResultEx.setFilter(result1 -> {
+                WVPResult<StreamInfo> wvpResult1 = (WVPResult<StreamInfo>) result1;
+                HookResult resultForEnd = new HookResult();
+                resultForEnd.setCode(wvpResult1.getCode());
+                resultForEnd.setMsg(wvpResult1.getMsg());
+                return resultForEnd;
+            });
+
+            // 录像查询以channelId作为deviceId查询
+            resultHolder.put(key, uuid, deferredResultEx);
+
+            if (!exist) {
+                playService.play(mediaInfo, deviceId, channelId, null, eventResult -> {
+                    msg.setData(new HookResult(eventResult.statusCode, eventResult.msg));
+                    resultHolder.invokeResult(msg);
+                }, null);
+            }
+            return result;
+        } else {
+            // 拉流代理
+            StreamProxyItem streamProxyByAppAndStream = streamProxyService.getStreamProxyByAppAndStream(param.getApp(), param.getStream());
+            if (streamProxyByAppAndStream != null && streamProxyByAppAndStream.isEnable_disable_none_reader()) {
+                streamProxyService.start(param.getApp(), param.getStream());
+            }
+            DeferredResult<HookResult> result = new DeferredResult<>();
+            result.setResult(HookResult.SUCCESS());
+            return result;
+        }
+    }
+
+    /**
+     * 服务器启动事件,可以用于监听服务器崩溃重启;此事件对回复不敏感。
+     */
+    @ResponseBody
+    @PostMapping(value = "/on_server_started", produces = "application/json;charset=UTF-8")
+    public HookResult onServerStarted(HttpServletRequest request, @RequestBody JSONObject jsonObject) {
+
+        jsonObject.put("ip", request.getRemoteAddr());
+        ZLMServerConfig zlmServerConfig = JSON.to(ZLMServerConfig.class, jsonObject);
+        zlmServerConfig.setIp(request.getRemoteAddr());
+        logger.info("[ZLM HOOK] zlm 启动 " + zlmServerConfig.getGeneralMediaServerId());
+        taskExecutor.execute(() -> {
+            List<ZlmHttpHookSubscribe.Event> subscribes = this.subscribe.getSubscribes(HookType.on_server_started);
+            if (subscribes != null && subscribes.size() > 0) {
+                for (ZlmHttpHookSubscribe.Event subscribe : subscribes) {
+                    subscribe.response(null, jsonObject);
+                }
+            }
+            mediaServerService.zlmServerOnline(zlmServerConfig);
+        });
+
+        return HookResult.SUCCESS();
+    }
+
+    /**
+     * 发送rtp(startSendRtp)被动关闭时回调
+     */
+    @ResponseBody
+    @PostMapping(value = "/on_send_rtp_stopped", produces = "application/json;charset=UTF-8")
+    public HookResult onSendRtpStopped(HttpServletRequest request, @RequestBody OnSendRtpStoppedHookParam param) {
+
+        logger.info("[ZLM HOOK] rtp发送关闭:{}->{}/{}", param.getMediaServerId(), param.getApp(), param.getStream());
+
+        // 查找对应的上级推流,发送停止
+        if (!"rtp".equals(param.getApp())) {
+            return HookResult.SUCCESS();
+        }
+        taskExecutor.execute(() -> {
+            List<SendRtpItem> sendRtpItems = redisCatchStorage.querySendRTPServerByStream(param.getStream());
+            if (sendRtpItems.size() > 0) {
+                for (SendRtpItem sendRtpItem : sendRtpItems) {
+                    ParentPlatform parentPlatform = storager.queryParentPlatByServerGBId(sendRtpItem.getPlatformId());
+                    try {
+                        commanderFroPlatform.streamByeCmd(parentPlatform, sendRtpItem.getCallId());
+                    } catch (SipException | InvalidArgumentException | ParseException e) {
+                        logger.error("[命令发送失败] 国标级联 发送BYE: {}", e.getMessage());
+                    }
+                    redisCatchStorage.deleteSendRTPServer(parentPlatform.getServerGBId(), sendRtpItem.getChannelId(),
+                            sendRtpItem.getCallId(), sendRtpItem.getStreamId());
+                }
+            }
+        });
+
+        return HookResult.SUCCESS();
+    }
+
+    /**
+     * rtpServer收流超时
+     */
+    @ResponseBody
+    @PostMapping(value = "/on_rtp_server_timeout", produces = "application/json;charset=UTF-8")
+    public HookResult onRtpServerTimeout(HttpServletRequest request, @RequestBody OnRtpServerTimeoutHookParam param) {
+        logger.info("[ZLM HOOK] rtpServer收流超时:{}->{}({})", param.getMediaServerId(), param.getStream_id(), param.getSsrc());
+
+        taskExecutor.execute(() -> {
+            JSONObject json = (JSONObject) JSON.toJSON(param);
+            List<ZlmHttpHookSubscribe.Event> subscribes = this.subscribe.getSubscribes(HookType.on_rtp_server_timeout);
+            if (subscribes != null && subscribes.size() > 0) {
+                for (ZlmHttpHookSubscribe.Event subscribe : subscribes) {
+                    subscribe.response(null, json);
+                }
+            }
+        });
+
+        return HookResult.SUCCESS();
+    }
+
+    private Map<String, String> urlParamToMap(String params) {
+        HashMap<String, String> map = new HashMap<>();
+        if (ObjectUtils.isEmpty(params)) {
+            return map;
+        }
+        String[] paramsArray = params.split("&");
+        if (paramsArray.length == 0) {
+            return map;
+        }
+        for (String param : paramsArray) {
+            String[] paramArray = param.split("=");
+            if (paramArray.length == 2) {
+                map.put(paramArray[0], paramArray[1]);
+            }
+        }
+        return map;
+    }
 }

+ 36 - 0
src/main/java/com/genersoft/iot/vmp/media/zlm/dto/hook/HookResult.java

@@ -0,0 +1,36 @@
+package com.genersoft.iot.vmp.media.zlm.dto.hook;
+
+public class HookResult {
+
+    private int code;
+    private String msg;
+
+
+    public HookResult() {
+    }
+
+    public HookResult(int code, String msg) {
+        this.code = code;
+        this.msg = msg;
+    }
+
+    public static HookResult SUCCESS(){
+        return new HookResult(0, "success");
+    }
+
+    public int getCode() {
+        return code;
+    }
+
+    public void setCode(int code) {
+        this.code = code;
+    }
+
+    public String getMsg() {
+        return msg;
+    }
+
+    public void setMsg(String msg) {
+        this.msg = msg;
+    }
+}

+ 44 - 0
src/main/java/com/genersoft/iot/vmp/media/zlm/dto/hook/HookResultForOnPublish.java

@@ -0,0 +1,44 @@
+package com.genersoft.iot.vmp.media.zlm.dto.hook;
+
+public class HookResultForOnPublish extends HookResult{
+
+    private boolean enable_audio;
+    private boolean enable_mp4;
+    private int mp4_max_second;
+
+    public HookResultForOnPublish() {
+    }
+
+    public static HookResultForOnPublish SUCCESS(){
+        return new HookResultForOnPublish(0, "success");
+    }
+
+    public HookResultForOnPublish(int code, String msg) {
+        setCode(code);
+        setMsg(msg);
+    }
+
+    public boolean isEnable_audio() {
+        return enable_audio;
+    }
+
+    public void setEnable_audio(boolean enable_audio) {
+        this.enable_audio = enable_audio;
+    }
+
+    public boolean isEnable_mp4() {
+        return enable_mp4;
+    }
+
+    public void setEnable_mp4(boolean enable_mp4) {
+        this.enable_mp4 = enable_mp4;
+    }
+
+    public int getMp4_max_second() {
+        return mp4_max_second;
+    }
+
+    public void setMp4_max_second(int mp4_max_second) {
+        this.mp4_max_second = mp4_max_second;
+    }
+}

+ 4 - 0
src/main/java/com/genersoft/iot/vmp/service/impl/GbStreamServiceImpl.java

@@ -152,6 +152,10 @@ public class GbStreamServiceImpl implements IGbStreamService {
 
     @Override
     public void sendCatalogMsg(GbStream gbStream, String type) {
+        if (gbStream == null || type == null) {
+            logger.warn("[发送目录订阅]类型:流信息或类型为NULL");
+            return;
+        }
         List<GbStream> gbStreams = new ArrayList<>();
         if (gbStream.getGbId() != null) {
             gbStreams.add(gbStream);

+ 3 - 1
src/main/java/com/genersoft/iot/vmp/service/impl/StreamPushServiceImpl.java

@@ -184,7 +184,9 @@ public class StreamPushServiceImpl implements IStreamPushService {
     @Override
     public boolean stop(String app, String streamId) {
         StreamPushItem streamPushItem = streamPushMapper.selectOne(app, streamId);
-        gbStreamService.sendCatalogMsg(streamPushItem, CatalogEvent.DEL);
+        if (streamPushItem != null) {
+            gbStreamService.sendCatalogMsg(streamPushItem, CatalogEvent.DEL);
+        }
 
         platformGbStreamMapper.delByAppAndStream(app, streamId);
         gbStreamMapper.del(app, streamId);

+ 2 - 1
src/main/java/com/genersoft/iot/vmp/storager/IVideoManagerStorage.java

@@ -2,7 +2,6 @@ package com.genersoft.iot.vmp.storager;
 
 import com.genersoft.iot.vmp.gb28181.bean.*;
 import com.genersoft.iot.vmp.media.zlm.dto.StreamProxyItem;
-import com.genersoft.iot.vmp.media.zlm.dto.StreamPushItem;
 import com.genersoft.iot.vmp.service.bean.GPSMsgInfo;
 import com.genersoft.iot.vmp.storager.dao.dto.ChannelSourceInfo;
 import com.genersoft.iot.vmp.vmanager.gb28181.platform.bean.ChannelReduce;
@@ -330,6 +329,8 @@ public interface IVideoManagerStorage {
 	 */
 	boolean resetChannels(String deviceId, List<DeviceChannel> deviceChannelList);
 
+	boolean updateChannels(String deviceId, List<DeviceChannel> deviceChannelList);
+
 	/**
 	 * 获取目录信息
 	 * @param platformId

+ 1 - 1
src/main/java/com/genersoft/iot/vmp/storager/dao/UserMapper.java

@@ -50,7 +50,7 @@ public interface UserMapper {
     @ResultMap(value="roleMap")
     List<User> selectAll();
 
-    @Select("select * from (select user.*, concat(#{callId}_', pushKey) as str1 from user) as u where md5(u.str1) = #{sign}")
+    @Select("select * from (select user.*, concat(concat(#{callId}, '_'), pushKey) as str1 from user) as u where md5(u.str1) = #{sign}")
     List<User> checkPushAuthorityByCallIdAndSign(String callId, String sign);
 
     @Select("select * from user where md5(pushKey) = #{sign}")

+ 113 - 0
src/main/java/com/genersoft/iot/vmp/storager/impl/VideoManagerStorageImpl.java

@@ -194,6 +194,119 @@ public class VideoManagerStorageImpl implements IVideoManagerStorage {
 
 	}
 
+
+	@Override
+	public boolean updateChannels(String deviceId, List<DeviceChannel> deviceChannelList) {
+		if (CollectionUtils.isEmpty(deviceChannelList)) {
+			return false;
+		}
+		List<DeviceChannel> allChannels = deviceChannelMapper.queryAllChannels(deviceId);
+		Map<String,DeviceChannel> allChannelMap = new ConcurrentHashMap<>();
+		if (allChannels.size() > 0) {
+			for (DeviceChannel deviceChannel : allChannels) {
+				allChannelMap.put(deviceChannel.getChannelId(), deviceChannel);
+			}
+		}
+		List<DeviceChannel> addChannels = new ArrayList<>();
+		List<DeviceChannel> updateChannels = new ArrayList<>();
+
+
+		TransactionStatus transactionStatus = dataSourceTransactionManager.getTransaction(transactionDefinition);
+		// 数据去重
+		StringBuilder stringBuilder = new StringBuilder();
+		Map<String, Integer> subContMap = new HashMap<>();
+		if (deviceChannelList.size() > 0) {
+			// 数据去重
+			Set<String> gbIdSet = new HashSet<>();
+			for (DeviceChannel deviceChannel : deviceChannelList) {
+				if (!gbIdSet.contains(deviceChannel.getChannelId())) {
+					gbIdSet.add(deviceChannel.getChannelId());
+					if (allChannelMap.containsKey(deviceChannel.getChannelId())) {
+						deviceChannel.setStreamId(allChannelMap.get(deviceChannel.getChannelId()).getStreamId());
+						deviceChannel.setHasAudio(allChannelMap.get(deviceChannel.getChannelId()).isHasAudio());
+						updateChannels.add(deviceChannel);
+					}else {
+						addChannels.add(deviceChannel);
+					}
+					if (!ObjectUtils.isEmpty(deviceChannel.getParentId())) {
+						if (subContMap.get(deviceChannel.getParentId()) == null) {
+							subContMap.put(deviceChannel.getParentId(), 1);
+						}else {
+							Integer count = subContMap.get(deviceChannel.getParentId());
+							subContMap.put(deviceChannel.getParentId(), count++);
+						}
+					}
+				}else {
+					stringBuilder.append(deviceChannel.getChannelId()).append(",");
+				}
+			}
+			if (addChannels.size() > 0) {
+				for (DeviceChannel channel : addChannels) {
+					if (subContMap.get(channel.getChannelId()) != null){
+						channel.setSubCount(subContMap.get(channel.getChannelId()));
+					}
+				}
+			}
+			if (updateChannels.size() > 0) {
+				for (DeviceChannel channel : updateChannels) {
+					if (subContMap.get(channel.getChannelId()) != null){
+						channel.setSubCount(subContMap.get(channel.getChannelId()));
+					}
+				}
+			}
+
+		}
+		if (stringBuilder.length() > 0) {
+			logger.info("[目录查询]收到的数据存在重复: {}" , stringBuilder);
+		}
+		if(CollectionUtils.isEmpty(updateChannels) && CollectionUtils.isEmpty(addChannels) ){
+			logger.info("通道更新,数据为空={}" , deviceChannelList);
+			return false;
+		}
+		try {
+			int limitCount = 300;
+			boolean result = false;
+			if (addChannels.size() > 0) {
+				if (addChannels.size() > limitCount) {
+					for (int i = 0; i < addChannels.size(); i += limitCount) {
+						int toIndex = i + limitCount;
+						if (i + limitCount > addChannels.size()) {
+							toIndex = addChannels.size();
+						}
+						result = result || deviceChannelMapper.batchAdd(addChannels.subList(i, toIndex)) < 0;
+					}
+				}else {
+					result = result || deviceChannelMapper.batchAdd(addChannels) < 0;
+				}
+			}
+			if (updateChannels.size() > 0) {
+				if (updateChannels.size() > limitCount) {
+					for (int i = 0; i < updateChannels.size(); i += limitCount) {
+						int toIndex = i + limitCount;
+						if (i + limitCount > updateChannels.size()) {
+							toIndex = updateChannels.size();
+						}
+						result = result || deviceChannelMapper.batchUpdate(updateChannels.subList(i, toIndex)) < 0;
+					}
+				}else {
+					result = result || deviceChannelMapper.batchUpdate(updateChannels) < 0;
+				}
+			}
+			if (result) {
+				//事务回滚
+				dataSourceTransactionManager.rollback(transactionStatus);
+			}else {
+				//手动提交
+				dataSourceTransactionManager.commit(transactionStatus);
+			}
+			return true;
+		}catch (Exception e) {
+			e.printStackTrace();
+			dataSourceTransactionManager.rollback(transactionStatus);
+			return false;
+		}
+	}
+
 	@Override
 	public void deviceChannelOnline(String deviceId, String channelId) {
 		deviceChannelMapper.online(deviceId, channelId);

+ 3 - 4
src/main/java/com/genersoft/iot/vmp/utils/redis/RedisUtil.java

@@ -1,14 +1,13 @@
 package com.genersoft.iot.vmp.utils.redis;
 
-import java.util.*;
-import java.util.concurrent.TimeUnit;
-
 import com.alibaba.fastjson2.JSONObject;
 import com.genersoft.iot.vmp.utils.SpringBeanFactory;
-import gov.nist.javax.sip.stack.UDPMessageChannel;
 import org.springframework.data.redis.core.*;
 import org.springframework.util.CollectionUtils;
 
+import java.util.*;
+import java.util.concurrent.TimeUnit;
+
 /**    
  * Redis工具类
  * @author swwheihei

+ 1 - 5
src/main/java/com/genersoft/iot/vmp/vmanager/user/UserController.java

@@ -11,17 +11,13 @@ import com.genersoft.iot.vmp.utils.DateUtil;
 import com.genersoft.iot.vmp.vmanager.bean.ErrorCode;
 import com.genersoft.iot.vmp.vmanager.bean.WVPResult;
 import com.github.pagehelper.PageInfo;
-
 import io.swagger.v3.oas.annotations.Operation;
 import io.swagger.v3.oas.annotations.Parameter;
 import io.swagger.v3.oas.annotations.tags.Tag;
 import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.http.HttpStatus;
-import org.springframework.http.ResponseEntity;
 import org.springframework.security.authentication.AuthenticationManager;
 import org.springframework.util.DigestUtils;
 import org.springframework.util.ObjectUtils;
-import org.springframework.util.StringUtils;
 import org.springframework.web.bind.annotation.*;
 
 import javax.security.sasl.AuthenticationException;
@@ -90,7 +86,7 @@ public class UserController {
 
 
     @PostMapping("/add")
-    @Operation(summary = "停止视频回放")
+    @Operation(summary = "添加用户")
     @Parameter(name = "username", description = "用户名", required = true)
     @Parameter(name = "password", description = "密码(未md5加密的密码)", required = true)
     @Parameter(name = "roleId", description = "角色ID", required = true)

+ 1 - 1
src/main/resources/all-application.yml

@@ -168,7 +168,7 @@ user-settings:
     # 保存移动位置历史轨迹:true:保留历史数据,false:仅保留最后的位置(默认)
     save-position-history: false
     # 点播等待超时时间,单位:毫秒
-    play-timeout: 3000
+    play-timeout: 18000
     # 上级点播等待超时时间,单位:毫秒
     platform-play-timeout: 60000
     # 是否开启接口鉴权

+ 2 - 1
web_src/src/components/GBRecordDetail.vue

@@ -181,7 +181,6 @@
       this.recordListStyle.height = this.winHeight + "px";
       this.playerStyle["height"] = this.winHeight + "px";
       this.chooseDate = moment().format('YYYY-MM-DD')
-      this.setTime(this.chooseDate + " 00:00:00", this.chooseDate + " 23:59:59");
       this.dateChange();
 		},
 		destroyed() {
@@ -192,6 +191,8 @@
         if (!this.chooseDate) {
           return;
         }
+
+        this.setTime(this.chooseDate + " 00:00:00", this.chooseDate + " 23:59:59");
         this.recordsLoading = true;
         this.detailFiles = [];
         this.$axios({