现在制造业很多都是用的海康的摄像头,作为程序员有时候需要对接海康摄像头,实现门禁访问控制,监控预览,录像文件下载等功能。
一、开发环境准备
在海康官网下载SDK开发文档及库文件:
https://open.hikvision.com/download/5cda567cf47ae80dd41a54b3?type=10
根据软件部署的平台选择对应的版本,这里以win64版本为例。
将下载好的库文件复制粘贴加入到java项目中,并改名为lib文件夹。
注意:demo里的examples.jar和jna.jar也需要放在lib文件下,以方便调试。
然后在project structure中加入lib库文件中。
将下载下来的demo中的文件复制粘贴到项目文件中,填入对应的摄像头的账号密码。执行程序,若显示登录成功,则表示调试成功。
二、实现java调用设备接口
打开下载的《设备网络SDK使用手册.chm》作为参考,在该手册中java如何对接海康摄像头有详细说明。
由于设备网络SDK是封装的动态链接库(Windows的dll或者Linux的so),各种开发语言对接SDK,都是通过加载动态库链接,调用动态库中的接口实现功能模块对接,因此,设备网络SDK的对接不区分开发语言,而且对接的流程和对应的接口都是通用的,各种语言调用动态库的方式有所不同。
java语言是通过JNA的方式调用动态链接库中的接口,实现在java语言中调用C/C++语言封装的接口。
因此,java的类文件不需要编写任何业务代码实现某接口来调用设备,而是声明一个接口就能调用设备的功能了。
这只需要在一个java接口中描述目标native library的函数与结构,JNA将自动实现Java接口到native function的映射,而不需要编写任何Native/JNI代码,大大降低了Java调用动态链接库的开发难度。
JNA调用C/C++的过程大致如下:
(一)加载动态链接库
通过java调用海康官方提供的设备功能,首先需要自定义一个接口加载dll文件,比如demo中是声明HCNetSDK
的接口,该接口继承Library 或 StdCallLibrary。
默认的是继承Library ,如果动态链接库里的函数是以stdcall方式输出的,那么就继承StdCallLibrary。接口内部需要一个公共静态常量:INSTANCE,通过这个常量,就可以获得这个接口的实例,从而使用接口的方法,也就是调用外部dll/so的函数。
INSTANCE常量通过 Native.loadLibrary() API函数获得,(新版本的jna中,常量是通过Native.load()函数获取的)该函数有2个参数:
// SDK接口说明,HCNetSDK.dllpublic interface HCNetSDK extends StdCallLibrary {HCNetSDK INSTANCE = (HCNetSDK) Native.loadLibrary("E:\\DEMO_TEST\\JAVA_Demo\\JNA_TEST\\lib\\HCNetSDK.dll", HCNetSDK.class);// 动态库中结构体、接口描述}
这里是采用的是绝对路径,为防止项目工程路径的变化,可以改为获取动态路径。
public class XXDemo {static HCNetSDK hCNetSDK = null; //动态库加载,根据软件所属操作系统的工程文件目录动态获取库文件路径private static boolean createSDKInstance() { if (hCNetSDK == null) { synchronized (HCNetSDK.class) { String strDllPath = ""; try { if (osSelect.isWindows()) //win系统加载库路径 strDllPath = System.getProperty("user.dir") + "\\lib\\HCNetSDK.dll"; else if (osSelect.isLinux()) //Linux系统加载库路径 strDllPath = System.getProperty("user.dir") + "/lib/libhcnetsdk.so"; hCNetSDK = (HCNetSDK) Native.loadLibrary(strDllPath, HCNetSDK.class); } catch (Exception ex) { System.out.println("loadLibrary: " + strDllPath + " Error: " + ex.getMessage()); return false; } } } return true;}public static void main(String[] args) throws InterruptedException {//调用函数之前先加载动态链接库if (hCNetSDK == null) { if (!createSDKInstance()) { System.out.println("Load SDK fail"); return; }}}}
类似的,别的dll库文件的接口也是如此进行声明。
// 播放库函数声明,PlayCtrl.dllpublic interface PlayCtrl extends StdCallLibrary{ PlayCtrl INSTANCE = (PlayCtrl) Native.loadLibrary("E:\\DEMO_TEST\\JAVA_Demo\\JNA_TEST\\lib\\PlayCtrl.dll", PlayCtrl.class); // 播放库中结构体,接口描述}
(二)结构体、接口重定义
dll和so是C/C++语言函数的集合和容器,与Java中的接口概念吻合,所以JNA把dll文件和so文件看成一个个接口。
在JNA中定义一个接口就是相当于了定义一个DLL/SO文件的描述文件,该接口代表了动态链接库中发布的所有函数,对于程序不需要的函数,可以不在接口中声明。
例如,官方demo中的HCNetSDK的java接口声明了HCNetSDK.dll/so文件很多的接口。在实际开发中可以不需要声明这么多接口,只取某些需要的接口重新进行分组。
若在《设备网络SDK使用手册》查找功能时发现某些接口在demo中没有,则可以在HCNetSDK.h
头文件搜索对应的接口,然后在java中声明该接口的定义。
1.类型映射
接口中使用的函数必须与链接库中的函数原型保持一致,因为C/C++的类型与Java的类型是不一样的,动态库中的C/C++的数据类型必须转换成java对应类型,这就是类型映射(Type Mappings)。
默认的类型映射可以参考JNA官网的类型映射表。(国内镜像链接 https://gitee.com/mirrors/jna/)
2.结构体和类的转换
Java中没有结构体(struct)这种数据类型,JNA为我们提供了Structure这个类,只要继承该类,就可实现java结构体,相当于转换成java中的类。
例如,NET_DVR_USER_LOGIN_INFO结构体需要在HCNetSDK接口中进行重定义,转换方式如下:
//C++中NET_DVR_USER_LOGIN_INFO结构体定义typedef struct{ char sDeviceAddress[NET_DVR_DEV_ADDRESS_MAX_LEN]; BYTE byUseTransport; // 是否启用能力集透传,0--不启用透传,默认,1--启用透传 WORD wPort; char sUserName[NET_DVR_LOGIN_USERNAME_MAX_LEN]; char sPassword[NET_DVR_LOGIN_PASSWD_MAX_LEN]; fLoginResultCallBack cbLoginResult; void *pUser; BOOL bUseAsynLogin; BYTE byProxyType; // 0:不使用代理,1:使用标准代理,2:使用EHome代理 BYTE byUseUTCTime; // 0-不进行转换,默认,1-接口上输入输出全部使用UTC时间,SDK完成UTC时间与设备时区的转换,2-接口上输入输出全部使用平台本地时间,SDK完成平台本地时间与设备时区的转换 BYTE byLoginMode; // 0-Private 1-ISAPI 2-自适应 BYTE byHttps; // 0-不适用tls,1-使用tls 2-自适应 LONG iProxyID; // 代理服务器序号,添加代理服务器信息时,相对应的服务器数组下表值 BYTE byVerifyMode; // 认证方式,0-不认证,1-双向认证,2-单向认证;认证仅在使用TLS的时候生效; BYTE byRes3[119];}NET_DVR_USER_LOGIN_INFO,*LPNET_DVR_USER_LOGIN_INFO;// 宏定义#define NET_DVR_DEV_ADDRESS_MAX_LEN 129#define NET_DVR_LOGIN_USERNAME_MAX_LEN 64#define NET_DVR_LOGIN_PASSWD_MAX_LEN 64
注意转换之后,java类中的属性都为public,这意味着可以直接进行属性的修改和读取,而不需要进行get和set方法的声明和操作。
// SDK接口说明,HCNetSDK.dllpublic interface HCNetSDK extends StdCallLibrary { HCNetSDK INSTANCE = (HCNetSDK) Native.loadLibrary("E:\\DEMO_TEST\\JAVA_Demo\\JNA_TEST\\lib\\HCNetSDK.dll", HCNetSDK.class); // 动态库中结构体、接口描述 public static class NET_DVR_USER_LOGIN_INFO extends Structure{ public byte[] sDeviceAddress = new byte[NET_DVR_DEV_ADDRESS_MAX_LEN]; public byte byUseTransport; public short wPort; public byte[] sUserName = new byte[NET_DVR_LOGIN_USERNAME_MAX_LEN]; public byte[] sPassword = new byte[NET_DVR_LOGIN_PASSWD_MAX_LEN]; public FLoginResultCallback cbLoginResult; public Pointer pUser; public boolean bUseAsynLogin; public byte byProxyType; // 0:不使用代理,1:使用标准代理,2:使用EHome代理 public byte byUseUTCTime; // 0-不进行转换,默认,1-接口上输入输出全部使用UTC时间,SDK完成UTC时间与设备时区的转换,2-接口上输入输出全部使用平台本地时间,SDK完成平台本地时间与设备时区的转换 public byte byLoginMode; // 0-Private 1-ISAPI 2-自适应 public byte byHttps; // 0-不适用tls,1-使用tls 2-自适应 public int iProxyID; // 代理服务器序号,添加代理服务器信息时,相对应的服务器数组下表值 public byte byVerifyMode; // 认证方式,0-不认证,1-双向认证,2-单向认证;认证仅在使用TLS的时候生效; public byte[] byRes2 = new byte[119]; // 结构体中重写getFieldOrder方法,FieldOrder顺序要和结构体中定义的顺序保持一致 @Override protected List getFieldOrder(){ return Arrays.asList("sDeviceAddress","byUseTransport","wPort","sUserName","sPassword", "cbLoginResult","pUser","bUseAsynLogin","byProxyType","byUseUTCTime", "byLoginMode","byHttps","iProxyID","byVerifyMode","byRes2"); } } // 常量(宏)定义 public static final int NET_DVR_DEV_ADDRESS_MAX_LEN = 129; public static final int NET_DVR_LOGIN_USERNAME_MAX_LEN = 64; public static final int NET_DVR_LOGIN_PASSWD_MAX_LEN = 64; }
3.接口转换
在HCNetSDK接口中声明的方法要和开发包中HCNetSDK.h的头文件中声明的函数对应上,其中方法名、参数列表、返回值都要和HCNetSDK.h中的函数对应,HCNetSDK.h头文件的函数转换到java中声明,转换方式如下所示:
/******************************** SDK接口函数声明 *********************************/// 初始化SDK,调用其他SDK函数的前提NET_DVR_API BOOL __stdcall NET_DVR_Init(); // 启用日志文件写入接口NET_DVR_API BOOL __stdcall NET_DVR_SetLogToFile(DWORD nLogLevel ,char* strLogDir, BOOL bAutoDel); // 返回最后操作的错误码NET_DVR_API DWORD __stdcall NET_DVR_GetLastError(); // 释放SDK资源,在程序结束之前调用NET_DVR_API BOOL __stdcall NET_DVR_Cleanup(); // 登录接口NET_DVR_API LONG __stdcall NET_DVR_Login_V40( LPNET_DVR_USER_LOGIN_INFO pLoginInfo, LPNET_DVR_DEVICEINFO_V40 lpDeviceInfo ); // 用户注销NET_DVR_API BOOL __stdcall NET_DVR_Logout(LONG lUserID); // 回调函数声明,登录状态回调函数typedef void (CALLBACK *fLoginResultCallBack) ( LONG lUserID, DWORD dwResult, LPNET_DVR_DEVICEINFO_V30 lpDeviceInfo , void* pUser );
转换到java中时,注意类型转换
// SDK接口说明,HCNetSDK.dllpublic interface HCNetSDK extends StdCallLibrary {HCNetSDK INSTANCE = (HCNetSDK) Native.loadLibrary("E:\\DEMO_TEST\\JAVA_Demo\\JNA_TEST\\lib\\HCNetSDK.dll", HCNetSDK.class); /*** API函数声明 ***/ // 初始化SDK,调用其他SDK函数的前提 boolean NET_DVR_Init(); // 启用日志文件写入接口 boolean NET_DVR_SetLogToFile(int bLogEnable , String strLogDir, boolean bAutoDel); // 返回最后操作的错误码 int NET_DVR_GetLastError(); // 释放SDK资源,在程序结束之前调用 boolean NET_DVR_Cleanup(); // 登录接口 int NET_DVR_Login_V40(NET_DVR_USER_LOGIN_INFO pLoginInfo, NET_DVR_DEVICEINFO_V40 lpDeviceInfo); // 用户注销 boolean NET_DVR_Logout(int lUserID); // 回调函数申明 public static interface FLoginResultCallback extends StdCallCallback{ // 登录状态回调函数 public int invoke(int lUserID,int dwResult,NET_DVR_DEVICEINFO_V30 lpDeviceinfo,Pointer pUser); } }
4.方法调用
经过上述的操作,JNA工程已经创建完成,结构体和函数也在HCNetSDK
接口类中进行了转换,后续就可以在主类中实现调用。
以下为官方demo中的示例,以实现用户注册功能模块为例,解释了接口调用的流程。
public class jna_test { // 接口的实例,通过接口实例调用外部dll/so的函数 static HCNetSDK hCNetSDK = HCNetSDK.INSTANCE; // 用户登录返回句柄 static int lUserID; int iErr = 0; public static void main(String[] args) throws InterruptedException { jna_test test01 = new jna_test(); // 初始化 boolean initSuc = hCNetSDK.NET_DVR_Init(); if (initSuc != true) { System.out.println("初始化失败"); } // 打印SDK日志 hCNetSDK.NET_DVR_SetLogToFile(3, ".\\SDKLog\\", false); // 用户登陆操作 test01.Login_V40("192.168.1.64",(short)8000,"admin","test12345"); /* *实现SDK中其余功能模快 */ Thread.sleep(5000); //用户注销,释放SDK test01.Logout(); } /** * * @param m_sDeviceIP 设备ip地址 * @param wPort 端口号,设备网络SDK登录默认端口8000 * @param m_sUsername 用户名 * @param m_sPassword 密码 */ public void Login_V40(String m_sDeviceIP,short wPort,String m_sUsername,String m_sPassword) { /* 注册 */ // 设备登录信息 HCNetSDK.NET_DVR_USER_LOGIN_INFO m_strLoginInfo = new HCNetSDK.NET_DVR_USER_LOGIN_INFO(); // 设备信息 HCNetSDK.NET_DVR_DEVICEINFO_V40 m_strDeviceInfo = new HCNetSDK.NET_DVR_DEVICEINFO_V40(); m_strLoginInfo.sDeviceAddress = new byte[HCNetSDK.NET_DVR_DEV_ADDRESS_MAX_LEN]; System.arraycopy(m_sDeviceIP.getBytes(), 0, m_strLoginInfo.sDeviceAddress, 0, m_sDeviceIP.length()); m_strLoginInfo.wPort =wPort ; m_strLoginInfo.sUserName = new byte[HCNetSDK.NET_DVR_LOGIN_USERNAME_MAX_LEN]; System.arraycopy(m_sUsername.getBytes(), 0, m_strLoginInfo.sUserName, 0, m_sUsername.length()); m_strLoginInfo.sPassword = new byte[HCNetSDK.NET_DVR_LOGIN_PASSWD_MAX_LEN]; System.arraycopy(m_sPassword.getBytes(), 0, m_strLoginInfo.sPassword, 0, m_sPassword.length()); // 是否异步登录:false- 否,true- 是 m_strLoginInfo.bUseAsynLogin = false; // write()调用后数据才写入到内存中 m_strLoginInfo.write(); lUserID = hCNetSDK.NET_DVR_Login_V40(m_strLoginInfo, m_strDeviceInfo); if (lUserID == -1) { System.out.println("登录失败,错误码为" + hCNetSDK.NET_DVR_GetLastError()); return; } else { System.out.println("登录成功!"); // read()后,结构体中才有对应的数据 m_strDeviceInfo.read(); return; } } //设备注销 SDK释放 public void Logout() { if (lUserID>=0) { if (hCNetSDK.NET_DVR_Logout(lUserID) == false) { System.out.println("注销失败,错误码为" + hCNetSDK.NET_DVR_GetLastError()); } System.out.println("注销成功"); hCNetSDK.NET_DVR_Cleanup(); return; } else{ System.out.println("设备未登录"); hCNetSDK.NET_DVR_Cleanup(); return; } }}
我自己也写了个DEMO,放在gitee仓库了。可以提供参考:https://gitee.com/ZachLong/java-hik-camera