23个文件已修改
10个文件已添加
1 文件已重命名
| | |
| | | - [ ] 添加系统配置 |
| | | - [ ] 添加用户管理 |
| | | |
| | | # 待实现: |
| | | web界面系统设置 |
| | | |
| | | |
| | | # 项目部署 |
| | | 参考:[WIKI](https://github.com/648540858/wvp-GB28181-pro/wiki) |
| | |
| | | |
| | | # 使用帮助 |
| | | QQ群: 901799015, 542509000(ZLM大群) |
| | | QQ私信一般不回, 精力有限.欢迎大家在群里讨论. |
| | | QQ私信一般不回, 精力有限.欢迎大家在群里讨论.觉得项目对你有帮助,欢迎star和提交pr。 |
| | | |
| | | # 致谢 |
| | | 感谢作者[夏楚](https://github.com/xia-chu) 提供这么棒的开源流媒体服务框架 |
| | |
| | | <version>2.3.0</version> |
| | | </dependency> |
| | | |
| | | <!--反向代理--> |
| | | <dependency> |
| | | <groupId>org.mitre.dsmiley.httpproxy</groupId> |
| | | <artifactId>smiley-http-proxy-servlet</artifactId> |
| | | <version>1.12</version> |
| | | </dependency> |
| | | |
| | | <!-- onvif协议栈 --> |
| | | <dependency> |
| | |
| | | @Value("${media.rtp.portRange}") |
| | | private String rtpPortRange; |
| | | |
| | | @Value("${media.recordAssistPort}") |
| | | private int recordAssistPort; |
| | | |
| | | public String getIp() { |
| | | return ip; |
| | | } |
| | |
| | | public void setRtspSSLPort(String rtspSSLPort) { |
| | | this.rtspSSLPort = rtspSSLPort; |
| | | } |
| | | |
| | | public int getRecordAssistPort() { |
| | | return recordAssistPort; |
| | | } |
| | | |
| | | public void setRecordAssistPort(int recordAssistPort) { |
| | | this.recordAssistPort = recordAssistPort; |
| | | } |
| | | } |
New file |
| | |
| | | package com.genersoft.iot.vmp.conf; |
| | | |
| | | import org.apache.http.HttpRequest; |
| | | import org.apache.http.HttpResponse; |
| | | import org.mitre.dsmiley.httpproxy.ProxyServlet; |
| | | import org.slf4j.Logger; |
| | | import org.slf4j.LoggerFactory; |
| | | import org.springframework.beans.factory.annotation.Autowired; |
| | | import org.springframework.boot.web.servlet.ServletRegistrationBean; |
| | | import org.springframework.context.annotation.Bean; |
| | | import org.springframework.context.annotation.Configuration; |
| | | import org.springframework.util.StringUtils; |
| | | |
| | | import javax.servlet.ServletException; |
| | | import java.io.IOException; |
| | | import java.net.ConnectException; |
| | | import java.util.Locale; |
| | | |
| | | |
| | | @Configuration |
| | | public class ProxyServletConfig { |
| | | |
| | | private final static Logger logger = LoggerFactory.getLogger(ProxyServletConfig.class); |
| | | |
| | | @Autowired |
| | | private MediaConfig mediaConfig; |
| | | |
| | | @Bean |
| | | public ServletRegistrationBean zlmServletRegistrationBean(){ |
| | | String ip = StringUtils.isEmpty(mediaConfig.getWanIp())? mediaConfig.getIp(): mediaConfig.getWanIp(); |
| | | ServletRegistrationBean servletRegistrationBean = new ServletRegistrationBean(new ZLMProxySerlet(),"/zlm/*"); |
| | | servletRegistrationBean.setName("zlm_Proxy"); |
| | | servletRegistrationBean.addInitParameter("targetUri", String.format("http://%s:%s", ip, mediaConfig.getHttpPort())); |
| | | if (logger.isDebugEnabled()) { |
| | | servletRegistrationBean.addInitParameter("log", "true"); |
| | | } |
| | | return servletRegistrationBean; |
| | | } |
| | | |
| | | class ZLMProxySerlet extends ProxyServlet{ |
| | | @Override |
| | | protected void handleRequestException(HttpRequest proxyRequest, HttpResponse proxyResonse, Exception e){ |
| | | System.out.println(e.getMessage()); |
| | | try { |
| | | super.handleRequestException(proxyRequest, proxyResonse, e); |
| | | } catch (ServletException servletException) { |
| | | logger.error("zlm 代理失败: ", e); |
| | | } catch (IOException ioException) { |
| | | if (ioException instanceof ConnectException) { |
| | | logger.error("zlm 连接失败"); |
| | | }else { |
| | | logger.error("zlm 代理失败: ", e); |
| | | } |
| | | } catch (RuntimeException exception){ |
| | | logger.error("zlm 代理失败: ", e); |
| | | } |
| | | } |
| | | } |
| | | |
| | | } |
| | |
| | | * 通过反射技术调用消息订阅处理器的相关方法进行一些业务处理
|
| | | *
|
| | | * @param connectionFactory
|
| | | * @param listenerAdapter
|
| | | * @return
|
| | | */
|
| | | @Bean
|
| | |
| | | @Value("${userSettings.savePositionHistory}") |
| | | boolean savePositionHistory; |
| | | |
| | | @Value("${userSettings.autoApplyPlay}") |
| | | private boolean autoApplyPlay; |
| | | |
| | | @Value("${userSettings.seniorSdp}") |
| | | private boolean seniorSdp; |
| | | |
| | | @Value("${userSettings.playTimeout}") |
| | | private long playTimeout; |
| | | |
| | | @Value("${userSettings.waitTrack}") |
| | | private boolean waitTrack; |
| | | |
| | | @Value("${userSettings.interfaceAuthentication}") |
| | | private boolean interfaceAuthentication; |
| | | |
| | | @Value("${userSettings.recordPushLive}") |
| | | private boolean recordPushLive; |
| | | |
| | | public boolean getSavePositionHistory() { |
| | | return savePositionHistory; |
| | | } |
| | | |
| | | public void setSavePositionHistory(boolean savePositionHistory) { |
| | | this.savePositionHistory = savePositionHistory; |
| | | public boolean isSavePositionHistory() { |
| | | return savePositionHistory; |
| | | } |
| | | |
| | | public boolean isAutoApplyPlay() { |
| | | return autoApplyPlay; |
| | | } |
| | | |
| | | public boolean isSeniorSdp() { |
| | | return seniorSdp; |
| | | } |
| | | |
| | | public long getPlayTimeout() { |
| | | return playTimeout; |
| | | } |
| | | |
| | | public boolean isWaitTrack() { |
| | | return waitTrack; |
| | | } |
| | | |
| | | public boolean isInterfaceAuthentication() { |
| | | return interfaceAuthentication; |
| | | } |
| | | |
| | | public boolean isRecordPushLive() { |
| | | return recordPushLive; |
| | | } |
| | | } |
| | |
| | | package com.genersoft.iot.vmp.conf.security; |
| | | |
| | | import com.genersoft.iot.vmp.conf.UserSetup; |
| | | import org.springframework.beans.factory.annotation.Autowired; |
| | | import org.springframework.beans.factory.annotation.Value; |
| | | import org.springframework.context.annotation.Bean; |
| | |
| | | @EnableGlobalMethodSecurity(prePostEnabled = true) |
| | | public class WebSecurityConfig extends WebSecurityConfigurerAdapter { |
| | | |
| | | @Value("${userSettings.interfaceAuthentication}") |
| | | private boolean interfaceAuthentication; |
| | | @Autowired |
| | | private UserSetup userSetup; |
| | | |
| | | @Autowired |
| | | private DefaultUserDetailsServiceImpl userDetailsService; |
| | |
| | | @Override |
| | | public void configure(WebSecurity web) { |
| | | |
| | | if (!interfaceAuthentication) { |
| | | if (!userSetup.isInterfaceAuthentication()) { |
| | | web.ignoring().antMatchers("**"); |
| | | }else { |
| | | // 可以直接访问的静态数据 |
| | |
| | | /** |
| | | * 监听失效的key |
| | | * @param message |
| | | * @param bytes |
| | | * @param pattern |
| | | */ |
| | | @Override |
| | | public void onMessage(Message message, byte[] pattern) { |
| | |
| | | import com.alibaba.fastjson.JSONObject;
|
| | | import com.genersoft.iot.vmp.common.StreamInfo;
|
| | | import com.genersoft.iot.vmp.conf.MediaConfig;
|
| | | import com.genersoft.iot.vmp.conf.UserSetup;
|
| | | import com.genersoft.iot.vmp.media.zlm.ZLMServerConfig;
|
| | | import com.genersoft.iot.vmp.gb28181.event.SipSubscribe;
|
| | | import com.genersoft.iot.vmp.media.zlm.ZLMHttpHookSubscribe;
|
| | |
| | | import org.slf4j.LoggerFactory;
|
| | | 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.DependsOn;
|
| | | import org.springframework.context.annotation.Lazy;
|
| | | import org.springframework.stereotype.Component;
|
| | |
| | | @Autowired
|
| | | private MediaConfig mediaConfig;
|
| | |
|
| | | @Value("${userSettings.seniorSdp}")
|
| | | private boolean seniorSdp;
|
| | |
|
| | | @Value("${userSettings.autoApplyPlay}")
|
| | | private boolean autoApplyPlay;
|
| | |
|
| | | @Value("${userSettings.waitTrack}")
|
| | | private boolean waitTrack;
|
| | | @Autowired
|
| | | private UserSetup userSetup;
|
| | |
|
| | | @Autowired
|
| | | private ZLMHttpHookSubscribe subscribe;
|
| | |
| | | subscribeKey.put("regist", true);
|
| | |
|
| | | subscribe.addSubscribe(ZLMHttpHookSubscribe.HookType.on_stream_changed, subscribeKey, json->{
|
| | | if (waitTrack && json.getJSONArray("tracks") == null) return;
|
| | | if (userSetup.isWaitTrack() && json.getJSONArray("tracks") == null) return;
|
| | | event.response(json);
|
| | | subscribe.removeSubscribe(ZLMHttpHookSubscribe.HookType.on_stream_changed, subscribeKey);
|
| | | });
|
| | |
| | | content.append("c=IN IP4 "+mediaInfo.getWanIp()+"\r\n");
|
| | | content.append("t=0 0\r\n");
|
| | |
|
| | | if (seniorSdp) {
|
| | | if (userSetup.isSeniorSdp()) {
|
| | | if("TCP-PASSIVE".equals(streamMode)) {
|
| | | content.append("m=video "+ mediaPort +" TCP/RTP/AVP 96 126 125 99 34 98 97\r\n");
|
| | | }else if ("TCP-ACTIVE".equals(streamMode)) {
|
| | |
| | | subscribeKey.put("regist", true);
|
| | | logger.debug("录像回放添加订阅,订阅内容:" + subscribeKey.toString());
|
| | | subscribe.addSubscribe(ZLMHttpHookSubscribe.HookType.on_stream_changed, subscribeKey, json->{
|
| | | if (waitTrack && json.getJSONArray("tracks") == null) return;
|
| | | if (userSetup.isWaitTrack() && json.getJSONArray("tracks") == null) return;
|
| | | event.response(json);
|
| | | subscribe.removeSubscribe(ZLMHttpHookSubscribe.HookType.on_stream_changed, subscribeKey);
|
| | | });
|
| | |
| | | }
|
| | | String streamMode = device.getStreamMode().toUpperCase();
|
| | |
|
| | | if (seniorSdp) {
|
| | | if (userSetup.isSeniorSdp()) {
|
| | | if("TCP-PASSIVE".equals(streamMode)) {
|
| | | content.append("m=video "+ mediaPort +" TCP/RTP/AVP 96 126 125 99 34 98 97\r\n");
|
| | | }else if ("TCP-ACTIVE".equals(streamMode)) {
|
| | |
| | | package com.genersoft.iot.vmp.media.zlm; |
| | | |
| | | import com.genersoft.iot.vmp.conf.MediaConfig; |
| | | import com.genersoft.iot.vmp.storager.IRedisCatchStorage; |
| | | import org.springframework.beans.factory.annotation.Autowired; |
| | | import org.springframework.beans.factory.annotation.Value; |
| | | import org.springframework.web.bind.annotation.*; |
| | | import org.springframework.web.client.HttpClientErrorException; |
| | | import org.springframework.web.client.RestTemplate; |
| | | |
| | | import javax.servlet.http.HttpServletRequest; |
| | | import javax.servlet.http.HttpServletResponse; |
| | | |
| | | @RestController |
| | | @RequestMapping("/zlm") |
| | | public class ZLMHTTPProxyController { |
| | | |
| | | |
| | | // private final static Logger logger = LoggerFactory.getLogger(ZLMHTTPProxyController.class); |
| | | |
| | | @Autowired |
| | | private IRedisCatchStorage redisCatchStorage; |
| | | |
| | | @Autowired |
| | | private MediaConfig mediaConfig; |
| | | |
| | | @ResponseBody |
| | | @RequestMapping(value = "/**/**/**", produces = "application/json;charset=UTF-8") |
| | | public Object proxy(HttpServletRequest request, HttpServletResponse response){ |
| | | |
| | | if (redisCatchStorage.getMediaInfo() == null) { |
| | | return "未接入流媒体"; |
| | | } |
| | | ZLMServerConfig mediaInfo = redisCatchStorage.getMediaInfo(); |
| | | String requestURI = String.format("http://%s:%s%s?%s&%s", |
| | | mediaInfo.getLocalIP(), |
| | | mediaConfig.getHttpPort(), |
| | | request.getRequestURI().replace("/zlm",""), |
| | | mediaInfo.getHookAdminParams(), |
| | | request.getQueryString() |
| | | ); |
| | | // 发送请求 |
| | | RestTemplate restTemplate = new RestTemplate(); |
| | | //将指定的url返回的参数自动封装到自定义好的对应类对象中 |
| | | Object result = null; |
| | | try { |
| | | result = restTemplate.getForObject(requestURI,Object.class); |
| | | |
| | | }catch (HttpClientErrorException httpClientErrorException) { |
| | | response.setStatus(httpClientErrorException.getStatusCode().value()); |
| | | } |
| | | return result; |
| | | } |
| | | } |
| | | //package com.genersoft.iot.vmp.media.zlm; |
| | | // |
| | | //import com.genersoft.iot.vmp.conf.MediaConfig; |
| | | //import com.genersoft.iot.vmp.storager.IRedisCatchStorage; |
| | | //import org.springframework.beans.factory.annotation.Autowired; |
| | | //import org.springframework.beans.factory.annotation.Value; |
| | | //import org.springframework.web.bind.annotation.*; |
| | | //import org.springframework.web.client.HttpClientErrorException; |
| | | //import org.springframework.web.client.RestTemplate; |
| | | // |
| | | //import javax.servlet.http.HttpServletRequest; |
| | | //import javax.servlet.http.HttpServletResponse; |
| | | // |
| | | //@RestController |
| | | //@RequestMapping("/zlm") |
| | | //public class ZLMHTTPProxyController { |
| | | // |
| | | // |
| | | // // private final static Logger logger = LoggerFactory.getLogger(ZLMHTTPProxyController.class); |
| | | // |
| | | // @Autowired |
| | | // private IRedisCatchStorage redisCatchStorage; |
| | | // |
| | | // @Autowired |
| | | // private MediaConfig mediaConfig; |
| | | // |
| | | // @ResponseBody |
| | | // @RequestMapping(value = "/**/**/**", produces = "application/json;charset=UTF-8") |
| | | // public Object proxy(HttpServletRequest request, HttpServletResponse response){ |
| | | // |
| | | // if (redisCatchStorage.getMediaInfo() == null) { |
| | | // return "未接入流媒体"; |
| | | // } |
| | | // ZLMServerConfig mediaInfo = redisCatchStorage.getMediaInfo(); |
| | | // String requestURI = String.format("http://%s:%s%s?%s&%s", |
| | | // mediaInfo.getLocalIP(), |
| | | // mediaConfig.getHttpPort(), |
| | | // request.getRequestURI().replace("/zlm",""), |
| | | // mediaInfo.getHookAdminParams(), |
| | | // request.getQueryString() |
| | | // ); |
| | | // // 发送请求 |
| | | // RestTemplate restTemplate = new RestTemplate(); |
| | | // //将指定的url返回的参数自动封装到自定义好的对应类对象中 |
| | | // Object result = null; |
| | | // try { |
| | | // result = restTemplate.getForObject(requestURI,Object.class); |
| | | // |
| | | // }catch (HttpClientErrorException httpClientErrorException) { |
| | | // response.setStatus(httpClientErrorException.getStatusCode().value()); |
| | | // } |
| | | // return result; |
| | | // } |
| | | //} |
| | |
| | | import com.alibaba.fastjson.JSONArray;
|
| | | import com.genersoft.iot.vmp.common.StreamInfo;
|
| | | import com.genersoft.iot.vmp.conf.MediaConfig;
|
| | | import com.genersoft.iot.vmp.conf.UserSetup;
|
| | | import com.genersoft.iot.vmp.gb28181.bean.Device;
|
| | | import com.genersoft.iot.vmp.storager.IRedisCatchStorage;
|
| | | import com.genersoft.iot.vmp.storager.IVideoManagerStorager;
|
| | |
| | | @Autowired
|
| | | private ZLMHttpHookSubscribe subscribe;
|
| | |
|
| | | @Value("${userSettings.autoApplyPlay}")
|
| | | private boolean autoApplyPlay;
|
| | | @Autowired
|
| | | private UserSetup userSetup;
|
| | |
|
| | | @Autowired
|
| | | private MediaConfig mediaConfig;
|
| | |
| | | @ResponseBody
|
| | | @PostMapping(value = "/on_publish", produces = "application/json;charset=UTF-8")
|
| | | public ResponseEntity<String> onPublish(@RequestBody JSONObject json){
|
| | | |
| | | if (logger.isDebugEnabled()) {
|
| | | logger.debug("ZLM HOOK on_publish API调用,参数:" + json.toString());
|
| | | }
|
| | |
|
| | | logger.debug("ZLM HOOK on_publish API调用,参数:" + json.toString());
|
| | |
|
| | | ZLMHttpHookSubscribe.Event subscribe = this.subscribe.getSubscribe(ZLMHttpHookSubscribe.HookType.on_publish, json);
|
| | | if (subscribe != null) subscribe.response(json);
|
| | |
| | | ret.put("code", 0);
|
| | | ret.put("msg", "success");
|
| | | ret.put("enableHls", true);
|
| | | ret.put("enableMP4", false);
|
| | | ret.put("enableMP4", userSetup.isRecordPushLive());
|
| | | ret.put("enableRtxp", true);
|
| | | return new ResponseEntity<String>(ret.toString(),HttpStatus.OK);
|
| | | }
|
| | |
| | | if (logger.isDebugEnabled()) {
|
| | | logger.debug("ZLM HOOK on_stream_not_found API调用,参数:" + json.toString());
|
| | | }
|
| | | if (autoApplyPlay) {
|
| | | if (userSetup.isAutoApplyPlay()) {
|
| | | String app = json.getString("app");
|
| | | String streamId = json.getString("stream");
|
| | | StreamInfo streamInfo = redisCatchStorage.queryPlayByStreamId(streamId);
|
| | |
| | | if (StringUtils.isEmpty(mediaConfig.getHookIp())) mediaConfig.setHookIp(sipConfig.getSipIp()); |
| | | String protocol = sslEnabled ? "https" : "http"; |
| | | String hookPrex = String.format("%s://%s:%s/index/hook", protocol, mediaConfig.getHookIp(), serverPort); |
| | | String recordHookPrex = null; |
| | | if (mediaConfig.getRecordAssistPort() != 0) { |
| | | recordHookPrex = String.format("http://127.0.0.1:%s/api/record", mediaConfig.getRecordAssistPort()); |
| | | } |
| | | Map<String, Object> param = new HashMap<>(); |
| | | param.put("api.secret",mediaConfig.getSecret()); // -profile:v Baseline |
| | | 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"); |
| | |
| | | param.put("hook.on_flow_report",""); |
| | | param.put("hook.on_play",String.format("%s/on_play", hookPrex)); |
| | | param.put("hook.on_http_access",""); |
| | | param.put("hook.on_publish",String.format("%s/on_publish", hookPrex)); |
| | | param.put("hook.on_record_mp4",""); |
| | | param.put("hook.on_publish", String.format("%s/on_publish", hookPrex)); |
| | | param.put("hook.on_record_mp4",recordHookPrex != null? String.format("%s/on_record_mp4", recordHookPrex): ""); |
| | | param.put("hook.on_record_ts",""); |
| | | param.put("hook.on_rtsp_auth",""); |
| | | param.put("hook.on_rtsp_realm",""); |
New file |
| | |
| | | package com.genersoft.iot.vmp.service; |
| | | |
| | | import com.genersoft.iot.vmp.storager.dao.dto.RecordInfo; |
| | | import com.github.pagehelper.PageInfo; |
| | | |
| | | public interface IRecordInfoServer { |
| | | PageInfo<RecordInfo> getRecordList(int page, int count); |
| | | } |
| | |
| | | import com.alibaba.fastjson.JSONArray; |
| | | import com.alibaba.fastjson.JSONObject; |
| | | import com.genersoft.iot.vmp.common.StreamInfo; |
| | | import com.genersoft.iot.vmp.conf.UserSetup; |
| | | import com.genersoft.iot.vmp.gb28181.bean.Device; |
| | | import com.genersoft.iot.vmp.gb28181.bean.DeviceChannel; |
| | | import com.genersoft.iot.vmp.gb28181.event.SipSubscribe; |
| | |
| | | @Autowired |
| | | private VideoStreamSessionManager streamSession; |
| | | |
| | | @Value("${userSettings.playTimeout}") |
| | | private long playTimeout; |
| | | @Autowired |
| | | private UserSetup userSetup; |
| | | |
| | | |
| | | @Override |
| | |
| | | playResult.setDevice(device); |
| | | UUID uuid = UUID.randomUUID(); |
| | | playResult.setUuid(uuid.toString()); |
| | | DeferredResult<ResponseEntity<String>> result = new DeferredResult<ResponseEntity<String>>(playTimeout); |
| | | DeferredResult<ResponseEntity<String>> result = new DeferredResult<ResponseEntity<String>>(userSetup.getPlayTimeout()); |
| | | playResult.setResult(result); |
| | | // 录像查询以channelId作为deviceId查询 |
| | | resultHolder.put(DeferredResultHolder.CALLBACK_CMD_PlAY + uuid, result); |
New file |
| | |
| | | package com.genersoft.iot.vmp.service.impl; |
| | | |
| | | import com.genersoft.iot.vmp.media.zlm.dto.StreamProxyItem; |
| | | import com.genersoft.iot.vmp.service.IRecordInfoServer; |
| | | import com.genersoft.iot.vmp.storager.dao.RecordInfoDao; |
| | | import com.genersoft.iot.vmp.storager.dao.dto.RecordInfo; |
| | | import com.github.pagehelper.PageHelper; |
| | | import com.github.pagehelper.PageInfo; |
| | | import org.springframework.beans.factory.annotation.Autowired; |
| | | import org.springframework.stereotype.Service; |
| | | |
| | | import java.util.List; |
| | | |
| | | @Service |
| | | public class RecordInfoServerImpl implements IRecordInfoServer { |
| | | |
| | | @Autowired |
| | | private RecordInfoDao recordInfoDao; |
| | | |
| | | @Override |
| | | public PageInfo<RecordInfo> getRecordList(int page, int count) { |
| | | PageHelper.startPage(page, count); |
| | | List<RecordInfo> all = recordInfoDao.selectAll(); |
| | | return new PageInfo<>(all); |
| | | } |
| | | } |
New file |
| | |
| | | package com.genersoft.iot.vmp.storager.dao; |
| | | |
| | | import com.genersoft.iot.vmp.storager.dao.dto.RecordInfo; |
| | | import com.genersoft.iot.vmp.storager.dao.dto.User; |
| | | import org.apache.ibatis.annotations.Delete; |
| | | import org.apache.ibatis.annotations.Insert; |
| | | import org.apache.ibatis.annotations.Mapper; |
| | | import org.apache.ibatis.annotations.Select; |
| | | import org.springframework.stereotype.Repository; |
| | | |
| | | import java.util.List; |
| | | |
| | | @Mapper |
| | | @Repository |
| | | public interface RecordInfoDao { |
| | | |
| | | @Insert("INSERT INTO recordInfo (app, stream, mediaServerId, createTime, type, deviceId, channelId, name) VALUES" + |
| | | "('${app}', '${stream}', '${mediaServerId}', datetime('now','localtime')), '${type}', '${deviceId}', '${channelId}', '${name}'") |
| | | int add(RecordInfo recordInfo); |
| | | |
| | | @Delete("DELETE FROM user WHERE createTime < '${beforeTime}'") |
| | | int deleteBefore(String beforeTime); |
| | | |
| | | @Select("select * FROM recordInfo") |
| | | List<RecordInfo> selectAll(); |
| | | } |
New file |
| | |
| | | package com.genersoft.iot.vmp.storager.dao.dto; |
| | | |
| | | /** |
| | | * 录像记录 |
| | | */ |
| | | public class RecordInfo { |
| | | |
| | | /** |
| | | * ID |
| | | */ |
| | | private int id; |
| | | |
| | | /** |
| | | * 应用名 |
| | | */ |
| | | private String app; |
| | | |
| | | /** |
| | | * 流ID |
| | | */ |
| | | private String stream; |
| | | |
| | | /** |
| | | * 对应的zlm流媒体的ID |
| | | */ |
| | | private String mediaServerId; |
| | | |
| | | /** |
| | | * 创建时间 |
| | | */ |
| | | private String createTime; |
| | | |
| | | /** |
| | | * 类型 对应zlm的 originType |
| | | * unknown = 0, |
| | | * rtmp_push=1, |
| | | * rtsp_push=2, |
| | | * rtp_push=3, |
| | | * pull=4, |
| | | * ffmpeg_pull=5, |
| | | * mp4_vod=6, |
| | | * device_chn=7, |
| | | * rtc_push=8 |
| | | */ |
| | | private int type; |
| | | |
| | | /** |
| | | * 国标录像时的设备ID |
| | | */ |
| | | private String deviceId; |
| | | |
| | | /** |
| | | * 国标录像时的通道ID |
| | | */ |
| | | private String channelId; |
| | | |
| | | /** |
| | | * 拉流代理录像时的名称 |
| | | */ |
| | | private String name; |
| | | |
| | | public int getId() { |
| | | return id; |
| | | } |
| | | |
| | | public void setId(int id) { |
| | | this.id = id; |
| | | } |
| | | |
| | | public String getApp() { |
| | | return app; |
| | | } |
| | | |
| | | public void setApp(String app) { |
| | | this.app = app; |
| | | } |
| | | |
| | | public String getStream() { |
| | | return stream; |
| | | } |
| | | |
| | | public void setStream(String stream) { |
| | | this.stream = stream; |
| | | } |
| | | |
| | | public String getMediaServerId() { |
| | | return mediaServerId; |
| | | } |
| | | |
| | | public void setMediaServerId(String mediaServerId) { |
| | | this.mediaServerId = mediaServerId; |
| | | } |
| | | |
| | | public String getCreateTime() { |
| | | return createTime; |
| | | } |
| | | |
| | | public void setCreateTime(String createTime) { |
| | | this.createTime = createTime; |
| | | } |
| | | |
| | | public int getType() { |
| | | return type; |
| | | } |
| | | |
| | | public void setType(int type) { |
| | | this.type = type; |
| | | } |
| | | |
| | | public String getDeviceId() { |
| | | return deviceId; |
| | | } |
| | | |
| | | public void setDeviceId(String deviceId) { |
| | | this.deviceId = deviceId; |
| | | } |
| | | |
| | | public String getChannelId() { |
| | | return channelId; |
| | | } |
| | | |
| | | public void setChannelId(String channelId) { |
| | | this.channelId = channelId; |
| | | } |
| | | |
| | | public String getName() { |
| | | return name; |
| | | } |
| | | |
| | | public void setName(String name) { |
| | | this.name = name; |
| | | } |
| | | } |
File was renamed from src/main/java/com/genersoft/iot/vmp/vmanager/gb28181/record/RecordController.java |
| | |
| | | @CrossOrigin |
| | | @RestController |
| | | @RequestMapping("/api/gb_record") |
| | | public class RecordController { |
| | | public class GBRecordController { |
| | | |
| | | private final static Logger logger = LoggerFactory.getLogger(RecordController.class); |
| | | private final static Logger logger = LoggerFactory.getLogger(GBRecordController.class); |
| | | |
| | | @Autowired |
| | | private SIPCommander cmder; |
New file |
| | |
| | | package com.genersoft.iot.vmp.vmanager.record; |
| | | |
| | | import com.genersoft.iot.vmp.conf.MediaConfig; |
| | | import com.genersoft.iot.vmp.media.zlm.ZLMServerConfig; |
| | | import com.genersoft.iot.vmp.storager.IRedisCatchStorage; |
| | | import org.springframework.beans.factory.annotation.Autowired; |
| | | import org.springframework.http.HttpStatus; |
| | | import org.springframework.util.StringUtils; |
| | | import org.springframework.web.bind.annotation.RequestMapping; |
| | | import org.springframework.web.bind.annotation.ResponseBody; |
| | | import org.springframework.web.bind.annotation.RestController; |
| | | import org.springframework.web.client.HttpClientErrorException; |
| | | import org.springframework.web.client.RestTemplate; |
| | | |
| | | import javax.servlet.http.HttpServletRequest; |
| | | import javax.servlet.http.HttpServletResponse; |
| | | import java.net.URLDecoder; |
| | | |
| | | @RestController |
| | | @RequestMapping("/record_proxy") |
| | | public class RecoderProxyController { |
| | | |
| | | |
| | | // private final static Logger logger = LoggerFactory.getLogger(ZLMHTTPProxyController.class); |
| | | |
| | | @Autowired |
| | | private IRedisCatchStorage redisCatchStorage; |
| | | |
| | | @Autowired |
| | | private MediaConfig mediaConfig; |
| | | |
| | | @ResponseBody |
| | | @RequestMapping(value = "/**/**/**", produces = "application/json;charset=UTF-8") |
| | | public Object proxy(HttpServletRequest request, HttpServletResponse response){ |
| | | |
| | | |
| | | String baseRequestURI = request.getRequestURI(); |
| | | String[] split = baseRequestURI.split("/"); |
| | | if (split.length <= 2) { |
| | | response.setStatus(HttpStatus.NOT_FOUND.value()); |
| | | return null; |
| | | } |
| | | String mediaId = split[2]; |
| | | if (StringUtils.isEmpty(mediaId)){ |
| | | response.setStatus(HttpStatus.BAD_REQUEST.value()); |
| | | return null; |
| | | } |
| | | // 后续改为根据Id获取对应的ZLM |
| | | ZLMServerConfig mediaInfo = redisCatchStorage.getMediaInfo(); |
| | | String requestURI = String.format("http://%s:%s%s?%s", |
| | | mediaInfo.getLocalIP(), |
| | | mediaConfig.getRecordAssistPort(), |
| | | baseRequestURI.substring(baseRequestURI.indexOf(mediaId) + mediaId.length()), |
| | | URLDecoder.decode(request.getQueryString()) |
| | | ); |
| | | // 发送请求 |
| | | RestTemplate restTemplate = new RestTemplate(); |
| | | //将指定的url返回的参数自动封装到自定义好的对应类对象中 |
| | | Object result = null; |
| | | try { |
| | | result = restTemplate.getForObject(requestURI,Object.class); |
| | | |
| | | }catch (HttpClientErrorException httpClientErrorException) { |
| | | response.setStatus(httpClientErrorException.getStatusCode().value()); |
| | | } |
| | | return result; |
| | | } |
| | | } |
New file |
| | |
| | | //package com.genersoft.iot.vmp.vmanager.record; |
| | | // |
| | | //import com.alibaba.fastjson.JSONObject; |
| | | //import com.genersoft.iot.vmp.media.zlm.dto.StreamPushItem; |
| | | //import com.genersoft.iot.vmp.service.IRecordInfoServer; |
| | | //import com.genersoft.iot.vmp.storager.dao.dto.RecordInfo; |
| | | //import com.genersoft.iot.vmp.vmanager.bean.WVPResult; |
| | | //import com.github.pagehelper.PageInfo; |
| | | //import io.swagger.annotations.Api; |
| | | //import io.swagger.annotations.ApiImplicitParam; |
| | | //import io.swagger.annotations.ApiImplicitParams; |
| | | //import io.swagger.annotations.ApiOperation; |
| | | //import org.springframework.beans.factory.annotation.Autowired; |
| | | //import org.springframework.web.bind.annotation.*; |
| | | // |
| | | //@Api(tags = "云端录像") |
| | | //@CrossOrigin |
| | | //@RestController |
| | | //@RequestMapping("/api/record") |
| | | //public class RecordController { |
| | | // |
| | | // @Autowired |
| | | // private IRecordInfoServer recordInfoServer; |
| | | // |
| | | // @ApiOperation("录像列表查询") |
| | | // @ApiImplicitParams({ |
| | | // @ApiImplicitParam(name="page", value = "当前页", required = true, dataTypeClass = Integer.class), |
| | | // @ApiImplicitParam(name="count", value = "每页查询数量", required = true, dataTypeClass = Integer.class), |
| | | // @ApiImplicitParam(name="query", value = "查询内容", dataTypeClass = String.class), |
| | | // }) |
| | | // @GetMapping(value = "/app/list") |
| | | // @ResponseBody |
| | | // public Object list(@RequestParam(required = false)Integer page, |
| | | // @RequestParam(required = false)Integer count ){ |
| | | // |
| | | // PageInfo<RecordInfo> recordList = recordInfoServer.getRecordList(page - 1, page - 1 + count); |
| | | // return recordList; |
| | | // } |
| | | // |
| | | // @ApiOperation("获取录像详情") |
| | | // @ApiImplicitParams({ |
| | | // @ApiImplicitParam(name="recordInfo", value = "录像记录", required = true, dataTypeClass = RecordInfo.class) |
| | | // }) |
| | | // @GetMapping(value = "/detail") |
| | | // @ResponseBody |
| | | // public JSONObject list(RecordInfo recordInfo, String time ){ |
| | | // |
| | | // |
| | | // return null; |
| | | // } |
| | | //} |
| | |
| | | package com.genersoft.iot.vmp.vmanager.server; |
| | | |
| | | import com.genersoft.iot.vmp.VManageBootstrap; |
| | | import com.genersoft.iot.vmp.media.zlm.ZLMServerConfig; |
| | | import com.genersoft.iot.vmp.storager.IRedisCatchStorage; |
| | | import com.genersoft.iot.vmp.storager.impl.RedisCatchStorageImpl; |
| | | import com.genersoft.iot.vmp.utils.SpringBeanFactory; |
| | | import gov.nist.javax.sip.SipStackImpl; |
| | | import io.swagger.annotations.Api; |
| | |
| | | import javax.sip.ListeningPoint; |
| | | import javax.sip.ObjectInUseException; |
| | | import javax.sip.SipProvider; |
| | | import java.util.ArrayList; |
| | | import java.util.Iterator; |
| | | |
| | | @SuppressWarnings("rawtypes") |
| | |
| | | @Autowired |
| | | private ConfigurableApplicationContext context; |
| | | |
| | | @Autowired |
| | | private IRedisCatchStorage redisCatchStorage; |
| | | |
| | | |
| | | @ApiOperation("流媒体服务列表") |
| | | @GetMapping(value = "/media_server/list") |
| | | @ResponseBody |
| | | public Object getMediaServerList(){ |
| | | // TODO 为后续多个zlm支持准备 |
| | | ZLMServerConfig mediaInfo = redisCatchStorage.getMediaInfo(); |
| | | ArrayList<ZLMServerConfig> result = new ArrayList<>(); |
| | | result.add(mediaInfo); |
| | | return result; |
| | | } |
| | | |
| | | @ApiOperation("重启服务") |
| | | @GetMapping(value = "/restart") |
| | |
| | | enable: true |
| | | # [可选] 在此范围内选择端口用于媒体流传输, |
| | | portRange: 30000,30500 # 端口范围 |
| | | # 录像辅助服务, 部署此服务可以实现zlm录像的管理与下载 |
| | | recordAssistPort: 18081 |
| | | |
| | | # [可选] 日志配置, 一般不需要改 |
| | | logging: |
| | |
| | | waitTrack: false |
| | | # 是否开启接口鉴权 |
| | | interfaceAuthentication: true |
| | | # 推流直播是否录制 |
| | | recordPushLive: true |
| | | |
| | | # 在线文档: swagger-ui(生产环境建议关闭) |
| | | springfox: |
| | |
| | | "dependencies": { |
| | | "axios": "^0.19.2", |
| | | "core-js": "^2.6.5", |
| | | "echarts": "^4.7.0", |
| | | "element-ui": "2.10.1", |
| | | "echarts": "^4.9.0", |
| | | "element-ui": "^2.15.1", |
| | | "fingerprintjs2": "^2.1.2", |
| | | "moment": "^2.29.1", |
| | | "postcss-pxtorem": "^5.1.1", |
New file |
| | |
| | | <template> |
| | | <div id="app"> |
| | | <el-container> |
| | | <el-header> |
| | | <uiHeader></uiHeader> |
| | | </el-header> |
| | | <el-main> |
| | | <div style="background-color: #FFFFFF; margin-bottom: 1rem; position: relative; padding: 0.5rem; text-align: left;"> |
| | | <span style="font-size: 1rem; font-weight: bold;">云端录像</span> |
| | | <div style="position: absolute; right: 1rem; top: 0.3rem;"> |
| | | <el-button v-if="!recordDetail" icon="el-icon-refresh-right" circle size="mini" :loading="loading" @click="getRecordList()"></el-button> |
| | | <el-button v-if="recordDetail" icon="el-icon-arrow-left" circle size="mini" @click="backToList()"></el-button> |
| | | </div> |
| | | </div> |
| | | <div v-if="!recordDetail"> |
| | | <div style="background-color: #FFFFFF; margin-bottom: 1rem; position: relative; padding: 0.5rem; text-align: left;font-size: 14px;"> |
| | | |
| | | |
| | | 节点选择: <el-select size="mini" @change="chooseMediaChange" style="width: 16rem; margin-right: 1rem;" v-model="mediaServer" placeholder="请选择" default-first-option> |
| | | <el-option |
| | | v-for="item in mediaServerList" |
| | | :key="item.generalMediaServerId" |
| | | :label="item.generalMediaServerId + '( ' + item.wanIp + ' )'" |
| | | :value="item"> |
| | | </el-option> |
| | | </el-select> |
| | | </div> |
| | | <!--设备列表--> |
| | | <el-table :data="recordList" border style="width: 100%" :height="winHeight"> |
| | | <el-table-column prop="app" label="应用名" align="center"> |
| | | </el-table-column> |
| | | <el-table-column prop="stream" label="流ID" align="center"> |
| | | </el-table-column> |
| | | <el-table-column prop="time" label="时间" align="center"> |
| | | </el-table-column> |
| | | <el-table-column label="操作" width="360" align="center" fixed="right"> |
| | | <template slot-scope="scope"> |
| | | <el-button-group> |
| | | <el-button size="mini" icon="el-icon-video-camera-solid" type="primary" @click="showRecordDetail(scope.row)">查看</el-button> |
| | | <!-- <el-button size="mini" icon="el-icon-delete" type="danger" @click="deleteRecord(scope.row)">删除</el-button>--> |
| | | </el-button-group> |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | <el-pagination |
| | | style="float: right" |
| | | @size-change="handleSizeChange" |
| | | @current-change="currentChange" |
| | | :current-page="currentPage" |
| | | :page-size="count" |
| | | :page-sizes="[15, 25, 35, 50]" |
| | | layout="total, sizes, prev, pager, next" |
| | | :total="total"> |
| | | </el-pagination> |
| | | </div> |
| | | <cloud-record-detail ref="cloudRecordDetail" v-if="recordDetail" :recordFile="chooseRecord" :mediaServer="mediaServer" ></cloud-record-detail> |
| | | </el-main> |
| | | </el-container> |
| | | </div> |
| | | </template> |
| | | |
| | | <script> |
| | | import uiHeader from './UiHeader.vue' |
| | | import cloudRecordDetail from './CloudRecordDetail.vue' |
| | | export default { |
| | | name: 'app', |
| | | components: { |
| | | uiHeader, cloudRecordDetail |
| | | }, |
| | | data() { |
| | | return { |
| | | mediaServerList: [], // 滅体节点列表 |
| | | mediaServer: null, // 媒体服务 |
| | | recordList: [], // 设备列表 |
| | | chooseRecord: null, // 媒体服务 |
| | | |
| | | updateLooper: 0, //数据刷新轮训标志 |
| | | winHeight: window.innerHeight - 250, |
| | | currentPage:1, |
| | | count:15, |
| | | total:0, |
| | | loading: false, |
| | | recordDetail: false |
| | | |
| | | }; |
| | | }, |
| | | computed: { |
| | | |
| | | }, |
| | | mounted() { |
| | | this.initData(); |
| | | }, |
| | | destroyed() { |
| | | // this.$destroy('videojs'); |
| | | }, |
| | | methods: { |
| | | initData: function() { |
| | | // 获取媒体节点列表 |
| | | this.getMediaServerList(); |
| | | // this.getRecordList(); |
| | | }, |
| | | currentChange: function(val){ |
| | | this.currentPage = val; |
| | | this.getRecordList(); |
| | | }, |
| | | handleSizeChange: function(val){ |
| | | this.count = val; |
| | | this.getRecordList(); |
| | | }, |
| | | getMediaServerList: function (){ |
| | | let that = this; |
| | | this.$axios({ |
| | | method: 'get', |
| | | url:`/api/server/media_server/list`, |
| | | }).then(function (res) { |
| | | console.log(res) |
| | | that.mediaServerList = res.data; |
| | | if (that.mediaServerList.length > 0) { |
| | | that.mediaServer = that.mediaServerList[0] |
| | | that.getRecordList(); |
| | | } |
| | | |
| | | }).catch(function (error) { |
| | | console.log(error); |
| | | }); |
| | | }, |
| | | getRecordList: function (){ |
| | | let that = this; |
| | | this.$axios({ |
| | | method: 'get', |
| | | url:`/record_proxy/${that.mediaServer.generalMediaServerId}/api/record/list`, |
| | | params: { |
| | | page: that.currentPage, |
| | | count: that.count |
| | | } |
| | | }).then(function (res) { |
| | | console.log(res) |
| | | that.total = res.data.data.total; |
| | | that.recordList = res.data.data.list; |
| | | that.loading = false; |
| | | }).catch(function (error) { |
| | | console.log(error); |
| | | that.loading = false; |
| | | }); |
| | | }, |
| | | backToList(){ |
| | | this.recordDetail= false; |
| | | }, |
| | | chooseMediaChange(val){ |
| | | console.log(val) |
| | | this.mediaServer = val; |
| | | this.getRecordList(); |
| | | }, |
| | | showRecordDetail(row){ |
| | | this.recordDetail = true; |
| | | this.chooseRecord = row; |
| | | // 查询是否存在录像 |
| | | // this.$axios({ |
| | | // method: 'delete', |
| | | // url:`/record_proxy/api/record/delete`, |
| | | // params: { |
| | | // page: this.currentPage, |
| | | // count: this.count |
| | | // } |
| | | // }).then((res) => { |
| | | // console.log(res) |
| | | // this.total = res.data.data.total; |
| | | // this.recordList = res.data.data.list; |
| | | // }).catch(function (error) { |
| | | // console.log(error); |
| | | // }); |
| | | |
| | | }, |
| | | deleteRecord(){ |
| | | // TODO |
| | | let that = this; |
| | | this.$axios({ |
| | | method: 'delete', |
| | | url:`/record_proxy/api/record/delete`, |
| | | params: { |
| | | page: that.currentPage, |
| | | count: that.count |
| | | } |
| | | }).then(function (res) { |
| | | console.log(res) |
| | | that.total = res.data.data.total; |
| | | that.recordList = res.data.data.list; |
| | | }).catch(function (error) { |
| | | console.log(error); |
| | | }); |
| | | } |
| | | |
| | | |
| | | } |
| | | }; |
| | | </script> |
| | | |
| | | <style> |
| | | |
| | | </style> |
New file |
| | |
| | | <template> |
| | | <div id="recordDetail"> |
| | | <el-container> |
| | | <el-aside width="300px"> |
| | | <div class="record-list-box-box"> |
| | | <el-date-picker size="mini" v-model="chooseDate" :picker-options="pickerOptions" type="date" value-format="yyyy-MM-dd" placeholder="日期" @change="dateChange()"></el-date-picker> |
| | | <div class="record-list-box" :style="recordListStyle"> |
| | | <ul v-if="detailFiles.length >0" class="infinite-list record-list" v-infinite-scroll="infiniteScroll" > |
| | | <li v-for="item in detailFiles" class="infinite-list-item record-list-item" @click="chooseFile(item)"> |
| | | <el-tag v-if="choosedFile != item"> |
| | | <i class="el-icon-video-camera" ></i> |
| | | {{ item.substring(0,17)}} |
| | | </el-tag> |
| | | <el-tag type="danger" v-if="choosedFile == item"> |
| | | <i class="el-icon-video-camera" ></i> |
| | | {{ item.substring(0,17)}} |
| | | </el-tag> |
| | | </li> |
| | | </ul> |
| | | </div> |
| | | <div v-if="detailFiles.length ==0" class="record-list-no-val" >暂无数据</div> |
| | | </div> |
| | | |
| | | <div class="record-list-option"> |
| | | <el-button size="mini" type="primary" icon="fa fa-cloud-download" style="margin: auto; " title="裁剪合并" @click="drawerOpen"></el-button> |
| | | </div> |
| | | </el-aside> |
| | | <el-main style="padding: 22px"> |
| | | <div class="playBox" :style="playerStyle"> |
| | | <player ref="recordVideoPlayer" :videoUrl="videoUrl" fluent autoplay :height="true" ></player> |
| | | </div> |
| | | <div class="player-option-box" > |
| | | <el-slider |
| | | class="playtime-slider" |
| | | v-model="playTime" |
| | | id="playtimeSlider" |
| | | :disabled="detailFiles.length === 0" |
| | | :min="sliderMIn" |
| | | :max="sliderMax" |
| | | :format-tooltip="playTimeFormat" |
| | | @change="playTimeChange" |
| | | :marks="playTimeSliderMarks"> |
| | | </el-slider> |
| | | <div class="slider-val-box"> |
| | | <div class="slider-val" v-for="item of detailFiles" :style="'width:' + getDataWidth(item) + '%; left:' + getDataLeft(item) + '%'"></div> |
| | | </div> |
| | | </div> |
| | | |
| | | </el-main> |
| | | </el-container> |
| | | <el-drawer |
| | | title="录像下载" |
| | | :visible.sync="drawer" |
| | | :direction="direction" |
| | | :before-close="drawerClose"> |
| | | <div class="drawer-box"> |
| | | <el-button icon="el-icon-plus" size="mini" type="primary" @click="addTask"></el-button> |
| | | <el-tabs type="border-card" style="height: 100%" v-model="tabVal" @tab-click="tabClick"> |
| | | <el-tab-pane name="running"> |
| | | <span slot="label"><i class="el-icon-scissors"></i>进行中</span> |
| | | <ul class="task-list"> |
| | | <li class="task-list-item" v-for="item in taskListForRuning"> |
| | | <div class="task-list-item-box"> |
| | | <span>{{ item.startTime.substr(10) }}-{{item.endTime.substr(10)}}</span> |
| | | <el-progress :percentage="(parseFloat(item.percentage)*100).toFixed(1)"></el-progress> |
| | | </div> |
| | | </li> |
| | | |
| | | </ul> |
| | | </el-tab-pane> |
| | | <el-tab-pane name="ended"> |
| | | <span slot="label"><i class="el-icon-finished"></i>已完成</span> |
| | | <ul class="task-list"> |
| | | <li class="task-list-item" v-for="item in taskListEnded"> |
| | | <div class="task-list-item-box" style="height: 2rem;line-height: 2rem;"> |
| | | <span>{{ item.startTime.substr(10) }}-{{item.endTime.substr(10)}}</span> |
| | | <a class="el-icon-download download-btn" :href="basePath + '/' + item.recordFile" download > |
| | | </a> |
| | | </div> |
| | | </li> |
| | | |
| | | </ul> |
| | | </el-tab-pane> |
| | | </el-tabs> |
| | | </div> |
| | | </el-drawer> |
| | | <el-dialog title="选择时间段" :visible.sync="showTaskBox"> |
| | | <el-date-picker |
| | | type="datetimerange" |
| | | v-model="taskTimeRange" |
| | | range-separator="至" |
| | | start-placeholder="开始时间" |
| | | end-placeholder="结束时间" |
| | | format="HH:mm:ss" |
| | | placeholder="选择时间范围"> |
| | | </el-date-picker> |
| | | <el-button size="mini" type="primary" @click="addTaskToServer">确认</el-button> |
| | | </el-dialog> |
| | | </div> |
| | | </template> |
| | | |
| | | |
| | | <script> |
| | | // TODO 根据查询的时间列表设置滑轨的最大值与最小值, |
| | | import uiHeader from './UiHeader.vue' |
| | | import player from './dialog/easyPlayer.vue' |
| | | import moment from 'moment' |
| | | export default { |
| | | name: 'app', |
| | | components: { |
| | | uiHeader, player |
| | | }, |
| | | props: ['recordFile', 'mediaServer', 'dateFiles'], |
| | | data() { |
| | | return { |
| | | basePath: process.env.NODE_ENV === 'development'?`${location.origin}/debug/zlm`:`${location.origin}/zlm`, |
| | | dateFilesObj: [], |
| | | detailFiles: [], |
| | | chooseDate: null, |
| | | videoUrl: null, |
| | | choosedFile: null, |
| | | queryDate: new Date(), |
| | | currentPage: 1, |
| | | count: 1000000, // TODO 分页导致滑轨视频有效值无法获取完全 |
| | | total: 0, |
| | | direction: "ltr", |
| | | drawer: false, |
| | | showTaskBox: false, |
| | | taskTimeRange: [], |
| | | taskListEnded: [], |
| | | taskListForRuning: [], |
| | | sliderMIn: 0, |
| | | sliderMax: 86400, |
| | | autoPlay: true, |
| | | taskUpdate: null, |
| | | tabVal: "running", |
| | | recordListStyle: { |
| | | height: this.winHeight + "px", |
| | | overflow: "auto", |
| | | margin: "10px auto 10px auto" |
| | | }, |
| | | playerStyle: { |
| | | "margin": "auto", |
| | | "margin-bottom": "20px", |
| | | "height": this.winHeight + "px", |
| | | }, |
| | | winHeight: window.innerHeight - 240, |
| | | playTime: 0, |
| | | playTimeSliderMarks: { |
| | | 0: "00:00", |
| | | 3600: "01:00", |
| | | 7200: "02:00", |
| | | 10800: "03:00", |
| | | 14400: "04:00", |
| | | 18000: "05:00", |
| | | 21600: "06:00", |
| | | 25200: "07:00", |
| | | 28800: "08:00", |
| | | 32400: "09:00", |
| | | 36000: "10:00", |
| | | 39600: "11:00", |
| | | 43200: "12:00", |
| | | 46800: "13:00", |
| | | 50400: "14:00", |
| | | 54000: "15:00", |
| | | 57600: "16:00", |
| | | 61200: "17:00", |
| | | 64800: "18:00", |
| | | 68400: "19:00", |
| | | 72000: "20:00", |
| | | 75600: "21:00", |
| | | 79200: "22:00", |
| | | 82800: "23:00", |
| | | 86400: "24:00", |
| | | }, |
| | | pickerOptions:{ |
| | | cellClassName:(date) =>{ |
| | | // 通过显示一个点标识这一天有录像 |
| | | let time = moment(date).format('YYYY-MM-DD') |
| | | if (this.dateFilesObj[time]){ |
| | | return "data-picker-true" |
| | | }else { |
| | | return "data-picker-false" |
| | | } |
| | | } |
| | | } |
| | | }; |
| | | }, |
| | | computed: { |
| | | |
| | | }, |
| | | mounted() { |
| | | this.recordListStyle.height = this.winHeight + "px"; |
| | | this.playerStyle["height"] = this.winHeight + "px"; |
| | | // 查询当年有视频的日期 |
| | | this.getDateInYear(()=>{ |
| | | if (Object.values(this.dateFilesObj).length > 0){ |
| | | this.chooseDate = Object.values(this.dateFilesObj)[Object.values(this.dateFilesObj).length -1]; |
| | | this.dateChange(); |
| | | } |
| | | }) |
| | | }, |
| | | destroyed() { |
| | | this.$destroy('recordVideoPlayer'); |
| | | }, |
| | | methods: { |
| | | dateChange(){ |
| | | this.playTime = 0; |
| | | this.detailFiles = []; |
| | | this.currentPage = 1; |
| | | this.sliderMIn= 0; |
| | | this.sliderMax= 86400; |
| | | let chooseFullDate = new Date(this.chooseDate + " " + "00:00:00"); |
| | | if (chooseFullDate.getFullYear() !== this.queryDate.getFullYear() |
| | | || chooseFullDate.getMonth() !== this.queryDate.getMonth()){ |
| | | // this.getDateInYear() |
| | | } |
| | | this.queryRecordDetails(()=>{ |
| | | if (this.detailFiles.length > 0){ |
| | | let timeForFile = this.getTimeForFile(this.detailFiles[0]); |
| | | let lastTimeForFile = this.getTimeForFile(this.detailFiles[this.detailFiles.length - 1]); |
| | | let timeNum = timeForFile[0].getTime() - new Date(this.chooseDate + " " + "00:00:00").getTime() |
| | | let lastTimeNum = lastTimeForFile[1].getTime() - new Date(this.chooseDate + " " + "00:00:00").getTime() |
| | | |
| | | this.playTime = parseInt(timeNum/1000) |
| | | this.sliderMIn = parseInt(timeNum/1000 - timeNum/1000%(60*60)) |
| | | this.sliderMax = parseInt(lastTimeNum/1000 - lastTimeNum/1000%(60*60)) + 60*60 |
| | | } |
| | | }); |
| | | }, |
| | | infiniteScroll(){ |
| | | if (this.total > this.detailFiles.length) { |
| | | this.currentPage ++; |
| | | this.queryRecordDetails(); |
| | | } |
| | | }, |
| | | queryRecordDetails: function (callback){ |
| | | let that = this; |
| | | that.$axios({ |
| | | method: 'get', |
| | | url:`/record_proxy/${that.mediaServer.generalMediaServerId}/api/record/file/list`, |
| | | params: { |
| | | app: that.recordFile.app, |
| | | stream: that.recordFile.stream, |
| | | startTime: that.chooseDate + " 00:00:00", |
| | | endTime: that.chooseDate + " 23:59:59", |
| | | page: that.currentPage, |
| | | count: that.count |
| | | } |
| | | }).then(function (res) { |
| | | that.total = res.data.data.total; |
| | | that.detailFiles = that.detailFiles.concat(res.data.data.list); |
| | | that.loading = false; |
| | | if (callback) callback(); |
| | | }).catch(function (error) { |
| | | console.log(error); |
| | | that.loading = false; |
| | | }); |
| | | }, |
| | | chooseFile(file){ |
| | | this.choosedFile = file; |
| | | if (file == null) { |
| | | this.videoUrl = ""; |
| | | }else { |
| | | // TODO 控制列表滚动条 |
| | | this.videoUrl = `${this.basePath}/${this.mediaServer.recordAppName}/${this.recordFile.app}/${this.recordFile.stream}/${this.chooseDate}/${this.choosedFile}` |
| | | console.log(this.videoUrl) |
| | | } |
| | | |
| | | }, |
| | | |
| | | getDataWidth(item){ |
| | | let timeForFile = this.getTimeForFile(item); |
| | | let result = (timeForFile[2])/((this.sliderMax - this.sliderMIn)*1000) |
| | | return result*100 |
| | | }, |
| | | getDataLeft(item){ |
| | | let timeForFile = this.getTimeForFile(item); |
| | | let differenceTime = timeForFile[0].getTime() - new Date(this.chooseDate + " 00:00:00").getTime() |
| | | return parseFloat((differenceTime - this.sliderMIn * 1000)/((this.sliderMax - this.sliderMIn)*1000))*100 ; |
| | | }, |
| | | playTimeChange(val){ |
| | | let minTime = this.getTimeForFile(this.detailFiles[0])[0] |
| | | let maxTime = this.getTimeForFile(this.detailFiles[this.detailFiles.length - 1])[1]; |
| | | this.chooseFile(null); |
| | | let timeMilli = new Date(this.chooseDate + " 00:00:00").getTime() + val*1000 |
| | | if (timeMilli >= minTime.getTime() && timeMilli <= maxTime.getTime()){ |
| | | for (let i = 0; i < this.detailFiles.length; i++) { |
| | | let timeForFile = this.getTimeForFile(this.detailFiles[i]); |
| | | if (timeMilli >= timeForFile[0].getTime() && timeMilli <= timeForFile[1].getTime()){ |
| | | // TODO 当前未按照实际时间偏移,仅仅是找到对应的文静播放 |
| | | this.chooseFile(this.detailFiles[i]) |
| | | return; |
| | | } |
| | | } |
| | | } |
| | | }, |
| | | getTimeForFile(file){ |
| | | let timeStr = file.substring(0,17); |
| | | let starTime = new Date(this.chooseDate + " " + timeStr.split("-")[0]); |
| | | let endTime = new Date(this.chooseDate + " " + timeStr.split("-")[1]); |
| | | return [starTime, endTime, endTime.getTime() - starTime.getTime()]; |
| | | }, |
| | | playTimeFormat(val){ |
| | | let h = parseInt(val/3600); |
| | | let m = parseInt((val - h*3600)/60); |
| | | let s = parseInt(val - h*3600 - m*60); |
| | | return h + ":" + m + ":" + s |
| | | }, |
| | | deleteRecord(){ |
| | | // TODO |
| | | let that = this; |
| | | this.$axios({ |
| | | method: 'delete', |
| | | url:`/record_proxy/${that.mediaServer.generalMediaServerId}/api/record/delete`, |
| | | params: { |
| | | page: that.currentPage, |
| | | count: that.count |
| | | } |
| | | }).then(function (res) { |
| | | if (res.data.code == 0) { |
| | | that.total = res.data.data.total; |
| | | that.recordList = res.data.data.list; |
| | | } |
| | | }).catch(function (error) { |
| | | console.log(error); |
| | | }); |
| | | }, |
| | | getDateInYear(callback){ |
| | | let that = this; |
| | | that.dateFilesObj = {}; |
| | | this.$axios({ |
| | | method: 'get', |
| | | url:`/record_proxy/${that.mediaServer.generalMediaServerId}/api/record/date/list`, |
| | | params: { |
| | | app: that.recordFile.app, |
| | | stream: that.recordFile.stream |
| | | } |
| | | }).then(function (res) { |
| | | if (res.data.code === 0) { |
| | | if (res.data.data.length > 0) { |
| | | for (let i = 0; i < res.data.data.length; i++) { |
| | | that.dateFilesObj[res.data.data[i]] = res.data.data[i] |
| | | } |
| | | |
| | | console.log(that.dateFilesObj) |
| | | } |
| | | } |
| | | if(callback)callback(); |
| | | }).catch(function (error) { |
| | | console.log(error); |
| | | }); |
| | | }, |
| | | tabClick(){ |
| | | this.getTaskList(this.tabVal === "ended") |
| | | }, |
| | | drawerClose(){ |
| | | this.drawer = false; |
| | | if (this.taskUpdate != null) { |
| | | window.clearInterval(this.taskUpdate) |
| | | } |
| | | }, |
| | | drawerOpen(){ |
| | | this.drawer = true; |
| | | if (this.taskUpdate != null) { |
| | | window.clearInterval(this.taskUpdate) |
| | | } |
| | | this.taskUpdate = setInterval(()=>{ |
| | | this.getTaskList(this.tabVal === "ended") |
| | | }, 1000) |
| | | }, |
| | | addTask(){ |
| | | this.showTaskBox = true; |
| | | let startTimeStr = this.chooseDate + " " + this.detailFiles[0].substring(0,8); |
| | | let endTimeStr = this.chooseDate + " " + this.detailFiles[this.detailFiles.length - 1].substring(9,17); |
| | | this.taskTimeRange[0] = new Date(startTimeStr) |
| | | this.taskTimeRange[1] = new Date(endTimeStr) |
| | | }, |
| | | addTaskToServer(){ |
| | | let that = this; |
| | | this.$axios({ |
| | | method: 'get', |
| | | url:`/record_proxy/${that.mediaServer.generalMediaServerId}/api/record/file/download/task/add`, |
| | | params: { |
| | | app: that.recordFile.app, |
| | | stream: that.recordFile.stream, |
| | | startTime: moment(this.taskTimeRange[0]).format('YYYY-MM-DD HH:mm:ss'), |
| | | endTime: moment(this.taskTimeRange[1]).format('YYYY-MM-DD HH:mm:ss'), |
| | | } |
| | | }).then(function (res) { |
| | | if (res.data.code === 0 && res.data.msg === "success") { |
| | | that.showTaskBox = false |
| | | that.getTaskList(false); |
| | | }else { |
| | | that.$message.error(res.data.msg); |
| | | } |
| | | }).catch(function (error) { |
| | | console.log(error); |
| | | }); |
| | | }, |
| | | handleTabClick() { |
| | | this.getTaskList(this.tabVal === "ended") |
| | | }, |
| | | getTaskList(isEnd){ |
| | | let that = this; |
| | | this.$axios({ |
| | | method: 'get', |
| | | url:`/record_proxy/${that.mediaServer.generalMediaServerId}/api/record/file/download/task/list`, |
| | | params: { |
| | | isEnd: isEnd, |
| | | } |
| | | }).then(function (res) { |
| | | if (res.data.code == 0) { |
| | | if (isEnd){ |
| | | that.taskListEnded = res.data.data; |
| | | }else { |
| | | that.taskListForRuning = res.data.data; |
| | | } |
| | | } |
| | | }).catch(function (error) { |
| | | console.log(error); |
| | | }); |
| | | } |
| | | } |
| | | }; |
| | | </script> |
| | | |
| | | <style> |
| | | .el-slider__runway { |
| | | background-color:rgba(206, 206, 206, 0.47) !important; |
| | | } |
| | | .el-slider__bar { |
| | | background-color: rgba(153, 153, 153, 0) !important; |
| | | } |
| | | .playtime-slider { |
| | | position: relative; |
| | | z-index: 100; |
| | | } |
| | | .data-picker-true{ |
| | | |
| | | } |
| | | .data-picker-true:after{ |
| | | content: ""; |
| | | position: absolute; |
| | | width: 4px; |
| | | height: 4px; |
| | | background-color: #606060; |
| | | border-radius: 4px; |
| | | left: 45%; |
| | | top: 74%; |
| | | |
| | | } |
| | | .data-picker-false{ |
| | | |
| | | } |
| | | .slider-val-box{ |
| | | height: 6px; |
| | | position: relative; |
| | | top: -22px; |
| | | } |
| | | .slider-val{ |
| | | height: 6px; |
| | | background-color: #007CFF; |
| | | position: absolute; |
| | | } |
| | | .record-list-box-box{ |
| | | width: 250px; |
| | | float: left; |
| | | } |
| | | .record-list-box{ |
| | | overflow: auto; |
| | | width: 220px; |
| | | list-style: none; |
| | | padding: 0; |
| | | margin: 0; |
| | | margin-top: 0px; |
| | | padding: 1rem 0; |
| | | background-color: #FFF; |
| | | margin-top: 10px; |
| | | } |
| | | .record-list{ |
| | | list-style: none; |
| | | padding: 0; |
| | | margin: 0; |
| | | background-color: #FFF; |
| | | |
| | | } |
| | | .record-list-no-val { |
| | | position: absolute; |
| | | color: #9f9f9f; |
| | | top: 50%; |
| | | left: 110px; |
| | | } |
| | | .record-list-item{ |
| | | padding: 0; |
| | | margin: 0; |
| | | margin: 0.5rem 0; |
| | | cursor: pointer; |
| | | } |
| | | .record-list-option { |
| | | width: 10px; |
| | | float: left; |
| | | margin-top: 39px; |
| | | |
| | | } |
| | | .drawer-box{ |
| | | height: 100%; |
| | | } |
| | | |
| | | .task-list{ |
| | | list-style: none; |
| | | padding: 0; |
| | | margin: 0; |
| | | background-color: #FFF; |
| | | } |
| | | |
| | | .task-list-item{ |
| | | padding: 0; |
| | | margin: 0; |
| | | margin: 1.5rem 0; |
| | | } |
| | | .task-list-item-box{ |
| | | text-align: left; |
| | | font-size: 13px; |
| | | color: #555; |
| | | } |
| | | .download-btn{ |
| | | display: inline-block; |
| | | line-height: 1; |
| | | white-space: nowrap; |
| | | cursor: pointer; |
| | | background: #FFF; |
| | | background-color: rgb(255, 255, 255); |
| | | border: 1px solid #DCDFE6; |
| | | border-top-color: rgb(220, 223, 230); |
| | | border-right-color: rgb(220, 223, 230); |
| | | border-bottom-color: rgb(220, 223, 230); |
| | | border-left-color: rgb(220, 223, 230); |
| | | border-top-color: rgb(220, 223, 230); |
| | | border-right-color: rgb(220, 223, 230); |
| | | border-bottom-color: rgb(220, 223, 230); |
| | | border-left-color: rgb(220, 223, 230); |
| | | -webkit-appearance: none; |
| | | text-align: center; |
| | | -webkit-box-sizing: border-box; |
| | | box-sizing: border-box; |
| | | outline: 0; |
| | | margin: 0; |
| | | -webkit-transition: .1s; |
| | | transition: .1s; |
| | | font-weight: 500; |
| | | padding: 7px 14px; |
| | | font-size: 0.875rem; |
| | | border-radius: 4px; |
| | | font-size: 0.75rem; |
| | | border-radius: 3px; |
| | | color: #FFF; |
| | | background-color: #409EFF; |
| | | border-color: #409EFF; |
| | | float: right; |
| | | } |
| | | .download-btn:hover{ |
| | | background: #66b1ff; |
| | | border-color: #66b1ff; |
| | | color: #FFF; |
| | | } |
| | | .time-box{ |
| | | } |
| | | </style> |
| | |
| | | currentPusher: {}, //当前操作设备对象 |
| | | updateLooper: 0, //数据刷新轮训标志 |
| | | currentDeviceChannelsLenth:0, |
| | | winHeight: window.innerHeight - 200, |
| | | winHeight: window.innerHeight - 250, |
| | | currentPage:1, |
| | | count:15, |
| | | total:0, |
| | |
| | | <el-menu-item index="/deviceList">设备列表</el-menu-item> |
| | | <el-menu-item index="/pushVideoList">推流列表</el-menu-item> |
| | | <el-menu-item index="/streamProxyList">拉流代理</el-menu-item> |
| | | <el-menu-item index="/cloudRecord">云端录像</el-menu-item> |
| | | <el-menu-item index="/parentPlatformList/15/1">国标级联</el-menu-item> |
| | | <el-menu-item @click="openDoc">在线文档</el-menu-item> |
| | | <!-- <el-submenu index="/setting">--> |
| | |
| | | <template> |
| | | <div id="devicePlayer" v-loading="isLoging"> |
| | | |
| | | <el-dialog title="视频播放" top="0" :close-on-click-modal="false" :visible.sync="showVideoDialog" :destroy-on-close="true" @close="close()"> |
| | | <el-dialog title="视频播放" top="0" :close-on-click-modal="false" :visible.sync="showVideoDialog" @close="close()"> |
| | | <!-- <LivePlayer v-if="showVideoDialog" ref="videoPlayer" :videoUrl="videoUrl" :error="videoError" :message="videoError" :hasaudio="hasaudio" fluent autoplay live></LivePlayer> --> |
| | | <player ref="videoPlayer" :visible.sync="showVideoDialog" :videoUrl="videoUrl" :error="videoError" :message="videoError" :hasaudio="hasaudio" fluent autoplay live></player> |
| | | <player ref="videoPlayer" :visible.sync="showVideoDialog" :videoUrl="videoUrl" :error="videoError" :message="videoError" :height="false" :hasaudio="hasaudio" fluent autoplay live ></player> |
| | | <div id="shared" style="text-align: right; margin-top: 1rem;"> |
| | | <el-tabs v-model="tabActiveName" @tab-click="tabHandleClick"> |
| | | <el-tab-pane label="实时视频" name="media"> |
| | |
| | | <template> |
| | | <div id="player"> |
| | | <div id="easyplayer"></div> |
| | | </div> |
| | | <div id="easyplayer"></div> |
| | | </template> |
| | | |
| | | <script> |
| | |
| | | easyPlayer: null |
| | | }; |
| | | }, |
| | | props: ['videoUrl', 'error', 'hasaudio'], |
| | | props: ['videoUrl', 'error', 'hasaudio', 'height'], |
| | | mounted () { |
| | | let paramUrl = decodeURIComponent(this.$route.params.url) |
| | | this.$nextTick(() =>{ |
| | |
| | | this.videoUrl = paramUrl; |
| | | } |
| | | console.log("初始化时的地址为: " + this.videoUrl) |
| | | this.easyPlayer = new WasmPlayer(null, 'easyplayer', this.eventcallbacK) |
| | | this.easyPlayer.play(this.videoUrl, 1) |
| | | this.play(this.videoUrl) |
| | | }) |
| | | }, |
| | | watch:{ |
| | | videoUrl(newData, oldData){ |
| | | this.easyPlayer.destroy() |
| | | this.easyPlayer = new WasmPlayer(null, 'easyplayer', this.eventcallbacK) |
| | | this.easyPlayer.play(newData, 1) |
| | | this.play(newData) |
| | | }, |
| | | immediate:true |
| | | }, |
| | | methods: { |
| | | play: function (url) { |
| | | this.easyPlayer = new WasmPlayer(null, 'easyplayer', this.eventcallbacK) |
| | | console.log(this.height) |
| | | if (this.easyPlayer != null) { |
| | | this.easyPlayer.destroy(); |
| | | } |
| | | if (typeof (this.height) == "undefined") { |
| | | this.height = false |
| | | } |
| | | this.easyPlayer = new WasmPlayer(null, 'easyplayer', this.eventcallbacK, {Height: this.height}) |
| | | this.easyPlayer.play(url, 1) |
| | | }, |
| | | pause: function () { |
| | | this.easyPlayer.destroy(); |
| | | this.easyPlayer.destroy(); |
| | | this.easyPlayer = null |
| | | }, |
| | | eventcallbacK: function(type, message) { |
| | | console.log("player 事件回调") |
| | | console.log(type) |
| | | console.log(message) |
| | | // console.log("player 事件回调") |
| | | // console.log(type) |
| | | // console.log(message) |
| | | } |
| | | }, |
| | | destroyed() { |
| | |
| | | <template> |
| | | <div id="test"> |
| | | <div class="timeQuery" id="timeQuery"> |
| | | <el-row > |
| | | <el-col :span="24"> |
| | | <div class="timeQuery-background" @mousemove="hoveEvent"></div> |
| | | <div class="timeQuery-pointer"> |
| | | <el-tooltip class="item" effect="dark" content="Top Center 提示文字" value="true" manual="true" hide-after="0" placement="top"> |
| | | <div class="timeQuery-pointer-content"></div> |
| | | </el-tooltip> |
| | | <div class="timeQuery-background" ></div> |
| | | <div class="timeQuery-pointer"> |
| | | <div class="timeQuery-pointer-content" id="timeQueryPointer"> |
| | | <div class="timeQuery-pointer-handle" @mousedown.left="mousedownHandler" ></div> |
| | | </div> |
| | | </div> |
| | | |
| | | <div class="timeQuery-data" > |
| | | <div class="timeQuery-data" > |
| | | |
| | | <div class="timeQuery-data-cell" v-for="item of recordData" :style="'width:' + getDataWidth(item) + '%; left:' + getDataLeft(item) + '%'" ></div> |
| | | <!-- <div class="timeQuery-data-cell" style="width: 30%; left: 20%" @click="timeChoose"></div>--> |
| | | <!-- <div class="timeQuery-data-cell" style="width: 60%; left: 20%" @click="timeChoose"></div>--> |
| | | <div class="timeQuery-data-cell" v-for="item of recordData" :style="'width:' + getDataWidth(item) + '%; left:' + getDataLeft(item) + '%'" ></div> |
| | | <!-- <div class="timeQuery-data-cell" style="width: 30%; left: 20%" @click="timeChoose"></div>--> |
| | | <!-- <div class="timeQuery-data-cell" style="width: 60%; left: 20%" @click="timeChoose"></div>--> |
| | | </div> |
| | | |
| | | <div class="timeQuery-label" > |
| | | <div class="timeQuery-label-cell" style="left: 0%"> |
| | | <div class="timeQuery-label-cell-label">0</div> |
| | | </div> |
| | | |
| | | <div class="timeQuery-label" > |
| | | <div class="timeQuery-label-cell" style="left: 0%"> |
| | | <div class="timeQuery-label-cell-label">0</div> |
| | | </div> |
| | | <div v-for="index of timeNode" class="timeQuery-label-cell" :style="'left:' + (100.0/timeNode*index).toFixed(4) + '%'"> |
| | | <div class="timeQuery-label-cell-label">{{24/timeNode * index}}</div> |
| | | </div> |
| | | <ul> |
| | | <li v-for="item of allDataList" >{{!!item.name?item.name:item.dname}}</li> |
| | | </ul> |
| | | <div v-for="index of timeNode" class="timeQuery-label-cell" :style="'left:' + (100.0/timeNode*index).toFixed(4) + '%'"> |
| | | <div class="timeQuery-label-cell-label">{{24/timeNode * index}}</div> |
| | | </div> |
| | | </el-col> |
| | | </el-row> |
| | | </div> |
| | | </div> |
| | | |
| | | </div> |
| | | </template> |
| | | |
| | |
| | | name: "test", |
| | | data() { |
| | | return { |
| | | allDataList:[], |
| | | mouseDown: false, |
| | | timeNode: 24, |
| | | recordData:[ |
| | | { |
| | |
| | | }; |
| | | }, |
| | | mounted() { |
| | | var list1 = [{ |
| | | key: Math.random()*10, |
| | | name: "人1" |
| | | },{ |
| | | key: Math.random()*10, |
| | | name: "人2" |
| | | },{ |
| | | key: Math.random()*10, |
| | | name: "人3" |
| | | }] |
| | | var list2 = [{ |
| | | key: Math.random()*10, |
| | | dname: "部门1" |
| | | },{ |
| | | key: Math.random()*10, |
| | | dname: "部门2" |
| | | },{ |
| | | key: Math.random()*10, |
| | | dname: "部门3" |
| | | }] |
| | | |
| | | var allData = list1.concat(list2) |
| | | allData.sort((a, b)=>{ |
| | | return a.key-b.key; |
| | | }) |
| | | this.allDataList = allData; |
| | | for (let i = 1; i <= 24; i++) { |
| | | console.log("<div class=\"timeQuery-label-cell\" style=\"left: " + (100.0/24*i).toFixed(4) + "%\"></div>") |
| | | } |
| | | document.body.addEventListener("mouseup", this.mouseupHandler, false) |
| | | document.body.addEventListener("mousemove", this.mousemoveHandler, false) |
| | | }, |
| | | methods:{ |
| | | getTimeNode(){ |
| | |
| | | }else if (width/20 > 6) { |
| | | return 6 |
| | | } |
| | | }, |
| | | hoveEvent(event){ |
| | | console.log(2222222) |
| | | console.log(event) |
| | | }, |
| | | timeChoose(event){ |
| | | console.log(event) |
| | |
| | | console.log(differenceTime) |
| | | console.log(result) |
| | | return parseFloat(differenceTime/(24*60*60*10)); |
| | | }, |
| | | mousedownHandler(event){ |
| | | this.mouseDown = true |
| | | }, |
| | | mousemoveHandler(event){ |
| | | if (this.mouseDown){ |
| | | document.getElementById("timeQueryPointer").style.left = (event.clientX - 20)+ "px" |
| | | } |
| | | }, |
| | | mouseupHandler(event){ |
| | | this.mouseDown = false |
| | | } |
| | | } |
| | | } |
| | |
| | | z-index: 11; |
| | | -webkit-box-shadow: #9d9d9d 0px 0px 10px inset; |
| | | margin-top: 3px; |
| | | top: 100%; |
| | | } |
| | | .timeQuery-label{ |
| | | height: 16px; |
| | |
| | | } |
| | | .timeQuery-pointer-content{ |
| | | width: 0px; |
| | | height: 16px; |
| | | height: 70px; |
| | | position: absolute; |
| | | border-right: 3px solid #f60303; |
| | | border-right: 2px solid #f60303; |
| | | z-index: 14; |
| | | top: -30px; |
| | | } |
| | | .timeQuery-pointer-handle { |
| | | width: 0; |
| | | height: 0; |
| | | border-top: 12px solid transparent; |
| | | border-right: 12px solid transparent; |
| | | border-bottom: 20px solid #ff0909; |
| | | border-left: 12px solid transparent; |
| | | cursor: no-drop; |
| | | position: absolute; |
| | | left: -11px; |
| | | top: 50px; |
| | | } |
| | | /*.timeQuery-cell:after{*/ |
| | | /* content: "";*/ |
New file |
| | |
| | | <template> |
| | | <div id="test2"> |
| | | <div class="timeQuery" style="width: 100%; height: 300px" id="timeQuery"> |
| | | </div> |
| | | </div> |
| | | </template> |
| | | |
| | | <script> |
| | | |
| | | import * as echarts from 'echarts'; |
| | | |
| | | export default { |
| | | name: "test2", |
| | | data() { |
| | | return { |
| | | }; |
| | | }, |
| | | mounted() { |
| | | var base = +new Date("2021-02-02 00:00:00"); |
| | | var oneDay = 24 * 3600 * 1000; |
| | | |
| | | var data = [[base, 10]]; |
| | | |
| | | for (var i = 1; i < 24; i++) { |
| | | var now = new Date(base += oneDay); |
| | | data.push([ |
| | | new Date("2021-02-02 " + i+":00:00"), 10 |
| | | ]); |
| | | } |
| | | // 基于准备好的dom,初始化echarts实例 |
| | | var myChart = echarts.init(document.getElementById('timeQuery')); |
| | | let option = { |
| | | |
| | | toolbox: { |
| | | feature: { |
| | | dataZoom: { |
| | | yAxisIndex: 'none' |
| | | }, |
| | | restore: {}, |
| | | saveAsImage: {} |
| | | } |
| | | }, |
| | | xAxis: { |
| | | type: 'time', |
| | | boundaryGap: false |
| | | }, |
| | | yAxis: { |
| | | type: 'value', |
| | | show: false, |
| | | splitLine:{show: false}, //去除网格线 |
| | | boundaryGap: [0, '100%'] |
| | | }, |
| | | dataZoom: [{ |
| | | type: 'inside', |
| | | start: 0, |
| | | end: 20 |
| | | }, { |
| | | start: 0, |
| | | end: 20 |
| | | }], |
| | | series: [ |
| | | { |
| | | name: '模拟数据', |
| | | type: 'line', |
| | | smooth: false, |
| | | symbol: 'none', |
| | | areaStyle: {}, |
| | | data: data |
| | | } |
| | | ] |
| | | }; |
| | | // 绘制图表 |
| | | myChart.setOption(option); |
| | | }, |
| | | methods:{ |
| | | getTimeNode(){ |
| | | let mine = 20 |
| | | let width = document.getElementById("timeQuery").offsetWidth |
| | | if (width/20 > 24){ |
| | | return 24 |
| | | }else if (width/20 > 12) { |
| | | return 12 |
| | | }else if (width/20 > 6) { |
| | | return 6 |
| | | } |
| | | }, |
| | | hoveEvent(event){ |
| | | console.log(2222222) |
| | | console.log(event) |
| | | }, |
| | | timeChoose(event){ |
| | | console.log(event) |
| | | }, |
| | | getDataWidth(item){ |
| | | let startTime = new Date(item.startTime); |
| | | let endTime = new Date(item.endTime); |
| | | let result = parseFloat((endTime.getTime() - startTime.getTime())/(24*60*60*10)) |
| | | // console.log(result) |
| | | return parseFloat((endTime.getTime() - startTime.getTime())/(24*60*60*10)) |
| | | }, |
| | | getDataLeft(item){ |
| | | let startTime = new Date(item.startTime); |
| | | let differenceTime = startTime.getTime() - new Date(item.startTime.substr(0,10) + " 00:00:00").getTime() |
| | | let result = differenceTime/(24*60*60*10) |
| | | console.log(differenceTime) |
| | | console.log(result) |
| | | return parseFloat(differenceTime/(24*60*60*10)); |
| | | } |
| | | } |
| | | } |
| | | </script> |
| | | |
| | | <style scoped> |
| | | .timeQuery{ |
| | | width: 96%; |
| | | margin-left: 2%; |
| | | margin-right: 2%; |
| | | margin-top: 20%; |
| | | position: absolute; |
| | | } |
| | | .timeQuery-background{ |
| | | height: 16px; |
| | | width: 100%; |
| | | background-color: #ececec; |
| | | position: absolute; |
| | | left: 0; |
| | | top: 0; |
| | | z-index: 10; |
| | | box-shadow: #9d9d9d 0px 0px 10px inset; |
| | | } |
| | | .timeQuery-data{ |
| | | height: 16px; |
| | | width: 100%; |
| | | position: absolute; |
| | | left: 0; |
| | | top: 0; |
| | | z-index: 11; |
| | | } |
| | | .timeQuery-data-cell{ |
| | | height: 10px; |
| | | background-color: #888787; |
| | | position: absolute; |
| | | z-index: 11; |
| | | -webkit-box-shadow: #9d9d9d 0px 0px 10px inset; |
| | | margin-top: 3px; |
| | | } |
| | | .timeQuery-label{ |
| | | height: 16px; |
| | | width: 100%; |
| | | position: absolute; |
| | | pointer-events: none; |
| | | left: 0; |
| | | top: 0; |
| | | z-index: 11; |
| | | } |
| | | .timeQuery-label-cell{ |
| | | height: 16px; |
| | | position: absolute; |
| | | z-index: 12; |
| | | width: 0px; |
| | | border-right: 1px solid #b7b7b7; |
| | | } |
| | | .timeQuery-label-cell-label { |
| | | width: 23px; |
| | | text-align: center; |
| | | height: 18px; |
| | | margin-left: -10px; |
| | | margin-top: -30px; |
| | | color: #444; |
| | | } |
| | | .timeQuery-pointer{ |
| | | width: 0px; |
| | | height: 18px; |
| | | position: absolute; |
| | | left: 0; |
| | | } |
| | | .timeQuery-pointer-content{ |
| | | width: 0px; |
| | | height: 16px; |
| | | position: absolute; |
| | | border-right: 3px solid #f60303; |
| | | z-index: 14; |
| | | } |
| | | /*.timeQuery-cell:after{*/ |
| | | /* content: "";*/ |
| | | /* height: 14px;*/ |
| | | /* border: 1px solid #e70303;*/ |
| | | /* position: absolute;*/ |
| | | /*}*/ |
| | | </style> |
| | |
| | | import devicePosition from '../components/devicePosition.vue' |
| | | import login from '../components/Login.vue' |
| | | import parentPlatformList from '../components/ParentPlatformList.vue' |
| | | import cloudRecord from '../components/CloudRecord.vue' |
| | | import test from '../components/test.vue' |
| | | import web from '../components/setting/Web.vue' |
| | | import sip from '../components/setting/Sip.vue' |
| | |
| | | component: devicePosition, |
| | | }, |
| | | { |
| | | path: '/cloudRecord', |
| | | name: 'cloudRecord', |
| | | component: cloudRecord, |
| | | }, |
| | | { |
| | | path: '/setting/web', |
| | | name: 'web', |
| | | component: web, |