From 4604aaea99925415db8d9efe1d7e68d6f59e93c8 Mon Sep 17 00:00:00 2001 From: 648540858 <648540858@qq.com> Date: Sun, 2 Jul 2023 13:53:45 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=E8=AF=AD=E9=9F=B3=E5=AF=B9?= =?UTF-8?q?=E8=AE=B2=E6=94=AF=E6=8C=81=E6=A0=B9=E6=8D=AE=E8=AE=BE=E5=A4=87?= =?UTF-8?q?=E8=AE=BE=E7=BD=AE=E9=87=8A=E6=94=BE=E6=94=B6=E5=88=B0ACK?= =?UTF-8?q?=E5=90=8E=E5=BC=80=E5=A7=8B=E5=8F=91=E6=B5=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../genersoft/iot/vmp/conf/UserSetting.java | 10 -- .../iot/vmp/gb28181/bean/Device.java | 11 +- .../request/impl/AckRequestProcessor.java | 139 +++++++++++------- .../request/impl/InviteRequestProcessor.java | 42 ++---- .../iot/vmp/service/IPlayService.java | 2 +- .../iot/vmp/service/impl/PlayServiceImpl.java | 13 +- .../iot/vmp/storager/dao/DeviceMapper.java | 9 ++ src/main/resources/local.jks | Bin 0 -> 5655 bytes web_src/src/components/dialog/deviceEdit.vue | 1 + 9 files changed, 127 insertions(+), 100 deletions(-) create mode 100644 src/main/resources/local.jks diff --git a/src/main/java/com/genersoft/iot/vmp/conf/UserSetting.java b/src/main/java/com/genersoft/iot/vmp/conf/UserSetting.java index eae96b9a..562c8640 100644 --- a/src/main/java/com/genersoft/iot/vmp/conf/UserSetting.java +++ b/src/main/java/com/genersoft/iot/vmp/conf/UserSetting.java @@ -47,8 +47,6 @@ public class UserSetting { private Boolean syncChannelOnDeviceOnline = Boolean.FALSE; - private Boolean pushStreamAfterAck = Boolean.FALSE; - private Boolean sipLog = Boolean.FALSE; private Boolean sqlLog = Boolean.FALSE; private Boolean sendToPlatformsWhenIdLost = Boolean.FALSE; @@ -234,14 +232,6 @@ public class UserSetting { this.broadcastForPlatform = broadcastForPlatform; } - public Boolean getPushStreamAfterAck() { - return pushStreamAfterAck; - } - - public void setPushStreamAfterAck(Boolean pushStreamAfterAck) { - this.pushStreamAfterAck = pushStreamAfterAck; - } - public Boolean getSipUseSourceIpAsRemoteAddress() { return sipUseSourceIpAsRemoteAddress; } diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/bean/Device.java b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/Device.java index 1318c59c..60f5cf69 100644 --- a/src/main/java/com/genersoft/iot/vmp/gb28181/bean/Device.java +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/Device.java @@ -188,8 +188,8 @@ public class Device { @Schema(description = "设备注册的事务信息") private SipTransactionInfo sipTransactionInfo; - - + @Schema(description = "控制语音对讲流程,释放收到ACK后发流") + private boolean broadcastPushAfterAck; public String getDeviceId() { return deviceId; @@ -465,4 +465,11 @@ public class Device { /*======================设备主子码流逻辑END=========================*/ + public boolean isBroadcastPushAfterAck() { + return broadcastPushAfterAck; + } + + public void setBroadcastPushAfterAck(boolean broadcastPushAfterAck) { + this.broadcastPushAfterAck = broadcastPushAfterAck; + } } diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/AckRequestProcessor.java b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/AckRequestProcessor.java index cc1f0c0c..7ca52efe 100644 --- a/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/AckRequestProcessor.java +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/AckRequestProcessor.java @@ -3,6 +3,7 @@ package com.genersoft.iot.vmp.gb28181.transmit.event.request.impl; import com.alibaba.fastjson2.JSONObject; import com.genersoft.iot.vmp.conf.DynamicTask; import com.genersoft.iot.vmp.conf.UserSetting; +import com.genersoft.iot.vmp.gb28181.bean.Device; import com.genersoft.iot.vmp.gb28181.bean.ParentPlatform; import com.genersoft.iot.vmp.gb28181.bean.SendRtpItem; import com.genersoft.iot.vmp.gb28181.transmit.SIPProcessorObserver; @@ -10,9 +11,8 @@ import com.genersoft.iot.vmp.gb28181.transmit.event.request.ISIPRequestProcessor import com.genersoft.iot.vmp.gb28181.transmit.event.request.SIPRequestProcessorParent; import com.genersoft.iot.vmp.media.zlm.ZLMRTPServerFactory; import com.genersoft.iot.vmp.media.zlm.ZlmHttpHookSubscribe; -import com.genersoft.iot.vmp.media.zlm.dto.HookSubscribeFactory; -import com.genersoft.iot.vmp.media.zlm.dto.HookSubscribeForRtpServerTimeout; import com.genersoft.iot.vmp.media.zlm.dto.MediaServerItem; +import com.genersoft.iot.vmp.service.IDeviceService; import com.genersoft.iot.vmp.service.IMediaServerService; import com.genersoft.iot.vmp.service.IPlayService; import com.genersoft.iot.vmp.service.bean.RequestPushStreamMsg; @@ -62,6 +62,9 @@ public class AckRequestProcessor extends SIPRequestProcessorParent implements In @Autowired private IVideoManagerStorage storager; + @Autowired + private IDeviceService deviceService; + @Autowired private ZLMRTPServerFactory zlmrtpServerFactory; @@ -87,40 +90,23 @@ public class AckRequestProcessor extends SIPRequestProcessorParent implements In @Override public void process(RequestEvent evt) { CallIdHeader callIdHeader = (CallIdHeader)evt.getRequest().getHeader(CallIdHeader.NAME); + String fromUserId = ((SipURI) ((HeaderAddress) evt.getRequest().getHeader(FromHeader.NAME)).getAddress().getURI()).getUser(); + String toUserId = ((SipURI) ((HeaderAddress) evt.getRequest().getHeader(ToHeader.NAME)).getAddress().getURI()).getUser(); + logger.info("[收到ACK]: 来自->{}", fromUserId); + SendRtpItem sendRtpItem = redisCatchStorage.querySendRTPServer(null, null, null, callIdHeader.getCallId()); + if (sendRtpItem == null) { + logger.warn("[收到ACK]:未找到来自{},目标为({})的推流信息",fromUserId, toUserId); + return; + } + logger.info("[收到ACK]:rtp/{}开始级推流, 目标={}:{},SSRC={}, RTCP={}", sendRtpItem.getStream(), + sendRtpItem.getIp(), sendRtpItem.getPort(), sendRtpItem.getSsrc(), sendRtpItem.isRtcp()); + // 取消设置的超时任务 + dynamicTask.stop(callIdHeader.getCallId()); + MediaServerItem mediaInfo = mediaServerService.getOne(sendRtpItem.getMediaServerId()); + ParentPlatform parentPlatform = storager.queryParentPlatByServerGBId(fromUserId); - String platformGbId = ((SipURI) ((HeaderAddress) evt.getRequest().getHeader(FromHeader.NAME)).getAddress().getURI()).getUser(); - logger.info("[收到ACK]: platformGbId->{}", platformGbId); - if (userSetting.getPushStreamAfterAck()) { - ParentPlatform parentPlatform = storager.queryParentPlatByServerGBId(platformGbId); - // 取消设置的超时任务 - dynamicTask.stop(callIdHeader.getCallId()); - String channelId = ((SipURI) ((HeaderAddress) evt.getRequest().getHeader(ToHeader.NAME)).getAddress().getURI()).getUser(); - SendRtpItem sendRtpItem = redisCatchStorage.querySendRTPServer(null, null, null, callIdHeader.getCallId()); - if (sendRtpItem == null) { - logger.warn("[收到ACK]:未找到通道({})的推流信息", channelId); - return; - } - String isUdp = sendRtpItem.isTcp() ? "0" : "1"; - MediaServerItem mediaInfo = mediaServerService.getOne(sendRtpItem.getMediaServerId()); - logger.info("收到ACK,rtp/{}开始级推流, 目标={}:{},SSRC={}, RTCP={}", sendRtpItem.getStream(), - sendRtpItem.getIp(), sendRtpItem.getPort(), sendRtpItem.getSsrc(), sendRtpItem.isRtcp()); - Map param = new HashMap<>(12); - param.put("vhost","__defaultVhost__"); - param.put("app",sendRtpItem.getApp()); - param.put("stream",sendRtpItem.getStream()); - param.put("ssrc", sendRtpItem.getSsrc()); - param.put("dst_url",sendRtpItem.getIp()); - param.put("dst_port", sendRtpItem.getPort()); - param.put("src_port", sendRtpItem.getLocalPort()); - param.put("pt", sendRtpItem.getPt()); - param.put("use_ps", sendRtpItem.isUsePs() ? "1" : "0"); - param.put("only_audio", sendRtpItem.isOnlyAudio() ? "1" : "0"); - param.put("is_udp", isUdp); - if (!sendRtpItem.isTcp()) { - // udp模式下开启rtcp保活 - param.put("udp_rtcp_timeout", sendRtpItem.isRtcp()? "1":"0"); - } - + if (parentPlatform != null) { + Map param = getSendRtpParam(sendRtpItem); if (mediaInfo == null) { RequestPushStreamMsg requestPushStreamMsg = RequestPushStreamMsg.getInstance( sendRtpItem.getMediaServerId(), sendRtpItem.getApp(), sendRtpItem.getStream(), @@ -130,30 +116,75 @@ public class AckRequestProcessor extends SIPRequestProcessorParent implements In playService.startSendRtpStreamHand(sendRtpItem, parentPlatform, json, param, callIdHeader); }); } else { - // 如果是非严格模式,需要关闭端口占用 - JSONObject startSendRtpStreamResult = null; - if (sendRtpItem.getLocalPort() != 0) { - if (sendRtpItem.isTcpActive()) { - startSendRtpStreamResult = zlmrtpServerFactory.startSendRtpPassive(mediaInfo, param); - }else { - param.put("dst_url", sendRtpItem.getIp()); - param.put("dst_port", sendRtpItem.getPort()); - startSendRtpStreamResult = zlmrtpServerFactory.startSendRtpStream(mediaInfo, param); - } - }else { - if (sendRtpItem.isTcpActive()) { - startSendRtpStreamResult = zlmrtpServerFactory.startSendRtpPassive(mediaInfo, param); - }else { - param.put("dst_url", sendRtpItem.getIp()); - param.put("dst_port", sendRtpItem.getPort()); - startSendRtpStreamResult = zlmrtpServerFactory.startSendRtpStream(mediaInfo, param); - } - } + JSONObject startSendRtpStreamResult = sendRtp(sendRtpItem, mediaInfo, param); if (startSendRtpStreamResult != null) { playService.startSendRtpStreamHand(sendRtpItem, parentPlatform, startSendRtpStreamResult, param, callIdHeader); } } + }else { + Device device = deviceService.getDevice(fromUserId); + if (device == null) { + logger.warn("[收到ACK]:来自{},目标为({})的推流信息为找到流体服务[{}]信息",fromUserId, toUserId, sendRtpItem.getMediaServerId()); + return; + } + // 设置为收到ACK后发送语音的设备已经在发送200OK开始发流了 + if (!device.isBroadcastPushAfterAck()) { + return; + } + if (mediaInfo == null) { + logger.warn("[收到ACK]:来自{},目标为({})的推流信息为找到流体服务[{}]信息",fromUserId, toUserId, sendRtpItem.getMediaServerId()); + return; + } + Map param = getSendRtpParam(sendRtpItem); + JSONObject startSendRtpStreamResult = sendRtp(sendRtpItem, mediaInfo, param); + if (startSendRtpStreamResult != null) { + playService.startSendRtpStreamHand(sendRtpItem, device, startSendRtpStreamResult, param, callIdHeader); + } } } + private Map getSendRtpParam(SendRtpItem sendRtpItem) { + String isUdp = sendRtpItem.isTcp() ? "0" : "1"; + Map param = new HashMap<>(12); + param.put("vhost","__defaultVhost__"); + param.put("app",sendRtpItem.getApp()); + param.put("stream",sendRtpItem.getStream()); + param.put("ssrc", sendRtpItem.getSsrc()); + param.put("dst_url",sendRtpItem.getIp()); + param.put("dst_port", sendRtpItem.getPort()); + param.put("src_port", sendRtpItem.getLocalPort()); + param.put("pt", sendRtpItem.getPt()); + param.put("use_ps", sendRtpItem.isUsePs() ? "1" : "0"); + param.put("only_audio", sendRtpItem.isOnlyAudio() ? "1" : "0"); + param.put("is_udp", isUdp); + if (!sendRtpItem.isTcp()) { + // udp模式下开启rtcp保活 + param.put("udp_rtcp_timeout", sendRtpItem.isRtcp()? "1":"0"); + } + return param; + } + + private JSONObject sendRtp(SendRtpItem sendRtpItem, MediaServerItem mediaInfo, Map param){ + JSONObject startSendRtpStreamResult = null; + if (sendRtpItem.getLocalPort() != 0) { + if (sendRtpItem.isTcpActive()) { + startSendRtpStreamResult = zlmrtpServerFactory.startSendRtpPassive(mediaInfo, param); + }else { + param.put("dst_url", sendRtpItem.getIp()); + param.put("dst_port", sendRtpItem.getPort()); + startSendRtpStreamResult = zlmrtpServerFactory.startSendRtpStream(mediaInfo, param); + } + }else { + if (sendRtpItem.isTcpActive()) { + startSendRtpStreamResult = zlmrtpServerFactory.startSendRtpPassive(mediaInfo, param); + }else { + param.put("dst_url", sendRtpItem.getIp()); + param.put("dst_port", sendRtpItem.getPort()); + startSendRtpStreamResult = zlmrtpServerFactory.startSendRtpStream(mediaInfo, param); + } + } + return startSendRtpStreamResult; + + } + } diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/InviteRequestProcessor.java b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/InviteRequestProcessor.java index 423a3b45..7594b482 100644 --- a/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/InviteRequestProcessor.java +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/InviteRequestProcessor.java @@ -427,23 +427,18 @@ public class InviteRequestProcessor extends SIPRequestProcessorParent implements try { // 超时未收到Ack应该回复bye,当前等待时间为10秒 - if (userSetting.getPushStreamAfterAck()) { - dynamicTask.startDelay(callIdHeader.getCallId(), () -> { - logger.info("Ack 等待超时"); - mediaServerService.releaseSsrc(mediaServerItemInUSe.getId(), sendRtpItem.getSsrc()); - // 回复bye - try { - cmderFroPlatform.streamByeCmd(platform, callIdHeader.getCallId()); - } catch (SipException | InvalidArgumentException | ParseException e) { - logger.error("[命令发送失败] 国标级联 发送BYE: {}", e.getMessage()); - } - }, 60 * 1000); - } + dynamicTask.startDelay(callIdHeader.getCallId(), () -> { + logger.info("Ack 等待超时"); + mediaServerService.releaseSsrc(mediaServerItemInUSe.getId(), sendRtpItem.getSsrc()); + // 回复bye + try { + cmderFroPlatform.streamByeCmd(platform, callIdHeader.getCallId()); + } catch (SipException | InvalidArgumentException | ParseException e) { + logger.error("[命令发送失败] 国标级联 发送BYE: {}", e.getMessage()); + } + }, 60 * 1000); - SIPResponse sipResponse = responseSdpAck(request, content.toString(), platform); - if (!userSetting.getPushStreamAfterAck()) { - playService.startPushStream(sendRtpItem, sipResponse, platform, request.getCallIdHeader()); - } + responseSdpAck(request, content.toString(), platform); } catch (SipException | InvalidArgumentException | ParseException e) { logger.error("[命令发送失败] 国标级联 回复SdpAck", e); } @@ -650,7 +645,6 @@ public class InviteRequestProcessor extends SIPRequestProcessorParent implements if (response != null) { sendRtpItem.setToTag(response.getToTag()); } - redisCatchStorage.updateSendRTPSever(sendRtpItem); } else { @@ -888,16 +882,8 @@ public class InviteRequestProcessor extends SIPRequestProcessorParent implements content.append("f=\r\n"); try { - SIPResponse sipResponse = responseSdpAck(request, content.toString(), platform); - if (!userSetting.getPushStreamAfterAck()) { - playService.startPushStream(sendRtpItem, sipResponse, platform, request.getCallIdHeader()); - } - return sipResponse; - } catch (SipException e) { - logger.error("未处理的异常 ", e); - } catch (InvalidArgumentException e) { - logger.error("未处理的异常 ", e); - } catch (ParseException e) { + return responseSdpAck(request, content.toString(), platform); + } catch (SipException | InvalidArgumentException | ParseException e) { logger.error("未处理的异常 ", e); } return null; @@ -1132,7 +1118,7 @@ public class InviteRequestProcessor extends SIPRequestProcessorParent implements audioBroadcastManager.update(audioBroadcastCatch); // 开启发流,大华在收到200OK后就会开始建立连接 - if (!userSetting.getPushStreamAfterAck()) { + if (!device.isBroadcastPushAfterAck()) { playService.startPushStream(sendRtpItem, sipResponse, parentPlatform, request.getCallIdHeader()); } diff --git a/src/main/java/com/genersoft/iot/vmp/service/IPlayService.java b/src/main/java/com/genersoft/iot/vmp/service/IPlayService.java index 44bf11b1..7725e1bc 100644 --- a/src/main/java/com/genersoft/iot/vmp/service/IPlayService.java +++ b/src/main/java/com/genersoft/iot/vmp/service/IPlayService.java @@ -64,7 +64,7 @@ public interface IPlayService { void startPushStream(SendRtpItem sendRtpItem, SIPResponse sipResponse, ParentPlatform platform, CallIdHeader callIdHeader); - void startSendRtpStreamHand(SendRtpItem sendRtpItem, ParentPlatform parentPlatform, + void startSendRtpStreamHand(SendRtpItem sendRtpItem, Object correlationInfo, JSONObject jsonObject, Map param, CallIdHeader callIdHeader); void talkCmd(Device device, String channelId, MediaServerItem mediaServerItem, String stream, AudioBroadcastEvent event); diff --git a/src/main/java/com/genersoft/iot/vmp/service/impl/PlayServiceImpl.java b/src/main/java/com/genersoft/iot/vmp/service/impl/PlayServiceImpl.java index 71e9b65b..ea998e9d 100644 --- a/src/main/java/com/genersoft/iot/vmp/service/impl/PlayServiceImpl.java +++ b/src/main/java/com/genersoft/iot/vmp/service/impl/PlayServiceImpl.java @@ -1481,7 +1481,7 @@ public class PlayServiceImpl implements IPlayService { } @Override - public void startSendRtpStreamHand(SendRtpItem sendRtpItem, ParentPlatform parentPlatform, + public void startSendRtpStreamHand(SendRtpItem sendRtpItem, Object correlationInfo, JSONObject jsonObject, Map param, CallIdHeader callIdHeader) { if (jsonObject == null) { logger.error("RTP推流失败: 请检查ZLM服务"); @@ -1504,10 +1504,13 @@ public class PlayServiceImpl implements IPlayService { } } else { // 向上级平台 - try { - commanderForPlatform.streamByeCmd(parentPlatform, callIdHeader.getCallId()); - } catch (SipException | InvalidArgumentException | ParseException e) { - logger.error("[命令发送失败] 国标级联 发送BYE: {}", e.getMessage()); + if (correlationInfo instanceof ParentPlatform) { + try { + ParentPlatform parentPlatform = (ParentPlatform)correlationInfo; + commanderForPlatform.streamByeCmd(parentPlatform, callIdHeader.getCallId()); + } catch (SipException | InvalidArgumentException | ParseException e) { + logger.error("[命令发送失败] 国标级联 发送BYE: {}", e.getMessage()); + } } } } diff --git a/src/main/java/com/genersoft/iot/vmp/storager/dao/DeviceMapper.java b/src/main/java/com/genersoft/iot/vmp/storager/dao/DeviceMapper.java index 96773fe9..e2497a79 100644 --- a/src/main/java/com/genersoft/iot/vmp/storager/dao/DeviceMapper.java +++ b/src/main/java/com/genersoft/iot/vmp/storager/dao/DeviceMapper.java @@ -43,6 +43,7 @@ public interface DeviceMapper { "on_line," + "media_server_id," + "switch_primary_sub_stream," + + "broadcast_push_after_ack," + "(SELECT count(0) FROM wvp_device_channel WHERE device_id=wvp_device.device_id) as channel_count "+ " FROM wvp_device WHERE device_id = #{deviceId}") Device getDeviceByDeviceId(String deviceId); @@ -73,6 +74,7 @@ public interface DeviceMapper { "subscribe_cycle_for_alarm,"+ "ssrc_check,"+ "as_message_channel,"+ + "broadcast_push_after_ack,"+ "geo_coord_sys,"+ "on_line"+ ") VALUES (" + @@ -101,6 +103,7 @@ public interface DeviceMapper { "#{subscribeCycleForAlarm}," + "#{ssrcCheck}," + "#{asMessageChannel}," + + "#{broadcastPushAfterAck}," + "#{geoCoordSys}," + "#{onLine}" + ")") @@ -155,6 +158,7 @@ public interface DeviceMapper { "subscribe_cycle_for_alarm,"+ "ssrc_check,"+ "as_message_channel,"+ + "broadcast_push_after_ack,"+ "geo_coord_sys,"+ "on_line,"+ "media_server_id,"+ @@ -196,6 +200,7 @@ public interface DeviceMapper { "subscribe_cycle_for_alarm,"+ "ssrc_check,"+ "as_message_channel,"+ + "broadcast_push_after_ack,"+ "geo_coord_sys,"+ "on_line"+ " FROM wvp_device WHERE on_line = true") @@ -226,6 +231,7 @@ public interface DeviceMapper { "subscribe_cycle_for_alarm,"+ "ssrc_check,"+ "as_message_channel,"+ + "broadcast_push_after_ack,"+ "geo_coord_sys,"+ "on_line"+ " FROM wvp_device WHERE ip = #{host} AND port=#{port}") @@ -247,6 +253,7 @@ public interface DeviceMapper { ", subscribe_cycle_for_alarm=#{subscribeCycleForAlarm}" + ", ssrc_check=#{ssrcCheck}" + ", as_message_channel=#{asMessageChannel}" + + ", broadcast_push_after_ack=#{broadcastPushAfterAck}" + ", geo_coord_sys=#{geoCoordSys}" + ", switch_primary_sub_stream=#{switchPrimarySubStream}" + ", media_server_id=#{mediaServerId}" + @@ -264,6 +271,7 @@ public interface DeviceMapper { "charset,"+ "ssrc_check,"+ "as_message_channel,"+ + "broadcastPushAfterAck,"+ "geo_coord_sys,"+ "on_line,"+ "media_server_id,"+ @@ -278,6 +286,7 @@ public interface DeviceMapper { "#{charset}," + "#{ssrcCheck}," + "#{asMessageChannel}," + + "#{broadcastPushAfterAck}," + "#{geoCoordSys}," + "#{onLine}," + "#{mediaServerId}," + diff --git a/src/main/resources/local.jks b/src/main/resources/local.jks new file mode 100644 index 0000000000000000000000000000000000000000..529be6b23903f176694f748058b5d2580f67d95d GIT binary patch literal 5655 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 c|D~suX($z#8NFXw+9WN& +