‘sxh’
2023-06-15 652489b47ef582b3f492039ab7fd58c558c1ba36
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
38个文件已修改
6个文件已添加
915 ■■■■ 已修改文件
README.md 3 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
doc/README.md 24 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
doc/_media/1372762149.jpg 补丁 | 查看 | 原始文档 | blame | 历史
doc/_media/903207146.jpg 补丁 | 查看 | 原始文档 | blame | 历史
pom.xml 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
sql/2.6.8升级2.6.9.sql 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/genersoft/iot/vmp/common/GeneralCallback.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/genersoft/iot/vmp/common/VideoManagerConstants.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/genersoft/iot/vmp/conf/redis/RedisMsgListenConfig.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/genersoft/iot/vmp/gb28181/bean/Gb28181Sdp.java 46 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/genersoft/iot/vmp/gb28181/session/SSRCFactory.java 24 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/genersoft/iot/vmp/gb28181/transmit/callback/DeferredResultHolder.java 4 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/genersoft/iot/vmp/gb28181/transmit/cmd/SIPRequestHeaderPlarformProvider.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/genersoft/iot/vmp/gb28181/transmit/cmd/impl/SIPCommander.java 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/InviteRequestProcessor.java 37 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/NotifyRequestForCatalogProcessor.java 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/NotifyRequestProcessor.java 11 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/notify/cmd/AlarmNotifyMessageHandler.java 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/notify/cmd/MobilePositionNotifyMessageHandler.java 10 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/message/response/cmd/MobilePositionResponseMessageHandler.java 30 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/response/impl/InviteResponseProcessor.java 17 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/genersoft/iot/vmp/gb28181/utils/SipUtils.java 77 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/genersoft/iot/vmp/media/zlm/ZLMRTPServerFactory.java 4 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/genersoft/iot/vmp/media/zlm/dto/StreamProxyItem.java 100 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/genersoft/iot/vmp/service/IStreamProxyService.java 3 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/genersoft/iot/vmp/service/impl/DeviceServiceImpl.java 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/genersoft/iot/vmp/service/impl/MediaServerServiceImpl.java 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/genersoft/iot/vmp/service/impl/StreamProxyServiceImpl.java 111 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/genersoft/iot/vmp/service/impl/StreamPushServiceImpl.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/genersoft/iot/vmp/service/redisMsg/RedisCloseStreamMsgListener.java 59 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/genersoft/iot/vmp/storager/IRedisCatchStorage.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/genersoft/iot/vmp/storager/dao/ParentPlatformMapper.java 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/genersoft/iot/vmp/storager/dao/PlatformCatalogMapper.java 8 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/genersoft/iot/vmp/storager/dao/StreamProxyMapper.java 24 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/genersoft/iot/vmp/storager/impl/RedisCatchStorageImpl.java 23 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/genersoft/iot/vmp/vmanager/bean/ErrorCode.java 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/genersoft/iot/vmp/vmanager/bean/SnapPath.java 50 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/genersoft/iot/vmp/vmanager/gb28181/MobilePosition/MobilePositionController.java 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/genersoft/iot/vmp/vmanager/gb28181/device/DeviceQuery.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/genersoft/iot/vmp/vmanager/streamProxy/StreamProxyController.java 44 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/all-application.yml 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
web_src/src/components/StreamProxyList.vue 51 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
web_src/src/components/dialog/StreamProxyEdit.vue 73 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
web_src/src/components/dialog/devicePlayer.vue 11 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
README.md
@@ -27,7 +27,7 @@
ZLM使用文档 [https://github.com/ZLMediaKit/ZLMediaKit](https://github.com/ZLMediaKit/ZLMediaKit)
> wvp文档由gitee提供服务,如果遇到打不开请多刷新几次。
# ç¤¾ç¾¤åœ°å€
# ä»˜è´¹ç¤¾ç¾¤
[![社群](doc/_media/shequ.png "shequ")](https://t.zsxq.com/0d8VAD3Dm)
> æ”¶è´¹æ˜¯ä¸ºäº†æä¾›æ›´å¥½çš„æœåŠ¡ï¼Œä¹Ÿæ˜¯å¯¹ä½œè€…æ›´å¤§çš„æ¿€åŠ±ã€‚åŠ å…¥æ˜Ÿçƒçš„ç”¨æˆ·ä¸‰å¤©åŽå¯ä»¥ç§ä¿¡æˆ‘ç•™ä¸‹å¾®ä¿¡å·ï¼Œæˆ‘ä¼šæ‹‰å¤§å®¶å…¥ç¾¤ã€‚åŠ å…¥ä¸‰å¤©å†…ä¸æ»¡æ„å¯ä»¥ç›´æŽ¥é€€æ¬¾ï¼Œå¤§å®¶ä¸éœ€è¦æœ‰é¡¾è™‘ï¼Œæ¥ç™½å«–ä¸‰å¤©ä¹Ÿä¸æ˜¯ä¸å¯ä»¥ã€‚
@@ -105,6 +105,7 @@
- [X] æ”¯æŒæ‰“包可执行jar和war
- [X] æ”¯æŒè·¨åŸŸè¯·æ±‚,支持前后端分离部署
- [X] æ”¯æŒMysql,Postgresql,金仓等数据库
- [X] æ”¯æŒOnvif(目前在onvif分支,需要安装onvif服务,服务请在知识星球获取)
# æŽˆæƒåè®®
本项目自有代码使用宽松的MIT协议,在保留版权信息的情况下可以自由应用于各自商用、非商业的项目。 ä½†æ˜¯æœ¬é¡¹ç›®ä¹Ÿé›¶ç¢Žçš„使用了一些其他的开源代码,在商用的情况下请自行替代或剔除; ç”±äºŽä½¿ç”¨æœ¬é¡¹ç›®è€Œäº§ç”Ÿçš„商业纠纷或侵权行为一概与本项目及开发者无关,请自行承担法律风险。 åœ¨ä½¿ç”¨æœ¬é¡¹ç›®ä»£ç æ—¶ï¼Œä¹Ÿåº”该在授权协议中同时表明本项目依赖的第三方库的协议
doc/README.md
@@ -14,7 +14,7 @@
- å®Œå…¨å¼€æºï¼Œä¸”使用MIT许可协议。保留版权的情况下可以用于商业项目。
- æ”¯æŒå¤šæµåª’体节点负载均衡。
# ç¤¾ç¾¤
# ä»˜è´¹ç¤¾ç¾¤
[![社群](_media/shequ.png "shequ")](https://t.zsxq.com/0d8VAD3Dm)
> æ”¶è´¹æ˜¯ä¸ºäº†æä¾›æ›´å¥½çš„æœåŠ¡ï¼Œä¹Ÿæ˜¯å¯¹ä½œè€…æ›´å¤§çš„æ¿€åŠ±ã€‚åŠ å…¥æ˜Ÿçƒçš„ç”¨æˆ·ä¸‰å¤©åŽå¯ä»¥ç§ä¿¡æˆ‘ç•™ä¸‹å¾®ä¿¡å·ï¼Œæˆ‘ä¼šæ‹‰å¤§å®¶å…¥ç¾¤ã€‚åŠ å…¥ä¸‰å¤©å†…ä¸æ»¡æ„å¯ä»¥ç›´æŽ¥é€€æ¬¾ï¼Œå¤§å®¶ä¸éœ€è¦æœ‰é¡¾è™‘ï¼Œæ¥ç™½å«–ä¸‰å¤©ä¹Ÿä¸æ˜¯ä¸å¯ä»¥ã€‚
@@ -62,16 +62,16 @@
- [X] æ³¨å†Œ
- [X] æ³¨é”€
- [X] å®žæ—¶è§†éŸ³é¢‘点播
- [ ] è®¾å¤‡æŽ§åˆ¶
  - [ ] äº‘台控制
- [X] è®¾å¤‡æŽ§åˆ¶
  - [X] äº‘台控制
  - [ ] è¿œç¨‹å¯åЍ
  - [ ] å½•像控制
  - [ ] æŠ¥è­¦å¸ƒé˜²/撤防
  - [ ] æŠ¥è­¦å¤ä½
  - [ ] å¼ºåˆ¶å…³é”®å¸§
  - [ ] æ‹‰æ¡†æ”¾å¤§
  - [ ] æ‹‰æ¡†ç¼©å°
  - [ ] çœ‹å®ˆä½æŽ§åˆ¶
  - [X] å½•像控制
  - [X] æŠ¥è­¦å¸ƒé˜²/撤防
  - [X] æŠ¥è­¦å¤ä½
  - [X] å¼ºåˆ¶å…³é”®å¸§
  - [X] æ‹‰æ¡†æ”¾å¤§
  - [X] æ‹‰æ¡†ç¼©å°
  - [X] çœ‹å®ˆä½æŽ§åˆ¶
  - [ ] è®¾å¤‡é…ç½®
- [ ] æŠ¥è­¦äº‹ä»¶é€šçŸ¥å’Œåˆ†å‘
- [X] è®¾å¤‡ç›®å½•订阅
@@ -79,7 +79,7 @@
  - [X] è®¾å¤‡ç›®å½•查询
  - [X] è®¾å¤‡çŠ¶æ€æŸ¥è¯¢
  - [ ] è®¾å¤‡é…ç½®æŸ¥è¯¢
  - [ ] è®¾å¤‡é¢„置位查询
  - [X] è®¾å¤‡é¢„置位查询
- [X] çŠ¶æ€ä¿¡æ¯æŠ¥é€
- [X] è®¾å¤‡è§†éŸ³é¢‘文件检索
- [X] åŽ†å²è§†éŸ³é¢‘çš„å›žæ”¾
@@ -87,7 +87,7 @@
  - [x] æš‚停
  - [x] è¿›/退
  - [x] åœæ­¢
- [ ] è§†éŸ³é¢‘文件下载
- [X] è§†éŸ³é¢‘文件下载
- [ ] ~~校时~~
- [X] è®¢é˜…和通知
  - [X] äº‹ä»¶è®¢é˜…
doc/_media/1372762149.jpg
doc/_media/903207146.jpg
pom.xml
@@ -11,7 +11,7 @@
    <groupId>com.genersoft</groupId>
    <artifactId>wvp-pro</artifactId>
    <version>2.6.8</version>
    <version>2.6.9</version>
    <name>web video platform</name>
    <description>国标28181视频平台</description>
    <packaging>${project.packaging}</packaging>
sql/2.6.8Éý¼¶2.6.9.sql
@@ -180,10 +180,6 @@
    change createTime create_time varchar(50) null;
alter table gb_stream
    add constraint gb_stream_pk
        primary key (gbStreamId);
alter table gb_stream
    change gbStreamId gb_stream_id int auto_increment;
alter table gb_stream
src/main/java/com/genersoft/iot/vmp/common/GeneralCallback.java
New file
@@ -0,0 +1,5 @@
package com.genersoft.iot.vmp.common;
public interface GeneralCallback<T>{
    void run(int code, String msg, T data);
}
src/main/java/com/genersoft/iot/vmp/common/VideoManagerConstants.java
@@ -107,6 +107,11 @@
    public static final String VM_MSG_STREAM_PUSH_RESPONSE = "VM_MSG_STREAM_PUSH_RESPONSE";
    /**
     * redis é€šçŸ¥å¹³å°å…³é—­æŽ¨æµ
     */
    public static final String VM_MSG_STREAM_PUSH_CLOSE = "VM_MSG_STREAM_PUSH_CLOSE";
    /**
     * redis æ¶ˆæ¯è¯·æ±‚所有的在线通道
     */
    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 @@
    @Autowired
    private RedisPushStreamResponseListener redisPushStreamResponseListener;
    @Autowired
    private RedisCloseStreamMsgListener redisCloseStreamMsgListener;
    /**
     * redis消息监听器容器 å¯ä»¥æ·»åŠ å¤šä¸ªç›‘å¬ä¸åŒè¯é¢˜çš„redis监听器,只需要把消息监听器和相应的消息订阅处理器绑定,该消息监听器
@@ -63,6 +66,7 @@
        container.addMessageListener(redisPushStreamStatusMsgListener, new PatternTopic(VideoManagerConstants.VM_MSG_PUSH_STREAM_STATUS_CHANGE));
        container.addMessageListener(redisPushStreamListMsgListener, new PatternTopic(VideoManagerConstants.VM_MSG_PUSH_STREAM_LIST_CHANGE));
        container.addMessageListener(redisPushStreamResponseListener, new PatternTopic(VideoManagerConstants.VM_MSG_STREAM_PUSH_RESPONSE));
        container.addMessageListener(redisCloseStreamMsgListener, new PatternTopic(VideoManagerConstants.VM_MSG_STREAM_PUSH_CLOSE));
        return container;
    }
}
src/main/java/com/genersoft/iot/vmp/gb28181/bean/Gb28181Sdp.java
New file
@@ -0,0 +1,46 @@
package com.genersoft.iot.vmp.gb28181.bean;
import javax.sdp.SessionDescription;
/**
 * 28181 çš„SDP解析器
 */
public class Gb28181Sdp  {
    private SessionDescription baseSdb;
    private String ssrc;
    private String mediaDescription;
    public static Gb28181Sdp getInstance(SessionDescription baseSdb, String ssrc, String mediaDescription) {
        Gb28181Sdp gb28181Sdp = new Gb28181Sdp();
        gb28181Sdp.setBaseSdb(baseSdb);
        gb28181Sdp.setSsrc(ssrc);
        gb28181Sdp.setMediaDescription(mediaDescription);
        return gb28181Sdp;
    }
    public SessionDescription getBaseSdb() {
        return baseSdb;
    }
    public void setBaseSdb(SessionDescription baseSdb) {
        this.baseSdb = baseSdb;
    }
    public String getSsrc() {
        return ssrc;
    }
    public void setSsrc(String ssrc) {
        this.ssrc = ssrc;
    }
    public String getMediaDescription() {
        return mediaDescription;
    }
    public void setMediaDescription(String mediaDescription) {
        this.mediaDescription = mediaDescription;
    }
}
src/main/java/com/genersoft/iot/vmp/gb28181/session/SSRCFactory.java
@@ -1,6 +1,7 @@
package com.genersoft.iot.vmp.gb28181.session;
import com.genersoft.iot.vmp.conf.SipConfig;
import com.genersoft.iot.vmp.conf.UserSetting;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
@@ -31,10 +32,13 @@
    @Autowired
    private SipConfig sipConfig;
    @Autowired
    private UserSetting userSetting;
    public void initMediaServerSSRC(String mediaServerId, Set<String> usedSet) {
        String ssrcPrefix = sipConfig.getDomain().substring(3, 8);
        String redisKey = SSRC_INFO_KEY + mediaServerId;
        String redisKey = SSRC_INFO_KEY + userSetting.getServerId() + "_" + mediaServerId;
        List<String> ssrcList = new ArrayList<>();
        for (int i = 1; i < MAX_STREAM_COUNT; i++) {
            String ssrc = String.format("%s%04d", ssrcPrefix, i);
@@ -77,7 +81,7 @@
            return;
        }
        String sn = ssrc.substring(1);
        String redisKey = SSRC_INFO_KEY + mediaServerId;
        String redisKey = SSRC_INFO_KEY + userSetting.getServerId() + "_" + mediaServerId;
        redisTemplate.opsForSet().add(redisKey, sn);
    }
@@ -86,7 +90,7 @@
     */
    private String getSN(String mediaServerId) {
        String sn = null;
        String redisKey = SSRC_INFO_KEY + mediaServerId;
        String redisKey = SSRC_INFO_KEY + userSetting.getServerId() + "_" + mediaServerId;
        Long size = redisTemplate.opsForSet().size(redisKey);
        if (size == null || size == 0) {
            throw new RuntimeException("ssrc已经用完");
@@ -113,20 +117,8 @@
     * @param mediaServerId æµåª’体服务ID
     */
    public boolean hasMediaServerSSRC(String mediaServerId) {
        String redisKey = SSRC_INFO_KEY + mediaServerId;
        String redisKey = SSRC_INFO_KEY + userSetting.getServerId() + "_" + mediaServerId;
        return redisTemplate.opsForSet().members(redisKey) != null;
    }
    /**
     * æŸ¥è¯¢ssrc是否可用
     *
     * @param mediaServerId
     * @param ssrc
     * @return
     */
    public boolean checkSsrc(String mediaServerId, String ssrc) {
        String sn = ssrc.substring(1);
        String redisKey = SSRC_INFO_KEY + mediaServerId;
        return redisTemplate.opsForSet().isMember(redisKey, sn) != null;
    }
}
src/main/java/com/genersoft/iot/vmp/gb28181/transmit/callback/DeferredResultHolder.java
@@ -39,11 +39,13 @@
    public static final String CALLBACK_CMD_DOWNLOAD = "CALLBACK_DOWNLOAD";
    public static final String CALLBACK_CMD_PROXY = "CALLBACK_PROXY";
    public static final String CALLBACK_CMD_STOP = "CALLBACK_STOP";
    public static final String UPLOAD_FILE_CHANNEL = "UPLOAD_FILE_CHANNEL";
    public static final String CALLBACK_CMD_MOBILEPOSITION = "CALLBACK_MOBILEPOSITION";
    public static final String CALLBACK_CMD_MOBILE_POSITION = "CALLBACK_CMD_MOBILE_POSITION";
    public static final String CALLBACK_CMD_PRESETQUERY = "CALLBACK_PRESETQUERY";
src/main/java/com/genersoft/iot/vmp/gb28181/transmit/cmd/SIPRequestHeaderPlarformProvider.java
@@ -54,8 +54,8 @@
                parentPlatform.getServerIP() + ":" + parentPlatform.getServerPort());
        //via
        ArrayList<ViaHeader> viaHeaders = new ArrayList<ViaHeader>();
        ViaHeader viaHeader = SipFactory.getInstance().createHeaderFactory().createViaHeader(parentPlatform.getServerIP(),
                parentPlatform.getServerPort(), parentPlatform.getTransport(), SipUtils.getNewViaTag());
        ViaHeader viaHeader = SipFactory.getInstance().createHeaderFactory().createViaHeader(parentPlatform.getDeviceIp(),
                Integer.parseInt(parentPlatform.getDevicePort()), parentPlatform.getTransport(), SipUtils.getNewViaTag());
        viaHeader.setRPort();
        viaHeaders.add(viaHeader);
        //from
src/main/java/com/genersoft/iot/vmp/gb28181/transmit/cmd/impl/SIPCommander.java
@@ -472,7 +472,7 @@
            }
            subscribe.removeSubscribe(hookSubscribe);
        });
        Request request = headerProvider.createPlaybackInviteRequest(device, channelId, content.toString(), null, SipUtils.getNewFromTag(), null,sipSender.getNewCallIdHeader(sipLayer.getLocalIp(device.getLocalIp()),device.getTransport()), ssrcInfo.getSsrc());
        Request request = headerProvider.createPlaybackInviteRequest(device, channelId, content.toString(), SipUtils.getNewViaTag(), SipUtils.getNewFromTag(), null,sipSender.getNewCallIdHeader(sipLayer.getLocalIp(device.getLocalIp()),device.getTransport()), ssrcInfo.getSsrc());
        sipSender.transmitRequest(sipLayer.getLocalIp(device.getLocalIp()), request, errorEvent, event -> {
            ResponseEvent responseEvent = (ResponseEvent) event.event;
@@ -588,17 +588,13 @@
                    });
        });
        Request request = headerProvider.createPlaybackInviteRequest(device, channelId, content.toString(), null, SipUtils.getNewFromTag(), null,newCallIdHeader, ssrcInfo.getSsrc());
        Request request = headerProvider.createPlaybackInviteRequest(device, channelId, content.toString(), SipUtils.getNewViaTag(), SipUtils.getNewFromTag(), null,newCallIdHeader, ssrcInfo.getSsrc());
        sipSender.transmitRequest(sipLayer.getLocalIp(device.getLocalIp()), request, errorEvent, event -> {
            ResponseEvent responseEvent = (ResponseEvent) event.event;
            SIPResponse response = (SIPResponse) responseEvent.getResponse();
            String contentString =new String(response.getRawContent());
            int ssrcIndex = contentString.indexOf("y=");
            String ssrc=ssrcInfo.getSsrc();
            if (ssrcIndex >= 0) {
                ssrc = contentString.substring(ssrcIndex + 2, ssrcIndex + 12);
            }
            String ssrc = SipUtils.getSsrcFromSdp(contentString);
            streamSession.put(device.getDeviceId(), channelId, response.getCallIdHeader().getCallId(), ssrcInfo.getStream(), ssrc, mediaServerItem.getId(), response, InviteSessionType.DOWNLOAD);
            okEvent.response(event);
        });
src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/InviteRequestProcessor.java
@@ -241,18 +241,8 @@
                // è§£æžsdp消息, ä½¿ç”¨jainsip è‡ªå¸¦çš„sdp解析方式
                String contentString = new String(request.getRawContent());
                // jainSip不支持y=字段, ç§»é™¤ä»¥è§£æžã€‚
                // æ£€æŸ¥æ˜¯å¦æœ‰y字段
                int ssrcIndex = contentString.indexOf("y=");
                SessionDescription sdp;
                if (ssrcIndex >= 0) {
                    //ssrc规定长度为10个字节,不取余下长度以避免后续还有“f=”字段
                    String substring = contentString.substring(0, ssrcIndex);
                    sdp = SdpFactory.getInstance().createSessionDescription(substring);
                } else {
                    sdp = SdpFactory.getInstance().createSessionDescription(contentString);
                }
                Gb28181Sdp gb28181Sdp = SipUtils.parseSDP(contentString);
                SessionDescription sdp = gb28181Sdp.getBaseSdb();
                String sessionName = sdp.getSessionName().getValue();
                Long startTime = null;
@@ -340,11 +330,11 @@
                    }
                    String ssrc;
                    if (userSetting.getUseCustomSsrcForParentInvite() || ssrcIndex < 0) {
                    if (userSetting.getUseCustomSsrcForParentInvite() || gb28181Sdp.getSsrc() == null) {
                        // ä¸Šçº§å¹³å°ç‚¹æ’­æ—¶ä¸ä½¿ç”¨ä¸Šçº§å¹³å°æŒ‡å®šçš„ssrc,使用自定义的ssrc,参考国标文档-点播外域设备媒体流SSRC处理方式
                        ssrc = "Play".equalsIgnoreCase(sessionName) ? ssrcFactory.getPlaySsrc(mediaServerItem.getId()) : ssrcFactory.getPlayBackSsrc(mediaServerItem.getId());
                    }else {
                        ssrc = contentString.substring(ssrcIndex + 2, ssrcIndex + 12);
                        ssrc = gb28181Sdp.getSsrc();
                    }
                    String streamTypeStr = null;
                    if (mediaTransmissionTCP) {
@@ -513,11 +503,11 @@
                } else if (gbStream != null) {
                    String ssrc;
                    if (userSetting.getUseCustomSsrcForParentInvite() || ssrcIndex < 0) {
                    if (userSetting.getUseCustomSsrcForParentInvite() || gb28181Sdp.getSsrc() == null) {
                        // ä¸Šçº§å¹³å°ç‚¹æ’­æ—¶ä¸ä½¿ç”¨ä¸Šçº§å¹³å°æŒ‡å®šçš„ssrc,使用自定义的ssrc,参考国标文档-点播外域设备媒体流SSRC处理方式
                        ssrc = "Play".equalsIgnoreCase(sessionName) ? ssrcFactory.getPlaySsrc(mediaServerItem.getId()) : ssrcFactory.getPlayBackSsrc(mediaServerItem.getId());
                    }else {
                        ssrc = contentString.substring(ssrcIndex + 2, ssrcIndex + 12);
                        ssrc = gb28181Sdp.getSsrc();
                    }
                    if("push".equals(gbStream.getStreamType())) {
@@ -891,20 +881,11 @@
            }
            String contentString = new String(request.getRawContent());
            // jainSip不支持y=字段, ç§»é™¤ç§»é™¤ä»¥è§£æžã€‚
            String substring = contentString;
            String ssrc = "0000000404";
            int ssrcIndex = contentString.indexOf("y=");
            if (ssrcIndex > 0) {
                substring = contentString.substring(0, ssrcIndex);
                ssrc = contentString.substring(ssrcIndex + 2, ssrcIndex + 12);
            }
            ssrcIndex = substring.indexOf("f=");
            if (ssrcIndex > 0) {
                substring = contentString.substring(0, ssrcIndex);
            }
            SessionDescription sdp = null;
            try {
                sdp = SdpFactory.getInstance().createSessionDescription(substring);
                Gb28181Sdp gb28181Sdp = SipUtils.parseSDP(contentString);
                SessionDescription sdp = gb28181Sdp.getBaseSdb();
                //  èŽ·å–æ”¯æŒçš„æ ¼å¼
                Vector mediaDescriptions = sdp.getMediaDescriptions(true);
                // æŸ¥çœ‹æ˜¯å¦æ”¯æŒPS è´Ÿè½½96
src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/NotifyRequestForCatalogProcessor.java
@@ -175,6 +175,11 @@
                                }
                            }else {
                                addChannelMap.put(channel.getChannelId(), channel);
                                if (userSetting.getDeviceStatusNotify()) {
                                    // å‘送redis消息
                                    redisCatchStorage.sendChannelAddOrDelete(device.getDeviceId(), channel.getChannelId(), true);
                                }
                                if (addChannelMap.keySet().size() > 300) {
                                    executeSaveForAdd();
                                }
@@ -185,6 +190,10 @@
                            // åˆ é™¤
                            logger.info("[收到删除通道通知] æ¥è‡ªè®¾å¤‡: {}, é€šé“ {}", device.getDeviceId(), channel.getChannelId());
                            deleteChannelList.add(channel);
                            if (userSetting.getDeviceStatusNotify()) {
                                // å‘送redis消息
                                redisCatchStorage.sendChannelAddOrDelete(device.getDeviceId(), channel.getChannelId(), false);
                            }
                            if (deleteChannelList.size() > 300) {
                                executeSaveForDelete();
                            }
@@ -205,6 +214,10 @@
                                if (addChannelMap.keySet().size() > 300) {
                                    executeSaveForAdd();
                                }
                                if (userSetting.getDeviceStatusNotify()) {
                                    // å‘送redis消息
                                    redisCatchStorage.sendChannelAddOrDelete(device.getDeviceId(), channel.getChannelId(), true);
                                }
                            }
                            break;
                        default:
src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/NotifyRequestProcessor.java
@@ -192,7 +192,12 @@
            mobilePosition.setDeviceId(device.getDeviceId());
            mobilePosition.setChannelId(channelId);
            String time = XmlUtil.getText(rootElement, "Time");
            mobilePosition.setTime(time);
            if (ObjectUtils.isEmpty(time)){
                mobilePosition.setTime(DateUtil.getNow());
            }else {
                mobilePosition.setTime(SipUtils.parseTime(time));
            }
            mobilePosition.setLongitude(Double.parseDouble(XmlUtil.getText(rootElement, "Longitude")));
            mobilePosition.setLatitude(Double.parseDouble(XmlUtil.getText(rootElement, "Latitude")));
            if (NumericUtil.isDouble(XmlUtil.getText(rootElement, "Speed"))) {
@@ -237,7 +242,7 @@
            // å‘送redis消息。 é€šçŸ¥ä½ç½®ä¿¡æ¯çš„变化
            JSONObject jsonObject = new JSONObject();
            jsonObject.put("time", time);
            jsonObject.put("time", DateUtil.yyyy_MM_dd_HH_mm_ssToISO8601(mobilePosition.getTime()));
            jsonObject.put("serial", deviceId);
            jsonObject.put("code", channelId);
            jsonObject.put("longitude", mobilePosition.getLongitude());
@@ -339,7 +344,7 @@
                storager.updateChannelPosition(deviceChannel);
                // å‘送redis消息。 é€šçŸ¥ä½ç½®ä¿¡æ¯çš„变化
                JSONObject jsonObject = new JSONObject();
                jsonObject.put("time", mobilePosition.getTime());
                jsonObject.put("time", DateUtil.yyyy_MM_dd_HH_mm_ssToISO8601(mobilePosition.getTime()));
                jsonObject.put("serial", deviceChannel.getDeviceId());
                jsonObject.put("code", deviceChannel.getChannelId());
                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 @@
                                // å‘送redis消息。 é€šçŸ¥ä½ç½®ä¿¡æ¯çš„变化
                                JSONObject jsonObject = new JSONObject();
                                jsonObject.put("time", mobilePosition.getTime());
                                jsonObject.put("time", DateUtil.yyyy_MM_dd_HH_mm_ssToISO8601(mobilePosition.getTime()));
                                jsonObject.put("serial", deviceChannel.getDeviceId());
                                jsonObject.put("code", deviceChannel.getChannelId());
                                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.impl.message.IMessageHandler;
import com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.notify.NotifyMessageHandler;
import com.genersoft.iot.vmp.gb28181.utils.NumericUtil;
import com.genersoft.iot.vmp.gb28181.utils.SipUtils;
import com.genersoft.iot.vmp.service.IDeviceChannelService;
import com.genersoft.iot.vmp.storager.IRedisCatchStorage;
import com.genersoft.iot.vmp.storager.IVideoManagerStorage;
@@ -95,7 +96,12 @@
                        }
                        mobilePosition.setDeviceId(sipMsgInfo.getDevice().getDeviceId());
                        mobilePosition.setChannelId(getText(rootElementAfterCharset, "DeviceID"));
                        mobilePosition.setTime(getText(rootElementAfterCharset, "Time"));
                        String time = getText(rootElementAfterCharset, "Time");
                        if (ObjectUtils.isEmpty(time)){
                            mobilePosition.setTime(DateUtil.getNow());
                        }else {
                            mobilePosition.setTime(SipUtils.parseTime(time));
                        }
                        mobilePosition.setLongitude(Double.parseDouble(getText(rootElementAfterCharset, "Longitude")));
                        mobilePosition.setLatitude(Double.parseDouble(getText(rootElementAfterCharset, "Latitude")));
                        if (NumericUtil.isDouble(getText(rootElementAfterCharset, "Speed"))) {
@@ -138,7 +144,7 @@
                        // å‘送redis消息。 é€šçŸ¥ä½ç½®ä¿¡æ¯çš„变化
                        JSONObject jsonObject = new JSONObject();
                        jsonObject.put("time", mobilePosition.getTime());
                        jsonObject.put("time", DateUtil.yyyy_MM_dd_HH_mm_ssToISO8601(mobilePosition.getTime()));
                        jsonObject.put("serial", deviceChannel.getDeviceId());
                        jsonObject.put("code", deviceChannel.getChannelId());
                        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 @@
import com.alibaba.fastjson2.JSONObject;
import com.genersoft.iot.vmp.conf.UserSetting;
import com.genersoft.iot.vmp.gb28181.bean.*;
import com.genersoft.iot.vmp.gb28181.bean.Device;
import com.genersoft.iot.vmp.gb28181.bean.DeviceChannel;
import com.genersoft.iot.vmp.gb28181.bean.MobilePosition;
import com.genersoft.iot.vmp.gb28181.bean.ParentPlatform;
import com.genersoft.iot.vmp.gb28181.transmit.callback.DeferredResultHolder;
import com.genersoft.iot.vmp.gb28181.transmit.callback.RequestMessage;
import com.genersoft.iot.vmp.gb28181.transmit.event.request.SIPRequestProcessorParent;
import com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.IMessageHandler;
import com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.response.ResponseMessageHandler;
import com.genersoft.iot.vmp.gb28181.utils.Coordtransform;
import com.genersoft.iot.vmp.gb28181.utils.NumericUtil;
import com.genersoft.iot.vmp.gb28181.utils.SipUtils;
import com.genersoft.iot.vmp.service.IDeviceChannelService;
import com.genersoft.iot.vmp.storager.IRedisCatchStorage;
import com.genersoft.iot.vmp.storager.IVideoManagerStorage;
import com.genersoft.iot.vmp.utils.DateUtil;
import com.genersoft.iot.vmp.utils.GpsUtil;
import gov.nist.javax.sip.message.SIPRequest;
import org.dom4j.DocumentException;
import org.dom4j.Element;
@@ -56,6 +60,9 @@
    @Autowired
    private IDeviceChannelService deviceChannelService;
    @Autowired
    private DeferredResultHolder resultHolder;
    @Override
    public void afterPropertiesSet() throws Exception {
        responseMessageHandler.addHandler(cmdType, this);
@@ -83,7 +90,13 @@
            }
            mobilePosition.setDeviceId(device.getDeviceId());
            mobilePosition.setChannelId(getText(rootElement, "DeviceID"));
            mobilePosition.setTime(getText(rootElement, "Time"));
            //兼容ISO 8601格式时间
            String time = getText(rootElement, "Time");
            if (ObjectUtils.isEmpty(time)){
                mobilePosition.setTime(DateUtil.getNow());
            }else {
                mobilePosition.setTime(SipUtils.parseTime(time));
            }
            mobilePosition.setLongitude(Double.parseDouble(getText(rootElement, "Longitude")));
            mobilePosition.setLatitude(Double.parseDouble(getText(rootElement, "Latitude")));
            if (NumericUtil.isDouble(getText(rootElement, "Speed"))) {
@@ -121,11 +134,18 @@
            if (userSetting.getSavePositionHistory()) {
                storager.insertMobilePosition(mobilePosition);
            }
            storager.updateChannelPosition(deviceChannel);
            String key = DeferredResultHolder.CALLBACK_CMD_MOBILE_POSITION + device.getDeviceId();
            RequestMessage msg = new RequestMessage();
            msg.setKey(key);
            msg.setData(mobilePosition);
            resultHolder.invokeAllResult(msg);
            // å‘送redis消息。 é€šçŸ¥ä½ç½®ä¿¡æ¯çš„变化
            JSONObject jsonObject = new JSONObject();
            jsonObject.put("time", mobilePosition.getTime());
            jsonObject.put("time", DateUtil.yyyy_MM_dd_HH_mm_ssToISO8601(mobilePosition.getTime()));
            jsonObject.put("serial", deviceChannel.getDeviceId());
            jsonObject.put("code", deviceChannel.getChannelId());
            jsonObject.put("longitude", mobilePosition.getLongitude());
src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/response/impl/InviteResponseProcessor.java
@@ -1,10 +1,12 @@
package com.genersoft.iot.vmp.gb28181.transmit.event.response.impl;
import com.genersoft.iot.vmp.gb28181.SipLayer;
import com.genersoft.iot.vmp.gb28181.bean.Gb28181Sdp;
import com.genersoft.iot.vmp.gb28181.transmit.SIPProcessorObserver;
import com.genersoft.iot.vmp.gb28181.transmit.SIPSender;
import com.genersoft.iot.vmp.gb28181.transmit.cmd.SIPRequestHeaderProvider;
import com.genersoft.iot.vmp.gb28181.transmit.event.response.SIPResponseProcessorAbstract;
import com.genersoft.iot.vmp.gb28181.utils.SipUtils;
import gov.nist.javax.sip.ResponseEventExt;
import gov.nist.javax.sip.message.SIPResponse;
import org.slf4j.Logger;
@@ -12,7 +14,6 @@
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import javax.sdp.SdpFactory;
import javax.sdp.SdpParseException;
import javax.sdp.SessionDescription;
import javax.sip.InvalidArgumentException;
@@ -79,18 +80,8 @@
                ResponseEventExt event = (ResponseEventExt)evt;
                String contentString = new String(response.getRawContent());
                // jainSip不支持y=字段, ç§»é™¤ä»¥è§£æžã€‚
                int ssrcIndex = contentString.indexOf("y=");
                // æ£€æŸ¥æ˜¯å¦æœ‰y字段
                SessionDescription sdp;
                if (ssrcIndex >= 0) {
                    //ssrc规定长度为10字节,不取余下长度以避免后续还有“f=”字段
                    String substring = contentString.substring(0, contentString.indexOf("y="));
                    sdp = SdpFactory.getInstance().createSessionDescription(substring);
                } else {
                    sdp = SdpFactory.getInstance().createSessionDescription(contentString);
                }
                Gb28181Sdp gb28181Sdp = SipUtils.parseSDP(contentString);
                SessionDescription sdp = gb28181Sdp.getBaseSdb();
                SipURI requestUri = SipFactory.getInstance().createAddressFactory().createSipURI(sdp.getOrigin().getUsername(), event.getRemoteIpAddress() + ":" + event.getRemotePort());
                Request reqAck = headerProvider.createAckRequest(response.getLocalAddress().getHostAddress(), requestUri, response);
src/main/java/com/genersoft/iot/vmp/gb28181/utils/SipUtils.java
@@ -1,14 +1,22 @@
package com.genersoft.iot.vmp.gb28181.utils;
import com.genersoft.iot.vmp.gb28181.bean.DeviceChannel;
import com.genersoft.iot.vmp.gb28181.bean.Gb28181Sdp;
import com.genersoft.iot.vmp.gb28181.bean.RemoteAddressInfo;
import com.genersoft.iot.vmp.utils.DateUtil;
import com.genersoft.iot.vmp.utils.GitUtil;
import gov.nist.javax.sip.address.AddressImpl;
import gov.nist.javax.sip.address.SipUri;
import gov.nist.javax.sip.header.Subject;
import gov.nist.javax.sip.message.SIPRequest;
import org.apache.commons.lang3.RandomStringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.ObjectUtils;
import javax.sdp.SdpFactory;
import javax.sdp.SdpParseException;
import javax.sdp.SessionDescription;
import javax.sip.PeerUnavailableException;
import javax.sip.SipFactory;
import javax.sip.header.FromHeader;
@@ -16,6 +24,8 @@
import javax.sip.header.UserAgentHeader;
import javax.sip.message.Request;
import java.text.ParseException;
import java.time.LocalDateTime;
import java.time.format.DateTimeParseException;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
@@ -27,6 +37,8 @@
 * @createTime 2021å¹´09月27日 15:12:00
 */
public class SipUtils {
    private final static Logger logger = LoggerFactory.getLogger(SipUtils.class);
    public static String getUserIdFromFromHeader(Request request) {
        FromHeader fromHeader = (FromHeader)request.getHeader(FromHeader.NAME);
@@ -51,7 +63,7 @@
    }
    public static  String getNewViaTag() {
        return "z9hG4bK" + System.currentTimeMillis();
        return "z9hG4bK" + RandomStringUtils.randomNumeric(10);
    }
    public static UserAgentHeader createUserAgentHeader(GitUtil gitUtil) throws PeerUnavailableException, ParseException {
@@ -189,4 +201,67 @@
        }
        return deviceChannel;
    }
    public static Gb28181Sdp parseSDP(String sdpStr) throws SdpParseException {
        // jainSip不支持y= f=字段, ç§»é™¤ä»¥è§£æžã€‚
        int ssrcIndex = sdpStr.indexOf("y=");
        int mediaDescriptionIndex = sdpStr.indexOf("f=");
        // æ£€æŸ¥æ˜¯å¦æœ‰y字段
        SessionDescription sdp;
        String ssrc = null;
        String mediaDescription = null;
        if (mediaDescriptionIndex == 0 && ssrcIndex == 0) {
            sdp = SdpFactory.getInstance().createSessionDescription(sdpStr);
        }else {
            String lines[] = sdpStr.split("\\r?\\n");
            StringBuilder sdpBuffer = new StringBuilder();
            for (String line : lines) {
                if (line.trim().startsWith("y=")) {
                    ssrc = line.substring(2);
                }else if (line.trim().startsWith("f=")) {
                    mediaDescription = line.substring(2);
                }else {
                    sdpBuffer.append(line.trim()).append("\r\n");
                }
            }
            sdp = SdpFactory.getInstance().createSessionDescription(sdpBuffer.toString());
        }
        return Gb28181Sdp.getInstance(sdp, ssrc, mediaDescription);
    }
    public static String getSsrcFromSdp(String sdpStr) {
        // jainSip不支持y= f=字段, ç§»é™¤ä»¥è§£æžã€‚
        int ssrcIndex = sdpStr.indexOf("y=");
        if (ssrcIndex == 0) {
            return null;
        }
        String lines[] = sdpStr.split("\\r?\\n");
        for (String line : lines) {
            if (line.trim().startsWith("y=")) {
                return line.substring(2);
            }
        }
        return null;
    }
    public static String parseTime(String timeStr) {
        if (ObjectUtils.isEmpty(timeStr)){
            return null;
        }
        System.out.println(timeStr);
        LocalDateTime localDateTime;
        try {
            localDateTime = LocalDateTime.parse(timeStr);
        }catch (DateTimeParseException e) {
            try {
                localDateTime = LocalDateTime.parse(timeStr, DateUtil.formatterISO8601);
            }catch (DateTimeParseException e2) {
                logger.error("[格式化时间] æ— æ³•格式化时间: {}", timeStr);
                return null;
            }
        }
        return localDateTime.format(DateUtil.formatterISO8601);
    }
}
src/main/java/com/genersoft/iot/vmp/media/zlm/ZLMRTPServerFactory.java
@@ -6,7 +6,9 @@
import com.genersoft.iot.vmp.common.CommonCallback;
import com.genersoft.iot.vmp.conf.UserSetting;
import com.genersoft.iot.vmp.gb28181.bean.SendRtpItem;
import com.genersoft.iot.vmp.media.zlm.dto.*;
import com.genersoft.iot.vmp.media.zlm.dto.HookSubscribeFactory;
import com.genersoft.iot.vmp.media.zlm.dto.HookSubscribeForRtpServerTimeout;
import com.genersoft.iot.vmp.media.zlm.dto.MediaServerItem;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
src/main/java/com/genersoft/iot/vmp/media/zlm/dto/StreamProxyItem.java
@@ -20,28 +20,26 @@
    @Schema(description = "拉流地址")
    private String url;
    @Schema(description = "拉流地址")
    private String src_url;
    private String srcUrl;
    @Schema(description = "目标地址")
    private String dst_url;
    private String dstUrl;
    @Schema(description = "超时时间")
    private int timeout_ms;
    private int timeoutMs;
    @Schema(description = "ffmpeg模板KEY")
    private String ffmpeg_cmd_key;
    private String ffmpegCmdKey;
    @Schema(description = "rtsp拉流时,拉流方式,0:tcp,1:udp,2:组播")
    private String rtp_type;
    private String rtpType;
    @Schema(description = "是否启用")
    private boolean enable;
    @Schema(description = "是否启用音频")
    private boolean enable_audio;
    private boolean enableAudio;
    @Schema(description = "是否启用MP4")
    private boolean enable_mp4;
    private boolean enableMp4;
    @Schema(description = "是否 æ— äººè§‚看时删除")
    private boolean enable_remove_none_reader;
    private boolean enableRemoveNoneReader;
    @Schema(description = "是否 æ— äººè§‚看时自动停用")
    private boolean enable_disable_none_reader;
    @Schema(description = "创建时间")
    private String createTime;
    private boolean enableDisableNoneReader;
    public String getType() {
        return type;
@@ -89,44 +87,44 @@
        this.url = url;
    }
    public String getSrc_url() {
        return src_url;
    public String getSrcUrl() {
        return srcUrl;
    }
    public void setSrc_url(String src_url) {
        this.src_url = src_url;
    public void setSrcUrl(String src_url) {
        this.srcUrl = src_url;
    }
    public String getDst_url() {
        return dst_url;
    public String getDstUrl() {
        return dstUrl;
    }
    public void setDst_url(String dst_url) {
        this.dst_url = dst_url;
    public void setDstUrl(String dst_url) {
        this.dstUrl = dst_url;
    }
    public int getTimeout_ms() {
        return timeout_ms;
    public int getTimeoutMs() {
        return timeoutMs;
    }
    public void setTimeout_ms(int timeout_ms) {
        this.timeout_ms = timeout_ms;
    public void setTimeoutMs(int timeout_ms) {
        this.timeoutMs = timeout_ms;
    }
    public String getFfmpeg_cmd_key() {
        return ffmpeg_cmd_key;
    public String getFfmpegCmdKey() {
        return ffmpegCmdKey;
    }
    public void setFfmpeg_cmd_key(String ffmpeg_cmd_key) {
        this.ffmpeg_cmd_key = ffmpeg_cmd_key;
    public void setFfmpegCmdKey(String ffmpeg_cmd_key) {
        this.ffmpegCmdKey = ffmpeg_cmd_key;
    }
    public String getRtp_type() {
        return rtp_type;
    public String getRtpType() {
        return rtpType;
    }
    public void setRtp_type(String rtp_type) {
        this.rtp_type = rtp_type;
    public void setRtpType(String rtp_type) {
        this.rtpType = rtp_type;
    }
    public boolean isEnable() {
@@ -137,45 +135,37 @@
        this.enable = enable;
    }
    public boolean isEnable_mp4() {
        return enable_mp4;
    public boolean isEnableMp4() {
        return enableMp4;
    }
    public void setEnable_mp4(boolean enable_mp4) {
        this.enable_mp4 = enable_mp4;
    public void setEnableMp4(boolean enable_mp4) {
        this.enableMp4 = enable_mp4;
    }
    @Override
    public String getCreateTime() {
        return createTime;
    public boolean isEnableRemoveNoneReader() {
        return enableRemoveNoneReader;
    }
    @Override
    public void setCreateTime(String createTime) {
        this.createTime = createTime;
    public void setEnableRemoveNoneReader(boolean enable_remove_none_reader) {
        this.enableRemoveNoneReader = enable_remove_none_reader;
    }
    public boolean isEnable_remove_none_reader() {
        return enable_remove_none_reader;
    public boolean isEnableDisableNoneReader() {
        return enableDisableNoneReader;
    }
    public void setEnable_remove_none_reader(boolean enable_remove_none_reader) {
        this.enable_remove_none_reader = enable_remove_none_reader;
    public void setEnableDisableNoneReader(boolean enable_disable_none_reader) {
        this.enableDisableNoneReader = enable_disable_none_reader;
    }
    public boolean isEnable_disable_none_reader() {
        return enable_disable_none_reader;
    public boolean isEnableAudio() {
        return enableAudio;
    }
    public void setEnable_disable_none_reader(boolean enable_disable_none_reader) {
        this.enable_disable_none_reader = enable_disable_none_reader;
    public void setEnableAudio(boolean enable_audio) {
        this.enableAudio = enable_audio;
    }
    public boolean isEnable_audio() {
        return enable_audio;
    }
    public void setEnable_audio(boolean enable_audio) {
        this.enable_audio = enable_audio;
    }
}
src/main/java/com/genersoft/iot/vmp/service/IStreamProxyService.java
@@ -1,6 +1,7 @@
package com.genersoft.iot.vmp.service;
import com.alibaba.fastjson2.JSONObject;
import com.genersoft.iot.vmp.common.GeneralCallback;
import com.genersoft.iot.vmp.common.StreamInfo;
import com.genersoft.iot.vmp.media.zlm.dto.MediaServerItem;
import com.genersoft.iot.vmp.media.zlm.dto.StreamProxyItem;
@@ -13,7 +14,7 @@
     * ä¿å­˜è§†é¢‘代理
     * @param param
     */
    StreamInfo save(StreamProxyItem param);
    void save(StreamProxyItem param, GeneralCallback<StreamInfo> callback);
    /**
     * æ·»åŠ è§†é¢‘ä»£ç†åˆ°zlm
src/main/java/com/genersoft/iot/vmp/service/impl/DeviceServiceImpl.java
@@ -187,7 +187,7 @@
    @Override
    public void offline(String deviceId, String reason) {
        logger.error("[设备离线],{}, device:{}", reason, deviceId);
        logger.warn("[设备离线],{}, device:{}", reason, deviceId);
        Device device = deviceMapper.getDeviceByDeviceId(deviceId);
        if (device == null) {
            return;
src/main/java/com/genersoft/iot/vmp/service/impl/MediaServerServiceImpl.java
@@ -418,7 +418,7 @@
        }
        final String zlmKeepaliveKey = zlmKeepaliveKeyPrefix + serverItem.getId();
        dynamicTask.stop(zlmKeepaliveKey);
        dynamicTask.startDelay(zlmKeepaliveKey, new KeepAliveTimeoutRunnable(serverItem), (Math.getExponent(serverItem.getHookAliveInterval()) + 5) * 1000);
        dynamicTask.startDelay(zlmKeepaliveKey, new KeepAliveTimeoutRunnable(serverItem), (serverItem.getHookAliveInterval().intValue() + 5) * 1000);
        publisher.zlmOnlineEventPublish(serverItem.getId());
        logger.info("[ZLM] è¿žæŽ¥æˆåŠŸ {} - {}:{} ",
src/main/java/com/genersoft/iot/vmp/service/impl/StreamProxyServiceImpl.java
@@ -2,12 +2,16 @@
import com.alibaba.fastjson2.JSONArray;
import com.alibaba.fastjson2.JSONObject;
import com.genersoft.iot.vmp.common.GeneralCallback;
import com.genersoft.iot.vmp.common.StreamInfo;
import com.genersoft.iot.vmp.conf.UserSetting;
import com.genersoft.iot.vmp.conf.exception.ControllerException;
import com.genersoft.iot.vmp.gb28181.event.EventPublisher;
import com.genersoft.iot.vmp.gb28181.event.subscribe.catalog.CatalogEvent;
import com.genersoft.iot.vmp.media.zlm.ZLMRESTfulUtils;
import com.genersoft.iot.vmp.media.zlm.ZlmHttpHookSubscribe;
import com.genersoft.iot.vmp.media.zlm.dto.HookSubscribeFactory;
import com.genersoft.iot.vmp.media.zlm.dto.HookSubscribeForStreamChange;
import com.genersoft.iot.vmp.media.zlm.dto.MediaServerItem;
import com.genersoft.iot.vmp.media.zlm.dto.StreamProxyItem;
import com.genersoft.iot.vmp.media.zlm.dto.hook.OnStreamChangedHookParam;
@@ -86,6 +90,9 @@
    private IMediaServerService mediaServerService;
    @Autowired
    private ZlmHttpHookSubscribe hookSubscribe;
    @Autowired
    DataSourceTransactionManager dataSourceTransactionManager;
    @Autowired
@@ -93,7 +100,7 @@
    @Override
    public StreamInfo save(StreamProxyItem param) {
    public void save(StreamProxyItem param, GeneralCallback<StreamInfo> callback) {
        MediaServerItem mediaInfo;
        if (ObjectUtils.isEmpty(param.getMediaServerId()) || "auto".equals(param.getMediaServerId())){
            mediaInfo = mediaServerService.getMediaServerForMinimumLoad(null);
@@ -104,10 +111,43 @@
            logger.warn("保存代理未找到在线的ZLM...");
            throw new ControllerException(ErrorCode.ERROR100.getCode(), "保存代理未找到在线的ZLM");
        }
        String dstUrl = String.format("rtmp://%s:%s/%s/%s", "127.0.0.1", mediaInfo.getRtmpPort(), param.getApp(),
                param.getStream() );
        param.setDst_url(dstUrl);
        StringBuffer resultMsg = new StringBuffer();
        String dstUrl;
        if ("ffmpeg".equalsIgnoreCase(param.getType())) {
            JSONObject jsonObject = zlmresTfulUtils.getMediaServerConfig(mediaInfo);
            if (jsonObject.getInteger("code") != 0) {
                throw new ControllerException(ErrorCode.ERROR100.getCode(), "获取流媒体配置失败");
            }
            JSONArray dataArray = jsonObject.getJSONArray("data");
            JSONObject mediaServerConfig = dataArray.getJSONObject(0);
            String ffmpegCmd = mediaServerConfig.getString(param.getFfmpegCmdKey());
            String schema = getSchemaFromFFmpegCmd(ffmpegCmd);
            if (schema == null) {
                throw new ControllerException(ErrorCode.ERROR100.getCode(), "ffmpeg拉流代理无法从ffmpeg cmd中获取到输出格式");
            }
            int port;
            String schemaForUri;
            if (schema.equalsIgnoreCase("rtsp")) {
                port = mediaInfo.getRtspPort();
                schemaForUri = schema;
            }else if (schema.equalsIgnoreCase("flv")) {
                port = mediaInfo.getHttpPort();
                schemaForUri = "http";
            }else if (schema.equalsIgnoreCase("rtmp")) {
                port = mediaInfo.getRtmpPort();
                schemaForUri = schema;
            }else {
                port = mediaInfo.getRtmpPort();
                schemaForUri = schema;
            }
            dstUrl = String.format("%s://%s:%s/%s/%s", schemaForUri, "127.0.0.1", port, param.getApp(),
                    param.getStream());
        }else {
            dstUrl = String.format("rtmp://%s:%s/%s/%s", "127.0.0.1", mediaInfo.getRtmpPort(), param.getApp(),
                    param.getStream());
        }
        param.setDstUrl(dstUrl);
        logger.info("[拉流代理] è¾“出地址为:{}", dstUrl);
        param.setMediaServerId(mediaInfo.getId());
        boolean saveResult;
        // æ›´æ–°
@@ -117,29 +157,60 @@
            saveResult = addStreamProxy(param);
        }
        if (!saveResult) {
            throw new ControllerException(ErrorCode.ERROR100.getCode(),"保存失败");
            callback.run(ErrorCode.ERROR100.getCode(), "保存失败", null);
            return;
        }
        StreamInfo resultForStreamInfo = null;
        resultMsg.append("保存成功");
        HookSubscribeForStreamChange hookSubscribeForStreamChange = HookSubscribeFactory.on_stream_changed(param.getApp(), param.getStream(), true, "rtsp", mediaInfo.getId());
        hookSubscribe.addSubscribe(hookSubscribeForStreamChange, (mediaServerItem, response) -> {
            StreamInfo streamInfo = mediaService.getStreamInfoByAppAndStream(
                    mediaInfo, param.getApp(), param.getStream(), null, null);
            callback.run(ErrorCode.SUCCESS.getCode(), ErrorCode.SUCCESS.getMsg(), streamInfo);
        });
        if (param.isEnable()) {
            JSONObject jsonObject = addStreamProxyToZlm(param);
            if (jsonObject == null || jsonObject.getInteger("code") != 0) {
                resultMsg.append(", ä½†æ˜¯å¯ç”¨å¤±è´¥ï¼Œè¯·æ£€æŸ¥æµåœ°å€æ˜¯å¦å¯ç”¨");
            if (jsonObject != null && jsonObject.getInteger("code") == 0) {
                hookSubscribe.removeSubscribe(hookSubscribeForStreamChange);
                StreamInfo streamInfo = mediaService.getStreamInfoByAppAndStream(
                        mediaInfo, param.getApp(), param.getStream(), null, null);
                callback.run(ErrorCode.SUCCESS.getCode(), ErrorCode.SUCCESS.getMsg(), streamInfo);
            }else {
                param.setEnable(false);
                // ç›´æŽ¥ç§»é™¤
                if (param.isEnable_remove_none_reader()) {
                if (param.isEnableRemoveNoneReader()) {
                    del(param.getApp(), param.getStream());
                }else {
                    updateStreamProxy(param);
                }
                if (jsonObject == null){
                    callback.run(ErrorCode.ERROR100.getCode(), "记录已保存,启用失败", null);
                    return;
                }else {
                    callback.run(ErrorCode.ERROR100.getCode(), jsonObject.getString("msg"), null);
                    return;
                }
            }
        }
    }
            }else {
                resultForStreamInfo = mediaService.getStreamInfoByAppAndStream(
                        mediaInfo, param.getApp(), param.getStream(), null, null);
    private String getSchemaFromFFmpegCmd(String ffmpegCmd) {
        ffmpegCmd = ffmpegCmd.replaceAll(" + ", " ");
        String[] paramArray = ffmpegCmd.split(" ");
        if (paramArray.length == 0) {
            return null;
        }
        for (int i = 0; i < paramArray.length; i++) {
            if (paramArray[i].equalsIgnoreCase("-f")) {
                if (i + 1 < paramArray.length - 1) {
                    return paramArray[i+1];
                }else {
                    return null;
                }
            }
        }
        return resultForStreamInfo;
        return null;
    }
    /**
@@ -228,11 +299,11 @@
        }
        if ("default".equals(param.getType())){
            result = zlmresTfulUtils.addStreamProxy(mediaServerItem, param.getApp(), param.getStream(), param.getUrl(),
                    param.isEnable_audio(), param.isEnable_mp4(), param.getRtp_type());
                    param.isEnableAudio(), param.isEnableMp4(), param.getRtpType());
        }else if ("ffmpeg".equals(param.getType())) {
            result = zlmresTfulUtils.addFFmpegSource(mediaServerItem, param.getSrc_url(), param.getDst_url(),
                    param.getTimeout_ms() + "", param.isEnable_audio(), param.isEnable_mp4(),
                    param.getFfmpeg_cmd_key());
            result = zlmresTfulUtils.addFFmpegSource(mediaServerItem, param.getSrcUrl(), param.getDstUrl(),
                    param.getTimeoutMs() + "", param.isEnableAudio(), param.isEnableMp4(),
                    param.getFfmpegCmdKey());
        }
        return result;
    }
@@ -286,7 +357,7 @@
                updateStreamProxy(streamProxy);
            }else {
                logger.info("启用代理失败: {}/{}->{}({})", app, stream, jsonObject.getString("msg"),
                        streamProxy.getSrc_url() == null? streamProxy.getUrl():streamProxy.getSrc_url());
                        streamProxy.getSrcUrl() == null? streamProxy.getUrl():streamProxy.getSrcUrl());
            }
        }
        return result;
src/main/java/com/genersoft/iot/vmp/service/impl/StreamPushServiceImpl.java
@@ -183,6 +183,7 @@
    @Override
    public boolean stop(String app, String streamId) {
        logger.info("[推流 ] åœæ­¢æµï¼š {}/{}", app, streamId);
        StreamPushItem streamPushItem = streamPushMapper.selectOne(app, streamId);
        if (streamPushItem != null) {
            gbStreamService.sendCatalogMsg(streamPushItem, CatalogEvent.DEL);
src/main/java/com/genersoft/iot/vmp/service/redisMsg/RedisCloseStreamMsgListener.java
New file
@@ -0,0 +1,59 @@
package com.genersoft.iot.vmp.service.redisMsg;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import com.genersoft.iot.vmp.service.IStreamPushService;
import org.jetbrains.annotations.NotNull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.data.redis.connection.Message;
import org.springframework.data.redis.connection.MessageListener;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.stereotype.Component;
import java.util.concurrent.ConcurrentLinkedQueue;
/**
 * æŽ¥æ”¶æ¥è‡ªredis的关闭流更新通知
 * @author lin
 */
@Component
public class RedisCloseStreamMsgListener implements MessageListener {
    private final static Logger logger = LoggerFactory.getLogger(RedisCloseStreamMsgListener.class);
    @Autowired
    private IStreamPushService pushService;
    private ConcurrentLinkedQueue<Message> taskQueue = new ConcurrentLinkedQueue<>();
    @Qualifier("taskExecutor")
    @Autowired
    private ThreadPoolTaskExecutor taskExecutor;
    @Override
    public void onMessage(@NotNull Message message, byte[] bytes) {
        boolean isEmpty = taskQueue.isEmpty();
        taskQueue.offer(message);
        if (isEmpty) {
            taskExecutor.execute(() -> {
                while (!taskQueue.isEmpty()) {
                    Message msg = taskQueue.poll();
                    try {
                        JSONObject jsonObject = JSON.parseObject(msg.getBody());
                        String app = jsonObject.getString("app");
                        String stream = jsonObject.getString("stream");
                        pushService.stop(app, stream);
                    }catch (Exception e) {
                        logger.warn("[REDIS的关闭推流通知] å‘现未处理的异常, \r\n{}", JSON.toJSONString(message));
                        logger.error("[REDIS的关闭推流通知] å¼‚常内容: ", e);
                    }
                }
            });
        }
    }
}
src/main/java/com/genersoft/iot/vmp/storager/IRedisCatchStorage.java
@@ -202,4 +202,5 @@
    void removeAllDevice();
    void sendDeviceOrChannelStatus(String deviceId, String channelId, boolean online);
    void sendChannelAddOrDelete(String deviceId, String channelId, boolean add);
}
src/main/java/com/genersoft/iot/vmp/storager/dao/ParentPlatformMapper.java
@@ -96,6 +96,6 @@
    @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} " +
            "union " +
            "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}")
            "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}")
    List<ChannelSourceInfo> getChannelSource(String platform_id, String gbId);
}
src/main/java/com/genersoft/iot/vmp/storager/dao/PlatformCatalogMapper.java
@@ -12,7 +12,7 @@
@Repository
public interface PlatformCatalogMapper {
    @Insert("INSERT INTO platform_catalog (id, name, platform_id, parent_id, civil_code, business_group_id) VALUES" +
    @Insert("INSERT INTO wvp_platform_catalog (id, name, platform_id, parent_id, civil_code, business_group_id) VALUES" +
            "(#{id}, #{name}, #{platformId}, #{parentId}, #{civilCode}, #{businessGroupId})")
    int add(PlatformCatalog platformCatalog);
@@ -32,7 +32,7 @@
    PlatformCatalog select(String id);
    @Update(value = {" <script>" +
            "UPDATE platform_catalog " +
            "UPDATE wvp_platform_catalog " +
            "SET name=#{name}" +
            "WHERE id=#{id}"+
            "</script>"})
@@ -41,11 +41,11 @@
    @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}")
    List<PlatformCatalog> selectByPlatForm(String platformId);
    @Select("SELECT pc.* FROM  platform_catalog pc WHERE pc.id = (SELECT pp.catalog_id from wvp_platform pp WHERE pp.server_gb_id=#{platformId})")
    @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})")
    PlatformCatalog selectDefaultByPlatFormId(String platformId);
    @Select("SELECT pc.* FROM  platform_catalog pc WHERE pc.id = #{id}")
    @Select("SELECT pc.* FROM  wvp_platform_catalog pc WHERE pc.id = #{id}")
    PlatformCatalog selectParentCatalog(String id);
    @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 @@
    @Insert("INSERT INTO wvp_stream_proxy (type, name, app, stream,media_server_id, url, src_url, dst_url, " +
            "timeout_ms, ffmpeg_cmd_key, rtp_type, enable_audio, enable_mp4, enable, status, enable_remove_none_reader, enable_disable_none_reader, create_time) VALUES" +
            "(#{type}, #{name}, #{app}, #{stream}, #{mediaServerId}, #{url}, #{src_url}, #{dst_url}, " +
            "#{timeout_ms}, #{ffmpeg_cmd_key}, #{rtp_type}, #{enable_audio}, #{enable_mp4}, #{enable}, #{status}, " +
            "#{enable_remove_none_reader}, #{enable_disable_none_reader}, #{createTime} )")
            "(#{type}, #{name}, #{app}, #{stream}, #{mediaServerId}, #{url}, #{srcUrl}, #{dstUrl}, " +
            "#{timeoutMs}, #{ffmpegCmdKey}, #{rtpType}, #{enableAudio}, #{enableMp4}, #{enable}, #{status}, " +
            "#{enableRemoveNoneReader}, #{enableDisableNoneReader}, #{createTime} )")
    int add(StreamProxyItem streamProxyDto);
    @Update("UPDATE wvp_stream_proxy " +
@@ -25,17 +25,17 @@
            "stream=#{stream}," +
            "url=#{url}, " +
            "media_server_id=#{mediaServerId}, " +
            "src_url=#{src_url}," +
            "dst_url=#{dst_url}, " +
            "timeout_ms=#{timeout_ms}, " +
            "ffmpeg_cmd_key=#{ffmpeg_cmd_key}, " +
            "rtp_type=#{rtp_type}, " +
            "enable_audio=#{enable_audio}, " +
            "src_url=#{srcUrl}," +
            "dst_url=#{dstUrl}, " +
            "timeout_ms=#{timeoutMs}, " +
            "ffmpeg_cmd_key=#{ffmpegCmdKey}, " +
            "rtp_type=#{rtpType}, " +
            "enable_audio=#{enableAudio}, " +
            "enable=#{enable}, " +
            "status=#{status}, " +
            "enable_remove_none_reader=#{enable_remove_none_reader}, " +
            "enable_disable_none_reader=#{enable_disable_none_reader}, " +
            "enable_mp4=#{enable_mp4} " +
            "enable_remove_none_reader=#{enableRemoveNoneReader}, " +
            "enable_disable_none_reader=#{enableDisableNoneReader}, " +
            "enable_mp4=#{enableMp4} " +
            "WHERE app=#{app} AND stream=#{stream}")
    int update(StreamProxyItem streamProxyDto);
src/main/java/com/genersoft/iot/vmp/storager/impl/RedisCatchStorageImpl.java
@@ -596,18 +596,29 @@
    @Override
    public void sendDeviceOrChannelStatus(String deviceId, String channelId, boolean online) {
        String key = VideoManagerConstants.VM_MSG_SUBSCRIBE_DEVICE_STATUS;
        if (channelId == null) {
            logger.info("[redis通知] æŽ¨é€è®¾å¤‡çŠ¶æ€ï¼Œ {}-{}", deviceId, online);
        }else {
            logger.info("[redis通知] æŽ¨é€é€šé“状态, {}/{}-{}", deviceId, channelId, online);
        }
        StringBuilder msg = new StringBuilder();
        msg.append(deviceId);
        if (channelId != null) {
            msg.append(":").append(channelId);
        }
        msg.append(" ").append(online? "ON":"OFF");
        logger.info("[redis通知] æŽ¨é€çŠ¶æ€-> {} ", msg);
        // ä½¿ç”¨ RedisTemplate<Object, Object> å‘送字符串消息会导致发送的消息多带了双引号
        stringRedisTemplate.convertAndSend(key, msg.toString());
    }
    @Override
    public void sendChannelAddOrDelete(String deviceId, String channelId, boolean add) {
        String key = VideoManagerConstants.VM_MSG_SUBSCRIBE_DEVICE_STATUS;
        StringBuilder msg = new StringBuilder();
        msg.append(deviceId);
        if (channelId != null) {
            msg.append(":").append(channelId);
        }
        msg.append(" ").append(add? "ADD":"DELETE");
        logger.info("[redis通知] æŽ¨é€é€šé“-> {}", msg);
        // ä½¿ç”¨ RedisTemplate<Object, Object> å‘送字符串消息会导致发送的消息多带了双引号
        stringRedisTemplate.convertAndSend(key, msg.toString());
    }
src/main/java/com/genersoft/iot/vmp/vmanager/bean/ErrorCode.java
@@ -6,7 +6,7 @@
public enum ErrorCode {
    SUCCESS(0, "成功"),
    ERROR100(100, "失败"),
    ERROR400(400, "参数不全或者错误"),
    ERROR400(400, "参数或方法错误"),
    ERROR404(404, "资源未找到"),
    ERROR403(403, "无权限操作"),
    ERROR401(401, "请登录后重新请求"),
src/main/java/com/genersoft/iot/vmp/vmanager/bean/SnapPath.java
New file
@@ -0,0 +1,50 @@
package com.genersoft.iot.vmp.vmanager.bean;
import io.swagger.v3.oas.annotations.media.Schema;
@Schema(description = "截图地址信息")
public class SnapPath {
    @Schema(description = "相对地址")
    private String path;
    @Schema(description = "绝对地址")
    private String absoluteFilePath;
    @Schema(description = "请求地址")
    private String url;
    public static SnapPath getInstance(String path, String absoluteFilePath, String url) {
        SnapPath snapPath = new SnapPath();
        snapPath.setPath(path);
        snapPath.setAbsoluteFilePath(absoluteFilePath);
        snapPath.setUrl(url);
        return snapPath;
    }
    public String getPath() {
        return path;
    }
    public void setPath(String path) {
        this.path = path;
    }
    public String getAbsoluteFilePath() {
        return absoluteFilePath;
    }
    public void setAbsoluteFilePath(String absoluteFilePath) {
        this.absoluteFilePath = absoluteFilePath;
    }
    public String getUrl() {
        return url;
    }
    public void setUrl(String url) {
        this.url = url;
    }
}
src/main/java/com/genersoft/iot/vmp/vmanager/gb28181/MobilePosition/MobilePositionController.java
@@ -102,7 +102,7 @@
    public DeferredResult<MobilePosition> realTimePosition(@PathVariable String deviceId) {
        Device device = storager.queryVideoDevice(deviceId);
        String uuid = UUID.randomUUID().toString();
        String key = DeferredResultHolder.CALLBACK_CMD_MOBILEPOSITION + deviceId;
        String key = DeferredResultHolder.CALLBACK_CMD_MOBILE_POSITION + deviceId;
        try {
            cmder.mobilePostitionQuery(device, event -> {
                RequestMessage msg = new RequestMessage();
src/main/java/com/genersoft/iot/vmp/vmanager/gb28181/device/DeviceQuery.java
@@ -466,10 +466,12 @@
    @Operation(summary = "请求截图")
    @Parameter(name = "deviceId", description = "设备国标编号", required = true)
    @Parameter(name = "channelId", description = "通道国标编号", required = true)
    public void getSnap(HttpServletResponse resp, @PathVariable String deviceId, @PathVariable String channelId) {
    @Parameter(name = "mark", description = "标识", required = false)
    public void getSnap(HttpServletResponse resp, @PathVariable String deviceId, @PathVariable String channelId, @RequestParam(required = false) String mark) {
        try {
            final InputStream in = Files.newInputStream(new File("snap" + File.separator + deviceId + "_" + channelId + ".jpg").toPath());
            final InputStream in = Files.newInputStream(new File("snap" + File.separator + deviceId + "_" + channelId + (mark == null? ".jpg": ("_" + mark + ".jpg"))).toPath());
            resp.setContentType(MediaType.IMAGE_PNG_VALUE);
            IOUtils.copy(in, resp.getOutputStream());
        } catch (IOException e) {
src/main/java/com/genersoft/iot/vmp/vmanager/streamProxy/StreamProxyController.java
@@ -1,13 +1,18 @@
package com.genersoft.iot.vmp.vmanager.streamProxy;
import com.alibaba.fastjson2.JSONObject;
import com.genersoft.iot.vmp.common.StreamInfo;
import com.genersoft.iot.vmp.conf.UserSetting;
import com.genersoft.iot.vmp.conf.exception.ControllerException;
import com.genersoft.iot.vmp.gb28181.transmit.callback.DeferredResultHolder;
import com.genersoft.iot.vmp.gb28181.transmit.callback.RequestMessage;
import com.genersoft.iot.vmp.media.zlm.dto.MediaServerItem;
import com.genersoft.iot.vmp.media.zlm.dto.StreamProxyItem;
import com.genersoft.iot.vmp.service.IMediaServerService;
import com.genersoft.iot.vmp.service.IStreamProxyService;
import com.genersoft.iot.vmp.vmanager.bean.ErrorCode;
import com.genersoft.iot.vmp.vmanager.bean.StreamContent;
import com.genersoft.iot.vmp.vmanager.bean.WVPResult;
import com.github.pagehelper.PageInfo;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
@@ -18,6 +23,9 @@
import org.springframework.stereotype.Controller;
import org.springframework.util.ObjectUtils;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.context.request.async.DeferredResult;
import java.util.UUID;
@SuppressWarnings("rawtypes")
/**
@@ -36,6 +44,12 @@
    @Autowired
    private IStreamProxyService streamProxyService;
    @Autowired
    private DeferredResultHolder resultHolder;
    @Autowired
    private UserSetting userSetting;
    @Operation(summary = "分页查询流代理")
@@ -58,7 +72,7 @@
    })
    @PostMapping(value = "/save")
    @ResponseBody
    public StreamContent save(@RequestBody StreamProxyItem param){
    public DeferredResult<Object> save(@RequestBody StreamProxyItem param){
        logger.info("添加代理: " + JSONObject.toJSONString(param));
        if (ObjectUtils.isEmpty(param.getMediaServerId())) {
            param.setMediaServerId("auto");
@@ -69,7 +83,33 @@
        if (ObjectUtils.isEmpty(param.getGbId())) {
            param.setGbId(null);
        }
        return new StreamContent(streamProxyService.save(param));
        RequestMessage requestMessage = new RequestMessage();
        String key = DeferredResultHolder.CALLBACK_CMD_PROXY + param.getApp() + param.getStream();
        requestMessage.setKey(key);
        String uuid = UUID.randomUUID().toString();
        requestMessage.setId(uuid);
        DeferredResult<Object> result = new DeferredResult<>(userSetting.getPlayTimeout().longValue());
        // å½•像查询以channelId作为deviceId查询
        resultHolder.put(key, uuid, result);
        result.onTimeout(()->{
            WVPResult<StreamInfo> wvpResult = new WVPResult<>();
            wvpResult.setCode(ErrorCode.ERROR100.getCode());
            wvpResult.setMsg("超时");
            requestMessage.setData(wvpResult);
            resultHolder.invokeAllResult(requestMessage);
        });
        streamProxyService.save(param, (code, msg, streamInfo) -> {
            logger.info("[拉流代理] {}", code == ErrorCode.SUCCESS.getCode()? "成功":"失败: " + msg);
            if (code == ErrorCode.SUCCESS.getCode()) {
                requestMessage.setData(new StreamContent(streamInfo));
            }else {
                requestMessage.setData(WVPResult.fail(code, msg));
            }
            resultHolder.invokeAllResult(requestMessage);
        });
        return result;
    }
    @GetMapping(value = "/ffmpeg_cmd/list")
src/main/resources/all-application.yml
@@ -43,10 +43,6 @@
            idle-timeout: 300000                  # å…è®¸è¿žæŽ¥åœ¨è¿žæŽ¥æ± ä¸­ç©ºé—²çš„æœ€é•¿æ—¶é—´ï¼ˆä»¥æ¯«ç§’为单位)
            max-lifetime: 1200000                 # æ˜¯æ± ä¸­è¿žæŽ¥å…³é—­åŽçš„æœ€é•¿ç”Ÿå‘½å‘¨æœŸï¼ˆä»¥æ¯«ç§’为单位)
# ä¿®æ”¹ä¸ºæ•°æ®åº“字段下划线分隔直接对应java驼峰命名
mybatis:
    configuration:
        map-underscore-to-camel-case: true
# ä¿®æ”¹åˆ†é¡µæ’件为 postgresql, æ•°æ®åº“类型为mysql不需要
#pagehelper:
web_src/src/components/StreamProxyList.vue
@@ -22,8 +22,8 @@
              {{scope.row.url}}
            </el-tag>
            <el-tag size="medium" v-if="scope.row.type != 'default'">
              <i class="cpoy-btn el-icon-document-copy"  title="点击拷贝" v-clipboard="scope.row.src_url" @success="$message({type:'success', message:'成功拷贝到粘贴板'})"></i>
              {{scope.row.src_url}}
              <i class="cpoy-btn el-icon-document-copy"  title="点击拷贝" v-clipboard="scope.row.srcUrl" @success="$message({type:'success', message:'成功拷贝到粘贴板'})"></i>
              {{scope.row.srcUrl}}
            </el-tag>
          </div>
        </template>
@@ -58,25 +58,25 @@
      <el-table-column label="音频" min-width="120" >
        <template slot-scope="scope">
          <div slot="reference" class="name-wrapper">
            <el-tag size="medium" v-if="scope.row.enable_audio">已启用</el-tag>
            <el-tag size="medium" type="info" v-if="!scope.row.enable_audio">未启用</el-tag>
            <el-tag size="medium" v-if="scope.row.enableAudio">已启用</el-tag>
            <el-tag size="medium" type="info" v-if="!scope.row.enableAudio">未启用</el-tag>
          </div>
        </template>
      </el-table-column>
      <el-table-column label="录制" min-width="120" >
        <template slot-scope="scope">
          <div slot="reference" class="name-wrapper">
            <el-tag size="medium" v-if="scope.row.enable_mp4">已启用</el-tag>
            <el-tag size="medium" type="info" v-if="!scope.row.enable_mp4">未启用</el-tag>
            <el-tag size="medium" v-if="scope.row.enableMp4">已启用</el-tag>
            <el-tag size="medium" type="info" v-if="!scope.row.enableMp4">未启用</el-tag>
          </div>
        </template>
      </el-table-column>
      <el-table-column label="无人观看" min-width="160" >
        <template slot-scope="scope">
          <div slot="reference" class="name-wrapper">
            <el-tag size="medium" v-if="scope.row.enable_remove_none_reader">移除</el-tag>
            <el-tag size="medium" v-if="scope.row.enable_disable_none_reader">停用</el-tag>
            <el-tag size="medium" type="info" v-if="!scope.row.enable_remove_none_reader && !scope.row.enable_disable_none_reader">不做处理</el-tag>
            <el-tag size="medium" v-if="scope.row.enableRemoveNoneReader">移除</el-tag>
            <el-tag size="medium" v-if="scope.row.enableDisableNoneReader">停用</el-tag>
            <el-tag size="medium" type="info" v-if="!scope.row.enableRemoveNoneReader && !scope.row.enableDisableNoneReader">不做处理</el-tag>
          </div>
        </template>
      </el-table-column>
@@ -197,7 +197,7 @@
              this.$refs.onvifEdit.openDialog(res.data.data, (url)=>{
                  if (url != null) {
                    this.$refs.onvifEdit.close();
                    this.$refs.streamProxyEdit.openDialog({type: "default", url: url, src_url: url}, this.initData())
                    this.$refs.streamProxyEdit.openDialog({type: "default", url: url, srcUrl: url}, this.initData())
                  }
              })
            }else {
@@ -245,18 +245,25 @@
            },
            deleteStreamProxy: function(row){
                let that = this;
                that.$axios({
                    method:"delete",
                    url:"/api/proxy/del",
                    params:{
                      app: row.app,
                      stream: row.stream
                    }
                }).then((res)=>{
                              that.initData()
                }).catch(function (error) {
                    console.log(error);
                });
        this.$confirm('确定删除此代理吗?', '提示', {
          confirmButtonText: '确定',
          cancelButtonText: '取消',
          type: 'warning'
        }).then(() => {
          that.$axios({
            method:"delete",
            url:"/api/proxy/del",
            params:{
              app: row.app,
              stream: row.stream
            }
          }).then((res)=>{
            that.initData()
          }).catch(function (error) {
            console.log(error);
          });
        }).catch(() => {
        });
            },
            start: function(row){
        this.stopUpdateList()
web_src/src/components/dialog/StreamProxyEdit.vue
@@ -33,13 +33,13 @@
              <el-form-item label="拉流地址" prop="url" v-if="proxyParam.type=='default'">
                <el-input v-model="proxyParam.url" clearable></el-input>
              </el-form-item>
              <el-form-item label="拉流地址" prop="src_url" v-if="proxyParam.type=='ffmpeg'">
                <el-input v-model="proxyParam.src_url" clearable></el-input>
              <el-form-item label="拉流地址" prop="srcUrl" v-if="proxyParam.type=='ffmpeg'">
                <el-input v-model="proxyParam.srcUrl" clearable></el-input>
              </el-form-item>
              <el-form-item label="超时时间:毫秒" prop="timeout_ms" v-if="proxyParam.type=='ffmpeg'">
                <el-input v-model="proxyParam.timeout_ms" clearable></el-input>
              <el-form-item label="超时时间:毫秒" prop="timeoutMs" v-if="proxyParam.type=='ffmpeg'">
                <el-input v-model="proxyParam.timeoutMs" clearable></el-input>
              </el-form-item>
              <el-form-item label="节点选择" prop="rtp_type">
              <el-form-item label="节点选择" prop="rtpType">
                <el-select
                  v-model="proxyParam.mediaServerId"
                  @change="mediaServerIdChange"
@@ -54,10 +54,9 @@
                  </el-option>
                </el-select>
              </el-form-item>
              <el-form-item label="FFmpeg命令模板" prop="ffmpeg_cmd_key" v-if="proxyParam.type=='ffmpeg'">
<!--                <el-input v-model="proxyParam.ffmpeg_cmd_key" clearable></el-input>-->
              <el-form-item label="FFmpeg命令模板" prop="ffmpegCmdKey" v-if="proxyParam.type=='ffmpeg'">
                <el-select
                  v-model="proxyParam.ffmpeg_cmd_key"
                  v-model="proxyParam.ffmpegCmdKey"
                  style="width: 100%"
                  placeholder="请选择FFmpeg命令模板"
                >
@@ -72,9 +71,9 @@
              <el-form-item label="国标编码" prop="gbId">
                <el-input v-model="proxyParam.gbId" placeholder="设置国标编码可推送到国标" clearable></el-input>
              </el-form-item>
              <el-form-item label="拉流方式" prop="rtp_type" v-if="proxyParam.type=='default'">
              <el-form-item label="拉流方式" prop="rtpType" v-if="proxyParam.type=='default'">
                <el-select
                  v-model="proxyParam.rtp_type"
                  v-model="proxyParam.rtpType"
                  style="width: 100%"
                  placeholder="请选择拉流方式"
                >
@@ -83,10 +82,10 @@
                  <el-option label="组播" value="2"></el-option>
                </el-select>
              </el-form-item>
            <el-form-item label="无人观看" prop="rtp_type" >
            <el-form-item label="无人观看" prop="rtpType" >
              <el-select
                @change="noneReaderHandler"
                v-model="proxyParam.none_reader"
                v-model="proxyParam.noneReader"
                style="width: 100%"
                placeholder="请选择无人观看的处理方式"
              >
@@ -98,8 +97,8 @@
              <el-form-item label="其他选项">
                <div style="float: left;">
                  <el-checkbox label="启用" v-model="proxyParam.enable" ></el-checkbox>
                  <el-checkbox label="开启音频" v-model="proxyParam.enable_audio" ></el-checkbox>
                  <el-checkbox label="录制" v-model="proxyParam.enable_mp4" ></el-checkbox>
                  <el-checkbox label="开启音频" v-model="proxyParam.enableAudio" ></el-checkbox>
                  <el-checkbox label="录制" v-model="proxyParam.enableMp4" ></el-checkbox>
                </div>
              </el-form-item>
@@ -155,17 +154,17 @@
          app: null,
          stream: null,
          url: "",
          src_url: null,
          timeout_ms: null,
          ffmpeg_cmd_key: null,
          srcUrl: null,
          timeoutMs: null,
          ffmpegCmdKey: null,
          gbId: null,
          rtp_type: null,
          rtpType: null,
          enable: true,
          enable_audio: true,
          enable_mp4: false,
          none_reader: null,
          enable_remove_none_reader: false,
          enable_disable_none_reader: false,
          enableAudio: true,
          enableMp4: false,
          noneReader: null,
          enableRemoveNoneReader: false,
          enableDisableNoneReader: false,
          platformGbId: null,
          mediaServerId: null,
      },
@@ -177,9 +176,9 @@
        app: [{ required: true, message: "请输入应用名", trigger: "blur" }],
        stream: [{ required: true, message: "请输入流ID", trigger: "blur" }],
        url: [{ required: true, message: "请输入要代理的流", trigger: "blur" }],
        src_url: [{ required: true, message: "请输入要代理的流", trigger: "blur" }],
        timeout_ms: [{ required: true, message: "请输入FFmpeg推流成功超时时间", trigger: "blur" }],
        ffmpeg_cmd_key: [{ required: false, message: "请输入FFmpeg命令参数模板(可选)", trigger: "blur" }],
        srcUrl: [{ required: true, message: "请输入要代理的流", trigger: "blur" }],
        timeoutMs: [{ required: true, message: "请输入FFmpeg推流成功超时时间", trigger: "blur" }],
        ffmpegCmdKey: [{ required: false, message: "请输入FFmpeg命令参数模板(可选)", trigger: "blur" }],
      },
    };
  },
@@ -189,7 +188,7 @@
      this.listChangeCallback = callback;
      if (proxyParam != null) {
        this.proxyParam = proxyParam;
        this.proxyParam.none_reader = null;
        this.proxyParam.noneReader = null;
      }
      let that = this;
@@ -218,7 +217,7 @@
          }
        }).then(function (res) {
          that.ffmpegCmdList = res.data.data;
          that.proxyParam.ffmpeg_cmd_key = Object.keys(res.data.data)[0];
          that.proxyParam.ffmpegCmdKey = Object.keys(res.data.data)[0];
        }).catch(function (error) {
          console.log(error);
        });
@@ -275,15 +274,15 @@
      }
    },
    noneReaderHandler: function() {
      if (this.proxyParam.none_reader === null || this.proxyParam.none_reader === "0") {
        this.proxyParam.enable_disable_none_reader = false;
        this.proxyParam.enable_remove_none_reader = false;
      }else if (this.proxyParam.none_reader === "1"){
        this.proxyParam.enable_disable_none_reader = true;
        this.proxyParam.enable_remove_none_reader = false;
      }else if (this.proxyParam.none_reader ==="2"){
        this.proxyParam.enable_disable_none_reader = false;
        this.proxyParam.enable_remove_none_reader = true;
      if (this.proxyParam.noneReader === null || this.proxyParam.noneReader === "0") {
        this.proxyParam.enableDisableNoneReader = false;
        this.proxyParam.enableRemoveNoneReader = false;
      }else if (this.proxyParam.noneReader === "1"){
        this.proxyParam.enableDisableNoneReader = true;
        this.proxyParam.enableRemoveNoneReader = false;
      }else if (this.proxyParam.noneReader ==="2"){
        this.proxyParam.enableDisableNoneReader = false;
        this.proxyParam.enableRemoveNoneReader = true;
      }
    },
  },
web_src/src/components/dialog/devicePlayer.vue
@@ -14,7 +14,6 @@
            <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>
          </el-tab-pane>
          <el-tab-pane label="h265web">h265web敬请期待</el-tab-pane>
          <el-tab-pane label="wsPlayer">wsPlayer æ•¬è¯·æœŸå¾…</el-tab-pane>
        </el-tabs>
        <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>
        <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 @@
        playFromStreamInfo: function (realHasAudio, streamInfo) {
          this.showVideoDialog = true;
          this.hasaudio = realHasAudio && this.hasaudio;
          this.$refs[this.activePlayer].play(this.getUrlByStreamInfo(streamInfo))
          if (this.$refs[this.activePlayer]) {
            this.$refs[this.activePlayer].play(this.getUrlByStreamInfo(streamInfo))
          }else {
            this.$nextTick(() => {
              this.$refs[this.activePlayer].play(this.getUrlByStreamInfo(streamInfo))
            });
          }
        },
        close: function () {
            console.log('关闭视频');