package com.ard.zlm.service.impl;
|
|
import com.ard.work.api.RemoteChannelService;
|
import com.ard.work.api.domian.ArdChannel;
|
import com.google.common.base.Joiner;
|
import com.ard.common.core.constant.HttpStatus;
|
import com.ard.common.core.constant.SecurityConstants;
|
import com.ard.common.core.domain.R;
|
import com.ard.common.core.enums.LiveStreamType;
|
import com.ard.common.core.utils.DateUtils;
|
import com.ard.qs.api.RemoteQsDeviceService;
|
import com.ard.qs.api.domain.QsDevice;
|
import com.ard.zlm.api.domain.MediaInfo;
|
import com.ard.zlm.api.domain.RTPServerParam;
|
import com.ard.zlm.api.domain.StreamInfo;
|
import com.ard.zlm.api.domain.StreamPullPlay;
|
import com.ard.zlm.common.InviteErrorCode;
|
import com.ard.zlm.domain.ZlmRecordPlan;
|
import com.ard.zlm.domain.ZlmRecordPlanItem;
|
import com.ard.zlm.event.MediaDepartureEvent;
|
import com.ard.zlm.mapper.ZlmRecordPlanItemMapper;
|
import com.ard.zlm.mapper.ZlmRecordPlanMapper;
|
import com.ard.zlm.service.IDevicePlayService;
|
import com.ard.zlm.service.IMediaServerService;
|
import com.ard.zlm.service.IZlmRecordPlanService;
|
import jakarta.annotation.Resource;
|
import lombok.extern.slf4j.Slf4j;
|
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.context.event.EventListener;
|
import org.springframework.scheduling.annotation.Async;
|
import org.springframework.stereotype.Service;
|
import org.springframework.transaction.annotation.Transactional;
|
|
import java.time.LocalDateTime;
|
import java.util.*;
|
|
/**
|
* 录像计划Service业务层处理
|
*
|
* @author fengcheng
|
* @date 2026-04-11
|
*/
|
@Slf4j
|
@Service
|
@Transactional(rollbackFor = Exception.class)
|
public class ZlmRecordPlanServiceImpl implements IZlmRecordPlanService {
|
@Resource
|
private ZlmRecordPlanMapper zlmRecordPlanMapper;
|
|
@Resource
|
private ZlmRecordPlanItemMapper zlmRecordPlanItemMapper;
|
|
@Resource
|
private RemoteQsDeviceService remoteQsDeviceService;
|
@Resource
|
private RemoteChannelService remoteChannelService;
|
@Resource
|
private IMediaServerService mediaServerService;
|
|
@Resource
|
private IDevicePlayService devicePlayService;
|
|
|
Map<String, StreamInfo> recordStreamMap = new HashMap<>();
|
|
/**
|
* 流离开的处理
|
*/
|
@Async("taskExecutor")
|
@EventListener
|
public void onApplicationEvent(MediaDepartureEvent event) {
|
// 流断开,检查是否还处于录像状态, 如果是则继续录像
|
String deviceId = recording(event.getApp(), event.getStream());
|
if (deviceId == null) {
|
return;
|
}
|
// 重新拉起
|
R<ArdChannel> r = remoteChannelService.getInfo(deviceId, SecurityConstants.INNER);
|
// R<QsDevice> r = remoteQsDeviceService.getQsDeviceInfo(deviceId, SecurityConstants.INNER);
|
if (r.getCode() != HttpStatus.SUCCESS) {
|
throw new RuntimeException("根据设备id查询设备信息失败");
|
}
|
|
ArdChannel device = r.getData();
|
if (device == null) {
|
log.warn("[录制计划] 流离开时拉起需要录像的流时, 发现设备不存在, id: {}", deviceId);
|
}
|
|
if (device == null) {
|
log.warn("[录制计划] 流离开时拉起需要录像的流时, 发现设备不存在, id: {}", deviceId);
|
return;
|
}
|
|
// if ("OFFLINE".equals(device.getDeviceStatus())) {
|
// log.warn("[录制计划] 流离开时拉起需要录像的流时, 发现设备不在线, id: {}", deviceId);
|
// return;
|
// }
|
// 开启点播,
|
devicePlayService.play(device, true, ((code, msg, streamInfo) -> {
|
if (code == InviteErrorCode.SUCCESS.getCode() && streamInfo != null) {
|
log.info("[录像] 流离开时拉起需要录像的流, 开启成功, 设备ID: {}", device.getId());
|
recordStreamMap.put(device.getId(), streamInfo);
|
} else {
|
recordStreamMap.remove(deviceId);
|
log.info("[录像] 流离开时拉起需要录像的流, 开启失败, 十分钟后重试, 设备ID: {}", device.getId());
|
}
|
}));
|
}
|
|
public String recording(String app, String stream) {
|
for (String deviceId : recordStreamMap.keySet()) {
|
StreamInfo streamInfo = recordStreamMap.get(deviceId);
|
if (streamInfo != null && streamInfo.getApp().equals(app) && streamInfo.getStream().equals(stream)) {
|
return deviceId;
|
}
|
}
|
return null;
|
}
|
|
|
/**
|
* 查询录像计划
|
*
|
* @param id 录像计划主键
|
* @return 录像计划
|
*/
|
@Override
|
public ZlmRecordPlan selectZlmRecordPlanById(Long id) {
|
ZlmRecordPlan zlmRecordPlan = zlmRecordPlanMapper.selectZlmRecordPlanById(id);
|
if (zlmRecordPlan == null) {
|
throw new RuntimeException("录像计划不存在");
|
}
|
List<ZlmRecordPlanItem> recordPlanItemList = zlmRecordPlanItemMapper.getItemList(id);
|
if (!recordPlanItemList.isEmpty()) {
|
zlmRecordPlan.setPlanItemList(recordPlanItemList);
|
}
|
return zlmRecordPlan;
|
}
|
|
/**
|
* 查询录像计划列表
|
*
|
* @param zlmRecordPlan 录像计划
|
* @return 录像计划
|
*/
|
@Override
|
public List<ZlmRecordPlan> selectZlmRecordPlanList(ZlmRecordPlan zlmRecordPlan) {
|
List<ZlmRecordPlan> zlmRecordPlans = zlmRecordPlanMapper.selectZlmRecordPlanList(zlmRecordPlan);
|
for (ZlmRecordPlan recordPlan : zlmRecordPlans) {
|
R<Integer> r = remoteQsDeviceService.countRecordPlanDevice(recordPlan.getId(), SecurityConstants.INNER);
|
if (r.getCode() != HttpStatus.SUCCESS) {
|
throw new RuntimeException("删除录像计划失败");
|
}
|
recordPlan.setChannelCount(r.getData());
|
|
}
|
return zlmRecordPlans;
|
}
|
|
/**
|
* 新增录像计划
|
*
|
* @param zlmRecordPlan 录像计划
|
* @return 结果
|
*/
|
@Override
|
public int insertZlmRecordPlan(ZlmRecordPlan zlmRecordPlan) {
|
zlmRecordPlan.setCreateTime(DateUtils.getNowDate());
|
int i = zlmRecordPlanMapper.insertZlmRecordPlan(zlmRecordPlan);
|
if (zlmRecordPlan.getId() > 0 && !zlmRecordPlan.getPlanItemList().isEmpty()) {
|
for (ZlmRecordPlanItem recordPlanItem : zlmRecordPlan.getPlanItemList()) {
|
recordPlanItem.setPlanId(zlmRecordPlan.getId());
|
}
|
zlmRecordPlanItemMapper.batchAddItem(zlmRecordPlan.getId(), zlmRecordPlan.getPlanItemList());
|
}
|
return i;
|
}
|
|
/**
|
* 修改录像计划
|
*
|
* @param zlmRecordPlan 录像计划
|
* @return 结果
|
*/
|
@Override
|
public int updateZlmRecordPlan(ZlmRecordPlan zlmRecordPlan) {
|
zlmRecordPlan.setUpdateTime(DateUtils.getNowDate());
|
zlmRecordPlanItemMapper.cleanItems(zlmRecordPlan.getId());
|
if (zlmRecordPlan.getPlanItemList() != null && !zlmRecordPlan.getPlanItemList().isEmpty()) {
|
List<ZlmRecordPlanItem> planItemList = new ArrayList<>();
|
for (ZlmRecordPlanItem recordPlanItem : zlmRecordPlan.getPlanItemList()) {
|
if (recordPlanItem.getStart() == null || recordPlanItem.getStop() == null || recordPlanItem.getWeekDay() == null) {
|
continue;
|
}
|
if (recordPlanItem.getPlanId() == null) {
|
recordPlanItem.setPlanId(zlmRecordPlan.getId());
|
}
|
planItemList.add(recordPlanItem);
|
}
|
if (!planItemList.isEmpty()) {
|
zlmRecordPlanItemMapper.batchAddItem(zlmRecordPlan.getId(), planItemList);
|
}
|
}
|
return zlmRecordPlanMapper.updateZlmRecordPlan(zlmRecordPlan);
|
}
|
|
/**
|
* 批量删除录像计划
|
*
|
* @param ids 需要删除的录像计划主键
|
* @return 结果
|
*/
|
@Override
|
public int deleteZlmRecordPlanByIds(Long[] ids) {
|
for (Long id : ids) {
|
zlmRecordPlanItemMapper.cleanItems(id);
|
R<Void> r = remoteQsDeviceService.cleanRecordPlanId(id, SecurityConstants.INNER);
|
if (r.getCode() != HttpStatus.SUCCESS) {
|
throw new RuntimeException("删除录像计划失败");
|
}
|
}
|
return zlmRecordPlanMapper.deleteZlmRecordPlanByIds(ids);
|
}
|
|
/**
|
* 录像计划任务
|
*/
|
@Override
|
public void task() {
|
// List<String> startDeviceIdList = queryCurrentChannelRecord();
|
// if (startDeviceIdList.isEmpty()) {
|
// // 当前没有录像任务, 如果存在旧的正在录像的就移除
|
// if (!recordStreamMap.isEmpty()) {
|
// Set<String> recordStreamSet = new HashSet<>(recordStreamMap.keySet());
|
// stopStreams(recordStreamSet, recordStreamMap);
|
// recordStreamMap.clear();
|
// }
|
// } else {
|
// // 当前存在录像任务, 获取正在录像中存在但是当前录制列表不存在的内容,进行停止; 获取正在录像中没有但是当前需录制的列表中存在的进行开启.
|
// Set<String> recordStreamSet = new HashSet<>(recordStreamMap.keySet());
|
// startDeviceIdList.forEach(recordStreamSet::remove);
|
// if (!recordStreamSet.isEmpty()) {
|
// // 正在录像中存在但是当前录制列表不存在的内容,进行停止;
|
// stopStreams(recordStreamSet, recordStreamMap);
|
// }
|
//
|
// // 移除startDeviceIdList中已经在录像的部分, 剩下的都是需要新添加的(正在录像中没有但是当前需录制的列表中存在的进行开启)
|
// recordStreamMap.keySet().forEach(startDeviceIdList::remove);
|
// if (!startDeviceIdList.isEmpty()) {
|
// // 获取所有的关联的设备
|
// // R<List<QsDevice>> r = remoteQsDeviceService.queryByIds(startDeviceIdList, SecurityConstants.INNER);
|
// R<List<ArdChannel>> r = remoteChannelService.queryByIds(startDeviceIdList, SecurityConstants.INNER);
|
// if (r.getCode() != HttpStatus.SUCCESS) {
|
// throw new RuntimeException("根据设备id集合查询设备信息失败");
|
// }
|
// List<ArdChannel> deviceList = r.getData();
|
// if (!deviceList.isEmpty()) {
|
// // 查找是否已经开启录像, 如果没有则开启录像
|
// for (ArdChannel device : deviceList) {
|
//// if ("OFFLINE".equals(device.getDeviceStatus())) {
|
//// log.warn("[录制计划] 流离开时拉起需要录像的流时, 发现设备不在线, id: {}", device.getId());
|
//// return;
|
//// }
|
////
|
//// if ("DEACTIVATE".equals(device.getStatus())) {
|
//// log.warn("[录制计划] 流离开时拉起需要录像的流时, 发现设备未启用, id: {}", device.getId());
|
//// return;
|
//// }
|
// // 开启点播,
|
// devicePlayService.play(device, true, ((code, msg, streamInfo) -> {
|
// if (code == InviteErrorCode.SUCCESS.getCode() && streamInfo != null) {
|
// log.info("[录像] 开启成功, 设备ID: {}", device.getId());
|
// recordStreamMap.put(device.getId(), streamInfo);
|
// } else {
|
// log.info("[录像] 开启失败, 十分钟后重试, 设备ID: {}", device.getId());
|
// }
|
// }));
|
// }
|
// } else {
|
// log.error("[录制计划] 数据异常, 这些关联的设备已经不存在了: {}", Joiner.on(",").join(startDeviceIdList));
|
// }
|
// }
|
// }
|
}
|
|
/**
|
* 停止录像
|
*
|
* @param devices 设备ID列表
|
* @param recordStreamMap 正在录制的流信息
|
*/
|
private void stopStreams(Collection<String> devices, Map<String, StreamInfo> recordStreamMap) {
|
for (String deviceId : devices) {
|
try {
|
StreamInfo streamInfo = recordStreamMap.get(deviceId);
|
if (streamInfo == null) {
|
continue;
|
}
|
|
// 查看是否有人观看,存在则不做处理,等待后续自然处理,如果无人观看,则关闭该流
|
MediaInfo mediaInfo = mediaServerService.getMediaInfo(streamInfo.getMediaServer(),
|
streamInfo.getApp(), streamInfo.getStream());
|
if (mediaInfo.getReaderCount() == null || mediaInfo.getReaderCount() == 0) {
|
R<ArdChannel> r = remoteChannelService.getInfo(deviceId, SecurityConstants.INNER);
|
// R<QsDevice> r = remoteQsDeviceService.getQsDeviceInfo(deviceId, SecurityConstants.INNER);
|
if (r.getCode() != HttpStatus.SUCCESS) {
|
throw new RuntimeException("根据通道id查询设备信息失败");
|
}
|
|
ArdChannel ardChannel = r.getData();
|
if (ardChannel == null) {
|
throw new RuntimeException("通道不存在");
|
}
|
|
|
// 播放海康sdk/播放海康isup/播放大华sdk
|
// if (LiveStreamType.HIK_SDK.getCode().equals(device.getType())
|
// || LiveStreamType.HIK_ISUP.getCode().equals(device.getType())
|
// || LiveStreamType.DAHUA_SDK.getCode().equals(device.getType())
|
// ) {
|
// RTPServerParam rtpServerParam = new RTPServerParam();
|
// rtpServerParam.setType(device.getType());
|
// rtpServerParam.setStreamId(device.getDeviceCode());
|
// rtpServerParam.setId(device.getId());
|
//
|
// mediaServerService.stopRtpPlay(rtpServerParam);
|
// }
|
|
// rtsp/rtmp/flv/hls/onvif
|
if (LiveStreamType.RTSP.getCode().equals(ardChannel.getType())
|
|| LiveStreamType.RTMP.getCode().equals(ardChannel.getType())
|
|| LiveStreamType.FLV.getCode().equals(ardChannel.getType())
|
|| LiveStreamType.HLS.getCode().equals(ardChannel.getType())
|
|| LiveStreamType.ONVIF.getCode().equals(ardChannel.getType())
|
) {
|
StreamPullPlay streamPullPlay = new StreamPullPlay();
|
streamPullPlay.setChannelId(ardChannel.getId());
|
streamPullPlay.setMediaServerId(ardChannel.getMediaServerId());
|
streamPullPlay.setStreamKey(ardChannel.getStreamKey());
|
mediaServerService.stopStreamPullPlay(streamPullPlay);
|
}
|
|
// 视频文件
|
// if (LiveStreamType.VIDEO_FILE.getCode().equals(device.getType())) {
|
// mediaServerService.closeStreams(device.getId());
|
// }
|
log.info("[录制计划] 停止, 设备ID: {}", deviceId);
|
}
|
} catch (Exception e) {
|
log.error("[录制计划] 停止时异常", e);
|
} finally {
|
recordStreamMap.remove(deviceId);
|
}
|
}
|
}
|
|
/**
|
* 获取当前时间段应该录像的设备Id列表
|
*/
|
private List<Long> queryCurrentChannelRecord() {
|
// 获取当前时间在一周内的序号, 数据库存储的从第几个30分钟开始, 0-47, 包括首尾
|
LocalDateTime now = LocalDateTime.now();
|
int week = now.getDayOfWeek().getValue();
|
|
int hour = now.getHour();
|
int minute = now.getMinute();
|
|
int index;
|
if (minute < 30) {
|
// 00-29 分:属于该小时的第 1 个 30 分钟 (偶数索引)
|
index = hour * 2;
|
} else {
|
// 30-59 分:属于该小时的第 2 个 30 分钟 (奇数索引)
|
index = hour * 2 + 1;
|
}
|
|
// 查询现在需要录像的设备Id
|
return zlmRecordPlanMapper.queryRecordIng(week, index);
|
}
|
}
|