ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

海康威视 SDK 在 Qt 中的实时预览实现:句柄模式与回调模式解析

2026/9/16 1:38:35 拓冰建站 浏览量
海康威视 SDK 在 Qt 中的实时预览实现:句柄模式与回调模式解析 简介面向视频监控与安防应用开发者的海康威视摄像头预览Demo基于Qt框架编写解决在Windows桌面端快速实现摄像头实时预览与播放的入门问题。资源共55个文件压缩包约9.08MB包含HCNetSDK、HCPreview等dll动态库与配套lib导入库可直接运行的exe程序以及cpp/h源码、Qt工程文件、UI设计文件既支持直接运行体验也便于对照源码进行二次开发。已有244人学习/下载。通过该Demo可清晰理解Qt图形视图框架与多媒体模块如何协同工作掌握海康设备登录、取流预览、画面渲染等关键流程工程中保留了Debug/Release配置和Makefile还附带sdkLog等调试信息有助于快速搭建自己的视频监控客户端并为后续接入录像回放、语音对讲等功能打下基础。1. 海康威视 Qt 预览 Demo 为什么值得自己重建一遍海康威视官方 SDK 里的预览示例Windows 下几乎清一色是 MFC 加原生 Win32 窗口。真要把预览功能搬进 Qt 程序时绝大多数问题不出在取流上而出在窗口句柄和线程上Qt 的 QWidget 并不保证一直持有稳定的 HWND而海康播放库却要拿这个 HWND 当画布两边默认行为互相打架。这个 QtPreviewDemoTest 类的资源包通常解压后就是 SDK 根目录、播放库、几个 VC 工程和可执行文件直接打开的 exe 能出画面换成自己的 Qt 工程就黑屏或崩溃。所以这里把这类 Demo 拆成 SDK 初始化、设备登录、RealPlay 取流、播放库解码、Qt 控件渲染五个环节按一线接入的常规做法走一遍。适合刚接手设备客户端、被“官方 Demo 正常、自己 Qt 工程不出图”卡住的人也适合想确认回调模式与 HWND 模式边界的老手。2. 初始化 HCNetSDKSDK 目录、Qt 工程链接与网络参数2.1 把 SDK 目录整理成 Qt 能直接引用的结构拿到海康威视 SDK 压缩包后不要整个文件夹拖进 Qt 工程源码树。Demo 里的路径往往是“D 盘某个 VC 工程下的相对路径”直接引用会让工程换台机器就断。常见的做法是在工程目录外放一个3rdparty/HCNetSDK保留 SDK 原始结构升级时整体替换目录代码不动。3rdparty/HCNetSDK/ ├── include/ │ ├── HCNetSDK.h │ └── PlayCtrl.h ├── lib/ │ ├── HCNetSDK.lib │ ├── PlayCtrl.lib │ └── AudioRender.lib ├── doc/ └── demo/工程里只引用 include 和 lib 两层。把 doc 和 demo 留在外面是因为 Demo 工程一般自带 MFC 依赖混进 Qt 工程后反而干扰编译。播放库的 PlayCtrl.h 与 HCNetSDK.h 是两套头文件前者管解码和渲染后者管设备登录与取流后面会看到它们如何配合。2.2 用 .pro 链接 HCNetSDK.lib 和 PlayCtrl.lib避开 MinGW 陷阱在 Qt 的 .pro 文件里加两行注意路径分隔符用正斜杠INCLUDEPATH $$PWD/../3rdparty/HCNetSDK/include LIBS $$PWD/../3rdparty/HCNetSDK/lib/HCNetSDK.lib \ $$PWD/../3rdparty/HCNetSDK/lib/PlayCtrl.lib$$PWD是当前工程文件目录这样路径不会因为你把工程拷到别处而失效。链接后运行程序前记得把 HCNetSDK.dll、PlayCtrl.dll 以及它们依赖的 hlog.dll、hpr.dll 等拷到 exe 所在目录或加到 PATH。漏掉依赖的典型症状是程序启动直接报0xc000007b这个错在 Win32 下几乎都是 DLL 位数不匹配或依赖缺失。这里有个容易踩的坑HCNetSDK.lib 是 MSVC 格式的 COFF 导入库MinGW 套件直接链会报找不到库或符号错。两个解法一是换用 MSVC 的 Qt 套件这是最省时间的路径二是用 LoadLibrary 动态加载 HCNetSDK.dll再用 GetProcAddress 取函数指针官方 Demo 里一般不这么写但 Qt 的 CMake 工程里很常见。如果只是做预览测试直接上 MSVC 套件。2.3 NET_DVR_Init 之后的三个网络参数决定重连体验初始化代码很短但参数直接影响后面预览的稳定性#include HCNetSDK.h // 程序启动时调用一次 NET_DVR_Init(); // 连接超时 2 秒最多尝试 1 次 NET_DVR_SetConnectTime(2000, 1); // 10 秒间隔自动重连 NET_DVR_SetReconnect(10000, true);NET_DVR_Init只需要在进程生命周期里调用一次模块卸载前由NET_DVR_Cleanup收尾。NET_DVR_SetConnectTime的两个参数分别是等待毫秒数和尝试次数局域网内 2000 毫秒够用跨交换机或 Wi-Fi 环境建议放到 3000 到 5000 毫秒否则设备响应稍慢就直接登录失败。NET_DVR_SetReconnect是设备链路层的自动重连第一个参数是重连间隔毫秒数第二个参数是否启用注意它保证的是 TCP 链路恢复不保证 RealPlay 画面自动恢复这个问题留到最后一章处理。这三个接口在不同 SDK 版本里行为几乎一致但文档位置可能不同。如果你手上的 SDK 包比较老没有NET_DVR_SetReconnect可以把重连逻辑放到业务层用定时器周期检测登录状态。3. 登录设备并启动实时预览NET_DVR_Login_V40 与 NET_DVR_RealPlay_V403.1 NET_DVR_USER_LOGIN_INFO 的填写与返回通道计数预览前必须先登录设备。海康 SDK 从 6.x 开始主推NET_DVR_Login_V40它用一个结构体统一了 IP 地址、端口、用户名、密码和异步开关NET_DVR_USER_LOGIN_INFO loginInfo {0}; NET_DVR_DEVICEINFO_V40 deviceInfo {0}; snprintf(loginInfo.sDeviceAddress, sizeof(loginInfo.sDeviceAddress), 192.168.1.64); loginInfo.wPort 8000; snprintf(loginInfo.sUserName, sizeof(loginInfo.sUserName), admin); snprintf(loginInfo.sPassword, sizeof(loginInfo.sPassword), your_password); loginInfo.bUseAsynLogin false; LONG lUserID NET_DVR_Login_V40(loginInfo, deviceInfo); if (lUserID 0) { qCritical() login failed, error code: NET_DVR_GetLastError(); return; }登录默认端口是 8000不是 RTSP 的 554这是很多人第一次接 SDK 时最容易搞混的。bUseAsynLogin置 false 表示同步登录函数返回时登录流程已经结束置 true 时需要通过回调或消息知道结果Demo 测试用同步就够了。登录成功后返回的lUserID是后续所有操作的大前提等于一个会话句柄。deviceInfo里有几个字段对预览很关键byChanNum是模拟通道数byIPChanNum是 IP 通道数。新出的 NVR 和 IPC 基本以 IP 通道为主预览通道号从 1 开始数但混合型 DVR既有模拟又有 IP的通道号规则在不同型号上不完全一致常见做法是先按byIPChanNum循环 1 到 N 试一遍不行再尝试byChanNum 32这类偏移量。官方 Demo 里这层判断写得比较绕实际测试时直接打印两个值再拿设备 Web 页面比对通道列表最直接。3.2 NET_DVR_PREVIEWINFO 里影响流畅度的字段登录成功后调用NET_DVR_RealPlay_V40启动实时预览。这个接口的参数是NET_DVR_PREVIEWINFO不同字段组合出来的效果差异很大字段常用值说明hPlayWnd控件 winId渲染窗口句柄回调模式下可传 NULLlChannel1 起通道号与设备通道列表对应dwStreamType0 / 10 主码流1 子码流dwLinkMode00 TCP1 UDP2 多播bBlockedtruetrue 阻塞到出流false 立即返回NET_DVR_PREVIEWINFO previewInfo {0}; previewInfo.hPlayWnd (HWND)ui-videoWidget-winId(); previewInfo.lChannel 1; previewInfo.dwStreamType 0; // 主码流清晰优先 previewInfo.dwLinkMode 0; // TCP 最稳 previewInfo.bBlocked true; // 等首帧出来再继续 LONG lRealHandle NET_DVR_RealPlay_V40(lUserID, previewInfo, NULL, NULL); if (lRealHandle 0) { qDebug() RealPlay failed: NET_DVR_GetLastError(); }多画面预览时建议用子码流dwStreamType 1铺满所有窗口单画面放大时再切成主码流这是设备端客户端的通用做法。带宽充足时也可以全部主码流但 16 路以上的画面轮巡容易把设备取流能力打满导致部分窗口黑屏。dwLinkMode在局域网内 TCP 足够UDP 延迟略低但弱网下花屏更明显。bBlocked设为 true 时NET_DVR_RealPlay_V40会阻塞到收到码流才返回UI 线程里调用会卡一下但好处是返回值拿到时画面已经准备好了。3.3 用 REAL_DATA_CALLBACK 把码流送进播放库当hPlayWnd传 NULL 时预览数据不会自动渲染而是回调给你。回调里拿到的是编码后的 H.264/H.265 码流不能直接上屏需要交给海康播放库解码void CALLBACK RealDataCallback(LONG lRealHandle, DWORD dwDataType, BYTE* pBuffer, DWORD dwBufSize, void* pUser) { // pUser 透传的是播放库端口号 int nPort *static_castint*(pUser); switch (dwDataType) { case NET_DVR_SYSHEAD: // 设备重连后会重新收到系统头先复位再打开 PlayM4_CloseStream(nPort); PlayM4_SetStreamOpenMode(nPort, STREAME_REALTIME); PlayM4_OpenStream(nPort, pBuffer, dwBufSize, 1024 * 1024); PlayM4_Play(nPort, NULL); break; case NET_DVR_STREAMDATA: PlayM4_InputData(nPort, pBuffer, dwBufSize); break; default: break; } }pUser在启动预览时传入回调线程里会一直持有所以不能传栈变量。播放库端口在登录后先申请PlayM4_GetPort(nPort)。NET_DVR_SYSHEAD是关键标识它代表一个完整码流的起始如果在PlayM4_OpenStream之前就收到NET_DVR_STREAMDATA直接丢弃即可。设备断电重启后SDK 重连成功会再次推送系统头这时必须先PlayM4_CloseStream再重新打开否则画面卡在最后一帧。播放库常量STREAME_REALTIME的值在不同版本的 PlayCtrl.h 里有差异但名字一直没变代码里直接写常量名不要写数字。用NET_DVR_RealPlay_V40启动预览时第三个回调参数传RealDataCallback第四个参数传端口指针LONG lRealHandle NET_DVR_RealPlay_V40(lUserID, previewInfo, RealDataCallback, nPort);这样回调与渲染分离取流在 SDK 回调线程解码在播放库内部线程渲染再交给 Qt三件事不会阻塞 UI。4. 在 Qt 界面上渲染视频帧HWND 句柄模式与 YUV 回调模式4.1 句柄模式把 QWidget 的 winId 交给 hPlayWnd最省事的做法是句柄模式即 3.2 里直接传hPlayWnd。播放库会在你给的 HWND 上创建子窗口渲染画面Qt 那边什么都不用画。但两个前提必须满足控件要先show()再调winId()否则拿到的句柄无效控件必须保持原生窗口属性防止句柄被 Qt 重建。// 构造时强制原生窗口避免后续 setParent/改 flags 导致句柄重建 ui-videoWidget-setAttribute(Qt::WA_NativeWindow, true); ui-videoWidget-show(); (void)ui-videoWidget-winId(); // 触发创建确保后面取到有效值句柄模式下只要 HWND 不失效画面就一直存在。容易出问题的是把videoWidget放进 QScrollArea 或 QStackedWidgetQt 在布局变化时可能销毁并重建原生句柄表现出来就是画面区域黑掉但程序不崩。解决办法有两个一是坚持WA_NativeWindow并避免在运行时替换父控件二是干脆放弃句柄模式走回调。4.2 回调模式YV12 数据转 QImage 并跨线程上屏回调模式完全绕开 HWND。在 3.3 的基础上给播放库注册解码回调PlayM4_SetDecCallBackEx(nPort, DecCBFun, NULL, 0); PlayM4_Play(nPort, NULL); // 播放窗口传 NULL只拿解码数据解码回调里拿到的是 YV12 原始帧。YV12 的平面布局是 Y 平面、V 平面、U 平面依次排列注意这个顺序和 I420 不一样。很多人在这一步直接把数据包成QImage::Format_YUV420P结果颜色发紫——QImage 的 YUV420P 期望 I420 排序两者 U/V 顺序正好相反。稳妥做法是转成 RGB32 再上屏void CALLBACK DecCBFun(int nPort, char* pBuf, int nSize, FRAME_INFO* pFrameInfo, void* pUser) { if (!pFrameInfo || pFrameInfo-nType ! T_YV12) return; int w pFrameInfo-nWidth; int h pFrameInfo-nHeight; const uchar* yPlane reinterpret_castconst uchar*(pBuf); const uchar* vPlane yPlane w * h; const uchar* uPlane vPlane (w * h) / 4; QImage img(w, h, QImage::Format_RGB32); for (int y 0; y h; y) { QRgb* line reinterpret_castQRgb*(img.scanLine(y)); for (int x 0; x w; x) { int Y yPlane[y * w x]; int U uPlane[(y / 2) * (w / 2) x / 2] - 128; int V vPlane[(y / 2) * (w / 2) x / 2] - 128; int R qBound(0, (int)(Y 1.14 * V), 255); int G qBound(0, (int)(Y - 0.394 * U - 0.581 * V), 255); int B qBound(0, (int)(Y 2.03 * U), 255); line[x] qRgb(R, G, B); } } VideoWidget* wgt static_castVideoWidget*(pUser); emit wgt-frameReady(img); // 跨线程信号槽 }逐像素转换在 1080P 下较慢压测不行时换 libyuv效果好得多libyuv::YV12ToARGB(yPlane, w, uPlane, w / 2, vPlane, w / 2, argbBuffer, w * 4, w, h);注意pUser。转换后的 QImage 是深拷贝不再依赖播放库缓冲可以安全发给 UI 线程。信号槽连接建议用显式 QueuedConnectionconnect(videoWidget, VideoWidget::frameReady, videoWidget, VideoWidget::showFrame, Qt::QueuedConnection);解码回调在播放库线程里发信号接收对象在 UI 线程。AutoConnection 在跨线程发射时会自动转成队列连接但显式写出来更清楚。播放库软解帧率通常在 25 到 30 帧每帧一个 QImage 信号队列里积压的帧不会造成内存无限增长因为 QImage 是引用计数emit 出去后原缓冲可回收。4.3 两种渲染方式对照比较点句柄模式回调解码模式实现成本低几行代码中多一层转换HWND 依赖强句柄重建即黑屏无UI 叠加业务不方便子窗口覆盖自由绘制高 DPI 缩放画面模糊或错位无影响截图播放库截图接口直接拿 QImage16 路多画面一个控件一个句柄开销大单 QImage 合并绘制实际项目里单画面快速验证用句柄模式介入成本最低要做带业务浮层、轮巡、缩略图墙的客户端回调模式是最终归宿尤其在 Qt 6 默认启用高 DPI 缩放的背景下。5. 收尾与进阶预览关闭顺序、掉线重连与画面自适应5.1 按依赖关系关闭 SDK 与播放库避免退出崩溃关闭顺序的规则是“谁后启动谁先关闭”。先停 RealPlay再停播放库然后注销登录最后清理 SDKNET_DVR_StopRealPlay(lRealHandle); PlayM4_Stop(nPort); PlayM4_CloseStream(nPort); PlayM4_FreePort(nPort); NET_DVR_Logout(lUserID); NET_DVR_Cleanup();如果反着来NET_DVR_Cleanup会杀掉 SDK 内部线程此时播放库还可能在等待输入数据典型表现是退出时崩溃或程序卡在 DLL 卸载。另外SDK 回调线程里的数据不能再触发任何 UI 操作否则在窗口销毁后还会收到 frameReady 信号。5.2 帧计数看门狗SDK 自动重连之外的兜底NET_DVR_SetReconnect只负责设备链路画面不一定自动恢复。常见做法是维护一个“最近一次收到解码帧”的时间戳每隔 1 秒检查一次超过 5 秒没收帧就主动重启预览void PreviewWidget::onTimer() { if (m_lastFrameTick.isValid() m_lastFrameTick.msecsTo(QTime::currentTime()) 5000) { restartPreview(); // NET_DVR_StopRealPlay 重新 RealPlay } }m_lastFrameTick在 DecCBFun 里每次更新。注意设备进入休眠或断网时回调线程也会停所以这个判断不能用“回调被调过”做依据必须是“最近一次时间戳”。5.3 保持宽高比绘制与高 DPI 下的显示区微调回调模式下绘制时保持宽高比很简单算目标矩形再画。想要画面更锐利开SmoothPixmapTransform会有效果但低配机器上 1080P 放大到 2K 会明显增加绘制耗时不建议在轮巡页面开。句柄模式下画面拉伸由播放库控制Qt 端无法干预遇到高 DPI 缩放问题时只能切回调模式这是 QWidget 与原生子窗口之间的固有限制。本文还有配套的精品资源点击获取