package com.inspect.nvr.service; import com.inspect.nvr.daHuaCarme.jna.NetSDKLib; import com.inspect.nvr.daHuaCarme.jna.dahua.ToolKits; import com.inspect.nvr.domain.Infrared.NvrInfo; 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.nio.file.Files; import java.nio.file.Path; import java.util.concurrent.*; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.locks.ReentrantLock; /** * 大华设备SDK服务 * 同一个IP,同一个通道,串行 * 同一个IP,不同通道,最多4个线程并发 * 不同IP,并发抓图,不限制 */ @Slf4j @Service public class DahuaCameraService extends CommonCameraService { /** * 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(); // 1. [新增]全局流水号生成器 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); } public byte[] capture(NvrInfo nvrInfo, int channel) { String ip = nvrInfo.getNvrIp(); Semaphore nvrSemaphore = getOrCreateSemaphore(ip); ReentrantLock channelLock = getOrCreateLock(ip, channel); // 1.先获取该NVR的全局并发许可 nvrSemaphore.acquireUninterruptibly(); try { // 2.再获取该通道的独占锁(保证同一通道串行) channelLock.lock(); try { return captureWithRetry(nvrInfo, channel); } finally { // 必须先unlock通道锁,再释放NVR全局许可 if (channelLock.isHeldByCurrentThread()) { channelLock.unlock(); } } } finally { // 3.释放NVR全局许可 nvrSemaphore.release(); } } private byte[] captureWithRetry(NvrInfo nvrInfo, int channel) { Path fullPath = getFullPath("dh", nvrInfo.getNvrIp(), channel); int retryCount = 0; int maxRetries = DEFAULT_MAX_RETRIES; while (retryCount < maxRetries) { try { byte[] imageBytes = snapPictureEx(nvrInfo, channel, fullPath); log.info("[大华]抓图成功:第{}次,文件地址:{}", retryCount + 1, fullPath); return imageBytes; } catch (Exception e) { log.error("[大华]抓图异常:第{}次,{}", retryCount + 1, e.getMessage()); } finally { retryCount++; } } // 当SDK抓图失败时,尝试使用Digest认证抓图 byte[] imageBytes = captureDigest(nvrInfo, channel, DIGEST_URL_TEMPLATE, fullPath); if (imageBytes == null) { // 当所有抓图方式均失败时,记录失败图片 writeCaptureFailedImage(fullPath); log.error("[大华]所有抓图方式均失败,图片地址:{}", fullPath); return new byte[0]; } log.info("[大华]digest抓图成功,图片地址:{}", fullPath); return imageBytes; } /** * 大华SDK抓图具体实现(异步) * 通过CompletableFuture实现异步回调通知 */ private byte[] snapPictureEx(NvrInfo nvrInfo, int channel, Path fullPath) { NetSDKLib.LLong loginID = dahuaLoginService.login(nvrInfo); NetSDKLib.SNAP_PARAMS snapParams = new NetSDKLib.SNAP_PARAMS(); snapParams.Channel = channel - 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("[大华]开始抓图,LoginID={},IP={},Channel={},Serial={}", loginID, nvrInfo.getNvrIp(), channel, mySerialId); boolean isCaptured = dhNetSDK.CLIENT_SnapPictureEx(loginID, snapParams, reference); if (!isCaptured) { String errorMsg = ToolKits.getErrorCodePrint(dhNetSDK.CLIENT_GetLastError()); throw new RuntimeException("SDK抓图失败:" + errorMsg); } byte[] byteArray = future.get(TIMEOUT_SEC, TimeUnit.SECONDS); Files.write(fullPath, byteArray); return byteArray; } catch (TimeoutException e) { log.error("[大华]抓图超时:在{}秒内未收到设备回调", TIMEOUT_SEC); } catch (Exception e) { log.error("[大华]抓图异常:", e); } finally { PENDING_REQUESTS.remove(requestKey); } return null; } /** * Digest认证抓图 */ public byte[] captureDigest(NvrInfo nvrInfo, int channel) { return captureDigest(nvrInfo, channel, DIGEST_URL_TEMPLATE, getFullPath("dh_digest", nvrInfo.getNvrIp(), channel)); } /** * 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) { // 1. 检查是否有等待该流水号的请求 String requestKey = lLoginID.longValue() + ":" + CmdSerial; CompletableFuture future = PENDING_REQUESTS.remove(requestKey); if (future != null) { log.error("[大华]匹配到抓图回调,LoginID={}, Serial={}", lLoginID, CmdSerial); if (pBuf != null && RevLen > 0) { // 2. 读取图片数据 byte[] data = pBuf.getByteArray(0, RevLen); // 3. 完成Future,通知主线程 future.complete(data); } else { future.completeExceptionally(new RuntimeException("Empty image data")); } } else { // 可能是由于超时已经被移除了,或者是其他类型的抓图 log.error("[大华]收到未匹配的抓图回调,LoginID={}, Serial={}", lLoginID, CmdSerial); } } } }