简介:面向Linux环境下海康威视网络摄像机二次开发的工程资料,适合具备C/C++基础、希望绕过官方云平台直连设备并实现视频流本地处理的开发者。压缩包共52个文件,约10.97MB,含24个so动态库、10个h头文件、5个cpp源码、5个o中间文件及ui界面、pro工程配置和Makefile,附带的说明文档也便于快速确认SDK依赖、编译入口与调用方式。资料围绕HikCamera工程实例,覆盖设备搜索连接、实时预览、录像回放、报警订阅、PTZ控制、用户管理等开发环节,示例代码从初始化到界面显示形成完整链路,可直观看到多线程与网络编程在视频流处理中的落地写法,并进一步学习报警回调、抓图与本地存储等常见扩展。已有4942人学习下载,适合在Linux下快速搭建海康摄像头二次开发环境,再按项目需求定制功能。
1. Linux 下的安防设备二次开发:这套 SDK 到底在解决什么问题
做 Linux 服务端接入安防设备时,最常见的诉求是把 NVR/DVR 上的实时码流拉下来,喂给算法容器做分析,顺便再处理抓图、云台控制和报警订阅。厂商提供的 RTSP 通常够用,但要绕开兼容性问题、拿到设备私有功能时,回到官方 Linux SDK 是绕不开的路。很多人卡在第一步:包解压后方向不清,文档偏厚,示例代码偏老,编译和登录的坑一个接一个。这篇文章按“解包、工程搭建、登录、取流、避坑、接解码器”的顺序把 Linux SDK 二次开发的完整链路过一遍,适合要给后台服务打通视频能力的嵌入式和服务端开发者参考。
2. 解包与工程搭建:五个目录、两个动态库和一条编译链
拿到离线包之后,建议先不要碰示例代码,而是花十分钟把包内目录和文档结构搞清楚。厂商提供的 Linux SDK 通常是一个压缩包,解压后包含 include、lib、doc、sample、bin 这几类目录。命名在不同版本里略有差异,但职责基本一致。花时间搞清目录不是为了学知识,是为了后面编译或运行时报错时不至于瞎猜。
2.1 include、lib、doc、sample、bin 各管什么
include 目录下,核心主头文件会继续 include 多个子头文件,所有导出函数、结构体、错误码宏都集中在这里。编译时把这个目录加进头文件搜索路径即可,不需要逐个 include 子文件。lib 目录通常放两个动态库:一个主功能库,一个基础运行库。主功能库负责设备登录、实时取流、报警回调等对外接口;基础运行库处理平台相关解析和认证,主库运行时会依赖它。漏掉任何一个,程序都会在启动阶段报找不到动态库的错误。
doc 目录里最有参考价值的是编程指南和错误码对照表。编程指南会把初始化、登录、取流、报警的调用顺序整理成流程图,照着走不容易漏步骤;错误码对照表是排查问题的第一入口,很多登录失败问题查一下错误码就能定位。sample 目录里是官方示例工程,覆盖登录、预览、抓图、对讲和报警,风格通常偏老,有些头文件依赖和编译宏还带着其他平台遗留的痕迹,直接拿过来在 Linux 上编译会报一堆干扰项。bin 目录里是官方编译好的 demo,我拿到包后的第一步往往是先跑一次 bin 里的可执行文件,确认设备和网络本身没问题,再回来搭自己的工程。
2.2 最小工程:Makefile 写法与链接顺序
刚开始不建议把 sample 整体搬进工程,而是写一个 main.c,先做到“登录-退出”两步,把编译和链接链路跑通,再逐步加功能。最小 Makefile 长这样:
CC := gcc TARGET := dev_demo SDK_PATH := /opt/vendor_sdk CFLAGS := -I$(SDK_PATH)/include -Wall -g LDFLAGS := -L$(SDK_PATH)/lib -lnetsdk -lhpr -lpthread -ldl -lm SRCS := main.c OBJS := $(SRCS:.c=.o) $(TARGET): $(OBJS) $(CC) -o $@ $^ $(LDFLAGS) %.o: %.c $(CC) $(CFLAGS) -c $< -o $@ clean: rm -f $(OBJS) $(TARGET) run: $(TARGET) export LD_LIBRARY_PATH=$(SDK_PATH)/lib; ./$(TARGET)这里有两个容易踩的点。第一个是链接顺序:GCC 对动态库的符号解析是单向的,主功能库 netsdk 要写在最前,它依赖的基础运行库 hpr 和系统库 pthread、dl、m 写在后面。如果先把 -lpthread 放在 -lnetsdk 前面,就可能看到一堆 undefined reference,那不是代码问题,是库顺序问题。第二个是运行时库搜索路径:编译成功不代表运行成功,动态库的搜索路径需要在运行前单独设置,这个步骤放在 Makefile 的 run 目标里最省事。
编译通过后,用 ldd 验证一下运行依赖:
export LD_LIBRARY_PATH=/opt/vendor_sdk/lib ldd dev_demoldd 输出里如果某个 .so 显示 not found,先确认路径是否设置正确。有些精简版的系统镜像还会缺系统基础库,按提示补装对应包即可。实际部署时注意不要在代码里用 setenv 去改 LD_LIBRARY_PATH,SDK 的库加载动作早于用户代码执行,改晚了没用。正确做法是启动脚本里先 export,再拉起程序。
2.3 交叉编译与动态库的两个边界
如果目标环境是 ARM 板,SDK 包要选择对应架构的版本,这个坑我见过不止一次。用 file 命令看 .so 的架构,x86-64 和 aarch64 不兼容,强行链接会出现 skipping incompatible 一类的提示,这不是代码问题,是选包问题。有些厂商会按平台拆成多个目录,比如 x86 和 arm 分开,先确认再编译。
另一个边界是版本。同一个系列的 SDK,API 是向下兼容的,但登录函数从旧版演进到新版本时,结构体字段变化很大,示例代码如果不从当前头文件里抄,编译阶段就会暴露字段缺失。所以不要凭记忆写结构体,尽量从 include 目录里的当前头文件复制定义。习惯用 CMake 的同事一般这样写:
cmake_minimum_required(VERSION 3.10) project(dev_demo C) set(CMAKE_C_STANDARD 99) set(SDK_PATH /opt/vendor_sdk) include_directories(${SDK_PATH}/include) link_directories(${SDK_PATH}/lib) add_executable(dev_demo main.c) target_link_libraries(dev_demo netsdk hpr pthread dl m)链接依赖与 Makefile 完全一致,只是换了工程组织方式。如果项目里还要编别的组件,把这几个库加进 target_link_libraries 即可。工程能编出来、能跑起来,才进入登录这一步。
3. 登录设备这一关:从 NET_DVR_Init 到 NET_DVR_Login_V40
登录是所有后续功能的前置,但刚上手时最容易把返回值、错误码和句柄混在一起。登录成功拿到的是一个句柄,不是布尔值;登录失败要看错误码,不能只盯着返回值小于零。
3.1 初始化的顺序与超时设置
最小登录代码先把整个链路跑通:
#include "NetSDK.h" #include <stdio.h> #include <string.h> int main(void) { NET_DVR_Init(); NET_DVR_SetConnectTime(5000, 1); NET_DVR_USER_LOGIN_INFO login = {0}; NET_DVR_DEVICEINFO_V40 devInfo = {0}; strcpy(login.sDeviceAddress, "192.168.10.101"); login.wPort = 8000; strcpy(login.sUserName, "admin"); strcpy(login.sPassword, "admin123"); login.bUseAsynLogin = 0; LONG userId = NET_DVR_Login_V40(&login, &devInfo); if (userId < 0) { printf("login failed, errcode = %d\n", NET_DVR_GetLastError()); NET_DVR_Cleanup(); return -1; } printf("device login ok, chans = %d\n", devInfo.struDeviceV30.byChanNum); NET_DVR_Logout(userId); NET_DVR_Cleanup(); return 0; }NET_DVR_Init 初始化整个 SDK 上下文,一个进程调用一次即可,不要反复 Init 和 Cleanup,否则会在报警回调和预览句柄上留下无效状态。SetConnectTime 第一个参数是连接超时毫秒数,5000 是多数局域网设备的合理值,第二个参数是重试次数;网络环境差可以调到 8000,但不要无限等。如果是无人值守的服务器,建议开启自动重连机制,否则设备重启一次,程序就再也回不来了。
3.2 USER_LOGIN_INFO 字段逐个说
登录参数集中在 NET_DVR_USER_LOGIN_INFO 结构体里,几个关键字段如下:
| 字段 | 含义 | 注意点 |
|---|---|---|
| sDeviceAddress | 设备 IP 或域名 | 直接填 IP 最稳,省去 DNS 解析 |
| wPort | 私有协议端口 | 默认 8000,不是 RTSP 的 554 |
| sUserName | 设备用户名 | 提前在设备端创建,别用默认账户 |
| sPassword | 设备密码 | 注意设备端锁定策略 |
| bUseAsynLogin | 同步/异步登录 | 0 同步,1 异步 |
同步登录返回时结果已经确定,适合绝大多数后台服务;异步登录需要注册回调,调用后立即返回,结果靠回调通知,状态机更复杂,刚开始不建议用。输出参数 NET_DVR_DEVICEINFO_V40 里最有用的字段是通道数:byChanNum 是模拟通道个数,byIPChanNum 是 IP 通道个数。新一代设备往往模拟通道为 0,有效信号源都在 IP 通道里,实际可用通道总数要按两者之和来算。还有一个隐蔽的坑:代码里通道号从 0 开始,设备端 Web 界面往往从 1 开始显示,换算错了会取到空流或者报通道号错误。
返回值 userId 是登录句柄,不是错误码。小于 0 才是失败,具体原因必须用 NET_DVR_GetLastError() 查。常见错误码里,7 是网络连接失败,10 是密码错误,11 是用户不存在,17 是用户被锁定。错误码 7 很迷惑人:设备 IP 明明 ping 得通,登录却报 7。原因多半是私有协议端口被防火墙拦了,或者设备端禁用了 SDK 服务。验证方法很简单:
telnet 192.168.10.101 8000telnet 能通,至少说明私有协议端口是开的;不通,去设备端查服务和防火墙策略。这个动作能省下很多排查时间。
3.3 错误码表与 C++ 链接细节
把常用错误码单独拎出来记,后续排查会快很多:
| 错误码 | 含义 | 处理方向 |
|---|---|---|
| 7 | 网络连接失败 | 查端口、防火墙、设备服务开关 |
| 10 | 密码错误 | 核对用户凭据 |
| 11 | 用户不存在 | 在设备端新建用户 |
| 17 | 用户被锁定 | 等待解锁或设备端重置 |
C++ 工程里有个链接细节:SDK 头文件按 C 接口设计,直接用 g++ 编译会出现找不到符号的链接错误。处理方式是在代码里包一层:
extern "C" { #include "NetSDK.h" }不要试图去改 SDK 自带的头文件,维护成本高,下次升级就覆盖了。版本兼容也要留意:同一个大版本的 SDK 升级后,如果混用旧头文件和新动态库,结构体长度不匹配,登录行为会非常奇怪。遇到这类问题,第一反应是重新拷贝完整头文件再编译,而不是逐行比对差异。
4. 实时取流回调:数据从回调线程到解码器
登录只是拿到句柄,真正干活的是取流阶段。实时流通过注册回调函数把数据主动推给业务层,这个机制的代价是回调运行在 SDK 内部线程里,处理方式直接决定画面是否流畅。
4.1 RealPlay_V40 的预览参数
登录之后,注册一路实时预览:
static void CALLBACK preview_cb(LONG lRealHandle, DWORD dwDataType, BYTE *pBuffer, DWORD dwBufSize, void *pUser); NET_DVR_PREVIEWINFO preview = {0}; preview.lChannel = 0; preview.dwStreamType = NET_DVR_MAIN_STREAM; preview.dwLinkMode = 0; preview.hPlayWnd = NULL; preview.bBlocked = 1; LONG realHandle = NET_DVR_RealPlay_V40(userId, &preview, preview_cb, (void *)&ctx); if (realHandle < 0) { printf("realplay failed, errcode = %d\n", NET_DVR_GetLastError()); return -1; }参数含义拆开看:
| 参数 | 取值 | 说明 |
|---|---|---|
| lChannel | 0 起始 | 对应总通道索引,不是设备端界面显示号 |
| dwStreamType | 主码流/子码流 | 主码流画质好,子码流省带宽 |
| dwLinkMode | TCP/UDP/多播 | 默认 TCP 最稳 |
| hPlayWnd | NULL | Linux 纯取流场景传 NULL |
| bBlocked | 1/0 | 1 阻塞直到连接建立,0 立即返回 |
主码流和子码流的选择取决于场景:只做画面预览或移动侦测,子码流足够,能省不少带宽和 CPU;画面细节要用于算法分析,用主码流更可靠。低延迟视频分析也建议主码流加 TCP。UDP 延迟理论更低,但引入乱序和丢包,通用做法不推荐。
4.2 回调数据的切帧与搬运
回调函数里先区分数据类型,再搬运数据:
void CALLBACK preview_cb(LONG lRealHandle, DWORD dwDataType, BYTE *pBuffer, DWORD dwBufSize, void *pUser) { switch (dwDataType) { case NET_DVR_SYSHEAD: // 头数据:SPS/PPS 等,接解码器时需要使用 save_decoder_head(pBuffer, dwBufSize); break; case NET_DVR_STREAMDATA: // 一帧 H.264 或 H.265 数据,前面 4 字节是帧长度 enqueue_video_frame(pBuffer + 4, dwBufSize - 4); break; case NET_DVR_AUDIO: // 音频数据,按需处理 break; default: break; } }回调线程是 SDK 内部线程,pBuffer 指向的内存只在本次回调内有效,不能长期持有指针,必须立刻复制到自己的缓冲区。视频帧的格式是每一帧以 4 字节大端长度开头,后面的字节才是完整的 NAL 数据;连续两帧之间没有其他分隔符,解析时必须严格按照这个长度字段切帧。
回调里只做复制和入队,一旦在回调里做磁盘写入或解码,画面会被拖垮。我见过有人在回调里直接写 JPEG,结果实时流卡成一秒一帧。正确的做法是回调只入队,让工作线程去消费:
void *worker_thread(void *arg) { for (;;) { frame_t *f = queue_pop(&g_queue, 1000); if (f) { save_or_decode(f); free(f); } } }这里要提醒一句:frame 建议用内存池或动态分配,避免在回调里频繁 malloc/free,造成锁竞争和内存碎片。回调返回太慢时,SDK 内部缓冲满了会自动丢帧,表现就是花屏或停顿。
4.3 抓图的两种姿势
实时流建立之后才能抓图,常用的是阻塞抓图:
int ret = NET_DVR_CapturePictureBlock(realHandle, NET_DVR_CAPTURE_JPEG, "/tmp/snapshot.jpg"); if (ret != 0) { printf("capture failed, errcode = %d\n", NET_DVR_GetLastError()); }阻塞抓图适合“按一次快门取一张图”的场景,第二个参数是 JPEG 编码,也可以换成 BMP;第三个参数是保存路径,SDK 内部会等待完整图像写入后返回。刚打开实时流立刻抓图,大概率抓到黑屏,原因是没有等到首帧关键帧。处理方式是先订阅流,收到第一帧数据后再触发抓图。
另一种是回调抓图,适合定时巡检场景,SDK 在收到图像后把数据抛给回调。图片数据同样是临时内存,需要复制。选哪种取决于业务:需要主动抓就阻塞式,需要持续收集图片就回调式。抓图接口在不同版本里名称略有差异,但底层逻辑一致。
5. 避坑与排查:编译到运行的四条血泪记录
以下几条都是实际开发中反复出现的典型问题,按“现象-原因-解决”整理出来。
5.1 现象:链接时报 undefined reference
编译时主功能库已经在链接参数里,但还是报一堆 undefined reference。
原因:动态库之间的顺序错了。主功能库依赖基础运行库,系统库充当最底层,必须按主库在前、依赖库在后的顺序排列。
解决:调整 LDFLAGS,把 -lnetsdk 放在最前,-lhpr、-lpthread 依次放在后面。顺序正确后重新 make,问题一般会消失。如果换了顺序还报错,用 nm -D 看一下主库的符号表,确认需要的符号确实在库文件里。
5.2 现象:设备 IP 能 ping 通,登录还是返回错误码 7
设备 IP 同一网段,ping 也通,TLS 或者其他业务端口也通,登录就是报网络失败。
原因:私有协议端口 8000 没有连通,或者设备端禁用了 SDK 服务。ping 测的是 ICMP,应用层端口是否开放是另一回事。
解决:用 telnet 直接探私有协议端口,照 3.2 里的命令执行。端口不通时检查部署环境防火墙,确认放行对应端口;端口通但登录依然失败,去设备端 Web 管理页检查服务开关和用户权限。
5.3 现象:回调里做处理就卡死,画面一停一停
把回调收到的数据直接写文件,或者直接调用解码库,结果画面开始卡顿。
原因:回调阻塞在耗时操作上,SDK 内部缓冲被占满,后续帧只能丢弃。
解决:回调里只复制数据和入队,所有耗时操作放到独立工作线程里。手动队列的方式参考 4.2,入队和出队之间做好加锁或使用无锁队列。只要回调函数能在几毫秒内返回,画面基本不会断层。
5.4 现象:程序退出时偶发崩溃
进程在结束阶段偶尔段错误,不是每次都复现,很难定位。
原因:释放顺序反了。先调 NET_DVR_Cleanup 再注销句柄,相当于把地基拆了再拆房子,遗留的取流线程还在访问已经清理的资源。
解决:退出顺序固定为四步:先停止取流句柄,再 Logout 登录句柄,然后清空并销毁数据队列,最后调用 NET_DVR_Cleanup。这个顺序可以作为统一的退出函数模板,每次都走同一套流程。
6. 进阶:把 H.264 码流接进 FFmpeg 解码链路
取到码流只是第一步,要让画面真正出来,一般会接 FFmpeg。FFmpeg 的 H.264 解码器能直接吃带长度前缀的封装格式,但很多自定义分析链路和部分解码库更习惯 Annex-B 起始码格式,转换是个常见动作。
static const uint8_t start_code[4] = {0, 0, 0, 1}; // 把一帧 NAL(不带长度前缀)转成带起始码的 Annex-B size_t to_annexb(const uint8_t *nal, size_t nal_len, uint8_t *out) { memcpy(out, start_code, 4); memcpy(out + 4, nal, nal_len); return nal_len + 4; }转换逻辑很简单:去掉回调数据里每个 NAL 前面的 4 字节长度,换成 4 字节起始码。处理完的包填入 AVPacket,再走 avcodec_send_packet 和 avcodec_receive_frame 的标准链路。注意头数据要单独保存,来自 NET_DVR_SYSHEAD 回调的 SPS/PPS 填充到解码器的 extradata 字段,解码器初始化时先设置好,这样每一帧就不需要重复携带头信息。
验证链路时,可以把保存下来的裸流用 ffprobe 扫一遍:
ffprobe -v error -show_streams output.h264重点看分辨率、编码 profile 是否和设备端配置一致。输出 Unknown 时,先检查起始码转换是否正确,再看 extradata 有没有填对。
最后说一个习惯问题。我后来每次接入新版本的 SDK,都会强制走一遍固定动作:解包、ldd 验证、最小工程登录、回调取流、存文件验证。这五步做完再写业务代码,很多玄学问题其实不是厂商代码的锅,而是链路里某个环节没验证。这套流程尤其适合手里刚拿到 SDK 离线包又需要快速出结果的人,照着走能少走好几步弯路。希望这份整理能帮到你。
本文还有配套的精品资源,点击获取