648540858
2023-06-20 fa62ab9a0143433a5d058ab3229a37e4a9a0f696
Merge pull request #893 from sxh-netizen/wvp-28181-2.0

新增设备主子码流开关选择,默认为不开启
25个文件已修改
899 ■■■■ 已修改文件
sql/2.6.6-2.6.7更新.sql 3 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/genersoft/iot/vmp/common/InviteInfo.java 37 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/genersoft/iot/vmp/common/StreamInfo.java 27 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/genersoft/iot/vmp/conf/UserSetting.java 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/genersoft/iot/vmp/gb28181/bean/Device.java 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/genersoft/iot/vmp/gb28181/transmit/callback/DeferredResultHolder.java 26 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/genersoft/iot/vmp/gb28181/transmit/cmd/ISIPCommander.java 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/genersoft/iot/vmp/gb28181/transmit/cmd/impl/SIPCommander.java 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/ByeRequestProcessor.java 20 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/InviteRequestProcessor.java 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/genersoft/iot/vmp/media/zlm/ZLMHttpHookListener.java 71 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/genersoft/iot/vmp/service/IInviteStreamService.java 48 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/genersoft/iot/vmp/service/IPlayService.java 6 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/genersoft/iot/vmp/service/impl/DeviceServiceImpl.java 25 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/genersoft/iot/vmp/service/impl/InviteStreamServiceImpl.java 160 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/genersoft/iot/vmp/service/impl/PlayServiceImpl.java 270 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/genersoft/iot/vmp/storager/dao/DeviceChannelMapper.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/genersoft/iot/vmp/storager/dao/DeviceMapper.java 9 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/genersoft/iot/vmp/vmanager/gb28181/play/PlayController.java 43 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/genersoft/iot/vmp/web/gb28181/ApiStreamController.java 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/application-dev.yml 67 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/application.yml 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
web_src/config/index.js 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
web_src/src/components/channelList.vue 17 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
web_src/src/components/dialog/deviceEdit.vue 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
sql/2.6.6-2.6.7¸üÐÂ.sql
@@ -7,6 +7,7 @@
alter table device
    add mediaServerId varchar(50) default null;
ALTER TABLE device
    ADD COLUMN `switchPrimarySubStream` bit(1) NOT NULL DEFAULT b'0' COMMENT '开启主子码流切换的开关(0-不开启,1-开启)现在已知支持设备为 å¤§åŽã€TP——LINK全系设备' AFTER `keepalive_interval_time`
src/main/java/com/genersoft/iot/vmp/common/InviteInfo.java
@@ -1,6 +1,7 @@
package com.genersoft.iot.vmp.common;
import com.genersoft.iot.vmp.service.bean.SSRCInfo;
import io.swagger.v3.oas.annotations.media.Schema;
/**
 * è®°å½•每次发送invite消息的状态
@@ -123,4 +124,40 @@
    public void setStreamMode(String streamMode) {
        this.streamMode = streamMode;
    }
    /*=========================设备主子码流逻辑START====================*/
    @Schema(description = "是否为子码流(true-是,false-主码流)")
    private boolean subStream;
    public boolean isSubStream() {
        return subStream;
    }
    public void setSubStream(boolean subStream) {
        this.subStream = subStream;
    }
    public static InviteInfo getInviteInfo(String deviceId, String channelId,Boolean isSubStream, String stream, SSRCInfo ssrcInfo,
                                           String receiveIp, Integer receivePort, String streamMode,
                                           InviteSessionType type, InviteSessionStatus status) {
        InviteInfo inviteInfo = new InviteInfo();
        inviteInfo.setDeviceId(deviceId);
        inviteInfo.setChannelId(channelId);
        inviteInfo.setStream(stream);
        inviteInfo.setSsrcInfo(ssrcInfo);
        inviteInfo.setReceiveIp(receiveIp);
        inviteInfo.setReceivePort(receivePort);
        inviteInfo.setStreamMode(streamMode);
        inviteInfo.setType(type);
        inviteInfo.setStatus(status);
        if(isSubStream != null){
            inviteInfo.setSubStream(isSubStream);
        }
        return inviteInfo;
    }
    /*=========================设备主子码流逻辑END====================*/
}
src/main/java/com/genersoft/iot/vmp/common/StreamInfo.java
@@ -528,4 +528,31 @@
        }
        return instance;
    }
    /*=========================设备主子码流逻辑START====================*/
    @Schema(description = "是否为子码流(true-是,false-主码流)")
    private boolean subStream;
    public boolean isSubStream() {
        return subStream;
    }
    public void setSubStream(boolean subStream) {
        this.subStream = subStream;
    }
    public static String getPlayStream(String deviceId,String channelId,boolean isSubStream){
        String streamId;
        if(isSubStream){
            streamId = String.format("%s_%s_%s","sub",deviceId, channelId);
        }else {
            streamId = String.format("%s_%s_%s","main", deviceId, channelId);
        }
        return streamId;
    }
    /*=========================设备主子码流逻辑END====================*/
}
src/main/java/com/genersoft/iot/vmp/conf/UserSetting.java
@@ -1,5 +1,6 @@
package com.genersoft.iot.vmp.conf;
import io.swagger.v3.oas.annotations.media.Schema;
import org.springframework.core.annotation.Order;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
@@ -25,11 +26,11 @@
    private int platformPlayTimeout = 60000;
    private Boolean interfaceAuthentication = Boolean.TRUE;
    private Boolean interfaceAuthentication = Boolean.FALSE;
    private Boolean recordPushLive = Boolean.TRUE;
    private Boolean recordPushLive = Boolean.FALSE;
    private Boolean recordSip = Boolean.TRUE;
    private Boolean recordSip = Boolean.FALSE;
    private Boolean logInDatebase = Boolean.TRUE;
src/main/java/com/genersoft/iot/vmp/gb28181/bean/Device.java
@@ -189,6 +189,8 @@
    private SipTransactionInfo sipTransactionInfo;
    public String getDeviceId() {
        return deviceId;
    }
@@ -447,4 +449,20 @@
    public void setSipTransactionInfo(SipTransactionInfo sipTransactionInfo) {
        this.sipTransactionInfo = sipTransactionInfo;
    }
    /*======================设备主子码流逻辑START=========================*/
    @Schema(description = "开启主子码流切换的开关(false-不开启,true-开启)")
    private boolean switchPrimarySubStream;
    public boolean isSwitchPrimarySubStream() {
        return switchPrimarySubStream;
    }
    public void setSwitchPrimarySubStream(boolean switchPrimarySubStream) {
        this.switchPrimarySubStream = switchPrimarySubStream;
    }
    /*======================设备主子码流逻辑END=========================*/
}
src/main/java/com/genersoft/iot/vmp/gb28181/transmit/callback/DeferredResultHolder.java
@@ -155,4 +155,30 @@
            map.remove(msg.getKey());
        }
    }
    /*============================设备主子码流逻辑START========================*/
    public static String getPlayKey(String deviceId,String channelId,boolean deviceSwitchSubStream,boolean isSubStream){
        String key = null;
        if(deviceSwitchSubStream){
            key = CALLBACK_CMD_PLAY + isSubStream + deviceId + channelId;
        }else {
            key = CALLBACK_CMD_PLAY +deviceId + channelId;
        }
        return key;
    }
    public static String getSnapKey(String deviceId,String channelId,boolean deviceSwitchSubStream,boolean isSubStream){
        String key = null;
        if(deviceSwitchSubStream){
            key = CALLBACK_CMD_SNAP + isSubStream + deviceId + channelId;
        }else {
            key = CALLBACK_CMD_SNAP +deviceId + channelId;
        }
        return key;
    }
    /*============================设备主子码流逻辑END========================*/
}
src/main/java/com/genersoft/iot/vmp/gb28181/transmit/cmd/ISIPCommander.java
@@ -98,7 +98,7 @@
     * @param device  è§†é¢‘设备
     * @param channelId  é¢„览通道
     */
    void playStreamCmd(MediaServerItem mediaServerItem, SSRCInfo ssrcInfo, Device device, String channelId, ZlmHttpHookSubscribe.Event event, SipSubscribe.Event okEvent, SipSubscribe.Event errorEvent) throws InvalidArgumentException, SipException, ParseException;
    void playStreamCmd(MediaServerItem mediaServerItem, SSRCInfo ssrcInfo, Device device, String channelId,boolean isSubStream, ZlmHttpHookSubscribe.Event event, SipSubscribe.Event okEvent, SipSubscribe.Event errorEvent) throws InvalidArgumentException, SipException, ParseException;
    /**
     * è¯·æ±‚回放视频流
src/main/java/com/genersoft/iot/vmp/gb28181/transmit/cmd/impl/SIPCommander.java
@@ -266,7 +266,7 @@
     * @param errorEvent sip错误订阅
     */
    @Override
    public void playStreamCmd(MediaServerItem mediaServerItem, SSRCInfo ssrcInfo, Device device, String channelId,
    public void playStreamCmd(MediaServerItem mediaServerItem, SSRCInfo ssrcInfo, Device device, String channelId,boolean isSubStream,
                              ZlmHttpHookSubscribe.Event event, SipSubscribe.Event okEvent, SipSubscribe.Event errorEvent) throws InvalidArgumentException, SipException, ParseException {
        String stream = ssrcInfo.getStream();
@@ -341,6 +341,22 @@
            }
        }
        if( device.isSwitchPrimarySubStream() ){
            if("TP-LINK".equals(device.getManufacturer())){
                if (isSubStream){
                    content.append("a=streamMode:sub\r\n");
                }else {
                    content.append("a=streamMode:main\r\n");
                }
            }else {
                if (isSubStream){
                    content.append("a=streamprofile:1\r\n");
                }else {
                    content.append("a=streamprofile:0\r\n");
                }
            }
        }
        content.append("y=" + ssrcInfo.getSsrc() + "\r\n");//ssrc
        // f字段:f= v/编码格式/分辨率/帧率/码率类型/码率大小a/编码格式/码率大小/采样率
//            content.append("f=v/2/5/25/1/4000a/1/8/1" + "\r\n"); // æœªå‘现支持此特性的设备
@@ -356,7 +372,11 @@
            // è¿™é‡Œä¸ºä¾‹é¿å…ä¸€ä¸ªé€šé“的点播只有一个callID这个参数使用一个固定值
            ResponseEvent responseEvent = (ResponseEvent) e.event;
            SIPResponse response = (SIPResponse) responseEvent.getResponse();
            if(device.isSwitchPrimarySubStream()){
                streamSession.put(device.getDeviceId(), channelId, "switch-play", stream, ssrcInfo.getSsrc(), mediaServerItem.getId(), response, InviteSessionType.PLAY);
            }else {
            streamSession.put(device.getDeviceId(), channelId, "play", stream, ssrcInfo.getSsrc(), mediaServerItem.getId(), response, InviteSessionType.PLAY);
            }
            okEvent.response(e);
        });
    }
src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/ByeRequestProcessor.java
@@ -142,8 +142,13 @@
            // å¯èƒ½æ˜¯è®¾å¤‡ä¸»åŠ¨åœæ­¢
            Device device = storager.queryVideoDeviceByChannelId(platformGbId);
            if (device != null) {
                SsrcTransaction ssrcTransactionForPlay = null;
                if (device.isSwitchPrimarySubStream() ) {
                    ssrcTransactionForPlay = streamSession.getSsrcTransaction(device.getDeviceId(), channelId,  "switch-play", null);
                } else {
                storager.stopPlay(device.getDeviceId(), channelId);
                SsrcTransaction ssrcTransactionForPlay = streamSession.getSsrcTransaction(device.getDeviceId(), channelId, "play", null);
                    ssrcTransactionForPlay = streamSession.getSsrcTransaction(device.getDeviceId(), channelId, "play", null);
                }
                if (ssrcTransactionForPlay != null){
                    if (ssrcTransactionForPlay.getCallId().equals(callIdHeader.getCallId())){
                        // é‡Šæ”¾ssrc
@@ -153,10 +158,17 @@
                        }
                        streamSession.remove(device.getDeviceId(), channelId, ssrcTransactionForPlay.getStream());
                    }
                    InviteInfo inviteInfo = inviteStreamService.getInviteInfoByDeviceAndChannel(InviteSessionType.PLAY, device.getDeviceId(), channelId);
                    if (inviteInfo != null) {
                    InviteInfo inviteInfo = null;
                    if (device.isSwitchPrimarySubStream() ) {
                        String streamType = ssrcTransactionForPlay.getStream().split("_")[0];
                        boolean isSubStream = "sub".equals(streamType);
                        inviteInfo = inviteStreamService.getInviteInfoByDeviceAndChannel(InviteSessionType.PLAY, device.getDeviceId(), channelId,isSubStream);
                        inviteStreamService.removeInviteInfo(inviteInfo.getType(),inviteInfo.getDeviceId(),inviteInfo.getChannelId(),isSubStream,inviteInfo.getStream());
                    }else {
                        inviteInfo = inviteStreamService.getInviteInfoByDeviceAndChannel(InviteSessionType.PLAY, device.getDeviceId(), channelId);
                        inviteStreamService.removeInviteInfo(inviteInfo);
                    }
                    if (inviteInfo != null) {
                        if (inviteInfo.getStreamInfo() != null) {
                            mediaServerService.closeRTPServer(inviteInfo.getStreamInfo().getMediaServerId(), inviteInfo.getStream());
                        }
src/main/java/com/genersoft/iot/vmp/gb28181/transmit/event/request/impl/InviteRequestProcessor.java
@@ -489,7 +489,7 @@
                        }
                        sendRtpItem.setStreamId(streamId);
                        redisCatchStorage.updateSendRTPSever(sendRtpItem);
                        playService.play(mediaServerItem, device.getDeviceId(), channelId, ((code, msg, data) -> {
                        playService.play(mediaServerItem, device.getDeviceId(), channelId,false, ((code, msg, data) -> {
                            if (code == InviteErrorCode.SUCCESS.getCode()){
                                hookEvent.run(code, msg, data);
                            }else if (code == InviteErrorCode.ERROR_FOR_SIGNALLING_TIMEOUT.getCode() || code == InviteErrorCode.ERROR_FOR_STREAM_TIMEOUT.getCode()){
src/main/java/com/genersoft/iot/vmp/media/zlm/ZLMHttpHookListener.java
@@ -289,6 +289,7 @@
    @ResponseBody
    @PostMapping(value = "/on_stream_changed", produces = "application/json;charset=UTF-8")
    public HookResult onStreamChanged(@RequestBody OnStreamChangedHookParam param) {
        if (param.isRegist()) {
            logger.info("[ZLM HOOK] æµæ³¨å†Œ, {}->{}->{}/{}", param.getMediaServerId(), param.getSchema(), param.getApp(), param.getStream());
        } else {
@@ -310,11 +311,13 @@
            List<OnStreamChangedHookParam.MediaTrack> tracks = param.getTracks();
            // TODO é‡æž„此处逻辑
            boolean isPush = false;
            if (param.isRegist()) {
                // å¤„理流注册的鉴权信息
                if (param.getOriginType() == OriginType.RTMP_PUSH.ordinal()
                        || param.getOriginType() == OriginType.RTSP_PUSH.ordinal()
                        || param.getOriginType() == OriginType.RTC_PUSH.ordinal()) {
                    isPush = true;
                    StreamAuthorityInfo streamAuthorityInfo = redisCatchStorage.getStreamAuthorityInfo(param.getApp(), param.getStream());
                    if (streamAuthorityInfo == null) {
                        streamAuthorityInfo = StreamAuthorityInfo.getInstanceByHook(param);
@@ -328,7 +331,7 @@
                redisCatchStorage.removeStreamAuthorityInfo(param.getApp(), param.getStream());
            }
            if ("rtmp".equals(param.getSchema())) {
            if ("rtsp".equals(param.getSchema())) {
                // æ›´æ–°æµåª’体负载信息
                if (param.isRegist()) {
                    mediaServerService.addCount(param.getMediaServerId());
@@ -342,10 +345,19 @@
                }
                if ("rtp".equals(param.getApp()) && !param.isRegist()) {
                    if(param.getStream().split("_").length == 3){
                        boolean isSubStream = "sub".equals(param.getStream().split("_")[0]);
                        InviteInfo inviteInfo = inviteStreamService.getInviteInfoByStream(null, param.getStream(), isSubStream);
                        if(inviteInfo != null && (inviteInfo.getType() == InviteSessionType.PLAY )){
                            inviteStreamService.removeInviteInfo(inviteInfo.getType(),inviteInfo.getDeviceId(),
                                    inviteInfo.getChannelId(),inviteInfo.isSubStream(),inviteInfo.getStream());
                        }
                    }else {
                    InviteInfo inviteInfo = inviteStreamService.getInviteInfoByStream(null, param.getStream());
                    if (inviteInfo != null && (inviteInfo.getType() == InviteSessionType.PLAY || inviteInfo.getType() == InviteSessionType.PLAYBACK)) {
                        inviteStreamService.removeInviteInfo(inviteInfo);
                        storager.stopPlay(inviteInfo.getDeviceId(), inviteInfo.getChannelId());
                        }
                    }
                } else {
                    if (!"rtp".equals(param.getApp())) {
@@ -360,8 +372,6 @@
                            StreamInfo streamInfoByAppAndStream = mediaService.getStreamInfoByAppAndStream(mediaInfo,
                                    param.getApp(), param.getStream(), tracks, callId);
                            param.setStreamInfo(new StreamContent(streamInfoByAppAndStream));
                            // å¦‚果是拉流代理产生的,不需要写入推流
                            redisCatchStorage.addStream(mediaInfo, type, param.getApp(), param.getStream(), param);
                            if (param.getOriginType() == OriginType.RTSP_PUSH.ordinal()
                                    || param.getOriginType() == OriginType.RTMP_PUSH.ordinal()
@@ -450,6 +460,11 @@
            InviteInfo inviteInfo = inviteStreamService.getInviteInfoByStream(null, param.getStream());
            // ç‚¹æ’­
            if (inviteInfo != null) {
                // å½•像下载
                if (inviteInfo.getType() == InviteSessionType.DOWNLOAD) {
                    ret.put("close", false);
                    return ret;
                }
                // æ”¶åˆ°æ— äººè§‚看说明流也没有在往上级推送
                if (redisCatchStorage.isChannelSendingRTP(inviteInfo.getChannelId())) {
                    List<SendRtpItem> sendRtpItems = redisCatchStorage.querySendRTPServerByChnnelId(
@@ -467,18 +482,19 @@
                        }
                    }
                }
                if (userSetting.getStreamOnDemand()) {
                    // å½•像下载
                    if (inviteInfo.getType() == InviteSessionType.DOWNLOAD) {
                        ret.put("close", false);
                        return ret;
                    }
                    Device device = deviceService.getDevice(inviteInfo.getDeviceId());
                    if (device != null) {
                        try {
                            if (inviteStreamService.getInviteInfo(inviteInfo.getType(), inviteInfo.getDeviceId(), inviteInfo.getChannelId(), inviteInfo.getStream()) != null) {
                        InviteInfo info = null;
                        if(device.isSwitchPrimarySubStream()){
                            boolean isSubStream = "sub".equals(param.getStream().split("_")[0]);
                            info = inviteStreamService.getInviteInfo(inviteInfo.getType(), inviteInfo.getDeviceId(), inviteInfo.getChannelId(),isSubStream, inviteInfo.getStream());
                        }else {
                            info = inviteStreamService.getInviteInfo(inviteInfo.getType(), inviteInfo.getDeviceId(), inviteInfo.getChannelId(), inviteInfo.getStream());
                        }
                        if (info != null) {
                                cmder.streamByeCmd(device, inviteInfo.getChannelId(),
                                        inviteInfo.getStream(), null);
                            }
@@ -488,6 +504,11 @@
                        }
                    }
                if(device.isSwitchPrimarySubStream()){
                    boolean isSubStream = "sub".equals(param.getStream().split("_")[0]);
                    inviteStreamService.removeInviteInfo(inviteInfo.getType(), inviteInfo.getDeviceId(),
                            inviteInfo.getChannelId(),isSubStream, inviteInfo.getStream());
                }else {
                    inviteStreamService.removeInviteInfo(inviteInfo.getType(), inviteInfo.getDeviceId(),
                            inviteInfo.getChannelId(), inviteInfo.getStream());
                    storager.stopPlay(inviteInfo.getDeviceId(), inviteInfo.getChannelId());
@@ -499,7 +520,7 @@
            // æ‹‰æµä»£ç†
            StreamProxyItem streamProxyItem = streamProxyService.getStreamProxyByAppAndStream(param.getApp(), param.getStream());
            if (streamProxyItem != null) {
                if (streamProxyItem.isEnableRemoveNoneReader()) {
                if (streamProxyItem.isEnableDisableNoneReader()) {
                    // æ— äººè§‚看自动移除
                    ret.put("close", true);
                    streamProxyService.del(param.getApp(), param.getStream());
@@ -544,12 +565,26 @@
        if ("rtp".equals(param.getApp())) {
            String[] s = param.getStream().split("_");
            if (!mediaInfo.isRtpEnable() || s.length != 2) {
            if (!mediaInfo.isRtpEnable() ) {
                defaultResult.setResult(HookResult.SUCCESS());
                return defaultResult;
            }else if(s.length != 2 && s.length != 3 ){
                defaultResult.setResult(HookResult.SUCCESS());
                return defaultResult;
            }
            String deviceId = s[0];
            String channelId = s[1];
            String deviceId = null;
            String channelId = null;
            boolean isSubStream = false;
            if (s[0].length() < 20) {
                if ("sub".equals(s[0])) {
                    isSubStream = true;
                }
                deviceId = s[1];
                channelId = s[2];
            } else {
                deviceId = s[0];
                channelId = s[1];
            }
            Device device = redisCatchStorage.getDevice(deviceId);
            if (device == null) {
                defaultResult.setResult(new HookResult(ErrorCode.ERROR404.getCode(), ErrorCode.ERROR404.getMsg()));
@@ -563,7 +598,7 @@
            logger.info("[ZLM HOOK] æµæœªæ‰¾åˆ°, å‘起自动点播:{}->{}->{}/{}", param.getMediaServerId(), param.getSchema(), param.getApp(), param.getStream());
            RequestMessage msg = new RequestMessage();
            String key = DeferredResultHolder.CALLBACK_CMD_PLAY + deviceId + channelId;
            String key = DeferredResultHolder.getPlayKey(deviceId, channelId, device.isSwitchPrimarySubStream(), isSubStream);
            boolean exist = resultHolder.exist(key, null);
            msg.setKey(key);
            String uuid = UUID.randomUUID().toString();
@@ -581,7 +616,7 @@
            resultHolder.put(key, uuid, result);
            if (!exist) {
                playService.play(mediaInfo, deviceId, channelId, (code, message, data) -> {
                playService.play(mediaInfo, deviceId, channelId,isSubStream, (code, message, data) -> {
                    msg.setData(new HookResult(code, message));
                    resultHolder.invokeResult(msg);
                });
src/main/java/com/genersoft/iot/vmp/service/IInviteStreamService.java
@@ -4,6 +4,8 @@
import com.genersoft.iot.vmp.common.InviteSessionType;
import com.genersoft.iot.vmp.service.bean.ErrorCallback;
import java.util.List;
/**
 * è®°å½•国标点播的状态,包括实时预览,下载,录像回放
 */
@@ -70,4 +72,50 @@
     * ç»Ÿè®¡åŒä¸€ä¸ªzlm下的国标收流个数
     */
    int getStreamInfoCount(String mediaServerId);
    /*======================设备主子码流逻辑START=========================*/
    /**
     * èŽ·å–ç‚¹æ’­çš„çŠ¶æ€ä¿¡æ¯
     */
    InviteInfo getInviteInfoByDeviceAndChannel(InviteSessionType type,
                                               String deviceId,
                                               String channelId,boolean isSubStream);
    void removeInviteInfoByDeviceAndChannel(InviteSessionType inviteSessionType, String deviceId, String channelId,boolean isSubStream);
    InviteInfo getInviteInfo(InviteSessionType type,
                             String deviceId,
                             String channelId,
                             boolean isSubStream,
                             String stream);
    void removeInviteInfo(InviteSessionType type,
                          String deviceId,
                          String channelId,
                          boolean isSubStream,
                          String stream);
    void once(InviteSessionType type, String deviceId, String channelId,boolean isSubStream, String stream,  ErrorCallback<Object> callback);
    void call(InviteSessionType type, String deviceId, String channelId,boolean isSubStream, String stream,  int code, String msg, Object data);
    void updateInviteInfoSub(InviteInfo inviteInfo);
    /**
     * èŽ·å–ç‚¹æ’­çš„çŠ¶æ€ä¿¡æ¯
     */
    InviteInfo getInviteInfoByStream(InviteSessionType type, String stream,boolean isSubStream);
    /**
     * èŽ·å–ç‚¹æ’­çš„çŠ¶æ€ä¿¡æ¯
     */
    List<Object> getInviteInfos(InviteSessionType type,
                                String deviceId,
                                String channelId,
                                String stream);
    /*======================设备主子码流逻辑END=========================*/
}
src/main/java/com/genersoft/iot/vmp/service/IPlayService.java
@@ -16,9 +16,9 @@
 */
public interface IPlayService {
    void play(MediaServerItem mediaServerItem, SSRCInfo ssrcInfo, Device device, String channelId,
    void play(MediaServerItem mediaServerItem, SSRCInfo ssrcInfo, Device device, String channelId,boolean isSubStream,
              ErrorCallback<Object> callback);
    SSRCInfo play(MediaServerItem mediaServerItem, String deviceId, String channelId, ErrorCallback<Object> callback);
    SSRCInfo play(MediaServerItem mediaServerItem, String deviceId, String channelId,boolean isSubStream, ErrorCallback<Object> callback);
    MediaServerItem getNewMediaServerItem(Device device);
@@ -43,5 +43,5 @@
    void resumeRtp(String streamId) throws ServiceException, InvalidArgumentException, ParseException, SipException;
    void getSnap(String deviceId, String channelId, String fileName, ErrorCallback errorCallback);
    void getSnap(String deviceId, String channelId, String fileName,boolean isSubStream, ErrorCallback errorCallback);
}
src/main/java/com/genersoft/iot/vmp/service/impl/DeviceServiceImpl.java
@@ -1,14 +1,17 @@
package com.genersoft.iot.vmp.service.impl;
import com.genersoft.iot.vmp.common.InviteSessionType;
import com.genersoft.iot.vmp.common.VideoManagerConstants;
import com.genersoft.iot.vmp.conf.DynamicTask;
import com.genersoft.iot.vmp.conf.UserSetting;
import com.genersoft.iot.vmp.conf.exception.SsrcTransactionNotFoundException;
import com.genersoft.iot.vmp.gb28181.bean.*;
import com.genersoft.iot.vmp.gb28181.session.VideoStreamSessionManager;
import com.genersoft.iot.vmp.gb28181.task.ISubscribeTask;
import com.genersoft.iot.vmp.gb28181.task.impl.CatalogSubscribeTask;
import com.genersoft.iot.vmp.gb28181.task.impl.MobilePositionSubscribeTask;
import com.genersoft.iot.vmp.gb28181.transmit.cmd.ISIPCommander;
import com.genersoft.iot.vmp.gb28181.transmit.cmd.impl.SIPCommander;
import com.genersoft.iot.vmp.gb28181.transmit.event.request.impl.message.response.cmd.CatalogResponseMessageHandler;
import com.genersoft.iot.vmp.service.IDeviceChannelService;
import com.genersoft.iot.vmp.service.IDeviceService;
@@ -47,6 +50,8 @@
    private final static Logger logger = LoggerFactory.getLogger(DeviceServiceImpl.class);
    @Autowired
    private SIPCommander cmder;
    @Autowired
    private DynamicTask dynamicTask;
@@ -131,6 +136,10 @@
            }
            sync(device);
        }else {
            if (deviceInDb != null) {
                device.setSwitchPrimarySubStream(deviceInDb.isSwitchPrimarySubStream());
            }
            if(!device.isOnLine()){
                device.setOnLine(true);
                device.setCreateTime(now);
@@ -460,6 +469,22 @@
            logger.warn("更新设备时未找到设备信息");
            return;
        }
        if(deviceInStore.isSwitchPrimarySubStream() != device.isSwitchPrimarySubStream()){
            //当修改设备的主子码流开关时,需要校验是否存在流,如果存在流则直接关闭
            List<SsrcTransaction> ssrcTransactionForAll = streamSession.getSsrcTransactionForAll(device.getDeviceId(), null, null, null);
            if(ssrcTransactionForAll != null){
                for (SsrcTransaction ssrcTransaction: ssrcTransactionForAll) {
                    try {
                        cmder.streamByeCmd(device, ssrcTransaction.getChannelId(), ssrcTransaction.getStream(), null, null);
                    } catch (InvalidArgumentException | SsrcTransactionNotFoundException | ParseException | SipException e) {
                        throw new RuntimeException(e);
                    }
                }
            }
            deviceChannelMapper.clearPlay(device.getDeviceId());
            inviteStreamService.clearInviteInfo(device.getDeviceId());
        }
        if (!ObjectUtils.isEmpty(device.getName())) {
            deviceInStore.setName(device.getName());
        }
src/main/java/com/genersoft/iot/vmp/service/impl/InviteStreamServiceImpl.java
@@ -198,4 +198,164 @@
        }
        return count;
    }
    /*======================设备主子码流逻辑START=========================*/
    @Override
    public InviteInfo getInviteInfoByDeviceAndChannel(InviteSessionType type, String deviceId, String channelId, boolean isSubStream) {
        return getInviteInfo(type, deviceId, channelId,isSubStream, null);
    }
    @Override
    public void removeInviteInfoByDeviceAndChannel(InviteSessionType inviteSessionType, String deviceId, String channelId, boolean isSubStream) {
        removeInviteInfo(inviteSessionType, deviceId, channelId,isSubStream, null);
    }
    @Override
    public InviteInfo getInviteInfo(InviteSessionType type, String deviceId, String channelId,boolean isSubStream, String stream) {
        String key = VideoManagerConstants.INVITE_PREFIX +
                "_" + (type != null ? type : "*") +
                "_" + (isSubStream ? "sub" : "main") +
                "_" + (deviceId != null ? deviceId : "*") +
                "_" + (channelId != null ? channelId : "*") +
                "_" + (stream != null ? stream : "*");
        List<Object> scanResult = RedisUtil.scan(redisTemplate, key);
        if (scanResult.size() != 1) {
            return null;
        }
        return (InviteInfo) redisTemplate.opsForValue().get(scanResult.get(0));
    }
    @Override
    public void removeInviteInfo(InviteSessionType type, String deviceId, String channelId, boolean isSubStream, String stream) {
        String scanKey = VideoManagerConstants.INVITE_PREFIX +
                "_" + (type != null ? type : "*") +
                "_" + (isSubStream ? "sub" : "main") +
                "_" + (deviceId != null ? deviceId : "*") +
                "_" + (channelId != null ? channelId : "*") +
                "_" + (stream != null ? stream : "*");
        List<Object> scanResult = RedisUtil.scan(redisTemplate, scanKey);
        if (scanResult.size() > 0) {
            for (Object keyObj : scanResult) {
                String key = (String) keyObj;
                InviteInfo inviteInfo = (InviteInfo) redisTemplate.opsForValue().get(key);
                if (inviteInfo == null) {
                    continue;
                }
                redisTemplate.delete(key);
                inviteErrorCallbackMap.remove(buildKey(type, deviceId, channelId, inviteInfo.getStream()));
            }
        }
    }
    @Override
    public void once(InviteSessionType type, String deviceId, String channelId, boolean isSubStream, String stream, ErrorCallback<Object> callback) {
        String key = buildSubStreamKey(type, deviceId, channelId,isSubStream, stream);
        List<ErrorCallback<Object>> callbacks = inviteErrorCallbackMap.get(key);
        if (callbacks == null) {
            callbacks = new CopyOnWriteArrayList<>();
            inviteErrorCallbackMap.put(key, callbacks);
        }
        callbacks.add(callback);
    }
    @Override
    public void call(InviteSessionType type, String deviceId, String channelId, boolean isSubStream, String stream, int code, String msg, Object data) {
        String key = buildSubStreamKey(type, deviceId, channelId,isSubStream, stream);
        List<ErrorCallback<Object>> callbacks = inviteErrorCallbackMap.get(key);
        if (callbacks == null) {
            return;
        }
        for (ErrorCallback<Object> callback : callbacks) {
            callback.run(code, msg, data);
        }
        inviteErrorCallbackMap.remove(key);
    }
    private String buildSubStreamKey(InviteSessionType type, String deviceId, String channelId, boolean isSubStream, String stream) {
        String key = type + "_" + (isSubStream ? "sub":"main") + "_" +  deviceId + "_" + channelId;
        // å¦‚æžœssrc为null那么可以实现一个通道只能一次操作,ssrc不为null则可以支持一个通道多次invite
        if (stream != null) {
            key += ("_" + stream);
        }
        return key;
    }
    @Override
    public void updateInviteInfoSub(InviteInfo inviteInfo) {
        if (inviteInfo == null || (inviteInfo.getDeviceId() == null || inviteInfo.getChannelId() == null)) {
            logger.warn("[更新Invite信息],参数不全: {}", JSON.toJSON(inviteInfo));
            return;
        }
        InviteInfo inviteInfoForUpdate = null;
        if (InviteSessionStatus.ready == inviteInfo.getStatus()) {
            if (inviteInfo.getDeviceId() == null
                    || inviteInfo.getChannelId() == null
                    || inviteInfo.getType() == null
                    || inviteInfo.getStream() == null
            ) {
                return;
            }
            inviteInfoForUpdate = inviteInfo;
        } else {
            InviteInfo inviteInfoInRedis = getInviteInfo(inviteInfo.getType(), inviteInfo.getDeviceId(),
                    inviteInfo.getChannelId(),inviteInfo.isSubStream(), inviteInfo.getStream());
            if (inviteInfoInRedis == null) {
                logger.warn("[更新Invite信息],未从缓存中读取到Invite信息: deviceId: {}, channel: {}, stream: {}",
                        inviteInfo.getDeviceId(), inviteInfo.getChannelId(), inviteInfo.getStream());
                return;
            }
            if (inviteInfo.getStreamInfo() != null) {
                inviteInfoInRedis.setStreamInfo(inviteInfo.getStreamInfo());
            }
            if (inviteInfo.getSsrcInfo() != null) {
                inviteInfoInRedis.setSsrcInfo(inviteInfo.getSsrcInfo());
            }
            if (inviteInfo.getStreamMode() != null) {
                inviteInfoInRedis.setStreamMode(inviteInfo.getStreamMode());
            }
            if (inviteInfo.getReceiveIp() != null) {
                inviteInfoInRedis.setReceiveIp(inviteInfo.getReceiveIp());
            }
            if (inviteInfo.getReceivePort() != null) {
                inviteInfoInRedis.setReceivePort(inviteInfo.getReceivePort());
            }
            if (inviteInfo.getStatus() != null) {
                inviteInfoInRedis.setStatus(inviteInfo.getStatus());
            }
            inviteInfoForUpdate = inviteInfoInRedis;
        }
        String key = VideoManagerConstants.INVITE_PREFIX +
                "_" + inviteInfoForUpdate.getType() +
                "_" + (inviteInfoForUpdate.isSubStream() ? "sub":"main") +
                "_" + inviteInfoForUpdate.getDeviceId() +
                "_" + inviteInfoForUpdate.getChannelId() +
                "_" + inviteInfoForUpdate.getStream();
        redisTemplate.opsForValue().set(key, inviteInfoForUpdate);
    }
    @Override
    public InviteInfo getInviteInfoByStream(InviteSessionType type, String stream, boolean isSubStream) {
        return getInviteInfo(type, null, null,isSubStream, stream);
    }
    @Override
    public List<Object> getInviteInfos(InviteSessionType type, String deviceId, String channelId, String stream) {
        String key = VideoManagerConstants.INVITE_PREFIX +
                "_" + (type != null ? type : "*") +
                "_" + (deviceId != null ? deviceId : "*") +
                "_" + (channelId != null ? channelId : "*") +
                "_" + (stream != null ? stream : "*");
        List<Object> scanResult = RedisUtil.scan(redisTemplate, key);
        return scanResult;
    }
    /*======================设备主子码流逻辑END=========================*/
}
src/main/java/com/genersoft/iot/vmp/service/impl/PlayServiceImpl.java
@@ -18,7 +18,6 @@
import com.genersoft.iot.vmp.gb28181.transmit.callback.DeferredResultHolder;
import com.genersoft.iot.vmp.gb28181.transmit.cmd.impl.SIPCommander;
import com.genersoft.iot.vmp.gb28181.transmit.cmd.impl.SIPCommanderFroPlatform;
import com.genersoft.iot.vmp.gb28181.utils.SipUtils;
import com.genersoft.iot.vmp.media.zlm.AssistRESTfulUtils;
import com.genersoft.iot.vmp.media.zlm.ZLMRESTfulUtils;
import com.genersoft.iot.vmp.media.zlm.ZLMRTPServerFactory;
@@ -116,28 +115,43 @@
    @Override
    public SSRCInfo play(MediaServerItem mediaServerItem, String deviceId, String channelId, ErrorCallback<Object> callback) {
    public SSRCInfo play(MediaServerItem mediaServerItem, String deviceId, String channelId,boolean isSubStream, ErrorCallback<Object> callback) {
        if (mediaServerItem == null) {
            throw new ControllerException(ErrorCode.ERROR100.getCode(), "未找到可用的zlm");
        }
        Device device = redisCatchStorage.getDevice(deviceId);
        InviteInfo inviteInfo = inviteStreamService.getInviteInfoByDeviceAndChannel(InviteSessionType.PLAY, deviceId, channelId);
        InviteInfo inviteInfo;
        if(device.isSwitchPrimarySubStream()){
            inviteInfo = inviteStreamService.getInviteInfoByDeviceAndChannel(InviteSessionType.PLAY, deviceId, channelId,isSubStream);
        }else {
            inviteInfo = inviteStreamService.getInviteInfoByDeviceAndChannel(InviteSessionType.PLAY, deviceId, channelId);
        }
        if (inviteInfo != null ) {
            if (inviteInfo.getStreamInfo() == null) {
                // ç‚¹æ’­å‘起了但是尚未成功, ä»…注册回调等待结果即可
                if(device.isSwitchPrimarySubStream()){
                    inviteStreamService.once(InviteSessionType.PLAY, deviceId, channelId,isSubStream, null, callback);
                }else {
                inviteStreamService.once(InviteSessionType.PLAY, deviceId, channelId, null, callback);
                }
                return inviteInfo.getSsrcInfo();
            }else {
                StreamInfo streamInfo = inviteInfo.getStreamInfo();
                String streamId = streamInfo.getStream();
                if (streamId == null) {
                    callback.run(InviteErrorCode.ERROR_FOR_CATCH_DATA.getCode(), "点播失败, redis缓存streamId等于null", null);
                    if(device.isSwitchPrimarySubStream()){
                        inviteStreamService.call(InviteSessionType.PLAY, device.getDeviceId(), channelId,isSubStream, null,
                                InviteErrorCode.ERROR_FOR_CATCH_DATA.getCode(),
                                "点播失败, redis缓存streamId等于null",
                                null);
                    }else {
                    inviteStreamService.call(InviteSessionType.PLAY, device.getDeviceId(), channelId, null,
                            InviteErrorCode.ERROR_FOR_CATCH_DATA.getCode(),
                            "点播失败, redis缓存streamId等于null",
                            null);
                    }
                    return inviteInfo.getSsrcInfo();
                }
                String mediaServerId = streamInfo.getMediaServerId();
@@ -146,41 +160,64 @@
                Boolean ready = zlmrtpServerFactory.isStreamReady(mediaInfo, "rtp", streamId);
                if (ready != null && ready) {
                    callback.run(InviteErrorCode.SUCCESS.getCode(), InviteErrorCode.SUCCESS.getMsg(), streamInfo);
                    if(device.isSwitchPrimarySubStream()){
                        inviteStreamService.call(InviteSessionType.PLAY, device.getDeviceId(), channelId,isSubStream, null,
                                InviteErrorCode.SUCCESS.getCode(),
                                InviteErrorCode.SUCCESS.getMsg(),
                                streamInfo);
                    }else {
                    inviteStreamService.call(InviteSessionType.PLAY, device.getDeviceId(), channelId, null,
                            InviteErrorCode.SUCCESS.getCode(),
                            InviteErrorCode.SUCCESS.getMsg(),
                            streamInfo);
                    }
                    return inviteInfo.getSsrcInfo();
                }else {
                    // ç‚¹æ’­å‘起了但是尚未成功, ä»…注册回调等待结果即可
                    if(device.isSwitchPrimarySubStream()) {
                    inviteStreamService.once(InviteSessionType.PLAY, deviceId, channelId, null, callback);
                    storager.stopPlay(streamInfo.getDeviceID(), streamInfo.getChannelId());
                    inviteStreamService.removeInviteInfoByDeviceAndChannel(InviteSessionType.PLAY, deviceId, channelId);
                    }else {
                        inviteStreamService.once(InviteSessionType.PLAY, deviceId, channelId,isSubStream, null, callback);
                        inviteStreamService.removeInviteInfoByDeviceAndChannel(InviteSessionType.PLAY, deviceId, channelId,isSubStream);
                    }
                }
            }
        }
        String streamId = null;
        if (mediaServerItem.isRtpEnable()) {
            if(device.isSwitchPrimarySubStream()){
                streamId = StreamInfo.getPlayStream(deviceId, channelId, isSubStream);
            }else {
            streamId = String.format("%s_%s", device.getDeviceId(), channelId);
            }
        }
        SSRCInfo ssrcInfo = mediaServerService.openRTPServer(mediaServerItem, streamId, null, device.isSsrcCheck(),  false, 0, false, device.getStreamModeForParam());
        if (ssrcInfo == null) {
            callback.run(InviteErrorCode.ERROR_FOR_RESOURCE_EXHAUSTION.getCode(), InviteErrorCode.ERROR_FOR_RESOURCE_EXHAUSTION.getMsg(), null);
            if(device.isSwitchPrimarySubStream()){
                inviteStreamService.call(InviteSessionType.PLAY, device.getDeviceId(), channelId,isSubStream, null,
                        InviteErrorCode.ERROR_FOR_RESOURCE_EXHAUSTION.getCode(),
                        InviteErrorCode.ERROR_FOR_RESOURCE_EXHAUSTION.getMsg(),
                        null);
            }else {
            inviteStreamService.call(InviteSessionType.PLAY, device.getDeviceId(), channelId, null,
                    InviteErrorCode.ERROR_FOR_RESOURCE_EXHAUSTION.getCode(),
                    InviteErrorCode.ERROR_FOR_RESOURCE_EXHAUSTION.getMsg(),
                    null);
            }
            return null;
        }
        // TODO è®°å½•点播的状态
        play(mediaServerItem, ssrcInfo, device, channelId, callback);
        play(mediaServerItem, ssrcInfo, device, channelId,isSubStream, callback);
        return ssrcInfo;
    }
    @Override
    public void play(MediaServerItem mediaServerItem, SSRCInfo ssrcInfo, Device device, String channelId,
    public void play(MediaServerItem mediaServerItem, SSRCInfo ssrcInfo, Device device, String channelId,boolean isSubStream,
                     ErrorCallback<Object> callback) {
        if (mediaServerItem == null || ssrcInfo == null) {
@@ -189,21 +226,11 @@
                    null);
            return;
        }
        logger.info("\r\n" +
                " [点播开始] \r\n" +
                "deviceId  : {}, \r\n" +
                "channelId : {},\r\n" +
                "收流端口    : {}, \r\n" +
                "收流模式    : {}, \r\n" +
                "SSRC      : {}, \r\n" +
                "SSRC校验   ï¼š{}",
                device.getDeviceId(),
                channelId,
                ssrcInfo.getPort(),
                device.getStreamMode(),
                ssrcInfo.getSsrc(),
                device.isSsrcCheck());
        if( device.isSwitchPrimarySubStream() ){
            logger.info("[点播开始] deviceId: {}, channelId: {},码流类型:{},收流端口: {}, æ”¶æµæ¨¡å¼ï¼š{}, SSRC: {}, SSRC校验:{}", device.getDeviceId(), channelId,isSubStream ? "辅码流" : "主码流", ssrcInfo.getPort(), device.getStreamMode(), ssrcInfo.getSsrc(), device.isSsrcCheck());
        }else {
            logger.info("[点播开始] deviceId: {}, channelId: {},收流端口: {}, æ”¶æµæ¨¡å¼ï¼š{}, SSRC: {}, SSRC校验:{}", device.getDeviceId(), channelId, ssrcInfo.getPort(), device.getStreamMode(), ssrcInfo.getSsrc(), device.isSsrcCheck());
        }
        //端口获取失败的ssrcInfo æ²¡æœ‰å¿…要发送点播指令
        if (ssrcInfo.getPort() <= 0) {
            logger.info("[点播端口分配异常],deviceId={},channelId={},ssrcInfo={}", device.getDeviceId(), channelId, ssrcInfo);
@@ -212,23 +239,50 @@
            streamSession.remove(device.getDeviceId(), channelId, ssrcInfo.getStream());
            callback.run(InviteErrorCode.ERROR_FOR_RESOURCE_EXHAUSTION.getCode(), "点播端口分配异常", null);
            if(device.isSwitchPrimarySubStream()){
                inviteStreamService.call(InviteSessionType.PLAY, device.getDeviceId(), channelId,isSubStream, null,
                        InviteErrorCode.ERROR_FOR_RESOURCE_EXHAUSTION.getCode(), "点播端口分配异常", null);
            }else {
            inviteStreamService.call(InviteSessionType.PLAY, device.getDeviceId(), channelId, null,
                    InviteErrorCode.ERROR_FOR_RESOURCE_EXHAUSTION.getCode(), "点播端口分配异常", null);
            }
            return;
        }
        // åˆå§‹åŒ–redis中的invite消息状态
        InviteInfo inviteInfo = InviteInfo.getinviteInfo(device.getDeviceId(), channelId, ssrcInfo.getStream(), ssrcInfo,
        InviteInfo inviteInfo;
        if(device.isSwitchPrimarySubStream()){
            // åˆå§‹åŒ–redis中的invite消息状态
            inviteInfo = InviteInfo.getInviteInfo(device.getDeviceId(), channelId,isSubStream, ssrcInfo.getStream(), ssrcInfo,
                    mediaServerItem.getSdpIp(), ssrcInfo.getPort(), device.getStreamMode(), InviteSessionType.PLAY,
                    InviteSessionStatus.ready);
            inviteStreamService.updateInviteInfoSub(inviteInfo);
        }else {
            // åˆå§‹åŒ–redis中的invite消息状态
            inviteInfo = InviteInfo.getinviteInfo(device.getDeviceId(), channelId, ssrcInfo.getStream(), ssrcInfo,
                mediaServerItem.getSdpIp(), ssrcInfo.getPort(), device.getStreamMode(), InviteSessionType.PLAY,
                InviteSessionStatus.ready);
        inviteStreamService.updateInviteInfo(inviteInfo);
        }
        // è¶…时处理
        String timeOutTaskKey = UUID.randomUUID().toString();
        dynamicTask.startDelay(timeOutTaskKey, () -> {
            // æ‰§è¡Œè¶…时任务时查询是否已经成功,成功了则不执行超时任务,防止超时任务取消失败的情况
            InviteInfo inviteInfoForTimeOut = inviteStreamService.getInviteInfoByDeviceAndChannel(InviteSessionType.PLAY, device.getDeviceId(), channelId);
            InviteInfo inviteInfoForTimeOut;
            if(device.isSwitchPrimarySubStream()){
                // åˆå§‹åŒ–redis中的invite消息状态
                inviteInfoForTimeOut = inviteStreamService.getInviteInfoByDeviceAndChannel(InviteSessionType.PLAY, device.getDeviceId(), channelId,isSubStream);
            }else {
                // åˆå§‹åŒ–redis中的invite消息状态
                inviteInfoForTimeOut = inviteStreamService.getInviteInfoByDeviceAndChannel(InviteSessionType.PLAY, device.getDeviceId(), channelId);
            }
            if (inviteInfoForTimeOut == null || inviteInfoForTimeOut.getStreamInfo() == null) {
                if( device.isSwitchPrimarySubStream()){
                    logger.info("[点播超时] æ”¶æµè¶…æ—¶ deviceId: {}, channelId: {},码流类型:{},端口:{}, SSRC: {}", device.getDeviceId(), channelId,isSubStream ? "辅码流" : "主码流", ssrcInfo.getPort(), ssrcInfo.getSsrc());
                }else {
                logger.info("[点播超时] æ”¶æµè¶…æ—¶ deviceId: {}, channelId: {},端口:{}, SSRC: {}", device.getDeviceId(), channelId, ssrcInfo.getPort(), ssrcInfo.getSsrc());
                }
                // ç‚¹æ’­è¶…时回复BYE åŒæ—¶é‡Šæ”¾ssrc以及此次点播的资源
//                InviteInfo inviteInfoForTimeout = inviteStreamService.getInviteInfoByDeviceAndChannel(InviteSessionType.play, device.getDeviceId(), channelId);
//                if (inviteInfoForTimeout == null) {
@@ -240,10 +294,16 @@
//                    // TODO å‘送cancel
//                }
                callback.run(InviteErrorCode.ERROR_FOR_STREAM_TIMEOUT.getCode(), InviteErrorCode.ERROR_FOR_STREAM_TIMEOUT.getMsg(), null);
                if( device.isSwitchPrimarySubStream()){
                    inviteStreamService.call(InviteSessionType.PLAY, device.getDeviceId(), channelId,isSubStream, null,
                            InviteErrorCode.ERROR_FOR_STREAM_TIMEOUT.getCode(), InviteErrorCode.ERROR_FOR_STREAM_TIMEOUT.getMsg(), null);
                    inviteStreamService.removeInviteInfoByDeviceAndChannel(InviteSessionType.PLAY, device.getDeviceId(), channelId,isSubStream);
                }else {
                inviteStreamService.call(InviteSessionType.PLAY, device.getDeviceId(), channelId, null,
                        InviteErrorCode.ERROR_FOR_STREAM_TIMEOUT.getCode(), InviteErrorCode.ERROR_FOR_STREAM_TIMEOUT.getMsg(), null);
                inviteStreamService.removeInviteInfoByDeviceAndChannel(InviteSessionType.PLAY, device.getDeviceId(), channelId);
                }
                try {
                    cmder.streamByeCmd(device, channelId, ssrcInfo.getStream(), null);
                } catch (InvalidArgumentException | ParseException | SipException | SsrcTransactionNotFoundException e) {
@@ -261,25 +321,42 @@
        }, userSetting.getPlayTimeout());
        try {
            cmder.playStreamCmd(mediaServerItem, ssrcInfo, device, channelId, (MediaServerItem mediaServerItemInuse, JSONObject response) -> {
            cmder.playStreamCmd(mediaServerItem, ssrcInfo, device, channelId,isSubStream, (MediaServerItem mediaServerItemInuse, JSONObject response) -> {
                logger.info("收到订阅消息: " + response.toJSONString());
                dynamicTask.stop(timeOutTaskKey);
                // hook响应
                StreamInfo streamInfo = onPublishHandlerForPlay(mediaServerItemInuse, response, device.getDeviceId(), channelId);
                StreamInfo streamInfo = onPublishHandlerForPlay(mediaServerItemInuse, response, device.getDeviceId(), channelId,isSubStream);
                if (streamInfo == null){
                    callback.run(InviteErrorCode.ERROR_FOR_STREAM_PARSING_EXCEPTIONS.getCode(),
                            InviteErrorCode.ERROR_FOR_STREAM_PARSING_EXCEPTIONS.getMsg(), null);
                    if( device.isSwitchPrimarySubStream()){
                        inviteStreamService.call(InviteSessionType.PLAY, device.getDeviceId(), channelId,isSubStream, null,
                                InviteErrorCode.ERROR_FOR_STREAM_PARSING_EXCEPTIONS.getCode(),
                                InviteErrorCode.ERROR_FOR_STREAM_PARSING_EXCEPTIONS.getMsg(), null);
                    }else {
                    inviteStreamService.call(InviteSessionType.PLAY, device.getDeviceId(), channelId, null,
                            InviteErrorCode.ERROR_FOR_STREAM_PARSING_EXCEPTIONS.getCode(),
                            InviteErrorCode.ERROR_FOR_STREAM_PARSING_EXCEPTIONS.getMsg(), null);
                    }
                    return;
                }
                callback.run(InviteErrorCode.SUCCESS.getCode(), InviteErrorCode.SUCCESS.getMsg(), streamInfo);
                if( device.isSwitchPrimarySubStream()){
                    inviteStreamService.call(InviteSessionType.PLAY, device.getDeviceId(), channelId,isSubStream, null,
                            InviteErrorCode.SUCCESS.getCode(),
                            InviteErrorCode.SUCCESS.getMsg(),
                            streamInfo);
                }else {
                inviteStreamService.call(InviteSessionType.PLAY, device.getDeviceId(), channelId, null,
                        InviteErrorCode.SUCCESS.getCode(),
                        InviteErrorCode.SUCCESS.getMsg(),
                        streamInfo);
                }
                if( device.isSwitchPrimarySubStream() ){
                    logger.info("[点播成功] deviceId: {}, channelId: {},码流类型:{}", device.getDeviceId(), channelId,isSubStream ? "辅码流" : "主码流");
                }else {
                logger.info("[点播成功] deviceId: {}, channelId: {}", device.getDeviceId(), channelId);
                }
                String streamUrl;
                if (mediaServerItemInuse.getRtspPort() != 0) {
                    streamUrl = String.format("rtsp://127.0.0.1:%s/%s/%s", mediaServerItemInuse.getRtspPort(), "rtp",  ssrcInfo.getStream());
@@ -298,16 +375,17 @@
                ResponseEvent responseEvent = (ResponseEvent) event.event;
                String contentString = new String(responseEvent.getResponse().getRawContent());
                // èŽ·å–ssrc
                String ssrcInResponse = SipUtils.getSsrcFromSdp(contentString);
                int ssrcIndex = contentString.indexOf("y=");
                // æ£€æŸ¥æ˜¯å¦æœ‰y字段
                if (ssrcInResponse != null) {
                if (ssrcIndex >= 0) {
                    //ssrc规定长度为10字节,不取余下长度以避免后续还有“f=”字段 TODO åŽç»­å¯¹ä¸è§„范的非10位ssrc兼容
                    String ssrcInResponse = contentString.substring(ssrcIndex + 2, ssrcIndex + 12).trim();
                    // æŸ¥è¯¢åˆ°ssrc不一致且开启了ssrc校验则需要针对处理
                    if (ssrcInfo.getSsrc().equals(ssrcInResponse)) {
                        if (device.getStreamMode().equalsIgnoreCase("TCP-ACTIVE")) {
                            String substring = contentString.substring(0, contentString.indexOf("y="));
                            try {
                                Gb28181Sdp gb28181Sdp = SipUtils.parseSDP(contentString);
                                SessionDescription sdp = gb28181Sdp.getBaseSdb();
                                SessionDescription sdp = SdpFactory.getInstance().createSessionDescription(substring);
                                int port = -1;
                                Vector mediaDescriptions = sdp.getMediaDescriptions(true);
                                for (Object description : mediaDescriptions) {
@@ -334,21 +412,24 @@
                                callback.run(InviteErrorCode.ERROR_FOR_SDP_PARSING_EXCEPTIONS.getCode(),
                                        InviteErrorCode.ERROR_FOR_SDP_PARSING_EXCEPTIONS.getMsg(), null);
                                if(device.isSwitchPrimarySubStream()){
                                    inviteStreamService.call(InviteSessionType.PLAY, device.getDeviceId(), channelId,isSubStream, null,
                                            InviteErrorCode.ERROR_FOR_SDP_PARSING_EXCEPTIONS.getCode(),
                                            InviteErrorCode.ERROR_FOR_SDP_PARSING_EXCEPTIONS.getMsg(), null);
                                }else {
                                inviteStreamService.call(InviteSessionType.PLAY, device.getDeviceId(), channelId, null,
                                        InviteErrorCode.ERROR_FOR_SDP_PARSING_EXCEPTIONS.getCode(),
                                        InviteErrorCode.ERROR_FOR_SDP_PARSING_EXCEPTIONS.getMsg(), null);
                            }
                        }
                        }
                        return;
                    }
                    logger.info("[点播消息] æ”¶åˆ°invite 200, å‘现下级自定义了ssrc: {}", ssrcInResponse);
                    if (!mediaServerItem.isRtpEnable() || device.isSsrcCheck()) {
                        logger.info("[点播消息] SSRC修正 {}->{}", ssrcInfo.getSsrc(), ssrcInResponse);
                        // é‡Šæ”¾ssrc
                        mediaServerService.releaseSsrc(mediaServerItem.getId(), ssrcInfo.getSsrc());
                        // å•端口模式streamId也有变化,重新设置监听即可
                        if (!mediaServerItem.isRtpEnable()) {
                            // æ·»åŠ è®¢é˜…
@@ -361,21 +442,34 @@
                                logger.info("[ZLM HOOK] ssrc修正后收到订阅消息: " + response.toJSONString());
                                dynamicTask.stop(timeOutTaskKey);
                                // hook响应
                                StreamInfo streamInfo = onPublishHandlerForPlay(mediaServerItemInUse, response, device.getDeviceId(), channelId);
                                StreamInfo streamInfo = onPublishHandlerForPlay(mediaServerItemInUse, response, device.getDeviceId(), channelId,isSubStream);
                                if (streamInfo == null){
                                    callback.run(InviteErrorCode.ERROR_FOR_STREAM_PARSING_EXCEPTIONS.getCode(),
                                            InviteErrorCode.ERROR_FOR_STREAM_PARSING_EXCEPTIONS.getMsg(), null);
                                    if( device.isSwitchPrimarySubStream()){
                                        inviteStreamService.call(InviteSessionType.PLAY, device.getDeviceId(), channelId,isSubStream, null,
                                                InviteErrorCode.ERROR_FOR_STREAM_PARSING_EXCEPTIONS.getCode(),
                                                InviteErrorCode.ERROR_FOR_STREAM_PARSING_EXCEPTIONS.getMsg(), null);
                                    }else {
                                    inviteStreamService.call(InviteSessionType.PLAY, device.getDeviceId(), channelId, null,
                                            InviteErrorCode.ERROR_FOR_STREAM_PARSING_EXCEPTIONS.getCode(),
                                            InviteErrorCode.ERROR_FOR_STREAM_PARSING_EXCEPTIONS.getMsg(), null);
                                    }
                                    return;
                                }
                                callback.run(InviteErrorCode.SUCCESS.getCode(),
                                        InviteErrorCode.SUCCESS.getMsg(), streamInfo);
                                if( device.isSwitchPrimarySubStream()){
                                    inviteStreamService.call(InviteSessionType.PLAY, device.getDeviceId(), channelId,isSubStream, null,
                                            InviteErrorCode.SUCCESS.getCode(),
                                            InviteErrorCode.SUCCESS.getMsg(),
                                            streamInfo);
                                }else {
                                inviteStreamService.call(InviteSessionType.PLAY, device.getDeviceId(), channelId, null,
                                        InviteErrorCode.SUCCESS.getCode(),
                                        InviteErrorCode.SUCCESS.getMsg(),
                                        streamInfo);
                                }
                            });
                            return;
                        }
@@ -391,14 +485,22 @@
                            }
                            dynamicTask.stop(timeOutTaskKey);
                            // é‡Šæ”¾ssrc
                            mediaServerService.releaseSsrc(mediaServerItem.getId(), ssrcInfo.getSsrc());
                            streamSession.remove(device.getDeviceId(), channelId, ssrcInfo.getStream());
                            callback.run(InviteErrorCode.ERROR_FOR_RESET_SSRC.getCode(),
                                    "下级自定义了ssrc,重新设置收流信息失败", null);
                            if( device.isSwitchPrimarySubStream()){
                                inviteStreamService.call(InviteSessionType.PLAY, device.getDeviceId(), channelId,isSubStream, null,
                                        InviteErrorCode.ERROR_FOR_RESET_SSRC.getCode(),
                                        "下级自定义了ssrc,重新设置收流信息失败", null);
                            }else {
                            inviteStreamService.call(InviteSessionType.PLAY, device.getDeviceId(), channelId, null,
                                    InviteErrorCode.ERROR_FOR_RESET_SSRC.getCode(),
                                    "下级自定义了ssrc,重新设置收流信息失败", null);
                            }
                        }else {
                            ssrcInfo.setSsrc(ssrcInResponse);
@@ -409,7 +511,11 @@
                        logger.info("[点播消息] æ”¶åˆ°invite 200, ä¸‹çº§è‡ªå®šä¹‰äº†ssrc, ä½†æ˜¯å½“前模式无需修正");
                    }
                }
                if(device.isSwitchPrimarySubStream()){
                    inviteStreamService.updateInviteInfoSub(inviteInfo);
                }else {
                inviteStreamService.updateInviteInfo(inviteInfo);
                }
            }, (event) -> {
                dynamicTask.stop(timeOutTaskKey);
                mediaServerService.closeRTPServer(mediaServerItem, ssrcInfo.getStream());
@@ -420,11 +526,19 @@
                callback.run(InviteErrorCode.ERROR_FOR_SIGNALLING_ERROR.getCode(),
                        String.format("点播失败, é”™è¯¯ç ï¼š %s, %s", event.statusCode, event.msg), null);
                if( device.isSwitchPrimarySubStream()){
                    inviteStreamService.call(InviteSessionType.PLAY, device.getDeviceId(), channelId,isSubStream, null,
                            InviteErrorCode.ERROR_FOR_RESET_SSRC.getCode(),
                            String.format("点播失败, é”™è¯¯ç ï¼š %s, %s", event.statusCode, event.msg), null);
                    inviteStreamService.removeInviteInfoByDeviceAndChannel(InviteSessionType.PLAY, device.getDeviceId(), channelId,isSubStream);
                }else {
                inviteStreamService.call(InviteSessionType.PLAY, device.getDeviceId(), channelId, null,
                        InviteErrorCode.ERROR_FOR_RESET_SSRC.getCode(),
                        String.format("点播失败, é”™è¯¯ç ï¼š %s, %s", event.statusCode, event.msg), null);
                inviteStreamService.removeInviteInfoByDeviceAndChannel(InviteSessionType.PLAY, device.getDeviceId(), channelId);
                }
            });
        } catch (InvalidArgumentException | SipException | ParseException e) {
@@ -438,6 +552,13 @@
            callback.run(InviteErrorCode.ERROR_FOR_SIP_SENDING_FAILED.getCode(),
                    InviteErrorCode.ERROR_FOR_SIP_SENDING_FAILED.getMsg(), null);
            if( device.isSwitchPrimarySubStream()){
                inviteStreamService.call(InviteSessionType.PLAY, device.getDeviceId(), channelId,isSubStream, null,
                        InviteErrorCode.ERROR_FOR_SIP_SENDING_FAILED.getCode(),
                        InviteErrorCode.ERROR_FOR_SIP_SENDING_FAILED.getMsg(), null);
                inviteStreamService.removeInviteInfoByDeviceAndChannel(InviteSessionType.PLAY, device.getDeviceId(), channelId,isSubStream);
            }else {
            inviteStreamService.call(InviteSessionType.PLAY, device.getDeviceId(), channelId, null,
                    InviteErrorCode.ERROR_FOR_SIP_SENDING_FAILED.getCode(),
                    InviteErrorCode.ERROR_FOR_SIP_SENDING_FAILED.getMsg(), null);
@@ -445,20 +566,37 @@
            inviteStreamService.removeInviteInfoByDeviceAndChannel(InviteSessionType.PLAY, device.getDeviceId(), channelId);
        }
    }
    }
    private StreamInfo onPublishHandlerForPlay(MediaServerItem mediaServerItem, JSONObject response, String deviceId, String channelId) {
        StreamInfo streamInfo = onPublishHandler(mediaServerItem, response, deviceId, channelId);
    private StreamInfo onPublishHandlerForPlay(MediaServerItem mediaServerItem, JSONObject response, String deviceId, String channelId,boolean isSubStream) {
        StreamInfo streamInfo = null;
        Device device = redisCatchStorage.getDevice(deviceId);
        if( device.isSwitchPrimarySubStream() ){
            streamInfo = onPublishHandler(mediaServerItem, response, deviceId, channelId,isSubStream);
        }else {
            streamInfo = onPublishHandler(mediaServerItem, response, deviceId, channelId);
        }
        if (streamInfo != null) {
            InviteInfo inviteInfo;
            if(device.isSwitchPrimarySubStream()){
                inviteInfo = inviteStreamService.getInviteInfoByDeviceAndChannel(InviteSessionType.PLAY, deviceId, channelId,isSubStream);
            }else {
            DeviceChannel deviceChannel = storager.queryChannel(deviceId, channelId);
            if (deviceChannel != null) {
                deviceChannel.setStreamId(streamInfo.getStream());
                storager.startPlay(deviceId, channelId, streamInfo.getStream());
            }
            InviteInfo inviteInfo = inviteStreamService.getInviteInfoByDeviceAndChannel(InviteSessionType.PLAY, deviceId, channelId);
                inviteInfo = inviteStreamService.getInviteInfoByDeviceAndChannel(InviteSessionType.PLAY, deviceId, channelId);
            }
            if (inviteInfo != null) {
                inviteInfo.setStatus(InviteSessionStatus.ok);
                inviteInfo.setStreamInfo(streamInfo);
                if(device.isSwitchPrimarySubStream()){
                    inviteStreamService.updateInviteInfoSub(inviteInfo);
                }else {
                inviteStreamService.updateInviteInfo(inviteInfo);
                }
            }
        }
        return streamInfo;
@@ -607,16 +745,17 @@
                        ResponseEvent responseEvent = (ResponseEvent) eventResult.event;
                        String contentString = new String(responseEvent.getResponse().getRawContent());
                        // èŽ·å–ssrc
                        String ssrcInResponse = SipUtils.getSsrcFromSdp(contentString);
                        int ssrcIndex = contentString.indexOf("y=");
                        // æ£€æŸ¥æ˜¯å¦æœ‰y字段
                        if (ssrcInResponse != null) {
                        if (ssrcIndex >= 0) {
                            //ssrc规定长度为10字节,不取余下长度以避免后续还有“f=”字段 TODO åŽç»­å¯¹ä¸è§„范的非10位ssrc兼容
                            String ssrcInResponse = contentString.substring(ssrcIndex + 2, ssrcIndex + 12);
                            // æŸ¥è¯¢åˆ°ssrc不一致且开启了ssrc校验则需要针对处理
                            if (ssrcInfo.getSsrc().equals(ssrcInResponse)) {
                                if (device.getStreamMode().equalsIgnoreCase("TCP-ACTIVE")) {
                                    String substring = contentString.substring(0, contentString.indexOf("y="));
                                    try {
                                        Gb28181Sdp gb28181Sdp = SipUtils.parseSDP(contentString);
                                        SessionDescription sdp = gb28181Sdp.getBaseSdb();
                                        SessionDescription sdp = SdpFactory.getInstance().createSessionDescription(substring);
                                        int port = -1;
                                        Vector mediaDescriptions = sdp.getMediaDescriptions(true);
                                        for (Object description : mediaDescriptions) {
@@ -684,6 +823,8 @@
                                    }
                                    dynamicTask.stop(playBackTimeOutTaskKey);
                                    // é‡Šæ”¾ssrc
                                    mediaServerService.releaseSsrc(mediaServerItem.getId(), ssrcInfo.getSsrc());
                                    streamSession.remove(device.getDeviceId(), channelId, ssrcInfo.getStream());
@@ -799,15 +940,17 @@
                        ResponseEvent responseEvent = (ResponseEvent) eventResult.event;
                        String contentString = new String(responseEvent.getResponse().getRawContent());
                        // èŽ·å–ssrc
                        String ssrcInResponse = SipUtils.getSsrcFromSdp(contentString);
                        int ssrcIndex = contentString.indexOf("y=");
                        // æ£€æŸ¥æ˜¯å¦æœ‰y字段
                        if (ssrcInResponse != null) {
                        if (ssrcIndex >= 0) {
                            //ssrc规定长度为10字节,不取余下长度以避免后续还有“f=”字段 TODO åŽç»­å¯¹ä¸è§„范的非10位ssrc兼容
                            String ssrcInResponse = contentString.substring(ssrcIndex + 2, ssrcIndex + 12);
                            // æŸ¥è¯¢åˆ°ssrc不一致且开启了ssrc校验则需要针对处理
                            if (ssrcInfo.getSsrc().equals(ssrcInResponse)) {
                                if (device.getStreamMode().equalsIgnoreCase("TCP-ACTIVE")) {
                                    String substring = contentString.substring(0, contentString.indexOf("y="));
                                    try {
                                        Gb28181Sdp gb28181Sdp = SipUtils.parseSDP(contentString);
                                        SessionDescription sdp = gb28181Sdp.getBaseSdb();
                                        SessionDescription sdp = SdpFactory.getInstance().createSessionDescription(substring);
                                        int port = -1;
                                        Vector mediaDescriptions = sdp.getMediaDescriptions(true);
                                        for (Object description : mediaDescriptions) {
@@ -872,6 +1015,8 @@
                                    }
                                    dynamicTask.stop(downLoadTimeOutTaskKey);
                                    // é‡Šæ”¾ssrc
                                    mediaServerService.releaseSsrc(mediaServerItem.getId(), ssrcInfo.getSsrc());
                                    streamSession.remove(device.getDeviceId(), channelId, ssrcInfo.getStream());
@@ -971,6 +1116,7 @@
        streamInfo.setChannelId(channelId);
        return streamInfo;
    }
    @Override
    public void zlmServerOffline(String mediaServerId) {
@@ -1108,14 +1254,18 @@
    }
    @Override
    public void getSnap(String deviceId, String channelId, String fileName, ErrorCallback errorCallback) {
    public void getSnap(String deviceId, String channelId, String fileName,boolean isSubStream, ErrorCallback errorCallback) {
        Device device = deviceService.getDevice(deviceId);
        if (device == null) {
            errorCallback.run(InviteErrorCode.ERROR_FOR_PARAMETER_ERROR.getCode(), InviteErrorCode.ERROR_FOR_PARAMETER_ERROR.getMsg(), null);
            return;
        }
        InviteInfo inviteInfo = inviteStreamService.getInviteInfoByDeviceAndChannel(InviteSessionType.PLAY, deviceId, channelId);
        InviteInfo inviteInfo;
        if(device.isSwitchPrimarySubStream()){
             inviteInfo = inviteStreamService.getInviteInfoByDeviceAndChannel(InviteSessionType.PLAY, deviceId, channelId,isSubStream);
        }else {
            inviteInfo = inviteStreamService.getInviteInfoByDeviceAndChannel(InviteSessionType.PLAY, deviceId, channelId);
        }
        if (inviteInfo != null) {
            if (inviteInfo.getStreamInfo() != null) {
                // å·²å­˜åœ¨çº¿ç›´æŽ¥æˆªå›¾
@@ -1130,10 +1280,9 @@
                // è¯·æ±‚截图
                logger.info("[请求截图]: " + fileName);
                zlmresTfulUtils.getSnap(mediaServerItemInuse, streamUrl, 15, 1, path, fileName);
                String filePath = path + File.separator + fileName;
                File snapFile = new File(path + File.separator + fileName);
                if (snapFile.exists()) {
                    errorCallback.run(InviteErrorCode.SUCCESS.getCode(), InviteErrorCode.SUCCESS.getMsg(), filePath);
                    errorCallback.run(InviteErrorCode.SUCCESS.getCode(), InviteErrorCode.SUCCESS.getMsg(), snapFile.getAbsoluteFile());
                }else {
                    errorCallback.run(InviteErrorCode.FAIL.getCode(), InviteErrorCode.FAIL.getMsg(), null);
                }
@@ -1142,11 +1291,11 @@
        }
        MediaServerItem newMediaServerItem = getNewMediaServerItem(device);
        play(newMediaServerItem, deviceId, channelId, (code, msg, data)->{
        play(newMediaServerItem, deviceId, channelId,isSubStream, (code, msg, data)->{
           if (code == InviteErrorCode.SUCCESS.getCode()) {
               InviteInfo inviteInfoForPlay = inviteStreamService.getInviteInfoByDeviceAndChannel(InviteSessionType.PLAY, deviceId, channelId);
               if (inviteInfoForPlay != null && inviteInfoForPlay.getStreamInfo() != null) {
                   getSnap(deviceId, channelId, fileName, errorCallback);
                   getSnap(deviceId, channelId, fileName,isSubStream, errorCallback);
               }else {
                   errorCallback.run(InviteErrorCode.FAIL.getCode(), InviteErrorCode.FAIL.getMsg(), null);
               }
@@ -1156,4 +1305,17 @@
        });
    }
    /*======================设备主子码流逻辑START=========================*/
    public StreamInfo onPublishHandler(MediaServerItem mediaServerItem, JSONObject resonse, String deviceId, String channelId,boolean isSubStream) {
        String streamId = resonse.getString("stream");
        JSONArray tracks = resonse.getJSONArray("tracks");
        StreamInfo streamInfo = mediaService.getStreamInfoByAppAndStream(mediaServerItem, "rtp", streamId, tracks, null);
        streamInfo.setDeviceID(deviceId);
        streamInfo.setChannelId(channelId);
        streamInfo.setSubStream(isSubStream);
        return streamInfo;
    }
    /*======================设备主子码流逻辑END=========================*/
}
src/main/java/com/genersoft/iot/vmp/storager/dao/DeviceChannelMapper.java
@@ -451,6 +451,10 @@
    @Select("select count(1) from wvp_device_channel")
    int getAllChannelCount();
    // è®¾å¤‡ä¸»å­ç æµé€»è¾‘START
    @Update(value = {"UPDATE wvp_device_channel SET stream_id=null WHERE device_id=#{deviceId}"})
    void clearPlay(String deviceId);
    // è®¾å¤‡ä¸»å­ç æµé€»è¾‘END
    @Select(value = {" <script>" +
            "select * " +
            "from device_channel " +
@@ -460,4 +464,5 @@
            " <if test='onlyCatalog == true '> and parental = 1 </if>" +
            " </script>"})
    List<DeviceChannel> getSubChannelsByDeviceId(String deviceId, String parentId, boolean onlyCatalog);
}
src/main/java/com/genersoft/iot/vmp/storager/dao/DeviceMapper.java
@@ -42,6 +42,7 @@
            "geo_coord_sys," +
            "on_line," +
            "media_server_id," +
            "switch_primary_sub_stream," +
            "(SELECT count(0) FROM wvp_device_channel WHERE device_id=wvp_device.device_id) as channel_count "+
            " FROM wvp_device WHERE device_id = #{deviceId}")
    Device getDeviceByDeviceId(String deviceId);
@@ -157,6 +158,7 @@
            "geo_coord_sys,"+
            "on_line,"+
            "media_server_id,"+
            "switch_primary_sub_stream switchPrimarySubStream,"+
            "(SELECT count(0) FROM wvp_device_channel WHERE device_id=de.device_id) as channel_count " +
            "FROM wvp_device de" +
            "<if test=\"onLine != null\"> where on_line=${onLine}</if>"+
@@ -246,6 +248,7 @@
            "<if test=\"ssrcCheck != null\">, ssrc_check=#{ssrcCheck}</if>" +
            "<if test=\"asMessageChannel != null\">, as_message_channel=#{asMessageChannel}</if>" +
            "<if test=\"geoCoordSys != null\">, geo_coord_sys=#{geoCoordSys}</if>" +
            "<if test=\"switchPrimarySubStream != null\">, switch_primary_sub_stream=#{switchPrimarySubStream}</if>" +
            "<if test=\"mediaServerId != null\">, media_server_id=#{mediaServerId}</if>" +
            "WHERE device_id=#{deviceId}"+
            " </script>"})
@@ -263,7 +266,8 @@
            "as_message_channel,"+
            "geo_coord_sys,"+
            "on_line,"+
            "media_server_id"+
            "media_server_id,"+
            "switch_primary_sub_stream"+
            ") VALUES (" +
            "#{deviceId}," +
            "#{name}," +
@@ -276,7 +280,8 @@
            "#{asMessageChannel}," +
            "#{geoCoordSys}," +
            "#{onLine}," +
            "#{mediaServerId}" +
            "#{mediaServerId}," +
            "#{switchPrimarySubStream}" +
            ")")
    void addCustomDevice(Device device);
src/main/java/com/genersoft/iot/vmp/vmanager/gb28181/play/PlayController.java
@@ -26,7 +26,6 @@
import com.genersoft.iot.vmp.storager.IVideoManagerStorage;
import com.genersoft.iot.vmp.utils.DateUtil;
import com.genersoft.iot.vmp.vmanager.bean.ErrorCode;
import com.genersoft.iot.vmp.vmanager.bean.SnapPath;
import com.genersoft.iot.vmp.vmanager.bean.StreamContent;
import com.genersoft.iot.vmp.vmanager.bean.WVPResult;
import io.swagger.v3.oas.annotations.Operation;
@@ -41,7 +40,6 @@
import javax.servlet.http.HttpServletRequest;
import javax.sip.InvalidArgumentException;
import javax.sip.SipException;
import java.io.File;
import java.text.ParseException;
import java.util.List;
import java.util.UUID;
@@ -90,16 +88,17 @@
    @Operation(summary = "开始点播")
    @Parameter(name = "deviceId", description = "设备国标编号", required = true)
    @Parameter(name = "channelId", description = "通道国标编号", required = true)
    @Parameter(name = "isSubStream", description = "是否子码流(true-子码流,false-主码流),默认为false", required = true)
    @GetMapping("/start/{deviceId}/{channelId}")
    public DeferredResult<WVPResult<StreamContent>> play(HttpServletRequest request, @PathVariable String deviceId,
                                                         @PathVariable String channelId) {
                                                         @PathVariable String channelId,boolean isSubStream) {
        // èŽ·å–å¯ç”¨çš„zlm
        Device device = storager.queryVideoDevice(deviceId);
        MediaServerItem newMediaServerItem = playService.getNewMediaServerItem(device);
        RequestMessage requestMessage = new RequestMessage();
        String key = DeferredResultHolder.CALLBACK_CMD_PLAY + deviceId + channelId;
        String key = DeferredResultHolder.getPlayKey(deviceId,channelId,device.isSwitchPrimarySubStream(),isSubStream);
        requestMessage.setKey(key);
        String uuid = UUID.randomUUID().toString();
        requestMessage.setId(uuid);
@@ -118,7 +117,7 @@
        // å½•像查询以channelId作为deviceId查询
        resultHolder.put(key, uuid, result);
        playService.play(newMediaServerItem, deviceId, channelId, (code, msg, data) -> {
        playService.play(newMediaServerItem, deviceId, channelId,isSubStream, (code, msg, data) -> {
            WVPResult<StreamContent> wvpResult = new WVPResult<>();
            if (code == InviteErrorCode.SUCCESS.getCode()) {
                wvpResult.setCode(ErrorCode.SUCCESS.getCode());
@@ -144,8 +143,9 @@
    @Operation(summary = "停止点播")
    @Parameter(name = "deviceId", description = "设备国标编号", required = true)
    @Parameter(name = "channelId", description = "通道国标编号", required = true)
    @Parameter(name = "isSubStream", description = "是否子码流(true-子码流,false-主码流),默认为false", required = true)
    @GetMapping("/stop/{deviceId}/{channelId}")
    public JSONObject playStop(@PathVariable String deviceId, @PathVariable String channelId) {
    public JSONObject playStop(@PathVariable String deviceId, @PathVariable String channelId,boolean isSubStream) {
        logger.debug(String.format("设备预览/回放停止API调用,streamId:%s_%s", deviceId, channelId ));
@@ -158,7 +158,12 @@
            throw new ControllerException(ErrorCode.ERROR100.getCode(), "设备[" + deviceId + "]不存在");
        }
        InviteInfo inviteInfo = inviteStreamService.getInviteInfoByDeviceAndChannel(InviteSessionType.PLAY, deviceId, channelId);
        InviteInfo inviteInfo =null;
        if(device.isSwitchPrimarySubStream()){
            inviteInfo = inviteStreamService.getInviteInfoByDeviceAndChannel(InviteSessionType.PLAY, deviceId, channelId,isSubStream);
        }else {
            inviteInfo = inviteStreamService.getInviteInfoByDeviceAndChannel(InviteSessionType.PLAY, deviceId, channelId);
        }
        if (inviteInfo == null) {
            throw new ControllerException(ErrorCode.ERROR100.getCode(), "点播未找到");
        }
@@ -171,12 +176,17 @@
                throw new ControllerException(ErrorCode.ERROR100.getCode(), "命令发送失败: " + e.getMessage());
            }
        }
        if(device.isSwitchPrimarySubStream()){
            inviteStreamService.removeInviteInfoByDeviceAndChannel(InviteSessionType.PLAY, deviceId, channelId,isSubStream);
        }else {
        inviteStreamService.removeInviteInfoByDeviceAndChannel(InviteSessionType.PLAY, deviceId, channelId);
        storager.stopPlay(deviceId, channelId);
        }
        JSONObject json = new JSONObject();
        json.put("deviceId", deviceId);
        json.put("channelId", channelId);
        json.put("isSubStream", isSubStream);
        return json;
    }
@@ -343,30 +353,27 @@
    @Operation(summary = "获取截图")
    @Parameter(name = "deviceId", description = "设备国标编号", required = true)
    @Parameter(name = "channelId", description = "通道国标编号", required = true)
    @Parameter(name = "isSubStream", description = "是否子码流(true-子码流,false-主码流),默认为false", required = true)
    @GetMapping("/snap")
    public DeferredResult<String> getSnap(HttpServletRequest request, String deviceId, String channelId) {
    public DeferredResult<String> getSnap(String deviceId, String channelId,boolean isSubStream) {
        if (logger.isDebugEnabled()) {
            logger.debug("获取截图: {}/{}", deviceId, channelId);
        }
        Device device = storager.queryVideoDevice(deviceId);
        DeferredResult<String> result = new DeferredResult<>(3 * 1000L);
        String key  = DeferredResultHolder.CALLBACK_CMD_SNAP + deviceId;
        String key = DeferredResultHolder.getSnapKey(deviceId,channelId,device.isSwitchPrimarySubStream(),isSubStream);
        String uuid  = UUID.randomUUID().toString();
        resultHolder.put(key, uuid,  result);
        RequestMessage message = new RequestMessage();
        message.setKey(key);
        message.setId(uuid);
        String nowForUrl = DateUtil.getNowForUrl();
        String fileName = deviceId + "_" + channelId + "_" + nowForUrl + ".jpg";
        playService.getSnap(deviceId, channelId, fileName, (code, msg, data) -> {
        String fileName = deviceId + "_" + channelId + "_" + DateUtil.getNowForUrl() + "jpg";
        playService.getSnap(deviceId, channelId, fileName,isSubStream, (code, msg, data) -> {
            if (code == InviteErrorCode.SUCCESS.getCode()) {
                File snapFile = new File((String)data);
                String fileNameForUrl = deviceId + "/" + channelId + "?mark=" + nowForUrl;
                String uri = request.getRequestURL().toString().replace(request.getRequestURI(), "/api/device/query/snap/" + fileNameForUrl);
                SnapPath snapPath = SnapPath.getInstance((String) data, snapFile.getAbsolutePath(), uri);
                message.setData(snapPath);
                message.setData(data);
            }else {
                message.setData(WVPResult.fail(code, msg));
            }
src/main/java/com/genersoft/iot/vmp/web/gb28181/ApiStreamController.java
@@ -122,7 +122,7 @@
        MediaServerItem newMediaServerItem = playService.getNewMediaServerItem(device);
        playService.play(newMediaServerItem, serial, code, (errorCode, msg, data) -> {
        playService.play(newMediaServerItem, serial, code,false, (errorCode, msg, data) -> {
            if (errorCode == InviteErrorCode.SUCCESS.getCode()) {
                InviteInfo inviteInfo = inviteStreamService.getInviteInfoByDeviceAndChannel(InviteSessionType.PLAY, serial, code);
                if (inviteInfo != null && inviteInfo.getStreamInfo() != null) {
src/main/resources/application-dev.yml
@@ -1,4 +1,6 @@
spring:
  thymeleaf:
    cache: false
  # [可选]上传文件大小限制
  servlet:
    multipart:
@@ -11,18 +13,18 @@
    # [必须修改] ç«¯å£å·
    port: 6379
    # [可选] æ•°æ®åº“ DB
    database: 6
    database: 7
    # [可选] è®¿é—®å¯†ç ,若你的redis服务器没有设置密码,就不需要用密码去连接
    password: face2020
    password:
    # [可选] è¶…æ—¶æ—¶é—´
    timeout: 10000
    # mysql数据源
  datasource:
    type: com.zaxxer.hikari.HikariDataSource
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://127.0.0.1:3306/wvp2?useUnicode=true&characterEncoding=UTF8&rewriteBatchedStatements=true&serverTimezone=PRC&useSSL=false&allowMultiQueries=true
    url: jdbc:mysql://127.0.0.1:3306/test_gb-89wulian?useUnicode=true&characterEncoding=UTF8&rewriteBatchedStatements=true&serverTimezone=PRC&useSSL=false&allowMultiQueries=true
    username: root
    password: 123456
    password: root
    hikari:
      connection-timeout: 20000             # æ˜¯å®¢æˆ·ç«¯ç­‰å¾…连接池连接的最大毫秒数
      initialSize: 10                       # è¿žæŽ¥æ± åˆå§‹åŒ–连接数
@@ -30,11 +32,19 @@
      minimum-idle: 5                       # è¿žæŽ¥æ± æœ€å°ç©ºé—²è¿žæŽ¥æ•°
      idle-timeout: 300000                  # å…è®¸è¿žæŽ¥åœ¨è¿žæŽ¥æ± ä¸­ç©ºé—²çš„æœ€é•¿æ—¶é—´ï¼ˆä»¥æ¯«ç§’为单位)
      max-lifetime: 1200000                 # æ˜¯æ± ä¸­è¿žæŽ¥å…³é—­åŽçš„æœ€é•¿ç”Ÿå‘½å‘¨æœŸï¼ˆä»¥æ¯«ç§’为单位)
#[可选] WVP监听的HTTP端口, ç½‘页和接口调用都是这个端口
server:
  port: 18080
  port: 18978
  # [可选] HTTPS配置, é»˜è®¤ä¸å¼€å¯
  ssl:
    # [可选] æ˜¯å¦å¼€å¯HTTPS访问
    enabled: false
    # [可选] è¯ä¹¦æ–‡ä»¶è·¯å¾„,放置在resource/目录下即可,修改xxx为文件名
    key-store: classpath:test.monitor.89iot.cn.jks
    # [可选] è¯ä¹¦å¯†ç 
    key-store-password: gpf64qmw
    # [可选] è¯ä¹¦ç±»åž‹ï¼Œ é»˜è®¤ä¸ºjks,根据实际修改
    key-store-type: JKS
# ä½œä¸º28181服务器的配置
sip:
@@ -42,26 +52,36 @@
  # å¦‚果要监听多张网卡,可以使用逗号分隔多个IP, ä¾‹å¦‚: 192.168.1.4,10.0.0.4
  # å¦‚果不明白,就使用0.0.0.0,大部分情况都是可以的
  # è¯·ä¸è¦ä½¿ç”¨127.0.0.1,任何包括localhost在内的域名都是不可以的。
  ip: 192.168.41.16
  ip: 192.168.1.18
  # [可选] 28181服务监听的端口
  port: 5060
  port: 8116
  # æ ¹æ®å›½æ ‡6.1.2中规定,domain宜采用ID统一编码的前十位编码。国标附录D中定义前8位为中心编码(由省级、市级、区级、基层编号组成,参照GB/T 2260-2007)
  # åŽä¸¤ä½ä¸ºè¡Œä¸šç¼–码,定义参照附录D.3
  # 3701020049标识山东济南历下区 ä¿¡æ¯è¡Œä¸šæŽ¥å…¥
  # [可选]
  domain: 4401020049
  domain: 4101050000
  # [可选]
  id: 44010200492000000001
  id: 41010500002000000001
  # [可选] é»˜è®¤è®¾å¤‡è®¤è¯å¯†ç ï¼ŒåŽç»­æ‰©å±•使用设备单独密码, ç§»é™¤å¯†ç å°†ä¸è¿›è¡Œæ ¡éªŒ
  password: admin123
  password: bajiuwulian1006
  # æ˜¯å¦å­˜å‚¨alarm信息
  alarm: true
#zlm é»˜è®¤æœåŠ¡å™¨é…ç½®
media:
  id: FQ3TF8yT83wh5Wvz
  id: 89wulian-one
  # [必须修改] zlm服务器的内网IP
  ip: 192.168.41.16
  ip: 192.168.1.18
  # [必须修改] zlm服务器的http.port
  http-port: 8091
  http-port: 80
  # [可选] è¿”回流地址时的ip,置空使用 media.ip
  stream-ip: 192.168.1.18
  # [可选] wvp在国标信令中使用的ip,此ip为摄像机可以访问到的ip, ç½®ç©ºä½¿ç”¨ media.ip
  sdp-ip: 192.168.1.18
  # [可选] zlm服务器的hook所使用的IP, é»˜è®¤ä½¿ç”¨sip.ip
  hook-ip: 192.168.1.18
  # [可选] zlm服务器的http.sslport, ç½®ç©ºä½¿ç”¨zlm配置文件配置
  http-ssl-port: 443
  # [可选] zlm服务器的hook.admin_params=secret
  secret: 035c73f7-bb6b-4889-a715-d9eb2d1925cc
  # å¯ç”¨å¤šç«¯å£æ¨¡å¼, å¤šç«¯å£æ¨¡å¼ä½¿ç”¨ç«¯å£åŒºåˆ†æ¯è·¯æµï¼Œå…¼å®¹æ€§æ›´å¥½ã€‚ å•端口使用流的ssrc区分, ç‚¹æ’­è¶…时建议使用多端口测试
@@ -69,11 +89,24 @@
    # [可选] æ˜¯å¦å¯ç”¨å¤šç«¯å£æ¨¡å¼, å¼€å¯åŽä¼šåœ¨portRange范围内选择端口用于媒体流传输
    enable: true
    # [可选] åœ¨æ­¤èŒƒå›´å†…选择端口用于媒体流传输, å¿…须提前在zlm上配置该属性,不然自动配置此属性可能不成功
    port-range: 30000,30500 # ç«¯å£èŒƒå›´
    port-range: 50000,50300 # ç«¯å£èŒƒå›´
    # [可选] å›½æ ‡çº§è”在此范围内选择端口发送媒体流,
    send-port-range: 30000,30500 # ç«¯å£èŒƒå›´
    send-port-range: 50000,50300 # ç«¯å£èŒƒå›´
  # å½•像辅助服务, éƒ¨ç½²æ­¤æœåŠ¡å¯ä»¥å®žçŽ°zlm录像的管理与下载, 0 è¡¨ç¤ºä¸ä½¿ç”¨
  record-assist-port: 18081
# [根据业务需求配置]
user-settings:
  # ç‚¹æ’­/录像回放 ç­‰å¾…è¶…æ—¶æ—¶é—´,单位:毫秒
  play-timeout: 180000
  # [可选] è‡ªåŠ¨ç‚¹æ’­ï¼Œ ä½¿ç”¨å›ºå®šæµåœ°å€è¿›è¡Œæ’­æ”¾æ—¶ï¼Œå¦‚果未点播则自动进行点播, éœ€è¦rtp.enable=true
  auto-apply-play: true
  # è®¾å¤‡/通道状态变化时发送消息
  device-status-notify: true
  # è·¨åŸŸé…ç½®ï¼Œé…ç½®ä½ è®¿é—®å‰ç«¯é¡µé¢çš„地址即可, å¯ä»¥é…ç½®å¤šä¸ª
  allowed-origins:
    - http://localhost:8080
    - http://127.0.0.1:8080
# [可选] æ—¥å¿—配置, ä¸€èˆ¬ä¸éœ€è¦æ”¹
logging:
  config: classpath:logback-spring-local.xml
src/main/resources/application.yml
@@ -2,4 +2,4 @@
  application:
    name: wvp
  profiles:
    active: local
    active: dev
web_src/config/index.js
@@ -12,14 +12,14 @@
    assetsPublicPath: '/',
    proxyTable: {
      '/debug': {
        target: 'http://localhost:18080',
        target: 'http://localhost:18978',
        changeOrigin: true,
        pathRewrite: {
          '^/debug': '/'
        }
      },
      '/static/snap': {
        target: 'http://localhost:18080',
        target: 'http://localhost:18978',
        changeOrigin: true,
        // pathRewrite: {
        //   '^/static/snap': '/static/snap'
web_src/src/components/channelList.vue
@@ -26,6 +26,12 @@
            <el-option label="在线" value="true"></el-option>
            <el-option label="离线" value="false"></el-option>
          </el-select>
          æ¸…晰度:
          <el-select size="mini" style="margin-right: 1rem;" @change="search" v-model="isSubStream" placeholder="请选择"
                     default-first-option>
            <el-option label="原画" :value="false"></el-option>
            <el-option label="流畅" :value="true"></el-option>
          </el-select>
        </div>
      <el-button icon="el-icon-refresh-right" circle size="mini" @click="refresh()"></el-button>
      <el-button v-if="showTree" icon="iconfont icon-list" circle size="mini" @click="switchList()"></el-button>
@@ -146,6 +152,7 @@
      searchSrt: "",
      channelType: "",
      online: "",
      isSubStream: false,
      winHeight: window.innerHeight - 200,
      currentPage: 1,
      count: 15,
@@ -237,7 +244,10 @@
      let that = this;
      this.$axios({
        method: 'get',
        url: '/api/play/start/' + deviceId + '/' + channelId
        url: '/api/play/start/' + deviceId + '/' + channelId,
        params:{
          isSubStream: this.isSubStream
        }
      }).then(function (res) {
        console.log(res)
        that.isLoging = false;
@@ -277,7 +287,10 @@
      var that = this;
      this.$axios({
        method: 'get',
        url: '/api/play/stop/' + this.deviceId + "/" + itemData.channelId
        url: '/api/play/stop/' + this.deviceId + "/" + itemData.channelId,
        params:{
          isSubStream: this.isSubStream
        }
      }).then(function (res) {
        that.initData();
      }).catch(function (error) {
web_src/src/components/dialog/deviceEdit.vue
@@ -58,6 +58,12 @@
          <el-form-item v-if="form.subscribeCycleForMobilePosition > 0" label="移动位置报送间隔" prop="subscribeCycleForCatalog" >
            <el-input v-model="form.mobilePositionSubmissionInterval" clearable ></el-input>
          </el-form-item>
          <el-form-item label="主子码流开关" prop="switchPrimarySubStream" >
            <el-select v-model="form.switchPrimarySubStream" style="float: left; width: 100%" >
              <el-option key="true" label="开启" :value="true"></el-option>
              <el-option key="false" label="关闭" :value="false"></el-option>
            </el-select>
          </el-form-item>
          <el-form-item label="其他选项">
            <el-checkbox label="SSRC校验" v-model="form.ssrcCheck" style="float: left"></el-checkbox>
            <el-checkbox label="作为消息通道" v-model="form.asMessageChannel" style="float: left"></el-checkbox>