package com.ard.work.sdk.zlxd.service; import com.ard.common.core.constant.CacheConstants; import com.ard.common.core.constant.SecurityConstants; 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.MimeTypeUtils; import com.ard.common.redis.service.RedisService; import com.ard.system.api.RemoteFileService; import com.ard.system.api.domain.SysFile; import com.ard.vtdu.api.RemoteArdVtduService; import com.ard.vtdu.api.domian.ArdVtdu; import com.ard.work.api.domian.*; import com.ard.work.device.camera.domain.PtzParamDTO; import com.ard.work.device.camera.service.IArdCameraPtzScopeService; import com.ard.work.event.LoginEvent; import com.ard.work.sdk.dh.common.Base64; import com.ard.work.sdk.model.DeviceCFG; import com.ard.work.sdk.model.PtzScope; import com.ard.work.sdk.service.CameraSDK; import com.ard.work.sdk.zlxd.cache.RecordSessionCache; import com.ard.work.sdk.zlxd.netty.message.parameters.CameraParameters; import com.ard.work.sdk.zlxd.netty.message.request.*; import com.ard.work.sdk.zlxd.netty.message.response.*; import com.ard.work.sdk.zlxd.netty.tcp.NettyTcpClient; import com.ard.work.sdk.zlxd.utils.PTZ3DLocationConverter; import com.ard.work.sdk.zlxd.utils.XmlUtils; import com.ard.work.utils.file.MultipartFileUtil; import com.ard.work.utils.gis.GisUtil; import com.galaxy.sdk.RecordSdk; import com.galaxy.sdk.SDKConfig; import jakarta.annotation.PostConstruct; import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.ApplicationEventPublisher; import org.springframework.http.MediaType; import org.springframework.stereotype.Service; import org.springframework.web.multipart.MultipartFile; import java.io.File; import java.math.BigDecimal; import java.math.RoundingMode; import java.nio.charset.Charset; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicInteger; /** * 中林信达sdk策略 * * @author 刘苏义 * @since 2023-11-07 */ @Service("zlxdSDK") @Slf4j public class ZlxdSDK implements CameraSDK { @Value("${sdk.tempDir}") private String tempDir; private RecordSdk recordSdk; @Resource private RemoteFileService remoteFileService; @Resource private RemoteArdVtduService remoteArdVtduService; @Resource private NettyTcpClient nettyTcpClient; @Resource private ApplicationEventPublisher eventPublisher; @Resource private RedisService redisService; @Resource private IArdCameraPtzScopeService ardCameraPtzScopeService; @PostConstruct private void initSdk() { recordSdk = RecordSdk.getInstance(); SDKConfig config = new SDKConfig(); String saveDir = tempDir + File.separator + "record"; config.setStoragePath(saveDir); config.setEnableAutoRetry(true); config.setMaxRetryCount(3); config.setMaxStorageSize(5L * 1024 * 1024 * 1024); //最大存储5G config.setPerFileMaxSize(1L * 1024 * 1024 * 1024); int ret = recordSdk.init(config); if (ret < 0) { System.out.println("SDK初始化失败"); } System.out.println("SDK初始化成功(预热已完成)"); System.out.println("SDK实例: " + recordSdk + ", 类加载器: " + recordSdk.getClass().getClassLoader()); } private final AtomicInteger SEQ = new AtomicInteger(1); // 安全的序列号生成 private synchronized int getSafeSeq() { int seq = SEQ.getAndIncrement(); // 检测并重置回绕 if (seq >= Integer.MAX_VALUE - 1000) { SEQ.set(1); log.warn("序列号即将达到上限,已重置"); } return seq; } /** * TCP+XML设备登录 * * @param camera 设备信息 * @return 登录ID,失败返回 -1 * @throws Exception 网络或解析异常由上层处理 */ @Override public void login(ArdCamera camera) { String ip = camera.getIp(); Integer port = camera.getPort(); // 建立TCP连接 boolean connected = nettyTcpClient.connectCamera(camera); if (!connected) { log.error("TCP连接建立失败: {}:{}", ip, port); camera.setState("0"); camera.setChanNum(0); camera.setLoginId(-1L); eventPublisher.publishEvent(new LoginEvent(camera)); return; } log.debug("TCP连接建立成功,准备发送登录请求"); // 1️⃣ 生成序列号 int preLoginSeq = getSafeSeq(); // 2️⃣ 预登录获取盐 String preLoginResp = nettyTcpClient.sendXmlAndWait(camera.getId(), new PreLoginRequest(preLoginSeq)); PreLoginResponse preRes = XmlUtils.fromXml(preLoginResp, PreLoginResponse.class); if (preRes.getResult().getCode() != 0) { camera.setLoginId(-1L); camera.setState("0"); Integer errorCode = preRes.getResult().getCode(); String errorDesc = ErrorCodeEnum.getDescByCode(errorCode); log.warn("设备[{}:{}]预登录失败,错误码:{},原因:{}", ip, port, errorCode, errorDesc); return; } // 3️⃣ 登录 int loginSeq = getSafeSeq(); String loginResp = nettyTcpClient.sendXmlAndWait(camera.getId(), new LoginRequest(camera, preRes.getParameters().getSessionID(), preRes.getParameters().getChallenge(), loginSeq)); LoginResponse res = XmlUtils.fromXml(loginResp, LoginResponse.class); if (res.getResult().getCode() != 0) { // 登录失败处理 handleLoginFail(camera, res); } else { // 登录成功处理 handleLoginSuccess(camera, res); } // 登录成功 log.debug("TCP+XML设备登录成功 [{}:{}]", ip, port); // 发布登录事件 eventPublisher.publishEvent(new LoginEvent(camera)); } /** * 登录失败处理 */ private void handleLoginFail(ArdCamera camera, LoginResponse res) { camera.setChanNum(0); camera.setLoginId(-1L); camera.setState("0"); camera.setChannelList(null); Integer errorCode = res.getResult().getCode(); String errorDesc = ErrorCodeEnum.getDescByCode(errorCode); log.warn("设备[{}:{}]登录失败,错误码:{},原因:{}", camera.getIp(), camera.getPort(), errorCode, errorDesc); } /** * 登录成功处理 */ private void handleLoginSuccess(ArdCamera camera, LoginResponse res) { camera.setState("1"); camera.setChanNum(2); Long loginId = System.currentTimeMillis(); // 可以用时间戳或其他唯一ID camera.setLoginId(loginId); String sessionId = res.getHeader().getSessionId(); SessionCache.put(camera.getId(), sessionId); // 获取最新通道 List channels = getChannels(camera); if (!channels.isEmpty()) { camera.setChannelList(channels); } log.info("设备登录成功 [{}:{}]", camera.getIp(), camera.getPort()); } @Override public void logout(String cameraId) { nettyTcpClient.disconnectCamera(cameraId); } // 建议按 cameraId 维度保存失败次数 private final ConcurrentHashMap offlineFailCountMap = new ConcurrentHashMap<>(); private static final int MAX_FAIL_COUNT = 2; @Override public Boolean isOnLine(CameraCmd cmd) { String cameraId = cmd.getCameraId(); AtomicInteger failCount = offlineFailCountMap.computeIfAbsent(cameraId, k -> new AtomicInteger(0)); try { int seq = getSafeSeq(); String sessionId = SessionCache.get(cameraId); GetDeviceStateRequest request = new GetDeviceStateRequest("", seq, sessionId); String response = nettyTcpClient.sendXmlAndWait(cameraId, request); GetDeviceStateResponse res = XmlUtils.fromXml(response, GetDeviceStateResponse.class); if (res != null && res.getResult() != null && res.getResult().getCode() == 0) { // ✅ 成功:清零失败计数 failCount.set(0); return true; } } catch (Exception e) { log.error("设备在线状态查询异常:{}", e.getMessage()); int count = failCount.incrementAndGet(); log.warn("设备 [{}] 在线检测异常,第 {} 次", cameraId, count); // ⭐ 关键:3 次以内,仍认为在线 if (count <= MAX_FAIL_COUNT) { return true; } } return false; } @Override public Boolean pTZControl(CameraCmd cameraCmd) { String cameraId = cameraCmd.getCameraId(); Integer chanNo = cameraCmd.getChanNo(); boolean enable = cameraCmd.isEnable(); Integer speed = cameraCmd.getSpeed(); Integer code = cameraCmd.getCode(); try { int seq = getSafeSeq(); String sessionId = SessionCache.get(cameraId); if (sessionId == null) { log.warn("【{}】未找到有效会话,PTZ控制失败", cameraId); return false; } String cmd = ""; String param = ""; // 1️⃣ 指令映射 switch (code) { case 1: cmd = "TUPL"; break; case 2: cmd = "TU"; break; case 3: cmd = "TUPR"; break; case 4: cmd = "PL"; break; case 5: break; case 6: cmd = "PR"; break; case 7: cmd = "TDPL"; break; case 8: cmd = "TD"; break; case 9: cmd = "TDPR"; break; case 10: cmd = "ZIN"; break; case 11: cmd = "ZOUT"; break; case 12: cmd = "FN"; break; case 13: cmd = "FR"; break; case 14: cmd = "IO"; break; case 15: cmd = "IC"; break; case 16: cmd = "WP_ON"; break; default: cmd = "STOP"; break; } // 2️⃣ 关闭动作处理 if (!enable) { param = cmd; cmd = "STOP"; } // 3️⃣ 速度映射 int bizSpeed = ((speed - 1) * 31) + 1; log.debug("【{}】发送PTZ指令: {} 参数:{} 速度:{}", cameraId, cmd, param, bizSpeed); ControlPTZRequest request = new ControlPTZRequest(chanNo, cmd, param, bizSpeed, seq, sessionId); // 4️⃣ 发送请求并解析响应 String response = nettyTcpClient.sendXmlAndWait(cameraId, request); ControlPTZResponse res = XmlUtils.fromXml(response, ControlPTZResponse.class); if (res != null && res.getResult() != null && res.getResult().getCode() == 0) { log.debug("【{}】PTZ指令发送成功: {} 参数:{} 速度:{}", cameraId, cmd, param, bizSpeed); return true; } else { Integer errorCode = (res != null && res.getResult() != null) ? res.getResult().getCode() : null; String errorDesc = ErrorCodeEnum.getDescByCode(errorCode); log.warn("【{}】PTZ控制失败,错误码:{} 原因:{}", cameraId, errorCode, errorDesc); return false; } } catch (Exception e) { log.error("【{}】PTZ控制异常:{}", cameraId, e.getMessage()); return false; } } /** * 设置聚焦值 * * @param cmd 相机命令 * @return true 成功,false 失败 */ @Override public Boolean setFocusPos(CameraCmd cmd) { String cameraId = cmd.getCameraId(); int chanNo = cmd.getChanNo(); Integer focus = cmd.getDwFocusPos(); try { int seq = getSafeSeq(); String sessionId = SessionCache.get(cameraId); // 1️⃣ 获取当前云台信息 GetPtzInfoRequest getPtzInfoRequest = new GetPtzInfoRequest(chanNo, seq, sessionId); String getPtzInfoXml = nettyTcpClient.sendXmlAndWait(cameraId, getPtzInfoRequest); GetPtzInfoResponse getPtzInfoRes = XmlUtils.fromXml(getPtzInfoXml, GetPtzInfoResponse.class); if (getPtzInfoRes == null || getPtzInfoRes.getResult() == null || getPtzInfoRes.getResult().getCode() != 0) { log.warn("【{}】获取当前云台信息失败", cameraId); return false; } Float pan = getPtzInfoRes.getParameters().getPanAngle(); Float tilt = getPtzInfoRes.getParameters().getTiltAngle(); Float zoom = getPtzInfoRes.getParameters().getZoom(); // 2️⃣ 设置绝对位置,包括聚焦值 SetAbsLocationZoomRequest request = new SetAbsLocationZoomRequest(chanNo, pan, tilt, zoom, focus, seq, sessionId); String response = nettyTcpClient.sendXmlAndWait(cameraId, request); GetPtzInfoResponse res = XmlUtils.fromXml(response, GetPtzInfoResponse.class); if (res != null && res.getResult() != null && res.getResult().getCode() == 0) { return true; } else { Integer errorCode = res != null && res.getResult() != null ? res.getResult().getCode() : null; String errorDesc = ErrorCodeEnum.getDescByCode(errorCode); log.warn("【{}】设置聚焦值失败,错误码:{},原因:{}", cameraId, errorCode, errorDesc); return false; } } catch (Exception e) { log.error("【{}】设置聚焦值异常: {}", cameraId, e.getMessage(), e); return false; } } @Override public Integer getFocusPos(CameraCmd cmd) { try { String cameraId = cmd.getCameraId(); int chanNo = cmd.getChanNo(); int seq = getSafeSeq(); String sessionId = SessionCache.get(cameraId); GetPtzInfoRequest request = new GetPtzInfoRequest(chanNo, seq, sessionId); String response = nettyTcpClient.sendXmlAndWait(cameraId, request); GetPtzInfoResponse res = XmlUtils.fromXml(response, GetPtzInfoResponse.class); if (res != null && res.getResult() != null && res.getResult().getCode() == 0) { Float focus = res.getParameters().getFocus(); return focus != null ? focus.intValue() : null; } } catch (Exception e) { log.error("获取聚焦值异常: {}", e.getMessage()); } return null; } @Override public Boolean setPreset(CameraCmd cmd) { throw new CameraSDKException( CameraSDKException.ErrorCode.CMD_NOT_SUPPORT.getCode(), cmd.getCameraId(), "设置预置点功能暂不支持" ); } @Override public Boolean gotoPreset(CameraCmd cmd) { throw new CameraSDKException( CameraSDKException.ErrorCode.CMD_NOT_SUPPORT.getCode(), cmd.getCameraId(), "调用预置位功能暂不支持" ); } @Override public VideoCompressionCfg getVideoCompressionCfg(CameraCmd cmd) { String cameraId = cmd.getCameraId(); Integer chanNo = cmd.getChanNo(); int seq = getSafeSeq(); String sessionId = SessionCache.get(cameraId); GetPuImageEncodeParaInfoRequest request = new GetPuImageEncodeParaInfoRequest(chanNo, seq, sessionId); String response = nettyTcpClient.sendXmlAndWait(cameraId, request); GetPuImageEncodeParaInfoResponse res = XmlUtils.fromXml(response, GetPuImageEncodeParaInfoResponse.class); if (res.getParameters() != null && !res.getParameters().getImageEncodeParas().isEmpty()) { String videoBitrate = res.getParameters().getImageEncodeParas().get(0).getBitRate().toString(); String resolution = res.getParameters().getImageEncodeParas().get(0).getImageSize(); String videoEncType = res.getParameters().getImageEncodeParas().get(0).getEncodeMode(); String nFrameRate = res.getParameters().getImageEncodeParas().get(0).getFrameRate().toString(); VideoCompressionCfg videoCompressionCfg = new VideoCompressionCfg(); videoCompressionCfg.setVideoBitrate(videoBitrate);//比特率 videoCompressionCfg.setResolution(resolution);//分辨率 videoCompressionCfg.setVideoEncType(videoEncType);//编码 videoCompressionCfg.setFrameRate(nFrameRate);//帧率 return videoCompressionCfg; } return null; } @Override public Boolean controlDefog(CameraCmd cmd) { String cameraId = cmd.getCameraId(); Integer chanNo = cmd.getChanNo(); int seq = getSafeSeq(); String sessionId = SessionCache.get(cameraId); // 光学除雾0: 关闭; 1: 黑白; 2: 彩色; 3: 自适应; 默认值:0. Integer enable = cmd.isEnable() ? 1 : 0; // 获取前端设备图像参数 GetGeneralCameraParaRequest getRequest = new GetGeneralCameraParaRequest(chanNo, seq, sessionId); String response = nettyTcpClient.sendXmlAndWait(cameraId, getRequest); GetGeneralCameraParaResponse getRes = XmlUtils.fromXml(response, GetGeneralCameraParaResponse.class); if (getRes == null || getRes.getResult() == null || getRes.getResult().getCode() != 0) { int errorCode = getRes != null && getRes.getResult() != null ? getRes.getResult().getCode() : -1; String errorDesc = ErrorCodeEnum.getDescByCode(errorCode); log.warn("获取前端设备图像参数失败,错误码:{},原因:{}", errorCode, errorDesc); return false; } // 修改聚焦模式 CameraParameters parameters = getRes.getParameters(); parameters.setOpticalDefog(enable); // 发送设置请求 seq = getSafeSeq(); SetGeneralCameraParaRequest setRequest = new SetGeneralCameraParaRequest(parameters, seq, sessionId); response = nettyTcpClient.sendXmlAndWait(cameraId, setRequest); SetGeneralCameraParaResponse setRes = XmlUtils.fromXml(response, SetGeneralCameraParaResponse.class); if (setRes == null || setRes.getResult() == null || setRes.getResult().getCode() != 0) { int errorCode = setRes != null && setRes.getResult() != null ? setRes.getResult().getCode() : -1; String errorDesc = ErrorCodeEnum.getDescByCode(errorCode); log.warn("设置透雾失败,错误码:{},原因:{}", errorCode, errorDesc); return false; } log.debug("设置透雾成功"); return true; } @Override public Boolean isEnableDefog(CameraCmd cmd) { String cameraId = cmd.getCameraId(); Integer chanNo = cmd.getChanNo(); int seq = getSafeSeq(); String sessionId = SessionCache.get(cameraId); // 获取前端设备参数 GetGeneralCameraParaRequest request = new GetGeneralCameraParaRequest(chanNo, seq, sessionId); String response = nettyTcpClient.sendXmlAndWait(cameraId, request); GetGeneralCameraParaResponse res = XmlUtils.fromXml(response, GetGeneralCameraParaResponse.class); if (res == null || res.getResult() == null || res.getResult().getCode() != 0) { int errorCode = res != null && res.getResult() != null ? res.getResult().getCode() : -1; String errorDesc = ErrorCodeEnum.getDescByCode(errorCode); String errorMsg = String.format("获取聚焦模式失败,错误码:%s,原因:%s", errorCode, errorDesc); log.warn(errorMsg); return null; // 或 false,根据你的业务逻辑 } // 0-自动;1-单次;2-手动 int mode = res.getParameters().getOpticalDefog(); //光学除雾0: 关闭; 1: 黑白; 2: 彩色; 3: 自适应; 默认值:0. return mode != 0; } @Override public Boolean controlInfrared(CameraCmd cmd) { String cameraId = cmd.getCameraId(); Integer chanNo = cmd.getChanNo(); int seq = getSafeSeq(); String sessionId = SessionCache.get(cameraId); // 1: 自动; 2: 黑白; 3: 彩色; 4: 报警触发; 5: 定时; Integer enable = cmd.isEnable() ? 2 : 3; // 获取前端设备图像参数 GetGeneralCameraParaRequest getRequest = new GetGeneralCameraParaRequest(chanNo, seq, sessionId); String response = nettyTcpClient.sendXmlAndWait(cameraId, getRequest); GetGeneralCameraParaResponse getRes = XmlUtils.fromXml(response, GetGeneralCameraParaResponse.class); if (getRes == null || getRes.getResult() == null || getRes.getResult().getCode() != 0) { int errorCode = getRes != null && getRes.getResult() != null ? getRes.getResult().getCode() : -1; String errorDesc = ErrorCodeEnum.getDescByCode(errorCode); log.warn("获取前端设备图像参数失败,错误码:{},原因:{}", errorCode, errorDesc); return false; } // 修改日夜模式 CameraParameters parameters = getRes.getParameters(); parameters.setDayNightMode(enable); // 发送设置请求 seq = getSafeSeq(); SetGeneralCameraParaRequest setRequest = new SetGeneralCameraParaRequest(parameters, seq, sessionId); response = nettyTcpClient.sendXmlAndWait(cameraId, setRequest); SetGeneralCameraParaResponse setRes = XmlUtils.fromXml(response, SetGeneralCameraParaResponse.class); if (setRes == null || setRes.getResult() == null || setRes.getResult().getCode() != 0) { int errorCode = setRes != null && setRes.getResult() != null ? setRes.getResult().getCode() : -1; String errorDesc = ErrorCodeEnum.getDescByCode(errorCode); log.warn("设置透雾失败,错误码:{},原因:{}", errorCode, errorDesc); return false; } log.debug("设置透雾成功"); return true; } @Override public Boolean isEnableInfrared(CameraCmd cmd) { String cameraId = cmd.getCameraId(); Integer chanNo = cmd.getChanNo(); int seq = getSafeSeq(); String sessionId = SessionCache.get(cameraId); // 获取前端设备参数 GetGeneralCameraParaRequest request = new GetGeneralCameraParaRequest(chanNo, seq, sessionId); String response = nettyTcpClient.sendXmlAndWait(cameraId, request); GetGeneralCameraParaResponse res = XmlUtils.fromXml(response, GetGeneralCameraParaResponse.class); if (res == null || res.getResult() == null || res.getResult().getCode() != 0) { int errorCode = res != null && res.getResult() != null ? res.getResult().getCode() : -1; String errorDesc = ErrorCodeEnum.getDescByCode(errorCode); String errorMsg = String.format("获取聚焦模式失败,错误码:%s,原因:%s", errorCode, errorDesc); log.warn(errorMsg); return null; // 或 false,根据你的业务逻辑 } // 日夜切换, 2: 黑白; 3: 彩色; int mode = res.getParameters().getDayNightMode(); //日夜切换, 1: 自动; 2: 黑白; 3: 彩色; 4: 报警触发; 5: 定时; return mode != 3; } @Override public Boolean controlFocusMode(CameraCmd cmd) { String cameraId = cmd.getCameraId(); Integer chanNo = cmd.getChanNo(); int seq = getSafeSeq(); String sessionId = SessionCache.get(cameraId); // 聚焦模式:0-自动;1-单次;2-手动 Integer enable = cmd.isEnable() ? 2 : 0; // 获取前端设备图像参数 GetGeneralCameraParaRequest getRequest = new GetGeneralCameraParaRequest(chanNo, seq, sessionId); String response = nettyTcpClient.sendXmlAndWait(cameraId, getRequest); GetGeneralCameraParaResponse getRes = XmlUtils.fromXml(response, GetGeneralCameraParaResponse.class); if (getRes == null || getRes.getResult() == null || getRes.getResult().getCode() != 0) { int errorCode = getRes != null && getRes.getResult() != null ? getRes.getResult().getCode() : -1; String errorDesc = ErrorCodeEnum.getDescByCode(errorCode); log.warn("获取前端设备图像参数失败,错误码:{},原因:{}", errorCode, errorDesc); return false; } // 修改聚焦模式 CameraParameters parameters = getRes.getParameters(); parameters.setFocusMode(enable); // 发送设置请求 seq = getSafeSeq(); SetGeneralCameraParaRequest setRequest = new SetGeneralCameraParaRequest(parameters, seq, sessionId); response = nettyTcpClient.sendXmlAndWait(cameraId, setRequest); SetGeneralCameraParaResponse setRes = XmlUtils.fromXml(response, SetGeneralCameraParaResponse.class); if (setRes == null || setRes.getResult() == null || setRes.getResult().getCode() != 0) { int errorCode = setRes != null && setRes.getResult() != null ? setRes.getResult().getCode() : -1; String errorDesc = ErrorCodeEnum.getDescByCode(errorCode); log.warn("设置聚焦模式失败,错误码:{},原因:{}", errorCode, errorDesc); return false; } log.debug("设置聚焦模式成功"); return true; } @Override public Boolean getFocusMode(CameraCmd cmd) { String cameraId = cmd.getCameraId(); Integer chanNo = cmd.getChanNo(); int seq = getSafeSeq(); String sessionId = SessionCache.get(cameraId); // 获取前端设备参数 GetGeneralCameraParaRequest request = new GetGeneralCameraParaRequest(chanNo, seq, sessionId); String response = nettyTcpClient.sendXmlAndWait(cameraId, request); GetGeneralCameraParaResponse res = XmlUtils.fromXml(response, GetGeneralCameraParaResponse.class); if (res == null || res.getResult() == null || res.getResult().getCode() != 0) { int errorCode = res != null && res.getResult() != null ? res.getResult().getCode() : -1; String errorDesc = ErrorCodeEnum.getDescByCode(errorCode); String errorMsg = String.format("获取聚焦模式失败,错误码:%s,原因:%s", errorCode, errorDesc); log.warn(errorMsg); return null; // 或 false,根据你的业务逻辑 } // 0-自动;1-单次;2-手动 int mode = res.getParameters().getFocusMode(); // 这里用 Boolean 返回:true=手动,false=自动 return mode == 2; } @Override public Boolean controlPTHeater(CameraCmd cmd) { throw new CameraSDKException( CameraSDKException.ErrorCode.CMD_NOT_SUPPORT.getCode(), cmd.getCameraId(), "功能暂不支持" ); } @Override public Boolean controlCameraDeicing(CameraCmd cmd) { throw new CameraSDKException( CameraSDKException.ErrorCode.CMD_NOT_SUPPORT.getCode(), cmd.getCameraId(), "功能暂不支持" ); } @Override public Boolean getPTZLockInfo(CameraCmd cmd) { String cameraId = cmd.getCameraId(); Integer chanNo = cmd.getChanNo(); int seq = getSafeSeq(); String sessionId = SessionCache.get(cameraId); // 获取前端设备参数 GetPuLockRequest request = new GetPuLockRequest(seq, sessionId); String response = nettyTcpClient.sendXmlAndWait(cameraId, request); GetPuLockResponse res = XmlUtils.fromXml(response, GetPuLockResponse.class); if (res == null || res.getResult() == null || res.getResult().getCode() != 0) { int errorCode = res != null && res.getResult() != null ? res.getResult().getCode() : -1; String errorDesc = ErrorCodeEnum.getDescByCode(errorCode); String errorMsg = String.format("获取聚焦模式失败,错误码:%s,原因:%s", errorCode, errorDesc); log.warn(errorMsg); return null; // 或 false,根据你的业务逻辑 } //0:未锁定;1:锁定 int mode = res.getParameters().getPuLock(); // 这里用 Boolean 返回:true=锁定,false=未锁定 return mode == 1; } @Override public Boolean setPTZLockInfo(CameraCmd cmd) { String cameraId = cmd.getCameraId(); Integer chanNo = cmd.getChanNo(); int seq = getSafeSeq(); String sessionId = SessionCache.get(cameraId); // 0: 解锁; 1: 锁定; Integer enable = cmd.isEnable() ? 1 : 0; // 设置云台锁定 SetPuLockRequest request = new SetPuLockRequest(enable, seq, sessionId); String response = nettyTcpClient.sendXmlAndWait(cameraId, request); SetPuLockResponse getRes = XmlUtils.fromXml(response, SetPuLockResponse.class); // 没返回 → 失败 if (getRes == null || getRes.getResult() == null) { log.warn("云台锁定:设备未返回结果"); return false; } int code = getRes.getResult().getCode(); if (code == 0) { return true; } else { int errorCode = getRes != null && getRes.getResult() != null ? getRes.getResult().getCode() : -1; String errorDesc = ErrorCodeEnum.getDescByCode(errorCode); log.warn("设置云台锁定参数失败,错误码:{},原因:{}", errorCode, errorDesc); return false; } } @Override public byte[] localCapture(CameraCmd cmd) { throw new CameraSDKException( CameraSDKException.ErrorCode.CMD_NOT_SUPPORT.getCode(), cmd.getCameraId(), "功能暂不支持" ); } @Override public String picCutCate(CameraCmd cmd) { String cameraId = cmd.getCameraId(); Integer chanNo = cmd.getChanNo(); String type = cmd.getType() == null ? "other" : cmd.getType(); String bucketName = cmd.getBucketName() == null ? "pic" : cmd.getBucketName(); int seq = getSafeSeq(); String sessionId = SessionCache.get(cameraId); // 获取前端设备图像参数 GetSnapPicSyncRequest getRequest = new GetSnapPicSyncRequest(chanNo, seq, sessionId); String response = nettyTcpClient.sendXmlAndWait(cameraId, getRequest); GetSnapPicSyncResponse getRes = XmlUtils.fromXml(response, GetSnapPicSyncResponse.class); if (getRes == null || getRes.getResult() == null || getRes.getResult().getCode() != 0) { int errorCode = getRes != null && getRes.getResult() != null ? getRes.getResult().getCode() : -1; String errorDesc = ErrorCodeEnum.getDescByCode(errorCode); log.warn("抓图失败,错误码:{},原因:{}", errorCode, errorDesc); return ""; } String base64Str = getRes.getParameters().getPicBuf(); //存储到minio String objectName = cameraId + "_" + chanNo + ".jpeg"; String url = ""; MultipartFile multipartFile = MultipartFileUtil.base64ToMultipartFile(base64Str, objectName, MimeTypeUtils.IMAGE_JPEG); R sysFileR = remoteFileService.upload(multipartFile, bucketName, type); if (sysFileR.getCode() == R.SUCCESS) { url = sysFileR.getData().getUrl(); log.debug("上传文件成功:{}", url); } return url; } @Override public String picCutCate(CameraCmd cmd, String bucketName, String objectName) { return ""; } @Override public PtzDto getPtz(CameraCmd cmd) { try { String cameraId = cmd.getCameraId(); Integer chanNo = cmd.getChanNo(); int seq = getSafeSeq(); String sessionId = SessionCache.get(cameraId); GetPtzInfoRequest request = new GetPtzInfoRequest(chanNo, seq, sessionId); String response = nettyTcpClient.sendXmlAndWait(cameraId, request); GetPtzInfoResponse res = XmlUtils.fromXml(response, GetPtzInfoResponse.class); // 合并空值判断 if (res == null || res.getResult() == null) { String reason = res == null ? "接口返回结果为空" : "接口返回无错误信息"; return null; } Integer errorCode = res.getResult().getCode(); if (errorCode != 0) { // 2. 通过ErrorCodeEnum获取错误描述 String errorDesc = ErrorCodeEnum.getDescByCode(errorCode); // 3. 拼接错误信息,返回包含错误码和描述的提示 String errorMsg = String.format("获取ptz失败,错误码:%s,原因:%s", errorCode, errorDesc); return null; } Float panAngle = res.getParameters().getPanAngle(); Float P = new BigDecimal(panAngle.toString()).setScale(2, RoundingMode.HALF_UP).floatValue(); Float tiltAngle = res.getParameters().getTiltAngle(); Float T = tiltAngle < 0 ? tiltAngle + 360 : tiltAngle; T = new BigDecimal(T.toString()).setScale(2, RoundingMode.HALF_UP).floatValue(); float zoomRatio = res.getParameters().getZoomRatio(); return new PtzDto(P, T, zoomRatio); } catch (Exception ex) { log.error("获取ptz异常:{}", ex.getMessage()); return null; } } @Override public PtzDto getPtzHigh(CameraCmd cmd) { return getPtz(cmd); } @Override public PtzScope getPtzScope(CameraCmd cmd) { PtzScope ptzScope = new PtzScope(); ptzScope.setPMax(360f); ptzScope.setPMin(0f); ptzScope.setTMax(90f); ptzScope.setTMin(270f); ptzScope.setZMax(40f); ptzScope.setZMin(0f); return ptzScope; } @Override public Boolean setPtz(CameraCmd cmd) { try { String cameraId = cmd.getCameraId(); Integer chanNo = cmd.getChanNo(); PtzDto ptzDto = cmd.getPtzDto(); int seq = getSafeSeq(); String sessionId = SessionCache.get(cameraId); //保留小数点2位 Float P = new BigDecimal(ptzDto.getP().toString()).setScale(2, RoundingMode.HALF_UP).floatValue(); Float T = ptzDto.getT() > 300 ? ptzDto.getT() - 360 : ptzDto.getT(); T = new BigDecimal(T.toString()).setScale(2, RoundingMode.HALF_UP).floatValue(); Float Z = ptzDto.getZ(); SetAbsLocationZoomRequest request = new SetAbsLocationZoomRequest(chanNo, P, T, Z, seq, sessionId); String response = nettyTcpClient.sendXmlAndWait(cameraId, request); GetPtzInfoResponse res = XmlUtils.fromXml(response, GetPtzInfoResponse.class); // 合并空值判断 if (res == null || res.getResult() == null) { String reason = res == null ? "接口返回结果为空" : "接口返回无错误信息"; log.warn(reason); return false; } Integer errorCode = res.getResult().getCode(); if (errorCode != 0) { // 2. 通过ErrorCodeEnum获取错误描述 String errorDesc = ErrorCodeEnum.getDescByCode(errorCode); // 3. 拼接错误信息,返回包含错误码和描述的提示 String errorMsg = String.format("设置ptz失败,错误码:%s,原因:%s", errorCode, errorDesc); log.warn(errorMsg); return false; } return true; } catch (Exception ex) { log.error("设置ptz异常:{}", ex.getMessage()); return false; } } @Override public Boolean setPtzHigh(CameraCmd cmd) { return setPtz(cmd); } @Override public Boolean setPtzHighT(CameraCmd cmd) { return setPtz(cmd); } @Override public Boolean setZeroPtz(CameraCmd cmd) { String cameraId = cmd.getCameraId(); Integer chanNo = cmd.getChanNo(); int seq = getSafeSeq(); String sessionId = SessionCache.get(cameraId); // 设置零方位角 SetLocationParaPtRequest request = new SetLocationParaPtRequest(seq, sessionId); String response = nettyTcpClient.sendXmlAndWait(cameraId, request); SetLocationParaPtResponse getRes = XmlUtils.fromXml(response, SetLocationParaPtResponse.class); // 没返回 → 失败 if (getRes == null || getRes.getResult() == null) { return false; } int code = getRes.getResult().getCode(); if (code == 0) { return true; } else { int errorCode = getRes != null && getRes.getResult() != null ? getRes.getResult().getCode() : -1; String errorDesc = ErrorCodeEnum.getDescByCode(errorCode); log.warn("设置零方位角失败,错误码:{},原因:{}", errorCode, errorDesc); return false; } } @Override public String record(CameraCmd cmd) { return ""; } @Override public Boolean guideTargetPosition(CameraCmd cmd) { String cameraId = cmd.getCameraId(); 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 cameraPTZ = GisUtil.getCameraPTZ(cameraPosition, targetPositions, 20, 150); cmd.setPtzDto(cameraPTZ); validatePtzRange(cmd); setPtz(cmd); return true; } /** * 校验 PTZ 参数是否在设备支持的范围内 * * @param cmd 相机命令,需包含 PTZ 参数 * @throws CameraSDKException 校验失败时抛出异常 */ protected void validatePtzRange(CameraCmd cmd) { String cameraId = cmd.getCameraId(); // 从数据库获取 PTZ 范围 PtzScope ptzScope = ardCameraPtzScopeService.getPtzScopeByCameraId(cameraId); if (ptzScope == null) { log.warn("获取 PTZ 范围失败: " + cameraId); throw new CameraSDKException(CameraSDKException.ErrorCode.OVER_GUIDANCE_RANGE.getCode(), cameraId, "获取 PTZ 范围失败,请检查设备配置"); } PtzDto ptzDto = cmd.getPtzDto(); if (ptzDto == null) { log.warn("PTZ 控制参数为空: " + cameraId); throw new CameraSDKException(CameraSDKException.ErrorCode.CMD_NOT_SUPPORT.getCode(), cameraId, "PTZ 控制参数为空"); } float p = ptzDto.getP(); float t = ptzDto.getT(); float z = ptzDto.getZ() < 1 ? 1 : ptzDto.getZ(); // 修正 z 小于 1 的情况 // 水平角度校验 if (p < ptzScope.getPMin() || p > ptzScope.getPMax()) { log.warn("水平角度超出范围:" + p + ",允许范围:" + ptzScope.getPMin() + " ~ " + ptzScope.getPMax()); throw new CameraSDKException(CameraSDKException.ErrorCode.OVER_GUIDANCE_RANGE.getCode(), cameraId, "水平角度超出范围:" + ptzScope.getPMin() + " ~ " + ptzScope.getPMax()); } // 俯仰角度校验 if (t <= 90) { if (t > ptzScope.getTMax()) { log.warn("俯仰角度超出向上范围:" + t + ",最大允许:" + ptzScope.getTMax()); throw new CameraSDKException(CameraSDKException.ErrorCode.OVER_GUIDANCE_RANGE.getCode(), cameraId, "俯仰角度超出向上范围:" + ptzScope.getTMax()); } } else if (t >= 270 && t <= 360) { if (t < ptzScope.getTMin()) { log.warn("俯仰角度超出向下范围:" + t + ",最小允许:" + ptzScope.getTMin()); throw new CameraSDKException(CameraSDKException.ErrorCode.OVER_GUIDANCE_RANGE.getCode(), cameraId, "俯仰角度超出向下范围:" + ptzScope.getTMin()); } } else { log.warn("俯仰角度非法:" + t); throw new CameraSDKException(CameraSDKException.ErrorCode.BEYOND_VISIBILITY_RANGE.getCode(), cameraId, "俯仰角度非法:" + t); } // 变倍校验 if (z > ptzScope.getZMax()) { log.warn("变倍超出范围:" + z + ",最大允许:" + ptzScope.getZMax()); throw new CameraSDKException(CameraSDKException.ErrorCode.OVER_GUIDANCE_RANGE.getCode(), cameraId, "变倍超出范围:" + ptzScope.getZMax()); } } @Override public Boolean recordStart(CameraCmd cmd) { String cameraId = cmd.getCameraId(); Integer chanNo = cmd.getChanNo(); String name = cameraId + "_" + chanNo; try { // 1. 获取流媒体信息 R info = remoteArdVtduService.getInfo(name, SecurityConstants.INNER); if (info.getCode() != R.SUCCESS || info.getData() == null) { log.warn("获取摄像头信息失败: {}, code: {}", name, info.getCode()); return false; } ArdVtdu ardVtdu = info.getData(); String rtspUrl = ardVtdu.getRtspUrl(); if (rtspUrl == null || rtspUrl.isEmpty()) { log.error("RTSP地址为空: {}", name); return false; } // 2. 开始录像 String sessionId = recordSdk.startRecording(cameraId, rtspUrl, null); if (sessionId == null || sessionId.isEmpty()) { log.error("录像SDK返回SessionId为空,开始录像失败: {}", name); return false; } // 3. 缓存 SessionId RecordSessionCache.put(name, sessionId); log.info("录像开始成功: {}, sessionId: {}", name, sessionId); return true; } catch (Exception ex) { log.error("开始录像发生未知异常: {}", name, ex); return false; } } @Override public String recordStop(CameraCmd cmd) { String cameraId = cmd.getCameraId(); Integer chanNo = cmd.getChanNo(); String name = cameraId + "_" + chanNo; String localFilePath = null; try { // 1. 获取并移除 SessionId (原子操作,防止并发问题) String sessionId = RecordSessionCache.remove(name); if (sessionId == null) { log.warn("未找到正在进行的录像会话,可能已停止或未开始: {}", name); return ""; } // 2. 停止录像并获取本地文件路径 List filePaths = recordSdk.stopRecording(sessionId); if (filePaths == null || filePaths.isEmpty()) { log.error("录像停止失败,未获取到文件路径: {}, sessionId: {}", name, sessionId); return ""; } localFilePath = filePaths.get(0); log.info("录像停止成功,本地文件: {}", localFilePath); // 3. 上传到 MinIO // 校验文件是否存在 java.io.File file = new java.io.File(localFilePath); if (!file.exists()) { log.error("录像文件不存在,无法上传: {}", localFilePath); return ""; } MultipartFile multipartFile = MultipartFileUtil.filePathToMultipartFile( localFilePath, name, MediaType.APPLICATION_OCTET_STREAM_VALUE); // 建议使用具体的 MimeType R sysFileR = remoteFileService.upload(multipartFile, "record", cmd.getType()); if (sysFileR.getCode() == R.SUCCESS && sysFileR.getData() != null) { String url = sysFileR.getData().getUrl(); log.info("录像上传MinIO成功: {}", url); return url; } else { log.error("录像上传MinIO失败: {}, msg: {}", name, sysFileR.getMsg()); return ""; } } catch (Exception ex) { log.error("停止录像过程发生异常: {}", name, ex); // 即使异常,SessionId 已经在第一步移除了,防止死锁 // 但这里可能需要记录日志以便人工排查残留文件 return ""; } finally { // 4. 【重要】清理本地临时文件 // 无论成功还是失败,只要生成了本地文件,上传完后都应该删除,防止磁盘爆满 if (localFilePath != null) { try { java.nio.file.Files.deleteIfExists(java.nio.file.Paths.get(localFilePath)); log.debug("本地临时录像文件已删除: {}", localFilePath); } catch (Exception e) { log.warn("删除本地临时文件失败: {}", localFilePath, e); } } } } @Override public PtzParamDTO getGisInfo(CameraCmd cmd) { String cameraId = cmd.getCameraId(); Integer chanNo = cmd.getChanNo(); try { int seq = getSafeSeq(); String sessionId = SessionCache.get(cameraId); GetPtzInfoRequest request = new GetPtzInfoRequest(chanNo, seq, sessionId); String response = nettyTcpClient.sendXmlAndWait(cameraId, request); GetPtzInfoResponse res = XmlUtils.fromXml(response, GetPtzInfoResponse.class); // 合并空值判断 if (res == null || res.getResult() == null) { String reason = res == null ? "接口返回结果为空" : "接口返回无错误信息"; return new PtzParamDTO(); } Integer errorCode = res.getResult().getCode(); if (errorCode != 0) { // 2. 通过ErrorCodeEnum获取错误描述 String errorDesc = ErrorCodeEnum.getDescByCode(errorCode); // 3. 拼接错误信息,返回包含错误码和描述的提示 String errorMsg = String.format("获取ptz失败,错误码:%s,原因:%s", errorCode, errorDesc); return new PtzParamDTO(); } Float panAngle = res.getParameters().getPanAngle(); Float tiltAngle = res.getParameters().getTiltAngle(); Float zoomRatio = res.getParameters().getZoomRatio(); Float zoomAngle = res.getParameters().getZoom();//水平视场角 Float tZoomAngle = res.getParameters().getTZoom();//垂直视场角 PtzParamDTO ptzDTO = new PtzParamDTO(); ptzDTO.setP(panAngle); ptzDTO.setT(tiltAngle); ptzDTO.setZ(zoomRatio); ptzDTO.setFHorFieldAngle(zoomAngle); ptzDTO.setFVerFieldAngle(tZoomAngle); return ptzDTO; } catch (Exception ex) { log.error("设备【{}】获取ptz异常:{}", cameraId, ex.getMessage()); return new PtzParamDTO(); } } @Override public Double correctPitch(CameraCmd cmd) { return 0.0; } @Override public List getChannels(ArdCamera camera) { if (camera == null || camera.getChanNum() <= 0) { return new ArrayList<>(); } String cameraId = camera.getId(); List channels = new ArrayList<>(); for (int i = 1; i <= 2; i++) { ArdChannel channel = new ArdChannel(); CameraCmd cameraCmd = new CameraCmd(cameraId, i); // 设置通道基本属性 channel.setDeviceId(cameraId); String channelName = getChannelName(cameraCmd); channel.setName(channelName); channel.setChanNo(i); // 通道号从1开始 VideoCompressionCfg videoCompressionCfg = getVideoCompressionCfg(cameraCmd); if (videoCompressionCfg != null) { channel.setVideoCode(videoCompressionCfg.getVideoEncType()); } // 获取rtsp地址 String rtspUrl = String.format("rtsp://%s:%s@%s:%d/00010%d", camera.getUsername(), camera.getPassword(), camera.getIp(), camera.getRtspPort(), channel.getChanNo() - 1 ); channel.setRtspUrl(rtspUrl); channels.add(channel); } return channels; } private String getChannelName(CameraCmd cmd) { String cameraId = cmd.getCameraId(); Integer chanNo = cmd.getChanNo(); String sessionId = SessionCache.get(cameraId); int seq = getSafeSeq(); GetPuImageTextParaRequest request = new GetPuImageTextParaRequest(chanNo, seq, sessionId); String response = nettyTcpClient.sendXmlAndWait(cameraId, request); GetPuImageTextParaResponse res = XmlUtils.fromXml(response, GetPuImageTextParaResponse.class); if (res.getParameters() != null && !res.getParameters().getBitmapText().isEmpty()) { String bitmapText = res.getParameters().getBitmapText(); return new String(Base64.getDecoder().decode(bitmapText), Charset.forName("GBK")); } return "通道" + chanNo; } @Override public Boolean set3DPosition(CameraCmd cmd) { try { String cameraId = cmd.getCameraId(); Integer chanNo = cmd.getChanNo(); PointFrame pointFrame = cmd.getPointFrame(); PTZ3DLocationConverter.LocationParam locationParam = PTZ3DLocationConverter.convertPrecise(pointFrame); int seq = getSafeSeq(); String sessionId = SessionCache.get(cameraId); SetPTZ3DLocationRequest request = new SetPTZ3DLocationRequest(chanNo, locationParam, seq, sessionId); String response = nettyTcpClient.sendXmlAndWait(cameraId, request); SetPTZ3DLocationResponse res = XmlUtils.fromXml(response, SetPTZ3DLocationResponse.class); // 合并空值判断 if (res == null || res.getResult() == null) { String reason = res == null ? "接口返回结果为空" : "接口返回无错误信息"; log.warn(reason); return false; } Integer errorCode = res.getResult().getCode(); if (errorCode != 0) { // 2. 通过ErrorCodeEnum获取错误描述 String errorDesc = ErrorCodeEnum.getDescByCode(errorCode); // 3. 拼接错误信息,返回包含错误码和描述的提示 String errorMsg = String.format("获取ptz失败,错误码:%s,原因:%s", errorCode, errorDesc); log.warn(errorMsg); return false; } return true; } catch (Exception e) { log.error("3D定位异常:{}", e.getMessage()); return false; } } @Override public DeviceCFG getSystemInfo(CameraCmd cmd) { try { String cameraId = cmd.getCameraId(); int seq = getSafeSeq(); String sessionId = SessionCache.get(cameraId); GetDeviceInfoRequest request = new GetDeviceInfoRequest(cameraId, seq, sessionId); String response = nettyTcpClient.sendXmlAndWait(cameraId, request); GetDeviceInfoResponse res = XmlUtils.fromXml(response, GetDeviceInfoResponse.class); // 合并空值判断 if (res == null || res.getResult() == null) { String reason = res == null ? "接口返回结果为空" : "接口返回无错误信息"; return null; } Integer errorCode = res.getResult().getCode(); if (errorCode != 0) { // 2. 通过ErrorCodeEnum获取错误描述 String errorDesc = ErrorCodeEnum.getDescByCode(errorCode); // 3. 拼接错误信息,返回包含错误码和描述的提示 String errorMsg = String.format("获取设备信息失败,错误码:%s,原因:%s", errorCode, errorDesc); return null; } DeviceCFG deviceCFG = new DeviceCFG(); deviceCFG.setSDVRName(res.getParameters().getDeviceName()); deviceCFG.setDwSoftwareVersion(res.getParameters().getFirmwareVersion()); return deviceCFG; } catch (Exception ex) { log.error("获取设备信息异常:{}", ex.getMessage()); return null; } } @Override public List> getCameraVideoLable(Long userId, CameraCmd cmd)throws ServiceException { return null; } }