简介:本资源是一套基于Java语言的海康威视设备SDK二次开发实战项目,面向安防监控领域Java开发者及音视频集成工程师,解决网络摄像机与NVR设备在Java生态中难以高效接入、流媒体推拉、图像抓取与录像下载等核心问题。项目完整封装了实时视频流与历史回放流的推流逻辑、定时/触发式抓图、录像文件按时间范围下载、云台控制等高级功能,适合作为智能安防平台、远程监看系统或AI视频分析前置模块的开发基础。压缩包共255个文件,含49个核心Java源码(含SDK调用封装与业务逻辑)、131个XML配置与依赖声明、25个Windows平台DLL及23个Linux平台SO动态库(如PlayCtrl.dll、libcrypto.so.1.0.0、SuperRender.dll等),辅以YML、Shell脚本与Vue前端示例,整体大小39.92MB。目前已有203人学习下载,提供开箱即用的工程结构、跨平台SDK适配方案与典型调用范例,显著降低海康设备Java集成门槛。
1. 为什么 Java 工程师一接到「海康威视 SDK 二次开发」需求就头皮发紧:这不是调个 REST API,而是要亲手把 H.264 流从黑匣子里拽出来再喂给 FFmpeg
你刚收到需求:用 Java 对接海康威视的 IPC(网络摄像机)和 NVR(网络硬盘录像机),实现三件事——实时视频流推送到 RTMP/HTTP-FLV、按时间范围下载历史录像、任意帧抓图保存为 JPEG。你以为只是加个http://ip:8000/ISAPI/Streaming/channels/101就完事?错。海康威视官方 SDK(iSecureCenter SDK / HCNetSDK)压根不提供标准 HTTP 接口;它是一套 C/C++ 编写的本地动态库(Windows 下是HCNetSDK.dll,Linux 下是libhcnetsdk.so),所有核心能力——包括登录、预览、回放、抓图、录像下载——都必须通过 JNI 调用底层 DLL/SO。Java 层只是个“调度员”,真正干活的是那个你连源码都看不到的黑匣子。更现实的是:你没法用 Spring Boot 写个 Controller 就返回视频流,得自己开线程管理 SDK 的回调、自己拼接 PS 流、自己处理时间戳乱序、自己扛住设备断连重连。这不是写业务逻辑,是写嵌入式级的音视频胶水代码。适合谁?有 Java 基础、能看懂 C 头文件、愿意啃.dll文档、能调试 native crash 的后端或安防集成工程师。别指望纯 Web 开发者三天上手——这活儿,血泪经验告诉我,没 3 天 debugNET_DVR_RealPlay_V30回调空指针,你都不算真正摸到门。
2. 从零搭起 Java 调用海康威视 SDK 的最小可行环境:DLL 加载、用户登录、设备在线状态校验
2.1 下载与部署 SDK 包:只认官方渠道,拒绝任何“精简版”或“免注册版”
海康威视 SDK 官方下载入口在 https://www.hikvision.com/cn/support/download/sdk/ (注意:必须用企业邮箱注册,个人邮箱常被拒)。你需要下载“iSecureCenter SDK for Windows/Linux”(不是“Web SDK”或“ISAPI SDK”),版本建议选v6.1.9.45 或 v6.1.10.47(这两个版本对 Java JNI 兼容性最稳,v6.2+ 开始强制要求 .NET Framework 4.7.2,Java 项目易踩坑)。解压后你会看到:
HCNetSDK.dll(Win64)、libhcnetsdk.so(Linux x64)PlayCtrl.dll(Win 下用于本地解码播放,Java 不直接用但依赖其导出函数)HCNetSDK.jar(官方提供的 Java 封装层,仅含接口定义,无实现!)Doc/目录下的HCNetSDK编程指南.pdf和HCNetSDK结构体说明.pdf
提示:
HCNetSDK.jar不能直接mvn install—— 它只是 Java 接口桩,实际逻辑全在 DLL/SO 里。你必须把HCNetSDK.dll放进java.library.path路径(如System.getProperty("java.library.path")输出的目录),或用System.setProperty("jna.library.path", "D:/sdk/lib");显式指定。Linux 下确保LD_LIBRARY_PATH包含libhcnetsdk.so所在目录,并chmod +x libhcnetsdk.so。
2.2 初始化 SDK 并完成设备登录:四步不可省,缺一即失败
海康威视 SDK 是典型的“先初始化、再登录、后操作”状态机。跳过任何一步,后续所有 API 都返回-1(失败)。以下是 Java 层最简登录代码(基于 JNA 封装,非官方 HCNetSDK.jar):
// 使用 JNA 自定义映射(比官方 jar 更可控) public interface HCNetSDK extends Library { HCNetSDK INSTANCE = Native.load("HCNetSDK", HCNetSDK.class); // 初始化 SDK(必须最先调用) boolean NET_DVR_Init(); // 设置 SDK 日志路径(关键!否则崩溃无日志) boolean NET_DVR_SetLogToFile(int level, String sLogPath, boolean bAutoDel); // 用户登录(返回 long 类型的用户句柄,-1 表示失败) int NET_DVR_Login_V30(String sIP, int wPort, String sUserName, String sPassword, NET_DVR_DEVICEINFO_V30 lpDeviceInfo); // 设备信息结构体(必须 new 出来并传引用) class NET_DVR_DEVICEINFO_V30 extends Structure { public byte[] sSerialNumber = new byte[48]; public byte byChannelNum; public byte byStartChan; public byte byIPChanNum; public byte byZeroChanNum; public byte[] sDeviceVersion = new byte[16]; // ... 其他字段省略,完整见 HCNetSDK结构体说明.pdf @Override protected List<String> getFieldOrder() { return Arrays.asList("sSerialNumber", "byChannelNum", "byStartChan", "byIPChanNum", "byZeroChanNum", "sDeviceVersion"); } } } // 登录主流程 public class HikLoginManager { private static int userId = -1; public static boolean initAndLogin(String ip, int port, String user, String pwd) { // Step 1: 初始化 SDK if (!HCNetSDK.INSTANCE.NET_DVR_Init()) { System.err.println("SDK 初始化失败,请检查 DLL 是否加载成功"); return false; } // Step 2: 设置日志(强烈建议!默认日志在 C:\Program Files\Hikvision\SDK\log) HCNetSDK.INSTANCE.NET_DVR_SetLogToFile(3, "D:/hik-log/", true); // 3=DEBUG 级别 // Step 3: 构造设备信息对象 HCNetSDK.NET_DVR_DEVICEINFO_V30 deviceInfo = new HCNetSDK.NET_DVR_DEVICEINFO_V30(); // Step 4: 登录(注意:端口必须是设备 Web 服务端口,通常是 8000,不是 RTSP 端口 554) userId = HCNetSDK.INSTANCE.NET_DVR_Login_V30(ip, port, user, pwd, deviceInfo); if (userId == -1) { int errCode = HCNetSDK.INSTANCE.NET_DVR_GetLastError(); System.err.println("登录失败,错误码:" + errCode + ",请查 HCNetSDK 错误码表"); return false; } System.out.println("登录成功,设备通道数:" + deviceInfo.byChannelNum); return true; } }参数说明与逻辑要点:
NET_DVR_Init()必须全局只调一次,通常放在static {}块中。多次调用不会报错但浪费资源。NET_DVR_SetLogToFile(3, "...", true)中level=3表示输出 DEBUG 日志(含每帧时间戳、回调触发详情),这是排查流中断、花屏的唯一依据。日志路径必须是已存在且可写的绝对路径,相对路径会静默失败。NET_DVR_Login_V30的port参数是设备的Web 服务端口(默认 8000),不是 RTSP 端口(554)或 ONVIF 端口(80)。若设备改过 Web 端口,此处必须同步。deviceInfo必须是new出来的实例,JNA 会自动将其内存地址传给 DLL。传null或未初始化字段会导致 DLL 访问非法内存而崩溃(黑屏、Java 进程退出)。
2.3 设备在线状态心跳检测:别等NET_DVR_Logout才发现设备已掉线
海康威视设备在局域网内可能因 IP 冲突、交换机故障、电源波动瞬间离线。SDK 不会主动通知你,userId句柄仍有效,但后续RealPlay会卡死或回调不触发。必须自己实现心跳:
// 每 30 秒 ping 一次设备 TCP 端口(8000) public static boolean isDeviceOnline(String ip, int port) { try (Socket socket = new Socket()) { socket.connect(new InetSocketAddress(ip, port), 2000); // 2秒超时 return true; } catch (IOException e) { return false; } } // 后台线程定时检测(示例) ScheduledExecutorService heartBeat = Executors.newSingleThreadScheduledExecutor(); heartBeat.scheduleAtFixedRate(() -> { if (!isDeviceOnline("192.168.1.64", 8000)) { System.err.println("设备 192.168.1.64 已离线,执行自动登出"); if (userId != -1) { HCNetSDK.INSTANCE.NET_DVR_Logout(userId); userId = -1; } // 触发重连逻辑... } }, 0, 30, TimeUnit.SECONDS);注意:不能依赖
NET_DVR_GetDVRState(该 API 在 v6.1.x 中对 NVR 返回不稳定),TCP 端口探测是最可靠方式。实测发现:设备 Web 服务端口通,基本意味着 SDK 通信可用。
3. 实时流拉取与推流:用NET_DVR_RealPlay_V30获取 PS 流,再用 FFmpeg 转封装为 RTMP
3.1 启动实时预览:回调函数接收原始 PS 流,不是 JPEG 或 H.264 Annex B
海康威视 IPC/NVR 输出的是MPEG-4 Part 2 / H.264 PS 流(Program Stream),不是常见的 Annex B 格式(00 00 00 01 分隔)。PS 流包含系统头、PES 包、时间戳,需完整解析才能喂给 FFmpeg。NET_DVR_RealPlay_V30的核心是设置一个fRealDataCallBack回调函数,SDK 每收到一帧数据就调用它:
// 定义回调函数(JNA 方式) public interface RealDataCallback extends StdCallLibrary.StdCallCallback { void callback(int nRealHandle, int nDataType, byte[] pBuffer, int dwBufSize, Object pUser); } // 启动预览(关键:nMode=0 表示 TCP 主动连接,nMode=1 表示 UDP,推荐 TCP) public static int startRealPlay(int userId, int channel, RealDataCallback callback) { HCNetSDK.NET_DVR_PREVIEWINFO previewInfo = new HCNetSDK.NET_DVR_PREVIEWINFO(); previewInfo.hPlayHandle = null; // 预留,SDK 内部填充 previewInfo.lChannel = channel; // 通道号,IPC 从 1 开始,NVR 从 1 开始(不是 0) previewInfo.dwStreamType = 1; // 0=主码流,1=子码流(节省带宽) previewInfo.dwLinkMode = 0; // 0=TCP,1=UDP(UDP 易丢包,慎用) previewInfo.bBlocked = true; // true=阻塞模式,false=非阻塞(推荐 true,避免回调堆积) // 注册回调(pUser 可传自定义对象,如 OutputStream) return HCNetSDK.INSTANCE.NET_DVR_RealPlay_V30(userId, previewInfo, callback, null, true); } // 回调实现:将 PS 流写入内存队列或文件 RealDataCallback realDataCB = (nRealHandle, nDataType, pBuffer, dwBufSize, pUser) -> { if (nDataType == HCNetSDK.NET_DVR_SYSHEAD) { // 系统头,仅首次出现 System.out.println("收到 PS 系统头,长度:" + dwBufSize); // 通常此处不做处理,FFmpeg 会自动识别 } else if (nDataType == HCNetSDK.NET_DVR_STREAMDATA) { // 实际视频流数据 // pBuffer 是 PS 流原始字节,直接写入 ByteArrayOutputStream 或 RingBuffer if (psOutputStream != null) { try { psOutputStream.write(pBuffer, 0, dwBufSize); } catch (IOException e) { e.printStackTrace(); } } } };关键参数解释:
dwStreamType=1:务必设为子码流(Sub-stream)。主码流(0)分辨率高、码率大(如 4K@8Mbps),Java 线程处理不过来,极易导致回调堆积、内存 OOM。子码流(如 720P@1Mbps)足够用于 Web 推流。dwLinkMode=0:TCP 模式保证数据不丢,但延迟略高(100~300ms);UDP 模式延迟低但丢包率高,需自行重传,生产环境禁用 UDP。bBlocked=true:阻塞模式下,SDK 会等待你的回调函数返回后再推送下一帧,避免缓冲区溢出;非阻塞模式下 SDK 可能因 Java 回调慢而丢帧。
3.2 将 PS 流实时推送到 RTMP:用 FFmpeg 进程管道注入,而非 Java 解码
你绝不能在 Java 里解析 PS 流再转 H.264 Annex B——计算量巨大,GC 频繁,10 路流就吃满 CPU。正确做法:把 PS 流当作二进制管道输入,交给本地 FFmpeg 进程实时转封装。FFmpeg 命令如下:
# 从 stdin 读取 PS 流,转为 RTMP 推送(关键参数说明) ffmpeg -f mpegts -i - \ -c:v copy -c:a aac -ar 44100 -ab 64k \ -f flv "rtmp://192.168.1.100/live/camera1"-f mpegts -i -:强制 FFmpeg 将 stdin 当作 MPEG-TS 流(海康 PS 流兼容 TS 解析器)-c:v copy:关键!不解码不重编码,直接拷贝视频流(零延迟、零 CPU)-c:a aac:音频流转 AAC(海康设备若无音频,此参数可删)-f flv:输出格式为 FLV(RTMP 协议要求)
Java 调用代码(使用ProcessBuilder):
public class FfmpegPusher { private Process ffmpegProcess; private OutputStream ffmpegStdin; public void startPush(String rtmpUrl) throws IOException { // 启动 FFmpeg 进程 ProcessBuilder pb = new ProcessBuilder( "ffmpeg", "-f", "mpegts", "-i", "-", "-c:v", "copy", "-c:a", "aac", "-ar", "44100", "-ab", "64k", "-f", "flv", rtmpUrl ); pb.redirectError(new File("D:/ffmpeg-error.log")); // 捕获 FFmpeg 错误日志 ffmpegProcess = pb.start(); ffmpegStdin = ffmpegProcess.getOutputStream(); // 启动线程,持续将 PS 流写入 FFmpeg stdin Thread pushThread = new Thread(() -> { try { byte[] buffer = new byte[65536]; int len; while ((len = psInputStream.read(buffer)) != -1) { ffmpegStdin.write(buffer, 0, len); ffmpegStdin.flush(); // 必须 flush!否则 FFmpeg 缓存不触发 } } catch (IOException e) { e.printStackTrace(); } }); pushThread.setDaemon(true); pushThread.start(); } public void stop() { if (ffmpegProcess != null && ffmpegProcess.isAlive()) { ffmpegProcess.destroy(); } } }提示:
ffmpegStdin.flush()是血泪经验——不 flush,FFmpeg 会一直等满 64KB 缓存才处理,导致首帧延迟 3~5 秒。每写一次write()就flush()一次。
4. 历史录像下载与抓图:用NET_DVR_PlayBackByTime_V40定位文件,NET_DVR_SaveRealData保存为 AVI
4.1 按时间范围搜索录像文件:NET_DVR_FindNextFile循环遍历,不是 SQL 查询
海康 SDK 不提供“查询 2024-05-01 08:00 到 09:00 的录像”这种高级 API。你必须:
- 调用
NET_DVR_FindFirstFile_V40指定起止时间、通道号、类型(录像/图片),获取第一个文件句柄 - 循环调用
NET_DVR_FindNextFile获取后续文件,直到返回false - 对每个文件,调用
NET_DVR_PlayBackByTime_V40启动回放,再用NET_DVR_SaveRealData保存为本地 AVI
// 搜索录像文件(返回文件信息列表) public List<HCNetSDK.NET_DVR_FINDDATA_V40> searchRecordFiles( int userId, int channel, Date startTime, Date endTime) { List<HCNetSDK.NET_DVR_FINDDATA_V40> files = new ArrayList<>(); HCNetSDK.NET_DVR_CONDITION_V40 condition = new HCNetSDK.NET_DVR_CONDITION_V40(); condition.struStartTime.dwYear = startTime.getYear() + 1900; condition.struStartTime.dwMonth = startTime.getMonth() + 1; condition.struStartTime.dwDay = startTime.getDate(); condition.struStartTime.dwHour = startTime.getHours(); condition.struStartTime.dwMinute = startTime.getMinutes(); condition.struStartTime.dwSecond = startTime.getSeconds(); condition.struStopTime.dwYear = endTime.getYear() + 1900; condition.struStopTime.dwMonth = endTime.getMonth() + 1; condition.struStopTime.dwDay = endTime.getDate(); condition.struStopTime.dwHour = endTime.getHours(); condition.struStopTime.dwMinute = endTime.getMinutes(); condition.struStopTime.dwSecond = endTime.getSeconds(); condition.dwChannel = channel; condition.dwFileType = 0x01; // 0x01=普通录像,0x02=移动侦测,0x04=报警录像 IntByReference lFindHandle = new IntByReference(); HCNetSDK.NET_DVR_FINDDATA_V40 findData = new HCNetSDK.NET_DVR_FINDDATA_V40(); // 第一次查找 int handle = HCNetSDK.INSTANCE.NET_DVR_FindFirstFile_V40(userId, condition, lFindHandle); if (handle == -1) { System.err.println("查找录像失败,错误码:" + HCNetSDK.INSTANCE.NET_DVR_GetLastError()); return files; } // 循环获取所有匹配文件 do { if (HCNetSDK.INSTANCE.NET_DVR_FindNextFile(lFindHandle.getValue(), findData)) { files.add(findData.clone()); // clone 避免内存复用 } else { break; } } while (true); HCNetSDK.INSTANCE.NET_DVR_FindClose(lFindHandle.getValue()); return files; }注意点:
dwFileType是位掩码,如需同时查普通录像和报警录像,应设为0x01 | 0x04。findData结构体中的struStartTime/struStopTime是该录像文件的实际起止时间,不是你搜索的时间范围。一个 1 小时的录像文件可能跨你搜索的两个时间段。
4.2 下载单个录像文件:启动回放 → 保存为 AVI → 关闭回放,三步缺一不可
// 下载单个录像文件(AVI 格式) public boolean downloadRecordFile(int userId, HCNetSDK.NET_DVR_FINDDATA_V40 file, String outputPath) { // Step 1: 启动回放(注意:lPlayHandle 是回放句柄,非预览句柄) HCNetSDK.NET_DVR_PLAYBACK_INFO_V40 playInfo = new HCNetSDK.NET_DVR_PLAYBACK_INFO_V40(); playInfo.struStartTime = file.struStartTime; playInfo.struStopTime = file.struStopTime; playInfo.dwChannel = file.dwChannel; playInfo.dwStreamType = 0; // 0=主码流,1=子码流(下载录像建议用主码流) IntByReference lPlayHandle = new IntByReference(); if (!HCNetSDK.INSTANCE.NET_DVR_PlayBackByTime_V40(userId, playInfo, lPlayHandle)) { System.err.println("启动回放失败:" + HCNetSDK.INSTANCE.NET_DVR_GetLastError()); return false; } // Step 2: 开始保存(outputPath 必须是 .avi 后缀) if (!HCNetSDK.INSTANCE.NET_DVR_SaveRealData(lPlayHandle.getValue(), outputPath)) { System.err.println("开始保存失败:" + HCNetSDK.INSTANCE.NET_DVR_GetLastError()); HCNetSDK.INSTANCE.NET_DVR_StopPlayBack(lPlayHandle.getValue()); return false; } // Step 3: 等待保存完成(轮询状态) int status; do { status = HCNetSDK.INSTANCE.NET_DVR_GetPlayBackPos(lPlayHandle.getValue()); System.out.println("当前进度:" + status + "%"); try { Thread.sleep(1000); } catch (InterruptedException e) {} } while (status < 100); // Step 4: 停止回放 HCNetSDK.INSTANCE.NET_DVR_StopPlayBack(lPlayHandle.getValue()); System.out.println("录像下载完成:" + outputPath); return true; }提示:
NET_DVR_SaveRealData保存的是 AVI 封装,内部视频流仍是 H.264 PS 格式。若需 MP4,需用 FFmpeg 转换:ffmpeg -i input.avi -c copy output.mp4(无损转换,秒级完成)。
4.3 抓图功能:NET_DVR_CaptureJPEGPicture生成 JPEG,不是截图
抓图不是截取当前播放画面,而是向设备发送指令,让 IPC/NVR硬件编码一张 JPEG 图片并返回,质量高、速度快、不依赖预览状态:
// 抓图(立即触发,无需预览) public boolean captureJpeg(int userId, int channel, String outputPath) { // 参数:用户句柄、通道号、抓图质量(0~63,推荐 50)、输出路径 boolean result = HCNetSDK.INSTANCE.NET_DVR_CaptureJPEGPicture( userId, channel, 50, outputPath ); if (!result) { int err = HCNetSDK.INSTANCE.NET_DVR_GetLastError(); System.err.println("抓图失败,错误码:" + err); return false; } System.out.println("抓图成功:" + outputPath); return true; }避坑点:
outputPath必须是绝对路径,且目录必须存在。D:/pic.jpg可以,./pic.jpg会失败。- 抓图质量
wQuality=50是平衡点:低于 30 图片模糊,高于 60 文件过大(单张 2MB+)。 - 此 API 不需要设备正在预览,只要登录成功即可调用,适合做定时快照任务。
5. 避坑:Java 调用海康 SDK 的 5 个真实翻车现场与解法
5.1 现象:Java 进程突然退出,控制台无任何异常堆栈
原因:SDK DLL 内部发生访问违规(Access Violation),如传入了已被 GC 回收的Structure对象指针,或回调函数中抛出了未捕获的 Java 异常(JNA 默认会终止进程)。
解决:
- 所有
Structure对象(如NET_DVR_DEVICEINFO_V30)必须在回调函数外new,并在整个生命周期内保持强引用(不要让 GC 回收); - 回调函数内
try-catch包裹全部逻辑,绝不让异常透出到 JNA 层; - 启用 SDK 日志(
NET_DVR_SetLogToFile),日志中会记录EXCEPTION_ACCESS_VIOLATION及内存地址,结合windbg分析(Windows)或gdb(Linux)。
5.2 现象:NET_DVR_RealPlay_V30返回 0,但回调函数从不触发
原因:最常见于dwLinkMode设为 1(UDP)且防火墙拦截了 UDP 端口,或设备 UDP 功能未开启;其次是bBlocked=false导致 SDK 回调队列满后静默丢弃。
解决:
- 统一使用
dwLinkMode=0(TCP); - 检查设备 Web 界面:配置 → 网络 → 高级配置 → TCP/UDP 设置 → 确保“TCP 主动连接”启用;
bBlocked=true,并确保回调函数执行时间 < 50ms(避免阻塞 SDK 线程)。
5.3 现象:推流到 Nginx-RTMP 后,播放器显示“无法连接”或“黑屏”
原因:FFmpeg 输入格式识别失败。海康 PS 流头部缺少标准 MPEG-TS sync 字节(0x47),FFmpeg 默认按 TS 解析失败。
解决:
- 强制指定输入格式为
mpegps(不是mpegts):ffmpeg -f mpegps -i - -c:v copy -c:a aac -f flv rtmp://... - 或添加
-analyzeduration 2000000延长分析时间(不推荐,增加首帧延迟)。
5.4 现象:NET_DVR_FindFirstFile_V40总是返回 0 个文件,但设备确有录像
原因:时间参数未对齐设备时区。海康设备默认使用本地时区(如东八区),而 JavaDate对象是 UTC 时间戳,直接赋值会导致时间偏移 8 小时。
解决:
- 不要用
date.getYear()等过时方法,改用Calendar或LocalDateTime转换:Calendar cal = Calendar.getInstance(TimeZone.getTimeZone("GMT+8")); cal.setTime(startTime); condition.struStartTime.dwYear = cal.get(Calendar.YEAR); condition.struStartTime.dwMonth = cal.get(Calendar.MONTH) + 1; // ... 其他字段同理
5.5 现象:多路预览时,某一路回调停止,其他路正常
原因:单个userId句柄的并发预览路数受限。海康 SDK 默认限制为8 路(IPC)或16 路(NVR),超出后新RealPlay返回 0,旧路不受影响。
解决:
- 查阅设备规格书确认最大预览路数;
- 超过限制时,必须创建多个用户登录句柄(即多次
NET_DVR_Login_V30),每 8 路分配一个userId; - 切勿复用同一
userId启动超过限制的预览,SDK 不报错但行为不可预测。
6. 进阶技巧:用NET_DVR_SetDVRMessage实现设备事件订阅,替代轮询告警
你是否还在每秒调用NET_DVR_GetAlarmQueue检查是否有移动侦测?太低效。海康 SDK 提供异步事件机制:NET_DVR_SetDVRMessage注册一个 Windows 消息回调(Windows)或信号量(Linux),当设备产生报警(移动侦测、遮挡、硬盘满),SDK 会主动向你的 Java 程序发送消息。
6.1 Windows 下用 JNA Hook Windows 消息循环
Java 本身无消息循环,需借助User32库创建隐藏窗口并监听:
public class AlarmMessageHandler { private static final int WM_USER = 0x0400; private static final int MSG_ALARM = WM_USER + 1001; // 定义窗口过程回调 public interface WndProc extends StdCallLibrary.StdCallCallback { LRESULT callback(HWND hWnd, int uMsg, WPARAM wParam, LPARAM lParam); } // 创建窗口并注册消息 public static HWND createAlarmWindow(WndProc proc) { WinDef.HINSTANCE hInstance = User32.INSTANCE.GetModuleHandle(null); HWND hwnd = User32.INSTANCE.CreateWindowEx( 0, "STATIC", "", 0, 0, 0, 0, 0, null, null, hInstance, null ); User32.INSTANCE.SetWindowLong(hwnd, User32.GWL_WNDPROC, proc); return hwnd; } // SDK 事件回调(在 WndProc 中触发) public static void onAlarmEvent(int nCommand, HCNetSDK.NET_DVR_ALARMINFO lpAlarmInfo) { if (nCommand == HCNetSDK.NET_DVR_ALARM_PIC || nCommand == HCNetSDK.NET_DVR_ALARM_MOTION) { System.out.println("收到移动侦测报警,通道:" + lpAlarmInfo.dwChannel); // 触发抓图、推流、通知等业务逻辑 } } } // 启动时注册 HWND hwnd = AlarmMessageHandler.createAlarmWindow((hWnd, uMsg, wParam, lParam) -> { if (uMsg == AlarmMessageHandler.MSG_ALARM) { // 解析 lParam 中的报警数据(需根据 SDK 文档反序列化) AlarmMessageHandler.onAlarmEvent(...); } return User32.INSTANCE.DefWindowProc(hWnd, uMsg, wParam, lParam); }); // 向 SDK 注册消息 HCNetSDK.INSTANCE.NET_DVR_SetDVRMessage(hwnd, AlarmMessageHandler.MSG_ALARM);6.2 Linux 下用信号量替代(sigwait)
Linux 无窗口消息,改用sigwait等待 SIGUSR1:
// C 侧注册信号处理(需编译为 .so 供 Java 调用) void setup_alarm_signal() { sigset_t set; sigemptyset(&set); sigaddset(&set, SIGUSR1); pthread_sigmask(SIG_BLOCK, &set, NULL); }Java 层用Runtime.getRuntime().exec("kill -USR1 " + pid)发送信号,或由 SDK 内部触发(需 SDK 支持)。
6.3 为什么值得投入:事件驱动 vs 轮询的性能对比
| 场景 | 轮询方案 | 事件驱动方案 | 优势 |
|---|---|---|---|
| 100 路设备报警检测 | 每秒 100 次NET_DVR_GetAlarmQueue,CPU 占用 35% | 仅在报警发生时触发回调,CPU 占用 < 2% | 降低 95% CPU 开销 |
| 报警响应延迟 | 500ms ~ 1s(取决于轮询间隔) | < 50ms(SDK 内部立即投递) | 实时性提升 20 倍 |
| 代码复杂度 | 需维护线程池、超时重试、状态机 | 单回调函数处理,无状态管理 | 减少 70% 告警模块代码 |
我上线过一个 200 路 NVR 的集中管理平台,切换事件驱动后,告警服务从 4C8G 降配到 2C4G,且再没出现过漏报。这件事教会我:海康 SDK 的价值不在“能做什么”,而在“怎么让它少做”——少一次轮询,就少一分不可靠。
希望帮到你。
本文还有配套的精品资源,点击获取