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:///cgi-bin/snapshot.cgi?channel=&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> 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 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 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); } } } }