package com.inspect.nvr.service.camera;
|
|
|
|
import com.inspect.nvr.service.camera.login.DahuaLoginService;
|
|
import com.inspect.nvr.daHuaCarme.jna.NetSDKLib;
|
|
import com.inspect.nvr.daHuaCarme.jna.dahua.ToolKits;
|
|
import com.inspect.nvr.daHuaCarme.utils.jna.DahuaUtils;
|
|
import com.inspect.nvr.domain.Infrared.Camera;
|
|
import com.inspect.nvr.domain.Infrared.TemperatureData;
|
|
import com.inspect.nvr.enums.CameraEnum;
|
|
import com.sun.jna.Pointer;
|
|
import com.sun.jna.ptr.IntByReference;
|
|
import lombok.extern.slf4j.Slf4j;
|
|
import org.springframework.stereotype.Service;
|
|
|
|
import javax.annotation.Resource;
|
|
import java.util.concurrent.CompletableFuture;
|
|
import java.util.concurrent.ConcurrentHashMap;
|
|
import java.util.concurrent.TimeUnit;
|
|
import java.util.concurrent.TimeoutException;
|
|
import java.util.concurrent.atomic.AtomicInteger;
|
|
|
|
/**
|
|
* 大华设备SDK服务
|
|
* 同一个IP,同一个通道,串行
|
|
* 同一个IP,不同通道,最大4个线程并发
|
|
* 不同IP,并发抓图,不限制
|
|
*/
|
|
@Slf4j
|
|
@Service
|
|
public class DahuaCameraService extends AbstractCameraService {
|
|
/**
|
|
* Digest认证抓图URL:
|
|
* http://<host>/cgi-bin/snapshot.cgi?channel=<channel>&subtype=<subtype>
|
|
* subtype: 0-主码流 1-子码流
|
|
*/
|
|
private static final String DIGEST_URL_TEMPLATE = "http://%s/cgi-bin/snapshot.cgi?channel=%d&subtype=1";
|
|
// Key: loginId+CmdSerial(登录句柄+流水号),Value: CompletableFuture(用于通知调用线程)
|
|
private static final ConcurrentHashMap<String, CompletableFuture<byte[]>> PENDING_REQUESTS = new ConcurrentHashMap<>();
|
|
private static final fCaptureReceiveCB CAPTURE_RECEIVE_CB = new fCaptureReceiveCB();
|
|
// 全局流水号生成器
|
|
private static final AtomicInteger SERIAL_COUNTER = new AtomicInteger(1);
|
|
@Resource
|
|
private NetSDKLib dhNetSDK;
|
|
@Resource
|
|
private DahuaLoginService dahuaLoginService;
|
|
|
|
// CmdSerial请求序列号,有效值范围 0~65535,超出范围会被截断
|
|
public static int nextSerial() {
|
|
return SERIAL_COUNTER.updateAndGet(current -> (current + 1) & 0xFFFF);
|
|
}
|
|
|
|
@Override
|
|
public CameraEnum cameraType() {
|
|
return CameraEnum.DAHUA;
|
|
}
|
|
|
|
@Override
|
|
protected String digestUrlTemplate() {
|
|
return DIGEST_URL_TEMPLATE;
|
|
}
|
|
|
|
@Override
|
|
protected byte[] doCapture(Camera camera) {
|
|
return snapPictureEx(camera);
|
|
}
|
|
|
|
/**
|
|
* 预置位跳转
|
|
*/
|
|
@Override
|
|
protected boolean doGotoPreset(Camera camera) {
|
|
NetSDKLib.LLong loginHandle = dahuaLoginService.login(camera);
|
|
log.info("[" + brandName() + "]预置位跳转,IP={},LoginHandle={},pointNum={}", camera.getIp(), loginHandle, camera.getPointNum());
|
|
boolean result = dhNetSDK.CLIENT_DHPTZControlEx2(loginHandle, camera.getChannel() - 1,
|
|
DahuaUtils.PTZCommand("GOTO_PRESET"), 0, camera.getPointNum(), 0, 0, null);
|
|
if (result) {
|
|
log.info("[" + brandName() + "]CLIENT_DHPTZControlEx success,pointNum={}", camera.getPointNum());
|
|
} else {
|
|
log.error("[" + brandName() + "]CLIENT_DHPTZControlEx Failed!!{}", ToolKits.getErrorCodePrint(dhNetSDK.CLIENT_GetLastError()));
|
|
}
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* 大华实时测温(蓝本:DahuaServiceImpl#StartRemote)。
|
|
* 通过 CLIENT_QueryDevInfo 查询测温规则区域温度。
|
|
*/
|
|
@Override
|
|
protected TemperatureData doMeasureTemperature(Camera camera) {
|
|
log.info("[" + brandName() + "]实时测温开始,ip={}, channel={}, presetId={}", camera.getIp(), camera.getChannel(), camera.getPresetId());
|
|
NetSDKLib.LLong loginHandle = dahuaLoginService.login(camera);
|
|
if (loginHandle == null) {
|
|
log.error("[" + brandName() + "]测温失败:登录句柄为空,ip={}", camera.getIp());
|
|
return null;
|
|
}
|
|
|
|
// 初始化输入结构体,设置条件参数
|
|
NetSDKLib.NET_IN_RADIOMETRY_GETTEMPER netIn = new NetSDKLib.NET_IN_RADIOMETRY_GETTEMPER();
|
|
netIn.stCondition.nPresetId = camera.getPresetId();
|
|
netIn.stCondition.nRuleId = camera.getRuleId();
|
|
netIn.stCondition.nMeterType = NetSDKLib.NET_RADIOMETRY_METERTYPE.NET_RADIOMETRY_METERTYPE_AREA;
|
|
netIn.stCondition.nChannel = camera.getChannel() - 1;
|
|
|
|
// 输出结构体
|
|
NetSDKLib.NET_OUT_RADIOMETRY_GETTEMPER netOut = new NetSDKLib.NET_OUT_RADIOMETRY_GETTEMPER();
|
|
netOut.stTempInfo = new NetSDKLib.NET_RADIOMETRYINFO();
|
|
netIn.write();
|
|
netOut.write();
|
|
|
|
boolean success = dhNetSDK.CLIENT_QueryDevInfo(
|
|
loginHandle,
|
|
NetSDKLib.NET_QUERY_DEV_RADIOMETRY_TEMPER,
|
|
netIn.getPointer(),
|
|
netOut.getPointer(),
|
|
null,
|
|
5000
|
|
);
|
|
|
|
if (!success) {
|
|
log.error("[" + brandName() + "]获取设备参数失败,错误码:{}", ToolKits.getErrorCodePrint(dhNetSDK.CLIENT_GetLastError()));
|
|
return null;
|
|
}
|
|
|
|
// 将本地内存同步到 Java 字段
|
|
netOut.read();
|
|
NetSDKLib.NET_RADIOMETRYINFO stTempInfo = netOut.stTempInfo;
|
|
log.info("[" + brandName() + "]测温结果:最高={}, 最低={}, 平均={}, 温差={}",
|
|
stTempInfo.fTemperMax, stTempInfo.fTemperMin, stTempInfo.fTemperAver, stTempInfo.fTemperStd);
|
|
return new TemperatureData(
|
|
String.valueOf(stTempInfo.fTemperMax),
|
|
String.valueOf(stTempInfo.fTemperMin),
|
|
stTempInfo.fTemperAver,
|
|
stTempInfo.fTemperStd,
|
|
netIn.stCondition.nPresetId,
|
|
netIn.stCondition.nRuleId
|
|
);
|
|
}
|
|
|
|
/**
|
|
* 大华SDK抓图具体实现(异步)
|
|
* 通过CompletableFuture实现异步回调通知
|
|
*/
|
|
private byte[] snapPictureEx(Camera camera) {
|
|
NetSDKLib.LLong loginID = dahuaLoginService.login(camera);
|
|
|
|
NetSDKLib.SNAP_PARAMS snapParams = new NetSDKLib.SNAP_PARAMS();
|
|
snapParams.Channel = camera.getChannel() - 1; // 通道号从0开始
|
|
snapParams.mode = 0; // 抓图模式:0-单次抓
|
|
snapParams.Quality = 3;
|
|
snapParams.InterSnap = 0;
|
|
int mySerialId = nextSerial();
|
|
snapParams.CmdSerial = mySerialId;
|
|
IntByReference reference = new IntByReference(0);
|
|
// 设置异步抓图回调函数
|
|
dhNetSDK.CLIENT_SetSnapRevCallBack(CAPTURE_RECEIVE_CB, null);
|
|
|
|
String requestKey = loginID.longValue() + ":" + mySerialId;
|
|
CompletableFuture<byte[]> future = new CompletableFuture<>();
|
|
PENDING_REQUESTS.put(requestKey, future);
|
|
final int TIMEOUT_SEC = 5;
|
|
try {
|
|
log.info("[" + brandName() + "]开始抓图,LoginID={},IP={},Channel={},Serial={}", loginID, camera.getIp(), camera.getChannel(), mySerialId);
|
|
boolean isCaptured = dhNetSDK.CLIENT_SnapPictureEx(loginID, snapParams, reference);
|
|
if (!isCaptured) {
|
|
String errorMsg = ToolKits.getErrorCodePrint(dhNetSDK.CLIENT_GetLastError());
|
|
throw new RuntimeException("SDK抓图失败:" + errorMsg);
|
|
}
|
|
return future.get(TIMEOUT_SEC, TimeUnit.SECONDS);
|
|
} catch (TimeoutException e) {
|
|
log.error("[" + brandName() + "]抓图超时:在 {} 秒内未收到设备回调,requestKey={}", TIMEOUT_SEC, requestKey);
|
|
} catch (Exception e) {
|
|
log.error("[" + brandName() + "]抓图异常:", e);
|
|
} finally {
|
|
PENDING_REQUESTS.remove(requestKey);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* CLIENT_SnapPictureEx异步抓图回调函数重写
|
|
*/
|
|
public static class fCaptureReceiveCB implements NetSDKLib.fSnapRev {
|
|
@Override
|
|
public void invoke(NetSDKLib.LLong lLoginID, Pointer pBuf, int RevLen, int EncodeType, int CmdSerial, Pointer dwUser) {
|
|
// 检查是否有等待该流水号的请求
|
|
String requestKey = lLoginID.longValue() + ":" + CmdSerial;
|
|
CompletableFuture<byte[]> future = PENDING_REQUESTS.remove(requestKey);
|
|
if (future != null) {
|
|
log.info("[" + CameraEnum.DAHUA.getName() + "]匹配到抓图回调,LoginID={}, Serial={}", lLoginID, CmdSerial);
|
|
if (pBuf != null && RevLen > 0) {
|
|
// 读取图片数据
|
|
byte[] data = pBuf.getByteArray(0, RevLen);
|
|
// 完成Future,通知主线程
|
|
future.complete(data);
|
|
} else {
|
|
future.completeExceptionally(new RuntimeException("Empty image data"));
|
|
}
|
|
} else {
|
|
// 可能是由于超时已经被移除了,或者是其他类型的抓图
|
|
log.error("[" + CameraEnum.DAHUA.getName() + "]收到未匹配的抓图回调,LoginID={}, Serial={}", lLoginID, CmdSerial);
|
|
}
|
|
}
|
|
}
|
|
}
|