简介:面向海康威视视频监控设备的C++二次开发者,这是一套基于V5.1.1.4 64位Windows中文版SDK的完整工程资源包,内置全套Demo代码,覆盖设备登录、实时预览、录像回放、云台控制、报警处理等典型开发链路。包内共1137个文件,以541个H头文件和501个CPP源文件为核心,同时提供DLL动态库、LIB静态库、PDB调试符号、ICO图标及BMP位图,整体压缩后约32.34MB,目录结构便于按功能模块检索。目前已有207人学习下载,适合作为入门海康SDK调用流程的实操参考。工程中还包含Visual Studio工程文件(dsp/vcproj)、CHM帮助文档和可执行示例,结合示例位图与图标素材,开发者可对照代码理解各接口的参数含义与返回状态,快速将登录、取流、云台控制等能力集成到自有监控平台;利用PDB调试文件和日志,还能有效排查网络、权限及设备兼容性等问题,缩短二次开发周期。
1. HCNetSDKV5.1.1.4_build20150420_WIN64_CN 到底能干什么
这套 SDK 的版本号一看就是安防网络摄像机领域最常见的那套设备接入库,2015 年的 Windows 64 位中文版。很多做物联网、安防平台、边缘网关的工程师,第一次接触设备接入拿到的就是这个包。它能让你用 C/C++ 或 C# 直接搜索局域网内的摄像机、登录设备、拉取实时预览、抓图,以及回放录像。但真正动手时,光看官方 README 远远不够——连接超时时间要自己设,登录结构体要按字节填,播放回调和抓图接口的配合方式也容易翻车。这篇笔记按从拿到压缩包到跑通一个最小取流 Demo 的顺序来写,能搜到设备、能拉出画面,再告诉你老版本 SDK 在 64 位系统下的那些坑。适合负责设备接入、需要在三天内交付一个客户端验证程序的开发者。
2. 拿到 V5.1.1.4_build20150420_WIN64_CN 先做什么:目录、运行库与语言选型
解压这个包之后,不要急着写代码。先把目录结构摸清楚,再把 64 位环境下的动态库依赖处理好。这个版本不像后来的 SDK 一个 NuGet 包全搞定,很多坑都出在手动拷贝 DLL 和路径指错上。
2.1 开发包目录结构与各文件的职责
常见解压后会有下面这些内容,不同渠道拿到的包可能略有差异,但核心文件基本一致。
| 文件名 / 目录 | 职责 | 是否必需 |
|---|---|---|
| HCNetSDK.dll | 主 SDK 动态库,所有设备访问逻辑都通过它 | 必需 |
| HCNetSDK.lib | 导入库,供 Visual C++ 链接 | C++ 必需 |
| HCNetSDK.h | 主头文件,函数和结构体声明 | C++ 必需 |
| PlayCtrl.dll / PlayCtrl.lib | 播放库,用于在窗口上渲染实时视频流 | 做预览必需 |
| HCNetSDKCom/ | 组件目录,内含协议库、加密库等子模块 | 必需,不能删 |
| 其他说明文件 | 版本说明、常用问题等 | 参考 |
拿到包之后,第一件事是检查 HCNetSDKCom 目录是否存在。很多时候为了省空间,只拷贝了主 DLL,运行时会报找不到指定的模块错误,就是缺了这个组件目录。HCNetSDK.dll 会在初始化时动态加载 HCNetSDKCom 下的子库,所以整个目录结构要保持完整,不能只拿一个 DLL 出来。
另外要注意,这个版本的主头文件里,关键接口的typedef大多数已经带上了stdcall调用约定,但少数回调函数的定义还是老的写法。在 C++ 里直接包含头文件问题不大,但如果你是动态加载 DLL,手工 typedef 时很容易把CALLBACK声明错,后面聊到回调时再细说。
2.2 64 位系统的运行库依赖:VC 运行库与 DLL 路径
这个 2015 年的版本编译时依赖的是 VS2013 工具集,对应的运行时是 VC12。在干净的 Windows Server 或精简版 Win10 上,没装 VC++ 运行库会导致LoadLibrary返回空,用GetLastError()查出来是 126(找不到指定的模块)或者 127(找不到指定的程序)。别只盯着 HCNetSDK.dll 是否存在,先装一遍 VS2013 对应的 VC++ Redistributable x64 包,大部分启动崩溃就消失了。
64 位系统下还有一个极易踩的路径坑:很多人把 HCNetSDK.dll 拷贝到C:\Windows\SysWOW64或C:\Windows\System32下,然后发现还是找不到。SysWOW64 是 32 位 DLL 的映射目录,从 64 位进程加载它反而会失败。正确做法是把 DLL 放在你程序的运行目录下,或者用绝对路径调用SetDllDirectory指定到放置的目录。我一般会在程序启动时显式加载一次:
#include <windows.h> bool LoadHCNetsdkDll() { // 先把当前 exe 所在目录加入动态库搜索路径 TCHAR exePath[MAX_PATH] = {0}; GetModuleFileName(NULL, exePath, MAX_PATH); std::string exeDir = std::string(exePath); size_t pos = exeDir.find_last_of('\\'); exeDir = exeDir.substr(0, pos); SetDllDirectory(exeDir.c_str()); HMODULE hDll = LoadLibraryEx(_T("HCNetSDK.dll"), NULL, LOAD_WITH_ALTERED_SEARCH_PATH); if (hDll == NULL) { DWORD err = GetLastError(); // 此处打日志,err=126 通常是缺依赖,err=127 通常是依赖文件不匹配 return false; } return true; }这里的SetDllDirectory只影响当前进程的搜索顺序,是相对稳妥的做法。如果直接放在运行目录,其实不调用这一步也能加载成功,但它能帮助我们排查问题——如果加载失败,多半就是依赖缺失,而不是搜索路径问题。
2.3 C++ 还是 C#:互操作方式决定你省不省力
SDK 本身是 C 接口,Windows 上最顺手的方式还是 C++ 直接包含头文件、链接 lib。但很多团队做上层平台用的是 C# / Java,这时你得通过 P/Invoke 调用。C# 版本要注意结构体布局的byte类型映射,否则传参错位会导致登录接口返回莫名的负数。
常见的 C# 声明例子:
[DllImport("HCNetSDK.dll", EntryPoint = "NET_DVR_Init")] public static extern bool NET_DVR_Init(); [DllImport("HCNetSDK.dll", EntryPoint = "NET_DVR_SetConnectTime")] public static extern bool NET_DVR_SetConnectTime(uint dwWaitTime, uint dwTryTimes); [StructLayout(LayoutKind.Sequential)] public struct NET_DVR_USER_LOGIN_INFO { [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 128)] public string sDeviceAddress; public ushort wPort; [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 64)] public string sUserName; [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 64)] public string sPassword; public byte bUseTransport; public ushort wLocalPort; public IntPtr pUserID; // 其他字段按需补齐 }注意结构体里的sDeviceAddress在 C 里其实是char[128],C# 中必须用ByValTStr和具体的SizeConst,不能用String直接代替。这是 C# 调这种嵌套字符串数组最容易翻车的地方。如果你拿到的头文件里字段顺序和这个不一致,以实际头文件为准,千万不要凭感觉猜。
选语言没有绝对标准,我的建议是:只要团队里有人能看 C 头文件并看懂结构体,就优先 C++;如果只能用 C#,务必写一个小的互操作测试程序,先把NET_DVR_Init跑通再继续,否则后面所有错误都会被误判成网络问题。
3. 初始化、设备搜索与登录:从空项目到拿到用户 ID
这一章是整个接入流程的地基。很多开发者跳过初始化直接NET_DVR_Login_V40,结果设备明明在线却登录失败。另外,老 SDK 的搜索接口和登录接口是两套逻辑,先搜出设备,再拿设备的 IP 去登录,是最稳的流程。
3.1 先调用 NET_DVR_Init 和连接超时设置
不要小看这两步。NET_DVR_Init会加载内部组件、初始化网络和日志系统,不调用它直接登录,十次有八次会失败。这个版本里,日志默认关闭,建议开发期开启以便排错:
NET_DVR_Init(); NET_DVR_SetConnectTime(3000, 2); // 超时 3 秒,尝试 2 次 NET_DVR_SetReconnect(5000, true); // 断线后每 5 秒重连一次,开启自动重连 NET_DVR_SetLogLevel(0, true); // 0 表示全量日志,输出到当前目录NET_DVR_SetConnectTime第一个参数是单次连接超时时间,单位毫秒;第二个是重试次数。内网设备建议 2000~3000 毫秒,跨公网接入要放到 5000 以上。NET_DVR_SetReconnect的第二个参数传true后,断线重连由 SDK 内部处理,上层只需关注NET_DVR_GetLastError。- 日志默认生成在程序运行目录,如果发现目录不可写,日志功能会导致后续操作异常,所以服务器环境里要注意权限。
这三行是标准动作,无论后面用什么接口,建议都在程序最开头执行。检查返回值也很关键,任何一个返回false都要立刻NET_DVR_GetLastError拿到错误码,否则后续所有报错都会被误导。
3.2 搜索设备:NET_DVR_SearchDevices 与网络环境检查
老版本支持两种搜索方式,一种是用NET_DVR_SearchDevices按网段搜,另一种是直接遍历 IP。最常用的还是前者:
NET_DVR_DEVICEINFO_V30 deviceInfo = {0}; NET_DVR_DEVICE_LIST deviceList = {0}; NET_DVR_IPADDR ipAddrInfo = {0}; // 初始化搜索设备结构,传入字符串长度 strcpy_s(deviceList.sDeviceList, sizeof(deviceList.sDeviceList), ""); strcpy_s(deviceList.sUsername, "admin"); strcpy_s(deviceList.sPassword, "password"); NET_DVR_SearchDevices(&deviceList, 1000, &ipAddrInfo, 100);这个接口在 SDK V5.1.1.4 里的行为有些特殊:它要求先调用NET_DVR_Init,而且搜索时对子网掩码的解析依赖本机网卡。如果你本机有多个网卡(虚拟机上很常见),搜索到的结果可能来自第一个网卡所在网段,不一定是你目标设备的网段。所以搜索不到设备时,先确认目标设备是否和当前主机的活动网卡在同一网段,再去查防火墙。
防火墙是搜索失败的第一大元凶。搜不到设备时,不要急着改代码,先在命令行执行:
# 检查能否 ping 通设备 IP,注意设备默认可能禁 ping ping 192.168.1.64 # 查看端口是否打开,海康设备默认 8000 端口 telnet 192.168.1.64 8000telnet 通了说明设备网络正常,再用 SDK 搜索。如果搜索还是失败,多半是 Windows 防火墙拦了 UDP 广播包。老版本 SDK 的搜索走的是 UDP 2001 或 37020 端口的广播,需要在防火墙里放行。这一块和代码无关,属于环境配置。
搜到设备后,正确做法是把设备 IP、端口、用户名密码填到登录结构体去登录,而不是依赖搜索结果里的用户 ID 直接取流。搜索结果的设备信息主要用于确认设备在线,登录必须显式传 IP。
3.3 NET_DVR_Login_V40 的结构体参数与返回值的真实含义
登录接口是这个 SDK 里最容易被误用的接口。V5.1.1.4 版本同时支持NET_DVR_Login_V30和NET_DVR_Login_V40,我建议直接用 V40,因为 V30 的NET_DVR_DEVICEINFO_V30结构体不够用,拿不到更多的通道能力信息。
NET_DVR_USER_LOGIN_INFO loginInfo = {0}; NET_DVR_DEVICEINFO_V40 deviceInfoV40 = {0}; strcpy_s(loginInfo.sDeviceAddress, sizeof(loginInfo.sDeviceAddress), "192.168.1.64"); loginInfo.wPort = 8000; strcpy_s(loginInfo.sUserName, sizeof(loginInfo.sUserName), "admin"); strcpy_s(loginInfo.sPassword, sizeof(loginInfo.sPassword), "password"); LONG lUserID = NET_DVR_Login_V40(&loginInfo, &deviceInfoV40); if (lUserID < 0) { LONG error = NET_DVR_GetLastError(); // error 常见值: // 1: 用户名密码错误 // 2: 账号被锁定 // 7: 连接失败,常见于端口不对或设备离线 // 9: 端口被占用或连接被重置 // 23: 设备不在线 }wPort默认是 8000。某些老设备可能改了端口,必须从设备配置里确认。- 登录返回的
LONG是一个用户 ID,之后所有操作都基于这个 ID。它是有符号数,返回负数就是失败。 NET_DVR_DEVICEINFO_V40的前几个成员描述了通道数、报警输入输出数等,后续预览需要用到起始通道号,所以这个结构体一定要原封保留。
一个很隐蔽的坑:NET_DVR_USER_LOGIN_INFO中bUseTransport字段。bUseTransport=1表示走 TCP 传输,允许跨网段访问;bUseTransport=0表示走 UDP,且只能同网段。如果两边的网段不一致,必须把该字段置 1。很多跨网段登录失败的案例都是这个字段没设置。我一般会直接写死为 1,除非你明确知道要 UDP。
4. 预览与取流:把画面真正拉起来
登录成功后,下一步是利用用户 ID 打开实时预览。这部分的重点是预览接口的参数、窗口句柄的传入方式,以及抓图和取流回调之间的关系。单独调用接口不复杂,组合起来用才容易混乱。
4.1 用 NET_DVR_RealPlay_V40 打开实时预览
NET_DVR_PREVIEWINFO previewInfo = {0}; previewInfo.hPlayWnd = m_hWnd; // 预览窗口句柄,可为 NULL(纯取流) previewInfo.lChannel = channelID; // 通道号,一般是 1 或从设备信息里获取 previewInfo.dwStreamType = 0; // 0 主码流,1 子码流 previewInfo.dwLinkMode = 0; // 0 TCP,1 UDP,2 组播 previewInfo.bBlocked = 1; // 阻塞式取流,1 表示连接成功后才返回 previewInfo.bDisableMultiPlay = 0; // 0 允许双码流 previewInfo.dwFrameRate = 0; // 0 按设备默认帧率 LONG lPlayHandle = NET_DVR_RealPlay_V40(lUserID, &previewInfo, NULL, NULL); if (lPlayHandle < 0) { LONG error = NET_DVR_GetLastError(); // 如果是 17,通常是窗口句柄无效或码流类型不支持 }- 如果只是取流回传数据而不需要显示画面,把
hPlayWnd设为NULL。这时 SD卡不会有视频窗口渲染,数据会通过回调函数给出。 dwLinkMode选 TCP 时,若前面登录的bUseTransport为 1,取流也会走 TCP;如果跨网段且这里选了 UDP,可能会取流失败。dwStreamType选主码流一般清晰度高,但网络差时容易丢帧。调试阶段建议先用子码流,把流程跑通再切主码流。
这个接口的返回句柄是之后管理码流的关键,停止预览时要调用NET_DVR_StopRealPlay,否则会造成句柄泄漏,最终导致资源耗尽无法再开新预览。
4.2 抓图:NET_DVR_CaptureJPEGPicture 与码流回调的取舍
抓图有两种常见路径:一种是拿到句柄后直接保存 JPEG;另一种是从预览回调的 PS 流中解析出IMAGE缩放快照。前者适合单张抓拍,后者适合做连续帧处理。
单张抓图相对简单:
char sJpegFile[256] = {0}; sprintf_s(sJpegFile, "snapshot_%d.jpg", channelID); NET_DVR_JPEGPARA jpegPara = {0}; jpegPara.wPicSize = 0; // 图片尺寸,0 默认 jpegPara.wPicQuality = 2; // 画质 0~4,2 为中等 BOOL bRet = NET_DVR_CaptureJPEGPicture(lUserID, channelID, &jpegPara, sJpegFile); if (!bRet) { LONG error = NET_DVR_GetLastError(); // error=27 是数值越界或图片质量参数非法 }注意这个接口的第二个参数有时是起始通道号,有时是逻辑通道号,取决于设备类型。如果抓到的是黑图,多半是把逻辑通道和物理通道搞混了。建议从设备能力集里解析byStartChan,而不是硬编码 1。
另一种是从实时回调中取流做帧分析。回调函数原型约为:
void CALLBACK fRealDataCallBack(LONG lRealHandle, DWORD dwDataType, BYTE* pBuffer, DWORD dwBufSize, void* pUser) { if (dwDataType == 0) { // 0 表示原始 PS 流(GOP 结构) // 可以将 pBuffer 写入文件或送入解码器 } else if (dwDataType == 4) { // 4 表示系统时标等附加数据,可忽略 } }这种回调模式最常踩的坑是:回调函数是在 SDK 内部线程里触发的,你不能在回调里做耗时操作,比如写数据库或直接调用 UI。正确做法是把pBuffer拷贝到自己的缓冲队列,再由业务线程消费。否则会出现画面卡顿、SDK 内部缓冲溢出,甚至直接崩。
4.3 连接超时、断线重连与日志的相互配合
很多工程师只在意登录和预览,忽略掉连接参数的组合效果。真正上线跑几分钟就掉线,往往就是超时时间设得太短、重连间隔太长。通常我建议:
- 登录超时放到 5000 毫秒,重试次数 2 次。
- 自动重连间隔 3000~5000 毫秒,开启自动重连。
- 每次失败后,用
NET_DVR_GetLastError把错误码打到日志里,再决定是重连还是主动重启。
补充一个细节:如果开启了重连,当设备断电再恢复时,SDK 会自动重新登录并恢复预览。但这只适用于本设备是否在线,如果设备 IP 变了(DHCP 场景),重连机制就失效,上层应用需要自己感知错误码并重新搜索。
日志方面,NET_DVR_SetLogLevel(0, true)会把日志写到当前目录的SdkLog文件夹下。部署到服务器时要预留磁盘空间,日志增长很快,尤其是 5 版 SDK,一天可能在几百 MB。建议线上关掉日志,只在现场排错时开启。
5. 避坑:老版本 V5.1.1.4 在 WIN64 下的五个常见问题
前面几章是按正常流程走,但真正把这些接口落到项目里的时候,几乎每个人都会遇到几个诡异的问题。这一章专门说现象、原因、解决,每条都是实际调试中会被反复追问的。
5.1 登录返回 9 号错误:端口被占用还是连接被重置
现象:设备在线、ping 通、telnet 8000 端口也能通,但NET_DVR_Login_V40返回 -1,NET_DVR_GetLastError得到 9。
原因:9 号错误在官方描述里是“端口被占用或连接被重置”。在 WIN64 系统上,大概率是上一个进程崩溃退出,但网络连接处于 TIME_WAIT 状态,或者防火墙对访问源 IP 做了连接数限制。另一种更隐蔽的原因是你同时调用了两次NET_DVR_Init,第二个进程的初始化操作把第一个进程的句柄表冲掉了。
解决:先重启应用确认是否恢复,不行就在命令行查看连接状态:
netstat -ano | findstr 8000看到大量TIME_WAIT说明是连接未释放,可以减小登录重试次数或等待几秒。如果连接数正常,检查是否在同一个进程中加载了两份 HCNetSDK.dll,比如一个在运行目录、一个在 system32。确认无误后,把NET_DVR_Init只在 main 函数开头调用一次。
5.2 回调函数注册后收不到任何数据
现象:NET_DVR_RealPlay_V40返回了正常的句柄,但fRealDataCallBack一次都没被调用。
原因:一种是回调函数声明未加CALLBACK约定,导致栈平衡被破坏,数据其实有回调但立即崩溃或静默失败;另一种是预览句柄被局部变量覆盖,回调拿到的句柄已经失效。还有一个常被忽视的:回调函数不是静态成员函数。如果你在类里声明回调成员函数,C++ 编译器会把它变成隐式this调用,SDK 会直接拒绝调用,而且没有任何错误提示。
解决:回调必须写成全局函数,或者类静态函数。如果是类成员函数,要构造一个静态函数并显式传this指针作为用户指针。示例:
class CRealPlay { public: void Start() { NET_DVR_RealPlay_V40(lUserID, &previewInfo, &CRealPlay::RealDataCallBack, this); } static void CALLBACK RealDataCallBack(LONG lRealHandle, DWORD dwDataType, BYTE* pBuffer, DWORD dwBufSize, void* pUser) { CRealPlay* self = static_cast<CRealPlay*>(pUser); // 回调中只做拷贝,不处理逻辑 self->OnData(dwDataType, pBuffer, dwBufSize); } };如果你用 C#,回调代理必须声明为一个字段并保持生命周期,否则 GC 回收后回调也会静默失效。
5.3 抓图成功但图片是纯黑色或花屏
现象:NET_DVR_CaptureJPEGPicture返回TRUE,打开图片却是一片黑。
原因:多数是通道号错误。登录返回的NET_DVR_DEVICEINFO_V40中,byStartChan是起始通道号,对某些 NVR 来说不是 1。你直接传 1 抓的是无效通道,设备可能返回默认错误或黑帧。另一个原因是主码流分辨率太大,SDK 内部编码缓冲不足,这个在老版本上出现过。
解决:先用预览接口把现场画面拉出来,确认是用哪个通道号;再检查NET_DVR_JPEGPARA.wPicSize=0是否被设备支持。如果黑图,把wPicSize设成具体值(比如 3 对应 704x576)试一下。还有一个土办法:先正常预览 2 秒,再调抓图接口,大部分情况下能拿到正确画面。
5.4 与新版 SDK 的 DLL 混用导致初始化失败
现象:项目里之前用过新版本的 HCNetSDK,后来你的程序链接的是旧版,运行时却报出结构体大小不一致,或者NET_DVR_Init直接返回 false。
原因:SDK 版本之间不仅接口返回值可能变化,结构体定义也会增减字段。更麻烦的是,如果旧版 DLL 和新版 DLL 同时被加载,操作系统不会加载两次相同名字的 DLL,而是复用先加载的那个,导致你的程序拿到的是旧版结构的解析,但数据来自新版 DLL,内存布局错位。
解决:严格保证程序目录下只有一份 HCNetSDK.dll,删除 system32 等全局路径下的旧文件。用dumpbin /dependents或工具查看实际加载路径。如果你要支持的设备比较老,建议一直用老版本 SDK;如果设备较新,尽量升级到配套版本。同一个项目里不要做版本混搭。
5.5 64 位进程加载 32 位播放库导致预览黑窗口
现象:预览接口返回成功,但窗口一片空白,拖动窗口还会闪烁或崩溃。
原因:HCNetSDK.dll 是 64 位,但 PlayCtrl.dll 和配套组件是 32 位。系统加载 DLL 时会忽略位数不匹配的依赖,播放库内部函数找不到入口,预览画面不能渲染。这种情况常见于你从某个 32 位项目里把整套 DLL 直接复制到 64 位工程里。
解决:确认整个文件体系全是 x64 副本。最简单的方法:发布前在程序里加一句检测句柄的代码,或者在加载 DLL 后用工具查看模块位标志。命令行可以用cff explorer或dumpbin /headers查看Machine字段是否为x64。确保HCNetSDK.dll、PlayCtrl.dll、HCCore.dll和HCNetSDKCom下的动态库全部为 64 位,否则即使能登录,预览也会出问题。
6. 进阶:用 HCNetSDK 实现一个无界面取流回写文件的最小框架
这一章给一个完整可跑的最小框架,不做界面,只完成初始化、搜索、登录、预览回写文件、退出清理五个动作。你可以把它当成一个基座,后续换成抓图、分析或转发都很方便。
#include "HCNetSDK.h" #include <string> #include <fstream> #pragma comment(lib, "HCNetSDK.lib") // 自定义消息类型:0 表示流数据 #define STREAM_DATA_TYPE 0 static std::ofstream g_psStreamFile; void CALLBACK OnData(LONG lRealHandle, DWORD dwDataType, BYTE* pBuffer, DWORD dwBufSize, void* pUser) { if (dwDataType == STREAM_DATA_TYPE) { if (g_psStreamFile.is_open()) { g_psStreamFile.write((char*)pBuffer, dwBufSize); } } } int RunStreamDemo() { // 1. 初始化 NET_DVR_Init(); NET_DVR_SetConnectTime(3000, 2); NET_DVR_SetReconnect(5000, true); // 2. 登录 NET_DVR_USER_LOGIN_INFO loginInfo = {0}; NET_DVR_DEVICEINFO_V40 deviceInfoV40 = {0}; strcpy_s(loginInfo.sDeviceAddress, "192.168.1.64"); loginInfo.wPort = 8000; loginInfo.bUseTransport = 1; strcpy_s(loginInfo.sUserName, "admin"); strcpy_s(loginInfo.sPassword, "password"); LONG lUserID = NET_DVR_Login_V40(&loginInfo, &deviceInfoV40); if (lUserID < 0) { printf("login failed, error: %d\n", NET_DVR_GetLastError()); return -1; } // 3. 预览并开始接收流数据 NET_DVR_PREVIEWINFO previewInfo = {0}; previewInfo.hPlayWnd = NULL; // 不需要渲染窗口 previewInfo.lChannel = deviceInfoV40.struDeviceV30.byStartChan; previewInfo.dwStreamType = 1; // 先取子码流 previewInfo.dwLinkMode = 0; // TCP previewInfo.bBlocked = 1; LONG lRealHandle = NET_DVR_RealPlay_V40(lUserID, &previewInfo, OnData, NULL); if (lRealHandle < 0) { printf("realplay failed, error: %d\n", NET_DVR_GetLastError()); NET_DVR_Logout(lUserID); return -1; } g_psStreamFile.open("stream.ps", std::ios::binary); // 4. 跑 10 秒后停止 Sleep(10000); g_psStreamFile.close(); NET_DVR_StopRealPlay(lRealHandle); NET_DVR_Logout(lUserID); NET_DVR_Cleanup(); return 0; }这段代码的所有关键参数都有注释。注意previewInfo.lChannel用的是deviceInfoV40.struDeviceV30.byStartChan,而不是硬编码 1,这是避免黑帧的关键。dwStreamType=1的子码流在带宽受限时也更可靠。
把代码跑通之后,建议做一个改进:用另一个线程去消费回调里拷贝出来的数据,而不是直接在回调中写文件。上面的示例只是演示完整导通,真正做平台接入时,回调写到磁盘会导致高码流下 IO 阻塞。你可以定义一个有界缓冲队列,回调只做入队,消费线程去写文件或送算法库。
还有一个验证技巧:写出来的stream.ps文件可以用 VLC 直接打开。VLC 选择“播放媒体流”并指定文件,如果不能播放,说明回调里收到的不是完整的 PS 流,先检查网络丢包和对齐。如果文件能播但帧率不对,多半是子码流帧率低,不影响流程验证。
我自己接设备时,习惯先跑通这个无界面回写,再往上加抓帧、HTTP 推送和 UI。因为回写文件是最容易交叉验证的步骤——用它确定数据通路没问题,再去折腾界面和业务逻辑才不至于上下左右全是故障点。希望这篇笔记能帮你少走几步弯路,接手这套 SDK 时,照着这个路径走基本能稳。
本文还有配套的精品资源,点击获取