Ver código fonte

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

# Conflicts:
#	src/main/java/com/genersoft/iot/vmp/gb28181/transmit/cmd/SIPRequestHeaderProvider.java
#	src/main/java/com/genersoft/iot/vmp/gb28181/transmit/cmd/impl/SIPCommander.java
#	src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/ByeRequestProcessor.java
#	src/main/java/com/genersoft/iot/vmp/media/zlm/AssistRESTfulUtils.java
#	src/main/java/com/genersoft/iot/vmp/media/zlm/ZLMHttpHookListener.java
#	src/main/java/com/genersoft/iot/vmp/media/zlm/dto/HookSubscribeFactory.java
#	src/main/java/com/genersoft/iot/vmp/service/impl/DeviceServiceImpl.java
#	src/main/java/com/genersoft/iot/vmp/service/impl/MediaServerServiceImpl.java
#	src/main/java/com/genersoft/iot/vmp/service/impl/PlayServiceImpl.java
#	src/main/java/com/genersoft/iot/vmp/storager/dao/PlatformChannelMapper.java
648540858 2 anos atrás
pai
commit
9a96597e66
100 arquivos alterados com 7153 adições e 16434 exclusões
  1. BIN
      libs/jdbc-x86/bcprov-jdk15on-1.70.jar
  2. BIN
      libs/jdbc-x86/kingbase8-8.6.0.jar
  3. BIN
      libs/jdbc-x86/kingbase8-8.6.0.jre6.jar
  4. BIN
      libs/jdbc-x86/kingbase8-8.6.0.jre7.jar
  5. BIN
      libs/jdbc-x86/postgresql-42.2.9.jar
  6. BIN
      libs/jdbc-x86/postgresql-42.2.9.jre6.jar
  7. BIN
      libs/jdbc-x86/postgresql-42.2.9.jre7.jar
  8. 24 22
      pom.xml
  9. 0 8
      sql/2.6.9更新.sql
  10. 9 0
      src/main/java/com/genersoft/iot/vmp/common/StreamInfo.java
  11. 2 1
      src/main/java/com/genersoft/iot/vmp/common/VideoManagerConstants.java
  12. 83 0
      src/main/java/com/genersoft/iot/vmp/conf/CloudRecordTimer.java
  13. 26 1
      src/main/java/com/genersoft/iot/vmp/conf/MediaConfig.java
  14. 8 1
      src/main/java/com/genersoft/iot/vmp/conf/SpringDocConfig.java
  15. 2 0
      src/main/java/com/genersoft/iot/vmp/conf/SystemInfoTimerTask.java
  16. 0 10
      src/main/java/com/genersoft/iot/vmp/conf/UserSetting.java
  17. 1 1
      src/main/java/com/genersoft/iot/vmp/conf/security/JwtUtils.java
  18. 4 2
      src/main/java/com/genersoft/iot/vmp/conf/security/WebSecurityConfig.java
  19. 4 4
      src/main/java/com/genersoft/iot/vmp/gb28181/event/subscribe/catalog/CatalogEventLister.java
  20. 40 2
      src/main/java/com/genersoft/iot/vmp/gb28181/session/VideoStreamSessionManager.java
  21. 2 0
      src/main/java/com/genersoft/iot/vmp/gb28181/task/SipRunner.java
  22. 2 0
      src/main/java/com/genersoft/iot/vmp/gb28181/transmit/cmd/SIPRequestHeaderProvider.java
  23. 13 12
      src/main/java/com/genersoft/iot/vmp/gb28181/transmit/cmd/impl/SIPCommander.java
  24. 5 4
      src/main/java/com/genersoft/iot/vmp/gb28181/transmit/cmd/impl/SIPCommanderFroPlatform.java
  25. 8 9
      src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/ByeRequestProcessor.java
  26. 4 6
      src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/InviteRequestProcessor.java
  27. 38 15
      src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/NotifyRequestForCatalogProcessor.java
  28. 1 1
      src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/notify/cmd/KeepaliveNotifyMessageHandler.java
  29. 5 1
      src/main/java/com/genersoft/iot/vmp/gb28181/utils/XmlUtil.java
  30. 150 39
      src/main/java/com/genersoft/iot/vmp/media/zlm/AssistRESTfulUtils.java
  31. 92 67
      src/main/java/com/genersoft/iot/vmp/media/zlm/ZLMHttpHookListener.java
  32. 10 2
      src/main/java/com/genersoft/iot/vmp/media/zlm/ZLMRESTfulUtils.java
  33. 11 0
      src/main/java/com/genersoft/iot/vmp/media/zlm/dto/HookSubscribeFactory.java
  34. 44 0
      src/main/java/com/genersoft/iot/vmp/media/zlm/dto/HookSubscribeForRecordMp4.java
  35. 20 10
      src/main/java/com/genersoft/iot/vmp/media/zlm/dto/MediaServerItem.java
  36. 11 1
      src/main/java/com/genersoft/iot/vmp/media/zlm/dto/hook/HookResultForOnPublish.java
  37. 114 0
      src/main/java/com/genersoft/iot/vmp/media/zlm/dto/hook/OnRecordMp4HookParam.java
  38. 58 0
      src/main/java/com/genersoft/iot/vmp/service/ICloudRecordService.java
  39. 1 10
      src/main/java/com/genersoft/iot/vmp/service/IMediaServerService.java
  40. 0 6
      src/main/java/com/genersoft/iot/vmp/service/IPlayService.java
  41. 1 0
      src/main/java/com/genersoft/iot/vmp/service/IStreamPushService.java
  42. 205 0
      src/main/java/com/genersoft/iot/vmp/service/bean/CloudRecordItem.java
  43. 41 0
      src/main/java/com/genersoft/iot/vmp/service/bean/DownloadFileInfo.java
  44. 5 5
      src/main/java/com/genersoft/iot/vmp/service/bean/WvpRedisMsg.java
  45. 242 0
      src/main/java/com/genersoft/iot/vmp/service/impl/CloudRecordServiceImpl.java
  46. 21 13
      src/main/java/com/genersoft/iot/vmp/service/impl/DeviceServiceImpl.java
  47. 0 3
      src/main/java/com/genersoft/iot/vmp/service/impl/GbStreamServiceImpl.java
  48. 4 1
      src/main/java/com/genersoft/iot/vmp/service/impl/InviteStreamServiceImpl.java
  49. 25 128
      src/main/java/com/genersoft/iot/vmp/service/impl/MediaServerServiceImpl.java
  50. 1 1
      src/main/java/com/genersoft/iot/vmp/service/impl/MediaServiceImpl.java
  51. 12 4
      src/main/java/com/genersoft/iot/vmp/service/impl/PlatformServiceImpl.java
  52. 134 59
      src/main/java/com/genersoft/iot/vmp/service/impl/PlayServiceImpl.java
  53. 9 1
      src/main/java/com/genersoft/iot/vmp/service/impl/StreamProxyServiceImpl.java
  54. 5 0
      src/main/java/com/genersoft/iot/vmp/service/impl/StreamPushServiceImpl.java
  55. 9 9
      src/main/java/com/genersoft/iot/vmp/service/redisMsg/RedisGbPlayMsgListener.java
  56. 3 2
      src/main/java/com/genersoft/iot/vmp/service/redisMsg/RedisGpsMsgListener.java
  57. 4 0
      src/main/java/com/genersoft/iot/vmp/storager/IRedisCatchStorage.java
  58. 122 0
      src/main/java/com/genersoft/iot/vmp/storager/dao/CloudRecordServiceMapper.java
  59. 83 11
      src/main/java/com/genersoft/iot/vmp/storager/dao/DeviceChannelMapper.java
  60. 1 1
      src/main/java/com/genersoft/iot/vmp/storager/dao/GbStreamMapper.java
  61. 12 0
      src/main/java/com/genersoft/iot/vmp/storager/dao/MediaServerMapper.java
  62. 2 4
      src/main/java/com/genersoft/iot/vmp/storager/dao/PlatformChannelMapper.java
  63. 4 1
      src/main/java/com/genersoft/iot/vmp/storager/dao/PlatformGbStreamMapper.java
  64. 5 4
      src/main/java/com/genersoft/iot/vmp/storager/dao/StreamPushMapper.java
  65. 17 2
      src/main/java/com/genersoft/iot/vmp/storager/impl/RedisCatchStorageImpl.java
  66. 22 0
      src/main/java/com/genersoft/iot/vmp/utils/CloudRecordUtils.java
  67. 31 0
      src/main/java/com/genersoft/iot/vmp/utils/DateUtil.java
  68. 16 0
      src/main/java/com/genersoft/iot/vmp/vmanager/bean/StreamContent.java
  69. 146 38
      src/main/java/com/genersoft/iot/vmp/vmanager/cloudRecord/CloudRecordController.java
  70. 7 5
      src/main/java/com/genersoft/iot/vmp/vmanager/gb28181/MobilePosition/MobilePositionController.java
  71. 5 3
      src/main/java/com/genersoft/iot/vmp/vmanager/gb28181/alarm/AlarmController.java
  72. 4 2
      src/main/java/com/genersoft/iot/vmp/vmanager/gb28181/device/DeviceConfig.java
  73. 10 8
      src/main/java/com/genersoft/iot/vmp/vmanager/gb28181/device/DeviceControl.java
  74. 16 14
      src/main/java/com/genersoft/iot/vmp/vmanager/gb28181/device/DeviceQuery.java
  75. 6 4
      src/main/java/com/genersoft/iot/vmp/vmanager/gb28181/gbStream/GbStreamController.java
  76. 3 1
      src/main/java/com/genersoft/iot/vmp/vmanager/gb28181/media/MediaController.java
  77. 18 16
      src/main/java/com/genersoft/iot/vmp/vmanager/gb28181/platform/PlatformController.java
  78. 9 7
      src/main/java/com/genersoft/iot/vmp/vmanager/gb28181/play/PlayController.java
  79. 8 6
      src/main/java/com/genersoft/iot/vmp/vmanager/gb28181/playback/PlaybackController.java
  80. 5 3
      src/main/java/com/genersoft/iot/vmp/vmanager/gb28181/ptz/PtzController.java
  81. 14 4
      src/main/java/com/genersoft/iot/vmp/vmanager/gb28181/record/GBRecordController.java
  82. 4 2
      src/main/java/com/genersoft/iot/vmp/vmanager/log/LogController.java
  83. 5 3
      src/main/java/com/genersoft/iot/vmp/vmanager/ps/PsController.java
  84. 0 51
      src/main/java/com/genersoft/iot/vmp/vmanager/record/RecordController.java
  85. 6 4
      src/main/java/com/genersoft/iot/vmp/vmanager/rtp/RtpController.java
  86. 12 10
      src/main/java/com/genersoft/iot/vmp/vmanager/server/ServerController.java
  87. 9 7
      src/main/java/com/genersoft/iot/vmp/vmanager/streamProxy/StreamProxyController.java
  88. 9 7
      src/main/java/com/genersoft/iot/vmp/vmanager/streamPush/StreamPushController.java
  89. 5 3
      src/main/java/com/genersoft/iot/vmp/vmanager/user/RoleController.java
  90. 8 7
      src/main/java/com/genersoft/iot/vmp/vmanager/user/UserController.java
  91. 15 9
      src/main/java/com/genersoft/iot/vmp/web/gb28181/ApiDeviceController.java
  92. 1 1
      src/main/java/com/genersoft/iot/vmp/web/gb28181/ApiStreamController.java
  93. 20 3
      src/main/resources/all-application.yml
  94. 2 4
      web_src/build/webpack.dev.conf.js
  95. 4466 15425
      web_src/package-lock.json
  96. 252 178
      web_src/src/components/CloudRecord.vue
  97. 18 22
      web_src/src/components/CloudRecordDetail.vue
  98. 241 102
      web_src/src/components/channelList.vue
  99. 1 1
      web_src/src/components/dialog/easyPlayer.vue
  100. 0 0
      web_src/src/components/common/jessibuca.vue

BIN
libs/jdbc-x86/bcprov-jdk15on-1.70.jar


BIN
libs/jdbc-x86/kingbase8-8.6.0.jar


BIN
libs/jdbc-x86/kingbase8-8.6.0.jre6.jar


BIN
libs/jdbc-x86/kingbase8-8.6.0.jre7.jar


BIN
libs/jdbc-x86/postgresql-42.2.9.jar


BIN
libs/jdbc-x86/postgresql-42.2.9.jre6.jar


BIN
libs/jdbc-x86/postgresql-42.2.9.jre7.jar


+ 24 - 22
pom.xml

@@ -11,7 +11,7 @@
 
     <groupId>com.genersoft</groupId>
     <artifactId>wvp-pro</artifactId>
-    <version>2.6.9</version>
+    <version>2.7.0</version>
     <name>web video platform</name>
     <description>国标28181视频平台</description>
     <packaging>${project.packaging}</packaging>
@@ -143,17 +143,24 @@
             <version>42.5.1</version>
         </dependency>
 
-        <!-- kingbase人大金仓 -->
-        <!-- 手动下载驱动后安装 -->
-        <!-- mvn install:install-file -Dfile=/home/lin/soft/kingbase/jdbc-aarch/kingbase8-8.6.0.jar -DgroupId=com.kingbase -DartifactId=kingbase8
-        -Dversion=8.6.0 -Dpackaging=jar -->
-        <dependency>
-            <groupId>com.kingbase</groupId>
-            <artifactId>kingbase8</artifactId>
-            <version>8.6.0</version>
-            <scope>system</scope>
-            <systemPath>${basedir}/libs/jdbc-aarch/kingbase8-8.6.0.jar</systemPath>
-        </dependency>
+		<!-- kingbase人大金仓 -->
+		<!-- 手动下载驱动后安装 -->
+		<!-- mvn install:install-file -Dfile=/home/lin/soft/kingbase/jdbc-aarch/kingbase8-8.6.0.jar -DgroupId=com.kingbase -DartifactId=kingbase8 -Dversion=8.6.0 -Dpackaging=jar
+ -->
+		<dependency>
+			<groupId>com.kingbase</groupId>
+			<artifactId>kingbase8</artifactId>
+			<version>8.6.0</version>
+			<scope>system</scope>
+			<systemPath>${basedir}/libs/jdbc-aarch/kingbase8-8.6.0.jar</systemPath>
+		</dependency>
+		<dependency>
+			<groupId>com.kingbase</groupId>
+			<artifactId>kingbase8</artifactId>
+			<version>8.6.0</version>
+			<scope>system</scope>
+			<systemPath>${basedir}/libs/jdbc-x86/kingbase8-8.6.0.jar</systemPath>
+		</dependency>
 
         <!--Mybatis分页插件 -->
         <dependency>
@@ -162,22 +169,17 @@
             <version>1.4.6</version>
         </dependency>
 
+        <!--在线文档 -->
         <!--在线文档 -->
         <dependency>
             <groupId>org.springdoc</groupId>
             <artifactId>springdoc-openapi-ui</artifactId>
-            <version>1.7.0</version>
-            <exclusions>
-                <exclusion>
-                    <groupId>org.yaml</groupId>
-                    <artifactId>snakeyaml</artifactId>
-                </exclusion>
-            </exclusions>
+            <version>1.6.10</version>
         </dependency>
         <dependency>
-            <groupId>org.yaml</groupId>
-            <artifactId>snakeyaml</artifactId>
-            <version>2.2</version>
+            <groupId>org.springdoc</groupId>
+            <artifactId>springdoc-openapi-security</artifactId>
+            <version>1.6.10</version>
         </dependency>
 
         <dependency>

+ 0 - 8
sql/2.6.9更新.sql

@@ -1,8 +0,0 @@
-alter table wvp_device_channel
-    change stream_id stream_id varying(255)
-
-alter table wvp_platform
-    add auto_push_channel bool default false
-
-alter table wvp_stream_proxy
-    add stream_key character varying(255)

+ 9 - 0
src/main/java/com/genersoft/iot/vmp/common/StreamInfo.java

@@ -1,5 +1,6 @@
 package com.genersoft.iot.vmp.common;
 
+import com.genersoft.iot.vmp.service.bean.DownloadFileInfo;
 import io.swagger.v3.oas.annotations.media.Schema;
 
 import java.io.Serializable;
@@ -76,6 +77,8 @@ public class StreamInfo implements Serializable, Cloneable{
     private String endTime;
     @Schema(description = "进度(录像下载使用)")
     private double progress;
+    @Schema(description = "文件下载地址(录像下载使用)")
+    private DownloadFileInfo downLoadFilePath;
 
     @Schema(description = "是否暂停(录像回放使用)")
     private boolean pause;
@@ -605,5 +608,11 @@ public class StreamInfo implements Serializable, Cloneable{
         this.subStream = subStream;
     }
 
+    public DownloadFileInfo getDownLoadFilePath() {
+        return downLoadFilePath;
+    }
 
+    public void setDownLoadFilePath(DownloadFileInfo downLoadFilePath) {
+        this.downLoadFilePath = downLoadFilePath;
+    }
 }

+ 2 - 1
src/main/java/com/genersoft/iot/vmp/common/VideoManagerConstants.java

@@ -53,7 +53,7 @@ public class VideoManagerConstants {
 
 	public static final String MEDIA_TRANSACTION_USED_PREFIX = "VMP_MEDIA_TRANSACTION_";
 
-	public static final String MEDIA_STREAM_AUTHORITY = "MEDIA_STREAM_AUTHORITY_";
+	public static final String MEDIA_STREAM_AUTHORITY = "VMP_MEDIA_STREAM_AUTHORITY_";
 
 	public static final String SIP_CSEQ_PREFIX = "VMP_SIP_CSEQ_";
 
@@ -71,6 +71,7 @@ public class VideoManagerConstants {
 	public static final String BROADCAST_WAITE_INVITE = "task_broadcast_waite_invite_";
 
 	public static final String REGISTER_EXPIRE_TASK_KEY_PREFIX = "VMP_device_register_expire_";
+	public static final String PUSH_STREAM_LIST = "VMP_PUSH_STREAM_LIST_";
 
 
 

+ 83 - 0
src/main/java/com/genersoft/iot/vmp/conf/CloudRecordTimer.java

@@ -0,0 +1,83 @@
+package com.genersoft.iot.vmp.conf;
+
+
+import com.alibaba.fastjson2.JSONObject;
+import com.genersoft.iot.vmp.media.zlm.AssistRESTfulUtils;
+import com.genersoft.iot.vmp.media.zlm.ZLMRESTfulUtils;
+import com.genersoft.iot.vmp.media.zlm.dto.MediaServerItem;
+import com.genersoft.iot.vmp.service.IMediaServerService;
+import com.genersoft.iot.vmp.service.bean.CloudRecordItem;
+import com.genersoft.iot.vmp.storager.dao.CloudRecordServiceMapper;
+import com.genersoft.iot.vmp.vmanager.cloudRecord.CloudRecordController;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Component;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.Calendar;
+import java.util.Date;
+import java.util.List;
+
+/**
+ * 录像文件定时删除
+ */
+@Component
+public class CloudRecordTimer {
+
+    private final static Logger logger = LoggerFactory.getLogger(CloudRecordTimer.class);
+
+    @Autowired
+    private IMediaServerService mediaServerService;
+
+    @Autowired
+    private CloudRecordServiceMapper cloudRecordServiceMapper;
+
+    @Autowired
+    private ZLMRESTfulUtils zlmresTfulUtils;
+
+    /**
+     * 定时查询待删除的录像文件
+     */
+//    @Scheduled(fixedRate = 10000) //每五秒执行一次,方便测试
+    @Scheduled(cron = "0 0 0 * * ?")   //每天的0点执行
+    public void execute(){
+        logger.info("[录像文件定时清理] 开始清理过期录像文件");
+        // 获取配置了assist的流媒体节点
+        List<MediaServerItem> mediaServerItemList =  mediaServerService.getAllOnline();
+        if (mediaServerItemList.isEmpty()) {
+            return;
+        }
+        long result = 0;
+        for (MediaServerItem mediaServerItem : mediaServerItemList) {
+
+            Calendar lastCalendar = Calendar.getInstance();
+            if (mediaServerItem.getRecordDay() > 0) {
+                lastCalendar.setTime(new Date());
+                // 获取保存的最后截至日[期,因为每个节点都有一个日期,也就是支持每个节点设置不同的保存日期,
+                lastCalendar.add(Calendar.DAY_OF_MONTH, -mediaServerItem.getRecordDay());
+                Long lastDate = lastCalendar.getTimeInMillis();
+
+                // 获取到截至日期之前的录像文件列表,文件列表满足未被收藏和保持的。这两个字段目前共能一致,
+                // 为我自己业务系统相关的代码,大家使用的时候直接使用收藏(collect)这一个类型即可
+                List<CloudRecordItem> cloudRecordItemList = cloudRecordServiceMapper.queryRecordListForDelete(lastDate, mediaServerItem.getId());
+                if (cloudRecordItemList.isEmpty()) {
+                    continue;
+                }
+                // TODO 后续可以删除空了的过期日期文件夹
+                for (CloudRecordItem cloudRecordItem : cloudRecordItemList) {
+                    String date = new File(cloudRecordItem.getFilePath()).getParentFile().getName();
+                    JSONObject jsonObject = zlmresTfulUtils.deleteRecordDirectory(mediaServerItem, cloudRecordItem.getApp(),
+                            cloudRecordItem.getStream(), date, cloudRecordItem.getFileName());
+                    if (jsonObject.getInteger("code") != 0) {
+                        logger.warn("[录像文件定时清理] 删除磁盘文件错误: {}:{}", cloudRecordItem.getFilePath(), jsonObject);
+                    }
+                }
+                result += cloudRecordServiceMapper.deleteList(cloudRecordItemList);
+            }
+        }
+        logger.info("[录像文件定时清理] 共清理{}个过期录像文件", result);
+    }
+}

+ 26 - 1
src/main/java/com/genersoft/iot/vmp/conf/MediaConfig.java

@@ -81,6 +81,12 @@ public class MediaConfig{
     @Value("${media.record-assist-port:0}")
     private Integer recordAssistPort = 0;
 
+    @Value("${media.record-day:7}")
+    private Integer recordDay;
+
+    @Value("${media.record-path:}")
+    private String recordPath;
+
     public String getId() {
         return id;
     }
@@ -212,13 +218,32 @@ public class MediaConfig{
         mediaServerItem.setSendRtpPortRange(rtpSendPortRange);
         mediaServerItem.setRecordAssistPort(recordAssistPort);
         mediaServerItem.setHookAliveInterval(30.00f);
-
+        mediaServerItem.setRecordDay(recordDay);
+        if (recordPath != null) {
+            mediaServerItem.setRecordPath(recordPath);
+        }
         mediaServerItem.setCreateTime(DateUtil.getNow());
         mediaServerItem.setUpdateTime(DateUtil.getNow());
 
         return mediaServerItem;
     }
 
+    public Integer getRecordDay() {
+        return recordDay;
+    }
+
+    public void setRecordDay(Integer recordDay) {
+        this.recordDay = recordDay;
+    }
+
+    public String getRecordPath() {
+        return recordPath;
+    }
+
+    public void setRecordPath(String recordPath) {
+        this.recordPath = recordPath;
+    }
+
     public String getRtpSendPortRange() {
         return rtpSendPortRange;
     }

+ 8 - 1
src/main/java/com/genersoft/iot/vmp/conf/SpringDocConfig.java

@@ -1,9 +1,12 @@
 package com.genersoft.iot.vmp.conf;
 
+import com.genersoft.iot.vmp.conf.security.JwtUtils;
+import io.swagger.v3.oas.models.Components;
 import io.swagger.v3.oas.models.OpenAPI;
 import io.swagger.v3.oas.models.info.Contact;
 import io.swagger.v3.oas.models.info.Info;
 import io.swagger.v3.oas.models.info.License;
+import io.swagger.v3.oas.models.security.SecurityScheme;
 import org.springframework.core.annotation.Order;
 import org.springdoc.core.GroupedOpenApi;
 import org.springframework.beans.factory.annotation.Value;
@@ -26,10 +29,14 @@ public class SpringDocConfig {
         contact.setName("pan");
         contact.setEmail("648540858@qq.com");
         return new OpenAPI()
+                .components(new Components()
+                        .addSecuritySchemes(JwtUtils.HEADER, new SecurityScheme()
+                                .type(SecurityScheme.Type.HTTP)
+                                .bearerFormat("JWT")))
                 .info(new Info().title("WVP-PRO 接口文档")
                         .contact(contact)
                         .description("开箱即用的28181协议视频平台")
-                        .version("v2.0")
+                        .version("v3.1.0")
                         .license(new License().name("Apache 2.0").url("http://springdoc.org")));
     }
 

+ 2 - 0
src/main/java/com/genersoft/iot/vmp/conf/SystemInfoTimerTask.java

@@ -39,4 +39,6 @@ public class SystemInfoTimerTask {
         }
 
     }
+
+
 }

+ 0 - 10
src/main/java/com/genersoft/iot/vmp/conf/UserSetting.java

@@ -56,8 +56,6 @@ public class UserSetting {
 
     private String serverId = "000000";
 
-    private String recordPath = null;
-
     private String thirdPartyGBIdReg = "[\\s\\S]*";
 
     private String broadcastForPlatform = "UDP";
@@ -262,14 +260,6 @@ public class UserSetting {
         this.refuseChannelStatusChannelFormNotify = refuseChannelStatusChannelFormNotify;
     }
 
-    public String getRecordPath() {
-        return recordPath;
-    }
-
-    public void setRecordPath(String recordPath) {
-        this.recordPath = recordPath;
-    }
-
     public int getMaxNotifyCountQueue() {
         return maxNotifyCountQueue;
     }

+ 1 - 1
src/main/java/com/genersoft/iot/vmp/conf/security/JwtUtils.java

@@ -28,7 +28,7 @@ public class JwtUtils implements InitializingBean {
 
     private static final Logger logger = LoggerFactory.getLogger(JwtUtils.class);
 
-    private static final String HEADER = "access-token";
+    public static final String HEADER = "access-token";
 
     private static final String AUDIENCE = "Audience";
 

+ 4 - 2
src/main/java/com/genersoft/iot/vmp/conf/security/WebSecurityConfig.java

@@ -68,6 +68,8 @@ public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
             matchers.add("/");
             matchers.add("/#/**");
             matchers.add("/static/**");
+            matchers.add("/swagger-ui.html");
+            matchers.add("/swagger-ui/");
             matchers.add("/index.html");
             matchers.add("/doc.html");
             matchers.add("/webjars/**");
@@ -77,7 +79,7 @@ public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
             matchers.add("/api/device/query/snap/**");
             matchers.add("/record_proxy/*/**");
             matchers.add("/api/emit");
-            matchers.addAll(userSetting.getInterfaceAuthenticationExcludes());
+            matchers.add("/favicon.ico");
             // 可以直接访问的静态数据
             web.ignoring().antMatchers(matchers.toArray(new String[0]));
         }
@@ -114,7 +116,7 @@ public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
                 .authorizeRequests()
                 .requestMatchers(CorsUtils::isPreFlightRequest).permitAll()
                 .antMatchers(userSetting.getInterfaceAuthenticationExcludes().toArray(new String[0])).permitAll()
-                .antMatchers("/api/user/login", "/index/hook/**").permitAll()
+                .antMatchers("/api/user/login", "/index/hook/**", "/swagger-ui/**", "/doc.html").permitAll()
                 .anyRequest().authenticated()
                 // 异常处理器
                 .and()

+ 4 - 4
src/main/java/com/genersoft/iot/vmp/gb28181/event/subscribe/catalog/CatalogEventLister.java

@@ -148,13 +148,13 @@ public class CatalogEventLister implements ApplicationListener<CatalogEvent> {
                      if (event.getDeviceChannels() != null) {
                          deviceChannelList.addAll(event.getDeviceChannels());
                      }
-                    if (event.getGbStreams() != null && event.getGbStreams().size() > 0){
+                    if (event.getGbStreams() != null && !event.getGbStreams().isEmpty()){
                         for (GbStream gbStream : event.getGbStreams()) {
                             deviceChannelList.add(
                                     gbStreamService.getDeviceChannelListByStreamWithStatus(gbStream, gbStream.getCatalogId(), parentPlatform));
                         }
                     }
-                    if (deviceChannelList.size() > 0) {
+                    if (!deviceChannelList.isEmpty()) {
                         logger.info("[Catalog事件: {}]平台:{},影响通道{}个", event.getType(), event.getPlatformId(), deviceChannelList.size());
                         try {
                             sipCommanderFroPlatform.sendNotifyForCatalogAddOrUpdate(event.getType(), parentPlatform, deviceChannelList, subscribe, null);
@@ -163,10 +163,10 @@ public class CatalogEventLister implements ApplicationListener<CatalogEvent> {
                             logger.error("[命令发送失败] 国标级联 Catalog通知: {}", e.getMessage());
                         }
                     }
-                }else if (parentPlatformMap.keySet().size() > 0) {
+                }else if (!parentPlatformMap.keySet().isEmpty()) {
                     for (String gbId : parentPlatformMap.keySet()) {
                         List<ParentPlatform> parentPlatforms = parentPlatformMap.get(gbId);
-                        if (parentPlatforms != null && parentPlatforms.size() > 0) {
+                        if (parentPlatforms != null && !parentPlatforms.isEmpty()) {
                             for (ParentPlatform platform : parentPlatforms) {
                                 SubscribeInfo subscribeInfo = subscribeHolder.getCatalogSubscribe(platform.getServerGBId());
                                 if (subscribeInfo == null) {

+ 40 - 2
src/main/java/com/genersoft/iot/vmp/gb28181/session/VideoStreamSessionManager.java

@@ -75,6 +75,33 @@ public class VideoStreamSessionManager {
 		return (SsrcTransaction)redisTemplate.opsForValue().get(scanResult.get(0));
 	}
 
+	public SsrcTransaction getSsrcTransactionByCallId(String callId){
+
+		if (ObjectUtils.isEmpty(callId)) {
+			return null;
+		}
+		String key = VideoManagerConstants.MEDIA_TRANSACTION_USED_PREFIX + userSetting.getServerId() + "_*_*_" + callId+ "_*";
+		List<Object> scanResult = RedisUtil.scan(redisTemplate, key);
+		if (!scanResult.isEmpty()) {
+			return (SsrcTransaction)redisTemplate.opsForValue().get(scanResult.get(0));
+		}else {
+			key = VideoManagerConstants.MEDIA_TRANSACTION_USED_PREFIX + userSetting.getServerId() + "_*_*_play_*";
+			scanResult = RedisUtil.scan(redisTemplate, key);
+			if (scanResult.isEmpty()) {
+				return null;
+			}
+			for (Object keyObj : scanResult) {
+				SsrcTransaction ssrcTransaction = (SsrcTransaction)redisTemplate.opsForValue().get(keyObj);
+				if (ssrcTransaction.getSipTransactionInfo() != null &&
+						ssrcTransaction.getSipTransactionInfo().getCallId().equals(callId)) {
+					return ssrcTransaction;
+				}
+			}
+			return null;
+		}
+
+	}
+
 	public List<SsrcTransaction> getSsrcTransactionForAll(String deviceId, String channelId, String callId, String stream){
 		if (ObjectUtils.isEmpty(deviceId)) {
 			deviceId ="*";
@@ -117,8 +144,19 @@ public class VideoStreamSessionManager {
 	}
 	
 	public void remove(String deviceId, String channelId, String stream) {
-		SsrcTransaction ssrcTransaction = getSsrcTransaction(deviceId, channelId, null, stream);
-		if (ssrcTransaction == null) {
+		List<SsrcTransaction> ssrcTransactionList = getSsrcTransactionForAll(deviceId, channelId, null, stream);
+		if (ssrcTransactionList == null || ssrcTransactionList.isEmpty()) {
+			return;
+		}
+		for (SsrcTransaction ssrcTransaction : ssrcTransactionList) {
+			redisTemplate.delete(VideoManagerConstants.MEDIA_TRANSACTION_USED_PREFIX + userSetting.getServerId() + "_"
+					+  deviceId + "_" + channelId + "_" + ssrcTransaction.getCallId() + "_" + ssrcTransaction.getStream());
+		}
+	}
+
+	public void removeByCallId(String deviceId, String channelId, String callId) {
+		SsrcTransaction ssrcTransaction = getSsrcTransaction(deviceId, channelId, callId, null);
+		if (ssrcTransaction == null ) {
 			return;
 		}
 		redisTemplate.delete(VideoManagerConstants.MEDIA_TRANSACTION_USED_PREFIX + userSetting.getServerId() + "_"

+ 2 - 0
src/main/java/com/genersoft/iot/vmp/gb28181/task/SipRunner.java

@@ -129,4 +129,6 @@ public class SipRunner implements CommandLineRunner {
             }
         }
     }
+
+
 }

+ 2 - 0
src/main/java/com/genersoft/iot/vmp/gb28181/transmit/cmd/SIPRequestHeaderProvider.java

@@ -164,6 +164,7 @@ public class SIPRequestHeaderProvider {
 		Request request = null;
 		//请求行
 		SipURI requestLine = SipFactory.getInstance().createAddressFactory().createSipURI(channelId, device.getHostAddress());
+//		SipURI requestLine = SipFactory.getInstance().createAddressFactory().createSipURI(device.getDeviceId(), device.getHostAddress());
 		// via
 		ArrayList<ViaHeader> viaHeaders = new ArrayList<ViaHeader>();
 		ViaHeader viaHeader = SipFactory.getInstance().createHeaderFactory().createViaHeader(sipLayer.getLocalIp(device.getLocalIp()), sipConfig.getPort(), device.getTransport(), SipUtils.getNewViaTag());
@@ -174,6 +175,7 @@ public class SIPRequestHeaderProvider {
 		FromHeader fromHeader = SipFactory.getInstance().createHeaderFactory().createFromHeader(fromAddress, transactionInfo.getFromTag());
 		//to
 		SipURI toSipURI = SipFactory.getInstance().createAddressFactory().createSipURI(channelId,device.getHostAddress());
+//		SipURI toSipURI = SipFactory.getInstance().createAddressFactory().createSipURI(device.getDeviceId(),device.getHostAddress());
 		Address toAddress = SipFactory.getInstance().createAddressFactory().createAddress(toSipURI);
 		ToHeader toHeader = SipFactory.getInstance().createHeaderFactory().createToHeader(toAddress,	transactionInfo.getToTag());
 

+ 13 - 12
src/main/java/com/genersoft/iot/vmp/gb28181/transmit/cmd/impl/SIPCommander.java

@@ -40,6 +40,8 @@ import javax.sip.SipFactory;
 import javax.sip.header.CallIdHeader;
 import javax.sip.message.Request;
 import java.text.ParseException;
+import java.util.ArrayList;
+import java.util.List;
 
 /**
  * @description:设备能力接口,用于定义设备的控制、查询能力
@@ -677,22 +679,21 @@ public class SIPCommander implements ISIPCommander {
      */
     @Override
     public void streamByeCmd(Device device, String channelId, String stream, String callId, SipSubscribe.Event okEvent) throws InvalidArgumentException, SipException, ParseException, SsrcTransactionNotFoundException {
-        SsrcTransaction ssrcTransaction;
-        if (callId != null) {
-            ssrcTransaction = streamSession.getSsrcTransaction(null, null, callId, null);
-        }else {
-            ssrcTransaction = streamSession.getSsrcTransaction(device.getDeviceId(), channelId, null, stream);
-        }
-        if (ssrcTransaction == null) {
+        List<SsrcTransaction> ssrcTransactionList = streamSession.getSsrcTransactionForAll(device.getDeviceId(), channelId, callId, stream);
+        if (ssrcTransactionList == null || ssrcTransactionList.isEmpty()) {
+            logger.info("[发送BYE] 未找到事务信息,设备: device: {}, channel: {}", device.getDeviceId(), channelId);
             throw new SsrcTransactionNotFoundException(device.getDeviceId(), channelId, callId, stream);
         }
 
-        mediaServerService.releaseSsrc(ssrcTransaction.getMediaServerId(), ssrcTransaction.getSsrc());
-        mediaServerService.closeRTPServer(ssrcTransaction.getMediaServerId(), ssrcTransaction.getStream());
-        streamSession.remove(ssrcTransaction.getDeviceId(), ssrcTransaction.getChannelId(), ssrcTransaction.getStream());
+        for (SsrcTransaction ssrcTransaction : ssrcTransactionList) {
+            logger.info("[发送BYE] 设备: device: {}, channel: {}, callId: {}", device.getDeviceId(), channelId, ssrcTransaction.getCallId());
+            mediaServerService.releaseSsrc(ssrcTransaction.getMediaServerId(), ssrcTransaction.getSsrc());
 
-        Request byteRequest = headerProvider.createByteRequest(device, channelId, ssrcTransaction.getSipTransactionInfo());
-        sipSender.transmitRequest(sipLayer.getLocalIp(device.getLocalIp()), byteRequest, null, okEvent);
+            mediaServerService.closeRTPServer(ssrcTransaction.getMediaServerId(), ssrcTransaction.getStream());
+            streamSession.removeByCallId(ssrcTransaction.getDeviceId(), ssrcTransaction.getChannelId(), ssrcTransaction.getCallId());
+            Request byteRequest = headerProvider.createByteRequest(device, channelId, ssrcTransaction.getSipTransactionInfo());
+            sipSender.transmitRequest(sipLayer.getLocalIp(device.getLocalIp()), byteRequest, null, okEvent);
+        }
     }
 
     @Override

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

@@ -579,7 +579,7 @@ public class SIPCommanderFroPlatform implements ISIPCommanderForPlatform {
 
     @Override
     public void sendNotifyForCatalogAddOrUpdate(String type, ParentPlatform parentPlatform, List<DeviceChannel> deviceChannels, SubscribeInfo subscribeInfo, Integer index) throws InvalidArgumentException, ParseException, NoSuchFieldException, SipException, IllegalAccessException {
-        if (parentPlatform == null || deviceChannels == null || deviceChannels.size() == 0 || subscribeInfo == null) {
+        if (parentPlatform == null || deviceChannels == null || deviceChannels.isEmpty() || subscribeInfo == null) {
             return;
         }
         if (index == null) {
@@ -597,6 +597,7 @@ public class SIPCommanderFroPlatform implements ISIPCommanderForPlatform {
         Integer finalIndex = index;
         String catalogXmlContent = getCatalogXmlContentForCatalogAddOrUpdate(parentPlatform, channels,
                 deviceChannels.size(), type, subscribeInfo);
+        logger.info("[发送NOTIFY通知]类型: {},发送数量: {}", type, channels.size());
         sendNotify(parentPlatform, catalogXmlContent, subscribeInfo, eventResult -> {
             logger.error("发送NOTIFY通知消息失败。错误:{} {}", eventResult.statusCode, eventResult.msg);
         }, (eventResult -> {
@@ -620,7 +621,7 @@ public class SIPCommanderFroPlatform implements ISIPCommanderForPlatform {
 
         SIPRequest notifyRequest = headerProviderPlatformProvider.createNotifyRequest(parentPlatform, catalogXmlContent, subscribeInfo);
 
-        sipSender.transmitRequest(parentPlatform.getDeviceIp(), notifyRequest);
+        sipSender.transmitRequest(parentPlatform.getDeviceIp(), notifyRequest, errorEvent, okEvent);
     }
 
     private  String getCatalogXmlContentForCatalogAddOrUpdate(ParentPlatform parentPlatform, List<DeviceChannel> channels, int sumNum, String type, SubscribeInfo subscribeInfo) {
@@ -632,9 +633,9 @@ public class SIPCommanderFroPlatform implements ISIPCommanderForPlatform {
                 .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("<SumNum>"+ sumNum +"</SumNum>\r\n")
                 .append("<DeviceList Num=\"" + channels.size() + "\">\r\n");
-        if (channels.size() > 0) {
+        if (!channels.isEmpty()) {
             for (DeviceChannel channel : channels) {
                 if (parentPlatform.getServerGBId().equals(channel.getParentId())) {
                     channel.setParentId(parentPlatform.getDeviceGBId());

+ 8 - 9
src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/ByeRequestProcessor.java

@@ -33,6 +33,7 @@ import javax.sip.header.CallIdHeader;
 import javax.sip.message.Response;
 import java.text.ParseException;
 import java.util.HashMap;
+import java.util.List;
 import java.util.Map;
 
 /**
@@ -167,14 +168,12 @@ public class ByeRequestProcessor extends SIPRequestProcessorParent implements In
 			}
 		}
 
-		// 发流端发送的停止
-		SsrcTransaction ssrcTransaction = streamSession.getSsrcTransaction(null, null, callIdHeader.getCallId(), null);
-		if (ssrcTransaction == null ) {
-			logger.info("[收到bye] 但是无法获取推流信息和发流信息,忽略此请求");
-			logger.info(request.toString());
-			return;
-		}
-
+			// 可能是设备发送的停止
+			SsrcTransaction ssrcTransaction = streamSession.getSsrcTransactionByCallId(callIdHeader.getCallId());
+			if (ssrcTransaction == null) {
+				return;
+			}
+			logger.info("[收到bye] 来自设备:{}, 通道已停止推流: {}", ssrcTransaction.getDeviceId(), ssrcTransaction.getChannelId());
 
 		ParentPlatform platform = platformService.queryPlatformByServerGBId(ssrcTransaction.getDeviceId());
 		if (platform != null ) {
@@ -216,7 +215,7 @@ public class ByeRequestProcessor extends SIPRequestProcessorParent implements In
 			if (mediaServerItem != null) {
 				mediaServerService.releaseSsrc(mediaServerItem.getId(), ssrcTransaction.getSsrc());
 			}
-			streamSession.remove(device.getDeviceId(), channel.getChannelId(), ssrcTransaction.getStream());
+			streamSession.removeByCallId(device.getDeviceId(), channel.getChannelId(), ssrcTransaction.getCallId());
 			if (ssrcTransaction.getType() == InviteSessionType.BROADCAST) {
 				// 查找来源的对讲设备,发送停止
 				Device sourceDevice = storager.queryVideoDeviceByPlatformIdAndChannelId(ssrcTransaction.getDeviceId(), ssrcTransaction.getChannelId());

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

@@ -152,7 +152,7 @@ public class InviteRequestProcessor extends SIPRequestProcessorParent implements
             String requesterId = SipUtils.getUserIdFromFromHeader(request);
             CallIdHeader callIdHeader = (CallIdHeader) request.getHeader(CallIdHeader.NAME);
             if (requesterId == null || channelId == null) {
-                logger.info("无法从FromHeader的Address中获取到平台id,返回400");
+                logger.info("无法从请求中获取到平台id,返回400");
                 // 参数不全, 发400,请求错误
                 try {
                     responseAck(request, Response.BAD_REQUEST);
@@ -745,13 +745,10 @@ public class InviteRequestProcessor extends SIPRequestProcessorParent implements
             dynamicTask.startDelay(callIdHeader.getCallId(), () -> {
                 logger.info("[ app={}, stream={} ] 等待设备开始推流超时", gbStream.getApp(), gbStream.getStream());
                 try {
+                    redisPushStreamResponseListener.removeEvent(gbStream.getApp(), gbStream.getStream());
                     mediaListManager.removedChannelOnlineEventLister(gbStream.getApp(), gbStream.getStream());
                     responseAck(request, Response.REQUEST_TIMEOUT); // 超时
-                } catch (SipException e) {
-                    logger.error("未处理的异常 ", e);
-                } catch (InvalidArgumentException e) {
-                    logger.error("未处理的异常 ", e);
-                } catch (ParseException e) {
+                } catch (SipException | InvalidArgumentException | ParseException e) {
                     logger.error("未处理的异常 ", e);
                 }
             }, userSetting.getPlatformPlayTimeout());
@@ -762,6 +759,7 @@ public class InviteRequestProcessor extends SIPRequestProcessorParent implements
             // 添加在本机上线的通知
             mediaListManager.addChannelOnlineEventLister(gbStream.getApp(), gbStream.getStream(), (app, stream, serverId) -> {
                 dynamicTask.stop(callIdHeader.getCallId());
+                redisPushStreamResponseListener.removeEvent(gbStream.getApp(), gbStream.getStream());
                 if (serverId.equals(userSetting.getServerId())) {
                     SendRtpItem sendRtpItem = zlmServerFactory.createSendRtpItem(mediaServerItem, addressStr, finalPort, ssrc, requesterId,
                             app, stream, channelId, mediaTransmissionTCP, platform.isRtcp());

+ 38 - 15
src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/NotifyRequestForCatalogProcessor.java

@@ -13,6 +13,7 @@ import com.genersoft.iot.vmp.gb28181.utils.SipUtils;
 import com.genersoft.iot.vmp.gb28181.utils.XmlUtil;
 import com.genersoft.iot.vmp.service.IDeviceChannelService;
 import com.genersoft.iot.vmp.storager.IRedisCatchStorage;
+import com.genersoft.iot.vmp.utils.DateUtil;
 import org.dom4j.DocumentException;
 import org.dom4j.Element;
 import org.slf4j.Logger;
@@ -185,6 +186,7 @@ public class NotifyRequestForCatalogProcessor extends SIPRequestProcessorParent
 							// 判断此通道是否存在
 							DeviceChannel deviceChannel = deviceChannelService.getOne(deviceId, channel.getChannelId());
 							if (deviceChannel != null) {
+								logger.info("[增加通道] 已存在,不发送通知只更新,设备: {}, 通道 {}", device.getDeviceId(), channel.getChannelId());
 								channel.setId(deviceChannel.getId());
 								updateChannelMap.put(channel.getChannelId(), channel);
 								if (updateChannelMap.keySet().size() > 300) {
@@ -222,6 +224,7 @@ public class NotifyRequestForCatalogProcessor extends SIPRequestProcessorParent
 							DeviceChannel deviceChannelForUpdate = deviceChannelService.getOne(deviceId, channel.getChannelId());
 							if (deviceChannelForUpdate != null) {
 								channel.setId(deviceChannelForUpdate.getId());
+								channel.setUpdateTime(DateUtil.getNow());
 								updateChannelMap.put(channel.getChannelId(), channel);
 								if (updateChannelMap.keySet().size() > 300) {
 									executeSaveForUpdate();
@@ -244,11 +247,11 @@ public class NotifyRequestForCatalogProcessor extends SIPRequestProcessorParent
 					// 转发变化信息
 					eventPublisher.catalogEventPublish(null, channel, event);
 
-					if (updateChannelMap.keySet().size() > 0
-							|| addChannelMap.keySet().size() > 0
-							|| updateChannelOnlineList.size() > 0
-							|| updateChannelOfflineList.size() > 0
-							|| deleteChannelList.size() > 0) {
+					if (!updateChannelMap.keySet().isEmpty()
+							|| !addChannelMap.keySet().isEmpty()
+							|| !updateChannelOnlineList.isEmpty()
+							|| !updateChannelOfflineList.isEmpty()
+							|| !deleteChannelList.isEmpty()) {
 
 						if (!dynamicTask.contains(talkKey)) {
 							dynamicTask.startDelay(talkKey, this::executeSave, 1000);
@@ -262,16 +265,36 @@ public class NotifyRequestForCatalogProcessor extends SIPRequestProcessorParent
 	}
 
 	private void executeSave(){
-		executeSaveForAdd();
-		executeSaveForUpdate();
-		executeSaveForDelete();
-		executeSaveForOnline();
-		executeSaveForOffline();
+		try {
+			executeSaveForAdd();
+		} catch (Exception e) {
+			logger.error("[存储收到的增加通道] 异常: ", e );
+		}
+		try {
+			executeSaveForUpdate();
+		} catch (Exception e) {
+			logger.error("[存储收到的更新通道] 异常: ", e );
+		}
+		try {
+			executeSaveForDelete();
+		} catch (Exception e) {
+			logger.error("[存储收到的删除通道] 异常: ", e );
+		}
+		try {
+			executeSaveForOnline();
+		} catch (Exception e) {
+			logger.error("[存储收到的通道上线] 异常: ", e );
+		}
+		try {
+			executeSaveForOffline();
+		} catch (Exception e) {
+			logger.error("[存储收到的通道离线] 异常: ", e );
+		}
 		dynamicTask.stop(talkKey);
 	}
 
 	private void executeSaveForUpdate(){
-		if (updateChannelMap.values().size() > 0) {
+		if (!updateChannelMap.values().isEmpty()) {
 			ArrayList<DeviceChannel> deviceChannels = new ArrayList<>(updateChannelMap.values());
 			updateChannelMap.clear();
 			deviceChannelService.batchUpdateChannel(deviceChannels);
@@ -280,7 +303,7 @@ public class NotifyRequestForCatalogProcessor extends SIPRequestProcessorParent
 	}
 
 	private void executeSaveForAdd(){
-		if (addChannelMap.values().size() > 0) {
+		if (!addChannelMap.values().isEmpty()) {
 			ArrayList<DeviceChannel> deviceChannels = new ArrayList<>(addChannelMap.values());
 			addChannelMap.clear();
 			deviceChannelService.batchAddChannel(deviceChannels);
@@ -288,21 +311,21 @@ public class NotifyRequestForCatalogProcessor extends SIPRequestProcessorParent
 	}
 
 	private void executeSaveForDelete(){
-		if (deleteChannelList.size() > 0) {
+		if (!deleteChannelList.isEmpty()) {
 			deviceChannelService.deleteChannels(deleteChannelList);
 			deleteChannelList.clear();
 		}
 	}
 
 	private void executeSaveForOnline(){
-		if (updateChannelOnlineList.size() > 0) {
+		if (!updateChannelOnlineList.isEmpty()) {
 			deviceChannelService.channelsOnline(updateChannelOnlineList);
 			updateChannelOnlineList.clear();
 		}
 	}
 
 	private void executeSaveForOffline(){
-		if (updateChannelOfflineList.size() > 0) {
+		if (!updateChannelOfflineList.isEmpty()) {
 			deviceChannelService.channelsOffline(updateChannelOfflineList);
 			updateChannelOfflineList.clear();
 		}

+ 1 - 1
src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/notify/cmd/KeepaliveNotifyMessageHandler.java

@@ -76,7 +76,7 @@ public class KeepaliveNotifyMessageHandler extends SIPRequestProcessorParent imp
 
         RemoteAddressInfo remoteAddressInfo = SipUtils.getRemoteAddressFromRequest(request, userSetting.getSipUseSourceIpAsRemoteAddress());
         if (!device.getIp().equalsIgnoreCase(remoteAddressInfo.getIp()) || device.getPort() != remoteAddressInfo.getPort()) {
-            logger.info("[心跳] 设备{}地址变化, 远程地址为: {}:{}", device.getDeviceId(), remoteAddressInfo.getIp(), remoteAddressInfo.getPort());
+            logger.info("[收到心跳] 设备{}地址变化, 远程地址为: {}:{}", device.getDeviceId(), remoteAddressInfo.getIp(), remoteAddressInfo.getPort());
             device.setPort(remoteAddressInfo.getPort());
             device.setHostAddress(remoteAddressInfo.getIp().concat(":").concat(String.valueOf(remoteAddressInfo.getPort())));
             device.setIp(remoteAddressInfo.getIp());

+ 5 - 1
src/main/java/com/genersoft/iot/vmp/gb28181/utils/XmlUtil.java

@@ -8,6 +8,7 @@ import com.genersoft.iot.vmp.gb28181.bean.Device;
 import com.genersoft.iot.vmp.gb28181.bean.DeviceChannel;
 import com.genersoft.iot.vmp.gb28181.event.subscribe.catalog.CatalogEvent;
 import com.genersoft.iot.vmp.utils.DateUtil;
+import org.apache.commons.lang3.StringUtils;
 import org.apache.commons.lang3.math.NumberUtils;
 import org.dom4j.Attribute;
 import org.dom4j.Document;
@@ -214,8 +215,11 @@ public class XmlUtil {
             return deviceChannel;
         }
         Element nameElement = itemDevice.element("Name");
-        if (nameElement != null) {
+        // 当通道名称为空时,设置通道名称为通道编码,避免级联时因通道名称为空导致上级接收通道失败
+        if (nameElement != null && StringUtils.isNotBlank(nameElement.getText())) {
             deviceChannel.setName(nameElement.getText());
+        } else {
+            deviceChannel.setName(channelId);
         }
         if(channelId.length() <= 8) {
             deviceChannel.setHasAudio(false);

+ 150 - 39
src/main/java/com/genersoft/iot/vmp/media/zlm/AssistRESTfulUtils.java

@@ -9,33 +9,58 @@ import org.jetbrains.annotations.NotNull;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.stereotype.Component;
+import org.springframework.util.ObjectUtils;
 
 import java.io.IOException;
 import java.net.ConnectException;
+import java.net.SocketTimeoutException;
 import java.util.HashMap;
+import java.util.List;
 import java.util.Map;
 import java.util.Objects;
+import java.util.concurrent.TimeUnit;
 
 @Component
 public class AssistRESTfulUtils {
 
     private final static Logger logger = LoggerFactory.getLogger(AssistRESTfulUtils.class);
 
+
+    private OkHttpClient client;
+
+
     public interface RequestCallback{
         void run(JSONObject response);
     }
 
     private OkHttpClient getClient(){
-        OkHttpClient.Builder httpClientBuilder = new OkHttpClient.Builder();
-        if (logger.isDebugEnabled()) {
-            HttpLoggingInterceptor logging = new HttpLoggingInterceptor(message -> {
-                logger.debug("http请求参数:" + message);
-            });
-            logging.setLevel(HttpLoggingInterceptor.Level.BASIC);
-            // OkHttp進行添加攔截器loggingInterceptor
-            httpClientBuilder.addInterceptor(logging);
+        return getClient(null);
+    }
+
+    private OkHttpClient getClient(Integer readTimeOut){
+        if (client == null) {
+            if (readTimeOut == null) {
+                readTimeOut = 10;
+            }
+            OkHttpClient.Builder httpClientBuilder = new OkHttpClient.Builder();
+            // 设置连接超时时间
+            httpClientBuilder.connectTimeout(8, TimeUnit.SECONDS);
+            // 设置读取超时时间
+            httpClientBuilder.readTimeout(readTimeOut,TimeUnit.SECONDS);
+            // 设置连接池
+            httpClientBuilder.connectionPool(new ConnectionPool(16, 5, TimeUnit.MINUTES));
+            if (logger.isDebugEnabled()) {
+                HttpLoggingInterceptor logging = new HttpLoggingInterceptor(message -> {
+                    logger.debug("http请求参数:" + message);
+                });
+                logging.setLevel(HttpLoggingInterceptor.Level.BASIC);
+                // OkHttp進行添加攔截器loggingInterceptor
+                httpClientBuilder.addInterceptor(logging);
+            }
+            client = httpClientBuilder.build();
         }
-        return httpClientBuilder.build();
+        return client;
+
     }
 
 
@@ -123,13 +148,91 @@ public class AssistRESTfulUtils {
         return responseJSON;
     }
 
+    public JSONObject sendPost(MediaServerItem mediaServerItem, String api, JSONObject param, ZLMRESTfulUtils.RequestCallback callback, Integer readTimeOut) {
+        OkHttpClient client = getClient(readTimeOut);
 
-    public JSONObject fileDuration(MediaServerItem mediaServerItem, String app, String stream, RequestCallback callback){
-        Map<String, Object> param = new HashMap<>();
-        param.put("app",app);
-        param.put("stream",stream);
-        param.put("recordIng",true);
-        return sendGet(mediaServerItem, "api/record/file/duration",param, callback);
+        if (mediaServerItem == null) {
+            return null;
+        }
+        String url = String.format("http://%s:%s/%s",  mediaServerItem.getIp(), mediaServerItem.getRecordAssistPort(), api);
+        JSONObject responseJSON = new JSONObject();
+        //-2自定义流媒体 调用错误码
+        responseJSON.put("code",-2);
+        responseJSON.put("msg","ASSIST调用失败");
+
+        RequestBody requestBodyJson = RequestBody.create(MediaType.parse("application/json; charset=utf-8"), param.toString());
+
+        Request request = new Request.Builder()
+                .post(requestBodyJson)
+                .url(url)
+                .addHeader("Content-Type", "application/json")
+                .build();
+        if (callback == null) {
+            try {
+                Response response = client.newCall(request).execute();
+                if (response.isSuccessful()) {
+                    ResponseBody responseBody = response.body();
+                    if (responseBody != null) {
+                        String responseStr = responseBody.string();
+                        responseJSON = JSON.parseObject(responseStr);
+                    }
+                }else {
+                    response.close();
+                    Objects.requireNonNull(response.body()).close();
+                }
+            }catch (IOException e) {
+                logger.error(String.format("[ %s ]ASSIST请求失败: %s", url, e.getMessage()));
+
+                if(e instanceof SocketTimeoutException){
+                    //读取超时超时异常
+                    logger.error(String.format("读取ASSIST数据失败: %s, %s", url, e.getMessage()));
+                }
+                if(e instanceof ConnectException){
+                    //判断连接异常,我这里是报Failed to connect to 10.7.5.144
+                    logger.error(String.format("连接ASSIST失败: %s, %s", url, e.getMessage()));
+                }
+
+            }catch (Exception e){
+                logger.error(String.format("访问ASSIST失败: %s, %s", url, e.getMessage()));
+            }
+        }else {
+            client.newCall(request).enqueue(new Callback(){
+
+                @Override
+                public void onResponse(@NotNull Call call, @NotNull Response response){
+                    if (response.isSuccessful()) {
+                        try {
+                            String responseStr = Objects.requireNonNull(response.body()).string();
+                            callback.run(JSON.parseObject(responseStr));
+                        } catch (IOException e) {
+                            logger.error(String.format("[ %s ]请求失败: %s", url, e.getMessage()));
+                        }
+
+                    }else {
+                        response.close();
+                        Objects.requireNonNull(response.body()).close();
+                    }
+                }
+
+                @Override
+                public void onFailure(@NotNull Call call, @NotNull IOException e) {
+                    logger.error(String.format("连接ZLM失败: %s, %s", call.request().toString(), e.getMessage()));
+
+                    if(e instanceof SocketTimeoutException){
+                        //读取超时超时异常
+                        logger.error(String.format("读取ZLM数据失败: %s, %s", call.request().toString(), e.getMessage()));
+                    }
+                    if(e instanceof ConnectException){
+                        //判断连接异常,我这里是报Failed to connect to 10.7.5.144
+                        logger.error(String.format("连接ZLM失败: %s, %s", call.request().toString(), e.getMessage()));
+                    }
+                }
+            });
+        }
+
+
+
+        return responseJSON;
     }
 
     public JSONObject getInfo(MediaServerItem mediaServerItem, RequestCallback callback){
@@ -137,33 +240,41 @@ public class AssistRESTfulUtils {
         return sendGet(mediaServerItem, "api/record/info",param, callback);
     }
 
-    public JSONObject addStreamCallInfo(MediaServerItem mediaServerItem, String app, String stream, String callId, RequestCallback callback){
-        Map<String, Object> param = new HashMap<>();
-        param.put("app",app);
-        param.put("stream",stream);
-        param.put("callId",callId);
-        return sendGet(mediaServerItem, "api/record/addStreamCallInfo",param, callback);
-    }
+    public JSONObject addTask(MediaServerItem mediaServerItem, String app, String stream, String startTime,
+                              String endTime, String callId, List<String> filePathList, String remoteHost) {
 
-    public JSONObject getDateList(MediaServerItem mediaServerItem, String app, String stream, int year, int month) {
-        Map<String, Object> param = new HashMap<>();
-        param.put("app", app);
-        param.put("stream", stream);
-        param.put("year", year);
-        param.put("month", month);
-        return sendGet(mediaServerItem, "api/record/date/list", param, null);
+        JSONObject videoTaskInfoJSON = new JSONObject();
+        videoTaskInfoJSON.put("app", app);
+        videoTaskInfoJSON.put("stream", stream);
+        videoTaskInfoJSON.put("startTime", startTime);
+        videoTaskInfoJSON.put("endTime", endTime);
+        videoTaskInfoJSON.put("callId", callId);
+        videoTaskInfoJSON.put("filePathList", filePathList);
+        if (!ObjectUtils.isEmpty(remoteHost)) {
+            videoTaskInfoJSON.put("remoteHost", remoteHost);
+        }
+
+        return sendPost(mediaServerItem, "api/record/file/download/task/add", videoTaskInfoJSON, null, 30);
     }
 
-    public JSONObject getFileList(MediaServerItem mediaServerItem, int page, int count, String app, String stream,
-                                  String startTime, String endTime) {
+    public JSONObject queryTaskList(MediaServerItem mediaServerItem, String app, String stream, String callId,  String taskId, Boolean isEnd) {
         Map<String, Object> param = new HashMap<>();
-        param.put("app", app);
-        param.put("stream", stream);
-        param.put("page", page);
-        param.put("count", count);
-        param.put("startTime", startTime);
-        param.put("endTime", endTime);
-        return sendGet(mediaServerItem, "api/record/file/listWithDate", param, null);
-    }
+        if (!ObjectUtils.isEmpty(app)) {
+            param.put("app", app);
+        }
+        if (!ObjectUtils.isEmpty(stream)) {
+            param.put("stream", stream);
+        }
+        if (!ObjectUtils.isEmpty(callId)) {
+            param.put("callId", callId);
+        }
+        if (!ObjectUtils.isEmpty(taskId)) {
+            param.put("taskId", taskId);
+        }
+        if (!ObjectUtils.isEmpty(isEnd)) {
+            param.put("isEnd", isEnd);
+        }
 
+        return sendGet(mediaServerItem, "api/record/file/download/task/list", param, null);
+    }
 }

+ 92 - 67
src/main/java/com/genersoft/iot/vmp/media/zlm/ZLMHttpHookListener.java

@@ -117,6 +117,9 @@ public class ZLMHttpHookListener {
     @Autowired
     private IUserService userService;
 
+    @Autowired
+    private ICloudRecordService cloudRecordService;
+
     @Autowired
     private VideoStreamSessionManager sessionManager;
 
@@ -238,12 +241,6 @@ public class ZLMHttpHookListener {
                 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());
@@ -251,6 +248,7 @@ public class ZLMHttpHookListener {
 
 
         HookResultForOnPublish result = HookResultForOnPublish.SUCCESS();
+        result.setEnable_audio(true);
         taskExecutor.execute(() -> {
             ZlmHttpHookSubscribe.Event subscribe = this.subscribe.sendNotify(HookType.on_publish, json);
             if (subscribe != null) {
@@ -268,7 +266,6 @@ public class ZLMHttpHookListener {
         } else {
             result.setEnable_mp4(userSetting.isRecordPushLive());
         }
-
         // 国标流
         if ("rtp".equals(param.getApp()) ) {
 
@@ -278,14 +275,24 @@ public class ZLMHttpHookListener {
             if (!mediaInfo.isRtpEnable() && inviteInfo == null) {
                 String ssrc = String.format("%010d", Long.parseLong(param.getStream(), 16));
                 inviteInfo = inviteStreamService.getInviteInfoBySSRC(ssrc);
-                result.setStream_replace(inviteInfo.getStream());
-                logger.info("[ZLM HOOK]推流鉴权 stream: {} 替换为 {}", param.getStream(), inviteInfo.getStream());
+                if (inviteInfo != null) {
+                    result.setStream_replace(inviteInfo.getStream());
+                    logger.info("[ZLM HOOK]推流鉴权 stream: {} 替换为 {}", param.getStream(), inviteInfo.getStream());
+                }
             }
 
             // 设置音频信息及录制信息
-            List<SsrcTransaction> ssrcTransactionForAll = (inviteInfo == null ? null :
-                    sessionManager.getSsrcTransactionForAll(inviteInfo.getDeviceId(), inviteInfo.getChannelId(), null, null));
+            List<SsrcTransaction> ssrcTransactionForAll = sessionManager.getSsrcTransactionForAll(null, null, null, param.getStream());
             if (ssrcTransactionForAll != null && ssrcTransactionForAll.size() == 1) {
+
+                // 为录制国标模拟一个鉴权信息, 方便后续写入录像文件时使用
+                StreamAuthorityInfo streamAuthorityInfo = StreamAuthorityInfo.getInstanceByHook(param);
+                streamAuthorityInfo.setApp(param.getApp());
+                streamAuthorityInfo.setStream(ssrcTransactionForAll.get(0).getStream());
+                streamAuthorityInfo.setCallId(ssrcTransactionForAll.get(0).getSipTransactionInfo().getCallId());
+
+                redisCatchStorage.updateStreamAuthorityInfo(param.getApp(), ssrcTransactionForAll.get(0).getStream(), streamAuthorityInfo);
+
                 String deviceId = ssrcTransactionForAll.get(0).getDeviceId();
                 String channelId = ssrcTransactionForAll.get(0).getChannelId();
                 DeviceChannel deviceChannel = storager.queryChannel(deviceId, channelId);
@@ -294,38 +301,29 @@ public class ZLMHttpHookListener {
                 }
                 // 如果是录像下载就设置视频间隔十秒
                 if (ssrcTransactionForAll.get(0).getType() == InviteSessionType.DOWNLOAD) {
-                    result.setMp4_max_second(10);
-                    result.setEnable_mp4(true);
+                    // 获取录像的总时长,然后设置为这个视频的时长
+                    InviteInfo inviteInfoForDownload = inviteStreamService.getInviteInfo(InviteSessionType.DOWNLOAD, deviceId, channelId, param.getStream());
+                    if (inviteInfoForDownload != null && inviteInfoForDownload.getStreamInfo() != null) {
+                        String startTime = inviteInfoForDownload.getStreamInfo().getStartTime();
+                        String endTime = inviteInfoForDownload.getStreamInfo().getEndTime();
+                        long difference = DateUtil.getDifference(startTime, endTime) / 1000;
+                        result.setMp4_max_second((int) difference);
+                        result.setEnable_mp4(true);
+                        // 设置为2保证得到的mp4的时长是正常的
+                        result.setModify_stamp(2);
+                    }
                 }
                 // 如果是talk对讲,则默认获取声音
                 if (ssrcTransactionForAll.get(0).getType() == InviteSessionType.TALK) {
                     result.setEnable_audio(true);
                 }
             }
-        }else if (param.getApp().equals("broadcast")) {
+        }
+        else if (param.getApp().equals("broadcast")) {
             result.setEnable_audio(true);
         }else if (param.getApp().equals("talk")) {
             result.setEnable_audio(true);
         }
-
-        if (mediaInfo.getRecordAssistPort() > 0 && userSetting.getRecordPath() == null) {
-            logger.info("推流时发现尚未设置录像路径,从assist服务中读取");
-            JSONObject info = assistRESTfulUtils.getInfo(mediaInfo, null);
-            if (info != null && info.getInteger("code") != null && info.getInteger("code") == 0) {
-                JSONObject dataJson = info.getJSONObject("data");
-                if (dataJson != null) {
-                    String recordPath = dataJson.getString("record");
-                    userSetting.setRecordPath(recordPath);
-                    result.setMp4_save_path(recordPath);
-                    // 修改zlm中的录像路径
-                    if (mediaInfo.isAutoConfig()) {
-                        taskExecutor.execute(() -> {
-                            mediaServerService.setZLMConfig(mediaInfo, false);
-                        });
-                    }
-                }
-            }
-        }
         if (param.getApp().equalsIgnoreCase("rtp")) {
             String receiveKey = VideoManagerConstants.WVP_OTHER_RECEIVE_RTP_INFO + userSetting.getServerId() + "_" + param.getStream();
             OtherRtpSendInfo otherRtpSendInfo = (OtherRtpSendInfo)redisTemplate.opsForValue().get(receiveKey);
@@ -371,13 +369,11 @@ public class ZLMHttpHookListener {
 
             List<OnStreamChangedHookParam.MediaTrack> tracks = param.getTracks();
             // TODO 重构此处逻辑
-            boolean isPush = false;
             if (param.isRegist()) {
-                // 处理流注册的鉴权信息
+                // 处理流注册的鉴权信息, 流注销这里不再删除鉴权信息,下次来了新的鉴权信息会对就的进行覆盖
                 if (param.getOriginType() == OriginType.RTMP_PUSH.ordinal()
                         || param.getOriginType() == OriginType.RTSP_PUSH.ordinal()
                         || param.getOriginType() == OriginType.RTC_PUSH.ordinal()) {
-                    isPush = true;
                     StreamAuthorityInfo streamAuthorityInfo = redisCatchStorage.getStreamAuthorityInfo(param.getApp(), param.getStream());
                     if (streamAuthorityInfo == null) {
                         streamAuthorityInfo = StreamAuthorityInfo.getInstanceByHook(param);
@@ -387,8 +383,6 @@ public class ZLMHttpHookListener {
                     }
                     redisCatchStorage.updateStreamAuthorityInfo(param.getApp(), param.getStream(), streamAuthorityInfo);
                 }
-            } else {
-                redisCatchStorage.removeStreamAuthorityInfo(param.getApp(), param.getStream());
             }
 
             if ("rtsp".equals(param.getSchema())) {
@@ -470,35 +464,40 @@ public class ZLMHttpHookListener {
                 } 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(), param.getTracks(), 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());
+                        if (param.isRegist()) {
+                            StreamAuthorityInfo streamAuthorityInfo = redisCatchStorage.getStreamAuthorityInfo(
+                                    param.getApp(), param.getStream());
+                            String callId = null;
+                            if (streamAuthorityInfo != null) {
+                                callId = streamAuthorityInfo.getCallId();
+                            }
+                            StreamInfo streamInfoByAppAndStream = mediaService.getStreamInfoByAppAndStream(mediaInfo,
+                                    param.getApp(), param.getStream(), tracks, callId);
+                            param.setStreamInfo(new StreamContent(streamInfoByAppAndStream));
+                            redisCatchStorage.addStream(mediaInfo, 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);
+
+                                // 冗余数据,自己系统中自用
+                                redisCatchStorage.addPushListItem(param.getApp(), param.getStream(), param);
+                            }
+                        } else {
+                            // 兼容流注销时类型从redis记录获取
+                            OnStreamChangedHookParam onStreamChangedHookParam = redisCatchStorage.getStreamInfo(
+                                    param.getApp(), param.getStream(), param.getMediaServerId());
+                            if (onStreamChangedHookParam != null) {
+                                type = OriginType.values()[onStreamChangedHookParam.getOriginType()].getType();
+                                redisCatchStorage.removeStream(mediaInfo.getId(), type, param.getApp(), param.getStream());
+                                if ("PUSH".equalsIgnoreCase(type)) {
+                                    // 冗余数据,自己系统中自用
+                                    redisCatchStorage.removePushListItem(param.getApp(), param.getStream(), param.getMediaServerId());
                                 }
-                                GbStream gbStream = storager.getGbStream(param.getApp(), param.getStream());
-                                if (gbStream != null) {
+                            }
+                            GbStream gbStream = storager.getGbStream(param.getApp(), param.getStream());
+                            if (gbStream != null) {
 //									eventPublisher.catalogEventPublishForStream(null, gbStream, CatalogEvent.OFF);
                             }
                             zlmMediaListManager.removeMedia(param.getApp(), param.getStream());
@@ -618,11 +617,15 @@ public class ZLMHttpHookListener {
                         if (info != null) {
                             cmder.streamByeCmd(device, inviteInfo.getChannelId(),
                                     inviteInfo.getStream(), null);
+                        }else {
+                            logger.info("[无人观看] 未找到设备的点播信息: {}, 流:{}", inviteInfo.getDeviceId(), param.getStream());
                         }
                     } catch (InvalidArgumentException | ParseException | SipException |
                              SsrcTransactionNotFoundException e) {
                         logger.error("[无人观看]点播, 发送BYE失败 {}", e.getMessage());
                     }
+                }else {
+                    logger.info("[无人观看] 未找到设备: {},流:{}", inviteInfo.getDeviceId(), param.getStream());
                 }
 
                 inviteStreamService.removeInviteInfo(inviteInfo.getType(), inviteInfo.getDeviceId(),
@@ -858,7 +861,7 @@ public class ZLMHttpHookListener {
         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) {
+            if (subscribes != null && !subscribes.isEmpty()) {
                 for (ZlmHttpHookSubscribe.Event subscribe : subscribes) {
                     subscribe.response(null, param);
                 }
@@ -868,6 +871,28 @@ public class ZLMHttpHookListener {
         return HookResult.SUCCESS();
     }
 
+    /**
+     * 录像完成事件
+     */
+    @ResponseBody
+    @PostMapping(value = "/on_record_mp4", produces = "application/json;charset=UTF-8")
+    public HookResult onRecordMp4(HttpServletRequest request, @RequestBody OnRecordMp4HookParam param) {
+        logger.info("[ZLM HOOK] 录像完成事件:{}->{}", param.getMediaServerId(), param.getFile_path());
+
+        taskExecutor.execute(() -> {
+            List<ZlmHttpHookSubscribe.Event> subscribes = this.subscribe.getSubscribes(HookType.on_record_mp4);
+            if (subscribes != null && !subscribes.isEmpty()) {
+                for (ZlmHttpHookSubscribe.Event subscribe : subscribes) {
+                    subscribe.response(null, param);
+                }
+            }
+            cloudRecordService.addRecord(param);
+
+        });
+
+        return HookResult.SUCCESS();
+    }
+
     private Map<String, String> urlParamToMap(String params) {
         HashMap<String, String> map = new HashMap<>();
         if (ObjectUtils.isEmpty(params)) {

+ 10 - 2
src/main/java/com/genersoft/iot/vmp/media/zlm/ZLMRESTfulUtils.java

@@ -25,8 +25,6 @@ public class ZLMRESTfulUtils {
 
     private OkHttpClient client;
 
-
-
     public interface RequestCallback{
         void run(JSONObject response);
     }
@@ -405,4 +403,14 @@ public class ZLMRESTfulUtils {
         param.put("stream_id", streamId);
         return sendPost(mediaServerItem, "updateRtpServerSSRC",param, null);
     }
+
+    public JSONObject deleteRecordDirectory(MediaServerItem mediaServerItem, String app, String stream, String date, String fileName) {
+        Map<String, Object> param = new HashMap<>(1);
+        param.put("vhost", "__defaultVhost__");
+        param.put("app", app);
+        param.put("stream", stream);
+        param.put("period", date);
+        param.put("name", fileName);
+        return sendPost(mediaServerItem, "deleteRecordDirectory",param, null);
+    }
 }

+ 11 - 0
src/main/java/com/genersoft/iot/vmp/media/zlm/dto/HookSubscribeFactory.java

@@ -57,4 +57,15 @@ public class HookSubscribeFactory {
         return hookSubscribe;
     }
 
+    public static HookSubscribeForRecordMp4 on_record_mp4(String mediaServerId, String app, String stream) {
+        HookSubscribeForRecordMp4 hookSubscribe = new HookSubscribeForRecordMp4();
+        JSONObject subscribeKey = new com.alibaba.fastjson2.JSONObject();
+        subscribeKey.put("app", app);
+        subscribeKey.put("stream", stream);
+        subscribeKey.put("mediaServerId", mediaServerId);
+        hookSubscribe.setContent(subscribeKey);
+
+        return hookSubscribe;
+    }
+
 }

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

@@ -0,0 +1,44 @@
+package com.genersoft.iot.vmp.media.zlm.dto;
+
+import com.alibaba.fastjson2.JSONObject;
+import com.alibaba.fastjson2.annotation.JSONField;
+
+import java.time.Instant;
+
+/**
+ * hook订阅-录像完成
+ * @author lin
+ */
+public class HookSubscribeForRecordMp4 implements IHookSubscribe{
+
+    private HookType hookType = HookType.on_record_mp4;
+
+    private JSONObject content;
+
+    @JSONField(format="yyyy-MM-dd HH:mm:ss")
+    private Instant expires;
+
+    @Override
+    public HookType getHookType() {
+        return hookType;
+    }
+
+    @Override
+    public JSONObject getContent() {
+        return content;
+    }
+
+    public void setContent(JSONObject content) {
+        this.content = content;
+    }
+
+    @Override
+    public Instant getExpires() {
+        return expires;
+    }
+
+    @Override
+    public void setExpires(Instant expires) {
+        this.expires = expires;
+    }
+}

+ 20 - 10
src/main/java/com/genersoft/iot/vmp/media/zlm/dto/MediaServerItem.java

@@ -80,9 +80,11 @@ public class MediaServerItem{
     @Schema(description = "是否是默认ZLM")
     private boolean defaultServer;
 
-    @Schema(description = "当前使用到的端口")
-    private int currentPort;
+    @Schema(description = "录像存储时长")
+    private int recordDay;
 
+    @Schema(description = "录像存储路径")
+    private String recordPath;
 
     public MediaServerItem() {
     }
@@ -269,14 +271,6 @@ public class MediaServerItem{
         this.updateTime = updateTime;
     }
 
-    public int getCurrentPort() {
-        return currentPort;
-    }
-
-    public void setCurrentPort(int currentPort) {
-        this.currentPort = currentPort;
-    }
-
     public boolean isStatus() {
         return status;
     }
@@ -308,4 +302,20 @@ public class MediaServerItem{
     public void setSendRtpPortRange(String sendRtpPortRange) {
         this.sendRtpPortRange = sendRtpPortRange;
     }
+
+    public int getRecordDay() {
+        return recordDay;
+    }
+
+    public void setRecordDay(int recordDay) {
+        this.recordDay = recordDay;
+    }
+
+    public String getRecordPath() {
+        return recordPath;
+    }
+
+    public void setRecordPath(String recordPath) {
+        this.recordPath = recordPath;
+    }
 }

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

@@ -7,6 +7,7 @@ public class HookResultForOnPublish extends HookResult{
     private int mp4_max_second;
     private String mp4_save_path;
     private String stream_replace;
+    private Integer modify_stamp;
 
     public HookResultForOnPublish() {
     }
@@ -60,14 +61,23 @@ public class HookResultForOnPublish extends HookResult{
         this.stream_replace = stream_replace;
     }
 
+    public Integer getModify_stamp() {
+        return modify_stamp;
+    }
+
+    public void setModify_stamp(Integer modify_stamp) {
+        this.modify_stamp = modify_stamp;
+    }
+
     @Override
     public String toString() {
         return "HookResultForOnPublish{" +
                 "enable_audio=" + enable_audio +
                 ", enable_mp4=" + enable_mp4 +
                 ", mp4_max_second=" + mp4_max_second +
-                ", stream_replace=" + stream_replace +
                 ", mp4_save_path='" + mp4_save_path + '\'' +
+                ", stream_replace='" + stream_replace + '\'' +
+                ", modify_stamp='" + modify_stamp + '\'' +
                 '}';
     }
 }

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

@@ -0,0 +1,114 @@
+package com.genersoft.iot.vmp.media.zlm.dto.hook;
+
+/**
+ * zlm hook事件中的on_rtp_server_timeout事件的参数
+ * @author lin
+ */
+public class OnRecordMp4HookParam extends HookParam{
+    private String app;
+    private String stream;
+    private String file_name;
+    private String file_path;
+    private long file_size;
+    private String folder;
+    private String url;
+    private String vhost;
+    private long start_time;
+    private double time_len;
+
+    public String getApp() {
+        return app;
+    }
+
+    public void setApp(String app) {
+        this.app = app;
+    }
+
+    public String getStream() {
+        return stream;
+    }
+
+    public void setStream(String stream) {
+        this.stream = stream;
+    }
+
+    public String getFile_name() {
+        return file_name;
+    }
+
+    public void setFile_name(String file_name) {
+        this.file_name = file_name;
+    }
+
+    public String getFile_path() {
+        return file_path;
+    }
+
+    public void setFile_path(String file_path) {
+        this.file_path = file_path;
+    }
+
+    public long getFile_size() {
+        return file_size;
+    }
+
+    public void setFile_size(long file_size) {
+        this.file_size = file_size;
+    }
+
+    public String getFolder() {
+        return folder;
+    }
+
+    public void setFolder(String folder) {
+        this.folder = folder;
+    }
+
+    public String getUrl() {
+        return url;
+    }
+
+    public void setUrl(String url) {
+        this.url = url;
+    }
+
+    public String getVhost() {
+        return vhost;
+    }
+
+    public void setVhost(String vhost) {
+        this.vhost = vhost;
+    }
+
+    public long getStart_time() {
+        return start_time;
+    }
+
+    public void setStart_time(long start_time) {
+        this.start_time = start_time;
+    }
+
+    public double getTime_len() {
+        return time_len;
+    }
+
+    public void setTime_len(double time_len) {
+        this.time_len = time_len;
+    }
+
+    @Override
+    public String toString() {
+        return "OnRecordMp4HookParam{" +
+                "app='" + app + '\'' +
+                ", stream='" + stream + '\'' +
+                ", file_name='" + file_name + '\'' +
+                ", file_path='" + file_path + '\'' +
+                ", file_size='" + file_size + '\'' +
+                ", folder='" + folder + '\'' +
+                ", url='" + url + '\'' +
+                ", vhost='" + vhost + '\'' +
+                ", start_time=" + start_time +
+                ", time_len=" + time_len +
+                '}';
+    }
+}

+ 58 - 0
src/main/java/com/genersoft/iot/vmp/service/ICloudRecordService.java

@@ -0,0 +1,58 @@
+package com.genersoft.iot.vmp.service;
+
+import com.alibaba.fastjson2.JSONArray;
+import com.genersoft.iot.vmp.media.zlm.dto.MediaServerItem;
+import com.genersoft.iot.vmp.media.zlm.dto.hook.OnRecordMp4HookParam;
+import com.genersoft.iot.vmp.service.bean.CloudRecordItem;
+import com.genersoft.iot.vmp.service.bean.DownloadFileInfo;
+import com.github.pagehelper.PageInfo;
+
+import java.util.List;
+
+/**
+ * 云端录像管理
+ * @author lin
+ */
+public interface ICloudRecordService {
+
+    /**
+     * 分页回去云端录像列表
+     */
+    PageInfo<CloudRecordItem> getList(int page, int count, String query,  String app, String stream, String startTime, String endTime, List<MediaServerItem> mediaServerItems);
+
+    /**
+     * 根据hook消息增加一条记录
+     */
+    void addRecord(OnRecordMp4HookParam param);
+
+    /**
+     * 获取所有的日期
+     */
+    List<String> getDateList(String app, String stream, int year, int month, List<MediaServerItem> mediaServerItems);
+
+    /**
+     * 添加合并任务
+     */
+    String addTask(String app, String stream, String mediaServerId, String startTime, String endTime, String callId, String remoteHost);
+
+
+    /**
+     * 查询合并任务列表
+     */
+    JSONArray queryTask(String app, String stream, String callId, String taskId, String mediaServerId, Boolean isEnd);
+
+    /**
+     * 收藏视频,收藏的视频过期不会删除
+     */
+    int changeCollect(boolean result, String app, String stream, String mediaServerId, String startTime, String endTime, String callId);
+
+    /**
+     * 添加指定录像收藏
+     */
+    int changeCollectById(Integer recordId, boolean result);
+
+    /**
+     * 获取播放地址
+     */
+    DownloadFileInfo getPlayUrlPath(Integer recordId);
+}

+ 1 - 10
src/main/java/com/genersoft/iot/vmp/service/IMediaServerService.java

@@ -89,21 +89,12 @@ public interface IMediaServerService {
 
     void updateMediaServerKeepalive(String mediaServerId, ServerKeepaliveData data);
 
-    boolean checkRtpServer(MediaServerItem mediaServerItem, String rtp, String stream);
-
     /**
      * 获取负载信息
      * @return
      */
     MediaServerLoad getLoad(MediaServerItem mediaServerItem);
 
-    /**
-     * 按时间查找录像文件
-     */
-    List<RecordFile> getRecords(String app, String stream, String startTime, String endTime, List<MediaServerItem> mediaServerItems);
+    List<MediaServerItem> getAllWithAssistPort();
 
-    /**
-     * 查找存在录像文件的时间
-     */
-    List<String> getRecordDates(String app, String stream, int year, int month, List<MediaServerItem> mediaServerItems);
 }

+ 0 - 6
src/main/java/com/genersoft/iot/vmp/service/IPlayService.java

@@ -33,11 +33,6 @@ public interface IPlayService {
 
     MediaServerItem getNewMediaServerItem(Device device);
 
-    /**
-     * 获取包含assist服务的节点
-     */
-    MediaServerItem getNewMediaServerItemHasAssist(Device device);
-
     void playBack(String deviceId, String channelId, String startTime, String endTime, ErrorCallback<Object> callback);
     void playBack(MediaServerItem mediaServerItem, SSRCInfo ssrcInfo, String deviceId, String channelId, String startTime, String endTime, ErrorCallback<Object> callback);
     void zlmServerOffline(String mediaServerId);
@@ -72,5 +67,4 @@ public interface IPlayService {
 
     void getSnap(String deviceId, String channelId, String fileName, ErrorCallback errorCallback);
 
-
 }

+ 1 - 0
src/main/java/com/genersoft/iot/vmp/service/IStreamPushService.java

@@ -114,4 +114,5 @@ public interface IStreamPushService {
      * @return
      */
     ResourceBaseInfo getOverview();
+
 }

+ 205 - 0
src/main/java/com/genersoft/iot/vmp/service/bean/CloudRecordItem.java

@@ -0,0 +1,205 @@
+package com.genersoft.iot.vmp.service.bean;
+
+import com.genersoft.iot.vmp.media.zlm.dto.hook.OnRecordMp4HookParam;
+
+/**
+ * 云端录像数据
+ */
+public class CloudRecordItem {
+    /**
+     * 主键
+     */
+    private int id;
+    
+    /**
+     * 应用名
+     */
+    private String app;
+    
+    /**
+     * 流
+     */
+    private String stream;
+    
+    /**
+     * 健全ID
+     */
+    private String callId;
+    
+    /**
+     * 开始时间
+     */
+    private long startTime;
+    
+    /**
+     * 结束时间
+     */
+    private long endTime;
+    
+    /**
+     * ZLM Id
+     */
+    private String mediaServerId;
+    
+    /**
+     * 文件名称
+     */
+    private String fileName;
+    
+    /**
+     * 文件路径
+     */
+    private String filePath;
+    
+    /**
+     * 文件夹
+     */
+    private String folder;
+    
+    /**
+     * 收藏,收藏的文件不移除
+     */
+    private Boolean collect;
+
+    /**
+     * 保留,收藏的文件不移除
+     */
+    private Boolean reserve;
+    
+    /**
+     * 文件大小
+     */
+    private long fileSize;
+    
+    /**
+     * 文件时长
+     */
+    private long timeLen;
+
+    public static CloudRecordItem getInstance(OnRecordMp4HookParam param) {
+        CloudRecordItem cloudRecordItem = new CloudRecordItem();
+        cloudRecordItem.setApp(param.getApp());
+        cloudRecordItem.setStream(param.getStream());
+        cloudRecordItem.setStartTime(param.getStart_time()*1000);
+        cloudRecordItem.setFileName(param.getFile_name());
+        cloudRecordItem.setFolder(param.getFolder());
+        cloudRecordItem.setFileSize(param.getFile_size());
+        cloudRecordItem.setFilePath(param.getFile_path());
+        cloudRecordItem.setMediaServerId(param.getMediaServerId());
+        cloudRecordItem.setTimeLen((long) param.getTime_len() * 1000);
+        cloudRecordItem.setEndTime((param.getStart_time() + (long)param.getTime_len()) * 1000);
+        return cloudRecordItem;
+    }
+
+    public int getId() {
+        return id;
+    }
+
+    public void setId(int id) {
+        this.id = id;
+    }
+
+    public String getApp() {
+        return app;
+    }
+
+    public void setApp(String app) {
+        this.app = app;
+    }
+
+    public String getStream() {
+        return stream;
+    }
+
+    public void setStream(String stream) {
+        this.stream = stream;
+    }
+
+    public String getCallId() {
+        return callId;
+    }
+
+    public void setCallId(String callId) {
+        this.callId = callId;
+    }
+
+    public long getStartTime() {
+        return startTime;
+    }
+
+    public void setStartTime(long startTime) {
+        this.startTime = startTime;
+    }
+
+    public long getEndTime() {
+        return endTime;
+    }
+
+    public void setEndTime(long endTime) {
+        this.endTime = endTime;
+    }
+
+    public String getMediaServerId() {
+        return mediaServerId;
+    }
+
+    public void setMediaServerId(String mediaServerId) {
+        this.mediaServerId = mediaServerId;
+    }
+
+    public String getFileName() {
+        return fileName;
+    }
+
+    public void setFileName(String fileName) {
+        this.fileName = fileName;
+    }
+
+    public String getFilePath() {
+        return filePath;
+    }
+
+    public void setFilePath(String filePath) {
+        this.filePath = filePath;
+    }
+
+    public String getFolder() {
+        return folder;
+    }
+
+    public void setFolder(String folder) {
+        this.folder = folder;
+    }
+
+    public long getFileSize() {
+        return fileSize;
+    }
+
+    public void setFileSize(long fileSize) {
+        this.fileSize = fileSize;
+    }
+
+    public long getTimeLen() {
+        return timeLen;
+    }
+
+    public void setTimeLen(long timeLen) {
+        this.timeLen = timeLen;
+    }
+
+    public Boolean getCollect() {
+        return collect;
+    }
+
+    public void setCollect(Boolean collect) {
+        this.collect = collect;
+    }
+
+    public Boolean getReserve() {
+        return reserve;
+    }
+
+    public void setReserve(Boolean reserve) {
+        this.reserve = reserve;
+    }
+}

+ 41 - 0
src/main/java/com/genersoft/iot/vmp/service/bean/DownloadFileInfo.java

@@ -0,0 +1,41 @@
+package com.genersoft.iot.vmp.service.bean;
+
+public class DownloadFileInfo {
+
+    private String httpPath;
+    private String httpsPath;
+    private String httpDomainPath;
+    private String httpsDomainPath;
+
+    public String getHttpPath() {
+        return httpPath;
+    }
+
+    public void setHttpPath(String httpPath) {
+        this.httpPath = httpPath;
+    }
+
+    public String getHttpsPath() {
+        return httpsPath;
+    }
+
+    public void setHttpsPath(String httpsPath) {
+        this.httpsPath = httpsPath;
+    }
+
+    public String getHttpDomainPath() {
+        return httpDomainPath;
+    }
+
+    public void setHttpDomainPath(String httpDomainPath) {
+        this.httpDomainPath = httpDomainPath;
+    }
+
+    public String getHttpsDomainPath() {
+        return httpsDomainPath;
+    }
+
+    public void setHttpsDomainPath(String httpsDomainPath) {
+        this.httpsDomainPath = httpsDomainPath;
+    }
+}

+ 5 - 5
src/main/java/com/genersoft/iot/vmp/service/bean/WvpRedisMsg.java

@@ -29,12 +29,12 @@ public class WvpRedisMsg {
      * 消息的ID
      */
     private String serial;
-    private Object content;
+    private String content;
 
     private final static String requestTag = "req";
     private final static String responseTag = "res";
 
-    public static WvpRedisMsg getRequestInstance(String fromId, String toId, String cmd, String serial, Object content) {
+    public static WvpRedisMsg getRequestInstance(String fromId, String toId, String cmd, String serial, String content) {
         WvpRedisMsg wvpRedisMsg = new WvpRedisMsg();
         wvpRedisMsg.setType(requestTag);
         wvpRedisMsg.setFromId(fromId);
@@ -51,7 +51,7 @@ public class WvpRedisMsg {
         return wvpRedisMsg;
     }
 
-    public static WvpRedisMsg getResponseInstance(String fromId, String toId, String cmd, String serial, Object content) {
+    public static WvpRedisMsg getResponseInstance(String fromId, String toId, String cmd, String serial, String content) {
         WvpRedisMsg wvpRedisMsg = new WvpRedisMsg();
         wvpRedisMsg.setType(responseTag);
         wvpRedisMsg.setFromId(fromId);
@@ -106,11 +106,11 @@ public class WvpRedisMsg {
         this.cmd = cmd;
     }
 
-    public Object getContent() {
+    public String getContent() {
         return content;
     }
 
-    public void setContent(Object content) {
+    public void setContent(String content) {
         this.content = content;
     }
 }

+ 242 - 0
src/main/java/com/genersoft/iot/vmp/service/impl/CloudRecordServiceImpl.java

@@ -0,0 +1,242 @@
+package com.genersoft.iot.vmp.service.impl;
+
+import com.alibaba.fastjson2.JSONArray;
+import com.alibaba.fastjson2.JSONObject;
+import com.genersoft.iot.vmp.conf.exception.ControllerException;
+import com.genersoft.iot.vmp.gb28181.session.VideoStreamSessionManager;
+import com.genersoft.iot.vmp.media.zlm.AssistRESTfulUtils;
+import com.genersoft.iot.vmp.media.zlm.dto.MediaServerItem;
+import com.genersoft.iot.vmp.media.zlm.dto.StreamAuthorityInfo;
+import com.genersoft.iot.vmp.media.zlm.dto.hook.OnRecordMp4HookParam;
+import com.genersoft.iot.vmp.service.ICloudRecordService;
+import com.genersoft.iot.vmp.service.IMediaServerService;
+import com.genersoft.iot.vmp.service.bean.CloudRecordItem;
+import com.genersoft.iot.vmp.service.bean.DownloadFileInfo;
+import com.genersoft.iot.vmp.storager.IRedisCatchStorage;
+import com.genersoft.iot.vmp.storager.dao.CloudRecordServiceMapper;
+import com.genersoft.iot.vmp.utils.CloudRecordUtils;
+import com.genersoft.iot.vmp.utils.DateUtil;
+import com.genersoft.iot.vmp.vmanager.bean.ErrorCode;
+import com.github.pagehelper.PageHelper;
+import com.github.pagehelper.PageInfo;
+import org.apache.commons.lang3.ObjectUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.time.*;
+import java.util.*;
+
+@Service
+public class CloudRecordServiceImpl implements ICloudRecordService {
+
+    private final static Logger logger = LoggerFactory.getLogger(CloudRecordServiceImpl.class);
+
+    @Autowired
+    private CloudRecordServiceMapper cloudRecordServiceMapper;
+
+    @Autowired
+    private IMediaServerService mediaServerService;
+
+    @Autowired
+    private IRedisCatchStorage redisCatchStorage;
+
+    @Autowired
+    private AssistRESTfulUtils assistRESTfulUtils;
+
+    @Autowired
+    private VideoStreamSessionManager streamSession;
+
+    @Override
+    public PageInfo<CloudRecordItem> getList(int page, int count, String query, String app, String stream, String startTime, String endTime, List<MediaServerItem> mediaServerItems) {
+        // 开始时间和结束时间在数据库中都是以秒为单位的
+        Long startTimeStamp = null;
+        Long endTimeStamp = null;
+        if (startTime != null ) {
+            if (!DateUtil.verification(startTime, DateUtil.formatter)) {
+                throw new ControllerException(ErrorCode.ERROR100.getCode(), "开始时间格式错误,正确格式为: " + DateUtil.formatter);
+            }
+            startTimeStamp = DateUtil.yyyy_MM_dd_HH_mm_ssToTimestamp(startTime);
+
+        }
+        if (endTime != null ) {
+            if (!DateUtil.verification(endTime, DateUtil.formatter)) {
+                throw new ControllerException(ErrorCode.ERROR100.getCode(), "结束时间格式错误,正确格式为: " + DateUtil.formatter);
+            }
+            endTimeStamp = DateUtil.yyyy_MM_dd_HH_mm_ssToTimestamp(endTime);
+
+        }
+        PageHelper.startPage(page, count);
+        List<CloudRecordItem> all = cloudRecordServiceMapper.getList(query, app, stream, startTimeStamp, endTimeStamp,
+                null, mediaServerItems);
+        return new PageInfo<>(all);
+    }
+
+    @Override
+    public List<String> getDateList(String app, String stream, int year, int month, List<MediaServerItem> mediaServerItems) {
+        LocalDate startDate = LocalDate.of(year, month, 1);
+        LocalDate endDate;
+        if (month == 12) {
+            endDate = LocalDate.of(year + 1, 1, 1);
+        }else {
+            endDate = LocalDate.of(year, month + 1, 1);
+        }
+        long startTimeStamp = startDate.atStartOfDay().toInstant(ZoneOffset.ofHours(8)).getEpochSecond();
+        long endTimeStamp = endDate.atStartOfDay().toInstant(ZoneOffset.ofHours(8)).getEpochSecond();
+        List<CloudRecordItem> cloudRecordItemList = cloudRecordServiceMapper.getList(null, app, stream, startTimeStamp,
+                endTimeStamp, null, mediaServerItems);
+        if (cloudRecordItemList.isEmpty()) {
+            return new ArrayList<>();
+        }
+        Set<String> resultSet = new HashSet<>();
+        cloudRecordItemList.stream().forEach(cloudRecordItem -> {
+            String date = DateUtil.timestampTo_yyyy_MM_dd(cloudRecordItem.getStartTime());
+            resultSet.add(date);
+        });
+        return new ArrayList<>(resultSet);
+    }
+
+    @Override
+    public void addRecord(OnRecordMp4HookParam param) {
+        CloudRecordItem cloudRecordItem = CloudRecordItem.getInstance(param);
+        StreamAuthorityInfo streamAuthorityInfo = redisCatchStorage.getStreamAuthorityInfo(param.getApp(), param.getStream());
+        if (streamAuthorityInfo != null) {
+            cloudRecordItem.setCallId(streamAuthorityInfo.getCallId());
+        }
+        logger.info("[添加录像记录] {}/{} 文件大小:{}, 时长: {}秒", param.getApp(), param.getStream(), param.getFile_size(),param.getTime_len());
+        cloudRecordServiceMapper.add(cloudRecordItem);
+    }
+
+    @Override
+    public String addTask(String app, String stream, String mediaServerId, String startTime, String endTime, String callId, String remoteHost) {
+        // 参数校验
+        assert app != null;
+        assert stream != null;
+        MediaServerItem mediaServerItem = null;
+        if (mediaServerId == null) {
+            mediaServerItem = mediaServerService.getDefaultMediaServer();
+        }else {
+            mediaServerItem = mediaServerService.getOne(mediaServerId);
+        }
+        if (mediaServerItem == null) {
+            throw new ControllerException(ErrorCode.ERROR100.getCode(), "未找到可用的流媒体");
+        }else {
+            if (remoteHost == null) {
+                remoteHost = "http://" + mediaServerItem.getStreamIp() + ":" + mediaServerItem.getRecordAssistPort();
+            }
+        }
+        if (mediaServerItem.getRecordAssistPort() == 0) {
+            throw new ControllerException(ErrorCode.ERROR100.getCode(), "为配置Assist服务");
+        }
+        Long startTimeStamp = null;
+        Long endTimeStamp = null;
+        if (startTime != null) {
+            startTimeStamp = DateUtil.yyyy_MM_dd_HH_mm_ssToTimestamp(startTime);
+        }
+        if (endTime != null) {
+            endTimeStamp = DateUtil.yyyy_MM_dd_HH_mm_ssToTimestamp(endTime);
+        }
+
+        List<MediaServerItem> mediaServers = new ArrayList<>();
+        mediaServers.add(mediaServerItem);
+        // 检索相关的录像文件
+        List<String> filePathList = cloudRecordServiceMapper.queryRecordFilePathList(app, stream, startTimeStamp, endTimeStamp, callId, mediaServers);
+        if (filePathList == null || filePathList.isEmpty()) {
+            throw new ControllerException(ErrorCode.ERROR100.getCode(), "未检索到视频文件");
+        }
+        JSONObject result =  assistRESTfulUtils.addTask(mediaServerItem, app, stream, startTime, endTime, callId, filePathList, remoteHost);
+        if (result.getInteger("code") != 0) {
+            throw new ControllerException(result.getInteger("code"), result.getString("msg"));
+        }
+        return result.getString("data");
+    }
+
+    @Override
+    public JSONArray queryTask(String app, String stream, String callId, String taskId, String mediaServerId, Boolean isEnd) {
+        MediaServerItem mediaServerItem = null;
+        if (mediaServerId == null) {
+            mediaServerItem = mediaServerService.getDefaultMediaServer();
+        }else {
+            mediaServerItem = mediaServerService.getOne(mediaServerId);
+        }
+        if (mediaServerItem == null) {
+            throw new ControllerException(ErrorCode.ERROR100.getCode(), "未找到可用的流媒体");
+        }
+        JSONObject result =  assistRESTfulUtils.queryTaskList(mediaServerItem, app, stream, callId, taskId, isEnd);
+        if (result.getInteger("code") != 0) {
+            throw new ControllerException(result.getInteger("code"), result.getString("msg"));
+        }
+        return result.getJSONArray("data");
+    }
+
+    @Override
+    public int changeCollect(boolean result, String app, String stream, String mediaServerId, String startTime, String endTime, String callId) {
+        // 开始时间和结束时间在数据库中都是以秒为单位的
+        Long startTimeStamp = null;
+        Long endTimeStamp = null;
+        if (startTime != null ) {
+            if (!DateUtil.verification(startTime, DateUtil.formatter)) {
+                throw new ControllerException(ErrorCode.ERROR100.getCode(), "开始时间格式错误,正确格式为: " + DateUtil.formatter);
+            }
+            startTimeStamp = DateUtil.yyyy_MM_dd_HH_mm_ssToTimestamp(startTime);
+
+        }
+        if (endTime != null ) {
+            if (!DateUtil.verification(endTime, DateUtil.formatter)) {
+                throw new ControllerException(ErrorCode.ERROR100.getCode(), "结束时间格式错误,正确格式为: " + DateUtil.formatter);
+            }
+            endTimeStamp = DateUtil.yyyy_MM_dd_HH_mm_ssToTimestamp(endTime);
+
+        }
+
+        List<MediaServerItem> mediaServerItems;
+        if (!ObjectUtils.isEmpty(mediaServerId)) {
+            mediaServerItems = new ArrayList<>();
+            MediaServerItem mediaServerItem = mediaServerService.getOne(mediaServerId);
+            if (mediaServerItem == null) {
+                throw new ControllerException(ErrorCode.ERROR100.getCode(), "未找到流媒体: " + mediaServerId);
+            }
+            mediaServerItems.add(mediaServerItem);
+        } else {
+            mediaServerItems = null;
+        }
+
+        List<CloudRecordItem> all = cloudRecordServiceMapper.getList(null, app, stream, startTimeStamp, endTimeStamp,
+                callId, mediaServerItems);
+        if (all.isEmpty()) {
+            throw new ControllerException(ErrorCode.ERROR100.getCode(), "未找到待收藏的视频");
+        }
+        int limitCount = 50;
+        int resultCount = 0;
+        if (all.size() > limitCount) {
+            for (int i = 0; i < all.size(); i += limitCount) {
+                int toIndex = i + limitCount;
+                if (i + limitCount > all.size()) {
+                    toIndex = all.size();
+                }
+                resultCount += cloudRecordServiceMapper.updateCollectList(result, all.subList(i, toIndex));
+
+            }
+        }else {
+            resultCount = cloudRecordServiceMapper.updateCollectList(result, all);
+        }
+        return resultCount;
+    }
+
+    @Override
+    public int changeCollectById(Integer recordId, boolean result) {
+       return cloudRecordServiceMapper.changeCollectById(result, recordId);
+    }
+
+    @Override
+    public DownloadFileInfo getPlayUrlPath(Integer recordId) {
+        CloudRecordItem recordItem = cloudRecordServiceMapper.queryOne(recordId);
+        if (recordItem == null) {
+            throw new ControllerException(ErrorCode.ERROR400.getCode(), "资源不存在");
+        }
+        String filePath = recordItem.getFilePath();
+        MediaServerItem mediaServerItem = mediaServerService.getOne(recordItem.getMediaServerId());
+        return CloudRecordUtils.getDownloadFilePath(mediaServerItem, filePath);
+    }
+}

+ 21 - 13
src/main/java/com/genersoft/iot/vmp/service/impl/DeviceServiceImpl.java

@@ -162,6 +162,19 @@ public class DeviceServiceImpl implements IDeviceService {
                     sync(device);
                     // TODO 如果设备下的通道级联到了其他平台,那么需要发送事件或者notify给上级平台
                 }
+                // 上线添加订阅
+                if (device.getSubscribeCycleForCatalog() > 0) {
+                    // 查询在线设备那些开启了订阅,为设备开启定时的目录订阅
+                    addCatalogSubscribe(device);
+                }
+                if (device.getSubscribeCycleForMobilePosition() > 0) {
+                    addMobilePositionSubscribe(device);
+                }
+                if (userSetting.getDeviceStatusNotify()) {
+                    // 发送redis消息
+                    redisCatchStorage.sendDeviceOrChannelStatus(device.getDeviceId(), null, true);
+                }
+
             }else {
                 if (deviceChannelMapper.queryAllChannels(device.getDeviceId()).size() == 0) {
                     logger.info("[设备上线]: {},通道数为0,查询通道信息", device.getDeviceId());
@@ -174,22 +187,10 @@ public class DeviceServiceImpl implements IDeviceService {
 
         }
 
-        // 上线添加订阅
-        if (device.getSubscribeCycleForCatalog() > 0) {
-            // 查询在线设备那些开启了订阅,为设备开启定时的目录订阅
-            addCatalogSubscribe(device);
-        }
-        if (device.getSubscribeCycleForMobilePosition() > 0) {
-            addMobilePositionSubscribe(device);
-        }
         // 刷新过期任务
         String registerExpireTaskKey = VideoManagerConstants.REGISTER_EXPIRE_TASK_KEY_PREFIX + device.getDeviceId();
         // 如果第一次注册那么必须在60 * 3时间内收到一个心跳,否则设备离线
         dynamicTask.startDelay(registerExpireTaskKey, ()-> offline(device.getDeviceId(), "首次注册后未能收到心跳"), device.getKeepaliveIntervalTime() * 1000 * 3);
-        if (userSetting.getDeviceStatusNotify()) {
-            // 发送redis消息
-            redisCatchStorage.sendDeviceOrChannelStatus(device.getDeviceId(), null, true);
-        }
 
 //
 //        try {
@@ -213,6 +214,13 @@ public class DeviceServiceImpl implements IDeviceService {
         }
         String registerExpireTaskKey = VideoManagerConstants.REGISTER_EXPIRE_TASK_KEY_PREFIX + deviceId;
         dynamicTask.stop(registerExpireTaskKey);
+        if (device.isOnLine()) {
+            if (userSetting.getDeviceStatusNotify()) {
+                // 发送redis消息
+                redisCatchStorage.sendDeviceOrChannelStatus(device.getDeviceId(), null, false);
+            }
+        }
+
         device.setOnLine(false);
         redisCatchStorage.updateDevice(device);
         deviceMapper.update(device);
@@ -224,7 +232,7 @@ public class DeviceServiceImpl implements IDeviceService {
             for (SsrcTransaction ssrcTransaction : ssrcTransactions) {
                 mediaServerService.releaseSsrc(ssrcTransaction.getMediaServerId(), ssrcTransaction.getSsrc());
                 mediaServerService.closeRTPServer(ssrcTransaction.getMediaServerId(), ssrcTransaction.getStream());
-                streamSession.remove(deviceId, ssrcTransaction.getChannelId(), ssrcTransaction.getStream());
+                streamSession.removeByCallId(deviceId, ssrcTransaction.getChannelId(), ssrcTransaction.getCallId());
             }
         }
         // 移除订阅

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

@@ -250,9 +250,6 @@ public class GbStreamServiceImpl implements IGbStreamService {
         if (platform == null) {
             return ;
         }
-        if (ObjectUtils.isEmpty(catalogId)) {
-            catalogId = platform.getDeviceGBId();
-        }
         if (platformGbStreamMapper.delByPlatformAndCatalogId(platformId, catalogId) > 0) {
             List<GbStream> gbStreams = platformGbStreamMapper.queryChannelInParentPlatformAndCatalog(platformId, catalogId);
             List<DeviceChannel> deviceChannelList = new ArrayList<>();

+ 4 - 1
src/main/java/com/genersoft/iot/vmp/service/impl/InviteStreamServiceImpl.java

@@ -116,9 +116,12 @@ public class InviteStreamServiceImpl implements IInviteStreamService {
                 ":" + (stream != null ? stream : "*")
                 + ":*";
         List<Object> scanResult = RedisUtil.scan(redisTemplate, key);
-        if (scanResult.size() != 1) {
+        if (scanResult.isEmpty()) {
             return null;
         }
+        if (scanResult.size() != 1) {
+            logger.warn("[获取InviteInfo] 发现 key: {}存在多条", key);
+        }
 
         return (InviteInfo) redisTemplate.opsForValue().get(scanResult.get(0));
     }

+ 25 - 128
src/main/java/com/genersoft/iot/vmp/service/impl/MediaServerServiceImpl.java

@@ -165,14 +165,13 @@ public class MediaServerServiceImpl implements IMediaServerService {
         if (streamId == null) {
             streamId = String.format("%08x", Long.parseLong(ssrc)).toUpperCase();
         }
-        int ssrcCheckParam = 0;
-        if (ssrcCheck && tcpMode > 1) {
+        if (ssrcCheck && tcpMode > 0) {
             // 目前zlm不支持 tcp模式更新ssrc,暂时关闭ssrc校验
-            logger.warn("[openRTPServer] TCP被动/TCP主动收流时,默认关闭ssrc检验");
+            logger.warn("[openRTPServer] 平台对接时下级可能自定义ssrc,但是tcp模式zlm收流目前无法更新ssrc,可能收流超时,此时请使用udp收流或者关闭ssrc校验");
         }
         int rtpServerPort;
         if (mediaServerItem.isRtpEnable()) {
-            rtpServerPort = zlmServerFactory.createRTPServer(mediaServerItem, streamId, (ssrcCheck && tcpMode == 0) ? Long.parseLong(ssrc) : 0, port, onlyAuto, reUsePort, tcpMode);
+            rtpServerPort = zlmServerFactory.createRTPServer(mediaServerItem, streamId, ssrcCheck ? Long.parseLong(ssrc) : 0, port, onlyAuto, reUsePort, tcpMode);
         } else {
             rtpServerPort = mediaServerItem.getRtpProxyPort();
         }
@@ -205,7 +204,10 @@ public class MediaServerServiceImpl implements IMediaServerService {
     @Override
     public void closeRTPServer(String mediaServerId, String streamId) {
         MediaServerItem mediaServerItem = this.getOne(mediaServerId);
-        closeRTPServer(mediaServerItem, streamId);
+        if (mediaServerItem.isRtpEnable()) {
+            closeRTPServer(mediaServerItem, streamId);
+        }
+        zlmresTfulUtils.closeStreams(mediaServerItem, "rtp", streamId);
     }
 
     @Override
@@ -428,17 +430,6 @@ public class MediaServerServiceImpl implements IMediaServerService {
 
 
         if (serverItem.isAutoConfig()) {
-            // 查看assist服务的录像路径配置
-            if (serverItem.getRecordAssistPort() > 0 && userSetting.getRecordPath() == null) {
-                JSONObject info = assistRESTfulUtils.getInfo(serverItem, null);
-                if (info != null && info.getInteger("code") != null && info.getInteger("code") == 0 ) {
-                    JSONObject dataJson = info.getJSONObject("data");
-                    if (dataJson != null) {
-                        String recordPath = dataJson.getString("record");
-                        userSetting.setRecordPath(recordPath);
-                    }
-                }
-            }
             setZLMConfig(serverItem, "0".equals(zlmServerConfig.getHookEnable()));
         }
         final String zlmKeepaliveKey = zlmKeepaliveKeyPrefix + serverItem.getId();
@@ -573,7 +564,7 @@ public class MediaServerServiceImpl implements IMediaServerService {
         logger.info("[ZLM] 正在设置 :{} -> {}:{}",
                 mediaServerItem.getId(), mediaServerItem.getIp(), mediaServerItem.getHttpPort());
         String protocol = sslEnabled ? "https" : "http";
-        String hookPrex = String.format("%s://%s:%s/index/hook", protocol, mediaServerItem.getHookIp(), serverPort);
+        String hookPrefix = String.format("%s://%s:%s/index/hook", protocol, mediaServerItem.getHookIp(), serverPort);
 
         Map<String, Object> param = new HashMap<>();
         param.put("api.secret",mediaServerItem.getSecret()); // -profile:v Baseline
@@ -582,25 +573,21 @@ public class MediaServerServiceImpl implements IMediaServerService {
         }
         param.put("hook.enable","1");
         param.put("hook.on_flow_report","");
-        param.put("hook.on_play",String.format("%s/on_play", hookPrex));
+        param.put("hook.on_play",String.format("%s/on_play", hookPrefix));
         param.put("hook.on_http_access","");
-        param.put("hook.on_publish", String.format("%s/on_publish", hookPrex));
+        param.put("hook.on_publish", String.format("%s/on_publish", hookPrefix));
         param.put("hook.on_record_ts","");
         param.put("hook.on_rtsp_auth","");
         param.put("hook.on_rtsp_realm","");
-        param.put("hook.on_server_started",String.format("%s/on_server_started", hookPrex));
+        param.put("hook.on_server_started",String.format("%s/on_server_started", hookPrefix));
         param.put("hook.on_shell_login","");
-        param.put("hook.on_stream_changed",String.format("%s/on_stream_changed", hookPrex));
-        param.put("hook.on_stream_none_reader",String.format("%s/on_stream_none_reader", hookPrex));
-        param.put("hook.on_stream_not_found",String.format("%s/on_stream_not_found", hookPrex));
-        param.put("hook.on_server_keepalive",String.format("%s/on_server_keepalive", hookPrex));
-        param.put("hook.on_send_rtp_stopped",String.format("%s/on_send_rtp_stopped", hookPrex));
-        param.put("hook.on_rtp_server_timeout",String.format("%s/on_rtp_server_timeout", hookPrex));
-        if (mediaServerItem.getRecordAssistPort() > 0) {
-            param.put("hook.on_record_mp4",String.format("http://127.0.0.1:%s/api/record/on_record_mp4", mediaServerItem.getRecordAssistPort()));
-        }else {
-            param.put("hook.on_record_mp4","");
-        }
+        param.put("hook.on_stream_changed",String.format("%s/on_stream_changed", hookPrefix));
+        param.put("hook.on_stream_none_reader",String.format("%s/on_stream_none_reader", hookPrefix));
+        param.put("hook.on_stream_not_found",String.format("%s/on_stream_not_found", hookPrefix));
+        param.put("hook.on_server_keepalive",String.format("%s/on_server_keepalive", hookPrefix));
+        param.put("hook.on_send_rtp_stopped",String.format("%s/on_send_rtp_stopped", hookPrefix));
+        param.put("hook.on_rtp_server_timeout",String.format("%s/on_rtp_server_timeout", hookPrefix));
+        param.put("hook.on_record_mp4",String.format("%s/on_record_mp4", hookPrefix));
         param.put("hook.timeoutSec","20");
         // 推流断开后可以在超时时间内重新连接上继续推流,这样播放器会接着播放。
         // 置0关闭此特性(推流断开会导致立即断开播放器)
@@ -609,15 +596,14 @@ public class MediaServerServiceImpl implements IMediaServerService {
         param.put("protocol.continue_push_ms", "3000" );
         // 最多等待未初始化的Track时间,单位毫秒,超时之后会忽略未初始化的Track, 设置此选项优化那些音频错误的不规范流,
         // 等zlm支持给每个rtpServer设置关闭音频的时候可以不设置此选项
-//        param.put("general.wait_track_ready_ms", "3000" );
         if (mediaServerItem.isRtpEnable() && !ObjectUtils.isEmpty(mediaServerItem.getRtpPortRange())) {
             param.put("rtp_proxy.port_range", mediaServerItem.getRtpPortRange().replace(",", "-"));
         }
 
-        if (userSetting.getRecordPath() != null) {
-            File recordPathFile = new File(userSetting.getRecordPath());
-            File mp4SavePathFile = recordPathFile.getParentFile().getAbsoluteFile();
-            param.put("protocol.mp4_save_path", mp4SavePathFile.getAbsoluteFile());
+        if (!ObjectUtils.isEmpty(mediaServerItem.getRecordPath())) {
+            File recordPathFile = new File(mediaServerItem.getRecordPath());
+            param.put("protocol.mp4_save_path", recordPathFile.getParentFile().getPath());
+            param.put("protocol.downloadRoot", recordPathFile.getParentFile().getPath());
             param.put("record.appName", recordPathFile.getName());
         }
 
@@ -722,6 +708,7 @@ public class MediaServerServiceImpl implements IMediaServerService {
             ssrcFactory.initMediaServerSSRC(mediaServerItem.getId(), null);
             String key = VideoManagerConstants.MEDIA_SERVER_PREFIX + userSetting.getServerId() + "_" + mediaServerItem.getId();
             redisTemplate.opsForValue().set(key, mediaServerItem);
+            resetOnlineServerItem(mediaServerItem);
             clearRTPServer(mediaServerItem);
         }
         final String zlmKeepaliveKey = zlmKeepaliveKeyPrefix + mediaServerItem.getId();
@@ -749,15 +736,6 @@ public class MediaServerServiceImpl implements IMediaServerService {
         }
     }
 
-    @Override
-    public boolean checkRtpServer(MediaServerItem mediaServerItem, String app, String stream) {
-        JSONObject rtpInfo = zlmresTfulUtils.getRtpInfo(mediaServerItem, stream);
-        if(rtpInfo.getInteger("code") == 0){
-            return rtpInfo.getBoolean("exist");
-        }
-        return false;
-    }
-
     @Override
     public MediaServerLoad getLoad(MediaServerItem mediaServerItem) {
         MediaServerLoad result = new MediaServerLoad();
@@ -771,88 +749,7 @@ public class MediaServerServiceImpl implements IMediaServerService {
     }
 
     @Override
-    public List<RecordFile> getRecords(String app, String stream, String startTime, String endTime, List<MediaServerItem> mediaServerItems) {
-        Assert.notNull(app, "app不存在");
-        Assert.notNull(stream, "stream不存在");
-        Assert.notNull(startTime, "startTime不存在");
-        Assert.notNull(endTime, "endTime不存在");
-        Assert.notEmpty(mediaServerItems, "流媒体列表为空");
-
-        CompletableFuture[] completableFutures = new CompletableFuture[mediaServerItems.size()];
-        for (int i = 0; i < mediaServerItems.size(); i++) {
-            completableFutures[i] = getRecordFilesForOne(app, stream, startTime, endTime, mediaServerItems.get(i));
-        }
-        List<RecordFile> result = new ArrayList<>();
-        for (int i = 0; i < completableFutures.length; i++) {
-            try {
-                List<RecordFile> list = (List<RecordFile>) completableFutures[i].get();
-                if (!list.isEmpty()) {
-                    for (int g = 0; g < list.size(); g++) {
-                        list.get(g).setMediaServerId(mediaServerItems.get(i).getId());
-                    }
-                    result.addAll(list);
-                }
-            } catch (InterruptedException e) {
-                throw new RuntimeException(e);
-            } catch (ExecutionException e) {
-                throw new RuntimeException(e);
-            }
-        }
-        Comparator<RecordFile> comparator = Comparator.comparing(RecordFile::getFileName);
-        result.sort(comparator);
-        return result;
-    }
-
-    @Override
-    public List<String> getRecordDates(String app, String stream, int year, int month, List<MediaServerItem> mediaServerItems) {
-        Assert.notNull(app, "app不存在");
-        Assert.notNull(stream, "stream不存在");
-        Assert.notEmpty(mediaServerItems, "流媒体列表为空");
-        CompletableFuture[] completableFutures = new CompletableFuture[mediaServerItems.size()];
-
-        for (int i = 0; i < mediaServerItems.size(); i++) {
-            completableFutures[i] = getRecordDatesForOne(app, stream, year, month, mediaServerItems.get(i));
-        }
-        List<String> result = new ArrayList<>();
-        CompletableFuture.allOf(completableFutures).join();
-        for (CompletableFuture completableFuture : completableFutures) {
-            try {
-                List<String> list = (List<String>) completableFuture.get();
-                result.addAll(list);
-            } catch (InterruptedException e) {
-                throw new RuntimeException(e);
-            } catch (ExecutionException e) {
-                throw new RuntimeException(e);
-            }
-        }
-        Collections.sort(result);
-        return result;
-    }
-
-    @Async
-    public CompletableFuture<List<String>> getRecordDatesForOne(String app, String stream, int year, int month, MediaServerItem mediaServerItem) {
-        JSONObject fileListJson = assistRESTfulUtils.getDateList(mediaServerItem, app, stream, year, month);
-        if (fileListJson != null && !fileListJson.isEmpty()) {
-            if (fileListJson.getString("code") != null && fileListJson.getInteger("code") == 0) {
-                JSONArray data = fileListJson.getJSONArray("data");
-                return CompletableFuture.completedFuture(data.toJavaList(String.class));
-            }
-        }
-        return CompletableFuture.completedFuture(new ArrayList<>());
-    }
-
-    @Async
-    public CompletableFuture<List<RecordFile>> getRecordFilesForOne(String app, String stream, String startTime, String endTime, MediaServerItem mediaServerItem) {
-        JSONObject fileListJson = assistRESTfulUtils.getFileList(mediaServerItem, 1, 100000000, app, stream, startTime, endTime);
-        if (fileListJson != null && !fileListJson.isEmpty()) {
-            if (fileListJson.getString("code") != null && fileListJson.getInteger("code") == 0) {
-                JSONObject data = fileListJson.getJSONObject("data");
-                JSONArray list = data.getJSONArray("list");
-                if (list != null) {
-                    return CompletableFuture.completedFuture(list.toJavaList(RecordFile.class));
-                }
-            }
-        }
-        return CompletableFuture.completedFuture(new ArrayList<>());
+    public List<MediaServerItem> getAllWithAssistPort() {
+        return mediaServerMapper.queryAllWithAssistPort();
     }
 }

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

@@ -64,7 +64,7 @@ public class MediaServiceImpl implements IMediaService {
                 if (data == null) {
                     return null;
                 }
-                JSONObject mediaJSON = JSON.parseObject(JSON.toJSONString(data.get(0)), JSONObject.class);
+                JSONObject mediaJSON = data.getJSONObject(0);
                 JSONArray tracks = mediaJSON.getJSONArray("tracks");
                 if (authority) {
                     streamInfo = getStreamInfoByAppAndStream(mediaInfo, app, stream, tracks, addr, calld, true);

+ 12 - 4
src/main/java/com/genersoft/iot/vmp/service/impl/PlatformServiceImpl.java

@@ -169,7 +169,7 @@ public class PlatformServiceImpl implements IPlatformService {
         dynamicTask.stop(registerTaskKey);
         // 注销旧的
         try {
-            if (parentPlatformOld.isStatus()) {
+            if (parentPlatformOld.isStatus() && parentPlatformCatchOld != null) {
                 logger.info("保存平台{}时发现旧平台在线,发送注销命令", parentPlatformOld.getServerGBId());
                 commanderForPlatform.unregister(parentPlatformOld, parentPlatformCatchOld.getSipTransactionInfo(), null, eventResult -> {
                     logger.info("[国标级联] 注销成功, 平台:{}", parentPlatformOld.getServerGBId());
@@ -286,6 +286,7 @@ public class PlatformServiceImpl implements IPlatformService {
         }
         if (parentPlatform.isAutoPushChannel()) {
             if (subscribeHolder.getCatalogSubscribe(parentPlatform.getServerGBId()) == null) {
+                logger.info("[国标级联]:{}, 添加自动通道推送模拟订阅信息", parentPlatform.getServerGBId());
                 addSimulatedSubscribeInfo(parentPlatform);
             }
         }else {
@@ -363,9 +364,16 @@ public class PlatformServiceImpl implements IPlatformService {
             // 清除心跳任务
             dynamicTask.stop(keepaliveTaskKey);
         }
-        // 停止目录订阅回复
-        logger.info("[平台离线] {}, 停止订阅回复", parentPlatform.getServerGBId());
-        subscribeHolder.removeAllSubscribe(parentPlatform.getServerGBId());
+        // 停止订阅回复
+        SubscribeInfo catalogSubscribe = subscribeHolder.getCatalogSubscribe(parentPlatform.getServerGBId());
+        if (catalogSubscribe != null) {
+            if (catalogSubscribe.getExpires() > 0) {
+                logger.info("[平台离线] {}, 停止目录订阅回复", parentPlatform.getServerGBId());
+                subscribeHolder.removeCatalogSubscribe(parentPlatform.getServerGBId());
+            }
+        }
+        logger.info("[平台离线] {}, 停止移动位置订阅回复", parentPlatform.getServerGBId());
+        subscribeHolder.removeMobilePositionSubscribe(parentPlatform.getServerGBId());
         // 发起定时自动重新注册
         if (!stopRegister) {
             // 设置为60秒自动尝试重新注册

+ 134 - 59
src/main/java/com/genersoft/iot/vmp/service/impl/PlayServiceImpl.java

@@ -1,5 +1,6 @@
 package com.genersoft.iot.vmp.service.impl;
 
+import com.alibaba.fastjson2.JSONArray;
 import com.alibaba.fastjson2.JSONObject;
 import com.genersoft.iot.vmp.common.InviteInfo;
 import com.genersoft.iot.vmp.common.InviteSessionStatus;
@@ -19,13 +20,19 @@ import com.genersoft.iot.vmp.gb28181.session.VideoStreamSessionManager;
 import com.genersoft.iot.vmp.gb28181.transmit.cmd.ISIPCommanderForPlatform;
 import com.genersoft.iot.vmp.gb28181.transmit.cmd.impl.SIPCommander;
 import com.genersoft.iot.vmp.gb28181.utils.SipUtils;
+import com.genersoft.iot.vmp.media.zlm.ZLMRESTfulUtils;
+import com.genersoft.iot.vmp.media.zlm.ZLMServerFactory;
+import com.genersoft.iot.vmp.media.zlm.ZlmHttpHookSubscribe;
+import com.genersoft.iot.vmp.media.zlm.dto.*;
 import com.genersoft.iot.vmp.media.zlm.*;
 import com.genersoft.iot.vmp.media.zlm.dto.HookSubscribeFactory;
 import com.genersoft.iot.vmp.media.zlm.dto.HookSubscribeForStreamChange;
 import com.genersoft.iot.vmp.media.zlm.dto.MediaServerItem;
 import com.genersoft.iot.vmp.media.zlm.dto.hook.HookParam;
+import com.genersoft.iot.vmp.media.zlm.dto.hook.OnRecordMp4HookParam;
 import com.genersoft.iot.vmp.media.zlm.dto.hook.OnStreamChangedHookParam;
 import com.genersoft.iot.vmp.service.*;
+import com.genersoft.iot.vmp.service.bean.*;
 import com.genersoft.iot.vmp.service.bean.ErrorCallback;
 import com.genersoft.iot.vmp.service.bean.InviteErrorCode;
 import com.genersoft.iot.vmp.service.bean.RequestPushStreamMsg;
@@ -33,6 +40,8 @@ import com.genersoft.iot.vmp.service.bean.SSRCInfo;
 import com.genersoft.iot.vmp.service.redisMsg.RedisGbPlayMsgListener;
 import com.genersoft.iot.vmp.storager.IRedisCatchStorage;
 import com.genersoft.iot.vmp.storager.IVideoManagerStorage;
+import com.genersoft.iot.vmp.storager.dao.CloudRecordServiceMapper;
+import com.genersoft.iot.vmp.utils.CloudRecordUtils;
 import com.genersoft.iot.vmp.utils.DateUtil;
 import com.genersoft.iot.vmp.vmanager.bean.AudioBroadcastResult;
 import com.genersoft.iot.vmp.vmanager.bean.ErrorCode;
@@ -89,12 +98,18 @@ public class PlayServiceImpl implements IPlayService {
     @Autowired
     private IInviteStreamService inviteStreamService;
 
+    @Autowired
+    private ZlmHttpHookSubscribe subscribe;
+
     @Autowired
     private SendRtpPortManager sendRtpPortManager;
 
     @Autowired
     private ZLMRESTfulUtils zlmresTfulUtils;
 
+    @Autowired
+    private ZLMServerFactory zlmServerFactory;
+
     @Autowired
     private AssistRESTfulUtils assistRESTfulUtils;
 
@@ -117,7 +132,7 @@ public class PlayServiceImpl implements IPlayService {
     private DynamicTask dynamicTask;
 
     @Autowired
-    private ZlmHttpHookSubscribe subscribe;
+    private CloudRecordServiceMapper cloudRecordServiceMapper;
 
     @Autowired
     private ISIPCommanderForPlatform commanderForPlatform;
@@ -407,6 +422,15 @@ public class PlayServiceImpl implements IPlayService {
                     HookSubscribeForStreamChange hookSubscribe = HookSubscribeFactory.on_stream_changed("rtp", ssrcInfo.getStream(), true, "rtsp", mediaServerItem.getId());
                     subscribe.removeSubscribe(hookSubscribe);
                 }
+            }else {
+                logger.info("[点播超时] 收流超时 deviceId: {}, channelId: {},码流类型:{},端口:{}, SSRC: {}",
+                        device.getDeviceId(), channelId, device.isSwitchPrimarySubStream() ? "辅码流" : "主码流",
+                        ssrcInfo.getPort(), ssrcInfo.getSsrc());
+
+                mediaServerService.releaseSsrc(mediaServerItem.getId(), ssrcInfo.getSsrc());
+
+                mediaServerService.closeRTPServer(mediaServerItem.getId(), ssrcInfo.getStream());
+                streamSession.remove(device.getDeviceId(), channelId, ssrcInfo.getStream());
             }
         }, userSetting.getPlayTimeout());
 
@@ -437,6 +461,7 @@ public class PlayServiceImpl implements IPlayService {
                 InviteOKHandler(eventResult, ssrcInfo, mediaServerItem, device, channelId,
                         timeOutTaskKey, callback, inviteInfo, InviteSessionType.PLAY);
             }, (event) -> {
+                logger.info("[点播失败] deviceId: {}, channelId:{}, {}: {}", device.getDeviceId(), channelId, event.statusCode, event.msg);
                 dynamicTask.stop(timeOutTaskKey);
                 mediaServerService.closeRTPServer(mediaServerItem, ssrcInfo.getStream());
                 // 释放ssrc
@@ -478,7 +503,13 @@ public class PlayServiceImpl implements IPlayService {
         if (!device.getStreamMode().equalsIgnoreCase("TCP-ACTIVE")) {
             return;
         }
-        String substring = contentString.substring(0, contentString.indexOf("y="));
+
+        String substring;
+        if (contentString.indexOf("y=") > 0) {
+            substring = contentString.substring(0, contentString.indexOf("y="));
+        }else {
+            substring = contentString;
+        }
         try {
             SessionDescription sdp = SdpFactory.getInstance().createSessionDescription(substring);
             int port = -1;
@@ -597,23 +628,6 @@ public class PlayServiceImpl implements IPlayService {
         return mediaServerItem;
     }
 
-    @Override
-    public MediaServerItem getNewMediaServerItemHasAssist(Device device) {
-        if (device == null) {
-            return null;
-        }
-        MediaServerItem mediaServerItem;
-        if (ObjectUtils.isEmpty(device.getMediaServerId()) || "auto".equals(device.getMediaServerId())) {
-            mediaServerItem = mediaServerService.getMediaServerForMinimumLoad(true);
-        } else {
-            mediaServerItem = mediaServerService.getOne(device.getMediaServerId());
-        }
-        if (mediaServerItem == null) {
-            logger.warn("[获取可用的ZLM节点]未找到可使用的ZLM...");
-        }
-        return mediaServerItem;
-    }
-
     @Override
     public void playBack(String deviceId, String channelId, String startTime,
                                                           String endTime, ErrorCallback<Object> callback) {
@@ -711,7 +725,6 @@ public class PlayServiceImpl implements IPlayService {
                         // 处理收到200ok后的TCP主动连接以及SSRC不一致的问题
                         InviteOKHandler(eventResult, ssrcInfo, mediaServerItem, device, channelId,
                                 playBackTimeOutTaskKey, callback, inviteInfo, InviteSessionType.PLAYBACK);
-
                     }, errorEvent);
         } catch (InvalidArgumentException | SipException | ParseException e) {
             logger.error("[命令发送失败] 录像回放: {}", e.getMessage());
@@ -732,6 +745,10 @@ public class PlayServiceImpl implements IPlayService {
         ResponseEvent responseEvent = (ResponseEvent) eventResult.event;
         String contentString = new String(responseEvent.getResponse().getRawContent());
         String ssrcInResponse = SipUtils.getSsrcFromSdp(contentString);
+        // 兼容回复的消息中缺少ssrc(y字段)的情况
+        if (ssrcInResponse == null) {
+            ssrcInResponse = ssrcInfo.getSsrc();
+        }
         if (ssrcInfo.getSsrc().equals(ssrcInResponse)) {
             // ssrc 一致
             if (mediaServerItem.isRtpEnable()) {
@@ -809,13 +826,15 @@ public class PlayServiceImpl implements IPlayService {
     }
 
 
+
+
     @Override
     public void download(String deviceId, String channelId, String startTime, String endTime, int downloadSpeed, ErrorCallback<Object> callback) {
         Device device = storager.queryVideoDevice(deviceId);
         if (device == null) {
             return;
         }
-        MediaServerItem newMediaServerItem = getNewMediaServerItemHasAssist(device);
+        MediaServerItem newMediaServerItem = this.getNewMediaServerItem(device);
         if (newMediaServerItem == null) {
             callback.run(InviteErrorCode.ERROR_FOR_ASSIST_NOT_READY.getCode(),
                     InviteErrorCode.ERROR_FOR_ASSIST_NOT_READY.getMsg(),
@@ -894,6 +913,28 @@ public class PlayServiceImpl implements IPlayService {
                         // 处理收到200ok后的TCP主动连接以及SSRC不一致的问题
                         InviteOKHandler(eventResult, ssrcInfo, mediaServerItem, device, channelId,
                                 downLoadTimeOutTaskKey, callback, inviteInfo, InviteSessionType.DOWNLOAD);
+
+                        // 注册录像回调事件,录像下载结束后写入下载地址
+                        ZlmHttpHookSubscribe.Event hookEventForRecord = (mediaServerItemInuse, hookParam) -> {
+                            logger.info("[录像下载] 收到录像写入磁盘消息: , {}/{}-{}",
+                                    inviteInfo.getDeviceId(), inviteInfo.getChannelId(), ssrcInfo.getStream());
+                            logger.info("[录像下载] 收到录像写入磁盘消息内容: " + hookParam);
+                            OnRecordMp4HookParam recordMp4HookParam = (OnRecordMp4HookParam)hookParam;
+                            String filePath = recordMp4HookParam.getFile_path();
+                            DownloadFileInfo downloadFileInfo = CloudRecordUtils.getDownloadFilePath(mediaServerItem, filePath);
+                            InviteInfo inviteInfoForNew = inviteStreamService.getInviteInfo(inviteInfo.getType(), inviteInfo.getDeviceId()
+                                    , inviteInfo.getChannelId(), inviteInfo.getStream());
+                            inviteInfoForNew.getStreamInfo().setDownLoadFilePath(downloadFileInfo);
+                            inviteStreamService.updateInviteInfo(inviteInfoForNew);
+                        };
+                        HookSubscribeForRecordMp4 hookSubscribe = HookSubscribeFactory.on_record_mp4(
+                                mediaServerItem.getId(), "rtp", ssrcInfo.getStream());
+
+                        // 设置过期时间,下载失败时自动处理订阅数据
+//                        long difference = DateUtil.getDifference(startTime, endTime)/1000;
+//                        Instant expiresInstant = Instant.now().plusSeconds(TimeUnit.MINUTES.toSeconds(difference * 2));
+//                        hookSubscribe.setExpires(expiresInstant);
+                        subscribe.addSubscribe(hookSubscribe, hookEventForRecord);
                     });
         } catch (InvalidArgumentException | SipException | ParseException e) {
             logger.error("[命令发送失败] 录像下载: {}", e.getMessage());
@@ -909,47 +950,71 @@ public class PlayServiceImpl implements IPlayService {
     @Override
     public StreamInfo getDownLoadInfo(String deviceId, String channelId, String stream) {
         InviteInfo inviteInfo = inviteStreamService.getInviteInfo(InviteSessionType.DOWNLOAD, deviceId, channelId, stream);
+        if (inviteInfo == null || inviteInfo.getStreamInfo() == null) {
+            logger.warn("[获取下载进度] 未查询到录像下载的信息");
+            return null;
+        }
 
-        if (inviteInfo != null && inviteInfo.getStreamInfo() != null) {
-            if (inviteInfo.getStreamInfo().getProgress() == 1) {
-                return inviteInfo.getStreamInfo();
-            }
+        if (inviteInfo.getStreamInfo().getProgress() == 1) {
+            return inviteInfo.getStreamInfo();
+        }
 
-            // 获取当前已下载时长
-            String mediaServerId = inviteInfo.getStreamInfo().getMediaServerId();
-            MediaServerItem mediaServerItem = mediaServerService.getOne(mediaServerId);
-            if (mediaServerItem == null) {
-                logger.warn("查询录像信息时发现节点已离线");
-                return null;
-            }
-            if (mediaServerItem.getRecordAssistPort() > 0) {
-                JSONObject jsonObject = assistRESTfulUtils.fileDuration(mediaServerItem, inviteInfo.getStreamInfo().getApp(), inviteInfo.getStreamInfo().getStream(), null);
-                if (jsonObject == null) {
-                    throw new ControllerException(ErrorCode.ERROR100.getCode(), "连接Assist服务失败");
-                }
-                if (jsonObject.getInteger("code") == 0) {
-                    long duration = jsonObject.getLong("data");
+        // 获取当前已下载时长
+        String mediaServerId = inviteInfo.getStreamInfo().getMediaServerId();
+        MediaServerItem mediaServerItem = mediaServerService.getOne(mediaServerId);
+        if (mediaServerItem == null) {
+            logger.warn("[获取下载进度] 查询录像信息时发现节点不存在");
+            return null;
+        }
+        SsrcTransaction ssrcTransaction = streamSession.getSsrcTransaction(deviceId, channelId, null, stream);
 
-                    if (duration == 0) {
-                        inviteInfo.getStreamInfo().setProgress(0);
-                    } else {
-                        String startTime = inviteInfo.getStreamInfo().getStartTime();
-                        String endTime = inviteInfo.getStreamInfo().getEndTime();
-                        long start = DateUtil.yyyy_MM_dd_HH_mm_ssToTimestamp(startTime);
-                        long end = DateUtil.yyyy_MM_dd_HH_mm_ssToTimestamp(endTime);
-
-                        BigDecimal currentCount = new BigDecimal(duration / 1000);
-                        BigDecimal totalCount = new BigDecimal(end - start);
-                        BigDecimal divide = currentCount.divide(totalCount, 2, RoundingMode.HALF_UP);
-                        double process = divide.doubleValue();
-                        inviteInfo.getStreamInfo().setProgress(process);
-                    }
-                    inviteStreamService.updateInviteInfo(inviteInfo);
-                }
+        if (ssrcTransaction == null) {
+            logger.warn("[获取下载进度] 下载已结束");
+            return null;
+        }
+
+        JSONObject mediaListJson= zlmresTfulUtils.getMediaList(mediaServerItem, "rtp", stream);
+        if (mediaListJson == null) {
+            logger.warn("[获取下载进度] 从zlm查询进度失败");
+            return null;
+        }
+        if (mediaListJson.getInteger("code") != 0) {
+            logger.warn("[获取下载进度] 从zlm查询进度出现错误: {}", mediaListJson.getString("msg"));
+            return null;
+        }
+        JSONArray data = mediaListJson.getJSONArray("data");
+        if (data == null) {
+            logger.warn("[获取下载进度] 从zlm查询进度时未返回数据");
+            return null;
+        }
+        JSONObject mediaJSON = data.getJSONObject(0);
+        JSONArray tracks = mediaJSON.getJSONArray("tracks");
+        if (tracks.isEmpty()) {
+            logger.warn("[获取下载进度] 从zlm查询进度时未返回数据");
+            return null;
+        }
+        JSONObject jsonObject = tracks.getJSONObject(0);
+        long duration = jsonObject.getLongValue("duration");
+        if (duration == 0) {
+            inviteInfo.getStreamInfo().setProgress(0);
+        } else {
+            String startTime = inviteInfo.getStreamInfo().getStartTime();
+            String endTime = inviteInfo.getStreamInfo().getEndTime();
+            // 此时start和end单位是秒
+            long start = DateUtil.yyyy_MM_dd_HH_mm_ssToTimestamp(startTime);
+            long end = DateUtil.yyyy_MM_dd_HH_mm_ssToTimestamp(endTime);
+
+            BigDecimal currentCount = new BigDecimal(duration);
+            BigDecimal totalCount = new BigDecimal((end - start) * 1000);
+            BigDecimal divide = currentCount.divide(totalCount, 2, RoundingMode.HALF_UP);
+            double process = divide.doubleValue();
+            if (process > 0.999) {
+                process = 1.0;
             }
-            return inviteInfo.getStreamInfo();
+            inviteInfo.getStreamInfo().setProgress(process);
         }
-        return null;
+        inviteStreamService.updateInviteInfo(inviteInfo);
+        return inviteInfo.getStreamInfo();
     }
 
     private StreamInfo onPublishHandlerForDownload(MediaServerItem mediaServerItemInuse, HookParam hookParam, String deviceId, String channelId, String startTime, String endTime) {
@@ -1219,7 +1284,12 @@ public class PlayServiceImpl implements IPlayService {
             throw new ServiceException("mediaServer不存在");
         }
         // zlm 暂停RTP超时检查
-        JSONObject jsonObject = zlmresTfulUtils.pauseRtpCheck(mediaServerItem, streamId);
+        // 使用zlm中的流ID
+        String streamKey = inviteInfo.getStream();
+        if (!mediaServerItem.isRtpEnable()) {
+            streamKey = Long.toHexString(Long.parseLong(inviteInfo.getSsrcInfo().getSsrc())).toUpperCase();
+        }
+        JSONObject jsonObject = zlmresTfulUtils.pauseRtpCheck(mediaServerItem, streamKey);
         if (jsonObject == null || jsonObject.getInteger("code") != 0) {
             throw new ServiceException("暂停RTP接收失败");
         }
@@ -1242,7 +1312,12 @@ public class PlayServiceImpl implements IPlayService {
             throw new ServiceException("mediaServer不存在");
         }
         // zlm 暂停RTP超时检查
-        JSONObject jsonObject = zlmresTfulUtils.resumeRtpCheck(mediaServerItem, streamId);
+        // 使用zlm中的流ID
+        String streamKey = inviteInfo.getStream();
+        if (!mediaServerItem.isRtpEnable()) {
+            streamKey = Long.toHexString(Long.parseLong(inviteInfo.getSsrcInfo().getSsrc())).toUpperCase();
+        }
+        JSONObject jsonObject = zlmresTfulUtils.resumeRtpCheck(mediaServerItem, streamKey);
         if (jsonObject == null || jsonObject.getInteger("code") != 0) {
             throw new ServiceException("继续RTP接收失败");
         }

+ 9 - 1
src/main/java/com/genersoft/iot/vmp/service/impl/StreamProxyServiceImpl.java

@@ -126,7 +126,13 @@ public class StreamProxyServiceImpl implements IStreamProxyService {
             }
             JSONArray dataArray = jsonObject.getJSONArray("data");
             JSONObject mediaServerConfig = dataArray.getJSONObject(0);
+            if (ObjectUtils.isEmpty(param.getFfmpegCmdKey())) {
+                param.setFfmpegCmdKey("ffmpeg.cmd");
+            }
             String ffmpegCmd = mediaServerConfig.getString(param.getFfmpegCmdKey());
+            if (ffmpegCmd == null) {
+                throw new ControllerException(ErrorCode.ERROR100.getCode(), "ffmpeg拉流代理无法获取ffmpeg cmd");
+            }
             String schema = getSchemaFromFFmpegCmd(ffmpegCmd);
             if (schema == null) {
                 throw new ControllerException(ErrorCode.ERROR100.getCode(), "ffmpeg拉流代理无法从ffmpeg cmd中获取到输出格式");
@@ -401,6 +407,8 @@ public class StreamProxyServiceImpl implements IStreamProxyService {
                 logger.info("启用代理失败: {}/{}->{}({})", app, stream, jsonObject.getString("msg"),
                         streamProxy.getSrcUrl() == null? streamProxy.getUrl():streamProxy.getSrcUrl());
             }
+        } else if (streamProxy != null && streamProxy.isEnable()) {
+           return true ;
         }
         return result;
     }
@@ -452,7 +460,7 @@ public class StreamProxyServiceImpl implements IStreamProxyService {
         streamProxyMapper.deleteAutoRemoveItemByMediaServerId(mediaServerId);
 
         // 移除拉流代理生成的流信息
-//        syncPullStream(mediaServerId);
+        syncPullStream(mediaServerId);
 
         // 恢复流代理, 只查找这个这个流媒体
         List<StreamProxyItem> streamProxyListForEnable = storager.getStreamProxyListForEnableInMediaServer(

+ 5 - 0
src/main/java/com/genersoft/iot/vmp/service/impl/StreamPushServiceImpl.java

@@ -282,6 +282,8 @@ public class StreamPushServiceImpl implements IStreamPushService {
                     redisCatchStorage.sendStreamChangeMsg(type, jsonObject);
                     // 移除redis内流的信息
                     redisCatchStorage.removeStream(mediaServerItem.getId(), "PUSH", offlineOnStreamChangedHookParam.getApp(), offlineOnStreamChangedHookParam.getStream());
+                    // 冗余数据,自己系统中自用
+                    redisCatchStorage.removePushListItem(offlineOnStreamChangedHookParam.getApp(), offlineOnStreamChangedHookParam.getStream(), mediaServerItem.getId());
                 }
             }
 
@@ -319,6 +321,9 @@ public class StreamPushServiceImpl implements IStreamPushService {
                 jsonObject.put("register", false);
                 jsonObject.put("mediaServerId", mediaServerId);
                 redisCatchStorage.sendStreamChangeMsg(type, jsonObject);
+
+                // 冗余数据,自己系统中自用
+                redisCatchStorage.removePushListItem(onStreamChangedHookParam.getApp(), onStreamChangedHookParam.getStream(), mediaServerId);
             }
         }
     }

+ 9 - 9
src/main/java/com/genersoft/iot/vmp/service/redisMsg/RedisGbPlayMsgListener.java

@@ -113,8 +113,8 @@ public class RedisGbPlayMsgListener implements MessageListener {
                 while (!taskQueue.isEmpty()) {
                     Message msg = taskQueue.poll();
                     try {
-                        JSONObject msgJSON = JSON.parseObject(msg.getBody(), JSONObject.class);
-                        WvpRedisMsg wvpRedisMsg = JSON.to(WvpRedisMsg.class, msgJSON);
+                        WvpRedisMsg wvpRedisMsg = JSON.parseObject(msg.getBody(), WvpRedisMsg.class);
+                        logger.info("[收到REDIS通知] 消息: {}", JSON.toJSONString(wvpRedisMsg));
                         if (!userSetting.getServerId().equals(wvpRedisMsg.getToId())) {
                             continue;
                         }
@@ -123,7 +123,7 @@ public class RedisGbPlayMsgListener implements MessageListener {
 
                             switch (wvpRedisMsg.getCmd()){
                                 case WvpRedisMsgCmd.GET_SEND_ITEM:
-                                    RequestSendItemMsg content = JSON.to(RequestSendItemMsg.class, wvpRedisMsg.getContent());
+                                    RequestSendItemMsg content = JSON.parseObject(wvpRedisMsg.getContent(), RequestSendItemMsg.class);
                                     requestSendItemMsgHand(content, wvpRedisMsg.getFromId(), wvpRedisMsg.getSerial());
                                     break;
                                 case WvpRedisMsgCmd.REQUEST_PUSH_STREAM:
@@ -242,7 +242,7 @@ public class RedisGbPlayMsgListener implements MessageListener {
         result.setData(content);
 
         WvpRedisMsg response = WvpRedisMsg.getResponseInstance(userSetting.getServerId(), toId,
-                WvpRedisMsgCmd.REQUEST_PUSH_STREAM, serial, result);
+                WvpRedisMsgCmd.REQUEST_PUSH_STREAM, serial, JSON.toJSONString(result));
         JSONObject jsonObject = (JSONObject)JSON.toJSON(response);
         redisTemplate.convertAndSend(WVP_PUSH_STREAM_KEY, jsonObject);
     }
@@ -260,7 +260,7 @@ public class RedisGbPlayMsgListener implements MessageListener {
             result.setMsg("流媒体不存在");
 
             WvpRedisMsg response = WvpRedisMsg.getResponseInstance(userSetting.getServerId(), toId,
-                    WvpRedisMsgCmd.GET_SEND_ITEM, serial, result);
+                    WvpRedisMsgCmd.GET_SEND_ITEM, serial, JSON.toJSONString(result));
 
             JSONObject jsonObject = (JSONObject)JSON.toJSON(response);
             redisTemplate.convertAndSend(WVP_PUSH_STREAM_KEY, jsonObject);
@@ -283,7 +283,7 @@ public class RedisGbPlayMsgListener implements MessageListener {
                 WVPResult<SendRtpItem> result = new WVPResult<>();
                 result.setCode(ERROR_CODE_TIMEOUT);
                 WvpRedisMsg response = WvpRedisMsg.getResponseInstance(
-                        userSetting.getServerId(), toId, WvpRedisMsgCmd.GET_SEND_ITEM, serial, result
+                        userSetting.getServerId(), toId, WvpRedisMsgCmd.GET_SEND_ITEM, serial, JSON.toJSONString(result)
                 );
                 JSONObject jsonObject = (JSONObject)JSON.toJSON(response);
                 redisTemplate.convertAndSend(WVP_PUSH_STREAM_KEY, jsonObject);
@@ -324,7 +324,7 @@ public class RedisGbPlayMsgListener implements MessageListener {
         result.setData(responseSendItemMsg);
 
         WvpRedisMsg response = WvpRedisMsg.getResponseInstance(
-                userSetting.getServerId(), toId, WvpRedisMsgCmd.GET_SEND_ITEM, serial, result
+                userSetting.getServerId(), toId, WvpRedisMsgCmd.GET_SEND_ITEM, serial, JSON.toJSONString(result)
         );
         JSONObject jsonObject = (JSONObject)JSON.toJSON(response);
         redisTemplate.convertAndSend(WVP_PUSH_STREAM_KEY, jsonObject);
@@ -350,7 +350,7 @@ public class RedisGbPlayMsgListener implements MessageListener {
         requestSendItemMsg.setServerId(serverId);
         String key = UUID.randomUUID().toString();
         WvpRedisMsg redisMsg = WvpRedisMsg.getRequestInstance(userSetting.getServerId(), serverId, WvpRedisMsgCmd.GET_SEND_ITEM,
-                key, requestSendItemMsg);
+                key, JSON.toJSONString(requestSendItemMsg));
 
         JSONObject jsonObject = (JSONObject)JSON.toJSON(redisMsg);
         logger.info("[请求推流SendItem] {}: {}", serverId, jsonObject);
@@ -375,7 +375,7 @@ public class RedisGbPlayMsgListener implements MessageListener {
     public void sendMsgForStartSendRtpStream(String serverId, RequestPushStreamMsg param, PlayMsgCallbackForStartSendRtpStream callback) {
         String key = UUID.randomUUID().toString();
         WvpRedisMsg redisMsg = WvpRedisMsg.getRequestInstance(userSetting.getServerId(), serverId,
-                WvpRedisMsgCmd.REQUEST_PUSH_STREAM, key, param);
+                WvpRedisMsgCmd.REQUEST_PUSH_STREAM, key, JSON.toJSONString(param));
 
         JSONObject jsonObject = (JSONObject)JSON.toJSON(redisMsg);
         logger.info("[REDIS 请求其他平台推流] {}: {}", serverId, jsonObject);

+ 3 - 2
src/main/java/com/genersoft/iot/vmp/service/redisMsg/RedisGpsMsgListener.java

@@ -50,11 +50,12 @@ public class RedisGpsMsgListener implements MessageListener {
                     Message msg = taskQueue.poll();
                     try {
                         GPSMsgInfo gpsMsgInfo = JSON.parseObject(msg.getBody(), GPSMsgInfo.class);
+                        logger.info("[REDIS的位置变化通知], {}", JSON.toJSONString(gpsMsgInfo));
                         // 只是放入redis缓存起来
                         redisCatchStorage.updateGpsMsgInfo(gpsMsgInfo);
                     }catch (Exception e) {
-                        logger.warn("[REDIS的ALARM通知] 发现未处理的异常, \r\n{}", JSON.toJSONString(message));
-                        logger.error("[REDIS的ALARM通知] 异常内容: ", e);
+                        logger.warn("[REDIS的位置变化通知] 发现未处理的异常, \r\n{}", JSON.toJSONString(message));
+                        logger.error("[REDIS的位置变化通知] 异常内容: ", e);
                     }
                 }
             });

+ 4 - 0
src/main/java/com/genersoft/iot/vmp/storager/IRedisCatchStorage.java

@@ -208,4 +208,8 @@ public interface IRedisCatchStorage {
     void sendPlatformStartPlayMsg(MessageForPushChannel messageForPushChannel);
 
     void sendPlatformStopPlayMsg(MessageForPushChannel messageForPushChannel);
+
+    void addPushListItem(String app, String stream, OnStreamChangedHookParam param);
+
+    void removePushListItem(String app, String stream, String mediaServerId);
 }

+ 122 - 0
src/main/java/com/genersoft/iot/vmp/storager/dao/CloudRecordServiceMapper.java

@@ -0,0 +1,122 @@
+package com.genersoft.iot.vmp.storager.dao;
+
+import com.genersoft.iot.vmp.media.zlm.dto.MediaServerItem;
+import com.genersoft.iot.vmp.service.bean.CloudRecordItem;
+import org.apache.ibatis.annotations.*;
+
+import java.util.List;
+
+@Mapper
+public interface CloudRecordServiceMapper {
+
+    @Insert(" <script>" +
+            "INSERT INTO wvp_cloud_record (" +
+            " app," +
+            " stream," +
+            "<if test=\"callId != null\"> call_id,</if>" +
+            " start_time," +
+            " end_time," +
+            " media_server_id," +
+            " file_name," +
+            " folder," +
+            " file_path," +
+            " file_size," +
+            " time_len ) " +
+            "VALUES (" +
+            " #{app}," +
+            " #{stream}," +
+            " <if test=\"callId != null\"> #{callId},</if>" +
+            " #{startTime}," +
+            " #{endTime}," +
+            " #{mediaServerId}," +
+            " #{fileName}," +
+            " #{folder}," +
+            " #{filePath}," +
+            " #{fileSize}," +
+            " #{timeLen})" +
+            " </script>")
+    int add(CloudRecordItem cloudRecordItem);
+
+    @Select(" <script>" +
+            "select * " +
+            " from wvp_cloud_record " +
+            " where 0 = 0" +
+            " <if test='query != null'> AND (app LIKE concat('%',#{query},'%') OR stream LIKE concat('%',#{query},'%') )</if> " +
+            " <if test= 'app != null '> and app=#{app}</if>" +
+            " <if test= 'stream != null '> and stream=#{stream}</if>" +
+            " <if test= 'startTimeStamp != null '> and end_time &gt;= #{startTimeStamp}</if>" +
+            " <if test= 'endTimeStamp != null '> and start_time &lt;= #{endTimeStamp}</if>" +
+            " <if test= 'callId != null '> and call_id = #{callId}</if>" +
+            " <if test= 'mediaServerItemList != null  ' > and media_server_id in " +
+            " <foreach collection='mediaServerItemList'  item='item'  open='(' separator=',' close=')' > #{item.id}</foreach>" +
+            " </if>" +
+            " order by start_time DESC" +
+
+            " </script>")
+    List<CloudRecordItem> getList(@Param("query") String query, @Param("app") String app, @Param("stream") String stream,
+                                  @Param("startTimeStamp")Long startTimeStamp, @Param("endTimeStamp")Long endTimeStamp,
+                                  @Param("callId")String callId, List<MediaServerItem> mediaServerItemList);
+
+
+    @Select(" <script>" +
+            "select file_path" +
+            " from wvp_cloud_record " +
+            " where 0 = 0" +
+            " <if test= 'app != null '> and app=#{app}</if>" +
+            " <if test= 'stream != null '> and stream=#{stream}</if>" +
+            " <if test= 'startTimeStamp != null '> and end_time &gt;= #{startTimeStamp}</if>" +
+            " <if test= 'endTimeStamp != null '> and start_time &lt;= #{endTimeStamp}</if>" +
+            " <if test= 'callId != null '> and call_id = #{callId}</if>" +
+            " <if test= 'mediaServerItemList != null  ' > and media_server_id in " +
+            " <foreach collection='mediaServerItemList'  item='item'  open='(' separator=',' close=')' > #{item.id}</foreach>" +
+            " </if>" +
+            " </script>")
+    List<String> queryRecordFilePathList(@Param("app") String app, @Param("stream") String stream,
+                                  @Param("startTimeStamp")Long startTimeStamp, @Param("endTimeStamp")Long endTimeStamp,
+                                  @Param("callId")String callId, List<MediaServerItem> mediaServerItemList);
+
+    @Update(" <script>" +
+            "update wvp_cloud_record set collect = #{collect} where file_path in " +
+            " <foreach collection='cloudRecordItemList'  item='item'  open='(' separator=',' close=')' > #{item.filePath}</foreach>" +
+            " </script>")
+    int updateCollectList(@Param("collect") boolean collect, List<CloudRecordItem> cloudRecordItemList);
+
+    @Delete(" <script>" +
+            "delete from wvp_cloud_record where media_server_id=#{mediaServerId} and file_path in " +
+            " <foreach collection='filePathList'  item='item'  open='(' separator=',' close=')' > #{item}</foreach>" +
+            " </script>")
+    void deleteByFileList(List<String> filePathList, @Param("mediaServerId") String mediaServerId);
+
+
+    @Select(" <script>" +
+            "select *" +
+            " from wvp_cloud_record " +
+            " where collect = false and end_time &lt;= #{endTimeStamp} and media_server_id  = #{mediaServerId} " +
+            " </script>")
+    List<CloudRecordItem> queryRecordListForDelete(@Param("endTimeStamp")Long endTimeStamp, String mediaServerId);
+
+    @Update(" <script>" +
+            "update wvp_cloud_record set collect = #{collect} where id = #{recordId} " +
+            " </script>")
+    int changeCollectById(@Param("collect") boolean collect, @Param("recordId") Integer recordId);
+
+    @Delete(" <script>" +
+            "delete from wvp_cloud_record where id in " +
+            " <foreach collection='cloudRecordItemIdList'  item='item'  open='(' separator=',' close=')' > #{item.id}</foreach>" +
+            " </script>")
+    int deleteList(List<CloudRecordItem> cloudRecordItemIdList);
+
+    @Select(" <script>" +
+            "select *" +
+            " from wvp_cloud_record " +
+            "where call_id = #{callId}" +
+            " </script>")
+    List<CloudRecordItem> getListByCallId(@Param("callId") String callId);
+
+    @Select(" <script>" +
+            "select *" +
+            " from wvp_cloud_record " +
+            "where id = #{id}" +
+            " </script>")
+    CloudRecordItem queryOne(@Param("id") Integer id);
+}

+ 83 - 11
src/main/java/com/genersoft/iot/vmp/storager/dao/DeviceChannelMapper.java

@@ -6,7 +6,6 @@ import com.genersoft.iot.vmp.gb28181.bean.DeviceChannelInPlatform;
 import com.genersoft.iot.vmp.vmanager.gb28181.platform.bean.ChannelReduce;
 import com.genersoft.iot.vmp.web.gb28181.dto.DeviceChannelExtend;
 import org.apache.ibatis.annotations.*;
-import org.apache.ibatis.annotations.Param;
 import org.springframework.stereotype.Repository;
 
 import java.util.List;
@@ -31,7 +30,7 @@ public interface DeviceChannelMapper {
     @Update(value = {" <script>" +
             "UPDATE wvp_device_channel " +
             "SET update_time=#{updateTime}" +
-            "<if test='name != null'>, name=#{name}</if>" +
+            ", custom_name=#{name}" +
             "<if test='manufacture != null'>, manufacture=#{manufacture}</if>" +
             "<if test='model != null'>, model=#{model}</if>" +
             "<if test='owner != null'>, owner=#{owner}</if>" +
@@ -49,12 +48,12 @@ public interface DeviceChannelMapper {
             "<if test='ipAddress != null'>, ip_address=#{ipAddress}</if>" +
             "<if test='port != null'>, port=#{port}</if>" +
             "<if test='password != null'>, password=#{password}</if>" +
-            "<if test='PTZType != null'>, ptz_type=#{PTZType}</if>" +
+            "<if test='PTZType != null'>, custom_ptz_type=#{PTZType}</if>" +
             "<if test='status != null'>, status=#{status}</if>" +
             "<if test='streamId != null'>, stream_id=#{streamId}</if>" +
             "<if test='hasAudio != null'>, has_audio=#{hasAudio}</if>" +
-            "<if test='longitude != null'>, longitude=#{longitude}</if>" +
-            "<if test='latitude != null'>, latitude=#{latitude}</if>" +
+            ", custom_longitude=#{longitude}" +
+            ", custom_latitude=#{latitude}" +
             "<if test='longitudeGcj02 != null'>, longitude_gcj02=#{longitudeGcj02}</if>" +
             "<if test='latitudeGcj02 != null'>, latitude_gcj02=#{latitudeGcj02}</if>" +
             "<if test='longitudeWgs84 != null'>, longitude_wgs84=#{longitudeWgs84}</if>" +
@@ -67,7 +66,43 @@ public interface DeviceChannelMapper {
 
     @Select(value = {" <script>" +
             "SELECT " +
-            "dc.* " +
+            "dc.id, " +
+            "dc.channel_id, " +
+            "COALESCE(dc.custom_name, dc.name) AS name, " +
+            "dc.manufacture, " +
+            "dc.model, " +
+            "dc.owner, " +
+            "dc.civil_code, " +
+            "dc.block, " +
+            "dc.address, " +
+            "dc.parent_id, " +
+            "dc.safety_way, " +
+            "dc.register_way, " +
+            "dc.cert_num, " +
+            "dc.certifiable, " +
+            "dc.err_code, " +
+            "dc.end_time, " +
+            "dc.secrecy, " +
+            "dc.ip_address, " +
+            "dc.port, " +
+            "dc.password, " +
+            "COALESCE(dc.custom_ptz_type, dc.ptz_type) AS ptz_type, " +
+            "dc.status, " +
+            "COALESCE(dc.custom_longitude, dc.longitude) AS longitude, " +
+            "COALESCE(dc.custom_latitude, dc.latitude) AS latitude, " +
+            "dc.stream_id, " +
+            "dc.device_id, " +
+            "dc.parental, " +
+            "dc.has_audio, " +
+            "dc.create_time, " +
+            "dc.update_time, " +
+            "dc.sub_count, " +
+            "dc.longitude_gcj02, " +
+            "dc.latitude_gcj02, " +
+            "dc.longitude_wgs84, " +
+            "dc.latitude_wgs84, " +
+            "dc.business_group_id, " +
+            "dc.gps_time " +
             "from " +
             "wvp_device_channel dc " +
             "WHERE " +
@@ -154,7 +189,7 @@ public interface DeviceChannelMapper {
             "    dc.id,\n" +
             "    dc.channel_id,\n" +
             "    dc.device_id,\n" +
-            "    dc.name,\n" +
+            "    COALESCE(dc.custom_name, dc.name) AS name,\n" +
             "    de.manufacturer,\n" +
             "    de.host_address,\n" +
             "    dc.sub_count,\n" +
@@ -392,10 +427,10 @@ public interface DeviceChannelMapper {
     @Select("select * from wvp_device_channel where device_id=#{deviceId} and SUBSTRING(channel_id, 11, 3)=#{typeCode}")
     List<DeviceChannel> getBusinessGroups(@Param("deviceId") String deviceId, @Param("typeCode") String typeCode);
 
-    @Select("select dc.id, dc.channel_id, dc.device_id, dc.name, dc.manufacture,dc.model,dc.owner, pc.civil_code,dc.block, " +
+    @Select("select dc.id, dc.channel_id, dc.device_id, COALESCE(dc.custom_name, dc.name) AS name, dc.manufacture,dc.model,dc.owner, pc.civil_code,dc.block, " +
             " dc.address, '0' as parental,'0' as channel_type, pc.id as parent_id, dc.safety_way, dc.register_way,dc.cert_num, dc.certifiable,  " +
-            " dc.err_code,dc.end_time, dc.secrecy,   dc.ip_address,  dc.port,  dc.ptz_type,  dc.password, dc.status, " +
-            " dc.longitude_wgs84 as longitude, dc.latitude_wgs84 as latitude,  pc.business_group_id " +
+            " dc.err_code,dc.end_time, dc.secrecy,   dc.ip_address,  dc.port,  COALESCE(dc.custom_ptz_type, dc.ptz_type) AS ptz_type,  dc.password, dc.status, " +
+            " COALESCE(dc.custom_longitude, dc.longitude) AS longitude, COALESCE(dc.custom_latitude, dc.latitude) AS latitude,  pc.business_group_id " +
             " from wvp_device_channel dc" +
             " LEFT JOIN wvp_platform_gb_channel pgc on  dc.id = pgc.device_channel_id" +
             " LEFT JOIN wvp_platform_catalog pc on pgc.catalog_id = pc.id and pgc.platform_id = pc.platform_id" +
@@ -457,7 +492,44 @@ public interface DeviceChannelMapper {
     void clearPlay(String deviceId);
     // 设备主子码流逻辑END
     @Select(value = {" <script>" +
-            "select * " +
+            "SELECT id,\n" +
+            "       channel_id,\n" +
+            "       COALESCE(custom_name, name)           AS name,\n" +
+            "       custom_name,\n" +
+            "       manufacture,\n" +
+            "       model,\n" +
+            "       owner,\n" +
+            "       civil_code,\n" +
+            "       block,\n" +
+            "       address,\n" +
+            "       parent_id,\n" +
+            "       safety_way,\n" +
+            "       register_way,\n" +
+            "       cert_num,\n" +
+            "       certifiable,\n" +
+            "       err_code,\n" +
+            "       end_time,\n" +
+            "       secrecy,\n" +
+            "       ip_address,\n" +
+            "       port,\n" +
+            "       password,\n" +
+            "       COALESCE(custom_ptz_type, ptz_type)   AS ptz_type,\n" +
+            "       status,\n" +
+            "       COALESCE(custom_longitude, longitude) AS longitude,\n" +
+            "       COALESCE(custom_latitude, latitude)   AS latitude,\n" +
+            "       stream_id,\n" +
+            "       device_id,\n" +
+            "       parental,\n" +
+            "       has_audio,\n" +
+            "       create_time,\n" +
+            "       update_time,\n" +
+            "       sub_count,\n" +
+            "       longitude_gcj02,\n" +
+            "       latitude_gcj02,\n" +
+            "       longitude_wgs84,\n" +
+            "       latitude_wgs84,\n" +
+            "       business_group_id,\n" +
+            "       gps_time\n" +
             "from wvp_device_channel " +
             "where device_id=#{deviceId}" +
             " <if test='parentId != null and parentId != deviceId'> and parent_id = #{parentId} </if>" +

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

@@ -158,7 +158,7 @@ public interface GbStreamMapper {
                 " <foreach collection='list' item='item' index='index' separator=';'>"+
                     "UPDATE wvp_gb_stream " +
                     " SET name=#{item.name},"+
-                    " gb_id=#{item.gb_id}"+
+                    " gb_id=#{item.gbId}"+
                     " WHERE app=#{item.app} and stream=#{item.stream}"+
                 "</foreach>"+
             "</script>")

+ 12 - 0
src/main/java/com/genersoft/iot/vmp/storager/dao/MediaServerMapper.java

@@ -31,6 +31,8 @@ public interface MediaServerMapper {
             "rtp_port_range,"+
             "send_rtp_port_range,"+
             "record_assist_port,"+
+            "record_day,"+
+            "record_path,"+
             "default_server,"+
             "create_time,"+
             "update_time,"+
@@ -55,6 +57,8 @@ public interface MediaServerMapper {
             "#{rtpPortRange}, " +
             "#{sendRtpPortRange}, " +
             "#{recordAssistPort}, " +
+            "#{recordDay}, " +
+            "#{recordPath}, " +
             "#{defaultServer}, " +
             "#{createTime}, " +
             "#{updateTime}, " +
@@ -82,6 +86,8 @@ public interface MediaServerMapper {
             "<if test=\"secret != null\">, secret=#{secret}</if>" +
             "<if test=\"recordAssistPort != null\">, record_assist_port=#{recordAssistPort}</if>" +
             "<if test=\"hookAliveInterval != null\">, hook_alive_interval=#{hookAliveInterval}</if>" +
+            "<if test=\"recordDay != null\">, record_day=#{recordDay}</if>" +
+            "<if test=\"recordPath != null\">, record_path=#{recordPath}</if>" +
             "WHERE id=#{id}"+
             " </script>"})
     int update(MediaServerItem mediaServerItem);
@@ -105,6 +111,8 @@ public interface MediaServerMapper {
             "<if test=\"sendRtpPortRange != null\">, send_rtp_port_range=#{sendRtpPortRange}</if>" +
             "<if test=\"secret != null\">, secret=#{secret}</if>" +
             "<if test=\"recordAssistPort != null\">, record_assist_port=#{recordAssistPort}</if>" +
+            "<if test=\"recordDay != null\">, record_day=#{recordDay}</if>" +
+            "<if test=\"recordPath != null\">, record_path=#{recordPath}</if>" +
             "<if test=\"hookAliveInterval != null\">, hook_alive_interval=#{hookAliveInterval}</if>" +
             "WHERE ip=#{ip} and http_port=#{httpPort}"+
             " </script>"})
@@ -130,4 +138,8 @@ public interface MediaServerMapper {
 
     @Select("SELECT * FROM wvp_media_server WHERE default_server=true")
     MediaServerItem queryDefault();
+
+    @Select("SELECT * FROM wvp_media_server WHERE record_assist_port > 0")
+    List<MediaServerItem> queryAllWithAssistPort();
+
 }

+ 2 - 4
src/main/java/com/genersoft/iot/vmp/storager/dao/PlatformChannelMapper.java

@@ -117,8 +117,6 @@ public interface PlatformChannelMapper {
             "where dc.channel_id = #{channelId} and pgc.platform_id=#{platformId}")
     List<Device> queryDeviceInfoByPlatformIdAndChannelId(@Param("platformId") String platformId, @Param("channelId") String channelId);
 
-    @Select("SELECT pgc.platform_id from wvp_platform_gb_channel pgc left join wvp_device_channel dc on dc.id = pgc.device_channel_id WHERE dc.channel_id='${channelId}'")
-    List<String> queryParentPlatformByChannelId(String channelId);
-
-
+    @Select("SELECT pgc.platform_id from wvp_platform_gb_channel pgc left join wvp_device_channel dc on dc.id = pgc.device_channel_id WHERE dc.channel_id=#{channelId}")
+    List<String> queryParentPlatformByChannelId(@Param("channelId") String channelId);
 }

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

@@ -103,6 +103,9 @@ public interface PlatformGbStreamMapper {
             "</script>")
     void delByAppAndStreamsByPlatformId(@Param("gbStreams") List<GbStream> gbStreams, @Param("platformId") String platformId);
 
-    @Delete("DELETE from wvp_platform_gb_stream WHERE platform_id=#{platformId} and catalog_id=#{catalogId}")
+    @Delete("<script> "+
+            "DELETE from wvp_platform_gb_stream WHERE platform_id=#{platformId}" +
+            " <if test='catalogId != null' >  and catalog_id=#{catalogId}</if>" +
+            "</script>")
     int delByPlatformAndCatalogId(@Param("platformId") String platformId, @Param("catalogId") String catalogId);
 }

+ 5 - 4
src/main/java/com/genersoft/iot/vmp/storager/dao/StreamPushMapper.java

@@ -13,9 +13,9 @@ import java.util.List;
 public interface StreamPushMapper {
 
     @Insert("INSERT INTO wvp_stream_push (app, stream, total_reader_count, origin_type, origin_type_str, " +
-            "push_time, alive_second, media_server_id, update_time, create_time, push_ing, self) VALUES" +
+            "push_time, alive_second, media_server_id, server_id, update_time, create_time, push_ing, self) VALUES" +
             "(#{app}, #{stream}, #{totalReaderCount}, #{originType}, #{originTypeStr}, " +
-            "#{pushTime}, #{aliveSecond}, #{mediaServerId} , #{updateTime} , #{createTime}, " +
+            "#{pushTime}, #{aliveSecond}, #{mediaServerId} , #{serverId} , #{updateTime} , #{createTime}, " +
             "#{pushIng}, #{self} )")
     int add(StreamPushItem streamPushItem);
 
@@ -24,6 +24,7 @@ public interface StreamPushMapper {
             "UPDATE wvp_stream_push " +
             "SET update_time=#{updateTime}" +
             "<if test=\"mediaServerId != null\">, media_server_id=#{mediaServerId}</if>" +
+            "<if test=\"serverId != null\">, server_id=#{serverId}</if>" +
             "<if test=\"totalReaderCount != null\">, total_reader_count=#{totalReaderCount}</if>" +
             "<if test=\"originType != null\">, origin_type=#{originType}</if>" +
             "<if test=\"originTypeStr != null\">, origin_type_str=#{originTypeStr}</if>" +
@@ -89,10 +90,10 @@ public interface StreamPushMapper {
 
     @Insert("<script>"  +
             "Insert INTO wvp_stream_push (app, stream, total_reader_count, origin_type, origin_type_str, " +
-            "create_time, alive_second, media_server_id, status, push_ing) " +
+            "create_time, alive_second, media_server_id, server_id, status, push_ing) " +
             "VALUES <foreach collection='streamPushItems' item='item' index='index' separator=','>" +
             "( #{item.app}, #{item.stream}, #{item.totalReaderCount}, #{item.originType}, " +
-            "#{item.originTypeStr},#{item.createTime}, #{item.aliveSecond}, #{item.mediaServerId}, #{item.status} ," +
+            "#{item.originTypeStr},#{item.createTime}, #{item.aliveSecond}, #{item.mediaServerId},#{item.serverId}, #{item.status} ," +
             " #{item.pushIng} )" +
             " </foreach>" +
             "</script>")

+ 17 - 2
src/main/java/com/genersoft/iot/vmp/storager/impl/RedisCatchStorageImpl.java

@@ -609,14 +609,13 @@ public class RedisCatchStorageImpl implements IRedisCatchStorage {
     @Override
     public void sendDeviceOrChannelStatus(String deviceId, String channelId, boolean online) {
         String key = VideoManagerConstants.VM_MSG_SUBSCRIBE_DEVICE_STATUS;
-        logger.info("[redis通知] 发送 推送设备/通道状态, {}/{}-{}", deviceId, channelId, online);
         StringBuilder msg = new StringBuilder();
         msg.append(deviceId);
         if (channelId != null) {
             msg.append(":").append(channelId);
         }
         msg.append(" ").append(online? "ON":"OFF");
-        logger.info("[redis通知] 推送状态-> {} ", msg);
+        logger.info("[redis通知] 推送设备/通道状态-> {} ", msg);
         // 使用 RedisTemplate<Object, Object> 发送字符串消息会导致发送的消息多带了双引号
         stringRedisTemplate.convertAndSend(key, msg.toString());
     }
@@ -650,4 +649,20 @@ public class RedisCatchStorageImpl implements IRedisCatchStorage {
         logger.info("[redis发送通知] 发送 上级平台停止观看 {}: {}/{}->{}", key, msg.getApp(), msg.getStream(), msg.getPlatFormId());
         redisTemplate.convertAndSend(key, JSON.toJSON(msg));
     }
+
+    @Override
+    public void addPushListItem(String app, String stream, OnStreamChangedHookParam param) {
+        String key = VideoManagerConstants.PUSH_STREAM_LIST + app + "_" + stream;
+        redisTemplate.opsForValue().set(key, param);
+    }
+
+    @Override
+    public void removePushListItem(String app, String stream, String mediaServerId) {
+        String key = VideoManagerConstants.PUSH_STREAM_LIST + app + "_" + stream;
+        OnStreamChangedHookParam param = (OnStreamChangedHookParam)redisTemplate.opsForValue().get(key);
+        if (param != null && param.getMediaServerId().equalsIgnoreCase(mediaServerId)) {
+            redisTemplate.delete(key);
+        }
+
+    }
 }

+ 22 - 0
src/main/java/com/genersoft/iot/vmp/utils/CloudRecordUtils.java

@@ -0,0 +1,22 @@
+package com.genersoft.iot.vmp.utils;
+
+import com.genersoft.iot.vmp.media.zlm.dto.MediaServerItem;
+import com.genersoft.iot.vmp.service.bean.DownloadFileInfo;
+
+public class CloudRecordUtils {
+
+    public static DownloadFileInfo getDownloadFilePath(MediaServerItem mediaServerItem, String filePath) {
+        DownloadFileInfo downloadFileInfo = new DownloadFileInfo();
+
+        String pathTemplate = "%s://%s:%s/index/api/downloadFile?file_path=" + filePath;
+
+        downloadFileInfo.setHttpPath(String.format(pathTemplate, "http", mediaServerItem.getStreamIp(),
+                mediaServerItem.getHttpPort()));
+
+        if (mediaServerItem.getHttpSSlPort() > 0) {
+            downloadFileInfo.setHttpsPath(String.format(pathTemplate, "https", mediaServerItem.getStreamIp(),
+                    mediaServerItem.getHttpSSlPort()));
+        }
+        return downloadFileInfo;
+    }
+}

+ 31 - 0
src/main/java/com/genersoft/iot/vmp/utils/DateUtil.java

@@ -40,11 +40,17 @@ public class DateUtil {
      */
     public static final String URL_PATTERN = "yyyyMMddHHmmss";
 
+    /**
+     * 日期格式
+     */
+    public static final String date_PATTERN = "yyyy-MM-dd";
+
     public static final String zoneStr = "Asia/Shanghai";
 
     public static final DateTimeFormatter formatterCompatibleISO8601 = DateTimeFormatter.ofPattern(ISO8601_COMPATIBLE_PATTERN, Locale.getDefault()).withZone(ZoneId.of(zoneStr));
     public static final DateTimeFormatter formatterISO8601 = DateTimeFormatter.ofPattern(ISO8601_PATTERN, Locale.getDefault()).withZone(ZoneId.of(zoneStr));
     public static final DateTimeFormatter formatter = DateTimeFormatter.ofPattern(PATTERN, Locale.getDefault()).withZone(ZoneId.of(zoneStr));
+    public static final DateTimeFormatter DateFormatter = DateTimeFormatter.ofPattern(date_PATTERN, Locale.getDefault()).withZone(ZoneId.of(zoneStr));
     public static final DateTimeFormatter urlFormatter = DateTimeFormatter.ofPattern(URL_PATTERN, Locale.getDefault()).withZone(ZoneId.of(zoneStr));
 
 	public static String yyyy_MM_dd_HH_mm_ssToISO8601(String formatTime) {
@@ -71,6 +77,22 @@ public class DateUtil {
         return instant.getEpochSecond();
 	}
 
+    /**
+     * 时间戳 转 yyyy_MM_dd_HH_mm_ss
+     */
+	public static String timestampTo_yyyy_MM_dd_HH_mm_ss(long timestamp) {
+        Instant instant = Instant.ofEpochSecond(timestamp);
+        return formatter.format(LocalDateTime.ofInstant(instant, ZoneId.of(zoneStr)));
+	}
+
+    /**
+     * 时间戳 转 yyyy_MM_dd
+     */
+    public static String timestampTo_yyyy_MM_dd(long timestamp) {
+        Instant instant = Instant.ofEpochMilli(timestamp);
+        return DateFormatter.format(LocalDateTime.ofInstant(instant, ZoneId.of(zoneStr)));
+    }
+
     /**
      * 获取当前时间
      * @return
@@ -117,4 +139,13 @@ public class DateUtil {
         Instant beforeInstant = Instant.from(formatter.parse(keepaliveTime));
         return ChronoUnit.MILLIS.between(beforeInstant, Instant.now());
     }
+
+    public static long getDifference(String startTime, String endTime) {
+        if (ObjectUtils.isEmpty(startTime) || ObjectUtils.isEmpty(endTime)) {
+            return 0;
+        }
+        Instant startInstant = Instant.from(formatter.parse(startTime));
+        Instant endInstant = Instant.from(formatter.parse(endTime));
+        return ChronoUnit.MILLIS.between(endInstant, startInstant);
+    }
 }

+ 16 - 0
src/main/java/com/genersoft/iot/vmp/vmanager/bean/StreamContent.java

@@ -1,6 +1,7 @@
 package com.genersoft.iot.vmp.vmanager.bean;
 
 import com.genersoft.iot.vmp.common.StreamInfo;
+import com.genersoft.iot.vmp.service.bean.DownloadFileInfo;
 import io.swagger.v3.oas.annotations.media.Schema;
 
 @Schema(description = "流信息")
@@ -93,6 +94,9 @@ public class StreamContent {
     @Schema(description = "结束时间")
     private String endTime;
 
+    @Schema(description = "文件下载地址(录像下载使用)")
+    private DownloadFileInfo downLoadFilePath;
+
     private double progress;
 
     public StreamContent(StreamInfo streamInfo) {
@@ -170,6 +174,10 @@ public class StreamContent {
         this.startTime = streamInfo.getStartTime();
         this.endTime = streamInfo.getEndTime();
         this.progress = streamInfo.getProgress();
+
+        if (streamInfo.getDownLoadFilePath() != null) {
+            this.downLoadFilePath = streamInfo.getDownLoadFilePath();
+        }
     }
 
     public String getApp() {
@@ -411,4 +419,12 @@ public class StreamContent {
     public void setProgress(double progress) {
         this.progress = progress;
     }
+
+    public DownloadFileInfo getDownLoadFilePath() {
+        return downLoadFilePath;
+    }
+
+    public void setDownLoadFilePath(DownloadFileInfo downLoadFilePath) {
+        this.downLoadFilePath = downLoadFilePath;
+    }
 }

+ 146 - 38
src/main/java/com/genersoft/iot/vmp/vmanager/cloudRecord/CloudRecordController.java

@@ -1,18 +1,22 @@
 package com.genersoft.iot.vmp.vmanager.cloudRecord;
 
+import com.alibaba.fastjson2.JSONArray;
 import com.genersoft.iot.vmp.conf.DynamicTask;
 import com.genersoft.iot.vmp.conf.UserSetting;
 import com.genersoft.iot.vmp.conf.exception.ControllerException;
+import com.genersoft.iot.vmp.conf.security.JwtUtils;
 import com.genersoft.iot.vmp.media.zlm.SendRtpPortManager;
 import com.genersoft.iot.vmp.media.zlm.ZLMServerFactory;
-import com.genersoft.iot.vmp.media.zlm.ZlmHttpHookSubscribe;
 import com.genersoft.iot.vmp.media.zlm.dto.MediaServerItem;
+import com.genersoft.iot.vmp.service.ICloudRecordService;
 import com.genersoft.iot.vmp.service.IMediaServerService;
+import com.genersoft.iot.vmp.service.bean.CloudRecordItem;
+import com.genersoft.iot.vmp.service.bean.DownloadFileInfo;
 import com.genersoft.iot.vmp.vmanager.bean.ErrorCode;
-import com.genersoft.iot.vmp.vmanager.bean.PageInfo;
-import com.genersoft.iot.vmp.vmanager.bean.RecordFile;
+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.security.SecurityRequirement;
 import io.swagger.v3.oas.annotations.tags.Tag;
 import org.apache.commons.lang3.ObjectUtils;
 import org.slf4j.Logger;
@@ -32,40 +36,27 @@ import java.util.List;
 @RequestMapping("/api/cloud/record")
 public class CloudRecordController {
 
-    @Autowired
-    private ZLMServerFactory zlmServerFactory;
-
-    @Autowired
-    private SendRtpPortManager sendRtpPortManager;
 
     private final static Logger logger = LoggerFactory.getLogger(CloudRecordController.class);
 
     @Autowired
-    private ZlmHttpHookSubscribe hookSubscribe;
+    private ICloudRecordService cloudRecordService;
 
     @Autowired
     private IMediaServerService mediaServerService;
 
-    @Autowired
-    private UserSetting userSetting;
-
-    @Autowired
-    private DynamicTask dynamicTask;
-
-    @Autowired
-    private RedisTemplate<Object, Object> redisTemplate;
 
     @ResponseBody
     @GetMapping("/date/list")
-    @Operation(summary = "查询存在云端录像的日期")
+    @Operation(summary = "查询存在云端录像的日期", security = @SecurityRequirement(name = JwtUtils.HEADER))
     @Parameter(name = "app", description = "应用名", required = true)
     @Parameter(name = "stream", description = "流ID", required = true)
     @Parameter(name = "year", description = "年,置空则查询当年", required = false)
     @Parameter(name = "month", description = "月,置空则查询当月", required = false)
     @Parameter(name = "mediaServerId", description = "流媒体ID,置空则查询全部", required = false)
     public List<String> openRtpServer(
-            @RequestParam String app,
-            @RequestParam String stream,
+            @RequestParam(required = true) String app,
+            @RequestParam(required = true) String stream,
             @RequestParam(required = false) int year,
             @RequestParam(required = false) int month,
             @RequestParam(required = false) String mediaServerId
@@ -95,26 +86,28 @@ public class CloudRecordController {
             return new ArrayList<>();
         }
 
-        return mediaServerService.getRecordDates(app, stream, year, month, mediaServerItems);
+        return cloudRecordService.getDateList(app, stream, year, month, mediaServerItems);
     }
 
     @ResponseBody
     @GetMapping("/list")
-    @Operation(summary = "分页查询云端录像")
-    @Parameter(name = "app", description = "应用名", required = true)
-    @Parameter(name = "stream", description = "流ID", required = true)
-    @Parameter(name = "page", description = "当前页", required = false)
-    @Parameter(name = "count", description = "每页查询数量", required = false)
-    @Parameter(name = "startTime", description = "开始时间(yyyy-MM-dd HH:mm:ss)", required = true)
-    @Parameter(name = "endTime", description = "结束时间(yyyy-MM-dd HH:mm:ss)", required = true)
+    @Operation(summary = "分页查询云端录像", security = @SecurityRequirement(name = JwtUtils.HEADER))
+    @Parameter(name = "query", description = "检索内容", required = false)
+    @Parameter(name = "app", description = "应用名", required = false)
+    @Parameter(name = "stream", description = "流ID", required = false)
+    @Parameter(name = "page", description = "当前页", required = true)
+    @Parameter(name = "count", description = "每页查询数量", required = true)
+    @Parameter(name = "startTime", description = "开始时间(yyyy-MM-dd HH:mm:ss)", required = false)
+    @Parameter(name = "endTime", description = "结束时间(yyyy-MM-dd HH:mm:ss)", required = false)
     @Parameter(name = "mediaServerId", description = "流媒体ID,置空则查询全部流媒体", required = false)
-    public PageInfo<RecordFile> openRtpServer(
-            @RequestParam String app,
-            @RequestParam String stream,
+    public PageInfo<CloudRecordItem> openRtpServer(
+            @RequestParam(required = false)  String query,
+            @RequestParam(required = false)  String app,
+            @RequestParam(required = false)  String stream,
             @RequestParam int page,
             @RequestParam int count,
-            @RequestParam String startTime,
-            @RequestParam String endTime,
+            @RequestParam(required = false)  String startTime,
+            @RequestParam(required = false)  String endTime,
             @RequestParam(required = false) String mediaServerId
 
     ) {
@@ -133,13 +126,128 @@ public class CloudRecordController {
             mediaServerItems = mediaServerService.getAll();
         }
         if (mediaServerItems.isEmpty()) {
-            return new PageInfo<>();
+            throw new ControllerException(ErrorCode.ERROR100.getCode(), "当前无流媒体");
+        }
+        if (query != null && ObjectUtils.isEmpty(query.trim())) {
+            query = null;
+        }
+        if (app != null && ObjectUtils.isEmpty(app.trim())) {
+            app = null;
+        }
+        if (stream != null && ObjectUtils.isEmpty(stream.trim())) {
+            stream = null;
+        }
+        if (startTime != null && ObjectUtils.isEmpty(startTime.trim())) {
+            startTime = null;
+        }
+        if (endTime != null && ObjectUtils.isEmpty(endTime.trim())) {
+            endTime = null;
         }
-        List<RecordFile> records = mediaServerService.getRecords(app, stream, startTime, endTime, mediaServerItems);
-        PageInfo<RecordFile> pageInfo = new PageInfo<>(records);
-        pageInfo.startPage(page, count);
-        return pageInfo;
+        return cloudRecordService.getList(page, count, query, app, stream, startTime, endTime, mediaServerItems);
     }
 
+    @ResponseBody
+    @GetMapping("/task/add")
+    @Operation(summary = "添加合并任务")
+    @Parameter(name = "app", description = "应用名", required = false)
+    @Parameter(name = "stream", description = "流ID", required = false)
+    @Parameter(name = "mediaServerId", description = "流媒体ID", required = false)
+    @Parameter(name = "startTime", description = "鉴权ID", required = false)
+    @Parameter(name = "endTime", description = "鉴权ID", required = false)
+    @Parameter(name = "callId", description = "鉴权ID", required = false)
+    @Parameter(name = "remoteHost", description = "返回地址时的远程地址", required = false)
+    public String addTask(
+            @RequestParam(required = false) String app,
+            @RequestParam(required = false) String stream,
+            @RequestParam(required = false) String mediaServerId,
+            @RequestParam(required = false) String startTime,
+            @RequestParam(required = false) String endTime,
+            @RequestParam(required = false) String callId,
+            @RequestParam(required = false) String remoteHost
+    ){
+        return cloudRecordService.addTask(app, stream, mediaServerId, startTime, endTime, callId, remoteHost);
+    }
+
+    @ResponseBody
+    @GetMapping("/task/list")
+    @Operation(summary = "查询合并任务")
+    @Parameter(name = "taskId", description = "任务Id", required = false)
+    @Parameter(name = "mediaServerId", description = "流媒体ID", required = false)
+    @Parameter(name = "isEnd", description = "是否结束", required = false)
+    public JSONArray queryTaskList(
+            @RequestParam(required = false) String app,
+            @RequestParam(required = false) String stream,
+            @RequestParam(required = false) String callId,
+            @RequestParam(required = false) String taskId,
+            @RequestParam(required = false) String mediaServerId,
+            @RequestParam(required = false) Boolean isEnd
+    ){
+        return cloudRecordService.queryTask(app, stream, callId, taskId, mediaServerId, isEnd);
+    }
 
+    @ResponseBody
+    @GetMapping("/collect/add")
+    @Operation(summary = "添加收藏")
+    @Parameter(name = "app", description = "应用名", required = false)
+    @Parameter(name = "stream", description = "流ID", required = false)
+    @Parameter(name = "mediaServerId", description = "流媒体ID", required = false)
+    @Parameter(name = "startTime", description = "鉴权ID", required = false)
+    @Parameter(name = "endTime", description = "鉴权ID", required = false)
+    @Parameter(name = "callId", description = "鉴权ID", required = false)
+    @Parameter(name = "recordId", description = "录像记录的ID,用于精准收藏一个视频文件", required = false)
+    public int addCollect(
+            @RequestParam(required = false) String app,
+            @RequestParam(required = false) String stream,
+            @RequestParam(required = false) String mediaServerId,
+            @RequestParam(required = false) String startTime,
+            @RequestParam(required = false) String endTime,
+            @RequestParam(required = false) String callId,
+            @RequestParam(required = false) Integer recordId
+    ){
+        logger.info("[云端录像] 添加收藏,app={},stream={},mediaServerId={},startTime={},endTime={},callId={},recordId={}",
+                app, stream, mediaServerId, startTime, endTime, callId, recordId);
+        if (recordId != null) {
+            return cloudRecordService.changeCollectById(recordId, true);
+        }else {
+            return cloudRecordService.changeCollect(true, app, stream, mediaServerId, startTime, endTime, callId);
+        }
+    }
+
+    @ResponseBody
+    @GetMapping("/collect/delete")
+    @Operation(summary = "移除收藏")
+    @Parameter(name = "app", description = "应用名", required = false)
+    @Parameter(name = "stream", description = "流ID", required = false)
+    @Parameter(name = "mediaServerId", description = "流媒体ID", required = false)
+    @Parameter(name = "startTime", description = "鉴权ID", required = false)
+    @Parameter(name = "endTime", description = "鉴权ID", required = false)
+    @Parameter(name = "callId", description = "鉴权ID", required = false)
+    @Parameter(name = "recordId", description = "录像记录的ID,用于精准精准移除一个视频文件的收藏", required = false)
+    public int deleteCollect(
+            @RequestParam(required = false) String app,
+            @RequestParam(required = false) String stream,
+            @RequestParam(required = false) String mediaServerId,
+            @RequestParam(required = false) String startTime,
+            @RequestParam(required = false) String endTime,
+            @RequestParam(required = false) String callId,
+            @RequestParam(required = false) Integer recordId
+    ){
+        logger.info("[云端录像] 移除收藏,app={},stream={},mediaServerId={},startTime={},endTime={},callId={},recordId={}",
+                app, stream, mediaServerId, startTime, endTime, callId, recordId);
+        if (recordId != null) {
+            return cloudRecordService.changeCollectById(recordId, false);
+        }else {
+            return cloudRecordService.changeCollect(false, app, stream, mediaServerId, startTime, endTime, callId);
+        }
+    }
+
+    @ResponseBody
+    @GetMapping("/play/path")
+    @Operation(summary = "获取播放地址")
+    @Parameter(name = "recordId", description = "录像记录的ID", required = true)
+    public DownloadFileInfo getPlayUrlPath(
+            @RequestParam(required = true) Integer recordId
+    ){
+        return cloudRecordService.getPlayUrlPath(recordId);
+    }
 }

+ 7 - 5
src/main/java/com/genersoft/iot/vmp/vmanager/gb28181/MobilePosition/MobilePositionController.java

@@ -1,6 +1,7 @@
 package com.genersoft.iot.vmp.vmanager.gb28181.MobilePosition;
 
 import com.genersoft.iot.vmp.conf.exception.ControllerException;
+import com.genersoft.iot.vmp.conf.security.JwtUtils;
 import com.genersoft.iot.vmp.gb28181.bean.Device;
 import com.genersoft.iot.vmp.gb28181.bean.MobilePosition;
 import com.genersoft.iot.vmp.gb28181.transmit.callback.DeferredResultHolder;
@@ -13,6 +14,7 @@ import com.genersoft.iot.vmp.vmanager.bean.ErrorCode;
 import com.github.pagehelper.util.StringUtil;
 import io.swagger.v3.oas.annotations.Operation;
 import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.security.SecurityRequirement;
 import io.swagger.v3.oas.annotations.tags.Tag;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -59,7 +61,7 @@ public class MobilePositionController {
      * @param end 结束时间
      * @return
      */
-    @Operation(summary = "查询历史轨迹")
+    @Operation(summary = "查询历史轨迹", security = @SecurityRequirement(name = JwtUtils.HEADER))
     @Parameter(name = "deviceId", description = "设备国标编号", required = true)
     @Parameter(name = "channelId", description = "通道国标编号")
     @Parameter(name = "start", description = "开始时间")
@@ -84,7 +86,7 @@ public class MobilePositionController {
      * @param deviceId 设备ID
      * @return
      */
-    @Operation(summary = "查询设备最新位置")
+    @Operation(summary = "查询设备最新位置", security = @SecurityRequirement(name = JwtUtils.HEADER))
     @Parameter(name = "deviceId", description = "设备国标编号", required = true)
     @GetMapping("/latest/{deviceId}")
     public MobilePosition latestPosition(@PathVariable String deviceId) {
@@ -96,7 +98,7 @@ public class MobilePositionController {
      * @param deviceId 设备ID
      * @return
      */
-    @Operation(summary = "获取移动位置信息")
+    @Operation(summary = "获取移动位置信息", security = @SecurityRequirement(name = JwtUtils.HEADER))
     @Parameter(name = "deviceId", description = "设备国标编号", required = true)
     @GetMapping("/realtime/{deviceId}")
     public DeferredResult<MobilePosition> realTimePosition(@PathVariable String deviceId) {
@@ -136,7 +138,7 @@ public class MobilePositionController {
      * @param interval 上报时间间隔
      * @return true = 命令发送成功
      */
-    @Operation(summary = "订阅位置信息")
+    @Operation(summary = "订阅位置信息", security = @SecurityRequirement(name = JwtUtils.HEADER))
     @Parameter(name = "deviceId", description = "设备国标编号", required = true)
     @Parameter(name = "expires", description = "订阅超时时间", required = true)
     @Parameter(name = "interval", description = "上报时间间隔", required = true)
@@ -162,7 +164,7 @@ public class MobilePositionController {
      * @param deviceId 设备ID
      * @return true = 命令发送成功
      */
-    @Operation(summary = "数据位置信息格式处理")
+    @Operation(summary = "数据位置信息格式处理", security = @SecurityRequirement(name = JwtUtils.HEADER))
     @Parameter(name = "deviceId", description = "设备国标编号", required = true)
     @GetMapping("/transform/{deviceId}")
     public void positionTransform(@PathVariable String deviceId) {

+ 5 - 3
src/main/java/com/genersoft/iot/vmp/vmanager/gb28181/alarm/AlarmController.java

@@ -1,6 +1,7 @@
 package com.genersoft.iot.vmp.vmanager.gb28181.alarm;
 
 import com.genersoft.iot.vmp.conf.exception.ControllerException;
+import com.genersoft.iot.vmp.conf.security.JwtUtils;
 import com.genersoft.iot.vmp.gb28181.bean.Device;
 import com.genersoft.iot.vmp.gb28181.bean.DeviceAlarm;
 import com.genersoft.iot.vmp.gb28181.bean.ParentPlatform;
@@ -13,6 +14,7 @@ import com.genersoft.iot.vmp.vmanager.bean.ErrorCode;
 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.security.SecurityRequirement;
 import io.swagger.v3.oas.annotations.tags.Tag;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -56,7 +58,7 @@ public class AlarmController {
      * @return
      */
     @DeleteMapping("/delete")
-    @Operation(summary = "删除报警")
+    @Operation(summary = "删除报警", security = @SecurityRequirement(name = JwtUtils.HEADER))
     @Parameter(name = "id", description = "ID")
     @Parameter(name = "deviceIds", description = "多个设备id,逗号分隔")
     @Parameter(name = "time", description = "结束时间")
@@ -93,7 +95,7 @@ public class AlarmController {
      * @return
      */
     @GetMapping("/test/notify/alarm")
-    @Operation(summary = "测试向上级/设备发送模拟报警通知")
+    @Operation(summary = "测试向上级/设备发送模拟报警通知", security = @SecurityRequirement(name = JwtUtils.HEADER))
     @Parameter(name = "deviceId", description = "设备国标编号")
     public void delete(@RequestParam String deviceId) {
         Device device = storage.queryVideoDevice(deviceId);
@@ -141,7 +143,7 @@ public class AlarmController {
      * @param endTime 结束时间
      * @return
      */
-    @Operation(summary = "分页查询报警")
+    @Operation(summary = "分页查询报警", security = @SecurityRequirement(name = JwtUtils.HEADER))
     @Parameter(name = "page",description = "当前页",required = true)
     @Parameter(name = "count",description = "每页查询数量",required = true)
     @Parameter(name = "deviceId",description = "设备id")

+ 4 - 2
src/main/java/com/genersoft/iot/vmp/vmanager/gb28181/device/DeviceConfig.java

@@ -9,6 +9,7 @@ package com.genersoft.iot.vmp.vmanager.gb28181.device;
 
 import com.alibaba.fastjson2.JSONObject;
 import com.genersoft.iot.vmp.conf.exception.ControllerException;
+import com.genersoft.iot.vmp.conf.security.JwtUtils;
 import com.genersoft.iot.vmp.gb28181.bean.Device;
 import com.genersoft.iot.vmp.gb28181.transmit.callback.DeferredResultHolder;
 import com.genersoft.iot.vmp.gb28181.transmit.callback.RequestMessage;
@@ -17,6 +18,7 @@ import com.genersoft.iot.vmp.storager.IVideoManagerStorage;
 import com.genersoft.iot.vmp.vmanager.bean.ErrorCode;
 import io.swagger.v3.oas.annotations.Operation;
 import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.security.SecurityRequirement;
 import io.swagger.v3.oas.annotations.tags.Tag;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -57,7 +59,7 @@ public class DeviceConfig {
 	 * @return
 	 */
 	@GetMapping("/basicParam/{deviceId}")
-	@Operation(summary = "基本配置设置命令")
+	@Operation(summary = "基本配置设置命令", security = @SecurityRequirement(name = JwtUtils.HEADER))
 	@Parameter(name = "deviceId", description = "设备国标编号", required = true)
 	@Parameter(name = "channelId", description = "通道国标编号", required = true)
 	@Parameter(name = "name", description = "名称")
@@ -113,7 +115,7 @@ public class DeviceConfig {
 	 * @param channelId 通道ID
 	 * @return
 	 */
-	@Operation(summary = "设备配置查询请求")
+	@Operation(summary = "设备配置查询请求", security = @SecurityRequirement(name = JwtUtils.HEADER))
 	@Parameter(name = "deviceId", description = "设备国标编号", required = true)
 	@Parameter(name = "channelId", description = "通道国标编号", required = true)
 	@Parameter(name = "configType", description = "配置类型")

+ 10 - 8
src/main/java/com/genersoft/iot/vmp/vmanager/gb28181/device/DeviceControl.java

@@ -9,6 +9,7 @@ package com.genersoft.iot.vmp.vmanager.gb28181.device;
 
 import com.alibaba.fastjson2.JSONObject;
 import com.genersoft.iot.vmp.conf.exception.ControllerException;
+import com.genersoft.iot.vmp.conf.security.JwtUtils;
 import com.genersoft.iot.vmp.gb28181.bean.Device;
 import com.genersoft.iot.vmp.gb28181.transmit.callback.DeferredResultHolder;
 import com.genersoft.iot.vmp.gb28181.transmit.callback.RequestMessage;
@@ -17,6 +18,7 @@ import com.genersoft.iot.vmp.storager.IVideoManagerStorage;
 import com.genersoft.iot.vmp.vmanager.bean.ErrorCode;
 import io.swagger.v3.oas.annotations.Operation;
 import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.security.SecurityRequirement;
 import io.swagger.v3.oas.annotations.tags.Tag;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -53,7 +55,7 @@ public class DeviceControl {
      * 
      * @param deviceId 设备ID
      */
-	@Operation(summary = "远程启动控制命令")
+	@Operation(summary = "远程启动控制命令", security = @SecurityRequirement(name = JwtUtils.HEADER))
 	@Parameter(name = "deviceId", description = "设备国标编号", required = true)
     @GetMapping("/teleboot/{deviceId}")
     public void teleBootApi(@PathVariable String deviceId) {
@@ -76,7 +78,7 @@ public class DeviceControl {
      * @param recordCmdStr  Record:手动录像,StopRecord:停止手动录像
      * @param channelId     通道编码(可选)
      */
-	@Operation(summary = "录像控制")
+	@Operation(summary = "录像控制", security = @SecurityRequirement(name = JwtUtils.HEADER))
 	@Parameter(name = "deviceId", description = "设备国标编号", required = true)
 	@Parameter(name = "channelId", description = "通道国标编号", required = true)
 	@Parameter(name = "recordCmdStr", description = "命令, 可选值:Record(手动录像),StopRecord(停止手动录像)", required = true)
@@ -125,7 +127,7 @@ public class DeviceControl {
 	 * @param	deviceId 设备ID
 	 * @param	guardCmdStr SetGuard:布防,ResetGuard:撤防
 	 */
-	@Operation(summary = "布防/撤防命令")
+	@Operation(summary = "布防/撤防命令", security = @SecurityRequirement(name = JwtUtils.HEADER))
 	@Parameter(name = "deviceId", description = "设备国标编号", required = true)
 	@Parameter(name = "guardCmdStr", description = "命令, 可选值:SetGuard(布防),ResetGuard(撤防)", required = true)
 	@GetMapping("/guard/{deviceId}/{guardCmdStr}")
@@ -170,7 +172,7 @@ public class DeviceControl {
 	 * @param	alarmMethod 报警方式(可选)
 	 * @param	alarmType   报警类型(可选)
 	 */
-	@Operation(summary = "报警复位")
+	@Operation(summary = "报警复位", security = @SecurityRequirement(name = JwtUtils.HEADER))
 	@Parameter(name = "deviceId", description = "设备国标编号", required = true)
 	@Parameter(name = "channelId", description = "通道国标编号", required = true)
 	@Parameter(name = "alarmMethod", description = "报警方式")
@@ -217,7 +219,7 @@ public class DeviceControl {
 	 * @param	deviceId 设备ID
 	 * @param	channelId  通道ID
 	 */
-	@Operation(summary = "强制关键帧")
+	@Operation(summary = "强制关键帧", security = @SecurityRequirement(name = JwtUtils.HEADER))
 	@Parameter(name = "deviceId", description = "设备国标编号", required = true)
 	@Parameter(name = "channelId", description = "通道国标编号")
 	@GetMapping("/i_frame/{deviceId}")
@@ -249,7 +251,7 @@ public class DeviceControl {
      * @param presetIndex   调用预置位编号(可选)
      * @param channelId     通道编码(可选)
 	 */
-	@Operation(summary = "看守位控制")
+	@Operation(summary = "看守位控制", security = @SecurityRequirement(name = JwtUtils.HEADER))
 	@Parameter(name = "deviceId", description = "设备国标编号", required = true)
 	@Parameter(name = "channelId", description = "通道国标编号", required = true)
 	@Parameter(name = "enabled", description = "是否开启看守位 1:开启,0:关闭", required = true)
@@ -309,7 +311,7 @@ public class DeviceControl {
 	 * @param lengthy 拉框宽度像素值
 	 * @return
 	 */
-	@Operation(summary = "拉框放大")
+	@Operation(summary = "拉框放大", security = @SecurityRequirement(name = JwtUtils.HEADER))
 	@Parameter(name = "deviceId", description = "设备国标编号", required = true)
 	@Parameter(name = "channelId", description = "通道国标编号", required = true)
 	@Parameter(name = "length", description = "播放窗口长度像素值", required = true)
@@ -359,7 +361,7 @@ public class DeviceControl {
 	 * @param lengthy 拉框宽度像素值
 	 * @return
 	 */
-	@Operation(summary = "拉框放大")
+	@Operation(summary = "拉框缩小", security = @SecurityRequirement(name = JwtUtils.HEADER))
 	@Parameter(name = "deviceId", description = "设备国标编号", required = true)
 	@Parameter(name = "channelId", description = "通道国标编号")
 	@Parameter(name = "length", description = "播放窗口长度像素值", required = true)

+ 16 - 14
src/main/java/com/genersoft/iot/vmp/vmanager/gb28181/device/DeviceQuery.java

@@ -3,6 +3,7 @@ package com.genersoft.iot.vmp.vmanager.gb28181.device;
 import com.alibaba.fastjson2.JSONObject;
 import com.genersoft.iot.vmp.conf.DynamicTask;
 import com.genersoft.iot.vmp.conf.exception.ControllerException;
+import com.genersoft.iot.vmp.conf.security.JwtUtils;
 import com.genersoft.iot.vmp.gb28181.bean.Device;
 import com.genersoft.iot.vmp.gb28181.bean.DeviceChannel;
 import com.genersoft.iot.vmp.gb28181.bean.SyncStatus;
@@ -23,6 +24,7 @@ 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.security.SecurityRequirement;
 import io.swagger.v3.oas.annotations.tags.Tag;
 import org.apache.commons.compress.utils.IOUtils;
 import org.apache.ibatis.annotations.Options;
@@ -85,7 +87,7 @@ public class DeviceQuery {
 	 * @param deviceId 国标ID
 	 * @return 国标设备
 	 */
-	@Operation(summary = "查询国标设备")
+	@Operation(summary = "查询国标设备", security = @SecurityRequirement(name = JwtUtils.HEADER))
 	@Parameter(name = "deviceId", description = "设备国标编号", required = true)
 	@GetMapping("/devices/{deviceId}")
 	public Device devices(@PathVariable String deviceId){
@@ -99,7 +101,7 @@ public class DeviceQuery {
 	 * @param count 每页查询数量
 	 * @return 分页国标列表
 	 */
-	@Operation(summary = "分页查询国标设备")
+	@Operation(summary = "分页查询国标设备", security = @SecurityRequirement(name = JwtUtils.HEADER))
 	@Parameter(name = "page", description = "当前页", required = true)
 	@Parameter(name = "count", description = "每页查询数量", required = true)
 	@GetMapping("/devices")
@@ -123,7 +125,7 @@ public class DeviceQuery {
 	 * @return 通道列表
 	 */
 	@GetMapping("/devices/{deviceId}/channels")
-	@Operation(summary = "分页查询通道")
+	@Operation(summary = "分页查询通道", security = @SecurityRequirement(name = JwtUtils.HEADER))
 	@Parameter(name = "deviceId", description = "设备国标编号", required = true)
 	@Parameter(name = "page", description = "当前页", required = true)
 	@Parameter(name = "count", description = "每页查询数量", required = true)
@@ -149,7 +151,7 @@ public class DeviceQuery {
 	 * @param deviceId 设备id
 	 * @return
 	 */
-	@Operation(summary = "同步设备通道")
+	@Operation(summary = "同步设备通道", security = @SecurityRequirement(name = JwtUtils.HEADER))
 	@Parameter(name = "deviceId", description = "设备国标编号", required = true)
 	@GetMapping("/devices/{deviceId}/sync")
 	public WVPResult<SyncStatus> devicesSync(@PathVariable String deviceId){
@@ -177,7 +179,7 @@ public class DeviceQuery {
 	 * @param deviceId 设备id
 	 * @return
 	 */
-	@Operation(summary = "移除设备")
+	@Operation(summary = "移除设备", security = @SecurityRequirement(name = JwtUtils.HEADER))
 	@Parameter(name = "deviceId", description = "设备国标编号", required = true)
 	@DeleteMapping("/devices/{deviceId}/delete")
 	public String delete(@PathVariable String deviceId){
@@ -222,7 +224,7 @@ public class DeviceQuery {
 	 * @param channelType 通道类型
 	 * @return 子通道列表
 	 */
-	@Operation(summary = "分页查询子目录通道")
+	@Operation(summary = "分页查询子目录通道", security = @SecurityRequirement(name = JwtUtils.HEADER))
 	@Parameter(name = "deviceId", description = "设备国标编号", required = true)
 	@Parameter(name = "channelId", description = "通道国标编号", required = true)
 	@Parameter(name = "page", description = "当前页", required = true)
@@ -254,7 +256,7 @@ public class DeviceQuery {
 	 * @param channel 通道
 	 * @return
 	 */
-	@Operation(summary = "更新通道信息")
+	@Operation(summary = "更新通道信息", security = @SecurityRequirement(name = JwtUtils.HEADER))
 	@Parameter(name = "deviceId", description = "设备国标编号", required = true)
 	@Parameter(name = "channel", description = "通道信息", required = true)
 	@PostMapping("/channel/update/{deviceId}")
@@ -268,7 +270,7 @@ public class DeviceQuery {
 	 * @param streamMode 数据流传输模式
 	 * @return
 	 */
-	@Operation(summary = "修改数据流传输模式")
+	@Operation(summary = "修改数据流传输模式", security = @SecurityRequirement(name = JwtUtils.HEADER))
 	@Parameter(name = "deviceId", description = "设备国标编号", required = true)
 	@Parameter(name = "streamMode", description = "数据流传输模式, 取值:" +
 			"UDP(udp传输),TCP-ACTIVE(tcp主动模式,暂不支持),TCP-PASSIVE(tcp被动模式)", required = true)
@@ -284,7 +286,7 @@ public class DeviceQuery {
 	 * @param device 设备信息
 	 * @return
 	 */
-	@Operation(summary = "添加设备信息")
+	@Operation(summary = "添加设备信息", security = @SecurityRequirement(name = JwtUtils.HEADER))
 	@Parameter(name = "device", description = "设备", required = true)
 	@PostMapping("/device/add/")
 	public void addDevice(Device device){
@@ -306,7 +308,7 @@ public class DeviceQuery {
 	 * @param device 设备信息
 	 * @return
 	 */
-	@Operation(summary = "更新设备信息")
+	@Operation(summary = "更新设备信息", security = @SecurityRequirement(name = JwtUtils.HEADER))
 	@Parameter(name = "device", description = "设备", required = true)
 	@PostMapping("/device/update/")
 	public void updateDevice(Device device){
@@ -321,7 +323,7 @@ public class DeviceQuery {
 	 * 
 	 * @param deviceId 设备id
 	 */
-	@Operation(summary = "设备状态查询")
+	@Operation(summary = "设备状态查询", security = @SecurityRequirement(name = JwtUtils.HEADER))
 	@Parameter(name = "deviceId", description = "设备国标编号", required = true)
 	@GetMapping("/devices/{deviceId}/status")
 	public DeferredResult<ResponseEntity<String>> deviceStatusApi(@PathVariable String deviceId) {
@@ -372,7 +374,7 @@ public class DeviceQuery {
 	 * @param endTime		报警发生终止时间(可选)
 	 * @return				true = 命令发送成功
 	 */
-	@Operation(summary = "设备状态查询")
+	@Operation(summary = "设备报警查询", security = @SecurityRequirement(name = JwtUtils.HEADER))
 	@Parameter(name = "deviceId", description = "设备国标编号", required = true)
 	@Parameter(name = "startPriority", description = "报警起始级别")
 	@Parameter(name = "endPriority", description = "报警终止级别")
@@ -422,7 +424,7 @@ public class DeviceQuery {
 
 
 	@GetMapping("/{deviceId}/sync_status")
-	@Operation(summary = "获取通道同步进度")
+	@Operation(summary = "获取通道同步进度", security = @SecurityRequirement(name = JwtUtils.HEADER))
 	@Parameter(name = "deviceId", description = "设备国标编号", required = true)
 	public WVPResult<SyncStatus> getSyncStatus(@PathVariable String deviceId) {
 		SyncStatus channelSyncStatus = deviceService.getChannelSyncStatus(deviceId);
@@ -442,7 +444,7 @@ public class DeviceQuery {
 	}
 
 	@GetMapping("/{deviceId}/subscribe_info")
-	@Operation(summary = "获取设备的订阅状态")
+	@Operation(summary = "获取设备的订阅状态", security = @SecurityRequirement(name = JwtUtils.HEADER))
 	@Parameter(name = "deviceId", description = "设备国标编号", required = true)
 	public WVPResult<Map<String, Integer>> getSubscribeInfo(@PathVariable String deviceId) {
 		Set<String> allKeys = dynamicTask.getAllKeys();

+ 6 - 4
src/main/java/com/genersoft/iot/vmp/vmanager/gb28181/gbStream/GbStreamController.java

@@ -1,6 +1,7 @@
 package com.genersoft.iot.vmp.vmanager.gb28181.gbStream;
 
 import com.genersoft.iot.vmp.conf.exception.ControllerException;
+import com.genersoft.iot.vmp.conf.security.JwtUtils;
 import com.genersoft.iot.vmp.gb28181.bean.GbStream;
 import com.genersoft.iot.vmp.gb28181.bean.ParentPlatform;
 import com.genersoft.iot.vmp.service.IGbStreamService;
@@ -11,6 +12,7 @@ import com.genersoft.iot.vmp.vmanager.gb28181.gbStream.bean.GbStreamParam;
 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.security.SecurityRequirement;
 import io.swagger.v3.oas.annotations.tags.Tag;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -43,7 +45,7 @@ public class GbStreamController {
      * @param platformId 平台ID
      * @return
      */
-    @Operation(summary = "查询国标通道")
+    @Operation(summary = "查询国标通道", security = @SecurityRequirement(name = JwtUtils.HEADER))
     @Parameter(name = "page", description = "当前页", required = true)
     @Parameter(name = "count", description = "每页条数", required = true)
     @Parameter(name = "platformId", description = "平台ID", required = true)
@@ -79,7 +81,7 @@ public class GbStreamController {
      * @param gbStreamParam
      * @return
      */
-    @Operation(summary = "移除国标关联")
+    @Operation(summary = "移除国标关联", security = @SecurityRequirement(name = JwtUtils.HEADER))
     @DeleteMapping(value = "/del")
     @ResponseBody
     public void del(@RequestBody GbStreamParam gbStreamParam){
@@ -99,7 +101,7 @@ public class GbStreamController {
      * @param gbStreamParam
      * @return
      */
-    @Operation(summary = "保存国标关联")
+    @Operation(summary = "保存国标关联", security = @SecurityRequirement(name = JwtUtils.HEADER))
     @PostMapping(value = "/add")
     @ResponseBody
     public void add(@RequestBody GbStreamParam gbStreamParam){
@@ -118,7 +120,7 @@ public class GbStreamController {
      * @param gbId
      * @return
      */
-    @Operation(summary = "保存国标关联")
+    @Operation(summary = "保存国标关联", security = @SecurityRequirement(name = JwtUtils.HEADER))
     @GetMapping(value = "/addWithGbid")
     @ResponseBody
     public void add(String gbId, String platformGbId, @RequestParam(required = false) String catalogGbId){

+ 3 - 1
src/main/java/com/genersoft/iot/vmp/vmanager/gb28181/media/MediaController.java

@@ -2,6 +2,7 @@ package com.genersoft.iot.vmp.vmanager.gb28181.media;
 
 import com.genersoft.iot.vmp.common.StreamInfo;
 import com.genersoft.iot.vmp.conf.exception.ControllerException;
+import com.genersoft.iot.vmp.conf.security.JwtUtils;
 import com.genersoft.iot.vmp.conf.security.SecurityUtils;
 import com.genersoft.iot.vmp.conf.security.dto.LoginUser;
 import com.genersoft.iot.vmp.media.zlm.dto.StreamAuthorityInfo;
@@ -12,6 +13,7 @@ import com.genersoft.iot.vmp.vmanager.bean.ErrorCode;
 import com.genersoft.iot.vmp.vmanager.bean.StreamContent;
 import io.swagger.v3.oas.annotations.Operation;
 import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.security.SecurityRequirement;
 import io.swagger.v3.oas.annotations.tags.Tag;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -45,7 +47,7 @@ public class MediaController {
      * @param stream 流id
      * @return
      */
-    @Operation(summary = "根据应用名和流id获取播放地址")
+    @Operation(summary = "根据应用名和流id获取播放地址", security = @SecurityRequirement(name = JwtUtils.HEADER))
     @Parameter(name = "app", description = "应用名", required = true)
     @Parameter(name = "stream", description = "流id", required = true)
     @Parameter(name = "mediaServerId", description = "媒体服务器id")

+ 18 - 16
src/main/java/com/genersoft/iot/vmp/vmanager/gb28181/platform/PlatformController.java

@@ -6,6 +6,7 @@ import com.genersoft.iot.vmp.common.VideoManagerConstants;
 import com.genersoft.iot.vmp.conf.DynamicTask;
 import com.genersoft.iot.vmp.conf.UserSetting;
 import com.genersoft.iot.vmp.conf.exception.ControllerException;
+import com.genersoft.iot.vmp.conf.security.JwtUtils;
 import com.genersoft.iot.vmp.gb28181.bean.ParentPlatform;
 import com.genersoft.iot.vmp.gb28181.bean.ParentPlatformCatch;
 import com.genersoft.iot.vmp.gb28181.bean.PlatformCatalog;
@@ -21,6 +22,7 @@ import com.genersoft.iot.vmp.vmanager.gb28181.platform.bean.UpdateChannelParam;
 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.security.SecurityRequirement;
 import io.swagger.v3.oas.annotations.tags.Tag;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -83,7 +85,7 @@ public class PlatformController {
      *
      * @return
      */
-    @Operation(summary = "获取国标服务的配置")
+    @Operation(summary = "获取国标服务的配置", security = @SecurityRequirement(name = JwtUtils.HEADER))
     @GetMapping("/server_config")
     public JSONObject serverConfig() {
         JSONObject result = new JSONObject();
@@ -99,7 +101,7 @@ public class PlatformController {
      *
      * @return
      */
-    @Operation(summary = "获取级联服务器信息")
+    @Operation(summary = "获取级联服务器信息", security = @SecurityRequirement(name = JwtUtils.HEADER))
     @Parameter(name = "id", description = "平台国标编号", required = true)
     @GetMapping("/info/{id}")
     public ParentPlatform getPlatform(@PathVariable String id) {
@@ -119,7 +121,7 @@ public class PlatformController {
      * @return
      */
     @GetMapping("/query/{count}/{page}")
-    @Operation(summary = "分页查询级联平台")
+    @Operation(summary = "分页查询级联平台", security = @SecurityRequirement(name = JwtUtils.HEADER))
     @Parameter(name = "page", description = "当前页", required = true)
     @Parameter(name = "count", description = "每页条数", required = true)
     public PageInfo<ParentPlatform> platforms(@PathVariable int page, @PathVariable int count) {
@@ -140,7 +142,7 @@ public class PlatformController {
      * @param parentPlatform
      * @return
      */
-    @Operation(summary = "添加上级平台信息")
+    @Operation(summary = "添加上级平台信息", security = @SecurityRequirement(name = JwtUtils.HEADER))
     @PostMapping("/add")
     @ResponseBody
     public void addPlatform(@RequestBody ParentPlatform parentPlatform) {
@@ -185,7 +187,7 @@ public class PlatformController {
      * @param parentPlatform
      * @return
      */
-    @Operation(summary = "保存上级平台信息")
+    @Operation(summary = "保存上级平台信息", security = @SecurityRequirement(name = JwtUtils.HEADER))
     @PostMapping("/save")
     @ResponseBody
     public void savePlatform(@RequestBody ParentPlatform parentPlatform) {
@@ -216,7 +218,7 @@ public class PlatformController {
      * @param serverGBId 上级平台国标ID
      * @return
      */
-    @Operation(summary = "删除上级平台")
+    @Operation(summary = "删除上级平台", security = @SecurityRequirement(name = JwtUtils.HEADER))
     @Parameter(name = "serverGBId", description = "上级平台的国标编号")
     @DeleteMapping("/delete/{serverGBId}")
     @ResponseBody
@@ -273,7 +275,7 @@ public class PlatformController {
      * @param serverGBId 上级平台国标ID
      * @return
      */
-    @Operation(summary = "查询上级平台是否存在")
+    @Operation(summary = "查询上级平台是否存在", security = @SecurityRequirement(name = JwtUtils.HEADER))
     @Parameter(name = "serverGBId", description = "上级平台的国标编号")
     @GetMapping("/exit/{serverGBId}")
     @ResponseBody
@@ -294,7 +296,7 @@ public class PlatformController {
      * @param channelType 通道类型
      * @return
      */
-    @Operation(summary = "查询上级平台是否存在")
+    @Operation(summary = "查询上级平台是否存在", security = @SecurityRequirement(name = JwtUtils.HEADER))
     @Parameter(name = "page", description = "当前页", required = true)
     @Parameter(name = "count", description = "每页条数", required = true)
     @Parameter(name = "platformId", description = "上级平台的国标编号")
@@ -331,7 +333,7 @@ public class PlatformController {
      * @param param 通道关联参数
      * @return
      */
-    @Operation(summary = "向上级平台添加国标通道")
+    @Operation(summary = "向上级平台添加国标通道", security = @SecurityRequirement(name = JwtUtils.HEADER))
     @PostMapping("/update_channel_for_gb")
     @ResponseBody
     public void updateChannelForGB(@RequestBody UpdateChannelParam param) {
@@ -360,7 +362,7 @@ public class PlatformController {
      * @param param 通道关联参数
      * @return
      */
-    @Operation(summary = "从上级平台移除国标通道")
+    @Operation(summary = "从上级平台移除国标通道", security = @SecurityRequirement(name = JwtUtils.HEADER))
     @DeleteMapping("/del_channel_for_gb")
     @ResponseBody
     public void delChannelForGB(@RequestBody UpdateChannelParam param) {
@@ -389,7 +391,7 @@ public class PlatformController {
      * @param parentId   目录父ID
      * @return
      */
-    @Operation(summary = "获取目录")
+    @Operation(summary = "获取目录", security = @SecurityRequirement(name = JwtUtils.HEADER))
     @Parameter(name = "platformId", description = "上级平台的国标编号", required = true)
     @Parameter(name = "parentId", description = "父级目录的国标编号", required = true)
     @GetMapping("/catalog")
@@ -420,7 +422,7 @@ public class PlatformController {
      * @param platformCatalog 目录
      * @return
      */
-    @Operation(summary = "添加目录")
+    @Operation(summary = "添加目录", security = @SecurityRequirement(name = JwtUtils.HEADER))
     @PostMapping("/catalog/add")
     @ResponseBody
     public void addCatalog(@RequestBody PlatformCatalog platformCatalog) {
@@ -445,7 +447,7 @@ public class PlatformController {
      * @param platformCatalog 目录
      * @return
      */
-    @Operation(summary = "编辑目录")
+    @Operation(summary = "编辑目录", security = @SecurityRequirement(name = JwtUtils.HEADER))
     @PostMapping("/catalog/edit")
     @ResponseBody
     public void editCatalog(@RequestBody PlatformCatalog platformCatalog) {
@@ -471,7 +473,7 @@ public class PlatformController {
      * @param platformId 平台Id
      * @return
      */
-    @Operation(summary = "删除目录")
+    @Operation(summary = "删除目录", security = @SecurityRequirement(name = JwtUtils.HEADER))
     @Parameter(name = "id", description = "目录Id", required = true)
     @Parameter(name = "platformId", description = "平台Id", required = true)
     @DeleteMapping("/catalog/del")
@@ -506,7 +508,7 @@ public class PlatformController {
      * @param platformCatalog 关联的信息
      * @return
      */
-    @Operation(summary = "删除关联")
+    @Operation(summary = "删除关联", security = @SecurityRequirement(name = JwtUtils.HEADER))
     @DeleteMapping("/catalog/relation/del")
     @ResponseBody
     public void delRelation(@RequestBody PlatformCatalog platformCatalog) {
@@ -529,7 +531,7 @@ public class PlatformController {
      * @param catalogId  目录Id
      * @return
      */
-    @Operation(summary = "修改默认目录")
+    @Operation(summary = "修改默认目录", security = @SecurityRequirement(name = JwtUtils.HEADER))
     @Parameter(name = "catalogId", description = "目录Id", required = true)
     @Parameter(name = "platformId", description = "平台Id", required = true)
     @PostMapping("/catalog/default/update")

+ 9 - 7
src/main/java/com/genersoft/iot/vmp/vmanager/gb28181/play/PlayController.java

@@ -9,6 +9,7 @@ import com.genersoft.iot.vmp.common.StreamInfo;
 import com.genersoft.iot.vmp.conf.UserSetting;
 import com.genersoft.iot.vmp.conf.exception.ControllerException;
 import com.genersoft.iot.vmp.conf.exception.SsrcTransactionNotFoundException;
+import com.genersoft.iot.vmp.conf.security.JwtUtils;
 import com.genersoft.iot.vmp.gb28181.bean.Device;
 import com.genersoft.iot.vmp.gb28181.bean.SsrcTransaction;
 import com.genersoft.iot.vmp.gb28181.session.VideoStreamSessionManager;
@@ -31,6 +32,7 @@ import com.genersoft.iot.vmp.vmanager.bean.StreamContent;
 import com.genersoft.iot.vmp.vmanager.bean.WVPResult;
 import io.swagger.v3.oas.annotations.Operation;
 import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.security.SecurityRequirement;
 import io.swagger.v3.oas.annotations.tags.Tag;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -92,7 +94,7 @@ public class PlayController {
 	@Autowired
 	private UserSetting userSetting;
 
-	@Operation(summary = "开始点播")
+	@Operation(summary = "开始点播", security = @SecurityRequirement(name = JwtUtils.HEADER))
 	@Parameter(name = "deviceId", description = "设备国标编号", required = true)
 	@Parameter(name = "channelId", description = "通道国标编号", required = true)
 	@GetMapping("/start/{deviceId}/{channelId}")
@@ -157,7 +159,7 @@ public class PlayController {
 		return result;
 	}
 
-	@Operation(summary = "停止点播")
+	@Operation(summary = "停止点播", security = @SecurityRequirement(name = JwtUtils.HEADER))
 	@Parameter(name = "deviceId", description = "设备国标编号", required = true)
 	@Parameter(name = "channelId", description = "通道国标编号", required = true)
 	@Parameter(name = "isSubStream", description = "是否子码流(true-子码流,false-主码流),默认为false", required = true)
@@ -202,7 +204,7 @@ public class PlayController {
 	 * 将不是h264的视频通过ffmpeg 转码为h264 + aac
 	 * @param streamId 流ID
 	 */
-	@Operation(summary = "将不是h264的视频通过ffmpeg 转码为h264 + aac")
+	@Operation(summary = "将不是h264的视频通过ffmpeg 转码为h264 + aac", security = @SecurityRequirement(name = JwtUtils.HEADER))
 	@Parameter(name = "streamId", description = "视频流ID", required = true)
 	@PostMapping("/convert/{streamId}")
 	public JSONObject playConvert(@PathVariable String streamId) {
@@ -244,7 +246,7 @@ public class PlayController {
 	/**
 	 * 结束转码
 	 */
-	@Operation(summary = "结束转码")
+	@Operation(summary = "结束转码", security = @SecurityRequirement(name = JwtUtils.HEADER))
 	@Parameter(name = "key", description = "视频流key", required = true)
 	@Parameter(name = "mediaServerId", description = "流媒体服务ID", required = true)
 	@PostMapping("/convertStop/{key}")
@@ -269,7 +271,7 @@ public class PlayController {
 		}
 	}
 
-	@Operation(summary = "语音广播命令")
+	@Operation(summary = "语音广播命令", security = @SecurityRequirement(name = JwtUtils.HEADER))
 	@Parameter(name = "deviceId", description = "设备国标编号", required = true)
 	@Parameter(name = "deviceId", description = "通道国标编号", required = true)
 	@Parameter(name = "timeout", description = "推流超时时间(秒)", required = true)
@@ -309,7 +311,7 @@ public class PlayController {
 		playService.stopAudioBroadcast(deviceId, channelId);
 	}
 
-	@Operation(summary = "获取所有的ssrc")
+	@Operation(summary = "获取所有的ssrc", security = @SecurityRequirement(name = JwtUtils.HEADER))
 	@GetMapping("/ssrc")
 	public JSONObject getSSRC() {
 		if (logger.isDebugEnabled()) {
@@ -332,7 +334,7 @@ public class PlayController {
 		return jsonObject;
 	}
 
-	@Operation(summary = "获取截图")
+	@Operation(summary = "获取截图", security = @SecurityRequirement(name = JwtUtils.HEADER))
 	@Parameter(name = "deviceId", description = "设备国标编号", required = true)
 	@Parameter(name = "channelId", description = "通道国标编号", required = true)
 	@Parameter(name = "isSubStream", description = "是否子码流(true-子码流,false-主码流),默认为false", required = true)

+ 8 - 6
src/main/java/com/genersoft/iot/vmp/vmanager/gb28181/playback/PlaybackController.java

@@ -7,6 +7,7 @@ import com.genersoft.iot.vmp.conf.UserSetting;
 import com.genersoft.iot.vmp.conf.exception.ControllerException;
 import com.genersoft.iot.vmp.conf.exception.ServiceException;
 import com.genersoft.iot.vmp.conf.exception.SsrcTransactionNotFoundException;
+import com.genersoft.iot.vmp.conf.security.JwtUtils;
 import com.genersoft.iot.vmp.gb28181.bean.Device;
 import com.genersoft.iot.vmp.gb28181.transmit.callback.DeferredResultHolder;
 import com.genersoft.iot.vmp.gb28181.transmit.callback.RequestMessage;
@@ -20,6 +21,7 @@ import com.genersoft.iot.vmp.vmanager.bean.StreamContent;
 import com.genersoft.iot.vmp.vmanager.bean.WVPResult;
 import io.swagger.v3.oas.annotations.Operation;
 import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.security.SecurityRequirement;
 import io.swagger.v3.oas.annotations.tags.Tag;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -68,7 +70,7 @@ public class PlaybackController {
 	@Autowired
 	private UserSetting userSetting;
 
-	@Operation(summary = "开始视频回放")
+	@Operation(summary = "开始视频回放", security = @SecurityRequirement(name = JwtUtils.HEADER))
 	@Parameter(name = "deviceId", description = "设备国标编号", required = true)
 	@Parameter(name = "channelId", description = "通道国标编号", required = true)
 	@Parameter(name = "startTime", description = "开始时间", required = true)
@@ -125,7 +127,7 @@ public class PlaybackController {
 	}
 
 
-	@Operation(summary = "停止视频回放")
+	@Operation(summary = "停止视频回放", security = @SecurityRequirement(name = JwtUtils.HEADER))
 	@Parameter(name = "deviceId", description = "设备国标编号", required = true)
 	@Parameter(name = "channelId", description = "通道国标编号", required = true)
 	@Parameter(name = "stream", description = "流ID", required = true)
@@ -149,7 +151,7 @@ public class PlaybackController {
 	}
 
 
-	@Operation(summary = "回放暂停")
+	@Operation(summary = "回放暂停", security = @SecurityRequirement(name = JwtUtils.HEADER))
 	@Parameter(name = "streamId", description = "回放流ID", required = true)
 	@GetMapping("/pause/{streamId}")
 	public void playPause(@PathVariable String streamId) {
@@ -165,7 +167,7 @@ public class PlaybackController {
 	}
 
 
-	@Operation(summary = "回放恢复")
+	@Operation(summary = "回放恢复", security = @SecurityRequirement(name = JwtUtils.HEADER))
 	@Parameter(name = "streamId", description = "回放流ID", required = true)
 	@GetMapping("/resume/{streamId}")
 	public void playResume(@PathVariable String streamId) {
@@ -180,7 +182,7 @@ public class PlaybackController {
 	}
 
 
-	@Operation(summary = "回放拖动播放")
+	@Operation(summary = "回放拖动播放", security = @SecurityRequirement(name = JwtUtils.HEADER))
 	@Parameter(name = "streamId", description = "回放流ID", required = true)
 	@Parameter(name = "seekTime", description = "拖动偏移量,单位s", required = true)
 	@GetMapping("/seek/{streamId}/{seekTime}")
@@ -200,7 +202,7 @@ public class PlaybackController {
 		}
 	}
 
-	@Operation(summary = "回放倍速播放")
+	@Operation(summary = "回放倍速播放", security = @SecurityRequirement(name = JwtUtils.HEADER))
 	@Parameter(name = "streamId", description = "回放流ID", required = true)
 	@Parameter(name = "speed", description = "倍速0.25 0.5 1、2、4", required = true)
 	@GetMapping("/speed/{streamId}/{speed}")

+ 5 - 3
src/main/java/com/genersoft/iot/vmp/vmanager/gb28181/ptz/PtzController.java

@@ -2,6 +2,7 @@ package com.genersoft.iot.vmp.vmanager.gb28181.ptz;
 
 
 import com.genersoft.iot.vmp.conf.exception.ControllerException;
+import com.genersoft.iot.vmp.conf.security.JwtUtils;
 import com.genersoft.iot.vmp.gb28181.bean.Device;
 import com.genersoft.iot.vmp.gb28181.transmit.callback.DeferredResultHolder;
 import com.genersoft.iot.vmp.gb28181.transmit.callback.RequestMessage;
@@ -10,6 +11,7 @@ import com.genersoft.iot.vmp.storager.IVideoManagerStorage;
 import com.genersoft.iot.vmp.vmanager.bean.ErrorCode;
 import io.swagger.v3.oas.annotations.Operation;
 import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.security.SecurityRequirement;
 import io.swagger.v3.oas.annotations.tags.Tag;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -50,7 +52,7 @@ public class PtzController {
 	 * @param zoomSpeed	    缩放速度
 	 */
 
-	@Operation(summary = "云台控制")
+	@Operation(summary = "云台控制", security = @SecurityRequirement(name = JwtUtils.HEADER))
 	@Parameter(name = "deviceId", description = "设备国标编号", required = true)
 	@Parameter(name = "channelId", description = "通道国标编号", required = true)
 	@Parameter(name = "command", description = "控制指令,允许值: left, right, up, down, upleft, upright, downleft, downright, zoomin, zoomout, stop", required = true)
@@ -113,7 +115,7 @@ public class PtzController {
 	}
 
 
-	@Operation(summary = "通用前端控制命令")
+	@Operation(summary = "通用前端控制命令", security = @SecurityRequirement(name = JwtUtils.HEADER))
 	@Parameter(name = "deviceId", description = "设备国标编号", required = true)
 	@Parameter(name = "channelId", description = "通道国标编号", required = true)
 	@Parameter(name = "cmdCode", description = "指令码", required = true)
@@ -137,7 +139,7 @@ public class PtzController {
 	}
 
 
-	@Operation(summary = "预置位查询")
+	@Operation(summary = "预置位查询", security = @SecurityRequirement(name = JwtUtils.HEADER))
 	@Parameter(name = "deviceId", description = "设备国标编号", required = true)
 	@Parameter(name = "channelId", description = "通道国标编号", required = true)
 	@GetMapping("/preset/query/{deviceId}/{channelId}")

+ 14 - 4
src/main/java/com/genersoft/iot/vmp/vmanager/gb28181/record/GBRecordController.java

@@ -1,16 +1,21 @@
 package com.genersoft.iot.vmp.vmanager.gb28181.record;
 
+import com.genersoft.iot.vmp.common.InviteInfo;
+import com.genersoft.iot.vmp.common.InviteSessionType;
 import com.genersoft.iot.vmp.common.StreamInfo;
 import com.genersoft.iot.vmp.conf.UserSetting;
 import com.genersoft.iot.vmp.conf.exception.ControllerException;
 import com.genersoft.iot.vmp.conf.exception.SsrcTransactionNotFoundException;
+import com.genersoft.iot.vmp.conf.security.JwtUtils;
 import com.genersoft.iot.vmp.gb28181.bean.Device;
 import com.genersoft.iot.vmp.gb28181.bean.RecordInfo;
 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.service.IDeviceService;
+import com.genersoft.iot.vmp.service.IInviteStreamService;
 import com.genersoft.iot.vmp.service.IPlayService;
+import com.genersoft.iot.vmp.service.bean.DownloadFileInfo;
 import com.genersoft.iot.vmp.service.bean.InviteErrorCode;
 import com.genersoft.iot.vmp.storager.IVideoManagerStorage;
 import com.genersoft.iot.vmp.utils.DateUtil;
@@ -19,10 +24,12 @@ import com.genersoft.iot.vmp.vmanager.bean.StreamContent;
 import com.genersoft.iot.vmp.vmanager.bean.WVPResult;
 import io.swagger.v3.oas.annotations.Operation;
 import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.security.SecurityRequirement;
 import io.swagger.v3.oas.annotations.tags.Tag;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.util.ObjectUtils;
 import org.springframework.web.bind.annotation.GetMapping;
 import org.springframework.web.bind.annotation.PathVariable;
 import org.springframework.web.bind.annotation.RequestMapping;
@@ -55,13 +62,16 @@ public class GBRecordController {
 	@Autowired
 	private IPlayService playService;
 
+	@Autowired
+	private IInviteStreamService inviteStreamService;
+
 	@Autowired
 	private IDeviceService deviceService;
 
 	@Autowired
 	private UserSetting userSetting;
 
-	@Operation(summary = "录像查询")
+	@Operation(summary = "录像查询", security = @SecurityRequirement(name = JwtUtils.HEADER))
 	@Parameter(name = "deviceId", description = "设备国标编号", required = true)
 	@Parameter(name = "channelId", description = "通道国标编号", required = true)
 	@Parameter(name = "startTime", description = "开始时间", required = true)
@@ -115,7 +125,7 @@ public class GBRecordController {
 	}
 
 
-	@Operation(summary = "开始历史媒体下载")
+	@Operation(summary = "开始历史媒体下载", security = @SecurityRequirement(name = JwtUtils.HEADER))
 	@Parameter(name = "deviceId", description = "设备国标编号", required = true)
 	@Parameter(name = "channelId", description = "通道国标编号", required = true)
 	@Parameter(name = "startTime", description = "开始时间", required = true)
@@ -164,7 +174,7 @@ public class GBRecordController {
 		return result;
 	}
 
-	@Operation(summary = "停止历史媒体下载")
+	@Operation(summary = "停止历史媒体下载", security = @SecurityRequirement(name = JwtUtils.HEADER))
 	@Parameter(name = "deviceId", description = "设备国标编号", required = true)
 	@Parameter(name = "channelId", description = "通道国标编号", required = true)
 	@Parameter(name = "stream", description = "流ID", required = true)
@@ -192,7 +202,7 @@ public class GBRecordController {
 		}
 	}
 
-	@Operation(summary = "获取历史媒体下载进度")
+	@Operation(summary = "获取历史媒体下载进度", security = @SecurityRequirement(name = JwtUtils.HEADER))
 	@Parameter(name = "deviceId", description = "设备国标编号", required = true)
 	@Parameter(name = "channelId", description = "通道国标编号", required = true)
 	@Parameter(name = "stream", description = "流ID", required = true)

+ 4 - 2
src/main/java/com/genersoft/iot/vmp/vmanager/log/LogController.java

@@ -2,6 +2,7 @@ package com.genersoft.iot.vmp.vmanager.log;
 
 import com.genersoft.iot.vmp.conf.UserSetting;
 import com.genersoft.iot.vmp.conf.exception.ControllerException;
+import com.genersoft.iot.vmp.conf.security.JwtUtils;
 import com.genersoft.iot.vmp.service.ILogService;
 import com.genersoft.iot.vmp.storager.dao.dto.LogDto;
 import com.genersoft.iot.vmp.utils.DateUtil;
@@ -9,6 +10,7 @@ import com.genersoft.iot.vmp.vmanager.bean.ErrorCode;
 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.security.SecurityRequirement;
 import io.swagger.v3.oas.annotations.tags.Tag;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -42,7 +44,7 @@ public class LogController {
      * @return
      */
     @GetMapping("/all")
-    @Operation(summary = "分页查询日志")
+    @Operation(summary = "分页查询日志", security = @SecurityRequirement(name = JwtUtils.HEADER))
     @Parameter(name = "query", description = "查询内容", required = true)
     @Parameter(name = "page", description = "当前页", required = true)
     @Parameter(name = "count", description = "每页查询数量", required = true)
@@ -84,7 +86,7 @@ public class LogController {
      *  清空日志
      *
      */
-    @Operation(summary = "清空日志")
+    @Operation(summary = "清空日志", security = @SecurityRequirement(name = JwtUtils.HEADER))
     @DeleteMapping("/clear")
     public void clear() {
         logService.clear();

+ 5 - 3
src/main/java/com/genersoft/iot/vmp/vmanager/ps/PsController.java

@@ -5,6 +5,7 @@ import com.genersoft.iot.vmp.common.VideoManagerConstants;
 import com.genersoft.iot.vmp.conf.DynamicTask;
 import com.genersoft.iot.vmp.conf.UserSetting;
 import com.genersoft.iot.vmp.conf.exception.ControllerException;
+import com.genersoft.iot.vmp.conf.security.JwtUtils;
 import com.genersoft.iot.vmp.media.zlm.SendRtpPortManager;
 import com.genersoft.iot.vmp.media.zlm.ZLMServerFactory;
 import com.genersoft.iot.vmp.media.zlm.ZlmHttpHookSubscribe;
@@ -19,6 +20,7 @@ import com.genersoft.iot.vmp.vmanager.bean.ErrorCode;
 import com.genersoft.iot.vmp.vmanager.bean.OtherPsSendInfo;
 import io.swagger.v3.oas.annotations.Operation;
 import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.security.SecurityRequirement;
 import io.swagger.v3.oas.annotations.tags.Tag;
 import okhttp3.OkHttpClient;
 import okhttp3.Request;
@@ -69,7 +71,7 @@ public class PsController {
 
     @GetMapping(value = "/receive/open")
     @ResponseBody
-    @Operation(summary = "开启收流和获取发流信息")
+    @Operation(summary = "开启收流和获取发流信息", security = @SecurityRequirement(name = JwtUtils.HEADER))
     @Parameter(name = "isSend", description = "是否发送,false时只开启收流, true同时返回推流信息", required = true)
     @Parameter(name = "callId", description = "整个过程的唯一标识,为了与后续接口关联", required = true)
     @Parameter(name = "ssrc", description = "来源流的SSRC,不传则不校验来源ssrc", required = false)
@@ -152,7 +154,7 @@ public class PsController {
 
     @GetMapping(value = "/receive/close")
     @ResponseBody
-    @Operation(summary = "关闭收流")
+    @Operation(summary = "关闭收流", security = @SecurityRequirement(name = JwtUtils.HEADER))
     @Parameter(name = "stream", description = "流的ID", required = true)
     public void closeRtpServer(String stream) {
         logger.info("[第三方PS服务对接->关闭收流] stream->{}", stream);
@@ -170,7 +172,7 @@ public class PsController {
 
     @GetMapping(value = "/send/start")
     @ResponseBody
-    @Operation(summary = "发送流")
+    @Operation(summary = "发送流", security = @SecurityRequirement(name = JwtUtils.HEADER))
     @Parameter(name = "ssrc", description = "发送流的SSRC", required = true)
     @Parameter(name = "dstIp", description = "目标收流IP", required = true)
     @Parameter(name = "dstPort", description = "目标收流端口", required = true)

+ 0 - 51
src/main/java/com/genersoft/iot/vmp/vmanager/record/RecordController.java

@@ -1,51 +0,0 @@
-//package com.genersoft.iot.vmp.vmanager.record;
-//
-//import com.alibaba.fastjson2.JSONObject;
-//import com.genersoft.iot.vmp.media.zlm.dto.StreamPushItem;
-//import com.genersoft.iot.vmp.service.IRecordInfoServer;
-//import com.genersoft.iot.vmp.storager.dao.dto.RecordInfo;
-//import com.genersoft.iot.vmp.vmanager.bean.WVPResult;
-//import com.github.pagehelper.PageInfo;
-//import io.swagger.annotations.Api;
-//import io.swagger.annotations.ApiImplicitParam;
-//import io.swagger.annotations.ApiImplicitParams;
-//import io.swagger.annotations.ApiOperation;
-//import org.springframework.beans.factory.annotation.Autowired;
-//import org.springframework.web.bind.annotation.*;
-//
-//@Tag(name  = "云端录像")
-//
-//@RestController
-//@RequestMapping("/api/record")
-//public class RecordController {
-//
-//    @Autowired
-//    private IRecordInfoServer recordInfoServer;
-//
-//     //@ApiOperation("录像列表查询")
-//    @ApiImplicitParams({
-//            @ApiImplicitParam(name="page", value = "当前页", required = true, dataTypeClass = Integer.class),
-//            @ApiImplicitParam(name="count", value = "每页查询数量", required = true, dataTypeClass = Integer.class),
-//            @ApiImplicitParam(name="query", value = "查询内容", dataTypeClass = String.class),
-//    })
-//    @GetMapping(value = "/app/list")
-//    @ResponseBody
-//    public Object list(@RequestParam(required = false)Integer page,
-//                                     @RequestParam(required = false)Integer count ){
-//
-//        PageInfo<RecordInfo> recordList = recordInfoServer.getRecordList(page - 1, page - 1 + count);
-//        return recordList;
-//    }
-//
-//     //@ApiOperation("获取录像详情")
-//    @ApiImplicitParams({
-//            @ApiImplicitParam(name="recordInfo", value = "录像记录", required = true, dataTypeClass = RecordInfo.class)
-//    })
-//    @GetMapping(value = "/detail")
-//    @ResponseBody
-//    public JSONObject list(RecordInfo recordInfo, String time ){
-//
-//
-//        return null;
-//    }
-//}

+ 6 - 4
src/main/java/com/genersoft/iot/vmp/vmanager/rtp/RtpController.java

@@ -5,6 +5,7 @@ import com.genersoft.iot.vmp.common.VideoManagerConstants;
 import com.genersoft.iot.vmp.conf.DynamicTask;
 import com.genersoft.iot.vmp.conf.UserSetting;
 import com.genersoft.iot.vmp.conf.exception.ControllerException;
+import com.genersoft.iot.vmp.conf.security.JwtUtils;
 import com.genersoft.iot.vmp.media.zlm.SendRtpPortManager;
 import com.genersoft.iot.vmp.media.zlm.ZLMServerFactory;
 import com.genersoft.iot.vmp.media.zlm.ZlmHttpHookSubscribe;
@@ -19,6 +20,7 @@ import com.genersoft.iot.vmp.vmanager.bean.ErrorCode;
 import com.genersoft.iot.vmp.vmanager.bean.OtherRtpSendInfo;
 import io.swagger.v3.oas.annotations.Operation;
 import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.security.SecurityRequirement;
 import io.swagger.v3.oas.annotations.tags.Tag;
 import okhttp3.OkHttpClient;
 import okhttp3.Request;
@@ -69,7 +71,7 @@ public class RtpController {
 
     @GetMapping(value = "/receive/open")
     @ResponseBody
-    @Operation(summary = "开启收流和获取发流信息")
+    @Operation(summary = "开启收流和获取发流信息", security = @SecurityRequirement(name = JwtUtils.HEADER))
     @Parameter(name = "isSend", description = "是否发送,false时只开启收流, true同时返回推流信息", required = true)
     @Parameter(name = "callId", description = "整个过程的唯一标识,为了与后续接口关联", required = true)
     @Parameter(name = "ssrc", description = "来源流的SSRC,不传则不校验来源ssrc", required = false)
@@ -156,7 +158,7 @@ public class RtpController {
 
     @GetMapping(value = "/receive/close")
     @ResponseBody
-    @Operation(summary = "关闭收流")
+    @Operation(summary = "关闭收流", security = @SecurityRequirement(name = JwtUtils.HEADER))
     @Parameter(name = "stream", description = "流的ID", required = true)
     public void closeRtpServer(String stream) {
         logger.info("[第三方服务对接->关闭收流] stream->{}", stream);
@@ -175,7 +177,7 @@ public class RtpController {
 
     @GetMapping(value = "/send/start")
     @ResponseBody
-    @Operation(summary = "发送流")
+    @Operation(summary = "发送流", security = @SecurityRequirement(name = JwtUtils.HEADER))
     @Parameter(name = "ssrc", description = "发送流的SSRC", required = true)
     @Parameter(name = "dstIpForAudio", description = "目标音频收流IP", required = false)
     @Parameter(name = "dstIpForVideo", description = "目标视频收流IP", required = false)
@@ -351,7 +353,7 @@ public class RtpController {
 
     @GetMapping(value = "/send/stop")
     @ResponseBody
-    @Operation(summary = "关闭发送流")
+    @Operation(summary = "关闭发送流", security = @SecurityRequirement(name = JwtUtils.HEADER))
     @Parameter(name = "callId", description = "整个过程的唯一标识,不传则使用随机端口发流", required = true)
     public void closeSendRTP(String callId) {
         logger.info("[第三方服务对接->关闭发送流] callId->{}", callId);

+ 12 - 10
src/main/java/com/genersoft/iot/vmp/vmanager/server/ServerController.java

@@ -8,6 +8,7 @@ import com.genersoft.iot.vmp.conf.SipConfig;
 import com.genersoft.iot.vmp.conf.UserSetting;
 import com.genersoft.iot.vmp.conf.VersionInfo;
 import com.genersoft.iot.vmp.conf.exception.ControllerException;
+import com.genersoft.iot.vmp.conf.security.JwtUtils;
 import com.genersoft.iot.vmp.media.zlm.SendRtpPortManager;
 import com.genersoft.iot.vmp.media.zlm.ZlmHttpHookSubscribe;
 import com.genersoft.iot.vmp.media.zlm.dto.IHookSubscribe;
@@ -21,6 +22,7 @@ import com.genersoft.iot.vmp.vmanager.bean.ResourceInfo;
 import com.genersoft.iot.vmp.vmanager.bean.SystemConfigInfo;
 import io.swagger.v3.oas.annotations.Operation;
 import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.security.SecurityRequirement;
 import io.swagger.v3.oas.annotations.tags.Tag;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.beans.factory.annotation.Value;
@@ -79,27 +81,27 @@ public class ServerController {
 
     @GetMapping(value = "/media_server/list")
     @ResponseBody
-    @Operation(summary = "流媒体服务列表")
+    @Operation(summary = "流媒体服务列表", security = @SecurityRequirement(name = JwtUtils.HEADER))
     public List<MediaServerItem> getMediaServerList() {
         return mediaServerService.getAll();
     }
 
     @GetMapping(value = "/media_server/online/list")
     @ResponseBody
-    @Operation(summary = "在线流媒体服务列表")
+    @Operation(summary = "在线流媒体服务列表", security = @SecurityRequirement(name = JwtUtils.HEADER))
     public List<MediaServerItem> getOnlineMediaServerList() {
         return mediaServerService.getAllOnline();
     }
 
     @GetMapping(value = "/media_server/one/{id}")
     @ResponseBody
-    @Operation(summary = "停止视频回放")
+    @Operation(summary = "停止视频回放", security = @SecurityRequirement(name = JwtUtils.HEADER))
     @Parameter(name = "id", description = "流媒体服务ID", required = true)
     public MediaServerItem getMediaServer(@PathVariable String id) {
         return mediaServerService.getOne(id);
     }
 
-    @Operation(summary = "测试流媒体服务")
+    @Operation(summary = "测试流媒体服务", security = @SecurityRequirement(name = JwtUtils.HEADER))
     @Parameter(name = "ip", description = "流媒体服务IP", required = true)
     @Parameter(name = "port", description = "流媒体服务HTT端口", required = true)
     @Parameter(name = "secret", description = "流媒体服务secret", required = true)
@@ -109,7 +111,7 @@ public class ServerController {
         return mediaServerService.checkMediaServer(ip, port, secret);
     }
 
-    @Operation(summary = "测试流媒体录像管理服务")
+    @Operation(summary = "测试流媒体录像管理服务", security = @SecurityRequirement(name = JwtUtils.HEADER))
     @Parameter(name = "ip", description = "流媒体服务IP", required = true)
     @Parameter(name = "port", description = "流媒体服务HTT端口", required = true)
     @GetMapping(value = "/media_server/record/check")
@@ -121,7 +123,7 @@ public class ServerController {
         }
     }
 
-    @Operation(summary = "保存流媒体服务")
+    @Operation(summary = "保存流媒体服务", security = @SecurityRequirement(name = JwtUtils.HEADER))
     @Parameter(name = "mediaServerItem", description = "流媒体信息", required = true)
     @PostMapping(value = "/media_server/save")
     @ResponseBody
@@ -135,7 +137,7 @@ public class ServerController {
         }
     }
 
-    @Operation(summary = "移除流媒体服务")
+    @Operation(summary = "移除流媒体服务", security = @SecurityRequirement(name = JwtUtils.HEADER))
     @Parameter(name = "id", description = "流媒体ID", required = true)
     @DeleteMapping(value = "/media_server/delete")
     @ResponseBody
@@ -148,7 +150,7 @@ public class ServerController {
     }
 
 
-    @Operation(summary = "重启服务")
+    @Operation(summary = "重启服务", security = @SecurityRequirement(name = JwtUtils.HEADER))
     @GetMapping(value = "/restart")
     @ResponseBody
     public void restart() {
@@ -173,7 +175,7 @@ public class ServerController {
 //        });
     };
 
-    @Operation(summary = "获取系统信息信息")
+    @Operation(summary = "获取系统信息信息", security = @SecurityRequirement(name = JwtUtils.HEADER))
     @GetMapping(value = "/system/configInfo")
     @ResponseBody
     public SystemConfigInfo getConfigInfo() {
@@ -185,7 +187,7 @@ public class ServerController {
         return systemConfigInfo;
     }
 
-    @Operation(summary = "获取版本信息")
+    @Operation(summary = "获取版本信息", security = @SecurityRequirement(name = JwtUtils.HEADER))
     @GetMapping(value = "/version")
     @ResponseBody
     public VersionPo VersionPogetVersion() {

+ 9 - 7
src/main/java/com/genersoft/iot/vmp/vmanager/streamProxy/StreamProxyController.java

@@ -4,6 +4,7 @@ import com.alibaba.fastjson2.JSONObject;
 import com.genersoft.iot.vmp.common.StreamInfo;
 import com.genersoft.iot.vmp.conf.UserSetting;
 import com.genersoft.iot.vmp.conf.exception.ControllerException;
+import com.genersoft.iot.vmp.conf.security.JwtUtils;
 import com.genersoft.iot.vmp.gb28181.transmit.callback.DeferredResultHolder;
 import com.genersoft.iot.vmp.gb28181.transmit.callback.RequestMessage;
 import com.genersoft.iot.vmp.media.zlm.dto.MediaServerItem;
@@ -16,6 +17,7 @@ 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.security.SecurityRequirement;
 import io.swagger.v3.oas.annotations.tags.Tag;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -52,7 +54,7 @@ public class StreamProxyController {
     private UserSetting userSetting;
 
 
-    @Operation(summary = "分页查询流代理")
+    @Operation(summary = "分页查询流代理", security = @SecurityRequirement(name = JwtUtils.HEADER))
     @Parameter(name = "page", description = "当前页")
     @Parameter(name = "count", description = "每页查询数量")
     @Parameter(name = "query", description = "查询内容")
@@ -67,7 +69,7 @@ public class StreamProxyController {
         return streamProxyService.getAll(page, count);
     }
 
-    @Operation(summary = "查询流代理")
+    @Operation(summary = "查询流代理", security = @SecurityRequirement(name = JwtUtils.HEADER))
     @Parameter(name = "app", description = "应用名")
     @Parameter(name = "stream", description = "流Id")
     @GetMapping(value = "/one")
@@ -77,7 +79,7 @@ public class StreamProxyController {
         return streamProxyService.getStreamProxyByAppAndStream(app, stream);
     }
 
-    @Operation(summary = "保存代理", parameters = {
+    @Operation(summary = "保存代理", security = @SecurityRequirement(name = JwtUtils.HEADER), parameters = {
             @Parameter(name = "param", description = "代理参数", required = true),
     })
     @PostMapping(value = "/save")
@@ -131,7 +133,7 @@ public class StreamProxyController {
 
     @GetMapping(value = "/ffmpeg_cmd/list")
     @ResponseBody
-    @Operation(summary = "获取ffmpeg.cmd模板")
+    @Operation(summary = "获取ffmpeg.cmd模板", security = @SecurityRequirement(name = JwtUtils.HEADER))
     @Parameter(name = "mediaServerId", description = "流媒体ID", required = true)
     public JSONObject getFFmpegCMDs(@RequestParam String mediaServerId){
         logger.debug("获取节点[ {} ]ffmpeg.cmd模板", mediaServerId );
@@ -145,7 +147,7 @@ public class StreamProxyController {
 
     @DeleteMapping(value = "/del")
     @ResponseBody
-    @Operation(summary = "移除代理")
+    @Operation(summary = "移除代理", security = @SecurityRequirement(name = JwtUtils.HEADER))
     @Parameter(name = "app", description = "应用名", required = true)
     @Parameter(name = "stream", description = "流id", required = true)
     public void del(@RequestParam String app, @RequestParam String stream){
@@ -159,7 +161,7 @@ public class StreamProxyController {
 
     @GetMapping(value = "/start")
     @ResponseBody
-    @Operation(summary = "启用代理")
+    @Operation(summary = "启用代理", security = @SecurityRequirement(name = JwtUtils.HEADER))
     @Parameter(name = "app", description = "应用名", required = true)
     @Parameter(name = "stream", description = "流id", required = true)
     public void start(String app, String stream){
@@ -172,7 +174,7 @@ public class StreamProxyController {
 
     @GetMapping(value = "/stop")
     @ResponseBody
-    @Operation(summary = "停用代理")
+    @Operation(summary = "停用代理", security = @SecurityRequirement(name = JwtUtils.HEADER))
     @Parameter(name = "app", description = "应用名", required = true)
     @Parameter(name = "stream", description = "流id", required = true)
     public void stop(String app, String stream){

+ 9 - 7
src/main/java/com/genersoft/iot/vmp/vmanager/streamPush/StreamPushController.java

@@ -6,6 +6,7 @@ import com.alibaba.excel.read.metadata.ReadSheet;
 import com.genersoft.iot.vmp.common.StreamInfo;
 import com.genersoft.iot.vmp.conf.UserSetting;
 import com.genersoft.iot.vmp.conf.exception.ControllerException;
+import com.genersoft.iot.vmp.conf.security.JwtUtils;
 import com.genersoft.iot.vmp.conf.security.SecurityUtils;
 import com.genersoft.iot.vmp.conf.security.dto.LoginUser;
 import com.genersoft.iot.vmp.gb28181.bean.GbStream;
@@ -20,6 +21,7 @@ import com.genersoft.iot.vmp.vmanager.bean.*;
 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.security.SecurityRequirement;
 import io.swagger.v3.oas.annotations.tags.Tag;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -64,7 +66,7 @@ public class StreamPushController {
 
     @GetMapping(value = "/list")
     @ResponseBody
-    @Operation(summary = "推流列表查询")
+    @Operation(summary = "推流列表查询", security = @SecurityRequirement(name = JwtUtils.HEADER))
     @Parameter(name = "page", description = "当前页")
     @Parameter(name = "count", description = "每页查询数量")
     @Parameter(name = "query", description = "查询内容")
@@ -88,7 +90,7 @@ public class StreamPushController {
 
     @PostMapping(value = "/save_to_gb")
     @ResponseBody
-    @Operation(summary = "将推流添加到国标")
+    @Operation(summary = "将推流添加到国标", security = @SecurityRequirement(name = JwtUtils.HEADER))
     public void saveToGB(@RequestBody GbStream stream){
         if (!streamPushService.saveToGB(stream)){
            throw new ControllerException(ErrorCode.ERROR100);
@@ -98,7 +100,7 @@ public class StreamPushController {
 
     @DeleteMapping(value = "/remove_form_gb")
     @ResponseBody
-    @Operation(summary = "将推流移出到国标")
+    @Operation(summary = "将推流移出到国标", security = @SecurityRequirement(name = JwtUtils.HEADER))
     public void removeFormGB(@RequestBody GbStream stream){
         if (!streamPushService.removeFromGB(stream)){
             throw new ControllerException(ErrorCode.ERROR100);
@@ -108,7 +110,7 @@ public class StreamPushController {
 
     @PostMapping(value = "/stop")
     @ResponseBody
-    @Operation(summary = "中止一个推流")
+    @Operation(summary = "中止一个推流", security = @SecurityRequirement(name = JwtUtils.HEADER))
     @Parameter(name = "app", description = "应用名", required = true)
     @Parameter(name = "stream", description = "流id", required = true)
     public void stop(String app, String streamId){
@@ -119,7 +121,7 @@ public class StreamPushController {
 
     @DeleteMapping(value = "/batchStop")
     @ResponseBody
-    @Operation(summary = "中止多个推流")
+    @Operation(summary = "中止多个推流", security = @SecurityRequirement(name = JwtUtils.HEADER))
     public void batchStop(@RequestBody BatchGBStreamParam batchGBStreamParam){
         if (batchGBStreamParam.getGbStreams().size() == 0) {
             throw new ControllerException(ErrorCode.ERROR100);
@@ -231,7 +233,7 @@ public class StreamPushController {
      */
     @GetMapping(value = "/getPlayUrl")
     @ResponseBody
-    @Operation(summary = "获取推流播放地址")
+    @Operation(summary = "获取推流播放地址", security = @SecurityRequirement(name = JwtUtils.HEADER))
     @Parameter(name = "app", description = "应用名", required = true)
     @Parameter(name = "stream", description = "流id", required = true)
     @Parameter(name = "mediaServerId", description = "媒体服务器id")
@@ -261,7 +263,7 @@ public class StreamPushController {
      */
     @PostMapping(value = "/add")
     @ResponseBody
-    @Operation(summary = "添加推流信息")
+    @Operation(summary = "添加推流信息", security = @SecurityRequirement(name = JwtUtils.HEADER))
     public void add(@RequestBody StreamPushItem stream){
         if (ObjectUtils.isEmpty(stream.getGbId())) {
             throw new ControllerException(ErrorCode.ERROR400.getCode(), "国标ID不可为空");

+ 5 - 3
src/main/java/com/genersoft/iot/vmp/vmanager/user/RoleController.java

@@ -1,6 +1,7 @@
 package com.genersoft.iot.vmp.vmanager.user;
 
 import com.genersoft.iot.vmp.conf.exception.ControllerException;
+import com.genersoft.iot.vmp.conf.security.JwtUtils;
 import com.genersoft.iot.vmp.conf.security.SecurityUtils;
 import com.genersoft.iot.vmp.service.IRoleService;
 import com.genersoft.iot.vmp.storager.dao.dto.Role;
@@ -8,6 +9,7 @@ import com.genersoft.iot.vmp.utils.DateUtil;
 import com.genersoft.iot.vmp.vmanager.bean.ErrorCode;
 import io.swagger.v3.oas.annotations.Operation;
 import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.security.SecurityRequirement;
 import io.swagger.v3.oas.annotations.tags.Tag;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.web.bind.annotation.*;
@@ -24,7 +26,7 @@ public class RoleController {
     private IRoleService roleService;
 
     @PostMapping("/add")
-    @Operation(summary = "添加角色")
+    @Operation(summary = "添加角色", security = @SecurityRequirement(name = JwtUtils.HEADER))
     @Parameter(name = "name", description = "角色名", required = true)
     @Parameter(name = "authority", description = "权限(自行定义内容,目前未使用)", required = true)
     public void add(@RequestParam String name,
@@ -49,7 +51,7 @@ public class RoleController {
     }
 
     @DeleteMapping("/delete")
-    @Operation(summary = "删除角色")
+    @Operation(summary = "删除角色", security = @SecurityRequirement(name = JwtUtils.HEADER))
     @Parameter(name = "id", description = "用户Id", required = true)
     public void delete(@RequestParam Integer id){
         // 获取当前登录用户id
@@ -66,7 +68,7 @@ public class RoleController {
     }
 
     @GetMapping("/all")
-    @Operation(summary = "查询角色")
+    @Operation(summary = "查询角色", security = @SecurityRequirement(name = JwtUtils.HEADER))
     public List<Role> all(){
         // 获取当前登录用户id
         List<Role> allRoles = roleService.getAll();

+ 8 - 7
src/main/java/com/genersoft/iot/vmp/vmanager/user/UserController.java

@@ -14,6 +14,7 @@ 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.security.SecurityRequirement;
 import io.swagger.v3.oas.annotations.tags.Tag;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.security.authentication.AuthenticationManager;
@@ -67,7 +68,7 @@ public class UserController {
 
 
     @PostMapping("/changePassword")
-    @Operation(summary = "修改密码")
+    @Operation(summary = "修改密码", security = @SecurityRequirement(name = JwtUtils.HEADER))
     @Parameter(name = "username", description = "用户名", required = true)
     @Parameter(name = "oldpassword", description = "旧密码(已md5加密的密码)", required = true)
     @Parameter(name = "password", description = "新密码(未md5加密的密码)", required = true)
@@ -96,7 +97,7 @@ public class UserController {
 
 
     @PostMapping("/add")
-    @Operation(summary = "添加用户")
+    @Operation(summary = "添加用户", security = @SecurityRequirement(name = JwtUtils.HEADER))
     @Parameter(name = "username", description = "用户名", required = true)
     @Parameter(name = "password", description = "密码(未md5加密的密码)", required = true)
     @Parameter(name = "roleId", description = "角色ID", required = true)
@@ -132,7 +133,7 @@ public class UserController {
     }
 
     @DeleteMapping("/delete")
-    @Operation(summary = "删除用户")
+    @Operation(summary = "删除用户", security = @SecurityRequirement(name = JwtUtils.HEADER))
     @Parameter(name = "id", description = "用户Id", required = true)
     public void delete(@RequestParam Integer id){
         // 获取当前登录用户id
@@ -148,7 +149,7 @@ public class UserController {
     }
 
     @GetMapping("/all")
-    @Operation(summary = "查询用户")
+    @Operation(summary = "查询用户", security = @SecurityRequirement(name = JwtUtils.HEADER))
     public List<User> all(){
         // 获取当前登录用户id
         return userService.getAllUsers();
@@ -162,7 +163,7 @@ public class UserController {
      * @return 分页用户列表
      */
     @GetMapping("/users")
-    @Operation(summary = "分页查询用户")
+    @Operation(summary = "分页查询用户", security = @SecurityRequirement(name = JwtUtils.HEADER))
     @Parameter(name = "page", description = "当前页", required = true)
     @Parameter(name = "count", description = "每页查询数量", required = true)
     public PageInfo<User> users(int page, int count) {
@@ -170,7 +171,7 @@ public class UserController {
     }
 
     @RequestMapping("/changePushKey")
-    @Operation(summary = "修改pushkey")
+    @Operation(summary = "修改pushkey", security = @SecurityRequirement(name = JwtUtils.HEADER))
     @Parameter(name = "userId", description = "用户Id", required = true)
     @Parameter(name = "pushKey", description = "新的pushKey", required = true)
     public void changePushKey(@RequestParam Integer userId,@RequestParam String pushKey) {
@@ -188,7 +189,7 @@ public class UserController {
     }
 
     @PostMapping("/changePasswordForAdmin")
-    @Operation(summary = "管理员修改普通用户密码")
+    @Operation(summary = "管理员修改普通用户密码", security = @SecurityRequirement(name = JwtUtils.HEADER))
     @Parameter(name = "adminId", description = "管理员id", required = true)
     @Parameter(name = "userId", description = "用户id", required = true)
     @Parameter(name = "password", description = "新密码(未md5加密的密码)", required = true)

+ 15 - 9
src/main/java/com/genersoft/iot/vmp/web/gb28181/ApiDeviceController.java

@@ -68,6 +68,7 @@ public class ApiDeviceController {
 //        if (logger.isDebugEnabled()) {
 //            logger.debug("查询所有视频设备API调用");
 //        }
+
         JSONObject result = new JSONObject();
         List<Device> devices;
         if (start == null || limit ==null) {
@@ -80,7 +81,7 @@ public class ApiDeviceController {
         }
 
         JSONArray deviceJSONList = new JSONArray();
-        for (Device device : devices) {
+        devices.stream().forEach(device -> {
             JSONObject deviceJsonObject = new JSONObject();
             deviceJsonObject.put("ID", device.getDeviceId());
             deviceJsonObject.put("Name", device.getName());
@@ -99,7 +100,7 @@ public class ApiDeviceController {
             deviceJsonObject.put("UpdatedAt", "");
             deviceJsonObject.put("CreatedAt", "");
             deviceJSONList.add(deviceJsonObject);
-        }
+        });
         result.put("DeviceList",deviceJSONList);
         return result;
     }
@@ -114,7 +115,6 @@ public class ApiDeviceController {
                                    @RequestParam(required = false)String q,
                                    @RequestParam(required = false)Boolean online ){
 
-
         JSONObject result = new JSONObject();
         List<DeviceChannelExtend> deviceChannels;
         List<String> channelIds = null;
@@ -127,13 +127,19 @@ public class ApiDeviceController {
             deviceChannels = allDeviceChannelList;
             result.put("ChannelCount", deviceChannels.size());
         }else {
-            deviceChannels = storager.queryChannelsByDeviceIdWithStartAndLimit(serial,channelIds, null, null, online,start, limit);
-            int total = allDeviceChannelList.size();
-            result.put("ChannelCount", total);
+            if (start > allDeviceChannelList.size()) {
+                deviceChannels = new ArrayList<>();
+            }else {
+                if (start + limit < allDeviceChannelList.size()) {
+                    deviceChannels = allDeviceChannelList.subList(start, start + limit);
+                }else {
+                    deviceChannels = allDeviceChannelList.subList(start, allDeviceChannelList.size());
+                }
+            }
+            result.put("ChannelCount", allDeviceChannelList.size());
         }
-
         JSONArray channleJSONList = new JSONArray();
-        for (DeviceChannelExtend deviceChannelExtend : deviceChannels) {
+        deviceChannels.stream().forEach(deviceChannelExtend -> {
             JSONObject deviceJOSNChannel = new JSONObject();
             deviceJOSNChannel.put("ID", deviceChannelExtend.getChannelId());
             deviceJOSNChannel.put("DeviceID", deviceChannelExtend.getDeviceId());
@@ -166,7 +172,7 @@ public class ApiDeviceController {
             deviceJOSNChannel.put("StreamID", deviceChannelExtend.getStreamId()); // StreamID 直播流ID, 有值表示正在直播
             deviceJOSNChannel.put("NumOutputs ", -1); // 直播在线人数
             channleJSONList.add(deviceJOSNChannel);
-        }
+        });
         result.put("ChannelList", channleJSONList);
         return result;
     }

+ 1 - 1
src/main/java/com/genersoft/iot/vmp/web/gb28181/ApiStreamController.java

@@ -92,7 +92,7 @@ public class ApiStreamController {
             result.put("error","device[ " + serial + " ]未找到");
             resultDeferredResult.setResult(result);
             return resultDeferredResult;
-        }else if (device.isOnLine()) {
+        }else if (!device.isOnLine()) {
             JSONObject result = new JSONObject();
             result.put("error","device[ " + code + " ]offline");
             resultDeferredResult.setResult(result);

+ 20 - 3
src/main/resources/all-application.yml

@@ -34,6 +34,19 @@ spring:
         poolMaxWait: 5
     # [必选] jdbc数据库配置
     datasource:
+        # kingbase配置
+        #        type: com.zaxxer.hikari.HikariDataSource
+        #        driver-class-name: com.kingbase8.Driver
+        #        url: jdbc:kingbase8://192.168.1.55:54321/wvp?useUnicode=true&characterEncoding=utf8
+        #        username: system
+        #        password: system
+        # postgresql配置
+        #    type: com.zaxxer.hikari.HikariDataSource
+        #    driver-class-name: org.postgresql.Driver
+        #    url: jdbc:postgresql://192.168.1.242:3306/242wvp
+        #    username: root
+        #    password: SYceshizu1234
+        # mysql配置
         type: com.zaxxer.hikari.HikariDataSource
         driver-class-name: com.mysql.cj.jdbc.Driver
         url: jdbc:mysql://127.0.0.1:3306/wvp2?useUnicode=true&characterEncoding=UTF8&rewriteBatchedStatements=true&serverTimezone=PRC&useSSL=false&allowMultiQueries=true&allowPublicKeyRetrieval=true
@@ -41,9 +54,9 @@ spring:
         password: root123
         hikari:
             connection-timeout: 20000             # 是客户端等待连接池连接的最大毫秒数
-            initialSize: 10                       # 连接池初始化连接数
+            initialSize: 50                       # 连接池初始化连接数
             maximum-pool-size: 200                # 连接池最大连接数
-            minimum-idle: 5                       # 连接池最小空闲连接数
+            minimum-idle: 10                       # 连接池最小空闲连接数
             idle-timeout: 300000                  # 允许连接在连接池中空闲的最长时间(以毫秒为单位)
             max-lifetime: 1200000                 # 是池中连接关闭后的最长生命周期(以毫秒为单位)
 
@@ -139,6 +152,10 @@ media:
     auto-config: true
     # [可选] zlm服务器的hook.admin_params=secret
     secret: 035c73f7-bb6b-4889-a715-d9eb2d1925cc
+    # 录像路径
+    record-path: ./www/record
+    # 录像保存时长
+    record-day: 7
     # 启用多端口模式, 多端口模式使用端口区分每路流,兼容性更好。 单端口使用流的ssrc区分, 点播超时建议使用多端口测试
     rtp:
         # [可选] 是否启用多端口模式, 开启后会在portRange范围内选择端口用于媒体流传输
@@ -182,7 +199,7 @@ user-settings:
     # 使用推流状态作为推流通道状态
     use-pushing-as-status: true
     # 使用来源请求ip作为streamIp,当且仅当你只有zlm节点它与wvp在一起的情况下开启
-    use-source-ip-as-stream-ip: true
+    use-source-ip-as-stream-ip: false
     # 国标点播 按需拉流, true:有人观看拉流,无人观看释放, false:拉起后不自动释放
     stream-on-demand: true
     # 推流鉴权, 默认开启

+ 2 - 4
web_src/build/webpack.dev.conf.js

@@ -10,7 +10,6 @@ const HtmlWebpackPlugin = require('html-webpack-plugin')
 const FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin')
 const portfinder = require('portfinder')
 
-const HOST = process.env.HOST
 const PORT = process.env.PORT && Number(process.env.PORT)
 
 const devWebpackConfig = merge(baseWebpackConfig, {
@@ -31,9 +30,8 @@ const devWebpackConfig = merge(baseWebpackConfig, {
     hot: true,
     contentBase: false, // since we use CopyWebpackPlugin.
     compress: true,
-    host: HOST || config.dev.host,
-    // host:'127.0.0.1',
-    port: PORT || config.dev.port,
+    host: config.dev.host,
+    port: config.dev.port,
     open: config.dev.autoOpenBrowser,
     overlay: config.dev.errorOverlay
       ? { warnings: false, errors: true }

Diferenças do arquivo suprimidas por serem muito extensas
+ 4466 - 15425
web_src/package-lock.json


+ 252 - 178
web_src/src/components/CloudRecord.vue

@@ -1,207 +1,281 @@
 <template>
-	<div id="app" style="width: 100%">
+  <div id="app" style="width: 100%">
     <div class="page-header">
       <div class="page-title">
-        <el-page-header v-if="recordDetail" @back="backToList" content="云端录像"></el-page-header>
-        <div v-if="!recordDetail">云端录像</div>
+        <div >云端录像</div>
       </div>
 
       <div class="page-header-btn">
+        搜索:
+        <el-input @input="getMediaServerList" style="margin-right: 1rem; width: auto;" size="mini" placeholder="关键字"
+                  prefix-icon="el-icon-search" v-model="search"  clearable></el-input>
+        开始时间:
+        <el-date-picker
+            v-model="startTime"
+            type="datetime"
+            value-format="yyyy-MM-dd HH:mm:ss"
+            @change="getMediaServerList"
+            placeholder="选择日期时间">
+        </el-date-picker>
+        结束时间:
+        <el-date-picker
+            v-model="endTime"
+            type="datetime"
+            value-format="yyyy-MM-dd HH:mm:ss"
+            @change="getMediaServerList"
+            placeholder="选择日期时间">
+        </el-date-picker>
         节点选择:
-        <el-select size="mini" @change="chooseMediaChange" style="width: 16rem; margin-right: 1rem;" v-model="mediaServerId" placeholder="请选择" :disabled="recordDetail">
+        <el-select size="mini" @change="getMediaServerList" style="width: 16rem; margin-right: 1rem;"
+                   v-model="mediaServerId" placeholder="请选择" >
+          <el-option label="全部" value=""></el-option>
           <el-option
-            v-for="item in mediaServerList"
-            :key="item.id"
-            :label="item.id"
-            :value="item.id">
+              v-for="item in mediaServerList"
+              :key="item.id"
+              :label="item.id"
+              :value="item.id">
           </el-option>
         </el-select>
-        <el-button v-if="!recordDetail" icon="el-icon-refresh-right" circle size="mini" :loading="loading" @click="getRecordList()"></el-button>
+<!--        <el-button size="mini" icon="el-icon-delete" type="danger" @click="deleteRecord()">批量删除</el-button>-->
+        <el-button icon="el-icon-refresh-right" circle size="mini" :loading="loading"
+                   @click="getRecordList()"></el-button>
       </div>
     </div>
-    <div v-if="!recordDetail">
-
-      <!--设备列表-->
-      <el-table :data="recordList" style="width: 100%" :height="winHeight">
-        <el-table-column prop="app" label="应用名" >
-        </el-table-column>
-        <el-table-column prop="stream" label="流ID" >
-        </el-table-column>
-        <el-table-column prop="time" label="时间" >
-        </el-table-column>
-        <el-table-column label="操作" width="360"  fixed="right">
-          <template slot-scope="scope">
-            <el-button size="medium" icon="el-icon-folder-opened" type="text" @click="showRecordDetail(scope.row)">查看</el-button>
-            <!--                  <el-button size="mini" icon="el-icon-delete" type="danger"  @click="deleteRecord(scope.row)">删除</el-button>-->
-          </template>
-        </el-table-column>
-      </el-table>
-      <el-pagination
-        style="float: right"
-        @size-change="handleSizeChange"
-        @current-change="currentChange"
-        :current-page="currentPage"
-        :page-size="count"
-        :page-sizes="[15, 25, 35, 50]"
-        layout="total, sizes, prev, pager, next"
-        :total="total">
-      </el-pagination>
-    </div>
-
+    <!--设备列表-->
+    <el-table :data="recordList" style="width: 100%" :height="winHeight">
+      <el-table-column
+        type="selection"
+        width="55">
+      </el-table-column>
+      <el-table-column prop="app" label="应用名">
+      </el-table-column>
+      <el-table-column prop="stream" label="流ID" width="380">
+      </el-table-column>
+      <el-table-column label="开始时间">
+        <template slot-scope="scope">
+          {{formatTimeStamp(scope.row.startTime)}}
+        </template>
+      </el-table-column>
+      <el-table-column label="结束时间">
+        <template slot-scope="scope">
+          {{formatTimeStamp(scope.row.endTime)}}
+        </template>
+      </el-table-column>
+      <el-table-column  label="时长">
+        <template slot-scope="scope">
+          <el-tag>{{formatTime(scope.row.timeLen)}}</el-tag>
+        </template>
+      </el-table-column>
+      <el-table-column prop="fileName" label="文件名称">
+      </el-table-column>
+      <el-table-column prop="mediaServerId" label="流媒体">
+      </el-table-column>
+      <el-table-column label="操作" width="200" fixed="right">
+        <template slot-scope="scope">
+          <el-button size="medium" icon="el-icon-video-play" type="text" @click="play(scope.row)">播放
+          </el-button>
+          <!--            <el-button size="medium" icon="el-icon-delete" type="text" style="color: #f56c6c"-->
+          <!--                       @click="deleteRecord(scope.row)">删除-->
+          <!--            </el-button>-->
+        </template>
+      </el-table-column>
+    </el-table>
+    <el-pagination
+      style="float: right"
+      @size-change="handleSizeChange"
+      @current-change="currentChange"
+      :current-page="currentPage"
+      :page-size="count"
+      :page-sizes="[15, 25, 35, 50]"
+      layout="total, sizes, prev, pager, next"
+      :total="total">
+    </el-pagination>
+    <el-dialog
+      :title="playerTitle"
+      :visible.sync="showPlayer"
+      width="50%">
+      <easyPlayer ref="recordVideoPlayer" :videoUrl="videoUrl" :height="false"  ></easyPlayer>
+    </el-dialog>
   </div>
 </template>
 
 <script>
-	import uiHeader from '../layout/UiHeader.vue'
-  import MediaServer from './service/MediaServer'
-	export default {
-		name: 'app',
-		components: {
-      uiHeader
-		},
-		data() {
-			return {
-        mediaServerList: [], // 滅体节点列表
-        mediaServerId: null, // 媒体服务
-        mediaServerPath: null, // 媒体服务地址
-        recordList: [], // 设备列表
-        chooseRecord: null, // 媒体服务
+import uiHeader from '../layout/UiHeader.vue'
+import MediaServer from './service/MediaServer'
+import easyPlayer from './common/easyPlayer.vue'
+import moment  from 'moment'
+import axios from "axios";
 
-        updateLooper: 0, //数据刷新轮训标志
-        winHeight: window.innerHeight - 250,
-        currentPage:1,
-        count:15,
-        total:0,
-        loading: false,
-        mediaServerObj : new MediaServer(),
-        recordDetail: false
+export default {
+  name: 'app',
+  components: {
+    uiHeader,easyPlayer
+  },
+  data() {
+    return {
+      search: '',
+      startTime: '',
+      endTime: '',
+      showPlayer: false,
+      playerTitle: '',
+      videoUrl: '',
+      playerStyle: {
+          "margin": "auto",
+          "margin-bottom": "20px",
+          "width": window.innerWidth/2 + "px",
+          "height": this.winHeight/2 + "px",
+      },
+      mediaServerList: [], // 滅体节点列表
+      mediaServerId: "", // 媒体服务
+      mediaServerPath: null, // 媒体服务地址
+      recordList: [], // 设备列表
+      chooseRecord: null, // 媒体服务
 
-			};
-		},
-		computed: {
+      updateLooper: 0, //数据刷新轮训标志
+      winHeight: window.innerHeight - 250,
+      currentPage: 1,
+      count: 15,
+      total: 0,
+      loading: false,
+      mediaServerObj: new MediaServer(),
 
-		},
-		mounted() {
-			this.initData();
-		},
-		destroyed() {
-			// this.$destroy('videojs');
-		},
-		methods: {
-			initData: function() {
-			  // 获取媒体节点列表
-			  this.getMediaServerList();
-			  // this.getRecordList();
-			},
-      currentChange: function(val){
-        this.currentPage = val;
-        this.getRecordList();
-      },
-      handleSizeChange: function(val){
-        this.count = val;
-        this.getRecordList();
-      },
-      getMediaServerList: function (){
-        let that = this;
-        that.mediaServerObj.getOnlineMediaServerList((data)=>{
-          that.mediaServerList = data.data;
-          if (that.mediaServerList.length > 0) {
-            that.mediaServerId = that.mediaServerList[0].id
-            that.setMediaServerPath(that.mediaServerId);
-            that.getRecordList();
-          }
-        })
-      },
-      setMediaServerPath: function (serverId) {
-        let that = this;
-        let i;
-        for (i = 0; i < that.mediaServerList.length; i++) {
-          if (serverId === that.mediaServerList[i].id) {
-            break;
-          }
+    };
+  },
+  computed: {},
+  mounted() {
+    this.initData();
+  },
+  destroyed() {
+      this.$destroy('recordVideoPlayer');
+  },
+  methods: {
+    initData: function () {
+      // 获取媒体节点列表
+      this.getMediaServerList();
+      this.getRecordList();
+    },
+    currentChange: function (val) {
+      this.currentPage = val;
+      this.getRecordList();
+    },
+    handleSizeChange: function (val) {
+      this.count = val;
+      this.getRecordList();
+    },
+    getMediaServerList: function () {
+      let that = this;
+      that.mediaServerObj.getOnlineMediaServerList((data) => {
+        that.mediaServerList = data.data;
+      })
+    },
+    setMediaServerPath: function (serverId) {
+      let that = this;
+      let i;
+      for (i = 0; i < that.mediaServerList.length; i++) {
+        if (serverId === that.mediaServerList[i].id) {
+          break;
         }
-        let port = that.mediaServerList[i].httpPort;
-        if (location.protocol === "https:" && that.mediaServerList[i].httpSSlPort) {
-          port = that.mediaServerList[i].httpSSlPort
+      }
+      let port = that.mediaServerList[i].httpPort;
+      if (location.protocol === "https:" && that.mediaServerList[i].httpSSlPort) {
+        port = that.mediaServerList[i].httpSSlPort
+      }
+      that.mediaServerPath = location.protocol + "//" + that.mediaServerList[i].streamIp + ":" + port
+      console.log(that.mediaServerPath)
+    },
+    getRecordList: function () {
+      this.$axios({
+        method: 'get',
+        url: `/api/cloud/record/list`,
+        params: {
+          app: '',
+          stream: '',
+          query: this.search,
+          startTime: this.startTime,
+          endTime: this.endTime,
+          mediaServerId: this.mediaServerId,
+          page: this.currentPage,
+          count: this.count
         }
-        that.mediaServerPath = location.protocol + "//" + that.mediaServerList[i].streamIp + ":" + port
-        console.log(that.mediaServerPath)
-      },
-      getRecordList: function (){
-        let that = this;
-        this.$axios({
-          method: 'get',
-          url:`/record_proxy/${that.mediaServerId}/api/record/list`,
-          params: {
-            page: that.currentPage,
-            count: that.count
-          }
-        }).then(function (res) {
-          console.log(res)
-          if (res.data.code === 0) {
-            that.total = res.data.data.total;
-            that.recordList = res.data.data.list;
-          }
-          that.loading = false;
-        }).catch(function (error) {
-          console.log(error);
-          that.loading = false;
-        });
-      },
-      backToList(){
-			  this.recordDetail= false;
-      },
-      chooseMediaChange(val){
-          console.log(val)
-          this.total = 0;
-          this.recordList = [];
-          this.setMediaServerPath(val);
-          this.getRecordList();
-      },
-      showRecordDetail(row){
-        this.recordDetail = true;
-        this.chooseRecord = row;
-        // 查询是否存在录像
-        // this.$axios({
-        //   method: 'delete',
-        //   url:`/record_proxy/api/record/delete`,
-        //   params: {
-        //     page: this.currentPage,
-        //     count: this.count
-        //   }
-        // }).then((res) => {
-        //   console.log(res)
-        //   this.total = res.data.data.total;
-        //   this.recordList = res.data.data.list;
-        // }).catch(function (error) {
-        //   console.log(error);
-        // });
-        this.$router.push(`/cloudRecordDetail/${row.app}/${row.stream}`)
-      },
-      deleteRecord(){
-			  // TODO
-        let that = this;
-        this.$axios({
-          method: 'delete',
-          url:`/record_proxy/api/record/delete`,
-          params: {
-            page: that.currentPage,
-            count: that.count
+      }).then((res) => {
+        console.log(res)
+        if (res.data.code === 0) {
+          this.total = res.data.data.total;
+          this.recordList = res.data.data.list;
+        }
+        this.loading = false;
+      }).catch((error) => {
+        console.log(error);
+        this.loading = false;
+      });
+    },
+    play(row) {
+      console.log(row)
+      this.chooseRecord = row;
+      this.showPlayer = true;
+      this.$axios({
+        method: 'get',
+        url: `/api/cloud/record/play/path`,
+        params: {
+          recordId: row.id,
+        }
+      }).then((res) => {
+        console.log(res)
+        if (res.data.code === 0) {
+          if (location.protocol === "https:") {
+            this.videoUrl = res.data.data.httpsPath;
+          }else {
+            this.videoUrl = res.data.data.httpPath;
           }
-        }).then(function (res) {
-          console.log(res)
-          if (res.data.code === 0) {
-            that.total = res.data.data.total;
-            that.recordList = res.data.data.list;
+          console.log(222 )
+          console.log(this.videoUrl )
+        }
+      }).catch((error) => {
+        console.log(error);
+      });
+    },
+      getFileBasePath(item) {
+          let basePath = ""
+          if (axios.defaults.baseURL.startsWith("http")) {
+              basePath = `${axios.defaults.baseURL}/record_proxy/${item.mediaServerId}`
+          }else {
+              basePath = `${window.location.origin}${axios.defaults.baseURL}/record_proxy/${item.mediaServerId}`
           }
-        }).catch(function (error) {
-          console.log(error);
-        });
+          return basePath;
       },
+    deleteRecord() {
+      // TODO
+      let that = this;
+      this.$axios({
+        method: 'delete',
+        url: `/record_proxy/api/record/delete`,
+        params: {
+          page: that.currentPage,
+          count: that.count
+        }
+      }).then(function (res) {
+        console.log(res)
+        if (res.data.code === 0) {
+          that.total = res.data.data.total;
+          that.recordList = res.data.data.list;
+        }
+      }).catch(function (error) {
+        console.log(error);
+      });
+    },
+    formatTime(time) {
+      const h = parseInt(time / 3600)
+      const minute = parseInt(time / 60 % 60)
+      const second = Math.ceil(time % 60)
 
+      return (h > 0 ? h + `小时` : '') + (minute > 0 ? minute + '分' : '') + second + '秒'
+    },
+    formatTimeStamp(time) {
+      return moment.unix(time).format('yyyy-MM-DD HH:mm:ss')
+    }
 
-		}
-	};
+  }
+};
 </script>
 
 <style>

+ 18 - 22
web_src/src/components/CloudRecordDetail.vue

@@ -37,13 +37,13 @@
           <div class="record-list-box" :style="recordListStyle">
             <ul v-if="detailFiles.length >0" class="infinite-list record-list" v-infinite-scroll="infiniteScroll" >
               <li v-for="(item,index) in detailFiles" :key="index" class="infinite-list-item record-list-item" >
-                <el-tag v-if="choosedFile !== item.filename" @click="chooseFile(item)">
+                <el-tag v-if="choosedFile !== item.fileName" @click="chooseFile(item)">
                   <i class="el-icon-video-camera"  ></i>
-                  {{ getFileShowName(item.fileName) }}
+                  {{ getFileShowName(item) }}
                 </el-tag>
-                <el-tag type="danger" v-if="choosedFile === item.filename">
+                <el-tag type="danger" v-if="choosedFile === item.fileName">
                   <i class="el-icon-video-camera"  ></i>
-                  {{ getFileShowName(item.fileName) }}
+                  {{ getFileShowName(item) }}
                 </el-tag>
                 <a class="el-icon-download" style="color: #409EFF;font-weight: 600;margin-left: 10px;"
                    :href="`${getFileBasePath(item)}/download.html?url=download/${app}/${stream}/${chooseDate}/${item.fileName}`"
@@ -135,7 +135,7 @@
 <script>
   // TODO 根据查询的时间列表设置滑轨的最大值与最小值,
 	import uiHeader from '../layout/UiHeader.vue'
-	import player from './dialog/easyPlayer.vue'
+	import player from './common/easyPlayer.vue'
   import moment  from 'moment'
   import axios from "axios";
 	export default {
@@ -319,7 +319,7 @@
           this.choosedFile = "";
         }else {
           this.choosedFile = file.fileName;
-          this.videoUrl = `${this.getFileBasePath(file)}/download/${this.app}/${this.stream}/${this.chooseDate}/${this.choosedFile}`
+          this.videoUrl = `${this.getFileBasePath(file)}/download/${this.app}/${this.stream}/${this.chooseDate}/${file.fileName}`
           console.log(this.videoUrl)
         }
 
@@ -327,9 +327,8 @@
       backToList() {
         this.$router.back()
       },
-      getFileShowName(name) {
-        return name.substring(0, 2) + ":" + name.substring(2, 4) + ":" + name.substring(4, 6) + "-" +
-            name.substring(7, 9) + ":" + name.substring(9, 11) + ":" + name.substring(11, 13)
+      getFileShowName(item) {
+          return  moment.unix(item.startTime).format('HH:mm:ss') + "-" + moment.unix(item.endTime).format('HH:mm:ss')
       },
       chooseMediaChange() {
 
@@ -376,13 +375,8 @@
       },
       getTimeForFile(file){
         console.log(file)
-        let timeStr = file.fileName.substring(0, 17);
-        if(timeStr.indexOf("~") > 0){
-          timeStr = timeStr.replaceAll("-",":")
-        }
-        let timeArr = timeStr.split("-");
-        let starTime = new Date(this.chooseDate + " " + timeArr[0]);
-        let endTime = new Date(this.chooseDate + " " + timeArr[1]);
+        let starTime = new Date(file.startTime * 1000);
+        let endTime = new Date(file.endTime * 1000);
         if(this.checkIsOver24h(starTime,endTime)){
            endTime = new Date(this.chooseDate + " " + "23:59:59");
         }
@@ -486,12 +480,13 @@
         let that = this;
         this.$axios({
           method: 'get',
-          url:`/record_proxy/${that.mediaServerId}/api/record/file/download/task/add`,
+          url:`/api/cloud/record/task/add`,
           params: {
-            app: that.app,
-            stream: that.stream,
-            startTime: moment(this.taskTimeRange[0]).format('YYYY-MM-DD HH:mm:ss'),
-            endTime: moment(this.taskTimeRange[1]).format('YYYY-MM-DD HH:mm:ss'),
+              app: this.app,
+              stream: this.stream,
+              mediaServerId: this.mediaServerId,
+              startTime: moment(this.taskTimeRange[0]).format('YYYY-MM-DD HH:mm:ss'),
+              endTime: moment(this.taskTimeRange[1]).format('YYYY-MM-DD HH:mm:ss'),
           }
         }).then(function (res) {
           if (res.data.code === 0 ) {
@@ -511,8 +506,9 @@
         let that = this;
         this.$axios({
           method: 'get',
-          url:`/record_proxy/${that.mediaServerId}/api/record/file/download/task/list`,
+          url:`/api/cloud/record/task/list`,
           params: {
+            mediaServerId: this.mediaServerId,
             isEnd: isEnd,
           }
         }).then(function (res) {

+ 241 - 102
web_src/src/components/channelList.vue

@@ -33,98 +33,156 @@
             <el-option label="流畅" :value="true"></el-option>
           </el-select>
         </div>
-      <el-button icon="el-icon-refresh-right" circle size="mini" @click="refresh()"></el-button>
-      <el-button v-if="showTree" icon="iconfont icon-list" circle size="mini" @click="switchList()"></el-button>
-      <el-button v-if="!showTree"  icon="iconfont icon-tree" circle size="mini" @click="switchTree()"></el-button>
+        <el-button icon="el-icon-refresh-right" circle size="mini" @click="refresh()"></el-button>
+        <el-button v-if="showTree" icon="iconfont icon-list" circle size="mini" @click="switchList()"></el-button>
+        <el-button v-if="!showTree" icon="iconfont icon-tree" circle size="mini" @click="switchTree()"></el-button>
+      </div>
     </div>
-  </div>
-  <devicePlayer ref="devicePlayer" ></devicePlayer>
-  <el-container v-loading="isLoging" style="height: 82vh;">
-    <el-aside width="auto" style="height: 82vh; background-color: #ffffff; overflow: auto" v-if="showTree" >
-      <DeviceTree ref="deviceTree" :device="device" :onlyCatalog="true" :clickEvent="treeNodeClickEvent" ></DeviceTree>
-    </el-aside>
-    <el-main style="padding: 5px;">
-      <el-table ref="channelListTable" :data="deviceChannelList" :height="winHeight" style="width: 100%" header-row-class-name="table-header">
-        <el-table-column prop="channelId" label="通道编号" min-width="200">
-        </el-table-column>
-        <el-table-column prop="deviceId" label="设备编号" min-width="200">
-        </el-table-column>
-        <el-table-column prop="name" label="通道名称" min-width="200">
-        </el-table-column>
-        <el-table-column label="快照" min-width="120">
-          <template v-slot:default="scope">
-            <el-image
-              :src="getSnap(scope.row)"
-              :preview-src-list="getBigSnap(scope.row)"
-              @error="getSnapErrorEvent(scope.row.deviceId, scope.row.channelId)"
-              :fit="'contain'"
-              style="width: 60px">
-              <div slot="error" class="image-slot">
-                <i class="el-icon-picture-outline"></i>
+    <devicePlayer ref="devicePlayer"></devicePlayer>
+    <el-container v-loading="isLoging" style="height: 82vh;">
+      <el-aside width="auto" style="height: 82vh; background-color: #ffffff; overflow: auto" v-if="showTree">
+        <DeviceTree ref="deviceTree" :device="device" :onlyCatalog="true" :clickEvent="treeNodeClickEvent"></DeviceTree>
+      </el-aside>
+      <el-main style="padding: 5px;">
+        <el-table ref="channelListTable" :data="deviceChannelList" :height="winHeight" style="width: 100%"
+                  header-row-class-name="table-header">
+          <el-table-column prop="channelId" label="通道编号" min-width="200">
+          </el-table-column>
+          <el-table-column prop="deviceId" label="设备编号" min-width="200">
+          </el-table-column>
+          <el-table-column prop="name" label="通道名称" min-width="200">
+            <template v-slot:default="scope">
+              <el-input
+                v-show="scope.row.edit"
+                v-model="scope.row.name"
+                placeholder="通道名称"
+                :maxlength="255"
+                show-word-limit
+                clearable
+              />
+              <span v-show="!scope.row.edit">{{ scope.row.name }}</span>
+            </template>
+          </el-table-column>
+          <el-table-column label="快照" min-width="120">
+            <template v-slot:default="scope">
+              <el-image
+                :src="getSnap(scope.row)"
+                :preview-src-list="getBigSnap(scope.row)"
+                @error="getSnapErrorEvent(scope.row.deviceId, scope.row.channelId)"
+                :fit="'contain'"
+                style="width: 60px">
+                <div slot="error" class="image-slot">
+                  <i class="el-icon-picture-outline"></i>
+                </div>
+              </el-image>
+            </template>
+          </el-table-column>
+          <el-table-column prop="subCount" label="子节点数" min-width="120">
+          </el-table-column>
+          <el-table-column prop="manufacture" label="厂家" min-width="120">
+          </el-table-column>
+          <el-table-column label="位置信息" min-width="200">
+            <template v-slot:default="scope">
+              <el-input
+                v-show="scope.row.edit"
+                v-model="scope.row.location"
+                placeholder="例:117.234,36.378"
+                :maxlength="30"
+                show-word-limit
+                clearable
+              />
+              <span v-show="!scope.row.edit">{{ scope.row.location }}</span>
+            </template>
+          </el-table-column>
+          <el-table-column prop="PTZType" label="云台类型" min-width="120">
+            <template v-slot:default="scope">
+              <el-select v-show="scope.row.edit" v-model="scope.row.PTZType"
+                         placeholder="云台类型" filterable>
+                <el-option
+                  v-for="(value, key) in ptzTypes"
+                  :key="key"
+                  :label="value"
+                  :value="key"
+                />
+              </el-select>
+              <div v-show="!scope.row.edit">{{ scope.row.PTZTypeText }}</div>
+            </template>
+          </el-table-column>
+          <el-table-column label="开启音频" min-width="120">
+            <template slot-scope="scope">
+              <el-switch @change="updateChannel(scope.row)" v-model="scope.row.hasAudio" active-color="#409EFF">
+              </el-switch>
+            </template>
+          </el-table-column>
+          <el-table-column label="状态" min-width="120">
+            <template slot-scope="scope">
+              <div slot="reference" class="name-wrapper">
+                <el-tag size="medium" v-if="scope.row.status === true">在线</el-tag>
+                <el-tag size="medium" type="info" v-if="scope.row.status === false">离线</el-tag>
               </div>
-            </el-image>
-          </template>
-        </el-table-column>
-        <el-table-column prop="subCount" label="子节点数" min-width="120">
-        </el-table-column>
-        <el-table-column prop="manufacture" label="厂家" min-width="120">
-        </el-table-column>
-        <el-table-column label="位置信息"  min-width="200">
-          <template slot-scope="scope">
-            <span v-if="scope.row.longitude*scope.row.latitude > 0">{{ scope.row.longitude }},<br>{{ scope.row.latitude }}</span>
-            <span v-if="scope.row.longitude*scope.row.latitude === 0">无</span>
-          </template>
-        </el-table-column>
-        <el-table-column prop="PTZTypeText" label="云台类型" min-width="120"/>
-        <el-table-column label="开启音频" min-width="120">
-          <template slot-scope="scope">
-            <el-switch @change="updateChannel(scope.row)" v-model="scope.row.hasAudio" active-color="#409EFF">
-            </el-switch>
-          </template>
-        </el-table-column>
-        <el-table-column label="状态" min-width="120">
-          <template slot-scope="scope">
-            <div slot="reference" class="name-wrapper">
-              <el-tag size="medium" v-if="scope.row.status === true">在线</el-tag>
-              <el-tag size="medium" type="info" v-if="scope.row.status === false">离线</el-tag>
-            </div>
-          </template>
-        </el-table-column>
+            </template>
+          </el-table-column>
 
 
-        <el-table-column label="操作" min-width="280" fixed="right">
-          <template slot-scope="scope">
-            <el-button size="medium" v-bind:disabled="device == null || device.online === 0" icon="el-icon-video-play" type="text" @click="sendDevicePush(scope.row)">播放</el-button>
-            <el-button size="medium" v-bind:disabled="device == null || device.online === 0" icon="el-icon-switch-button" type="text"  style="color: #f56c6c" v-if="!!scope.row.streamId"
-                       @click="stopDevicePush(scope.row)">停止
-            </el-button>
-            <el-divider direction="vertical"></el-divider>
-            <el-button size="medium" icon="el-icon-s-open" type="text" v-if="scope.row.subCount > 0 || scope.row.parental === 1"
-                       @click="changeSubchannel(scope.row)">查看
-            </el-button>
-            <el-divider v-if="scope.row.subCount > 0 || scope.row.parental === 1" direction="vertical"></el-divider>
-            <el-button size="medium" v-bind:disabled="device == null || device.online === 0" icon="el-icon-video-camera" type="text" @click="queryRecords(scope.row)">设备录像
-            </el-button>
-            <el-button size="medium" v-bind:disabled="device == null || device.online === 0" icon="el-icon-cloudy"
-                       type="text" @click="queryCloudRecords(scope.row)">云端录像
-            </el-button>
-          </template>
-        </el-table-column>
-      </el-table>
-      <el-pagination
-        style="float: right"
-        @size-change="handleSizeChange"
-        @current-change="currentChange"
-        :current-page="currentPage"
-        :page-size="count"
-        :page-sizes="[15, 25, 35, 50]"
-        layout="total, sizes, prev, pager, next"
-        :total="total">
-      </el-pagination>
-    </el-main>
-  </el-container>
+          <el-table-column label="操作" min-width="340" fixed="right">
+            <template slot-scope="scope">
+              <el-button size="medium" v-bind:disabled="device == null || device.online === 0" icon="el-icon-video-play"
+                         type="text" @click="sendDevicePush(scope.row)">播放
+              </el-button>
+              <el-button size="medium" v-bind:disabled="device == null || device.online === 0"
+                         icon="el-icon-switch-button"
+                         type="text" style="color: #f56c6c" v-if="!!scope.row.streamId"
+                         @click="stopDevicePush(scope.row)">停止
+              </el-button>
+              <el-divider direction="vertical"></el-divider>
+              <el-button
+                v-if="scope.row.edit"
+                size="medium"
+                type="text"
+                icon="el-icon-edit-outline"
+                @click="handleSave(scope.row)"
+              >
+                保存
+              </el-button>
+              <el-button
+                v-else
+                size="medium"
+                type="text"
+                icon="el-icon-edit"
+                @click="handleEdit(scope.row)"
+              >
+                编辑
+              </el-button>
+              <el-divider direction="vertical"></el-divider>
+              <el-button size="medium" icon="el-icon-s-open" type="text"
+                         v-if="scope.row.subCount > 0 || scope.row.parental === 1"
+                         @click="changeSubchannel(scope.row)">查看
+              </el-button>
+              <el-divider v-if="scope.row.subCount > 0 || scope.row.parental === 1" direction="vertical"></el-divider>
+              <el-button size="medium" v-bind:disabled="device == null || device.online === 0"
+                         icon="el-icon-video-camera"
+                         type="text" @click="queryRecords(scope.row)">设备录像
+              </el-button>
+              <el-button size="medium" v-bind:disabled="device == null || device.online === 0" icon="el-icon-cloudy"
+                         type="text" @click="queryCloudRecords(scope.row)">云端录像
+              </el-button>
+            </template>
+          </el-table-column>
+        </el-table>
+        <el-pagination
+          style="float: right"
+          @size-change="handleSizeChange"
+          @current-change="currentChange"
+          :current-page="currentPage"
+          :page-size="count"
+          :page-sizes="[15, 25, 35, 50]"
+          layout="total, sizes, prev, pager, next"
+          :total="total">
+        </el-pagination>
+      </el-main>
+    </el-container>
 
-  <!--设备列表-->
+    <!--设备列表-->
 
   </div>
 </template>
@@ -163,16 +221,23 @@ export default {
       beforeUrl: "/deviceList",
       isLoging: false,
       showTree: false,
-      loadSnap: {}
+      loadSnap: {},
+      ptzTypes: {
+        0: "未知",
+        1: "球机",
+        2: "半球",
+        3: "固定枪机",
+        4: "遥控枪机"
+      }
     };
   },
 
   mounted() {
     if (this.deviceId) {
-      this.deviceService.getDevice(this.deviceId, (result)=>{
-          this.device = result;
+      this.deviceService.getDevice(this.deviceId, (result) => {
+        this.device = result;
 
-      }, (error)=>{
+      }, (error) => {
         console.log("获取设备信息失败")
         console.error(error)
       })
@@ -227,6 +292,14 @@ export default {
         if (res.data.code === 0) {
           that.total = res.data.data.total;
           that.deviceChannelList = res.data.data.list;
+          that.deviceChannelList.forEach(e => {
+            e.PTZType = e.PTZType + "";
+            that.$set(e, "edit", false);
+            that.$set(e, "location", "");
+            if (e.longitude && e.latitude) {
+              that.$set(e, "location", e.longitude + "," + e.latitude);
+            }
+          });
           // 防止出现表格错位
           that.$nextTick(() => {
             that.$refs.channelListTable.doLayout();
@@ -248,7 +321,7 @@ export default {
       this.$axios({
         method: 'get',
         url: '/api/play/start/' + deviceId + '/' + channelId,
-        params:{
+        params: {
           isSubStream: this.isSubStream
         }
       }).then(function (res) {
@@ -271,7 +344,7 @@ export default {
             that.initData();
           }, 1000)
 
-        }else{
+        } else {
           that.$message.error(res.data.msg);
         }
       }).catch(function (e) {
@@ -297,7 +370,7 @@ export default {
       this.$axios({
         method: 'get',
         url: '/api/play/stop/' + this.deviceId + "/" + itemData.channelId,
-        params:{
+        params: {
           isSubStream: this.isSubStream
         }
       }).then(function (res) {
@@ -326,7 +399,7 @@ export default {
           return;
         }
         setTimeout(() => {
-          let url = (process.env.NODE_ENV === 'development'? "debug": "") + '/api/device/query/snap/' + deviceId + '/' + channelId
+          let url = (process.env.NODE_ENV === 'development' ? "debug" : "") + '/api/device/query/snap/' + deviceId + '/' + channelId
           this.loadSnap[deviceId + channelId]++
           document.getElementById(deviceId + channelId).setAttribute("src", url + '?' + new Date().getTime())
         }, 1000)
@@ -363,10 +436,18 @@ export default {
             online: this.online,
             channelType: this.channelType
           }
-        }).then( (res) =>{
+        }).then((res) => {
           if (res.data.code === 0) {
             this.total = res.data.data.total;
             this.deviceChannelList = res.data.data.list;
+            this.deviceChannelList.forEach(e => {
+              e.PTZType = e.PTZType + "";
+              this.$set(e, "edit", false);
+              this.$set(e, "location", "");
+              if (e.longitude && e.latitude) {
+                this.$set(e, "location", e.longitude + "," + e.latitude);
+              }
+            });
             // 防止出现表格错位
             this.$nextTick(() => {
               this.$refs.channelListTable.doLayout();
@@ -376,7 +457,7 @@ export default {
         }).catch(function (error) {
           console.log(error);
         });
-      }else {
+      } else {
         this.$axios({
           method: 'get',
           url: `/api/device/query/tree/channel/${this.deviceId}`,
@@ -385,7 +466,7 @@ export default {
             page: this.currentPage,
             count: this.count,
           }
-        }).then((res)=> {
+        }).then((res) => {
           if (res.data.code === 0) {
             this.total = res.data.total;
             this.deviceChannelList = res.data.list;
@@ -417,14 +498,14 @@ export default {
     refresh: function () {
       this.initData();
     },
-    switchTree: function (){
+    switchTree: function () {
       this.showTree = true;
       this.deviceChannelList = [];
       this.parentChannelId = 0;
       this.currentPage = 1;
 
     },
-    switchList: function (){
+    switchList: function () {
       this.showTree = false;
       this.deviceChannelList = [];
       this.parentChannelId = 0;
@@ -435,12 +516,70 @@ export default {
       console.log(device)
       if (!!!data.channelId) {
         this.parentChannelId = device.deviceId;
-      }else {
+      } else {
         this.parentChannelId = data.channelId;
       }
       this.initData();
-    }
+    },
+    // 保存
+    handleSave(row) {
+      if (row.location) {
+        const segements = row.location.split(",");
+        if (segements.length !== 2) {
+          this.$message.warning("位置信息格式有误,例:117.234,36.378");
+          return;
+        } else {
+          row.longitude = parseFloat(segements[0]);
+          row.latitude = parseFloat(segements[1]);
+          if (!(row.longitude && row.latitude)) {
+            this.$message.warning("位置信息格式有误,例:117.234,36.378");
+            return;
+          }
+        }
+      } else {
+        delete row.longitude;
+        delete row.latitude;
+      }
+      Object.keys(row).forEach(key => {
+        const value = row[key];
+        if (value === null || value === undefined || (typeof value === "string" && value.trim() === "")) {
+          delete row[key];
+        }
+      });
+      this.$axios({
+        method: 'post',
+        url: `/api/device/query/channel/update/${this.deviceId}`,
+        params: row
+      }).then(response => {
+        if (response.data.code === 0) {
+          this.$message.success("修改成功!");
+          this.initData();
+        } else {
+          this.$message.error("修改失败!");
+        }
+      }).catch(_ => {
+        this.$message.error("修改失败!");
+      })
+    },
+    // 是否正在编辑
+    isEdit() {
+      let editing = false;
+      this.deviceChannelList.forEach(e => {
+        if (e.edit) {
+          editing = true;
+        }
+      });
 
+      return editing;
+    },
+    // 编辑
+    handleEdit(row) {
+      if (this.isEdit()) {
+        this.$message.warning('请保存当前编辑项!');
+      } else {
+        row.edit = true;
+      }
+    }
   }
 };
 </script>

+ 1 - 1
web_src/src/components/dialog/easyPlayer.vue

@@ -1,5 +1,5 @@
 <template>
-  <div id="easyplayer"></div>
+  <div id="easyplayer" ></div>
 </template>
 
 <script>

+ 0 - 0
web_src/src/components/common/jessibuca.vue


Alguns arquivos não foram mostrados porque muitos arquivos mudaram nesse diff