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<ArdChannel> 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<ArdChannel> getChannels(ArdCamera camera) {
|
//获取通道
|
List<ArdChannel> 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<Map<String, Object>> getCameraVideoLable(Long userId, CameraCmd cmd)throws ServiceException {
|
return null;
|
}
|
}
|