Commit fa62ab9a0143433a5d058ab3229a37e4a9a0f696
Committed by
GitHub
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
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> | ... | ... |