Merge branch 'master' of http://49.232.154.205:10100/DeXun/rtsp-video-analysis-system
This commit is contained in:
@@ -9,12 +9,20 @@ export function listAlarm(query) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 查询报警记录详细
|
||||||
|
export function getAlarm(alarmId) {
|
||||||
|
return request({
|
||||||
|
url: '/video/alarm/' + alarmId,
|
||||||
|
method: 'get'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// 处理报警记录
|
// 处理报警记录
|
||||||
export function handleAlarm(data) {
|
export function handleAlarm(data) {
|
||||||
return request({
|
return request({
|
||||||
url: '/video/alarm/handle',
|
url: '/video/alarm/handle',
|
||||||
method: 'post',
|
method: 'put',
|
||||||
params: data
|
data: data
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -22,7 +30,16 @@ export function handleAlarm(data) {
|
|||||||
export function batchHandleAlarm(data) {
|
export function batchHandleAlarm(data) {
|
||||||
return request({
|
return request({
|
||||||
url: '/video/alarm/batchHandle',
|
url: '/video/alarm/batchHandle',
|
||||||
method: 'post',
|
method: 'put',
|
||||||
params: data
|
data: data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 导出报警记录
|
||||||
|
export function exportAlarm(query) {
|
||||||
|
return request({
|
||||||
|
url: '/video/alarm/export',
|
||||||
|
method: 'post',
|
||||||
|
params: query
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -79,7 +79,7 @@
|
|||||||
v-hasPermi="['video:alarm:export']"
|
v-hasPermi="['video:alarm:export']"
|
||||||
>导出</el-button>
|
>导出</el-button>
|
||||||
</el-col>
|
</el-col>
|
||||||
<right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
|
<right-toolbar v-model="showSearch" @queryTable="getList"></right-toolbar>
|
||||||
</el-row>
|
</el-row>
|
||||||
|
|
||||||
<el-table v-loading="loading" :data="alarmList" @selection-change="handleSelectionChange">
|
<el-table v-loading="loading" :data="alarmList" @selection-change="handleSelectionChange">
|
||||||
@@ -112,6 +112,17 @@
|
|||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
|
<el-table-column label="报警视频" align="center" prop="videoPath">
|
||||||
|
<template #default="scope">
|
||||||
|
<el-button
|
||||||
|
v-if="scope.row.videoPath"
|
||||||
|
link
|
||||||
|
type="primary"
|
||||||
|
icon="VideoPlay"
|
||||||
|
@click="handleView(scope.row)"
|
||||||
|
>播放</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
<el-table-column label="报警时间" align="center" prop="alarmTime" width="180">
|
<el-table-column label="报警时间" align="center" prop="alarmTime" width="180">
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
<span>{{ parseTime(scope.row.alarmTime, '{y}-{m}-{d} {h}:{i}:{s}') }}</span>
|
<span>{{ parseTime(scope.row.alarmTime, '{y}-{m}-{d} {h}:{i}:{s}') }}</span>
|
||||||
@@ -133,20 +144,20 @@
|
|||||||
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
|
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
|
||||||
<template #default="scope">
|
<template #default="scope">
|
||||||
<el-button
|
<el-button
|
||||||
|
v-if="scope.row.handleStatus === '0'"
|
||||||
link
|
link
|
||||||
type="success"
|
type="primary"
|
||||||
icon="Check"
|
icon="Check"
|
||||||
@click="handleProcess(scope.row, '1')"
|
@click="handleProcess(scope.row, '1')"
|
||||||
v-hasPermi="['video:alarm:handle']"
|
v-hasPermi="['video:alarm:handle']"
|
||||||
v-if="scope.row.handleStatus === '0'"
|
|
||||||
>处理</el-button>
|
>处理</el-button>
|
||||||
<el-button
|
<el-button
|
||||||
|
v-if="scope.row.handleStatus === '0'"
|
||||||
link
|
link
|
||||||
type="info"
|
type="info"
|
||||||
icon="Close"
|
icon="Close"
|
||||||
@click="handleProcess(scope.row, '2')"
|
@click="handleProcess(scope.row, '2')"
|
||||||
v-hasPermi="['video:alarm:handle']"
|
v-hasPermi="['video:alarm:handle']"
|
||||||
v-if="scope.row.handleStatus === '0'"
|
|
||||||
>忽略</el-button>
|
>忽略</el-button>
|
||||||
<el-button
|
<el-button
|
||||||
link
|
link
|
||||||
@@ -159,21 +170,17 @@
|
|||||||
</el-table>
|
</el-table>
|
||||||
|
|
||||||
<pagination
|
<pagination
|
||||||
v-show="total>0"
|
v-show="total > 0"
|
||||||
:total="total"
|
:total="total"
|
||||||
v-model:page="queryParams.pageNum"
|
:page="queryParams.pageNum"
|
||||||
v-model:limit="queryParams.pageSize"
|
:limit="queryParams.pageSize"
|
||||||
@pagination="getList"
|
@pagination="getList"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- 处理报警对话框 -->
|
<!-- 处理报警对话框 -->
|
||||||
<el-dialog :title="processTitle" v-model="processOpen" width="500px" append-to-body>
|
<el-dialog :title="processTitle" v-model="processOpen" width="500px" append-to-body>
|
||||||
<el-form ref="processRef" :model="processForm" label-width="80px">
|
<el-form ref="processRef" :model="processForm" label-width="80px">
|
||||||
<el-form-item label="处理状态">
|
<el-form-item label="处理备注" prop="handleRemark">
|
||||||
<el-tag v-if="processForm.handleStatus === '1'" type="success">已处理</el-tag>
|
|
||||||
<el-tag v-else-if="processForm.handleStatus === '2'" type="info">已忽略</el-tag>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="处理备注">
|
|
||||||
<el-input
|
<el-input
|
||||||
v-model="processForm.handleRemark"
|
v-model="processForm.handleRemark"
|
||||||
type="textarea"
|
type="textarea"
|
||||||
@@ -190,7 +197,7 @@
|
|||||||
</template>
|
</template>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
|
|
||||||
<!-- 查看报警详情对话框 -->
|
<!-- 查看详情对话框 -->
|
||||||
<el-dialog title="报警详情" v-model="viewOpen" width="800px" append-to-body>
|
<el-dialog title="报警详情" v-model="viewOpen" width="800px" append-to-body>
|
||||||
<el-descriptions :column="2" border>
|
<el-descriptions :column="2" border>
|
||||||
<el-descriptions-item label="报警ID">{{ viewForm.alarmId }}</el-descriptions-item>
|
<el-descriptions-item label="报警ID">{{ viewForm.alarmId }}</el-descriptions-item>
|
||||||
@@ -203,16 +210,20 @@
|
|||||||
<el-tag v-else-if="viewForm.alarmLevel === '3'" type="danger">高</el-tag>
|
<el-tag v-else-if="viewForm.alarmLevel === '3'" type="danger">高</el-tag>
|
||||||
</el-descriptions-item>
|
</el-descriptions-item>
|
||||||
<el-descriptions-item label="置信度">{{ (viewForm.confidence * 100).toFixed(1) }}%</el-descriptions-item>
|
<el-descriptions-item label="置信度">{{ (viewForm.confidence * 100).toFixed(1) }}%</el-descriptions-item>
|
||||||
<el-descriptions-item label="报警时间" :span="2">{{ parseTime(viewForm.alarmTime, '{y}-{m}-{d} {h}:{i}:{s}') }}</el-descriptions-item>
|
<el-descriptions-item label="报警时间" :span="2">
|
||||||
|
{{ parseTime(viewForm.alarmTime, '{y}-{m}-{d} {h}:{i}:{s}') }}
|
||||||
|
</el-descriptions-item>
|
||||||
<el-descriptions-item label="报警描述" :span="2">{{ viewForm.alarmDesc }}</el-descriptions-item>
|
<el-descriptions-item label="报警描述" :span="2">{{ viewForm.alarmDesc }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="处理状态">
|
<el-descriptions-item label="处理状态">
|
||||||
<el-tag v-if="viewForm.handleStatus === '0'" type="danger">未处理</el-tag>
|
<el-tag v-if="viewForm.handleStatus === '0'" type="danger">未处理</el-tag>
|
||||||
<el-tag v-else-if="viewForm.handleStatus === '1'" type="success">已处理</el-tag>
|
<el-tag v-else-if="viewForm.handleStatus === '1'" type="success">已处理</el-tag>
|
||||||
<el-tag v-else-if="viewForm.handleStatus === '2'" type="info">已忽略</el-tag>
|
<el-tag v-else-if="viewForm.handleStatus === '2'" type="info">已忽略</el-tag>
|
||||||
</el-descriptions-item>
|
</el-descriptions-item>
|
||||||
<el-descriptions-item label="处理人">{{ viewForm.handleBy || '无' }}</el-descriptions-item>
|
<el-descriptions-item label="处理人">{{ viewForm.handleBy || '-' }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="处理时间" :span="2">{{ parseTime(viewForm.handleTime, '{y}-{m}-{d} {h}:{i}:{s}') || '无' }}</el-descriptions-item>
|
<el-descriptions-item label="处理时间" :span="2">
|
||||||
<el-descriptions-item label="处理备注" :span="2">{{ viewForm.handleRemark || '无' }}</el-descriptions-item>
|
{{ viewForm.handleTime ? parseTime(viewForm.handleTime, '{y}-{m}-{d} {h}:{i}:{s}') : '-' }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="处理备注" :span="2">{{ viewForm.handleRemark || '-' }}</el-descriptions-item>
|
||||||
</el-descriptions>
|
</el-descriptions>
|
||||||
|
|
||||||
<div v-if="viewForm.imagePath" style="margin-top: 20px;">
|
<div v-if="viewForm.imagePath" style="margin-top: 20px;">
|
||||||
@@ -224,6 +235,15 @@
|
|||||||
fit="contain"
|
fit="contain"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div v-if="viewForm.videoPath" style="margin-top: 20px;">
|
||||||
|
<h4>报警视频:</h4>
|
||||||
|
<video
|
||||||
|
:src="viewForm.videoPath"
|
||||||
|
style="width: 100%; max-height: 420px; background: #000"
|
||||||
|
controls
|
||||||
|
preload="metadata"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -99,6 +99,10 @@
|
|||||||
<groupId>com.ruoyi</groupId>
|
<groupId>com.ruoyi</groupId>
|
||||||
<artifactId>ruoyi-common</artifactId>
|
<artifactId>ruoyi-common</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.ruoyi</groupId>
|
||||||
|
<artifactId>ruoyi-framework</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
|
|||||||
@@ -28,7 +28,6 @@ public class ModelController extends BaseController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** 新增模型(JSON) */
|
/** 新增模型(JSON) */
|
||||||
@PreAuthorize("@ss.hasPermi('video:model:add')")
|
|
||||||
@PostMapping
|
@PostMapping
|
||||||
public AjaxResult create(@RequestBody VModel model) {
|
public AjaxResult create(@RequestBody VModel model) {
|
||||||
if (StringUtils.isEmpty(model.getModelName())) {
|
if (StringUtils.isEmpty(model.getModelName())) {
|
||||||
@@ -44,8 +43,23 @@ public class ModelController extends BaseController {
|
|||||||
return rows > 0 ? AjaxResult.success(model) : AjaxResult.error("新增失败");
|
return rows > 0 ? AjaxResult.success(model) : AjaxResult.error("新增失败");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 修改模型 */
|
||||||
|
@PutMapping
|
||||||
|
public AjaxResult update(@RequestBody VModel model) {
|
||||||
|
if (model.getModelId() == null) {
|
||||||
|
return AjaxResult.error("模型ID不能为空");
|
||||||
|
}
|
||||||
|
if (StringUtils.isEmpty(model.getModelName())) {
|
||||||
|
return AjaxResult.error("模型名称不能为空");
|
||||||
|
}
|
||||||
|
if (StringUtils.isEmpty(model.getFramework())) {
|
||||||
|
model.setFramework("onnx");
|
||||||
|
}
|
||||||
|
int rows = modelService.update(model);
|
||||||
|
return rows > 0 ? AjaxResult.success("修改成功") : AjaxResult.error("修改失败");
|
||||||
|
}
|
||||||
|
|
||||||
/** 根据ID查询 */
|
/** 根据ID查询 */
|
||||||
@PreAuthorize("@ss.hasPermi('video:model:query')")
|
|
||||||
@GetMapping("/{id}")
|
@GetMapping("/{id}")
|
||||||
public AjaxResult get(@PathVariable("id") Long id) {
|
public AjaxResult get(@PathVariable("id") Long id) {
|
||||||
VModel model = modelService.selectById(id);
|
VModel model = modelService.selectById(id);
|
||||||
@@ -53,7 +67,6 @@ public class ModelController extends BaseController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** 列表查询(可选条件) */
|
/** 列表查询(可选条件) */
|
||||||
@PreAuthorize("@ss.hasPermi('video:model:list')")
|
|
||||||
@GetMapping("/list")
|
@GetMapping("/list")
|
||||||
public TableDataInfo list(@RequestParam(value = "modelName", required = false) String modelName,
|
public TableDataInfo list(@RequestParam(value = "modelName", required = false) String modelName,
|
||||||
@RequestParam(value = "framework", required = false) String framework,
|
@RequestParam(value = "framework", required = false) String framework,
|
||||||
@@ -70,7 +83,6 @@ public class ModelController extends BaseController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** 删除 */
|
/** 删除 */
|
||||||
@PreAuthorize("@ss.hasPermi('video:model:remove')")
|
|
||||||
@DeleteMapping("/{id}")
|
@DeleteMapping("/{id}")
|
||||||
public AjaxResult delete(@PathVariable("id") Long id) {
|
public AjaxResult delete(@PathVariable("id") Long id) {
|
||||||
int rows = modelService.deleteById(id);
|
int rows = modelService.deleteById(id);
|
||||||
@@ -78,7 +90,6 @@ public class ModelController extends BaseController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** 启用/禁用 */
|
/** 启用/禁用 */
|
||||||
@PreAuthorize("@ss.hasPermi('video:model:edit')")
|
|
||||||
@PutMapping("/{id}/enable")
|
@PutMapping("/{id}/enable")
|
||||||
public AjaxResult enable(@PathVariable("id") Long id,
|
public AjaxResult enable(@PathVariable("id") Long id,
|
||||||
@RequestParam("enabled") Integer enabled) {
|
@RequestParam("enabled") Integer enabled) {
|
||||||
@@ -90,7 +101,6 @@ public class ModelController extends BaseController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** 下载:直接 302 重定向到模型URL,确保可点击下载 */
|
/** 下载:直接 302 重定向到模型URL,确保可点击下载 */
|
||||||
@PreAuthorize("@ss.hasPermi('video:model:download')")
|
|
||||||
@GetMapping("/download/{id}")
|
@GetMapping("/download/{id}")
|
||||||
public void download(@PathVariable("id") Long id, HttpServletResponse response) throws IOException {
|
public void download(@PathVariable("id") Long id, HttpServletResponse response) throws IOException {
|
||||||
VModel model = modelService.selectById(id);
|
VModel model = modelService.selectById(id);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
package com.ruoyi.web.controller.system;
|
package com.ruoyi.video.controller;
|
||||||
|
|
||||||
import com.ruoyi.common.core.domain.AjaxResult;
|
import com.ruoyi.common.core.domain.AjaxResult;
|
||||||
import com.ruoyi.common.utils.StringUtils;
|
import com.ruoyi.common.utils.StringUtils;
|
||||||
@@ -9,6 +9,7 @@ import java.util.Map;
|
|||||||
public interface VModelMapper {
|
public interface VModelMapper {
|
||||||
int insertModel(VModel model);
|
int insertModel(VModel model);
|
||||||
VModel selectModelById(@Param("id") Long id);
|
VModel selectModelById(@Param("id") Long id);
|
||||||
|
int updateModel(VModel model);
|
||||||
int deleteModelById(@Param("id") Long id);
|
int deleteModelById(@Param("id") Long id);
|
||||||
int updateEnabled(@Param("id") Long id, @Param("enabled") Integer enabled);
|
int updateEnabled(@Param("id") Long id, @Param("enabled") Integer enabled);
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import java.util.Map;
|
|||||||
public interface IVModelService {
|
public interface IVModelService {
|
||||||
int insert(VModel model);
|
int insert(VModel model);
|
||||||
VModel selectById(Long id);
|
VModel selectById(Long id);
|
||||||
|
int update(VModel model);
|
||||||
int deleteById(Long id);
|
int deleteById(Long id);
|
||||||
int updateEnabled(Long id, Integer enabled);
|
int updateEnabled(Long id, Integer enabled);
|
||||||
List<VModel> selectList(Map<String, Object> params);
|
List<VModel> selectList(Map<String, Object> params);
|
||||||
|
|||||||
@@ -11,6 +11,14 @@ import com.ruoyi.video.service.InspectionTaskService;
|
|||||||
import com.ruoyi.video.thread.MediaTransferFlvByJavacv;
|
import com.ruoyi.video.thread.MediaTransferFlvByJavacv;
|
||||||
import com.ruoyi.video.common.ModelManager;
|
import com.ruoyi.video.common.ModelManager;
|
||||||
import com.ruoyi.video.thread.detector.YoloDetector;
|
import com.ruoyi.video.thread.detector.YoloDetector;
|
||||||
|
import com.ruoyi.common.utils.spring.SpringUtils;
|
||||||
|
import com.ruoyi.framework.service.MinioService;
|
||||||
|
import com.ruoyi.video.service.IVMinioObjectService;
|
||||||
|
import com.ruoyi.video.domain.VMinioObject;
|
||||||
|
import com.ruoyi.video.utils.CustomMultipartFile;
|
||||||
|
import com.ruoyi.video.utils.Overlay;
|
||||||
|
import org.bytedeco.javacv.FFmpegFrameRecorder;
|
||||||
|
import org.bytedeco.javacv.Java2DFrameConverter;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.bytedeco.javacv.Frame;
|
import org.bytedeco.javacv.Frame;
|
||||||
import org.bytedeco.javacv.FrameGrabber;
|
import org.bytedeco.javacv.FrameGrabber;
|
||||||
@@ -177,6 +185,8 @@ public class InspectionTaskServiceImpl implements InspectionTaskService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
FFmpegFrameGrabber grabber = null;
|
FFmpegFrameGrabber grabber = null;
|
||||||
|
FFmpegFrameRecorder sessionRecorder = null;
|
||||||
|
File sessionVideoTmp = null;
|
||||||
try {
|
try {
|
||||||
// 初始化模型管理器
|
// 初始化模型管理器
|
||||||
if (modelManager == null) {
|
if (modelManager == null) {
|
||||||
@@ -201,12 +211,32 @@ public class InspectionTaskServiceImpl implements InspectionTaskService {
|
|||||||
int frameCount = 0;
|
int frameCount = 0;
|
||||||
List<Detection> allDetections = new ArrayList<>();
|
List<Detection> allDetections = new ArrayList<>();
|
||||||
|
|
||||||
|
// 会话聚合参数与状态
|
||||||
|
final long minGapMs = 3000L; // 目标消失超过该值视为结束
|
||||||
|
final long maxDurationMs = 30000L; // 单次会话最长30s
|
||||||
|
final float detectionThreshold = task.getThreshold() != null ? task.getThreshold().floatValue() : 0.7f;
|
||||||
|
|
||||||
|
boolean sessionActive = false;
|
||||||
|
long sessionStartMs = 0L;
|
||||||
|
long lastSeenMs = 0L;
|
||||||
|
sessionRecorder = null;
|
||||||
|
sessionVideoTmp = null;
|
||||||
|
Long currentAlarmId = null;
|
||||||
|
|
||||||
while (System.currentTimeMillis() - startTime < duration) {
|
while (System.currentTimeMillis() - startTime < duration) {
|
||||||
Frame frame = grabber.grabImage();
|
Frame frame = grabber.grabImage();
|
||||||
if (frame == null) continue;
|
if (frame == null) continue;
|
||||||
|
|
||||||
frameCount++;
|
frameCount++;
|
||||||
|
|
||||||
|
// 录像帧追加
|
||||||
|
if (sessionActive && sessionRecorder != null) {
|
||||||
|
try {
|
||||||
|
sessionRecorder.record(frame);
|
||||||
|
} catch (Exception ignore) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 每隔一定帧数进行一次检测(避免过于频繁)
|
// 每隔一定帧数进行一次检测(避免过于频繁)
|
||||||
if (frameCount % 25 == 0) { // 假设25fps,每秒检测一次
|
if (frameCount % 25 == 0) { // 假设25fps,每秒检测一次
|
||||||
try {
|
try {
|
||||||
@@ -220,10 +250,166 @@ public class InspectionTaskServiceImpl implements InspectionTaskService {
|
|||||||
List<Detection> detections = performDetection(mat);
|
List<Detection> detections = performDetection(mat);
|
||||||
allDetections.addAll(detections);
|
allDetections.addAll(detections);
|
||||||
|
|
||||||
// 如果检测到异常,立即保存图片并记录报警
|
// 会话聚合:阈值触发开始,目标消失/超过30s结束;开始时叠框截图+告警,结束后上传视频并回填
|
||||||
if (!detections.isEmpty()) {
|
long now = System.currentTimeMillis();
|
||||||
String imagePath = saveFrameAsImage(frame, task.getTaskId());
|
|
||||||
handleDetectionResults(task.getTaskId(), detections, imagePath);
|
// 叠框,便于生成可核验证据图
|
||||||
|
try {
|
||||||
|
if (detections != null && !detections.isEmpty() && mat != null && !mat.empty()) {
|
||||||
|
Overlay.draw(detections, mat);
|
||||||
|
}
|
||||||
|
} catch (Exception ignore) {
|
||||||
|
}
|
||||||
|
|
||||||
|
// 最高置信度
|
||||||
|
Detection best = detections.stream()
|
||||||
|
.max(Comparator.comparingDouble(Detection::conf))
|
||||||
|
.orElse(null);
|
||||||
|
boolean hasTarget = best != null && best.conf() >= detectionThreshold;
|
||||||
|
|
||||||
|
if (hasTarget) {
|
||||||
|
lastSeenMs = now;
|
||||||
|
if (!sessionActive) {
|
||||||
|
// 1) 叠框后截图上传 + 插入告警(写 image_path)
|
||||||
|
try {
|
||||||
|
BufferedImage img = null;
|
||||||
|
try {
|
||||||
|
Java2DFrameConverter conv = new Java2DFrameConverter();
|
||||||
|
if (mat != null && !mat.empty()) {
|
||||||
|
img = conv.convert(new OpenCVFrameConverter.ToMat().convert(mat));
|
||||||
|
}
|
||||||
|
if (img == null) {
|
||||||
|
img = conv.convert(frame);
|
||||||
|
}
|
||||||
|
} catch (Exception ___) {
|
||||||
|
}
|
||||||
|
|
||||||
|
if (img != null) {
|
||||||
|
File tmp = File.createTempFile("snapshot_", ".jpg");
|
||||||
|
ImageIO.write(img, "jpg", tmp);
|
||||||
|
|
||||||
|
String originalName = "task_" + task.getTaskId() + "_" + now + ".jpg";
|
||||||
|
String uniqueObjectName = UUID.randomUUID().toString().replace("-", "") + ".jpg";
|
||||||
|
MinioService minio = SpringUtils.getBean(MinioService.class);
|
||||||
|
IVMinioObjectService objSvc = SpringUtils.getBean(IVMinioObjectService.class);
|
||||||
|
MinioService.UploadResult up = minio.uploadWithName(new CustomMultipartFile(tmp, "image/jpeg"), uniqueObjectName);
|
||||||
|
|
||||||
|
VMinioObject rec = new VMinioObject();
|
||||||
|
rec.setObjectName(up.getObjectName());
|
||||||
|
rec.setUrl(up.getUrl());
|
||||||
|
rec.setOriginalName(originalName);
|
||||||
|
objSvc.insert(rec);
|
||||||
|
|
||||||
|
AlarmRecord alarm = new AlarmRecord();
|
||||||
|
alarm.setTaskId(task.getTaskId());
|
||||||
|
alarm.setTaskName(task.getTaskName());
|
||||||
|
alarm.setDeviceId(task.getDeviceId());
|
||||||
|
alarm.setDeviceName(task.getDeviceName());
|
||||||
|
alarm.setAlarmType(best.cls());
|
||||||
|
alarm.setAlarmLevel(getAlarmLevel(best.conf()));
|
||||||
|
alarm.setAlarmDesc(String.format("检测到%s,置信度: %.2f", best.cls(), best.conf()));
|
||||||
|
alarm.setConfidence((double) best.conf());
|
||||||
|
alarm.setImagePath(up.getUrl());
|
||||||
|
alarm.setAlarmTime(new Date(now));
|
||||||
|
alarm.setHandleStatus("0");
|
||||||
|
alarm.setCreateBy(SecurityUtils.getUsername());
|
||||||
|
saveAlarmRecord(alarm);
|
||||||
|
currentAlarmId = alarm.getAlarmId();
|
||||||
|
|
||||||
|
try {
|
||||||
|
tmp.delete();
|
||||||
|
} catch (Exception ignore) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception ee) {
|
||||||
|
log.warn("巡检会话-保存截图失败: {}", ee.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2) 启动会话录像(mp4)
|
||||||
|
try {
|
||||||
|
sessionVideoTmp = File.createTempFile("evidence_", ".mp4");
|
||||||
|
sessionRecorder = new FFmpegFrameRecorder(
|
||||||
|
sessionVideoTmp,
|
||||||
|
grabber.getImageWidth(),
|
||||||
|
grabber.getImageHeight(),
|
||||||
|
grabber.getAudioChannels()
|
||||||
|
);
|
||||||
|
sessionRecorder.setFormat("mp4");
|
||||||
|
sessionRecorder.setInterleaved(true);
|
||||||
|
sessionRecorder.setVideoOption("preset", "ultrafast");
|
||||||
|
sessionRecorder.setVideoOption("tune", "zerolatency");
|
||||||
|
sessionRecorder.setFrameRate(25);
|
||||||
|
sessionRecorder.setGopSize(25);
|
||||||
|
sessionRecorder.start();
|
||||||
|
} catch (Exception sre) {
|
||||||
|
log.warn("巡检会话-录像器启动失败: {}", sre.getMessage());
|
||||||
|
sessionRecorder = null;
|
||||||
|
sessionVideoTmp = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
sessionActive = true;
|
||||||
|
sessionStartMs = now;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 会话结束判定:目标消失超过阈值或会话超过最大时长
|
||||||
|
if (sessionActive) {
|
||||||
|
boolean gapTimeout = (now - lastSeenMs) >= minGapMs;
|
||||||
|
boolean maxed = (now - sessionStartMs) >= maxDurationMs;
|
||||||
|
if (gapTimeout || maxed) {
|
||||||
|
File local = sessionVideoTmp;
|
||||||
|
FFmpegFrameRecorder rec = sessionRecorder;
|
||||||
|
sessionVideoTmp = null;
|
||||||
|
sessionRecorder = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (rec != null) {
|
||||||
|
try {
|
||||||
|
rec.stop();
|
||||||
|
} catch (Exception ignore) {
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
rec.release();
|
||||||
|
} catch (Exception ignore) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception se) {
|
||||||
|
log.debug("巡检会话-停止录像异常: {}", se.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (local != null && local.exists() && currentAlarmId != null) {
|
||||||
|
try {
|
||||||
|
MinioService minio = SpringUtils.getBean(MinioService.class);
|
||||||
|
IVMinioObjectService objSvc = SpringUtils.getBean(IVMinioObjectService.class);
|
||||||
|
String originalName = "evidence_" + now + ".mp4";
|
||||||
|
String uniqueName = UUID.randomUUID().toString().replace("-", "") + ".mp4";
|
||||||
|
MinioService.UploadResult upv = minio.uploadWithName(new CustomMultipartFile(local, "video/mp4"), uniqueName);
|
||||||
|
|
||||||
|
VMinioObject vrec = new VMinioObject();
|
||||||
|
vrec.setObjectName(upv.getObjectName());
|
||||||
|
vrec.setUrl(upv.getUrl());
|
||||||
|
vrec.setOriginalName(originalName);
|
||||||
|
objSvc.insert(vrec);
|
||||||
|
|
||||||
|
AlarmRecord patch = new AlarmRecord();
|
||||||
|
patch.setAlarmId(currentAlarmId);
|
||||||
|
patch.setVideoPath(upv.getUrl());
|
||||||
|
alarmRecordMapper.updateAlarmRecord(patch);
|
||||||
|
} catch (Exception ue) {
|
||||||
|
log.warn("巡检会话-上传/回填视频失败: {}", ue.getMessage());
|
||||||
|
} finally {
|
||||||
|
try {
|
||||||
|
local.delete();
|
||||||
|
} catch (Exception ignore) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sessionActive = false;
|
||||||
|
sessionStartMs = 0L;
|
||||||
|
lastSeenMs = 0L;
|
||||||
|
currentAlarmId = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
@@ -240,6 +426,31 @@ public class InspectionTaskServiceImpl implements InspectionTaskService {
|
|||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("视频分析失败: {}", e.getMessage(), e);
|
log.error("视频分析失败: {}", e.getMessage(), e);
|
||||||
} finally {
|
} finally {
|
||||||
|
// 兜底清理会话资源(缓存并置空,避免并发/重复close问题)
|
||||||
|
FFmpegFrameRecorder rec = sessionRecorder;
|
||||||
|
File local = sessionVideoTmp;
|
||||||
|
sessionRecorder = null;
|
||||||
|
sessionVideoTmp = null;
|
||||||
|
try {
|
||||||
|
if (rec != null) {
|
||||||
|
try {
|
||||||
|
rec.stop();
|
||||||
|
} catch (Exception ignore) {
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
rec.release();
|
||||||
|
} catch (Exception ignore) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception ignore) {
|
||||||
|
}
|
||||||
|
if (local != null) {
|
||||||
|
try {
|
||||||
|
local.delete();
|
||||||
|
} catch (Exception ignore) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (grabber != null) {
|
if (grabber != null) {
|
||||||
try {
|
try {
|
||||||
grabber.stop();
|
grabber.stop();
|
||||||
@@ -285,7 +496,7 @@ public class InspectionTaskServiceImpl implements InspectionTaskService {
|
|||||||
String filePath = saveDir + "/" + fileName;
|
String filePath = saveDir + "/" + fileName;
|
||||||
|
|
||||||
// 转换并保存图片
|
// 转换并保存图片
|
||||||
BufferedImage bufferedImage = new org.bytedeco.javacv.Java2DFrameConverter().convert(frame);
|
BufferedImage bufferedImage = new Java2DFrameConverter().convert(frame);
|
||||||
ImageIO.write(bufferedImage, "jpg", new File(filePath));
|
ImageIO.write(bufferedImage, "jpg", new File(filePath));
|
||||||
|
|
||||||
return filePath;
|
return filePath;
|
||||||
@@ -315,7 +526,7 @@ public class InspectionTaskServiceImpl implements InspectionTaskService {
|
|||||||
alarmRecord.setAlarmLevel(getAlarmLevel(detection.conf()));
|
alarmRecord.setAlarmLevel(getAlarmLevel(detection.conf()));
|
||||||
alarmRecord.setAlarmDesc(String.format("检测到%s,置信度: %.2f",
|
alarmRecord.setAlarmDesc(String.format("检测到%s,置信度: %.2f",
|
||||||
detection.cls(), detection.conf()));
|
detection.cls(), detection.conf()));
|
||||||
alarmRecord.setConfidence((double)detection.conf());
|
alarmRecord.setConfidence((double) detection.conf());
|
||||||
alarmRecord.setImagePath(imagePath);
|
alarmRecord.setImagePath(imagePath);
|
||||||
alarmRecord.setAlarmTime(new Date());
|
alarmRecord.setAlarmTime(new Date());
|
||||||
alarmRecord.setHandleStatus("0"); // 未处理
|
alarmRecord.setHandleStatus("0"); // 未处理
|
||||||
|
|||||||
@@ -28,6 +28,11 @@ public class VModelServiceImpl implements IVModelService {
|
|||||||
return mapper.selectModelById(id);
|
return mapper.selectModelById(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int update(VModel model) {
|
||||||
|
return mapper.updateModel(model);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int deleteById(Long id) {
|
public int deleteById(Long id) {
|
||||||
return mapper.deleteModelById(id);
|
return mapper.deleteModelById(id);
|
||||||
|
|||||||
@@ -8,6 +8,13 @@ import com.ruoyi.video.service.MediaService;
|
|||||||
import com.ruoyi.video.thread.detector.CompositeDetector;
|
import com.ruoyi.video.thread.detector.CompositeDetector;
|
||||||
import com.ruoyi.video.thread.detector.YoloDetector;
|
import com.ruoyi.video.thread.detector.YoloDetector;
|
||||||
import com.ruoyi.video.utils.Overlay;
|
import com.ruoyi.video.utils.Overlay;
|
||||||
|
import com.ruoyi.common.utils.spring.SpringUtils;
|
||||||
|
import com.ruoyi.framework.service.MinioService;
|
||||||
|
import com.ruoyi.video.service.IVMinioObjectService;
|
||||||
|
import com.ruoyi.video.domain.VMinioObject;
|
||||||
|
import com.ruoyi.video.domain.AlarmRecord;
|
||||||
|
import com.ruoyi.video.mapper.AlarmRecordMapper;
|
||||||
|
import com.ruoyi.video.utils.CustomMultipartFile;
|
||||||
import io.netty.buffer.Unpooled;
|
import io.netty.buffer.Unpooled;
|
||||||
import io.netty.channel.ChannelFuture;
|
import io.netty.channel.ChannelFuture;
|
||||||
import io.netty.channel.ChannelHandlerContext;
|
import io.netty.channel.ChannelHandlerContext;
|
||||||
@@ -24,14 +31,21 @@ import org.bytedeco.opencv.opencv_core.Mat;
|
|||||||
import org.springframework.util.CollectionUtils;
|
import org.springframework.util.CollectionUtils;
|
||||||
|
|
||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
|
import java.util.Date;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
import java.util.concurrent.atomic.AtomicReference;
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
import java.util.concurrent.locks.LockSupport;
|
import java.util.concurrent.locks.LockSupport;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
import javax.imageio.ImageIO;
|
||||||
|
import java.awt.image.BufferedImage;
|
||||||
|
|
||||||
import static org.bytedeco.opencv.global.opencv_core.CV_8UC3;
|
import static org.bytedeco.opencv.global.opencv_core.CV_8UC3;
|
||||||
|
|
||||||
@@ -57,6 +71,200 @@ public class MediaTransferFlvByJavacv extends MediaTransfer implements Runnable
|
|||||||
void onWindowFinished(Long jobId, Long deviceId, WindowStats stats);
|
void onWindowFinished(Long jobId, Long deviceId, WindowStats stats);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 启动会话视频录制(本地mp4临时文件,结束后上传MinIO) */
|
||||||
|
private void startSessionVideoRecorder() {
|
||||||
|
try {
|
||||||
|
// 以系统临时目录创建mp4
|
||||||
|
sessionVideoTmp = File.createTempFile("evidence_", ".mp4");
|
||||||
|
int w = (grabber != null) ? grabber.getImageWidth() : 1280;
|
||||||
|
int h = (grabber != null) ? grabber.getImageHeight() : 720;
|
||||||
|
int ac = (grabber != null) ? grabber.getAudioChannels() : 0;
|
||||||
|
|
||||||
|
sessionRecorder = new FFmpegFrameRecorder(sessionVideoTmp, w, h, ac);
|
||||||
|
sessionRecorder.setFormat("mp4");
|
||||||
|
sessionRecorder.setInterleaved(true);
|
||||||
|
sessionRecorder.setVideoOption("preset", "ultrafast");
|
||||||
|
sessionRecorder.setVideoOption("tune", "zerolatency");
|
||||||
|
sessionRecorder.setFrameRate(25);
|
||||||
|
sessionRecorder.setGopSize(25);
|
||||||
|
sessionRecorder.setVideoCodec(avcodec.AV_CODEC_ID_H264);
|
||||||
|
sessionRecorder.setPixelFormat(avutil.AV_PIX_FMT_YUV420P);
|
||||||
|
if (ac > 0) sessionRecorder.setAudioCodec(avcodec.AV_CODEC_ID_AAC);
|
||||||
|
sessionRecorder.start();
|
||||||
|
log.debug("会话录像器启动: {}", sessionVideoTmp);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("会话录像器启动失败: {}", e.getMessage());
|
||||||
|
try { if (sessionRecorder != null) sessionRecorder.close(); } catch (Exception ignore) {}
|
||||||
|
sessionRecorder = null;
|
||||||
|
sessionVideoTmp = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 结束会话录像并上传到 MinIO,然后更新 AlarmRecord.videoPath */
|
||||||
|
private void stopAndUploadSessionVideo(long nowMs) {
|
||||||
|
File local = sessionVideoTmp;
|
||||||
|
FFmpegFrameRecorder rec = sessionRecorder;
|
||||||
|
sessionVideoTmp = null;
|
||||||
|
sessionRecorder = null;
|
||||||
|
try {
|
||||||
|
if (rec != null) {
|
||||||
|
try { rec.stop(); } catch (Exception ignore) {}
|
||||||
|
try { rec.release(); } catch (Exception ignore) {}
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.debug("会话录像停止异常: {}", e.getMessage());
|
||||||
|
}
|
||||||
|
if (local == null || !local.exists()) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 上传
|
||||||
|
String originalName = buildVideoOriginalName(nowMs);
|
||||||
|
String uniqueObjectName = buildUniqueVideoObjectName(originalName);
|
||||||
|
MinioService minio = SpringUtils.getBean(MinioService.class);
|
||||||
|
IVMinioObjectService objSvc = SpringUtils.getBean(IVMinioObjectService.class);
|
||||||
|
AlarmRecordMapper alarmMapper = SpringUtils.getBean(AlarmRecordMapper.class);
|
||||||
|
|
||||||
|
CustomMultipartFile mf = new CustomMultipartFile(local, "video/mp4");
|
||||||
|
MinioService.UploadResult up = minio.uploadWithName(mf, uniqueObjectName);
|
||||||
|
|
||||||
|
// 入库 v_minio_object
|
||||||
|
VMinioObject recObj = new VMinioObject();
|
||||||
|
recObj.setObjectName(up.getObjectName());
|
||||||
|
recObj.setUrl(up.getUrl());
|
||||||
|
recObj.setOriginalName(originalName);
|
||||||
|
objSvc.insert(recObj);
|
||||||
|
|
||||||
|
// 回填 AlarmRecord 的 videoPath
|
||||||
|
if (currentAlarmId != null) {
|
||||||
|
AlarmRecord patch = new AlarmRecord();
|
||||||
|
patch.setAlarmId(currentAlarmId);
|
||||||
|
patch.setVideoPath(up.getUrl());
|
||||||
|
alarmMapper.updateAlarmRecord(patch);
|
||||||
|
log.info("会话视频上传并回填告警: alarmId={}, url={}", currentAlarmId, up.getUrl());
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("上传/回填会话视频失败: {}", e.getMessage());
|
||||||
|
} finally {
|
||||||
|
try { local.delete(); } catch (Exception ignore) {}
|
||||||
|
currentAlarmId = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String buildVideoOriginalName(long nowMs) {
|
||||||
|
return "evidence_" + nowMs + ".mp4";
|
||||||
|
}
|
||||||
|
|
||||||
|
private String buildUniqueVideoObjectName(String original) {
|
||||||
|
String ext = ".mp4";
|
||||||
|
int i = original.lastIndexOf('.');
|
||||||
|
if (i > 0) ext = original.substring(i);
|
||||||
|
return UUID.randomUUID().toString().replace("-", "") + ext;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 外部可调整置信度阈值 */
|
||||||
|
public void setDetectionThreshold(float threshold) {
|
||||||
|
this.detectionThreshold = threshold;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存一张叠好框的截图到本地 → 上传到 MinIO → v_minio_object 入库 → 写入一条报警记录。
|
||||||
|
*/
|
||||||
|
private void saveSnapshotAndCreateAlarm(List<Detection> detections, long nowMs) {
|
||||||
|
// 拿到叠好框的帧
|
||||||
|
Mat annotated = getLatestAnnotatedFrameCopy();
|
||||||
|
if (annotated == null || annotated.empty()) {
|
||||||
|
log.debug("没有可用的叠框帧,跳过截图保存");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
// 置信度阈值过滤(取最高分作为本次代表)
|
||||||
|
Detection best = detections.stream()
|
||||||
|
.max((a,b) -> Float.compare(a.conf(), b.conf()))
|
||||||
|
.orElse(null);
|
||||||
|
if (best == null || best.conf() < detectionThreshold) {
|
||||||
|
log.debug("最高置信度 {} 低于阈值 {},跳过落库与上传", best == null ? 0f : best.conf(), detectionThreshold);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 转为 BufferedImage
|
||||||
|
Java2DFrameConverter conv = new Java2DFrameConverter();
|
||||||
|
OpenCVFrameConverter.ToMat toMat = new OpenCVFrameConverter.ToMat();
|
||||||
|
Frame f = toMat.convert(annotated);
|
||||||
|
BufferedImage img = conv.convert(f);
|
||||||
|
if (img == null) {
|
||||||
|
log.debug("帧转图片失败");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 写到临时文件
|
||||||
|
String originalName = buildSnapshotOriginalName(nowMs);
|
||||||
|
File tmp = File.createTempFile("snapshot_", ".jpg");
|
||||||
|
ImageIO.write(img, "jpg", tmp);
|
||||||
|
|
||||||
|
// 上传到 MinIO(唯一名)
|
||||||
|
String uniqueObjectName = buildUniqueObjectName(originalName);
|
||||||
|
MinioService minio = SpringUtils.getBean(MinioService.class);
|
||||||
|
IVMinioObjectService objSvc = SpringUtils.getBean(IVMinioObjectService.class);
|
||||||
|
AlarmRecordMapper alarmMapper = SpringUtils.getBean(AlarmRecordMapper.class);
|
||||||
|
|
||||||
|
CustomMultipartFile mf = new CustomMultipartFile(tmp, "image/jpeg");
|
||||||
|
MinioService.UploadResult up = minio.uploadWithName(mf, uniqueObjectName);
|
||||||
|
|
||||||
|
// v_minio_object 入库
|
||||||
|
VMinioObject rec = new VMinioObject();
|
||||||
|
rec.setObjectName(up.getObjectName());
|
||||||
|
rec.setUrl(up.getUrl());
|
||||||
|
rec.setOriginalName(originalName);
|
||||||
|
objSvc.insert(rec);
|
||||||
|
|
||||||
|
// 写报警记录(仅图片)
|
||||||
|
AlarmRecord alarm = new AlarmRecord();
|
||||||
|
alarm.setAlarmType(best.cls());
|
||||||
|
alarm.setAlarmLevel(getLevel(best.conf()));
|
||||||
|
alarm.setAlarmDesc(String.format("检测到%s,置信度: %.2f",
|
||||||
|
best.cls(), best.conf()));
|
||||||
|
alarm.setConfidence((double)best.conf());
|
||||||
|
alarm.setImagePath(up.getUrl());
|
||||||
|
alarm.setAlarmTime(new Date(nowMs));
|
||||||
|
alarm.setHandleStatus("0");
|
||||||
|
// 绑定任务/设备:优先窗口上下文,其次 cameraDto 备注/URL
|
||||||
|
if (currentJobId != null) alarm.setTaskId(currentJobId);
|
||||||
|
if (currentDeviceId != null) alarm.setDeviceId(currentDeviceId);
|
||||||
|
if (cameraDto != null) {
|
||||||
|
String dn = cameraDto.getRemark();
|
||||||
|
if (dn == null || dn.isEmpty()) dn = cameraDto.getUrl();
|
||||||
|
alarm.setDeviceName(dn);
|
||||||
|
}
|
||||||
|
|
||||||
|
alarmMapper.insertAlarmRecord(alarm);
|
||||||
|
// 保存本次会话对应的告警ID,用于视频回填
|
||||||
|
currentAlarmId = alarm.getAlarmId();
|
||||||
|
|
||||||
|
// 清理临时文件
|
||||||
|
try { tmp.delete(); } catch (Exception ignore) {}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("保存/上传截图失败: {}", e.getMessage());
|
||||||
|
} finally {
|
||||||
|
try { annotated.release(); } catch (Exception ignore) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String buildSnapshotOriginalName(long nowMs) {
|
||||||
|
String ts = String.valueOf(nowMs);
|
||||||
|
return "snapshot_" + ts + ".jpg";
|
||||||
|
}
|
||||||
|
|
||||||
|
private String buildUniqueObjectName(String original) {
|
||||||
|
String ext = ".jpg";
|
||||||
|
int i = original.lastIndexOf('.');
|
||||||
|
if (i > 0) ext = original.substring(i);
|
||||||
|
return UUID.randomUUID().toString().replace("-", "") + ext;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getLevel(float conf) {
|
||||||
|
if (conf >= 0.9f) return "3";
|
||||||
|
if (conf >= 0.7f) return "2";
|
||||||
|
return "1";
|
||||||
|
}
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
public static class WindowStats {
|
public static class WindowStats {
|
||||||
private int frames;
|
private int frames;
|
||||||
@@ -123,6 +331,21 @@ public class MediaTransferFlvByJavacv extends MediaTransfer implements Runnable
|
|||||||
// 导出最近一次“叠好框的帧”用于截图存证
|
// 导出最近一次“叠好框的帧”用于截图存证
|
||||||
private final AtomicReference<Mat> latestAnnotatedFrame = new AtomicReference<>();
|
private final AtomicReference<Mat> latestAnnotatedFrame = new AtomicReference<>();
|
||||||
|
|
||||||
|
// =============== 轻量级“截图留存”会话聚合(不录视频) ===============
|
||||||
|
private boolean snapshotSessionActive = false;
|
||||||
|
private long sessionStartMs = 0L;
|
||||||
|
private long lastSeenMs = 0L;
|
||||||
|
// 连续存在的最小空窗阈值(超过视为结束),默认3秒
|
||||||
|
private long minGapMs = 3000L;
|
||||||
|
// 单次会话最长保存时长,默认30秒
|
||||||
|
private long maxDurationMs = 30000L;
|
||||||
|
// 置信度阈值(低于该值不落库不截图上传)
|
||||||
|
private float detectionThreshold = 0.7f;
|
||||||
|
// 会话视频录制器与上下文
|
||||||
|
private FFmpegFrameRecorder sessionRecorder;
|
||||||
|
private File sessionVideoTmp;
|
||||||
|
private Long currentAlarmId; // 用于会话结束后回填 videoPath
|
||||||
|
|
||||||
public MediaTransferFlvByJavacv(CameraDto cameraDto) {
|
public MediaTransferFlvByJavacv(CameraDto cameraDto) {
|
||||||
super();
|
super();
|
||||||
this.cameraDto = cameraDto;
|
this.cameraDto = cameraDto;
|
||||||
@@ -451,6 +674,37 @@ public class MediaTransferFlvByJavacv extends MediaTransfer implements Runnable
|
|||||||
// 更新"最近叠好框的帧"用于存证
|
// 更新"最近叠好框的帧"用于存证
|
||||||
updateLatestAnnotated(mat);
|
updateLatestAnnotated(mat);
|
||||||
|
|
||||||
|
// ============ 轻量版会话聚合:仅保存一张截图 ============
|
||||||
|
try {
|
||||||
|
long now = System.currentTimeMillis();
|
||||||
|
boolean hasTarget = currentDetections != null && !currentDetections.isEmpty();
|
||||||
|
if (hasTarget) {
|
||||||
|
lastSeenMs = now;
|
||||||
|
if (!snapshotSessionActive) {
|
||||||
|
// 会话开始:截一张叠好框的图并落库+上传
|
||||||
|
snapshotSessionActive = true;
|
||||||
|
sessionStartMs = now;
|
||||||
|
saveSnapshotAndCreateAlarm(currentDetections, now);
|
||||||
|
// 启动会话视频录制
|
||||||
|
startSessionVideoRecorder();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 结束条件:目标消失超过阈值,或会话超过最大时长
|
||||||
|
if (snapshotSessionActive) {
|
||||||
|
boolean gapTimeout = (now - lastSeenMs) >= minGapMs;
|
||||||
|
boolean maxed = (now - sessionStartMs) >= maxDurationMs;
|
||||||
|
if (gapTimeout || maxed) {
|
||||||
|
// 停止并上传会话视频,回填到本次告警
|
||||||
|
stopAndUploadSessionVideo(now);
|
||||||
|
snapshotSessionActive = false;
|
||||||
|
sessionStartMs = 0L;
|
||||||
|
lastSeenMs = 0L;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception ex) {
|
||||||
|
log.warn("轻量截图会话处理异常: {}", ex.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
// 统计(仅窗口巡检时)
|
// 统计(仅窗口巡检时)
|
||||||
if (windowMode) updateStats(currentDetections);
|
if (windowMode) updateStats(currentDetections);
|
||||||
|
|
||||||
@@ -486,6 +740,14 @@ public class MediaTransferFlvByJavacv extends MediaTransfer implements Runnable
|
|||||||
videoTS = 1000 * (now - startTime);
|
videoTS = 1000 * (now - startTime);
|
||||||
if (videoTS > recorder.getTimestamp()) recorder.setTimestamp(videoTS);
|
if (videoTS > recorder.getTimestamp()) recorder.setTimestamp(videoTS);
|
||||||
recorder.record(frame);
|
recorder.record(frame);
|
||||||
|
// 会话录制:同步追加处理后的帧
|
||||||
|
if (snapshotSessionActive && sessionRecorder != null) {
|
||||||
|
try {
|
||||||
|
sessionRecorder.record(frame);
|
||||||
|
} catch (Exception recErr) {
|
||||||
|
log.debug("会话录像写入失败: {}", recErr.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (FrameGrabber.Exception e) {
|
} catch (FrameGrabber.Exception e) {
|
||||||
@@ -536,6 +798,14 @@ public class MediaTransferFlvByJavacv extends MediaTransfer implements Runnable
|
|||||||
if (recorder != null) {
|
if (recorder != null) {
|
||||||
try { recorder.close(); } catch (Exception ignored) {}
|
try { recorder.close(); } catch (Exception ignored) {}
|
||||||
}
|
}
|
||||||
|
if (sessionRecorder != null) {
|
||||||
|
try { sessionRecorder.close(); } catch (Exception ignored) {}
|
||||||
|
sessionRecorder = null;
|
||||||
|
}
|
||||||
|
if (sessionVideoTmp != null) {
|
||||||
|
try { sessionVideoTmp.delete(); } catch (Exception ignore) {}
|
||||||
|
sessionVideoTmp = null;
|
||||||
|
}
|
||||||
|
|
||||||
if (grabber != null) {
|
if (grabber != null) {
|
||||||
try { grabber.close(); } catch (Exception ignored) {}
|
try { grabber.close(); } catch (Exception ignored) {}
|
||||||
|
|||||||
@@ -33,6 +33,23 @@
|
|||||||
SELECT * FROM v_model WHERE model_id = #{id}
|
SELECT * FROM v_model WHERE model_id = #{id}
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
|
<update id="updateModel" parameterType="com.ruoyi.video.domain.VModel">
|
||||||
|
UPDATE v_model
|
||||||
|
<trim prefix="SET" suffixOverrides=",">
|
||||||
|
<if test="modelName != null and modelName != ''">model_name = #{modelName},</if>
|
||||||
|
<if test="version != null">version = #{version},</if>
|
||||||
|
<if test="framework != null and framework != ''">framework = #{framework},</if>
|
||||||
|
<if test="url != null and url != ''">url = #{url},</if>
|
||||||
|
<if test="fileSize != null">file_size = #{fileSize},</if>
|
||||||
|
<if test="checksum != null">checksum = #{checksum},</if>
|
||||||
|
<if test="enabled != null">enabled = #{enabled},</if>
|
||||||
|
<if test="updateBy != null">update_by = #{updateBy},</if>
|
||||||
|
<if test="remark != null">remark = #{remark},</if>
|
||||||
|
update_time = NOW()
|
||||||
|
</trim>
|
||||||
|
WHERE model_id = #{modelId}
|
||||||
|
</update>
|
||||||
|
|
||||||
<delete id="deleteModelById" parameterType="long">
|
<delete id="deleteModelById" parameterType="long">
|
||||||
DELETE FROM v_model WHERE model_id = #{id}
|
DELETE FROM v_model WHERE model_id = #{id}
|
||||||
</delete>
|
</delete>
|
||||||
|
|||||||
Reference in New Issue
Block a user