瀏覽代碼

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

# Conflicts:
#	pom.xml
#	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/gb28181/transmit/event/request/impl/InviteRequestProcessor.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/PlatformServiceImpl.java
#	src/main/java/com/genersoft/iot/vmp/service/impl/PlayServiceImpl.java
#	src/main/java/com/genersoft/iot/vmp/storager/dao/PlatformChannelMapper.java
#	src/main/resources/all-application.yml
#	src/main/resources/application-dev.yml
648540858 1 年之前
父節點
當前提交
2e7c9a7341
共有 100 個文件被更改,包括 2909 次插入1198 次删除
  1. 二進制
      libs/jdbc-x86/bcprov-jdk15on-1.70.jar
  2. 二進制
      libs/jdbc-x86/kingbase8-8.6.0.jar
  3. 二進制
      libs/jdbc-x86/kingbase8-8.6.0.jre6.jar
  4. 二進制
      libs/jdbc-x86/kingbase8-8.6.0.jre7.jar
  5. 二進制
      libs/jdbc-x86/postgresql-42.2.9.jar
  6. 二進制
      libs/jdbc-x86/postgresql-42.2.9.jre6.jar
  7. 二進制
      libs/jdbc-x86/postgresql-42.2.9.jre7.jar
  8. 373 341
      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. 2 12
      src/main/java/com/genersoft/iot/vmp/conf/UserSetting.java
  17. 1 0
      src/main/java/com/genersoft/iot/vmp/conf/security/JwtAuthenticationFilter.java
  18. 2 1
      src/main/java/com/genersoft/iot/vmp/conf/security/JwtUtils.java
  19. 19 7
      src/main/java/com/genersoft/iot/vmp/conf/security/WebSecurityConfig.java
  20. 9 0
      src/main/java/com/genersoft/iot/vmp/conf/security/dto/JwtUser.java
  21. 127 95
      src/main/java/com/genersoft/iot/vmp/gb28181/conf/StackLoggerImpl.java
  22. 44 31
      src/main/java/com/genersoft/iot/vmp/gb28181/event/alarm/AlarmEventListener.java
  23. 6 1
      src/main/java/com/genersoft/iot/vmp/gb28181/event/record/RecordEndEventListener.java
  24. 4 4
      src/main/java/com/genersoft/iot/vmp/gb28181/event/subscribe/catalog/CatalogEventLister.java
  25. 2 1
      src/main/java/com/genersoft/iot/vmp/gb28181/session/SSRCFactory.java
  26. 40 2
      src/main/java/com/genersoft/iot/vmp/gb28181/session/VideoStreamSessionManager.java
  27. 2 0
      src/main/java/com/genersoft/iot/vmp/gb28181/task/SipRunner.java
  28. 2 2
      src/main/java/com/genersoft/iot/vmp/gb28181/transmit/SIPSender.java
  29. 2 0
      src/main/java/com/genersoft/iot/vmp/gb28181/transmit/cmd/SIPRequestHeaderProvider.java
  30. 19 13
      src/main/java/com/genersoft/iot/vmp/gb28181/transmit/cmd/impl/SIPCommander.java
  31. 13 8
      src/main/java/com/genersoft/iot/vmp/gb28181/transmit/cmd/impl/SIPCommanderFroPlatform.java
  32. 10 9
      src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/ByeRequestProcessor.java
  33. 21 14
      src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/InviteRequestProcessor.java
  34. 38 15
      src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/NotifyRequestForCatalogProcessor.java
  35. 20 16
      src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/RegisterRequestProcessor.java
  36. 7 2
      src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/notify/cmd/KeepaliveNotifyMessageHandler.java
  37. 1 0
      src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/query/cmd/RecordInfoQueryMessageHandler.java
  38. 5 1
      src/main/java/com/genersoft/iot/vmp/gb28181/utils/XmlUtil.java
  39. 162 42
      src/main/java/com/genersoft/iot/vmp/media/zlm/AssistRESTfulUtils.java
  40. 112 82
      src/main/java/com/genersoft/iot/vmp/media/zlm/ZLMHttpHookListener.java
  41. 10 2
      src/main/java/com/genersoft/iot/vmp/media/zlm/ZLMRESTfulUtils.java
  42. 11 0
      src/main/java/com/genersoft/iot/vmp/media/zlm/dto/HookSubscribeFactory.java
  43. 44 0
      src/main/java/com/genersoft/iot/vmp/media/zlm/dto/HookSubscribeForRecordMp4.java
  44. 20 10
      src/main/java/com/genersoft/iot/vmp/media/zlm/dto/MediaServerItem.java
  45. 11 1
      src/main/java/com/genersoft/iot/vmp/media/zlm/dto/hook/HookResultForOnPublish.java
  46. 114 0
      src/main/java/com/genersoft/iot/vmp/media/zlm/dto/hook/OnRecordMp4HookParam.java
  47. 93 28
      src/main/java/com/genersoft/iot/vmp/media/zlm/dto/hook/OnStreamChangedHookParam.java
  48. 59 0
      src/main/java/com/genersoft/iot/vmp/service/ICloudRecordService.java
  49. 1 10
      src/main/java/com/genersoft/iot/vmp/service/IMediaServerService.java
  50. 0 6
      src/main/java/com/genersoft/iot/vmp/service/IPlayService.java
  51. 1 0
      src/main/java/com/genersoft/iot/vmp/service/IStreamPushService.java
  52. 205 0
      src/main/java/com/genersoft/iot/vmp/service/bean/CloudRecordItem.java
  53. 41 0
      src/main/java/com/genersoft/iot/vmp/service/bean/DownloadFileInfo.java
  54. 5 5
      src/main/java/com/genersoft/iot/vmp/service/bean/WvpRedisMsg.java
  55. 235 0
      src/main/java/com/genersoft/iot/vmp/service/impl/CloudRecordServiceImpl.java
  56. 2 0
      src/main/java/com/genersoft/iot/vmp/service/impl/DeviceAlarmServiceImpl.java
  57. 6 0
      src/main/java/com/genersoft/iot/vmp/service/impl/DeviceChannelServiceImpl.java
  58. 24 14
      src/main/java/com/genersoft/iot/vmp/service/impl/DeviceServiceImpl.java
  59. 11 12
      src/main/java/com/genersoft/iot/vmp/service/impl/GbStreamServiceImpl.java
  60. 6 1
      src/main/java/com/genersoft/iot/vmp/service/impl/InviteStreamServiceImpl.java
  61. 2 0
      src/main/java/com/genersoft/iot/vmp/service/impl/LogServiceImpl.java
  62. 28 130
      src/main/java/com/genersoft/iot/vmp/service/impl/MediaServerServiceImpl.java
  63. 1 1
      src/main/java/com/genersoft/iot/vmp/service/impl/MediaServiceImpl.java
  64. 6 5
      src/main/java/com/genersoft/iot/vmp/service/impl/PlatformChannelServiceImpl.java
  65. 14 4
      src/main/java/com/genersoft/iot/vmp/service/impl/PlatformServiceImpl.java
  66. 137 60
      src/main/java/com/genersoft/iot/vmp/service/impl/PlayServiceImpl.java
  67. 2 0
      src/main/java/com/genersoft/iot/vmp/service/impl/RoleServerImpl.java
  68. 11 1
      src/main/java/com/genersoft/iot/vmp/service/impl/StreamProxyServiceImpl.java
  69. 7 0
      src/main/java/com/genersoft/iot/vmp/service/impl/StreamPushServiceImpl.java
  70. 2 0
      src/main/java/com/genersoft/iot/vmp/service/impl/UserServiceImpl.java
  71. 14 9
      src/main/java/com/genersoft/iot/vmp/service/redisMsg/RedisGbPlayMsgListener.java
  72. 3 2
      src/main/java/com/genersoft/iot/vmp/service/redisMsg/RedisGpsMsgListener.java
  73. 14 12
      src/main/java/com/genersoft/iot/vmp/service/redisMsg/RedisPushStreamCloseResponseListener.java
  74. 4 0
      src/main/java/com/genersoft/iot/vmp/storager/IRedisCatchStorage.java
  75. 122 0
      src/main/java/com/genersoft/iot/vmp/storager/dao/CloudRecordServiceMapper.java
  76. 88 12
      src/main/java/com/genersoft/iot/vmp/storager/dao/DeviceChannelMapper.java
  77. 2 2
      src/main/java/com/genersoft/iot/vmp/storager/dao/GbStreamMapper.java
  78. 12 0
      src/main/java/com/genersoft/iot/vmp/storager/dao/MediaServerMapper.java
  79. 6 5
      src/main/java/com/genersoft/iot/vmp/storager/dao/PlatformChannelMapper.java
  80. 10 4
      src/main/java/com/genersoft/iot/vmp/storager/dao/PlatformGbStreamMapper.java
  81. 5 4
      src/main/java/com/genersoft/iot/vmp/storager/dao/StreamPushMapper.java
  82. 17 2
      src/main/java/com/genersoft/iot/vmp/storager/impl/RedisCatchStorageImpl.java
  83. 2 0
      src/main/java/com/genersoft/iot/vmp/storager/impl/VideoManagerStorageImpl.java
  84. 22 0
      src/main/java/com/genersoft/iot/vmp/utils/CloudRecordUtils.java
  85. 31 0
      src/main/java/com/genersoft/iot/vmp/utils/DateUtil.java
  86. 16 0
      src/main/java/com/genersoft/iot/vmp/vmanager/bean/StreamContent.java
  87. 166 38
      src/main/java/com/genersoft/iot/vmp/vmanager/cloudRecord/CloudRecordController.java
  88. 7 5
      src/main/java/com/genersoft/iot/vmp/vmanager/gb28181/MobilePosition/MobilePositionController.java
  89. 0 37
      src/main/java/com/genersoft/iot/vmp/vmanager/gb28181/SseController/SseController.java
  90. 5 3
      src/main/java/com/genersoft/iot/vmp/vmanager/gb28181/alarm/AlarmController.java
  91. 4 2
      src/main/java/com/genersoft/iot/vmp/vmanager/gb28181/device/DeviceConfig.java
  92. 10 8
      src/main/java/com/genersoft/iot/vmp/vmanager/gb28181/device/DeviceControl.java
  93. 16 14
      src/main/java/com/genersoft/iot/vmp/vmanager/gb28181/device/DeviceQuery.java
  94. 8 6
      src/main/java/com/genersoft/iot/vmp/vmanager/gb28181/gbStream/GbStreamController.java
  95. 3 1
      src/main/java/com/genersoft/iot/vmp/vmanager/gb28181/media/MediaController.java
  96. 18 16
      src/main/java/com/genersoft/iot/vmp/vmanager/gb28181/platform/PlatformController.java
  97. 9 7
      src/main/java/com/genersoft/iot/vmp/vmanager/gb28181/play/PlayController.java
  98. 8 6
      src/main/java/com/genersoft/iot/vmp/vmanager/gb28181/playback/PlaybackController.java
  99. 5 3
      src/main/java/com/genersoft/iot/vmp/vmanager/gb28181/ptz/PtzController.java
  100. 0 0
      src/main/java/com/genersoft/iot/vmp/vmanager/gb28181/record/GBRecordController.java

二進制
libs/jdbc-x86/bcprov-jdk15on-1.70.jar


二進制
libs/jdbc-x86/kingbase8-8.6.0.jar


二進制
libs/jdbc-x86/kingbase8-8.6.0.jre6.jar


二進制
libs/jdbc-x86/kingbase8-8.6.0.jre7.jar


二進制
libs/jdbc-x86/postgresql-42.2.9.jar


二進制
libs/jdbc-x86/postgresql-42.2.9.jre6.jar


二進制
libs/jdbc-x86/postgresql-42.2.9.jre7.jar


+ 373 - 341
pom.xml

@@ -1,146 +1,147 @@
 <?xml version="1.0"?>
-<project 
-	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"
-	xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
-	<modelVersion>4.0.0</modelVersion>
-	<parent>
-		<groupId>org.springframework.boot</groupId>
-		<artifactId>spring-boot-starter-parent</artifactId>
-		<version>2.7.9</version>
-	</parent>
-
-	<groupId>com.genersoft</groupId>
-	<artifactId>wvp-pro</artifactId>
-	<version>2.6.9</version>
-	<name>web video platform</name>
-	<description>国标28181视频平台</description>
-	<packaging>${project.packaging}</packaging>
-
-	<repositories>
-		<repository>
-			<id>nexus-aliyun</id>
-			<name>Nexus aliyun</name>
-			<url>https://maven.aliyun.com/repository/public</url>
-			<layout>default</layout>
-			<snapshots>
-				<enabled>false</enabled>
-			</snapshots>
-			<releases>
-				<enabled>true</enabled>
-			</releases>
-		</repository>
-	</repositories>
-	<pluginRepositories>
-		<pluginRepository>
-			<id>nexus-aliyun</id>
-			<name>Nexus aliyun</name>
-			<url>https://maven.aliyun.com/repository/public</url>
-			<snapshots>
-				<enabled>false</enabled>
-			</snapshots>
-			<releases>
-				<enabled>true</enabled>
-			</releases>
-		</pluginRepository>
-	</pluginRepositories>
-
-	<properties>
-		<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
-		<maven.build.timestamp.format>MMddHHmm</maven.build.timestamp.format>
-		<maven-jar-plugin.version>3.1.1</maven-jar-plugin.version>
-
-		<!-- 依赖版本 -->
-		<snippetsDirectory>${project.build.directory}/generated-snippets</snippetsDirectory>
-		<asciidoctor.input.directory>${project.basedir}/docs/asciidoc</asciidoctor.input.directory>
-		<generated.asciidoc.directory>${project.build.directory}/asciidoc</generated.asciidoc.directory>
-		<asciidoctor.html.output.directory>${project.build.directory}/asciidoc/html</asciidoctor.html.output.directory>
-		<asciidoctor.pdf.output.directory>${project.build.directory}/asciidoc/pdf</asciidoctor.pdf.output.directory>
-	</properties>
-
-	<profiles>
-		<profile>
-			<id>jar</id>
-			<activation>
-				<activeByDefault>true</activeByDefault>
-			</activation>
-			<properties>
-				<project.packaging>jar</project.packaging>
-			</properties>
-		</profile>
-		<profile>
-			<id>war</id>
-			<properties>
-				<project.packaging>war</project.packaging>
-			</properties>
-			<dependencies>
-				<dependency>
-					<groupId>org.springframework.boot</groupId>
-					<artifactId>spring-boot-starter-web</artifactId>
-					<exclusions>
-						<exclusion>
-							<groupId>org.springframework.boot</groupId>
-							<artifactId>spring-boot-starter-jetty</artifactId>
-						</exclusion>
-					</exclusions>
-				</dependency>
-				<dependency>
-					<groupId>javax.servlet</groupId>
-					<artifactId>javax.servlet-api</artifactId>
-					<version>3.1.0</version>
-					<scope>provided</scope>
-				</dependency>
-			</dependencies>
-		</profile>
-	</profiles>
-
-	<dependencies>
-		<dependency>
-			<groupId>org.springframework.boot</groupId>
-			<artifactId>spring-boot-starter-data-redis</artifactId>
-		</dependency>
-		<dependency>
-			<groupId>org.springframework.boot</groupId>
-			<artifactId>spring-boot-starter-web</artifactId>
-		</dependency>
-		<dependency>
-			<groupId>org.springframework.boot</groupId>
-			<artifactId>spring-boot-configuration-processor</artifactId>
-			<optional>true</optional>
-		</dependency>
-		<dependency>
-			<groupId>org.mybatis.spring.boot</groupId>
-			<artifactId>mybatis-spring-boot-starter</artifactId>
-			<version>2.2.2</version>
-			<exclusions>
-				<exclusion>
-					<groupId>com.zaxxer</groupId>
-					<artifactId>HikariCP</artifactId>
-				</exclusion>
-			</exclusions>
-		</dependency>
-		<dependency>
-			<groupId>org.springframework.boot</groupId>
-			<artifactId>spring-boot-starter-security</artifactId>
-		</dependency>
-
-		<dependency>
-			<groupId>org.springframework.boot</groupId>
-			<artifactId>spring-boot-starter-jdbc</artifactId>
-		</dependency>
-
-		<!-- mysql数据库 -->
-		<dependency>
-			<groupId>mysql</groupId>
-			<artifactId>mysql-connector-java</artifactId>
-			<version>8.0.30</version>
-		</dependency>
-
-		<!--postgresql-->
-		<dependency>
-			<groupId>org.postgresql</groupId>
-			<artifactId>postgresql</artifactId>
-			<version>42.5.1</version>
-		</dependency>
+<project
+        xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"
+        xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
+    <modelVersion>4.0.0</modelVersion>
+    <parent>
+        <groupId>org.springframework.boot</groupId>
+        <artifactId>spring-boot-starter-parent</artifactId>
+        <version>2.7.17</version>
+    </parent>
+
+    <groupId>com.genersoft</groupId>
+    <artifactId>wvp-pro</artifactId>
+    <version>2.7.0</version>
+    <name>web video platform</name>
+    <description>国标28181视频平台</description>
+    <packaging>${project.packaging}</packaging>
+
+    <repositories>
+        <repository>
+            <id>nexus-aliyun</id>
+            <name>Nexus aliyun</name>
+            <url>https://maven.aliyun.com/repository/public</url>
+            <layout>default</layout>
+            <snapshots>
+                <enabled>false</enabled>
+            </snapshots>
+            <releases>
+                <enabled>true</enabled>
+            </releases>
+        </repository>
+    </repositories>
+
+    <pluginRepositories>
+        <pluginRepository>
+            <id>nexus-aliyun</id>
+            <name>Nexus aliyun</name>
+            <url>https://maven.aliyun.com/repository/public</url>
+            <snapshots>
+                <enabled>false</enabled>
+            </snapshots>
+            <releases>
+                <enabled>true</enabled>
+            </releases>
+        </pluginRepository>
+    </pluginRepositories>
+
+    <properties>
+        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+        <maven.build.timestamp.format>MMddHHmm</maven.build.timestamp.format>
+        <maven-jar-plugin.version>3.1.1</maven-jar-plugin.version>
+
+        <!-- 依赖版本 -->
+        <snippetsDirectory>${project.build.directory}/generated-snippets</snippetsDirectory>
+        <asciidoctor.input.directory>${project.basedir}/docs/asciidoc</asciidoctor.input.directory>
+        <generated.asciidoc.directory>${project.build.directory}/asciidoc</generated.asciidoc.directory>
+        <asciidoctor.html.output.directory>${project.build.directory}/asciidoc/html</asciidoctor.html.output.directory>
+        <asciidoctor.pdf.output.directory>${project.build.directory}/asciidoc/pdf</asciidoctor.pdf.output.directory>
+    </properties>
+
+    <profiles>
+        <profile>
+            <id>jar</id>
+            <activation>
+                <activeByDefault>true</activeByDefault>
+            </activation>
+            <properties>
+                <project.packaging>jar</project.packaging>
+            </properties>
+        </profile>
+        <profile>
+            <id>war</id>
+            <properties>
+                <project.packaging>war</project.packaging>
+            </properties>
+            <dependencies>
+                <dependency>
+                    <groupId>org.springframework.boot</groupId>
+                    <artifactId>spring-boot-starter-web</artifactId>
+                    <exclusions>
+                        <exclusion>
+                            <groupId>org.springframework.boot</groupId>
+                            <artifactId>spring-boot-starter-jetty</artifactId>
+                        </exclusion>
+                    </exclusions>
+                </dependency>
+                <dependency>
+                    <groupId>javax.servlet</groupId>
+                    <artifactId>javax.servlet-api</artifactId>
+                    <version>3.1.0</version>
+                    <scope>provided</scope>
+                </dependency>
+            </dependencies>
+        </profile>
+    </profiles>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-data-redis</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-web</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-configuration-processor</artifactId>
+            <optional>true</optional>
+        </dependency>
+        <dependency>
+            <groupId>org.mybatis.spring.boot</groupId>
+            <artifactId>mybatis-spring-boot-starter</artifactId>
+            <version>2.2.2</version>
+            <exclusions>
+                <exclusion>
+                    <groupId>com.zaxxer</groupId>
+                    <artifactId>HikariCP</artifactId>
+                </exclusion>
+            </exclusions>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-security</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-jdbc</artifactId>
+        </dependency>
+
+        <!-- mysql数据库 -->
+        <dependency>
+            <groupId>com.mysql</groupId>
+            <artifactId>mysql-connector-j</artifactId>
+            <version>8.2.0</version>
+        </dependency>
+
+        <!--postgresql-->
+        <dependency>
+            <groupId>org.postgresql</groupId>
+            <artifactId>postgresql</artifactId>
+            <version>42.5.1</version>
+        </dependency>
 
 		<!-- kingbase人大金仓 -->
 		<!-- 手动下载驱动后安装 -->
@@ -153,14 +154,41 @@
 			<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分页插件 -->
+        <!--Mybatis分页插件 -->
+        <dependency>
+            <groupId>com.github.pagehelper</groupId>
+            <artifactId>pagehelper-spring-boot-starter</artifactId>
+            <version>1.4.6</version>
+        </dependency>
+
+        <!--在线文档 -->
+        <!--在线文档 -->
+        <dependency>
+            <groupId>org.springdoc</groupId>
+            <artifactId>springdoc-openapi-ui</artifactId>
+            <version>1.6.10</version>
+        </dependency>
+        <dependency>
+            <groupId>org.springdoc</groupId>
+            <artifactId>springdoc-openapi-security</artifactId>
+            <version>1.6.10</version>
+        </dependency>
+		<!-- https://mvnrepository.com/artifact/com.baomidou/dynamic-datasource-spring-boot-starter -->
 		<dependency>
-			<groupId>com.github.pagehelper</groupId>
-			<artifactId>pagehelper-spring-boot-starter</artifactId>
-			<version>1.4.6</version>
+			<groupId>com.baomidou</groupId>
+			<artifactId>dynamic-datasource-spring-boot-starter</artifactId>
+			<version>3.6.1</version>
 		</dependency>
 
+
 		<!--在线文档 -->
 		<dependency>
 			<groupId>org.springdoc</groupId>
@@ -168,199 +196,203 @@
 			<version>1.6.10</version>
 		</dependency>
 
-		<dependency>
-			<groupId>com.github.xiaoymin</groupId>
-			<artifactId>knife4j-springdoc-ui</artifactId>
-			<version>3.0.3</version>
-		</dependency>
-
-		<!--参数校验 -->
-		<dependency>
-			<groupId>javax.validation</groupId>
-			<artifactId>validation-api</artifactId>
-		</dependency>
-
-		<!-- 日志相关 -->
-		<dependency>
-			<groupId>org.springframework.boot</groupId>
-			<artifactId>spring-boot-starter-aop</artifactId>
-		</dependency>
-
-		<!-- sip协议栈 -->
-		<dependency>
-			<groupId>javax.sip</groupId>
-			<artifactId>jain-sip-ri</artifactId>
-			<version>1.3.0-91</version>
-		</dependency>
-
-		<!-- 取代log4j -->
-		<dependency>
-			<groupId>org.slf4j</groupId>
-			<artifactId>log4j-over-slf4j</artifactId>
-			<version>1.7.36</version>
-		</dependency>
-
-		<!-- xml解析库 -->
-		<dependency>
-			<groupId>org.dom4j</groupId>
-			<artifactId>dom4j</artifactId>
-			<version>2.1.3</version>
-		</dependency>
-
-		<dependency>
-			<groupId>com.google.guava</groupId>
-			<artifactId>guava</artifactId>
-			<version>20.0</version>
-		</dependency>
-
-		<!-- json解析库fastjson2 -->
-		<dependency>
-			<groupId>com.alibaba.fastjson2</groupId>
-			<artifactId>fastjson2</artifactId>
-			<version>2.0.17</version>
-		</dependency>
-		<dependency>
-			<groupId>com.alibaba.fastjson2</groupId>
-			<artifactId>fastjson2-extension</artifactId>
-			<version>2.0.17</version>
-		</dependency>
-
-		<!-- okhttp -->
-		<dependency>
-			<groupId>com.squareup.okhttp3</groupId>
-			<artifactId>okhttp</artifactId>
-			<version>4.10.0</version>
-		</dependency>
-
-		<!-- okhttp 调试日志 -->
-		<dependency>
-			<groupId>com.squareup.okhttp3</groupId>
-			<artifactId>logging-interceptor</artifactId>
-			<version>4.10.0</version>
-		</dependency>
-
-		<!-- okhttp-digest -->
-		<dependency>
-			<groupId>io.github.rburgst</groupId>
-			<artifactId>okhttp-digest</artifactId>
-			<version>2.7</version>
-		</dependency>
-
-		<!-- https://mvnrepository.com/artifact/net.sf.kxml/kxml2 -->
-<!--		<dependency>-->
-<!--			<groupId>net.sf.kxml</groupId>-->
-<!--			<artifactId>kxml2</artifactId>-->
-<!--			<version>2.3.0</version>-->
-<!--		</dependency>-->
-
-		<!-- jwt实现 -->
-		<dependency>
-			<groupId>org.bitbucket.b_c</groupId>
-			<artifactId>jose4j</artifactId>
-			<version>0.9.3</version>
-		</dependency>
-
-		<!--反向代理-->
-		<dependency>
-			<groupId>org.mitre.dsmiley.httpproxy</groupId>
-			<artifactId>smiley-http-proxy-servlet</artifactId>
-			<version>1.12.1</version>
-		</dependency>
-
-		<!--excel解析库-->
-		<dependency>
-			<groupId>com.alibaba</groupId>
-			<artifactId>easyexcel</artifactId>
-			<version>3.1.1</version>
-		</dependency>
-
-		<!-- 获取系统信息 -->
-		<dependency>
-			<groupId>com.github.oshi</groupId>
-			<artifactId>oshi-core</artifactId>
-			<version>6.2.2</version>
-		</dependency>
-
-		<dependency>
-			<groupId>org.springframework.session</groupId>
-			<artifactId>spring-session-core</artifactId>
-		</dependency>
-
-<!--		&lt;!&ndash; 检测文件编码 &ndash;&gt;-->
-<!--		&lt;!&ndash; https://mvnrepository.com/artifact/cpdetector/cpdetector &ndash;&gt;-->
-<!--		<dependency>-->
-<!--			<groupId>cpdetector</groupId>-->
-<!--			<artifactId>cpdetector</artifactId>-->
-<!--			<version>1.0.8</version>-->
-<!--		</dependency>-->
-
-		<!-- https://mvnrepository.com/artifact/com.google.guava/guava -->
-		<dependency>
-			<groupId>com.google.guava</groupId>
-			<artifactId>guava</artifactId>
-			<version>31.1-jre</version>
-		</dependency>
-
-
-		<dependency>
-			<groupId>org.springframework.boot</groupId>
-			<artifactId>spring-boot-starter-test</artifactId>
-<!--			<scope>test</scope>-->
-		</dependency>
-
-	</dependencies>
-
-
-	<build>
-		<finalName>${project.artifactId}-${project.version}-${maven.build.timestamp}</finalName>
-		<plugins>
-			<plugin>
-				<groupId>org.springframework.boot</groupId>
-				<artifactId>spring-boot-maven-plugin</artifactId>
-				<version>2.7.2</version>
-				<configuration>
-					<includeSystemScope>true</includeSystemScope>
-				</configuration>
-			</plugin>
-			<plugin>
-				<groupId>org.apache.maven.plugins</groupId>
-				<artifactId>maven-compiler-plugin</artifactId>
-				<version>3.8.1</version>
-				<configuration>
-					<source>1.8</source>
-					<target>1.8</target>
-				</configuration>
-			</plugin>
-
-			<plugin>
-				<groupId>pl.project13.maven</groupId>
-				<artifactId>git-commit-id-plugin</artifactId>
-				<version>3.0.1</version>
-				<configuration>
-					<offline>true</offline>
-					<failOnNoGitDirectory>false</failOnNoGitDirectory>
-					<dateFormat>yyyyMMdd</dateFormat>
-				</configuration>
-			</plugin>
-			<plugin>
-				<groupId>org.apache.maven.plugins</groupId>
-				<artifactId>maven-surefire-plugin</artifactId>
-				<version>2.22.2</version>
-				<configuration>
-					<skipTests>true</skipTests>
-				</configuration>
-			</plugin>
-		</plugins>
-		<resources>
-			<resource>
-				<directory>src/main/resources</directory>
-			</resource>
-			<resource>
-				<directory>src/main/java</directory>
-				<includes>
-					<include>**/*.xml</include>
-				</includes>
-			</resource>
-		</resources>
-	</build>
+        <dependency>
+            <groupId>com.github.xiaoymin</groupId>
+            <artifactId>knife4j-springdoc-ui</artifactId>
+            <version>3.0.3</version>
+        </dependency>
+
+        <!--参数校验 -->
+        <dependency>
+            <groupId>javax.validation</groupId>
+            <artifactId>validation-api</artifactId>
+        </dependency>
+
+        <!-- 日志相关 -->
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-aop</artifactId>
+        </dependency>
+
+        <!-- sip协议栈 -->
+        <dependency>
+            <groupId>javax.sip</groupId>
+            <artifactId>jain-sip-ri</artifactId>
+            <version>1.3.0-91</version>
+        </dependency>
+
+        <!-- 取代log4j -->
+        <dependency>
+            <groupId>org.slf4j</groupId>
+            <artifactId>log4j-over-slf4j</artifactId>
+            <version>1.7.36</version>
+        </dependency>
+
+        <!-- xml解析库 -->
+        <dependency>
+            <groupId>org.dom4j</groupId>
+            <artifactId>dom4j</artifactId>
+            <version>2.1.3</version>
+        </dependency>
+
+        <!-- json解析库fastjson2 -->
+        <dependency>
+            <groupId>com.alibaba.fastjson2</groupId>
+            <artifactId>fastjson2</artifactId>
+            <version>2.0.17</version>
+        </dependency>
+        <dependency>
+            <groupId>com.alibaba.fastjson2</groupId>
+            <artifactId>fastjson2-extension</artifactId>
+            <version>2.0.17</version>
+        </dependency>
+
+        <!-- okhttp -->
+        <dependency>
+            <groupId>com.squareup.okhttp3</groupId>
+            <artifactId>okhttp</artifactId>
+            <version>4.10.0</version>
+        </dependency>
+
+        <!-- okhttp 调试日志 -->
+        <dependency>
+            <groupId>com.squareup.okhttp3</groupId>
+            <artifactId>logging-interceptor</artifactId>
+            <version>4.10.0</version>
+        </dependency>
+
+        <!-- okhttp-digest -->
+        <dependency>
+            <groupId>io.github.rburgst</groupId>
+            <artifactId>okhttp-digest</artifactId>
+            <version>2.7</version>
+        </dependency>
+
+        <!-- https://mvnrepository.com/artifact/net.sf.kxml/kxml2 -->
+        <!--		<dependency>-->
+        <!--			<groupId>net.sf.kxml</groupId>-->
+        <!--			<artifactId>kxml2</artifactId>-->
+        <!--			<version>2.3.0</version>-->
+        <!--		</dependency>-->
+
+        <!-- jwt实现 -->
+        <dependency>
+            <groupId>org.bitbucket.b_c</groupId>
+            <artifactId>jose4j</artifactId>
+            <version>0.9.3</version>
+        </dependency>
+
+        <!--反向代理-->
+        <dependency>
+            <groupId>org.mitre.dsmiley.httpproxy</groupId>
+            <artifactId>smiley-http-proxy-servlet</artifactId>
+            <version>1.12.1</version>
+        </dependency>
+
+        <!--excel解析库-->
+        <dependency>
+            <groupId>com.alibaba</groupId>
+            <artifactId>easyexcel</artifactId>
+            <version>3.3.2</version>
+            <exclusions>
+                <exclusion>
+                    <groupId>org.apache.commons</groupId>
+                    <artifactId>commons-compress</artifactId>
+                </exclusion>
+            </exclusions>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.commons</groupId>
+            <artifactId>commons-compress</artifactId>
+            <version>1.24.0</version>
+        </dependency>
+
+        <!-- 获取系统信息 -->
+        <dependency>
+            <groupId>com.github.oshi</groupId>
+            <artifactId>oshi-core</artifactId>
+            <version>6.2.2</version>
+        </dependency>
+
+        <dependency>
+            <groupId>org.springframework.session</groupId>
+            <artifactId>spring-session-core</artifactId>
+        </dependency>
+
+        <!-- 检测文件编码 -->
+        <!-- https://mvnrepository.com/artifact/cpdetector/cpdetector -->
+        <!--<dependency>-->
+        <!--    <groupId>cpdetector</groupId>-->
+        <!--    <artifactId>cpdetector</artifactId>-->
+        <!--    <version>1.0.8</version>-->
+        <!--</dependency>-->
+
+        <!-- https://mvnrepository.com/artifact/com.google.guava/guava -->
+        <dependency>
+            <groupId>com.google.guava</groupId>
+            <artifactId>guava</artifactId>
+            <version>32.1.3-jre</version>
+        </dependency>
+
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-test</artifactId>
+            <scope>test</scope>
+        </dependency>
+    </dependencies>
+
+    <build>
+        <finalName>${project.artifactId}-${project.version}-${maven.build.timestamp}</finalName>
+        <plugins>
+            <plugin>
+                <groupId>org.springframework.boot</groupId>
+                <artifactId>spring-boot-maven-plugin</artifactId>
+                <version>2.7.2</version>
+                <configuration>
+                    <includeSystemScope>true</includeSystemScope>
+                </configuration>
+            </plugin>
+
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-compiler-plugin</artifactId>
+                <version>3.8.1</version>
+                <configuration>
+                    <source>1.8</source>
+                    <target>1.8</target>
+                </configuration>
+            </plugin>
+
+            <plugin>
+                <groupId>pl.project13.maven</groupId>
+                <artifactId>git-commit-id-plugin</artifactId>
+                <version>3.0.1</version>
+                <configuration>
+                    <offline>true</offline>
+                    <failOnNoGitDirectory>false</failOnNoGitDirectory>
+                    <dateFormat>yyyyMMdd</dateFormat>
+                </configuration>
+            </plugin>
+
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-surefire-plugin</artifactId>
+                <version>2.22.2</version>
+                <configuration>
+                    <skipTests>true</skipTests>
+                </configuration>
+            </plugin>
+        </plugins>
+        <resources>
+            <resource>
+                <directory>src/main/resources</directory>
+            </resource>
+            <resource>
+                <directory>src/main/java</directory>
+                <includes>
+                    <include>**/*.xml</include>
+                </includes>
+            </resource>
+        </resources>
+    </build>
 </project>

+ 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 {
         }
 
     }
+
+
 }

+ 2 - 12
src/main/java/com/genersoft/iot/vmp/conf/UserSetting.java

@@ -23,7 +23,7 @@ public class UserSetting {
 
     private Integer playTimeout = 18000;
 
-    private int platformPlayTimeout = 60000;
+    private int platformPlayTimeout = 20000;
 
     private Boolean interfaceAuthentication = Boolean.TRUE;
 
@@ -51,13 +51,11 @@ public class UserSetting {
 
     private Boolean refuseChannelStatusChannelFormNotify = Boolean.FALSE;
 
-    private Boolean deviceStatusNotify = Boolean.FALSE;
+    private Boolean deviceStatusNotify = Boolean.TRUE;
     private Boolean useCustomSsrcForParentInvite = Boolean.TRUE;
 
     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 - 0
src/main/java/com/genersoft/iot/vmp/conf/security/JwtAuthenticationFilter.java

@@ -78,6 +78,7 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter {
 
         // 构建UsernamePasswordAuthenticationToken,这里密码为null,是因为提供了正确的JWT,实现自动登录
         User user = new User();
+        user.setId(jwtUser.getUserId());
         user.setUsername(jwtUser.getUserName());
         user.setPassword(jwtUser.getPassword());
         Role role = new Role();

+ 2 - 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";
 
@@ -144,6 +144,7 @@ public class JwtUtils implements InitializingBean {
             jwtUser.setUserName(username);
             jwtUser.setPassword(user.getPassword());
             jwtUser.setRoleId(user.getRole().getId());
+            jwtUser.setUserId(user.getId());
 
             return jwtUser;
         } catch (InvalidJwtException e) {

+ 19 - 7
src/main/java/com/genersoft/iot/vmp/conf/security/WebSecurityConfig.java

@@ -1,12 +1,12 @@
 package com.genersoft.iot.vmp.conf.security;
 
 import com.genersoft.iot.vmp.conf.UserSetting;
-import org.springframework.core.annotation.Order;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
+import org.springframework.core.annotation.Order;
 import org.springframework.security.authentication.AuthenticationManager;
 import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
 import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
@@ -25,9 +25,11 @@ import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
 
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Collections;
 
 /**
  * 配置Spring Security
+ *
  * @author lin
  */
 @Configuration
@@ -67,6 +69,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/**");
@@ -75,7 +79,8 @@ public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
             matchers.add("/js/**");
             matchers.add("/api/device/query/snap/**");
             matchers.add("/record_proxy/*/**");
-            matchers.addAll(userSetting.getInterfaceAuthenticationExcludes());
+            matchers.add("/api/emit");
+            matchers.add("/favicon.ico");
             // 可以直接访问的静态数据
             web.ignoring().antMatchers(matchers.toArray(new String[0]));
         }
@@ -83,6 +88,7 @@ public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
 
     /**
      * 配置认证方式
+     *
      * @param auth
      * @throws Exception
      */
@@ -111,7 +117,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/**","/zlm_Proxy/FhTuMYqB2HeCuNOb/record/t/1/2023-03-25/16:35:07-16:35:16-9353.mp4").permitAll()
+                .antMatchers("/api/user/login", "/index/hook/**", "/swagger-ui/**", "/doc.html").permitAll()
                 .anyRequest().authenticated()
                 // 异常处理器
                 .and()
@@ -124,18 +130,24 @@ public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
 
     }
 
-    CorsConfigurationSource configurationSource(){
+    CorsConfigurationSource configurationSource() {
         // 配置跨域
         CorsConfiguration corsConfiguration = new CorsConfiguration();
         corsConfiguration.setAllowedHeaders(Arrays.asList("*"));
         corsConfiguration.setAllowedMethods(Arrays.asList("*"));
         corsConfiguration.setMaxAge(3600L);
-        corsConfiguration.setAllowCredentials(true);
-        corsConfiguration.setAllowedOrigins(userSetting.getAllowedOrigins());
+        if (userSetting.getAllowedOrigins() != null && !userSetting.getAllowedOrigins().isEmpty()) {
+            corsConfiguration.setAllowCredentials(true);
+            corsConfiguration.setAllowedOrigins(userSetting.getAllowedOrigins());
+        }else {
+            corsConfiguration.setAllowCredentials(false);
+            corsConfiguration.setAllowedOrigins(Collections.singletonList(CorsConfiguration.ALL));
+        }
+
         corsConfiguration.setExposedHeaders(Arrays.asList(JwtUtils.getHeader()));
 
         UrlBasedCorsConfigurationSource url = new UrlBasedCorsConfigurationSource();
-        url.registerCorsConfiguration("/**",corsConfiguration);
+        url.registerCorsConfiguration("/**", corsConfiguration);
         return url;
     }
 

+ 9 - 0
src/main/java/com/genersoft/iot/vmp/conf/security/dto/JwtUser.java

@@ -21,6 +21,7 @@ public class JwtUser {
         EXCEPTION
     }
 
+    private int userId;
     private String userName;
 
     private String password;
@@ -29,6 +30,14 @@ public class JwtUser {
 
     private TokenStatus status;
 
+    public int getUserId() {
+        return userId;
+    }
+
+    public void setUserId(int userId) {
+        this.userId = userId;
+    }
+
     public String getUserName() {
         return userName;
     }

+ 127 - 95
src/main/java/com/genersoft/iot/vmp/gb28181/conf/StackLoggerImpl.java

@@ -1,8 +1,8 @@
 package com.genersoft.iot.vmp.gb28181.conf;
 
 import gov.nist.core.StackLogger;
-import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
+import org.slf4j.spi.LocationAwareLogger;
 import org.springframework.stereotype.Component;
 
 import java.util.Properties;
@@ -10,100 +10,132 @@ import java.util.Properties;
 @Component
 public class StackLoggerImpl implements StackLogger {
 
-    private final static Logger logger = LoggerFactory.getLogger(StackLoggerImpl.class);
+	/**
+	 * 完全限定类名(Fully Qualified Class Name),用于定位日志位置
+	 */
+	private static final String FQCN = StackLoggerImpl.class.getName();
+
+	/**
+	 * 获取栈中类信息(以便底层日志记录系统能够提取正确的位置信息(方法名、行号))
+	 * @return LocationAwareLogger
+	 */
+	private static LocationAwareLogger getLocationAwareLogger() {
+		return (LocationAwareLogger) LoggerFactory.getLogger(new Throwable().getStackTrace()[4].getClassName());
+	}
+
+
+	/**
+	 * 封装打印日志的位置信息
+	 * @param level   日志级别
+	 * @param message 日志事件的消息
+	 */
+	private static void log(int level, String message) {
+		LocationAwareLogger locationAwareLogger = getLocationAwareLogger();
+		locationAwareLogger.log(null, FQCN, level, message, null, null);
+	}
+
+	/**
+	 * 封装打印日志的位置信息
+	 * @param level   日志级别
+	 * @param message 日志事件的消息
+	 */
+	private static void log(int level, String message, Throwable throwable) {
+		LocationAwareLogger locationAwareLogger = getLocationAwareLogger();
+		locationAwareLogger.log(null, FQCN, level, message, null, throwable);
+	}
+
+	@Override
+	public void logStackTrace() {
+
+	}
+
+	@Override
+	public void logStackTrace(int traceLevel) {
+		System.out.println("traceLevel: " + traceLevel);
+	}
+
+	@Override
+	public int getLineCount() {
+		return 0;
+	}
+
+	@Override
+	public void logException(Throwable ex) {
+
+	}
+
+	@Override
+	public void logDebug(String message) {
+		log(LocationAwareLogger.INFO_INT, message);
+	}
+
+	@Override
+	public void logDebug(String message, Exception ex) {
+		log(LocationAwareLogger.INFO_INT, message, ex);
+	}
+
+	@Override
+	public void logTrace(String message) {
+		log(LocationAwareLogger.INFO_INT, message);
+	}
+
+	@Override
+	public void logFatalError(String message) {
+		log(LocationAwareLogger.INFO_INT, message);
+	}
+
+	@Override
+	public void logError(String message) {
+		log(LocationAwareLogger.INFO_INT, message);
+	}
+
+	@Override
+	public boolean isLoggingEnabled() {
+		return true;
+	}
+
+	@Override
+	public boolean isLoggingEnabled(int logLevel) {
+		return true;
+	}
+
+	@Override
+	public void logError(String message, Exception ex) {
+		log(LocationAwareLogger.INFO_INT, message, ex);
+	}
+
+	@Override
+	public void logWarning(String message) {
+		log(LocationAwareLogger.INFO_INT, message);
+	}
+
+	@Override
+	public void logInfo(String message) {
+		log(LocationAwareLogger.INFO_INT, message);
+	}
+
+	@Override
+	public void disableLogging() {
+
+	}
+
+	@Override
+	public void enableLogging() {
+
+	}
+
+	@Override
+	public void setBuildTimeStamp(String buildTimeStamp) {
+
+	}
+
+	@Override
+	public void setStackProperties(Properties stackProperties) {
 
-    @Override
-    public void logStackTrace() {
+	}
 
-    }
-
-    @Override
-    public void logStackTrace(int traceLevel) {
-        System.out.println("traceLevel: "  + traceLevel);
-    }
-
-    @Override
-    public int getLineCount() {
-        return 0;
-    }
-
-    @Override
-    public void logException(Throwable ex) {
-
-    }
-
-    @Override
-    public void logDebug(String message) {
-//        logger.debug(message);
-    }
-
-    @Override
-    public void logDebug(String message, Exception ex) {
-//        logger.debug(message);
-    }
-
-    @Override
-    public void logTrace(String message) {
-        logger.trace(message);
-    }
-
-    @Override
-    public void logFatalError(String message) {
-//        logger.error(message);
-    }
-
-    @Override
-    public void logError(String message) {
-//        logger.error(message);
-    }
-
-    @Override
-    public boolean isLoggingEnabled() {
-        return true;
-    }
-
-    @Override
-    public boolean isLoggingEnabled(int logLevel) {
-        return true;
-    }
-
-    @Override
-    public void logError(String message, Exception ex) {
-//        logger.error(message);
-    }
-
-    @Override
-    public void logWarning(String message) {
-        logger.warn(message);
-    }
-
-    @Override
-    public void logInfo(String message) {
-        logger.info(message);
-    }
-
-    @Override
-    public void disableLogging() {
-
-    }
-
-    @Override
-    public void enableLogging() {
-
-    }
-
-    @Override
-    public void setBuildTimeStamp(String buildTimeStamp) {
-
-    }
-
-    @Override
-    public void setStackProperties(Properties stackProperties) {
-
-    }
-
-    @Override
-    public String getLoggerName() {
-        return null;
-    }
+	@Override
+	public String getLoggerName() {
+		return null;
+	}
 }

+ 44 - 31
src/main/java/com/genersoft/iot/vmp/gb28181/event/alarm/AlarmEventListener.java

@@ -1,55 +1,68 @@
 package com.genersoft.iot.vmp.gb28181.event.alarm;
 
+import org.jetbrains.annotations.NotNull;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 import org.springframework.context.ApplicationListener;
 import org.springframework.stereotype.Component;
-import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
-import java.io.IOException;
-import java.util.Hashtable;
+
+import java.io.PrintWriter;
 import java.util.Iterator;
 import java.util.Map;
-
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
+import java.util.concurrent.ConcurrentHashMap;
 
 /**
- * @description: 报警事件监听
- * @author: lawrencehj
- * @data: 2021-01-20
+ * 报警事件监听器.
+ *
+ * @author lawrencehj
+ * @author <a href="mailto:xiaoQQya@126.com">xiaoQQya</a>
+ * @since 2021/01/20
  */
-
 @Component
 public class AlarmEventListener implements ApplicationListener<AlarmEvent> {
 
-    private final static Logger logger = LoggerFactory.getLogger(AlarmEventListener.class);
+    private static final Logger logger = LoggerFactory.getLogger(AlarmEventListener.class);
 
-    private static Map<String, SseEmitter> sseEmitters = new Hashtable<>();
+    private static final Map<String, PrintWriter> SSE_CACHE = new ConcurrentHashMap<>();
 
-    public void addSseEmitters(String browserId, SseEmitter sseEmitter) {
-        sseEmitters.put(browserId, sseEmitter);
+    public void addSseEmitter(String browserId, PrintWriter writer) {
+        SSE_CACHE.put(browserId, writer);
+        logger.info("SSE 在线数量: {}", SSE_CACHE.size());
+    }
+
+    public void removeSseEmitter(String browserId, PrintWriter writer) {
+        SSE_CACHE.remove(browserId, writer);
+        logger.info("SSE 在线数量: {}", SSE_CACHE.size());
     }
 
     @Override
-    public void onApplicationEvent(AlarmEvent event) {
+    public void onApplicationEvent(@NotNull AlarmEvent event) {
         if (logger.isDebugEnabled()) {
-            logger.debug("设备报警事件触发,deviceId:" + event.getAlarmInfo().getDeviceId() + ", "
-                    + event.getAlarmInfo().getAlarmDescription());
+            logger.debug("设备报警事件触发, deviceId: {}, {}", event.getAlarmInfo().getDeviceId(), event.getAlarmInfo().getAlarmDescription());
         }
-        String msg = "<strong>设备编码:</strong> <i>" + event.getAlarmInfo().getDeviceId() + "</i>"
-                    + "<br><strong>报警描述:</strong> <i>" + event.getAlarmInfo().getAlarmDescription() + "</i>"
-                    + "<br><strong>报警时间:</strong> <i>" + event.getAlarmInfo().getAlarmTime() + "</i>"
-                    + "<br><strong>报警位置:</strong> <i>" + event.getAlarmInfo().getLongitude() + "</i>"
-                    + ", <i>" + event.getAlarmInfo().getLatitude() + "</i>";
-
-        for (Iterator<Map.Entry<String, SseEmitter>> it = sseEmitters.entrySet().iterator(); it.hasNext();) {
-            Map.Entry<String, SseEmitter> emitter = it.next();
-            logger.info("推送到SSE连接,浏览器ID: " + emitter.getKey());
+
+        String msg = "<strong>设备编号:</strong> <i>" + event.getAlarmInfo().getDeviceId() + "</i>"
+                + "<br><strong>通道编号:</strong> <i>" + event.getAlarmInfo().getChannelId() + "</i>"
+                + "<br><strong>报警描述:</strong> <i>" + event.getAlarmInfo().getAlarmDescription() + "</i>"
+                + "<br><strong>报警时间:</strong> <i>" + event.getAlarmInfo().getAlarmTime() + "</i>";
+
+        for (Iterator<Map.Entry<String, PrintWriter>> it = SSE_CACHE.entrySet().iterator(); it.hasNext(); ) {
+            Map.Entry<String, PrintWriter> response = it.next();
+            logger.info("推送到 SSE 连接, 浏览器 ID: {}", response.getKey());
             try {
-                emitter.getValue().send(msg);
-            } catch (IOException | IllegalStateException e) {
-                if (logger.isDebugEnabled()) {
-                    logger.debug("SSE连接已关闭");
+                PrintWriter writer = response.getValue();
+
+                if (writer.checkError()) {
+                    it.remove();
+                    continue;
                 }
-                // 移除已关闭的连接
+
+                String sseMsg = "event:message\n" +
+                        "data:" + msg + "\n" +
+                        "\n";
+                writer.write(sseMsg);
+                writer.flush();
+            } catch (Exception e) {
                 it.remove();
             }
         }

+ 6 - 1
src/main/java/com/genersoft/iot/vmp/gb28181/event/record/RecordEndEventListener.java

@@ -35,7 +35,7 @@ public class RecordEndEventListener implements ApplicationListener<RecordEndEven
         int sumNum = event.getRecordInfo().getSumNum();
         logger.info("录像查询完成事件触发,deviceId:{}, channelId: {}, 录像数量{}/{}条", event.getRecordInfo().getDeviceId(),
                 event.getRecordInfo().getChannelId(), count,sumNum);
-        if (handlerMap.size() > 0) {
+        if (!handlerMap.isEmpty()) {
             RecordEndEventHandler handler = handlerMap.get(deviceId + channelId);
             if (handler !=null){
                 handler.handler(event.getRecordInfo());
@@ -43,6 +43,9 @@ public class RecordEndEventListener implements ApplicationListener<RecordEndEven
                     handlerMap.remove(deviceId + channelId);
                 }
             }
+        }else {
+            logger.info("录像查询完成事件触发, 但是订阅为空,取消发送,deviceId:{}, channelId: {}",
+                    event.getRecordInfo().getDeviceId(), event.getRecordInfo().getChannelId());
         }
     }
 
@@ -53,6 +56,7 @@ public class RecordEndEventListener implements ApplicationListener<RecordEndEven
      * @param recordEndEventHandler
      */
     public void addEndEventHandler(String device, String channelId, RecordEndEventHandler recordEndEventHandler) {
+        logger.info("录像查询事件添加监听,deviceId:{}, channelId: {}", device, channelId);
         handlerMap.put(device + channelId, recordEndEventHandler);
     }
     /**
@@ -61,6 +65,7 @@ public class RecordEndEventListener implements ApplicationListener<RecordEndEven
      * @param channelId
      */
     public void delEndEventHandler(String device, String channelId) {
+        logger.info("录像查询事件移除监听,deviceId:{}, channelId: {}", device, channelId);
         handlerMap.remove(device + channelId);
     }
 

+ 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) {

+ 2 - 1
src/main/java/com/genersoft/iot/vmp/gb28181/session/SSRCFactory.java

@@ -38,7 +38,8 @@ public class SSRCFactory {
 
 
     public void initMediaServerSSRC(String mediaServerId, Set<String> usedSet) {
-        String ssrcPrefix = sipConfig.getDomain().substring(3, 8);
+        String sipDomain = sipConfig.getDomain();
+        String ssrcPrefix = sipDomain.length() >= 8 ? sipDomain.substring(3, 8) : sipDomain;
         String redisKey = SSRC_INFO_KEY + userSetting.getServerId() + "_" + mediaServerId;
         List<String> ssrcList = new ArrayList<>();
         for (int i = 1; i < MAX_STREAM_COUNT; i++) {

+ 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 - 2
src/main/java/com/genersoft/iot/vmp/gb28181/transmit/SIPSender.java

@@ -66,17 +66,17 @@ public class SIPSender {
             // 添加错误订阅
             if (errorEvent != null) {
                 sipSubscribe.addErrorSubscribe(callIdHeader.getCallId(), (eventResult -> {
-                    errorEvent.response(eventResult);
                     sipSubscribe.removeErrorSubscribe(eventResult.callId);
                     sipSubscribe.removeOkSubscribe(eventResult.callId);
+                    errorEvent.response(eventResult);
                 }));
             }
             // 添加订阅
             if (okEvent != null) {
                 sipSubscribe.addOkSubscribe(callIdHeader.getCallId(), eventResult -> {
-                    okEvent.response(eventResult);
                     sipSubscribe.removeOkSubscribe(eventResult.callId);
                     sipSubscribe.removeErrorSubscribe(eventResult.callId);
+                    okEvent.response(eventResult);
                 });
             }
             if ("TCP".equals(transport)) {

+ 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());
 

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

@@ -40,6 +40,7 @@ import javax.sip.SipFactory;
 import javax.sip.header.CallIdHeader;
 import javax.sip.message.Request;
 import java.text.ParseException;
+import java.util.List;
 
 /**
  * @description:设备能力接口,用于定义设备的控制、查询能力
@@ -374,7 +375,8 @@ public class SIPCommander implements ISIPCommander {
         }), e -> {
             ResponseEvent responseEvent = (ResponseEvent) e.event;
             SIPResponse response = (SIPResponse) responseEvent.getResponse();
-            streamSession.put(device.getDeviceId(), channelId, "play", stream, ssrcInfo.getSsrc(), mediaServerItem.getId(), response,
+            String callId = response.getCallIdHeader().getCallId();
+            streamSession.put(device.getDeviceId(), channelId, callId, stream, ssrcInfo.getSsrc(), mediaServerItem.getId(), response,
                     InviteSessionType.PLAY);
             okEvent.response(e);
         });
@@ -676,22 +678,26 @@ 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 (device == null) {
+            logger.warn("[发送BYE] device为null");
+            return;
         }
-        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
@@ -1000,7 +1006,7 @@ public class SIPCommander implements ISIPCommander {
         catalogXml.append("<DeviceID>" + device.getDeviceId() + "</DeviceID>\r\n");
         catalogXml.append("</Query>\r\n");
 
-        
+
 
         Request request = headerProvider.createMessageRequest(device, catalogXml.toString(), null, SipUtils.getNewFromTag(), null,sipSender.getNewCallIdHeader(sipLayer.getLocalIp(device.getLocalIp()),device.getTransport()));
 

+ 13 - 8
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());
@@ -701,6 +702,7 @@ public class SIPCommanderFroPlatform implements ISIPCommanderForPlatform {
         }else {
             channels = deviceChannels.subList(index, deviceChannels.size());
         }
+        logger.info("[发送NOTIFY通知]类型: {},发送数量: {}", type, channels.size());
         Integer finalIndex = index;
         String catalogXmlContent = getCatalogXmlContentForCatalogOther(parentPlatform, channels, type);
         sendNotify(parentPlatform, catalogXmlContent, subscribeInfo, eventResult -> {
@@ -747,13 +749,14 @@ public class SIPCommanderFroPlatform implements ISIPCommanderForPlatform {
         if ( parentPlatform ==null) {
             return ;
         }
+        logger.info("[国标级联] 发送录像数据通道: {}", recordInfo.getChannelId());
         String characterSet = parentPlatform.getCharacterSet();
         StringBuffer recordXml = new StringBuffer(600);
         recordXml.append("<?xml version=\"1.0\" encoding=\"" + characterSet + "\"?>\r\n")
                 .append("<Response>\r\n")
                 .append("<CmdType>RecordInfo</CmdType>\r\n")
                 .append("<SN>" +recordInfo.getSn() + "</SN>\r\n")
-                .append("<DeviceID>" + recordInfo.getChannelId() + "</DeviceID>\r\n")
+                .append("<DeviceID>" + deviceChannel.getChannelId() + "</DeviceID>\r\n")
                 .append("<SumNum>" + recordInfo.getSumNum() + "</SumNum>\r\n");
         if (recordInfo.getRecordList() == null ) {
             recordXml.append("<RecordList Num=\"0\">\r\n");
@@ -763,7 +766,7 @@ public class SIPCommanderFroPlatform implements ISIPCommanderForPlatform {
                 for (RecordItem recordItem : recordInfo.getRecordList()) {
                     recordXml.append("<Item>\r\n");
                     if (deviceChannel != null) {
-                        recordXml.append("<DeviceID>" + recordItem.getDeviceId() + "</DeviceID>\r\n")
+                        recordXml.append("<DeviceID>" + deviceChannel.getChannelId() + "</DeviceID>\r\n")
                                 .append("<Name>" + recordItem.getName() + "</Name>\r\n")
                                 .append("<StartTime>" + DateUtil.yyyy_MM_dd_HH_mm_ssToISO8601(recordItem.getStartTime()) + "</StartTime>\r\n")
                                 .append("<EndTime>" + DateUtil.yyyy_MM_dd_HH_mm_ssToISO8601(recordItem.getEndTime()) + "</EndTime>\r\n")
@@ -783,12 +786,14 @@ public class SIPCommanderFroPlatform implements ISIPCommanderForPlatform {
 
         recordXml.append("</RecordList>\r\n")
                 .append("</Response>\r\n");
-
+        logger.info("[国标级联] 发送录像数据通道:{}, 内容: {}", recordInfo.getChannelId(), recordXml);
         // callid
         CallIdHeader callIdHeader = sipSender.getNewCallIdHeader(parentPlatform.getDeviceIp(),parentPlatform.getTransport());
 
         Request request = headerProviderPlatformProvider.createMessageRequest(parentPlatform, recordXml.toString(), fromTag, SipUtils.getNewViaTag(), callIdHeader);
-        sipSender.transmitRequest(parentPlatform.getDeviceIp(), request);
+        sipSender.transmitRequest(parentPlatform.getDeviceIp(), request, null, eventResult -> {
+            logger.info("[国标级联] 发送录像数据通道:{}, 发送成功", recordInfo.getChannelId());
+        });
 
     }
 

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

@@ -128,6 +128,9 @@ public class ByeRequestProcessor extends SIPRequestProcessorParent implements In
 			redisCatchStorage.deleteSendRTPServer(sendRtpItem.getPlatformId(), sendRtpItem.getChannelId(),
 					callIdHeader.getCallId(), null);
 			zlmServerFactory.stopSendRtpStream(mediaInfo, param);
+			if (userSetting.getUseCustomSsrcForParentInvite()) {
+				mediaServerService.releaseSsrc(mediaInfo.getId(), sendRtpItem.getSsrc());
+			}
 			if (sendRtpItem.getPlayType().equals(InviteStreamType.PUSH)) {
 				ParentPlatform platform = platformService.queryPlatformByServerGBId(sendRtpItem.getPlatformId());
 				if (platform != null) {
@@ -167,14 +170,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 +217,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());

+ 21 - 14
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);
@@ -162,6 +162,8 @@ public class InviteRequestProcessor extends SIPRequestProcessorParent implements
                 return;
             }
 
+            logger.info("[INVITE] requesterId: {}, callId: {}, 来自:{}:{}",
+                    requesterId, callIdHeader.getCallId(), request.getRemoteAddress(), request.getRemotePort());
 
             // 查询请求是否来自上级平台\设备
             ParentPlatform platform = storager.queryParentPlatByServerGBId(requesterId);
@@ -409,7 +411,16 @@ public class InviteRequestProcessor extends SIPRequestProcessorParent implements
                             // 非严格模式端口不统一, 增加兼容性,修改为一个不为0的端口
                             localPort = new Random().nextInt(65535) + 1;
                         }
-                        content.append("m=video " + localPort + " RTP/AVP 96\r\n");
+                        if (sendRtpItem.isTcp()) {
+                            content.append("m=video " + localPort + " TCP/RTP/AVP 96\r\n");
+                            if (!sendRtpItem.isTcpActive()) {
+                                content.append("a=setup:active\r\n");
+                            } else {
+                                content.append("a=setup:passive\r\n");
+                            }
+                        }else {
+                            content.append("m=video " + localPort + " RTP/AVP 96\r\n");
+                        }
                         content.append("a=sendonly\r\n");
                         content.append("a=rtpmap:96 PS/90000\r\n");
                         content.append("y=" + sendRtpItem.getSsrc() + "\r\n");
@@ -524,7 +535,10 @@ public class InviteRequestProcessor extends SIPRequestProcessorParent implements
                                     }
                                 });
                     } else {
-
+                        sendRtpItem.setPlayType(InviteStreamType.PLAY);
+                        String streamId = String.format("%s_%s", device.getDeviceId(), channelId);
+                        sendRtpItem.setStreamId(streamId);
+                        redisCatchStorage.updateSendRTPSever(sendRtpItem);
                         SSRCInfo ssrcInfo = playService.play(mediaServerItem, device.getDeviceId(), channelId, ssrc, ((code, msg, data) -> {
                             if (code == InviteErrorCode.SUCCESS.getCode()) {
                                 hookEvent.run(code, msg, data);
@@ -536,9 +550,6 @@ public class InviteRequestProcessor extends SIPRequestProcessorParent implements
                                 errorEvent.run(code, msg, data);
                             }
                         }));
-                        sendRtpItem.setPlayType(InviteStreamType.PLAY);
-                        String streamId = String.format("%s_%s", device.getDeviceId(), channelId);
-                        sendRtpItem.setStream(streamId);
                         sendRtpItem.setSsrc(ssrcInfo.getSsrc());
                         redisCatchStorage.updateSendRTPSever(sendRtpItem);
 
@@ -721,8 +732,6 @@ public class InviteRequestProcessor extends SIPRequestProcessorParent implements
                 zlmHttpHookSubscribe.removeSubscribe(hookSubscribe);
                 dynamicTask.stop(callIdHeader.getCallId());
             }
-
-
         } else if ("push".equals(gbStream.getStreamType())) {
             if (!platform.isStartOfflinePush()) {
                 // 平台设置中关闭了拉起离线的推流则直接回复
@@ -745,13 +754,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 +768,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());
@@ -826,7 +833,7 @@ public class InviteRequestProcessor extends SIPRequestProcessorParent implements
         // 发送redis消息
         redisGbPlayMsgListener.sendMsg(streamPushItem.getServerId(), streamPushItem.getMediaServerId(),
                 streamPushItem.getApp(), streamPushItem.getStream(), addressStr, port, ssrc, requesterId,
-                channelId, mediaTransmissionTCP, platform.isRtcp(), null, responseSendItemMsg -> {
+                channelId, mediaTransmissionTCP, platform.isRtcp(),platform.getName(), responseSendItemMsg -> {
                     SendRtpItem sendRtpItem = responseSendItemMsg.getSendRtpItem();
                     if (sendRtpItem == null || responseSendItemMsg.getMediaServerItem() == null) {
                         logger.warn("服务器端口资源不足");

+ 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();
 		}

+ 20 - 16
src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/RegisterRequestProcessor.java

@@ -106,23 +106,27 @@ public class RegisterRequestProcessor extends SIPRequestProcessorParent implemen
             String title = registerFlag ? "[注册请求]": "[注销请求]";
                     logger.info(title + "设备:{}, 开始处理: {}", deviceId, requestAddress);
             if (device != null &&
-                device.getSipTransactionInfo() != null &&
-                request.getCallIdHeader().getCallId().equals(device.getSipTransactionInfo().getCallId())) {
+                    device.getSipTransactionInfo() != null &&
+                    request.getCallIdHeader().getCallId().equals(device.getSipTransactionInfo().getCallId())) {
                 logger.info(title + "设备:{}, 注册续订: {}",device.getDeviceId(), device.getDeviceId());
-                device.setExpires(request.getExpires().getExpires());
-                device.setIp(remoteAddressInfo.getIp());
-                device.setPort(remoteAddressInfo.getPort());
-                device.setHostAddress(remoteAddressInfo.getIp().concat(":").concat(String.valueOf(remoteAddressInfo.getPort())));
-                device.setLocalIp(request.getLocalAddress().getHostAddress());
-                Response registerOkResponse = getRegisterOkResponse(request);
-                // 判断TCP还是UDP
-                ViaHeader reqViaHeader = (ViaHeader) request.getHeader(ViaHeader.NAME);
-                String transport = reqViaHeader.getTransport();
-                device.setTransport("TCP".equalsIgnoreCase(transport) ? "TCP" : "UDP");
-                sipSender.transmitRequest(request.getLocalAddress().getHostAddress(), registerOkResponse);
-                device.setRegisterTime(DateUtil.getNow());
-                SipTransactionInfo sipTransactionInfo = new SipTransactionInfo((SIPResponse)registerOkResponse);
-                deviceService.online(device, sipTransactionInfo);
+                if (registerFlag) {
+                    device.setExpires(request.getExpires().getExpires());
+                    device.setIp(remoteAddressInfo.getIp());
+                    device.setPort(remoteAddressInfo.getPort());
+                    device.setHostAddress(remoteAddressInfo.getIp().concat(":").concat(String.valueOf(remoteAddressInfo.getPort())));
+                    device.setLocalIp(request.getLocalAddress().getHostAddress());
+                    Response registerOkResponse = getRegisterOkResponse(request);
+                    // 判断TCP还是UDP
+                    ViaHeader reqViaHeader = (ViaHeader) request.getHeader(ViaHeader.NAME);
+                    String transport = reqViaHeader.getTransport();
+                    device.setTransport("TCP".equalsIgnoreCase(transport) ? "TCP" : "UDP");
+                    sipSender.transmitRequest(request.getLocalAddress().getHostAddress(), registerOkResponse);
+                    device.setRegisterTime(DateUtil.getNow());
+                    SipTransactionInfo sipTransactionInfo = new SipTransactionInfo((SIPResponse)registerOkResponse);
+                    deviceService.online(device, sipTransactionInfo);
+                }else {
+                    deviceService.offline(deviceId, "主动注销");
+                }
                 return;
             }
             String password = (device != null && !ObjectUtils.isEmpty(device.getPassword()))? device.getPassword() : sipConfig.getPassword();

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

@@ -61,7 +61,7 @@ public class KeepaliveNotifyMessageHandler extends SIPRequestProcessorParent imp
             return;
         }
         SIPRequest request = (SIPRequest) evt.getRequest();
-        logger.info("[收到心跳] device: {}, callId: {}", device.getDeviceId(), request.getCallIdHeader().getCallId());
+        logger.info("[收到心跳] device: {}, callId: {}", device.getDeviceId(), request.getCallIdHeader().getCallId());
 
         // 回复200 OK
         try {
@@ -76,10 +76,15 @@ 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());
+            // 设备地址变化会引起目录订阅任务失效,需要重新添加
+            if (device.getSubscribeCycleForCatalog() > 0) {
+                deviceService.removeCatalogSubscribe(device);
+                deviceService.addCatalogSubscribe(device);
+            }
         }
         if (device.getKeepaliveTime() == null) {
             device.setKeepaliveIntervalTime(60);

+ 1 - 0
src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/query/cmd/RecordInfoQueryMessageHandler.java

@@ -102,6 +102,7 @@ public class RecordInfoQueryMessageHandler extends SIPRequestProcessorParent imp
             // 接收录像数据
             recordEndEventListener.addEndEventHandler(deviceChannel.getDeviceId(), channelId, (recordInfo)->{
                 try {
+                    logger.info("[国标级联] 录像查询收到数据, 通道: {},准备转发===", channelId);
                     cmderFroPlatform.recordInfo(deviceChannel, parentPlatform, request.getFromTag(), recordInfo);
                 } catch (SipException | InvalidArgumentException | ParseException e) {
                     logger.error("[命令发送失败] 国标级联 回复录像数据: {}", e.getMessage());

+ 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);

+ 162 - 42
src/main/java/com/genersoft/iot/vmp/media/zlm/AssistRESTfulUtils.java

@@ -2,40 +2,69 @@ package com.genersoft.iot.vmp.media.zlm;
 
 import com.alibaba.fastjson2.JSON;
 import com.alibaba.fastjson2.JSONObject;
+import com.genersoft.iot.vmp.conf.exception.ControllerException;
 import com.genersoft.iot.vmp.media.zlm.dto.MediaServerItem;
+import com.genersoft.iot.vmp.vmanager.bean.ErrorCode;
 import okhttp3.*;
 import okhttp3.logging.HttpLoggingInterceptor;
 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.MalformedURLException;
+import java.net.SocketTimeoutException;
+import java.net.URL;
 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;
+
     }
 
 
@@ -49,11 +78,11 @@ public class AssistRESTfulUtils {
             logger.warn("未启用Assist服务");
             return null;
         }
-        StringBuffer stringBuffer = new StringBuffer();
-        stringBuffer.append(String.format("http://%s:%s/%s",  mediaServerItem.getIp(), mediaServerItem.getRecordAssistPort(), api));
+        StringBuilder stringBuffer = new StringBuilder();
+        stringBuffer.append(api);
         JSONObject responseJSON = null;
 
-        if (param != null && param.keySet().size() > 0) {
+        if (param != null && !param.keySet().isEmpty()) {
             stringBuffer.append("?");
             int index = 1;
             for (String key : param.keySet()){
@@ -68,6 +97,7 @@ public class AssistRESTfulUtils {
         }
 
         String url = stringBuffer.toString();
+        logger.info("[访问assist]: {}", url);
         Request request = new Request.Builder()
                 .get()
                 .url(url)
@@ -123,13 +153,93 @@ public class AssistRESTfulUtils {
         return responseJSON;
     }
 
+    public JSONObject sendPost(MediaServerItem mediaServerItem, String url,
+                               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;
+        }
+        logger.info("[访问assist]: {}, 参数: {}", url, param);
+        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 +247,43 @@ 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);
+        }
+        String urlStr = String.format("%s/api/record/file/download/task/add",  remoteHost);;
+        return sendPost(mediaServerItem, urlStr, 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, String scheme) {
         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);
+        }
+        String urlStr = String.format("%s://%s:%s/api/record/file/download/task/list",
+                scheme, mediaServerItem.getIp(), mediaServerItem.getRecordAssistPort());;
+        return sendGet(mediaServerItem, urlStr, param, null);
     }
-
 }

+ 112 - 82
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());
@@ -269,51 +266,57 @@ public class ZLMHttpHookListener {
         } else {
             result.setEnable_mp4(userSetting.isRecordPushLive());
         }
-        // 替换流地址
-        if ("rtp".equals(param.getApp()) && !mediaInfo.isRtpEnable()) {
-            String ssrc = String.format("%010d", Long.parseLong(param.getStream(), 16));;
-            InviteInfo inviteInfo = inviteStreamService.getInviteInfoBySSRC(ssrc);
-            if (inviteInfo != null) {
-                result.setStream_replace(inviteInfo.getStream());
-                logger.info("[ZLM HOOK]推流鉴权 stream: {} 替换为 {}", param.getStream(), inviteInfo.getStream());
-            }
-        }
-        List<SsrcTransaction> ssrcTransactionForAll = sessionManager.getSsrcTransactionForAll(null, null, null, param.getStream());
-        if (ssrcTransactionForAll != null && ssrcTransactionForAll.size() == 1) {
-            String deviceId = ssrcTransactionForAll.get(0).getDeviceId();
-            String channelId = ssrcTransactionForAll.get(0).getChannelId();
-            DeviceChannel deviceChannel = storager.queryChannel(deviceId, channelId);
-            if (deviceChannel != null) {
+        // 国标流
+        if ("rtp".equals(param.getApp()) ) {
 
-                result.setEnable_audio(deviceChannel.isHasAudio());
-            }
-            // 如果是录像下载就设置视频间隔十秒
-            if (ssrcTransactionForAll.get(0).getType() == InviteSessionType.DOWNLOAD) {
-                result.setMp4_max_second(10);
-                result.setEnable_mp4(true);
-            }
-            // 如果是talk对讲,则默认获取声音
-            if (ssrcTransactionForAll.get(0).getType() == InviteSessionType.TALK) {
-                result.setEnable_audio(true);
+            InviteInfo inviteInfo = inviteStreamService.getInviteInfoByStream(null, param.getStream());
+
+            // 单端口模式下修改流 ID
+            if (!mediaInfo.isRtpEnable() && inviteInfo == null) {
+                String ssrc = String.format("%010d", Long.parseLong(param.getStream(), 16));
+                inviteInfo = inviteStreamService.getInviteInfoBySSRC(ssrc);
+                if (inviteInfo != null) {
+                    result.setStream_replace(inviteInfo.getStream());
+                    logger.info("[ZLM HOOK]推流鉴权 stream: {} 替换为 {}", param.getStream(), inviteInfo.getStream());
+                }
             }
 
-        }
-        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);
-                        });
+            // 设置音频信息及录制信息
+            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);
+                if (deviceChannel != null) {
+                    result.setEnable_audio(deviceChannel.isHasAudio());
+                }
+                // 如果是录像下载就设置视频间隔十秒
+                if (ssrcTransactionForAll.get(0).getType() == InviteSessionType.DOWNLOAD) {
+                    // 获取录像的总时长,然后设置为这个视频的时长
+                    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);
+                }
             }
         }
         if (param.getApp().equalsIgnoreCase("rtp")) {
@@ -361,13 +364,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);
@@ -377,8 +378,6 @@ public class ZLMHttpHookListener {
                     }
                     redisCatchStorage.updateStreamAuthorityInfo(param.getApp(), param.getStream(), streamAuthorityInfo);
                 }
-            } else {
-                redisCatchStorage.removeStreamAuthorityInfo(param.getApp(), param.getStream());
             }
 
             if ("rtsp".equals(param.getSchema())) {
@@ -460,35 +459,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());
@@ -513,7 +517,7 @@ public class ZLMHttpHookListener {
                 }
                 if (!param.isRegist()) {
                     List<SendRtpItem> sendRtpItems = redisCatchStorage.querySendRTPServerByStream(param.getStream());
-                    if (sendRtpItems.size() > 0) {
+                    if (!sendRtpItems.isEmpty()) {
                         for (SendRtpItem sendRtpItem : sendRtpItems) {
                             if (sendRtpItem != null && sendRtpItem.getApp().equals(param.getApp())) {
                                 String platformId = sendRtpItem.getPlatformId();
@@ -608,11 +612,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(),
@@ -684,7 +692,7 @@ public class ZLMHttpHookListener {
             String deviceId = s[0];
             String channelId = s[1];
             Device device = redisCatchStorage.getDevice(deviceId);
-            if (device == null) {
+            if (device == null || !device.isOnLine()) {
                 defaultResult.setResult(new HookResult(ErrorCode.ERROR404.getCode(), ErrorCode.ERROR404.getMsg()));
                 return defaultResult;
             }
@@ -848,11 +856,33 @@ 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);
+                }
+            }
+        });
+
+        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();

+ 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 +
+                '}';
+    }
+}

+ 93 - 28
src/main/java/com/genersoft/iot/vmp/media/zlm/dto/hook/OnStreamChangedHookParam.java

@@ -120,17 +120,17 @@ public class OnStreamChangedHookParam extends HookParam{
         /**
          *  H264 = 0, H265 = 1, AAC = 2, G711A = 3, G711U = 4
          */
-        private int codecId;
+        private int codec_id;
 
         /**
          * 编码类型名称 CodecAAC CodecH264
          */
-        private String codecIdName;
+        private String codec_id_name;
 
         /**
          * Video = 0, Audio = 1
          */
-        private int codecType;
+        private int codec_type;
 
         /**
          * 轨道是否准备就绪
@@ -140,17 +140,17 @@ public class OnStreamChangedHookParam extends HookParam{
         /**
          * 音频采样位数
          */
-        private int sampleBit;
+        private int sample_bit;
 
         /**
          * 音频采样率
          */
-        private int sampleRate;
+        private int sample_rate;
 
         /**
          * 视频fps
          */
-        private int fps;
+        private float fps;
 
         /**
          * 视频高
@@ -162,6 +162,31 @@ public class OnStreamChangedHookParam extends HookParam{
          */
         private int width;
 
+        /**
+         * 帧数
+         */
+        private int frames;
+
+        /**
+         * 关键帧数
+         */
+        private int key_frames;
+
+        /**
+         * GOP大小
+         */
+        private int gop_size;
+
+        /**
+         * GOP间隔时长(ms)
+         */
+        private int gop_interval_ms;
+
+        /**
+         * 丢帧率
+         */
+        private float loss;
+
         public int getChannels() {
             return channels;
         }
@@ -170,28 +195,28 @@ public class OnStreamChangedHookParam extends HookParam{
             this.channels = channels;
         }
 
-        public int getCodecId() {
-            return codecId;
+        public int getCodec_id() {
+            return codec_id;
         }
 
-        public void setCodecId(int codecId) {
-            this.codecId = codecId;
+        public void setCodec_id(int codec_id) {
+            this.codec_id = codec_id;
         }
 
-        public String getCodecIdName() {
-            return codecIdName;
+        public String getCodec_id_name() {
+            return codec_id_name;
         }
 
-        public void setCodecIdName(String codecIdName) {
-            this.codecIdName = codecIdName;
+        public void setCodec_id_name(String codec_id_name) {
+            this.codec_id_name = codec_id_name;
         }
 
-        public int getCodecType() {
-            return codecType;
+        public int getCodec_type() {
+            return codec_type;
         }
 
-        public void setCodecType(int codecType) {
-            this.codecType = codecType;
+        public void setCodec_type(int codec_type) {
+            this.codec_type = codec_type;
         }
 
         public boolean isReady() {
@@ -202,27 +227,27 @@ public class OnStreamChangedHookParam extends HookParam{
             this.ready = ready;
         }
 
-        public int getSampleBit() {
-            return sampleBit;
+        public int getSample_bit() {
+            return sample_bit;
         }
 
-        public void setSampleBit(int sampleBit) {
-            this.sampleBit = sampleBit;
+        public void setSample_bit(int sample_bit) {
+            this.sample_bit = sample_bit;
         }
 
-        public int getSampleRate() {
-            return sampleRate;
+        public int getSample_rate() {
+            return sample_rate;
         }
 
-        public void setSampleRate(int sampleRate) {
-            this.sampleRate = sampleRate;
+        public void setSample_rate(int sample_rate) {
+            this.sample_rate = sample_rate;
         }
 
-        public int getFps() {
+        public float getFps() {
             return fps;
         }
 
-        public void setFps(int fps) {
+        public void setFps(float fps) {
             this.fps = fps;
         }
 
@@ -241,6 +266,46 @@ public class OnStreamChangedHookParam extends HookParam{
         public void setWidth(int width) {
             this.width = width;
         }
+
+        public int getFrames() {
+            return frames;
+        }
+
+        public void setFrames(int frames) {
+            this.frames = frames;
+        }
+
+        public int getKey_frames() {
+            return key_frames;
+        }
+
+        public void setKey_frames(int key_frames) {
+            this.key_frames = key_frames;
+        }
+
+        public int getGop_size() {
+            return gop_size;
+        }
+
+        public void setGop_size(int gop_size) {
+            this.gop_size = gop_size;
+        }
+
+        public int getGop_interval_ms() {
+            return gop_interval_ms;
+        }
+
+        public void setGop_interval_ms(int gop_interval_ms) {
+            this.gop_interval_ms = gop_interval_ms;
+        }
+
+        public float getLoss() {
+            return loss;
+        }
+
+        public void setLoss(float loss) {
+            this.loss = loss;
+        }
     }
 
     public static class OriginSock{

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

@@ -0,0 +1,59 @@
+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, MediaServerItem mediaServerItem, String startTime,
+                   String endTime, String callId, String remoteHost, boolean filterMediaServer);
+
+
+    /**
+     * 查询合并任务列表
+     */
+    JSONArray queryTask(String app, String stream, String callId, String taskId, String mediaServerId, Boolean isEnd, String scheme);
+
+    /**
+     * 收藏视频,收藏的视频过期不会删除
+     */
+    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;
     }
 }

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

@@ -0,0 +1,235 @@
+package com.genersoft.iot.vmp.service.impl;
+
+import com.alibaba.fastjson2.JSONArray;
+import com.alibaba.fastjson2.JSONObject;
+import com.baomidou.dynamic.datasource.annotation.DS;
+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
+@DS("share")
+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, MediaServerItem mediaServerItem, String startTime, String endTime,
+                          String callId, String remoteHost, boolean filterMediaServer) {
+        // 参数校验
+        assert app != null;
+        assert stream != null;
+        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, filterMediaServer ? mediaServers : null);
+        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, String scheme) {
+        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, scheme);
+        if (result == null || result.getInteger("code") != 0) {
+            throw new ControllerException(ErrorCode.ERROR100.getCode(), result == null ? "查询任务列表失败" : 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);
+    }
+}

+ 2 - 0
src/main/java/com/genersoft/iot/vmp/service/impl/DeviceAlarmServiceImpl.java

@@ -1,5 +1,6 @@
 package com.genersoft.iot.vmp.service.impl;
 
+import com.baomidou.dynamic.datasource.annotation.DS;
 import com.genersoft.iot.vmp.gb28181.bean.DeviceAlarm;
 import com.genersoft.iot.vmp.media.zlm.dto.StreamProxyItem;
 import com.genersoft.iot.vmp.service.IDeviceAlarmService;
@@ -12,6 +13,7 @@ import org.springframework.stereotype.Service;
 import java.util.List;
 
 @Service
+@DS("master")
 public class DeviceAlarmServiceImpl implements IDeviceAlarmService {
 
     @Autowired

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

@@ -1,5 +1,6 @@
 package com.genersoft.iot.vmp.service.impl;
 
+import com.baomidou.dynamic.datasource.annotation.DS;
 import com.genersoft.iot.vmp.common.InviteInfo;
 import com.genersoft.iot.vmp.common.InviteSessionType;
 import com.genersoft.iot.vmp.gb28181.bean.Device;
@@ -27,6 +28,7 @@ import java.util.concurrent.CopyOnWriteArrayList;
  * @author lin
  */
 @Service
+@DS("master")
 public class DeviceChannelServiceImpl implements IDeviceChannelService {
 
     private final static Logger logger = LoggerFactory.getLogger(DeviceChannelServiceImpl.class);
@@ -243,6 +245,10 @@ public class DeviceChannelServiceImpl implements IDeviceChannelService {
 
     @Override
     public void batchUpdateChannel(List<DeviceChannel> channels) {
+        String now = DateUtil.getNow();
+        for (DeviceChannel channel : channels) {
+            channel.setUpdateTime(now);
+        }
         channelMapper.batchUpdate(channels);
         for (DeviceChannel channel : channels) {
             if (channel.getParentId() != null) {

+ 24 - 14
src/main/java/com/genersoft/iot/vmp/service/impl/DeviceServiceImpl.java

@@ -1,5 +1,6 @@
 package com.genersoft.iot.vmp.service.impl;
 
+import com.baomidou.dynamic.datasource.annotation.DS;
 import com.genersoft.iot.vmp.common.VideoManagerConstants;
 import com.genersoft.iot.vmp.conf.DynamicTask;
 import com.genersoft.iot.vmp.conf.UserSetting;
@@ -46,6 +47,7 @@ import java.util.concurrent.TimeUnit;
  * 设备业务(目录订阅)
  */
 @Service
+@DS("master")
 public class DeviceServiceImpl implements IDeviceService {
 
     private final static Logger logger = LoggerFactory.getLogger(DeviceServiceImpl.class);
@@ -162,6 +164,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 +189,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 +216,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 +234,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());
             }
         }
         // 移除订阅
@@ -299,7 +309,7 @@ public class DeviceServiceImpl implements IDeviceService {
         // 设置最小值为30
         int subscribeCycleForCatalog = Math.max(device.getSubscribeCycleForMobilePosition(),30);
         // 刷新订阅
-        dynamicTask.startCron(device.getDeviceId() + "mobile_position" , mobilePositionSubscribeTask, (subscribeCycleForCatalog) * 1000);
+        dynamicTask.startCron(device.getDeviceId() + "mobile_position" , mobilePositionSubscribeTask, subscribeCycleForCatalog * 1000);
         return true;
     }
 

+ 11 - 12
src/main/java/com/genersoft/iot/vmp/service/impl/GbStreamServiceImpl.java

@@ -1,5 +1,6 @@
 package com.genersoft.iot.vmp.service.impl;
 
+import com.baomidou.dynamic.datasource.annotation.DS;
 import com.genersoft.iot.vmp.gb28181.bean.*;
 import com.genersoft.iot.vmp.gb28181.event.EventPublisher;
 import com.genersoft.iot.vmp.gb28181.event.subscribe.catalog.CatalogEvent;
@@ -25,6 +26,7 @@ import java.util.ArrayList;
 import java.util.List;
 
 @Service
+@DS("master")
 public class GbStreamServiceImpl implements IGbStreamService {
 
     private final static Logger logger = LoggerFactory.getLogger(GbStreamServiceImpl.class);
@@ -77,8 +79,6 @@ public class GbStreamServiceImpl implements IGbStreamService {
         }
         try {
             List<DeviceChannel> deviceChannelList = new ArrayList<>();
-
-
             for (int i = 0; i < gbStreams.size(); i++) {
                 GbStream gbStream = gbStreams.get(i);
                 gbStream.setCatalogId(catalogId);
@@ -251,18 +251,17 @@ public class GbStreamServiceImpl implements IGbStreamService {
             return ;
         }
         if (ObjectUtils.isEmpty(catalogId)) {
-            catalogId = platform.getDeviceGBId();
+            catalogId = null;
         }
-        if (platformGbStreamMapper.delByPlatformAndCatalogId(platformId, catalogId) > 0) {
-            List<GbStream> gbStreams = platformGbStreamMapper.queryChannelInParentPlatformAndCatalog(platformId, catalogId);
-            List<DeviceChannel> deviceChannelList = new ArrayList<>();
-            for (GbStream gbStream : gbStreams) {
-                DeviceChannel deviceChannel = new DeviceChannel();
-                deviceChannel.setChannelId(gbStream.getGbId());
-                deviceChannelList.add(deviceChannel);
-            }
-            eventPublisher.catalogEventPublish(platformId, deviceChannelList, CatalogEvent.DEL);
+        List<GbStream> gbStreams = platformGbStreamMapper.queryChannelInParentPlatformAndCatalog(platformId, catalogId);
+        List<DeviceChannel> deviceChannelList = new ArrayList<>();
+        for (GbStream gbStream : gbStreams) {
+            DeviceChannel deviceChannel = new DeviceChannel();
+            deviceChannel.setChannelId(gbStream.getGbId());
+            deviceChannelList.add(deviceChannel);
         }
+        eventPublisher.catalogEventPublish(platformId, deviceChannelList, CatalogEvent.DEL);
+        platformGbStreamMapper.delByPlatformAndCatalogId(platformId, catalogId);
     }
 
     @Override

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

@@ -1,6 +1,7 @@
 package com.genersoft.iot.vmp.service.impl;
 
 import com.alibaba.fastjson2.JSON;
+import com.baomidou.dynamic.datasource.annotation.DS;
 import com.genersoft.iot.vmp.common.InviteInfo;
 import com.genersoft.iot.vmp.common.InviteSessionStatus;
 import com.genersoft.iot.vmp.common.InviteSessionType;
@@ -20,6 +21,7 @@ import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.CopyOnWriteArrayList;
 
 @Service
+@DS("master")
 public class InviteStreamServiceImpl implements IInviteStreamService {
 
     private final Logger logger = LoggerFactory.getLogger(InviteStreamServiceImpl.class);
@@ -116,9 +118,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));
     }

+ 2 - 0
src/main/java/com/genersoft/iot/vmp/service/impl/LogServiceImpl.java

@@ -1,5 +1,6 @@
 package com.genersoft.iot.vmp.service.impl;
 
+import com.baomidou.dynamic.datasource.annotation.DS;
 import com.genersoft.iot.vmp.gb28181.bean.DeviceAlarm;
 import com.genersoft.iot.vmp.service.ILogService;
 import com.genersoft.iot.vmp.storager.dao.LogMapper;
@@ -12,6 +13,7 @@ import org.springframework.stereotype.Service;
 import java.util.List;
 
 @Service
+@DS("master")
 public class LogServiceImpl implements ILogService {
 
     @Autowired

+ 28 - 130
src/main/java/com/genersoft/iot/vmp/service/impl/MediaServerServiceImpl.java

@@ -3,6 +3,7 @@ package com.genersoft.iot.vmp.service.impl;
 import com.alibaba.fastjson2.JSON;
 import com.alibaba.fastjson2.JSONArray;
 import com.alibaba.fastjson2.JSONObject;
+import com.baomidou.dynamic.datasource.annotation.DS;
 import com.genersoft.iot.vmp.common.CommonCallback;
 import com.genersoft.iot.vmp.common.VideoManagerConstants;
 import com.genersoft.iot.vmp.conf.DynamicTask;
@@ -53,6 +54,7 @@ import java.util.concurrent.ExecutionException;
  * 媒体服务器节点管理
  */
 @Service
+@DS("master")
 public class MediaServerServiceImpl implements IMediaServerService {
 
     private final static Logger logger = LoggerFactory.getLogger(MediaServerServiceImpl.class);
@@ -165,14 +167,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 +206,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
@@ -313,7 +317,6 @@ public class MediaServerServiceImpl implements IMediaServerService {
 
     @Override
     public MediaServerItem getDefaultMediaServer() {
-
         return mediaServerMapper.queryDefault();
     }
 
@@ -428,17 +431,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,34 +565,30 @@ 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
         if (mediaServerItem.getRtspPort() != 0) {
-            param.put("ffmpeg.snap", "%s -rtsp_transport tcp -i %s -y -f mjpeg -t 0.001 %s");
+            param.put("ffmpeg.snap", "%s -rtsp_transport tcp -i %s -y -f mjpeg -frames:v 1 %s");
         }
         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 +597,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 +709,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 +737,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 +750,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);

+ 6 - 5
src/main/java/com/genersoft/iot/vmp/service/impl/PlatformChannelServiceImpl.java

@@ -1,5 +1,6 @@
 package com.genersoft.iot.vmp.service.impl;
 
+import com.baomidou.dynamic.datasource.annotation.DS;
 import com.genersoft.iot.vmp.gb28181.bean.*;
 import com.genersoft.iot.vmp.gb28181.event.EventPublisher;
 import com.genersoft.iot.vmp.gb28181.event.subscribe.catalog.CatalogEvent;
@@ -27,6 +28,7 @@ import java.util.Map;
  * @author lin
  */
 @Service
+@DS("master")
 public class PlatformChannelServiceImpl implements IPlatformChannelService {
 
     private final static Logger logger = LoggerFactory.getLogger(PlatformChannelServiceImpl.class);
@@ -165,10 +167,9 @@ public class PlatformChannelServiceImpl implements IPlatformChannelService {
            catalogId = null;
         }
 
-        if ((result = platformChannelMapper.delChannelForGBByCatalogId(platformId, catalogId)) > 0) {
-            List<DeviceChannel> deviceChannels = platformChannelMapper.queryAllChannelInCatalog(platformId, catalogId);
-            eventPublisher.catalogEventPublish(platformId, deviceChannels, CatalogEvent.DEL);
-        }
-        return result;
+        List<DeviceChannel> deviceChannels = platformChannelMapper.queryAllChannelInCatalog(platformId, catalogId);
+        eventPublisher.catalogEventPublish(platformId, deviceChannels, CatalogEvent.DEL);
+
+        return platformChannelMapper.delChannelForGBByCatalogId(platformId, catalogId);
     }
 }

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

@@ -2,6 +2,7 @@ package com.genersoft.iot.vmp.service.impl;
 
 import com.genersoft.iot.vmp.common.InviteInfo;
 import com.genersoft.iot.vmp.common.InviteSessionType;
+import com.baomidou.dynamic.datasource.annotation.DS;
 import com.genersoft.iot.vmp.conf.DynamicTask;
 import com.genersoft.iot.vmp.conf.UserSetting;
 import com.genersoft.iot.vmp.conf.exception.SsrcTransactionNotFoundException;
@@ -55,6 +56,7 @@ import java.util.*;
  * @author lin
  */
 @Service
+@DS("master")
 public class PlatformServiceImpl implements IPlatformService {
 
     private final static String REGISTER_KEY_PREFIX = "platform_register_";
@@ -169,7 +171,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 +288,7 @@ public class PlatformServiceImpl implements IPlatformService {
         }
         if (parentPlatform.isAutoPushChannel()) {
             if (subscribeHolder.getCatalogSubscribe(parentPlatform.getServerGBId()) == null) {
+                logger.info("[国标级联]:{}, 添加自动通道推送模拟订阅信息", parentPlatform.getServerGBId());
                 addSimulatedSubscribeInfo(parentPlatform);
             }
         }else {
@@ -363,9 +366,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秒自动尝试重新注册

+ 137 - 60
src/main/java/com/genersoft/iot/vmp/service/impl/PlayServiceImpl.java

@@ -1,6 +1,8 @@
 package com.genersoft.iot.vmp.service.impl;
 
+import com.alibaba.fastjson2.JSONArray;
 import com.alibaba.fastjson2.JSONObject;
+import com.baomidou.dynamic.datasource.annotation.DS;
 import com.genersoft.iot.vmp.common.InviteInfo;
 import com.genersoft.iot.vmp.common.InviteSessionStatus;
 import com.genersoft.iot.vmp.common.InviteSessionType;
@@ -23,7 +25,12 @@ 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.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.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.ErrorCallback;
@@ -31,8 +38,11 @@ import com.genersoft.iot.vmp.service.bean.InviteErrorCode;
 import com.genersoft.iot.vmp.service.bean.RequestPushStreamMsg;
 import com.genersoft.iot.vmp.service.bean.SSRCInfo;
 import com.genersoft.iot.vmp.service.redisMsg.RedisGbPlayMsgListener;
+import com.genersoft.iot.vmp.service.bean.*;
 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;
@@ -61,6 +71,7 @@ import java.util.*;
 
 @SuppressWarnings(value = {"rawtypes", "unchecked"})
 @Service
+@DS("master")
 public class PlayServiceImpl implements IPlayService {
 
     private final static Logger logger = LoggerFactory.getLogger(PlayServiceImpl.class);
@@ -92,12 +103,18 @@ public class PlayServiceImpl implements IPlayService {
     @Autowired
     private SendRtpPortManager sendRtpPortManager;
 
+    @Autowired
+    private ZlmHttpHookSubscribe subscribe;
+
     @Autowired
     private ZLMRESTfulUtils zlmresTfulUtils;
 
     @Autowired
     private AssistRESTfulUtils assistRESTfulUtils;
 
+    @Autowired
+    private ZLMServerFactory zlmServerFactory;
+
     @Autowired
     private IMediaService mediaService;
 
@@ -117,7 +134,7 @@ public class PlayServiceImpl implements IPlayService {
     private DynamicTask dynamicTask;
 
     @Autowired
-    private ZlmHttpHookSubscribe subscribe;
+    private CloudRecordServiceMapper cloudRecordServiceMapper;
 
     @Autowired
     private ISIPCommanderForPlatform commanderForPlatform;
@@ -407,6 +424,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 +463,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 +505,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;
@@ -568,7 +601,7 @@ public class PlayServiceImpl implements IPlayService {
                 deviceChannel.setStreamId(streamInfo.getStream());
                 storager.startPlay(deviceId, channelId, streamInfo.getStream());
             }
-            InviteInfo inviteInfo = inviteStreamService.getInviteInfoByDeviceAndChannel(InviteSessionType.PLAYBACK, deviceId, channelId);
+            InviteInfo inviteInfo = inviteStreamService.getInviteInfoByStream(InviteSessionType.PLAYBACK, ((OnStreamChangedHookParam) param).getStream());
             if (inviteInfo != null) {
                 inviteInfo.setStatus(InviteSessionStatus.ok);
 
@@ -597,23 +630,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 +727,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 +747,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 +828,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 +915,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 +952,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 +1286,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 +1314,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接收失败");
         }

+ 2 - 0
src/main/java/com/genersoft/iot/vmp/service/impl/RoleServerImpl.java

@@ -1,5 +1,6 @@
 package com.genersoft.iot.vmp.service.impl;
 
+import com.baomidou.dynamic.datasource.annotation.DS;
 import com.genersoft.iot.vmp.service.IRoleService;
 import com.genersoft.iot.vmp.storager.dao.RoleMapper;
 import com.genersoft.iot.vmp.storager.dao.dto.Role;
@@ -9,6 +10,7 @@ import org.springframework.stereotype.Service;
 import java.util.List;
 
 @Service
+@DS("master")
 public class RoleServerImpl implements IRoleService {
 
     @Autowired

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

@@ -2,6 +2,7 @@ package com.genersoft.iot.vmp.service.impl;
 
 import com.alibaba.fastjson2.JSONArray;
 import com.alibaba.fastjson2.JSONObject;
+import com.baomidou.dynamic.datasource.annotation.DS;
 import com.genersoft.iot.vmp.common.GeneralCallback;
 import com.genersoft.iot.vmp.common.StreamInfo;
 import com.genersoft.iot.vmp.conf.DynamicTask;
@@ -53,6 +54,7 @@ import java.util.stream.Collectors;
  * 视频代理业务
  */
 @Service
+@DS("master")
 public class StreamProxyServiceImpl implements IStreamProxyService {
 
     private final static Logger logger = LoggerFactory.getLogger(StreamProxyServiceImpl.class);
@@ -126,7 +128,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 +409,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 +462,7 @@ public class StreamProxyServiceImpl implements IStreamProxyService {
         streamProxyMapper.deleteAutoRemoveItemByMediaServerId(mediaServerId);
 
         // 移除拉流代理生成的流信息
-//        syncPullStream(mediaServerId);
+        syncPullStream(mediaServerId);
 
         // 恢复流代理, 只查找这个这个流媒体
         List<StreamProxyItem> streamProxyListForEnable = storager.getStreamProxyListForEnableInMediaServer(

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

@@ -4,6 +4,7 @@ import com.alibaba.fastjson2.JSON;
 import com.alibaba.fastjson2.JSONArray;
 import com.alibaba.fastjson2.JSONObject;
 import com.alibaba.fastjson2.TypeReference;
+import com.baomidou.dynamic.datasource.annotation.DS;
 import com.genersoft.iot.vmp.conf.MediaConfig;
 import com.genersoft.iot.vmp.conf.UserSetting;
 import com.genersoft.iot.vmp.gb28181.bean.*;
@@ -36,6 +37,7 @@ import java.util.*;
 import java.util.stream.Collectors;
 
 @Service
+@DS("master")
 public class StreamPushServiceImpl implements IStreamPushService {
 
     private final static Logger logger = LoggerFactory.getLogger(StreamPushServiceImpl.class);
@@ -282,6 +284,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 +323,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);
             }
         }
     }

+ 2 - 0
src/main/java/com/genersoft/iot/vmp/service/impl/UserServiceImpl.java

@@ -1,5 +1,6 @@
 package com.genersoft.iot.vmp.service.impl;
 
+import com.baomidou.dynamic.datasource.annotation.DS;
 import com.genersoft.iot.vmp.service.IUserService;
 import com.genersoft.iot.vmp.storager.dao.UserMapper;
 import com.genersoft.iot.vmp.storager.dao.dto.User;
@@ -12,6 +13,7 @@ import org.springframework.util.DigestUtils;
 import java.util.List;
 
 @Service
+@DS("master")
 public class UserServiceImpl implements IUserService {
 
     @Autowired

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

@@ -13,6 +13,7 @@ import com.genersoft.iot.vmp.media.zlm.dto.HookSubscribeForStreamChange;
 import com.genersoft.iot.vmp.media.zlm.dto.MediaServerItem;
 import com.genersoft.iot.vmp.service.IMediaServerService;
 import com.genersoft.iot.vmp.service.bean.*;
+import com.genersoft.iot.vmp.storager.IRedisCatchStorage;
 import com.genersoft.iot.vmp.utils.redis.RedisUtil;
 import com.genersoft.iot.vmp.vmanager.bean.WVPResult;
 import org.slf4j.Logger;
@@ -77,6 +78,9 @@ public class RedisGbPlayMsgListener implements MessageListener {
     @Autowired
     private IMediaServerService mediaServerService;
 
+    @Autowired
+    private IRedisCatchStorage redisCatchStorage;
+
 
     @Autowired
     private DynamicTask dynamicTask;
@@ -113,8 +117,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 +127,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 +246,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 +264,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 +287,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);
@@ -322,9 +326,10 @@ public class RedisGbPlayMsgListener implements MessageListener {
         responseSendItemMsg.setSendRtpItem(sendRtpItem);
         responseSendItemMsg.setMediaServerItem(mediaServerItem);
         result.setData(responseSendItemMsg);
+        redisCatchStorage.updateSendRTPSever(sendRtpItem);
 
         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 +355,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 +380,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);
                     }
                 }
             });

+ 14 - 12
src/main/java/com/genersoft/iot/vmp/service/redisMsg/RedisPushStreamCloseResponseListener.java

@@ -73,12 +73,20 @@ public class RedisPushStreamCloseResponseListener implements MessageListener {
         MessageForPushChannel pushChannel = JSON.parseObject(message.getBody(), MessageForPushChannel.class);
         StreamPushItem push = streamPushService.getPush(pushChannel.getApp(), pushChannel.getStream());
         if (push != null) {
-            if (redisCatchStorage.isChannelSendingRTP(push.getGbId())) {
-                List<SendRtpItem> sendRtpItems = redisCatchStorage.querySendRTPServerByChnnelId(
-                        push.getGbId());
-                if (sendRtpItems.size() > 0) {
-                    for (SendRtpItem sendRtpItem : sendRtpItems) {
-                        ParentPlatform parentPlatform = storager.queryParentPlatByServerGBId(sendRtpItem.getPlatformId());
+            List<SendRtpItem> sendRtpItems = redisCatchStorage.querySendRTPServerByChnnelId(
+                    push.getGbId());
+            if (!sendRtpItems.isEmpty()) {
+                for (SendRtpItem sendRtpItem : sendRtpItems) {
+                    ParentPlatform parentPlatform = storager.queryParentPlatByServerGBId(sendRtpItem.getPlatformId());
+                    if (parentPlatform != null) {
+                        redisCatchStorage.deleteSendRTPServer(sendRtpItem.getPlatformId(), sendRtpItem.getChannelId(), sendRtpItem.getCallId(), sendRtpItem.getStreamId());
+                        try {
+                            commanderFroPlatform.streamByeCmd(parentPlatform, sendRtpItem);
+                        } catch (SipException | InvalidArgumentException | ParseException e) {
+                            logger.error("[命令发送失败] 国标级联 发送BYE: {}", e.getMessage());
+                        }
+                    }
+                    if (push.isSelf()) {
                         // 停止向上级推流
                         String streamId = sendRtpItem.getStream();
                         Map<String, Object> param = new HashMap<>();
@@ -90,12 +98,6 @@ public class RedisPushStreamCloseResponseListener implements MessageListener {
                         MediaServerItem mediaInfo = mediaServerService.getOne(sendRtpItem.getMediaServerId());
                         redisCatchStorage.deleteSendRTPServer(sendRtpItem.getPlatformId(), sendRtpItem.getChannelId(), sendRtpItem.getCallId(), sendRtpItem.getStream());
                         zlmServerFactory.stopSendRtpStream(mediaInfo, param);
-
-                        try {
-                            commanderFroPlatform.streamByeCmd(parentPlatform, sendRtpItem);
-                        } catch (SipException | InvalidArgumentException | ParseException e) {
-                            logger.error("[命令发送失败] 国标级联 发送BYE: {}", e.getMessage());
-                        }
                         if (InviteStreamType.PUSH == sendRtpItem.getPlayType()) {
                             MessageForPushChannel messageForPushChannel = MessageForPushChannel.getInstance(0,
                                     sendRtpItem.getApp(), sendRtpItem.getStream(), sendRtpItem.getChannelId(),

+ 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);
+}

+ 88 - 12
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,12 +66,52 @@ 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 " +
             "dc.device_id = #{deviceId} " +
-" <if test='query != null'> AND (dc.channel_id LIKE concat('%',#{query},'%') OR dc.name LIKE concat('%',#{query},'%') OR dc.name LIKE concat('%',#{query},'%'))</if> " +
+            " <if test='query != null'> AND (" +
+            "dc.channel_id LIKE concat('%',#{query},'%') " +
+            "OR dc.name LIKE concat('%',#{query},'%') " +
+            "OR dc.custom_name LIKE concat('%',#{query},'%')" +
+            ")</if> " +
             " <if test='parentChannelId != null'> AND (dc.parent_id=#{parentChannelId} OR dc.civil_code = #{parentChannelId}) </if> " +
             " <if test='online == true' > AND dc.status= true</if>" +
             " <if test='online == false' > AND dc.status= false</if>" +
@@ -154,7 +193,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 +431,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 +496,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>" +

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

@@ -20,7 +20,7 @@ public interface GbStreamMapper {
             "(#{app}, #{stream}, #{gbId}, #{name}, " +
             "#{longitude}, #{latitude}, #{streamType}, " +
             "#{mediaServerId}, #{createTime})")
-    @Options(useGeneratedKeys = true, keyProperty = "gbStreamId", keyColumn = "gbStreamId")
+    @Options(useGeneratedKeys = true, keyProperty = "gbStreamId", keyColumn = "gb_stream_id")
     int add(GbStream gbStream);
 
     @Update("UPDATE wvp_gb_stream " +
@@ -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();
+
 }

+ 6 - 5
src/main/java/com/genersoft/iot/vmp/storager/dao/PlatformChannelMapper.java

@@ -58,7 +58,10 @@ public interface PlatformChannelMapper {
     @Select("SELECT dc.* from wvp_platform_gb_channel pgc left join wvp_device_channel dc on dc.id = pgc.device_channel_id WHERE dc.channel_id=#{channelId} and pgc.platform_id=#{platformId}")
     List<DeviceChannel> queryChannelInParentPlatform(@Param("platformId") String platformId, @Param("channelId") String channelId);
 
-    @Select("SELECT dc.* from wvp_platform_gb_channel pgc left join wvp_device_channel dc on dc.id = pgc.device_channel_id WHERE pgc.platform_id=#{platformId} and pgc.catalog_id=#{catalogId}")
+    @Select("<script> "+
+            "SELECT dc.* from wvp_platform_gb_channel pgc left join wvp_device_channel dc on dc.id = pgc.device_channel_id WHERE pgc.platform_id=#{platformId} " +
+            " <if test='catalogId != null' > and pgc.catalog_id=#{catalogId}</if>" +
+            "</script>")
     List<DeviceChannel> queryAllChannelInCatalog(@Param("platformId") String platformId, @Param("catalogId") String catalogId);
 
     @Select(" select dc.channel_id as id, dc.name as name, pgc.platform_id as platform_id, pgc.catalog_id as parent_id, 0 as children_count, 1 as type " +
@@ -117,8 +120,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);
 }

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

@@ -53,11 +53,14 @@ public interface PlatformGbStreamMapper {
             "WHERE gs.app=#{app} AND gs.stream=#{stream} AND pgs.platform_id=#{platformId}")
     StreamProxyItem selectOne(@Param("app") String app, @Param("stream") String stream, @Param("platformId") String platformId);
 
-    @Select("select gs.* \n" +
-            "from wvp_gb_stream gs\n" +
+    @Select("<script> " +
+            "select gs.* " +
+            " from wvp_gb_stream gs\n" +
             "    left join wvp_platform_gb_stream pgs\n" +
             "        on gs.gb_stream_id = pgs.gb_stream_id\n" +
-            "where pgs.platform_id=#{platformId} and pgs.catalog_id=#{catalogId}")
+            " where pgs.platform_id=#{platformId} " +
+            " <if test='catalogId != null' > and pgs.catalog_id=#{catalogId}</if>" +
+            "</script>")
     List<GbStream> queryChannelInParentPlatformAndCatalog(@Param("platformId") String platformId, @Param("catalogId") String catalogId);
 
     @Select("select gs.gb_id as id, gs.name as name, pgs.platform_id as platform_id, pgs.catalog_id as catalog_id , 0 as children_count, 2 as type\n" +
@@ -103,6 +106,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);
+        }
+
+    }
 }

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

@@ -1,5 +1,6 @@
 package com.genersoft.iot.vmp.storager.impl;
 
+import com.baomidou.dynamic.datasource.annotation.DS;
 import com.genersoft.iot.vmp.conf.SipConfig;
 import com.genersoft.iot.vmp.conf.UserSetting;
 import com.genersoft.iot.vmp.gb28181.bean.*;
@@ -39,6 +40,7 @@ import java.util.concurrent.ConcurrentHashMap;
  */
 @SuppressWarnings("rawtypes")
 @Component
+@DS("master")
 public class VideoManagerStorageImpl implements IVideoManagerStorage {
 
 	private final Logger logger = LoggerFactory.getLogger(VideoManagerStorageImpl.class);

+ 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;
+    }
 }

+ 166 - 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;
@@ -21,6 +25,7 @@ import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.data.redis.core.RedisTemplate;
 import org.springframework.web.bind.annotation.*;
 
+import javax.servlet.http.HttpServletRequest;
 import java.util.ArrayList;
 import java.util.Calendar;
 import java.util.List;
@@ -32,40 +37,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 +87,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 +127,147 @@ 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;
+        }
+        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(
+            HttpServletRequest request,
+            @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
+    ){
+        MediaServerItem mediaServerItem;
+        if (mediaServerId == null) {
+            mediaServerItem = mediaServerService.getDefaultMediaServer();
+        }else {
+            mediaServerItem = mediaServerService.getOne(mediaServerId);
         }
-        List<RecordFile> records = mediaServerService.getRecords(app, stream, startTime, endTime, mediaServerItems);
-        PageInfo<RecordFile> pageInfo = new PageInfo<>(records);
-        pageInfo.startPage(page, count);
-        return pageInfo;
+        if (mediaServerItem == null) {
+            throw new ControllerException(ErrorCode.ERROR100.getCode(), "未找到可用的流媒体");
+        }else {
+            if (remoteHost == null) {
+                remoteHost = request.getScheme() + "://" + mediaServerItem.getIp() + ":" + mediaServerItem.getRecordAssistPort();
+            }
+        }
+        return cloudRecordService.addTask(app, stream, mediaServerItem, startTime, endTime, callId, remoteHost, mediaServerId != null);
     }
 
+    @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(
+            HttpServletRequest request,
+            @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
+    ){
+       if (ObjectUtils.isEmpty(mediaServerId)) {
+           mediaServerId = null;
+       }
 
+       return cloudRecordService.queryTask(app, stream, callId, taskId, mediaServerId, isEnd, request.getScheme());
+    }
+
+    @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) {

+ 0 - 37
src/main/java/com/genersoft/iot/vmp/vmanager/gb28181/SseController/SseController.java

@@ -1,37 +0,0 @@
-package com.genersoft.iot.vmp.vmanager.gb28181.SseController;
-
-import com.genersoft.iot.vmp.gb28181.event.alarm.AlarmEventListener;
-
-import io.swagger.v3.oas.annotations.tags.Tag;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.stereotype.Controller;
-import org.springframework.web.bind.annotation.CrossOrigin;
-import org.springframework.web.bind.annotation.GetMapping;
-import org.springframework.web.bind.annotation.RequestMapping;
-import org.springframework.web.bind.annotation.RequestParam;
-import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
-
-/**
- * @description: SSE推送
- * @author: lawrencehj
- * @data: 2021-01-20
- */
-@Tag(name  = "SSE推送")
-
-@Controller
-@RequestMapping("/api")
-public class SseController {
-    @Autowired
-    AlarmEventListener alarmEventListener;
-
-    @GetMapping("/emit")
-    public SseEmitter emit(@RequestParam String browserId) {
-        final SseEmitter sseEmitter = new SseEmitter(0L);
-        try {
-            alarmEventListener.addSseEmitters(browserId, sseEmitter);
-        }catch (Exception e){
-            sseEmitter.completeWithError(e);
-        }
-        return sseEmitter;
-    }
-}

+ 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();

+ 8 - 6
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,12 +81,12 @@ 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){
 
-        if (gbStreamParam.getGbStreams() == null || gbStreamParam.getGbStreams().size() == 0) {
+        if (gbStreamParam.getGbStreams() == null || gbStreamParam.getGbStreams().isEmpty()) {
             if (gbStreamParam.isAll()) {
                 gbStreamService.delAllPlatformInfo(gbStreamParam.getPlatformId(), gbStreamParam.getCatalogId());
             }
@@ -99,11 +101,11 @@ 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){
-        if (gbStreamParam.getGbStreams() == null || gbStreamParam.getGbStreams().size() == 0) {
+        if (gbStreamParam.getGbStreams() == null || gbStreamParam.getGbStreams().isEmpty()) {
             if (gbStreamParam.isAll()) {
                 List<GbStream> allGBChannels = gbStreamService.getAllGBChannels(gbStreamParam.getPlatformId());
                 gbStreamService.addPlatformInfo(allGBChannels, gbStreamParam.getPlatformId(), gbStreamParam.getCatalogId());
@@ -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}")

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


Some files were not shown because too many files changed in this diff