Преглед изворни кода

feat(video-service): 集成国标视频服务模块替换原有视频设备管理

- 新增 video-service 独立模块,集成 GB28181 国标视频协议支持
- 添加云台控制、设备信息、实时直播等 API 兼容控制器
- 删除原有 StationVideoDevice 相关的 domain、mapper、service 及实现类
- RealtimeMonitorController 中替换为新的 VideoDeviceService 接口
- 添加 video-service 的 application.yml 配置,包含数据源、Redis、SIP、媒体服务器等参数
- 新增 AssistRESTfulUtils 用于 ZLM 媒体服务器 RESTful API 调用
- application.yml 中通过 config.import 导入 video-business.properties 配置文件
- 修复 EquipmentBase 中 @ApiModelProperty 注解的缩进格式
林仔 пре 1 недеља
родитељ
комит
a39424bcd0
100 измењених фајлова са 10321 додато и 112 уклоњено
  1. 17 0
      collection-integration-tests/pom.xml
  2. 4 0
      collection-integration-tests/src/test/resources/logback-test.xml
  3. 4 5
      pipe-network-service/zksy-admin/src/main/java/com/zksy/web/controller/monitor/RealtimeMonitorController.java
  4. 53 0
      pipe-network-service/zksy-admin/src/main/java/com/zksy/web/controller/video/VideoDeviceController.java
  5. 3 0
      pipe-network-service/zksy-admin/src/main/resources/application.yml
  6. 1 1
      pipe-network-service/zksy-system/src/main/java/com/zksy/base/domain/EquipmentBase.java
  7. 0 58
      pipe-network-service/zksy-system/src/main/java/com/zksy/base/domain/StationVideoDevice.java
  8. 39 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/base/mapper/EquipmentVideoBindingMapper.java
  9. 0 9
      pipe-network-service/zksy-system/src/main/java/com/zksy/base/mapper/StationVideoDeviceMapper.java
  10. 12 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/base/mapper/VideoSessionMapper.java
  11. 0 11
      pipe-network-service/zksy-system/src/main/java/com/zksy/base/service/StationVideoDeviceService.java
  12. 0 28
      pipe-network-service/zksy-system/src/main/java/com/zksy/base/service/impl/StationVideoDeviceServiceImpl.java
  13. 21 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/base/video/EquipmentVideoBinding.java
  14. 43 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/base/video/VideoCollectorClient.java
  15. 23 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/base/video/VideoDeviceRecord.java
  16. 282 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/base/video/VideoDeviceService.java
  17. 42 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/base/video/VideoRequests.java
  18. 22 0
      pipe-network-service/zksy-system/src/main/java/com/zksy/base/video/VideoSession.java
  19. 1 0
      pom.xml
  20. 25 0
      video-service/pom.xml
  21. 46 0
      video-service/src/main/java/com/ruoyi/wvp/common/CivilCodePo.java
  22. 5 0
      video-service/src/main/java/com/ruoyi/wvp/common/CommonCallback.java
  23. 5 0
      video-service/src/main/java/com/ruoyi/wvp/common/GeneralCallback.java
  24. 59 0
      video-service/src/main/java/com/ruoyi/wvp/common/InviteInfo.java
  25. 11 0
      video-service/src/main/java/com/ruoyi/wvp/common/InviteSessionStatus.java
  26. 9 0
      video-service/src/main/java/com/ruoyi/wvp/common/InviteSessionType.java
  27. 780 0
      video-service/src/main/java/com/ruoyi/wvp/common/StreamInfo.java
  28. 94 0
      video-service/src/main/java/com/ruoyi/wvp/common/StreamURL.java
  29. 54 0
      video-service/src/main/java/com/ruoyi/wvp/common/SystemAllInfo.java
  30. 149 0
      video-service/src/main/java/com/ruoyi/wvp/common/VersionPo.java
  31. 159 0
      video-service/src/main/java/com/ruoyi/wvp/common/VideoManagerConstants.java
  32. 21 0
      video-service/src/main/java/com/ruoyi/wvp/common/enums/ChannelDataType.java
  33. 77 0
      video-service/src/main/java/com/ruoyi/wvp/common/enums/DeviceControlType.java
  34. 159 0
      video-service/src/main/java/com/ruoyi/wvp/conf/DynamicTask.java
  35. 291 0
      video-service/src/main/java/com/ruoyi/wvp/conf/MediaConfig.java
  36. 13 0
      video-service/src/main/java/com/ruoyi/wvp/conf/MediaStatusTimerTask.java
  37. 37 0
      video-service/src/main/java/com/ruoyi/wvp/conf/ScheduleConfig.java
  38. 28 0
      video-service/src/main/java/com/ruoyi/wvp/conf/ServiceInfo.java
  39. 34 0
      video-service/src/main/java/com/ruoyi/wvp/conf/SipConfig.java
  40. 41 0
      video-service/src/main/java/com/ruoyi/wvp/conf/SystemInfoTimerTask.java
  41. 67 0
      video-service/src/main/java/com/ruoyi/wvp/conf/ThreadPoolTaskConfig.java
  42. 183 0
      video-service/src/main/java/com/ruoyi/wvp/conf/UserSetting.java
  43. 39 0
      video-service/src/main/java/com/ruoyi/wvp/conf/VersionConfig.java
  44. 27 0
      video-service/src/main/java/com/ruoyi/wvp/conf/VersionInfo.java
  45. 29 0
      video-service/src/main/java/com/ruoyi/wvp/conf/WVPTimerTask.java
  46. 24 0
      video-service/src/main/java/com/ruoyi/wvp/conf/redis/bean/RedisRpcMessage.java
  47. 93 0
      video-service/src/main/java/com/ruoyi/wvp/conf/redis/bean/RedisRpcRequest.java
  48. 99 0
      video-service/src/main/java/com/ruoyi/wvp/conf/redis/bean/RedisRpcResponse.java
  49. 244 0
      video-service/src/main/java/com/ruoyi/wvp/controller/GBRecordController.java
  50. 225 0
      video-service/src/main/java/com/ruoyi/wvp/controller/PlayController.java
  51. 259 0
      video-service/src/main/java/com/ruoyi/wvp/controller/PlaybackController.java
  52. 566 0
      video-service/src/main/java/com/ruoyi/wvp/controller/PtzController.java
  53. 153 0
      video-service/src/main/java/com/ruoyi/wvp/gb28181/ApiControlController.java
  54. 253 0
      video-service/src/main/java/com/ruoyi/wvp/gb28181/ApiDeviceController.java
  55. 276 0
      video-service/src/main/java/com/ruoyi/wvp/gb28181/ApiStreamController.java
  56. 186 0
      video-service/src/main/java/com/ruoyi/wvp/gb28181/SipLayer.java
  57. 235 0
      video-service/src/main/java/com/ruoyi/wvp/gb28181/auth/DigestServerAuthenticationHelper.java
  58. 24 0
      video-service/src/main/java/com/ruoyi/wvp/gb28181/bean/BaiduPoint.java
  59. 37 0
      video-service/src/main/java/com/ruoyi/wvp/gb28181/bean/CatalogChannelEvent.java
  60. 30 0
      video-service/src/main/java/com/ruoyi/wvp/gb28181/bean/CatalogData.java
  61. 8 0
      video-service/src/main/java/com/ruoyi/wvp/gb28181/bean/CmdType.java
  62. 411 0
      video-service/src/main/java/com/ruoyi/wvp/gb28181/bean/CommonGBChannel.java
  63. 235 0
      video-service/src/main/java/com/ruoyi/wvp/gb28181/bean/Device.java
  64. 322 0
      video-service/src/main/java/com/ruoyi/wvp/gb28181/bean/DeviceChannel.java
  65. 27 0
      video-service/src/main/java/com/ruoyi/wvp/gb28181/bean/DeviceNotFoundEvent.java
  66. 430 0
      video-service/src/main/java/com/ruoyi/wvp/gb28181/bean/GBStringMsgParser.java
  67. 46 0
      video-service/src/main/java/com/ruoyi/wvp/gb28181/bean/Gb28181Sdp.java
  68. 56 0
      video-service/src/main/java/com/ruoyi/wvp/gb28181/bean/GbCode.java
  69. 149 0
      video-service/src/main/java/com/ruoyi/wvp/gb28181/bean/GbSipDate.java
  70. 21 0
      video-service/src/main/java/com/ruoyi/wvp/gb28181/bean/GbStringMsgParserFactory.java
  71. 44 0
      video-service/src/main/java/com/ruoyi/wvp/gb28181/bean/HandlerCatchData.java
  72. 35 0
      video-service/src/main/java/com/ruoyi/wvp/gb28181/bean/Host.java
  73. 22 0
      video-service/src/main/java/com/ruoyi/wvp/gb28181/bean/InviteInfo.java
  74. 66 0
      video-service/src/main/java/com/ruoyi/wvp/gb28181/bean/MobilePosition.java
  75. 38 0
      video-service/src/main/java/com/ruoyi/wvp/gb28181/bean/NotifyCatalogChannel.java
  76. 12 0
      video-service/src/main/java/com/ruoyi/wvp/gb28181/bean/OpenRTPServerResult.java
  77. 14 0
      video-service/src/main/java/com/ruoyi/wvp/gb28181/bean/PlayException.java
  78. 12 0
      video-service/src/main/java/com/ruoyi/wvp/gb28181/bean/Preset.java
  79. 112 0
      video-service/src/main/java/com/ruoyi/wvp/gb28181/bean/RecordInfo.java
  80. 162 0
      video-service/src/main/java/com/ruoyi/wvp/gb28181/bean/RecordItem.java
  81. 27 0
      video-service/src/main/java/com/ruoyi/wvp/gb28181/bean/RemoteAddressInfo.java
  82. 73 0
      video-service/src/main/java/com/ruoyi/wvp/gb28181/bean/SipTransactionInfo.java
  83. 78 0
      video-service/src/main/java/com/ruoyi/wvp/gb28181/bean/SsrcTransaction.java
  84. 41 0
      video-service/src/main/java/com/ruoyi/wvp/gb28181/bean/SyncStatus.java
  85. 62 0
      video-service/src/main/java/com/ruoyi/wvp/gb28181/conf/DefaultProperties.java
  86. 91 0
      video-service/src/main/java/com/ruoyi/wvp/gb28181/conf/ServerLoggerImpl.java
  87. 141 0
      video-service/src/main/java/com/ruoyi/wvp/gb28181/conf/StackLoggerImpl.java
  88. 176 0
      video-service/src/main/java/com/ruoyi/wvp/gb28181/dao/provider/DeviceChannelProvider.java
  89. 235 0
      video-service/src/main/java/com/ruoyi/wvp/gb28181/dto/DeviceChannelExtend.java
  90. 42 0
      video-service/src/main/java/com/ruoyi/wvp/gb28181/event/EventPublisher.java
  91. 187 0
      video-service/src/main/java/com/ruoyi/wvp/gb28181/event/SipSubscribe.java
  92. 31 0
      video-service/src/main/java/com/ruoyi/wvp/gb28181/event/record/RecordEndEvent.java
  93. 73 0
      video-service/src/main/java/com/ruoyi/wvp/gb28181/event/record/RecordEndEventListener.java
  94. 48 0
      video-service/src/main/java/com/ruoyi/wvp/gb28181/event/sip/SipEvent.java
  95. 125 0
      video-service/src/main/java/com/ruoyi/wvp/gb28181/service/IDeviceChannelService.java
  96. 208 0
      video-service/src/main/java/com/ruoyi/wvp/gb28181/service/IDeviceService.java
  97. 85 0
      video-service/src/main/java/com/ruoyi/wvp/gb28181/service/IInviteStreamService.java
  98. 21 0
      video-service/src/main/java/com/ruoyi/wvp/gb28181/service/IPTZService.java
  99. 53 0
      video-service/src/main/java/com/ruoyi/wvp/gb28181/service/IPlayService.java
  100. 589 0
      video-service/src/main/java/com/ruoyi/wvp/gb28181/service/impl/DeviceChannelServiceImpl.java

+ 17 - 0
collection-integration-tests/pom.xml

@@ -0,0 +1,17 @@
+<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+    <parent><groupId>com.zksy</groupId><artifactId>pipe-ner-server</artifactId><version>1.0.0</version></parent>
+    <artifactId>collection-integration-tests</artifactId>
+    <dependencies>
+        <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency>
+        <dependency><groupId>com.zksy</groupId><artifactId>environment-service</artifactId><version>1.0.0</version><scope>test</scope></dependency>
+        <dependency><groupId>com.zksy</groupId><artifactId>manhole-service</artifactId><version>1.0.0</version><scope>test</scope></dependency>
+        <dependency><groupId>com.zksy</groupId><artifactId>firefighting-pressure-service</artifactId><version>1.0-SNAPSHOT</version><scope>test</scope></dependency>
+        <dependency><groupId>com.zksy</groupId><artifactId>flammable-gas-service</artifactId><version>1.0-SNAPSHOT</version><scope>test</scope></dependency>
+        <dependency><groupId>com.zksy</groupId><artifactId>telemetry-service</artifactId><version>1.0-SNAPSHOT</version><scope>test</scope></dependency>
+        <dependency><groupId>com.zksy</groupId><artifactId>radar-service</artifactId><version>1.0-SNAPSHOT</version><scope>test</scope></dependency>
+        <dependency><groupId>com.zksy</groupId><artifactId>water-level-service</artifactId><version>1.0-SNAPSHOT</version><scope>test</scope></dependency>
+        <dependency><groupId>com.zksy</groupId><artifactId>audio-service</artifactId><version>1.0-SNAPSHOT</version><scope>test</scope></dependency>
+    </dependencies>
+</project>

+ 4 - 0
collection-integration-tests/src/test/resources/logback-test.xml

@@ -0,0 +1,4 @@
+<configuration>
+    <!-- 故障由断言校验,避免70台并发注入异常打印数万行堆栈。 -->
+    <root level="OFF"/>
+</configuration>

+ 4 - 5
pipe-network-service/zksy-admin/src/main/java/com/zksy/web/controller/monitor/RealtimeMonitorController.java

@@ -1,8 +1,7 @@
 package com.zksy.web.controller.monitor;
 
-import com.zksy.base.domain.StationVideoDevice;
 import com.zksy.base.service.EquipmentBaseService;
-import com.zksy.base.service.StationVideoDeviceService;
+import com.zksy.base.video.VideoDeviceService;
 import com.zksy.common.annotation.Anonymous;
 import com.zksy.common.core.domain.AjaxResult;
 import io.swagger.annotations.Api;
@@ -21,10 +20,10 @@ import java.util.*;
 public class RealtimeMonitorController {
 
     @Autowired
-    private StationVideoDeviceService videoDeviceService;
+    private EquipmentBaseService equipmentService;
 
     @Autowired
-    private EquipmentBaseService equipmentService;
+    private VideoDeviceService videoDeviceService;
 
     @GetMapping("/pressure-history")
     @ApiOperation("压力历史曲线")
@@ -71,7 +70,7 @@ public class RealtimeMonitorController {
     @ApiOperation("获取视频设备列表")
     @Anonymous
     public AjaxResult getVideoList() {
-        return AjaxResult.success(videoDeviceService.list());
+        return AjaxResult.success(videoDeviceService.all());
     }
 
     @GetMapping("/points-with-latest-data")

+ 53 - 0
pipe-network-service/zksy-admin/src/main/java/com/zksy/web/controller/video/VideoDeviceController.java

@@ -0,0 +1,53 @@
+package com.zksy.web.controller.video;
+
+import java.util.*;
+import javax.validation.Valid;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.zksy.base.video.VideoDeviceRecord;
+import com.zksy.base.video.*;
+import com.zksy.common.core.domain.AjaxResult;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.*;
+
+@RestController
+@RequestMapping("/api/video")
+public class VideoDeviceController {
+    private final VideoDeviceService service;
+    public VideoDeviceController(VideoDeviceService service) {this.service=service;}
+    @PreAuthorize("@ss.hasPermi('video:device:list')")
+    @GetMapping("/devices")
+    public AjaxResult list(@RequestParam(defaultValue="1") int pageNum,@RequestParam(defaultValue="10") int pageSize,
+                           @RequestParam(required=false) String query,@RequestParam(required=false) String sourceType,@RequestParam(required=false) String stationId) {
+        Page<VideoDeviceRecord> page=service.list(pageNum,pageSize,query,sourceType,stationId);
+        AjaxResult result=AjaxResult.success();result.put("rows",page.getRecords());result.put("total",page.getTotal());return result;
+    }
+    @PreAuthorize("@ss.hasPermi('video:device:list')") @GetMapping("/devices/{id}")
+    public AjaxResult detail(@PathVariable String id) {return AjaxResult.success(service.one(id));}
+    @PreAuthorize("@ss.hasPermi('video:device:add')") @PostMapping("/devices")
+    public AjaxResult create(@Valid @RequestBody VideoRequests.Save input) { input.setVideoId(null); return AjaxResult.success((Object)service.save(input)); }
+    @PreAuthorize("@ss.hasPermi('video:device:edit')") @PutMapping("/devices/{id}")
+    public AjaxResult update(@PathVariable String id,@Valid @RequestBody VideoRequests.Save input) { input.setVideoId(id); return AjaxResult.success((Object)service.save(input)); }
+    @PreAuthorize("@ss.hasPermi('video:device:remove')") @DeleteMapping("/devices/{id}")
+    public AjaxResult remove(@PathVariable String id) {service.remove(id);return AjaxResult.success();}
+    @PreAuthorize("@ss.hasPermi('video:device:add')") @GetMapping("/discovered")
+    public AjaxResult discover(@RequestParam(required=false) String query,@RequestParam(required=false) Boolean online,
+                               @RequestParam(defaultValue="1") int pageNum,@RequestParam(defaultValue="10") int pageSize) {return AjaxResult.success(service.discover(query,online,pageNum,pageSize));}
+    @PreAuthorize("@ss.hasPermi('video:device:add')") @PostMapping("/discovered/{id}/sync")
+    public AjaxResult sync(@PathVariable String id) {service.sync(id);return AjaxResult.success("目录同步已发起");}
+    @PreAuthorize("@ss.hasPermi('video:device:add')") @PostMapping("/discovered/sync")
+    public AjaxResult syncAll() {return AjaxResult.success(service.syncAll());}
+    @PreAuthorize("@ss.hasPermi('video:device:play')") @PostMapping("/devices/{id}/live")
+    public AjaxResult live(@PathVariable String id) {return AjaxResult.success(service.start(id,null));}
+    @PreAuthorize("@ss.hasPermi('video:device:record')") @GetMapping("/devices/{id}/records")
+    public AjaxResult records(@PathVariable String id,@Valid VideoRequests.Range range) {return AjaxResult.success(service.records(id,range));}
+    @PreAuthorize("@ss.hasPermi('video:device:record')") @PostMapping("/devices/{id}/playback")
+    public AjaxResult playback(@PathVariable String id,@Valid @RequestBody VideoRequests.Range range) {return AjaxResult.success(service.start(id,range));}
+    @PreAuthorize("@ss.hasPermi('video:device:ptz')") @PostMapping("/devices/{id}/ptz")
+    public AjaxResult ptz(@PathVariable String id,@Valid @RequestBody VideoRequests.Ptz input) {service.ptz(id,input);return AjaxResult.success("云台命令已发送");}
+    @PreAuthorize("@ss.hasPermi('video:device:record')") @PostMapping("/sessions/{id}/control")
+    public AjaxResult control(@PathVariable String id,@Valid @RequestBody VideoRequests.Control input) {service.control(id,input);return AjaxResult.success();}
+    @PreAuthorize("@ss.hasAnyPermi('video:device:play,video:device:record')") @PostMapping("/sessions/{id}/heartbeat")
+    public AjaxResult heartbeat(@PathVariable String id) {service.heartbeat(id);return AjaxResult.success();}
+    @PreAuthorize("@ss.hasAnyPermi('video:device:play,video:device:record')") @PostMapping("/sessions/{id}/stop")
+    public AjaxResult stop(@PathVariable String id) {service.stop(id);return AjaxResult.success();}
+}

+ 3 - 0
pipe-network-service/zksy-admin/src/main/resources/application.yml

@@ -0,0 +1,3 @@
+spring:
+  config:
+    import: "optional:file:./.local/video-business.properties,optional:file:../.local/video-business.properties,optional:file:../../.local/video-business.properties"

+ 1 - 1
pipe-network-service/zksy-system/src/main/java/com/zksy/base/domain/EquipmentBase.java

@@ -90,7 +90,7 @@ public class EquipmentBase {
     @ApiModelProperty("设备图片URL")
     private String equipmentImage;
 
-    @ApiModelProperty("创建时间")
+@ApiModelProperty("创建时间")
     @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
     private LocalDateTime createTime;
 

+ 0 - 58
pipe-network-service/zksy-system/src/main/java/com/zksy/base/domain/StationVideoDevice.java

@@ -1,58 +0,0 @@
-package com.zksy.base.domain;
-
-import com.baomidou.mybatisplus.annotation.IdType;
-import com.baomidou.mybatisplus.annotation.TableId;
-import com.baomidou.mybatisplus.annotation.TableName;
-import com.fasterxml.jackson.annotation.JsonFormat;
-import io.swagger.annotations.ApiModelProperty;
-import lombok.AllArgsConstructor;
-import lombok.Data;
-import lombok.NoArgsConstructor;
-
-import java.time.LocalDateTime;
-
-/**
- * 场站视频监控设备表
- */
-@Data
-@AllArgsConstructor
-@NoArgsConstructor
-@TableName(value = "station_video_device", schema = "app_user")
-public class StationVideoDevice {
-
-    @TableId(value = "video_id", type = IdType.ASSIGN_UUID)
-    @ApiModelProperty("视频设备ID")
-    private String videoId;
-
-    @ApiModelProperty("关联场站ID")
-    private String stationId;
-
-    @ApiModelProperty("摄像头名称")
-    private String deviceName;
-
-    @ApiModelProperty("设备编码")
-    private String deviceCode;
-
-    @ApiModelProperty("视频流地址")
-    private String videoUrl;
-
-    @ApiModelProperty("安装位置描述")
-    private String location;
-
-    @ApiModelProperty("状态")
-    private String status;
-
-    @ApiModelProperty("厂商")
-    private String manufacturer;
-
-    @ApiModelProperty("备注")
-    private String remark;
-
-    @ApiModelProperty("创建时间")
-    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
-    private LocalDateTime createTime;
-
-    @ApiModelProperty("更新时间")
-    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
-    private LocalDateTime updateTime;
-}

+ 39 - 0
pipe-network-service/zksy-system/src/main/java/com/zksy/base/mapper/EquipmentVideoBindingMapper.java

@@ -0,0 +1,39 @@
+package com.zksy.base.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.zksy.base.video.EquipmentVideoBinding;
+import com.zksy.base.video.VideoDeviceRecord;
+import org.apache.ibatis.annotations.*;
+import java.util.List;
+
+@Mapper
+public interface EquipmentVideoBindingMapper extends BaseMapper<EquipmentVideoBinding> {
+    String VIEW = "SELECT e.*, e.equipment_id AS video_id, e.equipment_name AS device_name, " +
+        "e.equipment_code AS device_code, e.equipment_location AS location, t.type_name AS equipment_type_name, " +
+        "s.current_status, v.source_type, v.video_url, v.gb_device_id, v.gb_channel_id, v.station_id, v.status " +
+        "FROM app_user.equipment_base e JOIN app_user.equipment_video_binding v ON v.equipment_id=e.equipment_id " +
+        "LEFT JOIN app_user.equipment_type t ON t.id=e.equipment_type_id " +
+        "LEFT JOIN app_user.equipment_status s ON s.equipment_id=e.equipment_id AND s.del_flag='0' ";
+
+    @Select("<script>" + VIEW + "WHERE e.equipment_type_id=20 AND e.del_flag='0' " +
+        "<if test='query != null and query != &quot;&quot;'>AND (e.equipment_name LIKE CONCAT('%',#{query},'%') " +
+        "OR e.equipment_code LIKE CONCAT('%',#{query},'%') OR v.gb_device_id LIKE CONCAT('%',#{query},'%') " +
+        "OR v.gb_channel_id LIKE CONCAT('%',#{query},'%'))</if> " +
+        "<if test='sourceType != null and sourceType != &quot;&quot;'>AND v.source_type=#{sourceType}</if> " +
+        "<if test='stationId != null and stationId != &quot;&quot;'>AND v.station_id=#{stationId}</if> " +
+        "ORDER BY e.create_time DESC,e.equipment_id</script>")
+    Page<VideoDeviceRecord> list(Page<VideoDeviceRecord> page, @Param("query") String query,
+        @Param("sourceType") String sourceType, @Param("stationId") String stationId);
+
+    @Select(VIEW + "WHERE e.equipment_id=#{id} AND e.equipment_type_id=20 AND e.del_flag='0'")
+    VideoDeviceRecord find(@Param("id") String id);
+
+    @Select(VIEW + "WHERE e.equipment_type_id=20 AND e.del_flag='0' ORDER BY e.create_time DESC,e.equipment_id")
+    List<VideoDeviceRecord> all();
+
+    @Update("UPDATE app_user.equipment_video_binding SET source_type=#{sourceType},video_url=#{videoUrl}," +
+        "gb_device_id=#{gbDeviceId},gb_channel_id=#{gbChannelId},station_id=#{stationId}," +
+        "status=#{status},update_time=#{updateTime} WHERE equipment_id=#{equipmentId}")
+    int updateBinding(EquipmentVideoBinding binding);
+}

+ 0 - 9
pipe-network-service/zksy-system/src/main/java/com/zksy/base/mapper/StationVideoDeviceMapper.java

@@ -1,9 +0,0 @@
-package com.zksy.base.mapper;
-
-import com.baomidou.mybatisplus.core.mapper.BaseMapper;
-import com.zksy.base.domain.StationVideoDevice;
-import org.apache.ibatis.annotations.Mapper;
-
-@Mapper
-public interface StationVideoDeviceMapper extends BaseMapper<StationVideoDevice> {
-}

+ 12 - 0
pipe-network-service/zksy-system/src/main/java/com/zksy/base/mapper/VideoSessionMapper.java

@@ -0,0 +1,12 @@
+package com.zksy.base.mapper;
+
+import com.zksy.base.video.VideoSession;
+
+import org.apache.ibatis.annotations.*;
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+
+@Mapper
+public interface VideoSessionMapper extends BaseMapper<VideoSession> {
+    @Select("SELECT equipment_id FROM app_user.equipment_base WHERE equipment_id=#{videoId} FOR UPDATE")
+    String lockVideo(@Param("videoId") String videoId);
+}

+ 0 - 11
pipe-network-service/zksy-system/src/main/java/com/zksy/base/service/StationVideoDeviceService.java

@@ -1,11 +0,0 @@
-package com.zksy.base.service;
-
-import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
-import com.baomidou.mybatisplus.extension.service.IService;
-import com.zksy.base.domain.StationVideoDevice;
-
-public interface StationVideoDeviceService extends IService<StationVideoDevice> {
-
-    Page<StationVideoDevice> findByPage(long pageNum, long pageSize,
-                                        String deviceName, String stationId, String status);
-}

+ 0 - 28
pipe-network-service/zksy-system/src/main/java/com/zksy/base/service/impl/StationVideoDeviceServiceImpl.java

@@ -1,28 +0,0 @@
-package com.zksy.base.service.impl;
-
-import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
-import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
-import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
-import com.zksy.base.domain.StationVideoDevice;
-import com.zksy.base.mapper.StationVideoDeviceMapper;
-import com.zksy.base.service.StationVideoDeviceService;
-import lombok.extern.slf4j.Slf4j;
-import org.springframework.stereotype.Service;
-
-@Slf4j
-@Service
-public class StationVideoDeviceServiceImpl extends ServiceImpl<StationVideoDeviceMapper, StationVideoDevice>
-        implements StationVideoDeviceService {
-
-    @Override
-    public Page<StationVideoDevice> findByPage(long pageNum, long pageSize,
-                                               String deviceName, String stationId, String status) {
-        Page<StationVideoDevice> page = new Page<>(pageNum, pageSize);
-        LambdaQueryWrapper<StationVideoDevice> wrapper = new LambdaQueryWrapper<>();
-        wrapper.like(deviceName != null && !deviceName.isEmpty(), StationVideoDevice::getDeviceName, deviceName);
-        wrapper.eq(stationId != null && !stationId.isEmpty(), StationVideoDevice::getStationId, stationId);
-        wrapper.eq(status != null && !status.isEmpty(), StationVideoDevice::getStatus, status);
-        wrapper.orderByDesc(StationVideoDevice::getCreateTime);
-        return this.page(page, wrapper);
-    }
-}

+ 21 - 0
pipe-network-service/zksy-system/src/main/java/com/zksy/base/video/EquipmentVideoBinding.java

@@ -0,0 +1,21 @@
+package com.zksy.base.video;
+
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+import java.time.LocalDateTime;
+
+/** 仅存视频接入信息,设备基础与资产信息仍由 equipment_base 管理。 */
+@Data
+@TableName("equipment_video_binding")
+public class EquipmentVideoBinding {
+    @TableId private String equipmentId;
+    private String sourceType;
+    private String videoUrl;
+    private String gbDeviceId;
+    private String gbChannelId;
+    private String stationId;
+    private String status;
+    private LocalDateTime createTime;
+    private LocalDateTime updateTime;
+}

+ 43 - 0
pipe-network-service/zksy-system/src/main/java/com/zksy/base/video/VideoCollectorClient.java

@@ -0,0 +1,43 @@
+package com.zksy.base.video;
+
+import java.util.*;
+import java.net.URI;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Component;
+import org.springframework.http.*;
+import org.springframework.http.client.SimpleClientHttpRequestFactory;
+import org.springframework.web.client.RestTemplate;
+import org.springframework.web.client.RestClientException;
+import org.springframework.web.util.UriComponentsBuilder;
+import com.zksy.common.exception.ServiceException;
+
+@Component
+public class VideoCollectorClient {
+    private final String base;
+    private final String token;
+    private final RestTemplate http;
+    public VideoCollectorClient(@Value("${video.service-url:http://127.0.0.1:8330}") String base,
+                                @Value("${video.internal-token:${VIDEO_INTERNAL_TOKEN:}}") String token) {
+        this.base=base; this.token=token;
+        SimpleClientHttpRequestFactory factory=new SimpleClientHttpRequestFactory();
+        factory.setConnectTimeout(3000); factory.setReadTimeout(95000);
+        this.http=new RestTemplate(factory);
+    }
+    public Object get(String path,Map<String,?> params) { return call(HttpMethod.GET,path,params,null); }
+    public Object post(String path,Object body) { return call(HttpMethod.POST,path,Collections.emptyMap(),body); }
+    public Object call(HttpMethod method,String path,Map<String,?> params,Object body) {
+        if(token.length()<32) throw new ServiceException("视频服务未配置内部鉴权,请设置 VIDEO_INTERNAL_TOKEN 后重启主服务");
+        UriComponentsBuilder builder=UriComponentsBuilder.fromHttpUrl(base).path(path);
+        params.forEach((key,value)->{if(value!=null) builder.queryParam(key,value);});
+        URI uri=builder.build().encode().toUri();
+        HttpHeaders headers=new HttpHeaders(); headers.set("X-Video-Token",token); headers.setContentType(MediaType.APPLICATION_JSON);
+        try {
+            ResponseEntity<Map> response=http.exchange(uri,method,new HttpEntity<>(body,headers),Map.class);
+            Map payload=response.getBody();
+            if(payload==null) return Collections.emptyMap();
+            Number code=(Number)payload.get("code");
+            if(code!=null && code.intValue()!=0 && code.intValue()!=200) throw new ServiceException(String.valueOf(payload.getOrDefault("msg","视频操作失败")));
+            return payload.containsKey("data") ? payload.get("data") : payload;
+        } catch(RestClientException error) { throw new ServiceException("国标视频服务连接失败或操作超时,请检查 video-service 与媒体节点"); }
+    }
+}

+ 23 - 0
pipe-network-service/zksy-system/src/main/java/com/zksy/base/video/VideoDeviceRecord.java

@@ -0,0 +1,23 @@
+package com.zksy.base.video;
+
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import com.zksy.base.domain.EquipmentBase;
+
+
+/** equipment_base 与 equipment_video_binding 的联查视图,兼容视频页面字段。 */
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class VideoDeviceRecord extends EquipmentBase {
+    private String videoId;
+    private String stationId;
+    private String deviceName;
+    private String deviceCode;
+    private String videoUrl;
+    private String sourceType;
+    private String gbDeviceId;
+    private String gbChannelId;
+    private String location;
+    private String status;
+    private Integer currentStatus;
+}

+ 282 - 0
pipe-network-service/zksy-system/src/main/java/com/zksy/base/video/VideoDeviceService.java

@@ -0,0 +1,282 @@
+package com.zksy.base.video;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.zksy.base.domain.EquipmentBase;
+import com.zksy.base.mapper.EquipmentBaseMapper;
+import com.zksy.base.mapper.EquipmentVideoBindingMapper;
+import org.springframework.beans.BeanUtils;
+import com.zksy.base.mapper.VideoSessionMapper;
+import com.zksy.base.service.EquipmentBaseService;
+import com.zksy.common.exception.ServiceException;
+import com.zksy.common.utils.SecurityUtils;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.dao.DuplicateKeyException;
+import org.springframework.scheduling.annotation.EnableScheduling;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import java.net.URI;
+import java.time.Duration;
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.*;
+/** 视频设备业务。视频台账统一使用 equipment_base,国标信令和媒体仍由 video-service 负责。 */
+@Slf4j
+@Service
+@EnableScheduling
+public class VideoDeviceService {
+
+    public static final int VIDEO_EQUIPMENT_TYPE_ID = 20;
+
+    private static final String GB28181 = "GB28181";
+
+    private static final DateTimeFormatter FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
+    private final EquipmentBaseMapper devices;
+    private final EquipmentVideoBindingMapper bindings;
+    private final VideoCollectorClient collector;
+    private final VideoSessionMapper sessions;
+    private final EquipmentBaseService equipmentService;
+
+    public VideoDeviceService(EquipmentBaseMapper devices, VideoCollectorClient collector, VideoSessionMapper sessions, EquipmentBaseService equipmentService, EquipmentVideoBindingMapper bindings) {
+        this.bindings=bindings;
+        this.devices=devices;
+        this.collector=collector;
+        this.sessions=sessions;
+        this.equipmentService=equipmentService;
+    }
+
+    public Page<VideoDeviceRecord> list(int pageNum,int pageSize,String query,String sourceType,String stationId) {
+        return bindings.list(new Page<>(Math.max(1,pageNum),Math.max(1,Math.min(100,pageSize))),query,sourceType,stationId);
+    }
+
+    public VideoDeviceRecord one(String id) { return find(id); }
+    public List<VideoDeviceRecord> all() { return bindings.all(); }
+
+    @Transactional
+    public String save(VideoRequests.Save input) {
+        boolean insert=input.getVideoId()==null||input.getVideoId().isBlank();
+        if(insert&&!GB28181.equals(input.getSourceType())) throw new ServiceException("视频设备请从发现注册设备中纳管,手动添加入口已关闭");
+        EquipmentBase row=insert?new EquipmentBase():find(input.getVideoId());
+        if(!insert) { sessions.lockVideo(row.getEquipmentId()); ensureIdle(row.getEquipmentId()); }
+        if(input.getEquipment()!=null) {
+            BeanUtils.copyProperties(input.getEquipment(),row,"equipmentId","equipmentTypeId","createTime","updateTime");
+            if(row.getDistrict()==null || row.getDistrict()<0 || row.getDistrict()>2) throw new ServiceException("请选择有效的所属片区");
+        }
+        EquipmentVideoBinding binding=new EquipmentVideoBinding();
+        if(GB28181.equals(input.getSourceType())) {
+            requireGbId(input.getGbDeviceId()); requireGbId(input.getGbChannelId());
+            if(!Boolean.TRUE.equals(collector.get("/internal/video/exists",Map.of("deviceId",input.getGbDeviceId(),"channelId",input.getGbChannelId())))) throw new ServiceException("设备或通道尚未注册,请先同步目录");
+            LambdaQueryWrapper<EquipmentVideoBinding> duplicate=new LambdaQueryWrapper<EquipmentVideoBinding>()
+                .eq(EquipmentVideoBinding::getGbDeviceId,input.getGbDeviceId()).eq(EquipmentVideoBinding::getGbChannelId,input.getGbChannelId());
+            if(!insert) duplicate.ne(EquipmentVideoBinding::getEquipmentId,row.getEquipmentId());
+            if(bindings.selectCount(duplicate)>0) throw new ServiceException("此设备通道已纳管");
+            binding.setGbDeviceId(input.getGbDeviceId()); binding.setGbChannelId(input.getGbChannelId());
+        } else { validateManualUrl(input.getVideoUrl()); binding.setVideoUrl(input.getVideoUrl()); }
+        String code=input.getDeviceCode()==null||input.getDeviceCode().isBlank()?null:input.getDeviceCode().trim();
+        if(code==null) code=GB28181.equals(input.getSourceType())?input.getGbDeviceId()+"/"+input.getGbChannelId():row.getEquipmentCode();
+        if(code!=null) {
+            LambdaQueryWrapper<EquipmentBase> duplicateCode=new LambdaQueryWrapper<EquipmentBase>().eq(EquipmentBase::getEquipmentCode,code);
+            if(!insert) duplicateCode.ne(EquipmentBase::getEquipmentId,row.getEquipmentId());
+            if(devices.selectCount(duplicateCode)>0) throw new ServiceException("设备编码已被其他设备使用,请修改编码");
+        }
+        row.setEquipmentTypeId(VIDEO_EQUIPMENT_TYPE_ID); row.setEquipmentName(input.getDeviceName().trim());
+        row.setEquipmentCode(code); row.setEquipmentLocation(input.getLocation()); row.setManufacturer(input.getManufacturer());
+        row.setRemark(input.getRemark()); row.setUpdateTime(LocalDateTime.now());
+        binding.setSourceType(input.getSourceType()); binding.setStationId(input.getStationId());
+        binding.setStatus("0"); binding.setUpdateTime(row.getUpdateTime());
+        try {
+            if(insert) {
+                row.setCreateTime(LocalDateTime.now());
+                if(!equipmentService.saveWithCheck(row)) throw new ServiceException("设备保存失败");
+                binding.setEquipmentId(row.getEquipmentId()); binding.setCreateTime(row.getCreateTime());
+                bindings.insert(binding);
+            } else {
+                // Use a base entity so protocol view fields cannot enter the equipment mapper.
+                EquipmentBase equipment=new EquipmentBase(); BeanUtils.copyProperties(row,equipment);
+                if(input.getEquipment()!=null) {
+                    if(!equipmentService.updateWithCheck(equipment)) throw new ServiceException("设备修改失败");
+                } else if(devices.updateById(equipment)==0) throw new ServiceException("设备修改失败");
+                binding.setEquipmentId(row.getEquipmentId()); bindings.updateBinding(binding);
+            }
+            if(input.getCurrentStatus()!=null && !equipmentService.updateEquipmentStatus(row.getEquipmentId(),input.getCurrentStatus()))
+                throw new ServiceException("设备资产状态保存失败");
+        } catch(DuplicateKeyException error) { throw new ServiceException("设备编码或国标通道已存在,请刷新列表后检查"); }
+        return row.getEquipmentId();
+    }
+
+    @Transactional
+    public void remove(String id) {
+        VideoDeviceRecord row=find(id); sessions.lockVideo(id); ensureIdle(id);
+        // The binding foreign key follows the existing ledger deletion transaction.
+        equipmentService.removeWithCheck(row.getEquipmentId());
+    }
+
+    public Object discover(String query,Boolean online,int pageNum,int pageSize) {
+        Map<String,Object> p=new HashMap<>();
+        p.put("query",query);
+        p.put("online",online);
+        p.put("pageNum",pageNum);
+        p.put("pageSize",pageSize);
+        return collector.get("/internal/video/devices",p);
+    }
+
+    public void sync(String id) {
+        requireGbId(id);
+        collector.post("/internal/video/devices/"+id+"/sync",Map.of());
+    }
+    @SuppressWarnings("unchecked")
+
+    public Map<String,Object> syncAll() {
+        Object response=collector.post("/internal/video/devices/sync",Map.of());
+        return response instanceof Map?new HashMap<>((Map<String,Object>)response):Map.of();
+    }
+    @Transactional
+    @SuppressWarnings("unchecked") public Map<String,Object> start(String id,VideoRequests.Range range) {
+        VideoDeviceRecord row=find(id);
+        sessions.lockVideo(id);
+        String mode=range==null?"LIVE":"PLAYBACK";
+        Object response;
+        if(range!=null) {
+            requireGb(row);
+            validateRange(range);
+            if(sessions.selectCount(new LambdaQueryWrapper<VideoSession>().eq(VideoSession::getVideoId,id).eq(VideoSession::getMode,"PLAYBACK").eq(VideoSession::getStatus,"ACTIVE"))>0)throw new ServiceException("此通道已有录像回放,请结束后重试");
+            response=collector.get("/api/playback/start",rangeParams(row,range));
+        }
+        else if(GB28181.equals(row.getSourceType()))response=collector.get("/api/play/start/"+row.getGbDeviceId()+"/"+row.getGbChannelId(),Map.of());
+        else {
+            validateManualUrl(row.getVideoUrl());
+            String scheme=URI.create(row.getVideoUrl()).getScheme().toLowerCase(Locale.ROOT);
+            response=scheme.startsWith("rt")?collector.post("/internal/video/manual/start",Map.of("id",id,"url",row.getVideoUrl())):Map.of("url",row.getVideoUrl(),"native",true);
+        }
+        if(!(response instanceof Map))throw new ServiceException("媒体服务没有返回有效播放信息");
+        Map<String,Object> data=new HashMap<>((Map<String,Object>)response);
+        VideoSession session=new VideoSession();
+        session.setSessionId(UUID.randomUUID().toString());
+        session.setVideoId(id);
+        session.setUserId(SecurityUtils.getUserId());
+        session.setMode(mode);
+        session.setStatus("ACTIVE");
+        session.setGbDeviceId(row.getGbDeviceId());
+        session.setGbChannelId(row.getGbChannelId());
+        session.setStreamId((String)data.get("stream"));
+        session.setProxyKey((String)data.remove("proxyKey"));
+        session.setCreateTime(LocalDateTime.now());
+        session.setLastSeen(session.getCreateTime());
+        sessions.insert(session);
+        data.put("sessionId",session.getSessionId());
+        data.put("mode",mode);
+        return data;
+    }
+
+    public Object records(String id,VideoRequests.Range range) {
+        VideoDeviceRecord row=find(id);
+        requireGb(row);
+        validateRange(range);
+        return collector.get("/api/gb_record/query",rangeParams(row,range));
+    }
+
+    public void ptz(String id,VideoRequests.Ptz input) {
+        VideoDeviceRecord row=find(id);
+        requireGb(row);
+        collector.get("/api/front-end/ptz/"+row.getGbDeviceId()+"/"+row.getGbChannelId(),Map.of("command",input.getCommand(),"horizonSpeed",input.getSpeed(),"verticalSpeed",input.getSpeed(),"zoomSpeed",input.getZoomSpeed()));
+    }
+    private VideoSession owned(String id) {
+        VideoSession s=sessions.selectById(id);
+        if(s==null||!Objects.equals(s.getUserId(),SecurityUtils.getUserId()))throw new ServiceException("播放会话不存在或无权操作");
+        return s;
+    }
+    @Transactional
+
+    public void heartbeat(String id) {
+        VideoSession s=owned(id);
+        if("ACTIVE".equals(s.getStatus())) {
+            s.setLastSeen(LocalDateTime.now());
+            sessions.updateById(s);
+        }
+    }
+    @Transactional
+
+    public void stop(String id) {
+        finish(owned(id));
+    }
+    private void finish(VideoSession session) {
+        if(!"ACTIVE".equals(session.getStatus()))return;
+        sessions.lockVideo(session.getVideoId());
+        VideoSession current=sessions.selectById(session.getSessionId());
+        if(current==null||!"ACTIVE".equals(current.getStatus()))return;
+        long others=sessions.selectCount(new LambdaQueryWrapper<VideoSession>().eq(VideoSession::getVideoId,session.getVideoId()).eq(VideoSession::getStatus,"ACTIVE").eq(VideoSession::getMode,"LIVE").ne(VideoSession::getSessionId,session.getSessionId()));
+        if("PLAYBACK".equals(session.getMode()))collector.get("/api/playback/stop/"+session.getGbDeviceId()+"/"+session.getGbChannelId()+"/"+safeStream(session.getStreamId()),Map.of());
+        else if(others==0&&session.getGbDeviceId()!=null)collector.get("/api/play/stop/"+session.getGbDeviceId()+"/"+session.getGbChannelId(),Map.of());
+        else if(others==0&&session.getProxyKey()!=null)collector.post("/internal/video/manual/stop",Map.of("key",session.getProxyKey()));
+        current.setStatus("STOPPED");
+        current.setEndTime(LocalDateTime.now());
+        sessions.updateById(current);
+    }
+
+    public void control(String id,VideoRequests.Control input) {
+        VideoSession s=owned(id);
+        if(!"ACTIVE".equals(s.getStatus())||!"PLAYBACK".equals(s.getMode()))throw new ServiceException("没有正在进行的录像回放");
+        String suffix="";
+        if("speed".equals(input.getAction())) {
+            if(input.getValue()==null||!Set.of(0.25,0.5,1.0,2.0,4.0).contains(input.getValue()))throw new ServiceException("回放速度支持 0.25、0.5、1、2、4 倍");
+            suffix="/"+input.getValue();
+        }
+        else if("seek".equals(input.getAction())) {
+            if(input.getValue()==null||!Double.isFinite(input.getValue())||input.getValue()<0||input.getValue()>604800)throw new ServiceException("回放位置超出范围");
+            suffix="/"+input.getValue().longValue();
+        }
+        collector.get("/api/playback/"+input.getAction()+"/"+safeStream(s.getStreamId())+suffix,Map.of());
+    }
+    @Scheduled(fixedDelay=30000)
+    @Transactional public void expireSessions() {
+        for(VideoSession s:sessions.selectList(new LambdaQueryWrapper<VideoSession>().eq(VideoSession::getStatus,"ACTIVE").lt(VideoSession::getLastSeen,LocalDateTime.now().minusSeconds(120))))try {
+            finish(s);
+        }
+        catch(Exception e) {
+            log.warn("视频会话回收失败,稍后重试: {}",s.getSessionId());
+        }
+    }
+    private VideoDeviceRecord find(String id) {
+        VideoDeviceRecord row=bindings.find(id);
+        if(row==null) throw new ServiceException("视频设备不存在");
+        return row;
+    }
+    private void ensureIdle(String id) {
+        if(sessions.selectCount(new LambdaQueryWrapper<VideoSession>().eq(VideoSession::getVideoId,id).eq(VideoSession::getStatus,"ACTIVE"))>0)throw new ServiceException("设备正在播放,请结束播放后再修改或移除");
+    }
+
+    static void requireGbId(String value) {
+        if(value==null||!value.matches("[0-9]{20}"))throw new ServiceException("国标设备和通道编号必须为 20 位数字");
+    }
+
+    private static void requireGb(VideoDeviceRecord row) {
+        if(!GB28181.equals(row.getSourceType()))throw new ServiceException("此功能需要国标设备通道");
+    }
+
+    private static String safeStream(String value) {
+        if(value==null||!value.matches("[A-Za-z0-9_-]{1,200}"))throw new ServiceException("无效播放流编号");
+        return value;
+    }
+    static void validateManualUrl(String value) {
+        try {
+            URI uri=URI.create(value);
+            if(uri.getHost()==null||!Set.of("rtsp","rtsps","rtmp","rtmps","http","https","ws","wss").contains(uri.getScheme().toLowerCase(Locale.ROOT)))throw new IllegalArgumentException();
+        }
+        catch(Exception e) {
+            throw new ServiceException("请输入有效的视频地址");
+        }
+    }
+    static void validateRange(VideoRequests.Range range) {
+        try {
+            LocalDateTime from=LocalDateTime.parse(range.getStartTime(),FORMAT),to=LocalDateTime.parse(range.getEndTime(),FORMAT);
+            if(!to.isAfter(from)||Duration.between(from,to).compareTo(Duration.ofDays(7))>0)throw new IllegalArgumentException();
+        }
+        catch(Exception e) {
+            throw new ServiceException("录像时间格式为 yyyy-MM-dd HH:mm:ss,结束时间须晚于开始时间且范围不超过 7 天");
+        }
+    }
+
+    private static Map<String,Object> rangeParams(VideoDeviceRecord row,VideoRequests.Range range) {
+        return Map.of("deviceId",row.getGbDeviceId(),"channelId",row.getGbChannelId(),"startTime",range.getStartTime(),"endTime",range.getEndTime(),"type","all");
+    }
+}

+ 42 - 0
pipe-network-service/zksy-system/src/main/java/com/zksy/base/video/VideoRequests.java

@@ -0,0 +1,42 @@
+package com.zksy.base.video;
+
+import lombok.Data;
+import javax.validation.Valid;
+import com.zksy.base.domain.EquipmentBase;
+import javax.validation.constraints.*;
+
+public final class VideoRequests {
+    private VideoRequests() {}
+    @Data
+    public static class Save {
+        @Valid private EquipmentBase equipment;
+        @Min(0) @Max(2) private Integer currentStatus;
+        @Size(max=64) private String videoId;
+        @NotBlank @Size(max=256) private String deviceName;
+        @Size(max=64) private String deviceCode;
+        @Size(max=64) private String stationId;
+        @Size(max=512) private String location;
+        @Size(max=128) private String manufacturer;
+        @Size(max=2000) private String remark;
+        @Pattern(regexp="MANUAL|GB28181") @NotBlank private String sourceType;
+        @Size(max=512) private String videoUrl;
+        @Pattern(regexp="[0-9]{20}") private String gbDeviceId;
+        @Pattern(regexp="[0-9]{20}") private String gbChannelId;
+    }
+    @Data
+    public static class Range {
+        @NotBlank private String startTime;
+        @NotBlank private String endTime;
+    }
+    @Data
+    public static class Ptz {
+        @NotBlank @Pattern(regexp="left|right|up|down|upleft|upright|downleft|downright|zoomin|zoomout|stop") private String command;
+        @Min(1) @Max(255) private Integer speed=100;
+        @Min(1) @Max(15) private Integer zoomSpeed=8;
+    }
+    @Data
+    public static class Control {
+        @NotBlank @Pattern(regexp="pause|resume|seek|speed") private String action;
+        private Double value;
+    }
+}

+ 22 - 0
pipe-network-service/zksy-system/src/main/java/com/zksy/base/video/VideoSession.java

@@ -0,0 +1,22 @@
+package com.zksy.base.video;
+
+import com.baomidou.mybatisplus.annotation.*;
+import lombok.Data;
+import java.time.LocalDateTime;
+
+@Data
+@TableName(value="video_play_session",schema="app_user")
+public class VideoSession {
+    @TableId(type=IdType.INPUT) private String sessionId;
+    private String videoId;
+    private Long userId;
+    private String mode;
+    private String streamId;
+    private String proxyKey;
+    private String gbDeviceId;
+    private String gbChannelId;
+    private String status;
+    private LocalDateTime createTime;
+    private LocalDateTime lastSeen;
+    private LocalDateTime endTime;
+}

+ 1 - 0
pom.xml

@@ -27,6 +27,7 @@
         <module>radar-service</module>
         <module>audio-service</module>
         <module>water-level-service</module>
+        <module>video-service</module>
     </modules>
     <parent>
         <groupId>org.springframework.boot</groupId>

+ 25 - 0
video-service/pom.xml

@@ -0,0 +1,25 @@
+<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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
+  <modelVersion>4.0.0</modelVersion>
+  <parent><groupId>com.zksy</groupId><artifactId>pipe-ner-server</artifactId><version>1.0.0</version></parent>
+  <artifactId>video-service</artifactId>
+  <description>独立 GB28181 信令、设备采集及媒体控制服务</description>
+  <dependencies>
+    <dependency><groupId>com.zksy</groupId><artifactId>zksy-common</artifactId><version>3.9.0</version><exclusions><exclusion><groupId>com.zksy</groupId><artifactId>minioutil</artifactId></exclusion></exclusions></dependency>
+    <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-websocket</artifactId></dependency>
+    <dependency><groupId>com.baomidou</groupId><artifactId>dynamic-datasource-spring-boot-starter</artifactId><version>3.6.1</version></dependency>
+    <dependency><groupId>cn.com.kingbase</groupId><artifactId>kingbase8</artifactId></dependency>
+    <dependency><groupId>javax.sip</groupId><artifactId>jain-sip-ri</artifactId><version>1.2.307</version></dependency>
+    <dependency><groupId>org.slf4j</groupId><artifactId>log4j-over-slf4j</artifactId></dependency>
+    <dependency><groupId>org.dom4j</groupId><artifactId>dom4j</artifactId><version>2.1.3</version></dependency>
+    <dependency><groupId>com.google.guava</groupId><artifactId>guava</artifactId><version>32.1.3-jre</version></dependency>
+    <dependency><groupId>com.alibaba.fastjson2</groupId><artifactId>fastjson2-extension</artifactId><version>2.0.17</version></dependency>
+    <dependency><groupId>com.squareup.okhttp3</groupId><artifactId>okhttp</artifactId><version>4.10.0</version></dependency>
+    <dependency><groupId>com.squareup.okhttp3</groupId><artifactId>logging-interceptor</artifactId><version>4.10.0</version></dependency>
+    <dependency><groupId>io.github.rburgst</groupId><artifactId>okhttp-digest</artifactId><version>2.7</version></dependency>
+    <dependency><groupId>com.github.oshi</groupId><artifactId>oshi-core</artifactId><version>6.2.2</version></dependency>
+    <dependency><groupId>org.springframework.session</groupId><artifactId>spring-session-core</artifactId></dependency>
+    <dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><version>1.18.34</version><scope>provided</scope></dependency>
+    <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency>
+  </dependencies>
+  <build><finalName>video-service</finalName><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId><configuration><mainClass>com.zksy.video.VideoApplication</mainClass></configuration></plugin></plugins></build>
+</project>

+ 46 - 0
video-service/src/main/java/com/ruoyi/wvp/common/CivilCodePo.java

@@ -0,0 +1,46 @@
+package com.ruoyi.wvp.common;
+
+import org.springframework.util.ObjectUtils;
+
+public class CivilCodePo {
+
+    private String code;
+
+    private String name;
+
+    private String parentCode;
+
+    public static CivilCodePo getInstance(String[] infoArray) {
+        CivilCodePo civilCodePo = new CivilCodePo();
+        civilCodePo.setCode(infoArray[0]);
+        civilCodePo.setName(infoArray[1]);
+        if (!ObjectUtils.isEmpty(infoArray[2])) {
+            civilCodePo.setParentCode(infoArray[2]);
+        }
+        return civilCodePo;
+    }
+
+    public String getCode() {
+        return code;
+    }
+
+    public void setCode(String code) {
+        this.code = code;
+    }
+
+    public String getName() {
+        return name;
+    }
+
+    public void setName(String name) {
+        this.name = name;
+    }
+
+    public String getParentCode() {
+        return parentCode;
+    }
+
+    public void setParentCode(String parentCode) {
+        this.parentCode = parentCode;
+    }
+}

+ 5 - 0
video-service/src/main/java/com/ruoyi/wvp/common/CommonCallback.java

@@ -0,0 +1,5 @@
+package com.ruoyi.wvp.common;
+
+public interface CommonCallback<T>{
+    public void run(T t);
+}

+ 5 - 0
video-service/src/main/java/com/ruoyi/wvp/common/GeneralCallback.java

@@ -0,0 +1,5 @@
+package com.ruoyi.wvp.common;
+
+public interface GeneralCallback<T>{
+    void run(int code, String msg, T data);
+}

+ 59 - 0
video-service/src/main/java/com/ruoyi/wvp/common/InviteInfo.java

@@ -0,0 +1,59 @@
+package com.ruoyi.wvp.common;
+
+import com.ruoyi.wvp.service.bean.SSRCInfo;
+import lombok.Data;
+
+/**
+ * 记录每次发送invite消息的状态
+ */
+@Data
+public class InviteInfo {
+
+    private String deviceId;
+
+    private Integer channelId;
+
+    private String stream;
+
+    private SSRCInfo ssrcInfo;
+
+    private String receiveIp;
+
+    private Integer receivePort;
+
+    private String streamMode;
+
+    private InviteSessionType type;
+
+    private InviteSessionStatus status;
+
+    private StreamInfo streamInfo;
+
+    private String mediaServerId;
+
+    private Long expirationTime;
+
+    private Long createTime;
+
+    private Boolean record;
+
+
+    public static InviteInfo getInviteInfo(String deviceId, Integer channelId, String stream, SSRCInfo ssrcInfo, String mediaServerId,
+                                           String receiveIp, Integer receivePort, String streamMode,
+                                           InviteSessionType type, InviteSessionStatus status, Boolean record) {
+        InviteInfo inviteInfo = new InviteInfo();
+        inviteInfo.setDeviceId(deviceId);
+        inviteInfo.setChannelId(channelId);
+        inviteInfo.setStream(stream);
+        inviteInfo.setSsrcInfo(ssrcInfo);
+        inviteInfo.setReceiveIp(receiveIp);
+        inviteInfo.setReceivePort(receivePort);
+        inviteInfo.setStreamMode(streamMode);
+        inviteInfo.setType(type);
+        inviteInfo.setStatus(status);
+        inviteInfo.setMediaServerId(mediaServerId);
+        inviteInfo.setRecord(record);
+        return inviteInfo;
+    }
+
+}

+ 11 - 0
video-service/src/main/java/com/ruoyi/wvp/common/InviteSessionStatus.java

@@ -0,0 +1,11 @@
+package com.ruoyi.wvp.common;
+
+/**
+ * 标识invite消息发出后的各个状态,
+ * 收到ok钱停止invite发送cancel,
+ * 收到200ok后发送BYE停止invite
+ */
+public enum InviteSessionStatus {
+    ready,
+    ok,
+}

+ 9 - 0
video-service/src/main/java/com/ruoyi/wvp/common/InviteSessionType.java

@@ -0,0 +1,9 @@
+package com.ruoyi.wvp.common;
+
+public enum InviteSessionType {
+    PLAY,
+    PLAYBACK,
+    DOWNLOAD,
+    BROADCAST,
+    TALK
+}

+ 780 - 0
video-service/src/main/java/com/ruoyi/wvp/common/StreamInfo.java

@@ -0,0 +1,780 @@
+package com.ruoyi.wvp.common;
+
+
+import com.ruoyi.wvp.media.bean.MediaInfo;
+import com.ruoyi.wvp.media.bean.MediaServer;
+import com.ruoyi.wvp.service.bean.DownloadFileInfo;
+
+import java.io.Serializable;
+import java.util.Objects;
+
+
+/**
+ * 流信息
+ */
+public class StreamInfo implements Serializable, Cloneable{
+
+    /**
+     * Schema
+     */
+    private String app;
+    /**
+     * 流ID
+     */
+    private String stream;
+
+    /**
+     * 设备编号
+     */
+    private String deviceId;
+
+    /**
+     * 通道ID
+     */
+    private Integer channelId;
+
+    /**
+     * IP
+     */
+    private String ip;
+
+    /**
+     * HTTP-FLV流地址
+     */
+    private StreamURL flv;
+
+    /**
+     * HTTPS-FLV流地址
+     */
+    private StreamURL https_flv;
+
+    /**
+     * Websocket-FLV流地址
+     */
+    private StreamURL ws_flv;
+
+    /**
+     * Websockets-FLV流地址
+     */
+    private StreamURL wss_flv;
+
+    /**
+     * HTTP-FMP4流地址
+     */
+    private StreamURL fmp4;
+
+    /**
+     * HTTPS-FMP4流地址
+     */
+    private StreamURL https_fmp4;
+
+    /**
+     * Websocket-FMP4流地址
+     */
+    private StreamURL ws_fmp4;
+
+    /**
+     * Websockets-FMP4流地址
+     */
+    private StreamURL wss_fmp4;
+
+    /**
+     * HLS流地址
+     */
+    private StreamURL hls;
+
+    /**
+     * HTTPS-HLS流地址
+     */
+    private StreamURL https_hls;
+
+    /**
+     * Websocket-HLS流地址
+     */
+    private StreamURL ws_hls;
+
+    /**
+     * Websockets-HLS流地址
+     */
+    private StreamURL wss_hls;
+
+    /**
+     * HTTP-TS流地址
+     */
+    private StreamURL ts;
+
+    /**
+     * HTTPS-TS流地址
+     */
+    private StreamURL https_ts;
+
+    /**
+     * Websocket-TS流地址
+     */
+    private StreamURL ws_ts;
+
+    /**
+     * Websockets-TS流地址
+     */
+    private StreamURL wss_ts;
+
+    /**
+     * RTMP流地址
+     */
+    private StreamURL rtmp;
+
+    /**
+     * RTMPS流地址
+     */
+    private StreamURL rtmps;
+
+    /**
+     * RTSP流地址
+     */
+    private StreamURL rtsp;
+
+    /**
+     * RTSPS流地址
+     */
+    private StreamURL rtsps;
+
+    /**
+     * RTC流地址
+     */
+    private StreamURL rtc;
+
+    /**
+     * RTCS流地址
+     */
+    private StreamURL rtcs;
+
+    /**
+     * 流媒体节点
+     */
+    private MediaServer mediaServer;
+
+    /**
+     * 流编码信息
+     */
+    private MediaInfo mediaInfo;
+
+    /**
+     * 开始时间
+     */
+    private String startTime;
+
+    /**
+     * 结束时间
+     */
+    private String endTime;
+
+    /**
+     * 进度(录像下载使用)
+     */
+    private double progress;
+
+    /**
+     * 文件下载地址(录像下载使用)
+     */
+    private DownloadFileInfo downLoadFilePath;
+
+    /**
+     * 点播请求的callId
+     */
+    private String callId;
+
+    /**
+     * 是否暂停(录像回放使用)
+     */
+    private boolean pause;
+
+    /**
+     * 产生源类型,包括 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;
+
+    /**
+     * 转码后的视频流
+     */
+    private StreamInfo transcodeStream;
+
+    /**
+     * 使用的WVP ID
+     */
+    private String serverId;
+
+    public void setFlv(StreamURL flv) {
+        this.flv = flv;
+    }
+
+    public void setHttps_flv(StreamURL https_flv) {
+        this.https_flv = https_flv;
+    }
+
+    public void setWs_flv(StreamURL ws_flv) {
+        this.ws_flv = ws_flv;
+    }
+
+    public void setWss_flv(StreamURL wss_flv) {
+        this.wss_flv = wss_flv;
+    }
+
+    public void setFmp4(StreamURL fmp4) {
+        this.fmp4 = fmp4;
+    }
+
+    public void setHttps_fmp4(StreamURL https_fmp4) {
+        this.https_fmp4 = https_fmp4;
+    }
+
+    public void setWs_fmp4(StreamURL ws_fmp4) {
+        this.ws_fmp4 = ws_fmp4;
+    }
+
+    public void setWss_fmp4(StreamURL wss_fmp4) {
+        this.wss_fmp4 = wss_fmp4;
+    }
+
+    public void setHls(StreamURL hls) {
+        this.hls = hls;
+    }
+
+    public void setHttps_hls(StreamURL https_hls) {
+        this.https_hls = https_hls;
+    }
+
+    public void setWs_hls(StreamURL ws_hls) {
+        this.ws_hls = ws_hls;
+    }
+
+    public void setWss_hls(StreamURL wss_hls) {
+        this.wss_hls = wss_hls;
+    }
+
+    public void setTs(StreamURL ts) {
+        this.ts = ts;
+    }
+
+    public void setHttps_ts(StreamURL https_ts) {
+        this.https_ts = https_ts;
+    }
+
+    public void setWs_ts(StreamURL ws_ts) {
+        this.ws_ts = ws_ts;
+    }
+
+    public void setWss_ts(StreamURL wss_ts) {
+        this.wss_ts = wss_ts;
+    }
+
+    public void setRtmp(StreamURL rtmp) {
+        this.rtmp = rtmp;
+    }
+
+    public void setRtmps(StreamURL rtmps) {
+        this.rtmps = rtmps;
+    }
+
+    public void setRtsp(StreamURL rtsp) {
+        this.rtsp = rtsp;
+    }
+
+    public void setRtsps(StreamURL rtsps) {
+        this.rtsps = rtsps;
+    }
+
+    public void setRtc(StreamURL rtc) {
+        this.rtc = rtc;
+    }
+
+    public void setRtcs(StreamURL rtcs) {
+        this.rtcs = rtcs;
+    }
+
+    public void setRtmp(String host, int port, int sslPort, String app, String stream, String callIdParam) {
+        String file = String.format("%s/%s%s", app, stream, callIdParam);
+        if (port > 0) {
+            this.rtmp = new StreamURL("rtmp", host, port, file);
+        }
+        if (sslPort > 0) {
+            this.rtmps = new StreamURL("rtmps", host, sslPort, file);
+        }
+    }
+
+    public void setRtsp(String host, int port, int sslPort, String app, String stream, String callIdParam) {
+        String file = String.format("%s/%s%s", app, stream, callIdParam);
+        if (port > 0) {
+            this.rtsp = new StreamURL("rtsp", host, port, file);
+        }
+        if (sslPort > 0) {
+            this.rtsps = new StreamURL("rtsps", host, sslPort, file);
+        }
+    }
+
+    public void setFlv(String host, int port, int sslPort, String file) {
+        if (port > 0) {
+            this.flv = new StreamURL("http", host, port, file);
+        }
+        this.ws_flv = new StreamURL("ws", host, port, file);
+        if (sslPort > 0) {
+            this.https_flv = new StreamURL("https", host, sslPort, file);
+            this.wss_flv = new StreamURL("wss", host, sslPort, file);
+        }
+    }
+
+    public void setWsFlv(String host, int port, int sslPort, String file) {
+        if (port > 0) {
+            this.ws_flv = new StreamURL("ws", host, port, file);
+        }
+        if (sslPort > 0) {
+            this.wss_flv = new StreamURL("wss", host, sslPort, file);
+        }
+    }
+
+    public void setFmp4(String host, int port, int sslPort, String app, String stream, String callIdParam) {
+        String file = String.format("%s/%s.live.mp4%s", app, stream, callIdParam);
+        if (port > 0) {
+            this.fmp4 = new StreamURL("http", host, port, file);
+            this.ws_fmp4 = new StreamURL("ws", host, port, file);
+        }
+        if (sslPort > 0) {
+            this.https_fmp4 = new StreamURL("https", host, sslPort, file);
+            this.wss_fmp4 = new StreamURL("wss", host, sslPort, file);
+        }
+    }
+
+    public void setHls(String host, int port, int sslPort, String app, String stream, String callIdParam) {
+        String file = String.format("%s/%s/hls.m3u8%s", app, stream, callIdParam);
+        if (port > 0) {
+            this.hls = new StreamURL("http", host, port, file);
+            this.ws_hls = new StreamURL("ws", host, port, file);
+        }
+        if (sslPort > 0) {
+            this.https_hls = new StreamURL("https", host, sslPort, file);
+            this.wss_hls = new StreamURL("wss", host, sslPort, file);
+        }
+    }
+
+    public void setTs(String host, int port, int sslPort, String app, String stream, String callIdParam) {
+        String file = String.format("%s/%s.live.ts%s", app, stream, callIdParam);
+
+        if (port > 0) {
+            this.ts = new StreamURL("http", host, port, file);
+            this.ws_ts = new StreamURL("ws", host, port, file);
+        }
+        if (sslPort > 0) {
+            this.https_ts = new StreamURL("https", host, sslPort, file);
+            this.wss_ts = new StreamURL("wss", host, sslPort, file);
+        }
+    }
+
+    public void setRtc(String host, int port, int sslPort, String app, String stream, String callIdParam, boolean isPlay) {
+        if (callIdParam != null) {
+            callIdParam = Objects.equals(callIdParam, "") ? callIdParam : callIdParam.replace("?", "&");
+        }
+        String file = String.format("index/api/webrtc?app=%s&stream=%s&type=%s%s", app, stream, isPlay?"play":"push", callIdParam);
+        if (port > 0) {
+            this.rtc = new StreamURL("http", host, port, file);
+        }
+        if (sslPort > 0) {
+            this.rtcs = new StreamURL("https", host, sslPort, file);
+        }
+    }
+
+    public void channgeStreamIp(String localAddr) {
+        if (this.flv != null) {
+            this.flv.setHost(localAddr);
+        }
+        if (this.ws_flv != null ){
+            this.ws_flv.setHost(localAddr);
+        }
+        if (this.hls != null ) {
+            this.hls.setHost(localAddr);
+        }
+        if (this.ws_hls != null ) {
+            this.ws_hls.setHost(localAddr);
+        }
+        if (this.ts != null ) {
+            this.ts.setHost(localAddr);
+        }
+        if (this.ws_ts != null ) {
+            this.ws_ts.setHost(localAddr);
+        }
+        if (this.fmp4 != null ) {
+            this.fmp4.setHost(localAddr);
+        }
+        if (this.ws_fmp4 != null ) {
+            this.ws_fmp4.setHost(localAddr);
+        }
+        if (this.rtc != null ) {
+            this.rtc.setHost(localAddr);
+        }
+        if (this.https_flv != null) {
+            this.https_flv.setHost(localAddr);
+        }
+        if (this.wss_flv != null) {
+            this.wss_flv.setHost(localAddr);
+        }
+        if (this.https_hls != null) {
+            this.https_hls.setHost(localAddr);
+        }
+        if (this.wss_hls != null) {
+            this.wss_hls.setHost(localAddr);
+        }
+        if (this.wss_ts != null) {
+            this.wss_ts.setHost(localAddr);
+        }
+        if (this.https_fmp4 != null) {
+            this.https_fmp4.setHost(localAddr);
+        }
+        if (this.wss_fmp4 != null) {
+            this.wss_fmp4.setHost(localAddr);
+        }
+        if (this.rtcs != null) {
+            this.rtcs.setHost(localAddr);
+        }
+        if (this.rtsp != null) {
+            this.rtsp.setHost(localAddr);
+        }
+        if (this.rtsps != null) {
+            this.rtsps.setHost(localAddr);
+        }
+        if (this.rtmp != null) {
+            this.rtmp.setHost(localAddr);
+        }
+        if (this.rtmps != null) {
+            this.rtmps.setHost(localAddr);
+        }
+    }
+
+
+    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 Integer getChannelId() {
+        return channelId;
+    }
+
+    public void setChannelId(Integer channelId) {
+        this.channelId = channelId;
+    }
+
+    public String getStream() {
+        return stream;
+    }
+
+    public void setStream(String stream) {
+        this.stream = stream;
+    }
+
+    public String getIp() {
+        return ip;
+    }
+
+    public void setIp(String ip) {
+        this.ip = ip;
+    }
+
+    public StreamURL getFlv() {
+        return flv;
+    }
+
+    public StreamURL getHttps_flv() {
+        return https_flv;
+    }
+
+    public StreamURL getWs_flv() {
+        return ws_flv;
+    }
+
+
+    public StreamURL getWss_flv() {
+        return wss_flv;
+    }
+
+    public StreamURL getFmp4() {
+        return fmp4;
+    }
+
+
+
+    public StreamURL getHttps_fmp4() {
+        return https_fmp4;
+    }
+
+    public StreamURL getWs_fmp4() {
+        return ws_fmp4;
+    }
+
+    public StreamURL getWss_fmp4() {
+        return wss_fmp4;
+    }
+
+    public StreamURL getHls() {
+        return hls;
+    }
+
+
+    public StreamURL getHttps_hls() {
+        return https_hls;
+    }
+
+    public StreamURL getWs_hls() {
+        return ws_hls;
+    }
+
+    public StreamURL getWss_hls() {
+        return wss_hls;
+    }
+
+    public StreamURL getTs() {
+        return ts;
+    }
+
+
+    public StreamURL getHttps_ts() {
+        return https_ts;
+    }
+
+
+    public StreamURL getWs_ts() {
+        return ws_ts;
+    }
+
+
+    public StreamURL getWss_ts() {
+        return wss_ts;
+    }
+
+
+    public StreamURL getRtmp() {
+        return rtmp;
+    }
+
+    public StreamURL getRtmps() {
+        return rtmps;
+    }
+
+    public StreamURL getRtsp() {
+        return rtsp;
+    }
+
+    public StreamURL getRtsps() {
+        return rtsps;
+    }
+
+    public StreamURL getRtc() {
+        return rtc;
+    }
+
+    public StreamURL getRtcs() {
+        return rtcs;
+    }
+
+    public MediaServer getMediaServer() {
+        return mediaServer;
+    }
+
+    public void setMediaServer(MediaServer mediaServer) {
+        this.mediaServer = mediaServer;
+    }
+
+    public MediaInfo getMediaInfo() {
+        return mediaInfo;
+    }
+
+    public void setMediaInfo(MediaInfo mediaInfo) {
+        this.mediaInfo = mediaInfo;
+    }
+
+    public String getStartTime() {
+        return startTime;
+    }
+
+    public void setStartTime(String startTime) {
+        this.startTime = startTime;
+    }
+
+    public String getEndTime() {
+        return endTime;
+    }
+
+    public void setEndTime(String endTime) {
+        this.endTime = endTime;
+    }
+
+    public double getProgress() {
+        return progress;
+    }
+
+    public void setProgress(double progress) {
+        this.progress = progress;
+    }
+
+    public boolean isPause() {
+        return pause;
+    }
+
+    public void setPause(boolean pause) {
+        this.pause = pause;
+    }
+
+    public TransactionInfo getTransactionInfo() {
+        return transactionInfo;
+    }
+
+    public void setTransactionInfo(TransactionInfo transactionInfo) {
+        this.transactionInfo = transactionInfo;
+    }
+
+    public StreamInfo getTranscodeStream() {
+        return transcodeStream;
+    }
+
+    public void setTranscodeStream(StreamInfo transcodeStream) {
+        this.transcodeStream = transcodeStream;
+    }
+
+    @Override
+    public StreamInfo clone() {
+        StreamInfo instance = null;
+        try{
+            instance = (StreamInfo)super.clone();
+            if (this.flv != null) {
+                instance.flv=this.flv.clone();
+            }
+            if (this.ws_flv != null ){
+                instance.ws_flv= this.ws_flv.clone();
+            }
+            if (this.hls != null ) {
+                instance.hls= this.hls.clone();
+            }
+            if (this.ws_hls != null ) {
+                instance.ws_hls= this.ws_hls.clone();
+            }
+            if (this.ts != null ) {
+                instance.ts= this.ts.clone();
+            }
+            if (this.ws_ts != null ) {
+                instance.ws_ts= this.ws_ts.clone();
+            }
+            if (this.fmp4 != null ) {
+                instance.fmp4= this.fmp4.clone();
+            }
+            if (this.ws_fmp4 != null ) {
+                instance.ws_fmp4= this.ws_fmp4.clone();
+            }
+            if (this.rtc != null ) {
+                instance.rtc= this.rtc.clone();
+            }
+            if (this.https_flv != null) {
+                instance.https_flv= this.https_flv.clone();
+            }
+            if (this.wss_flv != null) {
+                instance.wss_flv= this.wss_flv.clone();
+            }
+            if (this.https_hls != null) {
+                instance.https_hls= this.https_hls.clone();
+            }
+            if (this.wss_hls != null) {
+                instance.wss_hls= this.wss_hls.clone();
+            }
+            if (this.wss_ts != null) {
+                instance.wss_ts= this.wss_ts.clone();
+            }
+            if (this.https_fmp4 != null) {
+                instance.https_fmp4= this.https_fmp4.clone();
+            }
+            if (this.wss_fmp4 != null) {
+                instance.wss_fmp4= this.wss_fmp4.clone();
+            }
+            if (this.rtcs != null) {
+                instance.rtcs= this.rtcs.clone();
+            }
+            if (this.rtsp != null) {
+                instance.rtsp= this.rtsp.clone();
+            }
+            if (this.rtsps != null) {
+                instance.rtsps= this.rtsps.clone();
+            }
+            if (this.rtmp != null) {
+                instance.rtmp= this.rtmp.clone();
+            }
+            if (this.rtmps != null) {
+                instance.rtmps= this.rtmps.clone();
+            }
+        }catch(CloneNotSupportedException e) {
+            e.printStackTrace();
+        }
+        return instance;
+    }
+
+
+    /*=========================设备主子码流逻辑START====================*/
+    /**
+     * 是否为子码流(true-是,false-主码流)
+     */
+    private boolean subStream;
+
+    public boolean isSubStream() {
+        return subStream;
+    }
+
+    public void setSubStream(boolean subStream) {
+        this.subStream = subStream;
+    }
+
+    public DownloadFileInfo getDownLoadFilePath() {
+        return downLoadFilePath;
+    }
+
+    public void setDownLoadFilePath(DownloadFileInfo downLoadFilePath) {
+        this.downLoadFilePath = downLoadFilePath;
+    }
+
+    public int getOriginType() {
+        return originType;
+    }
+
+    public void setOriginType(int originType) {
+        this.originType = originType;
+    }
+
+    public String getServerId() {
+        return serverId;
+    }
+
+    public void setServerId(String serverId) {
+        this.serverId = serverId;
+    }
+
+    public String getCallId() {
+        return callId;
+    }
+
+    public void setCallId(String callId) {
+        this.callId = callId;
+    }
+}

+ 94 - 0
video-service/src/main/java/com/ruoyi/wvp/common/StreamURL.java

@@ -0,0 +1,94 @@
+package com.ruoyi.wvp.common;
+
+import java.io.Serializable;
+
+
+/**
+ * 流地址信息
+ */
+public class StreamURL implements Serializable,Cloneable {
+
+    /**
+     * 协议
+     */
+    private String protocol;
+
+    /**
+     * 主机地址
+     */
+    private String host;
+
+    /**
+     * 端口
+     */
+    private int port = -1;
+
+    /**
+     * 定位位置
+     */
+    private String file;
+
+    /**
+     * 拼接后的地址
+     */
+    private String url;
+
+    public StreamURL() {
+    }
+
+    public StreamURL(String protocol, String host, int port, String file) {
+        this.protocol = protocol;
+        this.host = host;
+        this.port = port;
+        this.file = file;
+    }
+
+    public String getProtocol() {
+        return protocol;
+    }
+
+    public void setProtocol(String protocol) {
+        this.protocol = protocol;
+    }
+
+    public String getHost() {
+        return host;
+    }
+
+    public void setHost(String host) {
+        this.host = host;
+    }
+
+    public int getPort() {
+        return port;
+    }
+
+    public void setPort(int port) {
+        this.port = port;
+    }
+
+    public String getFile() {
+        return file;
+    }
+
+    public void setFile(String file) {
+        this.file = file;
+    }
+
+    public String getUrl() {
+        return this.toString();
+    }
+
+    @Override
+    public String toString() {
+        if (protocol != null && host != null && port != -1 ) {
+            return String.format("%s://%s:%s/%s", protocol, host, port, file);
+        }else {
+            return null;
+        }
+    }
+    @Override
+    public StreamURL clone() throws CloneNotSupportedException {
+        return (StreamURL) super.clone();
+    }
+}

+ 54 - 0
video-service/src/main/java/com/ruoyi/wvp/common/SystemAllInfo.java

@@ -0,0 +1,54 @@
+package com.ruoyi.wvp.common;
+
+import java.util.List;
+
+public class SystemAllInfo {
+
+    private List<Object> cpu;
+    private List<Object> mem;
+    private List<Object> net;
+
+    private long netTotal;
+
+    private Object disk;
+
+    public List<Object> getCpu() {
+        return cpu;
+    }
+
+    public void setCpu(List<Object> cpu) {
+        this.cpu = cpu;
+    }
+
+    public List<Object> getMem() {
+        return mem;
+    }
+
+    public void setMem(List<Object> mem) {
+        this.mem = mem;
+    }
+
+    public List<Object> getNet() {
+        return net;
+    }
+
+    public void setNet(List<Object> net) {
+        this.net = net;
+    }
+
+    public Object getDisk() {
+        return disk;
+    }
+
+    public void setDisk(Object disk) {
+        this.disk = disk;
+    }
+
+    public long getNetTotal() {
+        return netTotal;
+    }
+
+    public void setNetTotal(long netTotal) {
+        this.netTotal = netTotal;
+    }
+}

+ 149 - 0
video-service/src/main/java/com/ruoyi/wvp/common/VersionPo.java

@@ -0,0 +1,149 @@
+package com.ruoyi.wvp.common;
+
+import com.alibaba.fastjson2.annotation.JSONField;
+
+public class VersionPo {
+    /**
+     * git的全版本号
+     */
+    @JSONField(name="GIT_Revision")
+    private String GIT_Revision;
+    /**
+     * maven版本
+     */
+    @JSONField(name = "Create_By")
+    private String Create_By;
+    /**
+     * git的分支
+     */
+    @JSONField(name = "GIT_BRANCH")
+    private String GIT_BRANCH;
+    /**
+     * git的url
+     */
+    @JSONField(name = "GIT_URL")
+    private String GIT_URL;
+    /**
+     * 构建日期
+     */
+    @JSONField(name = "BUILD_DATE")
+    private String BUILD_DATE;
+    /**
+     * 构建日期
+     */
+    @JSONField(name = "GIT_DATE")
+    private String GIT_DATE;
+    /**
+     * 项目名称 配合pom使用
+     */
+    @JSONField(name = "artifactId")
+    private String artifactId;
+    /**
+     * git局部版本号
+     */
+    @JSONField(name = "GIT_Revision_SHORT")
+    private String GIT_Revision_SHORT;
+    /**
+     * 项目的版本如2.0.1.0 配合pom使用
+     */
+    @JSONField(name = "version")
+    private String version;
+    /**
+     * 子系统名称
+     */
+    @JSONField(name = "project")
+    private String project;
+    /**
+     * jdk版本
+     */
+    @JSONField(name="Build_Jdk")
+    private String Build_Jdk;
+
+    public void setGIT_Revision(String GIT_Revision) {
+        this.GIT_Revision = GIT_Revision;
+    }
+
+    public void setCreate_By(String create_By) {
+        Create_By = create_By;
+    }
+
+    public void setGIT_BRANCH(String GIT_BRANCH) {
+        this.GIT_BRANCH = GIT_BRANCH;
+    }
+
+    public void setGIT_URL(String GIT_URL) {
+        this.GIT_URL = GIT_URL;
+    }
+
+    public void setBUILD_DATE(String BUILD_DATE) {
+        this.BUILD_DATE = BUILD_DATE;
+    }
+
+    public void setArtifactId(String artifactId) {
+        this.artifactId = artifactId;
+    }
+
+    public void setGIT_Revision_SHORT(String GIT_Revision_SHORT) {
+        this.GIT_Revision_SHORT = GIT_Revision_SHORT;
+    }
+
+    public void setVersion(String version) {
+        this.version = version;
+    }
+
+    public void setProject(String project) {
+        this.project = project;
+    }
+
+    public void setBuild_Jdk(String build_Jdk) {
+        Build_Jdk = build_Jdk;
+    }
+
+    public String getGIT_Revision() {
+        return GIT_Revision;
+    }
+
+    public String getCreate_By() {
+        return Create_By;
+    }
+
+    public String getGIT_BRANCH() {
+        return GIT_BRANCH;
+    }
+
+    public String getGIT_URL() {
+        return GIT_URL;
+    }
+
+    public String getBUILD_DATE() {
+        return BUILD_DATE;
+    }
+
+    public String getArtifactId() {
+        return artifactId;
+    }
+
+    public String getGIT_Revision_SHORT() {
+        return GIT_Revision_SHORT;
+    }
+
+    public String getVersion() {
+        return version;
+    }
+
+    public String getProject() {
+        return project;
+    }
+
+    public String getBuild_Jdk() {
+        return Build_Jdk;
+    }
+
+    public String getGIT_DATE() {
+        return GIT_DATE;
+    }
+
+    public void setGIT_DATE(String GIT_DATE) {
+        this.GIT_DATE = GIT_DATE;
+    }
+}

+ 159 - 0
video-service/src/main/java/com/ruoyi/wvp/common/VideoManagerConstants.java

@@ -0,0 +1,159 @@
+package com.ruoyi.wvp.common;
+
+/**
+ * @description: 定义常量
+ * @author: swwheihei
+ * @date:   2019年5月30日 下午3:04:04
+ *
+ */
+public class VideoManagerConstants {
+
+	public static final String WVP_SERVER_PREFIX = "CITY_VIDEO_VMP_SIGNALLING_SERVER_INFO_";
+
+	public static final String WVP_SERVER_STREAM_PREFIX = "CITY_VIDEO_VMP_SIGNALLING_STREAM_";
+
+	public static final String MEDIA_SERVER_PREFIX = "CITY_VIDEO_VMP_MEDIA_SERVER_INFO:";
+
+	public static final String ONLINE_MEDIA_SERVERS_PREFIX = "CITY_VIDEO_VMP_ONLINE_MEDIA_SERVERS:";
+
+	public static final String DEVICE_PREFIX = "CITY_VIDEO_VMP_DEVICE_INFO";
+
+	public static final String INVITE_PREFIX = "CITY_VIDEO_VMP_GB_INVITE_INFO";
+
+	public static final String PLATFORM_CATCH_PREFIX = "CITY_VIDEO_VMP_PLATFORM_CATCH_";
+
+	public static final String PLATFORM_REGISTER_INFO_PREFIX = "CITY_VIDEO_VMP_PLATFORM_REGISTER_INFO_";
+
+	public static final String SEND_RTP_PORT = "VM_SEND_RTP_PORT:";
+	public static final String SEND_RTP_INFO_CALLID = "CITY_VIDEO_VMP_SEND_RTP_INFO:CALL_ID:";
+	public static final String SEND_RTP_INFO_STREAM = "CITY_VIDEO_VMP_SEND_RTP_INFO:STREAM:";
+	public static final String SEND_RTP_INFO_CHANNEL = "CITY_VIDEO_VMP_SEND_RTP_INFO:CHANNEL:";
+
+	public static final String SIP_INVITE_SESSION = "CITY_VIDEO_VMP_SIP_INVITE_SESSION_INFO:";
+	public static final String SIP_INVITE_SESSION_CALL_ID = SIP_INVITE_SESSION + "CALL_ID:";
+	public static final String SIP_INVITE_SESSION_STREAM = SIP_INVITE_SESSION + "STREAM:";
+
+	public static final String MEDIA_STREAM_AUTHORITY = "CITY_VIDEO_VMP_MEDIA_STREAM_AUTHORITY:";
+
+	public static final String SIP_CSEQ_PREFIX = "CITY_VIDEO_VMP_SIP_CSEQ_";
+
+	public static final String SIP_SUBSCRIBE_PREFIX = "CITY_VIDEO_VMP_SIP_SUBSCRIBE_";
+
+	public static final String SYSTEM_INFO_CPU_PREFIX = "CITY_VIDEO_VMP_SYSTEM_INFO_CPU_";
+
+	public static final String SYSTEM_INFO_MEM_PREFIX = "CITY_VIDEO_VMP_SYSTEM_INFO_MEM_";
+
+	public static final String SYSTEM_INFO_NET_PREFIX = "CITY_VIDEO_VMP_SYSTEM_INFO_NET_";
+
+	public static final String SYSTEM_INFO_DISK_PREFIX = "CITY_VIDEO_VMP_SYSTEM_INFO_DISK_";
+	public static final String BROADCAST_WAITE_INVITE = "task_broadcast_waite_invite_";
+
+	public static final String REGISTER_EXPIRE_TASK_KEY_PREFIX = "CITY_VIDEO_VMP_device_register_expire_";
+	public static final String PUSH_STREAM_LIST = "CITY_VIDEO_VMP_PUSH_STREAM_LIST_";
+	public static final String WAITE_SEND_PUSH_STREAM = "CITY_VIDEO_VMP_WAITE_SEND_PUSH_STREAM:";
+	public static final String START_SEND_PUSH_STREAM = "CITY_VIDEO_VMP_START_SEND_PUSH_STREAM:";
+	public static final String SSE_TASK_KEY = "SSE_TASK_";
+
+
+
+
+	//************************** redis 消息*********************************
+
+	/**
+	 * 流变化的通知
+	 */
+	public static final String WVP_MSG_STREAM_CHANGE_PREFIX = "WVP_MSG_STREAM_CHANGE_";
+
+	/**
+	 * 接收推流设备的GPS变化通知
+	 */
+	public static final String CITY_VIDEO_VM_MSG_GPS = "CITY_VIDEO_VM_MSG_GPS";
+
+	/**
+	 * 接收推流设备的GPS变化通知
+	 */
+	public static final String CITY_VIDEO_VM_MSG_PUSH_STREAM_STATUS_CHANGE = "CITY_VIDEO_VM_MSG_PUSH_STREAM_STATUS_CHANGE";
+	/**
+	 * 接收推流设备列表更新变化通知
+	 */
+	public static final String CITY_VIDEO_VM_MSG_PUSH_STREAM_LIST_CHANGE = "CITY_VIDEO_VM_MSG_PUSH_STREAM_LIST_CHANGE";
+
+	/**
+	 * redis 消息通知设备推流到平台
+	 */
+	public static final String CITY_VIDEO_VM_MSG_STREAM_PUSH_REQUESTED = "CITY_VIDEO_VM_MSG_STREAM_PUSH_REQUESTED";
+
+	/**
+	 * redis 消息通知上级平台开始观看流
+	 */
+	public static final String CITY_VIDEO_VM_MSG_STREAM_START_PLAY_NOTIFY = "CITY_VIDEO_VM_MSG_STREAM_START_PLAY_NOTIFY";
+
+	/**
+	 * redis 消息通知上级平台停止观看流
+	 */
+	public static final String CITY_VIDEO_VM_MSG_STREAM_STOP_PLAY_NOTIFY = "CITY_VIDEO_VM_MSG_STREAM_STOP_PLAY_NOTIFY";
+
+	/**
+	 * redis 消息接收关闭一个推流
+	 */
+	public static final String CITY_VIDEO_VM_MSG_STREAM_PUSH_CLOSE_REQUESTED = "CITY_VIDEO_VM_MSG_STREAM_PUSH_CLOSE_REQUESTED";
+
+
+	/**
+	 * redis 消息通知平台通知设备推流结果
+	 */
+	public static final String CITY_VIDEO_VM_MSG_STREAM_PUSH_RESPONSE = "CITY_VIDEO_VM_MSG_STREAM_PUSH_RESPONSE";
+
+	/**
+	 * redis 通知平台关闭推流
+	 */
+	public static final String CITY_VIDEO_VM_MSG_STREAM_PUSH_CLOSE = "CITY_VIDEO_VM_MSG_STREAM_PUSH_CLOSE";
+
+	/**
+	 * redis 消息请求所有的在线通道
+	 */
+	public static final String CITY_VIDEO_VM_MSG_GET_ALL_ONLINE_REQUESTED = "CITY_VIDEO_VM_MSG_GET_ALL_ONLINE_REQUESTED";
+
+	/**
+	 * 移动位置订阅通知
+	 */
+	public static final String CITY_VIDEO_VM_MSG_SUBSCRIBE_MOBILE_POSITION = "mobileposition";
+
+	/**
+	 * 报警订阅的通知(收到报警向redis发出通知)
+	 */
+	public static final String CITY_VIDEO_VM_MSG_SUBSCRIBE_ALARM = "alarm";
+
+
+	/**
+	 * 报警通知的发送 (收到redis发出的通知,转发给其他平台)
+	 */
+	public static final String CITY_VIDEO_VM_MSG_SUBSCRIBE_ALARM_RECEIVE= "alarm_receive";
+
+	/**
+	 * 设备状态订阅的通知
+	 */
+	public static final String CITY_VIDEO_VM_MSG_SUBSCRIBE_DEVICE_STATUS = "device";
+
+
+	//**************************    第三方  ****************************************
+
+	public static final String WVP_STREAM_GB_ID_PREFIX = "memberNo_";
+	public static final String WVP_STREAM_GPS_MSG_PREFIX = "WVP_STREAM_GPS_MSG_";
+	public static final String WVP_OTHER_SEND_RTP_INFO = "CITY_VIDEO_VMP_OTHER_SEND_RTP_INFO_";
+	public static final String WVP_OTHER_SEND_PS_INFO = "CITY_VIDEO_VMP_OTHER_SEND_PS_INFO_";
+	public static final String WVP_OTHER_RECEIVE_RTP_INFO = "CITY_VIDEO_VMP_OTHER_RECEIVE_RTP_INFO_";
+	public static final String WVP_OTHER_RECEIVE_PS_INFO = "CITY_VIDEO_VMP_OTHER_RECEIVE_PS_INFO_";
+
+	/**
+	 * Redis Const
+	 * 设备录像信息结果前缀
+	 */
+	public static final String REDIS_RECORD_INFO_RES_PRE = "GB_RECORD_INFO_RES_";
+	/**
+	 * Redis Const
+	 * 设备录像信息结果前缀
+	 */
+	public static final String REDIS_RECORD_INFO_RES_COUNT_PRE = "GB_RECORD_INFO_RES_COUNT:";
+
+}

+ 21 - 0
video-service/src/main/java/com/ruoyi/wvp/common/enums/ChannelDataType.java

@@ -0,0 +1,21 @@
+package com.ruoyi.wvp.common.enums;
+
+/**
+ * 支持的通道数据类型
+ */
+
+public enum ChannelDataType {
+
+    GB28181(1,"国标28181"),
+    STREAM_PUSH(2,"推流设备"),
+    STREAM_PROXY(3,"拉流代理");
+
+    public final int value;
+
+    public final String desc;
+
+    ChannelDataType(Integer value, String desc) {
+        this.value = value;
+        this.desc = desc;
+    }
+}

+ 77 - 0
video-service/src/main/java/com/ruoyi/wvp/common/enums/DeviceControlType.java

@@ -0,0 +1,77 @@
+package com.ruoyi.wvp.common.enums;
+
+import org.dom4j.Element;
+import org.springframework.util.ObjectUtils;
+
+
+/**
+ * @author gaofuwang
+ * @date 2023/01/18/ 10:09:00
+ * @since 1.0
+ */
+public enum DeviceControlType {
+
+    /**
+     * 云台控制
+     * 上下左右,预置位,扫描,辅助功能,巡航
+     */
+    PTZ("PTZCmd","云台控制"),
+    /**
+     * 远程启动
+     */
+    TELE_BOOT("TeleBoot","远程启动"),
+    /**
+     * 录像控制
+     */
+    RECORD("RecordCmd","录像控制"),
+    /**
+     * 布防撤防
+     */
+    GUARD("GuardCmd","布防撤防"),
+    /**
+     * 告警控制
+     */
+    ALARM("AlarmCmd","告警控制"),
+    /**
+     * 强制关键帧
+     */
+    I_FRAME("IFameCmd","强制关键帧"),
+    /**
+     * 拉框放大
+     */
+    DRAG_ZOOM_IN("DragZoomIn","拉框放大"),
+    /**
+     * 拉框缩小
+     */
+    DRAG_ZOOM_OUT("DragZoomOut","拉框缩小"),
+    /**
+     * 看守位
+     */
+    HOME_POSITION("HomePosition","看守位");
+
+    private final String val;
+
+    private final String desc;
+
+    DeviceControlType(String val, String desc) {
+        this.val = val;
+        this.desc = desc;
+    }
+
+    public String getVal() {
+        return val;
+    }
+
+    public String getDesc() {
+        return desc;
+    }
+
+    public static DeviceControlType typeOf(Element rootElement) {
+        for (DeviceControlType item : DeviceControlType.values()) {
+            if (!ObjectUtils.isEmpty(rootElement.element(item.val)) || !ObjectUtils.isEmpty(rootElement.elements(item.val))) {
+                return item;
+            }
+        }
+        return null;
+    }
+}

+ 159 - 0
video-service/src/main/java/com/ruoyi/wvp/conf/DynamicTask.java

@@ -0,0 +1,159 @@
+package com.ruoyi.wvp.conf;
+
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.ObjectUtils;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
+import org.springframework.stereotype.Component;
+
+import javax.annotation.PostConstruct;
+import java.time.Instant;
+import java.util.Date;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * 动态定时任务
+ * @author lin
+ */
+@Slf4j
+@Component
+public class DynamicTask {
+
+    private ThreadPoolTaskScheduler threadPoolTaskScheduler;
+
+    private final Map<String, ScheduledFuture<?>> futureMap = new ConcurrentHashMap<>();
+    private final Map<String, Runnable> runnableMap = new ConcurrentHashMap<>();
+
+    @PostConstruct
+    public void DynamicTask() {
+        threadPoolTaskScheduler = new ThreadPoolTaskScheduler();
+        threadPoolTaskScheduler.setPoolSize(300);
+        threadPoolTaskScheduler.setWaitForTasksToCompleteOnShutdown(true);
+        threadPoolTaskScheduler.setAwaitTerminationSeconds(10);
+        threadPoolTaskScheduler.setThreadNamePrefix("dynamicTask-");
+        threadPoolTaskScheduler.initialize();
+    }
+
+    /**
+     * 循环执行的任务
+     * @param key 任务ID
+     * @param task 任务
+     * @param cycleForCatalog 间隔 毫秒
+     * @return
+     */
+    public void startCron(String key, Runnable task, int cycleForCatalog) {
+        if(ObjectUtils.isEmpty(key)) {
+            return;
+        }
+        ScheduledFuture<?> future = futureMap.get(key);
+        if (future != null) {
+            if (future.isCancelled()) {
+                log.debug("任务【{}】已存在但是关闭状态!!!", key);
+            } else {
+                log.debug("任务【{}】已存在且已启动!!!", key);
+                return;
+            }
+        }
+        // scheduleWithFixedDelay 必须等待上一个任务结束才开始计时period, cycleForCatalog表示执行的间隔
+
+        future = threadPoolTaskScheduler.scheduleAtFixedRate(task, new Date(System.currentTimeMillis() + cycleForCatalog), cycleForCatalog);
+        if (future != null){
+            futureMap.put(key, future);
+            runnableMap.put(key, task);
+            log.debug("任务【{}】启动成功!!!", key);
+        }else {
+            log.debug("任务【{}】启动失败!!!", key);
+        }
+    }
+
+    /**
+     * 延时任务
+     * @param key 任务ID
+     * @param task 任务
+     * @param delay 延时 /毫秒
+     * @return
+     */
+    public void startDelay(String key, Runnable task, int delay) {
+        if(ObjectUtils.isEmpty(key)) {
+            return;
+        }
+        stop(key);
+
+        // 获取执行的时刻
+        Instant startInstant = Instant.now().plusMillis(TimeUnit.MILLISECONDS.toMillis(delay));
+
+        ScheduledFuture future = futureMap.get(key);
+        if (future != null) {
+            if (future.isCancelled()) {
+                log.debug("任务【{}】已存在但是关闭状态!!!", key);
+            } else {
+                log.debug("任务【{}】已存在且已启动!!!", key);
+                return;
+            }
+        }
+        // scheduleWithFixedDelay 必须等待上一个任务结束才开始计时period, cycleForCatalog表示执行的间隔
+        future = threadPoolTaskScheduler.schedule(task, startInstant);
+        if (future != null){
+            futureMap.put(key, future);
+            runnableMap.put(key, task);
+            log.debug("任务【{}】启动成功!!!", key);
+        }else {
+            log.debug("任务【{}】启动失败!!!", key);
+        }
+    }
+
+    public boolean stop(String key) {
+        if(ObjectUtils.isEmpty(key)) {
+            return false;
+        }
+        boolean result = false;
+        if (!ObjectUtils.isEmpty(futureMap.get(key)) && !futureMap.get(key).isCancelled() && !futureMap.get(key).isDone()) {
+            result = futureMap.get(key).cancel(false);
+            futureMap.remove(key);
+            runnableMap.remove(key);
+        }
+        return result;
+    }
+
+    public boolean contains(String key) {
+        if(ObjectUtils.isEmpty(key)) {
+            return false;
+        }
+        return futureMap.get(key) != null;
+    }
+
+    public Set<String> getAllKeys() {
+        return futureMap.keySet();
+    }
+
+    public Runnable get(String key) {
+        if(ObjectUtils.isEmpty(key)) {
+            return null;
+        }
+        return runnableMap.get(key);
+    }
+
+    /**
+     * 每五分钟检查失效的任务,并移除
+     */
+    @Scheduled(cron="0 0/5 * * * ?")
+    public void execute(){
+        if (futureMap.size() > 0) {
+            for (String key : futureMap.keySet()) {
+                ScheduledFuture<?> future = futureMap.get(key);
+                if (future.isDone() || future.isCancelled()) {
+                    futureMap.remove(key);
+                    runnableMap.remove(key);
+                }
+            }
+        }
+    }
+
+    public boolean isAlive(String key) {
+        return futureMap.get(key) != null && !futureMap.get(key).isDone() && !futureMap.get(key).isCancelled();
+    }
+}

+ 291 - 0
video-service/src/main/java/com/ruoyi/wvp/conf/MediaConfig.java

@@ -0,0 +1,291 @@
+package com.ruoyi.wvp.conf;
+
+import com.ruoyi.wvp.media.bean.MediaServer;
+import com.ruoyi.wvp.utils.DateUtil;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.core.annotation.Order;
+import org.springframework.util.ObjectUtils;
+
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.util.regex.Pattern;
+
+@Slf4j
+@Configuration("mediaConfig")
+@Order(0)
+public class MediaConfig{
+
+    // 修改必须配置,不再支持自动获取
+    @Value("${media.id}")
+    private String id;
+
+    @Value("${media.ip}")
+    private String ip;
+
+    @Value("${media.wan_ip:}")
+    private String wanIp;
+
+    @Value("${media.hook-ip:127.0.0.1}")
+    private String hookIp;
+
+    @Value("${sip.domain}")
+    private String sipDomain;
+
+    @Value("${media.sdp-ip:${media.wan_ip:}}")
+    private String sdpIp;
+
+    @Value("${media.stream-ip:${media.wan_ip:}}")
+    private String streamIp;
+
+    @Value("${media.http-port:0}")
+    private Integer httpPort;
+
+    @Value("${media.flv-port:0}")
+    private Integer flvPort = 0;
+
+    @Value("${media.ws-flv-port:0}")
+    private Integer wsFlvPort = 0;
+
+    @Value("${media.http-ssl-port:0}")
+    private Integer httpSSlPort = 0;
+
+    @Value("${media.flv-ssl-port:0}")
+    private Integer flvSSlPort = 0;
+
+    @Value("${media.ws-flv-ssl-port:0}")
+    private Integer wsFlvSSlPort = 0;
+
+    @Value("${media.rtmp-port:0}")
+    private Integer rtmpPort = 0;
+
+    @Value("${media.rtmp-ssl-port:0}")
+    private Integer rtmpSSlPort = 0;
+
+    @Value("${media.rtp-proxy-port:0}")
+    private Integer rtpProxyPort = 0;
+
+    @Value("${media.rtsp-port:0}")
+    private Integer rtspPort = 0;
+
+    @Value("${media.rtsp-ssl-port:0}")
+    private Integer rtspSSLPort = 0;
+
+    @Value("${media.auto-config:true}")
+    private boolean autoConfig = true;
+
+    @Value("${media.secret}")
+    private String secret;
+
+    @Value("${media.rtp.enable}")
+    private boolean rtpEnable;
+
+    @Value("${media.rtp.port-range}")
+    private String rtpPortRange;
+
+    @Value("${media.rtp.send-port-range}")
+    private String rtpSendPortRange;
+
+    @Value("${media.record-assist-port:0}")
+    private Integer recordAssistPort = 0;
+
+    @Value("${media.record-day:7}")
+    private Integer recordDay;
+
+    @Value("${media.record-path:}")
+    private String recordPath;
+
+    @Value("${media.type:zlm}")
+    private String type;
+
+    public String getId() {
+        return id;
+    }
+
+    public String getIp() {
+        return ip;
+    }
+
+    public String getHookIp() {
+        return hookIp;
+    }
+
+    public int getHttpPort() {
+        return httpPort;
+    }
+
+    public int getHttpSSlPort() {
+        return httpSSlPort;
+    }
+
+    public int getRtmpPort() {
+        return rtmpPort;
+    }
+
+    public int getRtmpSSlPort() {
+        return rtmpSSlPort;
+    }
+
+    public int getRtpProxyPort() {
+        if (rtpProxyPort == null) {
+            return 0;
+        }else {
+            return rtpProxyPort;
+        }
+
+    }
+
+    public int getRtspPort() {
+        return rtspPort;
+    }
+
+    public int getRtspSSLPort() {
+        return rtspSSLPort;
+    }
+
+    public boolean isAutoConfig() {
+        return autoConfig;
+    }
+
+    public String getSecret() {
+        return secret;
+    }
+
+    public boolean isRtpEnable() {
+        return rtpEnable;
+    }
+
+    public String getRtpPortRange() {
+        return rtpPortRange;
+    }
+
+    public int getRecordAssistPort() {
+        return recordAssistPort;
+    }
+
+    public String getSdpIp() {
+        if (ObjectUtils.isEmpty(sdpIp)){
+            return ip;
+        }else {
+            if (isValidIPAddress(sdpIp)) {
+                return sdpIp;
+            }else {
+                // 按照域名解析
+                String hostAddress = null;
+                try {
+                    hostAddress = InetAddress.getByName(sdpIp).getHostAddress();
+                } catch (UnknownHostException e) {
+                    log.error("[获取SDP IP]: 域名解析失败");
+                }
+                return hostAddress;
+            }
+        }
+    }
+
+    public String getStreamIp() {
+        if (ObjectUtils.isEmpty(streamIp)){
+            return ip;
+        }else {
+            return streamIp;
+        }
+    }
+
+    public String getSipDomain() {
+        return sipDomain;
+    }
+
+    public MediaServer getMediaSerItem(){
+        MediaServer mediaServer = new MediaServer();
+        mediaServer.setId(id);
+        mediaServer.setIp(ip);
+        mediaServer.setDefaultServer(true);
+        mediaServer.setHookIp(getHookIp());
+        mediaServer.setSdpIp(getSdpIp());
+        mediaServer.setStreamIp(getStreamIp());
+        mediaServer.setHttpPort(httpPort);
+        if (flvPort == 0) {
+            mediaServer.setFlvPort(httpPort);
+        }else {
+            mediaServer.setFlvPort(flvPort);
+        }
+        if (wsFlvPort == 0) {
+            mediaServer.setWsFlvPort(httpPort);
+        }else {
+            mediaServer.setWsFlvPort(wsFlvPort);
+        }
+        if (flvSSlPort == 0) {
+            mediaServer.setFlvSSLPort(httpSSlPort);
+        }else {
+            mediaServer.setFlvSSLPort(flvSSlPort);
+        }
+        if (wsFlvSSlPort == 0) {
+            mediaServer.setWsFlvSSLPort(httpSSlPort);
+        }else {
+            mediaServer.setWsFlvSSLPort(wsFlvSSlPort);
+        }
+
+        mediaServer.setHttpSSlPort(httpSSlPort);
+        mediaServer.setRtmpPort(rtmpPort);
+        mediaServer.setRtmpSSlPort(rtmpSSlPort);
+        mediaServer.setRtpProxyPort(getRtpProxyPort());
+        mediaServer.setRtspPort(rtspPort);
+        mediaServer.setRtspSSLPort(rtspSSLPort);
+        mediaServer.setAutoConfig(autoConfig);
+        mediaServer.setSecret(secret);
+        mediaServer.setRtpEnable(rtpEnable);
+        mediaServer.setRtpPortRange(rtpPortRange);
+        mediaServer.setSendRtpPortRange(rtpSendPortRange);
+        mediaServer.setRecordAssistPort(recordAssistPort);
+        mediaServer.setHookAliveInterval(10f);
+        mediaServer.setRecordDay(recordDay);
+        mediaServer.setStatus(false);
+        mediaServer.setType(type);
+        if (recordPath != null) {
+            mediaServer.setRecordPath(recordPath);
+        }
+        mediaServer.setCreateTime(DateUtil.getNow());
+        mediaServer.setUpdateTime(DateUtil.getNow());
+
+        return mediaServer;
+    }
+
+    public Integer getRecordDay() {
+        return recordDay;
+    }
+
+    public void setRecordDay(Integer recordDay) {
+        this.recordDay = recordDay;
+    }
+
+    public String getRecordPath() {
+        return recordPath;
+    }
+
+    public void setRecordPath(String recordPath) {
+        this.recordPath = recordPath;
+    }
+
+    public String getRtpSendPortRange() {
+        return rtpSendPortRange;
+    }
+
+    public void setRtpSendPortRange(String rtpSendPortRange) {
+        this.rtpSendPortRange = rtpSendPortRange;
+    }
+
+    private boolean isValidIPAddress(String ipAddress) {
+        if ((ipAddress != null) && (!ipAddress.isEmpty())) {
+            return Pattern.matches("^([1-9]|[1-9]\\d|1\\d{2}|2[0-4]\\d|25[0-5])(\\.(\\d|[1-9]\\d|1\\d{2}|2[0-4]\\d|25[0-5])){3}$", ipAddress);
+        }
+        return false;
+    }
+
+    public String getWanIp() {
+        return wanIp;
+    }
+
+    public void setWanIp(String wanIp) {
+        this.wanIp = wanIp;
+    }
+}

+ 13 - 0
video-service/src/main/java/com/ruoyi/wvp/conf/MediaStatusTimerTask.java

@@ -0,0 +1,13 @@
+package com.ruoyi.wvp.conf;
+
+/**
+ * 定时向zlm同步媒体流状态
+ */
+public class MediaStatusTimerTask {
+
+
+//    @Scheduled(fixedRate = 2 * 1000)   //每3秒执行一次
+    public void execute(){
+
+    }
+}

+ 37 - 0
video-service/src/main/java/com/ruoyi/wvp/conf/ScheduleConfig.java

@@ -0,0 +1,37 @@
+package com.ruoyi.wvp.conf;
+
+import org.apache.commons.lang3.concurrent.BasicThreadFactory;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.scheduling.annotation.SchedulingConfigurer;
+import org.springframework.scheduling.config.ScheduledTaskRegistrar;
+
+import java.util.concurrent.ScheduledThreadPoolExecutor;
+import java.util.concurrent.ThreadPoolExecutor;
+
+import static com.ruoyi.wvp.conf.ThreadPoolTaskConfig.cpuNum;
+
+/**
+ * "@Scheduled"是Spring框架提供的一种定时任务执行机制,默认情况下它是单线程的,在同时执行多个定时任务时可能会出现阻塞和性能问题。
+ * 为了解决这种单线程瓶颈问题,可以将定时任务的执行机制改为支持多线程
+ */
+@Configuration
+public class ScheduleConfig implements SchedulingConfigurer {
+
+	/**
+	 * 核心线程数(默认线程数)
+	 */
+	private static final int corePoolSize = Math.max(cpuNum, 20);
+
+	/**
+	 * 线程池名前缀
+	 */
+	private static final String threadNamePrefix = "schedule";
+
+	@Override
+	public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
+		ScheduledThreadPoolExecutor scheduledThreadPoolExecutor = new ScheduledThreadPoolExecutor(corePoolSize,
+				new BasicThreadFactory.Builder().namingPattern(threadNamePrefix).daemon(true).build(),
+				new ThreadPoolExecutor.CallerRunsPolicy());
+		taskRegistrar.setScheduler(scheduledThreadPoolExecutor);
+	}
+}

+ 28 - 0
video-service/src/main/java/com/ruoyi/wvp/conf/ServiceInfo.java

@@ -0,0 +1,28 @@
+package com.ruoyi.wvp.conf;
+
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.boot.web.context.WebServerInitializedEvent;
+import org.springframework.context.ApplicationListener;
+import org.springframework.stereotype.Component;
+
+@Slf4j
+@Component
+public class ServiceInfo implements ApplicationListener<WebServerInitializedEvent> {
+
+    private static int serverPort;
+
+    public static int getServerPort() {
+        return serverPort;
+    }
+
+    @Override
+    public void onApplicationEvent(WebServerInitializedEvent event) {
+        // 项目启动获取启动的端口号
+        ServiceInfo.serverPort = event.getWebServer().getPort();
+        log.info("项目启动获取启动的端口号:  " + ServiceInfo.serverPort);
+    }
+
+    public void setServerPort(int serverPort) {
+        ServiceInfo.serverPort = serverPort;
+    }
+}

+ 34 - 0
video-service/src/main/java/com/ruoyi/wvp/conf/SipConfig.java

@@ -0,0 +1,34 @@
+package com.ruoyi.wvp.conf;
+
+
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.core.annotation.Order;
+import org.springframework.stereotype.Component;
+
+@Component
+@ConfigurationProperties(prefix = "sip", ignoreInvalidFields = true)
+@Order(0)
+@Data
+public class SipConfig {
+
+	private String ip;
+
+	private String showIp;
+
+	private Integer port;
+
+	private String domain;
+
+	private String id;
+
+	private String password;
+
+	Integer ptzSpeed = 50;
+
+	Integer registerTimeInterval = 120;
+
+	private boolean alarm = false;
+
+	private long timeout = 15;
+}

+ 41 - 0
video-service/src/main/java/com/ruoyi/wvp/conf/SystemInfoTimerTask.java

@@ -0,0 +1,41 @@
+package com.ruoyi.wvp.conf;
+
+import com.ruoyi.wvp.storager.IRedisCatchStorage;
+import com.ruoyi.wvp.utils.SystemInfoUtils;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Component;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 获取系统信息写入redis
+ */
+@Slf4j
+@Component
+public class SystemInfoTimerTask {
+
+    @Autowired
+    private IRedisCatchStorage redisCatchStorage;
+
+    @Scheduled(fixedRate = 2000)   //每1秒执行一次
+    public void execute(){
+        try {
+            double cpuInfo = SystemInfoUtils.getCpuInfo();
+            redisCatchStorage.addCpuInfo(cpuInfo);
+            double memInfo = SystemInfoUtils.getMemInfo();
+            redisCatchStorage.addMemInfo(memInfo);
+            Map<String, Double> networkInterfaces = SystemInfoUtils.getNetworkInterfaces();
+            redisCatchStorage.addNetInfo(networkInterfaces);
+            List<Map<String, Object>> diskInfo =SystemInfoUtils.getDiskInfo();
+            redisCatchStorage.addDiskInfo(diskInfo);
+        } catch (InterruptedException e) {
+            log.error("[获取系统信息失败] {}", e.getMessage());
+        }
+
+    }
+
+
+}

+ 67 - 0
video-service/src/main/java/com/ruoyi/wvp/conf/ThreadPoolTaskConfig.java

@@ -0,0 +1,67 @@
+package com.ruoyi.wvp.conf;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.core.annotation.Order;
+import org.springframework.scheduling.annotation.EnableAsync;
+import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
+
+import java.util.concurrent.ThreadPoolExecutor;
+
+/**
+ *  ThreadPoolTask 配置类
+ * @author lin
+ */
+@Configuration
+@Order(1)
+@EnableAsync(proxyTargetClass = true)
+public class ThreadPoolTaskConfig {
+
+    public static final int cpuNum = Runtime.getRuntime().availableProcessors();
+
+    /**
+     *   默认情况下,在创建了线程池后,线程池中的线程数为0,当有任务来之后,就会创建一个线程去执行任务,
+     *    当线程池中的线程数目达到corePoolSize后,就会把到达的任务放到缓存队列当中;
+     *  当队列满了,就继续创建线程,当线程数量大于等于maxPoolSize后,开始使用拒绝策略拒绝
+     */
+
+    /**
+     * 核心线程数(默认线程数)
+     */
+    private static final int corePoolSize = Math.max(cpuNum * 2, 16);
+    /**
+     * 最大线程数
+     */
+    private static final int maxPoolSize = corePoolSize * 10;
+    /**
+     * 允许线程空闲时间(单位:默认为秒)
+     */
+    private static final int keepAliveTime = 30;
+
+    /**
+     * 缓冲队列大小
+     */
+    private static final int queueCapacity = 10000;
+    /**
+     * 线程池名前缀
+     */
+    private static final String threadNamePrefix = "async-";
+
+
+    @Bean("taskExecutor") // bean的名称,默认为首字母小写的方法名
+    public ThreadPoolTaskExecutor taskExecutor() {
+        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
+        executor.setCorePoolSize(corePoolSize);
+        executor.setMaxPoolSize(maxPoolSize);
+        executor.setQueueCapacity(queueCapacity);
+        executor.setKeepAliveSeconds(keepAliveTime);
+        executor.setThreadNamePrefix(threadNamePrefix);
+
+        // 线程池对拒绝任务的处理策略
+        // CallerRunsPolicy:由调用线程(提交任务的线程)处理该任务
+        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
+        // 初始化
+        executor.initialize();
+        return executor;
+    }
+}

+ 183 - 0
video-service/src/main/java/com/ruoyi/wvp/conf/UserSetting.java

@@ -0,0 +1,183 @@
+package com.ruoyi.wvp.conf;
+
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.core.annotation.Order;
+import org.springframework.stereotype.Component;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * 配置文件 user-settings 映射的配置信息
+ */
+@Component
+@ConfigurationProperties(prefix = "user-settings", ignoreInvalidFields = true)
+@Order(0)
+@Data
+public class UserSetting {
+
+    /**
+     * 是否保存位置的历史记录(轨迹)
+     */
+    private Boolean savePositionHistory = Boolean.FALSE;
+
+    /**
+     * 是否开始自动点播: 请求流为未拉起的流时,自动开启点播, 需要rtp.enable=true
+     */
+    private Boolean autoApplyPlay = Boolean.FALSE;
+
+    /**
+     * [可选] 部分设备需要扩展SDP,需要打开此设置,一般设备无需打开
+     */
+    private Boolean seniorSdp = Boolean.FALSE;
+
+    /**
+     * 点播/录像回放 等待超时时间,单位:毫秒
+     */
+    private Integer playTimeout = 10000;
+
+    /**
+     * 上级点播等待超时时间,单位:毫秒
+     */
+    private int platformPlayTimeout = 20000;
+
+    /**
+     * 是否开启接口鉴权
+     */
+    private Boolean interfaceAuthentication = Boolean.TRUE;
+
+    /**
+     * 接口鉴权例外的接口, 即不进行接口鉴权的接口,尽量详细书写,尽量不用/**,至少两级目录
+     */
+    private List<String> interfaceAuthenticationExcludes = new ArrayList<>();
+
+    /**
+     * 推流直播是否录制
+     */
+    private Boolean recordPushLive = Boolean.TRUE;
+
+    /**
+     * 国标是否录制
+     */
+    private Boolean recordSip = Boolean.TRUE;
+
+    /**
+     * 使用推流状态作为推流通道状态
+     */
+    private Boolean usePushingAsStatus = Boolean.FALSE;
+
+    /**
+     * 使用来源请求ip作为streamIp,当且仅当你只有zlm节点它与wvp在一起的情况下开启
+     */
+    private Boolean useSourceIpAsStreamIp = Boolean.FALSE;
+
+    /**
+     * 是否使用设备来源Ip作为回复IP, 不设置则为 false
+     */
+    private Boolean sipUseSourceIpAsRemoteAddress = Boolean.FALSE;
+
+    /**
+     * 国标点播 按需拉流, true:有人观看拉流,无人观看释放, false:拉起后不自动释放
+     */
+    private Boolean streamOnDemand = Boolean.TRUE;
+
+    /**
+     * 推流鉴权, 默认开启
+     */
+    private Boolean pushAuthority = Boolean.TRUE;
+
+    /**
+     * 设备上线时是否自动同步通道
+     */
+    private Boolean syncChannelOnDeviceOnline = Boolean.FALSE;
+
+    /**
+     * 是否开启sip日志
+     */
+    private Boolean sipLog = Boolean.FALSE;
+
+    /**
+     * 是否开启mybatis-sql日志
+     */
+    private Boolean sqlLog = Boolean.FALSE;
+
+    /**
+     * 消息通道功能-缺少国标ID是否给所有上级发送消息
+     */
+    private Boolean sendToPlatformsWhenIdLost = Boolean.FALSE;
+
+    /**
+     * 保持通道状态,不接受notify通道状态变化, 兼容海康平台发送错误消息
+     */
+    private Boolean refuseChannelStatusChannelFormNotify = Boolean.FALSE;
+
+    /**
+     * 设备/通道状态变化时发送消息
+     */
+    private Boolean deviceStatusNotify = Boolean.TRUE;
+
+    /**
+     * 上级平台点播时不使用上级平台指定的ssrc,使用自定义的ssrc,参考国标文档-点播外域设备媒体流SSRC处理方式
+     */
+    private Boolean useCustomSsrcForParentInvite = Boolean.TRUE;
+
+    /**
+     * 开启接口文档页面。 默认开启,生产环境建议关闭,遇到swagger相关的漏洞时也可以关闭
+     */
+    private Boolean docEnable = Boolean.TRUE;
+
+    /**
+     * 服务ID,不写则为000000
+     */
+    private String serverId = "000000";
+
+
+    /**
+     * 国标级联语音喊话发流模式 * UDP:udp传输 TCP-ACTIVE:tcp主动模式 TCP-PASSIVE:tcp被动模式
+     */
+    private String broadcastForPlatform = "UDP";
+
+    /**
+     * 行政区划信息文件,系统启动时会加载到系统里
+     */
+    private String civilCodeFile = "classpath:civilCode.csv";
+
+    /**
+     * 跨域配置,不配置此项则允许所有跨域请求,配置后则只允许配置的页面的地址请求, 可以配置多个
+     */
+    private List<String> allowedOrigins = new ArrayList<>();
+
+    /**
+     * 设置notify缓存队列最大长度,超过此长度的数据将返回486 BUSY_HERE,消息丢弃, 默认100000
+     */
+    private int maxNotifyCountQueue = 100000;
+
+    /**
+     * 国标级联离线后多久重试一次注册
+     */
+    private int registerAgainAfterTime = 60;
+
+    /**
+     * 国标续订方式,true为续订,每次注册在同一个会话里,false为重新注册,每次使用新的会话
+     */
+    private boolean registerKeepIntDialog = false;
+
+    /**
+     *     # 国标设备离线后的上线策略,
+     *     # 0: 国标标准实现,设备离线后不回复心跳,直到设备重新注册上线,
+     *     # 1(默认): 对于离线设备,收到心跳就把设备设置为上线,并更新注册时间为上次这次心跳的时间。防止过期时间判断异常
+     */
+    private int gbDeviceOnline = 1;
+
+    /**
+     *    登录超时时间(分钟),
+     */
+    private long loginTimeout = 30;
+
+    /**
+     * jwk文件路径,若不指定则使用resources目录下的jwk.json
+     */
+    private String jwkFile = "classpath:jwk.json";
+
+}

+ 39 - 0
video-service/src/main/java/com/ruoyi/wvp/conf/VersionConfig.java

@@ -0,0 +1,39 @@
+package com.ruoyi.wvp.conf;
+
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.core.annotation.Order;
+import org.springframework.stereotype.Component;
+
+@Component
+@ConfigurationProperties(prefix = "version")
+@Order(0)
+public class VersionConfig {
+
+    private String version;
+    private String artifactId;
+    private String description;
+
+    public void setVersion(String version) {
+        this.version = version;
+    }
+
+    public void setArtifactId(String artifactId) {
+        this.artifactId = artifactId;
+    }
+
+    public void setDescription(String description) {
+        this.description = description;
+    }
+
+    public String getVersion() {
+        return version;
+    }
+
+    public String getArtifactId() {
+        return artifactId;
+    }
+
+    public String getDescription() {
+        return description;
+    }
+}

+ 27 - 0
video-service/src/main/java/com/ruoyi/wvp/conf/VersionInfo.java

@@ -0,0 +1,27 @@
+package com.ruoyi.wvp.conf;
+
+
+import com.ruoyi.wvp.common.VersionPo;
+import com.ruoyi.wvp.utils.GitUtil;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+@Component
+public class VersionInfo {
+
+    @Autowired
+    GitUtil gitUtil;
+
+    public VersionPo getVersion() {
+        VersionPo versionPo = new VersionPo();
+        versionPo.setGIT_Revision(gitUtil.getGitCommitId());
+        versionPo.setGIT_BRANCH(gitUtil.getBranch());
+        versionPo.setGIT_URL(gitUtil.getGitUrl());
+        versionPo.setBUILD_DATE(gitUtil.getBuildDate());
+        versionPo.setGIT_Revision_SHORT(gitUtil.getCommitIdShort());
+        versionPo.setVersion(gitUtil.getBuildVersion());
+        versionPo.setGIT_DATE(gitUtil.getCommitTime());
+
+        return versionPo;
+    }
+}

+ 29 - 0
video-service/src/main/java/com/ruoyi/wvp/conf/WVPTimerTask.java

@@ -0,0 +1,29 @@
+package com.ruoyi.wvp.conf;
+
+import com.alibaba.fastjson2.JSONObject;
+import com.ruoyi.wvp.storager.IRedisCatchStorage;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Component;
+
+@Component
+public class WVPTimerTask {
+
+    @Autowired
+    private IRedisCatchStorage redisCatchStorage;
+
+    @Value("${server.port}")
+    private int serverPort;
+
+    @Autowired
+    private SipConfig sipConfig;
+
+    @Scheduled(fixedDelay = 2 * 1000)   //每3秒执行一次
+    public void execute(){
+        JSONObject jsonObject = new JSONObject();
+        jsonObject.put("ip", sipConfig.getShowIp());
+        jsonObject.put("port", serverPort);
+        redisCatchStorage.updateWVPInfo(jsonObject, 3);
+    }
+}

+ 24 - 0
video-service/src/main/java/com/ruoyi/wvp/conf/redis/bean/RedisRpcMessage.java

@@ -0,0 +1,24 @@
+package com.ruoyi.wvp.conf.redis.bean;
+
+public class RedisRpcMessage {
+
+    private RedisRpcRequest request;
+
+    private RedisRpcResponse response;
+
+    public RedisRpcRequest getRequest() {
+        return request;
+    }
+
+    public void setRequest(RedisRpcRequest request) {
+        this.request = request;
+    }
+
+    public RedisRpcResponse getResponse() {
+        return response;
+    }
+
+    public void setResponse(RedisRpcResponse response) {
+        this.response = response;
+    }
+}

+ 93 - 0
video-service/src/main/java/com/ruoyi/wvp/conf/redis/bean/RedisRpcRequest.java

@@ -0,0 +1,93 @@
+package com.ruoyi.wvp.conf.redis.bean;
+
+/**
+ * 通过redis发送请求
+ */
+public class RedisRpcRequest {
+
+    /**
+     * 来自的WVP ID
+     */
+    private String fromId;
+
+
+    /**
+     * 目标的WVP ID
+     */
+    private String toId;
+
+    /**
+     * 序列号
+     */
+    private long sn;
+
+    /**
+     * 访问的路径
+     */
+    private String uri;
+
+    /**
+     * 参数
+     */
+    private Object param;
+
+    public String getFromId() {
+        return fromId;
+    }
+
+    public void setFromId(String fromId) {
+        this.fromId = fromId;
+    }
+
+    public String getToId() {
+        return toId;
+    }
+
+    public void setToId(String toId) {
+        this.toId = toId;
+    }
+
+    public String getUri() {
+        return uri;
+    }
+
+    public void setUri(String uri) {
+        this.uri = uri;
+    }
+
+    public Object getParam() {
+        return param;
+    }
+
+    public void setParam(Object param) {
+        this.param = param;
+    }
+
+    public long getSn() {
+        return sn;
+    }
+
+    public void setSn(long sn) {
+        this.sn = sn;
+    }
+
+    @Override
+    public String toString() {
+        return "RedisRpcRequest{" +
+                "uri='" + uri + '\'' +
+                ", fromId='" + fromId + '\'' +
+                ", toId='" + toId + '\'' +
+                ", sn=" + sn +
+                ", param=" + param +
+                '}';
+    }
+
+    public RedisRpcResponse getResponse() {
+        RedisRpcResponse response = new RedisRpcResponse();
+        response.setFromId(fromId);
+        response.setToId(toId);
+        response.setSn(sn);
+        response.setUri(uri);
+        return response;
+    }
+}

+ 99 - 0
video-service/src/main/java/com/ruoyi/wvp/conf/redis/bean/RedisRpcResponse.java

@@ -0,0 +1,99 @@
+package com.ruoyi.wvp.conf.redis.bean;
+
+/**
+ * 通过redis发送回复
+ */
+public class RedisRpcResponse {
+
+    /**
+     * 来自的WVP ID
+     */
+    private String fromId;
+
+
+    /**
+     * 目标的WVP ID
+     */
+    private String toId;
+
+
+    /**
+     * 序列号
+     */
+    private long sn;
+
+    /**
+     * 状态码
+     */
+    private int statusCode;
+
+    /**
+     * 访问的路径
+     */
+    private String uri;
+
+    /**
+     * 参数
+     */
+    private Object body;
+
+    public String getFromId() {
+        return fromId;
+    }
+
+    public void setFromId(String fromId) {
+        this.fromId = fromId;
+    }
+
+    public String getToId() {
+        return toId;
+    }
+
+    public void setToId(String toId) {
+        this.toId = toId;
+    }
+
+    public long getSn() {
+        return sn;
+    }
+
+    public void setSn(long sn) {
+        this.sn = sn;
+    }
+
+    public int getStatusCode() {
+        return statusCode;
+    }
+
+    public void setStatusCode(int statusCode) {
+        this.statusCode = statusCode;
+    }
+
+    public String getUri() {
+        return uri;
+    }
+
+    public void setUri(String uri) {
+        this.uri = uri;
+    }
+
+    public Object getBody() {
+        return body;
+    }
+
+    public void setBody(Object body) {
+        this.body = body;
+    }
+
+    @Override
+    public String toString() {
+        return "RedisRpcResponse{" +
+                "uri='" + uri + '\'' +
+                ", fromId='" + fromId + '\'' +
+                ", toId='" + toId + '\'' +
+                ", sn=" + sn +
+                ", statusCode=" + statusCode +
+                ", body=" + body +
+                '}';
+    }
+}

+ 244 - 0
video-service/src/main/java/com/ruoyi/wvp/controller/GBRecordController.java

@@ -0,0 +1,244 @@
+package com.ruoyi.wvp.controller;
+
+import com.zksy.common.core.controller.BaseController;
+import com.zksy.common.core.domain.AjaxResult;
+import com.ruoyi.wvp.common.StreamInfo;
+import com.ruoyi.wvp.conf.UserSetting;
+import com.zksy.common.exception.ControllerException;
+import com.zksy.common.exception.SsrcTransactionNotFoundException;
+import com.ruoyi.wvp.gb28181.bean.Device;
+import com.ruoyi.wvp.gb28181.bean.DeviceChannel;
+import com.ruoyi.wvp.gb28181.service.IDeviceChannelService;
+import com.ruoyi.wvp.gb28181.service.IDeviceService;
+import com.ruoyi.wvp.gb28181.service.IPlayService;
+import com.ruoyi.wvp.gb28181.transmit.callback.DeferredResultHolder;
+import com.ruoyi.wvp.gb28181.transmit.callback.RequestMessage;
+import com.ruoyi.wvp.gb28181.transmit.cmd.impl.SIPCommander;
+import com.ruoyi.wvp.media.bean.RecordInfo;
+import com.ruoyi.wvp.service.bean.InviteErrorCode;
+import com.ruoyi.wvp.utils.DateUtil;
+import com.zksy.common.enums.ErrorCode;
+import com.ruoyi.wvp.vmanager.bean.StreamContent;
+import com.ruoyi.wvp.vmanager.bean.WVPResult;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.context.request.async.DeferredResult;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.sip.InvalidArgumentException;
+import javax.sip.SipException;
+import java.text.ParseException;
+import java.util.UUID;
+
+/**
+ * 国标录像
+ */
+@Slf4j
+@RestController
+@RequestMapping("/api/gb_record")
+public class GBRecordController extends BaseController {
+
+	@Autowired
+	private SIPCommander cmder;
+
+	@Autowired
+	private DeferredResultHolder resultHolder;
+
+	@Autowired
+	private IPlayService playService;
+
+	@Autowired
+	private IDeviceChannelService channelService;
+
+	@Autowired
+	private IDeviceService deviceService;
+
+	@Autowired
+	private UserSetting userSetting;
+
+	/**
+	 * 录像信息查询
+	 *
+	 * @param deviceId 设备国标编号
+	 * @param channelId 通道国标编号
+	 * @param startTime 开始时间
+	 * @param endTime 结束时间
+	 * @return
+	 */
+
+	@GetMapping("/query")
+	public DeferredResult<WVPResult<RecordInfo>> recordinfo( String deviceId, String channelId, String startTime, String endTime, String type){
+
+		if (log.isDebugEnabled()) {
+			log.debug(String.format("录像信息查询 API调用,deviceId:%s ,startTime:%s, endTime:%s",deviceId, startTime, endTime));
+		}
+		DeferredResult<WVPResult<RecordInfo>> result = new DeferredResult<>();
+		if (!DateUtil.verification(startTime, DateUtil.formatter)){
+			throw new ControllerException(ErrorCode.ERROR100.getCode(), "startTime格式为" + DateUtil.PATTERN);
+		}
+		if (!DateUtil.verification(endTime, DateUtil.formatter)){
+			throw new ControllerException(ErrorCode.ERROR100.getCode(), "endTime格式为" + DateUtil.PATTERN);
+		}
+
+		Device device = deviceService.getDeviceByDeviceId(deviceId);
+		// 指定超时时间 1分钟30秒
+		String uuid = UUID.randomUUID().toString();
+		int sn  =  (int)((Math.random()*9+1)*100000);
+		String key = DeferredResultHolder.CALLBACK_CMD_RECORDINFO + deviceId + sn;
+		RequestMessage msg = new RequestMessage();
+		msg.setId(uuid);
+		msg.setKey(key);
+		try {
+			cmder.recordInfoQuery(device, channelId, startTime, endTime, sn, null, type, null, (eventResult -> {
+				WVPResult<RecordInfo> wvpResult = new WVPResult<>();
+				wvpResult.setCode(ErrorCode.ERROR100.getCode());
+				wvpResult.setMsg("查询录像失败, status: " +  eventResult.statusCode + ", message: " + eventResult.msg);
+				msg.setData(wvpResult);
+				resultHolder.invokeResult(msg);
+			}));
+		} catch (InvalidArgumentException | SipException | ParseException e) {
+			log.error("[命令发送失败] 查询录像: {}", e.getMessage());
+			throw new ControllerException(ErrorCode.ERROR100.getCode(), "命令发送失败: " +  e.getMessage());
+		}
+
+		// 录像查询以channelId作为deviceId查询
+		resultHolder.put(key, uuid, result);
+		result.onTimeout(()->{
+			msg.setData("timeout");
+			WVPResult<RecordInfo> wvpResult = new WVPResult<>();
+			wvpResult.setCode(ErrorCode.ERROR100.getCode());
+			wvpResult.setMsg("timeout");
+			msg.setData(wvpResult);
+			resultHolder.invokeResult(msg);
+		});
+        return result;
+	}
+
+
+	/**
+	 * 录像下载
+	 *
+	 * @param request
+	 * @param deviceId 设备国标编号
+	 * @param channelId 通道国标编号
+	 * @param startTime 开始时间
+	 * @param endTime 结束时间
+	 * @param downloadSpeed 下载倍速
+	 * @return
+	 */
+
+	@GetMapping("/download/start")
+	public DeferredResult<WVPResult<StreamContent>> download(HttpServletRequest request,  String deviceId, String channelId,
+															 String startTime, String endTime, String downloadSpeed) {
+
+		if (log.isDebugEnabled()) {
+			log.debug(String.format("历史媒体下载 API调用,deviceId:%s,channelId:%s,downloadSpeed:%s", deviceId, channelId, downloadSpeed));
+		}
+
+		String uuid = UUID.randomUUID().toString();
+		String key = DeferredResultHolder.CALLBACK_CMD_DOWNLOAD + deviceId + channelId;
+		DeferredResult<WVPResult<StreamContent>> result = new DeferredResult<>(30000L);
+		resultHolder.put(key, uuid, result);
+		RequestMessage requestMessage = new RequestMessage();
+		requestMessage.setId(uuid);
+		requestMessage.setKey(key);
+
+		Device device = deviceService.getDeviceByDeviceId(deviceId);
+		if (device == null) {
+			log.warn("[开始历史媒体下载] 未找到设备 deviceId: {},channelId:{}", deviceId, channelId);
+			throw new ControllerException(ErrorCode.ERROR100.getCode(), "未找到设备:" + deviceId);
+		}
+
+		DeviceChannel channel = channelService.getOne(deviceId, channelId);
+		if (channel == null) {
+			log.warn("[开始历史媒体下载] 未找到通道 deviceId: {},channelId:{}", deviceId, channelId);
+			throw new ControllerException(ErrorCode.ERROR100.getCode(), "未找到通道:" + channelId);
+		}
+		playService.download(device, channel, startTime, endTime, Integer.parseInt(downloadSpeed),
+		(code, msg, data)->{
+
+			WVPResult<StreamContent> wvpResult = new WVPResult<>();
+			if (code == InviteErrorCode.SUCCESS.getCode()) {
+				wvpResult.setCode(ErrorCode.SUCCESS.getCode());
+				wvpResult.setMsg(ErrorCode.SUCCESS.getMsg());
+
+				if (data != null) {
+					StreamInfo streamInfo = (StreamInfo)data;
+					if (userSetting.getUseSourceIpAsStreamIp()) {
+						streamInfo.channgeStreamIp(request.getLocalAddr());
+					}
+					wvpResult.setData(new StreamContent(streamInfo));
+				}
+			}else {
+				wvpResult.setCode(code);
+				wvpResult.setMsg(msg);
+			}
+			requestMessage.setData(wvpResult);
+			resultHolder.invokeResult(requestMessage);
+		});
+
+		return result;
+	}
+
+	/**
+	 * 停止录像下载
+	 *
+	 * @param deviceId 设备国标编号
+	 * @param channelId 通道国标编号
+	 * @param stream 流ID
+	 */
+	@GetMapping("/download/stop/{deviceId}/{channelId}/{stream}")
+	public void playStop(@PathVariable String deviceId, @PathVariable String channelId, @PathVariable String stream) {
+
+		if (log.isDebugEnabled()) {
+			log.debug(String.format("设备历史媒体下载停止 API调用,deviceId/channelId:%s_%s", deviceId, channelId));
+		}
+
+		if (deviceId == null || channelId == null) {
+			throw new ControllerException(ErrorCode.ERROR400);
+		}
+
+		Device device = deviceService.getDeviceByDeviceId(deviceId);
+		if (device == null) {
+			throw new ControllerException(ErrorCode.ERROR400.getCode(), "设备:" + deviceId + "未找到");
+		}
+
+		try {
+			cmder.streamByeCmd(device, channelId, "rtp", stream, null, null);
+		} catch (InvalidArgumentException | ParseException | SipException | SsrcTransactionNotFoundException e) {
+			log.warn("[停止历史媒体下载]停止历史媒体下载,发送BYE失败 {}", e.getMessage());
+		}
+	}
+
+	/**
+	 * 获取历史媒体下载进度
+	 *
+	 * @param deviceId 设备国标编号
+	 * @param channelId 通道国标编号
+	 * @param stream 流ID
+	 * @return
+	 */
+	@GetMapping("/download/progress/{deviceId}/{channelId}/{stream}")
+	public AjaxResult getProgress(@PathVariable String deviceId, @PathVariable String channelId, @PathVariable String stream) {
+		Device device = deviceService.getDeviceByDeviceId(deviceId);
+		if (device == null) {
+			log.warn("[获取历史媒体下载进度] 未找到设备 deviceId: {},channelId:{}", deviceId, channelId);
+			throw new ControllerException(ErrorCode.ERROR100.getCode(), "未找到设备:" + deviceId);
+		}
+
+		DeviceChannel channel = channelService.getOne(deviceId, channelId);
+		if (channel == null) {
+			log.warn("[获取历史媒体下载进度] 未找到通道 deviceId: {},channelId:{}", deviceId, channelId);
+			throw new ControllerException(ErrorCode.ERROR100.getCode(), "未找到通道:" + channelId);
+		}
+		StreamInfo downLoadInfo = playService.getDownLoadInfo(device, channel, stream);
+		if (downLoadInfo == null) {
+			throw new ControllerException(ErrorCode.ERROR404);
+		}
+		return success(new StreamContent(downLoadInfo));
+	}
+}

+ 225 - 0
video-service/src/main/java/com/ruoyi/wvp/controller/PlayController.java

@@ -0,0 +1,225 @@
+package com.ruoyi.wvp.controller;
+
+import com.alibaba.fastjson2.JSONArray;
+import com.alibaba.fastjson2.JSONObject;
+import com.zksy.common.core.controller.BaseController;
+import com.zksy.common.core.domain.AjaxResult;
+import com.ruoyi.wvp.common.InviteSessionType;
+import com.ruoyi.wvp.common.StreamInfo;
+import com.ruoyi.wvp.conf.UserSetting;
+import com.zksy.common.exception.ControllerException;
+import com.ruoyi.wvp.gb28181.bean.Device;
+import com.ruoyi.wvp.gb28181.bean.DeviceChannel;
+import com.ruoyi.wvp.gb28181.bean.SsrcTransaction;
+import com.ruoyi.wvp.gb28181.service.IDeviceChannelService;
+import com.ruoyi.wvp.gb28181.service.IDeviceService;
+import com.ruoyi.wvp.gb28181.service.IInviteStreamService;
+import com.ruoyi.wvp.gb28181.service.IPlayService;
+import com.ruoyi.wvp.gb28181.session.SipInviteSessionManager;
+import com.ruoyi.wvp.gb28181.transmit.callback.DeferredResultHolder;
+import com.ruoyi.wvp.gb28181.transmit.callback.RequestMessage;
+import com.ruoyi.wvp.media.bean.MediaServer;
+import com.ruoyi.wvp.media.service.IMediaServerService;
+import com.ruoyi.wvp.service.bean.InviteErrorCode;
+import com.ruoyi.wvp.utils.DateUtil;
+import com.zksy.common.enums.ErrorCode;
+import com.ruoyi.wvp.vmanager.bean.StreamContent;
+import com.ruoyi.wvp.vmanager.bean.WVPResult;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.util.Assert;
+import org.springframework.util.ObjectUtils;
+import org.springframework.web.bind.annotation.*;
+import org.springframework.web.context.request.async.DeferredResult;
+
+import javax.servlet.http.HttpServletRequest;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.List;
+import java.util.UUID;
+
+
+/**
+ * 国标设备点播
+ *
+ * @author lin
+ */
+@Slf4j
+@RestController
+@RequestMapping("/api/play")
+public class PlayController extends BaseController {
+
+    @Autowired
+    private SipInviteSessionManager sessionManager;
+
+    @Autowired
+    private IInviteStreamService inviteStreamService;
+
+    @Autowired
+    private DeferredResultHolder resultHolder;
+
+    @Autowired
+    private IPlayService playService;
+
+    @Autowired
+    private IMediaServerService mediaServerService;
+
+    @Autowired
+    private UserSetting userSetting;
+
+    @Autowired
+    private IDeviceService deviceService;
+
+    @Autowired
+    private IDeviceChannelService deviceChannelService;
+
+    /**
+     * 播放视频
+     *
+     * @param request
+     * @param deviceId 设备国标编号
+     * @param channelId 通道国标编号
+     * @return
+     */
+
+    @GetMapping("/start/{deviceId}/{channelId}")
+    public DeferredResult<WVPResult<StreamContent>> play(HttpServletRequest request, @PathVariable String deviceId, @PathVariable String channelId) {
+        log.info("[开始点播] deviceId:{}, channelId:{}, ", deviceId, channelId);
+        Assert.notNull(deviceId, "设备国标编号不可为NULL");
+        Assert.notNull(channelId, "通道国标编号不可为NULL");
+        // 获取可用的zlm
+        Device device = deviceService.getDeviceByDeviceId(deviceId);
+        Assert.notNull(device, "设备不存在");
+        DeviceChannel channel = deviceChannelService.getOne(deviceId, channelId);
+        Assert.notNull(channel, "通道不存在");
+        MediaServer newMediaServerItem = playService.getNewMediaServerItem(device);
+
+        RequestMessage requestMessage = new RequestMessage();
+        String key = DeferredResultHolder.CALLBACK_CMD_PLAY + deviceId + channelId;
+        requestMessage.setKey(key);
+        String uuid = UUID.randomUUID().toString();
+        requestMessage.setId(uuid);
+        DeferredResult<WVPResult<StreamContent>> result = new DeferredResult<>(userSetting.getPlayTimeout().longValue());
+
+        result.onTimeout(() -> {
+            log.info("[点播等待超时] deviceId:{}, channelId:{}, ", deviceId, channelId);
+            // 释放rtpserver
+            WVPResult<StreamInfo> wvpResult = new WVPResult<>();
+            wvpResult.setCode(ErrorCode.ERROR100.getCode());
+            wvpResult.setMsg("点播超时");
+            requestMessage.setData(wvpResult);
+            resultHolder.invokeAllResult(requestMessage);
+            inviteStreamService.removeInviteInfoByDeviceAndChannel(InviteSessionType.PLAY, channel.getId());
+            deviceChannelService.stopPlay(channel.getId());
+        });
+
+        // 录像查询以channelId作为deviceId查询
+        resultHolder.put(key, uuid, result);
+
+        playService.play(newMediaServerItem, deviceId, channelId, null, (code, msg, streamInfo) -> {
+            WVPResult<StreamContent> wvpResult = new WVPResult<>();
+            if (code == InviteErrorCode.SUCCESS.getCode()) {
+                wvpResult.setCode(ErrorCode.SUCCESS.getCode());
+                wvpResult.setMsg(ErrorCode.SUCCESS.getMsg());
+
+                if (streamInfo != null) {
+                    if (userSetting.getUseSourceIpAsStreamIp()) {
+                        streamInfo = streamInfo.clone();//深拷贝
+                        String host;
+                        try {
+                            URL url = new URL(request.getRequestURL().toString());
+                            host = url.getHost();
+                        } catch (MalformedURLException e) {
+                            host = request.getLocalAddr();
+                        }
+                        streamInfo.channgeStreamIp(host);
+                    }
+                    if (!ObjectUtils.isEmpty(newMediaServerItem.getTranscodeSuffix()) && !"null".equalsIgnoreCase(newMediaServerItem.getTranscodeSuffix())) {
+                        streamInfo.setStream(streamInfo.getStream() + "_" + newMediaServerItem.getTranscodeSuffix());
+                    }
+                    wvpResult.setData(new StreamContent(streamInfo));
+                } else {
+                    wvpResult.setCode(code);
+                    wvpResult.setMsg(msg);
+                }
+            } else {
+                wvpResult.setCode(code);
+                wvpResult.setMsg(msg);
+            }
+            requestMessage.setData(wvpResult);
+            // 此处必须释放所有请求
+            resultHolder.invokeAllResult(requestMessage);
+        });
+        return result;
+    }
+
+    /**
+     * 停止点播
+     *
+     * @param deviceId  设备国标编号
+     * @param channelId 通道国标编号
+     * @return
+     */
+
+    @GetMapping("/stop/{deviceId}/{channelId}")
+    public AjaxResult playStop(@PathVariable String deviceId, @PathVariable String channelId) {
+        log.debug(String.format("设备预览/回放停止API调用,streamId:%s_%s", deviceId, channelId));
+        if (deviceId == null || channelId == null) {
+            throw new ControllerException(ErrorCode.ERROR400);
+        }
+
+        Device device = deviceService.getDeviceByDeviceId(deviceId);
+        DeviceChannel channel = deviceChannelService.getOneForSource(deviceId, channelId);
+        Assert.notNull(device, "设备不存在");
+        Assert.notNull(channel, "通道不存在");
+        String streamId = String.format("%s_%s", device.getDeviceId(), channel.getDeviceId());
+        playService.stop(InviteSessionType.PLAY, device, channel, streamId);
+        return success();
+    }
+
+    /**
+     * 结束转码
+     *
+     * @param key 视频流key
+     * @param mediaServerId 流媒体服务ID
+     */
+    @PostMapping("/convertStop/{key}")
+    public void playConvertStop(@PathVariable String key, String mediaServerId) {
+        if (mediaServerId == null) {
+            throw new ControllerException(ErrorCode.ERROR400.getCode(), "流媒体:" + mediaServerId + "不存在");
+        }
+        MediaServer mediaInfo = mediaServerService.getOne(mediaServerId);
+        if (mediaInfo == null) {
+            throw new ControllerException(ErrorCode.ERROR100.getCode(), "使用的流媒体已经停止运行");
+        } else {
+            Boolean deleted = mediaServerService.delFFmpegSource(mediaInfo, key);
+            if (!deleted) {
+                throw new ControllerException(ErrorCode.ERROR100);
+            }
+        }
+    }
+
+    @GetMapping("/ssrc")
+    public JSONObject getSSRC() {
+        if (log.isDebugEnabled()) {
+            log.debug("获取所有的ssrc");
+        }
+        JSONArray objects = new JSONArray();
+        List<SsrcTransaction> allSsrc = sessionManager.getAll();
+        for (SsrcTransaction transaction : allSsrc) {
+            JSONObject jsonObject = new JSONObject();
+            jsonObject.put("deviceId", transaction.getDeviceId());
+            jsonObject.put("channelId", transaction.getChannelId());
+            jsonObject.put("ssrc", transaction.getSsrc());
+            jsonObject.put("streamId", transaction.getStream());
+            objects.add(jsonObject);
+        }
+
+        JSONObject jsonObject = new JSONObject();
+        jsonObject.put("data", objects);
+        jsonObject.put("count", objects.size());
+        return jsonObject;
+    }
+
+}
+

+ 259 - 0
video-service/src/main/java/com/ruoyi/wvp/controller/PlaybackController.java

@@ -0,0 +1,259 @@
+package com.ruoyi.wvp.controller;
+
+import com.zksy.common.core.controller.BaseController;
+import com.zksy.common.core.domain.AjaxResult;
+import com.zksy.common.exception.ServiceException;
+import com.ruoyi.wvp.common.InviteInfo;
+import com.ruoyi.wvp.common.InviteSessionType;
+import com.ruoyi.wvp.common.StreamInfo;
+import com.ruoyi.wvp.conf.UserSetting;
+import com.zksy.common.exception.ControllerException;
+import com.ruoyi.wvp.gb28181.bean.Device;
+import com.ruoyi.wvp.gb28181.bean.DeviceChannel;
+import com.ruoyi.wvp.gb28181.service.IDeviceChannelService;
+import com.ruoyi.wvp.gb28181.service.IDeviceService;
+import com.ruoyi.wvp.gb28181.service.IInviteStreamService;
+import com.ruoyi.wvp.gb28181.service.IPlayService;
+import com.ruoyi.wvp.gb28181.transmit.callback.DeferredResultHolder;
+import com.ruoyi.wvp.gb28181.transmit.callback.RequestMessage;
+import com.ruoyi.wvp.gb28181.transmit.cmd.impl.SIPCommander;
+import com.ruoyi.wvp.service.bean.InviteErrorCode;
+import com.zksy.common.enums.ErrorCode;
+import com.ruoyi.wvp.vmanager.bean.StreamContent;
+import com.ruoyi.wvp.vmanager.bean.WVPResult;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.util.ObjectUtils;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.context.request.async.DeferredResult;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.sip.InvalidArgumentException;
+import javax.sip.SipException;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.text.ParseException;
+import java.util.UUID;
+
+/**
+ * 视频回放
+ *
+ * @author lin
+ */
+@Slf4j
+@RestController
+@RequestMapping("/api/playback")
+public class PlaybackController extends BaseController {
+
+	@Autowired
+	private SIPCommander cmder;
+
+	@Autowired
+	private IInviteStreamService inviteStreamService;
+
+	@Autowired
+	private IPlayService playService;
+
+	@Autowired
+	private DeferredResultHolder resultHolder;
+
+	@Autowired
+	private UserSetting userSetting;
+
+	@Autowired
+	private IDeviceService deviceService;
+
+	@Autowired
+	private IDeviceChannelService channelService;
+
+	/**
+	 * 开始回放
+	 *
+	 * @param request
+	 * @param deviceId 设备国标编号
+	 * @param channelId 通道国标编号
+	 * @param startTime 开始时间
+	 * @param endTime 结束时间
+	 * @return
+	 */
+
+	@GetMapping("/start")
+	public DeferredResult<WVPResult<StreamContent>> start(HttpServletRequest request, String deviceId,  String channelId,
+														  String startTime, String endTime) {
+
+		if (log.isDebugEnabled()) {
+			log.debug(String.format("设备回放 API调用,deviceId:%s ,channelId:%s", deviceId, channelId));
+		}
+
+		String uuid = UUID.randomUUID().toString();
+		String key = DeferredResultHolder.CALLBACK_CMD_PLAYBACK + deviceId + channelId;
+		DeferredResult<WVPResult<StreamContent>> result = new DeferredResult<>(userSetting.getPlayTimeout().longValue());
+		resultHolder.put(key, uuid, result);
+
+		RequestMessage requestMessage = new RequestMessage();
+		requestMessage.setKey(key);
+		requestMessage.setId(uuid);
+		Device device = deviceService.getDeviceByDeviceId(deviceId);
+		if (device == null) {
+			log.warn("[录像回放] 未找到设备 deviceId: {},channelId:{}", deviceId, channelId);
+			throw new ControllerException(ErrorCode.ERROR100.getCode(), "未找到设备:" + deviceId);
+		}
+
+		DeviceChannel channel = channelService.getOne(deviceId, channelId);
+		if (channel == null) {
+			log.warn("[录像回放] 未找到通道 deviceId: {},channelId:{}", deviceId, channelId);
+			throw new ControllerException(ErrorCode.ERROR100.getCode(), "未找到通道:" + channelId);
+		}
+		playService.playBack(device, channel, startTime, endTime,
+				(code, msg, data)->{
+
+					WVPResult<StreamContent> wvpResult = new WVPResult<>();
+					if (code == InviteErrorCode.SUCCESS.getCode()) {
+						wvpResult.setCode(ErrorCode.SUCCESS.getCode());
+						wvpResult.setMsg(ErrorCode.SUCCESS.getMsg());
+
+						if (data != null) {
+							StreamInfo streamInfo = (StreamInfo)data;
+							if (userSetting.getUseSourceIpAsStreamIp()) {
+								streamInfo=streamInfo.clone();//深拷贝
+								String host;
+								try {
+									URL url=new URL(request.getRequestURL().toString());
+									host=url.getHost();
+								} catch (MalformedURLException e) {
+									host=request.getLocalAddr();
+								}
+								streamInfo.channgeStreamIp(host);
+							}
+							wvpResult.setData(new StreamContent(streamInfo));
+						}
+					}else {
+						wvpResult.setCode(code);
+						wvpResult.setMsg(msg);
+					}
+					requestMessage.setData(wvpResult);
+					resultHolder.invokeResult(requestMessage);
+				});
+
+		return result;
+	}
+
+
+	/**
+	 * 停止回放
+	 *
+	 * @param deviceId 设备国标编号
+	 * @param channelId 通道国标编号
+	 * @param stream 流ID
+	 */
+	@GetMapping("/stop/{deviceId}/{channelId}/{stream}")
+	public AjaxResult playStop(
+			@PathVariable String deviceId,
+			@PathVariable String channelId,
+			@PathVariable String stream) {
+		if (ObjectUtils.isEmpty(deviceId) || ObjectUtils.isEmpty(channelId) || ObjectUtils.isEmpty(stream)) {
+			throw new ControllerException(ErrorCode.ERROR400);
+		}
+		Device device = deviceService.getDeviceByDeviceId(deviceId);
+		if (device == null) {
+			throw new ControllerException(ErrorCode.ERROR400.getCode(), "设备:" + deviceId + " 未找到");
+		}
+		DeviceChannel deviceChannel = channelService.getOneForSource(deviceId, channelId);
+		if (deviceChannel == null) {
+			throw new ControllerException(ErrorCode.ERROR400.getCode(), "通道:" + deviceChannel + " 未找到");
+		}
+		playService.stop(InviteSessionType.PLAYBACK, device, deviceChannel, stream);
+		return success();
+	}
+
+
+	/**
+	 * 暂停回放
+	 *
+	 * @param streamId 回放流ID
+	 */
+	@GetMapping("/pause/{streamId}")
+	public void playPause(@PathVariable String streamId) {
+		log.info("playPause: "+streamId);
+
+		try {
+			playService.pauseRtp(streamId);
+		} catch (ServiceException e) {
+			throw new ControllerException(ErrorCode.ERROR400.getCode(), e.getMessage());
+		} catch (InvalidArgumentException | ParseException | SipException e) {
+			throw new ControllerException(ErrorCode.ERROR100.getCode(), e.getMessage());
+		}
+	}
+
+	/**
+	 * 恢复回放
+	 *
+	 * @param streamId 回放流ID
+	 */
+	@GetMapping("/resume/{streamId}")
+	public void playResume(@PathVariable String streamId) {
+		log.info("playResume: "+streamId);
+		try {
+			playService.resumeRtp(streamId);
+		} catch (ServiceException e) {
+			throw new ControllerException(ErrorCode.ERROR400.getCode(), e.getMessage());
+		} catch (InvalidArgumentException | ParseException | SipException e) {
+			throw new ControllerException(ErrorCode.ERROR100.getCode(), e.getMessage());
+		}
+	}
+
+	/**
+	 * 拖动回放
+	 *
+	 * @param streamId 回放流ID
+	 * @param seekTime 拖动偏移量,单位s
+	 */
+	@GetMapping("/seek/{streamId}/{seekTime}")
+	public void playSeek(@PathVariable String streamId, @PathVariable long seekTime) {
+		log.info("playSeek: "+streamId+", "+seekTime);
+		InviteInfo inviteInfo = inviteStreamService.getInviteInfoByStream(InviteSessionType.PLAYBACK, streamId);
+
+		if (null == inviteInfo || inviteInfo.getStreamInfo() == null) {
+			log.warn("streamId不存在!");
+			throw new ControllerException(ErrorCode.ERROR400.getCode(), "streamId不存在");
+		}
+		Device device = deviceService.getDeviceByDeviceId(inviteInfo.getDeviceId());
+		DeviceChannel channel = channelService.getOneById(inviteInfo.getChannelId());
+		try {
+			cmder.playSeekCmd(device, channel, inviteInfo.getStreamInfo(), seekTime);
+		} catch (InvalidArgumentException | ParseException | SipException e) {
+			throw new ControllerException(ErrorCode.ERROR100.getCode(), e.getMessage());
+		}
+	}
+
+	/**
+	 * 回放倍速
+	 *
+	 * @param streamId 回放流ID
+	 * @param speed 倍速0.25 0.5 1、2、4
+	 */
+	@GetMapping("/speed/{streamId}/{speed}")
+	public void playSpeed(@PathVariable String streamId, @PathVariable Double speed) {
+		log.info("playSpeed: "+streamId+", "+speed);
+		InviteInfo inviteInfo = inviteStreamService.getInviteInfoByStream(InviteSessionType.PLAYBACK, streamId);
+
+		if (null == inviteInfo || inviteInfo.getStreamInfo() == null) {
+			log.warn("streamId不存在!");
+			throw new ControllerException(ErrorCode.ERROR400.getCode(), "streamId不存在");
+		}
+		if(speed != 0.25 && speed != 0.5 && speed != 1 && speed != 2.0 && speed != 4.0) {
+			log.warn("不支持的speed: " + speed);
+			throw new ControllerException(ErrorCode.ERROR100.getCode(), "不支持的speed(0.25 0.5 1、2、4)");
+		}
+		Device device = deviceService.getDeviceByDeviceId(inviteInfo.getDeviceId());
+		DeviceChannel channel = channelService.getOneById(inviteInfo.getChannelId());
+		try {
+			cmder.playSpeedCmd(device, channel, inviteInfo.getStreamInfo(), speed);
+		} catch (InvalidArgumentException | ParseException | SipException e) {
+			throw new ControllerException(ErrorCode.ERROR100.getCode(), e.getMessage());
+		}
+	}
+}

+ 566 - 0
video-service/src/main/java/com/ruoyi/wvp/controller/PtzController.java

@@ -0,0 +1,566 @@
+package com.ruoyi.wvp.controller;
+
+import com.zksy.common.core.controller.BaseController;
+import com.zksy.common.exception.ControllerException;
+import com.ruoyi.wvp.gb28181.bean.Device;
+import com.ruoyi.wvp.gb28181.service.IDeviceService;
+import com.ruoyi.wvp.gb28181.service.IPTZService;
+import com.ruoyi.wvp.gb28181.transmit.callback.DeferredResultHolder;
+import com.ruoyi.wvp.gb28181.transmit.callback.RequestMessage;
+import com.ruoyi.wvp.gb28181.transmit.cmd.impl.SIPCommander;
+import com.zksy.common.enums.ErrorCode;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.util.Assert;
+import org.springframework.util.ObjectUtils;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.context.request.async.DeferredResult;
+
+import javax.sip.InvalidArgumentException;
+import javax.sip.SipException;
+import java.text.ParseException;
+import java.util.UUID;
+
+/**
+ * 前端设备控制
+ */
+@Slf4j
+@RestController
+@RequestMapping("/api/front-end")
+public class PtzController extends BaseController {
+
+	@Autowired
+	private SIPCommander cmder;
+
+	@Autowired
+	private IDeviceService deviceService;
+
+	@Autowired
+	private IPTZService ptzService;
+
+	@Autowired
+	private DeferredResultHolder resultHolder;
+
+	/**
+	 * 云台控制
+	 *
+	 * @param deviceId 设备国标编号
+	 * @param channelId 通道国标编号
+	 * @param cmdCode 指令码(对应国标文档指令格式中的字节4)
+	 * @param parameter1 数据一(对应国标文档指令格式中的字节5, 范围0-255)
+	 * @param parameter2 数据二(对应国标文档指令格式中的字节6, 范围0-255)
+	 * @param combindCode2 组合码二(对应国标文档指令格式中的字节7, 范围0-15)
+	 */
+	@GetMapping("/common/{deviceId}/{channelId}")
+	public void frontEndCommand(@PathVariable String deviceId,@PathVariable String channelId,Integer cmdCode, Integer parameter1, Integer parameter2, Integer combindCode2){
+
+		if (log.isDebugEnabled()) {
+			log.debug(String.format("设备云台控制 API调用,deviceId:%s ,channelId:%s ,cmdCode:%d parameter1:%d parameter2:%d",deviceId, channelId, cmdCode, parameter1, parameter2));
+		}
+
+		if (parameter1 == null || parameter1 < 0 || parameter1 > 255) {
+			throw new ControllerException(ErrorCode.ERROR100.getCode(), "parameter1 为 0-255的数字");
+		}
+		if (parameter2 == null || parameter2 < 0 || parameter2 > 255) {
+			throw new ControllerException(ErrorCode.ERROR100.getCode(), "parameter2 为 0-255的数字");
+		}
+		if (combindCode2 == null || combindCode2 < 0 || combindCode2 > 15) {
+			throw new ControllerException(ErrorCode.ERROR100.getCode(), "combindCode2 为 0-15的数字");
+		}
+
+		Device device = deviceService.getDeviceByDeviceId(deviceId);
+
+		Assert.notNull(device, "设备[" + deviceId + "]不存在");
+
+		ptzService.frontEndCommand(device, channelId, cmdCode, parameter1, parameter2, combindCode2);
+	}
+
+	/**
+	 * 云台控制
+	 *
+	 * @param deviceId 设备国标编号
+	 * @param channelId 通道国标编号
+	 * @param command 控制指令,允许值: left, right, up, down, upleft, upright, downleft, downright, zoomin, zoomout, stop
+	 * @param horizonSpeed 水平速度(0-255)
+	 * @param verticalSpeed 垂直速度(0-255)
+	 * @param zoomSpeed 缩放速度(0-15)
+	 */
+	@GetMapping("/ptz/{deviceId}/{channelId}")
+	public void ptz(@PathVariable String deviceId,@PathVariable String channelId, String command, Integer horizonSpeed, Integer verticalSpeed, Integer zoomSpeed){
+
+		if (log.isDebugEnabled()) {
+			log.debug(String.format("设备云台控制 API调用,deviceId:%s ,channelId:%s ,command:%s ,horizonSpeed:%d ,verticalSpeed:%d ,zoomSpeed:%d",deviceId, channelId, command, horizonSpeed, verticalSpeed, zoomSpeed));
+		}
+		if (horizonSpeed == null) {
+			horizonSpeed = 100;
+		}else if (horizonSpeed < 0 || horizonSpeed > 255) {
+			throw new ControllerException(ErrorCode.ERROR100.getCode(), "horizonSpeed 为 0-255的数字");
+		}
+		if (verticalSpeed == null) {
+			verticalSpeed = 100;
+		}else if (verticalSpeed < 0 || verticalSpeed > 255) {
+			throw new ControllerException(ErrorCode.ERROR100.getCode(), "verticalSpeed 为 0-255的数字");
+		}
+		if (zoomSpeed == null) {
+			zoomSpeed = 16;
+		}else if (zoomSpeed < 0 || zoomSpeed > 15) {
+			throw new ControllerException(ErrorCode.ERROR100.getCode(), "zoomSpeed 为 0-15的数字");
+		}
+
+		int cmdCode = 0;
+		switch (command){
+			case "left":
+				cmdCode = 2;
+				break;
+			case "right":
+				cmdCode = 1;
+				break;
+			case "up":
+				cmdCode = 8;
+				break;
+			case "down":
+				cmdCode = 4;
+				break;
+			case "upleft":
+				cmdCode = 10;
+				break;
+			case "upright":
+				cmdCode = 9;
+				break;
+			case "downleft":
+				cmdCode = 6;
+				break;
+			case "downright":
+				cmdCode = 5;
+				break;
+			case "zoomin":
+				cmdCode = 16;
+				break;
+			case "zoomout":
+				cmdCode = 32;
+				break;
+			case "stop":
+				horizonSpeed = 0;
+				verticalSpeed = 0;
+				zoomSpeed = 0;
+				break;
+			default:
+				break;
+		}
+		frontEndCommand(deviceId, channelId, cmdCode, horizonSpeed, verticalSpeed, zoomSpeed);
+	}
+
+	/**
+	 * 设备光圈控制
+	 *
+	 * @param deviceId 设备国标编号
+	 * @param channelId 通道国标编号
+	 * @param command 控制指令,允许值: in, out, stop
+	 * @param speed 光圈速度(0-255)
+	 */
+	@GetMapping("/fi/iris/{deviceId}/{channelId}")
+	public void iris(@PathVariable String deviceId,@PathVariable String channelId, String command, Integer speed){
+
+		if (log.isDebugEnabled()) {
+			log.debug("设备光圈控制 API调用,deviceId:{} ,channelId:{} ,command:{} ,speed:{} ",deviceId, channelId, command, speed);
+		}
+
+		if (speed == null) {
+			speed = 100;
+		}else if (speed < 0 || speed > 255) {
+			throw new ControllerException(ErrorCode.ERROR100.getCode(), "speed 为 0-255的数字");
+		}
+
+		int cmdCode = 0x40;
+		switch (command){
+			case "in":
+				cmdCode = 0x44;
+				break;
+			case "out":
+				cmdCode = 0x48;
+				break;
+			case "stop":
+				speed = 0;
+				break;
+			default:
+				break;
+		}
+		frontEndCommand(deviceId, channelId, cmdCode, 0, speed, 0);
+	}
+
+	/**
+	 * 设备聚焦控制
+	 *
+	 * @param deviceId 设备国标编号
+	 * @param channelId 通道国标编号
+	 * @param command 控制指令,允许值: near, far, stop
+	 * @param speed 聚焦速度(0-255)
+	 */
+	@GetMapping("/fi/focus/{deviceId}/{channelId}")
+	public void focus(@PathVariable String deviceId,@PathVariable String channelId, String command, Integer speed){
+
+		if (log.isDebugEnabled()) {
+			log.debug("设备聚焦控制 API调用,deviceId:{} ,channelId:{} ,command:{} ,speed:{} ",deviceId, channelId, command, speed);
+		}
+
+		if (speed == null) {
+			speed = 100;
+		}else if (speed < 0 || speed > 255) {
+			throw new ControllerException(ErrorCode.ERROR100.getCode(), "speed 为 0-255的数字");
+		}
+
+		int cmdCode = 0x40;
+		switch (command){
+			case "near":
+				cmdCode = 0x42;
+				break;
+			case "far":
+				cmdCode = 0x41;
+				break;
+			case "stop":
+				speed = 0;
+				break;
+			default:
+				break;
+		}
+		frontEndCommand(deviceId, channelId, cmdCode, speed, 0, 0);
+	}
+
+	/**
+	 * 设备变倍控制
+	 *
+	 * @param deviceId 设备国标编号
+	 * @param channelId 通道国标编号
+	 * @return
+	 */
+	@GetMapping("/preset/query/{deviceId}/{channelId}")
+	public DeferredResult<String> queryPreset(@PathVariable String deviceId, @PathVariable String channelId) {
+		if (log.isDebugEnabled()) {
+			log.debug("设备预置位查询API调用");
+		}
+		Device device = deviceService.getDeviceByDeviceId(deviceId);
+		String uuid =  UUID.randomUUID().toString();
+		String key =  DeferredResultHolder.CALLBACK_CMD_PRESETQUERY + (ObjectUtils.isEmpty(channelId) ? deviceId : channelId);
+		DeferredResult<String> result = new DeferredResult<String> (3 * 1000L);
+		result.onTimeout(()->{
+			log.warn(String.format("获取设备预置位超时"));
+			// 释放rtpserver
+			RequestMessage msg = new RequestMessage();
+			msg.setId(uuid);
+			msg.setKey(key);
+			msg.setData("获取设备预置位超时");
+			resultHolder.invokeResult(msg);
+		});
+		if (resultHolder.exist(key, null)) {
+			return result;
+		}
+		resultHolder.put(key, uuid, result);
+		try {
+			cmder.presetQuery(device, channelId, event -> {
+				RequestMessage msg = new RequestMessage();
+				msg.setId(uuid);
+				msg.setKey(key);
+				msg.setData(String.format("获取设备预置位失败,错误码: %s, %s", event.statusCode, event.msg));
+				resultHolder.invokeResult(msg);
+			});
+		} catch (InvalidArgumentException | SipException | ParseException e) {
+			log.error("[命令发送失败] 获取设备预置位: {}", e.getMessage());
+			throw new ControllerException(ErrorCode.ERROR100.getCode(), "命令发送失败: " + e.getMessage());
+		}
+		return result;
+	}
+
+	/**
+	 * 设备预置位添加
+	 *
+	 * @param deviceId 设备国标编号
+	 * @param channelId 通道国标编号
+	 * @param presetId 预置位编号(1-255)
+	 */
+	@GetMapping("/preset/add/{deviceId}/{channelId}")
+	public void addPreset(@PathVariable String deviceId, @PathVariable String channelId, Integer presetId) {
+		if (presetId == null || presetId < 1 || presetId > 255) {
+			throw new ControllerException(ErrorCode.ERROR100.getCode(), "预置位编号必须为1-255之间的数字");
+		}
+		frontEndCommand(deviceId, channelId, 0x81, 1, presetId, 0);
+	}
+
+	/**
+	 * 设备预置位调用
+	 *
+	 * @param deviceId 设备国标编号
+	 * @param channelId 通道国标编号
+	 * @param presetId 预置位编号(1-255)
+	 */
+	@GetMapping("/preset/call/{deviceId}/{channelId}")
+	public void callPreset(@PathVariable String deviceId, @PathVariable String channelId, Integer presetId) {
+		if (presetId == null || presetId < 1 || presetId > 255) {
+			throw new ControllerException(ErrorCode.ERROR100.getCode(), "预置位编号必须为1-255之间的数字");
+		}
+		frontEndCommand(deviceId, channelId, 0x82, 1, presetId, 0);
+	}
+
+	/**
+	 * 设备预置位删除
+	 *
+	 * @param deviceId 设备国标编号
+	 * @param channelId 通道国标编号
+	 * @param presetId 预置位编号(1-255)
+	 */
+	@GetMapping("/preset/delete/{deviceId}/{channelId}")
+	public void deletePreset(@PathVariable String deviceId, @PathVariable String channelId, Integer presetId) {
+		if (presetId == null || presetId < 1 || presetId > 255) {
+			throw new ControllerException(ErrorCode.ERROR100.getCode(), "预置位编号必须为1-255之间的数字");
+		}
+		frontEndCommand(deviceId, channelId, 0x83, 1, presetId, 0);
+	}
+
+	/**
+	 * 设备巡航点添加
+	 *
+	 * @param deviceId 设备国标编号
+	 * @param channelId 通道国标编号
+	 * @param cruiseId 巡航组号(0-255)
+	 * @param presetId 预置位编号(1-255)
+	 */
+	@GetMapping("/cruise/point/add/{deviceId}/{channelId}")
+	public void addCruisePoint(@PathVariable String deviceId, @PathVariable String channelId, Integer cruiseId, Integer presetId) {
+		if (presetId == null || cruiseId == null || presetId < 1 || presetId > 255 || cruiseId < 0 || cruiseId > 255) {
+			throw new ControllerException(ErrorCode.ERROR100.getCode(), "编号必须为1-255之间的数字");
+		}
+		frontEndCommand(deviceId, channelId, 0x84, cruiseId, presetId, 0);
+	}
+
+	/**
+	 * 设备巡航点删除
+	 *
+	 * @param deviceId 设备国标编号
+	 * @param channelId 通道国标编号
+	 * @param cruiseId 巡航组号(1-255)
+	 * @param presetId 预置位编号(0-255, 为0时删除整个巡航)
+	 */
+	@GetMapping("/cruise/point/delete/{deviceId}/{channelId}")
+	public void deleteCruisePoint(@PathVariable String deviceId, @PathVariable String channelId, Integer cruiseId, Integer presetId) {
+		if (presetId == null || presetId < 0 || presetId > 255) {
+			throw new ControllerException(ErrorCode.ERROR100.getCode(), "预置位编号必须为0-255之间的数字, 为0时删除整个巡航");
+		}
+		if (cruiseId == null || cruiseId < 0 || cruiseId > 255) {
+			throw new ControllerException(ErrorCode.ERROR100.getCode(), "巡航组号必须为0-255之间的数字");
+		}
+		frontEndCommand(deviceId, channelId, 0x85, cruiseId, presetId, 0);
+	}
+
+	/**
+	 * 设备巡航速度设置
+	 *
+	 * @param deviceId 设备国标编号
+	 * @param channelId 通道国标编号
+	 * @param cruiseId 巡航组号(0-255)
+	 * @param speed 巡航速度(1-4095)
+	 */
+	@GetMapping("/cruise/speed/{deviceId}/{channelId}")
+	public void setCruiseSpeed(@PathVariable String deviceId, @PathVariable String channelId, Integer cruiseId, Integer speed) {
+		if (cruiseId == null || cruiseId < 0 || cruiseId > 255) {
+			throw new ControllerException(ErrorCode.ERROR100.getCode(), "巡航组号必须为0-255之间的数字");
+		}
+		if (speed == null || speed < 1 || speed > 4095) {
+			throw new ControllerException(ErrorCode.ERROR100.getCode(), "巡航速度必须为1-4095之间的数字");
+		}
+		int parameter2 = speed & 0xFF;
+		int combindCode2 =  speed >> 8;
+		frontEndCommand(deviceId, channelId, 0x86, cruiseId, parameter2, combindCode2);
+	}
+
+	/**
+	 * 设备巡航停留时间设置
+	 *
+	 * @param deviceId 设备国标编号
+	 * @param channelId 通道国标编号
+	 * @param cruiseId 巡航组号
+	 * @param time 巡航停留时间(1-4095)
+	 */
+	@GetMapping("/cruise/time/{deviceId}/{channelId}")
+	public void setCruiseTime(@PathVariable String deviceId, @PathVariable String channelId, Integer cruiseId, Integer time) {
+		if (cruiseId == null || cruiseId < 0 || cruiseId > 255) {
+			throw new ControllerException(ErrorCode.ERROR100.getCode(), "巡航组号必须为0-255之间的数字");
+		}
+		if (time == null || time < 1 || time > 4095) {
+			throw new ControllerException(ErrorCode.ERROR100.getCode(), "巡航停留时间必须为1-4095之间的数字");
+		}
+		int parameter2 = time & 0xFF;
+		int combindCode2 =  time >> 8;
+		frontEndCommand(deviceId, channelId, 0x87, cruiseId, parameter2, combindCode2);
+	}
+
+	/**
+	 * 设备巡航启动
+	 *
+	 * @param deviceId 设备国标编号
+	 * @param channelId 通道国标编号
+	 * @param cruiseId 巡航组号
+	 */
+	@GetMapping("/cruise/start/{deviceId}/{channelId}")
+	public void startCruise(@PathVariable String deviceId, @PathVariable String channelId, Integer cruiseId) {
+		if (cruiseId == null || cruiseId < 0 || cruiseId > 255) {
+			throw new ControllerException(ErrorCode.ERROR100.getCode(), "巡航组号必须为0-255之间的数字");
+		}
+		frontEndCommand(deviceId, channelId, 0x88, cruiseId, 0, 0);
+	}
+
+	/**
+	 * 设备巡航停止
+	 *
+	 * @param deviceId 设备国标编号
+	 * @param channelId 通道国标编号
+	 * @param cruiseId 巡航组号
+	 */
+	@GetMapping("/cruise/stop/{deviceId}/{channelId}")
+	public void stopCruise(@PathVariable String deviceId, @PathVariable String channelId, Integer cruiseId) {
+		if (cruiseId == null || cruiseId < 0 || cruiseId > 255) {
+			throw new ControllerException(ErrorCode.ERROR100.getCode(), "巡航组号必须为0-255之间的数字");
+		}
+		frontEndCommand(deviceId, channelId, 0, 0, 0, 0);
+	}
+
+	/**
+	 * 设备扫描启动
+	 *
+	 * @param deviceId 设备国标编号
+	 * @param channelId 通道国标编号
+	 * @param scanId 扫描组号(0-255)
+	 */
+	@GetMapping("/scan/start/{deviceId}/{channelId}")
+	public void startScan(@PathVariable String deviceId, @PathVariable String channelId, Integer scanId) {
+		if (scanId == null || scanId < 0 || scanId > 255 ) {
+			throw new ControllerException(ErrorCode.ERROR100.getCode(), "扫描组号必须为0-255之间的数字");
+		}
+		frontEndCommand(deviceId, channelId, 0x89, scanId, 0, 0);
+	}
+
+	/**
+	 * 设备扫描停止
+	 *
+	 * @param deviceId 设备国标编号
+	 * @param channelId 通道国标编号
+	 * @param scanId 扫描组号(0-255)
+	 */
+	@GetMapping("/scan/stop/{deviceId}/{channelId}")
+	public void stopScan(@PathVariable String deviceId, @PathVariable String channelId, Integer scanId) {
+		if (scanId == null || scanId < 0 || scanId > 255 ) {
+			throw new ControllerException(ErrorCode.ERROR100.getCode(), "扫描组号必须为0-255之间的数字");
+		}
+		frontEndCommand(deviceId, channelId, 0, 0, 0, 0);
+	}
+
+	/**
+	 * 设备扫描上
+	 *
+	 * @param deviceId 设备国标编号
+	 * @param channelId 通道国标编号
+	 * @param scanId 扫描组号(0-255)
+	 */
+	@GetMapping("/scan/set/left/{deviceId}/{channelId}")
+	public void setScanLeft(@PathVariable String deviceId, @PathVariable String channelId, Integer scanId) {
+		if (scanId == null || scanId < 0 || scanId > 255 ) {
+			throw new ControllerException(ErrorCode.ERROR100.getCode(), "扫描组号必须为0-255之间的数字");
+		}
+		frontEndCommand(deviceId, channelId, 0x89, scanId, 1, 0);
+	}
+
+	/**
+	 * 设备扫描下
+	 *
+	 * @param deviceId 设备国标编号
+	 * @param channelId 通道国标编号
+	 * @param scanId 扫描组号(0-255)
+	 */
+	@GetMapping("/scan/set/right/{deviceId}/{channelId}")
+	public void setScanRight(@PathVariable String deviceId, @PathVariable String channelId, Integer scanId) {
+		if (scanId == null || scanId < 0 || scanId > 255 ) {
+			throw new ControllerException(ErrorCode.ERROR100.getCode(), "扫描组号必须为0-255之间的数字");
+		}
+		frontEndCommand(deviceId, channelId, 0x89, scanId, 2, 0);
+	}
+
+
+	/**
+	 * 设备扫描左
+	 *
+	 * @param deviceId 设备国标编号
+	 * @param channelId 通道国标编号
+	 * @param scanId 扫描组号(0-255)
+	 * @param speed 自动扫描速度(1-4095)
+	 */
+	@GetMapping("/scan/set/speed/{deviceId}/{channelId}")
+	public void setScanSpeed(@PathVariable String deviceId, @PathVariable String channelId, Integer scanId, Integer speed) {
+		if (scanId == null || scanId < 0 || scanId > 255 ) {
+			throw new ControllerException(ErrorCode.ERROR100.getCode(), "扫描组号必须为0-255之间的数字");
+		}
+		if (speed == null || speed < 1 || speed > 4095) {
+			throw new ControllerException(ErrorCode.ERROR100.getCode(), "自动扫描速度必须为1-4095之间的数字");
+		}
+		int parameter2 = speed & 0xFF;
+		int combindCode2 =  speed >> 8;
+		frontEndCommand(deviceId, channelId, 0x8A, scanId, parameter2, combindCode2);
+	}
+
+	/**
+	 * 设备雨刷控制
+	 *
+	 * @param deviceId 设备国标编号
+	 * @param channelId 通道国标编号
+	 * @param command 控制指令,允许值: on, off
+	 */
+	@GetMapping("/wiper/{deviceId}/{channelId}")
+	public void wiper(@PathVariable String deviceId,@PathVariable String channelId, String command){
+
+		if (log.isDebugEnabled()) {
+			log.debug("辅助开关控制指令-雨刷控制 API调用,deviceId:{} ,channelId:{} ,command:{}",deviceId, channelId, command);
+		}
+
+		int cmdCode = 0;
+		switch (command){
+			case "on":
+				cmdCode = 0x8c;
+				break;
+			case "off":
+				cmdCode = 0x8d;
+				break;
+			default:
+				break;
+		}
+		frontEndCommand(deviceId, channelId, cmdCode, 1, 0, 0);
+	}
+
+	/**
+	 * 设备雨刷控制
+	 *
+	 * @param deviceId 设备国标编号
+	 * @param channelId 通道国标编号
+	 * @param command 控制指令,允许值: on, off
+	 * @param switchId 开关编号
+	 */
+	@GetMapping("/auxiliary/{deviceId}/{channelId}")
+	public void auxiliarySwitch(@PathVariable String deviceId,@PathVariable String channelId, String command, Integer switchId){
+
+		if (log.isDebugEnabled()) {
+			log.debug("辅助开关控制指令-雨刷控制 API调用,deviceId:{} ,channelId:{} ,command:{}, switchId: {}",deviceId, channelId, command, switchId);
+		}
+
+		int cmdCode = 0;
+		switch (command){
+			case "on":
+				cmdCode = 0x8c;
+				break;
+			case "off":
+				cmdCode = 0x8d;
+				break;
+			default:
+				break;
+		}
+		frontEndCommand(deviceId, channelId, cmdCode, switchId, 0, 0);
+	}
+}

+ 153 - 0
video-service/src/main/java/com/ruoyi/wvp/gb28181/ApiControlController.java

@@ -0,0 +1,153 @@
+package com.ruoyi.wvp.gb28181;
+
+import com.zksy.common.exception.ControllerException;
+import com.ruoyi.wvp.gb28181.bean.Device;
+import com.ruoyi.wvp.gb28181.service.IDeviceService;
+import com.ruoyi.wvp.gb28181.transmit.cmd.impl.SIPCommander;
+import com.zksy.common.enums.ErrorCode;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+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 javax.sip.InvalidArgumentException;
+import javax.sip.SipException;
+import java.text.ParseException;
+
+/**
+ * API兼容:设备控制
+ */
+@Slf4j
+@RestController
+@RequestMapping(value = "/api/v1/control")
+public class ApiControlController {
+
+    @Autowired
+    private SIPCommander cmder;
+
+    @Autowired
+    private IDeviceService deviceService;
+
+    /**
+     * 设备控制 - 云台控制
+     * @param serial 设备编号
+     * @param command 控制指令 允许值: left, right, up, down, upleft, upright, downleft, downright, zoomin, zoomout, stop
+     * @param channel 通道序号
+     * @param code 通道编号
+     * @param speed 速度(0~255) 默认值: 129
+     * @return
+     */
+    @GetMapping(value = "/ptz")
+    private void list(String serial,String command,
+                            @RequestParam(required = false)Integer channel,
+                            @RequestParam(required = false)String code,
+                            @RequestParam(required = false)Integer speed){
+
+        if (log.isDebugEnabled()) {
+            log.debug("模拟接口> 设备云台控制 API调用,deviceId:{} ,channelId:{} ,command:{} ,speed:{} ",
+                    serial, code, command, speed);
+        }
+        if (channel == null) {channel = 0;}
+        if (speed == null) {speed = 0;}
+        Device device = deviceService.getDeviceByDeviceId(serial);
+        if (device == null) {
+            throw new ControllerException(ErrorCode.ERROR100.getCode(), "device[ " + serial + " ]未找到");
+        }
+        int cmdCode = 0;
+        switch (command){
+            case "left":
+                cmdCode = 2;
+                break;
+            case "right":
+                cmdCode = 1;
+                break;
+            case "up":
+                cmdCode = 8;
+                break;
+            case "down":
+                cmdCode = 4;
+                break;
+            case "upleft":
+                cmdCode = 10;
+                break;
+            case "upright":
+                cmdCode = 9;
+                break;
+            case "downleft":
+                cmdCode = 6;
+                break;
+            case "downright":
+                cmdCode = 5;
+                break;
+            case "zoomin":
+                cmdCode = 16;
+                break;
+            case "zoomout":
+                cmdCode = 32;
+                break;
+            case "stop":
+                cmdCode = 0;
+                break;
+            default:
+                break;
+        }
+        // 默认值 50
+        try {
+            cmder.frontEndCmd(device, code, cmdCode, speed, speed, speed);
+        } catch (SipException | InvalidArgumentException | ParseException e) {
+            log.error("[命令发送失败] 云台控制: {}", e.getMessage());
+            throw new ControllerException(ErrorCode.ERROR100.getCode(), "命令发送失败: " + e.getMessage());
+        }
+    }
+
+    /**
+     * 设备控制 - 预置位控制
+     * @param serial 设备编号
+     * @param code 通道编号,通过 /api/v1/device/channellist 获取的 ChannelList.ID, 该参数和 channel 二选一传递即可
+     * @param channel 通道序号, 默认值: 1
+     * @param command 控制指令 允许值: set, goto, remove
+     * @param preset 预置位编号(1~255)
+     * @param name 预置位名称, command=set 时有效
+     * @return
+     */
+    @GetMapping(value = "/preset")
+    private void list(String serial,String command,
+                            @RequestParam(required = false)Integer channel,
+                            @RequestParam(required = false)String code,
+                            @RequestParam(required = false)String name,
+                            @RequestParam(required = false)Integer preset){
+
+        if (log.isDebugEnabled()) {
+            log.debug("模拟接口> 预置位控制 API调用,deviceId:{} ,channelId:{} ,command:{} ,name:{} ,preset:{} ",
+                    serial, code, command, name, preset);
+        }
+
+        if (channel == null) {channel = 0;}
+        Device device = deviceService.getDeviceByDeviceId(serial);
+        if (device == null) {
+            throw new ControllerException(ErrorCode.ERROR100.getCode(), "device[ " + serial + " ]未找到");
+        }
+        int cmdCode = 0;
+        switch (command){
+            case "set":
+                cmdCode = 129;
+                break;
+            case "goto":
+                cmdCode = 130;
+                break;
+            case "remove":
+                cmdCode = 131;
+                break;
+            default:
+                break;
+        }
+        try {
+            cmder.frontEndCmd(device, code, cmdCode, 0, preset, 0);
+        } catch (SipException | InvalidArgumentException | ParseException e) {
+            log.error("[命令发送失败] 预置位控制: {}", e.getMessage());
+            throw new ControllerException(ErrorCode.ERROR100.getCode(), "命令发送失败: " + e.getMessage());
+        }
+    }
+}

+ 253 - 0
video-service/src/main/java/com/ruoyi/wvp/gb28181/ApiDeviceController.java

@@ -0,0 +1,253 @@
+package com.ruoyi.wvp.gb28181;
+
+import com.alibaba.fastjson2.JSONArray;
+import com.alibaba.fastjson2.JSONObject;
+import com.zksy.common.exception.ControllerException;
+import com.ruoyi.wvp.gb28181.bean.Device;
+import com.ruoyi.wvp.gb28181.bean.Preset;
+import com.ruoyi.wvp.gb28181.dto.DeviceChannelExtend;
+import com.ruoyi.wvp.gb28181.service.IDeviceChannelService;
+import com.ruoyi.wvp.gb28181.service.IDeviceService;
+import com.ruoyi.wvp.gb28181.transmit.callback.DeferredResultHolder;
+import com.ruoyi.wvp.gb28181.transmit.callback.RequestMessage;
+import com.ruoyi.wvp.gb28181.transmit.cmd.impl.SIPCommander;
+import com.ruoyi.wvp.vmanager.bean.DeferredResultEx;
+import com.zksy.common.enums.ErrorCode;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.util.ObjectUtils;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.context.request.async.DeferredResult;
+
+import javax.sip.InvalidArgumentException;
+import javax.sip.SipException;
+import java.text.ParseException;
+import java.util.*;
+
+/**
+ * API兼容:设备信息
+ */
+@SuppressWarnings("unchecked")
+@Slf4j
+@RestController
+@RequestMapping(value = "/api/v1/device")
+public class ApiDeviceController {
+
+    @Autowired
+    private SIPCommander cmder;
+    @Autowired
+    private IDeviceChannelService channelService;
+
+    @Autowired
+    private DeferredResultHolder resultHolder;
+
+    @Autowired
+    private IDeviceService deviceService;
+
+
+    /**
+     * 分页获取设备列表 现在直接返回,尚未实现分页
+     * @param start
+     * @param limit
+     * @param q
+     * @param online
+     * @return
+     */
+    @GetMapping(value = "/list")
+    public JSONObject list( @RequestParam(required = false)Integer start,
+                            @RequestParam(required = false)Integer limit,
+                            @RequestParam(required = false)String q,
+                            @RequestParam(required = false)Boolean online ){
+
+//        if (logger.isDebugEnabled()) {
+//            logger.debug("查询所有视频设备API调用");
+//        }
+
+        JSONObject result = new JSONObject();
+        List<Device> devices;
+        if (start == null || limit ==null) {
+            devices = deviceService.getAllByStatus(online);
+            result.put("DeviceCount", devices.size());
+        }else {
+            Device device = new Device();
+            device.setPageNum(start / limit);
+            device.setPageSize(limit);
+            List<Device> list = deviceService.getAll(device);
+            result.put("DeviceCount", list.size());
+            devices = list;
+        }
+
+        JSONArray deviceJSONList = new JSONArray();
+        devices.stream().forEach(device -> {
+            JSONObject deviceJsonObject = new JSONObject();
+            deviceJsonObject.put("ID", device.getDeviceId());
+            deviceJsonObject.put("Name", device.getName());
+            deviceJsonObject.put("Type", "GB");
+            deviceJsonObject.put("ChannelCount", device.getChannelCount());
+            deviceJsonObject.put("RecvStreamIP", "");
+            deviceJsonObject.put("CatalogInterval", 3600); // 通道目录抓取周期
+            deviceJsonObject.put("SubscribeInterval", device.getSubscribeCycleForCatalog()); // 订阅周期(秒), 0 表示后台不周期订阅
+            deviceJsonObject.put("Online", device.isOnLine());
+            deviceJsonObject.put("Password", "");
+            deviceJsonObject.put("MediaTransport", device.getTransport());
+            deviceJsonObject.put("RemoteIP", device.getIp());
+            deviceJsonObject.put("RemotePort", device.getPort());
+            deviceJsonObject.put("LastRegisterAt", "");
+            deviceJsonObject.put("LastKeepaliveAt", "");
+            deviceJsonObject.put("UpdatedAt", "");
+            deviceJsonObject.put("CreatedAt", "");
+            deviceJSONList.add(deviceJsonObject);
+        });
+        result.put("DeviceList",deviceJSONList);
+        return result;
+    }
+
+    @GetMapping(value = "/channellist")
+    public JSONObject channellist( String serial,
+                                   @RequestParam(required = false)String channel_type,
+                                   @RequestParam(required = false)String code ,
+                                   @RequestParam(required = false)String dir_serial ,
+                                   @RequestParam(required = false)Integer start,
+                                   @RequestParam(required = false)Integer limit,
+                                   @RequestParam(required = false)String q,
+                                   @RequestParam(required = false)Boolean online ){
+
+        JSONObject result = new JSONObject();
+        List<DeviceChannelExtend> deviceChannels;
+        List<String> channelIds = null;
+        if (!ObjectUtils.isEmpty(code)) {
+            String[] split = code.trim().split(",");
+            channelIds = Arrays.asList(split);
+        }
+        List<DeviceChannelExtend> allDeviceChannelList = channelService.queryChannelExtendsByDeviceId(serial,channelIds,online);
+        if (start == null || limit ==null) {
+            deviceChannels = allDeviceChannelList;
+            result.put("ChannelCount", deviceChannels.size());
+        }else {
+            if (start > allDeviceChannelList.size()) {
+                deviceChannels = new ArrayList<>();
+            }else {
+                if (start + limit < allDeviceChannelList.size()) {
+                    deviceChannels = allDeviceChannelList.subList(start, start + limit);
+                }else {
+                    deviceChannels = allDeviceChannelList.subList(start, allDeviceChannelList.size());
+                }
+            }
+            result.put("ChannelCount", allDeviceChannelList.size());
+        }
+        JSONArray channleJSONList = new JSONArray();
+        deviceChannels.stream().forEach(deviceChannelExtend -> {
+            JSONObject deviceJOSNChannel = new JSONObject();
+            deviceJOSNChannel.put("ID", deviceChannelExtend.getChannelId());
+            deviceJOSNChannel.put("DeviceID", deviceChannelExtend.getDeviceId());
+            deviceJOSNChannel.put("DeviceName", deviceChannelExtend.getDeviceName());
+            deviceJOSNChannel.put("DeviceOnline", deviceChannelExtend.isDeviceOnline());
+            deviceJOSNChannel.put("Channel", 0); // TODO 自定义序号
+            deviceJOSNChannel.put("Name", deviceChannelExtend.getName());
+            deviceJOSNChannel.put("Custom", false);
+            deviceJOSNChannel.put("CustomName", "");
+            deviceJOSNChannel.put("SubCount", deviceChannelExtend.getSubCount()); // TODO ? 子节点数, SubCount > 0 表示该通道为子目录
+            deviceJOSNChannel.put("SnapURL", "");
+            deviceJOSNChannel.put("Manufacturer ", deviceChannelExtend.getManufacture());
+            deviceJOSNChannel.put("Model", deviceChannelExtend.getModel());
+            deviceJOSNChannel.put("Owner", deviceChannelExtend.getOwner());
+            deviceJOSNChannel.put("CivilCode", deviceChannelExtend.getCivilCode());
+            deviceJOSNChannel.put("Address", deviceChannelExtend.getAddress());
+            deviceJOSNChannel.put("Parental", deviceChannelExtend.getParental()); // 当为通道设备时, 是否有通道子设备, 1-有,0-没有
+            deviceJOSNChannel.put("ParentID", deviceChannelExtend.getParentId()); // 直接上级编号
+            deviceJOSNChannel.put("Secrecy", deviceChannelExtend.getSecrecy());
+            deviceJOSNChannel.put("RegisterWay", 1); // 注册方式, 缺省为1, 允许值: 1, 2, 3
+            // 1-IETF RFC3261,
+            // 2-基于口令的双向认证,
+            // 3-基于数字证书的双向认证
+            deviceJOSNChannel.put("Status", deviceChannelExtend.getStatus());
+            deviceJOSNChannel.put("Longitude", deviceChannelExtend.getLongitude());
+            deviceJOSNChannel.put("Latitude", deviceChannelExtend.getLatitude());
+            deviceJOSNChannel.put("PTZType ", deviceChannelExtend.getPTZType()); // 云台类型, 0 - 未知, 1 - 球机, 2 - 半球,
+            //   3 - 固定枪机, 4 - 遥控枪机
+            deviceJOSNChannel.put("CustomPTZType", "");
+            deviceJOSNChannel.put("StreamID", deviceChannelExtend.getStreamId()); // StreamID 直播流ID, 有值表示正在直播
+            deviceJOSNChannel.put("NumOutputs ", -1); // 直播在线人数
+            channleJSONList.add(deviceJOSNChannel);
+        });
+        result.put("ChannelList", channleJSONList);
+        return result;
+    }
+
+    /**
+     * 设备信息 - 获取下级通道预置位
+     * @param serial 设备编号
+     * @param code 通道编号,通过 /api/v1/device/channellist 获取的 ChannelList.ID, 该参数和 channel 二选一传递即可
+     * @param channel 通道序号, 默认值: 1
+     * @param fill 是否填充空置预置位,当下级返回预置位,但不够255个时,自动填充空置预置位到255个, 默认值: true, 允许值: true, false
+     * @param timeout 超时时间(秒) 默认值: 15
+     * @return
+     */
+    @GetMapping(value = "/fetchpreset")
+    private DeferredResult<Object>  list(String serial,
+                      @RequestParam(required = false)Integer channel,
+                      @RequestParam(required = false)String code,
+                      @RequestParam(required = false)Boolean fill,
+                      @RequestParam(required = false)Integer timeout){
+
+        if (log.isDebugEnabled()) {
+            log.debug("<模拟接口> 获取下级通道预置位 API调用,deviceId:{} ,channel:{} ,code:{} ,fill:{} ,timeout:{} ",
+                    serial, channel, code, fill, timeout);
+        }
+
+        Device device = deviceService.getDeviceByDeviceId(serial);
+        String uuid =  UUID.randomUUID().toString();
+        String key =  DeferredResultHolder.CALLBACK_CMD_PRESETQUERY + (ObjectUtils.isEmpty(code) ? serial : code);
+        DeferredResult<Object> result = new DeferredResult<> (timeout * 1000L);
+        DeferredResultEx<Object> deferredResultEx = new DeferredResultEx<>(result);
+        result.onTimeout(()->{
+            log.warn("<模拟接口> 获取设备预置位超时");
+            // 释放rtpserver
+            RequestMessage msg = new RequestMessage();
+            msg.setId(uuid);
+            msg.setKey(key);
+            msg.setData("wait for presetquery timeout["+timeout+"s]");
+            resultHolder.invokeResult(msg);
+        });
+        if (resultHolder.exist(key, null)) {
+            return result;
+        }
+
+        deferredResultEx.setFilter(filterResult->{
+            List<Preset> presetQuerySipReqList = (List<Preset>)filterResult;
+            HashMap<String, Object> resultMap = new HashMap<>();
+            resultMap.put("DeviceID", code);
+            resultMap.put("Result", "OK");
+            resultMap.put("SumNum", presetQuerySipReqList.size());
+            ArrayList<Map<String, Object>> presetItemList = new ArrayList<>(presetQuerySipReqList.size());
+            for (Preset presetQuerySipReq : presetQuerySipReqList) {
+                Map<String, Object> item = new HashMap<>();
+                item.put("PresetID", presetQuerySipReq.getPresetId());
+                item.put("PresetName", presetQuerySipReq.getPresetName());
+                item.put("PresetEnable", true);
+                presetItemList.add(item);
+            }
+            resultMap.put("PresetItemList",presetItemList );
+            return resultMap;
+        });
+
+        resultHolder.put(key, uuid, deferredResultEx);
+
+        try {
+            cmder.presetQuery(device, code, event -> {
+                RequestMessage msg = new RequestMessage();
+                msg.setId(uuid);
+                msg.setKey(key);
+                msg.setData(String.format("获取设备预置位失败,错误码: %s, %s", event.statusCode, event.msg));
+                resultHolder.invokeResult(msg);
+            });
+        } catch (InvalidArgumentException | SipException | ParseException e) {
+            log.error("[命令发送失败] 获取设备预置位: {}", e.getMessage());
+            throw new ControllerException(ErrorCode.ERROR100.getCode(), "命令发送失败: " + e.getMessage());
+        }
+        return result;
+    }
+}

+ 276 - 0
video-service/src/main/java/com/ruoyi/wvp/gb28181/ApiStreamController.java

@@ -0,0 +1,276 @@
+package com.ruoyi.wvp.gb28181;
+
+import com.alibaba.fastjson2.JSONObject;
+import com.ruoyi.wvp.common.InviteInfo;
+import com.ruoyi.wvp.common.InviteSessionType;
+import com.ruoyi.wvp.common.StreamInfo;
+import com.ruoyi.wvp.conf.UserSetting;
+import com.zksy.common.exception.SsrcTransactionNotFoundException;
+import com.ruoyi.wvp.gb28181.bean.Device;
+import com.ruoyi.wvp.gb28181.bean.DeviceChannel;
+import com.ruoyi.wvp.gb28181.service.IDeviceChannelService;
+import com.ruoyi.wvp.gb28181.service.IDeviceService;
+import com.ruoyi.wvp.gb28181.service.IInviteStreamService;
+import com.ruoyi.wvp.gb28181.service.IPlayService;
+import com.ruoyi.wvp.gb28181.transmit.cmd.impl.SIPCommander;
+import com.ruoyi.wvp.media.bean.MediaServer;
+import com.ruoyi.wvp.service.bean.InviteErrorCode;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+import org.springframework.web.context.request.async.DeferredResult;
+
+import javax.sip.InvalidArgumentException;
+import javax.sip.SipException;
+import java.text.ParseException;
+
+/**
+ * API兼容:实时直播
+ */
+@SuppressWarnings(value = {"rawtypes", "unchecked"})
+
+@Slf4j
+@RestController
+@RequestMapping(value = "/api/v1/stream")
+public class ApiStreamController {
+
+    @Autowired
+    private SIPCommander cmder;
+
+    @Autowired
+    private UserSetting userSetting;
+
+    @Autowired
+    private IDeviceService deviceService;
+
+    @Autowired
+    private IDeviceChannelService deviceChannelService;
+
+    @Autowired
+    private IPlayService playService;
+
+    @Autowired
+    private IInviteStreamService inviteStreamService;
+
+    /**
+     * 实时直播 - 开始直播
+     * @param serial 设备编号
+     * @param channel 通道序号 默认值: 1
+     * @param code 通道编号,通过 /api/v1/device/channellist 获取的 ChannelList.ID, 该参数和 channel 二选一传递即可
+     * @param cdn 转推 CDN 地址, 形如: [rtmp|rtsp]://xxx, encodeURIComponent
+     * @param audio 是否开启音频, 默认 开启
+     * @param transport 流传输模式, 默认 UDP
+     * @param checkchannelstatus 是否检查通道状态, 默认 false, 表示 拉流前不检查通道状态是否在线
+     * @param transportmode 当 transport=TCP 时有效, 指示流传输主被动模式, 默认被动
+     * @param timeout 拉流超时(秒),
+     * @return
+     */
+    @GetMapping("/start")
+    private DeferredResult<JSONObject> 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> result = new DeferredResult<>(userSetting.getPlayTimeout().longValue() + 10);
+        Device device = deviceService.getDeviceByDeviceId(serial);
+        if (device == null ) {
+            JSONObject resultJSON = new JSONObject();
+            resultJSON.put("error","device[ " + serial + " ]未找到");
+            result.setResult(resultJSON);
+            return result;
+        }else if (!device.isOnLine()) {
+            JSONObject resultJSON = new JSONObject();
+            resultJSON.put("error","device[ " + code + " ]offline");
+            result.setResult(resultJSON);
+            return result;
+        }
+
+
+        DeviceChannel deviceChannel = deviceChannelService.getOne(serial, code);
+        if (deviceChannel == null) {
+            JSONObject resultJSON = new JSONObject();
+            resultJSON.put("error","channel[ " + code + " ]未找到");
+            result.setResult(resultJSON);
+            return result;
+        }else if (!deviceChannel.getStatus().equalsIgnoreCase("ON")) {
+            JSONObject resultJSON = new JSONObject();
+            resultJSON.put("error","channel[ " + code + " ]offline");
+            result.setResult(resultJSON);
+            return result;
+        }
+
+        result.onTimeout(()->{
+            log.info("播放等待超时");
+            JSONObject resultJSON = new JSONObject();
+            resultJSON.put("error","timeout");
+            result.setResult(resultJSON);
+            inviteStreamService.removeInviteInfoByDeviceAndChannel(InviteSessionType.PLAY, deviceChannel.getId());
+            deviceChannelService.stopPlay(deviceChannel.getId());
+            // 清理RTP server
+        });
+
+        MediaServer newMediaServerItem = playService.getNewMediaServerItem(device);
+
+        playService.play(newMediaServerItem, serial, code, null, (errorCode, msg, data) -> {
+            if (errorCode == InviteErrorCode.SUCCESS.getCode()) {
+                if (data != null) {
+                    StreamInfo streamInfo = (StreamInfo)data;
+                    JSONObject resultJjson = new JSONObject();
+                    resultJjson.put("StreamID", streamInfo.getStream());
+                    resultJjson.put("DeviceID", serial);
+                    resultJjson.put("ChannelID", code);
+                    resultJjson.put("ChannelName", deviceChannel.getName());
+                    resultJjson.put("ChannelCustomName", "");
+                    if (streamInfo.getTranscodeStream() != null) {
+                        resultJjson.put("FLV", streamInfo.getTranscodeStream().getFlv().getUrl());
+                    }else {
+                        resultJjson.put("FLV", streamInfo.getFlv().getUrl());
+
+                    }
+                    if(streamInfo.getHttps_flv() != null) {
+                        if (streamInfo.getTranscodeStream() != null) {
+                            resultJjson.put("HTTPS_FLV", streamInfo.getTranscodeStream().getHttps_flv().getUrl());
+                        }else {
+                            resultJjson.put("HTTPS_FLV", streamInfo.getHttps_flv().getUrl());
+                        }
+                    }
+
+                    if (streamInfo.getTranscodeStream() != null) {
+                        resultJjson.put("WS_FLV", streamInfo.getTranscodeStream().getWs_flv().getUrl());
+                    }else {
+                        resultJjson.put("WS_FLV", streamInfo.getWs_flv().getUrl());
+                    }
+
+                    if(streamInfo.getWss_flv() != null) {
+                        if (streamInfo.getTranscodeStream() != null) {
+                            resultJjson.put("WSS_FLV", streamInfo.getTranscodeStream().getWss_flv().getUrl());
+                        }else {
+                            resultJjson.put("WSS_FLV", streamInfo.getWss_flv().getUrl());
+                        }
+                    }
+                    resultJjson.put("RTMP", streamInfo.getRtmp().getUrl());
+                    if (streamInfo.getRtmps() != null) {
+                        resultJjson.put("RTMPS", streamInfo.getRtmps().getUrl());
+                    }
+                    resultJjson.put("HLS", streamInfo.getHls().getUrl());
+                    if (streamInfo.getHttps_hls() != null) {
+                        resultJjson.put("HTTPS_HLS", streamInfo.getHttps_hls().getUrl());
+                    }
+                    resultJjson.put("RTSP", streamInfo.getRtsp().getUrl());
+                    if (streamInfo.getRtsps() != null) {
+                        resultJjson.put("RTSPS", streamInfo.getRtsps().getUrl());
+                    }
+                    resultJjson.put("WEBRTC", streamInfo.getRtc().getUrl());
+                    if (streamInfo.getRtcs() != null) {
+                        resultJjson.put("HTTPS_WEBRTC", streamInfo.getRtcs().getUrl());
+                    }
+                    resultJjson.put("CDN", "");
+                    resultJjson.put("SnapURL", "");
+                    resultJjson.put("Transport", device.getTransport());
+                    resultJjson.put("StartAt", "");
+                    resultJjson.put("Duration", "");
+                    resultJjson.put("SourceVideoCodecName", "");
+                    resultJjson.put("SourceVideoWidth", "");
+                    resultJjson.put("SourceVideoHeight", "");
+                    resultJjson.put("SourceVideoFrameRate", "");
+                    resultJjson.put("SourceAudioCodecName", "");
+                    resultJjson.put("SourceAudioSampleRate", "");
+                    resultJjson.put("AudioEnable", "");
+                    resultJjson.put("Ondemand", "");
+                    resultJjson.put("InBytes", "");
+                    resultJjson.put("InBitRate", "");
+                    resultJjson.put("OutBytes", "");
+                    resultJjson.put("NumOutputs", "");
+                    resultJjson.put("CascadeSize", "");
+                    resultJjson.put("RelaySize", "");
+                    resultJjson.put("ChannelPTZType", "0");
+                    result.setResult(resultJjson);
+                }else {
+                    JSONObject resultJjson = new JSONObject();
+                    resultJjson.put("error", "channel[ " + code + " ] " + msg);
+                    result.setResult(resultJjson);
+                }
+            }else {
+                JSONObject resultJjson = new JSONObject();
+                resultJjson.put("error", "channel[ " + code + " ] " + msg);
+                result.setResult(resultJjson);
+            }
+        });
+
+        return result;
+    }
+
+    /**
+     * 实时直播 - 直播流停止
+     * @param serial 设备编号
+     * @param channel 通道序号
+     * @param code 通道国标编号
+     * @param check_outputs
+     * @return
+     */
+    @GetMapping("/stop")
+    @ResponseBody
+    private JSONObject stop(String serial ,
+                             @RequestParam(required = false)Integer channel ,
+                             @RequestParam(required = false)String code,
+                             @RequestParam(required = false)String check_outputs
+
+    ){
+
+
+        Device device = deviceService.getDeviceByDeviceId(serial);
+        if (device == null) {
+            JSONObject result = new JSONObject();
+            result.put("error","未找到设备");
+            return result;
+        }
+        DeviceChannel deviceChannel = deviceChannelService.getOne(serial, code);
+        if (deviceChannel == null) {
+            JSONObject result = new JSONObject();
+            result.put("error","未找到通道");
+            return result;
+        }
+        InviteInfo inviteInfo = inviteStreamService.getInviteInfoByDeviceAndChannel(InviteSessionType.PLAY, deviceChannel.getId());
+        if (inviteInfo == null) {
+            JSONObject result = new JSONObject();
+            result.put("error","未找到流信息");
+            return result;
+        }
+
+        try {
+            cmder.streamByeCmd(device, code, "rtp", inviteInfo.getStream(), null, null);
+        } catch (InvalidArgumentException | ParseException | SipException | SsrcTransactionNotFoundException e) {
+            JSONObject result = new JSONObject();
+            result.put("error","发送BYE失败:" + e.getMessage());
+            return result;
+        }
+        inviteStreamService.removeInviteInfo(inviteInfo);
+        deviceChannelService.stopPlay(inviteInfo.getChannelId());
+        return null;
+    }
+
+    /**
+     * 实时直播 - 直播流保活
+     * @param serial 设备编号
+     * @param channel 通道序号
+     * @param code 通道国标编号
+     * @return
+     */
+    @GetMapping("/touch")
+    @ResponseBody
+    private JSONObject touch(String serial ,String t,
+                            @RequestParam(required = false)Integer channel ,
+                            @RequestParam(required = false)String code,
+                            @RequestParam(required = false)String autorestart,
+                            @RequestParam(required = false)String audio,
+                            @RequestParam(required = false)String cdn
+    ){
+        return null;
+    }
+}

+ 186 - 0
video-service/src/main/java/com/ruoyi/wvp/gb28181/SipLayer.java

@@ -0,0 +1,186 @@
+package com.ruoyi.wvp.gb28181;
+
+
+import com.ruoyi.wvp.conf.SipConfig;
+import com.ruoyi.wvp.conf.UserSetting;
+import com.ruoyi.wvp.gb28181.bean.GbStringMsgParserFactory;
+import com.ruoyi.wvp.gb28181.conf.DefaultProperties;
+import com.ruoyi.wvp.gb28181.transmit.ISIPProcessorObserver;
+import gov.nist.javax.sip.SipProviderImpl;
+import gov.nist.javax.sip.SipStackImpl;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.CommandLineRunner;
+import org.springframework.core.annotation.Order;
+import org.springframework.stereotype.Component;
+import org.springframework.util.ObjectUtils;
+
+import javax.sip.*;
+import java.net.Inet4Address;
+import java.net.InetAddress;
+import java.net.NetworkInterface;
+import java.util.*;
+import java.util.concurrent.ConcurrentHashMap;
+
+@Slf4j
+@Component
+@Order(value=10)
+public class SipLayer implements CommandLineRunner {
+
+	@Autowired
+	private SipConfig sipConfig;
+
+	@Autowired
+	private ISIPProcessorObserver sipProcessorObserver;
+
+	@Autowired
+	private UserSetting userSetting;
+
+	private final Map<String, SipProviderImpl> tcpSipProviderMap = new ConcurrentHashMap<>();
+	private final Map<String, SipProviderImpl> udpSipProviderMap = new ConcurrentHashMap<>();
+	private final List<String> monitorIps = new ArrayList<>();
+
+	@Override
+	public void run(String... args) {
+		if (ObjectUtils.isEmpty(sipConfig.getIp())) {
+			try {
+				// 获得本机的所有网络接口
+				Enumeration<NetworkInterface> nifs = NetworkInterface.getNetworkInterfaces();
+				while (nifs.hasMoreElements()) {
+					NetworkInterface nif = nifs.nextElement();
+					// 获得与该网络接口绑定的 IP 地址,一般只有一个
+					Enumeration<InetAddress> addresses = nif.getInetAddresses();
+					while (addresses.hasMoreElements()) {
+						InetAddress addr = addresses.nextElement();
+						if (addr instanceof Inet4Address) {
+							if (addr.getHostAddress().equals("127.0.0.1")){
+								continue;
+							}
+							if (nif.getName().startsWith("docker")) {
+								continue;
+							}
+							log.info("[自动配置SIP监听网卡] 网卡接口地址: {}", addr.getHostAddress());// 只关心 IPv4 地址
+							monitorIps.add(addr.getHostAddress());
+						}
+					}
+				}
+			}catch (Exception e) {
+				log.error("[读取网卡信息失败]", e);
+			}
+			if (monitorIps.isEmpty()) {
+				log.error("[自动配置SIP监听网卡信息失败], 请手动配置SIP.IP后重新启动");
+				System.exit(1);
+			}
+		}else {
+			// 使用逗号分割多个ip
+			String separator = ",";
+			if (sipConfig.getIp().indexOf(separator) > 0) {
+				String[] split = sipConfig.getIp().split(separator);
+				monitorIps.addAll(Arrays.asList(split));
+			}else {
+				monitorIps.add(sipConfig.getIp());
+			}
+		}
+		if (ObjectUtils.isEmpty(sipConfig.getShowIp())){
+			sipConfig.setShowIp(String.join(",", monitorIps));
+		}
+		SipFactory.getInstance().setPathName("gov.nist");
+		if (monitorIps.size() > 0) {
+			for (String monitorIp : monitorIps) {
+				addListeningPoint(monitorIp, sipConfig.getPort());
+			}
+			if (udpSipProviderMap.size() + tcpSipProviderMap.size() == 0) {
+				System.exit(1);
+			}
+		}
+	}
+
+	private void addListeningPoint(String monitorIp, int port){
+		SipStackImpl sipStack;
+		try {
+			sipStack = (SipStackImpl)SipFactory.getInstance().createSipStack(DefaultProperties.getProperties("GB28181_SIP", userSetting.getSipLog()));
+			sipStack.setMessageParserFactory(new GbStringMsgParserFactory());
+		} catch (PeerUnavailableException e) {
+			log.error("[SIP SERVER] SIP服务启动失败, 监听地址{}失败,请检查ip是否正确", monitorIp);
+			System.out.println(e.getMessage());
+			return;
+		}
+
+		try {
+			ListeningPoint tcpListeningPoint = sipStack.createListeningPoint(monitorIp, port, "TCP");
+			SipProviderImpl tcpSipProvider = (SipProviderImpl)sipStack.createSipProvider(tcpListeningPoint);
+
+			tcpSipProvider.setDialogErrorsAutomaticallyHandled();
+			tcpSipProvider.addSipListener(sipProcessorObserver);
+			tcpSipProviderMap.put(monitorIp, tcpSipProvider);
+			log.info("[SIP SERVER] tcp://{}:{} 启动成功", monitorIp, port);
+		} catch (TransportNotSupportedException
+				 | TooManyListenersException
+				 | ObjectInUseException
+				 | InvalidArgumentException e) {
+			log.error("[SIP SERVER] tcp://{}:{} SIP服务启动失败,请检查端口是否被占用或者ip是否正确"
+					, monitorIp, port);
+		}
+
+		try {
+			ListeningPoint udpListeningPoint = sipStack.createListeningPoint(monitorIp, port, "UDP");
+
+			SipProviderImpl udpSipProvider = (SipProviderImpl)sipStack.createSipProvider(udpListeningPoint);
+			udpSipProvider.addSipListener(sipProcessorObserver);
+			udpSipProvider.setDialogErrorsAutomaticallyHandled();
+			udpSipProviderMap.put(monitorIp, udpSipProvider);
+
+			log.info("[SIP SERVER] udp://{}:{} 启动成功", monitorIp, port);
+		} catch (TransportNotSupportedException
+				 | TooManyListenersException
+				 | ObjectInUseException
+				 | InvalidArgumentException e) {
+			log.error("[SIP SERVER] udp://{}:{} SIP服务启动失败,请检查端口是否被占用或者ip是否正确"
+					, monitorIp, port);
+		}
+	}
+
+	public SipProviderImpl getUdpSipProvider(String ip) {
+		if (udpSipProviderMap.size() == 1) {
+			return udpSipProviderMap.values().stream().findFirst().get();
+		}
+		if (ObjectUtils.isEmpty(ip)) {
+			return null;
+		}
+		return udpSipProviderMap.get(ip);
+	}
+
+	public SipProviderImpl getUdpSipProvider() {
+		if (udpSipProviderMap.size() != 1) {
+			return null;
+		}
+		return udpSipProviderMap.values().stream().findFirst().get();
+	}
+
+	public SipProviderImpl getTcpSipProvider() {
+		if (tcpSipProviderMap.size() != 1) {
+			return null;
+		}
+		return tcpSipProviderMap.values().stream().findFirst().get();
+	}
+
+	public SipProviderImpl getTcpSipProvider(String ip) {
+		if (tcpSipProviderMap.size() == 1) {
+			return tcpSipProviderMap.values().stream().findFirst().get();
+		}
+		if (ObjectUtils.isEmpty(ip)) {
+			return null;
+		}
+		return tcpSipProviderMap.get(ip);
+	}
+
+	public String getLocalIp(String deviceLocalIp) {
+		if (monitorIps.size() == 1) {
+			return monitorIps.get(0);
+		}
+		if (!ObjectUtils.isEmpty(deviceLocalIp)) {
+			return deviceLocalIp;
+		}
+		return getUdpSipProvider().getListeningPoint().getIPAddress();
+	}
+}

+ 235 - 0
video-service/src/main/java/com/ruoyi/wvp/gb28181/auth/DigestServerAuthenticationHelper.java

@@ -0,0 +1,235 @@
+/*
+ * 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 com.ruoyi.wvp.gb28181.auth;
+
+import gov.nist.core.InternalErrorHandler;
+import lombok.extern.slf4j.Slf4j;
+
+import javax.sip.address.URI;
+import javax.sip.header.AuthorizationHeader;
+import javax.sip.header.HeaderFactory;
+import javax.sip.header.WWWAuthenticateHeader;
+import javax.sip.message.Request;
+import javax.sip.message.Response;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.time.Instant;
+import java.util.Random;
+
+/**
+ * Implements the HTTP digest authentication method server side functionality.
+ *
+ * @author M. Ranganathan
+ * @author Marc Bednarek
+ */
+@Slf4j
+public class DigestServerAuthenticationHelper  {
+
+    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 DigestServerAuthenticationHelper()
+            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() {
+        long time = Instant.now().toEpochMilli();
+        Random rand = new Random();
+        long pad = rand.nextLong();
+        String nonceString = Long.valueOf(time).toString()
+                + Long.valueOf(pad).toString();
+        byte mdbytes[] = messageDigest.digest(nonceString.getBytes());
+        return toHexString(mdbytes);
+    }
+
+    public Response generateChallenge(HeaderFactory headerFactory, Response response, String realm) {
+        try {
+            WWWAuthenticateHeader proxyAuthenticate = headerFactory
+                    .createWWWAuthenticateHeader(DEFAULT_SCHEME);
+            proxyAuthenticate.setParameter("realm", realm);
+            proxyAuthenticate.setParameter("qop", "auth");
+            proxyAuthenticate.setParameter("nonce", generateNonce());
+            proxyAuthenticate.setParameter("algorithm", DEFAULT_ALGORITHM);
+
+            response.setHeader(proxyAuthenticate);
+        } catch (Exception ex) {
+            InternalErrorHandler.handleException(ex);
+        }
+        return response;
+    }
+    /**
+     * 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) {
+        AuthorizationHeader authHeader = (AuthorizationHeader) 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 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) {
+        AuthorizationHeader authHeader = (AuthorizationHeader) request.getHeader(AuthorizationHeader.NAME);
+        if ( authHeader == null || authHeader.getRealm() == null) {
+            return false;
+        }
+        String realm = authHeader.getRealm().trim();
+        String username = authHeader.getUsername().trim();
+
+        if ( username == null || realm == null ) {
+            return false;
+        }
+
+        String nonce = authHeader.getNonce();
+        URI uri = authHeader.getURI();
+        if (uri == null) {
+            return false;
+        }
+        // qop 保护质量 包含auth(默认的)和auth-int(增加了报文完整性检测)两种策略
+        String qop = authHeader.getQop();
+
+        // 客户端随机数,这是一个不透明的字符串值,由客户端提供,并且客户端和服务器都会使用,以避免用明文文本。
+        // 这使得双方都可以查验对方的身份,并对消息的完整性提供一些保护
+        String cnonce = authHeader.getCNonce();
+
+        // nonce计数器,是一个16进制的数值,表示同一nonce下客户端发送出请求的数量
+        int nc = authHeader.getNonceCount();
+        String ncStr = String.format("%08x", nc).toUpperCase();
+        // String ncStr = new DecimalFormat("00000000").format(nc);
+        // String ncStr = new DecimalFormat("00000000").format(Integer.parseInt(nc + "", 16));
+
+        String A1 = username + ":" + realm + ":" + pass;
+
+        String A2 = request.getMethod().toUpperCase() + ":" + uri.toString();
+
+        byte mdbytes[] = messageDigest.digest(A1.getBytes());
+        String HA1 = toHexString(mdbytes);
+        log.debug("A1: " + A1);
+        log.debug("A2: " + A2);
+        mdbytes = messageDigest.digest(A2.getBytes());
+        String HA2 = toHexString(mdbytes);
+        log.debug("HA1: " + HA1);
+        log.debug("HA2: " + HA2);
+        // String cnonce = authHeader.getCNonce();
+        log.debug("nonce: " + nonce);
+        log.debug("nc: " + ncStr);
+        log.debug("cnonce: " + cnonce);
+        log.debug("qop: " + qop);
+        String KD = HA1 + ":" + nonce;
+
+        if (qop != null && qop.equalsIgnoreCase("auth") ) {
+            if (nc != -1) {
+                KD += ":" + ncStr;
+            }
+            if (cnonce != null) {
+                KD += ":" + cnonce;
+            }
+            KD += ":" + qop;
+        }
+        KD += ":" + HA2;
+        log.debug("KD: " + KD);
+        mdbytes = messageDigest.digest(KD.getBytes());
+        String mdString = toHexString(mdbytes);
+        log.debug("mdString: " + mdString);
+        String response = authHeader.getResponse();
+        log.debug("response: " + response);
+        return mdString.equals(response);
+
+    }
+
+}

+ 24 - 0
video-service/src/main/java/com/ruoyi/wvp/gb28181/bean/BaiduPoint.java

@@ -0,0 +1,24 @@
+package com.ruoyi.wvp.gb28181.bean;
+
+public class BaiduPoint {
+
+    String bdLng;
+
+    String bdLat;
+
+    public String getBdLng() {
+        return bdLng;
+    }
+
+    public void setBdLng(String bdLng) {
+        this.bdLng = bdLng;
+    }
+
+    public String getBdLat() {
+        return bdLat;
+    }
+
+    public void setBdLat(String bdLat) {
+        this.bdLat = bdLat;
+    }
+}

+ 37 - 0
video-service/src/main/java/com/ruoyi/wvp/gb28181/bean/CatalogChannelEvent.java

@@ -0,0 +1,37 @@
+package com.ruoyi.wvp.gb28181.bean;
+
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.extern.slf4j.Slf4j;
+import org.dom4j.Element;
+
+import java.lang.reflect.InvocationTargetException;
+
+@Data
+@Slf4j
+@EqualsAndHashCode(callSuper = true)
+public class CatalogChannelEvent extends DeviceChannel{
+
+    private String event;
+
+    private DeviceChannel channel;
+
+    public static CatalogChannelEvent decode(Element element) throws InvocationTargetException, NoSuchMethodException, InstantiationException, IllegalAccessException {
+        Element eventElement = element.element("Event");
+        CatalogChannelEvent catalogChannelEvent = new CatalogChannelEvent();
+        if (eventElement != null) {
+            catalogChannelEvent.setEvent(eventElement.getText());
+        }else {
+            catalogChannelEvent.setEvent("ADD");
+        }
+        DeviceChannel deviceChannel;
+        if ("ADD".equalsIgnoreCase(catalogChannelEvent.getEvent()) ||
+        "UPDATE".equalsIgnoreCase(catalogChannelEvent.getEvent()) ){
+            deviceChannel = DeviceChannel.decode(element);
+        }else {
+            deviceChannel = DeviceChannel.decodeWithOnlyDeviceId(element);
+        }
+        catalogChannelEvent.setChannel(deviceChannel);
+        return catalogChannelEvent;
+    }
+}

+ 30 - 0
video-service/src/main/java/com/ruoyi/wvp/gb28181/bean/CatalogData.java

@@ -0,0 +1,30 @@
+package com.ruoyi.wvp.gb28181.bean;
+
+import lombok.Data;
+
+import java.time.Instant;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * @author lin
+ */
+@Data
+public class CatalogData {
+    /**
+     * 命令序列号
+     */
+    private int sn;
+    private Integer total;
+    private Instant time;
+    private Device device;
+    private String errorMsg;
+    private Set<String> redisKeysForChannel = new HashSet<>();
+    private Set<String> errorChannel = new HashSet<>();
+
+    public enum CatalogDataStatus{
+        ready, runIng, end
+    }
+    private CatalogDataStatus status;
+
+}

+ 8 - 0
video-service/src/main/java/com/ruoyi/wvp/gb28181/bean/CmdType.java

@@ -0,0 +1,8 @@
+package com.ruoyi.wvp.gb28181.bean;
+
+public class CmdType {
+
+    public static final String CATALOG = "Catalog";
+    public static final String ALARM = "Alarm";
+    public static final String MOBILE_POSITION = "MobilePosition";
+}

+ 411 - 0
video-service/src/main/java/com/ruoyi/wvp/gb28181/bean/CommonGBChannel.java

@@ -0,0 +1,411 @@
+package com.ruoyi.wvp.gb28181.bean;
+
+
+import lombok.Data;
+
+/**
+ * 国标通道
+ */
+@Data
+public class CommonGBChannel {
+
+    /**
+     * 国标-数据库自增ID
+     */
+    private int gbId;
+
+    /**
+     * 国标-编码
+     */
+    private String gbDeviceId;
+
+    /**
+     * 国标-名称
+     */
+    private String gbName;
+
+    /**
+     * 国标-设备厂商
+     */
+    private String gbManufacturer;
+
+    /**
+     * 国标-设备型号
+     */
+    private String gbModel;
+
+    /**
+     * 国标-设备归属
+     */
+    private String gbOwner;
+
+    /**
+     * 国标-行政区域
+     */
+    private String gbCivilCode;
+
+    /**
+     * 国标-警区
+     */
+    private String gbBlock;
+
+    /**
+     * 国标-安装地址
+     */
+    private String gbAddress;
+
+    /**
+     * 国标-是否有子设备
+     */
+    private Integer gbParental;
+
+    /**
+     * 国标-父节点ID
+     */
+    private String gbParentId;
+
+    /**
+     * 国标-信令安全模式
+     */
+    private Integer gbSafetyWay;
+
+    /**
+     * 国标-注册方式
+     */
+    private Integer gbRegisterWay;
+
+    /**
+     * 国标-证书序列号
+     */
+    private String gbCertNum;
+
+    /**
+     * 国标-证书有效标识
+     */
+    private Integer gbCertifiable;
+
+    /**
+     * 国标-无效原因码(有证书且证书无效的设备必选)
+     */
+    private Integer gbErrCode;
+
+    /**
+     * 国标-证书终止有效期(有证书且证书无效的设备必选)
+     */
+    private String gbEndTime;
+
+    /**
+     * 国标-保密属性(必选)缺省为0;0-不涉密,1-涉密
+     */
+    private Integer gbSecrecy;
+
+    /**
+     * 国标-设备/系统IPv4/IPv6地址
+     */
+    private String gbIpAddress;
+
+    /**
+     * 国标-设备/系统端口
+     */
+    private Integer gbPort;
+
+    /**
+     * 国标-设备口令
+     */
+    private String gbPassword;
+
+    /**
+     * 国标-设备状态
+     */
+    private String gbStatus;
+
+    /**
+     * 国标-经度 WGS-84坐标系
+     */
+    private Double gbLongitude;
+
+    /**
+     * 国标-纬度 WGS-84坐标系
+     */
+    private Double gbLatitude;
+
+    /**
+     * 国标-虚拟组织所属的业务分组ID
+     */
+    private String gbBusinessGroupId;
+
+    /**
+     * 国标-摄像机结构类型,标识摄像机类型: 1-球机; 2-半球; 3-固定枪机; 4-遥控枪机;5-遥控半球;6-多目设备的全景/拼接通道;7-多目设备的分割通道
+     */
+    private Integer gbPtzType;
+
+    /**
+     * 摄像机位置类型扩展。1-省际检查站、2-党政机关、3-车站码头、4-中心广场、5-体育场馆、6-商业中心、7-宗教场所、
+     * 8-校园周边、9-治安复杂区域、10-交通干线。当目录项为摄像机时可选。
+     */
+    private Integer gbPositionType;
+
+    /**
+     * 国标-摄像机安装位置室外、室内属性。1-室外、2-室内。
+     */
+    private Integer gbRoomType;
+
+    /**
+     * 国标-用途属性
+     */
+    private Integer gbUseType;
+
+    /**
+     * 国标-摄像机补光属性。1-无补光;2-红外补光;3-白光补光;4-激光补光;9-其他
+     */
+    private Integer gbSupplyLightType;
+
+    /**
+     * 国标-摄像机监视方位(光轴方向)属性。1-东(西向东)、2-西(东向西)、3-南(北向南)、4-北(南向北)、
+     * 5-东南(西北到东南)、6-东北(西南到东北)、7-西南(东北到西南)、8-西北(东南到西北)
+     */
+    private Integer gbDirectionType;
+
+    /**
+     * 国标-摄像机支持的分辨率,可多值
+     */
+    private String gbResolution;
+
+    /**
+     * 国标-下载倍速(可选),可多值
+     */
+    private String gbDownloadSpeed;
+
+    /**
+     * 国标-空域编码能力,取值0-不支持;1-1级增强(1个增强层);2-2级增强(2个增强层);3-3级增强(3个增强层)
+     */
+    private Integer gbSvcSpaceSupportMod;
+
+    /**
+     * 国标-时域编码能力,取值0-不支持;1-1级增强;2-2级增强;3-3级增强(可选)
+     */
+    private Integer gbSvcTimeSupportMode;
+
+    /**
+     * 二进制保存的录制计划, 每一位表示每个小时的前半个小时
+     */
+    private Long recordPLan;
+
+    /**
+     * 关联的数据类型
+     */
+    private Integer dataType;
+
+    /**
+     * 关联的设备ID
+     */
+    private Integer dataDeviceId;
+
+    /**
+     * 创建时间
+     */
+    private String createTime;
+
+    /**
+     * 更新时间
+     */
+    private String updateTime;
+
+    public String encode(String serverDeviceId) {
+        return encode(null, serverDeviceId);
+    }
+
+    public String encode(String event,String serverDeviceId) {
+        String content;
+        if (event == null) {
+            return getFullContent(null, serverDeviceId);
+        }
+        switch (event) {
+            case "DEL":
+            case "DEFECT":
+            case "VLOST":
+                content = "<Item>\n" +
+                        "<DeviceID>" + this.getGbDeviceId() + "</DeviceID>\n" +
+                        "<Event>" + event + "</Event>\n" +
+                        "</Item>\n";
+                break;
+            case "ON":
+            case "OFF":
+                content = "<Item>\n" +
+                        "<DeviceID>" + this.getGbDeviceId() + "</DeviceID>\n" +
+                        "<Event>" + event + "</Event>\r\n" +
+                        "</Item>\n";
+                break;
+            case "ADD":
+            case "UPDATE":
+                content = getFullContent(event, serverDeviceId);
+                break;
+            default:
+                content = null;
+                break;
+        }
+        return content;
+    }
+
+    private String getFullContent(String event, String serverDeviceId) {
+        StringBuilder content = new StringBuilder();
+        // 行政区划目录项
+        content.append("<Item>\n")
+                .append("<DeviceID>" + this.getGbDeviceId() + "</DeviceID>\n")
+                .append("<Name>" + this.getGbName() + "</Name>\n");
+
+
+        if (this.getGbDeviceId().length() > 8) {
+
+            String type = this.getGbDeviceId().substring(10, 13);
+            if (type.equals("200")) {
+                // 业务分组目录项
+                if (this.getGbManufacturer() != null) {
+                    content.append("<Manufacturer>" + this.getGbManufacturer() + "</Manufacturer>\n");
+                }
+                if (this.getGbModel() != null) {
+                    content.append("<Model>" + this.getGbModel() + "</Model>\n");
+                }
+                if (this.getGbOwner() != null) {
+                    content.append("<Owner>" + this.getGbOwner() + "</Owner>\n");
+                }
+                if (this.getGbCivilCode() != null) {
+                    content.append("<CivilCode>" + this.getGbCivilCode() + "</CivilCode>\n");
+                }
+                if (this.getGbAddress() != null) {
+                    content.append("<Address>" + this.getGbAddress() + "</Address>\n");
+                }
+                if (this.getGbRegisterWay() != null) {
+                    content.append("<RegisterWay>" + this.getGbRegisterWay() + "</RegisterWay>\n");
+                }
+                if (this.getGbSecrecy() != null) {
+                    content.append("<Secrecy>" + this.getGbSecrecy() + "</Secrecy>\n");
+                }
+            } else if (type.equals("215")) {
+                // 业务分组
+                if (this.getGbCivilCode() != null) {
+                    content.append("<CivilCode>" + this.getGbCivilCode() + "</CivilCode>\n");
+                }
+                content.append("<ParentID>" + serverDeviceId + "</ParentID>\n");
+            } else if (type.equals("216")) {
+                // 虚拟组织目录项
+                if (this.getGbCivilCode() != null) {
+                    content.append("<CivilCode>" + this.getGbCivilCode() + "</CivilCode>\n");
+                }
+                if (this.getGbParentId() != null) {
+                    content.append("<ParentID>" + this.getGbParentId() + "</ParentID>\n");
+                }
+                content.append("<BusinessGroupID>" + this.getGbBusinessGroupId() + "</BusinessGroupID>\n");
+            } else {
+                if (this.getGbManufacturer() != null) {
+                    content.append("<Manufacturer>" + this.getGbManufacturer() + "</Manufacturer>\n");
+                }
+                if (this.getGbModel() != null) {
+                    content.append("<Model>" + this.getGbModel() + "</Model>\n");
+                }
+                if (this.getGbOwner() != null) {
+                    content.append("<Owner>" + this.getGbOwner() + "</Owner>\n");
+                }
+                if (this.getGbCivilCode() != null) {
+                    content.append("<CivilCode>" + this.getGbCivilCode() + "</CivilCode>\n");
+                }
+                if (this.getGbAddress() != null) {
+                    content.append("<Address>" + this.getGbAddress() + "</Address>\n");
+                }
+                if (this.getGbRegisterWay() != null) {
+                    content.append("<RegisterWay>" + this.getGbRegisterWay() + "</RegisterWay>\n");
+                }
+                if (this.getGbSecrecy() != null) {
+                    content.append("<Secrecy>" + this.getGbSecrecy() + "</Secrecy>\n");
+                }
+                if (this.getGbParentId() != null) {
+                    content.append("<ParentID>" + this.getGbParentId() + "</ParentID>\n");
+                }
+                if (this.getGbParental() != null) {
+                    content.append("<Parental>" + this.getGbParental() + "</Parental>\n");
+                }
+                if (this.getGbSafetyWay() != null) {
+                    content.append("<SafetyWay>" + this.getGbSafetyWay() + "</SafetyWay>\n");
+                }
+                if (this.getGbRegisterWay() != null) {
+                    content.append("<RegisterWay>" + this.getGbRegisterWay() + "</RegisterWay>\n");
+                }
+                if (this.getGbCertNum() != null) {
+                    content.append("<CertNum>" + this.getGbCertNum() + "</CertNum>\n");
+                }
+                if (this.getGbCertifiable() != null) {
+                    content.append("<Certifiable>" + this.getGbCertifiable() + "</Certifiable>\n");
+                }
+                if (this.getGbErrCode() != null) {
+                    content.append("<ErrCode>" + this.getGbErrCode() + "</ErrCode>\n");
+                }
+                if (this.getGbEndTime() != null) {
+                    content.append("<EndTime>" + this.getGbEndTime() + "</EndTime>\n");
+                }
+                if (this.getGbSecrecy() != null) {
+                    content.append("<Secrecy>" + this.getGbSecrecy() + "</Secrecy>\n");
+                }
+                if (this.getGbIpAddress() != null) {
+                    content.append("<IPAddress>" + this.getGbIpAddress() + "</IPAddress>\n");
+                }
+                if (this.getGbPort() != null) {
+                    content.append("<Port>" + this.getGbPort() + "</Port>\n");
+                }
+                if (this.getGbPassword() != null) {
+                    content.append("<Password>" + this.getGbPassword() + "</Password>\n");
+                }
+                if (this.getGbStatus() != null) {
+                    content.append("<Status>" + this.getGbStatus() + "</Status>\n");
+                }
+                if (this.getGbLongitude() != null) {
+                    content.append("<Longitude>" + this.getGbLongitude() + "</Longitude>\n");
+                }
+                if (this.getGbLatitude() != null) {
+                    content.append("<Latitude>" + this.getGbLatitude() + "</Latitude>\n");
+                }
+                content.append("<Info>\n");
+
+                if (this.getGbPtzType() != null) {
+                    content.append("  <PTZType>" + this.getGbPtzType() + "</PTZType>\n");
+                }
+                if (this.getGbPositionType() != null) {
+                    content.append("  <PositionType>" + this.getGbPositionType() + "</PositionType>\n");
+                }
+                if (this.getGbRoomType() != null) {
+                    content.append("  <RoomType>" + this.getGbRoomType() + "</RoomType>\n");
+                }
+                if (this.getGbUseType() != null) {
+                    content.append("  <UseType>" + this.getGbUseType() + "</UseType>\n");
+                }
+                if (this.getGbSupplyLightType() != null) {
+                    content.append("  <SupplyLightType>" + this.getGbSupplyLightType() + "</SupplyLightType>\n");
+                }
+                if (this.getGbDirectionType() != null) {
+                    content.append("  <DirectionType>" + this.getGbDirectionType() + "</DirectionType>\n");
+                }
+                if (this.getGbResolution() != null) {
+                    content.append("  <Resolution>" + this.getGbResolution() + "</Resolution>\n");
+                }
+                if (this.getGbBusinessGroupId() != null) {
+                    content.append("  <BusinessGroupID>" + this.getGbBusinessGroupId() + "</BusinessGroupID>\n");
+                }
+                if (this.getGbDownloadSpeed() != null) {
+                    content.append("  <DownloadSpeed>" + this.getGbDownloadSpeed() + "</DownloadSpeed>\n");
+                }
+                if (this.getGbSvcSpaceSupportMod() != null) {
+                    content.append("  <SVCSpaceSupportMode>" + this.getGbSvcSpaceSupportMod() + "</SVCSpaceSupportMode>\n");
+                }
+                if (this.getGbSvcTimeSupportMode() != null) {
+                    content.append("  <SVCTimeSupportMode>" + this.getGbSvcTimeSupportMode() + "</SVCTimeSupportMode>\n");
+                }
+                content.append("</Info>\n");
+            }
+        }
+        if (event != null) {
+            content.append("<Event>" + event + "</Event>\n");
+        }
+        content.append("</Item>\n");
+        return content.toString();
+    }
+
+}

+ 235 - 0
video-service/src/main/java/com/ruoyi/wvp/gb28181/bean/Device.java

@@ -0,0 +1,235 @@
+package com.ruoyi.wvp.gb28181.bean;
+
+
+import com.zksy.common.core.domain.WvpBaseEntity;
+import lombok.Data;
+
+/**
+ * 国标设备/平台
+ *
+ * @author lin
+ */
+@Data
+public class Device extends WvpBaseEntity {
+
+    /**
+     * 当前页码
+     */
+    private int pageNum;
+
+    /**
+     * 每页大小
+     */
+    private int pageSize;
+
+    /**
+     * 部门ID
+     */
+    private Long deptId;
+
+    /**
+     * 部门名称
+     */
+    private String deptName;
+
+    /**
+     * 国标-数据库自增ID
+     */
+    private int id;
+
+    /**
+     * 设备国标编号
+     */
+    private String deviceId;
+
+    /**
+     * 设备名
+     */
+    private String name;
+
+    /**
+     * 生产厂商
+     */
+    private String manufacturer;
+
+    /**
+     * 型号
+     */
+    private String model;
+
+    /**
+     * 固件版本
+     */
+    private String firmware;
+
+    /**
+     * 在线状态
+     */
+    private String status;
+
+    /**
+     * 传输协议
+     * UDP/TCP
+     */
+    private String transport;
+
+    /**
+     * 数据流传输模式
+     * UDP:udp传输
+     * TCP-ACTIVE:tcp主动模式
+     * TCP-PASSIVE:tcp被动模式
+     */
+    private String streamMode;
+
+    /**
+     * wan地址_ip
+     */
+    private String ip;
+
+    /**
+     * wan地址_port
+     */
+    private int port;
+
+    /**
+     * wan地址
+     */
+    private String hostAddress;
+
+    /**
+     * 在线
+     * 是否在线,true为在线,false为离线
+     */
+    private boolean onLine;
+
+    /**
+     * 注册时间
+     */
+    private String registerTime;
+
+    /**
+     * 心跳时间
+     */
+    private String keepaliveTime;
+
+    /**
+     * 心跳间隔
+     */
+    private Integer heartBeatInterval;
+
+    /**
+     * 心跳超时次数
+     */
+    private Integer heartBeatCount;
+
+    /**
+     * 定位功能支持情况
+     * 取值:0-不支持;1-支持 GPS定位;2-支持北斗定位(可选,默认取值为0)
+     */
+    private Integer positionCapability;
+
+    /**
+     * 通道个数
+     */
+    private int channelCount;
+
+    /**
+     * 注册有效期
+     */
+    private int expires;
+
+    /**
+     * 创建时间
+     */
+    private String createTime;
+
+    /**
+     * 更新时间
+     */
+    private String updateTime;
+
+    /**
+     * 设备使用的媒体id, 默认为null
+     */
+    private String mediaServerId;
+
+    /**
+     * 字符集, 支持 UTF-8 与 GB2312
+     */
+    private String charset;
+
+    /**
+     * 目录订阅周期,0为不订阅
+     */
+    private int subscribeCycleForCatalog;
+
+    /**
+     * 移动设备位置订阅周期,0为不订阅
+     */
+    private int subscribeCycleForMobilePosition;
+
+    /**
+     * 移动设备位置信息上报时间间隔,单位:秒,默认值5
+     */
+    private int mobilePositionSubmissionInterval = 5;
+
+    /**
+     * 报警订阅周期,0为不订阅
+     */
+    private int subscribeCycleForAlarm;
+
+    /**
+     * 是否开启ssrc校验,默认关闭,开启可以防止串流
+     */
+    private boolean ssrcCheck = false;
+
+    /**
+     * 地理坐标系, 目前支持 WGS84,GCJ02, 此字段保留,暂无用
+     */
+    private String geoCoordSys;
+
+    /**
+     * 密码
+     */
+    private String password;
+
+    /**
+     * 收流IP
+     */
+    private String sdpIp;
+
+    /**
+     * SIP交互IP(设备访问平台的IP)
+     */
+    private String localIp;
+
+    /**
+     * 是否作为消息通道
+     */
+    private boolean asMessageChannel;
+
+    /**
+     * 设备注册的事务信息
+     */
+    private SipTransactionInfo sipTransactionInfo;
+
+    /**
+     * 控制语音对讲流程,释放收到ACK后发流
+     */
+    private boolean broadcastPushAfterAck;
+
+    /**
+     * 纬度
+     */
+    private String lat;
+
+    /**
+     * 经度
+     */
+    private String lng;
+
+    /**
+     * 地图定位地址
+     */
+    private String addressMap;
+}

+ 322 - 0
video-service/src/main/java/com/ruoyi/wvp/gb28181/bean/DeviceChannel.java

@@ -0,0 +1,322 @@
+package com.ruoyi.wvp.gb28181.bean;
+
+import com.ruoyi.wvp.common.enums.ChannelDataType;
+import com.ruoyi.wvp.gb28181.utils.MessageElementForCatalog;
+import com.ruoyi.wvp.gb28181.utils.XmlUtil;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.extern.slf4j.Slf4j;
+import org.dom4j.Element;
+import org.springframework.util.ObjectUtils;
+
+import java.lang.reflect.InvocationTargetException;
+
+/**
+ * 通道信息
+ */
+@Data
+@Slf4j
+@EqualsAndHashCode(callSuper = true)
+public class DeviceChannel extends CommonGBChannel {
+
+    /**
+     * 国标-数据库自增ID
+     */
+    private int id;
+
+    /**
+     * 国标-编码
+     */
+    @MessageElementForCatalog("DeviceID")
+    private String deviceId;
+
+    /**
+     * 国标-名称
+     */
+    @MessageElementForCatalog("Name")
+    private String name;
+
+    /**
+     * 国标-设备厂商
+     */
+    @MessageElementForCatalog("Manufacturer")
+    private String manufacturer;
+
+    /**
+     * 国标-设备型号
+     */
+    @MessageElementForCatalog("Model")
+    private String model;
+
+    /**
+     * 国标-设备归属
+     */
+    @MessageElementForCatalog("Owner")
+    private String owner;
+
+    /**
+     * 国标-行政区域
+     */
+    @MessageElementForCatalog("CivilCode")
+    private String civilCode;
+
+    /**
+     * 国标-警区
+     */
+    @MessageElementForCatalog("Block")
+    private String block;
+
+    /**
+     * 国标-安装地址
+     */
+    @MessageElementForCatalog("Address")
+    private String address;
+
+    /**
+     * 国标-是否有子设备(必选)1有,0没有
+     */
+    @MessageElementForCatalog("Parental")
+    private Integer parental;
+
+    /**
+     * 国标-父节点ID
+     */
+    @MessageElementForCatalog("ParentID")
+    private String parentId;
+
+    /**
+     * 国标-信令安全模式
+     */
+    @MessageElementForCatalog("SafetyWay")
+    private Integer safetyWay;
+
+    /**
+     * 国标-注册方式
+     */
+    @MessageElementForCatalog("RegisterWay")
+    private Integer registerWay;
+
+    /**
+     * 国标-证书序列号
+     */
+    @MessageElementForCatalog("CertNum")
+    private String certNum;
+
+    /**
+     * 国标-证书有效标识, 缺省为0;证书有效标识:0:无效 1:有效
+     */
+    @MessageElementForCatalog("Certifiable")
+    private Integer certifiable;
+
+    /**
+     * 国标-无效原因码(有证书且证书无效的设备必选)
+     */
+    @MessageElementForCatalog("ErrCode")
+    private Integer errCode;
+
+    /**
+     * 国标-证书终止有效期(有证书且证书无效的设备必选)
+     */
+    @MessageElementForCatalog("EndTime")
+    private String endTime;
+
+    /**
+     * 国标-保密属性(必选)缺省为0;0-不涉密,1-涉密
+     */
+    @MessageElementForCatalog("Secrecy")
+    private Integer secrecy;
+
+    /**
+     * 国标-设备/系统IPv4/IPv6地址
+     */
+    @MessageElementForCatalog("IPAddress")
+    private String ipAddress;
+
+    /**
+     * 国标-设备/系统端口
+     */
+    @MessageElementForCatalog("Port")
+    private Integer port;
+
+    /**
+     * 国标-设备口令
+     */
+    @MessageElementForCatalog("Password")
+    private String password;
+
+    /**
+     * 国标-设备状态
+     */
+    @MessageElementForCatalog("Status")
+    private String status;
+
+    /**
+     * 国标-经度 WGS-84坐标系
+     */
+    @MessageElementForCatalog("Longitude")
+    private Double longitude;
+
+    /**
+     * 国标-纬度 WGS-84坐标系
+     */
+    @MessageElementForCatalog("Latitude")
+    private Double latitude;
+
+    /**
+     * 国标-摄像机结构类型,标识摄像机类型: 1-球机; 2-半球; 3-固定枪机; 4-遥控枪机;5-遥控半球;6-多目设备的全景/拼接通道;7-多目设备的分割通道
+     */
+    @MessageElementForCatalog("Info.PTZType")
+    private Integer ptzType;
+
+    /**
+     * 国标-摄像机位置类型扩展。1-省际检查站、2-党政机关、3-车站码头、4-中心广场、5-体育场馆、6-商业中心、7-宗教场所、8-校园周边、9-治安复杂区域、10-交通干线
+     */
+    @MessageElementForCatalog("Info.PositionType")
+    private Integer positionType;
+
+    /**
+     * 国标-摄像机安装位置室外、室内属性。1-室外、2-室内。
+     */
+    @MessageElementForCatalog("Info.RoomType")
+    private Integer roomType;
+
+    /**
+     * 国标-用途属性, 1-治安、2-交通、3-重点。
+     */
+    @MessageElementForCatalog("Info.UseType")
+    private Integer useType;
+
+    /**
+     * 国标-摄像机补光属性。1-无补光;2-红外补光;3-白光补光;4-激光补光;9-其他
+     */
+    @MessageElementForCatalog("Info.SupplyLightType")
+    private Integer supplyLightType;
+
+    /**
+     * 国标-摄像机监视方位(光轴方向)属性。1-东(西向东)、2-西(东向西)、3-南(北向南)、4-北(南向北)、5-东南(西北到东南)、6-东北(西南到东北)、7-西南(东北到西南)、8-西北(东南到西北)
+     */
+    @MessageElementForCatalog("Info.DirectionType")
+    private Integer directionType;
+
+    /**
+     * 国标-摄像机支持的分辨率,可多值
+     */
+    @MessageElementForCatalog("Info.Resolution")
+    private String resolution;
+
+    /**
+     * 国标-虚拟组织所属的业务分组ID
+     */
+    @MessageElementForCatalog({"BusinessGroupID", "Info.BusinessGroupID"})
+    private String businessGroupId;
+
+    /**
+     * 国标-下载倍速(可选),可多值
+     */
+    @MessageElementForCatalog("Info.DownloadSpeed")
+    private String downloadSpeed;
+
+    /**
+     * 国标-空域编码能力,取值0-不支持;1-1级增强(1个增强层);2-2级增强(2个增强层);3-3级增强(3个增强层)
+     */
+    @MessageElementForCatalog("Info.SVCSpaceSupportMode")
+    private Integer svcSpaceSupportMod;
+
+    /**
+     * 国标-时域编码能力,取值0-不支持;1-1级增强;2-2级增强;3-3级增强(可选)
+     */
+    @MessageElementForCatalog("Info.SVCTimeSupportMode")
+    private Integer svcTimeSupportMode;
+
+    /**
+     * 国标-云台类型描述字符串
+     */
+    private String ptzTypeText;
+
+    /**
+     * 国标-子设备数
+     */
+    private int subCount;
+
+    /**
+     * 国标-流唯一编号,存在表示正在直播
+     */
+    private String streamId;
+
+    /**
+     * 国标-是否含有音频
+     */
+    private boolean hasAudio;
+
+    /**
+     * 国标-GPS的更新时间
+     */
+    private String gpsTime;
+
+    /**
+     * 国标-码流标识,优先级高于设备中码流标识,用于选择码流时组成码流标识。默认为null,不设置。可选值: stream/streamnumber/streamprofile/streamMode
+     */
+    private String streamIdentification;
+
+    /**
+     * 国标-通道类型, 默认0, 0: 普通通道,1 行政区划 2 业务分组/虚拟组织
+     */
+    private int channelType;
+
+    private Integer dataType = ChannelDataType.GB28181.value;
+
+    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;
+            case 5:
+                this.ptzTypeText = "遥控半球";
+                break;
+            case 6:
+                this.ptzTypeText = "多目设备的全景/拼接通道";
+                break;
+            case 7:
+                this.ptzTypeText = "多目设备的分割通道";
+                break;
+        }
+    }
+
+    public static DeviceChannel decode(Element element) throws InvocationTargetException, NoSuchMethodException, InstantiationException, IllegalAccessException {
+        DeviceChannel deviceChannel = XmlUtil.elementDecode(element, DeviceChannel.class);
+        if (deviceChannel.getCivilCode() != null) {
+            if (ObjectUtils.isEmpty(deviceChannel.getCivilCode())
+                    || deviceChannel.getCivilCode().length() > 8) {
+                deviceChannel.setCivilCode(null);
+            }
+            // 此处对于不在wvp缓存中的行政区划,默认直接存储.保证即使出现wvp的行政区划缓存过老,也可以通过用户自主创建的方式正常使用系统
+        }
+        GbCode gbCode = GbCode.decode(deviceChannel.getDeviceId());
+        if (gbCode != null && "138".equals(gbCode.getTypeCode())) {
+            deviceChannel.setHasAudio(true);
+        }
+        return deviceChannel;
+    }
+
+    public static DeviceChannel decodeWithOnlyDeviceId(Element element) {
+        Element deviceElement = element.element("DeviceID");
+        DeviceChannel deviceChannel = new DeviceChannel();
+        deviceChannel.setDeviceId(deviceElement.getText());
+        return deviceChannel;
+    }
+
+
+}

+ 27 - 0
video-service/src/main/java/com/ruoyi/wvp/gb28181/bean/DeviceNotFoundEvent.java

@@ -0,0 +1,27 @@
+package com.ruoyi.wvp.gb28181.bean;
+
+import javax.sip.Dialog;
+import java.util.EventObject;
+
+public class DeviceNotFoundEvent extends EventObject {
+
+    private String callId;
+
+    /**
+     * Constructs a prototypical Event.
+     *
+     * @param dialog
+     * @throws IllegalArgumentException if source is null.
+     */
+    public DeviceNotFoundEvent(Dialog dialog) {
+        super(dialog);
+    }
+
+    public String getCallId() {
+        return callId;
+    }
+
+    public void setCallId(String callId) {
+        this.callId = callId;
+    }
+}

+ 430 - 0
video-service/src/main/java/com/ruoyi/wvp/gb28181/bean/GBStringMsgParser.java

@@ -0,0 +1,430 @@
+package com.ruoyi.wvp.gb28181.bean;
+
+
+import gov.nist.core.Host;
+import gov.nist.core.HostNameParser;
+import gov.nist.javax.sip.SIPConstants;
+import gov.nist.javax.sip.address.AddressImpl;
+import gov.nist.javax.sip.address.GenericURI;
+import gov.nist.javax.sip.address.SipUri;
+import gov.nist.javax.sip.address.TelephoneNumber;
+import gov.nist.javax.sip.header.*;
+import gov.nist.javax.sip.message.SIPMessage;
+import gov.nist.javax.sip.message.SIPRequest;
+import gov.nist.javax.sip.message.SIPResponse;
+import gov.nist.javax.sip.parser.*;
+import lombok.extern.slf4j.Slf4j;
+
+import java.io.UnsupportedEncodingException;
+import java.text.ParseException;
+
+@Slf4j
+public class GBStringMsgParser implements MessageParser {
+
+    protected static boolean computeContentLengthFromMessage = false;
+
+    /**
+     * @since v0.9
+     */
+    public GBStringMsgParser() {
+        super();
+    }
+
+    /**
+     * Parse a buffer containing a single SIP Message where the body is an array
+     * of un-interpreted bytes. This is intended for parsing the message from a
+     * memory buffer when the buffer. Incorporates a bug fix for a bug that was
+     * noted by Will Sullin of Callcast
+     *
+     * @param msgBuffer a byte buffer containing the messages to be parsed. This can
+     *                  consist of multiple SIP Messages concatenated together.
+     * @return a SIPMessage[] structure (request or response) containing the
+     * parsed SIP message.
+     * @throws ParseException is thrown when an illegal message has been encountered
+     *                        (and the rest of the buffer is discarded).
+     * @see ParseExceptionListener
+     */
+    public SIPMessage parseSIPMessage(byte[] msgBuffer, boolean readBody, boolean strict, ParseExceptionListener parseExceptionListener) throws ParseException {
+        if (msgBuffer == null || msgBuffer.length == 0)
+            return null;
+
+        int i = 0;
+
+        // Squeeze out any leading control character.
+        try {
+            while (msgBuffer[i] < 0x20)
+                i++;
+        } catch (ArrayIndexOutOfBoundsException e) {
+            // Array contains only control char, return null.
+            if (log.isDebugEnabled()) {
+                log.debug("handled only control char so returning null");
+            }
+            return null;
+        }
+
+        // Iterate thru the request/status line and headers.
+        String currentLine = null;
+        String currentHeader = null;
+        boolean isFirstLine = true;
+        SIPMessage message = null;
+        do {
+            int lineStart = i;
+
+            // Find the length of the line.
+            try {
+                while (msgBuffer[i] != '\r' && msgBuffer[i] != '\n')
+                    i++;
+            } catch (ArrayIndexOutOfBoundsException e) {
+                // End of the message.
+                break;
+            }
+            int lineLength = i - lineStart;
+
+            // Make it a String.
+            try {
+                currentLine = new String(msgBuffer, lineStart, lineLength, "UTF-8");
+            } catch (UnsupportedEncodingException e) {
+                throw new ParseException("Bad message encoding!", 0);
+            }
+
+            currentLine = trimEndOfLine(currentLine);
+
+            if (currentLine.length() == 0) {
+                // Last header line, process the previous buffered header.
+                if (currentHeader != null && message != null) {
+                    processHeader(currentHeader, message, parseExceptionListener, msgBuffer);
+                }
+
+            } else {
+                if (isFirstLine) {
+                    message = processFirstLine(currentLine, parseExceptionListener, msgBuffer);
+                } else {
+                    char firstChar = currentLine.charAt(0);
+                    if (firstChar == '\t' || firstChar == ' ') {
+                        if (currentHeader == null)
+                            throw new ParseException("Bad header continuation.", 0);
+
+                        // This is a continuation, append it to the previous line.
+                        currentHeader += currentLine.substring(1);
+                    } else {
+                        if (currentHeader != null && message != null) {
+                            processHeader(currentHeader, message, parseExceptionListener, msgBuffer);
+                        }
+                        currentHeader = currentLine;
+                    }
+                }
+            }
+
+            if (msgBuffer[i] == '\r' && msgBuffer.length > i + 1 && msgBuffer[i + 1] == '\n')
+                i++;
+
+            i++;
+
+            isFirstLine = false;
+        } while (currentLine.length() > 0); // End do - while
+
+        if (message == null) throw new ParseException("Bad message", 0);
+        message.setSize(i);
+
+        // Check for content legth header
+        if (readBody && message.getContentLength() != null) {
+            if (message.getContentLength().getContentLength() != 0) {
+                int bodyLength = msgBuffer.length - i;
+
+                byte[] body = new byte[bodyLength];
+                System.arraycopy(msgBuffer, i, body, 0, bodyLength);
+                message.setMessageContent(body, !strict, computeContentLengthFromMessage, message.getContentLength().getContentLength());
+            } else if (message.getCSeqHeader().getMethod().equalsIgnoreCase("MESSAGE")) {
+                int bodyLength = msgBuffer.length - i;
+
+                byte[] body = new byte[bodyLength];
+                System.arraycopy(msgBuffer, i, body, 0, bodyLength);
+                message.setMessageContent(body, !strict, computeContentLengthFromMessage, bodyLength);
+            } else if (!computeContentLengthFromMessage && strict) {
+                String last4Chars = new String(msgBuffer, msgBuffer.length - 4, 4);
+                if (!"\r\n\r\n".equals(last4Chars)) {
+                    throw new ParseException("Extraneous characters at the end of the message ", i);
+                }
+            }
+        }
+
+        return message;
+    }
+
+    protected static String trimEndOfLine(String line) {
+        if (line == null)
+            return line;
+
+        int i = line.length() - 1;
+        while (i >= 0 && line.charAt(i) <= 0x20)
+            i--;
+
+        if (i == line.length() - 1)
+            return line;
+
+        if (i == -1)
+            return "";
+
+        return line.substring(0, i + 1);
+    }
+
+    protected SIPMessage processFirstLine(String firstLine, ParseExceptionListener parseExceptionListener, byte[] msgBuffer) throws ParseException {
+        SIPMessage message;
+        if (!firstLine.startsWith(SIPConstants.SIP_VERSION_STRING)) {
+            message = new SIPRequest();
+            try {
+                RequestLine requestLine = new RequestLineParser(firstLine + "\n")
+                        .parse();
+                ((SIPRequest) message).setRequestLine(requestLine);
+            } catch (ParseException ex) {
+                if (parseExceptionListener != null)
+                    try {
+                        parseExceptionListener.handleException(ex, message,
+                                RequestLine.class, firstLine, new String(msgBuffer, "UTF-8"));
+                    } catch (UnsupportedEncodingException e) {
+                        e.printStackTrace();
+                    }
+                else
+                    throw ex;
+
+            }
+        } else {
+            message = new SIPResponse();
+            try {
+                StatusLine sl = new StatusLineParser(firstLine + "\n").parse();
+                ((SIPResponse) message).setStatusLine(sl);
+            } catch (ParseException ex) {
+                if (parseExceptionListener != null) {
+                    try {
+                        parseExceptionListener.handleException(ex, message,
+                                StatusLine.class, firstLine, new String(msgBuffer, "UTF-8"));
+                    } catch (UnsupportedEncodingException e) {
+                        e.printStackTrace();
+                    }
+                } else
+                    throw ex;
+
+            }
+        }
+        return message;
+    }
+
+    protected void processHeader(String header, SIPMessage message, ParseExceptionListener parseExceptionListener, byte[] rawMessage) throws ParseException {
+        if (header == null || header.length() == 0)
+            return;
+
+        HeaderParser headerParser = null;
+        try {
+            headerParser = ParserFactory.createParser(header + "\n");
+        } catch (ParseException ex) {
+            // https://java.net/jira/browse/JSIP-456
+            if (parseExceptionListener != null) {
+                parseExceptionListener.handleException(ex, message, null,
+                        header, null);
+                return;
+            } else {
+                throw ex;
+            }
+        }
+
+        try {
+            SIPHeader sipHeader = headerParser.parse();
+            message.attachHeader(sipHeader, false);
+        } catch (ParseException ex) {
+            if (parseExceptionListener != null) {
+                String headerName = Lexer.getHeaderName(header);
+                Class headerClass = NameMap.getClassFromName(headerName);
+                if (headerClass == null) {
+                    headerClass = ExtensionHeaderImpl.class;
+
+                }
+                try {
+                    parseExceptionListener.handleException(ex, message,
+                            headerClass, header, new String(rawMessage, "UTF-8"));
+                } catch (UnsupportedEncodingException e) {
+                    e.printStackTrace();
+                }
+
+            }
+        }
+    }
+
+    /**
+     * Parse an address (nameaddr or address spec) and return and address
+     * structure.
+     *
+     * @param address is a String containing the address to be parsed.
+     * @return a parsed address structure.
+     * @throws ParseException when the address is badly formatted.
+     * @since v1.0
+     */
+    public AddressImpl parseAddress(String address) throws ParseException {
+        AddressParser addressParser = new AddressParser(address);
+        return addressParser.address(true);
+    }
+
+    /**
+     * Parse a host:port and return a parsed structure.
+     *
+     * @param hostport
+     *            is a String containing the host:port to be parsed
+     * @return a parsed address structure.
+     * @since v1.0
+     * @exception throws
+     *                a ParseException when the address is badly formatted.
+     *
+    public HostPort parseHostPort(String hostport) throws ParseException {
+    Lexer lexer = new Lexer("charLexer", hostport);
+    return new HostNameParser(lexer).hostPort();
+
+    }
+     */
+
+    /**
+     * Parse a host name and return a parsed structure.
+     *
+     * @param host is a String containing the host name to be parsed
+     * @return a parsed address structure.
+     * @throws ParseException a ParseException when the hostname is badly formatted.
+     * @since v1.0
+     */
+    public Host parseHost(String host) throws ParseException {
+        Lexer lexer = new Lexer("charLexer", host);
+        return new HostNameParser(lexer).host();
+
+    }
+
+    /**
+     * Parse a telephone number return a parsed structure.
+     *
+     * @param telephone_number is a String containing the telephone # to be parsed
+     * @return a parsed address structure.
+     * @throws ParseException a ParseException when the address is badly formatted.
+     * @since v1.0
+     */
+    public TelephoneNumber parseTelephoneNumber(String telephone_number)
+            throws ParseException {
+        // Bug fix contributed by Will Scullin
+        return new URLParser(telephone_number).parseTelephoneNumber(true);
+
+    }
+
+    /**
+     * Parse a SIP url from a string and return a URI structure for it.
+     *
+     * @param url a String containing the URI structure to be parsed.
+     * @return A parsed URI structure
+     * @throws ParseException if there was an error parsing the message.
+     */
+
+    public SipUri parseSIPUrl(String url) throws ParseException {
+        try {
+            return new URLParser(url).sipURL(true);
+        } catch (ClassCastException ex) {
+            throw new ParseException(url + " Not a SIP URL ", 0);
+        }
+    }
+
+    /**
+     * Parse a uri from a string and return a URI structure for it.
+     *
+     * @param url a String containing the URI structure to be parsed.
+     * @return A parsed URI structure
+     * @throws ParseException if there was an error parsing the message.
+     */
+
+    public GenericURI parseUrl(String url) throws ParseException {
+        return new URLParser(url).parse();
+    }
+
+    /**
+     * Parse an individual SIP message header from a string.
+     *
+     * @param header String containing the SIP header.
+     * @return a SIPHeader structure.
+     * @throws ParseException if there was an error parsing the message.
+     */
+    public static SIPHeader parseSIPHeader(String header) throws ParseException {
+        int start = 0;
+        int end = header.length() - 1;
+        try {
+            // Squeeze out any leading control character.
+            while (header.charAt(start) <= 0x20)
+                start++;
+
+            // Squeeze out any trailing control character.
+            while (header.charAt(end) <= 0x20)
+                end--;
+        } catch (ArrayIndexOutOfBoundsException e) {
+            // Array contains only control char.
+            throw new ParseException("Empty header.", 0);
+        }
+
+        StringBuilder buffer = new StringBuilder(end + 1);
+        int i = start;
+        int lineStart = start;
+        boolean endOfLine = false;
+        while (i <= end) {
+            char c = header.charAt(i);
+            if (c == '\r' || c == '\n') {
+                if (!endOfLine) {
+                    buffer.append(header.substring(lineStart, i));
+                    endOfLine = true;
+                }
+            } else {
+                if (endOfLine) {
+                    endOfLine = false;
+                    if (c == ' ' || c == '\t') {
+                        buffer.append(' ');
+                        lineStart = i + 1;
+                    } else {
+                        lineStart = i;
+                    }
+                }
+            }
+
+            i++;
+        }
+        buffer.append(header.substring(lineStart, i));
+        buffer.append('\n');
+
+        HeaderParser hp = ParserFactory.createParser(buffer.toString());
+        if (hp == null)
+            throw new ParseException("could not create parser", 0);
+        return hp.parse();
+    }
+
+    /**
+     * Parse the SIP Request Line
+     *
+     * @param requestLine a String containing the request line to be parsed.
+     * @return a RequestLine structure that has the parsed RequestLine
+     * @throws ParseException if there was an error parsing the requestLine.
+     */
+
+    public RequestLine parseSIPRequestLine(String requestLine)
+            throws ParseException {
+        requestLine += "\n";
+        return new RequestLineParser(requestLine).parse();
+    }
+
+    /**
+     * Parse the SIP Response message status line
+     *
+     * @param statusLine a String containing the Status line to be parsed.
+     * @return StatusLine class corresponding to message
+     * @throws ParseException if there was an error parsing
+     * @see StatusLine
+     */
+
+    public StatusLine parseSIPStatusLine(String statusLine)
+            throws ParseException {
+        statusLine += "\n";
+        return new StatusLineParser(statusLine).parse();
+    }
+
+    public static void setComputeContentLengthFromMessage(
+            boolean computeContentLengthFromMessage) {
+        GBStringMsgParser.computeContentLengthFromMessage = computeContentLengthFromMessage;
+    }
+}

+ 46 - 0
video-service/src/main/java/com/ruoyi/wvp/gb28181/bean/Gb28181Sdp.java

@@ -0,0 +1,46 @@
+package com.ruoyi.wvp.gb28181.bean;
+
+import javax.sdp.SessionDescription;
+
+/**
+ * 28181 的SDP解析器
+ */
+public class Gb28181Sdp  {
+    private SessionDescription baseSdb;
+    private String ssrc;
+
+    private String mediaDescription;
+
+    public static Gb28181Sdp getInstance(SessionDescription baseSdb, String ssrc, String mediaDescription) {
+        Gb28181Sdp gb28181Sdp = new Gb28181Sdp();
+        gb28181Sdp.setBaseSdb(baseSdb);
+        gb28181Sdp.setSsrc(ssrc);
+        gb28181Sdp.setMediaDescription(mediaDescription);
+        return gb28181Sdp;
+    }
+
+
+    public SessionDescription getBaseSdb() {
+        return baseSdb;
+    }
+
+    public void setBaseSdb(SessionDescription baseSdb) {
+        this.baseSdb = baseSdb;
+    }
+
+    public String getSsrc() {
+        return ssrc;
+    }
+
+    public void setSsrc(String ssrc) {
+        this.ssrc = ssrc;
+    }
+
+    public String getMediaDescription() {
+        return mediaDescription;
+    }
+
+    public void setMediaDescription(String mediaDescription) {
+        this.mediaDescription = mediaDescription;
+    }
+}

+ 56 - 0
video-service/src/main/java/com/ruoyi/wvp/gb28181/bean/GbCode.java

@@ -0,0 +1,56 @@
+package com.ruoyi.wvp.gb28181.bean;
+
+import lombok.Data;
+
+/**
+ * 国标编码对象
+ */
+@Data
+public class GbCode {
+
+    /**
+     * 中心编码,由监控中心所在地的行政区划代码确定,符合GB/T2260—2007的要求
+     */
+    private String centerCode;
+
+    /**
+     * 行业编码
+     */
+    private String industryCode;
+
+    /**
+     * 类型编码
+     */
+    private String typeCode;
+
+    /***
+     * 网络标识
+     */
+    private String netCode;
+
+    /**
+     * 序号
+     */
+    private String sn;
+
+    /**
+     * 解析国标编号
+     */
+    public static GbCode decode(String code){
+        if (code == null || code.trim().length() != 20) {
+            return null;
+        }
+        code = code.trim();
+        GbCode gbCode = new GbCode();
+        gbCode.setCenterCode(code.substring(0, 8));
+        gbCode.setIndustryCode(code.substring(8, 10));
+        gbCode.setTypeCode(code.substring(10, 13));
+        gbCode.setNetCode(code.substring(13, 14));
+        gbCode.setSn(code.substring(14));
+        return gbCode;
+    }
+
+    public String ecode(){
+        return centerCode + industryCode + typeCode + netCode + sn;
+    }
+}

+ 149 - 0
video-service/src/main/java/com/ruoyi/wvp/gb28181/bean/GbSipDate.java

@@ -0,0 +1,149 @@
+package com.ruoyi.wvp.gb28181.bean;
+
+import gov.nist.core.InternalErrorHandler;
+import gov.nist.javax.sip.header.SIPDate;
+
+import java.util.*;
+
+/**
+ * 重写jain sip的SIPDate解决与国标时间格式不一致的问题
+ */
+public class GbSipDate extends SIPDate {
+
+    /**
+     *
+     */
+    private static final long serialVersionUID = 1L;
+
+    private Calendar javaCal;
+
+    public GbSipDate(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);
+    }
+}

+ 21 - 0
video-service/src/main/java/com/ruoyi/wvp/gb28181/bean/GbStringMsgParserFactory.java

@@ -0,0 +1,21 @@
+package com.ruoyi.wvp.gb28181.bean;
+
+import gov.nist.javax.sip.parser.MessageParser;
+import gov.nist.javax.sip.parser.MessageParserFactory;
+import gov.nist.javax.sip.stack.SIPTransactionStack;
+
+public class GbStringMsgParserFactory implements MessageParserFactory {
+
+    /**
+     * msg parser is completely stateless, reuse isntance for the whole stack
+     * fixes https://github.com/RestComm/jain-sip/issues/92
+     */
+    private static GBStringMsgParser msgParser = new GBStringMsgParser();
+    /*
+     * (non-Javadoc)
+     * @see gov.nist.javax.sip.parser.MessageParserFactory#createMessageParser(gov.nist.javax.sip.stack.SIPTransactionStack)
+     */
+    public MessageParser createMessageParser(SIPTransactionStack stack) {
+        return msgParser;
+    }
+}

+ 44 - 0
video-service/src/main/java/com/ruoyi/wvp/gb28181/bean/HandlerCatchData.java

@@ -0,0 +1,44 @@
+package com.ruoyi.wvp.gb28181.bean;
+
+import org.dom4j.Element;
+
+import javax.sip.RequestEvent;
+
+/**
+ * @author lin
+ */
+public class HandlerCatchData {
+    private RequestEvent evt;
+    private Device device;
+    private Element rootElement;
+
+    public HandlerCatchData(RequestEvent evt, Device device, Element rootElement) {
+        this.evt = evt;
+        this.device = device;
+        this.rootElement = rootElement;
+    }
+
+    public RequestEvent getEvt() {
+        return evt;
+    }
+
+    public void setEvt(RequestEvent evt) {
+        this.evt = evt;
+    }
+
+    public Device getDevice() {
+        return device;
+    }
+
+    public void setDevice(Device device) {
+        this.device = device;
+    }
+
+    public Element getRootElement() {
+        return rootElement;
+    }
+
+    public void setRootElement(Element rootElement) {
+        this.rootElement = rootElement;
+    }
+}

+ 35 - 0
video-service/src/main/java/com/ruoyi/wvp/gb28181/bean/Host.java

@@ -0,0 +1,35 @@
+package com.ruoyi.wvp.gb28181.bean;
+
+
+
+public class Host {
+
+	private String ip;
+	private int port;
+	private String address;
+
+
+	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 getAddress() {
+		return address;
+	}
+
+	public void setAddress(String address) {
+		this.address = address;
+	}
+}

+ 22 - 0
video-service/src/main/java/com/ruoyi/wvp/gb28181/bean/InviteInfo.java

@@ -0,0 +1,22 @@
+package com.ruoyi.wvp.gb28181.bean;
+
+import lombok.Data;
+
+// 从INVITE消息中解析需要的信息
+@Data
+public class InviteInfo {
+    private String requesterId;
+    private String targetChannelId;
+    private String sourceChannelId;
+    private String sessionName;
+    private String ssrc;
+    private boolean tcp;
+    private boolean tcpActive;
+    private String callId;
+    private Long startTime;
+    private Long stopTime;
+    private String downloadSpeed;
+    private String ip;
+    private int port;
+
+}

+ 66 - 0
video-service/src/main/java/com/ruoyi/wvp/gb28181/bean/MobilePosition.java

@@ -0,0 +1,66 @@
+package com.ruoyi.wvp.gb28181.bean;
+
+import lombok.Data;
+
+/**
+ * @description: 移动位置bean
+ * @author: lawrencehj
+ * @date: 2021年1月23日
+ */
+
+@Data
+public class MobilePosition {
+    /**
+     * 设备Id
+     */
+    private String deviceId;
+
+    /**
+     * 通道Id
+     */
+    private Integer channelId;
+
+    /**
+     * 设备名称
+     */
+    private String deviceName;
+
+    /**
+     * 通知时间
+     */
+    private String time;
+
+    /**
+     * 经度
+     */
+    private double longitude;
+
+    /**
+     * 纬度
+     */
+    private double latitude;
+
+    /**
+     * 海拔高度
+     */
+    private double altitude;
+
+    /**
+     * 速度
+     */
+    private double speed;
+
+    /**
+     * 方向
+     */
+    private double direction;
+
+    /**
+     * 位置信息上报来源(Mobile Position、GPS Alarm)
+     */
+    private String reportSource;
+    /**
+     * 创建时间
+     */
+    private String createTime;
+}

+ 38 - 0
video-service/src/main/java/com/ruoyi/wvp/gb28181/bean/NotifyCatalogChannel.java

@@ -0,0 +1,38 @@
+package com.ruoyi.wvp.gb28181.bean;
+
+
+public class NotifyCatalogChannel {
+
+    private Type type;
+
+    private DeviceChannel channel;
+
+
+    public enum Type {
+        ADD, DELETE, UPDATE, STATUS_CHANGED
+    }
+
+
+    public static NotifyCatalogChannel getInstance(Type type, DeviceChannel channel) {
+        NotifyCatalogChannel notifyCatalogChannel = new NotifyCatalogChannel();
+        notifyCatalogChannel.setType(type);
+        notifyCatalogChannel.setChannel(channel);
+        return notifyCatalogChannel;
+    }
+
+    public Type getType() {
+        return type;
+    }
+
+    public void setType(Type type) {
+        this.type = type;
+    }
+
+    public DeviceChannel getChannel() {
+        return channel;
+    }
+
+    public void setChannel(DeviceChannel channel) {
+        this.channel = channel;
+    }
+}

+ 12 - 0
video-service/src/main/java/com/ruoyi/wvp/gb28181/bean/OpenRTPServerResult.java

@@ -0,0 +1,12 @@
+package com.ruoyi.wvp.gb28181.bean;
+
+import com.ruoyi.wvp.media.event.hook.HookData;
+import com.ruoyi.wvp.service.bean.SSRCInfo;
+import lombok.Data;
+
+@Data
+public class OpenRTPServerResult {
+
+    private SSRCInfo ssrcInfo;
+    private HookData hookData;
+}

+ 14 - 0
video-service/src/main/java/com/ruoyi/wvp/gb28181/bean/PlayException.java

@@ -0,0 +1,14 @@
+package com.ruoyi.wvp.gb28181.bean;
+
+import lombok.Data;
+
+@Data
+public class PlayException extends RuntimeException{
+    private int code;
+    private String msg;
+
+    public PlayException(int code, String msg) {
+        this.code = code;
+        this.msg = msg;
+    }
+}

+ 12 - 0
video-service/src/main/java/com/ruoyi/wvp/gb28181/bean/Preset.java

@@ -0,0 +1,12 @@
+package com.ruoyi.wvp.gb28181.bean;
+
+
+import lombok.Data;
+
+@Data
+public class Preset {
+
+    private String presetId;
+
+    private String presetName;
+}

+ 112 - 0
video-service/src/main/java/com/ruoyi/wvp/gb28181/bean/RecordInfo.java

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

+ 162 - 0
video-service/src/main/java/com/ruoyi/wvp/gb28181/bean/RecordItem.java

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

+ 27 - 0
video-service/src/main/java/com/ruoyi/wvp/gb28181/bean/RemoteAddressInfo.java

@@ -0,0 +1,27 @@
+package com.ruoyi.wvp.gb28181.bean;
+
+public class RemoteAddressInfo {
+    private String ip;
+    private int port;
+
+    public RemoteAddressInfo(String ip, int port) {
+        this.ip = ip;
+        this.port = port;
+    }
+
+    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;
+    }
+}

+ 73 - 0
video-service/src/main/java/com/ruoyi/wvp/gb28181/bean/SipTransactionInfo.java

@@ -0,0 +1,73 @@
+package com.ruoyi.wvp.gb28181.bean;
+
+import gov.nist.javax.sip.message.SIPResponse;
+
+public class SipTransactionInfo {
+
+    private String callId;
+    private String fromTag;
+    private String toTag;
+    private String viaBranch;
+
+    // 自己是否媒体流发送者
+    private boolean asSender;
+
+    public SipTransactionInfo(SIPResponse response, boolean asSender) {
+        this.callId = response.getCallIdHeader().getCallId();
+        this.fromTag = response.getFromTag();
+        this.toTag = response.getToTag();
+        this.viaBranch = response.getTopmostViaHeader().getBranch();
+        this.asSender = asSender;
+    }
+
+    public SipTransactionInfo(SIPResponse response) {
+        this.callId = response.getCallIdHeader().getCallId();
+        this.fromTag = response.getFromTag();
+        this.toTag = response.getToTag();
+        this.viaBranch = response.getTopmostViaHeader().getBranch();
+        this.asSender = false;
+    }
+
+    public SipTransactionInfo() {
+    }
+
+    public String getCallId() {
+        return callId;
+    }
+
+    public void setCallId(String callId) {
+        this.callId = callId;
+    }
+
+    public String getFromTag() {
+        return fromTag;
+    }
+
+    public void setFromTag(String fromTag) {
+        this.fromTag = fromTag;
+    }
+
+    public String getToTag() {
+        return toTag;
+    }
+
+    public void setToTag(String toTag) {
+        this.toTag = toTag;
+    }
+
+    public String getViaBranch() {
+        return viaBranch;
+    }
+
+    public void setViaBranch(String viaBranch) {
+        this.viaBranch = viaBranch;
+    }
+
+    public boolean isAsSender() {
+        return asSender;
+    }
+
+    public void setAsSender(boolean asSender) {
+        this.asSender = asSender;
+    }
+}

+ 78 - 0
video-service/src/main/java/com/ruoyi/wvp/gb28181/bean/SsrcTransaction.java

@@ -0,0 +1,78 @@
+package com.ruoyi.wvp.gb28181.bean;
+
+
+import com.ruoyi.wvp.common.InviteSessionType;
+import gov.nist.javax.sip.message.SIPResponse;
+import lombok.Data;
+
+@Data
+public class SsrcTransaction {
+
+    /**
+     * 设备编号
+     */
+    private String deviceId;
+
+    /**
+     * 上级平台的编号
+     */
+    private String platformId;
+
+    /**
+     * 通道的数据库ID
+     */
+    private Integer channelId;
+
+    /**
+     * 会话的CALL ID
+     */
+    private String callId;
+
+    /**
+     * 关联的流应用名
+     */
+    private String app;
+
+    /**
+     * 关联的流ID
+     */
+    private String stream;
+
+    /**
+     * 使用的流媒体
+     */
+    private String mediaServerId;
+
+    /**
+     * 使用的SSRC
+     */
+    private String ssrc;
+
+    /**
+     * 事务信息
+     */
+    private SipTransactionInfo sipTransactionInfo;
+
+    /**
+     * 类型
+     */
+    private InviteSessionType type;
+
+    public static SsrcTransaction buildForDevice(String deviceId, Integer channelId, String callId, String app, String stream,
+                                                 String ssrc, String mediaServerId, SIPResponse response, InviteSessionType type) {
+        SsrcTransaction ssrcTransaction = new SsrcTransaction();
+        ssrcTransaction.setDeviceId(deviceId);
+        ssrcTransaction.setChannelId(channelId);
+        ssrcTransaction.setCallId(callId);
+        ssrcTransaction.setApp(app);
+        ssrcTransaction.setStream(stream);
+        ssrcTransaction.setMediaServerId(mediaServerId);
+        ssrcTransaction.setSsrc(ssrc);
+        ssrcTransaction.setSipTransactionInfo(new SipTransactionInfo(response));
+        ssrcTransaction.setType(type);
+        return ssrcTransaction;
+    }
+
+    public SsrcTransaction() {
+    }
+}

+ 41 - 0
video-service/src/main/java/com/ruoyi/wvp/gb28181/bean/SyncStatus.java

@@ -0,0 +1,41 @@
+package com.ruoyi.wvp.gb28181.bean;
+
+
+import lombok.Data;
+
+import java.time.Instant;
+
+/**
+ * 摄像机同步状态
+ * @author lin
+ */
+@Data
+public class SyncStatus {
+
+    /**
+     * 总数
+     */
+    private Integer total;
+
+    /**
+     * 当前更新多少
+     */
+    private Integer current;
+
+    /**
+     * 错误描述
+     */
+    private String errorMsg;
+
+    /**
+     * 是否同步中
+     */
+    private Boolean syncIng;
+
+    /**
+     * 时间
+     */
+    private Instant time;
+
+
+}

+ 62 - 0
video-service/src/main/java/com/ruoyi/wvp/gb28181/conf/DefaultProperties.java

@@ -0,0 +1,62 @@
+package com.ruoyi.wvp.gb28181.conf;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.Properties;
+
+/**
+ * 获取sip默认配置
+ * @author lin
+ */
+public class DefaultProperties {
+
+    public static Properties getProperties(String name, boolean sipLog) {
+        Properties properties = new Properties();
+        properties.setProperty("javax.sip.STACK_NAME", name);
+//        properties.setProperty("javax.sip.IP_ADDRESS", ip);
+        // 关闭自动会话
+        properties.setProperty("javax.sip.AUTOMATIC_DIALOG_SUPPORT", "off");
+        /**
+         * 完整配置参考 gov.nist.javax.sip.SipStackImpl,需要下载源码
+         * gov/nist/javax/sip/SipStackImpl.class
+         * sip消息的解析在 gov.nist.javax.sip.stack.UDPMessageChannel的processIncomingDataPacket方法
+         */
+
+//		 * gov/nist/javax/sip/SipStackImpl.class
+        // 接收所有notify请求,即使没有订阅
+        properties.setProperty("gov.nist.javax.sip.DELIVER_UNSOLICITED_NOTIFY", "true");
+        properties.setProperty("gov.nist.javax.sip.AUTOMATIC_DIALOG_ERROR_HANDLING", "false");
+        properties.setProperty("gov.nist.javax.sip.CANCEL_CLIENT_TRANSACTION_CHECKED", "true");
+        // 为_NULL _对话框传递_终止的_事件
+        properties.setProperty("gov.nist.javax.sip.DELIVER_TERMINATED_EVENT_FOR_NULL_DIALOG", "true");
+        // 是否自动计算content length的实际长度,默认不计算
+        properties.setProperty("gov.nist.javax.sip.COMPUTE_CONTENT_LENGTH_FROM_MESSAGE_BODY", "true");
+        // 会话清理策略
+        properties.setProperty("gov.nist.javax.sip.RELEASE_REFERENCES_STRATEGY", "Normal");
+        // 处理由该服务器处理的基于底层TCP的保持生存超时
+        properties.setProperty("gov.nist.javax.sip.RELIABLE_CONNECTION_KEEP_ALIVE_TIMEOUT", "60");
+        // 获取实际内容长度,不使用header中的长度信息
+        properties.setProperty("gov.nist.javax.sip.COMPUTE_CONTENT_LENGTH_FROM_MESSAGE_BODY", "true");
+        // 线程可重入
+        properties.setProperty("gov.nist.javax.sip.REENTRANT_LISTENER", "true");
+        // 定义应用程序打算多久审计一次 SIP 堆栈,了解其内部线程的健康状况(该属性指定连续审计之间的时间(以毫秒为单位))
+        properties.setProperty("gov.nist.javax.sip.THREAD_AUDIT_INTERVAL_IN_MILLISECS", "30000");
+
+        properties.setProperty("gov.nist.javax.sip.MESSAGE_PROCESSOR_FACTORY", "gov.nist.javax.sip.stack.NioMessageProcessorFactory");
+
+        /**
+         * sip_server_log.log 和 sip_debug_log.log ERROR, INFO, WARNING, OFF, DEBUG, TRACE
+         */
+        Logger log = LoggerFactory.getLogger(DefaultProperties.class);
+        if (sipLog) {
+            properties.setProperty("gov.nist.javax.sip.STACK_LOGGER", "com.ruoyi.wvp.gb28181.conf.StackLoggerImpl");
+            properties.setProperty("gov.nist.javax.sip.SERVER_LOGGER", "com.ruoyi.wvp.gb28181.conf.ServerLoggerImpl");
+            properties.setProperty("gov.nist.javax.sip.LOG_MESSAGE_CONTENT", "true");
+            log.info("[SIP日志]已开启");
+        }else {
+            log.info("[SIP日志]已关闭");
+        }
+        return properties;
+    }
+}

+ 91 - 0
video-service/src/main/java/com/ruoyi/wvp/gb28181/conf/ServerLoggerImpl.java

@@ -0,0 +1,91 @@
+package com.ruoyi.wvp.gb28181.conf;
+
+import gov.nist.core.CommonLogger;
+import gov.nist.core.ServerLogger;
+import gov.nist.core.StackLogger;
+import gov.nist.javax.sip.message.SIPMessage;
+import gov.nist.javax.sip.stack.SIPTransactionStack;
+
+import javax.sip.SipStack;
+import java.util.Properties;
+
+public class ServerLoggerImpl implements ServerLogger {
+
+    private boolean showLog = true;
+
+    private SIPTransactionStack sipStack;
+
+    protected StackLogger stackLogger;
+
+    @Override
+    public void closeLogFile() {
+
+    }
+
+    @Override
+    public void logMessage(SIPMessage message, String from, String to, boolean sender, long time) {
+        if (showLog) {
+            return;
+        }
+        StringBuilder stringBuilder = new StringBuilder();
+        stringBuilder.append(sender? "发送:目标--->" + from:"接收:来自--->" + to)
+                .append("\r\n")
+                        .append(message);
+        this.stackLogger.logInfo(stringBuilder.toString());
+
+    }
+
+    @Override
+    public void logMessage(SIPMessage message, String from, String to, String status, boolean sender, long time) {
+        if (!showLog) {
+            return;
+        }
+        StringBuilder stringBuilder = new StringBuilder();
+        stringBuilder.append(sender? "发送: 目标->" + from :"接收:来自->" + to)
+                .append("\r\n")
+                .append(message);
+        this.stackLogger.logInfo(stringBuilder.toString());
+    }
+
+    @Override
+    public void logMessage(SIPMessage message, String from, String to, String status, boolean sender) {
+        if (!showLog) {
+            return;
+        }
+        StringBuilder stringBuilder = new StringBuilder();
+        stringBuilder.append(sender? "发送: 目标->" + from :"接收:来自->" + to)
+                .append("\r\n")
+                .append(message);
+        this.stackLogger.logInfo(stringBuilder.toString());
+    }
+
+    @Override
+    public void logException(Exception ex) {
+        if (!showLog) {
+            return;
+        }
+        this.stackLogger.logException(ex);
+    }
+
+    @Override
+    public void setStackProperties(Properties stackProperties) {
+        if (!showLog) {
+            return;
+        }
+        String TRACE_LEVEL = stackProperties.getProperty("gov.nist.javax.sip.TRACE_LEVEL");
+        if (TRACE_LEVEL != null) {
+            showLog = true;
+        }
+    }
+
+    @Override
+    public void setSipStack(SipStack sipStack) {
+        if (!showLog) {
+            return;
+        }
+        if(sipStack instanceof SIPTransactionStack) {
+            this.sipStack = (SIPTransactionStack)sipStack;
+            this.stackLogger = CommonLogger.getLogger(SIPTransactionStack.class);
+        }
+    }
+}

+ 141 - 0
video-service/src/main/java/com/ruoyi/wvp/gb28181/conf/StackLoggerImpl.java

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

+ 176 - 0
video-service/src/main/java/com/ruoyi/wvp/gb28181/dao/provider/DeviceChannelProvider.java

@@ -0,0 +1,176 @@
+package com.ruoyi.wvp.gb28181.dao.provider;
+
+import com.ruoyi.wvp.common.enums.ChannelDataType;
+import org.springframework.util.ObjectUtils;
+
+import java.util.List;
+import java.util.Map;
+
+public class DeviceChannelProvider {
+
+    public String getBaseSelectSql(){
+        return "SELECT " +
+                " dc.id,\n" +
+                " dc.data_device_id,\n" +
+                " dc.create_time,\n" +
+                " dc.update_time,\n" +
+                " dc.sub_count,\n" +
+                " dc.stream_id,\n" +
+                " dc.has_audio,\n" +
+                " dc.gps_time,\n" +
+                " dc.stream_identification,\n" +
+                " dc.channel_type,\n" +
+                " coalesce(dc.gb_device_id, dc.device_id) as device_id,\n" +
+                " coalesce(dc.gb_name, dc.name) as name,\n" +
+                " coalesce(dc.gb_manufacturer, dc.manufacturer) as manufacturer,\n" +
+                " coalesce(dc.gb_model, dc.model) as model,\n" +
+                " coalesce(dc.gb_owner, dc.owner) as owner,\n" +
+                " coalesce(dc.gb_civil_code, dc.civil_code) as civil_code,\n" +
+                " coalesce(dc.gb_block, dc.block) as block,\n" +
+                " coalesce(dc.gb_address, dc.address) as address,\n" +
+                " coalesce(dc.gb_parental, dc.parental) as parental,\n" +
+                " coalesce(dc.gb_parent_id, dc.parent_id) as parent_id,\n" +
+                " coalesce(dc.gb_safety_way, dc.safety_way) as safety_way,\n" +
+                " coalesce(dc.gb_register_way, dc.register_way) as register_way,\n" +
+                " coalesce(dc.gb_cert_num, dc.cert_num) as cert_num,\n" +
+                " coalesce(dc.gb_certifiable, dc.certifiable) as certifiable,\n" +
+                " coalesce(dc.gb_err_code, dc.err_code) as err_code,\n" +
+                " coalesce(dc.gb_end_time, dc.end_time) as end_time,\n" +
+                " coalesce(dc.gb_secrecy, dc.secrecy) as secrecy,\n" +
+                " coalesce(dc.gb_ip_address, dc.ip_address) as ip_address,\n" +
+                " coalesce(dc.gb_port, dc.port) as port,\n" +
+                " coalesce(dc.gb_password, dc.password) as password,\n" +
+                " coalesce(dc.gb_status, dc.status) as status,\n" +
+                " coalesce(dc.gb_longitude, dc.longitude) as longitude,\n" +
+                " coalesce(dc.gb_latitude, dc.latitude) as latitude,\n" +
+                " coalesce(dc.gb_ptz_type, dc.ptz_type) as ptz_type,\n" +
+                " coalesce(dc.gb_position_type, dc.position_type) as position_type,\n" +
+                " coalesce(dc.gb_room_type, dc.room_type) as room_type,\n" +
+                " coalesce(dc.gb_use_type, dc.use_type) as use_type,\n" +
+                " coalesce(dc.gb_supply_light_type, dc.supply_light_type) as supply_light_type,\n" +
+                " coalesce(dc.gb_direction_type, dc.direction_type) as direction_type,\n" +
+                " coalesce(dc.gb_resolution, dc.resolution) as resolution,\n" +
+                " coalesce(dc.gb_business_group_id, dc.business_group_id) as business_group_id,\n" +
+                " coalesce(dc.gb_download_speed, dc.download_speed) as download_speed,\n" +
+                " coalesce(dc.gb_svc_space_support_mod, dc.svc_space_support_mod) as svc_space_support_mod,\n" +
+                " coalesce(dc.gb_svc_time_support_mode,dc.svc_time_support_mode) as svc_time_support_mode\n" +
+                " from " +
+                " wvp_device_channel dc "
+                ;
+    }
+    public String queryChannels(Map<String, Object> params ){
+        StringBuilder sqlBuild = new StringBuilder();
+        sqlBuild.append(getBaseSelectSql());
+        sqlBuild.append(" where data_type = " + ChannelDataType.GB28181.value + " and dc.data_device_id = #{dataDeviceId} ");
+        if (params.get("businessGroupId") != null ) {
+            sqlBuild.append(" AND coalesce(dc.gb_business_group_id, dc.business_group_id)=#{businessGroupId} AND coalesce(dc.gb_parent_id, dc.parent_id) is null");
+        }else if (params.get("parentChannelId") != null ) {
+            sqlBuild.append(" AND coalesce(dc.gb_parent_id, dc.parent_id)=#{parentChannelId}");
+        }
+        if (params.get("civilCode") != null ) {
+            sqlBuild.append(" AND (coalesce(dc.gb_civil_code, dc.civil_code) = #{civilCode} " +
+                    "OR (LENGTH(coalesce(dc.gb_device_id, dc.device_id))=LENGTH(#{civilCode}) + 2) AND coalesce(dc.gb_device_id, dc.device_id) LIKE concat(#{civilCode},'%'))");
+        }
+        if (params.get("query") != null && !ObjectUtils.isEmpty(params.get("query"))) {
+            sqlBuild.append(" AND (coalesce(dc.gb_device_id, dc.device_id) LIKE concat('%',#{query},'%') escape '/'" +
+                    " OR coalesce(dc.gb_name, dc.name) LIKE concat('%',#{query},'%') escape '/')")
+            ;
+        }
+        if (params.get("online") != null && (Boolean)params.get("online")) {
+            sqlBuild.append(" AND coalesce(gb_status, status) = 'ON'");
+        }
+        if (params.get("online") != null && !(Boolean)params.get("online")) {
+            sqlBuild.append(" AND coalesce(gb_status, status) = 'OFF'");
+        }
+        if (params.get("hasSubChannel") != null && (Boolean)params.get("hasSubChannel")) {
+            sqlBuild.append(" AND dc.sub_count > 0");
+        }
+        if (params.get("hasSubChannel") != null && !(Boolean)params.get("hasSubChannel")) {
+            sqlBuild.append(" AND dc.sub_count = 0");
+        }
+        List<String> channelIds = (List<String>)params.get("channelIds");
+        if (channelIds != null && !channelIds.isEmpty()) {
+            sqlBuild.append(" AND dc.device_id in (");
+            boolean first = true;
+            for (String id : channelIds) {
+                if (!first) {
+                    sqlBuild.append(",");
+                }
+                sqlBuild.append(id);
+                first = false;
+            }
+            sqlBuild.append(" )");
+        }
+        return sqlBuild.toString();
+    }
+
+
+    public String queryChannelsByDeviceDbId(Map<String, Object> params ){
+        StringBuilder sqlBuild = new StringBuilder();
+        sqlBuild.append(getBaseSelectSql());
+        sqlBuild.append(" where data_type = " + ChannelDataType.GB28181.value + " and dc.data_device_id = #{dataDeviceId}");
+        return sqlBuild.toString();
+    }
+
+    public String queryAllChannels(Map<String, Object> params ){
+        StringBuilder sqlBuild = new StringBuilder();
+        sqlBuild.append(getBaseSelectSql());
+        sqlBuild.append(" where data_type = " + ChannelDataType.GB28181.value + " and dc.data_device_id = #{dataDeviceId}");
+        return sqlBuild.toString();
+    }
+
+    public String getOne(Map<String, Object> params ){
+        StringBuilder sqlBuild = new StringBuilder();
+        sqlBuild.append(getBaseSelectSql());
+        sqlBuild.append(" where dc.id=#{id}");
+        return sqlBuild.toString();
+    }
+
+    public String getOneByDeviceId(Map<String, Object> params ){
+        StringBuilder sqlBuild = new StringBuilder();
+        sqlBuild.append(getBaseSelectSql());
+        sqlBuild.append(" where data_type = " + ChannelDataType.GB28181.value + " and dc.data_device_id=#{dataDeviceId} and coalesce(dc.gb_device_id, dc.device_id) = #{channelId}");
+        return sqlBuild.toString();
+    }
+
+
+
+    public String queryByDeviceId(Map<String, Object> params ){
+        return getBaseSelectSql() + " where data_type = " + ChannelDataType.GB28181.value + " and channel_type = 0 and coalesce(gb_device_id, device_id) = #{gbDeviceId}";
+    }
+
+    public String queryById(Map<String, Object> params ){
+        return getBaseSelectSql() + " where data_type = " + ChannelDataType.GB28181.value + " and channel_type = 0 and id = #{gbId}";
+    }
+
+
+    public String queryList(Map<String, Object> params ){
+        StringBuilder sqlBuild = new StringBuilder();
+        sqlBuild.append(getBaseSelectSql());
+        sqlBuild.append(" where channel_type = 0 and data_type = " + ChannelDataType.GB28181.value);
+        if (params.get("query") != null) {
+            sqlBuild.append(" AND (coalesce(gb_device_id, device_id) LIKE concat('%',#{query},'%')" +
+                    " OR coalesce(gb_name, name) LIKE concat('%',#{query},'%') )")
+            ;
+        }
+        if (params.get("online") != null && (Boolean)params.get("online")) {
+            sqlBuild.append(" AND coalesce(gb_status, status) = 'ON'");
+        }
+        if (params.get("online") != null && !(Boolean)params.get("online")) {
+            sqlBuild.append(" AND coalesce(gb_status, status) = 'OFF'");
+        }
+        if (params.get("hasCivilCode") != null && (Boolean)params.get("hasCivilCode")) {
+            sqlBuild.append(" AND coalesce(gb_civil_code, civil_code) is not null");
+        }
+        if (params.get("hasCivilCode") != null && !(Boolean)params.get("hasCivilCode")) {
+            sqlBuild.append(" AND coalesce(gb_civil_code, civil_code) is null");
+        }
+        if (params.get("hasGroup") != null && (Boolean)params.get("hasGroup")) {
+            sqlBuild.append(" AND coalesce(gb_parent_id, parent_id) is not null");
+        }
+        if (params.get("hasGroup") != null && !(Boolean)params.get("hasGroup")) {
+            sqlBuild.append(" AND coalesce(gb_parent_id, parent_id) is null");
+        }
+        return sqlBuild.toString();
+    }
+}

+ 235 - 0
video-service/src/main/java/com/ruoyi/wvp/gb28181/dto/DeviceChannelExtend.java

@@ -0,0 +1,235 @@
+package com.ruoyi.wvp.gb28181.dto;
+
+import lombok.Data;
+
+@Data
+public class DeviceChannelExtend {
+
+
+	/**
+	 * 数据库自增ID
+	 */
+	private int id;
+
+	/**
+	 * 通道id
+	 */
+	private String channelId;
+
+	/**
+	 * 设备id
+	 */
+	private String deviceId;
+
+	/**
+	 * 通道名
+	 */
+	private String name;
+
+	private String deviceName;
+
+	private boolean deviceOnline;
+
+	/**
+	 * 生产厂商
+	 */
+	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 String status;
+
+	/**
+	 * 经度
+	 */
+	private double longitude;
+
+	/**
+	 * 纬度
+	 */
+	private double latitude;
+
+	/**
+	 * 经度 GCJ02
+	 */
+	private double longitudeGcj02;
+
+	/**
+	 * 纬度 GCJ02
+	 */
+	private double latitudeGcj02;
+
+	/**
+	 * 经度 WGS84
+	 */
+	private double longitudeWgs84;
+
+	/**
+	 * 纬度 WGS84
+	 */
+	private double latitudeWgs84;
+
+	/**
+	 * 子设备数
+	 */
+	private int subCount;
+
+	/**
+	 * 流唯一编号,存在表示正在直播
+	 */
+	private String  streamId;
+
+	/**
+	 *  是否含有音频
+	 */
+	private boolean hasAudio;
+
+	/**
+	 * 标记通道的类型,0->国标通道 1->直播流通道 2->业务分组/虚拟组织/行政区划
+	 */
+	private int channelType;
+
+	/**
+	 * 业务分组
+	 */
+	private String businessGroupId;
+
+	/**
+	 * GPS的更新时间
+	 */
+	private String gpsTime;
+
+
+	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;
+		}
+	}
+}

+ 42 - 0
video-service/src/main/java/com/ruoyi/wvp/gb28181/event/EventPublisher.java

@@ -0,0 +1,42 @@
+package com.ruoyi.wvp.gb28181.event;
+
+
+import com.ruoyi.wvp.gb28181.bean.*;
+import com.ruoyi.wvp.gb28181.event.record.RecordEndEvent;
+import com.ruoyi.wvp.media.bean.MediaServer;
+import com.ruoyi.wvp.media.event.mediaServer.MediaServerOfflineEvent;
+import com.ruoyi.wvp.media.event.mediaServer.MediaServerOnlineEvent;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.ApplicationEventPublisher;
+import org.springframework.stereotype.Component;
+
+
+/**
+ * @description:Event事件通知推送器,支持推送在线事件、离线事件
+ * @author: swwheihei
+ * @date:   2020年5月6日 上午11:30:50
+ */
+@Component
+public class EventPublisher {
+
+	@Autowired
+    private ApplicationEventPublisher applicationEventPublisher;
+
+	public void mediaServerOfflineEventPublish(MediaServer mediaServer){
+		MediaServerOfflineEvent outEvent = new MediaServerOfflineEvent(this);
+		outEvent.setMediaServer(mediaServer);
+		applicationEventPublisher.publishEvent(outEvent);
+	}
+
+	public void mediaServerOnlineEventPublish(MediaServer mediaServer) {
+		MediaServerOnlineEvent outEvent = new MediaServerOnlineEvent(this);
+		outEvent.setMediaServer(mediaServer);
+		applicationEventPublisher.publishEvent(outEvent);
+	}
+
+	public void recordEndEventPush(RecordInfo recordInfo) {
+		RecordEndEvent outEvent = new RecordEndEvent(this);
+		outEvent.setRecordInfo(recordInfo);
+		applicationEventPublisher.publishEvent(outEvent);
+	}
+}

+ 187 - 0
video-service/src/main/java/com/ruoyi/wvp/gb28181/event/SipSubscribe.java

@@ -0,0 +1,187 @@
+package com.ruoyi.wvp.gb28181.event;
+
+import com.ruoyi.wvp.gb28181.bean.DeviceNotFoundEvent;
+import com.ruoyi.wvp.gb28181.event.sip.SipEvent;
+import gov.nist.javax.sip.message.SIPRequest;
+import gov.nist.javax.sip.message.SIPResponse;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.ObjectUtils;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Component;
+
+import javax.sip.DialogTerminatedEvent;
+import javax.sip.ResponseEvent;
+import javax.sip.TimeoutEvent;
+import javax.sip.TransactionTerminatedEvent;
+import javax.sip.header.WarningHeader;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.DelayQueue;
+
+/**
+ * @author lin
+ */
+@Slf4j
+@Component
+public class SipSubscribe {
+
+    private final Map<String, SipEvent> subscribes = new ConcurrentHashMap<>();
+
+    private final DelayQueue<SipEvent> delayQueue = new DelayQueue<>();
+
+    @Scheduled(fixedDelay = 200)   //每200毫秒执行
+    public void execute(){
+        if (delayQueue.isEmpty()) {
+            return;
+        }
+        try {
+            SipEvent take = delayQueue.take();
+            // 出现超时异常
+            if(take.getErrorEvent() != null) {
+                EventResult<Object> eventResult = new EventResult<>();
+                eventResult.type = EventResultType.timeout;
+                eventResult.msg = "消息超时未回复";
+                eventResult.statusCode = -1024;
+                take.getErrorEvent().response(eventResult);
+            }
+            subscribes.remove(take.getKey());
+        } catch (InterruptedException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    public void updateTimeout(String callId) {
+        SipEvent sipEvent = subscribes.get(callId);
+        if (sipEvent != null) {
+            delayQueue.remove(sipEvent);
+            delayQueue.offer(sipEvent);
+        }
+    }
+
+    public interface Event { void response(EventResult eventResult);
+    }
+
+    /**
+     *
+     */
+    public enum EventResultType{
+        // 超时
+        timeout,
+        // 回复
+        response,
+        // 事务已结束
+        transactionTerminated,
+        // 会话已结束
+        dialogTerminated,
+        // 设备未找到
+        deviceNotFoundEvent,
+        // 消息发送失败
+        cmdSendFailEvent,
+        // 消息发送失败
+        failedToGetPort
+    }
+
+    public static class EventResult<EventObject>{
+        public int statusCode;
+        public EventResultType type;
+        public String msg;
+        public String callId;
+        public EventObject event;
+
+        public EventResult() {
+        }
+
+        public EventResult(EventObject event) {
+            this.event = event;
+            if (event instanceof ResponseEvent) {
+                ResponseEvent responseEvent = (ResponseEvent)event;
+                SIPResponse response = (SIPResponse)responseEvent.getResponse();
+                this.type = EventResultType.response;
+                if (response != null) {
+                    WarningHeader warningHeader = (WarningHeader)response.getHeader(WarningHeader.NAME);
+                    if (warningHeader != null && !ObjectUtils.isEmpty(warningHeader.getText())) {
+                        this.msg = "";
+                        if (warningHeader.getCode() > 0) {
+                            this.msg += warningHeader.getCode() + ":";
+                        }
+                        if (warningHeader.getAgent() != null) {
+                            this.msg += warningHeader.getCode() + ":";
+                        }
+                        if (warningHeader.getText() != null) {
+                            this.msg += warningHeader.getText();
+                        }
+                    }else {
+                        this.msg = response.getReasonPhrase();
+                    }
+                    this.statusCode = response.getStatusCode();
+                    this.callId = response.getCallIdHeader().getCallId();
+                }
+            }else if (event instanceof TimeoutEvent) {
+                TimeoutEvent timeoutEvent = (TimeoutEvent)event;
+                this.type = EventResultType.timeout;
+                this.msg = "消息超时未回复";
+                this.statusCode = -1024;
+                if (timeoutEvent.isServerTransaction()) {
+                    this.callId = ((SIPRequest)timeoutEvent.getServerTransaction().getRequest()).getCallIdHeader().getCallId();
+                }else {
+                    this.callId = ((SIPRequest)timeoutEvent.getClientTransaction().getRequest()).getCallIdHeader().getCallId();
+                }
+            }else if (event instanceof TransactionTerminatedEvent) {
+                TransactionTerminatedEvent transactionTerminatedEvent = (TransactionTerminatedEvent)event;
+                this.type = EventResultType.transactionTerminated;
+                this.msg = "事务已结束";
+                this.statusCode = -1024;
+                if (transactionTerminatedEvent.isServerTransaction()) {
+                    this.callId = ((SIPRequest)transactionTerminatedEvent.getServerTransaction().getRequest()).getCallIdHeader().getCallId();
+                }else {
+                    this.callId = ((SIPRequest)transactionTerminatedEvent.getClientTransaction().getRequest()).getCallIdHeader().getCallId();
+                }
+            }else if (event instanceof DialogTerminatedEvent) {
+                DialogTerminatedEvent dialogTerminatedEvent = (DialogTerminatedEvent)event;
+                this.type = EventResultType.dialogTerminated;
+                this.msg = "会话已结束";
+                this.statusCode = -1024;
+                this.callId = dialogTerminatedEvent.getDialog().getCallId().getCallId();
+            }else if (event instanceof DeviceNotFoundEvent) {
+                this.type = EventResultType.deviceNotFoundEvent;
+                this.msg = "设备未找到";
+                this.statusCode = -1024;
+                this.callId = ((DeviceNotFoundEvent) event).getCallId();
+            }
+        }
+    }
+
+
+    public void addSubscribe(String key, SipEvent event) {
+        SipEvent sipEvent = subscribes.get(key);
+        if (sipEvent != null) {
+            subscribes.remove(key);
+            delayQueue.remove(sipEvent);
+        }
+        subscribes.put(key, event);
+        delayQueue.offer(event);
+    }
+
+    public SipEvent getSubscribe(String key) {
+        return subscribes.get(key);
+    }
+
+    public void removeSubscribe(String key) {
+        if(key == null){
+            return;
+        }
+        SipEvent sipEvent = subscribes.get(key);
+        if (sipEvent != null) {
+            subscribes.remove(key);
+            delayQueue.remove(sipEvent);
+        }
+    }
+
+    public boolean isEmpty(){
+        return subscribes.isEmpty();
+    }
+
+    public Integer size() {
+        return subscribes.size();
+    }
+}

+ 31 - 0
video-service/src/main/java/com/ruoyi/wvp/gb28181/event/record/RecordEndEvent.java

@@ -0,0 +1,31 @@
+package com.ruoyi.wvp.gb28181.event.record;
+
+import com.ruoyi.wvp.gb28181.bean.RecordInfo;
+import org.springframework.context.ApplicationEvent;
+
+/**
+ * @description: 录像查询结束时间
+ * @author: pan
+ * @data: 2022-02-23
+ */
+
+public class RecordEndEvent extends ApplicationEvent {
+    /**
+     *
+     */
+    private static final long serialVersionUID = 1L;
+
+    public RecordEndEvent(Object source) {
+        super(source);
+    }
+
+    private RecordInfo recordInfo;
+
+    public RecordInfo getRecordInfo() {
+        return recordInfo;
+    }
+
+    public void setRecordInfo(RecordInfo recordInfo) {
+        this.recordInfo = recordInfo;
+    }
+}

+ 73 - 0
video-service/src/main/java/com/ruoyi/wvp/gb28181/event/record/RecordEndEventListener.java

@@ -0,0 +1,73 @@
+package com.ruoyi.wvp.gb28181.event.record;
+
+import com.ruoyi.wvp.gb28181.bean.RecordInfo;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.context.ApplicationListener;
+import org.springframework.stereotype.Component;
+
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * @description: 录像查询结束事件
+ * @author: pan
+ * @data: 2022-02-23
+ */
+@Slf4j
+@Component
+public class RecordEndEventListener implements ApplicationListener<RecordEndEvent> {
+
+    private Map<String, RecordEndEventHandler> handlerMap = new ConcurrentHashMap<>();
+
+    /**
+     * 事件处理接口
+     */
+    public interface RecordEndEventHandler{
+        void  handler(RecordInfo recordInfo);
+    }
+
+    /**
+     * 事件触发
+     *
+     * @param event
+     */
+    @Override
+    public void onApplicationEvent(RecordEndEvent event) {
+        String deviceId = event.getRecordInfo().getDeviceId();
+        String channelId = event.getRecordInfo().getChannelId();
+        int count = event.getRecordInfo().getCount();
+        int sumNum = event.getRecordInfo().getSumNum();
+        if (!handlerMap.isEmpty()) {
+            RecordEndEventHandler handler = handlerMap.get(deviceId + channelId);
+            log.info("录像查询事件触发, 发送订阅,deviceId:{}, channelId: {}",
+                    event.getRecordInfo().getDeviceId(), event.getRecordInfo().getChannelId());
+            if (handler !=null){
+                handler.handler(event.getRecordInfo());
+                if (count ==sumNum){
+                    handlerMap.remove(deviceId + channelId);
+                }
+            }
+        }
+    }
+
+    /**
+     * 添加
+     * @param device
+     * @param channelId
+     * @param recordEndEventHandler
+     */
+    public void addEndEventHandler(String device, String channelId, RecordEndEventHandler recordEndEventHandler) {
+        log.info("录像查询事件添加监听,deviceId:{}, channelId: {}", device, channelId);
+        handlerMap.put(device + channelId, recordEndEventHandler);
+    }
+    /**
+     * 添加
+     * @param device
+     * @param channelId
+     */
+    public void delEndEventHandler(String device, String channelId) {
+        log.info("录像查询事件移除监听,deviceId:{}, channelId: {}", device, channelId);
+        handlerMap.remove(device + channelId);
+    }
+
+}

+ 48 - 0
video-service/src/main/java/com/ruoyi/wvp/gb28181/event/sip/SipEvent.java

@@ -0,0 +1,48 @@
+package com.ruoyi.wvp.gb28181.event.sip;
+
+import com.ruoyi.wvp.gb28181.event.SipSubscribe;
+import lombok.Data;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.concurrent.Delayed;
+import java.util.concurrent.TimeUnit;
+
+@Data
+public class SipEvent implements Delayed {
+
+    private String key;
+
+    /**
+     * 成功的回调
+     */
+    private SipSubscribe.Event okEvent;
+
+    /**
+     * 错误的回调,包括超时
+     */
+    private SipSubscribe.Event errorEvent;
+
+    /**
+     * 超时时间(单位: 毫秒)
+     */
+    private long delay;
+
+    public static SipEvent getInstance(String key, SipSubscribe.Event okEvent, SipSubscribe.Event errorEvent, long delay) {
+        SipEvent sipEvent = new SipEvent();
+        sipEvent.setKey(key);
+        sipEvent.setOkEvent(okEvent);
+        sipEvent.setErrorEvent(errorEvent);
+        sipEvent.setDelay(delay);
+        return sipEvent;
+    }
+
+    @Override
+    public long getDelay(@NotNull TimeUnit unit) {
+        return unit.convert(delay, TimeUnit.MILLISECONDS);
+    }
+
+    @Override
+    public int compareTo(@NotNull Delayed o) {
+        return (int) (this.getDelay(TimeUnit.MILLISECONDS) - o.getDelay(TimeUnit.MILLISECONDS));
+    }
+}

+ 125 - 0
video-service/src/main/java/com/ruoyi/wvp/gb28181/service/IDeviceChannelService.java

@@ -0,0 +1,125 @@
+package com.ruoyi.wvp.gb28181.service;
+
+import com.ruoyi.wvp.common.enums.DeviceControlType;
+import com.ruoyi.wvp.gb28181.bean.Device;
+import com.ruoyi.wvp.gb28181.bean.DeviceChannel;
+import com.ruoyi.wvp.gb28181.dto.DeviceChannelExtend;
+import com.ruoyi.wvp.service.bean.ErrorCallback;
+import com.ruoyi.wvp.vmanager.bean.ResourceBaseInfo;
+import org.dom4j.Element;
+
+import javax.validation.constraints.NotNull;
+import java.util.List;
+
+/**
+ * 国标通道业务类
+ * @author lin
+ */
+public interface IDeviceChannelService {
+
+    /**
+     * 批量添加设备通道
+     */
+    int updateChannels(Device device, List<DeviceChannel> channels);
+
+    /**
+     * 获取统计信息
+     * @return
+     */
+    ResourceBaseInfo getOverview();
+
+    /**
+     * 查询所有未分配的通道
+     * @param platformId
+     * @return
+     */
+
+
+
+    /**
+     * 查询通道所属的设备
+     */
+    List<Device> getDeviceByChannelId(String channelId);
+
+    /**
+     * 批量删除通道
+     * @param deleteChannelList 待删除的通道列表
+     */
+    int deleteChannelsForNotify(List<DeviceChannel> deleteChannelList);
+
+    int updateChannelsStatus(List<DeviceChannel> channels);
+
+    /**
+     *  获取一个通道
+     */
+    DeviceChannel getOne(String deviceId, String channelId);
+
+    DeviceChannel getOneForSource(String deviceId, String channelId);
+
+    /**
+     * 直接批量更新通道
+     */
+    void batchUpdateChannelForNotify(List<DeviceChannel> channels);
+
+    /**
+     * 直接批量添加
+     */
+    void batchAddChannel(List<DeviceChannel> deviceChannels);
+
+    /**
+     * 修改通道的码流类型
+     */
+    void updateChannelStreamIdentification(DeviceChannel channel);
+
+    List<DeviceChannel> queryChaneListByDeviceId(String deviceId);
+
+
+    void startPlay(Integer channelId, String stream);
+
+    void stopPlay(Integer channelId);
+
+
+    void online(DeviceChannel channel);
+
+    void offline(DeviceChannel channel);
+
+    void delete(DeviceChannel channel);
+
+    void cleanChannelsForDevice(int deviceId);
+
+    boolean resetChannels(int deviceDbId, List<DeviceChannel> deviceChannels);
+
+    List<DeviceChannel> getSubChannels(int deviceDbId, String channelId, String query, Boolean channelType, Boolean online, int page, int count);
+
+    List<DeviceChannelExtend> queryChannelExtendsByDeviceId(String deviceId, List<String> channelIds, Boolean online);
+
+    List<DeviceChannel> queryChannelsByDeviceId(String deviceId, String query, Boolean channelType, Boolean online, int pageNum, int pageSize);
+
+
+    List<Device> queryDeviceWithAsMessageChannel();
+
+    DeviceChannel getRawChannel(int id);
+
+    DeviceChannel getOneById(Integer channelId);
+
+    DeviceChannel getOneForSourceById(Integer channelId);
+
+
+    void changeAudio(Integer channelId, Boolean audio);
+
+    void updateChannelStatus(DeviceChannel channel);
+
+    void addChannel(DeviceChannel channel);
+
+    void updateChannelForNotify(DeviceChannel channel);
+
+    DeviceChannel getOneForSource(int deviceDbId, String channelId);
+
+    DeviceChannel getOneBySourceId(int deviceDbId, String channelId);
+
+    List<DeviceChannel> queryChaneListByDeviceDbId(Integer deviceDbId);
+
+    List<Integer> queryChaneIdListByDeviceDbIds(List<Integer> deviceDbId);
+
+    void handlePtzCmd(@NotNull Integer dataDeviceId, @NotNull Integer gbId, Element rootElement, DeviceControlType type, ErrorCallback<String> callback);
+}

+ 208 - 0
video-service/src/main/java/com/ruoyi/wvp/gb28181/service/IDeviceService.java

@@ -0,0 +1,208 @@
+package com.ruoyi.wvp.gb28181.service;
+
+import com.ruoyi.wvp.common.CommonCallback;
+import com.ruoyi.wvp.gb28181.bean.Device;
+import com.ruoyi.wvp.gb28181.bean.SipTransactionInfo;
+import com.ruoyi.wvp.gb28181.bean.SyncStatus;
+import com.ruoyi.wvp.vmanager.bean.ResourceBaseInfo;
+
+import java.util.List;
+
+/**
+ * 设备相关业务处理
+ *
+ * @author lin
+ */
+public interface IDeviceService {
+
+    /**
+     * 设备上线
+     *
+     * @param device 设备信息
+     */
+    void online(Device device, SipTransactionInfo sipTransactionInfo);
+
+    /**
+     * 设备下线
+     *
+     * @param deviceId 设备编号
+     */
+    void offline(String deviceId, String reason);
+
+    /**
+     * 添加目录订阅
+     *
+     * @param device 设备信息
+     * @return 布尔
+     */
+    boolean addCatalogSubscribe(Device device);
+
+    /**
+     * 移除目录订阅
+     *
+     * @param device 设备信息
+     * @return 布尔
+     */
+    boolean removeCatalogSubscribe(Device device, CommonCallback<Boolean> callback);
+
+    /**
+     * 添加移动位置订阅
+     *
+     * @param device 设备信息
+     * @return 布尔
+     */
+
+
+    /**
+     * 移除移动位置订阅
+     *
+     * @param device 设备信息
+     * @return 布尔
+     */
+
+
+    /**
+     * 移除移动位置订阅
+     *
+     * @param deviceId 设备ID
+     * @return 同步状态
+     */
+    SyncStatus getChannelSyncStatus(String deviceId);
+
+    /**
+     * 查看是否仍在同步
+     *
+     * @param deviceId 设备ID
+     * @return 布尔
+     */
+    Boolean isSyncRunning(String deviceId);
+
+    /**
+     * 通道同步
+     *
+     * @param device 设备信息
+     */
+    void sync(Device device);
+
+    /**
+     * 查询设备信息
+     *
+     * @param deviceId 设备编号
+     * @return 设备信息
+     */
+    Device getDeviceByDeviceId(String deviceId);
+
+    /**
+     * 获取所有在线设备
+     *
+     * @return 设备列表
+     */
+    List<Device> getAllOnlineDevice();
+
+    List<Device> getAllByStatus(Boolean status);
+
+    /**
+     * 判断是否注册已经失效
+     *
+     * @param device 设备信息
+     * @return 布尔
+     */
+    boolean expire(Device device);
+
+    /**
+     * 检查设备状态
+     *
+     * @param device 设备信息
+     */
+    void checkDeviceStatus(Device device);
+
+    /**
+     * 根据IP和端口获取设备信息
+     *
+     * @param host IP
+     * @param port 端口
+     * @return 设备信息
+     */
+    Device getDeviceByHostAndPort(String host, int port);
+
+    /**
+     * 更新设备
+     *
+     * @param device 设备信息
+     */
+    void updateDevice(Device device);
+
+    /**
+     * 检查设备编号是否已经存在
+     *
+     * @param deviceId 设备编号
+     * @return
+     */
+    boolean isExist(String deviceId);
+
+    /**
+     * 添加设备
+     *
+     * @param device
+     */
+    void addDevice(Device device);
+
+    /**
+     * 页面表单更新设备信息
+     *
+     * @param device
+     */
+    void updateCustomDevice(Device device);
+
+    /**
+     * 删除设备
+     *
+     * @param deviceId
+     * @return
+     */
+    boolean delete(String deviceId);
+
+    /**
+     * 获取统计信息
+     *
+     * @return
+     */
+    ResourceBaseInfo getOverview();
+
+    /**
+     * 获取所有设备
+     */
+    List<Device> getAll();
+
+    /**
+     * 分页查询国标设备
+     *
+     * @return
+     */
+    List<Device> getAll(Device device);
+
+    Device getDevice(Integer gbDeviceDbId);
+
+    Device getDeviceByChannelId(Integer channelId);
+
+    Device getDeviceBySourceChannelDeviceId(String requesterId);
+
+    /**
+     * 开启/关闭目录订阅
+     *
+     * @param id    通道的Id
+     * @param cycle 订阅周期
+     */
+    void subscribeCatalog(int id, int cycle);
+
+    /**
+     * 开启/关闭移动位置订阅
+     *
+     * @param id       通道的Id
+     * @param cycle    订阅周期
+     * @param interval 报送间隔
+     */
+
+
+    void updateDeviceHeartInfo(Device device);
+}

+ 85 - 0
video-service/src/main/java/com/ruoyi/wvp/gb28181/service/IInviteStreamService.java

@@ -0,0 +1,85 @@
+package com.ruoyi.wvp.gb28181.service;
+
+import com.ruoyi.wvp.common.InviteInfo;
+import com.ruoyi.wvp.common.InviteSessionType;
+import com.ruoyi.wvp.common.StreamInfo;
+import com.ruoyi.wvp.service.bean.ErrorCallback;
+
+import java.util.List;
+
+/**
+ * 记录国标点播的状态,包括实时预览,下载,录像回放
+ */
+public interface IInviteStreamService {
+
+    /**
+     * 更新点播的状态信息
+     */
+    void updateInviteInfo(InviteInfo inviteInfo);
+
+    void updateInviteInfo(InviteInfo inviteInfo, Long time);
+
+    InviteInfo updateInviteInfoForStream(InviteInfo inviteInfo, String stream);
+
+    /**
+     * 获取点播的状态信息
+     */
+    InviteInfo getInviteInfo(InviteSessionType type, Integer channelId, String stream);
+
+    /**
+     * 移除点播的状态信息
+     */
+    void removeInviteInfo(InviteSessionType type, Integer channelId, String stream);
+    /**
+     * 移除点播的状态信息
+     */
+    void removeInviteInfo(InviteInfo inviteInfo);
+    /**
+     * 移除点播的状态信息
+     */
+    void removeInviteInfoByDeviceAndChannel(InviteSessionType inviteSessionType, Integer channelId);
+
+    List<InviteInfo> getAllInviteInfo();
+
+    /**
+     * 获取点播的状态信息
+     */
+    InviteInfo getInviteInfoByDeviceAndChannel(InviteSessionType type, Integer channelId);
+
+    /**
+     * 获取点播的状态信息
+     */
+    InviteInfo getInviteInfoByStream(InviteSessionType type, String stream);
+
+
+    /**
+     * 添加一个invite回调
+     */
+    void once(InviteSessionType type, Integer channelId, String stream,  ErrorCallback<StreamInfo> callback);
+
+    /**
+     * 调用一个invite回调
+     */
+    void call(InviteSessionType type,  Integer channelId, String stream,  int code, String msg, StreamInfo data);
+
+    /**
+     * 清空一个设备的所有invite信息
+     */
+    void clearInviteInfo(String deviceId);
+
+    /**
+     * 统计同一个zlm下的国标收流个数
+     */
+    int getStreamInfoCount(String mediaServerId);
+
+
+    /**
+     * 获取MediaServer下的流信息
+     */
+    InviteInfo getInviteInfoBySSRC(String ssrc);
+
+    /**
+     * 更新ssrc
+     */
+    InviteInfo updateInviteInfoForSSRC(InviteInfo inviteInfo, String ssrcInResponse);
+}

+ 21 - 0
video-service/src/main/java/com/ruoyi/wvp/gb28181/service/IPTZService.java

@@ -0,0 +1,21 @@
+package com.ruoyi.wvp.gb28181.service;
+
+
+import com.ruoyi.wvp.gb28181.bean.Device;
+import com.ruoyi.wvp.gb28181.bean.Preset;
+
+import java.util.List;
+
+public interface IPTZService {
+
+
+    List<Preset> queryPresetList(String deviceId, String channelDeviceId);
+
+    void addPreset(Preset preset);
+
+    void deletePreset(Integer qq);
+
+    void ptz(Device device, String channelId, int cmdCode, int horizonSpeed, int verticalSpeed, int zoomSpeed);
+
+    void frontEndCommand(Device device, String channelId, int cmdCode, int parameter1, int parameter2, int combindCode2);
+}

+ 53 - 0
video-service/src/main/java/com/ruoyi/wvp/gb28181/service/IPlayService.java

@@ -0,0 +1,53 @@
+package com.ruoyi.wvp.gb28181.service;
+
+import com.zksy.common.exception.ServiceException;
+import com.ruoyi.wvp.common.InviteSessionType;
+import com.ruoyi.wvp.common.StreamInfo;
+import com.ruoyi.wvp.gb28181.bean.*;
+import com.ruoyi.wvp.media.bean.MediaInfo;
+import com.ruoyi.wvp.media.bean.MediaServer;
+import com.ruoyi.wvp.service.bean.ErrorCallback;
+import com.ruoyi.wvp.service.bean.SSRCInfo;
+
+import javax.sip.InvalidArgumentException;
+import javax.sip.SipException;
+import java.text.ParseException;
+import com.ruoyi.wvp.common.InviteInfo;
+
+/**
+ * 点播处理
+ */
+public interface IPlayService {
+
+    SSRCInfo play(MediaServer mediaServerItem, String deviceId, String channelId, String ssrc, ErrorCallback<StreamInfo> callback);
+
+    StreamInfo onPublishHandlerForPlay(MediaServer mediaServerItem, MediaInfo mediaInfo, Device device, DeviceChannel channel);
+
+    MediaServer getNewMediaServerItem(Device device);
+
+    void playBack(Device device, DeviceChannel channel, String startTime, String endTime, ErrorCallback<StreamInfo> callback);
+    void zlmServerOffline(MediaServer mediaServer);
+
+    void download(Device device, DeviceChannel channel, String startTime, String endTime, int downloadSpeed, ErrorCallback<StreamInfo> callback);
+
+    StreamInfo getDownLoadInfo(Device device, DeviceChannel channel, String stream);
+
+    void zlmServerOnline(MediaServer mediaServer);
+
+
+    void pauseRtp(String streamId) throws ServiceException, InvalidArgumentException, ParseException, SipException;
+
+    void resumeRtp(String streamId) throws ServiceException, InvalidArgumentException, ParseException, SipException;
+
+
+    void stop(InviteSessionType type, Device device, DeviceChannel channel, String stream);
+
+    void stop(InviteInfo inviteInfo);
+
+    void play(CommonGBChannel channel, Boolean record, ErrorCallback<StreamInfo> callback);
+
+    void playBack(CommonGBChannel channel, Long startTime, Long stopTime, ErrorCallback<StreamInfo> callback);
+
+    void download(CommonGBChannel channel, Long startTime, Long stopTime, Integer downloadSpeed, ErrorCallback<StreamInfo> callback);
+
+}

+ 589 - 0
video-service/src/main/java/com/ruoyi/wvp/gb28181/service/impl/DeviceChannelServiceImpl.java

@@ -0,0 +1,589 @@
+package com.ruoyi.wvp.gb28181.service.impl;
+
+import com.baomidou.dynamic.datasource.annotation.DS;
+import com.ruoyi.wvp.common.InviteInfo;
+import com.ruoyi.wvp.common.InviteSessionType;
+import com.ruoyi.wvp.common.enums.ChannelDataType;
+import com.ruoyi.wvp.common.enums.DeviceControlType;
+import com.ruoyi.wvp.conf.UserSetting;
+import com.zksy.common.exception.ControllerException;
+import com.ruoyi.wvp.gb28181.bean.*;
+import com.ruoyi.wvp.gb28181.dto.DeviceChannelExtend;
+import com.ruoyi.wvp.gb28181.service.IDeviceChannelService;
+import com.ruoyi.wvp.gb28181.service.IInviteStreamService;
+import com.ruoyi.wvp.gb28181.transmit.cmd.ISIPCommander;
+import com.ruoyi.wvp.mapper.DeviceChannelMapper;
+import com.ruoyi.wvp.mapper.DeviceMapper;
+import com.ruoyi.wvp.service.bean.ErrorCallback;
+import com.ruoyi.wvp.storager.IRedisCatchStorage;
+import com.ruoyi.wvp.utils.DateUtil;
+import com.zksy.common.enums.ErrorCode;
+import com.ruoyi.wvp.vmanager.bean.ResourceBaseInfo;
+import lombok.extern.slf4j.Slf4j;
+import org.dom4j.Element;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.util.Assert;
+import org.springframework.util.CollectionUtils;
+import org.springframework.util.ObjectUtils;
+
+import javax.sip.InvalidArgumentException;
+import javax.sip.SipException;
+import javax.sip.message.Response;
+import javax.validation.constraints.NotNull;
+import java.text.ParseException;
+import java.util.*;
+
+import static com.zksy.common.utils.PageUtils.startPage;
+import static com.ruoyi.wvp.gb28181.utils.XmlUtil.getText;
+
+/**
+ * @author lin
+ */
+@Slf4j
+@Service
+@DS("master")
+@Transactional(rollbackFor = Exception.class)
+public class DeviceChannelServiceImpl implements IDeviceChannelService {
+
+    @Autowired
+    private IInviteStreamService inviteStreamService;
+
+    @Autowired
+    private DeviceChannelMapper channelMapper;
+
+    @Autowired
+    private DeviceMapper deviceMapper;
+
+    @Autowired
+    private UserSetting userSetting;
+
+    @Autowired
+    private IRedisCatchStorage redisCatchStorage;
+
+
+    @Autowired
+    private ISIPCommander cmder;
+
+
+    @Override
+    public int updateChannels(Device device, List<DeviceChannel> channels) {
+        List<DeviceChannel> addChannels = new ArrayList<>();
+        List<DeviceChannel> updateChannels = new ArrayList<>();
+        HashMap<String, DeviceChannel> channelsInStore = new HashMap<>();
+        int result = 0;
+        if (channels != null && !channels.isEmpty()) {
+            List<DeviceChannel> channelList = channelMapper.queryChannelsByDeviceDbId(device.getId());
+            if (channelList.isEmpty()) {
+                for (DeviceChannel channel : channels) {
+                    channel.setDataDeviceId(device.getId());
+                    InviteInfo inviteInfo = inviteStreamService.getInviteInfoByDeviceAndChannel(InviteSessionType.PLAY, channel.getId());
+                    if (inviteInfo != null && inviteInfo.getStreamInfo() != null) {
+                        channel.setStreamId(inviteInfo.getStreamInfo().getStream());
+                    }
+                    String now = DateUtil.getNow();
+                    channel.setUpdateTime(now);
+                    channel.setCreateTime(now);
+                    addChannels.add(channel);
+                }
+            } else {
+                for (DeviceChannel deviceChannel : channelList) {
+                    channelsInStore.put(deviceChannel.getDataDeviceId() + deviceChannel.getDeviceId(), deviceChannel);
+                }
+                for (DeviceChannel channel : channels) {
+                    InviteInfo inviteInfo = inviteStreamService.getInviteInfoByDeviceAndChannel(InviteSessionType.PLAY, channel.getId());
+                    if (inviteInfo != null && inviteInfo.getStreamInfo() != null) {
+                        channel.setStreamId(inviteInfo.getStreamInfo().getStream());
+                    }
+                    String now = DateUtil.getNow();
+                    channel.setUpdateTime(now);
+                    DeviceChannel deviceChannelInDb = channelsInStore.get(channel.getDataDeviceId() + channel.getDeviceId());
+                    if (deviceChannelInDb != null) {
+                        channel.setId(deviceChannelInDb.getId());
+                        channel.setUpdateTime(now);
+                        updateChannels.add(channel);
+                    } else {
+                        channel.setCreateTime(now);
+                        channel.setUpdateTime(now);
+                        addChannels.add(channel);
+                    }
+                }
+            }
+            Set<String> channelSet = new HashSet<>();
+            // 滤重
+            List<DeviceChannel> addChannelList = new ArrayList<>();
+            List<DeviceChannel> updateChannelList = new ArrayList<>();
+            addChannels.forEach(channel -> {
+                if (channelSet.add(channel.getDeviceId())) {
+                    addChannelList.add(channel);
+                }
+            });
+            channelSet.clear();
+            updateChannels.forEach(channel -> {
+                if (channelSet.add(channel.getDeviceId())) {
+                    updateChannelList.add(channel);
+                }
+            });
+
+            int limitCount = 500;
+            if (!addChannelList.isEmpty()) {
+                if (addChannelList.size() > limitCount) {
+                    for (int i = 0; i < addChannelList.size(); i += limitCount) {
+                        int toIndex = i + limitCount;
+                        if (i + limitCount > addChannelList.size()) {
+                            toIndex = addChannelList.size();
+                        }
+                        result += channelMapper.batchAdd(addChannelList.subList(i, toIndex));
+                    }
+                } else {
+                    result += channelMapper.batchAdd(addChannelList);
+                }
+            }
+            if (!updateChannelList.isEmpty()) {
+                if (updateChannelList.size() > limitCount) {
+                    for (int i = 0; i < updateChannelList.size(); i += limitCount) {
+                        int toIndex = i + limitCount;
+                        if (i + limitCount > updateChannelList.size()) {
+                            toIndex = updateChannelList.size();
+                        }
+                        result += channelMapper.batchUpdate(updateChannelList.subList(i, toIndex));
+                    }
+                } else {
+                    result += channelMapper.batchUpdate(updateChannelList);
+                }
+            }
+        }
+        return result;
+    }
+
+    @Override
+    public ResourceBaseInfo getOverview() {
+
+        int online = channelMapper.getOnlineCount();
+        int total = channelMapper.getAllChannelCount();
+
+        return new ResourceBaseInfo(total, online);
+    }
+
+    @Override
+    public List<Device> getDeviceByChannelId(String channelId) {
+
+        return channelMapper.getDeviceByChannelDeviceId(channelId);
+    }
+
+    @Override
+    @Transactional
+    public int deleteChannelsForNotify(List<DeviceChannel> channels) {
+        int limitCount = 1000;
+        int result = 0;
+        if (!channels.isEmpty()) {
+            if (channels.size() > limitCount) {
+                for (int i = 0; i < channels.size(); i += limitCount) {
+                    int toIndex = i + limitCount;
+                    if (i + limitCount > channels.size()) {
+                        toIndex = channels.size();
+                    }
+                    result += channelMapper.batchDel(channels.subList(i, toIndex));
+                }
+            } else {
+                result += channelMapper.batchDel(channels);
+            }
+        }
+        return result;
+    }
+
+    @Transactional
+    @Override
+    public int updateChannelsStatus(List<DeviceChannel> channels) {
+        int limitCount = 1000;
+        int result = 0;
+        if (!channels.isEmpty()) {
+            if (channels.size() > limitCount) {
+                for (int i = 0; i < channels.size(); i += limitCount) {
+                    int toIndex = i + limitCount;
+                    if (i + limitCount > channels.size()) {
+                        toIndex = channels.size();
+                    }
+                    result += channelMapper.batchUpdateStatus(channels.subList(i, toIndex));
+                }
+            } else {
+                result += channelMapper.batchUpdateStatus(channels);
+            }
+        }
+        return result;
+    }
+
+    @Override
+    public void online(DeviceChannel channel) {
+        channelMapper.online(channel.getId());
+    }
+
+    @Override
+    public void offline(DeviceChannel channel) {
+        channelMapper.offline(channel.getId());
+    }
+
+    @Override
+    public void delete(DeviceChannel channel) {
+        channelMapper.del(channel.getId());
+    }
+
+    @Override
+    public DeviceChannel getOne(String deviceId, String channelId) {
+        Device device = deviceMapper.getDeviceByDeviceId(deviceId);
+        if (device == null) {
+            throw new ControllerException(ErrorCode.ERROR100.getCode(), "未找到设备:" + deviceId);
+        }
+        return channelMapper.getOneByDeviceId(device.getId(), channelId);
+    }
+
+    @Override
+    public DeviceChannel getOneForSource(String deviceId, String channelId) {
+        Device device = deviceMapper.getDeviceByDeviceId(deviceId);
+        if (device == null) {
+            throw new ControllerException(ErrorCode.ERROR100.getCode(), "未找到设备:" + deviceId);
+        }
+        return channelMapper.getOneByDeviceIdForSource(device.getId(), channelId);
+    }
+
+    @Override
+    public DeviceChannel getOneForSource(int deviceDbId, String channelId) {
+        return channelMapper.getOneByDeviceIdForSource(deviceDbId, channelId);
+    }
+
+    @Override
+    public DeviceChannel getOneBySourceId(int deviceDbId, String channelId) {
+        return channelMapper.getOneBySourceChannelId(deviceDbId, channelId);
+    }
+
+    @Override
+    @Transactional
+    public synchronized void batchUpdateChannelForNotify(List<DeviceChannel> channels) {
+        String now = DateUtil.getNow();
+        for (DeviceChannel channel : channels) {
+            channel.setUpdateTime(now);
+        }
+        int limitCount = 1000;
+        if (!channels.isEmpty()) {
+            if (channels.size() > limitCount) {
+                for (int i = 0; i < channels.size(); i += limitCount) {
+                    int toIndex = i + limitCount;
+                    if (i + limitCount > channels.size()) {
+                        toIndex = channels.size();
+                    }
+                    channelMapper.batchUpdateForNotify(channels.subList(i, toIndex));
+                }
+            } else {
+                channelMapper.batchUpdateForNotify(channels);
+            }
+        }
+    }
+
+    @Override
+    @Transactional
+    public void batchAddChannel(List<DeviceChannel> channels) {
+        String now = DateUtil.getNow();
+        for (DeviceChannel channel : channels) {
+            channel.setUpdateTime(now);
+            channel.setCreateTime(now);
+        }
+        int limitCount = 1000;
+        if (!channels.isEmpty()) {
+            if (channels.size() > limitCount) {
+                for (int i = 0; i < channels.size(); i += limitCount) {
+                    int toIndex = i + limitCount;
+                    if (i + limitCount > channels.size()) {
+                        toIndex = channels.size();
+                    }
+                    channelMapper.batchAdd(channels.subList(i, toIndex));
+                }
+            } else {
+                channelMapper.batchAdd(channels);
+            }
+        }
+        for (DeviceChannel channel : channels) {
+            if (channel.getParentId() != null) {
+                channelMapper.updateChannelSubCount(channel.getDataDeviceId(), channel.getParentId());
+            }
+        }
+    }
+
+    @Override
+    public void updateChannelStreamIdentification(DeviceChannel channel) {
+        Assert.hasLength(channel.getStreamIdentification(), "码流标识必须存在");
+        if (ObjectUtils.isEmpty(channel.getStreamIdentification())) {
+            log.info("[重置通道码流类型] 设备: {}, 码流: {}", channel.getDeviceId(), channel.getStreamIdentification());
+        } else {
+            log.info("[更新通道码流类型] 设备: {}, 通道:{}, 码流: {}", channel.getDeviceId(), channel.getDeviceId(), channel.getStreamIdentification());
+        }
+        if (channel.getId() > 0) {
+            channelMapper.updateChannelStreamIdentification(channel);
+        } else {
+            channelMapper.updateAllChannelStreamIdentification(channel.getStreamIdentification());
+        }
+    }
+
+    @Override
+    public List<DeviceChannel> queryChaneListByDeviceId(String deviceId) {
+        Device device = deviceMapper.getDeviceByDeviceId(deviceId);
+        if (device == null) {
+            throw new ControllerException(ErrorCode.ERROR100.getCode(), "未找到通道:" + deviceId);
+        }
+        return channelMapper.queryChannelsByDeviceDbId(device.getId());
+    }
+
+    @Override
+    public List<DeviceChannel> queryChaneListByDeviceDbId(Integer deviceDbId) {
+        return channelMapper.queryChannelsByDeviceDbId(deviceDbId);
+    }
+
+    @Override
+    public List<Integer> queryChaneIdListByDeviceDbIds(List<Integer> deviceDbIds) {
+        return channelMapper.queryChaneIdListByDeviceDbIds(deviceDbIds);
+    }
+
+    @Override
+    public void handlePtzCmd(@NotNull Integer dataDeviceId, @NotNull Integer gbId, Element rootElement, DeviceControlType type, ErrorCallback<String> callback) {
+
+        // 根据通道ID,获取所属设备
+        Device device = deviceMapper.query(dataDeviceId);
+        if (device == null) {
+            // 不存在则回复404
+            log.warn("[INFO 消息] 通道所属设备不存在, 设备ID: {}", dataDeviceId);
+            callback.run(Response.NOT_FOUND, "device  not found", null);
+            return;
+        }
+
+        DeviceChannel deviceChannel = channelMapper.getOneForSource(gbId);
+        if (deviceChannel == null) {
+            log.warn("[deviceControl] 未找到设备原始通道, 设备: {}({}),通道编号:{}", device.getName(), device.getDeviceId(), gbId);
+            callback.run(Response.NOT_FOUND, "channel  not found", null);
+            return;
+        }
+        log.info("[deviceControl] 命令: {}, 设备: {}({}), 通道{}({}", type, device.getName(), device.getDeviceId(), deviceChannel.getName(), deviceChannel.getDeviceId());
+        String cmdString = getText(rootElement, type.getVal());
+        try {
+            cmder.fronEndCmd(device, deviceChannel.getDeviceId(), cmdString, errorResult -> {
+                callback.run(errorResult.statusCode, errorResult.msg, null);
+            }, errorResult -> {
+                callback.run(errorResult.statusCode, errorResult.msg, null);
+            });
+        } catch (InvalidArgumentException | SipException | ParseException e) {
+            log.error("[命令发送失败] 云台/前端: {}", e.getMessage());
+        }
+    }
+
+    @Override
+    public void startPlay(Integer channelId, String stream) {
+        channelMapper.startPlay(channelId, stream);
+    }
+
+    @Override
+    public void stopPlay(Integer channelId) {
+        channelMapper.stopPlayById(channelId);
+    }
+
+    @Override
+    public void cleanChannelsForDevice(int deviceId) {
+        channelMapper.cleanChannelsByDeviceId(deviceId);
+    }
+
+    @Override
+    @Transactional
+    public boolean resetChannels(int deviceDbId, List<DeviceChannel> deviceChannelList) {
+        if (CollectionUtils.isEmpty(deviceChannelList)) {
+            return false;
+        }
+        List<DeviceChannel> allChannels = channelMapper.queryAllChannelsForRefresh(deviceDbId);
+        Map<String, DeviceChannel> allChannelMap = new HashMap<>();
+        if (!allChannels.isEmpty()) {
+            for (DeviceChannel deviceChannel : allChannels) {
+                allChannelMap.put(deviceChannel.getDataDeviceId() + deviceChannel.getDeviceId(), deviceChannel);
+            }
+        }
+        // 数据去重
+        List<DeviceChannel> channels = new ArrayList<>();
+
+        List<DeviceChannel> updateChannels = new ArrayList<>();
+        List<DeviceChannel> addChannels = new ArrayList<>();
+        List<DeviceChannel> deleteChannels = new ArrayList<>();
+        StringBuilder stringBuilder = new StringBuilder();
+        Map<String, Integer> subContMap = new HashMap<>();
+
+        for (DeviceChannel deviceChannel : deviceChannelList) {
+            DeviceChannel channelInDb = allChannelMap.get(deviceChannel.getDataDeviceId() + deviceChannel.getDeviceId());
+            if (channelInDb != null) {
+                deviceChannel.setStreamId(channelInDb.getStreamId());
+                deviceChannel.setHasAudio(channelInDb.isHasAudio());
+                deviceChannel.setId(channelInDb.getId());
+                
+                deviceChannel.setUpdateTime(DateUtil.getNow());
+                updateChannels.add(deviceChannel);
+            } else {
+                deviceChannel.setCreateTime(DateUtil.getNow());
+                deviceChannel.setUpdateTime(DateUtil.getNow());
+                addChannels.add(deviceChannel);
+            }
+            allChannelMap.remove(deviceChannel.getDataDeviceId() + deviceChannel.getDeviceId());
+            channels.add(deviceChannel);
+            if (!ObjectUtils.isEmpty(deviceChannel.getParentId())) {
+                if (subContMap.get(deviceChannel.getParentId()) == null) {
+                    subContMap.put(deviceChannel.getParentId(), 1);
+                } else {
+                    Integer count = subContMap.get(deviceChannel.getParentId());
+                    subContMap.put(deviceChannel.getParentId(), count++);
+                }
+            }
+        }
+        deleteChannels.addAll(allChannelMap.values());
+        if (!channels.isEmpty()) {
+            for (DeviceChannel channel : channels) {
+                if (subContMap.get(channel.getDeviceId()) != null) {
+                    Integer count = subContMap.get(channel.getDeviceId());
+                    if (count > 0) {
+                        channel.setSubCount(count);
+                        channel.setParental(1);
+                    }
+                }
+            }
+        }
+
+        if (stringBuilder.length() > 0) {
+            log.info("[目录查询]收到的数据存在重复: {}", stringBuilder);
+        }
+        if (CollectionUtils.isEmpty(channels)) {
+            log.info("通道重设,数据为空={}", deviceChannelList);
+            return false;
+        }
+        int limitCount = 500;
+        if (!addChannels.isEmpty()) {
+            if (addChannels.size() > limitCount) {
+                for (int i = 0; i < addChannels.size(); i += limitCount) {
+                    int toIndex = i + limitCount;
+                    if (i + limitCount > addChannels.size()) {
+                        toIndex = addChannels.size();
+                    }
+                    channelMapper.batchAdd(addChannels.subList(i, toIndex));
+                }
+            } else {
+                channelMapper.batchAdd(addChannels);
+            }
+        }
+        if (!updateChannels.isEmpty()) {
+            if (updateChannels.size() > limitCount) {
+                for (int i = 0; i < updateChannels.size(); i += limitCount) {
+                    int toIndex = i + limitCount;
+                    if (i + limitCount > updateChannels.size()) {
+                        toIndex = updateChannels.size();
+                    }
+                    channelMapper.batchUpdate(updateChannels.subList(i, toIndex));
+                }
+            } else {
+                channelMapper.batchUpdate(updateChannels);
+            }
+            // 不对收到的通道做比较,已确定是否真的发生变化,所以不发送更新通知
+
+        }
+        if (!deleteChannels.isEmpty()) {
+            if (deleteChannels.size() > limitCount) {
+                for (int i = 0; i < deleteChannels.size(); i += limitCount) {
+                    int toIndex = i + limitCount;
+                    if (i + limitCount > deleteChannels.size()) {
+                        toIndex = deleteChannels.size();
+                    }
+                    channelMapper.batchDel(deleteChannels.subList(i, toIndex));
+                }
+            } else {
+                channelMapper.batchDel(deleteChannels);
+            }
+        }
+        return true;
+
+    }
+
+    @Override
+    public List<DeviceChannel> getSubChannels(int deviceDbId, String channelId, String query, Boolean channelType, Boolean online, int page, int count) {
+        String civilCode = null;
+        String parentId = null;
+        String businessGroupId = null;
+        if (channelId.length() <= 8) {
+            civilCode = channelId;
+        } else {
+            GbCode decode = GbCode.decode(channelId);
+            if (Integer.parseInt(decode.getTypeCode()) == 215) {
+                businessGroupId = channelId;
+            } else {
+                parentId = channelId;
+            }
+        }
+        if (query != null) {
+            query = query.replaceAll("/", "//")
+                    .replaceAll("%", "/%")
+                    .replaceAll("_", "/_");
+        }
+        return channelMapper.queryChannels(deviceDbId, civilCode, businessGroupId, parentId, query, channelType, online, null);
+    }
+
+    @Override
+    public List<DeviceChannelExtend> queryChannelExtendsByDeviceId(String deviceId, List<String> channelIds, Boolean online) {
+        return channelMapper.queryChannelsWithDeviceInfo(deviceId, null, null, null, online, channelIds);
+    }
+
+    @Override
+    public List<DeviceChannel> queryChannelsByDeviceId(String deviceId, String query, Boolean hasSubChannel, Boolean online, int pageNum, int pageSize) {
+        Device device = deviceMapper.getDeviceByDeviceId(deviceId);
+        if (device == null) {
+            throw new ControllerException(ErrorCode.ERROR100.getCode(), "未找到设备:" + deviceId);
+        }
+        if (query != null) {
+            query = query.replaceAll("/", "//")
+                    .replaceAll("%", "/%")
+                    .replaceAll("_", "/_");
+        }
+        startPage();
+        return channelMapper.queryChannels(device.getId(), null, null, null, query, hasSubChannel, online, null);
+    }
+
+    @Override
+    public List<Device> queryDeviceWithAsMessageChannel() {
+        return deviceMapper.queryDeviceWithAsMessageChannel();
+    }
+
+    @Override
+    public DeviceChannel getRawChannel(int id) {
+        return deviceMapper.getRawChannel(id);
+    }
+
+    @Override
+    public DeviceChannel getOneById(Integer channelId) {
+        return channelMapper.getOne(channelId);
+    }
+
+    @Override
+    public DeviceChannel getOneForSourceById(Integer channelId) {
+        return channelMapper.getOneForSource(channelId);
+    }
+
+    @Override
+    public void changeAudio(Integer channelId, Boolean audio) {
+        channelMapper.changeAudio(channelId, audio);
+    }
+
+    @Override
+    public void updateChannelStatus(DeviceChannel channel) {
+        channelMapper.updateStatus(channel);
+    }
+
+    @Override
+    public void addChannel(DeviceChannel channel) {
+        channel.setDataType(ChannelDataType.GB28181.value);
+        channel.setDataDeviceId(channel.getDataDeviceId());
+        channelMapper.add(channel);
+    }
+
+    @Override
+    public void updateChannelForNotify(DeviceChannel channel) {
+        channelMapper.updateChannelForNotify(channel);
+    }
+}

Неке датотеке нису приказане због велике количине промена