简介:这是一份面向 C# 开发者的海康威视摄像头二次开发 Demo,适合需要快速掌握海康 SDK 接入、设备控制与视频流处理的桌面应用工程师。压缩包共 187 个文件,约 23.38MB,以 dll 动态库为核心,配合 cs 源码、配置文件与可执行程序,涵盖设备初始化、视频预览、抓图、参数设置、事件回调与错误处理等关键环节;同时包含 Visual Studio 工程文件与资源文件,便于直接编译运行。已有 3903 人学习。通过学习该示例,可了解 SDK 常用 API 的封装方式、客户端程序结构以及摄像头管理界面的实现思路,减少自行摸索 SDK 文档的时间。
1. 为什么说海康威视摄像头C# Demo是上位机开发的敲门砖
做C#上位机最常遇上的硬需求之一,就是要把海康威视摄像头的画面接进自己的WinForm或WPF程序里。很多人第一反应就是下载那个海康威视摄像头C#Demo.rar,但解压后往往不是跑不起来,就是黑屏、闪退、崩溃三连,最后又回到百度里搜错误码。这个Demo真正的价值不是“能跑”,而是把海康SDK里最核心的登录、预览、抓图、录像四个动作,用C#代码完整走了一遍。搞懂它,你就等于拿到了海康设备网络SDK的钥匙:不用懂H.264封装,不用自己解析私有协议,也能在两天内把摄像头接进自己的系统。这篇笔记按我自己的落地路径来写,从SDK选型讲到代码实现,再到踩过的坑和最后的封装习惯,新手能跟着做,熟手也能对照着查边界。
2. 先搞清SDK的家底:选对包、放对DLL、调通第一个初始化
2.1 海康SDK的两种玩法:网络SDK与播放SDK
海康威视给C#开发者准备的东西,严格说是两套SDK组合着用的。第一套叫“设备网络SDK”,对应HCNetSDK.dll,负责设备发现、登录、取流、云台控制、参数配置这些和网络协议打交道的脏活累活。第二套叫“播放SDK”,对应PlayCtrl.dll和HCPreview.dll,负责把取回来的码流解码成画面,再渲染到窗口或转成位图。官方C#Demo.rar里实际上已经把这两套都封装进了HCNetSDK.cs这个文件,里面全是P/Invoke声明和各种结构体,所以你看到的C#代码就像在调用普通的C#类库,但底层其实是在和native DLL打交道。
除了SDK这条路,海康摄像头还支持RTSP取流和ISAPI的HTTP接口。RTSP适合用FFmpeg或VLC去拉,但如果你想在程序里做预览、抓图、录像,用第三方库解码是一条路,用官方SDK是另一条路。我自己的判断是:如果你的项目里摄像头数量少、功能简单,RTSP加FFmpeg也够用;但如果你要操作设备的OSD、报警、云台、参数设置,或者需要稳定的回放和抓图链路,老老实实回到官方SDK,这也是标题里这个Demo存在的意义。
2.2 解压C#Demo.rar后第一次编译:DLL复制与平台目标
很多人第一次编译这个Demo,报错不是代码问题,而是“找不到指定的模块”或者“未能加载DLL”。因为海康SDK是非托管的C++ DLL,不会像NuGet包那样自动复制到输出目录。你解压后会看到一堆DLL文件:HCNetSDK.dll、PlayCtrl.dll、HCPreview.dll、hlog.dll、hpr.dll、StreamTransClient.dll等等。常见做法是建一个sdk文件夹,把这些DLL文件全放进去,然后在Visual Studio里把项目的生成事件或者后期生成命令改成把DLL复制到exe输出目录。
另一个必踩的点是平台目标。官方C#Demo默认是x86编译的,因为老版本SDK只提供32位DLL。如果你的机器是64位系统,直接用AnyCPU编译,运行时会以64位进程加载32位DLL,直接报BadImageFormatException。在项目属性里把“平台目标”设为x86,或者你把目标平台改成x64并确保你用的是x64版的SDK包,并取消勾选“首选32位”,否则64位程序会加载不到正确的SDK。这个位数对齐问题贯穿整个开发周期,后面预览黑屏、抓图失败都可能和它有关,属于海康C#开发里的玄学之首。
2.3 起步代码:加载DLL并初始化SDK
先把最小可用的初始化代码贴上,这段代码应该出现在任何C#海康开发的第一步。
using System; using System.Runtime.InteropServices; class HikSdkStarter { [DllImport("HCNetSDK.dll")] private static extern bool NET_DVR_Init(); [DllImport("HCNetSDK.dll")] private static extern bool NET_DVR_SetConnectTime(uint dwWaitTime, uint dwTryTimes); [DllImport("HCNetSDK.dll")] private static extern uint NET_DVR_GetLastError(); static void Main() { bool initOk = NET_DVR_Init(); if (!initOk) { uint errCode = NET_DVR_GetLastError(); Console.WriteLine($"[SDK初始化失败] 错误码: {errCode}"); return; } NET_DVR_SetConnectTime(2000, 3); Console.WriteLine("SDK初始化成功"); } }逻辑说明:NET_DVR_Init是整个SDK的启动开关,必须在登录设备之前调用,并且整个进程生命周期里只需要调用一次。NET_DVR_SetConnectTime设置的是向设备发起连接时的超时时间和重试次数,第一个参数是超时毫秒数,第二个是重试次数。这个值别设太大,2秒超时、重试3次是我在局域网设备上的常用参数,如果摄像头在公网或4G环境下,可以放宽到5秒。NET_DVR_GetLastError是排查一切问题的入口,后面你会无数次用到它。
参数说明:如果你的程序在初始化这一步就失败,先别急着查网络,90%的可能是HCNetSDK.dll、hlog.dll等文件没找对位置。可以在[DllImport]里把DLL路径写全,例如[DllImport(@"D:\sdk\HCNetSDK.dll")],这样便于定位问题。另一个小习惯:把DLL放在exe同目录并用相对路径,部署时整套拷贝,最省心。
3. 登录设备与实时预览:把画面从摄像头拉到窗口
3.1 用户登录:用NET_DVR_Login_V40而不是老接口
SDK初始化通过后,第一步是登录设备。老版本SDK用NET_DVR_Login这个简单接口,但它能拿到的设备信息太少,现在官方SDK里主推NET_DVR_Login_V40。这个接口需要填充NET_DVR_USER_LOGIN_INFO结构体和NET_DVR_DEVICEINFO_V40结构体,前者是登录凭证,后者是设备能力信息,比如通道数。
using System; using System.Runtime.InteropServices; class HikLogin { [DllImport("HCNetSDK.dll")] private static extern int NET_DVR_Login_V40(ref NET_DVR_USER_LOGIN_INFO loginInfo, ref NET_DVR_DEVICEINFO_V40 deviceInfo); [DllImport("HCNetSDK.dll")] private static extern uint NET_DVR_GetLastError(); [StructLayout(LayoutKind.Sequential)] private struct NET_DVR_USER_LOGIN_INFO { [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 129)] 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 int cbSize; // 省略了私有保留字段,完整结构体请以HCNetSDK.cs为准 } [StructLayout(LayoutKind.Sequential)] private struct NET_DVR_DEVICEINFO_V40 { public byte byChanNum; // 其余字段以SDK头文件为准 } static void Login(string ip, string user, string password) { var loginInfo = new NET_DVR_USER_LOGIN_INFO { sDeviceAddress = ip, wPort = 8000, sUserName = user, sPassword = password, bUseTransport = 0 // 0 表示 TCP 连接 }; var deviceInfo = new NET_DVR_DEVICEINFO_V40(); int userId = NET_DVR_Login_V40(ref loginInfo, ref deviceInfo); if (userId == -1) { uint err = NET_DVR_GetLastError(); Console.WriteLine($"登录失败, 错误码: {err}"); } else { Console.WriteLine($"登录成功, 用户ID: {userId}, 模拟通道数: {deviceInfo.byChanNum}"); } } }逻辑说明:NET_DVR_Login_V40的返回值是用户ID,这个ID要存成全局变量,后面所有操作——预览、抓图、设置参数——都要拿它当凭证。端口默认是8000,海康设备的SDK端口,不是RTSP的554。bUseTransport字段设为0走TCP,如果设备和你的程序不在同一网段或者网络抖动严重,可以试UDP,但一般TCP最稳。结构体里的字段必须和官方HCNetSDK.cs里一致,特别是cbSize要赋值,否则SDK容易返回莫名其妙的参数错误。
参数说明:登录失败最常见的错误码是10001和10002,分别是密码错误和用户不存在。如果你确认密码正确但仍然报10001,先到设备网页端检查是否开启了非法登录锁定,或者你的用户账号被限制了远程登录权限。这个坑我放到第5章展开。
3.2 实时预览:句柄模式和回调模式怎么选
登录成功后要做的第一件事通常就是预览。NET_DVR_RealPlay_V40有两个用处:把画面直接显示到窗口,或者把码流数据回调给你自己处理。实际项目中两种模式都会用到,区分如下。
句柄模式最简单,适合快速验证。把WinForm里一个Panel的句柄传给SDK,SDK自己解码自己画,代码量最少。
NET_DVR_PREVIEWINFO previewInfo = new NET_DVR_PREVIEWINFO(); previewInfo.lChannel = 1; // 通道 1 previewInfo.dwStreamType = 0; // 0 主码流, 1 子码流 previewInfo.dwLinkMode = 0; // 0 TCP previewInfo.hPlayWnd = panelVideo.Handle; // 显示窗口句柄 int playHandle = NET_DVR_RealPlay_V40(userId, ref previewInfo, null, null); if (playHandle == -1) { uint err = NET_DVR_GetLastError(); Console.WriteLine($"预览失败, 错误码: {err}"); }逻辑说明:只要panelVideo.Handle传入SDK,画面就会自动绘制到该控件上,不需要你再做任何渲染操作。但句柄模式有个隐患:Panel控件一旦触发重绘或者句柄重建,画面可能直接黑掉。所以实际项目里,我通常把画面区域做成一个独立的、禁止重建句柄的控件类型。回调模式则不同,SDK会把码流数据一帧一帧交给你,你自己决定怎么处理,这也是实现抓图、录像、画面叠加的必经之路。
// 回调委托声明,和 HCNetSDK.cs 中保持一致 public delegate void REALDATACALLBACK(int lRealHandle, uint dwDataType, IntPtr pBuffer, uint dwBufSize, IntPtr pUser); // 启动预览,传入回调 int playHandle = NET_DVR_RealPlay_V40(userId, ref previewInfo, RealDataCallback, IntPtr.Zero); // 回调实现 private void RealDataCallback(int lRealHandle, uint dwDataType, IntPtr pBuffer, uint dwBufSize, IntPtr pUser) { // dwDataType == 0 表示原始码流数据 if (dwDataType == 0) { // 这里把数据喂给播放SDK去解码,或者自己保存成文件 PlayCtrl_InputData(playM4Port, pBuffer, dwBufSize); } }参数说明:dwDataType是数据类型,0是码流数据,3可能是语音数据,具体要看SDK头文件里的宏定义。很多人误以为回调里拿到的就是BMP或YUV裸数据,其实它是编码后的码流,必须继续交给PlayCtrl.dll解码。这个环节最容易被新手误解,我在第5章的避坑里会详细讲。
3.3 码流类型选择:主码流、子码流、三码流的适用场景
预览时要决定用哪一路码流,这直接关系到带宽占用和画面清晰度。主码流分辨率最高,适合本地大屏预览和录像;子码流分辨率低,适合多画面分割预览或者网络状况不好的场景。NET_DVR_PREVIEWINFO里的dwStreamType,0就是主码流,1是子码流,2是三码流。我一般建议在开发阶段用子码流,因为解码压力小,排查问题时画面更容易出来;等业务调通了,再切回主码流验证效果。切换码流需要重新调用NET_DVR_RealPlay_V40,先Stop再Start,否则可能花屏。
4. 抓图、录像与常用参数设置:把“看”变成“用”
4.1 抓图:手动抓图与定时抓图的两种实现
预览通了,最常接的需求就是抓图。海康SDK提供了两套抓图方式,一套是NET_DVR_CaptureJPEGPicture,另一套是走播放库。先看官方推荐的第一套:
[StructLayout(LayoutKind.Sequential)] private struct NET_DVR_JPEGPARA { public ushort wPicSize; // 图片尺寸, 0xff 表示原始分辨率 public ushort wPicQuality; // 图片质量, 0 为默认 } bool ret = NET_DVR_CaptureJPEGPicture(userId, 1, ref jpegPara, @"D:\capture\test.jpg"); if (!ret) { uint err = NET_DVR_GetLastError(); Console.WriteLine($"抓图失败, 错误码: {err}"); }逻辑说明:这个接口走的是设备端抓图,摄像头自己把JPEG生成好,再通过网络传回来,所以不占本地解码资源,速度取决于网络。wPicSize设成0xff表示按通道当前分辨率输出,如果你需要固定宽度,可以查SDK手册里的图片尺寸枚举,但项目里我几乎都用0xff。wPicQuality是质量等级,取值范围不是常规的0到100,而是SDK定义的0到2之类的小数值,所以我建议直接用0,让设备按默认策略处理。
参数说明:如果你的预览走回调模式,还有一条路是PlayCtrl.dll的PlayM4_GetJPEG,它在本地从解码后的画面里抓图,不需要和摄像头再次通信。这两种方式各有优劣。设备端抓图延迟高一点但画质可控,本地抓图响应快但拿到的画面是你正在预览的实时帧。做抓拍比对、事件联动这类功能时,我会优先用设备端抓图,可靠性更高。
4.2 本地录像:用SDK把码流直接落盘
录像功能在Demo里被一个函数带过,但实际使用时需要注意版本差异。老网络SDK的写法是NET_DVR_SaveRealData(LONG lUserID, DWORD dwChannel, string sFileName),新版本有些SDK改成了传实时预览句柄,不同版本的HCNetSDK.cs声明不一样。这里按最常见的老接口写:
bool saveOk = NET_DVR_SaveRealData(userId, 1, @"D:\record\20250220_01.h264"); if (!saveOk) { uint err = NET_DVR_GetLastError(); Console.WriteLine($"录像启动失败, 错误码: {err}"); } // 停止录像,必须调用对应接口 bool stopOk = NET_DVR_StopSaveRealData(userId);逻辑说明:NET_DVR_SaveRealData把正在登录通道的实时码流直接写入文件,不需要你先做解码再编码,所以对CPU几乎没有压力。这个文件是原始码流,不是MP4,用VLC能播放,但码流格式取决于设备编码方式,可能是H.264也可能是H.265。存储时会话里如果码流参数发生变化,文件可能会损坏,所以生产系统里建议分段保存,例如每小时一个文件,并在文件头写上当前时段。
参数说明:保存路径一定要用英文全路径,不要带中文和空格,否则有些老版本SDK会保存失败。停止录像时,注意有些SDK版本要求传预览句柄而不是用户ID,写代码前先检查你SDK包里的函数签名,别想当然照抄老代码。这也是为什么很多人从网上复制录像代码后会翻车的原因之一。
4.3 参数配置:OSD叠加与码流调整的边界
很多人拿到摄像头后,想把设备名称、时间戳叠加到画面上,第一反应就是调用SDK的NET_DVR_SetDVRConfig。海康SDK确实提供了OSD设置接口,命令码是NET_DVR_SET_OSD,对应的结构体是NET_DVR_OSDCFG,里面包含字符串叠加位置、时间格式、字符集等几十个字段。这个接口能用,但落地时非常繁琐:结构体字段多,不同固件版本支持的属性还不一样,容易在字节对齐和字符串编码上踩坑。
我的做法是:如果只改OSD文字和时间样式,优先到设备网页端配置,省心省力。如果一定要在程序里动态改OSD,比如把产线工单号实时叠加到画面里,那再走NET_DVR_SetDVRConfig,而且前提是设备固件版本足够新。代码的完整写法要参考你自己SDK包里的HCNetSDK.cs,结构体字段太多,不建议手敲。这里只提醒一句:调用SetDVRConfig之前,一定要先查一下设备是否支持该命令,用NET_DVR_GetDVRConfig先读一次,读能成功再写,否则容易把设备配置搞乱。
5. 用C#调海康SDK的5个高频坑:现象与排查
5.1 DLL加载失败:初始化就报DllNotFoundException
现象:程序一启动,在NET_DVR_Init处抛出DllNotFoundException,或者提示“找不到指定的模块”。
原因:HCNetSDK.dll、PlayCtrl.dll这些是非托管DLL,不会自动复制到生成目录。更隐蔽的是,HCNetSDK.dll依赖hlog.dll、hpr.dll等动态库,主DLL找到了但依赖DLL缺失时,异常信息仍然指向HCNetSDK.dll,导致排查方向跑偏。
解决:把SDK包里所有DLL文件都拷贝到exe的生成目录,不要只考HCNetSDK.dll一个。同时确认平台目标位数和DLL位数一致。检查方法很简单,用Dependency Walker之类的工具看HCNetSDK.dll头部的机器类型,x86和x64一眼就能看出来。
5.2 登录成功但预览黑屏:窗口句柄和码流类型最可疑
现象:NET_DVR_RealPlay_V40返回了正常句柄,但画面区域一直是黑的,没有报错。
原因:最常见的是传入的hPlayWnd无效或控件句柄被重建。WinForm里如果把Panel放在TabPage上,切页时Panel的句柄可能会重建,之前传入的句柄自然失效。另一个原因是通道选的码流类型不支持,比如IPC本身只配了子码流的编码参数,你非要取主码流,返回成功但画面出不来。
解决:把预览控件放到独立窗体,并用一个固定的句柄,不要在运行时动态创建和销毁预览容器。码流类型先切到子码流测试,确认通道和编码没有问题,再切主码流。
5.3 回调里更新UI控件闪退:跨线程操作卡在Invoke上
现象:在RealDataCallback回调里直接修改Label.Text或者进度条,程序要么闪退,要么报“线程间操作无效”。
原因:预览回调运行在SDK的私有线程里,不是UI线程,直接操作控件违反WinForm的线程访问规则。这个坑新手几乎必踩,网上的解决办法也很多,但有人用了Control.Invoke还是崩。
解决:回调里只做数据拷贝,把码流数据放进队列,再用Timer或异步线程把数据In到UI操作。我习惯用BeginInvoke而不是Invoke,前者是异步投递不阻塞SDK回调线程。如果你在回调里做耗时任务,直接Block住SDK线程,画面解码就会卡住或掉帧,这是另一个常见连锁反应。
5.4 程序关闭时进程不退:释放顺序错了
现象:关闭窗体后,程序的进程还在任务管理器里,或者退出时报访问冲突异常。
原因:SDK的预览线程、播放库的渲染线程还在运行,你没有按顺序停止。很多人只做了NET_DVR_Cleanup,但预览句柄还在,播放库的通道还没关闭。
解决:按照严格顺序释放。先NET_DVR_StopRealPlay停止预览,再NET_DVR_Logout注销登录,最后NET_DVR_Cleanup清理SDK。如果用了PlayCtrl.dll,还要加上PlayM4_Stop和PlayM4_Close。这个顺序不要颠倒,否则全卡在最后的Cleanup上。
if (playHandle != -1) { NET_DVR_StopRealPlay(playHandle); playHandle = -1; } if (userId != -1) { NET_DVR_Logout(userId); userId = -1; } NET_DVR_Cleanup();5.5 RTSP能拉流但SDK登录返回10001:设备端账号权限没给够
现象:用VLC的RTSP地址能拉到视频,但程序里SDK登录一直报10001密码错误。
原因:SDK登录和RTSP取流走的是两套权限机制。RTSP拉流只需要取流权限,而SDK登录需要用户账号具备远程操作权限。有时候设备端账号被锁定,或者账号类型不支持API访问,VLC那边不受影响,SDK这边就吃了闭门羹。
解决:到设备网页端,检查用户账号的类型和权限,确保勾选了远程操作相关的权限项。如果是非法登录锁定,先重置或解锁。这个问题的迷惑性在于它表面上和密码相关,但实际和权限模型相关,这也是海康开发里典型的血泪经验。
6. 把Demo升级成能用的工具:显示、压测和封装习惯
6.1 把预览画面嵌到WPF里:HwndHost是正路
WinForm里一个Panel就能解决的预览显示,到了WPF里就不能直接传句柄了。WPF的控件句柄不像WinForm那样随时可用。常见做法是使用WindowsFormsHost包一个WinForm Panel进去,再把Panel.Handle传给SDK,或者用HwndHost自己实现一个句柄宿主。WindowsFormsHost实现快但会有空气刘海式的问题,比如遮挡、焦点问题;HwndHost更底层,性能更好。做新项目时我会优先选HwndHost,把SDK的实时画面作为一个独立HWND嵌入WPF的可视化树。
6.2 验证Demo是否稳:跑一个72小时压力冒烟
摄像头接入最怕的是一天崩三次。我一般会在接入完成后写一个冒烟脚本:循环执行登录、预览、抓图、退出四个动作,每次记录错误码和耗时长日志跑一晚。关键观测点是两个:一是抓图是否周期性地失败,二是退出后进程数和句柄数是否持续增长。如果句柄数一直涨,说明哪里在泄漏,重点检查PlayM4_Close和NET_DVR_StopRealPlay是否每次都执行到位。跑过72小时零失败,我才会把它发布给现场。
6.3 封装一个自己的CameraService:把Demo代码变成业务积木
官方Demo最大的问题是一切写在Main方法里,没法直接复用。我的习惯是新建一个HikCameraService类,把登录、预览、抓图、录像、释放分别封装成方法,内部记录状态机:未初始化、已登录、预览中、已释放。状态机的好处是防止你重复登录或重复释放,这也是很多崩溃的根源。在这个类里把错误码统一转换成中文描述,比如10001映射成“用户名或密码错误”,10003映射成“权限不足”,日常排查看日志就能一眼定位。
最后再提醒一句我之前踩过的最深一坑:不要把SDK的DLL扔进系统目录,不要把平台目标随意改成AnyCPU,不要相信“上次明明能跑”的代码在别人机器上也能跑。海康SDK这事儿,环境问题排第一,代码逻辑只能排第二。我自己每换一次电脑都要花十分钟把DLL和平台目标重新对一遍,这已经是肌肉记忆了。希望帮到你。
本文还有配套的精品资源,点击获取