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://<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();
|
|
// 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<byte[]> 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("[大华]抓图超时:在 {} 秒内未收到设备回调,requestKey={}", TIMEOUT_SEC, requestKey);
|
|
} 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<byte[]> future = PENDING_REQUESTS.remove(requestKey);
|
|
if (future != null) {
|
|
log.info("[大华]匹配到抓图回调,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);
|
|
}
|
|
}
|
|
}
|
|
}
|