Commit e48fa711a3664bece9b3e58840a75fe7c05bc47c
1 parent
bd570d16
添加截图(快照)功能
Showing
7 changed files
with
208 additions
and
23 deletions
src/main/java/com/genersoft/iot/vmp/media/zlm/ZLMRESTfulUtils.java
| ... | ... | @@ -12,7 +12,7 @@ import org.springframework.beans.factory.annotation.Autowired; |
| 12 | 12 | import org.springframework.beans.factory.annotation.Value; |
| 13 | 13 | import org.springframework.stereotype.Component; |
| 14 | 14 | |
| 15 | -import java.io.IOException; | |
| 15 | +import java.io.*; | |
| 16 | 16 | import java.net.ConnectException; |
| 17 | 17 | import java.util.HashMap; |
| 18 | 18 | import java.util.Map; |
| ... | ... | @@ -26,6 +26,8 @@ public class ZLMRESTfulUtils { |
| 26 | 26 | @Autowired |
| 27 | 27 | private MediaConfig mediaConfig; |
| 28 | 28 | |
| 29 | + | |
| 30 | + | |
| 29 | 31 | public interface RequestCallback{ |
| 30 | 32 | void run(JSONObject response); |
| 31 | 33 | } |
| ... | ... | @@ -95,6 +97,53 @@ public class ZLMRESTfulUtils { |
| 95 | 97 | return responseJSON; |
| 96 | 98 | } |
| 97 | 99 | |
| 100 | + | |
| 101 | + public void sendPostForImg(String api, Map<String, Object> param, String targetPath, String fileName) { | |
| 102 | + OkHttpClient client = new OkHttpClient(); | |
| 103 | + String url = String.format("http://%s:%s/index/api/%s", mediaConfig.getIp(), mediaConfig.getHttpPort(), api); | |
| 104 | + JSONObject responseJSON = null; | |
| 105 | + logger.debug(url); | |
| 106 | + | |
| 107 | + FormBody.Builder builder = new FormBody.Builder(); | |
| 108 | + builder.add("secret",mediaConfig.getSecret()); | |
| 109 | + if (param != null && param.keySet().size() > 0) { | |
| 110 | + for (String key : param.keySet()){ | |
| 111 | + if (param.get(key) != null) { | |
| 112 | + builder.add(key, param.get(key).toString()); | |
| 113 | + } | |
| 114 | + } | |
| 115 | + } | |
| 116 | + | |
| 117 | + FormBody body = builder.build(); | |
| 118 | + | |
| 119 | + Request request = new Request.Builder() | |
| 120 | + .post(body) | |
| 121 | + .url(url) | |
| 122 | + .build(); | |
| 123 | + try { | |
| 124 | + Response response = client.newCall(request).execute(); | |
| 125 | + if (response.isSuccessful()) { | |
| 126 | + if (targetPath != null) { | |
| 127 | + File snapFolder = new File(targetPath); | |
| 128 | + if (!snapFolder.exists()) { | |
| 129 | + snapFolder.mkdirs(); | |
| 130 | + } | |
| 131 | + File snapFile = new File(targetPath + "/" + fileName); | |
| 132 | + FileOutputStream outStream = new FileOutputStream(snapFile); | |
| 133 | + outStream.write(response.body().bytes()); | |
| 134 | + outStream.close(); | |
| 135 | + } | |
| 136 | + } | |
| 137 | + } catch (ConnectException e) { | |
| 138 | + logger.error(String.format("连接ZLM失败: %s, %s", e.getCause().getMessage(), e.getMessage())); | |
| 139 | + logger.info("请检查media配置并确认ZLM已启动..."); | |
| 140 | + }catch (IOException e) { | |
| 141 | + logger.error(String.format("[ %s ]请求失败: %s", url, e.getMessage())); | |
| 142 | + } | |
| 143 | + | |
| 144 | + } | |
| 145 | + | |
| 146 | + | |
| 98 | 147 | public JSONObject getMediaList(String app, String stream, String schema, RequestCallback callback){ |
| 99 | 148 | Map<String, Object> param = new HashMap<>(); |
| 100 | 149 | if (app != null) param.put("app",app); |
| ... | ... | @@ -201,4 +250,12 @@ public class ZLMRESTfulUtils { |
| 201 | 250 | param.put("local_port", localPortSStr); |
| 202 | 251 | sendPost("kick_sessions",param, null); |
| 203 | 252 | } |
| 253 | + | |
| 254 | + public void getSnap(String flvUrl, int timeout_sec, int expire_sec, String targetPath, String fileName) { | |
| 255 | + Map<String, Object> param = new HashMap<>(); | |
| 256 | + param.put("url", flvUrl); | |
| 257 | + param.put("timeout_sec", timeout_sec); | |
| 258 | + param.put("expire_sec", expire_sec); | |
| 259 | + sendPostForImg("getSnap",param, targetPath, fileName); | |
| 260 | + } | |
| 204 | 261 | } | ... | ... |
src/main/java/com/genersoft/iot/vmp/service/impl/PlayServiceImpl.java
| ... | ... | @@ -15,6 +15,7 @@ import com.genersoft.iot.vmp.media.zlm.ZLMHttpHookSubscribe; |
| 15 | 15 | import com.genersoft.iot.vmp.media.zlm.ZLMRESTfulUtils; |
| 16 | 16 | import com.genersoft.iot.vmp.storager.IRedisCatchStorage; |
| 17 | 17 | import com.genersoft.iot.vmp.storager.IVideoManagerStorager; |
| 18 | +import com.genersoft.iot.vmp.vmanager.bean.WVPResult; | |
| 18 | 19 | import com.genersoft.iot.vmp.vmanager.gb28181.play.bean.PlayResult; |
| 19 | 20 | import com.genersoft.iot.vmp.service.IMediaService; |
| 20 | 21 | import com.genersoft.iot.vmp.service.IPlayService; |
| ... | ... | @@ -23,14 +24,18 @@ import org.slf4j.Logger; |
| 23 | 24 | import org.slf4j.LoggerFactory; |
| 24 | 25 | import org.springframework.beans.factory.annotation.Autowired; |
| 25 | 26 | import org.springframework.beans.factory.annotation.Value; |
| 27 | +import org.springframework.http.HttpStatus; | |
| 26 | 28 | import org.springframework.http.ResponseEntity; |
| 27 | 29 | import org.springframework.stereotype.Service; |
| 30 | +import org.springframework.util.ResourceUtils; | |
| 28 | 31 | import org.springframework.web.context.request.async.DeferredResult; |
| 29 | 32 | |
| 30 | 33 | import javax.sip.ClientTransaction; |
| 31 | 34 | import javax.sip.Dialog; |
| 32 | 35 | import javax.sip.header.CallIdHeader; |
| 33 | 36 | import javax.sip.message.Response; |
| 37 | +import java.io.File; | |
| 38 | +import java.io.FileNotFoundException; | |
| 34 | 39 | import java.util.UUID; |
| 35 | 40 | |
| 36 | 41 | @Service |
| ... | ... | @@ -82,9 +87,33 @@ public class PlayServiceImpl implements IPlayService { |
| 82 | 87 | cmder.closeRTPServer(playResult.getDevice(), channelId); |
| 83 | 88 | RequestMessage msg = new RequestMessage(); |
| 84 | 89 | msg.setId(DeferredResultHolder.CALLBACK_CMD_PlAY + playResult.getUuid()); |
| 85 | - msg.setData("Timeout"); | |
| 90 | + WVPResult wvpResult = new WVPResult(); | |
| 91 | + wvpResult.setCode(-1); | |
| 92 | + wvpResult.setMsg("Timeout"); | |
| 93 | + msg.setData(wvpResult); | |
| 86 | 94 | resultHolder.invokeResult(msg); |
| 87 | 95 | }); |
| 96 | + result.onCompletion(()->{ | |
| 97 | + // 点播结束时调用截图接口 | |
| 98 | + try { | |
| 99 | + String path = ResourceUtils.getURL("classpath:").getPath()+"static/static/snap/"; | |
| 100 | + String fileName = deviceId + "_" + channelId + ".jpg"; | |
| 101 | + ResponseEntity responseEntity = (ResponseEntity)result.getResult(); | |
| 102 | + if (responseEntity != null && responseEntity.getStatusCode() == HttpStatus.OK) { | |
| 103 | + WVPResult wvpResult = (WVPResult)responseEntity.getBody(); | |
| 104 | + if (wvpResult.getCode() == 0) { | |
| 105 | + StreamInfo streamInfoForSuccess = (StreamInfo)wvpResult.getData(); | |
| 106 | + String flvUrl = streamInfoForSuccess.getFlv(); | |
| 107 | + // 请求截图 | |
| 108 | + zlmresTfulUtils.getSnap(flvUrl, 5, 1, path, fileName); | |
| 109 | + } | |
| 110 | + } | |
| 111 | + | |
| 112 | + System.out.println(path); | |
| 113 | + } catch (FileNotFoundException e) { | |
| 114 | + e.printStackTrace(); | |
| 115 | + } | |
| 116 | + }); | |
| 88 | 117 | if (streamInfo == null) { |
| 89 | 118 | // 发送点播消息 |
| 90 | 119 | cmder.playStreamCmd(device, channelId, (JSONObject response) -> { |
| ... | ... | @@ -98,7 +127,10 @@ public class PlayServiceImpl implements IPlayService { |
| 98 | 127 | msg.setId(DeferredResultHolder.CALLBACK_CMD_PlAY + uuid); |
| 99 | 128 | Response response = event.getResponse(); |
| 100 | 129 | cmder.closeRTPServer(playResult.getDevice(), channelId); |
| 101 | - msg.setData(String.format("点播失败, 错误码: %s, %s", response.getStatusCode(), response.getReasonPhrase())); | |
| 130 | + WVPResult wvpResult = new WVPResult(); | |
| 131 | + wvpResult.setCode(-1); | |
| 132 | + wvpResult.setMsg(String.format("点播失败, 错误码: %s, %s", response.getStatusCode(), response.getReasonPhrase())); | |
| 133 | + msg.setData(wvpResult); | |
| 102 | 134 | resultHolder.invokeResult(msg); |
| 103 | 135 | if (errorEvent != null) { |
| 104 | 136 | errorEvent.response(event); |
| ... | ... | @@ -109,7 +141,10 @@ public class PlayServiceImpl implements IPlayService { |
| 109 | 141 | if (streamId == null) { |
| 110 | 142 | RequestMessage msg = new RequestMessage(); |
| 111 | 143 | msg.setId(DeferredResultHolder.CALLBACK_CMD_PlAY + uuid); |
| 112 | - msg.setData(String.format("点播失败, redis缓存streamId等于null")); | |
| 144 | + WVPResult wvpResult = new WVPResult(); | |
| 145 | + wvpResult.setCode(-1); | |
| 146 | + wvpResult.setMsg(String.format("点播失败, redis缓存streamId等于null")); | |
| 147 | + msg.setData(wvpResult); | |
| 113 | 148 | resultHolder.invokeResult(msg); |
| 114 | 149 | return playResult; |
| 115 | 150 | } |
| ... | ... | @@ -117,7 +152,13 @@ public class PlayServiceImpl implements IPlayService { |
| 117 | 152 | if (rtpInfo != null && rtpInfo.getBoolean("exist")) { |
| 118 | 153 | RequestMessage msg = new RequestMessage(); |
| 119 | 154 | msg.setId(DeferredResultHolder.CALLBACK_CMD_PlAY + uuid); |
| 120 | - msg.setData(JSON.toJSONString(streamInfo)); | |
| 155 | + | |
| 156 | + WVPResult wvpResult = new WVPResult(); | |
| 157 | + wvpResult.setCode(0); | |
| 158 | + wvpResult.setMsg("success"); | |
| 159 | + wvpResult.setData(streamInfo); | |
| 160 | + msg.setData(wvpResult); | |
| 161 | + | |
| 121 | 162 | resultHolder.invokeResult(msg); |
| 122 | 163 | if (hookEvent != null) { |
| 123 | 164 | hookEvent.response(JSONObject.parseObject(JSON.toJSONString(streamInfo))); |
| ... | ... | @@ -133,7 +174,11 @@ public class PlayServiceImpl implements IPlayService { |
| 133 | 174 | RequestMessage msg = new RequestMessage(); |
| 134 | 175 | msg.setId(DeferredResultHolder.CALLBACK_CMD_PlAY + uuid); |
| 135 | 176 | Response response = event.getResponse(); |
| 136 | - msg.setData(String.format("点播失败, 错误码: %s, %s", response.getStatusCode(), response.getReasonPhrase())); | |
| 177 | + | |
| 178 | + WVPResult wvpResult = new WVPResult(); | |
| 179 | + wvpResult.setCode(-1); | |
| 180 | + wvpResult.setMsg(String.format("点播失败, 错误码: %s, %s", response.getStatusCode(), response.getReasonPhrase())); | |
| 181 | + msg.setData(wvpResult); | |
| 137 | 182 | resultHolder.invokeResult(msg); |
| 138 | 183 | }); |
| 139 | 184 | } |
| ... | ... | @@ -163,6 +208,13 @@ public class PlayServiceImpl implements IPlayService { |
| 163 | 208 | streamInfo.setTransactionInfo(transactionInfo); |
| 164 | 209 | redisCatchStorage.startPlay(streamInfo); |
| 165 | 210 | msg.setData(JSON.toJSONString(streamInfo)); |
| 211 | + | |
| 212 | + WVPResult wvpResult = new WVPResult(); | |
| 213 | + wvpResult.setCode(0); | |
| 214 | + wvpResult.setMsg("sucess"); | |
| 215 | + wvpResult.setData(streamInfo); | |
| 216 | + msg.setData(wvpResult); | |
| 217 | + | |
| 166 | 218 | resultHolder.invokeResult(msg); |
| 167 | 219 | } else { |
| 168 | 220 | logger.warn("设备预览API调用失败!"); | ... | ... |
src/main/java/com/genersoft/iot/vmp/vmanager/gb28181/play/PlayController.java
| ... | ... | @@ -19,6 +19,7 @@ import org.slf4j.LoggerFactory; |
| 19 | 19 | import org.springframework.beans.factory.annotation.Autowired; |
| 20 | 20 | import org.springframework.http.HttpStatus; |
| 21 | 21 | import org.springframework.http.ResponseEntity; |
| 22 | +import org.springframework.util.ResourceUtils; | |
| 22 | 23 | import org.springframework.web.bind.annotation.CrossOrigin; |
| 23 | 24 | import org.springframework.web.bind.annotation.GetMapping; |
| 24 | 25 | import org.springframework.web.bind.annotation.PathVariable; |
| ... | ... | @@ -31,6 +32,7 @@ import com.genersoft.iot.vmp.gb28181.transmit.cmd.impl.SIPCommander; |
| 31 | 32 | import com.genersoft.iot.vmp.storager.IVideoManagerStorager; |
| 32 | 33 | import org.springframework.web.context.request.async.DeferredResult; |
| 33 | 34 | |
| 35 | +import java.io.FileNotFoundException; | |
| 34 | 36 | import java.util.UUID; |
| 35 | 37 | |
| 36 | 38 | import javax.sip.message.Response; | ... | ... |
web_src/config/index.js
web_src/src/components/Login.vue
web_src/src/components/channelList.vue
| ... | ... | @@ -30,10 +30,28 @@ |
| 30 | 30 | <el-table ref="channelListTable" :data="deviceChannelList" :height="winHeight" border style="width: 100%"> |
| 31 | 31 | <el-table-column prop="channelId" label="通道编号" width="210"> |
| 32 | 32 | </el-table-column> |
| 33 | - <el-table-column prop="channelId" label="设备编号" width="210"> | |
| 33 | + <el-table-column prop="deviceId" label="设备编号" width="210"> | |
| 34 | 34 | </el-table-column> |
| 35 | 35 | <el-table-column prop="name" label="通道名称"> |
| 36 | 36 | </el-table-column> |
| 37 | + <el-table-column label="快照" width="80" align="center"> | |
| 38 | + <template slot-scope="scope"> | |
| 39 | + <img style="max-height: 3rem;max-width: 4rem;" | |
| 40 | + :id="scope.row.deviceId + '_' + scope.row.channelId" | |
| 41 | + :src="getSnap(scope.row)" | |
| 42 | + @error="getSnapErrorEvent($event.target.id)" | |
| 43 | + alt=""> | |
| 44 | +<!-- <el-image--> | |
| 45 | +<!-- :id="'snapImg_' + scope.row.deviceId + '_' + scope.row.channelId"--> | |
| 46 | +<!-- :src="getSnap(scope.row)"--> | |
| 47 | +<!-- @error="getSnapErrorEvent($event, scope.row)"--> | |
| 48 | +<!-- :fit="'contain'">--> | |
| 49 | +<!-- <div slot="error" class="image-slot">--> | |
| 50 | +<!-- <i class="el-icon-picture-outline"></i>--> | |
| 51 | +<!-- </div>--> | |
| 52 | +<!-- </el-image>--> | |
| 53 | + </template> | |
| 54 | + </el-table-column> | |
| 37 | 55 | <el-table-column prop="subCount" label="子节点数"> |
| 38 | 56 | </el-table-column> |
| 39 | 57 | <el-table-column label="开启音频" align="center"> |
| ... | ... | @@ -100,7 +118,8 @@ export default { |
| 100 | 118 | total: 0, |
| 101 | 119 | beforeUrl: "/deviceList", |
| 102 | 120 | isLoging: false, |
| 103 | - autoList: true | |
| 121 | + autoList: true, | |
| 122 | + loadSnap:{} | |
| 104 | 123 | }; |
| 105 | 124 | }, |
| 106 | 125 | |
| ... | ... | @@ -122,7 +141,6 @@ export default { |
| 122 | 141 | } else { |
| 123 | 142 | this.showSubchannels(); |
| 124 | 143 | } |
| 125 | - | |
| 126 | 144 | }, |
| 127 | 145 | initParam: function () { |
| 128 | 146 | this.deviceId = this.$route.params.deviceId; |
| ... | ... | @@ -174,8 +192,6 @@ export default { |
| 174 | 192 | }).catch(function (error) { |
| 175 | 193 | console.log(error); |
| 176 | 194 | }); |
| 177 | - | |
| 178 | - | |
| 179 | 195 | }, |
| 180 | 196 | |
| 181 | 197 | //通知设备上传媒体流 |
| ... | ... | @@ -190,18 +206,22 @@ export default { |
| 190 | 206 | method: 'get', |
| 191 | 207 | url: '/api/play/start/' + deviceId + '/' + channelId |
| 192 | 208 | }).then(function (res) { |
| 193 | - console.log(res.data) | |
| 194 | - let streamId = res.data.streamId; | |
| 195 | 209 | that.isLoging = false; |
| 196 | - if (!!streamId) { | |
| 197 | - // that.$refs.devicePlayer.play(res.data, deviceId, channelId, itemData.hasAudio); | |
| 198 | - that.$refs.devicePlayer.openDialog("media", deviceId, channelId, { | |
| 199 | - streamInfo: res.data, | |
| 200 | - hasAudio: itemData.hasAudio | |
| 201 | - }); | |
| 202 | - that.initData(); | |
| 203 | - } else { | |
| 204 | - that.$message.error(res.data); | |
| 210 | + if (res.data.code == 0) { | |
| 211 | + | |
| 212 | + setTimeout(()=>{ | |
| 213 | + console.log("下载截图") | |
| 214 | + let snapId = deviceId + "_" + channelId; | |
| 215 | + that.loadSnap[snapId] = 0; | |
| 216 | + that.getSnapErrorEvent(snapId) | |
| 217 | + },5000) | |
| 218 | + that.$refs.devicePlayer.openDialog("media", deviceId, channelId, { | |
| 219 | + streamInfo: res.data.data, | |
| 220 | + hasAudio: itemData.hasAudio | |
| 221 | + }); | |
| 222 | + that.initData(); | |
| 223 | + }else { | |
| 224 | + that.$message.error(res.data.msg); | |
| 205 | 225 | } |
| 206 | 226 | }).catch(function (e) {}); |
| 207 | 227 | }, |
| ... | ... | @@ -228,7 +248,24 @@ export default { |
| 228 | 248 | } |
| 229 | 249 | }); |
| 230 | 250 | }, |
| 251 | + getSnap: function (row){ | |
| 252 | + return '/static/snap/' + row.deviceId + '_' + row.channelId + '.jpg' | |
| 253 | + }, | |
| 254 | + getSnapErrorEvent: function (id){ | |
| 255 | + | |
| 256 | + if (typeof (this.loadSnap[id]) != "undefined") { | |
| 257 | + console.log("下载截图" + this.loadSnap[id]) | |
| 258 | + if (this.loadSnap[id] > 5) { | |
| 259 | + delete this.loadSnap[id]; | |
| 260 | + return; | |
| 261 | + } | |
| 262 | + setTimeout(()=>{ | |
| 263 | + this.loadSnap[id] ++ | |
| 264 | + document.getElementById(id).setAttribute("src", '/static/snap/' + id + '.jpg?' + new Date().getTime()) | |
| 265 | + },1000) | |
| 231 | 266 | |
| 267 | + } | |
| 268 | + }, | |
| 232 | 269 | showDevice: function () { |
| 233 | 270 | this.$router.push(this.beforeUrl).then(() => { |
| 234 | 271 | this.initParam(); | ... | ... |
web_src/src/components/test.vue
| ... | ... | @@ -24,6 +24,9 @@ |
| 24 | 24 | <div v-for="index of timeNode" class="timeQuery-label-cell" :style="'left:' + (100.0/timeNode*index).toFixed(4) + '%'"> |
| 25 | 25 | <div class="timeQuery-label-cell-label">{{24/timeNode * index}}</div> |
| 26 | 26 | </div> |
| 27 | + <ul> | |
| 28 | + <li v-for="item of allDataList" >{{!!item.name?item.name:item.dname}}</li> | |
| 29 | + </ul> | |
| 27 | 30 | </div> |
| 28 | 31 | </el-col> |
| 29 | 32 | </el-row> |
| ... | ... | @@ -36,6 +39,7 @@ export default { |
| 36 | 39 | name: "test", |
| 37 | 40 | data() { |
| 38 | 41 | return { |
| 42 | + allDataList:[], | |
| 39 | 43 | timeNode: 24, |
| 40 | 44 | recordData:[ |
| 41 | 45 | { |
| ... | ... | @@ -58,6 +62,32 @@ export default { |
| 58 | 62 | }; |
| 59 | 63 | }, |
| 60 | 64 | mounted() { |
| 65 | + var list1 = [{ | |
| 66 | + key: Math.random()*10, | |
| 67 | + name: "人1" | |
| 68 | + },{ | |
| 69 | + key: Math.random()*10, | |
| 70 | + name: "人2" | |
| 71 | + },{ | |
| 72 | + key: Math.random()*10, | |
| 73 | + name: "人3" | |
| 74 | + }] | |
| 75 | + var list2 = [{ | |
| 76 | + key: Math.random()*10, | |
| 77 | + dname: "部门1" | |
| 78 | + },{ | |
| 79 | + key: Math.random()*10, | |
| 80 | + dname: "部门2" | |
| 81 | + },{ | |
| 82 | + key: Math.random()*10, | |
| 83 | + dname: "部门3" | |
| 84 | + }] | |
| 85 | + | |
| 86 | + var allData = list1.concat(list2) | |
| 87 | + allData.sort((a, b)=>{ | |
| 88 | + return a.key-b.key; | |
| 89 | + }) | |
| 90 | + this.allDataList = allData; | |
| 61 | 91 | for (let i = 1; i <= 24; i++) { |
| 62 | 92 | console.log("<div class=\"timeQuery-label-cell\" style=\"left: " + (100.0/24*i).toFixed(4) + "%\"></div>") |
| 63 | 93 | } | ... | ... |