ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

C#海康威视报警布防监听:从DllImport到回调的完整实现

C#海康威视报警布防监听:从DllImport到回调的完整实现 简介面向需要集成海康威视报警能力的C#开发者这份示例Demo演示了如何基于海康SDK完成设备登录、报警布防、事件订阅与消息处理适合安防项目二次开发参考。压缩包共51个文件、约12.55MB其中包含核心C#源码文件如CHCNetSDK.cs、AlarmDemo.cs以及运行所需的dll动态库和工程配置文件并附有说明文档便于快速理解项目结构并直接编译调试。包内示例工程已实现报警监听主流程开发者可重点参考事件委托注册、报警回调解析以及异常处理逻辑在此基础上扩展移动侦测、入侵报警等自定义业务。项目适配Visual Studio环境代码注释与文件组织清晰已有1326人学习下载适合具备一定C#基础、希望缩短海康SDK对接周期的读者参考学习。1. 报警布防监听demo到底在解决什么问题海康威视NVR或DVR安装好后报警事件通常只在设备本地屏幕上闪一下或者要登录web管理页才能看到。业务系统要接住这些报警最常见的手段就是让设备把事件主动上报。C#编写的海康威视设备报警布防监听demo走的是“登录设备、下发布防、接收回调”这条链路布防相当于打开设备的上报开关监听则是守在回调线程里把报警数据接住并转交给业务队列。这个demo真正有价值的不只是“能收到报警”还包括布防参数怎么传、回调线程怎么回UI、布防之后怎样验证它没有静默丢事件。接手过WinForms或WPF上位机的工程师照着这套思路可以在一小时内把最小可跑版本拼出来也能在后续排错时知道问题大概出在设备端还是SDK封装端。2. 报警布防监听之前的SDK基座C#侧DllImport与结构体2.1 按运行位数固定海康威视SDK文件写demo时遇到最多的启动错误不是编译错而是“试图加载格式不正确的DLL”。海康威视网络SDK以C头文件和动态库形式分发C#通过P/Invoke按进程位数加载动态库位数必须和主程序进程位数一致。项目里做的是纯报警布防监听场景只依赖HCNetSDK.dll预览、回放、语音对讲才会牵扯HCPreview.dll、AudioRender.dll这些播放相关依赖。我习惯在仓库根目录把SDK放成两份便于后续扩展demo/ ├── lib/ │ ├── x64/ │ │ ├── HCNetSDK.dll │ │ ├── HCPreview.dll │ │ └── AudioRender.dll │ └── x86/ │ ├── HCNetSDK.dll │ ├── HCPreview.dll │ └── AudioRender.dll ├── HikAlarmDemo/ │ └── HikAlarmDemo.csproj └── README.md如果确认只做报警布防监听可以只在x64目录放HCNetSDK.dll大大降低环境噪音。CS文件里配合CopyToOutputDirectory把dll带到输出目录同时把PlatformTarget指定为x64PropertyGroup PlatformTargetx64/PlatformTarget /PropertyGroup ItemGroup None Include..\lib\x64\HCNetSDK.dll LinkHCNetSDK.dll CopyToOutputDirectoryPreserveNewest / /ItemGroup这段配置的作用是告诉.NET项目生成时使用x64平台并把HCNetSDK.dll原样复制到运行目录。很多历史遗留项目还跑在x86上那lib/x86这份不能省但有一点要注意报警模块在较新SDK版本里对x64支持更完整x86下个别命令结构体大小校验会卡得比较死能不切x86就不切。2.2 报警布防监听要用的API入口把整个监听流程拆开看核心就八个函数先列出分工后续代码都基于这组入口。APIC#入口在报警布防监听中的角色NET_DVR_InitNET_DVR_Init()全局初始化所有API调用前必须执行一次NET_DVR_SetConnectTimeNET_DVR_SetConnectTime(uint, uint)设置连接等待时间和重试次数NET_DVR_SetReconnectNET_DVR_SetReconnect(int, int)设备断线后自动重连长挂监听建议开启NET_DVR_Login_V40NET_DVR_Login_V40(ref, ref)登录并取得用户句柄NET_DVR_SetupAlarmChan_V41NET_DVR_SetupAlarmChan_V41(int, ref, delegate, IntPtr)布防返回报警监听句柄NET_DVR_CloseAlarmChan_V41NET_DVR_CloseAlarmChan_V41(int)撤防并释放报警监听通道NET_DVR_LogoutNET_DVR_Logout(int)注销登录NET_DVR_CleanupNET_DVR_Cleanup()全局清理进程退出前调用布防返回的句柄和登录句柄不是同一个概念。登录句柄代表这个登录会话撤防时传布防句柄注销时传用户句柄。报警监听的生命周期以布防句柄为准这句对应关系弄混后面会出现撤防无效或者注销后回调还在泄的现象。2.3 DllImport声明与三个关键结构体的C#写法C#侧没有现成的SDK包一切入口靠DllImport声明。委托回调参数我一般写成IntPtr而不是直接ref结构体原因是海康报警信息结构体存在版本切换IntPtr配合Marshal.PtrToStructure会更灵活后面解析报警数据时可以直接躲开结构体版本错位的坑。public delegate void MSGCallBack(int lCommand, IntPtr pAlarmInfo, uint dwBufLen, IntPtr pUser); [DllImport(HCNetSDK.dll)] public static extern bool NET_DVR_Init(); [DllImport(HCNetSDK.dll)] public static extern bool NET_DVR_SetConnectTime(uint dwWaitTime, uint dwTryTimes); [DllImport(HCNetSDK.dll)] public static extern bool NET_DVR_SetReconnect(int dwInterval, int dwEnable); [DllImport(HCNetSDK.dll)] public static extern int NET_DVR_Login_V40(ref NET_DVR_LOGIN_INFO pLoginInfo, ref NET_DVR_DEVICEINFO_V40 lpDeviceInfo); [DllImport(HCNetSDK.dll)] public static extern int NET_DVR_SetupAlarmChan_V41(int lUserID, ref NET_DVR_SETUPALARM_PARAM lpSetupParam, MSGCallBack cbAlarm, IntPtr pUser); [DllImport(HCNetSDK.dll)] public static extern bool NET_DVR_CloseAlarmChan_V41(int lAlarmHandle); [DllImport(HCNetSDK.dll)] public static extern bool NET_DVR_Logout(int lUserID); [DllImport(HCNetSDK.dll)] public static extern void NET_DVR_Cleanup(); [DllImport(HCNetSDK.dll)] public static extern uint NET_DVR_GetLastError();这段声明的重点是函数返回类型。早期SDK有些接口返回BOOL有些返回LONG句柄C#里必须严格对应登录和布防在头文件里返回句柄值所以用int布尔型返回值接口用bool即可。接下来是登录信息结构体这里只保留最常用的字段完整定义建议以手里的SDK版本头文件为准[StructLayout(LayoutKind.Sequential, CharSet CharSet.Ansi)] public struct NET_DVR_LOGIN_INFO { public int dwSize; [MarshalAs(UnmanagedType.ByValTStr, SizeConst 129)] public string sDeviceAddress; public byte byUseTransport; public ushort wPort; [MarshalAs(UnmanagedType.ByValTStr, SizeConst 64)] public string sUserName; [MarshalAs(UnmanagedType.ByValTStr, SizeConst 64)] public string sPassword; public int cbLoginResult; public IntPtr pUser; public int bPasswordWrong; }sDeviceAddress按129字节定长声明这和SDK头文件里char[129]保持一致。wPort是ushort不能写成int否则结构体字节偏移直接错位。另一个常用结构体是设备信息NET_DVR_DEVICEINFO_V40它主要用来确认通道数和设备类型demo里只需要在登录时传入字段可以先声明到byStartChan确认基本通道即可。布防参数结构体NET_DVR_SETUPALARM_PARAM是监听配置的核心它决定回调里拿到的是哪种报警数据类型。demo先按最简字段声明[StructLayout(LayoutKind.Sequential)] public struct NET_DVR_SETUPALARM_PARAM { public int dwSize; public byte byLevel; public byte byAlarmInfoType; public byte byRetAlarmTypeV40; public byte byRetDevInfoVersion; public byte byRetVQDAlarmType; public byte byFaceAlarmDetection; public byte bySupport; public byte byBrokenNetHttp; }2.4 布防参数里真正影响监听行为的三个字段第一个是byLevel表示布防级别传1代表一级布防日常实时监听用1就够。第二个是byAlarmInfoType它决定报警信息结构体是哪一代0表示NET_DVR_ALARMINFO经典结构体1表示NET_DVR_ALARMINFO_V40扩展结构体。demo阶段为了少踩结构体长度坑先设0后面第4章解析数据时会说明差异。第三个是byRetAlarmTypeV40置1后SDK按V40规则组织返回字段。很多网上的代码直接照抄这段参数但没解释这三个字段等到设备报警类型是智能分析事件时才发现回调信息取不全是正常的。3. 报警布防监听主线登录、布防、回调转发3.1 登录设备并拿到布防需要的用户句柄登录用NET_DVR_Login_V40它的输入参数是登录信息和设备信息两个ref结构体输出是用户ID。写一个最小登录方法public int Login(string ip, ushort port, string userName, string password) { var loginInfo new NET_DVR_LOGIN_INFO { dwSize Marshal.SizeOfNET_DVR_LOGIN_INFO(), sDeviceAddress ip, wPort port, sUserName userName, sPassword password }; var deviceInfo new NET_DVR_DEVICEINFO_V40(); int userId NET_DVR_Login_V40(ref loginInfo, ref deviceInfo); if (userId 0) { uint error NET_DVR_GetLastError(); throw new Exception($登录失败错误码: {error}); } return userId; }登录前必须填dwSizeSDK通过字段长度判断传入的是哪一代结构体。port参数注意用ushort网络端口本身65535封顶。登录失败时userId为负值具体原因通过NET_DVR_GetLastError拿。这里是第一道常见坎用户名带中文或者密码带特殊字符时如果DllImport声明里没标CharSet.Ansi字符串编码错位会直接报用户密码错误。提示SDK调用顺序很重要。NET_DVR_Init必须在登录前执行程序退出前执行NET_DVR_Cleanup。顺序错乱时常见现象是登录偶发失败错误码不固定。3.2 布防并在C#侧保住回调委托布防方法入参是登录返回的userId真正的布防动作在NET_DVR_SetupAlarmChan_V41。它要求传入布防参数、回调委托、用户参数指针private MSGCallBack _alarmCallback; private int _alarmHandle; public void Arm(int userId) { var alarmParam new NET_DVR_SETUPALARM_PARAM { dwSize Marshal.SizeOfNET_DVR_SETUPALARM_PARAM(), byLevel 1, byAlarmInfoType 0, byRetAlarmTypeV40 1 }; _alarmCallback OnAlarmMessage; _alarmHandle NET_DVR_SetupAlarmChan_V41( userId, ref alarmParam, _alarmCallback, IntPtr.Zero); if (_alarmHandle 0) { uint error NET_DVR_GetLastError(); throw new Exception($布防失败错误码: {error}); } }这里最关键的是把委托赋给类字段_alarmCallback。SDK拿到的是托管方法指针如果委托是方法内的局部变量方法退出后委托会被GC回收结果是布防返回成功但回调一次都不进。这个问题在报警布防监听demo里出现频率非常高表现形式就是“设备触发了程序里完全没反应”。布防参数中把byAlarmInfoType设0是为了让demo阶段只解析NET_DVR_ALARMINFO。布防返回的_alarmHandle是撤防入口它和userId并存撤防时要传它。3.3 回调线程到UI线程的转发海康SDK回调跑在自身消息线程里回调方法里直接更新WPF控件会抛线程间操作异常WinForms则表现为控件闪烁或无缘无故卡死。常见做法是用ConcurrentQueue先缓冲再由UI定时器周期取数据。这也是C#上位机开发里经常遇到的“循环数据采集和UI刷新卡顿”问题的通用解法。private ConcurrentQueuestring _eventQueue new ConcurrentQueuestring();回调侧只入队不处理private void OnAlarmMessage(int lCommand, IntPtr pAlarmInfo, uint dwBufLen, IntPtr pUser) { string text $命令:0x{lCommand:X8}, 当前线程:{Environment.CurrentManagedThreadId}; _eventQueue.Enqueue(text); }UI侧用DispatcherTimer每200毫秒批量消费_logTimer new DispatcherTimer(); _logTimer.Interval TimeSpan.FromMilliseconds(200); _logTimer.Tick (s, e) { while (_eventQueue.TryDequeue(out string message)) LogPanel.AppendText(message Environment.NewLine); }; _logTimer.Start();这里把耗时操作都留在UI线程之外回调线程每进一次消息只做一次Enqueue即使报警事件密集UI也不会被拖垮。200毫秒轮询间隔对报警展示足够如果要做数据曲线建议间隔缩短到100毫秒并配合BeginInvoke批量更新。4. 报警数据解包与布防失败排错4.1 用回调命令码区分报警类型很多人第一次写回调时以为lCommand直接就是报警类型实际它是命令码叫法不同。lCommand决定报警数据结构体怎么解释报警信息里还有byAlarmType这类细分字段。常见命令常量在SDK头文件里静态定义demo中先集中处理三类lCommand常量值报警数据支线COMM_ALARM_V300x1003移动侦测、视频遮挡、硬盘异常等通用报警COMM_ALARM_RULE0x1102智能分析规则类报警COMM_UPLOAD_ALARM0x8060报警主机或扩展模块上传上来的报警回调解析代码的骨架先搭起来private void OnAlarmMessage(int lCommand, IntPtr pAlarmInfo, uint dwBufLen, IntPtr pUser) { switch (lCommand) { case 0x1003: var alarmInfo Marshal.PtrToStructureNET_DVR_ALARMINFO(pAlarmInfo); _eventQueue.Enqueue( $移动侦测, 通道号:{alarmInfo.dwChannelNo}, $报警类型:{alarmInfo.byAlarmType}); break; case 0x1102: // 规则报警结构体字段更多按SDK头文件完整声明后再解析 break; case 0x8060: // 报警主机上传数据解析逻辑有时要按协议单独拆 break; } }NET_DVR_ALARMINFO里的dwChannelNo是报警触发通道号byAlarmType是细分报警类型。把这两项取出来再叠加设备的IP地址就能知道是哪一路报警。移动侦测、视频遮挡和视频丢失都归在0x1003命令码下靠byAlarmType区分。4.2 用NET_DVR_GetLastError快速定位布防失败布防失败不像网络连接失败那么容易被发现很多情况是布防接口返回负值但界面看不出原因。排错统一走错误码常见几个错误码在SDK各版本里基本保持一致错误码错误宏布防监听场景里的可能原因7NET_DVR_NETWORK_FAIL_CONNECT设备IP不通或端口被防火墙拦截9NET_DVR_NETWORK_SEND_ERROR认证请求发出失败跨网段路由异常11NET_DVR_USERERROR用户名或密码不对17NET_DVR_PARAMETER_ERROR传入的结构体dwSize不正确19NET_DVR_ALLOC_RESOURCE_ERROR设备端资源不足常见于多客户端并发布防23NET_DVR_ORDER_ERROR调用顺序错NET_DVR_Init没执行或已Cleanup错误码转可读文本可以封装一个通用方法后续界面提示直接用public static string DescribeError(uint error) { switch (error) { case 7: return 网络连接失败请确认IP和端口; case 9: return 网络发送失败检查路由或防火墙; case 11: return 用户名或密码错误; case 17: return 参数错误结构体大小或字段不对; case 19: return 资源分配失败设备连接数可能已满; case 23: return 调用顺序错误初始化未完成; default: return $错误码:{error}; } }第17号错误码在C#里出现最多。结构体声明多了或少了字段会让dwSize和实际内存长度对不上一旦deviceInfo结构体长度小于SDK预期登录接口直接就拒绝。4.3 排除“布防成功但收不到报警”的排查顺序回调迟迟不触发时先从设备端检查再回到代码端。第一步看设备web端的报警配置。部分设备默认只启用本机报警输出没有勾选“联动报警上传”或“网络报警上报”。布防只是建立监听通道设备侧不配置上传事件不会发给SDK。第二步确认账号权限。海康设备的子用户权限列表里有一个“远程布防/撤防”如果用户只勾了预览权限布防返回成功后报警回调也进不来。第三步看同网段内是不是已经有一个客户端在撒防。某些老设备只允许单个会话布防另一个客户端登录后把布防撤掉本程序报警就断了。第四步检查程序里是否保存了委托实例委托丢失一直是最隐蔽的静默故障。提示回调线程里不要再调用任何SDK接口尤其不要调用NET_DVR_GetLastError。SDK内部是共享状态回调线程读取会导致主线程登录或布防卡死。5. 多设备同时布防的句柄管理与上线前验证5.1 多设备布防怎么识别报警来自哪台设备回调函数签名里没有userId多设备同时布防时不能靠接口参数判断来源。常见做法是在SetupAlarmChan_V41的pUser参数里传入一个设备上下文指针回调中从IntPtr还原出设备对象。C#里用GCHandle完成这个绑定public class DeviceContext { public string DeviceNo; public int UserId; public int AlarmHandle; public IntPtr UserParam; } private Dictionarystring, DeviceContext _devices new Dictionarystring, DeviceContext(); public int ArmDevice(DeviceContext ctx, ref NET_DVR_SETUPALARM_PARAM param) { GCHandle gch GCHandle.Alloc(ctx); ctx.UserParam GCHandle.ToIntPtr(gch); ctx.AlarmHandle NET_DVR_SetupAlarmChan_V41( ctx.UserId, ref param, _alarmCallback, ctx.UserParam); _devices[ctx.DeviceNo] ctx; return ctx.AlarmHandle; }回调里用pUser取回原始对象private void OnAlarmMessage(int lCommand, IntPtr pAlarmInfo, uint dwBufLen, IntPtr pUser) { if (pUser IntPtr.Zero) return; DeviceContext ctx GCHandle.FromIntPtr(pUser).Target as DeviceContext; var alarmInfo Marshal.PtrToStructureNET_DVR_ALARMINFO(pAlarmInfo); string line ${ctx?.DeviceNo} 通道:{alarmInfo.dwChannelNo} $类型:{alarmInfo.byAlarmType}; _eventQueue.Enqueue(line); }GCHandle保证了上下文对象在程序退出前不会被GC回收。撤防时再释放这个句柄顺序是先撤防后释放GCHandle不然回调里可能拿到已释放的Target再报空引用。5.2 上线前验证监听链路是否真的在干活布防返回成功只能证明命令被设备接受不代表报警链路全通。连线调试时先在设备侧手动触发一次报警输入再看回调日志里是否出现对应事件。注意报警类型值与设备主机的报警输入端口对应关系不同设备可能用不同byAlarmType值。长时间挂机验证时给日志加时间戳持续运行10分钟以上对比人工触发次数和回调日志条数。一旦有丢事件优先怀疑回调线程被耗时操作卡住。程序关闭时按“撤防、注销、清理”的顺序退出foreach (var dev in _devices.Values) { if (dev.AlarmHandle 0) NET_DVR_CloseAlarmChan_V41(dev.AlarmHandle); if (dev.UserId 0) NET_DVR_Logout(dev.UserId); if (dev.UserParam ! IntPtr.Zero) GCHandle.FromIntPtr(dev.UserParam).Free(); } NET_DVR_Cleanup();如果在客户现场发现程序退出后设备端仍显示“在线”多半是撤防或注销没有执行导致会话留在设备端占着布防名额。按这段顺序关闭后再打开设备web端确认在线列表为空报警布防监听demo的链路才算真正收口。本文还有配套的精品资源点击获取
返回列表