瀏覽代碼

feat: 用户权限部分升级
1. 下发拉流命令逻辑部分调整, 优化逻辑
2. sip管理功能界面制作完成
3. 优化推流缓存数据, 增加缓存适用性

kindring 1 年之前
父節點
當前提交
a2e98d46a2
共有 56 個文件被更改,包括 2365 次插入875 次删除
  1. 26 4
      src/main/java/com/genersoft/iot/vmp/common/StreamInfo.java
  2. 2 2
      src/main/java/com/genersoft/iot/vmp/conf/ApiAccessFilter.java
  3. 1 0
      src/main/java/com/genersoft/iot/vmp/conf/GlobalExceptionHandler.java
  4. 27 27
      src/main/java/com/genersoft/iot/vmp/conf/security/WebSecurityConfig.java
  5. 10 10
      src/main/java/com/genersoft/iot/vmp/gb28181/bean/Device.java
  6. 11 8
      src/main/java/com/genersoft/iot/vmp/gb28181/bean/SipUserConfig.java
  7. 15 4
      src/main/java/com/genersoft/iot/vmp/gb28181/transmit/callback/DeferredResultHolder.java
  8. 30 15
      src/main/java/com/genersoft/iot/vmp/gb28181/transmit/callback/RequestMessage.java
  9. 26 17
      src/main/java/com/genersoft/iot/vmp/gb28181/transmit/cmd/ISIPCommander.java
  10. 105 77
      src/main/java/com/genersoft/iot/vmp/gb28181/transmit/cmd/SIPRequestHeaderProvider.java
  11. 113 35
      src/main/java/com/genersoft/iot/vmp/gb28181/transmit/cmd/impl/SIPCommander.java
  12. 2 2
      src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/RegisterRequestProcessor.java
  13. 7 5
      src/main/java/com/genersoft/iot/vmp/service/IAccountService.java
  14. 30 0
      src/main/java/com/genersoft/iot/vmp/service/IAdminService.java
  15. 2 0
      src/main/java/com/genersoft/iot/vmp/service/IDeviceChannelService.java
  16. 5 1
      src/main/java/com/genersoft/iot/vmp/service/IPlayService.java
  17. 39 0
      src/main/java/com/genersoft/iot/vmp/service/ISipConfigService.java
  18. 2 0
      src/main/java/com/genersoft/iot/vmp/service/bean/BroadcastCallback.java
  19. 4 6
      src/main/java/com/genersoft/iot/vmp/service/impl/AccountServiceImpl.java
  20. 36 0
      src/main/java/com/genersoft/iot/vmp/service/impl/AdminServiceImpl.java
  21. 5 0
      src/main/java/com/genersoft/iot/vmp/service/impl/DeviceChannelServiceImpl.java
  22. 3 3
      src/main/java/com/genersoft/iot/vmp/service/impl/MediaServerServiceImpl.java
  23. 385 148
      src/main/java/com/genersoft/iot/vmp/service/impl/PlayServiceImpl.java
  24. 41 1
      src/main/java/com/genersoft/iot/vmp/service/impl/SipConfigService.java
  25. 8 0
      src/main/java/com/genersoft/iot/vmp/storager/IRedisCatchStorage.java
  26. 5 5
      src/main/java/com/genersoft/iot/vmp/storager/dao/AccountMapper.java
  27. 39 0
      src/main/java/com/genersoft/iot/vmp/storager/dao/AdminMapper.java
  28. 3 0
      src/main/java/com/genersoft/iot/vmp/storager/dao/DeviceChannelMapper.java
  29. 27 15
      src/main/java/com/genersoft/iot/vmp/storager/dao/SipConfigMapper.java
  30. 24 8
      src/main/java/com/genersoft/iot/vmp/storager/impl/RedisCatchStorageImpl.java
  31. 27 22
      src/main/java/com/genersoft/iot/vmp/storager/impl/VideoManagerStorageImpl.java
  32. 33 0
      src/main/java/com/genersoft/iot/vmp/utils/AuthorUtil.java
  33. 60 0
      src/main/java/com/genersoft/iot/vmp/utils/DeviceHelper.java
  34. 12 10
      src/main/java/com/genersoft/iot/vmp/utils/redis/RedisUtil.java
  35. 6 8
      src/main/java/com/genersoft/iot/vmp/vmanager/account/AccountController.java
  36. 54 9
      src/main/java/com/genersoft/iot/vmp/vmanager/account/AccountDeviceControl.java
  37. 4 1
      src/main/java/com/genersoft/iot/vmp/vmanager/bean/ErrorCode.java
  38. 6 0
      src/main/java/com/genersoft/iot/vmp/vmanager/bean/ErrorHook.java
  39. 0 4
      src/main/java/com/genersoft/iot/vmp/vmanager/bean/FailResult.java
  40. 16 3
      src/main/java/com/genersoft/iot/vmp/vmanager/bean/WVPResult.java
  41. 34 29
      src/main/java/com/genersoft/iot/vmp/vmanager/gb28181/device/DeviceQuery.java
  42. 88 137
      src/main/java/com/genersoft/iot/vmp/vmanager/gb28181/play/PlayController.java
  43. 75 54
      src/main/java/com/genersoft/iot/vmp/vmanager/gb28181/playback/PlaybackController.java
  44. 109 132
      src/main/java/com/genersoft/iot/vmp/vmanager/gb28181/ptz/PtzController.java
  45. 58 50
      src/main/java/com/genersoft/iot/vmp/vmanager/gb28181/record/GBRecordController.java
  46. 105 0
      src/main/java/com/genersoft/iot/vmp/vmanager/server/sipController.java
  47. 1 0
      web_src/src/components/channelList.vue
  48. 3 0
      web_src/src/components/com/livePlayBox.vue
  49. 295 0
      web_src/src/components/com/sipConfigEdit.vue
  50. 10 2
      web_src/src/components/console.vue
  51. 283 0
      web_src/src/components/setting/SipConfigs.vue
  52. 4 2
      web_src/src/layout/UiHeader.vue
  53. 1 1
      web_src/src/pages/index/main.js
  54. 22 16
      web_src/src/router/index.js
  55. 17 0
      web_src/src/until/FieldCheck.js
  56. 9 2
      设备绑定机制.md

+ 26 - 4
src/main/java/com/genersoft/iot/vmp/common/StreamInfo.java

@@ -19,6 +19,12 @@ public class StreamInfo implements Serializable, Cloneable{
     @Schema(description = "IP")
     private String ip;
 
+    @Schema(description = "ssrc")
+    private String ssrc;
+
+    @Schema(description = "isBack")
+    private Boolean isBack;
+
     @Schema(description = "HTTP-FLV流地址")
     private StreamURL flv;
 
@@ -312,8 +318,16 @@ public class StreamInfo implements Serializable, Cloneable{
         }
     }
 
+    public Boolean getBack() {
+        return isBack;
+    }
+
+    public void setBack(Boolean back) {
+        isBack = back;
+    }
+
 
-    public static class TransactionInfo{
+    public static class TransactionInfo {
         public String callId;
         public String localTag;
         public String remoteTag;
@@ -514,12 +528,20 @@ public class StreamInfo implements Serializable, Cloneable{
         this.transactionInfo = transactionInfo;
     }
 
+    public String getSsrc() {
+        return ssrc;
+    }
+
+    public void setSsrc(String ssrc) {
+        this.ssrc = ssrc;
+    }
+
     @Override
     public StreamInfo clone() {
         StreamInfo instance = null;
-        try{
-            instance = (StreamInfo)super.clone();
-        }catch(CloneNotSupportedException e) {
+        try {
+            instance = (StreamInfo) super.clone();
+        } catch (CloneNotSupportedException e) {
             e.printStackTrace();
         }
         return instance;

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

@@ -24,8 +24,8 @@ import java.io.IOException;
 /**
  * @author lin
  */
-@WebFilter(filterName = "ApiAccessFilter", urlPatterns = "/api/*", asyncSupported=true)
-@Component
+//@WebFilter(filterName = "ApiAccessFilter", urlPatterns = "/api/*", asyncSupported=true)
+//@Component
 public class ApiAccessFilter extends OncePerRequestFilter {
 
     private final static Logger logger = LoggerFactory.getLogger(ApiAccessFilter.class);

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

@@ -42,6 +42,7 @@ public class GlobalExceptionHandler {
     @ExceptionHandler(IllegalStateException.class)
     @ResponseStatus(HttpStatus.BAD_REQUEST)
     public WVPResult<String> exceptionHandler(IllegalStateException e) {
+        logger.error("[400异常]: 400", e);
         return WVPResult.fail(ErrorCode.ERROR400);
     }
 

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

@@ -131,36 +131,36 @@ public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
         }
     }
 
-    /**
-     * 配置认证方式
-     *
-     * @param auth
-     * @throws Exception
-     */
-    @Override
-    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
-        DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
-        // 设置不隐藏 未找到用户异常
-        provider.setHideUserNotFoundExceptions(true);
-        // 用户认证service - 查询数据库的逻辑
-        provider.setUserDetailsService(userDetailsService);
-        // 设置密码加密算法
-        provider.setPasswordEncoder(passwordEncoder());
-        auth.authenticationProvider(provider);
-    }
+//    /**
+//     * 配置认证方式
+//     *
+//     * @param auth
+//     * @throws Exception
+//     */
+//    @Override
+//    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
+//        DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
+//        // 设置不隐藏 未找到用户异常
+//        provider.setHideUserNotFoundExceptions(true);
+//        // 用户认证service - 查询数据库的逻辑
+//        provider.setUserDetailsService(userDetailsService);
+//        // 设置密码加密算法
+//        provider.setPasswordEncoder(passwordEncoder());
+//        auth.authenticationProvider(provider);
+//    }
 
 
     // 投票模式鉴权链接
-    @Bean
-    public AccessDecisionManager accessDecisionManager() {
-        List<AccessDecisionVoter<? extends Object>> decisionVoters = Arrays.asList(
-                new JwtAccess<>(),
-                new CustomAccess<>(deviceShare, matchShareUrl),
-                new queryAccess<>(matchUrl)
-        );
-        logger.info("decision {}", decisionVoters.size());
-        return new AffirmativeBased(decisionVoters);
-    }
+//    @Bean
+//    public AccessDecisionManager accessDecisionManager() {
+//        List<AccessDecisionVoter<? extends Object>> decisionVoters = Arrays.asList(
+//                new JwtAccess<>(),
+//                new CustomAccess<>(deviceShare, matchShareUrl),
+//                new queryAccess<>(matchUrl)
+//        );
+//        logger.info("decision {}", decisionVoters.size());
+//        return new AffirmativeBased(decisionVoters);
+//    }
 
     @Override
     protected void configure(HttpSecurity http) throws Exception {

+ 10 - 10
src/main/java/com/genersoft/iot/vmp/gb28181/bean/Device.java

@@ -11,10 +11,10 @@ import io.swagger.v3.oas.annotations.media.Schema;
 public class Device {
 
 	/**
-	 * 设备表id, 用于绑定设备
-	 */
-	@Schema(description = "设备id, 数据库Id")
-	private int id;
+     * 设备表id, 用于绑定设备
+     */
+    @Schema(description = "设备id, 数据库Id")
+    private String id;
 
 	/**
 	 * 设备国标编号
@@ -528,14 +528,14 @@ public class Device {
 
 	public void setPlayChannel(String playChannel) {
 		this.playChannel = playChannel;
-	}
+    }
 
-	public int getId() {
-		return id;
-	}
+    public String getId() {
+        return id;
+    }
 
-	public void setId(int id) {
-		this.id = id;
+    public void setId(String id) {
+        this.id = id;
 	}
 
 	public String getDomain() {

+ 11 - 8
src/main/java/com/genersoft/iot/vmp/gb28181/bean/SipUserConfig.java

@@ -16,9 +16,9 @@ import io.swagger.v3.oas.annotations.media.Schema;
  */
 public class SipUserConfig {
     @Schema(description = "配置id")
-    private int id;
+    private String id;
     @Schema(description = "管理员id")
-    private int adminId;
+    private String adminId;
     @Schema(description = "sip域名")
     private String sipDomain;
     @Schema(description = "服务器id")
@@ -36,11 +36,11 @@ public class SipUserConfig {
     @Schema(description = "创建时间")
     private String createTime;
 
-    public void setId(int id) {
+    public void setId(String id) {
         this.id = id;
     }
 
-    public void setAdminId(int adminId) {
+    public void setAdminId(String adminId) {
         this.adminId = adminId;
     }
 
@@ -69,11 +69,11 @@ public class SipUserConfig {
         this.createTime = createTime;
     }
 
-    public int getId() {
+    public String getId() {
         return id;
     }
 
-    public int getAdminId() {
+    public String getAdminId() {
         return adminId;
     }
 
@@ -97,7 +97,10 @@ public class SipUserConfig {
         return enable;
     }
 
-    public Boolean isEnable() {
+    public Boolean isEnableFlag() {
+        if (enable == null) {
+            return false;
+        }
         return enable.equals("1");
     }
 
@@ -113,7 +116,7 @@ public class SipUserConfig {
         this.enableBind = enableBind;
     }
 
-    public Boolean isEnableBind() {
+    public Boolean isEnableBindFlag() {
         // 为空判断
         if (enableBind == null) {
             return false;

+ 15 - 4
src/main/java/com/genersoft/iot/vmp/gb28181/transmit/callback/DeferredResultHolder.java

@@ -1,6 +1,9 @@
 package com.genersoft.iot.vmp.gb28181.transmit.callback;
 
 import com.genersoft.iot.vmp.vmanager.bean.DeferredResultEx;
+import com.genersoft.iot.vmp.vmanager.gb28181.play.PlayController;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 import org.springframework.stereotype.Component;
 import org.springframework.util.ObjectUtils;
 import org.springframework.web.context.request.async.DeferredResult;
@@ -18,8 +21,8 @@ import java.util.concurrent.ConcurrentHashMap;
 @SuppressWarnings(value = {"rawtypes", "unchecked"})
 @Component
 public class DeferredResultHolder {
-	
-	public static final String CALLBACK_CMD_DEVICESTATUS = "CALLBACK_DEVICESTATUS";
+    private final static Logger logger = LoggerFactory.getLogger(DeferredResultHolder.class);
+    public static final String CALLBACK_CMD_DEVICESTATUS = "CALLBACK_DEVICESTATUS";
 	
 	public static final String CALLBACK_CMD_DEVICEINFO = "CALLBACK_DEVICEINFO";
 	
@@ -77,7 +80,8 @@ public class DeferredResultHolder {
 	public void put(String key, String id, DeferredResult result) {
 		Map<String, DeferredResultEx> deferredResultMap = map.get(key);
 		if (deferredResultMap == null) {
-			deferredResultMap = new ConcurrentHashMap<>();
+            logger.info("put key: " + key + " id: " + id + " result: " + result);
+            deferredResultMap = new ConcurrentHashMap<>();
 			map.put(key, deferredResultMap);
 		}
 		deferredResultMap.put(id, new DeferredResultEx(result));
@@ -116,8 +120,10 @@ public class DeferredResultHolder {
 	 * @param msg
 	 */
 	public void invokeResult(RequestMessage msg) {
+        logger.info("release msg");
 		Map<String, DeferredResultEx> deferredResultMap = map.get(msg.getKey());
 		if (deferredResultMap == null) {
+            logger.warn("DeferredResultHolder invokeResult not found key: " + msg.getKey());
 			return;
 		}
 		if(msg.getId() == null) {
@@ -125,13 +131,16 @@ public class DeferredResultHolder {
 				result.getDeferredResult().setResult(msg.getData());
 			});
 			deferredResultMap.clear();
-			map.remove(msg.getKey());
+            map.remove(msg.getKey());
+            logger.warn("DeferredResultHolder msg id not found id: " + msg.getId());
 			return;
 		}
 		DeferredResultEx result = deferredResultMap.get(msg.getId());
 		if (result == null) {
+            logger.warn("DeferredResultHolder result not found id: " + msg.getId());
 			return;
 		}
+        // 设置结果
 		result.getDeferredResult().setResult(msg.getData());
 		deferredResultMap.remove(msg.getId());
 		if (deferredResultMap.size() == 0) {
@@ -170,4 +179,6 @@ public class DeferredResultHolder {
 			map.remove(msg.getKey());
 		}
 	}
+
+
 }

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

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

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

@@ -10,6 +10,7 @@ import com.genersoft.iot.vmp.gb28181.event.SipSubscribe;
 import com.genersoft.iot.vmp.media.zlm.ZlmHttpHookSubscribe;
 import com.genersoft.iot.vmp.media.zlm.dto.MediaServerItem;
 import com.genersoft.iot.vmp.service.bean.SSRCInfo;
+import com.genersoft.iot.vmp.vmanager.bean.ErrorHook;
 import gov.nist.javax.sip.message.SIPRequest;
 
 import javax.sip.InvalidArgumentException;
@@ -95,25 +96,33 @@ public interface ISIPCommander {
 	 * @param device		控制设备
 	 * @param channelId		预览通道
 	 * @param cmdString		前端控制指令串
-	 */
-	void fronEndCmd(Device device, String channelId, String cmdString, SipSubscribe.Event errorEvent, SipSubscribe.Event okEvent) throws InvalidArgumentException, SipException, ParseException;
+     */
+    void fronEndCmd(Device device, String channelId, String cmdString, SipSubscribe.Event errorEvent, SipSubscribe.Event okEvent) throws InvalidArgumentException, SipException, ParseException;
 
-	/**
-	 * 请求预览视频流
-	 * @param device  视频设备
-	 * @param channelId  预览通道
-	 */
-	void playStreamCmd(MediaServerItem mediaServerItem, SSRCInfo ssrcInfo, Device device, String channelId,int isUsePs, ZlmHttpHookSubscribe.Event event, SipSubscribe.Event okEvent, SipSubscribe.Event errorEvent) throws InvalidArgumentException, SipException, ParseException;
+    /**
+     * 请求预览视频流
+     *
+     * @param device    视频设备
+     * @param channelId 预览通道
+     */
+    void playStreamCmd(MediaServerItem mediaServerItem, SSRCInfo ssrcInfo, Device device, String channelId, int isUsePs, ZlmHttpHookSubscribe.Event event, SipSubscribe.Event okEvent, SipSubscribe.Event errorEvent) throws InvalidArgumentException, SipException, ParseException;
 
-	/**
-	 * 请求回放视频流
-	 * 
-	 * @param device  视频设备
-	 * @param channelId  预览通道
-	 * @param startTime 开始时间,格式要求:yyyy-MM-dd HH:mm:ss
-	 * @param endTime 结束时间,格式要求:yyyy-MM-dd HH:mm:ss
-	 */
-	void playbackStreamCmd(MediaServerItem mediaServerItem, SSRCInfo ssrcInf, Device device, String channelId, String startTime, String endTime,InviteStreamCallback inviteStreamCallback, InviteStreamCallback event, SipSubscribe.Event okEvent, SipSubscribe.Event errorEvent) throws InvalidArgumentException, SipException, ParseException;
+
+    void sendPlayCmd(
+            MediaServerItem mediaServerItem, SSRCInfo ssrcInfo,
+            Device device, String channelId, int isUsePs,
+            ErrorHook errorHook
+    ) throws InvalidArgumentException, SipException, ParseException;
+
+    /**
+     * 请求回放视频流
+     *
+     * @param device    视频设备
+     * @param channelId 预览通道
+     * @param startTime 开始时间,格式要求:yyyy-MM-dd HH:mm:ss
+     * @param endTime   结束时间,格式要求:yyyy-MM-dd HH:mm:ss
+     */
+    void playbackStreamCmd(MediaServerItem mediaServerItem, SSRCInfo ssrcInf, Device device, String channelId, String startTime, String endTime, InviteStreamCallback inviteStreamCallback, InviteStreamCallback event, SipSubscribe.Event okEvent, SipSubscribe.Event errorEvent) throws InvalidArgumentException, SipException, ParseException;
 
 	/**
 	 * 请求历史媒体下载

+ 105 - 77
src/main/java/com/genersoft/iot/vmp/gb28181/transmit/cmd/SIPRequestHeaderProvider.java

@@ -8,10 +8,13 @@ import com.genersoft.iot.vmp.gb28181.bean.SipUserConfig;
 import com.genersoft.iot.vmp.gb28181.session.VideoStreamSessionManager;
 import com.genersoft.iot.vmp.gb28181.utils.SipUtils;
 import com.genersoft.iot.vmp.service.ISipConfigService;
+import com.genersoft.iot.vmp.service.impl.PlayServiceImpl;
 import com.genersoft.iot.vmp.storager.IRedisCatchStorage;
 import com.genersoft.iot.vmp.utils.GitUtil;
 import gov.nist.javax.sip.message.SIPRequest;
 import gov.nist.javax.sip.message.SIPResponse;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Component;
 
@@ -32,9 +35,9 @@ import java.util.ArrayList;
  */
 @Component
 public class SIPRequestHeaderProvider {
-
-	@Autowired
-	private SipConfig sipConfig;
+    private final static Logger logger = LoggerFactory.getLogger(SIPRequestHeaderProvider.class);
+    @Autowired
+    private SipConfig sipConfig;
 	
 	@Autowired
 	private SipLayer sipLayer;
@@ -52,18 +55,22 @@ public class SIPRequestHeaderProvider {
 	private ISipConfigService sipConfigService;
 
 	public Request createMessageRequest(Device device, String content, String viaTag, String fromTag, String toTag, CallIdHeader callIdHeader) throws ParseException, InvalidArgumentException, PeerUnavailableException {
-		Request request = null;
-		SipUserConfig sipUserConfig = sipConfigService.getSipConfigByDomain(device.getDomain());
-		// sipuri
-		SipURI requestURI = sipLayer.getSipFactory().createAddressFactory().createSipURI(device.getDeviceId(), device.getHostAddress());
-		// via
-		ArrayList<ViaHeader> viaHeaders = new ArrayList<ViaHeader>();
-		ViaHeader viaHeader = sipLayer.getSipFactory().createHeaderFactory().createViaHeader(sipLayer.getLocalIp(device.getLocalIp()), sipConfig.getPort(), device.getTransport(), viaTag);
-		viaHeader.setRPort();
-		viaHeaders.add(viaHeader);
-
-		// from
-		SipURI fromSipURI = sipLayer.getSipFactory().createAddressFactory().createSipURI(sipUserConfig.getServerId(), sipUserConfig.getSipDomain());
+        Request request;
+        SipUserConfig sipUserConfig = sipConfigService.getSipConfigByDomain(device.getDomain());
+        if (sipUserConfig == null) {
+            logger.error("无法获取到对应的sip配置信息,domain:" + device.getDomain());
+            return null;
+        }
+        // sipuri
+        SipURI requestURI = sipLayer.getSipFactory().createAddressFactory().createSipURI(device.getDeviceId(), device.getHostAddress());
+        // via
+        ArrayList<ViaHeader> viaHeaders = new ArrayList<ViaHeader>();
+        ViaHeader viaHeader = sipLayer.getSipFactory().createHeaderFactory().createViaHeader(sipLayer.getLocalIp(device.getLocalIp()), sipConfig.getPort(), device.getTransport(), viaTag);
+        viaHeader.setRPort();
+        viaHeaders.add(viaHeader);
+
+        // from
+        SipURI fromSipURI = sipLayer.getSipFactory().createAddressFactory().createSipURI(sipUserConfig.getServerId(), sipUserConfig.getSipDomain());
 		Address fromAddress = sipLayer.getSipFactory().createAddressFactory().createAddress(fromSipURI);
 		FromHeader fromHeader = sipLayer.getSipFactory().createHeaderFactory().createFromHeader(fromAddress, fromTag);
 		// to
@@ -87,18 +94,22 @@ public class SIPRequestHeaderProvider {
 	}
 	
 	public Request createInviteRequest(Device device, String channelId, String content, String viaTag, String fromTag, String toTag, String ssrc, CallIdHeader callIdHeader) throws ParseException, InvalidArgumentException, PeerUnavailableException {
-		Request request = null;
-		SipUserConfig sipUserConfig = sipConfigService.getSipConfigByDomain(device.getDomain());
-		//请求行
-		SipURI requestLine = sipLayer.getSipFactory().createAddressFactory().createSipURI(channelId, device.getHostAddress());
-		//via
-		ArrayList<ViaHeader> viaHeaders = new ArrayList<ViaHeader>();
-		HeaderFactory headerFactory = sipLayer.getSipFactory().createHeaderFactory();
-		ViaHeader viaHeader = sipLayer.getSipFactory().createHeaderFactory().createViaHeader(sipLayer.getLocalIp(device.getLocalIp()), sipConfig.getPort(), device.getTransport(), viaTag);
-		viaHeader.setRPort();
-		viaHeaders.add(viaHeader);
-
-		//from
+        Request request;
+        SipUserConfig sipUserConfig = sipConfigService.getSipConfigByDomain(device.getDomain());
+        if (sipUserConfig == null) {
+            logger.error("无法获取到对应的sip配置信息,domain:" + device.getDomain());
+            return null;
+        }
+        //请求行
+        SipURI requestLine = sipLayer.getSipFactory().createAddressFactory().createSipURI(channelId, device.getHostAddress());
+        //via
+        ArrayList<ViaHeader> viaHeaders = new ArrayList<ViaHeader>();
+        HeaderFactory headerFactory = sipLayer.getSipFactory().createHeaderFactory();
+        ViaHeader viaHeader = sipLayer.getSipFactory().createHeaderFactory().createViaHeader(sipLayer.getLocalIp(device.getLocalIp()), sipConfig.getPort(), device.getTransport(), viaTag);
+        viaHeader.setRPort();
+        viaHeaders.add(viaHeader);
+
+        //from
 		SipURI fromSipURI = sipLayer.getSipFactory().createAddressFactory().createSipURI(sipUserConfig.getServerId(), sipUserConfig.getSipDomain());
 		Address fromAddress = sipLayer.getSipFactory().createAddressFactory().createAddress(fromSipURI);
 		FromHeader fromHeader = sipLayer.getSipFactory().createHeaderFactory().createFromHeader(fromAddress, fromTag); //必须要有标记,否则无法创建会话,无法回应ack
@@ -128,18 +139,22 @@ public class SIPRequestHeaderProvider {
 	}
 	
 	public Request createPlaybackInviteRequest(Device device, String channelId, String content, String viaTag, String fromTag, String toTag, CallIdHeader callIdHeader, String ssrc) throws ParseException, InvalidArgumentException, PeerUnavailableException {
-		Request request = null;
-		SipUserConfig sipUserConfig = sipConfigService.getSipConfigByDomain(device.getDomain());
-		//请求行
-		SipURI requestLine = sipLayer.getSipFactory().createAddressFactory().createSipURI(channelId, device.getHostAddress());
-		// via
-		ArrayList<ViaHeader> viaHeaders = new ArrayList<ViaHeader>();
-		ViaHeader viaHeader = sipLayer.getSipFactory().createHeaderFactory().createViaHeader(sipLayer.getLocalIp(device.getLocalIp()), sipConfig.getPort(), device.getTransport(), viaTag);
-		viaHeader.setRPort();
-		viaHeaders.add(viaHeader);
-		//from
-		SipURI fromSipURI = sipLayer.getSipFactory().createAddressFactory().createSipURI(sipUserConfig.getServerId(), sipUserConfig.getSipDomain());
-		Address fromAddress = sipLayer.getSipFactory().createAddressFactory().createAddress(fromSipURI);
+        Request request;
+        SipUserConfig sipUserConfig = sipConfigService.getSipConfigByDomain(device.getDomain());
+        if (sipUserConfig == null) {
+            logger.error("无法获取到对应的sip配置信息,domain:" + device.getDomain());
+            return null;
+        }
+        //请求行
+        SipURI requestLine = sipLayer.getSipFactory().createAddressFactory().createSipURI(channelId, device.getHostAddress());
+        // via
+        ArrayList<ViaHeader> viaHeaders = new ArrayList<ViaHeader>();
+        ViaHeader viaHeader = sipLayer.getSipFactory().createHeaderFactory().createViaHeader(sipLayer.getLocalIp(device.getLocalIp()), sipConfig.getPort(), device.getTransport(), viaTag);
+        viaHeader.setRPort();
+        viaHeaders.add(viaHeader);
+        //from
+        SipURI fromSipURI = sipLayer.getSipFactory().createAddressFactory().createSipURI(sipUserConfig.getServerId(), sipUserConfig.getSipDomain());
+        Address fromAddress = sipLayer.getSipFactory().createAddressFactory().createAddress(fromSipURI);
 		FromHeader fromHeader = sipLayer.getSipFactory().createHeaderFactory().createFromHeader(fromAddress, fromTag); //必须要有标记,否则无法创建会话,无法回应ack
 		//to
 		SipURI toSipURI = sipLayer.getSipFactory().createAddressFactory().createSipURI(channelId, device.getHostAddress());
@@ -169,18 +184,22 @@ public class SIPRequestHeaderProvider {
 	}
 
 	public Request createByteRequest(Device device, String channelId, SipTransactionInfo transactionInfo) throws ParseException, InvalidArgumentException, PeerUnavailableException {
-		Request request = null;
-		SipUserConfig sipUserConfig = sipConfigService.getSipConfigByDomain(device.getDomain());
-		//请求行
-		SipURI requestLine = sipLayer.getSipFactory().createAddressFactory().createSipURI(channelId, device.getHostAddress());
-		// via
-		ArrayList<ViaHeader> viaHeaders = new ArrayList<ViaHeader>();
-		ViaHeader viaHeader = sipLayer.getSipFactory().createHeaderFactory().createViaHeader(sipLayer.getLocalIp(device.getLocalIp()), sipConfig.getPort(), device.getTransport(), SipUtils.getNewViaTag());
-		viaHeaders.add(viaHeader);
-		//from
-		SipURI fromSipURI = sipLayer.getSipFactory().createAddressFactory().createSipURI(sipUserConfig.getServerId(), sipUserConfig.getSipDomain());
-		Address fromAddress = sipLayer.getSipFactory().createAddressFactory().createAddress(fromSipURI);
-		FromHeader fromHeader = sipLayer.getSipFactory().createHeaderFactory().createFromHeader(fromAddress, transactionInfo.getFromTag());
+        Request request = null;
+        SipUserConfig sipUserConfig = sipConfigService.getSipConfigByDomain(device.getDomain());
+        if (sipUserConfig == null) {
+            logger.error("无法获取到对应的sip配置信息,domain:" + device.getDomain());
+            return null;
+        }
+        //请求行
+        SipURI requestLine = sipLayer.getSipFactory().createAddressFactory().createSipURI(channelId, device.getHostAddress());
+        // via
+        ArrayList<ViaHeader> viaHeaders = new ArrayList<ViaHeader>();
+        ViaHeader viaHeader = sipLayer.getSipFactory().createHeaderFactory().createViaHeader(sipLayer.getLocalIp(device.getLocalIp()), sipConfig.getPort(), device.getTransport(), SipUtils.getNewViaTag());
+        viaHeaders.add(viaHeader);
+        //from
+        SipURI fromSipURI = sipLayer.getSipFactory().createAddressFactory().createSipURI(sipUserConfig.getServerId(), sipUserConfig.getSipDomain());
+        Address fromAddress = sipLayer.getSipFactory().createAddressFactory().createAddress(fromSipURI);
+        FromHeader fromHeader = sipLayer.getSipFactory().createHeaderFactory().createFromHeader(fromAddress, transactionInfo.getFromTag());
 		//to
 		SipURI toSipURI = sipLayer.getSipFactory().createAddressFactory().createSipURI(channelId,device.getHostAddress());
 		Address toAddress = sipLayer.getSipFactory().createAddressFactory().createAddress(toSipURI);
@@ -204,19 +223,24 @@ public class SIPRequestHeaderProvider {
 		return request;
 	}
 
-	public Request createSubscribeRequest(Device device, String content, SIPRequest requestOld, Integer expires, String event, CallIdHeader callIdHeader) throws ParseException, InvalidArgumentException, PeerUnavailableException {
-		Request request = null;
-		SipUserConfig sipUserConfig = sipConfigService.getSipConfigByDomain(device.getDomain());
 
-		// sipuri
-		SipURI requestURI = sipLayer.getSipFactory().createAddressFactory().createSipURI(device.getDeviceId(), device.getHostAddress());
-		// via
-		ArrayList<ViaHeader> viaHeaders = new ArrayList<ViaHeader>();
-		ViaHeader viaHeader = sipLayer.getSipFactory().createHeaderFactory().createViaHeader(sipLayer.getLocalIp(device.getLocalIp()), sipConfig.getPort(),
-				device.getTransport(), SipUtils.getNewViaTag());
-		viaHeader.setRPort();
-		viaHeaders.add(viaHeader);
-		// from
+    public Request createSubscribeRequest(Device device, String content, SIPRequest requestOld, Integer expires, String event, CallIdHeader callIdHeader) throws ParseException, InvalidArgumentException, PeerUnavailableException {
+        Request request = null;
+        SipUserConfig sipUserConfig = sipConfigService.getSipConfigByDomain(device.getDomain());
+        if (sipUserConfig == null) {
+            logger.error("无法获取到对应的sip配置信息,domain:" + device.getDomain());
+            return null;
+        }
+
+        // sipuri
+        SipURI requestURI = sipLayer.getSipFactory().createAddressFactory().createSipURI(device.getDeviceId(), device.getHostAddress());
+        // via
+        ArrayList<ViaHeader> viaHeaders = new ArrayList<ViaHeader>();
+        ViaHeader viaHeader = sipLayer.getSipFactory().createHeaderFactory().createViaHeader(sipLayer.getLocalIp(device.getLocalIp()), sipConfig.getPort(),
+                device.getTransport(), SipUtils.getNewViaTag());
+        viaHeader.setRPort();
+        viaHeaders.add(viaHeader);
+        // from
 		SipURI fromSipURI = sipLayer.getSipFactory().createAddressFactory().createSipURI(sipUserConfig.getServerId(), sipUserConfig.getSipDomain());
 		Address fromAddress = sipLayer.getSipFactory().createAddressFactory().createAddress(fromSipURI);
 		FromHeader fromHeader = sipLayer.getSipFactory().createHeaderFactory().createFromHeader(fromAddress, requestOld == null ? SipUtils.getNewFromTag() :requestOld.getFromTag());
@@ -259,20 +283,24 @@ public class SIPRequestHeaderProvider {
 
 	public SIPRequest createInfoRequest(Device device, String channelId, String content, SipTransactionInfo transactionInfo)
 			throws SipException, ParseException, InvalidArgumentException {
-		if (device == null || transactionInfo == null) {
-			return null;
-		}
-		SipUserConfig sipUserConfig = sipConfigService.getSipConfigByDomain(device.getDomain());
-		SIPRequest request = null;
-		//请求行
-		SipURI requestLine = sipLayer.getSipFactory().createAddressFactory().createSipURI(channelId, device.getHostAddress());
-		// via
-		ArrayList<ViaHeader> viaHeaders = new ArrayList<ViaHeader>();
-		ViaHeader viaHeader = sipLayer.getSipFactory().createHeaderFactory().createViaHeader(sipLayer.getLocalIp(device.getLocalIp()), sipConfig.getPort(), device.getTransport(), SipUtils.getNewViaTag());
-		viaHeaders.add(viaHeader);
-		//from
-		SipURI fromSipURI = sipLayer.getSipFactory().createAddressFactory().createSipURI(sipUserConfig.getServerId(), sipUserConfig.getSipDomain());
-		Address fromAddress = sipLayer.getSipFactory().createAddressFactory().createAddress(fromSipURI);
+        if (device == null || transactionInfo == null) {
+            return null;
+        }
+        SipUserConfig sipUserConfig = sipConfigService.getSipConfigByDomain(device.getDomain());
+        if (sipUserConfig == null) {
+            logger.error("无法获取到对应的sip配置信息,domain:" + device.getDomain());
+            return null;
+        }
+        SIPRequest request = null;
+        //请求行
+        SipURI requestLine = sipLayer.getSipFactory().createAddressFactory().createSipURI(channelId, device.getHostAddress());
+        // via
+        ArrayList<ViaHeader> viaHeaders = new ArrayList<ViaHeader>();
+        ViaHeader viaHeader = sipLayer.getSipFactory().createHeaderFactory().createViaHeader(sipLayer.getLocalIp(device.getLocalIp()), sipConfig.getPort(), device.getTransport(), SipUtils.getNewViaTag());
+        viaHeaders.add(viaHeader);
+        //from
+        SipURI fromSipURI = sipLayer.getSipFactory().createAddressFactory().createSipURI(sipUserConfig.getServerId(), sipUserConfig.getSipDomain());
+        Address fromAddress = sipLayer.getSipFactory().createAddressFactory().createAddress(fromSipURI);
 		FromHeader fromHeader = sipLayer.getSipFactory().createHeaderFactory().createFromHeader(fromAddress, transactionInfo.getFromTag());
 		//to
 		SipURI toSipURI = sipLayer.getSipFactory().createAddressFactory().createSipURI(channelId,device.getHostAddress());

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

@@ -25,6 +25,9 @@ import com.genersoft.iot.vmp.service.IMediaServerService;
 import com.genersoft.iot.vmp.service.bean.SSRCInfo;
 import com.genersoft.iot.vmp.storager.IVideoManagerStorage;
 import com.genersoft.iot.vmp.utils.DateUtil;
+import com.genersoft.iot.vmp.vmanager.bean.ErrorCode;
+import com.genersoft.iot.vmp.vmanager.bean.ErrorHook;
+import com.genersoft.iot.vmp.vmanager.bean.WVPResult;
 import gov.nist.javax.sip.message.SIPRequest;
 import gov.nist.javax.sip.message.SIPResponse;
 import org.slf4j.Logger;
@@ -35,6 +38,7 @@ import org.springframework.stereotype.Component;
 import org.springframework.util.ObjectUtils;
 
 import javax.sip.InvalidArgumentException;
+import javax.sip.PeerUnavailableException;
 import javax.sip.ResponseEvent;
 import javax.sip.SipException;
 import javax.sip.header.CallIdHeader;
@@ -203,10 +207,13 @@ public class SIPCommander implements ISIPCommander {
         ptzXml.append("<ControlPriority>5</ControlPriority>\r\n");
         ptzXml.append("</Info>\r\n");
         ptzXml.append("</Control>\r\n");
-        
-        Request request = headerProvider.createMessageRequest(device, ptzXml.toString(), SipUtils.getNewViaTag(), SipUtils.getNewFromTag(), null, sipSender.getNewCallIdHeader(sipLayer.getLocalIp(device.getLocalIp()),device.getTransport()));
 
-        sipSender.transmitRequest(sipLayer.getLocalIp(device.getLocalIp()),request);
+        Request request = headerProvider.createMessageRequest(device, ptzXml.toString(), SipUtils.getNewViaTag(), SipUtils.getNewFromTag(), null, sipSender.getNewCallIdHeader(sipLayer.getLocalIp(device.getLocalIp()), device.getTransport()));
+        if (request == null) {
+            SipException e = new SipException("无法获取到对应的sip配置信息,domain:" + device.getDomain());
+            throw e;
+        }
+        sipSender.transmitRequest(sipLayer.getLocalIp(device.getLocalIp()), request);
     }
 
 
@@ -329,10 +336,52 @@ public class SIPCommander implements ISIPCommander {
                 subscribe.removeSubscribe(hookSubscribe);
             }
         });
+
+        // f字段:f= v/编码格式/分辨率/帧率/码率类型/码率大小a/编码格式/码率大小/采样率
+//			content.append("f=v/2/5/25/1/4000a/1/8/1" + "\r\n"); // 未发现支持此特性的设备
+        StringBuffer content = buildPlayCmd(mediaServerItem, device, channelId, ssrcInfo.getPort(), ssrcInfo.getSsrc(), isUsePs, userSetting.isSeniorSdp());
+
+        Request request = headerProvider.createInviteRequest(device, channelId, content.toString(), SipUtils.getNewViaTag(), SipUtils.getNewFromTag(), null, ssrcInfo.getSsrc(), sipSender.getNewCallIdHeader(sipLayer.getLocalIp(device.getLocalIp()), device.getTransport()));
+        if (request == null) {
+            SipException e = new SipException("无法获取到对应的sip配置信息,domain:" + device.getDomain());
+            SipSubscribe.EventResult responseEvent = new SipSubscribe.EventResult(e);
+            errorEvent.response(responseEvent);
+            return;
+        }
+        sipSender.transmitRequest(sipLayer.getLocalIp(device.getLocalIp()), request,
+                (e -> {
+                    streamSession.remove(device.getDeviceId(), channelId, ssrcInfo.getStream());
+                    mediaServerService.releaseSsrc(mediaServerItem.getId(), ssrcInfo.getSsrc());
+                    errorEvent.response(e);
+                }), e -> {
+                    // 这里为例避免一个通道的点播只有一个callID这个参数使用一个固定值
+                    ResponseEvent responseEvent = (ResponseEvent) e.event;
+                    SIPResponse response = (SIPResponse) responseEvent.getResponse();
+                    streamSession.put(device.getDeviceId(), channelId, "play", stream, ssrcInfo.getSsrc(), mediaServerItem.getId(), response, VideoStreamSessionManager.SessionType.play);
+                    okEvent.response(e);
+                });
+    }
+
+    /**
+     * 构建播放 sip invite 信息
+     *
+     * @param mediaServerItem
+     * @param device
+     * @param channelId
+     * @param port
+     * @param ssrc
+     * @param isUsePs
+     * @param isSeniorSdp
+     * @return
+     */
+    private StringBuffer buildPlayCmd(
+            MediaServerItem mediaServerItem, Device device,
+            String channelId, int port, String ssrc,
+            int isUsePs, Boolean isSeniorSdp) {
         String sdpIp;
         if (!ObjectUtils.isEmpty(device.getSdpIp())) {
             sdpIp = device.getSdpIp();
-        }else {
+        } else {
             sdpIp = mediaServerItem.getSdpIp();
         }
         StringBuffer content = new StringBuffer(200);
@@ -341,14 +390,13 @@ public class SIPCommander implements ISIPCommander {
         content.append("s=Play\r\n");
         content.append("c=IN IP4 " + sdpIp + "\r\n");
         content.append("t=0 0\r\n");
-
         if (userSetting.isSeniorSdp()) {
             if ("TCP-PASSIVE".equalsIgnoreCase(device.getStreamMode())) {
-                content.append("m=video " + ssrcInfo.getPort() + " TCP/RTP/AVP 96 126 125 99 34 98 97\r\n");
+                content.append("m=video " + port + " TCP/RTP/AVP 96 126 125 99 34 98 97\r\n");
             } else if ("TCP-ACTIVE".equalsIgnoreCase(device.getStreamMode())) {
-                content.append("m=video " + ssrcInfo.getPort() + " TCP/RTP/AVP 96 126 125 99 34 98 97\r\n");
+                content.append("m=video " + port + " TCP/RTP/AVP 96 126 125 99 34 98 97\r\n");
             } else if ("UDP".equalsIgnoreCase(device.getStreamMode())) {
-                content.append("m=video " + ssrcInfo.getPort() + " RTP/AVP 96 126 125 99 34 98 97\r\n");
+                content.append("m=video " + port + " RTP/AVP 96 126 125 99 34 98 97\r\n");
             }
             content.append("a=recvonly\r\n");
             content.append("a=rtpmap:96 PS/90000\r\n");
@@ -368,14 +416,14 @@ public class SIPCommander implements ISIPCommander {
             }
         } else {
             if ("TCP-PASSIVE".equalsIgnoreCase(device.getStreamMode())) {
-                content.append("m=video " + ssrcInfo.getPort() + " TCP/RTP/AVP 96 97 98 99\r\n");
+                content.append("m=video " + port + " TCP/RTP/AVP 96 97 98 99\r\n");
             } else if ("TCP-ACTIVE".equalsIgnoreCase(device.getStreamMode())) {
-                content.append("m=video " + ssrcInfo.getPort() + " TCP/RTP/AVP 96 97 98 99\r\n");
+                content.append("m=video " + port + " TCP/RTP/AVP 96 97 98 99\r\n");
             } else if ("UDP".equalsIgnoreCase(device.getStreamMode())) {
-                content.append("m=video " + ssrcInfo.getPort() + " RTP/AVP 96 97 98 99\r\n");
+                content.append("m=video " + port + " RTP/AVP 96 97 98 99\r\n");
             }
             content.append("a=recvonly\r\n");
-            if(isUsePs == 1){
+            if (isUsePs == 1) {
                 // ps 改为手动启用
                 content.append("a=rtpmap:96 PS/90000\r\n");
             }
@@ -391,25 +439,40 @@ public class SIPCommander implements ISIPCommander {
                 content.append("a=connection:new\r\n");
             }
         }
-        logger.info("------------ invik : {} ", content.toString());
-        content.append("y=" + ssrcInfo.getSsrc() + "\r\n");//ssrc
-        // f字段:f= v/编码格式/分辨率/帧率/码率类型/码率大小a/编码格式/码率大小/采样率
-//			content.append("f=v/2/5/25/1/4000a/1/8/1" + "\r\n"); // 未发现支持此特性的设备
-
-
+        content.append("y=" + ssrc + "\r\n");//ssrc
+        return content;
+    }
 
-        Request request = headerProvider.createInviteRequest(device, channelId, content.toString(), SipUtils.getNewViaTag(), SipUtils.getNewFromTag(), null, ssrcInfo.getSsrc(),sipSender.getNewCallIdHeader(sipLayer.getLocalIp(device.getLocalIp()),device.getTransport()));
-        sipSender.transmitRequest(sipLayer.getLocalIp(device.getLocalIp()), request, (e -> {
-            streamSession.remove(device.getDeviceId(), channelId, ssrcInfo.getStream());
-            mediaServerService.releaseSsrc(mediaServerItem.getId(), ssrcInfo.getSsrc());
-            errorEvent.response(e);
-        }), e -> {
-            // 这里为例避免一个通道的点播只有一个callID这个参数使用一个固定值
-            ResponseEvent responseEvent = (ResponseEvent) e.event;
-            SIPResponse response = (SIPResponse) responseEvent.getResponse();
-            streamSession.put(device.getDeviceId(), channelId, "play", stream, ssrcInfo.getSsrc(), mediaServerItem.getId(), response, VideoStreamSessionManager.SessionType.play);
-            okEvent.response(e);
-        });
+    public void sendPlayCmd(
+            MediaServerItem mediaServerItem, SSRCInfo ssrcInfo,
+            Device device, String channelId, int isUsePs,
+            ErrorHook errorHook
+    ) throws InvalidArgumentException, SipException, ParseException {
+        StringBuffer content = buildPlayCmd(mediaServerItem, device, channelId, ssrcInfo.getPort(), ssrcInfo.getSsrc(), isUsePs, userSetting.isSeniorSdp());
+        Request request = headerProvider.createInviteRequest(device, channelId, content.toString(), SipUtils.getNewViaTag(), SipUtils.getNewFromTag(), null, ssrcInfo.getSsrc(), sipSender.getNewCallIdHeader(sipLayer.getLocalIp(device.getLocalIp()), device.getTransport()));
+        if (request == null) {
+            errorHook.run(WVPResult.fail(
+                    ErrorCode.ERROR_DATA,
+                    "无法获取到对应的sip配置信息,domain:" + device.getDomain()
+            ));
+            return;
+        }
+        // 发送请求
+        sipSender.transmitRequest(sipLayer.getLocalIp(device.getLocalIp()), request,
+                (e -> {
+                    errorHook.run(WVPResult.fail(
+                            ErrorCode.ERR_Invite_fail,
+                            "命令发送失败: "
+                    ));
+                    streamSession.remove(device.getDeviceId(), channelId, ssrcInfo.getStream());
+                    mediaServerService.releaseSsrc(mediaServerItem.getId(), ssrcInfo.getSsrc());
+                }), e -> {
+                    // 这里为例避免一个通道的点播只有一个callID这个参数使用一个固定值
+                    ResponseEvent responseEvent = (ResponseEvent) e.event;
+                    SIPResponse response = (SIPResponse) responseEvent.getResponse();
+                    streamSession.put(device.getDeviceId(), channelId, "play", ssrcInfo.getStream(), ssrcInfo.getSsrc(), mediaServerItem.getId(), response, VideoStreamSessionManager.SessionType.play);
+                    errorHook.run(WVPResult.success());
+                });
     }
 
     /**
@@ -741,16 +804,31 @@ public class SIPCommander implements ISIPCommander {
         mediaServerService.releaseSsrc(ssrcTransaction.getMediaServerId(), ssrcTransaction.getSsrc());
         mediaServerService.closeRTPServer(ssrcTransaction.getMediaServerId(), ssrcTransaction.getStream());
         streamSession.remove(ssrcTransaction.getDeviceId(), ssrcTransaction.getChannelId(), ssrcTransaction.getStream());
-//        if(ssrcTransaction.getApp){
-//            //        JSONObject result = mediaServerService.closeStreams(mediaServerItem, param.getApp(), param.getStream());
-//        }
         Request byteRequest = headerProvider.createByteRequest(device, channelId, ssrcTransaction.getSipTransactionInfo());
         sipSender.transmitRequest(sipLayer.getLocalIp(device.getLocalIp()), byteRequest, null, okEvent);
     }
 
-    public void streamByeBroadcastCmd(Device device,String channelId){
-
+    public void stopPlayCmd(Device device, String channelId, ErrorHook errorHook) throws InvalidArgumentException, SipException, ParseException {
+        SsrcTransaction ssrcTransaction = streamSession.getSsrcTransaction(device.getDeviceId(), channelId, null, null);
+        if (ssrcTransaction == null) {
+            // 无法找到
+            errorHook.run(WVPResult.fail(ErrorCode.ERROR100, "无法找到该设备的流信息"));
+            return;
+        }
         // 关闭推流通道
+        Request byteRequest = headerProvider.createByteRequest(device, channelId, ssrcTransaction.getSipTransactionInfo());
+        sipSender.transmitRequest(sipLayer.getLocalIp(device.getLocalIp()), byteRequest,
+                (e -> {
+                    errorHook.run(WVPResult.fail(
+                            ErrorCode.ERR_Invite_fail,
+                            "命令发送失败: "
+                    ));
+                }), e -> {
+                    // 这里为例避免一个通道的点播只有一个callID这个参数使用一个固定值
+                    ResponseEvent responseEvent = (ResponseEvent) e.event;
+                    SIPResponse response = (SIPResponse) responseEvent.getResponse();
+                    errorHook.run(WVPResult.success(response));
+                });
     }
 
 

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

@@ -99,7 +99,7 @@ public class RegisterRequestProcessor extends SIPRequestProcessorParent implemen
             // 获取对应的sip配置
             logger.info("[sip 注册信息] host" + uri.getHost());
             SipUserConfig sipUserConfig = sipConfigService.getSipConfigByDomain(uri.getHost());
-            if (sipUserConfig == null || !sipUserConfig.isEnable()) {
+            if (sipUserConfig == null || !sipUserConfig.isEnableFlag()) {
                 logger.error("[sip注册] 不受支持的sip域, 回复404: {}", uri.getHost());
                 response = getMessageFactory().createResponse(Response.NOT_FOUND, request);
                 response.setReasonPhrase("wrong domain");
@@ -192,7 +192,7 @@ public class RegisterRequestProcessor extends SIPRequestProcessorParent implemen
                 deviceService.online(device);
 
 
-                if (sipUserConfig.isEnableBind()) {
+                if (sipUserConfig.isEnableBindFlag()) {
                     logger.info("[sip注册成功] 设备:{} 连接的域启用绑定, 尝试获取设备绑定码中");
                     deviceService.loadBindCode(device);
                 }

+ 7 - 5
src/main/java/com/genersoft/iot/vmp/service/IAccountService.java

@@ -34,20 +34,22 @@ public interface IAccountService {
 
     /**
      * 绑定设备
+     *
      * @param id
      * @param devId
      * @param devCode
      * @return
      */
-    int bindDevice(int id, int devId, String devCode);
+    int bindDevice(String id, String devId, String devCode);
 
     /**
      * 解绑设备
+     *
      * @param userId
      * @param devId
      * @return
      */
-    int unBindDevice(int userId, int devId);
+    int unBindDevice(String userId, String devId);
 
     /**
      * 检查绑定码是否能够被绑定, 能够绑定则 允许 true
@@ -69,14 +71,14 @@ public interface IAccountService {
      * @param limit     账户
      * @return
      */
-    PageInfo<Device> getAccountDevices(int accountId, int page, int limit);
+    PageInfo<Device> getAccountDevices(String accountId, int page, int limit);
 
     /**
      * 获取指定用户的指定设备
      *
      * @param accountId 用户id
-     * @param deviceId  设备id
+     * @param deviceId  设备id,非国标id
      * @return Device
      */
-    Device getAccountDevice(int accountId, int deviceId);
+    Device getAccountDevice(String accountId, String deviceId);
 }

+ 30 - 0
src/main/java/com/genersoft/iot/vmp/service/IAdminService.java

@@ -1,5 +1,6 @@
 package com.genersoft.iot.vmp.service;
 
+import com.genersoft.iot.vmp.gb28181.bean.Device;
 import com.genersoft.iot.vmp.storager.dao.dto.AdminAccount;
 import com.github.pagehelper.PageInfo;
 
@@ -13,6 +14,8 @@ public interface IAdminService {
 
     AdminAccount getUserByUsername(String username);
 
+    AdminAccount getUserById(String adminId);
+
     int addUser(AdminAccount adminAccount);
 
     int deleteUser(int id);
@@ -28,4 +31,31 @@ public interface IAdminService {
     int changePushKey(int id, String pushKey);
 
     String getPushKey(int id);
+
+    /**
+     * 获取设备, 根据管理员ID和设备ID
+     *
+     * @param adminId
+     * @param devId   设备表id
+     * @return
+     */
+    Device getAdminDevice(String adminId, String devId);
+
+    /**
+     * 获取设备, 根据管理员ID和设备国标id
+     *
+     * @param adminId
+     * @param deviceSipId
+     * @return
+     */
+    Device getAdminDeviceBySipId(String adminId, String deviceSipId);
+
+    /**
+     * 获取设备列表, 根据管理员ID
+     *
+     * @param adminId
+     * @return
+     */
+    List<Device> getAdminDevices(String adminId);
+
 }

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

@@ -56,4 +56,6 @@ public interface IDeviceChannelService {
      * 查询通道所属的设备
      */
     List<Device> getDeviceByChannelId(String channelId);
+
+    List<DeviceChannel> getChannelListByDeviceId(String deviceId);
 }

+ 5 - 1
src/main/java/com/genersoft/iot/vmp/service/IPlayService.java

@@ -9,6 +9,7 @@ import com.genersoft.iot.vmp.gb28181.bean.Device;
 import com.genersoft.iot.vmp.gb28181.bean.InviteStreamCallback;
 import com.genersoft.iot.vmp.gb28181.bean.InviteStreamInfo;
 import com.genersoft.iot.vmp.gb28181.event.SipSubscribe;
+import com.genersoft.iot.vmp.gb28181.transmit.callback.RequestMessage;
 import com.genersoft.iot.vmp.media.zlm.ZlmHttpHookSubscribe;
 import com.genersoft.iot.vmp.media.zlm.dto.MediaServerItem;
 import com.genersoft.iot.vmp.service.bean.*;
@@ -22,7 +23,6 @@ import java.text.ParseException;
  */
 public interface IPlayService {
 
-    void onPublishHandlerForPlay(MediaServerItem mediaServerItem, JSONObject resonse, String deviceId, String channelId);
 
     void play(MediaServerItem mediaServerItem,
               SSRCInfo ssrcInfo,
@@ -41,11 +41,15 @@ public interface IPlayService {
             Runnable timeoutCallback);
 
 
+    void startPlay(RequestMessage requestMsg, MediaServerItem mediaServerItem, Device device, String channelId, int isUsePs);
+
+    void stopPlay(Device device, String channelId);
 
     void openBroadcast(MediaServerItem mediaServerItem,
                        Device device,
                        int waitTime,
                        BroadcastCallback callback);
+
     MediaServerItem getNewMediaServerItem(Device device);
 
     /**

+ 39 - 0
src/main/java/com/genersoft/iot/vmp/service/ISipConfigService.java

@@ -1,6 +1,7 @@
 package com.genersoft.iot.vmp.service;
 
 import com.genersoft.iot.vmp.gb28181.bean.SipUserConfig;
+import com.github.pagehelper.PageInfo;
 
 public interface ISipConfigService {
     /**
@@ -10,4 +11,42 @@ public interface ISipConfigService {
      * @return
      */
     SipUserConfig getSipConfigByDomain(String domain);
+
+    /**
+     * 获取指定管理员账户下的所有国标配置信息
+     *
+     * @param adminId
+     * @param page
+     * @param limit
+     * @return
+     */
+    PageInfo<SipUserConfig> getSipConfigsById(String adminId, int page, int limit);
+
+
+    /**
+     * 获取指定配置信息
+     *
+     * @param adminId
+     * @param sipId
+     * @return
+     */
+    SipUserConfig getSipConfigById(String adminId, String sipId);
+
+    /**
+     * 编辑国标配置
+     *
+     * @param sipId
+     * @param sipConfig
+     * @return
+     */
+    boolean editSipConfig(String sipId, SipUserConfig sipConfig);
+
+    /**
+     * 新增国标配置信息
+     */
+    boolean addSipConfig(String adminId, SipUserConfig sipConfig);
+
+
+    boolean deleteSipConfig(String adminId, String sipId);
+
 }

+ 2 - 0
src/main/java/com/genersoft/iot/vmp/service/bean/BroadcastCallback.java

@@ -11,3 +11,5 @@ public interface BroadcastCallback {
      */
     void run(int code, JSONObject json, SIPRequest request);
 }
+
+

+ 4 - 6
src/main/java/com/genersoft/iot/vmp/service/impl/AccountServiceImpl.java

@@ -60,13 +60,11 @@ public class AccountServiceImpl implements IAccountService {
         return accountMapper.login(account, password);
     }
 
-    public int bindDevice(int id, int devId, String devCode)
-    {
+    public int bindDevice(String id, String devId, String devCode) {
         return accountMapper.bindDevice(id, devId, devCode);
     }
 
-    public int unBindDevice(int userId, int devId)
-    {
+    public int unBindDevice(String userId, String devId) {
         return accountMapper.unBindDevice(userId, devId);
     }
 
@@ -82,13 +80,13 @@ public class AccountServiceImpl implements IAccountService {
     }
 
 
-    public PageInfo<Device> getAccountDevices(int accountId, int page, int count) {
+    public PageInfo<Device> getAccountDevices(String accountId, int page, int count) {
         PageHelper.startPage(page, count);
         List<Device> devices = accountMapper.getAccountDevices(accountId);
         return new PageInfo<>(devices);
     }
 
-    public Device getAccountDevice(int accountId, int deviceId) {
+    public Device getAccountDevice(String accountId, String deviceId) {
         return accountMapper.getAccountDevice(accountId, deviceId);
     }
 

+ 36 - 0
src/main/java/com/genersoft/iot/vmp/service/impl/AdminServiceImpl.java

@@ -1,10 +1,14 @@
 package com.genersoft.iot.vmp.service.impl;
 
+import com.genersoft.iot.vmp.gb28181.bean.Device;
 import com.genersoft.iot.vmp.service.IAdminService;
 import com.genersoft.iot.vmp.storager.dao.AdminMapper;
+import com.genersoft.iot.vmp.storager.dao.DeviceMapper;
 import com.genersoft.iot.vmp.storager.dao.dto.AdminAccount;
 import com.github.pagehelper.PageHelper;
 import com.github.pagehelper.PageInfo;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 import org.springframework.util.ObjectUtils;
@@ -14,9 +18,14 @@ import java.util.List;
 @Service
 public class AdminServiceImpl implements IAdminService {
 
+    private final static Logger logger = LoggerFactory.getLogger(AdminServiceImpl.class);
+
     @Autowired
     private AdminMapper adminMapper;
 
+    @Autowired
+    private DeviceMapper deviceMapper;
+
     @Override
     public AdminAccount getUser(String username, String password) {
         return adminMapper.select(username, password);
@@ -35,6 +44,11 @@ public class AdminServiceImpl implements IAdminService {
         return adminMapper.getUserByUsername(username);
     }
 
+    @Override
+    public AdminAccount getUserById(String adminId) {
+        return adminMapper.getUserById(adminId);
+    }
+
     @Override
     public int addUser(AdminAccount adminAccount) {
         AdminAccount adminAccountByUsername = adminMapper.getUserByUsername(adminAccount.getUsername());
@@ -89,4 +103,26 @@ public class AdminServiceImpl implements IAdminService {
         }
         return null;
     }
+
+    // 获取当前管理员的设备
+    public Device getAdminDevice(String adminId, String devId) {
+        return adminMapper.getDeviceByAdminIdAndDevId(adminId, devId);
+    }
+
+    // 获取某个管理员账户下的所有设备
+    public List<Device> getAdminDevices(String adminId) {
+        return adminMapper.getDevicesByAdminId(adminId);
+    }
+
+    //    获取某个管理员设备,根据设备国标id
+    public Device getAdminDeviceBySipId(String adminId, String deviceSipId) {
+//        logger.info("getAdminDeviceBySipId get admin:{} device:{}", adminId, deviceSipId);
+        Device device = adminMapper.getAdminDeviceBySipId(adminId, deviceSipId);
+        if (device == null) {
+            return null;
+        }
+        return device;
+    }
+
+
 }

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

@@ -210,4 +210,9 @@ public class DeviceChannelServiceImpl implements IDeviceChannelService {
         return channelMapper.getDeviceByChannelId(channelId);
     }
 
+    @Override
+    public List<DeviceChannel> getChannelListByDeviceId(String deviceId) {
+        return channelMapper.getChannelsByDeviceId(deviceId);
+    }
+
 }

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

@@ -121,7 +121,7 @@ public class MediaServerServiceImpl implements IMediaServerService {
 
     @Override
     public SSRCInfo openRTPServer(MediaServerItem mediaServerItem, String streamId, boolean ssrcCheck, boolean isPlayback) {
-        return openRTPServer(mediaServerItem, streamId, null, ssrcCheck,isPlayback);
+        return openRTPServer(mediaServerItem, streamId, null, ssrcCheck, isPlayback);
     }
 
 
@@ -129,7 +129,7 @@ public class MediaServerServiceImpl implements IMediaServerService {
     @Override
     public SSRCInfo openRTPServer(MediaServerItem mediaServerItem, String streamId, String presetSsrc, boolean ssrcCheck, boolean isPlayback, Integer port) {
         if (mediaServerItem == null || mediaServerItem.getId() == null) {
-            logger.info("[openRTPServer] 失败, mediaServerItem == null || mediaServerItem.getId() == null");
+            logger.warn("[openRTPServer] 失败, mediaServerItem == null || mediaServerItem.getId() == null");
             return null;
         }
         // 获取mediaServer可用的ssrc
@@ -137,7 +137,7 @@ public class MediaServerServiceImpl implements IMediaServerService {
 
         SsrcConfig ssrcConfig = mediaServerItem.getSsrcConfig();
         if (ssrcConfig == null) {
-            logger.info("media server [ {} ] ssrcConfig is null", mediaServerItem.getId());
+            logger.warn("media server [ {} ] ssrcConfig is null", mediaServerItem.getId());
             return null;
         }else {
             String ssrc;

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

@@ -14,6 +14,7 @@ import com.genersoft.iot.vmp.gb28181.GBHookSubscribeFactory;
 import com.genersoft.iot.vmp.gb28181.HookSubscribeForKey;
 import com.genersoft.iot.vmp.gb28181.bean.*;
 import com.genersoft.iot.vmp.gb28181.event.SipSubscribe;
+import com.genersoft.iot.vmp.gb28181.session.SsrcConfig;
 import com.genersoft.iot.vmp.gb28181.session.VideoStreamSessionManager;
 import com.genersoft.iot.vmp.gb28181.transmit.callback.DeferredResultHolder;
 import com.genersoft.iot.vmp.gb28181.transmit.callback.RequestMessage;
@@ -35,6 +36,8 @@ import com.genersoft.iot.vmp.storager.IVideoManagerStorage;
 import com.genersoft.iot.vmp.utils.DateUtil;
 import com.genersoft.iot.vmp.utils.redis.RedisUtil;
 import com.genersoft.iot.vmp.vmanager.bean.ErrorCode;
+import com.genersoft.iot.vmp.vmanager.bean.ErrorHook;
+import com.genersoft.iot.vmp.vmanager.bean.StreamContent;
 import com.genersoft.iot.vmp.vmanager.bean.WVPResult;
 import gov.nist.javax.sip.message.SIPRequest;
 import gov.nist.javax.sip.message.SIPResponse;
@@ -53,6 +56,7 @@ import javax.sip.SipException;
 import java.math.BigDecimal;
 import java.math.RoundingMode;
 import java.text.ParseException;
+import java.util.EventObject;
 import java.util.List;
 import java.util.UUID;
 
@@ -117,14 +121,14 @@ public class PlayServiceImpl implements IPlayService {
 
     @Override
     public void play(MediaServerItem mediaServerItem, String deviceId,
-                                String channelId,int isUsePs,
-                                 ZlmHttpHookSubscribe.Event hookEvent, SipSubscribe.Event errorEvent,
-                                 Runnable timeoutCallback) {
+                     String channelId, int isUsePs,
+                     ZlmHttpHookSubscribe.Event hookEvent, SipSubscribe.Event errorEvent,
+                     Runnable timeoutCallback) {
 
         String key = DeferredResultHolder.CALLBACK_CMD_PLAY + deviceId + channelId;
         WVPResult wvpResult = new WVPResult();
         RequestMessage msg = new RequestMessage();
-        Device device = redisCatchStorage.getDevice(deviceId);
+        Device device = storager.queryVideoDevice(deviceId);
         StreamInfo streamInfo = redisCatchStorage.queryPlayByDevice(deviceId, channelId);
 
         msg.setKey(key);
@@ -209,26 +213,25 @@ public class PlayServiceImpl implements IPlayService {
             }
             SSRCInfo ssrcInfo = mediaServerService.openRTPServer(mediaServerItem, streamId, device.isSsrcCheck(), false);
             logger.info(JSONObject.toJSONString(ssrcInfo));
-			if (ssrcInfo == null) {
+            if (ssrcInfo == null) {
                 wvpResult.setCode(ErrorCode.ERROR100.getCode());
                 wvpResult.setMsg("开启收流失败");
                 msg.setData(wvpResult);
                 resultHolder.invokeAllResult(msg);
                 return;
             }
-            play(mediaServerItem, ssrcInfo, device, channelId,
-                    isUsePs,
+            play(mediaServerItem, ssrcInfo, device, channelId, isUsePs,
                     (mediaServerItemInUse, response) -> {
-                if (hookEvent != null) {
-                    hookEvent.response(mediaServerItem, response);
-                }
-            }, event -> {
-                // sip error错误
-               logger.warn("sip 错误,点播失败");
-                wvpResult.setCode(ErrorCode.ERROR100.getCode());
-                wvpResult.setMsg(String.format("点播失败, 错误码: %s, %s", event.statusCode, event.msg));
-                msg.setData(wvpResult);
-                resultHolder.invokeAllResult(msg);
+                        if (hookEvent != null) {
+                            hookEvent.response(mediaServerItem, response);
+                        }
+                    }, event -> {
+                        // sip error错误
+                        logger.warn("sip 错误,点播失败");
+                        wvpResult.setCode(ErrorCode.ERROR100.getCode());
+                        wvpResult.setMsg(String.format("点播失败, 错误码: %s, %s", event.statusCode, event.msg));
+                        msg.setData(wvpResult);
+                        resultHolder.invokeAllResult(msg);
                 if (errorEvent != null) {
                     errorEvent.response(event);
                 }
@@ -280,6 +283,8 @@ public class PlayServiceImpl implements IPlayService {
                 }
             }
         }, userSetting.getPlayTimeout());
+        RequestMessage msg = new RequestMessage();
+        msg.setKey(DeferredResultHolder.CALLBACK_CMD_PLAY + device.getDeviceId() + channelId);
         //端口获取失败的ssrcInfo 没有必要发送点播指令
         if (ssrcInfo.getPort() <= 0) {
             logger.info("[点播端口分配异常],deviceId={},channelId={},ssrcInfo={}", device.getDeviceId(), channelId, ssrcInfo);
@@ -287,88 +292,87 @@ public class PlayServiceImpl implements IPlayService {
             // 释放ssrc
             mediaServerService.releaseSsrc(mediaServerItem.getId(), ssrcInfo.getSsrc());
             streamSession.remove(device.getDeviceId(), channelId, ssrcInfo.getStream());
-
-            RequestMessage msg = new RequestMessage();
-            msg.setKey(DeferredResultHolder.CALLBACK_CMD_PLAY + device.getDeviceId() + channelId);
             msg.setData(WVPResult.fail(ErrorCode.ERROR100.getCode(), "点播端口分配异常"));
             resultHolder.invokeAllResult(msg);
             return;
         }
         try {
-            cmder.playStreamCmd(mediaServerItem, ssrcInfo, device, channelId,isUsePs, (MediaServerItem mediaServerItemInuse, JSONObject response) -> {
-                logger.info("收到订阅消息: " + response.toJSONString());
-                logger.info("停止超时任务: " + timeOutTaskKey);
-                dynamicTask.stop(timeOutTaskKey);
-                // hook响应
-                onPublishHandlerForPlay(mediaServerItemInuse, response, device.getDeviceId(), channelId);
-                hookEvent.response(mediaServerItemInuse, response);
-                logger.info("[点播成功] deviceId: {}, channelId: {}", device.getDeviceId(), channelId);
-                String streamUrl = String.format("http://127.0.0.1:%s/%s/%s.live.flv", mediaServerItemInuse.getHttpPort(), "rtp",  ssrcInfo.getStream());
-                String path = "snap";
-                String fileName = device.getDeviceId() + "_" + channelId + ".jpg";
-                // 请求截图
-                logger.info("[请求截图]: " + fileName);
-                zlmresTfulUtils.getSnap(mediaServerItemInuse, streamUrl, 15, 1, path, fileName);
-
-            }, (event) -> {
-                ResponseEvent responseEvent = (ResponseEvent) event.event;
-                String contentString = new String(responseEvent.getResponse().getRawContent());
-                // 获取ssrc
-                int ssrcIndex = contentString.indexOf("y=");
-                // 检查是否有y字段
-                if (ssrcIndex >= 0) {
-                    //ssrc规定长度为10字节,不取余下长度以避免后续还有“f=”字段 TODO 后续对不规范的非10位ssrc兼容
-                    String ssrcInResponse = contentString.substring(ssrcIndex + 2, ssrcIndex + 12);
-                    // 查询到ssrc不一致且开启了ssrc校验则需要针对处理
-                    if (ssrcInfo.getSsrc().equals(ssrcInResponse)) {
-                        return;
-                    }
-                    logger.info("[点播消息] 收到invite 200, 发现下级自定义了ssrc: {}", ssrcInResponse);
-                    if (!mediaServerItem.isRtpEnable() || device.isSsrcCheck()) {
-                        logger.info("[点播消息] SSRC修正 {}->{}", ssrcInfo.getSsrc(), ssrcInResponse);
-
-                        if (!mediaServerItem.getSsrcConfig().checkSsrc(ssrcInResponse)) {
-                            // ssrc 不可用
-                            // 释放ssrc
-                            mediaServerService.releaseSsrc(mediaServerItem.getId(), ssrcInfo.getSsrc());
-                            streamSession.remove(device.getDeviceId(), channelId, ssrcInfo.getStream());
-                            event.msg = "下级自定义了ssrc,但是此ssrc不可用";
-                            event.statusCode = 400;
-                            errorEvent.response(event);
-                            return;
-                        }
+            cmder.playStreamCmd(mediaServerItem, ssrcInfo, device, channelId, isUsePs,
+                    (MediaServerItem mediaServerItemInuse, JSONObject response) -> {
+                        logger.info("收到订阅消息: " + response.toJSONString());
+                        logger.info("停止超时任务: " + timeOutTaskKey);
+                        dynamicTask.stop(timeOutTaskKey);
+                        // hook响应
+                        onPublishHandlerForPlay(msg, mediaServerItemInuse, response, device.getDeviceId(), channelId, ssrcInfo.getSsrc());
+                        hookEvent.response(mediaServerItemInuse, response);
+                        logger.info("[点播成功] deviceId: {}, channelId: {}", device.getDeviceId(), channelId);
+                        String streamUrl = String.format("http://127.0.0.1:%s/%s/%s.live.flv", mediaServerItemInuse.getHttpPort(), "rtp", ssrcInfo.getStream());
+                        String path = "snap";
+                        String fileName = device.getDeviceId() + "_" + channelId + ".jpg";
+                        // 请求截图
+                        logger.info("[请求截图]: " + fileName);
+                        zlmresTfulUtils.getSnap(mediaServerItemInuse, streamUrl, 15, 1, path, fileName);
+
+                    },
+                    (okEvent) -> {
+                        ResponseEvent responseEvent = (ResponseEvent) okEvent.event;
+                        String contentString = new String(responseEvent.getResponse().getRawContent());
+                        // 获取ssrc
+                        int ssrcIndex = contentString.indexOf("y=");
+                        // 检查是否有y字段
+                        if (ssrcIndex >= 0) {
+                            //ssrc规定长度为10字节,不取余下长度以避免后续还有“f=”字段 TODO 后续对不规范的非10位ssrc兼容
+                            String ssrcInResponse = contentString.substring(ssrcIndex + 2, ssrcIndex + 12);
+                            // 查询到ssrc不一致且开启了ssrc校验则需要针对处理
+                            if (ssrcInfo.getSsrc().equals(ssrcInResponse)) {
+                                return;
+                            }
+                            logger.info("[点播消息] 收到invite 200, 发现下级自定义了ssrc: {}", ssrcInResponse);
+                            if (!mediaServerItem.isRtpEnable() || device.isSsrcCheck()) {
+                                logger.info("[点播消息] SSRC修正 {}->{}", ssrcInfo.getSsrc(), ssrcInResponse);
+
+                                if (!mediaServerItem.getSsrcConfig().checkSsrc(ssrcInResponse)) {
+                                    // ssrc 不可用
+                                    // 释放ssrc
+                                    mediaServerService.releaseSsrc(mediaServerItem.getId(), ssrcInfo.getSsrc());
+                                    streamSession.remove(device.getDeviceId(), channelId, ssrcInfo.getStream());
+                                    okEvent.msg = "下级自定义了ssrc,但是此ssrc不可用";
+                                    okEvent.statusCode = 400;
+                                    errorEvent.response(okEvent);
+                                    return;
+                                }
 
-                        // 单端口模式streamId也有变化,需要重新设置监听
-                        if (!mediaServerItem.isRtpEnable()) {
-                            // 添加订阅
-                            HookSubscribeForStreamChange hookSubscribe = HookSubscribeFactory.on_stream_changed("rtp", ssrcInfo.getStream(), true, "rtsp", mediaServerItem.getId());
-                            subscribe.removeSubscribe(hookSubscribe);
-                            hookSubscribe.getContent().put("stream", String.format("%08x", Integer.parseInt(ssrcInResponse)).toUpperCase());
-                            subscribe.addSubscribe(hookSubscribe, (MediaServerItem mediaServerItemInUse, JSONObject response) -> {
-                                logger.info("[ZLM HOOK] ssrc修正后收到订阅消息: " + response.toJSONString());
-                                dynamicTask.stop(timeOutTaskKey);
-                                // hook响应
-                                onPublishHandlerForPlay(mediaServerItemInUse, response, device.getDeviceId(), channelId);
-                                hookEvent.response(mediaServerItemInUse, response);
-                            });
-                        }
-                        // 关闭rtp server
-                        mediaServerService.closeRTPServer(mediaServerItem, ssrcInfo.getStream());
-                        // 重新开启ssrc server
-                        mediaServerService.openRTPServer(mediaServerItem, ssrcInfo.getStream(), ssrcInResponse, device.isSsrcCheck(), false, ssrcInfo.getPort());
+                                // 单端口模式streamId也有变化,需要重新设置监听
+                                if (!mediaServerItem.isRtpEnable()) {
+                                    // 添加订阅
+                                    HookSubscribeForStreamChange hookSubscribe = HookSubscribeFactory.on_stream_changed("rtp", ssrcInfo.getStream(), true, "rtsp", mediaServerItem.getId());
+                                    subscribe.removeSubscribe(hookSubscribe);
+                                    hookSubscribe.getContent().put("stream", String.format("%08x", Integer.parseInt(ssrcInResponse)).toUpperCase());
+                                    subscribe.addSubscribe(hookSubscribe, (MediaServerItem mediaServerItemInUse, JSONObject response) -> {
+                                        logger.info("[ZLM HOOK] ssrc修正后收到订阅消息: " + response.toJSONString());
+                                        dynamicTask.stop(timeOutTaskKey);
+                                        // hook响应
+                                        onPublishHandlerForPlay(msg, mediaServerItemInUse, response, device.getDeviceId(), channelId, ssrcInfo.getSsrc());
+                                        hookEvent.response(mediaServerItemInUse, response);
+                                    });
+                                }
+                                // 关闭rtp server
+                                mediaServerService.closeRTPServer(mediaServerItem, ssrcInfo.getStream());
+                                // 重新开启ssrc server
+                                mediaServerService.openRTPServer(mediaServerItem, ssrcInfo.getStream(), ssrcInResponse, device.isSsrcCheck(), false, ssrcInfo.getPort());
 
+                            }
+                        }
                     }
-                }
-            }
-            , (event) -> {
-                dynamicTask.stop(timeOutTaskKey);
-                mediaServerService.closeRTPServer(mediaServerItem, ssrcInfo.getStream());
-                // 释放ssrc
-                mediaServerService.releaseSsrc(mediaServerItem.getId(), ssrcInfo.getSsrc());
+                    , (errEvent) -> {
+                        dynamicTask.stop(timeOutTaskKey);
+                        mediaServerService.closeRTPServer(mediaServerItem, ssrcInfo.getStream());
+                        // 释放ssrc
+                        mediaServerService.releaseSsrc(mediaServerItem.getId(), ssrcInfo.getSsrc());
 
-                streamSession.remove(device.getDeviceId(), channelId, ssrcInfo.getStream());
-                errorEvent.response(event);
-            });
+                        streamSession.remove(device.getDeviceId(), channelId, ssrcInfo.getStream());
+                        errorEvent.response(errEvent);
+                    });
         } catch (InvalidArgumentException | SipException | ParseException e) {
 
             logger.error("[命令发送失败] 点播消息: {}", e.getMessage());
@@ -378,86 +382,300 @@ public class PlayServiceImpl implements IPlayService {
             mediaServerService.releaseSsrc(mediaServerItem.getId(), ssrcInfo.getSsrc());
 
             streamSession.remove(device.getDeviceId(), channelId, ssrcInfo.getStream());
-            SipSubscribe.EventResult eventResult = new SipSubscribe.EventResult(new CmdSendFailEvent(null));
-            eventResult.msg = "命令发送失败";
+//            有什么大病一般的代码
+//            SipSubscribe.EventResult eventResult = new SipSubscribe.EventResult(new CmdSendFailEvent(null));
+//            eventResult.msg = "命令发送失败";
+            EventObject eventObject = new EventObject("命令发送失败");
+            SipSubscribe.EventResult eventResult = new SipSubscribe.EventResult(eventObject);
             errorEvent.response(eventResult);
+//
+//            RequestMessage msg = new RequestMessage();
+//            msg.setKey(DeferredResultHolder.CALLBACK_CMD_PLAY + device.getDeviceId() + channelId);
+//            msg.setData(WVPResult.fail(ErrorCode.ERR_Invite_fail, "点播命令发送失败: " + e.getMessage()));
+//            resultHolder.invokeAllResult(msg);
+        }
+    }
 
-            RequestMessage msg = new RequestMessage();
-            msg.setKey(DeferredResultHolder.CALLBACK_CMD_PLAY + device.getDeviceId() + channelId);
-            msg.setData(WVPResult.fail(ErrorCode.ERR_Invite_fail.getCode(), "点播命令发送至设备异常"));
-            resultHolder.invokeAllResult(msg);
+    /**
+     * 从缓存中获取流信息
+     *
+     * @param deviceId
+     * @param channelId
+     * @return
+     */
+    private StreamInfo getStreamInfo(String deviceId, String channelId, WVPResult wvpResult) {
+        StreamInfo streamInfo = redisCatchStorage.queryPlayByDevice(deviceId, channelId);
+        // 两种情况, 一种是已经在推流中了. 一种是还没有开始推流
+        if (streamInfo == null) {
+            // 未开始推流, 开启新推流
+            return null;
+        }
+
+        // 能够读取到视频缓存
+        String streamId = streamInfo.getStream();
+        // 应该是不可能出现的异常情况,
+        if (streamId == null) {
+            logger.warn("[读取推流信息] 失败, redis缓存异常 streamId 为空");
+            // 说明缓存的流信息有问题
+            // 移除redis 缓存
+            redisCatchStorage.stopPlay(streamInfo);
+            redisCatchStorage.deleteDeviceStream(deviceId, channelId);
+            storager.stopPlay(deviceId, channelId);
+            return null;
+        }
+        String mediaServerId = streamInfo.getMediaServerId();
+        // 获取视频推流通道对应的流媒体信息
+        MediaServerItem mediaServerItem = mediaServerService.getOne(mediaServerId);
+        JSONObject rtpInfo = zlmresTfulUtils.getRtpInfo(mediaServerItem, streamId);
+        if (rtpInfo == null) {
+            logger.warn("[读取推流信息] 失败, rtpInfo为null");
+            redisCatchStorage.stopPlay(streamInfo);
+            redisCatchStorage.deleteDeviceStream(deviceId, channelId);
+            storager.stopPlay(deviceId, channelId);
+            return null;
+        }
+        if (rtpInfo.getInteger("code") != 0) {
+            redisCatchStorage.stopPlay(streamInfo);
+            redisCatchStorage.deleteDeviceStream(deviceId, channelId);
+            storager.stopPlay(deviceId, channelId);
+            return null;
+        }
+        if (!rtpInfo.getBoolean("exist")) {
+            // 说明流已经停止
+            redisCatchStorage.stopPlay(streamInfo);
+            redisCatchStorage.deleteDeviceStream(deviceId, channelId);
+            storager.stopPlay(deviceId, channelId);
+            return null;
+        }
+        int localPort = rtpInfo.getInteger("local_port");
+        if (localPort == 0) {
+            logger.warn("[读取推流信息] 失败, rtpServer存在,但是尚未开始推流");
+            // 此时说明rtpServer已经创建但是流还没有推上来, 所以稍微等待一下重试即可
+            // 使用 wvpResult 返回对应信息提示
+            wvpResult.setCode(ErrorCode.ERROR_Retry.getCode());
+            wvpResult.setMsg("点播已经在进行中,请稍后进行重试");
         }
+        return streamInfo;
     }
 
+    // 直接抛弃原有的播放 函数 创建新地播放函数, 重新梳理播放逻辑
+    public void startPlay(RequestMessage requestMsg, MediaServerItem mediaServerItem, Device device, String channelId, int isUsePs) {
+        // 获取缓存的流信息
+        WVPResult wvpResult = new WVPResult();
+        StreamInfo streamInfo = getStreamInfo(device.getDeviceId(), channelId, wvpResult);
 
+        if (streamInfo != null) {
+            logger.info("已经启用视频流 {}", wvpResult.toString());
+            // 说明已经在推流中了
+            if (wvpResult.getCode() != ErrorCode.ERROR_Retry.getCode()) {
+                // 在获取流信息没有更改 wvpResult 时将返回值设置为成功
+                logger.warn("{} 已经在推流, 并且无异常", device.getDeviceId(), channelId);
+                // 数据类型转换, 懒得改前端了
+                wvpResult = WVPResult.success(
+                        new StreamContent(streamInfo)
+                );
+            }
+            resultHolder.invokeResult(requestMsg.setData(wvpResult));
+            return;
+        }
+        // 说明还没有开始推流
+        if (!mediaServerItem.isRtpEnable()) {
+            logger.warn("设备应用的流媒体未启用");
+            resultHolder.invokeAllResult(
+                    requestMsg.setData(WVPResult.fail(ErrorCode.ERROR100, "设备应用的流媒体未启用"))
+            );
+            return;
+        }
+        // 生成streamId
+        String streamId = String.format("%s_%s", device.getDeviceId(), channelId);
+        SSRCInfo ssrcInfo = mediaServerService.openRTPServer(mediaServerItem, streamId, device.isSsrcCheck(), false);
+        if (ssrcInfo == null) {
+            logger.warn("开启收流失败");
+            resultHolder.invokeAllResult(
+                    requestMsg.setData(WVPResult.fail(ErrorCode.ERROR100, "开启收流失败"))
+            );
+            return;
+        }
+        // 尝试启用收流
+        if (ssrcInfo.getPort() <= 0) {
+            logger.info("[点播端口分配异常],deviceId={},channelId={},ssrcInfo={}", device.getDeviceId(), channelId, ssrcInfo);
+            resultHolder.invokeAllResult(
+                    requestMsg.setData(WVPResult.fail(ErrorCode.ERROR100, "点播端口分配异常, 请重试"))
+            );
+            return;
+        }
+        executePlayLive(requestMsg, mediaServerItem, ssrcInfo, device, channelId, isUsePs);
+    }
+
+    /**
+     * 执行推流相关操作 发送命令
+     *
+     * @param requestMsg
+     * @param mediaServerItem
+     * @param device
+     * @param channelId
+     * @param isUsePs
+     */
+    private void executePlayLive(RequestMessage requestMsg, MediaServerItem mediaServerItem, SSRCInfo ssrcInfo, Device device, String channelId, int isUsePs) {
+        logger.info("[支持点播推流] 收流端口: {}, 收流模式:{}, SSRC: {}, SSRC校验:{}",
+                ssrcInfo.getPort(), device.getStreamMode(),
+                ssrcInfo.getSsrc(), device.isSsrcCheck());
+        String stream = ssrcInfo.getStream();
+        logger.info("{} 分配的ZLM为: {} [{}:{}]", stream, mediaServerItem.getId(), mediaServerItem.getIp(), ssrcInfo.getPort());
+
+        // 构建errorHook 回调函数
+        ErrorHook errorHook = wvpResult ->
+        {
+            logger.info("收到点播命令回调 {}", wvpResult);
+            if (wvpResult.getCode() != ErrorCode.SUCCESS.getCode()) {
+                logger.error("[发送点播命令] 失败 {}", wvpResult.getMsg());
+                requestMsg.setData(wvpResult);
+                stopPlay(device, channelId);
+//            resultHolder.invokeAllResult(requestMsg);
+            }
+            // 监听zlm的流改变事件, 在流改变时直接回复 http 信息
+            addHookSubscribeForStreamChange(requestMsg, mediaServerItem, device, channelId, ssrcInfo);
+        };
+
+        try {
+            // 发送命令
+            cmder.sendPlayCmd(mediaServerItem, ssrcInfo, device, channelId, isUsePs, errorHook);
+        } catch (Exception e) {
+            e.printStackTrace();
+            logger.error("[发送点播命令] 失败 {}", e.getMessage());
+            // 未知的错误, 打印错误堆栈
+            requestMsg.setData(WVPResult.fail(ErrorCode.ERROR100, "发送点播命令失败"));
+            resultHolder.invokeAllResult(requestMsg);
+
+        }
+    }
+
+    // 停止推流
+    public void stopPlay(Device device, String channelId) {
+        WVPResult wvpResult = new WVPResult();
+        StreamInfo streamInfo = getStreamInfo(device.getDeviceId(), channelId, wvpResult);
+        // 获取 流媒体
 
 
+        // 发送bye给设备
+        ErrorHook errorHook = wvpResult1 ->
+        {
+            logger.info("关闭推流回调 {}", wvpResult1);
+            if (wvpResult1.getCode() != ErrorCode.SUCCESS.getCode()) {
+                logger.error("[关闭推流回调] 失败 {}", wvpResult1.getMsg());
+                return;
+            }
+            //
+            if (streamInfo != null) {
+                logger.info("找到正在推流的视频流 尝试停止 {}", wvpResult.toString());
+                MediaServerItem mediaServerItem = mediaServerService.getOne(
+                        streamInfo.getMediaServerId()
+                );
+                // 获取ssrc
+                logger.info("移除并关闭推流通道");
+                redisCatchStorage.stopPlay(streamInfo);
+                redisCatchStorage.deleteDeviceStream(device.getDeviceId(), channelId);
+                mediaServerService.releaseSsrc(streamInfo.getMediaServerId(), streamInfo.getSsrc());
+                mediaServerService.closeRTPServer(streamInfo.getMediaServerId(), streamInfo.getStream());
+            } else {
+                logger.info("在没有服务端缓存的情况下关闭推流通道");
+            }
+        };
+        try {
+            // 发送命令
+            cmder.stopPlayCmd(device, channelId, errorHook);
+        } catch (Exception e) {
+            e.printStackTrace();
+            logger.error("[关闭推流] 失败 {}", e.getMessage());
+            // 未知的错误, 打印错误堆栈
+        }
+    }
+
+    /**
+     * 截取视频快照
+     *
+     * @param mediaServerItemInUse
+     * @param deviceId
+     * @param channelId
+     * @param ssrcInfo
+     */
+    public void screenshot(MediaServerItem mediaServerItemInUse, String deviceId, String channelId, SSRCInfo ssrcInfo) {
+        String streamUrl = String.format("http://127.0.0.1:%s/%s/%s.live.flv",
+                mediaServerItemInUse.getHttpPort(), "rtp", ssrcInfo.getStream());
+        String path = "snap";
+        String fileName = deviceId + "_" + channelId + ".jpg";
+        // 请求截图
+        logger.info("[尝试截取视频快照]: " + fileName);
+        zlmresTfulUtils.getSnap(mediaServerItemInUse, streamUrl, 15, 1, path, fileName);
+    }
+
     public void openBroadcast(MediaServerItem mediaServerItem,
                               Device device,
                               int waitTime,
-                              BroadcastCallback callback){
-            logger.warn("[语音广播] 开语音广播 新");
-            JSONObject errJson = new JSONObject();
-            try {
-                cmder.audioBroadcastCmd(device);
-            } catch (InvalidArgumentException | SipException | ParseException e) {
+                              BroadcastCallback callback) {
+        logger.warn("[语音广播] 开语音广播 新");
+        JSONObject errJson = new JSONObject();
+        try {
+            cmder.audioBroadcastCmd(device);
+        } catch (InvalidArgumentException | SipException | ParseException e) {
                 logger.error("[命令发送失败] 发送broadcast中 errorMsg: {}", e.getMessage());
                 errJson.put("msg", "[命令发送失败] 无法发送broadcast消息");
                 callback.run(2, errJson, null);
             }
             logger.warn("等待设备返回invite");
-            HookSubscribeForKey broadcastForInviteHook = GBHookSubscribeFactory.on_broadcast_invite(device.getDeviceId());
-            // 创建计时器,计时结束未收到invite则自动进行失败处理
-            String timeOutTaskKey = UUID.randomUUID().toString();
-            dynamicTask.startDelay(timeOutTaskKey, () -> {
-                // todo 发送 bye 通知给设备?
-                logger.warn("invite超时");
-                errJson.put("msg", "等待设备语音invite信息超时");
-                callback.run(1, errJson, null);
-            }, waitTime);
-
-            GBHookSubscribe.addInviteSubscribe(broadcastForInviteHook,
-                    (int code, JSONObject json, SIPRequest request) -> {
-                        // invite信息返回
-                        logger.info("[语音广播] 接收到设备invite信息___订阅事件触发 JSONDATA: {}", json.toJSONString());
-                        // 取消计时器
-                        dynamicTask.stop(timeOutTaskKey);
-                        callback.run(0, json, request);
-                    });
-    };
-    @Override
-    public void onPublishHandlerForPlay(MediaServerItem mediaServerItem, JSONObject response, String deviceId, String channelId) {
+        HookSubscribeForKey broadcastForInviteHook = GBHookSubscribeFactory.on_broadcast_invite(device.getDeviceId());
+        // 创建计时器,计时结束未收到invite则自动进行失败处理
+        String timeOutTaskKey = UUID.randomUUID().toString();
+        dynamicTask.startDelay(timeOutTaskKey, () -> {
+            // todo 发送 bye 通知给设备?
+            logger.warn("invite超时");
+            errJson.put("msg", "等待设备语音invite信息超时");
+            callback.run(1, errJson, null);
+        }, waitTime);
+
+        GBHookSubscribe.addInviteSubscribe(
+                broadcastForInviteHook,
+                (int code, JSONObject json, SIPRequest request) -> {
+                    // invite信息返回
+                    logger.info("[语音广播] 接收到设备invite信息___订阅事件触发 JSONDATA: {}", json.toJSONString());
+                    // 取消计时器
+                    dynamicTask.stop(timeOutTaskKey);
+                    callback.run(0, json, request);
+                });
+    }
+
+    ;
+
+    private void onPublishHandlerForPlay(
+            RequestMessage requestMsg, MediaServerItem mediaServerItem,
+            JSONObject response, String deviceId, String channelId, String ssrc) {
         StreamInfo streamInfo = onPublishHandler(mediaServerItem, response, deviceId, channelId);
-        RequestMessage msg = new RequestMessage();
-        msg.setKey(DeferredResultHolder.CALLBACK_CMD_PLAY + deviceId + channelId);
         if (streamInfo != null) {
+            streamInfo.setBack(false);
+            streamInfo.setSsrc(ssrc);
             DeviceChannel deviceChannel = storager.queryChannel(deviceId, channelId);
             if (deviceChannel != null) {
                 deviceChannel.setStreamId(streamInfo.getStream());
                 storager.startPlay(deviceId, channelId, streamInfo.getStream());
             }
             redisCatchStorage.startPlay(streamInfo);
-
-            WVPResult wvpResult = new WVPResult();
-            wvpResult.setCode(ErrorCode.SUCCESS.getCode());
-            wvpResult.setMsg(ErrorCode.SUCCESS.getMsg());
-            wvpResult.setData(streamInfo);
-
-            msg.setData(wvpResult);
-            resultHolder.invokeAllResult(msg);
-
+            // 流信息转换
+            requestMsg.setData(WVPResult.success(new StreamContent(streamInfo)));
+            resultHolder.invokeAllResult(requestMsg);
         } else {
             logger.warn("设备预览API调用失败!");
-            msg.setData(WVPResult.fail(ErrorCode.ERROR100.getCode(), "设备预览API调用失败!"));
-            resultHolder.invokeAllResult(msg);
+            requestMsg.setData(WVPResult.fail(ErrorCode.ERROR100, "设备预览API调用失败!"));
+            resultHolder.invokeAllResult(requestMsg);
         }
     }
 
-    private void onPublishHandlerForPlayback(MediaServerItem mediaServerItem, JSONObject response, String deviceId, String channelId, PlayBackCallback playBackCallback) {
+    private void onPublishHandlerForPlayback(MediaServerItem mediaServerItem, JSONObject response, String deviceId, String channelId, String ssrc, PlayBackCallback playBackCallback) {
 
         StreamInfo streamInfo = onPublishHandler(mediaServerItem, response, deviceId, channelId);
         PlayBackResult<StreamInfo> playBackResult = new PlayBackResult<>();
         if (streamInfo != null) {
+            streamInfo.setBack(true);
+            streamInfo.setSsrc(ssrc);
             DeviceChannel deviceChannel = storager.queryChannel(deviceId, channelId);
             if (deviceChannel != null) {
                 deviceChannel.setStreamId(streamInfo.getStream());
@@ -489,7 +707,6 @@ public class PlayServiceImpl implements IPlayService {
         } else {
             // 尝试获取device配置的zlm服务
             mediaServerItem = mediaServerService.getOne(device.getMediaServerId());
-
             // 如果默认zlm无法找到则随机分配一个zlm
             if(mediaServerItem == null){
                 logger.warn("无法找到设备默认流媒体服务,即将使用默认流媒体服务");
@@ -638,7 +855,7 @@ public class PlayServiceImpl implements IPlayService {
                                             logger.info("[ZLM HOOK] ssrc修正后收到订阅消息: " + response.toJSONString());
                                             dynamicTask.stop(playBackTimeOutTaskKey);
                                             // hook响应
-                                            onPublishHandlerForPlayback(mediaServerItemInUse, response, device.getDeviceId(), channelId, playBackCallback);
+                                            onPublishHandlerForPlayback(mediaServerItemInUse, response, device.getDeviceId(), channelId, ssrcInfo.getSsrc(), playBackCallback);
                                             hookEvent.call(new InviteStreamInfo(mediaServerItem, null, eventResult.callId, "rtp", ssrcInfo.getStream()));
                                         });
                                     }
@@ -777,7 +994,7 @@ public class PlayServiceImpl implements IPlayService {
                                             logger.info("[ZLM HOOK] ssrc修正后收到订阅消息: " + response.toJSONString());
                                             dynamicTask.stop(downLoadTimeOutTaskKey);
                                             // hook响应
-                                            onPublishHandlerForPlayback(mediaServerItemInUse, response, device.getDeviceId(), channelId, hookCallBack);
+                                            onPublishHandlerForPlayback(mediaServerItemInUse, response, device.getDeviceId(), channelId, ssrcInfo.getSsrc(), hookCallBack);
                                             hookEvent.call(new InviteStreamInfo(mediaServerItem, null, eventResult.callId, "rtp", ssrcInfo.getStream()));
                                         });
                                     }
@@ -847,7 +1064,8 @@ public class PlayServiceImpl implements IPlayService {
         RequestMessage msg = new RequestMessage();
         msg.setKey(DeferredResultHolder.CALLBACK_CMD_DOWNLOAD + deviceId + channelId);
         msg.setId(uuid);
-        StreamInfo streamInfo = onPublishHandler(inviteStreamInfo.getMediaServerItem(), inviteStreamInfo.getResponse(), deviceId, channelId);
+        StreamInfo streamInfo = onPublishHandler(
+                inviteStreamInfo.getMediaServerItem(), inviteStreamInfo.getResponse(), deviceId, channelId);
         if (streamInfo != null) {
             redisCatchStorage.startDownload(streamInfo, inviteStreamInfo.getCallId());
             msg.setData(JSON.toJSONString(streamInfo));
@@ -860,9 +1078,9 @@ public class PlayServiceImpl implements IPlayService {
     }
 
 
-    public StreamInfo onPublishHandler(MediaServerItem mediaServerItem, JSONObject resonse, String deviceId, String channelId) {
-        String streamId = resonse.getString("stream");
-        JSONArray tracks = resonse.getJSONArray("tracks");
+    public StreamInfo onPublishHandler(MediaServerItem mediaServerItem, JSONObject json, String deviceId, String channelId) {
+        String streamId = json.getString("stream");
+        JSONArray tracks = json.getJSONArray("tracks");
         StreamInfo streamInfo = mediaService.getStreamInfoByAppAndStream(mediaServerItem, "rtp", streamId, tracks, null);
         streamInfo.setDeviceID(deviceId);
         streamInfo.setChannelId(channelId);
@@ -1051,9 +1269,9 @@ public class PlayServiceImpl implements IPlayService {
                     null,
                     null);
 
-            callback.run(0, null,null);
-        }catch(InvalidArgumentException |  SipException | ParseException e){
-            logger.error("[下发audio拉流invite失败]",e);
+            callback.run(0, null, null);
+        } catch (InvalidArgumentException | SipException | ParseException e) {
+            logger.error("[下发audio拉流invite失败]", e);
             logger.error("[zlm控制异常] 创建媒体流失败");
             errJson.put("msg", "[zlm控制异常] 创建媒体流失败");
             callback.run(2, errJson, null);
@@ -1061,5 +1279,24 @@ public class PlayServiceImpl implements IPlayService {
 //                    eventResult.msg = "命令发送失败";
 //                    errorEvent.response(eventResult);
         }
-    };
+    }
+
+    ;
+
+
+    private void addHookSubscribeForStreamChange(
+            RequestMessage requestMsg, MediaServerItem mediaServerItem,
+            Device device, String channelId, SSRCInfo ssrcInfo) {
+        String stream = ssrcInfo.getStream();
+        HookSubscribeForStreamChange hookSubscribe = HookSubscribeFactory.on_stream_changed("rtp", stream, true, "rtsp", mediaServerItem.getId());
+        subscribe.addSubscribe(hookSubscribe, (MediaServerItem mediaServerItemInUse, JSONObject json) -> {
+            // zlm 事件触发. 流改变事件
+            logger.info("[ZLM HOOK] 收到ZLM流 编号信息: {}", json.toJSONString());
+            logger.info("[点播成功] deviceId: {}, channelId: {}", device.getDeviceId(), channelId);
+            // 处理结果
+            onPublishHandlerForPlay(requestMsg, mediaServerItemInUse, json, device.getDeviceId(), channelId, ssrcInfo.getSsrc());
+            // 请求截图
+            screenshot(mediaServerItemInUse, device.getDeviceId(), channelId, ssrcInfo);
+        });
+    }
 }

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

@@ -3,6 +3,9 @@ package com.genersoft.iot.vmp.service.impl;
 import com.genersoft.iot.vmp.gb28181.bean.SipUserConfig;
 import com.genersoft.iot.vmp.service.ISipConfigService;
 import com.genersoft.iot.vmp.storager.dao.SipConfigMapper;
+import com.genersoft.iot.vmp.utils.DateUtil;
+import com.github.pagehelper.PageHelper;
+import com.github.pagehelper.PageInfo;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.beans.factory.annotation.Autowired;
@@ -19,6 +22,43 @@ public class SipConfigService implements ISipConfigService {
 
     @Override
     public SipUserConfig getSipConfigByDomain(String domain) {
-        return sipConfigMapper.querySipConfigByDomain(domain);
+        logger.info("查询sip配置信息,domain:" + domain);
+        List<SipUserConfig> sipConfigs = sipConfigMapper.querySipConfigByDomain(domain);
+        // 如果有的话返回第一个
+        if (sipConfigs == null) {
+            logger.warn("无法找到对应的sip配置信息 {}", domain);
+            return null;
+        }
+        return sipConfigs.get(0);
     }
+
+    @Override
+    public PageInfo<SipUserConfig> getSipConfigsById(String adminId, int page, int limit) {
+        PageHelper.startPage(page, limit);
+        logger.info("获取管理员{}, 的sip配置信息", adminId);
+        List<SipUserConfig> sipConfigs = sipConfigMapper.querySipConfigsByAdminId(adminId);
+        return new PageInfo<>(sipConfigs);
+    }
+
+    @Override
+    public SipUserConfig getSipConfigById(String adminId, String sipId) {
+        return sipConfigMapper.querySipConfigsById(adminId, sipId);
+    }
+
+    public boolean editSipConfig(String sipId, SipUserConfig sipConfig) {
+        return sipConfigMapper.updateSipConfig(sipConfig, sipId) > 0;
+    }
+
+    public boolean addSipConfig(String adminId, SipUserConfig sipConfig) {
+        String createTime = "";
+        sipConfig.setAdminId(adminId);
+        sipConfig.setEnable("1");
+        sipConfig.setCreateTime(DateUtil.getNow());
+        return sipConfigMapper.addSipConfig(sipConfig) > 0;
+    }
+
+    public boolean deleteSipConfig(String adminId, String sipId) {
+        return sipConfigMapper.deleteSipConfig(sipId, adminId) > 0;
+    }
+
 }

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

@@ -290,5 +290,13 @@ public interface IRedisCatchStorage {
 
     boolean isBroadcastItem(String deviceId);
 
+    /**
+     * 移除设备通道所有的视频流缓存
+     *
+     * @param deviceId
+     * @param channelId
+     */
+    void deleteDeviceStream(String deviceId, String channelId);
+
 
 }

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

@@ -27,10 +27,10 @@ public interface AccountMapper {
     @Insert("INSERT INTO device_bind " +
             "(userId, devId, devCode) VALUES" +
             "(#{id}, #{deviceId}, #{devCode})")
-    int bindDevice(int id, int deviceId, String devCode);
+    int bindDevice(String id, String deviceId, String devCode);
 
     @Delete("DELETE FROM device_bind WHERE userId=#{userId} AND devId=#{deviceId}")
-    int unBindDevice(int userId, int deviceId);
+    int unBindDevice(String userId, String deviceId);
 
     @Select("SELECT * FROM device_bind where devCode = #{bindCode}")
     Device findDeviceByDevCode(String devCode);
@@ -38,13 +38,13 @@ public interface AccountMapper {
     // 查询用户设备
     //    SELECT * FROM `device_bind` as b LEFT JOIN `device` as d ON b.deviceId = d.id where b.userId = 1
     @Select("SELECT * FROM `device_bind` as b LEFT JOIN `device` as d ON b.devId = d.id where b.userId = #{id}")
-    List<Device> getAccountDevices(int id);
+    List<Device> getAccountDevices(String id);
 
     @Select("SELECT * FROM `device_bind` as b LEFT JOIN `device` as d ON b.devId = d.id where b.userId = #{userId} AND b.devId = #{devId}")
-    Device getAccountDevice(int userId, int devId);
+    Device getAccountDevice(String userId, String devId);
 
     @Select("SELECT * FROM account WHERE id = #{id}")
-    UserAccount selectById(int id);
+    UserAccount selectById(String id);
 
     @Select("SELECT * FROM account WHERE account = #{account}")
     UserAccount findByAccount(String account);

+ 39 - 0
src/main/java/com/genersoft/iot/vmp/storager/dao/AdminMapper.java

@@ -1,5 +1,6 @@
 package com.genersoft.iot.vmp.storager.dao;
 
+import com.genersoft.iot.vmp.gb28181.bean.Device;
 import com.genersoft.iot.vmp.storager.dao.dto.AdminAccount;
 import org.apache.ibatis.annotations.*;
 import org.springframework.stereotype.Repository;
@@ -42,6 +43,10 @@ public interface AdminMapper {
     @ResultMap(value = "roleMap")
     AdminAccount selectById(int id);
 
+    @Select("select u.*, r.id as roleID, r.name as roleName, r.authority as roleAuthority , r.createTime as roleCreateTime , r.updateTime as roleUpdateTime FROM user u, user_role r WHERE u.roleId=r.id and u.id=#{id}")
+    @ResultMap(value = "roleMap")
+    AdminAccount getUserById(String id);
+
     @Select("select u.*, r.id as roleID, r.name as roleName, r.authority as roleAuthority , r.createTime as roleCreateTime , r.updateTime as roleUpdateTime FROM user u, user_role r WHERE u.roleId=r.id and u.username=#{username}")
     @ResultMap(value = "roleMap")
     AdminAccount getUserByUsername(String username);
@@ -65,4 +70,38 @@ public interface AdminMapper {
 
     @Select("select u.id,u.pushKey from user as u where id=#{id}")
     List<AdminAccount> getPushKey(int id);
+
+//    SELECT dev.*
+//    FROM sip_config as sip
+//    JOIN device as dev ON sip.sipDomain = dev.domain
+//    WHERE sip.adminId = 1 AND dev.id = 15;
+
+    @Select("SELECT dev.* FROM sip_config as sip JOIN device as dev ON sip.sipDomain = dev.domain WHERE sip.adminId = #{adminId} AND dev.id = #{devId}")
+    /**
+     * 获取某个管理员设备,根据设备表id
+     * @param adminId 管理员id
+     * @param devId 设备表id
+     */
+    Device getDeviceByAdminIdAndDevId(String adminId, String devId);
+
+    @Select("SELECT dev.* FROM sip_config as sip JOIN device as dev ON sip.sipDomain = dev.domain WHERE sip.adminId = #{adminId} AND dev.deviceId = #{deviceSipId}")
+    /**
+     * 获取某个管理员设备,根据设备国标id
+     * @param adminId 管理员id
+     * @param deviceSipId 设备国标id
+     */
+    Device getAdminDeviceBySipId(String adminId, String deviceSipId);
+
+    @Select("SELECT dev.* FROM sip_config as sip JOIN device as dev ON sip.sipDomain = dev.domain WHERE sip.adminId = #{adminId}")
+    /**
+     * 获取某个管理员账户下的所有设备
+     */
+    List<Device> getDevicesByAdminId(String adminId);
+
+    @Select("SELECT * FROM  device as dev  WHERE dev.domain = #{domain}")
+    /**
+     * 获取某个国标域的所有设备
+     * @param domain 国标域
+     */
+    List<Device> getDevicesByDomain(String domain);
 }

+ 3 - 0
src/main/java/com/genersoft/iot/vmp/storager/dao/DeviceChannelMapper.java

@@ -403,4 +403,7 @@ public interface DeviceChannelMapper {
 
     @Select("select de.* from device de left join device_channel dc on de.deviceId = dc.deviceId where dc.channelId=#{channelId}")
     List<Device> getDeviceByChannelId(String channelId);
+
+    @Select("select * from device_channel where deviceId = #{deviceId}")
+    List<DeviceChannel> getChannelsByDeviceId(String deviceId);
 }

+ 27 - 15
src/main/java/com/genersoft/iot/vmp/storager/dao/SipConfigMapper.java

@@ -13,6 +13,8 @@ import com.genersoft.iot.vmp.gb28181.bean.SipUserConfig;
 import org.apache.ibatis.annotations.*;
 import org.springframework.stereotype.Repository;
 
+import java.util.List;
+
 /**
  * sip配置信息 sipConfig 表
  */
@@ -26,16 +28,22 @@ public interface SipConfigMapper {
             "sipDomain, " +
             "serverId, " +
             "password," +
+            "description," +
+            "enable," +
+            "enableBind," +
             "createTime" +
             ") VALUES (" +
             "#{adminId}, " +
             "#{sipDomain}, " +
             "#{serverId}, " +
             "#{password}, " +
+            "#{description}, " +
+            "#{enable}, " +
+            "#{enableBind}, " +
             "#{createTime} " +
             ")"
     )
-    int addSipConfig(int adminId, String sipDomain, String serverId, String password, String createTime);
+    int addSipConfig(SipUserConfig sipUserConfig);
 
     // 禁用规则
     @Update("UPDATE sip_config SET enable = 1 WHERE id = ${id}")
@@ -47,31 +55,35 @@ public interface SipConfigMapper {
 
     // 更新sip规则
     @Update("UPDATE sip_config SET " +
-            "adminId = #{adminId}, " +
-            "sipDomain = #{sipDomain}, " +
-            "serverId = #{serverId}, " +
-            "password = #{password}, " +
-            "description = #{description}, " +
-            "enable = #{enable}, " +
-            "createTime = #{createTime} " +
-            "WHERE id = #{id}"
+            "sipDomain = #{sipUserConfig.sipDomain}, " +
+            "serverId = #{sipUserConfig.serverId}, " +
+            "password = #{sipUserConfig.password}, " +
+            "description = #{sipUserConfig.description}, " +
+            "enable = #{sipUserConfig.enable}, " +
+            "enableBind = #{sipUserConfig.enableBind} " +
+            "WHERE id = #{SipId}"
     )
-    int updateSipConfig(SipUserConfig sipUserConfig);
+    int updateSipConfig(@Param("sipUserConfig") SipUserConfig sipUserConfig, @Param("SipId") String SipId);
+
 
     // 删除sip规则
-    @Delete("DELETE FROM sip_config WHERE id = ${id}")
-    int deleteSipConfig(SipUserConfig sipUserConfig);
+    @Delete("DELETE FROM sip_config WHERE id = ${sipId} adminId = ${adminId}")
+    int deleteSipConfig(String sipId, String adminId);
 
     // 查询根据域获取对应的sip配置
     @Select("SELECT * FROM sip_config WHERE sipDomain = ${sipDomain}")
-    SipUserConfig querySipConfigByDomain(String sipDomain);
+    List<SipUserConfig> querySipConfigByDomain(String sipDomain);
 
     // 获取指定管理员创建的sip规则
     @Select("SELECT * FROM sip_config WHERE adminId = ${adminId}")
-    SipUserConfig querySipConfigByAdminId(int adminId);
+    List<SipUserConfig> querySipConfigsByAdminId(String adminId);
+
+    @Select("SELECT * FROM sip_config WHERE adminId = ${adminId} and id =${sipId}")
+    SipUserConfig querySipConfigsById(String adminId, String sipId);
+
 
     // 获取所有sip规则
     @Select("SELECT * FROM sip_config")
-    SipUserConfig queryAllSipConfig();
+    List<SipUserConfig> queryAllSipConfig();
 
 }

+ 24 - 8
src/main/java/com/genersoft/iot/vmp/storager/impl/RedisCatchStorageImpl.java

@@ -91,9 +91,10 @@ public class RedisCatchStorageImpl implements IRedisCatchStorage {
      */
     @Override
     public boolean startPlay(StreamInfo stream) {
-
-        return RedisUtil.set(String.format("%S_%s_%s_%s_%s_%s", VideoManagerConstants.PLAYER_PREFIX, userSetting.getServerId(),
-                        stream.getMediaServerId(), stream.getStream(), stream.getDeviceID(), stream.getChannelId()),
+        return RedisUtil.set(String.format("%S_%s_%s_%s_%s_%s_%s_%s", VideoManagerConstants.PLAYER_PREFIX, userSetting.getServerId(),
+                        stream.getMediaServerId(), stream.getStream(), stream.getDeviceID(), stream.getChannelId(),
+                        stream.getSsrc(),
+                        stream.getBack()),
                 stream);
     }
 
@@ -157,14 +158,15 @@ public class RedisCatchStorageImpl implements IRedisCatchStorage {
 
     @Override
     public StreamInfo queryPlayByDevice(String deviceId, String channelId) {
-        List<Object> playLeys = RedisUtil.scan(String.format("%S_%s_*_*_%s_%s", VideoManagerConstants.PLAYER_PREFIX,
-                userSetting.getServerId(),
-                deviceId,
-                channelId));
+        List<Object> playLeys =
+                RedisUtil.scan(String.format("%S_%s_*_*_%s_%s", VideoManagerConstants.PLAYER_PREFIX,
+                        userSetting.getServerId(),
+                        deviceId,
+                        channelId));
         if (playLeys == null || playLeys.size() == 0) {
             return null;
         }
-        return (StreamInfo)RedisUtil.get(playLeys.get(0).toString());
+        return (StreamInfo) RedisUtil.get(playLeys.get(0).toString());
     }
 
     @Override
@@ -952,5 +954,19 @@ public class RedisCatchStorageImpl implements IRedisCatchStorage {
         return broadcastItem != null && broadcastItem.getIpcAudioPort() != null;
     }
 
+    // 移除设备视频流信息
+    @Override
+    public void deleteDeviceStream(String deviceId, String channelId) {
+        String searchKey = String.format("%S_%s_*_*_%s_%s",
+                VideoManagerConstants.PLAYER_PREFIX,
+                userSetting.getServerId(),
+                deviceId,
+                channelId);
+        List<Object> keys = RedisUtil.scan(searchKey);
+        for (Object key : keys) {
+            RedisUtil.del((String) key);
+        }
+    }
+
 
 }

+ 27 - 22
src/main/java/com/genersoft/iot/vmp/storager/impl/VideoManagerStorageImpl.java

@@ -635,28 +635,33 @@ public class VideoManagerStorageImpl implements IVideoManagerStorage {
 		return libData.getVersion();
 	}
 
-	public AiLib queryItemInfoByItemId(int itemId){
-		List<AiLib> aiLibItemArr = HfyDevAiMapper.queryItemInfoByItemId(itemId);
-		if(aiLibItemArr.isEmpty()){throw new ControllerException(ErrorCode.ERROR400.getCode(),"无法找到该数据,请检查id");}
-		return aiLibItemArr.stream().findFirst().get();
-	}
-	@Override
-	public PageInfo queryChannelsByDeviceId(String deviceId, String query, Boolean hasSubChannel, Boolean online, Boolean catalogUnderDevice, int page, int count) {
-		// 获取到所有正在播放的流
-		PageHelper.startPage(page, count);
-		List<DeviceChannel> all;
-		if (catalogUnderDevice != null && catalogUnderDevice) {
-			all = deviceChannelMapper.queryChannels(deviceId, deviceId, query, hasSubChannel, online,null);
-			// 海康设备的parentId是SIP id
-			List<DeviceChannel> deviceChannels = deviceChannelMapper.queryChannels(deviceId, sipConfig.getId(), query, hasSubChannel, online,null);
-			all.addAll(deviceChannels);
-		}else {
-			all = deviceChannelMapper.queryChannels(deviceId, null, query, hasSubChannel, online,null);
-		}
-		return new PageInfo<>(all);
-	}
-
-	@Override
+	public AiLib queryItemInfoByItemId(int itemId) {
+        List<AiLib> aiLibItemArr = HfyDevAiMapper.queryItemInfoByItemId(itemId);
+        if (aiLibItemArr.isEmpty()) {
+            throw new ControllerException(ErrorCode.ERROR400.getCode(), "无法找到该数据,请检查id");
+        }
+        return aiLibItemArr.stream().findFirst().get();
+    }
+
+    @Override
+    public PageInfo<DeviceChannel> queryChannelsByDeviceId(String deviceId, String query, Boolean hasSubChannel, Boolean online, Boolean catalogUnderDevice, int page, int count) {
+        PageHelper.startPage(page, count);
+        List<DeviceChannel> allChannels;
+        // 海康设备的parentId是SIP id
+        if (catalogUnderDevice != null && catalogUnderDevice) {
+            List<DeviceChannel> channels1 = deviceChannelMapper.queryChannels(deviceId, deviceId, query, hasSubChannel, online, null);
+            List<DeviceChannel> channels2 = deviceChannelMapper.queryChannels(deviceId, sipConfig.getId(), query, hasSubChannel, online, null);
+            allChannels = new ArrayList<>(channels1);
+            allChannels.addAll(channels2);
+        } else {
+            allChannels = deviceChannelMapper.queryChannels(deviceId, null, query, hasSubChannel, online, null);
+        }
+
+        return new PageInfo<>(allChannels);
+    }
+
+
+    @Override
 	public List<DeviceChannelExtend> queryChannelsByDeviceIdWithStartAndLimit(String deviceId, List<String> channelIds, String query, Boolean hasSubChannel, Boolean online, int start, int limit) {
 		return deviceChannelMapper.queryChannelsByDeviceIdWithStartAndLimit(deviceId, channelIds, null, query, hasSubChannel, online, start, limit);
 	}

+ 33 - 0
src/main/java/com/genersoft/iot/vmp/utils/AuthorUtil.java

@@ -0,0 +1,33 @@
+package com.genersoft.iot.vmp.utils;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class AuthorUtil {
+    private final static Logger logger = LoggerFactory.getLogger(AuthorUtil.class);
+
+    static public String getAdminId() {
+        try {
+            String adminId = StpAdminUtil.getLoginId().toString();
+            return adminId;
+        } catch (Exception e) {
+            logger.warn("[admin] 获取管理员id: {}", e.getMessage());
+            return null;
+        }
+    }
+
+    static public String getUserId() {
+        try {
+            String userId = StpUserUtil.getLoginId().toString();
+            return userId;
+        } catch (Exception e) {
+            logger.warn("[user] 获取用户id: {}", e.getMessage());
+            return null;
+        }
+    }
+
+    /**
+     * 判断设备id 是否能够和 用户id 或者 管理员id 对应
+     */
+
+}

+ 60 - 0
src/main/java/com/genersoft/iot/vmp/utils/DeviceHelper.java

@@ -0,0 +1,60 @@
+package com.genersoft.iot.vmp.utils;
+
+import com.genersoft.iot.vmp.gb28181.DeviceShare;
+import com.genersoft.iot.vmp.gb28181.bean.Device;
+import com.genersoft.iot.vmp.service.IAccountService;
+import com.genersoft.iot.vmp.service.IAdminService;
+import com.genersoft.iot.vmp.service.IDeviceService;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+@Component
+public class DeviceHelper {
+    private final static Logger logger = LoggerFactory.getLogger(DeviceHelper.class);
+    @Autowired
+    private IAdminService adminService;
+
+    @Autowired
+    private IAccountService accountService;
+
+    @Autowired
+    private IDeviceService deviceService;
+
+    @Autowired
+    private DeviceShare deviceShare;
+
+    /**
+     * 获取设备
+     *
+     * @param deviceId
+     * @param shareCode
+     * @return
+     */
+    public Device getDevice(String deviceId, String shareCode) {
+        Device device = null;
+        String userId = AuthorUtil.getUserId();
+        String adminId = AuthorUtil.getAdminId();
+        if (adminId != null) {
+            logger.info("get admin:{} device:{}", adminId, deviceId);
+            // 管理员用户目前基本使用 国标id 进行设备操作
+            device = adminService.getAdminDeviceBySipId(adminId, deviceId);
+        } else if (userId != null) {
+            device = accountService.getAccountDevice(userId, deviceId);
+        } else if (shareCode != null) {
+            // 判断是否为分享码访问
+            device = deviceService.getDevice(deviceId);
+            if (!deviceShare.shareCheck(device, shareCode, false)) {
+                // 判断设备分享是否过期,如果过期则自动进行关闭分享
+                if (deviceShare.checkShareCodeIsExpires(device, device.getShareCode()) == 0) {
+                    // 设备还在分享中, 延长分享时间
+                    deviceShare.updateTime(device);
+                } else {
+                    deviceShare.closeShare(device);
+                }
+            }
+        }
+        return device;
+    }
+}

+ 12 - 10
src/main/java/com/genersoft/iot/vmp/utils/redis/RedisUtil.java

@@ -868,16 +868,18 @@ public class RedisUtil {
         if (redisTemplate == null) {
             redisTemplate = SpringBeanFactory.getBean("redisTemplate");
         }
-        Set<String> resultKeys = (Set<String>) redisTemplate.execute((RedisCallback<Set<String>>) connection -> {
-            ScanOptions scanOptions = ScanOptions.scanOptions().match("*" + query + "*").count(1000).build();
-            Cursor<byte[]> scan = connection.scan(scanOptions);
-            Set<String> keys = new HashSet<>();
-            while (scan.hasNext()) {
-                byte[] next = scan.next();
-                keys.add(new String(next));
-            }
-            return keys;
-        });
+        Set<String> resultKeys = (Set<String>) redisTemplate.execute(
+                (RedisCallback<Set<String>>) connection ->
+                {
+                    ScanOptions scanOptions = ScanOptions.scanOptions().match("*" + query + "*").count(1000).build();
+                    Cursor<byte[]> scan = connection.scan(scanOptions);
+                    Set<String> keys = new HashSet<>();
+                    while (scan.hasNext()) {
+                        byte[] next = scan.next();
+                        keys.add(new String(next));
+                    }
+                    return keys;
+                });
 
         return new ArrayList<>(resultKeys);
     }

+ 6 - 8
src/main/java/com/genersoft/iot/vmp/vmanager/user/AccountController.java → src/main/java/com/genersoft/iot/vmp/vmanager/account/AccountController.java

@@ -1,8 +1,7 @@
-package com.genersoft.iot.vmp.vmanager.user;
+package com.genersoft.iot.vmp.vmanager.account;
 
 import cn.dev33.satoken.annotation.SaIgnore;
-import com.genersoft.iot.vmp.conf.security.SecurityUtils;
-import com.genersoft.iot.vmp.conf.security.dto.LoginUser;
+import cn.dev33.satoken.stp.SaTokenInfo;
 import com.genersoft.iot.vmp.conf.security.saToken.SaUserCheckRole;
 import com.genersoft.iot.vmp.gb28181.bean.Device;
 import com.genersoft.iot.vmp.service.IAccountService;
@@ -11,7 +10,6 @@ import com.genersoft.iot.vmp.storager.dao.dto.UserAccount;
 import com.genersoft.iot.vmp.utils.StpUserUtil;
 import com.genersoft.iot.vmp.vmanager.bean.ErrorCode;
 import com.genersoft.iot.vmp.vmanager.bean.WVPResult;
-import com.github.pagehelper.PageInfo;
 import io.swagger.v3.oas.annotations.Operation;
 import io.swagger.v3.oas.annotations.Parameter;
 import io.swagger.v3.oas.annotations.tags.Tag;
@@ -21,7 +19,6 @@ import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.web.bind.annotation.*;
 
 import javax.servlet.http.HttpServletRequest;
-import java.util.List;
 
 @Tag(name = "普通用户管理")
 @CrossOrigin(origins = "*")
@@ -73,8 +70,8 @@ public class AccountController {
     @Operation(summary = "登录")
     @Parameter(name = "account", description = "登陆账号", required = true)
     @Parameter(name = "password", description = "密码", required = true)
-    public WVPResult<String> login(@RequestParam String account,
-                                   @RequestParam String password) {
+    public WVPResult<SaTokenInfo> login(@RequestParam String account,
+                                        @RequestParam String password) {
         logger.info("[登录账号] account {}", account);
         UserAccount userAccount = accountService.login(account, password);
         if (userAccount == null) {
@@ -83,7 +80,8 @@ public class AccountController {
                     "账号或者密码错误");
         }
         StpUserUtil.login(userAccount.getId());
-        return WVPResult.success(null, "ok");
+
+        return WVPResult.success(StpUserUtil.getTokenInfo());
     }
 
     @GetMapping("/load/bind")

+ 54 - 9
src/main/java/com/genersoft/iot/vmp/vmanager/account/AccountDeviceControl.java

@@ -2,22 +2,29 @@ package com.genersoft.iot.vmp.vmanager.account;
 
 import com.genersoft.iot.vmp.conf.security.saToken.SaUserCheckRole;
 import com.genersoft.iot.vmp.gb28181.bean.Device;
+import com.genersoft.iot.vmp.gb28181.bean.DeviceChannel;
 import com.genersoft.iot.vmp.service.IAccountService;
 import com.genersoft.iot.vmp.service.IDeviceChannelService;
 import com.genersoft.iot.vmp.service.IDeviceService;
 import com.genersoft.iot.vmp.utils.StpUserUtil;
 import com.genersoft.iot.vmp.vmanager.bean.ErrorCode;
 import com.genersoft.iot.vmp.vmanager.bean.WVPResult;
-import com.genersoft.iot.vmp.vmanager.gb28181.platform.bean.ChannelReduce;
 import com.github.pagehelper.PageInfo;
 import io.swagger.v3.oas.annotations.Operation;
 import io.swagger.v3.oas.annotations.Parameter;
 import io.swagger.v3.oas.annotations.tags.Tag;
+import org.apache.commons.compress.utils.IOUtils;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.MediaType;
 import org.springframework.web.bind.annotation.*;
 
+import javax.servlet.http.HttpServletResponse;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Files;
 import java.util.List;
 
 @Tag(name = "普通用户设备管理")
@@ -54,7 +61,7 @@ public class AccountDeviceControl {
                     "设备已经绑定, 无法重新绑定");
         }
         if (accountService.bindDevice(
-                Integer.parseInt(userId),
+                userId,
                 device.getId(),
                 bindCode) < 1) {
             logger.warn("[绑定设备] 绑定失败");
@@ -71,7 +78,7 @@ public class AccountDeviceControl {
     public WVPResult<String> unbindDevice(
             @RequestParam String deviceId) {
         String userId = StpUserUtil.getLoginId().toString();
-        accountService.unBindDevice(Integer.parseInt(userId), Integer.parseInt(deviceId));
+        accountService.unBindDevice(userId, deviceId);
         return WVPResult.success("ok");
     }
 
@@ -85,7 +92,7 @@ public class AccountDeviceControl {
             @RequestParam(value = "l", required = false, defaultValue = "20") int limit) {
         String userId = StpUserUtil.getLoginId().toString();
         PageInfo<Device> pageResult = accountService.getAccountDevices(
-                Integer.parseInt(userId),
+                userId,
                 page,
                 limit);
         return WVPResult.success(
@@ -99,17 +106,55 @@ public class AccountDeviceControl {
     @GetMapping("/channels")
     @Operation(summary = "获取设备通道列表")
     @Parameter(name = "deviceId", description = "设备id", required = true)
-    public WVPResult<List<ChannelReduce>> getChannels(
-            @RequestParam(value = "deviceId", required = true) int deviceId) {
-        int userId = Integer.parseInt(StpUserUtil.getLoginId().toString());
+    public WVPResult<List<DeviceChannel>> getChannels(
+            @RequestParam(value = "deviceId", required = true) String deviceId) {
+        String userId = StpUserUtil.getLoginId().toString();
         // 判断当前用户是否有该设备
         Device device = accountService.getAccountDevice(userId, deviceId);
         if (device == null) {
             return WVPResult.fail(ErrorCode.ERROR403, "当前用户无权限访问此设备");
         }
-        List<ChannelReduce> channels = deviceChannelService.queryAllChannelList(device.getDeviceId());
+        logger.info(" 用户{} 获取设备{}:{} 通道", userId, deviceId, device.getDeviceId());
+        List<DeviceChannel> channels = deviceChannelService.getChannelListByDeviceId(device.getDeviceId());
         return WVPResult.success(channels);
     }
 
-    //
+    @GetMapping("/info")
+    @Operation(summary = "获取设备信息")
+    @Parameter(name = "deviceId", description = "设备id", required = true)
+    public WVPResult<Device> getDeviceInfo(
+            @RequestParam(value = "deviceId", required = true) String deviceId) {
+        logger.info("[web api] /device/info 获取设备信息");
+        String userId = StpUserUtil.getLoginId().toString();
+        // 判断当前用户是否有该设备
+        Device device = accountService.getAccountDevice(userId, deviceId);
+        if (device == null) {
+            return WVPResult.fail(ErrorCode.ERROR403, "当前用户无权限访问此设备");
+        }
+        return WVPResult.success(device);
+    }
+
+
+    // 获取通道截图
+    @GetMapping("/snap")
+    @Operation(summary = "请求截图")
+    @Parameter(name = "deviceId", description = "设备国标编号", required = true)
+    @Parameter(name = "channelId", description = "通道国标编号", required = true)
+    public void getSnap(HttpServletResponse resp, @RequestParam String deviceId, @RequestParam String channelId) {
+        logger.info("[web api] /device/snap?({}, {}) 请求设备截图",
+                deviceId, channelId);
+        String userId = StpUserUtil.getLoginId().toString();
+        // 判断当前用户是否有该设备
+        Device device = accountService.getAccountDevice(userId, deviceId);
+        if (device == null) {
+            resp.setStatus(HttpServletResponse.SC_FORBIDDEN);
+        }
+        try {
+            final InputStream in = Files.newInputStream(new File("snap" + File.separator + device.getDeviceId() + "_" + channelId + ".jpg").toPath());
+            resp.setContentType(MediaType.IMAGE_PNG_VALUE);
+            IOUtils.copy(in, resp.getOutputStream());
+        } catch (IOException e) {
+            resp.setStatus(HttpServletResponse.SC_NOT_FOUND);
+        }
+    }
 }

+ 4 - 1
src/main/java/com/genersoft/iot/vmp/vmanager/bean/ErrorCode.java

@@ -16,7 +16,10 @@ public enum ErrorCode {
     ERR_MEDIA(600, "流媒体服务异常"),
     ERR_NOTFOUND_STREAM(601, "无法找到流媒体"),
     ERR_Invite_fail(602, "流媒体交互异常"),
-    ERROR_Device_Busy(603, "设备繁忙");
+    ERROR_Device_Busy(603, "设备繁忙"),
+    ERROR_Retry(604, "稍后重试"),
+    // 数据异常
+    ERROR_DATA(1000, "数据异常");
 //    void ERROR200(200, "系统异常1");
 
     private final int code;

+ 6 - 0
src/main/java/com/genersoft/iot/vmp/vmanager/bean/ErrorHook.java

@@ -0,0 +1,6 @@
+package com.genersoft.iot.vmp.vmanager.bean;
+
+// 可以触发回调函数
+public interface ErrorHook {
+    void run(WVPResult wvpResult);
+}

+ 0 - 4
src/main/java/com/genersoft/iot/vmp/vmanager/bean/FailResult.java

@@ -1,4 +0,0 @@
-package com.genersoft.iot.vmp.vmanager.bean;
-
-public class FailResult {
-}

+ 16 - 3
src/main/java/com/genersoft/iot/vmp/vmanager/bean/WVPResult.java

@@ -133,12 +133,25 @@ public class WVPResult<T> implements Cloneable{
     }
 
     public void setTotal(int total) {
-        this.total =  total;
+        this.total = total;
     }
-	
-	@Override
+
+    @Override
     public Object clone() throws CloneNotSupportedException {
         return super.clone();
     }
 
+    // toString
+    @Override
+    public String toString() {
+        return "WVPResult{" +
+                "code=" + code +
+                ", msg='" + msg + '\'' +
+                ", data=" + data +
+                ", limit=" + limit +
+                ", page=" + page +
+                ", total=" + total +
+                '}';
+    }
+
 }

+ 34 - 29
src/main/java/com/genersoft/iot/vmp/vmanager/gb28181/device/DeviceQuery.java

@@ -6,6 +6,7 @@ import com.genersoft.iot.vmp.conf.DynamicTask;
 import com.genersoft.iot.vmp.conf.exception.ControllerException;
 import com.genersoft.iot.vmp.conf.security.saToken.SaAdminCheckLogin;
 import com.genersoft.iot.vmp.conf.security.saToken.SaAdminCheckRole;
+import com.genersoft.iot.vmp.conf.security.saToken.SaUserCheckRole;
 import com.genersoft.iot.vmp.gb28181.DeviceShare;
 import com.genersoft.iot.vmp.gb28181.bean.AiConfig;
 import com.genersoft.iot.vmp.gb28181.bean.Device;
@@ -21,6 +22,8 @@ import com.genersoft.iot.vmp.service.IDeviceChannelService;
 import com.genersoft.iot.vmp.service.IDeviceService;
 import com.genersoft.iot.vmp.storager.IRedisCatchStorage;
 import com.genersoft.iot.vmp.storager.IVideoManagerStorage;
+import com.genersoft.iot.vmp.utils.AuthorUtil;
+import com.genersoft.iot.vmp.utils.DeviceHelper;
 import com.genersoft.iot.vmp.utils.StpAdminUtil;
 import com.genersoft.iot.vmp.utils.StpUserUtil;
 import com.genersoft.iot.vmp.vmanager.bean.AiLib;
@@ -31,6 +34,7 @@ import com.github.pagehelper.PageInfo;
 import io.swagger.v3.oas.annotations.Operation;
 import io.swagger.v3.oas.annotations.Parameter;
 import io.swagger.v3.oas.annotations.tags.Tag;
+import jdk.nashorn.internal.ir.annotations.Ignore;
 import org.apache.commons.compress.utils.IOUtils;
 import org.apache.ibatis.annotations.Options;
 import org.slf4j.Logger;
@@ -55,10 +59,11 @@ import java.text.ParseException;
 import java.util.*;
 
 @Tag(name = "国标设备查询", description = "国标设备查询")
-@SuppressWarnings("rawtypes")
-@CrossOrigin
+//@SuppressWarnings("rawtypes")
+@CrossOrigin(origins = "*")
 @RestController
 @RequestMapping("/api/device/query")
+@SaAdminCheckRole("admin")
 public class DeviceQuery {
 
     private final static Logger logger = LoggerFactory.getLogger(DeviceQuery.class);
@@ -87,6 +92,9 @@ public class DeviceQuery {
     @Autowired
     private DeviceShare deviceShare;
 
+    @Autowired
+    private DeviceHelper deviceHelper;
+
     /**
      * 使用ID查询国标设备
      *
@@ -291,11 +299,12 @@ public class DeviceQuery {
     public PageInfo<Device> devices(int page,
                                     int count,
                                     @RequestParam(value = "online", defaultValue = "false", required = false) boolean online) {
-        String adminId = StpAdminUtil.getLoginId().toString();
-        String userId = StpUserUtil.getLoginId().toString();
+        String adminId = AuthorUtil.getAdminId();
+        String userId = AuthorUtil.getUserId();
         logger.info("[设备查询] 查询所有设备 {}", adminId);
+        //
         if (online) {
-            return storager.queryVideoDeviceList(page, count, online);
+            return storager.queryVideoDeviceList(page, count, true);
         }
         return storager.queryVideoDeviceList(page, count, null);
     }
@@ -322,6 +331,7 @@ public class DeviceQuery {
     @Parameter(name = "channelType", description = "设备/子目录-> false/true")
     @Parameter(name = "shareCode", description = "分享码", required = false)
     @Parameter(name = "catalogUnderDevice", description = "是否直属与设备的目录")
+    @Ignore
     public PageInfo<DeviceChannel> channels(
             @PathVariable String deviceId,
             int page, int count,
@@ -334,17 +344,14 @@ public class DeviceQuery {
         if (ObjectUtils.isEmpty(query)) {
             query = null;
         }
-        Device device = storager.queryVideoDevice(deviceId);
-        if(!deviceShare.shareCheck(device, shareCode, false)){
-            // 判断设备分享是否过期,如果过期则自动进行关闭分享
-            if(deviceShare.checkShareCodeIsExpires(device, device.getShareCode()) == 0){
-                // 设备还在分享中, 延长分享时间
-                deviceShare.updateTime(device);
-            }else{
-                deviceShare.closeShare(device);
-            }
+        logger.info("[查询通道] device:{}", deviceId);
+        Device device = deviceHelper.getDevice(deviceId, shareCode);
+        if (device == null) {
+            logger.warn("设备不存在,或者无权限访问. 设备ID:" + deviceId);
+            // 无权限
+            throw new ControllerException(ErrorCode.ERROR403.getCode(), "无法找到设备");
         }
-        return storager.queryChannelsByDeviceId(deviceId, query, channelType, online, catalogUnderDevice, page, count);
+        return storager.queryChannelsByDeviceId(device.getDeviceId(), query, channelType, online, catalogUnderDevice, page, count);
     }
 
     /**
@@ -785,6 +792,7 @@ public class DeviceQuery {
 
     @Operation(summary = "获取分享信息")
     @Parameter(name = "shareCode", description = "分享码", required = true)
+    @Ignore
     @GetMapping("/share/info")
     public WVPResult<Device> getShareInfo(@RequestParam String shareCode) {
         logger.info("获取分享信息: {}", shareCode);
@@ -793,11 +801,7 @@ public class DeviceQuery {
             throw new ControllerException(ErrorCode.ERROR100.getCode(), "无法找到对应的分享设备");
         }
         logger.info("设备 id: {}", device.getDeviceId());
-        WVPResult wvpResult = new WVPResult();
-        wvpResult.setCode(ErrorCode.SUCCESS.getCode());
-        wvpResult.setMsg(ErrorCode.SUCCESS.getMsg());
-        wvpResult.setData(device);
-        return wvpResult;
+        return WVPResult.success(device);
     }
 
     @Operation(summary = "分享设备通道")
@@ -805,10 +809,12 @@ public class DeviceQuery {
     @Parameter(name = "channelId", description = "通道国标编号", required = true)
     @GetMapping("/share/open")
     public WVPResult<String> shareOpen(@RequestParam String deviceId, @RequestParam String channelId) {
-        logger.info("[设备分享] 开启分享: {} {}", deviceId, channelId);
-        Device device = storager.queryVideoDevice(deviceId);
+        logger.info("[开启分享] {} {}", deviceId, channelId);
+        Device device = deviceHelper.getDevice(deviceId, null);
         if (device == null) {
-            throw new ControllerException(ErrorCode.ERROR100.getCode(), "无法找到对应的分享设备");
+            logger.warn("[开启分享] 设备不存在,或者无权限访问. 设备ID:" + deviceId);
+            // 无权限
+            throw new ControllerException(ErrorCode.ERROR403.getCode(), "无法找到设备");
         }
 
         DeviceChannel deviceChannel = storager.queryChannel(deviceId, channelId);
@@ -831,14 +837,13 @@ public class DeviceQuery {
     @GetMapping("/share/close")
     public WVPResult<String> shareClose(@RequestParam String deviceId) {
         logger.info("[设备分享] 关闭分享: {}", deviceId);
-        Device device = storager.queryVideoDevice(deviceId);
+        Device device = deviceHelper.getDevice(deviceId, null);
         if (device == null) {
-            throw new ControllerException(ErrorCode.ERROR100.getCode(), "无法找到设备");
+            logger.warn("[关闭分享] 设备不存在,或者无权限访问. 设备ID:" + deviceId);
+            // 无权限
+            return WVPResult.fail(ErrorCode.ERROR403, "无法找到设备");
         }
         deviceShare.closeShare(device);
-        WVPResult wvpResult = new WVPResult();
-        wvpResult.setCode(ErrorCode.SUCCESS.getCode());
-        wvpResult.setMsg(ErrorCode.SUCCESS.getMsg());
-        return wvpResult;
+        return WVPResult.success();
     }
 }

+ 88 - 137
src/main/java/com/genersoft/iot/vmp/vmanager/gb28181/play/PlayController.java

@@ -19,12 +19,11 @@ import com.genersoft.iot.vmp.gb28181.transmit.callback.RequestMessage;
 import com.genersoft.iot.vmp.gb28181.transmit.cmd.impl.SIPCommander;
 import com.genersoft.iot.vmp.media.zlm.ZLMRESTfulUtils;
 import com.genersoft.iot.vmp.media.zlm.dto.MediaServerItem;
-import com.genersoft.iot.vmp.service.IMediaServerService;
-import com.genersoft.iot.vmp.service.IMediaService;
-import com.genersoft.iot.vmp.service.IPlayService;
-import com.genersoft.iot.vmp.service.IAdminService;
+import com.genersoft.iot.vmp.service.*;
 import com.genersoft.iot.vmp.storager.IRedisCatchStorage;
 import com.genersoft.iot.vmp.storager.IVideoManagerStorage;
+import com.genersoft.iot.vmp.utils.AuthorUtil;
+import com.genersoft.iot.vmp.utils.DeviceHelper;
 import com.genersoft.iot.vmp.utils.Md5Utils;
 import com.genersoft.iot.vmp.vmanager.bean.DeferredResultEx;
 import com.genersoft.iot.vmp.vmanager.bean.ErrorCode;
@@ -50,7 +49,6 @@ import java.util.*;
 @CrossOrigin
 @RestController
 @RequestMapping("/api/play")
-@SaAdminCheckRole("admin")
 public class PlayController {
 
     private final static Logger logger = LoggerFactory.getLogger(PlayController.class);
@@ -89,7 +87,10 @@ public class PlayController {
     private GBEventSubscribe GBHookSubscribe;
 
     @Autowired
-    private IAdminService userService;
+    private IAdminService adminService;
+
+    @Autowired
+    private IAccountService accountService;
 
     @Autowired
     private GBStore gbStore;
@@ -97,6 +98,10 @@ public class PlayController {
     @Autowired
     private DeviceShare deviceShare;
 
+    @Autowired
+    private DeviceHelper deviceHelper;
+
+
     @Operation(summary = "开始点播")
     @Parameter(name = "deviceId", description = "设备国标编号", required = true)
     @Parameter(name = "channelId", description = "通道国标编号", required = true)
@@ -111,82 +116,55 @@ public class PlayController {
             @RequestParam(required = false) String shareCode
     ) {
         logger.info("[视频点播] {}-{}开始点播视频", deviceId, channelId);
-        // 获取可用的zlm
-        Device device = storager.queryVideoDevice(deviceId);
-        // 数据校验
-        if(!deviceShare.shareCheck(device, shareCode, false)){
-            // 判断设备分享是否过期,如果过期则自动进行关闭分享
-            if(deviceShare.checkShareCodeIsExpires(device, device.getShareCode()) == 0){
-                // 设备还在分享中, 延长分享时间
-                deviceShare.updateTime(device);
-            }else{
-                deviceShare.closeShare(device);
-            }
+        // 判断用户权限
+
+        // 只要有一个权限即可
+        DeferredResult<WVPResult<StreamContent>> result = new DeferredResult<>(userSetting.getPlayTimeout().longValue());
+        DeferredResultEx<WVPResult<StreamContent>> deferredResultEx = new DeferredResultEx<>(result);
+
+        Device device = deviceHelper.getDevice(deviceId, shareCode);
+        String errorText = "";
+        if (device == null) {
+            errorText = "无法操控此设备";
+            logger.warn(errorText);
+            result.setResult(WVPResult.fail(ErrorCode.ERROR403, errorText));
+            return result;
         }
-        MediaServerItem newMediaServerItem = playService.getNewMediaServerItem(device);
 
-        RequestMessage msg = new RequestMessage();
+        deviceId = device.getDeviceId();
+
+        String uuid = UUID.randomUUID().toString();
         String key = DeferredResultHolder.CALLBACK_CMD_PLAY + deviceId + channelId;
+        RequestMessage msg = new RequestMessage(key, uuid);
         boolean exist = resultHolder.exist(key, null);
-        msg.setKey(key);
-        String uuid = UUID.randomUUID().toString();
-        msg.setId(uuid);
-        DeferredResult<WVPResult<StreamContent>> result = new DeferredResult<>(userSetting.getPlayTimeout().longValue());
-        DeferredResultEx<WVPResult<StreamContent>> deferredResultEx = new DeferredResultEx<>(result);
+        resultHolder.put(key, uuid, deferredResultEx);
 
+        // 获取流媒体服务器
+        MediaServerItem newMediaServerItem = playService.getNewMediaServerItem(device);
+        if (newMediaServerItem == null) {
+            logger.warn("无法找到可用的流媒体服务器");
+            msg.setData(WVPResult.fail(ErrorCode.ERR_MEDIA, "无法找到可用的流媒体服务器"));
+            resultHolder.invokeResult(msg);
+        }
+        if (exist) {
+            logger.warn("当前设备通道在推流中");
+            msg.setData(WVPResult.fail(ErrorCode.ERROR100, "当前设备通道在推流中,请先停止(如遇非正常停止,请等待一分钟后重试)"));
+            resultHolder.invokeResult(msg);
+        }
         result.onTimeout(() -> {
             logger.info("点播接口等待超时");
-            // 释放rtpserver
-            WVPResult<StreamInfo> wvpResult = new WVPResult<>();
-            wvpResult.setCode(ErrorCode.ERROR100.getCode());
-            wvpResult.setMsg("点播超时");
-            msg.setData(wvpResult);
+            msg.setData(WVPResult.fail(ErrorCode.ERROR100, "点播接口等待超时"));
             resultHolder.invokeResult(msg);
-            // 强行移除 resultHolder 中的 DeferredResult
-        });
-        // TODO 在点播未成功的情况下在此调用接口点播会导致返回的流地址ip错误
-        deferredResultEx.setFilter(result1 -> {
-            logger.info("点播成功");
-
-            WVPResult<StreamInfo> wvpResult1 = (WVPResult<StreamInfo>) result1;
-            WVPResult<StreamContent> resultStream = new WVPResult<>();
-            resultStream.setCode(wvpResult1.getCode());
-            resultStream.setMsg(wvpResult1.getMsg());
-            if (wvpResult1.getCode() == ErrorCode.SUCCESS.getCode()) {
-                StreamInfo data = wvpResult1.getData().clone();
-                if (userSetting.getUseSourceIpAsStreamIp()) {
-                    data.channgeStreamIp(request.getLocalName());
-                }
-                resultStream.setData(new StreamContent(wvpResult1.getData()));
-            }
-//			resultHolder.invokeResult(msg);
-            return resultStream;
+            // 向设备发送 bye
+            playService.stopPlay(device, channelId);
         });
 
 
-        // 录像查询以channelId作为deviceId查询
-        resultHolder.put(key, uuid, deferredResultEx);
-
-        if (!exist) {
-            playService.play(newMediaServerItem, deviceId, channelId, isUsePs, null, null, null);
-        } else {
-            logger.info("当前设备通道在推流中");
-            // 释放rtpserver
-            WVPResult<StreamInfo> wvpResult = new WVPResult<>();
-            wvpResult.setCode(ErrorCode.ERROR100.getCode());
-            wvpResult.setMsg("当前设备通道在推流中,请先停止(如遇非正常停止,请等待一分钟后重试)");
-            msg.setData(wvpResult);
-            resultHolder.invokeResult(msg);
-            try {
-                resultHolder.invokeAllResult(msg);
-            } catch (Exception e) {
-                logger.error("关闭所有视频通道失败");
-            }
-
-        }
+        playService.startPlay(msg, newMediaServerItem, device, channelId, isUsePs);
         return result;
     }
 
+
     @Operation(summary = "停止点播")
     @Parameter(name = "deviceId", description = "设备国标编号", required = true)
     @Parameter(name = "channelId", description = "通道国标编号", required = true)
@@ -199,23 +177,13 @@ public class PlayController {
 
         logger.debug(String.format("设备预览/回放停止API调用,streamId:%s_%s", deviceId, channelId));
 
+        Device device = deviceHelper.getDevice(deviceId, shareCode);
 
-        if (deviceId == null || channelId == null) {
+        if (device == null || channelId == null) {
+            logger.warn("设备预览/回放停止API调用失败,设备:{} 或通道:{} 不存在", deviceId, channelId);
             throw new ControllerException(ErrorCode.ERROR400);
         }
 
-        Device device = storager.queryVideoDevice(deviceId);
-
-        // 数据校验
-        if(!deviceShare.shareCheck(device, shareCode, false)){
-            // 判断设备分享是否过期,如果过期则自动进行关闭分享
-            if(deviceShare.checkShareCodeIsExpires(device, device.getShareCode()) == 0){
-                // 设备还在分享中, 延长分享时间
-                deviceShare.updateTime(device);
-            }else{
-                deviceShare.closeShare(device);
-            }
-        }
 
         StreamInfo streamInfo = redisCatchStorage.queryPlayByDevice(deviceId, channelId);
         if (streamInfo == null) {
@@ -239,6 +207,7 @@ public class PlayController {
 
     }
 
+
     /**
      * 将不是h264的视频通过ffmpeg 转码为h264 + aac
      *
@@ -332,51 +301,41 @@ public class PlayController {
                                             defaultValue = "5000") int waitTimeStr,
                                     @RequestParam(required = false) String shareCode) {
 
-        WVPResult wvpResult = new WVPResult();
+        logger.info("[语音广播 step1] 尝试开启语音对讲");
 
-        Device device = storager.queryVideoDevice(deviceId);
-        // 数据校验
-        if(!deviceShare.shareCheck(device, shareCode, false)){
-            // 判断设备分享是否过期,如果过期则自动进行关闭分享
-            if(deviceShare.checkShareCodeIsExpires(device, device.getShareCode()) == 0){
-                // 设备还在分享中, 延长分享时间
-                deviceShare.updateTime(device);
-            }else{
-                deviceShare.closeShare(device);
-            }
+        Device device = deviceHelper.getDevice(deviceId, shareCode);
+
+        if (device == null || channelId == null) {
+            logger.warn("[语音广播 step1] 设备:{} 或通道:{} 不存在", deviceId, channelId);
+            return WVPResult.fail(ErrorCode.ERROR400, "设备或通道不存在");
         }
+
         MediaServerItem mediaServerItem = playService.getNewMediaServerItem(device);
 
         // 流媒体服务异常处理
         if (mediaServerItem == null) {
-            wvpResult.setCode(ErrorCode.ERR_MEDIA.getCode());
-            wvpResult.setMsg("暂时无法找到可用的zlm流媒体服务");
-            return wvpResult;
+            logger.error("[语音广播 step1] 无法连接至ZLM服务器");
+            return WVPResult.fail(ErrorCode.ERR_MEDIA, "暂时无法找到可用的zlm流媒体服务");
         }
 
         String app = "audio";
         String stream = "rtc_" + deviceId + "_" + channelId;
         String type = "push";
-        Map<String, Object> resultData = new HashMap<>(16);
-        LoginUser userInfo = SecurityUtils.getUserInfo();
-        String sign = Md5Utils.hash(userService.getUserByUsername(userInfo.getUsername()).getPushKey()); //获取推流鉴权密钥
+
+//        LoginUser userInfo = SecurityUtils.getUserInfo();
+        // todo 生成设备推流鉴权密钥, 无论是什么用户登录都可以正常使用
+        String sign = Md5Utils.hash(adminService.getUserById("1").getPushKey()); //获取推流鉴权密钥
         String webRtcPushUrl = String.format("https://%s:%s/index/api/webrtc?app=%s&stream=%s&type=%s&sign=%s", mediaServerItem.getIp(), mediaServerItem.getHttpSSlPort(), app, stream, type, sign);
 
         //首先判断设备是否正在对讲
         if (redisCatchStorage.isBroadcastItem(deviceId)) {
             // 设备正在进行语音对讲
-            wvpResult.setCode(ErrorCode.ERROR_Device_Busy.getCode());
-            wvpResult.setMsg(ErrorCode.ERROR_Device_Busy.getMsg());
-            return wvpResult;
-        }
-
-        if (mediaServerItem == null) {
-            logger.error("流媒体未找到");
-            wvpResult.setCode(ErrorCode.ERR_MEDIA.getCode());
-            wvpResult.setMsg(ErrorCode.ERR_MEDIA.getMsg());
-            return wvpResult;
+            logger.error("[语音广播 step1] 设备正在进行语音对讲");
+            return WVPResult.fail(ErrorCode.ERROR_Device_Busy, "设备正在进行语音对讲");
         }
 
+        // 构建返回数据
+        Map<String, Object> resultData = new HashMap<>(16);
         resultData.put("mediaId", mediaServerItem.getId());
         resultData.put("app", app);
         resultData.put("stream", stream);
@@ -384,10 +343,8 @@ public class PlayController {
         resultData.put("sign", sign);
         resultData.put("webRtcPushUrl", webRtcPushUrl);
         resultData.put("audioEncodePt", device.getAudioEncodePt());
-        wvpResult.setCode(ErrorCode.SUCCESS.getCode());
-        wvpResult.setMsg(ErrorCode.SUCCESS.getMsg());
-        wvpResult.setData(resultData);
-        return wvpResult;
+
+        return WVPResult.success(resultData);
     }
 
     @Operation(summary = "开始建立语音广播连接")
@@ -406,20 +363,19 @@ public class PlayController {
             @RequestParam(value = "stream") String stream,
             @RequestParam(required = false) String shareCode
     ) {
-        logger.info("[语音对讲] web端已经开启推流");
-
-        // 检查设备是否存在
-        Device device = storager.queryVideoDevice(deviceId);
-        // 数据校验
-        if(!deviceShare.shareCheck(device, shareCode, false)){
-            // 判断设备分享是否过期,如果过期则自动进行关闭分享
-            if(deviceShare.checkShareCodeIsExpires(device, device.getShareCode()) == 0){
-                // 设备还在分享中, 延长分享时间
-                deviceShare.updateTime(device);
-            }else{
-                deviceShare.closeShare(device);
-            }
+        logger.info("[语音对讲 step2] 音频流已经建立 app:{} stream:{}", app, stream);
+
+        // 检查是否有权限调用此设备
+        Device device = deviceHelper.getDevice(deviceId, shareCode);
+        if (device == null) {
+            logger.warn("[语音对讲 step2] 无权限 deviceId:{}", deviceId);
+            DeferredResult<WVPResult<String>> deferredResult = new DeferredResult<>();
+            deferredResult.setResult(
+                    WVPResult.fail(ErrorCode.ERROR400, "接口调用失败, 没有权限或者无法找到设备")
+            );
+            return deferredResult;
         }
+
         RequestMessage msg = new RequestMessage();
         // 返回invite信息给设备
         String key = DeferredResultHolder.CALLBACK_CMD_BROADCAST_INVITE + deviceId;
@@ -528,17 +484,14 @@ public class PlayController {
             @RequestParam(required = false) String shareCode) {
 
         WVPResult wvpResult = new WVPResult();
-        Device device = storager.queryVideoDevice(deviceId);
-        // 数据校验
-        if(!deviceShare.shareCheck(device, shareCode, false)){
-            // 判断设备分享是否过期,如果过期则自动进行关闭分享
-            if(deviceShare.checkShareCodeIsExpires(device, device.getShareCode()) == 0){
-                // 设备还在分享中, 延长分享时间
-                deviceShare.updateTime(device);
-            }else{
-                deviceShare.closeShare(device);
-            }
+        // 检查是否有权限调用此设备
+        Device device = deviceHelper.getDevice(deviceId, shareCode);
+        if (device == null) {
+            logger.warn("[语音对讲 step2] 无权限 deviceId:{} channelId:{}", deviceId, channelId);
+            // 检查是否有权限调用此设备
+            return WVPResult.fail(ErrorCode.ERROR400, "接口调用失败, 没有权限或者无法找到设备");
         }
+
         // 停止音频流,给设备发送bye
         try {
             logger.warn("[停止点播] {}/{}", deviceId, channelId);
@@ -559,8 +512,6 @@ public class PlayController {
             // 清除redis中的语音广播信息
             logger.error("[停止对讲] 移除语音广播缓存信息");
             gbStore.delBroadcastStore("broadcast_" + deviceId);
-            logger.error("[停止对讲] 移除语音广播redis缓存信息");
-            redisCatchStorage.deleteBroadcastItem(deviceId);
         }
         return wvpResult;
     }

+ 75 - 54
src/main/java/com/genersoft/iot/vmp/vmanager/gb28181/playback/PlaybackController.java

@@ -11,6 +11,8 @@ import com.genersoft.iot.vmp.gb28181.transmit.callback.RequestMessage;
 import com.genersoft.iot.vmp.media.zlm.ZLMRTPServerFactory;
 import com.genersoft.iot.vmp.storager.IRedisCatchStorage;
 import com.genersoft.iot.vmp.service.IPlayService;
+import com.genersoft.iot.vmp.utils.DeviceHelper;
+import com.genersoft.iot.vmp.vmanager.bean.DeferredResultEx;
 import com.genersoft.iot.vmp.vmanager.bean.ErrorCode;
 import com.genersoft.iot.vmp.vmanager.bean.StreamContent;
 import com.genersoft.iot.vmp.vmanager.bean.WVPResult;
@@ -44,7 +46,6 @@ import java.util.UUID;
 @CrossOrigin
 @RestController
 @RequestMapping("/api/playback")
-@SaAdminCheckRole("admin")
 public class PlaybackController {
 
 	private final static Logger logger = LoggerFactory.getLogger(PlaybackController.class);
@@ -61,37 +62,50 @@ public class PlaybackController {
 	@Autowired
 	private IRedisCatchStorage redisCatchStorage;
 
-	@Autowired
-	private IPlayService playService;
-
-	@Autowired
-	private DeferredResultHolder resultHolder;
-
-	@Autowired
-	private UserSetting userSetting;
-
-	@Operation(summary = "开始视频回放")
-	@Parameter(name = "deviceId", description = "设备国标编号", required = true)
-	@Parameter(name = "channelId", description = "通道国标编号", required = true)
-	@Parameter(name = "startTime", description = "开始时间", required = true)
-	@Parameter(name = "endTime", description = "结束时间", required = true)
-	@GetMapping("/start/{deviceId}/{channelId}")
-	public DeferredResult<WVPResult<StreamContent>> start(@PathVariable String deviceId, @PathVariable String channelId,
-														 String startTime, String endTime) {
-
-		if (logger.isDebugEnabled()) {
-			logger.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);
-
-		WVPResult<StreamContent> wvpResult = new WVPResult<>();
-
-		RequestMessage msg = new RequestMessage();
-		msg.setKey(key);
+    @Autowired
+    private IPlayService playService;
+
+    @Autowired
+    private DeferredResultHolder resultHolder;
+
+    @Autowired
+    private UserSetting userSetting;
+
+    @Autowired
+    private DeviceHelper deviceHelper; // 注入DeviceHelper
+
+
+    @Operation(summary = "开始视频回放")
+    @Parameter(name = "deviceId", description = "设备国标编号", required = true)
+    @Parameter(name = "channelId", description = "通道国标编号", required = true)
+    @Parameter(name = "startTime", description = "开始时间", required = true)
+    @Parameter(name = "endTime", description = "结束时间", required = true)
+    @GetMapping("/start/{deviceId}/{channelId}")
+    public DeferredResult<WVPResult<StreamContent>> start(@PathVariable String deviceId, @PathVariable String channelId,
+                                                          String startTime, String endTime) {
+
+        if (logger.isDebugEnabled()) {
+            logger.debug(String.format("设备回放 API调用,deviceId:%s ,channelId:%s", deviceId, channelId));
+        }
+        logger.warn("[录像回放] deviceId:{} channelId:{} {}-{}", deviceId, channelId, startTime, endTime);
+        Device device = deviceHelper.getDevice(deviceId, null);
+        if (device == null) {
+            logger.warn("[录像回放] 无权限 deviceId:{} channelId:{}", deviceId, channelId);
+            // 检查是否有权限调用此设备
+            DeferredResult<WVPResult<StreamContent>> result = new DeferredResult<>(userSetting.getPlayTimeout().longValue());
+            result.setResult(WVPResult.fail(ErrorCode.ERROR400, "接口调用失败, 没有权限或者无法找到设备"));
+            return result;
+        }
+
+        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);
+
+        WVPResult<StreamContent> wvpResult = new WVPResult<>();
+
+        RequestMessage msg = new RequestMessage();
+        msg.setKey(key);
 		msg.setId(uuid);
 
 		playService.playBack(deviceId, channelId, startTime, endTime, null,
@@ -110,28 +124,35 @@ public class PlaybackController {
 	}
 
 
-	@Operation(summary = "停止视频回放")
-	@Parameter(name = "deviceId", description = "设备国标编号", required = true)
-	@Parameter(name = "channelId", description = "通道国标编号", required = true)
-	@Parameter(name = "stream", description = "流ID", required = true)
-	@GetMapping("/stop/{deviceId}/{channelId}/{stream}")
-	public void 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 = storager.queryVideoDevice(deviceId);
-		if (device == null) {
-			throw new ControllerException(ErrorCode.ERROR400.getCode(), "设备:" + deviceId + " 未找到");
-		}
-		try {
-			cmder.streamByeCmd(device, channelId, stream, null);
-		} catch (InvalidArgumentException | ParseException | SipException | SsrcTransactionNotFoundException e) {
-			throw new ControllerException(ErrorCode.ERROR100.getCode(), "发送bye失败: " + e.getMessage());
-		}
-	}
+    @Operation(summary = "停止视频回放")
+    @Parameter(name = "deviceId", description = "设备国标编号", required = true)
+    @Parameter(name = "channelId", description = "通道国标编号", required = true)
+    @Parameter(name = "stream", description = "流ID", required = true)
+    @GetMapping("/stop/{deviceId}/{channelId}/{stream}")
+    public WVPResult playStop(
+            @PathVariable String deviceId,
+            @PathVariable String channelId,
+            @PathVariable String stream) {
+
+        if (ObjectUtils.isEmpty(deviceId) || ObjectUtils.isEmpty(channelId) || ObjectUtils.isEmpty(stream)) {
+            return WVPResult.fail(ErrorCode.ERROR400, "参数异常");
+        }
+
+        Device device = deviceHelper.getDevice(deviceId, null);
+        if (device == null) {
+            logger.warn("[停止视频回放] 无权限 deviceId:{} channelId:{}", deviceId, channelId);
+            // 检查是否有权限调用此设备
+            DeferredResult<WVPResult<StreamContent>> result = new DeferredResult<>(userSetting.getPlayTimeout().longValue());
+            return WVPResult.fail(ErrorCode.ERROR403, "接口调用失败, 没有权限或者无法找到设备");
+        }
+        try {
+            cmder.streamByeCmd(device, channelId, stream, null);
+        } catch (InvalidArgumentException | ParseException | SipException | SsrcTransactionNotFoundException e) {
+            logger.warn("[停止视频回放] 停止视频回放失败 {}", e.getMessage());
+            return WVPResult.fail(ErrorCode.ERROR100, e.getMessage());
+        }
+        return WVPResult.success();
+    }
 
 
 	@Operation(summary = "回放暂停")

+ 109 - 132
src/main/java/com/genersoft/iot/vmp/vmanager/gb28181/ptz/PtzController.java

@@ -10,6 +10,7 @@ import com.genersoft.iot.vmp.gb28181.transmit.callback.DeferredResultHolder;
 import com.genersoft.iot.vmp.gb28181.transmit.callback.RequestMessage;
 import com.genersoft.iot.vmp.gb28181.transmit.cmd.impl.SIPCommander;
 import com.genersoft.iot.vmp.storager.IVideoManagerStorage;
+import com.genersoft.iot.vmp.utils.DeviceHelper;
 import com.genersoft.iot.vmp.vmanager.bean.ErrorCode;
 import com.genersoft.iot.vmp.vmanager.bean.WVPResult;
 import io.swagger.v3.oas.annotations.Operation;
@@ -33,7 +34,6 @@ import java.util.UUID;
 @CrossOrigin
 @RestController
 @RequestMapping("/api/ptz")
-@SaAdminCheckRole("admin")
 public class PtzController {
 
 	private final static Logger logger = LoggerFactory.getLogger(PtzController.class);
@@ -41,24 +41,27 @@ public class PtzController {
 	@Autowired
 	private SIPCommander cmder;
 
-	@Autowired
-	private IVideoManagerStorage storager;
+    @Autowired
+    private IVideoManagerStorage storager;
 
-	@Autowired
-	private DeferredResultHolder resultHolder;
+    @Autowired
+    private DeferredResultHolder resultHolder;
 
-	@Autowired
-	private DeviceShare deviceShare;
+    @Autowired
+    private DeviceShare deviceShare;
 
-	/***
-	 * 云台控制
-	 * @param deviceId 设备id
-	 * @param channelId 通道id
-	 * @param command	控制指令
-	 * @param horizonSpeed	水平移动速度
-	 * @param verticalSpeed	垂直移动速度
-	 * @param zoomSpeed	    缩放速度
-	 */
+    @Autowired
+    private DeviceHelper deviceHelper;
+
+    /***
+     * 云台控制
+     * @param deviceId 设备id
+     * @param channelId 通道id
+     * @param command    控制指令
+     * @param horizonSpeed    水平移动速度
+     * @param verticalSpeed    垂直移动速度
+     * @param zoomSpeed        缩放速度
+     */
 
 	@Operation(summary = "云台控制")
 	@Parameter(name = "deviceId", description = "设备国标编号", required = true)
@@ -75,31 +78,26 @@ public class PtzController {
 					int horizonSpeed,
 					int verticalSpeed,
 					int zoomSpeed,
-					@RequestParam(required = false) String shareCode){
+					@RequestParam(required = false) String shareCode) {
 
-		if (logger.isDebugEnabled()) {
-			logger.debug(String.format("设备云台控制 API调用,deviceId:%s ,channelId:%s ,command:%s ,horizonSpeed:%d ,verticalSpeed:%d ,zoomSpeed:%d",deviceId, channelId, command, horizonSpeed, verticalSpeed, zoomSpeed));
-		}
-		Device device = storager.queryVideoDevice(deviceId);
-		// 数据校验
-		if(!deviceShare.shareCheck(device, shareCode, false)){
-			// 判断设备分享是否过期,如果过期则自动进行关闭分享
-			if(deviceShare.checkShareCodeIsExpires(device, device.getShareCode()) == 0){
-				// 设备还在分享中, 延长分享时间
-				deviceShare.updateTime(device);
-			}else{
-				deviceShare.closeShare(device);
-			}
-		}
-		int cmdCode = 0;
-		switch (command){
-			case "left":
-				cmdCode = 2;
-				break;
-			case "right":
-				cmdCode = 1;
-				break;
-			case "up":
+
+        logger.info("[云台控制] deviceId:{} ,channelId:{} ,command:{} ,horizonSpeed:{} ,verticalSpeed:{} ,zoomSpeed:{} ", deviceId, channelId, command, horizonSpeed, verticalSpeed, zoomSpeed);
+        Device device = deviceHelper.getDevice(deviceId, shareCode);
+        if (device == null) {
+            logger.warn("[云台控制] 设备不存在,设备ID:" + deviceId);
+            // 无权限
+            throw new ControllerException(ErrorCode.ERROR100.getCode(), "无法找到设备");
+        }
+
+        int cmdCode = 0;
+        switch (command) {
+            case "left":
+                cmdCode = 2;
+                break;
+            case "right":
+                cmdCode = 1;
+                break;
+            case "up":
 				cmdCode = 8;
 				break;
 			case "down":
@@ -149,31 +147,24 @@ public class PtzController {
 					   String c,
 					   String step,
 					   @RequestParam(required = false) String shareCode
-					   ){
-		if (logger.isDebugEnabled()) {
-			logger.debug(String.format("设备云台控制 API调用,deviceId:%s ,channelId:%s ,c:%s ,speed:%d ",deviceId, channelId, c, step));
-		}
-		logger.info("设备云台控制 API调用,deviceId:{} ,channelId:{} ,command:{} ,speed:{} ",deviceId, channelId, c, step);
-		Device device = storager.queryVideoDevice(deviceId);
-		// 数据校验
-		if(!deviceShare.shareCheck(device, shareCode, false)){
-			// 判断设备分享是否过期,如果过期则自动进行关闭分享
-			if(deviceShare.checkShareCodeIsExpires(device, device.getShareCode()) == 0){
-				// 设备还在分享中, 延长分享时间
-				deviceShare.updateTime(device);
-			}else{
-				deviceShare.closeShare(device);
-			}
-		}
-		int cmdCode = 0;
-		switch (c){
-			case "left":
-				cmdCode = 3;
-				break;
-			case "right":
-				cmdCode = 4;
-				break;
-			case "up":
+					   ) {
+        logger.info("设备云台控制 API调用,deviceId:{} ,channelId:{} ,command:{} ,speed:{} ", deviceId, channelId, c, step);
+        Device device = deviceHelper.getDevice(deviceId, shareCode);
+        if (device == null) {
+            logger.warn("设备不存在,设备ID:" + deviceId);
+            // 无权限
+            throw new ControllerException(ErrorCode.ERROR100.getCode(), "无法找到设备");
+        }
+
+        int cmdCode = 0;
+        switch (c) {
+            case "left":
+                cmdCode = 3;
+                break;
+            case "right":
+                cmdCode = 4;
+                break;
+            case "up":
 				cmdCode = 1;
 				break;
 			case "down":
@@ -207,29 +198,24 @@ public class PtzController {
 	public void focus(@PathVariable String deviceId,
 					  @PathVariable String channelId,
 					  @RequestParam(required = false) String shareCode
-					  ){
-		if (logger.isDebugEnabled()) {
-			logger.debug(String.format("设备云台控制聚焦 API调用,deviceId:%s ,channelId:%s ",deviceId, channelId));
-		}
-		logger.info("控制设备  {} - {} 通道聚焦",deviceId, channelId);
-		Device device = storager.queryVideoDevice(deviceId);
-		// 数据校验
-		if(!deviceShare.shareCheck(device, shareCode, false)){
-			// 判断设备分享是否过期,如果过期则自动进行关闭分享
-			if(deviceShare.checkShareCodeIsExpires(device, device.getShareCode()) == 0){
-				// 设备还在分享中, 延长分享时间
-				deviceShare.updateTime(device);
-			}else{
-				deviceShare.closeShare(device);
-			}
-		}
-		try {
-			cmder.ptzFocus(device, channelId);
-		} catch (SipException | InvalidArgumentException | ParseException e) {
-			logger.error("[命令发送失败] 云台控制: {}", e.getMessage());
-			throw new ControllerException(ErrorCode.ERROR100.getCode(), "命令发送失败: " + e.getMessage());
-		}
-	}
+					  ) {
+        if (logger.isDebugEnabled()) {
+            logger.debug(String.format("设备云台控制聚焦 API调用,deviceId:%s ,channelId:%s ", deviceId, channelId));
+        }
+        logger.info("控制设备  {} - {} 通道聚焦", deviceId, channelId);
+        Device device = deviceHelper.getDevice(deviceId, shareCode);
+        if (device == null) {
+            logger.warn("设备不存在,设备ID:" + deviceId);
+            // 无权限
+            throw new ControllerException(ErrorCode.ERROR100.getCode(), "无法找到设备");
+        }
+        try {
+            cmder.ptzFocus(device, channelId);
+        } catch (SipException | InvalidArgumentException | ParseException e) {
+            logger.error("[命令发送失败] 云台控制: {}", e.getMessage());
+            throw new ControllerException(ErrorCode.ERROR100.getCode(), "命令发送失败: " + e.getMessage());
+        }
+    }
 
 	@Operation(summary = "通用前端控制命令")
 	@Parameter(name = "deviceId", description = "设备国标编号", required = true)
@@ -247,29 +233,25 @@ public class PtzController {
 								int parameter2,
 								int combindCode2,
 								@RequestParam(required = false) String shareCode
-								){
+								) {
 
-		if (logger.isDebugEnabled()) {
-			logger.debug(String.format("设备云台控制 API调用,deviceId:%s ,channelId:%s ,cmdCode:%d parameter1:%d parameter2:%d",deviceId, channelId, cmdCode, parameter1, parameter2));
-		}
-		Device device = storager.queryVideoDevice(deviceId);
-		// 数据校验
-		if(!deviceShare.shareCheck(device, shareCode, false)){
-			// 判断设备分享是否过期,如果过期则自动进行关闭分享
-			if(deviceShare.checkShareCodeIsExpires(device, device.getShareCode()) == 0){
-				// 设备还在分享中, 延长分享时间
-				deviceShare.updateTime(device);
-			}else{
-				deviceShare.closeShare(device);
-			}
-		}
-		try {
-			cmder.frontEndCmd(device, channelId, cmdCode, parameter1, parameter2, combindCode2);
-		} catch (SipException | InvalidArgumentException | ParseException e) {
-			logger.error("[命令发送失败] 前端控制: {}", e.getMessage());
-			throw new ControllerException(ErrorCode.ERROR100.getCode(), "命令发送失败: " + e.getMessage());
-		}
-	}
+        if (logger.isDebugEnabled()) {
+            logger.debug(String.format("设备云台控制 API调用,deviceId:%s ,channelId:%s ,cmdCode:%d parameter1:%d parameter2:%d", deviceId, channelId, cmdCode, parameter1, parameter2));
+        }
+        Device device = deviceHelper.getDevice(deviceId, shareCode);
+        if (device == null) {
+            logger.warn("设备不存在,设备ID:" + deviceId);
+            // 无权限
+            throw new ControllerException(ErrorCode.ERROR100.getCode(), "无法找到设备");
+        }
+
+        try {
+            cmder.frontEndCmd(device, channelId, cmdCode, parameter1, parameter2, combindCode2);
+        } catch (SipException | InvalidArgumentException | ParseException e) {
+            logger.error("[命令发送失败] 前端控制: {}", e.getMessage());
+            throw new ControllerException(ErrorCode.ERROR100.getCode(), "命令发送失败: " + e.getMessage());
+        }
+    }
 
 
 	@Operation(summary = "预置位查询")
@@ -282,32 +264,27 @@ public class PtzController {
 			@PathVariable String channelId,
 			@RequestParam(required = false) String shareCode
 			) {
-		if (logger.isDebugEnabled()) {
-			logger.debug("设备预置位查询API调用");
-		}
-		logger.info("[预置位查询] 获取设备预置位信息");
-		Device device = storager.queryVideoDevice(deviceId);
+        logger.info("[预置位查询] 获取设备预置位信息");
 
-		// 分享码数据校验
-		if(!deviceShare.shareCheck(device, shareCode, false)){
-			// 判断设备分享是否过期,如果过期则自动进行关闭分享
-			if(deviceShare.checkShareCodeIsExpires(device, device.getShareCode()) == 0){
-				// 设备还在分享中, 延长分享时间
-				deviceShare.updateTime(device);
-			}else{
-				deviceShare.closeShare(device);
-			}
-		}
-		String uuid =  UUID.randomUUID().toString();
-		// 此处一开始使用的是channelId,但是发现有些设备没有channelId,所以改成deviceId
-		String key =  DeferredResultHolder.CALLBACK_CMD_PRESETQUERY + deviceId;
-		DeferredResult<WVPResult<List<PresetQuerySipReq>>> result = new DeferredResult<WVPResult<List<PresetQuerySipReq>>> (15 * 1000L);
-		WVPResult wvpResult = new WVPResult();
+        Device device = deviceHelper.getDevice(deviceId, shareCode);
+        DeferredResult<WVPResult<List<PresetQuerySipReq>>> result = new DeferredResult<WVPResult<List<PresetQuerySipReq>>>(15 * 1000L);
+
+        if (device == null) {
+            logger.warn("设备不存在,设备ID:" + deviceId);
+            // 无权限
+            result.setResult(WVPResult.fail(ErrorCode.ERROR100, "无法找到设备"));
+            return result;
+        }
+
+        String uuid = UUID.randomUUID().toString();
+        // 此处一开始使用的是channelId,但是发现有些设备没有channelId,所以改成deviceId
+        String key = DeferredResultHolder.CALLBACK_CMD_PRESETQUERY + deviceId;
 
+        WVPResult wvpResult = new WVPResult();
 
 
-		result.onTimeout(()->{
-			logger.warn(String.format("获取设备预置位超时"));
+        result.onTimeout(() -> {
+            logger.warn(String.format("获取设备预置位超时"));
 			// 释放rtpserver
 			RequestMessage msg = new RequestMessage();
 			msg.setId(uuid);

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

@@ -10,6 +10,7 @@ import com.genersoft.iot.vmp.service.IDeviceService;
 import com.genersoft.iot.vmp.service.IMediaServerService;
 import com.genersoft.iot.vmp.service.IPlayService;
 import com.genersoft.iot.vmp.utils.DateUtil;
+import com.genersoft.iot.vmp.utils.DeviceHelper;
 import com.genersoft.iot.vmp.vmanager.bean.ErrorCode;
 import com.genersoft.iot.vmp.vmanager.bean.StreamContent;
 import com.genersoft.iot.vmp.vmanager.bean.WVPResult;
@@ -42,7 +43,6 @@ import java.util.UUID;
 @CrossOrigin
 @RestController
 @RequestMapping("/api/gb_record")
-@SaAdminCheckRole("admin")
 public class GBRecordController {
 
 	private final static Logger logger = LoggerFactory.getLogger(GBRecordController.class);
@@ -53,39 +53,50 @@ public class GBRecordController {
 	@Autowired
 	private IVideoManagerStorage storager;
 
-	@Autowired
-	private DeferredResultHolder resultHolder;
-
-	@Autowired
-	private IPlayService playService;
-
-	@Autowired
-	private IDeviceService deviceService;
-
+    @Autowired
+    private DeferredResultHolder resultHolder;
+
+    @Autowired
+    private IPlayService playService;
+
+    @Autowired
+    private IDeviceService deviceService;
+
+    @Autowired
+    private DeviceHelper deviceHelper;
+
+
+    @Operation(summary = "录像查询")
+    @Parameter(name = "deviceId", description = "设备国标编号", required = true)
+    @Parameter(name = "channelId", description = "通道国标编号", required = true)
+    @Parameter(name = "startTime", description = "开始时间", required = true)
+    @Parameter(name = "endTime", description = "结束时间", required = true)
+    @GetMapping("/query/{deviceId}/{channelId}")
+    public DeferredResult<WVPResult<RecordInfo>> recordinfo(@PathVariable String deviceId, @PathVariable String channelId, String startTime, String endTime) {
+
+        logger.info("[设备录像查询] {}-{}", deviceId, channelId);
+        Device device = deviceHelper.getDevice(deviceId, null);
+        DeferredResult<WVPResult<RecordInfo>> result = new DeferredResult<>();
+        if (device == null) {
+            // 无权限
+            result.setResult(WVPResult.fail(ErrorCode.ERROR403, "无法操控此设备"));
+            return result;
+        }
+
+        // 时间转换
+        if (!DateUtil.verification(startTime, DateUtil.formatter)) {
+            result.setResult(
+                    WVPResult.fail(ErrorCode.ERROR100, "startTime error, format is " + DateUtil.PATTERN)
+            );
+            return result;
+        }
+        if (!DateUtil.verification(endTime, DateUtil.formatter)) {
+            result.setResult(
+                    WVPResult.fail(ErrorCode.ERROR100, "endTime error, format is " + DateUtil.PATTERN)
+            );
+            return result;
+        }
 
-
-
-	@Operation(summary = "录像查询")
-	@Parameter(name = "deviceId", description = "设备国标编号", required = true)
-	@Parameter(name = "channelId", description = "通道国标编号", required = true)
-	@Parameter(name = "startTime", description = "开始时间", required = true)
-	@Parameter(name = "endTime", description = "结束时间", required = true)
-	@GetMapping("/query/{deviceId}/{channelId}")
-	public DeferredResult<WVPResult<RecordInfo>> recordinfo(@PathVariable String deviceId, @PathVariable String channelId, String startTime, String endTime){
-
-		if (logger.isDebugEnabled()) {
-			logger.debug(String.format("录像信息查询 API调用,deviceId:%s ,startTime:%s, endTime:%s",deviceId, startTime, endTime));
-		}
-		logger.info("[设备录像查询] {}-{}",deviceId,channelId);
-		DeferredResult<WVPResult<RecordInfo>> result = new DeferredResult<>();
-		if (!DateUtil.verification(startTime, DateUtil.formatter)){
-			throw new ControllerException(ErrorCode.ERROR100.getCode(), "startTime error, format is " + DateUtil.PATTERN);
-		}
-		if (!DateUtil.verification(endTime, DateUtil.formatter)){
-			throw new ControllerException(ErrorCode.ERROR100.getCode(), "endTime error, format is " + DateUtil.PATTERN);
-		}
-
-		Device device = storager.queryVideoDevice(deviceId);
 		// 指定超时时间 1分钟30秒
 		String uuid = UUID.randomUUID().toString();
 		int sn  =  (int)((Math.random()*9+1)*100000);
@@ -94,27 +105,24 @@ public class GBRecordController {
 		msg.setId(uuid);
 		msg.setKey(key);
 		try {
-			cmder.recordInfoQuery(device, channelId,false, startTime, endTime, sn, null, null, 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) {
-			logger.error("[命令发送失败] 查询录像: {}", e.getMessage());
-			throw new ControllerException(ErrorCode.ERROR100.getCode(), "命令发送失败: " +  e.getMessage());
-		}
+            cmder.recordInfoQuery(device, channelId, false, startTime, endTime, sn, null, null, null,
+                    (eventResult -> {
+                        String errMsg = "查询录像失败, status: " + eventResult.statusCode + ", message: " + eventResult.msg;
+                        logger.error(errMsg);
+                        msg.setData(WVPResult.fail(ErrorCode.ERROR100, errMsg));
+                        resultHolder.invokeResult(msg);
+                    }));
+        } catch (InvalidArgumentException | SipException | ParseException e) {
+            logger.error("[命令发送失败] 查询录像: {}", e.getMessage());
+            msg.setData(WVPResult.fail(ErrorCode.ERROR100, "[命令发送失败] 查询录像"));
+            resultHolder.invokeResult(msg);
+        }
 
 		// 录像查询以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);
+            msg.setData(WVPResult.fail(ErrorCode.ERROR100, "[查询超时] 查询录像超时"));
+            resultHolder.invokeResult(msg);
 		});
         return result;
 	}

+ 105 - 0
src/main/java/com/genersoft/iot/vmp/vmanager/server/sipController.java

@@ -0,0 +1,105 @@
+package com.genersoft.iot.vmp.vmanager.server;
+
+
+import com.genersoft.iot.vmp.conf.security.saToken.SaAdminCheckRole;
+import com.genersoft.iot.vmp.gb28181.bean.SipUserConfig;
+import com.genersoft.iot.vmp.service.ISipConfigService;
+import com.genersoft.iot.vmp.utils.StpAdminUtil;
+import com.genersoft.iot.vmp.vmanager.bean.ErrorCode;
+import com.genersoft.iot.vmp.vmanager.bean.WVPResult;
+import com.github.pagehelper.PageInfo;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+@Tag(name = "平台sip信息管理")
+@CrossOrigin(origins = "*")
+@RestController
+@RequestMapping("/api/sip")
+@SaAdminCheckRole("admin")
+public class sipController {
+    private Logger logger = LoggerFactory.getLogger(sipController.class);
+
+    @Autowired
+    private ISipConfigService sipConfigService;
+
+    @GetMapping("/search")
+    @Parameter(name = "p", description = "当前页", required = true)
+    @Parameter(name = "l", description = "每页查询数量", required = true)
+    @Operation(summary = "搜索sip列表")
+    public WVPResult<List<SipUserConfig>> getSipConfigs(
+            @RequestParam(value = "p", required = false, defaultValue = "1") int page,
+            @RequestParam(value = "l", required = false, defaultValue = "20") int limit) {
+        logger.info("[sip配置检索]");
+        String adminId = StpAdminUtil.getLoginId().toString();
+        PageInfo<SipUserConfig> pageResult = sipConfigService.getSipConfigsById(
+                adminId,
+                page,
+                limit);
+        return WVPResult.success(
+                pageResult.getList(),
+                page,
+                pageResult.getPageSize(),
+                pageResult.getTotal());
+    }
+
+    @PostMapping("/edit")
+    @Parameter(name = "id", description = "sipId", required = true)
+    @Operation(summary = "编辑sip配置")
+    public WVPResult<String> editSipConfig(
+            @RequestParam String id,
+            @RequestBody SipUserConfig sipConfig) {
+        logger.info("[编辑sip配置信息] id:{}", id);
+        String adminId = StpAdminUtil.getLoginId().toString();
+        // 判断当前用户是否有该域
+        SipUserConfig oldSipConfig = sipConfigService.getSipConfigById(adminId, id);
+        if (oldSipConfig == null) {
+            return WVPResult.fail(ErrorCode.ERROR404, "用户无法编辑指定sip配置");
+        }
+        if (!sipConfigService.editSipConfig(id, sipConfig)) {
+            logger.error("[编辑sip配置失败]");
+            return WVPResult.fail(ErrorCode.ERROR100, "编辑sip配置失败");
+        }
+        return WVPResult.success();
+    }
+
+    @PostMapping("/add")
+    @Operation(summary = "新增sip配置")
+    public WVPResult<String> editSipConfig(
+            @RequestBody SipUserConfig sipConfig) {
+        String adminId = StpAdminUtil.getLoginId().toString();
+        logger.info("[新增sip配置信息] admin:{}", adminId);
+        if (!sipConfigService.addSipConfig(adminId, sipConfig)) {
+            logger.error("[新增sip配置失败]");
+            return WVPResult.fail(ErrorCode.ERROR100, "新增sip配置失败");
+        }
+        return WVPResult.success();
+    }
+
+    @PostMapping("/del")
+    @Parameter(name = "id", description = "sipId", required = true)
+    @Operation(summary = "删除sip配置")
+    public WVPResult<String> delSipConfig(
+            @RequestParam String id,
+            @RequestBody SipUserConfig sipConfig) {
+        logger.info("[删除sip配置信息] id:{}", id);
+        String adminId = StpAdminUtil.getLoginId().toString();
+        // 判断当前用户是否有该域
+        SipUserConfig oldSipConfig = sipConfigService.getSipConfigById(adminId, id);
+        if (oldSipConfig == null) {
+            return WVPResult.fail(ErrorCode.ERROR404, "用户无法删除指定sip配置");
+        }
+        if (!sipConfigService.deleteSipConfig(id, adminId)) {
+            logger.error("[删除sip配置失败]");
+            return WVPResult.fail(ErrorCode.ERROR100, "删除sip配置失败");
+        }
+        return WVPResult.success();
+    }
+
+}

+ 1 - 0
web_src/src/components/channelList.vue

@@ -332,6 +332,7 @@ export default {
       this.showPlayDialog = true;
       itemData.streamId = res.data.data.stream;
       // 强制更新 this.streamInfo
+      console.log(res.data.data)
       this.$set(this, 'streamInfo', res.data.data);
       this.$nextTick(() => {
         this.$set(this, 'streamInfo', res.data.data);

+ 3 - 0
web_src/src/components/com/livePlayBox.vue

@@ -196,6 +196,9 @@ export default {
       if (this.enableDebug) {
         console.log(`播放url: ${this.videoUrl}`);
       }
+      console.log(`播放url: ${this.videoUrl}`);
+      console.log(this.videoUrl)
+
       return this.videoUrl;
     },
     async queryMediaInfo(isFirst) {

+ 295 - 0
web_src/src/components/com/sipConfigEdit.vue

@@ -0,0 +1,295 @@
+<script>
+import {FormVerify} from "kind-form-verify";
+import fieldCheck from "@/until/FieldCheck";
+import handle from "@/until/handle";
+
+
+let fieldVerify = null;
+let baseSipConfig = {
+  id: null,
+  sipDomain: "",
+  serverId: "",
+  password: "",
+  description: "",
+  enable: "1",
+  enableBind: "1",
+}
+export default {
+  name: "sipConfigEdit",
+  props: {
+    config: {
+      default() {
+        return {
+          ...baseSipConfig
+        }
+      }
+    }
+  },
+  watch: {
+    config: {
+      handler(newName, oldName) {
+        this.initialValue()
+      },
+      immediate: true,
+      deep: true
+    }
+  },
+  data() {
+    return {
+      loading: false,
+      id: null,
+      form: {
+        sipDomain: {
+          val: "",
+          oldVal: "",
+          init: "",
+          msg: "",
+          showText: ""
+        },
+        serverId: {
+          val: "",
+          oldVal: "",
+          init: "",
+          msg: "",
+          showText: ""
+        },
+        password: {
+          val: "",
+          oldVal: "",
+          init: "",
+          msg: "",
+          showText: ""
+        },
+        description: {
+          val: "",
+          oldVal: "",
+          init: "",
+          msg: "",
+          showText: ""
+        },
+        enable: {
+          val: false,
+          oldVal: false,
+          init: false,
+          msg: "",
+          showText: ""
+        },
+        enableBind: {
+          val: false,
+          oldVal: false,
+          init: false,
+          msg: "",
+          showText: ""
+        },
+      },
+      beforeMount() {
+        fieldVerify = new FormVerify(this.form, fieldCheck.fieldCheck)
+      },
+
+    }
+  },
+  beforeMount() {
+    this.initialValue()
+  },
+  methods: {
+    initialValue() {
+      let config = {
+        ...baseSipConfig,
+        ...this.config
+      }
+      console.log(config);
+      // 判断是否为新增
+      this.form.sipDomain.init = config.sipDomain
+      this.form.serverId.init = config.serverId
+      this.form.password.init = config.password
+      this.form.enable.init = config.enable === "1"
+      this.form.enableBind.init = config.enableBind === "1"
+      this.form.description.init = config.description
+      this.id = config.id
+
+      this.form.sipDomain.val = config.sipDomain
+      this.form.serverId.val = config.serverId
+      this.form.password.val = config.password
+      this.form.enable.val = config.enable === "1"
+
+      this.form.enableBind.val = config.enableBind === "1"
+      console.log(`enable: ${this.form.enable.val} enableBind: ${this.form.enableBind.val}`)
+      this.form.description.val = config.description
+      fieldVerify = new FormVerify(this.form, fieldCheck.fieldCheck)
+      fieldVerify.onLog = (msg) => {
+        console.log(msg)
+      }
+      this.initForm();
+
+    },
+    initForm() {
+      fieldVerify.init()
+      console.log(`enable: ${this.form.enable.val} enableBind: ${this.form.enableBind.val}`)
+    },
+    checkForm() {
+      return true
+    },
+    formBlurHandle(key) {
+      console.log("formBlurHandle")
+      if (!this.form[key]) {
+        return console.warn("error field")
+      }
+      fieldVerify.checkItem(key)
+    },
+    formFocusHandle(type, key) {
+      let formObject = this.form;
+      if (!formObject[key]) {
+        return console.warn("error field")
+      }
+      formObject[key].msg = ""
+    },
+    resetHandle() {
+      this.initForm()
+    },
+    submitHandle() {
+      // 校验数据
+      if (!fieldVerify.check()) {
+        console.log("数据错误");
+        console.log(this.form);
+        this.$message.error("请正确填写国标配置信息")
+        return;
+      }
+      console.log(`enable: ${this.form.enable.val} enableBind: ${this.form.enableBind.val}`)
+      if (this.id == null) {
+        this.executeAddConfig();
+      } else {
+        this.executeEditConfig();
+      }
+    },
+    closePopHandle() {
+      this.$emit(`exitPop`)
+    },
+    async executeAddConfig() {
+      this.loading = true;
+      let url = "/api/sip/add"
+      let formData = fieldVerify.getFormData()
+      console.log(formData)
+      formData.enable = formData.enable ? "1" : "0"
+      formData.enableBind = formData.enableBind ? "1" : "0"
+      console.log(formData)
+      let [err, res] = await handle(
+          this.$axios.post(url, formData)
+      )
+      this.loading = false;
+      if (err || res.data.code != 0) {
+        let errStr = err ? err.message : res.data.msg
+        console.log(`[新增sip配置] ${errStr}`)
+        this.$message.error(`新增sip配置失败 ${errStr}`);
+        return;
+      }
+      this.$message.success("新增配置完成");
+      this.closePopHandle()
+    },
+    async executeEditConfig() {
+
+      let url = "/api/sip/edit"
+      if (!this.id) {
+        this.$message.error("无法编辑该配置,请练习管理员")
+        console.error("异常调用!!!!!!")
+        return ""
+      }
+      url += `?id=${this.id}`;
+      this.loading = true;
+      let formData = fieldVerify.getFormData()
+      formData.enable = formData.enable ? "1" : "0"
+      formData.enableBind = formData.enableBind ? "1" : "0"
+      console.log(formData)
+      let [err, res] = await handle(
+          this.$axios.post(url, formData)
+      )
+      this.loading = false;
+      if (err || res.data.code != 0) {
+        let errStr = err ? err.message : res.data.msg
+        console.log(`[新增sip配置] ${errStr}`)
+        this.$message.error(`编辑sip配置失败 ${errStr}`);
+        return;
+      }
+      this.$message.success("新增配置完成");
+      this.closePopHandle()
+    }
+  }
+}
+</script>
+
+<template>
+  <div class="full-box">
+    <div class="bind-box" :loading="loading">
+      <h2>{{ id ? "编辑国标配置" : "新增国标配置" }}</h2>
+      <el-form>
+        <el-form-item class="form-item" prop="sipDomain" :error="form.sipDomain.msg">
+          <el-input v-model="form.sipDomain.val"
+                    placeholder="国标域"
+                    @blur="formBlurHandle('sipDomain')"
+                    @focus="formFocusHandle( 'sip','sipDomain')"
+          />
+        </el-form-item>
+        <el-form-item class="form-item" prop="serverId" :error="form.serverId.msg">
+          <el-input v-model="form.serverId.val"
+                    placeholder="国标服务id"
+                    @blur="formBlurHandle('serverId')"
+                    @focus="formFocusHandle( 'sip','serverId')"
+          />
+        </el-form-item>
+        <el-form-item class="form-item" prop="password" :error="form.password.msg">
+          <el-input v-model="form.password.val"
+                    placeholder="国标连接密码"
+                    @blur="formBlurHandle('password')"
+                    @focus="formFocusHandle( 'sip','password')"
+          />
+        </el-form-item>
+        <el-form-item class="form-item" prop="description" :error="form.description.msg">
+          <el-input v-model="form.description.val"
+                    placeholder="配置描述信息"
+                    @blur="formBlurHandle('description')"
+                    @focus="formFocusHandle( 'sip','description')"
+          />
+        </el-form-item>
+
+        <el-tooltip
+            class="item"
+            effect="dark"
+            content="启用后连接该域的设备会尝试去获取设备的绑定码"
+            placement="top-start">
+          <el-form-item
+              class="form-item"
+              prop="enable"
+              :error="form.enableBind.msg"
+              label="是否绑定设备">
+            <el-switch
+                v-model="form.enableBind.val"
+                active-color="#13ce66"
+                inactive-color="#ff4949">
+            </el-switch>
+          </el-form-item>
+        </el-tooltip>
+
+        <el-form-item
+            class="form-item"
+            prop="enable"
+            :error="form.enable.msg"
+            label="是否启用">
+          <el-switch
+              v-model="form.enable.val"
+              active-color="#13ce66"
+              inactive-color="#ff4949">
+          </el-switch>
+        </el-form-item>
+      </el-form>
+      <el-button @click="resetHandle">
+        重置
+      </el-button>
+      <el-button @click="submitHandle">
+        {{ id == null ? "新增配置" : "保存配置" }}
+      </el-button>
+    </div>
+  </div>
+</template>
+
+<style scoped>
+
+</style>

+ 10 - 2
web_src/src/components/console.vue

@@ -3,6 +3,8 @@
     <div class="page-header">
       <div class="page-title">控制台</div>
       <div class="page-header-btn">
+        <el-button icon="el-icon-info" size="mini" style="margin-right: 1rem;" type="primary" @click="toSips">编辑Sip配置
+        </el-button>
         <el-button icon="el-icon-info" size="mini" style="margin-right: 1rem;" type="primary" @click="showInfo">平台信息
         </el-button>
       </div>
@@ -67,6 +69,7 @@ import consoleResource from './console/ConsoleResource.vue'
 import configInfo from './dialog/configInfo.vue'
 
 import echarts from 'echarts';
+import router from "@/router";
 
 export default {
   name: 'app',
@@ -152,15 +155,20 @@ export default {
       this.$axios.axios({
         method: 'get',
         url: `/api/server/system/configInfo`,
-      }).then( (res)=> {
+      }).then((res) => {
         console.log(res)
         if (res.data.code === 0) {
           console.log(2222)
           console.log(this.$refs.configInfo)
           this.$refs.configInfo.openDialog(res.data.data)
         }
-      }).catch( (error)=> {
+      }).catch((error) => {
       });
+    },
+
+    toSips() {
+      //
+      router.push('/sipConfigs');
     }
 
   }

+ 283 - 0
web_src/src/components/setting/SipConfigs.vue

@@ -0,0 +1,283 @@
+<script>
+import handle from "@/until/handle";
+import syncChannelProgress from "@components/dialog/SyncChannelProgress.vue";
+import BindDevice from "@components/account_com/bindDevice.vue";
+import SipConfigEdit from "@components/com/sipConfigEdit.vue";
+
+export default {
+  name: "sipConfigs",
+  components: {SipConfigEdit, BindDevice, syncChannelProgress},
+  data() {
+    return {
+      configs: [],
+      loading: false,
+      total: 0,
+      page: 1,
+      limit: 10,
+      showAddConfig: false,
+      editConfig: {
+        id: null,
+        sipDomain: "",
+        serverId: "",
+        password: "",
+        description: "",
+        enable: "",
+        enableBind: "",
+      }
+    }
+  },
+  beforeMount() {
+    this.loadConfigs();
+  },
+  methods: {
+    async loadConfigs() {
+      this.loading = true;
+      let url = "/api/sip/search"
+      let [err, res] = await handle(
+          this.$axios.get(url)
+      )
+      this.loading = false;
+      if (err || res.data.code != 0) {
+        let errStr = err ? err.message : res.data.msg
+        console.log(`[搜索sip配置表] ${errStr}`)
+        this.$message.error("加载配置失败");
+        return;
+      }
+      // 加载完成
+      let response = res.data;
+      this.configs = response.data;
+      this.total = response.total;
+    },
+
+    handleCurrentChange: function (val) {
+      this.currentPage = val;
+      this.loadConfigs();
+    },
+    handleSizeChange: function (val) {
+      this.count = val;
+      this.loadConfigs();
+    },
+    refreshLoad() {
+      this.loadConfigs()
+    },
+    async executeDeleteConfig(id) {
+
+      if (!id) {
+        console.log(`删除sip配置时出现 id 为空的情况`)
+        return
+      }
+      let url = `/api/sip/del?id=${id}`
+      this.loading = true;
+      let [err, res] = await handle(
+          this.$axios.post(url)
+      )
+      this.loading = false;
+      if (err || res.data.code != 0) {
+        let errStr = err ? err.message : res.data.msg
+        console.log(`[搜索sip配置表] ${errStr}`)
+        this.$message.error("加载配置失败");
+        return;
+      }
+      // 加载完成
+      let response = res.data;
+      this.configs = response.data;
+      this.total = response.total;
+    },
+
+    copyAtCmd(row) {
+      // 生成可用的AT命令
+      let atCmd = "AT+CAMPARA=27, "
+      // at+campara=27,"3402000000","34020000001320000242","34020000001320000001","12345678"
+      atCmd += `"${row.sipDomain}",`
+      atCmd += `"设备id",`
+      atCmd += `"${row.serverId}",`
+      atCmd += `"${row.password}",`
+    },
+
+    /**
+     * 开启新增配置弹窗
+     */
+    handleOpenAddConfig() {
+      this.executeOpenConfigPop();
+    },
+    /** 开启编辑弹窗 */
+    handleOpenEditConfig(item) {
+      this.editConfig = {...item}
+      console.log(this.editConfig)
+      this.executeOpenConfigPop();
+    },
+    handleDeleteConfig(item) {
+      executeDeleteConfig(item.id);
+    },
+    executeOpenConfigPop() {
+      this.showAddConfig = true;
+    },
+    /** 关闭配置弹窗 */
+    executeCloseConfigPop() {
+      this.showAddConfig = false;
+      this.editConfig = {};
+      this.loadConfigs();
+    },
+  }
+}
+
+</script>
+
+<template>
+  <div class="content device-full">
+    <!--    设备表格 -->
+    <!--设备列表-->
+    <div class="round-box search-header">
+      <div class="search-info">
+        <div class="page-title">sip配置中心</div>
+        <div class="page-header-btn">
+
+          <el-button icon="el-icon-plus" size="mini" style="margin-right: 1rem;"
+                     type="primary"
+                     :loading="loading"
+                     @click="handleOpenAddConfig">创建新配置
+          </el-button>
+          <!--        <el-button icon="el-icon-refresh-right" circle size="mini" :loading="getDeviceListLoading"-->
+          <!--                   @click="getDeviceList()"></el-button>-->
+
+          <el-button icon="el-icon-refresh-right" circle size="mini" :loading="loading"
+                     @click="refreshLoad()"></el-button>
+        </div>
+      </div>
+    </div>
+    <div class="round-box search-ctx mt-2" v-if="total > 0 || loading">
+      <el-table class="search-list" :loading="loading" :data="configs" :height="'100%'"
+                header-row-class-name="table-header">
+        <el-table-column prop="id" label="id" min-width="30">
+        </el-table-column>
+        <el-table-column prop="sipDomain" label="国标域" min-width="80">
+        </el-table-column>
+        <el-table-column prop="serverId" label="服务端id" min-width="120">
+        </el-table-column>
+        <el-table-column prop="password" label="连接密码" min-width="80">
+        </el-table-column>
+        <el-table-column label="是否启用" min-width="80">
+          <template slot-scope="scope">
+            <div slot="enable" class="name-wrapper">
+              <el-tag size="medium" v-if="scope.row.enable == 1">启用</el-tag>
+              <el-tag size="medium" type="info" v-if="scope.row.enable == 0">不启用</el-tag>
+            </div>
+          </template>
+        </el-table-column>
+        <el-table-column label="是否为绑定设备" min-width="80">
+          <template slot-scope="scope">
+            <div slot="enableBind" class="name-wrapper">
+              <el-tag size="medium" v-if="scope.row.enableBind == 1">绑定</el-tag>
+              <el-tag size="medium" type="info" v-if="scope.row.enableBind == 0">不绑定</el-tag>
+            </div>
+          </template>
+        </el-table-column>
+        <el-table-column prop="description" label="备注" min-width="160">
+        </el-table-column>
+
+        <el-table-column label="操作" min-width="140" fixed="right">
+          <template slot-scope="scope">
+            <el-button size="medium" icon="el-icon-edit" type="text" @click="handleOpenEditConfig(scope.row)">编辑
+            </el-button>
+            <el-divider direction="vertical"></el-divider>
+            <el-button size="medium" icon="el-icon-delete" type="text" @click="handleDeleteConfig(scope.row)"
+                       style="color: #f56c6c">删除
+            </el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+      <el-pagination
+          style="float: right"
+          @size-change="handleSizeChange"
+          @current-change="handleCurrentChange"
+          :current-page="page"
+          :page-size="limit"
+          :page-sizes="[10, 15, 20, 30, 50]"
+          layout="total, sizes, prev, pager, next"
+          :total="total">
+      </el-pagination>
+    </div>
+    <!--    没有设备提示 -->
+    <div class="round-box search-ctx mt-2 " v-else>
+      <div class="search-tips">
+        <h1>无法找到对应的国标配置</h1>
+        <div class="btn-group">
+          <el-button @click="refreshLoad">重新加载</el-button>
+          <el-button @click="handleOpenAddConfig()">新增国标配置</el-button>
+        </div>
+
+      </div>
+    </div>
+
+    <!--    绑定新设备-->
+    <el-dialog
+        :title="editConfig.id!=null?'编辑配置':'新增配置'"
+        :close-on-click-modal="false"
+        :visible.sync="showAddConfig"
+        :destroy-on-close="true"
+        @close="executeCloseConfigPop"
+    >
+      <sip-config-edit
+          :config="editConfig"
+          @exitPop="executeCloseConfigPop"
+      ></sip-config-edit>
+    </el-dialog>
+  </div>
+</template>
+
+<style scoped>
+.content {
+  width: 100%;
+  min-height: 100%;
+  position: relative;
+  /*top: 110px;*/
+  box-sizing: border-box;
+  padding: 5px;
+  background-color: #a4a4a4;
+}
+
+.round-box {
+  box-sizing: border-box;
+  padding: 5px;
+  background-color: #fff;
+  border-radius: 3px;
+}
+
+.mt-2 {
+  margin-top: 5px;
+}
+
+.device-full {
+  display: flex;
+  flex-direction: column;
+  height: calc(100vh - 70px);
+}
+
+.search-info {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  height: 35px;
+}
+
+.search-ctx {
+  width: 100%;
+  height: calc(100% - 50px);
+  display: flex;
+  flex-direction: column;
+}
+
+.search-list {
+  width: 100%;
+  height: calc(100% - 50px);
+}
+
+.search-tips {
+  width: 100%;
+  height: 100%;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  flex-direction: column;
+}
+</style>

+ 4 - 2
web_src/src/layout/UiHeader.vue

@@ -65,7 +65,7 @@ export default {
       activeIndex: this.$route.path,
       unreadAlarmCount: 0,
       // 刷新时间
-      refreshTime: 30 * 1000 * 10,
+      refreshTime: 30 * 1000,
       requestInProgress: false,
       editUser: false
     };
@@ -167,7 +167,9 @@ export default {
       this.requestInProgress = true;
       let [err,res] = await handle(this.$axios.get('ai/unread'));
       this.requestInProgress = false;
-      // setTimeout(() => { this.fetchAlarmCount(); }, this.refreshTime);
+      setTimeout(() => {
+        this.fetchAlarmCount();
+      }, this.refreshTime);
       if(err){
         return console.error(err);
       }

+ 1 - 1
web_src/src/pages/index/main.js

@@ -71,7 +71,7 @@ _axios.interceptors.response.use((response) => {
 _axios.interceptors.request.use(
   config => {
     if (userService.getToken() != null && config.url !== "/api/user/login") {
-      config.headers['access-token'] = `${userService.getToken()}`;
+      // config.headers['access-token'] = `${userService.getToken()}`;
     }
     return config;
   },

+ 22 - 16
web_src/src/router/index.js

@@ -32,11 +32,13 @@ import alarmInfo from "@/components/mediaView";
 
 import userCenter from "@/layout/userCenter";
 
+import sipConfigs from "@components/setting/SipConfigs.vue";
+
 import userInfo from "@/components/u_page/u_info";
 
 const originalPush = VueRouter.prototype.push
 VueRouter.prototype.push = function push(location) {
-  return originalPush.call(this, location).catch(err => err)
+    return originalPush.call(this, location).catch(err => err)
 }
 
 Vue.use(VueRouter)
@@ -51,21 +53,25 @@ export default new VueRouter({
       component: Layout,
       redirect: '/console',
       children: [
-        {
-          path: '/console',
-          component: console,
-        },
-        {
-          path: '/live',
-          component: live,
-        },
-        {
-          path: '/deviceList',
-          component: deviceList,
-        },
-        {
-          path: '/pushVideoList',
-          component: pushVideoList,
+          {
+              path: '/console',
+              component: console,
+          },
+          {
+              path: '/sipConfigs',
+              component: sipConfigs,
+          },
+          {
+              path: '/live',
+              component: live,
+          },
+          {
+              path: '/deviceList',
+              component: deviceList,
+          },
+          {
+              path: '/pushVideoList',
+              component: pushVideoList,
         },
         {
           path: '/streamProxyList',

+ 17 - 0
web_src/src/until/FieldCheck.js

@@ -44,6 +44,23 @@ fieldCheck.addRuleItem('bindCode', ['bindCode'], [
     }
 ]);
 
+fieldCheck.addRuleItem('sipDomain', ['sipDomain'], [
+    requiredRuleItem,
+    {
+        type: 'string',
+        length: 10,
+        message: "国标域为10位"
+    }
+]);
+
+fieldCheck.addRuleItem('serverId', ['serverId'], [
+    requiredRuleItem,
+    {
+        type: 'string',
+        length: 20,
+        message: "国标id应该位20位"
+    }
+]);
 
 export default {
     fieldCheck

+ 9 - 2
设备绑定机制.md

@@ -59,7 +59,7 @@
 | name    | varchar(100) | 昵称   | 随机用户id | 用户名称   |
 | passwd  | varchar(255) | 密码   | 无      | 用户密码摘要 |
 
-##### 管理员sip配置表
+##### 管理员sip配置表 sipConfig
 
 > 管理员sip配置表, 用于管理sip信息, 允许多种设备同步接入到平台
 > 可以指定域启用绑定设备, 用于设备上线时获取设备绑定码, 用于app端用户绑定设备
@@ -77,7 +77,14 @@
 | createTime  | datetime     | 创建时间     | 无  | 创建时间                |
 | enableBind  | char(1)      | 是否启用设备绑定 | 1  | 是否启用设备绑定 1 允许 0 不允许 |
 
-#### 设备表扩展
+#### 设备表扩展 devices
+
+| 字段       | 类型           | 值介绍   | 默认 | 备注       |
+|----------|--------------|-------|----|----------|
+| id       | int          | 主键    | 无  | 无        |
+| deviceId | varchar(100) | 设备id  | 无  | 设备sip id |
+| domain   | varchar(100) | sip域名 | 无  | sip域名    |
+| devCode  | varchar(255) | 设备绑定码 | 无  | 设备绑定码    |
 
 > 使用字段 `devCode` 设备注册时获取