.idea/encodings.xml
@@ -3,5 +3,6 @@ <component name="Encoding"> <file url="file://$PROJECT_DIR$" charset="UTF-8" /> <file url="file://$PROJECT_DIR$/src/main/java" charset="UTF-8" /> <file url="file://$PROJECT_DIR$/src/main/java/com/genersoft/iot/vmp/storager/redis/VideoManagerRedisStoragerImpl.java" charset="UTF-8" /> </component> </project> README.md
@@ -6,38 +6,25 @@ ### fork自 [swwheihei/wvp-GB28181](https://github.com/swwheihei/wvp-GB28181) # 应用场景: 主要应用在IPC等设备没有固定IP地址,但需要在互联网中观看的场景。 要求IPC设备可以访问互联网,有云服务器用于部署本服务。 预计7月可以达到商用级别的文档性 原项目比较侧重单个摄像机的接入,当前这个更侧重平台接入,当然,直接接摄像机也是没有问题的。 # 支持特性: 1、视频预览 2、云台控制(方向、缩放控制) 3、视频设备信息同步 4、离在线监控 5、录像查询与回放(基于NVR\DVR,暂不支持快进、seek操作) 6、无人观看自动断流 5、无人观看自动断流 # 2020路线图: 5月中旬-录像回放(基于NVR\DVR)、设备认证(基于密码) 5月下旬-设备报警 6月上旬-流媒体认证(ZLM推流、取流) 6月下旬-语音对讲、Android Deme\iOS Demo 7月下旬-设备认证(基于数字证书)、集群部署 8月下旬-云端录像与回放 9月下旬-Onvif协议支持 10月下旬-GB28181-2011版设备适配 # 待实现: 录像查询与回放(基于NVR\DVR,暂不支持快进、seek操作) 12月底-上级级联、时间同步、其他国标能力 # 项目部署 参考wiki说明 # 使用帮助 参考wiki说明 # 致谢 感谢作者[夏楚](https://github.com/xiongziliang) 提供这么棒的开源流媒体服务框架 感谢作者[kkkkk5G](https://gitee.com/kkkkk5G) 提供这么棒的前端UI []: https://github.com/swwheihei/wvp-GB28181 src/main/java/com/genersoft/iot/vmp/conf/VManagerConfig.java
@@ -21,5 +21,7 @@ public void setDatabase(String database) { this.database = database; } } src/main/java/com/genersoft/iot/vmp/gb28181/bean/DeviceChannel.java
@@ -129,6 +129,11 @@ private double latitude; /** * 子设备数 */ private int subCount; /** * 流唯一编号,存在表示正在直播 */ private String ssrc; @@ -332,4 +337,12 @@ public void setSsrc(String ssrc) { this.ssrc = ssrc; } public int getSubCount() { return subCount; } public void setSubCount(int subCount) { this.subCount = subCount; } } src/main/java/com/genersoft/iot/vmp/gb28181/transmit/SIPProcessorFactory.java
@@ -7,6 +7,7 @@ import javax.sip.message.Request; import javax.sip.message.Response; import com.alibaba.fastjson.JSON; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; @@ -126,6 +127,7 @@ processor.setRequestEvent(evt); return processor; } else if (Request.MESSAGE.equals(method)) { MessageRequestProcessor processor = new MessageRequestProcessor(); processor.setRequestEvent(evt); processor.setTcpSipProvider(getTcpSipProvider()); src/main/java/com/genersoft/iot/vmp/gb28181/transmit/request/impl/MessageRequestProcessor.java
@@ -144,6 +144,7 @@ private void processMessageCatalogList(RequestEvent evt) { try { Element rootElement = getRootElement(evt); String s = rootElement.toString(); Element deviceIdElement = rootElement.element("DeviceID"); String deviceId = deviceIdElement.getText().toString(); Element deviceListElement = rootElement.element("DeviceList"); @@ -171,10 +172,10 @@ DeviceChannel deviceChannel = new DeviceChannel(); deviceChannel.setName(channelName); deviceChannel.setChannelId(channelDeviceId); if(status.equals("ON")) { if(status.equals("ON") || status.equals("On")) { deviceChannel.setStatus(1); } if(status.equals("OFF")) { if(status.equals("OFF") || status.equals("Off")) { deviceChannel.setStatus(0); } @@ -185,7 +186,7 @@ deviceChannel.setBlock(XmlUtil.getText(itemDevice,"Block")); deviceChannel.setAddress(XmlUtil.getText(itemDevice,"Address")); deviceChannel.setParental(itemDevice.element("Parental") == null? 0:Integer.parseInt(XmlUtil.getText(itemDevice,"Parental"))); deviceChannel.setParentId(XmlUtil.getText(itemDevice,"ParentId")); deviceChannel.setParentId(XmlUtil.getText(itemDevice,"ParentID")); deviceChannel.setSafetyWay(itemDevice.element("SafetyWay") == null? 0:Integer.parseInt(XmlUtil.getText(itemDevice,"SafetyWay"))); deviceChannel.setRegisterWay(itemDevice.element("RegisterWay") == null? 1:Integer.parseInt(XmlUtil.getText(itemDevice,"RegisterWay"))); deviceChannel.setCertNum(XmlUtil.getText(itemDevice,"CertNum")); src/main/java/com/genersoft/iot/vmp/storager/IVideoManagerStorager.java
@@ -77,7 +77,7 @@ * @param count 每页数量 * @return */ public PageResult queryChannelsByDeviceId(String deviceId, int page, int count); public PageResult queryChannelsByDeviceId(String deviceId, String query, Boolean hasSubChannel, String online, int page, int count); /** * 获取某个设备的通道列表 @@ -161,6 +161,19 @@ */ public StreamInfo queryPlay(String deviceId, String channelId); /** * 查询子设备 * * @param deviceId * @param channelId * @param page * @param count * @return */ PageResult querySubChannels(String deviceId, String channelId, String query, Boolean hasSubChannel, String online, int page, int count); /** * 更新缓存 */ public void updateCatch(); } src/main/java/com/genersoft/iot/vmp/storager/VodeoMannagerTask.java
New file @@ -0,0 +1,17 @@ package com.genersoft.iot.vmp.storager; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.CommandLineRunner; import org.springframework.stereotype.Component; @Component public class VodeoMannagerTask implements CommandLineRunner { @Autowired private IVideoManagerStorager storager; @Override public void run(String... strings) throws Exception { storager.updateCatch(); } } src/main/java/com/genersoft/iot/vmp/storager/jdbc/VideoManagerJdbcStoragerImpl.java
@@ -79,9 +79,10 @@ } @Override public PageResult queryChannelsByDeviceId(String deviceId, int page, int count) { public PageResult queryChannelsByDeviceId(String deviceId, String query, Boolean hasSubChannel, String online, int page, int count) { return null; } @Override public List<DeviceChannel> queryChannelsByDeviceId(String deviceId) { @@ -161,4 +162,13 @@ return null; } @Override public PageResult querySubChannels(String deviceId, String channelId, String query, Boolean hasSubChannel, String online, int page, int count) { return null; } @Override public void updateCatch() { } } src/main/java/com/genersoft/iot/vmp/storager/redis/VideoManagerRedisStoragerImpl.java
@@ -1,7 +1,6 @@ package com.genersoft.iot.vmp.storager.redis; import java.util.ArrayList; import java.util.List; import java.util.*; import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSONObject; @@ -16,6 +15,7 @@ import com.genersoft.iot.vmp.gb28181.bean.Device; import com.genersoft.iot.vmp.storager.IVideoManagerStorager; import com.genersoft.iot.vmp.utils.redis.RedisUtil; import org.springframework.util.StringUtils; /** * @Description:视频设备数据存储-redis实现 @@ -27,6 +27,8 @@ @Autowired private RedisUtil redis; private HashMap<String, HashMap<String, HashSet<String>>> deviceMap = new HashMap<>(); /** @@ -61,9 +63,12 @@ */ @Override public boolean updateDevice(Device device) { List<Object> deviceChannelList = redis.keys(VideoManagerConstants.CACHEKEY_PREFIX + device.getDeviceId() + "_" + "*"); if (deviceMap.get(device.getDeviceId()) == null) { deviceMap.put(device.getDeviceId(), new HashMap<String, HashSet<String>>()); } // List<Object> deviceChannelList = redis.keys(VideoManagerConstants.CACHEKEY_PREFIX + device.getDeviceId() + "_" + "*"); // 更新device中的通道数量 device.setChannelCount(deviceChannelList.size()); device.setChannelCount(deviceMap.get(device.getDeviceId()).size()); // 存储device return redis.set(VideoManagerConstants.DEVICE_PREFIX+device.getDeviceId(), device); @@ -72,14 +77,49 @@ @Override public void updateChannel(String deviceId, DeviceChannel channel) { String channelId = channel.getChannelId(); HashMap<String, HashSet<String>> channelMap = deviceMap.get(deviceId); if (channelMap == null) return; // 作为父设备, 确定自己的子节点数 if (channelMap.get(channelId) == null) { channelMap.put(channelId, new HashSet<String>()); }else if (channelMap.get(channelId).size()> 0) { channel.setSubCount(channelMap.get(channelId).size()); } // 存储通道 redis.set(VideoManagerConstants.CACHEKEY_PREFIX+deviceId + "_" + channel.getChannelId(), redis.set(VideoManagerConstants.CACHEKEY_PREFIX + deviceId + "_" + channel.getChannelId() + ":" + channel.getName() + "_" + (channel.getStatus() == 1 ? "on":"off") + "_" + (channelMap.get(channelId).size() > 0)+ "_" + channel.getParentId(), channel); List<Object> deviceChannelList = redis.keys(VideoManagerConstants.CACHEKEY_PREFIX + deviceId + "_" + "*"); // 更新device中的通道数量 Device device = (Device)redis.get(VideoManagerConstants.DEVICE_PREFIX+deviceId); device.setChannelCount(deviceChannelList.size()); device.setChannelCount(deviceMap.get(deviceId).size()); redis.set(VideoManagerConstants.DEVICE_PREFIX+device.getDeviceId(), device); // 如果有父设备,更新父设备内子节点数 String parentId = channel.getParentId(); if (!StringUtils.isEmpty(parentId)) { if (channelMap.get(parentId) == null) { channelMap.put(parentId, new HashSet<>()); } channelMap.get(parentId).add(channelId); DeviceChannel deviceChannel = queryChannel(deviceId, parentId); if (deviceChannel != null) { deviceChannel.setSubCount(channelMap.get(parentId).size()); redis.set(VideoManagerConstants.CACHEKEY_PREFIX+deviceId + "_" + deviceChannel.getChannelId(), deviceChannel); } } } /** @@ -94,10 +134,21 @@ } @Override public PageResult queryChannelsByDeviceId(String deviceId, int page, int count) { public PageResult queryChannelsByDeviceId(String deviceId, String query, Boolean hasSubChannel, String online, int page, int count) { List<DeviceChannel> result = new ArrayList<>(); PageResult pageResult = new PageResult<DeviceChannel>(); List<Object> deviceChannelList = redis.keys(VideoManagerConstants.CACHEKEY_PREFIX + deviceId + "_" + "*"); String queryContent = "*"; if (!StringUtils.isEmpty(query)) queryContent = String.format("*%S*",query); String queryHasSubChannel = "*"; if (hasSubChannel != null) queryHasSubChannel = hasSubChannel?"true":"false"; String queryOnline = "*"; if (!StringUtils.isEmpty(online)) queryOnline = online; String queryStr = VideoManagerConstants.CACHEKEY_PREFIX + deviceId + "_" + queryContent + // 搜索编号和名称 "_" + queryOnline + // 搜索是否在线 "_" + queryHasSubChannel + // 搜索是否含有子节点 "_" + "*"; List<Object> deviceChannelList = redis.keys(queryStr); pageResult.setPage(page); pageResult.setCount(count); pageResult.setTotal(deviceChannelList.size()); @@ -125,8 +176,63 @@ } @Override public PageResult querySubChannels(String deviceId, String parentChannelId, String query, Boolean hasSubChannel, String online, int page, int count) { List<DeviceChannel> allDeviceChannels = new ArrayList<>(); String queryContent = "*"; if (!StringUtils.isEmpty(query)) queryContent = String.format("*%S*",query); String queryHasSubChannel = "*"; if (hasSubChannel != null) queryHasSubChannel = hasSubChannel?"true":"false"; String queryOnline = "*"; if (!StringUtils.isEmpty(online)) queryOnline = online; String queryStr = VideoManagerConstants.CACHEKEY_PREFIX + deviceId + "_" + queryContent + // 搜索编号和名称 "_" + queryOnline + // 搜索是否在线 "_" + queryHasSubChannel + // 搜索是否含有子节点 "_" + parentChannelId; List<Object> deviceChannelList = redis.keys(queryStr); if (deviceChannelList != null && deviceChannelList.size() > 0 ) { for (int i = 0; i < deviceChannelList.size(); i++) { DeviceChannel deviceChannel = (DeviceChannel)redis.get((String)deviceChannelList.get(i)); if (deviceChannel.getParentId() != null && deviceChannel.getParentId().equals(parentChannelId)) { allDeviceChannels.add(deviceChannel); } } } int maxCount = (page + 1 ) * count; PageResult pageResult = new PageResult<DeviceChannel>(); pageResult.setPage(page); pageResult.setCount(count); pageResult.setTotal(allDeviceChannels.size()); if (allDeviceChannels.size() > 0) { pageResult.setData(allDeviceChannels.subList( page * count, pageResult.getTotal() > maxCount ? maxCount : pageResult.getTotal() )); } return pageResult; } public List<DeviceChannel> querySubChannels(String deviceId, String parentChannelId) { List<DeviceChannel> allDeviceChannels = new ArrayList<>(); List<Object> deviceChannelList = redis.keys(VideoManagerConstants.CACHEKEY_PREFIX + deviceId + "_" + "*"); if (deviceChannelList != null && deviceChannelList.size() > 0 ) { for (int i = 0; i < deviceChannelList.size(); i++) { DeviceChannel deviceChannel = (DeviceChannel)redis.get((String)deviceChannelList.get(i)); if (deviceChannel.getParentId() != null && deviceChannel.getParentId().equals(parentChannelId)) { allDeviceChannels.add(deviceChannel); } } } return allDeviceChannels; } @Override public DeviceChannel queryChannel(String deviceId, String channelId) { return (DeviceChannel)redis.get(VideoManagerConstants.CACHEKEY_PREFIX + deviceId + "_" + channelId); return (DeviceChannel)redis.get(VideoManagerConstants.CACHEKEY_PREFIX + deviceId + "_" + channelId + "_"); } @@ -257,6 +363,8 @@ return (StreamInfo)redis.get(String.format("%S_%s_%s", VideoManagerConstants.PLAYER_PREFIX, deviceId, channelId)); } /** * 更新流媒体信息 * @param mediaServerConfig @@ -275,4 +383,35 @@ public MediaServerConfig getMediaInfo() { return (MediaServerConfig)redis.get(VideoManagerConstants.MEDIA_SERVER_PREFIX); } @Override public void updateCatch() { deviceMap = new HashMap<>(); // 更新设备 List<Device> devices = queryVideoDeviceList(null); if (devices == null && devices.size() == 0) return; for (Device device : devices) { // 更新设备下的通道 HashMap<String, HashSet<String>> channelMap = new HashMap<String, HashSet<String>>(); List<Object> deviceChannelList = redis.keys(VideoManagerConstants.CACHEKEY_PREFIX + device.getDeviceId() + "_" + "*"); if (deviceChannelList != null && deviceChannelList.size() > 0 ) { for (int i = 0; i < deviceChannelList.size(); i++) { String key = (String)deviceChannelList.get(i); String[] s = key.split("_"); String channelId = s[3]; HashSet<String> subChannel = channelMap.get(channelId); if (subChannel == null) { subChannel = new HashSet<>(); } if (s.length > 4) { subChannel.add(s[4]); } channelMap.put(channelId, subChannel); System.out.println(); } } deviceMap.put(device.getDeviceId(),channelMap); } } } src/main/java/com/genersoft/iot/vmp/vmanager/device/DeviceController.java
@@ -9,12 +9,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.CrossOrigin; 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; import org.springframework.web.bind.annotation.*; import org.springframework.web.context.request.async.DeferredResult; import com.alibaba.fastjson.JSONObject; @@ -72,12 +67,17 @@ * @return 通道列表 */ @GetMapping("devices/{deviceId}/channels") public ResponseEntity<PageResult> channels(@PathVariable String deviceId, int page, int count){ public ResponseEntity<PageResult> channels(@PathVariable String deviceId, int page, int count, @RequestParam(required = false) String query, @RequestParam(required = false) String online, @RequestParam(required = false) Boolean channelType ){ if (logger.isDebugEnabled()) { logger.debug("查询所有视频设备API调用"); } PageResult pageResult = storager.queryChannelsByDeviceId(deviceId, page, count); PageResult pageResult = storager.queryChannelsByDeviceId(deviceId, query, channelType, online, page, count); return new ResponseEntity<>(pageResult,HttpStatus.OK); } @@ -115,4 +115,33 @@ return new ResponseEntity<String>("设备预览API调用失败!", HttpStatus.INTERNAL_SERVER_ERROR); } } /** * 分页查询通道数 * @param channelId 通道id * @param page 当前页 * @param count 每页条数 * @return 子通道列表 */ @GetMapping("subChannels/{deviceId}/{channelId}/channels") public ResponseEntity<PageResult> subChannels(@PathVariable String deviceId, @PathVariable String channelId, int page, int count, @RequestParam(required = false) String query, @RequestParam(required = false) String online, @RequestParam(required = false) Boolean channelType){ if (logger.isDebugEnabled()) { logger.debug("查询所有视频设备API调用"); } DeviceChannel deviceChannel = storager.queryChannel(deviceId,channelId); if (deviceChannel == null) { PageResult<DeviceChannel> deviceChannelPageResult = new PageResult<>(); new ResponseEntity<>(deviceChannelPageResult,HttpStatus.OK); } PageResult pageResult = storager.querySubChannels(deviceId, channelId, query, channelType, online, page, count); return new ResponseEntity<>(pageResult,HttpStatus.OK); } } src/main/java/com/genersoft/iot/vmp/web/ApiDeviceController.java
@@ -68,7 +68,7 @@ devices = storager.queryVideoDeviceList(null); result.put("DeviceCount", devices.size()); }else { PageResult<Device> deviceList = storager.queryVideoDeviceList(null, start, limit); PageResult<Device> deviceList = storager.queryVideoDeviceList(null, start/limit, limit); result.put("DeviceCount", deviceList.getTotal()); devices = deviceList.getData(); } @@ -123,7 +123,7 @@ deviceChannels = storager.queryChannelsByDeviceId(serial); result.put("ChannelCount", deviceChannels.size()); }else { PageResult<DeviceChannel> pageResult = storager.queryChannelsByDeviceId(serial, start, limit); PageResult<DeviceChannel> pageResult = storager.queryChannelsByDeviceId(serial, null, null, null,start/limit, limit); result.put("ChannelCount", pageResult.getTotal()); deviceChannels = pageResult.getData(); } @@ -139,7 +139,7 @@ deviceJOSNChannel.put("Name", deviceChannel.getName()); deviceJOSNChannel.put("Custom", false); deviceJOSNChannel.put("CustomName", ""); deviceJOSNChannel.put("SubCount", 0); // TODO ? 子节点数, SubCount > 0 表示该通道为子目录 deviceJOSNChannel.put("SubCount", deviceChannel.getSubCount()); // TODO ? 子节点数, SubCount > 0 表示该通道为子目录 deviceJOSNChannel.put("SnapURL", ""); deviceJOSNChannel.put("Manufacturer ", deviceChannel.getManufacture()); deviceJOSNChannel.put("Model", deviceChannel.getModel()); src/main/resources/application.yml
@@ -12,7 +12,7 @@ port: 6379 database: 6 #访问密码,若你的redis服务器没有设置密码,就不需要用密码去连接 password: password: 4767cb971b40a1300fa09b7f87b09d1c #超时时间 timeout: 10000 datasource: @@ -31,8 +31,8 @@ # 根据国标6.1.2中规定,domain宜采用ID统一编码的前十位编码。国标附录D中定义前8位为中心编码(由省级、市级、区级、基层编号组成,参照GB/T 2260-2007) # 后两位为行业编码,定义参照附录D.3 # 3701020049标识山东济南历下区 信息行业接入 domain: 3701020049 id: 37010200492000000001 domain: 3402000000 id: 34020000002000000001 # 默认设备认证密码,后续扩展使用设备单独密码 password: 12345678 media: