1 简介
由于在开发过程中,本来以为抓图项目会部署在Windows服务器上,但随着项目的进行发现项目需要部署在Linux系统,甚至是国产化平台银河麒麟上,但在部署时发现在国产化平台部署时出现缺包的问题无法解决,经过沟通把项目部署在Ubuntu 16.04服务器中。自然这就涉及到使用Java调用海康威视提供的设备网络SDK,即提供的.so文件来实现摄像机抓拍的功能。
1.1 .so文件
so(Shared Object)是Linux环境下的程序函数库,即编译好的可以供其它程序使用的代码和数据,和Windows系统中的.dll文件差不多。在Linux操作系统中,函数库非常重要,因为很多的软件之间都会互相使用彼此提供的函数库来进行特殊功能的运行,诸如身份验证,SSL函数库进行联机加密。
2 注意事项
2.1 下载
请移步到海康威视官网下载设备网络SDK_V6.0.2.35(for Linux64)
2.2 目录结构
在当前的Linux设备网络SDK解压之后的目录结构如上图所示,可以看到较为关键的为lib目录, LinuxJavaDemo目录中的HCNetSDK.java文件,以及readme.txt.
2.3 readme.txt
该文件是Linux调用设备网络SDK的引导文档,具体内容如下:
如果HCNetSDKCom目录以及libhcnetsdk.so、libhpr.so、libHCCore文件和可执行文件在同一级目录下,则使用同级目录下的库文件;
如果不在同一级目录下,则需要将以上文件的目录加载到动态库搜索路径中,设置的方式有以下几种:
一. 将网络SDK各动态库路径加入到LD_LIBRARY_PATH环境变量
1.在当前的终端黑窗口直接输入:export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/XXX:/XXX/HCNetSDKCom 只在当前终端起作用
2. 修改~/.bashrc或~/.bash_profile,最后一行添加 export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/XXX:/XXX/HCNetSDKCom,保存之后,使用source .bashrc执行该文件 ,当前用户生效
3. 修改/etc/profile,添加内容如第2条,同样保存之后使用source执行该文件 所有用户生效
二.在/etc/ld.so.conf文件结尾添加网络sdk库的路径,如/XXX和/XXX/HCNetSDKCom/,保存之后,然后执行ldconfig
三.可以将网络sdk各依赖库放入到/lib64或usr/lib64下
四.可以在Makefile中使用-Wl,-rpath来指定动态路径,但是需要将网络sdk各个动态库都用 –l方式显示加载进来
比如:-Wl,-rpath=/XXX:/XXX/HCNetSDKCom -lhcnetsdk -lhpr –lHCCore –lHCCoreDevCfg –lStreamTransClient –lSystemTransform –lHCPreview –lHCAlarm –lHCGeneralCfgMgr –lHCIndustry –lHCPlayBack –lHCVoiceTalk –lanalyzedata -lHCDisplay
推荐使用一或二的方式,但要注意优先使用的是同级目录下的库文件。
需要注意的是在将以上文件的目录加载到动态库搜索路径中时方案一种的三种方法是并列的,只要实现了一种即可,并不需要把三种方法全部执行。在抓图项目启动时,采用了修改/etc/profile配置文件,这样对各个用户均有效。配置的具体过程参见配置LD_LIBRARY_PATH
2.4 设备抓图
在项目中使用摄像机进行图片抓取使用的是设备网络SDK中的设备抓图来实现的:
参见设备网络SDK压缩包解压之后的开发文档目录下的设备网络SDK编程指南(IPC).pdf,在第五章函数说明有更加准确的介绍。
3 源码实现
3.1 配置LD_LIBRARY_PATH
3.1.1 依赖
首先需要准备好海康威视SDK, 。
可以把该tar包解压到指定目录,比如把tar包放在了/home/sqh目录下。并解压
[email protected]:/home/sqh# ls
lib lib.tar
[email protected]:/home/sqh#
3.1.2 环境变量
确保环境变量${LD_LIBRARY_PATH}目录具有包含上述的目录。
[email protected]-28-T630:/home/sqh# echo ${LD_LIBRARY_PATH}
/usr/local/cuda-9.0/lib64:/usr/local/mpich3/lib::/home/sqh/lib:/home/sqh/lib/HCNetSDKCom。
若不包含/home/sqh/lib和/home/sqh/lib/HCNetSDKCom
则需要配置环境变量。
使用root用户修改配置文件/etc/profile
[email protected]-28-T630:/home/sqh# vim /etc/profile
在尾部追加如下的内容:
export TOMCAT_HOME=/usr/local/tomcat/apache-tomcat-8.5.38
export CLASSPATH=.:$JAVA_HOME/lib:$TOMCAT_HOME/lib
export PATH=$PATH:$TOMCAT_HOME/bin
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/home/sqh/lib:/home/sqh/lib/HCNetSDKCom
添加内容的另外一部分为添加的TOMCAT配置环境变量。
使用root权限执行source目录
source /etc/profile
然后执行命令
[email protected]-28-T630:/home/sqh# echo ${LD_LIBRARY_PATH}
/usr/local/cuda-9.0/lib64:/usr/local/mpich3/lib::/home/sqh/lib:/home/sqh/lib/HCNetSDKCom。
表示环境变量配置成功
3.2 修改HCNetSDK.java
该JNA映射接口文件在LinuxJavaDemo中的src目录,HCNetSDK.java文件需要导入项目,笔者导入了包`
com.example.screenshot.sdk.
需要配置Native.loadLibrary(“hcnetsdk”,HCNetSDK.class),第一个参数,确保LD_LIBRARY_PATH已经正确配置并且生效,此时可以使用libhcnetsdk.so文件名中的hcnetsdk完成so文件的载入。
package com.example.screenshot.sdk;
import com.sun.jna.*;
import com.sun.jna.examples.win32.GDI32.RECT;
import com.sun.jna.examples.win32.W32API;
import com.sun.jna.examples.win32.W32API.HDC;
import com.sun.jna.examples.win32.W32API.HWND;
import com.sun.jna.ptr.ByteByReference;
import com.sun.jna.ptr.IntByReference;
import com.sun.jna.ptr.NativeLongByReference;
import com.sun.jna.ptr.ShortByReference;
//SDK接口说明,HCNetSDK.dll
public interface HCHetSDK extends Library {
//依稀记得”hcnetsdk”可以使用文件名的绝对路径来代替,即也可使用
///”ome/sqh/lib/libhcnetsdk.so”来完成HCNetSDK的实例化。
HCNetSDK INSTANCE = (HCNetSDK) Native.loadLibrary("hcnetsdk",
HCNetSDK.class);
/***宏定义***/
//常量
public static final int MAX_NAMELEN = 16; //DVR本地登陆名
public static final int MAX_RIGHT = 32; //设备支持的权限(1-12表示本地权限,13-32表示远程权限)
public static final int NAME_LEN = 32; //用户名长度
public static final int PASSWD_LEN = 16; //密码长度
public static final int SERIALNO_LEN = 48; //***长度
3.2.1 Native.loadLibrary
在HCNetSDK.java文件中定义的HCNetSDK接口继承自Library。如果动态链接库里的函数是以StdCallLibrary,比如kernel32库,则继承StdCallLibrary,默认继承Library。
3.2.2 接口内部定义
接口内部需要一个公共的静态常量:Instance,通过这个常量,就可以获得这个接口的实例,从而使用该接口的方法,也就是调用外部dll/so的函数。
HCNetSDK INSTANCE = (HCNetSDK) Native.loadLibrary("hcnetsdk",HCNetSDK.class);
第一个参数是动态链接库dll/so的名称,但不带.dll或者.so这样的后缀。这符合JNI的规范,因为带了后缀名就不可以跨操作系统平台了。搜索动态链接库的路径顺序是:先从当前类的当前文件夹找,如果没有找到,再在工程当前文件夹下找win32/win64文件夹,找到后搜索对应的dll文件,如果找不到再到WINDOWS下去搜索,再找不到就报错
java.lang.UnsatisfiedLinkError: Unable to load library
第二个参数则是真实的类型,没什么解释的,不再赘述。
注意:so文件的命名方式为lib**.so,但是在加载.so文件的时候是不需要lib前缀的。
3.2.3 暂未验证的方式
在查询某些博客时发现了如下的解释,但没有验证(参见JNA路径问题–Native.loadLibrary(“NLPIR”, CLibrary.class))
Native.loadLibrary(“NLPIR”, CLibrary.class)会自动去项目的src/main/resources文件夹下寻找系统对应的dll和so。
(根据com.sun.jna.Platform类对应文件夹名)
3.3 编码实现定时抓图
由于iSC平台并未提供直接对某个摄像头进行抓图的功能接口,因此需要通过Java调用so动态链接库,直接操纵设备进行抓图。
以设备连接为例,程序工作的流程图如下:
设备断开连接类似,不再赘述。
3.3.1 Screenshot.java Controller
该文件负责接收请求,当进行设备连接时,则相应设备开始抓图,断开连接时,取消抓图。
package com.example.screenshot.controller;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.example.screenshot.common.CommonReturn;
import com.example.screenshot.model.Device;
import com.example.screenshot.sdk.HCNetSDK;
import com.example.screenshot.service.AsyncTask;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.*;
/**
* 抓图
*
* @Owner: SongQuanHeng
* @Time: 2019/3/14-14:44
* @Version:
* @Change:
*/
@RestController
public class ScreenShot {
private final static Logger logger = LoggerFactory.getLogger(ScreenShot.class);
private Map<String, Device> devices = new HashMap<>();
// 在该类中直接引用了HCNetSDK的静态变量INSTANCE,供自己使用
static HCNetSDK hCNetSDK = HCNetSDK.INSTANCE;
private final List<String> validKeys = new ArrayList<>(Arrays.asList("ip"));
@Autowired
private AsyncTask asyncTask;
@RequestMapping(value = "/connect", produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
public String connect(@RequestBody JSONObject param) throws Exception {
logger.debug("Enter connect");
logger.debug("hCNetSDK: "+hCNetSDK);
if (!hasValidKey(param)) {
return CommonReturn.httpReturnFailure("参数有误, 请传入监控点IP");
}
if (devices.isEmpty()) {
boolean initSucc = hCNetSDK.NET_DVR_Init();
if (!initSucc) {
return CommonReturn.httpReturnFailure("SDK初始化失败");
}
logger.debug("完成SDK初始化");
}
String ip = getIp(param);
if (devices.containsKey(ip)) {
return CommonReturn.httpReturnFailure("传入的设备ip已经连接");
}
logger.debug("ip: "+ip);
Device device = new Device(ip);
boolean connSucc = device.connect();
if (!connSucc) {
return CommonReturn.httpReturnFailure("设备链接失败");
}
device.setConnected(true);
devices.put(ip, device);
logger.debug("当前连接设备一共有: "+ devices.size());
asyncTask.executeAsyncTask(device);
return CommonReturn.httpReturnSuccess("设备连接成功");
}
private String getIp(JSONObject param) {
return param.getString("ip").trim();
}
@RequestMapping(value = "/disconnect", produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
public String disconnect(@RequestBody JSONObject param) {
if (!hasValidKey(param)) {
return CommonReturn.httpReturnFailure("参数有误, 请传入监控点IP");
}
String ip = getIp(param);
if (!devices.containsKey(ip)) {
return CommonReturn.httpReturnFailure("传入的IP未连接, 请检查参数");
}
Device device = devices.get(ip);
boolean operSucc = device.disconnect();
if (!operSucc) {
return CommonReturn.httpReturnFailure("断开连接失败");
}
devices.remove(ip);
if (devices.isEmpty()) {
boolean cleanUpSucc = hCNetSDK.NET_DVR_Cleanup();
if (!cleanUpSucc) {
printErrInfo("disconnect", "NET_DVR_Cleanup");
return CommonReturn.httpReturnFailure("SDK反初始化失败");
}
}
return CommonReturn.httpReturnSuccess("断开连接,"+"设备IP" + ip+"抓拍结束");
}
@RequestMapping(value = "/getConnectedDevices", produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
public String getDevices() {
logger.debug("Enter getDevices");
JSONObject result = new JSONObject();
result.put("size", devices.size());
JSONArray ips = new JSONArray();
for (String key: devices.keySet()) {
ips.add(key);
}
result.put("ips", ips);
return CommonReturn.httpReturnSuccess("获取在线设备成功", result);
}
private boolean hasValidKey(JSONObject param) {
for (String key : validKeys) {
if (!param.containsKey(key)){
return false;
}
}
return true;
}
public void printErrInfo(String inFunc, String afterFunc) {
int errCode = hCNetSDK.NET_DVR_GetLastError();
logger.debug("In "+inFunc," after Exec "+ afterFunc + " , errCode: "+ errCode);
}
public int getLastErrCode() {
logger.debug("hcNetSDK: " + hCNetSDK);
return hCNetSDK.NET_DVR_GetLastError();
}
}
3.3.2 AsyncTask异步任务执行类
该类型主要是负责异步抓图,并保存到指定目录,然后把映射的图片资源以HTTP请求的方式告诉第三方。
package com.example.screenshot.service;
import com.alibaba.fastjson.JSONObject;
import com.example.screenshot.config.ServerConfig;
import com.example.screenshot.model.Device;
import com.example.screenshot.sdk.HCNetSDK;
import com.example.screenshot.util.GeneralUtil;
import com.sun.jna.NativeLong;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import java.io.File;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.UUID;
/**
* 异步任务执行类
*
* @Owner: SongQuanHeng
* @Time: 2019/3/14-17:02
* @Version:
* @Change:
*/
@Service
public class AsyncTask {
static HCNetSDK hCNetSDK = HCNetSDK.INSTANCE;
private static String relativeDir = File.separator+"Img"+File.separator;
private static String imageLocation;
static {
String dir = GeneralUtil.getRootDir();
File file = new File(dir);
// 用来放置抓图的路径,并且把路径映射成了静态资源
imageLocation = file.getParentFile().getParentFile().getParentFile().getPath()+relativeDir;
}
@Value("${notification.url}")
private String notificationUrl;
@Autowired
private ServerConfig serverConfig;
private final Logger logger = LoggerFactory.getLogger(this.getClass());
private final DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss");
public static String getImageLocation() {
return imageLocation;
}
public static String getRelativeDir() {
return relativeDir;
}
@Async
public void executeAsyncTask(Device device) throws Exception {
logger.info("Enter executeAsyncTask");
HCNetSDK.NET_DVR_JPEGPARA jpegpara = new HCNetSDK.NET_DVR_JPEGPARA();
logger.info(Thread.currentThread().getName());
while (device.isConnected()) {
String fileName = getImgName();
logger.debug("produce Pic: " + fileName);
logger.debug("absolute dir: " + (getImageLocation()+fileName));
boolean operSucc = hCNetSDK.NET_DVR_CaptureJPEGPicture(device.getUserID(), new NativeLong(1), jpegpara, getImageLocation()+fileName);
if (!operSucc) {
printErrInfo("executeAsyncTask", "NET_DVR_CaptureJPEGPicture");
}
JSONObject message = getMessage(device, fileName);
logger.debug(message.toJSONString());
GeneralUtil.postJson(notificationUrl, message.toJSONString());
Thread.sleep(1000*4);
}
logger.info("Leave executeAsyncTask");
}
private JSONObject getMessage(Device device, String fileName) {
JSONObject message = new JSONObject();
message.fluentPut("imgUrl", serverConfig.getUrl() + getRelativeDir()+fileName);
message.put("ip", device.getIp());
message.put("produceTime", dateFormat.format(new Date()));
return message;
}
private String getImgName() {
return UUID.randomUUID().toString()+".jpg";
}
public void printErrInfo(String inFunc, String afterFunc) {
int errCode = hCNetSDK.NET_DVR_GetLastError();
logger.debug("In "+inFunc+" after Exec "+ afterFunc + " , errCode: "+ errCode);
}
public int getLastErrCode() {
logger.debug("hcNetSDK: " + hCNetSDK);
return hCNetSDK.NET_DVR_GetLastError();
}
}
3.3.3 Device Model
Device类用来为摄像头设备进行建模,其中包含主要的行为包括设备连接、设备断开连接。该类的实现如下:
package com.example.screenshot.model;
import com.example.screenshot.sdk.HCNetSDK;
import com.sun.jna.NativeLong;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* 对设备进行建模
*
* @Owner:
* @Time: 19-3-22-下午9:11
*/
public class Device {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
private String ip;
private NativeLong userID = new NativeLong(-1);
private short port = Short.parseShort("8000");
private String userName = "admin";
private String password = "hik12345+";
public NativeLong getUserID() {
return userID;
}
public boolean isConnected() {
return isConnected;
}
private boolean isConnected;
private static HCNetSDK hCNetSDK = HCNetSDK.INSTANCE;
private HCNetSDK.NET_DVR_DEVICEINFO_V30 deviceInfo;
public void setIp(String ip) {
this.ip = ip;
}
public Device(String ip) {
this.ip = ip;
setConnected(false);
}
public String getIp() {
return ip;
}
public void setConnected(boolean connected) {
isConnected = connected;
}
public void resetUserID() {
userID = new NativeLong(-1);
return ;
}
public boolean connect() {
logger.debug("Enter connect");
logger.debug("in connect: "+hCNetSDK);
userID = hCNetSDK.NET_DVR_Login_V30(ip, port, userName, password, deviceInfo);
int errCode = getLastErrCode();
logger.debug("After connect errCode: "+ errCode);
if (errCode!=0) {
setConnected(false);
return false;
} else {
setConnected(true);
return true;
}
}
public boolean disconnect() {
logger.debug("Enter disconnect");
boolean operSucc = hCNetSDK.NET_DVR_Logout(userID);
if (!operSucc) {
printErrInfo("disconnect", "NET_DVR_Logout");
}
resetUserID();
setConnected(false);
return operSucc;
}
public void printErrInfo(String inFunc, String afterFunc) {
int errCode = hCNetSDK.NET_DVR_GetLastError();
logger.debug("In "+inFunc," after Exec "+ afterFunc + " , errCode: "+ errCode);
}
public int getLastErrCode() {
logger.debug("hcNetSDK: " + hCNetSDK);
return hCNetSDK.NET_DVR_GetLastError();
}
}
3.3.4 WebMvcConfiger 将图片映射成静态资源实现类
package com.example.screenshot.config;
import com.example.screenshot.service.AsyncTask;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
import java.io.File;
/**
* 配置静态资源映射,把图片映射成HTTP资源,该类执行之后,则抓取的图片可以通过
* http://ip:port/Img/imgName在浏览器中访问
* @Owner:
* @Time: 19-3-21-下午10:44
*/
@Configuration
public class WebMvcConfigurer extends WebMvcConfigurerAdapter {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
logger.debug("Enter addResourceHandlers");
logger.debug("imageLocation: "+AsyncTask.getImageLocation());
File file = new File(AsyncTask.getImageLocation());
if (!file.exists()) {
logger.debug("Before mkdirs");
boolean mkFlag = file.mkdirs();
logger.debug("mkFalg: "+mkFlag);
} else {
logger.debug(AsyncTask.getImageLocation() + " has existed");
}
registry.addResourceHandler("/Img/**").addResourceLocations("file:"+AsyncTask.getImageLocation());
logger.debug("Leave addResourceHandlers");
}
}
通过这种方式可以把抓取的文件映射成静态HTTP资源,在供其他人进行算法分析时,只要通过httpclient发送post请求,然后把图片的url地址传给对方,这样对方即可以通过该url把图片拉取到本地进而继续进行算法分析的程序了,这不在本文的范围之内,不再赘述了。
4 总结
文档主要是介绍了在Linux环境下使用Java调用so文件的具体过程,其中演示环节使用了摄像机的抓拍能力,整体的流程是通过Spring Boot接收REST请求,在设备连接时开启图像抓拍,然后抓拍会保存一系列的图片到本地的目录,然后把该目录映射成HTTP资源供需要使用图片的人进行使用。
尤其需要注意的是,算法详细讲述了海康威视设备网络SDK的用法,并以摄像机抓拍为功能实现的基础阐述了设备网络SDK的目录结构,导入项目的步骤,JNA整合的详细过程。希望能辅助有相同开发需求场景。
5 参考
Windows下使用Java调用Hikvision设备网络SDK的使用指南