Commit 652489b47ef582b3f492039ab7fd58c558c1ba36

Authored by ‘sxh’
2 parents 05cc0d14 319cdd21

Merge remote-tracking branch 'origin/wvp-28181-2.0' into wvp-28181-2.0

# Conflicts:
#	src/main/java/com/genersoft/iot/vmp/media/zlm/ZLMHttpHookListener.java
#	src/main/java/com/genersoft/iot/vmp/service/impl/PlayServiceImpl.java
#	src/main/java/com/genersoft/iot/vmp/vmanager/gb28181/play/PlayController.java
Showing 44 changed files with 644 additions and 269 deletions
README.md
... ... @@ -27,7 +27,7 @@ wvp使用文档 [https://doc.wvp-pro.cn](https://doc.wvp-pro.cn)
27 27 ZLM使用文档 [https://github.com/ZLMediaKit/ZLMediaKit](https://github.com/ZLMediaKit/ZLMediaKit)
28 28 > wvp文档由gitee提供服务,如果遇到打不开请多刷新几次。
29 29  
30   -# 社群地址
  30 +# 付费社群
31 31 [![社群](doc/_media/shequ.png "shequ")](https://t.zsxq.com/0d8VAD3Dm)
32 32 > 收费是为了提供更好的服务,也是对作者更大的激励。加入星球的用户三天后可以私信我留下微信号,我会拉大家入群。加入三天内不满意可以直接退款,大家不需要有顾虑,来白嫖三天也不是不可以。
33 33  
... ... @@ -105,6 +105,7 @@ https://gitee.com/pan648540858/wvp-GB28181-pro.git
105 105 - [X] 支持打包可执行jar和war
106 106 - [X] 支持跨域请求,支持前后端分离部署
107 107 - [X] 支持Mysql,Postgresql,金仓等数据库
  108 +- [X] 支持Onvif(目前在onvif分支,需要安装onvif服务,服务请在知识星球获取)
108 109  
109 110 # 授权协议
110 111 本项目自有代码使用宽松的MIT协议,在保留版权信息的情况下可以自由应用于各自商用、非商业的项目。 但是本项目也零碎的使用了一些其他的开源代码,在商用的情况下请自行替代或剔除; 由于使用本项目而产生的商业纠纷或侵权行为一概与本项目及开发者无关,请自行承担法律风险。 在使用本项目代码时,也应该在授权协议中同时表明本项目依赖的第三方库的协议
... ...
doc/README.md
... ... @@ -14,7 +14,7 @@
14 14 - 完全开源,且使用MIT许可协议。保留版权的情况下可以用于商业项目。
15 15 - 支持多流媒体节点负载均衡。
16 16  
17   -# 社群
  17 +# 付费社群
18 18 [![社群](_media/shequ.png "shequ")](https://t.zsxq.com/0d8VAD3Dm)
19 19 > 收费是为了提供更好的服务,也是对作者更大的激励。加入星球的用户三天后可以私信我留下微信号,我会拉大家入群。加入三天内不满意可以直接退款,大家不需要有顾虑,来白嫖三天也不是不可以。
20 20  
... ... @@ -62,16 +62,16 @@
62 62 - [X] 注册
63 63 - [X] 注销
64 64 - [X] 实时视音频点播
65   -- [ ] 设备控制
66   - - [ ] 云台控制
  65 +- [X] 设备控制
  66 + - [X] 云台控制
67 67 - [ ] 远程启动
68   - - [ ] 录像控制
69   - - [ ] 报警布防/撤防
70   - - [ ] 报警复位
71   - - [ ] 强制关键帧
72   - - [ ] 拉框放大
73   - - [ ] 拉框缩小
74   - - [ ] 看守位控制
  68 + - [X] 录像控制
  69 + - [X] 报警布防/撤防
  70 + - [X] 报警复位
  71 + - [X] 强制关键帧
  72 + - [X] 拉框放大
  73 + - [X] 拉框缩小
  74 + - [X] 看守位控制
75 75 - [ ] 设备配置
76 76 - [ ] 报警事件通知和分发
77 77 - [X] 设备目录订阅
... ... @@ -79,7 +79,7 @@
79 79 - [X] 设备目录查询
80 80 - [X] 设备状态查询
81 81 - [ ] 设备配置查询
82   - - [ ] 设备预置位查询
  82 + - [X] 设备预置位查询
83 83 - [X] 状态信息报送
84 84 - [X] 设备视音频文件检索
85 85 - [X] 历史视音频的回放
... ... @@ -87,7 +87,7 @@
87 87 - [x] 暂停
88 88 - [x] 进/退
89 89 - [x] 停止
90   -- [ ] 视音频文件下载
  90 +- [X] 视音频文件下载
91 91 - [ ] ~~校时~~
92 92 - [X] 订阅和通知
93 93 - [X] 事件订阅
... ...
doc/_media/1372762149.jpg 0 → 100644

126 KB

doc/_media/903207146.jpg 0 → 100644

131 KB

... ... @@ -11,7 +11,7 @@
11 11  
12 12 <groupId>com.genersoft</groupId>
13 13 <artifactId>wvp-pro</artifactId>
14   - <version>2.6.8</version>
  14 + <version>2.6.9</version>
15 15 <name>web video platform</name>
16 16 <description>国标28181视频平台</description>
17 17 <packaging>${project.packaging}</packaging>
... ...
sql/2.6.8升级2.6.9.sql
... ... @@ -180,10 +180,6 @@ alter table device_mobile_position
180 180 change createTime create_time varchar(50) null;
181 181  
182 182 alter table gb_stream
183   - add constraint gb_stream_pk
184   - primary key (gbStreamId);
185   -
186   -alter table gb_stream
187 183 change gbStreamId gb_stream_id int auto_increment;
188 184  
189 185 alter table gb_stream
... ...
src/main/java/com/genersoft/iot/vmp/common/GeneralCallback.java 0 → 100644
  1 +package com.genersoft.iot.vmp.common;
  2 +
  3 +public interface GeneralCallback<T>{
  4 + void run(int code, String msg, T data);
  5 +}
... ...
src/main/java/com/genersoft/iot/vmp/common/VideoManagerConstants.java
... ... @@ -107,6 +107,11 @@ public class VideoManagerConstants {
107 107 public static final String VM_MSG_STREAM_PUSH_RESPONSE = "VM_MSG_STREAM_PUSH_RESPONSE";
108 108  
109 109 /**
  110 + * redis 通知平台关闭推流
  111 + */
  112 + public static final String VM_MSG_STREAM_PUSH_CLOSE = "VM_MSG_STREAM_PUSH_CLOSE";
  113 +
  114 + /**
110 115 * redis 消息请求所有的在线通道
111 116 */
112 117 public static final String VM_MSG_GET_ALL_ONLINE_REQUESTED = "VM_MSG_GET_ALL_ONLINE_REQUESTED";
... ...
src/main/java/com/genersoft/iot/vmp/conf/redis/RedisMsgListenConfig.java
... ... @@ -43,6 +43,9 @@ public class RedisMsgListenConfig {
43 43 @Autowired
44 44 private RedisPushStreamResponseListener redisPushStreamResponseListener;
45 45  
  46 + @Autowired
  47 + private RedisCloseStreamMsgListener redisCloseStreamMsgListener;
  48 +
46 49  
47 50 /**
48 51 * redis消息监听器容器 可以添加多个监听不同话题的redis监听器,只需要把消息监听器和相应的消息订阅处理器绑定,该消息监听器
... ... @@ -63,6 +66,7 @@ public class RedisMsgListenConfig {
63 66 container.addMessageListener(redisPushStreamStatusMsgListener, new PatternTopic(VideoManagerConstants.VM_MSG_PUSH_STREAM_STATUS_CHANGE));
64 67 container.addMessageListener(redisPushStreamListMsgListener, new PatternTopic(VideoManagerConstants.VM_MSG_PUSH_STREAM_LIST_CHANGE));
65 68 container.addMessageListener(redisPushStreamResponseListener, new PatternTopic(VideoManagerConstants.VM_MSG_STREAM_PUSH_RESPONSE));
  69 + container.addMessageListener(redisCloseStreamMsgListener, new PatternTopic(VideoManagerConstants.VM_MSG_STREAM_PUSH_CLOSE));
66 70 return container;
67 71 }
68 72 }
... ...
src/main/java/com/genersoft/iot/vmp/gb28181/bean/Gb28181Sdp.java 0 → 100644
  1 +package com.genersoft.iot.vmp.gb28181.bean;
  2 +
  3 +import javax.sdp.SessionDescription;
  4 +
  5 +/**
  6 + * 28181 的SDP解析器
  7 + */
  8 +public class Gb28181Sdp {
  9 + private SessionDescription baseSdb;
  10 + private String ssrc;
  11 +
  12 + private String mediaDescription;
  13 +
  14 + public static Gb28181Sdp getInstance(SessionDescription baseSdb, String ssrc, String mediaDescription) {
  15 + Gb28181Sdp gb28181Sdp = new Gb28181Sdp();
  16 + gb28181Sdp.setBaseSdb(baseSdb);
  17 + gb28181Sdp.setSsrc(ssrc);
  18 + gb28181Sdp.setMediaDescription(mediaDescription);
  19 + return gb28181Sdp;
  20 + }
  21 +
  22 +
  23 + public SessionDescription getBaseSdb() {
  24 + return baseSdb;
  25 + }
  26 +
  27 + public void setBaseSdb(SessionDescription baseSdb) {
  28 + this.baseSdb = baseSdb;
  29 + }
  30 +
  31 + public String getSsrc() {
  32 + return ssrc;
  33 + }
  34 +
  35 + public void setSsrc(String ssrc) {
  36 + this.ssrc = ssrc;
  37 + }
  38 +
  39 + public String getMediaDescription() {
  40 + return mediaDescription;
  41 + }
  42 +
  43 + public void setMediaDescription(String mediaDescription) {
  44 + this.mediaDescription = mediaDescription;
  45 + }
  46 +}
... ...
src/main/java/com/genersoft/iot/vmp/gb28181/session/SSRCFactory.java
1 1 package com.genersoft.iot.vmp.gb28181.session;
2 2  
3 3 import com.genersoft.iot.vmp.conf.SipConfig;
  4 +import com.genersoft.iot.vmp.conf.UserSetting;
4 5 import org.springframework.beans.factory.annotation.Autowired;
5 6 import org.springframework.data.redis.core.StringRedisTemplate;
6 7 import org.springframework.stereotype.Component;
... ... @@ -31,10 +32,13 @@ public class SSRCFactory {
31 32 @Autowired
32 33 private SipConfig sipConfig;
33 34  
  35 + @Autowired
  36 + private UserSetting userSetting;
  37 +
34 38  
35 39 public void initMediaServerSSRC(String mediaServerId, Set<String> usedSet) {
36 40 String ssrcPrefix = sipConfig.getDomain().substring(3, 8);
37   - String redisKey = SSRC_INFO_KEY + mediaServerId;
  41 + String redisKey = SSRC_INFO_KEY + userSetting.getServerId() + "_" + mediaServerId;
38 42 List<String> ssrcList = new ArrayList<>();
39 43 for (int i = 1; i < MAX_STREAM_COUNT; i++) {
40 44 String ssrc = String.format("%s%04d", ssrcPrefix, i);
... ... @@ -77,7 +81,7 @@ public class SSRCFactory {
77 81 return;
78 82 }
79 83 String sn = ssrc.substring(1);
80   - String redisKey = SSRC_INFO_KEY + mediaServerId;
  84 + String redisKey = SSRC_INFO_KEY + userSetting.getServerId() + "_" + mediaServerId;
81 85 redisTemplate.opsForSet().add(redisKey, sn);
82 86 }
83 87  
... ... @@ -86,7 +90,7 @@ public class SSRCFactory {
86 90 */
87 91 private String getSN(String mediaServerId) {
88 92 String sn = null;
89   - String redisKey = SSRC_INFO_KEY + mediaServerId;
  93 + String redisKey = SSRC_INFO_KEY + userSetting.getServerId() + "_" + mediaServerId;
90 94 Long size = redisTemplate.opsForSet().size(redisKey);
91 95 if (size == null || size == 0) {
92 96 throw new RuntimeException("ssrc已经用完");
... ... @@ -113,20 +117,8 @@ public class SSRCFactory {
113 117 * @param mediaServerId 流媒体服务ID
114 118 */
115 119 public boolean hasMediaServerSSRC(String mediaServerId) {
116   - String redisKey = SSRC_INFO_KEY + mediaServerId;
  120 + String redisKey = SSRC_INFO_KEY + userSetting.getServerId() + "_" + mediaServerId;
117 121 return redisTemplate.opsForSet().members(redisKey) != null;
118 122 }
119 123  
120   - /**
121   - * 查询ssrc是否可用
122   - *
123   - * @param mediaServerId
124   - * @param ssrc
125   - * @return
126   - */
127   - public boolean checkSsrc(String mediaServerId, String ssrc) {
128   - String sn = ssrc.substring(1);
129   - String redisKey = SSRC_INFO_KEY + mediaServerId;
130   - return redisTemplate.opsForSet().isMember(redisKey, sn) != null;
131   - }
132 124 }
... ...
src/main/java/com/genersoft/iot/vmp/gb28181/transmit/callback/DeferredResultHolder.java
... ... @@ -39,11 +39,13 @@ public class DeferredResultHolder {
39 39  
40 40 public static final String CALLBACK_CMD_DOWNLOAD = "CALLBACK_DOWNLOAD";
41 41  
  42 + public static final String CALLBACK_CMD_PROXY = "CALLBACK_PROXY";
  43 +
42 44 public static final String CALLBACK_CMD_STOP = "CALLBACK_STOP";
43 45  
44 46 public static final String UPLOAD_FILE_CHANNEL = "UPLOAD_FILE_CHANNEL";
45 47  
46   - public static final String CALLBACK_CMD_MOBILEPOSITION = "CALLBACK_MOBILEPOSITION";
  48 + public static final String CALLBACK_CMD_MOBILE_POSITION = "CALLBACK_CMD_MOBILE_POSITION";
47 49  
48 50 public static final String CALLBACK_CMD_PRESETQUERY = "CALLBACK_PRESETQUERY";
49 51  
... ...
src/main/java/com/genersoft/iot/vmp/gb28181/transmit/cmd/SIPRequestHeaderPlarformProvider.java
... ... @@ -54,8 +54,8 @@ public class SIPRequestHeaderPlarformProvider {
54 54 parentPlatform.getServerIP() + ":" + parentPlatform.getServerPort());
55 55 //via
56 56 ArrayList<ViaHeader> viaHeaders = new ArrayList<ViaHeader>();
57   - ViaHeader viaHeader = SipFactory.getInstance().createHeaderFactory().createViaHeader(parentPlatform.getServerIP(),
58   - parentPlatform.getServerPort(), parentPlatform.getTransport(), SipUtils.getNewViaTag());
  57 + ViaHeader viaHeader = SipFactory.getInstance().createHeaderFactory().createViaHeader(parentPlatform.getDeviceIp(),
  58 + Integer.parseInt(parentPlatform.getDevicePort()), parentPlatform.getTransport(), SipUtils.getNewViaTag());
59 59 viaHeader.setRPort();
60 60 viaHeaders.add(viaHeader);
61 61 //from
... ...
src/main/java/com/genersoft/iot/vmp/gb28181/transmit/cmd/impl/SIPCommander.java
... ... @@ -472,7 +472,7 @@ public class SIPCommander implements ISIPCommander {
472 472 }
473 473 subscribe.removeSubscribe(hookSubscribe);
474 474 });
475   - Request request = headerProvider.createPlaybackInviteRequest(device, channelId, content.toString(), null, SipUtils.getNewFromTag(), null,sipSender.getNewCallIdHeader(sipLayer.getLocalIp(device.getLocalIp()),device.getTransport()), ssrcInfo.getSsrc());
  475 + Request request = headerProvider.createPlaybackInviteRequest(device, channelId, content.toString(), SipUtils.getNewViaTag(), SipUtils.getNewFromTag(), null,sipSender.getNewCallIdHeader(sipLayer.getLocalIp(device.getLocalIp()),device.getTransport()), ssrcInfo.getSsrc());
476 476  
477 477 sipSender.transmitRequest(sipLayer.getLocalIp(device.getLocalIp()), request, errorEvent, event -> {
478 478 ResponseEvent responseEvent = (ResponseEvent) event.event;
... ... @@ -588,17 +588,13 @@ public class SIPCommander implements ISIPCommander {
588 588 });
589 589 });
590 590  
591   - Request request = headerProvider.createPlaybackInviteRequest(device, channelId, content.toString(), null, SipUtils.getNewFromTag(), null,newCallIdHeader, ssrcInfo.getSsrc());
  591 + Request request = headerProvider.createPlaybackInviteRequest(device, channelId, content.toString(), SipUtils.getNewViaTag(), SipUtils.getNewFromTag(), null,newCallIdHeader, ssrcInfo.getSsrc());
592 592  
593 593 sipSender.transmitRequest(sipLayer.getLocalIp(device.getLocalIp()), request, errorEvent, event -> {
594 594 ResponseEvent responseEvent = (ResponseEvent) event.event;
595 595 SIPResponse response = (SIPResponse) responseEvent.getResponse();
596 596 String contentString =new String(response.getRawContent());
597   - int ssrcIndex = contentString.indexOf("y=");
598   - String ssrc=ssrcInfo.getSsrc();
599   - if (ssrcIndex >= 0) {
600   - ssrc = contentString.substring(ssrcIndex + 2, ssrcIndex + 12);
601   - }
  597 + String ssrc = SipUtils.getSsrcFromSdp(contentString);
602 598 streamSession.put(device.getDeviceId(), channelId, response.getCallIdHeader().getCallId(), ssrcInfo.getStream(), ssrc, mediaServerItem.getId(), response, InviteSessionType.DOWNLOAD);
603 599 okEvent.response(event);
604 600 });
... ...
src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/InviteRequestProcessor.java
... ... @@ -241,18 +241,8 @@ public class InviteRequestProcessor extends SIPRequestProcessorParent implements
241 241 // 解析sdp消息, 使用jainsip 自带的sdp解析方式
242 242 String contentString = new String(request.getRawContent());
243 243  
244   - // jainSip不支持y=字段, 移除以解析。
245   - // 检查是否有y字段
246   - int ssrcIndex = contentString.indexOf("y=");
247   -
248   - SessionDescription sdp;
249   - if (ssrcIndex >= 0) {
250   - //ssrc规定长度为10个字节,不取余下长度以避免后续还有“f=”字段
251   - String substring = contentString.substring(0, ssrcIndex);
252   - sdp = SdpFactory.getInstance().createSessionDescription(substring);
253   - } else {
254   - sdp = SdpFactory.getInstance().createSessionDescription(contentString);
255   - }
  244 + Gb28181Sdp gb28181Sdp = SipUtils.parseSDP(contentString);
  245 + SessionDescription sdp = gb28181Sdp.getBaseSdb();
256 246 String sessionName = sdp.getSessionName().getValue();
257 247  
258 248 Long startTime = null;
... ... @@ -340,11 +330,11 @@ public class InviteRequestProcessor extends SIPRequestProcessorParent implements
340 330 }
341 331  
342 332 String ssrc;
343   - if (userSetting.getUseCustomSsrcForParentInvite() || ssrcIndex < 0) {
  333 + if (userSetting.getUseCustomSsrcForParentInvite() || gb28181Sdp.getSsrc() == null) {
344 334 // 上级平台点播时不使用上级平台指定的ssrc,使用自定义的ssrc,参考国标文档-点播外域设备媒体流SSRC处理方式
345 335 ssrc = "Play".equalsIgnoreCase(sessionName) ? ssrcFactory.getPlaySsrc(mediaServerItem.getId()) : ssrcFactory.getPlayBackSsrc(mediaServerItem.getId());
346 336 }else {
347   - ssrc = contentString.substring(ssrcIndex + 2, ssrcIndex + 12);
  337 + ssrc = gb28181Sdp.getSsrc();
348 338 }
349 339 String streamTypeStr = null;
350 340 if (mediaTransmissionTCP) {
... ... @@ -513,11 +503,11 @@ public class InviteRequestProcessor extends SIPRequestProcessorParent implements
513 503 } else if (gbStream != null) {
514 504  
515 505 String ssrc;
516   - if (userSetting.getUseCustomSsrcForParentInvite() || ssrcIndex < 0) {
  506 + if (userSetting.getUseCustomSsrcForParentInvite() || gb28181Sdp.getSsrc() == null) {
517 507 // 上级平台点播时不使用上级平台指定的ssrc,使用自定义的ssrc,参考国标文档-点播外域设备媒体流SSRC处理方式
518 508 ssrc = "Play".equalsIgnoreCase(sessionName) ? ssrcFactory.getPlaySsrc(mediaServerItem.getId()) : ssrcFactory.getPlayBackSsrc(mediaServerItem.getId());
519 509 }else {
520   - ssrc = contentString.substring(ssrcIndex + 2, ssrcIndex + 12);
  510 + ssrc = gb28181Sdp.getSsrc();
521 511 }
522 512  
523 513 if("push".equals(gbStream.getStreamType())) {
... ... @@ -891,20 +881,11 @@ public class InviteRequestProcessor extends SIPRequestProcessorParent implements
891 881 }
892 882 String contentString = new String(request.getRawContent());
893 883 // jainSip不支持y=字段, 移除移除以解析。
894   - String substring = contentString;
895 884 String ssrc = "0000000404";
896   - int ssrcIndex = contentString.indexOf("y=");
897   - if (ssrcIndex > 0) {
898   - substring = contentString.substring(0, ssrcIndex);
899   - ssrc = contentString.substring(ssrcIndex + 2, ssrcIndex + 12);
900   - }
901   - ssrcIndex = substring.indexOf("f=");
902   - if (ssrcIndex > 0) {
903   - substring = contentString.substring(0, ssrcIndex);
904   - }
905   - SessionDescription sdp = null;
  885 +
906 886 try {
907   - sdp = SdpFactory.getInstance().createSessionDescription(substring);
  887 + Gb28181Sdp gb28181Sdp = SipUtils.parseSDP(contentString);
  888 + SessionDescription sdp = gb28181Sdp.getBaseSdb();
908 889 // 获取支持的格式
909 890 Vector mediaDescriptions = sdp.getMediaDescriptions(true);
910 891 // 查看是否支持PS 负载96
... ...
src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/NotifyRequestForCatalogProcessor.java
... ... @@ -175,6 +175,11 @@ public class NotifyRequestForCatalogProcessor extends SIPRequestProcessorParent
175 175 }
176 176 }else {
177 177 addChannelMap.put(channel.getChannelId(), channel);
  178 + if (userSetting.getDeviceStatusNotify()) {
  179 + // 发送redis消息
  180 + redisCatchStorage.sendChannelAddOrDelete(device.getDeviceId(), channel.getChannelId(), true);
  181 + }
  182 +
178 183 if (addChannelMap.keySet().size() > 300) {
179 184 executeSaveForAdd();
180 185 }
... ... @@ -185,6 +190,10 @@ public class NotifyRequestForCatalogProcessor extends SIPRequestProcessorParent
185 190 // 删除
186 191 logger.info("[收到删除通道通知] 来自设备: {}, 通道 {}", device.getDeviceId(), channel.getChannelId());
187 192 deleteChannelList.add(channel);
  193 + if (userSetting.getDeviceStatusNotify()) {
  194 + // 发送redis消息
  195 + redisCatchStorage.sendChannelAddOrDelete(device.getDeviceId(), channel.getChannelId(), false);
  196 + }
188 197 if (deleteChannelList.size() > 300) {
189 198 executeSaveForDelete();
190 199 }
... ... @@ -205,6 +214,10 @@ public class NotifyRequestForCatalogProcessor extends SIPRequestProcessorParent
205 214 if (addChannelMap.keySet().size() > 300) {
206 215 executeSaveForAdd();
207 216 }
  217 + if (userSetting.getDeviceStatusNotify()) {
  218 + // 发送redis消息
  219 + redisCatchStorage.sendChannelAddOrDelete(device.getDeviceId(), channel.getChannelId(), true);
  220 + }
208 221 }
209 222 break;
210 223 default:
... ...
src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/NotifyRequestProcessor.java
... ... @@ -192,7 +192,12 @@ public class NotifyRequestProcessor extends SIPRequestProcessorParent implements
192 192 mobilePosition.setDeviceId(device.getDeviceId());
193 193 mobilePosition.setChannelId(channelId);
194 194 String time = XmlUtil.getText(rootElement, "Time");
195   - mobilePosition.setTime(time);
  195 + if (ObjectUtils.isEmpty(time)){
  196 + mobilePosition.setTime(DateUtil.getNow());
  197 + }else {
  198 + mobilePosition.setTime(SipUtils.parseTime(time));
  199 + }
  200 +
196 201 mobilePosition.setLongitude(Double.parseDouble(XmlUtil.getText(rootElement, "Longitude")));
197 202 mobilePosition.setLatitude(Double.parseDouble(XmlUtil.getText(rootElement, "Latitude")));
198 203 if (NumericUtil.isDouble(XmlUtil.getText(rootElement, "Speed"))) {
... ... @@ -237,7 +242,7 @@ public class NotifyRequestProcessor extends SIPRequestProcessorParent implements
237 242  
238 243 // 发送redis消息。 通知位置信息的变化
239 244 JSONObject jsonObject = new JSONObject();
240   - jsonObject.put("time", time);
  245 + jsonObject.put("time", DateUtil.yyyy_MM_dd_HH_mm_ssToISO8601(mobilePosition.getTime()));
241 246 jsonObject.put("serial", deviceId);
242 247 jsonObject.put("code", channelId);
243 248 jsonObject.put("longitude", mobilePosition.getLongitude());
... ... @@ -339,7 +344,7 @@ public class NotifyRequestProcessor extends SIPRequestProcessorParent implements
339 344 storager.updateChannelPosition(deviceChannel);
340 345 // 发送redis消息。 通知位置信息的变化
341 346 JSONObject jsonObject = new JSONObject();
342   - jsonObject.put("time", mobilePosition.getTime());
  347 + jsonObject.put("time", DateUtil.yyyy_MM_dd_HH_mm_ssToISO8601(mobilePosition.getTime()));
343 348 jsonObject.put("serial", deviceChannel.getDeviceId());
344 349 jsonObject.put("code", deviceChannel.getChannelId());
345 350 jsonObject.put("longitude", mobilePosition.getLongitude());
... ...
src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/notify/cmd/AlarmNotifyMessageHandler.java
... ... @@ -164,7 +164,7 @@ public class AlarmNotifyMessageHandler extends SIPRequestProcessorParent impleme
164 164  
165 165 // 发送redis消息。 通知位置信息的变化
166 166 JSONObject jsonObject = new JSONObject();
167   - jsonObject.put("time", mobilePosition.getTime());
  167 + jsonObject.put("time", DateUtil.yyyy_MM_dd_HH_mm_ssToISO8601(mobilePosition.getTime()));
168 168 jsonObject.put("serial", deviceChannel.getDeviceId());
169 169 jsonObject.put("code", deviceChannel.getChannelId());
170 170 jsonObject.put("longitude", mobilePosition.getLongitude());
... ...
src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/notify/cmd/MobilePositionNotifyMessageHandler.java
... ... @@ -7,6 +7,7 @@ import com.genersoft.iot.vmp.gb28181.transmit.event.request.SIPRequestProcessorP
7 7 import com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.IMessageHandler;
8 8 import com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.notify.NotifyMessageHandler;
9 9 import com.genersoft.iot.vmp.gb28181.utils.NumericUtil;
  10 +import com.genersoft.iot.vmp.gb28181.utils.SipUtils;
10 11 import com.genersoft.iot.vmp.service.IDeviceChannelService;
11 12 import com.genersoft.iot.vmp.storager.IRedisCatchStorage;
12 13 import com.genersoft.iot.vmp.storager.IVideoManagerStorage;
... ... @@ -95,7 +96,12 @@ public class MobilePositionNotifyMessageHandler extends SIPRequestProcessorParen
95 96 }
96 97 mobilePosition.setDeviceId(sipMsgInfo.getDevice().getDeviceId());
97 98 mobilePosition.setChannelId(getText(rootElementAfterCharset, "DeviceID"));
98   - mobilePosition.setTime(getText(rootElementAfterCharset, "Time"));
  99 + String time = getText(rootElementAfterCharset, "Time");
  100 + if (ObjectUtils.isEmpty(time)){
  101 + mobilePosition.setTime(DateUtil.getNow());
  102 + }else {
  103 + mobilePosition.setTime(SipUtils.parseTime(time));
  104 + }
99 105 mobilePosition.setLongitude(Double.parseDouble(getText(rootElementAfterCharset, "Longitude")));
100 106 mobilePosition.setLatitude(Double.parseDouble(getText(rootElementAfterCharset, "Latitude")));
101 107 if (NumericUtil.isDouble(getText(rootElementAfterCharset, "Speed"))) {
... ... @@ -138,7 +144,7 @@ public class MobilePositionNotifyMessageHandler extends SIPRequestProcessorParen
138 144  
139 145 // 发送redis消息。 通知位置信息的变化
140 146 JSONObject jsonObject = new JSONObject();
141   - jsonObject.put("time", mobilePosition.getTime());
  147 + jsonObject.put("time", DateUtil.yyyy_MM_dd_HH_mm_ssToISO8601(mobilePosition.getTime()));
142 148 jsonObject.put("serial", deviceChannel.getDeviceId());
143 149 jsonObject.put("code", deviceChannel.getChannelId());
144 150 jsonObject.put("longitude", mobilePosition.getLongitude());
... ...
src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/response/cmd/MobilePositionResponseMessageHandler.java
... ... @@ -2,17 +2,21 @@ package com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.respon
2 2  
3 3 import com.alibaba.fastjson2.JSONObject;
4 4 import com.genersoft.iot.vmp.conf.UserSetting;
5   -import com.genersoft.iot.vmp.gb28181.bean.*;
  5 +import com.genersoft.iot.vmp.gb28181.bean.Device;
  6 +import com.genersoft.iot.vmp.gb28181.bean.DeviceChannel;
  7 +import com.genersoft.iot.vmp.gb28181.bean.MobilePosition;
  8 +import com.genersoft.iot.vmp.gb28181.bean.ParentPlatform;
  9 +import com.genersoft.iot.vmp.gb28181.transmit.callback.DeferredResultHolder;
  10 +import com.genersoft.iot.vmp.gb28181.transmit.callback.RequestMessage;
6 11 import com.genersoft.iot.vmp.gb28181.transmit.event.request.SIPRequestProcessorParent;
7 12 import com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.IMessageHandler;
8 13 import com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.response.ResponseMessageHandler;
9   -import com.genersoft.iot.vmp.gb28181.utils.Coordtransform;
10 14 import com.genersoft.iot.vmp.gb28181.utils.NumericUtil;
  15 +import com.genersoft.iot.vmp.gb28181.utils.SipUtils;
11 16 import com.genersoft.iot.vmp.service.IDeviceChannelService;
12 17 import com.genersoft.iot.vmp.storager.IRedisCatchStorage;
13 18 import com.genersoft.iot.vmp.storager.IVideoManagerStorage;
14 19 import com.genersoft.iot.vmp.utils.DateUtil;
15   -import com.genersoft.iot.vmp.utils.GpsUtil;
16 20 import gov.nist.javax.sip.message.SIPRequest;
17 21 import org.dom4j.DocumentException;
18 22 import org.dom4j.Element;
... ... @@ -56,6 +60,9 @@ public class MobilePositionResponseMessageHandler extends SIPRequestProcessorPar
56 60 @Autowired
57 61 private IDeviceChannelService deviceChannelService;
58 62  
  63 + @Autowired
  64 + private DeferredResultHolder resultHolder;
  65 +
59 66 @Override
60 67 public void afterPropertiesSet() throws Exception {
61 68 responseMessageHandler.addHandler(cmdType, this);
... ... @@ -83,7 +90,13 @@ public class MobilePositionResponseMessageHandler extends SIPRequestProcessorPar
83 90 }
84 91 mobilePosition.setDeviceId(device.getDeviceId());
85 92 mobilePosition.setChannelId(getText(rootElement, "DeviceID"));
86   - mobilePosition.setTime(getText(rootElement, "Time"));
  93 + //兼容ISO 8601格式时间
  94 + String time = getText(rootElement, "Time");
  95 + if (ObjectUtils.isEmpty(time)){
  96 + mobilePosition.setTime(DateUtil.getNow());
  97 + }else {
  98 + mobilePosition.setTime(SipUtils.parseTime(time));
  99 + }
87 100 mobilePosition.setLongitude(Double.parseDouble(getText(rootElement, "Longitude")));
88 101 mobilePosition.setLatitude(Double.parseDouble(getText(rootElement, "Latitude")));
89 102 if (NumericUtil.isDouble(getText(rootElement, "Speed"))) {
... ... @@ -121,11 +134,18 @@ public class MobilePositionResponseMessageHandler extends SIPRequestProcessorPar
121 134 if (userSetting.getSavePositionHistory()) {
122 135 storager.insertMobilePosition(mobilePosition);
123 136 }
  137 +
124 138 storager.updateChannelPosition(deviceChannel);
125 139  
  140 + String key = DeferredResultHolder.CALLBACK_CMD_MOBILE_POSITION + device.getDeviceId();
  141 + RequestMessage msg = new RequestMessage();
  142 + msg.setKey(key);
  143 + msg.setData(mobilePosition);
  144 + resultHolder.invokeAllResult(msg);
  145 +
126 146 // 发送redis消息。 通知位置信息的变化
127 147 JSONObject jsonObject = new JSONObject();
128   - jsonObject.put("time", mobilePosition.getTime());
  148 + jsonObject.put("time", DateUtil.yyyy_MM_dd_HH_mm_ssToISO8601(mobilePosition.getTime()));
129 149 jsonObject.put("serial", deviceChannel.getDeviceId());
130 150 jsonObject.put("code", deviceChannel.getChannelId());
131 151 jsonObject.put("longitude", mobilePosition.getLongitude());
... ...
src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/response/impl/InviteResponseProcessor.java
1 1 package com.genersoft.iot.vmp.gb28181.transmit.event.response.impl;
2 2  
3 3 import com.genersoft.iot.vmp.gb28181.SipLayer;
  4 +import com.genersoft.iot.vmp.gb28181.bean.Gb28181Sdp;
4 5 import com.genersoft.iot.vmp.gb28181.transmit.SIPProcessorObserver;
5 6 import com.genersoft.iot.vmp.gb28181.transmit.SIPSender;
6 7 import com.genersoft.iot.vmp.gb28181.transmit.cmd.SIPRequestHeaderProvider;
7 8 import com.genersoft.iot.vmp.gb28181.transmit.event.response.SIPResponseProcessorAbstract;
  9 +import com.genersoft.iot.vmp.gb28181.utils.SipUtils;
8 10 import gov.nist.javax.sip.ResponseEventExt;
9 11 import gov.nist.javax.sip.message.SIPResponse;
10 12 import org.slf4j.Logger;
... ... @@ -12,7 +14,6 @@ import org.slf4j.LoggerFactory;
12 14 import org.springframework.beans.factory.annotation.Autowired;
13 15 import org.springframework.stereotype.Component;
14 16  
15   -import javax.sdp.SdpFactory;
16 17 import javax.sdp.SdpParseException;
17 18 import javax.sdp.SessionDescription;
18 19 import javax.sip.InvalidArgumentException;
... ... @@ -79,18 +80,8 @@ public class InviteResponseProcessor extends SIPResponseProcessorAbstract {
79 80 ResponseEventExt event = (ResponseEventExt)evt;
80 81  
81 82 String contentString = new String(response.getRawContent());
82   - // jainSip不支持y=字段, 移除以解析。
83   - int ssrcIndex = contentString.indexOf("y=");
84   - // 检查是否有y字段
85   - SessionDescription sdp;
86   - if (ssrcIndex >= 0) {
87   - //ssrc规定长度为10字节,不取余下长度以避免后续还有“f=”字段
88   - String substring = contentString.substring(0, contentString.indexOf("y="));
89   - sdp = SdpFactory.getInstance().createSessionDescription(substring);
90   - } else {
91   - sdp = SdpFactory.getInstance().createSessionDescription(contentString);
92   - }
93   -
  83 + Gb28181Sdp gb28181Sdp = SipUtils.parseSDP(contentString);
  84 + SessionDescription sdp = gb28181Sdp.getBaseSdb();
94 85 SipURI requestUri = SipFactory.getInstance().createAddressFactory().createSipURI(sdp.getOrigin().getUsername(), event.getRemoteIpAddress() + ":" + event.getRemotePort());
95 86 Request reqAck = headerProvider.createAckRequest(response.getLocalAddress().getHostAddress(), requestUri, response);
96 87  
... ...
src/main/java/com/genersoft/iot/vmp/gb28181/utils/SipUtils.java
1 1 package com.genersoft.iot.vmp.gb28181.utils;
2 2  
3 3 import com.genersoft.iot.vmp.gb28181.bean.DeviceChannel;
  4 +import com.genersoft.iot.vmp.gb28181.bean.Gb28181Sdp;
4 5 import com.genersoft.iot.vmp.gb28181.bean.RemoteAddressInfo;
  6 +import com.genersoft.iot.vmp.utils.DateUtil;
5 7 import com.genersoft.iot.vmp.utils.GitUtil;
6 8 import gov.nist.javax.sip.address.AddressImpl;
7 9 import gov.nist.javax.sip.address.SipUri;
8 10 import gov.nist.javax.sip.header.Subject;
9 11 import gov.nist.javax.sip.message.SIPRequest;
  12 +import org.apache.commons.lang3.RandomStringUtils;
  13 +import org.slf4j.Logger;
  14 +import org.slf4j.LoggerFactory;
10 15 import org.springframework.util.ObjectUtils;
11 16  
  17 +import javax.sdp.SdpFactory;
  18 +import javax.sdp.SdpParseException;
  19 +import javax.sdp.SessionDescription;
12 20 import javax.sip.PeerUnavailableException;
13 21 import javax.sip.SipFactory;
14 22 import javax.sip.header.FromHeader;
... ... @@ -16,6 +24,8 @@ import javax.sip.header.Header;
16 24 import javax.sip.header.UserAgentHeader;
17 25 import javax.sip.message.Request;
18 26 import java.text.ParseException;
  27 +import java.time.LocalDateTime;
  28 +import java.time.format.DateTimeParseException;
19 29 import java.util.ArrayList;
20 30 import java.util.List;
21 31 import java.util.UUID;
... ... @@ -28,6 +38,8 @@ import java.util.UUID;
28 38 */
29 39 public class SipUtils {
30 40  
  41 + private final static Logger logger = LoggerFactory.getLogger(SipUtils.class);
  42 +
31 43 public static String getUserIdFromFromHeader(Request request) {
32 44 FromHeader fromHeader = (FromHeader)request.getHeader(FromHeader.NAME);
33 45 return getUserIdFromFromHeader(fromHeader);
... ... @@ -51,7 +63,7 @@ public class SipUtils {
51 63 }
52 64  
53 65 public static String getNewViaTag() {
54   - return "z9hG4bK" + System.currentTimeMillis();
  66 + return "z9hG4bK" + RandomStringUtils.randomNumeric(10);
55 67 }
56 68  
57 69 public static UserAgentHeader createUserAgentHeader(GitUtil gitUtil) throws PeerUnavailableException, ParseException {
... ... @@ -189,4 +201,67 @@ public class SipUtils {
189 201 }
190 202 return deviceChannel;
191 203 }
  204 +
  205 + public static Gb28181Sdp parseSDP(String sdpStr) throws SdpParseException {
  206 +
  207 + // jainSip不支持y= f=字段, 移除以解析。
  208 + int ssrcIndex = sdpStr.indexOf("y=");
  209 + int mediaDescriptionIndex = sdpStr.indexOf("f=");
  210 + // 检查是否有y字段
  211 + SessionDescription sdp;
  212 + String ssrc = null;
  213 + String mediaDescription = null;
  214 + if (mediaDescriptionIndex == 0 && ssrcIndex == 0) {
  215 + sdp = SdpFactory.getInstance().createSessionDescription(sdpStr);
  216 + }else {
  217 + String lines[] = sdpStr.split("\\r?\\n");
  218 + StringBuilder sdpBuffer = new StringBuilder();
  219 + for (String line : lines) {
  220 + if (line.trim().startsWith("y=")) {
  221 + ssrc = line.substring(2);
  222 + }else if (line.trim().startsWith("f=")) {
  223 + mediaDescription = line.substring(2);
  224 + }else {
  225 + sdpBuffer.append(line.trim()).append("\r\n");
  226 + }
  227 + }
  228 + sdp = SdpFactory.getInstance().createSessionDescription(sdpBuffer.toString());
  229 + }
  230 + return Gb28181Sdp.getInstance(sdp, ssrc, mediaDescription);
  231 + }
  232 +
  233 + public static String getSsrcFromSdp(String sdpStr) {
  234 +
  235 + // jainSip不支持y= f=字段, 移除以解析。
  236 + int ssrcIndex = sdpStr.indexOf("y=");
  237 + if (ssrcIndex == 0) {
  238 + return null;
  239 + }
  240 + String lines[] = sdpStr.split("\\r?\\n");
  241 + for (String line : lines) {
  242 + if (line.trim().startsWith("y=")) {
  243 + return line.substring(2);
  244 + }
  245 + }
  246 + return null;
  247 + }
  248 +
  249 + public static String parseTime(String timeStr) {
  250 + if (ObjectUtils.isEmpty(timeStr)){
  251 + return null;
  252 + }
  253 + System.out.println(timeStr);
  254 + LocalDateTime localDateTime;
  255 + try {
  256 + localDateTime = LocalDateTime.parse(timeStr);
  257 + }catch (DateTimeParseException e) {
  258 + try {
  259 + localDateTime = LocalDateTime.parse(timeStr, DateUtil.formatterISO8601);
  260 + }catch (DateTimeParseException e2) {
  261 + logger.error("[格式化时间] 无法格式化时间: {}", timeStr);
  262 + return null;
  263 + }
  264 + }
  265 + return localDateTime.format(DateUtil.formatterISO8601);
  266 + }
192 267 }
... ...
src/main/java/com/genersoft/iot/vmp/media/zlm/ZLMRTPServerFactory.java
... ... @@ -6,7 +6,9 @@ import com.alibaba.fastjson2.JSONObject;
6 6 import com.genersoft.iot.vmp.common.CommonCallback;
7 7 import com.genersoft.iot.vmp.conf.UserSetting;
8 8 import com.genersoft.iot.vmp.gb28181.bean.SendRtpItem;
9   -import com.genersoft.iot.vmp.media.zlm.dto.*;
  9 +import com.genersoft.iot.vmp.media.zlm.dto.HookSubscribeFactory;
  10 +import com.genersoft.iot.vmp.media.zlm.dto.HookSubscribeForRtpServerTimeout;
  11 +import com.genersoft.iot.vmp.media.zlm.dto.MediaServerItem;
10 12 import org.slf4j.Logger;
11 13 import org.slf4j.LoggerFactory;
12 14 import org.springframework.beans.factory.annotation.Autowired;
... ...
src/main/java/com/genersoft/iot/vmp/media/zlm/dto/StreamProxyItem.java
... ... @@ -20,28 +20,26 @@ public class StreamProxyItem extends GbStream {
20 20 @Schema(description = "拉流地址")
21 21 private String url;
22 22 @Schema(description = "拉流地址")
23   - private String src_url;
  23 + private String srcUrl;
24 24 @Schema(description = "目标地址")
25   - private String dst_url;
  25 + private String dstUrl;
26 26 @Schema(description = "超时时间")
27   - private int timeout_ms;
  27 + private int timeoutMs;
28 28 @Schema(description = "ffmpeg模板KEY")
29   - private String ffmpeg_cmd_key;
  29 + private String ffmpegCmdKey;
30 30 @Schema(description = "rtsp拉流时,拉流方式,0:tcp,1:udp,2:组播")
31   - private String rtp_type;
  31 + private String rtpType;
32 32 @Schema(description = "是否启用")
33 33 private boolean enable;
34 34 @Schema(description = "是否启用音频")
35   - private boolean enable_audio;
  35 + private boolean enableAudio;
36 36 @Schema(description = "是否启用MP4")
37   - private boolean enable_mp4;
  37 + private boolean enableMp4;
38 38 @Schema(description = "是否 无人观看时删除")
39   - private boolean enable_remove_none_reader;
  39 + private boolean enableRemoveNoneReader;
40 40  
41 41 @Schema(description = "是否 无人观看时自动停用")
42   - private boolean enable_disable_none_reader;
43   - @Schema(description = "创建时间")
44   - private String createTime;
  42 + private boolean enableDisableNoneReader;
45 43  
46 44 public String getType() {
47 45 return type;
... ... @@ -89,44 +87,44 @@ public class StreamProxyItem extends GbStream {
89 87 this.url = url;
90 88 }
91 89  
92   - public String getSrc_url() {
93   - return src_url;
  90 + public String getSrcUrl() {
  91 + return srcUrl;
94 92 }
95 93  
96   - public void setSrc_url(String src_url) {
97   - this.src_url = src_url;
  94 + public void setSrcUrl(String src_url) {
  95 + this.srcUrl = src_url;
98 96 }
99 97  
100   - public String getDst_url() {
101   - return dst_url;
  98 + public String getDstUrl() {
  99 + return dstUrl;
102 100 }
103 101  
104   - public void setDst_url(String dst_url) {
105   - this.dst_url = dst_url;
  102 + public void setDstUrl(String dst_url) {
  103 + this.dstUrl = dst_url;
106 104 }
107 105  
108   - public int getTimeout_ms() {
109   - return timeout_ms;
  106 + public int getTimeoutMs() {
  107 + return timeoutMs;
110 108 }
111 109  
112   - public void setTimeout_ms(int timeout_ms) {
113   - this.timeout_ms = timeout_ms;
  110 + public void setTimeoutMs(int timeout_ms) {
  111 + this.timeoutMs = timeout_ms;
114 112 }
115 113  
116   - public String getFfmpeg_cmd_key() {
117   - return ffmpeg_cmd_key;
  114 + public String getFfmpegCmdKey() {
  115 + return ffmpegCmdKey;
118 116 }
119 117  
120   - public void setFfmpeg_cmd_key(String ffmpeg_cmd_key) {
121   - this.ffmpeg_cmd_key = ffmpeg_cmd_key;
  118 + public void setFfmpegCmdKey(String ffmpeg_cmd_key) {
  119 + this.ffmpegCmdKey = ffmpeg_cmd_key;
122 120 }
123 121  
124   - public String getRtp_type() {
125   - return rtp_type;
  122 + public String getRtpType() {
  123 + return rtpType;
126 124 }
127 125  
128   - public void setRtp_type(String rtp_type) {
129   - this.rtp_type = rtp_type;
  126 + public void setRtpType(String rtp_type) {
  127 + this.rtpType = rtp_type;
130 128 }
131 129  
132 130 public boolean isEnable() {
... ... @@ -137,45 +135,37 @@ public class StreamProxyItem extends GbStream {
137 135 this.enable = enable;
138 136 }
139 137  
140   - public boolean isEnable_mp4() {
141   - return enable_mp4;
  138 + public boolean isEnableMp4() {
  139 + return enableMp4;
142 140 }
143 141  
144   - public void setEnable_mp4(boolean enable_mp4) {
145   - this.enable_mp4 = enable_mp4;
  142 + public void setEnableMp4(boolean enable_mp4) {
  143 + this.enableMp4 = enable_mp4;
146 144 }
147 145  
148   - @Override
149   - public String getCreateTime() {
150   - return createTime;
  146 + public boolean isEnableRemoveNoneReader() {
  147 + return enableRemoveNoneReader;
151 148 }
152 149  
153   - @Override
154   - public void setCreateTime(String createTime) {
155   - this.createTime = createTime;
  150 + public void setEnableRemoveNoneReader(boolean enable_remove_none_reader) {
  151 + this.enableRemoveNoneReader = enable_remove_none_reader;
156 152 }
157 153  
158   - public boolean isEnable_remove_none_reader() {
159   - return enable_remove_none_reader;
  154 + public boolean isEnableDisableNoneReader() {
  155 + return enableDisableNoneReader;
160 156 }
161 157  
162   - public void setEnable_remove_none_reader(boolean enable_remove_none_reader) {
163   - this.enable_remove_none_reader = enable_remove_none_reader;
  158 + public void setEnableDisableNoneReader(boolean enable_disable_none_reader) {
  159 + this.enableDisableNoneReader = enable_disable_none_reader;
164 160 }
165 161  
166   - public boolean isEnable_disable_none_reader() {
167   - return enable_disable_none_reader;
  162 + public boolean isEnableAudio() {
  163 + return enableAudio;
168 164 }
169 165  
170   - public void setEnable_disable_none_reader(boolean enable_disable_none_reader) {
171   - this.enable_disable_none_reader = enable_disable_none_reader;
  166 + public void setEnableAudio(boolean enable_audio) {
  167 + this.enableAudio = enable_audio;
172 168 }
173 169  
174   - public boolean isEnable_audio() {
175   - return enable_audio;
176   - }
177 170  
178   - public void setEnable_audio(boolean enable_audio) {
179   - this.enable_audio = enable_audio;
180   - }
181 171 }
... ...
src/main/java/com/genersoft/iot/vmp/service/IStreamProxyService.java
1 1 package com.genersoft.iot.vmp.service;
2 2  
3 3 import com.alibaba.fastjson2.JSONObject;
  4 +import com.genersoft.iot.vmp.common.GeneralCallback;
4 5 import com.genersoft.iot.vmp.common.StreamInfo;
5 6 import com.genersoft.iot.vmp.media.zlm.dto.MediaServerItem;
6 7 import com.genersoft.iot.vmp.media.zlm.dto.StreamProxyItem;
... ... @@ -13,7 +14,7 @@ public interface IStreamProxyService {
13 14 * 保存视频代理
14 15 * @param param
15 16 */
16   - StreamInfo save(StreamProxyItem param);
  17 + void save(StreamProxyItem param, GeneralCallback<StreamInfo> callback);
17 18  
18 19 /**
19 20 * 添加视频代理到zlm
... ...
src/main/java/com/genersoft/iot/vmp/service/impl/DeviceServiceImpl.java
... ... @@ -187,7 +187,7 @@ public class DeviceServiceImpl implements IDeviceService {
187 187  
188 188 @Override
189 189 public void offline(String deviceId, String reason) {
190   - logger.error("[设备离线],{}, device:{}", reason, deviceId);
  190 + logger.warn("[设备离线],{}, device:{}", reason, deviceId);
191 191 Device device = deviceMapper.getDeviceByDeviceId(deviceId);
192 192 if (device == null) {
193 193 return;
... ...
src/main/java/com/genersoft/iot/vmp/service/impl/MediaServerServiceImpl.java
... ... @@ -418,7 +418,7 @@ public class MediaServerServiceImpl implements IMediaServerService {
418 418 }
419 419 final String zlmKeepaliveKey = zlmKeepaliveKeyPrefix + serverItem.getId();
420 420 dynamicTask.stop(zlmKeepaliveKey);
421   - dynamicTask.startDelay(zlmKeepaliveKey, new KeepAliveTimeoutRunnable(serverItem), (Math.getExponent(serverItem.getHookAliveInterval()) + 5) * 1000);
  421 + dynamicTask.startDelay(zlmKeepaliveKey, new KeepAliveTimeoutRunnable(serverItem), (serverItem.getHookAliveInterval().intValue() + 5) * 1000);
422 422 publisher.zlmOnlineEventPublish(serverItem.getId());
423 423  
424 424 logger.info("[ZLM] 连接成功 {} - {}:{} ",
... ...
src/main/java/com/genersoft/iot/vmp/service/impl/StreamProxyServiceImpl.java
... ... @@ -2,12 +2,16 @@ package com.genersoft.iot.vmp.service.impl;
2 2  
3 3 import com.alibaba.fastjson2.JSONArray;
4 4 import com.alibaba.fastjson2.JSONObject;
  5 +import com.genersoft.iot.vmp.common.GeneralCallback;
5 6 import com.genersoft.iot.vmp.common.StreamInfo;
6 7 import com.genersoft.iot.vmp.conf.UserSetting;
7 8 import com.genersoft.iot.vmp.conf.exception.ControllerException;
8 9 import com.genersoft.iot.vmp.gb28181.event.EventPublisher;
9 10 import com.genersoft.iot.vmp.gb28181.event.subscribe.catalog.CatalogEvent;
10 11 import com.genersoft.iot.vmp.media.zlm.ZLMRESTfulUtils;
  12 +import com.genersoft.iot.vmp.media.zlm.ZlmHttpHookSubscribe;
  13 +import com.genersoft.iot.vmp.media.zlm.dto.HookSubscribeFactory;
  14 +import com.genersoft.iot.vmp.media.zlm.dto.HookSubscribeForStreamChange;
11 15 import com.genersoft.iot.vmp.media.zlm.dto.MediaServerItem;
12 16 import com.genersoft.iot.vmp.media.zlm.dto.StreamProxyItem;
13 17 import com.genersoft.iot.vmp.media.zlm.dto.hook.OnStreamChangedHookParam;
... ... @@ -86,6 +90,9 @@ public class StreamProxyServiceImpl implements IStreamProxyService {
86 90 private IMediaServerService mediaServerService;
87 91  
88 92 @Autowired
  93 + private ZlmHttpHookSubscribe hookSubscribe;
  94 +
  95 + @Autowired
89 96 DataSourceTransactionManager dataSourceTransactionManager;
90 97  
91 98 @Autowired
... ... @@ -93,7 +100,7 @@ public class StreamProxyServiceImpl implements IStreamProxyService {
93 100  
94 101  
95 102 @Override
96   - public StreamInfo save(StreamProxyItem param) {
  103 + public void save(StreamProxyItem param, GeneralCallback<StreamInfo> callback) {
97 104 MediaServerItem mediaInfo;
98 105 if (ObjectUtils.isEmpty(param.getMediaServerId()) || "auto".equals(param.getMediaServerId())){
99 106 mediaInfo = mediaServerService.getMediaServerForMinimumLoad(null);
... ... @@ -104,10 +111,43 @@ public class StreamProxyServiceImpl implements IStreamProxyService {
104 111 logger.warn("保存代理未找到在线的ZLM...");
105 112 throw new ControllerException(ErrorCode.ERROR100.getCode(), "保存代理未找到在线的ZLM");
106 113 }
107   - String dstUrl = String.format("rtmp://%s:%s/%s/%s", "127.0.0.1", mediaInfo.getRtmpPort(), param.getApp(),
108   - param.getStream() );
109   - param.setDst_url(dstUrl);
110   - StringBuffer resultMsg = new StringBuffer();
  114 + String dstUrl;
  115 + if ("ffmpeg".equalsIgnoreCase(param.getType())) {
  116 + JSONObject jsonObject = zlmresTfulUtils.getMediaServerConfig(mediaInfo);
  117 + if (jsonObject.getInteger("code") != 0) {
  118 + throw new ControllerException(ErrorCode.ERROR100.getCode(), "获取流媒体配置失败");
  119 + }
  120 + JSONArray dataArray = jsonObject.getJSONArray("data");
  121 + JSONObject mediaServerConfig = dataArray.getJSONObject(0);
  122 + String ffmpegCmd = mediaServerConfig.getString(param.getFfmpegCmdKey());
  123 + String schema = getSchemaFromFFmpegCmd(ffmpegCmd);
  124 + if (schema == null) {
  125 + throw new ControllerException(ErrorCode.ERROR100.getCode(), "ffmpeg拉流代理无法从ffmpeg cmd中获取到输出格式");
  126 + }
  127 + int port;
  128 + String schemaForUri;
  129 + if (schema.equalsIgnoreCase("rtsp")) {
  130 + port = mediaInfo.getRtspPort();
  131 + schemaForUri = schema;
  132 + }else if (schema.equalsIgnoreCase("flv")) {
  133 + port = mediaInfo.getHttpPort();
  134 + schemaForUri = "http";
  135 + }else if (schema.equalsIgnoreCase("rtmp")) {
  136 + port = mediaInfo.getRtmpPort();
  137 + schemaForUri = schema;
  138 + }else {
  139 + port = mediaInfo.getRtmpPort();
  140 + schemaForUri = schema;
  141 + }
  142 +
  143 + dstUrl = String.format("%s://%s:%s/%s/%s", schemaForUri, "127.0.0.1", port, param.getApp(),
  144 + param.getStream());
  145 + }else {
  146 + dstUrl = String.format("rtmp://%s:%s/%s/%s", "127.0.0.1", mediaInfo.getRtmpPort(), param.getApp(),
  147 + param.getStream());
  148 + }
  149 + param.setDstUrl(dstUrl);
  150 + logger.info("[拉流代理] 输出地址为:{}", dstUrl);
111 151 param.setMediaServerId(mediaInfo.getId());
112 152 boolean saveResult;
113 153 // 更新
... ... @@ -117,29 +157,60 @@ public class StreamProxyServiceImpl implements IStreamProxyService {
117 157 saveResult = addStreamProxy(param);
118 158 }
119 159 if (!saveResult) {
120   - throw new ControllerException(ErrorCode.ERROR100.getCode(),"保存失败");
  160 + callback.run(ErrorCode.ERROR100.getCode(), "保存失败", null);
  161 + return;
121 162 }
122   - StreamInfo resultForStreamInfo = null;
123   - resultMsg.append("保存成功");
  163 +
  164 + HookSubscribeForStreamChange hookSubscribeForStreamChange = HookSubscribeFactory.on_stream_changed(param.getApp(), param.getStream(), true, "rtsp", mediaInfo.getId());
  165 + hookSubscribe.addSubscribe(hookSubscribeForStreamChange, (mediaServerItem, response) -> {
  166 + StreamInfo streamInfo = mediaService.getStreamInfoByAppAndStream(
  167 + mediaInfo, param.getApp(), param.getStream(), null, null);
  168 + callback.run(ErrorCode.SUCCESS.getCode(), ErrorCode.SUCCESS.getMsg(), streamInfo);
  169 + });
  170 +
124 171 if (param.isEnable()) {
125 172 JSONObject jsonObject = addStreamProxyToZlm(param);
126   - if (jsonObject == null || jsonObject.getInteger("code") != 0) {
127   - resultMsg.append(", 但是启用失败,请检查流地址是否可用");
  173 + if (jsonObject != null && jsonObject.getInteger("code") == 0) {
  174 + hookSubscribe.removeSubscribe(hookSubscribeForStreamChange);
  175 + StreamInfo streamInfo = mediaService.getStreamInfoByAppAndStream(
  176 + mediaInfo, param.getApp(), param.getStream(), null, null);
  177 + callback.run(ErrorCode.SUCCESS.getCode(), ErrorCode.SUCCESS.getMsg(), streamInfo);
  178 + }else {
128 179 param.setEnable(false);
129 180 // 直接移除
130   - if (param.isEnable_remove_none_reader()) {
  181 + if (param.isEnableRemoveNoneReader()) {
131 182 del(param.getApp(), param.getStream());
132 183 }else {
133 184 updateStreamProxy(param);
134 185 }
  186 + if (jsonObject == null){
  187 + callback.run(ErrorCode.ERROR100.getCode(), "记录已保存,启用失败", null);
  188 + return;
  189 + }else {
  190 + callback.run(ErrorCode.ERROR100.getCode(), jsonObject.getString("msg"), null);
  191 + return;
  192 + }
  193 + }
  194 + }
  195 + }
135 196  
136   - }else {
137   - resultForStreamInfo = mediaService.getStreamInfoByAppAndStream(
138   - mediaInfo, param.getApp(), param.getStream(), null, null);
  197 + private String getSchemaFromFFmpegCmd(String ffmpegCmd) {
  198 + ffmpegCmd = ffmpegCmd.replaceAll(" + ", " ");
  199 + String[] paramArray = ffmpegCmd.split(" ");
  200 + if (paramArray.length == 0) {
  201 + return null;
  202 + }
  203 + for (int i = 0; i < paramArray.length; i++) {
  204 + if (paramArray[i].equalsIgnoreCase("-f")) {
  205 + if (i + 1 < paramArray.length - 1) {
  206 + return paramArray[i+1];
  207 + }else {
  208 + return null;
  209 + }
139 210  
140 211 }
141 212 }
142   - return resultForStreamInfo;
  213 + return null;
143 214 }
144 215  
145 216 /**
... ... @@ -228,11 +299,11 @@ public class StreamProxyServiceImpl implements IStreamProxyService {
228 299 }
229 300 if ("default".equals(param.getType())){
230 301 result = zlmresTfulUtils.addStreamProxy(mediaServerItem, param.getApp(), param.getStream(), param.getUrl(),
231   - param.isEnable_audio(), param.isEnable_mp4(), param.getRtp_type());
  302 + param.isEnableAudio(), param.isEnableMp4(), param.getRtpType());
232 303 }else if ("ffmpeg".equals(param.getType())) {
233   - result = zlmresTfulUtils.addFFmpegSource(mediaServerItem, param.getSrc_url(), param.getDst_url(),
234   - param.getTimeout_ms() + "", param.isEnable_audio(), param.isEnable_mp4(),
235   - param.getFfmpeg_cmd_key());
  304 + result = zlmresTfulUtils.addFFmpegSource(mediaServerItem, param.getSrcUrl(), param.getDstUrl(),
  305 + param.getTimeoutMs() + "", param.isEnableAudio(), param.isEnableMp4(),
  306 + param.getFfmpegCmdKey());
236 307 }
237 308 return result;
238 309 }
... ... @@ -286,7 +357,7 @@ public class StreamProxyServiceImpl implements IStreamProxyService {
286 357 updateStreamProxy(streamProxy);
287 358 }else {
288 359 logger.info("启用代理失败: {}/{}->{}({})", app, stream, jsonObject.getString("msg"),
289   - streamProxy.getSrc_url() == null? streamProxy.getUrl():streamProxy.getSrc_url());
  360 + streamProxy.getSrcUrl() == null? streamProxy.getUrl():streamProxy.getSrcUrl());
290 361 }
291 362 }
292 363 return result;
... ...
src/main/java/com/genersoft/iot/vmp/service/impl/StreamPushServiceImpl.java
... ... @@ -183,6 +183,7 @@ public class StreamPushServiceImpl implements IStreamPushService {
183 183  
184 184 @Override
185 185 public boolean stop(String app, String streamId) {
  186 + logger.info("[推流 ] 停止流: {}/{}", app, streamId);
186 187 StreamPushItem streamPushItem = streamPushMapper.selectOne(app, streamId);
187 188 if (streamPushItem != null) {
188 189 gbStreamService.sendCatalogMsg(streamPushItem, CatalogEvent.DEL);
... ...
src/main/java/com/genersoft/iot/vmp/service/redisMsg/RedisCloseStreamMsgListener.java 0 → 100644
  1 +package com.genersoft.iot.vmp.service.redisMsg;
  2 +
  3 +import com.alibaba.fastjson2.JSON;
  4 +import com.alibaba.fastjson2.JSONObject;
  5 +import com.genersoft.iot.vmp.service.IStreamPushService;
  6 +import org.jetbrains.annotations.NotNull;
  7 +import org.slf4j.Logger;
  8 +import org.slf4j.LoggerFactory;
  9 +import org.springframework.beans.factory.annotation.Autowired;
  10 +import org.springframework.beans.factory.annotation.Qualifier;
  11 +import org.springframework.data.redis.connection.Message;
  12 +import org.springframework.data.redis.connection.MessageListener;
  13 +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
  14 +import org.springframework.stereotype.Component;
  15 +
  16 +import java.util.concurrent.ConcurrentLinkedQueue;
  17 +
  18 +/**
  19 + * 接收来自redis的关闭流更新通知
  20 + * @author lin
  21 + */
  22 +@Component
  23 +public class RedisCloseStreamMsgListener implements MessageListener {
  24 +
  25 + private final static Logger logger = LoggerFactory.getLogger(RedisCloseStreamMsgListener.class);
  26 +
  27 +
  28 + @Autowired
  29 + private IStreamPushService pushService;
  30 +
  31 + private ConcurrentLinkedQueue<Message> taskQueue = new ConcurrentLinkedQueue<>();
  32 +
  33 + @Qualifier("taskExecutor")
  34 + @Autowired
  35 + private ThreadPoolTaskExecutor taskExecutor;
  36 +
  37 + @Override
  38 + public void onMessage(@NotNull Message message, byte[] bytes) {
  39 + boolean isEmpty = taskQueue.isEmpty();
  40 + taskQueue.offer(message);
  41 + if (isEmpty) {
  42 + taskExecutor.execute(() -> {
  43 + while (!taskQueue.isEmpty()) {
  44 + Message msg = taskQueue.poll();
  45 + try {
  46 + JSONObject jsonObject = JSON.parseObject(msg.getBody());
  47 + String app = jsonObject.getString("app");
  48 + String stream = jsonObject.getString("stream");
  49 + pushService.stop(app, stream);
  50 +
  51 + }catch (Exception e) {
  52 + logger.warn("[REDIS的关闭推流通知] 发现未处理的异常, \r\n{}", JSON.toJSONString(message));
  53 + logger.error("[REDIS的关闭推流通知] 异常内容: ", e);
  54 + }
  55 + }
  56 + });
  57 + }
  58 + }
  59 +}
... ...
src/main/java/com/genersoft/iot/vmp/storager/IRedisCatchStorage.java
... ... @@ -202,4 +202,5 @@ public interface IRedisCatchStorage {
202 202 void removeAllDevice();
203 203  
204 204 void sendDeviceOrChannelStatus(String deviceId, String channelId, boolean online);
  205 + void sendChannelAddOrDelete(String deviceId, String channelId, boolean add);
205 206 }
... ...
src/main/java/com/genersoft/iot/vmp/storager/dao/ParentPlatformMapper.java
... ... @@ -96,6 +96,6 @@ public interface ParentPlatformMapper {
96 96  
97 97 @Select("select 'channel' as name, count(pgc.platform_id) count from wvp_platform_gb_channel pgc left join wvp_device_channel dc on dc.id = pgc.device_channel_id where pgc.platform_id=#{platform_id} and dc.channel_id =#{gbId} " +
98 98 "union " +
99   - "select 'stream' as name, count(pgs.platform_id) count from wvp_platform_gb_stream pgs left join wvp_gb_stream gs on pgs.gb_stream_id = gs.gb_stream_id where pgs.platform_id=#{platform_id} and gs.gb_id #{gbId}")
  99 + "select 'stream' as name, count(pgs.platform_id) count from wvp_platform_gb_stream pgs left join wvp_gb_stream gs on pgs.gb_stream_id = gs.gb_stream_id where pgs.platform_id=#{platform_id} and gs.gb_id =#{gbId}")
100 100 List<ChannelSourceInfo> getChannelSource(String platform_id, String gbId);
101 101 }
... ...
src/main/java/com/genersoft/iot/vmp/storager/dao/PlatformCatalogMapper.java
... ... @@ -12,7 +12,7 @@ import java.util.List;
12 12 @Repository
13 13 public interface PlatformCatalogMapper {
14 14  
15   - @Insert("INSERT INTO platform_catalog (id, name, platform_id, parent_id, civil_code, business_group_id) VALUES" +
  15 + @Insert("INSERT INTO wvp_platform_catalog (id, name, platform_id, parent_id, civil_code, business_group_id) VALUES" +
16 16 "(#{id}, #{name}, #{platformId}, #{parentId}, #{civilCode}, #{businessGroupId})")
17 17 int add(PlatformCatalog platformCatalog);
18 18  
... ... @@ -32,7 +32,7 @@ public interface PlatformCatalogMapper {
32 32 PlatformCatalog select(String id);
33 33  
34 34 @Update(value = {" <script>" +
35   - "UPDATE platform_catalog " +
  35 + "UPDATE wvp_platform_catalog " +
36 36 "SET name=#{name}" +
37 37 "WHERE id=#{id}"+
38 38 "</script>"})
... ... @@ -41,11 +41,11 @@ public interface PlatformCatalogMapper {
41 41 @Select("SELECT *, (SELECT COUNT(1) from wvp_platform_catalog where parent_id = pc.id) as children_count from wvp_platform_catalog pc WHERE pc.platform_id=#{platformId}")
42 42 List<PlatformCatalog> selectByPlatForm(String platformId);
43 43  
44   - @Select("SELECT pc.* FROM platform_catalog pc WHERE pc.id = (SELECT pp.catalog_id from wvp_platform pp WHERE pp.server_gb_id=#{platformId})")
  44 + @Select("SELECT pc.* FROM wvp_platform_catalog pc WHERE pc.id = (SELECT pp.catalog_id from wvp_platform pp WHERE pp.server_gb_id=#{platformId})")
45 45 PlatformCatalog selectDefaultByPlatFormId(String platformId);
46 46  
47 47  
48   - @Select("SELECT pc.* FROM platform_catalog pc WHERE pc.id = #{id}")
  48 + @Select("SELECT pc.* FROM wvp_platform_catalog pc WHERE pc.id = #{id}")
49 49 PlatformCatalog selectParentCatalog(String id);
50 50  
51 51 @Select("SELECT pc.id as channel_id, pc.name, pc.civil_code, pc.business_group_id,'1' as parental, pc.parent_id " +
... ...
src/main/java/com/genersoft/iot/vmp/storager/dao/StreamProxyMapper.java
... ... @@ -13,9 +13,9 @@ public interface StreamProxyMapper {
13 13  
14 14 @Insert("INSERT INTO wvp_stream_proxy (type, name, app, stream,media_server_id, url, src_url, dst_url, " +
15 15 "timeout_ms, ffmpeg_cmd_key, rtp_type, enable_audio, enable_mp4, enable, status, enable_remove_none_reader, enable_disable_none_reader, create_time) VALUES" +
16   - "(#{type}, #{name}, #{app}, #{stream}, #{mediaServerId}, #{url}, #{src_url}, #{dst_url}, " +
17   - "#{timeout_ms}, #{ffmpeg_cmd_key}, #{rtp_type}, #{enable_audio}, #{enable_mp4}, #{enable}, #{status}, " +
18   - "#{enable_remove_none_reader}, #{enable_disable_none_reader}, #{createTime} )")
  16 + "(#{type}, #{name}, #{app}, #{stream}, #{mediaServerId}, #{url}, #{srcUrl}, #{dstUrl}, " +
  17 + "#{timeoutMs}, #{ffmpegCmdKey}, #{rtpType}, #{enableAudio}, #{enableMp4}, #{enable}, #{status}, " +
  18 + "#{enableRemoveNoneReader}, #{enableDisableNoneReader}, #{createTime} )")
19 19 int add(StreamProxyItem streamProxyDto);
20 20  
21 21 @Update("UPDATE wvp_stream_proxy " +
... ... @@ -25,17 +25,17 @@ public interface StreamProxyMapper {
25 25 "stream=#{stream}," +
26 26 "url=#{url}, " +
27 27 "media_server_id=#{mediaServerId}, " +
28   - "src_url=#{src_url}," +
29   - "dst_url=#{dst_url}, " +
30   - "timeout_ms=#{timeout_ms}, " +
31   - "ffmpeg_cmd_key=#{ffmpeg_cmd_key}, " +
32   - "rtp_type=#{rtp_type}, " +
33   - "enable_audio=#{enable_audio}, " +
  28 + "src_url=#{srcUrl}," +
  29 + "dst_url=#{dstUrl}, " +
  30 + "timeout_ms=#{timeoutMs}, " +
  31 + "ffmpeg_cmd_key=#{ffmpegCmdKey}, " +
  32 + "rtp_type=#{rtpType}, " +
  33 + "enable_audio=#{enableAudio}, " +
34 34 "enable=#{enable}, " +
35 35 "status=#{status}, " +
36   - "enable_remove_none_reader=#{enable_remove_none_reader}, " +
37   - "enable_disable_none_reader=#{enable_disable_none_reader}, " +
38   - "enable_mp4=#{enable_mp4} " +
  36 + "enable_remove_none_reader=#{enableRemoveNoneReader}, " +
  37 + "enable_disable_none_reader=#{enableDisableNoneReader}, " +
  38 + "enable_mp4=#{enableMp4} " +
39 39 "WHERE app=#{app} AND stream=#{stream}")
40 40 int update(StreamProxyItem streamProxyDto);
41 41  
... ...
src/main/java/com/genersoft/iot/vmp/storager/impl/RedisCatchStorageImpl.java
... ... @@ -596,18 +596,29 @@ public class RedisCatchStorageImpl implements IRedisCatchStorage {
596 596 @Override
597 597 public void sendDeviceOrChannelStatus(String deviceId, String channelId, boolean online) {
598 598 String key = VideoManagerConstants.VM_MSG_SUBSCRIBE_DEVICE_STATUS;
599   - if (channelId == null) {
600   - logger.info("[redis通知] 推送设备状态, {}-{}", deviceId, online);
601   - }else {
602   - logger.info("[redis通知] 推送通道状态, {}/{}-{}", deviceId, channelId, online);
  599 + StringBuilder msg = new StringBuilder();
  600 + msg.append(deviceId);
  601 + if (channelId != null) {
  602 + msg.append(":").append(channelId);
603 603 }
  604 + msg.append(" ").append(online? "ON":"OFF");
  605 + logger.info("[redis通知] 推送状态-> {} ", msg);
  606 + // 使用 RedisTemplate<Object, Object> 发送字符串消息会导致发送的消息多带了双引号
  607 + stringRedisTemplate.convertAndSend(key, msg.toString());
  608 + }
  609 +
  610 + @Override
  611 + public void sendChannelAddOrDelete(String deviceId, String channelId, boolean add) {
  612 + String key = VideoManagerConstants.VM_MSG_SUBSCRIBE_DEVICE_STATUS;
  613 +
604 614  
605 615 StringBuilder msg = new StringBuilder();
606 616 msg.append(deviceId);
607 617 if (channelId != null) {
608 618 msg.append(":").append(channelId);
609 619 }
610   - msg.append(" ").append(online? "ON":"OFF");
  620 + msg.append(" ").append(add? "ADD":"DELETE");
  621 + logger.info("[redis通知] 推送通道-> {}", msg);
611 622 // 使用 RedisTemplate<Object, Object> 发送字符串消息会导致发送的消息多带了双引号
612 623 stringRedisTemplate.convertAndSend(key, msg.toString());
613 624 }
... ...
src/main/java/com/genersoft/iot/vmp/vmanager/bean/ErrorCode.java
... ... @@ -6,7 +6,7 @@ package com.genersoft.iot.vmp.vmanager.bean;
6 6 public enum ErrorCode {
7 7 SUCCESS(0, "成功"),
8 8 ERROR100(100, "失败"),
9   - ERROR400(400, "参数不全或者错误"),
  9 + ERROR400(400, "参数或方法错误"),
10 10 ERROR404(404, "资源未找到"),
11 11 ERROR403(403, "无权限操作"),
12 12 ERROR401(401, "请登录后重新请求"),
... ...
src/main/java/com/genersoft/iot/vmp/vmanager/bean/SnapPath.java 0 → 100644
  1 +package com.genersoft.iot.vmp.vmanager.bean;
  2 +
  3 +import io.swagger.v3.oas.annotations.media.Schema;
  4 +
  5 +@Schema(description = "截图地址信息")
  6 +public class SnapPath {
  7 +
  8 + @Schema(description = "相对地址")
  9 + private String path;
  10 +
  11 + @Schema(description = "绝对地址")
  12 + private String absoluteFilePath;
  13 +
  14 + @Schema(description = "请求地址")
  15 + private String url;
  16 +
  17 +
  18 + public static SnapPath getInstance(String path, String absoluteFilePath, String url) {
  19 + SnapPath snapPath = new SnapPath();
  20 + snapPath.setPath(path);
  21 + snapPath.setAbsoluteFilePath(absoluteFilePath);
  22 + snapPath.setUrl(url);
  23 + return snapPath;
  24 + }
  25 +
  26 +
  27 + public String getPath() {
  28 + return path;
  29 + }
  30 +
  31 + public void setPath(String path) {
  32 + this.path = path;
  33 + }
  34 +
  35 + public String getAbsoluteFilePath() {
  36 + return absoluteFilePath;
  37 + }
  38 +
  39 + public void setAbsoluteFilePath(String absoluteFilePath) {
  40 + this.absoluteFilePath = absoluteFilePath;
  41 + }
  42 +
  43 + public String getUrl() {
  44 + return url;
  45 + }
  46 +
  47 + public void setUrl(String url) {
  48 + this.url = url;
  49 + }
  50 +}
... ...
src/main/java/com/genersoft/iot/vmp/vmanager/gb28181/MobilePosition/MobilePositionController.java
... ... @@ -102,7 +102,7 @@ public class MobilePositionController {
102 102 public DeferredResult<MobilePosition> realTimePosition(@PathVariable String deviceId) {
103 103 Device device = storager.queryVideoDevice(deviceId);
104 104 String uuid = UUID.randomUUID().toString();
105   - String key = DeferredResultHolder.CALLBACK_CMD_MOBILEPOSITION + deviceId;
  105 + String key = DeferredResultHolder.CALLBACK_CMD_MOBILE_POSITION + deviceId;
106 106 try {
107 107 cmder.mobilePostitionQuery(device, event -> {
108 108 RequestMessage msg = new RequestMessage();
... ...
src/main/java/com/genersoft/iot/vmp/vmanager/gb28181/device/DeviceQuery.java
... ... @@ -466,10 +466,12 @@ public class DeviceQuery {
466 466 @Operation(summary = "请求截图")
467 467 @Parameter(name = "deviceId", description = "设备国标编号", required = true)
468 468 @Parameter(name = "channelId", description = "通道国标编号", required = true)
469   - public void getSnap(HttpServletResponse resp, @PathVariable String deviceId, @PathVariable String channelId) {
  469 + @Parameter(name = "mark", description = "标识", required = false)
  470 + public void getSnap(HttpServletResponse resp, @PathVariable String deviceId, @PathVariable String channelId, @RequestParam(required = false) String mark) {
470 471  
471 472 try {
472   - final InputStream in = Files.newInputStream(new File("snap" + File.separator + deviceId + "_" + channelId + ".jpg").toPath());
  473 +
  474 + final InputStream in = Files.newInputStream(new File("snap" + File.separator + deviceId + "_" + channelId + (mark == null? ".jpg": ("_" + mark + ".jpg"))).toPath());
473 475 resp.setContentType(MediaType.IMAGE_PNG_VALUE);
474 476 IOUtils.copy(in, resp.getOutputStream());
475 477 } catch (IOException e) {
... ...
src/main/java/com/genersoft/iot/vmp/vmanager/streamProxy/StreamProxyController.java
1 1 package com.genersoft.iot.vmp.vmanager.streamProxy;
2 2  
3 3 import com.alibaba.fastjson2.JSONObject;
  4 +import com.genersoft.iot.vmp.common.StreamInfo;
  5 +import com.genersoft.iot.vmp.conf.UserSetting;
4 6 import com.genersoft.iot.vmp.conf.exception.ControllerException;
  7 +import com.genersoft.iot.vmp.gb28181.transmit.callback.DeferredResultHolder;
  8 +import com.genersoft.iot.vmp.gb28181.transmit.callback.RequestMessage;
5 9 import com.genersoft.iot.vmp.media.zlm.dto.MediaServerItem;
6 10 import com.genersoft.iot.vmp.media.zlm.dto.StreamProxyItem;
7 11 import com.genersoft.iot.vmp.service.IMediaServerService;
8 12 import com.genersoft.iot.vmp.service.IStreamProxyService;
9 13 import com.genersoft.iot.vmp.vmanager.bean.ErrorCode;
10 14 import com.genersoft.iot.vmp.vmanager.bean.StreamContent;
  15 +import com.genersoft.iot.vmp.vmanager.bean.WVPResult;
11 16 import com.github.pagehelper.PageInfo;
12 17 import io.swagger.v3.oas.annotations.Operation;
13 18 import io.swagger.v3.oas.annotations.Parameter;
... ... @@ -18,6 +23,9 @@ import org.springframework.beans.factory.annotation.Autowired;
18 23 import org.springframework.stereotype.Controller;
19 24 import org.springframework.util.ObjectUtils;
20 25 import org.springframework.web.bind.annotation.*;
  26 +import org.springframework.web.context.request.async.DeferredResult;
  27 +
  28 +import java.util.UUID;
21 29  
22 30 @SuppressWarnings("rawtypes")
23 31 /**
... ... @@ -37,6 +45,12 @@ public class StreamProxyController {
37 45 @Autowired
38 46 private IStreamProxyService streamProxyService;
39 47  
  48 + @Autowired
  49 + private DeferredResultHolder resultHolder;
  50 +
  51 + @Autowired
  52 + private UserSetting userSetting;
  53 +
40 54  
41 55 @Operation(summary = "分页查询流代理")
42 56 @Parameter(name = "page", description = "当前页")
... ... @@ -58,7 +72,7 @@ public class StreamProxyController {
58 72 })
59 73 @PostMapping(value = "/save")
60 74 @ResponseBody
61   - public StreamContent save(@RequestBody StreamProxyItem param){
  75 + public DeferredResult<Object> save(@RequestBody StreamProxyItem param){
62 76 logger.info("添加代理: " + JSONObject.toJSONString(param));
63 77 if (ObjectUtils.isEmpty(param.getMediaServerId())) {
64 78 param.setMediaServerId("auto");
... ... @@ -69,7 +83,33 @@ public class StreamProxyController {
69 83 if (ObjectUtils.isEmpty(param.getGbId())) {
70 84 param.setGbId(null);
71 85 }
72   - return new StreamContent(streamProxyService.save(param));
  86 +
  87 + RequestMessage requestMessage = new RequestMessage();
  88 + String key = DeferredResultHolder.CALLBACK_CMD_PROXY + param.getApp() + param.getStream();
  89 + requestMessage.setKey(key);
  90 + String uuid = UUID.randomUUID().toString();
  91 + requestMessage.setId(uuid);
  92 + DeferredResult<Object> result = new DeferredResult<>(userSetting.getPlayTimeout().longValue());
  93 + // 录像查询以channelId作为deviceId查询
  94 + resultHolder.put(key, uuid, result);
  95 + result.onTimeout(()->{
  96 + WVPResult<StreamInfo> wvpResult = new WVPResult<>();
  97 + wvpResult.setCode(ErrorCode.ERROR100.getCode());
  98 + wvpResult.setMsg("超时");
  99 + requestMessage.setData(wvpResult);
  100 + resultHolder.invokeAllResult(requestMessage);
  101 + });
  102 +
  103 + streamProxyService.save(param, (code, msg, streamInfo) -> {
  104 + logger.info("[拉流代理] {}", code == ErrorCode.SUCCESS.getCode()? "成功":"失败: " + msg);
  105 + if (code == ErrorCode.SUCCESS.getCode()) {
  106 + requestMessage.setData(new StreamContent(streamInfo));
  107 + }else {
  108 + requestMessage.setData(WVPResult.fail(code, msg));
  109 + }
  110 + resultHolder.invokeAllResult(requestMessage);
  111 + });
  112 + return result;
73 113 }
74 114  
75 115 @GetMapping(value = "/ffmpeg_cmd/list")
... ...
src/main/resources/all-application.yml
... ... @@ -43,10 +43,6 @@ spring:
43 43 idle-timeout: 300000 # 允许连接在连接池中空闲的最长时间(以毫秒为单位)
44 44 max-lifetime: 1200000 # 是池中连接关闭后的最长生命周期(以毫秒为单位)
45 45  
46   -# 修改为数据库字段下划线分隔直接对应java驼峰命名
47   -mybatis:
48   - configuration:
49   - map-underscore-to-camel-case: true
50 46  
51 47 # 修改分页插件为 postgresql, 数据库类型为mysql不需要
52 48 #pagehelper:
... ...
web_src/src/components/StreamProxyList.vue
... ... @@ -22,8 +22,8 @@
22 22 {{scope.row.url}}
23 23 </el-tag>
24 24 <el-tag size="medium" v-if="scope.row.type != 'default'">
25   - <i class="cpoy-btn el-icon-document-copy" title="点击拷贝" v-clipboard="scope.row.src_url" @success="$message({type:'success', message:'成功拷贝到粘贴板'})"></i>
26   - {{scope.row.src_url}}
  25 + <i class="cpoy-btn el-icon-document-copy" title="点击拷贝" v-clipboard="scope.row.srcUrl" @success="$message({type:'success', message:'成功拷贝到粘贴板'})"></i>
  26 + {{scope.row.srcUrl}}
27 27 </el-tag>
28 28 </div>
29 29 </template>
... ... @@ -58,25 +58,25 @@
58 58 <el-table-column label="音频" min-width="120" >
59 59 <template slot-scope="scope">
60 60 <div slot="reference" class="name-wrapper">
61   - <el-tag size="medium" v-if="scope.row.enable_audio">已启用</el-tag>
62   - <el-tag size="medium" type="info" v-if="!scope.row.enable_audio">未启用</el-tag>
  61 + <el-tag size="medium" v-if="scope.row.enableAudio">已启用</el-tag>
  62 + <el-tag size="medium" type="info" v-if="!scope.row.enableAudio">未启用</el-tag>
63 63 </div>
64 64 </template>
65 65 </el-table-column>
66 66 <el-table-column label="录制" min-width="120" >
67 67 <template slot-scope="scope">
68 68 <div slot="reference" class="name-wrapper">
69   - <el-tag size="medium" v-if="scope.row.enable_mp4">已启用</el-tag>
70   - <el-tag size="medium" type="info" v-if="!scope.row.enable_mp4">未启用</el-tag>
  69 + <el-tag size="medium" v-if="scope.row.enableMp4">已启用</el-tag>
  70 + <el-tag size="medium" type="info" v-if="!scope.row.enableMp4">未启用</el-tag>
71 71 </div>
72 72 </template>
73 73 </el-table-column>
74 74 <el-table-column label="无人观看" min-width="160" >
75 75 <template slot-scope="scope">
76 76 <div slot="reference" class="name-wrapper">
77   - <el-tag size="medium" v-if="scope.row.enable_remove_none_reader">移除</el-tag>
78   - <el-tag size="medium" v-if="scope.row.enable_disable_none_reader">停用</el-tag>
79   - <el-tag size="medium" type="info" v-if="!scope.row.enable_remove_none_reader && !scope.row.enable_disable_none_reader">不做处理</el-tag>
  77 + <el-tag size="medium" v-if="scope.row.enableRemoveNoneReader">移除</el-tag>
  78 + <el-tag size="medium" v-if="scope.row.enableDisableNoneReader">停用</el-tag>
  79 + <el-tag size="medium" type="info" v-if="!scope.row.enableRemoveNoneReader && !scope.row.enableDisableNoneReader">不做处理</el-tag>
80 80 </div>
81 81 </template>
82 82 </el-table-column>
... ... @@ -197,7 +197,7 @@
197 197 this.$refs.onvifEdit.openDialog(res.data.data, (url)=>{
198 198 if (url != null) {
199 199 this.$refs.onvifEdit.close();
200   - this.$refs.streamProxyEdit.openDialog({type: "default", url: url, src_url: url}, this.initData())
  200 + this.$refs.streamProxyEdit.openDialog({type: "default", url: url, srcUrl: url}, this.initData())
201 201 }
202 202 })
203 203 }else {
... ... @@ -245,18 +245,25 @@
245 245 },
246 246 deleteStreamProxy: function(row){
247 247 let that = this;
248   - that.$axios({
249   - method:"delete",
250   - url:"/api/proxy/del",
251   - params:{
252   - app: row.app,
253   - stream: row.stream
254   - }
255   - }).then((res)=>{
256   - that.initData()
257   - }).catch(function (error) {
258   - console.log(error);
259   - });
  248 + this.$confirm('确定删除此代理吗?', '提示', {
  249 + confirmButtonText: '确定',
  250 + cancelButtonText: '取消',
  251 + type: 'warning'
  252 + }).then(() => {
  253 + that.$axios({
  254 + method:"delete",
  255 + url:"/api/proxy/del",
  256 + params:{
  257 + app: row.app,
  258 + stream: row.stream
  259 + }
  260 + }).then((res)=>{
  261 + that.initData()
  262 + }).catch(function (error) {
  263 + console.log(error);
  264 + });
  265 + }).catch(() => {
  266 + });
260 267 },
261 268 start: function(row){
262 269 this.stopUpdateList()
... ...
web_src/src/components/dialog/StreamProxyEdit.vue
... ... @@ -33,13 +33,13 @@
33 33 <el-form-item label="拉流地址" prop="url" v-if="proxyParam.type=='default'">
34 34 <el-input v-model="proxyParam.url" clearable></el-input>
35 35 </el-form-item>
36   - <el-form-item label="拉流地址" prop="src_url" v-if="proxyParam.type=='ffmpeg'">
37   - <el-input v-model="proxyParam.src_url" clearable></el-input>
  36 + <el-form-item label="拉流地址" prop="srcUrl" v-if="proxyParam.type=='ffmpeg'">
  37 + <el-input v-model="proxyParam.srcUrl" clearable></el-input>
38 38 </el-form-item>
39   - <el-form-item label="超时时间:毫秒" prop="timeout_ms" v-if="proxyParam.type=='ffmpeg'">
40   - <el-input v-model="proxyParam.timeout_ms" clearable></el-input>
  39 + <el-form-item label="超时时间:毫秒" prop="timeoutMs" v-if="proxyParam.type=='ffmpeg'">
  40 + <el-input v-model="proxyParam.timeoutMs" clearable></el-input>
41 41 </el-form-item>
42   - <el-form-item label="节点选择" prop="rtp_type">
  42 + <el-form-item label="节点选择" prop="rtpType">
43 43 <el-select
44 44 v-model="proxyParam.mediaServerId"
45 45 @change="mediaServerIdChange"
... ... @@ -54,10 +54,9 @@
54 54 </el-option>
55 55 </el-select>
56 56 </el-form-item>
57   - <el-form-item label="FFmpeg命令模板" prop="ffmpeg_cmd_key" v-if="proxyParam.type=='ffmpeg'">
58   -<!-- <el-input v-model="proxyParam.ffmpeg_cmd_key" clearable></el-input>-->
  57 + <el-form-item label="FFmpeg命令模板" prop="ffmpegCmdKey" v-if="proxyParam.type=='ffmpeg'">
59 58 <el-select
60   - v-model="proxyParam.ffmpeg_cmd_key"
  59 + v-model="proxyParam.ffmpegCmdKey"
61 60 style="width: 100%"
62 61 placeholder="请选择FFmpeg命令模板"
63 62 >
... ... @@ -72,9 +71,9 @@
72 71 <el-form-item label="国标编码" prop="gbId">
73 72 <el-input v-model="proxyParam.gbId" placeholder="设置国标编码可推送到国标" clearable></el-input>
74 73 </el-form-item>
75   - <el-form-item label="拉流方式" prop="rtp_type" v-if="proxyParam.type=='default'">
  74 + <el-form-item label="拉流方式" prop="rtpType" v-if="proxyParam.type=='default'">
76 75 <el-select
77   - v-model="proxyParam.rtp_type"
  76 + v-model="proxyParam.rtpType"
78 77 style="width: 100%"
79 78 placeholder="请选择拉流方式"
80 79 >
... ... @@ -83,10 +82,10 @@
83 82 <el-option label="组播" value="2"></el-option>
84 83 </el-select>
85 84 </el-form-item>
86   - <el-form-item label="无人观看" prop="rtp_type" >
  85 + <el-form-item label="无人观看" prop="rtpType" >
87 86 <el-select
88 87 @change="noneReaderHandler"
89   - v-model="proxyParam.none_reader"
  88 + v-model="proxyParam.noneReader"
90 89 style="width: 100%"
91 90 placeholder="请选择无人观看的处理方式"
92 91 >
... ... @@ -98,8 +97,8 @@
98 97 <el-form-item label="其他选项">
99 98 <div style="float: left;">
100 99 <el-checkbox label="启用" v-model="proxyParam.enable" ></el-checkbox>
101   - <el-checkbox label="开启音频" v-model="proxyParam.enable_audio" ></el-checkbox>
102   - <el-checkbox label="录制" v-model="proxyParam.enable_mp4" ></el-checkbox>
  100 + <el-checkbox label="开启音频" v-model="proxyParam.enableAudio" ></el-checkbox>
  101 + <el-checkbox label="录制" v-model="proxyParam.enableMp4" ></el-checkbox>
103 102 </div>
104 103  
105 104 </el-form-item>
... ... @@ -155,17 +154,17 @@ export default {
155 154 app: null,
156 155 stream: null,
157 156 url: "",
158   - src_url: null,
159   - timeout_ms: null,
160   - ffmpeg_cmd_key: null,
  157 + srcUrl: null,
  158 + timeoutMs: null,
  159 + ffmpegCmdKey: null,
161 160 gbId: null,
162   - rtp_type: null,
  161 + rtpType: null,
163 162 enable: true,
164   - enable_audio: true,
165   - enable_mp4: false,
166   - none_reader: null,
167   - enable_remove_none_reader: false,
168   - enable_disable_none_reader: false,
  163 + enableAudio: true,
  164 + enableMp4: false,
  165 + noneReader: null,
  166 + enableRemoveNoneReader: false,
  167 + enableDisableNoneReader: false,
169 168 platformGbId: null,
170 169 mediaServerId: null,
171 170 },
... ... @@ -177,9 +176,9 @@ export default {
177 176 app: [{ required: true, message: "请输入应用名", trigger: "blur" }],
178 177 stream: [{ required: true, message: "请输入流ID", trigger: "blur" }],
179 178 url: [{ required: true, message: "请输入要代理的流", trigger: "blur" }],
180   - src_url: [{ required: true, message: "请输入要代理的流", trigger: "blur" }],
181   - timeout_ms: [{ required: true, message: "请输入FFmpeg推流成功超时时间", trigger: "blur" }],
182   - ffmpeg_cmd_key: [{ required: false, message: "请输入FFmpeg命令参数模板(可选)", trigger: "blur" }],
  179 + srcUrl: [{ required: true, message: "请输入要代理的流", trigger: "blur" }],
  180 + timeoutMs: [{ required: true, message: "请输入FFmpeg推流成功超时时间", trigger: "blur" }],
  181 + ffmpegCmdKey: [{ required: false, message: "请输入FFmpeg命令参数模板(可选)", trigger: "blur" }],
183 182 },
184 183 };
185 184 },
... ... @@ -189,7 +188,7 @@ export default {
189 188 this.listChangeCallback = callback;
190 189 if (proxyParam != null) {
191 190 this.proxyParam = proxyParam;
192   - this.proxyParam.none_reader = null;
  191 + this.proxyParam.noneReader = null;
193 192 }
194 193  
195 194 let that = this;
... ... @@ -218,7 +217,7 @@ export default {
218 217 }
219 218 }).then(function (res) {
220 219 that.ffmpegCmdList = res.data.data;
221   - that.proxyParam.ffmpeg_cmd_key = Object.keys(res.data.data)[0];
  220 + that.proxyParam.ffmpegCmdKey = Object.keys(res.data.data)[0];
222 221 }).catch(function (error) {
223 222 console.log(error);
224 223 });
... ... @@ -275,15 +274,15 @@ export default {
275 274 }
276 275 },
277 276 noneReaderHandler: function() {
278   - if (this.proxyParam.none_reader === null || this.proxyParam.none_reader === "0") {
279   - this.proxyParam.enable_disable_none_reader = false;
280   - this.proxyParam.enable_remove_none_reader = false;
281   - }else if (this.proxyParam.none_reader === "1"){
282   - this.proxyParam.enable_disable_none_reader = true;
283   - this.proxyParam.enable_remove_none_reader = false;
284   - }else if (this.proxyParam.none_reader ==="2"){
285   - this.proxyParam.enable_disable_none_reader = false;
286   - this.proxyParam.enable_remove_none_reader = true;
  277 + if (this.proxyParam.noneReader === null || this.proxyParam.noneReader === "0") {
  278 + this.proxyParam.enableDisableNoneReader = false;
  279 + this.proxyParam.enableRemoveNoneReader = false;
  280 + }else if (this.proxyParam.noneReader === "1"){
  281 + this.proxyParam.enableDisableNoneReader = true;
  282 + this.proxyParam.enableRemoveNoneReader = false;
  283 + }else if (this.proxyParam.noneReader ==="2"){
  284 + this.proxyParam.enableDisableNoneReader = false;
  285 + this.proxyParam.enableRemoveNoneReader = true;
287 286 }
288 287 },
289 288 },
... ...
web_src/src/components/dialog/devicePlayer.vue
... ... @@ -14,7 +14,6 @@
14 14 <rtc-player v-if="activePlayer === 'webRTC'" ref="webRTC" :visible.sync="showVideoDialog" :videoUrl="videoUrl" :error="videoError" :message="videoError" height="100px" :hasAudio="hasAudio" fluent autoplay live ></rtc-player>
15 15 </el-tab-pane>
16 16 <el-tab-pane label="h265web">h265web敬请期待</el-tab-pane>
17   - <el-tab-pane label="wsPlayer">wsPlayer 敬请期待</el-tab-pane>
18 17 </el-tabs>
19 18 <jessibucaPlayer v-if="Object.keys(this.player).length == 1 && this.player.jessibuca" ref="jessibuca" :visible.sync="showVideoDialog" :videoUrl="videoUrl" :error="videoError" :message="videoError" height="100px" :hasAudio="hasAudio" fluent autoplay live ></jessibucaPlayer>
20 19 <rtc-player v-if="Object.keys(this.player).length == 1 && this.player.webRTC" ref="jessibuca" :visible.sync="showVideoDialog" :videoUrl="videoUrl" :error="videoError" :message="videoError" height="100px" :hasAudio="hasAudio" fluent autoplay live ></rtc-player>
... ... @@ -451,7 +450,15 @@ export default {
451 450 playFromStreamInfo: function (realHasAudio, streamInfo) {
452 451 this.showVideoDialog = true;
453 452 this.hasaudio = realHasAudio && this.hasaudio;
454   - this.$refs[this.activePlayer].play(this.getUrlByStreamInfo(streamInfo))
  453 + if (this.$refs[this.activePlayer]) {
  454 + this.$refs[this.activePlayer].play(this.getUrlByStreamInfo(streamInfo))
  455 + }else {
  456 + this.$nextTick(() => {
  457 + this.$refs[this.activePlayer].play(this.getUrlByStreamInfo(streamInfo))
  458 + });
  459 + }
  460 +
  461 +
455 462 },
456 463 close: function () {
457 464 console.log('关闭视频');
... ...