Browse Source

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

# Conflicts:
#	src/main/java/com/genersoft/iot/vmp/common/VideoManagerConstants.java
#	src/main/java/com/genersoft/iot/vmp/gb28181/transmit/cmd/ISIPCommander.java
#	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/info/InfoRequestProcessor.java
#	src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/MessageHandlerAbstract.java
#	src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/notify/cmd/KeepaliveNotifyMessageHandler.java
#	src/main/java/com/genersoft/iot/vmp/media/zlm/ZLMHttpHookListener.java
#	src/main/java/com/genersoft/iot/vmp/media/zlm/ZLMServerFactory.java
#	src/main/java/com/genersoft/iot/vmp/service/impl/MediaServerServiceImpl.java
648540858 2 năm trước cách đây
mục cha
commit
88350873ee
62 tập tin đã thay đổi với 3884 bổ sung2745 xóa
  1. 1 1
      doc/_content/introduction/config.md
  2. 63 63
      src/main/java/com/genersoft/iot/vmp/VManageBootstrap.java
  3. 177 175
      src/main/java/com/genersoft/iot/vmp/common/VideoManagerConstants.java
  4. 1 1
      src/main/java/com/genersoft/iot/vmp/conf/ApiAccessFilter.java
  5. 4 1
      src/main/java/com/genersoft/iot/vmp/conf/CivilCodeFileConf.java
  6. 3 2
      src/main/java/com/genersoft/iot/vmp/conf/DynamicTask.java
  7. 25 0
      src/main/java/com/genersoft/iot/vmp/conf/ProxyServletConfig.java
  8. 13 3
      src/main/java/com/genersoft/iot/vmp/conf/SipPlatformRunner.java
  9. 5 5
      src/main/java/com/genersoft/iot/vmp/conf/UserSetting.java
  10. 54 33
      src/main/java/com/genersoft/iot/vmp/conf/security/JwtUtils.java
  11. 141 141
      src/main/java/com/genersoft/iot/vmp/gb28181/SipLayer.java
  12. 187 187
      src/main/java/com/genersoft/iot/vmp/gb28181/bean/DeviceAlarm.java
  13. 102 102
      src/main/java/com/genersoft/iot/vmp/gb28181/bean/GBStringMsgParser.java
  14. 144 144
      src/main/java/com/genersoft/iot/vmp/gb28181/bean/RecordItem.java
  15. 6 6
      src/main/java/com/genersoft/iot/vmp/gb28181/bean/RemoteAddressInfo.java
  16. 118 118
      src/main/java/com/genersoft/iot/vmp/gb28181/bean/SubscribeInfo.java
  17. 139 139
      src/main/java/com/genersoft/iot/vmp/gb28181/event/SipSubscribe.java
  18. 13 1
      src/main/java/com/genersoft/iot/vmp/gb28181/task/ISubscribeTask.java
  19. 160 160
      src/main/java/com/genersoft/iot/vmp/gb28181/task/impl/CatalogSubscribeTask.java
  20. 39 39
      src/main/java/com/genersoft/iot/vmp/gb28181/transmit/callback/RequestMessage.java
  21. 364 364
      src/main/java/com/genersoft/iot/vmp/gb28181/transmit/cmd/ISIPCommander.java
  22. 2 0
      src/main/java/com/genersoft/iot/vmp/gb28181/transmit/cmd/ISIPCommanderForPlatform.java
  23. 6 6
      src/main/java/com/genersoft/iot/vmp/gb28181/transmit/cmd/SIPRequestHeaderPlarformProvider.java
  24. 2 1
      src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/NotifyRequestForCatalogProcessor.java
  25. 10 5
      src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/RegisterRequestProcessor.java
  26. 149 149
      src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/SubscribeRequestProcessor.java
  27. 11 1
      src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/IMessageHandler.java
  28. 1 0
      src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/MessageRequestProcessor.java
  29. 8 3
      src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/notify/cmd/KeepaliveNotifyMessageHandler.java
  30. 6 4
      src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/notify/cmd/MediaStatusNotifyMessageHandler.java
  31. 646 646
      src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/response/cmd/ConfigDownloadResponseMessageHandler.java
  32. 22 0
      src/main/java/com/genersoft/iot/vmp/media/zlm/AssistRESTfulUtils.java
  33. 33 9
      src/main/java/com/genersoft/iot/vmp/media/zlm/SendRtpPortManager.java
  34. 8 2
      src/main/java/com/genersoft/iot/vmp/media/zlm/ZLMHttpHookListener.java
  35. 1 1
      src/main/java/com/genersoft/iot/vmp/media/zlm/ZLMMediaListManager.java
  36. 11 0
      src/main/java/com/genersoft/iot/vmp/media/zlm/ZlmHttpHookSubscribe.java
  37. 19 6
      src/main/java/com/genersoft/iot/vmp/service/IMediaService.java
  38. 102 5
      src/main/java/com/genersoft/iot/vmp/service/impl/GbStreamServiceImpl.java
  39. 2 2
      src/main/java/com/genersoft/iot/vmp/service/impl/MediaServiceImpl.java
  40. 1 1
      src/main/java/com/genersoft/iot/vmp/service/impl/PlayServiceImpl.java
  41. 16 3
      src/main/java/com/genersoft/iot/vmp/storager/dao/DeviceMapper.java
  42. 5 0
      src/main/java/com/genersoft/iot/vmp/storager/impl/VideoManagerStorageImpl.java
  43. 50 50
      src/main/java/com/genersoft/iot/vmp/utils/GitUtil.java
  44. 45 45
      src/main/java/com/genersoft/iot/vmp/utils/SystemInfoUtils.java
  45. 47 46
      src/main/java/com/genersoft/iot/vmp/utils/redis/RedisUtil.java
  46. 137 0
      src/main/java/com/genersoft/iot/vmp/utils/redis/RedisUtil2.java
  47. 99 0
      src/main/java/com/genersoft/iot/vmp/vmanager/bean/OtherRtpSendInfo.java
  48. 53 0
      src/main/java/com/genersoft/iot/vmp/vmanager/bean/PlayTypeEnum.java
  49. 145 0
      src/main/java/com/genersoft/iot/vmp/vmanager/bean/ResourceBaseInfo.java
  50. 1 1
      src/main/java/com/genersoft/iot/vmp/vmanager/gb28181/MobilePosition/MobilePositionController.java
  51. 322 0
      src/main/java/com/genersoft/iot/vmp/vmanager/ps/PsController.java
  52. 2 4
      src/main/java/com/genersoft/iot/vmp/vmanager/record/RecordController.java
  53. 1 1
      src/main/java/com/genersoft/iot/vmp/vmanager/server/ServerController.java
  54. 1 1
      src/main/resources/all-application.yml
  55. 12 12
      src/main/resources/application-dev.yml
  56. 1 1
      src/main/resources/application.yml
  57. 2 4
      web_src/src/App.vue
  58. 114 51
      web_src/src/components/CloudRecordDetail.vue
  59. 6 0
      web_src/src/components/DeviceList.vue
  60. 6 0
      web_src/src/components/StreamProxyList.vue
  61. 9 0
      web_src/src/components/UserManager.vue
  62. 11 0
      web_src/src/components/common/ h265web.vue

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

@@ -153,7 +153,7 @@ user-settings:
     # 国标是否录制
     record-sip: true
     # 是否将日志存储进数据库
-    logInDatebase: true
+    logInDatabase: true
     # 第三方匹配,用于从stream钟获取有效信息
     thirdPartyGBIdReg: [\s\S]*
 ```

+ 63 - 63
src/main/java/com/genersoft/iot/vmp/VManageBootstrap.java

@@ -1,63 +1,63 @@
-package com.genersoft.iot.vmp;
-
-import com.genersoft.iot.vmp.utils.GitUtil;
-import com.genersoft.iot.vmp.utils.SpringBeanFactory;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import org.springframework.boot.SpringApplication;
-import org.springframework.boot.autoconfigure.SpringBootApplication;
-import org.springframework.boot.builder.SpringApplicationBuilder;
-import org.springframework.boot.web.servlet.ServletComponentScan;
-import org.springframework.boot.web.servlet.support.SpringBootServletInitializer;
-import org.springframework.context.ConfigurableApplicationContext;
-import org.springframework.scheduling.annotation.EnableScheduling;
-
-import javax.servlet.ServletContext;
-import javax.servlet.ServletException;
-import javax.servlet.SessionCookieConfig;
-import javax.servlet.SessionTrackingMode;
-import java.util.Collections;
-
-/**
- * 启动类
- */
-@ServletComponentScan("com.genersoft.iot.vmp.conf")
-@SpringBootApplication
-@EnableScheduling
-public class VManageBootstrap extends SpringBootServletInitializer {
-
-	private final static Logger logger = LoggerFactory.getLogger(VManageBootstrap.class);
-
-	private static String[] args;
-	private static ConfigurableApplicationContext context;
-	public static void main(String[] args) {
-		VManageBootstrap.args = args;
-		VManageBootstrap.context = SpringApplication.run(VManageBootstrap.class, args);
-		GitUtil gitUtil1 = SpringBeanFactory.getBean("gitUtil");
-		logger.info("构建版本: {}", gitUtil1.getBuildVersion());
-		logger.info("构建时间: {}", gitUtil1.getBuildDate());
-		logger.info("GIT最后提交时间: {}", gitUtil1.getCommitTime());
-	}
-	// 项目重启
-	public static void restart() {
-		context.close();
-		VManageBootstrap.context = SpringApplication.run(VManageBootstrap.class, args);
-	}
-
-	@Override
-	protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
-		return application.sources(VManageBootstrap.class);
-	}
-
-	@Override
-	public void onStartup(ServletContext servletContext) throws ServletException {
-		super.onStartup(servletContext);
-
-		servletContext.setSessionTrackingModes(
-				Collections.singleton(SessionTrackingMode.COOKIE)
-		);
-		SessionCookieConfig sessionCookieConfig = servletContext.getSessionCookieConfig();
-		sessionCookieConfig.setHttpOnly(true);
-
-	}
-}
+package com.genersoft.iot.vmp;
+
+import com.genersoft.iot.vmp.utils.GitUtil;
+import com.genersoft.iot.vmp.utils.SpringBeanFactory;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.boot.builder.SpringApplicationBuilder;
+import org.springframework.boot.web.servlet.ServletComponentScan;
+import org.springframework.boot.web.servlet.support.SpringBootServletInitializer;
+import org.springframework.context.ConfigurableApplicationContext;
+import org.springframework.scheduling.annotation.EnableScheduling;
+
+import javax.servlet.ServletContext;
+import javax.servlet.ServletException;
+import javax.servlet.SessionCookieConfig;
+import javax.servlet.SessionTrackingMode;
+import java.util.Collections;
+
+/**
+ * 启动类
+ */
+@ServletComponentScan("com.genersoft.iot.vmp.conf")
+@SpringBootApplication
+@EnableScheduling
+public class VManageBootstrap extends SpringBootServletInitializer {
+
+	private final static Logger logger = LoggerFactory.getLogger(VManageBootstrap.class);
+
+	private static String[] args;
+	private static ConfigurableApplicationContext context;
+	public static void main(String[] args) {
+		VManageBootstrap.args = args;
+		VManageBootstrap.context = SpringApplication.run(VManageBootstrap.class, args);
+		GitUtil gitUtil1 = SpringBeanFactory.getBean("gitUtil");
+		logger.info("构建版本: {}", gitUtil1.getBuildVersion());
+		logger.info("构建时间: {}", gitUtil1.getBuildDate());
+		logger.info("GIT最后提交时间: {}", gitUtil1.getCommitTime());
+	}
+	// 项目重启
+	public static void restart() {
+		context.close();
+		VManageBootstrap.context = SpringApplication.run(VManageBootstrap.class, args);
+	}
+
+	@Override
+	protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
+		return application.sources(VManageBootstrap.class);
+	}
+
+	@Override
+	public void onStartup(ServletContext servletContext) throws ServletException {
+		super.onStartup(servletContext);
+
+		servletContext.setSessionTrackingModes(
+				Collections.singleton(SessionTrackingMode.COOKIE)
+		);
+		SessionCookieConfig sessionCookieConfig = servletContext.getSessionCookieConfig();
+		sessionCookieConfig.setHttpOnly(true);
+
+	}
+}

+ 177 - 175
src/main/java/com/genersoft/iot/vmp/common/VideoManagerConstants.java

@@ -1,175 +1,177 @@
-package com.genersoft.iot.vmp.common;
-
-/**    
- * @description: 定义常量   
- * @author: swwheihei
- * @date:   2019年5月30日 下午3:04:04   
- *   
- */
-public class VideoManagerConstants {
-	
-	public static final String WVP_SERVER_PREFIX = "VMP_SIGNALLING_SERVER_INFO_";
-
-	public static final String WVP_SERVER_STREAM_PREFIX = "VMP_SIGNALLING_STREAM_";
-
-	public static final String MEDIA_SERVER_PREFIX = "VMP_MEDIA_SERVER_";
-
-	public static final String MEDIA_SERVERS_ONLINE_PREFIX = "VMP_MEDIA_ONLINE_SERVERS_";
-
-	public static final String DEVICE_PREFIX = "VMP_DEVICE_";
-
-	// 设备同步完成
-	public static final String DEVICE_SYNC_PREFIX = "VMP_DEVICE_SYNC_";
-
-	public static final String CACHEKEY_PREFIX = "VMP_CHANNEL_";
-
-	public static final String KEEPLIVEKEY_PREFIX = "VMP_KEEPALIVE_";
-
-	// TODO 此处多了一个_,暂不修改
-	public static final String INVITE_PREFIX = "VMP_INVITE";
-	public static final String PLAYER_PREFIX = "VMP_INVITE_PLAY_";
-	public static final String PLAY_BLACK_PREFIX = "VMP_INVITE_PLAYBACK_";
-	public static final String DOWNLOAD_PREFIX = "VMP_INVITE_DOWNLOAD_";
-
-	public static final String PLATFORM_KEEPALIVE_PREFIX = "VMP_PLATFORM_KEEPALIVE_";
-
-	public static final String PLATFORM_CATCH_PREFIX = "VMP_PLATFORM_CATCH_";
-
-	public static final String PLATFORM_REGISTER_PREFIX = "VMP_PLATFORM_REGISTER_";
-
-	public static final String PLATFORM_REGISTER_INFO_PREFIX = "VMP_PLATFORM_REGISTER_INFO_";
-
-	public static final String PLATFORM_SEND_RTP_INFO_PREFIX = "VMP_PLATFORM_SEND_RTP_INFO_";
-
-	public static final String EVENT_ONLINE_REGISTER = "1";
-
-	public static final String EVENT_ONLINE_MESSAGE = "3";
-
-	public static final String EVENT_OUTLINE_UNREGISTER = "1";
-	
-	public static final String EVENT_OUTLINE_TIMEOUT = "2";
-
-	public static final String MEDIA_SSRC_USED_PREFIX = "VMP_MEDIA_USED_SSRC_";
-
-	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 SIP_CSEQ_PREFIX = "VMP_SIP_CSEQ_";
-
-	public static final String SIP_SN_PREFIX = "VMP_SIP_SN_";
-
-	public static final String SIP_SUBSCRIBE_PREFIX = "VMP_SIP_SUBSCRIBE_";
-
-	public static final String SYSTEM_INFO_CPU_PREFIX = "VMP_SYSTEM_INFO_CPU_";
-
-	public static final String SYSTEM_INFO_MEM_PREFIX = "VMP_SYSTEM_INFO_MEM_";
-
-	public static final String SYSTEM_INFO_NET_PREFIX = "VMP_SYSTEM_INFO_NET_";
-
-	public static final String SYSTEM_INFO_DISK_PREFIX = "VMP_SYSTEM_INFO_DISK_";
-	public static final String BROADCAST_WAITE_INVITE = "task_broadcast_waite_invite_";
-
-	public static final String REGISTER_EXPIRE_TASK_KEY_PREFIX = "VMP_device_register_expire_";
-
-
-
-
-	//************************** redis 消息*********************************
-
-	/**
-	 * 流变化的通知
-	 */
-	public static final String WVP_MSG_STREAM_CHANGE_PREFIX = "WVP_MSG_STREAM_CHANGE_";
-
-	/**
-	 * 接收推流设备的GPS变化通知
-	 */
-	public static final String VM_MSG_GPS = "VM_MSG_GPS";
-
-	/**
-	 * 接收推流设备的GPS变化通知
-	 */
-	public static final String VM_MSG_PUSH_STREAM_STATUS_CHANGE = "VM_MSG_PUSH_STREAM_STATUS_CHANGE";
-	/**
-	 * 接收推流设备列表更新变化通知
-	 */
-	public static final String VM_MSG_PUSH_STREAM_LIST_CHANGE = "VM_MSG_PUSH_STREAM_LIST_CHANGE";
-
-	/**
-	 * redis 消息通知设备推流到平台
-	 */
-	public static final String VM_MSG_STREAM_PUSH_REQUESTED = "VM_MSG_STREAM_PUSH_REQUESTED";
-
-	/**
-	 * redis 消息通知上级平台开始观看流
-	 */
-	public static final String VM_MSG_STREAM_START_PLAY_NOTIFY = "VM_MSG_STREAM_START_PLAY_NOTIFY";
-
-	/**
-	 * redis 消息通知上级平台停止观看流
-	 */
-	public static final String VM_MSG_STREAM_STOP_PLAY_NOTIFY = "VM_MSG_STREAM_STOP_PLAY_NOTIFY";
-
-	/**
-	 * redis 消息接收关闭一个推流
-	 */
-	public static final String VM_MSG_STREAM_PUSH_CLOSE_REQUESTED = "VM_MSG_STREAM_PUSH_CLOSE_REQUESTED";
-
-
-	/**
-	 * redis 消息通知平台通知设备推流结果
-	 */
-	public static final String VM_MSG_STREAM_PUSH_RESPONSE = "VM_MSG_STREAM_PUSH_RESPONSE";
-
-	/**
-	 * redis 通知平台关闭推流
-	 */
-	public static final String VM_MSG_STREAM_PUSH_CLOSE = "VM_MSG_STREAM_PUSH_CLOSE";
-
-	/**
-	 * redis 消息请求所有的在线通道
-	 */
-	public static final String VM_MSG_GET_ALL_ONLINE_REQUESTED = "VM_MSG_GET_ALL_ONLINE_REQUESTED";
-
-	/**
-	 * 移动位置订阅通知
-	 */
-	public static final String VM_MSG_SUBSCRIBE_MOBILE_POSITION = "mobileposition";
-
-	/**
-	 * 报警订阅的通知(收到报警向redis发出通知)
-	 */
-	public static final String VM_MSG_SUBSCRIBE_ALARM = "alarm";
-
-
-	/**
-	 * 报警通知的发送 (收到redis发出的通知,转发给其他平台)
-	 */
-	public static final String VM_MSG_SUBSCRIBE_ALARM_RECEIVE= "alarm_receive";
-
-	/**
-	 * 设备状态订阅的通知
-	 */
-	public static final String VM_MSG_SUBSCRIBE_DEVICE_STATUS = "device";
-
-
-	//**************************    第三方  ****************************************
-
-	public static final String WVP_STREAM_GB_ID_PREFIX = "memberNo_";
-	public static final String WVP_STREAM_GPS_MSG_PREFIX = "WVP_STREAM_GPS_MSG_";
-	public static final String WVP_OTHER_SEND_RTP_INFO = "VMP_OTHER_SEND_RTP_INFO_";
-	public static final String WVP_OTHER_RECEIVE_RTP_INFO = "VMP_OTHER_RECEIVE_RTP_INFO_";
-
-	/**
-	 * Redis Const
-	 * 设备录像信息结果前缀
-	 */
-	public static final String REDIS_RECORD_INFO_RES_PRE = "GB_RECORD_INFO_RES_";
-	/**
-	 * Redis Const
-	 * 设备录像信息结果前缀
-	 */
-	public static final String REDIS_RECORD_INFO_RES_COUNT_PRE = "GB_RECORD_INFO_RES_COUNT:";
-
-}
+package com.genersoft.iot.vmp.common;
+
+/**    
+ * @description: 定义常量   
+ * @author: swwheihei
+ * @date:   2019年5月30日 下午3:04:04   
+ *   
+ */
+public class VideoManagerConstants {
+	
+	public static final String WVP_SERVER_PREFIX = "VMP_SIGNALLING_SERVER_INFO_";
+
+	public static final String WVP_SERVER_STREAM_PREFIX = "VMP_SIGNALLING_STREAM_";
+
+	public static final String MEDIA_SERVER_PREFIX = "VMP_MEDIA_SERVER_";
+
+	public static final String MEDIA_SERVERS_ONLINE_PREFIX = "VMP_MEDIA_ONLINE_SERVERS_";
+
+	public static final String DEVICE_PREFIX = "VMP_DEVICE_";
+
+	// 设备同步完成
+	public static final String DEVICE_SYNC_PREFIX = "VMP_DEVICE_SYNC_";
+
+	public static final String CACHEKEY_PREFIX = "VMP_CHANNEL_";
+
+	public static final String KEEPLIVEKEY_PREFIX = "VMP_KEEPALIVE_";
+
+	// TODO 此处多了一个_,暂不修改
+	public static final String INVITE_PREFIX = "VMP_INVITE";
+	public static final String PLAYER_PREFIX = "VMP_INVITE_PLAY_";
+	public static final String PLAY_BLACK_PREFIX = "VMP_INVITE_PLAYBACK_";
+	public static final String DOWNLOAD_PREFIX = "VMP_INVITE_DOWNLOAD_";
+
+	public static final String PLATFORM_KEEPALIVE_PREFIX = "VMP_PLATFORM_KEEPALIVE_";
+
+	public static final String PLATFORM_CATCH_PREFIX = "VMP_PLATFORM_CATCH_";
+
+	public static final String PLATFORM_REGISTER_PREFIX = "VMP_PLATFORM_REGISTER_";
+
+	public static final String PLATFORM_REGISTER_INFO_PREFIX = "VMP_PLATFORM_REGISTER_INFO_";
+
+	public static final String PLATFORM_SEND_RTP_INFO_PREFIX = "VMP_PLATFORM_SEND_RTP_INFO_";
+
+	public static final String EVENT_ONLINE_REGISTER = "1";
+
+	public static final String EVENT_ONLINE_MESSAGE = "3";
+
+	public static final String EVENT_OUTLINE_UNREGISTER = "1";
+	
+	public static final String EVENT_OUTLINE_TIMEOUT = "2";
+
+	public static final String MEDIA_SSRC_USED_PREFIX = "VMP_MEDIA_USED_SSRC_";
+
+	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 SIP_CSEQ_PREFIX = "VMP_SIP_CSEQ_";
+
+	public static final String SIP_SN_PREFIX = "VMP_SIP_SN_";
+
+	public static final String SIP_SUBSCRIBE_PREFIX = "VMP_SIP_SUBSCRIBE_";
+
+	public static final String SYSTEM_INFO_CPU_PREFIX = "VMP_SYSTEM_INFO_CPU_";
+
+	public static final String SYSTEM_INFO_MEM_PREFIX = "VMP_SYSTEM_INFO_MEM_";
+
+	public static final String SYSTEM_INFO_NET_PREFIX = "VMP_SYSTEM_INFO_NET_";
+
+	public static final String SYSTEM_INFO_DISK_PREFIX = "VMP_SYSTEM_INFO_DISK_";
+	public static final String BROADCAST_WAITE_INVITE = "task_broadcast_waite_invite_";
+
+	public static final String REGISTER_EXPIRE_TASK_KEY_PREFIX = "VMP_device_register_expire_";
+
+
+
+
+	//************************** redis 消息*********************************
+
+	/**
+	 * 流变化的通知
+	 */
+	public static final String WVP_MSG_STREAM_CHANGE_PREFIX = "WVP_MSG_STREAM_CHANGE_";
+
+	/**
+	 * 接收推流设备的GPS变化通知
+	 */
+	public static final String VM_MSG_GPS = "VM_MSG_GPS";
+
+	/**
+	 * 接收推流设备的GPS变化通知
+	 */
+	public static final String VM_MSG_PUSH_STREAM_STATUS_CHANGE = "VM_MSG_PUSH_STREAM_STATUS_CHANGE";
+	/**
+	 * 接收推流设备列表更新变化通知
+	 */
+	public static final String VM_MSG_PUSH_STREAM_LIST_CHANGE = "VM_MSG_PUSH_STREAM_LIST_CHANGE";
+
+	/**
+	 * redis 消息通知设备推流到平台
+	 */
+	public static final String VM_MSG_STREAM_PUSH_REQUESTED = "VM_MSG_STREAM_PUSH_REQUESTED";
+
+	/**
+	 * redis 消息通知上级平台开始观看流
+	 */
+	public static final String VM_MSG_STREAM_START_PLAY_NOTIFY = "VM_MSG_STREAM_START_PLAY_NOTIFY";
+
+	/**
+	 * redis 消息通知上级平台停止观看流
+	 */
+	public static final String VM_MSG_STREAM_STOP_PLAY_NOTIFY = "VM_MSG_STREAM_STOP_PLAY_NOTIFY";
+
+	/**
+	 * redis 消息接收关闭一个推流
+	 */
+	public static final String VM_MSG_STREAM_PUSH_CLOSE_REQUESTED = "VM_MSG_STREAM_PUSH_CLOSE_REQUESTED";
+
+
+	/**
+	 * redis 消息通知平台通知设备推流结果
+	 */
+	public static final String VM_MSG_STREAM_PUSH_RESPONSE = "VM_MSG_STREAM_PUSH_RESPONSE";
+
+	/**
+	 * redis 通知平台关闭推流
+	 */
+	public static final String VM_MSG_STREAM_PUSH_CLOSE = "VM_MSG_STREAM_PUSH_CLOSE";
+
+	/**
+	 * redis 消息请求所有的在线通道
+	 */
+	public static final String VM_MSG_GET_ALL_ONLINE_REQUESTED = "VM_MSG_GET_ALL_ONLINE_REQUESTED";
+
+	/**
+	 * 移动位置订阅通知
+	 */
+	public static final String VM_MSG_SUBSCRIBE_MOBILE_POSITION = "mobileposition";
+
+	/**
+	 * 报警订阅的通知(收到报警向redis发出通知)
+	 */
+	public static final String VM_MSG_SUBSCRIBE_ALARM = "alarm";
+
+
+	/**
+	 * 报警通知的发送 (收到redis发出的通知,转发给其他平台)
+	 */
+	public static final String VM_MSG_SUBSCRIBE_ALARM_RECEIVE= "alarm_receive";
+
+	/**
+	 * 设备状态订阅的通知
+	 */
+	public static final String VM_MSG_SUBSCRIBE_DEVICE_STATUS = "device";
+
+
+	//**************************    第三方  ****************************************
+
+	public static final String WVP_STREAM_GB_ID_PREFIX = "memberNo_";
+	public static final String WVP_STREAM_GPS_MSG_PREFIX = "WVP_STREAM_GPS_MSG_";
+	public static final String WVP_OTHER_SEND_RTP_INFO = "VMP_OTHER_SEND_RTP_INFO_";
+	public static final String WVP_OTHER_SEND_PS_INFO = "VMP_OTHER_SEND_PS_INFO_";
+	public static final String WVP_OTHER_RECEIVE_RTP_INFO = "VMP_OTHER_RECEIVE_RTP_INFO_";
+	public static final String WVP_OTHER_RECEIVE_PS_INFO = "VMP_OTHER_RECEIVE_PS_INFO_";
+
+	/**
+	 * Redis Const
+	 * 设备录像信息结果前缀
+	 */
+	public static final String REDIS_RECORD_INFO_RES_PRE = "GB_RECORD_INFO_RES_";
+	/**
+	 * Redis Const
+	 * 设备录像信息结果前缀
+	 */
+	public static final String REDIS_RECORD_INFO_RES_COUNT_PRE = "GB_RECORD_INFO_RES_COUNT:";
+
+}

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

@@ -51,7 +51,7 @@ public class ApiAccessFilter extends OncePerRequestFilter {
 
         filterChain.doFilter(servletRequest, servletResponse);
 
-        if (uriName != null && userSetting != null && userSetting.getLogInDatebase() != null && userSetting.getLogInDatebase()) {
+        if (uriName != null && userSetting != null && userSetting.getLogInDatabase() != null && userSetting.getLogInDatabase()) {
 
             LogDto logDto = new LogDto();
             logDto.setName(uriName);

+ 4 - 1
src/main/java/com/genersoft/iot/vmp/conf/CivilCodeFileConf.java

@@ -12,7 +12,10 @@ import org.springframework.core.annotation.Order;
 import org.springframework.core.io.ClassPathResource;
 import org.springframework.util.ObjectUtils;
 
-import java.io.*;
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.InputStream;
+import java.io.InputStreamReader;
 import java.nio.file.Files;
 import java.util.Map;
 

+ 3 - 2
src/main/java/com/genersoft/iot/vmp/conf/DynamicTask.java

@@ -111,7 +111,7 @@ public class DynamicTask {
         }
         boolean result = false;
         if (!ObjectUtils.isEmpty(futureMap.get(key)) && !futureMap.get(key).isCancelled() && !futureMap.get(key).isDone()) {
-            result = futureMap.get(key).cancel(true);
+            result = futureMap.get(key).cancel(false);
             futureMap.remove(key);
             runnableMap.remove(key);
         }
@@ -143,7 +143,8 @@ public class DynamicTask {
     public void execute(){
         if (futureMap.size() > 0) {
             for (String key : futureMap.keySet()) {
-                if (futureMap.get(key).isDone() || futureMap.get(key).isCancelled()) {
+                ScheduledFuture<?> future = futureMap.get(key);
+                if (future.isDone() || future.isCancelled()) {
                     futureMap.remove(key);
                     runnableMap.remove(key);
                 }

+ 25 - 0
src/main/java/com/genersoft/iot/vmp/conf/ProxyServletConfig.java

@@ -18,6 +18,7 @@ import org.springframework.util.ObjectUtils;
 
 import javax.servlet.ServletException;
 import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
 import java.io.IOException;
 import java.net.ConnectException;
 
@@ -64,6 +65,18 @@ public class ProxyServletConfig {
             return queryStr;
         }
 
+
+        @Override
+        protected HttpResponse doExecute(HttpServletRequest servletRequest, HttpServletResponse servletResponse,
+                                         HttpRequest proxyRequest) throws IOException {
+            HttpResponse response = super.doExecute(servletRequest, servletResponse, proxyRequest);
+            response.removeHeaders("Access-Control-Allow-Origin");
+            response.setHeader("Access-Control-Allow-Credentials","true");
+            response.removeHeaders("Access-Control-Allow-Credentials");
+
+            return response;
+        }
+
         /**
          * 异常处理
          */
@@ -181,6 +194,18 @@ public class ProxyServletConfig {
             return queryStr;
         }
 
+
+        @Override
+        protected HttpResponse doExecute(HttpServletRequest servletRequest, HttpServletResponse servletResponse,
+                                         HttpRequest proxyRequest) throws IOException {
+            HttpResponse response = super.doExecute(servletRequest, servletResponse, proxyRequest);
+            String origin = servletRequest.getHeader("origin");
+            response.setHeader("Access-Control-Allow-Origin",origin);
+            response.setHeader("Access-Control-Allow-Credentials","true");
+
+            return response;
+        }
+
         /**
          * 异常处理
          */

+ 13 - 3
src/main/java/com/genersoft/iot/vmp/conf/SipPlatformRunner.java

@@ -4,8 +4,11 @@ import com.genersoft.iot.vmp.gb28181.bean.ParentPlatform;
 import com.genersoft.iot.vmp.gb28181.bean.ParentPlatformCatch;
 import com.genersoft.iot.vmp.gb28181.transmit.cmd.ISIPCommanderForPlatform;
 import com.genersoft.iot.vmp.service.IPlatformService;
+import com.genersoft.iot.vmp.service.impl.PlatformServiceImpl;
 import com.genersoft.iot.vmp.storager.IRedisCatchStorage;
 import com.genersoft.iot.vmp.storager.IVideoManagerStorage;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.boot.CommandLineRunner;
 import org.springframework.core.annotation.Order;
@@ -33,6 +36,7 @@ public class SipPlatformRunner implements CommandLineRunner {
     @Autowired
     private ISIPCommanderForPlatform sipCommanderForPlatform;
 
+    private final static Logger logger = LoggerFactory.getLogger(PlatformServiceImpl.class);
 
     @Override
     public void run(String... args) throws Exception {
@@ -50,9 +54,15 @@ public class SipPlatformRunner implements CommandLineRunner {
             redisCatchStorage.updatePlatformCatchInfo(parentPlatformCatch);
             if (parentPlatformCatchOld != null) {
                 // 取消订阅
-                sipCommanderForPlatform.unregister(parentPlatform, parentPlatformCatchOld.getSipTransactionInfo(), null, (eventResult)->{
-                    platformService.login(parentPlatform);
-                });
+                try {
+                    sipCommanderForPlatform.unregister(parentPlatform, parentPlatformCatchOld.getSipTransactionInfo(), null, (eventResult)->{
+                        platformService.login(parentPlatform);
+                    });
+                } catch (Exception e) {
+                    logger.error("[命令发送失败] 国标级联 注销: {}", e.getMessage());
+                    platformService.offline(parentPlatform, true);
+                    continue;
+                }
             }
 
             // 设置所有平台离线

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

@@ -31,7 +31,7 @@ public class UserSetting {
 
     private Boolean recordSip = Boolean.TRUE;
 
-    private Boolean logInDatebase = Boolean.TRUE;
+    private Boolean logInDatabase = Boolean.TRUE;
 
     private Boolean usePushingAsStatus = Boolean.FALSE;
 
@@ -134,12 +134,12 @@ public class UserSetting {
         this.interfaceAuthenticationExcludes = interfaceAuthenticationExcludes;
     }
 
-    public Boolean getLogInDatebase() {
-        return logInDatebase;
+    public Boolean getLogInDatabase() {
+        return logInDatabase;
     }
 
-    public void setLogInDatebase(Boolean logInDatebase) {
-        this.logInDatebase = logInDatebase;
+    public void setLogInDatabase(Boolean logInDatabase) {
+        this.logInDatabase = logInDatabase;
     }
 
     public String getServerId() {

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

@@ -1,8 +1,10 @@
 package com.genersoft.iot.vmp.conf.security;
 
 import com.genersoft.iot.vmp.conf.security.dto.JwtUser;
-import org.jose4j.json.JsonUtil;
+import com.genersoft.iot.vmp.service.IUserService;
+import com.genersoft.iot.vmp.storager.dao.dto.User;
 import org.jose4j.jwk.RsaJsonWebKey;
+import org.jose4j.jwk.RsaJwkGenerator;
 import org.jose4j.jws.AlgorithmIdentifiers;
 import org.jose4j.jws.JsonWebSignature;
 import org.jose4j.jwt.JwtClaims;
@@ -14,45 +16,69 @@ import org.jose4j.jwt.consumer.JwtConsumerBuilder;
 import org.jose4j.lang.JoseException;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.InitializingBean;
+import org.springframework.stereotype.Component;
 
-import java.security.PrivateKey;
+import javax.annotation.Resource;
 import java.time.LocalDateTime;
 import java.time.ZoneOffset;
 
-public class JwtUtils {
+@Component
+public class JwtUtils implements InitializingBean {
 
     private static final Logger logger = LoggerFactory.getLogger(JwtUtils.class);
 
     private static final String HEADER = "access-token";
-    private static final String AUDIENCE = "Audience";
 
-    private static final long EXPIRED_THRESHOLD = 10 * 60;
+    private static final String AUDIENCE = "Audience";
 
     private static final String keyId = "3e79646c4dbc408383a9eed09f2b85ae";
-    private static final String privateKeyStr = "{\"kty\":\"RSA\",\"kid\":\"3e79646c4dbc408383a9eed09f2b85ae\",\"alg\":\"RS256\",\"n\":\"gndmVdiOTSJ5et2HIeTM5f1m61x5ojLUi5HDfvr-jRrESQ5kbKuySGHVwR4QhwinpY1wQqBnwc80tx7cb_6SSqsTOoGln6T_l3k2Pb54ClVnGWiW_u1kmX78V2TZOsVmZmwtdZCMi-2zWIyAdIEXE-gncIehoAgEoq2VAhaCURbJWro_EwzzQwNmCTkDodLAx4npXRd_qSu0Ayp0txym9OFovBXBULRvk4DPiy3i_bPUmCDxzC46pTtFOe9p82uybTehZfULZtXXqRm85FL9n5zkrsTllPNAyEGhgb0RK9sE5nK1m_wNNysDyfLC4EFf1VXTrKm14XNVjc2vqLb7Mw\",\"e\":\"AQAB\",\"d\":\"ed7U_k3rJ4yTk70JtRSIfjKGiEb67BO1TabcymnljKO7RU8nage84zZYuSu_XpQsHk6P1f0Gzxkicghm_Er-FrfVn2pp70Xu52z3yRd6BJUgWLDFk97ngScIyw5OiULKU9SrZk2frDpftNCSUcIgb50F8m0QAnBa_CdPsQKbuuhLv8V8tBAV7F_lAwvSBgu56wRo3hPz5dWH8YeXM7XBfQ9viFMNEKd21sP_j5C7ueUnXT66nBxe3ZJEU3iuMYM6D6dB_KW2GfZC6WmTgvGhhxJD0h7aYmfjkD99MDleB7SkpbvoODOqiQ5Epb7Nyh6kv5u4KUv2CJYtATLZkUeMkQ\",\"p\":\"uBUjWPWtlGksmOqsqCNWksfqJvMcnP_8TDYN7e4-WnHL4N-9HjRuPDnp6kHvCIEi9SEfxm7gNxlRcWegvNQr3IZCz7TnCTexXc5NOklB9OavWFla6u-s3Thn6Tz45-EUjpJr0VJMxhO-KxGmuTwUXBBp4vN6K2qV6rQNFmgkWzk\",\"q\":\"tW_i7cCec56bHkhITL_79dXHz_PLC_f7xlynmlZJGU_d6mqOKmLBNBbTMLnYW8uAFiFzWxDeDHh1o5uF0mSQR-Z1Fg35OftnpbWpy0Cbc2la5WgXQjOwtG1eLYIY2BD3-wQ1VYDBCvowr4FDi-sngxwLqvwmrJ0xjhi99O-Gzcs\",\"dp\":\"q1d5jE85Hz_6M-eTh_lEluEf0NtPEc-vvhw-QO4V-cecNpbrCBdTWBmr4dE3NdpFeJc5ZVFEv-SACyei1MBEh0ItI_pFZi4BmMfy2ELh8ptaMMkTOESYyVy8U7veDq9RnBcr5i1Nqr0rsBkA77-9T6gzdvycBZdzLYAkAmwzEvk\",\"dq\":\"q29A2K08Crs-jmp2Bi8Q_8QzvIX6wSBbwZ4ir24AO-5_HNP56IrPS0yV2GCB0pqCOGb6_Hz_koDvhtuYoqdqvMVAtMoXR3YJBUaVXPt65p4RyNmFwIPe31zHs_BNUTsXVRMw4c16mci03-Af1sEm4HdLfxAp6sfM3xr5wcnhcek\",\"qi\":\"rHPgVTyHUHuYzcxfouyBfb1XAY8nshwn0ddo81o1BccD4Z7zo5It6SefDHjxCAbcmbiCcXBSooLcY-NF5FMv3fg19UE21VyLQltHcVjRRp2tRs4OHcM8yaXIU2x6N6Z6BP2tOksHb9MOBY1wAQzFOAKg_G4Sxev6-_6ud6RISuc\"}";
-    private static final String publicKeyStr = "{\"kty\":\"RSA\",\"kid\":\"3e79646c4dbc408383a9eed09f2b85ae\",\"alg\":\"RS256\",\"n\":\"gndmVdiOTSJ5et2HIeTM5f1m61x5ojLUi5HDfvr-jRrESQ5kbKuySGHVwR4QhwinpY1wQqBnwc80tx7cb_6SSqsTOoGln6T_l3k2Pb54ClVnGWiW_u1kmX78V2TZOsVmZmwtdZCMi-2zWIyAdIEXE-gncIehoAgEoq2VAhaCURbJWro_EwzzQwNmCTkDodLAx4npXRd_qSu0Ayp0txym9OFovBXBULRvk4DPiy3i_bPUmCDxzC46pTtFOe9p82uybTehZfULZtXXqRm85FL9n5zkrsTllPNAyEGhgb0RK9sE5nK1m_wNNysDyfLC4EFf1VXTrKm14XNVjc2vqLb7Mw\",\"e\":\"AQAB\"}";
 
     /**
      * token过期时间(分钟)
      */
-    public static final long expirationTime = 30;
+    public static final long expirationTime = 30 * 24 * 60;
+
+    private static RsaJsonWebKey rsaJsonWebKey;
 
-    public static String createToken(String username, String password, Integer roleId) {
+    private static IUserService userService;
+
+    @Resource
+    public void setUserService(IUserService userService) {
+        JwtUtils.userService = userService;
+    }
+
+    @Override
+    public void afterPropertiesSet() {
         try {
-            /**
+            rsaJsonWebKey = generateRsaJsonWebKey();
+        } catch (JoseException e) {
+            logger.error("生成RsaJsonWebKey报错。", e);
+        }
+    }
+
+    /**
+     * 创建密钥对
+     * @throws JoseException JoseException
+     */
+    private RsaJsonWebKey generateRsaJsonWebKey() throws JoseException {
+        // 生成一个RSA密钥对,该密钥对将用于JWT的签名和验证,包装在JWK中
+        RsaJsonWebKey rsaJsonWebKey = RsaJwkGenerator.generateJwk(2048);
+        // 给JWK一个密钥ID
+        rsaJsonWebKey.setKeyId(keyId);
+        return rsaJsonWebKey;
+    }
+
+    public static String createToken(String username) {
+        try {
+            /*
              * “iss” (issuer)  发行人
-             *
              * “sub” (subject)  主题
-             *
              * “aud” (audience) 接收方 用户
-             *
              * “exp” (expiration time) 到期时间
-             *
              * “nbf” (not before)  在此之前不可用
-             *
              * “iat” (issued at)  jwt的签发时间
              */
-            //Payload
             JwtClaims claims = new JwtClaims();
             claims.setGeneratedJwtId();
             claims.setIssuedAtToNow();
@@ -62,9 +88,7 @@ public class JwtUtils {
             claims.setSubject("login");
             claims.setAudience(AUDIENCE);
             //添加自定义参数,必须是字符串类型
-            claims.setClaim("username", username);
-            claims.setClaim("password", password);
-            claims.setClaim("roleId", roleId);
+            claims.setClaim("userName", username);
 
             //jws
             JsonWebSignature jws = new JsonWebSignature();
@@ -73,12 +97,10 @@ public class JwtUtils {
             jws.setKeyIdHeaderValue(keyId);
             jws.setPayload(claims.toJson());
 
-            PrivateKey privateKey = new RsaJsonWebKey(JsonUtil.parseJson(privateKeyStr)).getPrivateKey();
-            jws.setKey(privateKey);
+            jws.setKey(rsaJsonWebKey.getPrivateKey());
 
             //get token
-            String idToken = jws.getCompactSerialization();
-            return idToken;
+            return jws.getCompactSerialization();
         } catch (JoseException e) {
             logger.error("[Token生成失败]: {}", e.getMessage());
         }
@@ -90,7 +112,6 @@ public class JwtUtils {
         return HEADER;
     }
 
-
     public static JwtUser verifyToken(String token) {
 
         JwtUser jwtUser = new JwtUser();
@@ -103,7 +124,7 @@ public class JwtUtils {
                     .setRequireSubject()
                     //.setExpectedIssuer("")
                     .setExpectedAudience(AUDIENCE)
-                    .setVerificationKey(new RsaJsonWebKey(JsonUtil.parseJson(publicKeyStr)).getPublicKey())
+                    .setVerificationKey(rsaJsonWebKey.getPublicKey())
                     .build();
 
             JwtClaims claims = consumer.processToClaims(token);
@@ -113,26 +134,26 @@ public class JwtUtils {
             long timeRemaining = LocalDateTime.now().toEpochSecond(ZoneOffset.ofHours(8)) - expirationTime.getValue();
             if (timeRemaining < 5 * 60) {
                 jwtUser.setStatus(JwtUser.TokenStatus.EXPIRING_SOON);
-            }else {
+            } else {
                 jwtUser.setStatus(JwtUser.TokenStatus.NORMAL);
             }
 
-            String username = (String) claims.getClaimValue("username");
-            String password = (String) claims.getClaimValue("password");
-            Long roleId = (Long) claims.getClaimValue("roleId");
+            String username = (String) claims.getClaimValue("userName");
+            User user = userService.getUserByUsername(username);
+
             jwtUser.setUserName(username);
-            jwtUser.setPassword(password);
-            jwtUser.setRoleId(roleId.intValue());
+            jwtUser.setPassword(user.getPassword());
+            jwtUser.setRoleId(user.getRole().getId());
 
             return jwtUser;
         } catch (InvalidJwtException e) {
             if (e.hasErrorCode(ErrorCodes.EXPIRED)) {
                 jwtUser.setStatus(JwtUser.TokenStatus.EXPIRED);
-            }else {
+            } else {
                 jwtUser.setStatus(JwtUser.TokenStatus.EXCEPTION);
             }
             return jwtUser;
-        }catch (Exception e) {
+        } catch (Exception e) {
             logger.error("[Token解析失败]: {}", e.getMessage());
             jwtUser.setStatus(JwtUser.TokenStatus.EXPIRED);
             return jwtUser;

+ 141 - 141
src/main/java/com/genersoft/iot/vmp/gb28181/SipLayer.java

@@ -1,141 +1,141 @@
-package com.genersoft.iot.vmp.gb28181;
-
-import com.genersoft.iot.vmp.conf.SipConfig;
-import com.genersoft.iot.vmp.conf.UserSetting;
-import com.genersoft.iot.vmp.gb28181.bean.GbStringMsgParserFactory;
-import com.genersoft.iot.vmp.gb28181.conf.DefaultProperties;
-import com.genersoft.iot.vmp.gb28181.transmit.ISIPProcessorObserver;
-import gov.nist.javax.sip.SipProviderImpl;
-import gov.nist.javax.sip.SipStackImpl;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.boot.CommandLineRunner;
-import org.springframework.core.annotation.Order;
-import org.springframework.stereotype.Component;
-import org.springframework.util.ObjectUtils;
-
-import javax.sip.*;
-import java.util.*;
-import java.util.concurrent.ConcurrentHashMap;
-
-@Component
-@Order(value=10)
-public class SipLayer implements CommandLineRunner {
-
-	private final static Logger logger = LoggerFactory.getLogger(SipLayer.class);
-
-	@Autowired
-	private SipConfig sipConfig;
-
-	@Autowired
-	private ISIPProcessorObserver sipProcessorObserver;
-
-	@Autowired
-	private UserSetting userSetting;
-
-	private final Map<String, SipProviderImpl> tcpSipProviderMap = new ConcurrentHashMap<>();
-	private final Map<String, SipProviderImpl> udpSipProviderMap = new ConcurrentHashMap<>();
-
-	@Override
-	public void run(String... args) {
-		List<String> monitorIps = new ArrayList<>();
-		// 使用逗号分割多个ip
-		String separator = ",";
-		if (sipConfig.getIp().indexOf(separator) > 0) {
-			String[] split = sipConfig.getIp().split(separator);
-			monitorIps.addAll(Arrays.asList(split));
-		}else {
-			monitorIps.add(sipConfig.getIp());
-		}
-
-		SipFactory.getInstance().setPathName("gov.nist");
-		if (monitorIps.size() > 0) {
-			for (String monitorIp : monitorIps) {
-				addListeningPoint(monitorIp, sipConfig.getPort());
-			}
-			if (udpSipProviderMap.size() + tcpSipProviderMap.size() == 0) {
-				System.exit(1);
-			}
-		}
-	}
-
-	private void addListeningPoint(String monitorIp, int port){
-		SipStackImpl sipStack;
-		try {
-			sipStack = (SipStackImpl)SipFactory.getInstance().createSipStack(DefaultProperties.getProperties("GB28181_SIP", userSetting.getSipLog()));
-			sipStack.setMessageParserFactory(new GbStringMsgParserFactory());
-		} catch (PeerUnavailableException e) {
-			logger.error("[SIP SERVER] SIP服务启动失败, 监听地址{}失败,请检查ip是否正确", monitorIp);
-			return;
-		}
-
-		try {
-			ListeningPoint tcpListeningPoint = sipStack.createListeningPoint(monitorIp, port, "TCP");
-			SipProviderImpl tcpSipProvider = (SipProviderImpl)sipStack.createSipProvider(tcpListeningPoint);
-
-			tcpSipProvider.setDialogErrorsAutomaticallyHandled();
-			tcpSipProvider.addSipListener(sipProcessorObserver);
-			tcpSipProviderMap.put(monitorIp, tcpSipProvider);
-			logger.info("[SIP SERVER] tcp://{}:{} 启动成功", monitorIp, port);
-		} catch (TransportNotSupportedException
-				 | TooManyListenersException
-				 | ObjectInUseException
-				 | InvalidArgumentException e) {
-			logger.error("[SIP SERVER] tcp://{}:{} SIP服务启动失败,请检查端口是否被占用或者ip是否正确"
-					, monitorIp, port);
-		}
-
-		try {
-			ListeningPoint udpListeningPoint = sipStack.createListeningPoint(monitorIp, port, "UDP");
-
-			SipProviderImpl udpSipProvider = (SipProviderImpl)sipStack.createSipProvider(udpListeningPoint);
-			udpSipProvider.addSipListener(sipProcessorObserver);
-
-			udpSipProviderMap.put(monitorIp, udpSipProvider);
-
-			logger.info("[SIP SERVER] udp://{}:{} 启动成功", monitorIp, port);
-		} catch (TransportNotSupportedException
-				 | TooManyListenersException
-				 | ObjectInUseException
-				 | InvalidArgumentException e) {
-			logger.error("[SIP SERVER] udp://{}:{} SIP服务启动失败,请检查端口是否被占用或者ip是否正确"
-					, monitorIp, port);
-		}
-	}
-
-	public SipProviderImpl getUdpSipProvider(String ip) {
-		if (ObjectUtils.isEmpty(ip)) {
-			return null;
-		}
-		return udpSipProviderMap.get(ip);
-	}
-
-	public SipProviderImpl getUdpSipProvider() {
-		if (udpSipProviderMap.size() != 1) {
-			return null;
-		}
-		return udpSipProviderMap.values().stream().findFirst().get();
-	}
-
-	public SipProviderImpl getTcpSipProvider() {
-		if (tcpSipProviderMap.size() != 1) {
-			return null;
-		}
-		return tcpSipProviderMap.values().stream().findFirst().get();
-	}
-
-	public SipProviderImpl getTcpSipProvider(String ip) {
-		if (ObjectUtils.isEmpty(ip)) {
-			return null;
-		}
-		return tcpSipProviderMap.get(ip);
-	}
-
-	public String getLocalIp(String deviceLocalIp) {
-		if (!ObjectUtils.isEmpty(deviceLocalIp)) {
-			return deviceLocalIp;
-		}
-		return getUdpSipProvider().getListeningPoint().getIPAddress();
-	}
-}
+package com.genersoft.iot.vmp.gb28181;
+
+import com.genersoft.iot.vmp.conf.SipConfig;
+import com.genersoft.iot.vmp.conf.UserSetting;
+import com.genersoft.iot.vmp.gb28181.bean.GbStringMsgParserFactory;
+import com.genersoft.iot.vmp.gb28181.conf.DefaultProperties;
+import com.genersoft.iot.vmp.gb28181.transmit.ISIPProcessorObserver;
+import gov.nist.javax.sip.SipProviderImpl;
+import gov.nist.javax.sip.SipStackImpl;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.CommandLineRunner;
+import org.springframework.core.annotation.Order;
+import org.springframework.stereotype.Component;
+import org.springframework.util.ObjectUtils;
+
+import javax.sip.*;
+import java.util.*;
+import java.util.concurrent.ConcurrentHashMap;
+
+@Component
+@Order(value=10)
+public class SipLayer implements CommandLineRunner {
+
+	private final static Logger logger = LoggerFactory.getLogger(SipLayer.class);
+
+	@Autowired
+	private SipConfig sipConfig;
+
+	@Autowired
+	private ISIPProcessorObserver sipProcessorObserver;
+
+	@Autowired
+	private UserSetting userSetting;
+
+	private final Map<String, SipProviderImpl> tcpSipProviderMap = new ConcurrentHashMap<>();
+	private final Map<String, SipProviderImpl> udpSipProviderMap = new ConcurrentHashMap<>();
+
+	@Override
+	public void run(String... args) {
+		List<String> monitorIps = new ArrayList<>();
+		// 使用逗号分割多个ip
+		String separator = ",";
+		if (sipConfig.getIp().indexOf(separator) > 0) {
+			String[] split = sipConfig.getIp().split(separator);
+			monitorIps.addAll(Arrays.asList(split));
+		}else {
+			monitorIps.add(sipConfig.getIp());
+		}
+
+		SipFactory.getInstance().setPathName("gov.nist");
+		if (monitorIps.size() > 0) {
+			for (String monitorIp : monitorIps) {
+				addListeningPoint(monitorIp, sipConfig.getPort());
+			}
+			if (udpSipProviderMap.size() + tcpSipProviderMap.size() == 0) {
+				System.exit(1);
+			}
+		}
+	}
+
+	private void addListeningPoint(String monitorIp, int port){
+		SipStackImpl sipStack;
+		try {
+			sipStack = (SipStackImpl)SipFactory.getInstance().createSipStack(DefaultProperties.getProperties("GB28181_SIP", userSetting.getSipLog()));
+			sipStack.setMessageParserFactory(new GbStringMsgParserFactory());
+		} catch (PeerUnavailableException e) {
+			logger.error("[SIP SERVER] SIP服务启动失败, 监听地址{}失败,请检查ip是否正确", monitorIp);
+			return;
+		}
+
+		try {
+			ListeningPoint tcpListeningPoint = sipStack.createListeningPoint(monitorIp, port, "TCP");
+			SipProviderImpl tcpSipProvider = (SipProviderImpl)sipStack.createSipProvider(tcpListeningPoint);
+
+			tcpSipProvider.setDialogErrorsAutomaticallyHandled();
+			tcpSipProvider.addSipListener(sipProcessorObserver);
+			tcpSipProviderMap.put(monitorIp, tcpSipProvider);
+			logger.info("[SIP SERVER] tcp://{}:{} 启动成功", monitorIp, port);
+		} catch (TransportNotSupportedException
+				 | TooManyListenersException
+				 | ObjectInUseException
+				 | InvalidArgumentException e) {
+			logger.error("[SIP SERVER] tcp://{}:{} SIP服务启动失败,请检查端口是否被占用或者ip是否正确"
+					, monitorIp, port);
+		}
+
+		try {
+			ListeningPoint udpListeningPoint = sipStack.createListeningPoint(monitorIp, port, "UDP");
+
+			SipProviderImpl udpSipProvider = (SipProviderImpl)sipStack.createSipProvider(udpListeningPoint);
+			udpSipProvider.addSipListener(sipProcessorObserver);
+
+			udpSipProviderMap.put(monitorIp, udpSipProvider);
+
+			logger.info("[SIP SERVER] udp://{}:{} 启动成功", monitorIp, port);
+		} catch (TransportNotSupportedException
+				 | TooManyListenersException
+				 | ObjectInUseException
+				 | InvalidArgumentException e) {
+			logger.error("[SIP SERVER] udp://{}:{} SIP服务启动失败,请检查端口是否被占用或者ip是否正确"
+					, monitorIp, port);
+		}
+	}
+
+	public SipProviderImpl getUdpSipProvider(String ip) {
+		if (ObjectUtils.isEmpty(ip)) {
+			return null;
+		}
+		return udpSipProviderMap.get(ip);
+	}
+
+	public SipProviderImpl getUdpSipProvider() {
+		if (udpSipProviderMap.size() != 1) {
+			return null;
+		}
+		return udpSipProviderMap.values().stream().findFirst().get();
+	}
+
+	public SipProviderImpl getTcpSipProvider() {
+		if (tcpSipProviderMap.size() != 1) {
+			return null;
+		}
+		return tcpSipProviderMap.values().stream().findFirst().get();
+	}
+
+	public SipProviderImpl getTcpSipProvider(String ip) {
+		if (ObjectUtils.isEmpty(ip)) {
+			return null;
+		}
+		return tcpSipProviderMap.get(ip);
+	}
+
+	public String getLocalIp(String deviceLocalIp) {
+		if (!ObjectUtils.isEmpty(deviceLocalIp)) {
+			return deviceLocalIp;
+		}
+		return getUdpSipProvider().getListeningPoint().getIPAddress();
+	}
+}

+ 187 - 187
src/main/java/com/genersoft/iot/vmp/gb28181/bean/DeviceAlarm.java

@@ -1,187 +1,187 @@
-package com.genersoft.iot.vmp.gb28181.bean;
-
-import io.swagger.v3.oas.annotations.media.Schema;
-
-/**
- * @author lin
- */
-@Schema(description = "报警信息")
-public class DeviceAlarm {
-
-	/**
-	 * 数据库id
-	 */
-	@Schema(description = "数据库id")
-	private String id;
-
-	/**
-	 * 设备Id
-	 */
-	@Schema(description = "设备的国标编号")
-	private String deviceId;
-
-	/**
-	 * 通道Id
-	 */
-	@Schema(description = "通道的国标编号")
-	private String channelId;
-
-	/**
-	 * 报警级别, 1为一级警情, 2为二级警情, 3为三级警情, 4为四级警情
-	 */
-	@Schema(description = "报警级别, 1为一级警情, 2为二级警情, 3为三级警情, 4为四级警情")
-	private String alarmPriority;
-
-	/**
-	 * 报警方式 , 1为电话报警, 2为设备报警, 3为短信报警, 4为 GPS报警, 5为视频报警, 6为设备故障报警,
-	 * 7其他报警;可以为直接组合如12为电话报警或 设备报警-
-	 */
-	@Schema(description = "报警方式 , 1为电话报警, 2为设备报警, 3为短信报警, 4为 GPS报警, 5为视频报警, 6为设备故障报警,\n" +
-			"\t * 7其他报警;可以为直接组合如12为电话报警或设备报警")
-	private String alarmMethod;
-
-	/**
-	 * 报警时间
-	 */
-	@Schema(description = "报警时间")
-	private String alarmTime;
-
-	/**
-	 * 报警内容描述
-	 */
-	@Schema(description = "报警内容描述")
-	private String alarmDescription;
-
-	/**
-	 * 经度
-	 */
-	@Schema(description = "经度")
-	private double longitude;
-
-	/**
-	 * 纬度
-	 */
-	@Schema(description = "纬度")
-	private double latitude;
-
-	/**
-	 * 报警类型,
-	 * 报警方式为2时,不携带 AlarmType为默认的报警设备报警,
-	 * 携带 AlarmType取值及对应报警类型如下:
-	 * 		1-视频丢失报警;
-	 * 		2-设备防拆报警;
-	 * 		3-存储设备磁盘满报警;
-	 * 		4-设备高温报警;
-	 * 		5-设备低温报警。
-	 * 报警方式为5时,取值如下:
-	 * 		1-人工视频报警;
-	 * 		2-运动目标检测报警;
-	 * 		3-遗留物检测报警;
-	 * 		4-物体移除检测报警;
-	 * 		5-绊线检测报警;
-	 * 		6-入侵检测报警;
-	 * 		7-逆行检测报警;
-	 * 		8-徘徊检测报警;
-	 * 		9-流量统计报警;
-	 * 		10-密度检测报警;
-	 * 		11-视频异常检测报警;
-	 * 		12-快速移动报警。
-	 * 报警方式为6时,取值下:
-	 * 		1-存储设备磁盘故障报警;
-	 * 		2-存储设备风扇故障报警。
-	 */
-	@Schema(description = "报警类型")
-	private String alarmType;
-
-	@Schema(description = "创建时间")
-	private String createTime;
-
-
-	public String getId() {
-		return id;
-	}
-
-	public void setId(String id) {
-		this.id = id;
-	}
-
-	public String getDeviceId() {
-		return deviceId;
-	}
-
-	public void setDeviceId(String deviceId) {
-		this.deviceId = deviceId;
-	}
-
-	public String getAlarmPriority() {
-		return alarmPriority;
-	}
-
-	public void setAlarmPriority(String alarmPriority) {
-		this.alarmPriority = alarmPriority;
-	}
-
-	public String getAlarmMethod() {
-		return alarmMethod;
-	}
-
-	public void setAlarmMethod(String alarmMethod) {
-		this.alarmMethod = alarmMethod;
-	}
-
-	public String getAlarmTime() {
-		return alarmTime;
-	}
-
-	public void setAlarmTime(String alarmTime) {
-		this.alarmTime = alarmTime;
-	}
-
-	public String getAlarmDescription() {
-		return alarmDescription;
-	}
-
-	public void setAlarmDescription(String alarmDescription) {
-		this.alarmDescription = alarmDescription;
-	}
-
-	public double getLongitude() {
-		return longitude;
-	}
-
-	public void setLongitude(double longitude) {
-		this.longitude = longitude;
-	}
-
-	public double getLatitude() {
-		return latitude;
-	}
-
-	public void setLatitude(double latitude) {
-		this.latitude = latitude;
-	}
-
-	public String getAlarmType() {
-		return alarmType;
-	}
-
-	public void setAlarmType(String alarmType) {
-		this.alarmType = alarmType;
-	}
-
-	public String getChannelId() {
-		return channelId;
-	}
-
-	public void setChannelId(String channelId) {
-		this.channelId = channelId;
-	}
-
-	public String getCreateTime() {
-		return createTime;
-	}
-
-	public void setCreateTime(String createTime) {
-		this.createTime = createTime;
-	}
-}
+package com.genersoft.iot.vmp.gb28181.bean;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+
+/**
+ * @author lin
+ */
+@Schema(description = "报警信息")
+public class DeviceAlarm {
+
+	/**
+	 * 数据库id
+	 */
+	@Schema(description = "数据库id")
+	private String id;
+
+	/**
+	 * 设备Id
+	 */
+	@Schema(description = "设备的国标编号")
+	private String deviceId;
+
+	/**
+	 * 通道Id
+	 */
+	@Schema(description = "通道的国标编号")
+	private String channelId;
+
+	/**
+	 * 报警级别, 1为一级警情, 2为二级警情, 3为三级警情, 4为四级警情
+	 */
+	@Schema(description = "报警级别, 1为一级警情, 2为二级警情, 3为三级警情, 4为四级警情")
+	private String alarmPriority;
+
+	/**
+	 * 报警方式 , 1为电话报警, 2为设备报警, 3为短信报警, 4为 GPS报警, 5为视频报警, 6为设备故障报警,
+	 * 7其他报警;可以为直接组合如12为电话报警或 设备报警-
+	 */
+	@Schema(description = "报警方式 , 1为电话报警, 2为设备报警, 3为短信报警, 4为 GPS报警, 5为视频报警, 6为设备故障报警,\n" +
+			"\t * 7其他报警;可以为直接组合如12为电话报警或设备报警")
+	private String alarmMethod;
+
+	/**
+	 * 报警时间
+	 */
+	@Schema(description = "报警时间")
+	private String alarmTime;
+
+	/**
+	 * 报警内容描述
+	 */
+	@Schema(description = "报警内容描述")
+	private String alarmDescription;
+
+	/**
+	 * 经度
+	 */
+	@Schema(description = "经度")
+	private double longitude;
+
+	/**
+	 * 纬度
+	 */
+	@Schema(description = "纬度")
+	private double latitude;
+
+	/**
+	 * 报警类型,
+	 * 报警方式为2时,不携带 AlarmType为默认的报警设备报警,
+	 * 携带 AlarmType取值及对应报警类型如下:
+	 * 		1-视频丢失报警;
+	 * 		2-设备防拆报警;
+	 * 		3-存储设备磁盘满报警;
+	 * 		4-设备高温报警;
+	 * 		5-设备低温报警。
+	 * 报警方式为5时,取值如下:
+	 * 		1-人工视频报警;
+	 * 		2-运动目标检测报警;
+	 * 		3-遗留物检测报警;
+	 * 		4-物体移除检测报警;
+	 * 		5-绊线检测报警;
+	 * 		6-入侵检测报警;
+	 * 		7-逆行检测报警;
+	 * 		8-徘徊检测报警;
+	 * 		9-流量统计报警;
+	 * 		10-密度检测报警;
+	 * 		11-视频异常检测报警;
+	 * 		12-快速移动报警。
+	 * 报警方式为6时,取值下:
+	 * 		1-存储设备磁盘故障报警;
+	 * 		2-存储设备风扇故障报警。
+	 */
+	@Schema(description = "报警类型")
+	private String alarmType;
+
+	@Schema(description = "创建时间")
+	private String createTime;
+
+
+	public String getId() {
+		return id;
+	}
+
+	public void setId(String id) {
+		this.id = id;
+	}
+
+	public String getDeviceId() {
+		return deviceId;
+	}
+
+	public void setDeviceId(String deviceId) {
+		this.deviceId = deviceId;
+	}
+
+	public String getAlarmPriority() {
+		return alarmPriority;
+	}
+
+	public void setAlarmPriority(String alarmPriority) {
+		this.alarmPriority = alarmPriority;
+	}
+
+	public String getAlarmMethod() {
+		return alarmMethod;
+	}
+
+	public void setAlarmMethod(String alarmMethod) {
+		this.alarmMethod = alarmMethod;
+	}
+
+	public String getAlarmTime() {
+		return alarmTime;
+	}
+
+	public void setAlarmTime(String alarmTime) {
+		this.alarmTime = alarmTime;
+	}
+
+	public String getAlarmDescription() {
+		return alarmDescription;
+	}
+
+	public void setAlarmDescription(String alarmDescription) {
+		this.alarmDescription = alarmDescription;
+	}
+
+	public double getLongitude() {
+		return longitude;
+	}
+
+	public void setLongitude(double longitude) {
+		this.longitude = longitude;
+	}
+
+	public double getLatitude() {
+		return latitude;
+	}
+
+	public void setLatitude(double latitude) {
+		this.latitude = latitude;
+	}
+
+	public String getAlarmType() {
+		return alarmType;
+	}
+
+	public void setAlarmType(String alarmType) {
+		this.alarmType = alarmType;
+	}
+
+	public String getChannelId() {
+		return channelId;
+	}
+
+	public void setChannelId(String channelId) {
+		this.channelId = channelId;
+	}
+
+	public String getCreateTime() {
+		return createTime;
+	}
+
+	public void setCreateTime(String createTime) {
+		this.createTime = createTime;
+	}
+}

+ 102 - 102
src/main/java/com/genersoft/iot/vmp/gb28181/bean/GBStringMsgParser.java

@@ -1,102 +1,102 @@
-package com.genersoft.iot.vmp.gb28181.bean;
-
-
-import io.swagger.v3.oas.annotations.media.Schema;
-
-import java.time.Instant;
-import java.util.List;
-
-/**    
- * @description:设备录像信息bean 
- * @author: swwheihei
- * @date:   2020年5月8日 下午2:05:56     
- */
-@Schema(description = "设备录像查询结果信息")
-public class RecordInfo {
-
-	@Schema(description = "设备编号")
-	private String deviceId;
-
-	@Schema(description = "通道编号")
-	private String channelId;
-
-	@Schema(description = "命令序列号")
-	private String sn;
-
-	@Schema(description = "设备名称")
-	private String name;
-
-	@Schema(description = "列表总数")
-	private int sumNum;
-
-	private int count;
-
-	private Instant lastTime;
-
-	@Schema(description = "")
-	private List<RecordItem> recordList;
-
-	public String getDeviceId() {
-		return deviceId;
-	}
-
-	public void setDeviceId(String deviceId) {
-		this.deviceId = deviceId;
-	}
-
-	public String getName() {
-		return name;
-	}
-
-	public void setName(String name) {
-		this.name = name;
-	}
-
-	public int getSumNum() {
-		return sumNum;
-	}
-
-	public void setSumNum(int sumNum) {
-		this.sumNum = sumNum;
-	}
-
-	public List<RecordItem> getRecordList() {
-		return recordList;
-	}
-
-	public void setRecordList(List<RecordItem> recordList) {
-		this.recordList = recordList;
-	}
-
-	public String getChannelId() {
-		return channelId;
-	}
-
-	public void setChannelId(String channelId) {
-		this.channelId = channelId;
-	}
-
-	public String getSn() {
-		return sn;
-	}
-
-	public void setSn(String sn) {
-		this.sn = sn;
-	}
-
-	public Instant getLastTime() {
-		return lastTime;
-	}
-
-	public void setLastTime(Instant lastTime) {
-		this.lastTime = lastTime;
-	}
-
-	public int getCount() {
-		return count;
-	}
-
-	public void setCount(int count) {
-		this.count = count;
-	}
-}
+package com.genersoft.iot.vmp.gb28181.bean;
+
+
+import io.swagger.v3.oas.annotations.media.Schema;
+
+import java.time.Instant;
+import java.util.List;
+
+/**    
+ * @description:设备录像信息bean 
+ * @author: swwheihei
+ * @date:   2020年5月8日 下午2:05:56     
+ */
+@Schema(description = "设备录像查询结果信息")
+public class RecordInfo {
+
+	@Schema(description = "设备编号")
+	private String deviceId;
+
+	@Schema(description = "通道编号")
+	private String channelId;
+
+	@Schema(description = "命令序列号")
+	private String sn;
+
+	@Schema(description = "设备名称")
+	private String name;
+
+	@Schema(description = "列表总数")
+	private int sumNum;
+
+	private int count;
+
+	private Instant lastTime;
+
+	@Schema(description = "")
+	private List<RecordItem> recordList;
+
+	public String getDeviceId() {
+		return deviceId;
+	}
+
+	public void setDeviceId(String deviceId) {
+		this.deviceId = deviceId;
+	}
+
+	public String getName() {
+		return name;
+	}
+
+	public void setName(String name) {
+		this.name = name;
+	}
+
+	public int getSumNum() {
+		return sumNum;
+	}
+
+	public void setSumNum(int sumNum) {
+		this.sumNum = sumNum;
+	}
+
+	public List<RecordItem> getRecordList() {
+		return recordList;
+	}
+
+	public void setRecordList(List<RecordItem> recordList) {
+		this.recordList = recordList;
+	}
+
+	public String getChannelId() {
+		return channelId;
+	}
+
+	public void setChannelId(String channelId) {
+		this.channelId = channelId;
+	}
+
+	public String getSn() {
+		return sn;
+	}
+
+	public void setSn(String sn) {
+		this.sn = sn;
+	}
+
+	public Instant getLastTime() {
+		return lastTime;
+	}
+
+	public void setLastTime(Instant lastTime) {
+		this.lastTime = lastTime;
+	}
+
+	public int getCount() {
+		return count;
+	}
+
+	public void setCount(int count) {
+		this.count = count;
+	}
+}

+ 144 - 144
src/main/java/com/genersoft/iot/vmp/gb28181/bean/RecordItem.java

@@ -1,144 +1,144 @@
-package com.genersoft.iot.vmp.gb28181.bean;
-
-
-import com.genersoft.iot.vmp.utils.DateUtil;
-import io.swagger.v3.oas.annotations.media.Schema;
-import org.jetbrains.annotations.NotNull;
-
-import java.time.Instant;
-import java.time.temporal.TemporalAccessor;
-
-/**
- * @description:设备录像bean 
- * @author: swwheihei
- * @date:   2020年5月8日 下午2:06:54     
- */
-@Schema(description = "设备录像详情")
-public class RecordItem  implements Comparable<RecordItem>{
-
-	@Schema(description = "设备编号")
-	private String deviceId;
-
-	@Schema(description = "名称")
-	private String name;
-
-	@Schema(description = "文件路径名 (可选)")
-	private String filePath;
-
-	@Schema(description = "录像文件大小,单位:Byte(可选)")
-	private String fileSize;
-
-	@Schema(description = "录像地址(可选)")
-	private String address;
-
-	@Schema(description = "录像开始时间(可选)")
-	private String startTime;
-
-	@Schema(description = "录像结束时间(可选)")
-	private String endTime;
-
-	@Schema(description = "保密属性(必选)缺省为0;0:不涉密,1:涉密")
-	private int secrecy;
-
-	@Schema(description = "录像产生类型(可选)time或alarm 或 manua")
-	private String type;
-
-	@Schema(description = "录像触发者ID(可选)")
-	private String recorderId;
-
-	public String getDeviceId() {
-		return deviceId;
-	}
-
-	public void setDeviceId(String deviceId) {
-		this.deviceId = deviceId;
-	}
-
-	public String getName() {
-		return name;
-	}
-
-	public void setName(String name) {
-		this.name = name;
-	}
-
-	public String getFilePath() {
-		return filePath;
-	}
-
-	public void setFilePath(String filePath) {
-		this.filePath = filePath;
-	}
-
-	public String getAddress() {
-		return address;
-	}
-
-	public void setAddress(String address) {
-		this.address = address;
-	}
-
-	public String getStartTime() {
-		return startTime;
-	}
-
-	public void setStartTime(String startTime) {
-		this.startTime = startTime;
-	}
-
-	public String getEndTime() {
-		return endTime;
-	}
-
-	public void setEndTime(String endTime) {
-		this.endTime = endTime;
-	}
-
-	public int getSecrecy() {
-		return secrecy;
-	}
-
-	public void setSecrecy(int secrecy) {
-		this.secrecy = secrecy;
-	}
-
-	public String getType() {
-		return type;
-	}
-
-	public void setType(String type) {
-		this.type = type;
-	}
-
-	public String getRecorderId() {
-		return recorderId;
-	}
-
-	public void setRecorderId(String recorderId) {
-		this.recorderId = recorderId;
-	}
-
-	public String getFileSize() {
-		return fileSize;
-	}
-
-	public void setFileSize(String fileSize) {
-		this.fileSize = fileSize;
-	}
-
-	@Override
-	public int compareTo(@NotNull RecordItem recordItem) {
-		TemporalAccessor startTimeNow = DateUtil.formatter.parse(startTime);
-		TemporalAccessor startTimeParam = DateUtil.formatter.parse(recordItem.getStartTime());
-		Instant startTimeParamInstant = Instant.from(startTimeParam);
-		Instant startTimeNowInstant = Instant.from(startTimeNow);
-		if (startTimeNowInstant.equals(startTimeParamInstant)) {
-			return 0;
-		}else if (Instant.from(startTimeParam).isAfter(Instant.from(startTimeNow)) ) {
-			return -1;
-		}else {
-			return 1;
-		}
-
-	}
-}
+package com.genersoft.iot.vmp.gb28181.bean;
+
+
+import com.genersoft.iot.vmp.utils.DateUtil;
+import io.swagger.v3.oas.annotations.media.Schema;
+import org.jetbrains.annotations.NotNull;
+
+import java.time.Instant;
+import java.time.temporal.TemporalAccessor;
+
+/**
+ * @description:设备录像bean 
+ * @author: swwheihei
+ * @date:   2020年5月8日 下午2:06:54     
+ */
+@Schema(description = "设备录像详情")
+public class RecordItem  implements Comparable<RecordItem>{
+
+	@Schema(description = "设备编号")
+	private String deviceId;
+
+	@Schema(description = "名称")
+	private String name;
+
+	@Schema(description = "文件路径名 (可选)")
+	private String filePath;
+
+	@Schema(description = "录像文件大小,单位:Byte(可选)")
+	private String fileSize;
+
+	@Schema(description = "录像地址(可选)")
+	private String address;
+
+	@Schema(description = "录像开始时间(可选)")
+	private String startTime;
+
+	@Schema(description = "录像结束时间(可选)")
+	private String endTime;
+
+	@Schema(description = "保密属性(必选)缺省为0;0:不涉密,1:涉密")
+	private int secrecy;
+
+	@Schema(description = "录像产生类型(可选)time或alarm 或 manua")
+	private String type;
+
+	@Schema(description = "录像触发者ID(可选)")
+	private String recorderId;
+
+	public String getDeviceId() {
+		return deviceId;
+	}
+
+	public void setDeviceId(String deviceId) {
+		this.deviceId = deviceId;
+	}
+
+	public String getName() {
+		return name;
+	}
+
+	public void setName(String name) {
+		this.name = name;
+	}
+
+	public String getFilePath() {
+		return filePath;
+	}
+
+	public void setFilePath(String filePath) {
+		this.filePath = filePath;
+	}
+
+	public String getAddress() {
+		return address;
+	}
+
+	public void setAddress(String address) {
+		this.address = address;
+	}
+
+	public String getStartTime() {
+		return startTime;
+	}
+
+	public void setStartTime(String startTime) {
+		this.startTime = startTime;
+	}
+
+	public String getEndTime() {
+		return endTime;
+	}
+
+	public void setEndTime(String endTime) {
+		this.endTime = endTime;
+	}
+
+	public int getSecrecy() {
+		return secrecy;
+	}
+
+	public void setSecrecy(int secrecy) {
+		this.secrecy = secrecy;
+	}
+
+	public String getType() {
+		return type;
+	}
+
+	public void setType(String type) {
+		this.type = type;
+	}
+
+	public String getRecorderId() {
+		return recorderId;
+	}
+
+	public void setRecorderId(String recorderId) {
+		this.recorderId = recorderId;
+	}
+
+	public String getFileSize() {
+		return fileSize;
+	}
+
+	public void setFileSize(String fileSize) {
+		this.fileSize = fileSize;
+	}
+
+	@Override
+	public int compareTo(@NotNull RecordItem recordItem) {
+		TemporalAccessor startTimeNow = DateUtil.formatter.parse(startTime);
+		TemporalAccessor startTimeParam = DateUtil.formatter.parse(recordItem.getStartTime());
+		Instant startTimeParamInstant = Instant.from(startTimeParam);
+		Instant startTimeNowInstant = Instant.from(startTimeNow);
+		if (startTimeNowInstant.equals(startTimeParamInstant)) {
+			return 0;
+		}else if (Instant.from(startTimeParam).isAfter(Instant.from(startTimeNow)) ) {
+			return -1;
+		}else {
+			return 1;
+		}
+
+	}
+}

+ 6 - 6
src/main/java/com/genersoft/iot/vmp/gb28181/bean/RemoteAddressInfo.java

@@ -2,12 +2,9 @@ package com.genersoft.iot.vmp.gb28181.bean;
 
 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.gb28181.task.ISubscribeTask;
 import com.genersoft.iot.vmp.gb28181.task.impl.MobilePositionSubscribeHandlerTask;
-import com.genersoft.iot.vmp.gb28181.transmit.cmd.ISIPCommanderForPlatform;
-import com.genersoft.iot.vmp.service.IPlatformService;
-import com.genersoft.iot.vmp.storager.IRedisCatchStorage;
-import com.genersoft.iot.vmp.storager.IVideoManagerStorage;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Component;
 
@@ -24,6 +21,9 @@ public class SubscribeHolder {
     @Autowired
     private DynamicTask dynamicTask;
 
+    @Autowired
+    private UserSetting userSetting;
+
     private final String taskOverduePrefix = "subscribe_overdue_";
 
     private static ConcurrentHashMap<String, SubscribeInfo> catalogMap = new ConcurrentHashMap<>();
@@ -58,7 +58,7 @@ public class SubscribeHolder {
 
     public void putMobilePositionSubscribe(String platformId, SubscribeInfo subscribeInfo) {
         mobilePositionMap.put(platformId, subscribeInfo);
-        String key = VideoManagerConstants.SIP_SUBSCRIBE_PREFIX +  "MobilePosition_" + platformId;
+        String key = VideoManagerConstants.SIP_SUBSCRIBE_PREFIX + userSetting.getServerId() + "MobilePosition_" + platformId;
         // 添加任务处理GPS定时推送
         dynamicTask.startCron(key, new MobilePositionSubscribeHandlerTask(platformId),
                 subscribeInfo.getGpsInterval() * 1000);
@@ -76,7 +76,7 @@ public class SubscribeHolder {
 
     public void removeMobilePositionSubscribe(String platformId) {
         mobilePositionMap.remove(platformId);
-        String key = VideoManagerConstants.SIP_SUBSCRIBE_PREFIX +  "MobilePosition_" + platformId;
+        String key = VideoManagerConstants.SIP_SUBSCRIBE_PREFIX + userSetting.getServerId() + "MobilePosition_" + platformId;
         // 结束任务处理GPS定时推送
         dynamicTask.stop(key);
         String taskOverdueKey = taskOverduePrefix +  "MobilePosition_" + platformId;

+ 118 - 118
src/main/java/com/genersoft/iot/vmp/gb28181/bean/SubscribeInfo.java

@@ -1,118 +1,118 @@
-package com.genersoft.iot.vmp.gb28181.event;
-
-import com.genersoft.iot.vmp.gb28181.bean.*;
-import com.genersoft.iot.vmp.gb28181.event.device.RequestTimeoutEvent;
-import com.genersoft.iot.vmp.gb28181.event.record.RecordEndEvent;
-import com.genersoft.iot.vmp.gb28181.event.subscribe.catalog.CatalogEvent;
-import com.genersoft.iot.vmp.media.zlm.event.ZLMOfflineEvent;
-import com.genersoft.iot.vmp.media.zlm.event.ZLMOnlineEvent;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.context.ApplicationEventPublisher;
-import org.springframework.stereotype.Component;
-
-import com.genersoft.iot.vmp.gb28181.event.alarm.AlarmEvent;
-
-import javax.sip.TimeoutEvent;
-import java.util.ArrayList;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Set;
-
-/**    
- * @description:Event事件通知推送器,支持推送在线事件、离线事件
- * @author: swwheihei
- * @date:   2020年5月6日 上午11:30:50     
- */
-@Component
-public class EventPublisher {
-
-	@Autowired
-    private ApplicationEventPublisher applicationEventPublisher;
-	
-	/**
-	 * 设备报警事件
-	 * @param deviceAlarm
-	 */
-	public void deviceAlarmEventPublish(DeviceAlarm deviceAlarm) {
-		AlarmEvent alarmEvent = new AlarmEvent(this);
-		alarmEvent.setAlarmInfo(deviceAlarm);
-		applicationEventPublisher.publishEvent(alarmEvent);
-	}
-
-	public void zlmOfflineEventPublish(String mediaServerId){
-		ZLMOfflineEvent outEvent = new ZLMOfflineEvent(this);
-		outEvent.setMediaServerId(mediaServerId);
-		applicationEventPublisher.publishEvent(outEvent);
-	}
-
-	public void zlmOnlineEventPublish(String mediaServerId) {
-		ZLMOnlineEvent outEvent = new ZLMOnlineEvent(this);
-		outEvent.setMediaServerId(mediaServerId);
-		applicationEventPublisher.publishEvent(outEvent);
-	}
-
-
-	public void catalogEventPublish(String platformId, DeviceChannel deviceChannel, String type) {
-		List<DeviceChannel> deviceChannelList = new ArrayList<>();
-		deviceChannelList.add(deviceChannel);
-		catalogEventPublish(platformId, deviceChannelList, type);
-	}
-
-
-	public void requestTimeOut(TimeoutEvent timeoutEvent) {
-		RequestTimeoutEvent requestTimeoutEvent = new RequestTimeoutEvent(this);
-		requestTimeoutEvent.setTimeoutEvent(timeoutEvent);
-		applicationEventPublisher.publishEvent(requestTimeoutEvent);
-	}
-
-
-	/**
-	 *
-	 * @param platformId
-	 * @param deviceChannels
-	 * @param type
-	 */
-	public void catalogEventPublish(String platformId, List<DeviceChannel> deviceChannels, String type) {
-		CatalogEvent outEvent = new CatalogEvent(this);
-		List<DeviceChannel> channels = new ArrayList<>();
-		if (deviceChannels.size() > 1) {
-			// 数据去重
-			Set<String> gbIdSet = new HashSet<>();
-			for (DeviceChannel deviceChannel : deviceChannels) {
-				if (!gbIdSet.contains(deviceChannel.getChannelId())) {
-					gbIdSet.add(deviceChannel.getChannelId());
-					channels.add(deviceChannel);
-				}
-			}
-		}else {
-			channels = deviceChannels;
-		}
-		outEvent.setDeviceChannels(channels);
-		outEvent.setType(type);
-		outEvent.setPlatformId(platformId);
-		applicationEventPublisher.publishEvent(outEvent);
-	}
-
-
-	public void catalogEventPublishForStream(String platformId, List<GbStream> gbStreams, String type) {
-		CatalogEvent outEvent = new CatalogEvent(this);
-		outEvent.setGbStreams(gbStreams);
-		outEvent.setType(type);
-		outEvent.setPlatformId(platformId);
-		applicationEventPublisher.publishEvent(outEvent);
-	}
-
-
-	public void catalogEventPublishForStream(String platformId, GbStream gbStream, String type) {
-		List<GbStream> gbStreamList = new ArrayList<>();
-		gbStreamList.add(gbStream);
-		catalogEventPublishForStream(platformId, gbStreamList, type);
-	}
-
-	public void recordEndEventPush(RecordInfo recordInfo) {
-		RecordEndEvent outEvent = new RecordEndEvent(this);
-		outEvent.setRecordInfo(recordInfo);
-		applicationEventPublisher.publishEvent(outEvent);
-	}
-
-}
+package com.genersoft.iot.vmp.gb28181.event;
+
+import com.genersoft.iot.vmp.gb28181.bean.*;
+import com.genersoft.iot.vmp.gb28181.event.device.RequestTimeoutEvent;
+import com.genersoft.iot.vmp.gb28181.event.record.RecordEndEvent;
+import com.genersoft.iot.vmp.gb28181.event.subscribe.catalog.CatalogEvent;
+import com.genersoft.iot.vmp.media.zlm.event.ZLMOfflineEvent;
+import com.genersoft.iot.vmp.media.zlm.event.ZLMOnlineEvent;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.ApplicationEventPublisher;
+import org.springframework.stereotype.Component;
+
+import com.genersoft.iot.vmp.gb28181.event.alarm.AlarmEvent;
+
+import javax.sip.TimeoutEvent;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+/**    
+ * @description:Event事件通知推送器,支持推送在线事件、离线事件
+ * @author: swwheihei
+ * @date:   2020年5月6日 上午11:30:50     
+ */
+@Component
+public class EventPublisher {
+
+	@Autowired
+    private ApplicationEventPublisher applicationEventPublisher;
+	
+	/**
+	 * 设备报警事件
+	 * @param deviceAlarm
+	 */
+	public void deviceAlarmEventPublish(DeviceAlarm deviceAlarm) {
+		AlarmEvent alarmEvent = new AlarmEvent(this);
+		alarmEvent.setAlarmInfo(deviceAlarm);
+		applicationEventPublisher.publishEvent(alarmEvent);
+	}
+
+	public void zlmOfflineEventPublish(String mediaServerId){
+		ZLMOfflineEvent outEvent = new ZLMOfflineEvent(this);
+		outEvent.setMediaServerId(mediaServerId);
+		applicationEventPublisher.publishEvent(outEvent);
+	}
+
+	public void zlmOnlineEventPublish(String mediaServerId) {
+		ZLMOnlineEvent outEvent = new ZLMOnlineEvent(this);
+		outEvent.setMediaServerId(mediaServerId);
+		applicationEventPublisher.publishEvent(outEvent);
+	}
+
+
+	public void catalogEventPublish(String platformId, DeviceChannel deviceChannel, String type) {
+		List<DeviceChannel> deviceChannelList = new ArrayList<>();
+		deviceChannelList.add(deviceChannel);
+		catalogEventPublish(platformId, deviceChannelList, type);
+	}
+
+
+	public void requestTimeOut(TimeoutEvent timeoutEvent) {
+		RequestTimeoutEvent requestTimeoutEvent = new RequestTimeoutEvent(this);
+		requestTimeoutEvent.setTimeoutEvent(timeoutEvent);
+		applicationEventPublisher.publishEvent(requestTimeoutEvent);
+	}
+
+
+	/**
+	 *
+	 * @param platformId
+	 * @param deviceChannels
+	 * @param type
+	 */
+	public void catalogEventPublish(String platformId, List<DeviceChannel> deviceChannels, String type) {
+		CatalogEvent outEvent = new CatalogEvent(this);
+		List<DeviceChannel> channels = new ArrayList<>();
+		if (deviceChannels.size() > 1) {
+			// 数据去重
+			Set<String> gbIdSet = new HashSet<>();
+			for (DeviceChannel deviceChannel : deviceChannels) {
+				if (!gbIdSet.contains(deviceChannel.getChannelId())) {
+					gbIdSet.add(deviceChannel.getChannelId());
+					channels.add(deviceChannel);
+				}
+			}
+		}else {
+			channels = deviceChannels;
+		}
+		outEvent.setDeviceChannels(channels);
+		outEvent.setType(type);
+		outEvent.setPlatformId(platformId);
+		applicationEventPublisher.publishEvent(outEvent);
+	}
+
+
+	public void catalogEventPublishForStream(String platformId, List<GbStream> gbStreams, String type) {
+		CatalogEvent outEvent = new CatalogEvent(this);
+		outEvent.setGbStreams(gbStreams);
+		outEvent.setType(type);
+		outEvent.setPlatformId(platformId);
+		applicationEventPublisher.publishEvent(outEvent);
+	}
+
+
+	public void catalogEventPublishForStream(String platformId, GbStream gbStream, String type) {
+		List<GbStream> gbStreamList = new ArrayList<>();
+		gbStreamList.add(gbStream);
+		catalogEventPublishForStream(platformId, gbStreamList, type);
+	}
+
+	public void recordEndEventPush(RecordInfo recordInfo) {
+		RecordEndEvent outEvent = new RecordEndEvent(this);
+		outEvent.setRecordInfo(recordInfo);
+		applicationEventPublisher.publishEvent(outEvent);
+	}
+
+}

+ 139 - 139
src/main/java/com/genersoft/iot/vmp/gb28181/event/SipSubscribe.java

@@ -1,139 +1,139 @@
-package com.genersoft.iot.vmp.gb28181.session;
-
-import com.genersoft.iot.vmp.common.InviteSessionType;
-import com.genersoft.iot.vmp.common.VideoManagerConstants;
-import com.genersoft.iot.vmp.conf.UserSetting;
-import com.genersoft.iot.vmp.gb28181.bean.SipTransactionInfo;
-import com.genersoft.iot.vmp.gb28181.bean.SsrcTransaction;
-import com.genersoft.iot.vmp.utils.JsonUtil;
-import com.genersoft.iot.vmp.utils.redis.RedisUtil;
-import gov.nist.javax.sip.message.SIPResponse;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.data.redis.core.RedisTemplate;
-import org.springframework.stereotype.Component;
-import org.springframework.util.ObjectUtils;
-
-import java.util.ArrayList;
-import java.util.List;
-
-/**
- * 视频流session管理器,管理视频预览、预览回放的通信句柄
- */
-@Component
-public class VideoStreamSessionManager {
-
-	@Autowired
-	private UserSetting userSetting;
-
-	@Autowired
-	private RedisTemplate<Object, Object> redisTemplate;
-
-	/**
-	 * 添加一个点播/回放的事务信息
-	 * 后续可以通过流Id/callID
-	 * @param deviceId 设备ID
-	 * @param channelId 通道ID
-	 * @param callId 一次请求的CallID
-	 * @param stream 流名称
-	 * @param mediaServerId 所使用的流媒体ID
-	 * @param response 回复
-	 */
-	public void put(String deviceId, String channelId, String callId, String stream, String ssrc, String mediaServerId, SIPResponse response, InviteSessionType type){
-		SsrcTransaction ssrcTransaction = new SsrcTransaction();
-		ssrcTransaction.setDeviceId(deviceId);
-		ssrcTransaction.setChannelId(channelId);
-		ssrcTransaction.setStream(stream);
-		ssrcTransaction.setSipTransactionInfo(new SipTransactionInfo(response));
-		ssrcTransaction.setCallId(callId);
-		ssrcTransaction.setSsrc(ssrc);
-		ssrcTransaction.setMediaServerId(mediaServerId);
-		ssrcTransaction.setType(type);
-
-		redisTemplate.opsForValue().set(VideoManagerConstants.MEDIA_TRANSACTION_USED_PREFIX + userSetting.getServerId()
-				+ "_" +  deviceId + "_" + channelId + "_" + callId + "_" + stream, ssrcTransaction);
-	}
-
-	public SsrcTransaction getSsrcTransaction(String deviceId, String channelId, String callId, String stream){
-
-		if (ObjectUtils.isEmpty(deviceId)) {
-			deviceId ="*";
-		}
-		if (ObjectUtils.isEmpty(channelId)) {
-			channelId ="*";
-		}
-		if (ObjectUtils.isEmpty(callId)) {
-			callId ="*";
-		}
-		if (ObjectUtils.isEmpty(stream)) {
-			stream ="*";
-		}
-		String key = VideoManagerConstants.MEDIA_TRANSACTION_USED_PREFIX + userSetting.getServerId() + "_" + deviceId + "_" + channelId + "_" + callId+ "_" + stream;
-		List<Object> scanResult = RedisUtil.scan(redisTemplate, key);
-		if (scanResult.size() == 0) {
-			return null;
-		}
-		return (SsrcTransaction)redisTemplate.opsForValue().get(scanResult.get(0));
-	}
-
-	public List<SsrcTransaction> getSsrcTransactionForAll(String deviceId, String channelId, String callId, String stream){
-		if (ObjectUtils.isEmpty(deviceId)) {
-			deviceId ="*";
-		}
-		if (ObjectUtils.isEmpty(channelId)) {
-			channelId ="*";
-		}
-		if (ObjectUtils.isEmpty(callId)) {
-			callId ="*";
-		}
-		if (ObjectUtils.isEmpty(stream)) {
-			stream ="*";
-		}
-		String key = VideoManagerConstants.MEDIA_TRANSACTION_USED_PREFIX + userSetting.getServerId() + "_" + deviceId + "_" + channelId + "_" + callId+ "_" + stream;
-		List<Object> scanResult = RedisUtil.scan(redisTemplate, key);
-		if (scanResult.size() == 0) {
-			return null;
-		}
-		List<SsrcTransaction> result = new ArrayList<>();
-		for (Object keyObj : scanResult) {
-			result.add((SsrcTransaction)redisTemplate.opsForValue().get(keyObj));
-		}
-		return result;
-	}
-
-	public String getMediaServerId(String deviceId, String channelId, String stream){
-		SsrcTransaction ssrcTransaction = getSsrcTransaction(deviceId, channelId, null, stream);
-		if (ssrcTransaction == null) {
-			return null;
-		}
-		return ssrcTransaction.getMediaServerId();
-	}
-
-	public String getSSRC(String deviceId, String channelId, String stream){
-		SsrcTransaction ssrcTransaction = getSsrcTransaction(deviceId, channelId, null, stream);
-		if (ssrcTransaction == null) {
-			return null;
-		}
-		return ssrcTransaction.getSsrc();
-	}
-	
-	public void remove(String deviceId, String channelId, String stream) {
-		SsrcTransaction ssrcTransaction = getSsrcTransaction(deviceId, channelId, null, stream);
-		if (ssrcTransaction == null) {
-			return;
-		}
-		redisTemplate.delete(VideoManagerConstants.MEDIA_TRANSACTION_USED_PREFIX + userSetting.getServerId() + "_"
-				+  deviceId + "_" + channelId + "_" + ssrcTransaction.getCallId() + "_" + ssrcTransaction.getStream());
-	}
-
-
-	public List<SsrcTransaction> getAllSsrc() {
-		List<Object> ssrcTransactionKeys = RedisUtil.scan(redisTemplate, String.format("%s_*_*_*_*", VideoManagerConstants.MEDIA_TRANSACTION_USED_PREFIX+ userSetting.getServerId()));
-		List<SsrcTransaction> result= new ArrayList<>();
-		for (Object ssrcTransactionKey : ssrcTransactionKeys) {
-			String key = (String) ssrcTransactionKey;
-			SsrcTransaction ssrcTransaction = JsonUtil.redisJsonToObject(redisTemplate, key, SsrcTransaction.class);
-			result.add(ssrcTransaction);
-		}
-		return result;
-	}
-}
+package com.genersoft.iot.vmp.gb28181.session;
+
+import com.genersoft.iot.vmp.common.InviteSessionType;
+import com.genersoft.iot.vmp.common.VideoManagerConstants;
+import com.genersoft.iot.vmp.conf.UserSetting;
+import com.genersoft.iot.vmp.gb28181.bean.SipTransactionInfo;
+import com.genersoft.iot.vmp.gb28181.bean.SsrcTransaction;
+import com.genersoft.iot.vmp.utils.JsonUtil;
+import com.genersoft.iot.vmp.utils.redis.RedisUtil;
+import gov.nist.javax.sip.message.SIPResponse;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.stereotype.Component;
+import org.springframework.util.ObjectUtils;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * 视频流session管理器,管理视频预览、预览回放的通信句柄
+ */
+@Component
+public class VideoStreamSessionManager {
+
+	@Autowired
+	private UserSetting userSetting;
+
+	@Autowired
+	private RedisTemplate<Object, Object> redisTemplate;
+
+	/**
+	 * 添加一个点播/回放的事务信息
+	 * 后续可以通过流Id/callID
+	 * @param deviceId 设备ID
+	 * @param channelId 通道ID
+	 * @param callId 一次请求的CallID
+	 * @param stream 流名称
+	 * @param mediaServerId 所使用的流媒体ID
+	 * @param response 回复
+	 */
+	public void put(String deviceId, String channelId, String callId, String stream, String ssrc, String mediaServerId, SIPResponse response, InviteSessionType type){
+		SsrcTransaction ssrcTransaction = new SsrcTransaction();
+		ssrcTransaction.setDeviceId(deviceId);
+		ssrcTransaction.setChannelId(channelId);
+		ssrcTransaction.setStream(stream);
+		ssrcTransaction.setSipTransactionInfo(new SipTransactionInfo(response));
+		ssrcTransaction.setCallId(callId);
+		ssrcTransaction.setSsrc(ssrc);
+		ssrcTransaction.setMediaServerId(mediaServerId);
+		ssrcTransaction.setType(type);
+
+		redisTemplate.opsForValue().set(VideoManagerConstants.MEDIA_TRANSACTION_USED_PREFIX + userSetting.getServerId()
+				+ "_" +  deviceId + "_" + channelId + "_" + callId + "_" + stream, ssrcTransaction);
+	}
+
+	public SsrcTransaction getSsrcTransaction(String deviceId, String channelId, String callId, String stream){
+
+		if (ObjectUtils.isEmpty(deviceId)) {
+			deviceId ="*";
+		}
+		if (ObjectUtils.isEmpty(channelId)) {
+			channelId ="*";
+		}
+		if (ObjectUtils.isEmpty(callId)) {
+			callId ="*";
+		}
+		if (ObjectUtils.isEmpty(stream)) {
+			stream ="*";
+		}
+		String key = VideoManagerConstants.MEDIA_TRANSACTION_USED_PREFIX + userSetting.getServerId() + "_" + deviceId + "_" + channelId + "_" + callId+ "_" + stream;
+		List<Object> scanResult = RedisUtil.scan(redisTemplate, key);
+		if (scanResult.size() == 0) {
+			return null;
+		}
+		return (SsrcTransaction)redisTemplate.opsForValue().get(scanResult.get(0));
+	}
+
+	public List<SsrcTransaction> getSsrcTransactionForAll(String deviceId, String channelId, String callId, String stream){
+		if (ObjectUtils.isEmpty(deviceId)) {
+			deviceId ="*";
+		}
+		if (ObjectUtils.isEmpty(channelId)) {
+			channelId ="*";
+		}
+		if (ObjectUtils.isEmpty(callId)) {
+			callId ="*";
+		}
+		if (ObjectUtils.isEmpty(stream)) {
+			stream ="*";
+		}
+		String key = VideoManagerConstants.MEDIA_TRANSACTION_USED_PREFIX + userSetting.getServerId() + "_" + deviceId + "_" + channelId + "_" + callId+ "_" + stream;
+		List<Object> scanResult = RedisUtil.scan(redisTemplate, key);
+		if (scanResult.size() == 0) {
+			return null;
+		}
+		List<SsrcTransaction> result = new ArrayList<>();
+		for (Object keyObj : scanResult) {
+			result.add((SsrcTransaction)redisTemplate.opsForValue().get(keyObj));
+		}
+		return result;
+	}
+
+	public String getMediaServerId(String deviceId, String channelId, String stream){
+		SsrcTransaction ssrcTransaction = getSsrcTransaction(deviceId, channelId, null, stream);
+		if (ssrcTransaction == null) {
+			return null;
+		}
+		return ssrcTransaction.getMediaServerId();
+	}
+
+	public String getSSRC(String deviceId, String channelId, String stream){
+		SsrcTransaction ssrcTransaction = getSsrcTransaction(deviceId, channelId, null, stream);
+		if (ssrcTransaction == null) {
+			return null;
+		}
+		return ssrcTransaction.getSsrc();
+	}
+	
+	public void remove(String deviceId, String channelId, String stream) {
+		SsrcTransaction ssrcTransaction = getSsrcTransaction(deviceId, channelId, null, stream);
+		if (ssrcTransaction == null) {
+			return;
+		}
+		redisTemplate.delete(VideoManagerConstants.MEDIA_TRANSACTION_USED_PREFIX + userSetting.getServerId() + "_"
+				+  deviceId + "_" + channelId + "_" + ssrcTransaction.getCallId() + "_" + ssrcTransaction.getStream());
+	}
+
+
+	public List<SsrcTransaction> getAllSsrc() {
+		List<Object> ssrcTransactionKeys = RedisUtil.scan(redisTemplate, String.format("%s_*_*_*_*", VideoManagerConstants.MEDIA_TRANSACTION_USED_PREFIX+ userSetting.getServerId()));
+		List<SsrcTransaction> result= new ArrayList<>();
+		for (Object ssrcTransactionKey : ssrcTransactionKeys) {
+			String key = (String) ssrcTransactionKey;
+			SsrcTransaction ssrcTransaction = JsonUtil.redisJsonToObject(redisTemplate, key, SsrcTransaction.class);
+			result.add(ssrcTransaction);
+		}
+		return result;
+	}
+}

+ 13 - 1
src/main/java/com/genersoft/iot/vmp/gb28181/task/ISubscribeTask.java

@@ -12,13 +12,19 @@ import com.genersoft.iot.vmp.media.zlm.dto.MediaServerItem;
 import com.genersoft.iot.vmp.service.IDeviceService;
 import com.genersoft.iot.vmp.service.IMediaServerService;
 import com.genersoft.iot.vmp.service.IPlatformService;
+import com.genersoft.iot.vmp.service.impl.PlatformServiceImpl;
 import com.genersoft.iot.vmp.storager.IRedisCatchStorage;
 import com.genersoft.iot.vmp.storager.IVideoManagerStorage;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.boot.CommandLineRunner;
 import org.springframework.core.annotation.Order;
 import org.springframework.stereotype.Component;
 
+import javax.sip.InvalidArgumentException;
+import javax.sip.SipException;
+import java.text.ParseException;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
@@ -59,6 +65,8 @@ public class SipRunner implements CommandLineRunner {
     @Autowired
     private ISIPCommanderForPlatform commanderForPlatform;
 
+    private final static Logger logger = LoggerFactory.getLogger(PlatformServiceImpl.class);
+
     @Override
     public void run(String... args) throws Exception {
         List<Device> deviceList = deviceService.getAllOnlineDevice();
@@ -110,7 +118,11 @@ public class SipRunner implements CommandLineRunner {
                     if (jsonObject != null && jsonObject.getInteger("code") == 0) {
                         ParentPlatform platform = platformService.queryPlatformByServerGBId(sendRtpItem.getPlatformId());
                         if (platform != null) {
-                            commanderForPlatform.streamByeCmd(platform, sendRtpItem.getCallId());
+                            try {
+                                commanderForPlatform.streamByeCmd(platform, sendRtpItem.getCallId());
+                            } catch (InvalidArgumentException | ParseException | SipException e) {
+                                logger.error("[命令发送失败] 国标级联 发送BYE: {}", e.getMessage());
+                            }
                         }
                     }
                 }

+ 160 - 160
src/main/java/com/genersoft/iot/vmp/gb28181/task/impl/CatalogSubscribeTask.java

@@ -1,160 +1,160 @@
-package com.genersoft.iot.vmp.gb28181.transmit.callback;
-
-import com.genersoft.iot.vmp.vmanager.bean.DeferredResultEx;
-import org.springframework.stereotype.Component;
-import org.springframework.util.ObjectUtils;
-import org.springframework.web.context.request.async.DeferredResult;
-
-import java.util.Collection;
-import java.util.Map;
-import java.util.Set;
-import java.util.concurrent.ConcurrentHashMap;
-
-/**    
- * @description: 异步请求处理
- * @author: swwheihei
- * @date:   2020年5月8日 下午7:59:05     
- */
-@SuppressWarnings(value = {"rawtypes", "unchecked"})
-@Component
-public class DeferredResultHolder {
-	
-	public static final String CALLBACK_CMD_DEVICESTATUS = "CALLBACK_DEVICESTATUS";
-	
-	public static final String CALLBACK_CMD_DEVICEINFO = "CALLBACK_DEVICEINFO";
-	
-	public static final String CALLBACK_CMD_DEVICECONTROL = "CALLBACK_DEVICECONTROL";
-	
-	public static final String CALLBACK_CMD_DEVICECONFIG = "CALLBACK_DEVICECONFIG";
-
-	public static final String CALLBACK_CMD_CONFIGDOWNLOAD = "CALLBACK_CONFIGDOWNLOAD";
-	
-	public static final String CALLBACK_CMD_CATALOG = "CALLBACK_CATALOG";
-	
-	public static final String CALLBACK_CMD_RECORDINFO = "CALLBACK_RECORDINFO";
-
-	public static final String CALLBACK_CMD_PLAY = "CALLBACK_PLAY";
-
-	public static final String CALLBACK_CMD_PLAYBACK = "CALLBACK_PLAYBACK";
-
-	public static final String CALLBACK_CMD_DOWNLOAD = "CALLBACK_DOWNLOAD";
-
-	public static final String CALLBACK_CMD_PROXY = "CALLBACK_PROXY";
-
-	public static final String CALLBACK_CMD_STOP = "CALLBACK_STOP";
-
-	public static final String UPLOAD_FILE_CHANNEL = "UPLOAD_FILE_CHANNEL";
-
-	public static final String CALLBACK_CMD_MOBILE_POSITION = "CALLBACK_CMD_MOBILE_POSITION";
-
-	public static final String CALLBACK_CMD_PRESETQUERY = "CALLBACK_PRESETQUERY";
-
-	public static final String CALLBACK_CMD_ALARM = "CALLBACK_ALARM";
-
-	public static final String CALLBACK_CMD_BROADCAST = "CALLBACK_BROADCAST";
-
-	public static final String CALLBACK_CMD_SNAP= "CALLBACK_SNAP";
-
-	private Map<String, Map<String, DeferredResultEx>> map = new ConcurrentHashMap<>();
-
-
-	public void put(String key, String id, DeferredResultEx result) {
-		Map<String, DeferredResultEx> deferredResultMap = map.get(key);
-		if (deferredResultMap == null) {
-			deferredResultMap = new ConcurrentHashMap<>();
-			map.put(key, deferredResultMap);
-		}
-		deferredResultMap.put(id, result);
-	}
-
-	public void put(String key, String id, DeferredResult result) {
-		Map<String, DeferredResultEx> deferredResultMap = map.get(key);
-		if (deferredResultMap == null) {
-			deferredResultMap = new ConcurrentHashMap<>();
-			map.put(key, deferredResultMap);
-		}
-		deferredResultMap.put(id, new DeferredResultEx(result));
-	}
-	
-	public DeferredResultEx get(String key, String id) {
-		Map<String, DeferredResultEx> deferredResultMap = map.get(key);
-		if (deferredResultMap == null || ObjectUtils.isEmpty(id)) {
-			return null;
-		}
-		return deferredResultMap.get(id);
-	}
-
-	public Collection<DeferredResultEx> getAllByKey(String key) {
-		Map<String, DeferredResultEx> deferredResultMap = map.get(key);
-		if (deferredResultMap == null) {
-			return null;
-		}
-		return deferredResultMap.values();
-	}
-
-	public boolean exist(String key, String id){
-		if (key == null) {
-			return false;
-		}
-		Map<String, DeferredResultEx> deferredResultMap = map.get(key);
-		if (id == null) {
-			return deferredResultMap != null;
-		}else {
-			return deferredResultMap != null && deferredResultMap.get(id) != null;
-		}
-	}
-
-	/**
-	 * 释放单个请求
-	 * @param msg
-	 */
-	public void invokeResult(RequestMessage msg) {
-		Map<String, DeferredResultEx> deferredResultMap = map.get(msg.getKey());
-		if (deferredResultMap == null) {
-			return;
-		}
-		DeferredResultEx result = deferredResultMap.get(msg.getId());
-		if (result == null) {
-			return;
-		}
-		result.getDeferredResult().setResult(msg.getData());
-		deferredResultMap.remove(msg.getId());
-		if (deferredResultMap.size() == 0) {
-			map.remove(msg.getKey());
-		}
-	}
-
-	/**
-	 * 释放所有的请求
-	 * @param msg
-	 */
-	public void invokeAllResult(RequestMessage msg) {
-		Map<String, DeferredResultEx> deferredResultMap = map.get(msg.getKey());
-		if (deferredResultMap == null) {
-			return;
-		}
-		synchronized (this) {
-			deferredResultMap = map.get(msg.getKey());
-			if (deferredResultMap == null) {
-				return;
-			}
-			Set<String> ids = deferredResultMap.keySet();
-			for (String id : ids) {
-				DeferredResultEx result = deferredResultMap.get(id);
-				if (result == null) {
-					return;
-				}
-				if (result.getFilter() != null) {
-					Object handler = result.getFilter().handler(msg.getData());
-					result.getDeferredResult().setResult(handler);
-				}else {
-					result.getDeferredResult().setResult(msg.getData());
-				}
-
-			}
-			map.remove(msg.getKey());
-		}
-	}
-
-
-}
+package com.genersoft.iot.vmp.gb28181.transmit.callback;
+
+import com.genersoft.iot.vmp.vmanager.bean.DeferredResultEx;
+import org.springframework.stereotype.Component;
+import org.springframework.util.ObjectUtils;
+import org.springframework.web.context.request.async.DeferredResult;
+
+import java.util.Collection;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**    
+ * @description: 异步请求处理
+ * @author: swwheihei
+ * @date:   2020年5月8日 下午7:59:05     
+ */
+@SuppressWarnings(value = {"rawtypes", "unchecked"})
+@Component
+public class DeferredResultHolder {
+	
+	public static final String CALLBACK_CMD_DEVICESTATUS = "CALLBACK_DEVICESTATUS";
+	
+	public static final String CALLBACK_CMD_DEVICEINFO = "CALLBACK_DEVICEINFO";
+	
+	public static final String CALLBACK_CMD_DEVICECONTROL = "CALLBACK_DEVICECONTROL";
+	
+	public static final String CALLBACK_CMD_DEVICECONFIG = "CALLBACK_DEVICECONFIG";
+
+	public static final String CALLBACK_CMD_CONFIGDOWNLOAD = "CALLBACK_CONFIGDOWNLOAD";
+	
+	public static final String CALLBACK_CMD_CATALOG = "CALLBACK_CATALOG";
+	
+	public static final String CALLBACK_CMD_RECORDINFO = "CALLBACK_RECORDINFO";
+
+	public static final String CALLBACK_CMD_PLAY = "CALLBACK_PLAY";
+
+	public static final String CALLBACK_CMD_PLAYBACK = "CALLBACK_PLAYBACK";
+
+	public static final String CALLBACK_CMD_DOWNLOAD = "CALLBACK_DOWNLOAD";
+
+	public static final String CALLBACK_CMD_PROXY = "CALLBACK_PROXY";
+
+	public static final String CALLBACK_CMD_STOP = "CALLBACK_STOP";
+
+	public static final String UPLOAD_FILE_CHANNEL = "UPLOAD_FILE_CHANNEL";
+
+	public static final String CALLBACK_CMD_MOBILE_POSITION = "CALLBACK_CMD_MOBILE_POSITION";
+
+	public static final String CALLBACK_CMD_PRESETQUERY = "CALLBACK_PRESETQUERY";
+
+	public static final String CALLBACK_CMD_ALARM = "CALLBACK_ALARM";
+
+	public static final String CALLBACK_CMD_BROADCAST = "CALLBACK_BROADCAST";
+
+	public static final String CALLBACK_CMD_SNAP= "CALLBACK_SNAP";
+
+	private Map<String, Map<String, DeferredResultEx>> map = new ConcurrentHashMap<>();
+
+
+	public void put(String key, String id, DeferredResultEx result) {
+		Map<String, DeferredResultEx> deferredResultMap = map.get(key);
+		if (deferredResultMap == null) {
+			deferredResultMap = new ConcurrentHashMap<>();
+			map.put(key, deferredResultMap);
+		}
+		deferredResultMap.put(id, result);
+	}
+
+	public void put(String key, String id, DeferredResult result) {
+		Map<String, DeferredResultEx> deferredResultMap = map.get(key);
+		if (deferredResultMap == null) {
+			deferredResultMap = new ConcurrentHashMap<>();
+			map.put(key, deferredResultMap);
+		}
+		deferredResultMap.put(id, new DeferredResultEx(result));
+	}
+	
+	public DeferredResultEx get(String key, String id) {
+		Map<String, DeferredResultEx> deferredResultMap = map.get(key);
+		if (deferredResultMap == null || ObjectUtils.isEmpty(id)) {
+			return null;
+		}
+		return deferredResultMap.get(id);
+	}
+
+	public Collection<DeferredResultEx> getAllByKey(String key) {
+		Map<String, DeferredResultEx> deferredResultMap = map.get(key);
+		if (deferredResultMap == null) {
+			return null;
+		}
+		return deferredResultMap.values();
+	}
+
+	public boolean exist(String key, String id){
+		if (key == null) {
+			return false;
+		}
+		Map<String, DeferredResultEx> deferredResultMap = map.get(key);
+		if (id == null) {
+			return deferredResultMap != null;
+		}else {
+			return deferredResultMap != null && deferredResultMap.get(id) != null;
+		}
+	}
+
+	/**
+	 * 释放单个请求
+	 * @param msg
+	 */
+	public void invokeResult(RequestMessage msg) {
+		Map<String, DeferredResultEx> deferredResultMap = map.get(msg.getKey());
+		if (deferredResultMap == null) {
+			return;
+		}
+		DeferredResultEx result = deferredResultMap.get(msg.getId());
+		if (result == null) {
+			return;
+		}
+		result.getDeferredResult().setResult(msg.getData());
+		deferredResultMap.remove(msg.getId());
+		if (deferredResultMap.size() == 0) {
+			map.remove(msg.getKey());
+		}
+	}
+
+	/**
+	 * 释放所有的请求
+	 * @param msg
+	 */
+	public void invokeAllResult(RequestMessage msg) {
+		Map<String, DeferredResultEx> deferredResultMap = map.get(msg.getKey());
+		if (deferredResultMap == null) {
+			return;
+		}
+		synchronized (this) {
+			deferredResultMap = map.get(msg.getKey());
+			if (deferredResultMap == null) {
+				return;
+			}
+			Set<String> ids = deferredResultMap.keySet();
+			for (String id : ids) {
+				DeferredResultEx result = deferredResultMap.get(id);
+				if (result == null) {
+					return;
+				}
+				if (result.getFilter() != null) {
+					Object handler = result.getFilter().handler(msg.getData());
+					result.getDeferredResult().setResult(handler);
+				}else {
+					result.getDeferredResult().setResult(msg.getData());
+				}
+
+			}
+			map.remove(msg.getKey());
+		}
+	}
+
+
+}

+ 39 - 39
src/main/java/com/genersoft/iot/vmp/gb28181/transmit/callback/RequestMessage.java

@@ -1,39 +1,39 @@
-package com.genersoft.iot.vmp.gb28181.transmit.callback;
-
-/**    
- * @description: 请求信息定义   
- * @author: swwheihei
- * @date:   2020年5月8日 下午1:09:18     
- */
-public class RequestMessage {
-	
-	private String id;
-
-	private String key;
-
-	private Object data;
-
-	public String getId() {
-		return id;
-	}
-
-	public void setId(String id) {
-		this.id = id;
-	}
-
-	public void setKey(String key) {
-		this.key = key;
-	}
-
-	public String getKey() {
-		return key;
-	}
-
-	public Object getData() {
-		return data;
-	}
-
-	public void setData(Object data) {
-		this.data = data;
-	}
-}
+package com.genersoft.iot.vmp.gb28181.transmit.callback;
+
+/**    
+ * @description: 请求信息定义   
+ * @author: swwheihei
+ * @date:   2020年5月8日 下午1:09:18     
+ */
+public class RequestMessage {
+	
+	private String id;
+
+	private String key;
+
+	private Object data;
+
+	public String getId() {
+		return id;
+	}
+
+	public void setId(String id) {
+		this.id = id;
+	}
+
+	public void setKey(String key) {
+		this.key = key;
+	}
+
+	public String getKey() {
+		return key;
+	}
+
+	public Object getData() {
+		return data;
+	}
+
+	public void setData(Object data) {
+		this.data = data;
+	}
+}

+ 364 - 364
src/main/java/com/genersoft/iot/vmp/gb28181/transmit/cmd/ISIPCommander.java

@@ -1,364 +1,364 @@
-package com.genersoft.iot.vmp.gb28181.transmit.cmd;
-
-import com.genersoft.iot.vmp.common.StreamInfo;
-import com.genersoft.iot.vmp.conf.exception.SsrcTransactionNotFoundException;
-import com.genersoft.iot.vmp.gb28181.bean.*;
-import com.genersoft.iot.vmp.gb28181.bean.Device;
-import com.genersoft.iot.vmp.gb28181.bean.DeviceAlarm;
-import com.genersoft.iot.vmp.gb28181.event.SipSubscribe;
-import com.genersoft.iot.vmp.media.zlm.ZlmHttpHookSubscribe;
-import com.genersoft.iot.vmp.media.zlm.dto.MediaServerItem;
-import com.genersoft.iot.vmp.service.bean.SSRCInfo;
-import gov.nist.javax.sip.message.SIPRequest;
-
-import javax.sip.InvalidArgumentException;
-import javax.sip.SipException;
-import java.text.ParseException;
-
-/**    
- * @description:设备能力接口,用于定义设备的控制、查询能力   
- * @author: swwheihei
- * @date:   2020年5月3日 下午9:16:34     
- */
-public interface ISIPCommander {
-
-	/**
-	 * 云台方向放控制,使用配置文件中的默认镜头移动速度
-	 * 
-	 * @param device  控制设备
-	 * @param channelId  预览通道
-	 * @param leftRight  镜头左移右移 0:停止 1:左移 2:右移
-     * @param upDown     镜头上移下移 0:停止 1:上移 2:下移
-	 */
-	void ptzdirectCmd(Device device,String channelId,int leftRight, int upDown) throws InvalidArgumentException, ParseException, SipException;
-	
-	/**
-	 * 云台方向放控制
-	 * 
-	 * @param device  控制设备
-	 * @param channelId  预览通道
-	 * @param leftRight  镜头左移右移 0:停止 1:左移 2:右移
-     * @param upDown     镜头上移下移 0:停止 1:上移 2:下移
-     * @param moveSpeed  镜头移动速度
-	 */
-	void ptzdirectCmd(Device device,String channelId,int leftRight, int upDown, int moveSpeed) throws InvalidArgumentException, ParseException, SipException;
-	
-	/**
-	 * 云台缩放控制,使用配置文件中的默认镜头缩放速度
-	 * 
-	 * @param device  控制设备
-	 * @param channelId  预览通道
-     * @param inOut      镜头放大缩小 0:停止 1:缩小 2:放大
-	 */
-	void ptzZoomCmd(Device device,String channelId,int inOut) throws InvalidArgumentException, ParseException, SipException;
-	
-	/**
-	 * 云台缩放控制
-	 * 
-	 * @param device  控制设备
-	 * @param channelId  预览通道
-     * @param inOut      镜头放大缩小 0:停止 1:缩小 2:放大
-	 */
-	void ptzZoomCmd(Device device,String channelId,int inOut, int moveSpeed) throws InvalidArgumentException, ParseException, SipException;
-	
-	/**
-	 * 云台控制,支持方向与缩放控制
-	 * 
-	 * @param device  控制设备
-	 * @param channelId  预览通道
-	 * @param leftRight  镜头左移右移 0:停止 1:左移 2:右移
-     * @param upDown     镜头上移下移 0:停止 1:上移 2:下移
-     * @param inOut      镜头放大缩小 0:停止 1:缩小 2:放大
-     * @param moveSpeed  镜头移动速度
-     * @param zoomSpeed  镜头缩放速度
-	 */
-	void ptzCmd(Device device,String channelId,int leftRight, int upDown, int inOut, int moveSpeed, int zoomSpeed) throws InvalidArgumentException, SipException, ParseException;
-	
-	/**
-	 * 前端控制,包括PTZ指令、FI指令、预置位指令、巡航指令、扫描指令和辅助开关指令
-	 * 
-	 * @param device  		控制设备
-	 * @param channelId		预览通道
-	 * @param cmdCode		指令码
-     * @param parameter1	数据1
-     * @param parameter2	数据2
-     * @param combineCode2	组合码2
-	 */
-	void frontEndCmd(Device device, String channelId, int cmdCode, int parameter1, int parameter2, int combineCode2) throws SipException, InvalidArgumentException, ParseException;
-	
-	/**
-	 * 前端控制指令(用于转发上级指令)
-	 * @param device		控制设备
-	 * @param channelId		预览通道
-	 * @param cmdString		前端控制指令串
-	 */
-	void fronEndCmd(Device device, String channelId, String cmdString, SipSubscribe.Event errorEvent, SipSubscribe.Event okEvent) throws InvalidArgumentException, SipException, ParseException;
-
-	/**
-	 * 请求预览视频流
-	 * @param device  视频设备
-	 * @param channelId  预览通道
-	 */
-	void playStreamCmd(MediaServerItem mediaServerItem, SSRCInfo ssrcInfo, Device device, String channelId, ZlmHttpHookSubscribe.Event event, SipSubscribe.Event okEvent, SipSubscribe.Event errorEvent) throws InvalidArgumentException, SipException, ParseException;
-
-	/**
-	 * 请求回放视频流
-	 * 
-	 * @param device  视频设备
-	 * @param channelId  预览通道
-	 * @param startTime 开始时间,格式要求:yyyy-MM-dd HH:mm:ss
-	 * @param endTime 结束时间,格式要求:yyyy-MM-dd HH:mm:ss
-	 */
-	void playbackStreamCmd(MediaServerItem mediaServerItem, SSRCInfo ssrcInf, Device device, String channelId, String startTime, String endTime,ZlmHttpHookSubscribe.Event event, SipSubscribe.Event okEvent, SipSubscribe.Event errorEvent) throws InvalidArgumentException, SipException, ParseException;
-
-	/**
-	 * 请求历史媒体下载
-	 * 
-	 * @param device  视频设备
-	 * @param channelId  预览通道
-	 * @param startTime 开始时间,格式要求:yyyy-MM-dd HH:mm:ss
-	 * @param endTime 结束时间,格式要求:yyyy-MM-dd HH:mm:ss
-	 * @param downloadSpeed 下载倍速参数
-	 */ 
-	void downloadStreamCmd(MediaServerItem mediaServerItem, SSRCInfo ssrcInfo, Device device, String channelId,
-						   String startTime, String endTime, int downloadSpeed, ZlmHttpHookSubscribe.Event hookEvent,
-						   SipSubscribe.Event errorEvent,SipSubscribe.Event okEvent) throws InvalidArgumentException, SipException, ParseException;
-
-
-	/**
-	 * 视频流停止
-	 */
-	void streamByeCmd(Device device, String channelId, String stream, String callId, SipSubscribe.Event okEvent) throws InvalidArgumentException, SipException, ParseException, SsrcTransactionNotFoundException;
-
-	void talkStreamCmd(MediaServerItem mediaServerItem, SendRtpItem sendRtpItem, Device device, String channelId, String callId, ZlmHttpHookSubscribe.Event event, ZlmHttpHookSubscribe.Event eventForPush, SipSubscribe.Event okEvent, SipSubscribe.Event errorEvent) throws InvalidArgumentException, SipException, ParseException;
-
-
-	void streamByeCmd(Device device, String channelId, String stream, String callId) throws InvalidArgumentException, ParseException, SipException, SsrcTransactionNotFoundException;
-
-	void streamByeCmd(Device device, String channelId, SipTransactionInfo sipTransactionInfo, SipSubscribe.Event okEvent) throws InvalidArgumentException, SipException, ParseException, SsrcTransactionNotFoundException;
-
-	/**
-	 * 回放暂停
-	 */
-	void playPauseCmd(Device device, StreamInfo streamInfo) throws InvalidArgumentException, ParseException, SipException;
-
-	/**
-	 * 回放恢复
-	 */
-	void playResumeCmd(Device device, StreamInfo streamInfo) throws InvalidArgumentException, ParseException, SipException;
-
-	/**
-	 * 回放拖动播放
-	 */
-	void playSeekCmd(Device device, StreamInfo streamInfo, long seekTime) throws InvalidArgumentException, ParseException, SipException;
-
-	/**
-	 * 回放倍速播放
-	 */
-	void playSpeedCmd(Device device, StreamInfo streamInfo, Double speed) throws InvalidArgumentException, ParseException, SipException;
-	
-	/**
-	 * 回放控制
-	 * @param device
-	 * @param streamInfo
-	 * @param content
-	 */
-	void playbackControlCmd(Device device, StreamInfo streamInfo, String content,SipSubscribe.Event errorEvent, SipSubscribe.Event okEvent) throws SipException, InvalidArgumentException, ParseException;
-
-
-    /**
-	 * /**
-	 * 语音广播
-	 *
-	 * @param device 视频设备
-	 */
-	void audioBroadcastCmd(Device device, String channelId, SipSubscribe.Event okEvent, SipSubscribe.Event errorEvent) throws InvalidArgumentException, SipException, ParseException;
-
-	/**
-	 * 音视频录像控制
-	 * 
-	 * @param device  		视频设备
-	 * @param channelId  	预览通道
-	 * @param recordCmdStr	录像命令:Record / StopRecord
-	 */
-	void recordCmd(Device device, String channelId, String recordCmdStr, SipSubscribe.Event errorEvent, SipSubscribe.Event okEvent) throws InvalidArgumentException, SipException, ParseException;
-	
-	/**
-	 * 远程启动控制命令
-	 * 
-	 * @param device	视频设备
-	 */
-	void teleBootCmd(Device device) throws InvalidArgumentException, SipException, ParseException;
-
-	/**
-	 * 报警布防/撤防命令
-	 * 
-	 * @param device  	视频设备
-	 */
-	void guardCmd(Device device, String guardCmdStr, SipSubscribe.Event errorEvent, SipSubscribe.Event okEvent) throws InvalidArgumentException, SipException, ParseException;
-	
-	/**
-	 * 报警复位命令
-	 * 
-	 * @param device		视频设备
-	 * @param alarmMethod	报警方式(可选)
-	 * @param alarmType		报警类型(可选)
-	 */
-	void alarmCmd(Device device, String alarmMethod, String alarmType, SipSubscribe.Event errorEvent, SipSubscribe.Event okEvent) throws InvalidArgumentException, SipException, ParseException;
-	
-	/**
-	 * 强制关键帧命令,设备收到此命令应立刻发送一个IDR帧
-	 * 
-	 * @param device  视频设备
-	 * @param channelId  预览通道
-	 */
-	void iFrameCmd(Device device, String channelId) throws InvalidArgumentException, SipException, ParseException;
-
-	/**
-	 * 看守位控制命令
-	 *
-	 * @param device      视频设备
-	 * @param channelId      通道id,非通道则是设备本身
-	 * @param enabled     看守位使能:1 = 开启,0 = 关闭
-	 * @param resetTime   自动归位时间间隔,开启看守位时使用,单位:秒(s)
-	 * @param presetIndex 调用预置位编号,开启看守位时使用,取值范围0~255
-	 */
-	void homePositionCmd(Device device, String channelId, String enabled, String resetTime, String presetIndex, SipSubscribe.Event errorEvent,SipSubscribe.Event okEvent) throws InvalidArgumentException, SipException, ParseException;
-
-	/**
-	 * 设备配置命令
-	 * 
-	 * @param device  视频设备
-	 */
-	void deviceConfigCmd(Device device);
-	
-	/**
-	 * 设备配置命令:basicParam
-	 * 
-	 * @param device  			视频设备
-	 * @param channelId			通道编码(可选)
-	 * @param name				设备/通道名称(可选)
-	 * @param expiration		注册过期时间(可选)
-	 * @param heartBeatInterval	心跳间隔时间(可选)
-	 * @param heartBeatCount	心跳超时次数(可选)
-	 */  
-	void deviceBasicConfigCmd(Device device, String channelId, String name, String expiration, String heartBeatInterval, String heartBeatCount, SipSubscribe.Event errorEvent) throws InvalidArgumentException, SipException, ParseException;
-
-	/**
-	 * 查询设备状态
-	 * 
-	 * @param device 视频设备
-	 */
-	void deviceStatusQuery(Device device, SipSubscribe.Event errorEvent) throws InvalidArgumentException, SipException, ParseException;
-	
-	/**
-	 * 查询设备信息
-	 * 
-	 * @param device 视频设备
-	 * @return 
-	 */
-	void deviceInfoQuery(Device device) throws InvalidArgumentException, SipException, ParseException;
-	
-	/**
-	 * 查询目录列表
-	 * 
-	 * @param device 视频设备
-	 */
-	void catalogQuery(Device device, int sn, SipSubscribe.Event errorEvent) throws SipException, InvalidArgumentException, ParseException;
-	
-	/**
-	 * 查询录像信息
-	 * 
-	 * @param device 视频设备
-	 * @param startTime 开始时间,格式要求:yyyy-MM-dd HH:mm:ss
-	 * @param endTime 结束时间,格式要求:yyyy-MM-dd HH:mm:ss
-	 * @param sn
-	 */
-	void recordInfoQuery(Device device, String channelId, String startTime, String endTime, int sn,  Integer Secrecy, String type, SipSubscribe.Event okEvent, SipSubscribe.Event errorEvent) throws InvalidArgumentException, SipException, ParseException;
-	
-	/**
-	 * 查询报警信息
-	 * 
-	 * @param device		视频设备
-	 * @param startPriority	报警起始级别(可选)
-	 * @param endPriority	报警终止级别(可选)
-	 * @param alarmMethod	报警方式条件(可选)
-	 * @param alarmType		报警类型
-	 * @param startTime		报警发生起始时间(可选)
-	 * @param endTime		报警发生终止时间(可选)
-	 * @return				true = 命令发送成功
-	 */
-	void alarmInfoQuery(Device device, String startPriority, String endPriority, String alarmMethod,
-							String alarmType, String startTime, String endTime, SipSubscribe.Event errorEvent) throws InvalidArgumentException, SipException, ParseException;
-	
-	/**
-	 * 查询设备配置
-	 * 
-	 * @param device 		视频设备
-	 * @param channelId		通道编码(可选)
-	 * @param configType	配置类型:
-	 */
-	void deviceConfigQuery(Device device, String channelId, String configType,  SipSubscribe.Event errorEvent) throws InvalidArgumentException, SipException, ParseException;
-	
-	/**
-	 * 查询设备预置位置
-	 * 
-	 * @param device 视频设备
-	 */
-	void presetQuery(Device device, String channelId, SipSubscribe.Event errorEvent) throws InvalidArgumentException, SipException, ParseException;
-	
-	/**
-	 * 查询移动设备位置数据
-	 * 
-	 * @param device 视频设备
-	 */
-	void mobilePostitionQuery(Device device, SipSubscribe.Event errorEvent) throws InvalidArgumentException, SipException, ParseException;
-
-	/**
-	 * 订阅、取消订阅移动位置
-	 * 
-	 * @param device	视频设备
-	 * @return			true = 命令发送成功
-	 */
-	SIPRequest mobilePositionSubscribe(Device device, SIPRequest request, SipSubscribe.Event okEvent , SipSubscribe.Event errorEvent) throws InvalidArgumentException, SipException, ParseException;
-
-	/**
-	 * 订阅、取消订阅报警信息
-	 * @param device		视频设备
-	 * @param expires		订阅过期时间(0 = 取消订阅)
-	 * @param startPriority	报警起始级别(可选)
-	 * @param endPriority	报警终止级别(可选)
-	 * @param alarmType		报警类型
-	 * @param startTime		报警发生起始时间(可选)
-	 * @param endTime		报警发生终止时间(可选)
-	 * @return				true = 命令发送成功
-	 */
-	void alarmSubscribe(Device device, int expires, String startPriority, String endPriority, String alarmMethod, String startTime, String endTime) throws InvalidArgumentException, SipException, ParseException;
-
-	/**
-	 * 订阅、取消订阅目录信息
-	 * @param device		视频设备
-	 * @return				true = 命令发送成功
-	 */
-	SIPRequest catalogSubscribe(Device device, SIPRequest request, SipSubscribe.Event okEvent ,SipSubscribe.Event errorEvent) throws InvalidArgumentException, SipException, ParseException;
-
-	/**
-	 * 拉框控制命令
-	 *
-	 * @param device    控制设备
-	 * @param channelId 通道id
-	 * @param cmdString 前端控制指令串
-	 */
-	void dragZoomCmd(Device device, String channelId, String cmdString) throws InvalidArgumentException, SipException, ParseException;
-
-
-	/**
-	 * 向设备发送报警NOTIFY消息, 用于互联结构下,此时将设备当成一个平级平台看待
-	 * @param device 设备
-	 * @param deviceAlarm 报警信息信息
-	 * @return
-	 */
-	void sendAlarmMessage(Device device, DeviceAlarm deviceAlarm) throws InvalidArgumentException, SipException, ParseException;
-
-
-}
+package com.genersoft.iot.vmp.gb28181.transmit.cmd;
+
+import com.genersoft.iot.vmp.common.StreamInfo;
+import com.genersoft.iot.vmp.conf.exception.SsrcTransactionNotFoundException;
+import com.genersoft.iot.vmp.gb28181.bean.*;
+import com.genersoft.iot.vmp.gb28181.bean.Device;
+import com.genersoft.iot.vmp.gb28181.bean.DeviceAlarm;
+import com.genersoft.iot.vmp.gb28181.event.SipSubscribe;
+import com.genersoft.iot.vmp.media.zlm.ZlmHttpHookSubscribe;
+import com.genersoft.iot.vmp.media.zlm.dto.MediaServerItem;
+import com.genersoft.iot.vmp.service.bean.SSRCInfo;
+import gov.nist.javax.sip.message.SIPRequest;
+
+import javax.sip.InvalidArgumentException;
+import javax.sip.SipException;
+import java.text.ParseException;
+
+/**    
+ * @description:设备能力接口,用于定义设备的控制、查询能力   
+ * @author: swwheihei
+ * @date:   2020年5月3日 下午9:16:34     
+ */
+public interface ISIPCommander {
+
+	/**
+	 * 云台方向放控制,使用配置文件中的默认镜头移动速度
+	 * 
+	 * @param device  控制设备
+	 * @param channelId  预览通道
+	 * @param leftRight  镜头左移右移 0:停止 1:左移 2:右移
+     * @param upDown     镜头上移下移 0:停止 1:上移 2:下移
+	 */
+	void ptzdirectCmd(Device device,String channelId,int leftRight, int upDown) throws InvalidArgumentException, ParseException, SipException;
+	
+	/**
+	 * 云台方向放控制
+	 * 
+	 * @param device  控制设备
+	 * @param channelId  预览通道
+	 * @param leftRight  镜头左移右移 0:停止 1:左移 2:右移
+     * @param upDown     镜头上移下移 0:停止 1:上移 2:下移
+     * @param moveSpeed  镜头移动速度
+	 */
+	void ptzdirectCmd(Device device,String channelId,int leftRight, int upDown, int moveSpeed) throws InvalidArgumentException, ParseException, SipException;
+	
+	/**
+	 * 云台缩放控制,使用配置文件中的默认镜头缩放速度
+	 * 
+	 * @param device  控制设备
+	 * @param channelId  预览通道
+     * @param inOut      镜头放大缩小 0:停止 1:缩小 2:放大
+	 */
+	void ptzZoomCmd(Device device,String channelId,int inOut) throws InvalidArgumentException, ParseException, SipException;
+	
+	/**
+	 * 云台缩放控制
+	 * 
+	 * @param device  控制设备
+	 * @param channelId  预览通道
+     * @param inOut      镜头放大缩小 0:停止 1:缩小 2:放大
+	 */
+	void ptzZoomCmd(Device device,String channelId,int inOut, int moveSpeed) throws InvalidArgumentException, ParseException, SipException;
+	
+	/**
+	 * 云台控制,支持方向与缩放控制
+	 * 
+	 * @param device  控制设备
+	 * @param channelId  预览通道
+	 * @param leftRight  镜头左移右移 0:停止 1:左移 2:右移
+     * @param upDown     镜头上移下移 0:停止 1:上移 2:下移
+     * @param inOut      镜头放大缩小 0:停止 1:缩小 2:放大
+     * @param moveSpeed  镜头移动速度
+     * @param zoomSpeed  镜头缩放速度
+	 */
+	void ptzCmd(Device device,String channelId,int leftRight, int upDown, int inOut, int moveSpeed, int zoomSpeed) throws InvalidArgumentException, SipException, ParseException;
+	
+	/**
+	 * 前端控制,包括PTZ指令、FI指令、预置位指令、巡航指令、扫描指令和辅助开关指令
+	 * 
+	 * @param device  		控制设备
+	 * @param channelId		预览通道
+	 * @param cmdCode		指令码
+     * @param parameter1	数据1
+     * @param parameter2	数据2
+     * @param combineCode2	组合码2
+	 */
+	void frontEndCmd(Device device, String channelId, int cmdCode, int parameter1, int parameter2, int combineCode2) throws SipException, InvalidArgumentException, ParseException;
+	
+	/**
+	 * 前端控制指令(用于转发上级指令)
+	 * @param device		控制设备
+	 * @param channelId		预览通道
+	 * @param cmdString		前端控制指令串
+	 */
+	void fronEndCmd(Device device, String channelId, String cmdString, SipSubscribe.Event errorEvent, SipSubscribe.Event okEvent) throws InvalidArgumentException, SipException, ParseException;
+
+	/**
+	 * 请求预览视频流
+	 * @param device  视频设备
+	 * @param channelId  预览通道
+	 */
+	void playStreamCmd(MediaServerItem mediaServerItem, SSRCInfo ssrcInfo, Device device, String channelId, ZlmHttpHookSubscribe.Event event, SipSubscribe.Event okEvent, SipSubscribe.Event errorEvent) throws InvalidArgumentException, SipException, ParseException;
+
+	/**
+	 * 请求回放视频流
+	 * 
+	 * @param device  视频设备
+	 * @param channelId  预览通道
+	 * @param startTime 开始时间,格式要求:yyyy-MM-dd HH:mm:ss
+	 * @param endTime 结束时间,格式要求:yyyy-MM-dd HH:mm:ss
+	 */
+	void playbackStreamCmd(MediaServerItem mediaServerItem, SSRCInfo ssrcInf, Device device, String channelId, String startTime, String endTime,ZlmHttpHookSubscribe.Event event, SipSubscribe.Event okEvent, SipSubscribe.Event errorEvent) throws InvalidArgumentException, SipException, ParseException;
+
+	/**
+	 * 请求历史媒体下载
+	 * 
+	 * @param device  视频设备
+	 * @param channelId  预览通道
+	 * @param startTime 开始时间,格式要求:yyyy-MM-dd HH:mm:ss
+	 * @param endTime 结束时间,格式要求:yyyy-MM-dd HH:mm:ss
+	 * @param downloadSpeed 下载倍速参数
+	 */ 
+	void downloadStreamCmd(MediaServerItem mediaServerItem, SSRCInfo ssrcInfo, Device device, String channelId,
+						   String startTime, String endTime, int downloadSpeed, ZlmHttpHookSubscribe.Event hookEvent,
+						   SipSubscribe.Event errorEvent,SipSubscribe.Event okEvent) throws InvalidArgumentException, SipException, ParseException;
+
+
+	/**
+	 * 视频流停止
+	 */
+	void streamByeCmd(Device device, String channelId, String stream, String callId, SipSubscribe.Event okEvent) throws InvalidArgumentException, SipException, ParseException, SsrcTransactionNotFoundException;
+
+	void talkStreamCmd(MediaServerItem mediaServerItem, SendRtpItem sendRtpItem, Device device, String channelId, String callId, ZlmHttpHookSubscribe.Event event, ZlmHttpHookSubscribe.Event eventForPush, SipSubscribe.Event okEvent, SipSubscribe.Event errorEvent) throws InvalidArgumentException, SipException, ParseException;
+
+
+	void streamByeCmd(Device device, String channelId, String stream, String callId) throws InvalidArgumentException, ParseException, SipException, SsrcTransactionNotFoundException;
+
+	void streamByeCmd(Device device, String channelId, SipTransactionInfo sipTransactionInfo, SipSubscribe.Event okEvent) throws InvalidArgumentException, SipException, ParseException, SsrcTransactionNotFoundException;
+
+	/**
+	 * 回放暂停
+	 */
+	void playPauseCmd(Device device, StreamInfo streamInfo) throws InvalidArgumentException, ParseException, SipException;
+
+	/**
+	 * 回放恢复
+	 */
+	void playResumeCmd(Device device, StreamInfo streamInfo) throws InvalidArgumentException, ParseException, SipException;
+
+	/**
+	 * 回放拖动播放
+	 */
+	void playSeekCmd(Device device, StreamInfo streamInfo, long seekTime) throws InvalidArgumentException, ParseException, SipException;
+
+	/**
+	 * 回放倍速播放
+	 */
+	void playSpeedCmd(Device device, StreamInfo streamInfo, Double speed) throws InvalidArgumentException, ParseException, SipException;
+	
+	/**
+	 * 回放控制
+	 * @param device
+	 * @param streamInfo
+	 * @param content
+	 */
+	void playbackControlCmd(Device device, StreamInfo streamInfo, String content,SipSubscribe.Event errorEvent, SipSubscribe.Event okEvent) throws SipException, InvalidArgumentException, ParseException;
+
+
+    /**
+	 * /**
+	 * 语音广播
+	 *
+	 * @param device 视频设备
+	 */
+	void audioBroadcastCmd(Device device, String channelId, SipSubscribe.Event okEvent, SipSubscribe.Event errorEvent) throws InvalidArgumentException, SipException, ParseException;
+
+	/**
+	 * 音视频录像控制
+	 * 
+	 * @param device  		视频设备
+	 * @param channelId  	预览通道
+	 * @param recordCmdStr	录像命令:Record / StopRecord
+	 */
+	void recordCmd(Device device, String channelId, String recordCmdStr, SipSubscribe.Event errorEvent, SipSubscribe.Event okEvent) throws InvalidArgumentException, SipException, ParseException;
+	
+	/**
+	 * 远程启动控制命令
+	 * 
+	 * @param device	视频设备
+	 */
+	void teleBootCmd(Device device) throws InvalidArgumentException, SipException, ParseException;
+
+	/**
+	 * 报警布防/撤防命令
+	 * 
+	 * @param device  	视频设备
+	 */
+	void guardCmd(Device device, String guardCmdStr, SipSubscribe.Event errorEvent, SipSubscribe.Event okEvent) throws InvalidArgumentException, SipException, ParseException;
+	
+	/**
+	 * 报警复位命令
+	 * 
+	 * @param device		视频设备
+	 * @param alarmMethod	报警方式(可选)
+	 * @param alarmType		报警类型(可选)
+	 */
+	void alarmCmd(Device device, String alarmMethod, String alarmType, SipSubscribe.Event errorEvent, SipSubscribe.Event okEvent) throws InvalidArgumentException, SipException, ParseException;
+	
+	/**
+	 * 强制关键帧命令,设备收到此命令应立刻发送一个IDR帧
+	 * 
+	 * @param device  视频设备
+	 * @param channelId  预览通道
+	 */
+	void iFrameCmd(Device device, String channelId) throws InvalidArgumentException, SipException, ParseException;
+
+	/**
+	 * 看守位控制命令
+	 *
+	 * @param device      视频设备
+	 * @param channelId      通道id,非通道则是设备本身
+	 * @param enabled     看守位使能:1 = 开启,0 = 关闭
+	 * @param resetTime   自动归位时间间隔,开启看守位时使用,单位:秒(s)
+	 * @param presetIndex 调用预置位编号,开启看守位时使用,取值范围0~255
+	 */
+	void homePositionCmd(Device device, String channelId, String enabled, String resetTime, String presetIndex, SipSubscribe.Event errorEvent,SipSubscribe.Event okEvent) throws InvalidArgumentException, SipException, ParseException;
+
+	/**
+	 * 设备配置命令
+	 * 
+	 * @param device  视频设备
+	 */
+	void deviceConfigCmd(Device device);
+	
+	/**
+	 * 设备配置命令:basicParam
+	 * 
+	 * @param device  			视频设备
+	 * @param channelId			通道编码(可选)
+	 * @param name				设备/通道名称(可选)
+	 * @param expiration		注册过期时间(可选)
+	 * @param heartBeatInterval	心跳间隔时间(可选)
+	 * @param heartBeatCount	心跳超时次数(可选)
+	 */  
+	void deviceBasicConfigCmd(Device device, String channelId, String name, String expiration, String heartBeatInterval, String heartBeatCount, SipSubscribe.Event errorEvent) throws InvalidArgumentException, SipException, ParseException;
+
+	/**
+	 * 查询设备状态
+	 * 
+	 * @param device 视频设备
+	 */
+	void deviceStatusQuery(Device device, SipSubscribe.Event errorEvent) throws InvalidArgumentException, SipException, ParseException;
+	
+	/**
+	 * 查询设备信息
+	 * 
+	 * @param device 视频设备
+	 * @return 
+	 */
+	void deviceInfoQuery(Device device) throws InvalidArgumentException, SipException, ParseException;
+	
+	/**
+	 * 查询目录列表
+	 * 
+	 * @param device 视频设备
+	 */
+	void catalogQuery(Device device, int sn, SipSubscribe.Event errorEvent) throws SipException, InvalidArgumentException, ParseException;
+	
+	/**
+	 * 查询录像信息
+	 * 
+	 * @param device 视频设备
+	 * @param startTime 开始时间,格式要求:yyyy-MM-dd HH:mm:ss
+	 * @param endTime 结束时间,格式要求:yyyy-MM-dd HH:mm:ss
+	 * @param sn
+	 */
+	void recordInfoQuery(Device device, String channelId, String startTime, String endTime, int sn,  Integer Secrecy, String type, SipSubscribe.Event okEvent, SipSubscribe.Event errorEvent) throws InvalidArgumentException, SipException, ParseException;
+	
+	/**
+	 * 查询报警信息
+	 * 
+	 * @param device		视频设备
+	 * @param startPriority	报警起始级别(可选)
+	 * @param endPriority	报警终止级别(可选)
+	 * @param alarmMethod	报警方式条件(可选)
+	 * @param alarmType		报警类型
+	 * @param startTime		报警发生起始时间(可选)
+	 * @param endTime		报警发生终止时间(可选)
+	 * @return				true = 命令发送成功
+	 */
+	void alarmInfoQuery(Device device, String startPriority, String endPriority, String alarmMethod,
+							String alarmType, String startTime, String endTime, SipSubscribe.Event errorEvent) throws InvalidArgumentException, SipException, ParseException;
+	
+	/**
+	 * 查询设备配置
+	 * 
+	 * @param device 		视频设备
+	 * @param channelId		通道编码(可选)
+	 * @param configType	配置类型:
+	 */
+	void deviceConfigQuery(Device device, String channelId, String configType,  SipSubscribe.Event errorEvent) throws InvalidArgumentException, SipException, ParseException;
+	
+	/**
+	 * 查询设备预置位置
+	 * 
+	 * @param device 视频设备
+	 */
+	void presetQuery(Device device, String channelId, SipSubscribe.Event errorEvent) throws InvalidArgumentException, SipException, ParseException;
+	
+	/**
+	 * 查询移动设备位置数据
+	 * 
+	 * @param device 视频设备
+	 */
+	void mobilePostitionQuery(Device device, SipSubscribe.Event errorEvent) throws InvalidArgumentException, SipException, ParseException;
+
+	/**
+	 * 订阅、取消订阅移动位置
+	 * 
+	 * @param device	视频设备
+	 * @return			true = 命令发送成功
+	 */
+	SIPRequest mobilePositionSubscribe(Device device, SIPRequest request, SipSubscribe.Event okEvent , SipSubscribe.Event errorEvent) throws InvalidArgumentException, SipException, ParseException;
+
+	/**
+	 * 订阅、取消订阅报警信息
+	 * @param device		视频设备
+	 * @param expires		订阅过期时间(0 = 取消订阅)
+	 * @param startPriority	报警起始级别(可选)
+	 * @param endPriority	报警终止级别(可选)
+	 * @param alarmType		报警类型
+	 * @param startTime		报警发生起始时间(可选)
+	 * @param endTime		报警发生终止时间(可选)
+	 * @return				true = 命令发送成功
+	 */
+	void alarmSubscribe(Device device, int expires, String startPriority, String endPriority, String alarmMethod, String startTime, String endTime) throws InvalidArgumentException, SipException, ParseException;
+
+	/**
+	 * 订阅、取消订阅目录信息
+	 * @param device		视频设备
+	 * @return				true = 命令发送成功
+	 */
+	SIPRequest catalogSubscribe(Device device, SIPRequest request, SipSubscribe.Event okEvent ,SipSubscribe.Event errorEvent) throws InvalidArgumentException, SipException, ParseException;
+
+	/**
+	 * 拉框控制命令
+	 *
+	 * @param device    控制设备
+	 * @param channelId 通道id
+	 * @param cmdString 前端控制指令串
+	 */
+	void dragZoomCmd(Device device, String channelId, String cmdString) throws InvalidArgumentException, SipException, ParseException;
+
+
+	/**
+	 * 向设备发送报警NOTIFY消息, 用于互联结构下,此时将设备当成一个平级平台看待
+	 * @param device 设备
+	 * @param deviceAlarm 报警信息信息
+	 * @return
+	 */
+	void sendAlarmMessage(Device device, DeviceAlarm deviceAlarm) throws InvalidArgumentException, SipException, ParseException;
+
+
+}

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

@@ -25,6 +25,8 @@ public interface ISIPCommanderForPlatform {
     void register(ParentPlatform parentPlatform, SipSubscribe.Event errorEvent , SipSubscribe.Event okEvent) throws InvalidArgumentException, ParseException, SipException;
 
     void register(ParentPlatform parentPlatform, SipTransactionInfo sipTransactionInfo, SipSubscribe.Event errorEvent , SipSubscribe.Event okEvent) throws InvalidArgumentException, ParseException, SipException;
+
+
     void register(ParentPlatform parentPlatform, SipTransactionInfo sipTransactionInfo, WWWAuthenticateHeader www, SipSubscribe.Event errorEvent , SipSubscribe.Event okEvent, boolean isRegister) throws SipException, InvalidArgumentException, ParseException;
 
     /**

+ 6 - 6
src/main/java/com/genersoft/iot/vmp/gb28181/transmit/cmd/SIPRequestHeaderPlarformProvider.java

@@ -21,10 +21,7 @@ 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.OnStreamChangedHookParam;
-import com.genersoft.iot.vmp.service.IMediaServerService;
-import com.genersoft.iot.vmp.service.IPlayService;
-import com.genersoft.iot.vmp.service.IStreamProxyService;
-import com.genersoft.iot.vmp.service.IStreamPushService;
+import com.genersoft.iot.vmp.service.*;
 import com.genersoft.iot.vmp.service.bean.ErrorCallback;
 import com.genersoft.iot.vmp.service.bean.InviteErrorCode;
 import com.genersoft.iot.vmp.service.bean.MessageForPushChannel;
@@ -83,6 +80,9 @@ public class InviteRequestProcessor extends SIPRequestProcessorParent implements
     @Autowired
     private IRedisCatchStorage redisCatchStorage;
 
+    @Autowired
+    private IInviteStreamService inviteStreamService;
+
     @Autowired
     private SSRCFactory ssrcFactory;
 
@@ -518,10 +518,10 @@ public class InviteRequestProcessor extends SIPRequestProcessorParent implements
                                         errorEvent.run(code, msg, data);
                                     }
                                 });
-                    }else {
+                    } else {
 
                         SSRCInfo ssrcInfo = playService.play(mediaServerItem, device.getDeviceId(), channelId, ssrc, ((code, msg, data) -> {
-                            if (code == InviteErrorCode.SUCCESS.getCode()){
+                            if (code == InviteErrorCode.SUCCESS.getCode()) {
                                 hookEvent.run(code, msg, data);
                             } else if (code == InviteErrorCode.ERROR_FOR_SIGNALLING_TIMEOUT.getCode() || code == InviteErrorCode.ERROR_FOR_STREAM_TIMEOUT.getCode()) {
                                 logger.info("[上级点播]超时, 用户:{}, 通道:{}", username, channelId);

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

@@ -132,7 +132,7 @@ public class NotifyRequestProcessor extends SIPRequestProcessorParent implements
 
 						if (CmdType.CATALOG.equals(cmd)) {
 							logger.info("接收到Catalog通知");
-//							processNotifyCatalogList(take.getEvt());
+							processNotifyCatalogList(take.getEvt());
 							notifyRequestForCatalogProcessor.process(take.getEvt());
 						} else if (CmdType.ALARM.equals(cmd)) {
 							logger.info("接收到Alarm通知");
@@ -319,6 +319,7 @@ public class NotifyRequestProcessor extends SIPRequestProcessorParent implements
 			logger.info("[收到Notify-Alarm]:{}/{}", device.getDeviceId(), deviceAlarm.getChannelId());
 			if ("4".equals(deviceAlarm.getAlarmMethod())) {
 				MobilePosition mobilePosition = new MobilePosition();
+				mobilePosition.setChannelId(channelId);
 				mobilePosition.setCreateTime(DateUtil.getNow());
 				mobilePosition.setDeviceId(deviceAlarm.getDeviceId());
 				mobilePosition.setTime(deviceAlarm.getAlarmTime());

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

@@ -88,7 +88,11 @@ public class RegisterRequestProcessor extends SIPRequestProcessorParent implemen
             Response response = null;
             boolean passwordCorrect = false;
             // 注册标志
-            boolean registerFlag;
+            boolean registerFlag = true;
+            if (request.getExpires().getExpires() == 0) {
+                // 注销成功
+                registerFlag = false;
+            }
             FromHeader fromHeader = (FromHeader) request.getHeader(FromHeader.NAME);
             AddressImpl address = (AddressImpl) fromHeader.getAddress();
             SipUri uri = (SipUri) address.getURI();
@@ -99,11 +103,12 @@ public class RegisterRequestProcessor extends SIPRequestProcessorParent implemen
             RemoteAddressInfo remoteAddressInfo = SipUtils.getRemoteAddressFromRequest(request,
                     userSetting.getSipUseSourceIpAsRemoteAddress());
             String requestAddress = remoteAddressInfo.getIp() + ":" + remoteAddressInfo.getPort();
-                    logger.info("[注册请求] 设备:{}, 开始处理: {}", deviceId, requestAddress);
+            String title = registerFlag ? "[注册请求]": "[注销请求]";
+                    logger.info(title + "设备:{}, 开始处理: {}", deviceId, requestAddress);
             if (device != null &&
                 device.getSipTransactionInfo() != null &&
                 request.getCallIdHeader().getCallId().equals(device.getSipTransactionInfo().getCallId())) {
-                logger.info("[注册请求] 设备:{}, 注册续订: {}",device.getDeviceId(), device.getDeviceId());
+                logger.info(title + "设备:{}, 注册续订: {}",device.getDeviceId(), device.getDeviceId());
                 device.setExpires(request.getExpires().getExpires());
                 device.setIp(remoteAddressInfo.getIp());
                 device.setPort(remoteAddressInfo.getPort());
@@ -123,7 +128,7 @@ public class RegisterRequestProcessor extends SIPRequestProcessorParent implemen
             String password = (device != null && !ObjectUtils.isEmpty(device.getPassword()))? device.getPassword() : sipConfig.getPassword();
             AuthorizationHeader authHead = (AuthorizationHeader) request.getHeader(AuthorizationHeader.NAME);
             if (authHead == null && !ObjectUtils.isEmpty(password)) {
-                logger.info("[注册请求] 设备:{}, 回复401: {}",deviceId, requestAddress);
+                logger.info(title + " 设备:{}, 回复401: {}",deviceId, requestAddress);
                 response = getMessageFactory().createResponse(Response.UNAUTHORIZED, request);
                 new DigestServerAuthenticationHelper().generateChallenge(getHeaderFactory(), response, sipConfig.getDomain());
                 sipSender.transmitRequest(request.getLocalAddress().getHostAddress(), response);
@@ -138,7 +143,7 @@ public class RegisterRequestProcessor extends SIPRequestProcessorParent implemen
                 // 注册失败
                 response = getMessageFactory().createResponse(Response.FORBIDDEN, request);
                 response.setReasonPhrase("wrong password");
-                logger.info("[注册请求] 设备:{}, 密码/SIP服务器ID错误, 回复403: {}", deviceId, requestAddress);
+                logger.info(title + " 设备:{}, 密码/SIP服务器ID错误, 回复403: {}", deviceId, requestAddress);
                 sipSender.transmitRequest(request.getLocalAddress().getHostAddress(), response);
                 return;
             }

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

@@ -1,149 +1,149 @@
-package com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.info;
-
-import com.genersoft.iot.vmp.common.InviteInfo;
-import com.genersoft.iot.vmp.common.InviteSessionType;
-import com.genersoft.iot.vmp.gb28181.bean.*;
-import com.genersoft.iot.vmp.gb28181.event.SipSubscribe;
-import com.genersoft.iot.vmp.gb28181.session.VideoStreamSessionManager;
-import com.genersoft.iot.vmp.gb28181.transmit.SIPProcessorObserver;
-import com.genersoft.iot.vmp.gb28181.transmit.cmd.impl.SIPCommander;
-import com.genersoft.iot.vmp.gb28181.transmit.event.request.ISIPRequestProcessor;
-import com.genersoft.iot.vmp.gb28181.transmit.event.request.SIPRequestProcessorParent;
-import com.genersoft.iot.vmp.gb28181.utils.SipUtils;
-import com.genersoft.iot.vmp.service.IInviteStreamService;
-import com.genersoft.iot.vmp.storager.IRedisCatchStorage;
-import com.genersoft.iot.vmp.storager.IVideoManagerStorage;
-import gov.nist.javax.sip.message.SIPRequest;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import org.springframework.beans.factory.InitializingBean;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.stereotype.Component;
-
-import javax.sip.InvalidArgumentException;
-import javax.sip.RequestEvent;
-import javax.sip.SipException;
-import javax.sip.header.CallIdHeader;
-import javax.sip.header.ContentTypeHeader;
-import javax.sip.message.Response;
-import java.text.ParseException;
-
-@Component
-public class InfoRequestProcessor extends SIPRequestProcessorParent implements InitializingBean, ISIPRequestProcessor {
-
-    private final static Logger logger = LoggerFactory.getLogger(InfoRequestProcessor.class);
-
-    private final String method = "INFO";
-
-    @Autowired
-    private SIPProcessorObserver sipProcessorObserver;
-
-    @Autowired
-    private IVideoManagerStorage storage;
-
-    @Autowired
-    private SipSubscribe sipSubscribe;
-
-    @Autowired
-    private IRedisCatchStorage redisCatchStorage;
-
-    @Autowired
-    private IInviteStreamService inviteStreamService;
-
-    @Autowired
-    private IVideoManagerStorage storager;
-
-    @Autowired
-    private SIPCommander cmder;
-
-    @Autowired
-    private VideoStreamSessionManager sessionManager;
-
-    @Override
-    public void afterPropertiesSet() throws Exception {
-        // 添加消息处理的订阅
-        sipProcessorObserver.addRequestProcessor(method, this);
-    }
-
-    @Override
-    public void process(RequestEvent evt) {
-        logger.debug("接收到消息:" + evt.getRequest());
-        SIPRequest request = (SIPRequest) evt.getRequest();
-        String deviceId = SipUtils.getUserIdFromFromHeader(request);
-        CallIdHeader callIdHeader = request.getCallIdHeader();
-        // 先从会话内查找
-        SsrcTransaction ssrcTransaction = sessionManager.getSsrcTransaction(null, null, callIdHeader.getCallId(), null);
-
-        // 兼容海康 媒体通知 消息from字段不是设备ID的问题
-        if (ssrcTransaction != null) {
-            deviceId = ssrcTransaction.getDeviceId();
-        }
-        // 查询设备是否存在
-        Device device = redisCatchStorage.getDevice(deviceId);
-        // 查询上级平台是否存在
-        ParentPlatform parentPlatform = storage.queryParentPlatByServerGBId(deviceId);
-        try {
-            if (device != null && parentPlatform != null) {
-                logger.warn("[重复]平台与设备编号重复:{}", deviceId);
-                String hostAddress = request.getRemoteAddress().getHostAddress();
-                int remotePort = request.getRemotePort();
-                if (device.getHostAddress().equals(hostAddress + ":" + remotePort)) {
-                    parentPlatform = null;
-                }else {
-                    device = null;
-                }
-            }
-            if (device == null && parentPlatform == null) {
-                // 不存在则回复404
-                responseAck(request, Response.NOT_FOUND, "device "+ deviceId +" not found");
-                logger.warn("[设备未找到 ]: {}", deviceId);
-                if (sipSubscribe.getErrorSubscribe(callIdHeader.getCallId()) != null){
-                    DeviceNotFoundEvent deviceNotFoundEvent = new DeviceNotFoundEvent(evt.getDialog());
-                    deviceNotFoundEvent.setCallId(callIdHeader.getCallId());
-                    SipSubscribe.EventResult eventResult = new SipSubscribe.EventResult(deviceNotFoundEvent);
-                    sipSubscribe.getErrorSubscribe(callIdHeader.getCallId()).response(eventResult);
-                };
-            }else {
-                ContentTypeHeader header = (ContentTypeHeader)evt.getRequest().getHeader(ContentTypeHeader.NAME);
-                String contentType = header.getContentType();
-                String contentSubType = header.getContentSubType();
-                if ("Application".equalsIgnoreCase(contentType) && "MANSRTSP".equalsIgnoreCase(contentSubType)) {
-                    SendRtpItem sendRtpItem = redisCatchStorage.querySendRTPServer(null, null, null, callIdHeader.getCallId());
-                    String streamId = sendRtpItem.getStream();
-                    InviteInfo inviteInfo = inviteStreamService.getInviteInfoByStream(InviteSessionType.PLAYBACK, streamId);
-                    if (null == inviteInfo) {
-                        responseAck(request, Response.NOT_FOUND, "stream " + streamId + " not found");
-                        return;
-                    }
-                    Device device1 = storager.queryVideoDevice(inviteInfo.getDeviceId());
-                    if (inviteInfo.getStreamInfo() != null) {
-                        cmder.playbackControlCmd(device1,inviteInfo.getStreamInfo(),new String(evt.getRequest().getRawContent()),eventResult -> {
-                            // 失败的回复
-                            try {
-                                responseAck(request, eventResult.statusCode, eventResult.msg);
-                            } catch (SipException | InvalidArgumentException | ParseException e) {
-                                logger.error("[命令发送失败] 国标级联 录像控制: {}", e.getMessage());
-                            }
-                        }, eventResult -> {
-                            // 成功的回复
-                            try {
-                                responseAck(request, eventResult.statusCode);
-                            } catch (SipException | InvalidArgumentException | ParseException e) {
-                                logger.error("[命令发送失败] 国标级联 录像控制: {}", e.getMessage());
-                            }
-                        });
-                    }
-
-                }
-            }
-        } catch (SipException e) {
-            logger.warn("SIP 回复错误", e);
-        } catch (InvalidArgumentException e) {
-            logger.warn("参数无效", e);
-        } catch (ParseException e) {
-            logger.warn("SIP回复时解析异常", e);
-        }
-    }
-
-
-}
+package com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.info;
+
+import com.genersoft.iot.vmp.common.InviteInfo;
+import com.genersoft.iot.vmp.common.InviteSessionType;
+import com.genersoft.iot.vmp.gb28181.bean.*;
+import com.genersoft.iot.vmp.gb28181.event.SipSubscribe;
+import com.genersoft.iot.vmp.gb28181.session.VideoStreamSessionManager;
+import com.genersoft.iot.vmp.gb28181.transmit.SIPProcessorObserver;
+import com.genersoft.iot.vmp.gb28181.transmit.cmd.impl.SIPCommander;
+import com.genersoft.iot.vmp.gb28181.transmit.event.request.ISIPRequestProcessor;
+import com.genersoft.iot.vmp.gb28181.transmit.event.request.SIPRequestProcessorParent;
+import com.genersoft.iot.vmp.gb28181.utils.SipUtils;
+import com.genersoft.iot.vmp.service.IInviteStreamService;
+import com.genersoft.iot.vmp.storager.IRedisCatchStorage;
+import com.genersoft.iot.vmp.storager.IVideoManagerStorage;
+import gov.nist.javax.sip.message.SIPRequest;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.InitializingBean;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+import javax.sip.InvalidArgumentException;
+import javax.sip.RequestEvent;
+import javax.sip.SipException;
+import javax.sip.header.CallIdHeader;
+import javax.sip.header.ContentTypeHeader;
+import javax.sip.message.Response;
+import java.text.ParseException;
+
+@Component
+public class InfoRequestProcessor extends SIPRequestProcessorParent implements InitializingBean, ISIPRequestProcessor {
+
+    private final static Logger logger = LoggerFactory.getLogger(InfoRequestProcessor.class);
+
+    private final String method = "INFO";
+
+    @Autowired
+    private SIPProcessorObserver sipProcessorObserver;
+
+    @Autowired
+    private IVideoManagerStorage storage;
+
+    @Autowired
+    private SipSubscribe sipSubscribe;
+
+    @Autowired
+    private IRedisCatchStorage redisCatchStorage;
+
+    @Autowired
+    private IInviteStreamService inviteStreamService;
+
+    @Autowired
+    private IVideoManagerStorage storager;
+
+    @Autowired
+    private SIPCommander cmder;
+
+    @Autowired
+    private VideoStreamSessionManager sessionManager;
+
+    @Override
+    public void afterPropertiesSet() throws Exception {
+        // 添加消息处理的订阅
+        sipProcessorObserver.addRequestProcessor(method, this);
+    }
+
+    @Override
+    public void process(RequestEvent evt) {
+        logger.debug("接收到消息:" + evt.getRequest());
+        SIPRequest request = (SIPRequest) evt.getRequest();
+        String deviceId = SipUtils.getUserIdFromFromHeader(request);
+        CallIdHeader callIdHeader = request.getCallIdHeader();
+        // 先从会话内查找
+        SsrcTransaction ssrcTransaction = sessionManager.getSsrcTransaction(null, null, callIdHeader.getCallId(), null);
+
+        // 兼容海康 媒体通知 消息from字段不是设备ID的问题
+        if (ssrcTransaction != null) {
+            deviceId = ssrcTransaction.getDeviceId();
+        }
+        // 查询设备是否存在
+        Device device = redisCatchStorage.getDevice(deviceId);
+        // 查询上级平台是否存在
+        ParentPlatform parentPlatform = storage.queryParentPlatByServerGBId(deviceId);
+        try {
+            if (device != null && parentPlatform != null) {
+                logger.warn("[重复]平台与设备编号重复:{}", deviceId);
+                String hostAddress = request.getRemoteAddress().getHostAddress();
+                int remotePort = request.getRemotePort();
+                if (device.getHostAddress().equals(hostAddress + ":" + remotePort)) {
+                    parentPlatform = null;
+                }else {
+                    device = null;
+                }
+            }
+            if (device == null && parentPlatform == null) {
+                // 不存在则回复404
+                responseAck(request, Response.NOT_FOUND, "device "+ deviceId +" not found");
+                logger.warn("[设备未找到 ]: {}", deviceId);
+                if (sipSubscribe.getErrorSubscribe(callIdHeader.getCallId()) != null){
+                    DeviceNotFoundEvent deviceNotFoundEvent = new DeviceNotFoundEvent(evt.getDialog());
+                    deviceNotFoundEvent.setCallId(callIdHeader.getCallId());
+                    SipSubscribe.EventResult eventResult = new SipSubscribe.EventResult(deviceNotFoundEvent);
+                    sipSubscribe.getErrorSubscribe(callIdHeader.getCallId()).response(eventResult);
+                };
+            }else {
+                ContentTypeHeader header = (ContentTypeHeader)evt.getRequest().getHeader(ContentTypeHeader.NAME);
+                String contentType = header.getContentType();
+                String contentSubType = header.getContentSubType();
+                if ("Application".equalsIgnoreCase(contentType) && "MANSRTSP".equalsIgnoreCase(contentSubType)) {
+                    SendRtpItem sendRtpItem = redisCatchStorage.querySendRTPServer(null, null, null, callIdHeader.getCallId());
+                    String streamId = sendRtpItem.getStream();
+                    InviteInfo inviteInfo = inviteStreamService.getInviteInfoByStream(InviteSessionType.PLAYBACK, streamId);
+                    if (null == inviteInfo) {
+                        responseAck(request, Response.NOT_FOUND, "stream " + streamId + " not found");
+                        return;
+                    }
+                    Device device1 = storager.queryVideoDevice(inviteInfo.getDeviceId());
+                    if (inviteInfo.getStreamInfo() != null) {
+                        cmder.playbackControlCmd(device1,inviteInfo.getStreamInfo(),new String(evt.getRequest().getRawContent()),eventResult -> {
+                            // 失败的回复
+                            try {
+                                responseAck(request, eventResult.statusCode, eventResult.msg);
+                            } catch (SipException | InvalidArgumentException | ParseException e) {
+                                logger.error("[命令发送失败] 国标级联 录像控制: {}", e.getMessage());
+                            }
+                        }, eventResult -> {
+                            // 成功的回复
+                            try {
+                                responseAck(request, eventResult.statusCode);
+                            } catch (SipException | InvalidArgumentException | ParseException e) {
+                                logger.error("[命令发送失败] 国标级联 录像控制: {}", e.getMessage());
+                            }
+                        });
+                    }
+
+                }
+            }
+        } catch (SipException e) {
+            logger.warn("SIP 回复错误", e);
+        } catch (InvalidArgumentException e) {
+            logger.warn("参数无效", e);
+        } catch (ParseException e) {
+            logger.warn("SIP回复时解析异常", e);
+        }
+    }
+
+
+}

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

@@ -3,7 +3,6 @@ package com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message;
 import com.genersoft.iot.vmp.gb28181.bean.Device;
 import com.genersoft.iot.vmp.gb28181.bean.ParentPlatform;
 import com.genersoft.iot.vmp.gb28181.transmit.event.request.SIPRequestProcessorParent;
-import gov.nist.javax.sip.message.SIPRequest;
 import org.dom4j.Element;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -24,6 +23,9 @@ public abstract class MessageHandlerAbstract extends SIPRequestProcessorParent i
 
     public Map<String, IMessageHandler> messageHandlerMap = new ConcurrentHashMap<>();
 
+    @Autowired
+    private IVideoManagerStorage storage;
+
     public void addHandler(String cmdType, IMessageHandler messageHandler) {
         messageHandlerMap.put(cmdType, messageHandler);
     }
@@ -40,7 +42,15 @@ public abstract class MessageHandlerAbstract extends SIPRequestProcessorParent i
             return;
         }
         IMessageHandler messageHandler = messageHandlerMap.get(cmd);
+
         if (messageHandler != null) {
+            //两个国标平台互相级联时由于上一步判断导致本该在平台处理的消息 放到了设备的处理逻辑
+            //所以对目录查询单独做了校验
+            if(messageHandler instanceof CatalogQueryMessageHandler){
+                ParentPlatform parentPlatform = storage.queryParentPlatByServerGBId(device.getDeviceId());
+                messageHandler.handForPlatform(evt, parentPlatform, element);
+                return;
+            }
             messageHandler.handForDevice(evt, device, element);
         }
     }

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

@@ -137,6 +137,7 @@ public class AlarmNotifyMessageHandler extends SIPRequestProcessorParent impleme
                                 MobilePosition mobilePosition = new MobilePosition();
                                 mobilePosition.setCreateTime(DateUtil.getNow());
                                 mobilePosition.setDeviceId(deviceAlarm.getDeviceId());
+                                mobilePosition.setChannelId(channelId);
                                 mobilePosition.setTime(deviceAlarm.getAlarmTime());
                                 mobilePosition.setLongitude(deviceAlarm.getLongitude());
                                 mobilePosition.setLatitude(deviceAlarm.getLatitude());

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

@@ -13,6 +13,7 @@ import com.genersoft.iot.vmp.gb28181.utils.SipUtils;
 import com.genersoft.iot.vmp.service.IDeviceService;
 import com.genersoft.iot.vmp.utils.DateUtil;
 import gov.nist.javax.sip.message.SIPRequest;
+import org.apache.commons.lang3.ObjectUtils;
 import org.dom4j.Element;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -68,7 +69,7 @@ public class KeepaliveNotifyMessageHandler extends SIPRequestProcessorParent imp
         } catch (SipException | InvalidArgumentException | ParseException e) {
             logger.error("[命令发送失败] 心跳回复: {}", e.getMessage());
         }
-        if (device.getKeepaliveTime() != null && DateUtil.getDifferenceForNow(device.getKeepaliveTime()) <= 3000L){
+        if (!ObjectUtils.isEmpty(device.getKeepaliveTime()) && DateUtil.getDifferenceForNow(device.getKeepaliveTime()) <= 3000L) {
             logger.info("[收到心跳] 心跳发送过于频繁,已忽略 device: {}, callId: {}", device.getDeviceId(), request.getCallIdHeader().getCallId());
             return;
         }
@@ -109,7 +110,11 @@ public class KeepaliveNotifyMessageHandler extends SIPRequestProcessorParent imp
 
     @Override
     public void handForPlatform(RequestEvent evt, ParentPlatform parentPlatform, Element element) {
-        // 不会收到上级平台的心跳信息
-
+        // 个别平台保活不回复200OK会判定离线
+        try {
+            responseAck((SIPRequest) evt.getRequest(), Response.OK);
+        } catch (SipException | InvalidArgumentException | ParseException e) {
+            logger.error("[命令发送失败] 心跳回复: {}", e.getMessage());
+        }
     }
 }

+ 6 - 4
src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/notify/cmd/MediaStatusNotifyMessageHandler.java

@@ -30,6 +30,7 @@ import java.util.ArrayList;
 import java.util.Iterator;
 import java.util.List;
 import java.util.concurrent.ConcurrentLinkedQueue;
+import java.util.concurrent.atomic.AtomicBoolean;
 
 /**
  * 目录查询的回复
@@ -61,6 +62,7 @@ public class CatalogResponseMessageHandler extends SIPRequestProcessorParent imp
 
     @Autowired
     private SipConfig sipConfig;
+    private AtomicBoolean processing = new AtomicBoolean(false);
 
     @Override
     public void afterPropertiesSet() throws Exception {
@@ -69,7 +71,6 @@ public class CatalogResponseMessageHandler extends SIPRequestProcessorParent imp
 
     @Override
     public void handForDevice(RequestEvent evt, Device device, Element element) {
-        boolean isEmpty = taskQueue.isEmpty();
         taskQueue.offer(new HandlerCatchData(evt, device, element));
         // 回复200 OK
         try {
@@ -77,8 +78,8 @@ public class CatalogResponseMessageHandler extends SIPRequestProcessorParent imp
         } catch (SipException | InvalidArgumentException | ParseException e) {
             logger.error("[命令发送失败] 目录查询回复: {}", e.getMessage());
         }
-        // 如果不为空则说明已经开启消息处理
-        if (isEmpty) {
+        // 已经开启消息处理则跳过
+        if (processing.compareAndSet(false, true)) {
             taskExecutor.execute(() -> {
                 while (!taskQueue.isEmpty()) {
                     // 全局异常捕获,保证下一条可以得到处理
@@ -147,11 +148,12 @@ public class CatalogResponseMessageHandler extends SIPRequestProcessorParent imp
                             }
 
                         }
-                    }catch (Exception e) {
+                    } catch (Exception e) {
                         logger.warn("[收到通道] 发现未处理的异常, \r\n{}", evt.getRequest());
                         logger.error("[收到通道] 异常内容: ", e);
                     }
                 }
+                processing.set(false);
             });
         }
 

+ 646 - 646
src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/response/cmd/ConfigDownloadResponseMessageHandler.java

@@ -1,647 +1,647 @@
-package com.genersoft.iot.vmp.gb28181.utils;
-
-import com.alibaba.fastjson2.JSONArray;
-import com.alibaba.fastjson2.JSONObject;
-import com.genersoft.iot.vmp.common.CivilCodePo;
-import com.genersoft.iot.vmp.conf.CivilCodeFileConf;
-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.math.NumberUtils;
-import org.dom4j.Attribute;
-import org.dom4j.Document;
-import org.dom4j.DocumentException;
-import org.dom4j.Element;
-import org.dom4j.io.SAXReader;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import org.springframework.util.ObjectUtils;
-import org.springframework.util.ReflectionUtils;
-
-import javax.sip.RequestEvent;
-import javax.sip.message.Request;
-import java.io.ByteArrayInputStream;
-import java.io.StringReader;
-import java.lang.reflect.Field;
-import java.lang.reflect.InvocationTargetException;
-import java.lang.reflect.ParameterizedType;
-import java.lang.reflect.Type;
-import java.util.*;
-
-/**
- * 基于dom4j的工具包
- *
- *
- */
-public class XmlUtil {
-    /**
-     * 日志服务
-     */
-    private static Logger logger = LoggerFactory.getLogger(XmlUtil.class);
-
-    /**
-     * 解析XML为Document对象
-     *
-     * @param xml 被解析的XMl
-     *
-     * @return Document
-     */
-    public static Element parseXml(String xml) {
-        Document document = null;
-        //
-        StringReader sr = new StringReader(xml);
-        SAXReader saxReader = new SAXReader();
-        try {
-            document = saxReader.read(sr);
-        } catch (DocumentException e) {
-            logger.error("解析失败", e);
-        }
-        return null == document ? null : document.getRootElement();
-    }
-
-    /**
-     * 获取element对象的text的值
-     *
-     * @param em  节点的对象
-     * @param tag 节点的tag
-     * @return 节点
-     */
-    public static String getText(Element em, String tag) {
-        if (null == em) {
-            return null;
-        }
-        Element e = em.element(tag);
-        //
-        return null == e ? null : e.getText().trim();
-    }
-
-    /**
-     * 递归解析xml节点,适用于 多节点数据
-     *
-     * @param node     node
-     * @param nodeName nodeName
-     * @return List<Map<String, Object>>
-     */
-    public static List<Map<String, Object>> listNodes(Element node, String nodeName) {
-        if (null == node) {
-            return null;
-        }
-        // 初始化返回
-        List<Map<String, Object>> listMap = new ArrayList<Map<String, Object>>();
-        // 首先获取当前节点的所有属性节点
-        List<Attribute> list = node.attributes();
-
-        Map<String, Object> map = null;
-        // 遍历属性节点
-        for (Attribute attribute : list) {
-            if (nodeName.equals(node.getName())) {
-                if (null == map) {
-                    map = new HashMap<String, Object>();
-                    listMap.add(map);
-                }
-                // 取到的节点属性放到map中
-                map.put(attribute.getName(), attribute.getValue());
-            }
-
-        }
-        // 遍历当前节点下的所有节点 ,nodeName 要解析的节点名称
-        // 使用递归
-        Iterator<Element> iterator = node.elementIterator();
-        while (iterator.hasNext()) {
-            Element e = iterator.next();
-            listMap.addAll(listNodes(e, nodeName));
-        }
-        return listMap;
-    }
-
-    /**
-     * xml转json
-     *
-     * @param element
-     * @param json
-     */
-    public static void node2Json(Element element, JSONObject json) {
-        // 如果是属性
-        for (Object o : element.attributes()) {
-            Attribute attr = (Attribute) o;
-            if (!ObjectUtils.isEmpty(attr.getValue())) {
-                json.put("@" + attr.getName(), attr.getValue());
-            }
-        }
-        List<Element> chdEl = element.elements();
-        if (chdEl.isEmpty() && !ObjectUtils.isEmpty(element.getText())) {// 如果没有子元素,只有一个值
-            json.put(element.getName(), element.getText());
-        }
-
-        for (Element e : chdEl) {   // 有子元素
-            if (!e.elements().isEmpty()) {  // 子元素也有子元素
-                JSONObject chdjson = new JSONObject();
-                node2Json(e, chdjson);
-                Object o = json.get(e.getName());
-                if (o != null) {
-                    JSONArray jsona = null;
-                    if (o instanceof JSONObject) {  // 如果此元素已存在,则转为jsonArray
-                        JSONObject jsono = (JSONObject) o;
-                        json.remove(e.getName());
-                        jsona = new JSONArray();
-                        jsona.add(jsono);
-                        jsona.add(chdjson);
-                    }
-                    if (o instanceof JSONArray) {
-                        jsona = (JSONArray) o;
-                        jsona.add(chdjson);
-                    }
-                    json.put(e.getName(), jsona);
-                } else {
-                    if (!chdjson.isEmpty()) {
-                        json.put(e.getName(), chdjson);
-                    }
-                }
-            } else { // 子元素没有子元素
-                for (Object o : element.attributes()) {
-                    Attribute attr = (Attribute) o;
-                    if (!ObjectUtils.isEmpty(attr.getValue())) {
-                        json.put("@" + attr.getName(), attr.getValue());
-                    }
-                }
-                if (!e.getText().isEmpty()) {
-                    json.put(e.getName(), e.getText());
-                }
-            }
-        }
-    }
-    public static  Element getRootElement(RequestEvent evt) throws DocumentException {
-
-        return getRootElement(evt, "gb2312");
-    }
-
-    public static Element getRootElement(RequestEvent evt, String charset) throws DocumentException {
-        Request request = evt.getRequest();
-        return getRootElement(request.getRawContent(), charset);
-    }
-
-    public static Element getRootElement(byte[] content, String charset) throws DocumentException {
-        if (charset == null) {
-            charset = "gb2312";
-        }
-        SAXReader reader = new SAXReader();
-        reader.setEncoding(charset);
-        Document xml = reader.read(new ByteArrayInputStream(content));
-        return xml.getRootElement();
-    }
-
-    private enum ChannelType{
-        CivilCode, BusinessGroup,VirtualOrganization,Other
-    }
-
-    public static DeviceChannel channelContentHandler(Element itemDevice, Device device, String event, CivilCodeFileConf civilCodeFileConf){
-        DeviceChannel deviceChannel = new DeviceChannel();
-        deviceChannel.setDeviceId(device.getDeviceId());
-        Element channdelIdElement = itemDevice.element("DeviceID");
-        if (channdelIdElement == null) {
-            logger.warn("解析Catalog消息时发现缺少 DeviceID");
-            return null;
-        }
-        String channelId = channdelIdElement.getTextTrim();
-        if (ObjectUtils.isEmpty(channelId)) {
-            logger.warn("解析Catalog消息时发现缺少 DeviceID");
-            return null;
-        }
-        deviceChannel.setChannelId(channelId);
-        if (event != null && !event.equals(CatalogEvent.ADD) && !event.equals(CatalogEvent.UPDATE)) {
-            // 除了ADD和update情况下需要识别全部内容,
-            return deviceChannel;
-        }
-        Element nameElement = itemDevice.element("Name");
-        if (nameElement != null) {
-            deviceChannel.setName(nameElement.getText());
-        }
-        if(channelId.length() <= 8) {
-            deviceChannel.setHasAudio(false);
-            CivilCodePo parentCode = civilCodeFileConf.getParentCode(channelId);
-            if (parentCode != null) {
-                deviceChannel.setParentId(parentCode.getCode());
-                deviceChannel.setCivilCode(parentCode.getCode());
-            }else {
-                logger.warn("[xml解析] 无法确定行政区划{}的上级行政区划", channelId);
-            }
-            deviceChannel.setStatus(true);
-            return deviceChannel;
-        }else {
-            if(channelId.length() != 20) {
-                logger.warn("[xml解析] 失败,编号不符合国标28181定义: {}", channelId);
-                return null;
-            }
-
-            int code = Integer.parseInt(channelId.substring(10, 13));
-            if (code == 136 || code == 137 || code == 138) {
-                deviceChannel.setHasAudio(true);
-            }else {
-                deviceChannel.setHasAudio(false);
-            }
-            // 设备厂商
-            String manufacturer = getText(itemDevice, "Manufacturer");
-            // 设备型号
-            String model = getText(itemDevice, "Model");
-            // 设备归属
-            String owner = getText(itemDevice, "Owner");
-            // 行政区域
-            String civilCode = getText(itemDevice, "CivilCode");
-            // 虚拟组织所属的业务分组ID,业务分组根据特定的业务需求制定,一个业务分组包含一组特定的虚拟组织
-            String businessGroupID = getText(itemDevice, "BusinessGroupID");
-            // 父设备/区域/系统ID
-            String parentID = getText(itemDevice, "ParentID");
-            if (parentID != null && parentID.equalsIgnoreCase("null")) {
-                parentID = null;
-            }
-            // 注册方式(必选)缺省为1;1:符合IETFRFC3261标准的认证注册模式;2:基于口令的双向认证注册模式;3:基于数字证书的双向认证注册模式
-            String registerWay = getText(itemDevice, "RegisterWay");
-            // 保密属性(必选)缺省为0;0:不涉密,1:涉密
-            String secrecy = getText(itemDevice, "Secrecy");
-            // 安装地址
-            String address = getText(itemDevice, "Address");
-
-            switch (code){
-                case 200:
-                    // 系统目录
-                    if (!ObjectUtils.isEmpty(manufacturer)) {
-                        deviceChannel.setManufacture(manufacturer);
-                    }
-                    if (!ObjectUtils.isEmpty(model)) {
-                        deviceChannel.setModel(model);
-                    }
-                    if (!ObjectUtils.isEmpty(owner)) {
-                        deviceChannel.setOwner(owner);
-                    }
-                    if (!ObjectUtils.isEmpty(civilCode)) {
-                        deviceChannel.setCivilCode(civilCode);
-                        deviceChannel.setParentId(civilCode);
-                    }else {
-                        if (!ObjectUtils.isEmpty(parentID)) {
-                            deviceChannel.setParentId(parentID);
-                        }
-                    }
-                    if (!ObjectUtils.isEmpty(address)) {
-                        deviceChannel.setAddress(address);
-                    }
-                    deviceChannel.setStatus(true);
-                    if (!ObjectUtils.isEmpty(registerWay)) {
-                        try {
-                            deviceChannel.setRegisterWay(Integer.parseInt(registerWay));
-                        }catch (NumberFormatException exception) {
-                            logger.warn("[xml解析] 从通道数据获取registerWay失败: {}", registerWay);
-                        }
-                    }
-                    if (!ObjectUtils.isEmpty(secrecy)) {
-                        deviceChannel.setSecrecy(secrecy);
-                    }
-                    return deviceChannel;
-                case 215:
-                    // 业务分组
-                    deviceChannel.setStatus(true);
-                    if (!ObjectUtils.isEmpty(parentID)) {
-                        if (!parentID.trim().equalsIgnoreCase(device.getDeviceId())) {
-                            deviceChannel.setParentId(parentID);
-                        }
-                    }else {
-                        logger.warn("[xml解析] 业务分组数据中缺少关键信息->ParentId");
-                        if (!ObjectUtils.isEmpty(civilCode)) {
-                            deviceChannel.setCivilCode(civilCode);
-                        }
-                    }
-                    break;
-                case 216:
-                    // 虚拟组织
-                    deviceChannel.setStatus(true);
-                    if (!ObjectUtils.isEmpty(businessGroupID)) {
-                        deviceChannel.setBusinessGroupId(businessGroupID);
-                    }
-
-                    if (!ObjectUtils.isEmpty(parentID)) {
-                        if (parentID.contains("/")) {
-                            String[] parentIdArray = parentID.split("/");
-                            parentID = parentIdArray[parentIdArray.length - 1];
-                        }
-                        deviceChannel.setParentId(parentID);
-                    }else {
-                        if (!ObjectUtils.isEmpty(businessGroupID)) {
-                            deviceChannel.setParentId(businessGroupID);
-                        }
-                    }
-                    break;
-                default:
-                    // 设备目录
-                    if (!ObjectUtils.isEmpty(manufacturer)) {
-                        deviceChannel.setManufacture(manufacturer);
-                    }
-                    if (!ObjectUtils.isEmpty(model)) {
-                        deviceChannel.setModel(model);
-                    }
-                    if (!ObjectUtils.isEmpty(owner)) {
-                        deviceChannel.setOwner(owner);
-                    }
-                    if (!ObjectUtils.isEmpty(civilCode)
-                            && civilCode.length() <= 8
-                            && NumberUtils.isParsable(civilCode)
-                            && civilCode.length()%2 == 0
-                    ) {
-                        deviceChannel.setCivilCode(civilCode);
-                    }
-                    if (!ObjectUtils.isEmpty(businessGroupID)) {
-                        deviceChannel.setBusinessGroupId(businessGroupID);
-                    }
-
-                    // 警区
-                    String block = getText(itemDevice, "Block");
-                    if (!ObjectUtils.isEmpty(block)) {
-                        deviceChannel.setBlock(block);
-                    }
-                    if (!ObjectUtils.isEmpty(address)) {
-                        deviceChannel.setAddress(address);
-                    }
-
-                    if (!ObjectUtils.isEmpty(secrecy)) {
-                        deviceChannel.setSecrecy(secrecy);
-                    }
-
-                    // 当为设备时,是否有子设备(必选)1有,0没有
-                    String parental = getText(itemDevice, "Parental");
-                    if (!ObjectUtils.isEmpty(parental)) {
-                        try {
-                            // 由于海康会错误的发送65535作为这里的取值,所以这里除非是0否则认为是1
-                            if (!ObjectUtils.isEmpty(parental) && parental.length() == 1 && Integer.parseInt(parental) == 0) {
-                                deviceChannel.setParental(0);
-                            }else {
-                                deviceChannel.setParental(1);
-                            }
-                        }catch (NumberFormatException e) {
-                            logger.warn("[xml解析] 从通道数据获取 parental失败: {}", parental);
-                        }
-                    }
-                    // 父设备/区域/系统ID
-
-                    if (!ObjectUtils.isEmpty(parentID) ) {
-                        if (parentID.contains("/")) {
-                            String[] parentIdArray = parentID.split("/");
-                            deviceChannel.setParentId(parentIdArray[parentIdArray.length - 1]);
-                        }else {
-                            if (parentID.length()%2 == 0) {
-                                deviceChannel.setParentId(parentID);
-                            }else {
-                                logger.warn("[xml解析] 不规范的parentID:{}, 已舍弃", parentID);
-                            }
-                        }
-                    }else {
-                        if (!ObjectUtils.isEmpty(businessGroupID)) {
-                            deviceChannel.setParentId(businessGroupID);
-                        }else {
-                            if (!ObjectUtils.isEmpty(deviceChannel.getCivilCode())) {
-                                deviceChannel.setParentId(deviceChannel.getCivilCode());
-                            }
-                        }
-                    }
-                    // 注册方式
-                    if (!ObjectUtils.isEmpty(registerWay)) {
-                        try {
-                            int registerWayInt = Integer.parseInt(registerWay);
-                            deviceChannel.setRegisterWay(registerWayInt);
-                        }catch (NumberFormatException exception) {
-                            logger.warn("[xml解析] 从通道数据获取registerWay失败: {}", registerWay);
-                            deviceChannel.setRegisterWay(1);
-                        }
-                    }else {
-                        deviceChannel.setRegisterWay(1);
-                    }
-
-                    // 信令安全模式(可选)缺省为0; 0:不采用;2:S/MIME 签名方式;3:S/MIME加密签名同时采用方式;4:数字摘要方式
-                    String safetyWay = getText(itemDevice, "SafetyWay");
-                    if (!ObjectUtils.isEmpty(safetyWay)) {
-                        try {
-                            deviceChannel.setSafetyWay(Integer.parseInt(safetyWay));
-                        }catch (NumberFormatException e) {
-                            logger.warn("[xml解析] 从通道数据获取 safetyWay失败: {}", safetyWay);
-                        }
-                    }
-
-                    // 证书序列号(有证书的设备必选)
-                    String certNum = getText(itemDevice, "CertNum");
-                    if (!ObjectUtils.isEmpty(certNum)) {
-                        deviceChannel.setCertNum(certNum);
-                    }
-
-                    // 证书有效标识(有证书的设备必选)缺省为0;证书有效标识:0:无效 1:有效
-                    String certifiable = getText(itemDevice, "Certifiable");
-                    if (!ObjectUtils.isEmpty(certifiable)) {
-                        try {
-                            deviceChannel.setCertifiable(Integer.parseInt(certifiable));
-                        }catch (NumberFormatException e) {
-                            logger.warn("[xml解析] 从通道数据获取 Certifiable失败: {}", certifiable);
-                        }
-                    }
-
-                    // 无效原因码(有证书且证书无效的设备必选)
-                    String errCode = getText(itemDevice, "ErrCode");
-                    if (!ObjectUtils.isEmpty(errCode)) {
-                        try {
-                            deviceChannel.setErrCode(Integer.parseInt(errCode));
-                        }catch (NumberFormatException e) {
-                            logger.warn("[xml解析] 从通道数据获取 ErrCode失败: {}", errCode);
-                        }
-                    }
-
-                    // 证书终止有效期(有证书的设备必选)
-                    String endTime = getText(itemDevice, "EndTime");
-                    if (!ObjectUtils.isEmpty(endTime)) {
-                        deviceChannel.setEndTime(endTime);
-                    }
-
-
-                    // 设备/区域/系统IP地址
-                    String ipAddress = getText(itemDevice, "IPAddress");
-                    if (!ObjectUtils.isEmpty(ipAddress)) {
-                        deviceChannel.setIpAddress(ipAddress);
-                    }
-
-                    // 设备/区域/系统端口
-                    String port = getText(itemDevice, "Port");
-                    if (!ObjectUtils.isEmpty(port)) {
-                        try {
-                            deviceChannel.setPort(Integer.parseInt(port));
-                        }catch (NumberFormatException e) {
-                            logger.warn("[xml解析] 从通道数据获取 Port失败: {}", port);
-                        }
-                    }
-
-                    // 设备口令
-                    String password = getText(itemDevice, "Password");
-                    if (!ObjectUtils.isEmpty(password)) {
-                        deviceChannel.setPassword(password);
-                    }
-
-
-                    // 设备状态
-                    String status = getText(itemDevice, "Status");
-                    if (status != null) {
-                        // ONLINE OFFLINE HIKVISION DS-7716N-E4 NVR的兼容性处理
-                        if (status.equals("ON") || status.equals("On") || status.equals("ONLINE") || status.equals("OK")) {
-                            deviceChannel.setStatus(true);
-                        }
-                        if (status.equals("OFF") || status.equals("Off") || status.equals("OFFLINE")) {
-                            deviceChannel.setStatus(false);
-                        }
-                    }else {
-                        deviceChannel.setStatus(true);
-                    }
-
-                    // 经度
-                    String longitude = getText(itemDevice, "Longitude");
-                    if (NumericUtil.isDouble(longitude)) {
-                        deviceChannel.setLongitude(Double.parseDouble(longitude));
-                    } else {
-                        deviceChannel.setLongitude(0.00);
-                    }
-
-                    // 纬度
-                    String latitude = getText(itemDevice, "Latitude");
-                    if (NumericUtil.isDouble(latitude)) {
-                        deviceChannel.setLatitude(Double.parseDouble(latitude));
-                    } else {
-                        deviceChannel.setLatitude(0.00);
-                    }
-
-                    deviceChannel.setGpsTime(DateUtil.getNow());
-
-                    // -摄像机类型扩展,标识摄像机类型:1-球机;2-半球;3-固定枪机;4-遥控枪机。当目录项为摄像机时可选
-                    String ptzType = getText(itemDevice, "PTZType");
-                    if (ObjectUtils.isEmpty(ptzType)) {
-                        //兼容INFO中的信息
-                        Element info = itemDevice.element("Info");
-                        String ptzTypeFromInfo = XmlUtil.getText(info, "PTZType");
-                        if(!ObjectUtils.isEmpty(ptzTypeFromInfo)){
-                            try {
-                                deviceChannel.setPTZType(Integer.parseInt(ptzTypeFromInfo));
-                            }catch (NumberFormatException e){
-                                logger.warn("[xml解析] 从通道数据info中获取PTZType失败: {}", ptzTypeFromInfo);
-                            }
-                        }
-                    } else {
-                        try {
-                            deviceChannel.setPTZType(Integer.parseInt(ptzType));
-                        }catch (NumberFormatException e){
-                            logger.warn("[xml解析] 从通道数据中获取PTZType失败: {}", ptzType);
-                        }
-                    }
-
-                    // TODO 摄像机位置类型扩展。
-                    // 1-省际检查站、
-                    // 2-党政机关、
-                    // 3-车站码头、
-                    // 4-中心广场、
-                    // 5-体育场馆、
-                    // 6-商业中心、
-                    // 7-宗教场所、
-                    // 8-校园周边、
-                    // 9-治安复杂区域、
-                    // 10-交通干线。
-                    // String positionType = getText(itemDevice, "PositionType");
-
-                    // TODO 摄像机安装位置室外、室内属性。1-室外、2-室内。
-                    // String roomType = getText(itemDevice, "RoomType");
-                    // TODO 摄像机用途属性
-                    // String useType = getText(itemDevice, "UseType");
-                    // TODO 摄像机补光属性。1-无补光、2-红外补光、3-白光补光
-                    // String supplyLightType = getText(itemDevice, "SupplyLightType");
-                    // TODO 摄像机监视方位属性。1-东、2-西、3-南、4-北、5-东南、6-东北、7-西南、8-西北。
-                    // String directionType = getText(itemDevice, "DirectionType");
-                    // TODO 摄像机支持的分辨率,可有多个分辨率值,各个取值间以“/”分隔。分辨率取值参见附录 F中SDPf字段规定
-                    // String resolution = getText(itemDevice, "Resolution");
-
-                    // TODO 下载倍速范围(可选),各可选参数以“/”分隔,如设备支持1,2,4倍速下载则应写为“1/2/4
-                    // String downloadSpeed = getText(itemDevice, "DownloadSpeed");
-                    // TODO 空域编码能力,取值0:不支持;1:1级增强(1个增强层);2:2级增强(2个增强层);3:3级增强(3个增强层)
-                    // String svcSpaceSupportMode = getText(itemDevice, "SVCSpaceSupportMode");
-                    // TODO 时域编码能力,取值0:不支持;1:1级增强;2:2级增强;3:3级增强
-                    // String svcTimeSupportMode = getText(itemDevice, "SVCTimeSupportMode");
-
-
-                    deviceChannel.setSecrecy(secrecy);
-                    break;
-            }
-        }
-
-        return deviceChannel;
-    }
-
-    /**
-     * 新增方法支持内部嵌套
-     *
-     * @param element xmlElement
-     * @param clazz 结果类
-     * @param <T> 泛型
-     * @return 结果对象
-     * @throws NoSuchMethodException
-     * @throws InvocationTargetException
-     * @throws InstantiationException
-     * @throws IllegalAccessException
-     */
-    public static <T> T loadElement(Element element, Class<T> clazz) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
-        Field[] fields = clazz.getDeclaredFields();
-        T t = clazz.getDeclaredConstructor().newInstance();
-        for (Field field : fields) {
-            ReflectionUtils.makeAccessible(field);
-            MessageElement annotation = field.getAnnotation(MessageElement.class);
-            if (annotation == null) {
-                continue;
-            }
-            String value = annotation.value();
-            String subVal = annotation.subVal();
-            Element element1 = element.element(value);
-            if (element1 == null) {
-                continue;
-            }
-            if ("".equals(subVal)) {
-                // 无下级数据
-                Object fieldVal = element1.isTextOnly() ? element1.getText() : loadElement(element1, field.getType());
-                Object o = simpleTypeDeal(field.getType(), fieldVal);
-                ReflectionUtils.setField(field, t,  o);
-            } else {
-                // 存在下级数据
-                ArrayList<Object> list = new ArrayList<>();
-                Type genericType = field.getGenericType();
-                if (!(genericType instanceof ParameterizedType)) {
-                    continue;
-                }
-                Class<?> aClass = (Class<?>) ((ParameterizedType) genericType).getActualTypeArguments()[0];
-                for (Element element2 : element1.elements(subVal)) {
-                    list.add(loadElement(element2, aClass));
-                }
-                ReflectionUtils.setField(field, t, list);
-            }
-        }
-        return t;
-    }
-
-    /**
-     * 简单类型处理
-     *
-     * @param tClass
-     * @param val
-     * @return
-     */
-    private static Object simpleTypeDeal(Class<?> tClass, Object val) {
-        if (tClass.equals(String.class)) {
-            return val.toString();
-        }
-        if (tClass.equals(Integer.class)) {
-            return Integer.valueOf(val.toString());
-        }
-        if (tClass.equals(Double.class)) {
-            return Double.valueOf(val.toString());
-        }
-        if (tClass.equals(Long.class)) {
-            return Long.valueOf(val.toString());
-        }
-        return val;
-    }
+package com.genersoft.iot.vmp.gb28181.utils;
+
+import com.alibaba.fastjson2.JSONArray;
+import com.alibaba.fastjson2.JSONObject;
+import com.genersoft.iot.vmp.common.CivilCodePo;
+import com.genersoft.iot.vmp.conf.CivilCodeFileConf;
+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.math.NumberUtils;
+import org.dom4j.Attribute;
+import org.dom4j.Document;
+import org.dom4j.DocumentException;
+import org.dom4j.Element;
+import org.dom4j.io.SAXReader;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.util.ObjectUtils;
+import org.springframework.util.ReflectionUtils;
+
+import javax.sip.RequestEvent;
+import javax.sip.message.Request;
+import java.io.ByteArrayInputStream;
+import java.io.StringReader;
+import java.lang.reflect.Field;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.ParameterizedType;
+import java.lang.reflect.Type;
+import java.util.*;
+
+/**
+ * 基于dom4j的工具包
+ *
+ *
+ */
+public class XmlUtil {
+    /**
+     * 日志服务
+     */
+    private static Logger logger = LoggerFactory.getLogger(XmlUtil.class);
+
+    /**
+     * 解析XML为Document对象
+     *
+     * @param xml 被解析的XMl
+     *
+     * @return Document
+     */
+    public static Element parseXml(String xml) {
+        Document document = null;
+        //
+        StringReader sr = new StringReader(xml);
+        SAXReader saxReader = new SAXReader();
+        try {
+            document = saxReader.read(sr);
+        } catch (DocumentException e) {
+            logger.error("解析失败", e);
+        }
+        return null == document ? null : document.getRootElement();
+    }
+
+    /**
+     * 获取element对象的text的值
+     *
+     * @param em  节点的对象
+     * @param tag 节点的tag
+     * @return 节点
+     */
+    public static String getText(Element em, String tag) {
+        if (null == em) {
+            return null;
+        }
+        Element e = em.element(tag);
+        //
+        return null == e ? null : e.getText().trim();
+    }
+
+    /**
+     * 递归解析xml节点,适用于 多节点数据
+     *
+     * @param node     node
+     * @param nodeName nodeName
+     * @return List<Map<String, Object>>
+     */
+    public static List<Map<String, Object>> listNodes(Element node, String nodeName) {
+        if (null == node) {
+            return null;
+        }
+        // 初始化返回
+        List<Map<String, Object>> listMap = new ArrayList<Map<String, Object>>();
+        // 首先获取当前节点的所有属性节点
+        List<Attribute> list = node.attributes();
+
+        Map<String, Object> map = null;
+        // 遍历属性节点
+        for (Attribute attribute : list) {
+            if (nodeName.equals(node.getName())) {
+                if (null == map) {
+                    map = new HashMap<String, Object>();
+                    listMap.add(map);
+                }
+                // 取到的节点属性放到map中
+                map.put(attribute.getName(), attribute.getValue());
+            }
+
+        }
+        // 遍历当前节点下的所有节点 ,nodeName 要解析的节点名称
+        // 使用递归
+        Iterator<Element> iterator = node.elementIterator();
+        while (iterator.hasNext()) {
+            Element e = iterator.next();
+            listMap.addAll(listNodes(e, nodeName));
+        }
+        return listMap;
+    }
+
+    /**
+     * xml转json
+     *
+     * @param element
+     * @param json
+     */
+    public static void node2Json(Element element, JSONObject json) {
+        // 如果是属性
+        for (Object o : element.attributes()) {
+            Attribute attr = (Attribute) o;
+            if (!ObjectUtils.isEmpty(attr.getValue())) {
+                json.put("@" + attr.getName(), attr.getValue());
+            }
+        }
+        List<Element> chdEl = element.elements();
+        if (chdEl.isEmpty() && !ObjectUtils.isEmpty(element.getText())) {// 如果没有子元素,只有一个值
+            json.put(element.getName(), element.getText());
+        }
+
+        for (Element e : chdEl) {   // 有子元素
+            if (!e.elements().isEmpty()) {  // 子元素也有子元素
+                JSONObject chdjson = new JSONObject();
+                node2Json(e, chdjson);
+                Object o = json.get(e.getName());
+                if (o != null) {
+                    JSONArray jsona = null;
+                    if (o instanceof JSONObject) {  // 如果此元素已存在,则转为jsonArray
+                        JSONObject jsono = (JSONObject) o;
+                        json.remove(e.getName());
+                        jsona = new JSONArray();
+                        jsona.add(jsono);
+                        jsona.add(chdjson);
+                    }
+                    if (o instanceof JSONArray) {
+                        jsona = (JSONArray) o;
+                        jsona.add(chdjson);
+                    }
+                    json.put(e.getName(), jsona);
+                } else {
+                    if (!chdjson.isEmpty()) {
+                        json.put(e.getName(), chdjson);
+                    }
+                }
+            } else { // 子元素没有子元素
+                for (Object o : element.attributes()) {
+                    Attribute attr = (Attribute) o;
+                    if (!ObjectUtils.isEmpty(attr.getValue())) {
+                        json.put("@" + attr.getName(), attr.getValue());
+                    }
+                }
+                if (!e.getText().isEmpty()) {
+                    json.put(e.getName(), e.getText());
+                }
+            }
+        }
+    }
+    public static  Element getRootElement(RequestEvent evt) throws DocumentException {
+
+        return getRootElement(evt, "gb2312");
+    }
+
+    public static Element getRootElement(RequestEvent evt, String charset) throws DocumentException {
+        Request request = evt.getRequest();
+        return getRootElement(request.getRawContent(), charset);
+    }
+
+    public static Element getRootElement(byte[] content, String charset) throws DocumentException {
+        if (charset == null) {
+            charset = "gb2312";
+        }
+        SAXReader reader = new SAXReader();
+        reader.setEncoding(charset);
+        Document xml = reader.read(new ByteArrayInputStream(content));
+        return xml.getRootElement();
+    }
+
+    private enum ChannelType{
+        CivilCode, BusinessGroup,VirtualOrganization,Other
+    }
+
+    public static DeviceChannel channelContentHandler(Element itemDevice, Device device, String event, CivilCodeFileConf civilCodeFileConf){
+        DeviceChannel deviceChannel = new DeviceChannel();
+        deviceChannel.setDeviceId(device.getDeviceId());
+        Element channdelIdElement = itemDevice.element("DeviceID");
+        if (channdelIdElement == null) {
+            logger.warn("解析Catalog消息时发现缺少 DeviceID");
+            return null;
+        }
+        String channelId = channdelIdElement.getTextTrim();
+        if (ObjectUtils.isEmpty(channelId)) {
+            logger.warn("解析Catalog消息时发现缺少 DeviceID");
+            return null;
+        }
+        deviceChannel.setChannelId(channelId);
+        if (event != null && !event.equals(CatalogEvent.ADD) && !event.equals(CatalogEvent.UPDATE)) {
+            // 除了ADD和update情况下需要识别全部内容,
+            return deviceChannel;
+        }
+        Element nameElement = itemDevice.element("Name");
+        if (nameElement != null) {
+            deviceChannel.setName(nameElement.getText());
+        }
+        if(channelId.length() <= 8) {
+            deviceChannel.setHasAudio(false);
+            CivilCodePo parentCode = civilCodeFileConf.getParentCode(channelId);
+            if (parentCode != null) {
+                deviceChannel.setParentId(parentCode.getCode());
+                deviceChannel.setCivilCode(parentCode.getCode());
+            }else {
+                logger.warn("[xml解析] 无法确定行政区划{}的上级行政区划", channelId);
+            }
+            deviceChannel.setStatus(true);
+            return deviceChannel;
+        }else {
+            if(channelId.length() != 20) {
+                logger.warn("[xml解析] 失败,编号不符合国标28181定义: {}", channelId);
+                return null;
+            }
+
+            int code = Integer.parseInt(channelId.substring(10, 13));
+            if (code == 136 || code == 137 || code == 138) {
+                deviceChannel.setHasAudio(true);
+            }else {
+                deviceChannel.setHasAudio(false);
+            }
+            // 设备厂商
+            String manufacturer = getText(itemDevice, "Manufacturer");
+            // 设备型号
+            String model = getText(itemDevice, "Model");
+            // 设备归属
+            String owner = getText(itemDevice, "Owner");
+            // 行政区域
+            String civilCode = getText(itemDevice, "CivilCode");
+            // 虚拟组织所属的业务分组ID,业务分组根据特定的业务需求制定,一个业务分组包含一组特定的虚拟组织
+            String businessGroupID = getText(itemDevice, "BusinessGroupID");
+            // 父设备/区域/系统ID
+            String parentID = getText(itemDevice, "ParentID");
+            if (parentID != null && parentID.equalsIgnoreCase("null")) {
+                parentID = null;
+            }
+            // 注册方式(必选)缺省为1;1:符合IETFRFC3261标准的认证注册模式;2:基于口令的双向认证注册模式;3:基于数字证书的双向认证注册模式
+            String registerWay = getText(itemDevice, "RegisterWay");
+            // 保密属性(必选)缺省为0;0:不涉密,1:涉密
+            String secrecy = getText(itemDevice, "Secrecy");
+            // 安装地址
+            String address = getText(itemDevice, "Address");
+
+            switch (code){
+                case 200:
+                    // 系统目录
+                    if (!ObjectUtils.isEmpty(manufacturer)) {
+                        deviceChannel.setManufacture(manufacturer);
+                    }
+                    if (!ObjectUtils.isEmpty(model)) {
+                        deviceChannel.setModel(model);
+                    }
+                    if (!ObjectUtils.isEmpty(owner)) {
+                        deviceChannel.setOwner(owner);
+                    }
+                    if (!ObjectUtils.isEmpty(civilCode)) {
+                        deviceChannel.setCivilCode(civilCode);
+                        deviceChannel.setParentId(civilCode);
+                    }else {
+                        if (!ObjectUtils.isEmpty(parentID)) {
+                            deviceChannel.setParentId(parentID);
+                        }
+                    }
+                    if (!ObjectUtils.isEmpty(address)) {
+                        deviceChannel.setAddress(address);
+                    }
+                    deviceChannel.setStatus(true);
+                    if (!ObjectUtils.isEmpty(registerWay)) {
+                        try {
+                            deviceChannel.setRegisterWay(Integer.parseInt(registerWay));
+                        }catch (NumberFormatException exception) {
+                            logger.warn("[xml解析] 从通道数据获取registerWay失败: {}", registerWay);
+                        }
+                    }
+                    if (!ObjectUtils.isEmpty(secrecy)) {
+                        deviceChannel.setSecrecy(secrecy);
+                    }
+                    return deviceChannel;
+                case 215:
+                    // 业务分组
+                    deviceChannel.setStatus(true);
+                    if (!ObjectUtils.isEmpty(parentID)) {
+                        if (!parentID.trim().equalsIgnoreCase(device.getDeviceId())) {
+                            deviceChannel.setParentId(parentID);
+                        }
+                    }else {
+                        logger.warn("[xml解析] 业务分组数据中缺少关键信息->ParentId");
+                        if (!ObjectUtils.isEmpty(civilCode)) {
+                            deviceChannel.setCivilCode(civilCode);
+                        }
+                    }
+                    break;
+                case 216:
+                    // 虚拟组织
+                    deviceChannel.setStatus(true);
+                    if (!ObjectUtils.isEmpty(businessGroupID)) {
+                        deviceChannel.setBusinessGroupId(businessGroupID);
+                    }
+
+                    if (!ObjectUtils.isEmpty(parentID)) {
+                        if (parentID.contains("/")) {
+                            String[] parentIdArray = parentID.split("/");
+                            parentID = parentIdArray[parentIdArray.length - 1];
+                        }
+                        deviceChannel.setParentId(parentID);
+                    }else {
+                        if (!ObjectUtils.isEmpty(businessGroupID)) {
+                            deviceChannel.setParentId(businessGroupID);
+                        }
+                    }
+                    break;
+                default:
+                    // 设备目录
+                    if (!ObjectUtils.isEmpty(manufacturer)) {
+                        deviceChannel.setManufacture(manufacturer);
+                    }
+                    if (!ObjectUtils.isEmpty(model)) {
+                        deviceChannel.setModel(model);
+                    }
+                    if (!ObjectUtils.isEmpty(owner)) {
+                        deviceChannel.setOwner(owner);
+                    }
+                    if (!ObjectUtils.isEmpty(civilCode)
+                            && civilCode.length() <= 8
+                            && NumberUtils.isParsable(civilCode)
+                            && civilCode.length()%2 == 0
+                    ) {
+                        deviceChannel.setCivilCode(civilCode);
+                    }
+                    if (!ObjectUtils.isEmpty(businessGroupID)) {
+                        deviceChannel.setBusinessGroupId(businessGroupID);
+                    }
+
+                    // 警区
+                    String block = getText(itemDevice, "Block");
+                    if (!ObjectUtils.isEmpty(block)) {
+                        deviceChannel.setBlock(block);
+                    }
+                    if (!ObjectUtils.isEmpty(address)) {
+                        deviceChannel.setAddress(address);
+                    }
+
+                    if (!ObjectUtils.isEmpty(secrecy)) {
+                        deviceChannel.setSecrecy(secrecy);
+                    }
+
+                    // 当为设备时,是否有子设备(必选)1有,0没有
+                    String parental = getText(itemDevice, "Parental");
+                    if (!ObjectUtils.isEmpty(parental)) {
+                        try {
+                            // 由于海康会错误的发送65535作为这里的取值,所以这里除非是0否则认为是1
+                            if (!ObjectUtils.isEmpty(parental) && parental.length() == 1 && Integer.parseInt(parental) == 0) {
+                                deviceChannel.setParental(0);
+                            }else {
+                                deviceChannel.setParental(1);
+                            }
+                        }catch (NumberFormatException e) {
+                            logger.warn("[xml解析] 从通道数据获取 parental失败: {}", parental);
+                        }
+                    }
+                    // 父设备/区域/系统ID
+
+                    if (!ObjectUtils.isEmpty(parentID) ) {
+                        if (parentID.contains("/")) {
+                            String[] parentIdArray = parentID.split("/");
+                            deviceChannel.setParentId(parentIdArray[parentIdArray.length - 1]);
+                        }else {
+                            if (parentID.length()%2 == 0) {
+                                deviceChannel.setParentId(parentID);
+                            }else {
+                                logger.warn("[xml解析] 不规范的parentID:{}, 已舍弃", parentID);
+                            }
+                        }
+                    }else {
+                        if (!ObjectUtils.isEmpty(businessGroupID)) {
+                            deviceChannel.setParentId(businessGroupID);
+                        }else {
+                            if (!ObjectUtils.isEmpty(deviceChannel.getCivilCode())) {
+                                deviceChannel.setParentId(deviceChannel.getCivilCode());
+                            }
+                        }
+                    }
+                    // 注册方式
+                    if (!ObjectUtils.isEmpty(registerWay)) {
+                        try {
+                            int registerWayInt = Integer.parseInt(registerWay);
+                            deviceChannel.setRegisterWay(registerWayInt);
+                        }catch (NumberFormatException exception) {
+                            logger.warn("[xml解析] 从通道数据获取registerWay失败: {}", registerWay);
+                            deviceChannel.setRegisterWay(1);
+                        }
+                    }else {
+                        deviceChannel.setRegisterWay(1);
+                    }
+
+                    // 信令安全模式(可选)缺省为0; 0:不采用;2:S/MIME 签名方式;3:S/MIME加密签名同时采用方式;4:数字摘要方式
+                    String safetyWay = getText(itemDevice, "SafetyWay");
+                    if (!ObjectUtils.isEmpty(safetyWay)) {
+                        try {
+                            deviceChannel.setSafetyWay(Integer.parseInt(safetyWay));
+                        }catch (NumberFormatException e) {
+                            logger.warn("[xml解析] 从通道数据获取 safetyWay失败: {}", safetyWay);
+                        }
+                    }
+
+                    // 证书序列号(有证书的设备必选)
+                    String certNum = getText(itemDevice, "CertNum");
+                    if (!ObjectUtils.isEmpty(certNum)) {
+                        deviceChannel.setCertNum(certNum);
+                    }
+
+                    // 证书有效标识(有证书的设备必选)缺省为0;证书有效标识:0:无效 1:有效
+                    String certifiable = getText(itemDevice, "Certifiable");
+                    if (!ObjectUtils.isEmpty(certifiable)) {
+                        try {
+                            deviceChannel.setCertifiable(Integer.parseInt(certifiable));
+                        }catch (NumberFormatException e) {
+                            logger.warn("[xml解析] 从通道数据获取 Certifiable失败: {}", certifiable);
+                        }
+                    }
+
+                    // 无效原因码(有证书且证书无效的设备必选)
+                    String errCode = getText(itemDevice, "ErrCode");
+                    if (!ObjectUtils.isEmpty(errCode)) {
+                        try {
+                            deviceChannel.setErrCode(Integer.parseInt(errCode));
+                        }catch (NumberFormatException e) {
+                            logger.warn("[xml解析] 从通道数据获取 ErrCode失败: {}", errCode);
+                        }
+                    }
+
+                    // 证书终止有效期(有证书的设备必选)
+                    String endTime = getText(itemDevice, "EndTime");
+                    if (!ObjectUtils.isEmpty(endTime)) {
+                        deviceChannel.setEndTime(endTime);
+                    }
+
+
+                    // 设备/区域/系统IP地址
+                    String ipAddress = getText(itemDevice, "IPAddress");
+                    if (!ObjectUtils.isEmpty(ipAddress)) {
+                        deviceChannel.setIpAddress(ipAddress);
+                    }
+
+                    // 设备/区域/系统端口
+                    String port = getText(itemDevice, "Port");
+                    if (!ObjectUtils.isEmpty(port)) {
+                        try {
+                            deviceChannel.setPort(Integer.parseInt(port));
+                        }catch (NumberFormatException e) {
+                            logger.warn("[xml解析] 从通道数据获取 Port失败: {}", port);
+                        }
+                    }
+
+                    // 设备口令
+                    String password = getText(itemDevice, "Password");
+                    if (!ObjectUtils.isEmpty(password)) {
+                        deviceChannel.setPassword(password);
+                    }
+
+
+                    // 设备状态
+                    String status = getText(itemDevice, "Status");
+                    if (status != null) {
+                        // ONLINE OFFLINE HIKVISION DS-7716N-E4 NVR的兼容性处理
+                        if (status.equals("ON") || status.equals("On") || status.equals("ONLINE") || status.equals("OK")) {
+                            deviceChannel.setStatus(true);
+                        }
+                        if (status.equals("OFF") || status.equals("Off") || status.equals("OFFLINE")) {
+                            deviceChannel.setStatus(false);
+                        }
+                    }else {
+                        deviceChannel.setStatus(true);
+                    }
+
+                    // 经度
+                    String longitude = getText(itemDevice, "Longitude");
+                    if (NumericUtil.isDouble(longitude)) {
+                        deviceChannel.setLongitude(Double.parseDouble(longitude));
+                    } else {
+                        deviceChannel.setLongitude(0.00);
+                    }
+
+                    // 纬度
+                    String latitude = getText(itemDevice, "Latitude");
+                    if (NumericUtil.isDouble(latitude)) {
+                        deviceChannel.setLatitude(Double.parseDouble(latitude));
+                    } else {
+                        deviceChannel.setLatitude(0.00);
+                    }
+
+                    deviceChannel.setGpsTime(DateUtil.getNow());
+
+                    // -摄像机类型扩展,标识摄像机类型:1-球机;2-半球;3-固定枪机;4-遥控枪机。当目录项为摄像机时可选
+                    String ptzType = getText(itemDevice, "PTZType");
+                    if (ObjectUtils.isEmpty(ptzType)) {
+                        //兼容INFO中的信息
+                        Element info = itemDevice.element("Info");
+                        String ptzTypeFromInfo = XmlUtil.getText(info, "PTZType");
+                        if(!ObjectUtils.isEmpty(ptzTypeFromInfo)){
+                            try {
+                                deviceChannel.setPTZType(Integer.parseInt(ptzTypeFromInfo));
+                            }catch (NumberFormatException e){
+                                logger.warn("[xml解析] 从通道数据info中获取PTZType失败: {}", ptzTypeFromInfo);
+                            }
+                        }
+                    } else {
+                        try {
+                            deviceChannel.setPTZType(Integer.parseInt(ptzType));
+                        }catch (NumberFormatException e){
+                            logger.warn("[xml解析] 从通道数据中获取PTZType失败: {}", ptzType);
+                        }
+                    }
+
+                    // TODO 摄像机位置类型扩展。
+                    // 1-省际检查站、
+                    // 2-党政机关、
+                    // 3-车站码头、
+                    // 4-中心广场、
+                    // 5-体育场馆、
+                    // 6-商业中心、
+                    // 7-宗教场所、
+                    // 8-校园周边、
+                    // 9-治安复杂区域、
+                    // 10-交通干线。
+                    // String positionType = getText(itemDevice, "PositionType");
+
+                    // TODO 摄像机安装位置室外、室内属性。1-室外、2-室内。
+                    // String roomType = getText(itemDevice, "RoomType");
+                    // TODO 摄像机用途属性
+                    // String useType = getText(itemDevice, "UseType");
+                    // TODO 摄像机补光属性。1-无补光、2-红外补光、3-白光补光
+                    // String supplyLightType = getText(itemDevice, "SupplyLightType");
+                    // TODO 摄像机监视方位属性。1-东、2-西、3-南、4-北、5-东南、6-东北、7-西南、8-西北。
+                    // String directionType = getText(itemDevice, "DirectionType");
+                    // TODO 摄像机支持的分辨率,可有多个分辨率值,各个取值间以“/”分隔。分辨率取值参见附录 F中SDPf字段规定
+                    // String resolution = getText(itemDevice, "Resolution");
+
+                    // TODO 下载倍速范围(可选),各可选参数以“/”分隔,如设备支持1,2,4倍速下载则应写为“1/2/4
+                    // String downloadSpeed = getText(itemDevice, "DownloadSpeed");
+                    // TODO 空域编码能力,取值0:不支持;1:1级增强(1个增强层);2:2级增强(2个增强层);3:3级增强(3个增强层)
+                    // String svcSpaceSupportMode = getText(itemDevice, "SVCSpaceSupportMode");
+                    // TODO 时域编码能力,取值0:不支持;1:1级增强;2:2级增强;3:3级增强
+                    // String svcTimeSupportMode = getText(itemDevice, "SVCTimeSupportMode");
+
+
+                    deviceChannel.setSecrecy(secrecy);
+                    break;
+            }
+        }
+
+        return deviceChannel;
+    }
+
+    /**
+     * 新增方法支持内部嵌套
+     *
+     * @param element xmlElement
+     * @param clazz 结果类
+     * @param <T> 泛型
+     * @return 结果对象
+     * @throws NoSuchMethodException
+     * @throws InvocationTargetException
+     * @throws InstantiationException
+     * @throws IllegalAccessException
+     */
+    public static <T> T loadElement(Element element, Class<T> clazz) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
+        Field[] fields = clazz.getDeclaredFields();
+        T t = clazz.getDeclaredConstructor().newInstance();
+        for (Field field : fields) {
+            ReflectionUtils.makeAccessible(field);
+            MessageElement annotation = field.getAnnotation(MessageElement.class);
+            if (annotation == null) {
+                continue;
+            }
+            String value = annotation.value();
+            String subVal = annotation.subVal();
+            Element element1 = element.element(value);
+            if (element1 == null) {
+                continue;
+            }
+            if ("".equals(subVal)) {
+                // 无下级数据
+                Object fieldVal = element1.isTextOnly() ? element1.getText() : loadElement(element1, field.getType());
+                Object o = simpleTypeDeal(field.getType(), fieldVal);
+                ReflectionUtils.setField(field, t,  o);
+            } else {
+                // 存在下级数据
+                ArrayList<Object> list = new ArrayList<>();
+                Type genericType = field.getGenericType();
+                if (!(genericType instanceof ParameterizedType)) {
+                    continue;
+                }
+                Class<?> aClass = (Class<?>) ((ParameterizedType) genericType).getActualTypeArguments()[0];
+                for (Element element2 : element1.elements(subVal)) {
+                    list.add(loadElement(element2, aClass));
+                }
+                ReflectionUtils.setField(field, t, list);
+            }
+        }
+        return t;
+    }
+
+    /**
+     * 简单类型处理
+     *
+     * @param tClass
+     * @param val
+     * @return
+     */
+    private static Object simpleTypeDeal(Class<?> tClass, Object val) {
+        if (tClass.equals(String.class)) {
+            return val.toString();
+        }
+        if (tClass.equals(Integer.class)) {
+            return Integer.valueOf(val.toString());
+        }
+        if (tClass.equals(Double.class)) {
+            return Double.valueOf(val.toString());
+        }
+        if (tClass.equals(Long.class)) {
+            return Long.valueOf(val.toString());
+        }
+        return val;
+    }
 }

+ 22 - 0
src/main/java/com/genersoft/iot/vmp/media/zlm/AssistRESTfulUtils.java

@@ -21,6 +21,7 @@ public class AssistRESTfulUtils {
 
     private final static Logger logger = LoggerFactory.getLogger(AssistRESTfulUtils.class);
 
+
     public interface RequestCallback{
         void run(JSONObject response);
     }
@@ -145,4 +146,25 @@ public class AssistRESTfulUtils {
         return sendGet(mediaServerItem, "api/record/addStreamCallInfo",param, callback);
     }
 
+    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);
+    }
+
+    public JSONObject getFileList(MediaServerItem mediaServerItem, int page, int count, String app, String stream,
+                                  String startTime, String endTime) {
+        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);
+    }
+
 }

+ 33 - 9
src/main/java/com/genersoft/iot/vmp/media/zlm/SendRtpPortManager.java

@@ -30,7 +30,7 @@ public class SendRtpPortManager {
 
     private final String KEY = "VM_MEDIA_SEND_RTP_PORT_";
 
-    public int getNextPort(MediaServerItem mediaServer) {
+    public synchronized int getNextPort(MediaServerItem mediaServer) {
         if (mediaServer == null) {
             logger.warn("[发送端口管理] 参数错误,mediaServer为NULL");
             return -1;
@@ -50,17 +50,15 @@ public class SendRtpPortManager {
         String sendRtpPortRange = mediaServer.getSendRtpPortRange();
         int startPort;
         int endPort;
-        if (sendRtpPortRange == null) {
-            logger.warn("{}未设置发送端口默认值,自动使用40000-50000作为端口范围", mediaServer.getId());
+        if (sendRtpPortRange != null) {
             String[] portArray = sendRtpPortRange.split(",");
             if (portArray.length != 2 || !NumberUtils.isParsable(portArray[0]) || !NumberUtils.isParsable(portArray[1])) {
-                logger.warn("{}发送端口配置格式错误,自动使用40000-50000作为端口范围", mediaServer.getId());
+                logger.warn("{}发送端口配置格式错误,自动使用50000-60000作为端口范围", mediaServer.getId());
                 startPort = 50000;
                 endPort = 60000;
             }else {
-
                 if ( Integer.parseInt(portArray[1]) - Integer.parseInt(portArray[0]) < 1) {
-                    logger.warn("{}发送端口配置错误,结束端口至少比开始端口大一,自动使用40000-50000作为端口范围", mediaServer.getId());
+                    logger.warn("{}发送端口配置错误,结束端口至少比开始端口大一,自动使用50000-60000作为端口范围", mediaServer.getId());
                     startPort = 50000;
                     endPort = 60000;
                 }else {
@@ -69,6 +67,7 @@ public class SendRtpPortManager {
                 }
             }
         }else {
+            logger.warn("{}未设置发送端口默认值,自动使用50000-60000作为端口范围", mediaServer.getId());
             startPort = 50000;
             endPort = 60000;
         }
@@ -76,10 +75,35 @@ public class SendRtpPortManager {
             logger.warn("{}获取redis连接信息失败", mediaServer.getId());
             return -1;
         }
+//        RedisAtomicInteger redisAtomicInteger = new RedisAtomicInteger(sendIndexKey , redisTemplate.getConnectionFactory());
+//        return redisAtomicInteger.getAndUpdate((current)->{
+//            return getPort(current, startPort, endPort, checkPort-> !sendRtpItemMap.containsKey(checkPort));
+//        });
+        return getSendPort(startPort, endPort, sendIndexKey, sendRtpItemMap);
+    }
+
+    private synchronized int getSendPort(int startPort, int endPort, String sendIndexKey, Map<Integer, SendRtpItem> sendRtpItemMap){
         RedisAtomicInteger redisAtomicInteger = new RedisAtomicInteger(sendIndexKey , redisTemplate.getConnectionFactory());
-        return redisAtomicInteger.getAndUpdate((current)->{
-            return getPort(current, startPort, endPort, checkPort-> !sendRtpItemMap.containsKey(checkPort));
-        });
+        if (redisAtomicInteger.get() < startPort) {
+            redisAtomicInteger.set(startPort);
+            return startPort;
+        }else {
+            int port = redisAtomicInteger.getAndIncrement();
+            if (port > endPort) {
+                redisAtomicInteger.set(startPort);
+                if (sendRtpItemMap.containsKey(startPort)) {
+                    return getSendPort(startPort, endPort, sendIndexKey, sendRtpItemMap);
+                }else {
+                    return startPort;
+                }
+            }
+            if (sendRtpItemMap.containsKey(port)) {
+                return getSendPort(startPort, endPort, sendIndexKey, sendRtpItemMap);
+            }else {
+                return port;
+            }
+        }
+
     }
 
     interface CheckPortCallback{

+ 8 - 2
src/main/java/com/genersoft/iot/vmp/media/zlm/ZLMHttpHookListener.java

@@ -30,6 +30,7 @@ import com.genersoft.iot.vmp.storager.IRedisCatchStorage;
 import com.genersoft.iot.vmp.storager.IVideoManagerStorage;
 import com.genersoft.iot.vmp.utils.DateUtil;
 import com.genersoft.iot.vmp.vmanager.bean.ErrorCode;
+import com.genersoft.iot.vmp.vmanager.bean.OtherPsSendInfo;
 import com.genersoft.iot.vmp.vmanager.bean.OtherRtpSendInfo;
 import com.genersoft.iot.vmp.vmanager.bean.StreamContent;
 import org.slf4j.Logger;
@@ -311,7 +312,10 @@ public class ZLMHttpHookListener {
         if (param.getApp().equalsIgnoreCase("rtp")) {
             String receiveKey = VideoManagerConstants.WVP_OTHER_RECEIVE_RTP_INFO + userSetting.getServerId() + "_" + param.getStream();
             OtherRtpSendInfo otherRtpSendInfo = (OtherRtpSendInfo)redisTemplate.opsForValue().get(receiveKey);
-            if (otherRtpSendInfo != null) {
+
+            String receiveKeyForPS = VideoManagerConstants.WVP_OTHER_RECEIVE_PS_INFO + userSetting.getServerId() + "_" + param.getStream();
+            OtherPsSendInfo otherPsSendInfo = (OtherPsSendInfo)redisTemplate.opsForValue().get(receiveKeyForPS);
+            if (otherRtpSendInfo != null || otherPsSendInfo != null) {
                 result.setEnable_mp4(true);
             }
         }
@@ -696,7 +700,9 @@ public class ZLMHttpHookListener {
                 result.onTimeout(() -> {
                     logger.info("[ZLM HOOK] 预览流自动点播, 等待超时");
                     msg.setData(new HookResult(ErrorCode.ERROR100.getCode(), "点播超时"));
-                    resultHolder.invokeResult(msg);
+                    resultHolder.invokeAllResult(msg);
+                    inviteStreamService.removeInviteInfoByDeviceAndChannel(InviteSessionType.PLAY, deviceId, channelId);
+                    storager.stopPlay(deviceId, channelId);
                 });
 
                 resultHolder.put(key, uuid, result);

+ 1 - 1
src/main/java/com/genersoft/iot/vmp/media/zlm/ZLMMediaListManager.java

@@ -42,7 +42,7 @@ public class ZLMServerFactory {
      * @param tcpMode 0/null udp 模式,1 tcp 被动模式, 2 tcp 主动模式。
      * @return
      */
-    public int createRTPServer(MediaServerItem mediaServerItem, String streamId, int ssrc, Integer port, Boolean onlyAuto, Boolean reUsePort, Integer tcpMode) {
+    public int createRTPServer(MediaServerItem mediaServerItem, String streamId, long ssrc, Integer port, Boolean onlyAuto, Boolean reUsePort, Integer tcpMode) {
         int result = -1;
         // 查询此rtp server 是否已经存在
         JSONObject rtpInfo = zlmresTfulUtils.getRtpInfo(mediaServerItem, streamId);

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

@@ -6,6 +6,7 @@ import com.genersoft.iot.vmp.media.zlm.dto.MediaServerItem;
 import com.genersoft.iot.vmp.media.zlm.dto.ServerKeepaliveData;
 import com.genersoft.iot.vmp.service.bean.MediaServerLoad;
 import com.genersoft.iot.vmp.service.bean.SSRCInfo;
+import com.genersoft.iot.vmp.vmanager.bean.RecordFile;
 
 import java.util.List;
 
@@ -95,4 +96,14 @@ public interface IMediaServerService {
      * @return
      */
     MediaServerLoad getLoad(MediaServerItem mediaServerItem);
+
+    /**
+     * 按时间查找录像文件
+     */
+    List<RecordFile> getRecords(String app, String stream, String startTime, String endTime, List<MediaServerItem> mediaServerItems);
+
+    /**
+     * 查找存在录像文件的时间
+     */
+    List<String> getRecordDates(String app, String stream, int year, int month, List<MediaServerItem> mediaServerItems);
 }

+ 19 - 6
src/main/java/com/genersoft/iot/vmp/service/IMediaService.java

@@ -115,6 +115,7 @@ public class DeviceServiceImpl implements IDeviceService {
             inviteStreamService.clearInviteInfo(device.getDeviceId());
         }
         device.setUpdateTime(now);
+        device.setKeepaliveTime(now);
         if (device.getKeepaliveIntervalTime() == 0) {
             // 默认心跳间隔60
             device.setKeepaliveIntervalTime(60);
@@ -535,6 +536,13 @@ public class DeviceServiceImpl implements IDeviceService {
         if (!ObjectUtils.isEmpty(device.getSdpIp())) {
             deviceInStore.setSdpIp(device.getSdpIp());
         }
+        if (!ObjectUtils.isEmpty(device.getPassword())) {
+            deviceInStore.setPassword(device.getPassword());
+        }
+        if (!ObjectUtils.isEmpty(device.getStreamMode())) {
+            deviceInStore.setStreamMode(device.getStreamMode());
+        }
+
 
         //  目录订阅相关的信息
         if (device.getSubscribeCycleForCatalog() > 0) {
@@ -568,18 +576,23 @@ public class DeviceServiceImpl implements IDeviceService {
         if (deviceInStore.getGeoCoordSys() != null) {
             // 坐标系变化,需要重新计算GCJ02坐标和WGS84坐标
             if (!deviceInStore.getGeoCoordSys().equals(device.getGeoCoordSys())) {
-                updateDeviceChannelGeoCoordSys(device);
+                deviceInStore.setGeoCoordSys(device.getGeoCoordSys());
+                updateDeviceChannelGeoCoordSys(deviceInStore);
             }
         }else {
-            device.setGeoCoordSys("WGS84");
+            deviceInStore.setGeoCoordSys("WGS84");
         }
         if (device.getCharset() == null) {
-            device.setCharset("GB2312");
+            deviceInStore.setCharset("GB2312");
         }
-
+        //SSRC校验
+        deviceInStore.setSsrcCheck(device.isSsrcCheck());
+        //作为消息通道
+        deviceInStore.setAsMessageChannel(device.isAsMessageChannel());
+        
         // 更新redis
-        redisCatchStorage.updateDevice(device);
-        deviceMapper.updateCustom(device);
+        deviceMapper.updateCustom(deviceInStore);
+        redisCatchStorage.removeDevice(deviceInStore.getDeviceId());
     }
 
     @Override

+ 102 - 5
src/main/java/com/genersoft/iot/vmp/service/impl/GbStreamServiceImpl.java

@@ -24,23 +24,30 @@ import com.genersoft.iot.vmp.utils.DateUtil;
 import com.genersoft.iot.vmp.utils.JsonUtil;
 import com.genersoft.iot.vmp.utils.redis.RedisUtil;
 import com.genersoft.iot.vmp.vmanager.bean.ErrorCode;
+import com.genersoft.iot.vmp.vmanager.bean.RecordFile;
 import okhttp3.OkHttpClient;
 import okhttp3.Request;
 import okhttp3.Response;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Qualifier;
 import org.springframework.beans.factory.annotation.Value;
 import org.springframework.data.redis.core.RedisTemplate;
 import org.springframework.jdbc.datasource.DataSourceTransactionManager;
+import org.springframework.scheduling.annotation.Async;
+import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.TransactionDefinition;
 import org.springframework.transaction.TransactionStatus;
+import org.springframework.util.Assert;
 import org.springframework.util.ObjectUtils;
 
 import java.io.File;
 import java.time.LocalDateTime;
 import java.util.*;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
 
 /**
  * 媒体服务器节点管理
@@ -104,6 +111,11 @@ public class MediaServerServiceImpl implements IMediaServerService {
     @Autowired
     private RedisTemplate<Object, Object> redisTemplate;
 
+    @Qualifier("taskExecutor")
+    @Autowired
+    private ThreadPoolTaskExecutor taskExecutor;
+
+
 
 
 
@@ -118,7 +130,7 @@ public class MediaServerServiceImpl implements IMediaServerService {
                 continue;
             }
             // 更新
-            if (ssrcFactory.hasMediaServerSSRC(mediaServerItem.getId())) {
+            if (!ssrcFactory.hasMediaServerSSRC(mediaServerItem.getId())) {
                 ssrcFactory.initMediaServerSSRC(mediaServerItem.getId(), null);
             }
             // 查询redis是否存在此mediaServer
@@ -151,7 +163,7 @@ public class MediaServerServiceImpl implements IMediaServerService {
         }
 
         if (streamId == null) {
-            streamId = String.format("%08x", Integer.parseInt(ssrc)).toUpperCase();
+            streamId = String.format("%08x", Long.parseLong(ssrc)).toUpperCase();
         }
         int ssrcCheckParam = 0;
         if (ssrcCheck && tcpMode > 1) {
@@ -160,7 +172,7 @@ public class MediaServerServiceImpl implements IMediaServerService {
         }
         int rtpServerPort;
         if (mediaServerItem.isRtpEnable()) {
-            rtpServerPort = zlmServerFactory.createRTPServer(mediaServerItem, streamId, (ssrcCheck && tcpMode == 0)?Integer.parseInt(ssrc):0, port, onlyAuto, reUsePort, tcpMode);
+            rtpServerPort = zlmServerFactory.createRTPServer(mediaServerItem, streamId, (ssrcCheck && tcpMode == 0) ? Long.parseLong(ssrc) : 0, port, onlyAuto, reUsePort, tcpMode);
         } else {
             rtpServerPort = mediaServerItem.getRtpProxyPort();
         }
@@ -225,7 +237,7 @@ public class MediaServerServiceImpl implements IMediaServerService {
         mediaServerMapper.update(mediaSerItem);
         MediaServerItem mediaServerItemInRedis = getOne(mediaSerItem.getId());
         MediaServerItem mediaServerItemInDataBase = mediaServerMapper.queryOne(mediaSerItem.getId());
-        if (mediaServerItemInRedis == null || ssrcFactory.hasMediaServerSSRC(mediaSerItem.getId())) {
+        if (mediaServerItemInRedis == null || !ssrcFactory.hasMediaServerSSRC(mediaSerItem.getId())) {
             ssrcFactory.initMediaServerSSRC(mediaServerItemInDataBase.getId(),null);
         }
         String key = VideoManagerConstants.MEDIA_SERVER_PREFIX + userSetting.getServerId() + "_" + mediaServerItemInDataBase.getId();
@@ -408,7 +420,7 @@ public class MediaServerServiceImpl implements IMediaServerService {
         }
         mediaServerMapper.update(serverItem);
         String key = VideoManagerConstants.MEDIA_SERVER_PREFIX + userSetting.getServerId() + "_" + zlmServerConfig.getGeneralMediaServerId();
-        if (ssrcFactory.hasMediaServerSSRC(serverItem.getId())) {
+        if (!ssrcFactory.hasMediaServerSSRC(serverItem.getId())) {
             ssrcFactory.initMediaServerSSRC(zlmServerConfig.getGeneralMediaServerId(), null);
         }
         redisTemplate.opsForValue().set(key, serverItem);
@@ -758,4 +770,89 @@ public class MediaServerServiceImpl implements IMediaServerService {
         return result;
     }
 
+    @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<>());
+    }
 }

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

@@ -251,7 +251,6 @@ public class PlatformServiceImpl implements IPlatformService {
                                         // 设置平台离线,并重新注册
                                         logger.info("[国标级联] 三次心跳超时, 平台{}({})离线", parentPlatform.getName(), parentPlatform.getServerGBId());
                                         offline(parentPlatform, false);
-
                                     }
 
                                 }else {
@@ -266,6 +265,7 @@ public class PlatformServiceImpl implements IPlatformService {
                                     platformCatch.setKeepAliveReply(0);
                                     redisCatchStorage.updatePlatformCatchInfo(platformCatch);
                                 }
+                                logger.info("[发送心跳] 国标级联 发送心跳, code: {}, msg: {}", eventResult.statusCode, eventResult.msg);
                             });
                         } catch (SipException | InvalidArgumentException | ParseException e) {
                             logger.error("[命令发送失败] 国标级联 发送心跳: {}", e.getMessage());
@@ -293,7 +293,7 @@ public class PlatformServiceImpl implements IPlatformService {
                         eventResult.statusCode, eventResult.msg);
                 offline(parentPlatform, false);
             }, null);
-        } catch (InvalidArgumentException | ParseException | SipException e) {
+        } catch (Exception e) {
             logger.error("[命令发送失败] 国标级联定时注册: {}", e.getMessage());
         }
     }

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

@@ -193,7 +193,7 @@ public interface DeviceChannelMapper {
     @Update(value = {"UPDATE wvp_device_channel SET status=false WHERE device_id=#{deviceId} AND channel_id=#{channelId}"})
     void offline(String deviceId,  String channelId);
 
-    @Update(value = {"UPDATE wvp_device_channel SET status=fasle WHERE device_id=#{deviceId}"})
+    @Update(value = {"UPDATE wvp_device_channel SET status=false WHERE device_id=#{deviceId}"})
     void offlineByDeviceId(String deviceId);
 
     @Insert("<script> " +

+ 16 - 3
src/main/java/com/genersoft/iot/vmp/storager/dao/DeviceMapper.java

@@ -16,6 +16,7 @@ import com.genersoft.iot.vmp.service.bean.GPSMsgInfo;
 import com.genersoft.iot.vmp.service.bean.MessageForPushChannel;
 import com.genersoft.iot.vmp.storager.IRedisCatchStorage;
 import com.genersoft.iot.vmp.storager.dao.DeviceChannelMapper;
+import com.genersoft.iot.vmp.storager.dao.DeviceMapper;
 import com.genersoft.iot.vmp.storager.dao.dto.PlatformRegisterInfo;
 import com.genersoft.iot.vmp.utils.DateUtil;
 import com.genersoft.iot.vmp.utils.JsonUtil;
@@ -40,6 +41,9 @@ public class RedisCatchStorageImpl implements IRedisCatchStorage {
     @Autowired
     private DeviceChannelMapper deviceChannelMapper;
 
+    @Autowired
+    private DeviceMapper deviceMapper;
+
     @Autowired
     private UserSetting userSetting;
 
@@ -375,7 +379,8 @@ public class RedisCatchStorageImpl implements IRedisCatchStorage {
         for (Object o : keys) {
             String key = (String) o;
             Device device = JsonUtil.redisJsonToObject(redisTemplate, key, Device.class);
-            if (Objects.nonNull(device)) { // 只取没有存过得
+            if (Objects.nonNull(device)) {
+                // 只取没有存过得
                 result.add(JsonUtil.redisJsonToObject(redisTemplate, key, Device.class));
             }
         }
@@ -386,14 +391,22 @@ public class RedisCatchStorageImpl implements IRedisCatchStorage {
     @Override
     public Device getDevice(String deviceId) {
         String key = VideoManagerConstants.DEVICE_PREFIX + userSetting.getServerId() + "_" + deviceId;
-        return JsonUtil.redisJsonToObject(redisTemplate, key, Device.class);
+        Device device = JsonUtil.redisJsonToObject(redisTemplate, key, Device.class);
+        if (device == null){
+            device = deviceMapper.getDeviceByDeviceId(deviceId);
+            if (device != null) {
+                updateDevice(device);
+            }
+        }
+        return device;
     }
 
     @Override
     public void updateGpsMsgInfo(GPSMsgInfo gpsMsgInfo) {
         String key = VideoManagerConstants.WVP_STREAM_GPS_MSG_PREFIX + userSetting.getServerId() + "_" + gpsMsgInfo.getId();
         Duration duration = Duration.ofSeconds(60L);
-        redisTemplate.opsForValue().set(key, gpsMsgInfo, duration); // 默认GPS消息保存1分钟
+        redisTemplate.opsForValue().set(key, gpsMsgInfo, duration);
+        // 默认GPS消息保存1分钟
     }
 
     @Override

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

@@ -1,6 +1,8 @@
 package com.genersoft.iot.vmp.utils;
 
 
+import org.apache.commons.lang3.ObjectUtils;
+
 import java.time.Instant;
 import java.time.LocalDate;
 import java.time.LocalDateTime;
@@ -109,6 +111,9 @@ public class DateUtil {
     }
 
     public static long getDifferenceForNow(String keepaliveTime) {
+        if (ObjectUtils.isEmpty(keepaliveTime)) {
+            return 0;
+        }
         Instant beforeInstant = Instant.from(formatter.parse(keepaliveTime));
         return ChronoUnit.MILLIS.between(beforeInstant, Instant.now());
     }

+ 50 - 50
src/main/java/com/genersoft/iot/vmp/utils/GitUtil.java

@@ -1,50 +1,50 @@
-package com.genersoft.iot.vmp.utils;
-
-import org.springframework.beans.BeansException;
-import org.springframework.context.ApplicationContext;
-import org.springframework.context.ApplicationContextAware;
-import org.springframework.stereotype.Component;
-
-/**    
- * @description:spring bean获取工厂,获取spring中的已初始化的bean
- * @author: swwheihei
- * @date:   2019年6月25日 下午4:51:52   
- * 
- */
-@Component
-public class SpringBeanFactory implements ApplicationContextAware {
-
-	// Spring应用上下文环境
-    private static ApplicationContext applicationContext;
-    
-    /**
-     * 实现ApplicationContextAware接口的回调方法,设置上下文环境
-     */
-    @Override
-    public void setApplicationContext(ApplicationContext applicationContext)
-            throws BeansException {
-    	SpringBeanFactory.applicationContext = applicationContext;
-    }
-
-    public static ApplicationContext getApplicationContext() {
-        return applicationContext;
-    }
-
-    /**
-     * 获取对象 这里重写了bean方法,起主要作用
-     */
-    public static  <T> T getBean(String beanId) throws BeansException {
-        if (applicationContext == null) {
-            return null;
-        }
-        return (T) applicationContext.getBean(beanId);
-    }
-
-    /**
-     * 获取当前环境
-     */
-    public static String getActiveProfile() {
-        return applicationContext.getEnvironment().getActiveProfiles()[0];
-    }
-
-}
+package com.genersoft.iot.vmp.utils;
+
+import org.springframework.beans.BeansException;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.ApplicationContextAware;
+import org.springframework.stereotype.Component;
+
+/**    
+ * @description:spring bean获取工厂,获取spring中的已初始化的bean
+ * @author: swwheihei
+ * @date:   2019年6月25日 下午4:51:52   
+ * 
+ */
+@Component
+public class SpringBeanFactory implements ApplicationContextAware {
+
+	// Spring应用上下文环境
+    private static ApplicationContext applicationContext;
+    
+    /**
+     * 实现ApplicationContextAware接口的回调方法,设置上下文环境
+     */
+    @Override
+    public void setApplicationContext(ApplicationContext applicationContext)
+            throws BeansException {
+    	SpringBeanFactory.applicationContext = applicationContext;
+    }
+
+    public static ApplicationContext getApplicationContext() {
+        return applicationContext;
+    }
+
+    /**
+     * 获取对象 这里重写了bean方法,起主要作用
+     */
+    public static  <T> T getBean(String beanId) throws BeansException {
+        if (applicationContext == null) {
+            return null;
+        }
+        return (T) applicationContext.getBean(beanId);
+    }
+
+    /**
+     * 获取当前环境
+     */
+    public static String getActiveProfile() {
+        return applicationContext.getEnvironment().getActiveProfiles()[0];
+    }
+
+}

+ 45 - 45
src/main/java/com/genersoft/iot/vmp/utils/SystemInfoUtils.java

@@ -1,45 +1,45 @@
-package com.genersoft.iot.vmp.utils.redis;
-
-import com.alibaba.fastjson2.JSON;
-import com.alibaba.fastjson2.JSONReader;
-import com.alibaba.fastjson2.JSONWriter;
-import org.springframework.data.redis.serializer.RedisSerializer;
-import org.springframework.data.redis.serializer.SerializationException;
-
-import java.nio.charset.Charset;
-
-/**    
- * @description:使用fastjson实现redis的序列化   
- * @author: swwheihei
- * @date:   2020年5月6日 下午8:40:11     
- */
-public class FastJsonRedisSerializer<T> implements RedisSerializer<T> {
-
-	public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8");
-	 
-    private Class<T> clazz;
- 
-    public FastJsonRedisSerializer(Class<T> clazz) {
-        super();
-        this.clazz = clazz;
-    }
- 
-    @Override
-    public byte[] serialize(T t) throws SerializationException {
-        if (t == null) {
-            return new byte[0];
-        }
-        return JSON.toJSONString(t, JSONWriter.Feature.WriteClassName, JSONWriter.Feature.WritePairAsJavaBean).getBytes(DEFAULT_CHARSET);
-    }
- 
-    @Override
-    public T deserialize(byte[] bytes) throws SerializationException {
-        if (bytes == null || bytes.length <= 0) {
-            return null;
-        }
-        String str = new String(bytes, DEFAULT_CHARSET);
-        return JSON.parseObject(str, clazz, JSONReader.Feature.SupportAutoType);
-    }
-
-
-}
+package com.genersoft.iot.vmp.utils.redis;
+
+import com.alibaba.fastjson2.JSON;
+import com.alibaba.fastjson2.JSONReader;
+import com.alibaba.fastjson2.JSONWriter;
+import org.springframework.data.redis.serializer.RedisSerializer;
+import org.springframework.data.redis.serializer.SerializationException;
+
+import java.nio.charset.Charset;
+
+/**    
+ * @description:使用fastjson实现redis的序列化   
+ * @author: swwheihei
+ * @date:   2020年5月6日 下午8:40:11     
+ */
+public class FastJsonRedisSerializer<T> implements RedisSerializer<T> {
+
+	public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8");
+	 
+    private Class<T> clazz;
+ 
+    public FastJsonRedisSerializer(Class<T> clazz) {
+        super();
+        this.clazz = clazz;
+    }
+ 
+    @Override
+    public byte[] serialize(T t) throws SerializationException {
+        if (t == null) {
+            return new byte[0];
+        }
+        return JSON.toJSONString(t, JSONWriter.Feature.WriteClassName, JSONWriter.Feature.WritePairAsJavaBean).getBytes(DEFAULT_CHARSET);
+    }
+ 
+    @Override
+    public T deserialize(byte[] bytes) throws SerializationException {
+        if (bytes == null || bytes.length <= 0) {
+            return null;
+        }
+        String str = new String(bytes, DEFAULT_CHARSET);
+        return JSON.parseObject(str, clazz, JSONReader.Feature.SupportAutoType);
+    }
+
+
+}

+ 47 - 46
src/main/java/com/genersoft/iot/vmp/utils/redis/RedisUtil.java

@@ -1,46 +1,47 @@
-package com.genersoft.iot.vmp.utils.redis;
-
-import org.springframework.data.redis.core.Cursor;
-import org.springframework.data.redis.core.RedisCallback;
-import org.springframework.data.redis.core.RedisTemplate;
-import org.springframework.data.redis.core.ScanOptions;
-
-import java.util.ArrayList;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Set;
-
-/**
- * Redis工具类
- *
- * @author swwheihei
- * @date 2020年5月6日 下午8:27:29
- */
-@SuppressWarnings(value = {"rawtypes", "unchecked"})
-public class RedisUtil {
-
-    /**
-     * 模糊查询
-     *
-     * @param query 查询参数
-     * @return
-     */
-    public static List<Object> scan(RedisTemplate redisTemplate, String query) {
-
-        Set<String> resultKeys = (Set<String>) redisTemplate.execute((RedisCallback<Set<String>>) connection -> {
-            ScanOptions scanOptions = ScanOptions.scanOptions().match("*" + query + "*").count(1000).build();
-            Cursor<byte[]> scan = connection.scan(scanOptions);
-            Set<String> keys = new HashSet<>();
-            while (scan.hasNext()) {
-                byte[] next = scan.next();
-                keys.add(new String(next));
-            }
-            return keys;
-        });
-
-        return new ArrayList<>(resultKeys);
-    }
-}
-
-
-
+package com.genersoft.iot.vmp.utils.redis;
+
+import com.google.common.collect.Lists;
+import org.springframework.data.redis.core.Cursor;
+import org.springframework.data.redis.core.RedisCallback;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.data.redis.core.ScanOptions;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * Redis工具类
+ *
+ * @author swwheihei
+ * @date 2020年5月6日 下午8:27:29
+ */
+@SuppressWarnings(value = {"rawtypes", "unchecked"})
+public class RedisUtil {
+
+    /**
+     * 模糊查询
+     *
+     * @param query 查询参数
+     * @return
+     */
+    public static List<Object> scan(RedisTemplate redisTemplate, String query) {
+
+        Set<String> resultKeys = (Set<String>) redisTemplate.execute((RedisCallback<Set<String>>) connection -> {
+            ScanOptions scanOptions = ScanOptions.scanOptions().match("*" + query + "*").count(1000).build();
+            Cursor<byte[]> scan = connection.scan(scanOptions);
+            Set<String> keys = new HashSet<>();
+            while (scan.hasNext()) {
+                byte[] next = scan.next();
+                keys.add(new String(next));
+            }
+            return keys;
+        });
+
+        return Lists.newArrayList(resultKeys);
+    }
+}
+
+
+

+ 137 - 0
src/main/java/com/genersoft/iot/vmp/utils/redis/RedisUtil2.java

@@ -0,0 +1,137 @@
+package com.genersoft.iot.vmp.vmanager.bean;
+
+public class OtherPsSendInfo {
+
+    /**
+     * 发流IP
+     */
+    private String sendLocalIp;
+
+    /**
+     * 发流端口
+     */
+    private int sendLocalPort;
+
+    /**
+     * 收流IP
+     */
+    private String receiveIp;
+
+    /**
+     * 收流端口
+     */
+    private int receivePort;
+
+
+    /**
+     * 会话ID
+     */
+    private String callId;
+
+    /**
+     * 流ID
+     */
+    private String stream;
+
+    /**
+     * 推流应用名
+     */
+    private String pushApp;
+
+    /**
+     * 推流流ID
+     */
+    private String pushStream;
+
+    /**
+     * 推流SSRC
+     */
+    private String pushSSRC;
+
+    public String getSendLocalIp() {
+        return sendLocalIp;
+    }
+
+    public void setSendLocalIp(String sendLocalIp) {
+        this.sendLocalIp = sendLocalIp;
+    }
+
+    public int getSendLocalPort() {
+        return sendLocalPort;
+    }
+
+    public void setSendLocalPort(int sendLocalPort) {
+        this.sendLocalPort = sendLocalPort;
+    }
+
+    public String getReceiveIp() {
+        return receiveIp;
+    }
+
+    public void setReceiveIp(String receiveIp) {
+        this.receiveIp = receiveIp;
+    }
+
+    public int getReceivePort() {
+        return receivePort;
+    }
+
+    public void setReceivePort(int receivePort) {
+        this.receivePort = receivePort;
+    }
+
+    public String getCallId() {
+        return callId;
+    }
+
+    public void setCallId(String callId) {
+        this.callId = callId;
+    }
+
+    public String getStream() {
+        return stream;
+    }
+
+    public void setStream(String stream) {
+        this.stream = stream;
+    }
+
+    public String getPushApp() {
+        return pushApp;
+    }
+
+    public void setPushApp(String pushApp) {
+        this.pushApp = pushApp;
+    }
+
+    public String getPushStream() {
+        return pushStream;
+    }
+
+    public void setPushStream(String pushStream) {
+        this.pushStream = pushStream;
+    }
+
+    public String getPushSSRC() {
+        return pushSSRC;
+    }
+
+    public void setPushSSRC(String pushSSRC) {
+        this.pushSSRC = pushSSRC;
+    }
+
+    @Override
+    public String toString() {
+        return "OtherPsSendInfo{" +
+                "sendLocalIp='" + sendLocalIp + '\'' +
+                ", sendLocalPort=" + sendLocalPort +
+                ", receiveIp='" + receiveIp + '\'' +
+                ", receivePort=" + receivePort +
+                ", callId='" + callId + '\'' +
+                ", stream='" + stream + '\'' +
+                ", pushApp='" + pushApp + '\'' +
+                ", pushStream='" + pushStream + '\'' +
+                ", pushSSRC='" + pushSSRC + '\'' +
+                '}';
+    }
+}

+ 99 - 0
src/main/java/com/genersoft/iot/vmp/vmanager/bean/OtherRtpSendInfo.java

@@ -0,0 +1,99 @@
+package com.genersoft.iot.vmp.vmanager.bean;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class PageInfo<T> {
+    //当前页
+    private int pageNum;
+    //每页的数量
+    private int pageSize;
+    //当前页的数量
+    private int size;
+    //总页数
+    private int pages;
+    //总数
+    private int total;
+
+    private List<T> resultData;
+
+    private List<T> list;
+
+    public PageInfo(List<T> resultData) {
+        this.resultData = resultData;
+    }
+
+    public PageInfo() {
+    }
+
+    public void startPage(int page, int count) {
+        if (count <= 0) count = 10;
+        if (page <= 0) page = 1;
+        this.pageNum = page;
+        this.pageSize = count;
+        this.total = resultData.size();
+
+        this.pages = total % count == 0 ? total / count : total / count + 1;
+        int fromIndx = (page - 1) * count;
+        if (fromIndx > this.total - 1) {
+            this.list = new ArrayList<>();
+            this.size = 0;
+            return;
+        }
+
+        int toIndx = page * count;
+        if (toIndx > this.total) {
+            toIndx = this.total;
+        }
+        this.list = this.resultData.subList(fromIndx, toIndx);
+        this.size = this.list.size();
+    }
+
+    public int getPageNum() {
+        return pageNum;
+    }
+
+    public void setPageNum(int pageNum) {
+        this.pageNum = pageNum;
+    }
+
+    public int getPageSize() {
+        return pageSize;
+    }
+
+    public void setPageSize(int pageSize) {
+        this.pageSize = pageSize;
+    }
+
+    public int getSize() {
+        return size;
+    }
+
+    public void setSize(int size) {
+        this.size = size;
+    }
+
+    public int getPages() {
+        return pages;
+    }
+
+    public void setPages(int pages) {
+        this.pages = pages;
+    }
+
+    public int getTotal() {
+        return total;
+    }
+
+    public void setTotal(int total) {
+        this.total = total;
+    }
+
+    public List<T> getList() {
+        return list;
+    }
+
+    public void setList(List<T> list) {
+        this.list = list;
+    }
+}

+ 53 - 0
src/main/java/com/genersoft/iot/vmp/vmanager/bean/PlayTypeEnum.java

@@ -0,0 +1,53 @@
+package com.genersoft.iot.vmp.vmanager.bean;
+
+public class RecordFile {
+    private String app;
+    private String stream;
+
+    private String fileName;
+
+    private String mediaServerId;
+
+    private String date;
+
+
+    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 getFileName() {
+        return fileName;
+    }
+
+    public void setFileName(String fileName) {
+        this.fileName = fileName;
+    }
+
+    public String getMediaServerId() {
+        return mediaServerId;
+    }
+
+    public void setMediaServerId(String mediaServerId) {
+        this.mediaServerId = mediaServerId;
+    }
+
+    public String getDate() {
+        return date;
+    }
+
+    public void setDate(String date) {
+        this.date = date;
+    }
+}

+ 145 - 0
src/main/java/com/genersoft/iot/vmp/vmanager/bean/ResourceBaseInfo.java

@@ -0,0 +1,145 @@
+package com.genersoft.iot.vmp.vmanager.cloudRecord;
+
+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.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.IMediaServerService;
+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 io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import org.apache.commons.lang3.ObjectUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.ArrayList;
+import java.util.Calendar;
+import java.util.List;
+
+@SuppressWarnings("rawtypes")
+@Tag(name = "云端录像接口")
+
+@RestController
+@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;
+
+    @Autowired
+    private IMediaServerService mediaServerService;
+
+    @Autowired
+    private UserSetting userSetting;
+
+    @Autowired
+    private DynamicTask dynamicTask;
+
+    @Autowired
+    private RedisTemplate<Object, Object> redisTemplate;
+
+    @ResponseBody
+    @GetMapping("/date/list")
+    @Operation(summary = "查询存在云端录像的日期")
+    @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 = false) int year,
+            @RequestParam(required = false) int month,
+            @RequestParam(required = false) String mediaServerId
+
+    ) {
+        logger.info("[云端录像] 查询存在云端录像的日期 app->{}, stream->{}, mediaServerId->{}, year->{}, month->{}",
+                app, stream, mediaServerId, year, month);
+        Calendar calendar = Calendar.getInstance();
+        if (ObjectUtils.isEmpty(year)) {
+            year = calendar.get(Calendar.YEAR);
+        }
+        if (ObjectUtils.isEmpty(month)) {
+            month = calendar.get(Calendar.MONTH) + 1;
+        }
+        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 = mediaServerService.getAll();
+        }
+        if (mediaServerItems.isEmpty()) {
+            return new ArrayList<>();
+        }
+
+        return mediaServerService.getRecordDates(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)
+    @Parameter(name = "mediaServerId", description = "流媒体ID,置空则查询全部流媒体", required = false)
+    public PageInfo<RecordFile> openRtpServer(
+            @RequestParam String app,
+            @RequestParam String stream,
+            @RequestParam int page,
+            @RequestParam int count,
+            @RequestParam String startTime,
+            @RequestParam String endTime,
+            @RequestParam(required = false) String mediaServerId
+
+    ) {
+        logger.info("[云端录像] 查询 app->{}, stream->{}, mediaServerId->{}, page->{}, count->{}, startTime->{}, endTime->{}",
+                app, stream, mediaServerId, page, count, startTime, 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 = mediaServerService.getAll();
+        }
+        if (mediaServerItems.isEmpty()) {
+            return new PageInfo<>();
+        }
+        List<RecordFile> records = mediaServerService.getRecords(app, stream, startTime, endTime, mediaServerItems);
+        PageInfo<RecordFile> pageInfo = new PageInfo<>(records);
+        pageInfo.startPage(page, count);
+        return pageInfo;
+    }
+
+
+}

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

@@ -61,7 +61,7 @@ public class LogController {
             query = null;
         }
 
-        if (!userSetting.getLogInDatebase()) {
+        if (!userSetting.getLogInDatabase()) {
             logger.warn("自动记录日志功能已关闭,查询结果可能不完整。");
         }
 

+ 322 - 0
src/main/java/com/genersoft/iot/vmp/vmanager/ps/PsController.java

@@ -0,0 +1,322 @@
+package com.genersoft.iot.vmp.vmanager.ps;
+
+import com.alibaba.fastjson2.JSONObject;
+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.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.HookSubscribeFactory;
+import com.genersoft.iot.vmp.media.zlm.dto.HookSubscribeForRtpServerTimeout;
+import com.genersoft.iot.vmp.media.zlm.dto.HookSubscribeForStreamChange;
+import com.genersoft.iot.vmp.media.zlm.dto.MediaServerItem;
+import com.genersoft.iot.vmp.media.zlm.dto.hook.OnRtpServerTimeoutHookParam;
+import com.genersoft.iot.vmp.service.IMediaServerService;
+import com.genersoft.iot.vmp.utils.redis.RedisUtil;
+import com.genersoft.iot.vmp.vmanager.bean.ErrorCode;
+import com.genersoft.iot.vmp.vmanager.bean.OtherPsSendInfo;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import okhttp3.OkHttpClient;
+import okhttp3.Request;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.web.bind.annotation.*;
+
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+import java.util.concurrent.TimeUnit;
+
+@SuppressWarnings("rawtypes")
+@Tag(name = "第三方PS服务对接")
+
+@RestController
+@RequestMapping("/api/ps")
+public class PsController {
+
+    private final static Logger logger = LoggerFactory.getLogger(PsController.class);
+
+    @Autowired
+    private ZLMServerFactory zlmServerFactory;
+
+    @Autowired
+    private ZlmHttpHookSubscribe hookSubscribe;
+
+    @Autowired
+    private IMediaServerService mediaServerService;
+
+    @Autowired
+    private SendRtpPortManager sendRtpPortManager;
+
+    @Autowired
+    private UserSetting userSetting;
+
+    @Autowired
+    private DynamicTask dynamicTask;
+
+
+    @Autowired
+    private RedisTemplate<Object, Object> redisTemplate;
+
+
+    @GetMapping(value = "/receive/open")
+    @ResponseBody
+    @Operation(summary = "开启收流和获取发流信息")
+    @Parameter(name = "isSend", description = "是否发送,false时只开启收流, true同时返回推流信息", required = true)
+    @Parameter(name = "callId", description = "整个过程的唯一标识,为了与后续接口关联", required = true)
+    @Parameter(name = "ssrc", description = "来源流的SSRC,不传则不校验来源ssrc", required = false)
+    @Parameter(name = "stream", description = "形成的流的ID", required = true)
+    @Parameter(name = "tcpMode", description = "收流模式, 0为UDP, 1为TCP被动", required = true)
+    @Parameter(name = "callBack", description = "回调地址,如果收流超时会通道回调通知,回调为get请求,参数为callId", required = true)
+    public OtherPsSendInfo openRtpServer(Boolean isSend, @RequestParam(required = false)String ssrc, String callId, String stream, Integer tcpMode, String callBack) {
+
+        logger.info("[第三方PS服务对接->开启收流和获取发流信息] isSend->{}, ssrc->{}, callId->{}, stream->{}, tcpMode->{}, callBack->{}",
+                isSend, ssrc, callId, stream, tcpMode==0?"UDP":"TCP被动", callBack);
+
+        MediaServerItem mediaServerItem = mediaServerService.getDefaultMediaServer();
+        if (mediaServerItem == null) {
+            throw new ControllerException(ErrorCode.ERROR100.getCode(),"没有可用的MediaServer");
+        }
+        if (stream == null) {
+            throw new ControllerException(ErrorCode.ERROR100.getCode(),"stream参数不可为空");
+        }
+        if (isSend != null && isSend && callId == null) {
+            throw new ControllerException(ErrorCode.ERROR100.getCode(),"isSend为true时,CallID不能为空");
+        }
+        long ssrcInt = 0;
+        if (ssrc != null) {
+            try {
+                ssrcInt = Long.parseLong(ssrc);
+            }catch (NumberFormatException e) {
+                throw new ControllerException(ErrorCode.ERROR100.getCode(),"ssrc格式错误");
+            }
+        }
+        String receiveKey = VideoManagerConstants.WVP_OTHER_RECEIVE_PS_INFO + userSetting.getServerId() + "_" + callId + "_"  + stream;
+        int localPort = zlmServerFactory.createRTPServer(mediaServerItem, stream, ssrcInt, null, false, tcpMode);
+        if (localPort == 0) {
+            throw new ControllerException(ErrorCode.ERROR100.getCode(), "获取端口失败");
+        }
+        // 注册回调如果rtp收流超时则通过回调发送通知
+        if (callBack != null) {
+            HookSubscribeForRtpServerTimeout hookSubscribeForRtpServerTimeout = HookSubscribeFactory.on_rtp_server_timeout(stream, String.valueOf(ssrcInt), mediaServerItem.getId());
+            // 订阅 zlm启动事件, 新的zlm也会从这里进入系统
+            hookSubscribe.addSubscribe(hookSubscribeForRtpServerTimeout,
+                    (mediaServerItemInUse, hookParam)->{
+                        OnRtpServerTimeoutHookParam serverTimeoutHookParam = (OnRtpServerTimeoutHookParam) hookParam;
+                        if (stream.equals(serverTimeoutHookParam.getStream_id())) {
+                            logger.info("[第三方PS服务对接->开启收流和获取发流信息] 等待收流超时 callId->{}, 发送回调", callId);
+                            // 将信息写入redis中,以备后用
+                            redisTemplate.delete(receiveKey);
+                            OkHttpClient.Builder httpClientBuilder = new OkHttpClient.Builder();
+                            OkHttpClient client = httpClientBuilder.build();
+                            String url = callBack + "?callId="  + callId;
+                            Request request = new Request.Builder().get().url(url).build();
+                            try {
+                                client.newCall(request).execute();
+                            } catch (IOException e) {
+                                logger.error("[第三方PS服务对接->开启收流和获取发流信息] 等待收流超时 callId->{}, 发送回调失败", callId, e);
+                            }
+                            hookSubscribe.removeSubscribe(hookSubscribeForRtpServerTimeout);
+                        }
+                    });
+        }
+        OtherPsSendInfo otherPsSendInfo = new OtherPsSendInfo();
+        otherPsSendInfo.setReceiveIp(mediaServerItem.getSdpIp());
+        otherPsSendInfo.setReceivePort(localPort);
+        otherPsSendInfo.setCallId(callId);
+        otherPsSendInfo.setStream(stream);
+
+        // 将信息写入redis中,以备后用
+        redisTemplate.opsForValue().set(receiveKey, otherPsSendInfo);
+        if (isSend != null && isSend) {
+            String key = VideoManagerConstants.WVP_OTHER_SEND_PS_INFO + userSetting.getServerId() + "_"  + callId;
+            // 预创建发流信息
+            int port = sendRtpPortManager.getNextPort(mediaServerItem);
+
+            otherPsSendInfo.setSendLocalIp(mediaServerItem.getSdpIp());
+            otherPsSendInfo.setSendLocalPort(port);
+            // 将信息写入redis中,以备后用
+            redisTemplate.opsForValue().set(key, otherPsSendInfo, 300, TimeUnit.SECONDS);
+            logger.info("[第三方PS服务对接->开启收流和获取发流信息] 结果,callId->{}, {}", callId, otherPsSendInfo);
+        }
+        return otherPsSendInfo;
+    }
+
+    @GetMapping(value = "/receive/close")
+    @ResponseBody
+    @Operation(summary = "关闭收流")
+    @Parameter(name = "stream", description = "流的ID", required = true)
+    public void closeRtpServer(String stream) {
+        logger.info("[第三方PS服务对接->关闭收流] stream->{}", stream);
+        MediaServerItem mediaServerItem = mediaServerService.getDefaultMediaServer();
+        zlmServerFactory.closeRtpServer(mediaServerItem,stream);
+        String receiveKey = VideoManagerConstants.WVP_OTHER_RECEIVE_PS_INFO + userSetting.getServerId() + "_*_"  + stream;
+        List<Object> scan = RedisUtil.scan(redisTemplate, receiveKey);
+        if (!scan.isEmpty()) {
+            for (Object key : scan) {
+                // 将信息写入redis中,以备后用
+                redisTemplate.delete(key);
+            }
+        }
+    }
+
+    @GetMapping(value = "/send/start")
+    @ResponseBody
+    @Operation(summary = "发送流")
+    @Parameter(name = "ssrc", description = "发送流的SSRC", required = true)
+    @Parameter(name = "dstIp", description = "目标收流IP", required = true)
+    @Parameter(name = "dstPort", description = "目标收流端口", required = true)
+    @Parameter(name = "app", description = "待发送应用名", required = true)
+    @Parameter(name = "stream", description = "待发送流Id", required = true)
+    @Parameter(name = "callId", description = "整个过程的唯一标识,不传则使用随机端口发流", required = true)
+    @Parameter(name = "isUdp", description = "是否为UDP", required = true)
+    public void sendRTP(String ssrc,
+                        String dstIp,
+                        Integer dstPort,
+                        String app,
+                        String stream,
+                        String callId,
+                        Boolean isUdp
+        ) {
+        logger.info("[第三方PS服务对接->发送流] " +
+                        "ssrc->{}, \r\n" +
+                        "dstIp->{}, \n" +
+                        "dstPort->{},  \n" +
+                        "app->{}, \n" +
+                        "stream->{}, \n" +
+                        "callId->{} \n",
+                        ssrc,
+                        dstIp,
+                        dstPort,
+                        app,
+                        stream,
+                        callId);
+        MediaServerItem mediaServerItem = mediaServerService.getDefaultMediaServer();
+        String key = VideoManagerConstants.WVP_OTHER_SEND_PS_INFO + userSetting.getServerId() + "_"  + callId;
+        OtherPsSendInfo sendInfo = (OtherPsSendInfo)redisTemplate.opsForValue().get(key);
+        if (sendInfo == null) {
+            sendInfo = new OtherPsSendInfo();
+        }
+        sendInfo.setPushApp(app);
+        sendInfo.setPushStream(stream);
+        sendInfo.setPushSSRC(ssrc);
+
+        Map<String, Object> param;
+
+
+        param = new HashMap<>();
+        param.put("vhost","__defaultVhost__");
+        param.put("app",app);
+        param.put("stream",stream);
+        param.put("ssrc", ssrc);
+
+        param.put("dst_url", dstIp);
+        param.put("dst_port", dstPort);
+        String is_Udp = isUdp ? "1" : "0";
+        param.put("is_udp", is_Udp);
+        param.put("src_port", sendInfo.getSendLocalPort());
+
+
+        Boolean streamReady = zlmServerFactory.isStreamReady(mediaServerItem, app, stream);
+        if (streamReady) {
+            JSONObject jsonObject = zlmServerFactory.startSendRtpStream(mediaServerItem, param);
+            if (jsonObject.getInteger("code") == 0) {
+                logger.info("[第三方PS服务对接->发送流] 视频流发流成功,callId->{},param->{}", callId, param);
+                redisTemplate.opsForValue().set(key, sendInfo);
+            }else {
+                redisTemplate.delete(key);
+                logger.info("[第三方PS服务对接->发送流] 视频流发流失败,callId->{}, {}", callId, jsonObject.getString("msg"));
+                throw new ControllerException(ErrorCode.ERROR100.getCode(), "[视频流发流失败] " + jsonObject.getString("msg"));
+            }
+        }else {
+            logger.info("[第三方PS服务对接->发送流] 流不存在,等待流上线,callId->{}", callId);
+            String uuid = UUID.randomUUID().toString();
+            HookSubscribeForStreamChange hookSubscribeForStreamChange = HookSubscribeFactory.on_stream_changed(app, stream, true, "rtsp", mediaServerItem.getId());
+            dynamicTask.startDelay(uuid, ()->{
+                logger.info("[第三方PS服务对接->发送流] 等待流上线超时 callId->{}", callId);
+                redisTemplate.delete(key);
+                hookSubscribe.removeSubscribe(hookSubscribeForStreamChange);
+            }, 10000);
+
+            // 订阅 zlm启动事件, 新的zlm也会从这里进入系统
+            OtherPsSendInfo finalSendInfo = sendInfo;
+            hookSubscribe.removeSubscribe(hookSubscribeForStreamChange);
+            hookSubscribe.addSubscribe(hookSubscribeForStreamChange,
+                    (mediaServerItemInUse, response)->{
+                        dynamicTask.stop(uuid);
+                        logger.info("[第三方PS服务对接->发送流] 流上线,开始发流 callId->{}", callId);
+                        try {
+                            Thread.sleep(400);
+                        } catch (InterruptedException e) {
+                            throw new RuntimeException(e);
+                        }
+                        JSONObject jsonObject = zlmServerFactory.startSendRtpStream(mediaServerItem, param);
+                        if (jsonObject.getInteger("code") == 0) {
+                            logger.info("[第三方PS服务对接->发送流] 视频流发流成功,callId->{},param->{}", callId, param);
+                            redisTemplate.opsForValue().set(key, finalSendInfo);
+                        }else {
+                            redisTemplate.delete(key);
+                            logger.info("[第三方PS服务对接->发送流] 视频流发流失败,callId->{}, {}", callId, jsonObject.getString("msg"));
+                            throw new ControllerException(ErrorCode.ERROR100.getCode(), "[视频流发流失败] " + jsonObject.getString("msg"));
+                        }
+                        hookSubscribe.removeSubscribe(hookSubscribeForStreamChange);
+                    });
+        }
+    }
+
+    @GetMapping(value = "/send/stop")
+    @ResponseBody
+    @Operation(summary = "关闭发送流")
+    @Parameter(name = "callId", description = "整个过程的唯一标识,不传则使用随机端口发流", required = true)
+    public void closeSendRTP(String callId) {
+        logger.info("[第三方PS服务对接->关闭发送流] callId->{}", callId);
+        String key = VideoManagerConstants.WVP_OTHER_SEND_PS_INFO + userSetting.getServerId() + "_"  + callId;
+        OtherPsSendInfo sendInfo = (OtherPsSendInfo)redisTemplate.opsForValue().get(key);
+        if (sendInfo == null){
+            throw new ControllerException(ErrorCode.ERROR100.getCode(), "未开启发流");
+        }
+        Map<String, Object> param = new HashMap<>();
+        param.put("vhost","__defaultVhost__");
+        param.put("app",sendInfo.getPushApp());
+        param.put("stream",sendInfo.getPushStream());
+        param.put("ssrc",sendInfo.getPushSSRC());
+        MediaServerItem mediaServerItem = mediaServerService.getDefaultMediaServer();
+        Boolean result = zlmServerFactory.stopSendRtpStream(mediaServerItem, param);
+        if (!result) {
+            logger.info("[第三方PS服务对接->关闭发送流] 失败 callId->{}", callId);
+            throw new ControllerException(ErrorCode.ERROR100.getCode(), "停止发流失败");
+        }else {
+            logger.info("[第三方PS服务对接->关闭发送流] 成功 callId->{}", callId);
+        }
+        redisTemplate.delete(key);
+    }
+
+
+    @GetMapping(value = "/getTestPort")
+    @ResponseBody
+    public int getTestPort() {
+        MediaServerItem defaultMediaServer = mediaServerService.getDefaultMediaServer();
+
+//        for (int i = 0; i <300; i++) {
+//            new Thread(() -> {
+//                int nextPort = sendRtpPortManager.getNextPort(defaultMediaServer);
+//                try {
+//                    Thread.sleep((int)Math.random()*10);
+//                } catch (InterruptedException e) {
+//                    throw new RuntimeException(e);
+//                }
+//                System.out.println(nextPort);
+//            }).start();
+//        }
+
+        return sendRtpPortManager.getNextPort(defaultMediaServer);
+    }
+}

+ 2 - 4
src/main/java/com/genersoft/iot/vmp/vmanager/record/RecordController.java

@@ -91,10 +91,10 @@ public class RtpController {
         if (isSend != null && isSend && callId == null) {
             throw new ControllerException(ErrorCode.ERROR100.getCode(),"isSend为true时,CallID不能为空");
         }
-        int ssrcInt = 0;
+        long ssrcInt = 0;
         if (ssrc != null) {
             try {
-                ssrcInt = Integer.parseInt(ssrc);
+                ssrcInt = Long.parseLong(ssrc);
             }catch (NumberFormatException e) {
                 throw new ControllerException(ErrorCode.ERROR100.getCode(),"ssrc格式错误");
             }
@@ -247,7 +247,6 @@ public class RtpController {
             String is_Udp = isUdp ? "1" : "0";
             paramForAudio.put("is_udp", is_Udp);
             paramForAudio.put("src_port", sendInfo.getSendLocalPortForAudio());
-            paramForAudio.put("use_ps", "0");
             paramForAudio.put("only_audio", "1");
             if (ptForAudio != null) {
                 paramForAudio.put("pt", ptForAudio);
@@ -268,7 +267,6 @@ public class RtpController {
             String is_Udp = isUdp ? "1" : "0";
             paramForVideo.put("is_udp", is_Udp);
             paramForVideo.put("src_port", sendInfo.getSendLocalPortForVideo());
-            paramForVideo.put("use_ps", "0");
             paramForVideo.put("only_audio", "0");
             if (ptForVideo != null) {
                 paramForVideo.put("pt", ptForVideo);

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

@@ -58,7 +58,7 @@ public class UserController {
         if (user == null) {
             throw new ControllerException(ErrorCode.ERROR100.getCode(), "用户名或密码错误");
         }else {
-            String jwt = JwtUtils.createToken(username, password, user.getRole().getId());
+            String jwt = JwtUtils.createToken(username);
             response.setHeader(JwtUtils.getHeader(), jwt);
             user.setAccessToken(jwt);
         }

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

@@ -178,7 +178,7 @@ user-settings:
     # 国标是否录制
     record-sip: true
     # 是否将日志存储进数据库
-    logInDatebase: true
+    logInDatabase: true
     # 使用推流状态作为推流通道状态
     use-pushing-as-status: true
     # 使用来源请求ip作为streamIp,当且仅当你只有zlm节点它与wvp在一起的情况下开启

+ 12 - 12
src/main/resources/application-dev.yml

@@ -19,14 +19,14 @@ spring:
     # [可选] 数据库 DB
     database: 7
     # [可选] 访问密码,若你的redis服务器没有设置密码,就不需要用密码去连接
-    password:
+    password: luna
     # [可选] 超时时间
     timeout: 10000
     # mysql数据源
   datasource:
     type: com.zaxxer.hikari.HikariDataSource
     driver-class-name: com.mysql.cj.jdbc.Driver
-    url: jdbc:mysql://127.0.0.1:3306/test_gb-89wulian?useUnicode=true&characterEncoding=UTF8&rewriteBatchedStatements=true&serverTimezone=PRC&useSSL=false&allowMultiQueries=true
+    url: jdbc:mysql://127.0.0.1:3306/wvp?useUnicode=true&characterEncoding=UTF8&rewriteBatchedStatements=true&serverTimezone=PRC&useSSL=false&allowMultiQueries=true
     username: root
     password: root
     hikari:
@@ -38,7 +38,7 @@ spring:
       max-lifetime: 1200000                 # 是池中连接关闭后的最长生命周期(以毫秒为单位)
 #[可选] WVP监听的HTTP端口, 网页和接口调用都是这个端口
 server:
-  port: 18978
+  port: 8080
   # [可选] HTTPS配置, 默认不开启
   ssl:
     # [可选] 是否开启HTTPS访问
@@ -56,7 +56,7 @@ sip:
   # 如果要监听多张网卡,可以使用逗号分隔多个IP, 例如: 192.168.1.4,10.0.0.4
   # 如果不明白,就使用0.0.0.0,大部分情况都是可以的
   # 请不要使用127.0.0.1,任何包括localhost在内的域名都是不可以的。
-  ip: 192.168.1.18
+  ip: 172.19.128.50
   # [可选] 28181服务监听的端口
   port: 8116
   # 根据国标6.1.2中规定,domain宜采用ID统一编码的前十位编码。国标附录D中定义前8位为中心编码(由省级、市级、区级、基层编号组成,参照GB/T 2260-2007)
@@ -73,21 +73,21 @@ sip:
 
 #zlm 默认服务器配置
 media:
-  id: 89wulian-one
+  id: zlmediakit-local
   # [必须修改] zlm服务器的内网IP
-  ip: 192.168.1.18
+  ip: 172.19.128.50
   # [必须修改] zlm服务器的http.port
-  http-port: 80
+  http-port: 9092
   # [可选] 返回流地址时的ip,置空使用 media.ip
-  stream-ip: 192.168.1.18
+  stream-ip: 172.19.128.50
   # [可选] wvp在国标信令中使用的ip,此ip为摄像机可以访问到的ip, 置空使用 media.ip
-  sdp-ip: 192.168.1.18
+  sdp-ip: 172.19.128.50
   # [可选] zlm服务器的hook所使用的IP, 默认使用sip.ip
-  hook-ip: 192.168.1.18
+  hook-ip: 172.19.128.50
   # [可选] zlm服务器的http.sslport, 置空使用zlm配置文件配置
-  http-ssl-port: 443
+  http-ssl-port: 1443
   # [可选] zlm服务器的hook.admin_params=secret
-  secret: 035c73f7-bb6b-4889-a715-d9eb2d1925cc
+  secret: 10000
   # 启用多端口模式, 多端口模式使用端口区分每路流,兼容性更好。 单端口使用流的ssrc区分, 点播超时建议使用多端口测试
   rtp:
     # [可选] 是否启用多端口模式, 开启后会在portRange范围内选择端口用于媒体流传输

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

@@ -2,4 +2,4 @@ spring:
   application:
     name: wvp
   profiles:
-    active: local
+    active: dev

+ 2 - 4
web_src/src/App.vue

@@ -47,19 +47,17 @@
         :total="total">
       </el-pagination>
     </div>
-    <cloud-record-detail ref="cloudRecordDetail" v-if="recordDetail" :recordFile="chooseRecord" :mediaServerId="mediaServerId" :mediaServerPath="mediaServerPath" ></cloud-record-detail>
 
   </div>
 </template>
 
 <script>
 	import uiHeader from '../layout/UiHeader.vue'
-	import cloudRecordDetail from './CloudRecordDetail.vue'
   import MediaServer from './service/MediaServer'
 	export default {
 		name: 'app',
 		components: {
-			uiHeader, cloudRecordDetail
+      uiHeader
 		},
 		data() {
 			return {
@@ -178,7 +176,7 @@
         // }).catch(function (error) {
         //   console.log(error);
         // });
-
+        this.$router.push(`/cloudRecordDetail/${row.app}/${row.stream}`)
       },
       deleteRecord(){
 			  // TODO

+ 114 - 51
web_src/src/components/CloudRecordDetail.vue

@@ -1,33 +1,62 @@
 <template>
-	<div id="recordDetail">
-		<el-container>
+  <div id="recordDetail" style="width: 100%">
+    <div class="page-header" style="margin-bottom: 0">
+      <div class="page-title">
+        <el-page-header @back="backToList" content="云端录像"></el-page-header>
+      </div>
+
+      <div class="page-header-btn" v-if="!this.$route.params.mediaServerId" style="padding-right: 1rem">
+        <!--        节点选择:-->
+        <!--        <el-select size="mini" @change="chooseMediaChange" style="width: 16rem; margin-right: 1rem;" v-model="mediaServerId" placeholder="请选择" >-->
+        <!--          <el-option-->
+        <!--              key="undefined"-->
+        <!--              label="全部"-->
+        <!--              value="undefined">-->
+        <!--          </el-option>-->
+        <!--          <el-option-->
+        <!--              v-for="item in mediaServerList"-->
+        <!--              :key="item"-->
+        <!--              :label="item"-->
+        <!--              :value="item">-->
+        <!--          </el-option>-->
+        <!--        </el-select>-->
+        <b>节点:</b> {{ mediaServerId }}
+      </div>
+      <div v-if="this.$route.params.mediaServerId" style="margin-right: 1rem;">
+        <span>流媒体:{{ this.$route.params.mediaServerId }}</span>
+      </div>
+    </div>
+    <el-container>
       <el-aside width="260px">
         <div class="record-list-box-box">
           <div style="margin-top: 20px">
-            <el-date-picker size="mini"  style="width: 160px" v-model="chooseDate" :picker-options="pickerOptions" type="date" value-format="yyyy-MM-dd" placeholder="日期" @change="dateChange()"></el-date-picker>
-            <el-button size="mini" type="primary" icon="fa fa-cloud-download" style="margin: auto; margin-left: 12px " title="裁剪合并" @click="drawerOpen"></el-button>
+            <el-date-picker size="mini" v-model="chooseDate" :picker-options="pickerOptions" type="date"
+                            value-format="yyyy-MM-dd" placeholder="日期" @change="dateChange()"></el-date-picker>
+            <!--            <el-button :disabled="!mediaServerId" size="mini" type="primary" icon="fa fa-cloud-download" style="margin: auto; margin-left: 12px " title="裁剪合并" @click="drawerOpen"></el-button>-->
           </div>
           <div class="record-list-box" :style="recordListStyle">
             <ul v-if="detailFiles.length >0" class="infinite-list record-list" v-infinite-scroll="infiniteScroll" >
               <li v-for="(item,index) in detailFiles" :key="index" class="infinite-list-item record-list-item" >
-                <el-tag v-if="choosedFile != item" @click="chooseFile(item)">
+                <el-tag v-if="choosedFile !== item.filename" @click="chooseFile(item)">
                   <i class="el-icon-video-camera"  ></i>
-                  {{ item.substring(0,17)}}
+                  {{ getFileShowName(item.fileName) }}
                 </el-tag>
-                <el-tag type="danger" v-if="choosedFile == item">
+                <el-tag type="danger" v-if="choosedFile === item.filename">
                   <i class="el-icon-video-camera"  ></i>
-                  {{ item.substring(0,17)}}
+                  {{ getFileShowName(item.fileName) }}
                 </el-tag>
-                <a class="el-icon-download" style="color: #409EFF;font-weight: 600;margin-left: 10px;" :href="`${getFileBasePath()}/download.html?url=download/${recordFile.app}/${recordFile.stream}/${chooseDate}/${item}`" target="_blank" />
+                <a class="el-icon-download" style="color: #409EFF;font-weight: 600;margin-left: 10px;"
+                   :href="`${getFileBasePath(item)}/download.html?url=download/${app}/${stream}/${chooseDate}/${item.fileName}`"
+                   target="_blank"/>
               </li>
             </ul>
           </div>
-          <div v-if="detailFiles.length ==0" class="record-list-no-val" >暂无数据</div>
+          <div v-if="detailFiles.length === 0" class="record-list-no-val">暂无数据</div>
         </div>
 
 
       </el-aside>
-			<el-main style="padding: 22px">
+      <el-main style="padding: 22px">
         <div class="playBox" :style="playerStyle">
           <player ref="recordVideoPlayer" :videoUrl="videoUrl" :height="true" style="width: 100%" ></player>
         </div>
@@ -48,8 +77,8 @@
           </div>
         </div>
 
-			</el-main>
-		</el-container>
+      </el-main>
+    </el-container>
     <el-drawer
       title="录像下载"
       :visible.sync="drawer"
@@ -76,7 +105,8 @@
               <li class="task-list-item" v-for="(item, index) in taskListEnded" :key="index">
                 <div class="task-list-item-box" style="height: 2rem;line-height: 2rem;">
                   <span>{{ item.startTime.substr(10) }}-{{item.endTime.substr(10)}}</span>
-                  <a class="el-icon-download download-btn" :href="getFileBasePath()  + '/download.html?url=download/' + item.recordFile" target="_blank">
+                  <a class="el-icon-download download-btn" :href="getFileBasePath()  + '/download.html?url=download/' "
+                     target="_blank">
                   </a>
                 </div>
               </li>
@@ -113,11 +143,16 @@
 		components: {
 			uiHeader, player
 		},
-    props: ['recordFile', 'mediaServerId', 'dateFiles'],
+    // props: [ 'mediaServerId',],
 		data() {
 			return {
+        app: this.$route.params.app,
+        stream: this.$route.params.stream,
+        mediaServerId: this.$route.params.mediaServerId,
 			  dateFilesObj: [],
+        mediaServerList: [],
 			  detailFiles: [],
+        loading: false,
         chooseDate: null,
         videoUrl: null,
         choosedFile: null,
@@ -195,6 +230,9 @@
 		mounted() {
       this.recordListStyle.height = this.winHeight + "px";
       this.playerStyle["height"] = this.winHeight + "px";
+      console.log(this.app)
+      console.log(this.stream)
+      console.log(this.mediaServerId)
       // 查询当年有视频的日期
       this.getDateInYear(()=>{
         if (Object.values(this.dateFilesObj).length > 0){
@@ -216,7 +254,8 @@
         let chooseFullDate = new Date(this.chooseDate +" " + this.timeFormat);
         if (chooseFullDate.getFullYear() !== this.queryDate.getFullYear()
           || chooseFullDate.getMonth() !== this.queryDate.getMonth()){
-          // this.getDateInYear()
+          this.queryDate = chooseFullDate;
+          this.getDateInYear()
         }
         this.queryRecordDetails(()=>{
           if (this.detailFiles.length > 0){
@@ -242,48 +281,69 @@
         }
       },
       queryRecordDetails: function (callback){
-        let that = this;
-        that.$axios({
+        this.$axios({
           method: 'get',
-          url:`/record_proxy/${that.mediaServerId}/api/record/file/list`,
+          url: `/api/cloud/record/list`,
           params: {
-            app: that.recordFile.app,
-            stream: that.recordFile.stream,
-            startTime: that.chooseDate + " 00:00:00",
-            endTime: that.chooseDate + " 23:59:59",
-            page: that.currentPage,
-            count: that.count
+            app: this.app,
+            stream: this.stream,
+            startTime: this.chooseDate + " 00:00:00",
+            endTime: this.chooseDate + " 23:59:59",
+            page: this.currentPage,
+            count: this.count,
+            mediaServerId: this.mediaServerId
           }
-        }).then(function (res) {
+        }).then((res) => {
           if (res.data.code === 0) {
-            that.total = res.data.data.total;
-            that.detailFiles = that.detailFiles.concat(res.data.data.list);
+            this.total = res.data.data.total;
+            this.detailFiles = this.detailFiles.concat(res.data.data.list);
+            let temp = new Set()
+            for (let i = 0; i < this.detailFiles.length; i++) {
+              temp.add(this.detailFiles[i].mediaServerId)
+            }
+            this.mediaServerList = Array.from(temp)
+            if (this.mediaServerList.length === 1) {
+              this.mediaServerId = this.mediaServerList[0]
+            }
           }
-          that.loading = false;
+          this.loading = false;
           if (callback) callback();
-        }).catch(function (error) {
+        }).catch((error) => {
           console.log(error);
-          that.loading = false;
+          this.loading = false;
         });
       },
       chooseFile(file){
-        this.choosedFile = file;
 			  if (file == null) {
           this.videoUrl = "";
+          this.choosedFile = "";
         }else {
-          this.videoUrl = `${this.getFileBasePath()}/download/${this.recordFile.app}/${this.recordFile.stream}/${this.chooseDate}/${this.choosedFile}`
-
+          this.choosedFile = file.fileName;
+          this.videoUrl = `${this.getFileBasePath(file)}/download/${this.app}/${this.stream}/${this.chooseDate}/${this.choosedFile}`
           console.log(this.videoUrl)
         }
 
       },
+      backToList() {
+        this.$router.back()
+      },
+      getFileShowName(name) {
+        return name.substring(0, 2) + ":" + name.substring(2, 4) + ":" + name.substring(4, 6) + "-" +
+            name.substring(7, 9) + ":" + name.substring(9, 11) + ":" + name.substring(11, 13)
+      },
+      chooseMediaChange() {
+
+      },
+      getRecordList() {
 
-      getFileBasePath(){
+      },
+
+      getFileBasePath(item) {
         let basePath = ""
         if (axios.defaults.baseURL.startsWith("http")) {
-          basePath = `${axios.defaults.baseURL}/record_proxy/${this.mediaServerId}`
+          basePath = `${axios.defaults.baseURL}/record_proxy/${item.mediaServerId}`
         }else {
-          basePath = `${window.location.origin}${axios.defaults.baseURL}/record_proxy/${this.mediaServerId}`
+          basePath = `${window.location.origin}${axios.defaults.baseURL}/record_proxy/${item.mediaServerId}`
         }
         return basePath;
       },
@@ -316,7 +376,7 @@
       },
       getTimeForFile(file){
         console.log(file)
-        let timeStr = file.substring(0,17);
+        let timeStr = file.fileName.substring(0, 17);
         if(timeStr.indexOf("~") > 0){
           timeStr = timeStr.replaceAll("-",":")
         }
@@ -370,27 +430,30 @@
         });
       },
       getDateInYear(callback){
-        let that = this;
-        that.dateFilesObj = {};
+        this.dateFilesObj = {};
         this.$axios({
           method: 'get',
-          url:`/record_proxy/${that.mediaServerId}/api/record/date/list`,
+          url: `/api/cloud/record/date/list`,
           params: {
-            app: that.recordFile.app,
-            stream: that.recordFile.stream
+            app: this.app,
+            stream: this.stream,
+            year: this.queryDate.getFullYear(),
+            month: this.queryDate.getMonth() + 1,
+            mediaServerId: this.mediaServerId,
           }
-        }).then(function (res) {
+        }).then((res) => {
+          console.log(res)
           if (res.data.code === 0) {
             if (res.data.data.length > 0) {
               for (let i = 0; i < res.data.data.length; i++) {
-                that.dateFilesObj[res.data.data[i]] = res.data.data[i]
+                this.dateFilesObj[res.data.data[i]] = res.data.data[i]
               }
 
-              console.log(that.dateFilesObj)
+              console.log(this.dateFilesObj)
             }
           }
           if(callback)callback();
-        }).catch(function (error) {
+        }).catch((error) => {
           console.log(error);
         });
       },
@@ -414,8 +477,8 @@
       },
       addTask(){
         this.showTaskBox = true;
-        let startTimeStr = this.chooseDate + " " + this.detailFiles[0].substring(0,8);
-        let endTimeStr = this.chooseDate + " " + this.detailFiles[this.detailFiles.length - 1].substring(9,17);
+        let startTimeStr = this.chooseDate + " " + this.detailFiles[0].fileName.substring(0, 8);
+        let endTimeStr = this.chooseDate + " " + this.detailFiles[this.detailFiles.length - 1].fileName.substring(9, 17);
         this.taskTimeRange[0] = new Date(startTimeStr)
         this.taskTimeRange[1] = new Date(endTimeStr)
       },
@@ -425,8 +488,8 @@
           method: 'get',
           url:`/record_proxy/${that.mediaServerId}/api/record/file/download/task/add`,
           params: {
-            app: that.recordFile.app,
-            stream: that.recordFile.stream,
+            app: that.app,
+            stream: that.stream,
             startTime: moment(this.taskTimeRange[0]).format('YYYY-MM-DD HH:mm:ss'),
             endTime: moment(this.taskTimeRange[1]).format('YYYY-MM-DD HH:mm:ss'),
           }

+ 6 - 0
web_src/src/components/DeviceList.vue

@@ -89,6 +89,8 @@
           <el-button size="medium" icon="el-icon-position" type="text" v-if="!!scope.row.gbId"
                      @click="removeFromGB(scope.row)">移出国标
           </el-button>
+          <el-button size="medium" icon="el-icon-cloudy" type="text" @click="queryCloudRecords(scope.row)">云端录像
+          </el-button>
         </template>
       </el-table-column>
     </el-table>
@@ -257,6 +259,10 @@ export default {
         console.error(error);
       });
     },
+    queryCloudRecords: function (row) {
+
+      this.$router.push(`/cloudRecordDetail/${row.app}/${row.stream}`)
+    },
     importChannel: function () {
       this.$refs.importChannel.openDialog(() => {
 

+ 6 - 0
web_src/src/components/StreamProxyList.vue

@@ -91,6 +91,8 @@
           <el-button size="medium" icon="el-icon-check" type="text" :loading="scope.row.startBtnLoading" v-if="!scope.row.enable" @click="start(scope.row)">启用</el-button>
           <el-divider v-if="!scope.row.enable" direction="vertical"></el-divider>
           <el-button size="medium" icon="el-icon-delete" type="text" style="color: #f56c6c" @click="deleteStreamProxy(scope.row)">删除</el-button>
+          <el-button size="medium" icon="el-icon-cloudy" type="text" @click="queryCloudRecords(scope.row)">云端录像
+          </el-button>
         </template>
       </el-table-column>
     </el-table>
@@ -243,6 +245,10 @@
 				});
 
 			},
+      queryCloudRecords: function (row) {
+
+        this.$router.push(`/cloudRecordDetail/${row.app}/${row.stream}`)
+      },
 			deleteStreamProxy: function(row){
 				let that = this;
         this.$confirm('确定删除此代理吗?', '提示', {

+ 9 - 0
web_src/src/components/UserManager.vue

@@ -105,6 +105,9 @@
             <el-divider v-if="scope.row.subCount > 0 || scope.row.parental === 1" direction="vertical"></el-divider>
             <el-button size="medium" v-bind:disabled="device == null || device.online === 0" icon="el-icon-video-camera" type="text" @click="queryRecords(scope.row)">设备录像
             </el-button>
+            <el-button size="medium" v-bind:disabled="device == null || device.online === 0" icon="el-icon-cloudy"
+                       type="text" @click="queryCloudRecords(scope.row)">云端录像
+            </el-button>
           </template>
         </el-table-column>
       </el-table>
@@ -283,6 +286,12 @@ export default {
 
       this.$router.push(`/gbRecordDetail/${deviceId}/${channelId}`)
     },
+    queryCloudRecords: function (itemData) {
+      let deviceId = this.deviceId;
+      let channelId = itemData.channelId;
+
+      this.$router.push(`/cloudRecordDetail/rtp/${deviceId}_${channelId}`)
+    },
     stopDevicePush: function (itemData) {
       var that = this;
       this.$axios({

+ 11 - 0
web_src/src/components/common/ h265web.vue

@@ -12,6 +12,7 @@ import map from '../components/map.vue'
 import login from '../components/Login.vue'
 import parentPlatformList from '../components/ParentPlatformList.vue'
 import cloudRecord from '../components/CloudRecord.vue'
+import cloudRecordDetail from '../components/CloudRecordDetail.vue'
 import mediaServerManger from '../components/MediaServerManger.vue'
 import web from '../components/setting/Web.vue'
 import sip from '../components/setting/Sip.vue'
@@ -85,6 +86,16 @@ export default new VueRouter({
           name: 'cloudRecord',
           component: cloudRecord,
         },
+        {
+          path: '/cloudRecordDetail/:app/:stream',
+          name: 'cloudRecordDetail',
+          component: cloudRecordDetail,
+        },
+        {
+          path: '/cloudRecordDetail/:mediaServerId/:app/:stream',
+          name: 'cloudRecordDetail',
+          component: cloudRecordDetail,
+        },
         {
           path: '/mediaServerManger',
           name: 'mediaServerManger',