SpringBoot整合海康網路裝置SDK

2023-03-24 18:00:41

SDK介紹

概述

裝置網路SDK是基於裝置私有網路通訊協定開發的,為嵌入式網路硬碟錄影機、NVR、網路攝像機、網路球機、視訊伺服器、解碼器、報警主機、網路儲存等產品服務的配套模組,用於遠端存取和控制裝置軟體的二次開發。

功能

影象預覽, 檔案回放和下載, 雲臺控制, 佈防/撤防, 語音對講, 紀錄檔管理, 解碼卡, 遠端升級, 遠端重啟/關閉, 格式化硬碟, 引數設定(系統設定, 通道設定, 串列埠設定, 報警設定, 使用者設定), 多路解碼器, 智慧裝置功能和獲取裝置能力集等。

下載

https://open.hikvision.com/download/5cda567cf47ae80dd41a54b3?type=10

對接指南

以java為例

由於我司提供的裝置網路SDK是封裝的動態連結庫(Windows的dll或者Linux的so),各種開發語言對接SDK,都是通過載入動態庫連結,呼叫動態庫中的介面實現功能模組對接,因此,裝置網路SDK的對接不區分開發語言,而且對接的流程和對應的介面都是通用的,各種語言呼叫動態庫的方式有所不同。本文重點介紹java開發語言如何對接裝置網路SDK。目前我司提供的java語言開發的demo是通過JNA的方式呼叫動態連結庫中的介面,JNA(Java Native Access)框架是SUN公司主導開發的開源java框架,是建立在JNI的基礎上的一個框架,JNA框架提供了一組java工具類用於在執行期間動態存取動態連結庫(native library:如Window的dll、Linux的so),實現在java語言中呼叫C/C++語言封裝的介面,java開發人員只需要在一個java介面中描述目標native library的函數與結構,JNA將自動實現Java介面到native function的對映,而不需要編寫任何Native/JNI程式碼,大大降低了Java呼叫動態連結庫的開發難度。相比於JNI的方式,JNA大大簡化了呼叫本地方法的過程,使用很方便,基本上不需要脫離Java環境就可以完成。JNA呼叫C/C++的過程大致如下:

整合

SpringBoot 專案為例,海康SDK版本為6.1.9.47,JNA版本為3.0.9,在windows環境使用Intellij IDEA 2022.2.3開發

初始化專案

  • 新建 SpringBoot 專案,版本 2.5.3
  • 新增pom依賴:jna,fastjson2

<dependency>
        <groupId>com.sun.jna</groupId>
        <artifactId>jna</artifactId>
        <version>3.0.9</version>
</dependency>


<dependency>
        <groupId>com.alibaba.fastjson2</groupId>
        <artifactId>fastjson2</artifactId>
        <version>2.0.20</version>
</dependency>          


  • 將下載下來的海康sdk放到專案目錄,並根據作業系統不同分別建立相應的資料夾
  • 若出現 HCNetSDK.NET_DVR_PREVIEWINFO.HWND找不到參照,可做如下處理:1:在ProjectStructure中引入官方範例程式碼中的 examples.jar ;2:將 HWND 型別修改為 int

  • 設定一個喜歡的埠 server.port

初始化SDK

初始化SDK概述

一般的,我們希望在程式啟動的時候就初始化sdk。

  • 這裡使用了 ApplicationRunner 作為初始化入口,當程式啟動成功後,將執行 Runner 做初始化
  • 為避免初始化sdk對主執行緒造成影響,所以 ApplicationRunner 需要放線上程池中 ThreadPoolExecutor,並新增try-catch處理
  • HCNetSDK是SDK範例程式碼中提供的一個物件,此物件繼承Library,負責和更底層的C/C++庫(更底層也許是C寫的,這裡不確定)互動,即執行 Native 呼叫。通過範例化此物件完成sdk依賴庫的匯入,並在後續業務開發中使用此物件向攝像機發布指令。
  • 涉及多作業系統平臺的差異性,官方分別提供不同sdk依賴庫,具體包含:win32,win64,linux32,linux64等,所以當初始化SDK的時候需要根據當前所處環境不同分別載入不同的依賴庫檔案
  • 上述提到的依賴庫檔案,在windows下就是 dll 字尾檔案 , 在 linux 下就是 so 字尾檔案
  • 真正執行初始化就是呼叫 hCNetSDK.NET_DVR_Init() 此方法,並可通過返回值為 truefalse 判斷初始化是否成功。

新建AppRunner

  • AppRunner 需要實現 ApplicationRunner 介面,並將
    AppRunner作為元件放到Spring 容器中管理
  • AppRunner 中注入SdkInitService ,並在run 方法中呼叫 SdkInitService 的initSdk 方法實現SDK的初始化

package com.ramble.hikvisionsdkintegration;
import com.ramble.hikvisionsdkintegration.service.SdkInitService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.stereotype.Component;

@Component
public class AppRunner  implements ApplicationRunner {
    @Autowired
    private SdkInitService hksdkInitService;
    @Override
    public void run(ApplicationArguments args) throws Exception {
        hksdkInitService.initSdk();
    }
}


新建SdkInitService

  • 定義一個公開的 hCNetSDK 屬性,型別為 HCNetSDK ,並在建構函式中初始化 hCNetSDK 屬性的值,此值需要全域性唯一,這裡參照官方程式碼做了單例處理。HCNetSDK 是官方提供的一個介面,一般的都是直接copy到專案原始碼中,你會發現,所有和裝置互動的地方都是通過這個介面來完成的
  • 內部定義一個異常回撥類,用來處理和裝置互動的時候全域性異常的處理
  • 注入 ThreadPoolExecutor 執行器,真正的初始化將放到子執行緒中進行
  • 定義 initSdk 方法用來執行初始化
  • 需要注意的是,建構函式中為 hCNetSDK 屬性初始化值,僅僅只是為了將 sdk 所需的依賴庫檔案 載入到執行時中,並沒有真正的做初始化SDK的工作
  • 需要重點關注OSUtils中的程式碼,載入依賴庫檔案的前提是找到對應的庫檔案,的操作是在 getLoadLibrary 方法中管理的,這裡編寫的程式碼需要和部署時候選擇的部署方式對應,否則可能會出現在windows中開發正常,部署到linux 中就報異常的問題

SdkInitService:


package com.ramble.hikvisionsdkintegration.service;
import com.ramble.hikvisionsdkintegration.sdklib.HCNetSDK;
import com.ramble.hikvisionsdkintegration.task.InitSdkTask;
import com.ramble.hikvisionsdkintegration.util.OSUtils;
import com.sun.jna.Native;
import com.sun.jna.Pointer;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.concurrent.ThreadPoolExecutor;

@Slf4j
@Component
public class SdkInitService {
    public static HCNetSDK hCNetSDK = null;
    static FExceptionCallBack_Imp fExceptionCallBack;
    static class FExceptionCallBack_Imp implements HCNetSDK.FExceptionCallBack {
        public void invoke(int dwType, int lUserID, int lHandle, Pointer pUser) {
            System.out.println("異常事件型別:" + dwType);
            return;
        }
    }
    public SdkInitService() {
        if (hCNetSDK == null) {
            synchronized (HCNetSDK.class) {
                try {
                    hCNetSDK = (HCNetSDK) Native.loadLibrary(OSUtils.getLoadLibrary(), HCNetSDK.class);
                } catch (Exception ex) {
                    log.error("SdkInitService-init-hCNetSDK-error");
                }
            }
        }
    }
    @Autowired
    private ThreadPoolExecutor executor;
    public void initSdk() {
        log.info("HKSDKInitService-init-coming");
        executor.execute(new InitSdkTask());
    }
}

OSUtils:


package com.ramble.hikvisionsdkintegration.util;
import com.sun.jna.Platform;
import lombok.extern.slf4j.Slf4j;
import java.io.File;

@Slf4j
public class OSUtils {
    // 獲取操作平臺資訊
    public static String getOsPrefix() {
        String arch = System.getProperty("os.arch").toLowerCase();
        final String name = System.getProperty("os.name");
        String osPrefix;
        if (Platform.isWindows()) {
            if ("i386".equals(arch)) {
                arch = "x86";
            }
            osPrefix = "win32-" + arch;
        } else if (Platform.isLinux()) {
            if ("x86".equals(arch)) {
                arch = "i386";
            } else if ("x86_64".equals(arch)) {
                arch = "amd64";
            }
            osPrefix = "linux-" + arch;
        } else {
            osPrefix = name.toLowerCase();
            if ("x86".equals(arch)) {
                arch = "i386";
            }
            if ("x86_64".equals(arch)) {
                arch = "amd64";
            }
            int space = osPrefix.indexOf(" ");
            if (space != -1) {
                osPrefix = osPrefix.substring(0, space);
            }
            osPrefix += "-" + arch;
        }
        return osPrefix;
    }
    public static String getOsName() {
        String osName = "";
        String osPrefix = getOsPrefix();
        if (osPrefix.toLowerCase().startsWith("win32-x86")
                || osPrefix.toLowerCase().startsWith("win32-amd64")) {
            osName = "win";
        } else if (osPrefix.toLowerCase().startsWith("linux-i386")
                || osPrefix.toLowerCase().startsWith("linux-amd64")) {
            osName = "linux";
        }
        return osName;
    }
    /**
     * 獲取庫檔案
     * 區分win、linux
     *
     * @return
     */
    public static String getLoadLibrary() {
        if (isChecking()) {
            return null;
        }
        String userDir = System.getProperty("user.dir");
        log.info("getLoadLibrary-userDir={}", userDir);
        String loadLibrary = "";
        String library = "";
        String osPrefix = getOsPrefix();
        if (osPrefix.toLowerCase().startsWith("win32-x86")) {
            loadLibrary = System.getProperty("user.dir") + File.separator + "sdk" + File.separator + "hklibwin32" + File.separator;
            library = "HCNetSDK.dll";
        } else if (osPrefix.toLowerCase().startsWith("win32-amd64")) {
            loadLibrary = System.getProperty("user.dir") + File.separator + "sdk" + File.separator + "hklibwin64" + File.separator;
            library = "HCNetSDK.dll";
        } else if (osPrefix.toLowerCase().startsWith("linux-i386")) {
            //同 linux-amd64
            loadLibrary = "";
            library = "libhcnetsdk.so";
        } else if (osPrefix.toLowerCase().startsWith("linux-amd64")) {
            //方式一:使用系統預設的載入庫路徑,在系統的/usr/lib檔案中加入你Java工程所需要使用的so檔案,然後將HCNetSDKCom資料夾下的元件庫也複製到/usr/lib目錄,HCNetSDKCom資料夾中的元件庫不要隨意更換路徑。CentOS 64位元需拷貝到/usr/lib64下。
            //針對方式一,字首就是絕對路徑
            //loadLibrary = "/usr/lib64/lib/hkliblinux64/";
            //方式二:設定LD_LIBRARY_PATH環境變數載入庫檔案;設定/etc/ld.so.conf,加上你自己的Java工程所需要的so檔案的路徑
            //針對方式二,無需新增字首,程式會從linux系統的so共用庫中查詢libhcnetsdk.so
            loadLibrary = "";
            library = "libhcnetsdk.so";
        }
        log.info("================= Load library Path :{} ==================", loadLibrary + library);
        return loadLibrary + library;
    }
    private static boolean checking = false;
    public static void setChecking() {
        checking = true;
    }
    public static void clearChecking() {
        checking = false;
    }
    public static boolean isChecking() {
        return checking;
    }
}


新建InitSdkTask

  • 此類實現 Runnable 介面,並重寫run方法。
  • 新建一個私有屬性 hCNetSDK 並賦值為 SdkInitService.hCNetSDK ,因為初始化需要用到 HCNetSDK 這個物件和裝置互動,所以初始化前必須確保此物件已經建立,本例中,程式在執行 SdkInitService 建構函式的時候初始化了 HCNetSDK 物件,並放到一個全域性靜態變數中
  • 其實也可以不新建私有屬性 hCNetSDK ,在需要用到此物件的地方 使用 SdkInitService.hCNetSDK 的方式獲取也可以
  • 通過呼叫  hCNetSDK.NET_DVR_Init 方法執行初始化,並可以通過返回值確定是否初始化成功,初始化成功後,將可以呼叫業務介面向裝置傳送指令。
  • NET_DVR_SetConnectTime,NET_DVR_SetReconnect 是可選的,並不會對初始化SDK本身造成影響。
  • 為了避免對主程式造成影響,初始化程式碼將需要做 try - catch 處理

InitSdkTask:


package com.ramble.hikvisionsdkintegration.task;
import com.ramble.hikvisionsdkintegration.sdklib.HCNetSDK;
import com.ramble.hikvisionsdkintegration.service.SdkInitService;
import com.ramble.hikvisionsdkintegration.util.OSUtils;
import lombok.extern.slf4j.Slf4j;
import java.util.Objects;

@Slf4j
public class InitSdkTask implements Runnable {
    /**
     * 裝配 sdk 所需依賴
     */
    private static HCNetSDK hCNetSDK = SdkInitService.hCNetSDK;
    @Override
    public void run() {
        try {
            if (Objects.equals(OSUtils.getOsName(), "linux")) {
                log.info("InitSdk-is-linux");
                String userDir = System.getProperty("user.dir");
                log.info("InitSdk-userDir={}", userDir);
                String osPrefix = OSUtils.getOsPrefix();
                if (osPrefix.toLowerCase().startsWith("linux-i386")) {
                    HCNetSDK.BYTE_ARRAY ptrByteArray1 = new HCNetSDK.BYTE_ARRAY(256);
                    HCNetSDK.BYTE_ARRAY ptrByteArray2 = new HCNetSDK.BYTE_ARRAY(256);
                    //這裡是庫的絕對路徑,請根據實際情況修改,注意改路徑必須有存取許可權
                    //linux 下, 庫載入參考:OSUtils.getLoadLibrary()
                    String strPath1 = System.getProperty("user.dir") + "/hkliblinux32/libcrypto.so.1.1";
                    String strPath2 = System.getProperty("user.dir") + "/hkliblinux32/libssl.so.1.1";
                    System.arraycopy(strPath1.getBytes(), 0, ptrByteArray1.byValue, 0, strPath1.length());
                    ptrByteArray1.write();
                    hCNetSDK.NET_DVR_SetSDKInitCfg(3, ptrByteArray1.getPointer());
                    System.arraycopy(strPath2.getBytes(), 0, ptrByteArray2.byValue, 0, strPath2.length());
                    ptrByteArray2.write();
                    hCNetSDK.NET_DVR_SetSDKInitCfg(4, ptrByteArray2.getPointer());
                    //linux 下, 庫載入參考:OSUtils.getLoadLibrary()
                    String strPathCom = System.getProperty("user.dir") + "/hkliblinux32/HCNetSDKCom/";
                    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(2, struComPath.getPointer());
                } else if (osPrefix.toLowerCase().startsWith("linux-amd64")) {
                    HCNetSDK.BYTE_ARRAY ptrByteArray1 = new HCNetSDK.BYTE_ARRAY(256);
                    HCNetSDK.BYTE_ARRAY ptrByteArray2 = new HCNetSDK.BYTE_ARRAY(256);
                    //這裡是庫的絕對路徑,請根據實際情況修改,注意改路徑必須有存取許可權
                    //linux 下, 庫載入參考:OSUtils.getLoadLibrary()
                    String strPath1 = System.getProperty("user.dir") + "/hkliblinux64/libcrypto.so.1.1";
                    String strPath2 = System.getProperty("user.dir") + "/hkliblinux64/libssl.so.1.1";
                    System.arraycopy(strPath1.getBytes(), 0, ptrByteArray1.byValue, 0, strPath1.length());
                    ptrByteArray1.write();
                    hCNetSDK.NET_DVR_SetSDKInitCfg(3, ptrByteArray1.getPointer());
                    System.arraycopy(strPath2.getBytes(), 0, ptrByteArray2.byValue, 0, strPath2.length());
                    ptrByteArray2.write();
                    hCNetSDK.NET_DVR_SetSDKInitCfg(4, ptrByteArray2.getPointer());
                    String strPathCom = System.getProperty("user.dir") + "/hkliblinux64/HCNetSDKCom/";
                    //linux 下, 庫載入參考:OSUtils.getLoadLibrary()
                    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(2, struComPath.getPointer());
                } else {
                    log.info("osPrefix={}", osPrefix);
                }
            }
            //初始化sdk
            boolean isOk = hCNetSDK.NET_DVR_Init();
            hCNetSDK.NET_DVR_SetConnectTime(10, 1);
            hCNetSDK.NET_DVR_SetReconnect(100, true);
            if (!isOk) {
                log.error("=================== InitSDK init fail ===================");
            } else {
                log.info("============== InitSDK init success ====================");
            }
        } catch (Exception e) {
            log.error("InitSDK-error,e={}", e.getMessage());
            e.printStackTrace();
        }
    }
}


新建 HCNetSDK

直接從官方範例程式碼中copy過來即可

呼叫業務介面

  • 新建一個controller ,嘗試呼叫 獲取SDK狀態 的介面。
  • 呼叫所有的業務介面之前都需要先登入

package com.ramble.hikvisionsdkintegration.controller;
import com.alibaba.fastjson2.JSON;
import com.ramble.hikvisionsdkintegration.dto.GlobalResponseEntity;
import com.ramble.hikvisionsdkintegration.sdklib.HCNetSDK;
import com.ramble.hikvisionsdkintegration.service.SdkInitService;
import com.sun.jna.Memory;
import com.sun.jna.Pointer;
import com.sun.jna.ptr.IntByReference;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;


@Slf4j
@AllArgsConstructor
@RestController
@RequestMapping("/test")
public class TestController {
    private static String m_sDeviceIP = "192.168.1.142";
    private static String m_sUsername = "xxx";
    private static String m_sPassword = "xxx";
    
    /**
     * 獲取sdk狀態
     *
     * @return {@link GlobalResponseEntity}<{@link String}>
     * 返回值舉例:{"success":true,"code":"000000","message":"request successfully",
     * "data":"{\"dwRes\":[0,0,0,0,0,0,0,0,0,0],\"dwTotalAlarmChanNum\":0,\"dwTotalBroadCastNum\":0,\"dwTotalFileSearchNum\":0,\"dwTotalFormatNum\":0,
     * \"dwTotalLogSearchNum\":0,\"dwTotalLoginNum\":1,\"dwTotalPlayBackNum\":0,\"dwTotalRealPlayNum\":0,\"dwTotalSerialNum\":0,\"dwTotalUpgradeNum\":0,
     * \"dwTotalVoiceComNum\":0,\"autoRead\":true,\"autoWrite\":true,\"pointer\":{\"size\":84,\"valid\":true}}"}
     */
     
    @GetMapping("/state")
    public GlobalResponseEntity<String> getSdkState() {
        //登入
        Integer userId = login();
        log.info("userId={}", userId);
        HCNetSDK.NET_DVR_SDKSTATE sdkState = new HCNetSDK.NET_DVR_SDKSTATE();
        //獲取當前SDK狀態資訊
        boolean result = SdkInitService.hCNetSDK.NET_DVR_GetSDKState(sdkState);
        if (result) {
            sdkState.read();
            String s = JSON.toJSONString(sdkState);
            return GlobalResponseEntity.success(s);
        } else {
            int error = SdkInitService.hCNetSDK.NET_DVR_GetLastError();
            return GlobalResponseEntity.error("獲取失敗,錯誤碼為:" + error);
        }
    }
    
    
    private Integer login() {
        HCNetSDK.NET_DVR_USER_LOGIN_INFO m_strLoginInfo = new HCNetSDK.NET_DVR_USER_LOGIN_INFO();//裝置登入資訊
        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.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());
        m_strLoginInfo.wPort = Short.valueOf("8000");
        m_strLoginInfo.bUseAsynLogin = false; //是否非同步登入:0- 否,1- 是
        m_strLoginInfo.write();
        HCNetSDK.NET_DVR_DEVICEINFO_V40 m_strDeviceInfo = new HCNetSDK.NET_DVR_DEVICEINFO_V40();//裝置資訊
        int loginHandler = SdkInitService.hCNetSDK.NET_DVR_Login_V40(m_strLoginInfo, m_strDeviceInfo);
        if (loginHandler == -1) {
            int errorCode = SdkInitService.hCNetSDK.NET_DVR_GetLastError();
            IntByReference errorInt = new IntByReference(errorCode);
            log.error("[HK] login fail errorCode:{}, errMsg:{}", errorCode, SdkInitService.hCNetSDK.NET_DVR_GetErrorMsg(errorInt));
            return null;
        } else {
            return loginHandler;
        }
    }
}


部署

拷貝so庫檔案到部署目錄

所有廠家的所有版本sdk庫檔案均維護在專案原始碼中,需要將linux庫檔案so檔案拷貝到部署根目錄,和jar檔案同級

追加環境變數

通過設定 LD_LIBRARY_PATH 環境變數載入庫檔案,開啟系統的 /etc/profile 組態檔,在最後追加so庫檔案所在目錄:


export  LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/home/app/jars/hkliblinux64:/home/app/jars/hkliblinux64/HCNetSDKCom

如上所示:
● 32位元就追加  hkliblinux32 目錄,64位元就追加 hkliblinux64 目錄
● 不要忘記 HCNetSDKCom 目錄也需要設定,因為裡面也有so庫檔案。
執行source 命令,讓設定生效:


source   /etc/profile

追加so庫載入路徑

開啟 /etc/ld.so.conf 組態檔,追加so庫檔案所在目錄


/home/app/jars/hkliblinux64
/home/app/jars/hkliblinux64/HCNetSDKCom

如上所示:
● 32位元就追加  hkliblinux32 目錄,64位元就追加 hkliblinux64 目錄。
● 不要忘記 HCNetSDKCom 目錄也需要設定,因為裡面也有so庫檔案。

執行 ldconfig 命令,讓設定生效:


ldconfig

驗證SDK初始化是否成功

一般來說,可以在程式初始化SDK的時候新增紀錄檔,通過紀錄檔輸出判斷是否初始化成功。

程式碼

https://gitee.com/naylor_personal/ramble-spring-boot/tree/master/hikvision-sdk-integration