You've already forked FrameTour-BE
feat(device): 实现设备视频连续性检查功能
All checks were successful
ZhenTu-BE/pipeline/head This commit looks good
All checks were successful
ZhenTu-BE/pipeline/head This commit looks good
- 新增设备视频连续性检查控制器 DeviceVideoContinuityController - 提供查询、手动触发和删除检查结果的 REST 接口 - 实现视频连续性检查核心逻辑,支持检测视频间隙 - 添加定时任务 DeviceVideoContinuityCheckTask 自动检查设备视频连续性 - 仅在生产环境(prod)启用,每天9点到18点间每5分钟执行一次 - 支持阿里云OSS和本地存储的视频连续性检查 - 检查结果缓存至 Redis,默认保留24小时 - 新增相关实体类: DeviceVideoContinuityCache、VideoContinuityGap、VideoContinuityResult - 在存储操作接口中增加 checkVideoContinuity 和 checkRecentVideoContinuity 方法 - 为不支持的存储类型提供默认不支持连续性检查的实现
This commit is contained in:
@@ -0,0 +1,347 @@
|
||||
package com.ycwl.basic.device.operator;
|
||||
|
||||
import com.ycwl.basic.device.entity.common.FileObject;
|
||||
import com.ycwl.basic.device.entity.common.VideoContinuityGap;
|
||||
import com.ycwl.basic.device.entity.common.VideoContinuityResult;
|
||||
import com.ycwl.basic.storage.adapters.IStorageAdapter;
|
||||
import com.ycwl.basic.storage.entity.StorageFileObject;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Calendar;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@Slf4j
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
@DisplayName("AliOssStorageOperator视频连续性检查测试")
|
||||
class AliOssStorageOperatorTest {
|
||||
|
||||
@Mock
|
||||
private IStorageAdapter mockAdapter;
|
||||
|
||||
private AliOssStorageOperator operator;
|
||||
|
||||
private static final String TEST_CONFIG_JSON = "{\"endpoint\":\"oss-cn-hangzhou.aliyuncs.com\","
|
||||
+ "\"accessKeyId\":\"test-key\",\"accessKeySecret\":\"test-secret\","
|
||||
+ "\"bucketName\":\"test-bucket\",\"prefix\":\"test/\"}";
|
||||
|
||||
@BeforeEach
|
||||
void setUp() throws Exception {
|
||||
operator = new AliOssStorageOperator(TEST_CONFIG_JSON);
|
||||
// 通过反射注入mock的adapter
|
||||
Field adapterField = AliOssStorageOperator.class.getDeclaredField("adapter");
|
||||
adapterField.setAccessible(true);
|
||||
adapterField.set(operator, mockAdapter);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("视频完全连续-无间隙")
|
||||
void testCheckVideoContinuity_Continuous() throws Exception {
|
||||
// Given: 创建连续的视频文件列表
|
||||
Date baseTime = new Date();
|
||||
List<StorageFileObject> mockStorageObjects = createMockStorageObjects(baseTime, new int[][] {
|
||||
{0, 10}, // 0-10秒
|
||||
{10, 20}, // 10-20秒(完美衔接)
|
||||
{20, 30}, // 20-30秒(完美衔接)
|
||||
{30, 40} // 30-40秒(完美衔接)
|
||||
});
|
||||
|
||||
when(mockAdapter.listDir(anyString())).thenReturn(mockStorageObjects);
|
||||
when(mockAdapter.getUrlForDownload(anyString())).thenReturn("http://test.com/video.ts");
|
||||
|
||||
// When: 检查视频连续性
|
||||
Calendar cal = Calendar.getInstance();
|
||||
cal.setTime(baseTime);
|
||||
Date startDate = cal.getTime();
|
||||
cal.add(Calendar.SECOND, 50);
|
||||
Date endDate = cal.getTime();
|
||||
|
||||
VideoContinuityResult result = operator.checkVideoContinuity(startDate, endDate, 2000L);
|
||||
|
||||
// Then: 应该是连续的
|
||||
assertTrue(result.isSupport(), "应该支持连续性检查");
|
||||
assertTrue(result.isContinuous(), "视频应该是连续的");
|
||||
assertEquals(0, result.getGapCount(), "不应该有间隙");
|
||||
assertEquals(4, result.getTotalVideos(), "应该有4个视频");
|
||||
assertEquals(40000L, result.getTotalDurationMs(), "总时长应该是40秒");
|
||||
log.info("测试通过: 视频完全连续");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("视频有小间隙-在允许范围内")
|
||||
void testCheckVideoContinuity_WithSmallGaps() throws Exception {
|
||||
// Given: 创建有小间隙的视频文件列表(间隙1秒)
|
||||
Date baseTime = new Date();
|
||||
List<StorageFileObject> mockStorageObjects = createMockStorageObjects(baseTime, new int[][] {
|
||||
{0, 10}, // 0-10秒
|
||||
{11, 21}, // 11-21秒(1秒间隙)
|
||||
{22, 32}, // 22-32秒(1秒间隙)
|
||||
{33, 43} // 33-43秒(1秒间隙)
|
||||
});
|
||||
|
||||
when(mockAdapter.listDir(anyString())).thenReturn(mockStorageObjects);
|
||||
when(mockAdapter.getUrlForDownload(anyString())).thenReturn("http://test.com/video.ts");
|
||||
|
||||
// When: 检查视频连续性(允许2秒间隙)
|
||||
Calendar cal = Calendar.getInstance();
|
||||
cal.setTime(baseTime);
|
||||
Date startDate = cal.getTime();
|
||||
cal.add(Calendar.SECOND, 50);
|
||||
Date endDate = cal.getTime();
|
||||
|
||||
VideoContinuityResult result = operator.checkVideoContinuity(startDate, endDate, 2000L);
|
||||
|
||||
// Then: 应该是连续的(间隙在允许范围内)
|
||||
assertTrue(result.isContinuous(), "视频应该是连续的(间隙在允许范围内)");
|
||||
assertEquals(0, result.getGapCount(), "不应该有超出允许范围的间隙");
|
||||
assertEquals(4, result.getTotalVideos(), "应该有4个视频");
|
||||
log.info("测试通过: 小间隙在允许范围内");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("视频有大间隙-超出允许范围")
|
||||
void testCheckVideoContinuity_WithLargeGaps() throws Exception {
|
||||
// Given: 创建有大间隙的视频文件列表(间隙5秒)
|
||||
Date baseTime = new Date();
|
||||
List<StorageFileObject> mockStorageObjects = createMockStorageObjects(baseTime, new int[][] {
|
||||
{0, 10}, // 0-10秒
|
||||
{15, 25}, // 15-25秒(5秒间隙)
|
||||
{30, 40}, // 30-40秒(5秒间隙)
|
||||
{45, 55} // 45-55秒(5秒间隙)
|
||||
});
|
||||
|
||||
when(mockAdapter.listDir(anyString())).thenReturn(mockStorageObjects);
|
||||
when(mockAdapter.getUrlForDownload(anyString())).thenReturn("http://test.com/video.ts");
|
||||
|
||||
// When: 检查视频连续性(允许2秒间隙)
|
||||
Calendar cal = Calendar.getInstance();
|
||||
cal.setTime(baseTime);
|
||||
Date startDate = cal.getTime();
|
||||
cal.add(Calendar.SECOND, 60);
|
||||
Date endDate = cal.getTime();
|
||||
|
||||
VideoContinuityResult result = operator.checkVideoContinuity(startDate, endDate, 2000L);
|
||||
|
||||
// Then: 应该不连续(有3个超出允许范围的间隙)
|
||||
assertFalse(result.isContinuous(), "视频应该不连续");
|
||||
assertEquals(3, result.getGapCount(), "应该有3个间隙");
|
||||
assertEquals(4, result.getTotalVideos(), "应该有4个视频");
|
||||
|
||||
// 验证间隙信息
|
||||
List<VideoContinuityGap> gaps = result.getGaps();
|
||||
for (VideoContinuityGap gap : gaps) {
|
||||
assertEquals(5000L, gap.getGapMs(), "每个间隙应该是5秒");
|
||||
assertNotNull(gap.getBeforeFile(), "应该有前一个文件");
|
||||
assertNotNull(gap.getAfterFile(), "应该有后一个文件");
|
||||
assertNotNull(gap.getGapStartTime(), "应该有间隙开始时间");
|
||||
assertNotNull(gap.getGapEndTime(), "应该有间隙结束时间");
|
||||
}
|
||||
log.info("测试通过: 检测到大间隙");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("空文件列表")
|
||||
void testCheckVideoContinuity_EmptyList() throws Exception {
|
||||
// Given: 返回空列表
|
||||
when(mockAdapter.listDir(anyString())).thenReturn(new ArrayList<>());
|
||||
|
||||
// When: 检查视频连续性
|
||||
Date startDate = new Date();
|
||||
Calendar cal = Calendar.getInstance();
|
||||
cal.add(Calendar.MINUTE, 10);
|
||||
Date endDate = cal.getTime();
|
||||
|
||||
VideoContinuityResult result = operator.checkVideoContinuity(startDate, endDate, 2000L);
|
||||
|
||||
// Then: 应该返回不连续(没有视频)
|
||||
assertTrue(result.isSupport(), "应该支持连续性检查");
|
||||
assertFalse(result.isContinuous(), "空列表应该返回不连续");
|
||||
assertEquals(0, result.getGapCount(), "不应该有间隙");
|
||||
assertEquals(0, result.getTotalVideos(), "视频数应该是0");
|
||||
assertEquals(0L, result.getTotalDurationMs(), "总时长应该是0");
|
||||
log.info("测试通过: 空文件列表");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("单个视频文件")
|
||||
void testCheckVideoContinuity_SingleVideo() throws Exception {
|
||||
// Given: 只有一个视频文件
|
||||
Date baseTime = new Date();
|
||||
List<StorageFileObject> mockStorageObjects = createMockStorageObjects(baseTime, new int[][] {
|
||||
{0, 10} // 0-10秒
|
||||
});
|
||||
|
||||
when(mockAdapter.listDir(anyString())).thenReturn(mockStorageObjects);
|
||||
when(mockAdapter.getUrlForDownload(anyString())).thenReturn("http://test.com/video.ts");
|
||||
|
||||
// When: 检查视频连续性
|
||||
Calendar cal = Calendar.getInstance();
|
||||
cal.setTime(baseTime);
|
||||
Date startDate = cal.getTime();
|
||||
cal.add(Calendar.SECOND, 20);
|
||||
Date endDate = cal.getTime();
|
||||
|
||||
VideoContinuityResult result = operator.checkVideoContinuity(startDate, endDate, 2000L);
|
||||
|
||||
// Then: 应该是连续的
|
||||
assertTrue(result.isSupport(), "应该支持连续性检查");
|
||||
assertTrue(result.isContinuous(), "单个视频应该是连续的");
|
||||
assertEquals(0, result.getGapCount(), "不应该有间隙");
|
||||
assertEquals(1, result.getTotalVideos(), "应该有1个视频");
|
||||
assertEquals(10000L, result.getTotalDurationMs(), "总时长应该是10秒");
|
||||
log.info("测试通过: 单个视频文件");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("使用真实时间范围-前7分钟到前2分钟")
|
||||
void testCheckVideoContinuity_RealTimeRange() throws Exception {
|
||||
// Given: 创建前7分钟到前2分钟的视频文件
|
||||
Calendar cal = Calendar.getInstance();
|
||||
cal.add(Calendar.MINUTE, -7);
|
||||
Date baseTime = cal.getTime();
|
||||
|
||||
// 创建5分钟的连续视频(每10秒一个片段)
|
||||
List<int[]> timeRanges = new ArrayList<>();
|
||||
for (int i = 0; i < 30; i++) {
|
||||
int start = i * 10;
|
||||
int end = start + 10;
|
||||
timeRanges.add(new int[]{start, end});
|
||||
}
|
||||
|
||||
List<StorageFileObject> mockStorageObjects = createMockStorageObjects(
|
||||
baseTime,
|
||||
timeRanges.toArray(new int[0][])
|
||||
);
|
||||
|
||||
when(mockAdapter.listDir(anyString())).thenReturn(mockStorageObjects);
|
||||
when(mockAdapter.getUrlForDownload(anyString())).thenReturn("http://test.com/video.ts");
|
||||
|
||||
// When: 检查近期视频连续性
|
||||
VideoContinuityResult result = operator.checkRecentVideoContinuity();
|
||||
|
||||
// Then: 验证结果
|
||||
assertNotNull(result, "结果不应该为空");
|
||||
assertEquals(30, result.getTotalVideos(), "应该有30个视频片段");
|
||||
assertTrue(result.isContinuous(), "视频应该是连续的");
|
||||
assertEquals(300000L, result.getTotalDurationMs(), "总时长应该是300秒(5分钟)");
|
||||
assertEquals(2000L, result.getMaxAllowedGapMs(), "允许的最大间隙应该是2秒");
|
||||
log.info("测试通过: 真实时间范围检查 - 视频数: {}, 总时长: {}ms, 连续: {}",
|
||||
result.getTotalVideos(), result.getTotalDurationMs(), result.isContinuous());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("混合场景-既有小间隙又有大间隙")
|
||||
void testCheckVideoContinuity_MixedGaps() throws Exception {
|
||||
// Given: 创建混合间隙的视频文件列表
|
||||
Date baseTime = new Date();
|
||||
List<StorageFileObject> mockStorageObjects = createMockStorageObjects(baseTime, new int[][] {
|
||||
{0, 10}, // 0-10秒
|
||||
{11, 21}, // 11-21秒(1秒间隙,允许)
|
||||
{22, 32}, // 22-32秒(1秒间隙,允许)
|
||||
{37, 47}, // 37-47秒(5秒间隙,超出)
|
||||
{48, 58}, // 48-58秒(1秒间隙,允许)
|
||||
{63, 73} // 63-73秒(5秒间隙,超出)
|
||||
});
|
||||
|
||||
when(mockAdapter.listDir(anyString())).thenReturn(mockStorageObjects);
|
||||
when(mockAdapter.getUrlForDownload(anyString())).thenReturn("http://test.com/video.ts");
|
||||
|
||||
// When: 检查视频连续性(允许2秒间隙)
|
||||
Calendar cal = Calendar.getInstance();
|
||||
cal.setTime(baseTime);
|
||||
Date startDate = cal.getTime();
|
||||
cal.add(Calendar.SECOND, 80);
|
||||
Date endDate = cal.getTime();
|
||||
|
||||
VideoContinuityResult result = operator.checkVideoContinuity(startDate, endDate, 2000L);
|
||||
|
||||
// Then: 应该检测到2个大间隙
|
||||
assertTrue(result.isSupport(), "应该支持连续性检查");
|
||||
assertFalse(result.isContinuous(), "视频应该不连续");
|
||||
assertEquals(2, result.getGapCount(), "应该有2个超出允许范围的间隙");
|
||||
assertEquals(6, result.getTotalVideos(), "应该有6个视频");
|
||||
|
||||
// 验证第一个大间隙
|
||||
VideoContinuityGap firstGap = result.getGaps().get(0);
|
||||
assertEquals(5000L, firstGap.getGapMs(), "第一个间隙应该是5秒");
|
||||
|
||||
// 验证第二个大间隙
|
||||
VideoContinuityGap secondGap = result.getGaps().get(1);
|
||||
assertEquals(5000L, secondGap.getGapMs(), "第二个间隙应该是5秒");
|
||||
|
||||
log.info("测试通过: 混合场景检测 - 总间隙: {}, 小间隙被忽略, 大间隙被检测", result.getGapCount());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("测试不支持的实现类返回support=false")
|
||||
void testUnsupportedOperator() {
|
||||
// Given: 使用不支持连续性检查的操作器
|
||||
LocalStorageOperator localOperator = new LocalStorageOperator("{}");
|
||||
|
||||
// When: 调用检查方法
|
||||
VideoContinuityResult result = localOperator.checkRecentVideoContinuity();
|
||||
|
||||
// Then: 应该返回support=false
|
||||
assertFalse(result.isSupport(), "LocalStorageOperator不应该支持连续性检查");
|
||||
assertFalse(result.isContinuous(), "不支持时应该返回不连续");
|
||||
assertEquals(0, result.getTotalVideos(), "视频数应该是0");
|
||||
assertEquals(0, result.getGapCount(), "间隙数应该是0");
|
||||
log.info("测试通过: 不支持的实现类正确返回support=false");
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建模拟的存储文件对象列表
|
||||
*
|
||||
* @param baseTime 基准时间
|
||||
* @param timeRanges 时间范围数组,每个元素是[开始秒数, 结束秒数]
|
||||
* @return 存储文件对象列表
|
||||
*/
|
||||
private List<StorageFileObject> createMockStorageObjects(Date baseTime, int[][] timeRanges) {
|
||||
List<StorageFileObject> objects = new ArrayList<>();
|
||||
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMdd");
|
||||
SimpleDateFormat timeFormat = new SimpleDateFormat("HHmmss");
|
||||
|
||||
String datePrefix = dateFormat.format(baseTime);
|
||||
Calendar cal = Calendar.getInstance();
|
||||
cal.setTime(baseTime);
|
||||
|
||||
for (int[] range : timeRanges) {
|
||||
int startSeconds = range[0];
|
||||
int endSeconds = range[1];
|
||||
|
||||
// 计算开始和结束时间
|
||||
cal.setTime(baseTime);
|
||||
cal.add(Calendar.SECOND, startSeconds);
|
||||
String startTime = timeFormat.format(cal.getTime());
|
||||
|
||||
cal.setTime(baseTime);
|
||||
cal.add(Calendar.SECOND, endSeconds);
|
||||
String endTime = timeFormat.format(cal.getTime());
|
||||
|
||||
// 创建文件名: 开始时间_结束时间.ts
|
||||
String fileName = startTime + "_" + endTime + ".ts";
|
||||
String path = datePrefix;
|
||||
|
||||
StorageFileObject obj = new StorageFileObject();
|
||||
obj.setPath(path);
|
||||
obj.setName(fileName);
|
||||
obj.setSize(1024L * 100L); // 假设每个文件100KB
|
||||
objects.add(obj);
|
||||
}
|
||||
|
||||
return objects;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user