简介:这是一份面向Linux平台C++开发者的海康威视SDK集成实战资源,聚焦在Qt框架下完成设备连接、实时视频流获取与云台控制三大核心功能,适合具备一定C++与网络编程基础、正在开发网络监控类应用的工程师参考。压缩包共36个文件,约10.34MB,以23个so动态库和4个h头文件为主体,涵盖海康SDK运行库与接口声明,另有2个cpp源文件、1个ui界面文件及CMakeLists等工程配置,构成可直接编译运行的完整示例工程。内容涉及设备搜索、通道打开、码流回调、PTZ控制命令调用,以及多线程处理与错误日志等关键环节,能帮助读者快速理清SDK接口调用顺序与依赖库链接方式。目前已有2768人学习下载,可作为Linux下海康SDK二次开发的起步模板与排错参照。
1. 从一台没有图形界面的工控机说起:这套 QT + 海康 SDK 方案到底能干什么
很多做安防集成的开发者都遇到过这种场景:现场是一台跑着 Linux 的工控机,没有桌面环境,但你需要它稳定地拉取海康摄像机的实时视频流,同时还要能控制云台转动、调焦、设置预置位。用浏览器插件?不现实。用厂商自带的客户端?那是 Windows 的活儿。这时候,QT + C++ 调用海康 SDK 就成了最务实的路线。
这份资源要解决的,就是 Linux 环境下用 QT 写一个能取流、能控云台的 C++ 程序。它适合两类人:一是刚接触海康 SDK、需要在 Linux 上快速跑通取流流程的开发者;二是已经能取流、但云台控制部分总是调不通、想看看别人怎么组织代码的熟手。核心链路其实就三步:初始化 SDK、登录设备、取流并渲染,云台控制则是在登录成功后通过透传接口下发 PTZ 命令。听起来简单,但 Linux 下的库依赖、回调线程、渲染方式,每一步都有坑。
2. 环境搭建与 SDK 接入:把海康的 .so 库塞进 QT 工程
2.1 Linux 下海康 SDK 的目录结构与依赖关系
海康威视为 Linux 提供的 SDK 通常是一个压缩包,解压后你会看到类似这样的结构:inc/放头文件,lib/放动态库,demo/放示例。头文件里最核心的是HCNetSDK.h,它定义了设备登录、取流、云台控制的所有接口。动态库方面,libhcnetsdk.so是主库,libHCCore.so是核心依赖库,另外还有libPlayCtrl.so用于流媒体播放、libSuperRender.so用于渲染。这些库之间有依赖顺序,加载时如果顺序不对,会出现符号找不到的错误。
我一般会把 SDK 的inc和lib目录直接复制到 QT 工程根目录下,建一个thirdparty/hikvision/的文件夹,里面再分include和lib。这样做的好处是工程自包含,换一台机器编译不用重新配路径。在.pro文件里这样写:
# 海康 SDK 头文件路径 INCLUDEPATH += $$PWD/thirdparty/hikvision/include # 链接库路径和具体库名 LIBS += -L$$PWD/thirdparty/hikvision/lib \ -lhcnetsdk \ -lHCCore \ -lPlayCtrl \ -lSuperRender # 运行时库搜索路径,避免部署时找不到 .so QMAKE_RPATHDIR += $$PWD/thirdparty/hikvision/lib这里有个细节:QMAKE_RPATHDIR会把库路径写进可执行文件的 RPATH 段,程序运行时动态链接器会优先去这个路径找.so。如果不加这一行,编译能过,但一运行就报error while loading shared libraries: libhcnetsdk.so: cannot open shared object file。这是 Linux 下调用第三方 SDK 最常见的翻车点,没有之一。
2.2 在 QT 工程中初始化 SDK 与登录设备
SDK 的初始化必须在使用任何其他接口之前完成,而且整个进程只需要初始化一次。我习惯把它放在main.cpp里,或者一个单例的HikvisionManager类的构造函数中。登录设备需要设备的 IP、端口、用户名和密码,返回一个LONG类型的用户句柄,后续所有操作都要带着这个句柄。
#include "HCNetSDK.h" // 全局用户句柄,登录成功后赋值 LONG g_lUserID = -1; bool initAndLogin(const QString &ip, quint16 port, const QString &user, const QString &pwd) { // 1. 初始化 SDK,整个进程只调一次 if (!NET_DVR_Init()) { qDebug() << "NET_DVR_Init failed, error:" << NET_DVR_GetLastError(); return false; } // 2. 设置连接超时和重连参数(可选但推荐) NET_DVR_SetConnectTime(2000, 1); NET_DVR_SetReconnect(10000, true); // 3. 填充登录参数结构体 NET_DVR_USER_LOGIN_INFO loginInfo = {0}; strncpy(loginInfo.sDeviceAddress, ip.toStdString().c_str(), sizeof(loginInfo.sDeviceAddress) - 1); loginInfo.wPort = port; strncpy(loginInfo.sUserName, user.toStdString().c_str(), sizeof(loginInfo.sUserName) - 1); strncpy(loginInfo.sPassword, pwd.toStdString().c_str(), sizeof(loginInfo.sPassword) - 1); loginInfo.bUseAsynLogin = false; // 同步登录,方便错误处理 // 4. 设备信息结构体,登录成功后会被填充 NET_DVR_DEVICEINFO_V40 deviceInfo = {0}; g_lUserID = NET_DVR_Login_V40(&loginInfo, &deviceInfo); if (g_lUserID < 0) { qDebug() << "Login failed, error:" << NET_DVR_GetLastError(); NET_DVR_Cleanup(); return false; } qDebug() << "Login success, userID:" << g_lUserID; return true; }NET_DVR_Login_V40是较新的登录接口,兼容 V3 和 V4 的设备信息结构。bUseAsynLogin设为false表示同步登录,函数返回时登录结果已经确定,这样错误处理更直观。如果设为true,你需要通过回调函数来接收登录结果,代码会复杂不少。对于大多数取流场景,同步登录足够了。
NET_DVR_SetConnectTime的第一个参数是超时时间,单位毫秒;第二个参数是尝试次数。NET_DVR_SetReconnect的第一个参数是重连间隔,单位毫秒;第二个参数表示是否启用断线重连。这两个接口在长时间运行的取流程序里几乎是必调的,否则网络抖动一下,流就断了,程序却不知道。
3. 取流与渲染:从 NET_DVR_RealPlay_V40 到 QT 窗口显示
3.1 实时预览接口的调用与回调数据解析
取流的核心接口是NET_DVR_RealPlay_V40。它需要一个预览参数结构体和一个回调函数。回调函数会在 SDK 内部线程中被调用,每次收到一帧数据就触发一次。这里的关键是:回调函数里不能做耗时操作,否则会阻塞 SDK 的取流线程,导致丢帧甚至断流。
// 预览参数结构体 NET_DVR_PREVIEWINFO previewInfo = {0}; previewInfo.lChannel = 1; // 通道号,通常从 1 开始 previewInfo.dwStreamType = 0; // 0-主码流,1-子码流 previewInfo.dwLinkMode = 0; // 0-TCP,1-UDP,2-多播 previewInfo.bBlocked = 1; // 阻塞取流,保证不丢帧 previewInfo.hPlayWnd = nullptr; // 不用 SDK 自带窗口,自己渲染 // 实时预览句柄 LONG g_lRealPlayHandle = -1; // 回调函数:SDK 线程中调用,只做数据拷贝,不做渲染 void CALLBACK realDataCallback(LONG lRealHandle, DWORD dwDataType, BYTE *pBuffer, DWORD dwBufSize, void *pUser) { switch (dwDataType) { case NET_DVR_SYSHEAD: // 系统头,包含流媒体信息,一般用于初始化解码器 break; case NET_DVR_STREAMDATA: // 码流数据,这里把数据推给解码队列 if (dwBufSize > 0) { // 注意:pBuffer 在回调返回后会被 SDK 回收 // 必须拷贝出来再异步处理 QByteArray frame((const char *)pBuffer, dwBufSize); // 通过信号槽投递到主线程,避免跨线程操作 UI emit HikvisionManager::instance()->frameReceived(frame); } break; default: break; } } // 开始取流 g_lRealPlayHandle = NET_DVR_RealPlay_V40(g_lUserID, &previewInfo, realDataCallback, nullptr); if (g_lRealPlayHandle < 0) { qDebug() << "RealPlay failed, error:" << NET_DVR_GetLastError(); }dwStreamType选主码流还是子码流,取决于你的用途。主码流分辨率高、码率大,适合本地显示或录像;子码流分辨率低、码率小,适合网络传输或远程预览。如果只是做云台控制附带看看画面,子码流就够了,能省不少带宽。
回调函数里的pBuffer指针在回调返回后就会被 SDK 回收,所以绝对不能直接把指针传出去异步使用。我见过有人把pBuffer存到一个队列里,然后在另一个线程里慢慢处理,结果画面全是花屏或者程序直接崩溃。正确的做法是在回调里立刻拷贝数据,然后通过 QT 的信号槽机制投递到主线程。
3.2 用 QT 的 QLabel 或 QOpenGLWidget 渲染视频帧
海康 SDK 取到的码流是压缩数据(H.264 或 H.265),不能直接显示,需要先解码。SDK 提供了PlayM4系列接口来做解码,解码后的数据是 YUV 或 RGB,再交给 QT 渲染。如果你不想用 PlayM4,也可以把码流交给 FFmpeg 解码,但那样就多了一个依赖。
用 PlayM4 解码的流程大致是:先调用PlayM4_GetPort获取一个播放端口,然后PlayM4_SetStreamOpenMode设置流模式,接着PlayM4_OpenStream打开流,最后在回调里用PlayM4_InputData喂数据。解码后的图像通过PlayM4_SetDecCallBack设置的回调出来,格式通常是 YV12。
// 解码回调:拿到 YV12 数据,转成 QImage 显示 void CALLBACK decCallback(LONG nPort, char *pBuf, LONG nSize, FRAME_INFO *pFrameInfo, void *pUser) { if (pFrameInfo->nType != T_YV12) return; int width = pFrameInfo->nWidth; int height = pFrameInfo->nHeight; // YV12 转 RGB888,这里用 QT 的 QImage 做转换 QImage img(width, height, QImage::Format_RGB888); // ... 逐像素转换,或者用 libyuv 加速 // 转换完成后通过信号槽发给 UI 线程 emit HikvisionManager::instance()->imageReady(img); }逐像素转换在 1080P 下会吃掉不少 CPU,如果对性能有要求,建议引入 libyuv 库做 SIMD 加速。不过对于一路流、分辨率不高的场景,QT 自带的转换也够用。渲染端我一般用QOpenGLWidget,把QImage转成纹理上传到 GPU,这样主线程的负担会小很多。如果只是简单显示,QLabel::setPixmap也能凑合,但帧率高的时候会卡。
注意:PlayM4 的解码回调也是在 SDK 线程中触发的,同样不能在里面做 UI 操作。所有跨线程的 UI 更新都必须通过信号槽或者
QMetaObject::invokeMethod投递到主线程。
4. 云台控制:PTZ 指令下发与预置位管理
4.1 NET_DVR_PTZControl 系列接口的参数含义
云台控制的核心接口是NET_DVR_PTZControlWithSpeed_Other或NET_DVR_PTZControl_Other。前者支持速度参数,后者只支持启停。以NET_DVR_PTZControlWithSpeed_Other为例,它的参数依次是:用户句柄、通道号、PTZ 命令、启停标志、速度。
// PTZ 命令常量(部分常用) // PAN_LEFT 向左 // PAN_RIGHT 向右 // TILT_UP 向上 // TILT_DOWN 向下 // ZOOM_IN 变倍+ // ZOOM_OUT 变倍- // UP_LEFT 左上 // UP_RIGHT 右上 // 开始向左转动,速度 4(范围 1-7) NET_DVR_PTZControlWithSpeed_Other(g_lUserID, 1, PAN_LEFT, 0, 4); // 停止向左转动 NET_DVR_PTZControlWithSpeed_Other(g_lUserID, 1, PAN_LEFT, 1, 4);第三个参数是 PTZ 命令,海康的头文件里定义了几十个常量,覆盖了上下左右、变倍、聚焦、光圈、预置位等所有操作。第四个参数是启停标志,0表示开始,1表示停止。第五个参数是速度,范围通常是 1 到 7,数值越大转得越快。
这里有个容易踩的坑:如果你调用了开始转动,但忘记调用停止,云台会一直转下去,直到碰到限位或者你手动发停止命令。我一般会在 UI 上做按钮的pressed和released信号绑定,按下时发开始命令,松开时发停止命令。如果是键盘控制,也要在按键释放时发停止。
4.2 预置位设置、调用与巡航的代码实现
预置位是云台控制里最实用的功能之一。设置预置位就是把当前云台的位置记下来,给一个编号;调用预置位就是让云台转到那个编号对应的位置。海康 SDK 里用NET_DVR_PTZPreset_Other接口来实现。
// 设置预置位:把当前位置保存为预置位 1 NET_DVR_PTZPreset_Other(g_lUserID, 1, SET_PRESET, 1); // 调用预置位:让云台转到预置位 1 NET_DVR_PTZPreset_Other(g_lUserID, 1, GOTO_PRESET, 1); // 删除预置位 1 NET_DVR_PTZPreset_Other(g_lUserID, 1, CLE_PRESET, 1);SET_PRESET、GOTO_PRESET、CLE_PRESET这三个常量在头文件里都有定义。预置位编号的范围取决于设备型号,一般支持 1 到 255 个。设置预置位之前,你需要先用 PTZ 控制接口把云台转到想要的位置,然后再调SET_PRESET。
巡航功能稍微复杂一点,需要先设置巡航路径(把若干个预置位串起来),然后启动巡航。海康 SDK 提供了NET_DVR_PTZCruise_Other接口,操作类型包括FILL_PRE_SEQ(添加预置位到巡航路径)、SET_SEQ(设置巡航路径参数)、RUN_SEQ(开始巡航)、STOP_SEQ(停止巡航)。这部分代码量不大,但参数比较多,建议直接参考 SDK 自带的 demo。
提示:云台控制命令下发后,设备需要一点时间响应。如果你连续快速发送多个命令,可能会出现命令丢失或执行顺序错乱。我一般会在命令之间加一个 50 到 100 毫秒的间隔,或者用队列串行下发。
5. 避坑与排查:Linux 下调用海康 SDK 的五个血泪教训
5.1 动态库加载失败:error while loading shared libraries
现象:程序编译通过,但一运行就报error while loading shared libraries: libhcnetsdk.so: cannot open shared object file。
原因:Linux 的动态链接器在运行时找不到.so文件。编译时的-L只告诉链接器去哪里找库,但不会把路径写进可执行文件。运行时链接器会去默认路径(如/usr/lib、/lib)和LD_LIBRARY_PATH环境变量指定的路径找。
解决:在.pro里加QMAKE_RPATHDIR += $$PWD/thirdparty/hikvision/lib,把库路径写进 RPATH。或者部署时设置LD_LIBRARY_PATH环境变量。我推荐前者,因为 RPATH 是编译期确定的,部署时不用额外配环境变量。
5.2 回调线程崩溃:在 SDK 回调里操作 UI
现象:程序运行一段时间后随机崩溃,崩溃栈指向 QT 的 UI 相关函数。
原因:SDK 的回调函数是在 SDK 内部线程中调用的,不是 QT 的主线程。在非主线程中操作 QT 的 UI 组件(如QLabel::setPixmap)会导致未定义行为,轻则界面卡死,重则程序崩溃。
解决:回调函数里只做数据拷贝,然后通过emit信号把数据发出去,在 UI 线程的槽函数里做渲染。如果不想用信号槽,也可以用QMetaObject::invokeMethod配合Qt::QueuedConnection投递到主线程。
5.3 取流花屏或卡顿:回调里做了耗时操作
现象:视频画面花屏、卡顿,或者取流一段时间后自动断开。
原因:SDK 的取流回调是在一个固定大小的缓冲区上工作的,如果回调函数执行时间过长,缓冲区会被填满,导致丢帧或断流。在回调里做解码、格式转换、文件写入等操作,都会拖慢回调速度。
解决:回调里只做内存拷贝,把数据放到一个线程安全的队列里,由另一个线程慢慢处理。如果队列满了,宁可丢帧也不要阻塞回调。我一般会用一个固定大小的环形缓冲区,满了就覆盖最旧的数据。
5.4 云台命令无响应:通道号和命令参数搞错
现象:调用 PTZ 控制接口返回成功,但云台没有任何动作。
原因:最常见的是通道号传错。海康设备的通道号从 1 开始,但有些接口需要传 0 表示当前通道。另外,部分 PTZ 命令需要设备支持,如果设备不支持该命令,接口可能返回成功但实际不执行。
解决:先用NET_DVR_GetDeviceAbility查询设备支持的能力集,确认 PTZ 功能可用。通道号统一从 1 开始试,如果不行再试 0。另外,检查登录时使用的用户是否有 PTZ 控制权限,只读用户是无法控制云台的。
5.5 程序退出时崩溃:忘记清理 SDK 资源
现象:关闭程序时崩溃,或者程序退出后设备仍然认为连接存在。
原因:没有调用NET_DVR_StopRealPlay停止取流,没有调用NET_DVR_Logout登出设备,没有调用NET_DVR_Cleanup清理 SDK。这些资源不释放,SDK 内部状态会残留,下次启动可能登录失败。
解决:在程序退出前,按顺序调用:先停止所有取流,再登出所有设备,最后调用NET_DVR_Cleanup。我一般会把这些清理逻辑放在QCoreApplication::aboutToQuit信号的槽函数里,确保无论从哪个路径退出都会执行。
6. 二次开发改造:从单路取流到多路管理与配置持久化
如果你已经跑通了单路取流和云台控制,下一步大概率是要做多路管理。我自己的做法是抽象一个HikCamera类,每个实例对应一路摄像机,内部持有自己的用户句柄、取流句柄和解码端口。这样多路之间互不干扰,增删摄像机只需要创建或销毁实例。
class HikCamera : public QObject { Q_OBJECT public: explicit HikCamera(QObject *parent = nullptr); ~HikCamera(); bool login(const QString &ip, quint16 port, const QString &user, const QString &pwd); void logout(); bool startPreview(int channel, int streamType); void stopPreview(); bool ptzControl(DWORD command, DWORD stop, DWORD speed); signals: void imageReady(const QImage &img); void errorOccurred(const QString &msg); private: LONG m_userID = -1; LONG m_realPlayHandle = -1; LONG m_decPort = -1; // ... 其他成员 };每个HikCamera实例在析构时自动清理自己的资源,这样上层代码不用关心 SDK 的细节。多路管理就是用一个QList<HikCamera*>或者QMap<QString, HikCamera*>来持有这些实例。
配置持久化方面,我一般用QSettings把摄像机的 IP、端口、用户名、密码、通道号存到 ini 文件里。密码不能明文存,至少做个简单的加密或者用系统的密钥环。启动时读取配置,自动登录所有摄像机并开始取流。如果某一路登录失败,记录日志并继续下一路,不要因为一路失败就整个程序退出。
还有一个实用技巧:把 PTZ 控制做成一个独立的PtzController类,接收HikCamera的指针,封装所有云台操作。这样 UI 层只需要调用ptzController->panLeft()这样的方法,不用关心底层的 SDK 接口。预置位的管理也可以放在这个类里,用一个QMap<int, QString>来维护预置位编号和名称的映射,方便 UI 显示。
从那以后我每次写涉及 SDK 回调的代码,都会强制走一遍「回调里只拷贝、不处理」的检查,不管多简单的场景都不例外。希望帮到你。
本文还有配套的精品资源,点击获取