648540858
2021-03-27 f8fe76add24fc2a26449c6b4007a303decb46d99
Merge pull request #74 from lawrencehj/wvp-28181-2.0

实现语音广播信令等(web语音推流开发中)
16个文件已修改
493 ■■■■ 已修改文件
src/main/java/com/genersoft/iot/vmp/conf/SipPlatformRunner.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/genersoft/iot/vmp/gb28181/SipLayer.java 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/genersoft/iot/vmp/gb28181/transmit/callback/DeferredResultHolder.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/genersoft/iot/vmp/gb28181/transmit/cmd/ISIPCommander.java 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/genersoft/iot/vmp/gb28181/transmit/cmd/SIPRequestHeaderPlarformProvider.java 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/genersoft/iot/vmp/gb28181/transmit/cmd/SIPRequestHeaderProvider.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/genersoft/iot/vmp/gb28181/transmit/cmd/impl/SIPCommander.java 63 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/genersoft/iot/vmp/gb28181/transmit/cmd/impl/SIPCommanderFroPlatform.java 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/genersoft/iot/vmp/gb28181/transmit/request/impl/InviteRequestProcessor.java 301 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/genersoft/iot/vmp/gb28181/transmit/request/impl/MessageRequestProcessor.java 41 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/genersoft/iot/vmp/gb28181/transmit/response/impl/RegisterResponseProcessor.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/genersoft/iot/vmp/storager/IRedisCatchStorage.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/genersoft/iot/vmp/storager/impl/RedisCatchStorageImpl.java 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/genersoft/iot/vmp/vmanager/play/PlayController.java 45 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
web_src/src/components/channelList.vue 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
web_src/src/components/devicePosition.vue 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/genersoft/iot/vmp/conf/SipPlatformRunner.java
@@ -33,6 +33,9 @@
        // 设置所有平台离线
        storager.outlineForAllParentPlatform();
        // 清理所有平台注册缓存
        redisCatchStorage.cleanPlatformRegisterInfos();
        List<ParentPlatform> parentPlatforms = storager.queryEnableParentPlatformList(true);
        for (ParentPlatform parentPlatform : parentPlatforms) {
src/main/java/com/genersoft/iot/vmp/gb28181/SipLayer.java
@@ -16,7 +16,7 @@
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
// import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.DependsOn;
import org.springframework.stereotype.Component;
src/main/java/com/genersoft/iot/vmp/gb28181/transmit/callback/DeferredResultHolder.java
@@ -41,6 +41,8 @@
    public static final String CALLBACK_CMD_ALARM = "CALLBACK_ALARM";
    public static final String CALLBACK_CMD_BROADCAST = "CALLBACK_BROADCAST";
    private Map<String, DeferredResult> map = new ConcurrentHashMap<String, DeferredResult>();
    public void put(String key, DeferredResult result) {
src/main/java/com/genersoft/iot/vmp/gb28181/transmit/cmd/ISIPCommander.java
@@ -120,6 +120,14 @@
    boolean audioBroadcastCmd(Device device,String channelId);
    
    /**
     * 语音广播
     *
     * @param device  视频设备
     */
    void audioBroadcastCmd(Device device, SipSubscribe.Event okEvent);
    boolean audioBroadcastCmd(Device device);
    /**
     * 音视频录像控制
     * 
     * @param device          视频设备
src/main/java/com/genersoft/iot/vmp/gb28181/transmit/cmd/SIPRequestHeaderPlarformProvider.java
@@ -3,7 +3,7 @@
import com.genersoft.iot.vmp.conf.SipConfig;
import com.genersoft.iot.vmp.gb28181.bean.ParentPlatform;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
// import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Component;
import org.springframework.util.DigestUtils;
src/main/java/com/genersoft/iot/vmp/gb28181/transmit/cmd/SIPRequestHeaderProvider.java
@@ -6,14 +6,14 @@
import javax.sip.InvalidArgumentException;
import javax.sip.PeerUnavailableException;
import javax.sip.SipFactory;
import javax.sip.SipProvider;
// import javax.sip.SipProvider;
import javax.sip.address.Address;
import javax.sip.address.SipURI;
import javax.sip.header.*;
import javax.sip.message.Request;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
// import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Component;
import com.genersoft.iot.vmp.conf.SipConfig;
src/main/java/com/genersoft/iot/vmp/gb28181/transmit/cmd/impl/SIPCommander.java
@@ -23,7 +23,7 @@
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.ComponentScan;
// import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.DependsOn;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Component;
@@ -47,8 +47,7 @@
public class SIPCommander implements ISIPCommander {
    private final Logger logger = LoggerFactory.getLogger(SIPCommander.class);
    @Autowired
    private SipConfig sipConfig;
@@ -623,11 +622,67 @@
     */
    @Override
    public boolean audioBroadcastCmd(Device device, String channelId) {
        // TODO Auto-generated method stub
        // 改为新的实现
        return false;
    }
    /**
     * 语音广播
     *
     * @param device  视频设备
     * @param channelId  预览通道
     */
    @Override
    public boolean audioBroadcastCmd(Device device) {
        try {
            StringBuffer broadcastXml = new StringBuffer(200);
            broadcastXml.append("<?xml version=\"1.0\" ?>\r\n");
            broadcastXml.append("<Notify>\r\n");
            broadcastXml.append("<CmdType>Broadcast</CmdType>\r\n");
            broadcastXml.append("<SN>" + (int)((Math.random()*9+1)*100000) + "</SN>\r\n");
            broadcastXml.append("<SourceID>" + sipConfig.getSipId() + "</SourceID>\r\n");
            broadcastXml.append("<TargetID>" + device.getDeviceId() + "</TargetID>\r\n");
            broadcastXml.append("</Notify>\r\n");
            String tm = Long.toString(System.currentTimeMillis());
            CallIdHeader callIdHeader = device.getTransport().equals("TCP") ? tcpSipProvider.getNewCallId()
                    : udpSipProvider.getNewCallId();
            Request request = headerProvider.createMessageRequest(device, broadcastXml.toString(), "z9hG4bK-ViaBcst-" + tm, "FromBcst" + tm, null, callIdHeader);
            transmitRequest(device, request);
            return true;
        } catch (SipException | ParseException | InvalidArgumentException e) {
            e.printStackTrace();
        }
        return false;
    }
    @Override
    public void audioBroadcastCmd(Device device, SipSubscribe.Event errorEvent) {
        try {
            StringBuffer broadcastXml = new StringBuffer(200);
            broadcastXml.append("<?xml version=\"1.0\" ?>\r\n");
            broadcastXml.append("<Notify>\r\n");
            broadcastXml.append("<CmdType>Broadcast</CmdType>\r\n");
            broadcastXml.append("<SN>" + (int)((Math.random()*9+1)*100000) + "</SN>\r\n");
            broadcastXml.append("<SourceID>" + sipConfig.getSipId() + "</SourceID>\r\n");
            broadcastXml.append("<TargetID>" + device.getDeviceId() + "</TargetID>\r\n");
            broadcastXml.append("</Notify>\r\n");
            String tm = Long.toString(System.currentTimeMillis());
            CallIdHeader callIdHeader = device.getTransport().equals("TCP") ? tcpSipProvider.getNewCallId()
                    : udpSipProvider.getNewCallId();
            Request request = headerProvider.createMessageRequest(device, broadcastXml.toString(), "z9hG4bK-ViaBcst-" + tm, "FromBcst" + tm, null, callIdHeader);
            transmitRequest(device, request, errorEvent);
        } catch (SipException | ParseException | InvalidArgumentException e) {
            e.printStackTrace();
        }
    }
    /**
     * 音视频录像控制
     * 
     * @param device        视频设备
src/main/java/com/genersoft/iot/vmp/gb28181/transmit/cmd/impl/SIPCommanderFroPlatform.java
@@ -15,7 +15,7 @@
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.ComponentScan;
// import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.DependsOn;
import org.springframework.context.annotation.Lazy;
import org.springframework.lang.Nullable;
src/main/java/com/genersoft/iot/vmp/gb28181/transmit/request/impl/InviteRequestProcessor.java
@@ -14,6 +14,7 @@
import com.genersoft.iot.vmp.conf.MediaServerConfig;
import com.genersoft.iot.vmp.gb28181.bean.Device;
import com.genersoft.iot.vmp.gb28181.bean.DeviceChannel;
import com.genersoft.iot.vmp.gb28181.bean.ParentPlatform;
import com.genersoft.iot.vmp.gb28181.bean.SendRtpItem;
import com.genersoft.iot.vmp.gb28181.transmit.cmd.impl.SIPCommander;
import com.genersoft.iot.vmp.gb28181.transmit.cmd.impl.SIPCommanderFroPlatform;
@@ -74,144 +75,216 @@
            Request request = evt.getRequest();
            SipURI sipURI = (SipURI) request.getRequestURI();
            String channelId = sipURI.getUser();
            String platformId = null;
            String requesterId = null;
            FromHeader fromHeader = (FromHeader)request.getHeader(FromHeader.NAME);
            AddressImpl address = (AddressImpl) fromHeader.getAddress();
            SipUri uri = (SipUri) address.getURI();
            platformId = uri.getUser();
            requesterId = uri.getUser();
            if (platformId == null || channelId == null) {
                logger.info("无法从FromHeader的Address中获取到平台id,返回404");
            if (requesterId == null || channelId == null) {
                logger.info("无法从FromHeader的Address中获取到平台id,返回400");
                responseAck(evt, Response.BAD_REQUEST); // 参数不全, 发400,请求错误
                return;
            }
            // 查询平台下是否有该通道
            DeviceChannel channel = storager.queryChannelInParentPlatform(platformId, channelId);
            if (channel == null) {
                logger.info("通道不存在,返回404");
                responseAck(evt, Response.NOT_FOUND); // 通道不存在,发404,资源不存在
                return;
            }else {
                responseAck(evt, Response.TRYING); // 通道存在,发100,trying
            }
            // 解析sdp消息, 使用jainsip 自带的sdp解析方式
            String contentString = new String(request.getRawContent());
            // jainSip不支持y=字段, 移除移除以解析。
            int ssrcIndex = contentString.indexOf("y=");
            String ssrc = contentString.substring(ssrcIndex + 2, ssrcIndex + 12);
            //ssrc规定长度为10字节,不取余下长度以避免后续还有“f=”字段
            // String ssrc = contentString.substring(ssrcIndex + 2, contentString.length())
            //         .replace("\r\n", "").replace("\n", "");
            // 查询请求方是否上级平台
            ParentPlatform platform = storager.queryParentPlatById(requesterId);
            if (platform != null) {
                // 查询平台下是否有该通道
                DeviceChannel channel = storager.queryChannelInParentPlatform(requesterId, channelId);
                if (channel == null) {
                    logger.info("通道不存在,返回404");
                    responseAck(evt, Response.NOT_FOUND); // 通道不存在,发404,资源不存在
                    return;
                }else {
                    responseAck(evt, Response.CALL_IS_BEING_FORWARDED); // 通道存在,发181,呼叫转接中
                }
                // 解析sdp消息, 使用jainsip 自带的sdp解析方式
                String contentString = new String(request.getRawContent());
            String substring = contentString.substring(0, contentString.indexOf("y="));
            SessionDescription sdp = SdpFactory.getInstance().createSessionDescription(substring);
                // jainSip不支持y=字段, 移除移除以解析。
                int ssrcIndex = contentString.indexOf("y=");
                //ssrc规定长度为10字节,不取余下长度以避免后续还有“f=”字段
                String ssrc = contentString.substring(ssrcIndex + 2, ssrcIndex + 12);
                String substring = contentString.substring(0, contentString.indexOf("y="));
                SessionDescription sdp = SdpFactory.getInstance().createSessionDescription(substring);
            //  获取支持的格式
            Vector mediaDescriptions = sdp.getMediaDescriptions(true);
            // 查看是否支持PS 负载96
            //String ip = null;
            int port = -1;
            //boolean recvonly = false;
            boolean mediaTransmissionTCP = false;
            Boolean tcpActive = null;
            for (int i = 0; i < mediaDescriptions.size(); i++) {
                MediaDescription mediaDescription = (MediaDescription)mediaDescriptions.get(i);
                Media media = mediaDescription.getMedia();
                //  获取支持的格式
                Vector mediaDescriptions = sdp.getMediaDescriptions(true);
                // 查看是否支持PS 负载96
                //String ip = null;
                int port = -1;
                //boolean recvonly = false;
                boolean mediaTransmissionTCP = false;
                Boolean tcpActive = null;
                for (int i = 0; i < mediaDescriptions.size(); i++) {
                    MediaDescription mediaDescription = (MediaDescription)mediaDescriptions.get(i);
                    Media media = mediaDescription.getMedia();
                Vector mediaFormats = media.getMediaFormats(false);
                if (mediaFormats.contains("96")) {
                    port = media.getMediaPort();
                    //String mediaType = media.getMediaType();
                    String protocol = media.getProtocol();
                    Vector mediaFormats = media.getMediaFormats(false);
                    if (mediaFormats.contains("96")) {
                        port = media.getMediaPort();
                        //String mediaType = media.getMediaType();
                        String protocol = media.getProtocol();
                    // 区分TCP发流还是udp, 当前默认udp
                    if ("TCP/RTP/AVP".equals(protocol)) {
                        String setup = mediaDescription.getAttribute("setup");
                        if (setup != null) {
                            mediaTransmissionTCP = true;
                            if ("active".equals(setup)) {
                                tcpActive = true;
                            }else if ("passive".equals(setup)) {
                                tcpActive = false;
                        // 区分TCP发流还是udp, 当前默认udp
                        if ("TCP/RTP/AVP".equals(protocol)) {
                            String setup = mediaDescription.getAttribute("setup");
                            if (setup != null) {
                                mediaTransmissionTCP = true;
                                if ("active".equals(setup)) {
                                    tcpActive = true;
                                }else if ("passive".equals(setup)) {
                                    tcpActive = false;
                                }
                            }
                        }
                        break;
                    }
                    break;
                }
            }
            if (port == -1) {
                logger.info("不支持的媒体格式,返回415");
                // 回复不支持的格式
                responseAck(evt, Response.UNSUPPORTED_MEDIA_TYPE); // 不支持的格式,发415
                return;
            }
            String username = sdp.getOrigin().getUsername();
            String addressStr = sdp.getOrigin().getAddress();
            //String sessionName = sdp.getSessionName().getValue();
            logger.info("[上级点播]用户:{}, 地址:{}:{}, ssrc:{}", username, addressStr, port, ssrc);
                if (port == -1) {
                    logger.info("不支持的媒体格式,返回415");
                    // 回复不支持的格式
                    responseAck(evt, Response.UNSUPPORTED_MEDIA_TYPE); // 不支持的格式,发415
                    return;
                }
                String username = sdp.getOrigin().getUsername();
                String addressStr = sdp.getOrigin().getAddress();
                //String sessionName = sdp.getSessionName().getValue();
                logger.info("[上级点播]用户:{}, 地址:{}:{}, ssrc:{}", username, addressStr, port, ssrc);
            Device device = storager.queryVideoDeviceByPlatformIdAndChannelId(platformId, channelId);
            if (device == null) {
                logger.warn("点播平台{}的通道{}时未找到设备信息", platformId, channel);
                responseAck(evt, Response.SERVER_INTERNAL_ERROR);
                return;
            }
            SendRtpItem sendRtpItem = zlmrtpServerFactory.createSendRtpItem(addressStr, port, ssrc, platformId, device.getDeviceId(), channelId,
                    mediaTransmissionTCP);
            if (tcpActive != null) {
                sendRtpItem.setTcpActive(tcpActive);
            }
            if (sendRtpItem == null) {
                logger.warn("服务器端口资源不足");
                responseAck(evt, Response.BUSY_HERE);
                return;
            }
                Device device = storager.queryVideoDeviceByPlatformIdAndChannelId(requesterId, channelId);
                if (device == null) {
                    logger.warn("点播平台{}的通道{}时未找到设备信息", requesterId, channel);
                    responseAck(evt, Response.SERVER_INTERNAL_ERROR);
                    return;
                }
                SendRtpItem sendRtpItem = zlmrtpServerFactory.createSendRtpItem(addressStr, port, ssrc, requesterId, device.getDeviceId(), channelId,
                        mediaTransmissionTCP);
                if (tcpActive != null) {
                    sendRtpItem.setTcpActive(tcpActive);
                }
                if (sendRtpItem == null) {
                    logger.warn("服务器端口资源不足");
                    responseAck(evt, Response.BUSY_HERE);
                    return;
                }
            // 写入redis, 超时时回复
            redisCatchStorage.updateSendRTPSever(sendRtpItem);
            // 通知下级推流,
            PlayResult playResult = playService.play(device.getDeviceId(), channelId, (responseJSON)->{
                // 收到推流, 回复200OK, 等待ack
                sendRtpItem.setStatus(1);
                // 写入redis, 超时时回复
                redisCatchStorage.updateSendRTPSever(sendRtpItem);
                // TODO 添加对tcp的支持
                MediaServerConfig mediaInfo = redisCatchStorage.getMediaInfo();
                StringBuffer content = new StringBuffer(200);
                content.append("v=0\r\n");
                content.append("o="+"00000"+" 0 0 IN IP4 "+mediaInfo.getWanIp()+"\r\n");
                content.append("s=Play\r\n");
                content.append("c=IN IP4 "+mediaInfo.getWanIp()+"\r\n");
                content.append("t=0 0\r\n");
                content.append("m=video "+ sendRtpItem.getLocalPort()+" RTP/AVP 96\r\n");
                content.append("a=sendonly\r\n");
                content.append("a=rtpmap:96 PS/90000\r\n");
                content.append("y="+ ssrc + "\r\n");
                content.append("f=\r\n");
                // 通知下级推流,
                PlayResult playResult = playService.play(device.getDeviceId(), channelId, (responseJSON)->{
                    // 收到推流, 回复200OK, 等待ack
                    sendRtpItem.setStatus(1);
                    redisCatchStorage.updateSendRTPSever(sendRtpItem);
                    // TODO 添加对tcp的支持
                    MediaServerConfig mediaInfo = redisCatchStorage.getMediaInfo();
                    StringBuffer content = new StringBuffer(200);
                    content.append("v=0\r\n");
                    content.append("o="+"00000"+" 0 0 IN IP4 "+mediaInfo.getWanIp()+"\r\n");
                    content.append("s=Play\r\n");
                    content.append("c=IN IP4 "+mediaInfo.getWanIp()+"\r\n");
                    content.append("t=0 0\r\n");
                    content.append("m=video "+ sendRtpItem.getLocalPort()+" RTP/AVP 96\r\n");
                    content.append("a=sendonly\r\n");
                    content.append("a=rtpmap:96 PS/90000\r\n");
                    content.append("y="+ ssrc + "\r\n");
                    content.append("f=\r\n");
                try {
                    responseAck(evt, content.toString());
                } catch (SipException e) {
                    e.printStackTrace();
                } catch (InvalidArgumentException e) {
                    e.printStackTrace();
                } catch (ParseException e) {
                    e.printStackTrace();
                    try {
                        responseAck(evt, content.toString());
                    } catch (SipException e) {
                        e.printStackTrace();
                    } catch (InvalidArgumentException e) {
                        e.printStackTrace();
                    } catch (ParseException e) {
                        e.printStackTrace();
                    }
                },(event -> {
                    // 未知错误。直接转发设备点播的错误
                    Response response = null;
                    try {
                        response = getMessageFactory().createResponse(event.getResponse().getStatusCode(), evt.getRequest());
                        getServerTransaction(evt).sendResponse(response);
                    } catch (ParseException | SipException | InvalidArgumentException e) {
                        e.printStackTrace();
                    }
                }));
                if (logger.isDebugEnabled()) {
                    logger.debug(playResult.getResult().toString());
                }
            },(event -> {
                // 未知错误。直接转发设备点播的错误
                Response response = null;
                try {
                    response = getMessageFactory().createResponse(event.getResponse().getStatusCode(), evt.getRequest());
                    getServerTransaction(evt).sendResponse(response);
            } else {
                // 非上级平台请求,查询是否设备请求(通常为接收语音广播的设备)
                Device device = storager.queryVideoDevice(requesterId);
                if (device != null) {
                    logger.info("收到设备" + requesterId + "的语音广播Invite请求");
                    responseAck(evt, Response.TRYING);
                } catch (ParseException | SipException | InvalidArgumentException e) {
                    e.printStackTrace();
                    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 = SdpFactory.getInstance().createSessionDescription(substring);
                    //  获取支持的格式
                    Vector mediaDescriptions = sdp.getMediaDescriptions(true);
                    // 查看是否支持PS 负载96
                    int port = -1;
                    //boolean recvonly = false;
                    boolean mediaTransmissionTCP = false;
                    Boolean tcpActive = null;
                    for (int i = 0; i < mediaDescriptions.size(); i++) {
                        MediaDescription mediaDescription = (MediaDescription)mediaDescriptions.get(i);
                        Media media = mediaDescription.getMedia();
                        Vector mediaFormats = media.getMediaFormats(false);
                        if (mediaFormats.contains("8")) {
                            port = media.getMediaPort();
                            String protocol = media.getProtocol();
                            // 区分TCP发流还是udp, 当前默认udp
                            if ("TCP/RTP/AVP".equals(protocol)) {
                                String setup = mediaDescription.getAttribute("setup");
                                if (setup != null) {
                                    mediaTransmissionTCP = true;
                                    if ("active".equals(setup)) {
                                        tcpActive = true;
                                    } else if ("passive".equals(setup)) {
                                        tcpActive = false;
                                    }
                                }
                            }
                            break;
                        }
                    }
                    if (port == -1) {
                        logger.info("不支持的媒体格式,返回415");
                        // 回复不支持的格式
                        responseAck(evt, Response.UNSUPPORTED_MEDIA_TYPE); // 不支持的格式,发415
                        return;
                    }
                    String username = sdp.getOrigin().getUsername();
                    String addressStr = sdp.getOrigin().getAddress();
                    logger.info("设备{}请求语音流,地址:{}:{},ssrc:{}", username, addressStr, port, ssrc);
                } else {
                    logger.warn("来自无效设备/平台的请求");
                    responseAck(evt, Response.BAD_REQUEST);
                }
            }));
            if (logger.isDebugEnabled()) {
                logger.debug(playResult.getResult().toString());
            }
        } catch (SipException | InvalidArgumentException | ParseException e) {
src/main/java/com/genersoft/iot/vmp/gb28181/transmit/request/impl/MessageRequestProcessor.java
@@ -93,7 +93,7 @@
    private static final String MESSAGE_ALARM = "Alarm";
    private static final String MESSAGE_RECORD_INFO = "RecordInfo";
    private static final String MESSAGE_MEDIA_STATUS = "MediaStatus";
    // private static final String MESSAGE_BROADCAST = "Broadcast";
    private static final String MESSAGE_BROADCAST = "Broadcast";
    private static final String MESSAGE_DEVICE_STATUS = "DeviceStatus";
    private static final String MESSAGE_DEVICE_CONTROL = "DeviceControl";
    private static final String MESSAGE_DEVICE_CONFIG = "DeviceConfig";
@@ -123,7 +123,7 @@
                logger.info("接收到Catalog消息");
                processMessageCatalogList(evt);
            } else if (MESSAGE_DEVICE_INFO.equals(cmd)) {
                //DeviceInfo消息处理
                // DeviceInfo消息处理
                processMessageDeviceInfo(evt);
            } else if (MESSAGE_DEVICE_STATUS.equals(cmd)) {
                // DeviceStatus消息处理
@@ -149,6 +149,9 @@
            } else if (MESSAGE_PRESET_QUERY.equals(cmd)) {
                logger.info("接收到PresetQuery消息");
                processMessagePresetQuery(evt);
            } else if (MESSAGE_BROADCAST.equals(cmd)) {
                // Broadcast消息处理
                processMessageBroadcast(evt);
            } else {
                logger.info("接收到消息:" + cmd);
                responseAck(evt);
@@ -298,7 +301,7 @@
                // 远程启动功能
                if (!XmlUtil.isEmpty(XmlUtil.getText(rootElement, "TeleBoot"))) {
                    if (deviceId.equals(targetGBId)) {
                        // 远程启动功能:需要在重新启动程序后先对SipStack解绑
                        // 远程启动本平台:需要在重新启动程序后先对SipStack解绑
                        logger.info("执行远程启动本平台命令");
                        ParentPlatform parentPlatform = storager.queryParentPlatById(platformId);
                        cmderFroPlatform.unregister(parentPlatform, null, null);
@@ -333,6 +336,7 @@
                        // 远程启动指定设备
                    }
                }
                // 云台/前端控制命令
                if (!XmlUtil.isEmpty(XmlUtil.getText(rootElement,"PTZCmd")) && !deviceId.equals(targetGBId)) {
                    String cmdString = XmlUtil.getText(rootElement,"PTZCmd");
                    Device device = storager.queryVideoDeviceByPlatformIdAndChannelId(platformId, deviceId);
@@ -895,6 +899,37 @@
        }
    }
    /**
     * 处理AudioBroadcast语音广播Message
     *
     * @param evt
     */
    private void processMessageBroadcast(RequestEvent evt) {
        try {
            Element rootElement = getRootElement(evt);
            String deviceId = XmlUtil.getText(rootElement, "DeviceID");
            // 回复200 OK
            responseAck(evt);
            if (rootElement.getName().equals("Response")) {
                    // 此处是对本平台发出Broadcast指令的应答
                JSONObject json = new JSONObject();
                XmlUtil.node2Json(rootElement, json);
                if (logger.isDebugEnabled()) {
                    logger.debug(json.toJSONString());
                }
                RequestMessage msg = new RequestMessage();
                msg.setDeviceId(deviceId);
                msg.setType(DeferredResultHolder.CALLBACK_CMD_BROADCAST);
                msg.setData(json);
                deferredResultHolder.invokeResult(msg);
            } else {
                // 此处是上级发出的Broadcast指令
            }
        } catch (ParseException | SipException | InvalidArgumentException | DocumentException e) {
            e.printStackTrace();
        }
    }
    /***
     * 回复200 OK
src/main/java/com/genersoft/iot/vmp/gb28181/transmit/response/impl/RegisterResponseProcessor.java
@@ -50,7 +50,6 @@
     */
    @Override
    public void process(ResponseEvent evt, SipLayer layer, SipConfig config) {
        // TODO Auto-generated method stub
        Response response = evt.getResponse();
        CallIdHeader callIdHeader = (CallIdHeader) response.getHeader(CallIdHeader.NAME);
        String callId = callIdHeader.getCallId();
src/main/java/com/genersoft/iot/vmp/storager/IRedisCatchStorage.java
@@ -81,6 +81,8 @@
    void delPlatformRegisterInfo(String callId);
    void cleanPlatformRegisterInfos();
    void updateSendRTPSever(SendRtpItem sendRtpItem);
    /**
src/main/java/com/genersoft/iot/vmp/storager/impl/RedisCatchStorageImpl.java
@@ -13,6 +13,7 @@
import java.util.*;
@SuppressWarnings("rawtypes")
@Component
public class RedisCatchStorageImpl implements IRedisCatchStorage {
@@ -213,6 +214,14 @@
    }
    @Override
    public void cleanPlatformRegisterInfos() {
        List regInfos = redis.scan(VideoManagerConstants.PLATFORM_REGISTER_INFO_PREFIX + "*");
        for (Object key : regInfos) {
            redis.del(key.toString());
        }
    }
    @Override
    public void updateSendRTPSever(SendRtpItem sendRtpItem) {
        String key = VideoManagerConstants.PLATFORM_SEND_RTP_INFO_PREFIX + sendRtpItem.getPlatformId() + "_" + sendRtpItem.getChannelId();
        redis.set(key, sendRtpItem);
src/main/java/com/genersoft/iot/vmp/vmanager/play/PlayController.java
@@ -2,6 +2,7 @@
import com.genersoft.iot.vmp.common.StreamInfo;
import com.genersoft.iot.vmp.conf.MediaServerConfig;
import com.genersoft.iot.vmp.gb28181.bean.Device;
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.ZLMRESTfulUtils;
@@ -26,6 +27,8 @@
import org.springframework.web.context.request.async.DeferredResult;
import java.util.UUID;
import javax.sip.message.Response;
@CrossOrigin
@RestController
@@ -204,5 +207,47 @@
        }
        return new ResponseEntity<String>( result.toJSONString(), HttpStatus.OK);
    }
    /**
     * 语音广播命令API接口
     *
     * @param deviceId
     */
    @GetMapping("/broadcast/{deviceId}")
    @PostMapping("/broadcast/{deviceId}")
    public DeferredResult<ResponseEntity<String>> broadcastApi(@PathVariable String deviceId) {
        if (logger.isDebugEnabled()) {
            logger.debug("语音广播API调用");
        }
        Device device = storager.queryVideoDevice(deviceId);
        cmder.audioBroadcastCmd(device, event -> {
            Response response = event.getResponse();
            RequestMessage msg = new RequestMessage();
            msg.setId(DeferredResultHolder.CALLBACK_CMD_BROADCAST + deviceId);
            JSONObject json = new JSONObject();
            json.put("DeviceID", deviceId);
            json.put("CmdType", "Broadcast");
            json.put("Result", "Failed");
            json.put("Description", String.format("语音广播操作失败,错误码: %s, %s", response.getStatusCode(), response.getReasonPhrase()));
            msg.setData(json);
            resultHolder.invokeResult(msg);
        });
        DeferredResult<ResponseEntity<String>> result = new DeferredResult<ResponseEntity<String>>(3 * 1000L);
        result.onTimeout(() -> {
            logger.warn(String.format("语音广播操作超时, 设备未返回应答指令"));
            RequestMessage msg = new RequestMessage();
            msg.setId(DeferredResultHolder.CALLBACK_CMD_BROADCAST + deviceId);
            JSONObject json = new JSONObject();
            json.put("DeviceID", deviceId);
            json.put("CmdType", "Broadcast");
            json.put("Result", "Failed");
            json.put("Error", "Timeout. Device did not response to broadcast command.");
            msg.setData(json);
            resultHolder.invokeResult(msg);
        });
        resultHolder.put(DeferredResultHolder.CALLBACK_CMD_BROADCAST + deviceId, result);
        return result;
    }
}
web_src/src/components/channelList.vue
@@ -99,7 +99,7 @@
            currentPage: parseInt(this.$route.params.page),
            count: parseInt(this.$route.params.count),
            total: 0,
            beforeUrl: "/videoList",
            beforeUrl: "/deviceList",
            isLoging: false,
            autoList: true
        };
@@ -131,7 +131,7 @@
            this.currentPage = parseInt(this.$route.params.page);
            this.count = parseInt(this.$route.params.count);
            if (this.parentChannelId == "" || this.parentChannelId == 0) {
                this.beforeUrl = "/videoList"
                this.beforeUrl = "/deviceList"
            }
        },
web_src/src/components/devicePosition.vue
@@ -81,7 +81,7 @@
            parentChannelId: this.$route.params.parentChannelId,
            updateLooper: 0, //数据刷新轮训标志
            total: 0,
            beforeUrl: "/videoList",
            beforeUrl: "/deviceList",
            isLoging: false,
            autoList: false,
        };
@@ -111,7 +111,7 @@
            // this.currentPage = parseInt(this.$route.params.page);
            // this.count = parseInt(this.$route.params.count);
            // if (this.parentChannelId == "" || this.parentChannelId == 0) {
            //     this.beforeUrl = "/videoList";
            //     this.beforeUrl = "/deviceList";
            // }
        },
        initBaiduMap() {