实现报警快照获取功能,优化相关接口返回数据类型

This commit is contained in:
lin 2026-04-07 08:58:42 +08:00
parent 2d1608ca7a
commit 149c5a550b
23 changed files with 335 additions and 40 deletions

View File

@ -235,6 +235,10 @@ public class UserSetting {
*/
private long alarmCatchSize = 10000;
/**
* 是否使用拉流的方式获取快照默认false避免流量大规模消耗开启后则使用拉流的方式获取快照
*/
private boolean snapByPullStream = false;
}

View File

@ -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");

View File

@ -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);
}

View File

@ -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);

View File

@ -14,4 +14,6 @@ public interface ISourcePlayService {
void stopPlay(CommonGBChannel channel);
void getSnap(CommonGBChannel channel, ErrorCallback<byte[]> callback);
}

View File

@ -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);
}
}

View File

@ -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())) {

View File

@ -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);
}
}
}

View File

@ -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) {
}
}

View File

@ -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

View File

@ -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) {

View File

@ -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);

View File

@ -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);

View File

@ -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

View File

@ -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

View File

@ -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) {

View File

@ -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

View File

@ -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);
}

View File

@ -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) {
}
}

View File

@ -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) {
}
}

View File

@ -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);
}
}
}

View File

@ -268,6 +268,8 @@ user-settings:
# 处理报警消息时会缓存通道数据如果超出则丢弃低热度消息被丢弃的通道下次使用就需要重新查询数据库默认10000
# 建议根据实际情况调整,过大可能会占用较多内存,过小可能会增加数据库查询压力,
alarm-catch-size: 10000
# 是否使用拉流的方式获取快照默认false避免流量大规模消耗开启后则使用拉流的方式获取快照
snap-by-pull-stream: true
# 关闭在线文档(生产环境建议关闭)
springdoc:

View File

@ -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
// 30301
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)
}
}
}