package com.ard.work.sdk.hb.service; import com.ard.common.core.constant.CacheConstants; import com.ard.common.core.exception.ServiceException; import com.ard.common.redis.service.RedisService; import com.ard.work.api.domian.*; import com.ard.work.device.camera.domain.PtzParamDTO; import com.ard.work.event.LoginEvent; import com.ard.work.sdk.common.osSelect; import com.ard.work.sdk.hb.cache.CameraRealHandleCache; import com.ard.work.sdk.hb.cache.HbDeviceInfoCache; import com.ard.work.sdk.hb.cache.HbRealCallbackCache; import com.ard.work.sdk.hb.cache.HbStreamStatsCache; import com.ard.work.sdk.hb.lib.HBNetSDK; import com.ard.work.sdk.hb.lib.HbSdkErrorCodeEnum; 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.utils.ArdTool; import com.sun.jna.Native; import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; import org.springframework.context.ApplicationEventPublisher; import org.springframework.stereotype.Service; import java.io.File; import java.io.IOException; import java.io.OutputStream; import java.nio.charset.Charset; import java.util.ArrayList; import java.util.List; import java.util.Map; /** * 类描述:汉邦SDK * * @author lsy * @since 2026/3/11 7:55 */ @Service("hbSDK") @Slf4j public class HbSDK implements CameraSDK { private static HBNetSDK hbNetSDK = null; @Resource private RedisService redisService; @Resource private ApplicationEventPublisher eventPublisher; static { // initSdk(); } public static void initSdk() { if (hbNetSDK == null) { if (!CreateSDKInstance()) { System.out.println("Load SDK fail"); return; } } // 初始化 if (!hbNetSDK.HB_NET_Init()) { log.warn("汉邦SDK初始化失败,错误码:{} 原因:{}", hbNetSDK.HB_NET_GetLastError(), HbSdkErrorCodeEnum.getDescByCode(hbNetSDK.HB_NET_GetLastError())); return; } hbNetSDK.HB_NET_SetLogLevel(-1); log.info("初始化汉邦SDK完成"); } @Override public void login(ArdCamera camera) { try { String ip = camera.getIp(); String username = camera.getUsername(); String password = camera.getPassword(); short port = camera.getPort().shortValue(); log.info("登录设备 IP: {}, Port: {}, User: {}, Password: {}", ip, port, username, password); HBNetSDK.HB_NET_DEVICEINFO deviceInfo = new HBNetSDK.HB_NET_DEVICEINFO(); long userId = hbNetSDK.HB_NET_Login(ip, port, username, password, deviceInfo); if (userId == 0) { handleLoginFail(camera, userId); } else { handleLoginSuccess(camera, userId, deviceInfo); } // 发布登录事件 eventPublisher.publishEvent(new LoginEvent(camera)); } catch (Exception e) { camera.setLoginId(-1L); log.error("登录失败: {}", e.getMessage()); } } /** * 登录失败处理 */ private void handleLoginFail(ArdCamera camera, Long loginId) { camera.setChanNum(0); camera.setLoginId(loginId); camera.setState("0"); int error = hbNetSDK.HB_NET_GetLastError(); log.warn("设备[{}:{}]登录失败,错误码:{} 原因:{}", camera.getIp(), camera.getPort(), error, HbSdkErrorCodeEnum.getDescByCode(error)); } /** * 登录成功处理 */ private void handleLoginSuccess(ArdCamera camera, Long loginId, HBNetSDK.HB_NET_DEVICEINFO deviceInfo) { deviceInfo.read(); // 更新设备信息 camera.setLoginId(loginId); camera.setState("1"); int chanNum = Byte.toUnsignedInt(deviceInfo.byChanNum); camera.setChanNum(chanNum); //log.info("设备名称: {}", ArdTool.parseBytesToString(deviceInfo.sDvrName, Charset.forName("GBK"))); HbDeviceInfoCache.put(camera.getId(), deviceInfo); // 获取最新通道 List channels = getChannels(camera); if (!channels.isEmpty()) { camera.setChannelList(channels); } startRealPlay(camera); // 登录成功 log.info("设备登录成功 [{}:{}]", camera.getIp(), camera.getPort()); } private long startRealPlay(ArdCamera camera) { String cameraId = camera.getId(); int chanNo = 1; Long lUserID = camera.getLoginId(); // 已保证登录成功 log.warn("开始预览,登录句柄:{}", lUserID); // 1. 初始化队列和运行状态 CameraStreamQueueManager.destroyCameraQueue(cameraId); HbStreamStatsCache.initRunningStatus(cameraId); // 初始化运行状态为true CameraStreamQueueManager.createCameraQueue(cameraId); // 启动推流线程 String RTSP_URL = "rtsp://192.168.1.2:8554/" + cameraId + "_" + chanNo; startPushThread(cameraId, RTSP_URL); // ===================== 核心:使用回调缓存 ===================== // 2.1 从缓存获取回调实例(无则创建) StreamCallBack streamCallback = HbRealCallbackCache.get(cameraId); if (streamCallback == null) { // 缓存中无实例,创建新的并存入缓存 streamCallback = new StreamCallBack(cameraId); HbRealCallbackCache.put(cameraId, streamCallback); log.info("相机[{}]创建新的流回调实例并缓存", cameraId); } else { log.info("相机[{}]复用已缓存的流回调实例", cameraId); } // 客户端信息 HBNetSDK.HB_NET_CLIENTINFO clientInfo = new HBNetSDK.HB_NET_CLIENTINFO(); clientInfo.dwSize = clientInfo.size(); clientInfo.lChannel = 0; clientInfo.lStreamType = 0; clientInfo.lLinkMode = 0; clientInfo.pfnCallback = streamCallback; clientInfo.dwRes = new int[2]; clientInfo.write(); // 启动预览 long handle = hbNetSDK.HB_NET_RealPlay(lUserID, clientInfo); if (handle < 0) { log.error("预览启动失败, 错误码: {}", hbNetSDK.HB_NET_GetLastError()); return -1; } hbNetSDK.HB_NET_MakeKeyFrame(lUserID, 0, 0); log.info("预览启动成功,句柄: {}", handle); return handle; } private void startPushThread(String cameraId, String RTSP_URL) { Thread pushThread = new Thread(() -> { log.info("推流线程启动,目标地址: {}", RTSP_URL); boolean hasKeyFrame = false; Process ffmpegProcess = null; OutputStream ffmpegInput = null; boolean isRunning = HbStreamStatsCache.getRunningStatus(cameraId); while (isRunning) { try { ProcessBuilder pb = new ProcessBuilder( "ffmpeg", "-loglevel", "error", // 错误日志级别 "-re", // 实时推流 "-i", "pipe:0", // 从标准输入读取 "-c:v", "copy", // 视频编码拷贝 "-an", // 无音频 "-f", "rtsp", // 输出格式RTSP "-rtsp_transport", "tcp", // TCP传输 "-timeout", "5000000", // 5秒超时 "-buffer_size", "1024000", // 缓冲区大小 "-max_delay", "500000", // 最大延迟 RTSP_URL ); pb.redirectErrorStream(true); ffmpegProcess = pb.start(); ffmpegInput = ffmpegProcess.getOutputStream(); log.info("FFmpeg进程启动成功"); while (HbStreamStatsCache.getRunningStatus(cameraId)) { if (!isProcessAlive(ffmpegProcess)) { log.error("FFmpeg进程已退出,准备重启"); break; } byte[] data = CameraStreamQueueManager.pollDataFromQueue(cameraId); if (data == null) { continue; } // 等待关键帧 if (!hasKeyFrame && data.length >= 5) { int nalType = data[4] & 0x1F; if (nalType == 5) { hasKeyFrame = true; } } if (!hasKeyFrame) { continue; } // 原样推流,不控制、不丢帧、不花屏 try { ffmpegInput.write(data); ffmpegInput.flush(); } catch (IOException e) { log.error("写入FFmpeg失败: {}", e.getMessage()); break; } } } catch (Exception e) { log.error("推流异常", e); } finally { try { if (ffmpegInput != null) ffmpegInput.close(); } catch (Exception ignored) { } try { if (ffmpegProcess != null) ffmpegProcess.destroy(); } catch (Exception ignored) { } isRunning = HbStreamStatsCache.getRunningStatus(cameraId); if (isRunning) { log.warn("相机[{}]推流中断,5秒后重启...", cameraId); try { Thread.sleep(5000); } catch (InterruptedException ignored) { } } } } log.info("推流线程结束"); }, "PushThread"); pushThread.setDaemon(true); pushThread.start(); } /** * 检查进程是否存活 */ private boolean isProcessAlive(Process process) { if (process == null) return false; try { process.exitValue(); return false; } catch (IllegalThreadStateException e) { return true; } } @Override public void logout(String cameraId) { try { // 1. 停止推流线程(让while循环退出) HbStreamStatsCache.stopRunning(cameraId); // 2. 移除状态缓存 HbStreamStatsCache.removeRunningStatus(cameraId); // 3. 停止SDK预览 Long lRealHandle = CameraRealHandleCache.get(cameraId); if (lRealHandle != null) { hbNetSDK.HB_NET_StopRealPlay(lRealHandle); CameraRealHandleCache.remove(cameraId); log.info("【{}】停止预览成功", cameraId); } // 4. 登出设备 Long loginId = redisService.getCacheMapValue(CacheConstants.CAMERA_ONLINE, cameraId); if (loginId != null) { hbNetSDK.HB_NET_Logout(loginId); redisService.deleteCacheMapValue(CacheConstants.CAMERA_ONLINE, cameraId); log.info("【{}】设备登出成功", cameraId); } // 5. 销毁队列 CameraStreamQueueManager.destroyCameraQueue(cameraId); log.info("【{}】注销完成 → 推流已停止", cameraId); } catch (Exception e) { log.error("【{}】注销异常", cameraId, e); } } @Override public Boolean isOnLine(CameraCmd cmd) { return true; } @Override public Boolean pTZControl(CameraCmd cmd) { return true; } @Override public Boolean setFocusPos(CameraCmd cmd) { return true; } @Override public Integer getFocusPos(CameraCmd cmd) { return 0; } @Override public Boolean setPreset(CameraCmd cmd) { return true; } @Override public Boolean gotoPreset(CameraCmd cmd) { return true; } @Override public VideoCompressionCfg getVideoCompressionCfg(CameraCmd cmd) { return null; } @Override public Boolean controlDefog(CameraCmd cmd) { return true; } @Override public Boolean isEnableDefog(CameraCmd cmd) { return true; } @Override public Boolean controlInfrared(CameraCmd cmd) { return true; } @Override public Boolean isEnableInfrared(CameraCmd cmd) { return true; } @Override public Boolean controlFocusMode(CameraCmd cmd) { return true; } @Override public Boolean getFocusMode(CameraCmd cmd) { return true; } @Override public Boolean controlPTHeater(CameraCmd cmd) { return true; } @Override public Boolean controlCameraDeicing(CameraCmd cmd) { return true; } @Override public Boolean getPTZLockInfo(CameraCmd cmd) { return true; } @Override public Boolean setPTZLockInfo(CameraCmd cmd) { return true; } @Override public byte[] localCapture(CameraCmd cmd) { return new byte[0]; } @Override public String picCutCate(CameraCmd cmd) { return ""; } @Override public String picCutCate(CameraCmd cmd, String bucketName, String objectName) { return ""; } @Override public PtzDto getPtz(CameraCmd cmd) { return null; } @Override public PtzScope getPtzScope(CameraCmd cmd) { return null; } @Override public Boolean setPtz(CameraCmd cmd) { return true; } @Override public Boolean setZeroPtz(CameraCmd cmd) { return true; } @Override public String record(CameraCmd cmd) { return ""; } @Override public Boolean guideTargetPosition(CameraCmd cmd) { return true; } @Override public Boolean recordStart(CameraCmd cmd) { return true; } @Override public String recordStop(CameraCmd cmd) { return ""; } @Override public PtzParamDTO getGisInfo(CameraCmd cmd) { return null; } @Override public Double correctPitch(CameraCmd cmd) { return 0.0; } @Override public List getChannels(ArdCamera camera) { //获取通道 List channelList = new ArrayList<>(); try { HBNetSDK.HB_NET_DEVICEINFO deviceInfo = HbDeviceInfoCache.get(camera.getId()); if (deviceInfo == null) { log.warn("汉邦设备信息为空,无法获取通道"); return channelList; } int chanNum = Byte.toUnsignedInt(deviceInfo.byChanNum); // 实际通道数 log.info("通道数量: {}", chanNum); String allChanNames = ArdTool.parseBytesToString(deviceInfo.sChanName, Charset.forName("GBK")); String[] chanNames = new String[chanNum]; // 按每个通道名称长度拆分(HB_NAME_LEN) for (int i = 0; i < chanNum; i++) { int start = i * 32; int end = Math.min(start + 32, allChanNames.length()); chanNames[i] = allChanNames.substring(start, end).trim(); log.info("通道号: {}", i); log.info("通道名称: {}", chanNames[i]); ArdChannel ardChannel = new ArdChannel(); ardChannel.setName(chanNames[i]); ardChannel.setChanNo(i + 1); ardChannel.setDeviceId(camera.getId()); channelList.add(ardChannel); } } catch (Exception ex) { log.error("获取IP通道异常:{}", ex.getMessage(), ex); } return channelList; } @Override public Boolean set3DPosition(CameraCmd cmd) { return null; } @Override public DeviceCFG getSystemInfo(CameraCmd cmd) { return null; } /** * 动态库加载 */ private static boolean CreateSDKInstance() { if (hbNetSDK == null) { synchronized (HBNetSDK.class) { String strDllPath = ""; try { String userDir = System.getProperty("user.dir"); if (osSelect.isWindows()) { strDllPath = userDir + "\\lib\\hb\\win64\\libnetsdk.dll"; } else if (osSelect.isLinux()) { if (osSelect.isArm64()) { strDllPath = userDir + "/lib/hb/linux-arm64/libnetsdk.so"; } else if (osSelect.isX64()) { strDllPath = userDir + "/lib/hb/linux64/libnetsdk.so"; } else { throw new UnsupportedOperationException("❌ 不支持的 Linux 架构: " + osSelect.getArch()); } } else { throw new UnsupportedOperationException("❌ 不支持的操作系统: " + osSelect.getOsName()); } // 校验文件是否存在 File sdkFile = new File(strDllPath); if (!sdkFile.exists()) { System.err.println("❌ SDK 动态库不存在: " + strDllPath); return false; } System.out.println("✅ 尝试加载动态库: " + strDllPath); hbNetSDK = Native.loadLibrary(strDllPath, HBNetSDK.class); System.out.println("✅ SDK 加载成功!"); } catch (Exception ex) { System.err.println("❌ SDK 加载失败: " + strDllPath + "\n异常信息: " + ex.getMessage()); ex.printStackTrace(); return false; } } } return true; } @Override public List> getCameraVideoLable(Long userId, CameraCmd cmd)throws ServiceException { return null; } }