Commit fa62ab9a0143433a5d058ab3229a37e4a9a0f696

Authored by 648540858
Committed by GitHub
2 parents b4dadf6c 9e28923b

Merge pull request #893 from sxh-netizen/wvp-28181-2.0

新增设备主子码流开关选择,默认为不开启
Showing 25 changed files with 844 additions and 203 deletions
sql/2.6.6-2.6.7更新.sql
@@ -7,6 +7,7 @@ alter table parent_platform @@ -7,6 +7,7 @@ alter table parent_platform
7 alter table device 7 alter table device
8 add mediaServerId varchar(50) default null; 8 add mediaServerId varchar(50) default null;
9 9
10 - 10 +ALTER TABLE device
  11 + ADD COLUMN `switchPrimarySubStream` bit(1) NOT NULL DEFAULT b'0' COMMENT '开启主子码流切换的开关(0-不开启,1-开启)现在已知支持设备为 大华、TP——LINK全系设备' AFTER `keepalive_interval_time`
11 12
12 13
src/main/java/com/genersoft/iot/vmp/common/InviteInfo.java
1 package com.genersoft.iot.vmp.common; 1 package com.genersoft.iot.vmp.common;
2 2
3 import com.genersoft.iot.vmp.service.bean.SSRCInfo; 3 import com.genersoft.iot.vmp.service.bean.SSRCInfo;
  4 +import io.swagger.v3.oas.annotations.media.Schema;
4 5
5 /** 6 /**
6 * 记录每次发送invite消息的状态 7 * 记录每次发送invite消息的状态
@@ -123,4 +124,40 @@ public class InviteInfo { @@ -123,4 +124,40 @@ public class InviteInfo {
123 public void setStreamMode(String streamMode) { 124 public void setStreamMode(String streamMode) {
124 this.streamMode = streamMode; 125 this.streamMode = streamMode;
125 } 126 }
  127 +
  128 +
  129 + /*=========================设备主子码流逻辑START====================*/
  130 + @Schema(description = "是否为子码流(true-是,false-主码流)")
  131 + private boolean subStream;
  132 +
  133 + public boolean isSubStream() {
  134 + return subStream;
  135 + }
  136 +
  137 + public void setSubStream(boolean subStream) {
  138 + this.subStream = subStream;
  139 + }
  140 +
  141 + public static InviteInfo getInviteInfo(String deviceId, String channelId,Boolean isSubStream, String stream, SSRCInfo ssrcInfo,
  142 + String receiveIp, Integer receivePort, String streamMode,
  143 + InviteSessionType type, InviteSessionStatus status) {
  144 + InviteInfo inviteInfo = new InviteInfo();
  145 + inviteInfo.setDeviceId(deviceId);
  146 + inviteInfo.setChannelId(channelId);
  147 + inviteInfo.setStream(stream);
  148 + inviteInfo.setSsrcInfo(ssrcInfo);
  149 + inviteInfo.setReceiveIp(receiveIp);
  150 + inviteInfo.setReceivePort(receivePort);
  151 + inviteInfo.setStreamMode(streamMode);
  152 + inviteInfo.setType(type);
  153 + inviteInfo.setStatus(status);
  154 + if(isSubStream != null){
  155 + inviteInfo.setSubStream(isSubStream);
  156 + }
  157 + return inviteInfo;
  158 + }
  159 + /*=========================设备主子码流逻辑END====================*/
  160 +
  161 +
  162 +
126 } 163 }
src/main/java/com/genersoft/iot/vmp/common/StreamInfo.java
@@ -528,4 +528,31 @@ public class StreamInfo implements Serializable, Cloneable{ @@ -528,4 +528,31 @@ public class StreamInfo implements Serializable, Cloneable{
528 } 528 }
529 return instance; 529 return instance;
530 } 530 }
  531 +
  532 +
  533 + /*=========================设备主子码流逻辑START====================*/
  534 + @Schema(description = "是否为子码流(true-是,false-主码流)")
  535 + private boolean subStream;
  536 +
  537 + public boolean isSubStream() {
  538 + return subStream;
  539 + }
  540 +
  541 + public void setSubStream(boolean subStream) {
  542 + this.subStream = subStream;
  543 + }
  544 +
  545 + public static String getPlayStream(String deviceId,String channelId,boolean isSubStream){
  546 + String streamId;
  547 + if(isSubStream){
  548 + streamId = String.format("%s_%s_%s","sub",deviceId, channelId);
  549 + }else {
  550 + streamId = String.format("%s_%s_%s","main", deviceId, channelId);
  551 + }
  552 + return streamId;
  553 + }
  554 +
  555 + /*=========================设备主子码流逻辑END====================*/
  556 +
  557 +
531 } 558 }
src/main/java/com/genersoft/iot/vmp/conf/UserSetting.java
1 package com.genersoft.iot.vmp.conf; 1 package com.genersoft.iot.vmp.conf;
2 2
  3 +import io.swagger.v3.oas.annotations.media.Schema;
3 import org.springframework.core.annotation.Order; 4 import org.springframework.core.annotation.Order;
4 import org.springframework.boot.context.properties.ConfigurationProperties; 5 import org.springframework.boot.context.properties.ConfigurationProperties;
5 import org.springframework.stereotype.Component; 6 import org.springframework.stereotype.Component;
@@ -25,11 +26,11 @@ public class UserSetting { @@ -25,11 +26,11 @@ public class UserSetting {
25 26
26 private int platformPlayTimeout = 60000; 27 private int platformPlayTimeout = 60000;
27 28
28 - private Boolean interfaceAuthentication = Boolean.TRUE; 29 + private Boolean interfaceAuthentication = Boolean.FALSE;
29 30
30 - private Boolean recordPushLive = Boolean.TRUE; 31 + private Boolean recordPushLive = Boolean.FALSE;
31 32
32 - private Boolean recordSip = Boolean.TRUE; 33 + private Boolean recordSip = Boolean.FALSE;
33 34
34 private Boolean logInDatebase = Boolean.TRUE; 35 private Boolean logInDatebase = Boolean.TRUE;
35 36
src/main/java/com/genersoft/iot/vmp/gb28181/bean/Device.java
@@ -189,6 +189,8 @@ public class Device { @@ -189,6 +189,8 @@ public class Device {
189 private SipTransactionInfo sipTransactionInfo; 189 private SipTransactionInfo sipTransactionInfo;
190 190
191 191
  192 +
  193 +
192 public String getDeviceId() { 194 public String getDeviceId() {
193 return deviceId; 195 return deviceId;
194 } 196 }
@@ -447,4 +449,20 @@ public class Device { @@ -447,4 +449,20 @@ public class Device {
447 public void setSipTransactionInfo(SipTransactionInfo sipTransactionInfo) { 449 public void setSipTransactionInfo(SipTransactionInfo sipTransactionInfo) {
448 this.sipTransactionInfo = sipTransactionInfo; 450 this.sipTransactionInfo = sipTransactionInfo;
449 } 451 }
  452 +
  453 + /*======================设备主子码流逻辑START=========================*/
  454 + @Schema(description = "开启主子码流切换的开关(false-不开启,true-开启)")
  455 + private boolean switchPrimarySubStream;
  456 +
  457 + public boolean isSwitchPrimarySubStream() {
  458 + return switchPrimarySubStream;
  459 + }
  460 +
  461 + public void setSwitchPrimarySubStream(boolean switchPrimarySubStream) {
  462 + this.switchPrimarySubStream = switchPrimarySubStream;
  463 + }
  464 +
  465 + /*======================设备主子码流逻辑END=========================*/
  466 +
  467 +
450 } 468 }
src/main/java/com/genersoft/iot/vmp/gb28181/transmit/callback/DeferredResultHolder.java
@@ -155,4 +155,30 @@ public class DeferredResultHolder { @@ -155,4 +155,30 @@ public class DeferredResultHolder {
155 map.remove(msg.getKey()); 155 map.remove(msg.getKey());
156 } 156 }
157 } 157 }
  158 +
  159 + /*============================设备主子码流逻辑START========================*/
  160 + public static String getPlayKey(String deviceId,String channelId,boolean deviceSwitchSubStream,boolean isSubStream){
  161 + String key = null;
  162 + if(deviceSwitchSubStream){
  163 + key = CALLBACK_CMD_PLAY + isSubStream + deviceId + channelId;
  164 + }else {
  165 + key = CALLBACK_CMD_PLAY +deviceId + channelId;
  166 + }
  167 + return key;
  168 + }
  169 +
  170 + public static String getSnapKey(String deviceId,String channelId,boolean deviceSwitchSubStream,boolean isSubStream){
  171 + String key = null;
  172 + if(deviceSwitchSubStream){
  173 + key = CALLBACK_CMD_SNAP + isSubStream + deviceId + channelId;
  174 + }else {
  175 + key = CALLBACK_CMD_SNAP +deviceId + channelId;
  176 + }
  177 + return key;
  178 + }
  179 +
  180 +
  181 + /*============================设备主子码流逻辑END========================*/
  182 +
  183 +
158 } 184 }
src/main/java/com/genersoft/iot/vmp/gb28181/transmit/cmd/ISIPCommander.java
@@ -98,7 +98,7 @@ public interface ISIPCommander { @@ -98,7 +98,7 @@ public interface ISIPCommander {
98 * @param device 视频设备 98 * @param device 视频设备
99 * @param channelId 预览通道 99 * @param channelId 预览通道
100 */ 100 */
101 - void playStreamCmd(MediaServerItem mediaServerItem, SSRCInfo ssrcInfo, Device device, String channelId, ZlmHttpHookSubscribe.Event event, SipSubscribe.Event okEvent, SipSubscribe.Event errorEvent) throws InvalidArgumentException, SipException, ParseException; 101 + 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;
102 102
103 /** 103 /**
104 * 请求回放视频流 104 * 请求回放视频流
src/main/java/com/genersoft/iot/vmp/gb28181/transmit/cmd/impl/SIPCommander.java
@@ -266,7 +266,7 @@ public class SIPCommander implements ISIPCommander { @@ -266,7 +266,7 @@ public class SIPCommander implements ISIPCommander {
266 * @param errorEvent sip错误订阅 266 * @param errorEvent sip错误订阅
267 */ 267 */
268 @Override 268 @Override
269 - public void playStreamCmd(MediaServerItem mediaServerItem, SSRCInfo ssrcInfo, Device device, String channelId, 269 + public void playStreamCmd(MediaServerItem mediaServerItem, SSRCInfo ssrcInfo, Device device, String channelId,boolean isSubStream,
270 ZlmHttpHookSubscribe.Event event, SipSubscribe.Event okEvent, SipSubscribe.Event errorEvent) throws InvalidArgumentException, SipException, ParseException { 270 ZlmHttpHookSubscribe.Event event, SipSubscribe.Event okEvent, SipSubscribe.Event errorEvent) throws InvalidArgumentException, SipException, ParseException {
271 String stream = ssrcInfo.getStream(); 271 String stream = ssrcInfo.getStream();
272 272
@@ -341,6 +341,22 @@ public class SIPCommander implements ISIPCommander { @@ -341,6 +341,22 @@ public class SIPCommander implements ISIPCommander {
341 } 341 }
342 } 342 }
343 343
  344 + if( device.isSwitchPrimarySubStream() ){
  345 + if("TP-LINK".equals(device.getManufacturer())){
  346 + if (isSubStream){
  347 + content.append("a=streamMode:sub\r\n");
  348 + }else {
  349 + content.append("a=streamMode:main\r\n");
  350 + }
  351 + }else {
  352 + if (isSubStream){
  353 + content.append("a=streamprofile:1\r\n");
  354 + }else {
  355 + content.append("a=streamprofile:0\r\n");
  356 + }
  357 + }
  358 + }
  359 +
344 content.append("y=" + ssrcInfo.getSsrc() + "\r\n");//ssrc 360 content.append("y=" + ssrcInfo.getSsrc() + "\r\n");//ssrc
345 // f字段:f= v/编码格式/分辨率/帧率/码率类型/码率大小a/编码格式/码率大小/采样率 361 // f字段:f= v/编码格式/分辨率/帧率/码率类型/码率大小a/编码格式/码率大小/采样率
346 // content.append("f=v/2/5/25/1/4000a/1/8/1" + "\r\n"); // 未发现支持此特性的设备 362 // content.append("f=v/2/5/25/1/4000a/1/8/1" + "\r\n"); // 未发现支持此特性的设备
@@ -356,7 +372,11 @@ public class SIPCommander implements ISIPCommander { @@ -356,7 +372,11 @@ public class SIPCommander implements ISIPCommander {
356 // 这里为例避免一个通道的点播只有一个callID这个参数使用一个固定值 372 // 这里为例避免一个通道的点播只有一个callID这个参数使用一个固定值
357 ResponseEvent responseEvent = (ResponseEvent) e.event; 373 ResponseEvent responseEvent = (ResponseEvent) e.event;
358 SIPResponse response = (SIPResponse) responseEvent.getResponse(); 374 SIPResponse response = (SIPResponse) responseEvent.getResponse();
359 - streamSession.put(device.getDeviceId(), channelId, "play", stream, ssrcInfo.getSsrc(), mediaServerItem.getId(), response, InviteSessionType.PLAY); 375 + if(device.isSwitchPrimarySubStream()){
  376 + streamSession.put(device.getDeviceId(), channelId, "switch-play", stream, ssrcInfo.getSsrc(), mediaServerItem.getId(), response, InviteSessionType.PLAY);
  377 + }else {
  378 + streamSession.put(device.getDeviceId(), channelId, "play", stream, ssrcInfo.getSsrc(), mediaServerItem.getId(), response, InviteSessionType.PLAY);
  379 + }
360 okEvent.response(e); 380 okEvent.response(e);
361 }); 381 });
362 } 382 }
src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/ByeRequestProcessor.java
@@ -142,8 +142,13 @@ public class ByeRequestProcessor extends SIPRequestProcessorParent implements In @@ -142,8 +142,13 @@ public class ByeRequestProcessor extends SIPRequestProcessorParent implements In
142 // 可能是设备主动停止 142 // 可能是设备主动停止
143 Device device = storager.queryVideoDeviceByChannelId(platformGbId); 143 Device device = storager.queryVideoDeviceByChannelId(platformGbId);
144 if (device != null) { 144 if (device != null) {
145 - storager.stopPlay(device.getDeviceId(), channelId);  
146 - SsrcTransaction ssrcTransactionForPlay = streamSession.getSsrcTransaction(device.getDeviceId(), channelId, "play", null); 145 + SsrcTransaction ssrcTransactionForPlay = null;
  146 + if (device.isSwitchPrimarySubStream() ) {
  147 + ssrcTransactionForPlay = streamSession.getSsrcTransaction(device.getDeviceId(), channelId, "switch-play", null);
  148 + } else {
  149 + storager.stopPlay(device.getDeviceId(), channelId);
  150 + ssrcTransactionForPlay = streamSession.getSsrcTransaction(device.getDeviceId(), channelId, "play", null);
  151 + }
147 if (ssrcTransactionForPlay != null){ 152 if (ssrcTransactionForPlay != null){
148 if (ssrcTransactionForPlay.getCallId().equals(callIdHeader.getCallId())){ 153 if (ssrcTransactionForPlay.getCallId().equals(callIdHeader.getCallId())){
149 // 释放ssrc 154 // 释放ssrc
@@ -153,10 +158,17 @@ public class ByeRequestProcessor extends SIPRequestProcessorParent implements In @@ -153,10 +158,17 @@ public class ByeRequestProcessor extends SIPRequestProcessorParent implements In
153 } 158 }
154 streamSession.remove(device.getDeviceId(), channelId, ssrcTransactionForPlay.getStream()); 159 streamSession.remove(device.getDeviceId(), channelId, ssrcTransactionForPlay.getStream());
155 } 160 }
156 - InviteInfo inviteInfo = inviteStreamService.getInviteInfoByDeviceAndChannel(InviteSessionType.PLAY, device.getDeviceId(), channelId);  
157 -  
158 - if (inviteInfo != null) { 161 + InviteInfo inviteInfo = null;
  162 + if (device.isSwitchPrimarySubStream() ) {
  163 + String streamType = ssrcTransactionForPlay.getStream().split("_")[0];
  164 + boolean isSubStream = "sub".equals(streamType);
  165 + inviteInfo = inviteStreamService.getInviteInfoByDeviceAndChannel(InviteSessionType.PLAY, device.getDeviceId(), channelId,isSubStream);
  166 + inviteStreamService.removeInviteInfo(inviteInfo.getType(),inviteInfo.getDeviceId(),inviteInfo.getChannelId(),isSubStream,inviteInfo.getStream());
  167 + }else {
  168 + inviteInfo = inviteStreamService.getInviteInfoByDeviceAndChannel(InviteSessionType.PLAY, device.getDeviceId(), channelId);
159 inviteStreamService.removeInviteInfo(inviteInfo); 169 inviteStreamService.removeInviteInfo(inviteInfo);
  170 + }
  171 + if (inviteInfo != null) {
160 if (inviteInfo.getStreamInfo() != null) { 172 if (inviteInfo.getStreamInfo() != null) {
161 mediaServerService.closeRTPServer(inviteInfo.getStreamInfo().getMediaServerId(), inviteInfo.getStream()); 173 mediaServerService.closeRTPServer(inviteInfo.getStreamInfo().getMediaServerId(), inviteInfo.getStream());
162 } 174 }
src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/InviteRequestProcessor.java
@@ -489,7 +489,7 @@ public class InviteRequestProcessor extends SIPRequestProcessorParent implements @@ -489,7 +489,7 @@ public class InviteRequestProcessor extends SIPRequestProcessorParent implements
489 } 489 }
490 sendRtpItem.setStreamId(streamId); 490 sendRtpItem.setStreamId(streamId);
491 redisCatchStorage.updateSendRTPSever(sendRtpItem); 491 redisCatchStorage.updateSendRTPSever(sendRtpItem);
492 - playService.play(mediaServerItem, device.getDeviceId(), channelId, ((code, msg, data) -> { 492 + playService.play(mediaServerItem, device.getDeviceId(), channelId,false, ((code, msg, data) -> {
493 if (code == InviteErrorCode.SUCCESS.getCode()){ 493 if (code == InviteErrorCode.SUCCESS.getCode()){
494 hookEvent.run(code, msg, data); 494 hookEvent.run(code, msg, data);
495 }else if (code == InviteErrorCode.ERROR_FOR_SIGNALLING_TIMEOUT.getCode() || code == InviteErrorCode.ERROR_FOR_STREAM_TIMEOUT.getCode()){ 495 }else if (code == InviteErrorCode.ERROR_FOR_SIGNALLING_TIMEOUT.getCode() || code == InviteErrorCode.ERROR_FOR_STREAM_TIMEOUT.getCode()){
src/main/java/com/genersoft/iot/vmp/media/zlm/ZLMHttpHookListener.java
@@ -289,6 +289,7 @@ public class ZLMHttpHookListener { @@ -289,6 +289,7 @@ public class ZLMHttpHookListener {
289 @ResponseBody 289 @ResponseBody
290 @PostMapping(value = "/on_stream_changed", produces = "application/json;charset=UTF-8") 290 @PostMapping(value = "/on_stream_changed", produces = "application/json;charset=UTF-8")
291 public HookResult onStreamChanged(@RequestBody OnStreamChangedHookParam param) { 291 public HookResult onStreamChanged(@RequestBody OnStreamChangedHookParam param) {
  292 +
292 if (param.isRegist()) { 293 if (param.isRegist()) {
293 logger.info("[ZLM HOOK] 流注册, {}->{}->{}/{}", param.getMediaServerId(), param.getSchema(), param.getApp(), param.getStream()); 294 logger.info("[ZLM HOOK] 流注册, {}->{}->{}/{}", param.getMediaServerId(), param.getSchema(), param.getApp(), param.getStream());
294 } else { 295 } else {
@@ -310,11 +311,13 @@ public class ZLMHttpHookListener { @@ -310,11 +311,13 @@ public class ZLMHttpHookListener {
310 311
311 List<OnStreamChangedHookParam.MediaTrack> tracks = param.getTracks(); 312 List<OnStreamChangedHookParam.MediaTrack> tracks = param.getTracks();
312 // TODO 重构此处逻辑 313 // TODO 重构此处逻辑
  314 + boolean isPush = false;
313 if (param.isRegist()) { 315 if (param.isRegist()) {
314 // 处理流注册的鉴权信息 316 // 处理流注册的鉴权信息
315 if (param.getOriginType() == OriginType.RTMP_PUSH.ordinal() 317 if (param.getOriginType() == OriginType.RTMP_PUSH.ordinal()
316 || param.getOriginType() == OriginType.RTSP_PUSH.ordinal() 318 || param.getOriginType() == OriginType.RTSP_PUSH.ordinal()
317 || param.getOriginType() == OriginType.RTC_PUSH.ordinal()) { 319 || param.getOriginType() == OriginType.RTC_PUSH.ordinal()) {
  320 + isPush = true;
318 StreamAuthorityInfo streamAuthorityInfo = redisCatchStorage.getStreamAuthorityInfo(param.getApp(), param.getStream()); 321 StreamAuthorityInfo streamAuthorityInfo = redisCatchStorage.getStreamAuthorityInfo(param.getApp(), param.getStream());
319 if (streamAuthorityInfo == null) { 322 if (streamAuthorityInfo == null) {
320 streamAuthorityInfo = StreamAuthorityInfo.getInstanceByHook(param); 323 streamAuthorityInfo = StreamAuthorityInfo.getInstanceByHook(param);
@@ -328,7 +331,7 @@ public class ZLMHttpHookListener { @@ -328,7 +331,7 @@ public class ZLMHttpHookListener {
328 redisCatchStorage.removeStreamAuthorityInfo(param.getApp(), param.getStream()); 331 redisCatchStorage.removeStreamAuthorityInfo(param.getApp(), param.getStream());
329 } 332 }
330 333
331 - if ("rtmp".equals(param.getSchema())) { 334 + if ("rtsp".equals(param.getSchema())) {
332 // 更新流媒体负载信息 335 // 更新流媒体负载信息
333 if (param.isRegist()) { 336 if (param.isRegist()) {
334 mediaServerService.addCount(param.getMediaServerId()); 337 mediaServerService.addCount(param.getMediaServerId());
@@ -342,10 +345,19 @@ public class ZLMHttpHookListener { @@ -342,10 +345,19 @@ public class ZLMHttpHookListener {
342 } 345 }
343 346
344 if ("rtp".equals(param.getApp()) && !param.isRegist()) { 347 if ("rtp".equals(param.getApp()) && !param.isRegist()) {
345 - InviteInfo inviteInfo = inviteStreamService.getInviteInfoByStream(null, param.getStream());  
346 - if (inviteInfo != null && (inviteInfo.getType() == InviteSessionType.PLAY || inviteInfo.getType() == InviteSessionType.PLAYBACK)) {  
347 - inviteStreamService.removeInviteInfo(inviteInfo);  
348 - storager.stopPlay(inviteInfo.getDeviceId(), inviteInfo.getChannelId()); 348 + if(param.getStream().split("_").length == 3){
  349 + boolean isSubStream = "sub".equals(param.getStream().split("_")[0]);
  350 + InviteInfo inviteInfo = inviteStreamService.getInviteInfoByStream(null, param.getStream(), isSubStream);
  351 + if(inviteInfo != null && (inviteInfo.getType() == InviteSessionType.PLAY )){
  352 + inviteStreamService.removeInviteInfo(inviteInfo.getType(),inviteInfo.getDeviceId(),
  353 + inviteInfo.getChannelId(),inviteInfo.isSubStream(),inviteInfo.getStream());
  354 + }
  355 + }else {
  356 + InviteInfo inviteInfo = inviteStreamService.getInviteInfoByStream(null, param.getStream());
  357 + if (inviteInfo != null && (inviteInfo.getType() == InviteSessionType.PLAY || inviteInfo.getType() == InviteSessionType.PLAYBACK)) {
  358 + inviteStreamService.removeInviteInfo(inviteInfo);
  359 + storager.stopPlay(inviteInfo.getDeviceId(), inviteInfo.getChannelId());
  360 + }
349 } 361 }
350 } else { 362 } else {
351 if (!"rtp".equals(param.getApp())) { 363 if (!"rtp".equals(param.getApp())) {
@@ -360,8 +372,6 @@ public class ZLMHttpHookListener { @@ -360,8 +372,6 @@ public class ZLMHttpHookListener {
360 StreamInfo streamInfoByAppAndStream = mediaService.getStreamInfoByAppAndStream(mediaInfo, 372 StreamInfo streamInfoByAppAndStream = mediaService.getStreamInfoByAppAndStream(mediaInfo,
361 param.getApp(), param.getStream(), tracks, callId); 373 param.getApp(), param.getStream(), tracks, callId);
362 param.setStreamInfo(new StreamContent(streamInfoByAppAndStream)); 374 param.setStreamInfo(new StreamContent(streamInfoByAppAndStream));
363 - // 如果是拉流代理产生的,不需要写入推流  
364 -  
365 redisCatchStorage.addStream(mediaInfo, type, param.getApp(), param.getStream(), param); 375 redisCatchStorage.addStream(mediaInfo, type, param.getApp(), param.getStream(), param);
366 if (param.getOriginType() == OriginType.RTSP_PUSH.ordinal() 376 if (param.getOriginType() == OriginType.RTSP_PUSH.ordinal()
367 || param.getOriginType() == OriginType.RTMP_PUSH.ordinal() 377 || param.getOriginType() == OriginType.RTMP_PUSH.ordinal()
@@ -450,6 +460,11 @@ public class ZLMHttpHookListener { @@ -450,6 +460,11 @@ public class ZLMHttpHookListener {
450 InviteInfo inviteInfo = inviteStreamService.getInviteInfoByStream(null, param.getStream()); 460 InviteInfo inviteInfo = inviteStreamService.getInviteInfoByStream(null, param.getStream());
451 // 点播 461 // 点播
452 if (inviteInfo != null) { 462 if (inviteInfo != null) {
  463 + // 录像下载
  464 + if (inviteInfo.getType() == InviteSessionType.DOWNLOAD) {
  465 + ret.put("close", false);
  466 + return ret;
  467 + }
453 // 收到无人观看说明流也没有在往上级推送 468 // 收到无人观看说明流也没有在往上级推送
454 if (redisCatchStorage.isChannelSendingRTP(inviteInfo.getChannelId())) { 469 if (redisCatchStorage.isChannelSendingRTP(inviteInfo.getChannelId())) {
455 List<SendRtpItem> sendRtpItems = redisCatchStorage.querySendRTPServerByChnnelId( 470 List<SendRtpItem> sendRtpItems = redisCatchStorage.querySendRTPServerByChnnelId(
@@ -467,27 +482,33 @@ public class ZLMHttpHookListener { @@ -467,27 +482,33 @@ public class ZLMHttpHookListener {
467 } 482 }
468 } 483 }
469 } 484 }
  485 + Device device = deviceService.getDevice(inviteInfo.getDeviceId());
  486 + if (device != null) {
  487 + try {
  488 + InviteInfo info = null;
  489 + if(device.isSwitchPrimarySubStream()){
  490 + boolean isSubStream = "sub".equals(param.getStream().split("_")[0]);
  491 + info = inviteStreamService.getInviteInfo(inviteInfo.getType(), inviteInfo.getDeviceId(), inviteInfo.getChannelId(),isSubStream, inviteInfo.getStream());
  492 + }else {
  493 + info = inviteStreamService.getInviteInfo(inviteInfo.getType(), inviteInfo.getDeviceId(), inviteInfo.getChannelId(), inviteInfo.getStream());
470 494
471 - if (userSetting.getStreamOnDemand()) {  
472 - // 录像下载  
473 - if (inviteInfo.getType() == InviteSessionType.DOWNLOAD) {  
474 - ret.put("close", false);  
475 - return ret;  
476 - } 495 + }
477 496
478 - Device device = deviceService.getDevice(inviteInfo.getDeviceId());  
479 - if (device != null) {  
480 - try {  
481 - if (inviteStreamService.getInviteInfo(inviteInfo.getType(), inviteInfo.getDeviceId(), inviteInfo.getChannelId(), inviteInfo.getStream()) != null) {  
482 - cmder.streamByeCmd(device, inviteInfo.getChannelId(),  
483 - inviteInfo.getStream(), null);  
484 - }  
485 - } catch (InvalidArgumentException | ParseException | SipException |  
486 - SsrcTransactionNotFoundException e) {  
487 - logger.error("[无人观看]点播, 发送BYE失败 {}", e.getMessage()); 497 + if (info != null) {
  498 + cmder.streamByeCmd(device, inviteInfo.getChannelId(),
  499 + inviteInfo.getStream(), null);
488 } 500 }
  501 + } catch (InvalidArgumentException | ParseException | SipException |
  502 + SsrcTransactionNotFoundException e) {
  503 + logger.error("[无人观看]点播, 发送BYE失败 {}", e.getMessage());
489 } 504 }
  505 + }
490 506
  507 + if(device.isSwitchPrimarySubStream()){
  508 + boolean isSubStream = "sub".equals(param.getStream().split("_")[0]);
  509 + inviteStreamService.removeInviteInfo(inviteInfo.getType(), inviteInfo.getDeviceId(),
  510 + inviteInfo.getChannelId(),isSubStream, inviteInfo.getStream());
  511 + }else {
491 inviteStreamService.removeInviteInfo(inviteInfo.getType(), inviteInfo.getDeviceId(), 512 inviteStreamService.removeInviteInfo(inviteInfo.getType(), inviteInfo.getDeviceId(),
492 inviteInfo.getChannelId(), inviteInfo.getStream()); 513 inviteInfo.getChannelId(), inviteInfo.getStream());
493 storager.stopPlay(inviteInfo.getDeviceId(), inviteInfo.getChannelId()); 514 storager.stopPlay(inviteInfo.getDeviceId(), inviteInfo.getChannelId());
@@ -499,7 +520,7 @@ public class ZLMHttpHookListener { @@ -499,7 +520,7 @@ public class ZLMHttpHookListener {
499 // 拉流代理 520 // 拉流代理
500 StreamProxyItem streamProxyItem = streamProxyService.getStreamProxyByAppAndStream(param.getApp(), param.getStream()); 521 StreamProxyItem streamProxyItem = streamProxyService.getStreamProxyByAppAndStream(param.getApp(), param.getStream());
501 if (streamProxyItem != null) { 522 if (streamProxyItem != null) {
502 - if (streamProxyItem.isEnableRemoveNoneReader()) { 523 + if (streamProxyItem.isEnableDisableNoneReader()) {
503 // 无人观看自动移除 524 // 无人观看自动移除
504 ret.put("close", true); 525 ret.put("close", true);
505 streamProxyService.del(param.getApp(), param.getStream()); 526 streamProxyService.del(param.getApp(), param.getStream());
@@ -544,12 +565,26 @@ public class ZLMHttpHookListener { @@ -544,12 +565,26 @@ public class ZLMHttpHookListener {
544 565
545 if ("rtp".equals(param.getApp())) { 566 if ("rtp".equals(param.getApp())) {
546 String[] s = param.getStream().split("_"); 567 String[] s = param.getStream().split("_");
547 - if (!mediaInfo.isRtpEnable() || s.length != 2) { 568 + if (!mediaInfo.isRtpEnable() ) {
  569 + defaultResult.setResult(HookResult.SUCCESS());
  570 + return defaultResult;
  571 + }else if(s.length != 2 && s.length != 3 ){
548 defaultResult.setResult(HookResult.SUCCESS()); 572 defaultResult.setResult(HookResult.SUCCESS());
549 return defaultResult; 573 return defaultResult;
550 } 574 }
551 - String deviceId = s[0];  
552 - String channelId = s[1]; 575 + String deviceId = null;
  576 + String channelId = null;
  577 + boolean isSubStream = false;
  578 + if (s[0].length() < 20) {
  579 + if ("sub".equals(s[0])) {
  580 + isSubStream = true;
  581 + }
  582 + deviceId = s[1];
  583 + channelId = s[2];
  584 + } else {
  585 + deviceId = s[0];
  586 + channelId = s[1];
  587 + }
553 Device device = redisCatchStorage.getDevice(deviceId); 588 Device device = redisCatchStorage.getDevice(deviceId);
554 if (device == null) { 589 if (device == null) {
555 defaultResult.setResult(new HookResult(ErrorCode.ERROR404.getCode(), ErrorCode.ERROR404.getMsg())); 590 defaultResult.setResult(new HookResult(ErrorCode.ERROR404.getCode(), ErrorCode.ERROR404.getMsg()));
@@ -563,7 +598,7 @@ public class ZLMHttpHookListener { @@ -563,7 +598,7 @@ public class ZLMHttpHookListener {
563 logger.info("[ZLM HOOK] 流未找到, 发起自动点播:{}->{}->{}/{}", param.getMediaServerId(), param.getSchema(), param.getApp(), param.getStream()); 598 logger.info("[ZLM HOOK] 流未找到, 发起自动点播:{}->{}->{}/{}", param.getMediaServerId(), param.getSchema(), param.getApp(), param.getStream());
564 599
565 RequestMessage msg = new RequestMessage(); 600 RequestMessage msg = new RequestMessage();
566 - String key = DeferredResultHolder.CALLBACK_CMD_PLAY + deviceId + channelId; 601 + String key = DeferredResultHolder.getPlayKey(deviceId, channelId, device.isSwitchPrimarySubStream(), isSubStream);
567 boolean exist = resultHolder.exist(key, null); 602 boolean exist = resultHolder.exist(key, null);
568 msg.setKey(key); 603 msg.setKey(key);
569 String uuid = UUID.randomUUID().toString(); 604 String uuid = UUID.randomUUID().toString();
@@ -581,7 +616,7 @@ public class ZLMHttpHookListener { @@ -581,7 +616,7 @@ public class ZLMHttpHookListener {
581 resultHolder.put(key, uuid, result); 616 resultHolder.put(key, uuid, result);
582 617
583 if (!exist) { 618 if (!exist) {
584 - playService.play(mediaInfo, deviceId, channelId, (code, message, data) -> { 619 + playService.play(mediaInfo, deviceId, channelId,isSubStream, (code, message, data) -> {
585 msg.setData(new HookResult(code, message)); 620 msg.setData(new HookResult(code, message));
586 resultHolder.invokeResult(msg); 621 resultHolder.invokeResult(msg);
587 }); 622 });
src/main/java/com/genersoft/iot/vmp/service/IInviteStreamService.java
@@ -4,6 +4,8 @@ import com.genersoft.iot.vmp.common.InviteInfo; @@ -4,6 +4,8 @@ import com.genersoft.iot.vmp.common.InviteInfo;
4 import com.genersoft.iot.vmp.common.InviteSessionType; 4 import com.genersoft.iot.vmp.common.InviteSessionType;
5 import com.genersoft.iot.vmp.service.bean.ErrorCallback; 5 import com.genersoft.iot.vmp.service.bean.ErrorCallback;
6 6
  7 +import java.util.List;
  8 +
7 /** 9 /**
8 * 记录国标点播的状态,包括实时预览,下载,录像回放 10 * 记录国标点播的状态,包括实时预览,下载,录像回放
9 */ 11 */
@@ -70,4 +72,50 @@ public interface IInviteStreamService { @@ -70,4 +72,50 @@ public interface IInviteStreamService {
70 * 统计同一个zlm下的国标收流个数 72 * 统计同一个zlm下的国标收流个数
71 */ 73 */
72 int getStreamInfoCount(String mediaServerId); 74 int getStreamInfoCount(String mediaServerId);
  75 +
  76 +
  77 + /*======================设备主子码流逻辑START=========================*/
  78 + /**
  79 + * 获取点播的状态信息
  80 + */
  81 + InviteInfo getInviteInfoByDeviceAndChannel(InviteSessionType type,
  82 + String deviceId,
  83 + String channelId,boolean isSubStream);
  84 +
  85 + void removeInviteInfoByDeviceAndChannel(InviteSessionType inviteSessionType, String deviceId, String channelId,boolean isSubStream);
  86 +
  87 + InviteInfo getInviteInfo(InviteSessionType type,
  88 + String deviceId,
  89 + String channelId,
  90 + boolean isSubStream,
  91 + String stream);
  92 +
  93 + void removeInviteInfo(InviteSessionType type,
  94 + String deviceId,
  95 + String channelId,
  96 + boolean isSubStream,
  97 + String stream);
  98 +
  99 + void once(InviteSessionType type, String deviceId, String channelId,boolean isSubStream, String stream, ErrorCallback<Object> callback);
  100 +
  101 + void call(InviteSessionType type, String deviceId, String channelId,boolean isSubStream, String stream, int code, String msg, Object data);
  102 +
  103 + void updateInviteInfoSub(InviteInfo inviteInfo);
  104 +
  105 + /**
  106 + * 获取点播的状态信息
  107 + */
  108 + InviteInfo getInviteInfoByStream(InviteSessionType type, String stream,boolean isSubStream);
  109 +
  110 + /**
  111 + * 获取点播的状态信息
  112 + */
  113 + List<Object> getInviteInfos(InviteSessionType type,
  114 + String deviceId,
  115 + String channelId,
  116 + String stream);
  117 + /*======================设备主子码流逻辑END=========================*/
  118 +
  119 +
  120 +
73 } 121 }
src/main/java/com/genersoft/iot/vmp/service/IPlayService.java
@@ -16,9 +16,9 @@ import java.text.ParseException; @@ -16,9 +16,9 @@ import java.text.ParseException;
16 */ 16 */
17 public interface IPlayService { 17 public interface IPlayService {
18 18
19 - void play(MediaServerItem mediaServerItem, SSRCInfo ssrcInfo, Device device, String channelId, 19 + void play(MediaServerItem mediaServerItem, SSRCInfo ssrcInfo, Device device, String channelId,boolean isSubStream,
20 ErrorCallback<Object> callback); 20 ErrorCallback<Object> callback);
21 - SSRCInfo play(MediaServerItem mediaServerItem, String deviceId, String channelId, ErrorCallback<Object> callback); 21 + SSRCInfo play(MediaServerItem mediaServerItem, String deviceId, String channelId,boolean isSubStream, ErrorCallback<Object> callback);
22 22
23 MediaServerItem getNewMediaServerItem(Device device); 23 MediaServerItem getNewMediaServerItem(Device device);
24 24
@@ -43,5 +43,5 @@ public interface IPlayService { @@ -43,5 +43,5 @@ public interface IPlayService {
43 43
44 void resumeRtp(String streamId) throws ServiceException, InvalidArgumentException, ParseException, SipException; 44 void resumeRtp(String streamId) throws ServiceException, InvalidArgumentException, ParseException, SipException;
45 45
46 - void getSnap(String deviceId, String channelId, String fileName, ErrorCallback errorCallback); 46 + void getSnap(String deviceId, String channelId, String fileName,boolean isSubStream, ErrorCallback errorCallback);
47 } 47 }
src/main/java/com/genersoft/iot/vmp/service/impl/DeviceServiceImpl.java
1 package com.genersoft.iot.vmp.service.impl; 1 package com.genersoft.iot.vmp.service.impl;
2 2
  3 +import com.genersoft.iot.vmp.common.InviteSessionType;
3 import com.genersoft.iot.vmp.common.VideoManagerConstants; 4 import com.genersoft.iot.vmp.common.VideoManagerConstants;
4 import com.genersoft.iot.vmp.conf.DynamicTask; 5 import com.genersoft.iot.vmp.conf.DynamicTask;
5 import com.genersoft.iot.vmp.conf.UserSetting; 6 import com.genersoft.iot.vmp.conf.UserSetting;
  7 +import com.genersoft.iot.vmp.conf.exception.SsrcTransactionNotFoundException;
6 import com.genersoft.iot.vmp.gb28181.bean.*; 8 import com.genersoft.iot.vmp.gb28181.bean.*;
7 import com.genersoft.iot.vmp.gb28181.session.VideoStreamSessionManager; 9 import com.genersoft.iot.vmp.gb28181.session.VideoStreamSessionManager;
8 import com.genersoft.iot.vmp.gb28181.task.ISubscribeTask; 10 import com.genersoft.iot.vmp.gb28181.task.ISubscribeTask;
9 import com.genersoft.iot.vmp.gb28181.task.impl.CatalogSubscribeTask; 11 import com.genersoft.iot.vmp.gb28181.task.impl.CatalogSubscribeTask;
10 import com.genersoft.iot.vmp.gb28181.task.impl.MobilePositionSubscribeTask; 12 import com.genersoft.iot.vmp.gb28181.task.impl.MobilePositionSubscribeTask;
11 import com.genersoft.iot.vmp.gb28181.transmit.cmd.ISIPCommander; 13 import com.genersoft.iot.vmp.gb28181.transmit.cmd.ISIPCommander;
  14 +import com.genersoft.iot.vmp.gb28181.transmit.cmd.impl.SIPCommander;
12 import com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.response.cmd.CatalogResponseMessageHandler; 15 import com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.response.cmd.CatalogResponseMessageHandler;
13 import com.genersoft.iot.vmp.service.IDeviceChannelService; 16 import com.genersoft.iot.vmp.service.IDeviceChannelService;
14 import com.genersoft.iot.vmp.service.IDeviceService; 17 import com.genersoft.iot.vmp.service.IDeviceService;
@@ -48,6 +51,8 @@ public class DeviceServiceImpl implements IDeviceService { @@ -48,6 +51,8 @@ public class DeviceServiceImpl implements IDeviceService {
48 private final static Logger logger = LoggerFactory.getLogger(DeviceServiceImpl.class); 51 private final static Logger logger = LoggerFactory.getLogger(DeviceServiceImpl.class);
49 52
50 @Autowired 53 @Autowired
  54 + private SIPCommander cmder;
  55 + @Autowired
51 private DynamicTask dynamicTask; 56 private DynamicTask dynamicTask;
52 57
53 @Autowired 58 @Autowired
@@ -131,6 +136,10 @@ public class DeviceServiceImpl implements IDeviceService { @@ -131,6 +136,10 @@ public class DeviceServiceImpl implements IDeviceService {
131 } 136 }
132 sync(device); 137 sync(device);
133 }else { 138 }else {
  139 +
  140 + if (deviceInDb != null) {
  141 + device.setSwitchPrimarySubStream(deviceInDb.isSwitchPrimarySubStream());
  142 + }
134 if(!device.isOnLine()){ 143 if(!device.isOnLine()){
135 device.setOnLine(true); 144 device.setOnLine(true);
136 device.setCreateTime(now); 145 device.setCreateTime(now);
@@ -460,6 +469,22 @@ public class DeviceServiceImpl implements IDeviceService { @@ -460,6 +469,22 @@ public class DeviceServiceImpl implements IDeviceService {
460 logger.warn("更新设备时未找到设备信息"); 469 logger.warn("更新设备时未找到设备信息");
461 return; 470 return;
462 } 471 }
  472 + if(deviceInStore.isSwitchPrimarySubStream() != device.isSwitchPrimarySubStream()){
  473 + //当修改设备的主子码流开关时,需要校验是否存在流,如果存在流则直接关闭
  474 + List<SsrcTransaction> ssrcTransactionForAll = streamSession.getSsrcTransactionForAll(device.getDeviceId(), null, null, null);
  475 + if(ssrcTransactionForAll != null){
  476 + for (SsrcTransaction ssrcTransaction: ssrcTransactionForAll) {
  477 + try {
  478 + cmder.streamByeCmd(device, ssrcTransaction.getChannelId(), ssrcTransaction.getStream(), null, null);
  479 + } catch (InvalidArgumentException | SsrcTransactionNotFoundException | ParseException | SipException e) {
  480 + throw new RuntimeException(e);
  481 + }
  482 + }
  483 + }
  484 + deviceChannelMapper.clearPlay(device.getDeviceId());
  485 + inviteStreamService.clearInviteInfo(device.getDeviceId());
  486 + }
  487 +
463 if (!ObjectUtils.isEmpty(device.getName())) { 488 if (!ObjectUtils.isEmpty(device.getName())) {
464 deviceInStore.setName(device.getName()); 489 deviceInStore.setName(device.getName());
465 } 490 }
src/main/java/com/genersoft/iot/vmp/service/impl/InviteStreamServiceImpl.java
@@ -198,4 +198,164 @@ public class InviteStreamServiceImpl implements IInviteStreamService { @@ -198,4 +198,164 @@ public class InviteStreamServiceImpl implements IInviteStreamService {
198 } 198 }
199 return count; 199 return count;
200 } 200 }
  201 +
  202 + /*======================设备主子码流逻辑START=========================*/
  203 +
  204 + @Override
  205 + public InviteInfo getInviteInfoByDeviceAndChannel(InviteSessionType type, String deviceId, String channelId, boolean isSubStream) {
  206 + return getInviteInfo(type, deviceId, channelId,isSubStream, null);
  207 + }
  208 +
  209 + @Override
  210 + public void removeInviteInfoByDeviceAndChannel(InviteSessionType inviteSessionType, String deviceId, String channelId, boolean isSubStream) {
  211 + removeInviteInfo(inviteSessionType, deviceId, channelId,isSubStream, null);
  212 + }
  213 +
  214 + @Override
  215 + public InviteInfo getInviteInfo(InviteSessionType type, String deviceId, String channelId,boolean isSubStream, String stream) {
  216 + String key = VideoManagerConstants.INVITE_PREFIX +
  217 + "_" + (type != null ? type : "*") +
  218 + "_" + (isSubStream ? "sub" : "main") +
  219 + "_" + (deviceId != null ? deviceId : "*") +
  220 + "_" + (channelId != null ? channelId : "*") +
  221 + "_" + (stream != null ? stream : "*");
  222 + List<Object> scanResult = RedisUtil.scan(redisTemplate, key);
  223 + if (scanResult.size() != 1) {
  224 + return null;
  225 + }
  226 + return (InviteInfo) redisTemplate.opsForValue().get(scanResult.get(0));
  227 + }
  228 +
  229 + @Override
  230 + public void removeInviteInfo(InviteSessionType type, String deviceId, String channelId, boolean isSubStream, String stream) {
  231 + String scanKey = VideoManagerConstants.INVITE_PREFIX +
  232 + "_" + (type != null ? type : "*") +
  233 + "_" + (isSubStream ? "sub" : "main") +
  234 + "_" + (deviceId != null ? deviceId : "*") +
  235 + "_" + (channelId != null ? channelId : "*") +
  236 + "_" + (stream != null ? stream : "*");
  237 + List<Object> scanResult = RedisUtil.scan(redisTemplate, scanKey);
  238 + if (scanResult.size() > 0) {
  239 + for (Object keyObj : scanResult) {
  240 + String key = (String) keyObj;
  241 + InviteInfo inviteInfo = (InviteInfo) redisTemplate.opsForValue().get(key);
  242 + if (inviteInfo == null) {
  243 + continue;
  244 + }
  245 + redisTemplate.delete(key);
  246 + inviteErrorCallbackMap.remove(buildKey(type, deviceId, channelId, inviteInfo.getStream()));
  247 + }
  248 + }
  249 + }
  250 +
  251 + @Override
  252 + public void once(InviteSessionType type, String deviceId, String channelId, boolean isSubStream, String stream, ErrorCallback<Object> callback) {
  253 + String key = buildSubStreamKey(type, deviceId, channelId,isSubStream, stream);
  254 + List<ErrorCallback<Object>> callbacks = inviteErrorCallbackMap.get(key);
  255 + if (callbacks == null) {
  256 + callbacks = new CopyOnWriteArrayList<>();
  257 + inviteErrorCallbackMap.put(key, callbacks);
  258 + }
  259 + callbacks.add(callback);
  260 + }
  261 +
  262 + @Override
  263 + public void call(InviteSessionType type, String deviceId, String channelId, boolean isSubStream, String stream, int code, String msg, Object data) {
  264 + String key = buildSubStreamKey(type, deviceId, channelId,isSubStream, stream);
  265 + List<ErrorCallback<Object>> callbacks = inviteErrorCallbackMap.get(key);
  266 + if (callbacks == null) {
  267 + return;
  268 + }
  269 + for (ErrorCallback<Object> callback : callbacks) {
  270 + callback.run(code, msg, data);
  271 + }
  272 + inviteErrorCallbackMap.remove(key);
  273 + }
  274 +
  275 +
  276 + private String buildSubStreamKey(InviteSessionType type, String deviceId, String channelId, boolean isSubStream, String stream) {
  277 + String key = type + "_" + (isSubStream ? "sub":"main") + "_" + deviceId + "_" + channelId;
  278 + // 如果ssrc为null那么可以实现一个通道只能一次操作,ssrc不为null则可以支持一个通道多次invite
  279 + if (stream != null) {
  280 + key += ("_" + stream);
  281 + }
  282 + return key;
  283 + }
  284 + @Override
  285 + public void updateInviteInfoSub(InviteInfo inviteInfo) {
  286 + if (inviteInfo == null || (inviteInfo.getDeviceId() == null || inviteInfo.getChannelId() == null)) {
  287 + logger.warn("[更新Invite信息],参数不全: {}", JSON.toJSON(inviteInfo));
  288 + return;
  289 + }
  290 + InviteInfo inviteInfoForUpdate = null;
  291 +
  292 + if (InviteSessionStatus.ready == inviteInfo.getStatus()) {
  293 + if (inviteInfo.getDeviceId() == null
  294 + || inviteInfo.getChannelId() == null
  295 + || inviteInfo.getType() == null
  296 + || inviteInfo.getStream() == null
  297 + ) {
  298 + return;
  299 + }
  300 + inviteInfoForUpdate = inviteInfo;
  301 + } else {
  302 + InviteInfo inviteInfoInRedis = getInviteInfo(inviteInfo.getType(), inviteInfo.getDeviceId(),
  303 + inviteInfo.getChannelId(),inviteInfo.isSubStream(), inviteInfo.getStream());
  304 + if (inviteInfoInRedis == null) {
  305 + logger.warn("[更新Invite信息],未从缓存中读取到Invite信息: deviceId: {}, channel: {}, stream: {}",
  306 + inviteInfo.getDeviceId(), inviteInfo.getChannelId(), inviteInfo.getStream());
  307 + return;
  308 + }
  309 + if (inviteInfo.getStreamInfo() != null) {
  310 + inviteInfoInRedis.setStreamInfo(inviteInfo.getStreamInfo());
  311 + }
  312 + if (inviteInfo.getSsrcInfo() != null) {
  313 + inviteInfoInRedis.setSsrcInfo(inviteInfo.getSsrcInfo());
  314 + }
  315 + if (inviteInfo.getStreamMode() != null) {
  316 + inviteInfoInRedis.setStreamMode(inviteInfo.getStreamMode());
  317 + }
  318 + if (inviteInfo.getReceiveIp() != null) {
  319 + inviteInfoInRedis.setReceiveIp(inviteInfo.getReceiveIp());
  320 + }
  321 + if (inviteInfo.getReceivePort() != null) {
  322 + inviteInfoInRedis.setReceivePort(inviteInfo.getReceivePort());
  323 + }
  324 + if (inviteInfo.getStatus() != null) {
  325 + inviteInfoInRedis.setStatus(inviteInfo.getStatus());
  326 + }
  327 +
  328 + inviteInfoForUpdate = inviteInfoInRedis;
  329 +
  330 + }
  331 + String key = VideoManagerConstants.INVITE_PREFIX +
  332 + "_" + inviteInfoForUpdate.getType() +
  333 + "_" + (inviteInfoForUpdate.isSubStream() ? "sub":"main") +
  334 + "_" + inviteInfoForUpdate.getDeviceId() +
  335 + "_" + inviteInfoForUpdate.getChannelId() +
  336 + "_" + inviteInfoForUpdate.getStream();
  337 + redisTemplate.opsForValue().set(key, inviteInfoForUpdate);
  338 + }
  339 +
  340 + @Override
  341 + public InviteInfo getInviteInfoByStream(InviteSessionType type, String stream, boolean isSubStream) {
  342 + return getInviteInfo(type, null, null,isSubStream, stream);
  343 + }
  344 +
  345 + @Override
  346 + public List<Object> getInviteInfos(InviteSessionType type, String deviceId, String channelId, String stream) {
  347 + String key = VideoManagerConstants.INVITE_PREFIX +
  348 + "_" + (type != null ? type : "*") +
  349 + "_" + (deviceId != null ? deviceId : "*") +
  350 + "_" + (channelId != null ? channelId : "*") +
  351 + "_" + (stream != null ? stream : "*");
  352 + List<Object> scanResult = RedisUtil.scan(redisTemplate, key);
  353 + return scanResult;
  354 + }
  355 +
  356 + /*======================设备主子码流逻辑END=========================*/
  357 +
  358 +
  359 +
  360 +
201 } 361 }
src/main/java/com/genersoft/iot/vmp/service/impl/PlayServiceImpl.java
@@ -18,7 +18,6 @@ import com.genersoft.iot.vmp.gb28181.session.VideoStreamSessionManager; @@ -18,7 +18,6 @@ import com.genersoft.iot.vmp.gb28181.session.VideoStreamSessionManager;
18 import com.genersoft.iot.vmp.gb28181.transmit.callback.DeferredResultHolder; 18 import com.genersoft.iot.vmp.gb28181.transmit.callback.DeferredResultHolder;
19 import com.genersoft.iot.vmp.gb28181.transmit.cmd.impl.SIPCommander; 19 import com.genersoft.iot.vmp.gb28181.transmit.cmd.impl.SIPCommander;
20 import com.genersoft.iot.vmp.gb28181.transmit.cmd.impl.SIPCommanderFroPlatform; 20 import com.genersoft.iot.vmp.gb28181.transmit.cmd.impl.SIPCommanderFroPlatform;
21 -import com.genersoft.iot.vmp.gb28181.utils.SipUtils;  
22 import com.genersoft.iot.vmp.media.zlm.AssistRESTfulUtils; 21 import com.genersoft.iot.vmp.media.zlm.AssistRESTfulUtils;
23 import com.genersoft.iot.vmp.media.zlm.ZLMRESTfulUtils; 22 import com.genersoft.iot.vmp.media.zlm.ZLMRESTfulUtils;
24 import com.genersoft.iot.vmp.media.zlm.ZLMRTPServerFactory; 23 import com.genersoft.iot.vmp.media.zlm.ZLMRTPServerFactory;
@@ -116,28 +115,43 @@ public class PlayServiceImpl implements IPlayService { @@ -116,28 +115,43 @@ public class PlayServiceImpl implements IPlayService {
116 115
117 116
118 @Override 117 @Override
119 - public SSRCInfo play(MediaServerItem mediaServerItem, String deviceId, String channelId, ErrorCallback<Object> callback) { 118 + public SSRCInfo play(MediaServerItem mediaServerItem, String deviceId, String channelId,boolean isSubStream, ErrorCallback<Object> callback) {
120 if (mediaServerItem == null) { 119 if (mediaServerItem == null) {
121 throw new ControllerException(ErrorCode.ERROR100.getCode(), "未找到可用的zlm"); 120 throw new ControllerException(ErrorCode.ERROR100.getCode(), "未找到可用的zlm");
122 } 121 }
123 122
124 Device device = redisCatchStorage.getDevice(deviceId); 123 Device device = redisCatchStorage.getDevice(deviceId);
125 - InviteInfo inviteInfo = inviteStreamService.getInviteInfoByDeviceAndChannel(InviteSessionType.PLAY, deviceId, channelId);  
126 - 124 + InviteInfo inviteInfo;
  125 + if(device.isSwitchPrimarySubStream()){
  126 + inviteInfo = inviteStreamService.getInviteInfoByDeviceAndChannel(InviteSessionType.PLAY, deviceId, channelId,isSubStream);
  127 + }else {
  128 + inviteInfo = inviteStreamService.getInviteInfoByDeviceAndChannel(InviteSessionType.PLAY, deviceId, channelId);
  129 + }
127 if (inviteInfo != null ) { 130 if (inviteInfo != null ) {
128 if (inviteInfo.getStreamInfo() == null) { 131 if (inviteInfo.getStreamInfo() == null) {
129 // 点播发起了但是尚未成功, 仅注册回调等待结果即可 132 // 点播发起了但是尚未成功, 仅注册回调等待结果即可
130 - inviteStreamService.once(InviteSessionType.PLAY, deviceId, channelId, null, callback); 133 + if(device.isSwitchPrimarySubStream()){
  134 + inviteStreamService.once(InviteSessionType.PLAY, deviceId, channelId,isSubStream, null, callback);
  135 + }else {
  136 + inviteStreamService.once(InviteSessionType.PLAY, deviceId, channelId, null, callback);
  137 + }
131 return inviteInfo.getSsrcInfo(); 138 return inviteInfo.getSsrcInfo();
132 }else { 139 }else {
133 StreamInfo streamInfo = inviteInfo.getStreamInfo(); 140 StreamInfo streamInfo = inviteInfo.getStreamInfo();
134 String streamId = streamInfo.getStream(); 141 String streamId = streamInfo.getStream();
135 if (streamId == null) { 142 if (streamId == null) {
136 callback.run(InviteErrorCode.ERROR_FOR_CATCH_DATA.getCode(), "点播失败, redis缓存streamId等于null", null); 143 callback.run(InviteErrorCode.ERROR_FOR_CATCH_DATA.getCode(), "点播失败, redis缓存streamId等于null", null);
137 - inviteStreamService.call(InviteSessionType.PLAY, device.getDeviceId(), channelId, null,  
138 - InviteErrorCode.ERROR_FOR_CATCH_DATA.getCode(),  
139 - "点播失败, redis缓存streamId等于null",  
140 - null); 144 + if(device.isSwitchPrimarySubStream()){
  145 + inviteStreamService.call(InviteSessionType.PLAY, device.getDeviceId(), channelId,isSubStream, null,
  146 + InviteErrorCode.ERROR_FOR_CATCH_DATA.getCode(),
  147 + "点播失败, redis缓存streamId等于null",
  148 + null);
  149 + }else {
  150 + inviteStreamService.call(InviteSessionType.PLAY, device.getDeviceId(), channelId, null,
  151 + InviteErrorCode.ERROR_FOR_CATCH_DATA.getCode(),
  152 + "点播失败, redis缓存streamId等于null",
  153 + null);
  154 + }
141 return inviteInfo.getSsrcInfo(); 155 return inviteInfo.getSsrcInfo();
142 } 156 }
143 String mediaServerId = streamInfo.getMediaServerId(); 157 String mediaServerId = streamInfo.getMediaServerId();
@@ -146,41 +160,64 @@ public class PlayServiceImpl implements IPlayService { @@ -146,41 +160,64 @@ public class PlayServiceImpl implements IPlayService {
146 Boolean ready = zlmrtpServerFactory.isStreamReady(mediaInfo, "rtp", streamId); 160 Boolean ready = zlmrtpServerFactory.isStreamReady(mediaInfo, "rtp", streamId);
147 if (ready != null && ready) { 161 if (ready != null && ready) {
148 callback.run(InviteErrorCode.SUCCESS.getCode(), InviteErrorCode.SUCCESS.getMsg(), streamInfo); 162 callback.run(InviteErrorCode.SUCCESS.getCode(), InviteErrorCode.SUCCESS.getMsg(), streamInfo);
149 - inviteStreamService.call(InviteSessionType.PLAY, device.getDeviceId(), channelId, null,  
150 - InviteErrorCode.SUCCESS.getCode(),  
151 - InviteErrorCode.SUCCESS.getMsg(),  
152 - streamInfo); 163 + if(device.isSwitchPrimarySubStream()){
  164 + inviteStreamService.call(InviteSessionType.PLAY, device.getDeviceId(), channelId,isSubStream, null,
  165 + InviteErrorCode.SUCCESS.getCode(),
  166 + InviteErrorCode.SUCCESS.getMsg(),
  167 + streamInfo);
  168 + }else {
  169 + inviteStreamService.call(InviteSessionType.PLAY, device.getDeviceId(), channelId, null,
  170 + InviteErrorCode.SUCCESS.getCode(),
  171 + InviteErrorCode.SUCCESS.getMsg(),
  172 + streamInfo);
  173 + }
153 return inviteInfo.getSsrcInfo(); 174 return inviteInfo.getSsrcInfo();
154 }else { 175 }else {
155 // 点播发起了但是尚未成功, 仅注册回调等待结果即可 176 // 点播发起了但是尚未成功, 仅注册回调等待结果即可
156 - inviteStreamService.once(InviteSessionType.PLAY, deviceId, channelId, null, callback);  
157 - storager.stopPlay(streamInfo.getDeviceID(), streamInfo.getChannelId());  
158 - inviteStreamService.removeInviteInfoByDeviceAndChannel(InviteSessionType.PLAY, deviceId, channelId); 177 + if(device.isSwitchPrimarySubStream()) {
  178 + inviteStreamService.once(InviteSessionType.PLAY, deviceId, channelId, null, callback);
  179 + storager.stopPlay(streamInfo.getDeviceID(), streamInfo.getChannelId());
  180 + inviteStreamService.removeInviteInfoByDeviceAndChannel(InviteSessionType.PLAY, deviceId, channelId);
  181 + }else {
  182 + inviteStreamService.once(InviteSessionType.PLAY, deviceId, channelId,isSubStream, null, callback);
  183 + inviteStreamService.removeInviteInfoByDeviceAndChannel(InviteSessionType.PLAY, deviceId, channelId,isSubStream);
  184 + }
159 } 185 }
160 } 186 }
161 } 187 }
162 188
163 String streamId = null; 189 String streamId = null;
164 if (mediaServerItem.isRtpEnable()) { 190 if (mediaServerItem.isRtpEnable()) {
165 - streamId = String.format("%s_%s", device.getDeviceId(), channelId); 191 + if(device.isSwitchPrimarySubStream()){
  192 + streamId = StreamInfo.getPlayStream(deviceId, channelId, isSubStream);
  193 + }else {
  194 + streamId = String.format("%s_%s", device.getDeviceId(), channelId);
  195 + }
166 } 196 }
167 SSRCInfo ssrcInfo = mediaServerService.openRTPServer(mediaServerItem, streamId, null, device.isSsrcCheck(), false, 0, false, device.getStreamModeForParam()); 197 SSRCInfo ssrcInfo = mediaServerService.openRTPServer(mediaServerItem, streamId, null, device.isSsrcCheck(), false, 0, false, device.getStreamModeForParam());
168 if (ssrcInfo == null) { 198 if (ssrcInfo == null) {
169 callback.run(InviteErrorCode.ERROR_FOR_RESOURCE_EXHAUSTION.getCode(), InviteErrorCode.ERROR_FOR_RESOURCE_EXHAUSTION.getMsg(), null); 199 callback.run(InviteErrorCode.ERROR_FOR_RESOURCE_EXHAUSTION.getCode(), InviteErrorCode.ERROR_FOR_RESOURCE_EXHAUSTION.getMsg(), null);
170 - inviteStreamService.call(InviteSessionType.PLAY, device.getDeviceId(), channelId, null,  
171 - InviteErrorCode.ERROR_FOR_RESOURCE_EXHAUSTION.getCode(),  
172 - InviteErrorCode.ERROR_FOR_RESOURCE_EXHAUSTION.getMsg(),  
173 - null); 200 + if(device.isSwitchPrimarySubStream()){
  201 + inviteStreamService.call(InviteSessionType.PLAY, device.getDeviceId(), channelId,isSubStream, null,
  202 + InviteErrorCode.ERROR_FOR_RESOURCE_EXHAUSTION.getCode(),
  203 + InviteErrorCode.ERROR_FOR_RESOURCE_EXHAUSTION.getMsg(),
  204 + null);
  205 + }else {
  206 + inviteStreamService.call(InviteSessionType.PLAY, device.getDeviceId(), channelId, null,
  207 + InviteErrorCode.ERROR_FOR_RESOURCE_EXHAUSTION.getCode(),
  208 + InviteErrorCode.ERROR_FOR_RESOURCE_EXHAUSTION.getMsg(),
  209 + null);
  210 + }
174 return null; 211 return null;
175 } 212 }
176 // TODO 记录点播的状态 213 // TODO 记录点播的状态
177 - play(mediaServerItem, ssrcInfo, device, channelId, callback); 214 + play(mediaServerItem, ssrcInfo, device, channelId,isSubStream, callback);
178 return ssrcInfo; 215 return ssrcInfo;
179 } 216 }
180 217
181 218
182 @Override 219 @Override
183 - public void play(MediaServerItem mediaServerItem, SSRCInfo ssrcInfo, Device device, String channelId, 220 + public void play(MediaServerItem mediaServerItem, SSRCInfo ssrcInfo, Device device, String channelId,boolean isSubStream,
184 ErrorCallback<Object> callback) { 221 ErrorCallback<Object> callback) {
185 222
186 if (mediaServerItem == null || ssrcInfo == null) { 223 if (mediaServerItem == null || ssrcInfo == null) {
@@ -189,21 +226,11 @@ public class PlayServiceImpl implements IPlayService { @@ -189,21 +226,11 @@ public class PlayServiceImpl implements IPlayService {
189 null); 226 null);
190 return; 227 return;
191 } 228 }
192 - logger.info("\r\n" +  
193 - " [点播开始] \r\n" +  
194 - "deviceId : {}, \r\n" +  
195 - "channelId : {},\r\n" +  
196 - "收流端口 : {}, \r\n" +  
197 - "收流模式 : {}, \r\n" +  
198 - "SSRC : {}, \r\n" +  
199 - "SSRC校验 :{}",  
200 - device.getDeviceId(),  
201 - channelId,  
202 - ssrcInfo.getPort(),  
203 - device.getStreamMode(),  
204 - ssrcInfo.getSsrc(),  
205 - device.isSsrcCheck());  
206 - 229 + if( device.isSwitchPrimarySubStream() ){
  230 + logger.info("[点播开始] deviceId: {}, channelId: {},码流类型:{},收流端口: {}, 收流模式:{}, SSRC: {}, SSRC校验:{}", device.getDeviceId(), channelId,isSubStream ? "辅码流" : "主码流", ssrcInfo.getPort(), device.getStreamMode(), ssrcInfo.getSsrc(), device.isSsrcCheck());
  231 + }else {
  232 + logger.info("[点播开始] deviceId: {}, channelId: {},收流端口: {}, 收流模式:{}, SSRC: {}, SSRC校验:{}", device.getDeviceId(), channelId, ssrcInfo.getPort(), device.getStreamMode(), ssrcInfo.getSsrc(), device.isSsrcCheck());
  233 + }
207 //端口获取失败的ssrcInfo 没有必要发送点播指令 234 //端口获取失败的ssrcInfo 没有必要发送点播指令
208 if (ssrcInfo.getPort() <= 0) { 235 if (ssrcInfo.getPort() <= 0) {
209 logger.info("[点播端口分配异常],deviceId={},channelId={},ssrcInfo={}", device.getDeviceId(), channelId, ssrcInfo); 236 logger.info("[点播端口分配异常],deviceId={},channelId={},ssrcInfo={}", device.getDeviceId(), channelId, ssrcInfo);
@@ -212,23 +239,50 @@ public class PlayServiceImpl implements IPlayService { @@ -212,23 +239,50 @@ public class PlayServiceImpl implements IPlayService {
212 streamSession.remove(device.getDeviceId(), channelId, ssrcInfo.getStream()); 239 streamSession.remove(device.getDeviceId(), channelId, ssrcInfo.getStream());
213 240
214 callback.run(InviteErrorCode.ERROR_FOR_RESOURCE_EXHAUSTION.getCode(), "点播端口分配异常", null); 241 callback.run(InviteErrorCode.ERROR_FOR_RESOURCE_EXHAUSTION.getCode(), "点播端口分配异常", null);
215 - inviteStreamService.call(InviteSessionType.PLAY, device.getDeviceId(), channelId, null,  
216 - InviteErrorCode.ERROR_FOR_RESOURCE_EXHAUSTION.getCode(), "点播端口分配异常", null); 242 + if(device.isSwitchPrimarySubStream()){
  243 + inviteStreamService.call(InviteSessionType.PLAY, device.getDeviceId(), channelId,isSubStream, null,
  244 + InviteErrorCode.ERROR_FOR_RESOURCE_EXHAUSTION.getCode(), "点播端口分配异常", null);
  245 + }else {
  246 + inviteStreamService.call(InviteSessionType.PLAY, device.getDeviceId(), channelId, null,
  247 + InviteErrorCode.ERROR_FOR_RESOURCE_EXHAUSTION.getCode(), "点播端口分配异常", null);
  248 + }
217 return; 249 return;
218 } 250 }
219 251
220 // 初始化redis中的invite消息状态 252 // 初始化redis中的invite消息状态
221 - InviteInfo inviteInfo = InviteInfo.getinviteInfo(device.getDeviceId(), channelId, ssrcInfo.getStream(), ssrcInfo,  
222 - mediaServerItem.getSdpIp(), ssrcInfo.getPort(), device.getStreamMode(), InviteSessionType.PLAY,  
223 - InviteSessionStatus.ready);  
224 - inviteStreamService.updateInviteInfo(inviteInfo); 253 + InviteInfo inviteInfo;
  254 +
  255 + if(device.isSwitchPrimarySubStream()){
  256 + // 初始化redis中的invite消息状态
  257 + inviteInfo = InviteInfo.getInviteInfo(device.getDeviceId(), channelId,isSubStream, ssrcInfo.getStream(), ssrcInfo,
  258 + mediaServerItem.getSdpIp(), ssrcInfo.getPort(), device.getStreamMode(), InviteSessionType.PLAY,
  259 + InviteSessionStatus.ready);
  260 + inviteStreamService.updateInviteInfoSub(inviteInfo);
  261 + }else {
  262 + // 初始化redis中的invite消息状态
  263 + inviteInfo = InviteInfo.getinviteInfo(device.getDeviceId(), channelId, ssrcInfo.getStream(), ssrcInfo,
  264 + mediaServerItem.getSdpIp(), ssrcInfo.getPort(), device.getStreamMode(), InviteSessionType.PLAY,
  265 + InviteSessionStatus.ready);
  266 + inviteStreamService.updateInviteInfo(inviteInfo);
  267 + }
225 // 超时处理 268 // 超时处理
226 String timeOutTaskKey = UUID.randomUUID().toString(); 269 String timeOutTaskKey = UUID.randomUUID().toString();
227 dynamicTask.startDelay(timeOutTaskKey, () -> { 270 dynamicTask.startDelay(timeOutTaskKey, () -> {
228 // 执行超时任务时查询是否已经成功,成功了则不执行超时任务,防止超时任务取消失败的情况 271 // 执行超时任务时查询是否已经成功,成功了则不执行超时任务,防止超时任务取消失败的情况
229 - InviteInfo inviteInfoForTimeOut = inviteStreamService.getInviteInfoByDeviceAndChannel(InviteSessionType.PLAY, device.getDeviceId(), channelId); 272 + InviteInfo inviteInfoForTimeOut;
  273 + if(device.isSwitchPrimarySubStream()){
  274 + // 初始化redis中的invite消息状态
  275 + inviteInfoForTimeOut = inviteStreamService.getInviteInfoByDeviceAndChannel(InviteSessionType.PLAY, device.getDeviceId(), channelId,isSubStream);
  276 + }else {
  277 + // 初始化redis中的invite消息状态
  278 + inviteInfoForTimeOut = inviteStreamService.getInviteInfoByDeviceAndChannel(InviteSessionType.PLAY, device.getDeviceId(), channelId);
  279 + }
230 if (inviteInfoForTimeOut == null || inviteInfoForTimeOut.getStreamInfo() == null) { 280 if (inviteInfoForTimeOut == null || inviteInfoForTimeOut.getStreamInfo() == null) {
231 - logger.info("[点播超时] 收流超时 deviceId: {}, channelId: {},端口:{}, SSRC: {}", device.getDeviceId(), channelId, ssrcInfo.getPort(), ssrcInfo.getSsrc()); 281 + if( device.isSwitchPrimarySubStream()){
  282 + logger.info("[点播超时] 收流超时 deviceId: {}, channelId: {},码流类型:{},端口:{}, SSRC: {}", device.getDeviceId(), channelId,isSubStream ? "辅码流" : "主码流", ssrcInfo.getPort(), ssrcInfo.getSsrc());
  283 + }else {
  284 + logger.info("[点播超时] 收流超时 deviceId: {}, channelId: {},端口:{}, SSRC: {}", device.getDeviceId(), channelId, ssrcInfo.getPort(), ssrcInfo.getSsrc());
  285 + }
232 // 点播超时回复BYE 同时释放ssrc以及此次点播的资源 286 // 点播超时回复BYE 同时释放ssrc以及此次点播的资源
233 // InviteInfo inviteInfoForTimeout = inviteStreamService.getInviteInfoByDeviceAndChannel(InviteSessionType.play, device.getDeviceId(), channelId); 287 // InviteInfo inviteInfoForTimeout = inviteStreamService.getInviteInfoByDeviceAndChannel(InviteSessionType.play, device.getDeviceId(), channelId);
234 // if (inviteInfoForTimeout == null) { 288 // if (inviteInfoForTimeout == null) {
@@ -240,10 +294,16 @@ public class PlayServiceImpl implements IPlayService { @@ -240,10 +294,16 @@ public class PlayServiceImpl implements IPlayService {
240 // // TODO 发送cancel 294 // // TODO 发送cancel
241 // } 295 // }
242 callback.run(InviteErrorCode.ERROR_FOR_STREAM_TIMEOUT.getCode(), InviteErrorCode.ERROR_FOR_STREAM_TIMEOUT.getMsg(), null); 296 callback.run(InviteErrorCode.ERROR_FOR_STREAM_TIMEOUT.getCode(), InviteErrorCode.ERROR_FOR_STREAM_TIMEOUT.getMsg(), null);
243 - inviteStreamService.call(InviteSessionType.PLAY, device.getDeviceId(), channelId, null,  
244 - InviteErrorCode.ERROR_FOR_STREAM_TIMEOUT.getCode(), InviteErrorCode.ERROR_FOR_STREAM_TIMEOUT.getMsg(), null); 297 + if( device.isSwitchPrimarySubStream()){
  298 + inviteStreamService.call(InviteSessionType.PLAY, device.getDeviceId(), channelId,isSubStream, null,
  299 + InviteErrorCode.ERROR_FOR_STREAM_TIMEOUT.getCode(), InviteErrorCode.ERROR_FOR_STREAM_TIMEOUT.getMsg(), null);
  300 + inviteStreamService.removeInviteInfoByDeviceAndChannel(InviteSessionType.PLAY, device.getDeviceId(), channelId,isSubStream);
245 301
246 - inviteStreamService.removeInviteInfoByDeviceAndChannel(InviteSessionType.PLAY, device.getDeviceId(), channelId); 302 + }else {
  303 + inviteStreamService.call(InviteSessionType.PLAY, device.getDeviceId(), channelId, null,
  304 + InviteErrorCode.ERROR_FOR_STREAM_TIMEOUT.getCode(), InviteErrorCode.ERROR_FOR_STREAM_TIMEOUT.getMsg(), null);
  305 + inviteStreamService.removeInviteInfoByDeviceAndChannel(InviteSessionType.PLAY, device.getDeviceId(), channelId);
  306 + }
247 try { 307 try {
248 cmder.streamByeCmd(device, channelId, ssrcInfo.getStream(), null); 308 cmder.streamByeCmd(device, channelId, ssrcInfo.getStream(), null);
249 } catch (InvalidArgumentException | ParseException | SipException | SsrcTransactionNotFoundException e) { 309 } catch (InvalidArgumentException | ParseException | SipException | SsrcTransactionNotFoundException e) {
@@ -261,25 +321,42 @@ public class PlayServiceImpl implements IPlayService { @@ -261,25 +321,42 @@ public class PlayServiceImpl implements IPlayService {
261 }, userSetting.getPlayTimeout()); 321 }, userSetting.getPlayTimeout());
262 322
263 try { 323 try {
264 - cmder.playStreamCmd(mediaServerItem, ssrcInfo, device, channelId, (MediaServerItem mediaServerItemInuse, JSONObject response) -> { 324 + cmder.playStreamCmd(mediaServerItem, ssrcInfo, device, channelId,isSubStream, (MediaServerItem mediaServerItemInuse, JSONObject response) -> {
265 logger.info("收到订阅消息: " + response.toJSONString()); 325 logger.info("收到订阅消息: " + response.toJSONString());
266 dynamicTask.stop(timeOutTaskKey); 326 dynamicTask.stop(timeOutTaskKey);
267 // hook响应 327 // hook响应
268 - StreamInfo streamInfo = onPublishHandlerForPlay(mediaServerItemInuse, response, device.getDeviceId(), channelId); 328 + StreamInfo streamInfo = onPublishHandlerForPlay(mediaServerItemInuse, response, device.getDeviceId(), channelId,isSubStream);
269 if (streamInfo == null){ 329 if (streamInfo == null){
270 callback.run(InviteErrorCode.ERROR_FOR_STREAM_PARSING_EXCEPTIONS.getCode(), 330 callback.run(InviteErrorCode.ERROR_FOR_STREAM_PARSING_EXCEPTIONS.getCode(),
271 InviteErrorCode.ERROR_FOR_STREAM_PARSING_EXCEPTIONS.getMsg(), null); 331 InviteErrorCode.ERROR_FOR_STREAM_PARSING_EXCEPTIONS.getMsg(), null);
272 - inviteStreamService.call(InviteSessionType.PLAY, device.getDeviceId(), channelId, null,  
273 - InviteErrorCode.ERROR_FOR_STREAM_PARSING_EXCEPTIONS.getCode(),  
274 - InviteErrorCode.ERROR_FOR_STREAM_PARSING_EXCEPTIONS.getMsg(), null); 332 + if( device.isSwitchPrimarySubStream()){
  333 + inviteStreamService.call(InviteSessionType.PLAY, device.getDeviceId(), channelId,isSubStream, null,
  334 + InviteErrorCode.ERROR_FOR_STREAM_PARSING_EXCEPTIONS.getCode(),
  335 + InviteErrorCode.ERROR_FOR_STREAM_PARSING_EXCEPTIONS.getMsg(), null);
  336 + }else {
  337 + inviteStreamService.call(InviteSessionType.PLAY, device.getDeviceId(), channelId, null,
  338 + InviteErrorCode.ERROR_FOR_STREAM_PARSING_EXCEPTIONS.getCode(),
  339 + InviteErrorCode.ERROR_FOR_STREAM_PARSING_EXCEPTIONS.getMsg(), null);
  340 + }
275 return; 341 return;
276 } 342 }
277 callback.run(InviteErrorCode.SUCCESS.getCode(), InviteErrorCode.SUCCESS.getMsg(), streamInfo); 343 callback.run(InviteErrorCode.SUCCESS.getCode(), InviteErrorCode.SUCCESS.getMsg(), streamInfo);
278 - inviteStreamService.call(InviteSessionType.PLAY, device.getDeviceId(), channelId, null,  
279 - InviteErrorCode.SUCCESS.getCode(),  
280 - InviteErrorCode.SUCCESS.getMsg(),  
281 - streamInfo);  
282 - logger.info("[点播成功] deviceId: {}, channelId: {}", device.getDeviceId(), channelId); 344 + if( device.isSwitchPrimarySubStream()){
  345 + inviteStreamService.call(InviteSessionType.PLAY, device.getDeviceId(), channelId,isSubStream, null,
  346 + InviteErrorCode.SUCCESS.getCode(),
  347 + InviteErrorCode.SUCCESS.getMsg(),
  348 + streamInfo);
  349 + }else {
  350 + inviteStreamService.call(InviteSessionType.PLAY, device.getDeviceId(), channelId, null,
  351 + InviteErrorCode.SUCCESS.getCode(),
  352 + InviteErrorCode.SUCCESS.getMsg(),
  353 + streamInfo);
  354 + }
  355 + if( device.isSwitchPrimarySubStream() ){
  356 + logger.info("[点播成功] deviceId: {}, channelId: {},码流类型:{}", device.getDeviceId(), channelId,isSubStream ? "辅码流" : "主码流");
  357 + }else {
  358 + logger.info("[点播成功] deviceId: {}, channelId: {}", device.getDeviceId(), channelId);
  359 + }
283 String streamUrl; 360 String streamUrl;
284 if (mediaServerItemInuse.getRtspPort() != 0) { 361 if (mediaServerItemInuse.getRtspPort() != 0) {
285 streamUrl = String.format("rtsp://127.0.0.1:%s/%s/%s", mediaServerItemInuse.getRtspPort(), "rtp", ssrcInfo.getStream()); 362 streamUrl = String.format("rtsp://127.0.0.1:%s/%s/%s", mediaServerItemInuse.getRtspPort(), "rtp", ssrcInfo.getStream());
@@ -298,16 +375,17 @@ public class PlayServiceImpl implements IPlayService { @@ -298,16 +375,17 @@ public class PlayServiceImpl implements IPlayService {
298 ResponseEvent responseEvent = (ResponseEvent) event.event; 375 ResponseEvent responseEvent = (ResponseEvent) event.event;
299 String contentString = new String(responseEvent.getResponse().getRawContent()); 376 String contentString = new String(responseEvent.getResponse().getRawContent());
300 // 获取ssrc 377 // 获取ssrc
301 - String ssrcInResponse = SipUtils.getSsrcFromSdp(contentString);  
302 - 378 + int ssrcIndex = contentString.indexOf("y=");
303 // 检查是否有y字段 379 // 检查是否有y字段
304 - if (ssrcInResponse != null) { 380 + if (ssrcIndex >= 0) {
  381 + //ssrc规定长度为10字节,不取余下长度以避免后续还有“f=”字段 TODO 后续对不规范的非10位ssrc兼容
  382 + String ssrcInResponse = contentString.substring(ssrcIndex + 2, ssrcIndex + 12).trim();
305 // 查询到ssrc不一致且开启了ssrc校验则需要针对处理 383 // 查询到ssrc不一致且开启了ssrc校验则需要针对处理
306 if (ssrcInfo.getSsrc().equals(ssrcInResponse)) { 384 if (ssrcInfo.getSsrc().equals(ssrcInResponse)) {
307 if (device.getStreamMode().equalsIgnoreCase("TCP-ACTIVE")) { 385 if (device.getStreamMode().equalsIgnoreCase("TCP-ACTIVE")) {
  386 + String substring = contentString.substring(0, contentString.indexOf("y="));
308 try { 387 try {
309 - Gb28181Sdp gb28181Sdp = SipUtils.parseSDP(contentString);  
310 - SessionDescription sdp = gb28181Sdp.getBaseSdb(); 388 + SessionDescription sdp = SdpFactory.getInstance().createSessionDescription(substring);
311 int port = -1; 389 int port = -1;
312 Vector mediaDescriptions = sdp.getMediaDescriptions(true); 390 Vector mediaDescriptions = sdp.getMediaDescriptions(true);
313 for (Object description : mediaDescriptions) { 391 for (Object description : mediaDescriptions) {
@@ -334,21 +412,24 @@ public class PlayServiceImpl implements IPlayService { @@ -334,21 +412,24 @@ public class PlayServiceImpl implements IPlayService {
334 412
335 callback.run(InviteErrorCode.ERROR_FOR_SDP_PARSING_EXCEPTIONS.getCode(), 413 callback.run(InviteErrorCode.ERROR_FOR_SDP_PARSING_EXCEPTIONS.getCode(),
336 InviteErrorCode.ERROR_FOR_SDP_PARSING_EXCEPTIONS.getMsg(), null); 414 InviteErrorCode.ERROR_FOR_SDP_PARSING_EXCEPTIONS.getMsg(), null);
337 - inviteStreamService.call(InviteSessionType.PLAY, device.getDeviceId(), channelId, null,  
338 - InviteErrorCode.ERROR_FOR_SDP_PARSING_EXCEPTIONS.getCode(),  
339 - InviteErrorCode.ERROR_FOR_SDP_PARSING_EXCEPTIONS.getMsg(), null); 415 + if(device.isSwitchPrimarySubStream()){
  416 + inviteStreamService.call(InviteSessionType.PLAY, device.getDeviceId(), channelId,isSubStream, null,
  417 + InviteErrorCode.ERROR_FOR_SDP_PARSING_EXCEPTIONS.getCode(),
  418 + InviteErrorCode.ERROR_FOR_SDP_PARSING_EXCEPTIONS.getMsg(), null);
  419 + }else {
  420 + inviteStreamService.call(InviteSessionType.PLAY, device.getDeviceId(), channelId, null,
  421 + InviteErrorCode.ERROR_FOR_SDP_PARSING_EXCEPTIONS.getCode(),
  422 + InviteErrorCode.ERROR_FOR_SDP_PARSING_EXCEPTIONS.getMsg(), null);
  423 + }
340 } 424 }
341 } 425 }
342 return; 426 return;
343 } 427 }
344 logger.info("[点播消息] 收到invite 200, 发现下级自定义了ssrc: {}", ssrcInResponse); 428 logger.info("[点播消息] 收到invite 200, 发现下级自定义了ssrc: {}", ssrcInResponse);
345 -  
346 if (!mediaServerItem.isRtpEnable() || device.isSsrcCheck()) { 429 if (!mediaServerItem.isRtpEnable() || device.isSsrcCheck()) {
347 logger.info("[点播消息] SSRC修正 {}->{}", ssrcInfo.getSsrc(), ssrcInResponse); 430 logger.info("[点播消息] SSRC修正 {}->{}", ssrcInfo.getSsrc(), ssrcInResponse);
348 -  
349 // 释放ssrc 431 // 释放ssrc
350 mediaServerService.releaseSsrc(mediaServerItem.getId(), ssrcInfo.getSsrc()); 432 mediaServerService.releaseSsrc(mediaServerItem.getId(), ssrcInfo.getSsrc());
351 -  
352 // 单端口模式streamId也有变化,重新设置监听即可 433 // 单端口模式streamId也有变化,重新设置监听即可
353 if (!mediaServerItem.isRtpEnable()) { 434 if (!mediaServerItem.isRtpEnable()) {
354 // 添加订阅 435 // 添加订阅
@@ -361,21 +442,34 @@ public class PlayServiceImpl implements IPlayService { @@ -361,21 +442,34 @@ public class PlayServiceImpl implements IPlayService {
361 logger.info("[ZLM HOOK] ssrc修正后收到订阅消息: " + response.toJSONString()); 442 logger.info("[ZLM HOOK] ssrc修正后收到订阅消息: " + response.toJSONString());
362 dynamicTask.stop(timeOutTaskKey); 443 dynamicTask.stop(timeOutTaskKey);
363 // hook响应 444 // hook响应
364 - StreamInfo streamInfo = onPublishHandlerForPlay(mediaServerItemInUse, response, device.getDeviceId(), channelId); 445 + StreamInfo streamInfo = onPublishHandlerForPlay(mediaServerItemInUse, response, device.getDeviceId(), channelId,isSubStream);
365 if (streamInfo == null){ 446 if (streamInfo == null){
366 callback.run(InviteErrorCode.ERROR_FOR_STREAM_PARSING_EXCEPTIONS.getCode(), 447 callback.run(InviteErrorCode.ERROR_FOR_STREAM_PARSING_EXCEPTIONS.getCode(),
367 InviteErrorCode.ERROR_FOR_STREAM_PARSING_EXCEPTIONS.getMsg(), null); 448 InviteErrorCode.ERROR_FOR_STREAM_PARSING_EXCEPTIONS.getMsg(), null);
368 - inviteStreamService.call(InviteSessionType.PLAY, device.getDeviceId(), channelId, null,  
369 - InviteErrorCode.ERROR_FOR_STREAM_PARSING_EXCEPTIONS.getCode(),  
370 - InviteErrorCode.ERROR_FOR_STREAM_PARSING_EXCEPTIONS.getMsg(), null); 449 + if( device.isSwitchPrimarySubStream()){
  450 + inviteStreamService.call(InviteSessionType.PLAY, device.getDeviceId(), channelId,isSubStream, null,
  451 + InviteErrorCode.ERROR_FOR_STREAM_PARSING_EXCEPTIONS.getCode(),
  452 + InviteErrorCode.ERROR_FOR_STREAM_PARSING_EXCEPTIONS.getMsg(), null);
  453 + }else {
  454 + inviteStreamService.call(InviteSessionType.PLAY, device.getDeviceId(), channelId, null,
  455 + InviteErrorCode.ERROR_FOR_STREAM_PARSING_EXCEPTIONS.getCode(),
  456 + InviteErrorCode.ERROR_FOR_STREAM_PARSING_EXCEPTIONS.getMsg(), null);
  457 + }
371 return; 458 return;
372 } 459 }
373 callback.run(InviteErrorCode.SUCCESS.getCode(), 460 callback.run(InviteErrorCode.SUCCESS.getCode(),
374 InviteErrorCode.SUCCESS.getMsg(), streamInfo); 461 InviteErrorCode.SUCCESS.getMsg(), streamInfo);
375 - inviteStreamService.call(InviteSessionType.PLAY, device.getDeviceId(), channelId, null,  
376 - InviteErrorCode.SUCCESS.getCode(),  
377 - InviteErrorCode.SUCCESS.getMsg(),  
378 - streamInfo); 462 + if( device.isSwitchPrimarySubStream()){
  463 + inviteStreamService.call(InviteSessionType.PLAY, device.getDeviceId(), channelId,isSubStream, null,
  464 + InviteErrorCode.SUCCESS.getCode(),
  465 + InviteErrorCode.SUCCESS.getMsg(),
  466 + streamInfo);
  467 + }else {
  468 + inviteStreamService.call(InviteSessionType.PLAY, device.getDeviceId(), channelId, null,
  469 + InviteErrorCode.SUCCESS.getCode(),
  470 + InviteErrorCode.SUCCESS.getMsg(),
  471 + streamInfo);
  472 + }
379 }); 473 });
380 return; 474 return;
381 } 475 }
@@ -391,14 +485,22 @@ public class PlayServiceImpl implements IPlayService { @@ -391,14 +485,22 @@ public class PlayServiceImpl implements IPlayService {
391 } 485 }
392 486
393 dynamicTask.stop(timeOutTaskKey); 487 dynamicTask.stop(timeOutTaskKey);
  488 + // 释放ssrc
  489 + mediaServerService.releaseSsrc(mediaServerItem.getId(), ssrcInfo.getSsrc());
394 490
395 streamSession.remove(device.getDeviceId(), channelId, ssrcInfo.getStream()); 491 streamSession.remove(device.getDeviceId(), channelId, ssrcInfo.getStream());
396 492
397 callback.run(InviteErrorCode.ERROR_FOR_RESET_SSRC.getCode(), 493 callback.run(InviteErrorCode.ERROR_FOR_RESET_SSRC.getCode(),
398 "下级自定义了ssrc,重新设置收流信息失败", null); 494 "下级自定义了ssrc,重新设置收流信息失败", null);
399 - inviteStreamService.call(InviteSessionType.PLAY, device.getDeviceId(), channelId, null,  
400 - InviteErrorCode.ERROR_FOR_RESET_SSRC.getCode(),  
401 - "下级自定义了ssrc,重新设置收流信息失败", null); 495 + if( device.isSwitchPrimarySubStream()){
  496 + inviteStreamService.call(InviteSessionType.PLAY, device.getDeviceId(), channelId,isSubStream, null,
  497 + InviteErrorCode.ERROR_FOR_RESET_SSRC.getCode(),
  498 + "下级自定义了ssrc,重新设置收流信息失败", null);
  499 + }else {
  500 + inviteStreamService.call(InviteSessionType.PLAY, device.getDeviceId(), channelId, null,
  501 + InviteErrorCode.ERROR_FOR_RESET_SSRC.getCode(),
  502 + "下级自定义了ssrc,重新设置收流信息失败", null);
  503 + }
402 504
403 }else { 505 }else {
404 ssrcInfo.setSsrc(ssrcInResponse); 506 ssrcInfo.setSsrc(ssrcInResponse);
@@ -409,7 +511,11 @@ public class PlayServiceImpl implements IPlayService { @@ -409,7 +511,11 @@ public class PlayServiceImpl implements IPlayService {
409 logger.info("[点播消息] 收到invite 200, 下级自定义了ssrc, 但是当前模式无需修正"); 511 logger.info("[点播消息] 收到invite 200, 下级自定义了ssrc, 但是当前模式无需修正");
410 } 512 }
411 } 513 }
412 - inviteStreamService.updateInviteInfo(inviteInfo); 514 + if(device.isSwitchPrimarySubStream()){
  515 + inviteStreamService.updateInviteInfoSub(inviteInfo);
  516 + }else {
  517 + inviteStreamService.updateInviteInfo(inviteInfo);
  518 + }
413 }, (event) -> { 519 }, (event) -> {
414 dynamicTask.stop(timeOutTaskKey); 520 dynamicTask.stop(timeOutTaskKey);
415 mediaServerService.closeRTPServer(mediaServerItem, ssrcInfo.getStream()); 521 mediaServerService.closeRTPServer(mediaServerItem, ssrcInfo.getStream());
@@ -420,11 +526,19 @@ public class PlayServiceImpl implements IPlayService { @@ -420,11 +526,19 @@ public class PlayServiceImpl implements IPlayService {
420 526
421 callback.run(InviteErrorCode.ERROR_FOR_SIGNALLING_ERROR.getCode(), 527 callback.run(InviteErrorCode.ERROR_FOR_SIGNALLING_ERROR.getCode(),
422 String.format("点播失败, 错误码: %s, %s", event.statusCode, event.msg), null); 528 String.format("点播失败, 错误码: %s, %s", event.statusCode, event.msg), null);
423 - inviteStreamService.call(InviteSessionType.PLAY, device.getDeviceId(), channelId, null,  
424 - InviteErrorCode.ERROR_FOR_RESET_SSRC.getCode(),  
425 - String.format("点播失败, 错误码: %s, %s", event.statusCode, event.msg), null); 529 + if( device.isSwitchPrimarySubStream()){
  530 + inviteStreamService.call(InviteSessionType.PLAY, device.getDeviceId(), channelId,isSubStream, null,
  531 + InviteErrorCode.ERROR_FOR_RESET_SSRC.getCode(),
  532 + String.format("点播失败, 错误码: %s, %s", event.statusCode, event.msg), null);
426 533
427 - inviteStreamService.removeInviteInfoByDeviceAndChannel(InviteSessionType.PLAY, device.getDeviceId(), channelId); 534 + inviteStreamService.removeInviteInfoByDeviceAndChannel(InviteSessionType.PLAY, device.getDeviceId(), channelId,isSubStream);
  535 + }else {
  536 + inviteStreamService.call(InviteSessionType.PLAY, device.getDeviceId(), channelId, null,
  537 + InviteErrorCode.ERROR_FOR_RESET_SSRC.getCode(),
  538 + String.format("点播失败, 错误码: %s, %s", event.statusCode, event.msg), null);
  539 +
  540 + inviteStreamService.removeInviteInfoByDeviceAndChannel(InviteSessionType.PLAY, device.getDeviceId(), channelId);
  541 + }
428 }); 542 });
429 } catch (InvalidArgumentException | SipException | ParseException e) { 543 } catch (InvalidArgumentException | SipException | ParseException e) {
430 544
@@ -438,27 +552,51 @@ public class PlayServiceImpl implements IPlayService { @@ -438,27 +552,51 @@ public class PlayServiceImpl implements IPlayService {
438 552
439 callback.run(InviteErrorCode.ERROR_FOR_SIP_SENDING_FAILED.getCode(), 553 callback.run(InviteErrorCode.ERROR_FOR_SIP_SENDING_FAILED.getCode(),
440 InviteErrorCode.ERROR_FOR_SIP_SENDING_FAILED.getMsg(), null); 554 InviteErrorCode.ERROR_FOR_SIP_SENDING_FAILED.getMsg(), null);
441 - inviteStreamService.call(InviteSessionType.PLAY, device.getDeviceId(), channelId, null,  
442 - InviteErrorCode.ERROR_FOR_SIP_SENDING_FAILED.getCode(),  
443 - InviteErrorCode.ERROR_FOR_SIP_SENDING_FAILED.getMsg(), null); 555 + if( device.isSwitchPrimarySubStream()){
  556 + inviteStreamService.call(InviteSessionType.PLAY, device.getDeviceId(), channelId,isSubStream, null,
  557 + InviteErrorCode.ERROR_FOR_SIP_SENDING_FAILED.getCode(),
  558 + InviteErrorCode.ERROR_FOR_SIP_SENDING_FAILED.getMsg(), null);
444 559
445 - inviteStreamService.removeInviteInfoByDeviceAndChannel(InviteSessionType.PLAY, device.getDeviceId(), channelId); 560 + inviteStreamService.removeInviteInfoByDeviceAndChannel(InviteSessionType.PLAY, device.getDeviceId(), channelId,isSubStream);
  561 + }else {
  562 + inviteStreamService.call(InviteSessionType.PLAY, device.getDeviceId(), channelId, null,
  563 + InviteErrorCode.ERROR_FOR_SIP_SENDING_FAILED.getCode(),
  564 + InviteErrorCode.ERROR_FOR_SIP_SENDING_FAILED.getMsg(), null);
  565 +
  566 + inviteStreamService.removeInviteInfoByDeviceAndChannel(InviteSessionType.PLAY, device.getDeviceId(), channelId);
  567 + }
446 } 568 }
447 } 569 }
448 570
449 - private StreamInfo onPublishHandlerForPlay(MediaServerItem mediaServerItem, JSONObject response, String deviceId, String channelId) {  
450 - StreamInfo streamInfo = onPublishHandler(mediaServerItem, response, deviceId, channelId); 571 + private StreamInfo onPublishHandlerForPlay(MediaServerItem mediaServerItem, JSONObject response, String deviceId, String channelId,boolean isSubStream) {
  572 + StreamInfo streamInfo = null;
  573 + Device device = redisCatchStorage.getDevice(deviceId);
  574 + if( device.isSwitchPrimarySubStream() ){
  575 + streamInfo = onPublishHandler(mediaServerItem, response, deviceId, channelId,isSubStream);
  576 + }else {
  577 + streamInfo = onPublishHandler(mediaServerItem, response, deviceId, channelId);
  578 + }
451 if (streamInfo != null) { 579 if (streamInfo != null) {
452 - DeviceChannel deviceChannel = storager.queryChannel(deviceId, channelId);  
453 - if (deviceChannel != null) {  
454 - deviceChannel.setStreamId(streamInfo.getStream());  
455 - storager.startPlay(deviceId, channelId, streamInfo.getStream()); 580 + InviteInfo inviteInfo;
  581 + if(device.isSwitchPrimarySubStream()){
  582 + inviteInfo = inviteStreamService.getInviteInfoByDeviceAndChannel(InviteSessionType.PLAY, deviceId, channelId,isSubStream);
  583 + }else {
  584 + DeviceChannel deviceChannel = storager.queryChannel(deviceId, channelId);
  585 + if (deviceChannel != null) {
  586 + deviceChannel.setStreamId(streamInfo.getStream());
  587 + storager.startPlay(deviceId, channelId, streamInfo.getStream());
  588 + }
  589 + inviteInfo = inviteStreamService.getInviteInfoByDeviceAndChannel(InviteSessionType.PLAY, deviceId, channelId);
456 } 590 }
457 - InviteInfo inviteInfo = inviteStreamService.getInviteInfoByDeviceAndChannel(InviteSessionType.PLAY, deviceId, channelId);  
458 if (inviteInfo != null) { 591 if (inviteInfo != null) {
459 inviteInfo.setStatus(InviteSessionStatus.ok); 592 inviteInfo.setStatus(InviteSessionStatus.ok);
460 inviteInfo.setStreamInfo(streamInfo); 593 inviteInfo.setStreamInfo(streamInfo);
461 - inviteStreamService.updateInviteInfo(inviteInfo); 594 + if(device.isSwitchPrimarySubStream()){
  595 + inviteStreamService.updateInviteInfoSub(inviteInfo);
  596 + }else {
  597 + inviteStreamService.updateInviteInfo(inviteInfo);
  598 + }
  599 +
462 } 600 }
463 } 601 }
464 return streamInfo; 602 return streamInfo;
@@ -607,16 +745,17 @@ public class PlayServiceImpl implements IPlayService { @@ -607,16 +745,17 @@ public class PlayServiceImpl implements IPlayService {
607 ResponseEvent responseEvent = (ResponseEvent) eventResult.event; 745 ResponseEvent responseEvent = (ResponseEvent) eventResult.event;
608 String contentString = new String(responseEvent.getResponse().getRawContent()); 746 String contentString = new String(responseEvent.getResponse().getRawContent());
609 // 获取ssrc 747 // 获取ssrc
610 - String ssrcInResponse = SipUtils.getSsrcFromSdp(contentString);  
611 - 748 + int ssrcIndex = contentString.indexOf("y=");
612 // 检查是否有y字段 749 // 检查是否有y字段
613 - if (ssrcInResponse != null) { 750 + if (ssrcIndex >= 0) {
  751 + //ssrc规定长度为10字节,不取余下长度以避免后续还有“f=”字段 TODO 后续对不规范的非10位ssrc兼容
  752 + String ssrcInResponse = contentString.substring(ssrcIndex + 2, ssrcIndex + 12);
614 // 查询到ssrc不一致且开启了ssrc校验则需要针对处理 753 // 查询到ssrc不一致且开启了ssrc校验则需要针对处理
615 if (ssrcInfo.getSsrc().equals(ssrcInResponse)) { 754 if (ssrcInfo.getSsrc().equals(ssrcInResponse)) {
616 if (device.getStreamMode().equalsIgnoreCase("TCP-ACTIVE")) { 755 if (device.getStreamMode().equalsIgnoreCase("TCP-ACTIVE")) {
  756 + String substring = contentString.substring(0, contentString.indexOf("y="));
617 try { 757 try {
618 - Gb28181Sdp gb28181Sdp = SipUtils.parseSDP(contentString);  
619 - SessionDescription sdp = gb28181Sdp.getBaseSdb(); 758 + SessionDescription sdp = SdpFactory.getInstance().createSessionDescription(substring);
620 int port = -1; 759 int port = -1;
621 Vector mediaDescriptions = sdp.getMediaDescriptions(true); 760 Vector mediaDescriptions = sdp.getMediaDescriptions(true);
622 for (Object description : mediaDescriptions) { 761 for (Object description : mediaDescriptions) {
@@ -684,6 +823,8 @@ public class PlayServiceImpl implements IPlayService { @@ -684,6 +823,8 @@ public class PlayServiceImpl implements IPlayService {
684 } 823 }
685 824
686 dynamicTask.stop(playBackTimeOutTaskKey); 825 dynamicTask.stop(playBackTimeOutTaskKey);
  826 + // 释放ssrc
  827 + mediaServerService.releaseSsrc(mediaServerItem.getId(), ssrcInfo.getSsrc());
687 828
688 streamSession.remove(device.getDeviceId(), channelId, ssrcInfo.getStream()); 829 streamSession.remove(device.getDeviceId(), channelId, ssrcInfo.getStream());
689 830
@@ -799,15 +940,17 @@ public class PlayServiceImpl implements IPlayService { @@ -799,15 +940,17 @@ public class PlayServiceImpl implements IPlayService {
799 ResponseEvent responseEvent = (ResponseEvent) eventResult.event; 940 ResponseEvent responseEvent = (ResponseEvent) eventResult.event;
800 String contentString = new String(responseEvent.getResponse().getRawContent()); 941 String contentString = new String(responseEvent.getResponse().getRawContent());
801 // 获取ssrc 942 // 获取ssrc
802 - String ssrcInResponse = SipUtils.getSsrcFromSdp(contentString); 943 + int ssrcIndex = contentString.indexOf("y=");
803 // 检查是否有y字段 944 // 检查是否有y字段
804 - if (ssrcInResponse != null) { 945 + if (ssrcIndex >= 0) {
  946 + //ssrc规定长度为10字节,不取余下长度以避免后续还有“f=”字段 TODO 后续对不规范的非10位ssrc兼容
  947 + String ssrcInResponse = contentString.substring(ssrcIndex + 2, ssrcIndex + 12);
805 // 查询到ssrc不一致且开启了ssrc校验则需要针对处理 948 // 查询到ssrc不一致且开启了ssrc校验则需要针对处理
806 if (ssrcInfo.getSsrc().equals(ssrcInResponse)) { 949 if (ssrcInfo.getSsrc().equals(ssrcInResponse)) {
807 if (device.getStreamMode().equalsIgnoreCase("TCP-ACTIVE")) { 950 if (device.getStreamMode().equalsIgnoreCase("TCP-ACTIVE")) {
  951 + String substring = contentString.substring(0, contentString.indexOf("y="));
808 try { 952 try {
809 - Gb28181Sdp gb28181Sdp = SipUtils.parseSDP(contentString);  
810 - SessionDescription sdp = gb28181Sdp.getBaseSdb(); 953 + SessionDescription sdp = SdpFactory.getInstance().createSessionDescription(substring);
811 int port = -1; 954 int port = -1;
812 Vector mediaDescriptions = sdp.getMediaDescriptions(true); 955 Vector mediaDescriptions = sdp.getMediaDescriptions(true);
813 for (Object description : mediaDescriptions) { 956 for (Object description : mediaDescriptions) {
@@ -872,6 +1015,8 @@ public class PlayServiceImpl implements IPlayService { @@ -872,6 +1015,8 @@ public class PlayServiceImpl implements IPlayService {
872 } 1015 }
873 1016
874 dynamicTask.stop(downLoadTimeOutTaskKey); 1017 dynamicTask.stop(downLoadTimeOutTaskKey);
  1018 + // 释放ssrc
  1019 + mediaServerService.releaseSsrc(mediaServerItem.getId(), ssrcInfo.getSsrc());
875 1020
876 streamSession.remove(device.getDeviceId(), channelId, ssrcInfo.getStream()); 1021 streamSession.remove(device.getDeviceId(), channelId, ssrcInfo.getStream());
877 1022
@@ -972,6 +1117,7 @@ public class PlayServiceImpl implements IPlayService { @@ -972,6 +1117,7 @@ public class PlayServiceImpl implements IPlayService {
972 return streamInfo; 1117 return streamInfo;
973 } 1118 }
974 1119
  1120 +
975 @Override 1121 @Override
976 public void zlmServerOffline(String mediaServerId) { 1122 public void zlmServerOffline(String mediaServerId) {
977 // 处理正在向上推流的上级平台 1123 // 处理正在向上推流的上级平台
@@ -1108,14 +1254,18 @@ public class PlayServiceImpl implements IPlayService { @@ -1108,14 +1254,18 @@ public class PlayServiceImpl implements IPlayService {
1108 } 1254 }
1109 1255
1110 @Override 1256 @Override
1111 - public void getSnap(String deviceId, String channelId, String fileName, ErrorCallback errorCallback) { 1257 + public void getSnap(String deviceId, String channelId, String fileName,boolean isSubStream, ErrorCallback errorCallback) {
1112 Device device = deviceService.getDevice(deviceId); 1258 Device device = deviceService.getDevice(deviceId);
1113 if (device == null) { 1259 if (device == null) {
1114 errorCallback.run(InviteErrorCode.ERROR_FOR_PARAMETER_ERROR.getCode(), InviteErrorCode.ERROR_FOR_PARAMETER_ERROR.getMsg(), null); 1260 errorCallback.run(InviteErrorCode.ERROR_FOR_PARAMETER_ERROR.getCode(), InviteErrorCode.ERROR_FOR_PARAMETER_ERROR.getMsg(), null);
1115 return; 1261 return;
1116 } 1262 }
1117 -  
1118 - InviteInfo inviteInfo = inviteStreamService.getInviteInfoByDeviceAndChannel(InviteSessionType.PLAY, deviceId, channelId); 1263 + InviteInfo inviteInfo;
  1264 + if(device.isSwitchPrimarySubStream()){
  1265 + inviteInfo = inviteStreamService.getInviteInfoByDeviceAndChannel(InviteSessionType.PLAY, deviceId, channelId,isSubStream);
  1266 + }else {
  1267 + inviteInfo = inviteStreamService.getInviteInfoByDeviceAndChannel(InviteSessionType.PLAY, deviceId, channelId);
  1268 + }
1119 if (inviteInfo != null) { 1269 if (inviteInfo != null) {
1120 if (inviteInfo.getStreamInfo() != null) { 1270 if (inviteInfo.getStreamInfo() != null) {
1121 // 已存在线直接截图 1271 // 已存在线直接截图
@@ -1130,10 +1280,9 @@ public class PlayServiceImpl implements IPlayService { @@ -1130,10 +1280,9 @@ public class PlayServiceImpl implements IPlayService {
1130 // 请求截图 1280 // 请求截图
1131 logger.info("[请求截图]: " + fileName); 1281 logger.info("[请求截图]: " + fileName);
1132 zlmresTfulUtils.getSnap(mediaServerItemInuse, streamUrl, 15, 1, path, fileName); 1282 zlmresTfulUtils.getSnap(mediaServerItemInuse, streamUrl, 15, 1, path, fileName);
1133 - String filePath = path + File.separator + fileName;  
1134 File snapFile = new File(path + File.separator + fileName); 1283 File snapFile = new File(path + File.separator + fileName);
1135 if (snapFile.exists()) { 1284 if (snapFile.exists()) {
1136 - errorCallback.run(InviteErrorCode.SUCCESS.getCode(), InviteErrorCode.SUCCESS.getMsg(), filePath); 1285 + errorCallback.run(InviteErrorCode.SUCCESS.getCode(), InviteErrorCode.SUCCESS.getMsg(), snapFile.getAbsoluteFile());
1137 }else { 1286 }else {
1138 errorCallback.run(InviteErrorCode.FAIL.getCode(), InviteErrorCode.FAIL.getMsg(), null); 1287 errorCallback.run(InviteErrorCode.FAIL.getCode(), InviteErrorCode.FAIL.getMsg(), null);
1139 } 1288 }
@@ -1142,11 +1291,11 @@ public class PlayServiceImpl implements IPlayService { @@ -1142,11 +1291,11 @@ public class PlayServiceImpl implements IPlayService {
1142 } 1291 }
1143 1292
1144 MediaServerItem newMediaServerItem = getNewMediaServerItem(device); 1293 MediaServerItem newMediaServerItem = getNewMediaServerItem(device);
1145 - play(newMediaServerItem, deviceId, channelId, (code, msg, data)->{ 1294 + play(newMediaServerItem, deviceId, channelId,isSubStream, (code, msg, data)->{
1146 if (code == InviteErrorCode.SUCCESS.getCode()) { 1295 if (code == InviteErrorCode.SUCCESS.getCode()) {
1147 InviteInfo inviteInfoForPlay = inviteStreamService.getInviteInfoByDeviceAndChannel(InviteSessionType.PLAY, deviceId, channelId); 1296 InviteInfo inviteInfoForPlay = inviteStreamService.getInviteInfoByDeviceAndChannel(InviteSessionType.PLAY, deviceId, channelId);
1148 if (inviteInfoForPlay != null && inviteInfoForPlay.getStreamInfo() != null) { 1297 if (inviteInfoForPlay != null && inviteInfoForPlay.getStreamInfo() != null) {
1149 - getSnap(deviceId, channelId, fileName, errorCallback); 1298 + getSnap(deviceId, channelId, fileName,isSubStream, errorCallback);
1150 }else { 1299 }else {
1151 errorCallback.run(InviteErrorCode.FAIL.getCode(), InviteErrorCode.FAIL.getMsg(), null); 1300 errorCallback.run(InviteErrorCode.FAIL.getCode(), InviteErrorCode.FAIL.getMsg(), null);
1152 } 1301 }
@@ -1156,4 +1305,17 @@ public class PlayServiceImpl implements IPlayService { @@ -1156,4 +1305,17 @@ public class PlayServiceImpl implements IPlayService {
1156 }); 1305 });
1157 } 1306 }
1158 1307
  1308 +
  1309 + /*======================设备主子码流逻辑START=========================*/
  1310 + public StreamInfo onPublishHandler(MediaServerItem mediaServerItem, JSONObject resonse, String deviceId, String channelId,boolean isSubStream) {
  1311 + String streamId = resonse.getString("stream");
  1312 + JSONArray tracks = resonse.getJSONArray("tracks");
  1313 + StreamInfo streamInfo = mediaService.getStreamInfoByAppAndStream(mediaServerItem, "rtp", streamId, tracks, null);
  1314 + streamInfo.setDeviceID(deviceId);
  1315 + streamInfo.setChannelId(channelId);
  1316 + streamInfo.setSubStream(isSubStream);
  1317 + return streamInfo;
  1318 + }
  1319 + /*======================设备主子码流逻辑END=========================*/
  1320 +
1159 } 1321 }
src/main/java/com/genersoft/iot/vmp/storager/dao/DeviceChannelMapper.java
@@ -451,6 +451,10 @@ public interface DeviceChannelMapper { @@ -451,6 +451,10 @@ public interface DeviceChannelMapper {
451 @Select("select count(1) from wvp_device_channel") 451 @Select("select count(1) from wvp_device_channel")
452 int getAllChannelCount(); 452 int getAllChannelCount();
453 453
  454 + // 设备主子码流逻辑START
  455 + @Update(value = {"UPDATE wvp_device_channel SET stream_id=null WHERE device_id=#{deviceId}"})
  456 + void clearPlay(String deviceId);
  457 + // 设备主子码流逻辑END
454 @Select(value = {" <script>" + 458 @Select(value = {" <script>" +
455 "select * " + 459 "select * " +
456 "from device_channel " + 460 "from device_channel " +
@@ -460,4 +464,5 @@ public interface DeviceChannelMapper { @@ -460,4 +464,5 @@ public interface DeviceChannelMapper {
460 " <if test='onlyCatalog == true '> and parental = 1 </if>" + 464 " <if test='onlyCatalog == true '> and parental = 1 </if>" +
461 " </script>"}) 465 " </script>"})
462 List<DeviceChannel> getSubChannelsByDeviceId(String deviceId, String parentId, boolean onlyCatalog); 466 List<DeviceChannel> getSubChannelsByDeviceId(String deviceId, String parentId, boolean onlyCatalog);
  467 +
463 } 468 }
src/main/java/com/genersoft/iot/vmp/storager/dao/DeviceMapper.java
@@ -42,6 +42,7 @@ public interface DeviceMapper { @@ -42,6 +42,7 @@ public interface DeviceMapper {
42 "geo_coord_sys," + 42 "geo_coord_sys," +
43 "on_line," + 43 "on_line," +
44 "media_server_id," + 44 "media_server_id," +
  45 + "switch_primary_sub_stream," +
45 "(SELECT count(0) FROM wvp_device_channel WHERE device_id=wvp_device.device_id) as channel_count "+ 46 "(SELECT count(0) FROM wvp_device_channel WHERE device_id=wvp_device.device_id) as channel_count "+
46 " FROM wvp_device WHERE device_id = #{deviceId}") 47 " FROM wvp_device WHERE device_id = #{deviceId}")
47 Device getDeviceByDeviceId(String deviceId); 48 Device getDeviceByDeviceId(String deviceId);
@@ -157,6 +158,7 @@ public interface DeviceMapper { @@ -157,6 +158,7 @@ public interface DeviceMapper {
157 "geo_coord_sys,"+ 158 "geo_coord_sys,"+
158 "on_line,"+ 159 "on_line,"+
159 "media_server_id,"+ 160 "media_server_id,"+
  161 + "switch_primary_sub_stream switchPrimarySubStream,"+
160 "(SELECT count(0) FROM wvp_device_channel WHERE device_id=de.device_id) as channel_count " + 162 "(SELECT count(0) FROM wvp_device_channel WHERE device_id=de.device_id) as channel_count " +
161 "FROM wvp_device de" + 163 "FROM wvp_device de" +
162 "<if test=\"onLine != null\"> where on_line=${onLine}</if>"+ 164 "<if test=\"onLine != null\"> where on_line=${onLine}</if>"+
@@ -246,6 +248,7 @@ public interface DeviceMapper { @@ -246,6 +248,7 @@ public interface DeviceMapper {
246 "<if test=\"ssrcCheck != null\">, ssrc_check=#{ssrcCheck}</if>" + 248 "<if test=\"ssrcCheck != null\">, ssrc_check=#{ssrcCheck}</if>" +
247 "<if test=\"asMessageChannel != null\">, as_message_channel=#{asMessageChannel}</if>" + 249 "<if test=\"asMessageChannel != null\">, as_message_channel=#{asMessageChannel}</if>" +
248 "<if test=\"geoCoordSys != null\">, geo_coord_sys=#{geoCoordSys}</if>" + 250 "<if test=\"geoCoordSys != null\">, geo_coord_sys=#{geoCoordSys}</if>" +
  251 + "<if test=\"switchPrimarySubStream != null\">, switch_primary_sub_stream=#{switchPrimarySubStream}</if>" +
249 "<if test=\"mediaServerId != null\">, media_server_id=#{mediaServerId}</if>" + 252 "<if test=\"mediaServerId != null\">, media_server_id=#{mediaServerId}</if>" +
250 "WHERE device_id=#{deviceId}"+ 253 "WHERE device_id=#{deviceId}"+
251 " </script>"}) 254 " </script>"})
@@ -263,7 +266,8 @@ public interface DeviceMapper { @@ -263,7 +266,8 @@ public interface DeviceMapper {
263 "as_message_channel,"+ 266 "as_message_channel,"+
264 "geo_coord_sys,"+ 267 "geo_coord_sys,"+
265 "on_line,"+ 268 "on_line,"+
266 - "media_server_id"+ 269 + "media_server_id,"+
  270 + "switch_primary_sub_stream"+
267 ") VALUES (" + 271 ") VALUES (" +
268 "#{deviceId}," + 272 "#{deviceId}," +
269 "#{name}," + 273 "#{name}," +
@@ -276,7 +280,8 @@ public interface DeviceMapper { @@ -276,7 +280,8 @@ public interface DeviceMapper {
276 "#{asMessageChannel}," + 280 "#{asMessageChannel}," +
277 "#{geoCoordSys}," + 281 "#{geoCoordSys}," +
278 "#{onLine}," + 282 "#{onLine}," +
279 - "#{mediaServerId}" + 283 + "#{mediaServerId}," +
  284 + "#{switchPrimarySubStream}" +
280 ")") 285 ")")
281 void addCustomDevice(Device device); 286 void addCustomDevice(Device device);
282 287
src/main/java/com/genersoft/iot/vmp/vmanager/gb28181/play/PlayController.java
@@ -26,7 +26,6 @@ import com.genersoft.iot.vmp.storager.IRedisCatchStorage; @@ -26,7 +26,6 @@ import com.genersoft.iot.vmp.storager.IRedisCatchStorage;
26 import com.genersoft.iot.vmp.storager.IVideoManagerStorage; 26 import com.genersoft.iot.vmp.storager.IVideoManagerStorage;
27 import com.genersoft.iot.vmp.utils.DateUtil; 27 import com.genersoft.iot.vmp.utils.DateUtil;
28 import com.genersoft.iot.vmp.vmanager.bean.ErrorCode; 28 import com.genersoft.iot.vmp.vmanager.bean.ErrorCode;
29 -import com.genersoft.iot.vmp.vmanager.bean.SnapPath;  
30 import com.genersoft.iot.vmp.vmanager.bean.StreamContent; 29 import com.genersoft.iot.vmp.vmanager.bean.StreamContent;
31 import com.genersoft.iot.vmp.vmanager.bean.WVPResult; 30 import com.genersoft.iot.vmp.vmanager.bean.WVPResult;
32 import io.swagger.v3.oas.annotations.Operation; 31 import io.swagger.v3.oas.annotations.Operation;
@@ -41,7 +40,6 @@ import org.springframework.web.context.request.async.DeferredResult; @@ -41,7 +40,6 @@ import org.springframework.web.context.request.async.DeferredResult;
41 import javax.servlet.http.HttpServletRequest; 40 import javax.servlet.http.HttpServletRequest;
42 import javax.sip.InvalidArgumentException; 41 import javax.sip.InvalidArgumentException;
43 import javax.sip.SipException; 42 import javax.sip.SipException;
44 -import java.io.File;  
45 import java.text.ParseException; 43 import java.text.ParseException;
46 import java.util.List; 44 import java.util.List;
47 import java.util.UUID; 45 import java.util.UUID;
@@ -90,16 +88,17 @@ public class PlayController { @@ -90,16 +88,17 @@ public class PlayController {
90 @Operation(summary = "开始点播") 88 @Operation(summary = "开始点播")
91 @Parameter(name = "deviceId", description = "设备国标编号", required = true) 89 @Parameter(name = "deviceId", description = "设备国标编号", required = true)
92 @Parameter(name = "channelId", description = "通道国标编号", required = true) 90 @Parameter(name = "channelId", description = "通道国标编号", required = true)
  91 + @Parameter(name = "isSubStream", description = "是否子码流(true-子码流,false-主码流),默认为false", required = true)
93 @GetMapping("/start/{deviceId}/{channelId}") 92 @GetMapping("/start/{deviceId}/{channelId}")
94 public DeferredResult<WVPResult<StreamContent>> play(HttpServletRequest request, @PathVariable String deviceId, 93 public DeferredResult<WVPResult<StreamContent>> play(HttpServletRequest request, @PathVariable String deviceId,
95 - @PathVariable String channelId) { 94 + @PathVariable String channelId,boolean isSubStream) {
96 95
97 // 获取可用的zlm 96 // 获取可用的zlm
98 Device device = storager.queryVideoDevice(deviceId); 97 Device device = storager.queryVideoDevice(deviceId);
99 MediaServerItem newMediaServerItem = playService.getNewMediaServerItem(device); 98 MediaServerItem newMediaServerItem = playService.getNewMediaServerItem(device);
100 99
101 RequestMessage requestMessage = new RequestMessage(); 100 RequestMessage requestMessage = new RequestMessage();
102 - String key = DeferredResultHolder.CALLBACK_CMD_PLAY + deviceId + channelId; 101 + String key = DeferredResultHolder.getPlayKey(deviceId,channelId,device.isSwitchPrimarySubStream(),isSubStream);
103 requestMessage.setKey(key); 102 requestMessage.setKey(key);
104 String uuid = UUID.randomUUID().toString(); 103 String uuid = UUID.randomUUID().toString();
105 requestMessage.setId(uuid); 104 requestMessage.setId(uuid);
@@ -118,7 +117,7 @@ public class PlayController { @@ -118,7 +117,7 @@ public class PlayController {
118 // 录像查询以channelId作为deviceId查询 117 // 录像查询以channelId作为deviceId查询
119 resultHolder.put(key, uuid, result); 118 resultHolder.put(key, uuid, result);
120 119
121 - playService.play(newMediaServerItem, deviceId, channelId, (code, msg, data) -> { 120 + playService.play(newMediaServerItem, deviceId, channelId,isSubStream, (code, msg, data) -> {
122 WVPResult<StreamContent> wvpResult = new WVPResult<>(); 121 WVPResult<StreamContent> wvpResult = new WVPResult<>();
123 if (code == InviteErrorCode.SUCCESS.getCode()) { 122 if (code == InviteErrorCode.SUCCESS.getCode()) {
124 wvpResult.setCode(ErrorCode.SUCCESS.getCode()); 123 wvpResult.setCode(ErrorCode.SUCCESS.getCode());
@@ -144,8 +143,9 @@ public class PlayController { @@ -144,8 +143,9 @@ public class PlayController {
144 @Operation(summary = "停止点播") 143 @Operation(summary = "停止点播")
145 @Parameter(name = "deviceId", description = "设备国标编号", required = true) 144 @Parameter(name = "deviceId", description = "设备国标编号", required = true)
146 @Parameter(name = "channelId", description = "通道国标编号", required = true) 145 @Parameter(name = "channelId", description = "通道国标编号", required = true)
  146 + @Parameter(name = "isSubStream", description = "是否子码流(true-子码流,false-主码流),默认为false", required = true)
147 @GetMapping("/stop/{deviceId}/{channelId}") 147 @GetMapping("/stop/{deviceId}/{channelId}")
148 - public JSONObject playStop(@PathVariable String deviceId, @PathVariable String channelId) { 148 + public JSONObject playStop(@PathVariable String deviceId, @PathVariable String channelId,boolean isSubStream) {
149 149
150 logger.debug(String.format("设备预览/回放停止API调用,streamId:%s_%s", deviceId, channelId )); 150 logger.debug(String.format("设备预览/回放停止API调用,streamId:%s_%s", deviceId, channelId ));
151 151
@@ -158,7 +158,12 @@ public class PlayController { @@ -158,7 +158,12 @@ public class PlayController {
158 throw new ControllerException(ErrorCode.ERROR100.getCode(), "设备[" + deviceId + "]不存在"); 158 throw new ControllerException(ErrorCode.ERROR100.getCode(), "设备[" + deviceId + "]不存在");
159 } 159 }
160 160
161 - InviteInfo inviteInfo = inviteStreamService.getInviteInfoByDeviceAndChannel(InviteSessionType.PLAY, deviceId, channelId); 161 + InviteInfo inviteInfo =null;
  162 + if(device.isSwitchPrimarySubStream()){
  163 + inviteInfo = inviteStreamService.getInviteInfoByDeviceAndChannel(InviteSessionType.PLAY, deviceId, channelId,isSubStream);
  164 + }else {
  165 + inviteInfo = inviteStreamService.getInviteInfoByDeviceAndChannel(InviteSessionType.PLAY, deviceId, channelId);
  166 + }
162 if (inviteInfo == null) { 167 if (inviteInfo == null) {
163 throw new ControllerException(ErrorCode.ERROR100.getCode(), "点播未找到"); 168 throw new ControllerException(ErrorCode.ERROR100.getCode(), "点播未找到");
164 } 169 }
@@ -171,12 +176,17 @@ public class PlayController { @@ -171,12 +176,17 @@ public class PlayController {
171 throw new ControllerException(ErrorCode.ERROR100.getCode(), "命令发送失败: " + e.getMessage()); 176 throw new ControllerException(ErrorCode.ERROR100.getCode(), "命令发送失败: " + e.getMessage());
172 } 177 }
173 } 178 }
174 - inviteStreamService.removeInviteInfoByDeviceAndChannel(InviteSessionType.PLAY, deviceId, channelId); 179 + if(device.isSwitchPrimarySubStream()){
  180 + inviteStreamService.removeInviteInfoByDeviceAndChannel(InviteSessionType.PLAY, deviceId, channelId,isSubStream);
  181 + }else {
  182 + inviteStreamService.removeInviteInfoByDeviceAndChannel(InviteSessionType.PLAY, deviceId, channelId);
  183 + storager.stopPlay(deviceId, channelId);
  184 + }
175 185
176 - storager.stopPlay(deviceId, channelId);  
177 JSONObject json = new JSONObject(); 186 JSONObject json = new JSONObject();
178 json.put("deviceId", deviceId); 187 json.put("deviceId", deviceId);
179 json.put("channelId", channelId); 188 json.put("channelId", channelId);
  189 + json.put("isSubStream", isSubStream);
180 return json; 190 return json;
181 } 191 }
182 192
@@ -343,30 +353,27 @@ public class PlayController { @@ -343,30 +353,27 @@ public class PlayController {
343 @Operation(summary = "获取截图") 353 @Operation(summary = "获取截图")
344 @Parameter(name = "deviceId", description = "设备国标编号", required = true) 354 @Parameter(name = "deviceId", description = "设备国标编号", required = true)
345 @Parameter(name = "channelId", description = "通道国标编号", required = true) 355 @Parameter(name = "channelId", description = "通道国标编号", required = true)
  356 + @Parameter(name = "isSubStream", description = "是否子码流(true-子码流,false-主码流),默认为false", required = true)
346 @GetMapping("/snap") 357 @GetMapping("/snap")
347 - public DeferredResult<String> getSnap(HttpServletRequest request, String deviceId, String channelId) { 358 + public DeferredResult<String> getSnap(String deviceId, String channelId,boolean isSubStream) {
348 if (logger.isDebugEnabled()) { 359 if (logger.isDebugEnabled()) {
349 logger.debug("获取截图: {}/{}", deviceId, channelId); 360 logger.debug("获取截图: {}/{}", deviceId, channelId);
350 } 361 }
351 362
  363 + Device device = storager.queryVideoDevice(deviceId);
352 DeferredResult<String> result = new DeferredResult<>(3 * 1000L); 364 DeferredResult<String> result = new DeferredResult<>(3 * 1000L);
353 - String key = DeferredResultHolder.CALLBACK_CMD_SNAP + deviceId; 365 + String key = DeferredResultHolder.getSnapKey(deviceId,channelId,device.isSwitchPrimarySubStream(),isSubStream);
354 String uuid = UUID.randomUUID().toString(); 366 String uuid = UUID.randomUUID().toString();
355 resultHolder.put(key, uuid, result); 367 resultHolder.put(key, uuid, result);
356 368
357 RequestMessage message = new RequestMessage(); 369 RequestMessage message = new RequestMessage();
358 message.setKey(key); 370 message.setKey(key);
359 message.setId(uuid); 371 message.setId(uuid);
360 - String nowForUrl = DateUtil.getNowForUrl();  
361 - String fileName = deviceId + "_" + channelId + "_" + nowForUrl + ".jpg";  
362 372
363 - playService.getSnap(deviceId, channelId, fileName, (code, msg, data) -> { 373 + String fileName = deviceId + "_" + channelId + "_" + DateUtil.getNowForUrl() + "jpg";
  374 + playService.getSnap(deviceId, channelId, fileName,isSubStream, (code, msg, data) -> {
364 if (code == InviteErrorCode.SUCCESS.getCode()) { 375 if (code == InviteErrorCode.SUCCESS.getCode()) {
365 - File snapFile = new File((String)data);  
366 - String fileNameForUrl = deviceId + "/" + channelId + "?mark=" + nowForUrl;  
367 - String uri = request.getRequestURL().toString().replace(request.getRequestURI(), "/api/device/query/snap/" + fileNameForUrl);  
368 - SnapPath snapPath = SnapPath.getInstance((String) data, snapFile.getAbsolutePath(), uri);  
369 - message.setData(snapPath); 376 + message.setData(data);
370 }else { 377 }else {
371 message.setData(WVPResult.fail(code, msg)); 378 message.setData(WVPResult.fail(code, msg));
372 } 379 }
src/main/java/com/genersoft/iot/vmp/web/gb28181/ApiStreamController.java
@@ -122,7 +122,7 @@ public class ApiStreamController { @@ -122,7 +122,7 @@ public class ApiStreamController {
122 MediaServerItem newMediaServerItem = playService.getNewMediaServerItem(device); 122 MediaServerItem newMediaServerItem = playService.getNewMediaServerItem(device);
123 123
124 124
125 - playService.play(newMediaServerItem, serial, code, (errorCode, msg, data) -> { 125 + playService.play(newMediaServerItem, serial, code,false, (errorCode, msg, data) -> {
126 if (errorCode == InviteErrorCode.SUCCESS.getCode()) { 126 if (errorCode == InviteErrorCode.SUCCESS.getCode()) {
127 InviteInfo inviteInfo = inviteStreamService.getInviteInfoByDeviceAndChannel(InviteSessionType.PLAY, serial, code); 127 InviteInfo inviteInfo = inviteStreamService.getInviteInfoByDeviceAndChannel(InviteSessionType.PLAY, serial, code);
128 if (inviteInfo != null && inviteInfo.getStreamInfo() != null) { 128 if (inviteInfo != null && inviteInfo.getStreamInfo() != null) {
src/main/resources/application-dev.yml
1 spring: 1 spring:
  2 + thymeleaf:
  3 + cache: false
2 # [可选]上传文件大小限制 4 # [可选]上传文件大小限制
3 servlet: 5 servlet:
4 multipart: 6 multipart:
@@ -11,18 +13,18 @@ spring: @@ -11,18 +13,18 @@ spring:
11 # [必须修改] 端口号 13 # [必须修改] 端口号
12 port: 6379 14 port: 6379
13 # [可选] 数据库 DB 15 # [可选] 数据库 DB
14 - database: 6 16 + database: 7
15 # [可选] 访问密码,若你的redis服务器没有设置密码,就不需要用密码去连接 17 # [可选] 访问密码,若你的redis服务器没有设置密码,就不需要用密码去连接
16 - password: face2020 18 + password:
17 # [可选] 超时时间 19 # [可选] 超时时间
18 timeout: 10000 20 timeout: 10000
19 # mysql数据源 21 # mysql数据源
20 datasource: 22 datasource:
21 type: com.zaxxer.hikari.HikariDataSource 23 type: com.zaxxer.hikari.HikariDataSource
22 driver-class-name: com.mysql.cj.jdbc.Driver 24 driver-class-name: com.mysql.cj.jdbc.Driver
23 - url: jdbc:mysql://127.0.0.1:3306/wvp2?useUnicode=true&characterEncoding=UTF8&rewriteBatchedStatements=true&serverTimezone=PRC&useSSL=false&allowMultiQueries=true 25 + url: jdbc:mysql://127.0.0.1:3306/test_gb-89wulian?useUnicode=true&characterEncoding=UTF8&rewriteBatchedStatements=true&serverTimezone=PRC&useSSL=false&allowMultiQueries=true
24 username: root 26 username: root
25 - password: 123456 27 + password: root
26 hikari: 28 hikari:
27 connection-timeout: 20000 # 是客户端等待连接池连接的最大毫秒数 29 connection-timeout: 20000 # 是客户端等待连接池连接的最大毫秒数
28 initialSize: 10 # 连接池初始化连接数 30 initialSize: 10 # 连接池初始化连接数
@@ -30,11 +32,19 @@ spring: @@ -30,11 +32,19 @@ spring:
30 minimum-idle: 5 # 连接池最小空闲连接数 32 minimum-idle: 5 # 连接池最小空闲连接数
31 idle-timeout: 300000 # 允许连接在连接池中空闲的最长时间(以毫秒为单位) 33 idle-timeout: 300000 # 允许连接在连接池中空闲的最长时间(以毫秒为单位)
32 max-lifetime: 1200000 # 是池中连接关闭后的最长生命周期(以毫秒为单位) 34 max-lifetime: 1200000 # 是池中连接关闭后的最长生命周期(以毫秒为单位)
33 -  
34 -  
35 #[可选] WVP监听的HTTP端口, 网页和接口调用都是这个端口 35 #[可选] WVP监听的HTTP端口, 网页和接口调用都是这个端口
36 server: 36 server:
37 - port: 18080 37 + port: 18978
  38 + # [可选] HTTPS配置, 默认不开启
  39 + ssl:
  40 + # [可选] 是否开启HTTPS访问
  41 + enabled: false
  42 + # [可选] 证书文件路径,放置在resource/目录下即可,修改xxx为文件名
  43 + key-store: classpath:test.monitor.89iot.cn.jks
  44 + # [可选] 证书密码
  45 + key-store-password: gpf64qmw
  46 + # [可选] 证书类型, 默认为jks,根据实际修改
  47 + key-store-type: JKS
38 48
39 # 作为28181服务器的配置 49 # 作为28181服务器的配置
40 sip: 50 sip:
@@ -42,26 +52,36 @@ sip: @@ -42,26 +52,36 @@ sip:
42 # 如果要监听多张网卡,可以使用逗号分隔多个IP, 例如: 192.168.1.4,10.0.0.4 52 # 如果要监听多张网卡,可以使用逗号分隔多个IP, 例如: 192.168.1.4,10.0.0.4
43 # 如果不明白,就使用0.0.0.0,大部分情况都是可以的 53 # 如果不明白,就使用0.0.0.0,大部分情况都是可以的
44 # 请不要使用127.0.0.1,任何包括localhost在内的域名都是不可以的。 54 # 请不要使用127.0.0.1,任何包括localhost在内的域名都是不可以的。
45 - ip: 192.168.41.16 55 + ip: 192.168.1.18
46 # [可选] 28181服务监听的端口 56 # [可选] 28181服务监听的端口
47 - port: 5060 57 + port: 8116
48 # 根据国标6.1.2中规定,domain宜采用ID统一编码的前十位编码。国标附录D中定义前8位为中心编码(由省级、市级、区级、基层编号组成,参照GB/T 2260-2007) 58 # 根据国标6.1.2中规定,domain宜采用ID统一编码的前十位编码。国标附录D中定义前8位为中心编码(由省级、市级、区级、基层编号组成,参照GB/T 2260-2007)
49 # 后两位为行业编码,定义参照附录D.3 59 # 后两位为行业编码,定义参照附录D.3
50 # 3701020049标识山东济南历下区 信息行业接入 60 # 3701020049标识山东济南历下区 信息行业接入
51 # [可选] 61 # [可选]
52 - domain: 4401020049 62 + domain: 4101050000
53 # [可选] 63 # [可选]
54 - id: 44010200492000000001 64 + id: 41010500002000000001
55 # [可选] 默认设备认证密码,后续扩展使用设备单独密码, 移除密码将不进行校验 65 # [可选] 默认设备认证密码,后续扩展使用设备单独密码, 移除密码将不进行校验
56 - password: admin123 66 + password: bajiuwulian1006
  67 + # 是否存储alarm信息
  68 + alarm: true
57 69
58 #zlm 默认服务器配置 70 #zlm 默认服务器配置
59 media: 71 media:
60 - id: FQ3TF8yT83wh5Wvz 72 + id: 89wulian-one
61 # [必须修改] zlm服务器的内网IP 73 # [必须修改] zlm服务器的内网IP
62 - ip: 192.168.41.16 74 + ip: 192.168.1.18
63 # [必须修改] zlm服务器的http.port 75 # [必须修改] zlm服务器的http.port
64 - http-port: 8091 76 + http-port: 80
  77 + # [可选] 返回流地址时的ip,置空使用 media.ip
  78 + stream-ip: 192.168.1.18
  79 + # [可选] wvp在国标信令中使用的ip,此ip为摄像机可以访问到的ip, 置空使用 media.ip
  80 + sdp-ip: 192.168.1.18
  81 + # [可选] zlm服务器的hook所使用的IP, 默认使用sip.ip
  82 + hook-ip: 192.168.1.18
  83 + # [可选] zlm服务器的http.sslport, 置空使用zlm配置文件配置
  84 + http-ssl-port: 443
65 # [可选] zlm服务器的hook.admin_params=secret 85 # [可选] zlm服务器的hook.admin_params=secret
66 secret: 035c73f7-bb6b-4889-a715-d9eb2d1925cc 86 secret: 035c73f7-bb6b-4889-a715-d9eb2d1925cc
67 # 启用多端口模式, 多端口模式使用端口区分每路流,兼容性更好。 单端口使用流的ssrc区分, 点播超时建议使用多端口测试 87 # 启用多端口模式, 多端口模式使用端口区分每路流,兼容性更好。 单端口使用流的ssrc区分, 点播超时建议使用多端口测试
@@ -69,11 +89,24 @@ media: @@ -69,11 +89,24 @@ media:
69 # [可选] 是否启用多端口模式, 开启后会在portRange范围内选择端口用于媒体流传输 89 # [可选] 是否启用多端口模式, 开启后会在portRange范围内选择端口用于媒体流传输
70 enable: true 90 enable: true
71 # [可选] 在此范围内选择端口用于媒体流传输, 必须提前在zlm上配置该属性,不然自动配置此属性可能不成功 91 # [可选] 在此范围内选择端口用于媒体流传输, 必须提前在zlm上配置该属性,不然自动配置此属性可能不成功
72 - port-range: 30000,30500 # 端口范围 92 + port-range: 50000,50300 # 端口范围
73 # [可选] 国标级联在此范围内选择端口发送媒体流, 93 # [可选] 国标级联在此范围内选择端口发送媒体流,
74 - send-port-range: 30000,30500 # 端口范围 94 + send-port-range: 50000,50300 # 端口范围
75 # 录像辅助服务, 部署此服务可以实现zlm录像的管理与下载, 0 表示不使用 95 # 录像辅助服务, 部署此服务可以实现zlm录像的管理与下载, 0 表示不使用
76 record-assist-port: 18081 96 record-assist-port: 18081
  97 +# [根据业务需求配置]
  98 +user-settings:
  99 + # 点播/录像回放 等待超时时间,单位:毫秒
  100 + play-timeout: 180000
  101 + # [可选] 自动点播, 使用固定流地址进行播放时,如果未点播则自动进行点播, 需要rtp.enable=true
  102 + auto-apply-play: true
  103 + # 设备/通道状态变化时发送消息
  104 + device-status-notify: true
  105 + # 跨域配置,配置你访问前端页面的地址即可, 可以配置多个
  106 + allowed-origins:
  107 + - http://localhost:8080
  108 + - http://127.0.0.1:8080
77 # [可选] 日志配置, 一般不需要改 109 # [可选] 日志配置, 一般不需要改
78 logging: 110 logging:
79 config: classpath:logback-spring-local.xml 111 config: classpath:logback-spring-local.xml
  112 +
src/main/resources/application.yml
@@ -2,4 +2,4 @@ spring: @@ -2,4 +2,4 @@ spring:
2 application: 2 application:
3 name: wvp 3 name: wvp
4 profiles: 4 profiles:
5 - active: local  
6 \ No newline at end of file 5 \ No newline at end of file
  6 + active: dev
7 \ No newline at end of file 7 \ No newline at end of file
web_src/config/index.js
@@ -12,14 +12,14 @@ module.exports = { @@ -12,14 +12,14 @@ module.exports = {
12 assetsPublicPath: '/', 12 assetsPublicPath: '/',
13 proxyTable: { 13 proxyTable: {
14 '/debug': { 14 '/debug': {
15 - target: 'http://localhost:18080', 15 + target: 'http://localhost:18978',
16 changeOrigin: true, 16 changeOrigin: true,
17 pathRewrite: { 17 pathRewrite: {
18 '^/debug': '/' 18 '^/debug': '/'
19 } 19 }
20 }, 20 },
21 '/static/snap': { 21 '/static/snap': {
22 - target: 'http://localhost:18080', 22 + target: 'http://localhost:18978',
23 changeOrigin: true, 23 changeOrigin: true,
24 // pathRewrite: { 24 // pathRewrite: {
25 // '^/static/snap': '/static/snap' 25 // '^/static/snap': '/static/snap'
web_src/src/components/channelList.vue
@@ -26,6 +26,12 @@ @@ -26,6 +26,12 @@
26 <el-option label="在线" value="true"></el-option> 26 <el-option label="在线" value="true"></el-option>
27 <el-option label="离线" value="false"></el-option> 27 <el-option label="离线" value="false"></el-option>
28 </el-select> 28 </el-select>
  29 + 清晰度:
  30 + <el-select size="mini" style="margin-right: 1rem;" @change="search" v-model="isSubStream" placeholder="请选择"
  31 + default-first-option>
  32 + <el-option label="原画" :value="false"></el-option>
  33 + <el-option label="流畅" :value="true"></el-option>
  34 + </el-select>
29 </div> 35 </div>
30 <el-button icon="el-icon-refresh-right" circle size="mini" @click="refresh()"></el-button> 36 <el-button icon="el-icon-refresh-right" circle size="mini" @click="refresh()"></el-button>
31 <el-button v-if="showTree" icon="iconfont icon-list" circle size="mini" @click="switchList()"></el-button> 37 <el-button v-if="showTree" icon="iconfont icon-list" circle size="mini" @click="switchList()"></el-button>
@@ -146,6 +152,7 @@ export default { @@ -146,6 +152,7 @@ export default {
146 searchSrt: "", 152 searchSrt: "",
147 channelType: "", 153 channelType: "",
148 online: "", 154 online: "",
  155 + isSubStream: false,
149 winHeight: window.innerHeight - 200, 156 winHeight: window.innerHeight - 200,
150 currentPage: 1, 157 currentPage: 1,
151 count: 15, 158 count: 15,
@@ -237,7 +244,10 @@ export default { @@ -237,7 +244,10 @@ export default {
237 let that = this; 244 let that = this;
238 this.$axios({ 245 this.$axios({
239 method: 'get', 246 method: 'get',
240 - url: '/api/play/start/' + deviceId + '/' + channelId 247 + url: '/api/play/start/' + deviceId + '/' + channelId,
  248 + params:{
  249 + isSubStream: this.isSubStream
  250 + }
241 }).then(function (res) { 251 }).then(function (res) {
242 console.log(res) 252 console.log(res)
243 that.isLoging = false; 253 that.isLoging = false;
@@ -277,7 +287,10 @@ export default { @@ -277,7 +287,10 @@ export default {
277 var that = this; 287 var that = this;
278 this.$axios({ 288 this.$axios({
279 method: 'get', 289 method: 'get',
280 - url: '/api/play/stop/' + this.deviceId + "/" + itemData.channelId 290 + url: '/api/play/stop/' + this.deviceId + "/" + itemData.channelId,
  291 + params:{
  292 + isSubStream: this.isSubStream
  293 + }
281 }).then(function (res) { 294 }).then(function (res) {
282 that.initData(); 295 that.initData();
283 }).catch(function (error) { 296 }).catch(function (error) {
web_src/src/components/dialog/deviceEdit.vue
@@ -58,6 +58,12 @@ @@ -58,6 +58,12 @@
58 <el-form-item v-if="form.subscribeCycleForMobilePosition > 0" label="移动位置报送间隔" prop="subscribeCycleForCatalog" > 58 <el-form-item v-if="form.subscribeCycleForMobilePosition > 0" label="移动位置报送间隔" prop="subscribeCycleForCatalog" >
59 <el-input v-model="form.mobilePositionSubmissionInterval" clearable ></el-input> 59 <el-input v-model="form.mobilePositionSubmissionInterval" clearable ></el-input>
60 </el-form-item> 60 </el-form-item>
  61 + <el-form-item label="主子码流开关" prop="switchPrimarySubStream" >
  62 + <el-select v-model="form.switchPrimarySubStream" style="float: left; width: 100%" >
  63 + <el-option key="true" label="开启" :value="true"></el-option>
  64 + <el-option key="false" label="关闭" :value="false"></el-option>
  65 + </el-select>
  66 + </el-form-item>
61 <el-form-item label="其他选项"> 67 <el-form-item label="其他选项">
62 <el-checkbox label="SSRC校验" v-model="form.ssrcCheck" style="float: left"></el-checkbox> 68 <el-checkbox label="SSRC校验" v-model="form.ssrcCheck" style="float: left"></el-checkbox>
63 <el-checkbox label="作为消息通道" v-model="form.asMessageChannel" style="float: left"></el-checkbox> 69 <el-checkbox label="作为消息通道" v-model="form.asMessageChannel" style="float: left"></el-checkbox>