mirror of
https://gitee.com/pan648540858/wvp-GB28181-pro.git
synced 2026-05-06 06:06:08 +08:00
实现报警快照获取功能,优化相关接口返回数据类型
This commit is contained in:
parent
2d1608ca7a
commit
149c5a550b
@ -235,6 +235,10 @@ public class UserSetting {
|
||||
*/
|
||||
private long alarmCatchSize = 10000;
|
||||
|
||||
/**
|
||||
* 是否使用拉流的方式获取快照,默认false,避免流量大规模消耗,开启后则使用拉流的方式获取快照
|
||||
*/
|
||||
private boolean snapByPullStream = false;
|
||||
|
||||
|
||||
}
|
||||
|
||||
@ -92,6 +92,7 @@ public class WebSecurityConfig {
|
||||
|
||||
defaultExcludes.add("/js/**");
|
||||
defaultExcludes.add("/api/device/query/snap/**");
|
||||
defaultExcludes.add("/api/alarm/snap/**");
|
||||
defaultExcludes.add("/record_proxy/*/**");
|
||||
defaultExcludes.add("/api/emit");
|
||||
defaultExcludes.add("/favicon.ico");
|
||||
|
||||
@ -38,4 +38,7 @@ public interface IGbChannelPlayService {
|
||||
void playbackSpeed(CommonGBChannel channel, String stream, Double speed);
|
||||
|
||||
void queryRecord(CommonGBChannel channel, String startTime, String endTime, ErrorCallback<List<CommonRecordInfo>> callback);
|
||||
|
||||
|
||||
void getSnap(CommonGBChannel channel, ErrorCallback<byte[]> callback);
|
||||
}
|
||||
|
||||
@ -66,6 +66,8 @@ public interface IPlayService {
|
||||
|
||||
void getSnap(String deviceId, String channelId, String fileName, ErrorCallback errorCallback);
|
||||
|
||||
void getSnap(CommonGBChannel channel, ErrorCallback<byte[]> errorCallback);
|
||||
|
||||
void stop(InviteSessionType type, Device device, DeviceChannel channel, String stream);
|
||||
|
||||
void stop(InviteInfo inviteInfo);
|
||||
|
||||
@ -14,4 +14,6 @@ public interface ISourcePlayService {
|
||||
|
||||
void stopPlay(CommonGBChannel channel);
|
||||
|
||||
void getSnap(CommonGBChannel channel, ErrorCallback<byte[]> callback);
|
||||
|
||||
}
|
||||
|
||||
@ -10,7 +10,6 @@ import com.genersoft.iot.vmp.gb28181.service.IGbChannelPlayService;
|
||||
import com.genersoft.iot.vmp.gb28181.service.ISourceDownloadService;
|
||||
import com.genersoft.iot.vmp.gb28181.service.ISourcePlayService;
|
||||
import com.genersoft.iot.vmp.gb28181.service.ISourcePlaybackService;
|
||||
import com.genersoft.iot.vmp.jt1078.service.Ijt1078PlayService;
|
||||
import com.genersoft.iot.vmp.service.bean.ErrorCallback;
|
||||
import com.genersoft.iot.vmp.service.bean.InviteErrorCode;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
@ -34,9 +33,6 @@ public class GbChannelPlayServiceImpl implements IGbChannelPlayService {
|
||||
@Autowired
|
||||
private Map<String, ISourcePlayService> sourcePlayServiceMap;
|
||||
|
||||
@Autowired
|
||||
private Ijt1078PlayService jt1078PlayService;
|
||||
|
||||
@Autowired
|
||||
private Map<String, ISourcePlaybackService> sourcePlaybackServiceMap;
|
||||
|
||||
@ -238,4 +234,17 @@ public class GbChannelPlayServiceImpl implements IGbChannelPlayService {
|
||||
}
|
||||
playbackService.queryRecord(channel, startTime, endTime, callback);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void getSnap(CommonGBChannel channel, ErrorCallback<byte[]> callback) {
|
||||
log.info("[通用通道] 获取快照, 类型: {}, 编号:{}", ChannelDataType.getDateTypeDesc(channel.getDataType()), channel.getGbDeviceId());
|
||||
Integer dataType = channel.getDataType();
|
||||
ISourcePlayService sourceChannelPlayService = sourcePlayServiceMap.get(ChannelDataType.PLAY_SERVICE + dataType);
|
||||
if (sourceChannelPlayService == null) {
|
||||
// 通道数据异常
|
||||
log.error("[通用通道] 获取快照 类型编号: {} 不支持实时流预览相关服务", ChannelDataType.getDateTypeDesc(channel.getDataType()));
|
||||
throw new PlayException(Response.BUSY_HERE, "channel not support");
|
||||
}
|
||||
sourceChannelPlayService.getSnap(channel, callback);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1630,6 +1630,57 @@ public class PlayServiceImpl implements IPlayService {
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void getSnap(CommonGBChannel channel, ErrorCallback<byte[]> errorCallback) {
|
||||
// 2016协议不支持直接获取国标通道的抓图, 只能通过点播的方式获取
|
||||
Device device = deviceService.getDevice(channel.getDataDeviceId());
|
||||
if (device == null) {
|
||||
log.warn("[快照] 未找到通道{}的设备信息", channel);
|
||||
errorCallback.run(InviteErrorCode.FAIL.getCode(), "未找到设备信息", null);
|
||||
return;
|
||||
}
|
||||
DeviceChannel deviceChannel = deviceChannelService.getOneForSourceById(channel.getGbId());
|
||||
if (deviceChannel == null) {
|
||||
log.warn("[快照] 未找到通道{}的设备信息", channel);
|
||||
errorCallback.run(InviteErrorCode.FAIL.getCode(), "未找到原始通道", null);
|
||||
return;
|
||||
}
|
||||
|
||||
InviteInfo inviteInfo = inviteStreamService.getInviteInfoByDeviceAndChannel(InviteSessionType.PLAY, channel.getGbId());
|
||||
if (inviteInfo != null) {
|
||||
if (inviteInfo.getStreamInfo() != null) {
|
||||
// 已存在线直接截图
|
||||
MediaServer mediaServer = inviteInfo.getStreamInfo().getMediaServer();
|
||||
String path = "snap";
|
||||
// 请求截图
|
||||
log.info("[请求截图]: 返回byte数组" );
|
||||
byte[] snapByteArray = mediaServerService.getSnap(mediaServer, MediaApp.GB28181, inviteInfo.getStreamInfo().getStream(), 15, 1, path, null);
|
||||
if (snapByteArray != null) {
|
||||
errorCallback.run(InviteErrorCode.SUCCESS.getCode(), InviteErrorCode.SUCCESS.getMsg(), snapByteArray);
|
||||
}else {
|
||||
errorCallback.run(InviteErrorCode.FAIL.getCode(), InviteErrorCode.FAIL.getMsg(), null);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
play(device, deviceChannel, (code, msg, data)->{
|
||||
if (code == InviteErrorCode.SUCCESS.getCode()) {
|
||||
InviteInfo inviteInfoForPlay = inviteStreamService.getInviteInfoByDeviceAndChannel(InviteSessionType.PLAY, channel.getGbId());
|
||||
if (inviteInfoForPlay != null && inviteInfoForPlay.getStreamInfo() != null) {
|
||||
byte[] snapByteArray = mediaServerService.getSnap(data.getMediaServer(), MediaApp.GB28181, data.getStream(), 15, 1, null, null);
|
||||
errorCallback.run(InviteErrorCode.SUCCESS.getCode(), InviteErrorCode.SUCCESS.getMsg(), snapByteArray);
|
||||
}else {
|
||||
errorCallback.run(InviteErrorCode.FAIL.getCode(), InviteErrorCode.FAIL.getMsg(), null);
|
||||
}
|
||||
}else {
|
||||
errorCallback.run(InviteErrorCode.FAIL.getCode(), InviteErrorCode.FAIL.getMsg(), null);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Override
|
||||
public void stop(InviteSessionType type, Device device, DeviceChannel channel, String stream) {
|
||||
if (!userSetting.getServerId().equals(device.getServerId())) {
|
||||
|
||||
@ -48,4 +48,19 @@ public class SourcePlayServiceForGbImpl implements ISourcePlayService {
|
||||
log.error("[停止点播失败] {}({})", channel.getGbName(), channel.getGbDeviceId(), e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void getSnap(CommonGBChannel channel, ErrorCallback<byte[]> callback) {
|
||||
try {
|
||||
deviceChannelPlayService.getSnap(channel, callback);
|
||||
} catch (PlayException e) {
|
||||
callback.run(e.getCode(), e.getMsg(), null);
|
||||
} catch (ControllerException e) {
|
||||
log.error("[获取抓图失败] {}({}), {}", channel.getGbName(), channel.getGbDeviceId(), e.getMsg());
|
||||
callback.run(Response.BUSY_HERE, "busy here", null);
|
||||
} catch (Exception e) {
|
||||
log.error("[获取抓图失败] {}({})", channel.getGbName(), channel.getGbDeviceId(), e);
|
||||
callback.run(Response.BUSY_HERE, "busy here", null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -44,4 +44,9 @@ public class SourcePlayServiceForJTImpl implements ISourcePlayService {
|
||||
log.error("[停止点播失败] {}({})", channel.getGbName(), channel.getGbDeviceId(), e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void getSnap(CommonGBChannel channel, ErrorCallback<byte[]> callback) {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@ -264,8 +264,8 @@ public class ABLMediaNodeServerService implements IMediaNodeServerService {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void getSnap(MediaServer mediaServer, String app, String stream, int timeoutSec, int expireSec, String path, String fileName) {
|
||||
ablresTfulUtils.getSnap(mediaServer, app, stream, timeoutSec, path, fileName);
|
||||
public byte[] getSnap(MediaServer mediaServer, String app, String stream, int timeoutSec, int expireSec, String path, String fileName) {
|
||||
return ablresTfulUtils.getSnap(mediaServer, app, stream, timeoutSec, path, fileName);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@ -203,11 +203,11 @@ public class ABLRESTfulUtils {
|
||||
return result;
|
||||
}
|
||||
|
||||
public void sendGetForImg(MediaServer mediaServerItem, String api, Map<String, Object> params, String targetPath, String fileName) {
|
||||
public byte[] sendGetForImg(MediaServer mediaServerItem, String api, Map<String, Object> params, String targetPath, String fileName) {
|
||||
String url = String.format("http://%s:%s/index/api/%s", mediaServerItem.getIp(), mediaServerItem.getHttpPort(), api);
|
||||
HttpUrl parseUrl = HttpUrl.parse(url);
|
||||
if (parseUrl == null) {
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
HttpUrl.Builder httpBuilder = parseUrl.newBuilder();
|
||||
|
||||
@ -239,9 +239,8 @@ public class ABLRESTfulUtils {
|
||||
outStream.write(Objects.requireNonNull(response.body()).bytes());
|
||||
outStream.flush();
|
||||
outStream.close();
|
||||
} else {
|
||||
logger.error(String.format("[ %s ]请求失败: %s %s", url, response.code(), response.message()));
|
||||
}
|
||||
return Objects.requireNonNull(response.body()).bytes();
|
||||
} else {
|
||||
logger.error(String.format("[ %s ]请求失败: %s %s", url, response.code(), response.message()));
|
||||
}
|
||||
@ -252,6 +251,7 @@ public class ABLRESTfulUtils {
|
||||
} catch (IOException e) {
|
||||
logger.error(String.format("[ %s ]请求失败: %s", url, e.getMessage()));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public void sendGetForImgForUrl(String url, String targetPath, String fileName) {
|
||||
@ -414,7 +414,7 @@ public class ABLRESTfulUtils {
|
||||
}
|
||||
}
|
||||
|
||||
public void getSnap(MediaServer mediaServer, String app, String stream, int timeoutSec, String path, String fileName) {
|
||||
public byte[] getSnap(MediaServer mediaServer, String app, String stream, int timeoutSec, String path, String fileName) {
|
||||
Map<String, Object> param = new HashMap<>();
|
||||
param.put("app", app);
|
||||
param.put("stream", stream);
|
||||
@ -425,8 +425,7 @@ public class ABLRESTfulUtils {
|
||||
// String url = jsonObject.getString("url");
|
||||
// sendGetForImgForUrl(url, path, fileName);
|
||||
// }
|
||||
sendGetForImg(mediaServer, "getSnap", param, path, fileName);
|
||||
|
||||
return sendGetForImg(mediaServer, "getSnap", param, path, fileName);
|
||||
}
|
||||
|
||||
public ABLResult addStreamProxy(MediaServer mediaServer, String app, String stream, String url, boolean disableAudio, boolean enableMp4, String rtpType, Integer timeout) {
|
||||
|
||||
@ -46,7 +46,7 @@ public interface IMediaNodeServerService {
|
||||
|
||||
Boolean connectRtpServer(MediaServer mediaServer, String address, int port, String app, String stream);
|
||||
|
||||
void getSnap(MediaServer mediaServer, String app, String stream, int timeoutSec, int expireSec, String path, String fileName);
|
||||
byte[] getSnap(MediaServer mediaServer, String app, String stream, int timeoutSec, int expireSec, String path, String fileName);
|
||||
|
||||
MediaInfo getMediaInfo(MediaServer mediaServer, String app, String stream);
|
||||
|
||||
|
||||
@ -83,7 +83,7 @@ public interface IMediaServerService {
|
||||
|
||||
Boolean connectRtpServer(MediaServer mediaServerItem, String address, int port, String app, String stream);
|
||||
|
||||
void getSnap(MediaServer mediaServer, String app, String stream, int timeoutSec, int expireSec, String path, String fileName);
|
||||
byte[] getSnap(MediaServer mediaServer, String app, String stream, int timeoutSec, int expireSec, String path, String fileName);
|
||||
|
||||
MediaInfo getMediaInfo(MediaServer mediaServerItem, String app, String stream);
|
||||
|
||||
|
||||
@ -634,13 +634,13 @@ public class MediaServerServiceImpl implements IMediaServerService {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void getSnap(MediaServer mediaServer, String app, String stream, int timeoutSec, int expireSec, String path, String fileName) {
|
||||
public byte[] getSnap(MediaServer mediaServer, String app, String stream, int timeoutSec, int expireSec, String path, String fileName) {
|
||||
IMediaNodeServerService mediaNodeServerService = nodeServerServiceMap.get(mediaServer.getType());
|
||||
if (mediaNodeServerService == null) {
|
||||
log.info("[getSnap] 失败, mediaServer的类型: {},未找到对应的实现类", mediaServer.getType());
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
mediaNodeServerService.getSnap(mediaServer, app, stream, timeoutSec, expireSec, path, fileName);
|
||||
return mediaNodeServerService.getSnap(mediaServer, app, stream, timeoutSec, expireSec, path, fileName);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@ -217,14 +217,14 @@ public class ZLMMediaNodeServerService implements IMediaNodeServerService {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void getSnap(MediaServer mediaServer, String app, String stream, int timeoutSec, int expireSec, String path, String fileName) {
|
||||
public byte[] getSnap(MediaServer mediaServer, String app, String stream, int timeoutSec, int expireSec, String path, String fileName) {
|
||||
String streamUrl;
|
||||
if (mediaServer.getRtspPort() != 0) {
|
||||
streamUrl = String.format("rtsp://127.0.0.1:%s/%s/%s", mediaServer.getRtspPort(), app, stream);
|
||||
} else {
|
||||
streamUrl = String.format("http://127.0.0.1:%s/%s/%s.live.mp4", mediaServer.getHttpPort(), app, stream);
|
||||
}
|
||||
zlmresTfulUtils.getSnap(mediaServer, streamUrl, timeoutSec, expireSec, path, fileName);
|
||||
return zlmresTfulUtils.getSnap(mediaServer, streamUrl, timeoutSec, expireSec, path, fileName);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@ -167,11 +167,11 @@ public class ZLMRESTfulUtils {
|
||||
return result;
|
||||
}
|
||||
|
||||
public void sendGetForImg(MediaServer mediaServer, String api, Map<String, Object> params, String targetPath, String fileName) {
|
||||
public byte[] sendGetForImg(MediaServer mediaServer, String api, Map<String, Object> params, String targetPath, String fileName) {
|
||||
String url = String.format("http://%s:%s/index/api/%s", mediaServer.getIp(), mediaServer.getHttpPort(), api);
|
||||
HttpUrl parseUrl = HttpUrl.parse(url);
|
||||
if (parseUrl == null) {
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
HttpUrl.Builder httpBuilder = parseUrl.newBuilder();
|
||||
|
||||
@ -188,6 +188,7 @@ public class ZLMRESTfulUtils {
|
||||
if (log.isDebugEnabled()){
|
||||
log.debug(request.toString());
|
||||
}
|
||||
byte[] result = null;
|
||||
try {
|
||||
OkHttpClient client = getClient();
|
||||
Response response = client.newCall(request).execute();
|
||||
@ -198,27 +199,24 @@ public class ZLMRESTfulUtils {
|
||||
if (!snapFolder.mkdirs()) {
|
||||
log.warn("{}路径创建失败", snapFolder.getAbsolutePath());
|
||||
}
|
||||
|
||||
}
|
||||
File snapFile = new File(targetPath + File.separator + fileName);
|
||||
FileOutputStream outStream = new FileOutputStream(snapFile);
|
||||
|
||||
outStream.write(Objects.requireNonNull(response.body()).bytes());
|
||||
result = Objects.requireNonNull(response.body()).bytes();
|
||||
outStream.write(result);
|
||||
outStream.flush();
|
||||
outStream.close();
|
||||
} else {
|
||||
log.error(String.format("[ %s ]请求失败: %s %s", url, response.code(), response.message()));
|
||||
}
|
||||
} else {
|
||||
log.error(String.format("[ %s ]请求失败: %s %s", url, response.code(), response.message()));
|
||||
log.error("[ {} ]请求失败: {} {}", url, response.code(), response.message());
|
||||
}
|
||||
Objects.requireNonNull(response.body()).close();
|
||||
} catch (ConnectException e) {
|
||||
log.error(String.format("连接ZLM失败: %s, %s", e.getCause().getMessage(), e.getMessage()));
|
||||
log.error("连接ZLM失败: {}, {}", e.getCause().getMessage(), e.getMessage());
|
||||
log.info("请检查media配置并确认ZLM已启动...");
|
||||
} catch (IOException e) {
|
||||
log.error(String.format("[ %s ]请求失败: %s", url, e.getMessage()));
|
||||
log.error("[ {} ]请求失败: {}", url, e.getMessage());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public ZLMResult<?> isMediaOnline(MediaServer mediaServer, String app, String stream, String schema){
|
||||
@ -657,13 +655,13 @@ public class ZLMRESTfulUtils {
|
||||
sendPost(mediaServer, "kick_sessions",param, null);
|
||||
}
|
||||
|
||||
public void getSnap(MediaServer mediaServer, String streamUrl, int timeout_sec, int expire_sec, String targetPath, String fileName) {
|
||||
public byte[] getSnap(MediaServer mediaServer, String streamUrl, int timeout_sec, int expire_sec, String targetPath, String fileName) {
|
||||
Map<String, Object> param = new HashMap<>(3);
|
||||
param.put("url", streamUrl);
|
||||
param.put("timeout_sec", timeout_sec);
|
||||
param.put("expire_sec", expire_sec);
|
||||
param.put("async", 1);
|
||||
sendGetForImg(mediaServer, "getSnap", param, targetPath, fileName);
|
||||
return sendGetForImg(mediaServer, "getSnap", param, targetPath, fileName);
|
||||
}
|
||||
|
||||
public ZLMResult<?> pauseRtpCheck(MediaServer mediaServer, String streamId) {
|
||||
|
||||
@ -3,10 +3,13 @@ package com.genersoft.iot.vmp.service.impl;
|
||||
import com.genersoft.iot.vmp.common.StreamInfo;
|
||||
import com.genersoft.iot.vmp.conf.SipConfig;
|
||||
import com.genersoft.iot.vmp.conf.UserSetting;
|
||||
import com.genersoft.iot.vmp.gb28181.bean.CommonGBChannel;
|
||||
import com.genersoft.iot.vmp.gb28181.bean.DeviceAlarmNotify;
|
||||
import com.genersoft.iot.vmp.gb28181.bean.DeviceChannel;
|
||||
import com.genersoft.iot.vmp.gb28181.event.alarm.DeviceAlarmEvent;
|
||||
import com.genersoft.iot.vmp.gb28181.service.IDeviceChannelService;
|
||||
import com.genersoft.iot.vmp.gb28181.service.IGbChannelPlayService;
|
||||
import com.genersoft.iot.vmp.gb28181.service.IGbChannelService;
|
||||
import com.genersoft.iot.vmp.service.IAlarmService;
|
||||
import com.genersoft.iot.vmp.service.bean.Alarm;
|
||||
import com.genersoft.iot.vmp.service.bean.AlarmType;
|
||||
@ -18,16 +21,20 @@ import com.github.pagehelper.PageHelper;
|
||||
import com.github.pagehelper.PageInfo;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.io.FileUtils;
|
||||
import org.springframework.context.event.EventListener;
|
||||
import org.springframework.scheduling.annotation.Async;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ConcurrentLinkedQueue;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class AlarmServiceImpl implements IAlarmService {
|
||||
@ -40,6 +47,10 @@ public class AlarmServiceImpl implements IAlarmService {
|
||||
|
||||
private final IDeviceChannelService deviceChannelService;
|
||||
|
||||
private final IGbChannelPlayService gbChannelPlayService;
|
||||
|
||||
private final IGbChannelService gbChannelService;
|
||||
|
||||
// 使用Caffeine缓存设备通道信息,避免频繁查询数据库,提升性能
|
||||
private Cache<String, DeviceChannel> channelCache = null;
|
||||
|
||||
@ -63,6 +74,7 @@ public class AlarmServiceImpl implements IAlarmService {
|
||||
if (event.getDeviceAlarmList().isEmpty()) {
|
||||
return;
|
||||
}
|
||||
log.info("收到设备报警事件,数量:{}", event.getDeviceAlarmList().size());
|
||||
for (DeviceAlarmNotify notify : event.getDeviceAlarmList()) {
|
||||
Alarm alarm = Alarm.buildFromDeviceAlarmNotify(notify);
|
||||
String key = notify.getDeviceId() + notify.getChannelId();
|
||||
@ -100,7 +112,31 @@ public class AlarmServiceImpl implements IAlarmService {
|
||||
List<Alarm> batchList = handlerCatchDataList.subList(i, end);
|
||||
alarmMapper.insertAlarms(batchList);
|
||||
}
|
||||
// 按照通道ID分组,去补充快照文件
|
||||
handlerCatchDataList.forEach(this::getSnapByAlarm);
|
||||
}
|
||||
|
||||
@Async
|
||||
public void getSnapByAlarm(Alarm alarm) {
|
||||
CommonGBChannel channel = gbChannelService.getOne(alarm.getChannelId());
|
||||
if (channel == null) {
|
||||
log.warn("未找到报警关联的通道信息,alarmId:{},channelId:{}", alarm.getId(), alarm.getChannelId());
|
||||
return;
|
||||
}
|
||||
gbChannelPlayService.getSnap(channel, (code, msg, data) -> {
|
||||
if (data == null) {
|
||||
return;
|
||||
}
|
||||
File file = new File(alarm.getSnapPath());
|
||||
if(file.exists()) {
|
||||
file.delete();
|
||||
}
|
||||
try {
|
||||
FileUtils.writeByteArrayToFile(file, data);
|
||||
} catch (Exception e) {
|
||||
log.warn("保存报警快照失败,alarmId:{},channelId:{}", alarm.getId(), alarm.getChannelId(), e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -132,7 +168,7 @@ public class AlarmServiceImpl implements IAlarmService {
|
||||
|
||||
@Override
|
||||
public String getAlarmSnapById(Long id) {
|
||||
return "";
|
||||
return alarmMapper.getSnapPathById(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@ -48,4 +48,7 @@ public interface AlarmMapper {
|
||||
"</foreach>" +
|
||||
"</script>")
|
||||
void insertAlarms(List<Alarm> handlerCatchDataList);
|
||||
|
||||
@Select("SELECT snap_path FROM wvp_alarm WHERE id = #{id}")
|
||||
String getSnapPathById(@Param("id") Long id);
|
||||
}
|
||||
|
||||
@ -39,4 +39,9 @@ public class SourcePlayServiceForStreamProxyImpl implements ISourcePlayService {
|
||||
log.error("[停止点播失败] {}({})", channel.getGbName(), channel.getGbDeviceId(), e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void getSnap(CommonGBChannel channel, ErrorCallback<byte[]> callback) {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@ -50,4 +50,9 @@ public class SourcePlayServiceForStreamPushImpl implements ISourcePlayService {
|
||||
log.error("[停止点播失败] {}({})", channel.getGbName(), channel.getGbDeviceId(), e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void getSnap(CommonGBChannel channel, ErrorCallback<byte[]> callback) {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@ -9,10 +9,17 @@ import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.io.IOUtils;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.file.Files;
|
||||
import java.util.List;
|
||||
|
||||
@Tag(name = "报警管理接口")
|
||||
@ -47,4 +54,28 @@ public class AlarmController {
|
||||
public void delete(@RequestBody List<Long> ids) {
|
||||
alarmService.deleteAlarmInfo(ids);
|
||||
}
|
||||
|
||||
@GetMapping("/snap/{id}")
|
||||
@Operation(summary = "获取报警快照图片")
|
||||
@Parameter(name = "id", description = "报警ID", required = true)
|
||||
public void snap(HttpServletResponse resp, @PathVariable Long id) {
|
||||
String snapPath = alarmService.getAlarmSnapById(id);
|
||||
if (snapPath == null || snapPath.isEmpty()) {
|
||||
resp.setStatus(HttpServletResponse.SC_NO_CONTENT);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
File file = new File(snapPath);
|
||||
if (!file.exists()) {
|
||||
resp.setStatus(HttpServletResponse.SC_NO_CONTENT);
|
||||
return;
|
||||
}
|
||||
try (InputStream in = Files.newInputStream(file.toPath())) {
|
||||
resp.setContentType(MediaType.IMAGE_JPEG_VALUE);
|
||||
IOUtils.copy(in, resp.getOutputStream());
|
||||
}
|
||||
} catch (IOException e) {
|
||||
resp.setStatus(HttpServletResponse.SC_NO_CONTENT);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -268,6 +268,8 @@ user-settings:
|
||||
# 处理报警消息时,会缓存通道数据,如果超出则丢弃低热度消息,被丢弃的通道下次使用就需要重新查询数据库,默认10000,
|
||||
# 建议根据实际情况调整,过大可能会占用较多内存,过小可能会增加数据库查询压力,
|
||||
alarm-catch-size: 10000
|
||||
# 是否使用拉流的方式获取快照,默认false,避免流量大规模消耗,开启后则使用拉流的方式获取快照
|
||||
snap-by-pull-stream: true
|
||||
|
||||
# 关闭在线文档(生产环境建议关闭)
|
||||
springdoc:
|
||||
|
||||
@ -77,6 +77,23 @@
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="快照" width="100">
|
||||
<template v-slot:default="scope">
|
||||
<el-image
|
||||
v-if="scope.row.snapPath"
|
||||
:src="getSnapUrl(scope.row.id)"
|
||||
:preview-src-list="[getSnapUrl(scope.row.id)]"
|
||||
fit="cover"
|
||||
style="width: 64px; height: 48px; cursor: pointer;"
|
||||
lazy
|
||||
>
|
||||
<div slot="error" style="width: 64px; height: 48px; line-height: 48px; text-align: center; color: #c0c4cc; font-size: 12px;">
|
||||
<i class="el-icon-picture-outline" />
|
||||
</div>
|
||||
</el-image>
|
||||
<span v-else style="color: #c0c4cc; font-size: 12px;">无</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="description" label="报警描述" show-overflow-tooltip />
|
||||
<el-table-column prop="channelName" label="通道名称" width="150" />
|
||||
<el-table-column prop="channelDeviceId" label="通道编号" width="180" />
|
||||
@ -87,8 +104,14 @@
|
||||
{{ formatTime(scope.row.alarmTime) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="100" fixed="right">
|
||||
<el-table-column label="操作" width="150" fixed="right">
|
||||
<template v-slot:default="scope">
|
||||
<el-button
|
||||
size="medium"
|
||||
icon="el-icon-video-play"
|
||||
type="text"
|
||||
@click="openPlayback(scope.row)"
|
||||
>回放</el-button>
|
||||
<el-button
|
||||
size="medium"
|
||||
icon="el-icon-delete"
|
||||
@ -110,10 +133,42 @@
|
||||
@current-change="currentChange"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 录像回放对话框 -->
|
||||
<el-dialog
|
||||
:title="playbackTitle"
|
||||
:visible.sync="playbackDialogVisible"
|
||||
width="800px"
|
||||
:before-close="closePlayback"
|
||||
destroy-on-close
|
||||
>
|
||||
<div v-if="playbackLoading" style="text-align: center; padding: 40px 0;">
|
||||
<i class="el-icon-loading" style="font-size: 32px;" />
|
||||
<div style="margin-top: 10px; color: #606266;">正在加载回放...</div>
|
||||
</div>
|
||||
<div v-else-if="playbackError" style="text-align: center; padding: 40px 0; color: #f56c6c;">
|
||||
<i class="el-icon-warning-outline" style="font-size: 32px;" />
|
||||
<div style="margin-top: 10px;">{{ playbackError }}</div>
|
||||
</div>
|
||||
<div v-else-if="playbackStreamInfo">
|
||||
<h265web
|
||||
ref="playbackPlayer"
|
||||
:video-url="playbackVideoUrl"
|
||||
:height="'400px'"
|
||||
:show-button="false"
|
||||
:has-audio="true"
|
||||
/>
|
||||
</div>
|
||||
<span slot="footer" class="dialog-footer">
|
||||
<el-button size="mini" @click="closePlayback">关闭</el-button>
|
||||
</span>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import h265web from '../common/h265web.vue'
|
||||
|
||||
const ALARM_TYPE_OPTIONS = [
|
||||
{ value: 'VideoLoss', label: '视频丢失报警' },
|
||||
{ value: 'DeviceTamper', label: '设备防拆报警' },
|
||||
@ -146,8 +201,17 @@ const ALARM_TYPE_OPTIONS = [
|
||||
{ value: 'Other', label: '其他报警' }
|
||||
]
|
||||
|
||||
function formatDatetime(ts) {
|
||||
if (!ts) return null
|
||||
const date = new Date(ts)
|
||||
const pad = n => String(n).padStart(2, '0')
|
||||
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ` +
|
||||
`${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`
|
||||
}
|
||||
|
||||
export default {
|
||||
name: 'AlarmManage',
|
||||
components: { h265web },
|
||||
data() {
|
||||
return {
|
||||
alarmList: [],
|
||||
@ -158,7 +222,15 @@ export default {
|
||||
currentPage: 1,
|
||||
count: 15,
|
||||
total: 0,
|
||||
selectedRows: []
|
||||
selectedRows: [],
|
||||
// 回放相关
|
||||
playbackDialogVisible: false,
|
||||
playbackLoading: false,
|
||||
playbackError: null,
|
||||
playbackStreamInfo: null,
|
||||
playbackVideoUrl: null,
|
||||
playbackTitle: '录像回放',
|
||||
currentPlaybackChannelId: null
|
||||
}
|
||||
},
|
||||
created() {
|
||||
@ -198,6 +270,61 @@ export default {
|
||||
console.log(error)
|
||||
})
|
||||
},
|
||||
getSnapUrl(id) {
|
||||
const baseUrl = window.baseUrl ? window.baseUrl : ''
|
||||
return ((process.env.NODE_ENV === 'development') ? process.env.VUE_APP_BASE_API : baseUrl) + `/api/alarm/snap/${id}`
|
||||
},
|
||||
openPlayback(row) {
|
||||
if (!row.channelId) {
|
||||
this.$message({ showClose: true, message: '该报警无关联通道,无法回放', type: 'warning' })
|
||||
return
|
||||
}
|
||||
this.playbackTitle = `录像回放 - ${row.channelName || row.channelDeviceId} (${this.formatTime(row.alarmTime)})`
|
||||
this.playbackDialogVisible = true
|
||||
this.playbackLoading = true
|
||||
this.playbackError = null
|
||||
this.playbackStreamInfo = null
|
||||
this.playbackVideoUrl = null
|
||||
this.currentPlaybackChannelId = row.channelId
|
||||
|
||||
// 开始时间:报警时间前30秒,结束时间:报警时间后30秒(共1分钟)
|
||||
const alarmTs = row.alarmTime
|
||||
const startTime = formatDatetime(alarmTs - 30 * 1000)
|
||||
const endTime = formatDatetime(alarmTs + 30 * 1000)
|
||||
|
||||
this.$store.dispatch('commonChanel/playback', {
|
||||
channelId: row.channelId,
|
||||
startTime: startTime,
|
||||
endTime: endTime
|
||||
}).then(data => {
|
||||
this.playbackStreamInfo = data
|
||||
if (location.protocol === 'https:') {
|
||||
this.playbackVideoUrl = data['wss_flv']
|
||||
} else {
|
||||
this.playbackVideoUrl = data['ws_flv']
|
||||
}
|
||||
this.playbackLoading = false
|
||||
}).catch(err => {
|
||||
this.playbackLoading = false
|
||||
this.playbackError = (err && err.msg) ? err.msg : '回放请求失败,请检查通道是否有该时段录像'
|
||||
console.log(err)
|
||||
})
|
||||
},
|
||||
closePlayback() {
|
||||
if (this.playbackStreamInfo && this.currentPlaybackChannelId) {
|
||||
this.$store.dispatch('commonChanel/stopPlayback', {
|
||||
channelId: this.currentPlaybackChannelId,
|
||||
stream: this.playbackStreamInfo.stream
|
||||
}).catch(err => {
|
||||
console.log(err)
|
||||
})
|
||||
}
|
||||
this.playbackDialogVisible = false
|
||||
this.playbackStreamInfo = null
|
||||
this.playbackVideoUrl = null
|
||||
this.playbackError = null
|
||||
this.currentPlaybackChannelId = null
|
||||
},
|
||||
deleteSingle(row) {
|
||||
this.$confirm('确定删除该报警记录?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
@ -244,10 +371,7 @@ export default {
|
||||
},
|
||||
formatTime(timestamp) {
|
||||
if (!timestamp) return '-'
|
||||
const date = new Date(timestamp)
|
||||
const pad = n => String(n).padStart(2, '0')
|
||||
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ` +
|
||||
`${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`
|
||||
return formatDatetime(timestamp)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user