648540858
2023-12-05 ad36354ef46a31f24b2583263f575d6736c0ad28
优化国标录像服务端,使用zlm新接口实现功能
12个文件已修改
2个文件已添加
330 ■■■■ 已修改文件
src/main/java/com/genersoft/iot/vmp/common/StreamInfo.java 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/genersoft/iot/vmp/media/zlm/ZLMHttpHookListener.java 14 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/genersoft/iot/vmp/media/zlm/dto/HookSubscribeFactory.java 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/genersoft/iot/vmp/media/zlm/dto/HookSubscribeForRecordMp4.java 44 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/genersoft/iot/vmp/media/zlm/dto/hook/HookResultForOnPublish.java 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/genersoft/iot/vmp/service/IPlayService.java 3 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/genersoft/iot/vmp/service/bean/DownloadFileInfo.java 41 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/genersoft/iot/vmp/service/impl/MediaServerServiceImpl.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/genersoft/iot/vmp/service/impl/MediaServiceImpl.java 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/genersoft/iot/vmp/service/impl/PlayServiceImpl.java 139 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/genersoft/iot/vmp/storager/dao/CloudRecordServiceMapper.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/genersoft/iot/vmp/utils/DateUtil.java 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/genersoft/iot/vmp/vmanager/gb28181/record/GBRecordController.java 36 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
web_src/src/components/dialog/recordDownload.vue 3 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/genersoft/iot/vmp/common/StreamInfo.java
@@ -1,5 +1,6 @@
package com.genersoft.iot.vmp.common;
import com.genersoft.iot.vmp.service.bean.DownloadFileInfo;
import io.swagger.v3.oas.annotations.media.Schema;
import java.io.Serializable;
@@ -76,6 +77,8 @@
    private String endTime;
    @Schema(description = "进度(录像下载使用)")
    private double progress;
    @Schema(description = "文件下载地址(录像下载使用)")
    private DownloadFileInfo downLoadFilePath;
    @Schema(description = "是否暂停(录像回放使用)")
    private boolean pause;
@@ -605,5 +608,11 @@
        this.subStream = subStream;
    }
    public DownloadFileInfo getDownLoadFilePath() {
        return downLoadFilePath;
    }
    public void setDownLoadFilePath(DownloadFileInfo downLoadFilePath) {
        this.downLoadFilePath = downLoadFilePath;
    }
}
src/main/java/com/genersoft/iot/vmp/media/zlm/ZLMHttpHookListener.java
@@ -275,7 +275,7 @@
        List<SsrcTransaction> ssrcTransactionForAll = sessionManager.getSsrcTransactionForAll(null, null, null, param.getStream());
        if (ssrcTransactionForAll != null && ssrcTransactionForAll.size() == 1) {
            // 为录制国标模拟一个鉴权信息
            // 为录制国标模拟一个鉴权信息, 方便后续写入录像文件时使用
            StreamAuthorityInfo streamAuthorityInfo = StreamAuthorityInfo.getInstanceByHook(param);
            streamAuthorityInfo.setApp(param.getApp());
            streamAuthorityInfo.setStream(ssrcTransactionForAll.get(0).getStream());
@@ -291,8 +291,18 @@
            }
            // 如果是录像下载就设置视频间隔十秒
            if (ssrcTransactionForAll.get(0).getType() == InviteSessionType.DOWNLOAD) {
                result.setMp4_max_second(30);
                // 获取录像的总时长,然后设置为这个视频的时长
                InviteInfo inviteInfo = inviteStreamService.getInviteInfo(InviteSessionType.DOWNLOAD, deviceId, channelId, param.getStream());
                if (inviteInfo.getStreamInfo() != null ) {
                    String startTime = inviteInfo.getStreamInfo().getStartTime();
                    String endTime = inviteInfo.getStreamInfo().getEndTime();
                    long difference = DateUtil.getDifference(startTime, endTime)/1000;
                    result.setMp4_max_second((int)difference);
                result.setEnable_mp4(true);
                    // 设置为2保证得到的mp4的时长是正常的
                    result.setModify_stamp(2);
                }
            }
        }
        if (param.getApp().equalsIgnoreCase("rtp")) {
src/main/java/com/genersoft/iot/vmp/media/zlm/dto/HookSubscribeFactory.java
@@ -41,4 +41,15 @@
        return hookSubscribe;
    }
    public static HookSubscribeForRecordMp4 on_record_mp4(String mediaServerId, String app, String stream) {
        HookSubscribeForRecordMp4 hookSubscribe = new HookSubscribeForRecordMp4();
        JSONObject subscribeKey = new com.alibaba.fastjson2.JSONObject();
        subscribeKey.put("app", app);
        subscribeKey.put("stream", stream);
        subscribeKey.put("mediaServerId", mediaServerId);
        hookSubscribe.setContent(subscribeKey);
        return hookSubscribe;
    }
}
src/main/java/com/genersoft/iot/vmp/media/zlm/dto/HookSubscribeForRecordMp4.java
New file
@@ -0,0 +1,44 @@
package com.genersoft.iot.vmp.media.zlm.dto;
import com.alibaba.fastjson2.JSONObject;
import com.alibaba.fastjson2.annotation.JSONField;
import java.time.Instant;
/**
 * hook订阅-录像完成
 * @author lin
 */
public class HookSubscribeForRecordMp4 implements IHookSubscribe{
    private HookType hookType = HookType.on_record_mp4;
    private JSONObject content;
    @JSONField(format="yyyy-MM-dd HH:mm:ss")
    private Instant expires;
    @Override
    public HookType getHookType() {
        return hookType;
    }
    @Override
    public JSONObject getContent() {
        return content;
    }
    public void setContent(JSONObject content) {
        this.content = content;
    }
    @Override
    public Instant getExpires() {
        return expires;
    }
    @Override
    public void setExpires(Instant expires) {
        this.expires = expires;
    }
}
src/main/java/com/genersoft/iot/vmp/media/zlm/dto/hook/HookResultForOnPublish.java
@@ -7,6 +7,7 @@
    private int mp4_max_second;
    private String mp4_save_path;
    private String stream_replace;
    private Integer modify_stamp;
    public HookResultForOnPublish() {
    }
@@ -60,14 +61,23 @@
        this.stream_replace = stream_replace;
    }
    public Integer getModify_stamp() {
        return modify_stamp;
    }
    public void setModify_stamp(Integer modify_stamp) {
        this.modify_stamp = modify_stamp;
    }
    @Override
    public String toString() {
        return "HookResultForOnPublish{" +
                "enable_audio=" + enable_audio +
                ", enable_mp4=" + enable_mp4 +
                ", mp4_max_second=" + mp4_max_second +
                ", stream_replace=" + stream_replace +
                ", mp4_save_path='" + mp4_save_path + '\'' +
                ", stream_replace='" + stream_replace + '\'' +
                ", modify_stamp='" + modify_stamp + '\'' +
                '}';
    }
}
src/main/java/com/genersoft/iot/vmp/service/IPlayService.java
@@ -4,6 +4,7 @@
import com.genersoft.iot.vmp.conf.exception.ServiceException;
import com.genersoft.iot.vmp.gb28181.bean.Device;
import com.genersoft.iot.vmp.media.zlm.dto.MediaServerItem;
import com.genersoft.iot.vmp.service.bean.DownloadFileInfo;
import com.genersoft.iot.vmp.service.bean.ErrorCallback;
import com.genersoft.iot.vmp.service.bean.SSRCInfo;
@@ -44,5 +45,5 @@
    void getSnap(String deviceId, String channelId, String fileName, ErrorCallback errorCallback);
    void getFilePath(String deviceId, String channelId, String stream, ErrorCallback<DownloadFileInfo> callback);
}
src/main/java/com/genersoft/iot/vmp/service/bean/DownloadFileInfo.java
New file
@@ -0,0 +1,41 @@
package com.genersoft.iot.vmp.service.bean;
public class DownloadFileInfo {
    private String httpPath;
    private String httpsPath;
    private String httpDomainPath;
    private String httpsDomainPath;
    public String getHttpPath() {
        return httpPath;
    }
    public void setHttpPath(String httpPath) {
        this.httpPath = httpPath;
    }
    public String getHttpsPath() {
        return httpsPath;
    }
    public void setHttpsPath(String httpsPath) {
        this.httpsPath = httpsPath;
    }
    public String getHttpDomainPath() {
        return httpDomainPath;
    }
    public void setHttpDomainPath(String httpDomainPath) {
        this.httpDomainPath = httpDomainPath;
    }
    public String getHttpsDomainPath() {
        return httpsDomainPath;
    }
    public void setHttpsDomainPath(String httpsDomainPath) {
        this.httpsDomainPath = httpsDomainPath;
    }
}
src/main/java/com/genersoft/iot/vmp/service/impl/MediaServerServiceImpl.java
@@ -592,6 +592,7 @@
        if (mediaServerItem.getRecordPath() != null) {
            File recordPathFile = new File(mediaServerItem.getRecordPath());
            param.put("protocol.mp4_save_path", recordPathFile.getParentFile().getPath());
            param.put("protocol.downloadRoot", recordPathFile.getParentFile().getPath());
            param.put("record.appName", recordPathFile.getName());
        }
src/main/java/com/genersoft/iot/vmp/service/impl/MediaServiceImpl.java
@@ -67,7 +67,7 @@
                if (data == null) {
                    return null;
                }
                JSONObject mediaJSON = JSON.parseObject(JSON.toJSONString(data.get(0)), JSONObject.class);
                JSONObject mediaJSON = data.getJSONObject(0);
                JSONArray tracks = mediaJSON.getJSONArray("tracks");
                if (authority) {
                    streamInfo = getStreamInfoByAppAndStream(mediaInfo, app, stream, tracks, addr, calld);
src/main/java/com/genersoft/iot/vmp/service/impl/PlayServiceImpl.java
@@ -1,5 +1,7 @@
package com.genersoft.iot.vmp.service.impl;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONArray;
import com.alibaba.fastjson2.JSONObject;
import com.genersoft.iot.vmp.common.InviteInfo;
import com.genersoft.iot.vmp.common.InviteSessionStatus;
@@ -21,16 +23,12 @@
import com.genersoft.iot.vmp.media.zlm.ZLMRESTfulUtils;
import com.genersoft.iot.vmp.media.zlm.ZLMServerFactory;
import com.genersoft.iot.vmp.media.zlm.ZlmHttpHookSubscribe;
import com.genersoft.iot.vmp.media.zlm.dto.HookSubscribeFactory;
import com.genersoft.iot.vmp.media.zlm.dto.HookSubscribeForStreamChange;
import com.genersoft.iot.vmp.media.zlm.dto.MediaServerItem;
import com.genersoft.iot.vmp.media.zlm.dto.*;
import com.genersoft.iot.vmp.media.zlm.dto.hook.HookParam;
import com.genersoft.iot.vmp.media.zlm.dto.hook.OnRecordMp4HookParam;
import com.genersoft.iot.vmp.media.zlm.dto.hook.OnStreamChangedHookParam;
import com.genersoft.iot.vmp.service.*;
import com.genersoft.iot.vmp.service.bean.CloudRecordItem;
import com.genersoft.iot.vmp.service.bean.ErrorCallback;
import com.genersoft.iot.vmp.service.bean.InviteErrorCode;
import com.genersoft.iot.vmp.service.bean.SSRCInfo;
import com.genersoft.iot.vmp.service.bean.*;
import com.genersoft.iot.vmp.storager.IRedisCatchStorage;
import com.genersoft.iot.vmp.storager.IVideoManagerStorage;
import com.genersoft.iot.vmp.storager.dao.CloudRecordServiceMapper;
@@ -77,16 +75,13 @@
    private IInviteStreamService inviteStreamService;
    @Autowired
    private DeferredResultHolder resultHolder;
    private ZlmHttpHookSubscribe subscribe;
    @Autowired
    private ZLMRESTfulUtils zlmresTfulUtils;
    @Autowired
    private ZLMServerFactory zlmServerFactory;
    @Autowired
    private AssistRESTfulUtils assistRESTfulUtils;
    @Autowired
    private IMediaService mediaService;
@@ -105,9 +100,6 @@
    @Autowired
    private DynamicTask dynamicTask;
    @Autowired
    private ZlmHttpHookSubscribe subscribe;
    @Autowired
    private CloudRecordServiceMapper cloudRecordServiceMapper;
@@ -741,8 +733,11 @@
    @Override
    public StreamInfo getDownLoadInfo(String deviceId, String channelId, String stream) {
        InviteInfo inviteInfo = inviteStreamService.getInviteInfo(InviteSessionType.DOWNLOAD, deviceId, channelId, stream);
        if (inviteInfo == null || inviteInfo.getStreamInfo() == null) {
            logger.warn("[获取下载进度] 未查询到录像下载的信息");
            return null;
        }
        if (inviteInfo != null && inviteInfo.getStreamInfo() != null) {
            if (inviteInfo.getStreamInfo().getProgress() == 1) {
                return inviteInfo.getStreamInfo();
            }
@@ -751,30 +746,38 @@
            String mediaServerId = inviteInfo.getStreamInfo().getMediaServerId();
            MediaServerItem mediaServerItem = mediaServerService.getOne(mediaServerId);
            if (mediaServerItem == null) {
                logger.warn("查询录像信息时发现节点已离线");
            logger.warn("[获取下载进度] 查询录像信息时发现节点不存在");
                return null;
            }
            if (mediaServerItem.getRecordAssistPort() == 0) {
                throw new ControllerException(ErrorCode.ERROR100.getCode(), "未配置Assist服务,无法完成录像下载");
            }
            SsrcTransaction ssrcTransaction = streamSession.getSsrcTransaction(deviceId, channelId, null, stream);
            if (ssrcTransaction == null) {
                logger.warn("[获取下载进度],未找到下载事务信息");
            logger.warn("[获取下载进度] 下载已结束");
                return null;
            }
            // 为了支持多个数据库,这里不能使用求和函数来直接获取总数了
            List<CloudRecordItem> cloudRecordItemList = cloudRecordServiceMapper.getList(null, "rtp", inviteInfo.getStream(), null, null, ssrcTransaction.getCallId(), null);
            if (cloudRecordItemList.isEmpty()) {
                logger.warn("[获取下载进度],未找到下载视频信息");
        JSONObject mediaListJson= zlmresTfulUtils.getMediaList(mediaServerItem, "rtp", stream);
        if (mediaListJson == null) {
            logger.warn("[获取下载进度] 从zlm查询进度失败");
                return null;
            }
            long duration = 0;
            for (CloudRecordItem cloudRecordItem : cloudRecordItemList) {
                duration += cloudRecordItem.getTimeLen();
        if (mediaListJson.getInteger("code") != 0) {
            logger.warn("[获取下载进度] 从zlm查询进度出现错误: {}", mediaListJson.getString("msg"));
            return null;
            }
        JSONArray data = mediaListJson.getJSONArray("data");
        if (data == null) {
            logger.warn("[获取下载进度] 从zlm查询进度时未返回数据");
            return null;
        }
        JSONObject mediaJSON = data.getJSONObject(0);
        JSONArray tracks = mediaJSON.getJSONArray("tracks");
        if (tracks.isEmpty()) {
            logger.warn("[获取下载进度] 从zlm查询进度时未返回数据");
            return null;
        }
        JSONObject jsonObject = tracks.getJSONObject(0);
        long duration = jsonObject.getLongValue("duration");
            if (duration == 0) {
                inviteInfo.getStreamInfo().setProgress(0);
            } else {
@@ -791,10 +794,86 @@
                inviteInfo.getStreamInfo().setProgress(process);
            }
            inviteStreamService.updateInviteInfo(inviteInfo);
            return inviteInfo.getStreamInfo();
        }
        return null;
    @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) {
        DownloadFileInfo downloadFileInfo = new DownloadFileInfo();
        String pathTemplate = "%s://%s:%s/index/api/downloadFile?file_path=" + filePath;
        downloadFileInfo.setHttpPath(String.format(pathTemplate, "http", mediaServerItem.getStreamIp(),
                mediaServerItem.getHttpPort()));
        if (mediaServerItem.getHttpSSlPort() > 0) {
            downloadFileInfo.setHttpsPath(String.format(pathTemplate, "https", mediaServerItem.getStreamIp(),
                    mediaServerItem.getHttpSSlPort()));
        }
        return downloadFileInfo;
    }
    private StreamInfo onPublishHandlerForDownload(MediaServerItem mediaServerItemInuse, HookParam hookParam, String deviceId, String channelId, String startTime, String endTime) {
src/main/java/com/genersoft/iot/vmp/storager/dao/CloudRecordServiceMapper.java
@@ -106,4 +106,10 @@
            " </script>")
    int deleteList(List<CloudRecordItem> cloudRecordItemIdList);
    @Select(" <script>" +
            "select *" +
            " from wvp_cloud_record " +
            "where call_id = #{callId}" +
            " </script>")
    List<CloudRecordItem> getListByCallId(@Param("callId") String callId);
}
src/main/java/com/genersoft/iot/vmp/utils/DateUtil.java
@@ -139,4 +139,13 @@
        Instant beforeInstant = Instant.from(formatter.parse(keepaliveTime));
        return ChronoUnit.MILLIS.between(beforeInstant, Instant.now());
    }
    public static long getDifference(String startTime, String endTime) {
        if (ObjectUtils.isEmpty(startTime) || ObjectUtils.isEmpty(endTime)) {
            return 0;
        }
        Instant startInstant = Instant.from(formatter.parse(startTime));
        Instant endInstant = Instant.from(formatter.parse(endTime));
        return ChronoUnit.MILLIS.between(endInstant, startInstant);
    }
}
src/main/java/com/genersoft/iot/vmp/vmanager/gb28181/record/GBRecordController.java
@@ -1,5 +1,7 @@
package com.genersoft.iot.vmp.vmanager.gb28181.record;
import com.genersoft.iot.vmp.common.InviteInfo;
import com.genersoft.iot.vmp.common.InviteSessionType;
import com.genersoft.iot.vmp.common.StreamInfo;
import com.genersoft.iot.vmp.conf.UserSetting;
import com.genersoft.iot.vmp.conf.exception.ControllerException;
@@ -10,7 +12,9 @@
import com.genersoft.iot.vmp.gb28181.transmit.callback.RequestMessage;
import com.genersoft.iot.vmp.gb28181.transmit.cmd.impl.SIPCommander;
import com.genersoft.iot.vmp.service.IDeviceService;
import com.genersoft.iot.vmp.service.IInviteStreamService;
import com.genersoft.iot.vmp.service.IPlayService;
import com.genersoft.iot.vmp.service.bean.DownloadFileInfo;
import com.genersoft.iot.vmp.service.bean.InviteErrorCode;
import com.genersoft.iot.vmp.storager.IVideoManagerStorage;
import com.genersoft.iot.vmp.utils.DateUtil;
@@ -23,6 +27,7 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.ObjectUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
@@ -54,6 +59,9 @@
    @Autowired
    private IPlayService playService;
    @Autowired
    private IInviteStreamService inviteStreamService;
    @Autowired
    private IDeviceService deviceService;
@@ -204,4 +212,32 @@
        }
        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;
    }
}
web_src/src/components/dialog/recordDownload.vue
@@ -6,8 +6,7 @@
        <el-progress :percentage="percentage"></el-progress>
      </el-col>
      <el-col :span="6" >
        <el-button icon="el-icon-download" v-if="percentage < 100" size="mini" title="点击下载可将以缓存部分下载到本地" @click="download()">停止缓存并下载</el-button>
        <el-button icon="el-icon-download" v-if="downloadFile" size="mini" title="点击下载" @click="downloadFileClientEvent()">点击下载</el-button>
        <el-button icon="el-icon-download" v-if="downloadFile" size="mini" title="点击下载" @click="downloadFileClientEvent()">下载</el-button>
      </el-col>
    </el-row>
  </el-dialog>