首页 / 资讯中心 / 文章详情

Java调用海康SDK:JNA封装实时流、抓图与录像下载实战

Java调用海康SDK:JNA封装实时流、抓图与录像下载实战 ★ FEATURED ARTICLE
简介面向需将海康威视网络摄像机或NVR接入自研系统的Java开发者这份二次开发资源包以实时流与历史流推流、抓图、录像下载及云台控制为核心解决C版SDK在Java环境中难调用的问题。无论是自建视频监控平台、远程巡检还是报警联动均可复用其中的接口调用思路降低视频功能集成门槛。压缩包共255个文件大小约39.92MB其中49个Java文件为核心业务源码131个XML用于配置与界面描述25个DLL与23个SO动态库提供底层支撑另含JAR依赖包、VUE页面及Shell辅助脚本便于按模块检索学习。目前已有203人学习下载适合具备Java基础、正在做摄像头或NVR接入的开发者参考。对照源码与动态库可在IDE中直接调试推流、抓图和录像下载流程掌握设备初始化、通道选择及参数设置等关键步骤有效缩短二次开发周期。1. 先搞明白Java 调海康 SDK本质是跨语言 DLL 封装问题第一次把海康威视网络摄像机的取流代码从 C# 迁移到 Java 时我最大的感受是难点不在业务逻辑而在跨语言调用那一层。海康官方 SDK 是 C 语言导出的 DLL / SOJava 要调用就得借助 JNA 或 JNI再加上实时流回调、历史流回放、抓图、录像下载这些功能都要通过同一个 native 层进出任何一个环节的库加载、内存释放、回调线程处理不当都会让整个 Java 服务在跑了几小时后悄悄崩掉。这篇笔记要拆的就是一套基于 JNA 封装 HCNetSDK 与 PlayCtrl 的 Java 源码覆盖实时流/历史流推流、抓图、录像下载和云台控制。它适合两类人一类是要把海康设备能力接入 Spring Boot 等 Java 后端的开发另一类是手头有设备但一直卡在 DLL 加载和回调处理的桌面应用开发者。如果你只是用网页版监控插件看一眼画面这套东西对你来说太重了。2. HCNetSDK 与 PlayCtrl两套库的分工和 JNA 侧的选择2.1 JNA 还是 JNI我为什么选 JNA先说结论这套源码用的是 JNA不是 JNI。海康官方没有为 Java 提供正式的 SDK 封装社区里常见的做法就是拿 JNA 把 HCNetSDK.dll 的导出函数重新声明一遍。JNA 和 JNI 的差别落到实际开发上很明显对比项JNAJNI开发效率Java 接口里直接映射 DLL 函数不用写 C 桥接代码每个函数都要写 C 包装层还要处理 javah 生成头文件回调支持接口里声明 Callback 即可回调运行在 SDK 自己的线程里要手动处理 JavaVM 指针和全局引用很容易踩内存坑性能默认映射模式有反射开销但开启 DirectMapping 后可接受接近原生调用维护成本换 SDK 版本时改 Java 接口声明就行C 层和 Java 层要同步改视频场景里真正的性能瓶颈在网络收包和内存拷贝JNA 那点反射开销在码流回调面前可以忽略。我一般会建议团队直接用 JNA省下来的时间够把登录、取流、录像下载这些业务逻辑写好。唯一要注意的是JNA 的 Interface Mapping 模式下每次调用会做参数校验和反射回调频繁时建议用Library接口上的DirectMapping特性或者把回调里的数据拷贝逻辑写得足够薄。2.2 核心接口梳理初始化、登录、取流三个入口海康 HCNetSDK 的函数非常多但真正决定业务主流程的就那么几个。这套源码里我把它们收敛成一张对照表方便你对着自己的 SDK 头文件找函数作用关键参数NET_DVR_Init初始化 SDK整个进程只调一次无NET_DVR_SetConnectTime设置连接超时时间和重试次数超时毫秒数、重试次数NET_DVR_Login_V40登录摄像头或 NVR返回登录句柄IP、端口、用户名、密码NET_DVR_RealPlay_V40开始实时预览取流数据通过回调返回登录句柄、通道号、码流类型、回调对象NET_DVR_CaptureJPEGPicture抓取 JPEG 图片到指定文件登录句柄、通道号、JPEG 参数结构体NET_DVR_PlayBackByName_V40按录像文件名回放历史流登录句柄、文件名、回放条件NET_DVR_GetFileByName按时间段下载录像文件登录句柄、通道号、起止时间、保存路径登录是后面所有操作的前提。海康的设备端口默认是 8000不是网页访问用的 80 端口这个很多人第一次写都会搞错。登录返回的loginUserId是负一表示失败失败后用NET_DVR_GetLastError拿错误码最常见的错误是密码错误和设备不存在。2.3 native 库加载先解决 DLL 加载再谈业务逻辑拿到这套源码包第一步不是改业务代码而是把start.bat看懂。文件清单里的start.bat本质上是设置 Java 进程的启动参数把 native 库路径指到 DLL 所在目录。我一般在 Windows 下这样写echo off cd /d %~dp0 set JNA_PATHlib\native set CLASS_PATHbin;conf;lib\* java -Xms256m -Xmx1024m -Djna.library.path%JNA_PATH% -cp %CLASS_PATH% com.yourpkg.Main pause关键在-Djna.library.path和cd /d %~dp0这两处。%~dp0是 bat 脚本所在目录不写这个的话你在资源管理器里双击 bat 和从命令行执行工作目录不一样DLL 就找不到了。这是最典型的启动失败场景。-Xms和-Xmx根据业务量调视频回调场景内存给到 1G 以上比较稳。Java 侧加载库的代码长这样public class HikSdk { private static HCNetSDK sdk null; private static int loginUserId -1; public static void init(String libPath) { // 这一行必须在 Native.load 之前设置否则 JNA 会去系统默认路径找 System.setProperty(jna.library.path, libPath); sdk Native.load(HCNetSDK, HCNetSDK.class); sdk.NET_DVR_Init(); sdk.NET_DVR_SetConnectTime(2000, 1); } public static int login(String ip, short port, String user, String pass) { NET_DVR_USER_LOGIN_INFO loginInfo new NET_DVR_USER_LOGIN_INFO(); loginInfo.sDeviceAddress ip; loginInfo.wPort port; loginInfo.sUserName user; loginInfo.sPassword pass; NET_DVR_DEVICEINFO_V40 deviceInfo new NET_DVR_DEVICEINFO_V40(); loginUserId sdk.NET_DVR_Login_V40(loginInfo, deviceInfo); if (loginUserId -1) { System.err.println(login failed, error code: sdk.NET_DVR_GetLastError()); } return loginUserId; } }这里有两个点要说明。NET_DVR_USER_LOGIN_INFO和NET_DVR_DEVICEINFO_V40是结构体在 Java 侧用 JNA 的Structure映射字段顺序必须和 C 头文件一致建议在类上用StructureFieldOrder显式声明避免不同 JNA 版本解析字段顺序不一致。另外NET_DVR_Login_V40返回的是 int 句柄这个句柄要存成全局变量后面所有操作都要用到。注意加载失败的排查顺序是——先确认jna.library.path指向的目录存在再用dumpbin /dependents HCNetSDK.dllWindows或ldd HCNetSDK.soLinux看依赖库是否齐全。HCNetSDK 依赖 OpenSSLWindows 下是libeay32.dllLinux 下是libcrypto.so.1.0.0只拷主 DLL 不拷依赖库的现象非常普遍后面避坑章节会细说。3. 实时流推流与抓图把摄像头画面变成可用的数据流3.1 实时流的数据通路与回调设计海康实时流取回来的数据是一条 PS 封装的 H.264 或 H.265 码流。SDK 内部起了一个取流线程收到数据后调用你注册的回调函数回调函数的签名一般是这样public interface RealDataCallback extends StdCallLibrary.StdCallCallback { void invoke(int lRealHandle, int dwDataType, ByteBuffer pBuffer, int dwBufSize, Pointer pUser); }lRealHandle是预览句柄dwDataType是数据类型0 表示码流数据2 表示 YV12 原始图像数据还有 3 是 PCM 音频。pBuffer指向的是 SDK 内部缓冲区这个缓冲区是复用的一块内存你必须在回调返回前把数据拷出来不能直接保存ByteBuffer引用否则下一帧数据一到上一帧的内容就被覆盖了。这套源码里的做法是回调里只做一件事——把数据拷贝到byte[]数组扔进一个有界BlockingQueue然后立即返回。消费端从队列里取数据做推流或者写文件。这样做的原因很简单SDK 的取流线程被阻塞的后果不是丢帧那么简单而是整个预览链路卡死登录句柄可能直接失效得重新登录才能恢复。排队策略我一般用LinkedBlockingQueue容量设 1000 到 2000消费端跟不上时丢弃最旧的数据而不是阻塞回调线程。3.2 推流实现把回调裸流变成下游能用的流拿到实时流裸数据后要推给下游常见有三条路第一种最省事——设备直接开 RTSP让下游自己连摄像头的 RTSP 地址SDK 只负责设备管理和抓图。第二种SDK 取流 FFmpeg 转封装Java 把回调数据喂给 FFmpeg 的标准输入FFmpeg 输出 RTMP 或 RTSP。第三种Java 自己解析 PS 包重新封 RTP这条路工作量极大除非你的下游协议是私有协议否则我强烈不建议自己写。我一般用第二种代码结构如下public class StreamPush { // 有界队列消费速度跟不上时丢弃最旧帧 private BlockingQueuebyte[] queue new LinkedBlockingQueue(1000); private volatile boolean running true; // 海康实时流回调只做入队绝不做 IO 或锁操作 public void receive(int lRealHandle, int dwDataType, ByteBuffer pBuffer, int dwBufSize, Pointer pUser) { if (dwDataType ! 0) return; // 只处理码流数据 byte[] frame new byte[dwBufSize]; pBuffer.get(frame); if (!queue.offer(frame)) { queue.poll(); // 丢最旧的一帧 queue.offer(frame); } } // 消费线程把 PS 流原样喂给 FFmpeg做转封装不转码 public void consume(String targetUrl) throws Exception { ProcessBuilder pb new ProcessBuilder( ffmpeg, -i, pipe:0, -c, copy, // 不转码保持实时性 -f, flv, targetUrl // 推 RTMP 用 flv 封装 ); pb.redirectErrorStream(true); Process process pb.start(); try (OutputStream out process.getOutputStream()) { while (running) { byte[] frame queue.poll(200, TimeUnit.MILLISECONDS); if (frame ! null) { out.write(frame); out.flush(); } } } process.waitFor(); } }这里几个参数说下。-c copy是流复制不做转码延迟最低但要注意如果源是 H.265 而下游播放器不支持 H.265画面会黑屏这时要么让设备切子码流一般子码流是 H.264要么用-c:v libx264转码。-f flv是推 RTMP 的固定封装格式推 RTSP 则改成-f rtsp。实际部署时FFmpeg 进程要加-rtsp_transport tcp之类的参数来避免 UDP 丢包这些参数按具体协议栈调。回调里新建byte[]数组在码率高的场景会产生大量垃圾内存后面避坑章节会讲如何用缓冲区池来优化。3.3 抓图手动抓抓 JPEG 与定时抓帧的实现差异抓图有两个入口。一个是 SDK 直接抓 JPEG调用NET_DVR_CaptureJPEGPicture设备端直接把一帧编码成 JPEG 存到指定路径适合定时抓图、事件抓图这种低频场景。另一个是前端显示时抓帧通过 PlayCtrl 播放库把当前画面保存成 BMP 或 JPEG适合桌面应用里“预览界面点一下截图”这种交互。写个简单的定时抓图示例public void captureJpeg(int userId, int channelNo, String savePath) { NET_DVR_JPEGPICTURE_INFO jpegInfo new NET_DVR_JPEGPICTURE_INFO(); jpegInfo.wPicSize 0; // 0 表示默认分辨率 jpegInfo.wPicQuality 0; // 0 表示默认画质 jpegInfo.sPicFileName savePath; // 设备端生成的文件路径 boolean ok sdk.NET_DVR_CaptureJPEGPicture(userId, channelNo, jpegInfo); if (!ok) { int err sdk.NET_DVR_GetLastError(); System.err.println(capture jpeg failed, error: err); } }注意这个函数在不同型号设备上行为略有差异。有些老型号只支持把图片存到设备本地或 SMB 路径不支持直接传回 PC 路径较新的型号支持在参数里指定路径。如果调用返回成功但文件没出现先用 SDK 自带的 MVA 软件在 PC 上测一下能不能抓到排除设备固件限制。定时抓图的调度我一般用ScheduledExecutorService固定频率 1 秒一张抓图本身是 IO 操作丢到线程池里执行不要阻塞取流回调。4. 历史流回放与录像下载按时间段取数的正确姿势4.1 录像文件检索与按名回放入口历史流回放的流程比实时流多一步先查录像文件列表拿到文件名再用文件名发起回放。海康老版接口是NET_DVR_FindFile配合NET_DVR_FindNextFile遍历V40 版本换成了NET_DVR_FindFile_V40结构体里带通道号和时间范围。查询结果里包含文件名、文件大小、开始时间、结束时间这些字段。回放时用NET_DVR_PlayBackByName_V40传入文件名和回放条件结构体数据同样通过回调返回数据格式和实时流一样是 PS 流区别只在数据来源是存储介质。查文件列表的典型写法public ListRecordFileInfo findRecordFiles(int userId, int channel, Date start, Date end) { ListRecordFileInfo list new ArrayList(); NET_DVR_FIND_COND cond new NET_DVR_FIND_COND(); cond.dwChannel channel; cond.struStartTime toHikTime(start); cond.struStopTime toHikTime(end); int handle sdk.NET_DVR_FindFile(userId, cond); if (handle -1) { System.err.println(find file failed, error: sdk.NET_DVR_GetLastError()); return list; } NET_DVR_FINDDATA findData new NET_DVR_FINDDATA(); while (sdk.NET_DVR_FindNextFile(handle, findData) 0) { list.add(new RecordFileInfo( findData.sFileName, findData.struStartTime, findData.struStopTime, findData.dwFileSize )); } sdk.NET_DVR_FindClose(handle); return list; }FindNextFile返回 0 表示还有下一个文件返回 -1 表示查询结束。这里有个细节遍历时findData结构体是复用的每次调用前要把它重置否则sFileName这种字符串字段可能残留上一轮的脏数据。JNA 结构体里的字符数组建议用byte[]映射而不是String取出来后再new String(bytes, GBK)转码因为海康的文件名字符串是 GBK 编码直接当 UTF-8 处理会乱码。4.2 录像下载的时间边界处理录像下载和回放是两条独立的路径。回放是边看边收数据下载是直接把录像文件从设备拉到本地。最常用的下载接口是NET_DVR_GetFileByName参数是通道号、起止时间和保存路径不需要先查文件名SDK 内部会自己匹配时间段对应的录像文件。这个函数名在不同 SDK 版本里稍有差异新版本叫NET_DVR_GetFileByName_V40核心逻辑没变。下载时要特别注意时间边界。海康的时间粒度是秒开始时间和结束时间的处理稍有不慎就会拿到空数据public boolean downloadRecord(int userId, int channel, String startTime, String endTime, String saveFile) { NET_DVR_PLAYCOND cond new NET_DVR_PLAYCOND(); cond.dwChannel channel; cond.struStartTime parseHikTime(startTime); // 精确到秒 cond.struStopTime parseHikTime(endTime); int handle sdk.NET_DVR_GetFileByName(userId, saveFile, cond); if (handle -1) { System.err.println(download failed, error: sdk.NET_DVR_GetLastError()); return false; } // 下载是异步的需要轮询进度或等待回调 while (sdk.NET_DVR_GetDownloadProgress(handle, null) ! 100) { Thread.sleep(1000); } sdk.NET_DVR_StopGetFile(handle); return true; }实践中有三个边界坑基本都是血泪教训第一跨天问题。用户输入2025-06-01 23:55:00到2025-06-02 00:10:00中间跨了午夜有的设备固件一次只能查一天必须拆成两段分别下载再合并。第二结束时间含最后一秒的问题。录像存储是按秒写索引的结束时间刚好落在某条录像的最后一秒时部分固件会认为该录像不完整而不返回数据常见的规避是把结束时间加 1 秒再查。第三时间段内没有录像时GetFileByName会返回句柄但下载进度直接 100%生成一个 0 字节文件业务侧要做文件大小校验而不是只看返回状态。4.3 码流类型与网络参数设置回放和下载都要面对码流类型选择这个参数直接影响带宽占用和清晰度。海康的码流类型号不是全局统一的但绝大多数设备遵循这个惯例码流类型值含义典型用途0主码流高清晰度预览、录像下载1子码流低带宽预览、多画面轮询2三码流手机端或第三方平台取流NVR 上的通道号有两个维度要分清。登录 NVR 后通道号对应的是 NVR 上的 IPC 通道索引一般从 1 开始和 NVR 本地画面显示的通道号一致但部分型号的NET_DVR_RealPlay_V40接口要求通道号从 0 开始预览通道 1 要传 0。我每次接新设备都会先调一次设备能力集接口读取通道总数和起始编号避免在客户现场翻车。网络参数方面NET_DVR_SetConnectTime(2000, 1)设置的是连接超时 2 秒、重试 1 次。内网环境下 2 秒够用跨公网访问时建议放宽到 5 秒。还有一个经常被忽略的NET_DVR_SetRecvTimeOut设置的是接收数据超时默认值较大在弱网环境下会导致掉线很久才感知到我会主动把它设成 3 到 5 秒配合心跳机制快速发现断线。5. 避坑与常见问题六个容易翻车的地方5.1 加载阶段DLL 找不到与依赖库缺失现象 1本地 IDE 里跑得好好的打成 jar 包部署到服务器就报UnsatisfiedLinkError: 找不到 HCNetSDK.dll。原因DLL 的搜索路径在打包后变了。IDE 里运行的jna.library.path指向的是开发目录服务器上这个路径不存在而且 DLL 不能从 jar 包内部直接加载。解决把 native 目录放在 jar 包外部保持目录结构不变。启动脚本里用cd /d %~dp0切到脚本所在目录再设置-Djna.library.pathlib\native部署时把整个工程目录拷贝过去而不是只拷 jar。现象 2HCNetSDK.dll 能加载但程序启动时报依赖库错误Windows 下通常是libeay32.dll找不到Linux 下是libcrypto.so.1.0.0找不到进程直接崩掉。原因海康的 HCNetSDK 依赖 OpenSSL 加密库。这个依赖库不是系统自带的也不会跟着主 DLL 自动加载。很多人只拷贝了 HCNetSDK.dll漏掉了文件清单里的libeay32.dll或libcrypto.so.1.0.0加载主库时依赖链断裂。解决把工程里 native 目录下的所有 DLL / SO 文件全部拷贝到目标机器同一个目录不要挑挑拣拣。Linux 下用ldd HCNetSDK.so查看完整的依赖树缺哪个补哪个libopenal.so.1是音频对讲相关的不用对讲功能时也建议放上避免某些固件启动时校验。现象 3双击start.bat窗口一闪而过没有任何报错信息但通过命令行直接敲 java 命令却能启动。原因闪退多半是脚本里的相对路径失效。双击 bat 时当前工作目录是 bat 所在目录没问题但从其他位置调用或计划任务触发时工作目录变了classpath 里的conf、lib就找不到了。解决bat 脚本第一行加cd /d %~dp0强制切换到脚本自身所在目录。这是 Java 工程启动脚本最基础的一条也是我最常看到别人踩的坑。5.2 取流阶段回调不触发、画面卡死、内存上涨现象 4登录成功实时流预览调用也返回了句柄但回调函数一次都没被调用画面完全空白。原因JNA 回调对象被垃圾回收了。如果回调对象是方法局部变量方法结束后 JNA 侧的引用被 GC 清除SDK 线程再去调回调就会静默失败。这是 JNA 封装 C SDK 的经典坑。解决回调对象必须持有一个强引用放在类的成员变量里保证生命周期覆盖整个取流过程private final RealDataCallback callback new RealDataCallback() { Override public void invoke(int handle, int dataType, ByteBuffer buf, int size, Pointer user) { // 处理数据 } };另外建议在调用NET_DVR_RealPlay_V40之前把回调先赋值给成员变量顺序反了在多线程场景下可能出现回调注册到一半时对象被回收的竞态。现象 5回调里写数据库、写文件后画面开始卡顿马赛克变多最后取流中断需要重新登录。原因SDK 的取流线程是专用的回调里做阻塞操作会拖慢整个收包过程SDK 内部缓冲区溢出后触发断流。数据库写入的延迟动不动几十毫秒在帧间隔只有几十毫秒的码流下几乎必然导致问题。解决回调里只做数据拷贝和入队所有 IO 操作交给独立的消费线程。队列满时丢弃最旧帧而不是阻塞保证回调线程永远不被阻塞。这个设计在 3.2 节已经写了是整个推流模块的核心。现象 6服务运行几个小时内存稳步上涨最终 OOM但堆转储看不出明显的大对象问题。原因回调里每次new byte[dwBufSize]产生大量短期大对象在码率 4Mbps 的设备上每秒产生上百次分配叠加 GC 压力。另一个隐蔽问题是 JNA 的ByteBuffer没有手动释放。解决用ByteBuffer.allocateDirect配合一个对象池复用缓冲区回调里从池里取、用后归还或者退一步用ThreadLocal缓存一个可扩容的byte[]实测能显著减少 GC 次数。DirectBuffer 在不用时用sun.misc.Cleaner清理避免 native 内存泄漏。5.3 平台迁移Windows 跑通、Linux 崩溃现象 7Windows 上一切正常部署到 Linux 服务器后一启动就报libcrypto.so.1.0.0: cannot open shared object file。原因Linux 的库搜索路径与 Windows 不同系统不会自动在当前目录找.so文件必须通过LD_LIBRARY_PATH显式指定。解决启动脚本里加一行export LD_LIBRARY_PATH/opt/hik/lib:$LD_LIBRARY_PATH同时把libcrypto.so.1.0.0、libopenal.so.1这些依赖库全部放到该目录。注意版本号不能改很多机器上自带的是libcrypto.so.1.1或更新版本SDK 编译时链接的是 1.0.0版本不匹配同样加载失败。Linux 下还可以用ln -s做软链来凑版本但前提是 API 兼容不要指望libcrypto.so.1.1能被1.0.0的依赖直接拉起来。6. 验证链路与上线检查一段自检代码跑通全流程无论你改动了登录参数、回调逻辑还是下载模块上线前我都建议跑一遍完整的链路自检。这段代码把所有关键节点串在一起任何一个环节失败都能快速定位public static void main(String[] args) { // 校验参数设备IP、用户名、密码 String ip args[0]; String user args[1]; String pass args[2]; // 1. 初始化并登录 init(lib/native); int uid login(ip, (short) 8000, user, pass); if (uid -1) { System.err.println(login failed, exit); return; } // 2. 实时流取流 5 秒验证回调有数据 CountDownLatch latch new CountDownLatch(1); RealDataCallback cb (handle, type, buf, size, p) - { if (size 0) latch.countDown(); }; int preview startRealPlay(uid, 0, cb); if (preview -1) { System.err.println(real play failed); return; } boolean dataReceived latch.await(5, TimeUnit.SECONDS); System.out.println(real stream data: (dataReceived ? OK : NO DATA)); // 3. 抓图验证 captureJpeg(uid, 0, selfcheck.jpg); File img new File(selfcheck.jpg); System.out.println(capture: (img.exists() img.length() 0 ? OK : FAILED)); // 4. 清理资源 stopRealPlay(preview); sdk.NET_DVR_Logout(uid); sdk.NET_DVR_Cleanup(); }这个自检流程我一直在用它验证了三件事DLL 加载和依赖库是否完整、登录凭据是否有效、取流回调和抓图链路是否通畅。如果第 2 步超时问题基本在回调注册或码流类型参数如果第 3 步失败但第 2 步正常问题在 JPEG 参数或设备固件限制。注意清理顺序不能乱先停预览再登出最后NET_DVR_Cleanup释放 SDK 全局资源顺序反了可能把 native 层的状态搞脏下次初始化出现诡异的登录超时。还有一个小技巧自检代码里把每一步的耗时打出来作为后续性能对比的基线。比如说登录耗时超过 1 秒多半是设备地址解析走了 DNS 而不是直连 IP取流首帧延迟超过 2 秒优先检查网络跨网段和 MTU 设置。从那以后我每次上线新设备或更新 SDK 版本都强制先跑一遍这个自检流程再进业务联调。这样做的好处是出问题时你只会在业务代码里找原因而不是在 JNA 封装层里做没有头绪的玄学排查。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站