src/main/java/com/genersoft/iot/vmp/common/VideoManagerConstants.java
@@ -18,6 +18,8 @@ public static final String PLAYER_PREFIX = "VMP_player_"; public static final String PLAY_BLACK_PREFIX = "VMP_playback_"; public static final String EVENT_ONLINE_REGISTER = "1"; public static final String EVENT_ONLINE_KEEPLIVE = "2"; src/main/java/com/genersoft/iot/vmp/gb28181/bean/RecordItem.java
@@ -1,12 +1,18 @@ package com.genersoft.iot.vmp.gb28181.bean; import org.jetbrains.annotations.NotNull; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.Date; /** * @Description:设备录像bean * @author: swwheihei * @date: 2020年5月8日 下午2:06:54 */ public class RecordItem { public class RecordItem implements Comparable<RecordItem>{ private String deviceId; @@ -97,4 +103,21 @@ public void setRecorderId(String recorderId) { this.recorderId = recorderId; } @Override public int compareTo(@NotNull RecordItem recordItem) { SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); try { Date startTime_now = sdf.parse(startTime); Date startTime_param = sdf.parse(recordItem.getStartTime()); if (startTime_param.compareTo(startTime_now) > 0) { return -1; }else { return 1; } } catch (ParseException e) { e.printStackTrace(); } return 0; } } src/main/java/com/genersoft/iot/vmp/gb28181/transmit/cmd/ISIPCommander.java
@@ -80,7 +80,7 @@ * @param startTime 开始时间,格式要求:yyyy-MM-dd HH:mm:ss * @param endTime 结束时间,格式要求:yyyy-MM-dd HH:mm:ss */ public String playbackStreamCmd(Device device,String channelId, String startTime, String endTime); public StreamInfo playbackStreamCmd(Device device,String channelId, String startTime, String endTime); /** * 视频流停止 src/main/java/com/genersoft/iot/vmp/gb28181/transmit/cmd/impl/SIPCommander.java
@@ -279,7 +279,7 @@ * @param endTime 结束时间,格式要求:yyyy-MM-dd HH:mm:ss */ @Override public String playbackStreamCmd(Device device, String channelId, String startTime, String endTime) { public StreamInfo playbackStreamCmd(Device device, String channelId, String startTime, String endTime) { try { MediaServerConfig mediaInfo = storager.getMediaInfo(); String ssrc = streamSession.createPlayBackSsrc(); @@ -324,7 +324,13 @@ ClientTransaction transaction = transmitRequest(device, request); streamSession.put(ssrc, transaction); return ssrc; StreamInfo streamInfo = new StreamInfo(); streamInfo.setSsrc(ssrc); streamInfo.setCahnnelId(channelId); streamInfo.setDeviceID(device.getDeviceId()); boolean b = storager.startPlayBlack(streamInfo); return streamInfo; } catch ( SipException | ParseException | InvalidArgumentException e) { e.printStackTrace(); src/main/java/com/genersoft/iot/vmp/gb28181/transmit/request/impl/MessageRequestProcessor.java
@@ -2,11 +2,7 @@ import java.io.ByteArrayInputStream; import java.text.ParseException; import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.*; import javax.sip.InvalidArgumentException; import javax.sip.RequestEvent; @@ -316,6 +312,7 @@ record.setRecorderId(XmlUtil.getText(itemRecord,"RecorderID")); recordList.add(record); } // recordList.sort(Comparator.naturalOrder()); recordInfo.setRecordList(recordList); } @@ -349,9 +346,13 @@ // 走到这里,有以下可能:1、没有录像信息,第一次收到recordinfo的消息即返回响应数据,无redis操作 // 2、有录像数据,且第一次即收到完整数据,返回响应数据,无redis操作 // 3、有录像数据,在超时时间内收到多次包组装后数量足够,返回数据 // 对记录进行排序 RequestMessage msg = new RequestMessage(); msg.setDeviceId(deviceId); msg.setType(DeferredResultHolder.CALLBACK_CMD_RECORDINFO); // 自然顺序排序, 元素进行升序排列 recordInfo.getRecordList().sort(Comparator.naturalOrder()); msg.setData(recordInfo); deferredResultHolder.invokeResult(msg); } catch (DocumentException e) { src/main/java/com/genersoft/iot/vmp/media/zlm/ZLMHttpHookListener.java
@@ -130,17 +130,27 @@ String streamId = json.getString("id"); // String ssrc = String.format("%10d", Integer.parseInt(streamId, 16)); // ZLM 要求大写且首位补零 String ssrc = new DecimalFormat("0000000000").format(Integer.parseInt(streamId, 16)); StreamInfo streamInfo = storager.queryPlayBySSRC(ssrc); if ("rtp".equals(app) && streamInfo != null ) { StreamInfo streamInfoForPlay = storager.queryPlayBySSRC(ssrc); if ("rtp".equals(app) && streamInfoForPlay != null ) { MediaServerConfig mediaInfo = storager.getMediaInfo(); streamInfo.setFlv(String.format("http://%s:%s/rtp/%s.flv", mediaInfo.getLocalIP(), mediaInfo.getHttpPort(), streamId)); streamInfo.setWs_flv(String.format("ws://%s:%s/rtp/%s.flv", mediaInfo.getLocalIP(), mediaInfo.getHttpPort(), streamId)); streamInfo.setRtmp(String.format("rtmp://%s:%s/rtp/%s", mediaInfo.getLocalIP(), mediaInfo.getRtmpPort(), streamId)); streamInfo.setHls(String.format("http://%s:%s/rtp/%s/hls.m3u8", mediaInfo.getLocalIP(), mediaInfo.getHttpPort(), streamId)); streamInfo.setRtsp(String.format("rtsp://%s:%s/rtp/%s", mediaInfo.getLocalIP(), mediaInfo.getRtspPort(), streamId)); storager.startPlay(streamInfo); streamInfoForPlay.setFlv(String.format("http://%s:%s/rtp/%s.flv", mediaInfo.getLocalIP(), mediaInfo.getHttpPort(), streamId)); streamInfoForPlay.setWs_flv(String.format("ws://%s:%s/rtp/%s.flv", mediaInfo.getLocalIP(), mediaInfo.getHttpPort(), streamId)); streamInfoForPlay.setRtmp(String.format("rtmp://%s:%s/rtp/%s", mediaInfo.getLocalIP(), mediaInfo.getRtmpPort(), streamId)); streamInfoForPlay.setHls(String.format("http://%s:%s/rtp/%s/hls.m3u8", mediaInfo.getLocalIP(), mediaInfo.getHttpPort(), streamId)); streamInfoForPlay.setRtsp(String.format("rtsp://%s:%s/rtp/%s", mediaInfo.getLocalIP(), mediaInfo.getRtspPort(), streamId)); storager.startPlay(streamInfoForPlay); } StreamInfo streamInfoForPlayBack = storager.queryPlayBlackBySSRC(ssrc); if ("rtp".equals(app) && streamInfoForPlayBack != null ) { MediaServerConfig mediaInfo = storager.getMediaInfo(); streamInfoForPlayBack.setFlv(String.format("http://%s:%s/rtp/%s.flv", mediaInfo.getLocalIP(), mediaInfo.getHttpPort(), streamId)); streamInfoForPlayBack.setWs_flv(String.format("ws://%s:%s/rtp/%s.flv", mediaInfo.getLocalIP(), mediaInfo.getHttpPort(), streamId)); streamInfoForPlayBack.setRtmp(String.format("rtmp://%s:%s/rtp/%s", mediaInfo.getLocalIP(), mediaInfo.getRtmpPort(), streamId)); streamInfoForPlayBack.setHls(String.format("http://%s:%s/rtp/%s/hls.m3u8", mediaInfo.getLocalIP(), mediaInfo.getHttpPort(), streamId)); streamInfoForPlayBack.setRtsp(String.format("rtsp://%s:%s/rtp/%s", mediaInfo.getLocalIP(), mediaInfo.getRtspPort(), streamId)); storager.startPlayBlack(streamInfoForPlayBack); } // TODO Auto-generated method stub src/main/java/com/genersoft/iot/vmp/media/zlm/ZLMUtils.java
@@ -30,11 +30,6 @@ param.put("stream_id", streamId); JSONObject jsonObject = zlmresTfulUtils.openRtpServer(param); if (jsonObject.getInteger("code") == 0) { System.out.println(11111111); System.out.println(streamId); System.out.println(ssrc); System.out.println(newPort); System.out.println(jsonObject.toJSONString()); return newPort; } else { return getNewRTPPort(ssrc); src/main/java/com/genersoft/iot/vmp/storager/IVideoManagerStorager.java
@@ -183,4 +183,12 @@ StreamInfo queryPlayByDevice(String deviceId, String code); Map<String, StreamInfo> queryPlayByDeviceId(String deviceId); boolean startPlayBlack(StreamInfo streamInfo); boolean stopPlayBlack(StreamInfo streamInfo); StreamInfo queryPlayBlackByDevice(String deviceId, String channelId); StreamInfo queryPlayBlackBySSRC(String ssrc); } src/main/java/com/genersoft/iot/vmp/storager/jdbc/VideoManagerJdbcStoragerImpl.java
@@ -190,6 +190,27 @@ @Override public Map<String, StreamInfo> queryPlayByDeviceId(String deviceId) { return null; } @Override public boolean startPlayBlack(StreamInfo streamInfo) { return false; } @Override public boolean stopPlayBlack(StreamInfo streamInfo) { return false; } @Override public StreamInfo queryPlayBlackByDevice(String deviceId, String channelId) { return null; } @Override public StreamInfo queryPlayBlackBySSRC(String ssrc) { return null; } } src/main/java/com/genersoft/iot/vmp/storager/redis/VideoManagerRedisStoragerImpl.java
@@ -101,7 +101,7 @@ // 如果有父设备,更新父设备内子节点数 String parentId = channel.getParentId(); if (!StringUtils.isEmpty(parentId)) { if (!StringUtils.isEmpty(parentId) && !parentId.equals(deviceId)) { if (channelMap.get(parentId) == null) { channelMap.put(parentId, new HashSet<String>()); @@ -111,8 +111,6 @@ DeviceChannel deviceChannel = queryChannel(deviceId, parentId); if (deviceChannel != null) { deviceChannel.setSubCount(channelMap.get(parentId).size()); // redis.set(VideoManagerConstants.CACHEKEY_PREFIX+deviceId + "_" + deviceChannel.getChannelId(), // deviceChannel); redis.set(VideoManagerConstants.CACHEKEY_PREFIX + deviceId + "_" + deviceChannel.getChannelId() + "_" + (deviceChannel.getStatus() == 1 ? "on":"off") + @@ -411,6 +409,14 @@ } @Override public StreamInfo queryPlayBlackBySSRC(String ssrc) { // List<Object> playLeys = redis.keys(String.format("%S_%s_*", VideoManagerConstants.PLAYER_PREFIX, ssrc)); List<Object> playLeys = redis.scan(String.format("%S_%s_*", VideoManagerConstants.PLAY_BLACK_PREFIX, ssrc)); if (playLeys == null || playLeys.size() == 0) return null; return (StreamInfo)redis.get(playLeys.get(0).toString()); } @Override public StreamInfo queryPlayByDevice(String deviceId, String code) { // List<Object> playLeys = redis.keys(String.format("%S_*_%s_%s", VideoManagerConstants.PLAYER_PREFIX, List<Object> playLeys = redis.scan(String.format("%S_*_%s_%s", VideoManagerConstants.PLAYER_PREFIX, @@ -448,7 +454,6 @@ for (Device device : devices) { // 更新设备下的通道 HashMap<String, HashSet<String>> channelMap = new HashMap<String, HashSet<String>>(); // List<Object> deviceChannelList = redis.keys(VideoManagerConstants.CACHEKEY_PREFIX + List<Object> deviceChannelList = redis.scan(VideoManagerConstants.CACHEKEY_PREFIX + device.getDeviceId() + "_" + "*"); if (deviceChannelList != null && deviceChannelList.size() > 0 ) { @@ -469,6 +474,7 @@ } deviceMap.put(device.getDeviceId(),channelMap); } System.out.println(); } @Override @@ -498,5 +504,37 @@ } @Override public boolean startPlayBlack(StreamInfo stream) { return redis.set(String.format("%S_%s_%s_%s", VideoManagerConstants.PLAY_BLACK_PREFIX, stream.getSsrc(),stream.getDeviceID(), stream.getCahnnelId()), stream); } @Override public boolean stopPlayBlack(StreamInfo streamInfo) { if (streamInfo == null) return false; DeviceChannel deviceChannel = queryChannel(streamInfo.getDeviceID(), streamInfo.getCahnnelId()); if (deviceChannel != null) { deviceChannel.setSsrc(null); deviceChannel.setPlay(false); updateChannel(streamInfo.getDeviceID(), deviceChannel); } return redis.del(String.format("%S_%s_%s_%s", VideoManagerConstants.PLAY_BLACK_PREFIX, streamInfo.getSsrc(), streamInfo.getDeviceID(), streamInfo.getCahnnelId())); } @Override public StreamInfo queryPlayBlackByDevice(String deviceId, String code) { String format = String.format("%S_*_%s_%s", VideoManagerConstants.PLAY_BLACK_PREFIX, deviceId, code); List<Object> playLeys = redis.scan(String.format("%S_*_%s_%s", VideoManagerConstants.PLAY_BLACK_PREFIX, deviceId, code)); if (playLeys == null || playLeys.size() == 0) return null; return (StreamInfo)redis.get(playLeys.get(0).toString()); } } src/main/java/com/genersoft/iot/vmp/vmanager/play/PlayController.java
@@ -71,7 +71,7 @@ }else { streamInfo = storager.queryPlayByDevice(deviceId, channelId); JSONObject rtpInfo = zlmresTfulUtils.getRtpInfo(streamId); if (rtpInfo != null && rtpInfo.getBoolean("exist") && streamInfo.getFlv() != null){ if (rtpInfo != null && rtpInfo.getBoolean("exist") && streamInfo != null && streamInfo.getFlv() != null){ logger.info("RTP已推流,查询编码信息:"+streamInfo.getFlv()); Thread.sleep(2000); JSONObject mediaInfo = zlmresTfulUtils.getMediaInfo("rtp", "rtmp", streamId); src/main/java/com/genersoft/iot/vmp/vmanager/playback/PlaybackController.java
@@ -1,5 +1,9 @@ package com.genersoft.iot.vmp.vmanager.playback; import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSONArray; import com.genersoft.iot.vmp.common.StreamInfo; import com.genersoft.iot.vmp.media.zlm.ZLMRESTfulUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; @@ -30,7 +34,10 @@ @Autowired private IVideoManagerStorager storager; @Autowired private ZLMRESTfulUtils zlmresTfulUtils; @GetMapping("/playback/{deviceId}/{channelId}") public ResponseEntity<String> play(@PathVariable String deviceId,@PathVariable String channelId, String startTime, String endTime){ @@ -43,25 +50,70 @@ logger.warn(log); return new ResponseEntity<String>(log,HttpStatus.BAD_REQUEST); } Device device = storager.queryVideoDevice(deviceId); String ssrc = cmder.playbackStreamCmd(device, channelId, startTime, endTime); StreamInfo streamInfo = storager.queryPlayBlackByDevice(deviceId, channelId); if (streamInfo != null) { cmder.streamByeCmd(streamInfo.getSsrc()); } // }else { // String streamId = String.format("%08x", Integer.parseInt(streamInfo.getSsrc())).toUpperCase(); // JSONObject rtpInfo = zlmresTfulUtils.getRtpInfo(streamId); // if (rtpInfo.getBoolean("exist")) { // return new ResponseEntity<String>(JSON.toJSONString(streamInfo),HttpStatus.OK); // }else { // storager.stopPlayBlack(streamInfo); // streamInfo = cmder.playbackStreamCmd(device, channelId, startTime, endTime); // } // } streamInfo = cmder.playbackStreamCmd(device, channelId, startTime, endTime); String streamId = String.format("%08x", Integer.parseInt(streamInfo.getSsrc())).toUpperCase(); if (logger.isDebugEnabled()) { logger.debug("设备回放 API调用,ssrc:"+ssrc+",ZLMedia streamId:"+Integer.toHexString(Integer.parseInt(ssrc))); logger.debug("设备回放 API调用,ssrc:" + streamInfo.getSsrc() + ",ZLMedia streamId:" + streamId); } if(ssrc!=null) { JSONObject json = new JSONObject(); json.put("ssrc", ssrc); return new ResponseEntity<String>(json.toString(),HttpStatus.OK); // 等待推流, TODO 默认超时15s boolean lockFlag = true; long lockStartTime = System.currentTimeMillis(); while (lockFlag) { try { if (System.currentTimeMillis() - lockStartTime > 75 * 1000) { storager.stopPlayBlack(streamInfo); return new ResponseEntity<String>("timeout",HttpStatus.OK); }else { streamInfo = storager.queryPlayBlackByDevice(deviceId, channelId); JSONObject rtpInfo = zlmresTfulUtils.getRtpInfo(streamId); if (rtpInfo != null && rtpInfo.getBoolean("exist") && streamInfo.getFlv() != null){ JSONObject mediaInfo = zlmresTfulUtils.getMediaInfo("rtp", "rtmp", streamId); if (mediaInfo.getInteger("code") == 0 && mediaInfo.getBoolean("online")) { lockFlag = false; JSONArray tracks = mediaInfo.getJSONArray("tracks"); streamInfo.setTracks(tracks); storager.startPlayBlack(streamInfo); }else { } }else { Thread.sleep(2000); continue; }; } } catch (InterruptedException e) { e.printStackTrace(); } } if(streamInfo!=null) { return new ResponseEntity<String>(JSON.toJSONString(streamInfo),HttpStatus.OK); } else { logger.warn("设备回放API调用失败!"); return new ResponseEntity<String>(HttpStatus.INTERNAL_SERVER_ERROR); } } @PostMapping("/playback/{ssrc}/stop") @RequestMapping("/playback/{ssrc}/stop") public ResponseEntity<String> playStop(@PathVariable String ssrc){ cmder.streamByeCmd(ssrc); web_src/index.html
@@ -3,7 +3,7 @@ <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width,initial-scale=1.0"> <title>GB28181服务器</title> <title>国标28181</title> </head> <body> <script type="text/javascript" src="./js/liveplayer-lib.min.js"></script> web_src/package-lock.json
@@ -5843,6 +5843,11 @@ "minimist": "^1.2.5" } }, "moment": { "version": "2.29.1", "resolved": "https://registry.npm.taobao.org/moment/download/moment-2.29.1.tgz?cache=0&sync_timestamp=1601983423917&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fmoment%2Fdownload%2Fmoment-2.29.1.tgz", "integrity": "sha1-sr52n6MZQL6e7qZGnAdeNQBvo9M=" }, "move-concurrently": { "version": "1.0.1", "resolved": "https://registry.npm.taobao.org/move-concurrently/download/move-concurrently-1.0.1.tgz", web_src/package.json
@@ -15,6 +15,7 @@ "core-js": "^2.6.5", "echarts": "^4.7.0", "element-ui": "2.10.1", "moment": "^2.29.1", "vue": "^2.6.11", "vue-clipboard2": "^0.3.1", "vue-cookies": "^1.7.4", web_src/src/components/Loading.vue
File was deleted web_src/src/components/Login.vue
@@ -10,16 +10,14 @@ <div class="log-logo">Welcome!</div> <div class="log-text"></div> </div> <div class="log-email"> <div class="log-email" v-loading="isLoging" > <input type="text" placeholder="用户名" :class="'log-input' + (username==''?' log-input-empty':'')" v-model="username"><input type="password" placeholder="密码" :class="'log-input' + (password==''?' log-input-empty':'')" v-model="password"> <a href="javascript:;" class="log-btn" @click="login" >登录</a> </div> <Loading v-if="isLoging" marginTop="-30%"></Loading> </div> </template> <script> import Loading from './Loading.vue' import crypto from 'crypto' export default { name: 'Login', @@ -29,9 +27,6 @@ username: '', password: '' } }, components:{ Loading }, created(){ var that = this; @@ -79,7 +74,8 @@ } }) .catch(function (error) { console.log(error); that.$message.error(error.response.statusText); that.isLoging = false; }); web_src/src/components/channelList.vue
@@ -26,7 +26,7 @@ </el-select> </div> <devicePlayer ref="devicePlayer"></devicePlayer> <devicePlayer ref="devicePlayer" v-loading="isLoging"></devicePlayer> <!--设备列表--> <el-table ref="channelListTable" :data="deviceChannelList" :height="winHeight" border style="width: 100%"> <el-table-column prop="channelId" label="通道编号" width="210"> @@ -58,8 +58,8 @@ <el-button size="mini" icon="el-icon-video-play" @click="sendDevicePush(scope.row)">播放</el-button> <el-button size="mini" icon="el-icon-switch-button" type="danger" v-if="scope.row.play" @click="stopDevicePush(scope.row)">停止</el-button> <el-button size="mini" icon="el-icon-s-open" type="primary" v-if="scope.row.parental == 1" @click="changeSubchannel(scope.row)">查看</el-button> <!-- <el-button size="mini" icon="el-icon-video-camera" type="primary" >设备录象</el-button>--> <!-- <el-button size="mini" @click="sendDevicePush(scope.row)">录像查询</el-button> --> <el-button size="mini" icon="el-icon-video-camera" type="primary" @click="queryRecords(scope.row)">设备录象</el-button> <!-- <el-button size="mini" @click="sendDevicePush(scope.row)">录像查询</el-button> --> </el-button-group> </template> </el-table-column> @@ -69,20 +69,18 @@ </el-main> </el-container> <Loading v-if="isLoging" marginTop="-50%"></Loading> </div> </template> <script> import devicePlayer from './gb28181/devicePlayer.vue' import uiHeader from './UiHeader.vue' import Loading from './Loading.vue' import moment from "moment"; export default { name: 'channelList', components: { devicePlayer, uiHeader, Loading uiHeader }, data() { return { @@ -209,13 +207,23 @@ let ssrc = res.data.ssrc; that.isLoging = false; if (!!ssrc) { that.$refs.devicePlayer.play(res.data, deviceId, channelId, itemData.hasAudio); // that.$refs.devicePlayer.play(res.data, deviceId, channelId, itemData.hasAudio); that.$refs.devicePlayer.openDialog("media", deviceId, channelId,{ streamInfo: res.data, hasAudio: itemData.hasAudio }); that.initData(); } else { that.$message.error(res.data); } }).catch(function (e) {}); }, queryRecords: function (itemData) { var format = moment().format("YYYY-M-D"); let deviceId = this.deviceId; let channelId = itemData.channelId; this.$refs.devicePlayer.openDialog("record", deviceId, channelId, {date:format}) }, stopDevicePush: function (itemData) { console.log(itemData) var that = this; web_src/src/components/gb28181/devicePlayer.vue
@@ -1,123 +1,196 @@ <template> <div id="devicePlayer"> <el-dialog title="视频播放" top="0" :visible.sync="showVideoDialog" :destroy-on-close="true" @close="close()"> <LivePlayer v-if="showVideoDialog" ref="videoPlayer" :videoUrl="videoUrl" :error="videoError" :hasaudio="hasaudio" fluent autoplay live></LivePlayer> <div id="shared" style="text-align: right; margin-top: 1rem;"> <el-tabs v-model="tabActiveName"> <el-tab-pane label="媒体流信息" name="media"> <div style="margin-bottom: 0.5rem;"> <el-button type="primary" size="small" @click="playRecord(true, '')">播放</el-button> <el-button type="primary" size="small" @click="startRecord()">录制</el-button> <el-button type="primary" size="small" @click="stopRecord()">停止录制</el-button> </div> <div style="display: flex; margin-bottom: 0.5rem; height: 2.5rem;"> <span style="width: 5rem; line-height: 2.5rem; text-align: right;">播放地址:</span> <el-input v-model="getPlayerShared.sharedUrl" :disabled="true" v-on:click.native="copySharedInfo(getPlayerShared.sharedUrl)"></el-input> </div> <div style="display: flex; margin-bottom: 0.5rem; height: 2.5rem;"> <span style="width: 5rem; line-height: 2.5rem; text-align: right;">iframe:</span> <el-input v-model="getPlayerShared.sharedIframe" :disabled="true" v-on:click.native="copySharedInfo(getPlayerShared.sharedIframe)"></el-input> </div> <div style="display: flex; margin-bottom: 0.5rem; height: 2.5rem;"> <span style="width: 5rem; line-height: 2.5rem; text-align: right;">资源地址:</span> <el-input v-model="getPlayerShared.sharedRtmp" :disabled="true" v-on:click.native="copySharedInfo(getPlayerShared.sharedRtmp)"></el-input> </div> </el-tab-pane> <!--{"code":0,"data":{"paths":["22-29-30.mp4"],"rootPath":"/home/kkkkk/Documents/ZLMediaKit/release/linux/Debug/www/record/hls/kkkkk/2020-05-11/"}}--> <el-tab-pane label="录像查询" name="second"> <el-date-picker v-model="videoHistory.startTime" type="datetime" value-format="yyyy-MM-dd HH:mm:ss" placeholder="开始时间" @change="recordList()"></el-date-picker> <el-date-picker v-model="videoHistory.endTime" type="datetime" value-format="yyyy-MM-dd HH:mm:ss" placeholder="结束时间" @change="recordList()"></el-date-picker> <el-table :data="videoHistory.searchHistoryResult" style="width: 100%"> <el-table-column label="名称" prop="name" width="150"></el-table-column> <el-table-column label="文件" prop="filePath" width="300"></el-table-column> <el-table-column label="开始时间" prop="startTime" width="160"></el-table-column> <el-table-column label="结束时间" prop="endTime" width="160"></el-table-column> <div id="devicePlayer"> <el-dialog title="视频播放" top="0" :close-on-click-modal="false" :visible.sync="showVideoDialog" :destroy-on-close="true" @close="close()"> <LivePlayer v-if="showVideoDialog" ref="videoPlayer" :videoUrl="videoUrl" :error="videoError" :hasaudio="hasaudio" fluent autoplay live ></LivePlayer> <div id="shared" style="text-align: right; margin-top: 1rem;"> <el-tabs v-model="tabActiveName"> <el-tab-pane label="实时视频" name="media"> <div style="margin-bottom: 0.5rem;"> <!-- <el-button type="primary" size="small" @click="playRecord(true, '')">播放</el-button>--> <!-- <el-button type="primary" size="small" @click="startRecord()">录制</el-button>--> <!-- <el-button type="primary" size="small" @click="stopRecord()">停止录制</el-button>--> </div> <div style="display: flex; margin-bottom: 0.5rem; height: 2.5rem;"> <span style="width: 5rem; line-height: 2.5rem; text-align: right;">播放地址:</span> <el-input v-model="getPlayerShared.sharedUrl" :disabled="true" v-on:click.native="copySharedInfo(getPlayerShared.sharedUrl)"></el-input> </div> <div style="display: flex; margin-bottom: 0.5rem; height: 2.5rem;"> <span style="width: 5rem; line-height: 2.5rem; text-align: right;">iframe:</span> <el-input v-model="getPlayerShared.sharedIframe" :disabled="true" v-on:click.native="copySharedInfo(getPlayerShared.sharedIframe)"></el-input> </div> <div style="display: flex; margin-bottom: 0.5rem; height: 2.5rem;"> <span style="width: 5rem; line-height: 2.5rem; text-align: right;">资源地址:</span> <el-input v-model="getPlayerShared.sharedRtmp" :disabled="true" v-on:click.native="copySharedInfo(getPlayerShared.sharedRtmp)"></el-input> </div> </el-tab-pane> <!--{"code":0,"data":{"paths":["22-29-30.mp4"],"rootPath":"/home/kkkkk/Documents/ZLMediaKit/release/linux/Debug/www/record/hls/kkkkk/2020-05-11/"}}--> <el-tab-pane label="录像查询" name="record"> <el-date-picker size="mini" v-model="videoHistory.date" type="date" value-format="yyyy-MM-dd" placeholder="日期" @change="queryRecords()"></el-date-picker> <!-- <el-slider style="margin: 0 1rem 1rem 1rem;"--> <!-- v-model="timeVal"--> <!-- :min="timeMin"--> <!-- :max="timeMax"--> <!-- :step="5"--> <!-- :marks="getTimeMakrs()"--> <!-- :format-tooltip="formatTooltip">--> <!-- </el-slider>--> <!-- <range-slider :min="timeMin"--> <!-- :max="timeMax"--> <!-- :step="5"></range-slider>--> <el-table-column label="操作"> <template slot-scope="scope"> <el-button type="primary" size="mini" @click="playRecord(false, scope.row)">播放</el-button> </template> </el-table-column> </el-table> </el-tab-pane> <!--遥控界面--> <el-tab-pane label="云台控制" name="third"> <div style="display: flex; justify-content: center;"> <div class="control-wrapper"> <div class="control-btn control-top" @mousedown="ptzCamera(0, 1, 0)" @mouseup="ptzCamera(0, 0, 0)"> <i class="el-icon-caret-top"></i> <div class="control-inner-btn control-inner"></div> </div> <div class="control-btn control-left" @mousedown="ptzCamera(1, 0, 0)" @mouseup="ptzCamera(0, 0, 0)"> <i class="el-icon-caret-left"></i> <div class="control-inner-btn control-inner"></div> </div> <div class="control-btn control-bottom" @mousedown="ptzCamera(0, 2, 0)" @mouseup="ptzCamera(0, 0, 0)"> <i class="el-icon-caret-bottom"></i> <div class="control-inner-btn control-inner"></div> </div> <div class="control-btn control-right" @mousedown="ptzCamera(2, 0, 0)" @mouseup="ptzCamera(0, 0, 0)"> <i class="el-icon-caret-right"></i> <div class="control-inner-btn control-inner"></div> </div> <div class="control-round"> <div class="control-round-inner"><i class="fa fa-pause-circle"></i></div> </div> <!-- <el-date-picker v-model="videoHistory.endTime" type="datetime" value-format="yyyy-MM-dd HH:mm:ss" placeholder="结束时间"--> <!-- @change="recordList()"></el-date-picker>--> <el-table :data="videoHistory.searchHistoryResult" height="150" v-load="recordsLoading"> <el-table-column label="名称" prop="name"></el-table-column> <el-table-column label="文件" prop="filePath"></el-table-column> <el-table-column label="开始时间" prop="startTime" :formatter="timeFormatter"></el-table-column> <el-table-column label="结束时间" prop="endTime" :formatter="timeFormatter"></el-table-column> <div style="position: absolute; left: 7.25rem; top: 1.25rem" @mousedown="ptzCamera(0, 0, 2)" @mouseup="ptzCamera(0, 0, 0)"><i class="el-icon-zoom-in" style="font-size: 1.875rem;"></i></div> <div style="position: absolute; left: 7.25rem; top: 3.25rem; font-size: 1.875rem;" @mousedown="ptzCamera(0, 0, 1)" @mouseup="ptzCamera(0, 0, 0)"><i class="el-icon-zoom-out"></i></div> </div> </div> <el-table-column label="操作" > <template slot-scope="scope"> <el-button icon="el-icon-video-play" size="mini" @click="playRecord(scope.row)">播放</el-button> </template> </el-table-column> </el-table> </el-tab-pane> <!--遥控界面--> <el-tab-pane label="云台控制" name="control"> <div style="display: flex; justify-content: center;"> <div class="control-wrapper"> <div class="control-btn control-top" @mousedown="ptzCamera(0, 1, 0)" @mouseup="ptzCamera(0, 0, 0)"> <i class="el-icon-caret-top"></i> <div class="control-inner-btn control-inner"></div> </div> <div class="control-btn control-left" @mousedown="ptzCamera(1, 0, 0)" @mouseup="ptzCamera(0, 0, 0)"> <i class="el-icon-caret-left"></i> <div class="control-inner-btn control-inner"></div> </div> <div class="control-btn control-bottom" @mousedown="ptzCamera(0, 2, 0)" @mouseup="ptzCamera(0, 0, 0)"> <i class="el-icon-caret-bottom"></i> <div class="control-inner-btn control-inner"></div> </div> <div class="control-btn control-right" @mousedown="ptzCamera(2, 0, 0)" @mouseup="ptzCamera(0, 0, 0)"> <i class="el-icon-caret-right"></i> <div class="control-inner-btn control-inner"></div> </div> <div class="control-round"> <div class="control-round-inner"><i class="fa fa-pause-circle"></i></div> </div> <div style="position: absolute; left: 7.25rem; top: 1.25rem" @mousedown="ptzCamera(0, 0, 2)" @mouseup="ptzCamera(0, 0, 0)"><i class="el-icon-zoom-in" style="font-size: 1.875rem;"></i></div> <div style="position: absolute; left: 7.25rem; top: 3.25rem; font-size: 1.875rem;" @mousedown="ptzCamera(0, 0, 1)" @mouseup="ptzCamera(0, 0, 0)"><i class="el-icon-zoom-out"></i></div> </div> </div> </el-tab-pane> </el-tabs> </div> </el-dialog> </div> </el-tab-pane> </el-tabs> </div> </el-dialog> </div> </template> <script> import LivePlayer from '@liveqing/liveplayer' export default { name: 'devicePlayer', props: {}, components: { LivePlayer }, computed: { getPlayerShared: function () { return { sharedUrl: window.location.host + '/' + this.videoUrl, sharedIframe: '<iframe src="' + window.location.host + '/' + this.videoUrl + '"></iframe>', sharedRtmp: this.videoUrl }; } }, created() { // this.videoHistory.searchHistoryResult = falsificationData.recordData.recordList; }, data() { import LivePlayer from '@liveqing/liveplayer' export default { name: 'devicePlayer', props: {}, components: { LivePlayer }, computed: { getPlayerShared: function() { return { video: 'http://lndxyj.iqilu.com/public/upload/2019/10/14/8c001ea0c09cdc59a57829dabc8010fa.mp4', videoUrl: '', videoHistory: { startTime: '', endTime: '', searchHistoryResult: [] //媒体流历史记录搜索结果 }, showVideoDialog: false, normalssrc: '', ssrc: '', deviceId: '', channelId: '', tabActiveName: 'media', hasaudio: false sharedUrl: window.location.host + '/' + this.videoUrl, sharedIframe: '<iframe src="' + window.location.host + '/' + this.videoUrl + '"></iframe>', sharedRtmp: this.videoUrl }; } }, created() { }, data() { return { video:'http://lndxyj.iqilu.com/public/upload/2019/10/14/8c001ea0c09cdc59a57829dabc8010fa.mp4', videoUrl: '', videoHistory: { date: '', searchHistoryResult: [] //媒体流历史记录搜索结果 }, timeMakrs:{ // 0 : "0:00", // // 60 : "1:00", // 120 : "2:00", // // 180 : "3:00", // 240 : "4:00", // // 300 : "5:00", // 360 : "6:00", // // 420 : "7:00", // 480 : "8:00", // 540 : "9:00", 600: "10:00", // 660 : "11:00", 720 : "12:00", // 780 : "13:00", 840 : "14:00", // 900 : "15:00", 960 : "16:00", // 1020 : "17:00", 1080 : "18:00", // 1140 : "19:00", // 1200 : "20:00", // // 1260 : "21:00", // 1320 : "22:00", // // 1380 : "23:00", // 1440 : "24:00" }, showVideoDialog: false, ssrc: '', deviceId: '', channelId: '', tabActiveName: 'media', hasaudio: false, loadingRecords: false, recordsLoading: false, timeVal: 0, timeMin: 0, timeMax: 1440, }; }, methods: { }; }, methods: { openDialog: function (tab, deviceId, channelId, param) { this.tabActiveName = tab; this.channelId = channelId; this.deviceId = deviceId; this.ssrc = ""; this.videoUrl = "" if (!!this.$refs.videoPlayer) { this.$refs.videoPlayer.pause(); } play: function (streamInfo, deviceId, channelId, hasAudio) { switch(tab) { case "media": this.play(param.streamInfo, param.hasAudio) break; case "record": this.showVideoDialog = true; this.videoHistory.date = param.date; this.queryRecords() break; case "control": break; } }, timeAxisSelTime:function (val) { console.log(val) }, getTimeMakrs(){ return this.timeMakrs; }, play: function (streamInfo, hasAudio) { this.hasaudio = hasAudio; // 根据媒体流信息二次判断 if (!!streamInfo.tracks && streamInfo.tracks.length > 0) { @@ -130,8 +203,6 @@ this.hasaudio = realHasAudio && this.hasaudio; } this.ssrc = streamInfo.ssrc; this.deviceId = deviceId; this.channelId = channelId; // this.$refs.videoPlayer.hasaudio = hasAudio; // this.videoUrl = streamInfo.flv + "?" + new Date().getTime(); this.videoUrl = streamInfo.ws_flv; @@ -165,211 +236,276 @@ ); }, recordList: function () { if (!this.videoHistory.startTime || !this.videoHistory.endTime) { return; } let that = this; this.$axios({ method: 'get', url: '/api/record/' + this.deviceId + '/' + this.channelId + '?startTime=' + this.videoHistory .startTime + '&endTime=' + this.videoHistory.endTime }).then(function (res) { console.log(JSON.stringify(res)); }).catch(function (e) { // that.videoHistory.searchHistoryResult = falsificationData.recordData; }); queryRecords: function() { if (!this.videoHistory.date) { return; } this.recordsLoading = true; let that = this; var startTime = this.videoHistory.date + " 00:00:00"; var endTime = this.videoHistory.date + " 23:59:59"; this.$axios({ method: 'get', url: '/api/record/' + this.deviceId + '/' + this.channelId + '?startTime=' + startTime + '&endTime=' + endTime }).then(function(res) { // 处理时间信息 that.videoHistory.searchHistoryResult = res.data.recordList; that.recordsLoading = false; }).catch(function(e) { // that.videoHistory.searchHistoryResult = falsificationData.recordData; }); }, playRecord: function (isBackLive, rowData) { let that = this; if (isBackLive) { this.videoUrl = this.getVideoUrlBySsrc(this.normalssrc); return; } this.$axios({ method: 'get', url: '/api/playback/' + this.deviceId + '/' + this.channelId + '?startTime=' + rowData.startTime + '&endTime=' + rowData.endTime }).then(function (res) { let ssrc = res.data.ssrc; that.videoUrl = that.getVideoUrlBySsrc(ssrc); //that.videoUrl='http://hls.cntv.kcdnvip.com/asp/hls/main/0303000a/3/default/f466089412c04a759c5515dbfcc3ac3d/main.m3u8?maxbr=2048'; }); }, ptzCamera: function (leftRight, upDown, zoom) { console.log('云台控制:' + leftRight + ' : ' + upDown + " : " + zoom); let that = this; this.$axios({ method: 'post', url: '/api/ptz/' + this.deviceId + '/' + this.channelId + '?leftRight=' + leftRight + '&upDown=' + upDown + '&inOut=' + zoom + '&moveSpeed=50&zoomSpeed=50' }).then(function (res) {}); }, //////////////////////播放器事件处理////////////////////////// videoError: function (e) { console.log("播放器错误:" + JSON.stringify(e)); }, onTimeChange: function (video) { // this.queryRecords() }, playRecord: function(row) { let that = this; if (that.ssrc != "") { that.stopPlayRecord(function (){ that.ssrc = "", that.playRecord(row); }) }else { this.$axios({ method: 'get', url: '/api/playback/' + this.deviceId + '/' + this.channelId + '?startTime=' + row.startTime + '&endTime=' + row.endTime }).then(function(res) { var streamInfo = res.data; that.ssrc = streamInfo.ssrc; that.videoUrl = streamInfo.ws_flv; }); } } }; }, stopPlayRecord: function (callback) { this.$refs.videoPlayer.pause(); this.videoUrl = ''; this.$axios({ method: 'get', url: '/api/playback/' + this.ssrc + '/stop' }).then(function(res) { if (callback) callback() }); }, ptzCamera: function(leftRight, upDown, zoom) { console.log('云台控制:' + leftRight + ' : ' + upDown + " : " + zoom); let that = this; this.$axios({ method: 'post', url: '/api/ptz/' + this.deviceId + '/' + this.channelId + '?leftRight=' + leftRight + '&upDown=' + upDown + '&inOut=' + zoom + '&moveSpeed=50&zoomSpeed=50' }).then(function(res) {}); }, //////////////////////播放器事件处理////////////////////////// videoError:function(e){ console.log("播放器错误:"+JSON.stringify(e)); }, formatTooltip:function(val) { var h = parseInt(val/60); var hStr = h< 10 ? ("0" + h):h; var s = val%60; var sStr = s< 10 ? ("0" + s):s; return h + ":" + sStr; }, timeFormatter: function (row, column, cellValue, index) { return cellValue.split( " ")[1]; }, mergeTime: function (timeArray) { var resultArray = []; for (let i = 0; i < timeArray.length; i++) { var startTime = new Date(timeArray[i].startTime); var endTime = new Date(timeArray[i].endTime); if (i ==0) { resultArray[0] = { startTime: startTime, endTime: endTime } } for (let j = 0; j < resultArray.length; j++) { if (startTime > resultArray[j].endTime ) { // 合并 if (startTime - resultArray[j].endTime <= 1000){ resultArray[j].endTime = endTime; }else { resultArray[resultArray.length] = { startTime: startTime, endTime: endTime } } }else if(resultArray[j].startTime > endTime ) { // 合并 if (resultArray[j].startTime - endTime <= 1000) { resultArray[j].startTime = startTime; }else { resultArray[resultArray.length] = { startTime: startTime, endTime: endTime } } } } } console.log(resultArray) return resultArray; } } }; </script> <style> .control-wrapper { position: relative; width: 6.25rem; height: 6.25rem; max-width: 6.25rem; max-height: 6.25rem; margin: 0 auto; border-radius: 100%; float: left; } .control-wrapper { position: relative; width: 6.25rem; height: 6.25rem; max-width: 6.25rem; max-height: 6.25rem; margin: 0 auto; border-radius: 100%; float: left; } .control-btn { display: flex; justify-content: center; position: absolute; width: 44%; height: 44%; border-radius: 5px; border: 1px solid #78aee4; box-sizing: border-box; transition: all 0.3s linear; } .control-btn { display: flex; justify-content: center; position: absolute; width: 44%; height: 44%; border-radius: 5px; border: 1px solid #78aee4; box-sizing: border-box; transition: all 0.3s linear; } .control-btn i { font-size: 20px; color: #78aee4; display: flex; justify-content: center; align-items: center; } .control-btn i { font-size: 20px; color: #78aee4; display: flex; justify-content: center; align-items: center; } .control-round { position: absolute; top: 21%; left: 21%; width: 58%; height: 58%; background: #fff; border-radius: 100%; } .control-round { position: absolute; top: 21%; left: 21%; width: 58%; height: 58%; background: #fff; border-radius: 100%; } .control-round-inner { position: absolute; left: 15%; top: 15%; display: flex; justify-content: center; align-items: center; width: 70%; height: 70%; font-size: 40px; color: #78aee4; border: 1px solid #78aee4; border-radius: 100%; transition: all 0.3s linear; } .control-round-inner { position: absolute; left: 15%; top: 15%; display: flex; justify-content: center; align-items: center; width: 70%; height: 70%; font-size: 40px; color: #78aee4; border: 1px solid #78aee4; border-radius: 100%; transition: all 0.3s linear; } .control-inner-btn { position: absolute; width: 60%; height: 60%; background: #fafafa; } .control-inner-btn { position: absolute; width: 60%; height: 60%; background: #fafafa; } .control-top { top: -8%; left: 27%; transform: rotate(-45deg); border-radius: 5px 100% 5px 0; } .control-top { top: -8%; left: 27%; transform: rotate(-45deg); border-radius: 5px 100% 5px 0; } .control-top i { transform: rotate(45deg); border-radius: 5px 100% 5px 0; } .control-top i { transform: rotate(45deg); border-radius: 5px 100% 5px 0; } .control-top .control-inner { left: -1px; bottom: 0; border-top: 1px solid #78aee4; border-right: 1px solid #78aee4; border-radius: 0 100% 0 0; } .control-top .control-inner { left: -1px; bottom: 0; border-top: 1px solid #78aee4; border-right: 1px solid #78aee4; border-radius: 0 100% 0 0; } .control-top .fa { transform: rotate(45deg) translateY(-7px); } .control-top .fa { transform: rotate(45deg) translateY(-7px); } .control-left { top: 27%; left: -8%; transform: rotate(45deg); border-radius: 5px 0 5px 100%; } .control-left { top: 27%; left: -8%; transform: rotate(45deg); border-radius: 5px 0 5px 100%; } .control-left i { transform: rotate(-45deg); } .control-left i { transform: rotate(-45deg); } .control-left .control-inner { right: -1px; top: -1px; border-bottom: 1px solid #78aee4; border-left: 1px solid #78aee4; border-radius: 0 0 0 100%; } .control-left .control-inner { right: -1px; top: -1px; border-bottom: 1px solid #78aee4; border-left: 1px solid #78aee4; border-radius: 0 0 0 100%; } .control-left .fa { transform: rotate(-45deg) translateX(-7px); } .control-left .fa { transform: rotate(-45deg) translateX(-7px); } .control-right { top: 27%; right: -8%; transform: rotate(45deg); border-radius: 5px 100% 5px 0; } .control-right { top: 27%; right: -8%; transform: rotate(45deg); border-radius: 5px 100% 5px 0; } .control-right i { transform: rotate(-45deg); } .control-right i { transform: rotate(-45deg); } .control-right .control-inner { left: -1px; bottom: -1px; border-top: 1px solid #78aee4; border-right: 1px solid #78aee4; border-radius: 0 100% 0 0; } .control-right .control-inner { left: -1px; bottom: -1px; border-top: 1px solid #78aee4; border-right: 1px solid #78aee4; border-radius: 0 100% 0 0; } .control-right .fa { transform: rotate(-45deg) translateX(7px); } .control-right .fa { transform: rotate(-45deg) translateX(7px); } .control-bottom { left: 27%; bottom: -8%; transform: rotate(45deg); border-radius: 0 5px 100% 5px; } .control-bottom { left: 27%; bottom: -8%; transform: rotate(45deg); border-radius: 0 5px 100% 5px; } .control-bottom i { transform: rotate(-45deg); } .control-bottom i { transform: rotate(-45deg); } .control-bottom .control-inner { top: -1px; left: -1px; border-bottom: 1px solid #78aee4; border-right: 1px solid #78aee4; border-radius: 0 0 100% 0; } .control-bottom .control-inner { top: -1px; left: -1px; border-bottom: 1px solid #78aee4; border-right: 1px solid #78aee4; border-radius: 0 0 100% 0; } .control-bottom .fa { transform: rotate(-45deg) translateY(7px); } .control-bottom .fa { transform: rotate(-45deg) translateY(7px); } </style> web_src/src/main.js
@@ -6,8 +6,8 @@ import router from './router/index.js'; import axios from 'axios'; import VueCookies from 'vue-cookies'; import echarts from 'echarts'; import VueClipboard from 'vue-clipboard2' Vue.use(VueClipboard) Vue.use(ElementUI);