Commit f8fe76add24fc2a26449c6b4007a303decb46d99
Committed by
GitHub
Merge pull request #74 from lawrencehj/wvp-28181-2.0
实现语音广播信令等(web语音推流开发中)
Showing
16 changed files
with
370 additions
and
139 deletions
src/main/java/com/genersoft/iot/vmp/conf/SipPlatformRunner.java
| ... | ... | @@ -33,6 +33,9 @@ public class SipPlatformRunner implements CommandLineRunner { |
| 33 | 33 | // 设置所有平台离线 |
| 34 | 34 | storager.outlineForAllParentPlatform(); |
| 35 | 35 | |
| 36 | + // 清理所有平台注册缓存 | |
| 37 | + redisCatchStorage.cleanPlatformRegisterInfos(); | |
| 38 | + | |
| 36 | 39 | List<ParentPlatform> parentPlatforms = storager.queryEnableParentPlatformList(true); |
| 37 | 40 | |
| 38 | 41 | for (ParentPlatform parentPlatform : parentPlatforms) { | ... | ... |
src/main/java/com/genersoft/iot/vmp/gb28181/SipLayer.java
| ... | ... | @@ -16,7 +16,7 @@ import org.slf4j.Logger; |
| 16 | 16 | import org.slf4j.LoggerFactory; |
| 17 | 17 | import org.springframework.beans.factory.annotation.Autowired; |
| 18 | 18 | import org.springframework.context.annotation.Bean; |
| 19 | -import org.springframework.context.annotation.ComponentScan; | |
| 19 | +// import org.springframework.context.annotation.ComponentScan; | |
| 20 | 20 | import org.springframework.context.annotation.DependsOn; |
| 21 | 21 | import org.springframework.stereotype.Component; |
| 22 | 22 | ... | ... |
src/main/java/com/genersoft/iot/vmp/gb28181/transmit/callback/DeferredResultHolder.java
| ... | ... | @@ -41,6 +41,8 @@ public class DeferredResultHolder { |
| 41 | 41 | |
| 42 | 42 | public static final String CALLBACK_CMD_ALARM = "CALLBACK_ALARM"; |
| 43 | 43 | |
| 44 | + public static final String CALLBACK_CMD_BROADCAST = "CALLBACK_BROADCAST"; | |
| 45 | + | |
| 44 | 46 | private Map<String, DeferredResult> map = new ConcurrentHashMap<String, DeferredResult>(); |
| 45 | 47 | |
| 46 | 48 | public void put(String key, DeferredResult result) { | ... | ... |
src/main/java/com/genersoft/iot/vmp/gb28181/transmit/cmd/ISIPCommander.java
| ... | ... | @@ -120,6 +120,14 @@ public interface ISIPCommander { |
| 120 | 120 | boolean audioBroadcastCmd(Device device,String channelId); |
| 121 | 121 | |
| 122 | 122 | /** |
| 123 | + * 语音广播 | |
| 124 | + * | |
| 125 | + * @param device 视频设备 | |
| 126 | + */ | |
| 127 | + void audioBroadcastCmd(Device device, SipSubscribe.Event okEvent); | |
| 128 | + boolean audioBroadcastCmd(Device device); | |
| 129 | + | |
| 130 | + /** | |
| 123 | 131 | * 音视频录像控制 |
| 124 | 132 | * |
| 125 | 133 | * @param device 视频设备 | ... | ... |
src/main/java/com/genersoft/iot/vmp/gb28181/transmit/cmd/SIPRequestHeaderPlarformProvider.java
| ... | ... | @@ -3,7 +3,7 @@ package com.genersoft.iot.vmp.gb28181.transmit.cmd; |
| 3 | 3 | import com.genersoft.iot.vmp.conf.SipConfig; |
| 4 | 4 | import com.genersoft.iot.vmp.gb28181.bean.ParentPlatform; |
| 5 | 5 | import org.springframework.beans.factory.annotation.Autowired; |
| 6 | -import org.springframework.beans.factory.annotation.Qualifier; | |
| 6 | +// import org.springframework.beans.factory.annotation.Qualifier; | |
| 7 | 7 | import org.springframework.stereotype.Component; |
| 8 | 8 | import org.springframework.util.DigestUtils; |
| 9 | 9 | ... | ... |
src/main/java/com/genersoft/iot/vmp/gb28181/transmit/cmd/SIPRequestHeaderProvider.java
| ... | ... | @@ -6,14 +6,14 @@ import java.util.ArrayList; |
| 6 | 6 | import javax.sip.InvalidArgumentException; |
| 7 | 7 | import javax.sip.PeerUnavailableException; |
| 8 | 8 | import javax.sip.SipFactory; |
| 9 | -import javax.sip.SipProvider; | |
| 9 | +// import javax.sip.SipProvider; | |
| 10 | 10 | import javax.sip.address.Address; |
| 11 | 11 | import javax.sip.address.SipURI; |
| 12 | 12 | import javax.sip.header.*; |
| 13 | 13 | import javax.sip.message.Request; |
| 14 | 14 | |
| 15 | 15 | import org.springframework.beans.factory.annotation.Autowired; |
| 16 | -import org.springframework.beans.factory.annotation.Qualifier; | |
| 16 | +// import org.springframework.beans.factory.annotation.Qualifier; | |
| 17 | 17 | import org.springframework.stereotype.Component; |
| 18 | 18 | |
| 19 | 19 | import com.genersoft.iot.vmp.conf.SipConfig; | ... | ... |
src/main/java/com/genersoft/iot/vmp/gb28181/transmit/cmd/impl/SIPCommander.java
| ... | ... | @@ -23,7 +23,7 @@ import org.slf4j.LoggerFactory; |
| 23 | 23 | import org.springframework.beans.factory.annotation.Autowired; |
| 24 | 24 | import org.springframework.beans.factory.annotation.Qualifier; |
| 25 | 25 | import org.springframework.beans.factory.annotation.Value; |
| 26 | -import org.springframework.context.annotation.ComponentScan; | |
| 26 | +// import org.springframework.context.annotation.ComponentScan; | |
| 27 | 27 | import org.springframework.context.annotation.DependsOn; |
| 28 | 28 | import org.springframework.context.annotation.Lazy; |
| 29 | 29 | import org.springframework.stereotype.Component; |
| ... | ... | @@ -47,8 +47,7 @@ import com.genersoft.iot.vmp.gb28181.utils.XmlUtil; |
| 47 | 47 | public class SIPCommander implements ISIPCommander { |
| 48 | 48 | |
| 49 | 49 | private final Logger logger = LoggerFactory.getLogger(SIPCommander.class); |
| 50 | - | |
| 51 | - | |
| 50 | + | |
| 52 | 51 | @Autowired |
| 53 | 52 | private SipConfig sipConfig; |
| 54 | 53 | |
| ... | ... | @@ -623,11 +622,67 @@ public class SIPCommander implements ISIPCommander { |
| 623 | 622 | */ |
| 624 | 623 | @Override |
| 625 | 624 | public boolean audioBroadcastCmd(Device device, String channelId) { |
| 626 | - // TODO Auto-generated method stub | |
| 625 | + // 改为新的实现 | |
| 627 | 626 | return false; |
| 628 | 627 | } |
| 629 | 628 | |
| 630 | 629 | /** |
| 630 | + * 语音广播 | |
| 631 | + * | |
| 632 | + * @param device 视频设备 | |
| 633 | + * @param channelId 预览通道 | |
| 634 | + */ | |
| 635 | + @Override | |
| 636 | + public boolean audioBroadcastCmd(Device device) { | |
| 637 | + try { | |
| 638 | + StringBuffer broadcastXml = new StringBuffer(200); | |
| 639 | + broadcastXml.append("<?xml version=\"1.0\" ?>\r\n"); | |
| 640 | + broadcastXml.append("<Notify>\r\n"); | |
| 641 | + broadcastXml.append("<CmdType>Broadcast</CmdType>\r\n"); | |
| 642 | + broadcastXml.append("<SN>" + (int)((Math.random()*9+1)*100000) + "</SN>\r\n"); | |
| 643 | + broadcastXml.append("<SourceID>" + sipConfig.getSipId() + "</SourceID>\r\n"); | |
| 644 | + broadcastXml.append("<TargetID>" + device.getDeviceId() + "</TargetID>\r\n"); | |
| 645 | + broadcastXml.append("</Notify>\r\n"); | |
| 646 | + | |
| 647 | + String tm = Long.toString(System.currentTimeMillis()); | |
| 648 | + | |
| 649 | + CallIdHeader callIdHeader = device.getTransport().equals("TCP") ? tcpSipProvider.getNewCallId() | |
| 650 | + : udpSipProvider.getNewCallId(); | |
| 651 | + | |
| 652 | + Request request = headerProvider.createMessageRequest(device, broadcastXml.toString(), "z9hG4bK-ViaBcst-" + tm, "FromBcst" + tm, null, callIdHeader); | |
| 653 | + transmitRequest(device, request); | |
| 654 | + return true; | |
| 655 | + } catch (SipException | ParseException | InvalidArgumentException e) { | |
| 656 | + e.printStackTrace(); | |
| 657 | + } | |
| 658 | + return false; | |
| 659 | + } | |
| 660 | + @Override | |
| 661 | + public void audioBroadcastCmd(Device device, SipSubscribe.Event errorEvent) { | |
| 662 | + try { | |
| 663 | + StringBuffer broadcastXml = new StringBuffer(200); | |
| 664 | + broadcastXml.append("<?xml version=\"1.0\" ?>\r\n"); | |
| 665 | + broadcastXml.append("<Notify>\r\n"); | |
| 666 | + broadcastXml.append("<CmdType>Broadcast</CmdType>\r\n"); | |
| 667 | + broadcastXml.append("<SN>" + (int)((Math.random()*9+1)*100000) + "</SN>\r\n"); | |
| 668 | + broadcastXml.append("<SourceID>" + sipConfig.getSipId() + "</SourceID>\r\n"); | |
| 669 | + broadcastXml.append("<TargetID>" + device.getDeviceId() + "</TargetID>\r\n"); | |
| 670 | + broadcastXml.append("</Notify>\r\n"); | |
| 671 | + | |
| 672 | + String tm = Long.toString(System.currentTimeMillis()); | |
| 673 | + | |
| 674 | + CallIdHeader callIdHeader = device.getTransport().equals("TCP") ? tcpSipProvider.getNewCallId() | |
| 675 | + : udpSipProvider.getNewCallId(); | |
| 676 | + | |
| 677 | + Request request = headerProvider.createMessageRequest(device, broadcastXml.toString(), "z9hG4bK-ViaBcst-" + tm, "FromBcst" + tm, null, callIdHeader); | |
| 678 | + transmitRequest(device, request, errorEvent); | |
| 679 | + } catch (SipException | ParseException | InvalidArgumentException e) { | |
| 680 | + e.printStackTrace(); | |
| 681 | + } | |
| 682 | + } | |
| 683 | + | |
| 684 | + | |
| 685 | + /** | |
| 631 | 686 | * 音视频录像控制 |
| 632 | 687 | * |
| 633 | 688 | * @param device 视频设备 | ... | ... |
src/main/java/com/genersoft/iot/vmp/gb28181/transmit/cmd/impl/SIPCommanderFroPlatform.java
| ... | ... | @@ -15,7 +15,7 @@ import com.genersoft.iot.vmp.storager.IRedisCatchStorage; |
| 15 | 15 | import org.springframework.beans.factory.annotation.Autowired; |
| 16 | 16 | import org.springframework.beans.factory.annotation.Qualifier; |
| 17 | 17 | import org.springframework.beans.factory.annotation.Value; |
| 18 | -import org.springframework.context.annotation.ComponentScan; | |
| 18 | +// import org.springframework.context.annotation.ComponentScan; | |
| 19 | 19 | import org.springframework.context.annotation.DependsOn; |
| 20 | 20 | import org.springframework.context.annotation.Lazy; |
| 21 | 21 | import org.springframework.lang.Nullable; | ... | ... |
src/main/java/com/genersoft/iot/vmp/gb28181/transmit/request/impl/InviteRequestProcessor.java
| ... | ... | @@ -14,6 +14,7 @@ import javax.sip.message.Response; |
| 14 | 14 | import com.genersoft.iot.vmp.conf.MediaServerConfig; |
| 15 | 15 | import com.genersoft.iot.vmp.gb28181.bean.Device; |
| 16 | 16 | import com.genersoft.iot.vmp.gb28181.bean.DeviceChannel; |
| 17 | +import com.genersoft.iot.vmp.gb28181.bean.ParentPlatform; | |
| 17 | 18 | import com.genersoft.iot.vmp.gb28181.bean.SendRtpItem; |
| 18 | 19 | import com.genersoft.iot.vmp.gb28181.transmit.cmd.impl.SIPCommander; |
| 19 | 20 | import com.genersoft.iot.vmp.gb28181.transmit.cmd.impl.SIPCommanderFroPlatform; |
| ... | ... | @@ -74,144 +75,216 @@ public class InviteRequestProcessor extends SIPRequestAbstractProcessor { |
| 74 | 75 | Request request = evt.getRequest(); |
| 75 | 76 | SipURI sipURI = (SipURI) request.getRequestURI(); |
| 76 | 77 | String channelId = sipURI.getUser(); |
| 77 | - String platformId = null; | |
| 78 | + String requesterId = null; | |
| 78 | 79 | |
| 79 | 80 | FromHeader fromHeader = (FromHeader)request.getHeader(FromHeader.NAME); |
| 80 | 81 | AddressImpl address = (AddressImpl) fromHeader.getAddress(); |
| 81 | 82 | SipUri uri = (SipUri) address.getURI(); |
| 82 | - platformId = uri.getUser(); | |
| 83 | + requesterId = uri.getUser(); | |
| 83 | 84 | |
| 84 | - if (platformId == null || channelId == null) { | |
| 85 | - logger.info("无法从FromHeader的Address中获取到平台id,返回404"); | |
| 85 | + if (requesterId == null || channelId == null) { | |
| 86 | + logger.info("无法从FromHeader的Address中获取到平台id,返回400"); | |
| 86 | 87 | responseAck(evt, Response.BAD_REQUEST); // 参数不全, 发400,请求错误 |
| 87 | 88 | return; |
| 88 | 89 | } |
| 89 | - // 查询平台下是否有该通道 | |
| 90 | - DeviceChannel channel = storager.queryChannelInParentPlatform(platformId, channelId); | |
| 91 | - if (channel == null) { | |
| 92 | - logger.info("通道不存在,返回404"); | |
| 93 | - responseAck(evt, Response.NOT_FOUND); // 通道不存在,发404,资源不存在 | |
| 94 | - return; | |
| 95 | - }else { | |
| 96 | - responseAck(evt, Response.TRYING); // 通道存在,发100,trying | |
| 97 | - } | |
| 98 | - // 解析sdp消息, 使用jainsip 自带的sdp解析方式 | |
| 99 | - String contentString = new String(request.getRawContent()); | |
| 100 | - | |
| 101 | - // jainSip不支持y=字段, 移除移除以解析。 | |
| 102 | - int ssrcIndex = contentString.indexOf("y="); | |
| 103 | - String ssrc = contentString.substring(ssrcIndex + 2, ssrcIndex + 12); | |
| 104 | - //ssrc规定长度为10字节,不取余下长度以避免后续还有“f=”字段 | |
| 105 | - // String ssrc = contentString.substring(ssrcIndex + 2, contentString.length()) | |
| 106 | - // .replace("\r\n", "").replace("\n", ""); | |
| 107 | - | |
| 108 | - String substring = contentString.substring(0, contentString.indexOf("y=")); | |
| 109 | - SessionDescription sdp = SdpFactory.getInstance().createSessionDescription(substring); | |
| 110 | - | |
| 111 | - // 获取支持的格式 | |
| 112 | - Vector mediaDescriptions = sdp.getMediaDescriptions(true); | |
| 113 | - // 查看是否支持PS 负载96 | |
| 114 | - //String ip = null; | |
| 115 | - int port = -1; | |
| 116 | - //boolean recvonly = false; | |
| 117 | - boolean mediaTransmissionTCP = false; | |
| 118 | - Boolean tcpActive = null; | |
| 119 | - for (int i = 0; i < mediaDescriptions.size(); i++) { | |
| 120 | - MediaDescription mediaDescription = (MediaDescription)mediaDescriptions.get(i); | |
| 121 | - Media media = mediaDescription.getMedia(); | |
| 122 | - | |
| 123 | - Vector mediaFormats = media.getMediaFormats(false); | |
| 124 | - if (mediaFormats.contains("96")) { | |
| 125 | - port = media.getMediaPort(); | |
| 126 | - //String mediaType = media.getMediaType(); | |
| 127 | - String protocol = media.getProtocol(); | |
| 128 | - | |
| 129 | - // 区分TCP发流还是udp, 当前默认udp | |
| 130 | - if ("TCP/RTP/AVP".equals(protocol)) { | |
| 131 | - String setup = mediaDescription.getAttribute("setup"); | |
| 132 | - if (setup != null) { | |
| 133 | - mediaTransmissionTCP = true; | |
| 134 | - if ("active".equals(setup)) { | |
| 135 | - tcpActive = true; | |
| 136 | - }else if ("passive".equals(setup)) { | |
| 137 | - tcpActive = false; | |
| 90 | + | |
| 91 | + // 查询请求方是否上级平台 | |
| 92 | + ParentPlatform platform = storager.queryParentPlatById(requesterId); | |
| 93 | + if (platform != null) { | |
| 94 | + // 查询平台下是否有该通道 | |
| 95 | + DeviceChannel channel = storager.queryChannelInParentPlatform(requesterId, channelId); | |
| 96 | + if (channel == null) { | |
| 97 | + logger.info("通道不存在,返回404"); | |
| 98 | + responseAck(evt, Response.NOT_FOUND); // 通道不存在,发404,资源不存在 | |
| 99 | + return; | |
| 100 | + }else { | |
| 101 | + responseAck(evt, Response.CALL_IS_BEING_FORWARDED); // 通道存在,发181,呼叫转接中 | |
| 102 | + } | |
| 103 | + // 解析sdp消息, 使用jainsip 自带的sdp解析方式 | |
| 104 | + String contentString = new String(request.getRawContent()); | |
| 105 | + | |
| 106 | + // jainSip不支持y=字段, 移除移除以解析。 | |
| 107 | + int ssrcIndex = contentString.indexOf("y="); | |
| 108 | + //ssrc规定长度为10字节,不取余下长度以避免后续还有“f=”字段 | |
| 109 | + String ssrc = contentString.substring(ssrcIndex + 2, ssrcIndex + 12); | |
| 110 | + String substring = contentString.substring(0, contentString.indexOf("y=")); | |
| 111 | + SessionDescription sdp = SdpFactory.getInstance().createSessionDescription(substring); | |
| 112 | + | |
| 113 | + // 获取支持的格式 | |
| 114 | + Vector mediaDescriptions = sdp.getMediaDescriptions(true); | |
| 115 | + // 查看是否支持PS 负载96 | |
| 116 | + //String ip = null; | |
| 117 | + int port = -1; | |
| 118 | + //boolean recvonly = false; | |
| 119 | + boolean mediaTransmissionTCP = false; | |
| 120 | + Boolean tcpActive = null; | |
| 121 | + for (int i = 0; i < mediaDescriptions.size(); i++) { | |
| 122 | + MediaDescription mediaDescription = (MediaDescription)mediaDescriptions.get(i); | |
| 123 | + Media media = mediaDescription.getMedia(); | |
| 124 | + | |
| 125 | + Vector mediaFormats = media.getMediaFormats(false); | |
| 126 | + if (mediaFormats.contains("96")) { | |
| 127 | + port = media.getMediaPort(); | |
| 128 | + //String mediaType = media.getMediaType(); | |
| 129 | + String protocol = media.getProtocol(); | |
| 130 | + | |
| 131 | + // 区分TCP发流还是udp, 当前默认udp | |
| 132 | + if ("TCP/RTP/AVP".equals(protocol)) { | |
| 133 | + String setup = mediaDescription.getAttribute("setup"); | |
| 134 | + if (setup != null) { | |
| 135 | + mediaTransmissionTCP = true; | |
| 136 | + if ("active".equals(setup)) { | |
| 137 | + tcpActive = true; | |
| 138 | + }else if ("passive".equals(setup)) { | |
| 139 | + tcpActive = false; | |
| 140 | + } | |
| 138 | 141 | } |
| 139 | 142 | } |
| 143 | + break; | |
| 140 | 144 | } |
| 141 | - break; | |
| 142 | 145 | } |
| 143 | - } | |
| 144 | - if (port == -1) { | |
| 145 | - logger.info("不支持的媒体格式,返回415"); | |
| 146 | - // 回复不支持的格式 | |
| 147 | - responseAck(evt, Response.UNSUPPORTED_MEDIA_TYPE); // 不支持的格式,发415 | |
| 148 | - return; | |
| 149 | - } | |
| 150 | - String username = sdp.getOrigin().getUsername(); | |
| 151 | - String addressStr = sdp.getOrigin().getAddress(); | |
| 152 | - //String sessionName = sdp.getSessionName().getValue(); | |
| 153 | - logger.info("[上级点播]用户:{}, 地址:{}:{}, ssrc:{}", username, addressStr, port, ssrc); | |
| 154 | - | |
| 155 | - Device device = storager.queryVideoDeviceByPlatformIdAndChannelId(platformId, channelId); | |
| 156 | - if (device == null) { | |
| 157 | - logger.warn("点播平台{}的通道{}时未找到设备信息", platformId, channel); | |
| 158 | - responseAck(evt, Response.SERVER_INTERNAL_ERROR); | |
| 159 | - return; | |
| 160 | - } | |
| 161 | - SendRtpItem sendRtpItem = zlmrtpServerFactory.createSendRtpItem(addressStr, port, ssrc, platformId, device.getDeviceId(), channelId, | |
| 162 | - mediaTransmissionTCP); | |
| 163 | - if (tcpActive != null) { | |
| 164 | - sendRtpItem.setTcpActive(tcpActive); | |
| 165 | - } | |
| 166 | - if (sendRtpItem == null) { | |
| 167 | - logger.warn("服务器端口资源不足"); | |
| 168 | - responseAck(evt, Response.BUSY_HERE); | |
| 169 | - return; | |
| 170 | - } | |
| 146 | + if (port == -1) { | |
| 147 | + logger.info("不支持的媒体格式,返回415"); | |
| 148 | + // 回复不支持的格式 | |
| 149 | + responseAck(evt, Response.UNSUPPORTED_MEDIA_TYPE); // 不支持的格式,发415 | |
| 150 | + return; | |
| 151 | + } | |
| 152 | + String username = sdp.getOrigin().getUsername(); | |
| 153 | + String addressStr = sdp.getOrigin().getAddress(); | |
| 154 | + //String sessionName = sdp.getSessionName().getValue(); | |
| 155 | + logger.info("[上级点播]用户:{}, 地址:{}:{}, ssrc:{}", username, addressStr, port, ssrc); | |
| 156 | + | |
| 157 | + Device device = storager.queryVideoDeviceByPlatformIdAndChannelId(requesterId, channelId); | |
| 158 | + if (device == null) { | |
| 159 | + logger.warn("点播平台{}的通道{}时未找到设备信息", requesterId, channel); | |
| 160 | + responseAck(evt, Response.SERVER_INTERNAL_ERROR); | |
| 161 | + return; | |
| 162 | + } | |
| 163 | + SendRtpItem sendRtpItem = zlmrtpServerFactory.createSendRtpItem(addressStr, port, ssrc, requesterId, device.getDeviceId(), channelId, | |
| 164 | + mediaTransmissionTCP); | |
| 165 | + if (tcpActive != null) { | |
| 166 | + sendRtpItem.setTcpActive(tcpActive); | |
| 167 | + } | |
| 168 | + if (sendRtpItem == null) { | |
| 169 | + logger.warn("服务器端口资源不足"); | |
| 170 | + responseAck(evt, Response.BUSY_HERE); | |
| 171 | + return; | |
| 172 | + } | |
| 171 | 173 | |
| 172 | - // 写入redis, 超时时回复 | |
| 173 | - redisCatchStorage.updateSendRTPSever(sendRtpItem); | |
| 174 | - // 通知下级推流, | |
| 175 | - PlayResult playResult = playService.play(device.getDeviceId(), channelId, (responseJSON)->{ | |
| 176 | - // 收到推流, 回复200OK, 等待ack | |
| 177 | - sendRtpItem.setStatus(1); | |
| 174 | + // 写入redis, 超时时回复 | |
| 178 | 175 | redisCatchStorage.updateSendRTPSever(sendRtpItem); |
| 179 | - // TODO 添加对tcp的支持 | |
| 180 | - MediaServerConfig mediaInfo = redisCatchStorage.getMediaInfo(); | |
| 181 | - StringBuffer content = new StringBuffer(200); | |
| 182 | - content.append("v=0\r\n"); | |
| 183 | - content.append("o="+"00000"+" 0 0 IN IP4 "+mediaInfo.getWanIp()+"\r\n"); | |
| 184 | - content.append("s=Play\r\n"); | |
| 185 | - content.append("c=IN IP4 "+mediaInfo.getWanIp()+"\r\n"); | |
| 186 | - content.append("t=0 0\r\n"); | |
| 187 | - content.append("m=video "+ sendRtpItem.getLocalPort()+" RTP/AVP 96\r\n"); | |
| 188 | - content.append("a=sendonly\r\n"); | |
| 189 | - content.append("a=rtpmap:96 PS/90000\r\n"); | |
| 190 | - content.append("y="+ ssrc + "\r\n"); | |
| 191 | - content.append("f=\r\n"); | |
| 192 | - | |
| 193 | - try { | |
| 194 | - responseAck(evt, content.toString()); | |
| 195 | - } catch (SipException e) { | |
| 196 | - e.printStackTrace(); | |
| 197 | - } catch (InvalidArgumentException e) { | |
| 198 | - e.printStackTrace(); | |
| 199 | - } catch (ParseException e) { | |
| 200 | - e.printStackTrace(); | |
| 176 | + // 通知下级推流, | |
| 177 | + PlayResult playResult = playService.play(device.getDeviceId(), channelId, (responseJSON)->{ | |
| 178 | + // 收到推流, 回复200OK, 等待ack | |
| 179 | + sendRtpItem.setStatus(1); | |
| 180 | + redisCatchStorage.updateSendRTPSever(sendRtpItem); | |
| 181 | + // TODO 添加对tcp的支持 | |
| 182 | + MediaServerConfig mediaInfo = redisCatchStorage.getMediaInfo(); | |
| 183 | + StringBuffer content = new StringBuffer(200); | |
| 184 | + content.append("v=0\r\n"); | |
| 185 | + content.append("o="+"00000"+" 0 0 IN IP4 "+mediaInfo.getWanIp()+"\r\n"); | |
| 186 | + content.append("s=Play\r\n"); | |
| 187 | + content.append("c=IN IP4 "+mediaInfo.getWanIp()+"\r\n"); | |
| 188 | + content.append("t=0 0\r\n"); | |
| 189 | + content.append("m=video "+ sendRtpItem.getLocalPort()+" RTP/AVP 96\r\n"); | |
| 190 | + content.append("a=sendonly\r\n"); | |
| 191 | + content.append("a=rtpmap:96 PS/90000\r\n"); | |
| 192 | + content.append("y="+ ssrc + "\r\n"); | |
| 193 | + content.append("f=\r\n"); | |
| 194 | + | |
| 195 | + try { | |
| 196 | + responseAck(evt, content.toString()); | |
| 197 | + } catch (SipException e) { | |
| 198 | + e.printStackTrace(); | |
| 199 | + } catch (InvalidArgumentException e) { | |
| 200 | + e.printStackTrace(); | |
| 201 | + } catch (ParseException e) { | |
| 202 | + e.printStackTrace(); | |
| 203 | + } | |
| 204 | + },(event -> { | |
| 205 | + // 未知错误。直接转发设备点播的错误 | |
| 206 | + Response response = null; | |
| 207 | + try { | |
| 208 | + response = getMessageFactory().createResponse(event.getResponse().getStatusCode(), evt.getRequest()); | |
| 209 | + getServerTransaction(evt).sendResponse(response); | |
| 210 | + } catch (ParseException | SipException | InvalidArgumentException e) { | |
| 211 | + e.printStackTrace(); | |
| 212 | + } | |
| 213 | + })); | |
| 214 | + if (logger.isDebugEnabled()) { | |
| 215 | + logger.debug(playResult.getResult().toString()); | |
| 201 | 216 | } |
| 202 | - },(event -> { | |
| 203 | - // 未知错误。直接转发设备点播的错误 | |
| 204 | - Response response = null; | |
| 205 | - try { | |
| 206 | - response = getMessageFactory().createResponse(event.getResponse().getStatusCode(), evt.getRequest()); | |
| 207 | - getServerTransaction(evt).sendResponse(response); | |
| 208 | - | |
| 209 | - } catch (ParseException | SipException | InvalidArgumentException e) { | |
| 210 | - e.printStackTrace(); | |
| 217 | + } else { | |
| 218 | + // 非上级平台请求,查询是否设备请求(通常为接收语音广播的设备) | |
| 219 | + Device device = storager.queryVideoDevice(requesterId); | |
| 220 | + if (device != null) { | |
| 221 | + logger.info("收到设备" + requesterId + "的语音广播Invite请求"); | |
| 222 | + responseAck(evt, Response.TRYING); | |
| 223 | + | |
| 224 | + String contentString = new String(request.getRawContent()); | |
| 225 | + // jainSip不支持y=字段, 移除移除以解析。 | |
| 226 | + String substring = contentString; | |
| 227 | + String ssrc = "0000000404"; | |
| 228 | + int ssrcIndex = contentString.indexOf("y="); | |
| 229 | + if (ssrcIndex > 0) { | |
| 230 | + substring = contentString.substring(0, ssrcIndex); | |
| 231 | + ssrc = contentString.substring(ssrcIndex + 2, ssrcIndex + 12); | |
| 232 | + } | |
| 233 | + ssrcIndex = substring.indexOf("f="); | |
| 234 | + if (ssrcIndex > 0) { | |
| 235 | + substring = contentString.substring(0, ssrcIndex); | |
| 236 | + } | |
| 237 | + SessionDescription sdp = SdpFactory.getInstance().createSessionDescription(substring); | |
| 238 | + | |
| 239 | + // 获取支持的格式 | |
| 240 | + Vector mediaDescriptions = sdp.getMediaDescriptions(true); | |
| 241 | + // 查看是否支持PS 负载96 | |
| 242 | + int port = -1; | |
| 243 | + //boolean recvonly = false; | |
| 244 | + boolean mediaTransmissionTCP = false; | |
| 245 | + Boolean tcpActive = null; | |
| 246 | + for (int i = 0; i < mediaDescriptions.size(); i++) { | |
| 247 | + MediaDescription mediaDescription = (MediaDescription)mediaDescriptions.get(i); | |
| 248 | + Media media = mediaDescription.getMedia(); | |
| 249 | + | |
| 250 | + Vector mediaFormats = media.getMediaFormats(false); | |
| 251 | + if (mediaFormats.contains("8")) { | |
| 252 | + port = media.getMediaPort(); | |
| 253 | + String protocol = media.getProtocol(); | |
| 254 | + // 区分TCP发流还是udp, 当前默认udp | |
| 255 | + if ("TCP/RTP/AVP".equals(protocol)) { | |
| 256 | + String setup = mediaDescription.getAttribute("setup"); | |
| 257 | + if (setup != null) { | |
| 258 | + mediaTransmissionTCP = true; | |
| 259 | + if ("active".equals(setup)) { | |
| 260 | + tcpActive = true; | |
| 261 | + } else if ("passive".equals(setup)) { | |
| 262 | + tcpActive = false; | |
| 263 | + } | |
| 264 | + } | |
| 265 | + } | |
| 266 | + break; | |
| 267 | + } | |
| 268 | + } | |
| 269 | + if (port == -1) { | |
| 270 | + logger.info("不支持的媒体格式,返回415"); | |
| 271 | + // 回复不支持的格式 | |
| 272 | + responseAck(evt, Response.UNSUPPORTED_MEDIA_TYPE); // 不支持的格式,发415 | |
| 273 | + return; | |
| 274 | + } | |
| 275 | + String username = sdp.getOrigin().getUsername(); | |
| 276 | + String addressStr = sdp.getOrigin().getAddress(); | |
| 277 | + logger.info("设备{}请求语音流,地址:{}:{},ssrc:{}", username, addressStr, port, ssrc); | |
| 278 | + | |
| 279 | + | |
| 280 | + | |
| 281 | + | |
| 282 | + | |
| 283 | + | |
| 284 | + } else { | |
| 285 | + logger.warn("来自无效设备/平台的请求"); | |
| 286 | + responseAck(evt, Response.BAD_REQUEST); | |
| 211 | 287 | } |
| 212 | - })); | |
| 213 | - if (logger.isDebugEnabled()) { | |
| 214 | - logger.debug(playResult.getResult().toString()); | |
| 215 | 288 | } |
| 216 | 289 | |
| 217 | 290 | } catch (SipException | InvalidArgumentException | ParseException e) { | ... | ... |
src/main/java/com/genersoft/iot/vmp/gb28181/transmit/request/impl/MessageRequestProcessor.java
| ... | ... | @@ -93,7 +93,7 @@ public class MessageRequestProcessor extends SIPRequestAbstractProcessor { |
| 93 | 93 | private static final String MESSAGE_ALARM = "Alarm"; |
| 94 | 94 | private static final String MESSAGE_RECORD_INFO = "RecordInfo"; |
| 95 | 95 | private static final String MESSAGE_MEDIA_STATUS = "MediaStatus"; |
| 96 | - // private static final String MESSAGE_BROADCAST = "Broadcast"; | |
| 96 | + private static final String MESSAGE_BROADCAST = "Broadcast"; | |
| 97 | 97 | private static final String MESSAGE_DEVICE_STATUS = "DeviceStatus"; |
| 98 | 98 | private static final String MESSAGE_DEVICE_CONTROL = "DeviceControl"; |
| 99 | 99 | private static final String MESSAGE_DEVICE_CONFIG = "DeviceConfig"; |
| ... | ... | @@ -123,7 +123,7 @@ public class MessageRequestProcessor extends SIPRequestAbstractProcessor { |
| 123 | 123 | logger.info("接收到Catalog消息"); |
| 124 | 124 | processMessageCatalogList(evt); |
| 125 | 125 | } else if (MESSAGE_DEVICE_INFO.equals(cmd)) { |
| 126 | - //DeviceInfo消息处理 | |
| 126 | + // DeviceInfo消息处理 | |
| 127 | 127 | processMessageDeviceInfo(evt); |
| 128 | 128 | } else if (MESSAGE_DEVICE_STATUS.equals(cmd)) { |
| 129 | 129 | // DeviceStatus消息处理 |
| ... | ... | @@ -149,6 +149,9 @@ public class MessageRequestProcessor extends SIPRequestAbstractProcessor { |
| 149 | 149 | } else if (MESSAGE_PRESET_QUERY.equals(cmd)) { |
| 150 | 150 | logger.info("接收到PresetQuery消息"); |
| 151 | 151 | processMessagePresetQuery(evt); |
| 152 | + } else if (MESSAGE_BROADCAST.equals(cmd)) { | |
| 153 | + // Broadcast消息处理 | |
| 154 | + processMessageBroadcast(evt); | |
| 152 | 155 | } else { |
| 153 | 156 | logger.info("接收到消息:" + cmd); |
| 154 | 157 | responseAck(evt); |
| ... | ... | @@ -298,7 +301,7 @@ public class MessageRequestProcessor extends SIPRequestAbstractProcessor { |
| 298 | 301 | // 远程启动功能 |
| 299 | 302 | if (!XmlUtil.isEmpty(XmlUtil.getText(rootElement, "TeleBoot"))) { |
| 300 | 303 | if (deviceId.equals(targetGBId)) { |
| 301 | - // 远程启动功能:需要在重新启动程序后先对SipStack解绑 | |
| 304 | + // 远程启动本平台:需要在重新启动程序后先对SipStack解绑 | |
| 302 | 305 | logger.info("执行远程启动本平台命令"); |
| 303 | 306 | ParentPlatform parentPlatform = storager.queryParentPlatById(platformId); |
| 304 | 307 | cmderFroPlatform.unregister(parentPlatform, null, null); |
| ... | ... | @@ -333,6 +336,7 @@ public class MessageRequestProcessor extends SIPRequestAbstractProcessor { |
| 333 | 336 | // 远程启动指定设备 |
| 334 | 337 | } |
| 335 | 338 | } |
| 339 | + // 云台/前端控制命令 | |
| 336 | 340 | if (!XmlUtil.isEmpty(XmlUtil.getText(rootElement,"PTZCmd")) && !deviceId.equals(targetGBId)) { |
| 337 | 341 | String cmdString = XmlUtil.getText(rootElement,"PTZCmd"); |
| 338 | 342 | Device device = storager.queryVideoDeviceByPlatformIdAndChannelId(platformId, deviceId); |
| ... | ... | @@ -895,6 +899,37 @@ public class MessageRequestProcessor extends SIPRequestAbstractProcessor { |
| 895 | 899 | } |
| 896 | 900 | } |
| 897 | 901 | |
| 902 | + /** | |
| 903 | + * 处理AudioBroadcast语音广播Message | |
| 904 | + * | |
| 905 | + * @param evt | |
| 906 | + */ | |
| 907 | + private void processMessageBroadcast(RequestEvent evt) { | |
| 908 | + try { | |
| 909 | + Element rootElement = getRootElement(evt); | |
| 910 | + String deviceId = XmlUtil.getText(rootElement, "DeviceID"); | |
| 911 | + // 回复200 OK | |
| 912 | + responseAck(evt); | |
| 913 | + if (rootElement.getName().equals("Response")) { | |
| 914 | + // 此处是对本平台发出Broadcast指令的应答 | |
| 915 | + JSONObject json = new JSONObject(); | |
| 916 | + XmlUtil.node2Json(rootElement, json); | |
| 917 | + if (logger.isDebugEnabled()) { | |
| 918 | + logger.debug(json.toJSONString()); | |
| 919 | + } | |
| 920 | + RequestMessage msg = new RequestMessage(); | |
| 921 | + msg.setDeviceId(deviceId); | |
| 922 | + msg.setType(DeferredResultHolder.CALLBACK_CMD_BROADCAST); | |
| 923 | + msg.setData(json); | |
| 924 | + deferredResultHolder.invokeResult(msg); | |
| 925 | + } else { | |
| 926 | + // 此处是上级发出的Broadcast指令 | |
| 927 | + } | |
| 928 | + } catch (ParseException | SipException | InvalidArgumentException | DocumentException e) { | |
| 929 | + e.printStackTrace(); | |
| 930 | + } | |
| 931 | + } | |
| 932 | + | |
| 898 | 933 | |
| 899 | 934 | /*** |
| 900 | 935 | * 回复200 OK | ... | ... |
src/main/java/com/genersoft/iot/vmp/gb28181/transmit/response/impl/RegisterResponseProcessor.java
| ... | ... | @@ -50,7 +50,6 @@ public class RegisterResponseProcessor implements ISIPResponseProcessor { |
| 50 | 50 | */ |
| 51 | 51 | @Override |
| 52 | 52 | public void process(ResponseEvent evt, SipLayer layer, SipConfig config) { |
| 53 | - // TODO Auto-generated method stub | |
| 54 | 53 | Response response = evt.getResponse(); |
| 55 | 54 | CallIdHeader callIdHeader = (CallIdHeader) response.getHeader(CallIdHeader.NAME); |
| 56 | 55 | String callId = callIdHeader.getCallId(); | ... | ... |
src/main/java/com/genersoft/iot/vmp/storager/IRedisCatchStorage.java
src/main/java/com/genersoft/iot/vmp/storager/impl/RedisCatchStorageImpl.java
| ... | ... | @@ -13,6 +13,7 @@ import org.springframework.stereotype.Component; |
| 13 | 13 | |
| 14 | 14 | import java.util.*; |
| 15 | 15 | |
| 16 | +@SuppressWarnings("rawtypes") | |
| 16 | 17 | @Component |
| 17 | 18 | public class RedisCatchStorageImpl implements IRedisCatchStorage { |
| 18 | 19 | |
| ... | ... | @@ -213,6 +214,14 @@ public class RedisCatchStorageImpl implements IRedisCatchStorage { |
| 213 | 214 | } |
| 214 | 215 | |
| 215 | 216 | @Override |
| 217 | + public void cleanPlatformRegisterInfos() { | |
| 218 | + List regInfos = redis.scan(VideoManagerConstants.PLATFORM_REGISTER_INFO_PREFIX + "*"); | |
| 219 | + for (Object key : regInfos) { | |
| 220 | + redis.del(key.toString()); | |
| 221 | + } | |
| 222 | + } | |
| 223 | + | |
| 224 | + @Override | |
| 216 | 225 | public void updateSendRTPSever(SendRtpItem sendRtpItem) { |
| 217 | 226 | String key = VideoManagerConstants.PLATFORM_SEND_RTP_INFO_PREFIX + sendRtpItem.getPlatformId() + "_" + sendRtpItem.getChannelId(); |
| 218 | 227 | redis.set(key, sendRtpItem); | ... | ... |
src/main/java/com/genersoft/iot/vmp/vmanager/play/PlayController.java
| ... | ... | @@ -2,6 +2,7 @@ package com.genersoft.iot.vmp.vmanager.play; |
| 2 | 2 | |
| 3 | 3 | import com.genersoft.iot.vmp.common.StreamInfo; |
| 4 | 4 | import com.genersoft.iot.vmp.conf.MediaServerConfig; |
| 5 | +import com.genersoft.iot.vmp.gb28181.bean.Device; | |
| 5 | 6 | import com.genersoft.iot.vmp.gb28181.transmit.callback.DeferredResultHolder; |
| 6 | 7 | import com.genersoft.iot.vmp.gb28181.transmit.callback.RequestMessage; |
| 7 | 8 | import com.genersoft.iot.vmp.media.zlm.ZLMRESTfulUtils; |
| ... | ... | @@ -27,6 +28,8 @@ import org.springframework.web.context.request.async.DeferredResult; |
| 27 | 28 | |
| 28 | 29 | import java.util.UUID; |
| 29 | 30 | |
| 31 | +import javax.sip.message.Response; | |
| 32 | + | |
| 30 | 33 | @CrossOrigin |
| 31 | 34 | @RestController |
| 32 | 35 | @RequestMapping("/api") |
| ... | ... | @@ -204,5 +207,47 @@ public class PlayController { |
| 204 | 207 | } |
| 205 | 208 | return new ResponseEntity<String>( result.toJSONString(), HttpStatus.OK); |
| 206 | 209 | } |
| 210 | + | |
| 211 | + /** | |
| 212 | + * 语音广播命令API接口 | |
| 213 | + * | |
| 214 | + * @param deviceId | |
| 215 | + */ | |
| 216 | + @GetMapping("/broadcast/{deviceId}") | |
| 217 | + @PostMapping("/broadcast/{deviceId}") | |
| 218 | + public DeferredResult<ResponseEntity<String>> broadcastApi(@PathVariable String deviceId) { | |
| 219 | + if (logger.isDebugEnabled()) { | |
| 220 | + logger.debug("语音广播API调用"); | |
| 221 | + } | |
| 222 | + Device device = storager.queryVideoDevice(deviceId); | |
| 223 | + cmder.audioBroadcastCmd(device, event -> { | |
| 224 | + Response response = event.getResponse(); | |
| 225 | + RequestMessage msg = new RequestMessage(); | |
| 226 | + msg.setId(DeferredResultHolder.CALLBACK_CMD_BROADCAST + deviceId); | |
| 227 | + JSONObject json = new JSONObject(); | |
| 228 | + json.put("DeviceID", deviceId); | |
| 229 | + json.put("CmdType", "Broadcast"); | |
| 230 | + json.put("Result", "Failed"); | |
| 231 | + json.put("Description", String.format("语音广播操作失败,错误码: %s, %s", response.getStatusCode(), response.getReasonPhrase())); | |
| 232 | + msg.setData(json); | |
| 233 | + resultHolder.invokeResult(msg); | |
| 234 | + }); | |
| 235 | + DeferredResult<ResponseEntity<String>> result = new DeferredResult<ResponseEntity<String>>(3 * 1000L); | |
| 236 | + result.onTimeout(() -> { | |
| 237 | + logger.warn(String.format("语音广播操作超时, 设备未返回应答指令")); | |
| 238 | + RequestMessage msg = new RequestMessage(); | |
| 239 | + msg.setId(DeferredResultHolder.CALLBACK_CMD_BROADCAST + deviceId); | |
| 240 | + JSONObject json = new JSONObject(); | |
| 241 | + json.put("DeviceID", deviceId); | |
| 242 | + json.put("CmdType", "Broadcast"); | |
| 243 | + json.put("Result", "Failed"); | |
| 244 | + json.put("Error", "Timeout. Device did not response to broadcast command."); | |
| 245 | + msg.setData(json); | |
| 246 | + resultHolder.invokeResult(msg); | |
| 247 | + }); | |
| 248 | + resultHolder.put(DeferredResultHolder.CALLBACK_CMD_BROADCAST + deviceId, result); | |
| 249 | + return result; | |
| 250 | + } | |
| 251 | + | |
| 207 | 252 | } |
| 208 | 253 | ... | ... |
web_src/src/components/channelList.vue
| ... | ... | @@ -99,7 +99,7 @@ export default { |
| 99 | 99 | currentPage: parseInt(this.$route.params.page), |
| 100 | 100 | count: parseInt(this.$route.params.count), |
| 101 | 101 | total: 0, |
| 102 | - beforeUrl: "/videoList", | |
| 102 | + beforeUrl: "/deviceList", | |
| 103 | 103 | isLoging: false, |
| 104 | 104 | autoList: true |
| 105 | 105 | }; |
| ... | ... | @@ -131,7 +131,7 @@ export default { |
| 131 | 131 | this.currentPage = parseInt(this.$route.params.page); |
| 132 | 132 | this.count = parseInt(this.$route.params.count); |
| 133 | 133 | if (this.parentChannelId == "" || this.parentChannelId == 0) { |
| 134 | - this.beforeUrl = "/videoList" | |
| 134 | + this.beforeUrl = "/deviceList" | |
| 135 | 135 | } |
| 136 | 136 | |
| 137 | 137 | }, | ... | ... |
web_src/src/components/devicePosition.vue
| ... | ... | @@ -81,7 +81,7 @@ export default { |
| 81 | 81 | parentChannelId: this.$route.params.parentChannelId, |
| 82 | 82 | updateLooper: 0, //数据刷新轮训标志 |
| 83 | 83 | total: 0, |
| 84 | - beforeUrl: "/videoList", | |
| 84 | + beforeUrl: "/deviceList", | |
| 85 | 85 | isLoging: false, |
| 86 | 86 | autoList: false, |
| 87 | 87 | }; |
| ... | ... | @@ -111,7 +111,7 @@ export default { |
| 111 | 111 | // this.currentPage = parseInt(this.$route.params.page); |
| 112 | 112 | // this.count = parseInt(this.$route.params.count); |
| 113 | 113 | // if (this.parentChannelId == "" || this.parentChannelId == 0) { |
| 114 | - // this.beforeUrl = "/videoList"; | |
| 114 | + // this.beforeUrl = "/deviceList"; | |
| 115 | 115 | // } |
| 116 | 116 | }, |
| 117 | 117 | initBaiduMap() { | ... | ... |