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 7 alter table device
8 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 1 package com.genersoft.iot.vmp.common;
2 2  
3 3 import com.genersoft.iot.vmp.service.bean.SSRCInfo;
  4 +import io.swagger.v3.oas.annotations.media.Schema;
4 5  
5 6 /**
6 7 * 记录每次发送invite消息的状态
... ... @@ -123,4 +124,40 @@ public class InviteInfo {
123 124 public void setStreamMode(String streamMode) {
124 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 528 }
529 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 1 package com.genersoft.iot.vmp.conf;
2 2  
  3 +import io.swagger.v3.oas.annotations.media.Schema;
3 4 import org.springframework.core.annotation.Order;
4 5 import org.springframework.boot.context.properties.ConfigurationProperties;
5 6 import org.springframework.stereotype.Component;
... ... @@ -25,11 +26,11 @@ public class UserSetting {
25 26  
26 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 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 189 private SipTransactionInfo sipTransactionInfo;
190 190  
191 191  
  192 +
  193 +
192 194 public String getDeviceId() {
193 195 return deviceId;
194 196 }
... ... @@ -447,4 +449,20 @@ public class Device {
447 449 public void setSipTransactionInfo(SipTransactionInfo sipTransactionInfo) {
448 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 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 98 * @param device 视频设备
99 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 266 * @param errorEvent sip错误订阅
267 267 */
268 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 270 ZlmHttpHookSubscribe.Event event, SipSubscribe.Event okEvent, SipSubscribe.Event errorEvent) throws InvalidArgumentException, SipException, ParseException {
271 271 String stream = ssrcInfo.getStream();
272 272  
... ... @@ -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 360 content.append("y=" + ssrcInfo.getSsrc() + "\r\n");//ssrc
345 361 // f字段:f= v/编码格式/分辨率/帧率/码率类型/码率大小a/编码格式/码率大小/采样率
346 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 372 // 这里为例避免一个通道的点播只有一个callID这个参数使用一个固定值
357 373 ResponseEvent responseEvent = (ResponseEvent) e.event;
358 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 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 142 // 可能是设备主动停止
143 143 Device device = storager.queryVideoDeviceByChannelId(platformGbId);
144 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 152 if (ssrcTransactionForPlay != null){
148 153 if (ssrcTransactionForPlay.getCallId().equals(callIdHeader.getCallId())){
149 154 // 释放ssrc
... ... @@ -153,10 +158,17 @@ public class ByeRequestProcessor extends SIPRequestProcessorParent implements In
153 158 }
154 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 169 inviteStreamService.removeInviteInfo(inviteInfo);
  170 + }
  171 + if (inviteInfo != null) {
160 172 if (inviteInfo.getStreamInfo() != null) {
161 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 489 }
490 490 sendRtpItem.setStreamId(streamId);
491 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 493 if (code == InviteErrorCode.SUCCESS.getCode()){
494 494 hookEvent.run(code, msg, data);
495 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 289 @ResponseBody
290 290 @PostMapping(value = "/on_stream_changed", produces = "application/json;charset=UTF-8")
291 291 public HookResult onStreamChanged(@RequestBody OnStreamChangedHookParam param) {
  292 +
292 293 if (param.isRegist()) {
293 294 logger.info("[ZLM HOOK] 流注册, {}->{}->{}/{}", param.getMediaServerId(), param.getSchema(), param.getApp(), param.getStream());
294 295 } else {
... ... @@ -310,11 +311,13 @@ public class ZLMHttpHookListener {
310 311  
311 312 List<OnStreamChangedHookParam.MediaTrack> tracks = param.getTracks();
312 313 // TODO 重构此处逻辑
  314 + boolean isPush = false;
313 315 if (param.isRegist()) {
314 316 // 处理流注册的鉴权信息
315 317 if (param.getOriginType() == OriginType.RTMP_PUSH.ordinal()
316 318 || param.getOriginType() == OriginType.RTSP_PUSH.ordinal()
317 319 || param.getOriginType() == OriginType.RTC_PUSH.ordinal()) {
  320 + isPush = true;
318 321 StreamAuthorityInfo streamAuthorityInfo = redisCatchStorage.getStreamAuthorityInfo(param.getApp(), param.getStream());
319 322 if (streamAuthorityInfo == null) {
320 323 streamAuthorityInfo = StreamAuthorityInfo.getInstanceByHook(param);
... ... @@ -328,7 +331,7 @@ public class ZLMHttpHookListener {
328 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 336 if (param.isRegist()) {
334 337 mediaServerService.addCount(param.getMediaServerId());
... ... @@ -342,10 +345,19 @@ public class ZLMHttpHookListener {
342 345 }
343 346  
344 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 362 } else {
351 363 if (!"rtp".equals(param.getApp())) {
... ... @@ -360,8 +372,6 @@ public class ZLMHttpHookListener {
360 372 StreamInfo streamInfoByAppAndStream = mediaService.getStreamInfoByAppAndStream(mediaInfo,
361 373 param.getApp(), param.getStream(), tracks, callId);
362 374 param.setStreamInfo(new StreamContent(streamInfoByAppAndStream));
363   - // 如果是拉流代理产生的,不需要写入推流
364   -
365 375 redisCatchStorage.addStream(mediaInfo, type, param.getApp(), param.getStream(), param);
366 376 if (param.getOriginType() == OriginType.RTSP_PUSH.ordinal()
367 377 || param.getOriginType() == OriginType.RTMP_PUSH.ordinal()
... ... @@ -450,6 +460,11 @@ public class ZLMHttpHookListener {
450 460 InviteInfo inviteInfo = inviteStreamService.getInviteInfoByStream(null, param.getStream());
451 461 // 点播
452 462 if (inviteInfo != null) {
  463 + // 录像下载
  464 + if (inviteInfo.getType() == InviteSessionType.DOWNLOAD) {
  465 + ret.put("close", false);
  466 + return ret;
  467 + }
453 468 // 收到无人观看说明流也没有在往上级推送
454 469 if (redisCatchStorage.isChannelSendingRTP(inviteInfo.getChannelId())) {
455 470 List<SendRtpItem> sendRtpItems = redisCatchStorage.querySendRTPServerByChnnelId(
... ... @@ -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 512 inviteStreamService.removeInviteInfo(inviteInfo.getType(), inviteInfo.getDeviceId(),
492 513 inviteInfo.getChannelId(), inviteInfo.getStream());
493 514 storager.stopPlay(inviteInfo.getDeviceId(), inviteInfo.getChannelId());
... ... @@ -499,7 +520,7 @@ public class ZLMHttpHookListener {
499 520 // 拉流代理
500 521 StreamProxyItem streamProxyItem = streamProxyService.getStreamProxyByAppAndStream(param.getApp(), param.getStream());
501 522 if (streamProxyItem != null) {
502   - if (streamProxyItem.isEnableRemoveNoneReader()) {
  523 + if (streamProxyItem.isEnableDisableNoneReader()) {
503 524 // 无人观看自动移除
504 525 ret.put("close", true);
505 526 streamProxyService.del(param.getApp(), param.getStream());
... ... @@ -544,12 +565,26 @@ public class ZLMHttpHookListener {
544 565  
545 566 if ("rtp".equals(param.getApp())) {
546 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 572 defaultResult.setResult(HookResult.SUCCESS());
549 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 588 Device device = redisCatchStorage.getDevice(deviceId);
554 589 if (device == null) {
555 590 defaultResult.setResult(new HookResult(ErrorCode.ERROR404.getCode(), ErrorCode.ERROR404.getMsg()));
... ... @@ -563,7 +598,7 @@ public class ZLMHttpHookListener {
563 598 logger.info("[ZLM HOOK] 流未找到, 发起自动点播:{}->{}->{}/{}", param.getMediaServerId(), param.getSchema(), param.getApp(), param.getStream());
564 599  
565 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 602 boolean exist = resultHolder.exist(key, null);
568 603 msg.setKey(key);
569 604 String uuid = UUID.randomUUID().toString();
... ... @@ -581,7 +616,7 @@ public class ZLMHttpHookListener {
581 616 resultHolder.put(key, uuid, result);
582 617  
583 618 if (!exist) {
584   - playService.play(mediaInfo, deviceId, channelId, (code, message, data) -> {
  619 + playService.play(mediaInfo, deviceId, channelId,isSubStream, (code, message, data) -> {
585 620 msg.setData(new HookResult(code, message));
586 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 4 import com.genersoft.iot.vmp.common.InviteSessionType;
5 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 72 * 统计同一个zlm下的国标收流个数
71 73 */
72 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 16 */
17 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 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 23 MediaServerItem getNewMediaServerItem(Device device);
24 24  
... ... @@ -43,5 +43,5 @@ public interface IPlayService {
43 43  
44 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 1 package com.genersoft.iot.vmp.service.impl;
2 2  
  3 +import com.genersoft.iot.vmp.common.InviteSessionType;
3 4 import com.genersoft.iot.vmp.common.VideoManagerConstants;
4 5 import com.genersoft.iot.vmp.conf.DynamicTask;
5 6 import com.genersoft.iot.vmp.conf.UserSetting;
  7 +import com.genersoft.iot.vmp.conf.exception.SsrcTransactionNotFoundException;
6 8 import com.genersoft.iot.vmp.gb28181.bean.*;
7 9 import com.genersoft.iot.vmp.gb28181.session.VideoStreamSessionManager;
8 10 import com.genersoft.iot.vmp.gb28181.task.ISubscribeTask;
9 11 import com.genersoft.iot.vmp.gb28181.task.impl.CatalogSubscribeTask;
10 12 import com.genersoft.iot.vmp.gb28181.task.impl.MobilePositionSubscribeTask;
11 13 import com.genersoft.iot.vmp.gb28181.transmit.cmd.ISIPCommander;
  14 +import com.genersoft.iot.vmp.gb28181.transmit.cmd.impl.SIPCommander;
12 15 import com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.response.cmd.CatalogResponseMessageHandler;
13 16 import com.genersoft.iot.vmp.service.IDeviceChannelService;
14 17 import com.genersoft.iot.vmp.service.IDeviceService;
... ... @@ -48,6 +51,8 @@ public class DeviceServiceImpl implements IDeviceService {
48 51 private final static Logger logger = LoggerFactory.getLogger(DeviceServiceImpl.class);
49 52  
50 53 @Autowired
  54 + private SIPCommander cmder;
  55 + @Autowired
51 56 private DynamicTask dynamicTask;
52 57  
53 58 @Autowired
... ... @@ -131,6 +136,10 @@ public class DeviceServiceImpl implements IDeviceService {
131 136 }
132 137 sync(device);
133 138 }else {
  139 +
  140 + if (deviceInDb != null) {
  141 + device.setSwitchPrimarySubStream(deviceInDb.isSwitchPrimarySubStream());
  142 + }
134 143 if(!device.isOnLine()){
135 144 device.setOnLine(true);
136 145 device.setCreateTime(now);
... ... @@ -460,6 +469,22 @@ public class DeviceServiceImpl implements IDeviceService {
460 469 logger.warn("更新设备时未找到设备信息");
461 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 488 if (!ObjectUtils.isEmpty(device.getName())) {
464 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 198 }
199 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 18 import com.genersoft.iot.vmp.gb28181.transmit.callback.DeferredResultHolder;
19 19 import com.genersoft.iot.vmp.gb28181.transmit.cmd.impl.SIPCommander;
20 20 import com.genersoft.iot.vmp.gb28181.transmit.cmd.impl.SIPCommanderFroPlatform;
21   -import com.genersoft.iot.vmp.gb28181.utils.SipUtils;
22 21 import com.genersoft.iot.vmp.media.zlm.AssistRESTfulUtils;
23 22 import com.genersoft.iot.vmp.media.zlm.ZLMRESTfulUtils;
24 23 import com.genersoft.iot.vmp.media.zlm.ZLMRTPServerFactory;
... ... @@ -116,28 +115,43 @@ public class PlayServiceImpl implements IPlayService {
116 115  
117 116  
118 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 119 if (mediaServerItem == null) {
121 120 throw new ControllerException(ErrorCode.ERROR100.getCode(), "未找到可用的zlm");
122 121 }
123 122  
124 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 130 if (inviteInfo != null ) {
128 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 138 return inviteInfo.getSsrcInfo();
132 139 }else {
133 140 StreamInfo streamInfo = inviteInfo.getStreamInfo();
134 141 String streamId = streamInfo.getStream();
135 142 if (streamId == null) {
136 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 155 return inviteInfo.getSsrcInfo();
142 156 }
143 157 String mediaServerId = streamInfo.getMediaServerId();
... ... @@ -146,41 +160,64 @@ public class PlayServiceImpl implements IPlayService {
146 160 Boolean ready = zlmrtpServerFactory.isStreamReady(mediaInfo, "rtp", streamId);
147 161 if (ready != null && ready) {
148 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 174 return inviteInfo.getSsrcInfo();
154 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 189 String streamId = null;
164 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 197 SSRCInfo ssrcInfo = mediaServerService.openRTPServer(mediaServerItem, streamId, null, device.isSsrcCheck(), false, 0, false, device.getStreamModeForParam());
168 198 if (ssrcInfo == null) {
169 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 211 return null;
175 212 }
176 213 // TODO 记录点播的状态
177   - play(mediaServerItem, ssrcInfo, device, channelId, callback);
  214 + play(mediaServerItem, ssrcInfo, device, channelId,isSubStream, callback);
178 215 return ssrcInfo;
179 216 }
180 217  
181 218  
182 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 221 ErrorCallback<Object> callback) {
185 222  
186 223 if (mediaServerItem == null || ssrcInfo == null) {
... ... @@ -189,21 +226,11 @@ public class PlayServiceImpl implements IPlayService {
189 226 null);
190 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 234 //端口获取失败的ssrcInfo 没有必要发送点播指令
208 235 if (ssrcInfo.getPort() <= 0) {
209 236 logger.info("[点播端口分配异常],deviceId={},channelId={},ssrcInfo={}", device.getDeviceId(), channelId, ssrcInfo);
... ... @@ -212,23 +239,50 @@ public class PlayServiceImpl implements IPlayService {
212 239 streamSession.remove(device.getDeviceId(), channelId, ssrcInfo.getStream());
213 240  
214 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 249 return;
218 250 }
219 251  
220 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 269 String timeOutTaskKey = UUID.randomUUID().toString();
227 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 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 286 // 点播超时回复BYE 同时释放ssrc以及此次点播的资源
233 287 // InviteInfo inviteInfoForTimeout = inviteStreamService.getInviteInfoByDeviceAndChannel(InviteSessionType.play, device.getDeviceId(), channelId);
234 288 // if (inviteInfoForTimeout == null) {
... ... @@ -240,10 +294,16 @@ public class PlayServiceImpl implements IPlayService {
240 294 // // TODO 发送cancel
241 295 // }
242 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 307 try {
248 308 cmder.streamByeCmd(device, channelId, ssrcInfo.getStream(), null);
249 309 } catch (InvalidArgumentException | ParseException | SipException | SsrcTransactionNotFoundException e) {
... ... @@ -261,25 +321,42 @@ public class PlayServiceImpl implements IPlayService {
261 321 }, userSetting.getPlayTimeout());
262 322  
263 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 325 logger.info("收到订阅消息: " + response.toJSONString());
266 326 dynamicTask.stop(timeOutTaskKey);
267 327 // hook响应
268   - StreamInfo streamInfo = onPublishHandlerForPlay(mediaServerItemInuse, response, device.getDeviceId(), channelId);
  328 + StreamInfo streamInfo = onPublishHandlerForPlay(mediaServerItemInuse, response, device.getDeviceId(), channelId,isSubStream);
269 329 if (streamInfo == null){
270 330 callback.run(InviteErrorCode.ERROR_FOR_STREAM_PARSING_EXCEPTIONS.getCode(),
271 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 341 return;
276 342 }
277 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 360 String streamUrl;
284 361 if (mediaServerItemInuse.getRtspPort() != 0) {
285 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 375 ResponseEvent responseEvent = (ResponseEvent) event.event;
299 376 String contentString = new String(responseEvent.getResponse().getRawContent());
300 377 // 获取ssrc
301   - String ssrcInResponse = SipUtils.getSsrcFromSdp(contentString);
302   -
  378 + int ssrcIndex = contentString.indexOf("y=");
303 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 383 // 查询到ssrc不一致且开启了ssrc校验则需要针对处理
306 384 if (ssrcInfo.getSsrc().equals(ssrcInResponse)) {
307 385 if (device.getStreamMode().equalsIgnoreCase("TCP-ACTIVE")) {
  386 + String substring = contentString.substring(0, contentString.indexOf("y="));
308 387 try {
309   - Gb28181Sdp gb28181Sdp = SipUtils.parseSDP(contentString);
310   - SessionDescription sdp = gb28181Sdp.getBaseSdb();
  388 + SessionDescription sdp = SdpFactory.getInstance().createSessionDescription(substring);
311 389 int port = -1;
312 390 Vector mediaDescriptions = sdp.getMediaDescriptions(true);
313 391 for (Object description : mediaDescriptions) {
... ... @@ -334,21 +412,24 @@ public class PlayServiceImpl implements IPlayService {
334 412  
335 413 callback.run(InviteErrorCode.ERROR_FOR_SDP_PARSING_EXCEPTIONS.getCode(),
336 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 426 return;
343 427 }
344 428 logger.info("[点播消息] 收到invite 200, 发现下级自定义了ssrc: {}", ssrcInResponse);
345   -
346 429 if (!mediaServerItem.isRtpEnable() || device.isSsrcCheck()) {
347 430 logger.info("[点播消息] SSRC修正 {}->{}", ssrcInfo.getSsrc(), ssrcInResponse);
348   -
349 431 // 释放ssrc
350 432 mediaServerService.releaseSsrc(mediaServerItem.getId(), ssrcInfo.getSsrc());
351   -
352 433 // 单端口模式streamId也有变化,重新设置监听即可
353 434 if (!mediaServerItem.isRtpEnable()) {
354 435 // 添加订阅
... ... @@ -361,21 +442,34 @@ public class PlayServiceImpl implements IPlayService {
361 442 logger.info("[ZLM HOOK] ssrc修正后收到订阅消息: " + response.toJSONString());
362 443 dynamicTask.stop(timeOutTaskKey);
363 444 // hook响应
364   - StreamInfo streamInfo = onPublishHandlerForPlay(mediaServerItemInUse, response, device.getDeviceId(), channelId);
  445 + StreamInfo streamInfo = onPublishHandlerForPlay(mediaServerItemInUse, response, device.getDeviceId(), channelId,isSubStream);
365 446 if (streamInfo == null){
366 447 callback.run(InviteErrorCode.ERROR_FOR_STREAM_PARSING_EXCEPTIONS.getCode(),
367 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 458 return;
372 459 }
373 460 callback.run(InviteErrorCode.SUCCESS.getCode(),
374 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 474 return;
381 475 }
... ... @@ -391,14 +485,22 @@ public class PlayServiceImpl implements IPlayService {
391 485 }
392 486  
393 487 dynamicTask.stop(timeOutTaskKey);
  488 + // 释放ssrc
  489 + mediaServerService.releaseSsrc(mediaServerItem.getId(), ssrcInfo.getSsrc());
394 490  
395 491 streamSession.remove(device.getDeviceId(), channelId, ssrcInfo.getStream());
396 492  
397 493 callback.run(InviteErrorCode.ERROR_FOR_RESET_SSRC.getCode(),
398 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 505 }else {
404 506 ssrcInfo.setSsrc(ssrcInResponse);
... ... @@ -409,7 +511,11 @@ public class PlayServiceImpl implements IPlayService {
409 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 519 }, (event) -> {
414 520 dynamicTask.stop(timeOutTaskKey);
415 521 mediaServerService.closeRTPServer(mediaServerItem, ssrcInfo.getStream());
... ... @@ -420,11 +526,19 @@ public class PlayServiceImpl implements IPlayService {
420 526  
421 527 callback.run(InviteErrorCode.ERROR_FOR_SIGNALLING_ERROR.getCode(),
422 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 543 } catch (InvalidArgumentException | SipException | ParseException e) {
430 544  
... ... @@ -438,27 +552,51 @@ public class PlayServiceImpl implements IPlayService {
438 552  
439 553 callback.run(InviteErrorCode.ERROR_FOR_SIP_SENDING_FAILED.getCode(),
440 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 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 591 if (inviteInfo != null) {
459 592 inviteInfo.setStatus(InviteSessionStatus.ok);
460 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 602 return streamInfo;
... ... @@ -607,16 +745,17 @@ public class PlayServiceImpl implements IPlayService {
607 745 ResponseEvent responseEvent = (ResponseEvent) eventResult.event;
608 746 String contentString = new String(responseEvent.getResponse().getRawContent());
609 747 // 获取ssrc
610   - String ssrcInResponse = SipUtils.getSsrcFromSdp(contentString);
611   -
  748 + int ssrcIndex = contentString.indexOf("y=");
612 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 753 // 查询到ssrc不一致且开启了ssrc校验则需要针对处理
615 754 if (ssrcInfo.getSsrc().equals(ssrcInResponse)) {
616 755 if (device.getStreamMode().equalsIgnoreCase("TCP-ACTIVE")) {
  756 + String substring = contentString.substring(0, contentString.indexOf("y="));
617 757 try {
618   - Gb28181Sdp gb28181Sdp = SipUtils.parseSDP(contentString);
619   - SessionDescription sdp = gb28181Sdp.getBaseSdb();
  758 + SessionDescription sdp = SdpFactory.getInstance().createSessionDescription(substring);
620 759 int port = -1;
621 760 Vector mediaDescriptions = sdp.getMediaDescriptions(true);
622 761 for (Object description : mediaDescriptions) {
... ... @@ -684,6 +823,8 @@ public class PlayServiceImpl implements IPlayService {
684 823 }
685 824  
686 825 dynamicTask.stop(playBackTimeOutTaskKey);
  826 + // 释放ssrc
  827 + mediaServerService.releaseSsrc(mediaServerItem.getId(), ssrcInfo.getSsrc());
687 828  
688 829 streamSession.remove(device.getDeviceId(), channelId, ssrcInfo.getStream());
689 830  
... ... @@ -799,15 +940,17 @@ public class PlayServiceImpl implements IPlayService {
799 940 ResponseEvent responseEvent = (ResponseEvent) eventResult.event;
800 941 String contentString = new String(responseEvent.getResponse().getRawContent());
801 942 // 获取ssrc
802   - String ssrcInResponse = SipUtils.getSsrcFromSdp(contentString);
  943 + int ssrcIndex = contentString.indexOf("y=");
803 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 948 // 查询到ssrc不一致且开启了ssrc校验则需要针对处理
806 949 if (ssrcInfo.getSsrc().equals(ssrcInResponse)) {
807 950 if (device.getStreamMode().equalsIgnoreCase("TCP-ACTIVE")) {
  951 + String substring = contentString.substring(0, contentString.indexOf("y="));
808 952 try {
809   - Gb28181Sdp gb28181Sdp = SipUtils.parseSDP(contentString);
810   - SessionDescription sdp = gb28181Sdp.getBaseSdb();
  953 + SessionDescription sdp = SdpFactory.getInstance().createSessionDescription(substring);
811 954 int port = -1;
812 955 Vector mediaDescriptions = sdp.getMediaDescriptions(true);
813 956 for (Object description : mediaDescriptions) {
... ... @@ -872,6 +1015,8 @@ public class PlayServiceImpl implements IPlayService {
872 1015 }
873 1016  
874 1017 dynamicTask.stop(downLoadTimeOutTaskKey);
  1018 + // 释放ssrc
  1019 + mediaServerService.releaseSsrc(mediaServerItem.getId(), ssrcInfo.getSsrc());
875 1020  
876 1021 streamSession.remove(device.getDeviceId(), channelId, ssrcInfo.getStream());
877 1022  
... ... @@ -972,6 +1117,7 @@ public class PlayServiceImpl implements IPlayService {
972 1117 return streamInfo;
973 1118 }
974 1119  
  1120 +
975 1121 @Override
976 1122 public void zlmServerOffline(String mediaServerId) {
977 1123 // 处理正在向上推流的上级平台
... ... @@ -1108,14 +1254,18 @@ public class PlayServiceImpl implements IPlayService {
1108 1254 }
1109 1255  
1110 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 1258 Device device = deviceService.getDevice(deviceId);
1113 1259 if (device == null) {
1114 1260 errorCallback.run(InviteErrorCode.ERROR_FOR_PARAMETER_ERROR.getCode(), InviteErrorCode.ERROR_FOR_PARAMETER_ERROR.getMsg(), null);
1115 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 1269 if (inviteInfo != null) {
1120 1270 if (inviteInfo.getStreamInfo() != null) {
1121 1271 // 已存在线直接截图
... ... @@ -1130,10 +1280,9 @@ public class PlayServiceImpl implements IPlayService {
1130 1280 // 请求截图
1131 1281 logger.info("[请求截图]: " + fileName);
1132 1282 zlmresTfulUtils.getSnap(mediaServerItemInuse, streamUrl, 15, 1, path, fileName);
1133   - String filePath = path + File.separator + fileName;
1134 1283 File snapFile = new File(path + File.separator + fileName);
1135 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 1286 }else {
1138 1287 errorCallback.run(InviteErrorCode.FAIL.getCode(), InviteErrorCode.FAIL.getMsg(), null);
1139 1288 }
... ... @@ -1142,11 +1291,11 @@ public class PlayServiceImpl implements IPlayService {
1142 1291 }
1143 1292  
1144 1293 MediaServerItem newMediaServerItem = getNewMediaServerItem(device);
1145   - play(newMediaServerItem, deviceId, channelId, (code, msg, data)->{
  1294 + play(newMediaServerItem, deviceId, channelId,isSubStream, (code, msg, data)->{
1146 1295 if (code == InviteErrorCode.SUCCESS.getCode()) {
1147 1296 InviteInfo inviteInfoForPlay = inviteStreamService.getInviteInfoByDeviceAndChannel(InviteSessionType.PLAY, deviceId, channelId);
1148 1297 if (inviteInfoForPlay != null && inviteInfoForPlay.getStreamInfo() != null) {
1149   - getSnap(deviceId, channelId, fileName, errorCallback);
  1298 + getSnap(deviceId, channelId, fileName,isSubStream, errorCallback);
1150 1299 }else {
1151 1300 errorCallback.run(InviteErrorCode.FAIL.getCode(), InviteErrorCode.FAIL.getMsg(), null);
1152 1301 }
... ... @@ -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 451 @Select("select count(1) from wvp_device_channel")
452 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 458 @Select(value = {" <script>" +
455 459 "select * " +
456 460 "from device_channel " +
... ... @@ -460,4 +464,5 @@ public interface DeviceChannelMapper {
460 464 " <if test='onlyCatalog == true '> and parental = 1 </if>" +
461 465 " </script>"})
462 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 42 "geo_coord_sys," +
43 43 "on_line," +
44 44 "media_server_id," +
  45 + "switch_primary_sub_stream," +
45 46 "(SELECT count(0) FROM wvp_device_channel WHERE device_id=wvp_device.device_id) as channel_count "+
46 47 " FROM wvp_device WHERE device_id = #{deviceId}")
47 48 Device getDeviceByDeviceId(String deviceId);
... ... @@ -157,6 +158,7 @@ public interface DeviceMapper {
157 158 "geo_coord_sys,"+
158 159 "on_line,"+
159 160 "media_server_id,"+
  161 + "switch_primary_sub_stream switchPrimarySubStream,"+
160 162 "(SELECT count(0) FROM wvp_device_channel WHERE device_id=de.device_id) as channel_count " +
161 163 "FROM wvp_device de" +
162 164 "<if test=\"onLine != null\"> where on_line=${onLine}</if>"+
... ... @@ -246,6 +248,7 @@ public interface DeviceMapper {
246 248 "<if test=\"ssrcCheck != null\">, ssrc_check=#{ssrcCheck}</if>" +
247 249 "<if test=\"asMessageChannel != null\">, as_message_channel=#{asMessageChannel}</if>" +
248 250 "<if test=\"geoCoordSys != null\">, geo_coord_sys=#{geoCoordSys}</if>" +
  251 + "<if test=\"switchPrimarySubStream != null\">, switch_primary_sub_stream=#{switchPrimarySubStream}</if>" +
249 252 "<if test=\"mediaServerId != null\">, media_server_id=#{mediaServerId}</if>" +
250 253 "WHERE device_id=#{deviceId}"+
251 254 " </script>"})
... ... @@ -263,7 +266,8 @@ public interface DeviceMapper {
263 266 "as_message_channel,"+
264 267 "geo_coord_sys,"+
265 268 "on_line,"+
266   - "media_server_id"+
  269 + "media_server_id,"+
  270 + "switch_primary_sub_stream"+
267 271 ") VALUES (" +
268 272 "#{deviceId}," +
269 273 "#{name}," +
... ... @@ -276,7 +280,8 @@ public interface DeviceMapper {
276 280 "#{asMessageChannel}," +
277 281 "#{geoCoordSys}," +
278 282 "#{onLine}," +
279   - "#{mediaServerId}" +
  283 + "#{mediaServerId}," +
  284 + "#{switchPrimarySubStream}" +
280 285 ")")
281 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 26 import com.genersoft.iot.vmp.storager.IVideoManagerStorage;
27 27 import com.genersoft.iot.vmp.utils.DateUtil;
28 28 import com.genersoft.iot.vmp.vmanager.bean.ErrorCode;
29   -import com.genersoft.iot.vmp.vmanager.bean.SnapPath;
30 29 import com.genersoft.iot.vmp.vmanager.bean.StreamContent;
31 30 import com.genersoft.iot.vmp.vmanager.bean.WVPResult;
32 31 import io.swagger.v3.oas.annotations.Operation;
... ... @@ -41,7 +40,6 @@ import org.springframework.web.context.request.async.DeferredResult;
41 40 import javax.servlet.http.HttpServletRequest;
42 41 import javax.sip.InvalidArgumentException;
43 42 import javax.sip.SipException;
44   -import java.io.File;
45 43 import java.text.ParseException;
46 44 import java.util.List;
47 45 import java.util.UUID;
... ... @@ -90,16 +88,17 @@ public class PlayController {
90 88 @Operation(summary = "开始点播")
91 89 @Parameter(name = "deviceId", description = "设备国标编号", required = true)
92 90 @Parameter(name = "channelId", description = "通道国标编号", required = true)
  91 + @Parameter(name = "isSubStream", description = "是否子码流(true-子码流,false-主码流),默认为false", required = true)
93 92 @GetMapping("/start/{deviceId}/{channelId}")
94 93 public DeferredResult<WVPResult<StreamContent>> play(HttpServletRequest request, @PathVariable String deviceId,
95   - @PathVariable String channelId) {
  94 + @PathVariable String channelId,boolean isSubStream) {
96 95  
97 96 // 获取可用的zlm
98 97 Device device = storager.queryVideoDevice(deviceId);
99 98 MediaServerItem newMediaServerItem = playService.getNewMediaServerItem(device);
100 99  
101 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 102 requestMessage.setKey(key);
104 103 String uuid = UUID.randomUUID().toString();
105 104 requestMessage.setId(uuid);
... ... @@ -118,7 +117,7 @@ public class PlayController {
118 117 // 录像查询以channelId作为deviceId查询
119 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 121 WVPResult<StreamContent> wvpResult = new WVPResult<>();
123 122 if (code == InviteErrorCode.SUCCESS.getCode()) {
124 123 wvpResult.setCode(ErrorCode.SUCCESS.getCode());
... ... @@ -144,8 +143,9 @@ public class PlayController {
144 143 @Operation(summary = "停止点播")
145 144 @Parameter(name = "deviceId", description = "设备国标编号", required = true)
146 145 @Parameter(name = "channelId", description = "通道国标编号", required = true)
  146 + @Parameter(name = "isSubStream", description = "是否子码流(true-子码流,false-主码流),默认为false", required = true)
147 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 150 logger.debug(String.format("设备预览/回放停止API调用,streamId:%s_%s", deviceId, channelId ));
151 151  
... ... @@ -158,7 +158,12 @@ public class PlayController {
158 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 167 if (inviteInfo == null) {
163 168 throw new ControllerException(ErrorCode.ERROR100.getCode(), "点播未找到");
164 169 }
... ... @@ -171,12 +176,17 @@ public class PlayController {
171 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 186 JSONObject json = new JSONObject();
178 187 json.put("deviceId", deviceId);
179 188 json.put("channelId", channelId);
  189 + json.put("isSubStream", isSubStream);
180 190 return json;
181 191 }
182 192  
... ... @@ -343,30 +353,27 @@ public class PlayController {
343 353 @Operation(summary = "获取截图")
344 354 @Parameter(name = "deviceId", description = "设备国标编号", required = true)
345 355 @Parameter(name = "channelId", description = "通道国标编号", required = true)
  356 + @Parameter(name = "isSubStream", description = "是否子码流(true-子码流,false-主码流),默认为false", required = true)
346 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 359 if (logger.isDebugEnabled()) {
349 360 logger.debug("获取截图: {}/{}", deviceId, channelId);
350 361 }
351 362  
  363 + Device device = storager.queryVideoDevice(deviceId);
352 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 366 String uuid = UUID.randomUUID().toString();
355 367 resultHolder.put(key, uuid, result);
356 368  
357 369 RequestMessage message = new RequestMessage();
358 370 message.setKey(key);
359 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 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 377 }else {
371 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 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 126 if (errorCode == InviteErrorCode.SUCCESS.getCode()) {
127 127 InviteInfo inviteInfo = inviteStreamService.getInviteInfoByDeviceAndChannel(InviteSessionType.PLAY, serial, code);
128 128 if (inviteInfo != null && inviteInfo.getStreamInfo() != null) {
... ...
src/main/resources/application-dev.yml
1 1 spring:
  2 + thymeleaf:
  3 + cache: false
2 4 # [可选]上传文件大小限制
3 5 servlet:
4 6 multipart:
... ... @@ -11,18 +13,18 @@ spring:
11 13 # [必须修改] 端口号
12 14 port: 6379
13 15 # [可选] 数据库 DB
14   - database: 6
  16 + database: 7
15 17 # [可选] 访问密码,若你的redis服务器没有设置密码,就不需要用密码去连接
16   - password: face2020
  18 + password:
17 19 # [可选] 超时时间
18 20 timeout: 10000
19 21 # mysql数据源
20 22 datasource:
21 23 type: com.zaxxer.hikari.HikariDataSource
22 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 26 username: root
25   - password: 123456
  27 + password: root
26 28 hikari:
27 29 connection-timeout: 20000 # 是客户端等待连接池连接的最大毫秒数
28 30 initialSize: 10 # 连接池初始化连接数
... ... @@ -30,11 +32,19 @@ spring:
30 32 minimum-idle: 5 # 连接池最小空闲连接数
31 33 idle-timeout: 300000 # 允许连接在连接池中空闲的最长时间(以毫秒为单位)
32 34 max-lifetime: 1200000 # 是池中连接关闭后的最长生命周期(以毫秒为单位)
33   -
34   -
35 35 #[可选] WVP监听的HTTP端口, 网页和接口调用都是这个端口
36 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 49 # 作为28181服务器的配置
40 50 sip:
... ... @@ -42,26 +52,36 @@ sip:
42 52 # 如果要监听多张网卡,可以使用逗号分隔多个IP, 例如: 192.168.1.4,10.0.0.4
43 53 # 如果不明白,就使用0.0.0.0,大部分情况都是可以的
44 54 # 请不要使用127.0.0.1,任何包括localhost在内的域名都是不可以的。
45   - ip: 192.168.41.16
  55 + ip: 192.168.1.18
46 56 # [可选] 28181服务监听的端口
47   - port: 5060
  57 + port: 8116
48 58 # 根据国标6.1.2中规定,domain宜采用ID统一编码的前十位编码。国标附录D中定义前8位为中心编码(由省级、市级、区级、基层编号组成,参照GB/T 2260-2007)
49 59 # 后两位为行业编码,定义参照附录D.3
50 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 70 #zlm 默认服务器配置
59 71 media:
60   - id: FQ3TF8yT83wh5Wvz
  72 + id: 89wulian-one
61 73 # [必须修改] zlm服务器的内网IP
62   - ip: 192.168.41.16
  74 + ip: 192.168.1.18
63 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 85 # [可选] zlm服务器的hook.admin_params=secret
66 86 secret: 035c73f7-bb6b-4889-a715-d9eb2d1925cc
67 87 # 启用多端口模式, 多端口模式使用端口区分每路流,兼容性更好。 单端口使用流的ssrc区分, 点播超时建议使用多端口测试
... ... @@ -69,11 +89,24 @@ media:
69 89 # [可选] 是否启用多端口模式, 开启后会在portRange范围内选择端口用于媒体流传输
70 90 enable: true
71 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 95 # 录像辅助服务, 部署此服务可以实现zlm录像的管理与下载, 0 表示不使用
76 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 110 logging:
79 111 config: classpath:logback-spring-local.xml
  112 +
... ...
src/main/resources/application.yml
... ... @@ -2,4 +2,4 @@ spring:
2 2 application:
3 3 name: wvp
4 4 profiles:
5   - active: local
6 5 \ No newline at end of file
  6 + active: dev
7 7 \ No newline at end of file
... ...
web_src/config/index.js
... ... @@ -12,14 +12,14 @@ module.exports = {
12 12 assetsPublicPath: '/',
13 13 proxyTable: {
14 14 '/debug': {
15   - target: 'http://localhost:18080',
  15 + target: 'http://localhost:18978',
16 16 changeOrigin: true,
17 17 pathRewrite: {
18 18 '^/debug': '/'
19 19 }
20 20 },
21 21 '/static/snap': {
22   - target: 'http://localhost:18080',
  22 + target: 'http://localhost:18978',
23 23 changeOrigin: true,
24 24 // pathRewrite: {
25 25 // '^/static/snap': '/static/snap'
... ...
web_src/src/components/channelList.vue
... ... @@ -26,6 +26,12 @@
26 26 <el-option label="在线" value="true"></el-option>
27 27 <el-option label="离线" value="false"></el-option>
28 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 35 </div>
30 36 <el-button icon="el-icon-refresh-right" circle size="mini" @click="refresh()"></el-button>
31 37 <el-button v-if="showTree" icon="iconfont icon-list" circle size="mini" @click="switchList()"></el-button>
... ... @@ -146,6 +152,7 @@ export default {
146 152 searchSrt: "",
147 153 channelType: "",
148 154 online: "",
  155 + isSubStream: false,
149 156 winHeight: window.innerHeight - 200,
150 157 currentPage: 1,
151 158 count: 15,
... ... @@ -237,7 +244,10 @@ export default {
237 244 let that = this;
238 245 this.$axios({
239 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 251 }).then(function (res) {
242 252 console.log(res)
243 253 that.isLoging = false;
... ... @@ -277,7 +287,10 @@ export default {
277 287 var that = this;
278 288 this.$axios({
279 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 294 }).then(function (res) {
282 295 that.initData();
283 296 }).catch(function (error) {
... ...
web_src/src/components/dialog/deviceEdit.vue
... ... @@ -58,6 +58,12 @@
58 58 <el-form-item v-if="form.subscribeCycleForMobilePosition > 0" label="移动位置报送间隔" prop="subscribeCycleForCatalog" >
59 59 <el-input v-model="form.mobilePositionSubmissionInterval" clearable ></el-input>
60 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 67 <el-form-item label="其他选项">
62 68 <el-checkbox label="SSRC校验" v-model="form.ssrcCheck" style="float: left"></el-checkbox>
63 69 <el-checkbox label="作为消息通道" v-model="form.asMessageChannel" style="float: left"></el-checkbox>
... ...