package com.ard.zlm.service.impl; import com.alibaba.nacos.api.model.v2.ErrorCode; import com.ard.common.core.utils.DateUtils; import com.ard.zlm.api.domain.DownloadFileInfo; import com.ard.zlm.api.domain.StreamInfo; import com.ard.zlm.api.domain.ZlmCloudRecord; import com.ard.zlm.api.domain.ZlmMediaServer; import com.ard.zlm.common.InviteErrorCode; import com.ard.zlm.config.UserSetting; import com.ard.zlm.domain.CloudRecordUrl; import com.ard.zlm.domain.RecordInfo; import com.ard.zlm.domain.dto.ZLMResult; import com.ard.zlm.hook.Hook; import com.ard.zlm.hook.HookSubscribe; import com.ard.zlm.hook.HookType; import com.ard.zlm.mapper.ZlmCloudRecordMapper; import com.ard.zlm.service.*; import com.ard.zlm.utils.DateUtil; import com.ard.zlm.utils.ZLMRESTfulUtils; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Service; import java.io.File; import java.util.*; /** * 云端录像Service业务层处理 * * @author fengcheng * @date 2026-04-10 */ @Slf4j @Service public class ZlmCloudRecordServiceImpl implements IZlmCloudRecordService { @Autowired private ZlmCloudRecordMapper zlmCloudRecordMapper; @Autowired private UserSetting userSetting; @Autowired @Lazy private IMediaServerService mediaServerService; @Autowired private Map nodeServerServiceMap; @Autowired private HookSubscribe subscribe; @Autowired private ZLMRESTfulUtils zlmresTfulUtils; @Autowired private IRedisRpcPlayService redisRpcPlayService; /** * 查询云端录像 * * @param id 云端录像主键 * @return 云端录像 */ @Override public ZlmCloudRecord selectZlmCloudRecordById(Long id) { return zlmCloudRecordMapper.selectZlmCloudRecordById(id); } /** * 查询云端录像列表 * * @param zlmCloudRecord 云端录像 * @return 云端录像 */ @Override public List selectZlmCloudRecordList(ZlmCloudRecord zlmCloudRecord) { if (zlmCloudRecord.getMediaServerId() != null) { ZlmMediaServer mediaServer = mediaServerService.getOne(zlmCloudRecord.getMediaServerId()); if (mediaServer == null) { throw new RuntimeException("未找到流媒体: " + zlmCloudRecord.getMediaServerId()); } } if (zlmCloudRecord.getQueryStartTime() != null) { if (!DateUtil.verification(zlmCloudRecord.getQueryStartTime(), DateUtil.formatter)) { throw new RuntimeException("开始时间格式错误,正确格式为: " + DateUtil.formatter); } zlmCloudRecord.setStartTime(DateUtil.yyyy_MM_dd_HH_mm_ssToTimestampMs(zlmCloudRecord.getQueryStartTime())); } if (zlmCloudRecord.getQueryEndTime() != null) { if (!DateUtil.verification(zlmCloudRecord.getQueryEndTime(), DateUtil.formatter)) { throw new RuntimeException("结束时间格式错误,正确格式为: " + DateUtil.formatter); } zlmCloudRecord.setEndTime(DateUtil.yyyy_MM_dd_HH_mm_ssToTimestampMs(zlmCloudRecord.getQueryEndTime())); } return zlmCloudRecordMapper.selectZlmCloudRecordList(zlmCloudRecord); } /** * 新增云端录像 * * @param zlmCloudRecord 云端录像 * @return 结果 */ @Override public int insertZlmCloudRecord(ZlmCloudRecord zlmCloudRecord) { zlmCloudRecord.setCreateTime(DateUtils.getNowDate()); return zlmCloudRecordMapper.insertZlmCloudRecord(zlmCloudRecord); } /** * 修改云端录像 * * @param zlmCloudRecord 云端录像 * @return 结果 */ @Override public int updateZlmCloudRecord(ZlmCloudRecord zlmCloudRecord) { zlmCloudRecord.setUpdateTime(DateUtils.getNowDate()); return zlmCloudRecordMapper.updateZlmCloudRecord(zlmCloudRecord); } /** * 批量删除云端录像 * * @param ids 需要删除的云端录像主键 * @return 结果 */ @Override public void deleteZlmCloudRecordByIds(Long[] ids) { log.info("[删除录像文件] ids: {}", Arrays.stream(ids).toArray()); List cloudRecordItemList = zlmCloudRecordMapper.queryZlmCloudRecordByIds(ids); if (cloudRecordItemList.isEmpty()) { return; } List cloudRecordItemIdListForDelete = new ArrayList<>(); StringBuilder stringBuilder = new StringBuilder(); for (ZlmCloudRecord cloudRecordItem : cloudRecordItemList) { String date = new File(cloudRecordItem.getFilePath()).getParentFile().getName(); ZlmMediaServer mediaServer = mediaServerService.getOne(cloudRecordItem.getMediaServerId()); if (mediaServer == null) { throw new RuntimeException("未找到流媒体: " + cloudRecordItem.getMediaServerId()); } try { boolean deleteResult = mediaServerService.deleteRecordDirectory(mediaServer, cloudRecordItem.getApp(), cloudRecordItem.getStream(), date, cloudRecordItem.getFileName()); if (deleteResult) { log.warn("[录像文件] 删除磁盘文件成功: {}", cloudRecordItem.getFilePath()); cloudRecordItemIdListForDelete.add(cloudRecordItem); } } catch (RuntimeException e) { if (stringBuilder.length() > 0) { stringBuilder.append(", "); } stringBuilder.append(cloudRecordItem.getFileName()); } } if (!cloudRecordItemIdListForDelete.isEmpty()) { zlmCloudRecordMapper.deleteZlmCloudRecordByIds(ids); } if (stringBuilder.length() > 0) { stringBuilder.append(" 删除失败"); throw new RuntimeException(stringBuilder.toString()); } } /** * 播放云端录像 * * @param id * @param callback */ @Override public void loadRecord(Long id, ErrorCallback callback) { ZlmCloudRecord zlmCloudRecord = zlmCloudRecordMapper.selectZlmCloudRecordById(id); if (zlmCloudRecord == null) { callback.run(InviteErrorCode.FAIL.getCode(), "录像不存在", null); return; } ZlmMediaServer mediaServer = mediaServerService.getOne(zlmCloudRecord.getMediaServerId()); if (mediaServer == null) { callback.run(InviteErrorCode.FAIL.getCode(), "无可用的节点", null); return; } loadMP4File(mediaServer, "record_file", zlmCloudRecord, ((code, msg, streamInfo) -> { callback.run(code, msg, streamInfo); })); } @Override public void closeStreams(Long id) { ZlmCloudRecord zlmCloudRecord = zlmCloudRecordMapper.selectZlmCloudRecordById(id); if (zlmCloudRecord == null) { throw new RuntimeException("录像不存在"); } ZlmMediaServer mediaServer = mediaServerService.getOne(zlmCloudRecord.getMediaServerId()); if (mediaServer == null) { throw new RuntimeException("无可用的节点"); } IMediaNodeServerService mediaNodeServerService = nodeServerServiceMap.get(mediaServer.getType()); if (mediaNodeServerService == null) { log.info("[closeStreams] 失败, mediaServer的类型: {},未找到对应的实现类", mediaServer.getType()); return; } mediaNodeServerService.closeStreams(mediaServer, "record_file", zlmCloudRecord.getStream()); } /** * 定时查询待删除的录像文件 */ @Override public void task() { log.info("[录像文件定时清理] 开始清理过期录像文件"); // 获取配置了assist的流媒体节点 List mediaServerItemList = mediaServerService.getAllOnlineMediaServe(); if (mediaServerItemList.isEmpty()) { return; } long result = 0; for (ZlmMediaServer mediaServerItem : mediaServerItemList) { Calendar lastCalendar = Calendar.getInstance(); if (mediaServerItem.getRecordDay() > 0) { lastCalendar.setTime(new Date()); // 获取保存的最后截至日[期,因为每个节点都有一个日期,也就是支持每个节点设置不同的保存日期, lastCalendar.add(Calendar.DAY_OF_MONTH, -mediaServerItem.getRecordDay()); Long lastDate = lastCalendar.getTimeInMillis(); // 获取到截至日期之前的录像文件列表,文件列表满足未被收藏和保持的。这两个字段目前共能一致, // 为我自己业务系统相关的代码,大家使用的时候直接使用收藏(collect)这一个类型即可 List cloudRecordItemList = zlmCloudRecordMapper.queryCloudRecordListForDelete(lastDate, mediaServerItem.getId()); if (cloudRecordItemList.isEmpty()) { continue; } // TODO 后续可以删除空了的过期日期文件夹 for (ZlmCloudRecord cloudRecordItem : cloudRecordItemList) { String date = new File(cloudRecordItem.getFilePath()).getParentFile().getName(); try { boolean deleteResult = mediaServerService.deleteRecordDirectory(mediaServerItem, cloudRecordItem.getApp(), cloudRecordItem.getStream(), date, cloudRecordItem.getFileName()); if (deleteResult) { log.warn("[录像文件定时清理] 删除磁盘文件成功: {}", cloudRecordItem.getFilePath()); } } catch (Exception ignored) { } } List idList = cloudRecordItemList.stream().map(ZlmCloudRecord::getId).toList(); result += zlmCloudRecordMapper.deleteZlmCloudRecordByIds(idList.toArray(Long[]::new)); } } log.info("[录像文件定时清理] 共清理{}个过期录像文件", result); } /** * 根据id获取url * * @param ids * @return */ @Override public List getUrlListByIds(List ids) { List cloudRecordItems = zlmCloudRecordMapper.queryZlmCloudRecordByIds(ids.toArray(Long[]::new)); if (cloudRecordItems.isEmpty()) { return List.of(); } return getCloudRecordUrl(cloudRecordItems); } /** * 设置录像播放速度 * * @param mediaServerId 使用的节点Id * @param app 应用名 * @param stream 流id * @param speed 播放速度 * @param schema 播放协议 */ @Override public void setRecordSpeed(String mediaServerId, String app, String stream, Integer speed, String schema) { ZlmMediaServer mediaServer = mediaServerService.getOne(mediaServerId); if (mediaServer == null) { throw new RuntimeException("媒体节点不存在: " + mediaServerId); } mediaServerService.setRecordSpeed(mediaServer, app, stream, speed, schema); } /** * 定位录像播放到制定位置 * * @param mediaServerId 使用的节点Id * @param app 应用名 * @param stream 流ID * @param stamp 要定位的时间位置,从录像开始的时间算起 * @param schema 播放协议 */ @Override public void seekRecord(String mediaServerId, String app, String stream, Double stamp, String schema) { ZlmMediaServer mediaServer = mediaServerService.getOne(mediaServerId); if (mediaServer == null) { throw new RuntimeException("媒体节点不存在: " + mediaServerId); } mediaServerService.seekRecordStamp(mediaServer, app, stream, stamp, schema); } private List getCloudRecordUrl(List cloudRecordItems) { if (cloudRecordItems.isEmpty()) { return List.of(); } List resultList = new ArrayList<>(); for (ZlmCloudRecord cloudRecordItem : cloudRecordItems) { CloudRecordUrl cloudRecordUrl = new CloudRecordUrl(); cloudRecordUrl.setId(cloudRecordItem.getId()); cloudRecordUrl.setFileName(cloudRecordItem.getStartTime() + ".mp4"); cloudRecordUrl.setFilePath(cloudRecordItem.getFilePath()); if (!userSetting.getServerId().equals(cloudRecordItem.getServerId())) { cloudRecordUrl.setDownloadUrl(redisRpcPlayService.getRecordPlayUrl(cloudRecordItem.getServerId(), cloudRecordItem.getId()).getHttpPath()); } else { ZlmMediaServer mediaServer = mediaServerService.getOne(cloudRecordItem.getMediaServerId()); if (mediaServer == null) { throw new RuntimeException("媒体节点不存在: " + cloudRecordItem.getMediaServerId()); } mediaServer.setStreamIp(mediaServer.getIp()); DownloadFileInfo downloadFilePath = mediaServerService.getDownloadFilePath(mediaServer, RecordInfo.getInstance(cloudRecordItem)); cloudRecordUrl.setDownloadUrl(downloadFilePath.getHttpPath()); } resultList.add(cloudRecordUrl); } return resultList; } private void loadMP4File(ZlmMediaServer mediaServer, String app, ZlmCloudRecord zlmCloudRecord, ErrorCallback callback) { IMediaNodeServerService mediaNodeServerService = nodeServerServiceMap.get(mediaServer.getType()); if (mediaNodeServerService == null) { log.info("[loadMP4File] 失败, mediaServer的类型: {},未找到对应的实现类", mediaServer.getType()); throw new RuntimeException("未找到mediaServer对应的实现类"); } StreamInfo streamData = mediaServerService.getStreamInfoByAppAndStreamWithCheck(app, zlmCloudRecord.getStream(), mediaServer.getId(), null, false); if (streamData != null) { callback.run(ErrorCode.SUCCESS.getCode(), ErrorCode.SUCCESS.getMsg(), streamData); return; } Hook hook = Hook.getInstance(HookType.on_media_arrival, app, zlmCloudRecord.getStream(), mediaServer.getServerId()); subscribe.addSubscribe(hook, (hookData) -> { StreamInfo streamInfo = mediaServerService.getStreamInfoByAppAndStream(mediaServer, app, zlmCloudRecord.getStream(), hookData.getMediaInfo()); if (callback != null) { callback.run(ErrorCode.SUCCESS.getCode(), ErrorCode.SUCCESS.getMsg(), streamInfo); } }); ZLMResult zlmResult = zlmresTfulUtils.loadMP4File(mediaServer, app, zlmCloudRecord.getStream(), zlmCloudRecord.getFilePath()); if (zlmResult == null) { throw new RuntimeException("请求失败"); } if (zlmResult.getCode() != 0) { throw new RuntimeException(zlmResult.getMsg()); } } }