From 8c5d685c4db9792200c6c12f731bff6d389d68bc Mon Sep 17 00:00:00 2001 From: yinhuaiwei Date: Thu, 18 Dec 2025 16:42:36 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20=E9=87=8D=E6=9E=84=E6=B5=B7?= =?UTF-8?q?=E5=BA=B7=E5=AE=9E=E6=97=B6=E6=B5=8B=E6=B8=A9=E6=96=B9=E6=B3=95?= =?UTF-8?q?=EF=BC=8C=E5=B0=86=E6=B5=8B=E6=B8=A9=E5=92=8C=E6=8A=93=E5=9B=BE?= =?UTF-8?q?=E6=96=B9=E6=B3=95=E5=BD=92=E4=B8=80=E7=AE=A1=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../nvr/controller/InfraredController.java | 3 +- .../nvr/controller/IvsCameraController.java | 3 +- ...eService.java => CommonCameraService.java} | 10 +- ...reService.java => DahuaCameraService.java} | 40 +-- .../inspect/nvr/service/HikCameraService.java | 268 ++++++++++++++++++ .../nvr/service/HikCaptureService.java | 161 ----------- .../service/impl/HikVisionServiceImpl.java | 145 +++++----- .../service/impl/IvsCameraServiceImpl.java | 24 +- 8 files changed, 386 insertions(+), 268 deletions(-) rename src/main/java/com/inspect/nvr/service/{BaseCaptureService.java => CommonCameraService.java} (97%) rename src/main/java/com/inspect/nvr/service/{DahuaCaptureService.java => DahuaCameraService.java} (90%) create mode 100644 src/main/java/com/inspect/nvr/service/HikCameraService.java delete mode 100644 src/main/java/com/inspect/nvr/service/HikCaptureService.java diff --git a/src/main/java/com/inspect/nvr/controller/InfraredController.java b/src/main/java/com/inspect/nvr/controller/InfraredController.java index 9c2b0f1..4886ad4 100644 --- a/src/main/java/com/inspect/nvr/controller/InfraredController.java +++ b/src/main/java/com/inspect/nvr/controller/InfraredController.java @@ -1,6 +1,7 @@ package com.inspect.nvr.controller; import com.alibaba.fastjson.JSONObject; +import com.inspect.nvr.aop.TimeTrace; import com.inspect.nvr.domain.Infrared.*; import com.inspect.nvr.service.HikVisionService; import com.inspect.nvr.hikVision.utils.AjaxResult; @@ -11,7 +12,6 @@ import org.springframework.core.io.InputStreamResource; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; -import javax.annotation.Resource; import java.io.InputStream; import java.util.ArrayList; import java.util.HashMap; @@ -53,6 +53,7 @@ public class InfraredController { // public AjaxResult cameraHongWaiHk(@RequestBody Camera camera) { // return hikVisionService.cameraHongWaiHk(camera); // } + @TimeTrace @PostMapping("/cameraHong") @ResponseBody public AjaxResult cameraHong(@RequestBody Camera camera) { diff --git a/src/main/java/com/inspect/nvr/controller/IvsCameraController.java b/src/main/java/com/inspect/nvr/controller/IvsCameraController.java index 991e8a7..3e153ba 100644 --- a/src/main/java/com/inspect/nvr/controller/IvsCameraController.java +++ b/src/main/java/com/inspect/nvr/controller/IvsCameraController.java @@ -102,12 +102,11 @@ public class IvsCameraController { @TimeTrace @GetMapping("/digest") - public ResponseEntity rtsp(@RequestParam("filesessionid") String fileSessionId) { + public ResponseEntity digest(@RequestParam("filesessionid") String fileSessionId) { byte[] bytes = ivsCameraService.captureDigest(fileSessionId); if (bytes == null || bytes.length == 0) { return ResponseEntity.ok().body("Not OK"); } return ResponseEntity.ok().body("OK"); } - } diff --git a/src/main/java/com/inspect/nvr/service/BaseCaptureService.java b/src/main/java/com/inspect/nvr/service/CommonCameraService.java similarity index 97% rename from src/main/java/com/inspect/nvr/service/BaseCaptureService.java rename to src/main/java/com/inspect/nvr/service/CommonCameraService.java index 31f4870..83648fd 100644 --- a/src/main/java/com/inspect/nvr/service/BaseCaptureService.java +++ b/src/main/java/com/inspect/nvr/service/CommonCameraService.java @@ -26,10 +26,10 @@ import java.util.concurrent.Semaphore; import java.util.concurrent.locks.ReentrantLock; /** - * 抓图基础类 + * 通用摄像头服务类 */ @Slf4j -public class BaseCaptureService { +public class CommonCameraService { // 兜底图路径 static final String IMAGE_CAPTURE_FAILED = "images\\imageCaptureFailed.jpg"; // 抓图保存目录 @@ -47,7 +47,7 @@ public class BaseCaptureService { * 抓图失败时,写入兜底图 * 可考虑返回byte[] */ - static void WriteCaptureFailedImage(Path fullPath) { + static void writeCaptureFailedImage(Path fullPath) { ClassPathResource imgFile = new ClassPathResource(IMAGE_CAPTURE_FAILED); try (InputStream inputStream = imgFile.getInputStream(); FileOutputStream out = new FileOutputStream(fullPath.toFile())) { @@ -90,7 +90,7 @@ public class BaseCaptureService { while ((len = in.read(buffer)) != -1) { out.write(buffer, 0, len); } - log.info("Digest认证抓图成功:图片地址{}", fullPath.toString()); + log.info("Digest认证抓图成功:图片地址{}", fullPath); return Files.readAllBytes(fullPath); } } else { @@ -130,7 +130,7 @@ public class BaseCaptureService { /** * 简单校验是否为有效 JPEG(检查文件头) */ - boolean isValidJPEG(Path file) { + boolean isValidJpeg(Path file) { try { byte[] header = Files.readAllBytes(file); return header.length >= 2 && diff --git a/src/main/java/com/inspect/nvr/service/DahuaCaptureService.java b/src/main/java/com/inspect/nvr/service/DahuaCameraService.java similarity index 90% rename from src/main/java/com/inspect/nvr/service/DahuaCaptureService.java rename to src/main/java/com/inspect/nvr/service/DahuaCameraService.java index 32da245..4a08a20 100644 --- a/src/main/java/com/inspect/nvr/service/DahuaCaptureService.java +++ b/src/main/java/com/inspect/nvr/service/DahuaCameraService.java @@ -16,14 +16,14 @@ import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.locks.ReentrantLock; /** - * 大华设备抓图服务 - * 同一个IP,同一个通道,串行抓图 - * 同一个IP,不同通道,最多4个线程并发抓图 + * 大华设备SDK服务 + * 同一个IP,同一个通道,串行 + * 同一个IP,不同通道,最多4个线程并发 * 不同IP,并发抓图,不限制 */ @Slf4j @Service -public class DahuaCaptureService extends BaseCaptureService { +public class DahuaCameraService extends CommonCameraService { /** * Digest认证抓图URL: * http:///cgi-bin/snapshot.cgi?channel=&subtype= @@ -87,7 +87,7 @@ public class DahuaCaptureService extends BaseCaptureService { byte[] imageBytes = captureDigest(nvrInfo, channel, DIGEST_URL_TEMPLATE, fullPath); if (imageBytes == null) { // 当所有抓图方式均失败时,记录失败图片 - WriteCaptureFailedImage(fullPath); + writeCaptureFailedImage(fullPath); log.error("[大华]所有抓图方式均失败,图片地址:{}", fullPath); return new byte[0]; } @@ -96,10 +96,10 @@ public class DahuaCaptureService extends BaseCaptureService { } /** - * 大华SDK抓图具体实现(异步抓图) + * 大华SDK抓图具体实现(异步) * 通过CompletableFuture实现异步回调通知 */ - private byte[] snapPictureEx(NvrInfo nvrInfo, int channel, Path fullPath) throws Exception { + private byte[] snapPictureEx(NvrInfo nvrInfo, int channel, Path fullPath) { NetSDKLib.LLong loginID = dahuaLoginService.login(nvrInfo); NetSDKLib.SNAP_PARAMS snapParams = new NetSDKLib.SNAP_PARAMS(); @@ -121,7 +121,6 @@ public class DahuaCaptureService extends BaseCaptureService { log.info("[大华]开始抓图,LoginID={},IP={},Channel={},Serial={}", loginID, nvrInfo.getNvrIp(), channel, mySerialId); boolean isCaptured = dhNetSDK.CLIENT_SnapPictureEx(loginID, snapParams, reference); if (!isCaptured) { - PENDING_REQUESTS.remove(requestKey); String errorMsg = ToolKits.getErrorCodePrint(dhNetSDK.CLIENT_GetLastError()); throw new RuntimeException("SDK抓图失败:" + errorMsg); } @@ -129,13 +128,20 @@ public class DahuaCaptureService extends BaseCaptureService { Files.write(fullPath, byteArray); return byteArray; } catch (TimeoutException e) { - // 超时移除 - PENDING_REQUESTS.remove(requestKey); - throw new TimeoutException("[大华]抓图超时:在 " + TIMEOUT_SEC + " 秒内未收到设备回调"); + log.error("[大华]抓图超时:在{}秒内未收到设备回调", TIMEOUT_SEC); } catch (Exception e) { + log.error("[大华]抓图异常:", e); + } finally { PENDING_REQUESTS.remove(requestKey); - throw e; } + return null; + } + + /** + * Digest认证抓图 + */ + public byte[] captureDigest(NvrInfo nvrInfo, int channel) { + return captureDigest(nvrInfo, channel, DIGEST_URL_TEMPLATE, getFullPath("dh_digest", nvrInfo.getNvrIp(), channel)); } /** @@ -148,6 +154,7 @@ public class DahuaCaptureService extends BaseCaptureService { 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); @@ -158,15 +165,8 @@ public class DahuaCaptureService extends BaseCaptureService { } } else { // 可能是由于超时已经被移除了,或者是其他类型的抓图 - log.error("[大华] 收到未匹配的抓图回调,LoginID={}, Serial={}", lLoginID, CmdSerial); + log.error("[大华]收到未匹配的抓图回调,LoginID={}, Serial={}", lLoginID, CmdSerial); } } } - - /** - * Digest认证抓图 - */ - public byte[] captureDigest(NvrInfo nvrInfo, int channel) { - return captureDigest(nvrInfo, channel, DIGEST_URL_TEMPLATE, getFullPath("dh_digest", nvrInfo.getNvrIp(), channel)); - } } diff --git a/src/main/java/com/inspect/nvr/service/HikCameraService.java b/src/main/java/com/inspect/nvr/service/HikCameraService.java new file mode 100644 index 0000000..40d3d26 --- /dev/null +++ b/src/main/java/com/inspect/nvr/service/HikCameraService.java @@ -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:///ISAPI/Streaming/channels//picture + * subtype: 01-主码流, 02-子码流 + */ + private static final String DIGEST_URL_TEMPLATE = "http://%s/ISAPI/Streaming/channels/%d02/picture"; + // 实时测温执行结果映射,避免并发访问冲突 + private static final ConcurrentHashMap> THERMOMETRY_REQUESTS = new ConcurrentHashMap<>(); + // 实时测温请求的唯一ID生成器 + private static final AtomicLong THERMOMETRY_SERIAL = new AtomicLong(1); + @Resource + private HikLoginService hikLoginService; + @Resource + private HCNetSDK hcNetSDK; + + public T withConcurrencyControl(NvrInfo nvrInfo, int channel, Callable 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 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 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); + } + } + } +} \ No newline at end of file diff --git a/src/main/java/com/inspect/nvr/service/HikCaptureService.java b/src/main/java/com/inspect/nvr/service/HikCaptureService.java deleted file mode 100644 index ac57d22..0000000 --- a/src/main/java/com/inspect/nvr/service/HikCaptureService.java +++ /dev/null @@ -1,161 +0,0 @@ -package com.inspect.nvr.service; - -import com.inspect.nvr.domain.Infrared.NvrInfo; -import com.inspect.nvr.hikVision.utils.jna.HCNetSDK; -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.Semaphore; -import java.util.concurrent.locks.ReentrantLock; - -/** - * 海康设备抓图服务 - * 同一个IP,同一个通道,串行抓图 - * 同一个IP,不同通道,最多4个线程并发抓图 - * 不同IP,并发抓图,不限制 - */ -@Slf4j -@Service -public class HikCaptureService extends BaseCaptureService { - /** - * Digest认证抓图URL: - * http:///ISAPI/Streaming/channels//picture - * subtype: 01-主码流, 02-子码流 - */ - private static final String DIGEST_URL_TEMPLATE = "http://%s/ISAPI/Streaming/channels/%d02/picture"; - @Resource - private HikLoginService hikLoginService; - @Resource - private HCNetSDK hcNetSDK; - - 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("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; - } - - /** - * 旧版抓图,备用 - */ - 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; - } - - 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; - } - - public byte[] captureDigest(NvrInfo nvrInfo, int channel) { - return captureDigest(nvrInfo, channel, DIGEST_URL_TEMPLATE, getFullPath("hk_digest", nvrInfo.getNvrIp(), channel)); - } -} diff --git a/src/main/java/com/inspect/nvr/service/impl/HikVisionServiceImpl.java b/src/main/java/com/inspect/nvr/service/impl/HikVisionServiceImpl.java index 140c9da..7f75825 100644 --- a/src/main/java/com/inspect/nvr/service/impl/HikVisionServiceImpl.java +++ b/src/main/java/com/inspect/nvr/service/impl/HikVisionServiceImpl.java @@ -17,6 +17,7 @@ import com.inspect.nvr.hikVision.utils.AjaxResult; import com.inspect.nvr.hikVision.utils.StringUtils; import com.inspect.nvr.hikVision.utils.jna.HCNetSDK; import com.inspect.nvr.hikVision.utils.jna.HikVisionUtils; +import com.inspect.nvr.service.HikCameraService; import com.inspect.nvr.service.HikFRemoteConfigCallBack_imp; import com.inspect.nvr.service.HikLoginService; import com.inspect.nvr.service.HikVisionService; @@ -66,14 +67,10 @@ import java.util.concurrent.*; @Service public class HikVisionServiceImpl implements HikVisionService { + private static HikFExceptionCallBack_Imp fExceptionCallBack; private final Logger log = LoggerFactory.getLogger(this.getClass()); - @Autowired private HCNetSDK hcNetSDK; - - private static HikFExceptionCallBack_Imp fExceptionCallBack; - - private String picPath; // @Value("${file.hrUavUrl:test}") @@ -112,16 +109,69 @@ public class HikVisionServiceImpl implements HikVisionService { @Resource private RedisService redisService; + @Resource + private HikCameraService hikCameraService; + private Integer lUserID; + @Autowired + private HikLoginService hikLoginService; + + //二维数组转 CSV + private static String convertMatrixToCsv(float[][] matrix) { + StringBuilder csvBuilder = new StringBuilder(); + for (float[] row : matrix) { + for (int i = 0; i < row.length; i++) { + csvBuilder.append(row[i]); + if (i < row.length - 1) { + csvBuilder.append(","); + } + } + csvBuilder.append("\n"); + } + return csvBuilder.toString(); + } + + public static String truncateFileNameToCsv(String originalPath) { + // 获取文件名部分(最后一个 '/' 之后的内容) + int lastSlashIndex = originalPath.lastIndexOf('/'); + if (lastSlashIndex == -1) { + // 如果没有路径分隔符,直接处理整个字符串 + return processFileNameToCsv(originalPath); + } + + // 分离路径和文件名 + String path = originalPath.substring(0, lastSlashIndex + 1); + String fileName = originalPath.substring(lastSlashIndex + 1); + + // 处理文件名部分并强制改为.csv + String processedFileName = processFileNameToCsv(fileName); + + + return path + processedFileName; + } + + private static String processFileNameToCsv(String fileName) { + // 去掉文件扩展名(如果有) + int lastDotIndex = fileName.lastIndexOf('.'); + if (lastDotIndex != -1) { + fileName = fileName.substring(0, lastDotIndex); + } + + // 去掉最后一个 '_' 及其后面的部分 + int lastUnderscoreIndex = fileName.lastIndexOf('_'); + if (lastUnderscoreIndex != -1) { + fileName = fileName.substring(0, lastUnderscoreIndex); + } + + // 强制添加.csv后缀 + return fileName + ".csv"; + } @Override public AjaxResult login(NvrInfo nvrInfo) { return login_V40(nvrInfo); } - @Autowired - private HikLoginService hikLoginService; - /** * 设备登录V40 与V30功能一致 @@ -172,9 +222,25 @@ public class HikVisionServiceImpl implements HikVisionService { } } - - //实时测温接口,获取最高温和最低温 + /** + * 海康实时测温接口 + * 优化并发场景 + */ public TemperatureData StartRemote(Camera camera) { + NvrInfo nvrInfo = new NvrInfo(); + nvrInfo.setNvrIp(camera.getIp()); + nvrInfo.setServerPort(camera.getPort()); + nvrInfo.setAccount(camera.getUserName()); + nvrInfo.setPassword(camera.getPassword()); + TemperatureData data = hikCameraService.realTimeThermometry(nvrInfo, camera.getChannel()); + return data; + } + + /** + * @deprecated 实时测温接口,获取最高温和最低温 + */ + @Deprecated + public TemperatureData StartRemoteOld(Camera camera) { int result = 0; TemperatureData temperatureData = null; @@ -316,7 +382,6 @@ public class HikVisionServiceImpl implements HikVisionService { return AjaxResult.success(); } - //抓图 单独测试 未使用 @Override public Camera cameraPictrue(Camera camera) throws Exception { @@ -627,27 +692,14 @@ public class HikVisionServiceImpl implements HikVisionService { return outputPath; } - //二维数组转 CSV - private static String convertMatrixToCsv(float[][] matrix) { - StringBuilder csvBuilder = new StringBuilder(); - for (float[] row : matrix) { - for (int i = 0; i < row.length; i++) { - csvBuilder.append(row[i]); - if (i < row.length - 1) { - csvBuilder.append(","); - } - } - csvBuilder.append("\n"); - } - return csvBuilder.toString(); - } - @Override public AjaxResult capturePicture(InfraPictureInfo infraPictureInfo) { return AjaxResult.success("设置成功"); } +//从ftp获取csv流转换成二维数组 + //框选温度值计算 public InfraredInfo matrixTemperatureShow(Coordinate coordinate, short width, short height, float[][] temperatureMatrix) { InfraredInfo infraredInfo = new InfraredInfo(); @@ -781,8 +833,6 @@ public class HikVisionServiceImpl implements HikVisionService { } -//从ftp获取csv流转换成二维数组 - private float[][] parseCsvFromFtp2(String csvUrl) throws IOException, URISyntaxException { log.info("连接ftp,获取csv文件"); InputStream inputStream = null; @@ -896,7 +946,6 @@ public class HikVisionServiceImpl implements HikVisionService { return inputStream; } - @Override public ResponseEntity irPicAnalyse(final String analyseRequestJson) { log.info("[INFRARED] irPicAnalyse: analyseRequestJson={}", analyseRequestJson); @@ -981,7 +1030,6 @@ public class HikVisionServiceImpl implements HikVisionService { return infraredInfo; } - @Override public InputStream downloadCsv(String filePath) { log.info("解析csv图片地址" + filePath); @@ -1034,43 +1082,6 @@ public class HikVisionServiceImpl implements HikVisionService { return inputStream; } - - public static String truncateFileNameToCsv(String originalPath) { - // 获取文件名部分(最后一个 '/' 之后的内容) - int lastSlashIndex = originalPath.lastIndexOf('/'); - if (lastSlashIndex == -1) { - // 如果没有路径分隔符,直接处理整个字符串 - return processFileNameToCsv(originalPath); - } - - // 分离路径和文件名 - String path = originalPath.substring(0, lastSlashIndex + 1); - String fileName = originalPath.substring(lastSlashIndex + 1); - - // 处理文件名部分并强制改为.csv - String processedFileName = processFileNameToCsv(fileName); - - - return path + processedFileName; - } - - private static String processFileNameToCsv(String fileName) { - // 去掉文件扩展名(如果有) - int lastDotIndex = fileName.lastIndexOf('.'); - if (lastDotIndex != -1) { - fileName = fileName.substring(0, lastDotIndex); - } - - // 去掉最后一个 '_' 及其后面的部分 - int lastUnderscoreIndex = fileName.lastIndexOf('_'); - if (lastUnderscoreIndex != -1) { - fileName = fileName.substring(0, lastUnderscoreIndex); - } - - // 强制添加.csv后缀 - return fileName + ".csv"; - } - // NVR跳转到预置位 public AjaxResult NvrCameraAngleJump(Camera camera) { diff --git a/src/main/java/com/inspect/nvr/service/impl/IvsCameraServiceImpl.java b/src/main/java/com/inspect/nvr/service/impl/IvsCameraServiceImpl.java index c8e9d0e..2ee3bcb 100644 --- a/src/main/java/com/inspect/nvr/service/impl/IvsCameraServiceImpl.java +++ b/src/main/java/com/inspect/nvr/service/impl/IvsCameraServiceImpl.java @@ -55,9 +55,9 @@ public class IvsCameraServiceImpl implements IvsCameraService { @Autowired private CameraController cameraController; @Autowired - private HikCaptureService hikCaptureService; + private HikCameraService hikCameraService; @Autowired - private DahuaCaptureService dhCaptureService; + private DahuaCameraService dahuaCameraService; @Autowired private HikLoginService hikLoginService; @@ -119,7 +119,7 @@ public class IvsCameraServiceImpl implements IvsCameraService { cameraHw.setPassword(splitArray[11]); //获取红外温度 TemperatureData temper = retry(cameraHw, camera, cameraType); - if(retryMode == 1 && StringUtils.isNull(temper)){ + if (retryMode == 1 && StringUtils.isNull(temper)) { ptzControlResult = PtzControlResult.builder() .resultCode("-1") .build(); @@ -397,12 +397,12 @@ public class IvsCameraServiceImpl implements IvsCameraService { nvrInfo.setPassword(camera.getPassword()); if (ObjectUtil.equals(camera.getCameraType(), 1)) { - log.info("大华相机拍照"); - byte[] bytes = dhCaptureService.capture(nvrInfo, camera.getChannel()); + log.info("大华相机抓图"); + byte[] bytes = dahuaCameraService.capture(nvrInfo, camera.getChannel()); return new ByteArrayInputStream(bytes); } else { - log.info("海康相机拍照"); - byte[] bytes = hikCaptureService.capture(nvrInfo, camera.getChannel()); + log.info("海康相机抓图"); + byte[] bytes = hikCameraService.capture(nvrInfo, camera.getChannel()); return new ByteArrayInputStream(bytes); } } @@ -412,7 +412,7 @@ public class IvsCameraServiceImpl implements IvsCameraService { final String rawString = StringHexConverter.fromHex(fileSessionId); log.info("downloadImage hexString: {}, rawString: {}", fileSessionId, rawString); String[] cameraAddressInfos = rawString.split((":")); - log.info("doCapture ip: {}, port: {}, channel: {}, pointName: {},cameraType: {}, username: {}, password: {}", cameraAddressInfos[0], cameraAddressInfos[1], cameraAddressInfos[2], cameraAddressInfos[3], cameraAddressInfos[4], cameraAddressInfos[5], cameraAddressInfos[6]); + log.info("captureDigest ip: {}, port: {}, channel: {}, pointName: {},cameraType: {}, username: {}, password: {}", cameraAddressInfos[0], cameraAddressInfos[1], cameraAddressInfos[2], cameraAddressInfos[3], cameraAddressInfos[4], cameraAddressInfos[5], cameraAddressInfos[6]); Camera camera = new Camera(); camera.setIp(cameraAddressInfos[0]); camera.setPort(Integer.parseInt(cameraAddressInfos[1])); @@ -429,12 +429,12 @@ public class IvsCameraServiceImpl implements IvsCameraService { nvrInfo.setPassword(camera.getPassword()); if (ObjectUtil.equals(camera.getCameraType(), 1)) { - log.info("大华相机拍照"); - byte[] bytes = dhCaptureService.captureDigest(nvrInfo, camera.getChannel()); + log.info("大华相机Digest抓图"); + byte[] bytes = dahuaCameraService.captureDigest(nvrInfo, camera.getChannel()); return bytes; } else { - log.info("海康相机拍照"); - byte[] bytes = hikCaptureService.captureDigest(nvrInfo, camera.getChannel()); + log.info("海康相机Digest抓图"); + byte[] bytes = hikCameraService.captureDigest(nvrInfo, camera.getChannel()); return bytes; } }