Commit ad93be12fb3705e995cf0dab933760edaef3dd7a
1 parent
d881cd7e
添加云端录像功能
Showing
23 changed files
with
541 additions
and
93 deletions
Too many changes to show.
To preserve performance only 23 of 34 files are displayed.
README.md
| ... | ... | @@ -92,9 +92,6 @@ https://gitee.com/18010473990/wvp-GB28181.git |
| 92 | 92 | - [ ] 添加系统配置 |
| 93 | 93 | - [ ] 添加用户管理 |
| 94 | 94 | |
| 95 | -# 待实现: | |
| 96 | -web界面系统设置 | |
| 97 | - | |
| 98 | 95 | |
| 99 | 96 | # 项目部署 |
| 100 | 97 | 参考:[WIKI](https://github.com/648540858/wvp-GB28181-pro/wiki) |
| ... | ... | @@ -104,7 +101,7 @@ https://gitee.com/18010473990/wvp-GB28181.git |
| 104 | 101 | |
| 105 | 102 | # 使用帮助 |
| 106 | 103 | QQ群: 901799015, 542509000(ZLM大群) |
| 107 | -QQ私信一般不回, 精力有限.欢迎大家在群里讨论. | |
| 104 | +QQ私信一般不回, 精力有限.欢迎大家在群里讨论.觉得项目对你有帮助,欢迎star和提交pr。 | |
| 108 | 105 | |
| 109 | 106 | # 致谢 |
| 110 | 107 | 感谢作者[夏楚](https://github.com/xia-chu) 提供这么棒的开源流媒体服务框架 | ... | ... |
pom.xml
| ... | ... | @@ -184,6 +184,12 @@ |
| 184 | 184 | <version>2.3.0</version> |
| 185 | 185 | </dependency> |
| 186 | 186 | |
| 187 | + <!--反向代理--> | |
| 188 | + <dependency> | |
| 189 | + <groupId>org.mitre.dsmiley.httpproxy</groupId> | |
| 190 | + <artifactId>smiley-http-proxy-servlet</artifactId> | |
| 191 | + <version>1.12</version> | |
| 192 | + </dependency> | |
| 187 | 193 | |
| 188 | 194 | <!-- onvif协议栈 --> |
| 189 | 195 | <dependency> | ... | ... |
src/main/java/com/genersoft/iot/vmp/conf/MediaConfig.java
| ... | ... | @@ -51,6 +51,9 @@ public class MediaConfig { |
| 51 | 51 | @Value("${media.rtp.portRange}") |
| 52 | 52 | private String rtpPortRange; |
| 53 | 53 | |
| 54 | + @Value("${media.recordAssistPort}") | |
| 55 | + private int recordAssistPort; | |
| 56 | + | |
| 54 | 57 | public String getIp() { |
| 55 | 58 | return ip; |
| 56 | 59 | } |
| ... | ... | @@ -174,4 +177,12 @@ public class MediaConfig { |
| 174 | 177 | public void setRtspSSLPort(String rtspSSLPort) { |
| 175 | 178 | this.rtspSSLPort = rtspSSLPort; |
| 176 | 179 | } |
| 180 | + | |
| 181 | + public int getRecordAssistPort() { | |
| 182 | + return recordAssistPort; | |
| 183 | + } | |
| 184 | + | |
| 185 | + public void setRecordAssistPort(int recordAssistPort) { | |
| 186 | + this.recordAssistPort = recordAssistPort; | |
| 187 | + } | |
| 177 | 188 | } | ... | ... |
src/main/java/com/genersoft/iot/vmp/conf/ProxyServletConfig.java
0 → 100644
| 1 | +package com.genersoft.iot.vmp.conf; | |
| 2 | + | |
| 3 | +import org.apache.http.HttpRequest; | |
| 4 | +import org.apache.http.HttpResponse; | |
| 5 | +import org.mitre.dsmiley.httpproxy.ProxyServlet; | |
| 6 | +import org.slf4j.Logger; | |
| 7 | +import org.slf4j.LoggerFactory; | |
| 8 | +import org.springframework.beans.factory.annotation.Autowired; | |
| 9 | +import org.springframework.boot.web.servlet.ServletRegistrationBean; | |
| 10 | +import org.springframework.context.annotation.Bean; | |
| 11 | +import org.springframework.context.annotation.Configuration; | |
| 12 | +import org.springframework.util.StringUtils; | |
| 13 | + | |
| 14 | +import javax.servlet.ServletException; | |
| 15 | +import java.io.IOException; | |
| 16 | +import java.net.ConnectException; | |
| 17 | +import java.util.Locale; | |
| 18 | + | |
| 19 | + | |
| 20 | +@Configuration | |
| 21 | +public class ProxyServletConfig { | |
| 22 | + | |
| 23 | + private final static Logger logger = LoggerFactory.getLogger(ProxyServletConfig.class); | |
| 24 | + | |
| 25 | + @Autowired | |
| 26 | + private MediaConfig mediaConfig; | |
| 27 | + | |
| 28 | + @Bean | |
| 29 | + public ServletRegistrationBean zlmServletRegistrationBean(){ | |
| 30 | + String ip = StringUtils.isEmpty(mediaConfig.getWanIp())? mediaConfig.getIp(): mediaConfig.getWanIp(); | |
| 31 | + ServletRegistrationBean servletRegistrationBean = new ServletRegistrationBean(new ZLMProxySerlet(),"/zlm/*"); | |
| 32 | + servletRegistrationBean.setName("zlm_Proxy"); | |
| 33 | + servletRegistrationBean.addInitParameter("targetUri", String.format("http://%s:%s", ip, mediaConfig.getHttpPort())); | |
| 34 | + if (logger.isDebugEnabled()) { | |
| 35 | + servletRegistrationBean.addInitParameter("log", "true"); | |
| 36 | + } | |
| 37 | + return servletRegistrationBean; | |
| 38 | + } | |
| 39 | + | |
| 40 | + class ZLMProxySerlet extends ProxyServlet{ | |
| 41 | + @Override | |
| 42 | + protected void handleRequestException(HttpRequest proxyRequest, HttpResponse proxyResonse, Exception e){ | |
| 43 | + System.out.println(e.getMessage()); | |
| 44 | + try { | |
| 45 | + super.handleRequestException(proxyRequest, proxyResonse, e); | |
| 46 | + } catch (ServletException servletException) { | |
| 47 | + logger.error("zlm 代理失败: ", e); | |
| 48 | + } catch (IOException ioException) { | |
| 49 | + if (ioException instanceof ConnectException) { | |
| 50 | + logger.error("zlm 连接失败"); | |
| 51 | + }else { | |
| 52 | + logger.error("zlm 代理失败: ", e); | |
| 53 | + } | |
| 54 | + } catch (RuntimeException exception){ | |
| 55 | + logger.error("zlm 代理失败: ", e); | |
| 56 | + } | |
| 57 | + } | |
| 58 | + } | |
| 59 | + | |
| 60 | +} | ... | ... |
src/main/java/com/genersoft/iot/vmp/conf/RedisConfig.java
src/main/java/com/genersoft/iot/vmp/conf/UserSetup.java
| ... | ... | @@ -8,11 +8,53 @@ public class UserSetup { |
| 8 | 8 | @Value("${userSettings.savePositionHistory}") |
| 9 | 9 | boolean savePositionHistory; |
| 10 | 10 | |
| 11 | + @Value("${userSettings.autoApplyPlay}") | |
| 12 | + private boolean autoApplyPlay; | |
| 13 | + | |
| 14 | + @Value("${userSettings.seniorSdp}") | |
| 15 | + private boolean seniorSdp; | |
| 16 | + | |
| 17 | + @Value("${userSettings.playTimeout}") | |
| 18 | + private long playTimeout; | |
| 19 | + | |
| 20 | + @Value("${userSettings.waitTrack}") | |
| 21 | + private boolean waitTrack; | |
| 22 | + | |
| 23 | + @Value("${userSettings.interfaceAuthentication}") | |
| 24 | + private boolean interfaceAuthentication; | |
| 25 | + | |
| 26 | + @Value("${userSettings.recordPushLive}") | |
| 27 | + private boolean recordPushLive; | |
| 28 | + | |
| 11 | 29 | public boolean getSavePositionHistory() { |
| 12 | 30 | return savePositionHistory; |
| 13 | 31 | } |
| 14 | 32 | |
| 15 | - public void setSavePositionHistory(boolean savePositionHistory) { | |
| 16 | - this.savePositionHistory = savePositionHistory; | |
| 33 | + public boolean isSavePositionHistory() { | |
| 34 | + return savePositionHistory; | |
| 35 | + } | |
| 36 | + | |
| 37 | + public boolean isAutoApplyPlay() { | |
| 38 | + return autoApplyPlay; | |
| 39 | + } | |
| 40 | + | |
| 41 | + public boolean isSeniorSdp() { | |
| 42 | + return seniorSdp; | |
| 43 | + } | |
| 44 | + | |
| 45 | + public long getPlayTimeout() { | |
| 46 | + return playTimeout; | |
| 47 | + } | |
| 48 | + | |
| 49 | + public boolean isWaitTrack() { | |
| 50 | + return waitTrack; | |
| 51 | + } | |
| 52 | + | |
| 53 | + public boolean isInterfaceAuthentication() { | |
| 54 | + return interfaceAuthentication; | |
| 55 | + } | |
| 56 | + | |
| 57 | + public boolean isRecordPushLive() { | |
| 58 | + return recordPushLive; | |
| 17 | 59 | } |
| 18 | 60 | } | ... | ... |
src/main/java/com/genersoft/iot/vmp/conf/security/WebSecurityConfig.java
| 1 | 1 | package com.genersoft.iot.vmp.conf.security; |
| 2 | 2 | |
| 3 | +import com.genersoft.iot.vmp.conf.UserSetup; | |
| 3 | 4 | import org.springframework.beans.factory.annotation.Autowired; |
| 4 | 5 | import org.springframework.beans.factory.annotation.Value; |
| 5 | 6 | import org.springframework.context.annotation.Bean; |
| ... | ... | @@ -22,8 +23,8 @@ import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; |
| 22 | 23 | @EnableGlobalMethodSecurity(prePostEnabled = true) |
| 23 | 24 | public class WebSecurityConfig extends WebSecurityConfigurerAdapter { |
| 24 | 25 | |
| 25 | - @Value("${userSettings.interfaceAuthentication}") | |
| 26 | - private boolean interfaceAuthentication; | |
| 26 | + @Autowired | |
| 27 | + private UserSetup userSetup; | |
| 27 | 28 | |
| 28 | 29 | @Autowired |
| 29 | 30 | private DefaultUserDetailsServiceImpl userDetailsService; |
| ... | ... | @@ -71,7 +72,7 @@ public class WebSecurityConfig extends WebSecurityConfigurerAdapter { |
| 71 | 72 | @Override |
| 72 | 73 | public void configure(WebSecurity web) { |
| 73 | 74 | |
| 74 | - if (!interfaceAuthentication) { | |
| 75 | + if (!userSetup.isInterfaceAuthentication()) { | |
| 75 | 76 | web.ignoring().antMatchers("**"); |
| 76 | 77 | }else { |
| 77 | 78 | // 可以直接访问的静态数据 | ... | ... |
src/main/java/com/genersoft/iot/vmp/gb28181/event/offline/KeepaliveTimeoutListenerForPlatform.java
src/main/java/com/genersoft/iot/vmp/gb28181/transmit/cmd/impl/SIPCommander.java
| ... | ... | @@ -12,6 +12,7 @@ import com.alibaba.fastjson.JSONArray; |
| 12 | 12 | import com.alibaba.fastjson.JSONObject; |
| 13 | 13 | import com.genersoft.iot.vmp.common.StreamInfo; |
| 14 | 14 | import com.genersoft.iot.vmp.conf.MediaConfig; |
| 15 | +import com.genersoft.iot.vmp.conf.UserSetup; | |
| 15 | 16 | import com.genersoft.iot.vmp.media.zlm.ZLMServerConfig; |
| 16 | 17 | import com.genersoft.iot.vmp.gb28181.event.SipSubscribe; |
| 17 | 18 | import com.genersoft.iot.vmp.media.zlm.ZLMHttpHookSubscribe; |
| ... | ... | @@ -24,7 +25,6 @@ import org.slf4j.Logger; |
| 24 | 25 | import org.slf4j.LoggerFactory; |
| 25 | 26 | import org.springframework.beans.factory.annotation.Autowired; |
| 26 | 27 | import org.springframework.beans.factory.annotation.Qualifier; |
| 27 | -import org.springframework.beans.factory.annotation.Value; | |
| 28 | 28 | import org.springframework.context.annotation.DependsOn; |
| 29 | 29 | import org.springframework.context.annotation.Lazy; |
| 30 | 30 | import org.springframework.stereotype.Component; |
| ... | ... | @@ -83,14 +83,8 @@ public class SIPCommander implements ISIPCommander { |
| 83 | 83 | @Autowired |
| 84 | 84 | private MediaConfig mediaConfig; |
| 85 | 85 | |
| 86 | - @Value("${userSettings.seniorSdp}") | |
| 87 | - private boolean seniorSdp; | |
| 88 | - | |
| 89 | - @Value("${userSettings.autoApplyPlay}") | |
| 90 | - private boolean autoApplyPlay; | |
| 91 | - | |
| 92 | - @Value("${userSettings.waitTrack}") | |
| 93 | - private boolean waitTrack; | |
| 86 | + @Autowired | |
| 87 | + private UserSetup userSetup; | |
| 94 | 88 | |
| 95 | 89 | @Autowired |
| 96 | 90 | private ZLMHttpHookSubscribe subscribe; |
| ... | ... | @@ -377,7 +371,7 @@ public class SIPCommander implements ISIPCommander { |
| 377 | 371 | subscribeKey.put("regist", true); |
| 378 | 372 | |
| 379 | 373 | subscribe.addSubscribe(ZLMHttpHookSubscribe.HookType.on_stream_changed, subscribeKey, json->{ |
| 380 | - if (waitTrack && json.getJSONArray("tracks") == null) return; | |
| 374 | + if (userSetup.isWaitTrack() && json.getJSONArray("tracks") == null) return; | |
| 381 | 375 | event.response(json); |
| 382 | 376 | subscribe.removeSubscribe(ZLMHttpHookSubscribe.HookType.on_stream_changed, subscribeKey); |
| 383 | 377 | }); |
| ... | ... | @@ -390,7 +384,7 @@ public class SIPCommander implements ISIPCommander { |
| 390 | 384 | content.append("c=IN IP4 "+mediaInfo.getWanIp()+"\r\n"); |
| 391 | 385 | content.append("t=0 0\r\n"); |
| 392 | 386 | |
| 393 | - if (seniorSdp) { | |
| 387 | + if (userSetup.isSeniorSdp()) { | |
| 394 | 388 | if("TCP-PASSIVE".equals(streamMode)) { |
| 395 | 389 | content.append("m=video "+ mediaPort +" TCP/RTP/AVP 96 126 125 99 34 98 97\r\n"); |
| 396 | 390 | }else if ("TCP-ACTIVE".equals(streamMode)) { |
| ... | ... | @@ -478,7 +472,7 @@ public class SIPCommander implements ISIPCommander { |
| 478 | 472 | subscribeKey.put("regist", true); |
| 479 | 473 | logger.debug("录像回放添加订阅,订阅内容:" + subscribeKey.toString()); |
| 480 | 474 | subscribe.addSubscribe(ZLMHttpHookSubscribe.HookType.on_stream_changed, subscribeKey, json->{ |
| 481 | - if (waitTrack && json.getJSONArray("tracks") == null) return; | |
| 475 | + if (userSetup.isWaitTrack() && json.getJSONArray("tracks") == null) return; | |
| 482 | 476 | event.response(json); |
| 483 | 477 | subscribe.removeSubscribe(ZLMHttpHookSubscribe.HookType.on_stream_changed, subscribeKey); |
| 484 | 478 | }); |
| ... | ... | @@ -500,7 +494,7 @@ public class SIPCommander implements ISIPCommander { |
| 500 | 494 | } |
| 501 | 495 | String streamMode = device.getStreamMode().toUpperCase(); |
| 502 | 496 | |
| 503 | - if (seniorSdp) { | |
| 497 | + if (userSetup.isSeniorSdp()) { | |
| 504 | 498 | if("TCP-PASSIVE".equals(streamMode)) { |
| 505 | 499 | content.append("m=video "+ mediaPort +" TCP/RTP/AVP 96 126 125 99 34 98 97\r\n"); |
| 506 | 500 | }else if ("TCP-ACTIVE".equals(streamMode)) { | ... | ... |
src/main/java/com/genersoft/iot/vmp/media/zlm/ZLMHTTPProxyController.java
| 1 | -package com.genersoft.iot.vmp.media.zlm; | |
| 2 | - | |
| 3 | -import com.genersoft.iot.vmp.conf.MediaConfig; | |
| 4 | -import com.genersoft.iot.vmp.storager.IRedisCatchStorage; | |
| 5 | -import org.springframework.beans.factory.annotation.Autowired; | |
| 6 | -import org.springframework.beans.factory.annotation.Value; | |
| 7 | -import org.springframework.web.bind.annotation.*; | |
| 8 | -import org.springframework.web.client.HttpClientErrorException; | |
| 9 | -import org.springframework.web.client.RestTemplate; | |
| 10 | - | |
| 11 | -import javax.servlet.http.HttpServletRequest; | |
| 12 | -import javax.servlet.http.HttpServletResponse; | |
| 13 | - | |
| 14 | -@RestController | |
| 15 | -@RequestMapping("/zlm") | |
| 16 | -public class ZLMHTTPProxyController { | |
| 17 | - | |
| 18 | - | |
| 19 | - // private final static Logger logger = LoggerFactory.getLogger(ZLMHTTPProxyController.class); | |
| 20 | - | |
| 21 | - @Autowired | |
| 22 | - private IRedisCatchStorage redisCatchStorage; | |
| 23 | - | |
| 24 | - @Autowired | |
| 25 | - private MediaConfig mediaConfig; | |
| 26 | - | |
| 27 | - @ResponseBody | |
| 28 | - @RequestMapping(value = "/**/**/**", produces = "application/json;charset=UTF-8") | |
| 29 | - public Object proxy(HttpServletRequest request, HttpServletResponse response){ | |
| 30 | - | |
| 31 | - if (redisCatchStorage.getMediaInfo() == null) { | |
| 32 | - return "未接入流媒体"; | |
| 33 | - } | |
| 34 | - ZLMServerConfig mediaInfo = redisCatchStorage.getMediaInfo(); | |
| 35 | - String requestURI = String.format("http://%s:%s%s?%s&%s", | |
| 36 | - mediaInfo.getLocalIP(), | |
| 37 | - mediaConfig.getHttpPort(), | |
| 38 | - request.getRequestURI().replace("/zlm",""), | |
| 39 | - mediaInfo.getHookAdminParams(), | |
| 40 | - request.getQueryString() | |
| 41 | - ); | |
| 42 | - // 发送请求 | |
| 43 | - RestTemplate restTemplate = new RestTemplate(); | |
| 44 | - //将指定的url返回的参数自动封装到自定义好的对应类对象中 | |
| 45 | - Object result = null; | |
| 46 | - try { | |
| 47 | - result = restTemplate.getForObject(requestURI,Object.class); | |
| 48 | - | |
| 49 | - }catch (HttpClientErrorException httpClientErrorException) { | |
| 50 | - response.setStatus(httpClientErrorException.getStatusCode().value()); | |
| 51 | - } | |
| 52 | - return result; | |
| 53 | - } | |
| 54 | -} | |
| 1 | +//package com.genersoft.iot.vmp.media.zlm; | |
| 2 | +// | |
| 3 | +//import com.genersoft.iot.vmp.conf.MediaConfig; | |
| 4 | +//import com.genersoft.iot.vmp.storager.IRedisCatchStorage; | |
| 5 | +//import org.springframework.beans.factory.annotation.Autowired; | |
| 6 | +//import org.springframework.beans.factory.annotation.Value; | |
| 7 | +//import org.springframework.web.bind.annotation.*; | |
| 8 | +//import org.springframework.web.client.HttpClientErrorException; | |
| 9 | +//import org.springframework.web.client.RestTemplate; | |
| 10 | +// | |
| 11 | +//import javax.servlet.http.HttpServletRequest; | |
| 12 | +//import javax.servlet.http.HttpServletResponse; | |
| 13 | +// | |
| 14 | +//@RestController | |
| 15 | +//@RequestMapping("/zlm") | |
| 16 | +//public class ZLMHTTPProxyController { | |
| 17 | +// | |
| 18 | +// | |
| 19 | +// // private final static Logger logger = LoggerFactory.getLogger(ZLMHTTPProxyController.class); | |
| 20 | +// | |
| 21 | +// @Autowired | |
| 22 | +// private IRedisCatchStorage redisCatchStorage; | |
| 23 | +// | |
| 24 | +// @Autowired | |
| 25 | +// private MediaConfig mediaConfig; | |
| 26 | +// | |
| 27 | +// @ResponseBody | |
| 28 | +// @RequestMapping(value = "/**/**/**", produces = "application/json;charset=UTF-8") | |
| 29 | +// public Object proxy(HttpServletRequest request, HttpServletResponse response){ | |
| 30 | +// | |
| 31 | +// if (redisCatchStorage.getMediaInfo() == null) { | |
| 32 | +// return "未接入流媒体"; | |
| 33 | +// } | |
| 34 | +// ZLMServerConfig mediaInfo = redisCatchStorage.getMediaInfo(); | |
| 35 | +// String requestURI = String.format("http://%s:%s%s?%s&%s", | |
| 36 | +// mediaInfo.getLocalIP(), | |
| 37 | +// mediaConfig.getHttpPort(), | |
| 38 | +// request.getRequestURI().replace("/zlm",""), | |
| 39 | +// mediaInfo.getHookAdminParams(), | |
| 40 | +// request.getQueryString() | |
| 41 | +// ); | |
| 42 | +// // 发送请求 | |
| 43 | +// RestTemplate restTemplate = new RestTemplate(); | |
| 44 | +// //将指定的url返回的参数自动封装到自定义好的对应类对象中 | |
| 45 | +// Object result = null; | |
| 46 | +// try { | |
| 47 | +// result = restTemplate.getForObject(requestURI,Object.class); | |
| 48 | +// | |
| 49 | +// }catch (HttpClientErrorException httpClientErrorException) { | |
| 50 | +// response.setStatus(httpClientErrorException.getStatusCode().value()); | |
| 51 | +// } | |
| 52 | +// return result; | |
| 53 | +// } | |
| 54 | +//} | ... | ... |
src/main/java/com/genersoft/iot/vmp/media/zlm/ZLMHttpHookListener.java
| ... | ... | @@ -7,6 +7,7 @@ import com.alibaba.fastjson.JSON; |
| 7 | 7 | import com.alibaba.fastjson.JSONArray; |
| 8 | 8 | import com.genersoft.iot.vmp.common.StreamInfo; |
| 9 | 9 | import com.genersoft.iot.vmp.conf.MediaConfig; |
| 10 | +import com.genersoft.iot.vmp.conf.UserSetup; | |
| 10 | 11 | import com.genersoft.iot.vmp.gb28181.bean.Device; |
| 11 | 12 | import com.genersoft.iot.vmp.storager.IRedisCatchStorage; |
| 12 | 13 | import com.genersoft.iot.vmp.storager.IVideoManagerStorager; |
| ... | ... | @@ -62,8 +63,8 @@ public class ZLMHttpHookListener { |
| 62 | 63 | @Autowired |
| 63 | 64 | private ZLMHttpHookSubscribe subscribe; |
| 64 | 65 | |
| 65 | - @Value("${userSettings.autoApplyPlay}") | |
| 66 | - private boolean autoApplyPlay; | |
| 66 | + @Autowired | |
| 67 | + private UserSetup userSetup; | |
| 67 | 68 | |
| 68 | 69 | @Autowired |
| 69 | 70 | private MediaConfig mediaConfig; |
| ... | ... | @@ -132,10 +133,8 @@ public class ZLMHttpHookListener { |
| 132 | 133 | @ResponseBody |
| 133 | 134 | @PostMapping(value = "/on_publish", produces = "application/json;charset=UTF-8") |
| 134 | 135 | public ResponseEntity<String> onPublish(@RequestBody JSONObject json){ |
| 135 | - | |
| 136 | - if (logger.isDebugEnabled()) { | |
| 137 | - logger.debug("ZLM HOOK on_publish API调用,参数:" + json.toString()); | |
| 138 | - } | |
| 136 | + | |
| 137 | + logger.debug("ZLM HOOK on_publish API调用,参数:" + json.toString()); | |
| 139 | 138 | |
| 140 | 139 | ZLMHttpHookSubscribe.Event subscribe = this.subscribe.getSubscribe(ZLMHttpHookSubscribe.HookType.on_publish, json); |
| 141 | 140 | if (subscribe != null) subscribe.response(json); |
| ... | ... | @@ -144,7 +143,7 @@ public class ZLMHttpHookListener { |
| 144 | 143 | ret.put("code", 0); |
| 145 | 144 | ret.put("msg", "success"); |
| 146 | 145 | ret.put("enableHls", true); |
| 147 | - ret.put("enableMP4", false); | |
| 146 | + ret.put("enableMP4", userSetup.isRecordPushLive()); | |
| 148 | 147 | ret.put("enableRtxp", true); |
| 149 | 148 | return new ResponseEntity<String>(ret.toString(),HttpStatus.OK); |
| 150 | 149 | } |
| ... | ... | @@ -333,7 +332,7 @@ public class ZLMHttpHookListener { |
| 333 | 332 | if (logger.isDebugEnabled()) { |
| 334 | 333 | logger.debug("ZLM HOOK on_stream_not_found API调用,参数:" + json.toString()); |
| 335 | 334 | } |
| 336 | - if (autoApplyPlay) { | |
| 335 | + if (userSetup.isAutoApplyPlay()) { | |
| 337 | 336 | String app = json.getString("app"); |
| 338 | 337 | String streamId = json.getString("stream"); |
| 339 | 338 | StreamInfo streamInfo = redisCatchStorage.queryPlayByStreamId(streamId); | ... | ... |
src/main/java/com/genersoft/iot/vmp/media/zlm/ZLMRunner.java
| ... | ... | @@ -109,6 +109,10 @@ public class ZLMRunner implements CommandLineRunner { |
| 109 | 109 | if (StringUtils.isEmpty(mediaConfig.getHookIp())) mediaConfig.setHookIp(sipConfig.getSipIp()); |
| 110 | 110 | String protocol = sslEnabled ? "https" : "http"; |
| 111 | 111 | String hookPrex = String.format("%s://%s:%s/index/hook", protocol, mediaConfig.getHookIp(), serverPort); |
| 112 | + String recordHookPrex = null; | |
| 113 | + if (mediaConfig.getRecordAssistPort() != 0) { | |
| 114 | + recordHookPrex = String.format("http://127.0.0.1:%s/api/record", mediaConfig.getRecordAssistPort()); | |
| 115 | + } | |
| 112 | 116 | Map<String, Object> param = new HashMap<>(); |
| 113 | 117 | param.put("api.secret",mediaConfig.getSecret()); // -profile:v Baseline |
| 114 | 118 | param.put("ffmpeg.cmd","%s -fflags nobuffer -rtsp_transport tcp -i %s -c:a aac -strict -2 -ar 44100 -ab 48k -c:v libx264 -f flv %s"); |
| ... | ... | @@ -116,8 +120,8 @@ public class ZLMRunner implements CommandLineRunner { |
| 116 | 120 | param.put("hook.on_flow_report",""); |
| 117 | 121 | param.put("hook.on_play",String.format("%s/on_play", hookPrex)); |
| 118 | 122 | param.put("hook.on_http_access",""); |
| 119 | - param.put("hook.on_publish",String.format("%s/on_publish", hookPrex)); | |
| 120 | - param.put("hook.on_record_mp4",""); | |
| 123 | + param.put("hook.on_publish", String.format("%s/on_publish", hookPrex)); | |
| 124 | + param.put("hook.on_record_mp4",recordHookPrex != null? String.format("%s/on_record_mp4", recordHookPrex): ""); | |
| 121 | 125 | param.put("hook.on_record_ts",""); |
| 122 | 126 | param.put("hook.on_rtsp_auth",""); |
| 123 | 127 | param.put("hook.on_rtsp_realm",""); | ... | ... |
src/main/java/com/genersoft/iot/vmp/service/IRecordInfoServer.java
0 → 100644
src/main/java/com/genersoft/iot/vmp/service/impl/PlayServiceImpl.java
| ... | ... | @@ -4,6 +4,7 @@ import com.alibaba.fastjson.JSON; |
| 4 | 4 | import com.alibaba.fastjson.JSONArray; |
| 5 | 5 | import com.alibaba.fastjson.JSONObject; |
| 6 | 6 | import com.genersoft.iot.vmp.common.StreamInfo; |
| 7 | +import com.genersoft.iot.vmp.conf.UserSetup; | |
| 7 | 8 | import com.genersoft.iot.vmp.gb28181.bean.Device; |
| 8 | 9 | import com.genersoft.iot.vmp.gb28181.bean.DeviceChannel; |
| 9 | 10 | import com.genersoft.iot.vmp.gb28181.event.SipSubscribe; |
| ... | ... | @@ -64,8 +65,8 @@ public class PlayServiceImpl implements IPlayService { |
| 64 | 65 | @Autowired |
| 65 | 66 | private VideoStreamSessionManager streamSession; |
| 66 | 67 | |
| 67 | - @Value("${userSettings.playTimeout}") | |
| 68 | - private long playTimeout; | |
| 68 | + @Autowired | |
| 69 | + private UserSetup userSetup; | |
| 69 | 70 | |
| 70 | 71 | |
| 71 | 72 | @Override |
| ... | ... | @@ -76,7 +77,7 @@ public class PlayServiceImpl implements IPlayService { |
| 76 | 77 | playResult.setDevice(device); |
| 77 | 78 | UUID uuid = UUID.randomUUID(); |
| 78 | 79 | playResult.setUuid(uuid.toString()); |
| 79 | - DeferredResult<ResponseEntity<String>> result = new DeferredResult<ResponseEntity<String>>(playTimeout); | |
| 80 | + DeferredResult<ResponseEntity<String>> result = new DeferredResult<ResponseEntity<String>>(userSetup.getPlayTimeout()); | |
| 80 | 81 | playResult.setResult(result); |
| 81 | 82 | // 录像查询以channelId作为deviceId查询 |
| 82 | 83 | resultHolder.put(DeferredResultHolder.CALLBACK_CMD_PlAY + uuid, result); | ... | ... |
src/main/java/com/genersoft/iot/vmp/service/impl/RecordInfoServerImpl.java
0 → 100644
| 1 | +package com.genersoft.iot.vmp.service.impl; | |
| 2 | + | |
| 3 | +import com.genersoft.iot.vmp.media.zlm.dto.StreamProxyItem; | |
| 4 | +import com.genersoft.iot.vmp.service.IRecordInfoServer; | |
| 5 | +import com.genersoft.iot.vmp.storager.dao.RecordInfoDao; | |
| 6 | +import com.genersoft.iot.vmp.storager.dao.dto.RecordInfo; | |
| 7 | +import com.github.pagehelper.PageHelper; | |
| 8 | +import com.github.pagehelper.PageInfo; | |
| 9 | +import org.springframework.beans.factory.annotation.Autowired; | |
| 10 | +import org.springframework.stereotype.Service; | |
| 11 | + | |
| 12 | +import java.util.List; | |
| 13 | + | |
| 14 | +@Service | |
| 15 | +public class RecordInfoServerImpl implements IRecordInfoServer { | |
| 16 | + | |
| 17 | + @Autowired | |
| 18 | + private RecordInfoDao recordInfoDao; | |
| 19 | + | |
| 20 | + @Override | |
| 21 | + public PageInfo<RecordInfo> getRecordList(int page, int count) { | |
| 22 | + PageHelper.startPage(page, count); | |
| 23 | + List<RecordInfo> all = recordInfoDao.selectAll(); | |
| 24 | + return new PageInfo<>(all); | |
| 25 | + } | |
| 26 | +} | ... | ... |
src/main/java/com/genersoft/iot/vmp/storager/dao/RecordInfoDao.java
0 → 100644
| 1 | +package com.genersoft.iot.vmp.storager.dao; | |
| 2 | + | |
| 3 | +import com.genersoft.iot.vmp.storager.dao.dto.RecordInfo; | |
| 4 | +import com.genersoft.iot.vmp.storager.dao.dto.User; | |
| 5 | +import org.apache.ibatis.annotations.Delete; | |
| 6 | +import org.apache.ibatis.annotations.Insert; | |
| 7 | +import org.apache.ibatis.annotations.Mapper; | |
| 8 | +import org.apache.ibatis.annotations.Select; | |
| 9 | +import org.springframework.stereotype.Repository; | |
| 10 | + | |
| 11 | +import java.util.List; | |
| 12 | + | |
| 13 | +@Mapper | |
| 14 | +@Repository | |
| 15 | +public interface RecordInfoDao { | |
| 16 | + | |
| 17 | + @Insert("INSERT INTO recordInfo (app, stream, mediaServerId, createTime, type, deviceId, channelId, name) VALUES" + | |
| 18 | + "('${app}', '${stream}', '${mediaServerId}', datetime('now','localtime')), '${type}', '${deviceId}', '${channelId}', '${name}'") | |
| 19 | + int add(RecordInfo recordInfo); | |
| 20 | + | |
| 21 | + @Delete("DELETE FROM user WHERE createTime < '${beforeTime}'") | |
| 22 | + int deleteBefore(String beforeTime); | |
| 23 | + | |
| 24 | + @Select("select * FROM recordInfo") | |
| 25 | + List<RecordInfo> selectAll(); | |
| 26 | +} | ... | ... |
src/main/java/com/genersoft/iot/vmp/storager/dao/dto/RecordInfo.java
0 → 100644
| 1 | +package com.genersoft.iot.vmp.storager.dao.dto; | |
| 2 | + | |
| 3 | +/** | |
| 4 | + * 录像记录 | |
| 5 | + */ | |
| 6 | +public class RecordInfo { | |
| 7 | + | |
| 8 | + /** | |
| 9 | + * ID | |
| 10 | + */ | |
| 11 | + private int id; | |
| 12 | + | |
| 13 | + /** | |
| 14 | + * 应用名 | |
| 15 | + */ | |
| 16 | + private String app; | |
| 17 | + | |
| 18 | + /** | |
| 19 | + * 流ID | |
| 20 | + */ | |
| 21 | + private String stream; | |
| 22 | + | |
| 23 | + /** | |
| 24 | + * 对应的zlm流媒体的ID | |
| 25 | + */ | |
| 26 | + private String mediaServerId; | |
| 27 | + | |
| 28 | + /** | |
| 29 | + * 创建时间 | |
| 30 | + */ | |
| 31 | + private String createTime; | |
| 32 | + | |
| 33 | + /** | |
| 34 | + * 类型 对应zlm的 originType | |
| 35 | + * unknown = 0, | |
| 36 | + * rtmp_push=1, | |
| 37 | + * rtsp_push=2, | |
| 38 | + * rtp_push=3, | |
| 39 | + * pull=4, | |
| 40 | + * ffmpeg_pull=5, | |
| 41 | + * mp4_vod=6, | |
| 42 | + * device_chn=7, | |
| 43 | + * rtc_push=8 | |
| 44 | + */ | |
| 45 | + private int type; | |
| 46 | + | |
| 47 | + /** | |
| 48 | + * 国标录像时的设备ID | |
| 49 | + */ | |
| 50 | + private String deviceId; | |
| 51 | + | |
| 52 | + /** | |
| 53 | + * 国标录像时的通道ID | |
| 54 | + */ | |
| 55 | + private String channelId; | |
| 56 | + | |
| 57 | + /** | |
| 58 | + * 拉流代理录像时的名称 | |
| 59 | + */ | |
| 60 | + private String name; | |
| 61 | + | |
| 62 | + public int getId() { | |
| 63 | + return id; | |
| 64 | + } | |
| 65 | + | |
| 66 | + public void setId(int id) { | |
| 67 | + this.id = id; | |
| 68 | + } | |
| 69 | + | |
| 70 | + public String getApp() { | |
| 71 | + return app; | |
| 72 | + } | |
| 73 | + | |
| 74 | + public void setApp(String app) { | |
| 75 | + this.app = app; | |
| 76 | + } | |
| 77 | + | |
| 78 | + public String getStream() { | |
| 79 | + return stream; | |
| 80 | + } | |
| 81 | + | |
| 82 | + public void setStream(String stream) { | |
| 83 | + this.stream = stream; | |
| 84 | + } | |
| 85 | + | |
| 86 | + public String getMediaServerId() { | |
| 87 | + return mediaServerId; | |
| 88 | + } | |
| 89 | + | |
| 90 | + public void setMediaServerId(String mediaServerId) { | |
| 91 | + this.mediaServerId = mediaServerId; | |
| 92 | + } | |
| 93 | + | |
| 94 | + public String getCreateTime() { | |
| 95 | + return createTime; | |
| 96 | + } | |
| 97 | + | |
| 98 | + public void setCreateTime(String createTime) { | |
| 99 | + this.createTime = createTime; | |
| 100 | + } | |
| 101 | + | |
| 102 | + public int getType() { | |
| 103 | + return type; | |
| 104 | + } | |
| 105 | + | |
| 106 | + public void setType(int type) { | |
| 107 | + this.type = type; | |
| 108 | + } | |
| 109 | + | |
| 110 | + public String getDeviceId() { | |
| 111 | + return deviceId; | |
| 112 | + } | |
| 113 | + | |
| 114 | + public void setDeviceId(String deviceId) { | |
| 115 | + this.deviceId = deviceId; | |
| 116 | + } | |
| 117 | + | |
| 118 | + public String getChannelId() { | |
| 119 | + return channelId; | |
| 120 | + } | |
| 121 | + | |
| 122 | + public void setChannelId(String channelId) { | |
| 123 | + this.channelId = channelId; | |
| 124 | + } | |
| 125 | + | |
| 126 | + public String getName() { | |
| 127 | + return name; | |
| 128 | + } | |
| 129 | + | |
| 130 | + public void setName(String name) { | |
| 131 | + this.name = name; | |
| 132 | + } | |
| 133 | +} | ... | ... |
src/main/java/com/genersoft/iot/vmp/vmanager/gb28181/record/RecordController.java renamed to src/main/java/com/genersoft/iot/vmp/vmanager/gb28181/record/GBRecordController.java
| ... | ... | @@ -26,9 +26,9 @@ import com.genersoft.iot.vmp.storager.IVideoManagerStorager; |
| 26 | 26 | @CrossOrigin |
| 27 | 27 | @RestController |
| 28 | 28 | @RequestMapping("/api/gb_record") |
| 29 | -public class RecordController { | |
| 29 | +public class GBRecordController { | |
| 30 | 30 | |
| 31 | - private final static Logger logger = LoggerFactory.getLogger(RecordController.class); | |
| 31 | + private final static Logger logger = LoggerFactory.getLogger(GBRecordController.class); | |
| 32 | 32 | |
| 33 | 33 | @Autowired |
| 34 | 34 | private SIPCommander cmder; | ... | ... |
src/main/java/com/genersoft/iot/vmp/vmanager/record/RecoderProxyController.java
0 → 100644
| 1 | +package com.genersoft.iot.vmp.vmanager.record; | |
| 2 | + | |
| 3 | +import com.genersoft.iot.vmp.conf.MediaConfig; | |
| 4 | +import com.genersoft.iot.vmp.media.zlm.ZLMServerConfig; | |
| 5 | +import com.genersoft.iot.vmp.storager.IRedisCatchStorage; | |
| 6 | +import org.springframework.beans.factory.annotation.Autowired; | |
| 7 | +import org.springframework.http.HttpStatus; | |
| 8 | +import org.springframework.util.StringUtils; | |
| 9 | +import org.springframework.web.bind.annotation.RequestMapping; | |
| 10 | +import org.springframework.web.bind.annotation.ResponseBody; | |
| 11 | +import org.springframework.web.bind.annotation.RestController; | |
| 12 | +import org.springframework.web.client.HttpClientErrorException; | |
| 13 | +import org.springframework.web.client.RestTemplate; | |
| 14 | + | |
| 15 | +import javax.servlet.http.HttpServletRequest; | |
| 16 | +import javax.servlet.http.HttpServletResponse; | |
| 17 | +import java.net.URLDecoder; | |
| 18 | + | |
| 19 | +@RestController | |
| 20 | +@RequestMapping("/record_proxy") | |
| 21 | +public class RecoderProxyController { | |
| 22 | + | |
| 23 | + | |
| 24 | + // private final static Logger logger = LoggerFactory.getLogger(ZLMHTTPProxyController.class); | |
| 25 | + | |
| 26 | + @Autowired | |
| 27 | + private IRedisCatchStorage redisCatchStorage; | |
| 28 | + | |
| 29 | + @Autowired | |
| 30 | + private MediaConfig mediaConfig; | |
| 31 | + | |
| 32 | + @ResponseBody | |
| 33 | + @RequestMapping(value = "/**/**/**", produces = "application/json;charset=UTF-8") | |
| 34 | + public Object proxy(HttpServletRequest request, HttpServletResponse response){ | |
| 35 | + | |
| 36 | + | |
| 37 | + String baseRequestURI = request.getRequestURI(); | |
| 38 | + String[] split = baseRequestURI.split("/"); | |
| 39 | + if (split.length <= 2) { | |
| 40 | + response.setStatus(HttpStatus.NOT_FOUND.value()); | |
| 41 | + return null; | |
| 42 | + } | |
| 43 | + String mediaId = split[2]; | |
| 44 | + if (StringUtils.isEmpty(mediaId)){ | |
| 45 | + response.setStatus(HttpStatus.BAD_REQUEST.value()); | |
| 46 | + return null; | |
| 47 | + } | |
| 48 | + // 后续改为根据Id获取对应的ZLM | |
| 49 | + ZLMServerConfig mediaInfo = redisCatchStorage.getMediaInfo(); | |
| 50 | + String requestURI = String.format("http://%s:%s%s?%s", | |
| 51 | + mediaInfo.getLocalIP(), | |
| 52 | + mediaConfig.getRecordAssistPort(), | |
| 53 | + baseRequestURI.substring(baseRequestURI.indexOf(mediaId) + mediaId.length()), | |
| 54 | + URLDecoder.decode(request.getQueryString()) | |
| 55 | + ); | |
| 56 | + // 发送请求 | |
| 57 | + RestTemplate restTemplate = new RestTemplate(); | |
| 58 | + //将指定的url返回的参数自动封装到自定义好的对应类对象中 | |
| 59 | + Object result = null; | |
| 60 | + try { | |
| 61 | + result = restTemplate.getForObject(requestURI,Object.class); | |
| 62 | + | |
| 63 | + }catch (HttpClientErrorException httpClientErrorException) { | |
| 64 | + response.setStatus(httpClientErrorException.getStatusCode().value()); | |
| 65 | + } | |
| 66 | + return result; | |
| 67 | + } | |
| 68 | +} | ... | ... |
src/main/java/com/genersoft/iot/vmp/vmanager/record/RecordController.java
0 → 100644
| 1 | +//package com.genersoft.iot.vmp.vmanager.record; | |
| 2 | +// | |
| 3 | +//import com.alibaba.fastjson.JSONObject; | |
| 4 | +//import com.genersoft.iot.vmp.media.zlm.dto.StreamPushItem; | |
| 5 | +//import com.genersoft.iot.vmp.service.IRecordInfoServer; | |
| 6 | +//import com.genersoft.iot.vmp.storager.dao.dto.RecordInfo; | |
| 7 | +//import com.genersoft.iot.vmp.vmanager.bean.WVPResult; | |
| 8 | +//import com.github.pagehelper.PageInfo; | |
| 9 | +//import io.swagger.annotations.Api; | |
| 10 | +//import io.swagger.annotations.ApiImplicitParam; | |
| 11 | +//import io.swagger.annotations.ApiImplicitParams; | |
| 12 | +//import io.swagger.annotations.ApiOperation; | |
| 13 | +//import org.springframework.beans.factory.annotation.Autowired; | |
| 14 | +//import org.springframework.web.bind.annotation.*; | |
| 15 | +// | |
| 16 | +//@Api(tags = "云端录像") | |
| 17 | +//@CrossOrigin | |
| 18 | +//@RestController | |
| 19 | +//@RequestMapping("/api/record") | |
| 20 | +//public class RecordController { | |
| 21 | +// | |
| 22 | +// @Autowired | |
| 23 | +// private IRecordInfoServer recordInfoServer; | |
| 24 | +// | |
| 25 | +// @ApiOperation("录像列表查询") | |
| 26 | +// @ApiImplicitParams({ | |
| 27 | +// @ApiImplicitParam(name="page", value = "当前页", required = true, dataTypeClass = Integer.class), | |
| 28 | +// @ApiImplicitParam(name="count", value = "每页查询数量", required = true, dataTypeClass = Integer.class), | |
| 29 | +// @ApiImplicitParam(name="query", value = "查询内容", dataTypeClass = String.class), | |
| 30 | +// }) | |
| 31 | +// @GetMapping(value = "/app/list") | |
| 32 | +// @ResponseBody | |
| 33 | +// public Object list(@RequestParam(required = false)Integer page, | |
| 34 | +// @RequestParam(required = false)Integer count ){ | |
| 35 | +// | |
| 36 | +// PageInfo<RecordInfo> recordList = recordInfoServer.getRecordList(page - 1, page - 1 + count); | |
| 37 | +// return recordList; | |
| 38 | +// } | |
| 39 | +// | |
| 40 | +// @ApiOperation("获取录像详情") | |
| 41 | +// @ApiImplicitParams({ | |
| 42 | +// @ApiImplicitParam(name="recordInfo", value = "录像记录", required = true, dataTypeClass = RecordInfo.class) | |
| 43 | +// }) | |
| 44 | +// @GetMapping(value = "/detail") | |
| 45 | +// @ResponseBody | |
| 46 | +// public JSONObject list(RecordInfo recordInfo, String time ){ | |
| 47 | +// | |
| 48 | +// | |
| 49 | +// return null; | |
| 50 | +// } | |
| 51 | +//} | ... | ... |
src/main/java/com/genersoft/iot/vmp/vmanager/server/ServerController.java
| 1 | 1 | package com.genersoft.iot.vmp.vmanager.server; |
| 2 | 2 | |
| 3 | 3 | import com.genersoft.iot.vmp.VManageBootstrap; |
| 4 | +import com.genersoft.iot.vmp.media.zlm.ZLMServerConfig; | |
| 5 | +import com.genersoft.iot.vmp.storager.IRedisCatchStorage; | |
| 6 | +import com.genersoft.iot.vmp.storager.impl.RedisCatchStorageImpl; | |
| 4 | 7 | import com.genersoft.iot.vmp.utils.SpringBeanFactory; |
| 5 | 8 | import gov.nist.javax.sip.SipStackImpl; |
| 6 | 9 | import io.swagger.annotations.Api; |
| ... | ... | @@ -12,6 +15,7 @@ import org.springframework.web.bind.annotation.*; |
| 12 | 15 | import javax.sip.ListeningPoint; |
| 13 | 16 | import javax.sip.ObjectInUseException; |
| 14 | 17 | import javax.sip.SipProvider; |
| 18 | +import java.util.ArrayList; | |
| 15 | 19 | import java.util.Iterator; |
| 16 | 20 | |
| 17 | 21 | @SuppressWarnings("rawtypes") |
| ... | ... | @@ -24,6 +28,20 @@ public class ServerController { |
| 24 | 28 | @Autowired |
| 25 | 29 | private ConfigurableApplicationContext context; |
| 26 | 30 | |
| 31 | + @Autowired | |
| 32 | + private IRedisCatchStorage redisCatchStorage; | |
| 33 | + | |
| 34 | + | |
| 35 | + @ApiOperation("流媒体服务列表") | |
| 36 | + @GetMapping(value = "/media_server/list") | |
| 37 | + @ResponseBody | |
| 38 | + public Object getMediaServerList(){ | |
| 39 | + // TODO 为后续多个zlm支持准备 | |
| 40 | + ZLMServerConfig mediaInfo = redisCatchStorage.getMediaInfo(); | |
| 41 | + ArrayList<ZLMServerConfig> result = new ArrayList<>(); | |
| 42 | + result.add(mediaInfo); | |
| 43 | + return result; | |
| 44 | + } | |
| 27 | 45 | |
| 28 | 46 | @ApiOperation("重启服务") |
| 29 | 47 | @GetMapping(value = "/restart") | ... | ... |
src/main/resources/application-dev.yml
| ... | ... | @@ -92,6 +92,8 @@ media: |
| 92 | 92 | enable: true |
| 93 | 93 | # [可选] 在此范围内选择端口用于媒体流传输, |
| 94 | 94 | portRange: 30000,30500 # 端口范围 |
| 95 | + # 录像辅助服务, 部署此服务可以实现zlm录像的管理与下载 | |
| 96 | + recordAssistPort: 18081 | |
| 95 | 97 | |
| 96 | 98 | # [可选] 日志配置, 一般不需要改 |
| 97 | 99 | logging: |
| ... | ... | @@ -118,6 +120,8 @@ userSettings: |
| 118 | 120 | waitTrack: false |
| 119 | 121 | # 是否开启接口鉴权 |
| 120 | 122 | interfaceAuthentication: true |
| 123 | + # 推流直播是否录制 | |
| 124 | + recordPushLive: true | |
| 121 | 125 | |
| 122 | 126 | # 在线文档: swagger-ui(生产环境建议关闭) |
| 123 | 127 | springfox: | ... | ... |
src/main/resources/wvp.sqlite
No preview for this file type