package com.inspect.nvr.hik.service;
|
|
|
|
import com.inspect.nvr.hik.domain.HikNvrPtzMode;
|
|
import com.inspect.nvr.hik.domain.HikNvrUiActionRequest;
|
|
import com.inspect.nvr.hik.domain.HikNvrUiActionResponse;
|
|
import com.inspect.nvr.hikVision.utils.jna.HCNetSDK;
|
|
import com.inspect.nvr.domain.Infrared.NvrInfo;
|
|
import com.inspect.nvr.ivs.mapper.IvsCameraMapper;
|
|
import com.inspect.nvr.ivs.pojo.IvsCamera;
|
|
import com.inspect.nvr.service.HikCameraService;
|
|
import com.inspect.nvr.service.HikLoginService;
|
|
import com.inspect.nvr.service.HikLoginSession;
|
|
import com.sun.jna.Pointer;
|
|
import org.junit.Before;
|
|
import org.junit.Test;
|
|
import org.junit.runner.RunWith;
|
|
import org.mockito.Mock;
|
|
import org.mockito.junit.MockitoJUnitRunner;
|
|
import org.springframework.test.util.ReflectionTestUtils;
|
|
|
|
import java.io.ByteArrayOutputStream;
|
|
import java.nio.charset.Charset;
|
|
import java.nio.charset.StandardCharsets;
|
|
import java.util.Map;
|
|
|
|
import static org.junit.Assert.assertEquals;
|
|
import static org.junit.Assert.assertTrue;
|
|
import static org.mockito.ArgumentMatchers.any;
|
|
import static org.mockito.ArgumentMatchers.anyInt;
|
|
import static org.mockito.ArgumentMatchers.eq;
|
|
import static org.mockito.Mockito.doReturn;
|
|
import static org.mockito.Mockito.doNothing;
|
|
import static org.mockito.Mockito.spy;
|
|
import static org.mockito.Mockito.times;
|
|
import static org.mockito.Mockito.timeout;
|
|
import static org.mockito.Mockito.verify;
|
|
import static org.mockito.Mockito.when;
|
|
|
|
@RunWith(MockitoJUnitRunner.class)
|
|
public class HikNvrUiActionServiceTest {
|
|
|
|
@Mock
|
|
private HikLoginService loginService;
|
|
@Mock
|
|
private HikCameraService cameraService;
|
|
@Mock
|
|
private HCNetSDK sdk;
|
|
@Mock
|
|
private IvsCameraMapper ivsCameraMapper;
|
|
|
|
private HikNvrUiActionService service;
|
|
|
|
@Before
|
|
public void setUp() {
|
|
// 构造完全来自数据库的 NVR 连接对象,服务不再依赖固定页面配置。
|
|
NvrInfo databaseNvr = new NvrInfo();
|
|
databaseNvr.setNvrName("测试 NVR");
|
|
databaseNvr.setNvrIp("192.168.1.249");
|
|
databaseNvr.setServerPort(8000);
|
|
databaseNvr.setRtspPort(554);
|
|
databaseNvr.setHttpPort(80);
|
|
databaseNvr.setAccount("admin");
|
|
databaseNvr.setPassword("secret");
|
|
databaseNvr.setStartChannel(1);
|
|
service = spy(new HikNvrUiActionService());
|
|
ReflectionTestUtils.setField(service, "boundNvrInfo", databaseNvr);
|
|
ReflectionTestUtils.setField(service, "loginService", loginService);
|
|
ReflectionTestUtils.setField(service, "cameraService", cameraService);
|
|
ReflectionTestUtils.setField(service, "sdk", sdk);
|
|
ReflectionTestUtils.setField(service, "ivsCameraMapper", ivsCameraMapper);
|
|
// 清理静态通道状态,避免不同测试之间复用上一次 PTZ 动作的停止命令。
|
|
Map<?, ?> channelStates = (Map<?, ?>) ReflectionTestUtils.getField(
|
|
HikNvrUiActionService.class, "PTZ_CHANNEL_STATES");
|
|
if (channelStates != null) {
|
|
channelStates.clear();
|
|
}
|
|
when(loginService.loginSession(any())).thenReturn(
|
|
new HikLoginSession(7, 0, 1, 8, 33));
|
|
}
|
|
|
|
@Test
|
|
public void callsSdkPresetWithoutIsapiRoundTrip() {
|
|
when(sdk.NET_DVR_PTZPreset_Other(
|
|
eq(7), eq(40), eq(HCNetSDK.GOTO_PRESET), eq(5))).thenReturn(true);
|
|
|
|
HikNvrUiActionResponse response = service.preset(HikNvrUiActionRequest.builder()
|
|
.channel(8)
|
|
.preset(5)
|
|
.build());
|
|
|
|
assertTrue(response.isSuccess());
|
|
assertEquals("已调用预置位 5(SDK)", response.getMessage());
|
|
assertEquals(Integer.valueOf(40), response.getSdkChannel());
|
|
verify(sdk).NET_DVR_PTZPreset_Other(7, 40, HCNetSDK.GOTO_PRESET, 5);
|
|
}
|
|
|
|
@Test
|
|
public void sendsPtzThroughSdkWithConfiguredSpeed() {
|
|
when(sdk.NET_DVR_PTZControlWithSpeed_Other(
|
|
eq(7), eq(40), eq(HCNetSDK.PAN_LEFT), eq(0), eq(6))).thenReturn(true);
|
|
|
|
HikNvrUiActionResponse response = service.ptz(HikNvrUiActionRequest.builder()
|
|
.channel(8)
|
|
.action("LEFT")
|
|
.speed(6)
|
|
.build());
|
|
|
|
assertTrue(response.isSuccess());
|
|
assertEquals(Integer.valueOf(40), response.getSdkChannel());
|
|
verify(sdk).NET_DVR_PTZControlWithSpeed_Other(7, 40, HCNetSDK.PAN_LEFT, 0, 6);
|
|
}
|
|
|
|
@Test
|
|
public void sendsPointPtzStartAndAutoStopAfterOneSecond() throws Exception {
|
|
when(sdk.NET_DVR_PTZControlWithSpeed_Other(
|
|
eq(7), eq(40), eq(HCNetSDK.PAN_LEFT), eq(0), eq(6))).thenReturn(true);
|
|
when(sdk.NET_DVR_PTZControlWithSpeed_Other(
|
|
eq(7), eq(40), eq(HCNetSDK.PAN_LEFT), eq(1), eq(6))).thenReturn(true);
|
|
|
|
HikNvrUiActionResponse response = service.ptz(
|
|
HikNvrUiActionRequest.builder()
|
|
.channel(8)
|
|
.action("LEFT")
|
|
.speed(6)
|
|
.build(),
|
|
HikNvrPtzMode.POINT);
|
|
|
|
assertTrue(response.isSuccess());
|
|
assertTrue(response.getMessage().contains("1秒后自动停止"));
|
|
verify(sdk).NET_DVR_PTZControlWithSpeed_Other(7, 40, HCNetSDK.PAN_LEFT, 0, 6);
|
|
verify(sdk, timeout(2500)).NET_DVR_PTZControlWithSpeed_Other(
|
|
7, 40, HCNetSDK.PAN_LEFT, 1, 6);
|
|
}
|
|
|
|
@Test
|
|
public void explicitStopCancelsPendingPointStop() throws Exception {
|
|
when(sdk.NET_DVR_PTZControlWithSpeed_Other(
|
|
eq(7), eq(40), eq(HCNetSDK.PAN_LEFT), eq(0), eq(6))).thenReturn(true);
|
|
when(sdk.NET_DVR_PTZControlWithSpeed_Other(
|
|
eq(7), eq(40), eq(HCNetSDK.PAN_LEFT), eq(1), eq(6))).thenReturn(true);
|
|
|
|
service.ptz(HikNvrUiActionRequest.builder()
|
|
.channel(8)
|
|
.action("LEFT")
|
|
.speed(6)
|
|
.build(), HikNvrPtzMode.POINT);
|
|
service.ptz(HikNvrUiActionRequest.builder()
|
|
.channel(8)
|
|
.action("STOP")
|
|
.speed(6)
|
|
.build());
|
|
|
|
Thread.sleep(1200L);
|
|
verify(sdk, times(1)).NET_DVR_PTZControlWithSpeed_Other(
|
|
7, 40, HCNetSDK.PAN_LEFT, 1, 6);
|
|
}
|
|
|
|
/** 验证光圈动作会映射为海康光圈放大命令并携带设备速度。 */
|
|
@Test
|
|
public void sendsLensCommandThroughSdkWithMappedCommand() {
|
|
when(sdk.NET_DVR_PTZControlWithSpeed_Other(
|
|
eq(7), eq(40), eq(HCNetSDK.IRIS_OPEN), eq(0), eq(7))).thenReturn(true);
|
|
|
|
HikNvrUiActionResponse response = service.ptz(HikNvrUiActionRequest.builder()
|
|
.channel(8)
|
|
.action("IRIS_OPEN")
|
|
.speed(7)
|
|
.build());
|
|
|
|
assertTrue(response.isSuccess());
|
|
verify(sdk).NET_DVR_PTZControlWithSpeed_Other(
|
|
7, 40, HCNetSDK.IRIS_OPEN, 0, 7);
|
|
}
|
|
|
|
/** 验证停止请求会复用最近一次启动的聚焦命令,而不是使用方向命令占位值。 */
|
|
@Test
|
|
public void stopsThePreviouslyStartedLensCommand() {
|
|
when(sdk.NET_DVR_PTZControlWithSpeed_Other(
|
|
eq(7), eq(40), eq(HCNetSDK.FOCUS_NEAR), eq(0), eq(5))).thenReturn(true);
|
|
when(sdk.NET_DVR_PTZControlWithSpeed_Other(
|
|
eq(7), eq(40), eq(HCNetSDK.FOCUS_NEAR), eq(1), eq(5))).thenReturn(true);
|
|
|
|
service.ptz(HikNvrUiActionRequest.builder()
|
|
.channel(8)
|
|
.action("FOCUS_NEAR")
|
|
.speed(5)
|
|
.build());
|
|
HikNvrUiActionResponse response = service.ptz(HikNvrUiActionRequest.builder()
|
|
.channel(8)
|
|
.action("STOP")
|
|
.speed(4)
|
|
.build());
|
|
|
|
assertTrue(response.isSuccess());
|
|
verify(sdk).NET_DVR_PTZControlWithSpeed_Other(
|
|
7, 40, HCNetSDK.FOCUS_NEAR, 1, 5);
|
|
}
|
|
|
|
/** 验证雨刷打开请求会调用海康辅助设备控制接口。 */
|
|
@Test
|
|
public void controlsWiperThroughAuxiliarySdkCommand() {
|
|
when(sdk.NET_DVR_PTZControl_Other(
|
|
eq(7), eq(40), eq(HCNetSDK.WIPER_PWRON), eq(0))).thenReturn(true);
|
|
|
|
HikNvrUiActionResponse response = service.auxiliaryControl(8, 1, true);
|
|
|
|
assertTrue(response.isSuccess());
|
|
verify(sdk).NET_DVR_PTZControl_Other(7, 40, HCNetSDK.WIPER_PWRON, 0);
|
|
}
|
|
|
|
/** 验证起始位调用会构造远程控制结构体并发送给海康 SDK。 */
|
|
@Test
|
|
public void callsConfiguredInitialPositionThroughRemoteControl() {
|
|
when(sdk.NET_DVR_RemoteControl(
|
|
eq(7), eq(HCNetSDK.NET_DVR_PTZ_INITIALPOSITIONCTRL),
|
|
any(Pointer.class), anyInt())).thenReturn(true);
|
|
|
|
HikNvrUiActionResponse response = service.gotoInitialPosition(8);
|
|
|
|
assertTrue(response.isSuccess());
|
|
assertEquals(Integer.valueOf(40), response.getSdkChannel());
|
|
verify(sdk).NET_DVR_RemoteControl(
|
|
eq(7), eq(HCNetSDK.NET_DVR_PTZ_INITIALPOSITIONCTRL),
|
|
any(Pointer.class), anyInt());
|
|
}
|
|
|
|
@Test
|
|
public void savesUnnamedPresetThroughSdkWithoutHttpNameSync() {
|
|
when(sdk.NET_DVR_PTZPreset_Other(
|
|
eq(7), eq(40), eq(HCNetSDK.SET_PRESET), eq(6))).thenReturn(true);
|
|
|
|
HikNvrUiActionResponse response = service.createPreset(HikNvrUiActionRequest.builder()
|
|
.channel(8)
|
|
.preset(6)
|
|
.build());
|
|
|
|
assertTrue(response.isSuccess());
|
|
assertEquals("已保存预置位 6(SDK)", response.getMessage());
|
|
assertEquals(Integer.valueOf(40), response.getSdkChannel());
|
|
verify(sdk).NET_DVR_PTZPreset_Other(7, 40, HCNetSDK.SET_PRESET, 6);
|
|
}
|
|
|
|
@Test
|
|
public void savesPresetNameThroughCameraSdkAndNvrIsapi() {
|
|
when(sdk.NET_DVR_PTZPreset_Other(
|
|
eq(7), eq(40), eq(HCNetSDK.SET_PRESET), eq(6))).thenReturn(true);
|
|
when(sdk.NET_DVR_SetDVRConfig(
|
|
eq(7), eq(HCNetSDK.NET_DVR_SET_PRESET_NAME), eq(40),
|
|
any(Pointer.class), anyInt())).thenReturn(true);
|
|
doNothing().when(service).setPresetByIsapi(eq(8), eq(6), eq("测试大门"));
|
|
|
|
HikNvrUiActionResponse response = service.createPreset(HikNvrUiActionRequest.builder()
|
|
.channel(8)
|
|
.preset(6)
|
|
.presetName("测试大门")
|
|
.build());
|
|
|
|
assertTrue(response.isSuccess());
|
|
assertEquals("已保存预置位 6(SDK),名称:测试大门", response.getMessage());
|
|
verify(sdk).NET_DVR_SetDVRConfig(
|
|
eq(7), eq(HCNetSDK.NET_DVR_SET_PRESET_NAME), eq(40),
|
|
any(Pointer.class), anyInt());
|
|
}
|
|
|
|
@Test
|
|
public void prefersClientDemoDirectCameraTargetWhenConfigured() {
|
|
// 直连目标只从当前 NVR 的 ivs_camera 记录读取。
|
|
IvsCamera camera = new IvsCamera();
|
|
camera.setCode("22078710632508667008#database-domain");
|
|
camera.setDevIP("192.168.1.254");
|
|
when(ivsCameraMapper.selectByNvrIp("192.168.1.249"))
|
|
.thenReturn(java.util.Collections.singletonList(camera));
|
|
doReturn(true).when(service).isCameraReachable(any(IvsCamera.class));
|
|
|
|
when(loginService.loginSession(any(NvrInfo.class))).thenAnswer(invocation -> {
|
|
NvrInfo info = invocation.getArgument(0);
|
|
if ("192.168.1.254".equals(info.getNvrIp())) {
|
|
return new HikLoginSession(9, 1, 1, 0, 0);
|
|
}
|
|
return new HikLoginSession(7, 0, 1, 8, 33);
|
|
});
|
|
when(sdk.NET_DVR_PTZPreset_Other(
|
|
eq(9), eq(1), eq(HCNetSDK.GOTO_PRESET), eq(5))).thenReturn(true);
|
|
|
|
HikNvrUiActionResponse response = service.preset(HikNvrUiActionRequest.builder()
|
|
.channel(8)
|
|
.preset(5)
|
|
.build());
|
|
|
|
assertTrue(response.isSuccess());
|
|
assertEquals(Integer.valueOf(1), response.getSdkChannel());
|
|
verify(sdk).NET_DVR_PTZPreset_Other(9, 1, HCNetSDK.GOTO_PRESET, 5);
|
|
verify(loginService).loginSession(any(NvrInfo.class));
|
|
}
|
|
|
|
@Test
|
|
public void decodesPresetNameUsingItsActualEncoding() {
|
|
String utf8Name = ReflectionTestUtils.invokeMethod(
|
|
service, "decodePresetName", "正确".getBytes(StandardCharsets.UTF_8));
|
|
String gbkName = ReflectionTestUtils.invokeMethod(
|
|
service, "decodePresetName", "预置点 3".getBytes(Charset.forName("GBK")));
|
|
|
|
assertEquals("正确", utf8Name);
|
|
assertEquals("预置点 3", gbkName);
|
|
}
|
|
|
|
@Test
|
|
public void normalizesMixedPresetXmlWithoutChangingUtf8Names() throws Exception {
|
|
ByteArrayOutputStream xml = new ByteArrayOutputStream();
|
|
xml.write(("<?xml version=\"1.0\" encoding=\"UTF-8\"?><PTZPresets>"
|
|
+ "<PTZPreset><id>1</id><presetName>").getBytes(StandardCharsets.UTF_8));
|
|
xml.write("正确".getBytes(StandardCharsets.UTF_8));
|
|
xml.write(("</presetName></PTZPreset><PTZPreset><id>3</id><presetName>")
|
|
.getBytes(StandardCharsets.UTF_8));
|
|
xml.write("预置点 3".getBytes(Charset.forName("GBK")));
|
|
xml.write("</presetName></PTZPreset></PTZPresets>"
|
|
.getBytes(StandardCharsets.UTF_8));
|
|
|
|
String normalized = ReflectionTestUtils.invokeMethod(
|
|
service, "normalizePresetXml", xml.toByteArray());
|
|
|
|
assertTrue(normalized.contains("<presetName>正确</presetName>"));
|
|
assertTrue(normalized.contains("<presetName>预置点 3</presetName>"));
|
|
}
|
|
|
|
@Test
|
|
public void buildsPresetNamePayloadAsUtf8() {
|
|
String xml = ReflectionTestUtils.invokeMethod(
|
|
service, "buildPresetXml", 6, "测试大门");
|
|
byte[] wireBytes = xml.getBytes(StandardCharsets.UTF_8);
|
|
|
|
assertTrue(new String(wireBytes, StandardCharsets.UTF_8)
|
|
.contains("<presetName>测试大门</presetName>"));
|
|
assertEquals(-1, new String(wireBytes, Charset.forName("GBK"))
|
|
.indexOf("<presetName>测试大门</presetName>"));
|
|
}
|
|
}
|