| | |
| | | taskExecutor.execute(() -> { |
| | | JSONObject json = (JSONObject) JSON.toJSON(param); |
| | | List<ZlmHttpHookSubscribe.Event> subscribes = this.subscribe.getSubscribes(HookType.on_rtp_server_timeout); |
| | | if (subscribes != null && subscribes.size() > 0) { |
| | | if (subscribes != null && !subscribes.isEmpty()) { |
| | | for (ZlmHttpHookSubscribe.Event subscribe : subscribes) { |
| | | subscribe.response(null, param); |
| | | } |
| | |
| | | logger.info("[ZLM HOOK] 录像完成事件:{}->{}", param.getMediaServerId(), param.getFile_path()); |
| | | |
| | | taskExecutor.execute(() -> { |
| | | List<ZlmHttpHookSubscribe.Event> subscribes = this.subscribe.getSubscribes(HookType.on_record_mp4); |
| | | if (subscribes != null && !subscribes.isEmpty()) { |
| | | for (ZlmHttpHookSubscribe.Event subscribe : subscribes) { |
| | | subscribe.response(null, param); |
| | | } |
| | | } |
| | | cloudRecordService.addRecord(param); |
| | | |
| | | }); |
| | | |
| | | return HookResult.SUCCESS(); |
| | |
| | | |
| | | void getSnap(String deviceId, String channelId, String fileName, ErrorCallback errorCallback); |
| | | |
| | | void getFilePath(String deviceId, String channelId, String stream, ErrorCallback<DownloadFileInfo> callback); |
| | | } |
| | |
| | | import java.math.BigDecimal; |
| | | import java.math.RoundingMode; |
| | | import java.text.ParseException; |
| | | import java.time.Instant; |
| | | import java.util.List; |
| | | import java.util.UUID; |
| | | import java.util.Vector; |
| | | import java.util.concurrent.TimeUnit; |
| | | |
| | | @SuppressWarnings(value = {"rawtypes", "unchecked"}) |
| | | @Service |
| | |
| | | // 处理收到200ok后的TCP主动连接以及SSRC不一致的问题 |
| | | InviteOKHandler(eventResult, ssrcInfo, mediaServerItem, device, channelId, |
| | | downLoadTimeOutTaskKey, callback, inviteInfo, InviteSessionType.DOWNLOAD); |
| | | |
| | | // 注册录像回调事件,录像下载结束后写入下载地址 |
| | | ZlmHttpHookSubscribe.Event hookEventForRecord = (mediaServerItemInuse, hookParam) -> { |
| | | logger.info("[录像下载] 收到录像写入磁盘消息: , {}/{}-{}", |
| | | inviteInfo.getDeviceId(), inviteInfo.getChannelId(), ssrcInfo.getStream()); |
| | | logger.info("[录像下载] 收到录像写入磁盘消息内容: " + hookParam); |
| | | OnRecordMp4HookParam recordMp4HookParam = (OnRecordMp4HookParam)hookParam; |
| | | String filePath = recordMp4HookParam.getFile_path(); |
| | | DownloadFileInfo downloadFileInfo = getDownloadFilePath(mediaServerItem, filePath); |
| | | InviteInfo inviteInfoForNew = inviteStreamService.getInviteInfo(inviteInfo.getType(), inviteInfo.getDeviceId() |
| | | , inviteInfo.getChannelId(), inviteInfo.getStream()); |
| | | inviteInfoForNew.getStreamInfo().setDownLoadFilePath(downloadFileInfo); |
| | | inviteStreamService.updateInviteInfo(inviteInfoForNew); |
| | | }; |
| | | HookSubscribeForRecordMp4 hookSubscribe = HookSubscribeFactory.on_record_mp4( |
| | | mediaServerItem.getId(), "rtp", ssrcInfo.getStream()); |
| | | |
| | | // 设置过期时间,下载失败时自动处理订阅数据 |
| | | // long difference = DateUtil.getDifference(startTime, endTime)/1000; |
| | | // Instant expiresInstant = Instant.now().plusSeconds(TimeUnit.MINUTES.toSeconds(difference * 2)); |
| | | // hookSubscribe.setExpires(expiresInstant); |
| | | subscribe.addSubscribe(hookSubscribe, hookEventForRecord); |
| | | }); |
| | | } catch (InvalidArgumentException | SipException | ParseException e) { |
| | | logger.error("[命令发送失败] 录像下载: {}", e.getMessage()); |
| | |
| | | BigDecimal totalCount = new BigDecimal((end - start) * 1000); |
| | | BigDecimal divide = currentCount.divide(totalCount, 2, RoundingMode.HALF_UP); |
| | | double process = divide.doubleValue(); |
| | | if (process > 0.999) { |
| | | process = 1.0; |
| | | } |
| | | inviteInfo.getStreamInfo().setProgress(process); |
| | | } |
| | | inviteStreamService.updateInviteInfo(inviteInfo); |
| | | return inviteInfo.getStreamInfo(); |
| | | } |
| | | |
| | | @Override |
| | | public void getFilePath(String deviceId, String channelId, String stream, ErrorCallback<DownloadFileInfo> callback) { |
| | | InviteInfo inviteInfo = inviteStreamService.getInviteInfo(InviteSessionType.DOWNLOAD, deviceId, channelId, stream); |
| | | if (inviteInfo == null || inviteInfo.getStreamInfo() == null) { |
| | | logger.warn("[获取录像下载文件地址] 未查询到录像下载的信息, {}/{}-{}", deviceId, channelId, stream); |
| | | callback.run(ErrorCode.ERROR100.getCode(), "未查询到录像下载的信息", null); |
| | | return ; |
| | | } |
| | | |
| | | if (!ObjectUtils.isEmpty(inviteInfo.getStreamInfo().getDownLoadFilePath())) { |
| | | callback.run(ErrorCode.SUCCESS.getCode(), ErrorCode.SUCCESS.getMsg(), |
| | | inviteInfo.getStreamInfo().getDownLoadFilePath()); |
| | | return; |
| | | } |
| | | |
| | | StreamAuthorityInfo streamAuthorityInfo = redisCatchStorage.getStreamAuthorityInfo("rtp", stream); |
| | | if (streamAuthorityInfo == null) { |
| | | logger.warn("[获取录像下载文件地址] 未查询到录像的视频信息, {}/{}-{}", deviceId, channelId, stream); |
| | | callback.run(ErrorCode.ERROR100.getCode(), "未查询到录像的视频信息", null); |
| | | return ; |
| | | } |
| | | |
| | | // 获取当前已下载时长 |
| | | String mediaServerId = inviteInfo.getStreamInfo().getMediaServerId(); |
| | | MediaServerItem mediaServerItem = mediaServerService.getOne(mediaServerId); |
| | | if (mediaServerItem == null) { |
| | | logger.warn("[获取录像下载文件地址] 查询录像信息时发现节点不存在, {}/{}-{}", deviceId, channelId, stream); |
| | | callback.run(ErrorCode.ERROR100.getCode(), "查询录像信息时发现节点不存在", null); |
| | | return ; |
| | | } |
| | | |
| | | List<CloudRecordItem> cloudRecordItemList = cloudRecordServiceMapper.getListByCallId(streamAuthorityInfo.getCallId()); |
| | | if (!cloudRecordItemList.isEmpty()) { |
| | | String filePath = cloudRecordItemList.get(0).getFilePath(); |
| | | |
| | | DownloadFileInfo downloadFileInfo = getDownloadFilePath(mediaServerItem, filePath); |
| | | inviteInfo.getStreamInfo().setDownLoadFilePath(downloadFileInfo); |
| | | inviteStreamService.updateInviteInfo(inviteInfo); |
| | | callback.run(ErrorCode.SUCCESS.getCode(), ErrorCode.SUCCESS.getMsg(), downloadFileInfo); |
| | | }else { |
| | | // 可能尚未生成,那就监听hook等着收到对应的录像通知 |
| | | ZlmHttpHookSubscribe.Event hookEvent = (mediaServerItemInuse, hookParam) -> { |
| | | logger.info("[录像下载]收到订阅消息: , {}/{}-{}", deviceId, channelId, stream); |
| | | logger.info("[录像下载]收到订阅消息内容: " + hookParam); |
| | | dynamicTask.stop(streamAuthorityInfo.getCallId()); |
| | | OnRecordMp4HookParam recordMp4HookParam = (OnRecordMp4HookParam)hookParam; |
| | | String filePath = recordMp4HookParam.getFile_path(); |
| | | DownloadFileInfo downloadFileInfo = getDownloadFilePath(mediaServerItem, filePath); |
| | | inviteInfo.getStreamInfo().setDownLoadFilePath(downloadFileInfo); |
| | | inviteStreamService.updateInviteInfo(inviteInfo); |
| | | callback.run(ErrorCode.SUCCESS.getCode(), ErrorCode.SUCCESS.getMsg(), downloadFileInfo); |
| | | }; |
| | | HookSubscribeForRecordMp4 hookSubscribe = HookSubscribeFactory.on_record_mp4(mediaServerId, "rtp", stream); |
| | | subscribe.addSubscribe(hookSubscribe, hookEvent); |
| | | |
| | | // 设置超时,超时结束监听 |
| | | dynamicTask.startDelay(streamAuthorityInfo.getCallId(), ()->{ |
| | | logger.info("[录像下载] 接收hook超时, {}/{}-{}", deviceId, channelId, stream); |
| | | subscribe.removeSubscribe(hookSubscribe); |
| | | callback.run(ErrorCode.ERROR100.getCode(), "接收hook超时", null); |
| | | }, 10000); |
| | | } |
| | | } |
| | | |
| | | private DownloadFileInfo getDownloadFilePath(MediaServerItem mediaServerItem, String filePath) { |
| | |
| | | package com.genersoft.iot.vmp.vmanager.bean; |
| | | |
| | | import com.genersoft.iot.vmp.common.StreamInfo; |
| | | import com.genersoft.iot.vmp.service.bean.DownloadFileInfo; |
| | | import io.swagger.v3.oas.annotations.media.Schema; |
| | | |
| | | @Schema(description = "流信息") |
| | |
| | | @Schema(description = "结束时间") |
| | | private String endTime; |
| | | |
| | | @Schema(description = "文件下载地址(录像下载使用)") |
| | | private DownloadFileInfo downLoadFilePath; |
| | | |
| | | private double progress; |
| | | |
| | | public StreamContent(StreamInfo streamInfo) { |
| | |
| | | this.startTime = streamInfo.getStartTime(); |
| | | this.endTime = streamInfo.getEndTime(); |
| | | this.progress = streamInfo.getProgress(); |
| | | |
| | | if (streamInfo.getDownLoadFilePath() != null) { |
| | | this.downLoadFilePath = streamInfo.getDownLoadFilePath(); |
| | | } |
| | | } |
| | | |
| | | public String getApp() { |
| | |
| | | public void setProgress(double progress) { |
| | | this.progress = progress; |
| | | } |
| | | |
| | | public DownloadFileInfo getDownLoadFilePath() { |
| | | return downLoadFilePath; |
| | | } |
| | | |
| | | public void setDownLoadFilePath(DownloadFileInfo downLoadFilePath) { |
| | | this.downLoadFilePath = downLoadFilePath; |
| | | } |
| | | } |
| | |
| | | } |
| | | return new StreamContent(downLoadInfo); |
| | | } |
| | | |
| | | @Operation(summary = "获取历史媒体下载文件地址") |
| | | @Parameter(name = "deviceId", description = "设备国标编号", required = true) |
| | | @Parameter(name = "channelId", description = "通道国标编号", required = true) |
| | | @Parameter(name = "stream", description = "流ID", required = true) |
| | | @GetMapping("/download/file/path/{deviceId}/{channelId}/{stream}") |
| | | public DeferredResult<WVPResult<DownloadFileInfo>> getDownloadFilePath(@PathVariable String deviceId, @PathVariable String channelId, @PathVariable String stream) { |
| | | |
| | | DeferredResult<WVPResult<DownloadFileInfo>> result = new DeferredResult<>(); |
| | | |
| | | result.onTimeout(()->{ |
| | | WVPResult<DownloadFileInfo> wvpResult = new WVPResult<>(); |
| | | wvpResult.setCode(ErrorCode.ERROR100.getCode()); |
| | | wvpResult.setMsg("timeout"); |
| | | result.setResult(wvpResult); |
| | | }); |
| | | |
| | | playService.getFilePath(deviceId, channelId, stream, (code, msg, data)->{ |
| | | WVPResult<DownloadFileInfo> wvpResult = new WVPResult<>(); |
| | | wvpResult.setCode(code); |
| | | wvpResult.setMsg(msg); |
| | | wvpResult.setData(data); |
| | | result.setResult(wvpResult); |
| | | }); |
| | | |
| | | |
| | | return result; |
| | | } |
| | | } |
| | |
| | | const FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin') |
| | | const portfinder = require('portfinder') |
| | | |
| | | const HOST = process.env.HOST |
| | | const PORT = process.env.PORT && Number(process.env.PORT) |
| | | |
| | | const devWebpackConfig = merge(baseWebpackConfig, { |
| | |
| | | hot: true, |
| | | contentBase: false, // since we use CopyWebpackPlugin. |
| | | compress: true, |
| | | host: HOST || config.dev.host, |
| | | // host:'127.0.0.1', |
| | | port: PORT || config.dev.port, |
| | | host: config.dev.host, |
| | | port: config.dev.port, |
| | | open: config.dev.autoOpenBrowser, |
| | | overlay: config.dev.errorOverlay |
| | | ? { warnings: false, errors: true } |
| | |
| | | streamInfo: null, |
| | | taskId: null, |
| | | getProgressRun: false, |
| | | getProgressForFileRun: false, |
| | | timer: null, |
| | | downloadFile: null, |
| | | |
| | |
| | | return; |
| | | } |
| | | if (this.percentage == 100 ) { |
| | | this.getFileDownload(); |
| | | |
| | | return; |
| | | } |
| | | setTimeout( ()=>{ |
| | |
| | | method: 'get', |
| | | url: `/api/gb_record/download/progress/${this.deviceId}/${this.channelId}/${this.stream}` |
| | | }).then((res)=> { |
| | | console.log(res) |
| | | if (res.data.code === 0) { |
| | | this.streamInfo = res.data.data; |
| | | if (parseFloat(res.data.progress) == 1) { |
| | | this.percentage = 100; |
| | | }else { |
| | | this.percentage = (parseFloat(res.data.data.progress)*100).toFixed(1); |
| | | } |
| | | if (this.streamInfo.downLoadFilePath) { |
| | | if (location.protocol === "https:") { |
| | | this.downloadFile = this.streamInfo.downLoadFilePath.httpsPath; |
| | | }else { |
| | | this.downloadFile = this.streamInfo.downLoadFilePath.httpPath; |
| | | } |
| | | this.getProgressRun = false; |
| | | this.downloadFileClientEvent() |
| | | } |
| | | if (callback)callback(); |
| | | }else { |
| | |
| | | } |
| | | this.showDialog=false; |
| | | this.getProgressRun = false; |
| | | this.getProgressForFileRun = false; |
| | | }, |
| | | gbScale: function (scale){ |
| | | this.scale = scale; |
| | | }, |
| | | download: function (){ |
| | | this.getProgressRun = false; |
| | | if (this.streamInfo != null ) { |
| | | if (this.streamInfo.progress < 1) { |
| | | // 发送停止缓存 |
| | | this.stopDownloadRecord((res)=>{ |
| | | this.getFileDownload() |
| | | }) |
| | | }else { |
| | | this.getFileDownload() |
| | | } |
| | | } |
| | | }, |
| | | |
| | | stopDownloadRecord: function (callback) { |
| | | this.$axios({ |
| | | method: 'get', |
| | |
| | | if (callback) callback(res) |
| | | }); |
| | | }, |
| | | getFileDownload: function (){ |
| | | this.$axios({ |
| | | method: 'get', |
| | | url:`/api/cloud/record/task/add`, |
| | | params: { |
| | | app: this.app, |
| | | stream: this.stream, |
| | | mediaServerId: this.mediaServerId, |
| | | startTime: null, |
| | | endTime: null, |
| | | } |
| | | }).then((res) =>{ |
| | | if (res.data.code === 0 ) { |
| | | // 查询进度 |
| | | this.title = "录像文件处理中..." |
| | | this.taskId = res.data.data; |
| | | this.percentage = 0.0; |
| | | this.getProgressForFileRun = true; |
| | | this.getProgressForFileTimer(); |
| | | } |
| | | }).catch(function (error) { |
| | | console.log(error); |
| | | }); |
| | | }, |
| | | getProgressForFileTimer: function (){ |
| | | if (!this.getProgressForFileRun || this.percentage == 100) { |
| | | return; |
| | | } |
| | | setTimeout( ()=>{ |
| | | if (!this.showDialog) return; |
| | | this.getProgressForFile(this.getProgressForFileTimer) |
| | | }, 1000) |
| | | }, |
| | | getProgressForFile: function (callback){ |
| | | this.$axios({ |
| | | method: 'get', |
| | | url:`/api/cloud/record/task/list`, |
| | | params: { |
| | | mediaServerId: this.mediaServerId, |
| | | taskId: this.taskId, |
| | | isEnd: true, |
| | | } |
| | | }).then((res) => { |
| | | console.log(res) |
| | | if (res.data.code === 0) { |
| | | if (res.data.data.length === 0){ |
| | | this.percentage = 0 |
| | | // 往往在多次请求后(实验五分钟的视频是三次请求),才会返回数据,第一次请求通常是返回空数组 |
| | | if (callback)callback() |
| | | return |
| | | } |
| | | // res.data.data应是数组类型 |
| | | this.percentage = parseFloat(res.data.data[0].percentage)*100 |
| | | if (res.data.data[0].percentage === '1') { |
| | | this.getProgressForFileRun = false; |
| | | this.downloadFile = res.data.data[0].downloadFile |
| | | this.title = "文件处理完成,点击按扭下载" |
| | | // window.open(res.data.data[0].downloadFile) |
| | | }else { |
| | | if (callback)callback() |
| | | } |
| | | } |
| | | }).catch(function (error) { |
| | | console.log(error); |
| | | }); |
| | | }, |
| | | downloadFileClientEvent: function (){ |
| | | window.open(this.downloadFile ) |
| | | // window.open(this.downloadFile ) |
| | | |
| | | let x = new XMLHttpRequest(); |
| | | x.open("GET", this.downloadFile, true); |
| | | x.responseType = 'blob'; |
| | | x.onload=(e)=> { |
| | | let url = window.URL.createObjectURL(x.response) |
| | | let a = document.createElement('a'); |
| | | a.href = url |
| | | a.download = this.deviceId + "-" + this.channelId + ".mp4"; |
| | | a.click() |
| | | } |
| | | x.send(); |
| | | } |
| | | }, |
| | | destroyed() { |