简介:基于Java对接海康威视SDK进行二次开发,资源实现了网络摄像头与门禁系统的完整联动,适用于毕业设计、课程设计或企业项目开发。资源包共186个文件,以174个Java源码为主,辅以XML/YML配置、JAR依赖、Dockerfile及说明文档,整体仅1.5MB,代码结构清晰,便于导入与二次修改。项目覆盖设备注册登录、局域网扫描发现、门禁人员与人脸信息管理、门禁卡与人脸下发、门禁事件布防及含照片的事件上传,同时支持摄像头当前帧获取与RTSP/SDK推流,功能链路完整。源码已经过严格测试,并附带MD文档说明,适合需要快速搭建海康设备接入方案的Java开发者参考,可在此基础上扩展业务逻辑。目前已有554人学习下载,实践参考价值较高。
1. 一个同时管理网络摄像头和门禁事件列表的 Java 安防工程该怎么搭
摄像头负责“看得见”,门禁负责“放行”,但把这两类硬件能力接到同一个业务系统里,才算完整的安防闭环。用 Java 对接海康威视 SDK 做二次开发的标准做法是:通过 JNA 绑定 HCNetSDK 动态库,代码里维护设备登录会话,视频通道负责预览、抓图和云台操作,门禁通道负责事件回调、远程开门,两类数据在同一套业务逻辑中做联动,比如“刷卡瞬间触发抓拍门内画面”。这个标题在毕业设计、课程设计、项目开发三个场景里都很常见,差异只在功能深度:毕设和课设要求能演示“预览 + 门禁 + 记录”,企业项目则要求长期稳定运行、异常恢复和设备兼容。它反直觉的地方在于:真正难的往往不是 Java 业务代码,而是 SDK 初始化时机、结构体字段顺序和回调线程模型,这三件事不提前规划,后续每接一台设备都容易返工。如果是 5 年以上的后端,这套体系的核心价值不在于拼装接口,而在于把设备侧的回调压力、掉线重连和业务高可用在同一个 Java 进程里处理干净。
2. 用 JNA 在 Maven 工程里搭出海康威视 SDK 能跑起来的最小骨架
2.1 JNA 与 JNI 的取舍:接口映射开销在哪里
先回答“为什么不是 JNI”。海康威视 SDK 提供的是 C 语言动态库,传统方案是写 JNI 桥接层,手工编译 .so、.dll。这种做法的缺点是:每升级一次 SDK 或换一台服务器 CPU 架构,桥接代码都要重新编译,而且 C 侧的头文件概念对纯 Java 团队是有学习门槛的。JNA 的做法是用一个 Java 接口声明动态库导出函数,用 Structure 描述 C 结构体,省掉编译环节。它的性能开销主要是结构体拷贝,但对 HCNetSDK 这种控制面接口,一次登录、一次抓图的调用频率远低于视频帧级别的吞吐,真正的瓶颈在回调线程里处理业务的速度,不在 JNA 映射本身。视频帧数据随后由 PlayCtrl 库在 native 侧解码,返回给显示层时已经是画面,不经过 Java 层逐帧拷贝,所以整体链路可行。
实际项目的常见结构是一个 HCNetSDK 接口加一个设备管理服务类,所有登录句柄、预览句柄、回调注册都收敛到一个服务里,避免多个模块拿到同一个句柄却各自释放,造成不稳定。
2.2 Maven 依赖和 SDK 本地库的加载方式
Maven 依赖只需要 JNA 本体,不需要引入海康威视的 Java 包,因为海康威视官方 JDK 包大多还是基于 JNI 的,对 SDK 版本很敏感。JNA 依赖写法如下:
<dependency> <groupId>net.java.dev.jna</groupId> <artifactId>jna</artifactId> <version>5.13.0</version> </dependency>本地库加载需要根据操作系统选择名称,Windows 下是hcnetsdk,Linux 下通常是libhcnetsdk。工程中建议封装一个加载器,避免在业务代码里到处写Native.load:
public class HikLibraryLoader { public static HCNetSDK load() { String libName = System.getProperty("os.name").contains("Windows") ? "hcnetsdk" : "libhcnetsdk"; try { return Native.load(libName, HCNetSDK.class); } catch (UnsatisfiedLinkError e) { throw new IllegalStateException( "加载海康威视SDK本地库失败,请确认库文件路径和JVM位数", e); } } }海康威视 SDK 的库文件不是单文件就能工作的,主库依赖同目录下的若干组件库。如果只拷走 HCNetSDK.dll 而忽略同级目录,初始化可能成功,但一旦登录设备就会异常退出。常见文件与被依赖关系见下表:
| 文件 | 作用 | 容易踩的坑 |
|---|---|---|
| HCNetSDK.dll / libhcnetsdk.so | 主控制库:登录、门禁、抓图 | 不能脱离同级组件库独立运行 |
| PlayCtrl.dll / libPlayCtrl.so | 视频预览解码库 | 只做抓图可以不加载 |
| HCNetSDKCom 子目录 | 协议组件库,部分设备型号依赖 | 启动时加载顺序错误会闪退 |
Windows 下需要把 DLL 所在目录加入 PATH,Linux 设置 LD_LIBRARY_PATH。这里最典型的现场排查是:本地 IDE 运行正常,打包部署到服务器后报“初始化失败”,多半是 jar 包里虽然有 DLL,但运行时没有被释放到正确的解压目录。建议部署脚本和 jar 包同级放libs/目录,启动参数指定-Djava.library.path=./libs。
2.3 初始化与设备登录的最小可用代码
SDK 初始化只需要一次,不要在每次抓图前反复调用。最小登录代码如下:
public class HikDeviceConnector { private final HCNetSDK sdk; public HikDeviceConnector(HCNetSDK sdk) { this.sdk = sdk; } public int login(String ip, int port, String user, String password) { sdk.NET_DVR_Init(); sdk.NET_DVR_SetConnectTime(5000, 3); HCNetSDK.NET_DVR_USER_LOGIN_INFO loginInfo = new HCNetSDK.NET_DVR_USER_LOGIN_INFO(); loginInfo.sDeviceAddress = ip.getBytes(StandardCharsets.UTF_8); loginInfo.wPort = (short) port; loginInfo.sUserName = user.getBytes(StandardCharsets.UTF_8); loginInfo.sPassword = password.getBytes(StandardCharsets.UTF_8); loginInfo.bUseTransport = 0; HCNetSDK.NET_DVR_DEVICEINFO_V40 deviceInfo = new HCNetSDK.NET_DVR_DEVICEINFO_V40(); int userId = sdk.NET_DVR_Login_V40(loginInfo, deviceInfo); if (userId == -1) { int err = sdk.NET_DVR_GetLastError(); throw new HikDeviceException("登录失败, code=" + err); } return userId; } }登录返回的userId是所有后续操作的会话句柄,它不是设备 ID,而是 SDK 内部维护的资源编号。NET_DVR_DEVICEINFO_V40会在登录时回填通道数byChanNum和起始通道号byStartChan,预览和抓图都基于这个通道号,不需要拿着设备 IP 再查一次。NET_DVR_SetConnectTime(5000, 3)表示单次网络操作超时 5 秒,最多重试 3 次,适合实验室局域网。bUseTransport=0表示不使用自动协商的传输加密,老式摄像头固件如果登录失败,可以尝试把这一位改成 1。
提示:一个 JVM 进程只调用一次
NET_DVR_Init。毕设里最常见的错误是每次抓图前重新初始化,导致句柄泄漏,运行半个小时后登录时报“内存不足”。
3. 网络摄像头模块:JPEG 抓图、实时预览与 PTZ 控制参数
3.1 先抓图还是先预览:按项目阶段选择调用方向
海康网络摄像头登录后,最容易立刻看到成果的是抓图,因为抓图是同步接口,调用完 JPEG 文件就落在磁盘,排障路径很短。实时预览则需要回调线程和播放库配合,链路长、问题点多。毕业设计排期建议先做抓图,再做门禁联动,最后把预览功能作为完整度展示补上。
在真实项目里,如果只是把摄像头画面嵌入 Web 前端,还有一条更省事的路线:海康网络摄像头本身支持 RTSP,Java 后端只需要在业务逻辑层维护“通道号—RTSP 地址”的映射,前端用原生播放器拉流即可,不需要用 SDK 预览。SDK 预览适合桌面客户端、视频墙或需要叠加 OSD 的场景。这个取舍写入技术方案文档,能省不少开发时间。
3.2 NET_DVR_CaptureJPEGPicture 抓图与参数设置
抓图接口入参包括登录句柄、通道号、JPEG 参数和输出文件路径:
public String capture(int userId, int channel, String targetDir) { HCNetSDK.NET_DVR_JPEGPARA para = new HCNetSDK.NET_DVR_JPEGPARA(); para.wPicSize = 0xff; // 0xff 表示原图尺寸,不做缩放 para.wPicQuality = 0; // 画质等级,0 代表最清晰 String file = targetDir + "/capture_" + System.currentTimeMillis() + ".jpg"; boolean ok = sdk.NET_DVR_CaptureJPEGPicture(userId, channel, para, file); if (!ok) { int code = sdk.NET_DVR_GetLastError(); throw new RuntimeException("抓图失败, code=" + code); } return file; }wPicSize是输出图片尺寸枚举,0xff在大多数固件里表示“保持原始分辨率”。wPicQuality是 0 到 100 的压缩质量,海康把 0 定义为最高质量,但这和很多图像库的习惯相反,正式环境建议做一次质量对比,否则抓出来的图可能体积过大。抓图是同步 IO,如果摄像头和服务器之间的链路带宽不足,调用可能阻塞数秒,因此不要把抓图直接放在 HTTP 请求线程里,应该丢给线程池异步执行。
3.3 RealPlay_V40 与 PlayCtrl 库配合的预览通道
如果项目必须展示“软件界面里的实时画面”,走NET_DVR_RealPlay_V40加 PlayCtrl 的标准链路。预览需要两个库配合:HCNetSDK 负责建立取流链路,PlayCtrl 负责解码与渲染。
HCNetSDK.NET_DVR_PREVIEWINFO previewInfo = new HCNetSDK.NET_DVR_PREVIEWINFO(); previewInfo.lChannel = channel; previewInfo.dwStreamType = 0; // 主码流 previewInfo.dwLinkMode = 0; // TCP 取流 previewInfo.bBlocked = 1; // 同步等待预览建立 int previewHandle = sdk.NET_DVR_RealPlay_V40(userId, previewInfo, onData, null); if (previewHandle == -1) { throw new RuntimeException("预览失败, code=" + sdk.NET_DVR_GetLastError()); }在onData回调里需要区分数据类型:
if (dataType == NET_DVR_SYSHEAD) { player.PlayM4_OpenStream(playId, buffer, bufSize, 2 * 1024 * 1024); player.PlayM4_Play(playId, 0); } else if (dataType == NET_DVR_STREAMDATA) { player.PlayM4_InputData(playId, buffer, bufSize); }NET_DVR_SYSHEAD是流头,必须先传给 PlayCtrl 建立解码上下文;NET_DVR_STREAMDATA是后续视频帧。最常见的问题是只处理了帧数据而忽略系统头,结果是画面黑屏或解码器一直等待初始化数据。还要注意回调函数运行在 SDK 内部创建的线程上,不要在回调里做超过 100 毫秒的操作,否则取流缓冲区会被反压填满。
3.4 PTZ 控制命令表与停止位处理
云台控制使用NET_DVR_PTZControlWithSpeed,常用命令及其含义如下:
| 动作 | 状态 |
|---|---|
| 左转 / 右转 | PAN_LEFT / PAN_RIGHT |
| 上仰 / 下俯 | TILT_UP / TILT_DOWN |
| 放大 / 缩小 | ZOOM_IN / ZOOM_OUT |
完整调用需要“开始 + 停止”两次命令:
public void ptz(int userId, int channel, int command, int speed, int durationMs) { sdk.NET_DVR_PTZControlWithSpeed(userId, channel, command, 0, speed); try { Thread.sleep(durationMs); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } finally { sdk.NET_DVR_PTZControlWithSpeed(userId, channel, command, 1, speed); } }第四个参数dwStop,0 表示开始,1 表示停止。如果不发停止命令,老式球机会一直转到限位;部分新固件支持单次调用转固定时长,但 SDK 层面不做保证,可移植写法仍然是“启停配对”。速度取值 1 到 7,1 最慢。这个接口可以和门禁联动,比如刷卡后可调用云台转向预设位再抓图。
4. 门禁系统模块:事件回调、远程开门与通行记录落地
4.1 报警回调注册与 MSGCallBack 的 JNA 写法
海康门禁的刷卡、门磁、人脸认证成功与失败都通过报警方式上报,因此要先注册回调,再让设备进入布防状态。回调接口的 JNA 映射如下:
public interface MSGCallBack extends Callback { void invoke(int lCommand, HCNetSDK.NET_DVR_ALARMER alarmer, com.sun.jna.Pointer alarmInfo, int bufLen, com.sun.jna.Pointer pUser); }注册回调通常使用NET_DVR_SetDVRMessageCallBack_V50。这里有一个必须注意的 Java 细节:回调对象在 JNA 里必须被强引用持有,如果定义成局部变量,GC 会在不确定时间回收它,导致设备一次正常刷卡后回调直接消失。正确做法是把回调实例保存为 Spring Bean 或单例字段。
注册回调后,还要为门禁设备建立报警监听通道。一般调用NET_DVR_SetupAlarmChan_V41,传入报警参数结构体,里面包含事件等级、离线重传、布防方式等字段。布防方式通常设为 0,表示所有事件都上报,默认值以开发包头文件为准。程序退出时,必须调用对应的关闭通道接口释放资源,否则再次初始化可能出现资源被占。
4.2 刷卡事件的 NET_DVR_ACS_EVENT 结构解析
事件回调收到的alarmInfo是一个指针,需要通过 JNA Structure 来读。门禁刷卡事件常对应NET_DVR_ACS_EVENT:
public static class NET_DVR_ACS_EVENT extends Structure { public int dwMajor; // 事件大类 public int dwMinor; // 事件小类 public NET_DVR_TIME struTime; // 事件发生时间 public byte[] sNetUser = new byte[32]; public byte[] sRemoteHostAddr = new byte[128]; public NET_DVR_ACS_CARD_INFO struCardInfo; public int dwCardRecNum; public int dwLockStatus; public int dwVipLevel; }在回调里不能直接把指针强转成对象,而是要从指针重新填充结构体:
if (lCommand == NET_DVR_ALARM_ACS_EVENT) { HCNetSDK.NET_DVR_ACS_EVENT event = new HCNetSDK.NET_DVR_ACS_EVENT(); event.readFields(alarmInfo); String cardNo = new String(event.struCardInfo.byCardNo).trim(); String eventTime = String.format("%04d-%02d-%02d %02d:%02d:%02d", event.struTime.dwYear, event.struTime.dwMonth, event.struTime.dwDay, event.struTime.dwHour, event.struTime.dwMinute, event.struTime.dwSecond); doorPassService.onPass(cardNo, eventTime, event.dwMajor, event.dwMinor); }这里最容易出的问题有两个。第一,JNA 结构体字段顺序必须与 C 头文件完全一致,顺序错了整条数据错位,典型表现是卡号正常但时间乱码。第二,Java 的 byte 数组里如果包含中文姓名,海康设备默认按 GBK 编码返回,要用 GBK 解码而不是 UTF-8。
4.3 远程开门接口的封装与设备差异处理
远程开门统一走NET_DVR_RemoteControl控制接口。它和视频预览共用同一个userId,但门禁一体机、分控器和人脸终端传入的控制参数并不完全一样:有的传门号,有的传卡号,有的只需要空指针,有的要求传具备门锁结构的输入缓冲。正确做法是先从海康开发包示例中找到对应设备型号的调用方式,把控制码宏名抄过来,不要用网上流传的魔法数字。
业务层封装时可以做一个门禁命令接口,屏蔽设备差异:
public interface DoorCommand { int getControlCode(); Pointer toNative(); int bufferSize(); }远程开门服务只依赖这个抽象:
public boolean openDoor(int userId, DoorCommand command) { boolean ok = sdk.NET_DVR_RemoteControl( userId, command.getControlCode(), command.toNative(), command.bufferSize()); if (!ok) { log.warn("远程开门失败, code={}", sdk.NET_DVR_GetLastError()); } return ok; }这样的好处是,当设备从分控器换成门禁一体机时,改动只发生在新增的DoorCommand实现里,门禁开门的业务代码不用动。多门设备还必须区分门编号,调用时给NET_DVR_RemoteControl传入包含门编号的结构体,否则可能默认操作第一道门。
4.4 通行记录的映射与入库 SQL
门禁事件最终要落库。通行记录表可以按事件类型拆分通用字段和扩展字段,最小结构如下:
| 概念 | 对应字段 | 说明 |
|---|---|---|
| 人员标识 | card_no, user_name | 卡号优先,姓名为冗余字段 |
| 门信息 | door_no | 多门设备必须有 |
| 发生时间 | event_time | 用设备回传的本地时间 |
| 事件类别 | event_type | 0 刷卡 1 远程开门 2 按铃 |
建表 SQL 参考:
CREATE TABLE door_pass_record ( id BIGINT PRIMARY KEY AUTO_INCREMENT, device_sn VARCHAR(32) COMMENT '设备序列号', card_no VARCHAR(64) COMMENT '刷卡号', user_name VARCHAR(64) COMMENT '人员姓名', door_no INT COMMENT '门编号', event_type TINYINT COMMENT '0-刷卡 1-远程开门 2-按铃', event_time DATETIME COMMENT '事件时间', create_time DATETIME DEFAULT CURRENT_TIMESTAMP, KEY idx_card_time (card_no, event_time) ) COMMENT '门禁通行记录';回调里拿到struTime后转成LocalDateTime再写入,不要直接存 SDK 返回的 UTC 秒数。设备时区如果没有设为北京时间,前后会出现 8 小时偏差,这个问题很难从代码层看出来,要在设备配置页核对。这张表也是后续统计人员进出频次、加班时间的基础。
5. 摄像头与门禁联动:线程池、掉线重连与部署验证
5.1 用一个阻塞队列解耦门禁事件与摄像头抓图
刷卡事件回调绝对不能直接调抓图接口,因为抓图是同步网络请求,会阻塞 SDK 的报警推送线程。通常做法是收到门禁事件后,把事件放进LinkedBlockingQueue,由单独消费线程触发抓图:
class PassEventTrigger implements Runnable { private final LinkedBlockingQueue<PassEvent> queue = new LinkedBlockingQueue<>(); public void offer(PassEvent e) { queue.offer(e); } @Override public void run() { while (!Thread.currentThread().isInterrupted()) { PassEvent e = queue.take(); String pic = cameraService.capture(e.getUserId(), e.getChannel(), "pass"); log.info("联动抓图完成, card={}, pic={}", e.getCardNo(), pic); } } }这种读写分离的好处是把高并发事件削峰,即使摄像头抓图超时到 5 秒,也只是队列积压,不会堵住回调线程。如果需要保证不丢事件,消费完成后在内存里维护一个小型重试队列,失败消息最多重试 3 次,超过次数写入死信表,方便人工排查。
5.2 掉线重连:把“设备重启”当成常态处理
网络摄像头和门禁主机在真实环境里会定期升级、重启,也可能因为交换机重启而断线。不要只在业务代码里做事后重连,而是在 SDK 层就启用自动重连。初始化后调用NET_DVR_SetReconnect(10000, true),表示每 10 秒尝试重连一次。需要注意,自动重连成功后的userId在部分 SDK 版本里并不会自动恢复有效,严谨做法是在回调或状态检测中发现连接断开时,主动NET_DVR_Logout,再重新登录,并把新userId更新到内存的设备映射表。
为了兜底“断线无通知”的情况,还可以加一层心跳检测,每 30 秒调用一次轻量配置读取接口,连续失败 3 次就把设备标记为离线。这样做还能顺便解决设备离线后门禁状态页面长时间不刷新的问题。
5.3 部署时容易被忽略的三个验证点
第一个验证点是 JVM 位数。海康威视官方 SDK 动态库对 32 位与 64 位区分很清楚,JVM 位数不匹配时,初始化阶段可能加载不到库文件,报UnsatisfiedLinkError,而代码和 Maven 依赖完全没问题。第二个验证点是防火墙和路由:SDK 通信默认端口一般是 8000,RTSP 是 554,设备如果在不同 VLAN,只放通 8000 而忽略 554,预览就会卡死但门禁正常。第三个验证点是把 DLL 或 so 文件打包进 jar 后,不是所有部署环境都能正确解压释放。建议部署脚本创建独立libs/目录,启动参数明确指定-Djava.library.path=./libs,比在代码里依赖相对路径更可控。
本文还有配套的精品资源,点击获取