|
|
|
@ -0,0 +1,268 @@ |
|
|
|
package com.inspect.nvr.service; |
|
|
|
|
|
|
|
import com.inspect.nvr.domain.Infrared.NvrInfo; |
|
|
|
import com.inspect.nvr.domain.Infrared.TemperatureData; |
|
|
|
import com.inspect.nvr.hikVision.utils.jna.HCNetSDK; |
|
|
|
import com.sun.jna.Pointer; |
|
|
|
import com.sun.jna.ptr.IntByReference; |
|
|
|
import lombok.extern.slf4j.Slf4j; |
|
|
|
import org.springframework.stereotype.Service; |
|
|
|
import org.springframework.util.StreamUtils; |
|
|
|
|
|
|
|
import javax.annotation.Resource; |
|
|
|
import java.io.File; |
|
|
|
import java.io.FileOutputStream; |
|
|
|
import java.io.InputStream; |
|
|
|
import java.nio.file.Files; |
|
|
|
import java.nio.file.Path; |
|
|
|
import java.util.concurrent.*; |
|
|
|
import java.util.concurrent.atomic.AtomicLong; |
|
|
|
import java.util.concurrent.locks.ReentrantLock; |
|
|
|
|
|
|
|
/** |
|
|
|
* 海康设备SDK服务 |
|
|
|
* 同一个IP,同一个通道,串行 |
|
|
|
* 同一个IP,不同通道,最多4个线程并发 |
|
|
|
* 不同IP,并发抓图,不限制 |
|
|
|
*/ |
|
|
|
@Slf4j |
|
|
|
@Service |
|
|
|
public class HikCameraService extends CommonCameraService { |
|
|
|
/** |
|
|
|
* Digest认证抓图URL: |
|
|
|
* http://<host>/ISAPI/Streaming/channels/<channel><subtype>/picture |
|
|
|
* subtype: 01-主码流, 02-子码流 |
|
|
|
*/ |
|
|
|
private static final String DIGEST_URL_TEMPLATE = "http://%s/ISAPI/Streaming/channels/%d02/picture"; |
|
|
|
// 实时测温执行结果映射,避免并发访问冲突 |
|
|
|
private static final ConcurrentHashMap<Long, CompletableFuture<TemperatureData>> THERMOMETRY_REQUESTS = new ConcurrentHashMap<>(); |
|
|
|
// 实时测温请求的唯一ID生成器 |
|
|
|
private static final AtomicLong THERMOMETRY_SERIAL = new AtomicLong(1); |
|
|
|
@Resource |
|
|
|
private HikLoginService hikLoginService; |
|
|
|
@Resource |
|
|
|
private HCNetSDK hcNetSDK; |
|
|
|
|
|
|
|
public <T> T withConcurrencyControl(NvrInfo nvrInfo, int channel, Callable<T> task) { |
|
|
|
String ip = nvrInfo.getNvrIp(); |
|
|
|
Semaphore nvrSemaphore = getOrCreateSemaphore(ip); |
|
|
|
ReentrantLock channelLock = getOrCreateLock(ip, channel); |
|
|
|
// 1.先获取该NVR的全局并发许可 |
|
|
|
nvrSemaphore.acquireUninterruptibly(); |
|
|
|
try { |
|
|
|
// 2.再获取该通道的独占锁(保证同一通道串行) |
|
|
|
channelLock.lock(); |
|
|
|
try { |
|
|
|
return task.call(); |
|
|
|
} catch (Exception e) { |
|
|
|
log.error("[海康]任务执行异常:", e); |
|
|
|
return null; |
|
|
|
} finally { |
|
|
|
// 必须先unlock通道锁,再释放NVR全局许可 |
|
|
|
if (channelLock.isHeldByCurrentThread()) { |
|
|
|
channelLock.unlock(); |
|
|
|
} |
|
|
|
} |
|
|
|
} finally { |
|
|
|
// 3.释放NVR全局许可 |
|
|
|
nvrSemaphore.release(); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* 受并发控制的海康抓图 |
|
|
|
*/ |
|
|
|
public byte[] capture(NvrInfo nvrInfo, int channel) { |
|
|
|
return withConcurrencyControl(nvrInfo, channel, () -> captureWithRetry(nvrInfo, channel)); |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* Digest认证抓图 |
|
|
|
*/ |
|
|
|
public byte[] captureDigest(NvrInfo nvrInfo, int channel) { |
|
|
|
return withConcurrencyControl(nvrInfo, channel, () -> captureDigest(nvrInfo, channel, DIGEST_URL_TEMPLATE, getFullPath("hk_digest", nvrInfo.getNvrIp(), channel))); |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* 受并发控制的海康实时测温 |
|
|
|
*/ |
|
|
|
public TemperatureData realTimeThermometry(NvrInfo nvrInfo, int channel) { |
|
|
|
return withConcurrencyControl(nvrInfo, channel, () -> getRealTimeThermometry(nvrInfo, channel)); |
|
|
|
} |
|
|
|
|
|
|
|
private byte[] captureWithRetry(NvrInfo nvrInfo, int channel) { |
|
|
|
Path fullPath = getFullPath("hk", nvrInfo.getNvrIp(), channel); |
|
|
|
ensureDirectoryExists(fullPath.getParent()); |
|
|
|
int retryCount = 0; |
|
|
|
int maxRetries = DEFAULT_MAX_RETRIES; |
|
|
|
while (retryCount < maxRetries) { |
|
|
|
try { |
|
|
|
// byte[] imageBytes = captureJPEGPicture(nvrInfo, channel, fullPath); |
|
|
|
byte[] imageBytes = captureJPEGPictureNew(nvrInfo, channel, fullPath); |
|
|
|
if (imageBytes == null) { |
|
|
|
int errorCode = hcNetSDK.NET_DVR_GetLastError(); |
|
|
|
throw new RuntimeException("SDK抓图失败,错误码" + errorCode); |
|
|
|
} |
|
|
|
log.info("[海康]抓图成功(第{}次):{}", retryCount + 1, fullPath); |
|
|
|
return imageBytes; |
|
|
|
} catch (Exception e) { |
|
|
|
log.error("[海康]抓图异常(第{}次):{}", retryCount + 1, e.getMessage()); |
|
|
|
try { |
|
|
|
Thread.sleep(2000); |
|
|
|
} catch (InterruptedException ignored) { |
|
|
|
Thread.currentThread().interrupt(); |
|
|
|
break; |
|
|
|
} |
|
|
|
} finally { |
|
|
|
retryCount++; |
|
|
|
} |
|
|
|
} |
|
|
|
// 当SDK抓图失败时,尝试使用Digest认证抓图 |
|
|
|
byte[] imageBytes = captureDigest(nvrInfo, channel, DIGEST_URL_TEMPLATE, fullPath); |
|
|
|
if (imageBytes == null) { |
|
|
|
// 当所有抓图方式均失败时,记录失败图片 |
|
|
|
writeCaptureFailedImage(fullPath); |
|
|
|
log.info("[海康]所有抓图方式均失败,图片地址:{}", fullPath); |
|
|
|
return new byte[0]; |
|
|
|
} |
|
|
|
log.info("[海康]digest抓图成功,图片地址:{}", fullPath); |
|
|
|
return imageBytes; |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* 大华SDK抓图具体实现(旧版) |
|
|
|
*/ |
|
|
|
private byte[] captureJPEGPicture(NvrInfo nvrInfo, int channel, Path fullPath) throws Exception { |
|
|
|
int userId = hikLoginService.login(nvrInfo); |
|
|
|
// 兼容 C/C++ 编写的本地库, C 语言用 \0 标记字符串结束 |
|
|
|
HCNetSDK.NET_DVR_JPEGPARA jpegpara = new HCNetSDK.NET_DVR_JPEGPARA(); |
|
|
|
jpegpara.wPicSize = 0xff; |
|
|
|
jpegpara.wPicQuality = 1; |
|
|
|
jpegpara.write(); |
|
|
|
byte[] filePathBytes = (fullPath.toString() + "\0").getBytes("GBK"); |
|
|
|
boolean isCaptured = hcNetSDK.NET_DVR_CaptureJPEGPicture(userId, channel, jpegpara, filePathBytes); |
|
|
|
if (isCaptured) { |
|
|
|
// 验证文件是否有效 |
|
|
|
if (!isValidJpeg(fullPath)) { |
|
|
|
// 文件乱码,修改文件名 |
|
|
|
log.info("[海康]文件无效,图片地址:{}", fullPath); |
|
|
|
String dirName = fullPath.getParent().toString(); |
|
|
|
String fileName = fullPath.getFileName().toString(); |
|
|
|
File dir = new File(dirName); |
|
|
|
File[] files = dir.listFiles((file, name) -> name.startsWith(fileName)); |
|
|
|
if (files != null) { |
|
|
|
for (File file : files) { |
|
|
|
file.renameTo(new File(fullPath.toString())); |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
// 读取文件并返回字节数组 |
|
|
|
try (InputStream inputStream = Files.newInputStream(fullPath)) { |
|
|
|
return StreamUtils.copyToByteArray(inputStream); |
|
|
|
} |
|
|
|
} |
|
|
|
return null; |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* 大华SDK抓图具体实现(新版) |
|
|
|
*/ |
|
|
|
private byte[] captureJPEGPictureNew(NvrInfo nvrInfo, int channel, Path fullPath) throws Exception { |
|
|
|
int userId = hikLoginService.login(nvrInfo); |
|
|
|
HCNetSDK.NET_DVR_JPEGPARA jpegpara = new HCNetSDK.NET_DVR_JPEGPARA(); |
|
|
|
jpegpara.wPicSize = 0xff; |
|
|
|
jpegpara.wPicQuality = 0; |
|
|
|
jpegpara.write(); |
|
|
|
HCNetSDK.BYTE_ARRAY byteArray = new HCNetSDK.BYTE_ARRAY(10 * 1024 * 1024); |
|
|
|
IntByReference ret = new IntByReference(0); |
|
|
|
log.info("[海康]开始抓图,UserID={},IP={},Channel={}", userId, nvrInfo.getNvrIp(), channel); |
|
|
|
boolean isCaptured = hcNetSDK.NET_DVR_CaptureJPEGPicture_NEW(userId, channel, jpegpara, byteArray.getPointer(), byteArray.size(), ret); |
|
|
|
if (isCaptured) { |
|
|
|
byteArray.read(); |
|
|
|
byte[] imageBytes = byteArray.byValue; |
|
|
|
// 图片写入本地 |
|
|
|
try (FileOutputStream fos = new FileOutputStream(fullPath.toString())) { |
|
|
|
fos.write(imageBytes, 0, ret.getValue()); |
|
|
|
} |
|
|
|
return imageBytes; |
|
|
|
} |
|
|
|
return null; |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* 海康SDK实施测温具体实现(异步) |
|
|
|
* 通过CompletableFuture实现异步回调通知 |
|
|
|
*/ |
|
|
|
private TemperatureData getRealTimeThermometry(NvrInfo nvrInfo, int channel) { |
|
|
|
int userId = hikLoginService.login(nvrInfo); |
|
|
|
HCNetSDK.NET_DVR_REALTIME_THERMOMETRY_COND cond = new HCNetSDK.NET_DVR_REALTIME_THERMOMETRY_COND(); |
|
|
|
cond.dwSize = cond.size(); |
|
|
|
cond.dwChan = channel; |
|
|
|
cond.byRuleID = 1;//规则ID,0代表获取全部规则,具体规则ID从1开始 |
|
|
|
cond.byMode = 1;//长连接模式:0-保留;1-定时模式;2-温差模式 |
|
|
|
cond.wInterval = 5;//上传间隔(仅温差模式支持),取值范围:1-3600 秒,填0则默认3600S上传一次 |
|
|
|
cond.write(); // 手动同步结构体到内存 |
|
|
|
|
|
|
|
Pointer lpInBuffer = cond.getPointer(); |
|
|
|
int dwInBufferSize = cond.size(); |
|
|
|
|
|
|
|
// 为每个测温请求生成唯一的请求ID |
|
|
|
long requestId = THERMOMETRY_SERIAL.getAndIncrement(); |
|
|
|
Pointer pUserData = Pointer.createConstant(requestId); |
|
|
|
|
|
|
|
CompletableFuture<TemperatureData> future = new CompletableFuture<>(); |
|
|
|
THERMOMETRY_REQUESTS.put(requestId, future); |
|
|
|
final int TIMEOUT_SEC = 5; |
|
|
|
// 测温返回的句柄 |
|
|
|
int lHandle = -1; |
|
|
|
try { |
|
|
|
log.info("[海康]开始实时测温,UserID={},IP={},Channel={},requestId={}", userId, nvrInfo.getNvrIp(), channel, requestId); |
|
|
|
lHandle = hcNetSDK.NET_DVR_StartRemoteConfig(userId, HCNetSDK.NET_DVR_GET_REALTIME_THERMOMETRY, lpInBuffer, dwInBufferSize, new fRemoteConfigCB(), pUserData); |
|
|
|
if (lHandle < 0) { |
|
|
|
int errorCode = hcNetSDK.NET_DVR_GetLastError(); |
|
|
|
throw new RuntimeException("SDK测温失败,错误码:" + errorCode); |
|
|
|
} |
|
|
|
|
|
|
|
return future.get(TIMEOUT_SEC, TimeUnit.SECONDS); |
|
|
|
} catch (TimeoutException e) { |
|
|
|
log.error("[海康]测温超时:在 " + TIMEOUT_SEC + " 秒内未收到设备回调"); |
|
|
|
} catch (Exception e) { |
|
|
|
log.error("[海康]测温异常:{}", e.getMessage()); |
|
|
|
} finally { |
|
|
|
THERMOMETRY_REQUESTS.remove(requestId); |
|
|
|
hcNetSDK.NET_DVR_StopRemoteConfig(lHandle); |
|
|
|
} |
|
|
|
return null; |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* NET_DVR_StartRemoteConfig实时测温回调函数重写 |
|
|
|
*/ |
|
|
|
public static class fRemoteConfigCB implements HCNetSDK.FRemoteConfigCallBack { |
|
|
|
@Override |
|
|
|
public void invoke(int dwType, Pointer lpBuffer, int dwBufLen, Pointer pUserData) { |
|
|
|
long requestId = Pointer.nativeValue(pUserData); |
|
|
|
CompletableFuture<TemperatureData> future = THERMOMETRY_REQUESTS.remove(requestId); |
|
|
|
if (future != null) { |
|
|
|
log.info("[海康]匹配到测温回调,类型: {}, 长度:{}", dwType, dwBufLen); |
|
|
|
if (dwType == 2) { |
|
|
|
HCNetSDK.NET_DVR_THERMOMETRY_UPLOAD thermometryUpload = new HCNetSDK.NET_DVR_THERMOMETRY_UPLOAD(); |
|
|
|
thermometryUpload.write(); |
|
|
|
Pointer pmt = thermometryUpload.getPointer(); |
|
|
|
pmt.write(0, lpBuffer.getByteArray(0, thermometryUpload.size()), 0, thermometryUpload.size()); |
|
|
|
thermometryUpload.read(); |
|
|
|
|
|
|
|
String strTemp = "规则ID:" + thermometryUpload.byRuleID + "规则名称:" + thermometryUpload.szRuleName + "规则类型:" + thermometryUpload.byRuleCalibType + "预置点号:" + thermometryUpload.wPresetNo + "点温度:" + thermometryUpload.struPointThermCfg.fTemperature + "点坐标:" + thermometryUpload.struPointThermCfg.struPoint.fX + "," + thermometryUpload.struPointThermCfg.struPoint.fY + "区域最高温度:" + thermometryUpload.struLinePolygonThermCfg.fMaxTemperature + "区域最低温度:" + thermometryUpload.struLinePolygonThermCfg.fMinTemperature + "区域平均温度:" + thermometryUpload.struLinePolygonThermCfg.fAverageTemperature + "区域温差:" + thermometryUpload.struLinePolygonThermCfg.fTemperatureDiff + "\n"; |
|
|
|
log.info("[海康]实时测温成功,requestId={}, data={}", requestId, strTemp); |
|
|
|
|
|
|
|
HCNetSDK.NET_DVR_LINEPOLYGON_THERM_CFG struLinePolygonThermCfg = thermometryUpload.struLinePolygonThermCfg; |
|
|
|
// 封装所有温度数据 |
|
|
|
TemperatureData data = new TemperatureData(String.valueOf(struLinePolygonThermCfg.fMaxTemperature), String.valueOf(struLinePolygonThermCfg.fMinTemperature), struLinePolygonThermCfg.fAverageTemperature, struLinePolygonThermCfg.fTemperatureDiff, thermometryUpload.dwChan, thermometryUpload.byRuleID); |
|
|
|
future.complete(data); |
|
|
|
} |
|
|
|
} else { |
|
|
|
log.error("[海康]收到未匹配的测温回调, requestId={}", requestId); |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |