You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 

172 lines
7.3 KiB

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);
}
}
}
}