From fa62ab9a0143433a5d058ab3229a37e4a9a0f696 Mon Sep 17 00:00:00 2001
From: 648540858 <648540858@qq.com>
Date: 星期二, 20 六月 2023 14:16:46 +0800
Subject: [PATCH] Merge pull request #893 from sxh-netizen/wvp-28181-2.0

---
 src/main/java/com/genersoft/iot/vmp/gb28181/bean/Device.java                                        |   18 +
 src/main/java/com/genersoft/iot/vmp/gb28181/transmit/cmd/impl/SIPCommander.java                     |   24 +
 src/main/java/com/genersoft/iot/vmp/common/StreamInfo.java                                          |   27 +
 src/main/java/com/genersoft/iot/vmp/web/gb28181/ApiStreamController.java                            |    2 
 src/main/java/com/genersoft/iot/vmp/gb28181/transmit/cmd/ISIPCommander.java                         |    2 
 src/main/java/com/genersoft/iot/vmp/storager/dao/DeviceMapper.java                                  |    9 
 web_src/src/components/dialog/deviceEdit.vue                                                        |    6 
 src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/ByeRequestProcessor.java    |   22 
 src/main/java/com/genersoft/iot/vmp/conf/UserSetting.java                                           |    7 
 src/main/resources/application-dev.yml                                                              |   67 ++
 src/main/java/com/genersoft/iot/vmp/service/impl/InviteStreamServiceImpl.java                       |  160 ++++++++
 src/main/java/com/genersoft/iot/vmp/service/IPlayService.java                                       |    6 
 src/main/java/com/genersoft/iot/vmp/service/impl/DeviceServiceImpl.java                             |   25 +
 web_src/config/index.js                                                                             |    4 
 src/main/java/com/genersoft/iot/vmp/service/impl/PlayServiceImpl.java                               |  390 +++++++++++++++------
 src/main/java/com/genersoft/iot/vmp/common/InviteInfo.java                                          |   37 ++
 src/main/java/com/genersoft/iot/vmp/service/IInviteStreamService.java                               |   48 ++
 src/main/java/com/genersoft/iot/vmp/gb28181/transmit/callback/DeferredResultHolder.java             |   26 +
 src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/InviteRequestProcessor.java |    2 
 web_src/src/components/channelList.vue                                                              |   17 
 src/main/java/com/genersoft/iot/vmp/media/zlm/ZLMHttpHookListener.java                              |   97 +++-
 sql/2.6.6-2.6.7更新.sql                                                                               |    3 
 src/main/java/com/genersoft/iot/vmp/vmanager/gb28181/play/PlayController.java                       |   45 +-
 src/main/resources/application.yml                                                                  |    2 
 src/main/java/com/genersoft/iot/vmp/storager/dao/DeviceChannelMapper.java                           |    5 
 25 files changed, 846 insertions(+), 205 deletions(-)

diff --git "a/sql/2.6.6-2.6.7\346\233\264\346\226\260.sql" "b/sql/2.6.6-2.6.7\346\233\264\346\226\260.sql"
index 09732b3..285d573 100755
--- "a/sql/2.6.6-2.6.7\346\233\264\346\226\260.sql"
+++ "b/sql/2.6.6-2.6.7\346\233\264\346\226\260.sql"
@@ -7,6 +7,7 @@
 alter table device
     add mediaServerId varchar(50) default null;
 
-
+ALTER TABLE device
+    ADD COLUMN `switchPrimarySubStream` bit(1) NOT NULL DEFAULT b'0' COMMENT '寮�鍚富瀛愮爜娴佸垏鎹㈢殑寮�鍏筹紙0-涓嶅紑鍚紝1-寮�鍚級鐜板湪宸茬煡鏀寔璁惧涓� 澶у崕銆乀P鈥斺�擫INK鍏ㄧ郴璁惧' AFTER `keepalive_interval_time`
 
 
diff --git a/src/main/java/com/genersoft/iot/vmp/common/InviteInfo.java b/src/main/java/com/genersoft/iot/vmp/common/InviteInfo.java
index 9fe43f7..029b7ca 100644
--- a/src/main/java/com/genersoft/iot/vmp/common/InviteInfo.java
+++ b/src/main/java/com/genersoft/iot/vmp/common/InviteInfo.java
@@ -1,6 +1,7 @@
 package com.genersoft.iot.vmp.common;
 
 import com.genersoft.iot.vmp.service.bean.SSRCInfo;
+import io.swagger.v3.oas.annotations.media.Schema;
 
 /**
  * 璁板綍姣忔鍙戦�乮nvite娑堟伅鐨勭姸鎬�
@@ -123,4 +124,40 @@
     public void setStreamMode(String streamMode) {
         this.streamMode = streamMode;
     }
+
+
+    /*=========================璁惧涓诲瓙鐮佹祦閫昏緫START====================*/
+    @Schema(description = "鏄惁涓哄瓙鐮佹祦(true-鏄紝false-涓荤爜娴�)")
+    private boolean subStream;
+
+    public boolean isSubStream() {
+        return subStream;
+    }
+
+    public void setSubStream(boolean subStream) {
+        this.subStream = subStream;
+    }
+
+    public static InviteInfo getInviteInfo(String deviceId, String channelId,Boolean isSubStream, String stream, SSRCInfo ssrcInfo,
+                                           String receiveIp, Integer receivePort, String streamMode,
+                                           InviteSessionType type, InviteSessionStatus status) {
+        InviteInfo inviteInfo = new InviteInfo();
+        inviteInfo.setDeviceId(deviceId);
+        inviteInfo.setChannelId(channelId);
+        inviteInfo.setStream(stream);
+        inviteInfo.setSsrcInfo(ssrcInfo);
+        inviteInfo.setReceiveIp(receiveIp);
+        inviteInfo.setReceivePort(receivePort);
+        inviteInfo.setStreamMode(streamMode);
+        inviteInfo.setType(type);
+        inviteInfo.setStatus(status);
+        if(isSubStream != null){
+            inviteInfo.setSubStream(isSubStream);
+        }
+        return inviteInfo;
+    }
+    /*=========================璁惧涓诲瓙鐮佹祦閫昏緫END====================*/
+
+
+
 }
diff --git a/src/main/java/com/genersoft/iot/vmp/common/StreamInfo.java b/src/main/java/com/genersoft/iot/vmp/common/StreamInfo.java
index a501daf..5bda636 100644
--- a/src/main/java/com/genersoft/iot/vmp/common/StreamInfo.java
+++ b/src/main/java/com/genersoft/iot/vmp/common/StreamInfo.java
@@ -528,4 +528,31 @@
         }
         return instance;
     }
+
+
+    /*=========================璁惧涓诲瓙鐮佹祦閫昏緫START====================*/
+    @Schema(description = "鏄惁涓哄瓙鐮佹祦(true-鏄紝false-涓荤爜娴�)")
+    private boolean subStream;
+
+    public boolean isSubStream() {
+        return subStream;
+    }
+
+    public void setSubStream(boolean subStream) {
+        this.subStream = subStream;
+    }
+
+    public static String getPlayStream(String deviceId,String channelId,boolean isSubStream){
+        String streamId;
+        if(isSubStream){
+            streamId = String.format("%s_%s_%s","sub",deviceId, channelId);
+        }else {
+            streamId = String.format("%s_%s_%s","main", deviceId, channelId);
+        }
+        return streamId;
+    }
+
+    /*=========================璁惧涓诲瓙鐮佹祦閫昏緫END====================*/
+
+
 }
diff --git a/src/main/java/com/genersoft/iot/vmp/conf/UserSetting.java b/src/main/java/com/genersoft/iot/vmp/conf/UserSetting.java
index 515f3e7..bd998c7 100644
--- a/src/main/java/com/genersoft/iot/vmp/conf/UserSetting.java
+++ b/src/main/java/com/genersoft/iot/vmp/conf/UserSetting.java
@@ -1,5 +1,6 @@
 package com.genersoft.iot.vmp.conf;
 
+import io.swagger.v3.oas.annotations.media.Schema;
 import org.springframework.core.annotation.Order;
 import org.springframework.boot.context.properties.ConfigurationProperties;
 import org.springframework.stereotype.Component;
@@ -25,11 +26,11 @@
 
     private int platformPlayTimeout = 60000;
 
-    private Boolean interfaceAuthentication = Boolean.TRUE;
+    private Boolean interfaceAuthentication = Boolean.FALSE;
 
-    private Boolean recordPushLive = Boolean.TRUE;
+    private Boolean recordPushLive = Boolean.FALSE;
 
-    private Boolean recordSip = Boolean.TRUE;
+    private Boolean recordSip = Boolean.FALSE;
 
     private Boolean logInDatebase = Boolean.TRUE;
 
diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/bean/Device.java b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/Device.java
index 0ef0629..1318c59 100644
--- a/src/main/java/com/genersoft/iot/vmp/gb28181/bean/Device.java
+++ b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/Device.java
@@ -189,6 +189,8 @@
 	private SipTransactionInfo sipTransactionInfo;
 
 
+
+
 	public String getDeviceId() {
 		return deviceId;
 	}
@@ -447,4 +449,20 @@
 	public void setSipTransactionInfo(SipTransactionInfo sipTransactionInfo) {
 		this.sipTransactionInfo = sipTransactionInfo;
 	}
+
+	/*======================璁惧涓诲瓙鐮佹祦閫昏緫START=========================*/
+	@Schema(description = "寮�鍚富瀛愮爜娴佸垏鎹㈢殑寮�鍏筹紙false-涓嶅紑鍚紝true-寮�鍚級")
+	private boolean switchPrimarySubStream;
+
+	public boolean isSwitchPrimarySubStream() {
+		return switchPrimarySubStream;
+	}
+
+	public void setSwitchPrimarySubStream(boolean switchPrimarySubStream) {
+		this.switchPrimarySubStream = switchPrimarySubStream;
+	}
+
+	/*======================璁惧涓诲瓙鐮佹祦閫昏緫END=========================*/
+
+
 }
diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/callback/DeferredResultHolder.java b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/callback/DeferredResultHolder.java
index 2175a45..5677c95 100644
--- a/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/callback/DeferredResultHolder.java
+++ b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/callback/DeferredResultHolder.java
@@ -155,4 +155,30 @@
 			map.remove(msg.getKey());
 		}
 	}
+
+	/*============================璁惧涓诲瓙鐮佹祦閫昏緫START========================*/
+	public static String getPlayKey(String deviceId,String channelId,boolean deviceSwitchSubStream,boolean isSubStream){
+		String key = null;
+		if(deviceSwitchSubStream){
+			key = CALLBACK_CMD_PLAY + isSubStream + deviceId + channelId;
+		}else {
+			key = CALLBACK_CMD_PLAY +deviceId + channelId;
+		}
+		return key;
+	}
+
+	public static String getSnapKey(String deviceId,String channelId,boolean deviceSwitchSubStream,boolean isSubStream){
+		String key = null;
+		if(deviceSwitchSubStream){
+			key = CALLBACK_CMD_SNAP + isSubStream + deviceId + channelId;
+		}else {
+			key = CALLBACK_CMD_SNAP +deviceId + channelId;
+		}
+		return key;
+	}
+
+
+	/*============================璁惧涓诲瓙鐮佹祦閫昏緫END========================*/
+
+
 }
diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/cmd/ISIPCommander.java b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/cmd/ISIPCommander.java
index d684056..344e16f 100644
--- a/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/cmd/ISIPCommander.java
+++ b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/cmd/ISIPCommander.java
@@ -98,7 +98,7 @@
 	 * @param device  瑙嗛璁惧
 	 * @param channelId  棰勮閫氶亾
 	 */
-	void playStreamCmd(MediaServerItem mediaServerItem, SSRCInfo ssrcInfo, Device device, String channelId, ZlmHttpHookSubscribe.Event event, SipSubscribe.Event okEvent, SipSubscribe.Event errorEvent) throws InvalidArgumentException, SipException, ParseException;
+	void playStreamCmd(MediaServerItem mediaServerItem, SSRCInfo ssrcInfo, Device device, String channelId,boolean isSubStream, ZlmHttpHookSubscribe.Event event, SipSubscribe.Event okEvent, SipSubscribe.Event errorEvent) throws InvalidArgumentException, SipException, ParseException;
 
 	/**
 	 * 璇锋眰鍥炴斁瑙嗛娴�
diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/cmd/impl/SIPCommander.java b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/cmd/impl/SIPCommander.java
index 1ce072f..ccf8151 100644
--- a/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/cmd/impl/SIPCommander.java
+++ b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/cmd/impl/SIPCommander.java
@@ -266,7 +266,7 @@
      * @param errorEvent sip閿欒璁㈤槄
      */
     @Override
-    public void playStreamCmd(MediaServerItem mediaServerItem, SSRCInfo ssrcInfo, Device device, String channelId,
+    public void playStreamCmd(MediaServerItem mediaServerItem, SSRCInfo ssrcInfo, Device device, String channelId,boolean isSubStream,
                               ZlmHttpHookSubscribe.Event event, SipSubscribe.Event okEvent, SipSubscribe.Event errorEvent) throws InvalidArgumentException, SipException, ParseException {
         String stream = ssrcInfo.getStream();
 
@@ -341,6 +341,22 @@
             }
         }
 
+        if( device.isSwitchPrimarySubStream() ){
+            if("TP-LINK".equals(device.getManufacturer())){
+                if (isSubStream){
+                    content.append("a=streamMode:sub\r\n");
+                }else {
+                    content.append("a=streamMode:main\r\n");
+                }
+            }else {
+                if (isSubStream){
+                    content.append("a=streamprofile:1\r\n");
+                }else {
+                    content.append("a=streamprofile:0\r\n");
+                }
+            }
+        }
+
         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"); // 鏈彂鐜版敮鎸佹鐗规�х殑璁惧
@@ -356,7 +372,11 @@
             // 杩欓噷涓轰緥閬垮厤涓�涓�氶亾鐨勭偣鎾彧鏈変竴涓猚allID杩欎釜鍙傛暟浣跨敤涓�涓浐瀹氬��
             ResponseEvent responseEvent = (ResponseEvent) e.event;
             SIPResponse response = (SIPResponse) responseEvent.getResponse();
-            streamSession.put(device.getDeviceId(), channelId, "play", stream, ssrcInfo.getSsrc(), mediaServerItem.getId(), response, InviteSessionType.PLAY);
+            if(device.isSwitchPrimarySubStream()){
+                streamSession.put(device.getDeviceId(), channelId, "switch-play", stream, ssrcInfo.getSsrc(), mediaServerItem.getId(), response, InviteSessionType.PLAY);
+            }else {
+                streamSession.put(device.getDeviceId(), channelId, "play", stream, ssrcInfo.getSsrc(), mediaServerItem.getId(), response, InviteSessionType.PLAY);
+            }
             okEvent.response(e);
         });
     }
diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/ByeRequestProcessor.java b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/ByeRequestProcessor.java
index 66a1ce0..43e1ef8 100644
--- a/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/ByeRequestProcessor.java
+++ b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/ByeRequestProcessor.java
@@ -142,8 +142,13 @@
 			// 鍙兘鏄澶囦富鍔ㄥ仠姝�
 			Device device = storager.queryVideoDeviceByChannelId(platformGbId);
 			if (device != null) {
-				storager.stopPlay(device.getDeviceId(), channelId);
-				SsrcTransaction ssrcTransactionForPlay = streamSession.getSsrcTransaction(device.getDeviceId(), channelId, "play", null);
+				SsrcTransaction ssrcTransactionForPlay = null;
+				if (device.isSwitchPrimarySubStream() ) {
+					ssrcTransactionForPlay = streamSession.getSsrcTransaction(device.getDeviceId(), channelId,  "switch-play", null);
+				} else {
+					storager.stopPlay(device.getDeviceId(), channelId);
+					ssrcTransactionForPlay = streamSession.getSsrcTransaction(device.getDeviceId(), channelId, "play", null);
+				}
 				if (ssrcTransactionForPlay != null){
 					if (ssrcTransactionForPlay.getCallId().equals(callIdHeader.getCallId())){
 						// 閲婃斁ssrc
@@ -153,10 +158,17 @@
 						}
 						streamSession.remove(device.getDeviceId(), channelId, ssrcTransactionForPlay.getStream());
 					}
-					InviteInfo inviteInfo = inviteStreamService.getInviteInfoByDeviceAndChannel(InviteSessionType.PLAY, device.getDeviceId(), channelId);
-
-					if (inviteInfo != null) {
+					InviteInfo inviteInfo = null;
+					if (device.isSwitchPrimarySubStream() ) {
+						String streamType = ssrcTransactionForPlay.getStream().split("_")[0];
+						boolean isSubStream = "sub".equals(streamType);
+						inviteInfo = inviteStreamService.getInviteInfoByDeviceAndChannel(InviteSessionType.PLAY, device.getDeviceId(), channelId,isSubStream);
+						inviteStreamService.removeInviteInfo(inviteInfo.getType(),inviteInfo.getDeviceId(),inviteInfo.getChannelId(),isSubStream,inviteInfo.getStream());
+					}else {
+						inviteInfo = inviteStreamService.getInviteInfoByDeviceAndChannel(InviteSessionType.PLAY, device.getDeviceId(), channelId);
 						inviteStreamService.removeInviteInfo(inviteInfo);
+					}
+					if (inviteInfo != null) {
 						if (inviteInfo.getStreamInfo() != null) {
 							mediaServerService.closeRTPServer(inviteInfo.getStreamInfo().getMediaServerId(), inviteInfo.getStream());
 						}
diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/InviteRequestProcessor.java b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/InviteRequestProcessor.java
index 635c3ca..2d28a21 100644
--- a/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/InviteRequestProcessor.java
+++ b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/InviteRequestProcessor.java
@@ -489,7 +489,7 @@
                         }
                         sendRtpItem.setStreamId(streamId);
                         redisCatchStorage.updateSendRTPSever(sendRtpItem);
-                        playService.play(mediaServerItem, device.getDeviceId(), channelId, ((code, msg, data) -> {
+                        playService.play(mediaServerItem, device.getDeviceId(), channelId,false, ((code, msg, data) -> {
                             if (code == InviteErrorCode.SUCCESS.getCode()){
                                 hookEvent.run(code, msg, data);
                             }else if (code == InviteErrorCode.ERROR_FOR_SIGNALLING_TIMEOUT.getCode() || code == InviteErrorCode.ERROR_FOR_STREAM_TIMEOUT.getCode()){
diff --git a/src/main/java/com/genersoft/iot/vmp/media/zlm/ZLMHttpHookListener.java b/src/main/java/com/genersoft/iot/vmp/media/zlm/ZLMHttpHookListener.java
index aa07e09..3810eca 100644
--- a/src/main/java/com/genersoft/iot/vmp/media/zlm/ZLMHttpHookListener.java
+++ b/src/main/java/com/genersoft/iot/vmp/media/zlm/ZLMHttpHookListener.java
@@ -289,6 +289,7 @@
     @ResponseBody
     @PostMapping(value = "/on_stream_changed", produces = "application/json;charset=UTF-8")
     public HookResult onStreamChanged(@RequestBody OnStreamChangedHookParam param) {
+
         if (param.isRegist()) {
             logger.info("[ZLM HOOK] 娴佹敞鍐�, {}->{}->{}/{}", param.getMediaServerId(), param.getSchema(), param.getApp(), param.getStream());
         } else {
@@ -310,11 +311,13 @@
 
             List<OnStreamChangedHookParam.MediaTrack> tracks = param.getTracks();
             // TODO 閲嶆瀯姝ゅ閫昏緫
+            boolean isPush = false;
             if (param.isRegist()) {
                 // 澶勭悊娴佹敞鍐岀殑閴存潈淇℃伅
                 if (param.getOriginType() == OriginType.RTMP_PUSH.ordinal()
                         || param.getOriginType() == OriginType.RTSP_PUSH.ordinal()
                         || param.getOriginType() == OriginType.RTC_PUSH.ordinal()) {
+                    isPush = true;
                     StreamAuthorityInfo streamAuthorityInfo = redisCatchStorage.getStreamAuthorityInfo(param.getApp(), param.getStream());
                     if (streamAuthorityInfo == null) {
                         streamAuthorityInfo = StreamAuthorityInfo.getInstanceByHook(param);
@@ -328,7 +331,7 @@
                 redisCatchStorage.removeStreamAuthorityInfo(param.getApp(), param.getStream());
             }
 
-            if ("rtmp".equals(param.getSchema())) {
+            if ("rtsp".equals(param.getSchema())) {
                 // 鏇存柊娴佸獟浣撹礋杞戒俊鎭�
                 if (param.isRegist()) {
                     mediaServerService.addCount(param.getMediaServerId());
@@ -342,10 +345,19 @@
                 }
 
                 if ("rtp".equals(param.getApp()) && !param.isRegist()) {
-                    InviteInfo inviteInfo = inviteStreamService.getInviteInfoByStream(null, param.getStream());
-                    if (inviteInfo != null && (inviteInfo.getType() == InviteSessionType.PLAY || inviteInfo.getType() == InviteSessionType.PLAYBACK)) {
-                        inviteStreamService.removeInviteInfo(inviteInfo);
-                        storager.stopPlay(inviteInfo.getDeviceId(), inviteInfo.getChannelId());
+                    if(param.getStream().split("_").length == 3){
+                        boolean isSubStream = "sub".equals(param.getStream().split("_")[0]);
+                        InviteInfo inviteInfo = inviteStreamService.getInviteInfoByStream(null, param.getStream(), isSubStream);
+                        if(inviteInfo != null && (inviteInfo.getType() == InviteSessionType.PLAY )){
+                            inviteStreamService.removeInviteInfo(inviteInfo.getType(),inviteInfo.getDeviceId(),
+                                    inviteInfo.getChannelId(),inviteInfo.isSubStream(),inviteInfo.getStream());
+                        }
+                    }else {
+                        InviteInfo inviteInfo = inviteStreamService.getInviteInfoByStream(null, param.getStream());
+                        if (inviteInfo != null && (inviteInfo.getType() == InviteSessionType.PLAY || inviteInfo.getType() == InviteSessionType.PLAYBACK)) {
+                            inviteStreamService.removeInviteInfo(inviteInfo);
+                            storager.stopPlay(inviteInfo.getDeviceId(), inviteInfo.getChannelId());
+                        }
                     }
                 } else {
                     if (!"rtp".equals(param.getApp())) {
@@ -360,8 +372,6 @@
                             StreamInfo streamInfoByAppAndStream = mediaService.getStreamInfoByAppAndStream(mediaInfo,
                                     param.getApp(), param.getStream(), tracks, callId);
                             param.setStreamInfo(new StreamContent(streamInfoByAppAndStream));
-                            // 濡傛灉鏄媺娴佷唬鐞嗕骇鐢熺殑锛屼笉闇�瑕佸啓鍏ユ帹娴�
-
                             redisCatchStorage.addStream(mediaInfo, type, param.getApp(), param.getStream(), param);
                             if (param.getOriginType() == OriginType.RTSP_PUSH.ordinal()
                                     || param.getOriginType() == OriginType.RTMP_PUSH.ordinal()
@@ -450,6 +460,11 @@
             InviteInfo inviteInfo = inviteStreamService.getInviteInfoByStream(null, param.getStream());
             // 鐐规挱
             if (inviteInfo != null) {
+                // 褰曞儚涓嬭浇
+                if (inviteInfo.getType() == InviteSessionType.DOWNLOAD) {
+                    ret.put("close", false);
+                    return ret;
+                }
                 // 鏀跺埌鏃犱汉瑙傜湅璇存槑娴佷篃娌℃湁鍦ㄥ線涓婄骇鎺ㄩ��
                 if (redisCatchStorage.isChannelSendingRTP(inviteInfo.getChannelId())) {
                     List<SendRtpItem> sendRtpItems = redisCatchStorage.querySendRTPServerByChnnelId(
@@ -467,27 +482,33 @@
                         }
                     }
                 }
+                Device device = deviceService.getDevice(inviteInfo.getDeviceId());
+                if (device != null) {
+                    try {
+                        InviteInfo info = null;
+                        if(device.isSwitchPrimarySubStream()){
+                            boolean isSubStream = "sub".equals(param.getStream().split("_")[0]);
+                            info = inviteStreamService.getInviteInfo(inviteInfo.getType(), inviteInfo.getDeviceId(), inviteInfo.getChannelId(),isSubStream, inviteInfo.getStream());
+                        }else {
+                            info = inviteStreamService.getInviteInfo(inviteInfo.getType(), inviteInfo.getDeviceId(), inviteInfo.getChannelId(), inviteInfo.getStream());
 
-                if (userSetting.getStreamOnDemand()) {
-                    // 褰曞儚涓嬭浇
-                    if (inviteInfo.getType() == InviteSessionType.DOWNLOAD) {
-                        ret.put("close", false);
-                        return ret;
-                    }
-
-                    Device device = deviceService.getDevice(inviteInfo.getDeviceId());
-                    if (device != null) {
-                        try {
-                            if (inviteStreamService.getInviteInfo(inviteInfo.getType(), inviteInfo.getDeviceId(), inviteInfo.getChannelId(), inviteInfo.getStream()) != null) {
-                                cmder.streamByeCmd(device, inviteInfo.getChannelId(),
-                                        inviteInfo.getStream(), null);
-                            }
-                        } catch (InvalidArgumentException | ParseException | SipException |
-                                 SsrcTransactionNotFoundException e) {
-                            logger.error("[鏃犱汉瑙傜湅]鐐规挱锛� 鍙戦�丅YE澶辫触 {}", e.getMessage());
                         }
-                    }
 
+                        if (info != null) {
+                            cmder.streamByeCmd(device, inviteInfo.getChannelId(),
+                                    inviteInfo.getStream(), null);
+                        }
+                    } catch (InvalidArgumentException | ParseException | SipException |
+                             SsrcTransactionNotFoundException e) {
+                        logger.error("[鏃犱汉瑙傜湅]鐐规挱锛� 鍙戦�丅YE澶辫触 {}", e.getMessage());
+                    }
+                }
+
+                if(device.isSwitchPrimarySubStream()){
+                    boolean isSubStream = "sub".equals(param.getStream().split("_")[0]);
+                    inviteStreamService.removeInviteInfo(inviteInfo.getType(), inviteInfo.getDeviceId(),
+                            inviteInfo.getChannelId(),isSubStream, inviteInfo.getStream());
+                }else {
                     inviteStreamService.removeInviteInfo(inviteInfo.getType(), inviteInfo.getDeviceId(),
                             inviteInfo.getChannelId(), inviteInfo.getStream());
                     storager.stopPlay(inviteInfo.getDeviceId(), inviteInfo.getChannelId());
@@ -499,7 +520,7 @@
             // 鎷夋祦浠g悊
             StreamProxyItem streamProxyItem = streamProxyService.getStreamProxyByAppAndStream(param.getApp(), param.getStream());
             if (streamProxyItem != null) {
-                if (streamProxyItem.isEnableRemoveNoneReader()) {
+                if (streamProxyItem.isEnableDisableNoneReader()) {
                     // 鏃犱汉瑙傜湅鑷姩绉婚櫎
                     ret.put("close", true);
                     streamProxyService.del(param.getApp(), param.getStream());
@@ -544,12 +565,26 @@
 
         if ("rtp".equals(param.getApp())) {
             String[] s = param.getStream().split("_");
-            if (!mediaInfo.isRtpEnable() || s.length != 2) {
+            if (!mediaInfo.isRtpEnable() ) {
+                defaultResult.setResult(HookResult.SUCCESS());
+                return defaultResult;
+            }else if(s.length != 2 && s.length != 3 ){
                 defaultResult.setResult(HookResult.SUCCESS());
                 return defaultResult;
             }
-            String deviceId = s[0];
-            String channelId = s[1];
+            String deviceId = null;
+            String channelId = null;
+            boolean isSubStream = false;
+            if (s[0].length() < 20) {
+                if ("sub".equals(s[0])) {
+                    isSubStream = true;
+                }
+                deviceId = s[1];
+                channelId = s[2];
+            } else {
+                deviceId = s[0];
+                channelId = s[1];
+            }
             Device device = redisCatchStorage.getDevice(deviceId);
             if (device == null) {
                 defaultResult.setResult(new HookResult(ErrorCode.ERROR404.getCode(), ErrorCode.ERROR404.getMsg()));
@@ -563,7 +598,7 @@
             logger.info("[ZLM HOOK] 娴佹湭鎵惧埌, 鍙戣捣鑷姩鐐规挱锛歿}->{}->{}/{}", param.getMediaServerId(), param.getSchema(), param.getApp(), param.getStream());
 
             RequestMessage msg = new RequestMessage();
-            String key = DeferredResultHolder.CALLBACK_CMD_PLAY + deviceId + channelId;
+            String key = DeferredResultHolder.getPlayKey(deviceId, channelId, device.isSwitchPrimarySubStream(), isSubStream);
             boolean exist = resultHolder.exist(key, null);
             msg.setKey(key);
             String uuid = UUID.randomUUID().toString();
@@ -581,7 +616,7 @@
             resultHolder.put(key, uuid, result);
 
             if (!exist) {
-                playService.play(mediaInfo, deviceId, channelId, (code, message, data) -> {
+                playService.play(mediaInfo, deviceId, channelId,isSubStream, (code, message, data) -> {
                     msg.setData(new HookResult(code, message));
                     resultHolder.invokeResult(msg);
                 });
diff --git a/src/main/java/com/genersoft/iot/vmp/service/IInviteStreamService.java b/src/main/java/com/genersoft/iot/vmp/service/IInviteStreamService.java
index 852a408..1036efd 100644
--- a/src/main/java/com/genersoft/iot/vmp/service/IInviteStreamService.java
+++ b/src/main/java/com/genersoft/iot/vmp/service/IInviteStreamService.java
@@ -4,6 +4,8 @@
 import com.genersoft.iot.vmp.common.InviteSessionType;
 import com.genersoft.iot.vmp.service.bean.ErrorCallback;
 
+import java.util.List;
+
 /**
  * 璁板綍鍥芥爣鐐规挱鐨勭姸鎬侊紝鍖呮嫭瀹炴椂棰勮锛屼笅杞斤紝褰曞儚鍥炴斁
  */
@@ -70,4 +72,50 @@
      * 缁熻鍚屼竴涓獄lm涓嬬殑鍥芥爣鏀舵祦涓暟
      */
     int getStreamInfoCount(String mediaServerId);
+
+
+    /*======================璁惧涓诲瓙鐮佹祦閫昏緫START=========================*/
+    /**
+     * 鑾峰彇鐐规挱鐨勭姸鎬佷俊鎭�
+     */
+    InviteInfo getInviteInfoByDeviceAndChannel(InviteSessionType type,
+                                               String deviceId,
+                                               String channelId,boolean isSubStream);
+
+    void removeInviteInfoByDeviceAndChannel(InviteSessionType inviteSessionType, String deviceId, String channelId,boolean isSubStream);
+
+    InviteInfo getInviteInfo(InviteSessionType type,
+                             String deviceId,
+                             String channelId,
+                             boolean isSubStream,
+                             String stream);
+
+    void removeInviteInfo(InviteSessionType type,
+                          String deviceId,
+                          String channelId,
+                          boolean isSubStream,
+                          String stream);
+
+    void once(InviteSessionType type, String deviceId, String channelId,boolean isSubStream, String stream,  ErrorCallback<Object> callback);
+
+    void call(InviteSessionType type, String deviceId, String channelId,boolean isSubStream, String stream,  int code, String msg, Object data);
+
+    void updateInviteInfoSub(InviteInfo inviteInfo);
+
+    /**
+     * 鑾峰彇鐐规挱鐨勭姸鎬佷俊鎭�
+     */
+    InviteInfo getInviteInfoByStream(InviteSessionType type, String stream,boolean isSubStream);
+
+    /**
+     * 鑾峰彇鐐规挱鐨勭姸鎬佷俊鎭�
+     */
+    List<Object> getInviteInfos(InviteSessionType type,
+                                String deviceId,
+                                String channelId,
+                                String stream);
+    /*======================璁惧涓诲瓙鐮佹祦閫昏緫END=========================*/
+
+
+
 }
diff --git a/src/main/java/com/genersoft/iot/vmp/service/IPlayService.java b/src/main/java/com/genersoft/iot/vmp/service/IPlayService.java
index 5162411..988326e 100644
--- a/src/main/java/com/genersoft/iot/vmp/service/IPlayService.java
+++ b/src/main/java/com/genersoft/iot/vmp/service/IPlayService.java
@@ -16,9 +16,9 @@
  */
 public interface IPlayService {
 
-    void play(MediaServerItem mediaServerItem, SSRCInfo ssrcInfo, Device device, String channelId,
+    void play(MediaServerItem mediaServerItem, SSRCInfo ssrcInfo, Device device, String channelId,boolean isSubStream,
               ErrorCallback<Object> callback);
-    SSRCInfo play(MediaServerItem mediaServerItem, String deviceId, String channelId, ErrorCallback<Object> callback);
+    SSRCInfo play(MediaServerItem mediaServerItem, String deviceId, String channelId,boolean isSubStream, ErrorCallback<Object> callback);
 
     MediaServerItem getNewMediaServerItem(Device device);
 
@@ -43,5 +43,5 @@
 
     void resumeRtp(String streamId) throws ServiceException, InvalidArgumentException, ParseException, SipException;
 
-    void getSnap(String deviceId, String channelId, String fileName, ErrorCallback errorCallback);
+    void getSnap(String deviceId, String channelId, String fileName,boolean isSubStream, ErrorCallback errorCallback);
 }
diff --git a/src/main/java/com/genersoft/iot/vmp/service/impl/DeviceServiceImpl.java b/src/main/java/com/genersoft/iot/vmp/service/impl/DeviceServiceImpl.java
index 0adbf9d..fe98d06 100644
--- a/src/main/java/com/genersoft/iot/vmp/service/impl/DeviceServiceImpl.java
+++ b/src/main/java/com/genersoft/iot/vmp/service/impl/DeviceServiceImpl.java
@@ -1,14 +1,17 @@
 package com.genersoft.iot.vmp.service.impl;
 
+import com.genersoft.iot.vmp.common.InviteSessionType;
 import com.genersoft.iot.vmp.common.VideoManagerConstants;
 import com.genersoft.iot.vmp.conf.DynamicTask;
 import com.genersoft.iot.vmp.conf.UserSetting;
+import com.genersoft.iot.vmp.conf.exception.SsrcTransactionNotFoundException;
 import com.genersoft.iot.vmp.gb28181.bean.*;
 import com.genersoft.iot.vmp.gb28181.session.VideoStreamSessionManager;
 import com.genersoft.iot.vmp.gb28181.task.ISubscribeTask;
 import com.genersoft.iot.vmp.gb28181.task.impl.CatalogSubscribeTask;
 import com.genersoft.iot.vmp.gb28181.task.impl.MobilePositionSubscribeTask;
 import com.genersoft.iot.vmp.gb28181.transmit.cmd.ISIPCommander;
+import com.genersoft.iot.vmp.gb28181.transmit.cmd.impl.SIPCommander;
 import com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.response.cmd.CatalogResponseMessageHandler;
 import com.genersoft.iot.vmp.service.IDeviceChannelService;
 import com.genersoft.iot.vmp.service.IDeviceService;
@@ -47,6 +50,8 @@
 
     private final static Logger logger = LoggerFactory.getLogger(DeviceServiceImpl.class);
 
+    @Autowired
+    private SIPCommander cmder;
     @Autowired
     private DynamicTask dynamicTask;
 
@@ -131,6 +136,10 @@
             }
             sync(device);
         }else {
+
+            if (deviceInDb != null) {
+                device.setSwitchPrimarySubStream(deviceInDb.isSwitchPrimarySubStream());
+            }
             if(!device.isOnLine()){
                 device.setOnLine(true);
                 device.setCreateTime(now);
@@ -460,6 +469,22 @@
             logger.warn("鏇存柊璁惧鏃舵湭鎵惧埌璁惧淇℃伅");
             return;
         }
+        if(deviceInStore.isSwitchPrimarySubStream() != device.isSwitchPrimarySubStream()){
+            //褰撲慨鏀硅澶囩殑涓诲瓙鐮佹祦寮�鍏虫椂锛岄渶瑕佹牎楠屾槸鍚﹀瓨鍦ㄦ祦锛屽鏋滃瓨鍦ㄦ祦鍒欑洿鎺ュ叧闂�
+            List<SsrcTransaction> ssrcTransactionForAll = streamSession.getSsrcTransactionForAll(device.getDeviceId(), null, null, null);
+            if(ssrcTransactionForAll != null){
+                for (SsrcTransaction ssrcTransaction: ssrcTransactionForAll) {
+                    try {
+                        cmder.streamByeCmd(device, ssrcTransaction.getChannelId(), ssrcTransaction.getStream(), null, null);
+                    } catch (InvalidArgumentException | SsrcTransactionNotFoundException | ParseException | SipException e) {
+                        throw new RuntimeException(e);
+                    }
+                }
+            }
+            deviceChannelMapper.clearPlay(device.getDeviceId());
+            inviteStreamService.clearInviteInfo(device.getDeviceId());
+        }
+
         if (!ObjectUtils.isEmpty(device.getName())) {
             deviceInStore.setName(device.getName());
         }
diff --git a/src/main/java/com/genersoft/iot/vmp/service/impl/InviteStreamServiceImpl.java b/src/main/java/com/genersoft/iot/vmp/service/impl/InviteStreamServiceImpl.java
index 2fb6816..ed73dd1 100644
--- a/src/main/java/com/genersoft/iot/vmp/service/impl/InviteStreamServiceImpl.java
+++ b/src/main/java/com/genersoft/iot/vmp/service/impl/InviteStreamServiceImpl.java
@@ -198,4 +198,164 @@
         }
         return count;
     }
+
+    /*======================璁惧涓诲瓙鐮佹祦閫昏緫START=========================*/
+
+    @Override
+    public InviteInfo getInviteInfoByDeviceAndChannel(InviteSessionType type, String deviceId, String channelId, boolean isSubStream) {
+        return getInviteInfo(type, deviceId, channelId,isSubStream, null);
+    }
+
+    @Override
+    public void removeInviteInfoByDeviceAndChannel(InviteSessionType inviteSessionType, String deviceId, String channelId, boolean isSubStream) {
+        removeInviteInfo(inviteSessionType, deviceId, channelId,isSubStream, null);
+    }
+
+    @Override
+    public InviteInfo getInviteInfo(InviteSessionType type, String deviceId, String channelId,boolean isSubStream, String stream) {
+        String key = VideoManagerConstants.INVITE_PREFIX +
+                "_" + (type != null ? type : "*") +
+                "_" + (isSubStream ? "sub" : "main") +
+                "_" + (deviceId != null ? deviceId : "*") +
+                "_" + (channelId != null ? channelId : "*") +
+                "_" + (stream != null ? stream : "*");
+        List<Object> scanResult = RedisUtil.scan(redisTemplate, key);
+        if (scanResult.size() != 1) {
+            return null;
+        }
+        return (InviteInfo) redisTemplate.opsForValue().get(scanResult.get(0));
+    }
+
+    @Override
+    public void removeInviteInfo(InviteSessionType type, String deviceId, String channelId, boolean isSubStream, String stream) {
+        String scanKey = VideoManagerConstants.INVITE_PREFIX +
+                "_" + (type != null ? type : "*") +
+                "_" + (isSubStream ? "sub" : "main") +
+                "_" + (deviceId != null ? deviceId : "*") +
+                "_" + (channelId != null ? channelId : "*") +
+                "_" + (stream != null ? stream : "*");
+        List<Object> scanResult = RedisUtil.scan(redisTemplate, scanKey);
+        if (scanResult.size() > 0) {
+            for (Object keyObj : scanResult) {
+                String key = (String) keyObj;
+                InviteInfo inviteInfo = (InviteInfo) redisTemplate.opsForValue().get(key);
+                if (inviteInfo == null) {
+                    continue;
+                }
+                redisTemplate.delete(key);
+                inviteErrorCallbackMap.remove(buildKey(type, deviceId, channelId, inviteInfo.getStream()));
+            }
+        }
+    }
+
+    @Override
+    public void once(InviteSessionType type, String deviceId, String channelId, boolean isSubStream, String stream, ErrorCallback<Object> callback) {
+        String key = buildSubStreamKey(type, deviceId, channelId,isSubStream, stream);
+        List<ErrorCallback<Object>> callbacks = inviteErrorCallbackMap.get(key);
+        if (callbacks == null) {
+            callbacks = new CopyOnWriteArrayList<>();
+            inviteErrorCallbackMap.put(key, callbacks);
+        }
+        callbacks.add(callback);
+    }
+
+    @Override
+    public void call(InviteSessionType type, String deviceId, String channelId, boolean isSubStream, String stream, int code, String msg, Object data) {
+        String key = buildSubStreamKey(type, deviceId, channelId,isSubStream, stream);
+        List<ErrorCallback<Object>> callbacks = inviteErrorCallbackMap.get(key);
+        if (callbacks == null) {
+            return;
+        }
+        for (ErrorCallback<Object> callback : callbacks) {
+            callback.run(code, msg, data);
+        }
+        inviteErrorCallbackMap.remove(key);
+    }
+
+
+    private String buildSubStreamKey(InviteSessionType type, String deviceId, String channelId, boolean isSubStream, String stream) {
+        String key = type + "_" + (isSubStream ? "sub":"main") + "_" +  deviceId + "_" + channelId;
+        // 濡傛灉ssrc涓簄ull閭d箞鍙互瀹炵幇涓�涓�氶亾鍙兘涓�娆℃搷浣滐紝ssrc涓嶄负null鍒欏彲浠ユ敮鎸佷竴涓�氶亾澶氭invite
+        if (stream != null) {
+            key += ("_" + stream);
+        }
+        return key;
+    }
+    @Override
+    public void updateInviteInfoSub(InviteInfo inviteInfo) {
+        if (inviteInfo == null || (inviteInfo.getDeviceId() == null || inviteInfo.getChannelId() == null)) {
+            logger.warn("[鏇存柊Invite淇℃伅]锛屽弬鏁颁笉鍏細 {}", JSON.toJSON(inviteInfo));
+            return;
+        }
+        InviteInfo inviteInfoForUpdate = null;
+
+        if (InviteSessionStatus.ready == inviteInfo.getStatus()) {
+            if (inviteInfo.getDeviceId() == null
+                    || inviteInfo.getChannelId() == null
+                    || inviteInfo.getType() == null
+                    || inviteInfo.getStream() == null
+            ) {
+                return;
+            }
+            inviteInfoForUpdate = inviteInfo;
+        } else {
+            InviteInfo inviteInfoInRedis = getInviteInfo(inviteInfo.getType(), inviteInfo.getDeviceId(),
+                    inviteInfo.getChannelId(),inviteInfo.isSubStream(), inviteInfo.getStream());
+            if (inviteInfoInRedis == null) {
+                logger.warn("[鏇存柊Invite淇℃伅]锛屾湭浠庣紦瀛樹腑璇诲彇鍒癐nvite淇℃伅锛� deviceId: {}, channel: {}, stream: {}",
+                        inviteInfo.getDeviceId(), inviteInfo.getChannelId(), inviteInfo.getStream());
+                return;
+            }
+            if (inviteInfo.getStreamInfo() != null) {
+                inviteInfoInRedis.setStreamInfo(inviteInfo.getStreamInfo());
+            }
+            if (inviteInfo.getSsrcInfo() != null) {
+                inviteInfoInRedis.setSsrcInfo(inviteInfo.getSsrcInfo());
+            }
+            if (inviteInfo.getStreamMode() != null) {
+                inviteInfoInRedis.setStreamMode(inviteInfo.getStreamMode());
+            }
+            if (inviteInfo.getReceiveIp() != null) {
+                inviteInfoInRedis.setReceiveIp(inviteInfo.getReceiveIp());
+            }
+            if (inviteInfo.getReceivePort() != null) {
+                inviteInfoInRedis.setReceivePort(inviteInfo.getReceivePort());
+            }
+            if (inviteInfo.getStatus() != null) {
+                inviteInfoInRedis.setStatus(inviteInfo.getStatus());
+            }
+
+            inviteInfoForUpdate = inviteInfoInRedis;
+
+        }
+        String key = VideoManagerConstants.INVITE_PREFIX +
+                "_" + inviteInfoForUpdate.getType() +
+                "_" + (inviteInfoForUpdate.isSubStream() ? "sub":"main") +
+                "_" + inviteInfoForUpdate.getDeviceId() +
+                "_" + inviteInfoForUpdate.getChannelId() +
+                "_" + inviteInfoForUpdate.getStream();
+        redisTemplate.opsForValue().set(key, inviteInfoForUpdate);
+    }
+
+    @Override
+    public InviteInfo getInviteInfoByStream(InviteSessionType type, String stream, boolean isSubStream) {
+        return getInviteInfo(type, null, null,isSubStream, stream);
+    }
+
+    @Override
+    public List<Object> getInviteInfos(InviteSessionType type, String deviceId, String channelId, String stream) {
+        String key = VideoManagerConstants.INVITE_PREFIX +
+                "_" + (type != null ? type : "*") +
+                "_" + (deviceId != null ? deviceId : "*") +
+                "_" + (channelId != null ? channelId : "*") +
+                "_" + (stream != null ? stream : "*");
+        List<Object> scanResult = RedisUtil.scan(redisTemplate, key);
+        return scanResult;
+    }
+
+    /*======================璁惧涓诲瓙鐮佹祦閫昏緫END=========================*/
+
+
+
+
 }
diff --git a/src/main/java/com/genersoft/iot/vmp/service/impl/PlayServiceImpl.java b/src/main/java/com/genersoft/iot/vmp/service/impl/PlayServiceImpl.java
index 3907895..cebfa1d 100644
--- a/src/main/java/com/genersoft/iot/vmp/service/impl/PlayServiceImpl.java
+++ b/src/main/java/com/genersoft/iot/vmp/service/impl/PlayServiceImpl.java
@@ -18,7 +18,6 @@
 import com.genersoft.iot.vmp.gb28181.transmit.callback.DeferredResultHolder;
 import com.genersoft.iot.vmp.gb28181.transmit.cmd.impl.SIPCommander;
 import com.genersoft.iot.vmp.gb28181.transmit.cmd.impl.SIPCommanderFroPlatform;
-import com.genersoft.iot.vmp.gb28181.utils.SipUtils;
 import com.genersoft.iot.vmp.media.zlm.AssistRESTfulUtils;
 import com.genersoft.iot.vmp.media.zlm.ZLMRESTfulUtils;
 import com.genersoft.iot.vmp.media.zlm.ZLMRTPServerFactory;
@@ -116,28 +115,43 @@
 
 
     @Override
-    public SSRCInfo play(MediaServerItem mediaServerItem, String deviceId, String channelId, ErrorCallback<Object> callback) {
+    public SSRCInfo play(MediaServerItem mediaServerItem, String deviceId, String channelId,boolean isSubStream, ErrorCallback<Object> callback) {
         if (mediaServerItem == null) {
             throw new ControllerException(ErrorCode.ERROR100.getCode(), "鏈壘鍒板彲鐢ㄧ殑zlm");
         }
 
         Device device = redisCatchStorage.getDevice(deviceId);
-        InviteInfo inviteInfo = inviteStreamService.getInviteInfoByDeviceAndChannel(InviteSessionType.PLAY, deviceId, channelId);
-
+        InviteInfo inviteInfo;
+        if(device.isSwitchPrimarySubStream()){
+            inviteInfo = inviteStreamService.getInviteInfoByDeviceAndChannel(InviteSessionType.PLAY, deviceId, channelId,isSubStream);
+        }else {
+            inviteInfo = inviteStreamService.getInviteInfoByDeviceAndChannel(InviteSessionType.PLAY, deviceId, channelId);
+        }
         if (inviteInfo != null ) {
             if (inviteInfo.getStreamInfo() == null) {
                 // 鐐规挱鍙戣捣浜嗕絾鏄皻鏈垚鍔�, 浠呮敞鍐屽洖璋冪瓑寰呯粨鏋滃嵆鍙�
-                inviteStreamService.once(InviteSessionType.PLAY, deviceId, channelId, null, callback);
+                if(device.isSwitchPrimarySubStream()){
+                    inviteStreamService.once(InviteSessionType.PLAY, deviceId, channelId,isSubStream, null, callback);
+                }else {
+                    inviteStreamService.once(InviteSessionType.PLAY, deviceId, channelId, null, callback);
+                }
                 return inviteInfo.getSsrcInfo();
             }else {
                 StreamInfo streamInfo = inviteInfo.getStreamInfo();
                 String streamId = streamInfo.getStream();
                 if (streamId == null) {
                     callback.run(InviteErrorCode.ERROR_FOR_CATCH_DATA.getCode(), "鐐规挱澶辫触锛� redis缂撳瓨streamId绛変簬null", null);
-                    inviteStreamService.call(InviteSessionType.PLAY, device.getDeviceId(), channelId, null,
-                            InviteErrorCode.ERROR_FOR_CATCH_DATA.getCode(),
-                            "鐐规挱澶辫触锛� redis缂撳瓨streamId绛変簬null",
-                            null);
+                    if(device.isSwitchPrimarySubStream()){
+                        inviteStreamService.call(InviteSessionType.PLAY, device.getDeviceId(), channelId,isSubStream, null,
+                                InviteErrorCode.ERROR_FOR_CATCH_DATA.getCode(),
+                                "鐐规挱澶辫触锛� redis缂撳瓨streamId绛変簬null",
+                                null);
+                    }else {
+                        inviteStreamService.call(InviteSessionType.PLAY, device.getDeviceId(), channelId, null,
+                                InviteErrorCode.ERROR_FOR_CATCH_DATA.getCode(),
+                                "鐐规挱澶辫触锛� redis缂撳瓨streamId绛変簬null",
+                                null);
+                    }
                     return inviteInfo.getSsrcInfo();
                 }
                 String mediaServerId = streamInfo.getMediaServerId();
@@ -146,41 +160,64 @@
                 Boolean ready = zlmrtpServerFactory.isStreamReady(mediaInfo, "rtp", streamId);
                 if (ready != null && ready) {
                     callback.run(InviteErrorCode.SUCCESS.getCode(), InviteErrorCode.SUCCESS.getMsg(), streamInfo);
-                    inviteStreamService.call(InviteSessionType.PLAY, device.getDeviceId(), channelId, null,
-                            InviteErrorCode.SUCCESS.getCode(),
-                            InviteErrorCode.SUCCESS.getMsg(),
-                            streamInfo);
+                    if(device.isSwitchPrimarySubStream()){
+                        inviteStreamService.call(InviteSessionType.PLAY, device.getDeviceId(), channelId,isSubStream, null,
+                                InviteErrorCode.SUCCESS.getCode(),
+                                InviteErrorCode.SUCCESS.getMsg(),
+                                streamInfo);
+                    }else {
+                        inviteStreamService.call(InviteSessionType.PLAY, device.getDeviceId(), channelId, null,
+                                InviteErrorCode.SUCCESS.getCode(),
+                                InviteErrorCode.SUCCESS.getMsg(),
+                                streamInfo);
+                    }
                     return inviteInfo.getSsrcInfo();
                 }else {
                     // 鐐规挱鍙戣捣浜嗕絾鏄皻鏈垚鍔�, 浠呮敞鍐屽洖璋冪瓑寰呯粨鏋滃嵆鍙�
-                    inviteStreamService.once(InviteSessionType.PLAY, deviceId, channelId, null, callback);
-                    storager.stopPlay(streamInfo.getDeviceID(), streamInfo.getChannelId());
-                    inviteStreamService.removeInviteInfoByDeviceAndChannel(InviteSessionType.PLAY, deviceId, channelId);
+                    if(device.isSwitchPrimarySubStream()) {
+                        inviteStreamService.once(InviteSessionType.PLAY, deviceId, channelId, null, callback);
+                        storager.stopPlay(streamInfo.getDeviceID(), streamInfo.getChannelId());
+                        inviteStreamService.removeInviteInfoByDeviceAndChannel(InviteSessionType.PLAY, deviceId, channelId);
+                    }else {
+                        inviteStreamService.once(InviteSessionType.PLAY, deviceId, channelId,isSubStream, null, callback);
+                        inviteStreamService.removeInviteInfoByDeviceAndChannel(InviteSessionType.PLAY, deviceId, channelId,isSubStream);
+                    }
                 }
             }
         }
 
         String streamId = null;
         if (mediaServerItem.isRtpEnable()) {
-            streamId = String.format("%s_%s", device.getDeviceId(), channelId);
+            if(device.isSwitchPrimarySubStream()){
+                streamId = StreamInfo.getPlayStream(deviceId, channelId, isSubStream);
+            }else {
+                streamId = String.format("%s_%s", device.getDeviceId(), channelId);
+            }
         }
         SSRCInfo ssrcInfo = mediaServerService.openRTPServer(mediaServerItem, streamId, null, device.isSsrcCheck(),  false, 0, false, device.getStreamModeForParam());
         if (ssrcInfo == null) {
             callback.run(InviteErrorCode.ERROR_FOR_RESOURCE_EXHAUSTION.getCode(), InviteErrorCode.ERROR_FOR_RESOURCE_EXHAUSTION.getMsg(), null);
-            inviteStreamService.call(InviteSessionType.PLAY, device.getDeviceId(), channelId, null,
-                    InviteErrorCode.ERROR_FOR_RESOURCE_EXHAUSTION.getCode(),
-                    InviteErrorCode.ERROR_FOR_RESOURCE_EXHAUSTION.getMsg(),
-                    null);
+            if(device.isSwitchPrimarySubStream()){
+                inviteStreamService.call(InviteSessionType.PLAY, device.getDeviceId(), channelId,isSubStream, null,
+                        InviteErrorCode.ERROR_FOR_RESOURCE_EXHAUSTION.getCode(),
+                        InviteErrorCode.ERROR_FOR_RESOURCE_EXHAUSTION.getMsg(),
+                        null);
+            }else {
+                inviteStreamService.call(InviteSessionType.PLAY, device.getDeviceId(), channelId, null,
+                        InviteErrorCode.ERROR_FOR_RESOURCE_EXHAUSTION.getCode(),
+                        InviteErrorCode.ERROR_FOR_RESOURCE_EXHAUSTION.getMsg(),
+                        null);
+            }
             return null;
         }
         // TODO 璁板綍鐐规挱鐨勭姸鎬�
-        play(mediaServerItem, ssrcInfo, device, channelId, callback);
+        play(mediaServerItem, ssrcInfo, device, channelId,isSubStream, callback);
         return ssrcInfo;
     }
 
 
     @Override
-    public void play(MediaServerItem mediaServerItem, SSRCInfo ssrcInfo, Device device, String channelId,
+    public void play(MediaServerItem mediaServerItem, SSRCInfo ssrcInfo, Device device, String channelId,boolean isSubStream,
                      ErrorCallback<Object> callback) {
 
         if (mediaServerItem == null || ssrcInfo == null) {
@@ -189,21 +226,11 @@
                     null);
             return;
         }
-        logger.info("\r\n" +
-                " [鐐规挱寮�濮媇 \r\n" +
-                "deviceId  : {}, \r\n" +
-                "channelId : {},\r\n" +
-                "鏀舵祦绔彛    : {}, \r\n" +
-                "鏀舵祦妯″紡    : {}, \r\n" +
-                "SSRC      : {}, \r\n" +
-                "SSRC鏍¢獙   锛歿}",
-                device.getDeviceId(),
-                channelId,
-                ssrcInfo.getPort(),
-                device.getStreamMode(),
-                ssrcInfo.getSsrc(),
-                device.isSsrcCheck());
-
+        if( device.isSwitchPrimarySubStream() ){
+            logger.info("[鐐规挱寮�濮媇 deviceId: {}, channelId: {},鐮佹祦绫诲瀷锛歿},鏀舵祦绔彛锛� {}, 鏀舵祦妯″紡锛歿}, SSRC: {}, SSRC鏍¢獙锛歿}", device.getDeviceId(), channelId,isSubStream ? "杈呯爜娴�" : "涓荤爜娴�", ssrcInfo.getPort(), device.getStreamMode(), ssrcInfo.getSsrc(), device.isSsrcCheck());
+        }else {
+            logger.info("[鐐规挱寮�濮媇 deviceId: {}, channelId: {},鏀舵祦绔彛锛� {}, 鏀舵祦妯″紡锛歿}, SSRC: {}, SSRC鏍¢獙锛歿}", device.getDeviceId(), channelId, ssrcInfo.getPort(), device.getStreamMode(), ssrcInfo.getSsrc(), device.isSsrcCheck());
+        }
         //绔彛鑾峰彇澶辫触鐨剆srcInfo 娌℃湁蹇呰鍙戦�佺偣鎾寚浠�
         if (ssrcInfo.getPort() <= 0) {
             logger.info("[鐐规挱绔彛鍒嗛厤寮傚父]锛宒eviceId={},channelId={},ssrcInfo={}", device.getDeviceId(), channelId, ssrcInfo);
@@ -212,23 +239,50 @@
             streamSession.remove(device.getDeviceId(), channelId, ssrcInfo.getStream());
 
             callback.run(InviteErrorCode.ERROR_FOR_RESOURCE_EXHAUSTION.getCode(), "鐐规挱绔彛鍒嗛厤寮傚父", null);
-            inviteStreamService.call(InviteSessionType.PLAY, device.getDeviceId(), channelId, null,
-                    InviteErrorCode.ERROR_FOR_RESOURCE_EXHAUSTION.getCode(), "鐐规挱绔彛鍒嗛厤寮傚父", null);
+            if(device.isSwitchPrimarySubStream()){
+                inviteStreamService.call(InviteSessionType.PLAY, device.getDeviceId(), channelId,isSubStream, null,
+                        InviteErrorCode.ERROR_FOR_RESOURCE_EXHAUSTION.getCode(), "鐐规挱绔彛鍒嗛厤寮傚父", null);
+            }else {
+                inviteStreamService.call(InviteSessionType.PLAY, device.getDeviceId(), channelId, null,
+                        InviteErrorCode.ERROR_FOR_RESOURCE_EXHAUSTION.getCode(), "鐐规挱绔彛鍒嗛厤寮傚父", null);
+            }
             return;
         }
 
         // 鍒濆鍖杛edis涓殑invite娑堟伅鐘舵��
-        InviteInfo inviteInfo = InviteInfo.getinviteInfo(device.getDeviceId(), channelId, ssrcInfo.getStream(), ssrcInfo,
-                mediaServerItem.getSdpIp(), ssrcInfo.getPort(), device.getStreamMode(), InviteSessionType.PLAY,
-                InviteSessionStatus.ready);
-        inviteStreamService.updateInviteInfo(inviteInfo);
+        InviteInfo inviteInfo;
+
+        if(device.isSwitchPrimarySubStream()){
+            // 鍒濆鍖杛edis涓殑invite娑堟伅鐘舵��
+            inviteInfo = InviteInfo.getInviteInfo(device.getDeviceId(), channelId,isSubStream, ssrcInfo.getStream(), ssrcInfo,
+                    mediaServerItem.getSdpIp(), ssrcInfo.getPort(), device.getStreamMode(), InviteSessionType.PLAY,
+                    InviteSessionStatus.ready);
+            inviteStreamService.updateInviteInfoSub(inviteInfo);
+        }else {
+            // 鍒濆鍖杛edis涓殑invite娑堟伅鐘舵��
+            inviteInfo = InviteInfo.getinviteInfo(device.getDeviceId(), channelId, ssrcInfo.getStream(), ssrcInfo,
+                    mediaServerItem.getSdpIp(), ssrcInfo.getPort(), device.getStreamMode(), InviteSessionType.PLAY,
+                    InviteSessionStatus.ready);
+            inviteStreamService.updateInviteInfo(inviteInfo);
+        }
         // 瓒呮椂澶勭悊
         String timeOutTaskKey = UUID.randomUUID().toString();
         dynamicTask.startDelay(timeOutTaskKey, () -> {
             // 鎵ц瓒呮椂浠诲姟鏃舵煡璇㈡槸鍚﹀凡缁忔垚鍔燂紝鎴愬姛浜嗗垯涓嶆墽琛岃秴鏃朵换鍔★紝闃叉瓒呮椂浠诲姟鍙栨秷澶辫触鐨勬儏鍐�
-            InviteInfo inviteInfoForTimeOut = inviteStreamService.getInviteInfoByDeviceAndChannel(InviteSessionType.PLAY, device.getDeviceId(), channelId);
+            InviteInfo inviteInfoForTimeOut;
+            if(device.isSwitchPrimarySubStream()){
+                // 鍒濆鍖杛edis涓殑invite娑堟伅鐘舵��
+                inviteInfoForTimeOut = inviteStreamService.getInviteInfoByDeviceAndChannel(InviteSessionType.PLAY, device.getDeviceId(), channelId,isSubStream);
+            }else {
+                // 鍒濆鍖杛edis涓殑invite娑堟伅鐘舵��
+                inviteInfoForTimeOut = inviteStreamService.getInviteInfoByDeviceAndChannel(InviteSessionType.PLAY, device.getDeviceId(), channelId);
+            }
             if (inviteInfoForTimeOut == null || inviteInfoForTimeOut.getStreamInfo() == null) {
-                logger.info("[鐐规挱瓒呮椂] 鏀舵祦瓒呮椂 deviceId: {}, channelId: {}锛岀鍙o細{}, SSRC: {}", device.getDeviceId(), channelId, ssrcInfo.getPort(), ssrcInfo.getSsrc());
+                if( device.isSwitchPrimarySubStream()){
+                    logger.info("[鐐规挱瓒呮椂] 鏀舵祦瓒呮椂 deviceId: {}, channelId: {},鐮佹祦绫诲瀷锛歿}锛岀鍙o細{}, SSRC: {}", device.getDeviceId(), channelId,isSubStream ? "杈呯爜娴�" : "涓荤爜娴�", ssrcInfo.getPort(), ssrcInfo.getSsrc());
+                }else {
+                    logger.info("[鐐规挱瓒呮椂] 鏀舵祦瓒呮椂 deviceId: {}, channelId: {}锛岀鍙o細{}, SSRC: {}", device.getDeviceId(), channelId, ssrcInfo.getPort(), ssrcInfo.getSsrc());
+                }
                 // 鐐规挱瓒呮椂鍥炲BYE 鍚屾椂閲婃斁ssrc浠ュ強姝ゆ鐐规挱鐨勮祫婧�
 //                InviteInfo inviteInfoForTimeout = inviteStreamService.getInviteInfoByDeviceAndChannel(InviteSessionType.play, device.getDeviceId(), channelId);
 //                if (inviteInfoForTimeout == null) {
@@ -240,10 +294,16 @@
 //                    // TODO 鍙戦�乧ancel
 //                }
                 callback.run(InviteErrorCode.ERROR_FOR_STREAM_TIMEOUT.getCode(), InviteErrorCode.ERROR_FOR_STREAM_TIMEOUT.getMsg(), null);
-                inviteStreamService.call(InviteSessionType.PLAY, device.getDeviceId(), channelId, null,
-                        InviteErrorCode.ERROR_FOR_STREAM_TIMEOUT.getCode(), InviteErrorCode.ERROR_FOR_STREAM_TIMEOUT.getMsg(), null);
+                if( device.isSwitchPrimarySubStream()){
+                    inviteStreamService.call(InviteSessionType.PLAY, device.getDeviceId(), channelId,isSubStream, null,
+                            InviteErrorCode.ERROR_FOR_STREAM_TIMEOUT.getCode(), InviteErrorCode.ERROR_FOR_STREAM_TIMEOUT.getMsg(), null);
+                    inviteStreamService.removeInviteInfoByDeviceAndChannel(InviteSessionType.PLAY, device.getDeviceId(), channelId,isSubStream);
 
-                inviteStreamService.removeInviteInfoByDeviceAndChannel(InviteSessionType.PLAY, device.getDeviceId(), channelId);
+                }else {
+                    inviteStreamService.call(InviteSessionType.PLAY, device.getDeviceId(), channelId, null,
+                            InviteErrorCode.ERROR_FOR_STREAM_TIMEOUT.getCode(), InviteErrorCode.ERROR_FOR_STREAM_TIMEOUT.getMsg(), null);
+                    inviteStreamService.removeInviteInfoByDeviceAndChannel(InviteSessionType.PLAY, device.getDeviceId(), channelId);
+                }
                 try {
                     cmder.streamByeCmd(device, channelId, ssrcInfo.getStream(), null);
                 } catch (InvalidArgumentException | ParseException | SipException | SsrcTransactionNotFoundException e) {
@@ -261,25 +321,42 @@
         }, userSetting.getPlayTimeout());
 
         try {
-            cmder.playStreamCmd(mediaServerItem, ssrcInfo, device, channelId, (MediaServerItem mediaServerItemInuse, JSONObject response) -> {
+            cmder.playStreamCmd(mediaServerItem, ssrcInfo, device, channelId,isSubStream, (MediaServerItem mediaServerItemInuse, JSONObject response) -> {
                 logger.info("鏀跺埌璁㈤槄娑堟伅锛� " + response.toJSONString());
                 dynamicTask.stop(timeOutTaskKey);
                 // hook鍝嶅簲
-                StreamInfo streamInfo = onPublishHandlerForPlay(mediaServerItemInuse, response, device.getDeviceId(), channelId);
+                StreamInfo streamInfo = onPublishHandlerForPlay(mediaServerItemInuse, response, device.getDeviceId(), channelId,isSubStream);
                 if (streamInfo == null){
                     callback.run(InviteErrorCode.ERROR_FOR_STREAM_PARSING_EXCEPTIONS.getCode(),
                             InviteErrorCode.ERROR_FOR_STREAM_PARSING_EXCEPTIONS.getMsg(), null);
-                    inviteStreamService.call(InviteSessionType.PLAY, device.getDeviceId(), channelId, null,
-                            InviteErrorCode.ERROR_FOR_STREAM_PARSING_EXCEPTIONS.getCode(),
-                            InviteErrorCode.ERROR_FOR_STREAM_PARSING_EXCEPTIONS.getMsg(), null);
+                    if( device.isSwitchPrimarySubStream()){
+                        inviteStreamService.call(InviteSessionType.PLAY, device.getDeviceId(), channelId,isSubStream, null,
+                                InviteErrorCode.ERROR_FOR_STREAM_PARSING_EXCEPTIONS.getCode(),
+                                InviteErrorCode.ERROR_FOR_STREAM_PARSING_EXCEPTIONS.getMsg(), null);
+                    }else {
+                        inviteStreamService.call(InviteSessionType.PLAY, device.getDeviceId(), channelId, null,
+                                InviteErrorCode.ERROR_FOR_STREAM_PARSING_EXCEPTIONS.getCode(),
+                                InviteErrorCode.ERROR_FOR_STREAM_PARSING_EXCEPTIONS.getMsg(), null);
+                    }
                     return;
                 }
                 callback.run(InviteErrorCode.SUCCESS.getCode(), InviteErrorCode.SUCCESS.getMsg(), streamInfo);
-                inviteStreamService.call(InviteSessionType.PLAY, device.getDeviceId(), channelId, null,
-                        InviteErrorCode.SUCCESS.getCode(),
-                        InviteErrorCode.SUCCESS.getMsg(),
-                        streamInfo);
-                logger.info("[鐐规挱鎴愬姛] deviceId: {}, channelId: {}", device.getDeviceId(), channelId);
+                if( device.isSwitchPrimarySubStream()){
+                    inviteStreamService.call(InviteSessionType.PLAY, device.getDeviceId(), channelId,isSubStream, null,
+                            InviteErrorCode.SUCCESS.getCode(),
+                            InviteErrorCode.SUCCESS.getMsg(),
+                            streamInfo);
+                }else {
+                    inviteStreamService.call(InviteSessionType.PLAY, device.getDeviceId(), channelId, null,
+                            InviteErrorCode.SUCCESS.getCode(),
+                            InviteErrorCode.SUCCESS.getMsg(),
+                            streamInfo);
+                }
+                if( device.isSwitchPrimarySubStream() ){
+                    logger.info("[鐐规挱鎴愬姛] deviceId: {}, channelId: {},鐮佹祦绫诲瀷锛歿}", device.getDeviceId(), channelId,isSubStream ? "杈呯爜娴�" : "涓荤爜娴�");
+                }else {
+                    logger.info("[鐐规挱鎴愬姛] deviceId: {}, channelId: {}", device.getDeviceId(), channelId);
+                }
                 String streamUrl;
                 if (mediaServerItemInuse.getRtspPort() != 0) {
                     streamUrl = String.format("rtsp://127.0.0.1:%s/%s/%s", mediaServerItemInuse.getRtspPort(), "rtp",  ssrcInfo.getStream());
@@ -298,16 +375,17 @@
                 ResponseEvent responseEvent = (ResponseEvent) event.event;
                 String contentString = new String(responseEvent.getResponse().getRawContent());
                 // 鑾峰彇ssrc
-                String ssrcInResponse = SipUtils.getSsrcFromSdp(contentString);
-
+                int ssrcIndex = contentString.indexOf("y=");
                 // 妫�鏌ユ槸鍚︽湁y瀛楁
-                if (ssrcInResponse != null) {
+                if (ssrcIndex >= 0) {
+                    //ssrc瑙勫畾闀垮害涓�10瀛楄妭锛屼笉鍙栦綑涓嬮暱搴︿互閬垮厤鍚庣画杩樻湁鈥渇=鈥濆瓧娈� TODO 鍚庣画瀵逛笉瑙勮寖鐨勯潪10浣峴src鍏煎
+                    String ssrcInResponse = contentString.substring(ssrcIndex + 2, ssrcIndex + 12).trim();
                     // 鏌ヨ鍒皊src涓嶄竴鑷翠笖寮�鍚簡ssrc鏍¢獙鍒欓渶瑕侀拡瀵瑰鐞�
                     if (ssrcInfo.getSsrc().equals(ssrcInResponse)) {
                         if (device.getStreamMode().equalsIgnoreCase("TCP-ACTIVE")) {
+                            String substring = contentString.substring(0, contentString.indexOf("y="));
                             try {
-                                Gb28181Sdp gb28181Sdp = SipUtils.parseSDP(contentString);
-                                SessionDescription sdp = gb28181Sdp.getBaseSdb();
+                                SessionDescription sdp = SdpFactory.getInstance().createSessionDescription(substring);
                                 int port = -1;
                                 Vector mediaDescriptions = sdp.getMediaDescriptions(true);
                                 for (Object description : mediaDescriptions) {
@@ -334,21 +412,24 @@
 
                                 callback.run(InviteErrorCode.ERROR_FOR_SDP_PARSING_EXCEPTIONS.getCode(),
                                         InviteErrorCode.ERROR_FOR_SDP_PARSING_EXCEPTIONS.getMsg(), null);
-                                inviteStreamService.call(InviteSessionType.PLAY, device.getDeviceId(), channelId, null,
-                                        InviteErrorCode.ERROR_FOR_SDP_PARSING_EXCEPTIONS.getCode(),
-                                        InviteErrorCode.ERROR_FOR_SDP_PARSING_EXCEPTIONS.getMsg(), null);
+                                if(device.isSwitchPrimarySubStream()){
+                                    inviteStreamService.call(InviteSessionType.PLAY, device.getDeviceId(), channelId,isSubStream, null,
+                                            InviteErrorCode.ERROR_FOR_SDP_PARSING_EXCEPTIONS.getCode(),
+                                            InviteErrorCode.ERROR_FOR_SDP_PARSING_EXCEPTIONS.getMsg(), null);
+                                }else {
+                                    inviteStreamService.call(InviteSessionType.PLAY, device.getDeviceId(), channelId, null,
+                                            InviteErrorCode.ERROR_FOR_SDP_PARSING_EXCEPTIONS.getCode(),
+                                            InviteErrorCode.ERROR_FOR_SDP_PARSING_EXCEPTIONS.getMsg(), null);
+                                }
                             }
                         }
                         return;
                     }
                     logger.info("[鐐规挱娑堟伅] 鏀跺埌invite 200, 鍙戠幇涓嬬骇鑷畾涔変簡ssrc: {}", ssrcInResponse);
-
                     if (!mediaServerItem.isRtpEnable() || device.isSsrcCheck()) {
                         logger.info("[鐐规挱娑堟伅] SSRC淇 {}->{}", ssrcInfo.getSsrc(), ssrcInResponse);
-
                         // 閲婃斁ssrc
                         mediaServerService.releaseSsrc(mediaServerItem.getId(), ssrcInfo.getSsrc());
-
                         // 鍗曠鍙fā寮弒treamId涔熸湁鍙樺寲锛岄噸鏂拌缃洃鍚嵆鍙�
                         if (!mediaServerItem.isRtpEnable()) {
                             // 娣诲姞璁㈤槄
@@ -361,21 +442,34 @@
                                 logger.info("[ZLM HOOK] ssrc淇鍚庢敹鍒拌闃呮秷鎭細 " + response.toJSONString());
                                 dynamicTask.stop(timeOutTaskKey);
                                 // hook鍝嶅簲
-                                StreamInfo streamInfo = onPublishHandlerForPlay(mediaServerItemInUse, response, device.getDeviceId(), channelId);
+                                StreamInfo streamInfo = onPublishHandlerForPlay(mediaServerItemInUse, response, device.getDeviceId(), channelId,isSubStream);
                                 if (streamInfo == null){
                                     callback.run(InviteErrorCode.ERROR_FOR_STREAM_PARSING_EXCEPTIONS.getCode(),
                                             InviteErrorCode.ERROR_FOR_STREAM_PARSING_EXCEPTIONS.getMsg(), null);
-                                    inviteStreamService.call(InviteSessionType.PLAY, device.getDeviceId(), channelId, null,
-                                            InviteErrorCode.ERROR_FOR_STREAM_PARSING_EXCEPTIONS.getCode(),
-                                            InviteErrorCode.ERROR_FOR_STREAM_PARSING_EXCEPTIONS.getMsg(), null);
+                                    if( device.isSwitchPrimarySubStream()){
+                                        inviteStreamService.call(InviteSessionType.PLAY, device.getDeviceId(), channelId,isSubStream, null,
+                                                InviteErrorCode.ERROR_FOR_STREAM_PARSING_EXCEPTIONS.getCode(),
+                                                InviteErrorCode.ERROR_FOR_STREAM_PARSING_EXCEPTIONS.getMsg(), null);
+                                    }else {
+                                        inviteStreamService.call(InviteSessionType.PLAY, device.getDeviceId(), channelId, null,
+                                                InviteErrorCode.ERROR_FOR_STREAM_PARSING_EXCEPTIONS.getCode(),
+                                                InviteErrorCode.ERROR_FOR_STREAM_PARSING_EXCEPTIONS.getMsg(), null);
+                                    }
                                     return;
                                 }
                                 callback.run(InviteErrorCode.SUCCESS.getCode(),
                                         InviteErrorCode.SUCCESS.getMsg(), streamInfo);
-                                inviteStreamService.call(InviteSessionType.PLAY, device.getDeviceId(), channelId, null,
-                                        InviteErrorCode.SUCCESS.getCode(),
-                                        InviteErrorCode.SUCCESS.getMsg(),
-                                        streamInfo);
+                                if( device.isSwitchPrimarySubStream()){
+                                    inviteStreamService.call(InviteSessionType.PLAY, device.getDeviceId(), channelId,isSubStream, null,
+                                            InviteErrorCode.SUCCESS.getCode(),
+                                            InviteErrorCode.SUCCESS.getMsg(),
+                                            streamInfo);
+                                }else {
+                                    inviteStreamService.call(InviteSessionType.PLAY, device.getDeviceId(), channelId, null,
+                                            InviteErrorCode.SUCCESS.getCode(),
+                                            InviteErrorCode.SUCCESS.getMsg(),
+                                            streamInfo);
+                                }
                             });
                             return;
                         }
@@ -391,14 +485,22 @@
                             }
 
                             dynamicTask.stop(timeOutTaskKey);
+                            // 閲婃斁ssrc
+                            mediaServerService.releaseSsrc(mediaServerItem.getId(), ssrcInfo.getSsrc());
 
                             streamSession.remove(device.getDeviceId(), channelId, ssrcInfo.getStream());
 
                             callback.run(InviteErrorCode.ERROR_FOR_RESET_SSRC.getCode(),
                                     "涓嬬骇鑷畾涔変簡ssrc,閲嶆柊璁剧疆鏀舵祦淇℃伅澶辫触", null);
-                            inviteStreamService.call(InviteSessionType.PLAY, device.getDeviceId(), channelId, null,
-                                    InviteErrorCode.ERROR_FOR_RESET_SSRC.getCode(),
-                                    "涓嬬骇鑷畾涔変簡ssrc,閲嶆柊璁剧疆鏀舵祦淇℃伅澶辫触", null);
+                            if( device.isSwitchPrimarySubStream()){
+                                inviteStreamService.call(InviteSessionType.PLAY, device.getDeviceId(), channelId,isSubStream, null,
+                                        InviteErrorCode.ERROR_FOR_RESET_SSRC.getCode(),
+                                        "涓嬬骇鑷畾涔変簡ssrc,閲嶆柊璁剧疆鏀舵祦淇℃伅澶辫触", null);
+                            }else {
+                                inviteStreamService.call(InviteSessionType.PLAY, device.getDeviceId(), channelId, null,
+                                        InviteErrorCode.ERROR_FOR_RESET_SSRC.getCode(),
+                                        "涓嬬骇鑷畾涔変簡ssrc,閲嶆柊璁剧疆鏀舵祦淇℃伅澶辫触", null);
+                            }
 
                         }else {
                             ssrcInfo.setSsrc(ssrcInResponse);
@@ -409,7 +511,11 @@
                         logger.info("[鐐规挱娑堟伅] 鏀跺埌invite 200, 涓嬬骇鑷畾涔変簡ssrc, 浣嗘槸褰撳墠妯″紡鏃犻渶淇");
                     }
                 }
-                inviteStreamService.updateInviteInfo(inviteInfo);
+                if(device.isSwitchPrimarySubStream()){
+                    inviteStreamService.updateInviteInfoSub(inviteInfo);
+                }else {
+                    inviteStreamService.updateInviteInfo(inviteInfo);
+                }
             }, (event) -> {
                 dynamicTask.stop(timeOutTaskKey);
                 mediaServerService.closeRTPServer(mediaServerItem, ssrcInfo.getStream());
@@ -420,11 +526,19 @@
 
                 callback.run(InviteErrorCode.ERROR_FOR_SIGNALLING_ERROR.getCode(),
                         String.format("鐐规挱澶辫触锛� 閿欒鐮侊細 %s, %s", event.statusCode, event.msg), null);
-                inviteStreamService.call(InviteSessionType.PLAY, device.getDeviceId(), channelId, null,
-                        InviteErrorCode.ERROR_FOR_RESET_SSRC.getCode(),
-                        String.format("鐐规挱澶辫触锛� 閿欒鐮侊細 %s, %s", event.statusCode, event.msg), null);
+                if( device.isSwitchPrimarySubStream()){
+                    inviteStreamService.call(InviteSessionType.PLAY, device.getDeviceId(), channelId,isSubStream, null,
+                            InviteErrorCode.ERROR_FOR_RESET_SSRC.getCode(),
+                            String.format("鐐规挱澶辫触锛� 閿欒鐮侊細 %s, %s", event.statusCode, event.msg), null);
 
-                inviteStreamService.removeInviteInfoByDeviceAndChannel(InviteSessionType.PLAY, device.getDeviceId(), channelId);
+                    inviteStreamService.removeInviteInfoByDeviceAndChannel(InviteSessionType.PLAY, device.getDeviceId(), channelId,isSubStream);
+                }else {
+                    inviteStreamService.call(InviteSessionType.PLAY, device.getDeviceId(), channelId, null,
+                            InviteErrorCode.ERROR_FOR_RESET_SSRC.getCode(),
+                            String.format("鐐规挱澶辫触锛� 閿欒鐮侊細 %s, %s", event.statusCode, event.msg), null);
+
+                    inviteStreamService.removeInviteInfoByDeviceAndChannel(InviteSessionType.PLAY, device.getDeviceId(), channelId);
+                }
             });
         } catch (InvalidArgumentException | SipException | ParseException e) {
 
@@ -438,27 +552,51 @@
 
             callback.run(InviteErrorCode.ERROR_FOR_SIP_SENDING_FAILED.getCode(),
                     InviteErrorCode.ERROR_FOR_SIP_SENDING_FAILED.getMsg(), null);
-            inviteStreamService.call(InviteSessionType.PLAY, device.getDeviceId(), channelId, null,
-                    InviteErrorCode.ERROR_FOR_SIP_SENDING_FAILED.getCode(),
-                    InviteErrorCode.ERROR_FOR_SIP_SENDING_FAILED.getMsg(), null);
+            if( device.isSwitchPrimarySubStream()){
+                inviteStreamService.call(InviteSessionType.PLAY, device.getDeviceId(), channelId,isSubStream, null,
+                        InviteErrorCode.ERROR_FOR_SIP_SENDING_FAILED.getCode(),
+                        InviteErrorCode.ERROR_FOR_SIP_SENDING_FAILED.getMsg(), null);
 
-            inviteStreamService.removeInviteInfoByDeviceAndChannel(InviteSessionType.PLAY, device.getDeviceId(), channelId);
+                inviteStreamService.removeInviteInfoByDeviceAndChannel(InviteSessionType.PLAY, device.getDeviceId(), channelId,isSubStream);
+            }else {
+                inviteStreamService.call(InviteSessionType.PLAY, device.getDeviceId(), channelId, null,
+                        InviteErrorCode.ERROR_FOR_SIP_SENDING_FAILED.getCode(),
+                        InviteErrorCode.ERROR_FOR_SIP_SENDING_FAILED.getMsg(), null);
+
+                inviteStreamService.removeInviteInfoByDeviceAndChannel(InviteSessionType.PLAY, device.getDeviceId(), channelId);
+            }
         }
     }
 
-    private StreamInfo onPublishHandlerForPlay(MediaServerItem mediaServerItem, JSONObject response, String deviceId, String channelId) {
-        StreamInfo streamInfo = onPublishHandler(mediaServerItem, response, deviceId, channelId);
+    private StreamInfo onPublishHandlerForPlay(MediaServerItem mediaServerItem, JSONObject response, String deviceId, String channelId,boolean isSubStream) {
+        StreamInfo streamInfo = null;
+        Device device = redisCatchStorage.getDevice(deviceId);
+        if( device.isSwitchPrimarySubStream() ){
+            streamInfo = onPublishHandler(mediaServerItem, response, deviceId, channelId,isSubStream);
+        }else {
+            streamInfo = onPublishHandler(mediaServerItem, response, deviceId, channelId);
+        }
         if (streamInfo != null) {
-            DeviceChannel deviceChannel = storager.queryChannel(deviceId, channelId);
-            if (deviceChannel != null) {
-                deviceChannel.setStreamId(streamInfo.getStream());
-                storager.startPlay(deviceId, channelId, streamInfo.getStream());
+            InviteInfo inviteInfo;
+            if(device.isSwitchPrimarySubStream()){
+                inviteInfo = inviteStreamService.getInviteInfoByDeviceAndChannel(InviteSessionType.PLAY, deviceId, channelId,isSubStream);
+            }else {
+                DeviceChannel deviceChannel = storager.queryChannel(deviceId, channelId);
+                if (deviceChannel != null) {
+                    deviceChannel.setStreamId(streamInfo.getStream());
+                    storager.startPlay(deviceId, channelId, streamInfo.getStream());
+                }
+                inviteInfo = inviteStreamService.getInviteInfoByDeviceAndChannel(InviteSessionType.PLAY, deviceId, channelId);
             }
-            InviteInfo inviteInfo = inviteStreamService.getInviteInfoByDeviceAndChannel(InviteSessionType.PLAY, deviceId, channelId);
             if (inviteInfo != null) {
                 inviteInfo.setStatus(InviteSessionStatus.ok);
                 inviteInfo.setStreamInfo(streamInfo);
-                inviteStreamService.updateInviteInfo(inviteInfo);
+                if(device.isSwitchPrimarySubStream()){
+                    inviteStreamService.updateInviteInfoSub(inviteInfo);
+                }else {
+                    inviteStreamService.updateInviteInfo(inviteInfo);
+                }
+
             }
         }
         return streamInfo;
@@ -607,16 +745,17 @@
                         ResponseEvent responseEvent = (ResponseEvent) eventResult.event;
                         String contentString = new String(responseEvent.getResponse().getRawContent());
                         // 鑾峰彇ssrc
-                        String ssrcInResponse = SipUtils.getSsrcFromSdp(contentString);
-
+                        int ssrcIndex = contentString.indexOf("y=");
                         // 妫�鏌ユ槸鍚︽湁y瀛楁
-                        if (ssrcInResponse != null) {
+                        if (ssrcIndex >= 0) {
+                            //ssrc瑙勫畾闀垮害涓�10瀛楄妭锛屼笉鍙栦綑涓嬮暱搴︿互閬垮厤鍚庣画杩樻湁鈥渇=鈥濆瓧娈� TODO 鍚庣画瀵逛笉瑙勮寖鐨勯潪10浣峴src鍏煎
+                            String ssrcInResponse = contentString.substring(ssrcIndex + 2, ssrcIndex + 12);
                             // 鏌ヨ鍒皊src涓嶄竴鑷翠笖寮�鍚簡ssrc鏍¢獙鍒欓渶瑕侀拡瀵瑰鐞�
                             if (ssrcInfo.getSsrc().equals(ssrcInResponse)) {
                                 if (device.getStreamMode().equalsIgnoreCase("TCP-ACTIVE")) {
+                                    String substring = contentString.substring(0, contentString.indexOf("y="));
                                     try {
-                                        Gb28181Sdp gb28181Sdp = SipUtils.parseSDP(contentString);
-                                        SessionDescription sdp = gb28181Sdp.getBaseSdb();
+                                        SessionDescription sdp = SdpFactory.getInstance().createSessionDescription(substring);
                                         int port = -1;
                                         Vector mediaDescriptions = sdp.getMediaDescriptions(true);
                                         for (Object description : mediaDescriptions) {
@@ -684,6 +823,8 @@
                                     }
 
                                     dynamicTask.stop(playBackTimeOutTaskKey);
+                                    // 閲婃斁ssrc
+                                    mediaServerService.releaseSsrc(mediaServerItem.getId(), ssrcInfo.getSsrc());
 
                                     streamSession.remove(device.getDeviceId(), channelId, ssrcInfo.getStream());
 
@@ -799,15 +940,17 @@
                         ResponseEvent responseEvent = (ResponseEvent) eventResult.event;
                         String contentString = new String(responseEvent.getResponse().getRawContent());
                         // 鑾峰彇ssrc
-                        String ssrcInResponse = SipUtils.getSsrcFromSdp(contentString);
+                        int ssrcIndex = contentString.indexOf("y=");
                         // 妫�鏌ユ槸鍚︽湁y瀛楁
-                        if (ssrcInResponse != null) {
+                        if (ssrcIndex >= 0) {
+                            //ssrc瑙勫畾闀垮害涓�10瀛楄妭锛屼笉鍙栦綑涓嬮暱搴︿互閬垮厤鍚庣画杩樻湁鈥渇=鈥濆瓧娈� TODO 鍚庣画瀵逛笉瑙勮寖鐨勯潪10浣峴src鍏煎
+                            String ssrcInResponse = contentString.substring(ssrcIndex + 2, ssrcIndex + 12);
                             // 鏌ヨ鍒皊src涓嶄竴鑷翠笖寮�鍚簡ssrc鏍¢獙鍒欓渶瑕侀拡瀵瑰鐞�
                             if (ssrcInfo.getSsrc().equals(ssrcInResponse)) {
                                 if (device.getStreamMode().equalsIgnoreCase("TCP-ACTIVE")) {
+                                    String substring = contentString.substring(0, contentString.indexOf("y="));
                                     try {
-                                        Gb28181Sdp gb28181Sdp = SipUtils.parseSDP(contentString);
-                                        SessionDescription sdp = gb28181Sdp.getBaseSdb();
+                                        SessionDescription sdp = SdpFactory.getInstance().createSessionDescription(substring);
                                         int port = -1;
                                         Vector mediaDescriptions = sdp.getMediaDescriptions(true);
                                         for (Object description : mediaDescriptions) {
@@ -872,6 +1015,8 @@
                                     }
 
                                     dynamicTask.stop(downLoadTimeOutTaskKey);
+                                    // 閲婃斁ssrc
+                                    mediaServerService.releaseSsrc(mediaServerItem.getId(), ssrcInfo.getSsrc());
 
                                     streamSession.remove(device.getDeviceId(), channelId, ssrcInfo.getStream());
 
@@ -971,6 +1116,7 @@
         streamInfo.setChannelId(channelId);
         return streamInfo;
     }
+
 
     @Override
     public void zlmServerOffline(String mediaServerId) {
@@ -1108,14 +1254,18 @@
     }
 
     @Override
-    public void getSnap(String deviceId, String channelId, String fileName, ErrorCallback errorCallback) {
+    public void getSnap(String deviceId, String channelId, String fileName,boolean isSubStream, ErrorCallback errorCallback) {
         Device device = deviceService.getDevice(deviceId);
         if (device == null) {
             errorCallback.run(InviteErrorCode.ERROR_FOR_PARAMETER_ERROR.getCode(), InviteErrorCode.ERROR_FOR_PARAMETER_ERROR.getMsg(), null);
             return;
         }
-
-        InviteInfo inviteInfo = inviteStreamService.getInviteInfoByDeviceAndChannel(InviteSessionType.PLAY, deviceId, channelId);
+        InviteInfo inviteInfo;
+        if(device.isSwitchPrimarySubStream()){
+             inviteInfo = inviteStreamService.getInviteInfoByDeviceAndChannel(InviteSessionType.PLAY, deviceId, channelId,isSubStream);
+        }else {
+            inviteInfo = inviteStreamService.getInviteInfoByDeviceAndChannel(InviteSessionType.PLAY, deviceId, channelId);
+        }
         if (inviteInfo != null) {
             if (inviteInfo.getStreamInfo() != null) {
                 // 宸插瓨鍦ㄧ嚎鐩存帴鎴浘
@@ -1130,10 +1280,9 @@
                 // 璇锋眰鎴浘
                 logger.info("[璇锋眰鎴浘]: " + fileName);
                 zlmresTfulUtils.getSnap(mediaServerItemInuse, streamUrl, 15, 1, path, fileName);
-                String filePath = path + File.separator + fileName;
                 File snapFile = new File(path + File.separator + fileName);
                 if (snapFile.exists()) {
-                    errorCallback.run(InviteErrorCode.SUCCESS.getCode(), InviteErrorCode.SUCCESS.getMsg(), filePath);
+                    errorCallback.run(InviteErrorCode.SUCCESS.getCode(), InviteErrorCode.SUCCESS.getMsg(), snapFile.getAbsoluteFile());
                 }else {
                     errorCallback.run(InviteErrorCode.FAIL.getCode(), InviteErrorCode.FAIL.getMsg(), null);
                 }
@@ -1142,11 +1291,11 @@
         }
 
         MediaServerItem newMediaServerItem = getNewMediaServerItem(device);
-        play(newMediaServerItem, deviceId, channelId, (code, msg, data)->{
+        play(newMediaServerItem, deviceId, channelId,isSubStream, (code, msg, data)->{
            if (code == InviteErrorCode.SUCCESS.getCode()) {
                InviteInfo inviteInfoForPlay = inviteStreamService.getInviteInfoByDeviceAndChannel(InviteSessionType.PLAY, deviceId, channelId);
                if (inviteInfoForPlay != null && inviteInfoForPlay.getStreamInfo() != null) {
-                   getSnap(deviceId, channelId, fileName, errorCallback);
+                   getSnap(deviceId, channelId, fileName,isSubStream, errorCallback);
                }else {
                    errorCallback.run(InviteErrorCode.FAIL.getCode(), InviteErrorCode.FAIL.getMsg(), null);
                }
@@ -1156,4 +1305,17 @@
         });
     }
 
+
+    /*======================璁惧涓诲瓙鐮佹祦閫昏緫START=========================*/
+    public StreamInfo onPublishHandler(MediaServerItem mediaServerItem, JSONObject resonse, String deviceId, String channelId,boolean isSubStream) {
+        String streamId = resonse.getString("stream");
+        JSONArray tracks = resonse.getJSONArray("tracks");
+        StreamInfo streamInfo = mediaService.getStreamInfoByAppAndStream(mediaServerItem, "rtp", streamId, tracks, null);
+        streamInfo.setDeviceID(deviceId);
+        streamInfo.setChannelId(channelId);
+        streamInfo.setSubStream(isSubStream);
+        return streamInfo;
+    }
+    /*======================璁惧涓诲瓙鐮佹祦閫昏緫END=========================*/
+
 }
diff --git a/src/main/java/com/genersoft/iot/vmp/storager/dao/DeviceChannelMapper.java b/src/main/java/com/genersoft/iot/vmp/storager/dao/DeviceChannelMapper.java
index bd0d74f..333a3af 100644
--- a/src/main/java/com/genersoft/iot/vmp/storager/dao/DeviceChannelMapper.java
+++ b/src/main/java/com/genersoft/iot/vmp/storager/dao/DeviceChannelMapper.java
@@ -451,6 +451,10 @@
     @Select("select count(1) from wvp_device_channel")
     int getAllChannelCount();
 
+    // 璁惧涓诲瓙鐮佹祦閫昏緫START
+    @Update(value = {"UPDATE wvp_device_channel SET stream_id=null WHERE device_id=#{deviceId}"})
+    void clearPlay(String deviceId);
+    // 璁惧涓诲瓙鐮佹祦閫昏緫END
     @Select(value = {" <script>" +
             "select * " +
             "from device_channel " +
@@ -460,4 +464,5 @@
             " <if test='onlyCatalog == true '> and parental = 1 </if>" +
             " </script>"})
     List<DeviceChannel> getSubChannelsByDeviceId(String deviceId, String parentId, boolean onlyCatalog);
+
 }
diff --git a/src/main/java/com/genersoft/iot/vmp/storager/dao/DeviceMapper.java b/src/main/java/com/genersoft/iot/vmp/storager/dao/DeviceMapper.java
index 12e550f..96773fe 100644
--- a/src/main/java/com/genersoft/iot/vmp/storager/dao/DeviceMapper.java
+++ b/src/main/java/com/genersoft/iot/vmp/storager/dao/DeviceMapper.java
@@ -42,6 +42,7 @@
             "geo_coord_sys," +
             "on_line," +
             "media_server_id," +
+            "switch_primary_sub_stream," +
             "(SELECT count(0) FROM wvp_device_channel WHERE device_id=wvp_device.device_id) as channel_count "+
             " FROM wvp_device WHERE device_id = #{deviceId}")
     Device getDeviceByDeviceId(String deviceId);
@@ -157,6 +158,7 @@
             "geo_coord_sys,"+
             "on_line,"+
             "media_server_id,"+
+            "switch_primary_sub_stream switchPrimarySubStream,"+
             "(SELECT count(0) FROM wvp_device_channel WHERE device_id=de.device_id) as channel_count " +
             "FROM wvp_device de" +
             "<if test=\"onLine != null\"> where on_line=${onLine}</if>"+
@@ -246,6 +248,7 @@
             "<if test=\"ssrcCheck != null\">, ssrc_check=#{ssrcCheck}</if>" +
             "<if test=\"asMessageChannel != null\">, as_message_channel=#{asMessageChannel}</if>" +
             "<if test=\"geoCoordSys != null\">, geo_coord_sys=#{geoCoordSys}</if>" +
+            "<if test=\"switchPrimarySubStream != null\">, switch_primary_sub_stream=#{switchPrimarySubStream}</if>" +
             "<if test=\"mediaServerId != null\">, media_server_id=#{mediaServerId}</if>" +
             "WHERE device_id=#{deviceId}"+
             " </script>"})
@@ -263,7 +266,8 @@
             "as_message_channel,"+
             "geo_coord_sys,"+
             "on_line,"+
-            "media_server_id"+
+            "media_server_id,"+
+            "switch_primary_sub_stream"+
             ") VALUES (" +
             "#{deviceId}," +
             "#{name}," +
@@ -276,7 +280,8 @@
             "#{asMessageChannel}," +
             "#{geoCoordSys}," +
             "#{onLine}," +
-            "#{mediaServerId}" +
+            "#{mediaServerId}," +
+            "#{switchPrimarySubStream}" +
             ")")
     void addCustomDevice(Device device);
 
diff --git a/src/main/java/com/genersoft/iot/vmp/vmanager/gb28181/play/PlayController.java b/src/main/java/com/genersoft/iot/vmp/vmanager/gb28181/play/PlayController.java
index 200a7d9..0cf4874 100644
--- a/src/main/java/com/genersoft/iot/vmp/vmanager/gb28181/play/PlayController.java
+++ b/src/main/java/com/genersoft/iot/vmp/vmanager/gb28181/play/PlayController.java
@@ -26,7 +26,6 @@
 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.SnapPath;
 import com.genersoft.iot.vmp.vmanager.bean.StreamContent;
 import com.genersoft.iot.vmp.vmanager.bean.WVPResult;
 import io.swagger.v3.oas.annotations.Operation;
@@ -41,7 +40,6 @@
 import javax.servlet.http.HttpServletRequest;
 import javax.sip.InvalidArgumentException;
 import javax.sip.SipException;
-import java.io.File;
 import java.text.ParseException;
 import java.util.List;
 import java.util.UUID;
@@ -90,16 +88,17 @@
 	@Operation(summary = "寮�濮嬬偣鎾�")
 	@Parameter(name = "deviceId", description = "璁惧鍥芥爣缂栧彿", required = true)
 	@Parameter(name = "channelId", description = "閫氶亾鍥芥爣缂栧彿", required = true)
+	@Parameter(name = "isSubStream", description = "鏄惁瀛愮爜娴侊紙true-瀛愮爜娴侊紝false-涓荤爜娴侊級锛岄粯璁や负false", required = true)
 	@GetMapping("/start/{deviceId}/{channelId}")
 	public DeferredResult<WVPResult<StreamContent>> play(HttpServletRequest request, @PathVariable String deviceId,
-														 @PathVariable String channelId) {
+														 @PathVariable String channelId,boolean isSubStream) {
 
 		// 鑾峰彇鍙敤鐨剒lm
 		Device device = storager.queryVideoDevice(deviceId);
 		MediaServerItem newMediaServerItem = playService.getNewMediaServerItem(device);
 
 		RequestMessage requestMessage = new RequestMessage();
-		String key = DeferredResultHolder.CALLBACK_CMD_PLAY + deviceId + channelId;
+		String key = DeferredResultHolder.getPlayKey(deviceId,channelId,device.isSwitchPrimarySubStream(),isSubStream);
 		requestMessage.setKey(key);
 		String uuid = UUID.randomUUID().toString();
 		requestMessage.setId(uuid);
@@ -118,7 +117,7 @@
 		// 褰曞儚鏌ヨ浠hannelId浣滀负deviceId鏌ヨ
 		resultHolder.put(key, uuid, result);
 
-		playService.play(newMediaServerItem, deviceId, channelId, (code, msg, data) -> {
+		playService.play(newMediaServerItem, deviceId, channelId,isSubStream, (code, msg, data) -> {
 			WVPResult<StreamContent> wvpResult = new WVPResult<>();
 			if (code == InviteErrorCode.SUCCESS.getCode()) {
 				wvpResult.setCode(ErrorCode.SUCCESS.getCode());
@@ -144,8 +143,9 @@
 	@Operation(summary = "鍋滄鐐规挱")
 	@Parameter(name = "deviceId", description = "璁惧鍥芥爣缂栧彿", required = true)
 	@Parameter(name = "channelId", description = "閫氶亾鍥芥爣缂栧彿", required = true)
+	@Parameter(name = "isSubStream", description = "鏄惁瀛愮爜娴侊紙true-瀛愮爜娴侊紝false-涓荤爜娴侊級锛岄粯璁や负false", required = true)
 	@GetMapping("/stop/{deviceId}/{channelId}")
-	public JSONObject playStop(@PathVariable String deviceId, @PathVariable String channelId) {
+	public JSONObject playStop(@PathVariable String deviceId, @PathVariable String channelId,boolean isSubStream) {
 
 		logger.debug(String.format("璁惧棰勮/鍥炴斁鍋滄API璋冪敤锛宻treamId锛�%s_%s", deviceId, channelId ));
 
@@ -158,7 +158,12 @@
 			throw new ControllerException(ErrorCode.ERROR100.getCode(), "璁惧[" + deviceId + "]涓嶅瓨鍦�");
 		}
 
-		InviteInfo inviteInfo = inviteStreamService.getInviteInfoByDeviceAndChannel(InviteSessionType.PLAY, deviceId, channelId);
+		InviteInfo inviteInfo =null;
+		if(device.isSwitchPrimarySubStream()){
+			inviteInfo = inviteStreamService.getInviteInfoByDeviceAndChannel(InviteSessionType.PLAY, deviceId, channelId,isSubStream);
+		}else {
+			inviteInfo = inviteStreamService.getInviteInfoByDeviceAndChannel(InviteSessionType.PLAY, deviceId, channelId);
+		}
 		if (inviteInfo == null) {
 			throw new ControllerException(ErrorCode.ERROR100.getCode(), "鐐规挱鏈壘鍒�");
 		}
@@ -171,12 +176,17 @@
 				throw new ControllerException(ErrorCode.ERROR100.getCode(), "鍛戒护鍙戦�佸け璐�: " + e.getMessage());
 			}
 		}
-		inviteStreamService.removeInviteInfoByDeviceAndChannel(InviteSessionType.PLAY, deviceId, channelId);
+		if(device.isSwitchPrimarySubStream()){
+			inviteStreamService.removeInviteInfoByDeviceAndChannel(InviteSessionType.PLAY, deviceId, channelId,isSubStream);
+		}else {
+			inviteStreamService.removeInviteInfoByDeviceAndChannel(InviteSessionType.PLAY, deviceId, channelId);
+			storager.stopPlay(deviceId, channelId);
+		}
 
-		storager.stopPlay(deviceId, channelId);
 		JSONObject json = new JSONObject();
 		json.put("deviceId", deviceId);
 		json.put("channelId", channelId);
+		json.put("isSubStream", isSubStream);
 		return json;
 	}
 
@@ -343,30 +353,27 @@
 	@Operation(summary = "鑾峰彇鎴浘")
 	@Parameter(name = "deviceId", description = "璁惧鍥芥爣缂栧彿", required = true)
 	@Parameter(name = "channelId", description = "閫氶亾鍥芥爣缂栧彿", required = true)
+	@Parameter(name = "isSubStream", description = "鏄惁瀛愮爜娴侊紙true-瀛愮爜娴侊紝false-涓荤爜娴侊級锛岄粯璁や负false", required = true)
 	@GetMapping("/snap")
-	public DeferredResult<String> getSnap(HttpServletRequest request, String deviceId, String channelId) {
+	public DeferredResult<String> getSnap(String deviceId, String channelId,boolean isSubStream) {
 		if (logger.isDebugEnabled()) {
 			logger.debug("鑾峰彇鎴浘: {}/{}", deviceId, channelId);
 		}
 
+		Device device = storager.queryVideoDevice(deviceId);
 		DeferredResult<String> result = new DeferredResult<>(3 * 1000L);
-		String key  = DeferredResultHolder.CALLBACK_CMD_SNAP + deviceId;
+		String key = DeferredResultHolder.getSnapKey(deviceId,channelId,device.isSwitchPrimarySubStream(),isSubStream);
 		String uuid  = UUID.randomUUID().toString();
 		resultHolder.put(key, uuid,  result);
 
 		RequestMessage message = new RequestMessage();
 		message.setKey(key);
 		message.setId(uuid);
-		String nowForUrl = DateUtil.getNowForUrl();
-		String fileName = deviceId + "_" + channelId + "_" + nowForUrl + ".jpg";
 
-		playService.getSnap(deviceId, channelId, fileName, (code, msg, data) -> {
+		String fileName = deviceId + "_" + channelId + "_" + DateUtil.getNowForUrl() + "jpg";
+		playService.getSnap(deviceId, channelId, fileName,isSubStream, (code, msg, data) -> {
 			if (code == InviteErrorCode.SUCCESS.getCode()) {
-				File snapFile = new File((String)data);
-				String fileNameForUrl = deviceId + "/" + channelId + "?mark=" + nowForUrl;
-				String uri = request.getRequestURL().toString().replace(request.getRequestURI(), "/api/device/query/snap/" + fileNameForUrl);
-				SnapPath snapPath = SnapPath.getInstance((String) data, snapFile.getAbsolutePath(), uri);
-				message.setData(snapPath);
+				message.setData(data);
 			}else {
 				message.setData(WVPResult.fail(code, msg));
 			}
diff --git a/src/main/java/com/genersoft/iot/vmp/web/gb28181/ApiStreamController.java b/src/main/java/com/genersoft/iot/vmp/web/gb28181/ApiStreamController.java
index 556adf6..29c776a 100644
--- a/src/main/java/com/genersoft/iot/vmp/web/gb28181/ApiStreamController.java
+++ b/src/main/java/com/genersoft/iot/vmp/web/gb28181/ApiStreamController.java
@@ -122,7 +122,7 @@
         MediaServerItem newMediaServerItem = playService.getNewMediaServerItem(device);
 
 
-        playService.play(newMediaServerItem, serial, code, (errorCode, msg, data) -> {
+        playService.play(newMediaServerItem, serial, code,false, (errorCode, msg, data) -> {
             if (errorCode == InviteErrorCode.SUCCESS.getCode()) {
                 InviteInfo inviteInfo = inviteStreamService.getInviteInfoByDeviceAndChannel(InviteSessionType.PLAY, serial, code);
                 if (inviteInfo != null && inviteInfo.getStreamInfo() != null) {
diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml
index 04f7742..f5404e2 100644
--- a/src/main/resources/application-dev.yml
+++ b/src/main/resources/application-dev.yml
@@ -1,4 +1,6 @@
 spring:
+  thymeleaf:
+    cache: false
   # [鍙�塢涓婁紶鏂囦欢澶у皬闄愬埗
   servlet:
     multipart:
@@ -11,18 +13,18 @@
     # [蹇呴』淇敼] 绔彛鍙�
     port: 6379
     # [鍙�塢 鏁版嵁搴� DB
-    database: 6
+    database: 7
     # [鍙�塢 璁块棶瀵嗙爜,鑻ヤ綘鐨剅edis鏈嶅姟鍣ㄦ病鏈夎缃瘑鐮侊紝灏变笉闇�瑕佺敤瀵嗙爜鍘昏繛鎺�
-    password: face2020
+    password:
     # [鍙�塢 瓒呮椂鏃堕棿
     timeout: 10000
     # mysql鏁版嵁婧�
   datasource:
     type: com.zaxxer.hikari.HikariDataSource
     driver-class-name: com.mysql.cj.jdbc.Driver
-    url: jdbc:mysql://127.0.0.1:3306/wvp2?useUnicode=true&characterEncoding=UTF8&rewriteBatchedStatements=true&serverTimezone=PRC&useSSL=false&allowMultiQueries=true
+    url: jdbc:mysql://127.0.0.1:3306/test_gb-89wulian?useUnicode=true&characterEncoding=UTF8&rewriteBatchedStatements=true&serverTimezone=PRC&useSSL=false&allowMultiQueries=true
     username: root
-    password: 123456
+    password: root
     hikari:
       connection-timeout: 20000             # 鏄鎴风绛夊緟杩炴帴姹犺繛鎺ョ殑鏈�澶ф绉掓暟
       initialSize: 10                       # 杩炴帴姹犲垵濮嬪寲杩炴帴鏁�
@@ -30,11 +32,19 @@
       minimum-idle: 5                       # 杩炴帴姹犳渶灏忕┖闂茶繛鎺ユ暟
       idle-timeout: 300000                  # 鍏佽杩炴帴鍦ㄨ繛鎺ユ睜涓┖闂茬殑鏈�闀挎椂闂达紙浠ユ绉掍负鍗曚綅锛�
       max-lifetime: 1200000                 # 鏄睜涓繛鎺ュ叧闂悗鐨勬渶闀跨敓鍛藉懆鏈燂紙浠ユ绉掍负鍗曚綅)
-
-
 #[鍙�塢 WVP鐩戝惉鐨凥TTP绔彛, 缃戦〉鍜屾帴鍙h皟鐢ㄩ兘鏄繖涓鍙�
 server:
-  port: 18080
+  port: 18978
+  # [鍙�塢 HTTPS閰嶇疆锛� 榛樿涓嶅紑鍚�
+  ssl:
+    # [鍙�塢 鏄惁寮�鍚疕TTPS璁块棶
+    enabled: false
+    # [鍙�塢 璇佷功鏂囦欢璺緞锛屾斁缃湪resource/鐩綍涓嬪嵆鍙紝淇敼xxx涓烘枃浠跺悕
+    key-store: classpath:test.monitor.89iot.cn.jks
+    # [鍙�塢 璇佷功瀵嗙爜
+    key-store-password: gpf64qmw
+    # [鍙�塢 璇佷功绫诲瀷锛� 榛樿涓簀ks锛屾牴鎹疄闄呬慨鏀�
+    key-store-type: JKS
 
 # 浣滀负28181鏈嶅姟鍣ㄧ殑閰嶇疆
 sip:
@@ -42,26 +52,36 @@
   # 濡傛灉瑕佺洃鍚寮犵綉鍗★紝鍙互浣跨敤閫楀彿鍒嗛殧澶氫釜IP锛� 渚嬪锛� 192.168.1.4,10.0.0.4
   # 濡傛灉涓嶆槑鐧斤紝灏变娇鐢�0.0.0.0锛屽ぇ閮ㄥ垎鎯呭喌閮芥槸鍙互鐨�
   # 璇蜂笉瑕佷娇鐢�127.0.0.1锛屼换浣曞寘鎷琹ocalhost鍦ㄥ唴鐨勫煙鍚嶉兘鏄笉鍙互鐨勩��
-  ip: 192.168.41.16
+  ip: 192.168.1.18
   # [鍙�塢 28181鏈嶅姟鐩戝惉鐨勭鍙�
-  port: 5060
+  port: 8116
   # 鏍规嵁鍥芥爣6.1.2涓瀹氾紝domain瀹滈噰鐢↖D缁熶竴缂栫爜鐨勫墠鍗佷綅缂栫爜銆傚浗鏍囬檮褰旸涓畾涔夊墠8浣嶄负涓績缂栫爜锛堢敱鐪佺骇銆佸競绾с�佸尯绾с�佸熀灞傜紪鍙风粍鎴愶紝鍙傜収GB/T 2260-2007锛�
   # 鍚庝袱浣嶄负琛屼笟缂栫爜锛屽畾涔夊弬鐓ч檮褰旸.3
   # 3701020049鏍囪瘑灞变笢娴庡崡鍘嗕笅鍖� 淇℃伅琛屼笟鎺ュ叆
   # [鍙�塢
-  domain: 4401020049
+  domain: 4101050000
   # [鍙�塢
-  id: 44010200492000000001
+  id: 41010500002000000001
   # [鍙�塢 榛樿璁惧璁よ瘉瀵嗙爜锛屽悗缁墿灞曚娇鐢ㄨ澶囧崟鐙瘑鐮�, 绉婚櫎瀵嗙爜灏嗕笉杩涜鏍¢獙
-  password: admin123
+  password: bajiuwulian1006
+  # 鏄惁瀛樺偍alarm淇℃伅
+  alarm: true
 
 #zlm 榛樿鏈嶅姟鍣ㄩ厤缃�
 media:
-  id: FQ3TF8yT83wh5Wvz
+  id: 89wulian-one
   # [蹇呴』淇敼] zlm鏈嶅姟鍣ㄧ殑鍐呯綉IP
-  ip: 192.168.41.16
+  ip: 192.168.1.18
   # [蹇呴』淇敼] zlm鏈嶅姟鍣ㄧ殑http.port
-  http-port: 8091
+  http-port: 80
+  # [鍙�塢 杩斿洖娴佸湴鍧�鏃剁殑ip锛岀疆绌轰娇鐢� media.ip
+  stream-ip: 192.168.1.18
+  # [鍙�塢 wvp鍦ㄥ浗鏍囦俊浠や腑浣跨敤鐨刬p锛屾ip涓烘憚鍍忔満鍙互璁块棶鍒扮殑ip锛� 缃┖浣跨敤 media.ip
+  sdp-ip: 192.168.1.18
+  # [鍙�塢 zlm鏈嶅姟鍣ㄧ殑hook鎵�浣跨敤鐨処P, 榛樿浣跨敤sip.ip
+  hook-ip: 192.168.1.18
+  # [鍙�塢 zlm鏈嶅姟鍣ㄧ殑http.sslport, 缃┖浣跨敤zlm閰嶇疆鏂囦欢閰嶇疆
+  http-ssl-port: 443
   # [鍙�塢 zlm鏈嶅姟鍣ㄧ殑hook.admin_params=secret
   secret: 035c73f7-bb6b-4889-a715-d9eb2d1925cc
   # 鍚敤澶氱鍙fā寮�, 澶氱鍙fā寮忎娇鐢ㄧ鍙e尯鍒嗘瘡璺祦锛屽吋瀹规�ф洿濂姐�� 鍗曠鍙d娇鐢ㄦ祦鐨剆src鍖哄垎锛� 鐐规挱瓒呮椂寤鸿浣跨敤澶氱鍙f祴璇�
@@ -69,11 +89,24 @@
     # [鍙�塢 鏄惁鍚敤澶氱鍙fā寮�, 寮�鍚悗浼氬湪portRange鑼冨洿鍐呴�夋嫨绔彛鐢ㄤ簬濯掍綋娴佷紶杈�
     enable: true
     # [鍙�塢 鍦ㄦ鑼冨洿鍐呴�夋嫨绔彛鐢ㄤ簬濯掍綋娴佷紶杈�, 蹇呴』鎻愬墠鍦▃lm涓婇厤缃灞炴�э紝涓嶇劧鑷姩閰嶇疆姝ゅ睘鎬у彲鑳戒笉鎴愬姛
-    port-range: 30000,30500 # 绔彛鑼冨洿
+    port-range: 50000,50300 # 绔彛鑼冨洿
     # [鍙�塢 鍥芥爣绾ц仈鍦ㄦ鑼冨洿鍐呴�夋嫨绔彛鍙戦�佸獟浣撴祦,
-    send-port-range: 30000,30500 # 绔彛鑼冨洿
+    send-port-range: 50000,50300 # 绔彛鑼冨洿
   # 褰曞儚杈呭姪鏈嶅姟锛� 閮ㄧ讲姝ゆ湇鍔″彲浠ュ疄鐜皕lm褰曞儚鐨勭鐞嗕笌涓嬭浇锛� 0 琛ㄧず涓嶄娇鐢�
   record-assist-port: 18081
+# [鏍规嵁涓氬姟闇�姹傞厤缃甝
+user-settings:
+  # 鐐规挱/褰曞儚鍥炴斁 绛夊緟瓒呮椂鏃堕棿,鍗曚綅锛氭绉�
+  play-timeout: 180000
+  # [鍙�塢 鑷姩鐐规挱锛� 浣跨敤鍥哄畾娴佸湴鍧�杩涜鎾斁鏃讹紝濡傛灉鏈偣鎾垯鑷姩杩涜鐐规挱, 闇�瑕乺tp.enable=true
+  auto-apply-play: true
+  # 璁惧/閫氶亾鐘舵�佸彉鍖栨椂鍙戦�佹秷鎭�
+  device-status-notify: true
+  # 璺ㄥ煙閰嶇疆锛岄厤缃綘璁块棶鍓嶇椤甸潰鐨勫湴鍧�鍗冲彲锛� 鍙互閰嶇疆澶氫釜
+  allowed-origins:
+    - http://localhost:8080
+    - http://127.0.0.1:8080
 # [鍙�塢 鏃ュ織閰嶇疆, 涓�鑸笉闇�瑕佹敼
 logging:
   config: classpath:logback-spring-local.xml
+
diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml
index 3f47844..80de5ef 100644
--- a/src/main/resources/application.yml
+++ b/src/main/resources/application.yml
@@ -2,4 +2,4 @@
   application:
     name: wvp
   profiles:
-    active: local
\ No newline at end of file
+    active: dev
\ No newline at end of file
diff --git a/web_src/config/index.js b/web_src/config/index.js
index b1e1cbe..a7b6846 100644
--- a/web_src/config/index.js
+++ b/web_src/config/index.js
@@ -12,14 +12,14 @@
     assetsPublicPath: '/',
     proxyTable: {
       '/debug': {
-        target: 'http://localhost:18080',
+        target: 'http://localhost:18978',
         changeOrigin: true,
         pathRewrite: {
           '^/debug': '/'
         }
       },
       '/static/snap': {
-        target: 'http://localhost:18080',
+        target: 'http://localhost:18978',
         changeOrigin: true,
         // pathRewrite: {
         //   '^/static/snap': '/static/snap'
diff --git a/web_src/src/components/channelList.vue b/web_src/src/components/channelList.vue
index 55cbc98..a8e8433 100644
--- a/web_src/src/components/channelList.vue
+++ b/web_src/src/components/channelList.vue
@@ -26,6 +26,12 @@
             <el-option label="鍦ㄧ嚎" value="true"></el-option>
             <el-option label="绂荤嚎" value="false"></el-option>
           </el-select>
+          娓呮櫚搴�:
+          <el-select size="mini" style="margin-right: 1rem;" @change="search" v-model="isSubStream" placeholder="璇烽�夋嫨"
+                     default-first-option>
+            <el-option label="鍘熺敾" :value="false"></el-option>
+            <el-option label="娴佺晠" :value="true"></el-option>
+          </el-select>
         </div>
       <el-button icon="el-icon-refresh-right" circle size="mini" @click="refresh()"></el-button>
       <el-button v-if="showTree" icon="iconfont icon-list" circle size="mini" @click="switchList()"></el-button>
@@ -146,6 +152,7 @@
       searchSrt: "",
       channelType: "",
       online: "",
+      isSubStream: false,
       winHeight: window.innerHeight - 200,
       currentPage: 1,
       count: 15,
@@ -237,7 +244,10 @@
       let that = this;
       this.$axios({
         method: 'get',
-        url: '/api/play/start/' + deviceId + '/' + channelId
+        url: '/api/play/start/' + deviceId + '/' + channelId,
+        params:{
+          isSubStream: this.isSubStream
+        }
       }).then(function (res) {
         console.log(res)
         that.isLoging = false;
@@ -277,7 +287,10 @@
       var that = this;
       this.$axios({
         method: 'get',
-        url: '/api/play/stop/' + this.deviceId + "/" + itemData.channelId
+        url: '/api/play/stop/' + this.deviceId + "/" + itemData.channelId,
+        params:{
+          isSubStream: this.isSubStream
+        }
       }).then(function (res) {
         that.initData();
       }).catch(function (error) {
diff --git a/web_src/src/components/dialog/deviceEdit.vue b/web_src/src/components/dialog/deviceEdit.vue
index 5591183..d833b13 100644
--- a/web_src/src/components/dialog/deviceEdit.vue
+++ b/web_src/src/components/dialog/deviceEdit.vue
@@ -58,6 +58,12 @@
           <el-form-item v-if="form.subscribeCycleForMobilePosition > 0" label="绉诲姩浣嶇疆鎶ラ�侀棿闅�" prop="subscribeCycleForCatalog" >
             <el-input v-model="form.mobilePositionSubmissionInterval" clearable ></el-input>
           </el-form-item>
+          <el-form-item label="涓诲瓙鐮佹祦寮�鍏�" prop="switchPrimarySubStream" >
+            <el-select v-model="form.switchPrimarySubStream" style="float: left; width: 100%" >
+              <el-option key="true" label="寮�鍚�" :value="true"></el-option>
+              <el-option key="false" label="鍏抽棴" :value="false"></el-option>
+            </el-select>
+          </el-form-item>
           <el-form-item label="鍏朵粬閫夐」">
             <el-checkbox label="SSRC鏍¢獙" v-model="form.ssrcCheck" style="float: left"></el-checkbox>
             <el-checkbox label="浣滀负娑堟伅閫氶亾" v-model="form.asMessageChannel" style="float: left"></el-checkbox>

--
Gitblit v1.8.0