springboot整合海康监控完整步骤及踩坑

一、背景及注意点

项目需要根据时间段从海康监控中获取对应的视频,我只参考文档做了这一个功能,其他功能可自行参考官方代码进行修改,重点讲下如何整合及踩坑

二、步骤

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);
        }
    }


}
相关推荐
a程序小傲几秒前
京东Java面试被问:动态规划的状态压缩和优化技巧
java·开发语言·mysql·算法·adb·postgresql·深度优先
仙俊红2 分钟前
spring的IoC(控制反转)面试题
java·后端·spring
阿湯哥3 分钟前
AgentScope Java 集成 Spring AI Alibaba Workflow 完整指南
java·人工智能·spring
CheungChunChiu13 分钟前
Linux 内核动态打印机制详解
android·linux·服务器·前端·ubuntu
小楼v14 分钟前
说说常见的限流算法及如何使用Redisson实现多机限流
java·后端·redisson·限流算法
与遨游于天地26 分钟前
NIO的三个组件解决三个问题
java·后端·nio
czlczl200209251 小时前
Guava Cache 原理与实战
java·后端·spring
yangminlei1 小时前
Spring 事务探秘:核心机制与应用场景解析
java·spring boot
BlueBirdssh2 小时前
linux 内核通过 dts 设备树 配置pcie 控制器 各种参数和中断等, 那freeRTOS 是通过直接设置PCIe寄存器吗
linux
Yuer20252 小时前
什么是 Rust 语境下的“量化算子”——一个工程对象的最小定义
开发语言·后端·rust·edca os·可控ai