
简介这是一份面向 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的句柄传给SDKSDK自己解码自己画代码量最少。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里的dwStreamType0就是主码流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和平台目标重新对一遍这已经是肌肉记忆了。希望帮到你。本文还有配套的精品资源点击获取