package com.ard.work.sdk.hp.service; import com.ard.common.core.constant.CacheConstants; import com.ard.common.core.domain.R; import com.ard.common.core.exception.ServiceException; import com.ard.common.core.exception.device.CameraSDKException; import com.ard.common.core.utils.StringUtils; import com.ard.common.core.utils.file.FileMultipartFile; import com.ard.common.core.utils.file.FileUtils; import com.ard.common.core.utils.file.MimeTypeUtils; import com.ard.common.redis.service.RedisService; import com.ard.system.api.RemoteFileService; import com.ard.system.api.domain.SysFile; import com.ard.work.api.domian.*; import com.ard.work.device.camera.domain.PtzParamDTO; import com.ard.work.event.HepuLoginEvent; import com.ard.work.event.LoginEvent; import com.ard.work.sdk.common.GlobalVariable; import com.ard.work.sdk.common.SdkErrorCodeEnum; import com.ard.work.sdk.fjr.netty.FjrNettyClient; import com.ard.work.sdk.fjr.netty.ThermalCommand; import com.ard.work.sdk.hik.lib.HCNetSDK; import com.ard.work.sdk.hik.service.AbstractHikVisionSDK; import com.ard.work.sdk.hp.cache.CameraPTZCache; import com.ard.work.sdk.hp.cache.CameraSerialCache; import com.ard.work.sdk.model.PtzScope; import com.ard.work.utils.ArdTool; import com.ard.work.utils.FFmpegUtils; import com.ard.work.utils.gis.GisUtil; import com.sun.jna.Pointer; import com.sun.jna.ptr.IntByReference; import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.ApplicationEventPublisher; import org.springframework.stereotype.Service; import org.springframework.util.StopWatch; import org.springframework.web.multipart.MultipartFile; import java.io.File; import java.util.ArrayList; import java.util.List; import java.util.concurrent.CompletableFuture; import static com.ard.work.sdk.dh.lib.ToolKits.getErrorCodePrint; import static com.ard.work.sdk.hik.lib.HCNetSDK.*; /** * @Description: 合浦sdk策略 继承海康抽象类 * @ClassName: HpSDK * @Author: 刘苏义 * @Date: 2026年03月06日13:05:09 **/ @Service("hpSDK") @Slf4j public class HpSDK extends AbstractHikVisionSDK { @Resource private RedisService redisService; @Resource private ApplicationEventPublisher eventPublisher; @Value("${sdk.tempDir}") private String tempDir; @Resource private RemoteFileService remoteFileService; @Resource private FjrNettyClient nettyClient; //登录 @Override public void login(ArdCamera camera) { // --- 步骤 1: 登录主设备--- long masterLoginId = doSingleCameraLogin(camera); if (masterLoginId < 0) { int errCode = hCNetSDK.NET_DVR_GetLastError(); log.warn("设备[{}:{}]登录失败,错误码:{} 原因:{}", camera.getIp(), camera.getPort(), errCode, SdkErrorCodeEnum.getDescByCode(errCode)); handleLoginFail(camera); eventPublisher.publishEvent(new LoginEvent(camera)); return; // 主设备失败,直接终止,无需尝试子设备 } camera.setLoginId(masterLoginId); int serialHandle = initTransparentSerial(camera); if (serialHandle < 0) { int errorCode = hCNetSDK.NET_DVR_GetLastError(); log.warn("【{}:{}】建立透明通道失败,错误码:{} 原因:{}", camera.getIp(), camera.getPort(), errorCode, SdkErrorCodeEnum.getDescByCode(errorCode)); handleLoginFail(camera); eventPublisher.publishEvent(new LoginEvent(camera)); return;// 建立透明串口失败,直接终止,无需尝试子设备 } CameraSerialCache.put(camera.getId(), serialHandle); log.info("【{}:{}】建立透明通道成功,serialHandle:{}", camera.getIp(), camera.getPort(), serialHandle); // 更新缓存 redisService.setCacheMapValue(CacheConstants.CAMERA_ONLINE, camera.getId(), masterLoginId); log.debug("主设备 [{}] 登录成功,ID: {}", camera.getIp(), masterLoginId); // --- 步骤 2: 登录机芯节点 --- List nodes = camera.getHepuNodes(); List> futures = new ArrayList<>(); // 1. 为每个子节点创建并行登录任务 for (ArdCameraHepu node : nodes) { futures.add(CompletableFuture.supplyAsync(() -> { try { int nodeLoginId = -1; switch (node.getNodeType()) { //可见光 case "1": nodeLoginId = hikNodeLogin(node); break; //热红外 case "2": nodeLoginId = fjrNodeLogin(node); break; } if (nodeLoginId > 0) { node.setLoginId((long) nodeLoginId); node.setStatus("1"); redisService.setCacheMapValue(CacheConstants.CAMERA_ONLINE, node.getId(), (long) nodeLoginId); } else { node.setLoginId(-1L); node.setStatus("0"); // 发布合浦节点登录事件 redisService.deleteCacheMapValue(CacheConstants.CAMERA_ONLINE, node.getId()); } // 发布合浦节点登录事件 eventPublisher.publishEvent(new HepuLoginEvent(node)); return nodeLoginId; } catch (Exception e) { log.error("子节点 {} 登录异常", node.getRemark(), e); return -1; } })); } // 2. 等待完成 CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join(); // 3. 统计成功数 (ID > 0 即为成功) long successCount = futures.stream().filter(f -> f.join() > 0).count(); boolean allSuccess = (successCount == nodes.size()); // 4. 只有全部成功 if (allSuccess) { handleLoginSuccess(camera); } else { handleLoginFail(camera); } eventPublisher.publishEvent(new LoginEvent(camera)); } //登录失败处理 private void handleLoginFail(ArdCamera camera) { camera.setChanNum(0); camera.setLoginId(-1L); camera.setState("0"); camera.setChannelList(null); } //登录成功处理 private void handleLoginSuccess(ArdCamera camera) { camera.setState("1"); camera.setStartChan(1); camera.setChanNum(2); // 获取最新通道 List channels = getChannels(camera); if (!channels.isEmpty()) { camera.setChannelList(channels); } log.info("设备登录成功 [{}:{}]", camera.getIp(), camera.getPort()); } // --- 公共核心登录方法 --- /** * 执行通用的 SDK 登录操作 * * @param ip 设备 IP * @param port 端口 * @param username 用户名 * @param password 密码 * @return 登录句柄 (成功 > 0, 失败 <= 0) */ private int doLoginCore(String ip, short port, String username, String password) { // 设备信息输出参数 NET_DVR_DEVICEINFO_V40 deviceInfo = new NET_DVR_DEVICEINFO_V40(); NET_DVR_USER_LOGIN_INFO loginInfo = new NET_DVR_USER_LOGIN_INFO(); // 1. 初始化字节数组 (防止空指针或长度不足) loginInfo.sDeviceAddress = new byte[HCNetSDK.NET_DVR_DEV_ADDRESS_MAX_LEN]; loginInfo.sUserName = new byte[HCNetSDK.NET_DVR_LOGIN_USERNAME_MAX_LEN]; loginInfo.sPassword = new byte[HCNetSDK.NET_DVR_LOGIN_PASSWD_MAX_LEN]; // 2. 填充数据 (注意处理字符串长度不超过数组最大长度的情况,虽然通常不会超) byte[] ipBytes = ip.getBytes(); byte[] userBytes = username.getBytes(); byte[] pwdBytes = password.getBytes(); System.arraycopy(ipBytes, 0, loginInfo.sDeviceAddress, 0, Math.min(ipBytes.length, loginInfo.sDeviceAddress.length)); System.arraycopy(userBytes, 0, loginInfo.sUserName, 0, Math.min(userBytes.length, loginInfo.sUserName.length)); System.arraycopy(pwdBytes, 0, loginInfo.sPassword, 0, Math.min(pwdBytes.length, loginInfo.sPassword.length)); // 3. 设置其他参数 loginInfo.wPort = port; loginInfo.byVerifyMode = 0; // 默认验证模式 loginInfo.byLoginMode = 0; // 默认登录模式 loginInfo.bUseAsynLogin = false; // 同步登录 // 4. 写入内存 (JNA 必需步骤) loginInfo.write(); // 5. 执行登录 int loginId = hCNetSDK.NET_DVR_Login_V40(loginInfo, deviceInfo); // 可选:如果登录失败,可以在这里统一记录错误码日志,或者由调用方处理 if (loginId <= 0) { int errCode = hCNetSDK.NET_DVR_GetLastError(); // 注意:这里不要打太频繁的日志,因为并发登录时失败可能会刷屏,建议由上层捕获后记录 // log.debug("登录失败 IP:{} Port:{} ErrCode:{}", ip, port, errCode); } return loginId; } // --- 主相机登录 (调用核心方法) --- private int doSingleCameraLogin(ArdCamera camera) { return doLoginCore( camera.getIp(), camera.getPort().shortValue(), camera.getUsername(), camera.getPassword() ); } // --- 合浦海康节点机芯登录 (调用核心方法) --- private int hikNodeLogin(ArdCameraHepu node) { return doLoginCore( node.getIp(), node.getPort().shortValue(), node.getUsername(), node.getPassword() ); } // --- 合浦富吉瑞节点机芯登录 (调用核心方法) --- private int fjrNodeLogin(ArdCameraHepu node) { ArdCamera camera = new ArdCamera(node.getIp(), node.getPort(), node.getUsername(), node.getPassword()); camera.setId(node.getId()); boolean success = nettyClient.connect(camera); if (!success) { log.error("设备 {} 登录失败", camera.getIp()); return -1; } return (int) (System.currentTimeMillis() / 1000); } // 注销登录 @Override public void logout(String cameraId) { // 1. 注销主设备 Long loginId = redisService.getCacheMapValue(CacheConstants.CAMERA_ONLINE, cameraId); if (loginId != null) { Integer lSerialHandle = CameraSerialCache.get(cameraId); if (lSerialHandle != null) { CameraSerialCache.remove(cameraId); if (hCNetSDK.NET_DVR_SerialStop(lSerialHandle)) { log.info("【{}】关闭透明串口成功", cameraId); } } redisService.deleteCacheMapValue(CacheConstants.CAMERA_ONLINE, cameraId); hCNetSDK.NET_DVR_Logout(loginId.intValue()); } // 2. 注销合浦节点1 Long node1 = redisService.getCacheMapValue(CacheConstants.CAMERA_ONLINE, cameraId + "_1"); if (node1 != null) { redisService.deleteCacheMapValue(CacheConstants.CAMERA_ONLINE, cameraId + "_1"); hCNetSDK.NET_DVR_Logout(node1.intValue()); } // 3. 注销合浦节点2 Long node2 = redisService.getCacheMapValue(CacheConstants.CAMERA_ONLINE, cameraId + "_2"); if (node2 != null) { redisService.deleteCacheMapValue(CacheConstants.CAMERA_ONLINE, cameraId + "_2"); // fjrSDK.logout(node2.intValue()); } } // 初始化透明串口 private int initTransparentSerial(ArdCamera ardCamera) { NET_DVR_SERIALSTART_V40 serialStartV40 = new NET_DVR_SERIALSTART_V40(); serialStartV40.dwSize = serialStartV40.size(); // 结构体大小 serialStartV40.dwSerialType = 2; // 串口类型:1-232 2-485 serialStartV40.bySerialNum = 0; // 串口号:0表示第一个串口 serialStartV40.write(); // 写入内存 Pointer lpInBuffer = serialStartV40.getPointer(); int dwInBufferSize = serialStartV40.size(); HpSerialDataCallBack serialDataCallback = new HpSerialDataCallBack(ardCamera); Pointer pUser = null; return hCNetSDK.NET_DVR_SerialStart_V40(ardCamera.getLoginId().intValue(), lpInBuffer, dwInBufferSize, serialDataCallback, pUser); } // 透明串口发送指令 private Boolean SerialSend(int m_lSerialHandle, byte[] data) { // 1. 定义查询指令 boolean b = hCNetSDK.NET_DVR_SerialSend(m_lSerialHandle, 1, data, data.length); if (!b) { int errorCode = hCNetSDK.NET_DVR_GetLastError(); log.error("透传指令发送失败,错误码:{}", errorCode); return false; } return true; } //透明串口云台控制 public Boolean serialControl(CameraCmd cmd) { String cameraId = cmd.getCameraId(); Integer serialHandle = CameraSerialCache.get(cameraId); if (serialHandle == null) { log.error("【{}】未找到对应的串口通道", cameraId); return false; } byte CMD_HEAD = (byte) 0xFF; byte ADDRESS = 0x01; byte COMMAND1 = 0x00; byte COMMAND2 = 0x00; int speed = cmd.getSpeed() * 10; if (speed <= 0) speed = 0x10; if (speed > 0x3F) speed = 0x3F; byte dataH = (byte) speed; byte dataL = (byte) speed; String action = ""; switch (cmd.getCode()) { case 1: COMMAND2 = 0x0C; action = "左上"; break; case 2: COMMAND2 = 0x08; action = "上"; break; case 3: COMMAND2 = 0x0A; action = "右上"; break; case 4: COMMAND2 = 0x04; action = "左"; break; case 6: COMMAND2 = 0x02; action = "右"; break; case 7: COMMAND2 = 0x14; action = "左下"; break; case 8: COMMAND2 = 0x10; action = "下"; break; case 9: COMMAND2 = 0x12; action = "右下"; break; case 10: COMMAND2 = 0x20; dataH = 0; dataL = 0; action = "变倍+"; break; case 11: COMMAND2 = 0x40; dataH = 0; dataL = 0; action = "变倍-"; break; case 12: COMMAND1 = 0x01; dataH = 0; dataL = 0; action = "近焦"; break; case 13: COMMAND1 = (byte) 0x80; dataH = 0; dataL = 0; action = "远焦"; break; case 14: COMMAND1 = 0x02; action = "光圈开"; break; case 15: COMMAND1 = 0x04; action = "光圈关"; break; default: log.warn("【{}】未知PTZ控制指令: {}", cameraId, cmd.getCode()); return false; } if (!cmd.isEnable()) { COMMAND1 = 0x00; COMMAND2 = 0x00; dataH = 0; dataL = 0; action = action + " 停止"; } int sum = (ADDRESS & 0xFF) + (COMMAND1 & 0xFF) + (COMMAND2 & 0xFF) + (dataH & 0xFF) + (dataL & 0xFF); byte checkSum = (byte) (sum & 0xFF); byte[] bytes = new byte[]{ CMD_HEAD, ADDRESS, COMMAND1, COMMAND2, dataH, dataL, checkSum }; // 打印完整指令 log.info("【{}】PTZ控制 -> 动作:{} 速度:{} 指令:{}", cameraId, action, speed, ArdTool.bytesToHex(bytes)); Boolean result = SerialSend(serialHandle, bytes); if (!result) { int errorCode = hCNetSDK.NET_DVR_GetLastError(); log.warn("【{}】PTZ串口发送失败 错误码:{} 原因:{}", cameraId, errorCode, SdkErrorCodeEnum.getDescByCode(errorCode)); } return result; } /** * 带速度的云台控制操作 * * @param cmd 相机命令 * @return true 表示控制命令发送成功 * @throws CameraSDKException SDK调用失败时抛出 */ @Override public Boolean pTZControl(CameraCmd cmd) { Integer chanNo = cmd.getChanNo(); // 2. 核心分流逻辑 // 假设 Code 10-13 是变倍和调焦相关指令 // 其他指令(1-9)是云台转动 boolean isRotationCommand = (cmd.getCode() >= 1 && cmd.getCode() <= 9); if (isRotationCommand) { //云台旋转指令走透明串口协议 return serialControl(cmd); } ArdCamera ardCamera = redisService.getCacheMapValue(CacheConstants.CAMERA_LIST, cmd.getCameraId()); ArdCameraHepu targetNode = ardCamera.getHepuNodes().stream() .filter(node -> node.getSortOrder().equals(chanNo)) .findFirst() .orElse(null); // 如果没找到,返回 null if (targetNode == null) return false; cmd.setCameraId(targetNode.getId()); switch (chanNo) { case 1: return hikControl(cmd); case 2: return fjrControl(cmd); } return false; } public Boolean hikControl(CameraCmd cmd) { String cameraId = cmd.getCameraId(); Integer chanNo = cmd.getChanNo(); Long userId = redisService.getCacheMapValue(CacheConstants.CAMERA_ONLINE, cameraId); if (userId == null) { throw new ServiceException("设备离线"); } int dwStop = cmd.isEnable() ? 0 : 1; int dwPTZCommand; switch (cmd.getCode()) { case 1: dwPTZCommand = HCNetSDK.UP_LEFT; break; case 2: dwPTZCommand = HCNetSDK.TILT_UP; break; case 3: dwPTZCommand = HCNetSDK.UP_RIGHT; break; case 4: dwPTZCommand = HCNetSDK.PAN_LEFT; break; case 5: dwPTZCommand = HCNetSDK.RUN_SEQ; break; case 6: dwPTZCommand = HCNetSDK.PAN_RIGHT; break; case 7: dwPTZCommand = HCNetSDK.DOWN_LEFT; break; case 8: dwPTZCommand = HCNetSDK.TILT_DOWN; break; case 9: dwPTZCommand = HCNetSDK.DOWN_RIGHT; break; case 10: dwPTZCommand = HCNetSDK.ZOOM_IN; break; case 11: dwPTZCommand = HCNetSDK.ZOOM_OUT; break; case 12: dwPTZCommand = HCNetSDK.FOCUS_NEAR; break; case 13: dwPTZCommand = HCNetSDK.FOCUS_FAR; break; case 14: dwPTZCommand = HCNetSDK.IRIS_OPEN; break; case 15: dwPTZCommand = HCNetSDK.IRIS_CLOSE; break; case 16: dwPTZCommand = HCNetSDK.WIPER_PWRON; break; default: return false; // 或者记录日志 } boolean success = hCNetSDK.NET_DVR_PTZControlWithSpeed_Other( userId.intValue(), cmd.getChanNo(), dwPTZCommand, dwStop, cmd.getSpeed() ); if (!success) { int errorCode = hCNetSDK.NET_DVR_GetLastError(); log.warn("【{}】PTZ控制失败,错误码:{}, 原因:{}", cameraId, errorCode, SdkErrorCodeEnum.getDescByCode(errorCode)); } return success; } public Boolean fjrControl(CameraCmd cmd) { String cameraId = cmd.getCameraId(); Integer speed = cmd.getSpeed(); // 1. 先判断是否是“停止”动作 // 如果 isEnable 为 false,说明按钮松开了,必须发送停止指令 if (!cmd.isEnable()) { ThermalCommand stopCmd = ThermalCommand.createFocusStopCmd(cameraId); nettyClient.send(cameraId, stopCmd); if (cmd.getCode() == 10 || cmd.getCode() == 11) { // 2. 延迟 300ms 发送自动调焦 ThermalCommand autoFocusCmd = ThermalCommand.createAutoFocusCmd(cameraId); nettyClient.sendDelay(cameraId, autoFocusCmd, 1000); } return true; } // 1. 创建一个统一速度指令 ThermalCommand uniformSpeedCmd = ThermalCommand.createUniformSpeedCmd(cameraId, speed); nettyClient.send(cameraId, uniformSpeedCmd); // 2. 如果是“开始”动作 (isEnable == true),再判断方向 switch (cmd.getCode()) { case 10: // 变倍+ ThermalCommand zoomPlus = ThermalCommand.createZoomCmd(cameraId, true); nettyClient.send(cameraId, zoomPlus); break; case 11: // 变倍- ThermalCommand zoomMinus = ThermalCommand.createZoomCmd(cameraId, false); nettyClient.send(cameraId, zoomMinus); break; case 12: // 调焦- // 这里假设你的 createFocusCmd 接受 boolean,true代表+,false代表- ThermalCommand cmdMinus = ThermalCommand.createFocusCmd(cameraId, false); nettyClient.send(cameraId, cmdMinus); break; case 13: // 调焦+ ThermalCommand cmdPlus = ThermalCommand.createFocusCmd(cameraId, true); nettyClient.send(cameraId, cmdPlus); break; default: System.err.println("未知的指令码: " + cmd.getCode()); return false; } return true; } // 切换透雾 @Override public Boolean controlDefog(CameraCmd cmd) { String cameraId = cmd.getCameraId(); Integer chanNo = cmd.getChanNo(); ArdCamera ardCamera = redisService.getCacheMapValue(CacheConstants.CAMERA_LIST, cameraId); ArdCameraHepu targetNode = ardCamera.getHepuNodes().stream() .filter(node -> node.getSortOrder().equals(chanNo)) .findFirst() .orElse(null); // 如果没找到,返回 null if (targetNode == null) return false; cmd.setCameraId(targetNode.getId()); switch (chanNo) { case 1: return hikControlDefog(cmd); case 2: throw new ServiceException("设备不支持"); } return false; } private Boolean hikControlDefog(CameraCmd cmd) { String cameraId = cmd.getCameraId(); Integer chanNo = cmd.getChanNo(); Long userId = redisService.getCacheMapValue(CacheConstants.CAMERA_ONLINE, cameraId); if (userId == null) { throw new ServiceException("设备离线"); } boolean enable = cmd.isEnable(); try { // 获取前端参数 NET_DVR_CAMERAPARAMCFG_EX cameraParam = new NET_DVR_CAMERAPARAMCFG_EX(); Pointer point = cameraParam.getPointer(); IntByReference ibrBytesReturned = new IntByReference(0); boolean gotParam = hCNetSDK.NET_DVR_GetDVRConfig(userId.intValue(), NET_DVR_GET_CCDPARAMCFG_EX, chanNo, point, cameraParam.size(), ibrBytesReturned); if (!gotParam) { log.warn("【{}】获取前端参数失败: {}", cameraId, getErrorCodePrint()); return false; } cameraParam.read(); log.debug("当前透雾模式: {}", cameraParam.struDefogCfg.byMode); // 设置透雾配置 NET_DVR_DEFOGCFG defogCfg = new NET_DVR_DEFOGCFG(); if (enable) { defogCfg.byMode = 2; // 常开模式 defogCfg.byLevel = 100; // 0-100 } else { defogCfg.byMode = 0; // 不启用 } cameraParam.struDefogCfg = defogCfg; cameraParam.write(); boolean success = hCNetSDK.NET_DVR_SetDVRConfig(userId.intValue(), NET_DVR_SET_CCDPARAMCFG_EX, chanNo, point, cameraParam.size()); if (!success) { log.warn("【{}】切换透雾失败: {}", cameraId, getErrorCodePrint()); return false; } log.debug("【{}】切换透雾成功", cameraId); return true; } catch (Exception ex) { log.error("切换透雾异常:{}", ex.getMessage()); return false; } } // 获取透雾状态 @Override public Boolean isEnableDefog(CameraCmd cmd) { String cameraId = cmd.getCameraId(); Integer chanNo = cmd.getChanNo(); ArdCamera ardCamera = redisService.getCacheMapValue(CacheConstants.CAMERA_LIST, cameraId); ArdCameraHepu targetNode = ardCamera.getHepuNodes().stream() .filter(node -> node.getSortOrder().equals(chanNo)) .findFirst() .orElse(null); // 如果没找到,返回 null if (targetNode == null) return false; cmd.setCameraId(targetNode.getId()); switch (chanNo) { case 1: return hikIsEnableDefog(cmd); case 2: throw new ServiceException("设备不支持"); } return false; } private Boolean hikIsEnableDefog(CameraCmd cmd) { String cameraId = cmd.getCameraId(); Integer chanNo = cmd.getChanNo(); Long userId = redisService.getCacheMapValue(CacheConstants.CAMERA_ONLINE, cameraId); if (userId == null) { throw new ServiceException("设备离线"); } try { NET_DVR_CAMERAPARAMCFG_EX strutCameraParam = new NET_DVR_CAMERAPARAMCFG_EX(); Pointer point = strutCameraParam.getPointer(); IntByReference ibrBytesReturned = new IntByReference(0); boolean success = hCNetSDK.NET_DVR_GetDVRConfig( userId.intValue(), NET_DVR_GET_CCDPARAMCFG_EX, chanNo, point, strutCameraParam.size(), ibrBytesReturned ); if (!success) { int errorCode = hCNetSDK.NET_DVR_GetLastError(); String errorDesc = SdkErrorCodeEnum.getDescByCode(errorCode); log.warn("【{}】获取前端参数失败,错误码:{}, 原因:{}", cameraId, errorCode, errorDesc); return false; // SDK 层只返回 true/false } strutCameraParam.read(); log.debug("【{}】是否开启透雾:{}", cameraId, strutCameraParam.struDefogCfg.byMode); return strutCameraParam.struDefogCfg.byMode != 0; } catch (Exception e) { log.error("【{}】查询透雾异常: {}", cameraId, e.getMessage()); return false; } } // 切换红外 @Override public Boolean controlInfrared(CameraCmd cmd) { String cameraId = cmd.getCameraId(); Integer chanNo = cmd.getChanNo(); ArdCamera ardCamera = redisService.getCacheMapValue(CacheConstants.CAMERA_LIST, cameraId); ArdCameraHepu targetNode = ardCamera.getHepuNodes().stream() .filter(node -> node.getSortOrder().equals(chanNo)) .findFirst() .orElse(null); // 如果没找到,返回 null if (targetNode == null) return false; cmd.setCameraId(targetNode.getId()); switch (chanNo) { case 1: return hikControlInfrared(cmd); case 2: throw new ServiceException("设备不支持"); } return false; } private Boolean hikControlInfrared(CameraCmd cmd) { String cameraId = cmd.getCameraId(); boolean enable = cmd.isEnable(); Integer chanNo = cmd.getChanNo(); Long userId = redisService.getCacheMapValue(CacheConstants.CAMERA_ONLINE, cameraId); if (userId == null) { throw new ServiceException("设备离线"); } try { NET_DVR_CAMERAPARAMCFG_EX cameraParamCfg = new NET_DVR_CAMERAPARAMCFG_EX(); Pointer point = cameraParamCfg.getPointer(); IntByReference ibrBytesReturned = new IntByReference(0); // 获取当前参数 boolean b_GetCameraParam = hCNetSDK.NET_DVR_GetDVRConfig(userId.intValue(), NET_DVR_GET_CCDPARAMCFG, chanNo, point, cameraParamCfg.size(), ibrBytesReturned); if (!b_GetCameraParam) { log.warn("【{}】获取前端参数失败: {}", cameraId, getErrorCodePrint()); return false; } cameraParamCfg.read(); log.debug("当前红外状态:{}", cameraParamCfg.struDayNight.byDayNightFilterType == 1 ? "夜晚" : "白天"); // 设置红外 NET_DVR_DAYNIGHT dayNight = new NET_DVR_DAYNIGHT(); dayNight.byDayNightFilterType = (byte) (enable ? 1 : 0); // 夜晚/白天 dayNight.bySwitchScheduleEnabled = 1; dayNight.byDayNightFilterTime = 60; cameraParamCfg.struDayNight = dayNight; cameraParamCfg.write(); boolean success = hCNetSDK.NET_DVR_SetDVRConfig(userId.intValue(), NET_DVR_SET_CCDPARAMCFG, chanNo, point, cameraParamCfg.size()); if (!success) { log.warn("【{}】切换红外失败: {}", cameraId, getErrorCodePrint()); return false; } log.info("【{}】切换红外成功", cameraId); return true; } catch (Exception e) { log.error("【{}】切换红外异常: {}", cameraId, e.getMessage()); return false; } } // 查询是否开启红外 @Override public Boolean isEnableInfrared(CameraCmd cmd) { String cameraId = cmd.getCameraId(); Integer chanNo = cmd.getChanNo(); ArdCamera ardCamera = redisService.getCacheMapValue(CacheConstants.CAMERA_LIST, cameraId); ArdCameraHepu targetNode = ardCamera.getHepuNodes().stream() .filter(node -> node.getSortOrder().equals(chanNo)) .findFirst() .orElse(null); // 如果没找到,返回 null if (targetNode == null) return false; cmd.setCameraId(targetNode.getId()); switch (chanNo) { case 1: return hikIsEnableInfrared(cmd); case 2: throw new ServiceException("设备不支持"); } return false; } private Boolean hikIsEnableInfrared(CameraCmd cmd) { String cameraId = cmd.getCameraId(); Integer chanNo = cmd.getChanNo(); Long userId = redisService.getCacheMapValue(CacheConstants.CAMERA_ONLINE, cameraId); if (userId == null) { throw new ServiceException("设备离线"); } try { NET_DVR_CAMERAPARAMCFG_EX cameraParamCfg = new NET_DVR_CAMERAPARAMCFG_EX(); Pointer point = cameraParamCfg.getPointer(); IntByReference ibrBytesReturned = new IntByReference(0); boolean success = hCNetSDK.NET_DVR_GetDVRConfig(userId.intValue(), NET_DVR_GET_CCDPARAMCFG, chanNo, point, cameraParamCfg.size(), ibrBytesReturned); if (!success) { int errorCode = hCNetSDK.NET_DVR_GetLastError(); String errorDesc = SdkErrorCodeEnum.getDescByCode(errorCode); log.warn("【{}】获取前端参数失败,错误码:{},原因:{}", cameraId, errorCode, errorDesc); return false; } cameraParamCfg.read(); // 1表示夜晚/红外开启,0表示白天/红外关闭 return cameraParamCfg.struDayNight.byDayNightFilterType == 1; } catch (Exception ex) { log.error("【{}】查询红外开关异常", cameraId, ex); return false; } } //切换聚焦模式 @Override public Boolean controlFocusMode(CameraCmd cmd) { String cameraId = cmd.getCameraId(); Integer chanNo = cmd.getChanNo(); ArdCamera ardCamera = redisService.getCacheMapValue(CacheConstants.CAMERA_LIST, cameraId); ArdCameraHepu targetNode = ardCamera.getHepuNodes().stream() .filter(node -> node.getSortOrder().equals(chanNo)) .findFirst() .orElse(null); // 如果没找到,返回 null if (targetNode == null) return false; cmd.setCameraId(targetNode.getId()); switch (chanNo) { case 1: return hikControlFocusMode(cmd); case 2: return fjrControlFocusMode(cmd); } return false; } private Boolean hikControlFocusMode(CameraCmd cmd) { String cameraId = cmd.getCameraId(); Integer chanNo = cmd.getChanNo(); boolean enable = cmd.isEnable(); Long userId = redisService.getCacheMapValue(CacheConstants.CAMERA_ONLINE, cameraId); if (userId == null) { throw new ServiceException("设备离线"); } NET_DVR_FOCUSMODE_CFG focusModeCfg = new NET_DVR_FOCUSMODE_CFG(); Pointer point = focusModeCfg.getPointer(); IntByReference ibrBytesReturned = new IntByReference(0); // 获取当前聚焦模式 boolean got = hCNetSDK.NET_DVR_GetDVRConfig(userId.intValue(), NET_DVR_GET_FOCUSMODECFG, chanNo, point, focusModeCfg.size(), ibrBytesReturned); if (!got) { int errorCode = hCNetSDK.NET_DVR_GetLastError(); String errorDesc = SdkErrorCodeEnum.getDescByCode(errorCode); log.warn("获取前端参数失败,错误码:{},原因:{}", errorCode, errorDesc); return false; } focusModeCfg.read(); log.debug("当前聚焦模式:{}", focusModeCfg.byFocusMode); // 设置聚焦模式 if (enable) { focusModeCfg.byFocusMode = 1; // 手动聚焦 focusModeCfg.byAutoFocusMode = 0; } else { focusModeCfg.byFocusMode = 2; // 自动聚焦 focusModeCfg.byAutoFocusMode = 1; } focusModeCfg.byFocusDefinitionDisplay = 1; focusModeCfg.byFocusSpeedLevel = 3; focusModeCfg.write(); boolean setOk = hCNetSDK.NET_DVR_SetDVRConfig(userId.intValue(), NET_DVR_SET_FOCUSMODECFG, chanNo, point, focusModeCfg.size()); if (!setOk) { int errorCode = hCNetSDK.NET_DVR_GetLastError(); String errorDesc = SdkErrorCodeEnum.getDescByCode(errorCode); log.warn("设置聚焦模式失败,错误码:{},原因:{}", errorCode, errorDesc); return false; } log.debug("设置聚焦模式成功"); return true; } private Boolean fjrControlFocusMode(CameraCmd cmd) { String cameraId = cmd.getCameraId(); Integer chanNo = cmd.getChanNo(); boolean enable = cmd.isEnable(); if (enable) { ThermalCommand autoFocusCmd = ThermalCommand.createAutoFocusCmd(cameraId); nettyClient.sendDelay(cameraId, autoFocusCmd, 300); } return true; } //获取聚焦模式 @Override public Boolean getFocusMode(CameraCmd cmd) { String cameraId = cmd.getCameraId(); Integer chanNo = cmd.getChanNo(); ArdCamera ardCamera = redisService.getCacheMapValue(CacheConstants.CAMERA_LIST, cameraId); ArdCameraHepu targetNode = ardCamera.getHepuNodes().stream() .filter(node -> node.getSortOrder().equals(chanNo)) .findFirst() .orElse(null); // 如果没找到,返回 null if (targetNode == null) return false; cmd.setCameraId(targetNode.getId()); switch (chanNo) { case 1: return hikGetFocusMode(cmd); case 2: throw new ServiceException("设备不支持"); } return false; } private Boolean hikGetFocusMode(CameraCmd cmd) { String cameraId = cmd.getCameraId(); Integer chanNo = cmd.getChanNo(); Long userId = redisService.getCacheMapValue(CacheConstants.CAMERA_ONLINE, cameraId); if (userId == null) { throw new ServiceException("设备离线"); } NET_DVR_FOCUSMODE_CFG strutFocusMode = new NET_DVR_FOCUSMODE_CFG(); Pointer point = strutFocusMode.getPointer(); IntByReference ibrBytesReturned = new IntByReference(0); boolean b_GetCameraParam = hCNetSDK.NET_DVR_GetDVRConfig(userId.intValue(), NET_DVR_GET_FOCUSMODECFG, chanNo, point, strutFocusMode.size(), ibrBytesReturned); if (!b_GetCameraParam) { int errorCode = hCNetSDK.NET_DVR_GetLastError(); String errorDesc = SdkErrorCodeEnum.getDescByCode(errorCode); String errorMsg = String.format("获取聚焦模式失败,错误码:%s,原因:%s", errorCode, errorDesc); log.warn(errorMsg); throw new CameraSDKException(errorCode, cameraId, errorMsg); } strutFocusMode.read(); // 判断手动/自动聚焦 // 0-自动,1-手动,2-半自动 return strutFocusMode.byFocusMode == 1; // true=手动 } //3D定位 @Override public Boolean set3DPosition(CameraCmd cmd) { String cameraId = cmd.getCameraId(); Integer chanNo = cmd.getChanNo(); ArdCamera ardCamera = redisService.getCacheMapValue(CacheConstants.CAMERA_LIST, cameraId); ArdCameraHepu targetNode = ardCamera.getHepuNodes().stream() .filter(node -> node.getSortOrder().equals(chanNo)) .findFirst() .orElse(null); // 如果没找到,返回 null if (targetNode == null) return false; cmd.setCameraId(targetNode.getId()); switch (chanNo) { case 1: return hikSet3DPosition(cmd); case 2: throw new ServiceException("设备不支持"); } return false; } private Boolean hikSet3DPosition(CameraCmd cmd) { String cameraId = cmd.getCameraId(); Integer chanNo = cmd.getChanNo(); PointFrame pointFrame = cmd.getPointFrame(); Long userId = redisService.getCacheMapValue(CacheConstants.CAMERA_ONLINE, cameraId); if (userId == null) { throw new ServiceException("设备离线"); } NET_DVR_POINT_FRAME netDvrPointFrame = new NET_DVR_POINT_FRAME(); netDvrPointFrame.xTop = pointFrame.getXTop(); netDvrPointFrame.yTop = pointFrame.getYTop(); netDvrPointFrame.xBottom = pointFrame.getXBottom(); netDvrPointFrame.yBottom = pointFrame.getYBottom(); netDvrPointFrame.write(); boolean bool = hCNetSDK.NET_DVR_PTZSelZoomIn_EX(userId.intValue(), chanNo, netDvrPointFrame); if (!bool) { int errorCode = hCNetSDK.NET_DVR_GetLastError(); String errorDesc = SdkErrorCodeEnum.getDescByCode(errorCode); String errorMsg = String.format("3D定位失败,错误码:%s,原因:%s", errorCode, errorDesc); log.warn(errorMsg); return false; } return true; } //获取ptz信息 @Override public PtzDto getPtz(CameraCmd cmd) { String cameraId = cmd.getCameraId(); PtzDto ptzDto = CameraPTZCache.get(cameraId); if (ptzDto == null) { ptzDto = new PtzDto(0f, 0f, 0f); CameraPTZCache.put(cameraId, ptzDto); } return ptzDto; } //获取高精度ptz信息 @Override public PtzDto getPtzHigh(CameraCmd cmd) { return getPtz(cmd); } //设置ptz信息 @Override public Boolean setPtz(CameraCmd cmd) { String cameraId = cmd.getCameraId(); PtzDto ptzDto = cmd.getPtzDto(); int m_lSerialHandle = CameraSerialCache.get(cameraId); float p = ptzDto.getP(); int dataInt = Math.round(p * 100); byte[] dataBytes = intTo2BytesBigEndian(dataInt); byte dataH = dataBytes[0]; // DataH(Byte5) byte dataL = dataBytes[1]; // DataL(Byte6) byte CMD_HEAD = (byte) 0xff; // 指令头(Byte1) byte ADDRESS = 0x01; // 设备地址(Byte2) byte COMMAND1 = 0x00; // 命令1(Byte3) byte COMMAND2 = 0x4B; // 命令2(Byte4) // CheckSum = (Address + Command1 + Command2 + DataH + DataL) 的低8位 int sum = (ADDRESS & 0xFF) + (COMMAND1 & 0xFF) + (COMMAND2 & 0xFF) + (dataH & 0xFF) + (dataL & 0xFF); byte checkSum = (byte) (sum & 0xFF); // 取低8位作为校验和 // 组装指令数组 byte[] cmdPBytes = new byte[]{CMD_HEAD, ADDRESS, COMMAND1, COMMAND2, dataH, dataL, checkSum}; // 总长度7字节 log.info("[PTZ报文:] cameraId={},p={}, hex={}", cameraId, ptzDto.getP(),bytesToHex(cmdPBytes)); Boolean b = SerialSend(m_lSerialHandle, cmdPBytes); try { Thread.sleep(100); } catch (InterruptedException e) { throw new RuntimeException(e); } if (!b) { int errorCode = hCNetSDK.NET_DVR_GetLastError(); String errorDesc = SdkErrorCodeEnum.getDescByCode(errorCode); String errorMsg = String.format("设置PTZ(P)失败,错误码:%s,原因:%s", errorCode, errorDesc); log.warn(errorMsg); return false; } dataInt = Math.round(ptzDto.getT() * 100); dataInt = 36000 - dataInt; dataBytes = intTo2BytesBigEndian(dataInt); dataH = dataBytes[0]; dataL = dataBytes[1]; COMMAND2 = 0x4D; // 5. Step3:计算校验和(单行版,规则同水平) sum = (ADDRESS & 0xFF) + (COMMAND1 & 0xFF) + (COMMAND2 & 0xFF) + (dataH & 0xFF) + (dataL & 0xFF); checkSum = (byte) (sum & 0xFF); // 组装指令数组 cmdPBytes = new byte[]{CMD_HEAD, ADDRESS, COMMAND1, COMMAND2, dataH, dataL, checkSum}; // 总长度7字节 log.info("[PTZ报文:] cameraId={},t={}, hex={}", cameraId, ptzDto.getT(),bytesToHex(cmdPBytes)); b = SerialSend(m_lSerialHandle, cmdPBytes); try { Thread.sleep(100); } catch (InterruptedException e) { throw new RuntimeException(e); } if (!b) { int errorCode = hCNetSDK.NET_DVR_GetLastError(); String errorDesc = SdkErrorCodeEnum.getDescByCode(errorCode); String errorMsg = String.format("设置PTZ(T)失败,错误码:%s,原因:%s", errorCode, errorDesc); log.warn(errorMsg); return false; } int z = ptzDto.getZ().intValue(); dataInt = convertFrontZoomToDeviceFocal(z); dataBytes = intTo2BytesBigEndian(dataInt); dataH = dataBytes[0]; dataL = dataBytes[1]; COMMAND2 = 0x4F; // 5. Step3:计算校验和(单行版,规则同水平) sum = (ADDRESS & 0xFF) + (COMMAND1 & 0xFF) + (COMMAND2 & 0xFF) + (dataH & 0xFF) + (dataL & 0xFF); checkSum = (byte) (sum & 0xFF); // 组装指令数组 cmdPBytes = new byte[]{CMD_HEAD, ADDRESS, COMMAND1, COMMAND2, dataH, dataL, checkSum}; // 总长度7字节 log.info("[PTZ报文:] cameraId={},z={}, hex={}", cameraId, ptzDto.getZ(),bytesToHex(cmdPBytes)); b = SerialSend(m_lSerialHandle, cmdPBytes); if (!b) { int errorCode = hCNetSDK.NET_DVR_GetLastError(); String errorDesc = SdkErrorCodeEnum.getDescByCode(errorCode); String errorMsg = String.format("设置PTZ(Z)失败,错误码:%s,原因:%s", errorCode, errorDesc); log.warn(errorMsg); return false; } return true; } /** * 前端1-60的倍率值 转换为 设备可识别的焦距控制值 * * @param frontZoom 前端传递的1~60的整数 * @return 设备焦距控制值(放大100倍的整数,如3.3mm→330,37mm→3700) */ public int convertFrontZoomToDeviceFocal(int frontZoom) { // 1. 定义固定参数(设备物理参数+映射关系) final float MIN_FOCAL = 3.3f; // 设备最小焦距(mm) final float MAX_FOCAL = 37.0f; // 设备最大焦距(mm) final int FRONT_MIN = 1; // 前端最小值 final int FRONT_MAX = 60; // 前端最大值 // 2. 校验前端输入范围(避免非法值) if (frontZoom < FRONT_MIN || frontZoom > FRONT_MAX) { throw new IllegalArgumentException("前端倍率值需在1~60之间,当前值:" + frontZoom); // 若不想抛异常,也可自动修正:frontZoom = Math.max(FRONT_MIN, Math.min(FRONT_MAX, frontZoom)); } // 3. 核心:线性映射公式(前端值 → 设备焦距mm) // 公式:目标值 = 最小值 + (最大值-最小值) × (当前前端值-前端最小值)/(前端最大值-前端最小值) float deviceFocal = MIN_FOCAL + (MAX_FOCAL - MIN_FOCAL) * (frontZoom - FRONT_MIN) / (FRONT_MAX - FRONT_MIN); // 4. 焦距值放大100倍并取整(设备底层常用整数控制,避免浮点精度问题) return Math.round(deviceFocal * 100); } /** * 整数转2字节数组(大端模式:高字节在前,低字节在后) * 示例:9000 → 0x23 0x28(因为9000的十六进制是0x2328) * * @param value 待转换整数(0~36000) * @return 2字节数组([0]=高字节,[1]=低字节) */ private byte[] intTo2BytesBigEndian(int value) { byte[] bytes = new byte[2]; bytes[0] = (byte) (value >> 8 & 0xFF); // 高字节(DataH) bytes[1] = (byte) (value & 0xFF); // 低字节(DataL) return bytes; } //设置高精度ptz信息 @Override public Boolean setPtzHighT(CameraCmd cmd) { return setPtz(cmd); } //非透传 旧设备使用 新固件会导致2通道不自动聚焦 @Override public Boolean setPtzHigh(CameraCmd cmd) { return setPtz(cmd); } //设置零方位角 @Override public Boolean setZeroPtz(CameraCmd cmd) { String cameraId = cmd.getCameraId(); int m_lSerialHandle = CameraSerialCache.get(cameraId); byte[] cmdPBytes = new byte[]{(byte) 0xff, 0x01, 0x00, 0x6F, 0x00, 0x00, 0x70}; boolean bool = SerialSend(m_lSerialHandle, cmdPBytes); if (!bool) { int errorCode = hCNetSDK.NET_DVR_GetLastError(); String errorDesc = SdkErrorCodeEnum.getDescByCode(errorCode); String errorMsg = String.format("设置零方位角失败,错误码:%s,原因:%s", errorCode, errorDesc); log.warn(errorMsg); return false; } return true; } //获取视场角 @Override public PtzParamDTO getGisInfo(CameraCmd cmd) { PtzParamDTO ptzParamDTO = new PtzParamDTO(); String cameraId = cmd.getCameraId(); PtzDto ptzDto = CameraPTZCache.get(cameraId); if (ptzDto == null) { ptzParamDTO.setP(0f); ptzParamDTO.setT(0f); ptzParamDTO.setZ(0f); } ptzParamDTO.setFHorFieldAngle(1.8f); ptzParamDTO.setFVerFieldAngle(1f); return ptzParamDTO; } //获取球机PTZ参数取值范围 @Override public PtzScope getPtzScope(CameraCmd cmd) { PtzScope ptzScope = new PtzScope(); ptzScope.setPMax(360f); ptzScope.setPMin(0f); ptzScope.setTMax(45f); ptzScope.setTMin(315f); ptzScope.setZMax(60f); ptzScope.setZMin(1f); return ptzScope; } //引导目标位置 @Override public Boolean guideTargetPosition(CameraCmd cmd) { String cameraId = cmd.getCameraId(); Float baseYaw = cmd.getBaseYaw(); ArdCamera ardCamera = redisService.getCacheMapValue(CacheConstants.CAMERA_LIST, cameraId); if (StringUtils.isNull(ardCamera)) { log.warn("未找到相机信息,cameraId:{}", cameraId); return false; } Point cameraPosition = new Point(ardCamera.getLongitude(), ardCamera.getLatitude(), ardCamera.getAltitude()); cameraPosition.validate(); // 校验目标位置 Point targetPositions = cmd.getTargetPosition(); targetPositions.validate(); // 校验目标位置 PtzDto targetPTZ = GisUtil.getCameraPTZ(cameraPosition, targetPositions, 20, 150); //若为车载光电,需要根据修改P值 if (ardCamera.getType().equals("4")) { targetPTZ.setP((targetPTZ.getP() - baseYaw + 360) % 360); } // pt校验范围 cmd.setPtzDto(targetPTZ); validatePtzRange(cmd); return setPtz(cmd); } //录像开始 @Override public Boolean recordStart(CameraCmd cmd) { String cameraId = cmd.getCameraId(); Integer chanNo = cmd.getChanNo(); ArdCamera ardCamera = redisService.getCacheMapValue(CacheConstants.CAMERA_LIST, cameraId); ArdCameraHepu targetNode = ardCamera.getHepuNodes().stream() .filter(node -> node.getSortOrder().equals(chanNo)) .findFirst() .orElse(null); // 如果没找到,返回 null if (targetNode == null) return false; cmd.setCameraId(targetNode.getId()); switch (chanNo) { case 1: return hikRecordStart(cmd); case 2: throw new ServiceException("设备不支持"); } return false; } private Boolean hikRecordStart(CameraCmd cmd) { try { String cameraId = cmd.getCameraId(); Integer chanNo = cmd.getChanNo(); String name = cameraId + "_" + chanNo; // 本地临时录像地址 String path = FileUtils.createFile(tempDir + "/record/" + name + ".mp4"); Long userId = redisService.getCacheMapValue(CacheConstants.CAMERA_ONLINE, cameraId); if (userId == null) { throw new ServiceException("设备离线"); } //强制I帧结构体对象 NET_DVR_I_FRAME netDvrIFrame = new NET_DVR_I_FRAME(); //新建结构体对象 netDvrIFrame.read(); netDvrIFrame.dwChannel = chanNo;//因为上文代码中设置了通道号,按照上文中的设置 netDvrIFrame.byStreamType = 0; netDvrIFrame.dwSize = netDvrIFrame.size(); netDvrIFrame.write(); if (!hCNetSDK.NET_DVR_RemoteControl(userId.intValue(), 3402, netDvrIFrame.getPointer(), netDvrIFrame.dwSize)) { int code = hCNetSDK.NET_DVR_GetLastError(); log.error("设置强制I帧错误:{}", SdkErrorCodeEnum.getDescByCode(code) + "(" + code + ")"); } //预览参数 NET_DVR_PREVIEWINFO previewInfo = new NET_DVR_PREVIEWINFO(); previewInfo.read(); previewInfo.lChannel = chanNo; previewInfo.dwStreamType = 0;//码流类型:0-主码流,1-子码流,2-三码流,3-虚拟码流,以此类推 previewInfo.dwLinkMode = 0;//连接方式:0-TCP方式,1-UDP方式,2-多播方式,3-RTP方式,4-RTP/RTSP,5-RTP/HTTP,6-HRUDP(可靠传输),7 // -RTSP/HTTPS,8-NPQ previewInfo.hPlayWnd = null;//播放窗口的句柄,为NULL表示不解码显示。 previewInfo.bBlocked = 0;//0- 非阻塞取流,1-阻塞取流 previewInfo.byNPQMode = 0;//NPQ模式:0-直连模式,1-过流媒体模式 previewInfo.write(); if (GlobalVariable.previewMap.containsKey(name)) { Integer lRealHandle = (Integer) GlobalVariable.previewMap.get(name); hCNetSDK.NET_DVR_StopRealPlay(lRealHandle); GlobalVariable.previewMap.remove(name); log.debug("停止当前录像"); } int lRealHandle = hCNetSDK.NET_DVR_RealPlay_V40(userId.intValue(), previewInfo, null, null); if (lRealHandle == -1) { log.error("取流失败:{}", hCNetSDK.NET_DVR_GetLastError()); return false; } log.debug("取流成功"); GlobalVariable.threadMap.put(cameraId, Thread.currentThread().getName()); GlobalVariable.previewMap.put(name, lRealHandle); if (!hCNetSDK.NET_DVR_SaveRealData_V30((int) GlobalVariable.previewMap.get(name), 2, path)) { log.error("保存视频文件到临时文件夹失败 错误码为:{}", hCNetSDK.NET_DVR_GetLastError()); return false; } log.debug("录像开始"); return true; } catch (Exception ex) { log.error("开始录像异常:{}", ex.getMessage()); return false; } } //录像停止 @Override public String recordStop(CameraCmd cmd) { String cameraId = cmd.getCameraId(); Integer chanNo = cmd.getChanNo(); ArdCamera ardCamera = redisService.getCacheMapValue(CacheConstants.CAMERA_LIST, cameraId); ArdCameraHepu targetNode = ardCamera.getHepuNodes().stream() .filter(node -> node.getSortOrder().equals(chanNo)) .findFirst() .orElse(null); // 如果没找到,返回 null if (targetNode == null) return ""; cmd.setCameraId(targetNode.getId()); switch (chanNo) { case 1: return hikRecordStop(cmd); case 2: throw new ServiceException("设备不支持"); } return ""; } private String hikRecordStop(CameraCmd cmd) { String url = ""; File videoFile = null; File transcodeFile = null; try { String cameraId = cmd.getCameraId(); Integer chanNo = cmd.getChanNo(); String name = cameraId + "_" + chanNo; // 本地临时录像地址 String path = FileUtils.createFile(tempDir + "/record/" + name + ".mp4"); Long userId = redisService.getCacheMapValue(CacheConstants.CAMERA_ONLINE, cameraId); if (userId == null) { throw new ServiceException("设备离线"); } // 停止录像 if (GlobalVariable.previewMap.containsKey(name)) { Integer lRealHandle = (Integer) GlobalVariable.previewMap.get(name); hCNetSDK.NET_DVR_StopRealPlay(lRealHandle); GlobalVariable.previewMap.remove(name); } log.debug("录像停止"); // 检查原录像文件是否存在 videoFile = new File(path); if (!videoFile.exists() || videoFile.length() == 0) { log.error("录像文件不存在或为空: {}", path); return ""; } // ffmpeg转码 StopWatch watchStop = new StopWatch(); watchStop.start(); String newpath = FileUtils.createFile(tempDir + "/record/" + name + "_c.mp4"); transcodeFile = new File(newpath); FFmpegUtils.transcodeToMP4(path, newpath); watchStop.stop(); log.warn("转码耗时:{}", watchStop.getTotalTimeMillis()); // 检查转码后的文件是否存在 if (!transcodeFile.exists() || transcodeFile.length() == 0) { log.error("转码文件不存在或为空: {}", newpath); return ""; } // 存入minio - 使用 FileMultipartFile String bucketName = "record"; String objectName = cameraId + "_" + chanNo + ".mp4"; String type = cmd.getType() == null ? "record" : cmd.getType(); MultipartFile multipartFile = new FileMultipartFile(transcodeFile, objectName, MimeTypeUtils.VIDEO_MP4); R sysFileR = remoteFileService.upload(multipartFile, bucketName, type); if (sysFileR.getCode() == R.SUCCESS) { url = sysFileR.getData().getUrl(); log.info("上传文件成功:{}", url); } return url; } catch (Exception ex) { log.error("录像异常:{}", ex.getMessage()); return ""; } finally { // 清理临时文件 try { if (videoFile != null && videoFile.exists()) { videoFile.delete(); log.debug("删除原录像文件: {}", videoFile.getPath()); } if (transcodeFile != null && transcodeFile.exists()) { transcodeFile.delete(); log.debug("删除转码文件: {}", transcodeFile.getPath()); } } catch (Exception e) { log.warn("删除临时文件失败", e); } } } //获取通道 @Override public List getChannels(ArdCamera camera) { List channelList = new ArrayList<>(); // 获取所有子节点 List nodes = camera.getHepuNodes(); if (nodes == null || nodes.isEmpty()) { log.warn("设备 {} 无子节点,无法获取通道", camera.getName()); return channelList; } log.debug("开始聚合子设备通道信息,共 {} 个子节点", nodes.size()); // 1. 遍历每一个子节点 for (ArdCameraHepu node : nodes) { ArdChannel channel = new ArdChannel(); if ("1".equals(node.getNodeType())) { Long nodeLoginId = redisService.getCacheMapValue(CacheConstants.CAMERA_ONLINE, node.getId()); if (nodeLoginId == null) { log.warn("子节点 {} 未登录或 LoginId 无效,跳过其通道获取", node.getRemark()); continue; } int chanNo = 1; // 子设备内部的物理通道号 (通常从 1 开始) NET_DVR_PICCFG_V40 strPicCfg = new NET_DVR_PICCFG_V40(); int structSize = strPicCfg.size(); IntByReference pInt = new IntByReference(0); // 复用结构体 strPicCfg.dwSize = structSize; strPicCfg.write(); // 调用 SDK 获取配置 (使用子节点的 loginId 和 物理通道号) boolean success = hCNetSDK.NET_DVR_GetDVRConfig( nodeLoginId.intValue(), HCNetSDK.NET_DVR_GET_PICCFG_V40, chanNo, strPicCfg.getPointer(), structSize, pInt ); if (!success) { int errorCode = hCNetSDK.NET_DVR_GetLastError(); if (errorCode != 0) { continue; } } // 读取结果 strPicCfg.read(); // 解析通道名称 String channelName; try { channelName = new String(strPicCfg.sChanName, "GBK").trim(); } catch (Exception e) { channelName = "通道" + chanNo; } // 如果名字为空,给个默认名 if (channelName.isEmpty() || channelName.contains("\u0000")) { channelName = "通道" + chanNo; } // 构建父设备视角的通道对象 channel.setDeviceId(camera.getId()); // 归属父设备 ID channel.setName(channelName); // 通道名称 channel.setChanNo(chanNo); // 【关键】使用全局递增的通道号 // 获取视频编码 CameraCmd cmd = new CameraCmd(node.getId(), chanNo); cmd.setLoginId(nodeLoginId); // 假设 getVideoCompressionCfg 能根据 cmd 里的信息去查 VideoCompressionCfg videoCfg = getVideoCompressionCfg(cmd); if (videoCfg != null) { channel.setVideoCode(videoCfg.getVideoEncType()); } String rtspUrl = buildRtspUrlForType1(node, chanNo); channel.setLiveAddress(rtspUrl); } else if ("2".equals(node.getNodeType())) { channel.setDeviceId(camera.getId()); // 归属父设备 ID channel.setName("热红外"); // 写死通道名称 channel.setChanNo(2); // 写死通道号2 channel.setVideoCode("h264"); // 写死通道编码格式 // 【新增】构建 rtspUrl - 类型2 String rtspUrl = buildRtspUrlForType2(node); channel.setLiveAddress(rtspUrl); } channelList.add(channel); } log.debug("聚合完成,父设备 {} 共获取到 {} 个逻辑通道", camera.getName(), channelList.size()); return channelList; } // 构建类型1的 RTSP URL private String buildRtspUrlForType1(ArdCameraHepu node, int chanNo) { return String.format("rtsp://%s:%s@%s:%d/h264/ch%d/main/av_stream", node.getUsername(), node.getPassword(), node.getIp(), node.getRtspPort(), chanNo ); } // 构建类型2的 RTSP URL private String buildRtspUrlForType2(ArdCameraHepu node) { return String.format("rtsp://%s:%d/live/video", node.getIp(), node.getRtspPort() ); } private String bytesToHex(byte[] bytes) { StringBuilder sb = new StringBuilder(); for (byte b : bytes) { sb.append(String.format("%02X ", b)); } return sb.toString().trim(); } }