☰
Linux下海康SDK与QT集成:实时预览与抓图Demo实战
2026/10/16 3:32:19 网站建设 项目流程

简介:这份资源是面向Linux平台监控视频开发者的Qt示例工程源码,适合具备C++基础、希望将海康摄像头接入自研客户端或做系统集成的技术人员。它围绕网络取流、视频解码、画面显示与用户交互等环节给出可运行参考,帮助解决Linux下监控视频播放与设备接入的落地问题。压缩包共220个文件,约1.2MB,以62个h头文件、58个cpp源文件、51个ui界面文件为主,另有png、bmp等界面素材及pro、pri工程配置,整体结构完整,便于按模块阅读与二次开发。源码涉及RTSP等网络通信、Qt多媒体播放、H.264解码、多线程处理、设备发现配置以及错误日志等要点,并包含登录、实时预览、录像回放等典型功能界面。目前已有1152人学习下载,可作为搭建Linux监控客户端、理解取流与播放链路的实用起点。

1. 从一台工控机说起:这个 QT demo 到底能跑出什么画面

前阵子帮朋友调一台 Linux 工控机上的监控预览程序,需求很朴素:接上海康的 IPC,把实时画面显示在 QT 界面上,能切换通道、能抓图、能断线重连。听起来不难,但真上手才发现,海康 SDK 在 Linux 下的编译、库依赖、回调线程模型,每一步都能让人卡半天。这份「Linux 下海康监控视频 QT demo 源码」解决的正是这个场景——它把海康设备网络 SDK 的实时预览、抓图、云台控制等能力,封装进一个可编译运行的 QT 工程里,适合做安防客户端、NVR 预览工具、工业视觉上位机的开发者直接拿来当起点。你不需要从零啃 SDK 文档,源码里已经把登录、预览、回调、释放这条链路串好了,改改 IP 和端口就能看到画面。

2. 工程结构与技术选型:QT + 海康 SDK 怎么搭在一起

2.1 目录结构长什么样

拿到源码先别急着编译,花五分钟把目录扫一遍,后面改代码能省很多事。这类 demo 的典型结构大致如下(不同版本可能略有出入,但核心文件跑不掉):

HikQtDemo/ ├── HikQtDemo.pro # qmake 工程文件,声明 QT 模块和 SDK 库路径 ├── main.cpp # 程序入口,初始化 QT 应用 ├── mainwindow.h/.cpp/.ui # 主窗口,负责 UI 布局和按钮事件 ├── hikvision/ │ ├── HCNetSDK.h # 海康 SDK 头文件 │ ├── libhcnetsdk.so # 核心网络库 │ ├── libhpr.so # 播放库 │ ├── libPlayCtrl.so # 回放控制库 │ └── HCNetSDKCom/ # SDK 组件目录,运行时必须能找到 ├── include/ │ └── hikmanager.h # 对 SDK 的二次封装类声明 ├── src/ │ └── hikmanager.cpp # 登录、预览、抓图、登出的具体实现 └── res/ └── logo.png # 界面资源

hikvision/目录是整个工程的命脉,海康的.so和HCNetSDKCom/组件目录必须一起带上,缺一个都会在NET_DVR_Init阶段直接返回失败。hikmanager这一层是作者加的封装,把 SDK 的 C 接口包成 C++ 类,主窗口只跟这个类打交道,不直接碰NET_DVR_*函数,这个分层思路值得保留。

2.2 技术选型:为什么是 QT 而不是别的

选 QT 做监控客户端有几个现实理由。第一,海康 SDK 的回调是独立线程,QT 的信号槽机制天然适合把回调数据跨线程丢回 UI 线程刷新,不用自己写锁和条件变量。第二,QT 的QLabel配合QImage可以直接把解码后的 YUV 或 RGB 数据画到界面上,比 MFC 省事。第三,Linux 下 QT 的部署相对干净,qmake或cmake都能管。

工程文件里通常这样声明依赖:

# HikQtDemo.pro 关键片段 QT += core gui widgets TARGET = HikQtDemo TEMPLATE = app # 海康 SDK 头文件路径 INCLUDEPATH += $$PWD/hikvision # 链接海康库,注意顺序:hcnetsdk 依赖 hpr 和 PlayCtrl LIBS += -L$$PWD/hikvision -lhcnetsdk -lhpr -lPlayCtrl # 运行时库搜索路径,保证程序能找到 .so QMAKE_RPATHDIR += $$PWD/hikvision

QMAKE_RPATHDIR这行很关键。很多人编译过了,一运行就报error while loading shared libraries: libhcnetsdk.so,就是因为没把 SDK 目录写进运行时搜索路径。加上这行,或者手动设LD_LIBRARY_PATH,二选一。

2.3 SDK 初始化与资源释放的配对

海康 SDK 有个铁律:NET_DVR_Init和NET_DVR_Cleanup必须配对,登录和登出必须配对。demo 里一般把初始化放在main.cpp或主窗口构造函数里:

// main.cpp 中的初始化片段 #include "HCNetSDK.h" int main(int argc, char *argv[]) { QApplication a(argc, argv); // SDK 初始化,整个进程只调一次 if (!NET_DVR_Init()) { qDebug() << "NET_DVR_Init failed, error:" << NET_DVR_GetLastError(); return -1; } // 设置连接超时和重连参数(单位毫秒) NET_DVR_SetConnectTime(2000, 1); NET_DVR_SetReconnect(10000, true); MainWindow w; w.show(); int ret = a.exec(); // 退出前清理,否则下次启动可能端口占用 NET_DVR_Cleanup(); return ret; }

NET_DVR_SetConnectTime(2000, 1)表示单次连接超时 2 秒、重试 1 次;NET_DVR_SetReconnect(10000, true)表示断线后每 10 秒尝试重连,第二个参数true开启自动重连。这两个参数不设,默认值在弱网环境下会让人等到怀疑人生。NET_DVR_Cleanup放在a.exec()返回之后,保证 UI 关闭后才释放 SDK 资源。

3. 核心模块拆解:登录、预览、回调、抓图一条龙

3.1 设备登录:NET_DVR_Login_V40 的参数怎么填

登录是第一步,demo 里通常封装成HikManager::login():

// hikmanager.cpp 登录实现 bool HikManager::login(const QString &ip, quint16 port, const QString &user, const QString &pwd) { NET_DVR_USER_LOGIN_INFO loginInfo = {0}; NET_DVR_DEVICEINFO_V40 deviceInfo = {0}; // 设备 IP,注意是 char* 不是 QString 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; m_userId = NET_DVR_Login_V40(&loginInfo, &deviceInfo); if (m_userId < 0) { qDebug() << "login failed, error:" << NET_DVR_GetLastError(); return false; } // 保存通道起始号,NVR 和 IPC 这里不一样 m_startChan = deviceInfo.struDeviceV30.byStartChan; m_chanNum = deviceInfo.struDeviceV30.byChanNum; return true; }

几个容易翻车的点:sDeviceAddress是定长字符数组,用strncpy而不是直接赋值;bUseAsynLogin = false走同步模式,登录结果直接看返回值,异步模式要等回调,demo 里一般用同步更直观;byStartChan是通道起始编号,IPC 通常是 1,NVR 可能是 33 或 1,预览时通道号填错就是黑屏。登录成功后拿到的m_userId是后续所有操作的句柄,登出时必须用它调NET_DVR_Logout。

3.2 实时预览:NET_DVR_RealPlay_V40 与回调注册

预览是 demo 的核心。海康的预览接口把码流通过回调吐出来,你在回调里解码并显示:

// 开始预览 bool HikManager::startPreview(int chan, QLabel *renderLabel) { NET_DVR_PREVIEWINFO previewInfo = {0}; previewInfo.lChannel = chan; // 通道号 previewInfo.dwStreamType = 0; // 0-主码流 1-子码流 previewInfo.dwLinkMode = 0; // 0-TCP 1-UDP 2-多播 previewInfo.bBlocked = 1; // 阻塞取流 previewInfo.hPlayWnd = nullptr; // 不用 SDK 窗口,自己渲染 m_renderLabel = renderLabel; // 注册回调,realDataCallBack 是静态函数或全局函数 m_realHandle = NET_DVR_RealPlay_V40( m_userId, &previewInfo, realDataCallBack, this); if (m_realHandle < 0) { qDebug() << "realplay failed, error:" << NET_DVR_GetLastError(); return false; } return true; }

dwStreamType选主码流还是子码流,直接影响带宽和清晰度。本地预览用子码流(1)更流畅,需要抓高清图时切主码流(0)。hPlayWnd = nullptr表示不用 SDK 自带的播放窗口,而是把码流数据回调出来自己解码渲染,这样才能嵌进 QT 的界面布局里。realDataCallBack是回调函数指针,海康要求它是全局函数或静态成员函数,所以this指针通过最后一个参数传进去,在回调里再转回来。

3.3 回调里解码:YUV 转 RGB 再刷到 QLabel

回调拿到的是码流,需要解码成图像。demo 里常见两种做法:用海康的PlayCtrl库解码,或者用 FFmpeg。用PlayCtrl更省事,因为 SDK 已经带了这个库:

// 回调函数:海康 SDK 在独立线程里调用它 void CALLBACK realDataCallBack(LONG handle, DWORD dataType, BYTE *pBuffer, DWORD bufSize, void *pUser) { HikManager *self = static_cast<HikManager *>(pUser); if (!self || dataType != NET_DVR_SYSHEAD && dataType != NET_DVR_STREAMDATA) { return; } // 系统头:初始化解码器 if (dataType == NET_DVR_SYSHEAD) { PlayM4_GetPort(&self->m_playPort); PlayM4_SetStreamOpenMode(self->m_playPort, STREAME_REALTIME); PlayM4_OpenStream(self->m_playPort, pBuffer, bufSize, 1024 * 1024); PlayM4_SetDecCallBack(self->m_playPort, decodeCallBack); PlayM4_Play(self->m_playPort, nullptr); } else { // 码流数据:喂给解码器 PlayM4_InputData(self->m_playPort, pBuffer, bufSize); } }

NET_DVR_SYSHEAD是码流头,只来一次,用来初始化解码通道;NET_DVR_STREAMDATA是后续的码流数据,持续喂给PlayM4_InputData。PlayM4_SetDecCallBack注册解码回调,解码后的 YUV 数据在那里出来。注意PlayM4_GetPort拿到的m_playPort是解码通道号,释放时要PlayM4_Stop+PlayM4_CloseStream+PlayM4_FreePort,三步缺一不可,否则反复开关预览会泄漏解码通道。

解码回调里把 YUV420 转成 RGB,再构造QImage:

void CALLBACK decodeCallBack(LONG port, char *pBuf, LONG size, FRAME_INFO *frameInfo, void *pUser) { HikManager *self = static_cast<HikManager *>(pUser); if (frameInfo->nType != T_YV12) return; // YUV420 -> RGB888 QImage img(frameInfo->nWidth, frameInfo->nHeight, QImage::Format_RGB888); // 这里调用转换函数,把 pBuf 里的 YUV 数据填进 img yuv420ToRgb(pBuf, img.bits(), frameInfo->nWidth, frameInfo->nHeight); // 跨线程刷新 UI,必须用信号槽 emit self->frameReady(img); }

T_YV12是海康解码输出的常见格式,YUV420 的一种排列。转换函数可以自己写,也可以用libyuv。最关键的是最后一行:回调在 SDK 的线程里,直接操作QLabel会崩,必须通过emit信号让 QT 把QImage投递到 UI 线程。这是新手最容易踩的坑,没有之一。

3.4 抓图与云台:两个高频扩展点

抓图一般用NET_DVR_CaptureJPEGPicture,直接存成 JPEG:

bool HikManager::capture(const QString &savePath, int chan) { // 文件名必须是 char*,且路径要可写 QByteArray path = savePath.toLocal8Bit(); if (!NET_DVR_CaptureJPEGPicture(m_userId, chan, &jpegPara, path.data())) { qDebug() << "capture failed, error:" << NET_DVR_GetLastError(); return false; } return true; }

jpegPara是NET_DVR_JPEGPARA结构,里面设图片质量和分辨率。抓图走的是主码流,如果当前预览用的是子码流,抓出来的图会偏小,需要先切主码流或单独开一路主码流预览。

云台控制用NET_DVR_PTZControlWithSpeed:

// 开始转动:command 如 TILT_UP、PAN_LEFT,speed 1-7 NET_DVR_PTZControlWithSpeed(m_userId, chan, TILT_UP, 0, 4); // 停止转动:同一 command,最后一个参数传 1 NET_DVR_PTZControlWithSpeed(m_userId, chan, TILT_UP, 1, 4);

云台控制必须成对调用,开始和停止用同一个命令字,停止时把dwStop参数设为 1。只发开始不发停止,镜头会一直转到底。

4. 编译运行与避坑:那些让人抓狂的报错

4.1 从零编译的完整步骤

假设你在一台 Ubuntu 机器上,源码已经解压到~/HikQtDemo:

# 1. 确认 QT 环境,qmake 能跑 qmake -v # 2. 进入工程目录 cd ~/HikQtDemo # 3. 生成 Makefile qmake HikQtDemo.pro # 4. 编译,-j4 按 CPU 核数调整 make -j4 # 5. 运行时指定库路径(如果 .pro 里没写 RPATHDIR) export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:~/HikQtDemo/hikvision # 6. 运行 ./HikQtDemo

如果第 4 步报找不到HCNetSDK.h,检查.pro里的INCLUDEPATH是否指向了正确的hikvision目录。如果第 6 步报libhcnetsdk.so: cannot open shared object file,就是LD_LIBRARY_PATH没设对,或者.pro里的QMAKE_RPATHDIR路径写错了。

4.2 常见问题排查:现象、原因、解决

现象一:NET_DVR_Init返回失败,错误码 1 或 2。原因通常是HCNetSDKCom/组件目录没跟.so放在一起,或者当前用户对 SDK 目录没有读权限。解决:确认hikvision/HCNetSDKCom/存在且里面有lib*文件,chmod -R 755 hikvision/。

现象二:登录成功但预览黑屏,错误码 0 或没有错误。原因多半是通道号填错。IPC 的通道号从 1 开始,NVR 的通道号从byStartChan开始,可能是 33。解决:登录后把deviceInfo.struDeviceV30.byStartChan打印出来,预览时用这个值加偏移。

现象三:程序跑一会儿崩溃,堆栈指向 QLabel 的 setPixmap。原因是在 SDK 回调线程里直接操作了 UI。解决:回调里只做数据转换,通过emit信号把QImage发到主线程,槽函数里再setPixmap。信号槽连接用Qt::QueuedConnection确保跨线程安全。

现象四:反复开关预览后,解码通道耗尽,新预览打不开。原因是PlayM4的解码通道没释放。解决:停止预览时按顺序调NET_DVR_StopRealPlay、PlayM4_Stop、PlayM4_CloseStream、PlayM4_FreePort,一个都不能少。

现象五:程序退出后再次启动,登录失败。原因是上次退出时没调NET_DVR_Logout和NET_DVR_Cleanup,设备端还占着会话。解决:在窗口的closeEvent里先登出再清理,或者用QObject::connect(qApp, &QApplication::aboutToQuit, ...)挂清理逻辑。

注意:海康 SDK 的错误码用NET_DVR_GetLastError()获取,每个失败分支都要打印,否则排查时就是黑匣子。

5. 二次开发改造:从 demo 到能用的客户端

5.1 多路预览的改造思路

demo 通常只开一路预览,实际项目往往要 4 路、9 路甚至 16 路。改造的核心是把HikManager从单例改成每路一个实例,或者用一个管理器持有多个m_userId和m_realHandle。每路预览对应一个独立的PlayM4解码通道和一个QLabel。回调里的pUser参数就是用来区分是哪一路的,把每路的上下文结构体传进去即可。

一个实用的做法是定义一个PreviewChannel结构:

struct PreviewChannel { LONG userId; // 登录句柄 LONG realHandle; // 预览句柄 LONG playPort; // 解码通道 int chan; // 设备通道号 QLabel *label; // 渲染目标 };

每个通道独立管理生命周期,开关互不影响。这样改完,从 1 路扩到 16 路只是循环创建的问题。

5.2 断线重连与状态回调

海康 SDK 支持异常回调NET_DVR_SetExceptionCallBack_V30,断线、重连成功都会触发:

// 注册异常回调 NET_DVR_SetExceptionCallBack_V30(0, nullptr, exceptionCallBack, this); void CALLBACK exceptionCallBack(DWORD type, LONG userId, void *pUser) { HikManager *self = static_cast<HikManager *>(pUser); switch (type) { case EXCEPTION_RECONNECT: // 重连成功 emit self->reconnected(); break; case EXCEPTION_ALARMRECONNECT: // 报警重连 break; default: break; } }

配合NET_DVR_SetReconnect(10000, true),SDK 会自动尝试重连,重连成功后触发EXCEPTION_RECONNECT。你只需要在这个回调里重新开始预览,因为重连后原来的realHandle可能已经失效。我一般会在重连回调里先NET_DVR_StopRealPlay再重新NET_DVR_RealPlay_V40,虽然 SDK 文档说重连后预览会自动恢复,但实测中手动重建更稳。

5.3 一个容易忽略的细节:时间戳和 OSD

监控画面通常要叠加时间戳和设备名。海康 SDK 支持在码流里叠加 OSD,用NET_DVR_SetDVRConfig配置NET_DVR_SET_OSDCFG。但更灵活的做法是在 QT 端自己画:在QLabel上再叠一层QPainter,把时间戳画上去。这样不依赖设备配置,换设备也不用重新设。

// 在显示帧的槽函数里叠加时间戳 void MainWindow::onFrameReady(const QImage &img) { QImage frame = img.copy(); QPainter painter(&frame); painter.setPen(Qt::yellow); painter.setFont(QFont("monospace", 14)); painter.drawText(10, 30, QDateTime::currentDateTime() .toString("yyyy-MM-dd HH:mm:ss")); ui->videoLabel->setPixmap(QPixmap::fromImage(frame)); }

img.copy()是必须的,因为回调传来的QImage可能在下一次回调时被复用,不拷贝直接画会出现画面撕裂。这个细节我在项目里翻过车,画面偶尔闪一下旧帧,查了半天才发现是没拷贝。

5.4 验证改造是否成功的清单

改完之后别急着交付,按这几条过一遍:连续开关预览 50 次,看解码通道有没有泄漏;拔网线等 30 秒再插上,看是否自动重连并恢复画面;同时开 4 路预览跑 2 小时,看内存是否稳定;抓图 100 次,看文件是否都能正常打开。这几条过了,基本就能拿去用了。

从那以后我每次接海康 SDK 的项目,都强制先把NET_DVR_Init、登录、预览、回调、释放这条最小链路跑通,再往上加功能。demo 源码的价值不在于它多完整,而在于它把这条链路给你串好了,你只需要在它基础上改。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询