简介:在Linux环境中接入海康监控摄像头并实现实时预览、回放与界面控制,是安防集成与嵌入式开发中的常见需求。这份QTdemo源码面向具备C++基础与初步Qt使用经验的开发者,完整演示了基于Qt多媒体模块的RTSP流获取、视频解码、画面显示,以及多线程调度、设备参数配置、错误处理与日志记录等关键环节,可直接作为二次开发的基础工程。资源共220个文件,以cpp、h源文件为主,配以ui界面布局、qrc资源定义、pri工程配置及bmp/jpg等界面素材,工程结构对应实时预览、回放、通道与远端参数设置等模块;压缩包整体仅1.2MB,轻量但覆盖度较高。已有1152人学习,适合希望快速搭建海康监控视频Demo,或在此基础上定制监控系统的开发人员参考。
1. 只做一件事:把海康监控视频在 Linux Qt 里流畅跑起来
在 Linux 下用 Qt 开发海康监控视频客户端,很多人第一反应是去官网找 SDK,但 SDK 自带 Demo 是标准 C 控制台程序,没有 Qt 界面参考。这份 Linux 下海康监控视频 QTdemo 源码正好补上这一环:登录设备、实时取流、画面显示、抓图和录像,整条链路都有参考实现。它解决的核心问题不是单点功能,而是把 SDK 回调线程和 Qt 事件循环连起来的工程化套路。适合在做工控软件、安防桌面客户端或嵌入式上位机的工程师,尤其适合第一次碰海康 SDK 的开发者——看完你能少走很多弯路。
2. 搭环境的正确姿势:SDK 目录、依赖库与 .pro 配置
2.1 先分清这三类文件
海康 Linux SDK 解压出来,目录里常见的几类文件要分清楚:头文件(HCNetSDK.h、HCNetSDKCom.h 等)、动态库(.so)以及示例程序。很多翻车就是把这几类文件混在一起,以为头文件拷到工程目录、库拷到系统目录就能编过。
动态库这边,常见是 libhcnetsdk.so、libhccore.so、libhpr.so 这几个,另外有些版本还会依赖系统的 openssl 库。libhcnetsdk.so 是主库,libhccore.so 是底层封装,libhpr.so 负责日志或网络相关功能。它们之间有依赖关系,只拷 libhcnetsdk.so 是不行的,运行时会直接报找不到 libhccore.so。
我一般会把整个 SDK 的 lib 目录原样拷到工程外的固定路径,比如 /opt/hik/libs,不放到源码目录里,这样既方便换 SDK 版本,也避免仓库被几十 MB 的二进制文件撑大。头文件路径也要注意,主头文件是 HCNetSDK.h,但实际编译时会发现它 include 了 HCNetSDKCom.h 和 Platform.h,所以 INCLUDEPATH 至少要包含 SDK 根目录和子目录 HCNetSDK 两层。有的版本把类型定义放在 HCNetSDK.h 里,有的拆成多个文件,最稳妥的办法是把 SDK 解压目录整体加进 INCLUDEPATH,让 include 自己去找。
2.2 .pro 的写法与路径陷阱
Qt 工程要编译,先看 .pro 文件。我常用的一套配置是这样:
QT += core gui network greaterThan(QT_MAJOR_VERSION, 4): QT += widgets TARGET = monitor_demo TEMPLATE = app DEFINES += _GLIBCXX_USE_CXX11_ABI=0 INCLUDEPATH += /opt/hik/HCNetSDK \ /opt/hik/HCNetSDK/HCNetSDK LIBS += -L/opt/hik/HCNetSDK \ -lhcnetsdk -lhccore -lhpr SOURCES += main.cpp \ MainWindow.cpp \ PreviewThread.cpp HEADERS += MainWindow.h \ PreviewThread.h这里 DEFINES 里写 _GLIBCXX_USE_CXX11_ABI=0 是为了兼容老编译链。海康 SDK 的动态库很多是用老 ABI 编译的,如果你用的是新版本 GCC,std::string 的内存布局不一样,链接时可能不报错,但运行时会莫名其妙崩溃,这是最隐蔽的坑之一。如果编译器足够新或 SDK 足够新,这个宏可以不加,但一般来说加了更稳。
LIBS 里 -L 指定库路径,-l 指定动态库名。海康的这几个库有些版本用大写 H,有些全是小写,比如 libHCCore.so 和 libhccore.so 实际是同一个文件,在 Linux 下文件名大小写敏感,写错一个字母后面就链接失败。建议在解压目录里用 ls 看一眼实际文件名,再决定 -l 后面写什么。这里顺带说明,SDK 不同版本之间库文件命名有点差异,不要直接照抄别人帖子里的写法。
运行时还有一个问题:编译能过,双击运行却报找不到动态库。这是因为程序运行时会按照 ld.so 的规则去 /usr/lib 或 LD_LIBRARY_PATH 找库。我用两种方式处理:一是给工程配置里加 LD_LIBRARY_PATH 环境变量,指向 /opt/hik/HCNetSDK;二是在安装部署时把一个 LD 配置文件丢到 /etc/ld.so.conf.d/ 下,再执行 ldconfig。开发期用第一种,打包交付用第二种。
2.3 从「最小登录工程」验证环境
拿到源码先别急着跑完整界面,我习惯先验证 SDK 本身是否可用。新建一个空 Qt Console 工程,只写登录和退出,能跑到登录成功再回来改大工程。这样能把环境问题和代码问题分开排查。
如果你不想先建工程,直接在命令行验证依赖更快:
ldd /opt/hik/HCNetSDK/libhcnetsdk.so | grep "not found"如果输出里有 not found,说明缺库。常见的解决办法是在 /etc/ld.so.conf.d/ 下建一个 hik.conf,写入库目录完整路径,然后执行 ldconfig。这一步对后面写安装脚本很有用,省得每台机器都手动 export 环境变量。
还有一种情况是 SDK 本身依赖 libcrypto.so 的特定版本,系统里装了 OpenSSL 1.1 但 SDK 是编在 1.0 上的,这种版本错位的现象在国产化环境下特别常见。解决方式不是硬替换系统库,而是把 SDK 自带的 so 和 libhcnetsdk.so 放在同一目录,让 loader 优先找到它。很多工程在客户机器上翻车都是这个原因。
最小验证工程的 main.cpp 只做三件事:初始化、登录、清理。如果这三步都跑通,说明 SDK 目录、库路径、头文件都没问题,后面接预览就只是线程和界面的问题。如果这里就报错,先回头检查路径和环境变量,不要急着去 GUI 里打断点。我见过不少同学拿到这份源码后直接编译,报错一大堆找不到头文件,然后以为是源码问题,实际上八成是 SDK 路径没设置对。先在命令行里敲 qmake -o Makefile monitor.pro,再用 make 编一次,比在 Qt Creator 里折腾更直观。
3. 登录链路:从 Init 到 Login_V40 的参数取舍
3.1 初始化、超时与重连参数
海康 SDK 的调用顺序并不复杂,但顺序错了会出非常难查的问题。按我自己的经验,进程起来之后第一件事是 NET_DVR_Init,等应用准备退出的时候再 NET_DVR_Cleanup。注意 Init 和 Cleanup 必须成对,而且要在同一进程里,不能在取流线程里初始化再跨线程清理,SDK 内部没有做线程间同步,这种写法迟早崩。
Init 之后有两步可选但建议做:设置连接超时和自动重连。海康 SDK 有 NET_DVR_SetConnectTime 和 NET_DVR_SetReconnect 这两个 API。前者是登录时的 TCP 连接超时,默认值可能偏长,局域网内问题不大,跨网段时建议设短一点,比如 2000ms,避免登录卡住;后者是断线后 SDK 自动重新连接的时间间隔,第二个参数是是否启用,我一般会启用,设 10000ms。
有人会把自动重连当成万能药,其实它只负责网络恢复后的重新连接,不负责会话恢复。如果你的应用在断线期间界面还要显示“设备离线”,最好自己维护一个在线状态,不要完全依赖 SDK 内部状态。设备掉线时 SDK 回调会通知,但回调里不能直接弹窗或刷新界面,只能发信号出去。
3.2 一次能跑通的登录代码
把登录封装成一个函数,先看代码:
#include "HCNetSDK.h" #include <QDebug> #include <cstring> bool loginDevice(LONG &loginId) { NET_DVR_Init(); // 建立连接超时 2000ms,失败后尝试 1 次 NET_DVR_SetConnectTime(2000, 1); // 断线后每 10 秒自动重连一次,启用 NET_DVR_SetReconnect(10000, true); NET_DVR_USER_LOGIN_INFO login = {0}; login.wPort = 8000; qstrcpy(login.sDeviceAddress, "192.168.1.64"); qstrcpy(login.sUserName, "admin"); qstrcpy(login.sPassword, "password123"); NET_DVR_DEVICEINFO_V40 devInfo = {0}; loginId = NET_DVR_Login_V40(&login, &devInfo); if (loginId < 0) { qWarning() << "登录失败,错误码:" << NET_DVR_GetLastError(); NET_DVR_Cleanup(); return false; } qDebug() << "登录成功,模拟通道:" << devInfo.byChanNum << ",数字通道:" << devInfo.byIPChanNum; return true; }参数说明:wPort 是 SDK 连接端口,海康设备默认是 8000,这个端口是设备网络 SDK 专用的,和 web 访问的 80 端口、RTSP 的 554 端口不是一个概念。设备可能开了 RTSP 却不开放 SDK 端口,所以登录前先确认设备网络设置里的 SDK 端口是打开的。sDeviceAddress、sUserName、sPassword 都是 char 数组,用 qstrcpy 前最好确认目标数组足够长,SDK 结构体里一般定义成固定长度,超出会截断或数组越界。
NET_DVR_Login_V40 的第二个参数返回设备信息,byChanNum 是模拟通道数,byIPChanNum 是数字通道数。有些新设备模拟通道是 0,通道号要从数字通道开始算,后面预览时容易踩坑。这段代码里把设备信息打出来,调试时可以一眼看出通道规划。
登录失败时,NET_DVR_GetLastError 返回错误码。我习惯把错误码转成字符串打印出来,方便定位。常见的两个错误码是 7 和 17,含义和处置方式不同:
| 错误码 | 常见原因 | 处置方式 |
|---|---|---|
| 7 | 网络连接失败 | telnet 设备IP 8000 验证端口,检查路由与防火墙 |
| 17 | 用户名或密码错误 | 用设备 web 页面修改密码后重试 |
3.3 登录句柄的正确管理方式
登录返回的 loginId 是后续所有操作的凭证,预览、抓图、云台控制都要用到。这个句柄要注意几点。
第一,多设备场景下,每台设备登录一次,得到不同的 loginId,不要复用。第二,SDK 句柄的有效期与登录会话绑定,网络断开后 SDK 会自动重连,但 loginId 在重连成功后是否仍然有效,取决于设备端 SDK 的实现,不同版本表现不一致。所以我的习惯是:网络异常回调里把 loginId 置为 -1,等重连成功后再重新登录。第三,退出程序的顺序很讲究,先停止预览、再登出(NET_DVR_Logout),最后 Cleanup,顺序反了可能出现资源泄漏或崩溃。
多设备登录时,建议用一张表把 loginId、设备序列号、通道数、在线状态存起来。回调里拿到数据时,先根据 lRealHandle 反查是哪台设备,再决定投递到哪个界面。否则同时接四台摄像机,画面很容易串台。通道规划也要提前做,部分设备通道号不是连续的,模拟通道占 1~4,IP 通道从 33 开始。Demo 源码里可能只用了 1,真正接现场时一定要把通道号做成可配置的,用下拉框或配置文件都行。我一般会在程序启动时读取设备信息,把可用通道列出来。
登录环节还有一个容易被忽略的参数:设备地址。如果设备在跨网段,比如电脑是 192.168.1.x,设备是 192.168.2.x,8000 端口虽然能通,但 SDK 登录却超时。这种情况先检查路由器和设备之间的通信端口是否真的通,不要直接怀疑 SDK。
4. 实时预览:回调线程、YUV 数据与 Qt 显示链路
4.1 两种取流方式,选哪个
登录成功只是第一步,真正复杂的是实时流。海康 SDK 给了两种取流路径:一种是 NET_DVR_RealPlay_V40 时直接把预览窗口句柄传进去,SDK 自己渲染画面;另一种是注册数据回调,把每一帧数据送到你的代码里,由你自己处理显示、录像和算法分析。
窗口句柄直出模式简单,调用代码少,适合做“只要能看到画面”的简易工具。但它有两个问题:一是窗口句柄在不同 Linux 桌面环境(X11/Wayland)下的兼容性不稳定,Qt 里拿到的是 QWidget 的 winId,未必直接可用;二是拿不到帧数据,截图、录像、AI 分析都做不了。所以这份 Qt Demo 里更关键的是回调模式,也是我推荐的模式。
回调模式拿到的数据格式,取决于注册的是哪个回调。NET_DVR_SetRealDataCallBack 拿到的是原始码流(可能是 H.264 或 H.265),需要自己接解码器;NET_DVR_SetStandardDataCallBack 拿到的是解码后的 YUV 数据,常见的是 YV12 或 NV12。对 Demo 来说,用标准数据回调最简单,省掉了解码环节,缺点是如果设备主码流是 H.265 而 SDK 版本较老,软解性能会吃紧。
4.2 预览参数与回调注册代码
预览要放在一个独立线程里,避免阻塞主线程。先看代码:
#include "HCNetSDK.h" #include <QThread> class PreviewThread : public QThread { Q_OBJECT public: PreviewThread(LONG loginId, int channel, QObject *parent = nullptr); void run() override; void stop(); signals: void frameReady(const QByteArray &yuv, int w, int h); void previewError(int errCode); private: LONG m_loginId; int m_channel; volatile bool m_running = true; }; void PreviewThread::run() { NET_DVR_PREVIEWINFO previewInfo = {0}; previewInfo.lChannel = m_channel; previewInfo.dwStreamType = 0; // 0=主码流, 1=子码流 previewInfo.dwLinkMode = 0; // 0=TCP, 1=UDP, 2=多播 previewInfo.byPreviewMode = 0; // 0=实时, 1=延迟, 2=丢帧 LONG realHandle = NET_DVR_RealPlay_V40(m_loginId, &previewInfo, nullptr, nullptr, 0); if (realHandle < 0) { emit previewError(NET_DVR_GetLastError()); return; } NET_DVR_SetStandardDataCallBack(realHandle, dataCallback, this); while (m_running) { msleep(10); } NET_DVR_StopRealPlay(realHandle); }逻辑说明:run 在线程启动后被调用,先配置预览参数,然后调用 RealPlay 建立预览会话,再把回调函数注册进去。回调函数里会频繁被触发,不能做耗时操作。while 循环加 msleep(10) 的意义只是让线程保持存活,真正的数据流不依赖这个线程。
参数说明:lChannel 通道号从 1 开始,很多设备有模拟通道和数字通道两段,如果 DeviceInfo 返回 byChanNum=1 而实际是 IP 摄像机,通道号可能是 33 之类,要看设备实际配置,不能硬编码。dwStreamType 选主码流还是子码流,界面预览用子码流更流畅,录像或识别用主码流更清晰。dwLinkMode 跨网段建议 TCP,局域网可以用 UDP,TCP 更可靠但带宽占用高些,UDP 延迟小,多播只适合在支持组播的局域网里。
回调函数是 C 风格函数指针,没法捕获 C++ lambda,最后一个参数 pUser 是唯一能传递上下文的地方。我一般把线程对象 this 传进去,在回调里 static_cast 回来,再判断帧数据类型、拷贝到内部缓冲区,最后用信号发出去。这个设计是整条链路的核心。理解了它,你就明白回调里不能随便 new 对象——回调是在 SDK 拉流线程里执行的,频繁 new/delete 会导致堆锁竞争,间接拖慢取流。正确做法是预分配一块缓冲区,回调里只做 memcpy。
4.3 从 YV12 到 QImage 的显示技巧
回调里拿到的 YUV 怎么显示,是这个 Demo 最需要替换的部分。不同版本 SDK 返回的数据格式有差异,但 YV12 最常见。YV12 的布局是:一整帧 Y 数据(宽×高),然后是 V 数据(宽×高/4),最后是 U 数据(宽×高/4)。注意顺序不是 U 在前。
一个常见误区是直接把 YUV 数据塞进 QImage,期望 Qt 自己显示,这在新版本 Qt 里并不可靠。我一般不会让 QImage 直接持有 YUV 内存,而是先转成 RGB32。转换公式来自 BT.601 标准,核心循环长这样:
// YV12 -> RGB 的核心循环,这里只展示像素转换 for (int y = 0; y < height; ++y) { for (int x = 0; x < width; ++x) { int Y = planeY[y * width + x]; int V = planeV[(y / 2) * (width / 2) + (x / 2)]; int U = planeU[(y / 2) * (width / 2) + (x / 2)]; int r = Y + 1.402 * (V - 128); int g = Y - 0.344 * (U - 128) - 0.714 * (V - 128); int b = Y + 1.772 * (U - 128); // 将 r/g/b 写入 QImage::Format_RGB32 的扫描线 } }公式顺序容易记混,我习惯先用一张测试图验证偏色方向,再决定要不要交换 U/V。如果你不想手动写转换,给工程加 OpenCV 或者 FFmpeg 都行。Demo 源码为了保持轻量,通常不强制依赖 OpenCV,但实际项目里我一般会引 swscale 来做转换,性能和正确性都有保障。
还有一处要注意:帧率。设备一般 25 帧每秒,如果每帧回调都触发一次 Qt 控件刷新,界面会非常卡。常见做法是把刷新频率限制在 15 到 20 帧,或者回调里只更新 QImage 成员变量,用一个 QTimer 在定时器里统一重绘,不要把回调里的帧队列和 UI 刷新绑成 1:1。这里也可以加一层环形缓冲区,界面永远取最新帧,旧帧直接丢弃,比排队处理更符合监控场景。
视频格式与带宽也要提前算:主码流一般 1080P H.264,码率在 4 到 8 Mbps,子码流在 0.5 到 1 Mbps。如果界面只是用来预览,强烈建议切子码流,回调和 UI 压力都会小很多。需要截图或录像时再临时切回主码流,或者同时开两路预览,一路主码流存盘、一路子码流显示,这也是工程里常见的做法。
5. 常见问题排查:五个让我翻车的细节
5.1 库文件与链接相关的坑
坑一:运行时报 cannot open shared object file
现象:编译成功,启动程序时终端输出 libhccore.so cannot open shared object file,程序直接退出。
检查原因:LD_LIBRARY_PATH 没设置,或者只拷了 libhcnetsdk.so 而没有把配套的 libhccore.so、libhpr.so 一起带过去。
解决:把整个 SDK 库目录设置到 LD_LIBRARY_PATH。如果是打包部署,写一个启动脚本,脚本里先 export LD_LIBRARY_PATH=./libs:$LD_LIBRARY_PATH 再 exec 程序本体,这样现场部署时不会因为环境变量丢失而翻车。
坑二:链接时提示找不到 -lhccore
现象:make 阶段报错,ld 找不到库文件,错误信息类似 cannot find -lhccore。
检查原因:库文件名大小写或版本号对不上,例如实际叫 libhccore.so,但工程里写的是 libHCCore.so。Linux 对大小写敏感,这个不像 Windows 那么宽容。还有一种情况是 SDK 解压后库文件带版本号后缀,比如 libhcnetsdk.so.6,-l 参数对不上。
解决:在库目录里执行 ls -l 看真实文件名,把 -l 参数严格对齐。如果带版本号后缀,建一个不带后缀的软链接即可,比如 ln -s libhcnetsdk.so.6 libhcnetsdk.so,这样 .pro 文件不用大改。
5.2 登录、取流与界面相关的坑
坑三:设备能 ping 通,但登录返回错误码 7
现象:局域网内设备能 ping 通,浏览器网页也能打开,但 SDK 登录失败,错误码是 7。
检查原因:登录用的 8000 端口不通。常见原因是设备启用了 Web 服务但没启用 SDK 服务,或者设备防火墙策略限制了端口,还有一种可能是设备被其他客户端占满了连接数。
解决:用 telnet 设备IP 8000 验证端口通不通。不通就先开设备网络页面里的 SDK 服务开关,再确认路由和防火墙。如果端口通但登录还是失败,检查是否已有其他客户端占用连接,适当调大设备的最大连接数。
坑四:画面全是绿色条纹或颜色偏得离谱
现象:预览能出画面,但整个图像发绿,或者画面颜色像负片,人脸完全没法看。
检查原因:YUV 数据格式处理错了。可能是把 NV12 当 YV12 用了,也可能是 U/V 平面顺序搞反。YV12 的平面顺序是 Y、V、U,I420 是 Y、U、V,NV12 则是 Y 平面加 UV 交错平面,三者布局完全不同。
解决:先确认回调里实际返回的格式,主流是 YV12 或 NV12。验证时打印第一帧的前几个字节,观察 Y 平面之后跟的是 U 还是 V,或者在转换函数里交换 U/V 试试。如果换了颜色就正常,那就是平面顺序问题,不用纠结公式。
坑五:程序运行几分钟后界面卡死,或直接崩溃
现象:预览正常运行,但只要拖动窗口或切换页面就卡死,有时直接段错误。
检查原因:SDK 回调线程里直接操作了 Qt 控件。回调线程不是 Qt 主线程,在回调里调用 label->setPixmap 或 update 会造成跨线程访问 UI,偶尔能跑,跑久了必崩。段错误往往发生在 closeEvent 之后,回调线程还在往已经销毁的控件里写数据。
解决:回调里只拷贝数据到成员变量或缓冲区,然后通过信号发到主线程。关闭窗口时先调用 stop(),等预览线程真正退出后再销毁界面对象。如果用了 Qt 的队列连接,信号参数最好用 QByteArray 或 QImage 的值传递,避免指针悬空。
这五个坑基本覆盖了最常见的翻车场景。前两个是环境问题,后三个是编码问题。环境问题用排查命令解决,编码问题用线程模型解决。把这些解决了,这份源码的核心链路才算真正跑起来。
6. 进阶落地:抓图、录像与线程模型
6.1 抓图与录像的最小实现
预览跑通之后,最常加的两个功能是抓图和录像。海康 SDK 直接给了一个抓图接口,不需要从回调里截帧:
NET_DVR_JPEGPARA jpegPara = {0}; jpegPara.wPicSize = 0; // 0 表示设备默认分辨率 jpegPara.wPicQuality = 0; // 质量等级 0~255 NET_DVR_CaptureJPEG_Picture(m_loginId, channel, &jpegPara, "/tmp/snapshot.jpg");这个接口是设备端抓图,保存的是 JPEG 文件,不占本地 CPU,但实际抓图速度受设备处理能力影响,连续抓图建议间隔 1 秒以上。录像用 NET_DVR_SaveRealData 更省事,传入预览句柄和文件路径,SDK 会把码流直接写文件,不需要自己处理编码和时间戳。但要注意,SaveRealData 保存的是原始码流,不是 MP4,后期播放可能需要转封装工具或者用海康播放器才能正常播放。
6.2 线程模型与云台控制
这份源码真正的价值不在界面多漂亮,而在于线程模型。我的建议是:主线程只管 UI,预览线程负责 SDK 会话,回调线程只做数据搬运,三个角色分工明确。如果要把抓图、录像、云台控制都加进来,也保持同样的原则:任何 SDK 调用都不放在 Qt 的 paintEvent 或按钮槽里直接做长时间阻塞操作。
云台控制接口也比较直接,NET_DVR_PTZControl 加命令码就能实现上下左右和变焦,但注意控制命令要持续发送才能连续转动,松开时发停止命令。这部分 Demo 里不一定写了,但接口是公开的,改起来不难。多通道预览时,每通道一个线程,线程内部再管理自己的回调上下文,不要在多个通道之间共享同一个缓冲区,不然画面会串。
说起来,我前阵子帮某公司的项目做设备接入,客户端跑了一整天,第二天设备掉线后界面直接黑屏,查到最后就是回调里干了重活。从那以后我每次拿到别人的 Qt 监控 Demo,第一件事不是看界面,而是翻回调函数里有没有直接操作控件、有没有在回调里 new 对象,看完再决定要不要改。希望这份源码能帮你把同样的弯路省掉,也希望你第一次跑通预览时,能感受到这种“黑匣子终于打开”的踏实感。希望帮到你。
本文还有配套的精品资源,点击获取