From 3a502b36a8cfcdd455edb22e5771115559873731 Mon Sep 17 00:00:00 2001 From: songww Date: Sun, 10 May 2020 22:33:21 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=8C=E5=96=84ssrc=E7=AC=A6=E5=90=88?= =?UTF-8?q?=E5=9B=BD=E6=A0=87=EF=BC=8C=E5=B9=B6=E5=AE=8C=E5=96=84=E5=BE=88?= =?UTF-8?q?=E5=A4=9A=E5=B0=8F=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/genersoft/iot/vmp/conf/SipConfig.java | 2 +- .../genersoft/iot/vmp/gb28181/SipLayer.java | 14 +-- .../iot/vmp/gb28181/bean/RecordItem.java | 10 +- .../gb28181/transmit/cmd/ISIPCommander.java | 12 ++- .../cmd/SIPRequestHeaderProvider.java | 10 +- .../transmit/cmd/impl/SIPCommander.java | 96 ++++++++++++++----- .../request/impl/MessageRequestProcessor.java | 58 +++++++++-- .../impl/RegisterRequestProcessor.java | 6 +- .../iot/vmp/gb28181/utils/DateUtil.java | 19 +++- .../iot/vmp/gb28181/utils/SsrcUtil.java | 91 ++++++++++++++++++ .../iot/vmp/utils/SpringBeanFactory.java | 47 +++++++++ .../vmp/vmanager/device/DeviceController.java | 2 - .../vmanager/playback/PlaybackController.java | 47 +++++++++ .../iot/vmp/vmanager/ptz/PtzController.java | 4 +- .../vmp/vmanager/record/RecordController.java | 9 +- src/main/resources/application.yml | 52 +++++++--- 16 files changed, 406 insertions(+), 73 deletions(-) create mode 100644 src/main/java/com/genersoft/iot/vmp/gb28181/utils/SsrcUtil.java create mode 100644 src/main/java/com/genersoft/iot/vmp/utils/SpringBeanFactory.java create mode 100644 src/main/java/com/genersoft/iot/vmp/vmanager/playback/PlaybackController.java diff --git a/src/main/java/com/genersoft/iot/vmp/conf/SipConfig.java b/src/main/java/com/genersoft/iot/vmp/conf/SipConfig.java index 6d530336..339389ff 100644 --- a/src/main/java/com/genersoft/iot/vmp/conf/SipConfig.java +++ b/src/main/java/com/genersoft/iot/vmp/conf/SipConfig.java @@ -3,7 +3,7 @@ package com.genersoft.iot.vmp.conf; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Configuration; -@Configuration +@Configuration("sipConfig") public class SipConfig { @Value("${sip.ip}") diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/SipLayer.java b/src/main/java/com/genersoft/iot/vmp/gb28181/SipLayer.java index 273f4ed4..2f585d2c 100644 --- a/src/main/java/com/genersoft/iot/vmp/gb28181/SipLayer.java +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/SipLayer.java @@ -42,7 +42,7 @@ public class SipLayer implements SipListener, Runnable { private final static Logger logger = LoggerFactory.getLogger(SipLayer.class); @Autowired - private SipConfig config; + private SipConfig sipConfig; private SipProvider tcpSipProvider; @@ -77,7 +77,7 @@ public class SipLayer implements SipListener, Runnable { Properties properties = new Properties(); properties.setProperty("javax.sip.STACK_NAME", "GB28181_SIP"); - properties.setProperty("javax.sip.IP_ADDRESS", config.getSipIp()); + properties.setProperty("javax.sip.IP_ADDRESS", sipConfig.getSipIp()); properties.setProperty("gov.nist.javax.sip.LOG_MESSAGE_CONTENT", "false"); /** * sip_server_log.log 和 sip_debug_log.log public static final int TRACE_NONE = @@ -92,20 +92,20 @@ public class SipLayer implements SipListener, Runnable { startTcpListener(); startUdpListener(); } catch (Exception e) { - logger.error("Sip Server 启动失败! port {" + config.getSipPort() + "}"); + logger.error("Sip Server 启动失败! port {" + sipConfig.getSipPort() + "}"); e.printStackTrace(); } - logger.info("Sip Server 启动成功 port {" + config.getSipPort() + "}"); + logger.info("Sip Server 启动成功 port {" + sipConfig.getSipPort() + "}"); } private void startTcpListener() throws Exception { - ListeningPoint tcpListeningPoint = sipStack.createListeningPoint(config.getSipIp(), config.getSipPort(), "TCP"); + ListeningPoint tcpListeningPoint = sipStack.createListeningPoint(sipConfig.getSipIp(), sipConfig.getSipPort(), "TCP"); tcpSipProvider = sipStack.createSipProvider(tcpListeningPoint); tcpSipProvider.addSipListener(this); } private void startUdpListener() throws Exception { - ListeningPoint udpListeningPoint = sipStack.createListeningPoint(config.getSipIp(), config.getSipPort(), "UDP"); + ListeningPoint udpListeningPoint = sipStack.createListeningPoint(sipConfig.getSipIp(), sipConfig.getSipPort(), "UDP"); udpSipProvider = sipStack.createSipProvider(udpListeningPoint); udpSipProvider.addSipListener(this); } @@ -126,7 +126,7 @@ public class SipLayer implements SipListener, Runnable { int status = response.getStatusCode(); if ((status >= 200) && (status < 300)) { // Success! ISIPResponseProcessor processor = processorFactory.createResponseProcessor(evt); - processor.process(evt, this, config); + processor.process(evt, this, sipConfig); } else { logger.warn("接收到失败的response响应!status:" + status + ",message:" + response.getContent().toString()); } diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/bean/RecordItem.java b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/RecordItem.java index b0e713bb..484c25ed 100644 --- a/src/main/java/com/genersoft/iot/vmp/gb28181/bean/RecordItem.java +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/bean/RecordItem.java @@ -23,7 +23,7 @@ public class RecordItem { private String type; - private String recordId; + private String recorderId; public String getDeviceId() { return deviceId; @@ -81,12 +81,12 @@ public class RecordItem { this.type = type; } - public String getRecordId() { - return recordId; + public String getRecorderId() { + return recorderId; } - public void setRecordId(String recordId) { - this.recordId = recordId; + public void setRecordId(String recorderId) { + this.recorderId = recorderId; } public String getEndTime() { diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/cmd/ISIPCommander.java b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/cmd/ISIPCommander.java index f226bf51..b0fa6cd9 100644 --- a/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/cmd/ISIPCommander.java +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/cmd/ISIPCommander.java @@ -71,6 +71,16 @@ public interface ISIPCommander { */ public String playStreamCmd(Device device,String channelId); + /** + * 请求回放视频流 + * + * @param device 视频设备 + * @param channelId 预览通道 + * @param startTime 开始时间,格式要求:yyyy-MM-dd HH:mm:ss + * @param endTime 结束时间,格式要求:yyyy-MM-dd HH:mm:ss + */ + public String playbackStreamCmd(Device device,String channelId, String recordId, String startTime, String endTime); + /** * 语音广播 * @@ -153,7 +163,7 @@ public interface ISIPCommander { * @param startTime 开始时间,格式要求:yyyy-MM-dd HH:mm:ss * @param endTime 结束时间,格式要求:yyyy-MM-dd HH:mm:ss */ - public boolean recordInfoQuery(Device device, String startTime, String endTime); + public boolean recordInfoQuery(Device device, String channelId, String startTime, String endTime); /** * 查询报警信息 diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/cmd/SIPRequestHeaderProvider.java b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/cmd/SIPRequestHeaderProvider.java index 55aa9fea..5adbf66a 100644 --- a/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/cmd/SIPRequestHeaderProvider.java +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/cmd/SIPRequestHeaderProvider.java @@ -35,7 +35,7 @@ public class SIPRequestHeaderProvider { private SipLayer layer; @Autowired - private SipConfig config; + private SipConfig sipConfig; public Request createMessageRequest(Device device, String content, String viaTag, String fromTag, String toTag) throws ParseException, InvalidArgumentException { Request request = null; @@ -44,12 +44,12 @@ public class SIPRequestHeaderProvider { SipURI requestURI = layer.getAddressFactory().createSipURI(device.getDeviceId(), host.getAddress()); // via ArrayList viaHeaders = new ArrayList(); - ViaHeader viaHeader = layer.getHeaderFactory().createViaHeader(config.getSipIp(), config.getSipPort(), + ViaHeader viaHeader = layer.getHeaderFactory().createViaHeader(sipConfig.getSipIp(), sipConfig.getSipPort(), device.getTransport(), viaTag); viaHeaders.add(viaHeader); // from SipURI fromSipURI = layer.getAddressFactory().createSipURI(device.getDeviceId(), - config.getSipIp() + ":" + config.getSipPort()); + sipConfig.getSipIp() + ":" + sipConfig.getSipPort()); Address fromAddress = layer.getAddressFactory().createAddress(fromSipURI); FromHeader fromHeader = layer.getHeaderFactory().createFromHeader(fromAddress, fromTag); // to @@ -78,11 +78,11 @@ public class SIPRequestHeaderProvider { SipURI requestLine = layer.getAddressFactory().createSipURI(device.getDeviceId(), host.getAddress()); //via ArrayList viaHeaders = new ArrayList(); - ViaHeader viaHeader = layer.getHeaderFactory().createViaHeader(config.getSipIp(), config.getSipPort(), device.getTransport(), viaTag); + ViaHeader viaHeader = layer.getHeaderFactory().createViaHeader(sipConfig.getSipIp(), sipConfig.getSipPort(), device.getTransport(), viaTag); viaHeader.setRPort(); viaHeaders.add(viaHeader); //from - SipURI fromSipURI = layer.getAddressFactory().createSipURI(device.getDeviceId(),config.getSipIp()+":"+config.getSipPort()); + SipURI fromSipURI = layer.getAddressFactory().createSipURI(device.getDeviceId(),sipConfig.getSipIp()+":"+sipConfig.getSipPort()); Address fromAddress = layer.getAddressFactory().createAddress(fromSipURI); FromHeader fromHeader = layer.getHeaderFactory().createFromHeader(fromAddress, fromTag); //必须要有标记,否则无法创建会话,无法回应ack //to diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/cmd/impl/SIPCommander.java b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/cmd/impl/SIPCommander.java index 630d44ce..b012f9a4 100644 --- a/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/cmd/impl/SIPCommander.java +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/cmd/impl/SIPCommander.java @@ -17,6 +17,9 @@ import com.genersoft.iot.vmp.gb28181.bean.Device; import com.genersoft.iot.vmp.gb28181.transmit.cmd.ISIPCommander; import com.genersoft.iot.vmp.gb28181.transmit.cmd.SIPRequestHeaderProvider; import com.genersoft.iot.vmp.gb28181.utils.DateUtil; +import com.genersoft.iot.vmp.gb28181.utils.SsrcUtil; + +import tk.mybatis.mapper.util.StringUtil; /** * @Description:设备能力接口,用于定义设备的控制、查询能力 @@ -27,7 +30,7 @@ import com.genersoft.iot.vmp.gb28181.utils.DateUtil; public class SIPCommander implements ISIPCommander { @Autowired - private SipConfig config; + private SipConfig sipConfig; @Autowired private SIPRequestHeaderProvider headerProvider; @@ -46,7 +49,7 @@ public class SIPCommander implements ISIPCommander { */ @Override public boolean ptzdirectCmd(Device device, String channelId, int leftRight, int upDown) { - return ptzCmd(device, channelId, leftRight, upDown, 0, config.getSpeed(), 0); + return ptzCmd(device, channelId, leftRight, upDown, 0, sipConfig.getSpeed(), 0); } /** @@ -72,7 +75,7 @@ public class SIPCommander implements ISIPCommander { */ @Override public boolean ptzZoomCmd(Device device, String channelId, int inOut) { - return ptzCmd(device, channelId, 0, 0, inOut, 0, config.getSpeed()); + return ptzCmd(device, channelId, 0, 0, inOut, 0, sipConfig.getSpeed()); } /** @@ -135,23 +138,19 @@ public class SIPCommander implements ISIPCommander { public String playStreamCmd(Device device, String channelId) { try { - //生成ssrc标识数据流 10位数字 - String ssrc = ""; - Random random = new Random(); - // ZLMediaServer最大识别7FFFFFFF即2147483647,所以随机数不能超过这个数 - ssrc = String.valueOf(random.nextInt(2147483647)); + String ssrc = SsrcUtil.getPlaySsrc(); // StringBuffer content = new StringBuffer(200); content.append("v=0\r\n"); - content.append("o="+channelId+" 0 0 IN IP4 "+config.getSipIp()+"\r\n"); + content.append("o="+channelId+" 0 0 IN IP4 "+sipConfig.getSipIp()+"\r\n"); content.append("s=Play\r\n"); - content.append("c=IN IP4 "+config.getMediaIp()+"\r\n"); + content.append("c=IN IP4 "+sipConfig.getMediaIp()+"\r\n"); content.append("t=0 0\r\n"); if(device.getTransport().equals("TCP")) { - content.append("m=video "+config.getMediaPort()+" TCP/RTP/AVP 96 98 97\r\n"); + content.append("m=video "+sipConfig.getMediaPort()+" TCP/RTP/AVP 96 98 97\r\n"); } if(device.getTransport().equals("UDP")) { - content.append("m=video "+config.getMediaPort()+" RTP/AVP 96 98 97\r\n"); + content.append("m=video "+sipConfig.getMediaPort()+" RTP/AVP 96 98 97\r\n"); } content.append("a=sendrecv\r\n"); content.append("a=rtpmap:96 PS/90000\r\n"); @@ -172,6 +171,53 @@ public class SIPCommander implements ISIPCommander { return null; } } + + /** + * 请求回放视频流 + * + * @param device 视频设备 + * @param channelId 预览通道 + * @param startTime 开始时间,格式要求:yyyy-MM-dd HH:mm:ss + * @param endTime 结束时间,格式要求:yyyy-MM-dd HH:mm:ss + */ + @Override + public String playbackStreamCmd(Device device, String channelId, String recordId, String startTime, String endTime) { + try { + + String ssrc = SsrcUtil.getPlayBackSsrc(); + // + StringBuffer content = new StringBuffer(200); + content.append("v=0\r\n"); + content.append("o="+channelId+" 0 0 IN IP4 "+sipConfig.getSipIp()+"\r\n"); + content.append("s=Playback\r\n"); + content.append("u="+recordId+":3\r\n"); + content.append("c=IN IP4 "+sipConfig.getMediaIp()+"\r\n"); + content.append("t="+DateUtil.yyyy_MM_dd_HH_mm_ssToTimestamp(startTime)+" "+DateUtil.yyyy_MM_dd_HH_mm_ssToTimestamp(endTime) +"\r\n"); + if(device.getTransport().equals("TCP")) { + content.append("m=video "+sipConfig.getMediaPort()+" TCP/RTP/AVP 96 98 97\r\n"); + } + if(device.getTransport().equals("UDP")) { + content.append("m=video "+sipConfig.getMediaPort()+" RTP/AVP 96 98 97\r\n"); + } + content.append("a=recvonly\r\n"); + content.append("a=rtpmap:96 PS/90000\r\n"); + content.append("a=rtpmap:98 H264/90000\r\n"); + content.append("a=rtpmap:97 MPEG4/90000\r\n"); + if(device.getTransport().equals("TCP")){ + content.append("a=setup:passive\r\n"); + content.append("a=connection:new\r\n"); + } + content.append("y="+ssrc+"\r\n");//ssrc + + Request request = headerProvider.createInviteRequest(device, content.toString(), null, "live", null); + + transmitRequest(device, request); + return ssrc; + } catch ( SipException | ParseException | InvalidArgumentException e) { + e.printStackTrace(); + return null; + } + } /** * 语音广播 @@ -323,22 +369,23 @@ public class SIPCommander implements ISIPCommander { * @param endTime 结束时间,格式要求:yyyy-MM-dd HH:mm:ss */ @Override - public boolean recordInfoQuery(Device device, String startTime, String endTime) { + public boolean recordInfoQuery(Device device, String channelId, String startTime, String endTime) { try { - StringBuffer catalogXml = new StringBuffer(200); - catalogXml.append(""); - catalogXml.append(""); - catalogXml.append("RecordInfo"); - catalogXml.append("" + (int)((Math.random()*9+1)*100000) + ""); - catalogXml.append("" + device.getDeviceId() + ""); - catalogXml.append("" + DateUtil.yyyy_MM_dd_HH_mm_ssToISO8601(startTime) + ""); - catalogXml.append("" + DateUtil.yyyy_MM_dd_HH_mm_ssToISO8601(endTime) + ""); + StringBuffer recordInfoXml = new StringBuffer(200); + recordInfoXml.append(""); + recordInfoXml.append(""); + recordInfoXml.append("RecordInfo"); + recordInfoXml.append("" + (int)((Math.random()*9+1)*100000) + ""); + recordInfoXml.append("" + channelId + ""); + recordInfoXml.append("" + DateUtil.yyyy_MM_dd_HH_mm_ssToISO8601(startTime) + ""); + recordInfoXml.append("" + DateUtil.yyyy_MM_dd_HH_mm_ssToISO8601(endTime) + ""); + recordInfoXml.append("0"); // 大华NVR要求必须增加一个值为all的文本元素节点Type - catalogXml.append("all"); - catalogXml.append(""); + recordInfoXml.append("all"); + recordInfoXml.append(""); - Request request = headerProvider.createMessageRequest(device, catalogXml.toString(), "ViaRecordInfoBranch", "FromRecordInfoTag", "ToRecordInfoTag"); + Request request = headerProvider.createMessageRequest(device, recordInfoXml.toString(), "ViaRecordInfoBranch", "FromRecordInfoTag", "ToRecordInfoTag"); transmitRequest(device, request); } catch (SipException | ParseException | InvalidArgumentException e) { e.printStackTrace(); @@ -398,4 +445,5 @@ public class SIPCommander implements ISIPCommander { sipLayer.getUdpSipProvider().sendRequest(request); } } + } diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/request/impl/MessageRequestProcessor.java b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/request/impl/MessageRequestProcessor.java index 453420a8..9a39d5c2 100644 --- a/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/request/impl/MessageRequestProcessor.java +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/request/impl/MessageRequestProcessor.java @@ -19,6 +19,8 @@ import org.dom4j.Document; import org.dom4j.DocumentException; import org.dom4j.Element; import org.dom4j.io.SAXReader; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; @@ -36,6 +38,7 @@ import com.genersoft.iot.vmp.gb28181.transmit.request.ISIPRequestProcessor; import com.genersoft.iot.vmp.gb28181.utils.DateUtil; import com.genersoft.iot.vmp.gb28181.utils.XmlUtil; import com.genersoft.iot.vmp.storager.IVideoManagerStorager; +import com.genersoft.iot.vmp.utils.redis.RedisUtil; /** * @Description:MESSAGE请求处理器 @@ -44,7 +47,9 @@ import com.genersoft.iot.vmp.storager.IVideoManagerStorager; */ @Component public class MessageRequestProcessor implements ISIPRequestProcessor { - + + private final static Logger logger = LoggerFactory.getLogger(MessageRequestProcessor.class); + private ServerTransaction transaction; private SipLayer layer; @@ -58,9 +63,14 @@ public class MessageRequestProcessor implements ISIPRequestProcessor { @Autowired private EventPublisher publisher; + @Autowired + private RedisUtil redis; + @Autowired private DeferredResultHolder deferredResultHolder; + private final static String CACHE_RECORDINFO_KEY = "CACHE_RECORDINFO_"; + /** * 处理MESSAGE请求 * @@ -77,14 +87,19 @@ public class MessageRequestProcessor implements ISIPRequestProcessor { Request request = evt.getRequest(); if (new String(request.getRawContent()).contains("Keepalive")) { + logger.info("接收到KeepAlive消息"); processMessageKeepAlive(evt); } else if (new String(request.getRawContent()).contains("Catalog")) { + logger.info("接收到Catalog消息"); processMessageCatalogList(evt); } else if (new String(request.getRawContent()).contains("DeviceInfo")) { + logger.info("接收到DeviceInfo消息"); processMessageDeviceInfo(evt); } else if (new String(request.getRawContent()).contains("Alarm")) { + logger.info("接收到Alarm消息"); processMessageAlarm(evt); - } else if (new String(request.getRawContent()).contains("recordInfo")) { + } else if (new String(request.getRawContent()).contains("RecordInfo")) { + logger.info("接收到RecordInfo消息"); processMessageRecordInfo(evt); } @@ -245,6 +260,7 @@ public class MessageRequestProcessor implements ISIPRequestProcessor { /*** * 收到catalog设备目录列表请求 处理 + * TODO 过期时间暂时写死180秒,后续与DeferredResult超时时间保持一致 * @param evt */ private void processMessageRecordInfo(RequestEvent evt) { @@ -256,15 +272,15 @@ public class MessageRequestProcessor implements ISIPRequestProcessor { recordInfo.setDeviceId(deviceId); recordInfo.setName(XmlUtil.getText(rootElement,"Name")); recordInfo.setSumNum(Integer.parseInt(XmlUtil.getText(rootElement,"SumNum"))); + String sn = XmlUtil.getText(rootElement,"SN"); Element recordListElement = rootElement.element("RecordList"); if (recordListElement == null) { return; } Iterator recordListIterator = recordListElement.elementIterator(); + List recordList = new ArrayList(); if (recordListIterator != null) { - - List recordList = new ArrayList(); RecordItem record = new RecordItem(); // 遍历DeviceList while (recordListIterator.hasNext()) { @@ -273,6 +289,7 @@ public class MessageRequestProcessor implements ISIPRequestProcessor { if (recordElement == null) { continue; } + record = new RecordItem(); record.setDeviceId(XmlUtil.getText(itemRecord,"DeviceID")); record.setName(XmlUtil.getText(itemRecord,"Name")); record.setFilePath(XmlUtil.getText(itemRecord,"FilePath")); @@ -281,13 +298,42 @@ public class MessageRequestProcessor implements ISIPRequestProcessor { record.setEndTime(DateUtil.ISO8601Toyyyy_MM_dd_HH_mm_ss(XmlUtil.getText(itemRecord,"EndTime"))); record.setSecrecy(itemRecord.element("Secrecy") == null? 0:Integer.parseInt(XmlUtil.getText(itemRecord,"Secrecy"))); record.setType(XmlUtil.getText(itemRecord,"Type")); - record.setRecordId(XmlUtil.getText(itemRecord,"RecordID")); + record.setRecordId(XmlUtil.getText(itemRecord,"RecorderID")); recordList.add(record); } recordInfo.setRecordList(recordList); } - + // 存在录像且如果当前录像明细个数小于总条数,说明拆包返回,需要组装,暂不返回 + if (recordInfo.getSumNum() > 0 && recordList.size() > 0 && recordList.size() < recordInfo.getSumNum()) { + // 为防止连续请求该设备的录像数据,返回数据错乱,特增加sn进行区分 + String cacheKey = CACHE_RECORDINFO_KEY+deviceId+sn; + // TODO 暂时直接操作redis存储,后续封装专用缓存接口,改为本地内存缓存 + if (redis.hasKey(cacheKey)) { + List previousList = (List) redis.get(cacheKey); + if (previousList != null && previousList.size() > 0) { + recordList.addAll(previousList); + } + // 本分支表示录像列表被拆包,且加上之前的数据还是不够,保存缓存返回,等待下个包再处理 + if (recordList.size() < recordInfo.getSumNum()) { + redis.set(cacheKey, recordList, 180); + return; + } else { + // 本分支表示录像被拆包,但加上之前的数据够足够,返回响应 + // 因设备心跳有监听redis过期机制,为提高性能,此处手动删除 + redis.del(cacheKey); + } + } else { + // 本分支有两种可能:1、录像列表被拆包,且是第一个包,直接保存缓存返回,等待下个包再处理 + // 2、之前有包,但超时清空了,那么这次sn批次的响应数据已经不完整,等待过期时间后redis自动清空数据 + redis.set(cacheKey, recordList, 180); + return; + } + + } + // 走到这里,有以下可能:1、没有录像信息,第一次收到recordinfo的消息即返回响应数据,无redis操作 + // 2、有录像数据,且第一次即收到完整数据,返回响应数据,无redis操作 + // 3、有录像数据,在超时时间内收到多次包组装后数量足够,返回数据 RequestMessage msg = new RequestMessage(); msg.setDeviceId(deviceId); msg.setType(DeferredResultHolder.CALLBACK_CMD_RECORDINFO); diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/request/impl/RegisterRequestProcessor.java b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/request/impl/RegisterRequestProcessor.java index ebb6db43..7ca2e439 100644 --- a/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/request/impl/RegisterRequestProcessor.java +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/transmit/request/impl/RegisterRequestProcessor.java @@ -45,7 +45,7 @@ import gov.nist.javax.sip.header.Expires; public class RegisterRequestProcessor implements ISIPRequestProcessor { @Autowired - private SipConfig config; + private SipConfig sipConfig; @Autowired private RegisterLogicHandler handler; @@ -77,7 +77,7 @@ public class RegisterRequestProcessor implements ISIPRequestProcessor { // 校验密码是否正确 if (authorhead != null) { passwordCorrect = new DigestServerAuthenticationHelper().doAuthenticatePlainTextPassword(request, - config.getSipPassword()); + sipConfig.getSipPassword()); } // 未携带授权头或者密码错误 回复401 @@ -89,7 +89,7 @@ public class RegisterRequestProcessor implements ISIPRequestProcessor { System.out.println("密码错误 回复401"); } response = layer.getMessageFactory().createResponse(Response.UNAUTHORIZED, request); - new DigestServerAuthenticationHelper().generateChallenge(layer.getHeaderFactory(), response, config.getSipDomain()); + new DigestServerAuthenticationHelper().generateChallenge(layer.getHeaderFactory(), response, sipConfig.getSipDomain()); } // 携带授权头并且密码正确 else if (passwordCorrect) { diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/utils/DateUtil.java b/src/main/java/com/genersoft/iot/vmp/gb28181/utils/DateUtil.java index e241bd50..5950d17e 100644 --- a/src/main/java/com/genersoft/iot/vmp/gb28181/utils/DateUtil.java +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/utils/DateUtil.java @@ -2,6 +2,7 @@ package com.genersoft.iot.vmp.gb28181.utils; import java.text.ParseException; import java.text.SimpleDateFormat; +import java.util.Date; import java.util.Locale; /** @@ -11,7 +12,8 @@ import java.util.Locale; */ public class DateUtil { - private static final String yyyy_MM_dd_T_HH_mm_ss_SSSXXX = "yyyy-MM-dd'T'HH:mm:ss.SSSXXX"; + //private static final String yyyy_MM_dd_T_HH_mm_ss_SSSXXX = "yyyy-MM-dd'T'HH:mm:ss.SSSXXX"; + private static final String yyyy_MM_dd_T_HH_mm_ss_SSSXXX = "yyyy-MM-dd'T'HH:mm:ss"; private static final String yyyy_MM_dd_HH_mm_ss = "yyyy-MM-dd HH:mm:ss"; public static String yyyy_MM_dd_HH_mm_ssToISO8601(String formatTime) { @@ -37,4 +39,19 @@ public class DateUtil { } return ""; } + + public static long yyyy_MM_dd_HH_mm_ssToTimestamp(String formatTime) { + SimpleDateFormat format=new SimpleDateFormat(yyyy_MM_dd_HH_mm_ss); + //设置要读取的时间字符串格式 + Date date; + try { + date = format.parse(formatTime); + Long timestamp=date.getTime(); + //转换为Date类 + return timestamp; + } catch (ParseException e) { + e.printStackTrace(); + } + return 0; + } } diff --git a/src/main/java/com/genersoft/iot/vmp/gb28181/utils/SsrcUtil.java b/src/main/java/com/genersoft/iot/vmp/gb28181/utils/SsrcUtil.java new file mode 100644 index 00000000..023b4f95 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/gb28181/utils/SsrcUtil.java @@ -0,0 +1,91 @@ +package com.genersoft.iot.vmp.gb28181.utils; + +import java.util.ArrayList; +import java.util.List; +import java.util.Random; + +import com.genersoft.iot.vmp.conf.SipConfig; +import com.genersoft.iot.vmp.utils.SpringBeanFactory; + +/** + * @Description:SIP信令中的SSRC工具类。SSRC值由10位十进制整数组成的字符串,第一位为0代表实况,为1则代表回放;第二位至第六位由监控域ID的第4位到第8位组成;最后4位为不重复的4个整数 + * @author: songww + * @date: 2020年5月10日 上午11:57:57 + */ +public class SsrcUtil { + + private static String ssrcPrefix; + + private static List isUsed; + + private static List notUsed; + + private static void init() { + SipConfig sipConfig = (SipConfig) SpringBeanFactory.getBean("sipConfig"); + ssrcPrefix = sipConfig.getSipDomain().substring(4, 9); + isUsed = new ArrayList(); + notUsed = new ArrayList(); + for (int i = 1; i < 10000; i++) { + if (i < 10) { + notUsed.add("000" + i); + } else if (i < 100) { + notUsed.add("00" + i); + } else if (i < 1000) { + notUsed.add("0" + i); + } else { + notUsed.add(String.valueOf(i)); + } + } + } + + /** + * 获取视频预览的SSRC值,第一位固定为0 + * + */ + public static String getPlaySsrc() { + return "0" + getSsrcPrefix() + getSN(); + } + + /** + * 获取录像回放的SSRC值,第一位固定为1 + * + */ + public static String getPlayBackSsrc() { + return "1" + getSsrcPrefix() + getSN(); + } + + /** + * 释放ssrc,主要用完的ssrc一定要释放,否则会耗尽 + * + */ + public static void releaseSsrc(String ssrc) { + String sn = ssrc.substring(6); + isUsed.remove(sn); + notUsed.add(sn); + } + + /** + * 获取后四位数SN,随机数 + * + */ + private static String getSN() { + String sn = null; + if (notUsed.size() == 0) { + throw new RuntimeException("ssrc已经用完"); + } else if (notUsed.size() == 1) { + sn = notUsed.get(0); + } else { + sn = notUsed.get(new Random().nextInt(notUsed.size() - 1)); + } + notUsed.remove(0); + isUsed.add(sn); + return sn; + } + + private static String getSsrcPrefix() { + if (ssrcPrefix == null) { + init(); + } + return ssrcPrefix; + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/utils/SpringBeanFactory.java b/src/main/java/com/genersoft/iot/vmp/utils/SpringBeanFactory.java new file mode 100644 index 00000000..273f5fbb --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/utils/SpringBeanFactory.java @@ -0,0 +1,47 @@ +package com.genersoft.iot.vmp.utils; + +import org.springframework.beans.BeansException; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.stereotype.Component; + +/** + * @Description:spring bean获取工厂,获取spring中的已初始化的bean + * @author: songww + * @date: 2019年6月25日 下午4:51:52 + * + */ +@Component +public class SpringBeanFactory implements ApplicationContextAware { + + // Spring应用上下文环境 + private static ApplicationContext applicationContext; + + /** + * 实现ApplicationContextAware接口的回调方法,设置上下文环境 + */ + @Override + public void setApplicationContext(ApplicationContext applicationContext) + throws BeansException { + SpringBeanFactory.applicationContext = applicationContext; + } + + public static ApplicationContext getApplicationContext() { + return applicationContext; + } + + /** + * 获取对象 这里重写了bean方法,起主要作用 + */ + public static Object getBean(String beanId) throws BeansException { + return applicationContext.getBean(beanId); + } + + /** + * 获取当前环境 + */ + public static String getActiveProfile() { + return applicationContext.getEnvironment().getActiveProfiles()[0]; + } + +} diff --git a/src/main/java/com/genersoft/iot/vmp/vmanager/device/DeviceController.java b/src/main/java/com/genersoft/iot/vmp/vmanager/device/DeviceController.java index ce869108..3115933d 100644 --- a/src/main/java/com/genersoft/iot/vmp/vmanager/device/DeviceController.java +++ b/src/main/java/com/genersoft/iot/vmp/vmanager/device/DeviceController.java @@ -1,8 +1,6 @@ package com.genersoft.iot.vmp.vmanager.device; import java.util.List; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; import org.slf4j.Logger; import org.slf4j.LoggerFactory; diff --git a/src/main/java/com/genersoft/iot/vmp/vmanager/playback/PlaybackController.java b/src/main/java/com/genersoft/iot/vmp/vmanager/playback/PlaybackController.java new file mode 100644 index 00000000..4be61105 --- /dev/null +++ b/src/main/java/com/genersoft/iot/vmp/vmanager/playback/PlaybackController.java @@ -0,0 +1,47 @@ +package com.genersoft.iot.vmp.vmanager.playback; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import com.genersoft.iot.vmp.gb28181.bean.Device; +import com.genersoft.iot.vmp.gb28181.transmit.cmd.impl.SIPCommander; +import com.genersoft.iot.vmp.storager.IVideoManagerStorager; + +@RestController +@RequestMapping("/api") +public class PlaybackController { + + private final static Logger logger = LoggerFactory.getLogger(PlaybackController.class); + + @Autowired + private SIPCommander cmder; + + @Autowired + private IVideoManagerStorager storager; + + @GetMapping("/playback/{deviceId}/{channelId}") + public ResponseEntity play(@PathVariable String deviceId,@PathVariable String channelId, String startTime, String endTime){ + + Device device = storager.queryVideoDevice(deviceId); + String ssrc = cmder.playStreamCmd(device, channelId); + + if (logger.isDebugEnabled()) { + logger.debug(String.format("设备预览 API调用,deviceId:%s ,channelId:%s",deviceId, channelId)); + logger.debug("设备预览 API调用,ssrc:"+ssrc+",ZLMedia streamId:"+Integer.toHexString(Integer.parseInt(ssrc))); + } + + if(ssrc!=null) { + return new ResponseEntity(ssrc,HttpStatus.OK); + } else { + logger.warn("设备预览API调用失败!"); + return new ResponseEntity(HttpStatus.INTERNAL_SERVER_ERROR); + } + } +} diff --git a/src/main/java/com/genersoft/iot/vmp/vmanager/ptz/PtzController.java b/src/main/java/com/genersoft/iot/vmp/vmanager/ptz/PtzController.java index bc9792e9..29279ef5 100644 --- a/src/main/java/com/genersoft/iot/vmp/vmanager/ptz/PtzController.java +++ b/src/main/java/com/genersoft/iot/vmp/vmanager/ptz/PtzController.java @@ -5,8 +5,8 @@ import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @@ -37,7 +37,7 @@ public class PtzController { * @param zoomSpeed * @return */ - @GetMapping("/ptz/{deviceId}_{channelId}") + @PostMapping("/ptz/{deviceId}/{channelId}") public ResponseEntity ptz(@PathVariable String deviceId,@PathVariable String channelId,int leftRight, int upDown, int inOut, int moveSpeed, int zoomSpeed){ if (logger.isDebugEnabled()) { diff --git a/src/main/java/com/genersoft/iot/vmp/vmanager/record/RecordController.java b/src/main/java/com/genersoft/iot/vmp/vmanager/record/RecordController.java index c115dd06..5b947c9e 100644 --- a/src/main/java/com/genersoft/iot/vmp/vmanager/record/RecordController.java +++ b/src/main/java/com/genersoft/iot/vmp/vmanager/record/RecordController.java @@ -31,17 +31,18 @@ public class RecordController { @Autowired private DeferredResultHolder resultHolder; - @GetMapping("/recordinfo/{deviceId}") - public DeferredResult> recordinfo(@PathVariable String deviceId, String startTime, String endTime){ + @GetMapping("/record/{deviceId}") + public DeferredResult> recordinfo(@PathVariable String deviceId, String channelId, String startTime, String endTime){ if (logger.isDebugEnabled()) { logger.debug(String.format("录像信息 API调用,deviceId:%s ,startTime:%s, startTime:%s",deviceId, startTime, endTime)); } Device device = storager.queryVideoDevice(deviceId); - cmder.recordInfoQuery(device, startTime, endTime); + cmder.recordInfoQuery(device, channelId, startTime, endTime); DeferredResult> result = new DeferredResult>(); - resultHolder.put(DeferredResultHolder.CALLBACK_CMD_CATALOG+deviceId, result); + // 录像查询以channelId作为deviceId查询 + resultHolder.put(DeferredResultHolder.CALLBACK_CMD_RECORDINFO+channelId, result); return result; } } diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 6314fa48..df0a1901 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -1,10 +1,13 @@ spring: application: - name: wvp - # 数据存储方式,暂只支持redis,后续支持jdbc + name: iot-vmp-vmanager + # 影子数据存储方式,支持redis、jdbc database: redis + # 通信方式,支持kafka、http + communicate: http redis: # Redis服务器IP + #host: 10.24.20.63 host: 127.0.0.1 #端口号 port: 6379 @@ -13,24 +16,49 @@ spring: password: #超时时间 timeout: 10000 + # 可用连接实例的最大数目,默认值为8 + maxTotal: 512 + #控制一个pool最多有多少个状态为idle(空闲的)的jedis实例,默认值也是8 + maxIdle: 100 + #最小空闲连接数 + minIdle: 50 + #获取连接时的最大等待毫秒数,小于零:阻塞不确定的时间,默认-1 + maxWaitMillis: 10000 + #每次释放连接的最大数目 + numTestsPerEvictionRun: 100 + #释放连接的扫描间隔(毫秒) + timeBetweenEvictionRunsMillis: 3000 + #连接最小空闲时间 + minEvictableIdleTimeMillis: 1800000 + #连接空闲多久后释放,当空闲时间>该值且空闲连接>最大空闲连接数时直接释放 + softMinEvictableIdleTimeMillis: 10000 + #在获取连接的时候检查有效性,默认false + testOnBorrow: true + #在空闲时检查有效性,默认false + testWhileIdle: true + #在归还给pool时,是否提前进行validate操作 + testOnReturn: true + #连接耗尽时是否阻塞,false报异常,ture阻塞直到超时,默认true + blockWhenExhausted: false datasource: - name: wcp - url: jdbc:mysql://127.0.0.1:3306/wcp?useUnicode=true&characterEncoding=UTF8&rewriteBatchedStatements=true + name: eiot + url: jdbc:mysql://10.24.20.63:3306/eiot?useUnicode=true&characterEncoding=UTF8&rewriteBatchedStatements=true username: root - password: 123456 + password: Ptjsinspur19.? type: com.alibaba.druid.pool.DruidDataSource driver-class-name: com.mysql.jdbc.Driver server: port: 8080 sip: - # 本地服务地址 - ip: 192.168.0.3 - server_id: 34020000002000000001 + ip: 10.200.64.63 port: 5060 - domain: 34020000 - # 暂时使用统一密码,后续改为一机一密 + # 根据国标6.1.2中规定,domain宜采用ID统一编码的前十位编码。国标附录D中定义前8位为中心编码(由省级、市级、区级、基层编号组成,参照GB/T 2260-2007) + # 后两位为行业编码,定义参照附录D.3 + # 3701020049标识山东济南历下区 信息行业接入 + domain: 3701020049 + server_id: 37010200492000000001 + # 默认设备认证密码,后续扩展使用设备单独密码 password: admin media: - # ZLMediaServer IP - ip: 192.168.0.4 + ip: 10.200.64.88 port: 10000 \ No newline at end of file