一、背景及注意点
项目需要根据时间段从海康监控中获取对应的视频,我只参考文档做了这一个功能,其他功能可自行参考官方代码进行修改,重点讲下如何整合及踩坑
二、步骤
2.1、官网
首先要去官网下载对应的开发包,一般都是window和linux的,都直接下载下来就行。https://open.hikvision.com/download/5cda567cf47ae80dd41a54b3?type=10
一般来说都是64位的,注意:这里是没有mac版本的
2.2、整合
2.2.1、jar包
首先将其中的jar包放到自己对应的resources目录下,有两个
然后pom中加上
xml
<dependency>
<groupId>examples</groupId>
<artifactId>examples</artifactId>
<version>1.0</version>
<scope>system</scope>
<systemPath>${basedir}/src/main/resources/lib/examples.jar</systemPath>
</dependency>
<dependency>
<groupId>jna</groupId>
<artifactId>jna</artifactId>
<version>1.0</version>
<scope>system</scope>
<systemPath>${basedir}/src/main/resources/lib/jna.jar</systemPath>
</dependency>
很重要的一点,打包插件一定要加上这个,否则jar包只会在resources中
xml
<!--设置将本地jar包导出到项目最终的依赖库中-->
<includeSystemScope>true</includeSystemScope>
2.2.2、文件
我把window的文件都放在这里了,可自行随意放置
2.2.3、yaml配置文件
我自己加了几个配置参数
yaml
#监控一些资源的配置
video:
monitor:
# window中ffmpeg路径,linux不用管
ffmpegpath:
window: D:/workplaces/device-supervision/monitorvideo/ffmpeg.exe
#监控需要加载的动态库,最后不用加斜杠
load:
window: D:/workplaces/jianshe/device-supervision/monitorvideo
linux: /home/monitorvideo
##监控连接相关配置
monitor:
ip:
# 这个默认就是8000,一般不用改
port: 8000
user:
password:
2.2.4、对应的Configuration文件啥的
MonitorProperties 读取监控相关配置
java
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
/**
* 读取监控相关配置
*/
@Component
@ConfigurationProperties(prefix = "monitor")
@Data
public class MonitorProperties {
/**
* 监控ip地址
*/
private String ip;
/**
* 监控端口
*/
private short port;
/**
* 用户名
*/
private String user;
/**
* 用户密码
*/
private String password;
}
HikVisionConfiguration 用来初始化加载海康的一些类,这些东西给的实例代码中都有的,像OsSelectUtil,FExceptionCallBackException啥的
java
@Configuration
@Slf4j
public class HikVisionConfiguration {
private HCNetSDK hCNetSDK = null;
private FExceptionCallBackException fExceptionCallBack;
@Value("${video.monitor.load.window}")
private String windowPath;
@Value("${video.monitor.load.linux}")
private String linuxPath;
/**
* 播放库加载
*/
@Bean
public PlayCtrl createPlayInstance() {
PlayCtrl playControl;
synchronized (PlayCtrl.class) {
String strPlayPath = "";
try {
if (OsSelectUtil.isWindows()) {
//win系统加载库路径
strPlayPath = windowPath + "\\lib\\HCNetSDK.dll";
} else if (OsSelectUtil.isLinux()) {
//Linux系统加载库路径
strPlayPath = linuxPath + "/lib/libPlayCtrl.so";
}
playControl = (PlayCtrl) Native.loadLibrary(strPlayPath, PlayCtrl.class);
} catch (Exception ex) {
log.error("加载失败,loadLibrary: {}", strPlayPath);
log.error(ex.getMessage(), ex);
return null;
}
}
log.info("加载PlayCtrl成功");
return playControl;
}
/**
* 动态库加载
*/
@Bean
public HCNetSDK init() {
if (hCNetSDK == null) {
synchronized (HCNetSDK.class) {
String strDllPath = "";
try {
//win系统加载库路径
if (OsSelectUtil.isWindows()) {
strDllPath = windowPath + "\\lib\\HCNetSDK.dll";
}
//linux系统建议调用以下接口加载组件库
else if (OsSelectUtil.isLinux()) {
strDllPath = linuxPath + "/lib/libhcnetsdk.so";
}
hCNetSDK = (HCNetSDK) Native.loadLibrary(strDllPath, HCNetSDK.class);
//linux系统建议调用以下接口加载组件库
if (OsSelectUtil.isLinux()) {
HCNetSDK.BYTE_ARRAY ptrByteArray1 = new HCNetSDK.BYTE_ARRAY(256);
HCNetSDK.BYTE_ARRAY ptrByteArray2 = new HCNetSDK.BYTE_ARRAY(256);
//这里是库的绝对路径,请根据实际情况修改,注意改路径必须有访问权限
String strPath1 = linuxPath + "/lib/libcrypto.so.1.1";
String strPath2 = linuxPath + "/lib/libssl.so.1.1";
log.info("strpaht1:{}",strPath1);
log.info("strpaht2:{}",strPath2);
System.arraycopy(strPath1.getBytes(), 0, ptrByteArray1.byValue, 0, strPath1.length());
ptrByteArray1.write();
hCNetSDK.NET_DVR_SetSDKInitCfg(HCNetSDK.NET_SDK_INIT_CFG_LIBEAY_PATH, ptrByteArray1.getPointer());
System.arraycopy(strPath2.getBytes(), 0, ptrByteArray2.byValue, 0, strPath2.length());
ptrByteArray2.write();
hCNetSDK.NET_DVR_SetSDKInitCfg(HCNetSDK.NET_SDK_INIT_CFG_SSLEAY_PATH, ptrByteArray2.getPointer());
String strPathCom = linuxPath + "/lib/";
HCNetSDK.NET_DVR_LOCAL_SDK_PATH struComPath = new HCNetSDK.NET_DVR_LOCAL_SDK_PATH();
System.arraycopy(strPathCom.getBytes(), 0, struComPath.sPath, 0, strPathCom.length());
struComPath.write();
hCNetSDK.NET_DVR_SetSDKInitCfg(HCNetSDK.NET_SDK_INIT_CFG_SDK_PATH, struComPath.getPointer());
}
} catch (Exception ex) {
log.error("加载失败,loadLibrary: {}" ,strDllPath);
log.error(ex.getMessage(), ex);
return null;
}
}
}
//SDK初始化,一个程序只需要调用一次
hCNetSDK.NET_DVR_Init();
//异常消息回调
if (fExceptionCallBack == null) {
fExceptionCallBack = new FExceptionCallBackException();
}
Pointer pUser = null;
if (!hCNetSDK.NET_DVR_SetExceptionCallBack_V30(0, 0, fExceptionCallBack, pUser)) {
log.error("设置异常消息回调失败");
return null;
}
//启动SDK写日志
hCNetSDK.NET_DVR_SetLogToFile(3, "./sdkLog", false);
log.info("加载hksdk成功");
return hCNetSDK;
}
@PreDestroy
public void destroy() {
if (null != hCNetSDK) {
log.info("释放hcNetSDK资源");
hCNetSDK.NET_DVR_Cleanup();
}
}
}
这个类我也不太清楚干啥的,不过源码中有就拿出来了
java
@Slf4j
public class FExceptionCallBackException implements HCNetSDK.FExceptionCallBack {
@Override
public void invoke(int dwType, int lUserID, int lHandle, Pointer pUser) {
log.error("异常事件类型:"+dwType);
}
}
用来登录的工具类
java
/**
* 监控工具类
*/
@Slf4j
public class MonitorUtil {
private static int userId = -1;
/**
* 登录设备,支持 V40 和 V30 版本,功能一致。
*
* @return 登录成功返回用户ID,失败返回-1
*/
public static int loginDevice() {
if (userId != -1) {
return userId;
}
MonitorProperties bean = SpringUtils.getBean(MonitorProperties.class);
HCNetSDK hCNetSDK = SpringUtils.getBean(HCNetSDK.class);
String ip = bean.getIp();
short port = bean.getPort();
String user = bean.getUser();
String psw = bean.getPassword();
// 创建设备登录信息和设备信息对象
HCNetSDK.NET_DVR_USER_LOGIN_INFO loginInfo = new HCNetSDK.NET_DVR_USER_LOGIN_INFO();
HCNetSDK.NET_DVR_DEVICEINFO_V40 deviceInfo = new HCNetSDK.NET_DVR_DEVICEINFO_V40();
// 设置设备IP地址
byte[] deviceAddress = new byte[HCNetSDK.NET_DVR_DEV_ADDRESS_MAX_LEN];
byte[] ipBytes = ip.getBytes();
System.arraycopy(ipBytes, 0, deviceAddress, 0, Math.min(ipBytes.length, deviceAddress.length));
loginInfo.sDeviceAddress = deviceAddress;
// 设置用户名和密码
byte[] userName = new byte[HCNetSDK.NET_DVR_LOGIN_USERNAME_MAX_LEN];
byte[] password = psw.getBytes();
System.arraycopy(user.getBytes(), 0, userName, 0, Math.min(user.length(), userName.length));
System.arraycopy(password, 0, loginInfo.sPassword, 0, Math.min(password.length, loginInfo.sPassword.length));
loginInfo.sUserName = userName;
// 设置端口和登录模式
loginInfo.wPort = port;
loginInfo.bUseAsynLogin = false; // 同步登录
loginInfo.byLoginMode = 0; // 使用SDK私有协议
if (userId == -1) {
synchronized (MonitorUtil.class) {
// 执行登录操作
userId = hCNetSDK.NET_DVR_Login_V40(loginInfo, deviceInfo);
if (userId == -1) {
log.error("登录失败,错误码为: {}", hCNetSDK.NET_DVR_GetLastError());
} else {
log.info("{}设备登录成功! ", ip);
// 处理通道号逻辑
int startDChan = deviceInfo.struDeviceV30.byStartDChan;
log.info("预览起始通道号: {}", startDChan);
}
}
}
return userId; // 返回登录结果
}
}
PlayCtrl 让我单独抽取出来了
java
//播放库函数声明,PlayCtrl.dll
public interface PlayCtrl extends Library {
public static final int STREAME_REALTIME = 0;
public static final int STREAME_FILE = 1;
boolean PlayM4_GetPort(IntByReference nPort);
boolean PlayM4_OpenStream(int nPort, Pointer pFileHeadBuf, int nSize, int nBufPoolSize);
boolean PlayM4_InputData(int nPort, Pointer pBuf, int nSize);
boolean PlayM4_CloseStream(int nPort);
boolean PlayM4_SetStreamOpenMode(int nPort, int nMode);
boolean PlayM4_Play(int nPort, W32API.HWND hWnd);
boolean PlayM4_Stop(int nPort);
boolean PlayM4_SetSecretKey(int nPort, int lKeyType, String pSecretKey, int lKeyLen);
boolean PlayM4_GetPictureSize(int nPort, IntByReference pWidth, IntByReference pHeight);
boolean PlayM4_GetJPEG(int nPort, Pointer pBitmap, int nBufSize, IntByReference pBmpSize);
int PlayM4_GetLastError(int nPort);
boolean PlayM4_SetDecCallBackExMend(int nPort, DecCallBack decCBFun, Pointer pDest, int nDestSize, int nUser);
public static interface DecCallBack extends Callback {
void invoke(int nPort, Pointer pBuf, int nSize, FRAME_INFO pFrameInfo, int nReserved1, int nReserved2);
}
public class FRAME_INFO extends Structure {
public int nWidth; /* 画面宽,单位像素。如果是音频数据,则为音频声道数 */
public int nHeight; /* 画面高,单位像素。如果是音频数据,则为样位率 */
public int nStamp; /* 时标信息,单位毫秒 */
public int nType; /* 数据类型,T_AUDIO16, T_RGB32, T_YV12 */
public int nFrameRate; /* 编码时产生的图像帧率,如果是音频数据则为采样率 */
public int dwFrameNum; /* 帧号 */
//高版本方法覆盖
@Override
protected List getFieldOrder() {
return Arrays.asList("nWidth","nHeight","nStamp","nType","nFrameRate","dwFrameNum");
}
}
}
2.25、对应的controller
HcVideoUtil 就是对应的源码中的VideoDemo,这个文件太大,就不粘贴了
java
@RestController
@RequestMapping("monitorvideo")
public class MonitorVideoController {
@Autowired
private HcVideoUtil hcVideoUtil;
@PostMapping("getvideo")
public void getVideo(String channel, Date startTime, Date endTime, String videoFile) {
int userId = MonitorUtil.loginDevice();
hcVideoUtil.dowmloadRecordByTime(userId, Integer.parseInt(channel), startTime, endTime, videoFile);
}
}
大体上就这些文件,额外需要自己在改改就能跑了,接下来就要说踩坑了,这才是重点(linux部署也放在踩坑里,这个是踩坑最多的)
三、踩坑
3.1、 Structure.getFieldOrder() 错误解决办法
可参考这篇文章,https://www.cnblogs.com/easyidea/p/16490708.html
使用他的工具类需要注意,就像博主说的,有些情况并没有处理,所以有的生成的getFieldOrder里面字段是多的,具体什么情况多的有些忘了,距离完成写这篇文章差不多过了半个月。。。
当然,要是没有用其他的jna版本,这个问题是不会出现的
3.2、linux环境部署
首先将对应的文件上传到响应的文件目录下,对应yaml中的文件路径
启动应该会报找不到一些配置文件,此时我们就参考官方文档
于是,我们就去找这个东西如何添加到变量中
参考这个
https://blog.csdn.net/x_y_csdn/article/details/110521330
完事后执行下这个命令 ldconfig
此时启动应该就不报错了
3.3、docker环境部署
首先说下,以jdk作为基础镜像是很难搞的 ,因为你不知道到底还缺少了多少动态库,所以建议直接以linux作为基础镜像 ,贴下我的dockerfile文件
LD_LIBRARY_PATH=/home/ypx/monitorvideo/lib:$LD_LIBRARY_PATH \ 就是上面提到的环境变量
dockerfile
FROM docker.m.daocloud.io/library/ubuntu:20.04
# 设置工作目录和临时卷
WORKDIR /app
VOLUME /tmp
# 更新apk包索引并安装ffmpeg
RUN apt-get update && \
apt-get install -y ffmpeg && \
apt-get install -y openjdk-8-jdk
# 复制本地编译好的 jar 文件(从target目录)
COPY device-supervision.jar app.jar
# 设置时区为中国时区
ENV TZ=Asia/Shanghai \
LD_LIBRARY_PATH=/home/ypx/monitorvideo/lib:$LD_LIBRARY_PATH \
#设置中文不乱码的
LANG=C.UTF-8 \
LC_ALL=C.UTF-8
# 暴露端口
EXPOSE 18881
# 设置 Java 运行参数
ENTRYPOINT ["java", \
"-Djava.security.egd=file:/dev/./urandom", \
"-XX:+UseContainerSupport", \
"-XX:MaxRAMPercentage=75.0", \
"-jar", \
"/app/app.jar"]
启动命令
docker run -d -p 18881:18881 --privileged=true -v /home/ypx/monitorvideo/:/home/ypx/monitorvideo/ --restart always --name device device:0.0.1
3.4、下载下来的视频可以本地播放,网页上放不了
https://blog.csdn.net/xcg340123/article/details/139825982
简单来说就是网页不支持这种海康格式的视频文件,我是用ffmpeg 转换的,贴下工具类
java
@Slf4j
public class VideoUtil {
/**
* 海康下载来的视频无法直接在浏览器播放,需要进行转码
*
* @param oldPath 旧的地址
* @param newPath 新的地址
*/
public static void convert(String oldPath, String newPath, String ffmpegPath) {
try {
//需要区分是否为linux环境
if (OsSelectUtil.isLinux()) {
ffmpegPath = "ffmpeg";
}
List<String> command = new ArrayList<String>();
command.add(ffmpegPath);
command.add("-i");
command.add(oldPath);
command.add("-c");
command.add("copy");
command.add("-an");
command.add(newPath);
ProcessBuilder builder = new ProcessBuilder(command);
Process process = builder.start();
//等待转换完成
process.waitFor();
log.info("转换视频{}完成!", newPath);
//需要删除源文件
FileUtil.del(oldPath);
} catch (Exception e) {
log.error("转换视频出错!");
log.error(e.getMessage(), e);
}
}
}