简介:面向在Linux ARM平台上通过C++接入海康威视摄像头、开发视频采集与监控应用的嵌入式开发者,这套工程围绕SDK调用提供了从依赖库到可构建代码的闭环。包内共36个文件、压缩包约9.34MB,其中21个so动态库涵盖HCNetSDK核心SDK、PlayCtrl播放控制库及ssl、z等依赖库;7个h头文件给出接口声明和关键数据结构;main.cpp、CMakeLists.txt与Device.ini则构成示例入口、构建配置和运行参数,便于理解整个工程的落地方式。已有1374人学习,适合正做海康设备接入的开发者参考。通过lib、include、src等目录组织和示例代码,可以较快掌握设备发现、登录、取流等核心接口的调用关系,节省环境配置与API摸索的时间。
1. 把x86上的摄像头代码搬到Linux ARM板,最容易在这三步翻车
把一段在x86上跑得稳稳的海康威视SDK C++代码移植到linux arm板子上,翻车往往从敲下./demo那一刻开始:要么报error while loading shared libraries,要么直接一个Illegal instruction崩溃。海康威视设备网络SDK(HCNetSDK)调摄像头本身不难,难的是ARM平台的库链路——aarch64还是armhf、libhpr.so带没带、目标板GLIBC版本够不够,任何一环对不上,摄像头API就成了黑匣子。这篇按我实际装过的顺序写:先确认平台和SDK版本,再搭交叉编译环境,跑通第一条C++调用链,最后把那些能让人折腾一晚上排查的坑逐个排掉。适合第一次在ARM开发板上用C++接海康摄像头的开发者照着做,也适合熟手对照检查自己踩过的版本雷区。
2. 先确认环境再动手:aarch64与armhf决定SDK能不能起来
很多人第一步是找个Linux版SDK压缩包就直接解压往板子上拷,结果板子上运行时报缺库、报段错误,回头才意识到架构压根不对。海康设备网络SDK的Linux版按CPU架构分了好几套包,选错版本不是“改个环境变量”能救回来的。这一章先把平台的底细摸清楚,再决定下载哪套SDK,同时把目标板运行环境做一次体检。
2.1 板子是什么架构:不查就是赌博
在板子上执行uname -m,是最直观的判断方式。常见输出有三种:
uname -m # aarch64:64位ARM,对应海康SDK的LinuxARM64包 # armv7l:32位ARM,对应海康SDK的LinuxARM(armhf)包 # x86_64:intel/amd架构,用Linux64包,和本文无关如果想更严谨,用readelf看系统第一个可执行文件的架构,确认内核ABI:
readelf -h /bin/sh | grep Machine # 输出 Machine: AArch64 说明是64位ARM海康Linux SDK的适配关系和这个输出直接挂钩:aarch64用ARM64包,armv7l用ARM(hf)包。这里有个实实在在的坑——aarch64的板子(绝大多数新出的ARM开发板都是)如果装成armhf 32位库,运行任何海康SDK程序都会报/lib/ld-linux-armhf.so.3: No such file or directory,因为64位系统默认没有32位动态链接器加载器。我在一块64位板子上试过把ARM包里的so直接拷进来,程序起不来,错误提示极其迷惑,最后查了半天才发现是架构不匹配。
建议先用一张小表核对清楚再下载,避免做无用功:
| 板子uname -m | 对应SDK包 | libhcnetsdk.so架构 | 注意 |
|---|---|---|---|
| x86_64 | Linux64 | x86-64 | 桌面PC常见 |
| aarch64 | LinuxARM64 | AArch64 | 新ARM板主流 |
| armv7l | LinuxARM | ARM 32-bit | 老板子、部分工控 |
选包时如果设备型号老,还要留意SDK包内部带的说明文件,有些历史版本只提供32位库。下载后立刻用file命令核对实际架构,别信压缩包文件名,这是我吃过亏后的习惯。
2.2 SDK解压后别急着编译:先做一次文件清单和依赖核对
海康Linux SDK解压后通常就是include和lib两个目录,头文件以HCNetSDK.h为核心,库文件以libhcnetsdk.so为核心,但真正运行时不止这一个库。我解压后第一件事是逐个检查库的真实架构和依赖项:
cd HCNetSDK_LinuxARM64 for f in lib/*.so*; do echo "==== $f ====" file "$f" done这一步能拦截绝大多数“版本选错”问题。正常ARM64包里,libhcnetsdk.so、libhpr.so都应该是ARM aarch64的ELF。如果其中某个so是x86的,说明包本身混杂了文件,这种事虽然少但确实存在,拷到板子上必然崩溃。
一个常见误区是只拷libhcnetsdk.so,忽略libhpr.so。libhpr是海康自己的基础库,hcnetsdk在运行时强依赖它,缺了它程序会在加载阶段直接报cannot open shared object file。除此之外,部分SDK包还带libcrypto、libssl、libz这些第三方库。我的做法是:把lib目录下所有so原始拷贝,不做任何裁剪。哪怕只是多占几十KB,也比上线后缺库强。
依赖关系可以用readelf查清楚:
aarch64-linux-gnu-readelf -d lib/libhcnetsdk.so | grep NEEDED # 输出里应能看到 libhpr.so、libdl.so.2、libpthread.so.0 等拿到这份NEEDED清单,后面在板子上做ldd排查时才能确认“缺的是谁、谁依赖谁”。
2.3 目标板运行环境体检:GLIBC、libstdc++和网络三件事
SDK库能不能跑,取决于目标板的glibc版本是否覆盖库的要求。交叉编译工具链太新、板子系统太旧,会出现GLIBC_2.29 not found之类的问题。先在板子上查当前glibc支持的上限:
strings /lib/aarch64-linux-gnu/libc.so.6 | grep '^GLIBC_' | tail -1 # 例如 GLIBC_2.28接着检查libstdc++和网络通断。板子上执行:
ls -l /usr/lib/aarch64-linux-gnu/libstdc++.so.6 ping -c 3 192.168.1.64 timeout 3 bash -c 'echo > /dev/tcp/192.168.1.64/8000' && echo "8000 open"最后那条命令用来探测海康设备的SDK通信端口是否可达。海康摄像头默认SDK端口是8000,和RTSP的554端口是两回事。很多开发者习惯先用VLC拉RTSP流验证摄像头通没通,然后代码里还用554去登录SDK,结果一直失败。记住:登录用的是8000,RTSP只在取流播放时才涉及。缺libstdc++时直接apt安装,板子能联网就一条命令解决:
apt-get update && apt-get install -y libstdc++6体检做完,板子系统和库的兼容性、网络可达性都有数了,再进入编译环节就不会被环境问题反复折腾。
3. 交叉编译与C++接入:从空工程到跑通登录预览
板子系统准备好之后,就是真正的C++编译接入环节。这里我推荐在x86的Ubuntu机器上交叉编译,产物拷贝到ARM板运行;如果板子的性能和磁盘空间都够,直接在板子上apt装build-essential然后本地编译也完全可行,两种方式各有取舍,下面会说明。
3.1 用aarch64-linux-gnu-g++做交叉编译:工具链版本是生死线
交叉编译的核心,是用与目标板架构一致的工具链生成可执行文件。开发机上安装:
sudo apt-get update sudo apt-get install -y gcc-aarch64-linux-gnu g++-aarch64-linux-gnu aarch64-linux-gnu-g++ --version安装完成后,这个编译器编出来的ELF默认是AArch64架构。这里有一个字字血泪的教训:工具链的glibc版本必须不高于目标板的glibc版本。怎么核?先看编译器的glibc等级:
aarch64-linux-gnu-g++ -v 2>&1 | grep 'glibc' # 部分工具链输出有 # 更准确的办法:在目标板上对照编译一个空main.cpp,看能否运行如果工具链太新、板子系统太老,编译出来的程序在板子上会报version GLIBC_2.34 not found,这种问题基本无解,只能换旧工具链或者升级板子系统。我的习惯是:先在板子上跑一下strings /lib/aarch64-linux-gnu/libc.so.6 | grep '^GLIBC_'记下最高版本,然后选择工具链时宁可装旧不装新。开源社区里有些第三方ARM工具链版本非常新,但未必适配你的内核,官方apt源里的交叉工具链反而是最稳的。
如果选择在板子上直接编译,命令是:
sudo apt-get install -y build-essential g++ --version这种方式不需要关心交叉工具链的glibc版本,因为编译器本身就是板子系统自带的,编出来的二进制一定和系统匹配。代价是ARM板编译速度慢,工程体量大时能明显感觉到,动不动就编译几分钟。我一般小demo直接在板子编译,大工程还是交叉编译后整体部署。
3.2 最小C++调用工程:初始化、登录、预览、抓图一屏走完
海康SDK的C++调用本质是C接口,流程固定:NET_DVR_Init初始化→NET_DVR_Login_V40登录→NET_DVR_RealPlay_V40建立预览通道→NET_DVR_CapturePicture抓图→清理退出。下面是一份能直接抄的最小工程:
// main.cpp #include <cstdio> #include <cstring> #include <unistd.h> #include "HCNetSDK.h" int main() { // 1. 初始化SDK NET_DVR_Init(); // 2. 写日志,路径固定,方便排查 NET_DVR_SetLogToFile(3, "/tmp/sdkLog", 1); // 3. 组装登录参数 NET_DVR_USER_LOGIN_INFO loginInfo = {0}; NET_DVR_DEVICEINFO_V40 deviceInfo = {0}; strncpy(loginInfo.sDeviceAddress, "192.168.1.64", NET_DVR_DEV_ADDRESS_MAX_LEN); loginInfo.wPort = 8000; strncpy(loginInfo.sUserName, "admin", NAME_MAX_LEN); strncpy(loginInfo.sPassword, "your_password", NAME_MAX_LEN); loginInfo.bUseAsynLogin = 0; // 0=同步登录,1=异步 LONG userId = NET_DVR_Login_V40(&loginInfo, &deviceInfo); if (userId < 0) { printf("login failed, error code: %d\n", NET_DVR_GetLastError()); NET_DVR_Cleanup(); return -1; } printf("login ok, start channel: %d\n", deviceInfo.struDeviceV30.byStartChan); // 4. 建立实时预览(本地抓图的前提) NET_DVR_PREVIEWINFO previewInfo = {0}; previewInfo.hPlayWnd = NULL; previewInfo.lChannel = deviceInfo.struDeviceV30.byStartChan; previewInfo.dwStreamType = 0; // 0=主码流,1=子码流 previewInfo.bBlocked = 1; // 阻塞取流 LONG realHandle = NET_DVR_RealPlay_V40(userId, &previewInfo, NULL, NULL); if (realHandle < 0) { printf("realplay failed, error code: %d\n", NET_DVR_GetLastError()); NET_DVR_Logout(userId); NET_DVR_Cleanup(); return -1; } // 5. 等两秒让码流稳定,然后抓图 sleep(2); NET_DVR_SetCapturePictureMode(1); // 1=JPEG,0=BMP if (!NET_DVR_CapturePicture(realHandle, 1, "/tmp/capture_1.jpg")) { printf("capture failed, error code: %d\n", NET_DVR_GetLastError()); } else { printf("capture ok: /tmp/capture_1.jpg\n"); } // 6. 按相反顺序释放资源 NET_DVR_StopRealPlay(realHandle); NET_DVR_Logout(userId); NET_DVR_Cleanup(); return 0; }这段代码有几个值得说的参数:登录端口wPort必须填8000,这是海康SDK默认端口;设备通道号从byStartChan取,海康多数设备为1,但NVR接入多个IPC时起始通道可能不是1,直接写死0是经典的失败写法;bBlocked填1表示阻塞式取流,抓图前需要码流已经推上来,sleep 2秒是我实际用下来比较稳的等待时间,太快抓图会遇到尚未出流导致失败。
抓图前必须调用NET_DVR_SetCapturePictureMode(1)切换成JPEG模式,否则默认输出BMP,一张图几十MB,写嵌入式设备存储容易直接爆掉。如果不想依赖预览句柄,还有一条路径是NET_DVR_CaptureJPEG_PIC_NEW直接请求设备端抓图,走HTTP协议,不需要预览通道,适合只想定时抓图的场景。但第一次跑通demo,走预览链路更直白,问题也更少。
3.3 编译链接与运行库路径:三个必须同时做到的配置
交叉编译命令如下,注意头文件、库路径和链接顺序:
aarch64-linux-gnu-g++ -std=c++11 -I./include main.cpp \ -o hik_demo \ -L./lib -lhcnetsdk -lhpr \ -Wl,-rpath,/opt/sdk/lib \ -lpthread-l参数顺序有讲究:可执行文件依赖libhcnetsdk,libhcnetsdk依赖libhpr,所以-lhcnetsdk必须出现在-lhpr前面,反了会报undefined reference。-Wl,-rpath,/opt/sdk/lib是把库搜索路径写进ELF里,这样运行时不需要依赖环境变量,也是最推荐的生产部署方式。
运行时库路径有“三件套”说法,分别是:LD_LIBRARY_PATH环境变量、ELF里的RPATH/RUNPATH、把so直接拷进系统/usr/lib。优先级从高到低,但各有坑:
# 方式一:环境变量,适合调试 export LD_LIBRARY_PATH=/opt/sdk/lib:$LD_LIBRARY_PATH # 方式二:编译期写死RPATH,适合正式部署 # 编译时加 -Wl,-rpath,/opt/sdk/lib # 方式三:拷到系统目录,适合固件集成 sudo cp lib/*.so /usr/lib/我一般调试期用环境变量,因为换SDK版本方便;正式烧录固件时用RPATH或直接拷库,因为设备上没人去手动export。三种方式都需要保证libhcnetsdk.so、libhpr.so等所有so都在同一个目录,只拷一半是最常见翻车原因。
最后把产物和库一起发布到板子:
scp hik_demo root@192.168.1.100:/home/arm/test/ scp -r lib root@192.168.1.100:/home/arm/test/板上运行时,先用ldd ./hik_demo | grep not found检查依赖是否齐全。看到“not found”就说明要么目录没拷全,要么LD_LIBRARY_PATH没包含库路径,按提示补即可。这里注意海康的so用ldd查不到完整依赖时,可能因为so之间的依赖关系用了绝对路径,如果SDK包里的so自带路径写得比较怪,直接chmod +x并确保目录禁止移动。
4. 常见坑与排查:五条能折腾一整晚的踩坑记录
这一章全是实际排查记录,每一条都按“现象→原因→解决”的顺序写。遇到类似问题时,先对着sdkLog目录里的日志看,再对照NET_DVR_GetLastError()返回的错误码,能省掉大量盲目试错时间。
4.1 编译通过了,运行却找不到libhpr.so
现象:板子上执行./hik_demo,直接报error while loading shared libraries: libhpr.so: cannot open shared object file。程序编译正常,但运行起不来。
原因:libhcnetsdk.so运行时会加载libhpr.so,这个库属于海康核心依赖,不会被打进可执行文件。如果只把demo拷贝到板子,或者库目录没有正确加入加载路径,就会出现这个提示。
解决:在板子上用ldd确认缺失项,然后把SDK的lib目录完整拷过去并生效:
ldd ./hik_demo | grep "not found" # libhpr.so => not found export LD_LIBRARY_PATH=/home/arm/test/lib:$LD_LIBRARY_PATH ./hik_demo更稳的做法是编译时就带-Wl,-rpath,/home/arm/test/lib,我前面3.3节已经演示了写法。注意有些SDK包里还有libcrypto和libssl,这些也要一起拷,缺一个同样会在ldd里标红。
4.2 库都加载了,还报GLIBC_2.29 not found
现象:程序能进入启动流程,随后报GLIBC_2.29 not found或FATAL: kernel too old,崩溃前没有任何业务日志。
原因:交叉编译工具链自带的glibc比目标板系统的glibc新。板子系统是比较精简的出厂固件,glibc停在2.28,而工具链直接编出了依赖2.29的代码。这类问题无法通过拷so文件解决,glibc是系统最底层的库,强行替换只会把系统搞崩。
解决:三个可行路径。第一,在板子上直接编译,彻底避开工具链版本差异;第二,换一个更旧、glibc版本匹配的交叉工具链;第三,升级板子的rootfs镜像到更新版本。最有效的排查方法是先把两边版本拉出来比对:
# 板子上执行 strings /lib/aarch64-linux-gnu/libc.so.6 | grep '^GLIBC_' | tail -1 # 开发机上查看交叉工具链默认glibc搜索路径 aarch64-linux-gnu-g++ -print-file-name=libc.so.6从那以后我拿到任何新板子,第一件事永远是确认板子glibc版本,再决定用哪个工具链,不再盲目追新。
4.3 登录超时:设备就在同一个交换机上却就是连不上
现象:NET_DVR_Login_V40返回-1,NET_DVR_GetLastError()返回22xx段的网络类错误码,sdkLog里能看到明显的Timeout记录。设备管理网页能打开,甚至RTSP都能拉流,但SDK登录就是失败。
原因:除此以外最隐蔽的原因是登录端口搞错。设备网页管理端口可能是80或443,RTSP是554,但SDK登录固定走8000,三者互不相通。如果代码里wPort填成554,必炸。其次才是网段隔离、防火墙拦截这类普通网络问题。
解决:先在板子上分别验证IP连通性和8000端口:
ping -c 3 192.168.1.64 timeout 3 bash -c 'echo > /dev/tcp/192.168.1.64/8000' && echo "port open"端口不通时检查板子和摄像头是否同网段,摄像机是否启用了“SDK加密”或“非法登录锁定”。某些设备连续错误密码后会把IP锁一段时间,表现为TCP能通但登录直接超时。此时等锁定时间过后再试,或者把设备断电重启。同时可以调短SDK连接超时参数,让失败快速返回:
NET_DVR_SetConnectTime(2000, 1); // 2秒超时,重试1次4.4 登录成功,预览和抓图却一直失败
现象:登录接口返回正常的userID,但NET_DVR_RealPlay_V40返回-1,错误码#23或者#29。抓图也返回失败,sdkLog提示通道错误或无码流。
原因:九成是通道号写错。海康设备的通道号从byStartChan开始,大概率是1,很多头回写的代码直接填0,SDK认为你提交了一个不存在的通道。另一成因是顺序问题,本地抓图NET_DVR_CapturePicture必须依赖一个正在工作的实时预览句柄,没有RealPlay成功之前直接抓图一定失败。
解决:不要硬编码通道号,从设备信息结构体里取:
// 用byStartChan作为通道基础值 LONG chan = deviceInfo.struDeviceV30.byStartChan; previewInfo.lChannel = chan;如果只是偶尔抓个图、不想要实时预览链路,可以直接走设备端抓图接口,绕开预览:
NET_DVR_CaptureJPEG_PIC_NEW(userId, chan, 0, 80, "/tmp/snap.jpg");这个接口把抓图请求直接发给设备,不需要RealPlay句柄,也不依赖码流稳定时间,适合写定时截图任务。我后来在做巡检抓图功能时就把整个链路换成了这个接口,稳定性明显好于预览后抓图。
4.5 中文用户名设置后,代码登录一直失败
现象:设备管理页把用户名改成中文后,C++代码里用strncpy(loginInfo.sUserName, "摄像头01", ...)登录,返回错误码#1005或登录失败。
原因:字符串编码不一致。设备管理页通常按UTF-8保存用户名,但嵌入式Linux终端环境如果locale是GBK,源码里写的字符串字面量会被存成GBK字节序列,两者对不上。这是个非常隐蔽的编码型故障,看代码完全正常,实际字节完全错误。
解决:源码统一用UTF-8字节,编译时加-finput-charset=UTF-8,源码文件本身保存成UTF-8无BOM;或者干脆不在代码里放中文,只从配置文件读取用户名。最省心的做法是设备端也只用admin这类ASCII账号做程序接入,中文用户名留给网页人工操作。碰到非用中文不可的项目,我会单独封装一层UTF-8转码函数,确保进入loginInfo.sUserName之前字节序列是UTF-8。
5. 装完怎么验证:一条命令确认SDK链路全通
环境装完不是目的,跑通验证才是。我习惯把验证拆成三步,每一步都能明确失败在哪个环节。
第一步,看依赖。板子上执行:
LD_LIBRARY_PATH=/home/arm/test/lib:$LD_LIBRARY_PATH ldd ./hik_demo | grep "not found"正常输出为空,说明所有动态库解析成功;有任何not found,直接对照拷库路径,别往下走。
第二步,跑登录。SDK初始化加登录成功后,终端会打印login ok, start channel: 1,这里确认两件事:设备IP和端口配置正确、账号密码对得上。如果想更稳,再加一层设备信息打印:
./hik_demo # 期望输出: # login ok, start channel: 1 # capture ok: /tmp/capture_1.jpg第三步,验证抓图产物有效性。抓图成功不代表图片真能看,JPEG文件被截断照样报成功,所以要检查文件头:
file /tmp/capture_1.jpg # 期望输出:JPEG image data, JFIF standard ls -l /tmp/capture_1.jpg # 期望大小在几十KB到几百KB之间出现capture failed, error code: 23之类的输出时,回到第四章4.4的通道号排查;出现TCP超时则检查防火墙与8000端口。整套验证流程走完,SDK调用链基本就通了。
最后补一个我个人的收尾习惯。每次换板子、换SDK版本、甚至只是换一条网线后,我都会强制走一遍上面的三步验证:先查GLIBC,再ldd,最后抓一张图。这条流程三分钟跑完,却帮我挡掉过好几次把坏库烧进固件的低级事故。希望帮到你。
本文还有配套的精品资源,点击获取