一、背景及注意点
项目需要根据时间段从海康监控中获取对应的视频,我只参考文档做了这一个功能,其他功能可自行参考官方代码进行修改,重点讲下如何整合及踩坑
二、步骤
2.1、官网
首先要去官网下载对应的开发包,一般都是window和linux的,都直接下载下来就行。https://open.hikvision.com/download/5cda567cf47ae80dd41a54b3?type=10
一般来说都是64位的,注意:这里是没有mac版本的
2.2、整合
2.2.1、jar包
首先将其中的jar包放到自己对应的resources目录下,有两个
然后pom中加上
<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中
<!--设置将本地jar包导出到项目最终的依赖库中--><includeSystemScope>true</includeSystemScope>
2.2.2、文件
我把window的文件都放在这里了,可自行随意放置
2.2.3、yaml配置文件
我自己加了几个配置参数
#监控一些资源的配置
video:monitor:# window中ffmpeg路径,linux不用管ffmpegpath:window: D:/workplaces/device-supervision/monitorvideo/ffmpeg.exe#监控需要加载的动态库,最后不用加斜杠load:window: D:/workplaces/jianshe/device-supervision/monitorvideolinux: /home/monitorvideo##监控连接相关配置
monitor:ip: # 这个默认就是8000,一般不用改port: 8000user: password:
2.2.4、对应的Configuration文件啥的
MonitorProperties 读取监控相关配置
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啥的
@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;/*** 播放库加载*/@Beanpublic 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;}/*** 动态库加载*/@Beanpublic 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;}@PreDestroypublic void destroy() {if (null != hCNetSDK) {log.info("释放hcNetSDK资源");hCNetSDK.NET_DVR_Cleanup();}}
}
这个类我也不太清楚干啥的,不过源码中有就拿出来了
@Slf4j
public class FExceptionCallBackException implements HCNetSDK.FExceptionCallBack {@Overridepublic void invoke(int dwType, int lUserID, int lHandle, Pointer pUser) {log.error("异常事件类型:"+dwType);}
}
用来登录的工具类
/*** 监控工具类*/
@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 让我单独抽取出来了
//播放库函数声明,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; /* 帧号 *///高版本方法覆盖@Overrideprotected List getFieldOrder() {return Arrays.asList("nWidth","nHeight","nStamp","nType","nFrameRate","dwFrameNum");}}}
2.25、对应的controller
HcVideoUtil 就是对应的源码中的VideoDemo,这个文件太大,就不粘贴了
@RestController
@RequestMapping("monitorvideo")
public class MonitorVideoController {@Autowiredprivate 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 \ 就是上面提到的环境变量
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 转换的,贴下工具类
@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);}}}