18339543638 3 سال پیش
والد
کامیت
4a205c4dc5
100فایلهای تغییر یافته به همراه10807 افزوده شده و 77 حذف شده
  1. 1 1
      jetlinks-components/common-component/src/main/java/org/jetlinks/community/annotation/MessageValueCodec.java
  2. 4 4
      jetlinks-components/network-component/coap-component/src/main/java/org/jetlinks/community/network/coap/resources/AbstractCoapResource.java
  3. 1 1
      jetlinks-components/network-component/network-core/src/main/java/org/jetlinks/community/network/DefaultNetworkType.java
  4. 10 10
      jetlinks-components/network-component/network-core/src/main/java/org/jetlinks/community/support/JetlinksExtendTopicMessageCodec.java
  5. 1 0
      jetlinks-components/network-component/pom.xml
  6. 32 0
      jetlinks-components/network-component/udp-component/pom.xml
  7. 32 0
      jetlinks-components/network-component/udp-component/src/main/java/org/jetlinks/community/network/udp/UdpMessage.java
  8. 29 0
      jetlinks-components/network-component/udp-component/src/main/java/org/jetlinks/community/network/udp/server/UdpServer.java
  9. 55 0
      jetlinks-components/network-component/udp-component/src/main/java/org/jetlinks/community/network/udp/server/UdpServerProperties.java
  10. 98 0
      jetlinks-components/network-component/udp-component/src/main/java/org/jetlinks/community/network/udp/server/UdpServerProvider.java
  11. 113 0
      jetlinks-components/network-component/udp-component/src/main/java/org/jetlinks/community/network/udp/server/VertxUdpServer.java
  12. 188 0
      jetlinks-components/network-component/udp-component/src/main/java/org/jetlinks/community/network/udp/server/device/UdpServerDeviceGateway.java
  13. 84 0
      jetlinks-components/network-component/udp-component/src/main/java/org/jetlinks/community/network/udp/server/device/UdpServerDeviceGatewayProvider.java
  14. 74 0
      jetlinks-components/network-component/udp-component/src/main/java/org/jetlinks/community/network/udp/server/session/UdpDeviceSession.java
  15. 188 0
      jetlinks-components/network-component/udp-component/udp-component.iml
  16. 1 1
      jetlinks-components/rule-engine-component/src/main/java/org/jetlinks/community/rule/engine/executor/DeviceMessageSendTaskExecutorProvider.java
  17. 0 5
      jetlinks-manager/bridge-manager/src/main/java/org/jetlinks/community/bridge/web/AliBridgeServerController.java
  18. 1 1
      jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/entity/DeviceTagEntity.java
  19. 3 3
      jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/measurements/DeviceEventMeasurement.java
  20. 2 2
      jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/measurements/DeviceEventsMeasurement.java
  21. 3 3
      jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/measurements/DevicePropertiesMeasurement.java
  22. 5 5
      jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/measurements/DevicePropertyMeasurement.java
  23. 3 3
      jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/measurements/status/DeviceStatusChangeMeasurement.java
  24. 4 4
      jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/measurements/topic/DeviceTopicMeasurement.java
  25. 1 1
      jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/message/DeviceCurrentStateSubscriptionProvider.java
  26. 3 3
      jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/message/DeviceMessageSendSubscriptionProvider.java
  27. 10 10
      jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/service/data/AbstractDeviceDataStoragePolicy.java
  28. 5 5
      jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/service/data/TimeSeriesColumnDeviceDataStoragePolicy.java
  29. 7 7
      jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/service/data/TimeSeriesRowDeviceDataStoreStoragePolicy.java
  30. 1 1
      jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/timeseries/DeviceEventTimeSeriesMetadata.java
  31. 1 1
      jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/timeseries/DeviceLogTimeSeriesMetadata.java
  32. 1 1
      jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/timeseries/DevicePropertiesTimeSeriesMetadata.java
  33. 1 1
      jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/timeseries/FixedPropertiesTimeSeriesMetadata.java
  34. 4 4
      jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/web/DeviceInstanceController.java
  35. 285 0
      jetlinks-manager/media-manager/media-manager.iml
  36. 94 0
      jetlinks-manager/media-manager/pom.xml
  37. 21 0
      jetlinks-manager/media-manager/src/main/java/org/jetlinks/community/media/bean/DeviceNotFoundEvent.java
  38. 66 0
      jetlinks-manager/media-manager/src/main/java/org/jetlinks/community/media/bean/EventResult.java
  39. 18 0
      jetlinks-manager/media-manager/src/main/java/org/jetlinks/community/media/bean/SSRCInfo.java
  40. 180 0
      jetlinks-manager/media-manager/src/main/java/org/jetlinks/community/media/bean/SendRtpItem.java
  41. 266 0
      jetlinks-manager/media-manager/src/main/java/org/jetlinks/community/media/bean/StreamInfo.java
  42. 18 0
      jetlinks-manager/media-manager/src/main/java/org/jetlinks/community/media/contanst/CmdType.java
  43. 72 0
      jetlinks-manager/media-manager/src/main/java/org/jetlinks/community/media/contanst/VideoManagerConstants.java
  44. 146 0
      jetlinks-manager/media-manager/src/main/java/org/jetlinks/community/media/controller/ApiStreamController.java
  45. 210 0
      jetlinks-manager/media-manager/src/main/java/org/jetlinks/community/media/core/DigestServerAuthenticationHelper2016.java
  46. 199 0
      jetlinks-manager/media-manager/src/main/java/org/jetlinks/community/media/entity/DeviceChannel.java
  47. 22 0
      jetlinks-manager/media-manager/src/main/java/org/jetlinks/community/media/entity/GbStream.java
  48. 211 0
      jetlinks-manager/media-manager/src/main/java/org/jetlinks/community/media/entity/MediaDevice.java
  49. 293 0
      jetlinks-manager/media-manager/src/main/java/org/jetlinks/community/media/entity/ParentPlatform.java
  50. 21 0
      jetlinks-manager/media-manager/src/main/java/org/jetlinks/community/media/entity/PlatformCatalog.java
  51. 14 0
      jetlinks-manager/media-manager/src/main/java/org/jetlinks/community/media/entity/PlatformGbStream.java
  52. 49 0
      jetlinks-manager/media-manager/src/main/java/org/jetlinks/community/media/entity/SipServerConfig.java
  53. 149 0
      jetlinks-manager/media-manager/src/main/java/org/jetlinks/community/media/entity/WvpSipDate.java
  54. 30 0
      jetlinks-manager/media-manager/src/main/java/org/jetlinks/community/media/enums/DeviceState.java
  55. 11 0
      jetlinks-manager/media-manager/src/main/java/org/jetlinks/community/media/enums/SipMethodType.java
  56. 15 0
      jetlinks-manager/media-manager/src/main/java/org/jetlinks/community/media/enums/StreamMode.java
  57. 24 0
      jetlinks-manager/media-manager/src/main/java/org/jetlinks/community/media/event/request/IMessageHandler.java
  58. 79 0
      jetlinks-manager/media-manager/src/main/java/org/jetlinks/community/media/event/request/ISipRequestProcessor.java
  59. 417 0
      jetlinks-manager/media-manager/src/main/java/org/jetlinks/community/media/event/request/impl/InviteRequestProcessor.java
  60. 99 0
      jetlinks-manager/media-manager/src/main/java/org/jetlinks/community/media/event/request/impl/MessageRequestProcessor.java
  61. 226 0
      jetlinks-manager/media-manager/src/main/java/org/jetlinks/community/media/event/request/impl/RegisterRequestProcessor.java
  62. 32 0
      jetlinks-manager/media-manager/src/main/java/org/jetlinks/community/media/event/request/impl/TimeoutProcessor.java
  63. 23 0
      jetlinks-manager/media-manager/src/main/java/org/jetlinks/community/media/event/request/message/IMessageHandler.java
  64. 44 0
      jetlinks-manager/media-manager/src/main/java/org/jetlinks/community/media/event/request/message/MessageHandlerAbstract.java
  65. 135 0
      jetlinks-manager/media-manager/src/main/java/org/jetlinks/community/media/event/request/message/MessageRequestProcessor.java
  66. 85 0
      jetlinks-manager/media-manager/src/main/java/org/jetlinks/community/media/event/request/message/notify/KeepaliveNotifyMessageHandler.java
  67. 56 0
      jetlinks-manager/media-manager/src/main/java/org/jetlinks/community/media/event/request/message/notify/NoSupportMessageHandler.java
  68. 75 0
      jetlinks-manager/media-manager/src/main/java/org/jetlinks/community/media/event/response/RegisterResponseProcessor.java
  69. 116 0
      jetlinks-manager/media-manager/src/main/java/org/jetlinks/community/media/service/LocalMediaDeviceService.java
  70. 337 0
      jetlinks-manager/media-manager/src/main/java/org/jetlinks/community/media/service/LocalPlayService.java
  71. 23 0
      jetlinks-manager/media-manager/src/main/java/org/jetlinks/community/media/service/SipService.java
  72. 143 0
      jetlinks-manager/media-manager/src/main/java/org/jetlinks/community/media/session/SsrcConfig.java
  73. 147 0
      jetlinks-manager/media-manager/src/main/java/org/jetlinks/community/media/sip/SipContext.java
  74. 216 0
      jetlinks-manager/media-manager/src/main/java/org/jetlinks/community/media/sip/SipRequestProcessorParent.java
  75. 110 0
      jetlinks-manager/media-manager/src/main/java/org/jetlinks/community/media/sip/SipServerHelper.java
  76. 27 0
      jetlinks-manager/media-manager/src/main/java/org/jetlinks/community/media/sip/processor/ISipProcessObserver.java
  77. 133 0
      jetlinks-manager/media-manager/src/main/java/org/jetlinks/community/media/sip/processor/SipProcessorObserver.java
  78. 210 0
      jetlinks-manager/media-manager/src/main/java/org/jetlinks/community/media/storage/IRedisCatchStorage.java
  79. 458 0
      jetlinks-manager/media-manager/src/main/java/org/jetlinks/community/media/storage/IVideoManagerStorager.java
  80. 516 0
      jetlinks-manager/media-manager/src/main/java/org/jetlinks/community/media/storage/impl/RedisCatchStorageImpl.java
  81. 305 0
      jetlinks-manager/media-manager/src/main/java/org/jetlinks/community/media/transmit/SIPRequestHeaderProvider.java
  82. 190 0
      jetlinks-manager/media-manager/src/main/java/org/jetlinks/community/media/transmit/cmd/SipCommander.java
  83. 15 0
      jetlinks-manager/media-manager/src/main/java/org/jetlinks/community/media/vmanager/gb28181/play/bean/PlayResult.java
  84. 502 0
      jetlinks-manager/media-manager/src/main/java/org/jetlinks/community/media/zlm/ZLMHttpHookListener.java
  85. 118 0
      jetlinks-manager/media-manager/src/main/java/org/jetlinks/community/media/zlm/ZLMHttpHookSubscribe.java
  86. 217 0
      jetlinks-manager/media-manager/src/main/java/org/jetlinks/community/media/zlm/ZLMMediaListManager.java
  87. 287 0
      jetlinks-manager/media-manager/src/main/java/org/jetlinks/community/media/zlm/ZLMRESTfulUtils.java
  88. 269 0
      jetlinks-manager/media-manager/src/main/java/org/jetlinks/community/media/zlm/ZLMRTPServerFactory.java
  89. 200 0
      jetlinks-manager/media-manager/src/main/java/org/jetlinks/community/media/zlm/ZLMRunner.java
  90. 805 0
      jetlinks-manager/media-manager/src/main/java/org/jetlinks/community/media/zlm/ZLMServerConfig.java
  91. 405 0
      jetlinks-manager/media-manager/src/main/java/org/jetlinks/community/media/zlm/dto/MediaItem.java
  92. 101 0
      jetlinks-manager/media-manager/src/main/java/org/jetlinks/community/media/zlm/dto/MediaServerItem.java
  93. 23 0
      jetlinks-manager/media-manager/src/main/java/org/jetlinks/community/media/zlm/dto/OriginType.java
  94. 24 0
      jetlinks-manager/media-manager/src/main/java/org/jetlinks/community/media/zlm/dto/StreamProxyItem.java
  95. 120 0
      jetlinks-manager/media-manager/src/main/java/org/jetlinks/community/media/zlm/dto/StreamPushItem.java
  96. 20 0
      jetlinks-manager/media-manager/src/main/java/org/jetlinks/community/media/zlm/dto/ZLMRunInfo.java
  97. 24 0
      jetlinks-manager/media-manager/src/main/java/org/jetlinks/community/media/zlm/event/ZLMEventAbstract.java
  98. 73 0
      jetlinks-manager/media-manager/src/main/java/org/jetlinks/community/media/zlm/event/ZLMKeepliveTimeoutListener.java
  99. 11 0
      jetlinks-manager/media-manager/src/main/java/org/jetlinks/community/media/zlm/event/ZLMOfflineEvent.java
  100. 11 0
      jetlinks-manager/media-manager/src/main/java/org/jetlinks/community/media/zlm/event/ZLMOnlineEvent.java

+ 1 - 1
jetlinks-components/common-component/src/main/java/org/jetlinks/community/annotation/MessageValueCodec.java

@@ -36,7 +36,7 @@ public class MessageValueCodec implements ValueCodec<Object, Object> {
         Object msgType = jsonObject.get("messageType");
         if(MessageType.CHILD.name().equals(msgType)){
             JSONObject childDeviceMessage = (JSONObject) jsonObject.get("childDeviceMessage");
-            return ChildDeviceMessage.create(String.valueOf(jsonObject.get("deviceId")), (DeviceMessage) this.decode(childDeviceMessage));
+            return ChildDeviceMessage.create(String.valueOf(jsonObject.get("id")), (DeviceMessage) this.decode(childDeviceMessage));
         }
         MessageType messageType = MessageType.of(String.valueOf(msgType)).orElse(MessageType.UNKNOWN);
         return jsonObject.toJavaObject(messageType.getNewInstance().get().getClass());

+ 4 - 4
jetlinks-components/network-component/coap-component/src/main/java/org/jetlinks/community/network/coap/resources/AbstractCoapResource.java

@@ -29,7 +29,7 @@ import java.util.function.Function;
 public abstract class AbstractCoapResource extends CoapResource {
     private final EmitterProcessor<CoapExchangeMessage> processor;
 
-    public static final String prefixTopicName="{productId}/{deviceId}/**";
+    public static final String prefixTopicName="{productId}/{id}/**";
 
     public static final AntPathMatcher matcher = new AntPathMatcher(File.separator);
     public AbstractCoapResource(EmitterProcessor<CoapExchangeMessage> processor) {
@@ -58,21 +58,21 @@ public abstract class AbstractCoapResource extends CoapResource {
         path=path.startsWith("/")?path.substring(1):path;
         String[] split = path.split("/");
         if(split.length<2){
-            log.warn("Coap连接,deviceId:[{}],productId:[{}],不能为空",exchange.getDeviceId(),exchange.getProductId());
+            log.warn("Coap连接,id:[{}],productId:[{}],不能为空",exchange.getDeviceId(),exchange.getProductId());
             exchange.reject();
         }
         exchange.setDeviceId(split[1]);
         exchange.setProductId(split[0]);
         Map<String, String> elseMap = new HashMap<>();
         map.forEach((k,v)->{
-            if(!"deviceId".equals(k)&&!"productId".equals(k)){
+            if(!"id".equals(k)&&!"productId".equals(k)){
                 elseMap.put(k,v);
             }
         });
         exchange.setElseParams(elseMap);
         if (StrUtil.isNullOrUndefined(exchange.getDeviceId())
             || StrUtil.isNullOrUndefined(exchange.getProductId())) {
-            log.warn("Coap连接,deviceId:[{}],productId:[{}],不能为空",exchange.getDeviceId(),exchange.getProductId());
+            log.warn("Coap连接,id:[{}],productId:[{}],不能为空",exchange.getDeviceId(),exchange.getProductId());
             exchange.reject();
         }
         if(processor.hasDownstreams()){

+ 1 - 1
jetlinks-components/network-component/network-core/src/main/java/org/jetlinks/community/network/DefaultNetworkType.java

@@ -26,7 +26,7 @@ public enum DefaultNetworkType implements NetworkType, EnumDict<String> {
     WEB_SOCKET_CLIENT("WebSocket客户端"),
     WEB_SOCKET_SERVER("WebSocket服务"),
 
-    UDP("UDP"),
+    UDP("UDP服务端"),
 
     COAP_CLIENT("CoAP客户端"),
     COAP_SERVER("CoAP服务"),

+ 10 - 10
jetlinks-components/network-component/network-core/src/main/java/org/jetlinks/community/support/JetlinksExtendTopicMessageCodec.java

@@ -55,7 +55,7 @@ public class JetlinksExtendTopicMessageCodec {
         private boolean log;
         public DecodeResult(String topic) {
             this.topic = topic;
-            args = TopicUtils.getPathVariables("/{productId}/{deviceId}/**", topic);
+            args = TopicUtils.getPathVariables("/{productId}/{id}/**", topic);
             if (topic.contains("child")) {
                 child = true;
                 args.putAll(TopicUtils.getPathVariables("/**/child/{childDeviceId}/**", topic));
@@ -93,7 +93,7 @@ public class JetlinksExtendTopicMessageCodec {
         private final String topic;
 
         public String getDeviceId() {
-            return args.get("deviceId");
+            return args.get("id");
         }
 
         public String getChildDeviceId() {
@@ -105,7 +105,7 @@ public class JetlinksExtendTopicMessageCodec {
 
     protected JetlinksExtendTopicMessageCodec.EncodedTopic encode(String deviceId, Message message) {
 
-        Assert.hasText(deviceId, "deviceId can not be null");
+        Assert.hasText(deviceId, "id can not be null");
         Assert.notNull(message, "message can not be null");
 
         if (message instanceof ReadPropertyMessage) {
@@ -113,7 +113,7 @@ public class JetlinksExtendTopicMessageCodec {
             JSONObject mqttData = new JSONObject();
             mqttData.put("messageId", message.getMessageId());
             mqttData.put("properties", ((ReadPropertyMessage) message).getProperties());
-            mqttData.put("deviceId", deviceId);
+            mqttData.put("id", deviceId);
 
             return new JetlinksExtendTopicMessageCodec.EncodedTopic(topic, mqttData);
         } else if (message instanceof WritePropertyMessage) {
@@ -121,7 +121,7 @@ public class JetlinksExtendTopicMessageCodec {
             JSONObject mqttData = new JSONObject();
             mqttData.put("messageId", message.getMessageId());
             mqttData.put("properties", ((WritePropertyMessage) message).getProperties());
-            mqttData.put("deviceId", deviceId);
+            mqttData.put("id", deviceId);
 
             return new JetlinksExtendTopicMessageCodec.EncodedTopic(topic, mqttData);
         } else if (message instanceof FunctionInvokeMessage) {
@@ -131,7 +131,7 @@ public class JetlinksExtendTopicMessageCodec {
             mqttData.put("messageId", message.getMessageId());
             mqttData.put("function", invokeMessage.getFunctionId());
             mqttData.put("inputs", invokeMessage.getInputs());
-            mqttData.put("deviceId", deviceId);
+            mqttData.put("id", deviceId);
 
             return new JetlinksExtendTopicMessageCodec.EncodedTopic(topic, mqttData);
         } else if (message instanceof UpgradeFirmwareMessage) {
@@ -145,14 +145,14 @@ public class JetlinksExtendTopicMessageCodec {
             mqttData.put("taskId",firmwareMessage.getTaskId());
             mqttData.put("signMethod", firmwareMessage.getSignMethod());
             mqttData.put("parameters", firmwareMessage.getParameters());
-            mqttData.put("deviceId", deviceId);
+            mqttData.put("id", deviceId);
 
             return new JetlinksExtendTopicMessageCodec.EncodedTopic(topic, mqttData);
         } else if (message instanceof ReadFirmwareMessage) {
             String topic = "/" .concat(deviceId).concat("/firmware/read");
             JSONObject mqttData = new JSONObject();
             mqttData.put("messageId", message.getMessageId());
-            mqttData.put("deviceId", deviceId);
+            mqttData.put("id", deviceId);
             return new JetlinksExtendTopicMessageCodec.EncodedTopic(topic, mqttData);
         } else if (message instanceof TimeSyncReplyMessage) {
             String topic = "/" .concat(deviceId).concat("/time-sync/reply");
@@ -171,13 +171,13 @@ public class JetlinksExtendTopicMessageCodec {
             mqttData.put("signMethod", firmwareMessage.getSignMethod());
             mqttData.put("parameters", firmwareMessage.getParameters());
             mqttData.put("taskId",firmwareMessage.getTaskId());
-            mqttData.put("deviceId", deviceId);
+            mqttData.put("id", deviceId);
             return new JetlinksExtendTopicMessageCodec.EncodedTopic(topic, mqttData);
         } else if (message instanceof ChildDeviceMessage) {
             ChildDeviceMessage childDeviceMessage = ((ChildDeviceMessage) message);
             JetlinksExtendTopicMessageCodec.EncodedTopic result = encode(childDeviceMessage.getChildDeviceId(), childDeviceMessage.getChildDeviceMessage());
             String topic = "/" .concat(deviceId).concat("/child").concat(result.topic);
-            result.payload.put("deviceId", childDeviceMessage.getChildDeviceId());
+            result.payload.put("id", childDeviceMessage.getChildDeviceId());
 
             return new JetlinksExtendTopicMessageCodec.EncodedTopic(topic, result.payload);
         }

+ 1 - 0
jetlinks-components/network-component/pom.xml

@@ -16,6 +16,7 @@
         <module>mqtt-component</module>
         <module>tcp-component</module>
         <module>coap-component</module>
+        <module>udp-component</module>
     </modules>
 
     <artifactId>network-component</artifactId>

+ 32 - 0
jetlinks-components/network-component/udp-component/pom.xml

@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <parent>
+        <artifactId>network-component</artifactId>
+        <groupId>org.jetlinks.community</groupId>
+        <version>1.10.0-SNAPSHOT</version>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+
+    <artifactId>udp-component</artifactId>
+
+    <dependencies>
+    <dependency>
+        <groupId>${project.groupId}</groupId>
+        <artifactId>network-core</artifactId>
+        <version>${project.version}</version>
+    </dependency>
+
+    <dependency>
+        <groupId>io.vertx</groupId>
+        <artifactId>vertx-core</artifactId>
+    </dependency>
+
+    <dependency>
+        <groupId>${project.groupId}</groupId>
+        <artifactId>gateway-component</artifactId>
+        <version>${project.version}</version>
+    </dependency>
+   </dependencies>
+</project>

+ 32 - 0
jetlinks-components/network-component/udp-component/src/main/java/org/jetlinks/community/network/udp/UdpMessage.java

@@ -0,0 +1,32 @@
+package org.jetlinks.community.network.udp;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.ByteBufUtil;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+import org.jetlinks.core.message.codec.EncodedMessage;
+
+/**
+ * @author bsetfeng
+ * @author zhouhao
+ * @since 1.0
+ **/
+@Getter
+@Setter
+@AllArgsConstructor
+@NoArgsConstructor
+public class UdpMessage implements EncodedMessage {
+
+    private ByteBuf payload;
+
+    @Override
+    public String toString() {
+        StringBuilder builder = new StringBuilder();
+
+        ByteBufUtil.appendPrettyHexDump(builder,payload);
+
+        return builder.toString();
+    }
+}

+ 29 - 0
jetlinks-components/network-component/udp-component/src/main/java/org/jetlinks/community/network/udp/server/UdpServer.java

@@ -0,0 +1,29 @@
+package org.jetlinks.community.network.udp.server;
+
+import io.vertx.core.net.NetSocket;
+import org.jetlinks.community.network.Network;
+import org.jetlinks.community.network.udp.UdpMessage;
+import reactor.core.publisher.Flux;
+
+/**
+ * TCP服务
+ *
+ * @author zhouhao
+ * @version 1.0
+ **/
+public interface UdpServer extends Network {
+
+    /**
+     * 订阅客户端连接
+     *
+     * @return 客户端流
+     * @see NetSocket
+     */
+    Flux<UdpMessage> handleConnection();
+
+    /**
+     * 关闭服务端
+     */
+    @Override
+    void shutdown();
+}

+ 55 - 0
jetlinks-components/network-component/udp-component/src/main/java/org/jetlinks/community/network/udp/server/UdpServerProperties.java

@@ -0,0 +1,55 @@
+package org.jetlinks.community.network.udp.server;
+
+import io.vertx.core.net.NetServerOptions;
+import io.vertx.core.net.SocketAddress;
+import lombok.*;
+import org.jetlinks.community.ValueObject;
+import org.jetlinks.rule.engine.executor.PayloadType;
+import org.springframework.util.StringUtils;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * @author bsetfeng
+ * @author zhouhao
+ * @since 1.0
+ **/
+@Getter
+@Setter
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class UdpServerProperties implements ValueObject {
+
+    private String id;
+
+    private NetServerOptions options;
+
+    private PayloadType payloadType;
+
+    private Map<String, Object> parserConfiguration = new HashMap<>();
+
+    private String host;
+
+    private int port;
+
+//    private boolean ssl;
+
+    //服务实例数量(线程数)
+    private int instance = Runtime.getRuntime().availableProcessors();
+
+    private String certId;
+
+    public SocketAddress createSocketAddress() {
+        if (StringUtils.isEmpty(host)) {
+            host = "localhost";
+        }
+        return SocketAddress.inetSocketAddress(port, host);
+    }
+
+    @Override
+    public Map<String, Object> values() {
+        return parserConfiguration;
+    }
+}

+ 98 - 0
jetlinks-components/network-component/udp-component/src/main/java/org/jetlinks/community/network/udp/server/UdpServerProvider.java

@@ -0,0 +1,98 @@
+package org.jetlinks.community.network.udp.server;
+
+import io.vertx.core.Vertx;
+import io.vertx.core.net.NetServer;
+import io.vertx.core.net.NetServerOptions;
+import lombok.extern.slf4j.Slf4j;
+import org.hswebframework.web.bean.FastBeanCopier;
+import org.jetlinks.community.network.*;
+import org.jetlinks.core.metadata.ConfigMetadata;
+import org.springframework.stereotype.Component;
+import reactor.core.publisher.Mono;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import java.time.Duration;
+import java.util.ArrayList;
+import java.util.List;
+
+@Component
+@Slf4j
+public class UdpServerProvider implements NetworkProvider<UdpServerProperties> {
+
+    private final Vertx vertx;
+
+
+    public UdpServerProvider(Vertx vertx) {
+        this.vertx = vertx;
+    }
+
+    @Nonnull
+    @Override
+    public NetworkType getType() {
+        return DefaultNetworkType.UDP;
+    }
+
+    @Nonnull
+    @Override
+    public VertxUdpServer createNetwork(@Nonnull UdpServerProperties properties) {
+
+        VertxUdpServer udpServer = new VertxUdpServer(properties.getId());
+        initUdpServer(udpServer, properties);
+
+        return udpServer;
+    }
+
+    private void initUdpServer(VertxUdpServer udpServer, UdpServerProperties properties) {
+        int instance = Math.max(2, properties.getInstance());
+        List<NetServer> instances = new ArrayList<>(instance);
+        for (int i = 0; i < instance; i++) {
+            instances.add(vertx.createNetServer(properties.getOptions()));
+        }
+        udpServer.setServer(instances);
+//        udpServer.setKeepAliveTimeout(properties.getLong("keepAliveTimeout", Duration.ofMinutes(10).toMillis()));
+        for (NetServer netServer : instances) {
+            netServer.listen(properties.createSocketAddress(), result -> {
+                if (result.succeeded()) {
+                    log.info("udp server startup on {}", result.result().actualPort());
+                } else {
+                    log.error("startup udp server error", result.cause());
+                }
+            });
+        }
+    }
+
+    @Override
+    public void reload(@Nonnull Network network, @Nonnull UdpServerProperties properties) {
+        VertxUdpServer udpServer = ((VertxUdpServer) network);
+        udpServer.shutdown();
+        initUdpServer(udpServer, properties);
+    }
+
+    @Nullable
+    @Override
+    public ConfigMetadata getConfigMetadata() {
+        return null;
+    }
+
+    @Nonnull
+    @Override
+    public Mono<UdpServerProperties> createConfig(@Nonnull NetworkProperties properties) {
+        return Mono.defer(() -> {
+            UdpServerProperties config = FastBeanCopier.copy(properties.getConfigurations(), new UdpServerProperties());
+            config.setId(properties.getId());
+            if (config.getOptions() == null) {
+                config.setOptions(new NetServerOptions());
+            }
+//            if (config.isSsl()) {
+//                config.getOptions().setSsl(true);
+//                return certificateManager.getCertificate(config.getCertId())
+//                    .map(VertxKeyCertTrustOptions::new)
+//                    .doOnNext(config.getOptions()::setKeyCertOptions)
+//                    .doOnNext(config.getOptions()::setTrustOptions)
+//                    .thenReturn(config);
+//            }
+            return Mono.just(config);
+        });
+    }
+}

+ 113 - 0
jetlinks-components/network-component/udp-component/src/main/java/org/jetlinks/community/network/udp/server/VertxUdpServer.java

@@ -0,0 +1,113 @@
+package org.jetlinks.community.network.udp.server;
+
+import io.netty.buffer.ByteBuf;
+import io.netty.buffer.ByteBufUtil;
+import io.vertx.core.buffer.Buffer;
+import io.vertx.core.net.NetServer;
+import io.vertx.core.net.NetSocket;
+import lombok.Getter;
+import lombok.Setter;
+import lombok.extern.slf4j.Slf4j;
+import org.jetlinks.community.network.DefaultNetworkType;
+import org.jetlinks.community.network.NetworkType;
+import org.jetlinks.community.network.udp.UdpMessage;
+import reactor.core.publisher.EmitterProcessor;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.FluxSink;
+import reactor.netty.udp.UdpClient;
+
+import javax.net.ssl.SSLSession;
+import java.time.Duration;
+import java.util.Collection;
+import java.util.function.Function;
+
+/**
+ * @author bsetfeng
+ * @since 1.0
+ **/
+@Slf4j
+public class VertxUdpServer implements UdpServer {
+
+    private Collection<NetServer> udpServers;
+
+    @Getter
+    private final String id;
+
+    private final EmitterProcessor<UdpMessage> processor = EmitterProcessor.create(false);
+
+    private final FluxSink<UdpMessage> sink = processor.sink(FluxSink.OverflowStrategy.BUFFER);
+
+
+    public VertxUdpServer(String id) {
+        this.id = id;
+    }
+
+    @Override
+    public Flux<UdpMessage> handleConnection() {
+        return processor
+            .map(Function.identity());
+    }
+
+    private void execute(Runnable runnable) {
+        try {
+            runnable.run();
+        } catch (Exception e) {
+            log.warn("close udp server error", e);
+        }
+    }
+
+    public void setServer(Collection<NetServer> servers) {
+        if (this.udpServers != null && !this.udpServers.isEmpty()) {
+            shutdown();
+        }
+        this.udpServers = servers;
+
+        for (NetServer udpServer : this.udpServers) {
+            udpServer.connectHandler(this::acceptUdpConnection);
+        }
+
+    }
+
+
+    protected void acceptUdpConnection(NetSocket socket) {
+        if (!processor.hasDownstreams()) {
+            log.warn("not handler for udp client[{}]", socket.remoteAddress());
+            socket.close();
+            return;
+        }
+        socket
+            .handler(buffer-> sink.next(new UdpMessage(buffer.getByteBuf())))
+            .exceptionHandler(err -> {
+            log.error("udp server client [{}] error", socket.remoteAddress(), err);
+        }).closeHandler((nil) -> {
+            log.debug("udp server client [{}] closed", socket.remoteAddress());
+        });
+        log.debug("accept udp client [{}] connection", socket.remoteAddress());
+
+    }
+
+    @Override
+    public NetworkType getType() {
+        return DefaultNetworkType.UDP;
+    }
+
+    @Override
+    public void shutdown() {
+        if (null != udpServers) {
+            for (NetServer udpServer : udpServers) {
+                execute(udpServer::close);
+            }
+            udpServers = null;
+        }
+    }
+
+    @Override
+    public boolean isAlive() {
+        return udpServers != null;
+    }
+
+    @Override
+    public boolean isAutoReload() {
+        return false;
+    }
+}

+ 188 - 0
jetlinks-components/network-component/udp-component/src/main/java/org/jetlinks/community/network/udp/server/device/UdpServerDeviceGateway.java

@@ -0,0 +1,188 @@
+package org.jetlinks.community.network.udp.server.device;
+
+import io.vertx.core.net.NetSocket;
+import lombok.Getter;
+import lombok.extern.slf4j.Slf4j;
+import org.hswebframework.web.logger.ReactiveLogger;
+import org.jetlinks.community.gateway.DeviceGateway;
+import org.jetlinks.community.gateway.monitor.DeviceGatewayMonitor;
+import org.jetlinks.community.gateway.monitor.GatewayMonitors;
+import org.jetlinks.community.gateway.monitor.MonitorSupportDeviceGateway;
+import org.jetlinks.community.network.DefaultNetworkType;
+import org.jetlinks.community.network.NetworkType;
+import org.jetlinks.community.network.udp.UdpMessage;
+import org.jetlinks.community.network.udp.server.UdpServer;
+import org.jetlinks.community.network.utils.DeviceGatewayHelper;
+import org.jetlinks.core.ProtocolSupport;
+import org.jetlinks.core.ProtocolSupports;
+import org.jetlinks.core.device.DeviceRegistry;
+import org.jetlinks.core.message.DeviceMessage;
+import org.jetlinks.core.message.Message;
+import org.jetlinks.core.message.codec.DefaultTransport;
+import org.jetlinks.core.message.codec.FromDeviceMessageContext;
+import org.jetlinks.core.message.codec.Transport;
+import org.jetlinks.core.server.session.DeviceSessionManager;
+import org.jetlinks.supports.server.DecodedClientMessageHandler;
+import reactor.core.Disposable;
+import reactor.core.publisher.EmitterProcessor;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.FluxSink;
+import reactor.core.publisher.Mono;
+import reactor.core.scheduler.Schedulers;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.LongAdder;
+
+@Slf4j(topic = "system.udp.gateway")
+public class UdpServerDeviceGateway implements DeviceGateway, MonitorSupportDeviceGateway {
+
+    @Getter
+    private final String id;
+
+    /**
+     * 维护所有创建的tcp server
+     */
+    private final UdpServer udpServer;
+
+    private final String protocol;
+
+    private final ProtocolSupports supports;
+
+    private final DeviceRegistry registry;
+
+    private final DeviceSessionManager sessionManager;
+
+    private final DeviceGatewayMonitor gatewayMonitor;
+    /**
+     * 连接计数器
+     */
+    private final LongAdder counter = new LongAdder();
+
+    private final EmitterProcessor<Message> processor = EmitterProcessor.create(false);
+
+    private final FluxSink<Message> sink = processor.sink(FluxSink.OverflowStrategy.BUFFER);
+
+    private final AtomicBoolean started = new AtomicBoolean();
+    private final DeviceGatewayHelper helper;
+    /**
+     * 数据流控开关
+     */
+    private Disposable disposable;
+
+    public UdpServerDeviceGateway(String id,
+                                  String protocol,
+                                  ProtocolSupports supports,
+                                  DeviceRegistry deviceRegistry,
+                                  DecodedClientMessageHandler clientMessageHandler,
+                                  DeviceSessionManager sessionManager,
+                                  UdpServer udpServer) {
+        this.gatewayMonitor = GatewayMonitors.getDeviceGatewayMonitor(id);
+        this.id = id;
+        this.protocol = protocol;
+        this.registry = deviceRegistry;
+        this.supports = supports;
+        this.udpServer = udpServer;
+        this.sessionManager = sessionManager;
+        this.helper = new DeviceGatewayHelper(registry, sessionManager, clientMessageHandler);
+    }
+
+    public Mono<ProtocolSupport> getProtocol() {
+        return supports.getProtocol(protocol);
+    }
+
+    /**
+     * 当前总链接
+     *
+     * @return 当前总链接
+     */
+    @Override
+    public long totalConnection() {
+        return counter.sum();
+    }
+
+    /**
+     * 传输协议
+     *
+     * @return {@link DefaultTransport}
+     */
+    @Override
+    public Transport getTransport() {
+        return DefaultTransport.UDP;
+    }
+
+    /**
+     * 网络类型
+     *
+     * @return {@link  DefaultNetworkType}
+     */
+    @Override
+    public NetworkType getNetworkType() {
+        return DefaultNetworkType.UDP;
+    }
+
+    /**
+     * 启动网关
+     */
+    private void doStart() {
+        if (started.getAndSet(true) || disposable != null) {
+            return;
+        }
+        disposable = udpServer
+            .handleConnection()
+            .publishOn(Schedulers.parallel())
+            .flatMap(this::handleMessage)
+            .onErrorContinue((err, obj) -> log.error(err.getMessage(), err))
+            .subscriberContext(ReactiveLogger.start("network", udpServer.getId()))
+            .subscribe(
+                ignore -> {
+                },
+                error -> log.error(error.getMessage(), error)
+            );
+    }
+
+    private Mono<Void> handleMessage(UdpMessage message) {
+        return getProtocol()
+            .flatMap(pt -> pt.getMessageCodec(getTransport()))
+            //udp 不保存会话信息
+            .flatMapMany(codec -> codec.decode(FromDeviceMessageContext.of(null, message, registry)))
+            .cast(DeviceMessage.class)
+            .doOnNext(msg -> gatewayMonitor.receivedMessage())
+            .doOnNext(msg->{
+                if (processor.hasDownstreams()) {
+                    sink.next(msg);
+                }
+            })
+            .doOnEach(ReactiveLogger.onError(err -> log.error("处理UDP消息失败:\n{}",
+                message
+                , err)))
+            .then();
+    }
+
+    @Override
+    public Flux<Message> onMessage() {
+        return processor;
+    }
+
+    @Override
+    public Mono<Void> pause() {
+        return Mono.fromRunnable(() -> started.set(false));
+    }
+
+    @Override
+    public Mono<Void> startup() {
+        return Mono.fromRunnable(this::doStart);
+    }
+
+    @Override
+    public Mono<Void> shutdown() {
+        return Mono.fromRunnable(() -> {
+            started.set(false);
+            disposable.dispose();
+            disposable = null;
+        });
+    }
+
+    @Override
+    public boolean isAlive() {
+        return started.get();
+    }
+}

+ 84 - 0
jetlinks-components/network-component/udp-component/src/main/java/org/jetlinks/community/network/udp/server/device/UdpServerDeviceGatewayProvider.java

@@ -0,0 +1,84 @@
+package org.jetlinks.community.network.udp.server.device;
+
+import org.jetlinks.community.gateway.DeviceGateway;
+import org.jetlinks.community.gateway.supports.DeviceGatewayProperties;
+import org.jetlinks.community.gateway.supports.DeviceGatewayProvider;
+import org.jetlinks.community.network.DefaultNetworkType;
+import org.jetlinks.community.network.NetworkManager;
+import org.jetlinks.community.network.NetworkType;
+import org.jetlinks.community.network.udp.server.UdpServer;
+import org.jetlinks.core.ProtocolSupports;
+import org.jetlinks.core.device.DeviceRegistry;
+import org.jetlinks.core.server.session.DeviceSessionManager;
+import org.jetlinks.supports.server.DecodedClientMessageHandler;
+import org.springframework.stereotype.Component;
+import org.springframework.util.Assert;
+import reactor.core.publisher.Mono;
+
+/**
+ * TCP服务设备网关提供商
+ *
+ * @author zhouhao
+ * @since 1.0
+ */
+@Component
+public class UdpServerDeviceGatewayProvider implements DeviceGatewayProvider {
+
+    private final NetworkManager networkManager;
+
+    private final DeviceRegistry registry;
+
+    private final DeviceSessionManager sessionManager;
+
+    private final DecodedClientMessageHandler messageHandler;
+
+    private final ProtocolSupports protocolSupports;
+
+
+    public UdpServerDeviceGatewayProvider(NetworkManager networkManager,
+                                          DeviceRegistry registry,
+                                          DeviceSessionManager sessionManager,
+                                          DecodedClientMessageHandler messageHandler,
+                                          ProtocolSupports protocolSupports) {
+        this.networkManager = networkManager;
+        this.registry = registry;
+        this.sessionManager = sessionManager;
+        this.messageHandler = messageHandler;
+        this.protocolSupports = protocolSupports;
+    }
+
+    @Override
+    public String getId() {
+        return "udp-server-gateway";
+    }
+
+    @Override
+    public String getName() {
+        return "UDP 透传接入";
+    }
+
+    @Override
+    public NetworkType getNetworkType() {
+        return DefaultNetworkType.UDP;
+    }
+
+    @Override
+    public Mono<DeviceGateway> createDeviceGateway(DeviceGatewayProperties properties) {
+        return networkManager
+            .<UdpServer>getNetwork(getNetworkType(), properties.getNetworkId())
+            .map(server -> {
+                String protocol = (String) properties.getConfiguration().get("protocol");
+
+                Assert.hasText(protocol, "protocol can not be empty");
+
+                return new UdpServerDeviceGateway(properties.getId(),
+                    protocol,
+                    protocolSupports,
+                    registry,
+                    messageHandler,
+                    sessionManager,
+                    server
+                );
+            });
+    }
+}

+ 74 - 0
jetlinks-components/network-component/udp-component/src/main/java/org/jetlinks/community/network/udp/server/session/UdpDeviceSession.java

@@ -0,0 +1,74 @@
+package org.jetlinks.community.network.udp.server.session;
+
+import org.jetlinks.core.device.DeviceOperator;
+import org.jetlinks.core.message.codec.EncodedMessage;
+import org.jetlinks.core.message.codec.Transport;
+import org.jetlinks.core.server.session.DeviceSession;
+import reactor.core.publisher.Mono;
+
+import javax.annotation.Nullable;
+
+/**
+ * @author lifang
+ * @version 1.0.0
+ * @ClassName UdpDeviceSession.java
+ * @Description TODO
+ * @createTime 2022年01月14日 17:45:00
+ */
+public class UdpDeviceSession  implements DeviceSession {
+    @Override
+    public String getId() {
+        return null;
+    }
+
+    @Override
+    public String getDeviceId() {
+        return null;
+    }
+
+    @Nullable
+    @Override
+    public DeviceOperator getOperator() {
+        return null;
+    }
+
+    @Override
+    public long lastPingTime() {
+        return 0;
+    }
+
+    @Override
+    public long connectTime() {
+        return 0;
+    }
+
+    @Override
+    public Mono<Boolean> send(EncodedMessage encodedMessage) {
+        return null;
+    }
+
+    @Override
+    public Transport getTransport() {
+        return null;
+    }
+
+    @Override
+    public void close() {
+
+    }
+
+    @Override
+    public void ping() {
+
+    }
+
+    @Override
+    public boolean isAlive() {
+        return false;
+    }
+
+    @Override
+    public void onClose(Runnable call) {
+
+    }
+}

+ 188 - 0
jetlinks-components/network-component/udp-component/udp-component.iml

@@ -0,0 +1,188 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<module org.jetbrains.idea.maven.project.MavenProjectsManager.isMavenModule="true" type="JAVA_MODULE" version="4">
+  <component name="FacetManager">
+    <facet type="Spring" name="Spring">
+      <configuration />
+    </facet>
+  </component>
+  <component name="NewModuleRootManager" LANGUAGE_LEVEL="JDK_1_8">
+    <output url="file://$MODULE_DIR$/target/classes" />
+    <output-test url="file://$MODULE_DIR$/target/test-classes" />
+    <content url="file://$MODULE_DIR$">
+      <sourceFolder url="file://$MODULE_DIR$/src/main/java" isTestSource="false" />
+      <sourceFolder url="file://$MODULE_DIR$/src/main/resources" type="java-resource" />
+      <sourceFolder url="file://$MODULE_DIR$/src/test/java" isTestSource="true" />
+      <excludeFolder url="file://$MODULE_DIR$/target" />
+    </content>
+    <orderEntry type="inheritedJdk" />
+    <orderEntry type="sourceFolder" forTests="false" />
+    <orderEntry type="module" module-name="network-core" />
+    <orderEntry type="library" name="Maven: org.jetlinks:rule-engine-support:1.1.7-SNAPSHOT" level="project" />
+    <orderEntry type="library" name="Maven: org.jetlinks:rule-engine-api:1.1.7-SNAPSHOT" level="project" />
+    <orderEntry type="library" name="Maven: org.jetlinks:jetlinks-supports:1.1.7-SNAPSHOT" level="project" />
+    <orderEntry type="library" name="Maven: com.google.guava:guava:30.1.1-jre" level="project" />
+    <orderEntry type="library" name="Maven: com.google.guava:failureaccess:1.0.1" level="project" />
+    <orderEntry type="library" name="Maven: com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava" level="project" />
+    <orderEntry type="library" name="Maven: org.checkerframework:checker-qual:3.8.0" level="project" />
+    <orderEntry type="library" name="Maven: com.google.errorprone:error_prone_annotations:2.5.1" level="project" />
+    <orderEntry type="library" name="Maven: com.google.j2objc:j2objc-annotations:1.3" level="project" />
+    <orderEntry type="library" name="Maven: org.hswebframework.web:hsweb-core:4.0.11" level="project" />
+    <orderEntry type="library" name="Maven: org.javassist:javassist:3.22.0-GA" level="project" />
+    <orderEntry type="library" name="Maven: org.springframework:spring-context:5.2.15.RELEASE" level="project" />
+    <orderEntry type="library" name="Maven: org.springframework:spring-aop:5.2.15.RELEASE" level="project" />
+    <orderEntry type="library" name="Maven: org.springframework:spring-expression:5.2.15.RELEASE" level="project" />
+    <orderEntry type="library" name="Maven: javax.validation:validation-api:2.0.1.Final" level="project" />
+    <orderEntry type="library" name="Maven: org.springframework:spring-aspects:5.2.15.RELEASE" level="project" />
+    <orderEntry type="library" name="Maven: org.aspectj:aspectjweaver:1.9.6" level="project" />
+    <orderEntry type="library" name="Maven: io.swagger.core.v3:swagger-annotations:2.1.4" level="project" />
+    <orderEntry type="library" name="Maven: org.glassfish:javax.el:3.0.0" level="project" />
+    <orderEntry type="library" name="Maven: org.hibernate.validator:hibernate-validator:6.1.7.Final" level="project" />
+    <orderEntry type="library" name="Maven: jakarta.validation:jakarta.validation-api:2.0.2" level="project" />
+    <orderEntry type="library" name="Maven: org.jboss.logging:jboss-logging:3.4.1.Final" level="project" />
+    <orderEntry type="library" name="Maven: com.fasterxml:classmate:1.5.1" level="project" />
+    <orderEntry type="library" name="Maven: com.digitalpetri.modbus:modbus-master-tcp:1.1.0" level="project" />
+    <orderEntry type="library" name="Maven: com.digitalpetri.modbus:modbus-codec:1.1.0" level="project" />
+    <orderEntry type="library" name="Maven: com.digitalpetri.modbus:modbus-core:1.1.0" level="project" />
+    <orderEntry type="library" name="Maven: io.dropwizard.metrics:metrics-core:4.1.21" level="project" />
+    <orderEntry type="library" name="Maven: com.fazecast:jSerialComm:2.6.2" level="project" />
+    <orderEntry type="library" name="Maven: javax.annotation:javax.annotation-api:1.3.2" level="project" />
+    <orderEntry type="library" name="Maven: com.google.code.findbugs:findbugs:3.0.1" level="project" />
+    <orderEntry type="library" name="Maven: net.jcip:jcip-annotations:1.0" level="project" />
+    <orderEntry type="library" name="Maven: com.google.code.findbugs:bcel-findbugs:6.0" level="project" />
+    <orderEntry type="library" name="Maven: com.google.code.findbugs:jFormatString:2.0.1" level="project" />
+    <orderEntry type="library" name="Maven: dom4j:dom4j:1.6.1" level="project" />
+    <orderEntry type="library" name="Maven: xml-apis:xml-apis:1.0.b2" level="project" />
+    <orderEntry type="library" name="Maven: org.ow2.asm:asm-debug-all:5.0.2" level="project" />
+    <orderEntry type="library" name="Maven: org.ow2.asm:asm-commons:5.0.2" level="project" />
+    <orderEntry type="library" name="Maven: org.ow2.asm:asm-tree:5.0.2" level="project" />
+    <orderEntry type="library" name="Maven: commons-lang:commons-lang:2.6" level="project" />
+    <orderEntry type="library" name="Maven: com.apple:AppleJavaExtensions:1.4" level="project" />
+    <orderEntry type="library" name="Maven: jaxen:jaxen:1.2.0" level="project" />
+    <orderEntry type="library" name="Maven: org.hswebframework:hsweb-easy-orm-rdb:4.0.13-SNAPSHOT" level="project" />
+    <orderEntry type="library" name="Maven: org.hswebframework:hsweb-easy-orm-core:4.0.13-SNAPSHOT" level="project" />
+    <orderEntry type="library" name="Maven: org.apache.commons:commons-collections4:4.4" level="project" />
+    <orderEntry type="library" name="Maven: commons-beanutils:commons-beanutils:1.9.4" level="project" />
+    <orderEntry type="library" name="Maven: commons-logging:commons-logging:1.2" level="project" />
+    <orderEntry type="library" name="Maven: commons-collections:commons-collections:3.2.2" level="project" />
+    <orderEntry type="library" name="Maven: com.alibaba:fastjson:1.2.75" level="project" />
+    <orderEntry type="library" name="Maven: org.hswebframework.web:hsweb-starter:4.0.12" level="project" />
+    <orderEntry type="library" name="Maven: org.hswebframework:hsweb-expands-script:3.0.2" level="project" />
+    <orderEntry type="library" name="Maven: org.hswebframework:hsweb-expands-security:3.0.2" level="project" />
+    <orderEntry type="library" name="Maven: org.springframework.boot:spring-boot-autoconfigure:2.3.11.RELEASE" level="project" />
+    <orderEntry type="library" name="Maven: org.hswebframework.web:hsweb-commons-crud:4.0.12" level="project" />
+    <orderEntry type="library" name="Maven: org.hswebframework.web:hsweb-concurrent-cache:4.0.12" level="project" />
+    <orderEntry type="library" name="Maven: org.springframework:spring-tx:5.2.15.RELEASE" level="project" />
+    <orderEntry type="library" name="Maven: org.hibernate.javax.persistence:hibernate-jpa-2.1-api:1.0.2.Final" level="project" />
+    <orderEntry type="library" name="Maven: org.hswebframework.web:hsweb-datasource-api:4.0.12" level="project" />
+    <orderEntry type="library" name="Maven: org.hswebframework.web:hsweb-commons-api:4.0.12" level="project" />
+    <orderEntry type="library" name="Maven: com.google.code.findbugs:jsr305:3.0.2" level="project" />
+    <orderEntry type="library" name="Maven: org.bouncycastle:bcprov-jdk15on:1.64" level="project" />
+    <orderEntry type="library" name="Maven: org.eclipse.californium:californium-core:2.2.3" level="project" />
+    <orderEntry type="library" name="Maven: org.eclipse.californium:element-connector:2.2.3" level="project" />
+    <orderEntry type="module" module-name="common-component" />
+    <orderEntry type="library" name="Maven: org.hswebframework.web:hsweb-authorization-api:4.0.12" level="project" />
+    <orderEntry type="library" name="Maven: io.micrometer:micrometer-core:1.5.14" level="project" />
+    <orderEntry type="library" name="Maven: org.hdrhistogram:HdrHistogram:2.1.12" level="project" />
+    <orderEntry type="library" scope="RUNTIME" name="Maven: org.latencyutils:LatencyUtils:2.0.3" level="project" />
+    <orderEntry type="library" name="Maven: org.jetlinks:reactor-ql:1.0.13" level="project" />
+    <orderEntry type="library" name="Maven: com.github.jsqlparser:jsqlparser:4.1" level="project" />
+    <orderEntry type="library" name="Maven: cn.hutool:hutool-json:5.7.16" level="project" />
+    <orderEntry type="library" name="Maven: cn.hutool:hutool-core:5.7.16" level="project" />
+    <orderEntry type="library" name="Maven: io.vertx:vertx-core:3.8.5" level="project" />
+    <orderEntry type="library" name="Maven: io.netty:netty-common:4.1.51.Final" level="project" />
+    <orderEntry type="library" name="Maven: io.netty:netty-buffer:4.1.51.Final" level="project" />
+    <orderEntry type="library" name="Maven: io.netty:netty-transport:4.1.51.Final" level="project" />
+    <orderEntry type="library" name="Maven: io.netty:netty-handler:4.1.51.Final" level="project" />
+    <orderEntry type="library" name="Maven: io.netty:netty-codec:4.1.51.Final" level="project" />
+    <orderEntry type="library" name="Maven: io.netty:netty-handler-proxy:4.1.51.Final" level="project" />
+    <orderEntry type="library" name="Maven: io.netty:netty-codec-socks:4.1.51.Final" level="project" />
+    <orderEntry type="library" name="Maven: io.netty:netty-codec-http:4.1.51.Final" level="project" />
+    <orderEntry type="library" name="Maven: io.netty:netty-codec-http2:4.1.51.Final" level="project" />
+    <orderEntry type="library" name="Maven: io.netty:netty-resolver:4.1.51.Final" level="project" />
+    <orderEntry type="library" name="Maven: io.netty:netty-resolver-dns:4.1.51.Final" level="project" />
+    <orderEntry type="library" name="Maven: io.netty:netty-codec-dns:4.1.51.Final" level="project" />
+    <orderEntry type="library" name="Maven: com.fasterxml.jackson.core:jackson-core:2.10.4" level="project" />
+    <orderEntry type="library" name="Maven: com.fasterxml.jackson.core:jackson-databind:2.10.4" level="project" />
+    <orderEntry type="library" name="Maven: com.fasterxml.jackson.core:jackson-annotations:2.10.4" level="project" />
+    <orderEntry type="module" module-name="gateway-component" />
+    <orderEntry type="library" name="Maven: org.jetlinks:jetlinks-core:1.1.7-SNAPSHOT" level="project" />
+    <orderEntry type="library" name="Maven: io.vavr:vavr:0.9.2" level="project" />
+    <orderEntry type="library" name="Maven: io.vavr:vavr-match:0.9.2" level="project" />
+    <orderEntry type="library" name="Maven: commons-codec:commons-codec:1.14" level="project" />
+    <orderEntry type="module" module-name="dashboard-component" />
+    <orderEntry type="module" module-name="timeseries-component" />
+    <orderEntry type="library" name="Maven: org.springframework.boot:spring-boot-actuator-autoconfigure:2.3.11.RELEASE" level="project" />
+    <orderEntry type="library" name="Maven: org.springframework.boot:spring-boot-actuator:2.3.11.RELEASE" level="project" />
+    <orderEntry type="library" name="Maven: com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.10.4" level="project" />
+    <orderEntry type="library" name="Maven: org.springframework.boot:spring-boot-starter-webflux:2.3.11.RELEASE" level="project" />
+    <orderEntry type="library" name="Maven: org.springframework.boot:spring-boot-starter-json:2.3.11.RELEASE" level="project" />
+    <orderEntry type="library" name="Maven: com.fasterxml.jackson.datatype:jackson-datatype-jdk8:2.10.4" level="project" />
+    <orderEntry type="library" name="Maven: com.fasterxml.jackson.module:jackson-module-parameter-names:2.10.4" level="project" />
+    <orderEntry type="library" name="Maven: org.springframework.boot:spring-boot-starter-reactor-netty:2.3.11.RELEASE" level="project" />
+    <orderEntry type="library" name="Maven: org.springframework:spring-web:5.2.15.RELEASE" level="project" />
+    <orderEntry type="library" name="Maven: org.springframework:spring-beans:5.2.15.RELEASE" level="project" />
+    <orderEntry type="library" name="Maven: org.springframework:spring-webflux:5.2.15.RELEASE" level="project" />
+    <orderEntry type="library" name="Maven: org.synchronoss.cloud:nio-multipart-parser:1.1.0" level="project" />
+    <orderEntry type="library" name="Maven: org.synchronoss.cloud:nio-stream-storage:1.1.3" level="project" />
+    <orderEntry type="library" scope="TEST" name="Maven: org.springframework.boot:spring-boot-starter-test:2.3.11.RELEASE" level="project" />
+    <orderEntry type="library" name="Maven: org.springframework.boot:spring-boot-starter:2.3.11.RELEASE" level="project" />
+    <orderEntry type="library" name="Maven: org.springframework.boot:spring-boot:2.3.11.RELEASE" level="project" />
+    <orderEntry type="library" name="Maven: org.springframework.boot:spring-boot-starter-logging:2.3.11.RELEASE" level="project" />
+    <orderEntry type="library" name="Maven: org.apache.logging.log4j:log4j-to-slf4j:2.13.3" level="project" />
+    <orderEntry type="library" name="Maven: org.apache.logging.log4j:log4j-api:2.13.3" level="project" />
+    <orderEntry type="library" name="Maven: org.slf4j:jul-to-slf4j:1.7.30" level="project" />
+    <orderEntry type="library" name="Maven: jakarta.annotation:jakarta.annotation-api:1.3.5" level="project" />
+    <orderEntry type="library" name="Maven: org.yaml:snakeyaml:1.26" level="project" />
+    <orderEntry type="library" scope="TEST" name="Maven: org.springframework.boot:spring-boot-test:2.3.11.RELEASE" level="project" />
+    <orderEntry type="library" scope="TEST" name="Maven: org.springframework.boot:spring-boot-test-autoconfigure:2.3.11.RELEASE" level="project" />
+    <orderEntry type="library" scope="TEST" name="Maven: com.jayway.jsonpath:json-path:2.4.0" level="project" />
+    <orderEntry type="library" scope="TEST" name="Maven: net.minidev:json-smart:2.3.1" level="project" />
+    <orderEntry type="library" scope="TEST" name="Maven: net.minidev:accessors-smart:2.3.1" level="project" />
+    <orderEntry type="library" name="Maven: org.ow2.asm:asm:5.0.4" level="project" />
+    <orderEntry type="library" scope="TEST" name="Maven: jakarta.xml.bind:jakarta.xml.bind-api:2.3.3" level="project" />
+    <orderEntry type="library" scope="TEST" name="Maven: jakarta.activation:jakarta.activation-api:1.2.2" level="project" />
+    <orderEntry type="library" scope="TEST" name="Maven: org.assertj:assertj-core:3.16.1" level="project" />
+    <orderEntry type="library" scope="TEST" name="Maven: org.hamcrest:hamcrest:2.2" level="project" />
+    <orderEntry type="library" scope="TEST" name="Maven: org.junit.jupiter:junit-jupiter:5.6.3" level="project" />
+    <orderEntry type="library" scope="TEST" name="Maven: org.junit.jupiter:junit-jupiter-api:5.6.3" level="project" />
+    <orderEntry type="library" scope="TEST" name="Maven: org.opentest4j:opentest4j:1.2.0" level="project" />
+    <orderEntry type="library" scope="TEST" name="Maven: org.junit.platform:junit-platform-commons:1.6.3" level="project" />
+    <orderEntry type="library" scope="TEST" name="Maven: org.junit.jupiter:junit-jupiter-params:5.6.3" level="project" />
+    <orderEntry type="library" scope="TEST" name="Maven: org.junit.jupiter:junit-jupiter-engine:5.6.3" level="project" />
+    <orderEntry type="library" scope="TEST" name="Maven: org.junit.vintage:junit-vintage-engine:5.6.3" level="project" />
+    <orderEntry type="library" scope="TEST" name="Maven: org.apiguardian:apiguardian-api:1.1.0" level="project" />
+    <orderEntry type="library" scope="TEST" name="Maven: org.junit.platform:junit-platform-engine:1.6.3" level="project" />
+    <orderEntry type="library" scope="TEST" name="Maven: org.mockito:mockito-core:3.3.3" level="project" />
+    <orderEntry type="library" scope="TEST" name="Maven: net.bytebuddy:byte-buddy:1.10.22" level="project" />
+    <orderEntry type="library" scope="TEST" name="Maven: net.bytebuddy:byte-buddy-agent:1.10.22" level="project" />
+    <orderEntry type="library" scope="TEST" name="Maven: org.objenesis:objenesis:2.6" level="project" />
+    <orderEntry type="library" scope="TEST" name="Maven: org.mockito:mockito-junit-jupiter:3.3.3" level="project" />
+    <orderEntry type="library" scope="TEST" name="Maven: org.skyscreamer:jsonassert:1.5.0" level="project" />
+    <orderEntry type="library" scope="TEST" name="Maven: com.vaadin.external.google:android-json:0.0.20131108.vaadin1" level="project" />
+    <orderEntry type="library" name="Maven: org.springframework:spring-core:5.2.15.RELEASE" level="project" />
+    <orderEntry type="library" name="Maven: org.springframework:spring-jcl:5.2.15.RELEASE" level="project" />
+    <orderEntry type="library" scope="TEST" name="Maven: org.springframework:spring-test:5.2.15.RELEASE" level="project" />
+    <orderEntry type="library" scope="TEST" name="Maven: org.xmlunit:xmlunit-core:2.7.0" level="project" />
+    <orderEntry type="library" name="Maven: dev.miku:r2dbc-mysql:0.8.2.RELEASE" level="project" />
+    <orderEntry type="library" name="Maven: io.projectreactor.addons:reactor-extra:3.3.6.RELEASE" level="project" />
+    <orderEntry type="library" name="Maven: io.projectreactor.netty:reactor-netty:0.9.20.RELEASE" level="project" />
+    <orderEntry type="library" name="Maven: io.netty:netty-transport-native-epoll:linux-x86_64:4.1.51.Final" level="project" />
+    <orderEntry type="library" name="Maven: io.netty:netty-transport-native-unix-common:4.1.51.Final" level="project" />
+    <orderEntry type="library" name="Maven: io.r2dbc:r2dbc-spi:0.8.5.RELEASE" level="project" />
+    <orderEntry type="library" name="Maven: io.projectreactor:reactor-tools:3.3.17.RELEASE" level="project" />
+    <orderEntry type="library" scope="TEST" name="Maven: io.projectreactor:reactor-test:3.3.17.RELEASE" level="project" />
+    <orderEntry type="library" name="Maven: org.springframework:spring-context-indexer:5.2.15.RELEASE" level="project" />
+    <orderEntry type="library" name="Maven: io.projectreactor:reactor-core:3.3.17.RELEASE" level="project" />
+    <orderEntry type="library" name="Maven: org.reactivestreams:reactive-streams:1.0.3" level="project" />
+    <orderEntry type="library" name="Maven: org.codehaus.groovy:groovy-all:2.4.17" level="project" />
+    <orderEntry type="library" scope="TEST" name="Maven: junit:junit:4.12" level="project" />
+    <orderEntry type="library" scope="TEST" name="Maven: org.hamcrest:hamcrest-core:2.2" level="project" />
+    <orderEntry type="library" name="Maven: org.slf4j:slf4j-api:1.7.30" level="project" />
+    <orderEntry type="library" name="Maven: ch.qos.logback:logback-classic:1.2.3" level="project" />
+    <orderEntry type="library" name="Maven: ch.qos.logback:logback-core:1.2.3" level="project" />
+    <orderEntry type="library" scope="PROVIDED" name="Maven: org.projectlombok:lombok:1.18.20" level="project" />
+    <orderEntry type="library" name="Maven: org.springframework.boot:spring-boot-configuration-processor:2.3.11.RELEASE" level="project" />
+    <orderEntry type="library" name="Maven: org.hswebframework:hsweb-utils:3.0.2" level="project" />
+    <orderEntry type="library" name="Maven: joda-time:joda-time:2.7" level="project" />
+  </component>
+</module>

+ 1 - 1
jetlinks-components/rule-engine-component/src/main/java/org/jetlinks/community/rule/engine/executor/DeviceMessageSendTaskExecutorProvider.java

@@ -115,7 +115,7 @@ public class DeviceMessageSendTaskExecutorProvider implements TaskExecutorProvid
         public Publisher<DeviceMessageReply> doSend(Map<String, Object> ctx, DeviceOperator device) {
             Map<String, Object> message = new HashMap<>(this.message);
             message.put("messageId", IDGenerator.SNOW_FLAKE_STRING.generate());
-            message.put("deviceId", device.getDeviceId());
+            message.put("id", device.getDeviceId());
             return Mono
                 .justOrEmpty(MessageType.convertMessage(message))
                 .cast(RepayableDeviceMessage.class)

+ 0 - 5
jetlinks-manager/bridge-manager/src/main/java/org/jetlinks/community/bridge/web/AliBridgeServerController.java

@@ -78,11 +78,6 @@ public class AliBridgeServerController implements
                 param.setIotInstanceId(param.getIotInstanceId());
                 return iotService.deleteProduct(param);
             }).then();
-//        return  Mono.zip(
-//            bridgeService.deleteById(id),
-//            bridgeDeviceService.createDelete().where(AliIotBridgeDeviceConfig::getBridgeId,id).execute(),
-//            bridgeGateway.delBridgeServer(id,true))
-//            .then();
     }
 
 

+ 1 - 1
jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/entity/DeviceTagEntity.java

@@ -35,7 +35,7 @@ import java.util.Objects;
 public class DeviceTagEntity extends GenericEntity<String> {
 
     @Column(length = 64, nullable = false, updatable = false)
-    @NotBlank(message = "[deviceId]不能为空", groups = CreateGroup.class)
+    @NotBlank(message = "[id]不能为空", groups = CreateGroup.class)
     @Schema(description = "设备ID")
     private String deviceId;
 

+ 3 - 3
jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/measurements/DeviceEventMeasurement.java

@@ -40,14 +40,14 @@ class DeviceEventMeasurement extends StaticMeasurement {
     }
 
     static ConfigMetadata configMetadata = new DefaultConfigMetadata()
-        .add("deviceId", "设备", "指定设备", new StringType().expand("selector", "device-selector"))
+        .add("id", "设备", "指定设备", new StringType().expand("selector", "device-selector"))
         .add("history", "历史数据量", "查询出历史数据后开始推送实时数据", new IntType().min(0).expand("defaultValue", 10));
 
 
     Flux<SimpleMeasurementValue> fromHistory(String deviceId, int history) {
         return history <= 0 ? Flux.empty() : QueryParamEntity.newQuery()
             .doPaging(0, history)
-            .where("deviceId", deviceId)
+            .where("id", deviceId)
             .execute(q->deviceDataService.queryEvent(deviceId,eventMetadata.getId(),q,false))
             .map(data -> SimpleMeasurementValue.of(data, data.getTimestamp()))
             .sort(MeasurementValue.sort());
@@ -91,7 +91,7 @@ class DeviceEventMeasurement extends StaticMeasurement {
 
         @Override
         public Flux<MeasurementValue> getValue(MeasurementParameter parameter) {
-            return Mono.justOrEmpty(parameter.getString("deviceId"))
+            return Mono.justOrEmpty(parameter.getString("id"))
                 .flatMapMany(deviceId -> {
                     int history = parameter.getInt("history").orElse(0);
                     //合并历史数据和实时数据

+ 2 - 2
jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/measurements/DeviceEventsMeasurement.java

@@ -74,7 +74,7 @@ class DeviceEventsMeasurement extends StaticMeasurement {
     }
 
     static ConfigMetadata configMetadata = new DefaultConfigMetadata()
-        .add("deviceId", "设备", "指定设备", new StringType().expand("selector", "device-selector"));
+        .add("id", "设备", "指定设备", new StringType().expand("selector", "device-selector"));
 
     /**
      * 实时设备事件
@@ -116,7 +116,7 @@ class DeviceEventsMeasurement extends StaticMeasurement {
         @Override
         public Flux<MeasurementValue> getValue(MeasurementParameter parameter) {
             return Mono
-                .justOrEmpty(parameter.getString("deviceId"))
+                .justOrEmpty(parameter.getString("id"))
                 .flatMapMany(deviceId -> {
                     int history = parameter.getInt("history").orElse(0);
                     return //合并历史数据和实时数据

+ 3 - 3
jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/measurements/DevicePropertiesMeasurement.java

@@ -109,7 +109,7 @@ class DevicePropertiesMeasurement extends StaticMeasurement {
     }
 
     static ConfigMetadata configMetadata = new DefaultConfigMetadata()
-        .add("deviceId", "设备", "指定设备", new StringType().expand("selector", "device-selector"));
+        .add("id", "设备", "指定设备", new StringType().expand("selector", "device-selector"));
 
     static Set<String> getPropertiesFromParameter(MeasurementParameter parameter) {
         return parameter
@@ -152,7 +152,7 @@ class DevicePropertiesMeasurement extends StaticMeasurement {
         @Override
         public Flux<MeasurementValue> getValue(MeasurementParameter parameter) {
             return Mono
-                .justOrEmpty(parameter.getString("deviceId"))
+                .justOrEmpty(parameter.getString("id"))
                 .flatMapMany(deviceId -> {
                     int history = parameter.getInt("history").orElse(1);
 
@@ -192,7 +192,7 @@ class DevicePropertiesMeasurement extends StaticMeasurement {
         @Override
         public Flux<MeasurementValue> getValue(MeasurementParameter parameter) {
             return Mono
-                .justOrEmpty(parameter.getString("deviceId"))
+                .justOrEmpty(parameter.getString("id"))
                 .flatMapMany(deviceId -> {
                     int history = parameter.getInt("history").orElse(0);
                     //合并历史数据和实时数据

+ 5 - 5
jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/measurements/DevicePropertyMeasurement.java

@@ -92,14 +92,14 @@ class DevicePropertyMeasurement extends StaticMeasurement {
     }
 
     static ConfigMetadata configMetadata = new DefaultConfigMetadata()
-        .add("deviceId", "设备", "指定设备", new StringType().expand("selector", "device-selector"))
+        .add("id", "设备", "指定设备", new StringType().expand("selector", "device-selector"))
         .add("history", "历史数据量", "查询出历史数据后开始推送实时数据", new IntType().min(0).expand("defaultValue", 10))
         .add("from", "时间从", "", StringType.GLOBAL)
         .add("to", "时间至", "", StringType.GLOBAL);
     ;
 
     static ConfigMetadata aggConfigMetadata = new DefaultConfigMetadata()
-        .add("deviceId", "设备ID", "", StringType.GLOBAL)
+        .add("id", "设备ID", "", StringType.GLOBAL)
         .add("time", "周期", "例如: 1h,10m,30s", StringType.GLOBAL)
         .add("agg", "聚合类型", "count,sum,avg,max,min", StringType.GLOBAL)
         .add("format", "时间格式", "如: MM-dd:HH", StringType.GLOBAL)
@@ -140,7 +140,7 @@ class DevicePropertyMeasurement extends StaticMeasurement {
         @Override
         public Flux<SimpleMeasurementValue> getValue(MeasurementParameter parameter) {
 
-            String deviceId = parameter.getString("deviceId", null);
+            String deviceId = parameter.getString("id", null);
             DeviceDataService.AggregationRequest request = new DeviceDataService.AggregationRequest();
             DeviceDataService.DevicePropertyAggregation aggregation = new DeviceDataService.DevicePropertyAggregation(
                 metadata.getId(), metadata.getId(), parameter.getString("agg").map(String::toUpperCase).map(Aggregation::valueOf).orElse(Aggregation.AVG)
@@ -205,7 +205,7 @@ class DevicePropertyMeasurement extends StaticMeasurement {
 
         @Override
         public Flux<MeasurementValue> getValue(MeasurementParameter parameter) {
-            return Mono.justOrEmpty(parameter.getString("deviceId"))
+            return Mono.justOrEmpty(parameter.getString("id"))
                 .flatMapMany(deviceId -> {
                     int history = parameter.getInt("history").orElse(1);
                     return  QueryParamEntity.newQuery()
@@ -253,7 +253,7 @@ class DevicePropertyMeasurement extends StaticMeasurement {
 
         @Override
         public Flux<MeasurementValue> getValue(MeasurementParameter parameter) {
-            return Mono.justOrEmpty(parameter.getString("deviceId"))
+            return Mono.justOrEmpty(parameter.getString("id"))
                 .flatMapMany(deviceId -> {
                     int history = parameter.getInt("history").orElse(0);
                     return  //合并历史数据和实时数据

+ 3 - 3
jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/measurements/status/DeviceStatusChangeMeasurement.java

@@ -39,7 +39,7 @@ class DeviceStatusChangeMeasurement extends StaticMeasurement {
     static MeasurementDefinition definition = MeasurementDefinition.of("change", "设备状态变更");
 
     static ConfigMetadata configMetadata = new DefaultConfigMetadata()
-        .add("deviceId", "设备", "指定设备", new StringType().expand("selector", "device-selector"));
+        .add("id", "设备", "指定设备", new StringType().expand("selector", "device-selector"));
 
     static DataType type = new EnumType()
         .addElement(EnumType.Element.of(MessageType.OFFLINE.name().toLowerCase(), "离线"))
@@ -148,7 +148,7 @@ class DeviceStatusChangeMeasurement extends StaticMeasurement {
         public Flux<MeasurementValue> getValue(MeasurementParameter parameter) {
 
 
-            return Mono.justOrEmpty(parameter.getString("deviceId"))
+            return Mono.justOrEmpty(parameter.getString("id"))
                 .flatMapMany(deviceId ->//从消息网关订阅消息
                     eventBus.subscribe(Subscription.of(
                         "RealTimeDeviceStateDimension"
@@ -165,7 +165,7 @@ class DeviceStatusChangeMeasurement extends StaticMeasurement {
         Map<String, Object> createStateValue(DeviceMessage message) {
             Map<String, Object> val = new HashMap<>();
             val.put("type", message.getMessageType().name().toLowerCase());
-            val.put("deviceId", message.getDeviceId());
+            val.put("id", message.getDeviceId());
             return val;
         }
     }

+ 4 - 4
jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/measurements/topic/DeviceTopicMeasurement.java

@@ -77,7 +77,7 @@ public class DeviceTopicMeasurement extends StaticMeasurement {
 
            return  Flux.interval(Duration.ofSeconds(2))
                 .map(t-> deviceRegistry.
-                    getDevice(String.valueOf( parameter.get("deviceId").orElse("null-")))
+                    getDevice(String.valueOf( parameter.get("id").orElse("null-")))
                     .map(DeviceOperator::getTopics))
                 .flatMap(Function.identity())
                 .flatMap(data -> Flux.just(SimpleMeasurementValue.of(
@@ -87,11 +87,11 @@ public class DeviceTopicMeasurement extends StaticMeasurement {
 
 //            return eventBus
 //                .subscribe(Subscription.of("real-time-device-topics",
-//                    String.format("/dashboard/device/%s/changed/topics",parameter.get("deviceId").orElse("null-")), Subscription.Feature.local))
+//                    String.format("/dashboard/device/%s/changed/topics",parameter.get("id").orElse("null-")), Subscription.Feature.local))
 //                .doOnNext(TopicPayload::release)
 //                .map(data->{
-//                    String deviceId=String.valueOf( parameter.get("deviceId").orElse("null-"));
-//                    return deviceRegistry.getDevice(deviceId)
+//                    String id=String.valueOf( parameter.get("id").orElse("null-"));
+//                    return deviceRegistry.getDevice(id)
 //                        .map(DeviceOperator::getTopics);
 //                })
 //                .flatMap(Function.identity())

+ 1 - 1
jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/message/DeviceCurrentStateSubscriptionProvider.java

@@ -38,7 +38,7 @@ public class DeviceCurrentStateSubscriptionProvider implements SubscriptionProvi
     @Override
     @SuppressWarnings("all")
     public Flux<Map<String, Object>> subscribe(SubscribeRequest request) {
-        List<String> deviceId = request.get("deviceId")
+        List<String> deviceId = request.get("id")
             .map(List.class::cast)
             .orElseThrow(() -> new IllegalArgumentException("deviceId不能为空"));
 

+ 3 - 3
jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/message/DeviceMessageSendSubscriptionProvider.java

@@ -51,8 +51,8 @@ public class DeviceMessageSendSubscriptionProvider implements SubscriptionProvid
 
         String topic = request.getTopic();
 
-        Map<String, String> variables = TopicUtils.getPathVariables("/device-message-sender/{productId}/{deviceId}", topic);
-        String deviceId = variables.get("deviceId");
+        Map<String, String> variables = TopicUtils.getPathVariables("/device-message-sender/{productId}/{id}", topic);
+        String deviceId = variables.get("id");
         String productId = variables.get("productId");
 
         //发给所有设备
@@ -72,7 +72,7 @@ public class DeviceMessageSendSubscriptionProvider implements SubscriptionProvid
 
     public Flux<Message> doSend(String requestId, String topic, String deviceId, Map<String, Object> message) {
         message.put("messageId", IDGenerator.SNOW_FLAKE_STRING.generate());
-        message.put("deviceId", deviceId);
+        message.put("id", deviceId);
 
         RepayableDeviceMessage<?> msg = MessageType.convertMessage(message)
             .filter(RepayableDeviceMessage.class::isInstance)

+ 10 - 10
jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/service/data/AbstractDeviceDataStoragePolicy.java

@@ -85,7 +85,7 @@ public abstract class AbstractDeviceDataStoragePolicy implements DeviceDataStora
     protected abstract Mono<Void> doSaveData(String metric, Flux<TimeSeriesData> data);
 
     /**
-     * 设备消息转换 二元组 {deviceId, tsData}
+     * 设备消息转换 二元组 {id, tsData}
      *
      * @param productId  产品ID
      * @param message    设备属性消息
@@ -160,7 +160,7 @@ public abstract class AbstractDeviceDataStoragePolicy implements DeviceDataStora
     }
 
     /**
-     * 设备消息转换成时序数据 二元组 {deviceId, tsData}
+     * 设备消息转换成时序数据 二元组 {id, tsData}
      *
      * @param message 设备消息
      * @return 二元组
@@ -208,7 +208,7 @@ public abstract class AbstractDeviceDataStoragePolicy implements DeviceDataStora
     }
 
     /**
-     * 事件消息转换成 二元组{deviceId, tsData}
+     * 事件消息转换成 二元组{id, tsData}
      *
      * @param productId 产品ID
      * @param message   事件消息
@@ -240,7 +240,7 @@ public abstract class AbstractDeviceDataStoragePolicy implements DeviceDataStora
                         data.put("value", tempValue);
                     }
                     data.put("id", createDataId(message));
-                    data.put("deviceId", device.getDeviceId());
+                    data.put("id", device.getDeviceId());
                     data.put("createTime", System.currentTimeMillis());
 
                     return TimeSeriesData.of(TimestampUtils.toMillis(message.getTimestamp()), data);
@@ -255,7 +255,7 @@ public abstract class AbstractDeviceDataStoragePolicy implements DeviceDataStora
             .flatMap(operator -> operator.getSelfConfig(DeviceConfigKey.productId))
             .flatMap(productId -> this
                 .doQueryPager(deviceLogMetricId(productId),
-                    entity.and("deviceId", TermType.eq, deviceId),
+                    entity.and("id", TermType.eq, deviceId),
                     data -> data.as(DeviceOperationLogEntity.class)
                 ))
             .defaultIfEmpty(PagerResult.empty());
@@ -273,7 +273,7 @@ public abstract class AbstractDeviceDataStoragePolicy implements DeviceDataStora
             .flatMap(device -> Mono.zip(device.getProduct(), device.getMetadata()))
             .flatMapMany(tp2 -> query
                 .toQuery()
-                .where("deviceId", deviceId)
+                .where("id", deviceId)
                 .execute(param -> this
                     .doQuery(deviceEventMetricId(tp2.getT1().getId(), event),
                         param,
@@ -298,7 +298,7 @@ public abstract class AbstractDeviceDataStoragePolicy implements DeviceDataStora
             .log()
             .flatMap(device -> Mono.zip(device.getProduct(), device.getMetadata()))
             .flatMap(tp2 -> query.toQuery()
-                .where("deviceId", deviceId)
+                .where("id", deviceId)
                 .execute(param -> this
                     .doQueryPager(deviceEventMetricId(tp2.getT1().getId(), event),
                         param,
@@ -387,7 +387,7 @@ public abstract class AbstractDeviceDataStoragePolicy implements DeviceDataStora
                             if (newData.isEmpty()) {
                                 return Mono.empty();
                             }
-                            newData.put("deviceId", message.getDeviceId());
+                            newData.put("id", message.getDeviceId());
                             newData.put("productId", productId);
                             newData.put("timestamp", TimestampUtils.toMillis(message.getTimestamp()));
                             newData.put("createTime", System.currentTimeMillis());
@@ -404,7 +404,7 @@ public abstract class AbstractDeviceDataStoragePolicy implements DeviceDataStora
     }
 
     /**
-     * 设备消息转换 二元组{deviceId, tsData}
+     * 设备消息转换 二元组{id, tsData}
      *
      * @param productId  产品ID
      * @param message    设备属性消息
@@ -465,7 +465,7 @@ public abstract class AbstractDeviceDataStoragePolicy implements DeviceDataStora
                                                         Object value) {
         Map<String, Object> propertyData = newMap(24);
         propertyData.put("id", DigestUtils.md5Hex(id));
-        propertyData.put("deviceId", deviceId);
+        propertyData.put("id", deviceId);
         propertyData.put("timestamp", timestamp);
         propertyData.put("property", property.getId());
         propertyData.put("createTime", System.currentTimeMillis());

+ 5 - 5
jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/service/data/TimeSeriesColumnDeviceDataStoragePolicy.java

@@ -88,7 +88,7 @@ public class TimeSeriesColumnDeviceDataStoragePolicy extends TimeSeriesDeviceDat
         return param
             .toQuery()
             .includes(property.keySet().toArray(new String[0]))
-            .where("deviceId", deviceId)
+            .where("id", deviceId)
             .execute(q -> timeSeriesManager.getService(getPropertyTimeSeriesMetric(productId)).query(q))
             .flatMap(data -> rowToProperty(data, property.values()));
     }
@@ -129,7 +129,7 @@ public class TimeSeriesColumnDeviceDataStoragePolicy extends TimeSeriesDeviceDat
                     return param
                         .toQuery()
                         .includes(property)
-                        .where("deviceId", deviceId)
+                        .where("id", deviceId)
                         .execute(query -> timeSeriesManager
                             .getService(devicePropertyMetric(tp2.getT1().getId()))
                             .queryPager(query,
@@ -162,7 +162,7 @@ public class TimeSeriesColumnDeviceDataStoragePolicy extends TimeSeriesDeviceDat
 
                     return query
                         .toQuery()
-                        .where("deviceId", deviceId)
+                        .where("id", deviceId)
                         .includes(property)
                         .execute(timeSeriesManager.getService(getPropertyTimeSeriesMetric(tp2.getT1().getId()))::query)
                         .flatMap(data -> Flux
@@ -239,7 +239,7 @@ public class TimeSeriesColumnDeviceDataStoragePolicy extends TimeSeriesDeviceDat
                                                                @Nonnull DeviceDataService.AggregationRequest request,
                                                                @Nonnull DeviceDataService.DevicePropertyAggregation... properties) {
 
-        request.filter.and("deviceId", "eq", deviceId);
+        request.filter.and("id", "eq", deviceId);
 
         return deviceRegistry
             .getDevice(deviceId)
@@ -249,7 +249,7 @@ public class TimeSeriesColumnDeviceDataStoragePolicy extends TimeSeriesDeviceDat
     }
 
     /**
-     * 设备消息转换 二元组{deviceId, tsData}
+     * 设备消息转换 二元组{id, tsData}
      *
      * @param productId  产品ID
      * @param message    设备属性消息

+ 7 - 7
jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/service/data/TimeSeriesRowDeviceDataStoreStoragePolicy.java

@@ -87,7 +87,7 @@ public class TimeSeriesRowDeviceDataStoreStoragePolicy extends TimeSeriesDeviceD
         if (property.size() == 1) {
             return param
                 .toQuery()
-                .where("deviceId", deviceId)
+                .where("id", deviceId)
                 .and("property", property.keySet().iterator().next())
                 .execute(timeSeriesManager.getService(devicePropertyMetric(productId))::query)
                 .map(data ->
@@ -105,7 +105,7 @@ public class TimeSeriesRowDeviceDataStoreStoragePolicy extends TimeSeriesDeviceD
                 .groupBy(new LimitGroup("property", "property", property.size() * 2)) //按property分组
                 .limit(property.size())
                 .filter(param)
-                .filter(query -> query.where("deviceId", deviceId))
+                .filter(query -> query.where("id", deviceId))
             ).map(data -> DeviceProperty
                 .of(data, data.getString("property").map(property::get).orElse(null))
                 .deviceId(deviceId));
@@ -141,7 +141,7 @@ public class TimeSeriesRowDeviceDataStoreStoragePolicy extends TimeSeriesDeviceD
             .flatMap(device -> Mono.zip(device.getProduct(), device.getMetadata()))
             .flatMap(tp2 -> param.toQuery()
                 .where("property", property)
-                .and("deviceId", deviceId)
+                .and("id", deviceId)
                 .execute(query -> timeSeriesManager
                     .getService(devicePropertyMetric(tp2.getT1().getId()))
                     .queryPager(query, data -> DeviceProperty.of(data, tp2.getT2().getPropertyOrNull(property)))));
@@ -164,7 +164,7 @@ public class TimeSeriesRowDeviceDataStoreStoragePolicy extends TimeSeriesDeviceD
                         .collect(Collectors.toMap(PropertyMetadata::getId, Function.identity(), (a, b) -> a));
 
                     return query.toQuery()
-                        .where("deviceId", deviceId)
+                        .where("id", deviceId)
                         .when(property.length > 0, q -> q.in("property", Arrays.asList(property)))
                         .execute(timeSeriesManager
                             .getService(DeviceTimeSeriesMetric.devicePropertyMetricId(tp2.getT1().getId()))::query)
@@ -194,7 +194,7 @@ public class TimeSeriesRowDeviceDataStoreStoragePolicy extends TimeSeriesDeviceD
                         .agg(new LimitAggregationColumn("property", "property", Aggregation.TOP, query.getPageSize()))
                         .groupBy(new LimitGroup("property", "property", propertiesMap.size() * 2)) //按property分组
                         .filter(query)
-                        .filter(q -> q.where("deviceId", deviceId).in("property", propertiesMap.keySet()))
+                        .filter(q -> q.where("id", deviceId).in("property", propertiesMap.keySet()))
                     ).map(data -> DeviceProperty
                         .of(data, data.getString("property").map(propertiesMap::get).orElse(null))
                         .deviceId(deviceId));
@@ -282,7 +282,7 @@ public class TimeSeriesRowDeviceDataStoreStoragePolicy extends TimeSeriesDeviceD
                                                                @Nonnull DeviceDataService.AggregationRequest request,
                                                                @Nonnull DeviceDataService.DevicePropertyAggregation... properties) {
 
-        request.filter.and("deviceId", "eq", deviceId);
+        request.filter.and("id", "eq", deviceId);
 
         return deviceRegistry
             .getDevice(deviceId)
@@ -292,7 +292,7 @@ public class TimeSeriesRowDeviceDataStoreStoragePolicy extends TimeSeriesDeviceD
     }
 
     /**
-     * 设备消息转换 二元组{deviceId, tsData}
+     * 设备消息转换 二元组{id, tsData}
      *
      * @param productId  产品ID
      * @param message    设备属性消息

+ 1 - 1
jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/timeseries/DeviceEventTimeSeriesMetadata.java

@@ -37,7 +37,7 @@ class DeviceEventTimeSeriesMetadata implements TimeSeriesMetadata {
         }
         {
             SimplePropertyMetadata property = new SimplePropertyMetadata();
-            property.setId("deviceId");
+            property.setId("id");
             property.setValueType(new StringType());
             property.setName("设备ID");
             defaultMetadata.add(property);

+ 1 - 1
jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/timeseries/DeviceLogTimeSeriesMetadata.java

@@ -47,7 +47,7 @@ class DeviceLogTimeSeriesMetadata implements TimeSeriesMetadata {
 
         {
             SimplePropertyMetadata property = new SimplePropertyMetadata();
-            property.setId("deviceId");
+            property.setId("id");
             property.setValueType(new StringType());
             property.setName("设备ID");
             metadata.add(property);

+ 1 - 1
jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/timeseries/DevicePropertiesTimeSeriesMetadata.java

@@ -79,7 +79,7 @@ class DevicePropertiesTimeSeriesMetadata implements TimeSeriesMetadata {
 
         {
             SimplePropertyMetadata property = new SimplePropertyMetadata();
-            property.setId("deviceId");
+            property.setId("id");
             property.setValueType(new StringType());
             property.setName("设备ID");
             metadata.add(property);

+ 1 - 1
jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/timeseries/FixedPropertiesTimeSeriesMetadata.java

@@ -36,7 +36,7 @@ class FixedPropertiesTimeSeriesMetadata implements TimeSeriesMetadata {
 
         {
             SimplePropertyMetadata property = new SimplePropertyMetadata();
-            property.setId("deviceId");
+            property.setId("id");
             property.setValueType(new StringType());
             property.setName("设备ID");
             metadata.add(property);

+ 4 - 4
jetlinks-manager/device-manager/src/main/java/org/jetlinks/community/device/web/DeviceInstanceController.java

@@ -393,7 +393,7 @@ public class DeviceInstanceController implements
     /**
      * 获取设备全部标签
      * <pre>
-     *     GET /device/instance/{deviceId}/tags
+     *     GET /device/instance/{id}/tags
      *
      *     [
      *      {
@@ -653,7 +653,7 @@ public class DeviceInstanceController implements
             .map(DeviceInstanceEntity::getShadow)
             .defaultIfEmpty(new DeviceShadowEntity());
 //        return registry
-//            .getDevice(deviceId)
+//            .getDevice(id)
 //            .flatMap(operator -> operator.getSelfConfig(DeviceConfigKey.shadow))
 //            .defaultIfEmpty("{\n}");
     }
@@ -721,7 +721,7 @@ public class DeviceInstanceController implements
                         DeviceMessage message = tp2.getT2();
 
                         Map<String, String> copy = new HashMap<>();
-                        copy.put("deviceId", deviceId);
+                        copy.put("id", deviceId);
                         if (!StringUtils.hasText(message.getMessageId())) {
                             copy.put("messageId", IDGenerator.SNOW_FLAKE_STRING.generate());
                         }
@@ -777,7 +777,7 @@ public class DeviceInstanceController implements
                 return devices
                     .flatMap(device -> {
                         Map<String, Object> copy = new HashMap<>(message);
-                        copy.put("deviceId", device.getDeviceId());
+                        copy.put("id", device.getDeviceId());
                         copy.putIfAbsent("messageId", IDGenerator.SNOW_FLAKE_STRING.generate());
                         //复制为新的消息,防止冲突
                         DeviceMessage copiedMessage = MessageType

+ 285 - 0
jetlinks-manager/media-manager/media-manager.iml

@@ -0,0 +1,285 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<module org.jetbrains.idea.maven.project.MavenProjectsManager.isMavenModule="true" type="JAVA_MODULE" version="4">
+  <component name="FacetManager">
+    <facet type="Spring" name="Spring">
+      <configuration />
+    </facet>
+  </component>
+  <component name="NewModuleRootManager" LANGUAGE_LEVEL="JDK_1_8">
+    <output url="file://$MODULE_DIR$/target/classes" />
+    <output-test url="file://$MODULE_DIR$/target/test-classes" />
+    <content url="file://$MODULE_DIR$">
+      <sourceFolder url="file://$MODULE_DIR$/src/main/java" isTestSource="false" />
+      <sourceFolder url="file://$MODULE_DIR$/src/main/resources" type="java-resource" />
+      <sourceFolder url="file://$MODULE_DIR$/src/test/java" isTestSource="true" />
+      <excludeFolder url="file://$MODULE_DIR$/target" />
+    </content>
+    <orderEntry type="inheritedJdk" />
+    <orderEntry type="sourceFolder" forTests="false" />
+    <orderEntry type="library" name="Maven: javax.sip:jain-sip-ri:1.3.0-91" level="project" />
+    <orderEntry type="library" name="Maven: com.squareup.okhttp3:okhttp:3.14.9" level="project" />
+    <orderEntry type="library" name="Maven: com.squareup.okio:okio:1.17.2" level="project" />
+    <orderEntry type="library" name="Maven: org.redisson:redisson-spring-boot-starter:3.13.6" level="project" />
+    <orderEntry type="library" name="Maven: org.springframework.boot:spring-boot-starter-actuator:2.3.11.RELEASE" level="project" />
+    <orderEntry type="library" name="Maven: org.springframework.boot:spring-boot-actuator-autoconfigure:2.3.11.RELEASE" level="project" />
+    <orderEntry type="library" name="Maven: org.springframework.boot:spring-boot-actuator:2.3.11.RELEASE" level="project" />
+    <orderEntry type="library" name="Maven: com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.10.4" level="project" />
+    <orderEntry type="library" name="Maven: io.micrometer:micrometer-core:1.5.14" level="project" />
+    <orderEntry type="library" name="Maven: org.hdrhistogram:HdrHistogram:2.1.12" level="project" />
+    <orderEntry type="library" scope="RUNTIME" name="Maven: org.latencyutils:LatencyUtils:2.0.3" level="project" />
+    <orderEntry type="library" name="Maven: org.springframework.boot:spring-boot-starter-data-redis:2.3.11.RELEASE" level="project" />
+    <orderEntry type="library" name="Maven: org.springframework.data:spring-data-redis:2.3.9.RELEASE" level="project" />
+    <orderEntry type="library" name="Maven: org.springframework.data:spring-data-keyvalue:2.3.9.RELEASE" level="project" />
+    <orderEntry type="library" name="Maven: org.springframework:spring-oxm:5.2.15.RELEASE" level="project" />
+    <orderEntry type="library" name="Maven: org.springframework:spring-aop:5.2.15.RELEASE" level="project" />
+    <orderEntry type="library" name="Maven: org.springframework:spring-context-support:5.2.15.RELEASE" level="project" />
+    <orderEntry type="library" name="Maven: org.redisson:redisson:3.13.6" level="project" />
+    <orderEntry type="library" name="Maven: io.netty:netty-common:4.1.51.Final" level="project" />
+    <orderEntry type="library" name="Maven: io.netty:netty-codec:4.1.51.Final" level="project" />
+    <orderEntry type="library" name="Maven: io.netty:netty-transport:4.1.51.Final" level="project" />
+    <orderEntry type="library" name="Maven: io.netty:netty-resolver-dns:4.1.51.Final" level="project" />
+    <orderEntry type="library" name="Maven: io.netty:netty-codec-dns:4.1.51.Final" level="project" />
+    <orderEntry type="library" name="Maven: io.netty:netty-handler:4.1.51.Final" level="project" />
+    <orderEntry type="library" name="Maven: javax.cache:cache-api:1.1.1" level="project" />
+    <orderEntry type="library" name="Maven: io.reactivex.rxjava2:rxjava:2.2.21" level="project" />
+    <orderEntry type="library" name="Maven: org.jboss.marshalling:jboss-marshalling-river:2.0.10.Final" level="project" />
+    <orderEntry type="library" name="Maven: org.jboss.marshalling:jboss-marshalling:2.0.10.Final" level="project" />
+    <orderEntry type="library" name="Maven: org.yaml:snakeyaml:1.26" level="project" />
+    <orderEntry type="library" name="Maven: com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:2.10.4" level="project" />
+    <orderEntry type="library" name="Maven: com.fasterxml.jackson.core:jackson-core:2.10.4" level="project" />
+    <orderEntry type="library" name="Maven: net.bytebuddy:byte-buddy:1.10.22" level="project" />
+    <orderEntry type="library" name="Maven: org.jodd:jodd-bean:5.1.6" level="project" />
+    <orderEntry type="library" name="Maven: org.jodd:jodd-core:5.1.6" level="project" />
+    <orderEntry type="library" name="Maven: org.redisson:redisson-spring-data-23:3.13.6" level="project" />
+    <orderEntry type="library" name="Maven: org.hswebframework.web:hsweb-authorization-api:4.0.12" level="project" />
+    <orderEntry type="library" name="Maven: org.hswebframework.web:hsweb-core:4.0.12" level="project" />
+    <orderEntry type="library" name="Maven: org.javassist:javassist:3.22.0-GA" level="project" />
+    <orderEntry type="library" name="Maven: org.springframework:spring-context:5.2.15.RELEASE" level="project" />
+    <orderEntry type="library" name="Maven: org.springframework:spring-expression:5.2.15.RELEASE" level="project" />
+    <orderEntry type="library" name="Maven: org.springframework:spring-web:5.2.15.RELEASE" level="project" />
+    <orderEntry type="library" name="Maven: javax.validation:validation-api:2.0.1.Final" level="project" />
+    <orderEntry type="library" name="Maven: org.springframework:spring-aspects:5.2.15.RELEASE" level="project" />
+    <orderEntry type="library" name="Maven: org.aspectj:aspectjweaver:1.9.6" level="project" />
+    <orderEntry type="library" name="Maven: org.glassfish:javax.el:3.0.0" level="project" />
+    <orderEntry type="library" name="Maven: org.hibernate.validator:hibernate-validator:6.1.7.Final" level="project" />
+    <orderEntry type="library" name="Maven: jakarta.validation:jakarta.validation-api:2.0.2" level="project" />
+    <orderEntry type="library" name="Maven: org.jboss.logging:jboss-logging:3.4.1.Final" level="project" />
+    <orderEntry type="library" name="Maven: com.fasterxml:classmate:1.5.1" level="project" />
+    <orderEntry type="library" name="Maven: com.alibaba:fastjson:1.2.75" level="project" />
+    <orderEntry type="library" name="Maven: io.swagger.core.v3:swagger-annotations:2.1.4" level="project" />
+    <orderEntry type="library" name="Maven: cn.hutool:hutool-all:5.7.16" level="project" />
+    <orderEntry type="module" module-name="elasticsearch-component" />
+    <orderEntry type="library" name="Maven: org.elasticsearch.plugin:transport-netty4-client:7.11.2" level="project" />
+    <orderEntry type="library" name="Maven: io.netty:netty-codec-http:4.1.51.Final" level="project" />
+    <orderEntry type="library" name="Maven: io.netty:netty-resolver:4.1.51.Final" level="project" />
+    <orderEntry type="library" name="Maven: org.apache.logging.log4j:log4j-core:2.13.3" level="project" />
+    <orderEntry type="library" name="Maven: org.apache.logging.log4j:log4j-api:2.13.3" level="project" />
+    <orderEntry type="library" name="Maven: org.slf4j:log4j-over-slf4j:1.7.30" level="project" />
+    <orderEntry type="library" name="Maven: org.hswebframework:hsweb-easy-orm-elasticsearch:4.0.13-SNAPSHOT" level="project" />
+    <orderEntry type="library" name="Maven: org.elasticsearch:elasticsearch:7.11.2" level="project" />
+    <orderEntry type="library" name="Maven: org.elasticsearch:elasticsearch-core:7.11.2" level="project" />
+    <orderEntry type="library" name="Maven: org.elasticsearch:elasticsearch-secure-sm:7.11.2" level="project" />
+    <orderEntry type="library" name="Maven: org.elasticsearch:elasticsearch-x-content:7.11.2" level="project" />
+    <orderEntry type="library" name="Maven: com.fasterxml.jackson.dataformat:jackson-dataformat-smile:2.10.4" level="project" />
+    <orderEntry type="library" name="Maven: com.fasterxml.jackson.dataformat:jackson-dataformat-cbor:2.10.4" level="project" />
+    <orderEntry type="library" name="Maven: org.elasticsearch:elasticsearch-geo:7.11.2" level="project" />
+    <orderEntry type="library" name="Maven: org.apache.lucene:lucene-core:8.7.0" level="project" />
+    <orderEntry type="library" name="Maven: org.apache.lucene:lucene-analyzers-common:8.7.0" level="project" />
+    <orderEntry type="library" name="Maven: org.apache.lucene:lucene-backward-codecs:8.7.0" level="project" />
+    <orderEntry type="library" name="Maven: org.apache.lucene:lucene-grouping:8.7.0" level="project" />
+    <orderEntry type="library" name="Maven: org.apache.lucene:lucene-highlighter:8.7.0" level="project" />
+    <orderEntry type="library" name="Maven: org.apache.lucene:lucene-join:8.7.0" level="project" />
+    <orderEntry type="library" name="Maven: org.apache.lucene:lucene-memory:8.7.0" level="project" />
+    <orderEntry type="library" name="Maven: org.apache.lucene:lucene-misc:8.7.0" level="project" />
+    <orderEntry type="library" name="Maven: org.apache.lucene:lucene-queries:8.7.0" level="project" />
+    <orderEntry type="library" name="Maven: org.apache.lucene:lucene-queryparser:8.7.0" level="project" />
+    <orderEntry type="library" name="Maven: org.apache.lucene:lucene-sandbox:8.7.0" level="project" />
+    <orderEntry type="library" name="Maven: org.apache.lucene:lucene-spatial-extras:8.7.0" level="project" />
+    <orderEntry type="library" name="Maven: org.apache.lucene:lucene-spatial3d:8.7.0" level="project" />
+    <orderEntry type="library" name="Maven: org.apache.lucene:lucene-suggest:8.7.0" level="project" />
+    <orderEntry type="library" name="Maven: org.elasticsearch:elasticsearch-cli:7.11.2" level="project" />
+    <orderEntry type="library" name="Maven: net.sf.jopt-simple:jopt-simple:5.0.2" level="project" />
+    <orderEntry type="library" name="Maven: com.carrotsearch:hppc:0.8.1" level="project" />
+    <orderEntry type="library" name="Maven: com.tdunning:t-digest:3.2" level="project" />
+    <orderEntry type="library" name="Maven: org.elasticsearch:jna:5.5.0" level="project" />
+    <orderEntry type="library" scope="RUNTIME" name="Maven: org.elasticsearch:elasticsearch-plugin-classloader:7.11.2" level="project" />
+    <orderEntry type="library" name="Maven: com.github.jsqlparser:jsqlparser:3.2" level="project" />
+    <orderEntry type="library" name="Maven: org.elasticsearch.client:elasticsearch-rest-high-level-client:7.11.2" level="project" />
+    <orderEntry type="library" name="Maven: org.elasticsearch.client:elasticsearch-rest-client:7.11.2" level="project" />
+    <orderEntry type="library" name="Maven: org.apache.httpcomponents:httpclient:4.5.13" level="project" />
+    <orderEntry type="library" name="Maven: org.apache.httpcomponents:httpcore:4.4.14" level="project" />
+    <orderEntry type="library" name="Maven: org.apache.httpcomponents:httpasyncclient:4.1.4" level="project" />
+    <orderEntry type="library" name="Maven: org.apache.httpcomponents:httpcore-nio:4.4.14" level="project" />
+    <orderEntry type="library" name="Maven: org.elasticsearch.plugin:mapper-extras-client:7.11.2" level="project" />
+    <orderEntry type="library" name="Maven: org.elasticsearch.plugin:parent-join-client:7.11.2" level="project" />
+    <orderEntry type="library" name="Maven: org.elasticsearch.plugin:aggs-matrix-stats-client:7.11.2" level="project" />
+    <orderEntry type="library" name="Maven: org.elasticsearch.plugin:rank-eval-client:7.11.2" level="project" />
+    <orderEntry type="library" name="Maven: org.elasticsearch.plugin:lang-mustache-client:7.11.2" level="project" />
+    <orderEntry type="library" name="Maven: com.github.spullara.mustache.java:compiler:0.9.6" level="project" />
+    <orderEntry type="library" name="Maven: org.springframework.data:spring-data-elasticsearch:4.0.9.RELEASE" level="project" />
+    <orderEntry type="library" name="Maven: org.springframework:spring-tx:5.2.15.RELEASE" level="project" />
+    <orderEntry type="library" name="Maven: org.springframework.data:spring-data-commons:2.3.9.RELEASE" level="project" />
+    <orderEntry type="library" name="Maven: org.elasticsearch.client:transport:7.6.2" level="project" />
+    <orderEntry type="library" name="Maven: org.elasticsearch.plugin:reindex-client:7.6.2" level="project" />
+    <orderEntry type="library" name="Maven: org.elasticsearch:elasticsearch-ssl-config:7.6.2" level="project" />
+    <orderEntry type="library" name="Maven: org.elasticsearch.plugin:percolator-client:7.6.2" level="project" />
+    <orderEntry type="library" name="Maven: org.hswebframework.web:hsweb-commons-crud:4.0.12" level="project" />
+    <orderEntry type="library" name="Maven: org.hswebframework.web:hsweb-concurrent-cache:4.0.12" level="project" />
+    <orderEntry type="library" name="Maven: org.hibernate.javax.persistence:hibernate-jpa-2.1-api:1.0.2.Final" level="project" />
+    <orderEntry type="library" name="Maven: org.hswebframework.web:hsweb-datasource-api:4.0.12" level="project" />
+    <orderEntry type="library" name="Maven: org.hswebframework.web:hsweb-commons-api:4.0.12" level="project" />
+    <orderEntry type="module" module-name="timeseries-component" />
+    <orderEntry type="module" module-name="common-component" />
+    <orderEntry type="library" name="Maven: org.jetlinks:reactor-ql:1.0.13" level="project" />
+    <orderEntry type="library" name="Maven: io.projectreactor.netty:reactor-netty:0.9.20.RELEASE" level="project" />
+    <orderEntry type="library" name="Maven: io.netty:netty-codec-http2:4.1.51.Final" level="project" />
+    <orderEntry type="library" name="Maven: io.netty:netty-handler-proxy:4.1.51.Final" level="project" />
+    <orderEntry type="library" name="Maven: io.netty:netty-codec-socks:4.1.51.Final" level="project" />
+    <orderEntry type="library" name="Maven: io.netty:netty-transport-native-epoll:linux-x86_64:4.1.51.Final" level="project" />
+    <orderEntry type="library" name="Maven: io.netty:netty-transport-native-unix-common:4.1.51.Final" level="project" />
+    <orderEntry type="library" name="Maven: org.springframework.boot:spring-boot-starter-webflux:2.3.11.RELEASE" level="project" />
+    <orderEntry type="library" name="Maven: org.springframework.boot:spring-boot-starter-json:2.3.11.RELEASE" level="project" />
+    <orderEntry type="library" name="Maven: com.fasterxml.jackson.datatype:jackson-datatype-jdk8:2.10.4" level="project" />
+    <orderEntry type="library" name="Maven: com.fasterxml.jackson.module:jackson-module-parameter-names:2.10.4" level="project" />
+    <orderEntry type="library" name="Maven: org.springframework.boot:spring-boot-starter-reactor-netty:2.3.11.RELEASE" level="project" />
+    <orderEntry type="library" name="Maven: org.synchronoss.cloud:nio-multipart-parser:1.1.0" level="project" />
+    <orderEntry type="library" name="Maven: org.synchronoss.cloud:nio-stream-storage:1.1.3" level="project" />
+    <orderEntry type="module" module-name="io-component" />
+    <orderEntry type="library" name="Maven: commons-io:commons-io:2.7" level="project" />
+    <orderEntry type="library" name="Maven: org.hswebframework:reactor-excel:1.0.1" level="project" />
+    <orderEntry type="library" name="Maven: org.apache.commons:commons-csv:1.8" level="project" />
+    <orderEntry type="library" name="Maven: org.apache.poi:poi:4.1.2" level="project" />
+    <orderEntry type="library" name="Maven: org.apache.commons:commons-math3:3.6.1" level="project" />
+    <orderEntry type="library" name="Maven: com.zaxxer:SparseBitSet:1.2" level="project" />
+    <orderEntry type="library" name="Maven: org.apache.poi:poi-scratchpad:4.1.2" level="project" />
+    <orderEntry type="library" name="Maven: org.apache.poi:poi-ooxml:4.1.2" level="project" />
+    <orderEntry type="library" name="Maven: org.apache.commons:commons-compress:1.19" level="project" />
+    <orderEntry type="library" name="Maven: com.github.virtuald:curvesapi:1.06" level="project" />
+    <orderEntry type="library" name="Maven: org.apache.poi:poi-excelant:4.1.2" level="project" />
+    <orderEntry type="library" name="Maven: org.apache.ant:ant:1.8.2" level="project" />
+    <orderEntry type="library" name="Maven: org.apache.ant:ant-launcher:1.8.2" level="project" />
+    <orderEntry type="library" name="Maven: org.apache.poi:poi-ooxml-schemas:4.1.2" level="project" />
+    <orderEntry type="library" name="Maven: org.apache.xmlbeans:xmlbeans:3.1.0" level="project" />
+    <orderEntry type="library" name="Maven: org.apache.poi:ooxml-schemas:1.4" level="project" />
+    <orderEntry type="library" name="Maven: com.alibaba:easyexcel:2.1.2" level="project" />
+    <orderEntry type="library" name="Maven: cglib:cglib:3.1" level="project" />
+    <orderEntry type="library" name="Maven: org.ow2.asm:asm:4.2" level="project" />
+    <orderEntry type="library" name="Maven: org.ehcache:ehcache:3.8.1" level="project" />
+    <orderEntry type="library" name="Maven: org.glassfish.jaxb:jaxb-runtime:2.3.4" level="project" />
+    <orderEntry type="library" name="Maven: org.glassfish.jaxb:txw2:2.3.4" level="project" />
+    <orderEntry type="library" name="Maven: com.sun.istack:istack-commons-runtime:3.0.12" level="project" />
+    <orderEntry type="library" scope="RUNTIME" name="Maven: com.sun.activation:jakarta.activation:1.2.2" level="project" />
+    <orderEntry type="library" name="Maven: org.springframework:spring-webflux:5.2.15.RELEASE" level="project" />
+    <orderEntry type="library" name="Maven: org.springframework:spring-beans:5.2.15.RELEASE" level="project" />
+    <orderEntry type="library" name="Maven: org.jetlinks:jetlinks-supports:1.1.7-SNAPSHOT" level="project" />
+    <orderEntry type="library" name="Maven: com.google.guava:guava:30.1.1-jre" level="project" />
+    <orderEntry type="library" name="Maven: com.google.guava:failureaccess:1.0.1" level="project" />
+    <orderEntry type="library" name="Maven: com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava" level="project" />
+    <orderEntry type="library" name="Maven: com.google.j2objc:j2objc-annotations:1.3" level="project" />
+    <orderEntry type="library" name="Maven: javax.annotation:javax.annotation-api:1.3.2" level="project" />
+    <orderEntry type="library" name="Maven: com.google.code.findbugs:findbugs:3.0.1" level="project" />
+    <orderEntry type="library" name="Maven: net.jcip:jcip-annotations:1.0" level="project" />
+    <orderEntry type="library" name="Maven: com.google.code.findbugs:bcel-findbugs:6.0" level="project" />
+    <orderEntry type="library" name="Maven: com.google.code.findbugs:jFormatString:2.0.1" level="project" />
+    <orderEntry type="library" name="Maven: dom4j:dom4j:1.6.1" level="project" />
+    <orderEntry type="library" name="Maven: xml-apis:xml-apis:1.0.b2" level="project" />
+    <orderEntry type="library" name="Maven: org.ow2.asm:asm-debug-all:5.0.2" level="project" />
+    <orderEntry type="library" name="Maven: org.ow2.asm:asm-commons:5.0.2" level="project" />
+    <orderEntry type="library" name="Maven: org.ow2.asm:asm-tree:5.0.2" level="project" />
+    <orderEntry type="library" name="Maven: commons-lang:commons-lang:2.6" level="project" />
+    <orderEntry type="library" name="Maven: com.apple:AppleJavaExtensions:1.4" level="project" />
+    <orderEntry type="library" name="Maven: jaxen:jaxen:1.2.0" level="project" />
+    <orderEntry type="library" name="Maven: org.jetlinks:jetlinks-core:1.1.7-SNAPSHOT" level="project" />
+    <orderEntry type="library" name="Maven: io.projectreactor.addons:reactor-extra:3.3.6.RELEASE" level="project" />
+    <orderEntry type="library" name="Maven: io.netty:netty-buffer:4.1.51.Final" level="project" />
+    <orderEntry type="library" name="Maven: com.google.code.findbugs:jsr305:2.0.1" level="project" />
+    <orderEntry type="library" name="Maven: io.vavr:vavr:0.9.2" level="project" />
+    <orderEntry type="library" name="Maven: io.vavr:vavr-match:0.9.2" level="project" />
+    <orderEntry type="library" name="Maven: commons-codec:commons-codec:1.14" level="project" />
+    <orderEntry type="library" name="Maven: org.hswebframework.web:hsweb-starter:4.0.12" level="project" />
+    <orderEntry type="library" name="Maven: com.fasterxml.jackson.core:jackson-databind:2.10.4" level="project" />
+    <orderEntry type="library" name="Maven: com.fasterxml.jackson.core:jackson-annotations:2.10.4" level="project" />
+    <orderEntry type="library" name="Maven: org.hswebframework:hsweb-expands-script:3.0.2" level="project" />
+    <orderEntry type="library" name="Maven: org.hswebframework:hsweb-expands-security:3.0.2" level="project" />
+    <orderEntry type="library" name="Maven: org.springframework.boot:spring-boot-autoconfigure:2.3.11.RELEASE" level="project" />
+    <orderEntry type="library" name="Maven: org.springframework.boot:spring-boot:2.3.11.RELEASE" level="project" />
+    <orderEntry type="library" name="Maven: io.r2dbc:r2dbc-h2:0.8.4.RELEASE" level="project" />
+    <orderEntry type="library" name="Maven: io.r2dbc:r2dbc-spi:0.8.5.RELEASE" level="project" />
+    <orderEntry type="library" name="Maven: com.h2database:h2:1.4.200" level="project" />
+    <orderEntry type="library" name="Maven: org.hswebframework:hsweb-easy-orm-rdb:4.0.13-SNAPSHOT" level="project" />
+    <orderEntry type="library" name="Maven: org.hswebframework:hsweb-easy-orm-core:4.0.13-SNAPSHOT" level="project" />
+    <orderEntry type="library" name="Maven: org.apache.commons:commons-collections4:4.4" level="project" />
+    <orderEntry type="library" name="Maven: commons-beanutils:commons-beanutils:1.9.4" level="project" />
+    <orderEntry type="library" name="Maven: commons-logging:commons-logging:1.2" level="project" />
+    <orderEntry type="library" name="Maven: commons-collections:commons-collections:3.2.2" level="project" />
+    <orderEntry type="module" module-name="gateway-component" />
+    <orderEntry type="module" module-name="network-core" />
+    <orderEntry type="library" name="Maven: org.jetlinks:rule-engine-support:1.1.7-SNAPSHOT" level="project" />
+    <orderEntry type="library" name="Maven: org.jetlinks:rule-engine-api:1.1.7-SNAPSHOT" level="project" />
+    <orderEntry type="library" name="Maven: com.digitalpetri.modbus:modbus-master-tcp:1.1.0" level="project" />
+    <orderEntry type="library" name="Maven: com.digitalpetri.modbus:modbus-codec:1.1.0" level="project" />
+    <orderEntry type="library" name="Maven: com.digitalpetri.modbus:modbus-core:1.1.0" level="project" />
+    <orderEntry type="library" name="Maven: io.dropwizard.metrics:metrics-core:4.1.21" level="project" />
+    <orderEntry type="library" name="Maven: com.fazecast:jSerialComm:2.6.2" level="project" />
+    <orderEntry type="library" name="Maven: org.bouncycastle:bcprov-jdk15on:1.64" level="project" />
+    <orderEntry type="library" name="Maven: org.eclipse.californium:californium-core:2.2.3" level="project" />
+    <orderEntry type="library" name="Maven: org.eclipse.californium:element-connector:2.2.3" level="project" />
+    <orderEntry type="library" name="Maven: io.vertx:vertx-core:3.8.5" level="project" />
+    <orderEntry type="library" name="Maven: cn.hutool:hutool-json:5.7.16" level="project" />
+    <orderEntry type="library" name="Maven: cn.hutool:hutool-core:5.7.16" level="project" />
+    <orderEntry type="module" module-name="dashboard-component" />
+    <orderEntry type="library" name="Maven: com.github.ben-manes.caffeine:caffeine:2.8.8" level="project" />
+    <orderEntry type="library" name="Maven: org.checkerframework:checker-qual:3.8.0" level="project" />
+    <orderEntry type="library" name="Maven: com.google.errorprone:error_prone_annotations:2.4.0" level="project" />
+    <orderEntry type="library" scope="TEST" name="Maven: org.springframework.boot:spring-boot-starter-test:2.3.11.RELEASE" level="project" />
+    <orderEntry type="library" name="Maven: org.springframework.boot:spring-boot-starter:2.3.11.RELEASE" level="project" />
+    <orderEntry type="library" name="Maven: org.springframework.boot:spring-boot-starter-logging:2.3.11.RELEASE" level="project" />
+    <orderEntry type="library" name="Maven: org.apache.logging.log4j:log4j-to-slf4j:2.13.3" level="project" />
+    <orderEntry type="library" name="Maven: org.slf4j:jul-to-slf4j:1.7.30" level="project" />
+    <orderEntry type="library" name="Maven: jakarta.annotation:jakarta.annotation-api:1.3.5" level="project" />
+    <orderEntry type="library" scope="TEST" name="Maven: org.springframework.boot:spring-boot-test:2.3.11.RELEASE" level="project" />
+    <orderEntry type="library" scope="TEST" name="Maven: org.springframework.boot:spring-boot-test-autoconfigure:2.3.11.RELEASE" level="project" />
+    <orderEntry type="library" scope="TEST" name="Maven: com.jayway.jsonpath:json-path:2.4.0" level="project" />
+    <orderEntry type="library" scope="TEST" name="Maven: net.minidev:json-smart:2.3.1" level="project" />
+    <orderEntry type="library" scope="TEST" name="Maven: net.minidev:accessors-smart:2.3.1" level="project" />
+    <orderEntry type="library" name="Maven: jakarta.xml.bind:jakarta.xml.bind-api:2.3.3" level="project" />
+    <orderEntry type="library" name="Maven: jakarta.activation:jakarta.activation-api:1.2.2" level="project" />
+    <orderEntry type="library" scope="TEST" name="Maven: org.assertj:assertj-core:3.16.1" level="project" />
+    <orderEntry type="library" scope="TEST" name="Maven: org.hamcrest:hamcrest:2.2" level="project" />
+    <orderEntry type="library" scope="TEST" name="Maven: org.junit.jupiter:junit-jupiter:5.6.3" level="project" />
+    <orderEntry type="library" scope="TEST" name="Maven: org.junit.jupiter:junit-jupiter-api:5.6.3" level="project" />
+    <orderEntry type="library" scope="TEST" name="Maven: org.opentest4j:opentest4j:1.2.0" level="project" />
+    <orderEntry type="library" scope="TEST" name="Maven: org.junit.platform:junit-platform-commons:1.6.3" level="project" />
+    <orderEntry type="library" scope="TEST" name="Maven: org.junit.jupiter:junit-jupiter-params:5.6.3" level="project" />
+    <orderEntry type="library" scope="TEST" name="Maven: org.junit.jupiter:junit-jupiter-engine:5.6.3" level="project" />
+    <orderEntry type="library" scope="TEST" name="Maven: org.junit.vintage:junit-vintage-engine:5.6.3" level="project" />
+    <orderEntry type="library" scope="TEST" name="Maven: org.apiguardian:apiguardian-api:1.1.0" level="project" />
+    <orderEntry type="library" scope="TEST" name="Maven: org.junit.platform:junit-platform-engine:1.6.3" level="project" />
+    <orderEntry type="library" scope="TEST" name="Maven: org.mockito:mockito-core:3.3.3" level="project" />
+    <orderEntry type="library" scope="TEST" name="Maven: net.bytebuddy:byte-buddy-agent:1.10.22" level="project" />
+    <orderEntry type="library" scope="TEST" name="Maven: org.objenesis:objenesis:2.6" level="project" />
+    <orderEntry type="library" scope="TEST" name="Maven: org.mockito:mockito-junit-jupiter:3.3.3" level="project" />
+    <orderEntry type="library" scope="TEST" name="Maven: org.skyscreamer:jsonassert:1.5.0" level="project" />
+    <orderEntry type="library" scope="TEST" name="Maven: com.vaadin.external.google:android-json:0.0.20131108.vaadin1" level="project" />
+    <orderEntry type="library" name="Maven: org.springframework:spring-core:5.2.15.RELEASE" level="project" />
+    <orderEntry type="library" name="Maven: org.springframework:spring-jcl:5.2.15.RELEASE" level="project" />
+    <orderEntry type="library" scope="TEST" name="Maven: org.springframework:spring-test:5.2.15.RELEASE" level="project" />
+    <orderEntry type="library" scope="TEST" name="Maven: org.xmlunit:xmlunit-core:2.7.0" level="project" />
+    <orderEntry type="library" name="Maven: dev.miku:r2dbc-mysql:0.8.2.RELEASE" level="project" />
+    <orderEntry type="library" name="Maven: io.projectreactor:reactor-tools:3.3.17.RELEASE" level="project" />
+    <orderEntry type="library" scope="TEST" name="Maven: io.projectreactor:reactor-test:3.3.17.RELEASE" level="project" />
+    <orderEntry type="library" name="Maven: org.springframework:spring-context-indexer:5.2.15.RELEASE" level="project" />
+    <orderEntry type="library" name="Maven: io.projectreactor:reactor-core:3.3.17.RELEASE" level="project" />
+    <orderEntry type="library" name="Maven: org.reactivestreams:reactive-streams:1.0.3" level="project" />
+    <orderEntry type="library" name="Maven: org.codehaus.groovy:groovy-all:2.4.17" level="project" />
+    <orderEntry type="library" scope="TEST" name="Maven: junit:junit:4.12" level="project" />
+    <orderEntry type="library" scope="TEST" name="Maven: org.hamcrest:hamcrest-core:2.2" level="project" />
+    <orderEntry type="library" name="Maven: org.slf4j:slf4j-api:1.7.30" level="project" />
+    <orderEntry type="library" name="Maven: ch.qos.logback:logback-classic:1.2.3" level="project" />
+    <orderEntry type="library" name="Maven: ch.qos.logback:logback-core:1.2.3" level="project" />
+    <orderEntry type="library" scope="PROVIDED" name="Maven: org.projectlombok:lombok:1.18.20" level="project" />
+    <orderEntry type="library" name="Maven: org.springframework.boot:spring-boot-configuration-processor:2.3.11.RELEASE" level="project" />
+    <orderEntry type="library" name="Maven: org.hswebframework:hsweb-utils:3.0.2" level="project" />
+    <orderEntry type="library" name="Maven: joda-time:joda-time:2.7" level="project" />
+  </component>
+</module>

+ 94 - 0
jetlinks-manager/media-manager/pom.xml

@@ -0,0 +1,94 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <parent>
+        <artifactId>jetlinks-components</artifactId>
+        <groupId>org.jetlinks.community</groupId>
+        <version>1.10.0-SNAPSHOT</version>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+
+    <artifactId>media-manager</artifactId>
+
+
+    <dependencies>
+        <dependency>
+            <groupId>javax.sip</groupId>
+            <artifactId>jain-sip-ri</artifactId>
+            <version>1.3.0-91</version>
+        </dependency>
+
+        <dependency>
+            <groupId>com.squareup.okhttp3</groupId>
+            <artifactId>okhttp</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.redisson</groupId>
+            <artifactId>redisson-spring-boot-starter</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.hswebframework.web</groupId>
+            <artifactId>hsweb-authorization-api</artifactId>
+            <version>${hsweb.framework.version}</version>
+        </dependency>
+
+        <dependency>
+            <groupId>cn.hutool</groupId>
+            <artifactId>hutool-all</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>${project.groupId}</groupId>
+            <artifactId>elasticsearch-component</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+
+        <dependency>
+            <groupId>${project.groupId}</groupId>
+            <artifactId>io-component</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+
+        <dependency>
+            <groupId>org.jetlinks</groupId>
+            <artifactId>jetlinks-supports</artifactId>
+            <version>${jetlinks.version}</version>
+        </dependency>
+
+        <dependency>
+            <groupId>org.jetlinks</groupId>
+            <artifactId>jetlinks-core</artifactId>
+            <version>${jetlinks.version}</version>
+        </dependency>
+
+        <dependency>
+            <groupId>org.hswebframework.web</groupId>
+            <artifactId>hsweb-starter</artifactId>
+            <version>${hsweb.framework.version}</version>
+        </dependency>
+
+
+        <dependency>
+            <groupId>io.r2dbc</groupId>
+            <artifactId>r2dbc-h2</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.hswebframework</groupId>
+            <artifactId>hsweb-easy-orm-rdb</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>${project.groupId}</groupId>
+            <artifactId>gateway-component</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+
+        <dependency>
+            <groupId>com.github.ben-manes.caffeine</groupId>
+            <artifactId>caffeine</artifactId>
+        </dependency>
+    </dependencies>
+</project>

+ 21 - 0
jetlinks-manager/media-manager/src/main/java/org/jetlinks/community/media/bean/DeviceNotFoundEvent.java

@@ -0,0 +1,21 @@
+package org.jetlinks.community.media.bean;
+
+import javax.sip.Dialog;
+import java.util.EventObject;
+
+public class DeviceNotFoundEvent extends EventObject {
+    /**
+     * Constructs a prototypical Event.
+     *
+     * @param dialog
+     * @throws IllegalArgumentException if source is null.
+     */
+    public DeviceNotFoundEvent(Dialog dialog) {
+        super(dialog);
+    }
+
+
+    public Dialog getDialog() {
+        return (Dialog)super.getSource();
+    }
+}

+ 66 - 0
jetlinks-manager/media-manager/src/main/java/org/jetlinks/community/media/bean/EventResult.java

@@ -0,0 +1,66 @@
+package org.jetlinks.community.media.bean;
+
+import lombok.Data;
+
+import javax.sip.*;
+import javax.sip.header.CallIdHeader;
+import javax.sip.message.Response;
+@Data
+public class EventResult<EventObject>{
+        public int statusCode;
+        public String type;
+        public String msg;
+        public String callId;
+        public Dialog dialog;
+        public EventObject event;
+
+        public EventResult() {
+        }
+
+        public boolean getSuccess(){
+            return event instanceof ResponseEvent;
+        }
+        public EventResult(EventObject event) {
+            this.event = event;
+            if (event instanceof ResponseEvent) {
+                ResponseEvent responseEvent = (ResponseEvent)event;
+                Response response = responseEvent.getResponse();
+                this.dialog = responseEvent.getDialog();
+                this.type = "response";
+                if (response != null) {
+                    this.msg = response.getReasonPhrase();
+                    this.statusCode = response.getStatusCode();
+                }
+                this.callId = ((CallIdHeader)response.getHeader(CallIdHeader.NAME)).getCallId();
+
+            }else if (event instanceof TimeoutEvent) {
+                TimeoutEvent timeoutEvent = (TimeoutEvent)event;
+                this.type = "timeout";
+                this.msg = "消息超时未回复";
+                this.statusCode = -1024;
+                this.callId = timeoutEvent.getClientTransaction().getDialog().getCallId().getCallId();
+                this.dialog = timeoutEvent.getClientTransaction().getDialog();
+            }else if (event instanceof TransactionTerminatedEvent) {
+                TransactionTerminatedEvent transactionTerminatedEvent = (TransactionTerminatedEvent)event;
+                this.type = "transactionTerminated";
+                this.msg = "事务已结束";
+                this.statusCode = -1024;
+                this.callId = transactionTerminatedEvent.getClientTransaction().getDialog().getCallId().getCallId();
+                this.dialog = transactionTerminatedEvent.getClientTransaction().getDialog();
+            }else if (event instanceof DialogTerminatedEvent) {
+                DialogTerminatedEvent dialogTerminatedEvent = (DialogTerminatedEvent)event;
+                this.type = "dialogTerminated";
+                this.msg = "会话已结束";
+                this.statusCode = -1024;
+                this.callId = dialogTerminatedEvent.getDialog().getCallId().getCallId();
+                this.dialog = dialogTerminatedEvent.getDialog();
+            }else if (event instanceof DeviceNotFoundEvent) {
+                DeviceNotFoundEvent deviceNotFoundEvent = (DeviceNotFoundEvent)event;
+                this.type = "deviceNotFoundEvent";
+                this.msg = "设备未找到";
+                this.statusCode = -1024;
+                this.callId = deviceNotFoundEvent.getDialog().getCallId().getCallId();
+                this.dialog = deviceNotFoundEvent.getDialog();
+            }
+        }
+    }

+ 18 - 0
jetlinks-manager/media-manager/src/main/java/org/jetlinks/community/media/bean/SSRCInfo.java

@@ -0,0 +1,18 @@
+package org.jetlinks.community.media.bean;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+
+/**
+ * 同步信源(SSRC)标识符
+ * 占32位,用于标识同步信源。该标识符是随机选择的,参加同一视频会议的两个同步信源不能有相同的SSRC。
+ */
+@Data
+@AllArgsConstructor(staticName = "of")
+public class SSRCInfo {
+
+    private int port;
+    private String ssrc;
+    private String streamId;
+
+}

+ 180 - 0
jetlinks-manager/media-manager/src/main/java/org/jetlinks/community/media/bean/SendRtpItem.java

@@ -0,0 +1,180 @@
+package org.jetlinks.community.media.bean;
+
+import lombok.Data;
+
+@Data
+public class SendRtpItem {
+
+    /**
+     * 推流ip
+     */
+    private String ip;
+
+    /**
+     * 推流端口
+     */
+    private int port;
+
+    /**
+     * 推流标识
+     */
+    private String ssrc;
+
+    /**
+     * 平台id
+     */
+    private String platformId;
+
+     /**
+     * 对应设备id
+     */
+    private String deviceId;
+
+    /**
+     * 直播流的应用名
+     */
+    private String app;
+
+   /**
+     * 通道id
+     */
+    private String channelId;
+
+    /**
+     * 推流状态
+     * 0 等待设备推流上来
+     * 1 等待上级平台回复ack
+     * 2 推流中
+     */
+    private int status = 0;
+
+
+    /**
+     * 设备推流的streamId
+     */
+    private String streamId;
+
+    /**
+     * 是否为tcp
+     */
+    private boolean tcp;
+
+    /**
+     * 是否为tcp主动模式
+     */
+    private boolean tcpActive;
+
+    /**
+     * 自己推流使用的端口
+     */
+    private int localPort;
+
+    /**
+     * 使用的流媒体
+     */
+    private String mediaServerId;
+
+    public String getIp() {
+        return ip;
+    }
+
+    public void setIp(String ip) {
+        this.ip = ip;
+    }
+
+    public int getPort() {
+        return port;
+    }
+
+    public void setPort(int port) {
+        this.port = port;
+    }
+
+    public String getSsrc() {
+        return ssrc;
+    }
+
+    public void setSsrc(String ssrc) {
+        this.ssrc = ssrc;
+    }
+
+    public String getPlatformId() {
+        return platformId;
+    }
+
+    public void setPlatformId(String platformId) {
+        this.platformId = platformId;
+    }
+
+    public String getDeviceId() {
+        return deviceId;
+    }
+
+    public void setDeviceId(String deviceId) {
+        this.deviceId = deviceId;
+    }
+
+    public String getChannelId() {
+        return channelId;
+    }
+
+    public void setChannelId(String channelId) {
+        this.channelId = channelId;
+    }
+
+    public int getStatus() {
+        return status;
+    }
+
+    public void setStatus(int status) {
+        this.status = status;
+    }
+
+    public String getApp() {
+        return app;
+    }
+
+    public void setApp(String app) {
+        this.app = app;
+    }
+
+    public String getStreamId() {
+        return streamId;
+    }
+
+    public void setStreamId(String streamId) {
+        this.streamId = streamId;
+    }
+
+    public boolean isTcp() {
+        return tcp;
+    }
+
+    public void setTcp(boolean tcp) {
+        this.tcp = tcp;
+    }
+
+    public int getLocalPort() {
+        return localPort;
+    }
+
+    public void setLocalPort(int localPort) {
+        this.localPort = localPort;
+    }
+
+    public boolean isTcpActive() {
+        return tcpActive;
+    }
+
+    public void setTcpActive(boolean tcpActive) {
+        this.tcpActive = tcpActive;
+    }
+
+    public String getMediaServerId() {
+        return mediaServerId;
+    }
+
+    public void setMediaServerId(String mediaServerId) {
+        this.mediaServerId = mediaServerId;
+    }
+}

+ 266 - 0
jetlinks-manager/media-manager/src/main/java/org/jetlinks/community/media/bean/StreamInfo.java

@@ -0,0 +1,266 @@
+package org.jetlinks.community.media.bean;
+
+
+public class StreamInfo {
+
+    private String app;
+    private String streamId;
+    private String deviceID;
+    private String channelId;
+    private String flv;
+    private String https_flv;
+    private String ws_flv;
+    private String wss_flv;
+    private String fmp4;
+    private String https_fmp4;
+    private String ws_fmp4;
+    private String wss_fmp4;
+    private String hls;
+    private String https_hls;
+    private String ws_hls;
+    private String wss_hls;
+    private String ts;
+    private String https_ts;
+    private String ws_ts;
+    private String wss_ts;
+    private String rtmp;
+    private String rtmps;
+    private String rtsp;
+    private String rtsps;
+    private String rtc;
+    private String mediaServerId;
+    private Object tracks;
+
+    public static class TransactionInfo{
+        public String callId;
+        public String localTag;
+        public String remoteTag;
+        public String branch;
+    }
+
+    private TransactionInfo transactionInfo;
+
+    public String getApp() {
+        return app;
+    }
+
+    public void setApp(String app) {
+        this.app = app;
+    }
+
+    public String getDeviceID() {
+        return deviceID;
+    }
+
+    public void setDeviceID(String deviceID) {
+        this.deviceID = deviceID;
+    }
+
+    public String getChannelId() {
+        return channelId;
+    }
+
+    public void setChannelId(String channelId) {
+        this.channelId = channelId;
+    }
+
+    public String getFlv() {
+        return flv;
+    }
+
+    public void setFlv(String flv) {
+        this.flv = flv;
+    }
+
+    public String getWs_flv() {
+        return ws_flv;
+    }
+
+    public void setWs_flv(String ws_flv) {
+        this.ws_flv = ws_flv;
+    }
+
+    public String getRtmp() {
+        return rtmp;
+    }
+
+    public void setRtmp(String rtmp) {
+        this.rtmp = rtmp;
+    }
+
+    public String getHls() {
+        return hls;
+    }
+
+    public void setHls(String hls) {
+        this.hls = hls;
+    }
+
+    public String getRtsp() {
+        return rtsp;
+    }
+
+    public void setRtsp(String rtsp) {
+        this.rtsp = rtsp;
+    }
+
+    public Object getTracks() {
+        return tracks;
+    }
+
+    public void setTracks(Object tracks) {
+        this.tracks = tracks;
+    }
+
+    public String getFmp4() {
+        return fmp4;
+    }
+
+    public void setFmp4(String fmp4) {
+        this.fmp4 = fmp4;
+    }
+
+    public String getWs_fmp4() {
+        return ws_fmp4;
+    }
+
+    public void setWs_fmp4(String ws_fmp4) {
+        this.ws_fmp4 = ws_fmp4;
+    }
+
+    public String getWs_hls() {
+        return ws_hls;
+    }
+
+    public void setWs_hls(String ws_hls) {
+        this.ws_hls = ws_hls;
+    }
+
+    public String getTs() {
+        return ts;
+    }
+
+    public void setTs(String ts) {
+        this.ts = ts;
+    }
+
+    public String getWs_ts() {
+        return ws_ts;
+    }
+
+    public void setWs_ts(String ws_ts) {
+        this.ws_ts = ws_ts;
+    }
+
+    public String getStreamId() {
+        return streamId;
+    }
+
+    public void setStreamId(String streamId) {
+        this.streamId = streamId;
+    }
+
+    public String getRtc() {
+        return rtc;
+    }
+
+    public void setRtc(String rtc) {
+        this.rtc = rtc;
+    }
+
+    public TransactionInfo getTransactionInfo() {
+        return transactionInfo;
+    }
+
+    public void setTransactionInfo(TransactionInfo transactionInfo) {
+        this.transactionInfo = transactionInfo;
+    }
+
+    public String getMediaServerId() {
+        return mediaServerId;
+    }
+
+    public void setMediaServerId(String mediaServerId) {
+        this.mediaServerId = mediaServerId;
+    }
+
+    public String getHttps_flv() {
+        return https_flv;
+    }
+
+    public void setHttps_flv(String https_flv) {
+        this.https_flv = https_flv;
+    }
+
+    public String getWss_flv() {
+        return wss_flv;
+    }
+
+    public void setWss_flv(String wss_flv) {
+        this.wss_flv = wss_flv;
+    }
+
+    public String getWss_fmp4() {
+        return wss_fmp4;
+    }
+
+    public void setWss_fmp4(String wss_fmp4) {
+        this.wss_fmp4 = wss_fmp4;
+    }
+
+    public String getWss_hls() {
+        return wss_hls;
+    }
+
+    public void setWss_hls(String wss_hls) {
+        this.wss_hls = wss_hls;
+    }
+
+    public String getWss_ts() {
+        return wss_ts;
+    }
+
+    public void setWss_ts(String wss_ts) {
+        this.wss_ts = wss_ts;
+    }
+
+    public String getRtmps() {
+        return rtmps;
+    }
+
+    public void setRtmps(String rtmps) {
+        this.rtmps = rtmps;
+    }
+
+    public String getRtsps() {
+        return rtsps;
+    }
+
+    public void setRtsps(String rtsps) {
+        this.rtsps = rtsps;
+    }
+
+    public String getHttps_hls() {
+        return https_hls;
+    }
+
+    public void setHttps_hls(String https_hls) {
+        this.https_hls = https_hls;
+    }
+
+    public String getHttps_fmp4() {
+        return https_fmp4;
+    }
+
+    public void setHttps_fmp4(String https_fmp4) {
+        this.https_fmp4 = https_fmp4;
+    }
+
+    public String getHttps_ts() {
+        return https_ts;
+    }
+
+    public void setHttps_ts(String https_ts) {
+        this.https_ts = https_ts;
+    }
+}

+ 18 - 0
jetlinks-manager/media-manager/src/main/java/org/jetlinks/community/media/contanst/CmdType.java

@@ -0,0 +1,18 @@
+package org.jetlinks.community.media.contanst;
+
+/**
+ * @author lifang
+ * @version 1.0.0
+ * @ClassName CmdType.java
+ * @Description TODO
+ * @createTime 2022年01月21日 11:14:00
+ */
+public class CmdType {
+    public static final String CATALOG = "Catalog";
+    public static final String ALARM = "Alarm";
+    public static final String MOBILE_POSITION = "MobilePosition";
+    public static final String KEEP_ALIVE = "Keepalive";
+    public static final String NOTIFY = "Notify";
+    public static final String QUERY = "Query";
+    public static final String RESPONSE = "Response";
+}

+ 72 - 0
jetlinks-manager/media-manager/src/main/java/org/jetlinks/community/media/contanst/VideoManagerConstants.java

@@ -0,0 +1,72 @@
+package org.jetlinks.community.media.contanst;
+
+/**    
+ * @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_SERVER_KEEPALIVE_PREFIX = "VMP_MEDIA_SERVER_KEEPALIVE_";
+
+	public static final String MEDIA_SERVERS_ONLINE_PREFIX = "VMP_MEDIA_ONLINE_SERVERS_";
+
+	public static final String MEDIA_STREAM_PREFIX = "VMP_MEDIA_STREAM";
+
+	public static final String DEVICE_PREFIX = "VMP_DEVICE_";
+
+	public static final String CACHEKEY_PREFIX = "VMP_CHANNEL_";
+
+	public static final String KEEPLIVEKEY_PREFIX = "VMP_KEEPALIVE_";
+
+	// 此处多了一个_,暂不修改
+	public static final String PLAYER_PREFIX = "VMP_PLAYER_";
+	public static final String PLAY_BLACK_PREFIX = "VMP_PLAYBACK_";
+
+	public static final String DOWNLOAD_PREFIX = "VMP_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_KEEPLIVE = "2";
+
+	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 SIP_CSEQ_PREFIX = "VMP_SIP_CSEQ_";
+
+	public static final String SIP_SN_PREFIX = "VMP_SIP_SN_";
+
+	public static final String SIP_SUBSCRIBE_PREFIX = "SIP_SUBSCRIBE_";
+
+	//************************** redis 消息*********************************
+	public static final String WVP_MSG_STREAM_CHANGE_PREFIX = "WVP_MSG_STREAM_CHANGE_";
+	public static final String WVP_MSG_GPS_PREFIX = "WVP_MSG_GPS_";
+
+	//**************************    第三方  ****************************************
+	public static final String WVP_STREAM_GB_ID_PREFIX = "memberNo_";
+	public static final String WVP_STREAM_GPS_MSG_PREFIX = "WVP_STREAM_GPS_MSG_";
+}

+ 146 - 0
jetlinks-manager/media-manager/src/main/java/org/jetlinks/community/media/controller/ApiStreamController.java

@@ -0,0 +1,146 @@
+//package org.jetlinks.community.media.controller;
+//
+//import com.alibaba.fastjson.JSONObject;
+//import io.swagger.v3.oas.annotations.tags.Tag;
+//import lombok.AllArgsConstructor;
+//import lombok.extern.slf4j.Slf4j;
+//import org.hswebframework.web.authorization.annotation.Authorize;
+//import org.hswebframework.web.authorization.annotation.Resource;
+//import org.jetlinks.community.media.bean.StreamInfo;
+//import org.jetlinks.community.media.entity.DeviceChannel;
+//import org.jetlinks.community.media.entity.MediaDevice;
+//import org.jetlinks.community.media.enums.DeviceState;
+//import org.jetlinks.community.media.vmanager.gb28181.play.bean.PlayResult;
+//import org.jetlinks.community.media.zlm.dto.MediaServerItem;
+//import org.jetlinks.community.utils.SipUtils;
+//import org.redisson.api.RedissonClient;
+//import org.springframework.web.bind.annotation.GetMapping;
+//import org.springframework.web.bind.annotation.RequestMapping;
+//import org.springframework.web.bind.annotation.RequestParam;
+//import org.springframework.web.bind.annotation.RestController;
+//import org.springframework.web.context.request.async.DeferredResult;
+//import reactor.core.publisher.Mono;
+//
+///**
+// * @author lifang
+// * @version 1.0.0
+// * @ClassName ApiStreamController.java
+// * @Description TODO
+// * @createTime 2022年01月22日 08:26:00
+// */
+//@RestController
+//@RequestMapping("/api/stream")
+//@Slf4j
+//@Authorize
+//@Resource(id="api-stream",name = "媒体流")
+//@AllArgsConstructor
+//@Tag(name = "媒体流")
+//public class ApiStreamController {
+//
+//    private final RedissonClient redissonClient;
+//    /**
+//     * 实时直播 - 开始直播
+//     * @param serial 设备编号
+//     * @param channel 通道序号 默认值: 1
+//     * @param code 通道编号,通过 /api/v1/device/channellist 获取的 ChannelList.ID, 该参数和 channel 二选一传递即可
+//     * @param cdn TODO 转推 CDN 地址, 形如: [rtmp|rtsp]://xxx, encodeURIComponent
+//     * @param audio TODO 是否开启音频, 默认 开启
+//     * @param transport 流传输模式, 默认 UDP
+//     * @param checkchannelstatus TODO 是否检查通道状态, 默认 false, 表示 拉流前不检查通道状态是否在线
+//     * @param transportmode TODO 当 transport=TCP 时有效, 指示流传输主被动模式, 默认被动
+//     * @param timeout TODO 拉流超时(秒),
+//     * @return
+//     */
+//    @GetMapping(value = "/start")
+//    public Mono<?> start(String serial ,
+//                         @RequestParam(required = false)Integer channel ,
+//                         @RequestParam(required = false)String code,
+//                         @RequestParam(required = false)String cdn,
+//                         @RequestParam(required = false)String audio,
+//                         @RequestParam(required = false)String transport,
+//                         @RequestParam(required = false)String checkchannelstatus ,
+//                         @RequestParam(required = false)String transportmode,
+//                         @RequestParam(required = false)String timeout
+//    ){
+//        DeferredResult<JSONObject> resultDeferredResult = new DeferredResult<>(userSetup.getPlayTimeout() + 10);
+//        MediaDevice device  = (MediaDevice) redissonClient.getBucket(SipUtils.keepAliveKey(serial)).get();
+////        MediaDevice device = storager.queryVideoDevice(serial);
+//        if (device == null ) {
+//            JSONObject result = new JSONObject();
+//            result.put("error","device[ " + serial + " ]未找到或已离线");
+//            resultDeferredResult.setResult(result);
+//        }
+//        resultDeferredResult.onTimeout(()->{
+//            log.info("播放等待超时");
+//            JSONObject result = new JSONObject();
+//            result.put("error","timeout");
+//            resultDeferredResult.setResult(result);
+//
+//            // 清理RTP server
+//        });
+//
+//        DeviceChannel deviceChannel = storager.queryChannel(serial, code);
+//        if (deviceChannel == null) {
+//            JSONObject result = new JSONObject();
+//            result.put("error","channel[ " + code + " ]未找到");
+//            resultDeferredResult.setResult(result);
+//        }else if (deviceChannel.getStatus() == 0) {
+//            JSONObject result = new JSONObject();
+//            result.put("error","channel[ " + code + " ]offline");
+//            resultDeferredResult.setResult(result);
+//        }
+//        MediaServerItem newMediaServerItem = playService.getNewMediaServerItem(device);
+//        PlayResult play = playService.play(newMediaServerItem, serial, code, (mediaServerItem, response)->{
+//            StreamInfo streamInfo = redisCatchStorage.queryPlayByDevice(serial, code);
+//            JSONObject result = new JSONObject();
+//            result.put("StreamID", streamInfo.getStreamId());
+//            result.put("DeviceID", device.getId());
+//            result.put("ChannelID", code);
+//            result.put("ChannelName", deviceChannel.getName());
+//            result.put("ChannelCustomName", "");
+//            result.put("FLV", streamInfo.getFlv());
+//            result.put("WS_FLV", streamInfo.getWs_flv());
+//            result.put("RTMP", streamInfo.getRtmp());
+//            result.put("HLS", streamInfo.getHls());
+//            result.put("RTSP", streamInfo.getRtsp());
+//            result.put("CDN", "");
+//            result.put("SnapURL", "");
+//            result.put("Transport", device.getTransport());
+//            result.put("StartAt", "");
+//            result.put("Duration", "");
+//            result.put("SourceVideoCodecName", "");
+//            result.put("SourceVideoWidth", "");
+//            result.put("SourceVideoHeight", "");
+//            result.put("SourceVideoFrameRate", "");
+//            result.put("SourceAudioCodecName", "");
+//            result.put("SourceAudioSampleRate", "");
+//            result.put("AudioEnable", "");
+//            result.put("Ondemand", "");
+//            result.put("InBytes", "");
+//            result.put("InBitRate", "");
+//            result.put("OutBytes", "");
+//            result.put("NumOutputs", "");
+//            result.put("CascadeSize", "");
+//            result.put("RelaySize", "");
+//            result.put("ChannelPTZType", "0");
+//            resultDeferredResult.setResult(result);
+////            Class<?> aClass = responseEntity.getClass().getSuperclass();
+////            Field body = null;
+////            try {
+////                // 使用反射动态修改返回的body
+////                body = aClass.getDeclaredField("body");
+////                body.setAccessible(true);
+////                body.set(responseEntity, result);
+////            } catch (NoSuchFieldException e) {
+////                e.printStackTrace();
+////            } catch (IllegalAccessException e) {
+////                e.printStackTrace();
+////            }
+//        }, (eventResult) -> {
+//            JSONObject result = new JSONObject();
+//            result.put("error", "channel[ " + code + " ] " + eventResult.msg);
+//            resultDeferredResult.setResult(result);
+//        });
+//        return Mono.just(resultDeferredResult);
+//    }
+//}

+ 210 - 0
jetlinks-manager/media-manager/src/main/java/org/jetlinks/community/media/core/DigestServerAuthenticationHelper2016.java

@@ -0,0 +1,210 @@
+/*
+ * Conditions Of Use
+ *
+ * This software was developed by employees of the National Institute of
+ * Standards and Technology (NIST), an agency of the Federal Government.
+ * Pursuant to title 15 Untied States Code Section 105, works of NIST
+ * employees are not subject to copyright protection in the United States
+ * and are considered to be in the public domain.  As a result, a formal
+ * license is not needed to use the software.
+ *
+ * This software is provided by NIST as a service and is expressly
+ * provided "AS IS."  NIST MAKES NO WARRANTY OF ANY KIND, EXPRESS, IMPLIED
+ * OR STATUTORY, INCLUDING, WITHOUT LIMITATION, THE IMPLIED WARRANTY OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, NON-INFRINGEMENT
+ * AND DATA ACCURACY.  NIST does not warrant or make any representations
+ * regarding the use of the software or the results thereof, including but
+ * not limited to the correctness, accuracy, reliability or usefulness of
+ * the software.
+ *
+ * Permission to use this software is contingent upon your acceptance
+ * of the terms of this agreement
+ *
+ * .
+ *
+ */
+package org.jetlinks.community.media.core;
+
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.Date;
+import java.util.Random;
+
+import gov.nist.core.InternalErrorHandler;
+import gov.nist.javax.sip.header.Authorization;
+import gov.nist.javax.sip.stack.SIPTransactionStack;
+
+import javax.sip.address.URI;
+import javax.sip.header.*;
+import javax.sip.message.Request;
+import javax.sip.message.Response;
+/**
+ * Implements the HTTP digest authentication method server side functionality.
+ *
+ * @author M. Ranganathan
+ * @author Marc Bednarek
+ */
+
+public class DigestServerAuthenticationHelper2016  {
+
+    private MessageDigest messageDigest;
+
+    public static final String DEFAULT_ALGORITHM = "MD5";
+    public static final String DEFAULT_SCHEME = "Digest";
+
+
+
+
+    /** to hex converter */
+    private static final char[] toHex = { '0', '1', '2', '3', '4', '5', '6',
+        '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' };
+
+    /**
+     * Default constructor.
+     * @throws NoSuchAlgorithmException
+     */
+    public DigestServerAuthenticationHelper2016()
+        throws NoSuchAlgorithmException {
+        messageDigest = MessageDigest.getInstance(DEFAULT_ALGORITHM);
+    }
+
+    public static String toHexString(byte b[]) {
+        int pos = 0;
+        char[] c = new char[b.length * 2];
+        for (int i = 0; i < b.length; i++) {
+            c[pos++] = toHex[(b[i] >> 4) & 0x0F];
+            c[pos++] = toHex[b[i] & 0x0f];
+        }
+        return new String(c);
+    }
+
+    /**
+     * Generate the challenge string.
+     *
+     * @return a generated nonce.
+     */
+    private String generateNonce() {
+        // Get the time of day and run MD5 over it.
+        Date date = new Date();
+        long time = date.getTime();
+        Random rand = new Random();
+        long pad = rand.nextLong();
+        String nonceString = (new Long(time)).toString()
+            + (new Long(pad)).toString();
+        byte mdbytes[] = messageDigest.digest(nonceString.getBytes());
+        // Convert the mdbytes array into a hex string.
+        return toHexString(mdbytes);
+    }
+
+    public void generateChallenge(HeaderFactory headerFactory, Response response, String realm  ) {
+        try {
+            WWWAuthenticateHeader authenticateHeader = headerFactory.createWWWAuthenticateHeader(DEFAULT_SCHEME);
+            authenticateHeader.setParameter("realm", realm);
+            authenticateHeader.setParameter("nonce", generateNonce());
+//            authenticateHeader.setParameter("opaque", "");
+//            authenticateHeader.setParameter("stale", "FALSE");
+//            authenticateHeader.setParameter("algorithm", DEFAULT_ALGORITHM);
+            response.setHeader(authenticateHeader);
+        } catch (Exception ex) {
+            InternalErrorHandler.handleException(ex);
+        }
+
+    }
+    /**
+     * Authenticate the inbound request.
+     *
+     * @param request - the request to authenticate.
+     * @param hashedPassword -- the MD5 hashed string of username:realm:plaintext password.
+     *
+     * @return true if authentication succeded and false otherwise.
+     */
+    public boolean doAuthenticateHashedPassword(Request request, String hashedPassword) {
+        ProxyAuthorizationHeader authHeader = (ProxyAuthorizationHeader) request.getHeader(ProxyAuthorizationHeader.NAME);
+        if ( authHeader == null ) return false;
+        String realm = authHeader.getRealm();
+        String username = authHeader.getUsername();
+
+        if ( username == null || realm == null ) {
+            return false;
+        }
+
+        String nonce = authHeader.getNonce();
+        URI uri = authHeader.getURI();
+        if (uri == null) {
+            return false;
+        }
+
+
+
+        String A2 = request.getMethod().toUpperCase() + ":" + uri.toString();
+        String HA1 = hashedPassword;
+
+
+        byte[] mdbytes = messageDigest.digest(A2.getBytes());
+        String HA2 = toHexString(mdbytes);
+
+        String cnonce = authHeader.getCNonce();
+        String KD = HA1 + ":" + nonce;
+        if (cnonce != null) {
+            KD += ":" + cnonce;
+        }
+        KD += ":" + HA2;
+        mdbytes = messageDigest.digest(KD.getBytes());
+        String mdString = toHexString(mdbytes);
+        String response = authHeader.getResponse();
+
+
+        return mdString.equals(response);
+    }
+
+    /**
+     * Authenticate the inbound request given plain text password.
+     *
+     * @param request - the request to authenticate.
+     * @param pass -- the plain text password.
+     *
+     * @return true if authentication succeded and false otherwise.
+     */
+    public boolean doAuthenticatePlainTextPassword(Request request, String pass) {
+//        ProxyAuthorizationHeader authHeader = (ProxyAuthorizationHeader) request.getHeader(ProxyAuthorizationHeader.NAME);
+        Authorization authHeader = (Authorization) request.getHeader(AuthorizationHeader.NAME);
+        if ( authHeader == null ) return false;
+        String realm = authHeader.getRealm();
+        String username = authHeader.getUsername();
+
+
+        if ( username == null || realm == null ) {
+            return false;
+        }
+
+
+        String nonce = authHeader.getNonce();
+        URI uri = authHeader.getURI();
+        if (uri == null) {
+            return false;
+        }
+
+
+        String A1 = username + ":" + realm + ":" + pass;
+        String A2 = request.getMethod().toUpperCase() + ":" + uri.toString();
+        byte mdbytes[] = messageDigest.digest(A1.getBytes());
+        String HA1 = toHexString(mdbytes);
+
+
+        mdbytes = messageDigest.digest(A2.getBytes());
+        String HA2 = toHexString(mdbytes);
+
+        String cnonce = authHeader.getCNonce();
+        String KD = HA1 + ":" + nonce;
+        if (cnonce != null) {
+            KD += ":" + cnonce;
+        }
+        KD += ":" + HA2;
+        mdbytes = messageDigest.digest(KD.getBytes());
+        String mdString = toHexString(mdbytes);
+        String response = authHeader.getResponse();
+        return mdString.equals(response);
+
+    }
+
+}

+ 199 - 0
jetlinks-manager/media-manager/src/main/java/org/jetlinks/community/media/entity/DeviceChannel.java

@@ -0,0 +1,199 @@
+package org.jetlinks.community.media.entity;
+
+import lombok.Data;
+
+@Data
+public class DeviceChannel {
+
+
+
+	/**
+	 * 通道id
+	 */
+	private String channelId;
+
+	/**
+	 * 设备id
+	 */
+	private String deviceId;
+	
+	/**
+	 * 通道名
+	 */
+	private String name;
+	
+	/**
+	 * 生产厂商
+	 */
+	private String manufacture;
+	
+	/**
+	 * 型号
+	 */
+	private String model;
+	
+	/**
+	 * 设备归属
+	 */
+	private String owner;
+	
+	/**
+	 * 行政区域
+	 */
+	private String civilCode;
+	
+	/**
+	 * 警区
+	 */
+	private String block;
+
+	/**
+	 * 安装地址
+	 */
+	private String address;
+	
+	/**
+	 * 是否有子设备 1有, 0没有
+	 */
+	private int parental;
+	
+	/**
+	 * 父级id
+	 */
+	private String parentId;
+	
+	/**
+	 * 信令安全模式  缺省为0; 0:不采用; 2: S/MIME签名方式; 3: S/ MIME加密签名同时采用方式; 4:数字摘要方式
+	 */
+	private int safetyWay;
+	
+	/**
+	 * 注册方式 缺省为1;1:符合IETFRFC3261标准的认证注册模 式; 2:基于口令的双向认证注册模式; 3:基于数字证书的双向认证注册模式
+	 */
+	private int registerWay;
+	
+	/**
+	 * 证书序列号
+	 */
+	private String certNum;
+	
+	/**
+	 * 证书有效标识 缺省为0;证书有效标识:0:无效1: 有效
+	 */
+	private int certifiable;
+	
+	/**
+	 * 证书无效原因码
+	 */
+	private int errCode;
+	
+	/**
+	 * 证书终止有效期
+	 */
+	private String endTime;
+	
+	/**
+	 * 保密属性 缺省为0; 0:不涉密, 1:涉密
+	 */
+	private String secrecy;
+	
+	/**
+	 * IP地址
+	 */
+	private String ipAddress;
+	
+	/**
+	 * 端口号
+	 */
+	private int port;
+	
+	/**
+	 * 密码
+	 */
+	private String password;
+
+	/**
+	 * 云台类型
+	 */
+	private int PTZType;
+
+	/**
+	 * 云台类型描述字符串
+	 */
+	private String PTZTypeText;
+
+	/**
+	 * 创建时间
+	 */
+	private String createTime;
+
+	/**
+	 * 更新时间
+	 */
+	private String updateTime;
+	
+	/**
+	 * 在线/离线
+	 * 1在线,0离线
+	 * 默认在线
+	 * 信令:
+	 * <Status>ON</Status>
+	 * <Status>OFF</Status>
+	 * 遇到过NVR下的IPC下发信令可以推流, 但是 Status 响应 OFF
+	 */
+	private int status;
+
+	/**
+	 * 经度
+	 */
+	private double longitude;
+	
+	/**
+	 * 纬度
+	 */
+	private double latitude;
+
+	/**
+	 * 子设备数
+	 */
+	private int subCount;
+
+	/**
+	 * 流唯一编号,存在表示正在直播
+	 */
+	private String  streamId;
+
+	/**
+	 *  是否含有音频
+	 */
+	private boolean hasAudio;
+
+	public String getDeviceId() {
+		return deviceId;
+	}
+
+	public void setDeviceId(String deviceId) {
+		this.deviceId = deviceId;
+	}
+
+	public void setPTZType(int PTZType) {
+		this.PTZType = PTZType;
+		switch (PTZType) {
+			case 0:
+				this.PTZTypeText = "未知";
+				break;
+			case 1:
+				this.PTZTypeText = "球机";
+				break;
+			case 2:
+				this.PTZTypeText = "半球";
+				break;
+			case 3:
+				this.PTZTypeText = "固定枪机";
+				break;
+			case 4:
+				this.PTZTypeText = "遥控枪机";
+				break;
+		}
+	}
+}

+ 22 - 0
jetlinks-manager/media-manager/src/main/java/org/jetlinks/community/media/entity/GbStream.java

@@ -0,0 +1,22 @@
+package org.jetlinks.community.media.entity;
+
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+/**
+ * 直播流关联国标上级平台
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class GbStream extends PlatformGbStream{
+
+    private String app;
+    private String stream;
+    private String gbId;
+    private String name;
+    private String mediaServerId;
+    private double longitude;
+    private double latitude;
+    private String streamType;
+    private boolean status;
+}

+ 211 - 0
jetlinks-manager/media-manager/src/main/java/org/jetlinks/community/media/entity/MediaDevice.java

@@ -0,0 +1,211 @@
+package org.jetlinks.community.media.entity;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import org.hswebframework.ezorm.rdb.mapping.annotation.ColumnType;
+import org.hswebframework.ezorm.rdb.mapping.annotation.DefaultValue;
+import org.hswebframework.ezorm.rdb.mapping.annotation.EnumCodec;
+import org.hswebframework.web.api.crud.entity.GenericEntity;
+import org.hswebframework.web.api.crud.entity.RecordCreationEntity;
+import org.jetlinks.community.media.enums.DeviceState;
+
+import javax.persistence.Column;
+import javax.persistence.Index;
+import javax.persistence.Table;
+
+@Data
+@Table(name = "media_device", schema = "媒体设备")
+public class MediaDevice extends GenericEntity<String> implements RecordCreationEntity {
+
+    @Column(name = "product_id")
+    @Schema(
+        description = "产品id"
+    )
+	private String productId;
+	/**
+	 * 设备名
+	 */
+    @Column(name = "name")
+    @Schema(
+        description = "设备名"
+    )
+	private String name;
+	
+	/**
+	 * 生产厂商
+	 */
+    @Column(name = "manufacturer")
+    @Schema(
+        description = "生产厂商"
+    )
+	private String manufacturer;
+	
+	/**
+	 * 型号
+	 */
+    @Column(name = "model")
+    @Schema(
+        description = "型号"
+    )
+	private String model;
+	
+	/**
+	 * 固件版本
+	 */
+    @Column(name = "firmware")
+    @Schema(
+        description = "固件版本"
+    )
+	private String firmware;
+
+	/**
+	 * 传输协议
+	 * UDP/TCP
+	 */
+    @Column(name = "transport")
+    @Schema(
+        description = "传输协议"
+    )
+	private String transport;
+
+	/**
+	 * 数据流传输模式
+	 * UDP:udp传输
+     *      服务端监听UDP端口,通过INVITE信令告知设备端口,设备主动向服务端发起流传输
+	 * TCP-ACTIVE:tcp主动模式
+     *      设备端告知服务端监听的TCP端口情况,服务端主动向设备拉流,此种场景较少,且设备所在网络可以被服务所在网络访问(如下级设备与上级GB28181服务在同一个局域网,或者都在公网上能相互访问)。
+	 * TCP-PASSIVE:tcp被动模式
+     *      服务端监听TCP端口,通过INVITE信令告知设备端口,设备向服务端发起流传输
+	 */
+    @Column(name = "stream_mode")
+    @Schema(
+        description = "数据流传输模式"
+    )
+	private String streamMode;
+
+	/**
+	 * wan地址_ip
+	 */
+    @Column(name = "ip", updatable = false)
+    @Schema(
+        description = "地址"
+        ,accessMode = Schema.AccessMode.READ_ONLY
+    )
+	private String  ip;
+
+	/**
+	 * wan地址_port
+	 */
+    @Column(name = "port", updatable = false)
+    @Schema(
+        description = "端口号"
+        ,accessMode = Schema.AccessMode.READ_ONLY
+    )
+	private int port;
+
+	/**
+	 * wan地址
+	 */
+    @Column(name = "host_address", updatable = false)
+    @Schema(
+        description = "地址"
+        ,accessMode = Schema.AccessMode.READ_ONLY
+    )
+	private String  hostAddress;
+	
+	/**
+	 * 在线
+	 */
+    @Column(name = "state",length = 16)
+    @EnumCodec
+    @ColumnType(javaType = String.class)
+    @DefaultValue("offline")
+    @Schema(
+        description = "状态(只读)"
+        ,accessMode = Schema.AccessMode.READ_ONLY
+        , defaultValue = "offline"
+    )
+	private DeviceState state;
+
+
+	/**
+	 * 注册时间
+	 */
+    @Column(name = "register_time", updatable = false)
+    @Schema(
+        description = "注册时间"
+        ,accessMode = Schema.AccessMode.READ_ONLY
+    )
+	private String registerTime;
+
+
+	/**
+	 * 心跳时间
+	 */
+    @Column(name = "keepalive_time", updatable = false)
+    @Schema(
+        description = "心跳时间"
+        ,accessMode = Schema.AccessMode.READ_ONLY
+    )
+	private String keepaliveTime;
+
+	/**
+	 * 通道个数
+	 */
+    @Column(name = "channel_count", updatable = false)
+    @Schema(
+        description = "通道个数"
+        ,accessMode = Schema.AccessMode.READ_ONLY
+    )
+	private int channelCount;
+
+	/**
+	 * 注册有效期
+	 */
+    @Column(name = "expires", updatable = false)
+    @Schema(
+        description = "注册有效期"
+        ,accessMode = Schema.AccessMode.READ_ONLY
+    )
+	private int expires;
+
+	/**
+	 * 设备使用的媒体id, 默认为null
+	 */
+    @Column(name = "media_server_id")
+    @Schema(
+        description = "注册有效期"
+        ,accessMode = Schema.AccessMode.READ_ONLY
+    )
+	private String mediaServerId;
+
+	/**
+	 * 首次注册
+	 */
+    @Column(insertable = false)
+	private boolean firsRegister;
+
+	/**
+	 * 字符集, 支持 utf-8 与 gb2312
+	 */
+    @Column(name = "charset")
+    @Schema(
+        description = "字符集"
+    )
+	private String charset ;
+
+
+	/**
+	 * 目录订阅周期,0为不订阅
+	 */
+    @Column(name = "subscribe_cycle_for_catalog")
+    @Schema(
+        description = "目录订阅周期"
+    )
+	private int subscribeCycleForCatalog ;
+
+	private String creatorId;
+
+
+	private Long createTime;
+}

+ 293 - 0
jetlinks-manager/media-manager/src/main/java/org/jetlinks/community/media/entity/ParentPlatform.java

@@ -0,0 +1,293 @@
+package org.jetlinks.community.media.entity;
+
+public class ParentPlatform {
+
+    /**
+     * id
+     */
+    private Integer id;
+
+    /**
+     * 是否启用
+     */
+    private boolean enable;
+
+    /**
+     * 名称
+     */
+    private String name;
+
+    /**
+     * SIP服务国标编码
+     */
+    private String serverGBId;
+
+    /**
+     * SIP服务国标域
+     */
+    private String serverGBDomain;
+
+    /**
+     * SIP服务IP
+     */
+    private String serverIP;
+
+    /**
+     * SIP服务端口
+     */
+    private int serverPort;
+
+    /**
+     * 设备国标编号
+     */
+    private String deviceGBId;
+
+    /**
+     * 设备ip
+     */
+    private String deviceIp;
+
+    /**
+     * 设备端口
+     */
+    private String devicePort;
+
+    /**
+     * SIP认证用户名(默认使用设备国标编号)
+     */
+    private String username;
+
+    /**
+     * SIP认证密码
+     */
+    private String password;
+
+    /**
+     * 注册周期 (秒)
+     */
+    private String expires;
+
+    /**
+     * 心跳周期(秒)
+     */
+    private String keepTimeout;
+
+    /**
+     * 传输协议
+     * UDP/TCP
+     */
+    private String transport;
+
+    /**
+     * 字符集
+     */
+    private String characterSet;
+
+    /**
+     * 允许云台控制
+     */
+    private boolean ptz;
+
+    /**
+     * RTCP流保活
+     * TODO 预留, 暂不实现
+     */
+    private boolean rtcp;
+
+    /**
+     * 在线状态
+     */
+    private boolean status;
+
+    /**
+     * 在线状态
+     */
+    private int channelCount;
+
+    /**
+     * 共享所有的直播流
+     */
+    private boolean shareAllLiveStream;
+
+    /**
+     * 默认目录Id,自动添加的通道多放在这个目录下
+     */
+    private String catalogId;
+
+    public Integer getId() {
+        return id;
+    }
+
+    public void setId(Integer id) {
+        this.id = id;
+    }
+
+    public boolean isEnable() {
+        return enable;
+    }
+
+    public void setEnable(boolean enable) {
+        this.enable = enable;
+    }
+
+    public String getName() {
+        return name;
+    }
+
+    public void setName(String name) {
+        this.name = name;
+    }
+
+    public String getServerGBId() {
+        return serverGBId;
+    }
+
+    public void setServerGBId(String serverGBId) {
+        this.serverGBId = serverGBId;
+    }
+
+    public String getServerGBDomain() {
+        return serverGBDomain;
+    }
+
+    public void setServerGBDomain(String serverGBDomain) {
+        this.serverGBDomain = serverGBDomain;
+    }
+
+    public String getServerIP() {
+        return serverIP;
+    }
+
+    public void setServerIP(String serverIP) {
+        this.serverIP = serverIP;
+    }
+
+    public int getServerPort() {
+        return serverPort;
+    }
+
+    public void setServerPort(int serverPort) {
+        this.serverPort = serverPort;
+    }
+
+    public String getDeviceGBId() {
+        return deviceGBId;
+    }
+
+    public void setDeviceGBId(String deviceGBId) {
+        this.deviceGBId = deviceGBId;
+    }
+
+    public String getDeviceIp() {
+        return deviceIp;
+    }
+
+    public void setDeviceIp(String deviceIp) {
+        this.deviceIp = deviceIp;
+    }
+
+    public String getDevicePort() {
+        return devicePort;
+    }
+
+    public void setDevicePort(String devicePort) {
+        this.devicePort = devicePort;
+    }
+
+    public String getUsername() {
+        return username;
+    }
+
+    public void setUsername(String username) {
+        this.username = username;
+    }
+
+    public String getPassword() {
+        return password;
+    }
+
+    public void setPassword(String password) {
+        this.password = password;
+    }
+
+    public String getExpires() {
+        return expires;
+    }
+
+    public void setExpires(String expires) {
+        this.expires = expires;
+    }
+
+    public String getKeepTimeout() {
+        return keepTimeout;
+    }
+
+    public void setKeepTimeout(String keepTimeout) {
+        this.keepTimeout = keepTimeout;
+    }
+
+    public String getTransport() {
+        return transport;
+    }
+
+    public void setTransport(String transport) {
+        this.transport = transport;
+    }
+
+    public String getCharacterSet() {
+        return characterSet;
+    }
+
+    public void setCharacterSet(String characterSet) {
+        this.characterSet = characterSet;
+    }
+
+    public boolean isPtz() {
+        return ptz;
+    }
+
+    public void setPtz(boolean ptz) {
+        this.ptz = ptz;
+    }
+
+    public boolean isRtcp() {
+        return rtcp;
+    }
+
+    public void setRtcp(boolean rtcp) {
+        this.rtcp = rtcp;
+    }
+
+    public boolean isStatus() {
+        return status;
+    }
+
+    public void setStatus(boolean status) {
+        this.status = status;
+    }
+
+    public int getChannelCount() {
+        return channelCount;
+    }
+
+    public void setChannelCount(int channelCount) {
+        this.channelCount = channelCount;
+    }
+
+
+    public boolean isShareAllLiveStream() {
+        return shareAllLiveStream;
+    }
+
+    public void setShareAllLiveStream(boolean shareAllLiveStream) {
+        this.shareAllLiveStream = shareAllLiveStream;
+    }
+
+    public String getCatalogId() {
+        return catalogId;
+    }
+
+    public void setCatalogId(String catalogId) {
+        this.catalogId = catalogId;
+    }
+}

+ 21 - 0
jetlinks-manager/media-manager/src/main/java/org/jetlinks/community/media/entity/PlatformCatalog.java

@@ -0,0 +1,21 @@
+package org.jetlinks.community.media.entity;
+
+import lombok.Data;
+
+@Data
+public class PlatformCatalog {
+    private String id;
+    private String name;
+    private String platformId;
+    private String parentId;
+    /**
+     * 子节点数
+     */
+    private int childrenCount;
+    /**
+     * 0、 目录,
+     * 1、 国标通道
+     * 2、 直播流
+     */
+    private int type;
+}

+ 14 - 0
jetlinks-manager/media-manager/src/main/java/org/jetlinks/community/media/entity/PlatformGbStream.java

@@ -0,0 +1,14 @@
+package org.jetlinks.community.media.entity;
+
+import lombok.Data;
+
+/**
+ * 平台直播流
+ */
+@Data
+public class PlatformGbStream {
+    private String app;
+    private String stream;
+    private String platformId;
+    private String catalogId;
+}

+ 49 - 0
jetlinks-manager/media-manager/src/main/java/org/jetlinks/community/media/entity/SipServerConfig.java

@@ -0,0 +1,49 @@
+package org.jetlinks.community.media.entity;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import org.hswebframework.ezorm.rdb.mapping.annotation.Comment;
+import org.hswebframework.web.exception.BusinessException;
+import java.util.*;
+/**
+ * @author lifang
+ * @version 1.0.0
+ * @ClassName SipServerConfig.java
+ * @Description sip服务器配置
+ * @createTime 2022年01月15日 10:52:00
+ */
+@Data
+@EqualsAndHashCode(callSuper = false)
+@AllArgsConstructor(staticName = "of")
+public class SipServerConfig {
+
+    private String id;
+
+    @Comment("sip服务地址")
+    private String host;
+
+    @Comment("sip服务端口")
+    private Integer port;
+
+    @Comment("通信协议,tcp、udp")
+    private String transport;
+
+    private String domain;
+
+    private String charset;
+
+    private String password="12345678";
+
+    /**
+     * 心跳时间
+     */
+    private Long timeout;
+
+    @Comment("产品id")
+    private String productId;
+    public void validateCreate(){
+        if(!"tcp".equals(transport.toLowerCase())&&!"udp".equals(transport.toLowerCase())){
+            throw new BusinessException("不支持该协议[{"+transport+"}],请从tcp、udp中选择");
+        }
+    }
+}

+ 149 - 0
jetlinks-manager/media-manager/src/main/java/org/jetlinks/community/media/entity/WvpSipDate.java

@@ -0,0 +1,149 @@
+package org.jetlinks.community.media.entity;
+
+import gov.nist.core.InternalErrorHandler;
+import gov.nist.javax.sip.header.SIPDate;
+
+import java.util.*;
+
+/**
+ * 重写jain sip的SIPDate解决与国标时间格式不一致的问题
+ */
+public class WvpSipDate extends SIPDate {
+
+    /**
+     *
+     */
+    private static final long serialVersionUID = 1L;
+    
+    private Calendar javaCal;
+
+    public WvpSipDate(long timeMillis) {
+        this.javaCal = new GregorianCalendar(TimeZone.getDefault(), Locale.getDefault());
+        Date date = new Date(timeMillis);
+        this.javaCal.setTime(date);
+        this.wkday = this.javaCal.get(7);
+        switch(this.wkday) {
+            case 1:
+                this.sipWkDay = "Sun";
+                break;
+            case 2:
+                this.sipWkDay = "Mon";
+                break;
+            case 3:
+                this.sipWkDay = "Tue";
+                break;
+            case 4:
+                this.sipWkDay = "Wed";
+                break;
+            case 5:
+                this.sipWkDay = "Thu";
+                break;
+            case 6:
+                this.sipWkDay = "Fri";
+                break;
+            case 7:
+                this.sipWkDay = "Sat";
+                break;
+            default:
+                InternalErrorHandler.handleException("No date map for wkday " + this.wkday);
+        }
+
+        this.day = this.javaCal.get(5);
+        this.month = this.javaCal.get(2);
+        switch(this.month) {
+            case 0:
+                this.sipMonth = "Jan";
+                break;
+            case 1:
+                this.sipMonth = "Feb";
+                break;
+            case 2:
+                this.sipMonth = "Mar";
+                break;
+            case 3:
+                this.sipMonth = "Apr";
+                break;
+            case 4:
+                this.sipMonth = "May";
+                break;
+            case 5:
+                this.sipMonth = "Jun";
+                break;
+            case 6:
+                this.sipMonth = "Jul";
+                break;
+            case 7:
+                this.sipMonth = "Aug";
+                break;
+            case 8:
+                this.sipMonth = "Sep";
+                break;
+            case 9:
+                this.sipMonth = "Oct";
+                break;
+            case 10:
+                this.sipMonth = "Nov";
+                break;
+            case 11:
+                this.sipMonth = "Dec";
+                break;
+            default:
+                InternalErrorHandler.handleException("No date map for month " + this.month);
+        }
+
+        this.year = this.javaCal.get(1);
+        this.hour = this.javaCal.get(11);
+        this.minute = this.javaCal.get(12);
+        this.second = this.javaCal.get(13);
+    }
+
+    @Override
+    public StringBuilder encode(StringBuilder var1) {
+        String var2;
+        if (this.month < 9) {
+            var2 = "0" + (this.month + 1);
+        } else {
+            var2 = "" + (this.month + 1);
+        }
+
+        String var3;
+        if (this.day < 10) {
+            var3 = "0" + this.day;
+        } else {
+            var3 = "" + this.day;
+        }
+
+        String var4;
+        if (this.hour < 10) {
+            var4 = "0" + this.hour;
+        } else {
+            var4 = "" + this.hour;
+        }
+
+        String var5;
+        if (this.minute < 10) {
+            var5 = "0" + this.minute;
+        } else {
+            var5 = "" + this.minute;
+        }
+
+        String var6;
+        if (this.second < 10) {
+            var6 = "0" + this.second;
+        } else {
+            var6 = "" + this.second;
+        }
+
+        int var8 = this.javaCal.get(14);
+        String var7;
+        if (var8 < 10) {
+            var7 = "00" + var8;
+        } else if (var8 < 100) {
+            var7 = "0" + var8;
+        } else {
+            var7 = "" + var8;
+        }
+
+        return var1.append(this.year).append("-").append(var2).append("-").append(var3).append("T").append(var4).append(":").append(var5).append(":").append(var6).append(".").append(var7);
+    }
+}

+ 30 - 0
jetlinks-manager/media-manager/src/main/java/org/jetlinks/community/media/enums/DeviceState.java

@@ -0,0 +1,30 @@
+package org.jetlinks.community.media.enums;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import org.hswebframework.web.dict.Dict;
+import org.hswebframework.web.dict.EnumDict;
+
+@AllArgsConstructor
+@Getter
+@Dict("media-device-state")
+public enum DeviceState implements EnumDict<String> {
+    offline("离线"),
+    online("在线");
+
+    private String text;
+
+    @Override
+    public String getValue() {
+        return name();
+    }
+
+    public static DeviceState of(byte state) {
+        switch (state) {
+            case org.jetlinks.core.device.DeviceState.offline:
+                return offline;
+            default:
+                return online;
+        }
+    }
+}

+ 11 - 0
jetlinks-manager/media-manager/src/main/java/org/jetlinks/community/media/enums/SipMethodType.java

@@ -0,0 +1,11 @@
+package org.jetlinks.community.media.enums;
+
+/**
+ * @author lifang
+ * @version 1.0.0
+ * @ClassName SipMethodType.java
+ * @Description TODO
+ * @createTime 2022年01月15日 14:49:00
+ */
+public enum  SipMethodType {
+}

+ 15 - 0
jetlinks-manager/media-manager/src/main/java/org/jetlinks/community/media/enums/StreamMode.java

@@ -0,0 +1,15 @@
+package org.jetlinks.community.media.enums;
+
+/**
+ * @author lifang
+ * @version 1.0.0
+ * @ClassName StreamMode.java
+ * @Description TODO
+ * @createTime 2022年01月22日 14:42:00
+ */
+public enum  StreamMode {
+    TCP_PASSIVE,
+    TCP_ACTIVE,
+    UDP;
+
+}

+ 24 - 0
jetlinks-manager/media-manager/src/main/java/org/jetlinks/community/media/event/request/IMessageHandler.java

@@ -0,0 +1,24 @@
+package org.jetlinks.community.media.event.request;
+
+
+import org.dom4j.Element;
+import org.jetlinks.community.media.entity.MediaDevice;
+import org.jetlinks.community.media.entity.ParentPlatform;
+
+import javax.sip.RequestEvent;
+
+public interface IMessageHandler {
+    /**
+     * 处理来自设备的信息
+     * @param evt
+     * @param device
+     */
+    void handForDevice(RequestEvent evt, MediaDevice device, Element element);
+
+    /**
+     * 处理来自平台的信息
+     * @param evt
+     * @param parentPlatform
+     */
+    void handForPlatform(RequestEvent evt, ParentPlatform parentPlatform, Element element);
+}

+ 79 - 0
jetlinks-manager/media-manager/src/main/java/org/jetlinks/community/media/event/request/ISipRequestProcessor.java

@@ -0,0 +1,79 @@
+package org.jetlinks.community.media.event.request;
+
+import lombok.extern.slf4j.Slf4j;
+import org.jetlinks.community.media.sip.processor.SipProcessorObserver;
+import org.springframework.beans.factory.annotation.Autowired;
+import javax.annotation.PostConstruct;
+import javax.sip.*;
+
+/**
+ * @description: 对SIP事件进行处理,包括request, response, timeout, ioException, transactionTerminated,dialogTerminated
+ * @author: panlinlin
+ * @date:   2021年11月5日 15:47
+ */
+@Slf4j
+public abstract class ISipRequestProcessor {
+
+    @Autowired
+    public SipProcessorObserver observer;
+
+    @PostConstruct
+    public void init(){
+        observer.handleDialogTerminatedEvent()
+            .doOnNext(this::processDialogTerminatedEvent)
+            .onErrorContinue((k,v)->{
+            })
+            .subscribe();
+        observer.handleIOExceptionEvent()
+            .doOnNext(this::processIoException)
+            .onErrorContinue((k,v)->{})
+            .subscribe();
+        observer.handleRequestEvent()
+            .filter(requestEvent -> requestEvent.getRequest().getMethod().equals(this.getMethod()))
+            .doOnNext(this::processRequest)
+            .onErrorContinue((k,v)->{
+                log.error("sip 处理请求失败,cause:[{}],paramters[{}]",k.getStackTrace(),v);
+            })
+            .subscribe();
+        observer.handleResponseEvent()
+            .doOnNext(this::processResponse)
+            .onErrorContinue((k,v)->{})
+            .subscribe();
+        observer.handleTimeoutEvent()
+            .doOnNext(this::processTimeout)
+            .onErrorContinue((k,v)->{})
+            .subscribe();
+        observer.handleTransactionTerminatedEvent()
+            .doOnNext(this::processTransactionTerminatedEvent)
+            .onErrorContinue((k,v)->{})
+            .subscribe();
+    }
+
+    public void processRequest(RequestEvent event){
+
+    }
+
+    public void processIoException(IOExceptionEvent event){
+
+    }
+
+    public void processResponse(ResponseEvent event){
+
+    }
+
+    public void processTimeout(TimeoutEvent event){
+
+    }
+
+    public void processTransactionTerminatedEvent(TransactionTerminatedEvent event){
+
+    }
+
+
+    public void processDialogTerminatedEvent(DialogTerminatedEvent event){
+
+    }
+
+    public abstract String getMethod();
+
+}

+ 417 - 0
jetlinks-manager/media-manager/src/main/java/org/jetlinks/community/media/event/request/impl/InviteRequestProcessor.java

@@ -0,0 +1,417 @@
+package org.jetlinks.community.media.event.request.impl;
+
+import gov.nist.javax.sip.address.AddressImpl;
+import gov.nist.javax.sip.address.SipUri;
+import lombok.SneakyThrows;
+import lombok.extern.slf4j.Slf4j;
+import org.jetlinks.community.media.sip.SipRequestProcessorParent;
+import org.jetlinks.community.media.storage.IVideoManagerStorager;
+import org.springframework.stereotype.Component;
+import javax.sip.RequestEvent;
+import javax.sip.address.SipURI;
+import javax.sip.header.FromHeader;
+import javax.sip.message.Request;
+import javax.sip.message.Response;
+
+/**
+ * SIP命令类型: INVITE请求
+ */
+@SuppressWarnings("rawtypes")
+@Component
+@Slf4j(topic = "sip-invite-request")
+public class InviteRequestProcessor extends SipRequestProcessorParent {
+
+    //	@Autowired
+//	private SIPCommanderFroPlatform cmderFroPlatform;
+//
+//	@Autowired
+    private IVideoManagerStorager storager;
+//
+//	@Autowired
+//	private IRedisCatchStorage  redisCatchStorage;
+//
+//	@Autowired
+//	private SIPCommander cmder;
+//
+//	@Autowired
+//	private IPlayService playService;
+//
+//	@Autowired
+//	private ZLMRTPServerFactory zlmrtpServerFactory;
+//
+//	@Autowired
+//	private IMediaServerService mediaServerService;
+
+    @Override
+    public String getMethod() {
+        return Request.INVITE;
+    }
+    /**
+     * 处理invite请求
+     *
+     * @param evt
+     *            请求消息
+     */
+
+    @Override
+    public void processRequest(RequestEvent evt) {
+//		try {
+        Request request = evt.getRequest();
+        SipURI sipUrl = (SipURI) request.getRequestURI();
+        String channelId = sipUrl.getUser();
+        String requesterId = null;
+
+        FromHeader fromHeader = (FromHeader)request.getHeader(FromHeader.NAME);
+        AddressImpl address = (AddressImpl) fromHeader.getAddress();
+        SipUri uri = (SipUri) address.getURI();
+        requesterId = uri.getUser();
+
+        try {
+            if (requesterId == null || channelId == null) {
+                log.info("无法从FromHeader的Address中获取到平台id,返回400");
+                // 参数不全, 发400,请求错误
+                responseAck(evt, Response.BAD_REQUEST);
+                return;
+            }
+            responseAck(evt, Response.CALL_IS_BEING_FORWARDED);
+        }catch (Exception e){
+            e.printStackTrace();
+        }
+
+//        // 查询请求方是否上级平台
+//        ParentPlatform platform = storager.queryParentPlatByServerGBId(requesterId);
+//        if(platform!=null){
+//            //平台下是否存在该通道 todo
+//            DeviceChannel channel=null;
+//            //平台下是否存在直播流 todo
+//            List<GbStream> gbStreams =null;
+//
+//            GbStream gbStream = CollectionUtil.isNotEmpty(gbStreams)? gbStreams.get(0):null;
+//
+//            MediaServerItem mediaServerItem = null;
+//            if(channel!=null&&gbStream==null){
+//                //通道存在但是通道离线了
+//                if(channel.getStatus()==0){
+//                    log.info("gb28181 通道离线,返回400");
+//                    responseAck(evt, Response.BAD_REQUEST, "channel [" + channel.getChannelId() + "] offline");
+//                    return ;
+//                }
+//                // 通道存在,发181,呼叫转接中
+//                responseAck(evt, Response.CALL_IS_BEING_FORWARDED);
+//            }else if(channel==null&&gbStream!=null){
+//                //直播流存在
+//                String mediaServerId = gbStream.getMediaServerId();
+//                //todo 获取直播流媒体服务器配置
+//                if(mediaServerItem==null){
+//                    log.info("[ app={}, stream={} ]找不到zlm {},返回410",gbStream.getApp(), gbStream.getStream(), mediaServerId);
+//                    responseAck(evt, Response.GONE, "media server not found");
+//                    return;
+//                }
+//                //查询待转推的流是否就绪
+//                Boolean streamReady=null;
+//                if(!Boolean.TRUE.equals(streamReady)){
+//                    log.info("[ app={}, stream={} ]通道离线,返回400",gbStream.getApp(), gbStream.getStream());
+//                    responseAck(evt, Response.BAD_REQUEST, "channel [" + gbStream.getGbId() + "] offline");
+//                    return;
+//                }
+//                // 通道存在,发181,呼叫转接中
+//                responseAck(evt, Response.CALL_IS_BEING_FORWARDED);
+//            }
+//        }
+//			if (platform != null) {
+//				// 查询平台下是否有该通道
+//				DeviceChannel channel = storager.queryChannelInParentPlatform(requesterId, channelId);
+//				List<GbStream> gbStreams = storager.queryStreamInParentPlatform(requesterId, channelId);
+//				PlatformCatalog catalog = storager.getCatalog(channelId);
+//				GbStream gbStream = gbStreams.size() > 0? gbStreams.get(0):null;
+//				MediaServerItem mediaServerItem = null;
+//				// 不是通道可能是直播流
+//				if (channel != null && gbStream == null ) {
+//					if (channel.getStatus() == 0) {
+//						log.info("通道离线,返回400");
+//						responseAck(evt, Response.BAD_REQUEST, "channel [" + channel.getChannelId() + "] offline");
+//						return;
+//					}
+//					responseAck(evt, Response.CALL_IS_BEING_FORWARDED); // 通道存在,发181,呼叫转接中
+//				}else if(channel == null && gbStream != null){
+//					String mediaServerId = gbStream.getMediaServerId();
+//					mediaServerItem = mediaServerService.getOne(mediaServerId);
+//					if (mediaServerItem == null) {
+//						log.info("[ app={}, stream={} ]找不到zlm {},返回410",gbStream.getApp(), gbStream.getStream(), mediaServerId);
+//						responseAck(evt, Response.GONE, "media server not found");
+//						return;
+//					}
+//					Boolean streamReady = zlmrtpServerFactory.isStreamReady(mediaServerItem, gbStream.getApp(), gbStream.getStream());
+//					if (!streamReady ) {
+//						log.info("[ app={}, stream={} ]通道离线,返回400",gbStream.getApp(), gbStream.getStream());
+//						responseAck(evt, Response.BAD_REQUEST, "channel [" + gbStream.getGbId() + "] offline");
+//						return;
+//					}
+//					responseAck(evt, Response.CALL_IS_BEING_FORWARDED); // 通道存在,发181,呼叫转接中
+//				}else if (catalog != null) {
+//					responseAck(evt, Response.BAD_REQUEST, "catalog channel can not play"); // 目录不支持点播
+//					return;
+//				} else {
+//					log.info("通道不存在,返回404");
+//					responseAck(evt, Response.NOT_FOUND); // 通道不存在,发404,资源不存在
+//					return;
+//				}
+//				// 解析sdp消息, 使用jainsip 自带的sdp解析方式
+//				String contentString = new String(request.getRawContent());
+//
+//				// jainSip不支持y=字段, 移除以解析。
+//				int ssrcIndex = contentString.indexOf("y=");
+//				// 检查是否有y字段
+//				String ssrcDefault = "0000000000";
+//				String ssrc;
+//				SessionDescription sdp;
+//				if (ssrcIndex >= 0) {
+//					//ssrc规定长度为10字节,不取余下长度以避免后续还有“f=”字段
+//					ssrc = contentString.substring(ssrcIndex + 2, ssrcIndex + 12);
+//					String substring = contentString.substring(0, contentString.indexOf("y="));
+//					sdp = SdpFactory.getInstance().createSessionDescription(substring);
+//				}else {
+//					ssrc = ssrcDefault;
+//					sdp = SdpFactory.getInstance().createSessionDescription(contentString);
+//				}
+//
+//				//  获取支持的格式
+//				Vector mediaDescriptions = sdp.getMediaDescriptions(true);
+//				// 查看是否支持PS 负载96
+//				//String ip = null;
+//				int port = -1;
+//				//boolean recvonly = false;
+//				boolean mediaTransmissionTCP = false;
+//				Boolean tcpActive = null;
+//				for (Object description : mediaDescriptions) {
+//					MediaDescription mediaDescription = (MediaDescription) description;
+//					Media media = mediaDescription.getMedia();
+//
+//					Vector mediaFormats = media.getMediaFormats(false);
+//					if (mediaFormats.contains("96")) {
+//						port = media.getMediaPort();
+//						//String mediaType = media.getMediaType();
+//						String protocol = media.getProtocol();
+//
+//						// 区分TCP发流还是udp, 当前默认udp
+//						if ("TCP/RTP/AVP".equals(protocol)) {
+//							String setup = mediaDescription.getAttribute("setup");
+//							if (setup != null) {
+//								mediaTransmissionTCP = true;
+//								if ("active".equals(setup)) {
+//									tcpActive = true;
+//								} else if ("passive".equals(setup)) {
+//									tcpActive = false;
+//								}
+//							}
+//						}
+//						break;
+//					}
+//				}
+//				if (port == -1) {
+//					log.info("不支持的媒体格式,返回415");
+//					// 回复不支持的格式
+//					responseAck(evt, Response.UNSUPPORTED_MEDIA_TYPE); // 不支持的格式,发415
+//					return;
+//				}
+//				String username = sdp.getOrigin().getUsername();
+//				String addressStr = sdp.getOrigin().getAddress();
+//				//String sessionName = sdp.getSessionName().getValue();
+//				log.info("[上级点播]用户:{}, 地址:{}:{}, ssrc:{}", username, addressStr, port, ssrc);
+//				Device device  = null;
+//				// 通过 channel 和 gbStream 是否为null 值判断来源是直播流合适国标
+//				if (channel != null) {
+//					device = storager.queryVideoDeviceByPlatformIdAndChannelId(requesterId, channelId);
+//					if (device == null) {
+//						log.warn("点播平台{}的通道{}时未找到设备信息", requesterId, channel);
+//						responseAck(evt, Response.SERVER_INTERNAL_ERROR);
+//						return;
+//					}
+//					mediaServerItem = playService.getNewMediaServerItem(device);
+//					if (mediaServerItem == null) {
+//						log.warn("未找到可用的zlm");
+//						responseAck(evt, Response.BUSY_HERE);
+//						return;
+//					}
+//					SendRtpItem sendRtpItem = zlmrtpServerFactory.createSendRtpItem(mediaServerItem, addressStr, port, ssrc, requesterId,
+//							device.getId(), channelId,
+//							mediaTransmissionTCP);
+//					if (tcpActive != null) {
+//						sendRtpItem.setTcpActive(tcpActive);
+//					}
+//					if (sendRtpItem == null) {
+//						log.warn("服务器端口资源不足");
+//						responseAck(evt, Response.BUSY_HERE);
+//						return;
+//					}
+//
+//					// 写入redis, 超时时回复
+//					redisCatchStorage.updateSendRTPSever(sendRtpItem);
+//					// 通知下级推流,
+//					PlayResult playResult = playService.play(mediaServerItem,device.getId(), channelId, (mediaServerItemInUSe, responseJSON)->{
+//						// 收到推流, 回复200OK, 等待ack
+//						// if (sendRtpItem == null) return;
+//						sendRtpItem.setStatus(1);
+//						redisCatchStorage.updateSendRTPSever(sendRtpItem);
+//						// TODO 添加对tcp的支持
+//
+//						StringBuffer content = new StringBuffer(200);
+//						content.append("v=0\r\n");
+//						content.append("o="+ channelId +" 0 0 IN IP4 "+mediaServerItemInUSe.getSdpIp()+"\r\n");
+//						content.append("s=Play\r\n");
+//						content.append("c=IN IP4 "+mediaServerItemInUSe.getSdpIp()+"\r\n");
+//						content.append("t=0 0\r\n");
+//						content.append("m=video "+ sendRtpItem.getLocalPort()+" RTP/AVP 96\r\n");
+//						content.append("a=sendonly\r\n");
+//						content.append("a=rtpmap:96 PS/90000\r\n");
+//						content.append("y="+ ssrc + "\r\n");
+//						content.append("f=\r\n");
+//
+//						try {
+//							responseSdpAck(evt, content.toString());
+//						} catch (SipException e) {
+//							e.printStackTrace();
+//						} catch (InvalidArgumentException e) {
+//							e.printStackTrace();
+//						} catch (ParseException e) {
+//							e.printStackTrace();
+//						}
+//					} ,((event) -> {
+//						// 未知错误。直接转发设备点播的错误
+//						Response response = null;
+//						try {
+//							response = getMessageFactory().createResponse(event.statusCode, evt.getRequest());
+//							ServerTransaction serverTransaction = getServerTransaction(evt);
+//							serverTransaction.sendResponse(response);
+//							if (serverTransaction.getDialog() != null) serverTransaction.getDialog().delete();
+//						} catch (ParseException | SipException | InvalidArgumentException e) {
+//							e.printStackTrace();
+//						}
+//					}));
+//					if (log.isDebugEnabled()) {
+//						log.debug(playResult.getResult().toString());
+//					}
+//
+//				}else if (gbStream != null) {
+//					SendRtpItem sendRtpItem = zlmrtpServerFactory.createSendRtpItem(mediaServerItem, addressStr, port, ssrc, requesterId,
+//							gbStream.getApp(), gbStream.getStream(), channelId,
+//							mediaTransmissionTCP);
+//
+//					if (tcpActive != null) {
+//						sendRtpItem.setTcpActive(tcpActive);
+//					}
+//					if (sendRtpItem == null) {
+//						log.warn("服务器端口资源不足");
+//						responseAck(evt, Response.BUSY_HERE);
+//						return;
+//					}
+//
+//					// 写入redis, 超时时回复
+//					redisCatchStorage.updateSendRTPSever(sendRtpItem);
+//
+//					sendRtpItem.setStatus(1);
+//					redisCatchStorage.updateSendRTPSever(sendRtpItem);
+//					// TODO 添加对tcp的支持
+//					StringBuffer content = new StringBuffer(200);
+//					content.append("v=0\r\n");
+//					content.append("o="+ channelId +" 0 0 IN IP4 "+mediaServerItem.getSdpIp()+"\r\n");
+//					content.append("s=Play\r\n");
+//					content.append("c=IN IP4 "+mediaServerItem.getSdpIp()+"\r\n");
+//					content.append("t=0 0\r\n");
+//					content.append("m=video "+ sendRtpItem.getLocalPort()+" RTP/AVP 96\r\n");
+//					content.append("a=sendonly\r\n");
+//					content.append("a=rtpmap:96 PS/90000\r\n");
+//					content.append("y="+ ssrc + "\r\n");
+//					content.append("f=\r\n");
+//
+//					try {
+//						responseSdpAck(evt, content.toString());
+//					} catch (SipException e) {
+//						e.printStackTrace();
+//					} catch (InvalidArgumentException e) {
+//						e.printStackTrace();
+//					} catch (ParseException e) {
+//						e.printStackTrace();
+//					}
+//				}
+//
+//			} else {
+//				// 非上级平台请求,查询是否设备请求(通常为接收语音广播的设备)
+//				MediaDevice device = redisCatchStorage.getDevice(requesterId);
+//				if (device != null) {
+//					log.info("收到设备" + requesterId + "的语音广播Invite请求");
+//					responseAck(evt, Response.TRYING);
+//
+//					String contentString = new String(request.getRawContent());
+//					// jainSip不支持y=字段, 移除移除以解析。
+//					String substring = contentString;
+//					String ssrc = "0000000404";
+//					int ssrcIndex = contentString.indexOf("y=");
+//					if (ssrcIndex > 0) {
+//						substring = contentString.substring(0, ssrcIndex);
+//						ssrc = contentString.substring(ssrcIndex + 2, ssrcIndex + 12);
+//					}
+//					ssrcIndex = substring.indexOf("f=");
+//					if (ssrcIndex > 0) {
+//						substring = contentString.substring(0, ssrcIndex);
+//					}
+//					SessionDescription sdp = SdpFactory.getInstance().createSessionDescription(substring);
+//
+//					//  获取支持的格式
+//					Vector mediaDescriptions = sdp.getMediaDescriptions(true);
+//					// 查看是否支持PS 负载96
+//					int port = -1;
+//					//boolean recvonly = false;
+//					boolean mediaTransmissionTCP = false;
+//					Boolean tcpActive = null;
+//					for (int i = 0; i < mediaDescriptions.size(); i++) {
+//						MediaDescription mediaDescription = (MediaDescription)mediaDescriptions.get(i);
+//						Media media = mediaDescription.getMedia();
+//
+//						Vector mediaFormats = media.getMediaFormats(false);
+//						if (mediaFormats.contains("8")) {
+//							port = media.getMediaPort();
+//							String protocol = media.getProtocol();
+//							// 区分TCP发流还是udp, 当前默认udp
+//							if ("TCP/RTP/AVP".equals(protocol)) {
+//								String setup = mediaDescription.getAttribute("setup");
+//								if (setup != null) {
+//									mediaTransmissionTCP = true;
+//									if ("active".equals(setup)) {
+//										tcpActive = true;
+//									} else if ("passive".equals(setup)) {
+//										tcpActive = false;
+//									}
+//								}
+//							}
+//							break;
+//						}
+//					}
+//					if (port == -1) {
+//						log.info("不支持的媒体格式,返回415");
+//						// 回复不支持的格式
+//						responseAck(evt, Response.UNSUPPORTED_MEDIA_TYPE); // 不支持的格式,发415
+//						return;
+//					}
+//					String username = sdp.getOrigin().getUsername();
+//					String addressStr = sdp.getOrigin().getAddress();
+//					log.info("设备{}请求语音流,地址:{}:{},ssrc:{}", username, addressStr, port, ssrc);
+//
+//				} else {
+//					log.warn("来自无效设备/平台的请求");
+//					responseAck(evt, Response.BAD_REQUEST);
+//				}
+//			}
+//
+//		} catch (SipException | InvalidArgumentException | ParseException e) {
+//			e.printStackTrace();
+//			log.warn("sdp解析错误");
+//			e.printStackTrace();
+//		} catch (SdpParseException e) {
+//			e.printStackTrace();
+//		} catch (SdpException e) {
+//			e.printStackTrace();
+//		}
+    }
+
+}

+ 99 - 0
jetlinks-manager/media-manager/src/main/java/org/jetlinks/community/media/event/request/impl/MessageRequestProcessor.java

@@ -0,0 +1,99 @@
+//package org.jetlinks.community.media.event.message;
+//
+//import org.dom4j.DocumentException;
+//import org.dom4j.Element;
+//import org.jetlinks.community.media.entity.MediaDevice;
+//import org.jetlinks.community.media.entity.ParentPlatform;
+//import org.jetlinks.community.media.event.request.IMessageHandler;
+//import org.jetlinks.community.media.event.request.ISipRequestProcessor;
+//import org.jetlinks.community.media.sip.SipRequestProcessorParent;
+//import org.jetlinks.community.media.sip.processor.SipProcessorObserver;
+//import org.jetlinks.community.utils.SipUtils;
+//import org.slf4j.Logger;
+//import org.slf4j.LoggerFactory;
+//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.message.Request;
+//import javax.sip.message.Response;
+//import java.text.ParseException;
+//import java.util.Map;
+//import java.util.concurrent.ConcurrentHashMap;
+//
+//@Component
+//public class MessageRequestProcessor extends SipRequestProcessorParent {
+//
+//    private final static Logger logger = LoggerFactory.getLogger(MessageRequestProcessor.class);
+//
+//    private static Map<String, IMessageHandler> messageHandlerMap = new ConcurrentHashMap<>();
+//
+//    @Autowired
+//    private SipProcessorObserver sipProcessorObserver;
+//
+//
+////    @Autowired
+////    private IVideoManagerStorager storage;
+////
+////    @Autowired
+////    private SipSubscribe sipSubscribe;
+//
+////    @Autowired
+////    private IRedisCatchStorage redisCatchStorage;
+//
+//
+//    @Override
+//    public String getMethod() {
+//        return Request.MESSAGE;
+//    }
+//
+//    @Override
+//    public void processRequest(RequestEvent evt) {
+//        logger.debug("接收到消息:" + evt.getRequest());
+//        String id = SipUtils.getUserIdFromFromHeader(evt.getRequest());
+//        CallIdHeader callIdHeader = (CallIdHeader)evt.getRequest().getHeader(CallIdHeader.NAME);
+//        // 查询设备是否存在
+//        MediaDevice device = redisCatchStorage.getDevice(id);
+//        // 查询上级平台是否存在
+//        ParentPlatform parentPlatform = storage.queryParentPlatByServerGBId(id);
+//        try {
+//            if (device == null && parentPlatform == null) {
+//                // 不存在则回复404
+//                responseAck(evt, Response.NOT_FOUND, "device "+ id +" not found");
+//                logger.warn("[设备未找到 ]: {}", id);
+//                if (sipSubscribe.getErrorSubscribe(callIdHeader.getCallId()) != null){
+//                    SipSubscribe.EventResult eventResult = new SipSubscribe.EventResult(new DeviceNotFoundEvent(evt.getDialog()));
+//                    sipSubscribe.getErrorSubscribe(callIdHeader.getCallId()).response(eventResult);
+//                };
+//            }else {
+//                Element rootElement = getRootElement(evt);
+//                String name = rootElement.getName();
+//                IMessageHandler messageHandler = messageHandlerMap.get(name);
+//                if (messageHandler != null) {
+//                    if (device != null) {
+//                        messageHandler.handleForDevice(evt, device, rootElement);
+//                    }else { // 由于上面已经判断都为null则直接返回,所以这里device和parentPlatform必有一个不为null
+//                        messageHandler.handleForPlatform(evt, parentPlatform, rootElement);
+//                    }
+//                }else {
+//                    // 不支持的message
+//                    // 不存在则回复415
+//                    responseAck(evt, Response.UNSUPPORTED_MEDIA_TYPE, "Unsupported message type, must Control/Notify/Query/Response");
+//                }
+//            }
+//        } catch (SipException e) {
+//            logger.warn("SIP 回复错误", e);
+//        } catch (InvalidArgumentException e) {
+//            logger.warn("参数无效", e);
+//        } catch (ParseException e) {
+//            logger.warn("SIP回复时解析异常", e);
+//        } catch (DocumentException e) {
+//            logger.warn("解析XML消息内容异常", e);
+//        }
+//    }
+//
+//
+//}

+ 226 - 0
jetlinks-manager/media-manager/src/main/java/org/jetlinks/community/media/event/request/impl/RegisterRequestProcessor.java

@@ -0,0 +1,226 @@
+package org.jetlinks.community.media.event.request.impl;
+import cn.hutool.core.util.StrUtil;
+import gov.nist.javax.sip.RequestEventExt;
+import gov.nist.javax.sip.header.Expires;
+import gov.nist.javax.sip.header.SIPDateHeader;
+import lombok.extern.slf4j.Slf4j;
+import org.jetlinks.community.media.entity.MediaDevice;
+import org.jetlinks.community.media.entity.SipServerConfig;
+import org.jetlinks.community.media.entity.WvpSipDate;
+import org.jetlinks.community.media.sip.SipContext;
+import org.jetlinks.community.media.sip.SipRequestProcessorParent;
+import org.jetlinks.community.utils.SipUtils;
+import org.jetlinks.core.event.EventBus;
+import org.redisson.api.RedissonClient;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+import org.springframework.util.StringUtils;
+
+import javax.sip.InvalidArgumentException;
+import javax.sip.RequestEvent;
+import javax.sip.ServerTransaction;
+import javax.sip.SipException;
+import javax.sip.header.*;
+import javax.sip.message.Request;
+import javax.sip.message.Response;
+import java.security.NoSuchAlgorithmException;
+import java.text.ParseException;
+import java.util.Calendar;
+import java.util.Locale;
+import java.util.Optional;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/**
+ * SIP命令类型: REGISTER请求
+ */
+@Component
+@Slf4j
+public class RegisterRequestProcessor extends SipRequestProcessorParent {
+
+//	@Autowired
+//	private RegisterLogicHandler handler;
+//
+//	@Autowired
+//	private IRedisCatchStorage redisCatchStorage;
+//
+//	@Autowired
+//	private IVideoManagerStorager storager;
+
+
+    private EventBus eventBus;
+
+    @Autowired
+    private RedissonClient redissonClient;
+    @Autowired
+    public RegisterRequestProcessor(EventBus eventBus) {
+        this.eventBus = eventBus;
+    }
+
+    @Override
+    public String getMethod() {
+        return Request.REGISTER;
+    }
+
+    /**
+     * 收到注册请求 处理
+     * 注册失败认为SIP代理处于离线状态,注册成功则认为其处于在线状态
+     * @param evt
+     */
+    @Override
+    public void processRequest(RequestEvent evt) {
+
+        try {
+            RequestEventExt evtExt = (RequestEventExt)evt;
+            String requestAddress = evtExt.getRemoteIpAddress() + ":" + evtExt.getRemotePort();
+            log.info("[{}] 收到sip注册请求,开始处理", requestAddress);
+            Request request = evt.getRequest();
+
+            Response response = null;
+            AtomicBoolean passwordCorrect = new AtomicBoolean(false);
+            // 注册标志  0:未携带授权头或者密码错误  1:注册成功   2:注销成功
+            int registerFlag = 0;
+            String deviceId =  SipUtils.getUserIdFromFromHeader(request);
+            //todo 获取设备信息
+            SipServerConfig sipConfig = SipContext.getConfig();
+
+            MediaDevice device = (MediaDevice)
+                Optional.ofNullable( redissonClient.getBucket(SipUtils.keepAliveKey(deviceId)).get()).orElse(new MediaDevice());
+            if(StrUtil.isEmpty(device.getId())){
+                //首次注册
+                device.setFirsRegister(true);
+            }
+            device.setId(deviceId);
+            //基于数字摘要的认证
+            response=baseDigest(request,sipConfig,passwordCorrect);
+
+            if (passwordCorrect.get()){
+                // 携带授权头并且密码正确
+                response = getMessageFactory().createResponse(Response.OK, request);
+                // 添加date头
+                SIPDateHeader dateHeader = new SIPDateHeader();
+                // 使用自己修改的
+                WvpSipDate wvpSipDate = new WvpSipDate(Calendar.getInstance(Locale.ENGLISH).getTimeInMillis());
+                dateHeader.setDate(wvpSipDate);
+                response.addHeader(dateHeader);
+
+                ExpiresHeader expiresHeader = (ExpiresHeader) request.getHeader(Expires.NAME);
+                if (expiresHeader == null) {
+                    response = getMessageFactory().createResponse(Response.BAD_REQUEST, request);
+                    ServerTransaction serverTransaction = getServerTransaction(evt);
+                    serverTransaction.sendResponse(response);
+                    if (serverTransaction.getDialog() != null) {
+                        serverTransaction.getDialog().delete();
+                    }
+                    return;
+                }
+                // 添加Contact头
+                response.addHeader(request.getHeader(ContactHeader.NAME));
+                // 添加Expires头
+                response.addHeader(request.getExpires());
+
+                // 获取到通信地址等信息
+                ViaHeader viaHeader = (ViaHeader) request.getHeader(ViaHeader.NAME);
+                String received = viaHeader.getReceived();
+                int rPort = viaHeader.getRPort();
+                // 解析本地地址替代
+                if (StringUtils.isEmpty(received) || rPort == -1) {
+                    received = viaHeader.getHost();
+                    rPort = viaHeader.getPort();
+                }
+                //
+                String transport = viaHeader.getTransport();
+                String protocol = viaHeader.getProtocol();
+                //todo
+                device.setStreamMode(protocol);
+                device.setTransport(transport);
+                device.setIp(received);
+                device.setPort(rPort);
+                device.setHostAddress(received.concat(":").concat(String.valueOf(rPort)));
+                // 注销成功
+                if (expiresHeader.getExpires() == 0) {
+                    registerFlag = 2;
+                }
+                // 注册成功
+                else {
+                    //todo 注册成功后,刷新redis中过期时间,当redis中不存在该设备时,则执行注销操作
+                    device.setExpires(expiresHeader.getExpires());
+                    registerFlag = 1;
+                    ViaHeader reqViaHeader = (ViaHeader) request.getHeader(ViaHeader.NAME);
+                    device.setTransport(reqViaHeader.getTransport());
+                }
+            }
+
+
+            ServerTransaction serverTransaction = getServerTransaction(evt);
+            serverTransaction.sendResponse(response);
+            if (serverTransaction.getDialog() != null) {
+                serverTransaction.getDialog().delete();
+            }
+            device.setProductId(sipConfig.getProductId());
+            // 注册成功
+            // 保存到redis
+            // 下发catelog查询目录
+            if (registerFlag == 1 ) {
+                log.info("[{}] 注册成功! id:" + device.getId(), requestAddress);
+                registerCommonDevice(device);
+                // 重新注册更新设备和通道,以免设备替换或更新后信息无法更新
+            } else if (registerFlag == 2) {
+                log.info("[{}] 注销成功! id:" + device.getId(), requestAddress);
+                unRegisterDevice(device);
+            }
+
+        } catch (SipException | InvalidArgumentException | NoSuchAlgorithmException | ParseException e) {
+            e.printStackTrace();
+        }
+
+    }
+
+    /**
+     * 媒体设备取消注册,改为离线状态
+     */
+    private void unRegisterDevice(MediaDevice device) {
+        eventBus
+            .publish(String.format("/media/device/%s/%s/unregister",device.getProductId(),device.getId()),device)
+            .subscribe();
+    }
+
+
+    /**
+     * 注册通用设备
+     */
+    private void registerCommonDevice(MediaDevice device){
+        eventBus
+            .publish(String.format("/media/device/%s/%s/register",device.getProductId(),device.getId()),device)
+            .subscribe();
+    }
+
+
+    /**
+     * 基于数字摘要的挑战应答
+     * a)1:SIP代理向SIP服务器发送Register请求;
+     * b)2:SIP服务器向SIP代理发送响应401,并在响应的消息头WWW_Authenticate字段中给出适合SIP代理的认证体制和参数;
+     * c)3:SIP代理重新向SIP服务器发送Register请求,在请求的Authorization字段给出信任书,包含认证信息;
+     * d)4:SIP服务器对请求进行验证,如果检查出SIP代理身份合法,向SIP代理发送成功响应200OK,如果身份不合法则发送拒绝服务应答
+     */
+    private Response baseDigest(Request request,SipServerConfig sipConfig,AtomicBoolean passwordCorrect) throws ParseException, NoSuchAlgorithmException {
+        AuthorizationHeader authorizationHeader = (AuthorizationHeader) request.getHeader(AuthorizationHeader.NAME);
+        Response response=null;
+        if(authorizationHeader==null){
+            return basicResponse(request,sipConfig);
+        }else {
+            //不需要密码
+            if (StringUtils.isEmpty(sipConfig.getPassword())) {
+                passwordCorrect.set(true);
+            } else {
+                passwordCorrect .set(authRequest(request,sipConfig));
+            }
+        }
+        // 注册失败
+        if(!passwordCorrect.get()){
+            response = getMessageFactory().createResponse(Response.FORBIDDEN, request);
+            response.setReasonPhrase("wrong password");
+            log.info("[{}] 密码/SIP服务器ID错误, 回复403", request.getRequestURI());
+        }
+        return response;
+    }
+}

+ 32 - 0
jetlinks-manager/media-manager/src/main/java/org/jetlinks/community/media/event/request/impl/TimeoutProcessor.java

@@ -0,0 +1,32 @@
+package org.jetlinks.community.media.event.request.impl;
+
+import org.jetlinks.community.media.sip.SipRequestProcessorParent;
+
+import javax.sip.TimeoutEvent;
+import javax.sip.header.CallIdHeader;
+
+/**
+ * @author lifang
+ * @version 1.0.0
+ * @ClassName TimeoutProcessor.java
+ * @Description TODO
+ * @createTime 2022年01月20日 08:10:00
+ */
+public class TimeoutProcessor extends SipRequestProcessorParent {
+
+
+    @Override
+    public String getMethod() {
+        return "timeout";
+    }
+
+    @Override
+    public void processTimeout(TimeoutEvent event) {
+        // TODO Auto-generated method stub
+        CallIdHeader callIdHeader = event.getClientTransaction().getDialog().getCallId();
+        String callId = callIdHeader.getCallId();
+//        SipSubscribe.Event errorSubscribe = sipSubscribe.getErrorSubscribe(callId);
+//        SipSubscribe.EventResult<TimeoutEvent> timeoutEventEventResult = new SipSubscribe.EventResult<>(event);
+//        errorSubscribe.response(timeoutEventEventResult);
+    }
+}

+ 23 - 0
jetlinks-manager/media-manager/src/main/java/org/jetlinks/community/media/event/request/message/IMessageHandler.java

@@ -0,0 +1,23 @@
+package org.jetlinks.community.media.event.request.message;
+
+import org.dom4j.Element;
+import org.jetlinks.community.media.entity.MediaDevice;
+import org.jetlinks.community.media.entity.ParentPlatform;
+
+import javax.sip.RequestEvent;
+
+public interface IMessageHandler {
+    /**
+     * 处理来自设备的信息
+     * @param evt
+     * @param device
+     */
+    void handleForDevice(RequestEvent evt, MediaDevice device, Element element);
+
+    /**
+     * 处理来自平台的信息
+     * @param evt
+     * @param parentPlatform
+     */
+    void handleForPlatform(RequestEvent evt, ParentPlatform parentPlatform, Element element);
+}

+ 44 - 0
jetlinks-manager/media-manager/src/main/java/org/jetlinks/community/media/event/request/message/MessageHandlerAbstract.java

@@ -0,0 +1,44 @@
+package org.jetlinks.community.media.event.request.message;
+
+import org.jetlinks.community.media.sip.SipRequestProcessorParent;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+import javax.annotation.PostConstruct;
+
+import static org.jetlinks.community.utils.XmlUtil.getText;
+@Component
+public abstract class MessageHandlerAbstract extends SipRequestProcessorParent implements IMessageHandler{
+
+
+    @Autowired
+    public MessageRequestProcessor messageRequestProcessor;
+
+
+    @PostConstruct
+    public void start(){
+        messageRequestProcessor.handleDeviceMessage()
+            .filter(tp3->matchCmd( getText(tp3._3(), "CmdType")))
+            .doOnNext(tp3-> handleForDevice(tp3._1(),tp3._2(),tp3._3()))
+            .subscribe();
+
+        messageRequestProcessor.handlePlatMessage()
+            .filter(tp3->matchCmd( getText(tp3._3(), "CmdType")))
+            .doOnNext(tp3-> handleForPlatform(tp3._1(),tp3._2(),tp3._3()))
+            .subscribe();
+    }
+
+
+    @Override
+    public String getMethod() {
+        return "";
+    }
+
+    /**
+     * 对cmd请求作出匹配
+     * @param requestCmd
+     * @return
+     */
+   public abstract boolean matchCmd(String requestCmd);
+
+}

+ 135 - 0
jetlinks-manager/media-manager/src/main/java/org/jetlinks/community/media/event/request/message/MessageRequestProcessor.java

@@ -0,0 +1,135 @@
+package org.jetlinks.community.media.event.request.message;
+
+import cn.hutool.core.util.StrUtil;
+import io.netty.util.internal.PlatformDependent;
+import io.vavr.Tuple;
+import io.vavr.Tuple3;
+import lombok.SneakyThrows;
+import lombok.extern.slf4j.Slf4j;
+import org.dom4j.DocumentException;
+import org.dom4j.Element;
+import org.jetlinks.community.media.entity.MediaDevice;
+import org.jetlinks.community.media.entity.ParentPlatform;
+import org.jetlinks.community.media.entity.PlatformCatalog;
+import org.jetlinks.community.media.service.LocalMediaDeviceService;
+import org.jetlinks.community.media.sip.SipContext;
+import org.jetlinks.community.media.sip.SipRequestProcessorParent;
+import org.jetlinks.community.media.sip.processor.SipProcessorObserver;
+import org.jetlinks.community.utils.SipUtils;
+import org.redisson.api.RedissonClient;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+import reactor.core.publisher.EmitterProcessor;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.FluxSink;
+import reactor.core.publisher.Mono;
+
+import javax.sip.InvalidArgumentException;
+import javax.sip.RequestEvent;
+import javax.sip.SipException;
+import javax.sip.header.CallIdHeader;
+import javax.sip.message.Request;
+import javax.sip.message.Response;
+import java.text.ParseException;
+import java.util.function.Function;
+
+@Component
+@Slf4j
+public class MessageRequestProcessor extends SipRequestProcessorParent {
+
+    //处理设备信息
+    private EmitterProcessor<Tuple3<RequestEvent,MediaDevice,Element>> deviceProcessor=EmitterProcessor.create();
+    private FluxSink<Tuple3<RequestEvent,MediaDevice,Element>> deviceFluxSink=deviceProcessor.sink();
+
+
+    //处理平台信息
+    private EmitterProcessor<Tuple3<RequestEvent,ParentPlatform,Element>> platProcessor=EmitterProcessor.create();
+    private FluxSink<Tuple3<RequestEvent,ParentPlatform,Element>> platFluxSink=platProcessor.sink();
+
+//    @Autowired
+//    private IVideoManagerStorager storage;
+//
+//    @Autowired
+//    private SipSubscribe sipSubscribe;
+//
+//    @Autowired
+//    private IRedisCatchStorage redisCatchStorage;
+
+    @Autowired
+    private RedissonClient client;
+    @Autowired
+    private LocalMediaDeviceService deviceService;
+    @SneakyThrows
+    @Override
+    public void processRequest(RequestEvent evt) {
+        log.debug("接收到消息:" + evt.getRequest());
+        String deviceId = SipUtils.getUserIdFromFromHeader(evt.getRequest());
+        CallIdHeader callIdHeader = (CallIdHeader)evt.getRequest().getHeader(CallIdHeader.NAME);
+        // 查询设备是否存在
+        Mono.zip(Mono.justOrEmpty((MediaDevice) (client.getBucket(SipUtils.keepAliveKey(deviceId)).get()))
+            .switchIfEmpty(deviceService.findById(deviceId)),
+            //todo
+            Mono.justOrEmpty(new ParentPlatform()))
+            .doOnNext(tuple2->{
+                MediaDevice device = tuple2.getT1();
+                ParentPlatform parentPlatform = tuple2.getT2();
+                try {
+                    if (device == null
+                        && parentPlatform == null) {
+                        // 不存在则回复404
+                        responseAck(evt, Response.NOT_FOUND, "device "+ deviceId +" not found");
+                        log.warn("[设备未找到 ]: {}", deviceId);
+//                if (sipSubscribe.getErrorSubscribe(callIdHeader.getCallId()) != null){
+//                    SipSubscribe.EventResult eventResult = new SipSubscribe.EventResult(new DeviceNotFoundEvent(evt.getDialog()));
+//                    sipSubscribe.getErrorSubscribe(callIdHeader.getCallId()).response(eventResult);
+//                };
+                    }else {
+                        Element rootElement = getRootElement(evt, SipContext.getConfig().getCharset());
+                        if (device != null) {
+                            if(deviceProcessor.hasDownstreams()){
+                                deviceFluxSink.next(Tuple.of(evt,device,rootElement));
+                            }
+                        }else { // 由于上面已经判断都为null则直接返回,所以这里device和parentPlatform必有一个不为null
+                            if(platProcessor.hasDownstreams()){
+                                platFluxSink.next(Tuple.of(evt,parentPlatform,rootElement));
+                            }
+                        }
+                        // 不支持的message
+                        // 不存在则回复415
+                        responseAck(evt, Response.UNSUPPORTED_MEDIA_TYPE, "Unsupported message type, must Control/Notify/Query/Response");
+                    }
+
+                } catch (SipException e) {
+                    log.warn("SIP 回复错误", e);
+                } catch (InvalidArgumentException e) {
+                    log.warn("参数无效", e);
+                } catch (ParseException e) {
+                    log.warn("SIP回复时解析异常", e);
+                } catch (DocumentException e) {
+                    log.warn("解析XML消息内容异常", e);
+                }
+            }).subscribe();
+
+        // 查询上级平台是否存在
+//        ParentPlatform parentPlatform = storage.queryParentPlatByServerGBId(id);
+        ParentPlatform parentPlatform = null;
+//        deviceFluxSink.next(Tuple.of(evt,device,getRootElement(evt, SipContext.getConfig().getCharset())));
+
+    }
+
+    public Flux<Tuple3<RequestEvent,MediaDevice,Element>> handleDeviceMessage(){
+        return deviceProcessor.map(Function.identity());
+    }
+
+    public Flux<Tuple3<RequestEvent, ParentPlatform,Element>> handlePlatMessage(){
+        return platProcessor.map(Function.identity());
+    }
+
+
+    @Override
+    public String getMethod() {
+        return Request.MESSAGE;
+    }
+}

+ 85 - 0
jetlinks-manager/media-manager/src/main/java/org/jetlinks/community/media/event/request/message/notify/KeepaliveNotifyMessageHandler.java

@@ -0,0 +1,85 @@
+package org.jetlinks.community.media.event.request.message.notify;
+
+import lombok.extern.slf4j.Slf4j;
+import org.dom4j.Element;
+import org.jetlinks.community.media.contanst.CmdType;
+import org.jetlinks.community.media.entity.MediaDevice;
+import org.jetlinks.community.media.entity.ParentPlatform;
+import org.jetlinks.community.media.event.request.message.MessageHandlerAbstract;
+import org.jetlinks.core.event.EventBus;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+import org.springframework.util.StringUtils;
+
+import javax.sip.InvalidArgumentException;
+import javax.sip.RequestEvent;
+import javax.sip.SipException;
+import javax.sip.header.ViaHeader;
+import javax.sip.message.Response;
+import java.text.ParseException;
+
+@Component
+@Slf4j
+public class KeepaliveNotifyMessageHandler extends MessageHandlerAbstract {
+
+//    @Autowired
+//    private NotifyMessageHandler notifyMessageHandler;
+//
+//    @Autowired
+//    private EventPublisher publisher;
+//
+//    @Autowired
+//    private IVideoManagerStorager videoManagerStorager;
+//
+//    @Autowired
+//    private IRedisCatchStorage redisCatchStorage;
+
+    @Autowired
+    private EventBus eventBus;
+
+    @Override
+    public void handleForDevice(RequestEvent evt, MediaDevice device, Element element) {
+        // 检查设备是否存在并在线, 不在线则设置为在线
+        try {
+            if (device != null ) {
+                // 回复200 OK
+                responseAck(evt, Response.OK);
+                // 判断RPort是否改变,改变则说明路由nat信息变化,修改设备信息
+                // 获取到通信地址等信息
+                ViaHeader viaHeader = (ViaHeader) evt.getRequest().getHeader(ViaHeader.NAME);
+                String received = viaHeader.getReceived();
+                int rPort = viaHeader.getRPort();
+                // 解析本地地址替代
+                if (StringUtils.isEmpty(received) || rPort == -1) {
+                    received = viaHeader.getHost();
+                    rPort = viaHeader.getPort();
+                }
+                //todo 端口改变,更新端口号
+                if (device.getPort() != rPort) {
+                    device.setPort(rPort);
+//                    videoManagerStorager.updateDevice(device);
+//                    redisCatchStorage.updateDevice(device);
+                }
+                //todo 心跳成功,重置过期时间
+                eventBus.publish("/media/device/heart-beat",device).subscribe();
+            }
+        } catch (SipException e) {
+            e.printStackTrace();
+        } catch (InvalidArgumentException e) {
+            log.error("sip 参数错误,",e);
+        } catch (ParseException e) {
+            log.error("sip 解析错误,",e);
+        }
+    }
+
+    @Override
+    public boolean matchCmd(String requestCmd) {
+        return CmdType.KEEP_ALIVE.equals(requestCmd);
+    }
+
+
+    @Override
+    public void handleForPlatform(RequestEvent evt, ParentPlatform parentPlatform, Element element) {
+// 不会收到上级平台的心跳信息
+    }
+}

+ 56 - 0
jetlinks-manager/media-manager/src/main/java/org/jetlinks/community/media/event/request/message/notify/NoSupportMessageHandler.java

@@ -0,0 +1,56 @@
+package org.jetlinks.community.media.event.request.message.notify;
+
+import lombok.extern.slf4j.Slf4j;
+import org.dom4j.Element;
+import org.jetlinks.community.media.contanst.CmdType;
+import org.jetlinks.community.media.entity.MediaDevice;
+import org.jetlinks.community.media.entity.ParentPlatform;
+import org.jetlinks.community.media.event.request.message.MessageHandlerAbstract;
+import org.springframework.stereotype.Component;
+
+import javax.sip.InvalidArgumentException;
+import javax.sip.RequestEvent;
+import javax.sip.SipException;
+import javax.sip.message.Response;
+import java.text.ParseException;
+
+/**
+ * @author lifang
+ * @version 1.0.0
+ * @ClassName NoSupportMessageHandler.java
+ * @Description TODO
+ * @createTime 2022年01月21日 14:55:00
+ */
+@Slf4j
+@Component
+public class NoSupportMessageHandler extends MessageHandlerAbstract {
+    @Override
+    public boolean matchCmd(String requestCmd) {
+        return !CmdType.KEEP_ALIVE.equals(requestCmd)
+            && !CmdType.NOTIFY.equals(requestCmd)
+            &&!CmdType.QUERY.equals(requestCmd)
+            &&!CmdType.RESPONSE.equals(requestCmd);
+    }
+
+    @Override
+    public void handleForDevice(RequestEvent evt, MediaDevice device, Element element) {
+       unSupport(evt);
+    }
+
+    @Override
+    public void handleForPlatform(RequestEvent evt, ParentPlatform parentPlatform, Element element) {
+        unSupport(evt);
+    }
+
+    private void unSupport(RequestEvent evt){
+        try {
+            responseAck(evt, Response.UNSUPPORTED_MEDIA_TYPE, "Unsupported message type, must Control/Notify/Query/Response");
+        } catch (SipException e) {
+            log.warn("SIP 回复错误", e);
+        } catch (InvalidArgumentException e) {
+            log.warn("参数无效", e);
+        } catch (ParseException e) {
+            log.warn("SIP回复时解析异常", e);
+        }
+    }
+}

+ 75 - 0
jetlinks-manager/media-manager/src/main/java/org/jetlinks/community/media/event/response/RegisterResponseProcessor.java

@@ -0,0 +1,75 @@
+package org.jetlinks.community.media.event.response;
+
+import gov.nist.javax.sip.ResponseEventExt;
+import gov.nist.javax.sip.stack.SIPDialog;
+import lombok.AllArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.jetlinks.community.media.event.request.ISipRequestProcessor;
+import org.jetlinks.community.media.storage.IRedisCatchStorage;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+import javax.sip.InvalidArgumentException;
+import javax.sip.ResponseEvent;
+import javax.sip.SipException;
+import javax.sip.address.SipURI;
+import javax.sip.header.CSeqHeader;
+import javax.sip.message.Request;
+import javax.sip.message.Response;
+import java.text.ParseException;
+
+/**    
+ * @description:Register响应处理器
+ * @author: swwheihei
+ * @date:   2020年5月3日 下午5:32:23     
+ */
+@Component
+@AllArgsConstructor
+@Slf4j
+public class RegisterResponseProcessor extends ISipRequestProcessor {
+
+	/**
+	 * 处理Register响应
+	 *
+ 	 * @param evt 事件
+	 */
+	@Override
+	public void processResponse(ResponseEvent evt) {
+        try {
+            Response response = evt.getResponse();
+            int statusCode = response.getStatusCode();
+            // trying不会回复
+            if (statusCode == Response.TRYING) {
+
+            }
+            // 成功响应
+            // 下发ack
+            if (statusCode == Response.OK) {
+                ResponseEventExt event = (ResponseEventExt)evt;
+                SIPDialog dialog = (SIPDialog)evt.getDialog();
+                CSeqHeader cseq = (CSeqHeader) response.getHeader(CSeqHeader.NAME);
+                Request reqAck = dialog.createAck(cseq.getSeqNumber());
+                SipURI requestUrl = (SipURI) reqAck.getRequestURI();
+                try {
+                    requestUrl.setHost(event.getRemoteIpAddress());
+                    requestUrl.setPort(event.getRemotePort());
+                    reqAck.setRequestURI(requestUrl);
+                    log.info("向 " + event.getRemoteIpAddress() + ":" + event.getRemotePort() + "回复ack");
+//                    SipURI sipUrl = (SipURI)dialog.getRemoteParty().getURI();
+//                    String deviceId = requestUrl.getUser();
+//                    String channelId = sipUrl.getUser();
+                    dialog.sendAck(reqAck);
+                } catch (ParseException e) {
+                    log.error("sip回复解析地址失败,",e);
+                }
+
+            }
+        } catch (InvalidArgumentException | SipException e) {
+            log.warn("sip回复请求失败,",e);
+        }
+	}
+
+    @Override
+    public String getMethod() {
+        return Request.REGISTER;
+    }
+}

+ 116 - 0
jetlinks-manager/media-manager/src/main/java/org/jetlinks/community/media/service/LocalMediaDeviceService.java

@@ -0,0 +1,116 @@
+package org.jetlinks.community.media.service;
+
+import cn.hutool.core.date.DateUtil;
+import lombok.AllArgsConstructor;
+import org.hswebframework.web.crud.service.GenericReactiveCrudService;
+import org.jetlinks.community.gateway.annotation.Subscribe;
+import org.jetlinks.community.media.entity.MediaDevice;
+import org.jetlinks.community.media.entity.SipServerConfig;
+import org.jetlinks.community.media.enums.DeviceState;
+import org.jetlinks.community.media.sip.SipContext;
+import org.jetlinks.community.utils.SipUtils;
+import org.jetlinks.core.device.DeviceOperator;
+import org.jetlinks.core.device.DeviceRegistry;
+import org.jetlinks.core.event.EventBus;
+import org.jetlinks.core.message.DeviceOfflineMessage;
+import org.jetlinks.core.message.DeviceOnlineMessage;
+import org.jetlinks.core.message.DeviceRegisterMessage;
+import org.jetlinks.core.utils.IdUtils;
+import org.jetlinks.supports.server.DecodedClientMessageHandler;
+import org.redisson.api.RBucket;
+import org.redisson.api.RedissonClient;
+import org.springframework.stereotype.Service;
+import reactor.core.publisher.Mono;
+
+import java.util.concurrent.TimeUnit;
+
+/**
+ * @author lifang
+ * @version 1.0.0
+ * @ClassName LocalMediaDeviceService.java
+ * @Description TODO
+ * @createTime 2022年01月21日 14:59:00
+ */
+@Service
+@AllArgsConstructor
+public class LocalMediaDeviceService extends GenericReactiveCrudService<MediaDevice, String> {
+    private final RedissonClient redissonClient;
+
+    private final DecodedClientMessageHandler messageHandler;
+
+    private final EventBus eventBus;
+    private final DeviceRegistry registry;
+    @Subscribe("/media/device/*/*/register")
+    public void register(MediaDevice mediaDevice){
+        //todo 当设备为首次注册时,拉取设备信息
+        if(mediaDevice.isFirsRegister()){
+
+        }
+        //注册设备
+        this.updateById(mediaDevice.getId(),mediaDevice)
+            .filter(count->count==0)
+            .flatMap(ignore->save(mediaDevice))
+            .concatWith(
+                Mono.just(mediaDevice)
+                    .flatMap(device -> {
+                        DeviceRegisterMessage registerMessage = new DeviceRegisterMessage();
+                        registerMessage.setDeviceId(mediaDevice.getId());
+                        registerMessage.setMessageId(IdUtils.newUUID());
+                        registerMessage.setTimestamp(System.currentTimeMillis());
+                        registerMessage.addHeader("deviceName","GB28181-2016 "+mediaDevice.getId());
+                        registerMessage.addHeader("productId",mediaDevice.getProductId());
+                        return eventBus.publish("/device/*/*/register",registerMessage)
+                            .mergeWith(eventBus.publish("/media/device/heart-beat",mediaDevice)).then(Mono.empty());
+                    })
+                    .then(Mono.empty())
+            )
+            .subscribe();
+
+
+    }
+
+    @Subscribe("/media/device/*/*/unregister")
+    public void unRegister(MediaDevice mediaDevice){
+        //取消注册媒体设备
+
+        //通用设备下线
+        registry.getDevice(mediaDevice.getId())
+            .flatMap(operator -> {
+                DeviceOfflineMessage message = new DeviceOfflineMessage();
+                message.setTimestamp(System.currentTimeMillis());
+                message.setDeviceId(mediaDevice.getIp());
+                message.setMessageId(IdUtils.newUUID());
+                return messageHandler.handleMessage(operator,message);
+            }).subscribe();
+    }
+
+
+    @Subscribe("/media/device/heart-beat")
+    public void heartBeat(MediaDevice device){
+        SipServerConfig config = SipContext.getConfig();
+        String key = SipUtils.keepAliveKey(device.getId());
+
+        RBucket<MediaDevice> bucket = redissonClient.getBucket(key);
+        Mono<DeviceOperator> operatorMono = registry.getDevice(device.getId());
+        if (!bucket.isExists()) {
+            //心跳过期 ,设备上线
+            bucket.set(device,config.getTimeout(), TimeUnit.SECONDS);
+            DeviceOnlineMessage onlineMessage = new DeviceOnlineMessage();
+            onlineMessage.setDeviceId(device.getId());
+            onlineMessage.setTimestamp(System.currentTimeMillis());
+            onlineMessage.setMessageId(IdUtils.newUUID());
+            operatorMono
+                .flatMap(operator -> messageHandler.handleMessage(operator,onlineMessage));
+        }else {
+            bucket.expire(config.getTimeout(), TimeUnit.SECONDS);
+        }
+        //更新设备心跳时间
+        device.setKeepaliveTime(DateUtil.now());
+
+        device.setState(DeviceState.online);
+        //未注册的设备不进行更新
+        operatorMono
+            .flatMap(ignore->updateById(device.getId(),device))
+            .subscribe();
+    }
+}

+ 337 - 0
jetlinks-manager/media-manager/src/main/java/org/jetlinks/community/media/service/LocalPlayService.java

@@ -0,0 +1,337 @@
+//package org.jetlinks.community.media.service;
+//
+//import com.alibaba.fastjson.JSON;
+//import com.alibaba.fastjson.JSONArray;
+//import com.alibaba.fastjson.JSONObject;
+//import com.genersoft.iot.vmp.common.StreamInfo;
+//import com.genersoft.iot.vmp.conf.UserSetup;
+//import com.genersoft.iot.vmp.gb28181.bean.Device;
+//import com.genersoft.iot.vmp.gb28181.bean.DeviceChannel;
+//import com.genersoft.iot.vmp.gb28181.event.SipSubscribe;
+//import com.genersoft.iot.vmp.gb28181.session.VideoStreamSessionManager;
+//import com.genersoft.iot.vmp.gb28181.transmit.callback.DeferredResultHolder;
+//import com.genersoft.iot.vmp.gb28181.transmit.callback.RequestMessage;
+//import com.genersoft.iot.vmp.gb28181.transmit.cmd.impl.SIPCommander;
+//import com.genersoft.iot.vmp.media.zlm.ZLMHttpHookSubscribe;
+//import com.genersoft.iot.vmp.media.zlm.ZLMRESTfulUtils;
+//import com.genersoft.iot.vmp.media.zlm.dto.MediaServerItem;
+//import com.genersoft.iot.vmp.service.IMediaServerService;
+//import com.genersoft.iot.vmp.service.bean.SSRCInfo;
+//import com.genersoft.iot.vmp.storager.IRedisCatchStorage;
+//import com.genersoft.iot.vmp.storager.IVideoManagerStorager;
+//import com.genersoft.iot.vmp.utils.redis.RedisUtil;
+//import com.genersoft.iot.vmp.vmanager.bean.WVPResult;
+//import com.genersoft.iot.vmp.vmanager.gb28181.play.bean.PlayResult;
+//import com.genersoft.iot.vmp.service.IMediaService;
+//import com.genersoft.iot.vmp.service.IPlayService;
+//import gov.nist.javax.sip.stack.SIPDialog;
+//import lombok.AllArgsConstructor;
+//import lombok.extern.slf4j.Slf4j;
+//import org.jetlinks.community.media.zlm.dto.MediaServerItem;
+//import org.redisson.api.RedissonClient;
+//import org.slf4j.Logger;
+//import org.slf4j.LoggerFactory;
+//import org.springframework.beans.factory.annotation.Autowired;
+//import org.springframework.http.HttpStatus;
+//import org.springframework.http.ResponseEntity;
+//import org.springframework.stereotype.Service;
+//import org.springframework.util.ResourceUtils;
+//import org.springframework.web.context.request.async.DeferredResult;
+//
+//import java.io.FileNotFoundException;
+//import java.util.Objects;
+//import java.util.UUID;
+//
+//@SuppressWarnings(value = {"rawtypes", "unchecked"})
+//@Service
+//@Slf4j
+//@AllArgsConstructor
+//public class LocalPlayService  {
+//
+//
+//    private final RedissonClient redissonClient;
+////    @Autowired
+////    private IVideoManagerStorager storager;
+//
+////    @Autowired
+////    private SIPCommander cmder;
+//
+////    @Autowired
+////    private IRedisCatchStorage redisCatchStorage;
+////
+////    @Autowired
+////    private RedisUtil redis;
+////
+////    @Autowired
+////    private DeferredResultHolder resultHolder;
+////
+////    @Autowired
+////    private ZLMRESTfulUtils zlmresTfulUtils;
+////
+////    @Autowired
+////    private IMediaService mediaService;
+////
+////    @Autowired
+////    private IMediaServerService mediaServerService;
+////
+////    @Autowired
+////    private VideoStreamSessionManager streamSession;
+////
+////    @Autowired
+////    private UserSetup userSetup;
+//
+//
+//
+//    public Mono<PlayResult> play(MediaServerItem mediaServerItem, String deviceId, String channelId, ZLMHttpHookSubscribe.Event hookEvent, SipSubscribe.Event errorEvent) {
+//        PlayResult playResult = new PlayResult();
+//        RequestMessage msg = new RequestMessage();
+//        String key = DeferredResultHolder.CALLBACK_CMD_PLAY + deviceId + channelId;
+//        msg.setKey(key);
+//        String uuid = UUID.randomUUID().toString();
+//        msg.setId(uuid);
+//        playResult.setUuid(uuid);
+//        DeferredResult<ResponseEntity<String>> result = new DeferredResult<>(userSetup.getPlayTimeout());
+//        playResult.setResult(result);
+//        // 录像查询以channelId作为deviceId查询
+//        resultHolder.put(key, uuid, result);
+//        if (mediaServerItem == null) {
+//            WVPResult wvpResult = new WVPResult();
+//            wvpResult.setCode(-1);
+//            wvpResult.setMsg("未找到可用的zlm");
+//            msg.setData(wvpResult);
+//            resultHolder.invokeResult(msg);
+//            return playResult;
+//        }
+//        Device device = redisCatchStorage.getDevice(deviceId);
+//        StreamInfo streamInfo = redisCatchStorage.queryPlayByDevice(deviceId, channelId);
+//        playResult.setDevice(device);
+//        // 超时处理
+//        result.onTimeout(()->{
+//            log.warn(String.format("设备点播超时,deviceId:%s ,channelId:%s", deviceId, channelId));
+//            WVPResult wvpResult = new WVPResult();
+//            wvpResult.setCode(-1);
+//            SIPDialog dialog = streamSession.getDialog(deviceId, channelId);
+//            if (dialog != null) {
+//                wvpResult.setMsg("收流超时,请稍候重试");
+//            }else {
+//                wvpResult.setMsg("点播超时,请稍候重试");
+//            }
+//            msg.setData(wvpResult);
+//            // 点播超时回复BYE
+//            cmder.streamByeCmd(device.getDeviceId(), channelId);
+//            // 释放rtpserver
+//            mediaServerService.closeRrpServer(playResult.getDevice(), channelId);
+//            // 回复之前所有的点播请求
+//            resultHolder.invokeAllResult(msg);
+//        });
+//        result.onCompletion(()->{
+//            // 点播结束时调用截图接口
+//            try {
+//                String classPath = ResourceUtils.getURL("classpath:").getPath();
+//                // System.out.println(classPath);
+//                // 兼容打包为jar的class路径
+//                if(classPath.contains("jar")) {
+//                    classPath = classPath.substring(0, classPath.lastIndexOf("."));
+//                    classPath = classPath.substring(0, classPath.lastIndexOf("/") + 1);
+//                }
+//                if (classPath.startsWith("file:")) {
+//                    classPath = classPath.substring(classPath.indexOf(":") + 1);
+//                }
+//                String path = classPath + "static/static/snap/";
+//                // 兼容Windows系统路径(去除前面的“/”)
+//                if(System.getProperty("os.name").contains("indows")) {
+//                    path = path.substring(1);
+//                }
+//                String fileName =  deviceId + "_" + channelId + ".jpg";
+//                ResponseEntity responseEntity =  (ResponseEntity)result.getResult();
+//                if (responseEntity != null && responseEntity.getStatusCode() == HttpStatus.OK) {
+//                    WVPResult wvpResult = (WVPResult)responseEntity.getBody();
+//                    if (Objects.requireNonNull(wvpResult).getCode() == 0) {
+//                        StreamInfo streamInfoForSuccess = (StreamInfo)wvpResult.getData();
+//                        MediaServerItem mediaInfo = mediaServerService.getOne(streamInfoForSuccess.getMediaServerId());
+//                        String streamUrl = streamInfoForSuccess.getFmp4();
+//                        // 请求截图
+//                        log.info("[请求截图]: " + fileName);
+//                        zlmresTfulUtils.getSnap(mediaInfo, streamUrl, 15, 1, path, fileName);
+//                    }
+//                }
+//            } catch (FileNotFoundException e) {
+//                e.printStackTrace();
+//            }
+//        });
+//        if (streamInfo == null) {
+//            SSRCInfo ssrcInfo;
+//            String streamId = null;
+//            if (mediaServerItem.isRtpEnable()) {
+//                streamId = String.format("%s_%s", device.getDeviceId(), channelId);
+//            }
+//
+//            ssrcInfo = mediaServerService.openRTPServer(mediaServerItem, streamId);
+//
+//            // 发送点播消息
+//            cmder.playStreamCmd(mediaServerItem, ssrcInfo, device, channelId, (MediaServerItem mediaServerItemInUse, JSONObject response) -> {
+//                log.info("收到订阅消息: " + response.toJSONString());
+//                onPublishHandlerForPlay(mediaServerItemInUse, response, deviceId, channelId, uuid);
+//                if (hookEvent != null) {
+//                    hookEvent.response(mediaServerItem, response);
+//                }
+//            }, (event) -> {
+//                WVPResult wvpResult = new WVPResult();
+//                wvpResult.setCode(-1);
+//                // 点播返回sip错误
+//                mediaServerService.closeRrpServer(playResult.getDevice(), channelId);
+//                wvpResult.setMsg(String.format("点播失败, 错误码: %s, %s", event.statusCode, event.msg));
+//                msg.setData(wvpResult);
+//                resultHolder.invokeAllResult(msg);
+//                if (errorEvent != null) {
+//                    errorEvent.response(event);
+//                }
+//
+//
+//            });
+//        } else {
+//            String streamId = streamInfo.getStreamId();
+//            if (streamId == null) {
+//                WVPResult wvpResult = new WVPResult();
+//                wvpResult.setCode(-1);
+//                wvpResult.setMsg("点播失败, redis缓存streamId等于null");
+//                msg.setData(wvpResult);
+//                resultHolder.invokeAllResult(msg);
+//                return playResult;
+//            }
+//            String mediaServerId = streamInfo.getMediaServerId();
+//            MediaServerItem mediaInfo = mediaServerService.getOne(mediaServerId);
+//
+//            JSONObject rtpInfo = zlmresTfulUtils.getRtpInfo(mediaInfo, streamId);
+//            if (rtpInfo != null && rtpInfo.getBoolean("exist")) {
+//
+//                WVPResult wvpResult = new WVPResult();
+//                wvpResult.setCode(0);
+//                wvpResult.setMsg("success");
+//                wvpResult.setData(streamInfo);
+//                msg.setData(wvpResult);
+//
+//                resultHolder.invokeAllResult(msg);
+//                if (hookEvent != null) {
+//                    hookEvent.response(mediaServerItem, JSONObject.parseObject(JSON.toJSONString(streamInfo)));
+//                }
+//            } else {
+//                // TODO 点播前是否重置状态
+//                redisCatchStorage.stopPlay(streamInfo);
+//                storager.stopPlay(streamInfo.getDeviceID(), streamInfo.getChannelId());
+//                SSRCInfo ssrcInfo;
+//                String streamId2 = null;
+//                if (mediaServerItem.isRtpEnable()) {
+//                    streamId2 = String.format("%s_%s", device.getDeviceId(), channelId);
+//                }
+//                ssrcInfo = mediaServerService.openRTPServer(mediaServerItem, streamId2);
+//
+//                cmder.playStreamCmd(mediaServerItem, ssrcInfo, device, channelId, (MediaServerItem mediaServerItemInuse, JSONObject response) -> {
+//                    log.info("收到订阅消息: " + response.toJSONString());
+//                    onPublishHandlerForPlay(mediaServerItemInuse, response, deviceId, channelId, uuid);
+//                }, (event) -> {
+//                    mediaServerService.closeRrpServer(playResult.getDevice(), channelId);
+//                    WVPResult wvpResult = new WVPResult();
+//                    wvpResult.setCode(-1);
+//                    wvpResult.setMsg(String.format("点播失败, 错误码: %s, %s", event.statusCode, event.msg));
+//                    msg.setData(wvpResult);
+//                    resultHolder.invokeAllResult(msg);
+//                });
+//            }
+//        }
+//
+//        return playResult;
+//    }
+//
+////    @Override
+////    public void onPublishHandlerForPlay(MediaServerItem mediaServerItem, JSONObject resonse, String deviceId, String channelId, String uuid) {
+////        RequestMessage msg = new RequestMessage();
+////        msg.setId(uuid);
+////        msg.setKey(DeferredResultHolder.CALLBACK_CMD_PLAY + deviceId + channelId);
+////        StreamInfo streamInfo = onPublishHandler(mediaServerItem, resonse, deviceId, channelId, uuid);
+////        if (streamInfo != null) {
+////            DeviceChannel deviceChannel = storager.queryChannel(deviceId, channelId);
+////            if (deviceChannel != null) {
+////                deviceChannel.setStreamId(streamInfo.getStreamId());
+////                storager.startPlay(deviceId, channelId, streamInfo.getStreamId());
+////            }
+////            redisCatchStorage.startPlay(streamInfo);
+////            msg.setData(JSON.toJSONString(streamInfo));
+////
+////            WVPResult wvpResult = new WVPResult();
+////            wvpResult.setCode(0);
+////            wvpResult.setMsg("success");
+////            wvpResult.setData(streamInfo);
+////            msg.setData(wvpResult);
+////
+////            resultHolder.invokeAllResult(msg);
+////        } else {
+////            log.warn("设备预览API调用失败!");
+////            msg.setData("设备预览API调用失败!");
+////            resultHolder.invokeAllResult(msg);
+////        }
+////    }
+////
+////    @Override
+////    public MediaServerItem getNewMediaServerItem(Device device) {
+////        if (device == null) return null;
+////        String mediaServerId = device.getMediaServerId();
+////        MediaServerItem mediaServerItem;
+////        if (mediaServerId == null) {
+////            mediaServerItem = mediaServerService.getMediaServerForMinimumLoad();
+////        }else {
+////            mediaServerItem = mediaServerService.getOne(mediaServerId);
+////        }
+////        if (mediaServerItem == null) {
+////            log.warn("点播时未找到可使用的ZLM...");
+////        }
+////        return mediaServerItem;
+////    }
+////
+////
+////    @Override
+////    public void onPublishHandlerForPlayBack(MediaServerItem mediaServerItem, JSONObject resonse, String deviceId, String channelId, String uuid) {
+////        RequestMessage msg = new RequestMessage();
+////        msg.setKey(DeferredResultHolder.CALLBACK_CMD_PLAYBACK + deviceId + channelId);
+////        msg.setId(uuid);
+////        StreamInfo streamInfo = onPublishHandler(mediaServerItem, resonse, deviceId, channelId, uuid);
+////        if (streamInfo != null) {
+////            redisCatchStorage.startPlayback(streamInfo);
+////            msg.setData(JSON.toJSONString(streamInfo));
+////            resultHolder.invokeResult(msg);
+////        } else {
+////            log.warn("设备回放API调用失败!");
+////            msg.setData("设备回放API调用失败!");
+////            resultHolder.invokeResult(msg);
+////        }
+////    }
+////
+////
+////    @Override
+////    public void onPublishHandlerForDownload(MediaServerItem mediaServerItem, JSONObject response, String deviceId, String channelId, String uuid) {
+////        RequestMessage msg = new RequestMessage();
+////        msg.setKey(DeferredResultHolder.CALLBACK_CMD_DOWNLOAD + deviceId + channelId);
+////        msg.setId(uuid);
+////        StreamInfo streamInfo = onPublishHandler(mediaServerItem, response, deviceId, channelId, uuid);
+////        if (streamInfo != null) {
+////            redisCatchStorage.startDownload(streamInfo);
+////            msg.setData(JSON.toJSONString(streamInfo));
+////            resultHolder.invokeResult(msg);
+////        } else {
+////            log.warn("设备预览API调用失败!");
+////            msg.setData("设备预览API调用失败!");
+////            resultHolder.invokeResult(msg);
+////        }
+////    }
+////
+////
+////    public StreamInfo onPublishHandler(MediaServerItem mediaServerItem, JSONObject resonse, String deviceId, String channelId, String uuid) {
+////        String streamId = resonse.getString("stream");
+////        JSONArray tracks = resonse.getJSONArray("tracks");
+////        StreamInfo streamInfo = mediaService.getStreamInfoByAppAndStream(mediaServerItem,"rtp", streamId, tracks);
+////        streamInfo.setDeviceID(deviceId);
+////        streamInfo.setChannelId(channelId);
+////        return streamInfo;
+////    }
+//
+//}

+ 23 - 0
jetlinks-manager/media-manager/src/main/java/org/jetlinks/community/media/service/SipService.java

@@ -0,0 +1,23 @@
+//package org.jetlinks.community.media.service;
+//
+//import org.hswebframework.web.crud.service.GenericReactiveCrudService;
+//import org.jetlinks.community.media.entity.SipServerConfig;
+//import org.springframework.stereotype.Service;
+//import reactor.core.publisher.Mono;
+//
+///**
+// * @author lifang
+// * @version 1.0.0
+// * @ClassName SipService.java
+// * @Description TODO
+// * @createTime 2022年01月15日 13:57:00
+// */
+//@Service
+//public class SipService  extends GenericReactiveCrudService<SipServerConfig, String> {
+//    public Mono<Void> doStart(SipServerConfig serverConfig){
+//
+//        return Mono.empty();
+//    }
+//
+//
+//}

+ 143 - 0
jetlinks-manager/media-manager/src/main/java/org/jetlinks/community/media/session/SsrcConfig.java

@@ -0,0 +1,143 @@
+package org.jetlinks.community.media.session;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Random;
+import java.util.Set;
+
+public class SsrcConfig {
+
+    /**
+     * 播流最大并发个数
+     */
+    public static final Integer MAX_STRTEAM_COUNT = 10000;
+
+    /**
+     * zlm流媒体服务器Id
+     */
+    private String mediaServerId;
+
+    private String ssrcPrefix;
+    /**
+     * zlm流媒体服务器已用会话句柄
+     */
+    private List<String> isUsed;
+    /**
+     * zlm流媒体服务器可用会话句柄
+     */
+    private List<String> notUsed;
+
+    public SsrcConfig() {
+    }
+
+    public SsrcConfig(String mediaServerId, Set<String> usedSet, String sipDomain) {
+        this.mediaServerId = mediaServerId;
+        this.isUsed = new ArrayList<>();
+        this.ssrcPrefix = sipDomain.substring(3, 8);
+        this.notUsed = new ArrayList<>();
+        for (int i = 1; i < MAX_STRTEAM_COUNT; i++) {
+            String ssrc;
+            if (i < 10) {
+                ssrc = "000" + i;
+            } else if (i < 100) {
+                ssrc = "00" + i;
+            } else if (i < 1000) {
+                ssrc = "0" + i;
+            } else {
+                ssrc = String.valueOf(i);
+            }
+            if (null == usedSet || !usedSet.contains(ssrc)) {
+                this.notUsed.add(ssrc);
+            } else {
+                this.isUsed.add(ssrc);
+            }
+        }
+    }
+
+
+    /**
+     * 获取视频预览的SSRC值,第一位固定为0
+     * @return ssrc
+     */
+    public String getPlaySsrc() {
+        return "0" + getSsrcPrefix() + getSN();
+    }
+
+    /**
+     * 获取录像回放的SSRC值,第一位固定为1
+     *
+     */
+    public String getPlayBackSsrc() {
+        return "1" + getSsrcPrefix() + getSN();
+    }
+
+    /**
+     * 释放ssrc,主要用完的ssrc一定要释放,否则会耗尽
+     * @param ssrc 需要重置的ssrc
+     */
+    public void releaseSsrc(String ssrc) {
+        if (ssrc == null) {
+            return;
+        }
+        String sn = ssrc.substring(6);
+        try {
+            isUsed.remove(sn);
+            notUsed.add(sn);
+        }catch (NullPointerException e){
+            System.out.printf("11111");
+        }
+    }
+
+    /**
+     * 获取后四位数SN,随机数
+     *
+     */
+    private String getSN() {
+        String sn = null;
+        int index = 0;
+        if (notUsed.size() == 0) {
+            throw new RuntimeException("ssrc已经用完");
+        } else if (notUsed.size() == 1) {
+            sn = notUsed.get(0);
+        } else {
+            index = new Random().nextInt(notUsed.size() - 1);
+            sn = notUsed.get(index);
+        }
+        notUsed.remove(index);
+        isUsed.add(sn);
+        return sn;
+    }
+
+    public String getSsrcPrefix() {
+        return ssrcPrefix;
+    }
+
+    public String getMediaServerId() {
+        return mediaServerId;
+    }
+
+    public void setMediaServerId(String mediaServerId) {
+        this.mediaServerId = mediaServerId;
+    }
+
+    public void setSsrcPrefix(String ssrcPrefix) {
+        this.ssrcPrefix = ssrcPrefix;
+    }
+
+    public List<String> getIsUsed() {
+        return isUsed;
+    }
+
+    public void setIsUsed(List<String> isUsed) {
+        this.isUsed = isUsed;
+    }
+
+    public List<String> getNotUsed() {
+        return notUsed;
+    }
+
+    public void setNotUsed(List<String> notUsed) {
+        this.notUsed = notUsed;
+    }
+
+}

+ 147 - 0
jetlinks-manager/media-manager/src/main/java/org/jetlinks/community/media/sip/SipContext.java

@@ -0,0 +1,147 @@
+package org.jetlinks.community.media.sip;
+
+import gov.nist.javax.sip.SipProviderImpl;
+import io.vavr.Tuple;
+import io.vavr.Tuple3;
+import lombok.extern.slf4j.Slf4j;
+import org.hswebframework.web.exception.BusinessException;
+import org.jetlinks.community.media.entity.SipServerConfig;
+import javax.sip.*;
+import javax.sip.address.URI;
+import javax.sip.header.ToHeader;
+import javax.sip.header.ViaHeader;
+import java.util.*;
+
+/**
+ * @author lifang
+ * @version 1.0.0
+ * @ClassName SipContext.java
+ * @Description sip服务器上下文,存储sip服务的一些实体类
+ * @createTime 2022年01月17日 16:23:00
+ */
+@Slf4j
+public class SipContext {
+    private static SipFactory sipFactory=null;
+
+    private static Tuple3<SipServerConfig,SipStack,Map<String,SipProvider>> tuple3Map=Tuple.of(null,null,null);
+
+
+    public static void updateSipServerConfig(SipServerConfig config) throws TransportNotSupportedException, InvalidArgumentException {
+        SipStack sipStack = tuple3Map._2();
+        SipProviderImpl provider = (SipProviderImpl) tuple3Map._3();
+        if(provider!=null){
+            provider.removeListeningPoints();
+        }
+        if(sipStack!=null){
+            sipStack.stop();
+        }
+        tuple3Map=tuple3Map.update1(config);
+        getSipStack(config);
+        getSipProviders(config);
+    }
+
+    public static SipServerConfig getConfig(){
+        return tuple3Map._1();
+    }
+
+
+    public static SipStack getSipStack(){
+        return tuple3Map._2();
+    }
+
+
+    public static Map<String,SipProvider> getSipProviders(){
+        return tuple3Map._3();
+    }
+
+    public static SipProvider getSipProvider(){
+        return tuple3Map._3().values().stream().findFirst().get();
+    }
+
+
+    public static SipFactory getSipFactory(){
+        if(sipFactory==null){
+            sipFactory=SipFactory.getInstance();
+            sipFactory.setPathName("gov.nist");
+        }
+        return sipFactory;
+    }
+
+    public static SipStack getSipStack(SipServerConfig config){
+        tuple3Map=tuple3Map.update2(createSipStack(config.getHost())) ;
+        tuple3Map=tuple3Map.update1(config);
+        return getSipStack();
+    }
+
+
+    public static Map<String,SipProvider> getSipProviders(SipServerConfig config) throws InvalidArgumentException, TransportNotSupportedException {
+        SipStack sipStack = getSipStack();
+        if(sipStack==null){
+            sipStack=getSipStack(config);
+        }
+        try {
+            listenPoint(sipStack,config.getHost(),config.getPort(),config.getTransport());
+        }catch (TransportAlreadySupportedException e){
+            e.printStackTrace();
+        }catch (ObjectInUseException e){
+            e.printStackTrace();
+        }
+        return getSipProviders();
+    }
+
+    public static  void listenPoint(String host,int port,String transport) throws InvalidArgumentException, TransportNotSupportedException {
+        try {
+            listenPoint(getSipStack(),host,port,transport);
+        } catch (TransportAlreadySupportedException e) {
+            log.warn("sip协议接口已被使用,host:[{}],port:[{}],transport:[{}]",host,port,transport);
+        }catch (ObjectInUseException e) {
+        }
+    }
+
+    private static void  listenPoint(SipStack sipStack,String host,int port,String transport) throws InvalidArgumentException, TransportNotSupportedException, ObjectInUseException, TransportAlreadySupportedException {
+        ListeningPoint listeningPoint = sipStack.createListeningPoint(host, port, transport);
+        Map<String, SipProvider> providerMap =Optional.ofNullable( tuple3Map._3()).orElse(new HashMap<>());
+        String key=transport.toUpperCase()+port;
+        SipProvider provider = providerMap.get(key);
+        if(provider==null){
+            provider=sipStack.createSipProvider(listeningPoint);
+        }else {
+            provider.addListeningPoint(listeningPoint);
+        }
+        //todo
+        provider.setAutomaticDialogSupportEnabled(true);
+        providerMap.put(key,provider);
+        tuple3Map=tuple3Map.update3(providerMap);
+    }
+    private static SipStack createSipStack(String host) {
+        Properties properties = new Properties();
+        properties.setProperty("javax.sip.STACK_NAME", "GB28181_SIP");
+        properties.setProperty("javax.sip.IP_ADDRESS", host);
+        properties.setProperty("gov.nist.javax.sip.LOG_MESSAGE_CONTENT", "false");
+        /**
+         * sip_server_log.log 和 sip_debug_log.log public static final int TRACE_NONE =
+         * 0; public static final int TRACE_MESSAGES = 16; public static final int
+         * TRACE_EXCEPTION = 17; public static final int TRACE_DEBUG = 32;
+         */
+        properties.setProperty("gov.nist.javax.sip.TRACE_LEVEL", "0");
+        properties.setProperty("gov.nist.javax.sip.SERVER_LOG", "sip_server_log");
+        properties.setProperty("gov.nist.javax.sip.DEBUG_LOG", "sip_debug_log");
+        try {
+            return getSipFactory().createSipStack(properties);
+        } catch (PeerUnavailableException e) {
+            log.error("创建sipStack失败,",e);
+            throw new BusinessException("系统内部错误,创建失败");
+        }
+    }
+
+    public static SipProvider getSipProvider(RequestEvent evt) {
+        String url = evt.getRequest().getRequestURI().toString();
+        String[] split = url.split(":");
+        String port=split[split.length-1];
+        ViaHeader reqViaHeader = (ViaHeader) evt.getRequest().getHeader(ViaHeader.NAME);
+        String key=reqViaHeader.getTransport().toUpperCase()+port;
+        return tuple3Map._3().get(key) ;
+    }
+
+
+}

+ 216 - 0
jetlinks-manager/media-manager/src/main/java/org/jetlinks/community/media/sip/SipRequestProcessorParent.java

@@ -0,0 +1,216 @@
+package org.jetlinks.community.media.sip;
+
+
+import gov.nist.javax.sip.SipStackImpl;
+import gov.nist.javax.sip.message.SIPRequest;
+import gov.nist.javax.sip.stack.SIPServerTransaction;
+import lombok.extern.slf4j.Slf4j;
+import org.dom4j.Document;
+import org.dom4j.DocumentException;
+import org.dom4j.Element;
+import org.dom4j.io.SAXReader;
+import org.jetlinks.community.media.core.DigestServerAuthenticationHelper2016;
+import org.jetlinks.community.media.entity.SipServerConfig;
+import org.jetlinks.community.media.event.request.ISipRequestProcessor;
+import javax.sip.*;
+import javax.sip.address.Address;
+import javax.sip.address.AddressFactory;
+import javax.sip.address.SipURI;
+import javax.sip.header.ContentTypeHeader;
+import javax.sip.header.ExpiresHeader;
+import javax.sip.header.HeaderFactory;
+import javax.sip.header.ViaHeader;
+import javax.sip.message.MessageFactory;
+import javax.sip.message.Request;
+import javax.sip.message.Response;
+import java.io.ByteArrayInputStream;
+import java.security.NoSuchAlgorithmException;
+import java.text.ParseException;
+/**
+ * @description:处理接收IPCamera发来的SIP协议请求消息
+ * @author: songww
+ * @date:   2020年5月3日 下午4:42:22     
+ */
+@Slf4j
+public abstract class SipRequestProcessorParent extends ISipRequestProcessor {
+
+
+
+    /**
+     * 根据 RequestEvent 获取 ServerTransaction
+     * @param evt
+     * @return
+     */
+    public ServerTransaction getServerTransaction(RequestEvent evt) {
+        Request request = evt.getRequest();
+        ServerTransaction serverTransaction = evt.getServerTransaction();
+        // 判断TCP还是UDP
+        ViaHeader reqViaHeader = (ViaHeader) request.getHeader(ViaHeader.NAME);
+        String transport = reqViaHeader.getTransport();
+        if (serverTransaction == null) {
+            try {
+                SipStackImpl stack = (SipStackImpl)SipContext.getSipStack();
+                serverTransaction = (SIPServerTransaction) stack.findTransaction((SIPRequest)request, true);
+                if (serverTransaction == null) {
+//                    serverTransaction = SipContext.getSipProviders().getNewServerTransaction(request);
+                    serverTransaction = SipContext.getSipProvider(evt).getNewServerTransaction(request);
+                }
+            } catch (TransactionAlreadyExistsException e) {
+                log.error(e.getMessage());
+            } catch (TransactionUnavailableException e) {
+                log.error(e.getMessage());
+            }
+        }
+        return serverTransaction;
+    }
+
+    public AddressFactory getAddressFactory() {
+        try {
+            return SipFactory.getInstance().createAddressFactory();
+        } catch (PeerUnavailableException e) {
+            e.printStackTrace();
+        }
+        return null;
+    }
+
+    public HeaderFactory getHeaderFactory() {
+        try {
+            return SipFactory.getInstance().createHeaderFactory();
+        } catch (PeerUnavailableException e) {
+            e.printStackTrace();
+        }
+        return null;
+    }
+
+    public MessageFactory getMessageFactory() {
+        try {
+            return SipFactory.getInstance().createMessageFactory();
+        } catch (PeerUnavailableException e) {
+            e.printStackTrace();
+        }
+        return null;
+    }
+
+    /***
+     * 回复状态码
+     * 100 trying
+     * 200 OK
+     * 400
+     * 404
+     * @param evt
+     * @param statusCode Response.ACCEPTED
+     * @throws SipException
+     * @throws InvalidArgumentException
+     * @throws ParseException
+     */
+    public void responseAck(RequestEvent evt,int statusCode) throws SipException, InvalidArgumentException, ParseException {
+        Response response = getMessageFactory().createResponse(statusCode, evt.getRequest());
+        ServerTransaction serverTransaction = getServerTransaction(evt);
+        if (serverTransaction == null) {
+            log.warn("回复失败:{}", response);
+            return;
+        }
+        serverTransaction.sendResponse(response);
+        if (statusCode >= 200 && !Request.NOTIFY.equals(evt.getRequest().getMethod())) {
+            if (serverTransaction.getDialog() != null) serverTransaction.getDialog().delete();
+        }
+    }
+
+    public void responseAck(RequestEvent evt, int statusCode, String msg) throws SipException, InvalidArgumentException, ParseException {
+        Response response = getMessageFactory().createResponse(statusCode, evt.getRequest());
+        response.setReasonPhrase(msg);
+        ServerTransaction serverTransaction = getServerTransaction(evt);
+        serverTransaction.sendResponse(response);
+        if (statusCode >= 200 && !Request.NOTIFY.equals(evt.getRequest().getMethod())) {
+            if (serverTransaction.getDialog() != null) serverTransaction.getDialog().delete();
+        }
+    }
+
+    /**
+     * 回复带sdp的200
+     * @param evt
+     * @param sdp
+     * @throws SipException
+     * @throws InvalidArgumentException
+     * @throws ParseException
+     */
+    public void responseSdpAck(RequestEvent evt, String sdp) throws SipException, InvalidArgumentException, ParseException {
+        Response response = getMessageFactory().createResponse(Response.OK, evt.getRequest());
+        SipFactory sipFactory = SipFactory.getInstance();
+        ContentTypeHeader contentTypeHeader = sipFactory.createHeaderFactory().createContentTypeHeader("APPLICATION", "SDP");
+        response.setContent(sdp, contentTypeHeader);
+
+        SipURI sipUrl = (SipURI)evt.getRequest().getRequestURI();
+
+        Address concatAddress = sipFactory.createAddressFactory().createAddress(
+            sipFactory.createAddressFactory().createSipURI(sipUrl.getUser(),  sipUrl.getHost()+":"+sipUrl.getPort()
+            ));
+        response.addHeader(sipFactory.createHeaderFactory().createContactHeader(concatAddress));
+        getServerTransaction(evt).sendResponse(response);
+    }
+
+    /**
+     * 回复带xml的200
+     * @param evt
+     * @param xml
+     * @throws SipException
+     * @throws InvalidArgumentException
+     * @throws ParseException
+     */
+    public Response responseXmlAck(RequestEvent evt, String xml) throws SipException, InvalidArgumentException, ParseException {
+        Response response = getMessageFactory().createResponse(Response.OK, evt.getRequest());
+        SipFactory sipFactory = SipFactory.getInstance();
+        ContentTypeHeader contentTypeHeader = sipFactory.createHeaderFactory().createContentTypeHeader("APPLICATION", "MANSCDP+xml");
+        response.setContent(xml, contentTypeHeader);
+
+        SipURI sipUrl = (SipURI)evt.getRequest().getRequestURI();
+
+        Address concatAddress = sipFactory.createAddressFactory().createAddress(
+            sipFactory.createAddressFactory().createSipURI(sipUrl.getUser(),  sipUrl.getHost()+":"+sipUrl.getPort()
+            ));
+        response.addHeader(sipFactory.createHeaderFactory().createContactHeader(concatAddress));
+        response.addHeader(evt.getRequest().getHeader(ExpiresHeader.NAME));
+        getServerTransaction(evt).sendResponse(response);
+        return response;
+    }
+
+    public Element getRootElement(RequestEvent evt, String charset) throws DocumentException {
+        if (charset == null) {
+            throw new UnknownError("SipRequestProcessorParent.getRootElement(),charset is null");
+        };
+        Request request = evt.getRequest();
+        SAXReader reader = new SAXReader();
+        reader.setEncoding(charset);
+        Document xml = reader.read(new ByteArrayInputStream(request.getRawContent()));
+        return xml.getRootElement();
+    }
+
+
+    /**
+     * 当请求未授权时的基础响应
+     * 在WWW_Authenticate请求头中传输认证参数
+     * @return
+     */
+    public Response basicResponse(Request request, SipServerConfig sipConfig) throws NoSuchAlgorithmException, ParseException {
+        Response response=null;
+        log.info("[{}] 未携带授权头 回复401", request.getRequestURI());
+        response = getMessageFactory().createResponse(Response.UNAUTHORIZED, request);
+        new DigestServerAuthenticationHelper2016()
+            .generateChallenge(getHeaderFactory(), response, sipConfig.getDomain());
+        return response;
+    }
+
+
+    /**
+     * 对请求进行认证
+     * @param request
+     * @param sipConfig
+     * @return
+     * @throws NoSuchAlgorithmException
+     */
+    public boolean authRequest( Request request,SipServerConfig sipConfig) throws NoSuchAlgorithmException {
+        return new DigestServerAuthenticationHelper2016()
+            .doAuthenticatePlainTextPassword(request,
+                sipConfig.getPassword());
+    }
+}

+ 110 - 0
jetlinks-manager/media-manager/src/main/java/org/jetlinks/community/media/sip/SipServerHelper.java

@@ -0,0 +1,110 @@
+package org.jetlinks.community.media.sip;
+
+import gov.nist.javax.sip.SipProviderImpl;
+import lombok.AllArgsConstructor;
+import lombok.SneakyThrows;
+import lombok.extern.slf4j.Slf4j;
+import org.hswebframework.web.exception.BusinessException;
+import org.jetlinks.community.media.bean.EventResult;
+import org.jetlinks.community.media.bean.SSRCInfo;
+import org.jetlinks.community.media.entity.MediaDevice;
+import org.jetlinks.community.media.entity.SipServerConfig;
+import org.jetlinks.community.media.enums.StreamMode;
+import org.jetlinks.community.media.event.request.impl.InviteRequestProcessor;
+import org.jetlinks.community.media.event.request.impl.RegisterRequestProcessor;
+import org.jetlinks.community.media.event.request.message.MessageRequestProcessor;
+import org.jetlinks.community.media.event.request.message.notify.KeepaliveNotifyMessageHandler;
+import org.jetlinks.community.media.sip.processor.SipProcessorObserver;
+import org.jetlinks.community.media.transmit.SIPRequestHeaderProvider;
+import org.jetlinks.community.media.transmit.cmd.SipCommander;
+import org.springframework.beans.factory.config.BeanPostProcessor;
+import org.springframework.stereotype.Component;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+import javax.sip.*;
+import java.util.*;
+
+/**
+ * @author lifang
+ * @version 1.0.0
+ * @ClassName SipServerHelper.java
+ * @Description TODO
+ * @createTime 2022年01月15日 14:05:00
+ */
+@Component
+@Slf4j
+@AllArgsConstructor
+public class SipServerHelper implements BeanPostProcessor {
+
+
+    private final SipProcessorObserver sipProcessorObserver;
+
+
+    public Mono<SipProcessorObserver> createSip(SipServerConfig sipConfig){
+        sipConfig.validateCreate();
+        try {
+            SipContext.updateSipServerConfig(sipConfig);
+        } catch (TransportNotSupportedException e) {
+            return Mono.error( new BusinessException("不支持该协议[{"+sipConfig.getTransport()+"}],请从tcp、udp中选择"));
+        } catch (InvalidArgumentException e) {
+            return Mono.error( new BusinessException(String.format("无法使用 [ {%s}:{%s} ]作为SIP[ UDP ]服务,可排查: 1. sip.monitor-ip 是否为本机网卡IP; 2. sip.port 是否已被占用"
+                , sipConfig.getHost(), sipConfig.getPort())));
+
+        }
+        return Mono.just(sipProcessorObserver)
+            .doOnNext(observer->{
+                Map<String,SipProvider> sipProviders =  SipContext.getSipProviders();
+                if(sipProviders==null){
+                    log.error("sip启动失败");
+                    throw new BusinessException("sip启动失败");
+                }
+                for (SipProvider provider : sipProviders.values()) {
+                    SipListener sipListener = ((SipProviderImpl)provider).getSipListener();
+                    if(sipListener!=null){
+                        provider.removeSipListener(sipListener);
+                    }
+                    try {
+                        provider.addSipListener(sipProcessorObserver);
+                    } catch (Exception e) {
+                        log.warn("sipProvider 添加 sipListener 失败");
+                    }
+                }
+            });
+    }
+
+    @SneakyThrows
+    public static void main(String[] args) {
+        SipProcessorObserver sipProcessorObserver = new SipProcessorObserver();
+        RegisterRequestProcessor registerRequestProcessor = new RegisterRequestProcessor(null);
+        registerRequestProcessor.observer=sipProcessorObserver;
+        registerRequestProcessor.init();
+        InviteRequestProcessor inviteRequestProcessor = new InviteRequestProcessor();
+        inviteRequestProcessor.observer=sipProcessorObserver;
+        inviteRequestProcessor.init();
+        MessageRequestProcessor messageRequestProcessor = new MessageRequestProcessor();
+        messageRequestProcessor.observer=sipProcessorObserver;
+        messageRequestProcessor.init();
+        KeepaliveNotifyMessageHandler keepaliveNotifyMessageHandler = new KeepaliveNotifyMessageHandler();
+        keepaliveNotifyMessageHandler.observer=sipProcessorObserver;
+        keepaliveNotifyMessageHandler.init();
+        keepaliveNotifyMessageHandler.messageRequestProcessor=messageRequestProcessor;
+        keepaliveNotifyMessageHandler.start();
+        new SipServerHelper(sipProcessorObserver).
+            createSip( SipServerConfig.of("1","0.0.0.0", 7000,"udp","340200000","utf-8","12345678",10L,""))
+            .subscribe();
+        SipCommander sipCommander = new SipCommander(new SIPRequestHeaderProvider());
+        SSRCInfo ssrcInfo = SSRCInfo.of(5003,null,null);
+        MediaDevice mediaDevice = new MediaDevice();
+        mediaDevice.setStreamMode(StreamMode.TCP_ACTIVE.name());
+        mediaDevice.setId("34020000001320000003");
+        mediaDevice.setHostAddress("192.168.104.244");
+        mediaDevice.setPort(5063);
+        mediaDevice.setTransport("udp");
+        Flux<EventResult> eventResultFlux = sipCommander.playStreamCmd(null, ssrcInfo,mediaDevice, "34020000001320000003");
+        eventResultFlux
+            .doOnNext(s->{
+                System.out.println(s);
+            })
+            .subscribe();
+    }
+}

+ 27 - 0
jetlinks-manager/media-manager/src/main/java/org/jetlinks/community/media/sip/processor/ISipProcessObserver.java

@@ -0,0 +1,27 @@
+package org.jetlinks.community.media.sip.processor;
+
+import reactor.core.publisher.Flux;
+
+import javax.sip.*;
+
+/**
+ * @author lifang
+ * @version 1.0.0
+ * @ClassName ISipProcessObserver.java
+ * @Description 监听sip服务器所接收到的消息
+ * @createTime 2022年01月15日 16:32:00
+ * @see  SipProcessorObserver
+ */
+public interface ISipProcessObserver extends SipListener {
+    Flux<RequestEvent> handleRequestEvent();
+
+    Flux<ResponseEvent> handleResponseEvent();
+
+    Flux<TimeoutEvent> handleTimeoutEvent();
+
+    Flux<IOExceptionEvent> handleIOExceptionEvent();
+
+    Flux<TransactionTerminatedEvent> handleTransactionTerminatedEvent();
+
+    Flux<DialogTerminatedEvent> handleDialogTerminatedEvent();
+}

+ 133 - 0
jetlinks-manager/media-manager/src/main/java/org/jetlinks/community/media/sip/processor/SipProcessorObserver.java

@@ -0,0 +1,133 @@
+package org.jetlinks.community.media.sip.processor;
+
+import gov.nist.javax.sip.parser.HeaderParser;
+import gov.nist.javax.sip.parser.ParserFactory;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Component;
+import reactor.core.publisher.EmitterProcessor;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.FluxSink;
+import reactor.core.publisher.UnicastProcessor;
+
+import javax.sip.*;
+import javax.sip.header.AuthorizationHeader;
+import javax.sip.message.Request;
+import java.text.ParseException;
+import java.util.ListIterator;
+import java.util.function.Function;
+
+/**
+ * @description: SIP信令处理类观察者
+ * @author: lifang
+ * @date:   2021年11月5日 下午15:32
+ */
+@Component
+@Slf4j
+public class SipProcessorObserver implements ISipProcessObserver {
+
+
+    private EmitterProcessor<RequestEvent> requestProcessor= EmitterProcessor.create();
+    private FluxSink<RequestEvent> requestSink=requestProcessor.sink();
+
+    private EmitterProcessor<ResponseEvent> responseProcessor=EmitterProcessor.create();
+    private FluxSink<ResponseEvent> responseSink=responseProcessor.sink();
+
+    private EmitterProcessor<TimeoutEvent> timeoutProcessor=EmitterProcessor.create();
+    private FluxSink<TimeoutEvent> timeoutSink=timeoutProcessor.sink();
+
+    private EmitterProcessor<IOExceptionEvent> ioExceptionProcessor =EmitterProcessor.create();
+    private FluxSink<IOExceptionEvent> ioExceptionSink = ioExceptionProcessor.sink();
+
+    private EmitterProcessor<TransactionTerminatedEvent> transactionTerminatedProcessor=EmitterProcessor.create();
+    private FluxSink<TransactionTerminatedEvent> transactionTerminatedSink=transactionTerminatedProcessor.sink();
+
+    private EmitterProcessor<DialogTerminatedEvent> dialogTerminatedProcessor=EmitterProcessor.create();
+    private FluxSink<DialogTerminatedEvent> dialogTerminatedSink=dialogTerminatedProcessor.sink();
+
+
+
+    /**
+     * 分发RequestEvent事件
+     * @param requestEvent RequestEvent事件
+     */
+    @Override
+    public void processRequest(RequestEvent requestEvent) {
+        if(requestProcessor.hasDownstreams()){
+            requestSink.next(requestEvent);
+        }
+    }
+
+    /**
+     * 分发ResponseEvent事件
+     * @param responseEvent responseEvent事件
+     */
+    @Override
+    public void processResponse(ResponseEvent responseEvent) {
+        if(responseProcessor.hasDownstreams()){
+            responseSink.next(responseEvent);
+        }
+    }
+
+    /**
+     * 向超时订阅发送消息
+     * @param timeoutEvent timeoutEvent事件
+     */
+    @Override
+    public void processTimeout(TimeoutEvent timeoutEvent) {
+        if(timeoutProcessor.hasDownstreams()){
+            timeoutSink.next(timeoutEvent);
+        }
+    }
+
+    @Override
+    public void processIOException(IOExceptionEvent exceptionEvent) {
+        if(ioExceptionProcessor.hasDownstreams()){
+            ioExceptionSink.next(exceptionEvent);
+        }
+    }
+
+    @Override
+    public void processTransactionTerminated(TransactionTerminatedEvent transactionTerminatedEvent) {
+        if(transactionTerminatedProcessor.hasDownstreams()){
+            transactionTerminatedSink.next(transactionTerminatedEvent);
+        }
+    }
+
+    @Override
+    public void processDialogTerminated(DialogTerminatedEvent dialogTerminatedEvent) {
+        if(dialogTerminatedProcessor.hasDownstreams()){
+            dialogTerminatedSink.next(dialogTerminatedEvent);
+        }
+    }
+
+
+    @Override
+    public Flux<RequestEvent> handleRequestEvent() {
+        return requestProcessor.map(Function.identity());
+    }
+
+    @Override
+    public Flux<ResponseEvent> handleResponseEvent() {
+        return responseProcessor.map(Function.identity());
+    }
+
+    @Override
+    public Flux<TimeoutEvent> handleTimeoutEvent() {
+        return timeoutProcessor.map(Function.identity());
+    }
+
+    @Override
+    public Flux<IOExceptionEvent> handleIOExceptionEvent() {
+        return ioExceptionProcessor.map(Function.identity());
+    }
+
+    @Override
+    public Flux<TransactionTerminatedEvent> handleTransactionTerminatedEvent() {
+        return transactionTerminatedProcessor.map(Function.identity());
+    }
+
+    @Override
+    public Flux<DialogTerminatedEvent> handleDialogTerminatedEvent() {
+        return dialogTerminatedProcessor.map(Function.identity());
+    }
+}

+ 210 - 0
jetlinks-manager/media-manager/src/main/java/org/jetlinks/community/media/storage/IRedisCatchStorage.java

@@ -0,0 +1,210 @@
+package org.jetlinks.community.media.storage;
+
+import com.alibaba.fastjson.JSONObject;
+import org.jetlinks.community.media.bean.StreamInfo;
+import org.jetlinks.community.media.entity.MediaDevice;
+import org.jetlinks.community.media.entity.ParentPlatform;
+import org.jetlinks.community.media.zlm.dto.MediaItem;
+
+import java.util.List;
+import java.util.Map;
+
+public interface IRedisCatchStorage {
+
+    /**
+     * 计数器。为cseq进行计数
+     *
+     * @param method sip 方法
+     * @return
+     */
+    Long getCSEQ(String method);
+
+    /**
+     * 开始播放时将流存入
+     *
+     * @param stream 流信息
+     * @return
+     */
+    boolean startPlay(StreamInfo stream);
+
+
+    /**
+     * 停止播放时删除
+     *
+     * @return
+     */
+    boolean stopPlay(StreamInfo streamInfo);
+
+    /**
+     * 查询播放列表
+     * @return
+     */
+    StreamInfo queryPlay(StreamInfo streamInfo);
+
+    StreamInfo queryPlayByStreamId(String steamId);
+
+    StreamInfo queryPlaybackByStreamId(String steamId);
+
+    StreamInfo queryPlayByDevice(String deviceId, String channelId);
+
+    Map<String, StreamInfo> queryPlayByDeviceId(String deviceId);
+
+    boolean startPlayback(StreamInfo stream);
+
+    boolean stopPlayback(StreamInfo streamInfo);
+
+    StreamInfo queryPlaybackByDevice(String deviceId, String code);
+
+//    void updatePlatformCatchInfo(ParentPlatformCatch parentPlatformCatch);
+//
+//    ParentPlatformCatch queryPlatformCatchInfo(String platformGbId);
+
+    void delPlatformCatchInfo(String platformGbId);
+
+    void updatePlatformKeepalive(ParentPlatform parentPlatform);
+
+    void delPlatformKeepalive(String platformGbId);
+
+    void updatePlatformRegister(ParentPlatform parentPlatform);
+
+    void delPlatformRegister(String platformGbId);
+
+    void updatePlatformRegisterInfo(String callId, String platformGbId);
+
+    String queryPlatformRegisterInfo(String callId);
+
+    void delPlatformRegisterInfo(String callId);
+
+    void cleanPlatformRegisterInfos();
+
+//    void updateSendRTPSever(SendRtpItem sendRtpItem);
+
+    /**
+     * 查询RTP推送信息缓存
+     * @param platformGbId
+     * @param channelId
+     * @return sendRtpItem
+     */
+//    SendRtpItem querySendRTPServer(String platformGbId, String channelId);
+//
+//    List<SendRtpItem> querySendRTPServer(String platformGbId);
+
+    /**
+     * 删除RTP推送信息缓存
+     * @param platformGbId
+     * @param channelId
+     */
+    void deleteSendRTPServer(String platformGbId, String channelId);
+
+    /**
+     * 查询某个通道是否存在上级点播(RTP推送)
+     * @param channelId
+     */
+    boolean isChannelSendingRTP(String channelId);
+
+    /**
+     * 清空某个设备的所有缓存
+     * @param deviceId 设备ID
+     */
+    void clearCatchByDeviceId(String deviceId);
+
+    /**
+     * 获取mediaServer节点
+     * @param mediaServerId
+     * @return
+     */
+//    MediaServerItem getMediaInfo(String mediaServerId);
+
+    /**
+     * 设置所有设备离线
+     */
+    void outlineForAll();
+
+    /**
+     * 获取所有在线的
+     */
+    List<String> getOnlineForAll();
+
+    /**
+     * 在redis添加wvp的信息
+     */
+    void updateWVPInfo(JSONObject jsonObject, int time);
+
+    /**
+     * 发送推流生成与推流消失消息
+     * @param jsonObject 消息内容
+     */
+    void sendStreamChangeMsg(String type, JSONObject jsonObject);
+
+    /**
+     * 添加流信息到redis
+     * @param mediaServerItem
+     * @param app
+     * @param streamId
+     */
+//    void addStream(MediaServerItem mediaServerItem, String type, String app, String streamId, MediaItem item);
+
+    /**
+     * 移除流信息从redis
+     * @param mediaServerId
+     * @param app
+     * @param streamId
+     */
+    void removeStream(String mediaServerId, String type, String app, String streamId);
+
+
+    /**
+     * 移除流信息从redis
+     * @param mediaServerId
+     */
+    void removeStream(String mediaServerId, String type);
+
+    /**
+     * 开始下载录像时存入
+     * @param streamInfo
+     */
+    boolean startDownload(StreamInfo streamInfo);
+
+    StreamInfo queryDownloadByStreamId(String streamId);
+
+//    /**
+//     * 查找第三方系统留下的国标预设值
+//     * @param queryKey
+//     * @return
+//     */
+//    ThirdPartyGB queryMemberNoGBId(String queryKey);
+
+    List<MediaItem> getStreams(String mediaServerId, String pull);
+
+    /**
+     * 将device信息写入redis
+     * @param device
+     */
+    void updateDevice(MediaDevice device);
+
+    void removeDevice(String deviceId);
+
+    /**
+     * 获取Device
+     */
+    MediaDevice getDevice(String deviceId);
+
+    void resetAllCSEQ();
+
+//    void updateGpsMsgInfo(GPSMsgInfo gpsMsgInfo);
+
+//    GPSMsgInfo getGpsMsgInfo(String gbId);
+//    List<GPSMsgInfo> getAllGpsMsgInfo();
+
+    Long getSN(String method);
+
+    void resetAllSN();
+
+//    void updateSubscribe(String key, SubscribeInfo subscribeInfo);
+
+//    SubscribeInfo getSubscribe(String key);
+
+    void delSubscribe(String key);
+
+    MediaItem getStreamInfo(String app, String streamId, String mediaServerId);
+}

+ 458 - 0
jetlinks-manager/media-manager/src/main/java/org/jetlinks/community/media/storage/IVideoManagerStorager.java

@@ -0,0 +1,458 @@
+package org.jetlinks.community.media.storage;
+
+import org.jetlinks.community.media.entity.DeviceChannel;
+import org.jetlinks.community.media.entity.MediaDevice;
+import org.jetlinks.community.media.entity.ParentPlatform;
+
+import java.util.List;
+
+/**    
+ * @description:视频设备数据存储接口
+ * @author: swwheihei
+ * @date:   2020年5月6日 下午2:14:31     
+ */
+@SuppressWarnings("rawtypes")
+public interface IVideoManagerStorager {
+
+	/**   
+	 * 根据设备ID判断设备是否存在
+	 * 
+	 * @param deviceId 设备ID
+	 * @return true:存在  false:不存在
+	 */
+	public boolean exists(String deviceId);
+	
+	/**   
+	 * 视频设备创建
+	 * 
+	 * @param device 设备对象
+	 * @return true:创建成功  false:创建失败
+	 */
+	public boolean create(MediaDevice device);
+	
+	/**   
+	 * 视频设备更新
+	 * 
+	 * @param device 设备对象
+	 * @return true:创建成功  false:创建失败
+	 */
+	public boolean updateDevice(MediaDevice device);
+
+	/**
+	 * 添加设备通道
+	 *
+	 * @param deviceId 设备id
+	 * @param channel 通道
+	 */
+	public void updateChannel(String deviceId, DeviceChannel channel);
+
+	/**
+	 * 批量添加设备通道
+	 *
+	 * @param deviceId 设备id
+	 * @param channels 多个通道
+	 */
+	public int updateChannels(String deviceId, List<DeviceChannel> channels);
+
+	/**
+	 * 开始播放
+	 * @param deviceId 设备id
+	 * @param channelId 通道ID
+	 * @param streamId 流地址
+	 */
+	public void startPlay(String deviceId, String channelId, String streamId);
+
+	/**
+	 * 停止播放
+	 * @param deviceId 设备id
+	 * @param channelId 通道ID
+	 */
+	public void stopPlay(String deviceId, String channelId);
+	
+	/**   
+	 * 获取设备
+	 * 
+	 * @param deviceId 设备ID
+	 * @return DShadow 设备对象
+	 */
+	public MediaDevice queryVideoDevice(String deviceId);
+
+//	/**
+//	 * 获取某个设备的通道列表
+//	 *
+//	 * @param id 设备ID
+//	 * @param page 分页 当前页
+//	 * @param count 每页数量
+//	 * @return
+//	 */
+//	public PageInfo queryChannelsByDeviceId(String id, String query, Boolean hasSubChannel, Boolean online, int page, int count);
+	
+	public List<DeviceChannel> queryChannelsByDeviceIdWithStartAndLimit(String deviceId, String query, Boolean hasSubChannel, Boolean online, int start, int limit);
+
+	/**
+	 * 获取某个设备的通道列表
+	 *
+	 * @param id 设备ID
+	 * @return
+	 */
+//	public List<DeviceChannel> queryChannelsByDeviceId(String id);
+
+	/**
+	 * 获取某个设备的通道
+	 * @param id 设备ID
+	 * @param channelId 通道ID
+	 */
+//	public DeviceChannel queryChannel(String id, String channelId);
+
+	/**
+	 * 删除通道
+	 * @param deviceId 设备ID
+	 * @param channelId 通道ID
+	 */
+	public int delChannel(String deviceId, String channelId);
+
+	/**
+	 * 获取多个设备
+	 * @param page 当前页数
+	 * @param count 每页数量
+	 * @return List<Device> 设备对象数组
+	 */
+//	public PageInfo<MediaDevice> queryVideoDeviceList(int page, int count);
+
+	/**
+	 * 获取多个设备
+	 *
+	 * @return List<Device> 设备对象数组
+	 */
+	public List<MediaDevice> queryVideoDeviceList();
+
+	/**   
+	 * 删除设备
+	 * 
+	 * @param deviceId 设备ID
+	 * @return true:删除成功  false:删除失败
+	 */
+	public boolean delete(String deviceId);
+	
+	/**   
+	 * 更新设备在线
+	 * 
+	 * @param deviceId 设备ID
+	 * @return true:更新成功  false:更新失败
+	 */
+	public boolean online(String deviceId);
+	
+	/**   
+	 * 更新设备离线
+	 * 
+	 * @param deviceId 设备ID
+	 * @return true:更新成功  false:更新失败
+	 */
+	public boolean outline(String deviceId);
+
+	/**
+	 * 更新所有设备离线
+	 *
+	 * @return true:更新成功  false:更新失败
+	 */
+	public boolean outlineForAll();
+
+
+//	/**
+//	 * 查询子设备
+//	 *
+//	 * @param id
+//	 * @param channelId
+//	 * @param page
+//	 * @param count
+//	 * @return
+//	 */
+//	PageInfo querySubChannels(String id, String channelId, String query, Boolean hasSubChannel, String online, int page, int count);
+//
+//
+//	/**
+//	 * 清空通道
+//	 * @param id
+//	 */
+//	void cleanChannelsForDevice(String id);
+//
+//
+//	/**
+//	 * 更新上级平台
+//	 * @param parentPlatform
+//	 */
+//	boolean updateParentPlatform(ParentPlatform parentPlatform);
+//
+//
+//	/**
+//	 * 添加上级平台
+//	 * @param parentPlatform
+//	 */
+//	boolean addParentPlatform(ParentPlatform parentPlatform);
+//
+//	/**
+//	 * 删除上级平台
+//	 * @param parentPlatform
+//	 */
+//	boolean deleteParentPlatform(ParentPlatform parentPlatform);
+//
+//
+//	/**
+//	 * 分页获取上级平台
+//	 * @param page
+//	 * @param count
+//	 * @return
+//	 */
+//	PageInfo<ParentPlatform> queryParentPlatformList(int page, int count);
+//
+//	/**
+//	 * 获取所有已启用的平台
+//	 * @return
+//	 */
+//	List<ParentPlatform> queryEnableParentPlatformList(boolean enable);
+//
+//	/**
+//	 * 获取上级平台
+//	 * @param platformGbId
+//	 * @return
+//	 */
+	ParentPlatform queryParentPlatByServerGBId(String platformGbId);
+//
+//	/**
+//	 * 所有平台离线
+//	 */
+//	void outlineForAllParentPlatform();
+//
+//	/**
+//	 * 查询通道信息,不区分设备(已关联平台或全部)
+//	 */
+//	PageInfo<ChannelReduce> queryAllChannelList(int page, int count, String query, Boolean online, Boolean channelType, String platformId, Boolean inPlatform);
+//
+//	/**
+//	 * 查询设备的通道信息
+//	 */
+//	List<ChannelReduce> queryChannelListInParentPlatform(String platformId);
+//
+//
+//	/**
+//	 * 更新上级平台的通道信息
+//	 * @param platformId
+//	 * @param channelReduces
+//	 * @return
+//	 */
+//	int updateChannelForGB(String platformId, List<ChannelReduce> channelReduces, String catalogId);
+//
+//	/**
+//	 *  移除上级平台的通道信息
+//	 * @param platformId
+//	 * @param channelReduces
+//	 * @return
+//	 */
+//	int delChannelForGB(String platformId, List<ChannelReduce> channelReduces);
+//
+//
+//    DeviceChannel queryChannelInParentPlatform(String platformId, String channelId);
+//
+//    List<PlatformCatalog> queryChannelInParentPlatformAndCatalog(String platformId, String catalogId);
+//    List<PlatformCatalog> queryStreamInParentPlatformAndCatalog(String platformId, String catalogId);
+//
+//    MediaDevice queryVideoDeviceByPlatformIdAndChannelId(String platformId, String channelId);
+//
+//
+//	/**
+//	 * 添加Mobile Position设备移动位置
+//	 * @param mobilePosition
+//	 * @return
+//	 */
+//	public boolean insertMobilePosition(MobilePosition mobilePosition);
+//
+//	/**
+//	 * 查询移动位置轨迹
+//	 * @param id
+//	 * @param startTime
+//	 * @param endTime
+//	 */
+//	public List<MobilePosition> queryMobilePositions(String id, String startTime, String endTime);
+//
+//	/**
+//	 * 查询最新移动位置
+//	 * @param id
+//	 */
+//	public MobilePosition queryLatestPosition(String id);
+//
+//	/**
+//	 * 删除指定设备的所有移动位置
+//	 * @param id
+//	 */
+//	public int clearMobilePositionsByDeviceId(String id);
+//
+//	/**
+//	 * 新增代理流
+//	 * @param streamProxyDto
+//	 * @return
+//	 */
+//	public boolean addStreamProxy(StreamProxyItem streamProxyDto);
+//
+//	/**
+//	 * 更新代理流
+//	 * @param streamProxyDto
+//	 * @return
+//	 */
+//	public boolean updateStreamProxy(StreamProxyItem streamProxyDto);
+//
+//	/**
+//	 * 移除代理流
+//	 * @param app
+//	 * @param stream
+//	 * @return
+//	 */
+//	public int deleteStreamProxy(String app, String stream);
+//
+//	/**
+//	 * 按照是否启用获取代理流
+//	 * @param enable
+//	 * @return
+//	 */
+//	public List<StreamProxyItem> getStreamProxyListForEnable(boolean enable);
+//
+//	/**
+//	 * 按照是app和stream获取代理流
+//	 * @param app
+//	 * @param stream
+//	 * @return
+//	 */
+//	public StreamProxyItem queryStreamProxy(String app, String stream);
+//
+//	/**
+//	 * 获取代理流
+//	 * @param page
+//	 * @param count
+//	 * @return
+//	 */
+//	PageInfo<StreamProxyItem> queryStreamProxyList(Integer page, Integer count);
+//
+//	/**
+//	 * 根据国标ID获取平台关联的直播流
+//	 * @param platformId
+//	 * @param channelId
+//	 * @return
+//	 */
+//	List<GbStream> queryStreamInParentPlatform(String platformId, String channelId);
+//
+//	/**
+//	 * 获取平台关联的直播流
+//	 * @param platformId
+//	 * @return
+//	 */
+//	List<GbStream> queryGbStreamListInPlatform(String platformId);
+//
+//	/**
+//	 * 批量更新推流列表
+//	 * @param streamPushItems
+//	 */
+//	void updateMediaList(List<StreamPushItem> streamPushItems);
+//
+//	/**
+//	 * 更新单个推流
+//	 * @param streamPushItem
+//	 */
+//	void updateMedia(StreamPushItem streamPushItem);
+//
+//	/**
+//	 * 移除单个推流
+//	 * @param app
+//	 * @param stream
+//	 */
+//	int removeMedia(String app, String stream);
+//
+//
+//	/**
+//	 * 清空推流列表
+//	 */
+//	void clearMediaList();
+//
+//	/**
+//	 * 设置流离线
+//	 * @param app
+//	 * @param streamId
+//	 */
+//	int mediaOutline(String app, String streamId);
+//
+//	/**
+//	 * 设置平台在线/离线
+//	 * @param online
+//	 */
+//	void updateParentPlatformStatus(String platformGbID, boolean online);
+//
+//	/**
+//	 * 更新媒体节点
+//	 * @param mediaServerItem
+//	 */
+//	void updateMediaServer(MediaServerItem mediaServerItem);
+//
+//	/**
+//	 * 根据媒体ID获取启用/不启用的代理列表
+//	 * @param id 媒体ID
+//	 * @param b 启用/不启用
+//	 * @return
+//	 */
+//	List<StreamProxyItem> getStreamProxyListForEnableInMediaServer(String id, boolean b);
+//
+//	/**
+//	 * 根据通道ID获取其所在设备
+//	 * @param channelId  通道ID
+//	 * @return
+//	 */
+//    Device queryVideoDeviceByChannelId(String channelId);
+//
+//	/**
+//	 * 通道上线
+//	 * @param channelId 通道ID
+//	 */
+//	void deviceChannelOnline(String id, String channelId);
+//
+//	/**
+//	 * 通道离线
+//	 * @param channelId 通道ID
+//	 */
+//	void deviceChannelOffline(String id, String channelId);
+//
+//	/**
+//	 * 通过app与stream获取StreamProxy
+//	 * @param app
+//	 * @param streamId
+//	 * @return
+//	 */
+//    StreamProxyItem getStreamProxyByAppAndStream(String app, String streamId);
+//
+//	/**
+//	 * catlog查询结束后完全重写通道信息
+//	 * @param id
+//	 * @param deviceChannelList
+//	 */
+//	boolean resetChannels(String id, List<DeviceChannel> deviceChannelList);
+//
+//	/**
+//	 * 获取目录信息
+//	 * @param platformId
+//	 * @param parentId
+//	 * @return
+//	 */
+//    List<PlatformCatalog> getChildrenCatalogByPlatform(String platformId, String parentId);
+//
+//	int addCatalog(PlatformCatalog platformCatalog);
+//
+//	PlatformCatalog getCatalog(String id);
+//
+//	int delCatalog(String id);
+//
+//	int updateCatalog(PlatformCatalog platformCatalog);
+//
+//	int setDefaultCatalog(String platformId, String catalogId);
+//
+//	List<PlatformCatalog> queryCatalogInPlatform(String serverGBId);
+//
+//    int delRelation(PlatformCatalog platformCatalog);
+//
+//	int updateStreamGPS(List<GPSMsgInfo> gpsMsgInfo);
+}

+ 516 - 0
jetlinks-manager/media-manager/src/main/java/org/jetlinks/community/media/storage/impl/RedisCatchStorageImpl.java

@@ -0,0 +1,516 @@
+//package org.jetlinks.community.media.storage.impl;
+//
+//import com.alibaba.fastjson.JSONObject;
+//import lombok.extern.slf4j.Slf4j;
+//import org.jetlinks.community.media.bean.StreamInfo;
+//import org.jetlinks.community.media.contanst.VideoManagerConstants;
+//import org.jetlinks.community.media.entity.DeviceChannel;
+//import org.jetlinks.community.media.entity.MediaDevice;
+//import org.jetlinks.community.media.entity.ParentPlatform;
+//import org.jetlinks.community.media.storage.IRedisCatchStorage;
+//import org.jetlinks.community.media.zlm.dto.MediaItem;
+//import org.jetlinks.community.media.zlm.dto.MediaServerItem;
+//import org.jetlinks.community.utils.RedisUtil;
+//import org.springframework.beans.factory.annotation.Autowired;
+//import org.springframework.stereotype.Component;
+//
+//import java.text.SimpleDateFormat;
+//import java.util.ArrayList;
+//import java.util.HashMap;
+//import java.util.List;
+//import java.util.Map;
+//
+//@SuppressWarnings("rawtypes")
+//@Component
+//@Slf4j
+//public class RedisCatchStorageImpl implements IRedisCatchStorage {
+//
+//
+//    @Autowired
+//	private final RedisUtil redis;
+//
+//    @Autowired
+//    private final DeviceChannelMapper deviceChannelMapper;
+//
+//    @Autowired
+//    private final UserSetup userSetup;
+//
+//    @Autowired
+//    public RedisCatchStorageImpl(RedisUtil redis, DeviceChannelMapper deviceChannelMapper, UserSetup userSetup) {
+//        this.redis = redis;
+//        this.deviceChannelMapper = deviceChannelMapper;
+//        this.userSetup = userSetup;
+//    }
+//
+//    private SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
+//
+//    @Override
+//    public Long getCSEQ(String method) {
+//        String key = VideoManagerConstants.SIP_CSEQ_PREFIX  + userSetup.getServerId() + "_" +  method;
+//
+//        long result =  redis.incr(key, 1L);
+//        if (result > Integer.MAX_VALUE) {
+//            redis.set(key, 1);
+//            result = 1;
+//        }
+//        return result;
+//    }
+//
+//    @Override
+//    public Long getSN(String method) {
+//        String key = VideoManagerConstants.SIP_SN_PREFIX  + userSetup.getServerId() + "_" +  method;
+//
+//        long result =  redis.incr(key, 1L);
+//        if (result > Integer.MAX_VALUE) {
+//            redis.set(key, 1);
+//            result = 1;
+//        }
+//        return result;
+//    }
+//
+//    @Override
+//    public void resetAllCSEQ() {
+//        String scanKey = VideoManagerConstants.SIP_CSEQ_PREFIX  + userSetup.getServerId() + "_*";
+//        List<Object> keys = redis.scan(scanKey);
+//        for (int i = 0; i < keys.size(); i++) {
+//            String key = (String) keys.get(i);
+//            redis.set(key, 1);
+//        }
+//    }
+//
+//    @Override
+//    public void resetAllSN() {
+//        String scanKey = VideoManagerConstants.SIP_SN_PREFIX  + userSetup.getServerId() + "_*";
+//        List<Object> keys = redis.scan(scanKey);
+//        for (int i = 0; i < keys.size(); i++) {
+//            String key = (String) keys.get(i);
+//            redis.set(key, 1);
+//        }
+//    }
+//
+//    /**
+//     * 开始播放时将流存入redis
+//     *
+//     * @return
+//     */
+//    @Override
+//    public boolean startPlay(StreamInfo stream) {
+//        return redis.set(String.format("%S_%S_%s_%s_%s", VideoManagerConstants.PLAYER_PREFIX, userSetup.getServerId(), stream.getStreamId(),stream.getDeviceID(), stream.getChannelId()),
+//                stream);
+//    }
+//
+//    /**
+//     * 停止播放时从redis删除
+//     *
+//     * @return
+//     */
+//    @Override
+//    public boolean stopPlay(StreamInfo streamInfo) {
+//        if (streamInfo == null) return false;
+//        return redis.del(String.format("%S_%s_%s_%s_%s", VideoManagerConstants.PLAYER_PREFIX,
+//                userSetup.getServerId(),
+//                streamInfo.getStreamId(),
+//                streamInfo.getDeviceID(),
+//                streamInfo.getChannelId()));
+//    }
+//
+//    /**
+//     * 查询播放列表
+//     * @return
+//     */
+//    @Override
+//    public StreamInfo queryPlay(StreamInfo streamInfo) {
+//        return (StreamInfo)redis.get(String.format("%S_%s_%s_%s_%s",
+//                VideoManagerConstants.PLAYER_PREFIX,
+//                userSetup.getServerId(),
+//                streamInfo.getStreamId(),
+//                streamInfo.getDeviceID(),
+//                streamInfo.getChannelId()));
+//    }
+//    @Override
+//    public StreamInfo queryPlayByStreamId(String streamId) {
+//        List<Object> playLeys = redis.scan(String.format("%S_%s_%s_*", VideoManagerConstants.PLAYER_PREFIX, userSetup.getServerId(), streamId));
+//        if (playLeys == null || playLeys.size() == 0) return null;
+//        return (StreamInfo)redis.get(playLeys.get(0).toString());
+//    }
+//
+//    @Override
+//    public StreamInfo queryPlaybackByStreamId(String streamId) {
+//        List<Object> playLeys = redis.scan(String.format("%S_%s_%s_*", VideoManagerConstants.PLAY_BLACK_PREFIX, userSetup.getServerId(),  streamId));
+//        if (playLeys == null || playLeys.size() == 0) return null;
+//        return (StreamInfo)redis.get(playLeys.get(0).toString());
+//    }
+//
+//    @Override
+//    public StreamInfo queryPlayByDevice(String deviceId, String channelId) {
+////		List<Object> playLeys = redis.keys(String.format("%S_*_%s_%s", VideoManagerConstants.PLAYER_PREFIX,
+//        List<Object> playLeys = redis.scan(String.format("%S_%s_*_%s_%s", VideoManagerConstants.PLAYER_PREFIX,
+//                userSetup.getServerId(),
+//                deviceId,
+//                channelId));
+//        if (playLeys == null || playLeys.size() == 0) return null;
+//        return (StreamInfo)redis.get(playLeys.get(0).toString());
+//    }
+//
+//    @Override
+//    public Map<String, StreamInfo> queryPlayByDeviceId(String deviceId) {
+//        Map<String, StreamInfo> streamInfos = new HashMap<>();
+////		List<Object> playLeys = redis.keys(String.format("%S_*_%S_*", VideoManagerConstants.PLAYER_PREFIX, deviceId));
+//        List<Object> players = redis.scan(String.format("%S_%s_*_%S_*", VideoManagerConstants.PLAYER_PREFIX, userSetup.getServerId(),deviceId));
+//        if (players.size() == 0) return streamInfos;
+//        for (int i = 0; i < players.size(); i++) {
+//            String key = (String) players.get(i);
+//            StreamInfo streamInfo = (StreamInfo)redis.get(key);
+//            streamInfos.put(streamInfo.getDeviceID() + "_" + streamInfo.getChannelId(), streamInfo);
+//        }
+//        return streamInfos;
+//    }
+//
+//
+//    @Override
+//    public boolean startPlayback(StreamInfo stream) {
+//        return redis.set(String.format("%S_%s_%s_%s_%s", VideoManagerConstants.PLAY_BLACK_PREFIX, userSetup.getServerId(),stream.getStreamId(),
+//                        stream.getDeviceID(), stream.getChannelId()), stream);
+//    }
+//
+//    @Override
+//    public boolean startDownload(StreamInfo streamInfo) {
+//        return redis.set(String.format("%S_%s_%s_%s_%s", VideoManagerConstants.DOWNLOAD_PREFIX, userSetup.getServerId(),streamInfo.getStreamId(),
+//                        streamInfo.getDeviceID(), streamInfo.getChannelId()), streamInfo);
+//    }
+//
+//    @Override
+//    public boolean stopPlayback(StreamInfo streamInfo) {
+//        if (streamInfo == null) return false;
+//        DeviceChannel deviceChannel = deviceChannelMapper.queryChannel(streamInfo.getDeviceID(), streamInfo.getChannelId());
+//        if (deviceChannel != null) {
+//            deviceChannel.setStreamId(null);
+//            deviceChannel.setDeviceId(streamInfo.getDeviceID());
+//            deviceChannelMapper.update(deviceChannel);
+//        }
+//        return redis.del(String.format("%S_%s_%s_%s_%s", VideoManagerConstants.PLAY_BLACK_PREFIX,
+//                userSetup.getServerId(),
+//                streamInfo.getStreamId(),
+//                streamInfo.getDeviceID(),
+//                streamInfo.getChannelId()));
+//    }
+//
+//    @Override
+//    public StreamInfo queryPlaybackByDevice(String deviceId, String code) {
+//        // String format = String.format("%S_*_%s_%s", VideoManagerConstants.PLAY_BLACK_PREFIX,
+//        //         deviceId,
+//        //         code);
+//        List<Object> playLeys = redis.scan(String.format("%S_%s_*_%s_%s", VideoManagerConstants.PLAY_BLACK_PREFIX,
+//                userSetup.getServerId(),
+//                deviceId,
+//                code));
+//        if (playLeys == null || playLeys.size() == 0) {
+//            playLeys = redis.scan(String.format("%S_%s_*_*_%s", VideoManagerConstants.PLAY_BLACK_PREFIX,
+//                    userSetup.getServerId(),
+//                    deviceId));
+//        }
+//        if (playLeys == null || playLeys.size() == 0) return null;
+//        return (StreamInfo)redis.get(playLeys.get(0).toString());
+//    }
+//
+//    @Override
+//    public void updatePlatformCatchInfo(ParentPlatformCatch parentPlatformCatch) {
+//        String key = VideoManagerConstants.PLATFORM_CATCH_PREFIX  + userSetup.getServerId() + "_" +  parentPlatformCatch.getId();
+//        redis.set(key, parentPlatformCatch);
+//    }
+//
+//    @Override
+//    public void updatePlatformKeepalive(ParentPlatform parentPlatform) {
+//        String key = VideoManagerConstants.PLATFORM_KEEPALIVE_PREFIX  + userSetup.getServerId() + "_" + parentPlatform.getServerGBId();
+//        redis.set(key, "", Integer.parseInt(parentPlatform.getKeepTimeout()));
+//    }
+//
+//    @Override
+//    public void updatePlatformRegister(ParentPlatform parentPlatform) {
+//        String key = VideoManagerConstants.PLATFORM_REGISTER_PREFIX + userSetup.getServerId() + "_" + parentPlatform.getServerGBId();
+//        redis.set(key, "", Integer.parseInt(parentPlatform.getExpires()));
+//    }
+//
+//    @Override
+//    public ParentPlatformCatch queryPlatformCatchInfo(String platformGbId) {
+//        return (ParentPlatformCatch)redis.get(VideoManagerConstants.PLATFORM_CATCH_PREFIX + userSetup.getServerId() + "_" + platformGbId);
+//    }
+//
+//    @Override
+//    public void delPlatformCatchInfo(String platformGbId) {
+//        redis.del(VideoManagerConstants.PLATFORM_CATCH_PREFIX + userSetup.getServerId() + "_" + platformGbId);
+//    }
+//
+//    @Override
+//    public void delPlatformKeepalive(String platformGbId) {
+//        redis.del(VideoManagerConstants.PLATFORM_KEEPALIVE_PREFIX + userSetup.getServerId() + "_" + platformGbId);
+//    }
+//
+//    @Override
+//    public void delPlatformRegister(String platformGbId) {
+//        redis.del(VideoManagerConstants.PLATFORM_REGISTER_PREFIX + userSetup.getServerId() + "_" + platformGbId);
+//    }
+//
+//
+//    @Override
+//    public void updatePlatformRegisterInfo(String callId, String platformGbId) {
+//        String key = VideoManagerConstants.PLATFORM_REGISTER_INFO_PREFIX + userSetup.getServerId() + "_" + callId;
+//        redis.set(key, platformGbId);
+//    }
+//
+//
+//    @Override
+//    public String queryPlatformRegisterInfo(String callId) {
+//        return (String)redis.get(VideoManagerConstants.PLATFORM_REGISTER_INFO_PREFIX + userSetup.getServerId() + "_" + callId);
+//    }
+//
+//    @Override
+//    public void delPlatformRegisterInfo(String callId) {
+//        redis.del(VideoManagerConstants.PLATFORM_REGISTER_INFO_PREFIX + userSetup.getServerId() + "_" + callId);
+//    }
+//
+//    @Override
+//    public void cleanPlatformRegisterInfos() {
+//        List regInfos = redis.scan(VideoManagerConstants.PLATFORM_REGISTER_INFO_PREFIX + userSetup.getServerId() + "_" + "*");
+//        for (Object key : regInfos) {
+//            redis.del(key.toString());
+//        }
+//    }
+//
+//    @Override
+//    public void updateSendRTPSever(SendRtpItem sendRtpItem) {
+//        String key = VideoManagerConstants.PLATFORM_SEND_RTP_INFO_PREFIX + userSetup.getServerId() + "_" + sendRtpItem.getPlatformId() + "_" + sendRtpItem.getChannelId();
+//        redis.set(key, sendRtpItem);
+//    }
+//
+//    @Override
+//    public SendRtpItem querySendRTPServer(String platformGbId, String channelId) {
+//        String key = VideoManagerConstants.PLATFORM_SEND_RTP_INFO_PREFIX + userSetup.getServerId() + "_" + platformGbId + "_" + channelId;
+//        return (SendRtpItem)redis.get(key);
+//    }
+//
+//    @Override
+//    public List<SendRtpItem> querySendRTPServer(String platformGbId) {
+//        String key = VideoManagerConstants.PLATFORM_SEND_RTP_INFO_PREFIX + userSetup.getServerId() + "_" + platformGbId + "_*";
+//        List<Object> queryResult = redis.scan(key);
+//        List<SendRtpItem> result= new ArrayList<>();
+//
+//        for (int i = 0; i < queryResult.size(); i++) {
+//            String keyItem = (String) queryResult.get(i);
+//            result.add((SendRtpItem)redis.get(keyItem));
+//        }
+//
+//        return result;
+//    }
+//
+//    /**
+//     * 删除RTP推送信息缓存
+//     * @param platformGbId
+//     * @param channelId
+//     */
+//    @Override
+//    public void deleteSendRTPServer(String platformGbId, String channelId) {
+//        String key = VideoManagerConstants.PLATFORM_SEND_RTP_INFO_PREFIX + userSetup.getServerId() + "_" + platformGbId + "_" + channelId;
+//        redis.del(key);
+//    }
+//
+//    /**
+//     * 查询某个通道是否存在上级点播(RTP推送)
+//     * @param channelId
+//     */
+//    @Override
+//    public boolean isChannelSendingRTP(String channelId) {
+//        String key = VideoManagerConstants.PLATFORM_SEND_RTP_INFO_PREFIX + userSetup.getServerId() + "_" + "*_" + channelId;
+//        List<Object> RtpStreams = redis.scan(key);
+//        if (RtpStreams.size() > 0) {
+//            return true;
+//        } else {
+//            return false;
+//        }
+//    }
+//
+//    @Override
+//    public void clearCatchByDeviceId(String deviceId) {
+//        List<Object> playLeys = redis.scan(String.format("%S_%s_*_%s_*", VideoManagerConstants.PLAYER_PREFIX,
+//                userSetup.getServerId(),
+//                deviceId));
+//        if (playLeys.size() > 0) {
+//            for (Object key : playLeys) {
+//                redis.del(key.toString());
+//            }
+//        }
+//
+//        List<Object> playBackers = redis.scan(String.format("%S_%s_*_%s_*", VideoManagerConstants.PLAY_BLACK_PREFIX,
+//                userSetup.getServerId(),
+//                deviceId));
+//        if (playBackers.size() > 0) {
+//            for (Object key : playBackers) {
+//                redis.del(key.toString());
+//            }
+//        }
+//
+//        List<Object> deviceCache = redis.scan(String.format("%S%s_%s", VideoManagerConstants.DEVICE_PREFIX,
+//                userSetup.getServerId(),
+//                deviceId));
+//        if (deviceCache.size() > 0) {
+//            for (Object key : deviceCache) {
+//                redis.del(key.toString());
+//            }
+//        }
+//    }
+//
+//    @Override
+//    public void outlineForAll() {
+//        List<Object> onlineDevices = redis.scan(VideoManagerConstants.KEEPLIVEKEY_PREFIX + userSetup.getServerId() + "_" + "*" );
+//        for (int i = 0; i < onlineDevices.size(); i++) {
+//            String key = (String) onlineDevices.get(i);
+//            redis.del(key);
+//        }
+//    }
+//
+//    @Override
+//    public List<String> getOnlineForAll() {
+//        List<String> result = new ArrayList<>();
+//        List<Object> onlineDevices = redis.scan(VideoManagerConstants.KEEPLIVEKEY_PREFIX + userSetup.getServerId() + "_"  + "*" );
+//        for (int i = 0; i < onlineDevices.size(); i++) {
+//            String key = (String) onlineDevices.get(i);
+//            result.add((String) redis.get(key));
+//        }
+//        return result;
+//    }
+//
+//    @Override
+//    public void updateWVPInfo(JSONObject jsonObject, int time) {
+//        String key = VideoManagerConstants.WVP_SERVER_PREFIX + userSetup.getServerId();
+//        redis.set(key, jsonObject, time);
+//    }
+//
+//    @Override
+//    public void sendStreamChangeMsg(String type, JSONObject jsonObject) {
+//        String key = VideoManagerConstants.WVP_MSG_STREAM_CHANGE_PREFIX + type;
+//        log.debug("[redis 流变化事件] {}: {}", key, jsonObject.toString());
+//        redis.convertAndSend(key, jsonObject);
+//    }
+//
+//    @Override
+//    public void addStream(MediaServerItem mediaServerItem, String type, String app, String streamId, MediaItem mediaItem) {
+//        String key = VideoManagerConstants.WVP_SERVER_STREAM_PREFIX  + userSetup.getServerId() + "_" + type + "_" + app + "_" + streamId + "_" + mediaServerItem.getId();
+//        redis.set(key, mediaItem);
+//    }
+//
+//    @Override
+//    public void removeStream(String mediaServerId, String type, String app, String streamId) {
+//        String key = VideoManagerConstants.WVP_SERVER_STREAM_PREFIX + userSetup.getServerId() + "_" + type + "_"  + app + "_" + streamId + "_" + mediaServerId;
+//        redis.del(key);
+//    }
+//
+//    @Override
+//    public StreamInfo queryDownloadByStreamId(String streamId) {
+//        List<Object> playLeys = redis.scan(String.format("%S_%s_%s_*", VideoManagerConstants.DOWNLOAD_PREFIX, userSetup.getServerId(), streamId));
+//        if (playLeys == null || playLeys.size() == 0) return null;
+//        return (StreamInfo)redis.get(playLeys.get(0).toString());
+//    }
+//
+//    @Override
+//    public ThirdPartyGB queryMemberNoGBId(String queryKey) {
+//        String key = VideoManagerConstants.WVP_STREAM_GB_ID_PREFIX + queryKey;
+//        JSONObject jsonObject = (JSONObject)redis.get(key);
+//        return  JSONObject.toJavaObject(jsonObject, ThirdPartyGB.class);
+//    }
+//
+//    @Override
+//    public void removeStream(String mediaServerId, String type) {
+//        String key = VideoManagerConstants.WVP_SERVER_STREAM_PREFIX + userSetup.getServerId() + "_" + type + "_*_*_" + mediaServerId;
+//        List<Object> streams = redis.scan(key);
+//        for (Object stream : streams) {
+//            redis.del((String) stream);
+//        }
+//    }
+//
+//    @Override
+//    public List<MediaItem> getStreams(String mediaServerId, String type) {
+//        List<MediaItem> result = new ArrayList<>();
+//        String key = VideoManagerConstants.WVP_SERVER_STREAM_PREFIX + userSetup.getServerId() + "_" + type + "_*_*_" + mediaServerId;
+//        List<Object> streams = redis.scan(key);
+//        for (Object stream : streams) {
+//            MediaItem mediaItem = (MediaItem)redis.get((String) stream);
+//            result.add(mediaItem);
+//        }
+//        return result;
+//    }
+//
+//    @Override
+//    public void updateDevice(MediaDevice device) {
+//        String key = VideoManagerConstants.DEVICE_PREFIX + userSetup.getServerId() + "_" + device.getId();
+//        redis.set(key, device);
+//    }
+//
+//    @Override
+//    public void removeDevice(String deviceId) {
+//        String key = VideoManagerConstants.DEVICE_PREFIX + userSetup.getServerId() + "_" + deviceId;
+//        redis.del(key);
+//    }
+//
+//    @Override
+//    public MediaDevice getDevice(String deviceId) {
+//        String key = VideoManagerConstants.DEVICE_PREFIX + userSetup.getServerId() + "_" + deviceId;
+//        return (MediaDevice)redis.get(key);
+//    }
+//
+//    @Override
+//    public void updateGpsMsgInfo(GPSMsgInfo gpsMsgInfo) {
+//        String key = VideoManagerConstants.WVP_STREAM_GPS_MSG_PREFIX + userSetup.getServerId() + "_" + gpsMsgInfo.getId();
+//        redis.set(key, gpsMsgInfo, 60); // 默认GPS消息保存1分钟
+//    }
+//
+//    @Override
+//    public GPSMsgInfo getGpsMsgInfo(String gbId) {
+//        String key = VideoManagerConstants.WVP_STREAM_GPS_MSG_PREFIX + userSetup.getServerId() + "_" + gbId;
+//        return (GPSMsgInfo)redis.get(key);
+//    }
+//
+//    @Override
+//    public void updateSubscribe(String key, SubscribeInfo subscribeInfo) {
+//        redis.set(key, subscribeInfo, subscribeInfo.getExpires());
+//    }
+//
+//    @Override
+//    public SubscribeInfo getSubscribe(String key) {
+//        return (SubscribeInfo)redis.get(key);
+//    }
+//
+//    @Override
+//    public void delSubscribe(String key) {
+//        redis.del(key);
+//    }
+//
+//    @Override
+//    public List<GPSMsgInfo> getAllGpsMsgInfo() {
+//        String scanKey = VideoManagerConstants.WVP_STREAM_GPS_MSG_PREFIX + userSetup.getServerId() + "_*";
+//        List<GPSMsgInfo> result = new ArrayList<>();
+//        List<Object> keys = redis.scan(scanKey);
+//        for (int i = 0; i < keys.size(); i++) {
+//            String key = (String) keys.get(i);
+//            GPSMsgInfo gpsMsgInfo = (GPSMsgInfo) redis.get(key);
+//            if (!gpsMsgInfo.isStored()) { // 只取没有存过得
+//                result.add((GPSMsgInfo)redis.get(key));
+//            }
+//        }
+//
+//        return result;
+//    }
+//
+//    @Override
+//    public MediaItem getStreamInfo(String app, String streamId, String mediaServerId) {
+//        String scanKey = VideoManagerConstants.WVP_SERVER_STREAM_PREFIX  + userSetup.getServerId() + "_*_" + app + "_" + streamId + "_" + mediaServerId;
+//
+//        MediaItem result = null;
+//        List<Object> keys = redis.scan(scanKey);
+//        if (keys.size() > 0) {
+//            String key = (String) keys.get(0);
+//            result = (MediaItem)redis.get(key);
+//        }
+//
+//        return result;
+//    }
+//}

+ 305 - 0
jetlinks-manager/media-manager/src/main/java/org/jetlinks/community/media/transmit/SIPRequestHeaderProvider.java

@@ -0,0 +1,305 @@
+package org.jetlinks.community.media.transmit;
+
+
+import gov.nist.javax.sip.header.SIPDateHeader;
+import lombok.AllArgsConstructor;
+import org.jetlinks.community.media.bean.StreamInfo;
+import org.jetlinks.community.media.entity.MediaDevice;
+import org.jetlinks.community.media.entity.SipServerConfig;
+import org.jetlinks.community.media.entity.WvpSipDate;
+import org.jetlinks.community.media.sip.SipContext;
+import org.jetlinks.community.media.storage.IRedisCatchStorage;
+import org.jetlinks.community.utils.RedisUtil;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.stereotype.Component;
+
+import javax.sip.Dialog;
+import javax.sip.InvalidArgumentException;
+import javax.sip.PeerUnavailableException;
+import javax.sip.SipFactory;
+import javax.sip.address.Address;
+import javax.sip.address.SipURI;
+import javax.sip.header.*;
+import javax.sip.message.Request;
+import java.text.ParseException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Calendar;
+import java.util.Locale;
+
+/**
+ * @description:摄像头命令request创造器 TODO 冗余代码太多待优化
+ * @author: swwheihei
+ * @date: 2020年5月6日 上午9:29:02
+ */
+@Component
+public class SIPRequestHeaderProvider {
+
+
+
+	private IRedisCatchStorage redisCatchStorage;
+
+//	@Autowired
+//	private VideoStreamSessionManager streamSession;
+
+	public Request createMessageRequest(MediaDevice device, String content, String viaTag, String fromTag, String toTag, CallIdHeader callIdHeader) throws ParseException, InvalidArgumentException, PeerUnavailableException {
+        SipServerConfig sipConfig=SipContext.getConfig();
+        SipFactory sipFactory=SipContext.getSipFactory();
+		Request request = null;
+		// sipuri
+		SipURI requestUrl = sipFactory.createAddressFactory().createSipURI(device.getId(), device.getHostAddress());
+		// via
+		ArrayList<ViaHeader> viaHeaders = new ArrayList<ViaHeader>();
+		ViaHeader viaHeader = sipFactory.createHeaderFactory().createViaHeader(sipConfig.getHost(), sipConfig.getPort(), device.getTransport(), viaTag);
+		viaHeader.setRPort();
+		viaHeaders.add(viaHeader);
+		// from
+		SipURI fromSipUrl = sipFactory.createAddressFactory().createSipURI(sipConfig.getId(),
+				sipConfig.getHost() + ":" + sipConfig.getPort());
+		Address fromAddress = sipFactory.createAddressFactory().createAddress(fromSipUrl);
+		FromHeader fromHeader = sipFactory.createHeaderFactory().createFromHeader(fromAddress, fromTag);
+		// to
+		SipURI toSipUrl = sipFactory.createAddressFactory().createSipURI(device.getId(), sipConfig.getDomain());
+		Address toAddress = sipFactory.createAddressFactory().createAddress(toSipUrl);
+		ToHeader toHeader = sipFactory.createHeaderFactory().createToHeader(toAddress, toTag);
+
+		// Forwards
+		MaxForwardsHeader maxForwards = sipFactory.createHeaderFactory().createMaxForwardsHeader(70);
+		// ceq
+		CSeqHeader cSeqHeader = sipFactory.createHeaderFactory().createCSeqHeader(1L, Request.MESSAGE);
+
+		request = sipFactory.createMessageFactory().createRequest(requestUrl, Request.MESSAGE, callIdHeader, cSeqHeader, fromHeader,
+				toHeader, viaHeaders, maxForwards);
+		ContentTypeHeader contentTypeHeader = sipFactory.createHeaderFactory().createContentTypeHeader("APPLICATION", "MANSCDP+xml");
+		request.setContent(content, contentTypeHeader);
+		return request;
+	}
+	int count=1;
+	public Request createInviteRequest(MediaDevice device, String channelId, String content, String viaTag, String fromTag, String toTag, String ssrc, CallIdHeader callIdHeader) throws ParseException, InvalidArgumentException, PeerUnavailableException {
+        SipServerConfig sipConfig=SipContext.getConfig();
+        SipFactory sipFactory=SipContext.getSipFactory();
+		Request request = null;
+		//请求行
+		SipURI requestLine = sipFactory.createAddressFactory().createSipURI(channelId, device.getHostAddress());
+		//via
+		ArrayList<ViaHeader> viaHeaders = new ArrayList<ViaHeader>();
+		ViaHeader viaHeader = sipFactory.createHeaderFactory().createViaHeader(sipConfig.getHost(), sipConfig.getPort(), device.getTransport(), viaTag);
+		//todo
+		viaHeader.setRPort();
+		viaHeaders.add(viaHeader);
+
+		//from
+//		SipURI fromSipUrl = sipFactory.createAddressFactory().createSipURI(sipConfig.getId(),sipConfig.getDomain());
+        SipURI fromSipUrl = sipFactory.createAddressFactory().createSipURI(sipConfig.getId(),sipConfig.getHost()+":"+sipConfig.getPort());
+		Address fromAddress = sipFactory.createAddressFactory().createAddress(fromSipUrl);
+		FromHeader fromHeader = sipFactory.createHeaderFactory().createFromHeader(fromAddress, fromTag);
+		//必须要有标记,否则无法创建会话,无法回应ack
+		//to
+		SipURI toSipURI = sipFactory.createAddressFactory().createSipURI(device.getId(),device.getHostAddress());
+		Address toAddress = sipFactory.createAddressFactory().createAddress(toSipURI);
+		ToHeader toHeader = sipFactory.createHeaderFactory().createToHeader(toAddress,null);
+		
+		//Forwards
+		MaxForwardsHeader maxForwards = sipFactory.createHeaderFactory().createMaxForwardsHeader(70);
+
+
+        //ceq
+//		CSeqHeader cSeqHeader = sipFactory.createHeaderFactory().createCSeqHeader(redisCatchStorage.getCSEQ(Request.INVITE), Request.INVITE);
+        CSeqHeader cSeqHeader = sipFactory.createHeaderFactory().createCSeqHeader(++count, Request.INVITE);
+		request = sipFactory.createMessageFactory().createRequest(requestLine, Request.INVITE, callIdHeader, cSeqHeader,fromHeader, toHeader, viaHeaders, maxForwards);
+		
+		Address concatAddress = sipFactory.createAddressFactory().createAddress(sipFactory.createAddressFactory().createSipURI(sipConfig.getId(), sipConfig.getHost()+":"+sipConfig.getPort()));
+        request.addHeader(sipFactory.createHeaderFactory().createContactHeader(concatAddress));
+		// Subject    媒体流发送者设备编码:发送端媒体流序列号,媒体流接收者设备编码:接收端媒体流序列号
+		SubjectHeader subjectHeader = sipFactory.createHeaderFactory().createSubjectHeader(String.format("%s:%s,%s:%s", device.getId(), ssrc, channelId, 0));
+		request.addHeader(subjectHeader);
+		ContentTypeHeader contentTypeHeader = sipFactory.createHeaderFactory().createContentTypeHeader("APPLICATION", "SDP");
+
+        //ua
+        UserAgentHeader userAgentHeader = sipFactory.createHeaderFactory().createUserAgentHeader(Arrays.asList("eXosip/4.1.0"));
+        request.setHeader(userAgentHeader);
+
+		//date
+        WvpSipDate wvpSipDate = new WvpSipDate(Calendar.getInstance(Locale.ENGLISH).getTimeInMillis());
+        SIPDateHeader dateHeader = new SIPDateHeader();
+        dateHeader.setDate(wvpSipDate);
+        request.setHeader(dateHeader);
+
+
+		request.setContent(content, contentTypeHeader);
+		return request;
+	}
+	
+	public Request createPlaybackInviteRequest(MediaDevice device, String channelId, String content, String viaTag, String fromTag, String toTag, CallIdHeader callIdHeader, String ssrc) throws ParseException, InvalidArgumentException, PeerUnavailableException {
+        SipServerConfig sipConfig=SipContext.getConfig();
+        SipFactory sipFactory=SipContext.getSipFactory();
+		Request request = null;
+		//请求行
+		SipURI requestLine = sipFactory.createAddressFactory().createSipURI(channelId, device.getHostAddress());
+		// via
+		ArrayList<ViaHeader> viaHeaders = new ArrayList<ViaHeader>();
+		ViaHeader viaHeader = sipFactory.createHeaderFactory().createViaHeader(sipConfig.getHost(), sipConfig.getPort(), device.getTransport(), viaTag);
+		viaHeader.setRPort();
+		viaHeaders.add(viaHeader);
+		//from
+		SipURI fromSipURI = sipFactory.createAddressFactory().createSipURI(sipConfig.getId(),sipConfig.getDomain());
+		Address fromAddress = sipFactory.createAddressFactory().createAddress(fromSipURI);
+		FromHeader fromHeader = sipFactory.createHeaderFactory().createFromHeader(fromAddress, fromTag); //必须要有标记,否则无法创建会话,无法回应ack
+		//to
+		SipURI toSipURI = sipFactory.createAddressFactory().createSipURI(channelId, sipConfig.getDomain());
+		Address toAddress = sipFactory.createAddressFactory().createAddress(toSipURI);
+		ToHeader toHeader = sipFactory.createHeaderFactory().createToHeader(toAddress,null);
+		
+		//Forwards
+		MaxForwardsHeader maxForwards = sipFactory.createHeaderFactory().createMaxForwardsHeader(70);
+		
+		//ceq
+		CSeqHeader cSeqHeader = sipFactory.createHeaderFactory().createCSeqHeader(redisCatchStorage.getCSEQ(Request.INVITE), Request.INVITE);
+		request = sipFactory.createMessageFactory().createRequest(requestLine, Request.INVITE, callIdHeader, cSeqHeader,fromHeader, toHeader, viaHeaders, maxForwards);
+		
+		Address concatAddress = sipFactory.createAddressFactory().createAddress(sipFactory.createAddressFactory().createSipURI(sipConfig.getId(), sipConfig.getHost()+":"+sipConfig.getPort()));
+		// Address concatAddress = sipFactory.createAddressFactory().createAddress(sipFactory.createAddressFactory().createSipURI(sipConfig.getId(), device.getHost().getIp()+":"+device.getHost().getPort()));
+		request.addHeader(sipFactory.createHeaderFactory().createContactHeader(concatAddress));
+		// Subject
+		SubjectHeader subjectHeader = sipFactory.createHeaderFactory().createSubjectHeader(String.format("%s:%s,%s:%s", channelId, ssrc, sipConfig.getId(), 0));
+		request.addHeader(subjectHeader);
+
+		ContentTypeHeader contentTypeHeader = sipFactory.createHeaderFactory().createContentTypeHeader("APPLICATION", "SDP");
+		request.setContent(content, contentTypeHeader);
+		return request;
+	}
+
+	public Request createByteRequest(MediaDevice device, String channelId, String viaTag, String fromTag, String toTag, String callId) throws ParseException, InvalidArgumentException, PeerUnavailableException {
+        SipServerConfig sipConfig=SipContext.getConfig();
+        SipFactory sipFactory=SipContext.getSipFactory();
+		Request request = null;
+		//请求行
+		SipURI requestLine = sipFactory.createAddressFactory().createSipURI(channelId, device.getHostAddress());
+		// via
+		ArrayList<ViaHeader> viaHeaders = new ArrayList<ViaHeader>();
+		ViaHeader viaHeader = sipFactory.createHeaderFactory().createViaHeader(sipConfig.getHost(), sipConfig.getPort(), device.getTransport(), viaTag);
+		viaHeaders.add(viaHeader);
+		//from
+		SipURI fromSipURI = sipFactory.createAddressFactory().createSipURI(sipConfig.getId(),sipConfig.getDomain());
+		Address fromAddress = sipFactory.createAddressFactory().createAddress(fromSipURI);
+		FromHeader fromHeader = sipFactory.createHeaderFactory().createFromHeader(fromAddress, fromTag); //必须要有标记,否则无法创建会话,无法回应ack
+		//to
+		SipURI toSipURI = sipFactory.createAddressFactory().createSipURI(channelId,sipConfig.getDomain());
+		Address toAddress = sipFactory.createAddressFactory().createAddress(toSipURI);
+		ToHeader toHeader = sipFactory.createHeaderFactory().createToHeader(toAddress,toTag);
+
+		//Forwards
+		MaxForwardsHeader maxForwards = sipFactory.createHeaderFactory().createMaxForwardsHeader(70);
+
+		//ceq
+		CSeqHeader cSeqHeader = sipFactory.createHeaderFactory().createCSeqHeader(1L, Request.BYE);
+		CallIdHeader callIdHeader = sipFactory.createHeaderFactory().createCallIdHeader(callId);
+		request = sipFactory.createMessageFactory().createRequest(requestLine, Request.BYE, callIdHeader, cSeqHeader,fromHeader, toHeader, viaHeaders, maxForwards);
+
+		Address concatAddress = sipFactory.createAddressFactory().createAddress(sipFactory.createAddressFactory().createSipURI(sipConfig.getId(), sipConfig.getHost()+":"+sipConfig.getPort()));
+
+		return request;
+	}
+
+	public Request createSubscribeRequest(MediaDevice device, String content, String viaTag, String fromTag, String toTag, Integer expires, String event, CallIdHeader callIdHeader) throws ParseException, InvalidArgumentException, PeerUnavailableException {
+        SipServerConfig sipConfig=SipContext.getConfig();
+        SipFactory sipFactory=SipContext.getSipFactory();
+		Request request = null;
+		// sipuri
+		SipURI requestURI = sipFactory.createAddressFactory().createSipURI(device.getId(), device.getHostAddress());
+		// via
+		ArrayList<ViaHeader> viaHeaders = new ArrayList<ViaHeader>();
+		ViaHeader viaHeader = sipFactory.createHeaderFactory().createViaHeader(sipConfig.getHost(), sipConfig.getPort(),
+				device.getTransport(), viaTag);
+		viaHeader.setRPort();
+		viaHeaders.add(viaHeader);
+		// from
+		SipURI fromSipURI = sipFactory.createAddressFactory().createSipURI(sipConfig.getId(),
+				sipConfig.getHost() + ":" + sipConfig.getPort());
+		Address fromAddress = sipFactory.createAddressFactory().createAddress(fromSipURI);
+		FromHeader fromHeader = sipFactory.createHeaderFactory().createFromHeader(fromAddress, fromTag);
+		// to
+		SipURI toSipURI = sipFactory.createAddressFactory().createSipURI(device.getId(), sipConfig.getDomain());
+		Address toAddress = sipFactory.createAddressFactory().createAddress(toSipURI);
+		ToHeader toHeader = sipFactory.createHeaderFactory().createToHeader(toAddress, toTag);
+
+		// Forwards
+		MaxForwardsHeader maxForwards = sipFactory.createHeaderFactory().createMaxForwardsHeader(70);
+
+		// ceq
+		CSeqHeader cSeqHeader = sipFactory.createHeaderFactory().createCSeqHeader(redisCatchStorage.getCSEQ(Request.SUBSCRIBE), Request.SUBSCRIBE);
+
+		request = sipFactory.createMessageFactory().createRequest(requestURI, Request.SUBSCRIBE, callIdHeader, cSeqHeader, fromHeader,
+				toHeader, viaHeaders, maxForwards);
+
+
+		Address concatAddress = sipFactory.createAddressFactory().createAddress(sipFactory.createAddressFactory().createSipURI(sipConfig.getId(), sipConfig.getHost()+":"+sipConfig.getPort()));
+		request.addHeader(sipFactory.createHeaderFactory().createContactHeader(concatAddress));
+
+		// Expires
+		ExpiresHeader expireHeader = sipFactory.createHeaderFactory().createExpiresHeader(expires);
+		request.addHeader(expireHeader);
+
+		// Event
+		EventHeader eventHeader = sipFactory.createHeaderFactory().createEventHeader(event);
+		request.addHeader(eventHeader);
+
+		ContentTypeHeader contentTypeHeader = sipFactory.createHeaderFactory().createContentTypeHeader("APPLICATION", "MANSCDP+xml");
+		request.setContent(content, contentTypeHeader);
+		return request;
+	}
+
+	public Request createInfoRequest(MediaDevice device, StreamInfo streamInfo, String content, Long cseq)
+			throws PeerUnavailableException, ParseException, InvalidArgumentException {
+        SipServerConfig sipConfig=SipContext.getConfig();
+        SipFactory sipFactory=SipContext.getSipFactory();
+		Request request = null;
+		//todo
+//		Dialog dialog = streamSession.getDialog(streamInfo.getDeviceID(), streamInfo.getChannelId());
+
+        Dialog dialog=null;
+		SipURI requestLine = sipFactory.createAddressFactory().createSipURI(device.getId(),
+				device.getHostAddress());
+		// via
+		ArrayList<ViaHeader> viaHeaders = new ArrayList<ViaHeader>();
+		ViaHeader viaHeader = sipFactory.createHeaderFactory().createViaHeader(device.getIp(), device.getPort(),
+				device.getTransport(), null);
+		viaHeader.setRPort();
+		viaHeaders.add(viaHeader);
+		// from
+		SipURI fromSipURI = sipFactory.createAddressFactory().createSipURI(sipConfig.getId(),
+				sipConfig.getDomain());
+		Address fromAddress = sipFactory.createAddressFactory().createAddress(fromSipURI);
+		FromHeader fromHeader = sipFactory.createHeaderFactory().createFromHeader(fromAddress, dialog.getLocalTag());
+		// to
+		SipURI toSipURI = sipFactory.createAddressFactory().createSipURI(streamInfo.getChannelId(),
+				sipConfig.getDomain());
+		Address toAddress = sipFactory.createAddressFactory().createAddress(toSipURI);
+		ToHeader toHeader = sipFactory.createHeaderFactory().createToHeader(toAddress, dialog.getRemoteTag());
+
+		// callid
+		CallIdHeader callIdHeader = dialog.getCallId();
+
+		// Forwards
+		MaxForwardsHeader maxForwards = sipFactory.createHeaderFactory().createMaxForwardsHeader(70);
+		if (cseq == null) {
+			cseq = redisCatchStorage.getCSEQ(Request.INFO);
+		}
+		// ceq
+		CSeqHeader cSeqHeader = sipFactory.createHeaderFactory()
+				.createCSeqHeader(cseq, Request.INFO);
+
+		request = sipFactory.createMessageFactory().createRequest(requestLine, Request.INFO, callIdHeader, cSeqHeader,
+				fromHeader, toHeader, viaHeaders, maxForwards);
+		Address concatAddress = sipFactory.createAddressFactory().createAddress(sipFactory.createAddressFactory()
+				.createSipURI(sipConfig.getId(), sipConfig.getHost() + ":" + sipConfig.getPort()));
+		request.addHeader(sipFactory.createHeaderFactory().createContactHeader(concatAddress));
+
+		ContentTypeHeader contentTypeHeader = sipFactory.createHeaderFactory().createContentTypeHeader("Application",
+				"MANSRTSP");
+		request.setContent(content, contentTypeHeader);
+		return request;
+	}
+
+}

+ 190 - 0
jetlinks-manager/media-manager/src/main/java/org/jetlinks/community/media/transmit/cmd/SipCommander.java

@@ -0,0 +1,190 @@
+package org.jetlinks.community.media.transmit.cmd;
+import lombok.AllArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.hswebframework.web.exception.BusinessException;
+import org.jetlinks.community.media.bean.EventResult;
+import org.jetlinks.community.media.bean.SSRCInfo;
+import org.jetlinks.community.media.entity.MediaDevice;
+import org.jetlinks.community.media.entity.SipServerConfig;
+import org.jetlinks.community.media.sip.SipContext;
+import org.jetlinks.community.media.transmit.SIPRequestHeaderProvider;
+import org.jetlinks.community.media.zlm.ZLMHttpHookSubscribe;
+import org.jetlinks.community.media.zlm.dto.MediaServerItem;
+import org.springframework.stereotype.Service;
+import reactor.core.publisher.*;
+
+import javax.sip.*;
+import javax.sip.header.CallIdHeader;
+import javax.sip.message.Request;
+import java.text.ParseException;
+import java.time.Duration;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.function.Consumer;
+
+/**
+ * @author lifang
+ * @version 1.0.0
+ * @ClassName SipCommand.java
+ * @Description TODO
+ * @createTime 2022年01月22日 09:02:00
+ */
+@Service
+@Slf4j
+@AllArgsConstructor
+public class SipCommander {
+
+    private final SIPRequestHeaderProvider headerProvider;
+
+    public static final Map<String, FluxProcessor<EventResult,EventResult>> replyProcessor=new ConcurrentHashMap<>(1024);
+
+
+    public Flux<EventResult> playStreamCmd(MediaServerItem mediaServerItem, SSRCInfo ssrcInfo, MediaDevice device, String channelId) {
+        String streamId = ssrcInfo.getStreamId();
+        SipServerConfig sipConfig = SipContext.getConfig();
+        try {
+            if (device == null){
+                return Flux.error(new BusinessException("设备不存在"));
+            }
+            String streamMode = device.getStreamMode().toUpperCase();
+
+//            log.info("{} 分配的ZLM为: {} [{}:{}]", streamId, mediaServerItem.getId(), mediaServerItem.getIp(), ssrcInfo.getPort());
+//            // 添加订阅
+//            JSONObject subscribeKey = new JSONObject();
+//            subscribeKey.put("app", "rtp");
+//            subscribeKey.put("stream", streamId);
+//            subscribeKey.put("regist", true);
+//            subscribeKey.put("mediaServerId", mediaServerItem.getId());
+//            subscribe.addSubscribe(ZLMHttpHookSubscribe.HookType.on_stream_changed, subscribeKey,
+//                (MediaServerItem mediaServerItemInUse, JSONObject json)->{
+//                    if (userSetup.isWaitTrack() && json.getJSONArray("tracks") == null) return;
+//                    event.response(mediaServerItemInUse, json);
+//                    subscribe.removeSubscribe(ZLMHttpHookSubscribe.HookType.on_stream_changed, subscribeKey);
+//                });
+//            //
+            StringBuffer content = new StringBuffer(200);
+            content.append("v=0\r\n");
+//            content.append("o="+ sipConfig.getId()+" 0 0 IN IP4 "+ mediaServerItem.getSdpIp() +"\r\n");
+            content.append("o=34020000002000000001 0 0 IN IP4 192.168.104.244 \r\n");
+            content.append("s=Play\r\n");
+//            content.append("c=IN IP4 "+ mediaServerItem.getSdpIp() +"\r\n");
+            content.append("c=IN IP4 192.168.104.244 \r\n");
+            content.append("t=0 0\r\n");
+
+            //是否需要扩展sdp
+//            if (userSetup.isSeniorSdp()) {
+            if (false) {
+                if("TCP-PASSIVE".equals(streamMode)) {
+                    content.append("m=video "+ ssrcInfo.getPort() +" TCP/RTP/AVP 96 126 125 99 34 98 97\r\n");
+                }else if ("TCP-ACTIVE".equals(streamMode)) {
+                    content.append("m=video "+ ssrcInfo.getPort() +" TCP/RTP/AVP 96 126 125 99 34 98 97\r\n");
+                }else if("UDP".equals(streamMode)) {
+                    content.append("m=video "+ ssrcInfo.getPort() +" RTP/AVP 96 126 125 99 34 98 97\r\n");
+                }
+                /**各个字段含义
+                 * a=rtpmap:<负载类型><编码名>/<时钟速率>[/<编码参数>]
+                 *     对于音频流,<编码参数>说明了音频的通道数。通道数默认缺省值为1。对于视频流,现阶段没有<编码参数>。
+                 * a=framerate:<帧速率>
+                 * v sdp的版本号,不包含此次的版本号
+                 * o=<用户名> <session id> <会话版本> <网络类型><地址类型> <地址>
+                 *     o=34020000002000000001 0 0 IN IP4 192.168.104.244
+                 * s=<会话名>
+                 *     s=Play
+                 *
+                 * c=<网络类型><地址类型><地址(单播:域名)(多播:ip地址)>
+                 *
+                 * m=<media><port> <transport> <fmt list>    一个会话描述包括几个媒体描述。一个媒体描述以”m=”开始到下一个”m=”结束。
+                 *      <media>:表示媒体类型。有"audio", "video","application"(例白板信息), "data"(不向用户显示的数据) 和"control"(描述额外的控制通道)
+                 *      <port>:媒体流发往传输层的端口。取决于c=行规定的网络类型和接下来的传送层协议:对UDP为1024-65535;
+                 *              对于RTP为偶数。当分层编码流被发送到一个单播地址时,需要列出多个端口。方式如下:
+                 *              对于RTP,偶数端口被用来传输数据,奇数端口用来传输RTCP包。例:m=video49170/2 RTP/AVP 31
+                 *
+                 *
+                 * 请求头:
+                 * subject 媒体流发送者设备编码:发送端媒体流序列号,媒体流接收者设备编码:接收端媒体流序列
+                 */
+                content.append("a=recvonly\r\n");
+                content.append("a=rtpmap:96 PS/90000\r\n");
+                content.append("a=fmtp:126 profile-level-id=42e01e\r\n");
+                content.append("a=rtpmap:126 H264/90000\r\n");
+                content.append("a=rtpmap:125 H264S/90000\r\n");
+                content.append("a=fmtp:125 profile-level-id=42e01e\r\n");
+                content.append("a=rtpmap:99 MP4V-ES/90000\r\n");
+                content.append("a=fmtp:99 profile-level-id=3\r\n");
+                content.append("a=rtpmap:98 H264/90000\r\n");
+                content.append("a=rtpmap:97 MPEG4/90000\r\n");
+                if("TCP-PASSIVE".equals(streamMode)){ // tcp被动模式
+                    content.append("a=setup:passive\r\n");
+                    content.append("a=connection:new\r\n");
+                }else if ("TCP-ACTIVE".equals(streamMode)) { // tcp主动模式
+                    content.append("a=setup:active\r\n");
+                    content.append("a=connection:new\r\n");
+                }
+            }else {
+                if("TCP-PASSIVE".equals(streamMode)) {
+                    content.append("m=video "+ ssrcInfo.getPort() +" TCP/RTP/AVP 96 98 97\r\n");
+                }else if ("TCP-ACTIVE".equals(streamMode)) {
+                    content.append("m=video "+ ssrcInfo.getPort() +" TCP/RTP/AVP 96 98 97\r\n");
+                }else if("UDP".equals(streamMode)) {
+                    content.append("m=video "+ ssrcInfo.getPort() +" RTP/AVP 96 98 97\r\n");
+                }
+                content.append("a=recvonly\r\n");
+                content.append("a=rtpmap:96 PS/90000\r\n");
+                content.append("a=rtpmap:98 H264/90000\r\n");
+                content.append("a=rtpmap:97 MPEG4/90000\r\n");
+                if ("TCP-PASSIVE".equals(streamMode)) { // tcp被动模式
+                    content.append("a=setup:passive\r\n");
+                    content.append("a=connection:new\r\n");
+                } else if ("TCP-ACTIVE".equals(streamMode)) { // tcp主动模式
+                    content.append("a=setup:active\r\n");
+                    content.append("a=connection:new\r\n");
+                }
+            }
+
+            content.append("y="+ssrcInfo.getSsrc()+"\r\n");//ssrc
+
+            String tm = Long.toString(System.currentTimeMillis());
+
+            SipProvider sipProvider = SipContext.getSipProvider();
+
+            CallIdHeader callIdHeader =sipProvider.getNewCallId();
+
+
+            Request request = headerProvider.createInviteRequest(device, channelId, content.toString(), null, "FromInvt" + tm, null, ssrcInfo.getSsrc(), callIdHeader);
+
+            String finalStreamId = streamId;
+            return transmitRequest(sipProvider,request)
+                .flatMap(result->{
+                    if(result.getSuccess()){
+                        //消息处理成功 TODO
+                        return Mono.just(result);
+                    }else {
+                        //消息处理失败 todo
+                        return Mono.error(new BusinessException("请求失败,"+result.getMsg()));
+                    }
+                });
+
+
+        } catch ( SipException | ParseException | InvalidArgumentException e) {
+            return Flux.error(new BusinessException("服务器异常"));
+        }
+    }
+
+
+    private Flux<EventResult> transmitRequest(SipProvider sipProvider, Request request) throws SipException {
+        ClientTransaction clientTransaction = sipProvider.getNewClientTransaction(request);
+        CallIdHeader callIdHeader = (CallIdHeader)request.getHeader(CallIdHeader.NAME);
+        clientTransaction.sendRequest();
+        return  handleReply(callIdHeader.getCallId(),Duration.ofSeconds(10));
+    }
+
+    private Flux<EventResult> handleReply(String callId, Duration timeout) {
+        return replyProcessor
+            .computeIfAbsent(callId, ignore -> UnicastProcessor.create())
+            .timeout(timeout, Mono.error(() -> new BusinessException("设备响应超时")))
+            .doFinally(signal -> {
+                replyProcessor.remove(callId);
+            });
+    }
+
+}

+ 15 - 0
jetlinks-manager/media-manager/src/main/java/org/jetlinks/community/media/vmanager/gb28181/play/bean/PlayResult.java

@@ -0,0 +1,15 @@
+package org.jetlinks.community.media.vmanager.gb28181.play.bean;
+
+import lombok.Data;
+import org.jetlinks.community.media.entity.MediaDevice;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.context.request.async.DeferredResult;
+
+@Data
+public class PlayResult {
+
+    private DeferredResult<ResponseEntity<String>> result;
+    private String uuid;
+
+    private MediaDevice device;
+}

+ 502 - 0
jetlinks-manager/media-manager/src/main/java/org/jetlinks/community/media/zlm/ZLMHttpHookListener.java

@@ -0,0 +1,502 @@
+//package org.jetlinks.community.media.zlm;
+//
+//import com.alibaba.fastjson.JSON;
+//import com.alibaba.fastjson.JSONObject;
+//import com.genersoft.iot.vmp.common.StreamInfo;
+//import com.genersoft.iot.vmp.conf.MediaConfig;
+//import com.genersoft.iot.vmp.conf.UserSetup;
+//import com.genersoft.iot.vmp.gb28181.bean.Device;
+//import com.genersoft.iot.vmp.gb28181.transmit.cmd.impl.SIPCommander;
+//import com.genersoft.iot.vmp.media.zlm.dto.*;
+//import com.genersoft.iot.vmp.service.*;
+//import com.genersoft.iot.vmp.service.bean.SSRCInfo;
+//import com.genersoft.iot.vmp.storager.IRedisCatchStorage;
+//import com.genersoft.iot.vmp.storager.IVideoManagerStorager;
+//import org.slf4j.Logger;
+//import org.slf4j.LoggerFactory;
+//import org.springframework.beans.factory.annotation.Autowired;
+//import org.springframework.http.HttpStatus;
+//import org.springframework.http.ResponseEntity;
+//import org.springframework.web.bind.annotation.*;
+//
+//import javax.servlet.http.HttpServletRequest;
+//import java.util.List;
+//import java.util.UUID;
+//
+///**
+// * @description:针对 ZLMediaServer的hook事件监听
+// * @author: swwheihei
+// * @date:   2020年5月8日 上午10:46:48
+// */
+//@RestController
+//@RequestMapping("/index/hook")
+//public class ZLMHttpHookListener {
+//
+//	private final static Logger logger = LoggerFactory.getLogger(ZLMHttpHookListener.class);
+//
+//	@Autowired
+//	private SIPCommander cmder;
+//
+//	@Autowired
+//	private IPlayService playService;
+//
+//	@Autowired
+//	private IVideoManagerStorager storager;
+//
+//	@Autowired
+//	private IRedisCatchStorage redisCatchStorage;
+//
+//	@Autowired
+//	private IMediaServerService mediaServerService;
+//
+//	@Autowired
+//	private IStreamProxyService streamProxyService;
+//
+//	@Autowired
+//	private IStreamPushService streamPushService;
+//
+//	@Autowired
+//	private IMediaService mediaService;
+//
+//	@Autowired
+//	private ZLMRESTfulUtils zlmresTfulUtils;
+//
+//	 @Autowired
+//	 private ZLMMediaListManager zlmMediaListManager;
+//
+//	@Autowired
+//	private ZLMHttpHookSubscribe subscribe;
+//
+//	@Autowired
+//	private UserSetup userSetup;
+//
+//	@Autowired
+//	private MediaConfig mediaConfig;
+//
+//	/**
+//	 * 服务器定时上报时间,上报间隔可配置,默认10s上报一次
+//	 *
+//	 */
+//	@ResponseBody
+//	@PostMapping(value = "/on_server_keepalive", produces = "application/json;charset=UTF-8")
+//	public ResponseEntity<String> onServerKeepalive(@RequestBody JSONObject json){
+//
+//		if (logger.isDebugEnabled()) {
+//			logger.debug("[ ZLM HOOK ]on_server_keepalive API调用,参数:" + json.toString());
+//		}
+//		String mediaServerId = json.getString("mediaServerId");
+//
+//		List<ZLMHttpHookSubscribe.Event> subscribes = this.subscribe.getSubscribes(ZLMHttpHookSubscribe.HookType.on_server_keepalive);
+//		if (subscribes != null  && subscribes.size() > 0) {
+//			for (ZLMHttpHookSubscribe.Event subscribe : subscribes) {
+//				subscribe.response(null, json);
+//			}
+//		}
+//
+//		JSONObject ret = new JSONObject();
+//		ret.put("code", 0);
+//		ret.put("msg", "success");
+//		return new ResponseEntity<String>(ret.toString(), HttpStatus.OK);
+//	}
+//
+//	/**
+//	 * 流量统计事件,播放器或推流器断开时并且耗用流量超过特定阈值时会触发此事件,阈值通过配置文件general.flowThreshold配置;此事件对回复不敏感。
+//	 *
+//	 */
+//	@ResponseBody
+//	@PostMapping(value = "/on_flow_report", produces = "application/json;charset=UTF-8")
+//	public ResponseEntity<String> onFlowReport(@RequestBody JSONObject json){
+//
+//		if (logger.isDebugEnabled()) {
+//			logger.debug("[ ZLM HOOK ]on_flow_report API调用,参数:" + json.toString());
+//		}
+//		String mediaServerId = json.getString("mediaServerId");
+//		JSONObject ret = new JSONObject();
+//		ret.put("code", 0);
+//		ret.put("msg", "success");
+//		return new ResponseEntity<String>(ret.toString(), HttpStatus.OK);
+//	}
+//
+//	/**
+//	 * 访问http文件服务器上hls之外的文件时触发。
+//	 *
+//	 */
+//	@ResponseBody
+//	@PostMapping(value = "/on_http_access", produces = "application/json;charset=UTF-8")
+//	public ResponseEntity<String> onHttpAccess(@RequestBody JSONObject json){
+//
+//		if (logger.isDebugEnabled()) {
+//			logger.debug("[ ZLM HOOK ]on_http_access API 调用,参数:" + json.toString());
+//		}
+//		String mediaServerId = json.getString("mediaServerId");
+//		JSONObject ret = new JSONObject();
+//		ret.put("code", 0);
+//		ret.put("err", "");
+//		ret.put("path", "");
+//		ret.put("second", 600);
+//		return new ResponseEntity<String>(ret.toString(), HttpStatus.OK);
+//	}
+//
+//	/**
+//	 * 播放器鉴权事件,rtsp/rtmp/http-flv/ws-flv/hls的播放都将触发此鉴权事件。
+//	 *
+//	 */
+//	@ResponseBody
+//	@PostMapping(value = "/on_play", produces = "application/json;charset=UTF-8")
+//	public ResponseEntity<String> onPlay(@RequestBody JSONObject json){
+//
+//		if (logger.isDebugEnabled()) {
+//			logger.debug("[ ZLM HOOK ]on_play API调用,参数:" + json.toString());
+//		}
+//		String mediaServerId = json.getString("mediaServerId");
+//		ZLMHttpHookSubscribe.Event subscribe = this.subscribe.getSubscribe(ZLMHttpHookSubscribe.HookType.on_play, json);
+//		if (subscribe != null ) {
+//			MediaServerItem mediaInfo = mediaServerService.getOne(mediaServerId);
+//			if (mediaInfo != null) {
+//				subscribe.response(mediaInfo, json);
+//			}
+//
+//		}
+//		JSONObject ret = new JSONObject();
+//		ret.put("code", 0);
+//		ret.put("msg", "success");
+//		return new ResponseEntity<String>(ret.toString(), HttpStatus.OK);
+//	}
+//
+//	/**
+//	 * rtsp/rtmp/rtp推流鉴权事件。
+//	 *
+//	 */
+//	@ResponseBody
+//	@PostMapping(value = "/on_publish", produces = "application/json;charset=UTF-8")
+//	public ResponseEntity<String> onPublish(@RequestBody JSONObject json) {
+//
+//		logger.debug("[ ZLM HOOK ]on_publish API调用,参数:" + json.toString());
+//		JSONObject ret = new JSONObject();
+//		ret.put("code", 0);
+//		ret.put("msg", "success");
+//		ret.put("enableHls", true);
+//		ret.put("enableMP4", userSetup.isRecordPushLive());
+//		String mediaServerId = json.getString("mediaServerId");
+//		ZLMHttpHookSubscribe.Event subscribe = this.subscribe.getSubscribe(ZLMHttpHookSubscribe.HookType.on_publish, json);
+//		if (subscribe != null) {
+//			MediaServerItem mediaInfo = mediaServerService.getOne(mediaServerId);
+//			if (mediaInfo != null) {
+//				subscribe.response(mediaInfo, json);
+//			}else {
+//				ret.put("code", 1);
+//				ret.put("msg", "zlm not register");
+//			}
+//		}
+//	 	String app = json.getString("app");
+//	 	String stream = json.getString("stream");
+//		StreamInfo streamInfo = redisCatchStorage.queryPlaybackByStreamId(stream);
+//
+//		// 录像回放时不进行录像下载
+//		if (streamInfo != null) {
+//			ret.put("enableMP4", false);
+//		}else {
+//			ret.put("enableMP4", userSetup.isRecordPushLive());
+//		}
+//
+//		return new ResponseEntity<String>(ret.toString(), HttpStatus.OK);
+//	}
+//
+//	/**
+//	 * 录制mp4完成后通知事件;此事件对回复不敏感。
+//	 *
+//	 */
+//	@ResponseBody
+//	@PostMapping(value = "/on_record_mp4", produces = "application/json;charset=UTF-8")
+//	public ResponseEntity<String> onRecordMp4(@RequestBody JSONObject json){
+//
+//		if (logger.isDebugEnabled()) {
+//			logger.debug("[ ZLM HOOK ]on_record_mp4 API调用,参数:" + json.toString());
+//		}
+//		String mediaServerId = json.getString("mediaServerId");
+//		JSONObject ret = new JSONObject();
+//		ret.put("code", 0);
+//		ret.put("msg", "success");
+//		return new ResponseEntity<String>(ret.toString(), HttpStatus.OK);
+//	}
+//
+//	/**
+//	 * rtsp专用的鉴权事件,先触发on_rtsp_realm事件然后才会触发on_rtsp_auth事件。
+//	 *
+//	 */
+//	@ResponseBody
+//	@PostMapping(value = "/on_rtsp_realm", produces = "application/json;charset=UTF-8")
+//	public ResponseEntity<String> onRtspRealm(@RequestBody JSONObject json){
+//
+//		if (logger.isDebugEnabled()) {
+//			logger.debug("[ ZLM HOOK ]on_rtsp_realm API调用,参数:" + json.toString());
+//		}
+//		String mediaServerId = json.getString("mediaServerId");
+//		JSONObject ret = new JSONObject();
+//		ret.put("code", 0);
+//		ret.put("realm", "");
+//		return new ResponseEntity<String>(ret.toString(), HttpStatus.OK);
+//	}
+//
+//
+//	/**
+//	 * 该rtsp流是否开启rtsp专用方式的鉴权事件,开启后才会触发on_rtsp_auth事件。需要指出的是rtsp也支持url参数鉴权,它支持两种方式鉴权。
+//	 *
+//	 */
+//	@ResponseBody
+//	@PostMapping(value = "/on_rtsp_auth", produces = "application/json;charset=UTF-8")
+//	public ResponseEntity<String> onRtspAuth(@RequestBody JSONObject json){
+//
+//		if (logger.isDebugEnabled()) {
+//			logger.debug("[ ZLM HOOK ]on_rtsp_auth API调用,参数:" + json.toString());
+//		}
+//		String mediaServerId = json.getString("mediaServerId");
+//		JSONObject ret = new JSONObject();
+//		ret.put("code", 0);
+//		ret.put("encrypted", false);
+//		ret.put("passwd", "test");
+//		return new ResponseEntity<String>(ret.toString(), HttpStatus.OK);
+//	}
+//
+//	/**
+//	 * shell登录鉴权,ZLMediaKit提供简单的telnet调试方式,使用telnet 127.0.0.1 9000能进入MediaServer进程的shell界面。
+//	 *
+//	 */
+//	@ResponseBody
+//	@PostMapping(value = "/on_shell_login", produces = "application/json;charset=UTF-8")
+//	public ResponseEntity<String> onShellLogin(@RequestBody JSONObject json){
+//
+//		if (logger.isDebugEnabled()) {
+//			logger.debug("[ ZLM HOOK ]on_shell_login API调用,参数:" + json.toString());
+//		}
+//		// TODO 如果是带有rtpstream则开启按需拉流
+//		// String app = json.getString("app");
+//		// String stream = json.getString("stream");
+//		String mediaServerId = json.getString("mediaServerId");
+//		ZLMHttpHookSubscribe.Event subscribe = this.subscribe.getSubscribe(ZLMHttpHookSubscribe.HookType.on_shell_login, json);
+//		if (subscribe != null ) {
+//			MediaServerItem mediaInfo = mediaServerService.getOne(mediaServerId);
+//			if (mediaInfo != null) {
+//				subscribe.response(mediaInfo, json);
+//			}
+//
+//		}
+//
+//		JSONObject ret = new JSONObject();
+//		ret.put("code", 0);
+//		ret.put("msg", "success");
+//		return new ResponseEntity<String>(ret.toString(), HttpStatus.OK);
+//	}
+//
+//	/**
+//	 * rtsp/rtmp流注册或注销时触发此事件;此事件对回复不敏感。
+//	 *
+//	 */
+//	@ResponseBody
+//	@PostMapping(value = "/on_stream_changed", produces = "application/json;charset=UTF-8")
+//	public ResponseEntity<String> onStreamChanged(@RequestBody MediaItem item){
+//
+//		if (logger.isDebugEnabled()) {
+//			logger.debug("[ ZLM HOOK ]on_stream_changed API调用,参数:" + JSONObject.toJSONString(item));
+//		}
+//		String mediaServerId = item.getMediaServerId();
+//		JSONObject json = (JSONObject) JSON.toJSON(item);
+//		ZLMHttpHookSubscribe.Event subscribe = this.subscribe.getSubscribe(ZLMHttpHookSubscribe.HookType.on_stream_changed, json);
+//		if (subscribe != null ) {
+//			MediaServerItem mediaInfo = mediaServerService.getOne(mediaServerId);
+//			if (mediaInfo != null) {
+//				subscribe.response(mediaInfo, json);
+//			}
+//
+//		}
+//		// 流消失移除redis play
+//		String app = item.getApp();
+//		String streamId = item.getStream();
+//		String schema = item.getSchema();
+//		List<MediaItem.MediaTrack> tracks = item.getTracks();
+//		boolean regist = item.isRegist();
+//		if ("rtmp".equals(schema)){
+//			logger.info("on_stream_changed:注册->{}, app->{}, stream->{}", regist, app, streamId);
+//			if (regist) {
+//				mediaServerService.addCount(mediaServerId);
+//			}else {
+//				mediaServerService.removeCount(mediaServerId);
+//			}
+//			if ("rtp".equals(app) && !regist ) {
+//				StreamInfo streamInfo = redisCatchStorage.queryPlayByStreamId(streamId);
+//				if (streamInfo!=null){
+//					redisCatchStorage.stopPlay(streamInfo);
+//					storager.stopPlay(streamInfo.getDeviceID(), streamInfo.getChannelId());
+//				}else{
+//					streamInfo = redisCatchStorage.queryPlaybackByStreamId(streamId);
+//					redisCatchStorage.stopPlayback(streamInfo);
+//				}
+//			}else {
+//				if (!"rtp".equals(app)){
+//					String type = OriginType.values()[item.getOriginType()].getType();
+//					MediaServerItem mediaServerItem = mediaServerService.getOne(mediaServerId);
+//					if (mediaServerItem != null){
+//						if (regist) {
+//							redisCatchStorage.addStream(mediaServerItem, type, app, streamId, item);
+//							if (item.getOriginType() == OriginType.RTSP_PUSH.ordinal()
+//									|| item.getOriginType() == OriginType.RTMP_PUSH.ordinal()
+//									|| item.getOriginType() == OriginType.RTC_PUSH.ordinal() ) {
+//								zlmMediaListManager.addPush(item);
+//							}
+//						}else {
+//							// 兼容流注销时类型从redis记录获取
+//							MediaItem mediaItem = redisCatchStorage.getStreamInfo(app, streamId, mediaServerId);
+//							type = OriginType.values()[mediaItem.getOriginType()].getType();
+//							zlmMediaListManager.removeMedia(app, streamId);
+//							redisCatchStorage.removeStream(mediaServerItem.getId(), type, app, streamId);
+//						}
+//						// 发送流变化redis消息
+//						JSONObject jsonObject = new JSONObject();
+//						jsonObject.put("serverId", userSetup.getServerId());
+//						jsonObject.put("app", app);
+//						jsonObject.put("stream", streamId);
+//						jsonObject.put("register", regist);
+//						jsonObject.put("mediaServerId", mediaServerId);
+//						redisCatchStorage.sendStreamChangeMsg(type, jsonObject);
+//					}
+//				}
+//			}
+//		}
+//
+//		JSONObject ret = new JSONObject();
+//		ret.put("code", 0);
+//		ret.put("msg", "success");
+//		return new ResponseEntity<String>(ret.toString(), HttpStatus.OK);
+//	}
+//
+//	/**
+//	 * 流无人观看时事件,用户可以通过此事件选择是否关闭无人看的流。
+//	 *
+//	 */
+//	@ResponseBody
+//	@PostMapping(value = "/on_stream_none_reader", produces = "application/json;charset=UTF-8")
+//	public ResponseEntity<String> onStreamNoneReader(@RequestBody JSONObject json){
+//
+//		if (logger.isDebugEnabled()) {
+//			logger.debug("[ ZLM HOOK ]on_stream_none_reader API调用,参数:" + json.toString());
+//		}
+//		String mediaServerId = json.getString("mediaServerId");
+//		String streamId = json.getString("stream");
+//		String app = json.getString("app");
+//		JSONObject ret = new JSONObject();
+//		ret.put("code", 0);
+//		if ("rtp".equals(app)){
+//			ret.put("close", true);
+//			StreamInfo streamInfoForPlayCatch = redisCatchStorage.queryPlayByStreamId(streamId);
+//			if (streamInfoForPlayCatch != null) {
+//				// 如果在给上级推流,也不停止。
+//				if (redisCatchStorage.isChannelSendingRTP(streamInfoForPlayCatch.getChannelId())) {
+//					ret.put("close", false);
+//				} else {
+//					cmder.streamByeCmd(streamInfoForPlayCatch.getDeviceID(), streamInfoForPlayCatch.getChannelId());
+//					redisCatchStorage.stopPlay(streamInfoForPlayCatch);
+//					storager.stopPlay(streamInfoForPlayCatch.getDeviceID(), streamInfoForPlayCatch.getChannelId());
+//				}
+//			}else{
+//				StreamInfo streamInfoForPlayBackCatch = redisCatchStorage.queryPlaybackByStreamId(streamId);
+//				if (streamInfoForPlayBackCatch != null) {
+//					cmder.streamByeCmd(streamInfoForPlayBackCatch.getDeviceID(), streamInfoForPlayBackCatch.getChannelId());
+//					redisCatchStorage.stopPlayback(streamInfoForPlayBackCatch);
+//				}else {
+//					StreamInfo streamInfoForDownload = redisCatchStorage.queryDownloadByStreamId(streamId);
+//					// 进行录像下载时无人观看不断流
+//					if (streamInfoForDownload != null) {
+//						ret.put("close", false);
+//					}
+//				}
+//			}
+//			MediaServerItem mediaServerItem = mediaServerService.getOne(mediaServerId);
+//			if (mediaServerItem != null && "-1".equals(mediaServerItem.getStreamNoneReaderDelayMS())) {
+//				ret.put("close", false);
+//			}
+//			return new ResponseEntity<String>(ret.toString(), HttpStatus.OK);
+//		}else {
+//			StreamProxyItem streamProxyItem = streamProxyService.getStreamProxyByAppAndStream(app, streamId);
+//			if (streamProxyItem != null && streamProxyItem.isEnable_remove_none_reader()) {
+//				ret.put("close", true);
+//				streamProxyService.del(app, streamId);
+//				String url = streamProxyItem.getUrl() != null?streamProxyItem.getUrl():streamProxyItem.getSrc_url();
+//				logger.info("[{}/{}]<-[{}] 拉流代理无人观看已经移除",  app, streamId, url);
+//			}else {
+//				ret.put("close", false);
+//			}
+//			return new ResponseEntity<String>(ret.toString(), HttpStatus.OK);
+//		}
+//	}
+//
+//	/**
+//	 * 流未找到事件,用户可以在此事件触发时,立即去拉流,这样可以实现按需拉流;此事件对回复不敏感。
+//	 *
+//	 */
+//	@ResponseBody
+//	@PostMapping(value = "/on_stream_not_found", produces = "application/json;charset=UTF-8")
+//	public ResponseEntity<String> onStreamNotFound(@RequestBody JSONObject json){
+//		if (logger.isDebugEnabled()) {
+//			logger.debug("[ ZLM HOOK ]on_stream_not_found API调用,参数:" + json.toString());
+//		}
+//		String mediaServerId = json.getString("mediaServerId");
+//		MediaServerItem mediaInfo = mediaServerService.getOne(mediaServerId);
+//		if (userSetup.isAutoApplyPlay() && mediaInfo != null) {
+//			String app = json.getString("app");
+//			String streamId = json.getString("stream");
+//			if ("rtp".equals(app)) {
+//				String[] s = streamId.split("_");
+//				if (s.length == 2) {
+//					String id = s[0];
+//					String channelId = s[1];
+//					Device device = redisCatchStorage.getDevice(id);
+//					if (device != null) {
+//						UUID uuid = UUID.randomUUID();
+//						SSRCInfo ssrcInfo;
+//						String streamId2 = null;
+//						if (mediaInfo.isRtpEnable()) {
+//							streamId2 = String.format("%s_%s", device.getId(), channelId);
+//						}
+//						ssrcInfo = mediaServerService.openRTPServer(mediaInfo, streamId2);
+//						cmder.playStreamCmd(mediaInfo, ssrcInfo, device, channelId, (MediaServerItem mediaServerItemInuse, JSONObject response) -> {
+//							logger.info("收到订阅消息: " + response.toJSONString());
+//							playService.onPublishHandlerForPlay(mediaServerItemInuse, response, id, channelId, uuid.toString());
+//						}, null);
+//					}
+//
+//				}
+//			}
+//
+//		}
+//
+//		JSONObject ret = new JSONObject();
+//		ret.put("code", 0);
+//		ret.put("msg", "success");
+//		return new ResponseEntity<String>(ret.toString(), HttpStatus.OK);
+//	}
+//
+//	/**
+//	 * 服务器启动事件,可以用于监听服务器崩溃重启;此事件对回复不敏感。
+//	 *
+//	 */
+//	@ResponseBody
+//	@PostMapping(value = "/on_server_started", produces = "application/json;charset=UTF-8")
+//	public ResponseEntity<String> onServerStarted(HttpServletRequest request, @RequestBody JSONObject jsonObject){
+//
+//		if (logger.isDebugEnabled()) {
+//			logger.debug("[ ZLM HOOK ]on_server_started API调用,参数:" + jsonObject.toString());
+//		}
+//		String remoteAddr = request.getRemoteAddr();
+//		jsonObject.put("ip", remoteAddr);
+//		List<ZLMHttpHookSubscribe.Event> subscribes = this.subscribe.getSubscribes(ZLMHttpHookSubscribe.HookType.on_server_started);
+//		if (subscribes != null  && subscribes.size() > 0) {
+//			for (ZLMHttpHookSubscribe.Event subscribe : subscribes) {
+//				subscribe.response(null, jsonObject);
+//			}
+//		}
+//		JSONObject ret = new JSONObject();
+//		ret.put("code", 0);
+//		ret.put("msg", "success");
+//		return new ResponseEntity<String>(ret.toString(), HttpStatus.OK);
+//	}
+//}

+ 118 - 0
jetlinks-manager/media-manager/src/main/java/org/jetlinks/community/media/zlm/ZLMHttpHookSubscribe.java

@@ -0,0 +1,118 @@
+package org.jetlinks.community.media.zlm;
+
+import com.alibaba.fastjson.JSONObject;
+import org.jetlinks.community.media.zlm.dto.MediaServerItem;
+import org.springframework.stereotype.Component;
+
+import java.util.*;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * @description:针对 ZLMediaServer的hook事件订阅
+ * @author: pan
+ * @date:   2020年12月2日 21:17:32
+ */
+@Component
+public class ZLMHttpHookSubscribe {
+
+    public enum HookType{
+        on_flow_report,
+        on_http_access,
+        on_play,
+        on_publish,
+        on_record_mp4,
+        on_rtsp_auth,
+        on_rtsp_realm,
+        on_shell_login,
+        on_stream_changed,
+        on_stream_none_reader,
+        on_stream_not_found,
+        on_server_started,
+        on_server_keepalive
+    }
+
+    public interface Event{
+        void response(MediaServerItem mediaServerItem, JSONObject response);
+    }
+
+    private Map<HookType, Map<JSONObject, Event>> allSubscribes = new ConcurrentHashMap<>();
+
+    public void addSubscribe(HookType type, JSONObject hookResponse, ZLMHttpHookSubscribe.Event event) {
+        Map<JSONObject, Event> eventMap = allSubscribes.get(type);
+        if (eventMap == null) {
+            eventMap = new HashMap<JSONObject, Event>();
+            allSubscribes.put(type,eventMap);
+        }
+        eventMap.put(hookResponse, event);
+    }
+
+    public ZLMHttpHookSubscribe.Event getSubscribe(HookType type, JSONObject hookResponse) {
+        ZLMHttpHookSubscribe.Event event= null;
+        Map<JSONObject, Event> eventMap = allSubscribes.get(type);
+        if (eventMap == null) {
+            return null;
+        }
+        for (JSONObject key : eventMap.keySet()) {
+            Boolean result = null;
+            for (String s : key.keySet()) {
+                if (result == null) {
+                    result = key.getString(s).equals(hookResponse.getString(s));
+                }else {
+                    if (key.getString(s) == null) {
+                        continue;
+                    }
+                    result = result && key.getString(s).equals(hookResponse.getString(s));
+                }
+
+            }
+            if (null != result && result) {
+                event = eventMap.get(key);
+            }
+        }
+        return event;
+    }
+
+    public void removeSubscribe(HookType type, JSONObject hookResponse) {
+        Map<JSONObject, Event> eventMap = allSubscribes.get(type);
+        if (eventMap == null) {
+            return;
+        }
+        Iterator<Map.Entry<JSONObject, Event>> iterator = eventMap.entrySet().iterator();
+        while (iterator.hasNext()){
+            Map.Entry<JSONObject, Event> next = iterator.next();
+            JSONObject key = next.getKey();
+            Boolean result = null;
+            for (String s : key.keySet()) {
+                if (result == null) {
+                    result = key.getString(s).equals(hookResponse.getString(s));
+                }else {
+                    if (key.getString(s) == null) continue;
+                    result = result && key.getString(s).equals(hookResponse.getString(s));
+                }
+            }
+            if (null != result && result){
+                iterator.remove();
+            }
+        }
+    }
+
+    /**
+     * 获取某个类型的所有的订阅
+     * @param type
+     * @return
+     */
+    public List<Event> getSubscribes(HookType type) {
+        // ZLMHttpHookSubscribe.Event event= null;
+        Map<JSONObject, Event> eventMap = allSubscribes.get(type);
+        if (eventMap == null) {
+            return null;
+        }
+        List<Event> result = new ArrayList<>();
+        for (JSONObject key : eventMap.keySet()) {
+            result.add(eventMap.get(key));
+        }
+        return result;
+    }
+
+
+}

+ 217 - 0
jetlinks-manager/media-manager/src/main/java/org/jetlinks/community/media/zlm/ZLMMediaListManager.java

@@ -0,0 +1,217 @@
+//package org.jetlinks.community.media.zlm;
+//
+//import com.alibaba.fastjson.JSONObject;
+//import com.genersoft.iot.vmp.conf.UserSetup;
+//import com.genersoft.iot.vmp.gb28181.bean.GbStream;
+//import com.genersoft.iot.vmp.media.zlm.dto.MediaItem;
+//import com.genersoft.iot.vmp.media.zlm.dto.MediaServerItem;
+//import com.genersoft.iot.vmp.media.zlm.dto.StreamProxyItem;
+//import com.genersoft.iot.vmp.media.zlm.dto.StreamPushItem;
+//import com.genersoft.iot.vmp.service.IStreamProxyService;
+//import com.genersoft.iot.vmp.service.IStreamPushService;
+//import com.genersoft.iot.vmp.service.bean.ThirdPartyGB;
+//import com.genersoft.iot.vmp.storager.IRedisCatchStorage;
+//import com.genersoft.iot.vmp.storager.IVideoManagerStorager;
+//import com.genersoft.iot.vmp.storager.dao.GbStreamMapper;
+//import com.genersoft.iot.vmp.storager.dao.PlatformGbStreamMapper;
+//import com.genersoft.iot.vmp.storager.dao.StreamPushMapper;
+//import org.slf4j.Logger;
+//import org.slf4j.LoggerFactory;
+//import org.springframework.beans.factory.annotation.Autowired;
+//import org.springframework.stereotype.Component;
+//import org.springframework.util.StringUtils;
+//
+//import java.util.HashMap;
+//import java.util.List;
+//import java.util.Map;
+//import java.util.regex.Matcher;
+//import java.util.regex.Pattern;
+//
+//@Component
+//public class ZLMMediaListManager {
+//
+//    private Logger logger = LoggerFactory.getLogger("ZLMMediaListManager");
+//
+//    @Autowired
+//    private ZLMRESTfulUtils zlmresTfulUtils;
+//
+//    @Autowired
+//    private IRedisCatchStorage redisCatchStorage;
+//
+//    @Autowired
+//    private IVideoManagerStorager storager;
+//
+//    @Autowired
+//    private GbStreamMapper gbStreamMapper;
+//
+//    @Autowired
+//    private PlatformGbStreamMapper platformGbStreamMapper;
+//
+//    @Autowired
+//    private IStreamPushService streamPushService;
+//
+//    @Autowired
+//    private IStreamProxyService streamProxyService;
+//
+//    @Autowired
+//    private StreamPushMapper streamPushMapper;
+//
+//    @Autowired
+//    private ZLMHttpHookSubscribe subscribe;
+//
+//    @Autowired
+//    private UserSetup userSetup;
+//
+//
+//    public void updateMediaList(MediaServerItem mediaServerItem) {
+//        storager.clearMediaList();
+//
+//        // 使用异步的当时更新媒体流列表
+//        zlmresTfulUtils.getMediaList(mediaServerItem, (mediaList ->{
+//            if (mediaList == null) return;
+//            String dataStr = mediaList.getString("data");
+//
+//            Integer code = mediaList.getInteger("code");
+//            Map<String, StreamPushItem> result = new HashMap<>();
+//            List<StreamPushItem> streamPushItems = null;
+//            // 获取所有的国标关联
+////            List<GbStream> gbStreams = gbStreamMapper.selectAllByMediaServerId(mediaServerItem.getId());
+//            if (code == 0 ) {
+//                if (dataStr != null) {
+//                    streamPushItems = streamPushService.handleJSON(dataStr, mediaServerItem);
+//                }
+//            }else {
+//                logger.warn("更新视频流失败,错误code: " + code);
+//            }
+//
+//            if (streamPushItems != null) {
+//                storager.updateMediaList(streamPushItems);
+//                for (StreamPushItem streamPushItem : streamPushItems) {
+//                    JSONObject jsonObject = new JSONObject();
+//                    jsonObject.put("app", streamPushItem.getApp());
+//                    jsonObject.put("stream", streamPushItem.getStream());
+//                    jsonObject.put("mediaServerId", mediaServerItem.getId());
+//                    subscribe.addSubscribe(ZLMHttpHookSubscribe.HookType.on_play,jsonObject,
+//                            (MediaServerItem mediaServerItemInuse, JSONObject response)->{
+//                                updateMedia(mediaServerItem, response.getString("app"), response.getString("stream"));
+//                            }
+//                    );
+//                }
+//            }
+//        }));
+//
+//    }
+//
+//    public void addMedia(MediaServerItem mediaServerItem, String app, String streamId) {
+//        //使用异步更新推流
+//        updateMedia(mediaServerItem, app, streamId);
+//    }
+//
+//    public void addPush(MediaItem mediaItem) {
+//        // 查找此直播流是否存在redis预设gbId
+//        StreamPushItem transform = streamPushService.transform(mediaItem);
+//        // 从streamId取出查询关键值
+//        Pattern pattern = Pattern.compile(userSetup.getThirdPartyGBIdReg());
+//        Matcher matcher = pattern.matcher(mediaItem.getStream());// 指定要匹配的字符串
+//        String queryKey = null;
+//        if (matcher.find()) { //此处find()每次被调用后,会偏移到下一个匹配
+//            queryKey = matcher.group();
+//        }
+//        if (queryKey != null) {
+//            ThirdPartyGB thirdPartyGB = redisCatchStorage.queryMemberNoGBId(queryKey);
+//            if (thirdPartyGB != null && !StringUtils.isEmpty(thirdPartyGB.getNationalStandardNo())) {
+//                transform.setGbId(thirdPartyGB.getNationalStandardNo());
+//                transform.setName(thirdPartyGB.getName());
+//            }
+//        }
+//        storager.updateMedia(transform);
+//        if (!StringUtils.isEmpty(transform.getGbId())) {
+//            // 如果这个国标ID已经给了其他推流且流已离线,则移除其他推流
+//            List<GbStream> gbStreams = gbStreamMapper.selectByGBId(transform.getGbId());
+//            if (gbStreams.size() > 0) {
+//                for (GbStream gbStream : gbStreams) {
+//                    // 出现使用相同国标Id的视频流时,使用新流替换旧流,
+//                    gbStreamMapper.del(gbStream.getApp(), gbStream.getStream());
+//                    platformGbStreamMapper.delByAppAndStream(gbStream.getApp(), gbStream.getStream());
+//                    if (!gbStream.isStatus()) {
+//                        streamPushMapper.del(gbStream.getApp(), gbStream.getStream());
+//                    }
+//                }
+//            }
+//            if (gbStreamMapper.selectOne(transform.getApp(), transform.getStream()) != null) {
+//                gbStreamMapper.update(transform);
+//            }else {
+//                gbStreamMapper.add(transform);
+//            }
+//        }
+//    }
+//
+//
+//    public void updateMedia(MediaServerItem mediaServerItem, String app, String streamId) {
+//        //使用异步更新推流
+//        zlmresTfulUtils.getMediaList(mediaServerItem, app, streamId, "rtmp", json->{
+//
+//            if (json == null) return;
+//            String dataStr = json.getString("data");
+//
+//            Integer code = json.getInteger("code");
+//            Map<String, StreamPushItem> result = new HashMap<>();
+//            List<StreamPushItem> streamPushItems = null;
+//            if (code == 0 ) {
+//                if (dataStr != null) {
+//                    streamPushItems = streamPushService.handleJSON(dataStr, mediaServerItem);
+//                }
+//            }else {
+//                logger.warn("更新视频流失败,错误code: " + code);
+//            }
+//
+//            if (streamPushItems != null && streamPushItems.size() == 1) {
+//                storager.updateMedia(streamPushItems.get(0));
+//            }
+//        });
+//    }
+//
+//
+//    public int removeMedia(String app, String streamId) {
+//        // 查找是否关联了国标, 关联了不删除, 置为离线
+//        StreamProxyItem streamProxyItem = gbStreamMapper.selectOne(app, streamId);
+//        int result = 0;
+//        if (streamProxyItem == null) {
+//            result = storager.removeMedia(app, streamId);
+//        }else {
+//            result =storager.mediaOutline(app, streamId);
+//        }
+//        return result;
+//    }
+//
+//
+//
+////    public void clearAllSessions() {
+////        logger.info("清空所有国标相关的session");
+////        JSONObject allSessionJSON = zlmresTfulUtils.getAllSession();
+////        ZLMServerConfig mediaInfo = redisCatchStorage.getMediaInfo();
+////        HashSet<String> allLocalPorts = new HashSet();
+////        if (allSessionJSON.getInteger("code") == 0) {
+////            JSONArray data = allSessionJSON.getJSONArray("data");
+////            if (data.size() > 0) {
+////                for (int i = 0; i < data.size(); i++) {
+////                    JSONObject sessionJOSN = data.getJSONObject(i);
+////                    Integer local_port = sessionJOSN.getInteger("local_port");
+////                    if (!local_port.equals(Integer.valueOf(mediaInfo.getHttpPort())) &&
+////                        !local_port.equals(Integer.valueOf(mediaInfo.getHttpSSLport())) &&
+////                        !local_port.equals(Integer.valueOf(mediaInfo.getRtmpPort())) &&
+////                        !local_port.equals(Integer.valueOf(mediaInfo.getRtspPort())) &&
+////                        !local_port.equals(Integer.valueOf(mediaInfo.getRtspSSlport())) &&
+////                        !local_port.equals(Integer.valueOf(mediaInfo.getHookOnFlowReport()))){
+////                        allLocalPorts.add(sessionJOSN.getInteger("local_port") + "");
+////                     }
+////                }
+////            }
+////        }
+////        if (allLocalPorts.size() > 0) {
+////            List<String> result = new ArrayList<>(allLocalPorts);
+////            String localPortSStr = String.join(",", result);
+////            zlmresTfulUtils.kickSessions(localPortSStr);
+////        }
+////    }
+//}

+ 287 - 0
jetlinks-manager/media-manager/src/main/java/org/jetlinks/community/media/zlm/ZLMRESTfulUtils.java

@@ -0,0 +1,287 @@
+package org.jetlinks.community.media.zlm;
+
+import cn.hutool.http.HttpRequest;
+import cn.hutool.http.HttpUtil;
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.JSONObject;
+import lombok.extern.slf4j.Slf4j;
+import okhttp3.*;
+import org.jetlinks.community.media.zlm.dto.MediaServerItem;
+import org.springframework.stereotype.Component;
+
+import javax.validation.constraints.NotNull;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.net.ConnectException;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Objects;
+import java.util.concurrent.TimeUnit;
+
+@Component
+@Slf4j(topic = "ZLMRESTfulUtils")
+public class ZLMRESTfulUtils {
+
+
+    public interface RequestCallback{
+        void run(JSONObject response);
+    }
+
+    public JSONObject sendPost(MediaServerItem mediaServerItem, String api, Map<String, Object> param, RequestCallback callback) {
+        OkHttpClient client = new OkHttpClient();
+        String url = String.format("http://%s:%s/index/api/%s",  mediaServerItem.getIp(), mediaServerItem.getHttpPort(), api);
+
+        JSONObject responseJSON = null;
+
+        FormBody.Builder builder = new FormBody.Builder();
+        builder.add("secret",mediaServerItem.getSecret());
+        if (param != null && param.keySet().size() > 0) {
+            for (String key : param.keySet()){
+                if (param.get(key) != null) {
+                    builder.add(key, param.get(key).toString());
+                }
+            }
+        }
+
+        FormBody body = builder.build();
+
+        Request request = new Request.Builder()
+                .post(body)
+                .url(url)
+                .build();
+            if (callback == null) {
+                try {
+                    Response response = client.newCall(request).execute();
+                    if (response.isSuccessful()) {
+                        ResponseBody responseBody = response.body();
+                        if (responseBody != null) {
+                            String responseStr = responseBody.string();
+                            responseJSON = JSON.parseObject(responseStr);
+                        }
+                    }else {
+                        response.close();
+                        Objects.requireNonNull(response.body()).close();
+                    }
+                } catch (ConnectException e) {
+                    log.error(String.format("连接ZLM失败: %s, %s", e.getCause().getMessage(), e.getMessage()));
+                    log.info("请检查media配置并确认ZLM已启动...");
+                }catch (IOException e) {
+                    log.error(String.format("[ %s ]请求失败: %s", url, e.getMessage()));
+                }
+            }else {
+                client.newCall(request).enqueue(new Callback(){
+
+                    @Override
+                    public void onResponse(@NotNull Call call, @NotNull Response response){
+                        if (response.isSuccessful()) {
+                            try {
+                                String responseStr = Objects.requireNonNull(response.body()).string();
+                                callback.run(JSON.parseObject(responseStr));
+                            } catch (IOException e) {
+                                log.error(String.format("[ %s ]请求失败: %s", url, e.getMessage()));
+                            }
+
+                        }else {
+                            response.close();
+                            Objects.requireNonNull(response.body()).close();
+                        }
+                    }
+
+                    @Override
+                    public void onFailure(@NotNull Call call, @NotNull IOException e) {
+                        log.error(String.format("连接ZLM失败: %s, %s", e.getCause().getMessage(), e.getMessage()));
+                        log.info("请检查media配置并确认ZLM已启动...");
+                    }
+                });
+            }
+
+
+
+        return responseJSON;
+    }
+
+    public void sendGetForImg(MediaServerItem mediaServerItem, String api, Map<String, Object> params, String targetPath, String fileName) {
+        String url = String.format("http://%s:%s/index/api/%s", mediaServerItem.getIp(), mediaServerItem.getHttpPort(), api);
+        log.debug(url);
+        HttpUrl parseUrl = HttpUrl.parse(url);
+        if (parseUrl == null) {
+            return;
+        }
+        HttpUrl.Builder httpBuilder = parseUrl.newBuilder();
+
+        httpBuilder.addQueryParameter("secret", mediaServerItem.getSecret());
+        if (params != null) {
+            for (Map.Entry<String, Object> param : params.entrySet()) {
+                httpBuilder.addQueryParameter(param.getKey(), param.getValue().toString());
+            }
+        }
+
+        Request request = new Request.Builder()
+                .url(httpBuilder.build())
+                .build();
+        log.info(request.toString());
+        try {
+            OkHttpClient client = new OkHttpClient.Builder()
+                    .readTimeout(10, TimeUnit.SECONDS)
+                    .build();
+            Response response = client.newCall(request).execute();
+            if (response.isSuccessful()) {
+                log.info("response body contentType: " + Objects.requireNonNull(response.body()).contentType());
+                if (targetPath != null) {
+                    File snapFolder = new File(targetPath);
+                    if (!snapFolder.exists()) {
+                        if (!snapFolder.mkdirs()) {
+                            log.warn("{}路径创建失败", snapFolder.getAbsolutePath());
+                        }
+
+                    }
+                    File snapFile = new File(targetPath + "/" + fileName);
+                    FileOutputStream outStream = new FileOutputStream(snapFile);
+
+                    outStream.write(Objects.requireNonNull(response.body()).bytes());
+                    outStream.close();
+                } else {
+                    log.error(String.format("[ %s ]请求失败: %s %s", url, response.code(), response.message()));
+                }
+                Objects.requireNonNull(response.body()).close();
+            } else {
+                log.error(String.format("[ %s ]请求失败: %s %s", url, response.code(), response.message()));
+            }
+        } catch (ConnectException e) {
+            log.error(String.format("连接ZLM失败: %s, %s", e.getCause().getMessage(), e.getMessage()));
+            log.info("请检查media配置并确认ZLM已启动...");
+        } catch (IOException e) {
+            log.error(String.format("[ %s ]请求失败: %s", url, e.getMessage()));
+        }
+    }
+
+    public JSONObject getMediaList(MediaServerItem mediaServerItem, String app, String stream, String schema, RequestCallback callback){
+        Map<String, Object> param = new HashMap<>();
+        if (app != null) {
+            param.put("app",app);
+        }
+        if (stream != null) {
+            param.put("stream",stream);
+        }
+        if (schema != null) {
+            param.put("schema",schema);
+        }
+        param.put("vhost","__defaultVhost__");
+        return sendPost(mediaServerItem, "getMediaList",param, callback);
+    }
+
+    public JSONObject getMediaList(MediaServerItem mediaServerItem, String app, String stream){
+        return getMediaList(mediaServerItem, app, stream,null,  null);
+    }
+
+    public JSONObject getMediaList(MediaServerItem mediaServerItem, RequestCallback callback){
+        return sendPost(mediaServerItem, "getMediaList",null, callback);
+    }
+
+    public JSONObject getMediaInfo(MediaServerItem mediaServerItem, String app, String schema, String stream){
+        Map<String, Object> param = new HashMap<>();
+        param.put("app",app);
+        param.put("schema",schema);
+        param.put("stream",stream);
+        param.put("vhost","__defaultVhost__");
+        return sendPost(mediaServerItem, "getMediaInfo",param, null);
+    }
+
+    public JSONObject getRtpInfo(MediaServerItem mediaServerItem, String stream_id){
+        Map<String, Object> param = new HashMap<>();
+        param.put("stream_id",stream_id);
+        return sendPost(mediaServerItem, "getRtpInfo",param, null);
+    }
+
+    public JSONObject addFFmpegSource(MediaServerItem mediaServerItem, String src_url, String dst_url, String timeout_ms,
+                                      boolean enable_hls, boolean enable_mp4, String ffmpeg_cmd_key){
+        log.info(src_url);
+        log.info(dst_url);
+        Map<String, Object> param = new HashMap<>();
+        param.put("src_url", src_url);
+        param.put("dst_url", dst_url);
+        param.put("timeout_ms", timeout_ms);
+        param.put("enable_hls", enable_hls);
+        param.put("enable_mp4", enable_mp4);
+        param.put("ffmpeg_cmd_key", ffmpeg_cmd_key);
+        return sendPost(mediaServerItem, "addFFmpegSource",param, null);
+    }
+
+    public JSONObject delFFmpegSource(MediaServerItem mediaServerItem, String key){
+        Map<String, Object> param = new HashMap<>();
+        param.put("key", key);
+        return sendPost(mediaServerItem, "delFFmpegSource",param, null);
+    }
+
+    public JSONObject getMediaServerConfig(MediaServerItem mediaServerItem){
+        return sendPost(mediaServerItem, "getServerConfig",null, null);
+    }
+
+    public JSONObject setServerConfig(MediaServerItem mediaServerItem, Map<String, Object> param){
+        return sendPost(mediaServerItem,"setServerConfig",param, null);
+    }
+
+    public JSONObject openRtpServer(MediaServerItem mediaServerItem, Map<String, Object> param){
+        return sendPost(mediaServerItem, "openRtpServer",param, null);
+    }
+
+    public JSONObject closeRtpServer(MediaServerItem mediaServerItem, Map<String, Object> param) {
+        return sendPost(mediaServerItem, "closeRtpServer",param, null);
+    }
+
+    public JSONObject listRtpServer(MediaServerItem mediaServerItem) {
+        return sendPost(mediaServerItem, "listRtpServer",null, null);
+    }
+
+    public JSONObject startSendRtp(MediaServerItem mediaServerItem, Map<String, Object> param) {
+        return sendPost(mediaServerItem, "startSendRtp",param, null);
+    }
+
+    public JSONObject stopSendRtp(MediaServerItem mediaServerItem, Map<String, Object> param) {
+        return sendPost(mediaServerItem, "stopSendRtp",param, null);
+    }
+
+    public JSONObject restartServer(MediaServerItem mediaServerItem) {
+        return sendPost(mediaServerItem, "restartServer",null, null);
+    }
+
+    public JSONObject addStreamProxy(MediaServerItem mediaServerItem, String app, String stream, String url, boolean enable_hls, boolean enable_mp4, String rtp_type) {
+        Map<String, Object> param = new HashMap<>();
+        param.put("vhost", "__defaultVhost__");
+        param.put("app", app);
+        param.put("stream", stream);
+        param.put("url", url);
+        param.put("enable_hls", enable_hls?1:0);
+        param.put("enable_mp4", enable_mp4?1:0);
+        param.put("rtp_type", rtp_type);
+        return sendPost(mediaServerItem, "addStreamProxy",param, null);
+    }
+
+    public JSONObject closeStreams(MediaServerItem mediaServerItem, String app, String stream) {
+        Map<String, Object> param = new HashMap<>();
+        param.put("vhost", "__defaultVhost__");
+        param.put("app", app);
+        param.put("stream", stream);
+        param.put("force", 1);
+        return sendPost(mediaServerItem, "close_streams",param, null);
+    }
+
+    public JSONObject getAllSession(MediaServerItem mediaServerItem) {
+        return sendPost(mediaServerItem, "getAllSession",null, null);
+    }
+
+    public void kickSessions(MediaServerItem mediaServerItem, String localPortSStr) {
+        Map<String, Object> param = new HashMap<>();
+        param.put("local_port", localPortSStr);
+        sendPost(mediaServerItem, "kick_sessions",param, null);
+    }
+
+    public void getSnap(MediaServerItem mediaServerItem, String flvUrl, int timeout_sec, int expire_sec, String targetPath, String fileName) {
+        Map<String, Object> param = new HashMap<>();
+        param.put("url", flvUrl);
+        param.put("timeout_sec", timeout_sec);
+        param.put("expire_sec", expire_sec);
+        sendGetForImg(mediaServerItem, "getSnap", param, targetPath, fileName);
+    }
+}

+ 269 - 0
jetlinks-manager/media-manager/src/main/java/org/jetlinks/community/media/zlm/ZLMRTPServerFactory.java

@@ -0,0 +1,269 @@
+package org.jetlinks.community.media.zlm;
+
+import com.alibaba.fastjson.JSONArray;
+import com.alibaba.fastjson.JSONObject;
+import lombok.AllArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.jetlinks.community.media.bean.SendRtpItem;
+import org.jetlinks.community.media.zlm.dto.MediaServerItem;
+import org.springframework.stereotype.Component;
+import org.springframework.util.StringUtils;
+
+import java.util.HashMap;
+import java.util.Map;
+
+@Component
+@Slf4j(topic = "ZLMRTPServerFactory")
+@AllArgsConstructor
+public class ZLMRTPServerFactory {
+
+    private final ZLMRESTfulUtils zlmresTfulUtils;
+
+    private int[] portRangeArray = new int[2];
+
+    public int createRTPServer(MediaServerItem mediaServerItem, String streamId) {
+        Map<String, Integer> currentStreams = new HashMap<>();
+        JSONObject listRtpServerJsonResult = zlmresTfulUtils.listRtpServer(mediaServerItem);
+        if (listRtpServerJsonResult != null) {
+            JSONArray data = listRtpServerJsonResult.getJSONArray("data");
+            if (data != null) {
+                for (int i = 0; i < data.size(); i++) {
+                    JSONObject dataItem = data.getJSONObject(i);
+                    currentStreams.put(dataItem.getString("stream_id"), dataItem.getInteger("port"));
+                }
+            }
+        }
+        // 已经在推流
+        if (currentStreams.get(streamId) != null) {
+            Map<String, Object> closeRtpServerParam = new HashMap<>();
+            closeRtpServerParam.put("stream_id", streamId);
+            zlmresTfulUtils.closeRtpServer(mediaServerItem, closeRtpServerParam);
+            currentStreams.remove(streamId);
+        }
+
+        Map<String, Object> param = new HashMap<>();
+        int result = -1;
+        /**
+         * 不设置推流端口端则使用随机端口
+         */
+        if (StringUtils.isEmpty(mediaServerItem.getSendRtpPortRange())){
+            param.put("port", 0);
+        }else {
+            int newPort = getPortFromportRange(mediaServerItem);
+            param.put("port", newPort);
+        }
+        param.put("enable_tcp", 1);
+        param.put("stream_id", streamId);
+        JSONObject openRtpServerResultJson = zlmresTfulUtils.openRtpServer(mediaServerItem, param);
+
+        if (openRtpServerResultJson != null) {
+            switch (openRtpServerResultJson.getInteger("code")){
+                case 0:
+                    result= openRtpServerResultJson.getInteger("port");
+                    break;
+                case -300: // id已经存在, 可能已经在其他端口推流
+                    Map<String, Object> closeRtpServerParam = new HashMap<>();
+                    closeRtpServerParam.put("stream_id", streamId);
+                    zlmresTfulUtils.closeRtpServer(mediaServerItem, closeRtpServerParam);
+                    result = createRTPServer(mediaServerItem, streamId);;
+                    break;
+                case -400: // 端口占用
+                    result= createRTPServer(mediaServerItem, streamId);
+                    break;
+                default:
+                    log.error("创建RTP Server 失败 {}: " + openRtpServerResultJson.getString("msg"),  param.get("port"));
+                    break;
+            }
+        }else {
+            //  检查ZLM状态
+            log.error("创建RTP Server 失败 {}: 请检查ZLM服务", param.get("port"));
+        }
+        return result;
+    }
+
+    public boolean closeRrpServer(MediaServerItem serverItem, String streamId) {
+        boolean result = false;
+        if (serverItem !=null){
+            Map<String, Object> param = new HashMap<>();
+            param.put("stream_id", streamId);
+            JSONObject jsonObject = zlmresTfulUtils.closeRtpServer(serverItem, param);
+            if (jsonObject != null ) {
+                if (jsonObject.getInteger("code") == 0) {
+                    result = jsonObject.getInteger("hit") == 1;
+                }else {
+                    log.error("关闭RTP Server 失败: " + jsonObject.getString("msg"));
+                }
+            }else {
+                //  检查ZLM状态
+                log.error("关闭RTP Server 失败: 请检查ZLM服务");
+            }
+        }
+        return result;
+    }
+
+    private int getPortFromportRange(MediaServerItem mediaServerItem) {
+        int currentPort = mediaServerItem.getCurrentPort();
+        if (currentPort == 0) {
+            String[] portRangeStrArray = mediaServerItem.getSendRtpPortRange().split(",");
+            if (portRangeStrArray.length != 2) {
+                portRangeArray[0] = 30000;
+                portRangeArray[1] = 30500;
+            }else {
+                portRangeArray[0] = Integer.parseInt(portRangeStrArray[0]);
+                portRangeArray[1] = Integer.parseInt(portRangeStrArray[1]);
+            }
+        }
+
+        if (currentPort == 0 || currentPort++ > portRangeArray[1]) {
+            currentPort = portRangeArray[0];
+            mediaServerItem.setCurrentPort(currentPort);
+            return portRangeArray[0];
+        } else {
+            if (currentPort % 2 == 1) {
+                currentPort++;
+            }
+            currentPort++;
+            mediaServerItem.setCurrentPort(currentPort);
+            return currentPort;
+        }
+    }
+
+    /**
+     * 创建一个国标推流
+     * @param ip 推流ip
+     * @param port 推流端口
+     * @param ssrc 推流唯一标识
+     * @param platformId 平台id
+     * @param channelId 通道id
+     * @param tcp 是否为tcp
+     * @return SendRtpItem
+     */
+    public SendRtpItem createSendRtpItem(MediaServerItem serverItem, String ip, int port, String ssrc, String platformId, String id, String channelId, boolean tcp){
+
+        // 使用RTPServer 功能找一个可用的端口
+        String playSsrc = serverItem.getSsrcConfig().getPlaySsrc();
+        int localPort = createRTPServer(serverItem, playSsrc);
+        if (localPort != -1) {
+            // TODO 高并发时可能因为未放入缓存而ssrc冲突
+            serverItem.getSsrcConfig().releaseSsrc(playSsrc);
+            closeRrpServer(serverItem, playSsrc);
+        }else {
+            log.error("没有可用的端口");
+            return null;
+        }
+        SendRtpItem sendRtpItem = new SendRtpItem();
+        sendRtpItem.setIp(ip);
+        sendRtpItem.setPort(port);
+        sendRtpItem.setSsrc(ssrc);
+        sendRtpItem.setPlatformId(platformId);
+        sendRtpItem.setDeviceId(id);
+        sendRtpItem.setChannelId(channelId);
+        sendRtpItem.setTcp(tcp);
+        sendRtpItem.setApp("rtp");
+        sendRtpItem.setLocalPort(localPort);
+        sendRtpItem.setMediaServerId(serverItem.getId());
+        return sendRtpItem;
+    }
+
+    /**
+     * 创建一个直播推流
+     * @param ip 推流ip
+     * @param port 推流端口
+     * @param ssrc 推流唯一标识
+     * @param platformId 平台id
+     * @param channelId 通道id
+     * @param tcp 是否为tcp
+     * @return SendRtpItem
+     */
+    public SendRtpItem createSendRtpItem(MediaServerItem serverItem, String ip, int port, String ssrc, String platformId, String app, String stream, String channelId, boolean tcp){
+        String playSsrc = serverItem.getSsrcConfig().getPlaySsrc();
+        int localPort = createRTPServer(serverItem, playSsrc);
+        if (localPort != -1) {
+            // TODO 高并发时可能因为未放入缓存而ssrc冲突
+            serverItem.getSsrcConfig().releaseSsrc(ssrc);
+            closeRrpServer(serverItem, playSsrc);
+        }else {
+            log.error("没有可用的端口");
+            return null;
+        }
+        SendRtpItem sendRtpItem = new SendRtpItem();
+        sendRtpItem.setIp(ip);
+        sendRtpItem.setPort(port);
+        sendRtpItem.setSsrc(ssrc);
+        sendRtpItem.setApp(app);
+        sendRtpItem.setStreamId(stream);
+        sendRtpItem.setPlatformId(platformId);
+        sendRtpItem.setChannelId(channelId);
+        sendRtpItem.setTcp(tcp);
+        sendRtpItem.setLocalPort(localPort);
+        sendRtpItem.setMediaServerId(serverItem.getId());
+        return sendRtpItem;
+    }
+
+    /**
+     * 调用zlm RESTful API —— startSendRtp
+     */
+    public Boolean startSendRtpStream(MediaServerItem mediaServerItem, Map<String, Object>param) {
+        Boolean result = false;
+        JSONObject jsonObject = zlmresTfulUtils.startSendRtp(mediaServerItem, param);
+        if (jsonObject == null) {
+            log.error("RTP推流失败: 请检查ZLM服务");
+        } else if (jsonObject.getInteger("code") == 0) {
+            result= true;
+            log.info("RTP推流[ {}/{} ]请求成功,本地推流端口:{}" ,param.get("app"), param.get("stream"), jsonObject.getString("local_port"));
+        } else {
+            log.error("RTP推流失败: " + jsonObject.getString("msg"));
+        }
+        return result;
+    }
+
+    /**
+     * 查询待转推的流是否就绪
+     */
+    public Boolean isRtpReady(MediaServerItem mediaServerItem, String streamId) {
+        JSONObject mediaInfo = zlmresTfulUtils.getMediaInfo(mediaServerItem,"rtp", "rtmp", streamId);
+        return (mediaInfo.getInteger("code") == 0 && mediaInfo.getBoolean("online"));
+    }
+
+    /**
+     * 查询待转推的流是否就绪
+     */
+    public Boolean isStreamReady(MediaServerItem mediaServerItem, String app, String streamId) {
+        JSONObject mediaInfo = zlmresTfulUtils.getMediaInfo(mediaServerItem, app, "rtmp", streamId);
+        return (mediaInfo.getInteger("code") == 0 && mediaInfo.getBoolean("online"));
+    }
+
+    /**
+     * 查询转推的流是否有其它观看者
+     * @param streamId
+     * @return
+     */
+    public int totalReaderCount(MediaServerItem mediaServerItem, String app, String streamId) {
+        JSONObject mediaInfo = zlmresTfulUtils.getMediaInfo(mediaServerItem, app, "rtmp", streamId);
+        if (mediaInfo == null) {
+            return 0;
+        }
+        return mediaInfo.getInteger("totalReaderCount");
+    }
+
+    /**
+     * 调用zlm RESTful API —— stopSendRtp
+     */
+    public Boolean stopSendRtpStream(MediaServerItem mediaServerItem, Map<String, Object>param) {
+        Boolean result = false;
+        JSONObject jsonObject = zlmresTfulUtils.stopSendRtp(mediaServerItem, param);
+        if (jsonObject == null) {
+            log.error("停止RTP推流失败: 请检查ZLM服务");
+        } else if (jsonObject.getInteger("code") == 0) {
+            result= true;
+            log.info("停止RTP推流成功");
+        } else {
+            log.error("停止RTP推流失败: " + jsonObject.getString("msg"));
+        }
+        return result;
+    }
+
+    public void closeAllSendRtpStream() {
+
+    }
+}

+ 200 - 0
jetlinks-manager/media-manager/src/main/java/org/jetlinks/community/media/zlm/ZLMRunner.java

@@ -0,0 +1,200 @@
+//package org.jetlinks.community.media.zlm;
+//
+//import com.alibaba.fastjson.JSON;
+//import com.alibaba.fastjson.JSONArray;
+//import com.alibaba.fastjson.JSONObject;
+//import com.genersoft.iot.vmp.conf.MediaConfig;
+//import com.genersoft.iot.vmp.gb28181.event.EventPublisher;
+//import com.genersoft.iot.vmp.media.zlm.dto.MediaServerItem;
+//import com.genersoft.iot.vmp.service.IMediaServerService;
+//import com.genersoft.iot.vmp.service.IStreamProxyService;
+//import com.genersoft.iot.vmp.storager.IRedisCatchStorage;
+//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.boot.CommandLineRunner;
+//import org.springframework.core.annotation.Order;
+//import org.springframework.scheduling.annotation.Async;
+//import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
+//import org.springframework.stereotype.Component;
+//import org.springframework.util.StringUtils;
+//
+//import java.util.*;
+//
+//@Component
+//@Order(value=1)
+//public class ZLMRunner implements CommandLineRunner {
+//
+//    private final static Logger logger = LoggerFactory.getLogger(ZLMRunner.class);
+//
+//    private Map<String, Boolean> startGetMedia;
+//
+//    @Autowired
+//    private ZLMRESTfulUtils zlmresTfulUtils;
+//
+//    @Autowired
+//    private ZLMHttpHookSubscribe hookSubscribe;
+//
+//    @Autowired
+//    private IStreamProxyService streamProxyService;
+//
+//    @Autowired
+//    private EventPublisher publisher;
+//
+//    @Autowired
+//    private IMediaServerService mediaServerService;
+//
+//    @Autowired
+//    private IRedisCatchStorage redisCatchStorage;
+//
+//    @Autowired
+//    private MediaConfig mediaConfig;
+//
+//    @Qualifier("taskExecutor")
+//    @Autowired
+//    private ThreadPoolTaskExecutor taskExecutor;
+//
+//    @Override
+//    public void run(String... strings) throws Exception {
+//        mediaServerService.clearMediaServerForOnline();
+//        MediaServerItem defaultMediaServer = mediaServerService.getDefaultMediaServer();
+//        if (defaultMediaServer == null) {
+//            mediaServerService.addToDatabase(mediaConfig.getMediaSerItem());
+//        }else {
+//            MediaServerItem mediaSerItem = mediaConfig.getMediaSerItem();
+//            mediaSerItem.setId(defaultMediaServer.getId());
+//            mediaServerService.updateToDatabase(mediaSerItem);
+//        }
+//
+//        // 订阅 zlm启动事件, 新的zlm也会从这里进入系统
+//        hookSubscribe.addSubscribe(ZLMHttpHookSubscribe.HookType.on_server_started,null,
+//                (MediaServerItem mediaServerItem, JSONObject response)->{
+//            ZLMServerConfig zlmServerConfig = JSONObject.toJavaObject(response, ZLMServerConfig.class);
+//            if (zlmServerConfig !=null ) {
+//                if (startGetMedia != null) {
+//                    startGetMedia.remove(zlmServerConfig.getGeneralMediaServerId());
+//                }
+//                mediaServerService.zlmServerOnline(zlmServerConfig);
+//            }
+//        });
+//
+//        // 订阅 zlm保活事件, 当zlm离线时做业务的处理
+//        hookSubscribe.addSubscribe(ZLMHttpHookSubscribe.HookType.on_server_keepalive,null,
+//                (MediaServerItem mediaServerItem, JSONObject response)->{
+//                    String mediaServerId = response.getString("mediaServerId");
+//                    if (mediaServerId !=null ) {
+//                        mediaServerService.updateMediaServerKeepalive(mediaServerId, response.getJSONObject("data"));
+//                    }
+//                });
+//
+//        // 获取zlm信息
+//        logger.info("[zlm接入]等待默认zlm中...");
+//
+//        // 获取所有的zlm, 并开启主动连接
+//        List<MediaServerItem> all = mediaServerService.getAllFromDatabase();
+//        if (all.size() == 0) {
+//            all.add(mediaConfig.getMediaSerItem());
+//        }
+//        for (MediaServerItem mediaServerItem : all) {
+//            if (startGetMedia == null) startGetMedia = new HashMap<>();
+//            startGetMedia.put(mediaServerItem.getId(), true);
+//            taskExecutor.execute(()->{
+//                connectZlmServer(mediaServerItem);
+//            });
+//        }
+//        Timer timer = new Timer();
+//        // 10分钟后未连接到则不再去主动连接, TODO 并对重启前使用此在zlm的通道发送bye
+//        timer.schedule(new TimerTask() {
+//            @Override
+//            public void run() {
+//            if (startGetMedia != null) {
+//                Set<String> allZlmId = startGetMedia.keySet();
+//                for (String id : allZlmId) {
+//                    logger.error("[ {} ]]主动连接失败,不再主动连接", id);
+//                }
+//                startGetMedia = null;
+//            }
+//            //  TODO 清理数据库中与redis不匹配的zlm
+//            }
+//        }, 60 * 1000 * 10);
+//    }
+//
+//    @Async
+//    public void connectZlmServer(MediaServerItem mediaServerItem){
+//        ZLMServerConfig zlmServerConfig = getMediaServerConfig(mediaServerItem, 1);
+//        if (zlmServerConfig != null) {
+//            zlmServerConfig.setIp(mediaServerItem.getIp());
+//            zlmServerConfig.setHttpPort(mediaServerItem.getHttpPort());
+//            startGetMedia.remove(mediaServerItem.getId());
+//            mediaServerService.zlmServerOnline(zlmServerConfig);
+//        }
+//    }
+//
+//    public ZLMServerConfig getMediaServerConfig(MediaServerItem mediaServerItem, int index) {
+//        if (startGetMedia == null) { return null;}
+//        if (!mediaServerItem.isDefaultServer() && mediaServerService.getOne(mediaServerItem.getId()) == null) {
+//            return null;
+//        }
+//        if ( startGetMedia.get(mediaServerItem.getId()) == null || !startGetMedia.get(mediaServerItem.getId())) {
+//            return null;
+//        }
+//        JSONObject responseJSON = zlmresTfulUtils.getMediaServerConfig(mediaServerItem);
+//        ZLMServerConfig ZLMServerConfig = null;
+//        if (responseJSON != null) {
+//            JSONArray data = responseJSON.getJSONArray("data");
+//            if (data != null && data.size() > 0) {
+//                ZLMServerConfig = JSON.parseObject(JSON.toJSONString(data.get(0)), ZLMServerConfig.class);
+//                ZLMServerConfig.setIp(mediaServerItem.getIp());
+//            }
+//        } else {
+//            logger.error("[ {} ]-[ {}:{} ]第{}次主动连接失败, 2s后重试",
+//                    mediaServerItem.getId(), mediaServerItem.getIp(), mediaServerItem.getHttpPort(), index);
+//            if (index == 1 && !StringUtils.isEmpty(mediaServerItem.getId())) {
+//                logger.info("[ {} ]-[ {}:{} ]第{}次主动连接失败, 开始清理相关资源",
+//                        mediaServerItem.getId(), mediaServerItem.getIp(), mediaServerItem.getHttpPort(), index);
+//                publisher.zlmOfflineEventPublish(mediaServerItem.getId());
+//            }
+//            try {
+//                Thread.sleep(2000);
+//            } catch (InterruptedException e) {
+//                e.printStackTrace();
+//            }
+//            ZLMServerConfig = getMediaServerConfig(mediaServerItem, index += 1);
+//        }
+//        return ZLMServerConfig;
+//
+//    }
+//
+//    /**
+//     * zlm 连接成功或者zlm重启后
+//     */
+////    private void zLmRunning(ZLMServerConfig zlmServerConfig){
+////        logger.info( "[ id: " + zlmServerConfig.getGeneralMediaServerId() + "] zlm接入成功...");
+////        // 关闭循环获取zlm配置
+////        startGetMedia = false;
+////        MediaServerItem mediaServerItem = new MediaServerItem(zlmServerConfig, sipIp);
+////        storager.updateMediaServer(mediaServerItem);
+////
+////        if (mediaServerItem.isAutoConfig()) setZLMConfig(mediaServerItem);
+////        zlmServerManger.updateServerCatchFromHook(zlmServerConfig);
+////
+////        // 清空所有session
+//////        zlmMediaListManager.clearAllSessions();
+////
+////        // 更新流列表
+////        zlmMediaListManager.updateMediaList(mediaServerItem);
+////        // 恢复流代理, 只查找这个这个流媒体
+////        List<StreamProxyItem> streamProxyListForEnable = storager.getStreamProxyListForEnableInMediaServer(
+////                mediaServerItem.getId(), true);
+////        for (StreamProxyItem streamProxyDto : streamProxyListForEnable) {
+////            logger.info("恢复流代理," + streamProxyDto.getApp() + "/" + streamProxyDto.getStream());
+////            JSONObject jsonObject = streamProxyService.addStreamProxyToZlm(streamProxyDto);
+////            if (jsonObject == null) {
+////                // 设置为未启用
+////                logger.info("恢复流代理失败,请检查流地址后重新启用" + streamProxyDto.getApp() + "/" + streamProxyDto.getStream());
+////                streamProxyService.stop(streamProxyDto.getApp(), streamProxyDto.getStream());
+////            }
+////        }
+////    }
+//}

+ 805 - 0
jetlinks-manager/media-manager/src/main/java/org/jetlinks/community/media/zlm/ZLMServerConfig.java

@@ -0,0 +1,805 @@
+package org.jetlinks.community.media.zlm;
+
+import com.alibaba.fastjson.annotation.JSONField;
+
+public class ZLMServerConfig {
+
+    @JSONField(name = "api.apiDebug")
+    private String apiDebug;
+
+    @JSONField(name = "api.secret")
+    private String apiSecret;
+
+    @JSONField(name = "ffmpeg.bin")
+    private String ffmpegBin;
+
+    @JSONField(name = "ffmpeg.cmd")
+    private String ffmpegCmd;
+
+    @JSONField(name = "ffmpeg.log")
+    private String ffmpegLog;
+
+    @JSONField(name = "general.enableVhost")
+    private String generalEnableVhost;
+
+    @JSONField(name = "general.mediaServerId")
+    private String generalMediaServerId;
+
+    @JSONField(name = "general.flowThreshold")
+    private String generalFlowThreshold;
+
+    @JSONField(name = "general.maxStreamWaitMS")
+    private String generalMaxStreamWaitMS;
+
+    @JSONField(name = "general.streamNoneReaderDelayMS")
+    private int generalStreamNoneReaderDelayMS;
+
+    @JSONField(name = "ip")
+    private String ip;
+
+    private String sdpIp;
+
+    private String streamIp;
+
+    private String hookIp;
+
+    private String updateTime;
+
+    private String createTime;
+
+    @JSONField(name = "hls.fileBufSize")
+    private String hlsFileBufSize;
+
+    @JSONField(name = "hls.filePath")
+    private String hlsFilePath;
+
+    @JSONField(name = "hls.segDur")
+    private String hlsSegDur;
+
+    @JSONField(name = "hls.segNum")
+    private String hlsSegNum;
+
+    @JSONField(name = "hook.access_file_except_hls")
+    private String hookAccessFileExceptHLS;
+
+    @JSONField(name = "hook.admin_params")
+    private String hookAdminParams;
+
+    @JSONField(name = "hook.alive_interval")
+    private int hookAliveInterval;
+
+    @JSONField(name = "hook.enable")
+    private String hookEnable;
+
+    @JSONField(name = "hook.on_flow_report")
+    private String hookOnFlowReport;
+
+    @JSONField(name = "hook.on_http_access")
+    private String hookOnHttpAccess;
+
+    @JSONField(name = "hook.on_play")
+    private String hookOnPlay;
+
+    @JSONField(name = "hook.on_publish")
+    private String hookOnPublish;
+
+    @JSONField(name = "hook.on_record_mp4")
+    private String hookOnRecordMp4;
+
+    @JSONField(name = "hook.on_rtsp_auth")
+    private String hookOnRtspAuth;
+
+    @JSONField(name = "hook.on_rtsp_realm")
+    private String hookOnRtspRealm;
+
+    @JSONField(name = "hook.on_shell_login")
+    private String hookOnShellLogin;
+
+    @JSONField(name = "hook.on_stream_changed")
+    private String hookOnStreamChanged;
+
+    @JSONField(name = "hook.on_stream_none_reader")
+    private String hookOnStreamNoneReader;
+
+    @JSONField(name = "hook.on_stream_not_found")
+    private String hookOnStreamNotFound;
+
+    @JSONField(name = "hook.timeoutSec")
+    private String hookTimeoutSec;
+
+    @JSONField(name = "http.charSet")
+    private String httpCharSet;
+
+    @JSONField(name = "http.keepAliveSecond")
+    private String httpKeepAliveSecond;
+
+    @JSONField(name = "http.maxReqCount")
+    private String httpMaxReqCount;
+
+    @JSONField(name = "http.maxReqSize")
+    private String httpMaxReqSize;
+
+    @JSONField(name = "http.notFound")
+    private String httpNotFound;
+
+    @JSONField(name = "http.port")
+    private int httpPort;
+
+    @JSONField(name = "http.rootPath")
+    private String httpRootPath;
+
+    @JSONField(name = "http.sendBufSize")
+    private String httpSendBufSize;
+
+    @JSONField(name = "http.sslport")
+    private int httpSSLport;
+
+    @JSONField(name = "multicast.addrMax")
+    private String multicastAddrMax;
+
+    @JSONField(name = "multicast.addrMin")
+    private String multicastAddrMin;
+
+    @JSONField(name = "multicast.udpTTL")
+    private String multicastUdpTTL;
+
+    @JSONField(name = "record.appName")
+    private String recordAppName;
+
+    @JSONField(name = "record.filePath")
+    private String recordFilePath;
+
+    @JSONField(name = "record.fileSecond")
+    private String recordFileSecond;
+
+    @JSONField(name = "record.sampleMS")
+    private String recordFileSampleMS;
+
+    @JSONField(name = "rtmp.handshakeSecond")
+    private String rtmpHandshakeSecond;
+
+    @JSONField(name = "rtmp.keepAliveSecond")
+    private String rtmpKeepAliveSecond;
+
+    @JSONField(name = "rtmp.modifyStamp")
+    private String rtmpModifyStamp;
+
+    @JSONField(name = "rtmp.port")
+    private int rtmpPort;
+
+    @JSONField(name = "rtmp.sslport")
+    private int rtmpSslPort;
+
+    @JSONField(name = "rtp.audioMtuSize")
+    private String rtpAudioMtuSize;
+
+    @JSONField(name = "rtp.clearCount")
+    private String rtpClearCount;
+
+    @JSONField(name = "rtp.cycleMS")
+    private String rtpCycleMS;
+
+    @JSONField(name = "rtp.maxRtpCount")
+    private String rtpMaxRtpCount;
+
+    @JSONField(name = "rtp.videoMtuSize")
+    private String rtpVideoMtuSize;
+
+    @JSONField(name = "rtp_proxy.checkSource")
+    private String rtpProxyCheckSource;
+
+    @JSONField(name = "rtp_proxy.dumpDir")
+    private String rtpProxyDumpDir;
+
+    @JSONField(name = "rtp_proxy.port")
+    private int rtpProxyPort;
+
+    @JSONField(name = "rtp_proxy.timeoutSec")
+    private String rtpProxyTimeoutSec;
+
+    @JSONField(name = "rtsp.authBasic")
+    private String rtspAuthBasic;
+
+    @JSONField(name = "rtsp.handshakeSecond")
+    private String rtspHandshakeSecond;
+
+    @JSONField(name = "rtsp.keepAliveSecond")
+    private String rtspKeepAliveSecond;
+
+    @JSONField(name = "rtsp.port")
+    private int rtspPort;
+
+    @JSONField(name = "rtsp.sslport")
+    private int rtspSSlport;
+
+    @JSONField(name = "shell.maxReqSize")
+    private String shellMaxReqSize;
+
+    @JSONField(name = "shell.shell")
+    private String shellPhell;
+
+
+    public String getHookIp() {
+        return hookIp;
+    }
+
+    public void setHookIp(String hookIp) {
+        this.hookIp = hookIp;
+    }
+
+    public String getApiDebug() {
+        return apiDebug;
+    }
+
+    public void setApiDebug(String apiDebug) {
+        this.apiDebug = apiDebug;
+    }
+
+    public String getApiSecret() {
+        return apiSecret;
+    }
+
+    public void setApiSecret(String apiSecret) {
+        this.apiSecret = apiSecret;
+    }
+
+    public String getFfmpegBin() {
+        return ffmpegBin;
+    }
+
+    public void setFfmpegBin(String ffmpegBin) {
+        this.ffmpegBin = ffmpegBin;
+    }
+
+    public String getFfmpegCmd() {
+        return ffmpegCmd;
+    }
+
+    public void setFfmpegCmd(String ffmpegCmd) {
+        this.ffmpegCmd = ffmpegCmd;
+    }
+
+    public String getFfmpegLog() {
+        return ffmpegLog;
+    }
+
+    public void setFfmpegLog(String ffmpegLog) {
+        this.ffmpegLog = ffmpegLog;
+    }
+
+    public String getGeneralEnableVhost() {
+        return generalEnableVhost;
+    }
+
+    public void setGeneralEnableVhost(String generalEnableVhost) {
+        this.generalEnableVhost = generalEnableVhost;
+    }
+
+    public String getGeneralMediaServerId() {
+        return generalMediaServerId;
+    }
+
+    public void setGeneralMediaServerId(String generalMediaServerId) {
+        this.generalMediaServerId = generalMediaServerId;
+    }
+
+    public String getGeneralFlowThreshold() {
+        return generalFlowThreshold;
+    }
+
+    public void setGeneralFlowThreshold(String generalFlowThreshold) {
+        this.generalFlowThreshold = generalFlowThreshold;
+    }
+
+    public String getGeneralMaxStreamWaitMS() {
+        return generalMaxStreamWaitMS;
+    }
+
+    public void setGeneralMaxStreamWaitMS(String generalMaxStreamWaitMS) {
+        this.generalMaxStreamWaitMS = generalMaxStreamWaitMS;
+    }
+
+    public int getGeneralStreamNoneReaderDelayMS() {
+        return generalStreamNoneReaderDelayMS;
+    }
+
+    public void setGeneralStreamNoneReaderDelayMS(int generalStreamNoneReaderDelayMS) {
+        this.generalStreamNoneReaderDelayMS = generalStreamNoneReaderDelayMS;
+    }
+
+    public String getIp() {
+        return ip;
+    }
+
+    public void setIp(String ip) {
+        this.ip = ip;
+    }
+
+    public String getSdpIp() {
+        return sdpIp;
+    }
+
+    public void setSdpIp(String sdpIp) {
+        this.sdpIp = sdpIp;
+    }
+
+    public String getStreamIp() {
+        return streamIp;
+    }
+
+    public void setStreamIp(String streamIp) {
+        this.streamIp = streamIp;
+    }
+
+    public String getUpdateTime() {
+        return updateTime;
+    }
+
+    public void setUpdateTime(String updateTime) {
+        this.updateTime = updateTime;
+    }
+
+    public String getCreateTime() {
+        return createTime;
+    }
+
+    public void setCreateTime(String createTime) {
+        this.createTime = createTime;
+    }
+
+    public String getHlsFileBufSize() {
+        return hlsFileBufSize;
+    }
+
+    public void setHlsFileBufSize(String hlsFileBufSize) {
+        this.hlsFileBufSize = hlsFileBufSize;
+    }
+
+    public String getHlsFilePath() {
+        return hlsFilePath;
+    }
+
+    public void setHlsFilePath(String hlsFilePath) {
+        this.hlsFilePath = hlsFilePath;
+    }
+
+    public String getHlsSegDur() {
+        return hlsSegDur;
+    }
+
+    public void setHlsSegDur(String hlsSegDur) {
+        this.hlsSegDur = hlsSegDur;
+    }
+
+    public String getHlsSegNum() {
+        return hlsSegNum;
+    }
+
+    public void setHlsSegNum(String hlsSegNum) {
+        this.hlsSegNum = hlsSegNum;
+    }
+
+    public String getHookAccessFileExceptHLS() {
+        return hookAccessFileExceptHLS;
+    }
+
+    public void setHookAccessFileExceptHLS(String hookAccessFileExceptHLS) {
+        this.hookAccessFileExceptHLS = hookAccessFileExceptHLS;
+    }
+
+    public String getHookAdminParams() {
+        return hookAdminParams;
+    }
+
+    public void setHookAdminParams(String hookAdminParams) {
+        this.hookAdminParams = hookAdminParams;
+    }
+
+    public String getHookEnable() {
+        return hookEnable;
+    }
+
+    public void setHookEnable(String hookEnable) {
+        this.hookEnable = hookEnable;
+    }
+
+    public String getHookOnFlowReport() {
+        return hookOnFlowReport;
+    }
+
+    public void setHookOnFlowReport(String hookOnFlowReport) {
+        this.hookOnFlowReport = hookOnFlowReport;
+    }
+
+    public String getHookOnHttpAccess() {
+        return hookOnHttpAccess;
+    }
+
+    public void setHookOnHttpAccess(String hookOnHttpAccess) {
+        this.hookOnHttpAccess = hookOnHttpAccess;
+    }
+
+    public String getHookOnPlay() {
+        return hookOnPlay;
+    }
+
+    public void setHookOnPlay(String hookOnPlay) {
+        this.hookOnPlay = hookOnPlay;
+    }
+
+    public String getHookOnPublish() {
+        return hookOnPublish;
+    }
+
+    public void setHookOnPublish(String hookOnPublish) {
+        this.hookOnPublish = hookOnPublish;
+    }
+
+    public String getHookOnRecordMp4() {
+        return hookOnRecordMp4;
+    }
+
+    public void setHookOnRecordMp4(String hookOnRecordMp4) {
+        this.hookOnRecordMp4 = hookOnRecordMp4;
+    }
+
+    public String getHookOnRtspAuth() {
+        return hookOnRtspAuth;
+    }
+
+    public void setHookOnRtspAuth(String hookOnRtspAuth) {
+        this.hookOnRtspAuth = hookOnRtspAuth;
+    }
+
+    public String getHookOnRtspRealm() {
+        return hookOnRtspRealm;
+    }
+
+    public void setHookOnRtspRealm(String hookOnRtspRealm) {
+        this.hookOnRtspRealm = hookOnRtspRealm;
+    }
+
+    public String getHookOnShellLogin() {
+        return hookOnShellLogin;
+    }
+
+    public void setHookOnShellLogin(String hookOnShellLogin) {
+        this.hookOnShellLogin = hookOnShellLogin;
+    }
+
+    public String getHookOnStreamChanged() {
+        return hookOnStreamChanged;
+    }
+
+    public void setHookOnStreamChanged(String hookOnStreamChanged) {
+        this.hookOnStreamChanged = hookOnStreamChanged;
+    }
+
+    public String getHookOnStreamNoneReader() {
+        return hookOnStreamNoneReader;
+    }
+
+    public void setHookOnStreamNoneReader(String hookOnStreamNoneReader) {
+        this.hookOnStreamNoneReader = hookOnStreamNoneReader;
+    }
+
+    public String getHookOnStreamNotFound() {
+        return hookOnStreamNotFound;
+    }
+
+    public void setHookOnStreamNotFound(String hookOnStreamNotFound) {
+        this.hookOnStreamNotFound = hookOnStreamNotFound;
+    }
+
+    public String getHookTimeoutSec() {
+        return hookTimeoutSec;
+    }
+
+    public void setHookTimeoutSec(String hookTimeoutSec) {
+        this.hookTimeoutSec = hookTimeoutSec;
+    }
+
+    public String getHttpCharSet() {
+        return httpCharSet;
+    }
+
+    public void setHttpCharSet(String httpCharSet) {
+        this.httpCharSet = httpCharSet;
+    }
+
+    public String getHttpKeepAliveSecond() {
+        return httpKeepAliveSecond;
+    }
+
+    public void setHttpKeepAliveSecond(String httpKeepAliveSecond) {
+        this.httpKeepAliveSecond = httpKeepAliveSecond;
+    }
+
+    public String getHttpMaxReqCount() {
+        return httpMaxReqCount;
+    }
+
+    public void setHttpMaxReqCount(String httpMaxReqCount) {
+        this.httpMaxReqCount = httpMaxReqCount;
+    }
+
+    public String getHttpMaxReqSize() {
+        return httpMaxReqSize;
+    }
+
+    public void setHttpMaxReqSize(String httpMaxReqSize) {
+        this.httpMaxReqSize = httpMaxReqSize;
+    }
+
+    public String getHttpNotFound() {
+        return httpNotFound;
+    }
+
+    public void setHttpNotFound(String httpNotFound) {
+        this.httpNotFound = httpNotFound;
+    }
+
+    public int getHttpPort() {
+        return httpPort;
+    }
+
+    public void setHttpPort(int httpPort) {
+        this.httpPort = httpPort;
+    }
+
+    public String getHttpRootPath() {
+        return httpRootPath;
+    }
+
+    public void setHttpRootPath(String httpRootPath) {
+        this.httpRootPath = httpRootPath;
+    }
+
+    public String getHttpSendBufSize() {
+        return httpSendBufSize;
+    }
+
+    public void setHttpSendBufSize(String httpSendBufSize) {
+        this.httpSendBufSize = httpSendBufSize;
+    }
+
+    public int getHttpSSLport() {
+        return httpSSLport;
+    }
+
+    public void setHttpSSLport(int httpSSLport) {
+        this.httpSSLport = httpSSLport;
+    }
+
+    public String getMulticastAddrMax() {
+        return multicastAddrMax;
+    }
+
+    public void setMulticastAddrMax(String multicastAddrMax) {
+        this.multicastAddrMax = multicastAddrMax;
+    }
+
+    public String getMulticastAddrMin() {
+        return multicastAddrMin;
+    }
+
+    public void setMulticastAddrMin(String multicastAddrMin) {
+        this.multicastAddrMin = multicastAddrMin;
+    }
+
+    public String getMulticastUdpTTL() {
+        return multicastUdpTTL;
+    }
+
+    public void setMulticastUdpTTL(String multicastUdpTTL) {
+        this.multicastUdpTTL = multicastUdpTTL;
+    }
+
+    public String getRecordAppName() {
+        return recordAppName;
+    }
+
+    public void setRecordAppName(String recordAppName) {
+        this.recordAppName = recordAppName;
+    }
+
+    public String getRecordFilePath() {
+        return recordFilePath;
+    }
+
+    public void setRecordFilePath(String recordFilePath) {
+        this.recordFilePath = recordFilePath;
+    }
+
+    public String getRecordFileSecond() {
+        return recordFileSecond;
+    }
+
+    public void setRecordFileSecond(String recordFileSecond) {
+        this.recordFileSecond = recordFileSecond;
+    }
+
+    public String getRecordFileSampleMS() {
+        return recordFileSampleMS;
+    }
+
+    public void setRecordFileSampleMS(String recordFileSampleMS) {
+        this.recordFileSampleMS = recordFileSampleMS;
+    }
+
+    public String getRtmpHandshakeSecond() {
+        return rtmpHandshakeSecond;
+    }
+
+    public void setRtmpHandshakeSecond(String rtmpHandshakeSecond) {
+        this.rtmpHandshakeSecond = rtmpHandshakeSecond;
+    }
+
+    public String getRtmpKeepAliveSecond() {
+        return rtmpKeepAliveSecond;
+    }
+
+    public void setRtmpKeepAliveSecond(String rtmpKeepAliveSecond) {
+        this.rtmpKeepAliveSecond = rtmpKeepAliveSecond;
+    }
+
+    public String getRtmpModifyStamp() {
+        return rtmpModifyStamp;
+    }
+
+    public void setRtmpModifyStamp(String rtmpModifyStamp) {
+        this.rtmpModifyStamp = rtmpModifyStamp;
+    }
+
+    public int getRtmpPort() {
+        return rtmpPort;
+    }
+
+    public void setRtmpPort(int rtmpPort) {
+        this.rtmpPort = rtmpPort;
+    }
+
+    public int getRtmpSslPort() {
+        return rtmpSslPort;
+    }
+
+    public void setRtmpSslPort(int rtmpSslPort) {
+        this.rtmpSslPort = rtmpSslPort;
+    }
+
+    public String getRtpAudioMtuSize() {
+        return rtpAudioMtuSize;
+    }
+
+    public void setRtpAudioMtuSize(String rtpAudioMtuSize) {
+        this.rtpAudioMtuSize = rtpAudioMtuSize;
+    }
+
+    public String getRtpClearCount() {
+        return rtpClearCount;
+    }
+
+    public void setRtpClearCount(String rtpClearCount) {
+        this.rtpClearCount = rtpClearCount;
+    }
+
+    public String getRtpCycleMS() {
+        return rtpCycleMS;
+    }
+
+    public void setRtpCycleMS(String rtpCycleMS) {
+        this.rtpCycleMS = rtpCycleMS;
+    }
+
+    public String getRtpMaxRtpCount() {
+        return rtpMaxRtpCount;
+    }
+
+    public void setRtpMaxRtpCount(String rtpMaxRtpCount) {
+        this.rtpMaxRtpCount = rtpMaxRtpCount;
+    }
+
+    public String getRtpVideoMtuSize() {
+        return rtpVideoMtuSize;
+    }
+
+    public void setRtpVideoMtuSize(String rtpVideoMtuSize) {
+        this.rtpVideoMtuSize = rtpVideoMtuSize;
+    }
+
+    public String getRtpProxyCheckSource() {
+        return rtpProxyCheckSource;
+    }
+
+    public void setRtpProxyCheckSource(String rtpProxyCheckSource) {
+        this.rtpProxyCheckSource = rtpProxyCheckSource;
+    }
+
+    public String getRtpProxyDumpDir() {
+        return rtpProxyDumpDir;
+    }
+
+    public void setRtpProxyDumpDir(String rtpProxyDumpDir) {
+        this.rtpProxyDumpDir = rtpProxyDumpDir;
+    }
+
+    public int getRtpProxyPort() {
+        return rtpProxyPort;
+    }
+
+    public void setRtpProxyPort(int rtpProxyPort) {
+        this.rtpProxyPort = rtpProxyPort;
+    }
+
+    public String getRtpProxyTimeoutSec() {
+        return rtpProxyTimeoutSec;
+    }
+
+    public void setRtpProxyTimeoutSec(String rtpProxyTimeoutSec) {
+        this.rtpProxyTimeoutSec = rtpProxyTimeoutSec;
+    }
+
+    public String getRtspAuthBasic() {
+        return rtspAuthBasic;
+    }
+
+    public void setRtspAuthBasic(String rtspAuthBasic) {
+        this.rtspAuthBasic = rtspAuthBasic;
+    }
+
+    public String getRtspHandshakeSecond() {
+        return rtspHandshakeSecond;
+    }
+
+    public void setRtspHandshakeSecond(String rtspHandshakeSecond) {
+        this.rtspHandshakeSecond = rtspHandshakeSecond;
+    }
+
+    public String getRtspKeepAliveSecond() {
+        return rtspKeepAliveSecond;
+    }
+
+    public void setRtspKeepAliveSecond(String rtspKeepAliveSecond) {
+        this.rtspKeepAliveSecond = rtspKeepAliveSecond;
+    }
+
+    public int getRtspPort() {
+        return rtspPort;
+    }
+
+    public void setRtspPort(int rtspPort) {
+        this.rtspPort = rtspPort;
+    }
+
+    public int getRtspSSlport() {
+        return rtspSSlport;
+    }
+
+    public void setRtspSSlport(int rtspSSlport) {
+        this.rtspSSlport = rtspSSlport;
+    }
+
+    public String getShellMaxReqSize() {
+        return shellMaxReqSize;
+    }
+
+    public void setShellMaxReqSize(String shellMaxReqSize) {
+        this.shellMaxReqSize = shellMaxReqSize;
+    }
+
+    public String getShellPhell() {
+        return shellPhell;
+    }
+
+    public void setShellPhell(String shellPhell) {
+        this.shellPhell = shellPhell;
+    }
+
+    public int getHookAliveInterval() {
+        return hookAliveInterval;
+    }
+
+    public void setHookAliveInterval(int hookAliveInterval) {
+        this.hookAliveInterval = hookAliveInterval;
+    }
+}

+ 405 - 0
jetlinks-manager/media-manager/src/main/java/org/jetlinks/community/media/zlm/dto/MediaItem.java

@@ -0,0 +1,405 @@
+package org.jetlinks.community.media.zlm.dto;
+
+import java.util.List;
+
+public class MediaItem {
+
+    /**
+     * 注册/注销
+     */
+    private boolean regist;
+
+    /**
+     * 应用名
+     */
+    private String app;
+
+    /**
+     * 流id
+     */
+    private String stream;
+
+    /**
+     * 观看总人数,包括hls/rtsp/rtmp/http-flv/ws-flv
+     */
+    private String totalReaderCount;
+
+    /**
+     * 协议 包括hls/rtsp/rtmp/http-flv/ws-flv
+     */
+    private String schema;
+
+
+    /**
+     * 产生源类型,
+     * unknown = 0,
+     * rtmp_push=1,
+     * rtsp_push=2,
+     * rtp_push=3,
+     * pull=4,
+     * ffmpeg_pull=5,
+     * mp4_vod=6,
+     * device_chn=7
+     */
+    private int originType;
+
+    /**
+     * 客户端和服务器网络信息,可能为null类型
+     */
+    private OriginSock originSock;
+
+    /**
+     * 产生源类型的字符串描述
+     */
+    private String originTypeStr;
+
+    /**
+     * 产生源的url
+     */
+    private String originUrl;
+
+    /**
+     * 服务器id
+     */
+    private String mediaServerId;
+
+    /**
+     * GMT unix系统时间戳,单位秒
+     */
+    private Long createStamp;
+
+    /**
+     * 存活时间,单位秒
+     */
+    private Long aliveSecond;
+
+    /**
+     * 数据产生速度,单位byte/s
+     */
+    private Long bytesSpeed;
+
+    /**
+     * 音视频轨道
+     */
+    private List<MediaTrack> tracks;
+
+    /**
+     * 音视频轨道
+     */
+    private String vhost;
+
+    public boolean isRegist() {
+        return regist;
+    }
+
+    public void setRegist(boolean regist) {
+        this.regist = regist;
+    }
+
+    /**
+     * 是否是docker部署, docker部署不会自动更新zlm使用的端口,需要自己手动修改
+     */
+    private boolean docker;
+
+    public static class MediaTrack {
+        /**
+         * 音频通道数
+         */
+        private int channels;
+
+        /**
+         *  H264 = 0, H265 = 1, AAC = 2, G711A = 3, G711U = 4
+         */
+        private int codecId;
+
+        /**
+         * 编码类型名称 CodecAAC CodecH264
+         */
+        private String codecIdName;
+
+        /**
+         * Video = 0, Audio = 1
+         */
+        private int codecType;
+
+        /**
+         * 轨道是否准备就绪
+         */
+        private boolean ready;
+
+        /**
+         * 音频采样位数
+         */
+        private int sampleBit;
+
+        /**
+         * 音频采样率
+         */
+        private int sampleRate;
+
+        /**
+         * 视频fps
+         */
+        private int fps;
+
+        /**
+         * 视频高
+         */
+        private int height;
+
+        /**
+         * 视频宽
+         */
+        private int width;
+
+        public int getChannels() {
+            return channels;
+        }
+
+        public void setChannels(int channels) {
+            this.channels = channels;
+        }
+
+        public int getCodecId() {
+            return codecId;
+        }
+
+        public void setCodecId(int codecId) {
+            this.codecId = codecId;
+        }
+
+        public String getCodecIdName() {
+            return codecIdName;
+        }
+
+        public void setCodecIdName(String codecIdName) {
+            this.codecIdName = codecIdName;
+        }
+
+        public int getCodecType() {
+            return codecType;
+        }
+
+        public void setCodecType(int codecType) {
+            this.codecType = codecType;
+        }
+
+        public boolean isReady() {
+            return ready;
+        }
+
+        public void setReady(boolean ready) {
+            this.ready = ready;
+        }
+
+        public int getSampleBit() {
+            return sampleBit;
+        }
+
+        public void setSampleBit(int sampleBit) {
+            this.sampleBit = sampleBit;
+        }
+
+        public int getSampleRate() {
+            return sampleRate;
+        }
+
+        public void setSampleRate(int sampleRate) {
+            this.sampleRate = sampleRate;
+        }
+
+        public int getFps() {
+            return fps;
+        }
+
+        public void setFps(int fps) {
+            this.fps = fps;
+        }
+
+        public int getHeight() {
+            return height;
+        }
+
+        public void setHeight(int height) {
+            this.height = height;
+        }
+
+        public int getWidth() {
+            return width;
+        }
+
+        public void setWidth(int width) {
+            this.width = width;
+        }
+    }
+
+    public static class OriginSock{
+        private String identifier;
+        private String local_ip;
+        private int local_port;
+        private String peer_ip;
+        private int peer_port;
+
+        public String getIdentifier() {
+            return identifier;
+        }
+
+        public void setIdentifier(String identifier) {
+            this.identifier = identifier;
+        }
+
+        public String getLocal_ip() {
+            return local_ip;
+        }
+
+        public void setLocal_ip(String local_ip) {
+            this.local_ip = local_ip;
+        }
+
+        public int getLocal_port() {
+            return local_port;
+        }
+
+        public void setLocal_port(int local_port) {
+            this.local_port = local_port;
+        }
+
+        public String getPeer_ip() {
+            return peer_ip;
+        }
+
+        public void setPeer_ip(String peer_ip) {
+            this.peer_ip = peer_ip;
+        }
+
+        public int getPeer_port() {
+            return peer_port;
+        }
+
+        public void setPeer_port(int peer_port) {
+            this.peer_port = peer_port;
+        }
+    }
+
+    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 getTotalReaderCount() {
+        return totalReaderCount;
+    }
+
+    public void setTotalReaderCount(String totalReaderCount) {
+        this.totalReaderCount = totalReaderCount;
+    }
+
+
+    public int getOriginType() {
+        return originType;
+    }
+
+    public void setOriginType(int originType) {
+        this.originType = originType;
+    }
+
+
+    public String getOriginTypeStr() {
+        return originTypeStr;
+    }
+
+    public void setOriginTypeStr(String originTypeStr) {
+        this.originTypeStr = originTypeStr;
+    }
+
+    public String getOriginUrl() {
+        return originUrl;
+    }
+
+    public void setOriginUrl(String originUrl) {
+        this.originUrl = originUrl;
+    }
+
+    public Long getCreateStamp() {
+        return createStamp;
+    }
+
+    public void setCreateStamp(Long createStamp) {
+        this.createStamp = createStamp;
+    }
+
+    public Long getAliveSecond() {
+        return aliveSecond;
+    }
+
+    public void setAliveSecond(Long aliveSecond) {
+        this.aliveSecond = aliveSecond;
+    }
+
+    public List<MediaTrack> getTracks() {
+        return tracks;
+    }
+
+    public void setTracks(List<MediaTrack> tracks) {
+        this.tracks = tracks;
+    }
+
+    public String getSchema() {
+        return schema;
+    }
+
+    public void setSchema(String schema) {
+        this.schema = schema;
+    }
+
+    public void setOriginSock(OriginSock originSock) {
+        this.originSock = originSock;
+    }
+
+    public Long getBytesSpeed() {
+        return bytesSpeed;
+    }
+
+    public void setBytesSpeed(Long bytesSpeed) {
+        this.bytesSpeed = bytesSpeed;
+    }
+
+    public String getVhost() {
+        return vhost;
+    }
+
+    public void setVhost(String vhost) {
+        this.vhost = vhost;
+    }
+
+    public OriginSock getOriginSock() {
+        return originSock;
+    }
+
+    public boolean isDocker() {
+        return docker;
+    }
+
+    public void setDocker(boolean docker) {
+        this.docker = docker;
+    }
+
+    public String getMediaServerId() {
+        return mediaServerId;
+    }
+
+    public void setMediaServerId(String mediaServerId) {
+        this.mediaServerId = mediaServerId;
+    }
+}

+ 101 - 0
jetlinks-manager/media-manager/src/main/java/org/jetlinks/community/media/zlm/dto/MediaServerItem.java

@@ -0,0 +1,101 @@
+package org.jetlinks.community.media.zlm.dto;
+
+
+import lombok.Data;
+//import org.jetlinks.community.media.zlm.ZLMServerConfig;
+import org.jetlinks.community.media.session.SsrcConfig;
+import org.jetlinks.community.media.zlm.ZLMServerConfig;
+import org.springframework.util.StringUtils;
+
+import java.util.HashMap;
+@Data
+public class MediaServerItem{
+
+    private String id;
+
+    private String ip;
+
+    private String hookIp;
+
+    private String sdpIp;
+
+    private String streamIp;
+
+    private int httpPort;
+
+    private int httpSSlPort;
+
+    private int rtmpPort;
+
+    private int rtmpSSlPort;
+
+    private int rtpProxyPort;
+
+    private int rtspPort;
+
+    private int rtspSSLPort;
+
+    private boolean autoConfig;
+
+    private String secret;
+
+    private int streamNoneReaderDelayMS;
+
+    private int hookAliveInterval;
+
+    private boolean rtpEnable;
+
+    private boolean status;
+
+    private String rtpPortRange;
+
+    private String sendRtpPortRange;
+
+    private int recordAssistPort;
+
+    private String createTime;
+
+    private String updateTime;
+
+    private String lastKeepaliveTime;
+
+    private boolean defaultServer;
+
+    private SsrcConfig ssrcConfig;
+
+    private int currentPort;
+
+
+    /**
+     * 每一台ZLM都有一套独立的SSRC列表
+     * 在ApplicationCheckRunner里对mediaServerSsrcMap进行初始化
+     */
+//    private HashMap<String, SsrcConfig> mediaServerSsrcMap;
+
+    public MediaServerItem() {
+    }
+
+    public MediaServerItem(ZLMServerConfig zlmServerConfig, String sipIp) {
+        id = zlmServerConfig.getGeneralMediaServerId();
+        ip = zlmServerConfig.getIp();
+        hookIp = StringUtils.isEmpty(zlmServerConfig.getHookIp())? sipIp: zlmServerConfig.getHookIp();
+        sdpIp = StringUtils.isEmpty(zlmServerConfig.getSdpIp())? zlmServerConfig.getIp(): zlmServerConfig.getSdpIp();
+        streamIp = StringUtils.isEmpty(zlmServerConfig.getStreamIp())? zlmServerConfig.getIp(): zlmServerConfig.getStreamIp();
+        httpPort = zlmServerConfig.getHttpPort();
+        httpSSlPort = zlmServerConfig.getHttpSSLport();
+        rtmpPort = zlmServerConfig.getRtmpPort();
+        rtmpSSlPort = zlmServerConfig.getRtmpSslPort();
+        rtpProxyPort = zlmServerConfig.getRtpProxyPort();
+        rtspPort = zlmServerConfig.getRtspPort();
+        rtspSSLPort = zlmServerConfig.getRtspSSlport();
+        autoConfig = true; // 默认值true;
+        secret = zlmServerConfig.getApiSecret();
+        streamNoneReaderDelayMS = zlmServerConfig.getGeneralStreamNoneReaderDelayMS();
+        hookAliveInterval = zlmServerConfig.getHookAliveInterval();
+        rtpEnable = false; // 默认使用单端口;直到用户自己设置开启多端口
+        rtpPortRange = "30000,30500"; // 默认使用30000,30500作为级联时发送流的端口号
+        sendRtpPortRange = "30000,30500"; // 默认使用30000,30500作为级联时发送流的端口号
+        recordAssistPort = 0; // 默认关闭
+
+    }
+}

+ 23 - 0
jetlinks-manager/media-manager/src/main/java/org/jetlinks/community/media/zlm/dto/OriginType.java

@@ -0,0 +1,23 @@
+package org.jetlinks.community.media.zlm.dto;
+
+public enum OriginType {
+    // 不可调整顺序
+    UNKNOWN("UNKNOWN"),
+    RTMP_PUSH("PUSH"),
+    RTSP_PUSH("PUSH"),
+    RTP_PUSH("RTP"),
+    PULL("PULL"),
+    FFMPEG_PULL("PULL"),
+    MP4_VOD("MP4_VOD"),
+    DEVICE_CHN("DEVICE_CHN"),
+    RTC_PUSH("PUSH");
+
+    private final String type;
+    OriginType(String type) {
+        this.type = type;
+    }
+
+    public String getType() {
+        return type;
+    }
+}

+ 24 - 0
jetlinks-manager/media-manager/src/main/java/org/jetlinks/community/media/zlm/dto/StreamProxyItem.java

@@ -0,0 +1,24 @@
+package org.jetlinks.community.media.zlm.dto;
+
+import lombok.Data;
+import org.jetlinks.community.media.entity.GbStream;
+@Data
+public class StreamProxyItem extends GbStream {
+
+    private String type;
+    private String app;
+    private String stream;
+    private String mediaServerId;
+    private String url;
+    private String src_url;
+    private String dst_url;
+    private int timeout_ms;
+    private String ffmpeg_cmd_key;
+    private String rtp_type;
+    private boolean enable;
+    private boolean enable_hls;
+    private boolean enable_mp4;
+    private boolean enable_remove_none_reader; // 无人观看时删除
+    private String platformGbId;
+    private String createTime;
+}

+ 120 - 0
jetlinks-manager/media-manager/src/main/java/org/jetlinks/community/media/zlm/dto/StreamPushItem.java

@@ -0,0 +1,120 @@
+package org.jetlinks.community.media.zlm.dto;
+
+import lombok.Data;
+import org.jetlinks.community.media.entity.GbStream;
+
+import javax.validation.constraints.NotNull;
+import java.util.List;
+
+@Data
+public class StreamPushItem extends GbStream implements Comparable<StreamPushItem>{
+
+    /**
+     * 应用名
+     */
+    private String app;
+
+    /**
+     * 流id
+     */
+    private String stream;
+
+    /**
+     * 观看总人数,包括hls/rtsp/rtmp/http-flv/ws-flv
+     */
+    private String totalReaderCount;
+
+    /**
+     * 协议 包括hls/rtsp/rtmp/http-flv/ws-flv
+     */
+    private List<MediaSchema> schemas;
+
+    /**
+     * 产生源类型,
+     * unknown = 0,
+     * rtmp_push=1,
+     * rtsp_push=2,
+     * rtp_push=3,
+     * pull=4,
+     * ffmpeg_pull=5,
+     * mp4_vod=6,
+     * device_chn=7
+     */
+    private int originType;
+
+    /**
+     * 客户端和服务器网络信息,可能为null类型
+     */
+    private MediaItem.OriginSock originSock;
+
+    /**
+     * 产生源类型的字符串描述
+     */
+    private String originTypeStr;
+
+    /**
+     * 产生源的url
+     */
+    private String originUrl;
+
+    /**
+     * GMT unix系统时间戳,单位秒
+     */
+    private Long createStamp;
+
+    /**
+     * 存活时间,单位秒
+     */
+    private Long aliveSecond;
+
+    /**
+     * 音视频轨道
+     */
+    private List<MediaItem.MediaTrack> tracks;
+
+    /**
+     * 音视频轨道
+     */
+    private String vhost;
+
+    /**
+     * 使用的流媒体ID
+     */
+    private String mediaServerId;
+
+    public String getVhost() {
+        return vhost;
+    }
+
+    public void setVhost(String vhost) {
+        this.vhost = vhost;
+    }
+
+
+    @Override
+    public int compareTo(@NotNull StreamPushItem streamPushItem) {
+        return Long.valueOf(this.createStamp - streamPushItem.getCreateStamp().intValue()).intValue();
+    }
+
+    public static class MediaSchema {
+        private String schema;
+        private Long bytesSpeed;
+
+        public String getSchema() {
+            return schema;
+        }
+
+        public void setSchema(String schema) {
+            this.schema = schema;
+        }
+
+        public Long getBytesSpeed() {
+            return bytesSpeed;
+        }
+
+        public void setBytesSpeed(Long bytesSpeed) {
+            this.bytesSpeed = bytesSpeed;
+        }
+    }
+}
+

+ 20 - 0
jetlinks-manager/media-manager/src/main/java/org/jetlinks/community/media/zlm/dto/ZLMRunInfo.java

@@ -0,0 +1,20 @@
+package org.jetlinks.community.media.zlm.dto;
+
+import lombok.Data;
+
+/**
+ * 记录zlm运行中一些参数
+ */
+@Data
+public class ZLMRunInfo {
+
+    /**
+     * zlm当前流数量
+     */
+    private int mediaCount;
+
+    /**
+     * 在线状态
+     */
+    private boolean online;
+}

+ 24 - 0
jetlinks-manager/media-manager/src/main/java/org/jetlinks/community/media/zlm/event/ZLMEventAbstract.java

@@ -0,0 +1,24 @@
+package org.jetlinks.community.media.zlm.event;
+
+import org.springframework.context.ApplicationEvent;
+
+public abstract class ZLMEventAbstract extends ApplicationEvent {
+
+
+    private static final long serialVersionUID = 1L;
+
+    private String mediaServerId;
+
+
+    public ZLMEventAbstract(Object source) {
+        super(source);
+    }
+
+    public String getMediaServerId() {
+        return mediaServerId;
+    }
+
+    public void setMediaServerId(String mediaServerId) {
+        this.mediaServerId = mediaServerId;
+    }
+}

+ 73 - 0
jetlinks-manager/media-manager/src/main/java/org/jetlinks/community/media/zlm/event/ZLMKeepliveTimeoutListener.java

@@ -0,0 +1,73 @@
+//package org.jetlinks.community.media.zlm.event;
+//
+//import com.alibaba.fastjson.JSONObject;
+//import com.genersoft.iot.vmp.common.VideoManagerConstants;
+//import com.genersoft.iot.vmp.conf.RedisKeyExpirationEventMessageListener;
+//import com.genersoft.iot.vmp.conf.UserSetup;
+//import com.genersoft.iot.vmp.gb28181.event.EventPublisher;
+//import com.genersoft.iot.vmp.media.zlm.ZLMRESTfulUtils;
+//import com.genersoft.iot.vmp.media.zlm.dto.MediaServerItem;
+//import com.genersoft.iot.vmp.service.IMediaServerService;
+//import org.slf4j.Logger;
+//import org.slf4j.LoggerFactory;
+//import org.springframework.beans.factory.annotation.Autowired;
+//import org.springframework.data.redis.connection.Message;
+//import org.springframework.data.redis.listener.RedisMessageListenerContainer;
+//import org.springframework.stereotype.Component;
+//
+///**
+// * @description:设备心跳超时监听,借助redis过期特性,进行监听,监听到说明设备心跳超时,发送离线事件
+// * @author: swwheihei
+// * @date:   2020年5月6日 上午11:35:46
+// */
+//@Component
+//public class ZLMKeepliveTimeoutListener extends RedisKeyExpirationEventMessageListener {
+//
+//    private Logger logger = LoggerFactory.getLogger(ZLMKeepliveTimeoutListener.class);
+//
+//	@Autowired
+//	private EventPublisher publisher;
+//
+//	@Autowired
+//	private ZLMRESTfulUtils zlmresTfulUtils;
+//
+//	@Autowired
+//	private UserSetup userSetup;
+//
+//	@Autowired
+//	private IMediaServerService mediaServerService;
+//
+//    public ZLMKeepliveTimeoutListener(RedisMessageListenerContainer listenerContainer, UserSetup userSetup) {
+//        super(listenerContainer, userSetup);
+//    }
+//
+//
+//    /**
+//     * 监听失效的key,key格式为keeplive_deviceId
+//     * @param message
+//     * @param pattern
+//     */
+//    @Override
+//    public void onMessage(Message message, byte[] pattern) {
+//        //  获取失效的key
+//        String expiredKey = message.toString();
+//        String KEEPLIVEKEY_PREFIX = VideoManagerConstants.MEDIA_SERVER_KEEPALIVE_PREFIX + userSetup.getServerId() + "_";
+//        if(!expiredKey.startsWith(KEEPLIVEKEY_PREFIX)){
+//        	return;
+//        }
+//
+//        String mediaServerId = expiredKey.substring(KEEPLIVEKEY_PREFIX.length(),expiredKey.length());
+//        logger.info("[zlm心跳到期]:" + mediaServerId);
+//        // 发起http请求验证zlm是否确实无法连接,如果确实无法连接则发送离线事件,否则不作处理
+//        MediaServerItem mediaServerItem = mediaServerService.getOne(mediaServerId);
+//        JSONObject mediaServerConfig = zlmresTfulUtils.getMediaServerConfig(mediaServerItem);
+//        if (mediaServerConfig == null) {
+//            publisher.zlmOfflineEventPublish(mediaServerId);
+//        }else {
+//            logger.info("[zlm心跳到期]:{}验证后zlm仍在线,回复心跳信息", mediaServerId);
+//            // 添加zlm信息
+//            mediaServerService.updateMediaServerKeepalive(mediaServerId, mediaServerConfig);
+//        }
+//
+//    }
+//}

+ 11 - 0
jetlinks-manager/media-manager/src/main/java/org/jetlinks/community/media/zlm/event/ZLMOfflineEvent.java

@@ -0,0 +1,11 @@
+package org.jetlinks.community.media.zlm.event;
+
+/**
+ * zlm离线事件类
+ */
+public class ZLMOfflineEvent extends ZLMEventAbstract {
+
+	public ZLMOfflineEvent(Object source) {
+		super(source);
+	}
+}

+ 11 - 0
jetlinks-manager/media-manager/src/main/java/org/jetlinks/community/media/zlm/event/ZLMOnlineEvent.java

@@ -0,0 +1,11 @@
+package org.jetlinks.community.media.zlm.event;
+
+/**
+ * zlm在线事件
+ */
+public class ZLMOnlineEvent extends ZLMEventAbstract {
+
+	public ZLMOnlineEvent(Object source) {
+		super(source);
+	}
+}

برخی فایل ها در این مقایسه diff نمایش داده نمی شوند زیرا تعداد فایل ها بسیار زیاد است